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

44 KiB
Raw Blame History

科研捐赠管理模块 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 语句:

-- ============================================================
-- 科研捐赠管理模块 — 建表脚本
-- ============================================================

-- 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 模式参考,但大幅简化:无缴费逻辑)

这是后端最核心的文件,包含以下功能模块:

功能清单

  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: 提交

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 的完整模式创建列表页,关键差异:

  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: 提交

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 模式:

  • 列表展示待审核/已审核的捐赠记录

  • 批量审核、批量生效、批量作废等操作

  • 调用 fetchResearchDonationAuditList API

  • 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 只读展示
删除(单个/批量) 软删除
审核流程 审核页面操作
状态变更 生效/过期/作废
转细胞存储 捐赠 → 细胞存储协议
细胞存储转科研 细胞存储 → 科研捐赠