# UR#96 店铺业务员归属功能总结 ## 本次交付范围 本次完成店铺业务员归属的六个后端纵向切片:持久化、平台创建设置或继承、代理创建安全继承、独立编辑、列表/详情/候选 Query,以及通知接收人解析 Port/Adapter。 业务员归属是平台内部业务责任关系,只保存当前店铺自己的 `business_owner_account_id`。它不参与店铺层级、数据权限、佣金、分销或提现计算,也不会因父店铺后续修改而级联变化。 ## 创建与编辑契约 - `POST /api/admin/shops` 支持存在性感知的 `business_owner_account_id`:平台/超管可显式设置、显式 `null` 清空;字段缺失时复制直属上级店铺当时保存的原始 ID。 - 代理创建直属下级店铺时不得提交该字段;字段缺失时由服务端复制直属上级店铺当时保存的原始 ID,包括已停用或软删除账号的历史 ID。 - `PUT /api/admin/shops/:id` 中字段缺失表示保持不变,显式 `null` 表示清空,正 ID 表示重新绑定。 - 只有超级管理员和平台账号可以人工设置、清空或更换,并在事务内重新校验候选仍为启用、未删除的普通平台账号。 - 店铺、初始主账号、账号角色、店铺角色和主/分佣钱包在同一 GORM 事务内创建,避免多表半成品。 Audit Event 写入按七月测试环境 Change 冻结到任务 6.5,本次没有把审计延期扩散到业务事务、权限或可靠性边界。 ## Query 与前端契约 `GET /api/admin/shops` 新增 `business_owner_account_id` 精确筛选,并与其他筛选条件按 AND 组合。创建、编辑、列表和详情统一返回: - `business_owner_account_id` - `business_owner_username` - `business_owner_phone_summary` - `business_owner_available` 列表只针对当前页收集业务员 ID,并通过一次批量查询投影账号名、前三后四手机号摘要和可用状态,不产生逐店铺 N+1。软删除账号使用只读历史投影保留摘要,并将 `business_owner_available` 标记为 `false`。 新增接口: - `GET /api/admin/shops/:id`:返回与列表一致的店铺及业务员摘要,并继续应用现有店铺数据范围。 - `GET /api/admin/shops/business-owner-candidates`:仅超级管理员和平台账号可调用,只返回启用、未删除的普通平台账号 ID、账号名和手机号摘要;支持用户名/手机号关键词、默认 20、最大 100 的分页。 代理端只读展示业务员摘要,不展示候选选择或清空控件。停用或删除账号应显示历史摘要和“不可用”,空归属显示为“-”。 ## 通知接收人解析边界 `NotificationRecipientResolver` Port 由 PostgreSQL `RecipientResolver` Adapter 实现。它按目标店铺当前保存的业务员 ID 解析接收人,并同时返回当前启用、未删除的店铺主账号: - 业务员只有仍为 `user_type=2`、启用且未删除时才返回。 - 店铺主账号只有仍为代理类型、主账号、启用且未删除时才返回。 - 同一账号按稳定账号 ID 去重并排序。 - 店铺不存在、无归属或账号永久不可用时返回空集合,不作为无限重试错误。 - 数据库故障仍返回可重试错误。 - 解析不读取父店铺、祖先店铺或创建人,不会把代理数据权限误当作通知关系。 该接缝供公共站内通知的动态接收人解析复用;UR#96 本身不实现套餐临期、钱包低余额等业务触发规则,也不发送短信或企业微信通知。 ## 迁移、发布与回滚 迁移 `000169_add_shop_business_owner` 为 `tb_shop` 增加 nullable bigint 字段和普通索引,不建立外键、不回填存量数据、不运行父子级联脚本。 发布前应只读核验店铺层级异常和平台账号状态;发布后抽查平台显式设置/清空、代理继承、父级修改不级联、历史不可用账号展示和接收人解析。应用回滚应保留字段和已产生的历史归属;down 迁移检测到任何非空归属时会拒绝删列,要求前向修复。 ## 当前验证状态 已执行 `gofmt`、`git diff --check` 和 `go build ./...`。按本 Change 的测试环境豁免,本次未新增或运行 `_test.go`,也未连接真实 PostgreSQL/Redis;集成、HTTP、迁移演练和前端人工验收分别转任务 6.1、6.3 和 6.6。