Files
junhong_cmp_fiber/openspec/specs/employee-collection-bill/spec.md
break 5ed6b39deb
Some checks failed
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Has been cancelled
feat(收口): 补齐 8 月迭代缺口并同步 Spec 与证据链
- 新增六对成对迁移 000232–000237:H5 弹窗类型、退款结算标识与申请人备注、优先轮询事实字段与两个新终态、通道阈值命中留痕、手机号最近解绑人、提现资格校验留痕
- 退款:原因必填与申请人备注、来源支付与渠道流水冻结、线下处理流水号补录审计、按订单查询可选退款方式、企微审批材料补齐且新增字段缺失映射即明确失败
- 优先轮询:人工关闭、有效期到期独立周期任务、失败与过期人工重触发、事实字段与异常重试查询、资产解析端点只读投影
- 通道阈值:命中事实同事务留痕与命中记录查询;员工账单:列表筛选与详情投影;商户池:列表投影与统计周期语义;H5:弹窗类型与类别排序
- 手机号:有效关联数量与最近解绑人、短信验证码失败次数限制;导出:佣金明细十五列与报表序号列
- 时间筛选:三处新增筛选纳入统一严格解析契约,员工账单产生时间参数改名
- 同步 12 份主 Spec 需求、两端点与异步任务证据链,门禁 context-health 与 OpenSpec 校验通过
2026-09-18 15:34:29 +08:00

213 lines
19 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# employee-collection-bill Specification
## Purpose
为后台账号代客户经办的线下套餐购买和代理预存款充值建立独立的员工应收与核销核验闭环,核销申请以企业微信为唯一终审;该能力只保存可追溯的本地业务事实,不推测或回填历史第三方付款。
## Requirements
### Requirement: 员工代收款账单来源、金额与建账判据
系统 SHALL 仅为下列业务创建员工代收款账单,并以实际发起该业务的后台账号作为不可修改的欠款人:
- 后台线下套餐订单,同时满足 `payment_method = offline``operator_account_type = platform`、订单不含赠送套餐、且订单 `actual_paid_amount > 0`;账单应收金额取订单 `actual_paid_amount`,代理代购和无代理归属自营 C 端均适用。`actual_paid_amount ≤ 0` 的订单 MUST NOT 创建账单。该订单创建成功后仍按既有规则立即激活。
- 代理线下预存款/主钱包充值完成入账后,账单金额取对应充值记录 `amount`;入账完成以“平台账号发起的线下充值入账成功”这一既有事实为准,既有企业微信终审通过入账与既有线下充值人工确认入账均适用。
客户自行线上支付、平台代理 C 端客户充值资产钱包、订单失败或取消、赠送套餐等不产生员工账单的既有线下订单,以及“其他”手工来源 MUST NOT 创建账单。系统 MUST 为同一来源业务建立至多一张账单,并保存来源类型、来源 ID、来源单号、客户/店铺快照、欠款人、应收金额和创建时间。建账 MUST 只由来源成功事务携带的来源主键触发;系统 MUST NOT 通过扫描历史订单或充值记录补建账单。
#### Scenario: 后台线下套餐订单产生账单
- **WHEN** 平台业务员或超级管理员成功创建一个满足建账判据、且 `actual_paid_amount` 大于零的后台线下套餐订单
- **THEN** 系统以该操作账号为欠款人、以订单 `actual_paid_amount` 为应收金额创建唯一待核销账单,且订单无需因未上传付款凭证而阻断
#### Scenario: 线下充值完成入账产生账单
- **WHEN** 平台账号发起的代理线下预存款/主钱包充值经企业微信最终通过并完成入账
- **THEN** 系统以实际发起充值的后台账号和充值 `amount` 创建唯一待核销账单
#### Scenario: 人工确认入账同样产生账单
- **WHEN** 未关联企业微信审批实例的线下充值经后台人工确认入口完成入账
- **THEN** 系统同样以发起充值的后台账号和充值 `amount` 创建唯一待核销账单
#### Scenario: 重复来源不产生重复账单
- **WHEN** 同一来源业务被重复处理、重复回调或重试
- **THEN** 系统至多保留一张来源关联账单,不新增第二张账单
#### Scenario: 零金额与赠送订单不建账
- **WHEN** 后台线下套餐订单的 `actual_paid_amount` 小于等于零,或订单包含赠送套餐
- **THEN** 系统不创建任何账单,且该订单的创建时付款凭证要求保持既有规则
#### Scenario: 历史来源记录不被扫描建账
- **WHEN** 建账用例运行,而某笔上线前已存在的订单或充值从未由本次来源成功事务携带来源主键
- **THEN** 系统不为该记录创建账单
### Requirement: 账单余额、状态与关闭
账单 SHALL 独立维护 `待核销``部分核销``已核销``已关闭` 状态及应收金额、已核销金额、审批中预占金额和剩余可核销金额。只有企业微信最终通过的分摊增加已核销金额;审批中的分摊预占剩余可核销金额,防止并发申请超额核销。账单已核销金额等于应收金额时 MUST 为已核销;关闭账单只作废当时未核销余额,已核销金额必须保留。
欠款人离职、禁用或变更组织后,账单欠款人身份和既有账单范围 MUST 保持不变。欠款人(平台账号)仅可查询本人账单与本人申请;超级管理员可查询全部;本期 MUST NOT 引入“财务”角色MUST NOT 依赖 `RequirePermission` 路由鉴权体系实现可见性。仅超级管理员可代办创建、修改或重提申请,且必须记录实际代办人和原因。
系统 SHALL 提供与账单列表相同筛选条件和相同可见性范围的统计聚合,返回应收金额合计、已核销金额合计、未核销金额合计和待处理账单数。
#### Scenario: 欠款人仅见本人账单与申请
- **WHEN** 非超级管理员的后台账号查询账单列表、账单详情或申请
- **THEN** 系统仅返回以该账号为欠款人的账单及由该账号发起的申请,且无权限与不存在不产生可枚举差异
#### Scenario: 超级管理员查询全部账单
- **WHEN** 超级管理员查询账单列表
- **THEN** 系统返回其可见范围内的全部账单,不按欠款人账号过滤
#### Scenario: 统计与列表同口径
- **WHEN** 查询统计聚合与使用相同筛选条件的账单列表
- **THEN** 统计的应收、已核销、未核销金额与待处理账单数与列表数据一致
#### Scenario: 部分核销后仍可继续核销
- **WHEN** 一张账单存在企业微信已通过但未结清的分摊
- **THEN** 系统增加已核销金额、将账单标记为部分核销,并仅允许新的分摊使用未被已通过或审批中分摊占用的余额
#### Scenario: 关闭未结清账单
- **WHEN** 超级管理员对待核销、部分核销或已驳回关联申请的账单填写关闭原因并执行关闭,且账单不存在审批中申请
- **THEN** 系统作废未核销余额、将账单标记为已关闭、保留已核销金额和操作审计
#### Scenario: 审批中账单不可关闭
- **WHEN** 超级管理员尝试关闭存在审批中核销申请的账单
- **THEN** 系统拒绝关闭,账单金额和状态不变
### Requirement: 外部付款核销申请与分摊
员工 SHALL 按一笔外部付款创建一张核销申请。申请 MUST 选择一个启用的线下收款方式字典项,并保存其稳定编码和名称快照;必须保存经人工确认的付款金额、付款方、付款时间、外部交易流水号、至少一个支付凭证和可选其他凭证、备注及一个或多个账单分摊。支付凭证 MUST 复用既有附件键,只保存对象键引用并经既有预签名下载能力访问,本能力 MUST NOT 承诺资源级附件鉴权。申请人只能通过勾选可见账单创建分摊,系统带出只读来源订单、客户和资产信息。
系统 SHALL 校验申请人提交的账单分摊:申请人按账单产生时间从早到晚用本次付款金额预填分摊、最后一张填入剩余金额,并可在提交前修改各分摊金额;服务端 MUST 按校验规则拒绝越界分摊。单笔分摊 MUST 大于零且不得超过该账单可核销余额;分摊总额 MUST 不超过本次人工确认付款金额。同一外部付款可被多个核销申请引用,本期 MUST NOT 对跨申请累计分摊金额实施系统防重或金额上限校验。
#### Scenario: 一笔付款分摊多张账单
- **WHEN** 员工选择多张可见账单并提交一笔外部付款的核销申请
- **THEN** 系统校验每张账单可核销余额和申请总额、拒绝越界分摊,并为该申请创建唯一企业微信审批实例
#### Scenario: 账单并发申请预占
- **WHEN** 两个核销申请并发选择同一账单的剩余余额
- **THEN** 系统至多接受不超过该账单未核销余额的审批中和已通过分摊,其余申请返回余额不足且不创建超额分摊
### Requirement: 核销申请审批、重提与幂等
核销申请状态 SHALL 为 `审批中``已通过``已驳回``已撤销/已关闭`,且不得以申请状态覆盖账单核销状态。提交或重提时系统 MUST 新增一条不可变审批尝试记录(冻结当次收款方式、外部付款、附件、备注、账单分摊及审批材料快照)并为其创建新的企业微信审批实例;申请只保存最新审批实例 ID 用于展示。企业微信业务类型 MUST 为 `employee_collection_approval`,其业务标识 MUST 取审批尝试记录主键,使同一申请的多次提交各自持有独立审批实例,且 MUST NOT 修改既有审批实例的共享唯一约束或既有 `refund_approval``offline_recharge_approval` 场景语义。
企业微信最终通过时,系统 MUST 幂等地将申请标记为已通过、将各分摊写入账单已核销金额并释放其预占;最终驳回时 MUST 标记申请已驳回、释放全部预占且保留审批意见。已驳回申请可修改全部申请内容后重提,历史审批实例、材料和结果不得覆盖;已通过分摊不可修改。企业微信提交失败、回调延迟或结果未知时申请保持在途,系统 MUST 使用既有查询/恢复机制确认渠道结果,且不得由本地人工通过或拒绝绕过企业微信。企业微信对已通过申请撤销时,系统 MUST NOT 回滚已核销金额MUST 将申请转为异常终态、保留审计与账单详情提示,并禁止自动重提。同一撤销结果在本地申请仍为审批中时(回调乱序或通过结果未被消费),系统 MUST 释放该次审批尝试的全部审批中预占、将申请转为异常终态并记录终态原因、保留审计MUST NOT 自动重提。
#### Scenario: 企业微信通过核销申请
- **WHEN** 企业微信对含多笔分摊的核销申请返回最终通过,且该结果首次被消费
- **THEN** 系统仅一次更新申请、各账单已核销金额和状态,并保留审批实例及冻结快照
#### Scenario: 企业微信驳回后重提
- **WHEN** 企业微信最终驳回核销申请
- **THEN** 系统释放预占、保留驳回实例和意见;员工或有代办权限的超级管理员修改申请后重提时新增审批尝试记录并创建新的审批实例,历史实例与材料不被覆盖
#### Scenario: 企业微信通过后撤销
- **WHEN** 已通过的核销申请收到企业微信撤销结果
- **THEN** 系统不回滚已核销金额,将申请标记为异常终态、保留审计并在账单详情提示,且不允许自动重提
#### Scenario: 审批中收到通过后撤销
- **WHEN** 企业微信对仍处于审批中的核销申请返回“通过后撤销”(回调乱序或本地未消费到通过结果)
- **THEN** 系统释放该申请全部审批中预占、将申请转为异常终态并记录终态原因、保留审计,且不回滚任何已核销金额;该申请禁止自动重提,账单可重新发起新的核销申请
### Requirement: 收款方式字典、退款联动与可追溯性
系统 SHALL 提供唯一固定分类的线下收款方式字典,其对外契约 MUST 包含 ID、稳定编码、名称和启用状态引用方 MUST 保存名称快照。超级管理员可维护名称、稳定编码、排序、启停和备注;已被业务引用的字典项 MUST NOT 被物理删除只能停用且历史申请继续显示冻结名称。该字典分类由本能力独占维护MUST NOT 被定义为“核销专用”,本能力也 MUST NOT 修改企业微信 `offline_recharge_approval` 场景的业务字段。
来源套餐订单退款成功时,系统 SHALL 以本次退款成功金额与账单应收(即来源订单 `actual_paid_amount`)比较:退款成功金额等于或超过账单应收且账单不存在已通过或审批中分摊时,系统 MUST 自动关闭账单并记录“来源订单全额退款”;退款成功金额小于账单应收且账单不存在已通过或审批中分摊时,系统 MUST 按退款金额冲减账单应收金额并保留来源订单退款冲销记录。账单存在任一已通过分摊或任一审批中(预占)分摊时,系统 MUST NOT 自动冲销,仅在账单详情提示来源订单退款。同一退款对同一账单 MUST 至多产生一条冲销记录。退款不恢复已核销账单的员工欠款,系统 MUST NOT 实现退款审批、退款状态机或支付渠道退款。
账单、申请、分摊、附件、审批实例、字典快照、关闭和退款冲销 MUST 可按权限查询并记录操作审计审计和日志不得保存完整支付凭证敏感内容。OCR 若可用仅用于预填,必须允许申请人或审核人更正,且识别值不是资金事实。
#### Scenario: 来源订单部分退款且未核销
- **WHEN** 来源套餐订单退款成功金额小于账单应收,且其账单不存在任何已通过或审批中分摊
- **THEN** 系统按退款金额冲减账单应收金额,保留退款冲销关联,并重新计算账单状态和可核销余额
#### Scenario: 来源订单全额退款且未核销
- **WHEN** 来源套餐订单退款成功金额等于或超过账单应收,且其账单不存在任何已通过或审批中分摊
- **THEN** 系统自动关闭账单、记录“来源订单全额退款”,并保留已核销金额与审计
#### Scenario: 存在审批中分摊的账单退款不自动冲销
- **WHEN** 来源套餐订单退款成功,而其账单存在审批中(预占)分摊但不存在已通过分摊
- **THEN** 系统不修改账单应收、已核销或预占金额,仅写退款关联提示
#### Scenario: 被引用字典项停用
- **WHEN** 超级管理员停用已被核销申请引用的线下收款方式
- **THEN** 新申请不可选择该方式,历史申请仍展示其冻结名称和稳定编码
### Requirement: 员工账单列表查询与筛选
账单列表 SHALL 在既有可见性范围内支持以下筛选:账单编号(精确)、来源订单号(精确)、来源业务类型、账单状态、产生时间范围、核销通过时间范围、欠款人姓名或账号(模糊)、客户名称(模糊)。账单编号 MUST 为账单自身的唯一标识(即账单记录标识);系统 MUST NOT 为满足本要求新增编号字段或编号生成规则。欠款人姓名或账号与客户名称的模糊匹配 MUST 由服务端按去空白后的子串匹配实现MUST NOT 要求调用方先获得账号 ID 或客户 ID。
产生时间范围 MUST 使用统一参数名 `start_time``end_time`;核销通过时间范围 MUST 使用 `decided_start_time``decided_end_time`。两类时间参数 MUST 使用同一严格解析契约:取值为带显式时区的 RFC3339 秒级时间,解析结果归一为 UTC 瞬时筛选区间为闭区间含两端开始时间晚于结束时间时拒绝请求旧参数名与旧格式含仅日期的取值MUST 被拒绝MUST NOT 保留静默兼容或别名。
核销通过时间 MUST 取该账单经「状态为已通过的分摊」关联到的申请表审批终态时间;账单从未存在已通过分摊时该筛选 MUST 不命中该账单。系统 MUST NOT 使用分摊记录的释放时间作为核销通过时间:该字段在既有实现中同时承担「审批通过转为已核销」与「驳回或释放预占」两种语义。
列表响应 SHALL 同时返回「未核销金额」与「剩余可核销金额」两个金额:未核销金额 MUST 为应收金额减已核销金额;剩余可核销金额 MUST 为应收金额减已核销金额再减审批中预占金额,已关闭账单两者均为零。已核销金额大于应收金额(异常数据)时,两个金额 MUST 均按 0 返回MUST NOT 返回负数。列表 MUST NOT 因新增模糊筛选改变既有可见性规则:欠款人仅可见本人账单,超级管理员可见全部。
#### Scenario: 按账单编号精确查询
- **WHEN** 超级管理员以账单编号筛选账单列表
- **THEN** 系统返回该编号对应账单,且不返回其它账单
#### Scenario: 按核销通过时间筛选
- **WHEN** 调用方以核销通过时间范围筛选
- **THEN** 系统只返回在该范围内存在已通过分摊的账单,从未核销的账单不出现在结果中;筛选依据为该账单已通过分摊关联的申请表审批终态时间
#### Scenario: 拒绝旧时间参数与旧格式
- **WHEN** 调用方携带旧参数名或仅日期、无时区、带小数秒的时间取值请求账单列表
- **THEN** 系统以参数非法错误拒绝请求,且不返回任何账单
#### Scenario: 按员工姓名或客户名称模糊查询
- **WHEN** 调用方提交员工姓名子串或客户名称子串
- **THEN** 系统返回欠款人账号名称或客户名称包含该子串的账单,无需调用方提供账号 ID 或客户 ID
#### Scenario: 未核销金额与剩余可核销金额
- **GIVEN** 一张应收 100 元、已核销 30 元且存在审批中预占 20 元的账单
- **WHEN** 查询账单列表
- **THEN** 未核销金额返回 70 元,剩余可核销金额返回 50 元
#### Scenario: 已关闭账单两个金额为零
- **WHEN** 查询一张已关闭账单
- **THEN** 未核销金额与剩余可核销金额均返回零,已核销金额保持关闭时的值
### Requirement: 员工账单详情投影
账单详情 SHALL 在既有账单、来源、客户与员工信息之外,返回该账单的操作日志与来源订单快照。操作日志 MUST 取该账单维度既有审计事实的投影至少包含动作、操作账号名称、操作时间与变更前后摘要MUST NOT 包含支付凭证内容、完整收款信息或其它敏感原文。来源订单快照 MUST 取来源订单或来源充值记录的既有只读字段投影,至少包含来源单号、来源业务类型、来源实收或充值金额、来源创建时间与资产标识;客户与员工信息 MUST 复用账单创建时冻结的快照。来源记录不可读或字段缺失时 MUST 返回空值 MUST NOT 阻断详情响应MUST NOT 为此新增来源业务写入路径。
#### Scenario: 详情返回操作日志
- **WHEN** 授权账号查询一条存在创建、提交与审批终态的账单详情
- **THEN** 响应的操作日志按时间升序返回各次动作、操作账号名称、操作时间与前后值摘要
#### Scenario: 详情返回来源订单快照
- **GIVEN** 账单来源为后台线下套餐订单或代理线下充值
- **WHEN** 查询该账单详情
- **THEN** 响应返回来源单号、来源业务类型、来源金额、来源创建时间与资产标识
#### Scenario: 来源记录不可读
- **GIVEN** 账单来源记录已被物理删除或字段为空
- **WHEN** 查询该账单详情
- **THEN** 来源订单快照对应字段返回空值,详情其余内容正常返回
#### Scenario: 操作日志不含敏感原文
- **WHEN** 账单存在包含凭证上传与收款方式变更的操作
- **THEN** 操作日志只返回对象键引用、编码与名称摘要,不返回凭证内容或完整收款文本
## 可达操作索引
本节只用于入口导航,不是行为 Requirement业务义务以上述 Requirements 为准。
### 线下收款方式字典
`GET /api/admin/employee-collection-payment-methods`(查询线下收款方式);`POST /api/admin/employee-collection-payment-methods`(创建线下收款方式);`PUT /api/admin/employee-collection-payment-methods/{id}`(更新线下收款方式);`DELETE /api/admin/employee-collection-payment-methods/{id}`(删除线下收款方式)。
### 员工代收款账单
`GET /api/admin/employee-collection-bills`(查询本人或全部账单);`GET /api/admin/employee-collection-bills/statistics`(账单金额统计);`GET /api/admin/employee-collection-bills/{id}`(查询账单详情);`POST /api/admin/employee-collection-bills/{id}/close`(关闭账单)。
### 核销申请
`POST /api/admin/employee-collection-applications`(创建核销申请);`GET /api/admin/employee-collection-applications`(查询核销申请);`GET /api/admin/employee-collection-applications/{id}`(查询核销申请详情);`PUT /api/admin/employee-collection-applications/{id}`(修改并重提核销申请)。