1. 城市轨道交通安全管理的业务边界真需求长什么样做系统最怕的一件事就是对着一个假需求埋头开发。这个项目如果只停留在“用户表、车站表、隐患表做增删改查”的层面那它就是一个普通的管理后台谈不上“安全管理”。我接到这个命题后的第一反应是先想明白轨道交通场景下的安全管理和通用型的OA系统到底有哪里不一样。先描述一个很真实的场景——某运营公司的安全科每天的工作量大头不在“发现风险”而在“盯住风险”。今天巡检发现了一处车站站台门与屏蔽门之间间隙过大的隐患拍照、登记、上报这只是第一步。接下来这个隐患要派给哪个部门、整改时限是多久、整改完谁来验证、验证不通过怎么退回、月底的周报月报怎么统计。很多公司这一整套靠的是微信群Excel隐患状态全靠人工维护一个项目拖了三个月没人跟进是常有的事。所以这个系统真正要解决的是“责任落实”和“闭环管理”的问题。由此我梳理出三个核心业务边界职责分工清晰每个角色登录系统后看到的应该是自己的待办而不是所有模块的完整菜单。领导只看统计看板和分析报告巡查人员只操作巡检和隐患上报安全科负责审核和指派维修部门只处理分派给自己的工单。闭环是硬约束隐患从发现到销号状态必须环环相扣中间不允许随意跳转。比如“已整改”必须经过“复查验收”才能变为“已销号”不能因为某个部门改了数据库状态就直接跳过验证。可追溯是底线每一次状态变更、每一个审批动作都要留下记录。哪天出了事倒查责任时系统要能回答“这件事谁发现的、谁指派的、谁处理的、谁验证的”。还有一个常被忽视的点是这类系统的一线用户往往没有太多计算机操作经验。很多巡查人员年纪偏大你让他打开电脑去填一个二十个字段的表单他宁可继续拍照发微信群。所以系统设计上必须做减法——核心录入页面应该尽可能少移动端的优先级甚至比PC端更高。这个认知会在后面反复影响技术选型和功能设计。2. 技术栈与数据模型为什么这样搭最省事2.1 选型逻辑单体优先别上来就整微服务城市轨道交通安全管理系统的并发量不会高一个运营公司的内部系统同时在线几十人已经不少了。它的复杂度完全在业务逻辑上不在流量上。所以架构上我倾向于Spring Boot单体应用 前后端分离守好Modular Monolith的边界就够了。我选型时定了这么一套组合层次选型理由后端框架Spring Boot 2.7.x稳定、生态成熟、招人容易持久层MyBatis-Plus单表CRUD省事复杂查询用XML手写SQL数据库MySQL 8.0完全够用部署运维成本低缓存与瞬时消息Redis用户会话、待办计数、应急预警推送权限框架Sa-Token 或 Spring Security更推荐前者API简单上手成本低前端Vue 3 Element Plus中后台首选表格表单组件成熟接口文档SpringDoc OpenAPI前后端联调效率最大化服务端如果要拆分我认为最多是把“报表统计”部分拆出去做一个异步任务模块用定时任务晚上算好结果。其他业务像隐患流转、巡检任务、应急管理耦合度其实很高硬拆微服务只会给自己找麻烦。另外一个细节对象存储。隐患上报必然要传现场照片少则一张多则五六张。不建议直接往数据库里塞Base64也不建议存到应用服务器本地磁盘。我的做法是抽象一个FileStorage接口本地磁盘和MinIO各实现一套部署时用配置切换。项目早期用本地存储正式上线切MinIO代码不用改。2.2 数据模型设计把“隐患”当成系统的主角这套系统的数据模型我反复调整过三轮最终的核心思路可以概括为一句话一切业务围绕隐患Hazard展开巡检任务和应急预案都是隐患的触发源或处理手段。最核心的几张表sys_user用户表包含部门ID、岗位类型、手机号。岗位类型决定了默认的角色权限比如巡查员、安全员、部门负责人、系统管理员。base_station / base_line车站和线路的基础数据。车站表的关键字段是所属线路、站区编码、经纬度坐标经纬度后面给巡检定位用。hazard_record隐患主表。字段包括隐患编号、标题、描述、等级一般/较大/重大、发现方式人工巡检/设备报警/乘客投诉、发现人、发现时间、所在车站、所在线路、现场照片附件ID、当前状态、责任部门、整改时限。hazard_status_log隐患状态流转历史表。每一次状态变更都插入一条记录包含操作人、操作时间、动作、意见。这张表不能省它是“可追溯”落到数据库层面的体现。check_task / check_record巡检任务表和巡检记录表。任务表记录计划安排记录表记录实际执行结果支持“正常/异常”两种结论。一次巡检可以关联生成多条隐患记录。我要特别说一个设计小技巧等级和状态字段在数据库里存数字编码但代码里要映射成枚举且枚举是显式状态机不是随便一个数字字段。状态机的设计我放在下一节细说。2.3 接口设计URL是给人看的REST约束要带业务语义接口路径设计我用了一组比较直观的约定POST /api/v1/hazards # 上报隐患 PUT /api/v1/hazards/{id}/assign # 指派责任部门 PUT /api/v1/hazards/{id}/process # 整改处理提交整改结果 PUT /api/v1/hazards/{id}/verify # 复查验收 GET /api/v1/hazards/my-todo # 我的待办列表 GET /api/v1/hazards/report/monthly # 月度报告数据关键动作都用明确的动词子资源不用一个含糊的PUT /hazards/{id}去承载所有变更。因为隐患的操作类型很多——指派、处理、退回、验收、销号——如果共用一个更新接口后台Service里必然长出一大堆if else判断状态一多就会失控。这里还有一处别忘记所有写操作都要防重复提交。巡查人员在现场网络不好的时候经常会连点两下提交按钮。我在写接口时统一做了处理前端按钮loading置灰后端接口做一个简单幂等校验——通过Redis记录每个请求的唯一流水号重复请求直接返回上一次的处理结果。3. 核心业务实现隐患状态机和巡检防作弊3.1 隐患状态机用显式约束替代自由流转这部分是这个系统能不能真正用起来的关键。隐患的状态流转如果设计不好轻则数据乱掉重则整个闭环形同虚设。我定义的状态流是这样的待审核 - 待指派 - 整改中 - 待验收 - 已销号 └---- 退回整改从整改中重新开始 待审核 - 驳回 审核不通过需补充信息代码实现上我推荐用一种比较简单且直观的方式在每个状态处理方法开头做一个状态迁移校验校验不通过直接抛业务异常。不需要引入复杂的状态机框架靠枚举加断言就够了。public enum HazardState { PENDING_REVIEW, // 待审核 PENDING_ASSIGN, // 待指派 PROCESSING, // 整改中 PENDING_VERIFY, // 待验收 REJECTED, // 已驳回 CLOSED; // 已销号 private static final MapHazardState, SetHazardState TRANSITIONS new EnumMap(HazardState.class); static { TRANSITIONS.put(PENDING_REVIEW, EnumSet.of(PENDING_ASSIGN, REJECTED)); TRANSITIONS.put(PENDING_ASSIGN, EnumSet.of(PROCESSING)); TRANSITIONS.put(PROCESSING, EnumSet.of(PENDING_VERIFY, PROCESSING)); TRANSITIONS.put(PENDING_VERIFY, EnumSet.of(CLOSED, PROCESSING)); TRANSITIONS.put(REJECTED, EnumSet.of(PENDING_REVIEW)); } public boolean canTransferTo(HazardState target) { return TRANSITIONS.get(this).contains(target); } }这里有个细节容易被忽略“退回整改”其实没有让状态跳回PROCESSING之外的地方但退回理由和整改意见必须记录在流转日志里。只有记录状态历史是不够的整改退回的整改要求文本、验证不通过的驳回理由都要一并存到hazard_status_log表。这样的状态约束从技术上讲挡住了一种非常现实的情况——各角色手里有后台修改权限某个部门负责人为了把数据做得好看在整改根本没完成时就把状态改成待验收。操作会被规则挡下来。3.2 巡检任务位置可信度与防代打卡设计巡检打卡功能看起来只是记录“人到了、看过了”但实际操作中发现的问题非常多。最典型的是代打卡——几个人商量好一个人到站把所有人的打卡都做了。我的解决方案不是一味加强硬件投入而是从软件层面设置两道校验LBS粗校验基于定位判断提交人员当前位置与目标车站的距离超过200米直接拒绝打卡。NFC/二维码复验每个车站巡检点部署一个固定的二维码或者NFC标签系统统一生成巡检人员可拍照后扫码提交打卡时必须先扫码后定位两道信息缺一不可。在实际项目中NFC方案推广起来有一定阻力——需要在各车站布点需要反复给一线人员做培训。所以第一个版本里我留的是二维码方案系统后台根据车站ID生成带随机盐值的二维码打印后贴到设备附近手机扫码获得token结合位置信息提交后端校验。public CheckResult validateLocation(BigDecimal lat, BigDecimal lng, Long stationId) { Station station stationMapper.selectById(stationId); double distance GeoUtil.distance(lat, lng, station.getLatitude(), station.getLongitude()); if (distance MAX_CHECK_IN_DISTANCE_METERS) { return CheckResult.fail(当前位置与巡检目标距离过远无法打卡); } return CheckResult.success(); }这里要留意一个边界问题地铁车站大多在地下GPS信号常常不准确甚至偏差出几百米。如果严格按照200米限制地下站的巡检任务大概率打不了卡。我后来的调整是实现一个巡检点签到码API巡检人员到达车站后通过扫描现场张贴的二维码签到。扫码签到的信息比GPS更可靠现场执行起来也快捷实测运行很稳。GPS校验作为后台参考项扫码方式可以大幅减少一线人员的操作阻力。3.3 应急预警短时效消息如何保证触达城市轨道交通的特殊性在于一旦发生突发事件系统本身不能成为信息传递的瓶颈。应急预警这部分我用了“Redis发布订阅 WebSocket推送 短信兜底”三层触达机制。运营人员触发应急预警后后端会做三件事写数据库记录预警信息状态为“待确认”通过WebSocket向所有在线的应急小组成员推送提醒更改前端页面顶部出现红色横幅调用短信服务给未在线成员发送模板短信短信内容必须包含“确认按钮”对应的链接。这里我踩过一个坑最开始WebSocket推送用的单机推送没有考虑部署多实例的问题。后来系统计划部署两台服务器做负载均衡后连接分散在不同实例上一条预警只推送到了其中一台服务器上的连接另一台服务器的用户完全没收到。后来改成借助Redis的Pub/Sub做广播——应用实例订阅同一个频道收到预警消息后调用各自本地WebSocket推给自己的在线连接这样多实例下也不会漏推。实现了“未确认自动升级”机制预警发出后20秒内没人确认系统自动电话等方式通知值班调度员这个功能在培训演练时价值很大。4. 报表统计与权限细节让领导爱看、让一线少填4.1 统计看板不要在大屏上做实时全表扫描管理层最关心的几个指标永远是本月新增隐患数量、按时整改率、超期未整改数量、重大隐患数量以及各条线路的对比。这些数字如果每次打开页面都实时从hazard表里count表数据量过万时MySQL也能扛但一旦超过几十万条还带多条件join页面响应就会明显变慢。我的处理方案是双轨制当日和实时数据走Redis的计数器比如“今日新增隐患数”每次新增用incr命令加一页面直接读缓存历史汇总数据每天凌晨用定时任务跑统计数据落入汇总表日报、月报都从汇总表查。这个方案的优点是查询响应时间可控。注意一个细节定时任务跑批时要按照“业务日期”统计而不是跑批那天的自然日期。因为凌晨0点到跑批开始之间发生的业务如果按自然日处理很容易被算到前一天或后一天周报月报对不上账。准备好一个汇总表里面直接放年、月、线路ID、隐患等级、数量等分组维度的数字。4.2 权限模型越细越好还是越简单越好给运营公司设计权限时最容易犯的错是把权限粒度做得特别细比如“可以查看但不可以编辑”“只可以看某条线路的数据”然后每个页面都要做一行行权限判断开发效率被拖垮。我的经验是分两层角色做粗粒度划分系统管理员、安全管理人员、部门负责人、巡查人员、领导。每个角色绑定一套菜单权限和按钮权限比如“导出报表”按钮只有安全科和领导可见。数据范围做细粒度控制用数据权限注解实现比如巡查员只能看自己负责的线路或者片区部门负责人只能看本部门相关工单。这一层通过自定义拦截器SQL拼接实现不侵入业务代码。权限这块我踩过较大的坑是登录逻辑。一开始用的方案是JWT无状态token结果当安全管理员把某个人移除系统时他手上的token在过期前依然有效。换成Sa-Token的登录态机制后就方便了踢人下线、顶号登录都是现成API省了很多事。4.3 一线录入表格能少一个字段就少一个这个系统最容易被吐槽的地方往往不是性能而是录入页面让一线人员嫌烦。我的原则是隐藏能自动带出的所有东西。隐患上报表单只需要填四样——所在车站下拉选择、隐患描述拍照自动带出时间地点、照片上传、等级默认一般重大需复核。其余字段全部后端自动补齐上报人从登录态取、上报时间取当前时间、所在线路根据车站档案自动关联。还有一个实用技巧Excel批量导入必须支持。我没做复杂的批量导入界面只实现了标准模板的导入模板第一行由系统导出含字段说明和枚举值提示。导入后出个结果文件标注哪一行格式有误。这个功能让安全科整理历史遗留台账时省了非常多的工作量。5. 上线落地后的复盘如果重新做一次会改哪里系统开发完成试点上线运行了大概两个月撤回了不少设计也验证了不少设计。如果让我重新做一次我会在三个地方做调整。第一移动端优先的级别要更高。前面说过一线巡查人员根本不开电脑因此网页版开发得再好使用率还是上不去。建议技术方案直接定为H5应用部署在现有办公App内因为地铁运营公司一般不允许员工手机随意安装外部应用。H5页面发布不需要走应用商店审核流程流程和界面都更适合移动办公。第二应急预案模块应该从“静态文档上传”改成“结构化流程配置”。最好的实现方式是让运行人员在后台以步骤式表单配置处置流程比如每步的负责人、期望处置时长、超时升级对象然后由流程引擎去驱动。这个改造工程量比较大但带来的价值非常直接应急人员可以在关键步骤上获得系统协助。第三数据对接要提前规划。轨道交通安全管理的很多隐患其实来自设备本身的报警比如闸机故障、信号系统异常。如果系统能对接车站级的设备监控接口把设备报警自动转化为隐患记录比人工发现要早好几个小时。我当时由于对接对象的系统接口文档不够明确只能先做人工上报拓展对接后自动化水平会明显提升。这个项目做下来我最大的体会是搞企业内部系统难点从来不在框架和中间件上而在于你能否真正理解使用者的工作方式。技术选型上不追求新、不追求复杂而是选择最稳妥、最容易维护的方案业务处理上不贪多、不冒进而是把最核心的闭环做到极致。把隐患管理这件事想透彻了系统自然就立住了。最后分享一个上线时的小技巧系统正式运行的头一个月千万别急着关掉Excel台账的入口。给安全科留一个“导出当前查询结果”的按钮让他们拿系统数据回Excel也好做自己的归档也好有一个过渡适应期。很多人不信任新系统但当你导出的Excel和他们的模板对得上时信任就建立起来了。这个操作看起来和“信息化”背道而驰实际效果却比任何培训都好。