Files
zkxw/docs/superpowers/plans/2026-05-17-research-donation-module.md
T
2026-05-18 16:21:38 +08:00

994 lines
44 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 科研捐赠管理模块 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:** 实现科研捐赠管理模块——独立的新业务模块,支持协议 CRUD、审核、状态流转、与细胞存储的双向转化。
**Architecture:** 方案 C — 独立数据库表(6 张)+ 独立后端 API/CRUD + 独立前端页面,复用基础数据表和提取公共前端组件。不侵入现有细胞存储代码。
**Tech Stack:** MySQL 5.7 · SQLAlchemy · Pydantic v2 · FastAPI · Vue 3 · Element Plus · Tailwind CSS 4 · TypeScript
**Design Spec:** `docs/superpowers/specs/2026-05-17-research-donation-module-design.md`
---
## File Structure
### New Files (Backend)
- `docs/sql/research_donation_tables.sql` — 6 张新建表 DDL
- `backend/app/models/research_donation.py` — 6 个 SQLAlchemy Model(独立文件避免 models.py 过大)
- `backend/app/schemas/research_donation.py` — Pydantic schemas(注意:细胞存储没有独立 schema 文件,但科研捐赠新建一个更清晰)
- `backend/app/crud/research_donation.py` — CRUD/业务逻辑
- `backend/app/api/research_donation.py` — API 路由
### Modified Files (Backend)
- `backend/app/models/__init__.py` — import 新 models
- `backend/app/api/__init__.py` — 注册新路由
- `backend/app/main.py` — include 新路由
### New Files (Frontend)
- `web-admin/src/api/research-donation.ts` — API 模块
- `web-admin/src/views/research-donation/list/index.vue` — 捐赠协议列表页
- `web-admin/src/views/research-donation/list/DonationFormDialog.vue` — 新增/编辑弹窗
- `web-admin/src/views/research-donation/list/DonationViewDialog.vue` — 查看详情弹窗
- `web-admin/src/views/research-donation/list/DonationAuditDialog.vue` — 审核弹窗
- `web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue` — 状态变更弹窗
- `web-admin/src/views/research-donation/audit/index.vue` — 审核管理页
### Modified Files (Frontend)
- `web-admin/src/router/modules/sqxp.ts` — 新增科研捐赠菜单路由
- `web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue` — 增加「转科研」操作
### Reference Files (DO NOT MODIFY — 用于参照模式)
- `backend/app/models/models.py:707-937` — 细胞存储 Model 定义
- `backend/app/api/collection_protocol.py` — 细胞存储 API 路由模式
- `backend/app/crud/collection_protocol.py` — 细胞存储 CRUD 模式
- `web-admin/src/views/cell-storage/protocol/index.vue` — 协议列表页模式
- `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue` — 表单弹窗模式
- `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue` — 查看弹窗模式
- `web-admin/src/views/cell-storage/protocol/ProtocolAuditDialog.vue` — 审核弹窗模式
- `web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue` — 状态变更弹窗模式
- `web-admin/src/views/cell-storage/protocol-audit/index.vue` — 审核管理页模式
- `web-admin/src/api/collection-protocol.ts` — API 模块模式
- `web-admin/src/router/modules/sqxp.ts:204-252` — 细胞存储路由注册
---
## Task 1: SQL 建表脚本
**Files:**
- Create: `docs/sql/research_donation_tables.sql`
**Reference:** `docs/sql/qc_item_param_and_task_result.sql`(SQL 格式参考),`backend/app/models/models.py:707-937`(字段定义参考)
- [ ] **Step 1: 编写 6 张表的 DDL**
创建 `docs/sql/research_donation_tables.sql`,包含以下 6 张表的 `CREATE TABLE IF NOT EXISTS` 语句:
```sql
-- ============================================================
-- 科研捐赠管理模块 — 建表脚本
-- ============================================================
-- 1. 科研捐赠主表
CREATE TABLE IF NOT EXISTS `biz_research_donation` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`donation_no` varchar(64) NOT NULL COMMENT '捐赠协议编号',
`donation_status` varchar(16) NOT NULL DEFAULT 'dsx' COMMENT '状态 dsx待生效 sxz生效中 ygq已过期 yzf已作废 zckc转细胞存储',
`donation_date` char(8) DEFAULT NULL COMMENT '捐赠日期YYYYMMDD',
`source_type` varchar(20) NOT NULL DEFAULT 'direct' COMMENT '来源类型 direct独立新建 from_storage从细胞存储转来',
`source_protocol_id` varchar(64) DEFAULT NULL COMMENT '来源协议ID(from_storage时填源细胞存储协议ID)',
`consent_form_urls` text DEFAULT NULL COMMENT '知情同意书附件URL(逗号分隔,必填)',
`attachment_urls` text DEFAULT NULL COMMENT '其他附件URL(逗号分隔)',
`collector_user_id` varchar(64) DEFAULT NULL COMMENT '采集专员用户ID',
`effective_time` datetime DEFAULT NULL COMMENT '生效时间',
`effective_desc` varchar(512) DEFAULT NULL COMMENT '生效描述',
`expire_time` datetime DEFAULT NULL COMMENT '过期时间',
`expire_desc` varchar(512) DEFAULT NULL COMMENT '过期描述',
`void_time` datetime DEFAULT NULL COMMENT '作废时间',
`void_desc` varchar(512) DEFAULT NULL COMMENT '作废描述',
`to_storage_time` datetime DEFAULT NULL COMMENT '转存储时间',
`to_storage_desc` varchar(512) DEFAULT NULL COMMENT '转存储描述',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注信息',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rd_tenant_status` (`tenant_no`, `donation_status`, `del_flag`),
KEY `idx_rd_collector` (`collector_user_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠主表';
-- 2. 科研捐赠审核记录
CREATE TABLE IF NOT EXISTS `biz_research_donation_audit` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠ID',
`auditor_user_id` varchar(64) DEFAULT NULL COMMENT '审核人',
`audit_remark` varchar(1000) DEFAULT NULL COMMENT '审核备注',
`audit_status` varchar(16) NOT NULL COMMENT '审核状态 shtg审核通过 shth审核驳回',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rda_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠审核记录';
-- 3. 科研捐赠当事人信息
CREATE TABLE IF NOT EXISTS `biz_research_donation_party` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠ID',
`customer_id` varchar(64) DEFAULT NULL COMMENT '客户ID',
`mother_name` varchar(100) DEFAULT NULL COMMENT '母亲姓名',
`mother_phone` varchar(32) DEFAULT NULL COMMENT '母亲电话',
`mother_id_card` varchar(32) DEFAULT NULL COMMENT '母亲身份证号',
`mother_birth_ym` char(6) DEFAULT NULL COMMENT '母亲出生年月YYYYMM',
`mother_ethnic` varchar(32) DEFAULT NULL COMMENT '母亲民族',
`mother_address` varchar(512) DEFAULT NULL COMMENT '母亲地址',
`father_name` varchar(100) DEFAULT NULL COMMENT '父亲姓名',
`father_phone` varchar(32) DEFAULT NULL COMMENT '父亲电话',
`father_id_card` varchar(32) DEFAULT NULL COMMENT '父亲身份证号',
`newborn_name` varchar(100) DEFAULT NULL COMMENT '新生儿姓名',
`newborn_id_card` varchar(32) DEFAULT NULL COMMENT '新生儿身份证号',
`newborn_delivery_time` datetime DEFAULT NULL COMMENT '新生儿分娩时间',
`newborn_sex` char(1) DEFAULT NULL COMMENT '新生儿性别',
`delivery_mode` varchar(16) DEFAULT NULL COMMENT '分娩方式',
`birth_order` int(11) DEFAULT NULL COMMENT '出生顺序',
`newborn_weight_gram` int(11) DEFAULT NULL COMMENT '新生儿体重(克)',
`apgar_1min` decimal(4,1) DEFAULT NULL COMMENT 'Apgar 1分钟评分',
`apgar_5min` decimal(4,1) DEFAULT NULL COMMENT 'Apgar 5分钟评分',
`collector_user_id` varchar(64) DEFAULT NULL COMMENT '采集专员用户ID',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rdp_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠当事人信息';
-- 4. 科研捐赠采集信息
CREATE TABLE IF NOT EXISTS `biz_research_donation_collection` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠ID',
`region_id` varchar(64) DEFAULT NULL COMMENT '区域ID',
`hospital_id` varchar(64) DEFAULT NULL COMMENT '医院ID',
`medical_staff` varchar(200) DEFAULT NULL COMMENT '医护人员',
`sales_user_id` varchar(64) DEFAULT NULL COMMENT '销售人员用户ID',
`sample_collect_time` datetime DEFAULT NULL COMMENT '样本采集时间',
`maternal_blood_collect_time` datetime DEFAULT NULL COMMENT '母血采集时间',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rdc_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠采集信息';
-- 5. 科研捐赠样本信息(无金额字段)
CREATE TABLE IF NOT EXISTS `biz_research_donation_sample` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠ID',
`sample_type_id` varchar(64) DEFAULT NULL COMMENT '样本类型ID',
`sample_code_id` varchar(64) DEFAULT NULL COMMENT '样本编码ID',
`sample_code` varchar(32) DEFAULT NULL COMMENT '样本编码',
`collect_amount` decimal(18,4) DEFAULT NULL COMMENT '采集量',
`unit_id` varchar(64) DEFAULT NULL COMMENT '单位ID',
`storage_years` int(11) DEFAULT NULL COMMENT '存储年限',
`sample_status` varchar(16) DEFAULT NULL COMMENT '样本状态',
`storage_in_date` char(8) DEFAULT NULL COMMENT '入库日期YYYYMMDD',
`detect_start_date` char(8) DEFAULT NULL COMMENT '检测开始日期YYYYMMDD',
`void_date` char(8) DEFAULT NULL COMMENT '作废日期YYYYMMDD',
`storage_expire_date` char(8) DEFAULT NULL COMMENT '存储到期日期YYYYMMDD',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rds_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠样本信息';
-- 6. 科研捐赠样本运输
CREATE TABLE IF NOT EXISTS `biz_research_donation_sample_transport` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`research_donation_id` varchar(64) NOT NULL COMMENT '科研捐赠ID',
`sample_id` varchar(64) NOT NULL COMMENT '样本ID',
`transport_type` varchar(16) NOT NULL COMMENT 'kdys快递运输 ryxd人员携带',
`express_company_id` varchar(64) DEFAULT NULL COMMENT '快递公司ID',
`waybill_no` varchar(128) DEFAULT NULL COMMENT '快递单号',
`dest_site_id` varchar(64) DEFAULT NULL COMMENT '目的站点ID',
`express_fee` decimal(18,2) DEFAULT NULL COMMENT '快递费用(元)',
`eta_time` datetime DEFAULT NULL COMMENT '预计到达时间',
`carrier_name` varchar(100) DEFAULT NULL COMMENT '携带人员姓名',
`carrier_phone` varchar(32) DEFAULT NULL COMMENT '携带人员联系电话',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记',
PRIMARY KEY (`id`),
KEY `idx_rdst_donation` (`research_donation_id`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠样本运输';
```
- [ ] **Step 2: 提交**
```bash
git add docs/sql/research_donation_tables.sql
git commit -m "feat: 科研捐赠管理模块建表脚本"
```
---
## Task 2: 后端 Model 定义
**Files:**
- Create: `backend/app/models/research_donation.py`
- Modify: `backend/app/models/__init__.py`
**Reference:** `backend/app/models/models.py:707-937`(细胞存储 Model 定义模式)
- [ ] **Step 1: 创建 research_donation.py Model 文件**
创建 `backend/app/models/research_donation.py`,包含 6 个 Model 类。模式完全对齐 `models.py` 中的细胞存储 Model(Column 定义风格、公共字段、default 值):
```python
"""科研捐赠管理 — 数据模型"""
from datetime import datetime
from sqlalchemy import Column, DateTime, Integer, Numeric, SmallInteger, String
from sqlalchemy.ext.declarative import declarative_base
from app.models.base import Base
class BizResearchDonation(Base):
"""科研捐赠主表 biz_research_donation"""
__tablename__ = "biz_research_donation"
id = Column(String(64), primary_key=True, comment="主键ID")
donation_no = Column(String(64), nullable=False, comment="捐赠协议编号")
donation_status = Column(String(16), nullable=False, default="dsx", comment="状态 dsx/sxz/ygq/yzf/zckc")
donation_date = Column(String(8), nullable=True, comment="捐赠日期YYYYMMDD")
source_type = Column(String(20), nullable=False, default="direct", comment="来源类型 direct/from_storage")
source_protocol_id = Column(String(64), nullable=True, comment="来源协议ID")
consent_form_urls = Column(String(4000), nullable=True, comment="知情同意书附件URL(逗号分隔)")
attachment_urls = Column(String(4000), nullable=True, comment="其他附件URL(逗号分隔)")
collector_user_id = Column(String(64), nullable=True, comment="采集专员用户ID")
effective_time = Column(DateTime, nullable=True, comment="生效时间")
effective_desc = Column(String(512), nullable=True, comment="生效描述")
expire_time = Column(DateTime, nullable=True, comment="过期时间")
expire_desc = Column(String(512), nullable=True, comment="过期描述")
void_time = Column(DateTime, nullable=True, comment="作废时间")
void_desc = Column(String(512), nullable=True, comment="作废描述")
to_storage_time = Column(DateTime, nullable=True, comment="转存储时间")
to_storage_desc = Column(String(512), nullable=True, comment="转存储描述")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注信息")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizResearchDonationAudit(Base):
"""科研捐赠审核记录 biz_research_donation_audit"""
__tablename__ = "biz_research_donation_audit"
id = Column(String(64), primary_key=True, comment="主键ID")
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
auditor_user_id = Column(String(64), nullable=True, comment="审核人")
audit_remark = Column(String(1000), nullable=True, comment="审核备注")
audit_status = Column(String(16), nullable=False, comment="审核状态 shtg/shth")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizResearchDonationParty(Base):
"""科研捐赠当事人信息 biz_research_donation_party"""
__tablename__ = "biz_research_donation_party"
id = Column(String(64), primary_key=True, comment="主键ID")
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
customer_id = Column(String(64), nullable=True, comment="客户ID")
mother_name = Column(String(100), nullable=True, comment="母亲姓名")
mother_phone = Column(String(32), nullable=True, comment="母亲电话")
mother_id_card = Column(String(32), nullable=True, comment="母亲身份证号")
mother_birth_ym = Column(String(6), nullable=True, comment="母亲出生年月YYYYMM")
mother_ethnic = Column(String(32), nullable=True, comment="母亲民族")
mother_address = Column(String(512), nullable=True, comment="母亲地址")
father_name = Column(String(100), nullable=True, comment="父亲姓名")
father_phone = Column(String(32), nullable=True, comment="父亲电话")
father_id_card = Column(String(32), nullable=True, comment="父亲身份证号")
newborn_name = Column(String(100), nullable=True, comment="新生儿姓名")
newborn_id_card = Column(String(32), nullable=True, comment="新生儿身份证号")
newborn_delivery_time = Column(DateTime, nullable=True, comment="新生儿分娩时间")
newborn_sex = Column(String(1), nullable=True, comment="新生儿性别")
delivery_mode = Column(String(16), nullable=True, comment="分娩方式")
birth_order = Column(Integer, nullable=True, comment="出生顺序")
newborn_weight_gram = Column(Integer, nullable=True, comment="新生儿体重(克)")
apgar_1min = Column(Numeric(4, 1), nullable=True, comment="Apgar 1分钟评分")
apgar_5min = Column(Numeric(4, 1), nullable=True, comment="Apgar 5分钟评分")
collector_user_id = Column(String(64), nullable=True, comment="采集专员用户ID")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizResearchDonationCollection(Base):
"""科研捐赠采集信息 biz_research_donation_collection"""
__tablename__ = "biz_research_donation_collection"
id = Column(String(64), primary_key=True, comment="主键ID")
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
region_id = Column(String(64), nullable=True, comment="区域ID")
hospital_id = Column(String(64), nullable=True, comment="医院ID")
medical_staff = Column(String(200), nullable=True, comment="医护人员")
sales_user_id = Column(String(64), nullable=True, comment="销售人员用户ID")
sample_collect_time = Column(DateTime, nullable=True, comment="样本采集时间")
maternal_blood_collect_time = Column(DateTime, nullable=True, comment="母血采集时间")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizResearchDonationSample(Base):
"""科研捐赠样本信息 biz_research_donation_sample(无金额字段)"""
__tablename__ = "biz_research_donation_sample"
id = Column(String(64), primary_key=True, comment="主键ID")
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
sample_type_id = Column(String(64), nullable=True, comment="样本类型ID")
sample_code_id = Column(String(64), nullable=True, comment="样本编码ID")
sample_code = Column(String(32), nullable=True, comment="样本编码")
collect_amount = Column(Numeric(18, 4), nullable=True, comment="采集量")
unit_id = Column(String(64), nullable=True, comment="单位ID")
storage_years = Column(Integer, nullable=True, comment="存储年限")
sample_status = Column(String(16), nullable=True, comment="样本状态")
storage_in_date = Column(String(8), nullable=True, comment="入库日期YYYYMMDD")
detect_start_date = Column(String(8), nullable=True, comment="检测开始日期YYYYMMDD")
void_date = Column(String(8), nullable=True, comment="作废日期YYYYMMDD")
storage_expire_date = Column(String(8), nullable=True, comment="存储到期日期YYYYMMDD")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
class BizResearchDonationSampleTransport(Base):
"""科研捐赠样本运输 biz_research_donation_sample_transport"""
__tablename__ = "biz_research_donation_sample_transport"
id = Column(String(64), primary_key=True, comment="主键ID")
research_donation_id = Column(String(64), nullable=False, comment="科研捐赠ID")
sample_id = Column(String(64), nullable=False, comment="样本ID")
transport_type = Column(String(16), nullable=False, comment="kdys快递运输 ryxd人员携带")
express_company_id = Column(String(64), nullable=True, comment="快递公司ID")
waybill_no = Column(String(128), nullable=True, comment="快递单号")
dest_site_id = Column(String(64), nullable=True, comment="目的站点ID")
express_fee = Column(Numeric(18, 2), nullable=True, comment="快递费用(元)")
eta_time = Column(DateTime, nullable=True, comment="预计到达时间")
carrier_name = Column(String(100), nullable=True, comment="携带人员姓名")
carrier_phone = Column(String(32), nullable=True, comment="携带人员联系电话")
tenant_no = Column(String(64), nullable=False, default="zkxw", comment="租户编号")
order_num = Column(Integer, nullable=True, comment="排序号")
remark = Column(String(512), nullable=True, comment="备注")
create_by = Column(String(64), nullable=True, comment="创建人id")
create_dept_id = Column(String(64), nullable=False, default="", comment="创建人部门ID")
create_time = Column(DateTime, nullable=False, default=datetime.now, comment="创建时间")
update_by = Column(String(64), nullable=True, comment="更新人id")
update_time = Column(DateTime, nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间")
del_flag = Column(SmallInteger, nullable=False, default=0, comment="删除标记")
```
- [ ] **Step 2: 在 models/__init__.py 中注册**
读取 `backend/app/models/__init__.py`,在 import 区增加:
```python
from app.models.research_donation import ( # noqa: F401
BizResearchDonation,
BizResearchDonationAudit,
BizResearchDonationParty,
BizResearchDonationCollection,
BizResearchDonationSample,
BizResearchDonationSampleTransport,
)
```
- [ ] **Step 3: 提交**
```bash
git add backend/app/models/research_donation.py backend/app/models/__init__.py
git commit -m "feat: 科研捐赠管理模块 — SQLAlchemy Model 定义"
```
---
## Task 3: 后端 CRUD 层
**Files:**
- Create: `backend/app/crud/research_donation.py`
**Reference:** `backend/app/crud/collection_protocol.py`(CRUD 模式参考,但大幅简化:无缴费逻辑)
这是后端最核心的文件,包含以下功能模块:
### 功能清单
1. **列表查询** `list_donations(db, tenant_no, page, page_size, keyword, donation_status, collector_user_id)` — 分页、关键字搜索(编号/母亲姓名)、状态过滤、数据权限
2. **详情查询** `get_donation_detail(db, donation_id)` — 主表 + 所有子资源(当事人、采集、样本+运输)
3. **创建** `create_donation(db, data, tenant_no, user_id)` — 主表 + 子资源整体创建
4. **更新** `update_donation(db, donation_id, data, tenant_no, user_id)` — 子资源 upsert(先删后插)
5. **删除** `delete_donation(db, donation_id, tenant_no)` — 软删除主表 + 所有子资源
6. **批量删除** `batch_delete_donations(db, ids, tenant_no)`
7. **审核列表** `list_audits(db, tenant_no, page, page_size, keyword, audit_status)`
8. **提交审核** `submit_audit(db, donation_id, audit_status, audit_remark, user_id, tenant_no)`
9. **状态变更** `change_status(db, donation_id, new_status, desc, user_id, tenant_no)` — 含状态机校验
10. **从细胞存储转化** `convert_from_storage(db, protocol_id, tenant_no, user_id)` — 读源协议 → 创建捐赠记录
11. **转为细胞存储** `convert_to_storage(db, donation_id, tenant_no, user_id)` — 读捐赠 → 创建协议记录
### 关键模式(参照 collection_protocol.py)
- UUID 生成:`import uuid; uuid.uuid4().hex`
- camelCase → snake_case 转换:参照 `_normalize_fields()` 模式
- 附件处理:`urls_to_attachment_list()` 复用
- 子资源 upsert:先删除旧数据 `del_flag=1`,再批量 insert 新数据
- 租户隔离:所有查询加 `tenant_no` 过滤
- 软删除:设置 `del_flag=1`,查询默认 `del_flag=0`
### 状态机
```
合法转换:
dsx → sxz(生效)
dsx → yzf(作废)
sxz → ygq(过期)
sxz → yzf(作废)
sxz → zckc(转细胞存储)
```
> 注意:审核通过后才允许从 dsx → sxz。审核驳回后状态不变。
**实现指导:** 参照 `backend/app/crud/collection_protocol.py` 的整体结构,但去掉所有 payment/payment_detail/storage_cert 相关函数。CRUD 文件预计 600-800 行。
- [ ] **Step 1: 创建 crud/research_donation.py**
编写完整的 CRUD 文件,包含上述 11 个功能。关键点:
- `create_donation` 接收一个包含所有子资源的 dict,在事务中创建主表 + party + collection + samples + transports
- `update_donation` 对子资源使用"软删旧 + 插新"的 upsert 模式
- `convert_from_storage` 读取 `BizCollectionProtocol` 及其子资源,映射到捐赠字段
- `convert_to_storage` 反向操作
- [ ] **Step 2: 提交**
```bash
git add backend/app/crud/research_donation.py
git commit -m "feat: 科研捐赠管理模块 — CRUD 业务逻辑层"
```
---
## Task 4: 后端 API 路由
**Files:**
- Create: `backend/app/api/research_donation.py`
- Modify: `backend/app/api/__init__.py`
- Modify: `backend/app/main.py`
**Reference:** `backend/app/api/collection_protocol.py`(API 路由模式)
- [ ] **Step 1: 创建 API 路由文件**
创建 `backend/app/api/research_donation.py`:
```python
"""科研捐赠管理 — API 路由"""
from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin
from app.core.response import error_response, success_response
from app.crud import research_donation as rd_svc
from app.models.models import SysUser
router = APIRouter(prefix="/web/admin", tags=["科研捐赠管理"])
```
端点定义(参照 collection_protocol.py 的装饰器模式):
| 端点 | 方法 | 路径 | 说明 |
|---|---|---|---|
| 列表 | GET | `/research-donations` | page/pageSize/keyword/donationStatus 参数 |
| 详情 | GET | `/research-donations/{id}` | 返回主表 + 所有子资源 |
| 创建 | POST | `/research-donations` | 整体提交含子资源 |
| 更新 | PUT | `/research-donations/{id}` | 整体提交含子资源 |
| 删除 | DELETE | `/research-donations/{id}` | 软删除 |
| 批量删除 | POST | `/research-donations/batch-delete` | body: `{ids: []}` |
| 审核列表 | GET | `/research-donations/audits` | page/pageSize/keyword/auditStatus |
| 提交审核 | POST | `/research-donations/{id}/audit` | body: `{auditStatus, auditRemark}` |
| 状态变更 | POST | `/research-donations/{id}/status-change` | body: `{donationStatus, desc, time}` |
| 从存储转化 | POST | `/research-donations/convert-from-storage` | body: `{protocolId}` |
| 转为存储 | POST | `/research-donations/{id}/convert-to-storage` | body: `{desc}` |
每个端点的模式:
```python
@router.get("/research-donations")
def list_research_donations(
page: int = Query(1, ge=1),
pageSize: int = Query(20, ge=1, le=200),
keyword: Optional[str] = Query(None),
donationStatus: Optional[str] = Query(None),
db: Session = Depends(get_db),
user: dict = Depends(require_admin_or_super_admin),
):
tenant_no = _get_tenant_no(db, user)
collector_user_id = _get_current_user_id(user)
result = rd_svc.list_donations(db, tenant_no, page, pageSize, keyword, donationStatus, collector_user_id)
return success_response(result)
```
- [ ] **Step 2: 注册路由**
在 `backend/app/api/__init__.py` 增加:
```python
from app.api.research_donation import router as research_donation_router
```
并在 `__all__` 列表增加 `"research_donation_router"`。
在 `backend/app/main.py` 的 import 行和 include_router 行增加 `research_donation_router`。
- [ ] **Step 3: 提交**
```bash
git add backend/app/api/research_donation.py backend/app/api/__init__.py backend/app/main.py
git commit -m "feat: 科研捐赠管理模块 — API 路由定义与注册"
```
---
## Task 5: 前端路由与 API 模块
**Files:**
- Create: `web-admin/src/api/research-donation.ts`
- Modify: `web-admin/src/router/modules/sqxp.ts`
**Reference:** `web-admin/src/api/collection-protocol.ts`(API 模式),`web-admin/src/router/modules/sqxp.ts:204-252`(路由注册模式)
- [ ] **Step 1: 创建 API 模块**
创建 `web-admin/src/api/research-donation.ts`:
```typescript
import request from '@/utils/http'
type PageList<T> = {
list: T[]
total: number
page: number
pageSize: number
totalPages: number
}
export type ResearchDonationRow = Record<string, unknown>
// 列表
export function fetchResearchDonationList(params?: {
page?: number
pageSize?: number
keyword?: string
donationStatus?: string
}) {
return request.get<PageList<ResearchDonationRow>>({
url: '/web/admin/research-donations',
params
})
}
// 详情
export function fetchResearchDonationDetail(id: string) {
return request.get<{
donation: Record<string, unknown>
party: Record<string, unknown> | null
collection: Record<string, unknown> | null
samples: Array<Record<string, unknown>> | null
attachments?: Array<Record<string, unknown>>
}>({
url: `/web/admin/research-donations/${id}`
})
}
// 创建
export function fetchCreateResearchDonation(data: Record<string, unknown>) {
return request.post({
url: '/web/admin/research-donations',
data,
showSuccessMessage: true
})
}
// 更新
export function fetchUpdateResearchDonation(id: string, data: Record<string, unknown>) {
return request.put({
url: `/web/admin/research-donations/${id}`,
data,
showSuccessMessage: true
})
}
// 删除
export function fetchDeleteResearchDonation(id: string) {
return request.del({
url: `/web/admin/research-donations/${id}`,
showSuccessMessage: true
})
}
// 批量删除
export function fetchBatchDeleteResearchDonation(ids: string[]) {
return request.post({
url: '/web/admin/research-donations/batch-delete',
data: { ids },
showSuccessMessage: true
})
}
// 审核列表
export function fetchResearchDonationAuditList(params?: {
page?: number
pageSize?: number
keyword?: string
auditStatus?: string
}) {
return request.get<PageList<Record<string, unknown>>>({
url: '/web/admin/research-donations/audits',
params
})
}
// 提交审核
export function fetchSubmitDonationAudit(
donationId: string,
data: { auditStatus: string; auditRemark?: string },
options?: { showSuccessMessage?: boolean }
) {
return request.post({
url: `/web/admin/research-donations/${donationId}/audit`,
data,
showSuccessMessage: options?.showSuccessMessage ?? true
})
}
// 状态变更
export function fetchDonationStatusChange(
donationId: string,
data: { donationStatus: string; desc?: string; time?: string }
) {
return request.post({
url: `/web/admin/research-donations/${donationId}/status-change`,
data,
showSuccessMessage: true
})
}
// 从细胞存储转化
export function fetchConvertFromStorage(protocolId: string) {
return request.post({
url: '/web/admin/research-donations/convert-from-storage',
data: { protocolId },
showSuccessMessage: true
})
}
// 转为细胞存储
export function fetchConvertToStorage(
donationId: string,
data?: { desc?: string }
) {
return request.post({
url: `/web/admin/research-donations/${donationId}/convert-to-storage`,
data: data || {},
showSuccessMessage: true
})
}
```
- [ ] **Step 2: 添加路由定义**
在 `web-admin/src/router/modules/sqxp.ts` 的细胞存储路由组之后(约 line 252),增加:
```typescript
{
id: '60000000000000000000000000000001',
name: 'ResearchDonation',
path: '/research-donation',
component: '/index/index',
meta: {
title: '科研捐赠管理',
icon: 'ri:hand-heart-line',
keepAlive: false,
roles: ['admin']
},
children: [
{
id: '60000000000000000000000000000002',
name: 'ResearchDonationList',
path: 'list',
component: '/research-donation/list/index',
meta: { title: '捐赠协议单', keepAlive: true }
},
{
id: '60000000000000000000000000000003',
name: 'ResearchDonationAudit',
path: 'audit',
component: '/research-donation/audit/index',
meta: { title: '捐赠协议审核', keepAlive: true }
}
]
}
```
- [ ] **Step 3: 提交**
```bash
git add web-admin/src/api/research-donation.ts web-admin/src/router/modules/sqxp.ts
git commit -m "feat: 科研捐赠管理模块 — 前端 API 模块与路由定义"
```
---
## Task 6: 前端 — 捐赠协议列表页
**Files:**
- Create: `web-admin/src/views/research-donation/list/index.vue`
**Reference:** `web-admin/src/views/cell-storage/protocol/index.vue`(列表页模式)
**实现指导:**
参照 `cell-storage/protocol/index.vue` 的完整模式创建列表页,关键差异:
1. **搜索条件**:关键字(编号/母亲姓名)+ 状态下拉(dsx/sxz/ygq/yzf/zckc)
2. **表格列**:捐赠编号、捐赠日期、母亲姓名、状态、来源类型、采集专员、创建时间、操作
3. **操作按钮**:新增、批量删除、导出(可选)
4. **行操作**:查看、编辑、审核、状态变更、删除
5. **状态标签颜色**:dsx=info, sxz=success, ygq=warning, yzf=danger, zckc=primary
6. **无缴费相关按钮和筛选**(与细胞存储的关键区别)
组件导入:DonationFormDialog, DonationViewDialog, DonationAuditDialog, DonationStatusChangeDialog
- [ ] **Step 1: 创建列表页**
- [ ] **Step 2: 提交**
```bash
git add web-admin/src/views/research-donation/list/index.vue
git commit -m "feat: 科研捐赠管理模块 — 捐赠协议列表页"
```
---
## Task 7: 前端 — 新增/编辑弹窗
**Files:**
- Create: `web-admin/src/views/research-donation/list/DonationFormDialog.vue`
**Reference:** `web-admin/src/views/cell-storage/protocol/ProtocolFormDialog.vue`(表单弹窗模式)
**实现指导:**
参照 ProtocolFormDialog.vue 的模式,5 个 Tab:
| Tab | 字段 | 说明 |
|---|---|---|
| 基本信息 | donationNo, donationDate, sourceType, remark | 编号自动/手动、日期、来源类型只读 |
| 当事人信息 | 母亲/父亲/新生儿全部字段 | 与细胞存储一致 |
| 采集信息 | 区域/医院/采集人/时间 | 与细胞存储一致 |
| 样本信息 | 样本类型/编码/数量/储存年限(**无 price 字段**) | 可多行添加删除 |
| 附件 | 知情同意书(**必传**)+ 其他附件 | 知情同意书独立上传区域 |
关键点:
- 宽度 `1200px`,`label-position="top"`
- 知情同意书 Tab 中分两个区域:必传的知情同意书 + 可选的其他附件
- 提交时校验 `consentFormUrls` 不能为空
- 样本 Tab 无金额/费用相关字段
- [ ] **Step 1: 创建表单弹窗组件**
- [ ] **Step 2: 提交**
```bash
git add web-admin/src/views/research-donation/list/DonationFormDialog.vue
git commit -m "feat: 科研捐赠管理模块 — 新增/编辑弹窗(5 Tab)"
```
---
## Task 8: 前端 — 查看/审核/状态变更弹窗
**Files:**
- Create: `web-admin/src/views/research-donation/list/DonationViewDialog.vue`
- Create: `web-admin/src/views/research-donation/list/DonationAuditDialog.vue`
- Create: `web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue`
**Reference:**
- `web-admin/src/views/cell-storage/protocol/ProtocolViewDialog.vue`
- `web-admin/src/views/cell-storage/protocol/ProtocolAuditDialog.vue`
- `web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue`
- [ ] **Step 1: 创建 DonationViewDialog.vue**
参照 ProtocolViewDialog.vue,5 个 Tab 只读展示。宽度 `1600px`。
- [ ] **Step 2: 创建 DonationAuditDialog.vue**
参照 ProtocolAuditDialog.vue。审核状态选择(shtg/shth)+ 备注。支持批量。
- [ ] **Step 3: 创建 DonationStatusChangeDialog.vue**
参照 ProtocolStatusChangeDialog.vue。支持的操作:
- 生效(dsx → sxz)
- 过期(sxz → ygq)
- 作废(dsx/sxz → yzf)
- **转细胞存储**(sxz → zckc)— 新增操作
每个操作有时间 + 描述字段。
- [ ] **Step 4: 提交**
```bash
git add web-admin/src/views/research-donation/list/DonationViewDialog.vue \
web-admin/src/views/research-donation/list/DonationAuditDialog.vue \
web-admin/src/views/research-donation/list/DonationStatusChangeDialog.vue
git commit -m "feat: 科研捐赠管理模块 — 查看/审核/状态变更弹窗"
```
---
## Task 9: 前端 — 审核管理页
**Files:**
- Create: `web-admin/src/views/research-donation/audit/index.vue`
**Reference:** `web-admin/src/views/cell-storage/protocol-audit/index.vue`
- [ ] **Step 1: 创建审核管理页**
参照 protocol-audit/index.vue 模式:
- 列表展示待审核/已审核的捐赠记录
- 批量审核、批量生效、批量作废等操作
- 调用 `fetchResearchDonationAuditList` API
- [ ] **Step 2: 提交**
```bash
git add web-admin/src/views/research-donation/audit/index.vue
git commit -m "feat: 科研捐赠管理模块 — 审核管理页"
```
---
## Task 10: 细胞存储端 — 增加「转科研」操作
**Files:**
- Modify: `web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue`
**Reference:** 读取现有文件,在状态变更操作中增加 `zky`(转科研)选项
- [ ] **Step 1: 修改 ProtocolStatusChangeDialog.vue**
在现有的状态变更操作中增加「转科研」选项:
- 操作类型增加 `zky`
- 调用 `fetchConvertFromStorage(protocolId)` API(从 research-donation.ts 导入)
- 成功后刷新列表
- [ ] **Step 2: 提交**
```bash
git add web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue
git commit -m "feat: 细胞存储协议 — 增加转科研操作"
```
---
## Task 11: 联调与验证
- [ ] **Step 1: 后端验证**
```bash
cd backend && source venv/bin/activate
python -c "from app.models.research_donation import *; print('Models OK')"
python -c "from app.api.research_donation import router; print('Router OK')"
```
- [ ] **Step 2: 前端验证**
```bash
cd web-admin && pnpm build
```
预期:构建通过,无 TS 错误。
- [ ] **Step 3: 数据库建表**
在开发环境执行 `docs/sql/research_donation_tables.sql`。
- [ ] **Step 4: 功能验证清单**
| 功能 | 验证方式 |
|------|---------|
| 列表展示 | 访问 `/#/research-donation/list` |
| 新增捐赠 | 填写 5 个 Tab,提交 |
| 知情同意书必填校验 | 不传知情同意书,提交应报错 |
| 编辑捐赠 | 修改后保存 |
| 查看详情 | 5 Tab 只读展示 |
| 删除(单个/批量) | 软删除 |
| 审核流程 | 审核页面操作 |
| 状态变更 | 生效/过期/作废 |
| 转细胞存储 | 捐赠 → 细胞存储协议 |
| 细胞存储转科研 | 细胞存储 → 科研捐赠 |