Browse Source

Add 添加实验模块

Yue 1 month ago
parent
commit
cd7c92d379
59 changed files with 3540 additions and 933 deletions
  1. 132 0
      SERVER/ChickenFarmV3/.script/sql/new/init_y.sql
  2. 15 10
      SERVER/ChickenFarmV3/pom.xml
  3. 4 0
      SERVER/ChickenFarmV3/vb-admin/pom.xml
  4. 3 2
      SERVER/ChickenFarmV3/vb-admin/src/main/java/cn/vber/web/controller/IndexController.java
  5. 37 0
      SERVER/ChickenFarmV3/vb-common/vb-common-core/src/main/java/cn/vber/common/core/constant/ConfigKeyConstants.java
  6. 2 1
      SERVER/ChickenFarmV3/vb-common/vb-common-core/src/main/java/cn/vber/common/core/constant/SystemConstants.java
  7. 0 5
      SERVER/ChickenFarmV3/vb-common/vb-common-oss/src/main/java/cn/vber/common/oss/constant/OssConstant.java
  8. 1 0
      SERVER/ChickenFarmV3/vb-modules/pom.xml
  9. 7 6
      SERVER/ChickenFarmV3/vb-modules/vb-breeding/src/main/java/cn/vber/breeding/controller/ChickenController.java
  10. 3 2
      SERVER/ChickenFarmV3/vb-modules/vb-device/src/main/java/cn/vber/device/service/impl/DeviceOrderServiceImpl.java
  11. 117 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/pom.xml
  12. 133 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/controller/ExperimentController.java
  13. 134 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/controller/SampleController.java
  14. 77 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/Experiment.java
  15. 45 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/ExperimentSample.java
  16. 73 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/Sample.java
  17. 47 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/SampleFlow.java
  18. 26 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentAuditBo.java
  19. 63 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentBo.java
  20. 27 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentSubmitBo.java
  21. 74 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/SampleBo.java
  22. 48 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentSampleVo.java
  23. 97 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentVo.java
  24. 68 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/SampleFlowVo.java
  25. 107 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/SampleVo.java
  26. 28 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/enums/ExperimentStatusEnum.java
  27. 38 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/enums/SampleStatusEnum.java
  28. 17 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/ExperimentMapper.java
  29. 21 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/ExperimentSampleMapper.java
  30. 15 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/SampleFlowMapper.java
  31. 22 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/SampleMapper.java
  32. 74 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/IExperimentService.java
  33. 68 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/ISampleService.java
  34. 286 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/ExperimentServiceImpl.java
  35. 194 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/SampleServiceImpl.java
  36. 7 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/ExperimentMapper.xml
  37. 7 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/ExperimentSampleMapper.xml
  38. 7 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/SampleFlowMapper.xml
  39. 7 0
      SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/SampleMapper.xml
  40. 1 1
      SERVER/ChickenFarmV3/vb-modules/vb-generator/src/main/java/cn/vber/generator/util/GenUtils.java
  41. 5 1
      SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/controller/system/SysUserController.java
  42. 2 1
      SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/listener/SysUserImportListener.java
  43. 5 0
      SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/service/impl/SysUserServiceImpl.java
  44. 91 0
      UI/VB.VUE/src/api/experiment/_experiment.ts
  45. 93 0
      UI/VB.VUE/src/api/experiment/_sample.ts
  46. 14 0
      UI/VB.VUE/src/api/experiment/index.ts
  47. 3 0
      UI/VB.VUE/src/api/index.ts
  48. 149 0
      UI/VB.VUE/src/components/modal-select/SampleSelect.vue
  49. 5 1
      UI/VB.VUE/src/layouts/main/header/navbar/WorkOrderMenu.vue
  50. 11 2
      UI/VB.VUE/src/router/_staticRouter.ts
  51. 2 0
      UI/VB.VUE/src/views/common/modal/chickenModal.vue
  52. 1 1
      UI/VB.VUE/src/views/device/inspection/index.vue
  53. 339 0
      UI/VB.VUE/src/views/experiment/experiment/index.vue
  54. 580 0
      UI/VB.VUE/src/views/experiment/sample/_sample.vue
  55. 22 0
      UI/VB.VUE/src/views/experiment/sample/index.vue
  56. 86 0
      UI/VB.VUE/src/views/experiment/sample/sampleFlow.vue
  57. 0 296
      UI/VB.VUE/vite.config.ts.timestamp-1751524516784-ff27d13c9582a.mjs
  58. 0 302
      UI/VB.VUE/vite.config.ts.timestamp-1761050177294-53d1d8a12451c.mjs
  59. 0 302
      UI/VB.VUE/vite.config.ts.timestamp-1761120477090-c28d50e98d6ac.mjs

+ 132 - 0
SERVER/ChickenFarmV3/.script/sql/new/init_y.sql

@@ -347,6 +347,8 @@ INSERT INTO `sys_dict_type` VALUES (307, '000000', '点检签到类型', 'device
 INSERT INTO `sys_dict_data` VALUES (341, '000000', 0, '未签到', '0', 'device_inspection_type', '', 'danger', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '未签到');
 INSERT INTO `sys_dict_data` VALUES (342, '000000', 0, '签到', '1', 'device_inspection_type', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '签到');
 
+INSERT INTO `sys_menu` VALUES (20, '工单管理', 0, 21, 'workOrderMg', NULL, '', 1, 0, 'M', '1', '0', 'workOrder', 'suitcase-lg', '', '', 100, 1, '2025-10-15 12:00:00', 1, '2025-10-15 12:00:00', '设备管理');
+
 INSERT INTO `sys_menu` VALUES (21, '设备管理', 0, 21, 'deviceMg', NULL, '', 1, 0, 'M', '0', '0', '', 'suitcase-lg', '', '', 100, 1, '2025-10-15 12:00:00', 1, '2025-10-15 12:00:00', '设备管理');
 
 INSERT INTO `sys_menu` VALUES (351, '设备资产', 21, 1, 'device', 'device/device/index', '', 1, 0, 'C', '0', '0', 'device:device', '#', NULL, NULL, 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
@@ -382,3 +384,133 @@ INSERT INTO `sys_menu` VALUES (2052, '机修工维修明细表', 356, 2, 'device
 INSERT INTO `sys_menu` VALUES (2053, '点检签到漏点统计表', 356, 3, 'inspectionLose', 'device/report/inspection', '', 1, 0, 'C', '0', '0', 'device:report:inspection', '#', NULL, NULL, 100, 1, '2025-10-15 15:02:19', NULL, '2025-10-15 15:02:19', '');
 
 -- INSERT INTO `sys_role_menu` (`role_id`, `menu_id`) VALUES (2, );
+
+
+-- ----------------------------
+-- Table structure for i_sample
+-- ----------------------------
+DROP TABLE IF EXISTS `e_sample`;
+CREATE TABLE `e_sample` (
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '样本ID',
+    `sample_name` varchar(255) NOT NULL COMMENT '样本名称',
+    `batch_num` varchar(32) DEFAULT NULL COMMENT '取样的批次',
+    `chicken_id` bigint DEFAULT NULL COMMENT '取样的个体id',
+    `sample_time` datetime DEFAULT NULL COMMENT '取样时间',
+    `description` varchar(500) DEFAULT NULL COMMENT '样本描述',
+    `sample_type` tinyint NULL COMMENT '样品类型(1:血液 2:粪便)',
+    `sample_status` tinyint NULL DEFAULT '1' COMMENT '样品状态(1:已创建 2:已采样 3:实验中 4:已销毁)',
+    `create_org` bigint NOT NULL COMMENT '创建组织',
+    `create_by` bigint NOT NULL COMMENT '创建人',
+    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_by` bigint NOT NULL COMMENT '更新人',
+    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '删除标志(0:未删除, 1:已删除)',
+    PRIMARY KEY (`id`),
+    KEY `idx_sample_batch` (`batch_id`),
+    KEY `idx_sample_chicken_id` (`chicken_id`),
+    KEY `idx_sample_status` (`sample_status`),
+    KEY `idx_sample_create_by` (`create_by`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='样本表';
+-- ----------------------------
+-- Table structure for i_sample_flow
+-- ----------------------------
+DROP TABLE IF EXISTS `e_sample_flow`;
+CREATE TABLE `e_sample_flow` (
+     `id` bigint NOT NULL AUTO_INCREMENT COMMENT '流转记录ID',
+     `sample_id` bigint NOT NULL COMMENT '样品ID',
+     `handler` bigint NOT NULL COMMENT '经手人',
+     `handle_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '处理时间',
+     `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+     PRIMARY KEY (`id`),
+     KEY `idx_sample_flow_sample` (`sample_id`),
+     KEY `idx_sample_flow_handler` (`handler`),
+     KEY `idx_sample_flow_time` (`handle_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='样本流转表';
+
+-- ----------------------------
+-- Table structure for i_experiment
+-- ----------------------------
+DROP TABLE IF EXISTS `e_experiment`;
+CREATE TABLE `e_experiment` (
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '实验ID',
+    `experiment_name` varchar(255) NOT NULL COMMENT '实验名称',
+    `experiment_type` tinyint NOT NULL COMMENT '实验类型(1:平板凝集-沙门氏菌 2:ELISA-白血病P27抗原 3:ELISA-抗体 4:荧光定量PCR 5:HI-抗体)',
+    `experiment_manager` bigint DEFAULT NULL COMMENT '实验负责人',
+    `review_manager` bigint DEFAULT NULL COMMENT '审核负责人',
+    `raw_data_url` varchar(500) DEFAULT NULL COMMENT '实验原始数据URL(文件格式)',
+    `report_url` varchar(500) DEFAULT NULL COMMENT '实验报告URL(文件格式)',
+    `experiment_status` tinyint DEFAULT NULL DEFAULT '1' COMMENT '实验状态(1:未开始 2:进行中 3:审核中 4:发回报告 5:已结束)',
+    `create_org` bigint NOT NULL COMMENT '创建组织',
+    `create_by` bigint NOT NULL COMMENT '创建人',
+    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_by` bigint NOT NULL COMMENT '更新人',
+    `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '删除标志(0:未删除, 1:已删除)',
+    PRIMARY KEY (`id`),
+    KEY `idx_experiment_manager` (`experiment_manager`),
+    KEY `idx_review_manager` (`review_manager`),
+    KEY `idx_experiment_status` (`experiment_status`),
+    KEY `idx_experiment_create_by` (`create_by`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='实验管理表';
+
+-- ----------------------------
+-- Table structure for e_experiment_sample
+-- ----------------------------
+DROP TABLE IF EXISTS `e_experiment_sample`;
+CREATE TABLE `e_experiment_sample` (
+       `id` bigint NOT NULL AUTO_INCREMENT COMMENT '关联ID',
+       `sample_id` bigint NOT NULL COMMENT '样本ID',
+       `experiment_id` bigint NOT NULL COMMENT '实验ID',
+       `create_by` bigint NOT NULL COMMENT '创建人',
+       `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+       PRIMARY KEY (`id`),
+       UNIQUE KEY `uk_sample_experiment` (`sample_id`,`experiment_id`),
+       KEY `idx_sample_experiment_sample` (`sample_id`),
+       KEY `idx_sample_experiment_experiment` (`experiment_id`),
+       KEY `idx_sample_experiment_create_by` (`create_by`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='样本-实验关联表';
+
+INSERT INTO `sys_config` VALUES (12, '000000', '实验审核人员USER_ID', 'sys.experiment.review.userIds', '8,9', 'Y', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '实验审核人员ID');
+
+INSERT INTO `sys_dict_type` VALUES (311, '000000', '实验样品类型', 'experiment_sample_type', 100, 1, '2025-10-15 12:00:00',  NULL, NULL, '1:血液 2:粪便');
+INSERT INTO `sys_dict_data` VALUES (351, '000000', 0, '血液', '1', 'experiment_sample_type', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '血液');
+INSERT INTO `sys_dict_data` VALUES (352, '000000', 0, '粪便', '2', 'experiment_sample_type', '', 'warning', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '粪便');
+
+INSERT INTO `sys_dict_type` VALUES (312, '000000', '实验样品状态', 'experiment_sample_status', 100, 1, '2025-10-15 12:00:00',  NULL, NULL, '1:新建 2:已采样 3:实验中 4:已销毁');
+INSERT INTO `sys_dict_data` VALUES (355, '000000', 0, '已创建', '1', 'experiment_sample_status', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '新建');
+INSERT INTO `sys_dict_data` VALUES (356, '000000', 0, '已采样', '2', 'experiment_sample_status', '', 'success', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '已采样');
+INSERT INTO `sys_dict_data` VALUES (357, '000000', 0, '实验中', '3', 'experiment_sample_status', '', 'warning', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '实验中');
+INSERT INTO `sys_dict_data` VALUES (358, '000000', 0, '已销毁', '4', 'experiment_sample_status', '', 'danger', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '已销毁');
+
+INSERT INTO `sys_dict_type` VALUES (313, '000000', '实验类型', 'experiment_type', 100, 1, '2025-10-15 12:00:00',  NULL, NULL, '1:平板凝集-沙门氏菌 2:ELISA-白血病P27抗原 3:ELISA-抗体 4:荧光定量PCR 5:HI-抗体');
+INSERT INTO `sys_dict_data` VALUES (361, '000000', 0, '平板凝集-沙门氏菌', '1', 'experiment_type', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '平板凝集-沙门氏菌');
+INSERT INTO `sys_dict_data` VALUES (362, '000000', 0, 'ELISA-白血病P27抗原', '2', 'experiment_type', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, 'ELISA-白血病P27抗原');
+INSERT INTO `sys_dict_data` VALUES (363, '000000', 0, 'ELISA-抗体', '3', 'experiment_type', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, 'ELISA-抗体');
+INSERT INTO `sys_dict_data` VALUES (364, '000000', 0, '荧光定量PCR', '4', 'experiment_type', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '荧光定量PCR');
+INSERT INTO `sys_dict_data` VALUES (365, '000000', 0, 'HI-抗体', '5', 'experiment_type', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, 'HI-抗体');
+
+INSERT INTO `sys_dict_type` VALUES (314, '000000', '实验状态', 'experiment_status', 100, 1, '2025-10-15 12:00:00',  NULL, NULL, '1:新建 2:进行中 3:审核中 4:发回报告 5:已结束');
+INSERT INTO `sys_dict_data` VALUES (371, '000000', 0, '未开始', '1', 'experiment_status', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '新建');
+INSERT INTO `sys_dict_data` VALUES (372, '000000', 0, '进行中', '2', 'experiment_status', '', 'success', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '进行中');
+INSERT INTO `sys_dict_data` VALUES (373, '000000', 0, '审核中', '3', 'experiment_status', '', 'warning', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '审核中');
+INSERT INTO `sys_dict_data` VALUES (374, '000000', 0, '发回报告', '4', 'experiment_status', '', 'danger', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '发回报告');
+INSERT INTO `sys_dict_data` VALUES (375, '000000', 0, '已结束', '5', 'experiment_status', '', 'primary', 'N', 100, 1, '2025-10-15 12:00:00', NULL, NULL, '已结束');
+
+
+INSERT INTO `sys_menu` VALUES (22, '检验(实验)管理', 0, 22, 'experimentMg', NULL, '', 1, 0, 'M', '0', '0', '', 'suitcase-lg', '', '', 100, 1, '2025-10-15 12:00:00', 1, '2025-10-15 12:00:00', '设备管理');
+
+INSERT INTO `sys_menu` VALUES (361, '样品管理', 22, 1, 'sample', 'experiment/sample/index', '', 1, 0, 'C', '0', '0', 'experiment:sample', '#', NULL, NULL, 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2081, '查询样品', 361, 1, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:sample:query', 'eye', '', '', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2082, '新增样品', 361, 2, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:sample:add', 'plus-square', 'btn btn-light-primary', 'handleCreate', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2083, '修改样品', 361, 3, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:sample:edit', 'pencil-square', 'btn btn-light-success', 'handleUpdate@1', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2084, '删除样品', 361, 4, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:sample:remove', 'dash-square', 'btn btn-light-danger', 'handleDelete@0', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2085, '销毁样品', 361, 5, '#', NULL, '', 1, 0, 'F', '1', '0', 'experiment:sample:destroy', 'dash-square', 'btn btn-light-danger', 'handleDestroy@1', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+
+INSERT INTO `sys_menu` VALUES (362, '实验管理', 22, 2, 'experiment', 'experiment/experiment/index', '', 1, 0, 'C', '0', '0', 'experiment:experiment', '#', NULL, NULL, 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2091, '查询实验', 362, 1, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:experiment:query', 'eye', '', '', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2092, '新增实验', 362, 2, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:experiment:add', 'plus-square', 'btn btn-light-primary', 'handleCreate', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2093, '修改实验', 362, 3, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:experiment:edit', 'pencil-square', 'btn btn-light-success', 'handleUpdate@1', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (2094, '删除实验', 362, 4, '#', NULL, '', 1, 0, 'F', '0', '0', 'experiment:experiment:remove', 'dash-square', 'btn btn-light-danger', 'handleDelete@0', 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+INSERT INTO `sys_menu` VALUES (363, '实验审核', 22, 3, 'experimentAudit', 'experiment/experiment/audit', '', 1, 0, 'C', '0', '0', 'experiment:experiment:audit', 'patch-check-fill', NULL, NULL, 100, 1, '2025-10-15 15:02:26', NULL, '2025-10-15 15:02:26', '');
+
+

+ 15 - 10
SERVER/ChickenFarmV3/pom.xml

@@ -71,16 +71,16 @@
     </properties>
 
     <profiles>
-<!--        <profile>-->
-<!--            <id>local</id>-->
-<!--            <properties>-->
-<!--                &lt;!&ndash; 环境标识,需要与配置文件的名称相对应 &ndash;&gt;-->
-<!--                <profiles.active>local</profiles.active>-->
-<!--                <logging.level>info</logging.level>-->
-<!--                <monitor.username>vber</monitor.username>-->
-<!--                <monitor.password>vb123456</monitor.password>-->
-<!--            </properties>-->
-<!--        </profile>-->
+        <!--        <profile>-->
+        <!--            <id>local</id>-->
+        <!--            <properties>-->
+        <!--                &lt;!&ndash; 环境标识,需要与配置文件的名称相对应 &ndash;&gt;-->
+        <!--                <profiles.active>local</profiles.active>-->
+        <!--                <logging.level>info</logging.level>-->
+        <!--                <monitor.username>vber</monitor.username>-->
+        <!--                <monitor.password>vb123456</monitor.password>-->
+        <!--            </properties>-->
+        <!--        </profile>-->
         <profile>
             <id>dev</id>
             <properties>
@@ -391,6 +391,11 @@
                 <artifactId>vb-device</artifactId>
                 <version>${revision}</version>
             </dependency>
+            <dependency>
+                <groupId>com.vap</groupId>
+                <artifactId>vb-experiment</artifactId>
+                <version>${revision}</version>
+            </dependency>
         </dependencies>
     </dependencyManagement>
 

+ 4 - 0
SERVER/ChickenFarmV3/vb-admin/pom.xml

@@ -90,6 +90,10 @@
             <groupId>com.vap</groupId>
             <artifactId>vb-device</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-experiment</artifactId>
+        </dependency>
         <dependency>
             <groupId>de.codecentric</groupId>
             <artifactId>spring-boot-admin-starter-client</artifactId>

+ 3 - 2
SERVER/ChickenFarmV3/vb-admin/src/main/java/cn/vber/web/controller/IndexController.java

@@ -2,6 +2,7 @@ package cn.vber.web.controller;
 
 import cn.dev33.satoken.annotation.SaIgnore;
 import cn.vber.common.core.config.VbConfig;
+import cn.vber.common.core.constant.ConfigKeyConstants;
 import cn.vber.common.core.domain.R;
 import cn.vber.common.core.utils.StringUtils;
 import cn.vber.common.tenant.helper.TenantHelper;
@@ -45,9 +46,9 @@ public class IndexController {
         configVo.setCopyrightYear(vbConfig.getCopyrightYear());
         configVo.setTenantEnabled(TenantHelper.isEnable());
         configVo.setCaptchaEnabled(!"dev".equals(vbConfig.getMode()) && captchaProperties.getEnable());
-        String registerUser = sysConfigService.selectConfigByKey("sys.account.registerUser");
+        String registerUser = sysConfigService.selectConfigByKey(ConfigKeyConstants.SYS_ACCOUNT_REGISTER_USER);
         configVo.setRegisterEnabled(StringUtils.isNotEmpty(registerUser) ? Boolean.valueOf("true".equals(registerUser)) : vbConfig.getRegisterEnabled());
-        String previewListResource = sysConfigService.selectConfigByKey("sys.oss.previewListResource");
+        String previewListResource = sysConfigService.selectConfigByKey(ConfigKeyConstants.SYS_OSS_PREVIEW_LIST_RESOURCE);
         configVo.setOssPreviewEnabled(StringUtils.isNotEmpty(previewListResource) ? Boolean.valueOf("true".equals(previewListResource)) : vbConfig.getOssPreviewEnabled());
         configVo.setWorkflowEnabled(vbConfig.getWorkflowEnabled());
         return R.ok(configVo);

+ 37 - 0
SERVER/ChickenFarmV3/vb-common/vb-common-core/src/main/java/cn/vber/common/core/constant/ConfigKeyConstants.java

@@ -0,0 +1,37 @@
+package cn.vber.common.core.constant;
+
+public interface ConfigKeyConstants {
+    /**
+     * 用户初始密码
+     */
+    String SYS_USER_INIT_PASSWORD = "sys.user.initPassword";
+    /**
+     * 是否允许注册用户
+     */
+    String SYS_ACCOUNT_REGISTER_USER = "sys.account.registerUser";
+
+    /**
+     * OSS预览列表资源开关
+     */
+    String SYS_OSS_PREVIEW_LIST_RESOURCE = "sys.oss.previewListResource";
+
+    /**
+     * 维修人员USER_ID
+     */
+    String SYS_REPAIR_USER_IDS = "sys.repair.userIds";
+
+    /**
+     * 实验审核人员USER_ID
+     */
+    String SYS_EXPERIMENT_REVIEW_USER_IDS = "sys.experiment.review.userIds";
+
+    /**
+     * 可入孵出库的蛋类型
+     */
+    String SYS_BREEDING_EGG_STORE_INCUBATION_EGG_TYPE = "breeding:eggStore:incubation:eggType";
+    /**
+     * 仓库管理人员IDS
+     */
+    String SYS_ERP_STORE_USER_IDS = "erp:Store:userIds";
+
+}

+ 2 - 1
SERVER/ChickenFarmV3/vb-common/vb-common-core/src/main/java/cn/vber/common/core/constant/SystemConstants.java

@@ -164,8 +164,9 @@ public interface SystemConstants {
      */
     Long DEFAULT_ORG_ID = 100L;
 
-     /**
+    /**
      * 根菜单ID
      */
     Long MENU_ROOT_ID = 0L;
 }
+

+ 0 - 5
SERVER/ChickenFarmV3/vb-common/vb-common-oss/src/main/java/cn/vber/common/oss/constant/OssConstant.java

@@ -17,11 +17,6 @@ public interface OssConstant {
      */
     String DEFAULT_CONFIG_KEY = GlobalConstants.GLOBAL_REDIS_KEY + "sys_oss:default_config";
 
-    /**
-     * 预览列表资源开关Key
-     */
-    String PEREVIEW_LIST_RESOURCE_KEY = "sys.oss.previewListResource";
-
     /**
      * 系统数据ids
      */

+ 1 - 0
SERVER/ChickenFarmV3/vb-modules/pom.xml

@@ -16,6 +16,7 @@
         <module>vb-workflow</module>
         <module>vb-base</module>
         <module>vb-breeding</module>
+        <module>vb-experiment</module>
         <module>vb-device</module>
         <module>vb-erp</module>
     </modules>

+ 7 - 6
SERVER/ChickenFarmV3/vb-modules/vb-breeding/src/main/java/cn/vber/breeding/controller/ChickenController.java

@@ -65,7 +65,6 @@ public class ChickenController extends BaseController {
     /**
      * 查询鸡只个体信息列表
      */
-    @SaCheckPermission("breeding:chicken")
     @GetMapping("/list")
     public TableDataInfo<ChickenVo> list(ChickenBo bo, PageQuery pageQuery) {
         return chickenService.queryPageList(bo, pageQuery);
@@ -84,6 +83,7 @@ public class ChickenController extends BaseController {
         });
         ExcelUtil.exportExcel(listVo2, "鸡只个体信息", ChickenExportVo.class, response);
     }
+
     /**
      * 导入数据
      *
@@ -93,7 +93,7 @@ public class ChickenController extends BaseController {
     @Log(title = "用户管理", businessType = BusinessType.IMPORT)
     @SaCheckPermission("breeding:chicken:import")
     @PostMapping(value = "/importData", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
-    public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport,String batchNum) throws Exception {
+    public R<Void> importData(@RequestPart("file") MultipartFile file, boolean updateSupport, String batchNum) throws Exception {
         BatchVo batch = batchService.queryById(batchNum);
         if (batch == null) {
             return R.fail("批次不存在");
@@ -104,7 +104,7 @@ public class ChickenController extends BaseController {
         }
         ExcelResult<ChickenImportVo> result = ExcelUtil.importExcel(file.getInputStream()
                 , ChickenImportVo.class
-                , new ChickenImportListener(updateSupport,batch));
+                , new ChickenImportListener(updateSupport, batch));
         return R.ok(result.getAnalysis());
     }
 
@@ -165,17 +165,18 @@ public class ChickenController extends BaseController {
     @Log(title = "鸡只个体淘汰", businessType = BusinessType.UPDATE)
     @RepeatSubmit()
     @PostMapping("/cull")
-    public R<Void> cull(@RequestBody CullChickenBo bo){
+    public R<Void> cull(@RequestBody CullChickenBo bo) {
         return toAjax(chickenService.cull(bo));
     }
+
     @SaCheckPermission("breeding:chicken")
     @Log(title = "个体SOP绑定记录", businessType = BusinessType.UPDATE)
     @RepeatSubmit()
     @GetMapping("/querySop")
     public List<SopBindLogVo> queryChickenSopRecord(QuerySopLogBo bo) {
-        Long chickenId = bo.getChickenId() ;
+        Long chickenId = bo.getChickenId();
         String batchNum = bo.getBatchNum();
-        return sopService.queryChickenRecord(chickenId, batchNum,bo.getType());
+        return sopService.queryChickenRecord(chickenId, batchNum, bo.getType());
 //        String type = bo.getType();
 //        return switch (type) {
 //            case "feed" -> sopService.queryChickenRecord(chickenId, batchNum,feed);

+ 3 - 2
SERVER/ChickenFarmV3/vb-modules/vb-device/src/main/java/cn/vber/device/service/impl/DeviceOrderServiceImpl.java

@@ -1,6 +1,7 @@
 package cn.vber.device.service.impl;
 
 import cn.hutool.core.convert.Convert;
+import cn.vber.common.core.constant.ConfigKeyConstants;
 import cn.vber.common.core.domain.dto.UserSelectDTO;
 import cn.vber.common.core.enums.FormatsType;
 import cn.vber.common.core.exception.ServiceException;
@@ -123,7 +124,7 @@ public class DeviceOrderServiceImpl implements IDeviceOrderService {
         if (flag) {
             insertDeviceOrderFlow(add, WorkOrderFlowTypeEnum.CREATE, "创建" + WorkOrderTypeEnum.of(add.getOrderType()).getName() + "工单:工单编号:" + add.getOrderNo() + ",设备:" + bo.getDeviceName() + ",工单内容:" + bo.getContent());
         }
-        String userIds = SpringUtils.getBean(ConfigService.class).getConfigValue("sys.repair.userIds");
+        String userIds = SpringUtils.getBean(ConfigService.class).getConfigValue(ConfigKeyConstants.SYS_REPAIR_USER_IDS);
         if (StringUtils.isBlank(userIds)) {
             throw new ServiceException("工单接收人ID配置不能为空");
         }
@@ -261,7 +262,7 @@ public class DeviceOrderServiceImpl implements IDeviceOrderService {
 
     @Override
     public List<UserSelectDTO> getAssUsers() {
-        String userIds = SpringUtils.getBean(ConfigService.class).getConfigValue("sys.repair.userIds");
+        String userIds = SpringUtils.getBean(ConfigService.class).getConfigValue(ConfigKeyConstants.SYS_REPAIR_USER_IDS);
         if (StringUtils.isBlank(userIds)) {
             throw new ServiceException("工单接收人ID配置不能为空");
         }

+ 117 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/pom.xml

@@ -0,0 +1,117 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xmlns="http://maven.apache.org/POM/4.0.0"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>com.vap</groupId>
+        <artifactId>vb-modules</artifactId>
+        <version>${revision}</version>
+    </parent>
+
+    <artifactId>vb-experiment</artifactId>
+    <name>${project.artifactId}</name>
+    <description>实验模块</description>
+
+
+    <dependencies>
+        <!-- 通用工具-->
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-doc</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-mybatis</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-translation</artifactId>
+        </dependency>
+
+        <!-- OSS功能模块 -->
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-oss</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-log</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-mail</artifactId>
+        </dependency>
+
+        <!-- excel-->
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-excel</artifactId>
+        </dependency>
+
+        <!-- SMS功能模块 -->
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-sms</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-tenant</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-security</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-web</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-idempotent</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-encrypt</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-websocket</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-common-sse</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.vap</groupId>
+            <artifactId>vb-system</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-autoconfigure</artifactId>
+        </dependency>
+
+    </dependencies>
+
+
+</project>

+ 133 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/controller/ExperimentController.java

@@ -0,0 +1,133 @@
+package cn.vber.experiment.controller;
+
+import java.util.List;
+
+import cn.vber.common.core.domain.dto.UserSelectDTO;
+import cn.vber.experiment.domain.bo.ExperimentAuditBo;
+import cn.vber.experiment.domain.bo.ExperimentSubmitBo;
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import cn.vber.common.idempotent.annotation.RepeatSubmit;
+import cn.vber.common.log.annotation.Log;
+import cn.vber.common.web.core.BaseController;
+import cn.vber.common.mybatis.core.page.PageQuery;
+import cn.vber.common.core.domain.R;
+import cn.vber.common.core.validate.AddGroup;
+import cn.vber.common.core.validate.EditGroup;
+import cn.vber.common.log.enums.BusinessType;
+import cn.vber.common.excel.utils.ExcelUtil;
+import cn.vber.experiment.domain.vo.ExperimentVo;
+import cn.vber.experiment.domain.bo.ExperimentBo;
+import cn.vber.experiment.service.IExperimentService;
+import cn.vber.common.mybatis.core.page.TableDataInfo;
+
+/**
+ * 实验管理
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/experiment/experiment")
+public class ExperimentController extends BaseController {
+
+    private final IExperimentService experimentService;
+
+    /**
+     * 查询实验管理列表
+     */
+    @SaCheckPermission("experiment:experiment")
+    @GetMapping("/list")
+    public TableDataInfo<ExperimentVo> list(ExperimentBo bo, PageQuery pageQuery) {
+        return experimentService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出实验管理列表
+     */
+    @SaCheckPermission("experiment:experiment:export")
+    @Log(title = "实验管理", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(ExperimentBo bo, HttpServletResponse response) {
+        List<ExperimentVo> list = experimentService.queryList(bo);
+        ExcelUtil.exportExcel(list, "实验管理", ExperimentVo.class, response);
+    }
+
+    /**
+     * 获取实验管理详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("experiment:experiment:query")
+    @GetMapping("/{id}")
+    public R<ExperimentVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(experimentService.queryById(id));
+    }
+
+    /**
+     * 新增实验管理
+     */
+    @SaCheckPermission("experiment:experiment:add")
+    @Log(title = "实验管理", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody ExperimentBo bo) {
+        return toAjax(experimentService.insertByBo(bo));
+    }
+
+    /**
+     * 修改实验管理
+     */
+    @SaCheckPermission("experiment:experiment:edit")
+    @Log(title = "实验管理", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody ExperimentBo bo) {
+        return toAjax(experimentService.updateByBo(bo));
+    }
+
+    /**
+     * 删除实验管理
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("experiment:experiment:remove")
+    @Log(title = "实验管理", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
+        return toAjax(experimentService.deleteWithValidByIds(List.of(ids), true));
+    }
+
+    @SaCheckPermission("experiment:experiment:query")
+    @GetMapping("/getExperimentUsers")
+    public R<List<UserSelectDTO>> getExperimentUsers() {
+        return R.ok(experimentService.getExperimentUsers());
+    }
+
+    @SaCheckPermission("experiment:experiment:edit")
+    @Log(title = "实验管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/start/{id}")
+    public R<Void> start(@NotEmpty(message = "主键不能为空") @PathVariable Long id) {
+        return toAjax(experimentService.start(id));
+    }
+
+    @SaCheckPermission("experiment:experiment:edit")
+    @Log(title = "实验管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/submitExperiment")
+    public R<Void> submitExperiment(@RequestBody ExperimentSubmitBo bo) {
+        return toAjax(experimentService.submitExperiment(bo));
+    }
+
+    @SaCheckPermission("experiment:experiment:audit")
+    @Log(title = "实验管理", businessType = BusinessType.UPDATE)
+    @PostMapping("/auditExperiment")
+    public R<Void> auditExperiment(@RequestBody ExperimentAuditBo bo) {
+        return toAjax(experimentService.auditExperiment(bo));
+    }
+}

+ 134 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/controller/SampleController.java

@@ -0,0 +1,134 @@
+package cn.vber.experiment.controller;
+
+import java.util.List;
+
+import cn.vber.experiment.domain.vo.SampleFlowVo;
+import lombok.RequiredArgsConstructor;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.constraints.*;
+import cn.dev33.satoken.annotation.SaCheckPermission;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.validation.annotation.Validated;
+import cn.vber.common.idempotent.annotation.RepeatSubmit;
+import cn.vber.common.log.annotation.Log;
+import cn.vber.common.web.core.BaseController;
+import cn.vber.common.mybatis.core.page.PageQuery;
+import cn.vber.common.core.domain.R;
+import cn.vber.common.core.validate.AddGroup;
+import cn.vber.common.core.validate.EditGroup;
+import cn.vber.common.log.enums.BusinessType;
+import cn.vber.common.excel.utils.ExcelUtil;
+import cn.vber.experiment.domain.vo.SampleVo;
+import cn.vber.experiment.domain.bo.SampleBo;
+import cn.vber.experiment.service.ISampleService;
+import cn.vber.common.mybatis.core.page.TableDataInfo;
+
+/**
+ * 实验样本
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Validated
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/experiment/sample")
+public class SampleController extends BaseController {
+
+    private final ISampleService sampleService;
+
+    /**
+     * 查询实验样本列表
+     */
+    @SaCheckPermission("experiment:sample")
+    @GetMapping("/list")
+    public TableDataInfo<SampleVo> list(SampleBo bo, PageQuery pageQuery) {
+        return sampleService.queryPageList(bo, pageQuery);
+    }
+
+    /**
+     * 导出实验样本列表
+     */
+    @SaCheckPermission("experiment:sample:export")
+    @Log(title = "实验样本", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(SampleBo bo, HttpServletResponse response) {
+        List<SampleVo> list = sampleService.queryList(bo);
+        ExcelUtil.exportExcel(list, "实验样本", SampleVo.class, response);
+    }
+
+    /**
+     * 获取实验样本详细信息
+     *
+     * @param id 主键
+     */
+    @SaCheckPermission("experiment:sample:query")
+    @GetMapping("/{id}")
+    public R<SampleVo> getInfo(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(sampleService.queryById(id));
+    }
+
+    /**
+     * 新增实验样本
+     */
+    @SaCheckPermission("experiment:sample:add")
+    @Log(title = "实验样本", businessType = BusinessType.INSERT)
+    @RepeatSubmit()
+    @PostMapping()
+    public R<Void> add(@Validated(AddGroup.class) @RequestBody SampleBo bo) {
+        return toAjax(sampleService.insertByBo(bo));
+    }
+
+    /**
+     * 修改实验样本
+     */
+    @SaCheckPermission("experiment:sample:edit")
+    @Log(title = "实验样本", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @PutMapping()
+    public R<Void> edit(@Validated(EditGroup.class) @RequestBody SampleBo bo) {
+        return toAjax(sampleService.updateByBo(bo));
+    }
+
+    /**
+     * 删除实验样本
+     *
+     * @param ids 主键串
+     */
+    @SaCheckPermission("experiment:sample:remove")
+    @Log(title = "实验样本", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{ids}")
+    public R<Void> remove(@NotEmpty(message = "主键不能为空") @PathVariable Long[] ids) {
+        return toAjax(sampleService.deleteWithValidByIds(List.of(ids), true));
+    }
+
+    @SaCheckPermission("experiment:sample")
+    @Log(title = "实验样本", businessType = BusinessType.UPDATE)
+    @PostMapping("/sample")
+    public R<Void> sample(@RequestBody SampleBo bo) {
+        return toAjax(sampleService.sample(bo));
+    }
+
+    @SaCheckPermission("experiment:sample:destroy")
+    @Log(title = "实验样本", businessType = BusinessType.UPDATE)
+    @PostMapping("/destroy/{id}")
+    public R<Void> destroy(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return toAjax(sampleService.destroy(id));
+    }
+
+    @SaCheckPermission("experiment:sample:query")
+    @Log(title = "实验样本", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @GetMapping("/querySample/{id}")
+    public R<SampleVo> querySample(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(sampleService.querySample(id));
+    }
+
+    @SaCheckPermission("experiment:sample:query")
+    @Log(title = "实验样本", businessType = BusinessType.UPDATE)
+    @RepeatSubmit()
+    @GetMapping("/queryFlowLogs/{id}")
+    public R<List<SampleFlowVo>> queryFlowLogs(@NotNull(message = "主键不能为空") @PathVariable Long id) {
+        return R.ok(sampleService.queryFlowLogs(id));
+    }
+}

+ 77 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/Experiment.java

@@ -0,0 +1,77 @@
+package cn.vber.experiment.domain;
+
+import cn.vber.common.mybatis.core.domain.BaseEntity;
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+
+/**
+ * 实验管理对象 e_experiment
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("e_experiment")
+public class Experiment extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 实验ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 实验名称
+     */
+    private String experimentName;
+
+    /**
+     * 实验类型
+     */
+    private Integer experimentType;
+
+    /**
+     * 实验负责人
+     */
+    private Long experimentManager;
+
+    /**
+     * 审核负责人
+     */
+    private Long reviewManager;
+
+    /**
+     * 实验原始数据
+     */
+    private String rawDataUrl;
+
+    /**
+     * 实验报告
+     */
+    private String reportUrl;
+
+    /**
+     * 实验状态
+     */
+    private Integer experimentStatus;
+
+    /**
+     * 删除标志(0:未删除, 1:已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+
+}
+
+
+
+
+

+ 45 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/ExperimentSample.java

@@ -0,0 +1,45 @@
+package cn.vber.experiment.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.util.Date;
+
+/**
+ * 样本-实验关联对象 e_sample_experiment
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@Accessors(chain = true)
+@TableName("e_experiment_sample")
+public class ExperimentSample {
+
+
+    /**
+     * 关联ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 样本ID
+     */
+    private Long sampleId;
+
+    /**
+     * 实验ID
+     */
+    private Long experimentId;
+
+    private Long createBy;
+    private Date createTime;
+
+
+}
+
+
+

+ 73 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/Sample.java

@@ -0,0 +1,73 @@
+package cn.vber.experiment.domain;
+
+import cn.vber.common.mybatis.core.domain.BaseEntity;
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.io.Serial;
+import java.util.Date;
+
+/**
+ * 实验样本对象 e_sample
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@TableName("e_sample")
+public class Sample extends BaseEntity {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 样本ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 样本名称
+     */
+    private String sampleName;
+
+    /**
+     * 取样批次
+     */
+    private String batchNum;
+
+    /**
+     * 取样个体
+     */
+    private Long chickenId;
+
+    /**
+     * 取样时间
+     */
+    private Date sampleTime;
+
+    /**
+     * 样本描述
+     */
+    private String description;
+
+    /**
+     * 样品类型
+     */
+    private Integer sampleType;
+
+    /**
+     * 样品状态
+     */
+    private Integer sampleStatus;
+
+    /**
+     * 删除标志(0:未删除, 1:已删除)
+     */
+    @TableLogic
+    private String delFlag;
+
+
+}

+ 47 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/SampleFlow.java

@@ -0,0 +1,47 @@
+package cn.vber.experiment.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 样本流转对象 e_sample_flow
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@TableName("e_sample_flow")
+public class SampleFlow {
+
+
+    /**
+     * 流转记录ID
+     */
+    @TableId(value = "id")
+    private Long id;
+
+    /**
+     * 样品
+     */
+    private Long sampleId;
+
+    /**
+     * 经手人
+     */
+    private Long handler;
+
+    /**
+     * 处理时间
+     */
+    private Date handleTime;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+
+}

+ 26 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentAuditBo.java

@@ -0,0 +1,26 @@
+package cn.vber.experiment.domain.bo;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class ExperimentAuditBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotNull(message = "实验ID不能为空")
+    private Long id;
+    /**
+     * 是否通过
+     */
+    private boolean isPass;
+
+    /**
+     * 拒绝理由
+     */
+    private String reason;
+}

+ 63 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentBo.java

@@ -0,0 +1,63 @@
+package cn.vber.experiment.domain.bo;
+
+import cn.vber.experiment.domain.Experiment;
+import cn.vber.common.mybatis.core.domain.BaseEntity;
+import cn.vber.common.core.validate.AddGroup;
+import cn.vber.common.core.validate.EditGroup;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import jakarta.validation.constraints.*;
+
+import java.util.List;
+
+/**
+ * 实验管理业务对象 e_experiment
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = Experiment.class, reverseConvertGenerate = false)
+
+public class ExperimentBo extends BaseEntity {
+
+    /**
+     * 实验ID
+     */
+    @NotNull(message = "实验ID不能为空", groups = {EditGroup.class})
+    private Long id;
+
+    /**
+     * 实验名称
+     */
+    @NotBlank(message = "实验名称不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String experimentName;
+
+    /**
+     * 实验类型
+     */
+    @NotNull(message = "实验类型不能为空", groups = {AddGroup.class, EditGroup.class})
+    private Integer experimentType;
+
+    /**
+     * 实验负责人
+     */
+    private Long experimentManager;
+
+    /**
+     * 审核负责人
+     */
+    private Long reviewManager;
+
+    private List<Long> sampleIds;
+
+
+    /**
+     * 实验状态
+     */
+    private Integer experimentStatus;
+
+
+}

+ 27 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/ExperimentSubmitBo.java

@@ -0,0 +1,27 @@
+package cn.vber.experiment.domain.bo;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+@Data
+public class ExperimentSubmitBo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    @NotNull(message = "实验ID不能为空")
+    private Long id;
+    /**
+     * 实验原始数据
+     */
+    private String rawDataUrl;
+
+    /**
+     * 实验报告
+     */
+    private String reportUrl;
+}
+

+ 74 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/bo/SampleBo.java

@@ -0,0 +1,74 @@
+package cn.vber.experiment.domain.bo;
+
+import cn.vber.experiment.domain.Sample;
+import cn.vber.common.mybatis.core.domain.BaseEntity;
+import cn.vber.common.core.validate.AddGroup;
+import cn.vber.common.core.validate.EditGroup;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import jakarta.validation.constraints.*;
+
+import java.util.Date;
+
+/**
+ * 实验样本业务对象 e_sample
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@AutoMapper(target = Sample.class, reverseConvertGenerate = false)
+
+public class SampleBo extends BaseEntity {
+
+    /**
+     * 样本ID
+     */
+    @NotNull(message = "样本ID不能为空", groups = {EditGroup.class})
+    private Long id;
+
+    /**
+     * 样本名称
+     */
+    @NotBlank(message = "样本名称不能为空", groups = {AddGroup.class, EditGroup.class})
+    private String sampleName;
+
+    /**
+     * 取样批次
+     */
+    private String batchNum;
+
+    /**
+     * 取样个体
+     */
+    private Long chickenId;
+
+
+    /**
+     * 取样时间
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date sampleTime;
+
+
+    /**
+     * 样本描述
+     */
+    private String description;
+
+    /**
+     * 样品类型
+     */
+    private Integer sampleType;
+
+    /**
+     * 样品状态
+     */
+    private Integer sampleStatus;
+
+    private Long createBy;
+
+}

+ 48 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentSampleVo.java

@@ -0,0 +1,48 @@
+package cn.vber.experiment.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.vber.experiment.domain.ExperimentSample;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 样本-实验关联视图对象 e_sample_experiment
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = ExperimentSample.class)
+
+public class ExperimentSampleVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 关联ID
+     */
+    @ExcelProperty(value = "关联ID")
+    private Long id;
+
+    /**
+     * 样本ID
+     */
+    @ExcelProperty(value = "样本ID")
+    private Long sampleId;
+
+    /**
+     * 实验ID
+     */
+    @ExcelProperty(value = "实验ID")
+    private Long experimentId;
+
+
+}
+
+

+ 97 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/ExperimentVo.java

@@ -0,0 +1,97 @@
+package cn.vber.experiment.domain.vo;
+
+import cn.vber.common.translation.annotation.Translation;
+import cn.vber.common.translation.constant.TransConstant;
+import cn.vber.experiment.domain.Experiment;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.vber.common.excel.annotation.ExcelDictFormat;
+import cn.vber.common.excel.convert.ExcelDictConvert;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+import java.util.List;
+
+
+/**
+ * 实验管理视图对象 e_experiment
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = Experiment.class)
+
+public class ExperimentVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 实验ID
+     */
+    @ExcelProperty(value = "实验ID")
+    private Long id;
+
+    /**
+     * 实验名称
+     */
+    @ExcelProperty(value = "实验名称")
+    private String experimentName;
+
+    /**
+     * 实验类型
+     */
+    @ExcelProperty(value = "实验类型", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(dictType = "experiment_type")
+    private Integer experimentType;
+
+    /**
+     * 实验负责人
+     */
+    @ExcelProperty(value = "实验负责人ID")
+    private Long experimentManager;
+
+    @ExcelProperty(value = "实验负责人")
+    @Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "experimentManager")
+    private String experimentManagerName;
+
+    /**
+     * 审核负责人
+     */
+    @ExcelProperty(value = "审核负责人ID")
+    private Long reviewManager;
+
+    @ExcelProperty(value = "审核负责人")
+    @Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "reviewManager")
+    private String reviewManagerName;
+
+    /**
+     * 实验原始数据
+     */
+    @ExcelProperty(value = "实验原始数据")
+    private String rawDataUrl;
+
+    /**
+     * 实验报告
+     */
+    @ExcelProperty(value = "实验报告", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(dictType = "experiment_status")
+    private String reportUrl;
+
+    /**
+     * 实验状态
+     */
+    @ExcelProperty(value = "实验状态")
+    private Integer experimentStatus;
+
+    /**
+     * 实验样本列表
+     */
+    private List<ExperimentSampleVo> sampleList;
+
+}

+ 68 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/SampleFlowVo.java

@@ -0,0 +1,68 @@
+package cn.vber.experiment.domain.vo;
+
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.vber.common.translation.annotation.Translation;
+import cn.vber.common.translation.constant.TransConstant;
+import cn.vber.experiment.domain.SampleFlow;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 样本流转视图对象 e_sample_flow
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = SampleFlow.class)
+
+public class SampleFlowVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 流转记录ID
+     */
+    @ExcelProperty(value = "流转记录ID")
+    private Long id;
+
+    /**
+     * 样品
+     */
+    @ExcelProperty(value = "样品")
+    private Long sampleId;
+
+    /**
+     * 经手人
+     */
+    @ExcelProperty(value = "经手人")
+    private Long handler;
+
+    /**
+     * 经手人姓名
+     */
+    @ExcelProperty(value = "经手人姓名")
+    @Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "handler")
+    private String handlerName;
+
+    /**
+     * 处理时间
+     */
+    @ExcelProperty(value = "处理时间")
+    private Date handleTime;
+
+    /**
+     * 备注
+     */
+    @ExcelProperty(value = "备注")
+    private String remark;
+
+
+}

+ 107 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/domain/vo/SampleVo.java

@@ -0,0 +1,107 @@
+package cn.vber.experiment.domain.vo;
+
+import cn.vber.common.translation.annotation.Translation;
+import cn.vber.common.translation.constant.TransConstant;
+import cn.vber.experiment.domain.Sample;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import cn.vber.common.excel.annotation.ExcelDictFormat;
+import cn.vber.common.excel.convert.ExcelDictConvert;
+import io.github.linpeilie.annotations.AutoMapper;
+import lombok.Data;
+
+import java.io.Serial;
+import java.io.Serializable;
+import java.util.Date;
+
+
+/**
+ * 实验样本视图对象 e_sample
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Data
+@ExcelIgnoreUnannotated
+@AutoMapper(target = Sample.class)
+
+public class SampleVo implements Serializable {
+
+    @Serial
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 样本ID
+     */
+    @ExcelProperty(value = "样本ID")
+    private Long id;
+
+    /**
+     * 样本名称
+     */
+    @ExcelProperty(value = "样本名称")
+    private String sampleName;
+
+    /**
+     * 取样批次
+     */
+    @ExcelProperty(value = "取样批次")
+    private String batchNum;
+
+    /**
+     * 取样个体
+     */
+    @ExcelProperty(value = "取样个体")
+    private Long chickenId;
+
+    /**
+     * 翅号Num(关联f_wing_tag)
+     */
+    @ExcelProperty(value = "取样翅号")
+    @Translation(type = TransConstant.CHICKEN_ID_TO_OTHER_INFO, mapper = "chickenId", other = "wing_num")
+    private String wingTagNum;
+
+    /**
+     * 取样时间
+     */
+    @ExcelProperty(value = "取样时间")
+    private Date sampleTime;
+
+    /**
+     * 样本描述
+     */
+    @ExcelProperty(value = "样本描述")
+    private String description;
+
+    /**
+     * 样品类型
+     */
+    @ExcelProperty(value = "样品类型", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(dictType = "experiment_sample_type")
+    private Integer sampleType;
+
+    /**
+     * 样品状态
+     */
+    @ExcelProperty(value = "样品状态", converter = ExcelDictConvert.class)
+    @ExcelDictFormat(dictType = "experiment_sample_status")
+    private Integer sampleStatus;
+
+
+    /**
+     * 创建人 ID
+     */
+    @ExcelProperty(value = "创建人ID")
+    private Long createBy;
+    /**
+     * 创建人名称
+     */
+    @ExcelProperty(value = "创建人名称")
+    @Translation(type = TransConstant.USER_ID_TO_NICKNAME, mapper = "createBy")
+    private String createByName;
+}
+
+
+
+
+

+ 28 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/enums/ExperimentStatusEnum.java

@@ -0,0 +1,28 @@
+package cn.vber.experiment.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum ExperimentStatusEnum {
+    // 1:新建 2:进行中 3:审核中 4:发回报告 5:已结束
+
+    NEW(1),
+    IN_PROGRESS(2),
+    AUDITING(3),
+    RETURN_REPORT(4),
+    FINISHED(5);
+    private final Integer status;
+
+    ExperimentStatusEnum(Integer status) {
+        this.status = status;
+    }
+
+    public static ExperimentStatusEnum getByStatus(Integer status) {
+        for (ExperimentStatusEnum value : values()) {
+            if (value.status.equals(status)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}

+ 38 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/enums/SampleStatusEnum.java

@@ -0,0 +1,38 @@
+package cn.vber.experiment.enums;
+
+import lombok.Getter;
+
+@Getter
+public enum SampleStatusEnum {
+    /**
+     * 已创建
+     */
+    NEW(1),
+    /**
+     * 已采样
+     */
+    SAMPLED(2),
+    /**
+     * 实验中
+     */
+    EXPERIMENTING(3),
+    /**
+     * 已销毁
+     */
+    DESTROYED(4);
+    private final Integer status;
+
+    SampleStatusEnum(Integer code) {
+        this.status = code;
+    }
+
+    public static SampleStatusEnum getByStatus(Integer status) {
+        for (SampleStatusEnum value : values()) {
+            if (value.status.equals(status)) {
+                return value;
+            }
+        }
+        return null;
+    }
+}
+

+ 17 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/ExperimentMapper.java

@@ -0,0 +1,17 @@
+package cn.vber.experiment.mapper;
+
+import cn.vber.common.mybatis.core.mapper.BaseMapperPlus;
+import cn.vber.experiment.domain.Experiment;
+import cn.vber.experiment.domain.vo.ExperimentVo;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 实验管理Mapper接口
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Repository
+public interface ExperimentMapper extends BaseMapperPlus<Experiment, ExperimentVo> {
+
+}

+ 21 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/ExperimentSampleMapper.java

@@ -0,0 +1,21 @@
+package cn.vber.experiment.mapper;
+
+import cn.vber.common.mybatis.core.mapper.BaseMapperPlus;
+import cn.vber.experiment.domain.ExperimentSample;
+import cn.vber.experiment.domain.SampleFlow;
+import cn.vber.experiment.domain.vo.ExperimentSampleVo;
+import cn.vber.experiment.domain.vo.SampleFlowVo;
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 实验样本Mapper接口
+ *
+ * @author Yue
+ */
+@InterceptorIgnore(dataPermission = "true", tenantLine = "true")
+@Repository
+public interface ExperimentSampleMapper extends BaseMapperPlus<ExperimentSample, ExperimentSampleVo> {
+}
+
+

+ 15 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/SampleFlowMapper.java

@@ -0,0 +1,15 @@
+package cn.vber.experiment.mapper;
+
+import cn.vber.common.mybatis.core.mapper.BaseMapperPlus;
+import cn.vber.experiment.domain.SampleFlow;
+import cn.vber.experiment.domain.vo.SampleFlowVo;
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import org.springframework.stereotype.Repository; /**
+ * 实验样本流转Mapper接口
+ *
+ * @author Yue
+ */
+@InterceptorIgnore(dataPermission = "true", tenantLine = "true")
+@Repository
+public interface SampleFlowMapper extends BaseMapperPlus<SampleFlow, SampleFlowVo> {
+}

+ 22 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/mapper/SampleMapper.java

@@ -0,0 +1,22 @@
+package cn.vber.experiment.mapper;
+
+import cn.vber.common.mybatis.core.mapper.BaseMapperPlus;
+import cn.vber.experiment.domain.ExperimentSample;
+import cn.vber.experiment.domain.Sample;
+import cn.vber.experiment.domain.vo.ExperimentSampleVo;
+import cn.vber.experiment.domain.vo.SampleVo;
+import cn.vber.system.domain.SysNoticeStatus;
+import com.baomidou.mybatisplus.annotation.InterceptorIgnore;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 实验样本Mapper接口
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@Repository
+public interface SampleMapper extends BaseMapperPlus<Sample, SampleVo> {
+
+}
+

+ 74 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/IExperimentService.java

@@ -0,0 +1,74 @@
+package cn.vber.experiment.service;
+
+import cn.vber.common.core.domain.dto.UserSelectDTO;
+import cn.vber.experiment.domain.Experiment;
+import cn.vber.experiment.domain.bo.ExperimentAuditBo;
+import cn.vber.experiment.domain.bo.ExperimentSubmitBo;
+import cn.vber.experiment.domain.vo.ExperimentVo;
+import cn.vber.experiment.domain.bo.ExperimentBo;
+import cn.vber.common.mybatis.core.page.TableDataInfo;
+import cn.vber.common.mybatis.core.page.PageQuery;
+import jakarta.validation.constraints.NotEmpty;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 实验管理Service接口
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+public interface IExperimentService {
+
+    /**
+     * 查询实验管理
+     */
+    ExperimentVo queryById(Long id);
+
+    /**
+     * 查询实验管理列表
+     */
+    TableDataInfo<ExperimentVo> queryPageList(ExperimentBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询实验管理列表
+     */
+    List<ExperimentVo> queryList(ExperimentBo bo);
+
+    /**
+     * 新增实验管理
+     */
+    Boolean insertByBo(ExperimentBo bo);
+
+    /**
+     * 修改实验管理
+     */
+    Boolean updateByBo(ExperimentBo bo);
+
+    /**
+     * 校验并批量删除实验管理信息
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 查询实验管理用户选择
+     */
+    List<UserSelectDTO> getExperimentUsers();
+
+    /**
+     * 启动实验
+     */
+    int start(Long id);
+
+    /**
+     * 提交实验
+     */
+    int submitExperiment(ExperimentSubmitBo bo);
+
+    /**
+     * 审核实验
+     */
+    int auditExperiment(ExperimentAuditBo bo);
+
+}

+ 68 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/ISampleService.java

@@ -0,0 +1,68 @@
+package cn.vber.experiment.service;
+
+import cn.vber.experiment.domain.Sample;
+import cn.vber.experiment.domain.vo.SampleFlowVo;
+import cn.vber.experiment.domain.vo.SampleVo;
+import cn.vber.experiment.domain.bo.SampleBo;
+import cn.vber.common.mybatis.core.page.TableDataInfo;
+import cn.vber.common.mybatis.core.page.PageQuery;
+import jakarta.validation.constraints.NotEmpty;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * 实验样本Service接口
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+public interface ISampleService {
+
+    /**
+     * 查询实验样本
+     */
+    SampleVo queryById(Long id);
+
+    /**
+     * 查询实验样本列表
+     */
+    TableDataInfo<SampleVo> queryPageList(SampleBo bo, PageQuery pageQuery);
+
+    /**
+     * 查询实验样本列表
+     */
+    List<SampleVo> queryList(SampleBo bo);
+
+    /**
+     * 新增实验样本
+     */
+    Boolean insertByBo(SampleBo bo);
+
+    /**
+     * 修改实验样本
+     */
+    Boolean updateByBo(SampleBo bo);
+
+    /**
+     * 校验并批量删除实验样本信息
+     */
+    Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
+
+    /**
+     * 取样
+     */
+    int sample(SampleBo bo);
+
+    /**
+     * 销毁
+     */
+    int destroy(Long id);
+
+    List<SampleFlowVo> queryFlowLogs(Long id);
+
+    /**
+     * 查询实验样本详情
+     */
+    SampleVo querySample(Long id);
+}

+ 286 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/ExperimentServiceImpl.java

@@ -0,0 +1,286 @@
+package cn.vber.experiment.service.impl;
+
+import cn.hutool.core.convert.Convert;
+import cn.vber.common.core.constant.ConfigKeyConstants;
+import cn.vber.common.core.domain.dto.UserSelectDTO;
+import cn.vber.common.core.exception.ServiceException;
+import cn.vber.common.core.service.ConfigService;
+import cn.vber.common.core.service.UserService;
+import cn.vber.common.core.utils.DateUtils;
+import cn.vber.common.core.utils.MapstructUtils;
+import cn.vber.common.core.utils.SpringUtils;
+import cn.vber.common.core.utils.StringUtils;
+import cn.vber.common.mybatis.core.page.TableDataInfo;
+import cn.vber.common.mybatis.core.page.PageQuery;
+import cn.vber.common.notice.core.NoticePusher;
+import cn.vber.common.notice.dto.NoticeMessageDetailDto;
+import cn.vber.common.notice.dto.NoticeMessageDto;
+import cn.vber.common.satoken.utils.LoginHelper;
+import cn.vber.experiment.domain.ExperimentSample;
+import cn.vber.experiment.domain.Sample;
+import cn.vber.experiment.domain.bo.ExperimentAuditBo;
+import cn.vber.experiment.domain.bo.ExperimentSubmitBo;
+import cn.vber.experiment.domain.vo.ExperimentSampleVo;
+import cn.vber.experiment.enums.ExperimentStatusEnum;
+import cn.vber.experiment.enums.SampleStatusEnum;
+import cn.vber.experiment.mapper.ExperimentSampleMapper;
+import cn.vber.experiment.mapper.SampleMapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import cn.vber.experiment.domain.bo.ExperimentBo;
+import cn.vber.experiment.domain.vo.ExperimentVo;
+import cn.vber.experiment.domain.Experiment;
+import cn.vber.experiment.mapper.ExperimentMapper;
+import cn.vber.experiment.service.IExperimentService;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+
+/**
+ * 实验管理Service业务层处理
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@RequiredArgsConstructor
+@Service
+public class ExperimentServiceImpl implements IExperimentService {
+
+    private final ExperimentMapper baseMapper;
+    private final SampleMapper sampleMapper;
+    private final ExperimentSampleMapper experimentSampleMapper;
+
+    /**
+     * 查询实验管理
+     */
+    @Override
+    public ExperimentVo queryById(Long id) {
+        List<ExperimentSampleVo> sampleList = experimentSampleMapper.selectVoList(new LambdaQueryWrapper<ExperimentSample>().eq(ExperimentSample::getExperimentId, id));
+
+        ExperimentVo experimentVo = baseMapper.selectVoById(id);
+        experimentVo.setSampleList(sampleList);
+        return experimentVo;
+    }
+
+    /**
+     * 查询实验管理列表
+     */
+    @Override
+    public TableDataInfo<ExperimentVo> queryPageList(ExperimentBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<Experiment> lqw = buildQueryWrapper(bo);
+        Page<ExperimentVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询实验管理列表
+     */
+    @Override
+    public List<ExperimentVo> queryList(ExperimentBo bo) {
+        LambdaQueryWrapper<Experiment> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<Experiment> buildQueryWrapper(ExperimentBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<Experiment> lqw = Wrappers.lambdaQuery();
+        lqw.like(StringUtils.isNotBlank(bo.getExperimentName()), Experiment::getExperimentName, bo.getExperimentName());
+        lqw.eq(bo.getExperimentType() != null, Experiment::getExperimentType, bo.getExperimentType());
+        lqw.eq(bo.getExperimentManager() != null, Experiment::getExperimentManager, bo.getExperimentManager());
+        lqw.eq(bo.getReviewManager() != null, Experiment::getReviewManager, bo.getReviewManager());
+        lqw.eq(bo.getExperimentStatus() != null, Experiment::getExperimentStatus, bo.getExperimentStatus());
+        return lqw;
+    }
+
+    /**
+     * 新增实验管理
+     */
+    @Override
+    @Transactional
+    public Boolean insertByBo(ExperimentBo bo) {
+        Experiment add = MapstructUtils.convert(bo, Experiment.class);
+        validEntityBeforeSave(add);
+
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+            insertExperimentSample(bo, add);
+        }
+        return flag;
+    }
+
+
+    /**
+     * 修改实验管理
+     */
+    @Override
+    @Transactional
+    public Boolean updateByBo(ExperimentBo bo) {
+        Experiment update = MapstructUtils.convert(bo, Experiment.class);
+        validEntityBeforeSave(update);
+        boolean flag = baseMapper.updateById(update) > 0;
+        if (flag) {
+            experimentSampleMapper.delete(new LambdaUpdateWrapper<ExperimentSample>().eq(ExperimentSample::getExperimentId, update.getId()));
+            insertExperimentSample(bo, update);
+        }
+        return flag;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(Experiment entity) {
+        entity.setExperimentStatus(ExperimentStatusEnum.NEW.getStatus());
+    }
+
+    /**
+     * 批量插入实验样本关联
+     */
+    private void insertExperimentSample(ExperimentBo bo, Experiment add) {
+        if (bo.getSampleIds() != null && !bo.getSampleIds().isEmpty()) {
+            List<ExperimentSample> sampleList = bo.getSampleIds().stream().map(id ->
+                    new ExperimentSample()
+                            .setExperimentId(add.getId())
+                            .setSampleId(id)
+                            .setCreateBy(LoginHelper.getUserId())
+                            .setCreateTime(DateUtils.getNowDate())
+            ).toList();
+            experimentSampleMapper.insertBatch(sampleList);
+        }
+    }
+
+    /**
+     * 批量删除实验管理
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+
+    /**
+     * 获取实验审核人
+     *
+     * @return 实验审核人列表
+     */
+    @Override
+    public List<UserSelectDTO> getExperimentUsers() {
+        String userIds = SpringUtils.getBean(ConfigService.class).getConfigValue(ConfigKeyConstants.SYS_EXPERIMENT_REVIEW_USER_IDS);
+        if (StringUtils.isBlank(userIds)) {
+            throw new ServiceException("实验审核人ID配置不能为空");
+        }
+        List<UserSelectDTO> users = new ArrayList<>();
+        for (Long id : StringUtils.splitTo(userIds, Convert::toLong)) {
+            UserSelectDTO user = new UserSelectDTO();
+            user.setUserId(id);
+            user.setNickname(SpringUtils.getBean(UserService.class).selectNicknameById(id));
+            user.setUsername(SpringUtils.getBean(UserService.class).selectUserNameById(id));
+            users.add(user);
+        }
+        return users;
+    }
+
+    @Override
+    @Transactional
+    public int start(Long id) {
+        Experiment experiment = baseMapper.selectById(id);
+        if (experiment == null) {
+            throw new ServiceException("实验不存在");
+        }
+        if (!Objects.equals(experiment.getExperimentStatus(), ExperimentStatusEnum.NEW.getStatus())) {
+            throw new ServiceException("实验已开始");
+        }
+        List<ExperimentSample> eSampleList = experimentSampleMapper.selectList(
+                new LambdaQueryWrapper<ExperimentSample>().eq(ExperimentSample::getExperimentId, id));
+        if (eSampleList == null || eSampleList.isEmpty()) {
+            throw new ServiceException("实验样本不能为空");
+        }
+//        List<Long> ids = eSampleList.stream().map(ExperimentSample::getSampleId).toList();
+//        List<Sample> sampleList = sampleMapper.selectList(new LambdaQueryWrapper<Sample>()
+//                .in(
+//                Sample::getId, ids).ne(Sample::getSampleStatus, SampleStatusEnum.SAMPLED.getStatus()));
+//        if (sampleList != null && !sampleList.isEmpty()) {
+//            throw new ServiceException("存在样本状态不是已采集的");
+//        }
+        eSampleList.forEach(item -> {
+            Sample sample = sampleMapper.selectById(item.getSampleId());
+            if (sample == null) {
+                throw new ServiceException("样本不存在");
+            }
+            if (!Objects.equals(sample.getSampleStatus(), SampleStatusEnum.SAMPLED.getStatus())) {
+                throw new ServiceException("样本状态不是已采集");
+            }
+            sample.setSampleStatus(SampleStatusEnum.EXPERIMENTING.getStatus());
+            sampleMapper.updateById(sample);
+        });
+
+        return baseMapper.update(experiment, new LambdaUpdateWrapper<Experiment>()
+                .set(Experiment::getExperimentStatus, ExperimentStatusEnum.IN_PROGRESS.getStatus()));
+    }
+
+    /**
+     * 提交实验
+     *
+     * @param bo 实验提交信息
+     * @return 提交结果
+     */
+    @Override
+    public int submitExperiment(ExperimentSubmitBo bo) {
+        Experiment experiment = baseMapper.selectById(bo.getId());
+        if (experiment == null) {
+            throw new ServiceException("实验不存在");
+        }
+        int flag = baseMapper.update(experiment, new LambdaUpdateWrapper<Experiment>()
+                .set(Experiment::getRawDataUrl, bo.getRawDataUrl())
+                .set(Experiment::getReportUrl, bo.getReportUrl())
+                .set(Experiment::getExperimentStatus, ExperimentStatusEnum.AUDITING.getStatus()));
+        if (flag > 0) {
+            NoticeMessageDto message = new NoticeMessageDto();
+            message.setTitle("实验报告已提交,请审核!");
+            NoticeMessageDetailDto detail = new NoticeMessageDetailDto();
+            detail.setAlertType("success");
+            detail.setBusinessType("e_audit");
+            detail.setData(experiment);
+            detail.setContent("请前往实验审核页面查看");
+            message.setDetail(detail);
+            NoticePusher.push(message, List.of(experiment.getReviewManager()));
+        }
+        return flag;
+    }
+
+    /**
+     * 审核实验
+     *
+     * @param bo 审核信息
+     * @return 审核结果
+     */
+    @Override
+    public int auditExperiment(ExperimentAuditBo bo) {
+        Experiment experiment = baseMapper.selectById(bo.getId());
+        if (experiment == null) {
+            throw new ServiceException("实验不存在");
+        }
+        int flag = baseMapper.update(experiment, new LambdaUpdateWrapper<Experiment>()
+                .set(Experiment::getExperimentStatus, bo.isPass() ? ExperimentStatusEnum.FINISHED.getStatus() : ExperimentStatusEnum.RETURN_REPORT.getStatus())
+        );
+        if (flag > 0) {
+            NoticeMessageDto message = new NoticeMessageDto();
+            message.setTitle(bo.isPass() ? "实验报告已审核通过!" : "实验报告未通过审核,请重新提交报告!");
+            NoticeMessageDetailDto detail = new NoticeMessageDetailDto();
+            detail.setAlertType(bo.isPass() ? "success" : "error");
+            detail.setBusinessType("e_report");
+            detail.setData(experiment);
+            detail.setContent("请前往实验审核页面查看");
+            message.setDetail(detail);
+            NoticePusher.push(message, List.of(experiment.getExperimentManager()));
+        }
+        return flag;
+    }
+}

+ 194 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/java/cn/vber/experiment/service/impl/SampleServiceImpl.java

@@ -0,0 +1,194 @@
+package cn.vber.experiment.service.impl;
+
+import cn.vber.common.core.exception.ServiceException;
+import cn.vber.common.core.utils.DateUtils;
+import cn.vber.common.core.utils.MapstructUtils;
+import cn.vber.common.core.utils.StringUtils;
+import cn.vber.common.mybatis.core.page.TableDataInfo;
+import cn.vber.common.mybatis.core.page.PageQuery;
+import cn.vber.common.satoken.utils.LoginHelper;
+import cn.vber.experiment.domain.SampleFlow;
+import cn.vber.experiment.domain.vo.SampleFlowVo;
+import cn.vber.experiment.enums.SampleStatusEnum;
+import cn.vber.experiment.mapper.SampleFlowMapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import cn.vber.experiment.domain.bo.SampleBo;
+import cn.vber.experiment.domain.vo.SampleVo;
+import cn.vber.experiment.domain.Sample;
+import cn.vber.experiment.mapper.SampleMapper;
+import cn.vber.experiment.service.ISampleService;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Collection;
+import java.util.Objects;
+
+/**
+ * 实验样本Service业务层处理
+ *
+ * @author IwbY
+ * @date 2025-10-27
+ */
+@RequiredArgsConstructor
+@Service
+public class SampleServiceImpl implements ISampleService {
+
+    private final SampleMapper baseMapper;
+    private final SampleFlowMapper flowMapper;
+
+    /**
+     * 查询实验样本
+     */
+    @Override
+    public SampleVo queryById(Long id) {
+        return baseMapper.selectVoById(id);
+    }
+
+    /**
+     * 查询实验样本列表
+     */
+    @Override
+    public TableDataInfo<SampleVo> queryPageList(SampleBo bo, PageQuery pageQuery) {
+        LambdaQueryWrapper<Sample> lqw = buildQueryWrapper(bo);
+        Page<SampleVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
+        return TableDataInfo.build(result);
+    }
+
+    /**
+     * 查询实验样本列表
+     */
+    @Override
+    public List<SampleVo> queryList(SampleBo bo) {
+        LambdaQueryWrapper<Sample> lqw = buildQueryWrapper(bo);
+        return baseMapper.selectVoList(lqw);
+    }
+
+    private LambdaQueryWrapper<Sample> buildQueryWrapper(SampleBo bo) {
+        Map<String, Object> params = bo.getParams();
+        LambdaQueryWrapper<Sample> lqw = Wrappers.lambdaQuery();
+        lqw.like(StringUtils.isNotBlank(bo.getSampleName()), Sample::getSampleName, bo.getSampleName());
+        lqw.eq(bo.getBatchNum() != null, Sample::getBatchNum, bo.getBatchNum());
+        lqw.eq(bo.getChickenId() != null, Sample::getChickenId, bo.getChickenId());
+        lqw.eq(StringUtils.isNotBlank(bo.getDescription()), Sample::getDescription, bo.getDescription());
+        lqw.eq(bo.getSampleType() != null, Sample::getSampleType, bo.getSampleType());
+        lqw.eq(bo.getSampleStatus() != null, Sample::getSampleStatus, bo.getSampleStatus());
+        return lqw;
+    }
+
+    /**
+     * 新增实验样本
+     */
+    @Override
+    public Boolean insertByBo(SampleBo bo) {
+        Sample add = MapstructUtils.convert(bo, Sample.class);
+        validEntityBeforeSave(add);
+        boolean flag = baseMapper.insert(add) > 0;
+        if (flag) {
+            bo.setId(add.getId());
+        }
+        return flag;
+    }
+
+    /**
+     * 修改实验样本
+     */
+    @Override
+    public Boolean updateByBo(SampleBo bo) {
+        Sample update = MapstructUtils.convert(bo, Sample.class);
+        validEntityBeforeSave(update);
+        return baseMapper.updateById(update) > 0;
+    }
+
+    /**
+     * 保存前的数据校验
+     */
+    private void validEntityBeforeSave(Sample entity) {
+        //TODO 做一些数据校验,如唯一约束
+    }
+
+    /**
+     * 批量删除实验样本
+     */
+    @Override
+    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
+        if (isValid) {
+            //TODO 做一些业务上的校验,判断是否需要校验
+        }
+        return baseMapper.deleteByIds(ids) > 0;
+    }
+
+    /**
+     * 取样
+     */
+    @Override
+    public int sample(SampleBo bo) {
+        if (bo.getChickenId() == null) {
+            throw new ServiceException("取样个体不能为空");
+        }
+        if (bo.getSampleStatus() == null) {
+            throw new ServiceException("样本状态不能为空");
+        }
+        Sample sample = baseMapper.selectById(bo.getId());
+        if (sample == null) {
+            throw new ServiceException("取样个体不存在");
+        }
+        if (!Objects.equals(sample.getSampleStatus(), SampleStatusEnum.NEW.getStatus())) {
+            throw new ServiceException("取样个体状态错误");
+        }
+        sample.setSampleName(bo.getSampleName());
+        sample.setDescription(bo.getDescription());
+        sample.setChickenId(bo.getChickenId());
+        sample.setBatchNum(bo.getBatchNum());
+        sample.setSampleType(bo.getSampleType());
+        sample.setSampleStatus(SampleStatusEnum.SAMPLED.getStatus());
+        sample.setSampleTime(DateUtils.getNowDate());
+        return baseMapper.updateById(sample);
+    }
+
+    @Override
+    public int destroy(Long id) {
+        Sample sample = baseMapper.selectById(id);
+        if (sample == null) {
+            throw new ServiceException("销毁个体不存在");
+        }
+        if (!Objects.equals(sample.getSampleStatus(), SampleStatusEnum.SAMPLED.getStatus())) {
+            throw new ServiceException("销毁个体状态错误");
+        }
+        sample.setSampleStatus(SampleStatusEnum.DESTROYED.getStatus());
+        return baseMapper.updateById(sample);
+    }
+
+    @Override
+    public List<SampleFlowVo> queryFlowLogs(Long id) {
+        LambdaQueryWrapper<SampleFlow> lqw = Wrappers.lambdaQuery();
+        lqw.eq(SampleFlow::getSampleId, id);
+        return flowMapper.selectVoList(lqw);
+    }
+
+    /**
+     * 查询个体信息
+     */
+    @Override
+    public SampleVo querySample(Long id) {
+        SampleVo sample = baseMapper.selectVoById(id);
+        if (sample == null) {
+            throw new ServiceException("查询个体不存在");
+        }
+        SampleFlow lastFlow = flowMapper.selectList(new LambdaQueryWrapper<SampleFlow>()
+                .eq(SampleFlow::getHandler, LoginHelper.getUserId())
+                .orderByDesc(SampleFlow::getId)).get(0);
+        if (lastFlow != null && DateUtils.getNowDate().before(DateUtils.addHours(lastFlow.getHandleTime(), 1))) {
+            return sample;
+        }
+        SampleFlow sampleFlow = new SampleFlow();
+        sampleFlow.setSampleId(id);
+        sampleFlow.setHandler(LoginHelper.getUserId());
+        sampleFlow.setHandleTime(DateUtils.getNowDate());
+        flowMapper.insert(sampleFlow);
+        return sample;
+    }
+}

+ 7 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/ExperimentMapper.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.vber.experiment.mapper.ExperimentMapper">
+
+</mapper>

+ 7 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/ExperimentSampleMapper.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.vber.experiment.mapper.ExperimentSampleMapper">
+
+</mapper>

+ 7 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/SampleFlowMapper.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.vber.experiment.mapper.SampleFlowMapper">
+
+</mapper>

+ 7 - 0
SERVER/ChickenFarmV3/vb-modules/vb-experiment/src/main/resources/mapper/experiment/SampleMapper.xml

@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="cn.vber.experiment.mapper.SampleMapper">
+
+</mapper>

+ 1 - 1
SERVER/ChickenFarmV3/vb-modules/vb-generator/src/main/java/cn/vber/generator/util/GenUtils.java

@@ -65,7 +65,7 @@ public class GenUtils {
 
             // 数据库的数字字段与java不匹配 且很多数据库的数字字段很模糊 例如oracle只有number没有细分
             // 所以默认数字类型全为Long可在界面上自行编辑想要的类型 有什么特殊需求也可以在这里特殊处理
-            column.setJavaType(GenConstants.TYPE_LONG);
+            column.setJavaType(column.getColumnType().contains("bigint") ? GenConstants.TYPE_LONG : GenConstants.TYPE_INTEGER);
         }
 
         // BO对象 默认插入勾选

+ 5 - 1
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/controller/system/SysUserController.java

@@ -5,9 +5,12 @@ import cn.dev33.satoken.secure.BCrypt;
 import cn.hutool.core.lang.tree.Tree;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.vber.common.core.constant.ConfigKeyConstants;
 import cn.vber.common.core.constant.SystemConstants;
 import cn.vber.common.core.domain.R;
 import cn.vber.common.core.domain.model.LoginUser;
+import cn.vber.common.core.service.ConfigService;
+import cn.vber.common.core.utils.SpringUtils;
 import cn.vber.common.core.utils.StreamUtils;
 import cn.vber.common.core.utils.StringUtils;
 import cn.vber.common.encrypt.annotation.ApiEncrypt;
@@ -179,7 +182,8 @@ public class SysUserController extends BaseController {
                 return R.fail("当前租户下用户名额不足,请联系管理员");
             }
         }
-        user.setPassword(BCrypt.hashpw(user.getPassword()));
+        String password = StringUtils.isBlank(user.getPassword()) ? SpringUtils.getBean(ConfigService.class).getConfigValue(ConfigKeyConstants.SYS_USER_INIT_PASSWORD) : user.getPassword();
+        user.setPassword(BCrypt.hashpw(password));
         return toAjax(userService.insertUser(user));
     }
 

+ 2 - 1
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/listener/SysUserImportListener.java

@@ -6,6 +6,7 @@ import cn.hutool.crypto.digest.BCrypt;
 import cn.hutool.http.HtmlUtil;
 import cn.idev.excel.context.AnalysisContext;
 import cn.idev.excel.event.AnalysisEventListener;
+import cn.vber.common.core.constant.ConfigKeyConstants;
 import cn.vber.common.core.exception.ServiceException;
 import cn.vber.common.core.utils.SpringUtils;
 import cn.vber.common.core.utils.StreamUtils;
@@ -45,7 +46,7 @@ public class SysUserImportListener extends AnalysisEventListener<SysUserImportVo
     private int failureNum = 0;
 
     public SysUserImportListener(Boolean isUpdateSupport) {
-        String initPassword = SpringUtils.getBean(ISysConfigService.class).selectConfigByKey("sys.user.initPassword");
+        String initPassword = SpringUtils.getBean(ISysConfigService.class).selectConfigByKey(ConfigKeyConstants.SYS_USER_INIT_PASSWORD);
         this.userService = SpringUtils.getBean(ISysUserService.class);
         this.password = BCrypt.hashpw(initPassword);
         this.isUpdateSupport = isUpdateSupport;

+ 5 - 0
SERVER/ChickenFarmV3/vb-modules/vb-system/src/main/java/cn/vber/system/service/impl/SysUserServiceImpl.java

@@ -1,10 +1,13 @@
 package cn.vber.system.service.impl;
 
+import cn.dev33.satoken.secure.BCrypt;
 import cn.hutool.core.bean.BeanUtil;
 import cn.hutool.core.collection.CollUtil;
 import cn.hutool.core.convert.Convert;
 import cn.hutool.core.util.ArrayUtil;
 import cn.hutool.core.util.ObjectUtil;
+import cn.vber.common.core.constant.ConfigKeyConstants;
+import cn.vber.common.core.service.ConfigService;
 import com.baomidou.mybatisplus.core.conditions.Wrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
@@ -320,6 +323,8 @@ public class SysUserServiceImpl implements ISysUserService, UserService {
     @Transactional(rollbackFor = Exception.class)
     public int insertUser(SysUserBo user) {
         SysUser sysUser = MapstructUtils.convert(user, SysUser.class);
+
+
         // 新增用户信息
         int rows = baseMapper.insert(sysUser);
         user.setUserId(sysUser.getUserId());

+ 91 - 0
UI/VB.VUE/src/api/experiment/_experiment.ts

@@ -0,0 +1,91 @@
+import Rs from "@/core/services/RequestService"
+
+class experimentApi {
+	tableUrl = "/experiment/experiment/list"
+	exportUrl = "/experiment/experiment/export"
+
+	// 查询实验管理列表
+	list = (query: any) => {
+		return Rs.get({
+			url: "/experiment/experiment/list",
+			params: query,
+			loading: false
+		})
+	}
+
+	// 查询实验管理详细
+	get = (id: string) => {
+		return Rs.get({
+			url: "/experiment/experiment/" + id,
+			loading: false
+		})
+	}
+
+	// 新增或修改实验管理
+	addOrUpdate = (data: any) => {
+		return new Promise((resolve) => {
+			if (data.id) {
+				this.update(data).then((res: any) => {
+					message.msgSuccess("修改成功")
+					resolve(res)
+				})
+			} else {
+				this.add(data).then((res: any) => {
+					message.msgSuccess("新增成功")
+					resolve(res)
+				})
+			}
+		})
+	}
+
+	// 新增实验管理
+	add = (data: any) => {
+		return Rs.post({
+			url: "/experiment/experiment",
+			data: data,
+			successAlert: false
+		})
+	}
+
+	// 修改实验管理
+	update = (data: any) => {
+		return Rs.put({
+			url: "/experiment/experiment",
+			data: data,
+			successAlert: false
+		})
+	}
+
+	// 删除实验管理
+	del = (id: string | string[]) => {
+		return Rs.del({
+			url: "/experiment/experiment/" + id
+		})
+	}
+
+	getExperimentUsers = () => {
+		return Rs.get({
+			url: "/experiment/experiment/getExperimentUsers"
+		})
+	}
+	start = (id: string) => {
+		return Rs.post({
+			url: "/experiment/experiment/start/" + id
+		})
+	}
+	submitExperiment = (data: any) => {
+		return Rs.post({
+			url: "/experiment/experiment/submitExperiment",
+			data: data
+		})
+	}
+
+	auditExperiment = (data: any) => {
+		return Rs.post({
+			url: "/experiment/experiment/auditExperiment",
+			data: data
+		})
+	}
+}
+
+export default experimentApi

+ 93 - 0
UI/VB.VUE/src/api/experiment/_sample.ts

@@ -0,0 +1,93 @@
+import Rs from "@/core/services/RequestService"
+
+class sampleApi {
+	tableUrl = "/experiment/sample/list"
+	exportUrl = "/experiment/sample/export"
+
+	// 查询实验样本列表
+	list = (query: any) => {
+		return Rs.get({
+			url: "/experiment/sample/list",
+			params: query,
+			loading: false
+		})
+	}
+
+	// 查询实验样本详细
+	get = (id: string) => {
+		return Rs.get({
+			url: "/experiment/sample/" + id,
+			loading: false
+		})
+	}
+
+	// 新增或修改实验样本
+	addOrUpdate = (data: any) => {
+		return new Promise((resolve) => {
+			if (data.id) {
+				this.update(data).then((res: any) => {
+					message.msgSuccess("修改成功")
+					resolve(res)
+				})
+			} else {
+				this.add(data).then((res: any) => {
+					message.msgSuccess("新增成功")
+					resolve(res)
+				})
+			}
+		})
+	}
+
+	// 新增实验样本
+	add = (data: any) => {
+		return Rs.post({
+			url: "/experiment/sample",
+			data: data,
+			successAlert: false
+		})
+	}
+
+	// 修改实验样本
+	update = (data: any) => {
+		return Rs.put({
+			url: "/experiment/sample",
+			data: data,
+			successAlert: false
+		})
+	}
+
+	// 删除实验样本
+	del = (id: string | string[]) => {
+		return Rs.del({
+			url: "/experiment/sample/" + id
+		})
+	}
+	// 修改实验样本
+	smaple = (data: any) => {
+		return Rs.post({
+			url: "/experiment/sample/sample",
+			data: data,
+			successAlert: false
+		})
+	}
+
+	destroy = (id: string | string[]) => {
+		return Rs.post({
+			url: "/experiment/sample/destroy/" + id
+		})
+	}
+
+	querySample = (id: string | number) => {
+		return Rs.get({
+			url: "/experiment/sample/querySample/" + id
+		})
+	}
+
+	queryFlowLogs = (id: string | number) => {
+		return Rs.get({
+			url: "/experiment/sample/queryFlowLogs/" + id
+		})
+	}
+}
+
+export default sampleApi

+ 14 - 0
UI/VB.VUE/src/api/experiment/index.ts

@@ -0,0 +1,14 @@
+import Experiment from "./_experiment"
+import Sample from "./_sample"
+
+export interface IExperimentApi {
+	experimentApi: Experiment
+	sampleApi: Sample
+}
+
+export const apis: IExperimentApi = {
+	experimentApi: new Experiment(),
+	sampleApi: new Sample()
+}
+
+export default apis

+ 3 - 0
UI/VB.VUE/src/api/index.ts

@@ -7,6 +7,7 @@ import tool, { type IToolApi } from "./tool"
 import demo, { type IDemoApi } from "./demo"
 import breeding, { type IBreedingApi } from "./breeding"
 import reports, { type IReportApi } from "./reports"
+import experiment, { type IExperimentApi } from "./experiment"
 import device, { type IDeviceApi } from "./device"
 import erp, { type IErpApi } from "./erp"
 
@@ -20,6 +21,7 @@ export interface IAppApi {
 	base: IBaseApi
 	breeding: IBreedingApi
 	reports: IReportApi
+	experiment: IExperimentApi
 	device: IDeviceApi
 	erp: IErpApi
 }
@@ -34,6 +36,7 @@ export const apis: IAppApi = {
 	base,
 	breeding,
 	reports,
+	experiment,
 	device,
 	erp
 }

+ 149 - 0
UI/VB.VUE/src/components/modal-select/SampleSelect.vue

@@ -0,0 +1,149 @@
+<script setup lang="ts">
+import apis from "@a"
+import dayjs from "dayjs"
+
+const props = withDefaults(
+	defineProps<{
+		modelValue?: any[] | any
+		modalConfig?: any
+		multiple?: boolean
+		showTag?: boolean
+		optionSelectFun?: (v: any) => any // 自定义选项数据转换
+		convertDataFun?: (v: any) => any
+		searchFormItems?: any[]
+		saveAutoClose?: boolean
+	}>(),
+	{
+		showTag: true,
+		multiple: true,
+		saveAutoClose: true
+	}
+)
+const emits = defineEmits<{
+	(e: "update:modelValue", v: any[]): void
+	(e: "confirm", v: any[]): void
+}>()
+const selectRef = ref()
+
+const selectIds = ref(props.modelValue)
+const selectList = ref<any[]>([])
+
+const tableOpts = reactive({
+	columns: [
+		{ field: "id", name: "样本ID", width: 100, isSort: true, visible: false, tooltip: true },
+		{
+			field: "sampleName",
+			name: "样本名称",
+			visible: true,
+			isSort: false,
+			width: "200",
+			tooltip: true
+		},
+		{
+			field: "description",
+			name: "样本描述",
+			visible: true,
+			isSort: false,
+			width: "auto",
+			tooltip: true
+		},
+		// {
+		// 	field: "batchId",
+		// 	name: "取样批次",
+		// 	visible: true,
+		// 	isSort: false,
+		// 	width: "auto",
+		// 	tooltip: true
+		// },
+		{
+			field: "wingTagNum",
+			name: "取样个体翅号",
+			visible: true,
+			isSort: false,
+			width: "120",
+			tooltip: true
+		},
+		// {field: "description", name: "样本描述", visible: true, isSort: false, width: "auto", tooltip: true},
+		{ field: "sampleType", name: "样品类型", visible: true, isSort: false, width: 120 },
+		{ field: "sampleTime", name: "取样时间", visible: true, isSort: false, width: 140 },
+		{ field: "sampleStatus", name: "样品状态", visible: true, isSort: false, width: 120 }
+	] as any[],
+	queryParams: {
+		sampleName: undefined,
+		sampleStatus: 2
+	},
+	searchFormItems:
+		props.searchFormItems ||
+		([
+			{
+				field: "sampleName",
+				label: "样品名称",
+				class: "w-100",
+				required: false,
+				placeholder: "请输入样品名称",
+				listeners: {
+					keyup: (e: KeyboardEvent) => {
+						if (e.code == "Enter") {
+							selectRef.value.handleQuery()
+						}
+					}
+				},
+				span: 6
+			}
+		] as any),
+	customBtns: [],
+	tableListFun: apis.experiment.sampleApi.list
+})
+const convertRowDataFun = (row: any) => {
+	return props.convertDataFun ? props.convertDataFun(row) : row
+}
+function open() {
+	selectRef.value.open()
+}
+function close() {
+	selectRef.value.close()
+}
+function getSelectList() {
+	if (selectList.value.length > 0) {
+		return selectList.value
+	}
+}
+defineExpose({
+	open,
+	close,
+	getSelectList
+})
+</script>
+<template>
+	<ModalSelect
+		ref="selectRef"
+		:table-opts="tableOpts"
+		modalTitle="选择设备"
+		v-model="selectIds"
+		v-model:select-list="selectList"
+		:modal-config="modalConfig"
+		:multiple="multiple"
+		:show-tag="showTag"
+		:tag-id="'id'"
+		:tag-name="'sampleName'"
+		:convert-data-fun="convertRowDataFun"
+		:save-auto-close="saveAutoClose"
+		@update:modelValue="emits('update:modelValue', $event)"
+		@confirm="emits('confirm', $event)">
+		<template #sampleType="{ row }">
+			<DictTag type="experiment_sample_type" :value-is-number="1" :value="row.sampleType"></DictTag>
+		</template>
+		<template #sampleStatus="{ row }">
+			<DictTag
+				type="experiment_sample_status"
+				:value-is-number="1"
+				:value="row.sampleStatus"></DictTag>
+		</template>
+		<template #sampleTime="{ row }">
+			<template v-if="row.sampleTime">
+				{{ dayjs(row.sampleTime).format("YYYY/MM/DD HH:mm:ss") }}
+			</template>
+			<template v-else>-</template>
+		</template>
+	</ModalSelect>
+</template>

+ 5 - 1
UI/VB.VUE/src/layouts/main/header/navbar/WorkOrderMenu.vue

@@ -8,7 +8,11 @@
 			<span class="menu-title">故障报修</span>
 		</router-link>
 	</div>
-	<div class="menu-item" data-vb-menu-trigger="hover" data-vb-menu-placement="left-start">
+	<div
+		class="menu-item"
+		data-vb-menu-trigger="hover"
+		data-vb-menu-placement="left-start"
+		v-has-permission="'workOrder'">
 		<span class="menu-link px-5">
 			<span class="menu-icon">
 				<VbIcon icon-name="finance-calculator" icon-type="duotone" class="me-2 fs-3"></VbIcon>

+ 11 - 2
UI/VB.VUE/src/router/_staticRouter.ts

@@ -78,14 +78,23 @@ export const staticRouter: RouteRecordRaw[] = [
 		]
 	},
 	{
-		path: "/is/checkin/:id?",
-		name: "inspectionRule",
+		path: "/checkin/:id?",
+		name: "inspectionRuleCheckin",
 		component: () => import("@/views/device/inspection/checkin.vue"),
 		meta: {
 			title: "点检签到",
 			noCache: false
 		}
 	},
+	{
+		path: "/sample/:id?",
+		name: "sampleFlow",
+		component: () => import("@/views/experiment/sample/sampleFlow.vue"),
+		meta: {
+			title: "查看样品",
+			noCache: false
+		}
+	},
 	{
 		path: "/",
 		children: [

+ 2 - 0
UI/VB.VUE/src/views/common/modal/chickenModal.vue

@@ -21,6 +21,7 @@ const modalRef = ref()
 const tableRef = ref()
 
 const queryParams = ref({
+	status: "0",
 	wingTagNum: undefined,
 	batchNum: undefined,
 	gender: undefined
@@ -63,6 +64,7 @@ function handleQuery(queryParam?: any) {
 /** 重置按钮操作 */
 function resetQuery() {
 	queryParams.value = {
+		status: "0",
 		batchNum: undefined,
 		wingTagNum: undefined,
 		gender: undefined

+ 1 - 1
UI/VB.VUE/src/views/device/inspection/index.vue

@@ -282,7 +282,7 @@ const qrCode = ref({
 })
 function handleQrCode(row) {
 	qrModalData.value = row
-	qrCode.value.text = `vb@device@/is/checkin/${row.id}`
+	qrCode.value.text = `vb@device@/checkin/${row.id}`
 	qrModalRef.value.show()
 }
 function handleDownloadQr(id: string) {

+ 339 - 0
UI/VB.VUE/src/views/experiment/experiment/index.vue

@@ -0,0 +1,339 @@
+<script setup lang="ts" name="Experiment">
+import apis from "@a"
+import dayjs from "dayjs"
+
+const experimentUsersOptions = ref([])
+const tableRef = ref()
+const modalRef = ref()
+const sampleSelectRef = ref()
+const opts = reactive({
+	columns: [
+		{ field: "id", name: "实验ID", width: 100, isSort: true, visible: false, tooltip: true },
+		{
+			field: "experimentName",
+			name: "实验名称",
+			visible: true,
+			isSort: false,
+			width: "auto",
+			tooltip: true
+		},
+		{ field: "experimentType", name: "实验类型", visible: true, isSort: false, width: 100 },
+		{
+			field: "experimentManager",
+			name: "实验负责人",
+			visible: true,
+			isSort: false,
+			width: "auto",
+			tooltip: true
+		},
+		{
+			field: "reviewManager",
+			name: "审核负责人",
+			visible: true,
+			isSort: false,
+			width: "auto",
+			tooltip: true
+		},
+		{
+			field: "experimentStatus",
+			name: "实验状态",
+			visible: true,
+			isSort: false,
+			width: "auto",
+			tooltip: true
+		},
+		{
+			field: "rawDataUrl",
+			name: "原始数据",
+			visible: true,
+			isSort: false,
+			width: "auto",
+			tooltip: true
+		},
+		{ field: "reportUrl", name: "实验报告", visible: true, isSort: false, width: 100 },
+
+		{ field: "actions", name: `操作`, width: 150 }
+	] as any[],
+	queryParams: {
+		experimentName: undefined,
+		experimentType: undefined,
+		experimentStatus: undefined
+	},
+	searchFormItems: [
+		{
+			field: "experimentName",
+			label: "实验名称",
+			class: "w-100",
+			required: false,
+			placeholder: "请输入实验名称",
+			component: "I",
+			listeners: {
+				keyup: (e: KeyboardEvent) => {
+					if (e.code == "Enter") {
+						handleQuery()
+					}
+				}
+			}
+		},
+		{
+			field: "experimentType",
+			label: "实验类型",
+			class: "w-100",
+			required: false,
+			component: "Dict",
+			props: {
+				placeholder: "请选择实验类型",
+				dictType: "experiment_type",
+				valueIsNumber: 1,
+				type: "select"
+			},
+			listeners: {
+				change: () => {
+					handleQuery()
+				}
+			}
+		},
+		{
+			field: "experimentStatus",
+			label: "实验状态",
+			class: "w-100",
+			required: false,
+			component: "Dict",
+			placeholder: "请选择实验状态",
+			props: {
+				dictType: "experiment_status",
+				valueIsNumber: 1,
+				type: "select"
+			},
+			listeners: {
+				change: () => {
+					handleQuery()
+				}
+			}
+		}
+	] as any,
+	permission: "experiment:experiment",
+	handleBtns: [],
+	handleFuns: {
+		handleCreate,
+		handleUpdate: () => {
+			const row = tableRef.value.getSelected()
+			handleUpdate(row)
+		},
+		handleDelete: () => {
+			const rows = tableRef.value.getSelecteds()
+			handleDelete(rows)
+		}
+	},
+	customBtns: [],
+	tableListFun: apis.experiment.experimentApi.list,
+	getEntityFun: apis.experiment.experimentApi.get,
+	deleteEntityFun: apis.experiment.experimentApi.del,
+	exportUrl: apis.experiment.experimentApi.exportUrl,
+	exportName: "Experiment",
+	modalTitle: "实验管理",
+	formItems: [
+		{
+			field: "experimentName",
+			label: "实验名称",
+			class: "w-100",
+			required: true,
+			placeholder: "请输入实验名称",
+			component: "I"
+		},
+		{
+			field: "experimentType",
+			label: "实验类型",
+			class: "w-100",
+			required: true,
+			component: "Dict",
+			props: {
+				placeholder: "请选择实验类型",
+				dictType: "experiment_type",
+				type: "select",
+				valueIsNumber: 1
+			}
+		},
+		{
+			field: "sampleNames",
+			label: "实验样品",
+			class: "w-100",
+			required: true,
+			placeholder: "请选择实验样品",
+			data: () => [],
+			props: {
+				type: "select"
+			},
+			append: "icon",
+			appendClickFunc: () => {
+				sampleSelectRef.value.open()
+			}
+		},
+		{
+			field: "reviewManager",
+			label: "审核负责人",
+			class: "w-100",
+			required: true,
+			placeholder: "请选择审核负责人",
+			component: "VS",
+			data: () => experimentUsersOptions.value,
+			props: {
+				valueIsNumber: 1,
+				type: "select"
+			}
+		},
+		{
+			field: "description",
+			label: "实验描述",
+			class: "w-100",
+			required: true,
+			placeholder: "请输入实验描述",
+			component: "I",
+			props: {
+				type: "textarea"
+			}
+		}
+	] as any,
+	resetForm: () => {
+		form.value = emptyFormData.value
+	},
+	labelWidth: "80px",
+	emptyFormData: {
+		id: undefined,
+		experimentName: undefined,
+		experimentType: undefined,
+		sampleIds: undefined,
+		sampleNames: undefined,
+		reviewManager: undefined
+	}
+})
+const { queryParams, emptyFormData } = toRefs(opts)
+const form = ref<any>(emptyFormData.value)
+
+/** 搜索按钮操作 */
+function handleQuery(query?: any) {
+	query = query || tableRef.value?.getQueryParams() || queryParams.value
+	addDateRange(query, query.dateRangeCreateTime)
+	addDateRange(query, query.dateRangeUpdateTime, "UpdateTime")
+	tableRef.value?.query(query)
+}
+
+/** 重置按钮操作 */
+function resetQuery(query?: any) {
+	query = query || tableRef.value?.getQueryParams() || queryParams.value
+	query.dateRangeCreateTime = [] as any
+	addDateRange(query, query.dateRangeCreateTime)
+	query.dateRangeUpdateTime = [] as any
+	addDateRange(query, query.dateRangeUpdateTime, "UpdateTime")
+	//
+}
+
+function handleCreate() {
+	tableRef.value.defaultHandleFuns.handleCreate()
+}
+
+/** 修改按钮操作 */
+function handleUpdate(row: any) {
+	tableRef.value.defaultHandleFuns.handleUpdate("", row)
+}
+
+/** 删除按钮操作 */
+function handleDelete(rows: any[]) {
+	tableRef.value.defaultHandleFuns.handleDelete("", rows)
+}
+
+/** 提交按钮 */
+function submitForm() {
+	apis.experiment.experimentApi.addOrUpdate(form.value).then(() => {
+		handleQuery()
+	})
+}
+
+function onSampleSelectConfirm(data: any) {
+	console.log("data", data)
+	form.value.sampleIds = data.map((item) => item.id)
+	form.value.sampleNames = data.map((item) => item.sampleName)
+}
+
+function getExperimentUsersOptions() {
+	apis.experiment.experimentApi.getExperimentUsers().then((res) => {
+		experimentUsersOptions.value = res.data.map((item) => {
+			return {
+				label: `${item.nickname}(${item.username})`,
+				value: item.userId
+			}
+		})
+	})
+}
+function init() {
+	getExperimentUsersOptions()
+}
+onMounted(init)
+</script>
+<template>
+	<div class="app-container">
+		<VbDataTable
+			ref="tableRef"
+			keyField="id"
+			:columns="opts.columns"
+			:handle-perm="opts.permission"
+			:handle-btns="opts.handleBtns"
+			:handle-funs="opts.handleFuns"
+			:search-form-items="opts.searchFormItems"
+			:custom-btns="opts.customBtns"
+			:remote-fun="opts.tableListFun"
+			:get-entity-fun="opts.getEntityFun"
+			:delete-entity-fun="opts.deleteEntityFun"
+			:export-url="opts.exportUrl"
+			:export-name="opts.exportName"
+			:modal="modalRef"
+			:reset-form-fun="opts.resetForm"
+			v-model:form-data="form"
+			v-model:query-params="queryParams"
+			:check-multiple="true"
+			:reset-search-form-fun="resetQuery"
+			:custom-search-fun="handleQuery">
+			<template #experimentType="{ row }">
+				<DictTag type="experiment_type" :value-is-number="1" :value="row.experimentType"></DictTag>
+			</template>
+			<template #reportUrl="{ row }"></template>
+			<template #experimentStatus="{ row }">
+				<DictTag type="experiment_status" :value-is-number="0" :value="row.reportUrl"></DictTag>
+			</template>
+			<template #actions="{ row }">
+				<vb-tooltip content="修改" placement="top">
+					<el-button
+						link
+						type="primary"
+						@click="handleUpdate(row)"
+						v-hasPermission="'experiment:experiment:edit'">
+						<template #icon>
+							<VbIcon icon-name="notepad-edit" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
+				<vb-tooltip content="删除" placement="top">
+					<el-button
+						link
+						type="primary"
+						@click="handleDelete([row])"
+						v-hasPermission="'experiment:experiment:remove'">
+						<template #icon>
+							<VbIcon icon-name="trash-square" icon-type="duotone" class="fs-3"></VbIcon>
+						</template>
+					</el-button>
+				</vb-tooltip>
+			</template>
+		</VbDataTable>
+		<VbModal
+			v-model:modal="modalRef"
+			:title="opts.modalTitle"
+			:form-data="form"
+			:form-items="opts.formItems"
+			:label-width="opts.labelWidth"
+			append-to-body
+			@confirm="submitForm"></VbModal>
+
+		<SampleSelect ref="sampleSelectRef" @confirm="onSampleSelectConfirm"></SampleSelect>
+	</div>
+</template>

+ 580 - 0
UI/VB.VUE/src/views/experiment/sample/_sample.vue

@@ -0,0 +1,580 @@
+<script setup lang="ts" name="Sample">
+import apis from "@a"
+import dayjs from "dayjs"
+import ChickenModal from "@v/common/modal/chickenModal.vue"
+import vueQr from "vue-qr/src/packages/vue-qr.vue"
+import type { ToolBtn } from "@@@/table/models"
+
+const props = withDefaults(
+	defineProps<{
+		actions: ("update" | "delete" | "sample" | "destroy" | "flow" | "qrCode" | "audit" | "reject")[]
+		customBtns?: ToolBtn[]
+		status?: number
+		createBy?: number
+	}>(),
+	{
+		customBtns: () => {
+			return []
+		}
+	}
+)
+
+const tableRef = ref()
+const modalRef = ref()
+const modalType = ref("C")
+const opts = reactive({
+	columns: () =>
+		[
+			{ field: "id", name: "样本ID", width: 100, isSort: true, visible: false, tooltip: true },
+			{
+				field: "sampleName",
+				name: "样本名称",
+				visible: true,
+				isSort: false,
+				width: "200",
+				tooltip: true
+			},
+			{
+				field: "description",
+				name: "样本描述",
+				visible: true,
+				isSort: false,
+				width: "auto",
+				tooltip: true
+			},
+			// {
+			// 	field: "batchId",
+			// 	name: "取样批次",
+			// 	visible: true,
+			// 	isSort: false,
+			// 	width: "auto",
+			// 	tooltip: true
+			// },
+			{
+				field: "wingTagNum",
+				name: "取样个体翅号",
+				visible: true,
+				isSort: false,
+				width: "120",
+				tooltip: true
+			},
+			// {field: "description", name: "样本描述", visible: true, isSort: false, width: "auto", tooltip: true},
+			{ field: "sampleType", name: "样品类型", visible: true, isSort: false, width: 120 },
+			{ field: "sampleTime", name: "取样时间", visible: true, isSort: false, width: 140 },
+			{ field: "sampleStatus", name: "样品状态", visible: true, isSort: false, width: 120 },
+			{ field: "actions", name: `操作`, width: 150 }
+		] as any[],
+	queryParams: {
+		sampleName: undefined,
+		batchId: undefined,
+		chickenId: undefined,
+		wingTagNum: undefined,
+		description: undefined,
+		sampleType: undefined,
+		sampleStatus: props.status
+	},
+	searchFormItems: [
+		{
+			field: "sampleName",
+			label: "样本名称",
+			class: "w-100",
+			required: false,
+			placeholder: "请输入样本名称",
+			component: "I",
+			listeners: {
+				keyup: (e: KeyboardEvent) => {
+					if (e.code == "Enter") {
+						handleQuery()
+					}
+				}
+			}
+		},
+		{
+			field: "wingTagNum",
+			disabled: true,
+			label: "取样翅号",
+			class: "w-100",
+			required: false,
+			placeholder: "请选择翅号",
+			component: "I",
+			append: "icon",
+			appendClickFunc: () => {
+				handleOpenChickenModal("search")
+			}
+		},
+		{
+			field: "sampleType",
+			label: "样品类型",
+			class: "w-100",
+			required: false,
+			component: "Dict",
+			props: {
+				placeholder: "请选择样品类型",
+				dictType: "experiment_sample_type",
+				valueIsNumber: 1,
+				type: "select"
+			},
+			listeners: {
+				change: () => {
+					handleQuery()
+				}
+			}
+		},
+		{
+			show: () => props.status == undefined,
+			field: "sampleStatus",
+			label: "样品状态",
+			class: "w-100",
+			required: false,
+			component: "Dict",
+			props: {
+				placeholder: "请选择样品状态",
+				dictType: "experiment_sample_status",
+				valueIsNumber: 1,
+				type: "select"
+			},
+			listeners: {
+				change: () => {
+					handleQuery()
+				}
+			}
+		}
+	] as any,
+	permission: "",
+	handleBtns: [],
+	handleFuns: {
+		handleCreate,
+		handleUpdate: () => {
+			const row = tableRef.value.getSelected()
+			handleUpdate(row)
+		},
+		handleDelete: () => {
+			const rows = tableRef.value.getSelecteds()
+			handleDelete(rows)
+		}
+	},
+	customBtns: props.customBtns,
+	tableListFun: apis.experiment.sampleApi.list,
+	getEntityFun: apis.experiment.sampleApi.get,
+	deleteEntityFun: apis.experiment.sampleApi.del,
+	exportUrl: apis.experiment.sampleApi.exportUrl,
+	exportName: "Sample",
+	modalTitle: "实验样本",
+	formItems: [
+		{
+			field: "sampleName",
+			label: "样本名称",
+			class: "w-100",
+			required: true,
+			placeholder: "请输入样本名称",
+			component: "I"
+		},
+		// {
+		// 	field: "batchId",
+		// 	label: "取样批次",
+		// 	class: "w-100",
+		// 	required: false,
+		// 	placeholder: "请输入取样批次",
+		// 	component: "I"
+		// },
+		{
+			show: () => modalType.value == "S",
+			field: "wingTagNum",
+			label: "取样翅号",
+			class: "w-100",
+			required: true,
+			placeholder: "请选择取样翅号",
+			component: "I",
+			append: "icon",
+			appendClickFunc: () => {
+				handleOpenChickenModal("form")
+			}
+		},
+		{
+			show: () => modalType.value == "S",
+			field: "sampleType",
+			label: "样品类型",
+			class: "w-100",
+			required: true,
+			component: "Dict",
+			props: {
+				placeholder: "请选择样品类型",
+				dictType: "experiment_sample_type",
+				type: "select",
+				valueIsNumber: 1
+			}
+		},
+		{
+			field: "description",
+			label: "样本描述",
+			class: "w-100",
+			required: false,
+			placeholder: "请输入样本描述",
+			component: "I",
+			props: {
+				type: "textarea",
+				rows: 5
+			}
+		}
+	] as any,
+	resetForm: () => {
+		form.value = emptyFormData.value
+	},
+	labelWidth: "80px",
+	emptyFormData: {
+		id: undefined,
+		sampleName: undefined,
+		batchId: undefined,
+		chickenId: undefined,
+		wingTagNum: undefined,
+		description: undefined,
+		sampleType: undefined,
+		sampleStatus: undefined
+	}
+})
+const { queryParams, emptyFormData } = toRefs(opts)
+const form = ref<any>(emptyFormData.value)
+const modalTitle = computed(() => {
+	return modalType.value == "C" || modalType.value == "U"
+		? "样本"
+		: modalType.value == "S"
+			? "个体取样"
+			: ""
+})
+/** 搜索按钮操作 */
+function handleQuery(query?: any) {
+	query = query || tableRef.value?.getQueryParams() || queryParams.value
+	addDateRange(query, query.dateRangeCreateTime)
+	tableRef.value?.query(query)
+}
+
+/** 重置按钮操作 */
+function resetQuery(query?: any) {
+	query = query || tableRef.value?.getQueryParams() || queryParams.value
+	query.dateRangeCreateTime = [] as any
+	addDateRange(query, query.dateRangeCreateTime)
+
+	//
+}
+
+function handleCreate() {
+	modalType.value = "C"
+	tableRef.value.defaultHandleFuns.handleCreate()
+}
+
+/** 修改按钮操作 */
+function handleUpdate(row: any) {
+	modalType.value = "U"
+	tableRef.value.defaultHandleFuns.handleUpdate("", row)
+}
+function handleSample(row: any) {
+	modalType.value = "S"
+	apis.experiment.sampleApi.get(row.id).then((res: any) => {
+		form.value = res.data
+		form.value.sampleStatus = 2
+		modalRef.value.changePrefixTitle("")
+		modalRef.value.show()
+	})
+}
+
+/** 删除按钮操作 */
+function handleDelete(rows: any[]) {
+	tableRef.value.defaultHandleFuns.handleDelete("", rows)
+}
+
+/** 提交按钮 */
+function submitForm() {
+	if (modalType.value == "C" || modalType.value == "U") {
+		apis.experiment.sampleApi.addOrUpdate(form.value).then(() => {
+			handleQuery()
+		})
+	} else if (modalType.value == "S") {
+		apis.experiment.sampleApi.smaple(form.value).then(() => {
+			message.msgSuccess("取样成功")
+			handleQuery()
+		})
+	}
+}
+
+function checkBtnShow(btn: any, row: any) {
+	if (props.actions.includes(btn)) {
+		return true
+	}
+	return false
+}
+
+const chickenModalRef = ref()
+const chickenModalSelectType = ref()
+function handleOpenChickenModal(type: string) {
+	chickenModalSelectType.value = type
+	if (chickenModalSelectType.value == "search") {
+		queryParams.value.chickenId = undefined
+		queryParams.value.wingTagNum = ""
+		tableRef.value.setQueryParams(queryParams.value)
+	}
+	chickenModalRef.value.open()
+}
+function onChickenConfirm(data: any) {
+	if (chickenModalSelectType.value == "search") {
+		queryParams.value.chickenId = data[0].id
+		queryParams.value.wingTagNum = data[0].wingTagNum
+
+		handleQuery(queryParams.value)
+	} else if (chickenModalSelectType.value === "form") {
+		console.log("data", data)
+		form.value.chickenId = data[0].id
+		form.value.wingTagNum = data[0].wingTagNum
+		form.value.batchNum = data[0].batchNum
+	}
+}
+
+function handleDestory(row) {
+	message.confirm("确定要销毁该样本吗?").then(() => {
+		apis.experiment.sampleApi.destroy(row.id).then(() => {
+			handleQuery()
+		})
+	})
+}
+const flowModalRef = ref()
+const flowData = ref()
+function handleFlow(row) {
+	apis.experiment.sampleApi.queryFlowLogs(row.id).then((res: any) => {
+		flowData.value = res.data
+		flowModalRef.value.show()
+	})
+}
+const qrModalRef = ref()
+const qrModalData = ref()
+const qrCode = ref({
+	logo: "/media/logo.png",
+	size: 300,
+	colorDark: "#0e9489",
+	text: ""
+})
+function handleQrCode(row) {
+	qrModalData.value = row
+	qrCode.value.text = `vb@sample@/sample/${row.id}`
+	qrModalRef.value.show()
+}
+function handleDownloadQr(id: string) {
+	let myImg = document.querySelector("#" + id + " img") as HTMLImageElement
+	let url = myImg?.src
+	let a = document.createElement("a")
+	a.href = url
+	a.download = `${qrModalData.value.taskName}签到二维码`
+	a.click()
+}
+// function handlePrintQr(id: string) {
+// 	const printContent = document.querySelector("#" + id)
+// 	if (!printContent) return
+
+// 	// 创建打印样式
+// 	const style = document.createElement("style")
+// 	style.innerHTML = `
+// 		@media print {
+// 			body * {
+// 				display: none !important;
+//       }
+// 			#print-area, #print-area * {
+// 				display: block !important;
+// 			}
+// 			#print-area {
+// 				position: absolute;
+// 				top: 0;
+// 				left: 0;
+//         width: 100vw;
+//         height: 600px;
+// 				display: flex !important;
+// 				flex-direction: column;
+// 				align-items: center;
+// 				justify-content: center;
+// 				font-family: Arial, sans-serif;
+// 			}
+
+// 		}
+// 	`
+// 	document.head.appendChild(style)
+
+// 	// 创建打印区域
+// 	const printArea = document.createElement("div")
+// 	printArea.id = "print-area"
+// 	// printArea.innerHTML = `
+// 	// 	<div style="width:100%;display:flex;flex-direction: column;align-items: center;justify-content: center;">${printContent.innerHTML}</div>
+// 	// `
+// 	printArea.innerHTML = printContent.innerHTML
+// 	document.body.appendChild(printArea)
+
+// 	// 打印
+// 	window.print()
+
+// 	// 清理
+// 	setTimeout(() => {
+// 		document.head.removeChild(style)
+// 		document.body.removeChild(printArea)
+// 	}, 500)
+// }
+</script>
+<template>
+	<div class="app-container">
+		<VbDataTable
+			ref="tableRef"
+			keyField="id"
+			:columns="[]"
+			:columns-fun="opts.columns"
+			:handle-perm="opts.permission"
+			:handle-btns="opts.handleBtns"
+			:handle-funs="opts.handleFuns"
+			:search-form-items="opts.searchFormItems"
+			:custom-btns="opts.customBtns"
+			:remote-fun="opts.tableListFun"
+			:get-entity-fun="opts.getEntityFun"
+			:delete-entity-fun="opts.deleteEntityFun"
+			:export-url="opts.exportUrl"
+			:export-name="opts.exportName"
+			:modal="modalRef"
+			:reset-form-fun="opts.resetForm"
+			v-model:form-data="form"
+			v-model:query-params="queryParams"
+			:check-multiple="true"
+			:reset-search-form-fun="resetQuery"
+			:custom-search-fun="handleQuery">
+			<template #sampleType="{ row }">
+				<DictTag
+					type="experiment_sample_type"
+					:value-is-number="1"
+					:value="row.sampleType"></DictTag>
+			</template>
+			<template #sampleStatus="{ row }">
+				<DictTag
+					type="experiment_sample_status"
+					:value-is-number="1"
+					:value="row.sampleStatus"></DictTag>
+			</template>
+			<template #sampleTime="{ row }">
+				<template v-if="row.sampleTime">
+					{{ dayjs(row.sampleTime).format("YYYY/MM/DD HH:mm:ss") }}
+				</template>
+				<template v-else>-</template>
+			</template>
+			<template #actions="{ row }">
+				<template v-if="row.sampleStatus == 1">
+					<vb-tooltip v-if="checkBtnShow('update', row)" content="修改" placement="top">
+						<el-button
+							link
+							type="primary"
+							@click="handleUpdate(row)"
+							v-hasPermission="'experiment:sample:edit'">
+							<template #icon>
+								<VbIcon icon-name="notepad-edit" icon-type="duotone" class="fs-3"></VbIcon>
+							</template>
+						</el-button>
+					</vb-tooltip>
+
+					<vb-tooltip v-if="checkBtnShow('sample', row)" content="取样" placement="top">
+						<el-button link type="success" @click="handleSample(row)">
+							<template #icon>
+								<VbIcon icon-name="shield-tick" icon-type="duotone" class="fs-3"></VbIcon>
+							</template>
+						</el-button>
+					</vb-tooltip>
+				</template>
+				<template v-if="row.sampleStatus == 2 || row.sampleStatus == 3">
+					<vb-tooltip v-if="checkBtnShow('flow', row)" content="流转记录" placement="top">
+						<el-button link type="primary" @click="handleFlow(row)">
+							<template #icon>
+								<VbIcon icon-name="book" icon-type="duotone" class="fs-3"></VbIcon>
+							</template>
+						</el-button>
+					</vb-tooltip>
+					<vb-tooltip v-if="checkBtnShow('qrCode', row)" content="生成二维码" placement="top">
+						<el-button link type="success" @click="handleQrCode(row)">
+							<template #icon>
+								<VbIcon icon-name="scan-barcode" icon-type="duotone" class="fs-3"></VbIcon>
+							</template>
+						</el-button>
+					</vb-tooltip>
+				</template>
+				<template v-if="row.sampleStatus == 2">
+					<vb-tooltip v-if="checkBtnShow('destroy', row)" content="销毁" placement="top">
+						<el-button
+							link
+							type="danger"
+							@click="handleDestory(row)"
+							v-hasPermission="'experiment:sample:destroy'">
+							<template #icon>
+								<VbIcon icon-name="trash-square" icon-type="duotone" class="fs-3"></VbIcon>
+							</template>
+						</el-button>
+					</vb-tooltip>
+				</template>
+				<template v-if="row.sampleStatus == 4">
+					<vb-tooltip v-if="checkBtnShow('delete', row)" content="删除" placement="top">
+						<el-button
+							link
+							type="danger"
+							@click="handleDelete([row])"
+							v-hasPermission="'experiment:sample:remove'">
+							<template #icon>
+								<VbIcon icon-name="trash-square" icon-type="duotone" class="fs-3"></VbIcon>
+							</template>
+						</el-button>
+					</vb-tooltip>
+				</template>
+			</template>
+		</VbDataTable>
+		<VbModal
+			v-model:modal="modalRef"
+			:title="modalTitle"
+			:form-data="form"
+			:form-items="opts.formItems"
+			:label-width="opts.labelWidth"
+			append-to-body
+			@confirm="submitForm"></VbModal>
+
+		<ChickenModal
+			ref="chickenModalRef"
+			modal-title="请选择翅号"
+			:multiple="false"
+			@confirm="onChickenConfirm"></ChickenModal>
+
+		<VbModal
+			v-model:modal="qrModalRef"
+			title="样品二维码"
+			:confirm-btn="false"
+			:close-btn-class="'btn btn-danger'"
+			append-to-body>
+			<template #body>
+				<div
+					id="qr"
+					class="w-100 w-100 d-flex flex-column justify-content-center align-items-center">
+					<span class="fs-5 fw-bold my-5 qr-title">{{ qrModalData?.sampleName }} 样品二维码</span>
+					<vue-qr
+						:text="qrCode.text"
+						:size="200"
+						:logoSrc="qrCode.logo"
+						:logoCornerRadius="`50%`"
+						:color-dark="qrCode.colorDark"
+						:width="qrCode.size"
+						:height="qrCode.size"></vue-qr>
+				</div>
+				<div class="text-center">
+					<el-button type="primary" class="mx-5 w-150px" @click="handleDownloadQr('qr')">
+						保存
+					</el-button>
+					<!-- <el-button type="primary" class="mx-5 w-150px" @click="handlePrintQr('qr')">
+						打印
+					</el-button> -->
+				</div>
+			</template>
+		</VbModal>
+
+		<VbModal
+			v-model:modal="flowModalRef"
+			title="样品流转记录"
+			:confirm-btn="false"
+			:close-btn-class="'btn btn-danger'"
+			append-to-body>
+			<template #body></template>
+		</VbModal>
+	</div>
+</template>

+ 22 - 0
UI/VB.VUE/src/views/experiment/sample/index.vue

@@ -0,0 +1,22 @@
+<script setup lang="ts" name="Sample">
+import appStore from "@s"
+import Sample from "./_sample.vue"
+
+const customBtns = [
+	{
+		key: "handleCreate",
+		name: "创建样本",
+		icon: "bi bi-plus-square",
+		disabledFun: () => false
+	}
+]
+const actions = ["update", "delete", "sample", "destroy", "flow", "qrCode"]
+</script>
+<template>
+	<div class="app-container">
+		<Sample
+			:create-by="appStore.authStore.user.userId"
+			:actions="actions as any"
+			:custom-btns="customBtns" />
+	</div>
+</template>

+ 86 - 0
UI/VB.VUE/src/views/experiment/sample/sampleFlow.vue

@@ -0,0 +1,86 @@
+<script setup lang="ts">
+import apis from "@a"
+import dayjs from "dayjs"
+
+const sampleData = ref()
+const sampleId = computed(() => {
+	return useRoute().params.id
+})
+function getSample(id) {
+	apis.experiment.sampleApi.querySample(id).then((res) => {
+		sampleData.value = res.data
+	})
+}
+function init() {
+	getSample(sampleId.value)
+}
+onMounted(init)
+</script>
+
+<template>
+	<div class="app-container p-10">
+		<el-card v-if="sampleData" shadow="always" header="样品详细信息">
+			<template #header>
+				<div class="header text-primary fw-bold text-center fs-2">样品详细信息</div>
+			</template>
+
+			<dl class="sample-info">
+				<dt>样品名称:</dt>
+				<dd>{{ sampleData?.sampleName }}</dd>
+			</dl>
+			<dl class="sample-info">
+				<dt>样品状态:</dt>
+				<dd>
+					<DictTag
+						type="experiment_sample_status"
+						:value-is-number="1"
+						:value="sampleData?.sampleStatus"></DictTag>
+				</dd>
+			</dl>
+			<dl class="sample-info">
+				<dt>样品类型:</dt>
+				<dd>
+					<DictTag
+						type="experiment_sample_type"
+						:value-is-number="1"
+						:value="sampleData?.sampleType"></DictTag>
+				</dd>
+			</dl>
+			<dl class="sample-info">
+				<dt>取样批次:</dt>
+				<dd>{{ sampleData?.batchNum }}</dd>
+			</dl>
+			<dl class="sample-info">
+				<dt>取样翅号:</dt>
+				<dd>{{ sampleData?.wingTagNum }}</dd>
+			</dl>
+			<dl class="sample-info">
+				<dt>取样时间:</dt>
+				<dd>{{ dayjs(sampleData?.sampleTime).format("YYYY/MM/DD HH:mm:ss") }}</dd>
+			</dl>
+			<dl class="sample-info">
+				<dt>样品描述:</dt>
+				<dd>{{ sampleData?.description }}</dd>
+			</dl>
+			<dl class="sample-info">
+				<dt>取样人:</dt>
+				<dd>{{ sampleData?.createByName }}</dd>
+			</dl>
+		</el-card>
+	</div>
+</template>
+
+<style lang="scss" scoped>
+.sample-info {
+	display: flex;
+	margin-bottom: 10px;
+	dt {
+		width: 80px;
+		text-align: right;
+		margin-right: 10px;
+	}
+	dd {
+		flex: 1;
+	}
+}
+</style>

File diff suppressed because it is too large
+ 0 - 296
UI/VB.VUE/vite.config.ts.timestamp-1751524516784-ff27d13c9582a.mjs


File diff suppressed because it is too large
+ 0 - 302
UI/VB.VUE/vite.config.ts.timestamp-1761050177294-53d1d8a12451c.mjs


File diff suppressed because it is too large
+ 0 - 302
UI/VB.VUE/vite.config.ts.timestamp-1761120477090-c28d50e98d6ac.mjs


Some files were not shown because too many files changed in this diff