docs: add research donation list Excel parity implementation plan
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
1 parent
c22c9467f5
commit
56f379dd0c
1 file changed
+646
@@ -0,0 +1,646 @@
|
||||
# 科研捐赠列表对齐 Excel「科研表」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:** Web / 小程序「科研捐赠」列表字段对齐 Excel「科研表」,并在创建时生成当月核量号(`YYYYMM###`);历史不回填。
|
||||
|
||||
**Architecture:** `biz_research_donation.monthly_verify_no` 落库;创建与「转科研」建单路径统一取号;Web 列表经 flatten 展列;小程序 `protocolCard` 对 `research_donation` 使用独立字段集。附件有无由前端对 URL 字段判空。
|
||||
|
||||
**Tech Stack:** FastAPI / SQLAlchemy / pytest、Vue 3 + Element Plus(web-admin)、mobile-oa-h5(node:test)
|
||||
|
||||
**Spec:** `docs/superpowers/specs/2026-10-13-research-donation-list-excel-parity-design.md`
|
||||
|
||||
---
|
||||
|
||||
## File map
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `docs/sql/migrations/2026-10-13-research-donation-monthly-verify-no.sql` | 加列 + 唯一索引 |
|
||||
| `backend/app/models/research_donation.py` | ORM 字段 |
|
||||
| `backend/app/crud/research_donation.py` | 取号、create / 转科研写号、list 补字段、`_donation_to_dict` |
|
||||
| `backend/tests/test_research_donation_monthly_verify_no.py` | 取号与创建单测(可 mock Session) |
|
||||
| `backend/app/services/market_protocol.py` | 小程序列表补齐科研字段 |
|
||||
| `web-admin/src/utils/protocol-audit-flatten.ts` | flatten 增加科研列表列字段 |
|
||||
| `web-admin/src/utils/protocol-audit-flatten.test.ts` | flatten 单测 |
|
||||
| `web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue` | 表格列替换 |
|
||||
| `mobile-oa-h5/src/market/list-presenter.js` | 科研捐赠卡片字段 |
|
||||
| `mobile-oa-h5/src/market/list-presenter.test.js` | 卡片契约测试 |
|
||||
|
||||
---
|
||||
|
||||
### Task 1: DB 迁移 + ORM 字段
|
||||
|
||||
**Files:**
|
||||
- Create: `docs/sql/migrations/2026-10-13-research-donation-monthly-verify-no.sql`
|
||||
- Modify: `backend/app/models/research_donation.py`
|
||||
|
||||
- [ ] **Step 1: 写迁移 SQL**
|
||||
|
||||
```sql
|
||||
-- 科研捐赠:当月核量号(创建时按 create_time 年月生成 YYYYMM###;历史可空)
|
||||
-- Spec: docs/superpowers/specs/2026-10-13-research-donation-list-excel-parity-design.md
|
||||
|
||||
SET NAMES utf8mb4;
|
||||
|
||||
ALTER TABLE `biz_research_donation`
|
||||
ADD COLUMN `monthly_verify_no` varchar(16) DEFAULT NULL COMMENT '当月核量号 YYYYMM+3位序号' AFTER `donation_no`;
|
||||
|
||||
-- 租户内号唯一;多行 NULL 在 MySQL 下不冲突
|
||||
ALTER TABLE `biz_research_donation`
|
||||
ADD UNIQUE KEY `uk_rd_tenant_monthly_verify_no` (`tenant_no`, `monthly_verify_no`);
|
||||
```
|
||||
|
||||
- [ ] **Step 2: ORM 增加列**
|
||||
|
||||
在 `BizResearchDonation` 中 `donation_no` 后增加:
|
||||
|
||||
```python
|
||||
monthly_verify_no = Column(String(16), nullable=True, comment="当月核量号 YYYYMM+3位序号")
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add docs/sql/migrations/2026-10-13-research-donation-monthly-verify-no.sql \
|
||||
backend/app/models/research_donation.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(db): add research donation monthly_verify_no column
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: 取号函数(TDD)
|
||||
|
||||
**Files:**
|
||||
- Create: `backend/tests/test_research_donation_monthly_verify_no.py`
|
||||
- Modify: `backend/app/crud/research_donation.py`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
```python
|
||||
"""当月核量号:按租户+创建年月自增,含软删占用。"""
|
||||
from datetime import datetime
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from app.crud import research_donation as crud
|
||||
|
||||
|
||||
def test_allocate_monthly_verify_no_first_of_month():
|
||||
db = MagicMock()
|
||||
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = (None,)
|
||||
no = crud.allocate_monthly_verify_no(
|
||||
db, tenant_no="zkxw", create_time=datetime(2025, 8, 15, 10, 0, 0)
|
||||
)
|
||||
assert no == "202508001"
|
||||
|
||||
|
||||
def test_allocate_monthly_verify_no_increments():
|
||||
db = MagicMock()
|
||||
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = (
|
||||
"202508007",
|
||||
)
|
||||
no = crud.allocate_monthly_verify_no(
|
||||
db, tenant_no="zkxw", create_time=datetime(2025, 8, 20, 12, 0, 0)
|
||||
)
|
||||
assert no == "202508008"
|
||||
|
||||
|
||||
def test_allocate_monthly_verify_no_new_month_resets():
|
||||
db = MagicMock()
|
||||
db.query.return_value.filter.return_value.order_by.return_value.first.return_value = (None,)
|
||||
no = crud.allocate_monthly_verify_no(
|
||||
db, tenant_no="zkxw", create_time=datetime(2025, 9, 1, 9, 0, 0)
|
||||
)
|
||||
assert no == "202509001"
|
||||
```
|
||||
|
||||
> 实现若用 `db.execute(text(...))` 查 MAX,则测试改为 mock `execute` / `scalar`,断言仍保持上述期望值。
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_research_donation_monthly_verify_no.py -v
|
||||
```
|
||||
|
||||
Expected: FAIL(`allocate_monthly_verify_no` 不存在)
|
||||
|
||||
- [ ] **Step 3: 实现取号**
|
||||
|
||||
在 `research_donation.py` 增加(查询须**不排除** `del_flag=1`):
|
||||
|
||||
```python
|
||||
def allocate_monthly_verify_no(
|
||||
db: Session,
|
||||
*,
|
||||
tenant_no: str,
|
||||
create_time: datetime,
|
||||
) -> str:
|
||||
"""按租户 + create_time 年月分配 YYYYMM###;含软删占用,不回收。"""
|
||||
ym = create_time.strftime("%Y%m")
|
||||
prefix_len = len(ym)
|
||||
row = (
|
||||
db.query(BizResearchDonation.monthly_verify_no)
|
||||
.filter(
|
||||
BizResearchDonation.tenant_no == tenant_no,
|
||||
BizResearchDonation.monthly_verify_no.isnot(None),
|
||||
BizResearchDonation.monthly_verify_no.like(f"{ym}%"),
|
||||
)
|
||||
.order_by(BizResearchDonation.monthly_verify_no.desc())
|
||||
.first()
|
||||
)
|
||||
max_no = (row[0] if row else None) or ""
|
||||
seq = 0
|
||||
if max_no.startswith(ym) and len(max_no) >= prefix_len + 3:
|
||||
try:
|
||||
seq = int(max_no[prefix_len : prefix_len + 3])
|
||||
except ValueError:
|
||||
seq = 0
|
||||
nxt = seq + 1
|
||||
if nxt > 999:
|
||||
raise ValueError(f"当月核量号已用尽({ym})")
|
||||
return f"{ym}{nxt:03d}"
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_research_donation_monthly_verify_no.py -v
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/crud/research_donation.py backend/tests/test_research_donation_monthly_verify_no.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(backend): allocate research donation monthly verify numbers
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 创建与转科研写号 + 序列化
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/crud/research_donation.py`(`create_donation`、转科研建主表处、`_donation_to_dict`、`list_donations` 展平 party/collection)
|
||||
|
||||
- [ ] **Step 1: `_donation_to_dict` 增加字段**
|
||||
|
||||
```python
|
||||
"monthlyVerifyNo": d.monthly_verify_no or "",
|
||||
```
|
||||
|
||||
放在 `donationNo` 附近。
|
||||
|
||||
- [ ] **Step 2: `create_donation` 写号**
|
||||
|
||||
在构造 `BizResearchDonation(...)` 前:
|
||||
|
||||
```python
|
||||
monthly_verify_no = allocate_monthly_verify_no(
|
||||
db, tenant_no=tenant_no, create_time=now
|
||||
)
|
||||
```
|
||||
|
||||
并传入 `monthly_verify_no=monthly_verify_no`。唯一约束冲突时可在循环内重试最多 3 次(可选,最小实现可先不加)。
|
||||
|
||||
- [ ] **Step 3: 转科研建单写号**
|
||||
|
||||
`convert_protocol_to_donation`(或同文件内直接 `BizResearchDonation(...)` 的 `source_type="from_storage"` 路径,约 1607 行)同样在 `create_time=now` 后调用 `allocate_monthly_verify_no` 并写入。
|
||||
|
||||
- [ ] **Step 4: `list_donations` 补齐列表缺字段**
|
||||
|
||||
在 party 展平处增加:
|
||||
|
||||
```python
|
||||
d["birthOrder"] = pc.get("birthOrder")
|
||||
```
|
||||
|
||||
无 party 时 `d["birthOrder"] = None`。
|
||||
|
||||
在 collection 展平处增加:
|
||||
|
||||
```python
|
||||
d["maternalBloodCollectTime"] = ci.get("maternalBloodCollectTime", "")
|
||||
```
|
||||
|
||||
无 collection 时置 `""`。
|
||||
|
||||
(`voidDesc` / 三类附件 URL 已在 `_donation_to_dict`;确认 list 走该序列化即可。)
|
||||
|
||||
- [ ] **Step 5: 补充集成向测试(可选轻量)**
|
||||
|
||||
在同测试文件增加对「create 路径会调用 allocate」的 mock 断言,或文档化手动跑迁移后冒烟;至少保证 Task 2 单测仍绿。
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/crud/research_donation.py backend/tests/test_research_donation_monthly_verify_no.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(backend): persist monthly verify no on donation create and convert
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 小程序列表 API 字段补齐
|
||||
|
||||
**Files:**
|
||||
- Modify: `backend/app/services/market_protocol.py`(`_enrich_protocol_list_items` 或同类循环 `item.update(...)` 处)
|
||||
- Test: `backend/tests/test_market_protocol_api.py`(若有科研列表断言则扩展;否则新增断言字段存在)
|
||||
|
||||
- [ ] **Step 1: 在 list enrich 中为每条 item 增加**
|
||||
|
||||
对科研捐赠主表字段(从 query 到的 `BizResearchDonation` row 或已有 item):
|
||||
|
||||
```python
|
||||
monthlyVerifyNo=getattr(row, 'monthly_verify_no', '') or item.get('monthlyVerifyNo', ''),
|
||||
voidDesc=getattr(row, 'void_desc', '') or '',
|
||||
consentFormUrls=getattr(row, 'consent_form_urls', '') or '',
|
||||
infectiousDiseaseReportUrls=getattr(row, 'infectious_disease_report_urls', '') or '',
|
||||
attachmentUrls=getattr(row, 'attachment_urls', '') or '',
|
||||
remark=getattr(row, 'remark', '') or item.get('remark', ''),
|
||||
birthOrder=getattr(party, 'birth_order', None),
|
||||
maternalBloodCollectTime=_fmt_dt(getattr(collection, 'maternal_blood_collect_time', None)),
|
||||
newbornDeliveryTime=_fmt_dt(getattr(party, 'newborn_delivery_time', None)),
|
||||
regionManagerUserName=user_names.get(
|
||||
getattr(collection, 'region_manager_user_id', ''), ''
|
||||
),
|
||||
```
|
||||
|
||||
说明:现网已有部分字段(`motherName`、`labStorageCode`、`transportMode` 等);本步只补缺口。`managerName` 已存在时可同时保证 `regionManagerUserName` 同值,方便前端统一读。
|
||||
|
||||
- [ ] **Step 2: 跑市场协议相关测试**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_market_protocol_api.py -v --tb=short
|
||||
```
|
||||
|
||||
Expected: PASS(或按新增断言修复)
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add backend/app/services/market_protocol.py backend/tests/test_market_protocol_api.py
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(backend): expose research donation list fields for market mobile
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5: Web flatten 扩展(TDD)
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/utils/protocol-audit-flatten.ts`
|
||||
- Modify: `web-admin/src/utils/protocol-audit-flatten.test.ts`
|
||||
|
||||
- [ ] **Step 1: 扩展 `ProtocolAuditFlatRow` 类型与 flatten 输出**
|
||||
|
||||
新增字段(从 `protocol` 读取):
|
||||
|
||||
```typescript
|
||||
monthlyVerifyNo: string
|
||||
donationStatus: string
|
||||
voidDesc: string
|
||||
newbornDeliveryTime: string
|
||||
newbornSex: string
|
||||
newbornWeightGram: string
|
||||
deliveryMode: string
|
||||
birthOrder: string
|
||||
maternalBloodCollectTime: string
|
||||
consentFormUrls: string
|
||||
infectiousDiseaseReportUrls: string
|
||||
attachmentUrls: string
|
||||
remark: string
|
||||
```
|
||||
|
||||
在 `out.push({...})` 中赋值,例如:
|
||||
|
||||
```typescript
|
||||
monthlyVerifyNo: str(protocol.monthlyVerifyNo),
|
||||
donationStatus: str(protocol.donationStatus),
|
||||
voidDesc: str(protocol.voidDesc),
|
||||
newbornDeliveryTime: str(protocol.newbornDeliveryTime),
|
||||
newbornSex: str(protocol.newbornSex),
|
||||
newbornWeightGram:
|
||||
protocol.newbornWeightGram == null || protocol.newbornWeightGram === ''
|
||||
? ''
|
||||
: String(protocol.newbornWeightGram),
|
||||
deliveryMode: str(protocol.deliveryMode),
|
||||
birthOrder:
|
||||
protocol.birthOrder == null || protocol.birthOrder === ''
|
||||
? ''
|
||||
: String(protocol.birthOrder),
|
||||
maternalBloodCollectTime: str(protocol.maternalBloodCollectTime),
|
||||
consentFormUrls: str(protocol.consentFormUrls),
|
||||
infectiousDiseaseReportUrls: str(protocol.infectiousDiseaseReportUrls),
|
||||
attachmentUrls: str(protocol.attachmentUrls),
|
||||
remark: str(protocol.remark),
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 单测断言新字段从 protocol 透传**
|
||||
|
||||
在 `protocol-audit-flatten.test.ts` 增加用例:输入含 `monthlyVerifyNo`、`consentFormUrls` 等的捐赠行,断言 flat 行对应字段。
|
||||
|
||||
- [ ] **Step 3: 跑测试**
|
||||
|
||||
```bash
|
||||
cd web-admin && pnpm exec vitest run src/utils/protocol-audit-flatten.test.ts
|
||||
```
|
||||
|
||||
(若项目用其他 runner,以该文件现有跑法为准,例如 `node --import tsx` / `pnpm test`。)
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 4: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/utils/protocol-audit-flatten.ts \
|
||||
web-admin/src/utils/protocol-audit-flatten.test.ts
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): flatten research donation excel-parity list fields
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6: Web `DonationProtocolTab` 列对齐
|
||||
|
||||
**Files:**
|
||||
- Modify: `web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue`
|
||||
|
||||
- [ ] **Step 1: 调整表格列**
|
||||
|
||||
按 spec §4.1 顺序替换列(去掉「是否同存」):
|
||||
|
||||
1. 选择、序号、采集日期(已有)
|
||||
2. **当月核量号** → `row.monthlyVerifyNo`,monospace,空「—」
|
||||
3. 实验室流转编号(已有)
|
||||
4. **协议状态** → `donationStatusDisplayLabel` + `el-tag`(从 `asDonation(row)` 或 `row.donationStatus`;复用 `research-donation/list` 或现有 status 工具)
|
||||
5. **废弃原因** → `row.voidDesc || '—'`
|
||||
6. 母亲姓名、样本类型(已有;去掉是否同存列)
|
||||
7. 采集地区、采集医院(已有)
|
||||
8. **分娩时间** → `formatDisplayOrDash(row.newbornDeliveryTime)`
|
||||
9. 采集时间、市场专员、地区经理、运输方式(已有)
|
||||
10. **新生儿性别** → `newbornSexLabel(row.newbornSex)`(从 `@/utils/...` 或与 `research-donation/list/index.vue` 相同 import)
|
||||
11. **新生儿体重** → 有值显示数字,空「—」
|
||||
12. **分娩方式** → `deliveryModeLabel`
|
||||
13. **胎次** → `row.birthOrder || '—'`
|
||||
14. **母血采集时间** → `formatDisplayOrDash(row.maternalBloodCollectTime)`
|
||||
15. **知情同意书 / 传染病检测报告 / 协议书** → 非空 URL 显示「有」,否则「—」
|
||||
|
||||
辅助函数:
|
||||
|
||||
```typescript
|
||||
function hasAttachmentUrls(raw: string | undefined | null): boolean {
|
||||
return Boolean(String(raw || '').trim())
|
||||
}
|
||||
```
|
||||
|
||||
模板示例:
|
||||
|
||||
```vue
|
||||
{{ hasAttachmentUrls(row.consentFormUrls) ? '有' : '—' }}
|
||||
```
|
||||
|
||||
16. **备注** → `row.remark || '—'`,可 `show-overflow-tooltip`
|
||||
17. 操作列(已有,`min-width` ≥ 260)
|
||||
|
||||
- [ ] **Step 2: 本地目视**
|
||||
|
||||
打开 `/#/market/cell-storage/protocol-audit?tab=research_donation`,确认列顺序、无运费/是否同存、横向可滚。
|
||||
|
||||
- [ ] **Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add web-admin/src/views/cell-storage/protocol-audit/DonationProtocolTab.vue
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(web-admin): align research donation audit list with excel columns
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 小程序科研捐赠列表卡片
|
||||
|
||||
**Files:**
|
||||
- Modify: `mobile-oa-h5/src/market/list-presenter.js`
|
||||
- Modify: `mobile-oa-h5/src/market/list-presenter.test.js`
|
||||
|
||||
- [ ] **Step 1: 写失败测试**
|
||||
|
||||
在 `list-presenter.test.js`:
|
||||
|
||||
```javascript
|
||||
test('research donation protocol card matches excel-parity field set', () => {
|
||||
const card = buildMarketCard('protocol', 'specialist', {
|
||||
source: 'research_donation',
|
||||
motherName: '曹陆双',
|
||||
sampleCode: 'S-D1',
|
||||
donationStatus: 'sdfq',
|
||||
sampleCollectTime: '2025-08-29 12:27:00',
|
||||
monthlyVerifyNo: '202508001',
|
||||
labStorageCode: 'A0000001',
|
||||
voidDesc: '母血乙肝阳性',
|
||||
sampleTypeName: '围产期造血干细胞',
|
||||
regionName: '亳州',
|
||||
hospitalName: '涡阳县人民医院',
|
||||
newbornDeliveryTime: '2025-08-29 12:26:00',
|
||||
salesUserName: '杨莉莉',
|
||||
regionManagerUserName: '张颖',
|
||||
managerName: '张颖',
|
||||
transportMode: 'kdys',
|
||||
transportModeLabel: '快递运输',
|
||||
newbornSex: '0',
|
||||
newbornWeightGram: 3000,
|
||||
deliveryMode: 'sc',
|
||||
birthOrder: 1,
|
||||
maternalBloodCollectTime: '2025-08-29 13:30:00',
|
||||
consentFormUrls: 'https://x/a.pdf',
|
||||
infectiousDiseaseReportUrls: '',
|
||||
attachmentUrls: 'https://x/b.pdf',
|
||||
remark: '备注一',
|
||||
allowedActions: ['edit']
|
||||
})
|
||||
const labels = card.fields.map((f) => f.label)
|
||||
for (const label of [
|
||||
'当月核量号', '实验室流转编号', '协议状态', '废弃原因', '样本类型',
|
||||
'采集地区', '采集医院', '分娩时间', '采集时间', '市场专员', '地区经理',
|
||||
'运输方式', '新生儿性别', '新生儿体重', '分娩方式', '胎次', '母血采集时间',
|
||||
'知情同意书', '传染病检测报告', '协议书', '备注'
|
||||
]) {
|
||||
assert.ok(labels.includes(label), `missing ${label}`)
|
||||
}
|
||||
assert.equal(card.fields.find((f) => f.label === '当月核量号').value, '202508001')
|
||||
assert.equal(card.fields.find((f) => f.label === '知情同意书').value, '有')
|
||||
assert.equal(card.fields.find((f) => f.label === '传染病检测报告').value, '—')
|
||||
assert.equal(card.fields.find((f) => f.label === '协议书').value, '有')
|
||||
assert.ok(!labels.includes('是否同存'))
|
||||
})
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试确认失败**
|
||||
|
||||
```bash
|
||||
cd mobile-oa-h5 && node --test src/market/list-presenter.test.js
|
||||
```
|
||||
|
||||
Expected: FAIL(缺字段)
|
||||
|
||||
- [ ] **Step 3: 实现 `protocolCard` 分支**
|
||||
|
||||
```javascript
|
||||
function attachmentPresence(urls) {
|
||||
return String(urls || '').trim() ? '有' : '—'
|
||||
}
|
||||
|
||||
function protocolCard(item) {
|
||||
const actions = allowed(item, ['edit', 'delete'])
|
||||
const isDonation = item.source === 'research_donation'
|
||||
const status = label(PROTOCOL_STATUS_LABELS, item.protocolStatus || item.donationStatus || item.status)
|
||||
if (isDonation) {
|
||||
const collectDate = String(item.sampleCollectTime || '').slice(0, 10)
|
||||
return {
|
||||
title: text(item.motherName || item.customerName),
|
||||
subtitle: `${label(SOURCE_LABELS, item.source)} · ${text(item.labStorageCode || item.sampleCode)}`,
|
||||
status,
|
||||
fields: [
|
||||
field('采集日期', collectDate || item.donationDate),
|
||||
field('当月核量号', item.monthlyVerifyNo),
|
||||
field('实验室流转编号', item.labStorageCode || item.sampleCode, { wide: true }),
|
||||
field('协议状态', status, { tag: true }),
|
||||
field('废弃原因', item.voidDesc),
|
||||
field('母亲姓名', item.motherName || item.customerName),
|
||||
field('样本类型', item.sampleTypeName),
|
||||
field('采集地区', referenceName(item.regionName, item.regionId)),
|
||||
field('采集医院', referenceName(item.hospitalName, item.hospitalId), { wide: true }),
|
||||
field('分娩时间', item.newbornDeliveryTime, { wide: true }),
|
||||
field('采集时间', item.sampleCollectTime, { wide: true }),
|
||||
field('市场专员', personName(item.salesUserName, item.salesUserId)),
|
||||
field('地区经理', personName(item.regionManagerUserName || item.managerName, item.regionManagerUserId)),
|
||||
field('运输方式', item.transportModeLabel || item.transportMode),
|
||||
field('新生儿性别', label(NEWBORN_SEX_LABELS, item.newbornSex)),
|
||||
field('新生儿体重', item.newbornWeightGram),
|
||||
field('分娩方式', label(DELIVERY_MODE_LABELS, item.deliveryMode)),
|
||||
field('胎次', item.birthOrder),
|
||||
field('母血采集时间', item.maternalBloodCollectTime, { wide: true }),
|
||||
field('知情同意书', attachmentPresence(item.consentFormUrls)),
|
||||
field('传染病检测报告', attachmentPresence(item.infectiousDiseaseReportUrls)),
|
||||
field('协议书', attachmentPresence(item.attachmentUrls)),
|
||||
field('备注', item.remark, { wide: true })
|
||||
],
|
||||
actions: ['view', ...actions]
|
||||
}
|
||||
}
|
||||
// 原细胞存储卡片逻辑保持不变
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
保留现有 cell_storage 分支与原测试。
|
||||
|
||||
- [ ] **Step 4: 跑测试确认通过**
|
||||
|
||||
```bash
|
||||
cd mobile-oa-h5 && node --test src/market/list-presenter.test.js
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
git add mobile-oa-h5/src/market/list-presenter.js mobile-oa-h5/src/market/list-presenter.test.js
|
||||
git commit -m "$(cat <<'EOF'
|
||||
feat(mobile-oa-h5): align research donation protocol card with excel fields
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 回归验证 + graphify
|
||||
|
||||
- [ ] **Step 1: 后端测试**
|
||||
|
||||
```bash
|
||||
cd backend && source venv/bin/activate && pytest tests/test_research_donation_monthly_verify_no.py tests/test_market_protocol_api.py -v --tb=short
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 2: 前端 / 小程序测试**
|
||||
|
||||
```bash
|
||||
cd web-admin && pnpm exec vitest run src/utils/protocol-audit-flatten.test.ts
|
||||
cd mobile-oa-h5 && node --test src/market/list-presenter.test.js
|
||||
```
|
||||
|
||||
Expected: PASS
|
||||
|
||||
- [ ] **Step 3: 更新知识图谱(backend 有模型/CRUD 变更)**
|
||||
|
||||
```bash
|
||||
./scripts/update-graph.sh backend
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 手动验收清单**
|
||||
|
||||
- [ ] 迁移已在目标库执行
|
||||
- [ ] 新建科研捐赠出现 `YYYYMM001` 形态核量号;同月第二笔递增
|
||||
- [ ] 历史单据核量号为空显示「—」
|
||||
- [ ] Web 科研捐赠列表列符合 spec;细胞存储 Tab 无变化
|
||||
- [ ] 小程序科研捐赠卡片字段齐全;细胞存储卡片无回归
|
||||
- [ ] 附件三列仅「有」/「—」;无运费、无是否同存
|
||||
|
||||
- [ ] **Step 5: 若 graphify 有变更则提交**
|
||||
|
||||
```bash
|
||||
git add backend/graphify-out
|
||||
git commit -m "$(cat <<'EOF'
|
||||
chore: update backend graphify after monthly verify no
|
||||
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Spec coverage checklist
|
||||
|
||||
| Spec 要求 | Task |
|
||||
|-----------|------|
|
||||
| `monthly_verify_no` 落库 + 唯一索引 | 1 |
|
||||
| 格式 `YYYYMM###`、按创建时间年月、含软删 | 2 |
|
||||
| 创建写号;历史不回填 | 3 |
|
||||
| 转科研建单写号 | 3 |
|
||||
| list 暴露核量号 + birthOrder + 母血时间等 | 3、4 |
|
||||
| Web 列顺序 / 附件有无 / 去是否同存与运费 | 5、6 |
|
||||
| 小程序科研卡片对齐 | 4、7 |
|
||||
| 详情/表单不展示核量号 | 不做 UI(默认满足) |
|
||||
| 细胞存储 Tab 不动 | 6、7 仅改科研路径 |
|
||||
|
||||
## Placeholder / ambiguity notes(已拍板)
|
||||
|
||||
- 附件:前端 URL 判空,不新增布尔字段。
|
||||
- 废弃原因:列表展示 `voidDesc`。
|
||||
- 协议书:`attachmentUrls`(其他附件)。
|
||||
Reference in new issue
Block a user