130 lines
13 KiB
Markdown
130 lines
13 KiB
Markdown
# PRD:UR#96 店铺业务员归属、继承与筛选
|
||
|
||
Status: ready-for-agent
|
||
|
||
---
|
||
|
||
## Problem Statement
|
||
|
||
店铺目前只有上下级代理关系,没有明确的平台业务员归属。运营无法按负责员工筛选店铺,套餐临期和钱包低余额等通知也无法稳定找到对应平台员工。
|
||
|
||
代理账号可以发展下级代理,因此业务员归属不能只在平台创建店铺时人工填写。下级代理应默认继承直属上级店铺当时的业务员,但代理不得自行指定或更换平台员工;同时,平台后续调整上级店铺业务员不能自动覆盖已有下级店铺,否则会破坏平台对单个下级店铺的独立调整。
|
||
|
||
## Solution
|
||
|
||
在 `tb_shop` 增加可空 `business_owner_account_id`,表示该店铺当前负责业务员。代理创建下级店铺时由服务端复制所选直属上级店铺当时的业务员 ID,代理请求不得提供该字段;复制完成后父子店铺各自保存,后续不做级联同步。
|
||
|
||
只有超级管理员和平台账号可以显式设置、清空或更换业务员,且只能选择当前启用、未删除的普通平台账号。店铺列表、详情和筛选 Query 批量投影业务员账号名及手机号摘要;该字段只用于平台内部业务归属和通知接收,不进入代理层级、数据权限、佣金或分销计算。
|
||
|
||
## User Stories
|
||
|
||
1. 作为平台运营人员,我希望创建或编辑店铺时可以选择一个启用的平台业务员,也可以清空归属。
|
||
2. 作为平台运营人员,我希望按业务员筛选我权限范围内的店铺,并在列表和详情看到业务员摘要。
|
||
3. 作为代理账号,我希望发展下级代理时,新店铺自动沿用我的店铺当前业务员,无需也不能自行选择。
|
||
4. 作为平台运营人员,我希望修改上级店铺业务员后,已有下级店铺不被级联覆盖,便于逐店铺独立管理。
|
||
5. 作为代理账号,我希望能查看有权限店铺的业务员归属,但不能通过构造请求篡改它。
|
||
6. 作为通知系统,我希望按店铺稳定找到当前业务员;账号停用或删除时保留归属历史,但不向不可用账号发送通知。
|
||
7. 作为审计人员,我希望区分平台人工设置、代理建店继承、清空和更换,并能看到变更前后账号。
|
||
|
||
## Implementation Decisions
|
||
|
||
### 领域含义与数据模型
|
||
|
||
- `tb_shop` 新增 `business_owner_account_id BIGINT NULL`,不建立数据库外键或 GORM 关联标签;Model 只保存 ID,关联账号由 Query 显式批量加载。
|
||
- `business_owner_account_id` 表示平台内部当前业务负责人和通知接收关系,不是“发展人”层级事实。它不参与 `parent_id` 店铺层级、数据权限、授权范围、代理分销、佣金、提现或客户归属计算。
|
||
- 可被人工选择的业务员必须满足 `tb_account.user_type=2`、`status=1`、`deleted_at IS NULL`。超级管理员可以管理该字段,但 `user_type=1` 的超级管理员账号本身不是业务员候选。
|
||
- 一个店铺本期最多一个业务员,可为空;一个平台业务员可以负责多个店铺。
|
||
- 业务员账号禁用或软删除后不清空店铺字段,不级联更新店铺;列表/详情仍尽可能按原 ID 展示历史账号摘要并标记当前不可用,通知接收人解析时跳过。账号重新启用后,该未变更关联重新成为可用通知关系。
|
||
- 为 `business_owner_account_id` 建普通索引以支持筛选,不建立唯一约束。
|
||
|
||
### 创建时继承
|
||
|
||
- 创建店铺继续使用 `POST /api/admin/shops`,新增存在性感知的可空字段 `business_owner_account_id`。
|
||
- 代理账号创建其现有权限允许的新下级店铺时,业务员只能由服务端从最终校验通过的 `parent_id` 店铺复制:
|
||
- 上级有业务员 ID:原值复制到新店铺,包括上级账号此刻已停用/软删除但关联仍保留的情况。
|
||
- 上级业务员为空:新店铺也为空。
|
||
- 请求 JSON 只要主动出现 `business_owner_account_id`,无论值为原值、其他 ID 或 `null`,都返回禁止操作,不静默忽略。
|
||
- 代理建店必须先复用现有店铺层级与管理权限校验,确保 `parent_id` 确实处于调用者允许发展的范围;不能通过选择无权上级间接复制归属或创建店铺。本需求不扩大代理原有发展层级权限。
|
||
- 超级管理员或平台账号创建店铺时:
|
||
- 显式传正整数 ID:校验为当前可用平台业务员后设置。
|
||
- 显式传 `null`:创建为空业务员。
|
||
- 字段缺失且存在上级店铺:默认复制上级店铺当前业务员。
|
||
- 字段缺失且为顶级店铺:默认为空。
|
||
- 创建时复制是一次性快照关系。上级店铺以后被设置、清空或更换业务员,既有下级、孙级店铺均不自动更新;只有之后新创建的直属下级读取上级当时的当前值。
|
||
- 店铺、初始主账号、角色、主/分佣钱包、业务员归属和创建审计属于同一个完整创建用例,必须在同一数据库事务内成功或失败,避免当前多表创建留下半成品。
|
||
|
||
### 编辑权限和三态字段
|
||
|
||
- `PUT /api/admin/shops/{id}` 增加存在性感知的可空 `business_owner_account_id`,语义为:字段缺失保持不变,显式 `null` 清空,正整数校验后替换。
|
||
- DTO/解析必须真实区分“字段缺失”和“显式 null”,不能用普通 `*uint` 把二者都解释成 nil;可使用项目内明确的 Optional/Nullable 类型,但不得把这个差异留给 Handler 猜测。
|
||
- 超级管理员和平台账号可以设置、清空、更换;代理账号不得修改。代理更新其他店铺资料且字段缺失时保留原业务员,字段一旦出现则返回统一禁止访问错误。
|
||
- 企业账号不具备业务员候选或写权限。所有写操作除账号类型检查外,仍要执行目标店铺的既有资源权限校验;无权或不存在统一返回“无权限操作该资源或资源不存在”。
|
||
- 设置 ID 时在事务内重新校验业务员仍为启用平台账号,不能仅相信前端候选列表。禁用、删除、代理或企业账号 ID 均返回参数/业务错误且不更新店铺。
|
||
- 平台手工修改一个店铺只影响该店铺,不向上、向下或同级传播,也不修改历史通知和审计快照。
|
||
|
||
### Query 与 API
|
||
|
||
- `GET /api/admin/shops` 新增可选精确筛选 `business_owner_account_id`,与店铺名、编号、联系电话、层级、状态等现有条件按 AND 组合;分页、排序和调用者数据范围保持原契约。
|
||
- 店铺响应增加:
|
||
- `business_owner_account_id:uint|null`
|
||
- `business_owner_username:string`
|
||
- `business_owner_phone_summary:string`
|
||
- `business_owner_available:bool`
|
||
- 手机号摘要固定保留前三位和后四位,例如 `138****8000`;空值返回空字符串。普通店铺 Query 不返回业务员完整手机号。
|
||
- 列表按当前页业务员 ID 一次批量加载,包括必要的软删除只读投影,禁止每行查询账号。筛选按店铺保存的 ID 执行,因此账号停用/删除后仍可用该 ID 查到历史负责店铺。
|
||
- 若前端现有店铺详情没有独立接口,应补齐 `GET /api/admin/shops/{id}` 并返回同一 `ShopResponse`;动态路由必须排在 `/cascade`、`/fund-summary`、`/business-owner-candidates` 等静态路由之后,避免吞掉现有路径。
|
||
- 新增最小候选 Query:`GET /api/admin/shops/business-owner-candidates?keyword=&page=&page_size=`,仅超级管理员和平台账号可调用。固定筛选当前启用、未删除的 `user_type=2` 账号,`keyword` 对用户名或手机号做受控查询,返回 `id/username/phone_summary`,默认 20、最大 100。
|
||
- 代理端不需要候选接口;其创建/编辑表单只展示继承或现有业务员摘要,不显示可搜索选择框。
|
||
- 新字段向后兼容:旧平台客户端不传字段时,创建子店铺按继承规则、编辑保持不变;旧代理客户端不传字段时正常继承/保留。
|
||
|
||
### Application、通知与审计
|
||
|
||
- 本需求使用 Application 事务脚本,不创建空洞 Shop 聚合。Application 负责操作者类型、资源权限、上级解析、继承/三态命令、候选账号校验、事务保存和 Audit Event;Query 负责列表、详情、候选及 DTO 投影。
|
||
- 店铺创建事件记录 `assignment_source=inherited/explicit/empty`、上级店铺 ID 和最终业务员 ID。编辑事件只有字段实际变化时记录业务员变更,前后数据均使用 ID、账号名摘要和可用状态。
|
||
- 业务员归属变更属于平台内部敏感业务操作,接入统一 Audit Writer;关键成功审计与店铺事务同事务,失败/拒绝按公共审计规则记录。
|
||
- 公共通知接收人解析按店铺当前 `business_owner_account_id` 查账号并再次校验可用性;不得因为父店铺后来变更业务员而动态向上追溯,也不得沿代理层级向所有上级业务员发送。
|
||
- 账号禁用/删除不触发批量清空或改派。运营通过店铺列表按该业务员筛选后逐个或后续独立批量能力处理,本需求不建设自动转派。
|
||
- 新增/修改 DTO、Model、Handler 和迁移时遵循相应项目专项规范;新增 Handler 后同步两个 OpenAPI 文档生成器。
|
||
|
||
### 前端交互
|
||
|
||
- 超级管理员和平台账号的店铺创建/编辑表单增加可搜索业务员下拉:支持不选择、清空、加载、无候选和失败重试;候选显示账号名与手机号摘要。
|
||
- 创建下级店铺时字段初始显示继承到的上级业务员;平台可覆盖或清空。前端是否展示默认值不作为业务规则,最终继承由后端保证。
|
||
- 代理账号只读展示“业务员”,不显示选择或清空控件;创建下级时提示“默认继承上级店铺业务员”。
|
||
- 店铺列表增加业务员列和筛选。停用/删除账号显示原摘要及“已停用/不可用”,不误显示为空;清空归属显示“-”。
|
||
- 编辑提交期间禁用重复提交;权限拒绝、候选失效和并发变更原地展示后端中文错误,成功后重新请求店铺详情。
|
||
|
||
### 发布与历史数据
|
||
|
||
- 迁移新增可空字段和普通索引,不回填存量店铺,不根据祖先或创建人猜测历史业务员。所有存量店铺初始为空,由平台后续维护。
|
||
- 部署后新建店铺立即按新规则写入;不运行父子全量级联脚本。回滚应用时新增字段可以保留,已产生的业务员关联和审计不得清空。
|
||
- 发布前只读核验 `tb_shop` 层级异常和平台账号状态;发布后抽查平台显式设置、代理继承、父级修改不级联及通知接收人解析。
|
||
|
||
## Testing Decisions
|
||
|
||
- Application 测试覆盖超级管理员、平台、代理、企业四类操作者的创建和编辑矩阵;验证只有前两类可显式设置/清空/更换。
|
||
- 创建测试覆盖:代理上级有/无业务员、上级业务员可用/停用/软删除、代理主动传相同 ID/其他 ID/null、平台显式 ID/null/字段缺失和顶级店铺。
|
||
- 继承回归验证父店铺后续设置、清空和更换均不改变既有子孙店铺;新建直属下级只复制创建时父店铺当前值。
|
||
- 候选与写入测试验证只有 `user_type=2 + status=1 + 未删除` 可选择;超级管理员、代理、企业、停用和软删除账号均不能被人工新绑定。
|
||
- 权限测试验证代理不能利用无权 `parent_id` 创建店铺,不能通过字段缺失/null 混淆清空归属,企业和越权平台请求使用统一安全错误语义。
|
||
- PostgreSQL 集成测试验证创建多表事务原子性、业务员索引筛选、AND 条件、分页总数、软删除账号历史投影和批量查询无 N+1。
|
||
- HTTP 集成测试穿过真实 Fiber 认证、Handler、Application/Query、GORM 和统一响应,特别验证 JSON 字段缺失、null、0、正 ID 四种解析语义。
|
||
- API 测试覆盖店铺列表、详情和候选;验证手机号固定脱敏,代理不能调用候选,静态候选/资金概况/级联路由不被详情动态路由吞掉。
|
||
- Audit 测试验证继承、显式设置、清空、更换和拒绝结果,事务失败不留下店铺/账号/钱包半成品或成功审计。
|
||
- 前端验收覆盖平台选择/清空、代理只读、停用业务员展示、业务员筛选、空态、失败态和父级修改不级联。
|
||
|
||
## Out of Scope
|
||
|
||
- 不恢复需求 16 的分销码、发展关系、佣金、提现或 H5 代理申请;“业务员”不等于分销发展人。
|
||
- 不因业务员归属扩大账号的数据权限或店铺管理范围。
|
||
- 不自动级联上级业务员的后续变更,不批量回填存量子店铺。
|
||
- 不允许代理账号自行设置、清空或更换业务员。
|
||
- 不建设多业务员、团队、部门、区域或自动轮转分配。
|
||
- 不在业务员账号停用/删除时自动改派,也不发送外部渠道通知。
|
||
|
||
## Further Notes
|
||
|
||
- 当前代码的 `Shop`、创建/更新 DTO 和列表 Query 均没有业务员字段;店铺创建还连续创建店铺、主账号、角色和两个钱包但没有显式完整事务,接入继承时必须把该完整创建用例收口。
|
||
- 当前已有平台账号列表能力,但返回字段和权限范围大于下拉框所需;专用候选 Query 用于最小披露和明确写权限。
|
||
- 用户确认的核心口径是“创建时继承、之后独立、不自动级联”;实现不得把查询时动态向父级取值当作继承。
|