## Context

本 Change 以 `docs/7月迭代/七月迭代需求范围确认表.md` 为业务范围，以 `docs/7月迭代/物联网卡管系统-需求.csv` 为原始来源，以 `docs/7月迭代/企业微信审批官方接口调研.md` 为企微能力依据。旧 Change 已完成部分公共能力，但其剩余任务存在漏项、错误口径和过度 DDD 迁移，不能继续作为实施顺序。

当前仓库已经具备可复用的通用审批实例、标准决策同步、公共 Outbox/Relay、异步任务、对象存储、站内通知、`system_config`、钱包流水和 Integration Log。本设计只补业务接线和确实缺失的企微 Adapter，不重建这些基础设施。

涉及方包括后端、前端、测试、部署人员，以及企业微信和 Gateway 配置负责人。当前仓库没有前端源码，因此本 Change 交付后端接口、OpenAPI 契约和联调门禁。

## Goals / Non-Goals

**Goals:**

- 按已确认口径覆盖所有“本期做”需求，并核验所有“已完成，只联调/验收”需求。
- 让简单字段、筛选、配置和 Bug 保持简单，能够独立开发、独立验证、独立上线。
- 让退款和员工线下代充值真正通过企微模板审批闭环，同时保留金额、幂等和终态处理的现有安全边界。
- 冻结前后端需要的字段、接口、状态和错误语义，避免实现阶段再次扩张范围。

**Non-Goals:**

- 不继续执行旧 Change 中未被本 Change重新列出的任务。
- 不做代理在线扫码充值、原路退款、聚水潭、跨品类换货、分销码/佣金提现、自动限速、通用营销/ERP、本地审批流程设计器、未来钉钉适配或全局 Audit Event 专项。
- 不迁移未触碰的旧模块，不因为新增字段或筛选建立聚合、Repository 接口、事件消费者或历史投影。
- 不强制吊销已登录 C 端 Token；店铺限制只阻止新的登录流程。
- 本次执行不编写或补齐测试代码，不运行测试、构建、LSP、迁移、OpenAPI 生成或真实外部环境验收；实现收口到生产代码可联调状态。

## Decisions

### 1. 新 Change 是后续实施的唯一契约

本 Change 校验通过后，`complete-july-iteration-test-release` 只作为历史完成证据归档，不再从其中领取任务。已完成代码不会回滚或重复实现；本 Change 的 tasks 会先做一次证据核验，再只实现真实缺口。

**理由**：旧 Change 的 task 勾选、proposal/spec 和仓库状态已经漂移，继续补丁式修订容易再次执行错误任务。

**否决方案**：在旧 Change 上继续增删 191 个任务。该方案无法可靠区分已完成公共能力、错误口径和本期真实需求。

### 2. 按纵向切片选择最小架构通道

| 需求切片 | 主通道 | 完整业务边界 | 明确不迁移范围 |
| --- | --- | --- | --- |
| #189、#181、#57 | 旧 `Handler → Service → Store → Model` | 对现有换货、退款、订单路径做局部修复和拦截 | 不迁移换货、退款或订单模块 |
| #182、#44、#53 | 既有 Query 或旧 Store 查询 | 列表/详情字段、实名筛选、批量账号名称解析 | 不建聚合、历史投影或事件消费者 |
| #41、#62、#48 | 简单写 + 既有读取 | 店铺开关、实名策略、系统配置的更新和查询 | 不建领域模型，不重构认证/支付模块 |
| #188、#97、#33 | Application/既有通知 Adapter | 产生明确业务事件后向既有站内通知接线 | 不建营销平台或新通知中心 |
| #36、#42、#49 | 既有异步导入/导出 Infrastructure + 业务 Service | 文件校验、异步处理、结果文件和业务写入 | 不再建第二套任务、对象存储或进度平台 |
| #47 | 旧 IoT 卡 Service + Gateway Adapter | 固定档位校验、卡 ICCID 权限解析、一次 Gateway 调用 | 不提供设备限速，不通过设备绑定关系间接限速，不自动计算运营商策略，不迁移资产模块 |
| #34、#35 | 复杂写 Application/Domain | 申请快照、审批终态、充值入账或退款终结 | 不把企微 DTO/状态码写入资金领域 |
| #37 | Infrastructure Adapter + Application Port | 通讯录、模板、提交、回调、轮询和状态翻译 | 不建设本地审批流引擎或继续抽象未来渠道 |
| #40 | 旧 Service/Query | 下架套餐续费资格与可购列表过滤 | 不迁移套餐或订单全模块 |

依赖通过构造函数结构体字段注入。旧 Service 保持现有注入方式；企微 Adapter 实现现有 `internal/application/approval` Port，由 bootstrap 同时注入 API、回调和 Worker。新增 Handler 必须同步两个文档生成器。

### 3. API 只表达当前业务，不暴露通用平台

所有接口继续使用 Fiber、Validator、`pkg/errors` 和 `{code,msg,data,timestamp}` 统一响应；既有项目字段名为 `msg`，不引入另一套 `message` 字段。列表默认 20、最大 100，平台/代理数据权限沿用现有中间件与 GORM Callback。

关键契约如下：

| 场景 | 接口决定 |
| --- | --- |
| 店铺 C 端登录限制 | 店铺模型和管理接口增加 `client_login_disabled`，默认 `false`；复用店铺更新接口或增加单一语义 PATCH。C 端 `verify-asset` 在签发 asset token 前按资产 `shop_id` 拦截 |
| 企微账号绑定 | 账号列表/详情返回 `wecom_corp_id`、`wecom_userid`、`wecom_name`；管理员更新账号时选择通讯录成员，不提供个人扫码接口 |
| 企微通讯录 | 提供分页成员选择接口，支持姓名或 userid 搜索；只展示应用可见成员，不依赖手机号/邮箱 |
| 企微场景配置 | 管理员配置 `business_type → template_id` 及业务字段到控件 ID/类型/选项 key 的映射；模板必须先在企微后台创建 |
| 实名顺序 | 保持已约定的单资产 PATCH、卡批量 POST、设备批量 POST；枚举仅为 `none/before_order/after_order`，批量上限 500、全成全败 |
| 列表人员字段 | 退款、充值、换货返回提交人 ID/名称；审批人仅从已同步企微详情中的 userid 批量映射，映射不到返回空，不实时调用企微 |
| 批量订购 | CSV 只有资产标识列；请求额外选择一个套餐和一个支付方式，不选择代理；整批使用相同参数 |
| 导出 | 复用现有导出任务入口，通过六个 datasource code 区分；每类只输出本业务已有且已确认的字段 |
| 支付方式 | `system_config` 分别保存卡和设备允许的支付方式；两类资产默认均启用 `wallet/wechat/alipay`，三项可独立取消但至少保留一项；C 端返回当前购包场景有效集合，创建订单时后端再次校验 |
| 设备批量分配 | 复用现有导入任务 API 和状态模型，CSV 单列设备标识；业务参数选择“代理”或“套餐系列”及目标 ID |

### 4. 店铺登录限制在资产令牌前拦截

`client_auth.Service.VerifyAsset` 解析出卡或设备后取得资产 `shop_id`，再读取店铺开关。无店铺/平台库存保持当前行为；开关关闭时返回统一禁止错误，不签发短期资产令牌，因此不会进入微信登录、客户创建或资产绑定。

不在每个 C 端业务接口重复查询店铺开关，也不吊销已有 Token。这严格对应“不能登录”而不是“立即踢下线”，并避免在本期引入 Token 版本和全局会话撤销。

### 5. 企微只做 Adapter，审批流程仍由企微模板拥有

系统按应用保存加密 Secret、回调 Token、EncodingAESKey、`corp_id/agent_id` 和一个从当前可见成员中选择的默认审批发起人；`access_token` 按应用缓存并预留提前刷新。管理员从应用可见通讯录选择成员，账号绑定键为 `(corp_id, userid)`，姓名和部门仅为展示快照。内部员工已绑定且仍可见时优先以本人发起，代理等非企微账号始终以应用默认成员发起；本地申请仍保留真实业务提交人，不用默认成员冒充业务操作者。

场景配置保存已知 `template_id`。保存或发布时调用 `oa/gettemplatedetail` 校验控件 ID、类型、必填项和选择项 key。系统不创建模板、不保存审批节点、不计算部门领导或财务人员；`oa/applyevent` 使用 `use_template_approver=1`，审批人由企微后台模板决定。

提交过程：

1. 业务用例校验内部提交人的企微绑定或应用默认发起人仍可见、场景已启用且模板结构有效。
2. 退款或线下充值在同一 GORM 事务保存业务申请、通用审批实例和提交 Outbox。
3. Adapter 消费提交事件，上传必要附件并调用 `oa/applyevent`，保存返回的 `sp_no` 到通用实例 `external_ref`。
4. 外部调用记录 Integration Log；结果未知时不盲目创建第二张审批单，转人工/查询恢复。
5. 回调端点在 5 秒内完成验签解密、必要幂等记录和快速响应，再异步调用 `getapprovaldetail`。
6. Adapter 将企微状态翻译为现有标准决策；现有决策 Outbox 分别驱动退款终结或线下代充入账。
7. 定时任务扫描未终态实例，并用批量拉取审批单号与详情查询补偿回调遗漏。

企业微信审批详情的审批节点 userid 可以用于页面展示，但只做读取投影；不能映射系统账号时返回空名称，不影响业务终态。

### 6. 通知只复用现有站内能力

- 创建物流换货单成功后，向关联个人客户写一次站内通知；C 端按既有未读弹窗机制展示。
- 店铺主钱包余额从不低于 100 元变为低于 100 元时，向 `business_owner_account_id` 对应后台账号发送一次通知；余额恢复到阈值以上后允许下一次下降再次提醒。
- 每日扫描预计套餐到期日在 15、7、3 天节点的资产，生成后台/代理临期列表所需标识，并向关联个人客户发送节点通知。相同资产、节点、到期日只通知一次。

通知属于可靠副作用时使用现有 Outbox 和幂等键；不新增短信、企微业务员提醒或自定义营销内容。

### 7. 批量与导出复用已完成基础设施

批量订购和设备分配只新增业务解析器/执行器，继续使用现有对象存储、任务五态、Asynq 重试、失败明细和下载能力。文件级校验失败不写业务数据；涉及扣款/订购时按现有订单幂等键和钱包流水保证不重复扣款。

六类导出分别实现 datasource，查询直接投影 DTO，不串联多个业务迁移。不存在的字段不得临时创造含义；先从既有数据组合，确认确实缺失后再增加最小字段和迁移。

### 8. 数据、事务、常量与缓存

- 新表/字段通过 PostgreSQL 增量迁移添加，不建外键，不写 GORM 关联标签。
- 简单列表字段使用批量 `WHERE id IN (...)` 或 JOIN 投影，禁止逐行查询账号。
- 批量实名策略在单事务内校验全部资产后统一更新；任何冲突则全部回滚。
- 退款和充值金额继续使用现有 Domain Ledger/钱包流水与乐观锁；审批 Adapter 不直接修改余额或退款金额。
- 所有新枚举、任务类型、场景编码、导出 datasource code 和 Redis key 在 `pkg/constants` 定义并写中文注释。
- 企微 token 使用 Redis 缓存和短锁；Redis 不可用时允许受控回源，但不得把 token 返回前端。

### 9. Audit Event、Domain Ledger、Integration Log 与 Outbox 决策

| 切片 | Audit Event | Domain Ledger | Integration Log | Outbox |
| --- | --- | --- | --- | --- |
| 字段、筛选、Bug、只读导出 | N/A：无敏感状态变更；实现时登记覆盖基线理由 | N/A 或沿用既有订单/套餐事实 | N/A | N/A |
| 店铺登录开关、企微账号绑定、模板场景、支付方式配置 | 使用现有统一审计接缝记录操作者和前后值；不建设新的审计中心 | N/A | 模板校验调用记录 | 缓存失效仅沿用现有配置机制 |
| 退款、线下代充 | 审批申请人和配置操作进入既有审计/申请快照；企微自动终态不伪造人工操作者 | 沿用退款、订单、钱包流水 | 所有企微请求、响应摘要、回调和结果未知 | 申请提交、标准终态和资金后续动作使用现有 Outbox |
| 通知 | N/A：通知记录本身是投递事实 | N/A | N/A | 必须，按业务键和接收人防重 |
| Gateway 限速 | 记录后台操作者、资产和档位 | N/A | 必须记录请求、响应及结果未知 | 无后续可靠副作用时 N/A |
| 批量订购/分配 | 沿用任务提交审计；资金动作沿用既有记录 | 订购扣款沿用订单/钱包流水 | 涉及外部支付或 Gateway 时沿用 | 仅业务已有可靠副作用继续使用 |

实现前必须增量更新 `.scratch/tech-global-audit/审计覆盖基线.md`，不得因本 Change 排除全局 Audit Event 专项而跳过上述已有审计接缝。

### 10. 续费、待支付订单与强充支付方式边界

- 历史已支付订单和资产当前套餐只提供稳定的资产、套餐引用。用户点击续费时继续调用现有 C 端创建订单接口并生成一张新订单，不新增续费接口，也不在旧订单上修改支付方式。
- 续费新订单创建前按当前 `system_config`、当前套餐价格/流量和当前强充状态重新选择支付方式；订单创建成功后，和普通订单一样固化所选方式。既有待支付订单再次支付时只能执行其已保存方式，不允许切换。
- 卡和设备分别维护非空支付方式集合，合法选项均为 `wallet`、`wechat`、`alipay`，三项可独立启停；首次初始化两类资产均全选。配置非法或读取失败时失败关闭，不推断或放开支付渠道。
- C 端购包接口返回“配置集合与当前业务场景的交集”。当前资产仍命中既有强充规则时必须剔除 `wallet`；普通钱包充值场景固定剔除 `wallet`。后端在创建订单、创建充值单和支付准备时执行相同校验，不能信任前端展示结果。
- 强充继续沿用现有流程：第三方支付充值成功后先入资产钱包，再通过既有异步自动购包任务扣钱包并购买所选套餐。支付方式配置不得把强充改成提示后由用户手工充值。
- 强充资格必须复用既有一次性佣金状态语义：`first_recharge` 与 `accumulated_recharge` 是同一强充/自动购包流程的不同触发配置；该系列一次性佣金已经触发后不再强充。不得在 C 端订单服务复制一套遗漏已触发状态的判断。

## Risks / Trade-offs

- **[企微应用看不到提交人]** → 保存绑定前只允许选择应用可见成员，提交前再次校验；权限变化时明确失败，不创建业务申请和审批实例。
- **[管理员修改企微模板导致控件映射失效]** → 保存/发布场景时拉取模板详情并保存指纹，提交前校验关键控件；失效只阻断新提交，存量审批继续同步。
- **[回调丢失或重复]** → 回调快速响应、按 `sp_no` 幂等同步，保留未终态轮询和时间窗补偿。
- **[企微提交超时但实际成功]** → 标记结果未知并先查详情/批量单号，不直接重试创建，避免重复审批。
- **[简单需求再次被扩张]** → tasks 明确每个切片的不迁移范围；实现阶段不得改变架构通道，确需改变必须先更新提案。
- **[大批量影响接口响应]** → 实名批量限制 500 且同步事务；CSV 和导出走异步任务，列表/人员映射使用批量查询。
- **[店铺开关不能立即踢掉在线用户]** → 这是本期有意取舍；接口和文档明确只阻止新登录。

## Migration Plan

1. 先完成仓库证据核验，确认公共 Outbox、审批核心、通知、导入/导出和 `system_config` 可复用，禁止重复建表。
2. 按纵向切片先交付无外部依赖的 Bug、字段、筛选、开关、实名、支付配置和本地通知。
3. 增量迁移企微应用/场景/账号绑定及确实缺失的业务字段，迁移脚本保持向前兼容，不删除旧字段。
4. 部署企微 Adapter、回调和轮询 Worker，配置测试应用、模板和账号绑定，完成真实退款/线下代充闭环后再关闭旧人工审批入口。
5. 部署批量订购、导出、限速和设备批量分配，完成前后端联调与任务失败恢复验证。
6. 完成生产代码、必要迁移文件、接口契约、配置说明和实施证据，以 `gofmt`、只读检查与 `git diff --check` 做静态收口并交付联调；自动化测试、构建、迁移执行和真实外部环境验收由后续流程承担。

回滚时，简单字段和兼容接口可回退应用版本；新增列/表暂不删除。产生企微审批、退款、充值、钱包流水、订单、通知、Outbox 或 Integration Log 后不得清表或恢复旧 Writer，只能暂停新入口并前向修复。旧人工审批入口仅在新链路验证完成后停用，因此切换前仍可回退。

## Open Questions

无阻塞性业务问题。实施和部署阶段仅需提供测试环境企微应用 Secret、可见范围、模板 ID、可信 IP、回调 Token/EncodingAESKey，以及 Gateway 测试配置；这些是环境输入，不改变需求范围。
