This commit is contained in:
zlwu committed 2026-05-18 16:21:38 +08:00
1 parent 2a4767a014
commit f269a994f4
61 files changed
+27430 -8

No files matched your search

+49
View File
@@ -0,0 +1,49 @@
-- 体检机构项目套餐(主表 + 明细表)
-- 套餐总价不落库,由明细 project_fee 汇总计算
-- ----------------------------
-- Table structure for biz_checkup_institution_package
-- ----------------------------
DROP TABLE IF EXISTS `biz_checkup_institution_package_item`;
DROP TABLE IF EXISTS `biz_checkup_institution_package`;
CREATE TABLE `biz_checkup_institution_package` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`checkup_institution_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '体检机构ID',
`package_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐名称',
`package_code` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐编码(必填,机构内未删除记录唯一,应用层校验)',
`status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '1正常 0停用',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_ck_pkg_inst` (`checkup_institution_id`,`del_flag`),
KEY `idx_ck_pkg_code` (`checkup_institution_id`,`package_code`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构项目套餐';
CREATE TABLE `biz_checkup_institution_package_item` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`package_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐ID',
`checkup_institution_project_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '机构项目ID',
`project_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '项目名称(快照)',
`project_fee` decimal(18,2) DEFAULT NULL COMMENT '套餐内项目费用(元),可独立于机构项目价',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ck_pkg_item` (`package_id`,`checkup_institution_project_id`),
KEY `idx_ck_pkg_item_pkg` (`package_id`,`del_flag`),
KEY `idx_ck_pkg_item_proj` (`checkup_institution_project_id`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构项目套餐明细';
@@ -0,0 +1,3 @@
-- 体检机构项目及价格表:增加状态字段
ALTER TABLE `biz_checkup_institution_project`
ADD COLUMN `status` char(1) NOT NULL DEFAULT '1' COMMENT '1正常 0停用' AFTER `project_fee`;
+294
View File
@@ -0,0 +1,294 @@
-- ============================================================================
-- 生产管理模块 DDL(11张业务表 + 菜单 INSERT)
-- ============================================================================
-- ---------------------------------------------------------------------------
-- 1. 生产耗材 biz_consumable
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_consumable` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`consumable_code` varchar(64) NOT NULL COMMENT '耗材编码',
`consumable_name` varchar(100) NOT NULL COMMENT '耗材名称',
`spec_model` varchar(200) DEFAULT NULL COMMENT '规格型号',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`description` varchar(500) 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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_consumable_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='生产耗材';
-- ---------------------------------------------------------------------------
-- 2. 试剂 biz_reagent
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_reagent` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`reagent_code` varchar(64) NOT NULL COMMENT '试剂编码',
`reagent_name` varchar(100) NOT NULL COMMENT '试剂名称',
`spec` varchar(200) DEFAULT NULL COMMENT '规格',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`description` varchar(500) 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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_reagent_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='试剂';
-- ---------------------------------------------------------------------------
-- 3. SOP文档 biz_sop_document
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_sop_document` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`sop_code` varchar(64) NOT NULL COMMENT 'SOP编号',
`sop_name` varchar(200) NOT NULL COMMENT 'SOP文档名称',
`version_no` varchar(50) DEFAULT NULL COMMENT '版本号',
`sop_attachments` varchar(2000) DEFAULT NULL COMMENT 'SOP文档附件(逗号分隔URL)',
`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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_sop_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='SOP文档';
-- ---------------------------------------------------------------------------
-- 4. 工序分类 biz_process_category
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_category` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`category_code` varchar(64) NOT NULL COMMENT '分类编码',
`category_name` varchar(100) NOT 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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pcat_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工序分类';
-- ---------------------------------------------------------------------------
-- 5. 产出类型 biz_output_type
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_output_type` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`type_code` varchar(64) NOT NULL COMMENT '类型编码',
`type_name` varchar(100) NOT 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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_otype_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='产出类型';
-- ---------------------------------------------------------------------------
-- 6. 耗材包 biz_consumable_pack
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_consumable_pack` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`pack_code` varchar(64) NOT NULL COMMENT '编号',
`pack_name` varchar(100) NOT NULL COMMENT '名称',
`usage_scene` varchar(200) DEFAULT NULL COMMENT '使用场景',
`pack_type` varchar(20) DEFAULT NULL COMMENT '类型(bzhcb-标准耗材包 zdyhcb-自定义耗材包)',
`status` char(1) DEFAULT '1' COMMENT '状态(1-启用 0-停用)',
`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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_cpack_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='耗材包';
-- ---------------------------------------------------------------------------
-- 7. 耗材包明细 biz_consumable_pack_item
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_consumable_pack_item` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`pack_id` varchar(64) NOT NULL COMMENT '耗材包id',
`consumable_code` varchar(64) DEFAULT NULL COMMENT '耗材编码',
`consumable_name` varchar(100) DEFAULT NULL COMMENT '耗材名称',
`spec_model` varchar(200) DEFAULT NULL COMMENT '耗材规格型号',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) 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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_cpi_pack` (`pack_id`, `del_flag`),
KEY `idx_cpi_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='耗材包明细';
-- ---------------------------------------------------------------------------
-- 8. 工艺 biz_process
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_code` varchar(64) NOT NULL COMMENT '工序代码',
`category_id` varchar(64) DEFAULT NULL COMMENT '工序分类id',
`process_name` varchar(100) NOT NULL COMMENT '工序名称',
`description` varchar(500) DEFAULT NULL COMMENT '描述',
`equipment_name` varchar(200) DEFAULT NULL COMMENT '预计使用设备名称',
`estimated_duration` varchar(100) DEFAULT NULL COMMENT '预估耗时',
`sop_ids` varchar(1000) DEFAULT NULL COMMENT '关联SOP文档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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_process_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺';
-- ---------------------------------------------------------------------------
-- 9. 工艺试剂 biz_process_reagent
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_reagent` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_id` varchar(64) NOT NULL COMMENT '工艺id',
`reagent_code` varchar(64) DEFAULT NULL COMMENT '试剂编码',
`reagent_name` varchar(100) DEFAULT NULL COMMENT '试剂名称',
`spec` varchar(200) DEFAULT NULL COMMENT '规格',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`unit` varchar(64) DEFAULT NULL COMMENT '单位',
`is_key_material` char(1) DEFAULT '0' COMMENT '是否关键物料(0-否 1-是)',
`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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pr_process` (`process_id`, `del_flag`),
KEY `idx_pr_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺试剂';
-- ---------------------------------------------------------------------------
-- 10. 工艺耗材 biz_process_consumable
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_consumable` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_id` varchar(64) NOT NULL COMMENT '工艺id',
`consumable_code` varchar(64) DEFAULT NULL COMMENT '耗材编码',
`consumable_name` varchar(100) DEFAULT NULL COMMENT '耗材名称',
`spec_model` varchar(200) DEFAULT NULL COMMENT '耗材规格型号',
`standard_usage` varchar(100) DEFAULT NULL COMMENT '标准用量',
`is_required` char(1) DEFAULT '1' COMMENT '是否必需(1-必须 0-可选)',
`remark` varchar(512) DEFAULT NULL COMMENT '备注',
`tenant_no` varchar(64) NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) 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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_pc_process` (`process_id`, `del_flag`),
KEY `idx_pc_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺耗材';
-- ---------------------------------------------------------------------------
-- 11. 工艺产出物 biz_process_output
-- ---------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `biz_process_output` (
`id` varchar(64) NOT NULL COMMENT '主键ID',
`process_id` varchar(64) NOT NULL COMMENT '工艺id',
`output_type_id` varchar(64) DEFAULT NULL COMMENT '产出类型id',
`output_name` varchar(200) DEFAULT NULL COMMENT '产出物名称',
`output_barcode` varchar(200) 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 DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_po_process` (`process_id`, `del_flag`),
KEY `idx_po_tenant` (`tenant_no`, `del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='工艺产出物';
-- ============================================================================
-- 菜单数据 — 生产管理(父级 id='2' Web端菜单)
-- ============================================================================
-- 生产管理 目录(parent_id=2 即 Web端菜单根)
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000001', '生产管理', '2', 'M', '/production', '/index/index', NULL, 1, '1', '1', 'zkxw', 7, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 耗材管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000002', '耗材管理', '90000000000000000000000000000001', 'C', 'consumable', '/production/consumable/index', 'production:consumable:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 试剂管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000003', '试剂管理', '90000000000000000000000000000001', 'C', 'reagent', '/production/reagent/index', 'production:reagent:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- SOP文档
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000004', 'SOP文档', '90000000000000000000000000000001', 'C', 'sop', '/production/sop/index', 'production:sop:list', 1, '1', '1', 'zkxw', 3, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 工艺工序分类
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000005', '工艺工序分类', '90000000000000000000000000000001', 'C', 'process-category', '/production/process-category/index', 'production:process-category:list', 1, '1', '1', 'zkxw', 4, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 工艺产出物类型
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000006', '工艺产出物类型', '90000000000000000000000000000001', 'C', 'output-type', '/production/output-type/index', 'production:output-type:list', 1, '1', '1', 'zkxw', 5, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 耗材包管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000007', '耗材包管理', '90000000000000000000000000000001', 'C', 'consumable-pack', '/production/consumable-pack/index', 'production:consumable-pack:list', 1, '1', '1', 'zkxw', 6, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
-- 工艺库管理
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES
('90000000000000000000000000000008', '工艺库管理', '90000000000000000000000000000001', 'C', 'process', '/production/process/index', 'production:process:list', 1, '1', '1', 'zkxw', 7, NULL, NULL, '', '2026-05-18 00:00:00', NULL, '2026-05-18 00:00:00', 0);
+176
View File
@@ -0,0 +1,176 @@
-- 科研捐赠单主表
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时关联)',
`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`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='科研捐赠单主表';
-- 科研捐赠单-审核记录
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='科研捐赠单-审核记录';
-- 科研捐赠单-当事人客户信息
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='科研捐赠单-当事人客户信息';
-- 科研捐赠单-采集信息
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='科研捐赠单-采集信息';
-- 科研捐赠单-样本信息
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='科研捐赠单-样本信息';
-- 科研捐赠单-样本运输
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='科研捐赠单-样本运输';
-- ============================================================
-- 科研捐赠管理 — sys_menu 菜单数据(ID 与 sqxp.ts 路由 ID 对应)
-- parent_id = '2' 表示挂在 Web 端菜单根节点下
-- ============================================================
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('60000000000000000000000000000001', '科研捐赠管理', '2', 'M', '/research-donation', '/index/index', NULL, 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('60000000000000000000000000000002', '捐赠协议单', '60000000000000000000000000000001', 'C', 'list', '/research-donation/list/index', 'biz:researchDonation:list', 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`) VALUES ('60000000000000000000000000000003', '捐赠协议审核', '60000000000000000000000000000001', 'C', 'audit', '/research-donation/audit/index', 'biz:researchDonationAudit:list', 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);
+25
View File
@@ -0,0 +1,25 @@
-- 中试服务管理菜单 (Web端,parent_id=2 为 Web 端根菜单)
-- 与前端路由 sqxp.ts 中 PilotService 路由块对应
-- 一级菜单目录
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000001', '中试服务管理', '2', 'M', '/pilot-service', NULL, NULL, 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0);
-- 二级菜单
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000002', '细胞产品类型管理', '80000000000000000000000000000001', 'C', 'cell-product-type', 'pilot-service/cell-product-type/index', NULL, 1, '1', '1', 'zkxw', 1, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000003', '细胞产品规格管理', '80000000000000000000000000000001', 'C', 'cell-product-spec', 'pilot-service/cell-product-spec/index', NULL, 1, '1', '1', 'zkxw', 2, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000004', '细胞产品价格体系管理', '80000000000000000000000000000001', 'C', 'cell-product-price', 'pilot-service/cell-product-price/index', NULL, 1, '1', '1', 'zkxw', 3, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000005', '细胞产品项目价格管理', '80000000000000000000000000000001', 'C', 'project-price-system', 'pilot-service/project-price-system/index', NULL, 1, '1', '1', 'zkxw', 4, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000006', '渠道信息管理', '80000000000000000000000000000001', 'C', 'channel', 'pilot-service/channel/index', NULL, 1, '1', '1', 'zkxw', 5, NULL, NULL, '', NOW(), NULL, NOW(), 0);
INSERT INTO `sys_menu` (`id`, `menu_name`, `parent_id`, `menu_type`, `path`, `component`, `perms`, `is_frame`, `visible`, `status`, `tenant_no`, `order_num`, `remark`, `create_by`, `create_dept_id`, `create_time`, `update_by`, `update_time`, `del_flag`)
VALUES ('80000000000000000000000000000007', '体检机构及项目价格', '80000000000000000000000000000001', 'C', 'checkup-institution', 'pilot-service/checkup-institution/index', NULL, 1, '1', '1', 'zkxw', 6, NULL, NULL, '', NOW(), NULL, NOW(), 0);
+64 -1
View File
@@ -370,7 +370,7 @@ CREATE TABLE `biz_channel_agreement` (
`credit_limit_amount` decimal(18,2) DEFAULT NULL COMMENT '授信额度(元)',
`effective_date` char(8) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '生效日期YYYYMMDD',
`expire_date` char(8) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '到期日期YYYYMMDD',
`agreement_status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'sxz' COMMENT 'sxz生效中 yzf已作废',
`agreement_status` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'dsx' COMMENT 'dsx待生效 sxz生效中 yzf已作废',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
@@ -649,6 +649,7 @@ CREATE TABLE `biz_checkup_institution_project` (
`checkup_institution_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '体检机构ID',
`project_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '项目名称',
`project_fee` decimal(18,2) DEFAULT NULL COMMENT '项目费用(元)',
`status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '1正常 0停用',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
@@ -668,6 +669,68 @@ CREATE TABLE `biz_checkup_institution_project` (
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for biz_checkup_institution_package
-- ----------------------------
DROP TABLE IF EXISTS `biz_checkup_institution_package_item`;
DROP TABLE IF EXISTS `biz_checkup_institution_package`;
CREATE TABLE `biz_checkup_institution_package` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`checkup_institution_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '体检机构ID',
`package_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐名称',
`package_code` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐编码(必填,机构内未删除记录唯一,应用层校验)',
`status` char(1) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1' COMMENT '1正常 0停用',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
KEY `idx_ck_pkg_inst` (`checkup_institution_id`,`del_flag`),
KEY `idx_ck_pkg_code` (`checkup_institution_id`,`package_code`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构项目套餐';
-- ----------------------------
-- Records of biz_checkup_institution_package
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for biz_checkup_institution_package_item
-- ----------------------------
DROP TABLE IF EXISTS `biz_checkup_institution_package_item`;
CREATE TABLE `biz_checkup_institution_package_item` (
`id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '主键ID',
`package_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '套餐ID',
`checkup_institution_project_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '机构项目ID',
`project_name` varchar(200) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '项目名称(快照)',
`project_fee` decimal(18,2) DEFAULT NULL COMMENT '套餐内项目费用(元),可独立于机构项目价',
`tenant_no` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'zkxw' COMMENT '租户编号',
`order_num` int(11) DEFAULT NULL COMMENT '排序号',
`remark` varchar(512) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`create_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '创建人id',
`create_dept_id` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '创建人部门ID',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_by` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '更新人id',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`del_flag` tinyint(4) NOT NULL DEFAULT '0' COMMENT '删除标记:0-正常 1-已删除',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_ck_pkg_item` (`package_id`,`checkup_institution_project_id`),
KEY `idx_ck_pkg_item_pkg` (`package_id`,`del_flag`),
KEY `idx_ck_pkg_item_proj` (`checkup_institution_project_id`,`del_flag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='体检机构项目套餐明细';
-- ----------------------------
-- Records of biz_checkup_institution_package_item
-- ----------------------------
BEGIN;
COMMIT;
-- ----------------------------
-- Table structure for biz_clean_room
-- ----------------------------
@@ -0,0 +1,660 @@
# 中试服务管理 6 模块实现计划
> **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:** 实现中试服务管理下 6 个基础数据 CRUD 模块(后端 API + 前端页面 + 路由 + 菜单),覆盖细胞产品类型/规格/价格体系/项目价格、渠道信息、体检机构。
**Architecture:** 后端单文件 `pilot_service.py`(CRUD + Route),Model 追加到 `models.py`;前端 6 个 Vue 页面 + API 函数追加到 `admin.ts`;菜单 SQL 独立文件。所有模块严格复制现有 `customer.py` / `item_unit.py` / `basic_info.py` 模式。
**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2(后端);Vue 3 / TypeScript / Element Plus / Tailwind CSS 4(前端);MySQL 5.7
**Design Spec:** `docs/superpowers/specs/2026-05-17-pilot-service-management-design.md`
---
## File Structure
| # | 文件 | 操作 | 职责 |
|---|------|------|------|
| 1 | `backend/app/models/models.py` | 追加 | 13 个 SQLAlchemy Model |
| 2 | `backend/app/crud/pilot_service.py` | 新建 | 全部 6 模块 CRUD 函数 |
| 3 | `backend/app/api/pilot_service.py` | 新建 | 全部 6 模块路由 + Pydantic Body |
| 4 | `backend/app/api/__init__.py` | 追加 | import + __all__ |
| 5 | `backend/app/main.py` | 追加 | include_router |
| 6 | `web-admin/src/api/admin.ts` | 追加 | API 函数 |
| 7-12 | `web-admin/src/views/pilot-service/{module}/index.vue` | 新建 ×6 | 前端页面 |
| 13 | `web-admin/src/router/modules/sqxp.ts` | 追加 | 路由配置 |
| 14 | `docs/sql/sys_menu_pilot_service.sql` | 新建 | 菜单 INSERT |
---
## Task 1: 后端 — 新增 13 个 SQLAlchemy Model
**Files:**
- Modify: `backend/app/models/models.py` (在 L1354 `BizExhibitionAppointment` 之后、L1356 qc_param import 之前追加)
- [ ] **Step 1: 在 `models.py` 末尾(qc_param import 行之前)追加 13 个 Model 类**
在 `BizExhibitionAppointment` 类定义之后、`from app.models.qc_param import ...` 行之前,追加以下 13 个 Model。每个 Model 严格遵循 `id → 业务字段 → 公共字段` 顺序,列定义与 `zkxw.sql` 中 CREATE TABLE 语句一一对应。
需要追加的 13 个类:
```
# 2.3.1 细胞产品类型
BizCellProductType(biz_cell_product_type)
id, product_type_code, product_type_name → 公共字段
# 2.3.2 细胞产品规格
BizCellProductSpec(biz_cell_product_spec)
id, spec_code, spec_name → 公共字段
# 2.3.3 细胞产品价格体系
BizCellProductPrice(biz_cell_product_price)
id, project_name, project_code, cell_type_code, cell_type_name, spec_text, spec_id, cell_product_type_id, unit_price(Numeric(18,2)) → 公共字段
# 2.3.4 细胞产品项目价格体系(主表)
BizCellProductProjectSystem(biz_cell_product_project_system)
id, system_name, system_code, status(CHAR(1), default="1") → 公共字段
# 2.3.4 细胞产品项目价格体系(明细子表)
BizCellProductProjectSystemItem(biz_cell_product_project_system_item)
id, project_system_id, source_price_id, project_name, project_code, cell_type_code, cell_type_name, spec_text, spec_id, cell_product_type_id, unit_price(Numeric(18,2)) → 公共字段
# 2.3.5 渠道信息(主表)
BizChannel(biz_channel)
id, channel_name, credit_code, channel_type, contact_name, contact_phone, contact_email, protocol_attachments(String(4000)) → 公共字段
# 2.3.5 渠道协议
BizChannelAgreement(biz_channel_agreement)
id, channel_id, agreement_type, business_type, settle_mode, credit_limit_amount(Numeric(18,2)), effective_date(CHAR(8)), expire_date(CHAR(8)), agreement_status(default="sxz") → 公共字段
# 2.3.5 渠道协议-产品价格
BizChannelAgreementProductPrice(biz_channel_agreement_product_price)
id, channel_agreement_id, project_name, project_code, cell_type_code, cell_type_name, spec_text, original_price(Numeric(18,2)), agreement_price(Numeric(18,2)) → 公共字段
# 2.3.5 渠道开票信息
BizChannelInvoice(biz_channel_invoice)
id, channel_id, company_name, tax_no, company_address, company_phone, bank_name, bank_account → 公共字段
# 2.3.5 渠道邮寄地址
BizChannelMailAddress(biz_channel_mail_address)
id, channel_id, mail_address, mail_contact, mail_phone → 公共字段
# 2.3.6 体检机构
BizCheckupInstitution(biz_checkup_institution)
id, institution_name, institution_address, contact_name, contact_phone, status(CHAR(1), default="1") → 公共字段
# 2.3.6 体检机构开票信息
BizCheckupInstitutionInvoice(biz_checkup_institution_invoice)
id, checkup_institution_id, company_name, tax_no, company_address, company_phone, bank_name, bank_account → 公共字段
# 2.3.6 体检机构项目及价格
BizCheckupInstitutionProject(biz_checkup_institution_project)
id, checkup_institution_id, project_name, project_fee(Numeric(18,2)) → 公共字段
```
每个类的公共字段(从 `tenant_no` 到 `del_flag`)完全复制 `BizExhibitionAppointment` 的模式。
- [ ] **Step 2: 验证 Model 无语法错误**
Run: `cd backend && source venv/bin/activate && python -c "from app.models.models import BizCellProductType, BizCellProductSpec, BizCellProductPrice, BizCellProductProjectSystem, BizCellProductProjectSystemItem, BizChannel, BizChannelAgreement, BizChannelAgreementProductPrice, BizChannelInvoice, BizChannelMailAddress, BizCheckupInstitution, BizCheckupInstitutionInvoice, BizCheckupInstitutionProject; print('All 13 models imported OK')"`
Expected: `All 13 models imported OK`
- [ ] **Step 3: Commit**
```bash
git add backend/app/models/models.py
git commit -m "feat: add 13 SQLAlchemy models for pilot service management (2.3.1-2.3.6)"
```
---
## Task 2: 后端 — 创建 CRUD 文件
**Files:**
- Create: `backend/app/crud/pilot_service.py`
- [ ] **Step 1: 创建 `backend/app/crud/pilot_service.py`**
单文件,按模块分段组织。文件结构:
```python
"""中试服务管理 CRUD(细胞产品类型/规格/价格体系/项目价格、渠道信息、体检机构)"""
from __future__ import annotations
import math
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple
from sqlalchemy import and_, func, or_
from sqlalchemy.orm import Session
from app.core.utils import generate_uuid
from app.models.models import (
BizCellProductType, BizCellProductSpec, BizCellProductPrice,
BizCellProductProjectSystem, BizCellProductProjectSystemItem,
BizChannel, BizChannelAgreement, BizChannelAgreementProductPrice,
BizChannelInvoice, BizChannelMailAddress,
BizCheckupInstitution, BizCheckupInstitutionInvoice, BizCheckupInstitutionProject,
)
```
每个实体遵循 `customer.py` 的完整模式:
1. `_row_to_dict_xxx()` — camelCase 转换
2. `list_xxx()` — 分页 + keyword + tenant_no 过滤
3. `get_xxx()` — 单条查询
4. `xxx_code_exists()` — 编码唯一性校验(有编码字段的实体)
5. `create_xxx()` — 新增(generate_uuid, validate, commit+refresh)
6. `update_xxx()` — 更新(validate, commit+refresh)
7. `soft_delete_xxx()` — 软删除(del_flag=1)
**简单实体(复制 customer.py 模式,字段替换)**:
- `BizCellProductType` — keyword 搜索 product_type_code / product_type_name
- `BizCellProductSpec` — keyword 搜索 spec_code / spec_name
- `BizCellProductPrice` — keyword 搜索 project_name / project_code / cell_type_name;额外接受 `cell_type_code` 过滤参数
**主子表实体(主表 CRUD + 子表 CRUD)**:
`BizCellProductProjectSystem` + `BizCellProductProjectSystemItem`:
- 主表:标准 7 函数
- 子表:`list_project_system_items(db, project_system_id, tenant_no)` + `create_project_system_item()` + `update_project_system_item()` + `soft_delete_project_system_item()`
- `_row_to_dict_project_system_item()` 中包含完整字段
`BizChannel` + 4 张子表:
- 主表 BizChannel:标准 7 函数,keyword 搜索 channel_name / credit_code / contact_name
- 子表 BizChannelInvoice:标准子表 CRUD(by channel_id)
- 子表 BizChannelMailAddress:标准子表 CRUD(by channel_id)
- 子表 BizChannelAgreement:标准子表 CRUD(by channel_id)+ `activate_channel_agreement()` (设置 agreement_status="sxz") + `void_channel_agreement()` (设置 agreement_status="yzf")
- 子表 BizChannelAgreementProductPrice:标准子表 CRUD(by channel_agreement_id)
`BizCheckupInstitution` + 2 张子表:
- 主表 BizCheckupInstitution:标准 7 函数
- 子表 BizCheckupInstitutionInvoice:标准子表 CRUD(by checkup_institution_id)
- 子表 BizCheckupInstitutionProject:标准子表 CRUD(by checkup_institution_id)
末尾提供 `total_pages(total, page_size)` 工具函数(与 customer.py 一致)。
- [ ] **Step 2: 验证 CRUD 文件可导入**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud import pilot_service; print('CRUD imported OK')"`
Expected: `CRUD imported OK`
- [ ] **Step 3: Commit**
```bash
git add backend/app/crud/pilot_service.py
git commit -m "feat: add CRUD functions for pilot service management (6 modules, 13 entities)"
```
---
## Task 3: 后端 — 创建路由文件 + 注册
**Files:**
- Create: `backend/app/api/pilot_service.py`
- Modify: `backend/app/api/__init__.py`
- Modify: `backend/app/main.py`
- [ ] **Step 1: 创建 `backend/app/api/pilot_service.py`**
复制 `basic_info.py` 的头部模式:
```python
"""中试服务管理 API(细胞产品类型/规格/价格体系/项目价格、渠道信息、体检机构)"""
from typing import Any, List, Optional
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
from app.core.response import error_response, success_response
from app.crud import pilot_service as ps_svc
from app.models.models import SysUser
router = APIRouter(prefix="/web/admin", tags=["中试服务管理"])
def _get_tenant_no(db: Session, user: dict) -> str:
user_id = user.get("user_id")
if not user_id:
raise PermissionError("用户身份信息缺失")
sys_user = db.query(SysUser).filter(SysUser.id == user_id).first()
if not sys_user or not sys_user.tenant_no:
raise PermissionError("用户租户信息缺失")
return sys_user.tenant_no
```
**每个简单实体提供 4 个端点**(GET list / POST create / PUT update / DELETE),完全复制 `basic_info.py` 中客户管理的模式。
**端点清单**:
简单实体(4 端点 × 3):
- `/cell-product-types` — 2.3.1
- `/cell-product-specs` — 2.3.2
- `/cell-product-prices` — 2.3.3(CreateBody 额外含 cell_type_code, cell_type_name, spec_id, spec_text, cell_product_type_id, unit_price)
主子表实体(主表 4 端点 + 子表 4 端点):
- `/project-price-systems` — 2.3.4 主表
- `/project-price-systems/{id}/items` GET — 列出子表
- `/project-price-systems/{id}/items` POST — 新增子表行
- `/project-system-items/{id}` PUT — 更新子表行
- `/project-system-items/{id}` DELETE — 删除子表行
渠道(主表 4 端点 + 各子表端点):
- `/channels` — 2.3.5 主表 CRUD
- `/channels/{id}/invoices` GET/POST — 开票信息
- `/channel-invoices/{id}` PUT/DELETE — 开票信息
- `/channels/{id}/mail-addresses` GET/POST — 邮寄地址
- `/channel-mail-addresses/{id}` PUT/DELETE — 邮寄地址
- `/channels/{id}/agreements` GET/POST — 协议
- `/channel-agreements/{id}` PUT/DELETE — 协议
- `/channel-agreements/{id}/activate` PUT — 协议生效
- `/channel-agreements/{id}/void` PUT — 协议作废
- `/channel-agreements/{id}/product-prices` GET/POST — 协议产品价格
- `/channel-agreement-product-prices/{id}` PUT/DELETE — 协议产品价格
体检机构(主表 4 端点 + 子表端点):
- `/checkup-institutions` — 2.3.6 主表 CRUD
- `/checkup-institutions/{id}/invoices` GET/POST — 开票信息
- `/checkup-institution-invoices/{id}` PUT/DELETE — 开票信息
- `/checkup-institutions/{id}/projects` GET/POST — 项目及价格
- `/checkup-institution-projects/{id}` PUT/DELETE — 项目及价格
**Pydantic Body Models** 定义在路由文件内部(与 `basic_info.py` 一致),使用 `Field(..., alias="camelCase")` 映射。
**List 端点**暂时使用 `require_admin_or_super_admin`(与 `admin.py` 中物品单位管理一致),不引入 menu_code。后续菜单配置完成后再按需调整。
- [ ] **Step 2: 注册路由到 `__init__.py`**
在 `backend/app/api/__init__.py` 中追加:
```python
from app.api.pilot_service import router as pilot_service_router
```
并在 `__all__` 列表中追加 `"pilot_service_router"`。
- [ ] **Step 3: 注册路由到 `main.py`**
在 `backend/app/main.py` 中:
- import 行追加 `from app.api import ..., pilot_service_router`
- 在已有的 `app.include_router(...)` 行之后追加:
`app.include_router(pilot_service_router, prefix=settings.API_V1_PREFIX)`
- [ ] **Step 4: 验证路由注册成功**
Run: `cd backend && source venv/bin/activate && python -c "from app.main import app; routes = [r.path for r in app.routes]; print(any('cell-product-types' in str(r) for r in routes))"`
Expected: `True`
- [ ] **Step 5: 启动服务验证无报错**
Run: `cd backend && source venv/bin/activate && timeout 5 python -c "import uvicorn; from app.main import app; print('App loads OK')" 2>&1`
Expected: `App loads OK`
- [ ] **Step 6: Commit**
```bash
git add backend/app/api/pilot_service.py backend/app/api/__init__.py backend/app/main.py
git commit -m "feat: add pilot service API routes + registration (6 modules, 13 entities)"
```
---
## Task 4: 前端 — API 函数
**Files:**
- Modify: `web-admin/src/api/admin.ts`
- [ ] **Step 1: 在 `admin.ts` 末尾追加所有 API 函数**
每个实体 4 个函数(fetchXxxList / fetchCreateXxx / fetchUpdateXxx / fetchDeleteXxx),遵循现有模式:
```typescript
// URL 模式: GET /web/admin/cell-product-types, POST /web/admin/cell-product-types, 等
// request.post 使用 params 而非 data(HTTP client 自动处理)
```
需要追加的函数组:
**2.3.1 细胞产品类型** (4 函数)
**2.3.2 细胞产品规格** (4 函数)
**2.3.3 细胞产品价格体系** (4 函数,list 额外支持 cellTypeCode 过滤参数)
**2.3.4 项目价格体系主表** (4 函数)
**2.3.4 项目价格体系子表** (4 函数 + fetchProjectSystemItems)
**2.3.5 渠道主表** (4 函数)
**2.3.5 渠道开票信息子表** (4 函数 + fetchChannelInvoices)
**2.3.5 渠道邮寄地址子表** (4 函数 + fetchChannelMailAddresses)
**2.3.5 渠道协议子表** (4 函数 + fetchChannelAgreements + fetchActivateChannelAgreement + fetchVoidChannelAgreement)
**2.3.5 渠道协议价格子表** (4 函数 + fetchChannelAgreementPrices)
**2.3.6 体检机构主表** (4 函数)
**2.3.6 体检机构开票信息子表** (4 函数 + fetchCheckupInstitutionInvoices)
**2.3.6 体检机构项目子表** (4 函数 + fetchCheckupInstitutionProjects)
总计约 50+ API 函数。
- [ ] **Step 2: 验证 TypeScript 无报错**
Run: `cd web-admin && npx tsc --noEmit src/api/admin.ts 2>&1 | head -20`
Expected: 无错误输出(或仅与本次修改无关的已有警告)
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/api/admin.ts
git commit -m "feat: add frontend API functions for pilot service management (6 modules)"
```
---
## Task 5: 前端 — 细胞产品类型管理页面 (2.3.1)
**Files:**
- Create: `web-admin/src/views/pilot-service/cell-product-type/index.vue`
- [ ] **Step 1: 创建页面文件**
复制 `web-admin/src/views/system/unit/index.vue` 作为模板,修改为:
- `defineOptions({ name: 'PilotCellProductType' })`
- API 调用替换为 `fetchCellProductTypeList` / `fetchCreateCellProductType` / `fetchUpdateCellProductType` / `fetchDeleteCellProductType`
- import 来源 `@/api/admin`
- 列表列:序号、类型编码(`productTypeCode`)、类型名称(`productTypeName`)、备注(`remark`)、创建时间(`createTime`)、操作
- 表单字段:类型编码(必填)、类型名称(必填)、备注
- 搜索:keyword 搜索编码/名称
- 标题:`细胞产品类型管理`
- 保持 `unit/index.vue` 的所有 CSS 类和布局结构不变
- 操作列按钮:查看、编辑、删除
- 不展示排序字段(按 AGENTS.md 约定)
- [ ] **Step 2: 验证页面可正常加载(dev server)**
Run: `cd web-admin && npx vue-tsc --noEmit src/views/pilot-service/cell-product-type/index.vue 2>&1 | head -10`
Expected: 无错误
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/views/pilot-service/cell-product-type/index.vue
git commit -m "feat: add cell product type management page (2.3.1)"
```
---
## Task 6: 前端 — 细胞产品规格管理页面 (2.3.2)
**Files:**
- Create: `web-admin/src/views/pilot-service/cell-product-spec/index.vue`
- [ ] **Step 1: 创建页面文件**
复制 Task 5 的 `cell-product-type/index.vue`,修改为:
- `defineOptions({ name: 'PilotCellProductSpec' })`
- API 调用替换为 `fetchCellProductSpecList` / `fetchCreateCellProductSpec` / ...
- 列表列:序号、规格编码(`specCode`)、规格名称(`specName`)、备注、创建时间、操作
- 表单字段:规格编码(必填)、规格名称(必填)、备注
- 标题:`细胞产品规格管理`
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/views/pilot-service/cell-product-spec/index.vue
git commit -m "feat: add cell product spec management page (2.3.2)"
```
---
## Task 7: 前端 — 细胞产品价格体系管理页面 (2.3.3)
**Files:**
- Create: `web-admin/src/views/pilot-service/cell-product-price/index.vue`
- [ ] **Step 1: 创建页面文件**
复制 Task 5 的 `cell-product-type/index.vue`,扩展为:
- `defineOptions({ name: 'PilotCellProductPrice' })`
- API 调用替换为对应函数
- 列表列:序号、项目名称(`projectName`)、项目编码(`projectCode`)、细胞类型名称(`cellTypeName`)、规格(`specText`)、单价(`unitPrice`)、备注、创建时间、操作
- **新增/编辑弹窗增加下拉关联**:
- 细胞产品类型:`el-select` 调用 `fetchCellProductTypeList({ page: 1, pageSize: 999 })` 获取选项,选择后填充 `cellTypeCode` + `cellTypeName` + `cellProductTypeId`
- 规格:`el-select` 调用 `fetchCellProductSpecList({ page: 1, pageSize: 999 })` 获取选项,选择后填充 `specId` + `specText`
- 单价:`el-input-number` (precision=2, min=0)
- 表单字段:项目名称(必填)、项目编码(必填)、细胞产品类型(下拉)、规格(下拉)、单价(元)、备注
- import 新增:`fetchCellProductTypeList`, `fetchCellProductSpecList`
- 标题:`细胞产品价格体系管理`
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/views/pilot-service/cell-product-price/index.vue
git commit -m "feat: add cell product price system page with dropdown relations (2.3.3)"
```
---
## Task 8: 前端 — 细胞产品项目价格管理页面 (2.3.4)
**Files:**
- Create: `web-admin/src/views/pilot-service/project-price-system/index.vue`
- [ ] **Step 1: 创建页面文件**
参考 `web-admin/src/views/system/category/index.vue` 主子表模式,构建:
- `defineOptions({ name: 'PilotProjectPriceSystem' })`
- **列表区**:主表分页列表
- 列:序号、体系名称(`systemName`)、体系编码(`systemCode`)、状态(tag: 正常/停用)、备注、创建时间、操作
- 操作按钮:查看、编辑、删除
- **新增/编辑弹窗**:
- 上半部分:主表字段(体系名称必填、体系编码必填、状态下拉、备注)
- 下半部分:`el-table` 明细表(可编辑)
- 列:项目名称、项目编码、细胞类型名称、规格、单价(可编辑 el-input-number)、操作(删除)
- 底部按钮:"从价格体系添加" — 弹出 `el-dialog` 选择价格体系(2.3.3)的行,选择后自动填充明细行数据
- 支持手动修改 `unitPrice`
- 保存时:先 save 主表 → 获取 id → 逐行 save 子表
- import:`fetchProjectSystemList`, `fetchCreateProjectSystem`, ... 以及 `fetchCellProductPriceList`(用于选择添加)
- 标题:`细胞产品项目价格管理`
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/views/pilot-service/project-price-system/index.vue
git commit -m "feat: add project price system page with master-detail table (2.3.4)"
```
---
## Task 9: 前端 — 渠道信息管理页面 (2.3.5)
**Files:**
- Create: `web-admin/src/views/pilot-service/channel/index.vue`
- [ ] **Step 1: 创建页面文件**
参考 `web-admin/src/views/system/product/index.vue` 多 section 模式,构建:
- `defineOptions({ name: 'PilotChannel' })`
- **列表区**:渠道分页列表
- 列:序号、渠道名称(`channelName`)、统一社会信用代码(`creditCode`)、渠道类型(tag: yy医院/hzjg合作机构/zs诊所/zfjg政府机构/qt其他)、联系人(`contactName`)、联系电话(`contactPhone`)、创建时间、操作
- **新增/编辑弹窗**(宽度 `1000px`,使用 `el-tabs`):
- **Tab 1 基础信息**:渠道名称(必填)、统一社会信用代码、渠道类型(下拉)、联系人姓名、联系电话、联系邮箱、渠道协议附件(上传)、备注
- **Tab 2 开票信息**:`el-table` 子表 + 新增/编辑/删除按钮
- 弹窗表单:单位名称(必填)、税号、单位地址、电话、开户银行、银行账号
- **Tab 3 邮寄地址**:`el-table` 子表 + 新增/编辑/删除按钮
- 弹窗表单:邮寄地址(必填)、邮寄联系人、邮寄联系电话
- **Tab 4 协议信息**:`el-table` 子表 + 新增/编辑/删除按钮
- 弹窗表单:协议类型(必填,下拉 kyxy/zsxy/lcxy)、结算方式(下拉)、授信额度、生效日期、到期日期
- 每行操作按钮:查看价格明细、生效、作废
- **价格明细子弹窗**:`el-table` 显示协议价格
- 底部按钮:"从价格体系添加"、"从项目价格体系添加"
- 选择后自动填充项目名称/编码/细胞类型/规格/原价,协议价需手动输入
- 支持编辑协议价、删除行
- import:渠道相关 API + `fetchCellProductPriceList` + `fetchProjectSystemList` + `fetchProjectSystemItems`
- 标题:`渠道信息管理`
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/views/pilot-service/channel/index.vue
git commit -m "feat: add channel management page with multi-tab sub-tables (2.3.5)"
```
---
## Task 10: 前端 — 体检机构及项目价格页面 (2.3.6)
**Files:**
- Create: `web-admin/src/views/pilot-service/checkup-institution/index.vue`
- [ ] **Step 1: 创建页面文件**
参考 `category/index.vue` 主子表模式 + Task 9 的 tab 模式:
- `defineOptions({ name: 'PilotCheckupInstitution' })`
- **列表区**:分页列表
- 列:序号、机构名称(`institutionName`)、机构地址(`institutionAddress`)、联系人(`contactName`)、联系电话(`contactPhone`)、状态(tag)、创建时间、操作
- **新增/编辑弹窗**(宽度 `900px`,`el-tabs`):
- **Tab 1 基础信息**:机构名称(必填)、机构地址、联系人、联系电话、状态(正常/停用)、备注
- **Tab 2 开票信息**:`el-table` 子表 + 新增/编辑/删除
- 弹窗表单:单位名称(必填)、税号、单位地址、电话、开户银行、银行账号
- **Tab 3 项目及价格**:`el-table` 子表 + 新增/编辑/删除
- 弹窗表单:项目名称(必填)、项目费用(元, el-input-number)
- 标题:`体检机构及项目价格`
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/views/pilot-service/checkup-institution/index.vue
git commit -m "feat: add checkup institution management page with sub-tables (2.3.6)"
```
---
## Task 11: 前端路由 + 菜单 SQL
**Files:**
- Modify: `web-admin/src/router/modules/sqxp.ts`
- Create: `docs/sql/sys_menu_pilot_service.sql`
- [ ] **Step 1: 在 `sqxp.ts` 中追加中试服务管理路由块**
在 `CellStorage` 路由块之后追加新的路由组:
```typescript
{
id: '80000000000000000000000000000001',
name: 'PilotService',
path: '/pilot-service',
component: '/index/index',
meta: {
title: '中试服务管理',
icon: 'ri:flask-line',
keepAlive: false,
roles: ['admin']
},
children: [
{
id: '80000000000000000000000000000002',
name: 'CellProductType',
path: 'cell-product-type',
component: '/pilot-service/cell-product-type/index',
meta: { title: '细胞产品类型管理', keepAlive: true }
},
{
id: '80000000000000000000000000000003',
name: 'CellProductSpec',
path: 'cell-product-spec',
component: '/pilot-service/cell-product-spec/index',
meta: { title: '细胞产品规格管理', keepAlive: true }
},
{
id: '80000000000000000000000000000004',
name: 'CellProductPrice',
path: 'cell-product-price',
component: '/pilot-service/cell-product-price/index',
meta: { title: '细胞产品价格体系管理', keepAlive: true }
},
{
id: '80000000000000000000000000000005',
name: 'ProjectPriceSystem',
path: 'project-price-system',
component: '/pilot-service/project-price-system/index',
meta: { title: '细胞产品项目价格管理', keepAlive: true }
},
{
id: '80000000000000000000000000000006',
name: 'Channel',
path: 'channel',
component: '/pilot-service/channel/index',
meta: { title: '渠道信息管理', keepAlive: true }
},
{
id: '80000000000000000000000000000007',
name: 'CheckupInstitution',
path: 'checkup-institution',
component: '/pilot-service/checkup-institution/index',
meta: { title: '体检机构及项目价格', keepAlive: true }
}
]
}
```
注意:追加在 `sqxpAdminRoutes` 数组中 `CellStorage` 路由块之后。
- [ ] **Step 2: 创建 `docs/sql/sys_menu_pilot_service.sql`**
```sql
-- 中试服务管理菜单 (Web端,parent_id=2 为 Web 端根菜单)
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES ('80000000000000000000000000000001', '中试服务管理', 2, 5, '/pilot-service', NULL, 'D', '1', '1', NULL, 'ri:flask-line', NOW(), NOW());
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES ('80000000000000000000000000000002', '细胞产品类型管理', '80000000000000000000000000000001', 1, 'cell-product-type', 'pilot-service/cell-product-type/index', 'C', '1', '1', NULL, NULL, NOW(), NOW());
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES ('80000000000000000000000000000003', '细胞产品规格管理', '80000000000000000000000000000001', 2, 'cell-product-spec', 'pilot-service/cell-product-spec/index', 'C', '1', '1', NULL, NULL, NOW(), NOW());
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES ('80000000000000000000000000000004', '细胞产品价格体系管理', '80000000000000000000000000000001', 3, 'cell-product-price', 'pilot-service/cell-product-price/index', 'C', '1', '1', NULL, NULL, NOW(), NOW());
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES ('80000000000000000000000000000005', '细胞产品项目价格管理', '80000000000000000000000000000001', 4, 'project-price-system', 'pilot-service/project-price-system/index', 'C', '1', '1', NULL, NULL, NOW(), NOW());
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES ('80000000000000000000000000000006', '渠道信息管理', '80000000000000000000000000000001', 5, 'channel', 'pilot-service/channel/index', 'C', '1', '1', NULL, NULL, NOW(), NOW());
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, path, component, menu_type, visible, status, perms, icon, create_time, update_time)
VALUES ('80000000000000000000000000000007', '体检机构及项目价格', '80000000000000000000000000000001', 6, 'checkup-institution', 'pilot-service/checkup-institution/index', 'C', '1', '1', NULL, NULL, NOW(), NOW());
```
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/router/modules/sqxp.ts docs/sql/sys_menu_pilot_service.sql
git commit -m "feat: add pilot service routes and menu SQL (6 modules)"
```
---
## Task 12: 最终验证
- [ ] **Step 1: 后端启动验证**
Run: `cd backend && source venv/bin/activate && timeout 8 python -c "from app.main import app; print('Backend loads OK')" 2>&1`
Expected: `Backend loads OK`
- [ ] **Step 2: 前端构建验证**
Run: `cd web-admin && pnpm build 2>&1 | tail -5`
Expected: 构建成功,无错误
- [ ] **Step 3: LSP 诊断检查**
对以下文件运行 `lsp_diagnostics`:
- `backend/app/api/pilot_service.py`
- `backend/app/crud/pilot_service.py`
- `web-admin/src/views/pilot-service/channel/index.vue`
Expected: 无 error 级别诊断
- [ ] **Step 4: Final commit (if any fixes needed)**
```bash
git add -A
git commit -m "fix: address final verification issues for pilot service management"
```
@@ -0,0 +1,815 @@
# 生产管理模块 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:** Implement 7 production management modules (耗材管理、试剂管理、SOP文档、工艺工序分类、工艺产出物类型、耗材包管理、工艺库管理) with backend API + frontend UI, following existing codebase patterns exactly. 工艺路线配置 deferred to next iteration.
**Architecture:** FastAPI backend with flat-function CRUD (no service layer), Vue 3 + Element Plus frontend with standard list page template. Batch 1 (5 simple CRUD modules) has no dependencies. Batch 2 (耗材包+工艺库) depends on Batch 1. All modules share a new API route file `app/api/production.py` and frontend route group under `/production`.
**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2 (backend); Vue 3 / TypeScript / Element Plus / Tailwind CSS 4 (frontend)
**Design Spec:** `docs/superpowers/specs/2026-05-17-production-management-design.md`
**Key Reference Files (READ FIRST):**
- Backend CRUD pattern: `backend/app/crud/item_unit.py` — **canonical simple CRUD**
- Backend API pattern: `backend/app/api/admin.py` (item-unit section, lines 968-1059)
- Backend model pattern: `backend/app/models/models.py` (class BizItemUnit)
- Frontend standard list: `web-admin/src/views/system/unit/index.vue` — **canonical list page**
- Frontend API pattern: `web-admin/src/api/admin.ts` (fetchXxxList/fetchCreateXxx/fetchUpdateXxx/fetchDeleteXxx)
- Frontend router: `web-admin/src/router/modules/sqxp.ts`
- Upload pattern: `web-admin/src/api/business.ts` (fetchUploadAttachment)
---
## File Structure
### Backend (create/modify)
| File | Action | Purpose |
|------|--------|---------|
| `docs/sql/production.sql` | CREATE | All 11 tables DDL + menu INSERT statements |
| `backend/app/models/models.py` | MODIFY | Add 11 model classes at end of file |
| `backend/app/crud/consumable.py` | CREATE | 耗材 CRUD |
| `backend/app/crud/reagent.py` | CREATE | 试剂 CRUD |
| `backend/app/crud/sop_document.py` | CREATE | SOP文档 CRUD |
| `backend/app/crud/process_category.py` | CREATE | 工艺工序分类 CRUD |
| `backend/app/crud/output_type.py` | CREATE | 工艺产出物类型 CRUD |
| `backend/app/crud/consumable_pack.py` | CREATE | 耗材包 CRUD (主表+明细) |
| `backend/app/crud/process.py` | CREATE | 工艺库 CRUD (主表+3子表) |
| `backend/app/api/production.py` | CREATE | All 7 modules API routes |
| `backend/app/api/__init__.py` | MODIFY | Import + export production_router |
| `backend/app/main.py` | MODIFY | Import + register production_router |
### Frontend (create/modify)
| File | Action | Purpose |
|------|--------|---------|
| `web-admin/src/api/production.ts` | CREATE | All 7 modules API functions |
| `web-admin/src/router/modules/sqxp.ts` | MODIFY | Add 生产管理 route group |
| `web-admin/src/views/production/consumable/index.vue` | CREATE | 耗材管理标准列表页 |
| `web-admin/src/views/production/reagent/index.vue` | CREATE | 试剂管理标准列表页 |
| `web-admin/src/views/production/sop/index.vue` | CREATE | SOP文档列表页(含附件上传) |
| `web-admin/src/views/production/process-category/index.vue` | CREATE | 工艺工序分类标准列表页 |
| `web-admin/src/views/production/output-type/index.vue` | CREATE | 工艺产出物类型标准列表页 |
| `web-admin/src/views/production/consumable-pack/index.vue` | CREATE | 耗材包管理列表页(含明细子表) |
| `web-admin/src/views/production/process/index.vue` | CREATE | 工艺库管理列表页(Tab式编辑) |
---
## Task 1: SQL DDL + Menu Entries
**Files:**
- Create: `docs/sql/production.sql`
- [ ] **Step 1: Create the SQL DDL file with all 11 tables and menu entries**
Create `docs/sql/production.sql` with the complete DDL. All tables follow project conventions: `id VARCHAR(64) UUID`, business fields first, then public fields (`tenant_no` through `del_flag`), `ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci`.
Tables to create:
1. `biz_consumable` — consumable_code, consumable_name, spec_model, standard_usage, description
2. `biz_reagent` — reagent_code, reagent_name, spec, standard_usage, unit, description
3. `biz_sop_document` — sop_code, sop_name, version_no, sop_attachments
4. `biz_process_category` — category_code, category_name
5. `biz_output_type` — type_code, type_name
6. `biz_consumable_pack` — pack_code, pack_name, usage_scene, pack_type, status
7. `biz_consumable_pack_item` — pack_id, consumable_code, consumable_name, spec_model, standard_usage, remark
8. `biz_process` — process_code, category_id, process_name, description, equipment_name, estimated_duration, sop_ids
9. `biz_process_reagent` — process_id, reagent_code, reagent_name, spec, standard_usage, unit, is_key_material
10. `biz_process_consumable` — process_id, consumable_code, consumable_name, spec_model, standard_usage, is_required, remark
11. `biz_process_output` — process_id, output_type_id, output_name, output_barcode
Also include INSERT statements for `sys_menu`:
- Top-level menu "生产管理" under Web root (parent_id pointing to the Web menu root, which has id='1')
- 7 child menu items, each with appropriate `menu_code` (e.g., `production:consumable:list`, `production:reagent:list`, etc.)
**Verification:** File contains valid SQL, all tables have public fields, all tables use `VARCHAR(64)` for id, charset utf8mb4.
- [ ] **Step 2: Commit**
```bash
git add docs/sql/production.sql
git commit -m "feat: add production management SQL DDL and menu entries"
```
---
## Task 2: Backend Models (11 SQLAlchemy models)
**Files:**
- Modify: `backend/app/models/models.py` (append at end)
- Modify: `backend/app/models/__init__.py` (re-export new models)
- [ ] **Step 1: Add 11 model classes to models.py**
Append after the last class in `backend/app/models/models.py`. Follow the exact pattern of `BizItemUnit`:
- Inherit from `Base` (from `app.core.database`)
- All columns use `Column(...)` with explicit types
- Public fields in exact order: tenant_no, order_num, remark, create_by, create_dept_id, create_time, update_by, update_time, del_flag
- `create_time` uses `default=datetime.now`, `update_time` uses `default=datetime.now, onupdate=datetime.now`
- `del_flag` uses `SmallInteger` with `default=0`
Models:
- `BizConsumable` → `biz_consumable`
- `BizReagent` → `biz_reagent`
- `BizSopDocument` → `biz_sop_document`
- `BizProcessCategory` → `biz_process_category`
- `BizOutputType` → `biz_output_type`
- `BizConsumablePack` → `biz_consumable_pack`
- `BizConsumablePackItem` → `biz_consumable_pack_item`
- `BizProcess` → `biz_process`
- `BizProcessReagent` → `biz_process_reagent`
- `BizProcessConsumable` → `biz_process_consumable`
- `BizProcessOutput` → `biz_process_output`
- [ ] **Step 2: Re-export in `backend/app/models/__init__.py`**
Add all 11 new classes to the imports and `__all__` list.
- [ ] **Step 3: Verify**
Run: `cd backend && source venv/bin/activate && python -c "from app.models.models import BizConsumable, BizReagent, BizSopDocument, BizProcessCategory, BizOutputType, BizConsumablePack, BizConsumablePackItem, BizProcess, BizProcessReagent, BizProcessConsumable, BizProcessOutput; print('All models imported OK')"`
- [ ] **Step 4: Commit**
```bash
git add backend/app/models/
git commit -m "feat: add 11 production management SQLAlchemy models"
```
---
## Task 3: Backend CRUD — Batch 1 (5 simple modules)
**Files:**
- Create: `backend/app/crud/consumable.py`
- Create: `backend/app/crud/reagent.py`
- Create: `backend/app/crud/sop_document.py`
- Create: `backend/app/crud/process_category.py`
- Create: `backend/app/crud/output_type.py`
Each CRUD file follows the EXACT pattern from `backend/app/crud/item_unit.py`:
1. `_row_to_dict(obj)` — returns dict with camelCase keys
2. `list_xxx(db, *, page, page_size, keyword, tenant_no)` — paginated list with keyword search + tenant filter + soft-delete filter
3. `get_xxx(db, id)` — single record by id with del_flag=0 check
4. `xxx_code_exists(db, code, tenant_no, exclude_id=None)` — uniqueness check on code field
5. `create_xxx(db, *, ...fields..., tenant_no, user_id)` — validate + create with `generate_uuid()`
6. `update_xxx(db, id, *, ...fields..., user_id)` — validate + update
7. `soft_delete_xxx(db, id)` — set del_flag=1
8. `total_pages(total, page_size)` — pagination helper
- [ ] **Step 1: Create `backend/app/crud/consumable.py`**
Fields: consumable_code, consumable_name, spec_model, standard_usage, description
Keyword search on: consumable_code, consumable_name
- [ ] **Step 2: Create `backend/app/crud/reagent.py`**
Fields: reagent_code, reagent_name, spec, standard_usage, unit, description
Keyword search on: reagent_code, reagent_name
- [ ] **Step 3: Create `backend/app/crud/sop_document.py`**
Fields: sop_code, sop_name, version_no, sop_attachments
Keyword search on: sop_code, sop_name
NOTE: `sop_attachments` stores comma-separated URLs, returned as-is in `_row_to_dict`
- [ ] **Step 4: Create `backend/app/crud/process_category.py`**
Fields: category_code, category_name
Keyword search on: category_code, category_name
- [ ] **Step 5: Create `backend/app/crud/output_type.py`**
Fields: type_code, type_name
Keyword search on: type_code, type_name
- [ ] **Step 6: Verify all CRUD modules import correctly**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud import consumable, reagent, sop_document, process_category, output_type; print('All CRUD modules OK')"`
- [ ] **Step 7: Commit**
```bash
git add backend/app/crud/consumable.py backend/app/crud/reagent.py backend/app/crud/sop_document.py backend/app/crud/process_category.py backend/app/crud/output_type.py
git commit -m "feat: add batch 1 CRUD modules (consumable, reagent, sop, process-category, output-type)"
```
---
## Task 4: Backend CRUD — Batch 2 (耗材包 + 工艺库)
**Files:**
- Create: `backend/app/crud/consumable_pack.py`
- Create: `backend/app/crud/process.py`
### 耗材包 CRUD (主表+明细)
- [ ] **Step 1: Create `backend/app/crud/consumable_pack.py`**
This module needs:
- `BizConsumablePack` main table CRUD (list, get, create, update, soft_delete)
- `BizConsumablePackItem` detail table CRUD
- **Create**: Accept main fields + list of items. Generate UUID for pack, then create each item with `pack_id`.
- **Update**: Accept main fields + list of items. Update main record, then **delete existing items + re-insert** (simple replace strategy).
- **Get detail**: Return main record + list of child items (`_row_to_dict` for main includes `items: [...]`)
- **Soft delete**: Also soft-delete all child items (set their del_flag=1)
- `_row_to_dict` for main includes: pack items as nested list
`_row_to_dict` for pack item:
```python
{
"id": item.id,
"packId": item.pack_id,
"consumableCode": item.consumable_code,
"consumableName": item.consumable_name,
"specModel": item.spec_model,
"standardUsage": item.standard_usage,
"remark": item.remark or "",
}
```
### 工艺库 CRUD (主表+3个子表)
- [ ] **Step 2: Create `backend/app/crud/process.py`**
This module needs:
- `BizProcess` main table CRUD
- `BizProcessReagent` sub-table CRUD
- `BizProcessConsumable` sub-table CRUD
- `BizProcessOutput` sub-table CRUD
- **Create**: Accept main fields + 3 lists (reagents, consumables, outputs). Generate UUID for process, then create each sub-record with `process_id`.
- **Update**: Replace strategy — update main, delete+re-insert all sub-records.
- **Get detail**: Return main record + 3 nested lists.
- **Soft delete**: Also soft-delete all child records.
`_row_to_dict` for process includes:
```python
{
"id": ...,
"processCode": ...,
"categoryId": ...,
"processName": ...,
"description": ...,
"equipmentName": ...,
"estimatedDuration": ...,
"sopIds": ...,
# ... public fields ...
"reagents": [reagent_row_to_dict(r) for r in reagents],
"consumables": [consumable_row_to_dict(c) for c in consumables],
"outputs": [output_row_to_dict(o) for o in outputs],
}
```
- [ ] **Step 3: Verify**
Run: `cd backend && source venv/bin/activate && python -c "from app.crud import consumable_pack, process; print('Batch 2 CRUD OK')"`
- [ ] **Step 4: Commit**
```bash
git add backend/app/crud/consumable_pack.py backend/app/crud/process.py
git commit -m "feat: add batch 2 CRUD modules (consumable-pack, process)"
```
---
## Task 5: Backend API Routes + Registration
**Files:**
- Create: `backend/app/api/production.py`
- Modify: `backend/app/api/__init__.py`
- Modify: `backend/app/main.py`
- [ ] **Step 1: Create `backend/app/api/production.py`**
Follow the exact pattern from `backend/app/api/admin.py` (item-units section).
Structure:
```python
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, Field
from sqlalchemy.orm import Session
from typing import Any, Optional, List
from app.core.database import get_db
from app.core.menu_guard import require_admin_or_super_admin, require_menu_code
from app.core.response import success_response, error_response
from app.crud import consumable as consumable_svc
from app.crud import reagent as reagent_svc
from app.crud import sop_document as sop_document_svc
from app.crud import process_category as process_category_svc
from app.crud import output_type as output_type_svc
from app.crud import consumable_pack as consumable_pack_svc
from app.crud import process as process_svc
from app.models.models import SysUser
router = APIRouter(prefix="/web/admin", tags=["生产管理"])
def _get_tenant_no(db: Session, user: dict) -> str:
# Same pattern as admin.py
...
```
**Endpoints to create (7 modules × 4 CRUD operations = 28 endpoints):**
| Module | List (GET) | Create (POST) | Update (PUT) | Delete (DELETE) |
|--------|-----------|---------------|--------------|-----------------|
| 耗材 | `/consumables` | `/consumables` | `/consumables/{id}` | `/consumables/{id}` |
| 试剂 | `/reagents` | `/reagents` | `/reagents/{id}` | `/reagents/{id}` |
| SOP | `/sop-documents` | `/sop-documents` | `/sop-documents/{id}` | `/sop-documents/{id}` |
| 工序分类 | `/process-categories` | `/process-categories` | `/process-categories/{id}` | `/process-categories/{id}` |
| 产出物类型 | `/output-types` | `/output-types` | `/output-types/{id}` | `/output-types/{id}` |
| 耗材包 | `/consumable-packs` | `/consumable-packs` | `/consumable-packs/{id}` | `/consumable-packs/{id}` |
| 工艺库 | `/processes` | `/processes` | `/processes/{id}` | `/processes/{id}` |
**Additional list-only endpoints needed by dropdowns:**
- GET `/reagers/all` — all reagents (no pagination, for dropdown in process form)
- GET `/consumables/all` — all consumables (for dropdown in pack/process form)
- GET `/sop-documents/all` — all SOP docs (for multi-select in process form)
- GET `/process-categories/all` — all categories (for dropdown)
- GET `/output-types/all` — all output types (for dropdown)
- GET `/consumable-packs/{id}/items` — pack items (for importing into process)
**Request body patterns:**
Simple CRUD (耗材 as example):
```python
class ConsumableCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
consumable_code: str = Field(..., alias="consumableCode")
consumable_name: str = Field(..., alias="consumableName")
spec_model: Optional[str] = Field(None, alias="specModel")
standard_usage: Optional[str] = Field(None, alias="standardUsage")
description: Optional[str] = None
order_num: Optional[int] = Field(0, alias="orderNum")
remark: Optional[str] = None
```
耗材包 Create/Update (includes items):
```python
class PackItemInput(BaseModel):
model_config = ConfigDict(populate_by_name=True)
consumable_code: Optional[str] = Field(None, alias="consumableCode")
consumable_name: Optional[str] = Field(None, alias="consumableName")
spec_model: Optional[str] = Field(None, alias="specModel")
standard_usage: Optional[str] = Field(None, alias="standardUsage")
remark: Optional[str] = None
class ConsumablePackCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
pack_code: str = Field(..., alias="packCode")
pack_name: str = Field(..., alias="packName")
usage_scene: Optional[str] = Field(None, alias="usageScene")
pack_type: Optional[str] = Field(None, alias="packType")
status: Optional[str] = "1"
order_num: Optional[int] = Field(0, alias="orderNum")
remark: Optional[str] = None
items: List[PackItemInput] = []
```
工艺库 Create/Update (includes 3 sub-lists):
```python
class ProcessReagentInput(BaseModel):
model_config = ConfigDict(populate_by_name=True)
reagent_code: Optional[str] = Field(None, alias="reagentCode")
reagent_name: Optional[str] = Field(None, alias="reagentName")
spec: Optional[str] = None
standard_usage: Optional[str] = Field(None, alias="standardUsage")
unit: Optional[str] = None
is_key_material: Optional[str] = Field("0", alias="isKeyMaterial")
class ProcessConsumableInput(BaseModel):
model_config = ConfigDict(populate_by_name=True)
consumable_code: Optional[str] = Field(None, alias="consumableCode")
consumable_name: Optional[str] = Field(None, alias="consumableName")
spec_model: Optional[str] = Field(None, alias="specModel")
standard_usage: Optional[str] = Field(None, alias="standardUsage")
is_required: Optional[str] = Field("1", alias="isRequired")
remark: Optional[str] = None
class ProcessOutputInput(BaseModel):
model_config = ConfigDict(populate_by_name=True)
output_type_id: Optional[str] = Field(None, alias="outputTypeId")
output_name: Optional[str] = Field(None, alias="outputName")
output_barcode: Optional[str] = Field(None, alias="outputBarcode")
class ProcessCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
process_code: str = Field(..., alias="processCode")
category_id: Optional[str] = Field(None, alias="categoryId")
process_name: str = Field(..., alias="processName")
description: Optional[str] = None
equipment_name: Optional[str] = Field(None, alias="equipmentName")
estimated_duration: Optional[str] = Field(None, alias="estimatedDuration")
sop_ids: Optional[str] = Field(None, alias="sopIds")
order_num: Optional[int] = Field(0, alias="orderNum")
remark: Optional[str] = None
reagents: List[ProcessReagentInput] = []
consumables: List[ProcessConsumableInput] = []
outputs: List[ProcessOutputInput] = []
```
**Auth patterns:**
- List endpoints: `Depends(require_menu_code("production:xxx:list"))`
- Write endpoints: `Depends(require_admin_or_super_admin)`
- "All" dropdown endpoints: `Depends(require_admin_or_super_admin)` (lightweight, no pagination)
- [ ] **Step 2: Register router in `backend/app/api/__init__.py`**
Add:
```python
from app.api.production import router as production_router
```
Add `production_router` to `__all__`.
- [ ] **Step 3: Register router in `backend/app/main.py`**
Add import and:
```python
app.include_router(production_router, prefix=settings.API_V1_PREFIX)
```
- [ ] **Step 4: Verify backend starts**
Run: `cd backend && source venv/bin/activate && python -c "from app.main import app; routes = [r.path for r in app.routes]; prod = [r for r in routes if 'consumable' in r or 'reagent' in r or 'sop-document' in r or 'process-categor' in r or 'output-type' in r or 'consumable-pack' in r or 'processes' in r]; print(f'Production routes: {len(prod)}'); print(prod[:10])"`
Expected: 28+ production routes registered.
- [ ] **Step 5: Commit**
```bash
git add backend/app/api/production.py backend/app/api/__init__.py backend/app/main.py
git commit -m "feat: add production management API routes (7 modules, 28 endpoints)"
```
---
## Task 6: Frontend API Functions
**Files:**
- Create: `web-admin/src/api/production.ts`
- [ ] **Step 1: Create `web-admin/src/api/production.ts`**
Follow the pattern from `web-admin/src/api/admin.ts`. Use `request.get/post/put/del` with `params` for JSON body and `data` for FormData.
For each of the 7 modules, define 4 API functions (list, create, update, delete). Also define "all" functions for dropdown data.
```typescript
import request from '@/utils/http'
// ==================== 耗材管理 ====================
export function fetchConsumableList(params?: { page?: number; pageSize?: number; keyword?: string }) {
return request.get<{ list: any[]; total: number; page: number; pageSize: number; totalPages: number }>({
url: '/web/admin/consumables', params
})
}
export function fetchAllConsumables() {
return request.get<any[]>({ url: '/web/admin/consumables/all' })
}
export function fetchCreateConsumable(data: Record<string, any>) {
return request.post({ url: '/web/admin/consumables', params: data })
}
export function fetchUpdateConsumable(id: string, data: Record<string, any>) {
return request.put({ url: `/web/admin/consumables/${id}`, params: data })
}
export function fetchDeleteConsumable(id: string) {
return request.del({ url: `/web/admin/consumables/${id}` })
}
// ==================== 试剂管理 ====================
// (same pattern: fetchReagentList, fetchAllReagents, fetchCreateReagent, fetchUpdateReagent, fetchDeleteReagent)
// ==================== SOP文档 ====================
// (same pattern: fetchSopList, fetchAllSopDocuments, fetchCreateSop, fetchUpdateSop, fetchDeleteSop)
// ==================== 工艺工序分类 ====================
// (same pattern: fetchProcessCategoryList, fetchAllProcessCategories, fetchCreateProcessCategory, ...)
// ==================== 工艺产出物类型 ====================
// (same pattern: fetchOutputTypeList, fetchAllOutputTypes, fetchCreateOutputType, ...)
// ==================== 耗材包管理 ====================
// (same pattern, but create/update body includes items array)
// ==================== 工艺库管理 ====================
// (same pattern, but create/update body includes reagents, consumables, outputs arrays)
```
**Important convention from codebase:** POST/PUT use `params` for JSON body, `showSuccessMessage: true` is NOT needed (messages shown in page component).
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/api/production.ts
git commit -m "feat: add production management frontend API functions"
```
---
## Task 7: Frontend Routes + Menu
**Files:**
- Modify: `web-admin/src/router/modules/sqxp.ts`
- [ ] **Step 1: Add production route group to sqxp.ts**
Add a new top-level route entry. Use a unique ID (e.g., `'90000000000000000000000000000001'`). Follow the existing pattern of other top-level groups.
```typescript
{
id: '90000000000000000000000000000001',
name: 'ProductionManagement',
path: '/production',
component: '/index/index',
meta: { title: '生产管理', icon: 'ri:factory-line', keepAlive: false, roles: ['admin'] },
children: [
{
id: '90000000000000000000000000000010',
name: 'ProductionConsumable',
path: 'consumable',
component: '/production/consumable/index',
meta: { title: '耗材管理', keepAlive: true, menuCode: 'production:consumable:list' }
},
{
id: '90000000000000000000000000000020',
name: 'ProductionReagent',
path: 'reagent',
component: '/production/reagent/index',
meta: { title: '试剂管理', keepAlive: true, menuCode: 'production:reagent:list' }
},
{
id: '90000000000000000000000000000030',
name: 'ProductionSop',
path: 'sop',
component: '/production/sop/index',
meta: { title: 'SOP文档', keepAlive: true, menuCode: 'production:sop:list' }
},
{
id: '90000000000000000000000000000040',
name: 'ProductionProcessCategory',
path: 'process-category',
component: '/production/process-category/index',
meta: { title: '工艺工序分类', keepAlive: true, menuCode: 'production:process-category:list' }
},
{
id: '90000000000000000000000000000050',
name: 'ProductionOutputType',
path: 'output-type',
component: '/production/output-type/index',
meta: { title: '工艺产出物类型', keepAlive: true, menuCode: 'production:output-type:list' }
},
{
id: '90000000000000000000000000000060',
name: 'ProductionConsumablePack',
path: 'consumable-pack',
component: '/production/consumable-pack/index',
meta: { title: '耗材包管理', keepAlive: true, menuCode: 'production:consumable-pack:list' }
},
{
id: '90000000000000000000000000000070',
name: 'ProductionProcess',
path: 'process',
component: '/production/process/index',
meta: { title: '工艺库管理', keepAlive: true, menuCode: 'production:process:list' }
}
]
}
```
NOTE: Add this to `sqxpAdminRoutes` array in `sqxp.ts`. Icon only on top-level (sidebar convention).
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/router/modules/sqxp.ts
git commit -m "feat: add production management frontend routes"
```
---
## Task 8: Frontend Pages — Batch 1 (5 simple CRUD pages)
**Files:**
- Create: `web-admin/src/views/production/consumable/index.vue`
- Create: `web-admin/src/views/production/reagent/index.vue`
- Create: `web-admin/src/views/production/sop/index.vue`
- Create: `web-admin/src/views/production/process-category/index.vue`
- Create: `web-admin/src/views/production/output-type/index.vue`
All 5 pages follow the **exact same pattern** as `web-admin/src/views/system/unit/index.vue` (327 lines). Read that file first and replicate its structure.
### Common page structure:
```
page-container
page-head (h1 title + description)
panel
panel-top (title-bar-heading with green vertical bar)
filter-block (keyword search + search/reset buttons)
list-toolbar (selected count + add button)
list-body/list-wrap (el-table with columns + pagination)
el-dialog (add/edit form, label-position="top", form-grid-2)
```
### Per-page differences:
- [ ] **Step 1: Create 耗材管理 page (`production/consumable/index.vue`)**
`defineOptions({ name: 'ProductionConsumable' })`
- Table columns: 耗材编码, 耗材名称, 规格型号, 标准用量, 描述, 创建时间, 操作
- Search keyword searches on: consumableCode, consumableName
- Form fields: consumableCode (required), consumableName (required), specModel, standardUsage, description, remark
- API: `fetchConsumableList, fetchCreateConsumable, fetchUpdateConsumable, fetchDeleteConsumable` from `@/api/production`
- [ ] **Step 2: Create 试剂管理 page (`production/reagent/index.vue`)**
`defineOptions({ name: 'ProductionReagent' })`
- Table columns: 试剂编码, 试剂名称, 规格, 标准用量, 单位, 描述, 创建时间, 操作
- Form fields: reagentCode (required), reagentName (required), spec, standardUsage, unit (el-select dropdown from item-unit API), description, remark
- **Special**: "单位" field is an el-select that loads options from `fetchAllItemUnits()` (import from `@/api/admin`)
- API: `fetchReagentList, fetchCreateReagent, fetchUpdateReagent, fetchDeleteReagent` from `@/api/production`
- [ ] **Step 3: Create SOP文档 page (`production/sop/index.vue`)**
`defineOptions({ name: 'ProductionSop' })`
- Table columns: SOP编号, SOP文档名称, 版本号, 附件数, 创建时间, 操作
- Form fields: sopCode (required), sopName (required), versionNo, sopAttachments (file upload card)
- **Special**: File upload using the card layout pattern from `ProtocolFormDialog.vue`:
- Use `el-upload` with `:auto-upload="false"`, `@change="handleFileChange"`
- Upload via `fetchUploadAttachment(formData)` from `@/api/business`
- Store comma-separated URLs in `sopAttachments` field
- Display attachment count in table
- API: `fetchSopList, fetchCreateSop, fetchUpdateSop, fetchDeleteSop` from `@/api/production`
- [ ] **Step 4: Create 工艺工序分类 page (`production/process-category/index.vue`)**
`defineOptions({ name: 'ProductionProcessCategory' })`
- Table columns: 分类编码, 分类名称, 创建时间, 操作
- Form fields: categoryCode (required), categoryName (required), remark
- Simplest page — identical to unit template
- API: `fetchProcessCategoryList, fetchCreateProcessCategory, fetchUpdateProcessCategory, fetchDeleteProcessCategory` from `@/api/production`
- [ ] **Step 5: Create 工艺产出物类型 page (`production/output-type/index.vue`)**
`defineOptions({ name: 'ProductionOutputType' })`
- Table columns: 类型编码, 类型名称, 创建时间, 操作
- Form fields: typeCode (required), typeName (required), remark
- Same pattern as 工序分类 — very simple
- API: `fetchOutputTypeList, fetchCreateOutputType, fetchUpdateOutputType, fetchDeleteOutputType` from `@/api/production`
- [ ] **Step 6: Verify all 5 pages load**
Run: `cd web-admin && pnpm build 2>&1 | tail -5`
Expected: Build succeeds with no errors.
- [ ] **Step 7: Commit**
```bash
git add web-admin/src/views/production/
git commit -m "feat: add batch 1 frontend pages (consumable, reagent, sop, process-category, output-type)"
```
---
## Task 9: Frontend Page — 耗材包管理
**Files:**
- Create: `web-admin/src/views/production/consumable-pack/index.vue`
- [ ] **Step 1: Create 耗材包管理 page**
`defineOptions({ name: 'ProductionConsumablePack' })`
**List page**: Standard template with additional columns:
- Table columns: 编号, 名称, 使用场景, 类型(display dict: bzhcb=标准耗材包, zdyhcb=自定义耗材包), 状态(el-switch or tag: 1=启用, 0=停用), 创建时间, 操作
**Add/Edit dialog**: Larger dialog with 2 sections:
1. **基本信息**: packCode, packName, usageScene, packType (el-radio-group: bzhcb/zdyhcb), status (el-switch), remark
2. **耗材明细**: Embedded el-table inside the dialog for managing pack items:
- Each row: consumableCode, consumableName, specModel, standardUsage, remark
- "添加耗材" button opens a sub-dialog to select from consumable list (use `fetchAllConsumables()` dropdown or table select)
- After selecting, auto-fill code/name/spec from the consumable record
- Row-level delete button
- Inline editing for standardUsage
**Data flow**:
- On dialog open (edit mode): Load pack detail which includes `items` array
- On save: Submit main fields + items array together to create/update API
- [ ] **Step 2: Verify page loads**
Run: `cd web-admin && pnpm build 2>&1 | tail -5`
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/views/production/consumable-pack/
git commit -m "feat: add consumable pack management frontend page"
```
---
## Task 10: Frontend Page — 工艺库管理
**Files:**
- Create: `web-admin/src/views/production/process/index.vue`
- [ ] **Step 1: Create 工艺库管理 page**
`defineOptions({ name: 'ProductionProcess' })`
**List page**: Standard template:
- Table columns: 工序代码, 工序名称, 工序分类(resolve from category map), 描述, 设备名称, 预估耗时, 创建时间, 操作
- On load: also fetch all categories, SOP docs for display resolution
**Add/Edit dialog**: Wide dialog (width="900px") with `el-tabs`:
- **Tab 1 — 基本信息**: processCode, categoryId (el-select from fetchAllProcessCategories), processName, description, equipmentName, estimatedDuration, sopIds (el-select multiple from fetchAllSopDocuments), remark
- **Tab 2 — 试剂关联**: el-table with rows:
- Columns: reagentCode, reagentName, spec, standardUsage, unit, isKeyMaterial (el-switch/checkbox)
- "添加试剂" button → select from reagent dropdown (fetchAllReagents), auto-fill code/name/spec/usage/unit
- User can modify standardUsage and isKeyMaterial
- Row delete button
- **Tab 3 — 耗材关联**: el-table with rows:
- Columns: consumableCode, consumableName, specModel, standardUsage, isRequired (el-switch), remark
- Two add modes:
1. "添加耗材" button → select from consumable dropdown
2. "从耗材包导入" button → select pack (el-select from fetchAllConsumablePacks), then load pack items and merge into table
- Row delete button
- **Tab 4 — 产出物**: el-table with rows:
- Columns: outputTypeId (display resolved name), outputName, outputBarcode
- "添加产出物" button → row with el-select for output type + input for name
- Row delete button
**Data flow**:
- On dialog open (edit mode): Load process detail which includes reagents/consumables/outputs arrays
- On tab switch: No special action needed (all data in local reactive state)
- On save: Collect all 4 sections' data and submit together
**Key considerations**:
- Load dropdown data (categories, reagents, consumables, SOP docs, output types, consumable packs) when dialog opens
- Use `onMounted` or dialog open handler to pre-fetch reference data
- Display category name in table by maintaining a category map `{id: name}`
- [ ] **Step 2: Verify page loads**
Run: `cd web-admin && pnpm build 2>&1 | tail -5`
- [ ] **Step 3: Commit**
```bash
git add web-admin/src/views/production/process/
git commit -m "feat: add process library management frontend page"
```
---
## Task 11: Final Verification
- [ ] **Step 1: Full frontend build**
Run: `cd web-admin && pnpm build`
Expected: Exit code 0, no errors.
- [ ] **Step 2: Backend import check**
Run: `cd backend && source venv/bin/activate && python -c "from app.main import app; print('Backend OK')"`
- [ ] **Step 3: Manual smoke test (if dev server available)**
1. Start backend: `cd backend && ./start-local.sh`
2. Start frontend: `cd web-admin && pnpm dev`
3. Navigate to each of the 7 pages under 生产管理 sidebar
4. Test CRUD on each page (add, edit, delete, search)
5. Test dropdown loading on 试剂 (unit dropdown), 工艺库 (category/reagent/consumable/SOP/output dropdowns)
6. Test file upload on SOP文档
7. Test耗材包 item management (add/remove items)
8. Test工艺库 tab management (add/remove reagents, consumables, outputs)
---
## Dependency Graph
```
Task 1 (SQL)
↓
Task 2 (Models)
↓
Task 3 (CRUD Batch 1) ← independent from Task 6/7
↓
Task 4 (CRUD Batch 2)
↓
Task 5 (API Routes) ← depends on Task 3 + 4
↓
Task 6 (Frontend API) ← independent, can parallel with Task 5
Task 7 (Frontend Routes) ← independent, can parallel with Task 5
↓
Task 8 (Frontend Batch 1) ← depends on Task 5 + 6 + 7
↓
Task 9 (Frontend 耗材包) ← depends on Task 5 + 6 + 7
Task 10 (Frontend 工艺库) ← depends on Task 5 + 6 + 7 + 8 (needs dropdowns from batch 1)
↓
Task 11 (Final Verification) ← depends on all above
```
**Parallelization opportunities:**
- Tasks 6 + 7 can run in parallel with Task 5
- Tasks 8 + 9 can run in parallel (both depend on Task 5+6+7)
- Task 10 should wait for Task 8 (needs dropdown APIs working)
@@ -0,0 +1,993 @@
# 科研捐赠管理模块 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 只读展示 |
| 删除(单个/批量) | 软删除 |
| 审核流程 | 审核页面操作 |
| 状态变更 | 生效/过期/作废 |
| 转细胞存储 | 捐赠 → 细胞存储协议 |
| 细胞存储转科研 | 细胞存储 → 科研捐赠 |
@@ -0,0 +1,553 @@
# 体检机构项目套餐(Phase 1)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:** 在体检机构维度维护「项目套餐」(主子表),套餐总价为明细费用之和,明细价可编辑;机构项目软删除不阻断、不级联套餐行。
**Architecture:** 新增 `biz_checkup_institution_package` + `_package_item` 两张表;CRUD 追加到 `pilot_service.py`;API 在 `pilot_service.py` 路由文件增加 5 个端点,POST/PUT 一次提交 `items[]` 并在事务内全量替换明细;前端在 `checkup-institution/index.vue` 的「项目价格配置」弹窗内用 Tab 分离「项目列表 / 套餐管理」,套餐编辑交互对齐 `project-price-system` 主子表弹窗。
**Tech Stack:** Python 3.9 / FastAPI / SQLAlchemy / Pydantic v2;Vue 3 / Element Plus / TypeScript;MySQL 5.7
**Design Spec:** `docs/superpowers/specs/2026-05-18-checkup-project-package-design.md`
**SQL:** `docs/sql/checkup_institution_package.sql`
**Out of Scope (Phase 2):** 中试申请单体检「从套餐添加」展开预约明细
---
## File Structure
| # | 文件 | 操作 | 职责 |
|---|------|------|------|
| 1 | `docs/sql/checkup_institution_package.sql` | 已有 | 建表 DDL(实施前在目标库执行) |
| 2 | `docs/sql/zkxw.sql` | 追加 | 基线库同步两张新表(与增量 SQL 一致) |
| 3 | `backend/app/models/models.py` | 追加 | `BizCheckupInstitutionPackage` / `Item` |
| 4 | `backend/app/crud/pilot_service.py` | 追加 | 套餐 CRUD + 校验辅助函数 |
| 5 | `backend/app/api/pilot_service.py` | 追加 | 路由 + Pydantic Body |
| 6 | `web-admin/src/api/admin.ts` | 追加 | 5 个 fetch 函数 |
| 7 | `web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue` | 新建 | 套餐 Tab:列表 + 主子表弹窗 + 选项目子弹窗 |
| 8 | `web-admin/src/views/pilot-service/checkup-institution/index.vue` | 修改 | 项目配置弹窗改为 `el-tabs`;引入套餐面板 |
| 9 | `web-admin/src/App.vue` | 按需 | 若套餐弹窗标题需竖条,在 `panelTitleBarTextRules` 登记(可选) |
---
## Task 1: 数据库
**Files:**
- Execute: `docs/sql/checkup_institution_package.sql`
- Modify: `docs/sql/zkxw.sql`(在 `biz_checkup_institution_project` 表定义之后插入两张新表,内容与增量 SQL 一致)
- [ ] **Step 1: 在开发/测试库执行增量 SQL**
```bash
mysql -h127.0.0.1 -uroot -p zkxw < docs/sql/checkup_institution_package.sql
```
Expected: `Query OK`,`SHOW TABLES LIKE 'biz_checkup_institution_package%'` 返回 2 行。
- [ ] **Step 2: 将建表语句合并进 `docs/sql/zkxw.sql`**
在 `biz_checkup_institution_project` 段落后、`biz_clean_room` 之前插入 `checkup_institution_package.sql` 中两张 `CREATE TABLE`(勿重复 DROP 破坏全量脚本顺序时可只保留 CREATE)。
- [ ] **Step 3: Commit**
```bash
git add docs/sql/checkup_institution_package.sql docs/sql/zkxw.sql
git commit -m "feat(db): add checkup institution package tables"
```
---
## Task 2: 后端 Model
**Files:**
- Modify: `backend/app/models/models.py`(在 `BizCheckupInstitutionProject` 类之后、`# 生产管理` 注释之前)
- [ ] **Step 1: 追加两个 Model 类**
```python
class BizCheckupInstitutionPackage(Base):
"""体检机构项目套餐 biz_checkup_institution_package"""
__tablename__ = "biz_checkup_institution_package"
id = Column(String(64), primary_key=True, comment="主键ID")
checkup_institution_id = Column(String(64), nullable=False, comment="体检机构ID")
package_name = Column(String(200), nullable=False, comment="套餐名称")
package_code = Column(String(64), nullable=False, comment="套餐编码")
status = Column(CHAR(1), nullable=False, default="1", comment="1正常 0停用")
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 BizCheckupInstitutionPackageItem(Base):
"""体检机构项目套餐明细 biz_checkup_institution_package_item"""
__tablename__ = "biz_checkup_institution_package_item"
id = Column(String(64), primary_key=True, comment="主键ID")
package_id = Column(String(64), nullable=False, comment="套餐ID")
checkup_institution_project_id = Column(String(64), nullable=False, comment="机构项目ID")
project_name = Column(String(200), nullable=False, comment="项目名称快照")
project_fee = Column(Numeric(18, 2), 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: 更新 `pilot_service.py` CRUD 顶部 import**
在 `backend/app/crud/pilot_service.py` 的 `from app.models.models import (` 中追加:
```python
BizCheckupInstitutionPackage,
BizCheckupInstitutionPackageItem,
```
- [ ] **Step 3: 验证 import**
```bash
cd backend && source venv/bin/activate && python -c "from app.models.models import BizCheckupInstitutionPackage, BizCheckupInstitutionPackageItem; print('OK')"
```
Expected: `OK`
- [ ] **Step 4: Commit**
```bash
git add backend/app/models/models.py backend/app/crud/pilot_service.py
git commit -m "feat: add SQLAlchemy models for checkup institution packages"
```
---
## Task 3: 后端 CRUD
**Files:**
- Modify: `backend/app/crud/pilot_service.py`(在 `soft_delete_checkup_institution_project` 之后、`# Shared utility` 之前新增 Entity 14 段)
- [ ] **Step 1: 实现字典转换与辅助函数**
新增函数(命名固定,便于 API 层调用):
| 函数 | 职责 |
|------|------|
| `_row_to_dict_checkup_institution_package(row, *, total_fee=None, item_count=None)` | 头表 camelCase + `totalFee` / `itemCount` |
| `_row_to_dict_checkup_institution_package_item(row, *, source_project_deleted=False)` | 明细 + `sourceProjectDeleted` |
| `_sum_package_item_fees(items)` | `sum(project_fee or 0)` |
| `checkup_package_code_exists(db, institution_id, code, tenant_no, exclude_id=None)` | 同机构 `del_flag=0` 编码唯一 |
| `_get_checkup_project_for_package(db, project_id, institution_id, tenant_no)` | 查源项目,不存在返回 None |
| `_validate_package_items_for_save(db, institution_id, items, tenant_no, existing_item_project_ids=None)` | 见下方规则 |
**`_validate_package_items_for_save` 规则:**
1. `items` 至少 1 条;同一请求内 `checkup_institution_project_id` 不重复。
2. 对每条明细:
- 若 `project_id` 属于 `existing_item_project_ids`(更新时保留的历史行):允许源项目 `del_flag=1`,但须校验曾属于本机构。
- 否则:源项目须 `del_flag=0`、`status=1`、且 `checkup_institution_id` 匹配。
3. 若任一新添加行源项目 `status=0`:抛出 `ValueError("套餐不能包含已停用的体检项目")`。
- [ ] **Step 2: 实现列表 / 详情 / 创建 / 更新 / 删除**
```python
def list_checkup_institution_packages(db, checkup_institution_id, tenant_no=None) -> List[Dict[str, Any]]:
# 查头表 del_flag=0,按 create_time desc
# 对每个 package 查明细 del_flag=0,计算 totalFee、itemCount
...
def get_checkup_institution_package(db, package_id, tenant_no=None):
# 头表单条,不存在返回 None
...
def get_checkup_institution_package_detail(db, package_id, tenant_no=None) -> Optional[Dict[str, Any]]:
# 头表 dict + items[](join 源项目 del_flag 填 sourceProjectDeleted)
...
def create_checkup_institution_package(
db, *, checkup_institution_id, package_name, package_code, status="1",
items, tenant_no, order_num=None, remark=None, user_id=None,
):
# 1. checkup_package_code_exists → ValueError
# 2. _validate_package_items_for_save(..., existing_item_project_ids=None)
# 3. 插入头表 + 循环插入明细(project_name 优先 body,否则源项目名)
# 4. commit;返回 get_checkup_institution_package_detail(...)
...
def update_checkup_institution_package(
db, package_id, *, package_name=None, package_code=None, status=None,
items=None, order_num=None, remark=None, user_id=None, tenant_no=None,
):
# 1. get 头表
# 2. 若改 package_code 做唯一校验
# 3. 若 items is not None:
# - 读取旧明细 project_id 集合 → existing_item_project_ids
# - _validate_package_items_for_save(..., existing_item_project_ids=...)
# - 旧明细全部 soft_delete (del_flag=1)
# - 插入新明细行(新 id)
# 4. commit;返回 detail
...
def soft_delete_checkup_institution_package(db, package_id, tenant_no=None):
# 头表 del_flag=1;其下明细 del_flag=1
...
```
**机构项目删除:** 确认 `soft_delete_checkup_institution_project` **不增加**套餐引用检查(保持现状即可,在 commit message 中注明符合设计)。
- [ ] **Step 3: 本地 CRUD 冒烟(可选,Django shell 等价)**
```bash
cd backend && source venv/bin/activate && python - <<'PY'
from app.core.database import SessionLocal
from app.crud import pilot_service as ps
db = SessionLocal()
# 替换为库内真实 institution_id / project_id
inst_id = "REPLACE"
proj_id = "REPLACE"
try:
detail = ps.create_checkup_institution_package(
db,
checkup_institution_id=inst_id,
package_name="基础套餐",
package_code="PKG_BASE_01",
status="1",
items=[{"checkup_institution_project_id": proj_id, "project_name": "血常规", "project_fee": 100.0}],
tenant_no="zkxw",
user_id="admin",
)
print("totalFee", detail.get("totalFee"), "items", len(detail.get("items", [])))
finally:
db.close()
PY
```
Expected: 打印 `totalFee 100.0 items 1`(无异常;测试后可软删该套餐)
- [ ] **Step 4: Commit**
```bash
git add backend/app/crud/pilot_service.py
git commit -m "feat: add CRUD for checkup institution project packages"
```
---
## Task 4: 后端 API 路由
**Files:**
- Modify: `backend/app/api/pilot_service.py`(在体检机构项目路由段之后追加)
- [ ] **Step 1: 定义 Pydantic Body**
```python
class CheckupPackageItemBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
checkup_institution_project_id: str = Field(..., alias="checkupInstitutionProjectId")
project_name: Optional[str] = Field(None, alias="projectName")
project_fee: Optional[float] = Field(None, alias="projectFee")
order_num: Optional[int] = Field(None, alias="orderNum")
remark: Optional[str] = None
class CheckupInstitutionPackageCreateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
package_name: str = Field(..., alias="packageName")
package_code: str = Field(..., alias="packageCode")
status: Optional[str] = Field("1", alias="status")
order_num: Optional[int] = Field(0, alias="orderNum")
remark: Optional[str] = None
items: List[CheckupPackageItemBody] = Field(default_factory=list)
class CheckupInstitutionPackageUpdateBody(BaseModel):
model_config = ConfigDict(populate_by_name=True)
package_name: Optional[str] = Field(None, alias="packageName")
package_code: Optional[str] = Field(None, alias="packageCode")
status: Optional[str] = Field(None, alias="status")
order_num: Optional[int] = Field(None, alias="orderNum")
remark: Optional[str] = None
items: Optional[List[CheckupPackageItemBody]] = None
```
- [ ] **Step 2: 注册 5 个路由**
| 方法 | 路径 | 处理 |
|------|------|------|
| GET | `/checkup-institutions/{institution_id}/packages` | `list_checkup_institution_packages` → `success_response(list)` |
| POST | `/checkup-institutions/{institution_id}/packages` | `create_checkup_institution_package`,body 转 `items` 为 dict 列表 |
| GET | `/checkup-institution-packages/{package_id}` | `get_checkup_institution_package_detail` |
| PUT | `/checkup-institution-packages/{package_id}` | `update_checkup_institution_package` |
| DELETE | `/checkup-institution-packages/{package_id}` | `soft_delete_checkup_institution_package` |
权限:`require_admin_or_super_admin`;`tenant_no = _get_tenant_no(db, user)`。
- [ ] **Step 3: 启动后端并用 curl 验证(需有效 JWT)**
```bash
# 登录后替换 TOKEN、INSTITUTION_ID
curl -s -H "Authorization: Bearer TOKEN" \
"http://127.0.0.1:8000/api/web/admin/checkup-institutions/INSTITUTION_ID/packages" | head -c 500
```
Expected: `code:200`,`data` 为数组(可为 `[]`)。
- [ ] **Step 4: Commit**
```bash
git add backend/app/api/pilot_service.py
git commit -m "feat: add REST API for checkup institution project packages"
```
---
## Task 5: 前端 API
**Files:**
- Modify: `web-admin/src/api/admin.ts`(在 `fetchDeleteCheckupInstitutionProject` 之后)
- [ ] **Step 1: 追加 5 个函数**
```typescript
// 中试服务管理 — 体检机构项目套餐
export function fetchCheckupInstitutionPackages(institutionId: string) {
return request.get({ url: `/web/admin/checkup-institutions/${institutionId}/packages` })
}
export function fetchCheckupInstitutionPackageDetail(packageId: string) {
return request.get({ url: `/web/admin/checkup-institution-packages/${packageId}` })
}
export function fetchCreateCheckupInstitutionPackage(
institutionId: string,
data: Record<string, unknown>
) {
return request.post({
url: `/web/admin/checkup-institutions/${institutionId}/packages`,
params: data
})
}
export function fetchUpdateCheckupInstitutionPackage(
packageId: string,
data: Record<string, unknown>
) {
return request.put({ url: `/web/admin/checkup-institution-packages/${packageId}`, params: data })
}
export function fetchDeleteCheckupInstitutionPackage(packageId: string) {
return request.del({ url: `/web/admin/checkup-institution-packages/${packageId}` })
}
```
- [ ] **Step 2: Commit**
```bash
git add web-admin/src/api/admin.ts
git commit -m "feat(web-admin): add API helpers for checkup institution packages"
```
---
## Task 6: 前端 — `CheckupPackagePanel.vue`
**Files:**
- Create: `web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue`
**Props:**
```typescript
const props = defineProps<{
institutionId: string
institutionName: string
}>()
```
- [ ] **Step 1: 套餐列表区**
- 工具栏:已选 N 项 +「新增套餐」(唯一 `type="primary"`)
- 表格列:序号、套餐名称、套餐编码、项目数(`itemCount`)、合计费用(`totalFee`)、状态(tag)、备注、操作(编辑/删除)
- `loadPackages()` → `fetchCheckupInstitutionPackages(props.institutionId)`,解析 `Array.isArray(res) ? res : res?.list ?? []`
- [ ] **Step 2: 套餐新增/编辑弹窗(主子表)**
对齐 `project-price-system/index.vue`:
**头部表单**(`form-grid-2`,`label-position="top"`):
| 字段 | 校验 |
|------|------|
| packageName | required |
| packageCode | required |
| status | `1` / `0` |
| remark | 可选 |
**明细区**:
- 按钮「添加项目」→ 打开子弹窗
- 表格列:序号、项目名称(+ `sourceProjectDeleted` 时 `<span class="form-label-tip">源项目已删除</span>`)、项目费用(可编辑 `el-input-number`)、操作(删除行)
- 底部:`套餐合计:¥ {{ packageTotalFee }}`(computed,`detailItems` 的 `projectFee` 求和)
**提交 payload:**
```typescript
{
packageName: string,
packageCode: string,
status: string,
remark?: string,
items: detailItems.value.map((row, idx) => ({
checkupInstitutionProjectId: row.checkupInstitutionProjectId,
projectName: row.projectName,
projectFee: row.projectFee,
orderNum: idx + 1
}))
}
```
- 新增:`fetchCreateCheckupInstitutionPackage(institutionId, payload)`
- 编辑:`fetchUpdateCheckupInstitutionPackage(id, payload)`;打开时 `fetchCheckupInstitutionPackageDetail(id)` 填充 `detailItems`
- [ ] **Step 3: 「添加项目」子弹窗**
- 调用已有 `fetchCheckupInstitutionProjects(props.institutionId)`
- 仅展示 `status == '1'` 且未在 `detailItems` 中的项目
- 多选确认后 push:
```typescript
{
checkupInstitutionProjectId: row.id,
projectName: row.projectName,
projectFee: row.projectFee ?? 0,
sourceProjectDeleted: false
}
```
- [ ] **Step 4: 删除套餐**
`ElMessageBox.confirm` → `fetchDeleteCheckupInstitutionPackage` → `loadPackages()`
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/views/pilot-service/checkup-institution/CheckupPackagePanel.vue
git commit -m "feat(web-admin): add checkup package panel component"
```
---
## Task 7: 前端 — 接入 `index.vue` Tab
**Files:**
- Modify: `web-admin/src/views/pilot-service/checkup-institution/index.vue`
- [ ] **Step 1: 将「项目价格配置」弹窗正文改为 `el-tabs`**
```vue
<el-tabs v-model="itemConfigTab" class="item-config-tabs">
<el-tab-pane label="项目列表" name="projects">
<!-- 现有 list-toolbar + el-table + 项目嵌套弹窗 移入此 pane -->
</el-tab-pane>
<el-tab-pane label="套餐管理" name="packages">
<CheckupPackagePanel
v-if="itemConfigTab === 'packages' && currentInstitutionId"
:institution-id="currentInstitutionId"
:institution-name="currentInstitutionName"
/>
</el-tab-pane>
</el-tabs>
```
- [ ] **Step 2: script 增加**
```typescript
import CheckupPackagePanel from './CheckupPackagePanel.vue'
const itemConfigTab = ref<'projects' | 'packages'>('projects')
```
- `openItemDialog` 时重置 `itemConfigTab = 'projects'`
- [ ] **Step 3: 弹窗宽度**
项目配置弹窗 `width` 调至 `960px` 或 `1000px`,避免 Tab 内表格拥挤。
- [ ] **Step 4: 机构操作列(可选)**
主列表操作列可增加「套餐配置」按钮,`openItemDialog(row)` 且 `itemConfigTab = 'packages'`(非必须,Tab 已可达)。
- [ ] **Step 5: Commit**
```bash
git add web-admin/src/views/pilot-service/checkup-institution/index.vue
git commit -m "feat(web-admin): integrate package tab in checkup institution config dialog"
```
---
## Task 8: 联调与验收
- [ ] **Step 1: 后端 lint / 启动**
```bash
cd backend && source venv/bin/activate && python -c "from app.api.pilot_service import router; print('routes OK')"
cd web-admin && pnpm lint
```
- [ ] **Step 2: 手工验收清单(Phase 1)**
| # | 场景 | 预期 |
|---|------|------|
| 1 | 新增套餐,编码必填,选 2 个项目并改价 | 保存成功;列表 `totalFee` = 两行费用之和 |
| 2 | 同机构重复套餐编码 | 接口 400,提示编码已存在 |
| 3 | 套餐含停用项目(新加行) | 保存失败 |
| 4 | 编辑套餐:删除一行、改价 | 合计随动;保存后列表更新 |
| 5 | 软删除某机构项目(被套餐引用) | 删除成功;编辑套餐见「源项目已删除」,合计仍含该行 |
| 6 | 删除套餐 | 列表不再显示 |
- [ ] **Step 3: Commit(若有验收期小修)**
```bash
git commit -m "fix: checkup package QA adjustments after manual test"
```
---
## Spec Coverage(自检)
| 设计要求 | 任务 |
|----------|------|
| 套餐编码必填、机构内唯一 | Task 3 `checkup_package_code_exists`;Task 6 表单校验 |
| 总价 = 明细之和、不落库 | Task 3 `_sum_package_item_fees`;Task 6 computed |
| 明细价可改、不回写机构项目 | Task 3 只写 package_item;Task 6 可编辑列 |
| 机构项目软删不拦截、明细保留 | Task 3 校验 + `sourceProjectDeleted`;Task 3 不改 delete 逻辑 |
| 含停用项禁止保存(新行) | Task 3 `_validate_package_items_for_save` |
| 5 个 API 端点 | Task 4 |
| Tab:项目 / 套餐 | Task 7 |
| Phase 2 从套餐带入 | **未纳入本计划** |
---
## Phase 2 备忘(本计划不实施)
中试申请单体检模块开发时:
- 选机构 →「从套餐添加」→ 仅展开 `del_flag=0 && status=1` 的源项目行
- 写入 `biz_pilot_checkup_reserve_item` 快照
详见设计文档 §6。
@@ -0,0 +1,359 @@
# 中试服务管理 — 6 模块实现设计规格
> 日期: 2026-05-17
> 状态: DRAFT
> 需求来源: `docs/中科兴皖需求V0.3.md` §2.3
---
## 1. 概述
一次性实现中试服务管理下 6 个基础数据管理模块,涵盖后端 API、前端页面、路由和菜单配置。数据库表已全部建好(`docs/sql/zkxw.sql`),无数据。
### 模块清单
| # | 模块 | 表 | 前端模板 | 复杂度 |
|---|------|-----|----------|--------|
| 2.3.1 | 细胞产品类型管理 | `biz_cell_product_type` | `unit/index.vue` | 简单 CRUD |
| 2.3.2 | 细胞产品规格管理 | `biz_cell_product_spec` | `unit/index.vue` | 简单 CRUD |
| 2.3.3 | 细胞产品价格体系管理 | `biz_cell_product_price` | `unit/index.vue` | 中等 — 含下拉关联 |
| 2.3.4 | 细胞产品项目价格管理 | `biz_cell_product_project_system` + `biz_cell_product_project_system_item` | `category/index.vue` | 较复杂 — 主子表 |
| 2.3.5 | 渠道信息管理 | `biz_channel` + 4 张子表 | `product/index.vue` | 复杂 — 多 tab + 子表 |
| 2.3.6 | 体检机构及项目价格 | `biz_checkup_institution` + 2 张子表 | `category/index.vue` | 较复杂 — 主子表 |
### 实现顺序
```
2.3.1 → 2.3.2 → 2.3.3 → 2.3.4 → 2.3.6 → 2.3.5
```
理由:2.3.3 依赖 2.3.1/2.3.2 的下拉数据;2.3.4 依赖 2.3.3;2.3.5 渠道协议价格引用 2.3.1/2.3.2/2.3.3。
---
## 2. 架构决策
| 决策 | 选择 | 理由 |
|------|------|------|
| 后端路由文件 | `backend/app/api/pilot_service.py` 单文件 | 6 模块关联紧密,跟随 `basic_info.py` 模式 |
| 后端 CRUD 文件 | `backend/app/crud/pilot_service.py` 单文件 | 与路由文件对应 |
| 后端 Model | 在 `backend/app/models/models.py` 中追加 | 遵循现有单文件模式 |
| 前端 API | 在 `web-admin/src/api/admin.ts` 中追加 | 遵循现有模式 |
| 前端页面目录 | `web-admin/src/views/pilot-service/{module}/index.vue` | 独立一级路由 |
| 前端路由 | `web-admin/src/router/modules/sqxp.ts` 中追加 | 现有中试服务路由位置 |
| 菜单 SQL | `docs/sql/sys_menu_pilot_service.sql` | 独立增量 SQL |
| 渠道协议价格来源 | 同时支持价格体系(2.3.3)和项目价格体系(2.3.4) | 用户确认,方便渠道自由组合 |
---
## 3. 后端设计
### 3.1 Model(`backend/app/models/models.py`)
在文件末尾追加 13 个 SQLAlchemy Model 类。所有 Model 遵循现有模式:
```
id → 业务字段 → tenant_no, order_num, remark, create_by, create_dept_id, create_time, update_by, update_time, del_flag
```
新增 Model 清单:
1. `BizCellProductType` — `biz_cell_product_type`
2. `BizCellProductSpec` — `biz_cell_product_spec`
3. `BizCellProductPrice` — `biz_cell_product_price`
4. `BizCellProductProjectSystem` — `biz_cell_product_project_system`
5. `BizCellProductProjectSystemItem` — `biz_cell_product_project_system_item`
6. `BizChannel` — `biz_channel`
7. `BizChannelAgreement` — `biz_channel_agreement`
8. `BizChannelAgreementProductPrice` — `biz_channel_agreement_product_price`
9. `BizChannelInvoice` — `biz_channel_invoice`
10. `BizChannelMailAddress` — `biz_channel_mail_address`
11. `BizCheckupInstitution` — `biz_checkup_institution`
12. `BizCheckupInstitutionInvoice` — `biz_checkup_institution_invoice`
13. `BizCheckupInstitutionProject` — `biz_checkup_institution_project`
### 3.2 CRUD(`backend/app/crud/pilot_service.py`)
单文件,按模块分段组织,每段包含:
- `_row_to_dict_xxx()` — 转换为 camelCase 字典
- `list_xxx()` — 分页列表 + keyword 搜索 + tenant_no 过滤
- `get_xxx()` — 单条查询
- `xxx_code_exists()` — 唯一性校验(编码字段)
- `create_xxx()` — 新增
- `update_xxx()` — 更新
- `soft_delete_xxx()` — 软删除
子表额外函数:
- `list_xxx_items(parent_id)` — 按 parent_id 查子表列表
- `create_xxx_item()` / `update_xxx_item()` / `soft_delete_xxx_item()` — 子表 CRUD
### 3.3 Route(`backend/app/api/pilot_service.py`)
单文件,`APIRouter(prefix="/web/admin", tags=["中试服务管理"])`。
每个实体提供 4 个标准端点:
| 方法 | 路径 | 说明 | 权限 |
|------|------|------|------|
| GET | `/cell-product-types` | 分页列表 | `require_menu_code` |
| POST | `/cell-product-types` | 新增 | `require_admin_or_super_admin` |
| PUT | `/cell-product-types/{id}` | 更新 | `require_admin_or_super_admin` |
| DELETE | `/cell-product-types/{id}` | 删除 | `require_admin_or_super_admin` |
子表额外端点(如项目价格体系明细、渠道子表等):
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/cell-product-project-systems/{id}/items` | 获取子表明细 |
| POST | `/cell-product-project-systems/{id}/items` | 新增子表行 |
| PUT | `/project-system-items/{id}` | 更新子表行 |
| DELETE | `/project-system-items/{id}` | 删除子表行 |
Pydantic Body Model 定义在路由文件内部,使用 `alias` 支持 camelCase。
渠道信息管理(2.3.5)因其复杂度,需要以下额外端点:
- 渠道基础信息 CRUD(含渠道协议附件上传)
- 渠道开票信息 CRUD(子表)
- 渠道邮寄地址 CRUD(子表)
- 渠道协议 CRUD(子表,含状态变更:生效/作废)
- 渠道协议-产品价格 CRUD(协议的子表)
- 协议状态变更端点:`PUT /channel-agreements/{id}/activate` 和 `PUT /channel-agreements/{id}/void`
### 3.4 路由注册
1. `backend/app/api/__init__.py` — 添加 `from app.api.pilot_service import router as pilot_service_router` + `__all__`
2. `backend/app/main.py` — `from app.api import ..., pilot_service_router` + `app.include_router(pilot_service_router, prefix=settings.API_V1_PREFIX)`
---
## 4. 前端设计
### 4.1 API 函数(`web-admin/src/api/admin.ts`)
追加以下 API 函数(每个实体 4 个:list / create / update / delete):
```
// 2.3.1 细胞产品类型
fetchCellProductTypeList / fetchCreateCellProductType / fetchUpdateCellProductType / fetchDeleteCellProductType
// 2.3.2 细胞产品规格
fetchCellProductSpecList / fetchCreateCellProductSpec / fetchUpdateCellProductSpec / fetchDeleteCellProductSpec
// 2.3.3 细胞产品价格体系
fetchCellProductPriceList / fetchCreateCellProductPrice / fetchUpdateCellProductPrice / fetchDeleteCellProductPrice
// 2.3.4 细胞产品项目价格体系
fetchProjectSystemList / fetchCreateProjectSystem / fetchUpdateProjectSystem / fetchDeleteProjectSystem
fetchProjectSystemItems / fetchCreateProjectSystemItem / fetchUpdateProjectSystemItem / fetchDeleteProjectSystemItem
// 2.3.5 渠道信息
fetchChannelList / fetchCreateChannel / fetchUpdateChannel / fetchDeleteChannel
fetchChannelInvoices / fetchCreateChannelInvoice / fetchUpdateChannelInvoice / fetchDeleteChannelInvoice
fetchChannelMailAddresses / fetchCreateChannelMailAddress / fetchUpdateChannelMailAddress / fetchDeleteChannelMailAddress
fetchChannelAgreements / fetchCreateChannelAgreement / fetchUpdateChannelAgreement / fetchDeleteChannelAgreement
fetchChannelAgreementPrices / fetchCreateChannelAgreementPrice / fetchUpdateChannelAgreementPrice / fetchDeleteChannelAgreementPrice
fetchActivateChannelAgreement / fetchVoidChannelAgreement
// 2.3.6 体检机构
fetchCheckupInstitutionList / fetchCreateCheckupInstitution / fetchUpdateCheckupInstitution / fetchDeleteCheckupInstitution
fetchCheckupInstitutionInvoices / fetchCreateCheckupInstitutionInvoice / ...
fetchCheckupInstitutionProjects / fetchCreateCheckupInstitutionProject / ...
```
### 4.2 页面文件
```
web-admin/src/views/pilot-service/
├── cell-product-type/index.vue # 2.3.1 — 复制 unit/index.vue
├── cell-product-spec/index.vue # 2.3.2 — 复制 unit/index.vue
├── cell-product-price/index.vue # 2.3.3 — 复制 unit/index.vue,表单含下拉关联
├── project-price-system/index.vue # 2.3.4 — 复制 category/index.vue,主子表
├── channel/index.vue # 2.3.5 — 复制 product/index.vue,多 tab + 子表
└── checkup-institution/index.vue # 2.3.6 — 复制 category/index.vue,主子表
```
### 4.3 页面详细设计
#### 2.3.1 细胞产品类型管理(简单 CRUD)
- **列表列**: 序号、类型编码(`product_type_code`)、类型名称(`product_type_name`)、排序号、备注、创建时间、操作
- **新增/编辑弹窗**: 2 列表单,字段:类型编码(必填)、类型名称(必填)、排序号、备注
- **查看弹窗**: `SystemViewDialog`,展示基本信息
- **搜索**: keyword 搜索编码/名称
#### 2.3.2 细胞产品规格管理(简单 CRUD)
- **列表列**: 序号、规格编码(`spec_code`)、规格名称(`spec_name`)、排序号、备注、创建时间、操作
- **新增/编辑弹窗**: 2 列表单,字段:规格编码(必填)、规格名称(必填)、排序号、备注
- **查看弹窗**: `SystemViewDialog`
- **搜索**: keyword 搜索编码/名称
#### 2.3.3 细胞产品价格体系管理(含下拉关联)
- **列表列**: 序号、项目名称(`project_name`)、项目编码(`project_code`)、细胞类型名称(`cell_type_name`)、规格(`spec_text`)、单价(`unit_price`)、排序号、备注、创建时间、操作
- **新增/编辑弹窗**: 2 列表单,字段:
- 项目名称(必填)、项目编码(必填)
- 细胞产品类型(下拉,从 2.3.1 获取,选择后填充 `cell_type_code` + `cell_type_name`)
- 规格(下拉,从 2.3.2 获取,选择后填充 `spec_id` + `spec_text`)
- 单价(元,数字输入)、排序号、备注
- **搜索**: keyword + 细胞类型下拉筛选
#### 2.3.4 细胞产品项目价格管理(主子表)
- **列表列**: 序号、体系名称(`system_name`)、体系编码(`system_code`)、状态(tag)、排序号、备注、创建时间、操作
- **新增/编辑弹窗**:
- 顶部:体系名称(必填)、体系编码(必填)、状态(下拉:正常/停用)、排序号、备注
- 下方:明细表格(可编辑表格)
- 操作列:添加行(从价格体系 2.3.3 选择)、删除行
- 列:项目名称、项目编码、细胞类型名称、规格、单价(可修改)
- 选择后自动填充:`source_price_id`, `project_name`, `project_code`, `cell_type_code`, `cell_type_name`, `spec_text`, `spec_id`, `cell_product_type_id`
- `unit_price` 默认带入选中的价格,但可修改
- **查看弹窗**: 展示主表信息 + 明细列表
#### 2.3.5 渠道信息管理(多 Tab + 子表)
- **列表列**: 序号、渠道名称(`channel_name`)、统一社会信用代码(`credit_code`)、渠道类型(tag)、联系人、联系电话、排序号、创建时间、操作
- **新增/编辑弹窗**: 使用 Tab 分区
- **Tab 1 基础信息**: 渠道名称(必填)、统一社会信用代码、渠道类型(下拉:yy/hzjg/zs/zfjg/qt)、联系人姓名、联系电话、联系邮箱、渠道协议附件(上传)、排序号、备注
- **Tab 2 开票信息**: 子表列表 + 新增/编辑/删除
- 字段:单位名称(必填)、税号、单位地址、电话、开户银行、银行账号
- **Tab 3 邮寄地址**: 子表列表 + 新增/编辑/删除
- 字段:邮寄地址(必填)、邮寄联系人、邮寄联系电话
- **Tab 4 协议信息**: 子表列表 + 新增/编辑/删除
- 每个协议可展开查看/编辑协议价格明细
- 协议字段:协议类型(必填,下拉 kyxy/zsxy/lcxy)、业务类型(随协议类型自动填充)、结算方式(下拉)、授信额度、生效日期、到期日期、协议状态(tag)
- 操作按钮:生效、作废(仅状态变更)
- **协议价格明细子表**:
- 可从价格体系(2.3.3)或项目价格体系(2.3.4)中选择添加
- 选择后自动填充:项目名称、项目编码、细胞类型编码、细胞类型名称、规格
- 原价(自动带入,可修改)、协议价(必填)
#### 2.3.6 体检机构及项目价格(主子表)
- **列表列**: 序号、机构名称(`institution_name`)、机构地址、联系人、联系电话、状态(tag)、排序号、创建时间、操作
- **新增/编辑弹窗**: 使用 Tab 分区
- **Tab 1 基础信息**: 机构名称(必填)、机构地址、联系人、联系电话、状态(正常/停用)、排序号、备注
- **Tab 2 开票信息**: 子表列表 + 新增/编辑/删除
- 字段:单位名称(必填)、税号、单位地址、电话、开户银行、银行账号
- **Tab 3 项目及价格**: 子表列表 + 新增/编辑/删除
- 字段:项目名称(必填)、项目费用(元)
### 4.4 路由配置
在 `web-admin/src/router/modules/sqxp.ts` 中添加:
```typescript
{
id: '80000000000000000000000000000001',
name: 'PilotService',
path: '/pilot-service',
component: '/index/index',
meta: {
title: '中试服务管理',
icon: 'ri:flask-line',
keepAlive: false,
roles: ['admin']
},
children: [
{
id: '80000000000000000000000000000002',
name: 'CellProductType',
path: 'cell-product-type',
component: '/pilot-service/cell-product-type/index',
meta: { title: '细胞产品类型管理', keepAlive: true }
},
{
id: '80000000000000000000000000000003',
name: 'CellProductSpec',
path: 'cell-product-spec',
component: '/pilot-service/cell-product-spec/index',
meta: { title: '细胞产品规格管理', keepAlive: true }
},
{
id: '80000000000000000000000000000004',
name: 'CellProductPrice',
path: 'cell-product-price',
component: '/pilot-service/cell-product-price/index',
meta: { title: '细胞产品价格体系管理', keepAlive: true }
},
{
id: '80000000000000000000000000000005',
name: 'ProjectPriceSystem',
path: 'project-price-system',
component: '/pilot-service/project-price-system/index',
meta: { title: '细胞产品项目价格管理', keepAlive: true }
},
{
id: '80000000000000000000000000000006',
name: 'Channel',
path: 'channel',
component: '/pilot-service/channel/index',
meta: { title: '渠道信息管理', keepAlive: true }
},
{
id: '80000000000000000000000000000007',
name: 'CheckupInstitution',
path: 'checkup-institution',
component: '/pilot-service/checkup-institution/index',
meta: { title: '体检机构及项目价格', keepAlive: true }
}
]
}
```
---
## 5. 菜单 SQL
新增文件 `docs/sql/sys_menu_pilot_service.sql`,包含:
- 一级菜单目录:中试服务管理(parent_id 指向 Web 端根菜单,即 `id=2`)
- 6 个二级菜单项
- 与路由 id 保持一致
---
## 6. UI 规范遵循
所有页面严格遵循 `docs/web-admin-ui-spec-fresh-mint.md` 和 AGENTS.md 中的 UI 约定:
- **颜色**: 主色 `#10b981`(Fresh Mint),不使用小程序颜色
- **表单**: `label-position="top"`,使用 `.dialog-form-layout` + `.form-grid-2`
- **弹窗**: `backdrop-filter: none`,`max-height: calc(100vh - 100px)` 只 body 滚动
- **标题**: `panel-top` → `h2.title-bar-heading` 带竖线
- **按钮**: 每行只有一个 `type="primary"`,工具栏"已选 N 项"在前
- **排序字段**: 列表和弹窗中默认不展示 `order_num` 排序字段(按 AGENTS.md 约定)
- **操作列**: `fixed="right"`,`min-width ≥ 260px`
---
## 7. 交付物清单
| # | 文件 | 操作 |
|---|------|------|
| 1 | `backend/app/models/models.py` | 追加 13 个 Model |
| 2 | `backend/app/crud/pilot_service.py` | 新建 |
| 3 | `backend/app/api/pilot_service.py` | 新建 |
| 4 | `backend/app/api/__init__.py` | 追加 import + __all__ |
| 5 | `backend/app/main.py` | 追加 include_router |
| 6 | `web-admin/src/api/admin.ts` | 追加 API 函数 |
| 7 | `web-admin/src/views/pilot-service/cell-product-type/index.vue` | 新建 |
| 8 | `web-admin/src/views/pilot-service/cell-product-spec/index.vue` | 新建 |
| 9 | `web-admin/src/views/pilot-service/cell-product-price/index.vue` | 新建 |
| 10 | `web-admin/src/views/pilot-service/project-price-system/index.vue` | 新建 |
| 11 | `web-admin/src/views/pilot-service/channel/index.vue` | 新建 |
| 12 | `web-admin/src/views/pilot-service/checkup-institution/index.vue` | 新建 |
| 13 | `web-admin/src/router/modules/sqxp.ts` | 追加路由 |
| 14 | `docs/sql/sys_menu_pilot_service.sql` | 新建 |
---
## 8. 不在范围内
以下内容不在本次实现中(后续模块需要):
- 2.3.7 渠道服务购买订单管理
- 2.3.8 渠道服务购买订单管理(财务确认)
- 2.3.9 中试服务单管理
- 工作流集成
- 数据权限(`apply_data_scope`)— 本次使用 `require_admin_or_super_admin`,后续按需添加
@@ -0,0 +1,224 @@
# 生产管理模块设计规格
## 概述
实现需求文档 2.5 节中生产管理下的 7 个子模块(工艺路线配置暂不实现)。
新增一级菜单【生产管理】,下设 7 个二级菜单。按依赖关系分 2 批实现。
## 范围
| 批次 | 模块 | 需求章节 | 依赖 |
|------|------|----------|------|
| 第1批 | 耗材管理 | 2.5.1 | 无 |
| 第1批 | 试剂管理 | 2.5.3 | 物品单位管理(已有) |
| 第1批 | SOP文档 | 2.5.4 | 无(附件上传) |
| 第1批 | 工艺工序分类 | 2.5.5 | 无 |
| 第1批 | 工艺产出物类型 | 2.3.6 | 无 |
| 第2批 | 耗材包管理 | 2.5.2 | 耗材管理 |
| 第2批 | 工艺库管理 | 2.3.7 | 试剂、耗材、SOP、工序分类、产出物类型 |
**不在范围内**: 工艺路线配置[2.3.8] — 留待下次迭代。
## SQL 表(需新建 11 张)
### 第1批:单表模块(5 张)
#### 1. biz_consumable — 耗材
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| consumable_code | VARCHAR(64) NOT NULL | 耗材编码 |
| consumable_name | VARCHAR(100) NOT NULL | 耗材名称 |
| spec_model | VARCHAR(200) DEFAULT NULL | 规格型号 |
| standard_usage | VARCHAR(100) DEFAULT NULL | 标准用量 |
| description | VARCHAR(500) DEFAULT NULL | 描述 |
| + 公共字段 | | tenant_no ~ del_flag |
#### 2. biz_reagent — 试剂
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| reagent_code | VARCHAR(64) NOT NULL | 试剂编码 |
| reagent_name | VARCHAR(100) NOT NULL | 试剂名称 |
| spec | VARCHAR(200) DEFAULT NULL | 规格 |
| standard_usage | VARCHAR(100) DEFAULT NULL | 标准用量 |
| unit | VARCHAR(64) DEFAULT NULL | 单位(下拉自物品单位管理) |
| description | VARCHAR(500) DEFAULT NULL | 描述 |
| + 公共字段 | | |
#### 3. biz_sop_document — SOP文档
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| sop_code | VARCHAR(64) NOT NULL | SOP编号 |
| sop_name | VARCHAR(200) NOT NULL | SOP文档名称 |
| version_no | VARCHAR(50) DEFAULT NULL | 版本号 |
| sop_attachments | VARCHAR(2000) DEFAULT NULL | SOP文档附件(逗号分隔URL) |
| + 公共字段 | | |
#### 4. biz_process_category — 工艺工序分类
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| category_code | VARCHAR(64) NOT NULL | 分类编码 |
| category_name | VARCHAR(100) NOT NULL | 分类名称 |
| + 公共字段 | | |
#### 5. biz_output_type — 工艺产出物类型
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| type_code | VARCHAR(64) NOT NULL | 类型编码 |
| type_name | VARCHAR(100) NOT NULL | 类型名称 |
| + 公共字段 | | |
### 第2批:主表+子表模块(6 张)
#### 6. biz_consumable_pack — 耗材包主表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| pack_code | VARCHAR(64) NOT NULL | 编号 |
| pack_name | VARCHAR(100) NOT NULL | 名称 |
| usage_scene | VARCHAR(200) DEFAULT NULL | 使用场景 |
| pack_type | VARCHAR(20) DEFAULT NULL | 类型(bzhcb-标准耗材包、zdyhcb-自定义耗材包) |
| status | CHAR(1) DEFAULT '1' | 状态(1-启用、0-停用) |
| + 公共字段 | | |
#### 7. biz_consumable_pack_item — 耗材包耗材明细
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| pack_id | VARCHAR(64) NOT NULL | 耗材包id |
| consumable_code | VARCHAR(64) DEFAULT NULL | 耗材编码 |
| consumable_name | VARCHAR(100) DEFAULT NULL | 耗材名称 |
| spec_model | VARCHAR(200) DEFAULT NULL | 耗材规格型号 |
| standard_usage | VARCHAR(100) DEFAULT NULL | 标准用量 |
| remark | VARCHAR(512) DEFAULT NULL | 备注 |
| + 公共字段 | | |
#### 8. biz_process — 工艺库主表
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| process_code | VARCHAR(64) NOT NULL | 工序代码 |
| category_id | VARCHAR(64) DEFAULT NULL | 工序分类id(下拉自工艺工序分类) |
| process_name | VARCHAR(100) NOT NULL | 工序名称 |
| description | VARCHAR(500) DEFAULT NULL | 描述 |
| equipment_name | VARCHAR(200) DEFAULT NULL | 预计使用设备名称 |
| estimated_duration | VARCHAR(100) DEFAULT NULL | 预估耗时 |
| sop_ids | VARCHAR(1000) DEFAULT NULL | 关联SOP文档id(逗号分隔) |
| + 公共字段 | | |
#### 9. biz_process_reagent — 工艺-试剂关联
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| process_id | VARCHAR(64) NOT NULL | 工艺id |
| reagent_code | VARCHAR(64) DEFAULT NULL | 试剂编码 |
| reagent_name | VARCHAR(100) DEFAULT NULL | 试剂名称 |
| spec | VARCHAR(200) DEFAULT NULL | 规格 |
| standard_usage | VARCHAR(100) DEFAULT NULL | 标准用量 |
| unit | VARCHAR(64) DEFAULT NULL | 单位 |
| is_key_material | CHAR(1) DEFAULT '0' | 是否关键物料(0-否、1-是) |
| + 公共字段 | | |
#### 10. biz_process_consumable — 工艺-耗材关联
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| process_id | VARCHAR(64) NOT NULL | 工艺id |
| consumable_code | VARCHAR(64) DEFAULT NULL | 耗材编码 |
| consumable_name | VARCHAR(100) DEFAULT NULL | 耗材名称 |
| spec_model | VARCHAR(200) DEFAULT NULL | 耗材规格型号 |
| standard_usage | VARCHAR(100) DEFAULT NULL | 标准用量 |
| is_required | CHAR(1) DEFAULT '1' | 是否必需(1-必须、0-可选) |
| remark | VARCHAR(512) DEFAULT NULL | 备注 |
| + 公共字段 | | |
#### 11. biz_process_output — 工艺-产出关联
| 字段 | 类型 | 说明 |
|------|------|------|
| id | VARCHAR(64) PK | UUID |
| process_id | VARCHAR(64) NOT NULL | 工艺id |
| output_type_id | VARCHAR(64) DEFAULT NULL | 产出类型(下拉自工艺产出物类型) |
| output_name | VARCHAR(200) DEFAULT NULL | 产出物名称 |
| output_barcode | VARCHAR(200) DEFAULT NULL | 产出物条码(生产时才有数据) |
| + 公共字段 | | |
## 模块设计
### 模块 1-5: 耗材/试剂/SOP/工序分类/产出物类型(简单 CRUD)
- **类型**: 标准列表模式
- **后端**: Model + CRUD + API 路由(完全参照 `item_unit.py` 模式)
- **前端**: 参照 `system/unit/index.vue` 标准列表
- **特殊**: 试剂管理的"单位"字段下拉数据从物品单位管理获取;SOP文档需要附件上传
### 模块 6: 耗材包管理(主表+明细)
- **类型**: 标准列表 + 弹窗内明细表格
- **后端**: 主表 CRUD + 明细子表 CRUD(级联创建/更新/删除)
- **前端**: 列表参照 `unit/index.vue`;新增/编辑弹窗中嵌入耗材明细表格,支持:
- 从耗材库逐条添加(下拉选择耗材后自动带入编码/名称/规格/用量)
- 行内编辑标准用量
- 行内删除
- **API**: `/web/admin/consumable-packs` + `/web/admin/consumable-packs/{id}` (含明细一起提交)
### 模块 7: 工艺库管理(主表+3个关联子表)
- **类型**: 标准列表 + Tab式编辑弹窗
- **后端**: 主表 CRUD + 3 个子表 CRUD(级联操作)
- **前端**: 列表页标准列表;新增/编辑弹窗分 4 个 Tab:
- Tab1「基本信息」: 工序代码、工序分类(下拉)、工序名称、描述、设备名称、预估耗时、关联SOP(多选下拉)
- Tab2「试剂关联」: 表格形式,下拉选试剂后自动带入编码/名称/规格/用量/单位,可改标准用量,标记是否关键物料
- Tab3「耗材关联」: 表格形式,支持从耗材包批量导入或从耗材库逐条添加;可改标准用量,标记是否必需
- Tab4「产出物」: 表格形式,下拉选产出类型 + 填产出物名称
- **API**: `/web/admin/processes` + `/web/admin/processes/{id}` (含所有子表一起提交)
## 路由与菜单
### 前端路由
在 `sqxp.ts` 新增顶级路由组:
```
/production
/production/consumable 耗材管理
/production/reagent 试剂管理
/production/sop SOP文档
/production/process-category 工艺工序分类
/production/output-type 工艺产出物类型
/production/consumable-pack 耗材包管理
/production/process 工艺库管理
```
### 后端 API
统一路由前缀 `/web/admin`,新增以下端点:
```
GET/POST /consumables 耗材列表/创建
GET/PUT/DELETE /consumables/{id} 耗材详情/更新/删除
GET/POST /reagents 试剂列表/创建
GET/PUT/DELETE /reagents/{id} 试剂详情/更新/删除
GET/POST /sop-documents SOP列表/创建
GET/PUT/DELETE /sop-documents/{id} SOP详情/更新/删除
GET/POST /process-categories 工序分类列表/创建
GET/PUT/DELETE /process-categories/{id} 工序分类详情/更新/删除
GET/POST /output-types 产出物类型列表/创建
GET/PUT/DELETE /output-types/{id} 产出物类型详情/更新/删除
GET/POST /consumable-packs 耗材包列表/创建
GET/PUT/DELETE /consumable-packs/{id} 耗材包详情/更新/删除
GET/POST /processes 工艺库列表/创建
GET/PUT/DELETE /processes/{id} 工艺库详情/更新/删除
```
### 菜单 SQL
在 `sys_menu` 中新增生产管理顶级菜单(parent_id 指向 Web 端菜单根节点)及 7 个子菜单项。
## 技术规范
- 后端模式:flat 函数式 CRUD(参照 `app/crud/item_unit.py`),不用 service 层
- 前端模式:API 函数加入 `production.ts`,路由加入 `sqxp.ts`
- 所有查询带 `tenant_no` 过滤,删除为软删除
- Fresh Mint 配色 (`--primary #10b981`)
- 附件使用 MinIO,多附件逗号分隔 URL
- 编码字段需做唯一性校验(同 tenant_no + del_flag=0 下)
## 实施顺序
1. **第1批**(5 个简单模块,可并行):
- SQL DDL → Model → CRUD → API → 注册 → 前端 API → 路由 → 页面
2. **第2批**(2 个复杂模块,串行):
- 耗材包管理 → 工艺库管理
3. **菜单 SQL**:与第1批一起执行
@@ -0,0 +1,231 @@
# 科研捐赠管理模块设计文档
> 日期:2026-05-17
> 方案:C — 独立业务 + 共享基础数据
> 状态:已确认
## 背景与需求
客户现有「细胞存储管理」业务,现需新增「科研捐赠」业务。两者关系:
- **独立新建**:科研捐赠可以直接创建
- **双向转化**:细胞存储协议可「转科研」→ 科研捐赠;科研捐赠可「转存储」→ 细胞存储协议
- **核心区别**:
1. 科研捐赠不收费(无缴费相关流程)
2. 科研捐赠必须上传知情同意书附件
- **相同部分**:当事人信息、采集信息、样本信息、审核流程、状态生命周期
## 方案选型
| 方案 | 描述 | 结论 |
|------|------|------|
| A. 完全复制+裁剪 | Clone 细胞存储全部代码,删缴费/储存证书 | 代码重复严重 |
| B. 共享表+类型区分 | 同一套表,用 business_type 区分 | 耦合高,维护难 |
| **C. 独立业务+共享基础数据** | 独立表和 CRUD,复用基础数据和前端组件 | **已选择** |
选择理由:符合项目现有模式(每业务独立路由/CRUD),风险最低,独立演进。
---
## Section 1:数据库设计
### 新建表(6 张)
#### `biz_research_donation` — 主表
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | VARCHAR(64) UUID | 主键 |
| `donation_no` | VARCHAR(64) | 捐赠协议编号 |
| `status` | VARCHAR(20) | 状态:dsx/sxz/ygq/yzf/zckc |
| `donation_date` | DATE | 捐赠日期 |
| `source_type` | VARCHAR(20) | 来源类型:`direct`(独立新建)/ `from_storage`(从存储转来) |
| `source_protocol_id` | VARCHAR(64) | 来源协议 ID(from_storage 时填源细胞存储协议 ID) |
| `consent_form_urls` | TEXT | 知情同意书附件 URL(逗号分隔,必填) |
| `attachment_urls` | TEXT | 其他附件 URL(逗号分隔,可选) |
| `remark` | VARCHAR(500) | 备注 |
| `tenant_no` | VARCHAR(64) | 租户编号 |
| `order_num` | INT | 排序号 |
| `create_by` | VARCHAR(64) | 创建人 |
| `create_dept_id` | VARCHAR(64) | 创建部门 |
| `create_time` | DATETIME | 创建时间 |
| `update_by` | VARCHAR(64) | 更新人 |
| `update_time` | DATETIME | 更新时间 |
| `del_flag` | CHAR(1) DEFAULT '0' | 删除标记 |
#### `biz_research_donation_audit` — 审核记录
结构与 `biz_protocol_audit` 一致:审核状态(shtg/shth)+ 备注 + 审核人 + 审核时间。
#### `biz_research_donation_party` — 当事人信息
结构与 `biz_protocol_party_customer` 一致:母亲/父亲/新生儿信息。
#### `biz_research_donation_collection` — 采集信息
结构与 `biz_protocol_collection_info` 一致:区域/医院/采集人/时间。
#### `biz_research_donation_sample` — 样本信息
参照 `biz_protocol_sample`,**去掉**金额/费用相关字段,保留:样本类型、编码、数量、储存年限。
#### `biz_research_donation_sample_transport` — 样本运输信息
结构与 `biz_protocol_sample_transport` 一致。
### 不新建的表
| 细胞存储表 | 原因 |
|---|---|
| `biz_protocol_payment` | 科研捐赠不收费 |
| `biz_protocol_payment_detail` | 科研捐赠不收费 |
| `biz_protocol_storage_cert` | 科研捐赠无储存证书 |
### 复用的基础数据表(不新建)
- `biz_customer` — 客户主数据
- `biz_cell_sample_type` — 样本类型
- `biz_collection_region` / `biz_collection_hospital` — 采集区域/医院
- `biz_sample_code` — 样本编码池
- `biz_attachment` — 通用附件
### 状态生命周期
```
科研捐赠:dsx(待生效) → sxz(生效中) → ygq(已过期) / yzf(已作废) / zckc(转细胞存储)
细胞存储:dsx(待生效) → sxz(生效中) → ygq(已过期) / yzf(已作废) / zky(转科研)
```
### 双向转化机制
- **细胞存储 → 科研捐赠**:细胞存储协议状态变为 `zky`,新建科研捐赠记录(`source_type=from_storage`, `source_protocol_id=源协议ID`)
- **科研捐赠 → 细胞存储**:科研捐赠状态变为 `zckc`,新建细胞存储协议记录(缴费信息留空待补填),科研捐赠记录保留关联
---
## Section 2:后端架构
### 文件结构
```
backend/app/
├── api/
│ ├── research_donation.py # 新建:科研捐赠 API 路由
│ └── __init__.py # 注册新路由
├── crud/
│ └── research_donation.py # 新建:科研捐赠 CRUD/业务逻辑
├── models/
│ └── models.py # 新增 6 个 Model 类
├── schemas/
│ └── research_donation.py # 新建:Pydantic schemas
└── main.py # 注册新路由
```
### API 路由
前缀:`/web/admin/research-donations`,tag `"科研捐赠管理"`
| 端点 | 方法 | 说明 |
|---|---|---|
| `/` | GET | 列表查询(分页、关键字、状态筛选) |
| `/` | POST | 新建科研捐赠 |
| `/{id}` | GET | 详情(含所有子资源) |
| `/{id}` | PUT | 更新 |
| `/{id}` | DELETE | 删除(软删除) |
| `/batch-delete` | POST | 批量删除 |
| `/audit/list` | GET | 审核列表 |
| `/audit/submit` | POST | 提交审核(单个/批量) |
| `/status-change` | POST | 状态变更(生效/过期/作废/转存储) |
| `/convert-from-storage` | POST | 从细胞存储协议转化 |
| `/convert-to-storage` | POST | 转为细胞存储 |
### CRUD 层
参照 `collection_protocol.py` 模式,简化(无缴费逻辑):
- 子资源整体 upsert
- 审核逻辑(shtg/shth + remark)
- 状态机校验
- 转化逻辑(双向数据复制 + 关联)
- 租户隔离 + 数据权限过滤
### Schemas
- `ResearchDonationCreate` / `ResearchDonationUpdate`
- `ResearchDonationResponse` / `ResearchDonationListResponse`
- `ResearchDonationAuditSubmit`
- `ResearchDonationStatusChange`
- `ConvertFromStorageRequest` / `ConvertToStorageRequest`
### 代码复用策略
- **复用**:基础数据查询函数、附件处理工具、字段命名转换工具
- **不复用**:CRUD 代码(独立文件,避免耦合)
---
## Section 3:前端结构
### 路由注册(`sqxp.ts`)
```
科研捐赠管理 (id: 6000...)
├── /research-donation/list → 捐赠协议单管理
├── /research-donation/audit → 捐赠协议审核
```
### 页面与组件
```
web-admin/src/
├── views/research-donation/
│ ├── list/
│ │ ├── index.vue # 列表页
│ │ ├── DonationFormDialog.vue # 新增/编辑弹窗(5 Tab)
│ │ ├── DonationViewDialog.vue # 查看详情弹窗
│ │ ├── DonationAuditDialog.vue # 审核弹窗
│ │ └── DonationStatusChangeDialog.vue # 状态变更弹窗(含转存储)
│ └── audit/
│ └── index.vue # 审核管理页
├── api/
│ └── research-donation.ts # API 模块
└── components/ # 从细胞存储提取的公共组件
├── party-info/ # 当事人信息(Form + View)
├── collection-info/ # 采集信息(Form + View)
└── sample-info/ # 样本信息(Form + View,捐赠版去金额)
```
### 捐赠表单弹窗(5 个 Tab)
| Tab | 说明 | 来源 |
|---|---|---|
| 基本信息 | 协议编号、状态、日期、来源类型、备注 | 新写 |
| 当事人信息 | 母亲/父亲/新生儿 | 提取公共组件 `PartyInfoForm` |
| 采集信息 | 区域/医院/采集人/时间 | 提取公共组件 `CollectionInfoForm` |
| 样本信息 | 样本类型/编码/数量/储存年限(无金额) | 提取公共组件 `SampleInfoForm` |
| 附件 | 知情同意书(必传)+ 其他附件 | 新写 |
### 知情同意书上传
在「附件」Tab 中分两个区域:
1. **知情同意书**(必传)— 红色 `*` 标注,提交时校验至少 1 份
2. **其他附件** — 可选
### 细胞存储端最小改动
- `ProtocolStatusChangeDialog.vue` 增加 `zky`(转科研)操作
- 列表页状态筛选增加「转科研」选项
- 不改表结构,不改现有 CRUD 逻辑
---
## 实施顺序建议
1. SQL 建表脚本 → `docs/sql/`
2. 后端 Model + Schema
3. 后端 CRUD(子资源 upsert、审核、状态机)
4. 后端 API 路由 + 注册
5. 提取前端公共组件(当事人/采集/样本)
6. 前端页面(列表 → 表单弹窗 → 查看弹窗 → 审核弹窗 → 状态变更)
7. 双向转化功能(转科研 + 转存储)
8. 联调测试
@@ -0,0 +1,240 @@
# 体检机构项目套餐设计文档
> 日期:2026-05-18
> 状态:已确认
> 父模块:中试服务管理 §2.3.6 体检机构及项目价格
> 需求来源:`docs/中科兴皖需求V0.3.md` §2.3.6(增强)、探索结论 2026-05-18
---
## 1. 背景与目标
运营在维护各体检机构的**原子项目**后,常需将多项组合成固定搭配(如「基础套餐」「全面套餐」),供后续业务人员一键选用,减少逐条勾选与漏项。
**目标**
- 在机构维度维护「套餐」:勾选多个本机构项目,可调整套餐内各行费用。
- **套餐总价 = 所含项目费用之和**(不单独维护打包价,口径简单、易解释)。
- 业务落单(中试申请单体检预约等)时**展开为明细行**写入现有 `biz_pilot_checkup_reserve_item`,不引入新的结算实体。
**非目标(首期不做)**
- 跨机构共用套餐模板库
- 套餐固定优惠价(与明细之和不一致)
- 仅保存套餐头、不展开明细的预约模型
---
## 2. 已确认业务规则
| 规则 | 说明 |
|------|------|
| 套餐总价 | **始终**为明细 `project_fee` 之和;**不存**套餐头「一口价」字段 |
| 明细价格 | 从机构项目带入默认费用后,**可在套餐内单独修改**;修改仅影响该套餐行,**不回写**机构项目主数据 |
| 套餐归属 | 仅属于一个 `checkup_institution_id`,不跨机构 |
| 项目引用 | 明细行关联 `checkup_institution_project_id`;保存时校验项目属于同一机构且未删除 |
| 停用项目 | 套餐保存/启用时:若含 `status=0` 的项目,**禁止保存或禁止启用**(二选一实现,推荐禁止保存含停用项) |
| 历史业务单 | 下游预约/实际明细为快照(名称+费用),套餐后续调价**不影响**已生成单据 |
| 名称快照 | 明细行冗余 `project_name`(与 2.3.4 项目价格体系明细一致),避免主数据改名后历史套餐展示歧义 |
| **套餐编码** | **必填**;同一机构下未删除(`del_flag=0`)的套餐编码**唯一**(应用层校验) |
| **删除机构项目** | **允许软删除**,不因被套餐引用而拦截;**不级联**删除或修改套餐明细行 |
| 删除后展示 | 套餐明细仍按行内 `project_name` / `project_fee` 快照展示;源项目已删时 UI 标注「源项目已删除」,**不影响历史查看与合计** |
### 2.1 机构项目软删除与套餐的关系
```
运营删除机构项目(del_flag=1)
│
├─► 套餐明细行:保留,不自动移除
│ 展示:project_name(快照)+ 可选标签「源项目已删除」
│ 合计:仍计入该行 project_fee(套餐定义不变)
│
├─► 编辑套餐:不可再「添加」已删除/停用的源项目;已有行可删除或改价
│
└─► Phase 2 从套餐带入预约:仅展开源项目仍有效(del_flag=0 且 status=1)的行;
已删除源项目行跳过(快照仅用于套餐档案展示)
```
与中试单预约/实际明细一致:业务单落库为快照,**机构项目删除不影响已生成业务单展示**。
---
## 3. 数据模型
### 3.1 ER 关系
```
biz_checkup_institution
│
├──1:N── biz_checkup_institution_project (原子项目,已有)
│
└──1:N── biz_checkup_institution_package (套餐头,新增)
│
└──1:N── biz_checkup_institution_package_item (套餐明细,新增)
│
└──N:1── biz_checkup_institution_project
```
### 3.2 表:`biz_checkup_institution_package`(套餐头)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | VARCHAR(64) | 主键 UUID |
| `checkup_institution_id` | VARCHAR(64) | 所属体检机构 |
| `package_name` | VARCHAR(200) | 套餐名称(必填) |
| `package_code` | VARCHAR(64) | 套餐编码(**必填**,机构内 `del_flag=0` 唯一) |
| `status` | CHAR(1) | `1` 正常 / `0` 停用 |
| `tenant_no` … `del_flag` | — | 公共字段(见 database-conventions) |
**不建字段**:`package_fee` / `total_fee`(总价由明细汇总计算)。
### 3.3 表:`biz_checkup_institution_package_item`(套餐明细)
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | VARCHAR(64) | 主键 |
| `package_id` | VARCHAR(64) | 套餐头 ID |
| `checkup_institution_project_id` | VARCHAR(64) | 来源机构项目 ID |
| `project_name` | VARCHAR(200) | 项目名称快照(从项目带入,可展示用) |
| `project_fee` | DECIMAL(18,2) | **套餐内**项目费用(元),可编辑 |
| `tenant_no` … `del_flag` | — | 公共字段 |
**约束**
- 同一 `package_id` 下 `checkup_institution_project_id` 不重复。
- `project_fee` ≥ 0;允许为 NULL 时按 0 参与合计(建议表单必填 ≥0)。
### 3.4 API 派生字段
列表/详情响应增加只读字段(不落库):
```json
{
"totalFee": 1280.00,
"itemCount": 5
}
```
`totalFee = SUM(package_item.project_fee)`,后端计算返回。
---
## 4. 后端设计
沿用 `pilot_service.py` + `pilot_service.py` CRUD 分段模式。
### 4.1 端点
| 方法 | 路径 | 说明 |
|------|------|------|
| GET | `/checkup-institutions/{institution_id}/packages` | 机构下套餐列表(含 `totalFee`、`itemCount`) |
| POST | `/checkup-institutions/{institution_id}/packages` | 新增套餐(Body 含 `items[]`) |
| GET | `/checkup-institution-packages/{package_id}` | 套餐详情 + 明细 |
| PUT | `/checkup-institution-packages/{package_id}` | 更新套餐头 + 全量替换明细(与 2.3.4 主子表保存方式一致) |
| DELETE | `/checkup-institution-packages/{package_id}` | 软删除套餐及明细 |
权限、租户隔离、数据权限与现有中试服务接口一致。
### 4.2 保存逻辑(要点)
1. 校验 `package_code`、`package_name` 非空;`package_code` 在同机构未删除套餐中唯一。
2. 校验 `items` 非空(至少 1 项)。
3. **新增/全量替换明细**时:每项 `checkup_institution_project_id` 须属于当前机构、`del_flag=0`、`status=1`。
4. **更新且明细含历史行**(源项目已软删):允许保留该行(`project_name` / `project_fee` 以 Body 或库内快照为准),不允许新增指向已删项目的行。
5. 写入明细时:`project_name` 默认取机构项目当前名称;`project_fee` 取 Body 传入值。
6. 不写入、不校验套餐头总价字段。
### 4.3 机构项目删除(`soft_delete_checkup_institution_project`)
- **不检查**套餐引用,直接软删除机构项目。
- **不修改** `biz_checkup_institution_package_item` 任意字段。
- 列表/详情 join 源项目时附带 `sourceProjectDeleted: true`(源 `del_flag=1` 或不存在时),供前端展示标签。
### 4.4 Model
在 `backend/app/models/models.py` 追加:
- `BizCheckupInstitutionPackage`
- `BizCheckupInstitutionPackageItem`
---
## 5. 前端设计(web-admin)
### 5.1 入口
在 **体检机构及项目价格** 页(`checkup-institution/index.vue`)的「项目价格配置」弹窗内增加 Tab 或并列入口:
- **Tab 1:项目列表**(现有)
- **Tab 2:套餐管理**(新增)
也可采用「套餐配置」独立按钮 + 弹窗,与「项目价格配置」同级;以不挤占单弹窗高度为准。
### 5.2 套餐列表
列:序号、套餐名称、套餐编码、项目数、**合计费用(元)**、状态、备注、操作(编辑/删除)。
合计费用列展示 API 返回的 `totalFee`(只读)。
### 5.3 套餐新增/编辑弹窗
**头部**:套餐名称(必填)、**套餐编码(必填)**、状态(正常/停用)、备注。
**明细区**(可编辑表格,对齐 `project-price-system` 明细交互):
| 操作 | 说明 |
|------|------|
| 添加项目 | 从当前机构「正常」状态项目中多选/逐条添加 |
| 删除行 | 移除明细 |
| 项目名称 | 只读展示(快照);源项目已删时后缀 muted 文案「源项目已删除」 |
| 项目费用(元) | **可编辑** `el-input-number`,默认 = 机构项目当前 `projectFee` |
| 合计 | 弹窗底部展示「套餐合计:¥ xxx.xx」= 各行费用实时求和 |
保存时提交 `items: [{ checkupInstitutionProjectId, projectName, projectFee }, ...]`,不传 `totalFee`。
### 5.4 UI 规范
- 遵循 Fresh Mint 与 `dialog-form-layout`、`form-grid-2`、列表工具栏等既有约定。
- 树/路径无涉;明细勾选来自 flat 项目列表。
---
## 6. 与下游业务对接(Phase 2)
中试申请单体检模块(`biz_pilot_checkup` + `reserve_item`)实现时:
1. 选择机构后,提供 **「从套餐添加」**:选套餐 → 将有效 `package_item` 展开为预约明细行(`project_name` + `project_fee`)。
2. **有效行**:源机构项目 `del_flag=0` 且 `status=1`;源已删/停用的套餐行**跳过**,不写入预约明细(套餐档案仍完整展示)。
3. 展开后各行仍可在预约单内微调(与 V0.3 明细模型一致)。
4. 可选:明细行增加 `source_package_id` 字段便于统计(**非首期必需**)。
---
## 7. 实现分期
| 阶段 | 范围 |
|------|------|
| **Phase 1** | 表结构 + 后端 CRUD + web-admin 套餐维护(本文 §3–5) |
| **Phase 2** | 中试申请单体检预约「从套餐添加」展开明细 |
---
## 8. SQL 增量
见 `docs/sql/checkup_institution_package.sql`(`CREATE TABLE` 两张 + 索引)。
---
## 9. 与 V0.3 / 原设计的关系
- V0.3 §2.3.6 仅定义机构级原子项目;本设计为**已确认增强**,不改变下游明细表结构。
- 模式对齐 §2.3.4「细胞产品项目价格体系」主子表,降低实现与认知成本。
---
## 10. 产品确认记录(2026-05-18 补充)
| 议题 | 结论 |
|------|------|
| 套餐编码 | **必填**;机构内未删除套餐编码唯一 |
| 删除被套餐引用的机构项目 | **允许软删除**;套餐明细**不自动移除**;靠快照保证历史展示;业务带入时跳过无效源项目 |