docs: 生产接收接种量文本化与冻存仅日期实现计划
按设计拆为字段单测、弹窗、后端 SQL/API、验收四步。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
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` 全计划一致。
|
||||
Reference in new issue
Block a user