酒店夜审计费引擎:业务日期语义与幂等落账¶
酒店会计的"一天"和自然日不一样:客人 23:00 入住、凌晨 02:00 结账,这笔生意属于哪个营业日?夜审计费引擎的全部复杂度都藏在这个问题的答案里——业务日期(business date)。
业务日期是什么¶
业务日期由夜审推进:每晚一次夜审,把"今晚"的所有在住账单落一笔房费,然后把业务日期 +1。它解决三个问题:
- 间夜归属:某晚房费属于哪个营业日,报表按它复算;
- 跨零点业务:凌晨的入账仍然记在"昨晚",账不平就能对出来;
- 结账截止:夜审 = 昨天的账封账,今天的账开账。
工程实现上它是配置表里的一个字符串键(hotel.business_date),由夜审事务原子推进。所有房费流水的写入都带 business_date 字段——这一列后来成了对账、报表、审计的地基。
幂等:夜审引擎的生命线¶
夜审必须能在任何时刻安全重跑(服务重启、断电、重复点击)。我们的做法是给每类落账定语义锚点:
- 夜审房费:
event_id = audit:<业务日期>:<folio>,全局唯一——重跑时命中唯一键即跳过; - 退房补差:按 folio 统计"已入住宿口径房费",补账金额 = 应收 − 已入,天然幂等;
- 团队/联房的批量操作:每笔子流水独立锚点,部分成功可续跑。
幂等的反面教训来自一个真实事故:退房补账没有锚点时,"夜审一次 + 退房一次 = 房费双记"。修复方式不是删流水,而是把补账逻辑改成"应收 − 已入"的差额模式——先算已入,再补差额,从此任何时点的重复调用都收敛到同一结果。
折扣与优惠的结算时点¶
计费引擎最容易出错的是"优惠什么时候给"。以连住折扣为例,两种口径:
- 入住时预折:报价即折后价,写进每晚房价。问题:客人提前离店,优惠已给、不符合条件,账上没有回追机制;
- 退房时判定(我们最终采用的):入住与夜审全程按原价落账,退房时按实际连住晚数判定——满阈值且规则在生效期内,落一笔负向优惠行;不满足按原价,不补不退。
口径 2 的代价是退房时要多一步判定与落账,收益是账务上"先足额、后优惠",任何时点的账都能解释。优惠行本身也要幂等:按 folio 检查是否已有未冲红的优惠行,撤销退房后再退房不会双发。
试算与过账分离¶
夜审做成了两段式:GET 试算(只读,返回每张在住账单当晚的应落房费与优惠明细),POST 过账(按试算结果落账并推进业务日期)。前台在夜审发生前就能看到"今晚大概收多少",过账入口则要求权限与二次确认。
试算与过账共用同一套取价逻辑(日历覆盖价 → 方案价 → 会员折扣的叠加顺序),只是试算不落库。计费规则只有一份实现,是避免"试算说 380,账上记 388"这类事故的根本。
小结¶
夜审引擎的工程要点可以压成三句话:业务日期是一等公民;每类落账都有语义锚点;计费规则单实现、两段式暴露。剩下的都是酒店业务本身的细节——而细节恰恰是这个行业软件的护城河。