# PRD：UR#44 退款、代理充值与换货列表补充提交人和审批摘要

Status: ready-for-agent

---

## Problem Statement

当前退款、代理充值和换货列表看不到创建业务单的账号名称，运营人员需要进入详情或查询日志才能判断是谁提交。退款与平台员工线下充值接入企业微信审批后，业务列表还需要同时区分“企微审批状态”和“审批通过后的本地业务处理状态”，否则会把已通过但仍在退款/入账中的业务误认为已经完成。

现有三张业务表只保留了提交人账号 ID，没有不可变的提交人名称快照；当前仓库也尚未实现七月企微审批实例。列表扩展必须依赖统一企微审批 Query 批量读取当前页摘要，不能创建另一套本地审批流、逐行查询企微或写死“部门领导/财务”等节点。

## Solution

在退款、代理充值和换货业务表增加 `submitter_name` 快照，新业务创建时与业务单同事务保存当前账号用户名。对存量记录按原提交人账号 ID 读取包括软删除记录在内的最后用户名并一次性回填，账号确实不存在时写“未知账号”。

复用现有三个列表接口。退款和代理充值列表按当前页业务记录批量读取统一企微审批实例摘要，返回审批来源、审批状态以及本地处理状态；当前审批人摘要按主体权限投影，只有平台/超级管理员按业务权限可见，代理响应必须为空。换货当前不走企微审批，只返回提交人，并以 `approval_source=none` 明确表示不适用。

## User Stories

1. 作为运营人员，我希望在退款、充值和换货列表直接看到最初提交账号。
2. 作为财务人员，我希望区分企微仍在审批、企微已经通过以及本地退款/入账是否完成。
3. 作为代理在线充值的查看者，我希望审批列明确显示不需要审批，而不是误显示为待审批。
4. 作为历史数据查看者，我希望老记录保留可解释的提交人和“历史审批”标识，但不出现伪造的企微节点。
5. 作为系统维护人员，我希望一页审批摘要使用批量查询，不因列表 100 条产生 100 次数据库或企微请求。

## Implementation Decisions

### 接口范围

- 扩展现有接口，不新增专门的列表或审批摘要接口：
  - `GET /api/admin/refunds`
  - `GET /api/admin/agent-recharges`
  - `GET /api/admin/exchanges`
- 保持三个接口现有分页、筛选、排序和数据权限不变；新增展示字段不能扩大可见数据范围。
- 列表只返回展示所需的审批摘要。完整企微意见、附件元数据和时间线继续在业务详情或统一企微审批详情中查询。

### 提交人快照

- `tb_refund_request`、`tb_agent_recharge_record`、`tb_exchange_order` 增加非空 `submitter_name varchar(50)`，含义固定为“业务单首次创建时的系统账号用户名快照”。
- 新建业务单时从已认证上下文获取当前账号 ID 和用户名，在创建业务单的同一个 PostgreSQL 事务中保存；禁止依赖前端传值。
- 一张退款单只保存首次创建时的提交人快照，并只对应一条企微审批申请。企微拒绝后原退款单不可修改或重提；后续仍需退款时创建新退款单，新单按当次认证上下文保存新的提交人快照和独立企微审批事实。
- 后续账号改名、禁用、解绑企微或软删除都不修改已保存的业务提交人快照。
- 用户已确认存量数据执行回填：
  - 退款按 `BaseModel.creator` 定位账号。
  - 换货按 `BaseModel.creator` 定位账号。
  - 代理充值按 `user_id` 定位账号。
  - 查询账号必须包含软删除记录，并按账号主键精确匹配，不能按可能被重新注册的用户名反查。
  - 找到账号时写其最后保存的 `username`；账号 ID 为 0、账号记录缺失或用户名为空时写固定文案“未知账号”。
- 回填脚本需先输出总数、成功匹配数和未知账号数供核对，再执行可重复的条件更新；已有非空合法快照不覆盖。
- 列表 `submitter_name` 始终返回非空字符串，新数据也不得以空字符串代替未知账号。

### 统一列表字段

- 三类列表项统一增加以下字段，均为必返字段：

```json
{
  "submitter_name": "zhangsan",
  "approval_source": "wecom",
  "approval_status": 4,
  "approval_status_name": "审批中",
  "current_approver_summary": "财务审批：李四",
  "processing_status": 0,
  "processing_status_name": "未触发"
}
```

- `approval_source` 是方式类字符串枚举：
  - `none`：该业务不需要审批。
  - `wecom`：存在当前有效的企微审批实例。
  - `legacy`：停机切换前已经结束、没有企微实例的历史本地审批。
- `approval_status`、`processing_status` 是生命周期状态 int；具体数字和值名必须复用 UR#37 企业微信审批公共常量及 UR#34/35 充值、退款处理常量，DTO description 从 constants 原文抄写，不在 UR#44 创建第二套枚举。
- `approval_source=none` 时：`approval_status=0`、`approval_status_name="无需审批"`、`current_approver_summary=""`。
- `approval_source=legacy` 时返回可由原业务事实证明的历史终态名称，`current_approver_summary=""`；不得用旧 `processor_id` 伪造成企微当前审批人或多节点流程。
- `processing_status` 描述企微终态之后本地退款、钱包回溯或充值入账的结果，不能拿业务表原有总状态冒充。尚不适用或尚未触发时统一为 0，并返回对应 `_name`。
- 换货当前固定返回 `approval_source=none` 及上述不适用值；不因为字段结构统一而把换货接入审批。

### 不同业务的审批来源

- 退款：
  - 七月切换后的退款使用 `wecom`，摘要按业务表明确保存的唯一 `approval_instance_id` 读取该退款单的审批申请；一张退款单只有一条审批，不存在按轮次或创建时间猜测“当前审批”的逻辑。
  - 发布前已经结束且无企微实例的历史退款使用 `legacy`。
  - 发布时仍未结束的退款必须按企微方案在维护窗口形成真实企微实例，不能继续作为本地待审批。
- 代理充值：
  - 代理本人发起的微信/支付宝在线扫码充值固定 `approval_source=none`。
  - 平台员工创建的线下代充值使用 `wecom`。
  - 发布前已经结束且无企微实例的历史线下审批使用 `legacy`。
- 换货：固定 `none`，本需求只增加提交人。
- 判定审批来源必须基于业务类型、支付方式及真实实例关联，不能仅根据业务状态猜测。

### 当前审批人摘要

- `current_approver_summary` 只在企微实例处于审批中且存在未完成审批节点时返回，其余状态为空字符串。
- 对代理账号，无论企微实例状态如何，`current_approver_summary` 固定返回空字符串；代理不得从列表获取平台内部审批人信息。平台账号和超级管理员仍需先具备该业务单的查看权限，才能按以下规则得到摘要。
- 数据来自最后一次权威 `getapprovaldetail` 保存的节点与审批人快照，不在列表请求中实时调用企业微信。
- 摘要按企微节点顺序稳定生成，至少包含当前节点名称；能确认待处理成员时追加成员名称，多人使用“、”连接，并保留会签/或签语义。
- 多个当前节点并行时使用“；”连接。字段设定最大安全长度，超出时由后端返回确定性截断摘要并在末尾显示总人数，前端用省略号和悬浮展示完整返回值。
- 不展示企微 `userid`、手机号或其他内部标识；成员名称取审批详情保存的快照，不能用当前账号绑定覆盖历史名称。

### Query 与性能

- 三个列表属于读取 Query，不经过聚合根。先按原条件分页查询业务数据，再收集本页所有当前企微实例 ID，一次批量读取实例及审批人摘要并在内存按业务键合并。
- 禁止对每条业务记录调用一次实例查询、账号查询或企业微信 API。
- `submitter_name` 直接读取业务表快照，列表运行时不再联表解析账号；账号表只用于一次性迁移和新建时兜底校验。
- 统一企微 Query 应提供按实例 ID 批量返回摘要的稳定接口，供退款列表、充值列表和 UR#42 导出复用，避免各模块复制 JSON 快照解析逻辑。
- 列表 `Count` 查询保持原过滤条件，不加入审批详情联表；数据查询的额外审批批量查询仅作用于当前页。
- 列表最大 100 条时仍应满足项目 P95/P99 目标，并通过查询计数测试证明不存在 N+1。

### 权限与敏感信息

- 业务列表沿用现有 GORM 数据范围和业务权限；企微摘要 Query 只能接收已经通过业务列表权限过滤的实例 ID。
- 无权查看某条业务单的账号不得通过实例 ID直接探测审批状态、审批人或业务处理结果。
- 列表不返回审批意见正文、附件 Key、附件 URL、企微 `userid`、外部原始响应或支付通道敏感信息。
- 代理列表除不返回上述内容外，也不返回当前审批人摘要；平台内部审批身份不能因统一 DTO 或批量 Query 泄露给代理。
- “未知账号”只表示原提交账号事实无法恢复，不暴露账号是否已物理删除或迁移异常。

### 前端展示

- 三个列表增加“提交人”列，直接展示 `submitter_name`。
- 退款与充值列表增加“审批状态”“当前审批人”“业务处理状态”；换货列表不增加无意义的审批列，尽管后端为统一 DTO 返回 `none`。
- `approval_source=none` 时审批区域显示“-”或隐藏；`legacy` 显示“历史审批”标签，只读且不提供企微操作；`wecom` 展示后端状态名称。只有平台/超级管理员响应中存在 `current_approver_summary` 时才展示当前审批人，代理不展示该列或固定显示“-”。
- 企微已通过但 `processing_status` 仍为处理中或失败时，审批列必须显示“已通过”，处理列独立显示“处理中/失败”，不能合并成“待审批”。
- 处理失败时列表提供进入详情或统一企微运行页面的受控入口，但不在列表增加本地通过、驳回、退回按钮。

### 发布与回滚

- UR#44 的企微摘要依赖 UR#37 统一企微实例 Query，以及 UR#34/35 的充值、退款业务处理状态；依赖能力未上线时不得伪造静态摘要。
- 发布顺序为：增量字段与历史回填 → 企微公共能力和业务终态字段 → 后端列表 Query → 前端列表展示。
- 发布前核对三表记录总数、提交人回填命中率、未知账号清单、历史审批分类和发布时未结束业务单迁移结果。
- 回滚应用时保留 `submitter_name` 快照和已经产生的真实企微关联；不得删除或回写历史业务事实。旧版本忽略新增列即可。

## Testing Decisions

- 新建测试验证退款、在线充值、线下充值和换货均从认证上下文保存正确用户名，前端伪造字段无效，事务失败不留下半条快照。
- 回填测试覆盖正常账号、软删除账号、用户名后来修改、账号 ID 为 0、账号物理缺失、空用户名、已有快照以及重复执行迁移。
- 退款列表测试覆盖 `wecom` 审批中/通过/驳回/撤销/异常状态、`legacy` 历史终态和本地处理未触发/处理中/成功/失败。
- 充值列表测试覆盖在线微信/支付宝固定 `none`、员工线下充值 `wecom`、历史线下充值 `legacy`，以及审批通过但入账失败不会显示为全部完成。
- 换货列表测试验证提交人正确且审批来源固定 `none`，不会创建或查询企微实例。
- 当前审批人测试覆盖单节点、多人会签、多人或签、并行节点、已完成节点、成员名称快照、超长摘要和无当前节点。
- 性能测试统计一页 1、20、100 条数据的 SQL 次数，确认账号不逐行查询、企微实例与审批人按页批量查询、列表请求不调用企微网络接口。
- 越权测试验证平台代理、店铺层级和企业数据范围保持不变，不能用实例 ID 或摘要批量 Query 获取其他租户审批信息。
- 信息最小化测试验证同一退款列表记录在平台视角可返回当前审批人摘要，而代理视角字段固定为空；代理不能通过排序、筛选、导出或其他列表接口旁路获得审批人、意见或审批附件。
- HTTP 集成测试穿过 Fiber、认证、Query、GORM/PostgreSQL 和统一响应，验证所有新增字段必返、类型稳定、`status_name` 与 constants 一致。
- 前端验收覆盖三类列表、历史审批标签、无需审批空态、长审批人摘要、审批与处理状态分列以及错误/空列表状态。

## Out of Scope

- 不为换货增加企业微信审批。
- 不建设本地审批任务、审批人配置或列表审批按钮。
- 不在列表返回完整审批意见、附件和时间线。
- 不为历史业务伪造企微实例、企微单号、审批节点或审批人。
- 不改变退款、充值、换货原有分页筛选和数据权限。
- 不把账号当前用户名作为新业务单提交人快照的动态展示值。

## Further Notes

- 当前退款和换货分别已有 `creator`，代理充值已有 `user_id`，但三者均没有用户名快照；历史回填有可靠主键来源。
- `tb_account` 使用软删除，用户名唯一约束只覆盖未删除记录，因此回填必须按账号 ID 无作用域读取，不能按用户名反向猜测。
- 当前仓库没有七月企微审批实例表；审批摘要只能在 UR#37 公共能力落地后实现。
- 用户已明确确认历史提交人执行回填，缺失账号显示“未知账号”。
