Files
junhong_cmp_fiber/openspec/changes/archive/2026-09-21-add-agent-registration-management/design.md
break 4a16eb0b1e
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m41s
新增代理注册申请管理
2026-09-21 16:51:37 +08:00

89 lines
6.5 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
代理扫码注册在审批前已保存完整注册记录,并关联一个业务类型为 `agent_distribution_approval` 的通用审批实例。终态消费者在通过时原子创建店铺、代理账号和钱包,在驳回时只更新注册记录。现有对外入口只有无需认证的创建接口,后台没有查询或恢复入口。
注册记录的 `parent_shop_id` 是提交时分销码所属直接上级店铺事实,可直接支撑代理直属范围;审批前不存在下级店铺,因此不能复用店铺列表或基于已建店层级查询。通用审批与企业微信上下文已经保存提交状态、外部审批单号、失败摘要和恢复时间,可用于只读投影与原实例恢复。
## Goals / Non-Goals
**Goals:**
- 为平台与直接上级代理提供注册申请列表和详情,区分业务状态、审批状态和提交状态。
- 以注册时冻结的 `parent_shop_id` 固化代理直属可见范围。
- 提供提交失败或结果未知时的原审批恢复,不改变注册终审和建店事务。
- 在允许业务处理所需完整联系资料的同时,排除认证凭证和内部集成细节。
**Non-Goals:**
- 不把待审批注册记录投影为虚拟店铺,不加入现有店铺列表。
- 不提供本地人工通过、驳回、修改注册资料或删除申请。
- 不允许代理查看更深层下级申请,不使用 `SubordinateShopIDs` 扩大范围。
- 不新增数据库迁移、导出、批量审批或第三方依赖。
## Decisions
### 使用独立注册 Query不复用店铺或通用审批列表
`distributionwithdrawal` 查询边界增加注册列表与详情。主查询以 `tb_agent_distribution_registration` 为业务事实,审批实例和企业微信上下文只用于状态投影。
备选把申请混入店铺列表;放弃,因为审批前不存在店铺,虚拟店铺会污染 ID、状态和权限语义。备选只查询通用审批列表放弃因为通用快照不是完整注册查询模型也不适合业务筛选。
### 代理范围精确匹配直接上级店铺
超级管理员和平台用户不加店铺过滤。代理账号必须有有效当前店铺,并施加 `parent_shop_id = 当前店铺 ID`;范围为空时列表返回空,详情和恢复返回统一不可见错误。企业账号和其他类型拒绝。
不使用当前店铺的下级集合:业务已确认代理只看使用自己分销码提交的直接申请,更深层申请属于其下级代理的运营范围。注册记录保持提交时的直接上级快照,即使之后层级变化也不重算。
### 查询分别投影三层状态
响应分别返回:
- 注册业务状态:待审批、已通过、已驳回;
- 通用审批状态:提交中、审批中及各终态;
- 企业微信提交状态:待提交、发送中、已提交、失败、结果未知。
可恢复标记由服务端基于三层事实和上级店铺启用状态计算,前端不得自行推断。列表按页收集上级店铺和审批实例 ID批量读取店铺名称、审批实例及企微上下文避免 N+1。
筛选时间复用严格 RFC3339 闭区间解析规则;申请时间使用 `start_time/end_time`,决定时间使用 `decided_start_time/decided_end_time`。手机号精确匹配,店铺名称、编号、用户名和联系人使用受长度限制的子串匹配。
### 完整联系资料仅作为授权业务投影
根据业务决定平台账号和有权直接上级代理均返回完整手机号、完整省市区和详细地址便于处理注册申请。DTO 明确禁止包含密码哈希验证码没有持久化在注册记录也不得通过其它联查返回。失败摘要只使用安全文本不返回企微响应原文、Outbox 事件键、租约或内部重试字段。
审计只记录注册 ID、店铺编号、上级店铺、状态和脱敏手机号不复制完整手机号或地址。列表/详情访问沿用统一访问审计,不为每次读取新增业务动作。
### 恢复用例编排通用审批可靠事实
新增 `POST /api/admin/agent-distribution-registrations/{id}/recover-approval`。Application 用例在事务内锁定注册记录,校验状态仍待审批、实例非空、实例业务类型和业务 ID 一致,并复核权限及上级店铺仍启用。
恢复顺序:
1. 已有企微 `sp_no`:请求同步原审批详情。
2. 提交结果未知:触发原实例确认流程,禁止直接重提。
3. 明确失败且确认未受理:恢复 `approval:{instance_id}:submission` 稳定事件。
4. 事件等待重试、处理中或审批进行中:幂等返回当前状态。
5. 注册或审批终态:状态冲突。
HTTP 不同步调用企微创建审批接口,外部查询和提交继续由 Worker 执行。恢复不修改注册资料或密码哈希,不创建任何业务实体,也不新增审批实例。
### 停用上级只读不恢复
列表与详情读取历史注册记录时不要求上级店铺启用,保证运营可追溯。恢复必须要求上级店铺仍存在且启用,因为审批通过终态同样要求启用上级;在无落地条件时恢复只会制造无法完成的审批。
恢复动作写审计,记录操作者及其店铺、实例、前置状态、所选动作和安全结果;不复制个人敏感原文。
## Risks / Trade-offs
- [代理可见完整手机号与地址扩大个人信息暴露] → 严格限制为直接上级店铺,企业账号拒绝,响应与审计白名单排除认证凭证和不必要复制。
- [平台用户当前无实际店铺范围] → 按已确认规则平台全量,不引入未定义的平台范围推导。
- [上级店铺后续迁移导致历史归属与当前层级不同] → 以注册时 `parent_shop_id` 为稳定运营事实,不重算。
- [恢复与正常 Worker 重试竞争] → 实例锁和稳定事件条件更新保证单次状态变化,活动事件只返回当前状态。
- [关键词筛选拖慢列表] → 限制输入长度并仅在注册业务列上查询;数据规模扩大后再评估专用索引,不预建检索系统。
## Migration Plan
1. 不执行数据库迁移;发布注册 Query、恢复用例、后台路由及审批状态投影。
2. 在维护者指定测试环境创建直属、非直属、待审批、已通过、已驳回及提交异常申请,验证数据范围和筛选。
3. 验证明确失败、结果未知、已有 `sp_no`、并发恢复和上级停用拒绝,不产生第二个实例或任何半套业务实体。
4. 回滚时移除后台入口并回退二进制;注册记录、审批实例和审计事实保持不变。