Files
break 22c3e7cc1a
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m42s
实施业务用户组成员管理
2026-09-21 21:20:38 +08:00

73 lines
5.3 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
当前成员关系表已保证一个平台账号至多一条未删除归属。`SetMembers` 实际只迁移请求中的账号,不会替换目标组完整集合;`ClearMembers` 则按账号全局清空归属,不校验这些账号属于哪个组。缺少成员和候选查询使前端无法展示真实归属,全局清空入口还允许错误页面移除其他组成员。
现有写用例已具备启用平台账号校验、账号行按 ID 升序加锁、唯一约束裁决和逐账号审计。成员查询需要保留禁用及软删除账号关系,而候选查询只面向启用未删除平台用户。
## Goals / Non-Goals
**Goals:**
- 为每个用户组提供可分页的成员和候选查询,明确账号当前归属与下一步动作。
- 把增加和移除都约束到目标组,避免整体覆盖和跨组误清空。
- 保持现有唯一归属、事务回滚、并发锁和审计不变量。
- 让成员变化继续实时驱动店铺所属组推导。
**Non-Goals:**
- 不引入完整成员集合保存、乐观版本号或组层级。
- 不改变角色、登录、权限、数据范围或店铺负责人本身。
- 不新增数据库迁移、导入导出或成员审批。
- 不保留旧成员写入口兼容别名。
## Decisions
### 查询拆分为当前成员与可选候选
新增组内成员 Query成员关系为主表并以 `Unscoped` 联查账号,使禁用和软删除账号仍可展示。关键字匹配用户名或手机号;账号状态筛选区分启用、禁用和已删除。返回关系创建时间及目标组快照。
新增候选 Query以启用未删除的平台账号为主表LEFT JOIN 当前未删除成员关系和用户组。支持全部、未分组、当前组和其他组筛选,并按当前关系计算 `add``already_member``move`。目标组停用时仍可查询,但统一返回不可添加标记。
选择两个端点而非在一个端点用模式参数混合:成员查询需要保留删除账号,候选查询必须排除它们,主表与授权语义不同。
### 增加成员沿用唯一归属替换语义并细分结果
把现有 `PUT /:id/members` 改为 `POST /:id/members`。写用例继续要求目标组启用、全部账号为可用平台用户,并按账号 ID 升序锁账号行。锁后读取原归属:无关系新增、其他组更新、当前组不写。
结果按锁后事实统计 `added_count``moved_count``unchanged_count`。只为新增或迁移账号写审计;当前组幂等项不写审计。请求外成员不参与任何删除或更新。
### 移除成员必须验证目标组归属
新增 `DELETE /:id/members`。目标组只要求存在未删除,停用组允许移除。事务内按账号 ID 升序锁账号行,再读取成员关系并要求每个账号当前都属于目标组;任何缺失或其他组关系导致整批状态冲突。
校验通过后执行 `WHERE business_user_group_id = ? AND account_id IN ? AND deleted_at IS NULL` 软删除,并要求影响行数等于请求账号数。校验账号本身时使用保留关系和 `Unscoped` 账号读取,因此禁用或已删除账号仍可清理。移除审计以账号为主资源、原组为引用资源。
备选继续调用全局 `ClearMembers`;放弃,因为缺少目标组谓词,无法阻止跨组误清空。
### 干净移除旧写入口
删除全局 `DELETE /business-user-groups/members` 路由、Handler 方法及专用 DTO现有 `PUT /:id/members` 改为 POST不保留 PUT 别名。前端同步迁移到组作用域端点。
这是有意的破坏性收敛:保留旧入口会留下两套不同安全边界,并使调用方继续把操作理解为整体保存或全局清空。
### 成员数在组查询中批量投影
用户组列表按当页组 ID 一次聚合成员关系数量,详情按目标组计数。计数口径为未删除成员关系,不因账号禁用或软删除排除,和“关系保留”契约一致。成员写入后无需更新组表或缓存。
店铺所属组仍通过当前负责人和成员关系实时推导;本变更不写店铺组 ID 或业务线。
## Risks / Trade-offs
- [破坏性路由切换影响前端] → 同一发布批次同步前端方法与路径OpenAPI 只保留新端点,旧路由明确不可达。
- [软删除账号缺少可读字段] → 使用账号表 Unscoped 读取历史字段并标记已删除,不恢复账号或关系。
- [并发迁移与移除同一账号] → 两条用例都按账号 ID 升序锁账号行,锁后重读归属,唯一索引作为最终裁决。
- [大组成员查询] → 强制分页和关键字长度限制;成员表已有组索引,不预建新索引。
- [候选手机号属于敏感信息] → 该能力仅超级管理员和平台用户可用,沿用现有平台业务员候选的手机号展示策略;实现时固定响应口径并避免审计复制完整手机号。
## Migration Plan
1. 不执行数据库迁移;发布成员/候选 Query、增量写用例和新路由。
2. 同步前端PUT 增加改为 POST全局 DELETE 改为携带组 ID 的 DELETE接入成员和候选分页。
3. 在维护者指定测试环境验证新增、迁移、幂等、停用组移除、删除账号清理、跨组拒绝和并发裁决。
4. 回滚需同步恢复旧前端调用和旧二进制;成员关系本身仍兼容旧模型,不需要数据回滚。