归档员工代收款账单闭环变更并同步主规格
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1m36s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1m36s
- 新增主规格 openspec/specs/employee-collection-bill/spec.md(5 条 Requirement、22 个 Scenario) - 变更目录归档至 openspec/changes/archive/2026-09-11-add-employee-collection-bills
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
@@ -0,0 +1,130 @@
|
||||
## Context
|
||||
|
||||
见 `proposal.md` 与 `specs/employee-collection-bill/spec.md`。现有后台套餐订单、代理充值、企业微信审批和审计已有各自业务事实,但没有将“后台账号代客户经办后的公司应收”作为独立对象保存。现有 `tb_agent_recharge_record` 已有金额、支付凭证和审批实例关联;套餐订单已有 `actual_paid_amount`。这些来源只能提供已确定的金额和关联键,不能被新功能改写。
|
||||
|
||||
本设计只处理上线后事件,且不新增任何运行时开关或配置来表达上线切割点:建账只允许由“本次来源成功事务”携带的来源主键触发,禁止任何按历史订单或充值扫描、补建的代码路径;来源唯一键 `order:{id}` / `recharge:{id}` 兜底幂等。新表对既有旧代码无影响,单批发布即可。
|
||||
|
||||
线下付款并非本系统支付渠道事实,企业微信审批人员以第三方记录核验,因此本地只留存申请人声明、附件、冻结快照和企业微信最终结果。
|
||||
|
||||
术语边界:员工代收款账单是后台账号代客户经办业务形成的本地暂挂欠款,不等同于来源订单、客户付款或企业微信审批单。核销申请对应一笔外部付款及一次企业微信审批实例;核销分摊是该申请对单张账单确认的本次金额,账单核销状态与申请审批状态独立保存。审批尝试记录是同一核销申请每次提交或重提对应的不可变审批材料与实例关联事实;申请行只保存最新审批实例 ID 用于展示。企业微信审批实例仅是本地业务单的外部审批渠道生命周期事实;外部交易流水号可由 OCR 预填,但必须经人工确认。线下收款方式是固定注册分类下的业务字典项,不等同于线上支付方式枚举或收款账户目录;已被引用的字典项只可停用并保留历史名称快照。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 让账单、申请和分摊形成可并发保护、可重提、可审计的本地财务事实。
|
||||
- 使企业微信是唯一终审来源,同时沿用已有可靠提交、回调和轮询恢复机制。
|
||||
- 在订单/充值入账、退款和核销之间建立明确且幂等的关联。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不建设公司收款账户目录,不接入或改造 OCR 契约,不校验同一外部付款在不同申请间的累计分摊。
|
||||
- 不回填历史业务,不允许“其他”来源手工建账,不处理平台代理 C 端资产钱包充值。
|
||||
- 不以账单功能重构订单、代理充值或企业微信通用审批模块。
|
||||
- 不引入“财务”角色,也不启用 `RequirePermission` 路由鉴权体系;本期可见性只区分欠款人本人与超级管理员,财务独立可见性作为后续权限 Change 的待办。
|
||||
- 不新增导出;本期只提供与账单列表同筛选、同数据范围的统计聚合。
|
||||
- 不实施 AUG26-017 的线下充值材料字段,也不修改企业微信 `offline_recharge_approval` 场景业务字段。
|
||||
|
||||
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 账单、申请、分摊分表保存
|
||||
|
||||
建立员工代收款账单、核销申请、核销分摊、审批尝试记录和线下收款方式字典事实。账单绑定唯一来源业务;申请绑定一次外部付款,并保存最新企业微信审批实例 ID;每次提交或重提新增一条审批尝试记录并创建独立审批实例;分摊连接申请与账单并冻结账单来源摘要。附件复用现有对象存储键/附件模式,审批提交与渠道上下文复用既有通用审批能力。
|
||||
|
||||
不把多笔付款、账单状态或附件塞入来源订单 JSON:来源订单既有生命周期不等于员工欠款,且一笔付款对多账单是独立关系。
|
||||
|
||||
### 2. 金额统一使用分,分摊在锁定账单中预占
|
||||
|
||||
账单应收、已核销、预占和申请付款金额均用 `int64` 分。提交/重提在同一 GORM 事务中按账单 ID 升序 `FOR UPDATE` 锁定,重新计算“应收金额 - 已通过分摊 - 其他审批中分摊”,再写申请与分摊。企业微信通过消费同样锁定申请和相关账单,使用审批实例唯一关联/状态条件更新保证至多入账一次。
|
||||
|
||||
不使用乐观展示余额或仅在回调时校验;那会让并发审批中申请超额占用同一账单。
|
||||
|
||||
### 3. 企业微信审批作为唯一状态推进器,重提新增审批尝试记录
|
||||
|
||||
申请提交事务只写本地申请、冻结快照、审批尝试记录、审批实例及可靠提交请求。审批回调和既有兜底查询都进入同一个幂等消费用例:通过才计入账单已核销,驳回才释放预占。提交失败或渠道未知保持在途,禁止本地财务人工改审批结果。
|
||||
|
||||
新增企业微信业务类型 `employee_collection_approval`,其 `business_id` 取本 Change 的**审批尝试记录主键**,申请行只保存最新审批实例 ID 用于展示。因此每次提交或重提都会新增一条审批尝试记录和一个审批实例,历史实例、材料与结果不被覆盖,列表按申请聚合、审计可回放每次审批。既有 `refund_approval`、`offline_recharge_approval` 两场景继续“一业务单一个审批实例”的语义,**不修改共享的唯一约束**。
|
||||
|
||||
企业微信“通过后撤销”不回滚已核销金额:申请转异常终态、保留审计、详情提示、禁止自动重提,与既有退款链路对 `revoked_after_approved` 的处理一致。
|
||||
|
||||
### 4. 建账在来源成功事务内直建,不新增来源事件
|
||||
|
||||
两类来源都在**各自成功事务内直接创建账单**,以来源唯一键去重,不新增来源 Outbox 事件类型、不引入消费者:建账只能由“本次来源成功事务”携带的来源主键触发,不存在按历史记录扫描或补建的路径。
|
||||
|
||||
- 后台线下套餐订单:建账判据为 `payment_method = offline` ∧ `operator_account_type = platform` ∧ 订单不含赠送套餐 ∧ `actual_paid_amount > 0`;`actual_paid_amount ≤ 0` 不建账,避免 0 元账单立即成为已核销。建账点位于订单激活事务内、`tx.Create(order)` 之后,来源唯一键 `order:{id}`。创建时付款凭证的放宽范围与该判据**严格互补**:建账订单不再强制创建时凭证,赠送等不建账订单保持既有凭证要求。
|
||||
- 代理线下预存款/主钱包充值:锚点为“平台账号发起的线下充值入账成功”这一既有事实,金额取充值记录的 `amount`,来源唯一键 `recharge:{id}`。该事实不依赖具体申请表结构,也不使用申请表未落库的新字段;既有企业微信终审通过入账与既有线下充值人工确认入账都产生该事实,因此两条入账入口完成入账时都建账。线上充值、审批未通过或未完成入账不建账。
|
||||
|
||||
订单创建不得等待企业微信或外部付款。重复订单事务、重复回调、重放或消费者重试都返回同一账单,不重复建账。该判据是订单建账的**唯一共享事实来源**,因此后台线下套餐订单与 H5 平台代购 offline 入口(同为平台账号操作、`actual_paid_amount > 0`)走同一判据建账,不因入口不同产生差异。
|
||||
|
||||
### 5. 退款只自动冲销不存在已通过或审批中分摊的账单
|
||||
|
||||
退款处理只在既有的**退款成功事务**内查找来源账单并锁定:接入点是当前已存在的退款成功处理链路,不实现 AUG26-006 的退款审批、状态机或渠道退款。
|
||||
|
||||
全额与部分以“**本次退款成功金额 vs 账单应收**”判定,账单应收即来源订单的 `actual_paid_amount`:
|
||||
|
||||
- 本次退款成功金额等于或超过账单应收且账单不存在已通过分摊 → 关闭账单,原因记为来源订单全额退款;
|
||||
- 本次退款成功金额小于账单应收且账单不存在已通过分摊 → 按退款金额冲减账单应收;
|
||||
- 账单存在任一**已通过分摊**或任一**审批中(预占)分摊** → 不自动冲销,只在账单详情写退款关联提示。
|
||||
|
||||
把审批中分摊与已通过分摊同等对待,是因为预占余额可能大于冲减后的剩余应收,从而在审批通过后产生 `已核销 > 应收`。冲销写入必须自带唯一幂等事实(`bill_id + refund_id` 唯一),不得依赖退款成功事务的 `changed` 标志——该事务在重复投递时仍会执行资金写入。
|
||||
|
||||
本期冲销**只由企业微信审批通过的退款成功事务触发**。既有 legacy 人工退款审批通过入口(`internal/service/refund/service.go` 的 `Approve`,受默认开启的 `Approval.LegacyRefundManualEnabled` 开关控制)不在本期接入范围:该入口对携带审批实例的退款直接拒绝,仅历史上无审批实例的存量待审批退款可达,本 Change 上线后新产生的来源订单退款不可达该路径。这是**已登记的缺口**,若后续要求该入口一致冲销,应单独作为后续 Change 处理。
|
||||
|
||||
## 业务动作契约
|
||||
|
||||
以下是本 Change 新增后台动作的已确认设计;路径遵循既有 `/api/admin` 路由约定,所有金额字段均为 `int64` 分,所有成功响应使用既有 `pkg/response` 包装。
|
||||
|
||||
### 收款方式字典维护
|
||||
|
||||
- `GET /employee-collection-payment-methods`:登录后台账号均可读取。超级管理员返回全部字典项并支持按启停状态与编码/名称关键字筛选;其他后台账号仅返回启用项,用于新核销申请选择收款方式。分页返回 ID、稳定编码、名称、启用状态、排序、备注与创建/更新时间。
|
||||
- `POST /employee-collection-payment-methods`:仅超级管理员。请求包含 `code`(1~64 字符、全局唯一)、`name`(1~100 字符)、`sort`(非负整数)、`enabled`、`remark`(最多 500 字符)。创建后返回字典 ID、字段值和创建时间。
|
||||
- `PUT /employee-collection-payment-methods/:id`:仅超级管理员;不得修改已引用项的 `code`,可修改名称、排序、启停和备注。不存在返回既有“资源不存在”错误;重复编码返回稳定“收款方式编码已存在”错误。
|
||||
- `DELETE /employee-collection-payment-methods/:id`:仅未被申请引用的项可删除;已引用返回“收款方式已被引用,只能停用”。每个成功写操作记录操作者和前后快照。
|
||||
- 未引用项的删除以 `deleted_at` 软删除实现(`gorm.Model.DeletedAt`),对外语义与物理删除一致:列表与详情不可见、编码可被后续创建复用;保留软删除只为保住审计快照的可追溯性,不构成对外行为差异。
|
||||
|
||||
### 账单查询与关闭
|
||||
|
||||
- `GET /employee-collection-bills`:欠款人本人强制加 `debtor_account_id=当前账号`;超级管理员查询全部。支持来源类型、来源单号、账单状态、欠款人、客户/店铺、创建时间范围筛选和分页。每行返回账单 ID、来源摘要、欠款人快照、应收、已核销、预占、剩余、状态和创建时间。
|
||||
- `GET /employee-collection-bills/statistics`:与列表相同的筛选条件与数据范围,返回应收、已核销、未核销金额合计和待处理账单数。
|
||||
- `GET /employee-collection-bills/:id`:在同一可见性校验后返回账单、来源摘要、退款冲销、分摊、申请与审批历史;附件只返回既有对象键引用并经既有预签名下载能力访问,不返回对象存储敏感内容,也不承诺资源级鉴权。
|
||||
- `POST /employee-collection-bills/:id/close`:仅超级管理员;请求 `reason` 必填、最长 500 字符。事务中锁定账单,存在审批中申请返回“账单存在审批中核销申请,不能关闭”;已关闭返回既有状态冲突;成功时仅作废未核销余额并写关闭审计。
|
||||
|
||||
### 核销申请创建、修改与重提
|
||||
|
||||
- `POST /employee-collection-applications`:员工为本人可见账单创建,超级管理员可代办但请求必须附 `acting_reason`(1~500 字符)。请求包含 `payment_method_id`、`paid_amount`(正分)、`payer_name`、`paid_at`(带时区 RFC3339 时间)、`external_transaction_no`、`payment_voucher_keys`(1~5 个既有附件键)、`remark`、`allocations[]`;每个分摊包含 `bill_id` 和正的 `amount`。申请人按所选账单确定:本人办理时固定为当前账号且所选账单必须全部属于该账号;超级管理员代办时申请人为所选账单的唯一欠款人,所选账单必须同属该欠款人,代办人与原因分别记录在 `acting_operator_id` 与 `acting_reason`。
|
||||
- 服务按账单 ID 升序锁定,校验字典启用、账单可见且未关闭、分摊不超过该账单 `应收-已核销-其他审批中预占`、分摊总额不超过 `paid_amount`。成功时新增一条不可变审批尝试记录(冻结当次收款方式、外部付款、附件、备注与分摊快照)并创建对应企业微信审批实例,申请只保存最新审批实例 ID;返回申请 ID、状态 `审批中`、审批实例 ID、冻结快照与各分摊。任一校验失败时不保存申请、分摊或预占。
|
||||
- 分摊金额由申请人在提交前填写:界面可按账单产生时间从早到晚用本次付款金额预填、最后一张填入剩余金额,但服务端只接受客户端提交的 `allocations[]` 并按其校验,越界分摊一律拒绝;服务端不重新计算或改写申请人提交的分摊金额。
|
||||
- `PUT /employee-collection-applications/:id`:仅申请人或代办超级管理员,且仅已驳回申请可修改;入参同创建。事务释放旧驳回版本无预占事实,重新锁定和校验账单,新增一条审批尝试记录与一个新的企业微信审批实例,历史尝试、材料与结果不被覆盖。已通过、审批中、已撤销/关闭状态返回状态冲突。
|
||||
|
||||
### 企业微信审批结果消费
|
||||
|
||||
- 企业微信回调和既有状态恢复任务均按审批实例 ID 进入同一应用用例,不提供后台“通过/驳回”接口。
|
||||
- 最终通过:锁定申请及按 ID 升序的全部账单;仅当申请仍为审批中时,将每笔分摊从预占转入已核销,重新计算账单 `待核销/部分核销/已核销` 状态,标记申请已通过,并记录审批结果。重复或乱序的同一终态不重复增加已核销金额。
|
||||
- 最终驳回:仅当申请仍为审批中时释放全部预占,标记已驳回并保存审批意见;重复回调不重复释放。提交失败、回调延迟和未知结果维持在途,由既有查询恢复任务确认,不得人工改写终态。
|
||||
- 通过后撤销:已通过后收到企业微信撤销时**不回滚已核销金额**;申请转异常终态、保留审计、账单详情提示来源申请异常,并禁止自动重提。若该撤销结果到达时本地申请仍为审批中(回调乱序或通过结果未被消费),同样按异常终态处理并释放本次尝试的全部审批中预占——否则申请会永久停在审批中、预占永久占用账单,既不可关闭也不可重提。
|
||||
|
||||
### 来源建账与退款冲销
|
||||
|
||||
- 后台线下套餐订单在成功事务内、`tx.Create(order)` 之后按决策 4 的判据建账:以实际操作的平台账号为欠款人、`actual_paid_amount` 为应收、`order:{id}` 为来源唯一键。重复订单事务、重放或重试均返回同一账单;不存在按历史订单扫描建账的路径;创建时付款凭证的放宽与该判据严格互补。
|
||||
- 代理线下充值在“平台账号发起的线下充值入账成功”事实达成时、于同一入账事务内建账,以充值记录 `amount` 为应收、`recharge:{id}` 为来源唯一键,欠款人为发起充值的平台账号;既有企业微信终审通过入账与既有线下充值人工确认入账两条入口都建账,线上充值、审批未通过或未完成入账不建账;不存在按历史充值扫描建账的路径。
|
||||
- 套餐退款成功时按决策 5 的口径锁定来源账单并冲销或仅提示;冲销以 `bill_id + refund_id` 唯一事实幂等,重复投递不二次冲减;接入点只在既有退款成功处理事务内,不实现 AUG26-006 的退款审批、状态机或渠道退款。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [企业微信回调重复、乱序、未知或通过后撤销] → 以审批实例、审批尝试记录、申请状态和分摊状态条件更新幂等消费,复用渠道查询恢复;通过后撤销不回滚已核销,转异常终态并禁止自动重提。
|
||||
- [外部付款敏感信息泄露] → 附件只保存既有对象键引用并经既有预签名下载访问,不承诺资源级鉴权;日志、审计和错误只记录脱敏摘要与业务 ID。
|
||||
- [建账与来源事务不一致] → 在来源成功事务内直建账单,来源唯一键 `order:{id}` / `recharge:{id}` 兜底;不新增来源事件与消费者,也不存在历史扫描路径。
|
||||
- [既有 legacy 线下充值人工确认入账路径] → 该路径(默认开启的兼容入口)同样产生“入账成功”事实并按本设计建账,避免员工欠款漏记;本 Change 不改该开关、不改该入口,开关是否关闭属运维独立决策。
|
||||
- [超额核销] → 提交、重提和审批通过均锁定账单并校验预占余额。
|
||||
- [退款与核销并发] → 退款冲销与审批消费按相同账单锁顺序串行;存在已通过或审批中分摊时一律不自动冲销,只写退款关联提示。
|
||||
- [权限口径缺失] → 既有系统无“财务”角色且未启用 `RequirePermission` 路由鉴权,本期只区分欠款人本人与超级管理员;财务独立可见性留给后续权限 Change。
|
||||
- [重提与审批核心唯一约束] → 新业务类型以审批尝试记录主键作为 `business_id`,不改共享唯一约束,既有两个审批场景语义不变。
|
||||
- [财务审计时间线未纳入新业务类型] → `internal/query/audit/finance.go` 的 `expandApprovals` 与 `approvalBusinessRefs` 的 `business_type` 分支、以及资金资源类型清单均未包含 `employee_collection_approval`,财务调查视图不会为该场景展开业务关联(不报错,仅缺失跳转)。spec 未要求,**不属本期范围**,登记为后续 Change 待办,不在本次改动内。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移(`.up.sql`/`.down.sql`)创建字典、账单、申请、分摊、审批尝试记录与退款冲销关联所需表、唯一约束和查询索引;不修改既有迁移,也不预占迁移编号——实施开始前按当时 `migrations/` 目录的最大编号顺延。
|
||||
2. 迁移扩展 `tb_wecom_approval_scene` 的 `business_type` CHECK 以纳入 `employee_collection_approval`(同样以新迁移扩展,不修改既有迁移)。施工时列全四处注册点:业务类型常量、`validApprovalBusinessType` 与 `sceneBusinessFields`、数据库 CHECK、`cmd/worker/main.go` 的审批决策消费者注册。
|
||||
3. 单批发布即可:新表对既有旧代码无影响,也不存在需要开关或基准数据表达的上线切割点;建账只由部署后发生的来源成功事务触发。
|
||||
4. 按 ENG-TEST-001 在维护者指定的 `junhong_cmp_test` PostgreSQL + Redis DB 6 验证:迁移从本地工作区以显式 `DB_*` 执行 `scripts/migrate.sh`,只创建、删除本 Change 自己的 fixture,禁止重置整库;仅连接、迁移或实际行为失败时才阻塞对应场景。验证项:不存在按历史记录扫描或补建的路径、重复消费来源不重复建账、并发预占、通过/驳回/重提/通过后撤销、退款三类联动、迁移 up/down/up。
|
||||
5. 回滚时先停止新入口;已有账单事实保留,只有维护者确认未产生不可逆业务数据时才执行 down。
|
||||
@@ -0,0 +1,33 @@
|
||||
## Why
|
||||
|
||||
平台代理或无代理归属的 C 端客户以线下方式购买套餐、或代理线下充值预存款时,实际经办后台账号形成公司应收欠款。当前系统只有订单或充值审批,无法将这笔欠款、客户外部付款凭证、企业微信核验和最终核销结果形成独立、可分摊且可审计的闭环。
|
||||
|
||||
本 Change 落实讨论稿 AUG26-001:只覆盖功能上线后的新增业务,不回填任何历史账单,避免把历史支付事实以推测方式写入新财务账。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增员工代收款账单:后台线下套餐订单创建成功后,或代理线下预存款/主钱包充值完成入账后,按确定金额为实际经办账号创建唯一账单。线下充值以“平台账号发起的线下充值入账成功”这一既有事实为锚点,既有企业微信终审通过入账与既有线下充值人工确认入账均适用;建账只由来源成功事务触发,不扫描或补建历史记录。
|
||||
- 新增核销申请和账单分摊:员工按一笔外部付款创建一张申请,可选择多张账单并填写各自分摊金额;同一账单可由多笔已通过申请分次核销。
|
||||
- 新增固定分类的线下收款方式字典;申请冻结字典名称、外部付款、附件、账单分摊,每次提交或重提另存一条审批尝试记录。OCR 仅可预填流水号和付款金额,人工确认值才是业务事实。
|
||||
- 核销申请仅由企业微信最终结果驱动(通过、驳回、通过后撤销);提交失败、回调延迟或结果未知复用既有审批查询/恢复闭环,禁止本地人工绕过终审,通过后撤销不回滚已核销金额。
|
||||
- 新增账单、核销申请、分摊、附件与审批记录的权限受控查询;超级管理员关闭未结清账单、来源订单退款时的账单冲销均保留审计。
|
||||
- **BREAKING**:会生成员工账单的后台线下套餐订单不再在创建时强制上传付款凭证;凭证和外部付款信息改为核销申请必填。赠送套餐等不生成账单的既有线下订单继续保持原凭证要求。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `employee-collection-bill`: 员工代收款账单、线下收款方式、分摊核销申请、企业微信审批闭环、退款冲销和受控查询。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。本 Change 通过新能力监听并引用既有订单和代理充值的已确定业务事实,不重写其既有主规格。
|
||||
|
||||
## Impact
|
||||
|
||||
- 数据:新增账单、收款方式字典、核销申请、分摊、审批尝试记录和退款冲销关联等表;订单/充值来源只保存可追溯关联,不回填历史记录。
|
||||
- 写侧:后台线下套餐下单、代理线下充值入账、核销提交/重提/关闭、企业微信审批结果消费、套餐退款。
|
||||
- 读取:账单与申请列表、详情,以及与列表同筛选、同数据范围的统计聚合;欠款人仅看本人,超级管理员看全部;不新增导出。
|
||||
- 配置:不新增任何运行时开关或配置来表达上线切割点;新表对既有旧代码无影响,单批发布即可。
|
||||
- 权限:本期不引入“财务”角色,也不启用 `RequirePermission` 路由鉴权体系;财务独立可见性留待后续权限 Change。
|
||||
- 依赖:复用既有企业微信通用审批实例、可靠提交和状态恢复机制;OCR 与外部付款渠道不在本 Change 新建契约。
|
||||
@@ -0,0 +1,125 @@
|
||||
## Purpose
|
||||
|
||||
为后台账号代客户经办的线下套餐购买和代理预存款充值建立独立的员工应收与核销核验闭环,核销申请以企业微信为唯一终审;该能力只保存可追溯的本地业务事实,不推测或回填历史第三方付款。
|
||||
|
||||
## ADDED 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** 新申请不可选择该方式,历史申请仍展示其冻结名称和稳定编码
|
||||
@@ -0,0 +1,27 @@
|
||||
## 1. 账单数据与基础契约
|
||||
|
||||
- [x] 1.1 盘点现有订单、代理充值、通用企业微信审批、钱包入账、附件、Outbox 和审计模型,确定来源建账点、附件键和审批尝试记录的复用点;确认新业务类型 `employee_collection_approval` 以审批尝试记录主键作为 `business_id` 的兼容方案;不得复制敏感付款内容。
|
||||
- [x] 1.2 新增成对迁移与 GORM 模型:线下收款方式字典、员工代收款账单、核销申请、申请—账单分摊、审批尝试记录、退款冲销关联;为来源唯一性、审批实例唯一性、账单查询和分摊锁定建立约束/索引。同时新增迁移扩展 `tb_wecom_approval_scene` 的 `business_type` CHECK 以纳入新业务类型(不修改既有迁移)。迁移不预占编号,按实施开始时 `migrations/` 目录最大编号顺延。
|
||||
- [x] 1.3 定义金额分、账单状态、申请状态、来源类型和稳定错误码;实现中文名称、DTO 枚举说明及金额/附件/分摊校验。
|
||||
- [x] 1.4 实现超级管理员维护线下收款方式字典的新增、编辑、启停和受引用不可删除规则,并写配置审计。
|
||||
|
||||
## 2. 来源建账与账单读取
|
||||
|
||||
- [x] 2.1 在后台线下套餐订单成功事务内(`tx.Create(order)` 之后)按判据 `payment_method = offline` ∧ `operator_account_type = platform` ∧ 订单不含赠送套餐 ∧ `actual_paid_amount > 0` 以实际操作平台账号为欠款人、以 `actual_paid_amount` 为应收创建账单;`actual_paid_amount ≤ 0` 不建账;以 `order:{id}` 唯一键保证重复处理、重放或重试返回同一账单;创建时付款凭证的放宽范围与该判据严格互补,赠送等不建账订单保持既有凭证要求;不新增来源 Outbox 事件类型,不存在按历史订单扫描建账的路径。
|
||||
- [x] 2.2 在“平台账号发起的线下充值入账成功”这一既有事实上、于入账事务内以 `recharge:{id}` 唯一键创建账单,金额取充值 `amount`,欠款人为发起充值的平台账号;覆盖既有企业微信终审通过入账与既有线下充值人工确认入账两条入口;线上充值、审批未通过或未完成入账不建账;不依赖申请表未落库的新字段,不存在按历史充值扫描建账的路径。
|
||||
- [x] 2.3 实现账单列表、详情和统计 Query:按来源、状态、时间、欠款人、客户筛选,欠款人仅见本人,超级管理员见全部;统计与列表使用相同筛选条件与可见性范围,返回应收、已核销、未核销金额合计与待处理账单数;列表返回应收、已核销、预占和未核销金额及审批中标识。
|
||||
- [x] 2.4 实现账单关闭用例:仅超级管理员、仅允许无审批中申请的未结清账单、必须填写原因,并在事务内保存状态变化与成功审计。
|
||||
|
||||
## 3. 核销申请、审批与退款联动
|
||||
|
||||
- [x] 3.1 实现核销申请创建和已驳回重提:锁定选中账单、按时间预填、校验付款金额与分摊、预占余额、每次提交或重提新增一条不可变审批尝试记录(冻结收款方式/外部付款/附件/账单摘要)并以尝试记录主键为 `business_id` 创建新的企业微信审批实例和可靠提交请求;申请只保存最新审批实例 ID。
|
||||
- [x] 3.2 接入企业微信最终通过、驳回、通过后撤销、提交失败和状态查询恢复:通过时一次性增加已核销并释放预占,驳回时释放预占,通过后撤销时不回滚已核销并将申请转异常终态、保留审计、禁止自动重提;用审批实例、审批尝试记录、状态条件更新和账单锁保证重复/乱序回调不重复核销;施工时列全四处注册点:业务类型常量、`validApprovalBusinessType` 与 `sceneBusinessFields`、数据库 CHECK、`cmd/worker/main.go` 的审批决策消费者注册。
|
||||
- [x] 3.3 实现代办权限、申请/分摊/审批历史查询及附件访问:附件只返回既有对象键引用并复用既有预签名下载能力,不承诺资源级鉴权;超级管理员代办创建、修改或重提时强制记录实际代办人和原因。
|
||||
- [x] 3.4 在既有套餐退款成功处理事务内实现账单冲销:以本次退款成功金额与账单应收(来源订单 `actual_paid_amount`)比较,等于应收且无已通过或审批中分摊时自动关闭、小于应收且无已通过或审批中分摊时冲减应收;存在已通过或审批中(预占)分摊时只写退款关联提示,不修改应收、已核销或预占;以 `bill_id + refund_id` 唯一事实幂等,不依赖退款事务的 `changed` 标志;不实现 AUG26-006 的退款审批、状态机或渠道退款。
|
||||
- [x] 3.5 为建账、申请提交/重提、审批通过/驳回/通过后撤销、账单关闭和退款冲销补齐事务内审计;检查日志、审计和错误不暴露附件内容、完整交易敏感体或 OCR 原始结果。
|
||||
|
||||
## 4. 路由、文档与验证
|
||||
|
||||
- [x] 4.1 注册账单(含统计聚合)、申请和字典所需路由及 RouteSpec,补齐 `internal/bootstrap`、`cmd/api/docs.go` 和 `cmd/gendocs/main.go` 装配;Handler 使用 `pkg/response` 和稳定错误;不新增导出路由。
|
||||
- [x] 4.2 按 ENG-TEST-001 在维护者指定的 `junhong_cmp_test` PostgreSQL + Redis DB 6 验证:迁移从本地工作区以显式 `DB_*` 执行 `scripts/migrate.sh`,只创建、删除本 Change 自己的 fixture,禁止重置整库,不额外要求独立数据库、Redis DB 或 namespace,仅连接、迁移或实际行为失败时才阻塞对应场景。人工核对:不存在按历史记录扫描或补建的路径、重复消费来源不重复建账、并发预占、审批通过/驳回/重提/通过后撤销/重放、关闭限制、退款三类联动(全额关闭、部分冲减、存在已通过或审批中分摊仅提示),以及迁移 up/down/up。
|
||||
- [x] 4.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-employee-collection-bills --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
Reference in New Issue
Block a user