# PRD：UR#37 企业微信审批公共能力

Status: ready-for-agent

---

## Problem Statement

当前退款和平台员工线下代充值分别维护本地审批动作，系统内存在通过、拒绝、退回、重提和确认入账等接口及页面。审批节点、审批人和业务终态混在各自 Service 中，无法复用企业微信已有的多级、会签、或签、意见、附件和待办能力，也容易在重复回调或异步失败时重复执行资金动作。

系统当前没有企业微信审批的稳定业务场景、模板版本、平台账号绑定、代理代提交身份、审批实例、回调解密、轮询补偿或统一运行查询。若直接在退款和充值代码中分别调用企微，将产生两套 Token、模板控件、回调、状态和幂等实现。

企微发起人还存在两类身份：平台账号发起业务时必须使用本人绑定的企微成员；代理账号不是企微成员，需要使用固定企微账号代提交。但无论实际调用企微的是谁，审批展示、通知、权限和审计中的申请人都必须是本系统真实业务提交人，不能把固定代提交账号误认为代理本人。

企微模板及控件 ID 会随模板编辑变化；远程申请又不能与本地数据库处于同一事务。系统必须解决模板安全切换、提交结果未知、重复回调、轮询并发、终态只处理一次、敏感配置保护和不同主体的信息可见性。

## Solution

建设一套企业微信审批公共能力，第一版固定服务退款审批和平台员工线下充值审批。企业微信负责模板、节点、审批人、会签/或签、通过/拒绝、意见、附件和企微待办；本系统只负责业务快照、真实业务提交人、企微发起身份、审批状态镜像、回调与轮询同步，以及把首次终态可靠交给对应业务用例。

业务代码只引用稳定场景码。模板 ID、控件 ID、模板详情和字段映射形成不可变版本；模板维护必须先暂停场景、等待在途提交租约释放，再发布新版本并恢复。场景不可用时，退款或线下充值创建在写入任何业务单、审批实例和 Outbox 前直接失败。

平台/超级管理员通过官方企业微信 Web 登录二维码绑定本人 `userid`；代理不绑定企微，由部署配置中的固定成员代提交。审批实例同时冻结真实业务提交人快照、实际企微发起身份及其来源。平台完整查看审批资料，代理只能在原业务数据范围内查看最小化审批摘要。

本地业务单与审批实例同事务创建并写提交 Outbox，由 Worker 上传附件副本并调用企微。回调和每 2 分钟轮询共同进入同一个状态同步用例，以 `getapprovaldetail` 为权威；首次进入终态时同事务写业务终态 Outbox，业务完成标记保证资金动作只执行一次。

## User Stories

1. 作为超级管理员，我希望查看企微连接和代理固定代提交账号是否就绪，以便在开放业务前发现配置问题。
2. 作为超级管理员，我希望按稳定业务场景管理退款与线下充值模板，而不是让业务代码依赖易变的模板 ID。
3. 作为超级管理员，我希望暂停场景后确认没有旧模板提交仍在运行，再去企业微信编辑模板。
4. 作为超级管理员，我希望读取企微模板并可视化映射业务字段，发布前由后端重新验证控件存在、类型和必填项。
5. 作为平台或超级管理员账号，我希望扫码绑定本人企微身份，以便我创建的审批由本人企微账号发起。
6. 作为平台账号，我只能查看、换绑或解绑自己的企微身份，不能查看其他员工的绑定清单或代其修改 `userid`。
7. 作为代理账号，我不需要也不能绑定企微；我创建退款时由系统固定账号代提交，但审批中仍显示我是业务提交人。
8. 作为业务提交人，我希望场景暂停、模板失效或企微身份不可用时在提交表单阶段立即得到明确失败，并保留已填写内容。
9. 作为审批人，我希望企微审批表单包含正确的业务编号、金额、原因、附件和真实业务提交人。
10. 作为运营人员，我希望在审批运行页按业务类型、状态、单号和处理结果定位审批，并查看最后一次权威同步的详情。
11. 作为有审批运营权限的平台人员，我希望手动触发一次详情同步，但不能在本系统执行通过、拒绝或退回。
12. 作为异常恢复人员，我希望对企微提交结果未知的记录安全地绑定已存在的 `sp_no`，或在确认企微未创建后重新发送，避免重复审批。
13. 作为真实业务提交人，我希望审批终态产生站内结果通知，不因实际企微发起身份是代理固定账号而把通知发错人。
14. 作为代理，我希望在有权查看的退款详情中看到申请人、审批状态、状态时间和业务处理结果，但看不到平台内部审批人、意见和审批附件。
15. 作为平台或超级管理员，我希望在具备原业务查看权限时查看审批人、意见、时间线和审批附件。
16. 作为财务人员，我希望企微通过与本地资金处理结果分开显示，避免把“审批通过但业务处理失败”误认为已经完成。
17. 作为审计人员，我希望模板发布、场景变更、账号绑定、异常恢复、回调和外部调用均有可追溯记录，且不泄漏密钥或附件内容。
18. 作为系统维护人员，我希望回调、轮询和重复任务共享同一幂等规则，避免一张业务单重复退款或重复入账。
19. 作为退款发起人，我希望企微拒绝后原退款单保持不可变；若纠正问题后仍需退款，我会创建一张全新的退款单和审批。
20. 作为历史数据查看者，我希望发布前已经结束的本地审批仍以只读历史事实展示，而不是伪造企微单号或审批节点。

## Implementation Decisions

### 范围与系统边界

- 第一版稳定场景固定为：
  - `refund_approval`：退款审批。
  - `offline_recharge_approval`：平台员工线下充值审批。
- 企业微信负责模板编辑、节点、审批人、会签/或签、通过、拒绝、意见、附件和企微待办。本系统不建立本地审批任务、候选审批人、节点配置或审批按钮。
- UR#37 交付公共企微配置、身份、场景/模板、审批实例、提交、回调、轮询、状态同步、运行 Query 和异常恢复能力。退款及线下充值的具体金额和资金终态分别由 UR#35、UR#34 的业务用例消费公共终态事件。
- 旧退款和线下充值本地审批入口不与企微长期并存；停机发布时由对应业务需求下线通过、拒绝、退回、重提、人工确认入账和操作密码入口。
- 在线微信/支付宝代理充值不进入企微审批；换货也不在本需求接入审批。

### 配置、Token 与敏感信息

- 企微连接通过 Viper 和环境变量配置 CorpID、AgentID、AgentSecret、回调 Token、EncodingAESKey、账号绑定回调地址、代理固定代提交 `userid`、请求超时、2 分钟审批轮询和 10 分钟模板验证间隔。
- Secret、Token、EncodingAESKey 和代理固定原始 `userid` 不进入通用配置表，不由后台在线编辑，也不得通过 API、日志、审计或文档返回。
- `GET /api/admin/wecom/status` 只返回配置就绪状态、最近连通时间、最近错误、回调最近成功时间、Token 最近获取时间、代理固定成员是否就绪及成员显示名。
- Access Token 缓存在 Redis，TTL 使用企微 `expires_in - 300秒`，通过 Redis 锁避免多进程并发刷新。
- `auth/getuserinfo`、成员读取、模板读取、附件上传、发起审批和审批详情查询都写 Integration Log，仅记录接口、耗时、企微错误码/摘要和请求关联信息，不记录令牌、密钥或完整附件。
- 正式接入前必须轮换曾出现在演示材料中的旧企微密钥，新密钥只能经部署配置提供。

### 审批场景与模板版本

- 场景状态是生命周期 `int`：`0=已暂停, 1=启用, 2=暂停中`。`暂停中` 由系统维护，前端不能直接写入。
- 模板版本状态是生命周期 `int`：`0=已停用, 1=启用, 2=失效`。
- 场景保存稳定 `scene_code`、中文名、状态、当前模板版本 ID、暂停原因和乐观锁版本；不建立数据库外键或 GORM 关联标签。
- 模板版本保存场景码、递增版本号、企微模板 ID、名称、控件映射、模板快照、指纹、验证结果、发布人和发布时间。历史版本不可修改；每个场景只能有一个启用版本。
- 模板发布流程为：暂停场景并停止发放新提交租约，等待所有未过期租约释放，读取新模板，完成业务字段到控件的可视化映射，后端重新调用企微校验，事务内停用旧版本、写新版本、切换当前版本并恢复场景。
- 退款稳定业务字段至少包含店铺、退款单号、订单号、资产标识、订单实收金额、可退款区间、固定申请退款金额、退款原因、申请备注、附件和真实业务提交人；除申请备注可为空外均需完成控件映射。金额控件只用于展示，审批人不得修改；线下充值至少包含店铺、充值单号、金额、备注、附件和真实业务提交人。
- 后端必须验证模板可访问、控件存在、控件类型匹配和所有必填业务字段已映射。前端不能提交控件类型或名称作为可信事实，也不直接编辑原始映射 JSON。
- 后台每 10 分钟验证启用模板。模板不可访问或指纹变化时暂停对应场景并告警；系统不能假设能从旧模板 ID 自动发现企微生成的新模板 ID。
- 场景处于暂停中、已暂停或当前模板失效时，创建退款或线下充值必须在任何业务单、审批实例或 Outbox 写入前失败。前端保留表单，恢复后用户重新发起创建请求；不留下待补提的孤儿业务单。
- 暂停前已成功创建的审批继续接收回调、参与轮询和终态处理，不因场景暂停而中止。

### 平台账号绑定与代理固定代提交

- 平台和超级管理员必须通过官方企微 Web 登录二维码绑定本人企微成员。只有已登录、启用的当前账号可为自己创建绑定会话；代理账号不能创建绑定会话。
- 绑定会话使用 Redis：随机 `state` 有效 5 分钟且通过原子读取删除保证单次消费；独立 `session_id` 保存目标账号、发起账号、固定后台 Origin、状态和错误，有效 10 分钟供原页面查询结果。
- 回调不接受前端传入的账号 ID。后端使用同一 CorpID 和自建应用 Access Token 调用 `auth/getuserinfo`；只返回 `openid` 的非本企业成员不能绑定。
- 账号 ID 与企微 `userid` 都一对一唯一。企微成员已绑定其他账号时拒绝覆盖，必须先由超级管理员强制解绑。换绑仍需本人重新扫码。
- 回调成功页只向服务端配置的后台 Origin 使用 `postMessage` 通知原窗口；通信失败时前端按 `session_id` 轮询兜底。Origin 不接受请求参数覆盖。
- 平台/超级管理员创建审批前，其本人绑定必须启用且成员可用；不满足时创建请求在业务落库前失败，并向前端返回可原地拉起绑定流程的错误状态。
- 代理账号不绑定企微。代理创建退款时使用部署配置中的固定成员代提交；该成员必须属于当前企业、启用并在应用可见范围内。未配置或失效时同样在业务落库前拒绝。
- 后台只展示固定成员是否就绪及显示名，不提供查看或修改原始固定 `userid` 的入口。
- 绑定、换绑和固定代提交成员变更只影响新审批。历史审批保留提交时的真实业务提交人、实际企微发起 `userid` 和身份来源快照。

### 真实业务提交人与身份语义

- `business_submitter` 是在本系统实际创建退款或线下充值的登录账号。它是列表、详情、通知、权限和审计中的“提交人/申请人”。
- `wecom_creator_userid` 是调用企微 `applyevent` 的实际成员。身份来源是方式类 `string`：`self_binding` 或 `agent_proxy`。
- 业务创建事务和审批实例同时保存真实提交人的账号 ID、账号名称、角色/店铺显示快照，以及实际企微发起身份及来源；前端提交的申请人字段不可信。
- 企微模板的 `submitter` 必填字段始终写真实业务提交人名称和账号标识。代理审批单不能只显示固定代提交账号。
- Worker 使用审批实例在创建时冻结的企微身份，不在真正发送时按账号当前绑定或当前配置重新选择。

### 审批实例与一单一审批

- 审批实例保存业务类型、业务 ID/编号、场景码、模板版本及映射快照、真实业务提交人快照、实际企微发起身份及来源、`sp_no`、提交业务快照、企微详情/审批人快照、提交错误、租约、同步时间、业务处理结果和乐观锁版本。
- `(biz_type, biz_id)` 唯一，`sp_no` 非空时全局唯一；业务表使用唯一 `approval_instance_id` 指向实例。不使用 `round_no`，不存在从同一业务单推算“当前审批轮次”的逻辑。
- 审批状态是生命周期 `int`：
  - `0=提交中`
  - `1=审批中`
  - `2=已通过`
  - `3=已驳回`
  - `4=已撤销`
  - `5=通过后撤销`
  - `6=已删除`
  - `7=提交失败`
  - `8=提交结果未知`
- 各 DTO 的状态 description 必须从公共 constants 原文复制，并返回对应中文 `status_name`；不得在退款、充值或列表模块复制第二套审批枚举。
- 一张退款单只对应一条企微审批。企微拒绝同时终结审批和当前退款单，原退款单不可修改、不可重提。若处理拒绝原因后仍需退款，重新调用创建退款接口，生成新的退款 ID、退款单号、业务快照、提交人快照和审批实例。
- 已拒绝退款不视为活跃退款；新建仍需拒绝同一订单或资产存在其他审批中或业务处理未完成的退款。企微撤销、删除或通过后撤销属于异常处置，不自动作为新建退款的放行条件。

### 异步提交、租约与附件

- 业务单、唯一审批实例和 `WeComApprovalSubmissionRequested` Outbox 在同一 PostgreSQL 事务创建。业务 API 返回本地业务 ID和“提交中”，不等待远程企微完成。
- Worker 通过状态和 `submit_lease_until` 条件更新领取短租约；租约覆盖对象存储下载、企微附件上传和 `applyevent`，处理期间续租，并通过 `defer` 释放。提交结果未知也必须先持久化再释放。
- 暂停场景先进入“暂停中”并停止新租约；只有未过期提交租约全部消失后才进入“已暂停”。
- 本地私有对象存储 Key 是附件权威引用。Worker 下载到受控临时文件后调用企微 `media/upload` 获得临时 `media_id`；不得把临时 `media_id`、预签名 URL 或敏感密钥写入业务快照作为永久依据。
- `applyevent` 固定使用企微模板审批人配置，不由本系统提交或计算审批节点。

### 提交结果未知与异常恢复

- 企微明确返回失败且确认未创建审批时，实例进入“提交失败”，修复配置后可对同一审批实例再次执行技术发送；这不是业务重提。
- 建连失败且可证明请求未发送时允许自动重试。
- 请求已发送后超时或连接中断时进入“提交结果未知”，禁止自动再次调用 `applyevent`。
- 异常恢复只允许超级管理员，或具备独立“企微审批异常恢复”权限的平台账号执行，并提供两个动作：
  - 绑定已有 `sp_no`：先查询企微详情并核对企业、模板版本、实际企微发起身份及来源、业务场景、真实业务提交人字段和业务快照，全部一致才允许绑定。
  - 确认企微未创建后重新发送：保留原尝试，在同一审批实例下记录新的技术提交尝试，不创建业务审批轮次。
- 两种恢复都要求填写恢复原因，保存原请求尝试、校验依据、操作人、前后状态和结果的高风险 Audit Event；Integration Log 不被覆盖。
- 企微已经通过、拒绝或进入其他明确终态后，不允许再对原业务单执行提交恢复。退款拒绝后的再次退款只能创建新业务单。
- 管理 API 采用明确动作接口：
  - `POST /api/admin/wecom/approvals/{id}/bind-sp-no`，请求包含 `sp_no` 与 `reason`。
  - `POST /api/admin/wecom/approvals/{id}/confirm-not-created-and-resend`，请求包含 `reason`。

### 回调、轮询与状态同步

- `GET /api/callback/wecom/approval` 按企微协议完成 URL 校验；`POST /api/callback/wecom/approval` 校验 SHA1 签名、AES-CBC 解密并核对 `receiveID=CorpID`。
- 回调只快速保存 Integration Log 和触发同步，然后按企微约定返回成功；回调事件中的状态不是最终业务权威。
- `getapprovaldetail` 是状态和审批详情的权威来源。回调和轮询都调用同一个 `SyncApprovalStatus` 用例。
- 轮询保持每 2 分钟扫描应轮询的审批中实例，单批最多 100 条并按最久未轮询优先。轮询通过租约或条件更新避免多实例重复占用。
- 状态同步保存审批详情、审批人名称快照、意见、附件元数据和时间线，以乐观锁更新状态。状态未变化时不产生第二个业务事件。
- 首次进入终态时，同一事务写对应业务终态 Outbox。业务 Worker 失败可重试，但 `business_processed_at` 非空后禁止再次执行资金动作。
- 审批状态和业务处理状态必须独立展示。审批已通过但退款或入账处理中/失败时，审批状态仍保持已通过。
- 通过后撤销且资金动作已经完成时不自动冲正，记录 `critical` Audit Event、站内告警并进入人工处理；尚未执行时阻止后续资金任务。

### 权限与信息可见性

- 连接状态、场景、模板版本、模板读取/发布、暂停/恢复、平台账号绑定列表和强制解绑仅超级管理员访问。
- 平台账号只能查询、重新绑定和解绑本人企微身份；代理无绑定能力。
- 审批运行列表、详情和立即同步仅超级管理员，或具备独立“企微审批运营”权限的平台账号访问。该权限不包含配置、模板、绑定管理或异常恢复。
- 提交未知恢复仅超级管理员，或具备独立“企微审批异常恢复”权限的平台账号访问。
- 代理无企微配置、绑定、审批运行列表/详情及恢复接口权限，只能按现有店铺层级数据范围读取退款业务详情中的审批摘要。
- 代理摘要只包含真实业务提交人、审批状态、状态时间和业务处理结果，以及其原业务权限允许查看的退款资料与业务凭证。代理不得看到审批人、内部意见、审批人上传附件或企微内部标识。
- 平台/超级管理员仍必须具备对应业务查看权限，才可读取完整审批人、意见、时间线和审批附件；企微运营权限本身不能绕过退款/充值数据权限。
- 详情、附件解析、受保护下载和导出复用同一主体权限投影。持有附件引用或历史导出链接不能绕过当前权限。
- 所有越权响应使用统一禁止访问错误，不能区分资源不存在与无权限。

### API 契约

- 连接及场景：
  - `GET /api/admin/wecom/status`
  - `GET /api/admin/wecom/approval-scenes`
  - `PUT /api/admin/wecom/approval-scenes/{scene_code}/status`，请求包含目标 `status`（只接受暂停或启用）与 `reason`。
- 模板：
  - `POST /api/admin/wecom/approval-templates/inspect`
  - `POST /api/admin/wecom/approval-templates/publish`
  - `GET /api/admin/wecom/approval-templates?scene_code=`
- 本人绑定：
  - `GET /api/admin/wecom/account-binding/me`
  - `POST /api/admin/wecom/account-binding/sessions`
  - `GET /api/admin/wecom/account-binding/sessions/{session_id}`
  - `DELETE /api/admin/wecom/account-binding/me`
  - `GET /api/callback/wecom/account-binding?code=&state=`
- 超级管理员绑定管理：
  - `GET /api/admin/wecom/account-bindings?page=&page_size=&binding_status=`
  - `DELETE /api/admin/wecom/account-bindings/{account_id}`
- 审批运行：
  - `GET /api/admin/wecom/approvals?biz_type=&status=&sp_no=&biz_no=&page=&page_size=`
  - `GET /api/admin/wecom/approvals/{id}`
  - `POST /api/admin/wecom/approvals/{id}/sync`
  - 两个提交未知恢复接口见上文。
- 企微审批回调：
  - `GET /api/callback/wecom/approval`
  - `POST /api/callback/wecom/approval`
- 列表默认每页 20、最大 100；查询保持稳定排序，审批运行页默认按更新时间倒序并以 ID 作为并列排序键。
- 所有后台接口使用统一 `{code,msg,data,timestamp}` 响应，列表使用项目统一分页结构；参数验证失败返回统一参数错误，不向调用方泄漏底层企微、数据库或验证器错误。

### 前端页面与交互

- `/system/wecom` 仅超级管理员可见，包含连接状态、代理固定成员就绪状态、审批场景、模板版本、可视化控件映射、平台账号绑定清单和异常审批入口；不提供 Secret 或固定 `userid` 编辑。
- 平台/超级管理员个人中心展示本人绑定状态、成员名称、最近验证时间、绑定/换绑/解绑；代理不展示绑定入口。
- 平台/超级管理员创建需审批业务时若未绑定，在当前表单原地提供扫码入口；成功后继续填写。代理固定成员不可用、场景暂停或模板失效时保留表单并展示后端明确原因。
- `/operations/wecom-approvals` 及稳定详情路由用于运行监控，展示业务类型/编号、`sp_no`、审批状态、模板版本、真实业务提交人、实际企微身份来源、更新时间、业务处理结果和异常标识。
- “立即同步”只调用详情同步，不执行审批动作。提交未知记录按恢复权限展示两个高风险动作及二次确认；业务处理失败只展示错误摘要和任务状态。
- 业务详情复用统一只读 `approval` 区块。平台按业务权限显示完整详情；代理使用最小投影。所有页面均覆盖加载、空、失败、无权限、场景维护、绑定过期和高风险异常状态。
- 页面不得出现本地通过、拒绝、退回、审批金额修改或人工确认退款按钮。

### 架构与迁移

- 场景、模板版本、审批状态机、提交租约、身份绑定和终态幂等属于复杂写，采用 `Handler → Application UseCase → Domain → Repository/WeCom Adapter`。
- 审批运行列表、详情、业务摘要和权限投影走 Query，可直接使用 GORM 做分页、批量关联与 DTO 投影，不经过聚合根。
- 企业微信 Token、身份、模板、媒体、审批和回调加解密统一封装在 WeCom Adapter；退款和充值不得各自调用企微 HTTP API。
- 不建立数据库外键，不使用 GORM 关联标签；关联 ID 在 Application/Domain 中显式维护。
- 停机发布时创建场景、模板版本、平台绑定和审批实例数据结构，配置真实企微回调/可信域名，验证平台发起人绑定和代理固定成员，再发布模板映射、Worker、业务接入和前端。
- 已结束的历史本地审批保留为 `legacy` 只读事实，不伪造企微实例。发布时仍未结束的退款/线下充值按创建人类型形成真实企微审批；缺少平台绑定、固定代理身份或创建人事实的记录进入迁移待处理清单，不能继续使用旧本地审批接口。
- 回滚应用时保留已经形成的企微实例、提交人/身份快照、Integration Log 和 Audit Event。已有企微申请进入运行后不得恢复旧本地审批动作，只能继续同步或人工处置。

## 公共能力发布依赖

- 最终 14 号真实企微发布票阻塞于公共基础 12 号票、全局审计 19 号票和公共通知 08 号票；退款业务消费者还必须替换为 UR#35 的具体纵向 Ticket。
- 审批终态事件必须登记版本和消费者幂等键；审批实例是领域事实，企微请求/回调进入 Integration Log，提交、人工同步和状态变化进入 Audit Event。

## Testing Decisions

- 主要自动化接缝采用最高公共行为边界：Fiber HTTP 路由与认证 → Application/Domain/Query → GORM → 开发 PostgreSQL、Redis 和 Asynq 可控队列；企业微信网络统一替换为可编程 WeCom Adapter。测试不直接断言私有函数或目录结构。
- 回调测试从真实 HTTP 回调入口注入使用测试 Token/AES Key 生成的签名密文，验证 URL 校验、签名错误、解密错误、错误 CorpID、重复事件、快速响应和统一状态同步，不绕过回调协议直接调用内部函数。
- 使用现有测试 PostgreSQL 与本地 Redis DB 7 验证迁移、唯一约束、绑定会话、Token 锁、提交租约、轮询领取和幂等；测试启动时必须校验 Redis Client、Asynq Client 和 Worker Server 的实际 DB 均为 7，禁止向已部署测试环境使用的 DB 6 投递任务，也不得执行 `FLUSHDB`。测试及日志不得输出任何连接密码或企微密钥。
- WeCom Adapter 契约测试覆盖 Token 获取/刷新、成员身份、模板读取、附件上传、发起审批、详情查询，以及企微非零错误、超时、响应丢失和限流。自动化测试不依赖真实企微网络。
- 场景测试覆盖首次发布、暂停中停止发租约、租约自然释放/续租、已暂停、新版本原子切换、模板不可访问、指纹变化、必填映射缺失、控件类型不匹配和发布失败保持暂停。
- 身份测试覆盖平台本人扫码、代理禁止绑定、非企业成员、过期/重复 `state`、同一成员绑定第二账号、换绑、自助解绑、强制解绑、固定代理成员可用/失效，以及历史实例不随绑定变化。
- 业务创建前置测试证明场景暂停、模板失效、平台未绑定、代理固定身份不可用时，不产生业务单、审批实例或 Outbox；前端重试成功后只产生一组事实。
- 提交测试覆盖 Outbox 重投、并发 Worker、租约过期恢复、附件上传部分失败、明确企微失败、确认未发送的连接失败、已发送响应未知和成功返回 `sp_no`。
- 异常恢复测试覆盖无权限、错误状态、`sp_no` 不存在、企业/模板/身份/场景/提交人/业务快照任一不匹配、成功绑定、确认未创建后重新发送、重复操作和高风险审计完整性。
- 状态同步测试覆盖审批中、通过、拒绝、撤销、删除、通过后撤销、详情无变化、回调与轮询并发、重复终态以及乐观锁冲突；验证每个业务终态只产生一次 Outbox。
- 轮询测试覆盖 2 分钟资格、每批 100、最久未轮询优先、多实例并发领取、详情失败重排和回调先到后轮询不重复处理。
- 权限测试覆盖超级管理员、无专项权限平台、仅企微运营平台、仅异常恢复平台、同时具备权限平台和代理；证明企微运营不能维护模板或恢复未知，代理不能访问公共企微接口。
- 信息投影测试对同一退款分别以平台和代理身份读取：平台在具备业务权限时可见审批人/意见/审批附件，代理只见最小摘要；无业务权限的平台即使有企微运营权限也不能读取该业务资料或附件。
- 一单一审批测试验证 `(biz_type,biz_id)` 并发唯一性。企微拒绝后原退款详情只读、旧重提接口不存在；再次退款经创建接口生成新 ID、退款单号、提交人快照和独立审批，新旧单互不继承审批资料。
- 日志与响应测试验证 Secret、Token、EncodingAESKey、对象 Key、临时 `media_id`、原始固定 `userid` 和完整附件不会出现在 API、日志或审计详情中。
- 停机迁移测试覆盖历史终态标为 `legacy`、待审批按平台/代理身份迁移、缺失绑定进入待处理、重复执行不重复创建实例，以及旧审批路由确实不再注册。
- 前端人工验收覆盖连接配置、模板维护、暂停等待、本人扫码绑定、代理代提交、审批运行筛选、立即同步、两种异常恢复、权限菜单、详情投影、通过后撤销红色告警以及所有加载/空/失败状态。
- 真实企微验收是企微公共能力及首个接入业务的实现完成门禁，不得只推迟到后续联调。至少使用真实模板完成平台本人发起、代理固定账号代发、真实附件、通过/拒绝、加密回调和回调丢失后的轮询兜底；回调可按“企业微信 → 用户提供的中转应用 → 本地服务”原样转发，后端仍完整验签解密。自动化 Adapter 继续覆盖超时、响应未知、重复/乱序、撤销、删除、通过后撤销和限流等难以稳定人工制造的异常。真实参数和验收记录不得泄漏密钥或个人敏感信息。

## Out of Scope

- 不建设本地通用审批流引擎、BPMN、节点设计器、部门模型、直属领导推导、待我审批或本地审批按钮。
- 不让普通运营手工录入平台员工或代理固定账号的企微 `userid`。
- 不为代理账号建立企微绑定，也不把固定代提交账号当成真实业务提交人。
- 不在 UR#37 内定义退款金额、代理钱包回溯或线下充值入账的完整业务规则；这些由 UR#35、UR#34 消费公共终态事件实现。
- 不把代理在线微信/支付宝充值或换货接入审批。
- 不调用微信、支付宝等支付渠道自动退款。
- 不在企微审批人端修改退款金额；企微只同意或拒绝提交时固定的业务快照。
- 不允许企微拒绝后的原退款单编辑或重提，不保留 `resubmit` 兼容接口。
- 不因通过后撤销自动冲正已经完成的资金动作。
- 不向代理公开平台内部审批人、意见、审批附件或企微内部标识。
- 不把企微临时 `media_id`、预签名 URL 或外部原始响应当作永久业务资料。

## Further Notes

- 当前代码尚无企微审批公共模块；退款仍注册本地 `approve/reject/return/resubmit`，旧线下充值也保留本地审核路径。实现必须以停机切换后的契约为准，不能把现状默认为最终产品决定。
- 当前退款活跃校验已把待审批和已通过视为活跃；新模型还需覆盖审批通过后的业务处理未完成。已拒绝退款明确不活跃，因此允许同一订单在纠正问题后重新创建新退款单。
- UR#44 列表审批摘要、UR#42 审批附件导出、UR#35 退款终态和 UR#34 线下充值终态都依赖本公共能力；它们必须复用统一实例、状态常量、Query、附件权限和 WeCom Adapter，不能各自建立替代模型。
- 该需求横跨配置、身份、模板、远程提交、回调、轮询、运行 Query 与安全边界，预计超过一个高质量实现上下文。进入实现前应基于本 Spec 评估并拆分窄的端到端 tracer-bullet tickets，而不是按 Model/Service/Handler 水平切层。
