日期:2026-11-08
我们服务的这家区域汽车后市场连锁,底下十几家门店,洗车、美容、保养各自记各自的账。用户在一店充了值,到二店想用,系统里查不到余额,店员只能让用户现场再付一次,回头再退,体验非常糟。老板想做会员储值和到店核销的闭环,但卡在两点:一是门店系统版本不一,有的还是单机版;二是储值涉及预付资金,财务那边对合规清结算盯得紧,不敢乱来。
我们进场时他们已经试过一套外部SaaS,结果垮在跨店余额同步上,高峰期两家店同时扣一个用户的钱,余额变成负数,用户截图发朋友圈,口碑直接翻车。这个项目最关键的不是功能多,而是余额扣减这件事必须稳,宁可慢一拍,也不能重复扣。
落地后用户在小程序开卡储值,金额进平台统一账户,门店只是核销端。到店洗车或做美容套餐,店员扫用户码,系统按门店和套餐规则实时扣减余额,扣完给电子小票。跨店通用是默认能力,不另收手续费。
套餐这块,他们把洗美和保养做成组合权益,比如储值满一定额度送几次精洗,这种权益核销要区分"通用次数"和"限定门店次数",规则比单纯扣余额复杂。我们当时图省事,最初一版把权益和余额混在一个字段里扣,结果月底对账谁都说不清送的几次精洗到底用了多少,被财务打回重做。
后台核心是核销流水和余额账。每笔核销落一条不可变流水,余额是流水聚合出来的结果,而不是一个被直接改写的字段。这样任何对账差异都能回溯到具体哪一笔。资金侧走的是持牌清结算通道,平台不碰资金池,只做对账和分账指令。
绕不开的是跨店余额一致性。我们没有用各门店本地库各自记余额再定时同步,而是把余额当作中心账户的派生值,所有扣减走中心服务,门店端只发请求。这样从根上消灭了"两店同扣"的竞态,代价是门店断网时不能核销,我们用离线预授权做了兜底,网络恢复后补录。
紧接着要处理的是核销幂等。用户扫码这一下,手机弱网卡了可能连点两次,或者门店网络抖动导致请求重试。我们把核销请求带上客户端唯一单号,中心服务按单号去重,重复请求直接返回首次结果,绝不二次扣减。这是我们踩过扣成负数的坑之后,强制加上的铁律。
再往下是权益核销规则。送的精洗次数和现金余额要分开账本,核销时先扣权益次数再扣余额,顺序错了月中对账就乱。我们把规则做成可配置的表达式,门店套餐变更不用改代码,运营自己在后台配。
最后是资金合规。储值预付在监管上属于应隔离资金,我们坚决不碰资金池,核销只产生分账指令,真实资金由持牌方清结算,平台每日对账,差异秒级告警。
案例片段(已脱敏): 核销幂等扣减核心逻辑(节选):
def redeem(req): key = f"redeem:{req.client_nonce}" # 客户端唯一单号 if redis.setnx(key, 1, ex=86400): with db.transaction(): acct = lock_account(req.user_id) if acct.balance < req.amount: raise Insufficient("余额不足") acct.balance -= req.amount append_ledger(req) # 不可变流水 return ok(acct.balance) return cached_result(key) # 重复请求直接返回首结果压测日志:单中心账户峰值扣减 320 TPS,重复请求拦截率 100%,余额零负差。
上线两个月,跨店核销率,即非开户门店产生的核销笔数占比,稳定在四成左右,说明用户是真的在跨店用,不是被关在一家店。余额对账差异率从外部SaaS时期的数千分之一,压到万分之二以内,且全部能在十分钟内回溯到具体流水。
核销失败率,主要是弱网导致的离线预授权未补录,控制在千分之五以下,事后补录成功率接近百分之百。储值开卡数环比涨了约两成半,复购用户的到店频次提升明显。文中数据为项目复盘口径,均为示意值,非审计级精确数字。
财务侧最满意的是资金隔离和自动对账,原来三位同事对账一整天,现在系统自动出差异清单,人只处理异常。我们当时担心离线预授权会带来坏账,实际跑下来预授权未核销的占比极低,可以忽略。
储值核销这类系统,余额绝对不能做成一个被随意改写的字段,它必须是流水聚合出来的派生值,每一笔扣减都得能回溯。幂等单号是底线,弱网卡顿下的重复请求比你想的多,没有去重迟早扣成负数。
资金合规别抱侥幸,平台不碰资金池,把核销变成分账指令交给持牌方,反而省掉一堆麻烦。权益和现金一定要分账本,别图省事混在一起,月底对账会教你做人。门店断网要有离线预授权兜底,但回网补录必须强制,否则线下账迟早对不上。
这个项目里我后来复盘,最值钱的经验不是技术实现,而是早点和财务把资金隔离这关对齐,省掉了后面一堆扯皮。技术上幂等和流水账说到底都是为了防止那一次扣成负数的事故重演,账做对体验才好谈。我们当时担心离线预授权会带坏账,实际跑下来占比极低,这份担心也算落地时的一课。