# 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。
