Day-End Closeout & Cash Drawer | 更新时间: 2026-02-09
admin/day-end-closeout/page.tsx,
stores/[id]/page.tsx (入口按钮)
DayEndCloseoutModal.tsx (主 5 步向导),
OrderStatusStep.tsx (Step 1),
TipsSettlementStep.tsx (Step 2),
RegisterClosureStep.tsx (Step 3),
PaymentReconStep.tsx (Step 4),
DataLockStep.tsx (Step 5),
CloseRegisterModal.tsx,
CurrencyCountModal.tsx,
TipDistributionPreview.tsx,
ChecklistStep.tsx,
CloseoutCompletedView.tsx,
UnlockCloseoutModal.tsx
CloseRegisterModal.tsx (legacy duplicate)
| CUJ | 优先级 | 描述 | 触发点 | 业务价值 | E2E 状态 |
|---|---|---|---|---|---|
| C1 | P0 | 完整日结流程 5 步走 | 营业日结束时 | 核心财务流程 — 确保账目准确并锁定 | 部分覆盖 |
| C2 | P0 | 收银台管理 | 营业日开始/结束 | 现金流追踪,差异管控 | 部分覆盖 |
| C3 | P1 | 日结历史查询 | 管理员查看过往记录 | 审计追溯,财务核查 | 部分覆盖 |
| C4 | P1 | 解锁日结 | 发现日结有误需修正 | 纠错能力(需超级管理员权限) | 缺失 |
flowchart TD
ENTRY1["admin/day-end-closeout/page.tsx"] --> MODAL
ENTRY2["stores/[id]/page.tsx 日结按钮"] --> MODAL
MODAL["DayEndCloseoutModal.tsx - 5 步向导"] --> S1["Step 1: OrderStatusStep"]
MODAL --> S2["Step 2: TipsSettlementStep"]
MODAL --> S3["Step 3: RegisterClosureStep"]
MODAL --> S4["Step 4: PaymentReconStep"]
MODAL --> S5["Step 5: DataLockStep"]
S2 --> TDP["TipDistributionPreview"]
S3 --> CRM["CloseRegisterModal"]
CRM --> CCM["CurrencyCountModal 面额清点"]
S5 -->|"完成"| CCV["CloseoutCompletedView 汇总"]
style MODAL fill:#2196F3,stroke:#1565C0,color:#fff
style CCV fill:#4CAF50,stroke:#2E7D32,color:#fff
stateDiagram-v2
[*] --> in_progress: 点击 Start Closeout
in_progress --> completed: Step 5 确认完成
completed --> locked: 自动锁定
locked --> completed: 超级管理员解锁
completed --> locked: 重新锁定
note right of in_progress: 5 步向导进行中
note right of locked: 数据不可修改
| 步骤 | 组件 | 阻塞条件 | API |
|---|---|---|---|
| Step 1 订单状态 | OrderStatusStep |
有预约状态不在 completed/cancelled 中 | GET /api/day-end-closeout/:id/pending-orders |
| Step 2 小费结算 | TipsSettlementStep |
Step 1 未完成 / 有未分配小费 | GET|POST /api/day-end-closeout/:id/pending-tips |
| Step 3 收银台关闭 | RegisterClosureStep |
Step 1+2 未完成 / 有未关闭的 cash_drawer_session | GET /api/day-end-closeout/:id/unclosed-registers |
| Step 4 支付对账 | PaymentReconStep |
MVP 无阻塞,手动确认即可 | GET /api/day-end-closeout/:id/payment-summary |
| Step 5 确认锁定 | DataLockStep |
Step 1-4 全部完成 | POST /api/day-end-closeout/:id/complete |
status 字段。日结锁定的是日期级别的数据 — 锁定后该 business_date 下的所有预约、发票、交易均变为只读。
dataLockingService 拦截equal(均分)或 by_service_price(按服务金额比例),可配置店铺抽成比例P0 核心财务流程 — 确保每日营业数据准确并锁定
flowchart TD
A["admin/day-end-closeout 或 stores 页面"] --> B["点击 Start Closeout"]
B --> C["创建 day_end_closeouts 记录 (in_progress)"]
C --> D["DayEndCloseoutModal 打开"]
D --> S1["Step 1: OrderStatusStep"]
S1 --> S1A{"有待处理预约?"}
S1A -->|"是"| S1B["列出待处理预约,逐个处理"]
S1A -->|"否"| S1C["自动标记完成"]
S1B --> S1C
S1C --> S2["Step 2: TipsSettlementStep"]
S2 --> S2A["TipDistributionPreview 显示预览"]
S2A --> S2B["点击 Distribute Tips"]
S2B --> S2C["tipDistributionService 执行分配"]
S2C --> S3["Step 3: RegisterClosureStep"]
S3 --> S3A{"有未关闭收银台?"}
S3A -->|"是"| S3B["打开 CloseRegisterModal"]
S3B --> S3C["CurrencyCountModal 面额清点"]
S3C --> S3D["对比: 实际 vs 系统记录"]
S3D --> S3E{"有差异?"}
S3E -->|"是"| S3F["填写差异原因"]
S3E -->|"否"| S3G["关闭收银台"]
S3F --> S3G
S3A -->|"否"| S4
S3G --> S4["Step 4: PaymentReconStep (MVP)"]
S4 --> S4A["显示卡/现金交易汇总"]
S4A --> S4B["点击 Confirm Reviewed"]
S4B --> S5["Step 5: DataLockStep"]
S5 --> S5A["显示全日汇总数据"]
S5A --> S5B["点击 Complete Closeout"]
S5B --> S5C["status: completed → locked"]
S5C --> S5D["CloseoutCompletedView 完成"]
style D fill:#2196F3,stroke:#1565C0,color:#fff
style S5D fill:#4CAF50,stroke:#2E7D32,color:#fff
style S3F fill:#FF9800,stroke:#E65100,color:#fff
day_end_closeout:start、day_end_closeout:view、day_end_closeout:complete 权限,且 scope 覆盖当前门店in_progresslocked,显示 CloseoutCompletedView测试: day-end-closeout.spec.ts (部分覆盖 — 缺完整 5 步 E2E)
day_end_closeout:view 与 day_end_closeout:complete 权限,且 scope 覆盖当前门店pending_payment(服务完成未结账)测试: day-end-closeout.spec.ts (部分覆盖)
day_end_closeout:complete 权限,且 scope 覆盖当前门店by_service_pricedistributed测试: 缺失 — 需新增
day_end_closeout:complete 与 cash_drawer:close 权限,且 scope 覆盖当前门店closed测试: cash-drawer/day-end-closeout.spec.ts, full-flow/11-cash-drawer.spec.ts (部分覆盖)
day_end_closeout:complete 权限,且 scope 覆盖当前门店day_end_closeout:complete 权限,且 scope 覆盖当前门店in_progress → completed → lockedlocked_at 时间戳记录测试: 缺失 — 需完整 5 步 E2E
day_end_closeout:view 权限,且 scope 覆盖当前门店in_progress 的日结记录(昨天开始未完成)P0 现金流追踪 — 收银台开启/关闭/对账
stateDiagram-v2
[*] --> open: POST /api/cash-drawer/open
open --> open: 现金交易累计
open --> closed: POST /api/cash-drawer/close
note right of open: 追踪所有现金进出
note right of closed: 计算差异并记录
cash_drawer:open 权限,且 scope 覆盖当前门店open)测试: full-flow/11-cash-drawer.spec.ts (部分覆盖)
cash_drawer:open 权限,且 scope 覆盖当前门店cash_drawer:close 权限,且 scope 覆盖当前门店closed
cash_drawer:close 权限,且 scope 覆盖当前门店测试: cash-drawer/day-end-closeout.spec.ts (部分覆盖)
cash_drawer:close 权限,且 scope 覆盖当前门店测试: full-flow/11-cash-drawer.spec.ts (部分覆盖)
cash_drawer:open 权限,且 scope 覆盖当前门店GET /api/cash-drawer/current?store_id=xxxP1 审计追溯 — 查看过往日结记录
locked)
测试: day-end-closeout.spec.ts (部分覆盖)
in_progress 的日结记录P1 纠错能力 — 超级管理员解锁已完成的日结
flowchart TD
A["查看已锁定的日结 (status: locked)"] --> B["发现数据有误"]
B --> C["点击 Unlock Closeout"]
C --> D{"权限检查: day_end_closeout:unlock"}
D -->|"有权限"| E["UnlockCloseoutModal 打开"]
D -->|"无权限"| F["按钮不可见/禁用"]
E --> G["输入解锁原因(必填)"]
G --> H["POST /api/day-end-closeout/:id/unlock"]
H --> I["status: locked → completed"]
I --> J["数据解锁,可修改"]
J --> K["修正数据后重新完成日结"]
style E fill:#FF9800,stroke:#E65100,color:#fff
style I fill:#4CAF50,stroke:#2E7D32,color:#fff
style F fill:#f44336,stroke:#c62828,color:#fff
day_end_closeout:unlock 权限与对应范围(scope)locked 的日结completed(非 in_progress)测试: 缺失 — 需新增
day_end_closeout:unlock 权限测试: 缺失 — 需新增
completedlocked| 测试文件 | 覆盖的 CUJ | 测试场景 |
|---|---|---|
| day-end-closeout.spec.ts | C1, C3 | 日结页面加载、检查项列表、历史查看 |
| cash-drawer/day-end-closeout.spec.ts | C1 (Step 3), C2 | 现金柜关闭、差异对账 |
| full-flow/11-cash-drawer.spec.ts | C2 | 现金柜完整流程(开启→交易→关闭) |
| 缺口 | CUJ | 缺失内容 |
|---|---|---|
| 完整 5 步端到端 | C1 | 从 Start → Step 1-5 → locked 的完整 E2E |
| 小费分配流程 | C1 (Step 2) | TipDistributionPreview + distribute-tips 调用 |
| 解锁日结 | C4 | UnlockCloseoutModal 完整 E2E |
| 数据锁定验证 | C1 | locked 后尝试修改预约/交易被拦截 |