Files
junhong_cmp_fiber/openspec/changes/deliver-july-iteration-confirmed-scope/design.md
break 73f5125d3d
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 8m26s
七月迭代短暂完结,还有很多后端的关键东西没有弄,这是一版赶时间做的东西
2026-07-25 17:06:58 +08:00

164 lines
17 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.
## 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 测试配置;这些是环境输入,不改变需求范围。