多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于SpringBoot的医疗设备维护平台:从工单到保养的工程实践

基于SpringBoot的医疗设备维护平台:从工单到保养的工程实践 设备科的微信群从早上七点就开始响ICU的呼吸机报错误3号楼的注射泵需要校准检验科的离心机又转不动了。在这套系统之前整个过程就是临床科室打电话报修、设备科手写工单、中午才能把单子送到工程师手里修完再补一张纸质记录。备件库的账永远对不上哪些设备到了校准周期全靠工程师脑子记。这些混乱叠加在一起就是我要做基于SpringBoot的医疗设备维护平台的根本原因。这篇文章不是教科书式的需求分析而是我从零设计、开发到上线这套系统的完整记录。内容涵盖数据模型设计、工单全流程流转、定时保养任务、权限与操作审计、Vue前端整合SpringBoot的部署方式最后还有一批真实踩坑记录。如果你正好在用SpringBoot做业务管理系统或者打算进入医疗信息化这个方向这篇文章能让你少走不少弯路。1. 医疗设备维护的痛点与SpringBoot的适配性分析1.1 设备科日常救火式管理下被忽视的三件事我调研设备科工作流程的时候发现一个规律大家忙到飞起但问题永远在原地打转。大部分精力都花在救火上——设备坏了赶紧修修完就完了没人关心这台设备这个月坏了几次、平均修复时长是多少、哪个品牌故障率偏高。这种模式长期运行下去会出现三个容易被忽视的缺口第一是设备档案不成体系。医疗设备从采购到报废要经历十几年中间有安装记录、校准记录、维修记录、配件更换记录。纸质单据一旦丢失等于这台设备的履历断档。第二是报修工单不闭环。临床科室报修后维修结果有没有反馈、科室认不认可、备件用了什么完全靠工程师自觉。第三是预防性维护缺失。很多设备故障其实是可以靠定期保养避免的但保养计划往往停留在Excel表格里过期了也没人提醒。做一个维护平台本质上是把这三块补上让每一台设备有完整档案让每一次报修有闭环记录让保养计划自动触发并留痕。这个认知决定了系统最核心的三个模块——设备管理、工单管理、保养计划而不是一上来就堆砌花哨功能。1.2 技术选型为什么SpringBoot是我最后的决定我见过不少同行一上手就想上微服务、用Kafka、搞分布式事务。但医疗设备维护平台的实际场景是医院内部部署用户量是几百人设备量是几千台数据量在百万级别。这个规模下微服务的复杂度会直接拖垮维护效率反而单体应用最合适。选SpringBoot有几个明确理由。第一生态成熟。认证授权用Spring SecurityORM用MyBatis-Plus定时任务用Spring Task消息推送用WebSocket或者对接钉钉/企业微信机器人这些组件都有完整文档出了问题也容易搜到答案。第二部署简单。打出一个可执行jar包就能跑医院内网服务器条件普遍一般一个jar包加一个MySQL实例是最省心的组合。第三社区活跃度够高。基于SpringBoot的开源后台管理系统模板很多虽然我最终没有直接套模板但参考价值很大。技术选型这事我的原则是够用就好不追新。医疗行业对系统稳定性极其敏感医院信息科更看重可控可维护而不是技术多新潮。SpringBoot 2.7.x配JDK8在这个场景下比SpringBoot 3.x配JDK17更稳妥具体原因后面踩坑部分细说。1.3 可扩展的工程结构模块分包与统一响应体项目结构我采用了按业务模块分包的方式而不是经典的按技术层分包。一个设备模块里同时放Controller、Service、Mapper改动设备相关功能时只需要进入这一个包不用频繁切换目录。com.hospital.emms ├── common // 统一返回体、异常、常量、工具类 ├── config // 全局配置类 ├── controller // 接口层 ├── service // 业务层 ├── mapper // MyBatis-Plus数据访问层 ├── entity // 数据库实体 ├── dto // 入参校验对象 ├── vo // 出参封装对象 └── module ├── equipment // 设备档案模块 ├── workorder // 工单模块 ├── maintenance // 保养计划模块 ├── parts // 备件管理模块 ├── user // 用户权限模块 └── report // 统计报表模块所有接口返回统一使用R 对象包含code、message和data三个字段。前端请求拦截器里只要判断code是否为200就能决定是否弹出错误信息省去一大堆重复判断。2. 核心数据模型让设备、工单、保养计划互相咬合2.1 设备档案表固定资产编码是唯一主键设备档案是整个平台的数据基石。医疗设备有个特点固定资产编号和设备序列号是两套体系。设备序列号是厂家出厂时的全球唯一标识而固定资产编号是医院资产管理部门自己编的用于财务入账和实物盘点。系统设计时必须用固定资产编号作为主键维度因为医院内部所有流程——领用、报修、转移、报废——都以这个编号为准。CREATE TABLE device_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, device_code VARCHAR(32) NOT NULL COMMENT 固定资产编号, device_name VARCHAR(128) NOT NULL COMMENT 设备名称, model VARCHAR(64) COMMENT 型号, device_type VARCHAR(32) COMMENT 设备分类, department_id BIGINT COMMENT 所在科室, supplier VARCHAR(128) COMMENT 供应商, install_date DATE COMMENT 安装日期, warranty_end DATE COMMENT 保修截止日期, status TINYINT COMMENT 0-停机 1-正常 2-维修中 3-报废, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_device_code (device_code) ) COMMENT设备档案表;这里面有两点要提醒。一是device_type不要直接用字符串建议做一张设备分类字典表因为医院设备分类如生命体征监测设备医学影像设备急救设备是树形结构用字典表方便后续按分类统计故障率。二是status字段务必用数字枚举不要用字符串否则后续写统计SQL时会非常痛苦。2.2 工单状态机一次报修要经历什么工单是平台的流转核心状态不能随意跳转必须要有一个状态机来约束。我把工单定义为六个状态待派单、待处理、处理中、待验收、已完成、已关闭。每个状态之间的跳转有明确的操作条件和角色要求见下表。状态操作人动作触发条件待派单设备科管理员受理报修用户提单成功待派单设备科管理员转外修内部无法处理待处理维修工程师接单管理员派单或工程师抢单处理中维修工程师开始维修工程师到场操作待验收报修科室/管理员验收工程师提交维修结果已完成系统关闭工单验收通过已关闭设备科管理员关闭报修取消或设备报废状态机的好处在于第一避免工程师跳过处理中直接提交完成保证流程可溯源第二系统可以基于状态自动计算每个环节的耗时比如待派单阶段平均耗时和响应时效这些指标是设备科月度考核的重要依据。2.3 保养计划与备件台账防止该保养没保养、该换件没换件保养计划表是预防性维护的数据源记录了每台设备的保养周期和上次保养时间。关键字段包括设备ID、保养类型日保/周保/月保/季保/年保、上次保养日期、下次保养日期、负责人。系统每天定时扫描下次保养日期小于等于今天的记录自动生成保养工单。备件管理表分为两层备件主表维护备件编码、名称、规格和供应商备件库存表维护当前库存和安全库存阈值。这两个表分开设计的原因是同一备件可能放在多个库房主表在业务上更清晰。这里的核心规则是备件出库必须关联工单ID。维修工单提交时如果涉及更换备件系统自动扣减库存并生成出库记录这样哪台设备在什么时候换过什么配件就完全可追溯。这个设计在实际运维中非常重要因为医疗设备检测和审计时经常需要这类数据。3. 工单全流程实现从提单到验收的事务与并发控制3.1 重复提单的防重逻辑实际运行中第一个遇到的问题就是重复提单。临床科室的设备管理员在系统里填完报修单后因为网络慢或者操作不熟练经常会点两次提交按钮。如果没有防重逻辑一个故障会生成两张甚至三张工单设备科就得花时间合并挺闹心的。我的处理方案是双重防重。前端在提交按钮上做loading禁用这个不细说。后端在Service层做时间段校验同一台设备在五分钟内如果已存在待处理状态的工单直接拒绝再次提交。Override Transactional(rollbackFor Exception.class) public Long createWorkOrder(WorkOrderCreateDTO dto) { DeviceInfo device deviceMapper.selectById(dto.getDeviceId()); if (device null || device.getStatus() DEVICE_STATUS_SCRAPPED) { throw new BizException(设备不存在或已报废); } LocalDateTime now LocalDateTime.now(); Integer count workOrderMapper.countByDeviceAndTime( dto.getDeviceId(), now.minusMinutes(5), WorkOrderStatus.PENDING_DISPATCH.getValue()); if (count 0) { throw new BizException(该设备近期已存在待处理工单请勿重复提交); } WorkOrder order new WorkOrder(); BeanUtils.copyProperties(dto, order); order.setOrderNo(generateOrderNo()); order.setStatus(WorkOrderStatus.PENDING_DISPATCH.getValue()); workOrderMapper.insert(order); return order.getId(); }这个方案没有用分布式锁因为单机部署下时间窗口校验已经足够。但要注意这里的数据库查询必须走设备ID状态组合索引否则工单量涨上去之后查询会变慢。3.2 派单策略自动匹配技能与负载均衡派单功能我做了两种模式并行。第一种是管理员手动派单直接在工单详情页选择工程师。第二种是自动派单系统根据两个维度推荐最合适的工程师一是工程师的技能标签比如某工程师擅长呼吸机、麻醉机设备类型匹配度高的优先二是工程师当前待处理工单数量排单越少的越优先兼顾负载均衡。自动派单的具体实现并不复杂本质上是一次带排序条件的查询。在维护工程师表时增加一行skill_tags字段用逗号分隔的字符串存储技能标签然后使用MySQL的FIND_IN_SET函数做匹配筛选加上当前待处理数量字段做二次排序。public ListEngineerWorkloadVO recommendEngineers(Long deviceId) { DeviceInfo device deviceMapper.selectById(deviceId); return engineerMapper.selectEngineerByDeviceType( device.getDeviceType(), WorkOrderStatus.PENDING.getValue()); }我实际运行后认为必须保留手动派单。自动派单只是减少管理员的重复工作但医院场景总是有特殊情况某工程师今天排休、某科室指定要某个老师傅、有些外包维修必须指派给供应商。这些特殊规则永远存在自动派单做不了系统必须有兜底。3.3 备件出库的行锁细节备件出库是最容易出现并发问题的地方。两个工程师同时提交工单都需要从备件库领取同一个型号的配件这时候如果只是先查库存再更新库存大概率会出现超扣现象。库存为负对医疗设备维护来说是不可接受的因为备件台账要和财务对账。解决方案很简单使用MySQL的SELECT ... FOR UPDATE行锁出库和生成出库记录放在同一个事务里。Transactional(rollbackFor Exception.class) public void outbound(PartOutboundDTO dto) { PartStock stock partStockMapper.selectByPartIdForUpdate(dto.getPartId()); if (stock.getStock() dto.getCount()) { throw new BizException(备件库存不足); } stock.setStock(stock.getStock() - dto.getCount()); partStockMapper.updateById(stock); PartRecord record new PartRecord(); record.setPartId(dto.getPartId()); record.setWorkOrderId(dto.getWorkOrderId()); record.setCount(dto.getCount()); record.setType(PartRecordType.OUT); partRecordMapper.insert(record); }必须提醒的是行锁只有放在事务里才生效。如果你在方法上漏掉Transactional注解锁会在SQL执行完立即释放并发问题依然存在。另外selectByPartIdForUpdate这个方法不能走MyBatis-Plus的通用查询必须在Mapper XML里手写SELECT * FROM part_stock WHERE part_id #{partId} FOR UPDATE。3.4 验收闭环与维修记录生成工程师维修完成后需要填写处理内容、更换备件明细、维修结论已修复/待观察/无法修复需要外修然后提交进入待验收状态。此时系统会给报修科室发送通知科室确认验收后工单才能真正关闭。验收环节我增加了一个强制性科室验收时必须在评价中勾选设备运行是否正常这个看似简单的设计其实很有用。一是让报修科室有参与感二是后续统计维修后复发率时可以直接通过这个评价字段筛数据。如果只做流程闭环而忽略评价数据系统的分析价值会打很大折扣。工单关闭后系统会自动向设备档案表追加一条维修记录。这个记录不需要单独建表直接写在工单表里即可需要查看设备履历时按device_id查工单列表就行。这里要注意查询时最好带上状态条件避免把待派单、已关闭这些没有实际维修动作的记录也展示出来。4. 自动保养提醒定时任务与消息通知的工程化4.1 Scheduled的默认坑单线程与多实例重复执行Spring Task的Scheduled注解用起来很简单但深入使用就会发现两个隐蔽的坑。第一个坑是默认单线程执行。Spring的定时任务默认使用单线程调度器如果定义了多个Scheduled任务它们会排队执行。某个任务阻塞了后续任务全部被卡住。我实际遇到一次凌晨的保养任务生成逻辑里调用了一个外部接口接口超时了30秒结果当天上午的数据统计定时任务延迟执行后台数据滞后了整整一个上午。解决办法是显式配置调度线程池。Configuration public class ScheduleConfig implements SchedulingConfigurer { Override public void configureTasks(ScheduledTaskRegistrar taskRegistrar) { taskRegistrar.setScheduler(Executors.newScheduledThreadPool(5)); } }第二个坑是多实例部署重复执行。如果系统部署了两台实例所有Scheduled任务会在两台机器上同时执行结果就是同一批保养工单生成两遍、通知短信发两遍。这个问题的解决方案放到第七章踩坑记录里详细说因为我是上线之后才踩到的。4.2 保养任务生成幂等与去重保养工单的生成逻辑核心是扫描maintenance_plan表里下次保养日期小于等于今天的过期计划然后为每个计划生成一张保养工单。但这里有一个前提同一条保养计划不能被重复转换成工单。我的做法是去重判断加状态转换两步走。查询保养计划时只查状态为正常的计划生成工单后立刻把计划状态改成已生成待执行这样即使同一个计划被并发扫描到两次第二次查询也查不到这条记录。Component public class MaintenanceScheduler { Scheduled(cron 0 30 2 * * ?) public void generateMaintenanceOrders() { ListMaintenancePlan plans maintenancePlanMapper.selectExpiredPlans(LocalDate.now()); for (MaintenancePlan plan : plans) { Boolean updated maintenancePlanMapper.compareAndSetStatus( plan.getId(), MaintenancePlanStatus.NORMAL.getValue(), MaintenancePlanStatus.GENERATED.getValue()); if (!updated) { // 另一线程已处理跳过 continue; } createMaintenanceOrder(plan); } } }这里用了一个compareAndSetStatus操作本质上是数据库中乐观锁的变种。UPDATE语句里带上WHERE status NORMAL条件如果更新影响行数为0说明这条记录已被其他线程处理过直接跳过。这样即使定时任务被手动触发两次也不会产生重复保养工单。4.3 消息通知渠道的取舍系统上线初期我同时做了站内信和短信通知。结果发现短信费用太高而且用于内部系统有点浪费。后来调整成站内信保留但把通知优先级改为钉钉群机器人Webhook为主。设备科和工程师基本都在钉钉工作群里机器人推送一条您有一张新的待处理工单编号W20250612001的信息比短信更及时而且完全不花钱。技术实现其实很简单就是调用钉钉开放平台的Webhook地址POST一个JSON。SpringBoot里封装一个DingTalkNotifier组件全局共用。Component public class DingTalkNotifier { Value(${dingtalk.webhook}) private String webhook; public void sendMaintenanceRemind(String content) { MapString, Object body new HashMap(); body.put(msgtype, text); MapString, String text new HashMap(); text.put(content, content); body.put(text, text); restTemplate.postForEntity(webhook, body, String.class); } }这里要提醒的是Webhook机器人有频率限制高峰时段一次推送太多消息会被限流。所以批量生成保养工单时不要每张工单单独推一条而是汇总成一两条摘要消息推送。比如今日共生成保养工单15张其中危急值设备2台请注意处理。5. 权限控制与操作留痕医疗合规要求的落地5.1 RBAC角色权限设计医疗设备维护平台虽然用户量不大但权限模型绝不能省。因为不同角色的操作边界非常清晰临床科室只能提单和验收工程师只能处理分配给自己的工单设备科管理员拥有全部管理权限院领导可以看到所有统计报表。我实现了标准的RBAC模型五张核心表用户表、角色表、菜单权限表、用户角色关联表、角色菜单关联表。基于Spring Security的PreAuthorize注解做方法级权限控制比如只有拥有workorder:assign权限的角色才能调用派单接口。角色划分这里有一个经验不要把权限粒度设计得太细。我最初设计了增删改查每个接口对应一个权限标识结果配置起来非常繁琐管理员配置角色时也一头雾水。后来简化为设备管理、工单管理、保养计划、备件管理、统计报表、系统设置六大模块每个模块对应查看和操作两种权限配置成本降了一半实际使用完全够用。5.2 Spring Security集成思路集成Spring Security时我没有直接用默认的登录方式和Session机制而是选择了JWT无状态认证配合Spring Security的过滤器链。后端提供/login接口校验账号密码后签发JWT令牌前端在请求头里携带Authorization: Bearer 。核心配置类里需要重写三个关键部分configure(HttpSecurity http)定义哪些接口放行、哪些需要认证configure(AuthenticationManagerBuilder auth)配置自定义UserDetailsService查询用户增加JwtAuthenticationFilter作为认证过滤器。这里要说一个坑JWT的密钥和过期时间必须放在配置文件里不要硬编码。我项目迭代过程中改过一次密钥当时为了省事写死在Filter里结果改配置还要重新编译打包。另外JWT过期后用户会被强制退出这个提示信息要友好前端在收到401时自动跳转登录页。5.3 AOP操作日志与设备变更记录操作日志是医疗合规要求的硬性指标。设备科复查一台设备半年内的维修履历时不能只看工单还要知道这台设备的基础信息什么时候被改过、谁改的、改成了什么。这个需求用AOP切面实现最合理。Aspect Component public class OperationLogAspect { Around(annotation(operationLog)) public Object around(ProceedingJoinPoint joinPoint, OperationLog operationLog) throws Throwable { long startTime System.currentTimeMillis(); Object result; try { result joinPoint.proceed(); } catch (Exception e) { saveLog(joinPoint, operationLog, 失败, e.getMessage()); throw e; } saveLog(joinPoint, operationLog, 成功, 耗时 (System.currentTimeMillis() - startTime) ms); return result; } }这个切面的核心价值不在于记录某人访问了某个接口而在于配合后台操作日志查询页面让设备科主任可以在五分钟内查清任何一次设备状态变更的前因后果。操作日志表不需要存太多冗余字段操作人、操作模块、操作类型、请求参数、执行结果、耗费时间六个字段足够。设备关键信息的变更记录我单独做了一张device_log表通过MyBatis-Plus的字段自动填充功能记录创建人和创建时间再在Service层统一写入变更日志。这样设备档案的每一次修改都留痕不会出现设备状态莫名从正常变成维修中却找不到操作记录的情况。6. Vue前端打包放进SpringBoot的部署实践6.1 为什么选择前后端合并部署很多项目团队采用前后端完全分离前端Vue项目部署在Nginx后端SpringBoot单独部署。但我这次选择了一个更保守的方案——前端构建产物直接放进SpringBoot的static目录整个系统最终只有一个可执行jar包。原因很简单医院内网的运维环境有限。很多医院机房没有标准的Nginx环境信息科人员对Linux操作也不够熟练我如果交付一个需要部署两个节点的系统后续排除故障时所有排查动作都要加倍。合并部署后信息科只需要执行java -jar emms.jar就能启动系统数据升级、服务重启都只需操作一个进程运维负担小非常多。6.2 dist目录集成与history路由刷新404处理前端Vue项目开发完成后执行npm run build生成dist目录把dist里的所有文件复制到SpringBoot的src/main/resources/static目录下。默认情况下SpringBoot会把static目录映射为根路径/访问http://ip:8080/时自动返回index.html。但是前端路由如果用了history模式即URL里没有#号刷新页面时会直接请求后端接口路径比如刷新http://ip:8080/device/listSpringBoot找不到对应的Controller就会返回404。解决这个问题需要在后端做一个SpaForwardController把前端路由下的非API路径统一forward到index.html。Controller public class SpaForwardController { GetMapping(value {/, /{path:[^\\.]*}, /**/{path:[^\\.]*}}) public String forward() { return forward:/index.html; } }这个方案的关键在于正则表达式[^\\.]*意思是路径中不包含点号。这样既能匹配前端路由又不会拦截静态资源文件CSS、JS都带点号和带点号的API路径。我最初没加这个判断结果把所有静态资源请求全部转发了页面样式全部丢失调试了好一阵子才发现是这个正则没写对。6.3 内网部署的依赖与版本对齐前端整合SpringBoot部署还有一个容易被忽略的问题接口请求路径要统一加/api前缀并在SpringBoot配置一个server.servlet.context-path/api否则前端请求和后端Controller路径可能产生冲突。另一个实际工作中遇到的问题是关于前端构建配置的。Vue项目使用Vite构建时需要把base设置为./如果保持默认的绝对路径打包后的资源引用可能会出现找不到静态资源的错误。这个配置在vite.config.js里export default defineConfig({ base: ./, // 其他配置 })采用相对路径之后即使把系统部署到某个子路径下比如http://ip:8080/emms/前端资源也能正常加载。7. 上线后的踩坑记录与调优建议7.1 SpringBoot版本别追新JDK版本是硬约束我最初用的是SpringBoot 3.2 JDK17开发阶段一切正常但到了部署阶段发现医院内网服务器装的是JDK8。SpringBoot 3.x最低要求JDK17这意味着要么让信息科升级JDK要么我回退版本重写一遍代码。考虑到医院系统软件迭代的谨慎性让信息科升JDK并不现实最终只能回退到SpringBoot 2.7.18 JDK8把代码里用到的新语法全部改回旧版。这是我的第一个教训技术选型的第一步不是看框架有多新而是确认目标环境的JDK版本。医疗行业的老旧服务器远比想象中多JDK8仍然是最保守可靠的选择。慎重起见我建议如果目标环境未知优先选择SpringBoot 2.7.x这是最后一个兼容JDK8的大版本而且还在社区支持周期内。7.2 MyBatis-Plus分页插件与自定义SQL的冲突统计报表模块上线初期我发现分页查询的total数量经常错误的翻倍排查定位后发现是MyBatis-Plus分页插件和自定义JOIN查询冲突了。MyBatis-Plus的Page分页插件自动在SQL尾部拼接LIMIT语句但如果使用自定义SQL且包含JOIN操作插件在统计总数时无法正确解析COUNT语句中的子查询导致总数虚高。解决方法是针对自定义SQL在Mapper XML里手写完整的COUNT语句确保总数查询与分页数据查询完全独立。更简单的办法是把分页查询拆成两步第一步查符合条件的ID列表第二步按ID列表查详细数据虽然多了一次查询但逻辑上更可控。7.3 慢查询索引优化实测工单量积累到几万条后设备维修报表的查询开始变慢。设备科想要按科室和月份统计维修次数和平均修复时长我最初用了一个比较复杂的SQL查询全表扫描耗时三秒以上。这种报表页面每次打开都转圈用户体验很差。优化方式是加组合索引。工单表原本只有id主键我在department_id和create_time两个字段上建立了联合索引查询瞬间降到200毫秒以内。这个案例告诉我们对于报表类查询不要一开始就想着用缓存先把数据库索引设计好效果立竿见影而且零代码修改。7.4 Redis分布式锁解决双实例重复生成保养工单上线约一个月后系统部署方式升级为双实例集群。第二天早上设备科反馈保养工单全部翻了一倍工程师收到了一大堆重复任务。原因正如前面提到的两台实例同时执行Scheduled任务各自扫描到相同的过期保养计划各自生成了相同工单。解决这个问题我引入了Redis分布式锁。生成保养任务的定时方法在开始时先尝试获取锁只有拿到锁的实例才能继续执行另一台实例直接跳过本次任务。Component public class MaintenanceScheduler { Autowired private StringRedisTemplate redisTemplate; Scheduled(cron 0 30 2 * * ?) public void generateMaintenanceOrders() { String lockKey emms:maintenance:generate; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, Duration.ofMinutes(10)); if (!Boolean.TRUE.equals(locked)) { log.info(其他实例正在执行保养任务生成本次跳过); return; } try { // 原有生成逻辑 } finally { redisTemplate.delete(lockKey); } } }这个方案已经稳定运行了一段时间。要注意的是锁的过期时间要设置得足够长必须保证业务逻辑能在锁过期之前执行完成否则可能出现两个实例同时执行的极端情况。我设置10分钟单次生成几百张工单其实几秒就能完成时间上留了很大余量。更新完这个方案后我又对整个系统做了一遍排查把其他几个定时任务也统一加了分布式锁。这个教训的根源还是那句老话凡是涉及定时任务的业务逻辑在上多实例之前一定要先考虑幂等性和并发控制不要等线上出了故障再临时救火。最后再分享一个小技巧。如果开发设备维护类系统建议从开始就在工单表里保留一个source字段标记工单来自人工报修、自动保养还是巡检发现。有了这个维度后续统计报修渠道占比预防性维护贡献率就非常方便设备科在向院里汇报工作时这些数字非常有用。
返回列表