Просмотр исходного кода

docs: add customer family protocol link design.

Record agreed model for mother/father/child customer IDs on protocol parties, save-time sync, id-card merge, and customer detail relations.

Co-authored-by: Cursor <cursoragent@cursor.com>
zlwu 4 недель назад
Родитель
Сommit
e027b5abc4

+ 210 - 0
docs/superpowers/specs/2026-10-07-customer-family-protocol-link-design.md

@@ -0,0 +1,210 @@
+# 客户管理 · 协议家庭成员关联设计
+
+> 日期:2026-10-07  
+> 状态:已确认(2026-10-07)  
+> 范围:市场基础信息「客户管理」+ 细胞存储采集协议单 + 科研捐赠协议单  
+
+---
+
+## 1. 背景
+
+`biz_customer` 是独立客户主数据。采集协议保存时,目前只把**母亲**同步进客户表,并写到当事人表唯一的 `customer_id`。父亲、子女姓名等信息只存在当事人子表字段上,没有客户 ID。客户详情只展示本人字段,看不到同协议其他人和关联协议。
+
+产品要求:
+
+- 母亲、父亲、子女只要有姓名即可写入客户管理(有证、无证均可)。
+- 同一张协议上的三人通过协议当事人上的三个客户 ID 关联。
+- 点开任一客户详情:上方按协议看同单其他人,下方看关联协议摘要;点协议号弹出协议只读详情。
+- 一人可对应多张协议,详情按协议分组列出。
+- 细胞存储与科研捐赠一起做。
+
+---
+
+## 2. 目标与非目标
+
+### 2.1 目标
+
+- 当事人表增加父亲、子女客户 ID;母亲继续用已有 `customer_id`。
+- 保存细胞存储 / 科研捐赠协议时,对本单三个角色走同一套 upsert 同步(仅当前协议,不全库)。
+- 上线跑一次幂等迁移,把存量当事人补建客户并回写 ID;之后保存继续同步。
+- 补身份证撞到已有客户时:合并有用字段 → 全部引用改挂 → 软删临时客户。
+- 客户详情提供「关联客户」「关联协议」;点协议号打开对应查看弹窗。
+
+### 2.2 非目标
+
+- 不新建「协议–客户–角色」关系表。
+- 不把客户管理改成协议列表的派生视图。
+- 不在客户详情内嵌完整协议表单,只弹现有只读查看。
+- 不自动因协议软删而删除客户。
+- 不把客户管理里的手工改名回写到协议当事人字段。
+- 本期不改中试申请患者 ↔ 客户逻辑。
+
+---
+
+## 3. 数据模型
+
+不改 `biz_customer` 结构。扩展两张当事人表:
+
+### 3.1 `biz_protocol_party_customer`
+
+| 字段 | 说明 |
+|------|------|
+| `customer_id` | 已有,**母亲**客户 ID |
+| `father_customer_id` | 新增 VARCHAR(64) NULL,父亲客户 ID |
+| `newborn_customer_id` | 新增 VARCHAR(64) NULL,子女客户 ID |
+
+### 3.2 `biz_research_donation_party`
+
+同样增加 `father_customer_id`、`newborn_customer_id`;已有 `customer_id` 仍表示母亲。
+
+不把 `customer_id` 改名为 `mother_customer_id`,避免无谓迁移;代码与 API 注释写明「母亲」。
+
+SQL 放在 `docs/sql/migrations/`(例如 `2026-10-07-party-father-newborn-customer-id.sql`),并同步 SQLAlchemy 模型。
+
+关联约定:同一行上的三个 ID 即同一协议家庭;查询某客户相关协议时:
+
+```text
+customer_id = :id
+OR father_customer_id = :id
+OR newborn_customer_id = :id
+```
+
+细胞存储与科研捐赠各查一次,按租户、`del_flag=0` 过滤。
+
+---
+
+## 4. 同步规则(保存当前协议)
+
+抽公共函数(建议 `backend/app/services/party_customer_sync.py`),细胞存储与科研捐赠保存当事人时调用。**范围仅当前这一张协议的三个角色。**
+
+每个角色(母亲 / 父亲 / 子女)独立处理:
+
+1. **无姓名**:不同步客户;该角色客户 ID 置空(不删已有 `biz_customer` 记录)。
+2. **有姓名、无身份证**:新建客户,回写本单对应客户 ID。不按姓名去重(允许重名)。
+3. **有姓名、有身份证**:同租户未删除客户中按身份证查找(排除软删)。
+   - 未找到:若本单该角色已有客户 ID,则**不换 ID**,把身份证等写到该客户;若无客户 ID,则新建并回写。
+   - 找到且就是本单已挂的客户:更新该客户可写字段(姓名、电话等按下方合并策略)。
+   - 找到且是**另一条**客户:进入第 5 节合并改挂。
+
+性别默认:母亲 `1`,父亲 `0`,子女用当事人 `newborn_sex`,缺省 `2`。
+
+年龄:能从身份证推算则写入,否则空。
+
+协议当事人上的母亲/父亲/子女姓名电话等业务字段仍照旧写入子表,客户表只存客户主数据字段。
+
+---
+
+## 5. 补身份证撞库:合并后软删
+
+当本单某角色「当前挂着的客户 A」与「身份证已存在的客户 B」且 `A.id != B.id`:
+
+1. 把 A 的有用信息合并进 B:
+   - `phone_no`、`age`、`sex`(仅当目标为空或未知 `2` 时用源值)、`remark`:**目标为空才补,不覆盖已有**。
+   - `customer_tags`:按逗号拆开后做并集再写回 B。
+   - `customer_name`:**保留 B**(身份证命中的已有客户为准)。
+2. 将细胞存储当事人、科研捐赠当事人中,三个客户 ID 字段里所有等于 A 的值,改为 B(避免 A 仍被其它协议引用)。
+3. 本单该角色 ID 写为 B。
+4. 软删 A(`del_flag=1`)。
+5. 客户管理列表默认不展示 A。
+
+若身份证不撞库:不换 ID,只更新当前客户。
+
+---
+
+## 6. 存量迁移
+
+上线脚本(Python 或 SQL + 服务函数均可,须幂等):
+
+- 扫描未删除的 `biz_protocol_party_customer`、`biz_research_donation_party`。
+- 对已有角色姓名、却缺少对应客户 ID 的记录,调用与保存相同的同步函数。
+- 已有正确客户 ID 的跳过。
+- 可重复执行,结果一致。
+
+之后日常只在保存**当前协议**时同步,不再全库重跑。
+
+---
+
+## 7. 客户详情
+
+### 7.1 界面
+
+客户管理查看弹窗自上而下:
+
+1. **基本信息**:现有字段不变。
+2. **关联客户**:按该客户出现过的协议分组(协议 `create_time` 倒序)。每组展示:
+   - 类型(细胞存储 / 科研捐赠)、协议号、本客户角色(母亲 / 父亲 / 子女)
+   - 同单已填角色(含本人,标记「本人」):角色、姓名、电话、身份证;非本人点姓名打开对方客户详情
+   - 未填角色不展示
+3. **关联协议**:摘要列表字段:协议号、类型、状态、日期、本客户角色。  
+   日期:细胞存储用 `register_date`,科研捐赠用 `donation_date`,接口统一字段名 `registerDate`。  
+   **点击协议号**弹出只读详情:
+   - 细胞存储 → `ProtocolViewDialog`
+   - 科研捐赠 → `DonationViewDialog`
+
+无关联协议时,关联客户、关联协议区块展示空态文案。
+
+### 7.2 API
+
+`GET /api/admin/customers/{id}/relations`(需客户查看权限,租户隔离):
+
+```text
+{
+  customer: { ...与客户详情一致 },
+  relatedGroups: [
+    {
+      bizType,              // cell_storage | research_donation
+      protocolId,
+      protocolNo,           // 细胞存储 protocol_no;科研捐赠 donation_no
+      protocolStatus,       // protocol_status / donation_status
+      registerDate,         // register_date / donation_date
+      myRole,               // mother | father | newborn
+      members: [
+        { role, customerId, name, phone, idCardNo }
+      ]
+    }
+  ],
+  protocols: [
+    { bizType, protocolId, protocolNo, protocolStatus, registerDate, myRole }
+  ]
+}
+```
+
+`relatedGroups` 与 `protocols` 同源:每张相关协议一组;`members` 含本客户及同单其他人(有客户 ID 或至少有姓名的角色)。`protocols` 便于下方摘要表直接绑定。
+
+打开协议详情:前端复用现有协议/捐赠详情接口与弹窗。无对应协议查看权限时:协议号不可点或点击后提示无权限(与现有权限码一致,不新造权限)。
+
+---
+
+## 8. 列表与其它入口
+
+- 客户列表仍是 `biz_customer` 全量未删除记录;父亲、子女会出现为独立行。
+- 协议表单「选择客户」仍选自 `biz_customer`;保存后按第 4 节回写三个 ID。
+- 客户管理手工新增/编辑/删除行为不变;删除客户不级联改协议(协议上可能留下已删 ID;详情关联客户对该 ID 显示姓名占位或「客户已删除」)。
+
+---
+
+## 9. 测试要点
+
+- 无证三人保存 → 客户表 3 条,当事人三个 ID 均有值。
+- 后补子女身份证撞已有客户 → 字段合并、所有引用改挂、临时户软删、详情指向 B。
+- 后补身份证不撞库 → 客户 ID 不变,身份证写到原客户。
+- 同一母亲两张协议 → 详情两组关联客户 + 两条协议摘要。
+- 点协议号分别打开细胞存储 / 科研捐赠只读弹窗。
+- 仅填母亲 → 只同步母亲;关联客户组内无父/子女行。
+- 迁移脚本跑两遍结果一致。
+- 科研捐赠保存与细胞存储同一套规则。
+
+---
+
+## 10. 已确认决策
+
+| 项 | 结论 |
+|----|------|
+| 关联落点 | 当事人表三个客户 ID,不新建关系表 |
+| 母亲 ID | 沿用已有 `customer_id` |
+| 业务范围 | 细胞存储 + 科研捐赠 |
+| 无证 | 有姓名即可建客户 |
+| 补证撞库 | 合并后软删临时客户,协议改挂已有客户 |
+| 存量 | 上线全量脚本 + 之后仅保存当前单时同步 |
+| 详情布局 | 上:按协议分组的关联客户;下:协议摘要;点协议号弹详情 |
+| 中试 | 本期不动 |