44 KiB
科研捐赠管理模块 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 张新建表 DDLbackend/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 新 modelsbackend/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 语句:
-- ============================================================
-- 科研捐赠管理模块 — 建表脚本
-- ============================================================
-- 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: 提交
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 值):
"""科研捐赠管理 — 数据模型"""
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 区增加:
from app.models.research_donation import ( # noqa: F401
BizResearchDonation,
BizResearchDonationAudit,
BizResearchDonationParty,
BizResearchDonationCollection,
BizResearchDonationSample,
BizResearchDonationSampleTransport,
)
- Step 3: 提交
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 模式参考,但大幅简化:无缴费逻辑)
这是后端最核心的文件,包含以下功能模块:
功能清单
- 列表查询
list_donations(db, tenant_no, page, page_size, keyword, donation_status, collector_user_id)— 分页、关键字搜索(编号/母亲姓名)、状态过滤、数据权限 - 详情查询
get_donation_detail(db, donation_id)— 主表 + 所有子资源(当事人、采集、样本+运输) - 创建
create_donation(db, data, tenant_no, user_id)— 主表 + 子资源整体创建 - 更新
update_donation(db, donation_id, data, tenant_no, user_id)— 子资源 upsert(先删后插) - 删除
delete_donation(db, donation_id, tenant_no)— 软删除主表 + 所有子资源 - 批量删除
batch_delete_donations(db, ids, tenant_no) - 审核列表
list_audits(db, tenant_no, page, page_size, keyword, audit_status) - 提交审核
submit_audit(db, donation_id, audit_status, audit_remark, user_id, tenant_no) - 状态变更
change_status(db, donation_id, new_status, desc, user_id, tenant_no)— 含状态机校验 - 从细胞存储转化
convert_from_storage(db, protocol_id, tenant_no, user_id)— 读源协议 → 创建捐赠记录 - 转为细胞存储
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: 提交
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:
"""科研捐赠管理 — 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} |
每个端点的模式:
@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 增加:
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: 提交
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:
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),增加:
{
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: 提交
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 的完整模式创建列表页,关键差异:
- 搜索条件:关键字(编号/母亲姓名)+ 状态下拉(dsx/sxz/ygq/yzf/zckc)
- 表格列:捐赠编号、捐赠日期、母亲姓名、状态、来源类型、采集专员、创建时间、操作
- 操作按钮:新增、批量删除、导出(可选)
- 行操作:查看、编辑、审核、状态变更、删除
- 状态标签颜色:dsx=info, sxz=success, ygq=warning, yzf=danger, zckc=primary
- 无缴费相关按钮和筛选(与细胞存储的关键区别)
组件导入:DonationFormDialog, DonationViewDialog, DonationAuditDialog, DonationStatusChangeDialog
-
Step 1: 创建列表页
-
Step 2: 提交
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: 提交
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: 提交
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 模式:
-
列表展示待审核/已审核的捐赠记录
-
批量审核、批量生效、批量作废等操作
-
调用
fetchResearchDonationAuditListAPI -
Step 2: 提交
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: 提交
git add web-admin/src/views/cell-storage/protocol/ProtocolStatusChangeDialog.vue
git commit -m "feat: 细胞存储协议 — 增加转科研操作"
Task 11: 联调与验证
- Step 1: 后端验证
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: 前端验证
cd web-admin && pnpm build
预期:构建通过,无 TS 错误。
- Step 3: 数据库建表
在开发环境执行 docs/sql/research_donation_tables.sql。
- Step 4: 功能验证清单
| 功能 | 验证方式 |
|---|---|
| 列表展示 | 访问 /#/research-donation/list |
| 新增捐赠 | 填写 5 个 Tab,提交 |
| 知情同意书必填校验 | 不传知情同意书,提交应报错 |
| 编辑捐赠 | 修改后保存 |
| 查看详情 | 5 Tab 只读展示 |
| 删除(单个/批量) | 软删除 |
| 审核流程 | 审核页面操作 |
| 状态变更 | 生效/过期/作废 |
| 转细胞存储 | 捐赠 → 细胞存储协议 |
| 细胞存储转科研 | 细胞存储 → 科研捐赠 |