docs: 生产接收接种量文本化与冻存仅日期实现计划

按设计拆为字段单测、弹窗、后端 SQL/API、验收四步。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zlwuandCursor committed 2026-08-31 14:56:52 +08:00
1 parent fdd219c1e7
commit 431d7b9835
1 file changed
+426
@@ -0,0 +1,426 @@
# 生产接收:接种量文本化、冻存时间仅日期 — 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:** 将生产样本接收的「接种量」改为最长 255 的自由文本,将「冻存时间」改为仅日期(选择/展示 `YYYY-MM-DD`)。
**Architecture:** 库表 `inoculation_amount` 由 `decimal(18,4)` 改为 `VARCHAR(255)`;前后端按字符串读写。冻存时间列保持 `datetime`,前端 `kind: 'date'` + `el-date-picker type="date"`,展示用 `formatDateOnly`;后端继续 `parse_datetime` 兼容仅日期字符串。弹窗改动集中在共用 `SampleReceptionDialog.vue`。
**Tech Stack:** Vue 3 + Element Plus(web-admin)、FastAPI + SQLAlchemy(backend)、MySQL 5.7、node:test(前端纯函数单测)
**Spec:** `docs/superpowers/specs/2026-08-31-production-reception-inoculation-cryo-design.md`
---
## File map
| File | Responsibility |
|------|----------------|
| `docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql` | 线上/本地 `ALTER` 迁移 |
| `docs/sql/zkxw.sql` | 基线 CREATE 中列类型同步 |
| `backend/app/models/models.py` | ORM 列类型 |
| `backend/app/api/sample_production_reception.py` | Pydantic body 类型 |
| `backend/app/crud/sample_production_reception.py` | 读写作字符串 |
| `web-admin/src/utils/sample-production-reception-fields.ts` | 字段 kind / 类型 / 展示 |
| `web-admin/src/utils/sample-production-reception-fields.test.ts` | 字段定义与展示单测 |
| `web-admin/src/api/sample-production-reception.ts` | Payload TS 类型 |
| `web-admin/src/views/production/production-work-order/SampleReceptionDialog.vue` | 表单控件与回填 |
---
### Task 1: 前端字段定义 + 展示(TDD)
**Files:**
- Create: `web-admin/src/utils/sample-production-reception-fields.test.ts`
- Modify: `web-admin/src/utils/sample-production-reception-fields.ts`
- [ ] **Step 1: Write the failing test**
```ts
import { describe, it } from 'node:test'
import assert from 'node:assert/strict'
import {
resolveProductionReceptionFields,
formatProductionFieldValue,
type ProductionReceptionRecord
} from './sample-production-reception-fields'
describe('inoculationAmount / cryopreservationTime field kinds', () => {
it('marks inoculationAmount as text and cryopreservationTime as date for UC-MSC profiles', () => {
for (const source of ['cell_storage', 'research_donation'] as const) {
const fields = resolveProductionReceptionFields(source, '脐带间充质干细胞')
const inoc = fields.find((f) => f.key === 'inoculationAmount')
const cryo = fields.find((f) => f.key === 'cryopreservationTime')
assert.ok(inoc)
assert.equal(inoc.kind, 'text')
assert.equal(inoc.maxLength, 255)
assert.ok(cryo)
assert.equal(cryo.kind, 'date')
}
})
it('formats free-text inoculation and date-only cryopreservation', () => {
const fields = resolveProductionReceptionFields('cell_storage', '脐带间充质干细胞')
const inoc = fields.find((f) => f.key === 'inoculationAmount')!
const cryo = fields.find((f) => f.key === 'cryopreservationTime')!
const record: ProductionReceptionRecord = {
inoculationAmount: '约 5×10⁶ cells,接种体积 2ml',
cryopreservationTime: '2026-07-14 15:30:00'
}
assert.equal(formatProductionFieldValue(inoc, record), '约 5×10⁶ cells,接种体积 2ml')
assert.equal(formatProductionFieldValue(cryo, record), '2026-07-14')
})
})
```
- [ ] **Step 2: Run test to verify it fails**
```bash
cd web-admin && node --import tsx --test src/utils/sample-production-reception-fields.test.ts
```
Expected: FAIL(`kind` 仍为 `number`/`datetime`,或 `maxLength` / `date` 分支不存在)
- [ ] **Step 3: Update field definitions and formatter**
In `web-admin/src/utils/sample-production-reception-fields.ts`:
1. Extend kind union:
```ts
kind: 'number' | 'text' | 'datetime' | 'date' | 'select' | 'integer'
maxLength?: number
```
2. Change defs:
```ts
inoculationAmount: {
key: 'inoculationAmount',
label: '接种量',
kind: 'text',
maxLength: 255
},
cryopreservationTime: {
key: 'cryopreservationTime',
label: '冻存时间',
kind: 'date'
},
```
3. Change record type:
```ts
inoculationAmount?: string
```
4. In `formatProductionFieldValue`, after infectious / before or with datetime:
```ts
if (def.kind === 'datetime') {
return formatDateTimeDisplay(String(raw)) || '—'
}
if (def.kind === 'date') {
return formatDateOnly(String(raw)) || '—'
}
```
(`formatDateOnly` 已在本文件顶部从 `@/utils/datetime-display` 导入。)
- [ ] **Step 4: Run test to verify it passes**
```bash
cd web-admin && node --import tsx --test src/utils/sample-production-reception-fields.test.ts
```
Expected: PASS
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/utils/sample-production-reception-fields.ts \
web-admin/src/utils/sample-production-reception-fields.test.ts
git commit -m "$(cat <<'EOF'
feat(web-admin): 生产接收接种量改文本、冻存时间改日期展示
字段 kind 与只读格式对齐产品:自由文本接种量、冻存仅 YYYY-MM-DD。
EOF
)"
```
---
### Task 2: 弹窗表单与 API 类型
**Files:**
- Modify: `web-admin/src/api/sample-production-reception.ts`
- Modify: `web-admin/src/views/production/production-work-order/SampleReceptionDialog.vue`
- [ ] **Step 1: Update API payload type**
In `web-admin/src/api/sample-production-reception.ts`:
```ts
inoculationAmount?: string
```
- [ ] **Step 2: Update SampleReceptionDialog template**
Replace the text / datetime branches so text supports `maxLength`,并增加 `date`:
```vue
<el-input
v-if="field.kind === 'text'"
v-model="prodForm[field.key]"
clearable
:maxlength="field.maxLength"
:show-word-limit="!!field.maxLength"
/>
<DecimalInput
v-else-if="field.kind === 'number' || field.kind === 'integer'"
v-model="prodForm[field.key]"
:min="0"
:precision="field.kind === 'integer' ? 0 : 4"
style="width: 100%"
/>
<el-date-picker
v-else-if="field.kind === 'date'"
v-model="prodForm[field.key]"
type="date"
value-format="YYYY-MM-DD"
placeholder="选择日期"
style="width: 100%"
/>
<el-date-picker
v-else-if="field.kind === 'datetime'"
v-model="prodForm[field.key]"
type="datetime"
value-format="YYYY-MM-DD HH:mm:ss"
placeholder="选择时间"
style="width: 100%"
/>
```
- [ ] **Step 3: Fix populateFromProduction for date fields**
In `populateFromProduction`,对 `date`(及处理时间同类)回填截日期:
```ts
function populateFromProduction(rec: ProductionReceptionRecord) {
prodForm.processTime = formatProcessTimeDate(rec.processTime) || todayDate()
prodForm.handlerUserId = rec.handlerUserId || ''
prodForm.handlerName = rec.handlerName || ''
prodForm.remark = rec.remark || ''
for (const field of productionFieldDefs.value) {
const raw = rec[field.key as keyof ProductionReceptionRecord]
if (field.kind === 'date') {
prodForm[field.key] = formatProcessTimeDate(raw != null ? String(raw) : '') || ''
} else {
prodForm[field.key] = raw as never
}
}
}
```
(`formatProcessTimeDate` 即 `formatDateOnly` 封装,已导入本文件。)
`resetProdForm` 中 `date` 与 `text` 一样置 `''`(现有 `integer || number ? undefined : ''` 已覆盖)。
- [ ] **Step 4: Smoke-check TypeScript on touched files(optional local)**
```bash
cd web-admin && pnpm exec vue-tsc --noEmit 2>&1 | head -n 40
```
若环境慢可跳过;至少确认无新增明显类型错误。
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/api/sample-production-reception.ts \
web-admin/src/views/production/production-work-order/SampleReceptionDialog.vue
git commit -m "$(cat <<'EOF'
feat(web-admin): 样本接收弹窗接种量文本输入、冻存仅选日期
共用 SampleReceptionDialog,生产工单与我的生产任务一并生效。
EOF
)"
```
---
### Task 3: 后端模型 / API / CRUD + SQL
**Files:**
- Create: `docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql`
- Modify: `docs/sql/zkxw.sql`(CREATE 中 `inoculation_amount` 一行)
- Modify: `backend/app/models/models.py`
- Modify: `backend/app/api/sample_production_reception.py`
- Modify: `backend/app/crud/sample_production_reception.py`
- [ ] **Step 1: Add migration SQL**
Create `docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql`:
```sql
-- 生产接收接种量:数值 → 自由文本(最长 255)
-- 执行前请备份;MySQL 会将既有 decimal 值转为对应数字字符串。
ALTER TABLE biz_sample_production_reception
MODIFY COLUMN inoculation_amount VARCHAR(255) NULL COMMENT '接种量';
```
- [ ] **Step 2: Sync baseline zkxw.sql CREATE column**
In `docs/sql/zkxw.sql`,将:
```sql
`inoculation_amount` decimal(18,4) DEFAULT NULL COMMENT '接种量',
```
改为:
```sql
`inoculation_amount` varchar(255) DEFAULT NULL COMMENT '接种量',
```
(仅改 CREATE 定义行;不必改历史 INSERT 中的 NULL/数值字面量。)
- [ ] **Step 3: Update SQLAlchemy model**
In `backend/app/models/models.py` `BizSampleProductionReception`:
```python
inoculation_amount = Column(String(255), nullable=True, comment="接种量")
```
- [ ] **Step 4: Update API body type**
In `backend/app/api/sample_production_reception.py` `ProductionReceptionBody`:
```python
inoculation_amount: Optional[str] = Field(None, alias="inoculationAmount")
```
`_body_to_payload` 保持:
```python
"inoculationAmount": body.inoculation_amount,
```
(可选:写入前 `strip`;若在 CRUD 统一 strip 亦可。)
`cryopreservation_time` 仍 `parse_datetime`——`YYYY-MM-DD` 经 `fromisoformat` 可得当日 0 点,无需改解析。
- [ ] **Step 5: Update CRUD read/write**
In `backend/app/crud/sample_production_reception.py` `_row_to_dict`:
```python
"inoculationAmount": row.inoculation_amount or "",
```
In `_apply_payload_to_row`:
```python
row.inoculation_amount = (
(str(data.get("inoculationAmount") or "").strip() or None)
if "inoculationAmount" in allowed
else None
)
```
- [ ] **Step 6: Apply migration on local DB(if available)**
```bash
# 按本地习惯执行 ALTER;示例(需正确库连接):
# mysql ... < docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql
```
若本机无库,跳过并在验收说明中注明「需执行迁移」。
- [ ] **Step 7: Commit**
```bash
git add docs/sql/2026-08-31-production-reception-inoculation-amount-varchar.sql \
docs/sql/zkxw.sql \
backend/app/models/models.py \
backend/app/api/sample_production_reception.py \
backend/app/crud/sample_production_reception.py
git commit -m "$(cat <<'EOF'
feat(backend): 生产接收接种量改为 VARCHAR(255) 自由文本
同步模型、API、CRUD 与 SQL 迁移/基线,支持数字与说明文案组合录入。
EOF
)"
```
---
### Task 4: 图谱更新与验收
**Files:**
- 可能更新:`backend/graphify-out/*`、`web-admin/graphify-out/*`(由脚本生成)
- [ ] **Step 1: Update knowledge graph**
```bash
./scripts/update-graph.sh all
```
- [ ] **Step 2: Re-run frontend unit test**
```bash
cd web-admin && node --import tsx --test src/utils/sample-production-reception-fields.test.ts
```
Expected: PASS
- [ ] **Step 3: Manual acceptance checklist**
1. 脐带间充质干细胞:接种量可输入含中文/符号的长文案(≤255),保存回显一致。
2. 冻存时间选择器无时分秒;保存后只读与再编辑均为 `YYYY-MM-DD`。
3. 历史冻存带时分秒数据打开弹窗可回填日期部分。
4. 生产工单与我的生产任务行为一致。
5. 围产期造血干细胞 profile 数值字段仍为 `DecimalInput`。
- [ ] **Step 4: Mark design implemented**
Update header in `docs/superpowers/specs/2026-08-31-production-reception-inoculation-cryo-design.md`:
```markdown
> 状态:已实现
> 完成日期:2026-08-31
> 实现计划:`docs/superpowers/plans/2026-08-31-production-reception-inoculation-cryo.md`
```
- [ ] **Step 5: Commit remaining docs/graph if dirty**
```bash
git add docs/superpowers/specs/2026-08-31-production-reception-inoculation-cryo-design.md \
docs/superpowers/plans/2026-08-31-production-reception-inoculation-cryo.md \
backend/graphify-out web-admin/graphify-out 2>/dev/null || true
git status
# 若有变更:
git commit -m "$(cat <<'EOF'
docs: 标记接种量/冻存时间改造已实现并更新图谱
EOF
)"
```
---
## Spec coverage check
| Spec 项 | Task |
|---------|------|
| 接种量 VARCHAR(255) + 文本框 | Task 1–3 |
| 冻存仅日期、列仍 datetime | Task 1–2(展示/表单)、Task 3(解析不变) |
| 共用弹窗两页生效 | Task 2 |
| SQL 迁移 + zkxw 基线 | Task 3 |
| 验收场景 | Task 4 |
## Placeholder scan
无 TBD /「类似 Task N」占位;类型名与 `inoculationAmount` / `cryopreservationTime` 全计划一致。