docs: add implementation plan for customer family protocol link.
Break work into schema, sync service, save wiring, relations API, backfill, and customer detail UI tasks. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
e027b5abc4
commit
69e76b85ab
1 file changed
+559
@@ -0,0 +1,559 @@
|
|||||||
|
# 客户管理 · 协议家庭成员关联 Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 细胞存储 / 科研捐赠协议保存时把母亲、父亲、子女同步进 `biz_customer` 并写回当事人三个客户 ID;客户详情按协议展示关联人与协议摘要,点协议号打开只读详情。
|
||||||
|
|
||||||
|
**Architecture:** 当事人表各增 `father_customer_id` / `newborn_customer_id`(母亲沿用 `customer_id`)。公共同步服务 `party_customer_sync` 在保存当前协议时 upsert 三人,补证撞库则合并后改挂并软删临时客户。详情用 `GET .../customers/{id}/relations` 聚合两边当事人。前端客户查看弹窗扩展关联区块与协议弹窗。
|
||||||
|
|
||||||
|
**Tech Stack:** FastAPI + SQLAlchemy + MySQL;Vue 3 + Element Plus;pytest
|
||||||
|
|
||||||
|
**Design Doc:** `docs/superpowers/specs/2026-10-07-customer-family-protocol-link-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure Map
|
||||||
|
|
||||||
|
### New files
|
||||||
|
|
||||||
|
| File | Responsibility |
|
||||||
|
|------|----------------|
|
||||||
|
| `docs/sql/migrations/2026-10-07-party-father-newborn-customer-id.sql` | 两张当事人表加列 |
|
||||||
|
| `backend/app/services/party_customer_sync.py` | 三角色 upsert、撞库合并、改挂、存量回填入口 |
|
||||||
|
| `backend/app/services/customer_relations.py` | 按客户 ID 聚合关联协议与同单成员 |
|
||||||
|
| `backend/tests/test_party_customer_sync.py` | 同步 / 合并规则单测 |
|
||||||
|
| `backend/tests/test_customer_relations.py` | 关联聚合单测 |
|
||||||
|
| `backend/scripts/backfill_party_customer_ids.py` | 存量幂等回填脚本 |
|
||||||
|
|
||||||
|
### Modified files
|
||||||
|
|
||||||
|
| File | Change |
|
||||||
|
|------|--------|
|
||||||
|
| `backend/app/models/models.py` | `BizProtocolPartyCustomer` 加两列 |
|
||||||
|
| `backend/app/models/research_donation.py` | `BizResearchDonationParty` 加两列 |
|
||||||
|
| `backend/app/crud/collection_protocol.py` | `_party_to_dict` / `upsert_party_customer` 支持新字段 |
|
||||||
|
| `backend/app/crud/research_donation.py` | `_party_to_dict` / `_PARTY_STR_FIELDS` / convert 复制字段 |
|
||||||
|
| `backend/app/api/collection_protocol.py` | 用 sync 服务替换 `_ensure_customer_for_mother` |
|
||||||
|
| `backend/app/api/research_donation.py` 或 `crud/research_donation.py` | create/update party 前调用 sync |
|
||||||
|
| `backend/app/api/basic_info.py` | `GET /customers/{id}/relations` |
|
||||||
|
| `web-admin/src/api/admin.ts` | `fetchCustomerRelations` |
|
||||||
|
| `web-admin/src/types/api.d.ts` | Relations 类型 |
|
||||||
|
| `web-admin/src/components/business/system-view-dialog/index.vue` | 增加 `#append` slot(基本信息之后) |
|
||||||
|
| `web-admin/src/views/basic-info/customer/index.vue` | 详情加载 relations;关联客户/协议;点协议号弹窗 |
|
||||||
|
| `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue`(若 payload 需透传) | 确保当事人可带 `fatherCustomerId` / `newbornCustomerId`(读回写回即可,一般后端覆盖) |
|
||||||
|
|
||||||
|
### Unchanged (explicit)
|
||||||
|
|
||||||
|
- `biz_customer` 表结构
|
||||||
|
- 中试患者 ↔ 客户逻辑
|
||||||
|
- 不新建关系表
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 数据库迁移 + 模型字段
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `docs/sql/migrations/2026-10-07-party-father-newborn-customer-id.sql`
|
||||||
|
- Modify: `backend/app/models/models.py`(`BizProtocolPartyCustomer`)
|
||||||
|
- Modify: `backend/app/models/research_donation.py`(`BizResearchDonationParty`)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写迁移 SQL**
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 采集协议当事人:父亲/子女客户 ID(母亲沿用 customer_id)
|
||||||
|
ALTER TABLE biz_protocol_party_customer
|
||||||
|
ADD COLUMN father_customer_id VARCHAR(64) DEFAULT NULL COMMENT '父亲客户ID' AFTER customer_id,
|
||||||
|
ADD COLUMN newborn_customer_id VARCHAR(64) DEFAULT NULL COMMENT '子女客户ID' AFTER father_customer_id;
|
||||||
|
|
||||||
|
-- 科研捐赠当事人:同上
|
||||||
|
ALTER TABLE biz_research_donation_party
|
||||||
|
ADD COLUMN father_customer_id VARCHAR(64) DEFAULT NULL COMMENT '父亲客户ID' AFTER customer_id,
|
||||||
|
ADD COLUMN newborn_customer_id VARCHAR(64) DEFAULT NULL COMMENT '子女客户ID' AFTER father_customer_id;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 更新 SQLAlchemy 模型**
|
||||||
|
|
||||||
|
在 `BizProtocolPartyCustomer` 的 `customer_id` 后增加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
father_customer_id = Column(String(64), nullable=True, comment="父亲客户ID")
|
||||||
|
newborn_customer_id = Column(String(64), nullable=True, comment="子女客户ID")
|
||||||
|
```
|
||||||
|
|
||||||
|
在 `BizResearchDonationParty` 同样增加两列(注释写明母亲仍用 `customer_id`)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add docs/sql/migrations/2026-10-07-party-father-newborn-customer-id.sql \
|
||||||
|
backend/app/models/models.py backend/app/models/research_donation.py
|
||||||
|
git commit -m "feat(db): add father/newborn customer ids on protocol parties"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 2: `party_customer_sync` 核心逻辑(TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `backend/app/services/party_customer_sync.py`
|
||||||
|
- Create: `backend/tests/test_party_customer_sync.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写失败单测(无 DB 依赖的纯函数优先;需要 Session 的用 unittest.mock)**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""当事人三角色客户同步 / 撞库合并"""
|
||||||
|
import unittest
|
||||||
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
|
from app.services.party_customer_sync import (
|
||||||
|
merge_customer_fields,
|
||||||
|
sync_party_customers,
|
||||||
|
ROLE_MOTHER,
|
||||||
|
ROLE_FATHER,
|
||||||
|
ROLE_NEWBORN,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestMergeCustomerFields(unittest.TestCase):
|
||||||
|
def test_fill_empty_only_and_union_tags(self):
|
||||||
|
target = MagicMock(
|
||||||
|
customer_name="已有",
|
||||||
|
phone_no="111",
|
||||||
|
age=None,
|
||||||
|
sex="2",
|
||||||
|
remark="",
|
||||||
|
customer_tags="vip",
|
||||||
|
)
|
||||||
|
source = MagicMock(
|
||||||
|
phone_no="222",
|
||||||
|
age=30,
|
||||||
|
sex="1",
|
||||||
|
remark="备注A",
|
||||||
|
customer_tags="a,vip",
|
||||||
|
)
|
||||||
|
merge_customer_fields(target, source)
|
||||||
|
self.assertEqual(target.phone_no, "111") # 不覆盖
|
||||||
|
self.assertEqual(target.age, 30)
|
||||||
|
self.assertEqual(target.sex, "1") # 目标为未知才补
|
||||||
|
self.assertEqual(target.remark, "备注A")
|
||||||
|
tags = set((target.customer_tags or "").split(","))
|
||||||
|
self.assertEqual(tags, {"vip", "a"})
|
||||||
|
self.assertEqual(target.customer_name, "已有")
|
||||||
|
|
||||||
|
|
||||||
|
class TestSyncPartyCustomers(unittest.TestCase):
|
||||||
|
def test_no_name_clears_role_id(self):
|
||||||
|
db = MagicMock()
|
||||||
|
party = {
|
||||||
|
"motherName": "",
|
||||||
|
"customerId": "old-m",
|
||||||
|
"fatherName": "",
|
||||||
|
"fatherCustomerId": "old-f",
|
||||||
|
}
|
||||||
|
out = sync_party_customers(db, party, tenant_no="zkxw", user_id="u1")
|
||||||
|
self.assertEqual(out.get("customer_id") or out.get("customerId"), "")
|
||||||
|
self.assertEqual(out.get("father_customer_id") or out.get("fatherCustomerId"), "")
|
||||||
|
|
||||||
|
def test_three_names_without_id_card_creates_three(self):
|
||||||
|
db = MagicMock()
|
||||||
|
created = []
|
||||||
|
|
||||||
|
def fake_create(db_, **kwargs):
|
||||||
|
obj = MagicMock()
|
||||||
|
obj.id = f"c{len(created)+1}"
|
||||||
|
created.append(kwargs)
|
||||||
|
return obj
|
||||||
|
|
||||||
|
party = {
|
||||||
|
"motherName": "妈",
|
||||||
|
"fatherName": "爸",
|
||||||
|
"newbornName": "宝",
|
||||||
|
"newbornSex": "0",
|
||||||
|
}
|
||||||
|
with patch("app.services.party_customer_sync.customer_crud.create_customer", side_effect=fake_create):
|
||||||
|
with patch("app.services.party_customer_sync.find_customer_by_id_card", return_value=None):
|
||||||
|
with patch("app.services.party_customer_sync.get_customer", return_value=None):
|
||||||
|
out = sync_party_customers(db, party, tenant_no="zkxw", user_id="u1")
|
||||||
|
self.assertEqual(len(created), 3)
|
||||||
|
self.assertTrue(out.get("customer_id") or out.get("customerId"))
|
||||||
|
self.assertTrue(out.get("father_customer_id") or out.get("fatherCustomerId"))
|
||||||
|
self.assertTrue(out.get("newborn_customer_id") or out.get("newbornCustomerId"))
|
||||||
|
```
|
||||||
|
|
||||||
|
再补撞库合并用例:已有临时客户 A + 身份证命中 B → 调用 `repoint_customer_refs` + 软删 A,返回 B 的 id(可用 mock 断言调用)。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 跑测确认失败**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend && source venv/bin/activate
|
||||||
|
pytest tests/test_party_customer_sync.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: FAIL(模块或符号不存在)
|
||||||
|
|
||||||
|
- [ ] **Step 3: 实现 `party_customer_sync.py`**
|
||||||
|
|
||||||
|
实现要点(完整代码按 spec §4–§5):
|
||||||
|
|
||||||
|
```python
|
||||||
|
# backend/app/services/party_customer_sync.py
|
||||||
|
ROLE_MOTHER = "mother"
|
||||||
|
ROLE_FATHER = "father"
|
||||||
|
ROLE_NEWBORN = "newborn"
|
||||||
|
|
||||||
|
# 字段映射:角色 → (name_keys, phone_keys, id_card_keys, customer_id_keys, default_sex)
|
||||||
|
# sync_party_customers(db, party_dict, tenant_no, user_id) -> dict(snake 键写回)
|
||||||
|
# ensure_one_role(...): 无姓名清 ID;有姓名按证查找/新建/更新/合并
|
||||||
|
# merge_customer_fields(target, source)
|
||||||
|
# repoint_customer_refs(db, from_id, to_id, tenant_no): 更新两边当事人三列
|
||||||
|
# soft_delete via customer_crud 或直接 del_flag=1
|
||||||
|
```
|
||||||
|
|
||||||
|
输出字典必须用 snake_case 键:`customer_id`、`father_customer_id`、`newborn_customer_id`,以便 `upsert_party_customer` / `_create_party` 直接吃。输入兼容 camelCase。
|
||||||
|
|
||||||
|
有姓名无证、且本角色尚无客户 ID:`create_customer`。
|
||||||
|
有姓名有证未撞库且已有客户 ID:更新该客户(姓名、电话、身份证),不换 ID。
|
||||||
|
撞库且 `A != B`:`merge_customer_fields(B, A)` → `repoint_customer_refs(A→B)` → 软删 A → 返回 B.id。
|
||||||
|
|
||||||
|
年龄:可选简单从 18 位身份证第 7–14 位算周岁;算不出则 `None`。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 跑测通过**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_party_customer_sync.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
Expected: PASS
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/services/party_customer_sync.py backend/tests/test_party_customer_sync.py
|
||||||
|
git commit -m "feat: sync mother/father/newborn customers on protocol party save"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3: CRUD / API 读写新字段 + 接入保存路径
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `backend/app/crud/collection_protocol.py`(`_party_to_dict`、`upsert_party_customer` 的 `_STR_FIELDS`)
|
||||||
|
- Modify: `backend/app/crud/research_donation.py`(`_party_to_dict`、`_PARTY_STR_FIELDS`、双向转化复制 `father_customer_id`/`newborn_customer_id`)
|
||||||
|
- Modify: `backend/app/api/collection_protocol.py`(删除或废弃 `_ensure_customer_for_mother`,在 `_persist_protocol_sub_resources` 调用 `sync_party_customers`)
|
||||||
|
- Modify: `backend/app/crud/research_donation.py` 的 `create_donation` / `update_donation`(在 `_create_party` 前 sync)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 扩展 dict / 字段列表**
|
||||||
|
|
||||||
|
`collection_protocol._party_to_dict` 增加:
|
||||||
|
|
||||||
|
```python
|
||||||
|
"fatherCustomerId": p.father_customer_id or "",
|
||||||
|
"newbornCustomerId": p.newborn_customer_id or "",
|
||||||
|
```
|
||||||
|
|
||||||
|
`upsert_party_customer` 的 `_STR_FIELDS` 增加 `"father_customer_id"`, `"newborn_customer_id"`。
|
||||||
|
|
||||||
|
科研捐赠 `_party_to_dict` / `_PARTY_STR_FIELDS` 同样增加;`convert` 复制 party 时带上两列。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 采集协议保存接入**
|
||||||
|
|
||||||
|
在 `_persist_protocol_sub_resources`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.party_customer_sync import sync_party_customers
|
||||||
|
|
||||||
|
if party_customer:
|
||||||
|
_parse_datetime_fields(party_customer, ["newbornDeliveryTime"])
|
||||||
|
party_customer = sync_party_customers(
|
||||||
|
db, party_customer, tenant_no=tenant_no, user_id=user_id
|
||||||
|
)
|
||||||
|
cp_svc.upsert_party_customer(db, protocol_id, tenant_no, user_id, **party_customer)
|
||||||
|
```
|
||||||
|
|
||||||
|
删除对 `_ensure_customer_for_mother` 的调用;可删函数本身以免双轨。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 科研捐赠保存接入**
|
||||||
|
|
||||||
|
在 `create_donation` / `update_donation` 中,归一化 `party` 后:
|
||||||
|
|
||||||
|
```python
|
||||||
|
from app.services.party_customer_sync import sync_party_customers
|
||||||
|
|
||||||
|
party_raw = sync_party_customers(db, party_raw, tenant_no=tenant_no, user_id=user_id)
|
||||||
|
_create_party(db, donation_id, tenant_no, user_id, party_raw, now)
|
||||||
|
```
|
||||||
|
|
||||||
|
(update 路径:软删旧 party 后同样先 sync 再 create。)
|
||||||
|
|
||||||
|
- [ ] **Step 4: 跑同步单测 + 相关协议测试冒烟**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_party_customer_sync.py tests/test_collection_protocol_save_bundle.py -v --tb=short
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/crud/collection_protocol.py backend/app/crud/research_donation.py \
|
||||||
|
backend/app/api/collection_protocol.py
|
||||||
|
git commit -m "feat: wire party customer sync into storage and donation saves"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 4: 客户关联查询 API(TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `backend/app/services/customer_relations.py`
|
||||||
|
- Create: `backend/tests/test_customer_relations.py`
|
||||||
|
- Modify: `backend/app/api/basic_info.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 写聚合单测(mock 查询结果)**
|
||||||
|
|
||||||
|
覆盖:同一客户两张协议 → `relatedGroups` 长度 2;角色识别 mother/father/newborn;`protocols` 与 groups 同源字段。
|
||||||
|
|
||||||
|
- [ ] **Step 2: 实现 `build_customer_relations(db, customer_id, tenant_no)`**
|
||||||
|
|
||||||
|
逻辑:
|
||||||
|
|
||||||
|
1. 查 `BizCustomer`(租户 + 未删),没有则返回 `None`。
|
||||||
|
2. 查 `BizProtocolPartyCustomer`:`del_flag=0` 且三 ID 任一等于 customer_id;join 主表取 `protocol_no`/`protocol_status`/`register_date`/`create_time`。
|
||||||
|
3. 查 `BizResearchDonationParty` 同样,join 捐赠主表取 `donation_no`/`donation_status`/`donation_date`。
|
||||||
|
4. 合并后按主表 `create_time` 倒序。
|
||||||
|
5. 每组 `members`:有姓名的角色都列入;`customerId` 可空但姓名用当事人字段;已删客户显示当事人姓名。
|
||||||
|
6. `myRole` 按哪个 ID 列命中判定。
|
||||||
|
|
||||||
|
返回结构与 spec §7.2 一致。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 注册路由**
|
||||||
|
|
||||||
|
在 `basic_info.py`,放在 `GET /customers` 与 `PUT /customers/{id}` 之间注意路径冲突——`relations` 子路径须注册在会吞掉的动态路由之前,或使用:
|
||||||
|
|
||||||
|
```python
|
||||||
|
@router.get("/customers/{customer_id}/relations")
|
||||||
|
async def get_customer_relations(
|
||||||
|
customer_id: str,
|
||||||
|
user: dict = Depends(require_menu_code("biz:customer:list")),
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
):
|
||||||
|
tenant_no = _get_tenant_no(db, user)
|
||||||
|
data = build_customer_relations(db, customer_id, tenant_no)
|
||||||
|
if not data:
|
||||||
|
return error_response(404, "客户不存在")
|
||||||
|
return success_response(data)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: 跑测**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pytest tests/test_customer_relations.py tests/test_party_customer_sync.py -v
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/app/services/customer_relations.py backend/tests/test_customer_relations.py \
|
||||||
|
backend/app/api/basic_info.py
|
||||||
|
git commit -m "feat: add customer relations API for family and protocols"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 5: 存量回填脚本
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `backend/scripts/backfill_party_customer_ids.py`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 实现幂等脚本**
|
||||||
|
|
||||||
|
```python
|
||||||
|
"""回填当事人父亲/子女/缺失母亲客户 ID。可重复执行。
|
||||||
|
|
||||||
|
用法(在 backend 目录、已配置 .env):
|
||||||
|
python -m scripts.backfill_party_customer_ids
|
||||||
|
"""
|
||||||
|
# 对每条未删 party:组装 dict → sync_party_customers → 写回三 ID(仅当有变更)
|
||||||
|
# 细胞存储用 upsert_party_customer;捐赠用更新现有 party 行字段后 commit
|
||||||
|
# 打印处理条数 / 跳过条数
|
||||||
|
```
|
||||||
|
|
||||||
|
脚本内部调用与保存相同的 `sync_party_customers`,禁止另写一套匹配规则。
|
||||||
|
|
||||||
|
- [ ] **Step 2: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/scripts/backfill_party_customer_ids.py
|
||||||
|
git commit -m "chore: add idempotent backfill for party customer ids"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 6: 前端类型、API、查看弹窗
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `web-admin/src/types/api.d.ts`
|
||||||
|
- Modify: `web-admin/src/api/admin.ts`
|
||||||
|
- Modify: `web-admin/src/components/business/system-view-dialog/index.vue`
|
||||||
|
- Modify: `web-admin/src/views/basic-info/customer/index.vue`
|
||||||
|
|
||||||
|
- [ ] **Step 1: 类型与 API**
|
||||||
|
|
||||||
|
`api.d.ts` 的 `BizCustomer` 增加:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
interface RelationMember {
|
||||||
|
role: 'mother' | 'father' | 'newborn'
|
||||||
|
customerId: string
|
||||||
|
name: string
|
||||||
|
phone: string
|
||||||
|
idCardNo: string
|
||||||
|
}
|
||||||
|
interface RelationGroup {
|
||||||
|
bizType: 'cell_storage' | 'research_donation'
|
||||||
|
protocolId: string
|
||||||
|
protocolNo: string
|
||||||
|
protocolStatus: string
|
||||||
|
registerDate: string
|
||||||
|
myRole: 'mother' | 'father' | 'newborn'
|
||||||
|
members: RelationMember[]
|
||||||
|
}
|
||||||
|
interface ProtocolSummary {
|
||||||
|
bizType: 'cell_storage' | 'research_donation'
|
||||||
|
protocolId: string
|
||||||
|
protocolNo: string
|
||||||
|
protocolStatus: string
|
||||||
|
registerDate: string
|
||||||
|
myRole: 'mother' | 'father' | 'newborn'
|
||||||
|
}
|
||||||
|
interface CustomerRelations {
|
||||||
|
customer: CustomerItem
|
||||||
|
relatedGroups: RelationGroup[]
|
||||||
|
protocols: ProtocolSummary[]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`admin.ts`:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
export function fetchCustomerRelations(id: string) {
|
||||||
|
return request.get<Api.BizCustomer.CustomerRelations>({
|
||||||
|
url: `/web/admin/customers/${id}/relations`
|
||||||
|
})
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: SystemViewDialog 增加 append slot**
|
||||||
|
|
||||||
|
在 sections 循环之后、滚动容器结束前:
|
||||||
|
|
||||||
|
```vue
|
||||||
|
<div v-if="$slots.append" class="view-append">
|
||||||
|
<slot name="append" />
|
||||||
|
</div>
|
||||||
|
```
|
||||||
|
|
||||||
|
样式对齐 `.view-prepend`(若有)。
|
||||||
|
|
||||||
|
- [ ] **Step 3: 客户页详情**
|
||||||
|
|
||||||
|
`handleView` 改为加载 relations:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
async function handleView(row: Api.BizCustomer.CustomerItem) {
|
||||||
|
viewRow.value = row
|
||||||
|
viewVisible.value = true
|
||||||
|
relationsLoading.value = true
|
||||||
|
try {
|
||||||
|
const res = await fetchCustomerRelations(row.id)
|
||||||
|
relations.value = res.data // 按项目 request 解包习惯调整
|
||||||
|
viewRow.value = relations.value.customer
|
||||||
|
} finally {
|
||||||
|
relationsLoading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`SystemViewDialog` 增加 `#append`:
|
||||||
|
|
||||||
|
1. **关联客户**:`v-for` `relatedGroups`;组头显示类型 Tag + 协议号链接 + 本人角色;成员表/列表,本人标「本人」,他人姓名可点 → 再调 `handleView({ id: member.customerId, ... })`。
|
||||||
|
2. **关联协议**:`el-table` 或简洁列表;协议号 `@click`:
|
||||||
|
- `cell_storage` → 打开 `ProtocolViewDialog`(`:protocol-id` / 现有 props)
|
||||||
|
- `research_donation` → 打开 `DonationViewDialog`
|
||||||
|
3. 空态:「暂无关联协议」。
|
||||||
|
|
||||||
|
协议状态展示用现有 `status-tag-registry`(若已有协议/捐赠状态 label 函数则复用)。
|
||||||
|
|
||||||
|
Import:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import ProtocolViewDialog from '@/views/cell-storage/protocol/ProtocolViewDialog.vue'
|
||||||
|
import DonationViewDialog from '@/views/research-donation/list/DonationViewDialog.vue'
|
||||||
|
```
|
||||||
|
|
||||||
|
按钮图标规则:若新增可点链接旁的按钮,须带 `el-icon`。
|
||||||
|
|
||||||
|
- [ ] **Step 4: 本地类型检查(可选冒烟)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -60
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add web-admin/src/types/api.d.ts web-admin/src/api/admin.ts \
|
||||||
|
web-admin/src/components/business/system-view-dialog/index.vue \
|
||||||
|
web-admin/src/views/basic-info/customer/index.vue
|
||||||
|
git commit -m "feat(web-admin): show related family and protocols on customer detail"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 7: 图谱更新 + 手工验收清单
|
||||||
|
|
||||||
|
**Files:** 无业务代码(或仅 graphify 产物)
|
||||||
|
|
||||||
|
- [ ] **Step 1: 更新知识图谱**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./scripts/update-graph.sh backend
|
||||||
|
./scripts/update-graph.sh frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: 手工验收(本地已跑迁移 + 回填后)**
|
||||||
|
|
||||||
|
1. 新建细胞存储协议,填母/父/子女姓名(无证)→ 客户列表出现 3 人;三人详情互相看到同组。
|
||||||
|
2. 给子女补身份证且库中已有同证客户 → 临时户软删,协议挂到已有客户。
|
||||||
|
3. 同一母亲第二张协议 → 详情两组关联。
|
||||||
|
4. 科研捐赠同样验证。
|
||||||
|
5. 点协议号弹出对应只读弹窗。
|
||||||
|
6. 回填脚本跑两遍,第二次无新增客户。
|
||||||
|
|
||||||
|
- [ ] **Step 3: Commit graphify(若有变更)**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add backend/graphify-out web-admin/graphify-out
|
||||||
|
git commit -m "chore(graphify): refresh after customer family protocol link"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Spec coverage checklist
|
||||||
|
|
||||||
|
| Spec 项 | Task |
|
||||||
|
|---------|------|
|
||||||
|
| 当事人加父亲/子女客户 ID | Task 1 |
|
||||||
|
| 保存当前单同步三人 | Task 2–3 |
|
||||||
|
| 补证合并软删 | Task 2 |
|
||||||
|
| 存量脚本 + 保存继续同步 | Task 3 + 5 |
|
||||||
|
| relations API | Task 4 |
|
||||||
|
| 详情关联客户 + 协议摘要 + 弹窗 | Task 6 |
|
||||||
|
| 细胞存储 + 科研捐赠 | Task 3 |
|
||||||
|
| 中试不动 | (无 Task) |
|
||||||
|
|
||||||
|
## Self-review notes
|
||||||
|
|
||||||
|
- 无 TBD /「类似 Task N」占位。
|
||||||
|
- `sync_party_customers` 与回填共用,避免双轨规则。
|
||||||
|
- 路由 `GET /customers/{id}/relations` 与现有 CRUD 并存。
|
||||||
|
- 前端用 `#append` 保证基本信息在上、关联在下。
|
||||||
Reference in new issue
Block a user