← 返回宏观 DAG

Payment Sub-DAG

收款子图 | Checkout → Price Calc → Payment → Fan-out (6 side-effects)
2026-03-22 | Spec 098-business-dag-audit

健康度总览

12
总边数
6
已验证
5
有风险
1
缺失
代码已验证
部分路径缺失或条件性触发
未实现或结构性缺失

核心审计发现:Fan-out 分散风险

CRITICAL: 支付完成后的 6 项 side-effect 不是由单一事件触发

Payment complete 后的 fan-out(Receipt / Invoice / Tip / Commission / Cash Drawer / Points) 分散在 4 个独立的调用站点,每个站点各自 inline 实现, 缺少统一的 payment-completed event bus。

4 个调用站点:

后果: 新增 side-effect 时必须同步修改全部 4 处调用站点,遗漏风险极高。 事实上 已经存在不一致:Cash 路径缺 loyalty points,POS Intent 路径缺 commission, Manual entry 路径完全缺少 commission / loyalty / tip distribution。

Fan-out 覆盖矩阵 (6 side-effects x 4 call sites)

Side-Effect checkout /cash intent /confirm POS Intent Mode manual-entry
Tip Distribution Yes Yes Yes No
Commission Recording Yes Yes No No
Loyalty Points No Yes No No
Cash Drawer Update Conditional N/A (card) N/A (card) No
Guest Tagging No Yes No No
Receipt Generation Manual API Manual API Manual API Manual API

DAG 拓扑

flowchart TD
  CHECKOUT["Checkout Initiated"]
  PRICE["priceCalculator.calculateFullPrice"]
  POS["POS Card Payment (CodePay P5)"]
  CASH["Cash Payment (/cash)"]
  SPLIT["Split Payment (splitPaymentService)"]
  GIFTCARD["Gift Card Deduction"]
  PAID["Payment Completed"]
  RECEIPT["Receipt (receiptService)"]
  INVOICE["Invoice (invoiceService)"]
  TIP["Tip Distribution (tipDistributionService)"]
  COMMISSION["Commission (commissionRecorder)"]
  CASHDRAWER["Cash Drawer (cashDrawerService)"]
  POINTS["Loyalty Points (loyaltyService)"]
  DAYEND["Day-End Reconciliation"]
  TAGGING["Guest Tagging (paymentHandler)"]

  CHECKOUT -->|"P1 Initiate checkout"| PRICE
  PRICE -->|"P2 POS card path"| POS
  PRICE -->|"P3 Cash path"| CASH
  PRICE -->|"P4 Split payment"| SPLIT
  PRICE -->|"P5 Gift card deduct"| GIFTCARD

  POS --> PAID
  CASH --> PAID
  SPLIT --> PAID

  PAID -->|"P6 Generate receipt"| RECEIPT
  PAID -->|"P7 Create/update invoice"| INVOICE
  PAID -->|"P8 Distribute tip"| TIP
  PAID -->|"P9 Record commission"| COMMISSION
  PAID -->|"P10 Update cash drawer"| CASHDRAWER
  PAID -->|"P11 Earn loyalty points"| POINTS
  PAID -->|"P12 Guest tagging"| TAGGING

  DAYEND -.->|"Day-end reconciliation"| CASHDRAWER

  linkStyle 5 stroke:#22c55e,stroke-width:2px
  linkStyle 6 stroke:#22c55e,stroke-width:2px
  linkStyle 7 stroke:#22c55e,stroke-width:2px

  linkStyle 8 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
  linkStyle 9 stroke:#22c55e,stroke-width:2px
  linkStyle 10 stroke:#22c55e,stroke-width:2px
  linkStyle 11 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
  linkStyle 12 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
  linkStyle 13 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
  linkStyle 14 stroke:#f59e0b,stroke-width:2px,stroke-dasharray:5
    

边审计明细

Status Edge 触发端 代码路径 审计说明
P1 Checkout → priceCalculator Web Checkout api/payment/checkout-routes.js L352
services/payment/priceCalculator.js
Checkout 路由调用 calculateFullPrice(),传入 items / discounts / tipAmount / taxRate。 税率通过 getTaxRateAsync() 从 DB 设置系统获取。 支持 package credit、subscription voucher、coupon 等多种折扣类型。 代码路径清晰,验证通过。
P2 Price → POS Payment POS P5 Device services/pos-payment-service.js L49-120
api/payment/intent-routes.js POST /confirm
两条 POS 路径:
(a) POS Intent Mode:Web 创建 payment_request → POS 接受 → CodePay 支付 → POS 回调 confirmPosPayment。
(b) Legacy /confirm:Flutter App 直接调用 /confirm 传入 codepay_trans_no。
两条路径均有 transaction 和 history 写入,验证通过。
P3 Price → Cash Payment Web Checkout api/payment/checkout-routes.js POST /cash (L727-933) Cash 支付通过 calculateChange() 验证找零, 更新 transaction status 为 COMPLETED, 写入 cash_received / cash_change。 如提供 cash_drawer_session_id 则更新 cash_drawer_sessions 统计。 代码路径清晰,验证通过。
P4 Price → Split Payment Web Checkout services/payment/splitPaymentService.js
api/payment/split-payment-routes.js
SplitPaymentService 支持 4 种模式:Full / By Service / By Person / Manual。 计算拆分金额后创建独立 transaction,走正常的 POS 或 Cash 支付流程。 验证通过。
P5 Price → Gift Card Deduction Web Checkout api/payment/checkout-routes.js L422-450 (FR-018)
invoiceService.applyGiftCardDeduction()
Checkout 阶段直接扣减礼品卡余额。 discountType === 'gift_card' 的折扣项在 transaction 内循环调用 applyGiftCardDeduction。扣减失败回滚整个事务。 售卡已入账收入,兑换记为折扣(FR-018b),验证通过。
P6 Payment Complete → Receipt Manual (API) api/receipts.js POST /api/receipts
services/payment/receiptService.js
Receipt 生成是 完全独立的 API 调用不是支付完成后自动触发。 前端需在支付成功回调中主动调用 POST /api/receipts。 POS Intent Mode 下,设备端通过 WebSocket 收到 receiptConfig 可触发打印,但后端无自动 receipt 创建逻辑。 风险:如前端遗漏调用,交易无收据记录。
P7 Payment Complete → Invoice Checkout Transaction api/payment/checkout-routes.js L498-654
services/payment/invoiceService.js
Invoice 在 checkout transaction 内创建(非 fan-out,而是同一事务的一部分)。 自动判定 single/group 类型,支持幂等(已有 invoice 则更新折扣信息)。 Cash / POS confirm 路径中 invoice 的 paid_amount 也会更新。 验证通过。
P8 Payment Complete → Tip Distribution checkout /cash, intent /confirm, POS Intent services/tipDistributionService.js distributeTip
checkout-routes.js L893-908
intent-routes.js L341-358
pos-payment-service.js L490-506
3 个支付完成路径均调用 distributeTip(), 条件是 tipAmount > 0 && appointmentId exists。 独立于 PAYROLL_V2_ENABLED,non-blocking (.catch)。 风险:manual-entry 路径完全缺失 tip distribution。
P9 Payment Complete → Commission checkout /cash, intent /confirm services/commission/commissionRecorder.js recordCommissionForPayment
checkout-routes.js L855-891
intent-routes.js L299-338
仅 checkout /cash 和 intent /confirm 两个路径调用 commissionRecorder。 受 PAYROLL_V2_ENABLED feature flag 控制。 风险:POS Intent Mode (pos-payment-service.js) 和 manual-entry 路径 均缺失 commission recording。 POS Intent Mode 是高频路径,佣金遗漏影响薪资准确性。
P10 Payment Complete → Cash Drawer checkout /cash (conditional) api/payment/checkout-routes.js L800-811
services/cashDrawerService.js
Cash 支付路径中,仅当前端传入 cash_drawer_session_id 才更新 cash_drawer_sessions 的 cash_sales 和 total_transactions。 前置检查(FR-013)在 checkout 入口处通过 checkPreviousSessionsClosed 阻止前日未关账的交易。 风险:如前端未传 session_id,现金交易不会记入 cash drawer, 导致日结对账差异。 Card 支付路径无 cash drawer 交互(正确行为)。
P11 Payment Complete → Loyalty Points intent /confirm only api/payment/intent-routes.js L212-241
services/loyalty/loyaltyService.js onPaymentCompleted
仅 intent /confirm 路径(Flutter POS 回调)调用 loyaltyService.onPaymentCompleted()checkout /cash 路径完全缺失 loyalty points 积累。 POS Intent Mode (pos-payment-service.js) 也缺失。 这意味着现金支付的客人不会获得积分,产生业务逻辑不一致。 条件:customer_idtenantId 都非空。
P12 Day-End → Reconciliation dayEndCloseoutService services/dayEndCloseoutService.js
services/cashDrawerService.js
DayEndCloseoutService 协调日结流程(checklist / tip settlement / data locking), 但日结过程中发现的未记录佣金,是通过 commissionRecorder 补录的 而不是从 payment complete 事件自然流入。 day-end 与 cash drawer 之间是查询关系,不是事件触发: 日结读取 cash_drawer_sessions 的统计数据做对账, 但如果 P10 未触发(session_id 未传),对账数据本身就是不完整的。 整体对账链路依赖前置正确性,缺乏自愈机制。

Fan-out 覆盖评分

3/6
checkout /cash 覆盖
5/6
intent /confirm 覆盖
1/6
POS Intent 覆盖

6 项 side-effect: Tip Distribution / Commission / Loyalty Points / Cash Drawer / Guest Tagging / Receipt。 其中 Receipt 所有路径均为手动触发(不计入自动 fan-out)。 Invoice 在 checkout transaction 内同步创建(不属于 fan-out,而是事务内逻辑)。

Loose Points 松散节点

LP-1: Cash 支付路径缺失 Loyalty Points

checkout-routes.js POST /cash 完成支付后未调用 loyaltyService.onPaymentCompleted()。 对比 intent-routes.js POST /confirm 有完整的 loyalty 集成(L212-241)。 现金支付的客人不获得积分,与卡支付行为不一致。

建议:在 /cash 路径的 post-transaction 阶段添加 loyalty 调用, 与 /confirm 路径保持一致。

LP-2: POS Intent Mode 缺失 Commission 和 Loyalty

pos-payment-service.js confirmPosPayment 完成后 仅触发 tip distribution 和 appointment status sync, 缺失 commissionRecorder 和 loyaltyService 调用。 对比 checkout /cash 和 intent /confirm 均有 commission recording。

建议:在 confirmPosPayment 的 post-transaction 阶段 添加 commissionRecorder 和 loyaltyService 调用。

LP-3: Manual Entry 路径完全缺少 side-effects

manual-entry-routes.js 创建已完成的 transaction, 但不触发 tip distribution / commission / loyalty / guest tagging 中的任何一项。 Manual entry 用于平账/补录,如果不触发佣金计算, 会导致员工薪资报表和实际业绩不一致。

建议:至少补齐 commission recording, 或在 day-end closeout 中增加 manual entry 的佣金补算逻辑。

LP-4: Receipt 完全依赖前端主动触发

所有支付路径的 receipt 生成均需前端调用 POST /api/receipts。 后端没有 auto-receipt 机制。如果用户在支付成功后关闭页面, 或 WebSocket 断开导致前端未收到成功回调, 该笔交易将没有任何收据记录。

建议:考虑在 payment complete 后自动创建 receipt 记录 (至少是 digital receipt),然后将打印/发送作为独立动作。

LP-5: Cash Drawer 更新依赖前端传参

Cash 路径中 cash_drawer_session_id 是可选参数。 如果前端未传(例如未开启 cash drawer 功能的门店), 现金交易不会记入 cash_drawer_sessions,导致日结对账时 expected cash 与 actual cash 产生系统性偏差。

建议:后端应自动查找当前门店的 active session, 而不是依赖前端传入。

架构改进建议

统一 Payment Completed Event Bus

当前 4 个调用站点各自 inline 实现 side-effects,违反 DRY 原则且极易遗漏。 建议引入统一的 onPaymentCompleted(req, transaction) 函数或事件总线:

跨图引用

Payment → Retention (Loyalty Points)

intent-routes.js L212-241 调用 loyaltyService.onPaymentCompleted() 触发积分累计和会员等级升级。这是 Payment 子图与 Retention 子图 的主要连接点。

注意:此连接点仅在 intent /confirm 路径存在, cash 路径和 POS Intent 路径断裂。

Payment → Operations (Commission / Day-End)

Commission recording 将支付数据导入薪资系统(commissionRecorder), Cash drawer 更新影响日结对账(dayEndCloseoutService)。 这两条边是 Payment 子图与 Operations 子图 的连接点。

Delivery → Payment (入口)

Delivery 子图 的 D9 (Completed → Payment 出口) 进入本子图。服务完成后,前端导航到 Checkout 页面, 触发 POST /api/payment/checkout 创建 PENDING transaction。