去年年底,我们信息科接到后勤管理部的需求:医院目前的餐补管理太乱了,财务每个月对账单要加班三天,职工投诉餐补不到账的工单一个月能接到近百条。领导拍板说,这个系统你们信息科主导,得尽快上线。
刚开始我以为这无非就是个"充钱-扣钱-查余额"的CRUD系统,结果需求评审一开,发现事情远比想象中复杂。多院区多食堂多消费终端的数据同步、HRP系统的实时对接、定时任务的可靠性保障、高并发消费场景下的数据一致性,还有各种诡异的补贴清零规则,哪个都不是省油的灯。这篇文档把我们在"好伙狮"平台基础上做二次开发和对接落地的过程梳理出来,给同行做个参考。
一、系统架构设计
先给个整体架构的大图。我们采用了微服务+消息队列的架构,核心服务拆分为五大模块:
补贴人员管理服务(User-Wallet Service):负责职工信息同步、钱包创建、补贴账户管理。这个模块是基础,所有后续的发放和消费都依赖它。我们和HRP系统做了双向对接,HRP推职工变更事件过来,本服务消费后更新本地缓存。
补贴发放引擎(Subsidy-Dispatch Engine):核心发放逻辑的承载模块。支持批量导入发放、定时发放任务调度、发放规则校验。这里我们用Quartz做调度,Redis做分布式锁防止重复执行。
消费交易服务(Transaction Service):处理各个消费终端的实时交易请求。这个模块对并发和响应时间要求最高,午间高峰时段TPS能达到三百左右。我们用了本地缓存+Redis二级缓存来扛住读压力。
补贴清零服务(Clearance Service):管理各种清零规则。按月清零、按日清零、按人员分组差异化清零。这个模块的难点在于规则引擎的设计,后面会详细展开。
统计报表服务(Report Service):定时跑批生成月度汇总表和发放明细表,数据量大时走ClickHouse做OLAP查询,避免拖垮主库。
二、核心数据流程:从发放到消费再到清零的完整链路
先说最重要的主流程。一笔餐补从财务发起发放到最终被职工消费掉的完整链路是这样的:
第一步,财务人员在PC管理端导入补贴发放Excel,或者设定定时发放任务。系统拿到发放数据后,先做基础校验:职工ID是否存在、金额是否合法、发放周期是否重复。校验通过后,生成一批"待发放"状态的补贴记录,写入MySQL的subsidy_dispatch表。
第二步,发放引擎异步处理。每批发放记录按职工ID哈希分片,投递到不同的Kafka分区,各消费线程并行处理。对于每个职工的补贴记录,先扣减财务账户的预算额度,再增加职工补贴钱包的余额,最后更新发放记录状态为"已发放"。这里必须保证财务账户扣减和职工钱包增加要么同时成功、要么同时失败,所以我们引入了TCC分布式事务方案——try阶段锁定双方账户余额,confirm阶段执行真正的加减操作,cancel阶段回滚。
第三步,职工在食堂消费终端刷卡或刷脸。消费终端发起的请求量在午餐高峰时段非常集中,我们的消费交易服务采用了一致性哈希路由,确保同一职工的请求始终落在同一台服务器上,这样可以直接使用本地内存中的余额缓存来判断是否足以支付,而不需要每次都查数据库。余额扣减走Redis的Lua脚本,原子性地完成余额检查和扣减。
第四步,到了清零时间点,清零服务被触发。它会扫描所有符合清零规则的职工账户,执行余额清零操作,并生成清零日志。这个操作必须非常谨慎——我们曾经因为一个清零规则的BUG,把全院两千多职工的当月餐补全部清掉了,那次应急恢复熬了个通宵。后面会细说这个教训。
三、HRP系统对接的设计与踩坑
HRP系统的对接是项目中最让人头疼的部分。医院用的是某品牌的老版本HRP,接口文档不全,而且只支持被动查询模式——也就是说,我们没法让HRP在人员变动时主动推送消息过来,只能我们定时去拉。
最初的方案是全量拉取:每天凌晨2点,定时任务走HTTP接口拉取全院职工列表,拿回来和本地数据库做diff,找出新增的、离职的、部门变动的,然后更新补贴系统中的职工档案。这个方案一开始能跑,但问题很快就暴露了。凌晨跑全量同步时,全院一万多名职工的数据拉取需要差不多十五分钟,接口还时不时超时。更要命的是,上午十点如果有新职工入职,他要等到第二天凌晨才能用上餐补,后勤那边意见很大。
后来我们做了两点优化。第一,跟HRP厂商协商后开放了一个增量查询接口,按lastModifyTime分页拉取变更记录,每五分钟轮询一次,每次只拉变更了的那几十条数据,效率提升了两个数量级。第二,对于紧急入职场景,增加了管理后台的"手动同步"按钮,管理员可以手动触发单个人的信息同步。
对接中另一个坑是数据格式不一致。HRP中的部门编码和我们补贴系统中的科室编码完全是两套体系——HRP用的是三位数字编码,我们用的是科室拼音首字母缩写。最初我们写死了一个映射表来转换,结果每次医院调整科室架构时都要更新映射表。最终的方案是在中间层建了一个"组织架构字典表",HRP和补贴系统各自维护自己的编码,通过字典表建立映射关系,新增科室时管理员在后台配置一下就行。
四、定时发放任务的可靠性设计
财务部门对定时发放的要求非常明确:每月1号上午9点准时到账,一次都不能出错、一次都不能延迟。这个看似简单的要求,在分布式环境下其实有不少坑。
第一个坑是重复执行。Quartz在集群模式下通过数据库锁来保证同一时刻只有一个节点执行某个任务,但网络抖动时可能出现锁超时释放,导致两个节点同时拿到了执行权。我们的解决方式是增加业务层面的幂等校验:每次发放任务执行前,先检查当天是否已经有过成功的发放记录,如果有则跳过。发放批次号用日期+递增序号组成,数据库做了唯一约束,防重复写入。
第二个坑是执行中断。全院六千多职工的补贴发放,如果中途服务挂了,已经发放的两千多人的补贴不能丢,也不能重发。我们采用了"分批提交"的策略:每处理完两百人的发放,提交一次事务。服务重启后,从未完成的批次继续执行,已完成的批次不受影响。
第三个坑是时间漂移。有一段时间我们用的服务器没有配置NTP,系统时间比实际时间慢了大概三分钟。结果就是职工在群里刷屏"说好九点到账的餐补呢",而我们查日志发现任务明明执行了,仔细一看是还没到时间。后来强制所有服务器走NTP同步,并且在发放任务执行前加了一个对时校验,如果和NTP时间偏差超过三十秒就报警。
五、清零规则的配置化引擎
清零逻辑看起来简单——到时间了把余额归零就行。但医院的实际规则非常复杂:普通职工月底清零,但临床一线人员可以保留半年;外包服务人员按日清零;有些科室领导有"免清零"特权;还有春节加班期间发放的专项补贴不能清零。如果把这些规则硬编码到代码里,每次政策调整都得改代码发版。
我们的做法是设计一套规则配置引擎。清零规则抽象为三个维度:对象范围(全员/部门/分组/个人)、时间周期(按日/按周/按月/按年/不执行)、豁免条件(指定人员/指定时间段/指定补贴类型)。每个规则是一条配置记录,清零引擎在执行时按优先级加载所有适用规则,对每个职工账户计算出最终的清零策略。
值得一提的是我们踩过的一个大坑。上线初期,清零服务是每天凌晨跑一个全表扫描,把所有"余额不为零且已过清零日期"的账户找出来清零。这个SQL在数据量小的时候没问题,但运行半年后,历史账户记录超过几十万条,全表扫描直接拖死了数据库。后来的优化方案是:建一个"待清零索引表",只记录当前有余额且设置了清零规则的账户,清零服务只扫描索引表。同时把清零执行时间从凌晨挪到凌晨三点,避开备份窗口。
六、消费终端的接口设计
食堂档口的消费机是一个典型的嵌入式设备,Android系统,内存有限。我们的消费接口必须足够轻量,响应时间要控制在五百毫秒以内——你肯定不想看到职工刷了脸还要站着等三秒钟才扣款成功。
接口设计上,消费请求的报文非常精简:
请求只包含三个字段:用户标识(手机号或工号)、消费金额、消费终端编号。服务端拿到请求后,依次校验用户是否存在、补贴钱包余额是否充足、消费金额是否超过单笔限额、当天消费次数是否超限。校验通过后扣减余额并写入消费流水。所有校验和扣减都在Redis中通过一个Lua脚本原子完成,避免了网络往返带来的性能损耗和并发问题。
消费接口的另一个要点是"组合支付"。医院的要求是:消费时优先扣减补贴钱包,补贴不够的部分从充值钱包补足。这个逻辑在Lua脚本中实现:先尝试从补贴钱包扣减全部金额,如果余额不足则扣减剩余部分到补贴为零,再从充值钱包扣减差额。整个过程在一个Redis事务中完成,保证原子性。
我们还做了一个关键的容灾设计。万一Redis挂了或者网络断了,消费终端不能完全停摆。我们在消费终端上做了本地缓存:每次成功交易后,终端把用户当前余额缓存到本地。断网时终端可以读取本地缓存的余额做离线扣减,恢复网络后批量同步交易记录到服务端。当然这个离线模式的余额不是实时准确的,所以我们限定了离线模式下的单笔消费上限和累计消费上限,控制风险。
七、数据一致性的兜底方案
再好的架构也架不住各种意外。数据库主从切换、消息队列堆积、网络分区,这些在生产环境迟早会碰上。我们为补贴系统设计了多层兜底机制。
第一层是实时对账。每一笔发放和消费交易都生成一个全局唯一的交易流水号,写入MySQL的同时也发一条Kafka消息给对账服务。对账服务消费消息后写入Elasticsearch,提供实时的交易查询能力。如果服务端标识交易成功但Kafka消息没有送达,对账服务会通过定时任务扫描MySQL的流水表补齐。
第二层是T+1日终对账。每天凌晨跑批,把前一天的发放总额、消费总额、各账户余额汇总,与财务系统做交叉核对。如果出现差异——比如所有职工的余额总和与财务账面不符——立即告警并触发人工核查。
第三层是月末全量对账。每月最后一天生成完整的对账报告,包括期初余额、当月发放、当月消费、当月清零、期末余额,每个数字都有明细支持,确保"账账相符、账实相符"。我们曾通过这个对账机制发现了一个隐蔽的BUG:在某些特定条件下,消费接口超时后事务回滚了,但Redis中的余额已经扣减,导致数据库和Redis数据不一致。发现问题后我们立刻修复了事务边界的问题。
八、上线效果与总结
系统上线运行至今已经超过一年,覆盖了医院三个院区、六个食堂、近万职工。几个关键指标的变化足以说明效果:财务月末对账时间从近三天压缩到了半天以内,对账准确率从约九成提升到超过百分之九十九;职工关于餐补的咨询工单减少了约八成;补贴发放从过去由专人花费半天手动操作,变成了一键导入后系统自动完成。
回顾整个项目,我最大的体会是:做医院内部的数字化系统,技术难点往往不在于某个高深算法或前沿框架,而在于对业务规则的精确理解和边界条件的妥善处理。一个补贴清零规则没想清楚,就可能让全院职工的餐补一夜归零;一个HRP接口的超时没处理好,新入职职工就要饿一天肚子。这些细节,恰恰也是好伙狮数字食堂这类垂直行业平台的价值所在——它们沉淀了大量医院场景的实践经验,让你不需要从零开始踩坑。
如果你也正在或将要负责类似的医院后勤数字化项目,希望这篇记录能提供一点参考。遇到坑是难免的,但踩得越早,上线就越稳。