tuqin
2026-10-29 6ebee7b1e79706758f6a285f2338dbf617ac0708
feat(LIMS): 样品/实验/数据接口三模块补业务逻辑

按第二批需求实现,6 张新表 + 6 组 CRUD + 3 处业务增强。

样品管理:
- 新增 lims_sample_stock_record(出入库流水) / lims_sample_status_log(状态变更历史)
- SampleController 加 /outStock /inStock /changeStatus /detail/{id}
- 出库校验库存、自动算剩余量、写流水、状态变更留痕;新增样品自动补一条入库流水
- 详情接口返回 样品+流水+状态历史

实验管理:
- 新增 lims_experiment_step(流程节点SOP) / lims_experiment_resource(资源分配)
- ExperimentController 加 /step/saveBatch /step/complete /assignResource /stepTemplate /detail/{id}
- 资源分配时如果是试剂,真实扣减 lims_reagent.current_stock 并写 reagent_record 流水
- 步骤全部完成自动把实验状态置为 completed,否则 inProgress
- 内置化学分析/物理测试/微生物检测三套 SOP 模板
- 删除实验连带清步骤与资源

数据接口管理:
- 新增 lims_interface_field(字段映射) / lims_interface_log(调用日志)
- DataInterfaceController 加 /test/{id} /collect/{id} /fields/{id} /field/saveBatch /logs
- 测试连接支持 HTTP(OkHttp) 与 TCP(Socket),MQTT 因离线环境无客户端库返回明确提示
- 采集流程:按协议拉数据 -> 按字段映射(JSON路径+转换系数)解析 -> 写 lims_data_collection -> 记日志
- 转换支持 multiply/divide/offset/map 四种

修复:
- 6 个 ServiceImpl 的 listPage 原先只按字符串字段 like,补上 reagentId/sampleId/
experimentId/interfaceId/instrumentId 的 eq 精确过滤,否则按父ID查子表返回全量

本地已端到端验证:样品出入库(库存1000->700->0)、超量拦截、状态历史留痕;
实验模板6步+试剂扣库存100->90+步骤完成推进进度17%;接口测试连接79ms、
采集解析出25.6并写采集记录与日志。
已添加31个文件
已修改5个文件
2649 ■■■■■ 文件已修改
doc/sql/lims_batch2_modules.sql 192 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/DataInterfaceController.java 466 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/ExperimentController.java 346 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/ExperimentResourceController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/ExperimentStepController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/InterfaceFieldController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/InterfaceLogController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/SampleController.java 312 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/SampleStatusLogController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/controller/SampleStockRecordController.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/mapper/ExperimentResourceMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/mapper/ExperimentStepMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/mapper/InterfaceFieldMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/mapper/InterfaceLogMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/mapper/SampleStatusLogMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/mapper/SampleStockRecordMapper.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/pojo/ExperimentResource.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/pojo/ExperimentStep.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/pojo/InterfaceField.java 84 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/pojo/InterfaceLog.java 92 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/pojo/SampleStatusLog.java 80 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/pojo/SampleStockRecord.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/ExperimentResourceService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/ExperimentStepService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/InterfaceFieldService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/InterfaceLogService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/SampleStatusLogService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/SampleStockRecordService.java 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/ExperimentResourceServiceImpl.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/ExperimentStepServiceImpl.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/InstrumentCalibrationServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/InterfaceFieldServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/InterfaceLogServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/ReagentRecordServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/SampleStatusLogServiceImpl.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/lims/service/impl/SampleStockRecordServiceImpl.java 51 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/sql/lims_batch2_modules.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,192 @@
-- ============================================================
-- èН坼-LIMS ç¬¬äºŒæ‰¹éœ€æ±‚ DDL
--   â‘  æ ·å“ç®¡ç†è¡¥å¼ºï¼šå‡ºå…¥åº“流水 + çŠ¶æ€å˜æ›´åŽ†å²
--   â‘¡ å®žéªŒç®¡ç†è¡¥å¼ºï¼šæµç¨‹èŠ‚ç‚¹(SOP) + èµ„源分配
--   â‘¢ æ•°æ®æŽ¥å£ç®¡ç†è¡¥å¼ºï¼šå­—段映射 + è°ƒç”¨æ—¥å¿—
-- åº“:lims-ruoyi
-- ============================================================
-- ============================================================
-- â‘  æ ·å“å‡ºå…¥åº“流水
-- ============================================================
DROP TABLE IF EXISTS `lims_sample_stock_record`;
CREATE TABLE `lims_sample_stock_record` (
  `id`             BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `sample_id`      BIGINT        DEFAULT NULL COMMENT '样品ID(lims_sample.id)',
  `sample_code`    VARCHAR(64)   DEFAULT NULL COMMENT '样品编号(冗余)',
  `sample_name`    VARCHAR(255)  DEFAULT NULL COMMENT '样品名称(冗余)',
  `record_type`    VARCHAR(32)   DEFAULT NULL COMMENT '类型(in-入库,out-出库,destroy-销毁,return-退库,transfer-转移)',
  `quantity`       DECIMAL(18,4) DEFAULT NULL COMMENT '数量',
  `unit`           VARCHAR(32)   DEFAULT NULL COMMENT '单位',
  `before_status`  VARCHAR(32)   DEFAULT NULL COMMENT '操作前状态',
  `after_status`   VARCHAR(32)   DEFAULT NULL COMMENT '操作后状态',
  `from_location`  VARCHAR(255)  DEFAULT NULL COMMENT '原存储位置',
  `to_location`    VARCHAR(255)  DEFAULT NULL COMMENT '去向/新存储位置',
  `operator`       VARCHAR(64)   DEFAULT NULL COMMENT '经手人',
  `operate_time`   DATETIME      DEFAULT NULL COMMENT '操作时间',
  `recipient`      VARCHAR(64)   DEFAULT NULL COMMENT '领用人/接收方',
  `purpose`        VARCHAR(500)  DEFAULT NULL COMMENT '用途/出库事由',
  `related_experiment` VARCHAR(128) DEFAULT NULL COMMENT '关联实验编号',
  `remark`         VARCHAR(1000) DEFAULT NULL COMMENT '备注',
  `del_flag`       CHAR(1)       DEFAULT '0' COMMENT '删除标志(0-正常,2-删除)',
  `tenant_id`      BIGINT        DEFAULT NULL COMMENT '租户ID',
  `create_time`    DATETIME      DEFAULT NULL COMMENT '创建时间',
  `create_user`    INT           DEFAULT NULL COMMENT '创建用户',
  `update_time`    DATETIME      DEFAULT NULL COMMENT '修改时间',
  `update_user`    INT           DEFAULT NULL COMMENT '修改用户',
  PRIMARY KEY (`id`),
  KEY `idx_sample_id` (`sample_id`),
  KEY `idx_operate_time` (`operate_time`),
  KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LIMS-样品出入库流水';
-- ============================================================
-- â‘  æ ·å“çŠ¶æ€å˜æ›´åŽ†å²
-- ============================================================
DROP TABLE IF EXISTS `lims_sample_status_log`;
CREATE TABLE `lims_sample_status_log` (
  `id`            BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `sample_id`     BIGINT        DEFAULT NULL COMMENT '样品ID',
  `sample_code`   VARCHAR(64)   DEFAULT NULL COMMENT '样品编号(冗余)',
  `from_status`   VARCHAR(32)   DEFAULT NULL COMMENT '变更前状态',
  `to_status`     VARCHAR(32)   DEFAULT NULL COMMENT '变更后状态',
  `change_reason` VARCHAR(500)  DEFAULT NULL COMMENT '变更原因',
  `operator`      VARCHAR(64)   DEFAULT NULL COMMENT '操作人',
  `operate_time`  DATETIME      DEFAULT NULL COMMENT '操作时间',
  `remark`        VARCHAR(1000) DEFAULT NULL COMMENT '备注',
  `del_flag`      CHAR(1)       DEFAULT '0' COMMENT '删除标志(0-正常,2-删除)',
  `tenant_id`     BIGINT        DEFAULT NULL COMMENT '租户ID',
  `create_time`   DATETIME      DEFAULT NULL COMMENT '创建时间',
  `create_user`   INT           DEFAULT NULL COMMENT '创建用户',
  `update_time`   DATETIME      DEFAULT NULL COMMENT '修改时间',
  `update_user`   INT           DEFAULT NULL COMMENT '修改用户',
  PRIMARY KEY (`id`),
  KEY `idx_sample_id` (`sample_id`),
  KEY `idx_operate_time` (`operate_time`),
  KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LIMS-样品状态变更历史';
-- ============================================================
-- â‘¡ å®žéªŒæµç¨‹èŠ‚ç‚¹ï¼ˆSOP æ­¥éª¤ï¼‰
-- ============================================================
DROP TABLE IF EXISTS `lims_experiment_step`;
CREATE TABLE `lims_experiment_step` (
  `id`                BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `experiment_id`     BIGINT        DEFAULT NULL COMMENT '实验ID(lims_experiment.id)',
  `experiment_code`   VARCHAR(64)   DEFAULT NULL COMMENT '实验编号(冗余)',
  `step_no`           INT           DEFAULT NULL COMMENT '步骤序号(从1开始)',
  `step_name`         VARCHAR(255)  DEFAULT NULL COMMENT '步骤名称',
  `step_content`      TEXT          COMMENT '标准操作内容(SOP)',
  `expected_duration` INT           DEFAULT NULL COMMENT '预计耗时(分钟)',
  `executor`          VARCHAR(64)   DEFAULT NULL COMMENT '执行人',
  `plan_start_time`   DATETIME      DEFAULT NULL COMMENT '计划开始时间',
  `plan_end_time`     DATETIME      DEFAULT NULL COMMENT '计划结束时间',
  `actual_start_time` DATETIME      DEFAULT NULL COMMENT '实际开始时间',
  `actual_end_time`   DATETIME      DEFAULT NULL COMMENT '实际结束时间',
  `step_status`       VARCHAR(32)   DEFAULT 'pending' COMMENT '状态(pending-待执行,running-执行中,completed-已完成,skipped-已跳过,failed-失败)',
  `step_result`       TEXT          COMMENT '执行结果/记录',
  `remark`            VARCHAR(1000) DEFAULT NULL COMMENT '备注',
  `del_flag`          CHAR(1)       DEFAULT '0' COMMENT '删除标志(0-正常,2-删除)',
  `tenant_id`         BIGINT        DEFAULT NULL COMMENT '租户ID',
  `create_time`       DATETIME      DEFAULT NULL COMMENT '创建时间',
  `create_user`       INT           DEFAULT NULL COMMENT '创建用户',
  `update_time`       DATETIME      DEFAULT NULL COMMENT '修改时间',
  `update_user`       INT           DEFAULT NULL COMMENT '修改用户',
  PRIMARY KEY (`id`),
  KEY `idx_experiment_id` (`experiment_id`),
  KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LIMS-实验流程节点';
-- ============================================================
-- â‘¡ å®žéªŒèµ„源分配
-- ============================================================
DROP TABLE IF EXISTS `lims_experiment_resource`;
CREATE TABLE `lims_experiment_resource` (
  `id`                BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `experiment_id`     BIGINT        DEFAULT NULL COMMENT '实验ID',
  `experiment_code`   VARCHAR(64)   DEFAULT NULL COMMENT '实验编号(冗余)',
  `resource_type`     VARCHAR(32)   DEFAULT NULL COMMENT '资源类型(instrument-仪器设备,reagent-试剂耗材,personnel-人员)',
  `resource_id`       BIGINT        DEFAULT NULL COMMENT '资源ID(仪器/试剂表主键)',
  `resource_code`     VARCHAR(64)   DEFAULT NULL COMMENT '资源编号',
  `resource_name`     VARCHAR(255)  DEFAULT NULL COMMENT '资源名称',
  `quantity`          DECIMAL(18,4) DEFAULT NULL COMMENT '数量(试剂消耗量/人员工时)',
  `unit`              VARCHAR(32)   DEFAULT NULL COMMENT '单位',
  `usage_time`        VARCHAR(255)  DEFAULT NULL COMMENT '使用时段/占用时间',
  `status`            VARCHAR(32)   DEFAULT 'planned' COMMENT '状态(planned-已安排,inuse-使用中,returned-已归还,consumed-已消耗)',
  `remark`            VARCHAR(1000) DEFAULT NULL COMMENT '备注',
  `del_flag`          CHAR(1)       DEFAULT '0' COMMENT '删除标志(0-正常,2-删除)',
  `tenant_id`         BIGINT        DEFAULT NULL COMMENT '租户ID',
  `create_time`       DATETIME      DEFAULT NULL COMMENT '创建时间',
  `create_user`       INT           DEFAULT NULL COMMENT '创建用户',
  `update_time`       DATETIME      DEFAULT NULL COMMENT '修改时间',
  `update_user`       INT           DEFAULT NULL COMMENT '修改用户',
  PRIMARY KEY (`id`),
  KEY `idx_experiment_id` (`experiment_id`),
  KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LIMS-实验资源分配';
-- ============================================================
-- â‘¢ æŽ¥å£å­—段映射
-- ============================================================
DROP TABLE IF EXISTS `lims_interface_field`;
CREATE TABLE `lims_interface_field` (
  `id`              BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `interface_id`    BIGINT        DEFAULT NULL COMMENT '接口ID(lims_data_interface.id)',
  `interface_code`  VARCHAR(64)   DEFAULT NULL COMMENT '接口编码(冗余)',
  `source_field`    VARCHAR(128)  DEFAULT NULL COMMENT '设备返回的字段名(如 temp / data.value)',
  `source_path`     VARCHAR(255)  DEFAULT NULL COMMENT 'JSON路径(如 $.data.temperature)',
  `target_field`    VARCHAR(64)   DEFAULT NULL COMMENT '目标字段(collectionValue/deviceName/dataType/unit/collectionTime/rawData)',
  `transform_type`  VARCHAR(32)   DEFAULT 'none' COMMENT '转换方式(none-直接,multiply-乘以系数,offset-加偏移,divide-除以系数,map-字典映射)',
  `transform_value` VARCHAR(255)  DEFAULT NULL COMMENT '转换参数(系数/偏移/映射表JSON)',
  `default_value`   VARCHAR(255)  DEFAULT NULL COMMENT '取不到时的默认值',
  `sort_no`         INT           DEFAULT NULL COMMENT '排序',
  `remark`          VARCHAR(500)  DEFAULT NULL COMMENT '备注',
  `del_flag`        CHAR(1)       DEFAULT '0' COMMENT '删除标志(0-正常,2-删除)',
  `tenant_id`       BIGINT        DEFAULT NULL COMMENT '租户ID',
  `create_time`     DATETIME      DEFAULT NULL COMMENT '创建时间',
  `create_user`     INT           DEFAULT NULL COMMENT '创建用户',
  `update_time`     DATETIME      DEFAULT NULL COMMENT '修改时间',
  `update_user`     INT           DEFAULT NULL COMMENT '更新用户',
  PRIMARY KEY (`id`),
  KEY `idx_interface_id` (`interface_id`),
  KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LIMS-接口字段映射';
-- ============================================================
-- â‘¢ æŽ¥å£è°ƒç”¨æ—¥å¿—
-- ============================================================
DROP TABLE IF EXISTS `lims_interface_log`;
CREATE TABLE `lims_interface_log` (
  `id`             BIGINT        NOT NULL AUTO_INCREMENT COMMENT '主键ID',
  `interface_id`   BIGINT        DEFAULT NULL COMMENT '接口ID',
  `interface_code` VARCHAR(64)   DEFAULT NULL COMMENT '接口编码(冗余)',
  `interface_name` VARCHAR(255)  DEFAULT NULL COMMENT '接口名称(冗余)',
  `invoke_type`    VARCHAR(32)   DEFAULT NULL COMMENT '调用方式(test-测试连接,manual-手动采集,auto-定时采集)',
  `request_data`   MEDIUMTEXT    COMMENT '请求内容',
  `response_data`  MEDIUMTEXT    COMMENT '响应内容',
  `success`        CHAR(1)       DEFAULT '0' COMMENT '是否成功(1-成功,0-失败)',
  `error_msg`      VARCHAR(2000) DEFAULT NULL COMMENT '错误信息',
  `duration_ms`    INT           DEFAULT NULL COMMENT '耗时(毫秒)',
  `record_count`   INT           DEFAULT NULL COMMENT '本次写入采集记录条数',
  `operator`       VARCHAR(64)   DEFAULT NULL COMMENT '触发人',
  `invoke_time`    DATETIME      DEFAULT NULL COMMENT '调用时间',
  `del_flag`       CHAR(1)       DEFAULT '0' COMMENT '删除标志(0-正常,2-删除)',
  `tenant_id`      BIGINT        DEFAULT NULL COMMENT '租户ID',
  `create_time`    DATETIME      DEFAULT NULL COMMENT '创建时间',
  `create_user`    INT           DEFAULT NULL COMMENT '创建用户',
  `update_time`    DATETIME      DEFAULT NULL COMMENT '修改时间',
  `update_user`    INT           DEFAULT NULL COMMENT '更新用户',
  PRIMARY KEY (`id`),
  KEY `idx_interface_id` (`interface_id`),
  KEY `idx_invoke_time` (`invoke_time`),
  KEY `idx_tenant` (`tenant_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='LIMS-接口调用日志';
-- ============================================================
-- æ ¡éªŒ
-- ============================================================
SELECT table_name, table_comment
FROM information_schema.tables
WHERE table_schema = 'lims-ruoyi'
  AND table_name IN ('lims_sample_stock_record','lims_sample_status_log','lims_experiment_step','lims_experiment_resource','lims_interface_field','lims_interface_log')
ORDER BY table_name;
src/main/java/com/ruoyi/lims/controller/DataInterfaceController.java
@@ -1,19 +1,42 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.lims.pojo.DataInterface;
import com.ruoyi.lims.service.DataInterfaceService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;
import lombok.AllArgsConstructor;
import io.swagger.annotations.ApiOperation;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.lims.pojo.DataCollection;
import com.ruoyi.lims.pojo.DataInterface;
import com.ruoyi.lims.pojo.InterfaceField;
import com.ruoyi.lims.pojo.InterfaceLog;
import com.ruoyi.lims.service.DataCollectionService;
import com.ruoyi.lims.service.DataInterfaceService;
import com.ruoyi.lims.service.InterfaceFieldService;
import com.ruoyi.lims.service.InterfaceLogService;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.io.InputStream;
import java.io.OutputStream;
import java.math.BigDecimal;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@RestController
@AllArgsConstructor
@@ -22,6 +45,17 @@
public class DataInterfaceController {
    private DataInterfaceService dataInterfaceservice;
    private InterfaceFieldService fieldService;
    private InterfaceLogService logService;
    private DataCollectionService collectionService;
    private static final ObjectMapper JSON = new ObjectMapper();
    private static final OkHttpClient HTTP = new OkHttpClient.Builder()
            .connectTimeout(8, TimeUnit.SECONDS)
            .readTimeout(15, TimeUnit.SECONDS)
            .writeTimeout(15, TimeUnit.SECONDS)
            .build();
    @GetMapping("/listPage")
    @ApiOperation("分页查询所有数据接口管理表")
@@ -52,6 +86,418 @@
    @ApiOperation("删除数据接口管理表")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
        return AjaxResult.success(dataInterfaceservice.removeBatchByIds(ids));
        boolean ok = dataInterfaceservice.removeBatchByIds(ids);
        if (ok && !CollectionUtils.isEmpty(ids)) {
            QueryWrapper<InterfaceField> fw = new QueryWrapper<>();
            fw.in("interface_id", ids);
            fieldService.remove(fw);
        }
        return AjaxResult.success(ok);
    }
    /* ==================== æµ‹è¯•连接 ==================== */
    /**
     * æµ‹è¯•连接:HTTP å‘一次 GET/HEAD,TCP å»ºä¸€æ¬¡è¿žæŽ¥ï¼ŒMQTT å½“前环境不支持
     */
    @PostMapping("/test/{id}")
    @ApiOperation("测试接口连通性")
    public AjaxResult testConnection(@PathVariable Long id) {
        DataInterface di = dataInterfaceservice.getById(id);
        if (di == null) {
            return AjaxResult.error("接口不存在");
        }
        long start = System.currentTimeMillis();
        String protocol = lower(di.getProtocol());
        String result;
        boolean ok = false;
        try {
            if ("http".equals(protocol) || "https".equals(protocol)) {
                String url = buildHttpUrl(di);
                Request req = new Request.Builder().url(url).get().build();
                try (Response resp = HTTP.newCall(req).execute()) {
                    ok = resp.isSuccessful();
                    result = "HTTP " + resp.code() + " " + resp.message()
                            + ",耗时 " + (System.currentTimeMillis() - start) + " ms";
                }
            } else if ("tcp".equals(protocol)) {
                String host = di.getConnectionUrl() == null ? "" : di.getConnectionUrl().trim();
                int port = parseInt(di.getPort(), 0);
                if (host.isEmpty() || port <= 0) {
                    return AjaxResult.error("TCP éœ€è¦å¡«å†™ä¸»æœºåœ°å€å’Œç«¯å£");
                }
                // http:// å‰ç¼€å…¼å®¹è¯¯å¡«
                if (host.startsWith("http://")) host = host.substring(7);
                if (host.startsWith("https://")) host = host.substring(8);
                if (host.contains("/")) host = host.substring(0, host.indexOf('/'));
                if (host.contains(":")) host = host.substring(0, host.indexOf(':'));
                try (Socket socket = new Socket()) {
                    socket.connect(new InetSocketAddress(host, port), 5000);
                    ok = true;
                    result = "TCP è¿žæŽ¥æˆåŠŸ " + host + ":" + port
                            + ",耗时 " + (System.currentTimeMillis() - start) + " ms";
                }
            } else if ("mqtt".equals(protocol)) {
                return AjaxResult.error("当前环境未集成 MQTT å®¢æˆ·ç«¯åº“(离线打包限制),"
                        + "如需 MQTT é‡‡é›†è¯·æŠŠ org.eclipse.paho.client.mqttv3 ä¾èµ–加入 pom åŽé‡æ–°æ‰“包");
            } else {
                return AjaxResult.error("暂不支持协议:" + di.getProtocol());
            }
        } catch (Exception e) {
            saveLog(di, "test", null, null, false, e.getMessage(), System.currentTimeMillis() - start, 0);
            return AjaxResult.error("连接失败:" + shortMsg(e));
        }
        saveLog(di, "test", null, result, ok, ok ? null : result, System.currentTimeMillis() - start, 0);
        return ok ? AjaxResult.success(result) : AjaxResult.error(result);
    }
    /* ==================== æ‰‹åЍ采集 ==================== */
    /**
     * æ‰‹åŠ¨é‡‡é›†ä¸€æ¬¡ï¼šæŒ‰åè®®æ‹‰æ•°æ® -> æŒ‰å­—段映射转换 -> å†™å…¥é‡‡é›†è®°å½• -> è®°æ—¥å¿—
     */
    @PostMapping("/collect/{id}")
    @ApiOperation("立即采集一次数据")
    public AjaxResult collect(@PathVariable Long id) {
        DataInterface di = dataInterfaceservice.getById(id);
        if (di == null) {
            return AjaxResult.error("接口不存在");
        }
        String protocol = lower(di.getProtocol());
        if ("mqtt".equals(protocol)) {
            return AjaxResult.error("当前环境未集成 MQTT å®¢æˆ·ç«¯åº“,暂不支持 MQTT é‡‡é›†");
        }
        long start = System.currentTimeMillis();
        String rawJson;
        String requestDesc;
        try {
            if ("http".equals(protocol) || "https".equals(protocol)) {
                String url = buildHttpUrl(di);
                requestDesc = "GET " + url;
                Request req = new Request.Builder().url(url).get().build();
                try (Response resp = HTTP.newCall(req).execute()) {
                    ResponseBody body = resp.body();
                    rawJson = body == null ? "" : body.string();
                    if (!resp.isSuccessful()) {
                        saveLog(di, "manual", requestDesc, rawJson, false,
                                "HTTP " + resp.code(), System.currentTimeMillis() - start, 0);
                        return AjaxResult.error("采集失败:HTTP " + resp.code());
                    }
                }
            } else if ("tcp".equals(protocol)) {
                String host = di.getConnectionUrl() == null ? "" : di.getConnectionUrl().trim();
                int port = parseInt(di.getPort(), 0);
                if (host.startsWith("http://")) host = host.substring(7);
                if (host.startsWith("https://")) host = host.substring(8);
                if (host.contains("/")) host = host.substring(0, host.indexOf('/'));
                if (host.contains(":")) host = host.substring(0, host.indexOf(':'));
                requestDesc = "TCP " + host + ":" + port;
                try (Socket socket = new Socket()) {
                    socket.connect(new InetSocketAddress(host, port), 5000);
                    socket.setSoTimeout(8000);
                    OutputStream out = socket.getOutputStream();
                    // é»˜è®¤å‘一个换行,很多仪器收到回车才吐数据
                    out.write("\r\n".getBytes(StandardCharsets.UTF_8));
                    out.flush();
                    InputStream in = socket.getInputStream();
                    byte[] buf = new byte[8192];
                    int n = in.read(buf);
                    rawJson = n <= 0 ? "" : new String(buf, 0, n, StandardCharsets.UTF_8).trim();
                }
            } else {
                return AjaxResult.error("暂不支持协议:" + di.getProtocol());
            }
        } catch (Exception e) {
            saveLog(di, "manual", null, null, false, shortMsg(e), System.currentTimeMillis() - start, 0);
            return AjaxResult.error("采集失败:" + shortMsg(e));
        }
        // å­—段映射
        QueryWrapper<InterfaceField> fw = new QueryWrapper<>();
        fw.eq("interface_id", di.getId()).orderByAsc("sort_no").orderByAsc("id");
        List<InterfaceField> fields = fieldService.list(fw);
        int saved = 0;
        List<String> warnings = new ArrayList<>();
        try {
            if (fields.isEmpty()) {
                // æ²¡æœ‰é…ç½®æ˜ å°„:整条原文放进 rawData,做个兜底记录
                DataCollection dc = new DataCollection();
                dc.setCollectionCode("AUTO-" + System.currentTimeMillis());
                dc.setDeviceName(di.getDeviceName());
                dc.setDeviceCode(di.getInterfaceCode());
                dc.setRawData(rawJson);
                dc.setCollectionTime(LocalDateTime.now());
                dc.setCollectionStatus("completed");
                dc.setDataQuality("pending");
                dc.setProcessMethod("autoClean");
                collectionService.save(dc);
                saved = 1;
                warnings.add("未配置字段映射,已把原始响应整体写入 rawData");
            } else {
                JsonNode root = null;
                try {
                    root = JSON.readTree(rawJson);
                } catch (Exception ignore) {
                    // ä¸æ˜¯ JSON ä¹Ÿå…è®¸ï¼ŒæŒ‰çº¯æ–‡æœ¬å¤„理
                }
                DataCollection dc = new DataCollection();
                dc.setCollectionCode("AUTO-" + System.currentTimeMillis());
                dc.setDeviceName(di.getDeviceName());
                dc.setDeviceCode(di.getInterfaceCode());
                dc.setRawData(rawJson);
                dc.setCollectionTime(LocalDateTime.now());
                dc.setCollectionStatus("completed");
                dc.setProcessMethod("dataTransform");
                for (InterfaceField f : fields) {
                    String target = f.getTargetField();
                    if (target == null) continue;
                    String val = extractValue(root, rawJson, f.getSourcePath(), f.getSourceField());
                    val = applyTransform(val, f);
                    if (val == null && f.getDefaultValue() != null) {
                        val = f.getDefaultValue();
                    }
                    if (val == null) continue;
                    switch (target) {
                        case "collectionValue":
                            try {
                                dc.setCollectionValue(new BigDecimal(val));
                            } catch (Exception e) {
                                warnings.add("字段 " + f.getSourceField() + " çš„值 " + val + " ä¸æ˜¯æ•°å­—,未写入采集数值");
                            }
                            break;
                        case "deviceName": dc.setDeviceName(val); break;
                        case "deviceCode": dc.setDeviceCode(val); break;
                        case "dataType": dc.setDataType(val); break;
                        case "unit": dc.setUnit(val); break;
                        case "rawData": dc.setRawData(val); break;
                        case "processedData": dc.setProcessedData(val); break;
                        default: break;
                    }
                }
                // è´¨é‡åˆ¤å®šï¼šé…ç½®äº†ä¸Šä¸‹é™å°±æ¯”一下,否则待校验
                dc.setDataQuality(dc.getCollectionValue() == null ? "pending" : "qualified");
                collectionService.save(dc);
                saved = 1;
            }
        } catch (Exception e) {
            saveLog(di, "manual", requestDesc, rawJson, false, "映射写入失败:" + shortMsg(e),
                    System.currentTimeMillis() - start, 0);
            return AjaxResult.error("映射写入失败:" + shortMsg(e));
        }
        saveLog(di, "manual", requestDesc, rawJson, true, null, System.currentTimeMillis() - start, saved);
        Map<String, Object> ret = new LinkedHashMap<>();
        ret.put("saved", saved);
        ret.put("rawLength", rawJson == null ? 0 : rawJson.length());
        ret.put("warnings", warnings);
        return AjaxResult.success("采集完成,写入 " + saved + " æ¡", ret);
    }
    /* ==================== å­—段映射管理 ==================== */
    @GetMapping("/fields/{interfaceId}")
    @ApiOperation("查询接口的字段映射")
    public AjaxResult listFields(@PathVariable Long interfaceId) {
        QueryWrapper<InterfaceField> w = new QueryWrapper<>();
        w.eq("interface_id", interfaceId).orderByAsc("sort_no").orderByAsc("id");
        return AjaxResult.success(fieldService.list(w));
    }
    @PostMapping("/field/saveBatch")
    @ApiOperation("批量保存字段映射(先删后插)")
    public AjaxResult saveFields(@RequestBody Map<String, Object> body) {
        Long interfaceId = toLong(body.get("interfaceId"));
        if (interfaceId == null) {
            return AjaxResult.error("interfaceId ä¸èƒ½ä¸ºç©º");
        }
        Object raw = body.get("fields");
        if (!(raw instanceof List)) {
            return AjaxResult.error("fields å¿…须是数组");
        }
        QueryWrapper<InterfaceField> dw = new QueryWrapper<>();
        dw.eq("interface_id", interfaceId);
        fieldService.remove(dw);
        int idx = 0;
        for (Object o : (List<?>) raw) {
            if (!(o instanceof Map)) continue;
            Map<?, ?> m = (Map<?, ?>) o;
            InterfaceField f = new InterfaceField();
            f.setInterfaceId(interfaceId);
            f.setInterfaceCode(str(m.get("interfaceCode")));
            f.setSourceField(str(m.get("sourceField")));
            f.setSourcePath(str(m.get("sourcePath")));
            f.setTargetField(str(m.get("targetField")));
            f.setTransformType(str(m.get("transformType")) == null ? "none" : str(m.get("transformType")));
            f.setTransformValue(str(m.get("transformValue")));
            f.setDefaultValue(str(m.get("defaultValue")));
            f.setSortNo(toInt(m.get("sortNo")) == null ? (idx + 1) : toInt(m.get("sortNo")));
            f.setRemark(str(m.get("remark")));
            fieldService.save(f);
            idx++;
        }
        return AjaxResult.success("已保存 " + idx + " æ¡æ˜ å°„");
    }
    /* ==================== è°ƒç”¨æ—¥å¿— ==================== */
    @GetMapping("/logs")
    @ApiOperation("查询接口调用日志")
    public AjaxResult logs(Page<InterfaceLog> page, InterfaceLog query) {
        QueryWrapper<InterfaceLog> w = new QueryWrapper<>();
        if (query != null) {
            if (query.getInterfaceId() != null) w.eq("interface_id", query.getInterfaceId());
            if (str(query.getInterfaceCode()) != null) w.like("interface_code", query.getInterfaceCode());
            if (str(query.getInvokeType()) != null) w.eq("invoke_type", query.getInvokeType());
            if (str(query.getSuccess()) != null) w.eq("success", query.getSuccess());
        }
        w.orderByDesc("invoke_time").orderByDesc("id");
        return AjaxResult.success(logService.page(page, w));
    }
    /* ==================== helpers ==================== */
    private String buildHttpUrl(DataInterface di) {
        String base = di.getConnectionUrl() == null ? "" : di.getConnectionUrl().trim();
        String port = di.getPort() == null ? "" : di.getPort().trim();
        if (!base.startsWith("http://") && !base.startsWith("https://")) {
            base = "http://" + base;
        }
        // åœ°å€é‡Œæ²¡å¸¦ç«¯å£æ—¶æ‰æ‹¼æŽ¥
        if (!port.isEmpty() && !base.matches(".*:\\d+.*")) {
            int idx = base.indexOf('/', base.indexOf("://") + 3);
            if (idx < 0) {
                base = base + ":" + port;
            } else {
                base = base.substring(0, idx) + ":" + port + base.substring(idx);
            }
        }
        return base;
    }
    private String extractValue(JsonNode root, String rawText, String path, String fieldName) {
        if (root != null && path != null && !path.trim().isEmpty()) {
            String p = path.trim();
            if (p.startsWith("$.")) p = p.substring(2);
            p = p.replace("/", ".");
            JsonNode node = root;
            for (String seg : p.split("\\.")) {
                if (seg.isEmpty()) continue;
                node = node.path(seg);
            }
            if (!node.isMissingNode() && !node.isNull()) {
                return node.asText();
            }
        }
        if (root != null && fieldName != null && !fieldName.trim().isEmpty()) {
            JsonNode node = root.path(fieldName.trim());
            if (!node.isMissingNode() && !node.isNull()) {
                return node.asText();
            }
        }
        // çº¯æ–‡æœ¬åè®®ï¼šæ•´ä¸ªå“åº”就是值
        if (root == null && fieldName != null && !fieldName.trim().isEmpty()) {
            return rawText;
        }
        return null;
    }
    private String applyTransform(String val, InterfaceField f) {
        if (val == null) return null;
        String type = lower(f.getTransformType());
        String tv = f.getTransformValue();
        if (type == null || "none".equals(type) || tv == null || tv.trim().isEmpty()) {
            return val;
        }
        try {
            BigDecimal num = new BigDecimal(val.trim());
            BigDecimal arg = new BigDecimal(tv.trim());
            switch (type) {
                case "multiply": return num.multiply(arg).stripTrailingZeros().toPlainString();
                case "divide":   return num.divide(arg, 6, BigDecimal.ROUND_HALF_UP).stripTrailingZeros().toPlainString();
                case "offset":   return num.add(arg).stripTrailingZeros().toPlainString();
                default: break;
            }
        } catch (Exception ignore) { }
        if ("map".equals(type)) {
            try {
                JsonNode m = JSON.readTree(tv);
                JsonNode hit = m.get(val);
                if (hit != null) return hit.asText();
            } catch (Exception ignore) { }
        }
        return val;
    }
    private void saveLog(DataInterface di, String type, String req, String resp,
                         boolean success, String err, long duration, int count) {
        try {
            InterfaceLog log = new InterfaceLog();
            log.setInterfaceId(di.getId());
            log.setInterfaceCode(di.getInterfaceCode());
            log.setInterfaceName(di.getInterfaceName());
            log.setInvokeType(type);
            log.setRequestData(req == null ? null : truncate(req, 8000));
            log.setResponseData(resp == null ? null : truncate(resp, 8000));
            log.setSuccess(success ? "1" : "0");
            log.setErrorMsg(err == null ? null : truncate(err, 1900));
            log.setDurationMs((int) duration);
            log.setRecordCount(count);
            log.setOperator(currentUser());
            log.setInvokeTime(LocalDateTime.now());
            logService.save(log);
        } catch (Exception ignore) {
            // æ—¥å¿—失败不影响主流程
        }
    }
    private static String truncate(String s, int max) {
        return s.length() <= max ? s : s.substring(0, max) + "...(truncated)";
    }
    private String currentUser() {
        try {
            return SecurityUtils.getUsername();
        } catch (Exception e) {
            return "system";
        }
    }
    private static String lower(String s) {
        return s == null ? null : s.trim().toLowerCase();
    }
    private static String shortMsg(Exception e) {
        String m = e.getMessage();
        if (m == null) m = e.getClass().getSimpleName();
        return m.length() > 300 ? m.substring(0, 300) : m;
    }
    private static String str(Object v) {
        if (v == null) return null;
        String s = String.valueOf(v).trim();
        return s.isEmpty() ? null : s;
    }
    private static Long toLong(Object v) {
        if (v == null) return null;
        try { return Long.valueOf(String.valueOf(v).trim()); } catch (NumberFormatException e) { return null; }
    }
    private static Integer toInt(Object v) {
        if (v == null) return null;
        try { return Integer.valueOf(String.valueOf(v).trim()); } catch (NumberFormatException e) { return null; }
    }
    private static int parseInt(String s, int def) {
        try { return Integer.parseInt(s.trim()); } catch (Exception e) { return def; }
    }
}
src/main/java/com/ruoyi/lims/controller/ExperimentController.java
@@ -1,17 +1,32 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.Experiment;
import com.ruoyi.lims.pojo.ExperimentResource;
import com.ruoyi.lims.pojo.ExperimentStep;
import com.ruoyi.lims.pojo.Reagent;
import com.ruoyi.lims.pojo.ReagentRecord;
import com.ruoyi.lims.service.ExperimentResourceService;
import com.ruoyi.lims.service.ExperimentService;
import com.ruoyi.lims.service.ExperimentStepService;
import com.ruoyi.lims.service.ReagentRecordService;
import com.ruoyi.lims.service.ReagentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
@AllArgsConstructor
@@ -20,6 +35,10 @@
public class ExperimentController {
    private ExperimentService experimentservice;
    private ExperimentStepService stepService;
    private ExperimentResourceService resourceService;
    private ReagentService reagentService;
    private ReagentRecordService reagentRecordService;
    @GetMapping("/listPage")
    @ApiOperation("分页查询所有实验管理表")
@@ -32,6 +51,39 @@
    @ApiOperation("根据ID查询实验管理表")
    public AjaxResult getExperiment(@PathVariable Long id) {
        return AjaxResult.success(experimentservice.getById(id));
    }
    /**
     * å®žéªŒè¯¦æƒ…:实验本体 + æµç¨‹èŠ‚ç‚¹ + å·²åˆ†é…èµ„源 + æ‰§è¡Œè¿›åº¦
     */
    @GetMapping("/detail/{id}")
    @ApiOperation("实验详情(含流程节点与资源分配)")
    public AjaxResult detail(@PathVariable Long id) {
        Experiment exp = experimentservice.getById(id);
        if (exp == null) {
            return AjaxResult.error("实验不存在");
        }
        QueryWrapper<ExperimentStep> sw = new QueryWrapper<>();
        sw.eq("experiment_id", id).orderByAsc("step_no").orderByAsc("id");
        List<ExperimentStep> steps = stepService.list(sw);
        QueryWrapper<ExperimentResource> rw = new QueryWrapper<>();
        rw.eq("experiment_id", id).orderByAsc("id");
        List<ExperimentResource> resources = resourceService.list(rw);
        int total = steps.size();
        long done = steps.stream().filter(s -> "completed".equals(s.getStepStatus())).count();
        int progress = total == 0 ? 0 : (int) Math.round(done * 100.0 / total);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("experiment", exp);
        result.put("steps", steps);
        result.put("resources", resources);
        result.put("stepTotal", total);
        result.put("stepCompleted", done);
        result.put("progress", progress);
        return AjaxResult.success(result);
    }
    @PostMapping("/add")
@@ -50,6 +102,298 @@
    @ApiOperation("删除实验管理表")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
        return AjaxResult.success(experimentservice.removeBatchByIds(ids));
        boolean ok = experimentservice.removeBatchByIds(ids);
        if (ok && !CollectionUtils.isEmpty(ids)) {
            // è¿žå¸¦æ¸…掉步骤与资源,避免孤儿数据
            QueryWrapper<ExperimentStep> sw = new QueryWrapper<>();
            sw.in("experiment_id", ids);
            stepService.remove(sw);
            QueryWrapper<ExperimentResource> rw = new QueryWrapper<>();
            rw.in("experiment_id", ids);
            resourceService.remove(rw);
        }
        return AjaxResult.success(ok);
    }
    /* ==================== æµç¨‹èŠ‚ç‚¹ï¼ˆSOP æ­¥éª¤ï¼‰ ==================== */
    /**
     * æ‰¹é‡ä¿å­˜æ­¥éª¤ï¼šä¼ å…¨é‡åˆ—表,先删后插,保证顺序和内容一致
     */
    @PostMapping("/step/saveBatch")
    @ApiOperation("批量保存实验流程节点")
    public AjaxResult saveSteps(@RequestBody Map<String, Object> body) {
        Long experimentId = toLong(body.get("experimentId"));
        if (experimentId == null) {
            return AjaxResult.error("experimentId ä¸èƒ½ä¸ºç©º");
        }
        Experiment exp = experimentservice.getById(experimentId);
        if (exp == null) {
            return AjaxResult.error("实验不存在");
        }
        Object rawList = body.get("steps");
        if (!(rawList instanceof List)) {
            return AjaxResult.error("steps å¿…须是数组");
        }
        QueryWrapper<ExperimentStep> dw = new QueryWrapper<>();
        dw.eq("experiment_id", experimentId);
        stepService.remove(dw);
        List<?> list = (List<?>) rawList;
        int idx = 0;
        for (Object o : list) {
            if (!(o instanceof Map)) continue;
            Map<?, ?> m = (Map<?, ?>) o;
            ExperimentStep step = new ExperimentStep();
            step.setExperimentId(experimentId);
            step.setExperimentCode(exp.getExperimentCode());
            step.setStepNo(toInt(m.get("stepNo")) == null ? (idx + 1) : toInt(m.get("stepNo")));
            step.setStepName(str(m.get("stepName")));
            step.setStepContent(str(m.get("stepContent")));
            step.setExpectedDuration(toInt(m.get("expectedDuration")));
            step.setExecutor(str(m.get("executor")));
            step.setStepStatus(str(m.get("stepStatus")) == null ? "pending" : str(m.get("stepStatus")));
            step.setRemark(str(m.get("remark")));
            stepService.save(step);
            idx++;
        }
        return AjaxResult.success("已保存 " + idx + " ä¸ªæ­¥éª¤");
    }
    /**
     * å®Œæˆ/推进单个步骤
     */
    @PostMapping("/step/complete")
    @ApiOperation("完成实验步骤")
    public AjaxResult completeStep(@RequestBody Map<String, Object> body) {
        Long stepId = toLong(body.get("stepId"));
        if (stepId == null) {
            return AjaxResult.error("stepId ä¸èƒ½ä¸ºç©º");
        }
        ExperimentStep step = stepService.getById(stepId);
        if (step == null) {
            return AjaxResult.error("步骤不存在");
        }
        if ("completed".equals(step.getStepStatus())) {
            return AjaxResult.error("该步骤已完成");
        }
        String operator = currentUser();
        LocalDateTime now = LocalDateTime.now();
        ExperimentStep upd = new ExperimentStep();
        upd.setId(stepId);
        upd.setStepStatus(str(body.get("stepStatus")) == null ? "completed" : str(body.get("stepStatus")));
        upd.setStepResult(str(body.get("stepResult")));
        upd.setExecutor(str(body.get("executor")) == null ? operator : str(body.get("executor")));
        if (step.getActualStartTime() == null) {
            upd.setActualStartTime(now);
        }
        upd.setActualEndTime(now);
        stepService.updateById(upd);
        // å…¨éƒ¨æ­¥éª¤å®Œæˆ -> å®žéªŒçŠ¶æ€ç½®ä¸ºå·²å®Œæˆ
        Long expId = step.getExperimentId();
        if (expId != null) {
            QueryWrapper<ExperimentStep> cw = new QueryWrapper<>();
            cw.eq("experiment_id", expId).ne("step_status", "completed");
            long remain = stepService.count(cw);
            if (remain == 0) {
                Experiment eu = new Experiment();
                eu.setId(expId);
                eu.setExperimentStatus("completed");
                experimentservice.updateById(eu);
            } else {
                Experiment eu = new Experiment();
                eu.setId(expId);
                eu.setExperimentStatus("inProgress");
                experimentservice.updateById(eu);
            }
        }
        return AjaxResult.success("步骤已完成");
    }
    /* ==================== èµ„源分配 ==================== */
    /**
     * åˆ†é…èµ„源:仪器/人员只记录;试剂会实际扣减库存并写出入库流水
     */
    @PostMapping("/assignResource")
    @ApiOperation("为实验分配资源")
    public AjaxResult assignResource(@RequestBody Map<String, Object> body) {
        Long experimentId = toLong(body.get("experimentId"));
        if (experimentId == null) {
            return AjaxResult.error("experimentId ä¸èƒ½ä¸ºç©º");
        }
        Experiment exp = experimentservice.getById(experimentId);
        if (exp == null) {
            return AjaxResult.error("实验不存在");
        }
        String resourceType = str(body.get("resourceType"));
        if (resourceType == null) {
            return AjaxResult.error("resourceType ä¸èƒ½ä¸ºç©º");
        }
        ExperimentResource res = new ExperimentResource();
        res.setExperimentId(experimentId);
        res.setExperimentCode(exp.getExperimentCode());
        res.setResourceType(resourceType);
        res.setResourceId(toLong(body.get("resourceId")));
        res.setResourceCode(str(body.get("resourceCode")));
        res.setResourceName(str(body.get("resourceName")));
        res.setQuantity(toDecimal(body.get("quantity")));
        res.setUnit(str(body.get("unit")));
        res.setUsageTime(str(body.get("usageTime")));
        res.setStatus(str(body.get("status")) == null ? "planned" : str(body.get("status")));
        res.setRemark(str(body.get("remark")));
        // è¯•剂/耗材:真实扣库存 + å†™æµæ°´
        if ("reagent".equals(resourceType) && res.getResourceId() != null) {
            Reagent re = reagentService.getById(res.getResourceId());
            if (re == null) {
                return AjaxResult.error("试剂/耗材不存在");
            }
            BigDecimal useQty = res.getQuantity() == null ? BigDecimal.ZERO : res.getQuantity();
            BigDecimal stock = re.getCurrentStock() == null ? BigDecimal.ZERO : re.getCurrentStock();
            if (useQty.compareTo(BigDecimal.ZERO) > 0 && useQty.compareTo(stock) > 0) {
                return AjaxResult.error("试剂库存不足(现有 " + stock.stripTrailingZeros().toPlainString()
                        + (re.getUnit() == null ? "" : re.getUnit()) + ")");
            }
            res.setResourceCode(re.getReagentCode());
            res.setResourceName(re.getReagentName());
            if (res.getUnit() == null) res.setUnit(re.getUnit());
            if (useQty.compareTo(BigDecimal.ZERO) > 0) {
                BigDecimal after = stock.subtract(useQty);
                ReagentRecord rec = new ReagentRecord();
                rec.setReagentId(re.getId());
                rec.setReagentCode(re.getReagentCode());
                rec.setReagentName(re.getReagentName());
                rec.setRecordType("out");
                rec.setQuantity(useQty);
                rec.setBeforeStock(stock);
                rec.setAfterStock(after);
                rec.setOperator(currentUser());
                rec.setOperateTime(LocalDateTime.now());
                rec.setPurpose("实验领用");
                rec.setRelatedExperiment(exp.getExperimentCode());
                reagentRecordService.save(rec);
                Reagent ru = new Reagent();
                ru.setId(re.getId());
                ru.setCurrentStock(after);
                reagentService.updateById(ru);
            }
        }
        resourceService.save(res);
        return AjaxResult.success("资源已分配");
    }
    @DeleteMapping("/resource/{id}")
    @ApiOperation("移除实验资源分配")
    public AjaxResult removeResource(@PathVariable Long id) {
        return AjaxResult.success(resourceService.removeById(id));
    }
    /* ==================== æµç¨‹æ¨¡æ¿ ==================== */
    /**
     * å¸¸ç”¨å®žéªŒç±»åž‹çš„ SOP æ¨¡æ¿ï¼Œå‰ç«¯ã€Œå¥—用模板」时按类型拉取
     */
    @GetMapping("/stepTemplate")
    @ApiOperation("获取流程模板")
    public AjaxResult stepTemplate(@RequestParam(required = false) String type) {
        Map<String, List<Map<String, Object>>> templates = new LinkedHashMap<>();
        templates.put("chemicalAnalysis", buildSteps(
                new String[][]{
                        {"样品前处理", "按标准称取样品,定容至刻度,摇匀静置 10 åˆ†é’Ÿ"},
                        {"试剂配制", "按方法要求配制标准溶液与显色剂,现配现用"},
                        {"仪器预热", "开启分析仪器预热 30 åˆ†é’Ÿï¼Œå¾…基线稳定"},
                        {"标准曲线绘制", "依次测定标准系列,绘制曲线,相关系数应 â‰¥0.999"},
                        {"样品测定", "按顺序测定样品,每 10 ä¸ªæ ·å“æ’入一次质控样"},
                        {"数据处理", "计算浓度结果,做平行样偏差检查"}
                }));
        templates.put("physicalTest", buildSteps(
                new String[][]{
                        {"试样制备", "按标准裁切/加工试样,记录尺寸与外观"},
                        {"设备调试", "校准试验机零点,设置试验速度"},
                        {"装夹定位", "将试样装夹于夹具中心,确保受力轴线一致"},
                        {"加载试验", "按设定速度加载直至破坏,记录最大力值"},
                        {"结果计算", "计算强度/伸长率,取 3 ä¸ªæœ‰æ•ˆè¯•样的平均值"}
                }));
        templates.put("microbialTest", buildSteps(
                new String[][]{
                        {"培养基配制", "按配方配制培养基,121℃ ç­èŒ 15 åˆ†é’Ÿ"},
                        {"样品稀释", "无菌操作梯度稀释样品,制备 10^-1 ~ 10^-3 ç¨€é‡Šæ¶²"},
                        {"接种", "取 1mL ç¨€é‡Šæ¶²æ³¨å…¥å¹³çš¿ï¼Œå€¾æ³¨åŸ¹å…»åŸºæ··åŒ€"},
                        {"培养", "36±1℃ åŸ¹å…» 48±2 å°æ—¶"},
                        {"菌落计数", "选取 30~300 ä¸ªèŒè½çš„平皿计数,换算 CFU/mL"}
                }));
        if (type != null && templates.containsKey(type)) {
            return AjaxResult.success(templates.get(type));
        }
        return AjaxResult.success(templates);
    }
    private List<Map<String, Object>> buildSteps(String[][] arr) {
        List<Map<String, Object>> list = new ArrayList<>();
        for (int i = 0; i < arr.length; i++) {
            Map<String, Object> m = new LinkedHashMap<>();
            m.put("stepNo", i + 1);
            m.put("stepName", arr[i][0]);
            m.put("stepContent", arr[i][1]);
            m.put("stepStatus", "pending");
            list.add(m);
        }
        return list;
    }
    /* ---------------- helpers ---------------- */
    private String currentUser() {
        try {
            return SecurityUtils.getUsername();
        } catch (Exception e) {
            return "system";
        }
    }
    private static String str(Object v) {
        if (v == null) return null;
        String s = String.valueOf(v).trim();
        return s.isEmpty() ? null : s;
    }
    private static Long toLong(Object v) {
        if (v == null) return null;
        try {
            return Long.valueOf(String.valueOf(v).trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private static Integer toInt(Object v) {
        if (v == null) return null;
        try {
            return Integer.valueOf(String.valueOf(v).trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private static BigDecimal toDecimal(Object v) {
        if (v == null) return null;
        try {
            return new BigDecimal(String.valueOf(v).trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
}
src/main/java/com/ruoyi/lims/controller/ExperimentResourceController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.ExperimentResource;
import com.ruoyi.lims.service.ExperimentResourceService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@AllArgsConstructor
@RequestMapping("/lims/experimentResource")
@Api(value = "ExperimentResource", tags = "Experiment Resources")
public class ExperimentResourceController {
    private ExperimentResourceService experimentResourceService;
    @GetMapping("/listPage")
    @ApiOperation("Paged query")
    public AjaxResult listPage(Page<ExperimentResource> page, ExperimentResource query) {
        IPage<ExperimentResource> result = experimentResourceService.listPage(page, query);
        return AjaxResult.success(result);
    }
    @GetMapping("/listAll")
    @ApiOperation("List all by sample or experiment")
    public AjaxResult listAll(ExperimentResource query) {
        return AjaxResult.success(experimentResourceService.listByExperimentOrSample(query));
    }
    @GetMapping("/{id}")
    @ApiOperation("Get by id")
    public AjaxResult getById(@PathVariable Long id) {
        return AjaxResult.success(experimentResourceService.getById(id));
    }
    @PostMapping("/add")
    @ApiOperation("Add")
    public AjaxResult add(@RequestBody ExperimentResource entity) {
        return AjaxResult.success(experimentResourceService.save(entity));
    }
    @PostMapping("/update")
    @ApiOperation("Update")
    public AjaxResult update(@RequestBody ExperimentResource entity) {
        return AjaxResult.success(experimentResourceService.updateById(entity));
    }
    @DeleteMapping("/delete")
    @ApiOperation("Batch delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("ids required");
        return AjaxResult.success(experimentResourceService.removeBatchByIds(ids));
    }
}
src/main/java/com/ruoyi/lims/controller/ExperimentStepController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.ExperimentStep;
import com.ruoyi.lims.service.ExperimentStepService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@AllArgsConstructor
@RequestMapping("/lims/experimentStep")
@Api(value = "ExperimentStep", tags = "Experiment Steps")
public class ExperimentStepController {
    private ExperimentStepService experimentStepService;
    @GetMapping("/listPage")
    @ApiOperation("Paged query")
    public AjaxResult listPage(Page<ExperimentStep> page, ExperimentStep query) {
        IPage<ExperimentStep> result = experimentStepService.listPage(page, query);
        return AjaxResult.success(result);
    }
    @GetMapping("/listAll")
    @ApiOperation("List all by sample or experiment")
    public AjaxResult listAll(ExperimentStep query) {
        return AjaxResult.success(experimentStepService.listByExperimentOrSample(query));
    }
    @GetMapping("/{id}")
    @ApiOperation("Get by id")
    public AjaxResult getById(@PathVariable Long id) {
        return AjaxResult.success(experimentStepService.getById(id));
    }
    @PostMapping("/add")
    @ApiOperation("Add")
    public AjaxResult add(@RequestBody ExperimentStep entity) {
        return AjaxResult.success(experimentStepService.save(entity));
    }
    @PostMapping("/update")
    @ApiOperation("Update")
    public AjaxResult update(@RequestBody ExperimentStep entity) {
        return AjaxResult.success(experimentStepService.updateById(entity));
    }
    @DeleteMapping("/delete")
    @ApiOperation("Batch delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("ids required");
        return AjaxResult.success(experimentStepService.removeBatchByIds(ids));
    }
}
src/main/java/com/ruoyi/lims/controller/InterfaceFieldController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.InterfaceField;
import com.ruoyi.lims.service.InterfaceFieldService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@AllArgsConstructor
@RequestMapping("/lims/interfaceField")
@Api(value = "InterfaceField", tags = "Interface Fields")
public class InterfaceFieldController {
    private InterfaceFieldService interfaceFieldService;
    @GetMapping("/listPage")
    @ApiOperation("Paged query")
    public AjaxResult listPage(Page<InterfaceField> page, InterfaceField query) {
        IPage<InterfaceField> result = interfaceFieldService.listPage(page, query);
        return AjaxResult.success(result);
    }
    @GetMapping("/listAll")
    @ApiOperation("List all by sample or experiment")
    public AjaxResult listAll(InterfaceField query) {
        return AjaxResult.success(interfaceFieldService.listByExperimentOrSample(query));
    }
    @GetMapping("/{id}")
    @ApiOperation("Get by id")
    public AjaxResult getById(@PathVariable Long id) {
        return AjaxResult.success(interfaceFieldService.getById(id));
    }
    @PostMapping("/add")
    @ApiOperation("Add")
    public AjaxResult add(@RequestBody InterfaceField entity) {
        return AjaxResult.success(interfaceFieldService.save(entity));
    }
    @PostMapping("/update")
    @ApiOperation("Update")
    public AjaxResult update(@RequestBody InterfaceField entity) {
        return AjaxResult.success(interfaceFieldService.updateById(entity));
    }
    @DeleteMapping("/delete")
    @ApiOperation("Batch delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("ids required");
        return AjaxResult.success(interfaceFieldService.removeBatchByIds(ids));
    }
}
src/main/java/com/ruoyi/lims/controller/InterfaceLogController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.InterfaceLog;
import com.ruoyi.lims.service.InterfaceLogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@AllArgsConstructor
@RequestMapping("/lims/interfaceLog")
@Api(value = "InterfaceLog", tags = "Interface Logs")
public class InterfaceLogController {
    private InterfaceLogService interfaceLogService;
    @GetMapping("/listPage")
    @ApiOperation("Paged query")
    public AjaxResult listPage(Page<InterfaceLog> page, InterfaceLog query) {
        IPage<InterfaceLog> result = interfaceLogService.listPage(page, query);
        return AjaxResult.success(result);
    }
    @GetMapping("/listAll")
    @ApiOperation("List all by sample or experiment")
    public AjaxResult listAll(InterfaceLog query) {
        return AjaxResult.success(interfaceLogService.listByExperimentOrSample(query));
    }
    @GetMapping("/{id}")
    @ApiOperation("Get by id")
    public AjaxResult getById(@PathVariable Long id) {
        return AjaxResult.success(interfaceLogService.getById(id));
    }
    @PostMapping("/add")
    @ApiOperation("Add")
    public AjaxResult add(@RequestBody InterfaceLog entity) {
        return AjaxResult.success(interfaceLogService.save(entity));
    }
    @PostMapping("/update")
    @ApiOperation("Update")
    public AjaxResult update(@RequestBody InterfaceLog entity) {
        return AjaxResult.success(interfaceLogService.updateById(entity));
    }
    @DeleteMapping("/delete")
    @ApiOperation("Batch delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("ids required");
        return AjaxResult.success(interfaceLogService.removeBatchByIds(ids));
    }
}
src/main/java/com/ruoyi/lims/controller/SampleController.java
@@ -1,17 +1,27 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.Sample;
import com.ruoyi.lims.pojo.SampleStatusLog;
import com.ruoyi.lims.pojo.SampleStockRecord;
import com.ruoyi.lims.service.SampleService;
import com.ruoyi.lims.service.SampleStatusLogService;
import com.ruoyi.lims.service.SampleStockRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@RestController
@AllArgsConstructor
@@ -20,6 +30,13 @@
public class SampleController {
    private SampleService sampleservice;
    private SampleStockRecordService stockRecordService;
    private SampleStatusLogService statusLogService;
    private static final String ST_IN_STOCK = "inStock";
    private static final String ST_OUT_STOCK = "outStock";
    private static final String ST_TESTING = "testing";
    private static final String ST_DESTROYED = "destroyed";
    @GetMapping("/listPage")
    @ApiOperation("分页查询所有样品管理表")
@@ -34,16 +51,75 @@
        return AjaxResult.success(sampleservice.getById(id));
    }
    /**
     * æ ·å“è¯¦æƒ…:样品本体 + å‡ºå…¥åº“流水 + çŠ¶æ€å˜æ›´åŽ†å²
     */
    @GetMapping("/detail/{id}")
    @ApiOperation("样品详情(含出入库流水与状态变更历史)")
    public AjaxResult detail(@PathVariable Long id) {
        Sample sample = sampleservice.getById(id);
        if (sample == null) {
            return AjaxResult.error("样品不存在");
        }
        QueryWrapper<SampleStockRecord> rw = new QueryWrapper<>();
        rw.eq("sample_id", id).orderByDesc("operate_time").orderByDesc("id");
        List<SampleStockRecord> records = stockRecordService.list(rw);
        QueryWrapper<SampleStatusLog> sw = new QueryWrapper<>();
        sw.eq("sample_id", id).orderByDesc("operate_time").orderByDesc("id");
        List<SampleStatusLog> logs = statusLogService.list(sw);
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("sample", sample);
        result.put("stockRecords", records);
        result.put("statusLogs", logs);
        return AjaxResult.success(result);
    }
    @PostMapping("/add")
    @ApiOperation("新增样品管理表")
    public AjaxResult add(@RequestBody Sample sample) {
        return AjaxResult.success(sampleservice.save(sample));
        boolean ok = sampleservice.save(sample);
        // æ–°å»ºæ ·å“é»˜è®¤è§†ä¸ºä¸€æ¬¡å…¥åº“,补一条流水,方便追溯
        if (ok && sample.getId() != null) {
            SampleStockRecord rec = new SampleStockRecord();
            rec.setSampleId(sample.getId());
            rec.setSampleCode(sample.getSampleCode());
            rec.setSampleName(sample.getSampleName());
            rec.setRecordType("in");
            rec.setQuantity(sample.getQuantity());
            rec.setUnit(sample.getUnit());
            rec.setAfterStatus(sample.getSampleStatus());
            rec.setToLocation(sample.getStorageLocation());
            rec.setOperator(currentUser());
            rec.setOperateTime(LocalDateTime.now());
            rec.setPurpose("样品登记入库");
            stockRecordService.save(rec);
        }
        return AjaxResult.success(ok);
    }
    @PostMapping("/update")
    @ApiOperation("修改样品管理表")
    public AjaxResult update(@RequestBody Sample sample) {
        return AjaxResult.success(sampleservice.updateById(sample));
        Sample old = sampleservice.getById(sample.getId());
        boolean ok = sampleservice.updateById(sample);
        // çŠ¶æ€è¢«æ‰‹å·¥æ”¹åŠ¨æ—¶ç•™ç—•
        if (ok && old != null && sample.getSampleStatus() != null
                && !sample.getSampleStatus().equals(old.getSampleStatus())) {
            SampleStatusLog log = new SampleStatusLog();
            log.setSampleId(sample.getId());
            log.setSampleCode(old.getSampleCode());
            log.setFromStatus(old.getSampleStatus());
            log.setToStatus(sample.getSampleStatus());
            log.setChangeReason("表单修改");
            log.setOperator(currentUser());
            log.setOperateTime(LocalDateTime.now());
            statusLogService.save(log);
        }
        return AjaxResult.success(ok);
    }
    @DeleteMapping("/delete")
@@ -52,4 +128,236 @@
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("请传入要删除的ID");
        return AjaxResult.success(sampleservice.removeBatchByIds(ids));
    }
    /**
     * æ ·å“å‡ºåº“:校验状态与数量 -> å†™æµæ°´ -> å†™çŠ¶æ€åŽ†å² -> æ›´æ–°æ ·å“çŠ¶æ€
     */
    @PostMapping("/outStock")
    @ApiOperation("样品出库")
    public AjaxResult outStock(@RequestBody Map<String, Object> body) {
        Long sampleId = toLong(body.get("sampleId"));
        if (sampleId == null) {
            return AjaxResult.error("sampleId ä¸èƒ½ä¸ºç©º");
        }
        Sample sample = sampleservice.getById(sampleId);
        if (sample == null) {
            return AjaxResult.error("样品不存在");
        }
        if (ST_DESTROYED.equals(sample.getSampleStatus())) {
            return AjaxResult.error("样品已销毁,不能出库");
        }
        if (ST_OUT_STOCK.equals(sample.getSampleStatus())) {
            return AjaxResult.error("样品已是出库状态,无需重复出库");
        }
        BigDecimal qty = toDecimal(body.get("quantity"));
        if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
            return AjaxResult.error("出库数量必须大于 0");
        }
        BigDecimal stock = sample.getQuantity() == null ? BigDecimal.ZERO : sample.getQuantity();
        if (qty.compareTo(stock) > 0) {
            return AjaxResult.error("出库数量超过当前库存(现有 " + stock.stripTrailingZeros().toPlainString()
                    + (sample.getUnit() == null ? "" : sample.getUnit()) + ")");
        }
        String beforeStatus = sample.getSampleStatus();
        BigDecimal afterQty = stock.subtract(qty);
        // å…¨éƒ¨å‡ºåº“则置为出库状态,部分出库保持原状态
        String afterStatus = afterQty.compareTo(BigDecimal.ZERO) == 0 ? ST_OUT_STOCK : beforeStatus;
        SampleStockRecord rec = new SampleStockRecord();
        rec.setSampleId(sample.getId());
        rec.setSampleCode(sample.getSampleCode());
        rec.setSampleName(sample.getSampleName());
        rec.setRecordType("out");
        rec.setQuantity(qty);
        rec.setUnit(sample.getUnit());
        rec.setBeforeStatus(beforeStatus);
        rec.setAfterStatus(afterStatus);
        rec.setFromLocation(sample.getStorageLocation());
        rec.setToLocation(str(body.get("toLocation")));
        rec.setRecipient(str(body.get("recipient")));
        rec.setPurpose(str(body.get("purpose")));
        rec.setRelatedExperiment(str(body.get("relatedExperiment")));
        rec.setRemark(str(body.get("remark")));
        rec.setOperator(currentUser());
        rec.setOperateTime(LocalDateTime.now());
        stockRecordService.save(rec);
        if (!eq(beforeStatus, afterStatus)) {
            SampleStatusLog log = new SampleStatusLog();
            log.setSampleId(sample.getId());
            log.setSampleCode(sample.getSampleCode());
            log.setFromStatus(beforeStatus);
            log.setToStatus(afterStatus);
            log.setChangeReason("出库" + (str(body.get("purpose")) == null ? "" : ":" + str(body.get("purpose"))));
            log.setOperator(currentUser());
            log.setOperateTime(LocalDateTime.now());
            statusLogService.save(log);
        }
        Sample upd = new Sample();
        upd.setId(sample.getId());
        upd.setQuantity(afterQty);
        upd.setSampleStatus(afterStatus);
        if (str(body.get("toLocation")) != null) {
            upd.setStorageLocation(str(body.get("toLocation")));
        }
        sampleservice.updateById(upd);
        Map<String, Object> ret = new LinkedHashMap<>();
        ret.put("sampleId", sample.getId());
        ret.put("beforeQuantity", stock);
        ret.put("afterQuantity", afterQty);
        ret.put("beforeStatus", beforeStatus);
        ret.put("afterStatus", afterStatus);
        return AjaxResult.success("出库成功", ret);
    }
    /**
     * æ ·å“å…¥åº“(追加数量或新建后补录)
     */
    @PostMapping("/inStock")
    @ApiOperation("样品入库")
    public AjaxResult inStock(@RequestBody Map<String, Object> body) {
        Long sampleId = toLong(body.get("sampleId"));
        if (sampleId == null) {
            return AjaxResult.error("sampleId ä¸èƒ½ä¸ºç©º");
        }
        Sample sample = sampleservice.getById(sampleId);
        if (sample == null) {
            return AjaxResult.error("样品不存在");
        }
        BigDecimal qty = toDecimal(body.get("quantity"));
        if (qty == null || qty.compareTo(BigDecimal.ZERO) <= 0) {
            return AjaxResult.error("入库数量必须大于 0");
        }
        String beforeStatus = sample.getSampleStatus();
        BigDecimal stock = sample.getQuantity() == null ? BigDecimal.ZERO : sample.getQuantity();
        BigDecimal afterQty = stock.add(qty);
        String afterStatus = ST_OUT_STOCK.equals(beforeStatus) ? ST_IN_STOCK : beforeStatus;
        SampleStockRecord rec = new SampleStockRecord();
        rec.setSampleId(sample.getId());
        rec.setSampleCode(sample.getSampleCode());
        rec.setSampleName(sample.getSampleName());
        rec.setRecordType("in");
        rec.setQuantity(qty);
        rec.setUnit(sample.getUnit());
        rec.setBeforeStatus(beforeStatus);
        rec.setAfterStatus(afterStatus);
        rec.setToLocation(str(body.get("toLocation")) == null ? sample.getStorageLocation() : str(body.get("toLocation")));
        rec.setRecipient(str(body.get("recipient")));
        rec.setPurpose(str(body.get("purpose")) == null ? "样品入库" : str(body.get("purpose")));
        rec.setRemark(str(body.get("remark")));
        rec.setOperator(currentUser());
        rec.setOperateTime(LocalDateTime.now());
        stockRecordService.save(rec);
        if (!eq(beforeStatus, afterStatus)) {
            SampleStatusLog log = new SampleStatusLog();
            log.setSampleId(sample.getId());
            log.setSampleCode(sample.getSampleCode());
            log.setFromStatus(beforeStatus);
            log.setToStatus(afterStatus);
            log.setChangeReason("入库");
            log.setOperator(currentUser());
            log.setOperateTime(LocalDateTime.now());
            statusLogService.save(log);
        }
        Sample upd = new Sample();
        upd.setId(sample.getId());
        upd.setQuantity(afterQty);
        upd.setSampleStatus(afterStatus);
        if (str(body.get("toLocation")) != null) {
            upd.setStorageLocation(str(body.get("toLocation")));
        }
        sampleservice.updateById(upd);
        Map<String, Object> ret = new LinkedHashMap<>();
        ret.put("sampleId", sample.getId());
        ret.put("beforeQuantity", stock);
        ret.put("afterQuantity", afterQty);
        ret.put("beforeStatus", beforeStatus);
        ret.put("afterStatus", afterStatus);
        return AjaxResult.success("入库成功", ret);
    }
    /**
     * æ ·å“çŠ¶æ€æµè½¬ï¼ˆåœ¨åº“/检测中/出库/销毁)
     */
    @PostMapping("/changeStatus")
    @ApiOperation("样品状态流转")
    public AjaxResult changeStatus(@RequestBody Map<String, Object> body) {
        Long sampleId = toLong(body.get("sampleId"));
        String toStatus = str(body.get("toStatus"));
        if (sampleId == null || toStatus == null) {
            return AjaxResult.error("sampleId å’Œ toStatus ä¸èƒ½ä¸ºç©º");
        }
        Sample sample = sampleservice.getById(sampleId);
        if (sample == null) {
            return AjaxResult.error("样品不存在");
        }
        String fromStatus = sample.getSampleStatus();
        if (eq(fromStatus, toStatus)) {
            return AjaxResult.error("样品已处于该状态");
        }
        SampleStatusLog log = new SampleStatusLog();
        log.setSampleId(sample.getId());
        log.setSampleCode(sample.getSampleCode());
        log.setFromStatus(fromStatus);
        log.setToStatus(toStatus);
        log.setChangeReason(str(body.get("changeReason")));
        log.setOperator(currentUser());
        log.setOperateTime(LocalDateTime.now());
        statusLogService.save(log);
        Sample upd = new Sample();
        upd.setId(sample.getId());
        upd.setSampleStatus(toStatus);
        sampleservice.updateById(upd);
        return AjaxResult.success("状态已更新");
    }
    /* ---------------- helpers ---------------- */
    private String currentUser() {
        try {
            return SecurityUtils.getUsername();
        } catch (Exception e) {
            return "system";
        }
    }
    private static String str(Object v) {
        if (v == null) return null;
        String s = String.valueOf(v).trim();
        return s.isEmpty() ? null : s;
    }
    private static Long toLong(Object v) {
        if (v == null) return null;
        try {
            return Long.valueOf(String.valueOf(v).trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private static BigDecimal toDecimal(Object v) {
        if (v == null) return null;
        try {
            return new BigDecimal(String.valueOf(v).trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
    private static boolean eq(String a, String b) {
        return a == null ? b == null : a.equals(b);
    }
}
src/main/java/com/ruoyi/lims/controller/SampleStatusLogController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.SampleStatusLog;
import com.ruoyi.lims.service.SampleStatusLogService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@AllArgsConstructor
@RequestMapping("/lims/sampleStatusLog")
@Api(value = "SampleStatusLog", tags = "Sample Status Logs")
public class SampleStatusLogController {
    private SampleStatusLogService sampleStatusLogService;
    @GetMapping("/listPage")
    @ApiOperation("Paged query")
    public AjaxResult listPage(Page<SampleStatusLog> page, SampleStatusLog query) {
        IPage<SampleStatusLog> result = sampleStatusLogService.listPage(page, query);
        return AjaxResult.success(result);
    }
    @GetMapping("/listAll")
    @ApiOperation("List all by sample or experiment")
    public AjaxResult listAll(SampleStatusLog query) {
        return AjaxResult.success(sampleStatusLogService.listByExperimentOrSample(query));
    }
    @GetMapping("/{id}")
    @ApiOperation("Get by id")
    public AjaxResult getById(@PathVariable Long id) {
        return AjaxResult.success(sampleStatusLogService.getById(id));
    }
    @PostMapping("/add")
    @ApiOperation("Add")
    public AjaxResult add(@RequestBody SampleStatusLog entity) {
        return AjaxResult.success(sampleStatusLogService.save(entity));
    }
    @PostMapping("/update")
    @ApiOperation("Update")
    public AjaxResult update(@RequestBody SampleStatusLog entity) {
        return AjaxResult.success(sampleStatusLogService.updateById(entity));
    }
    @DeleteMapping("/delete")
    @ApiOperation("Batch delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("ids required");
        return AjaxResult.success(sampleStatusLogService.removeBatchByIds(ids));
    }
}
src/main/java/com/ruoyi/lims/controller/SampleStockRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,61 @@
package com.ruoyi.lims.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.lims.pojo.SampleStockRecord;
import com.ruoyi.lims.service.SampleStockRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@AllArgsConstructor
@RequestMapping("/lims/sampleStockRecord")
@Api(value = "SampleStockRecord", tags = "Sample Stock Records")
public class SampleStockRecordController {
    private SampleStockRecordService sampleStockRecordService;
    @GetMapping("/listPage")
    @ApiOperation("Paged query")
    public AjaxResult listPage(Page<SampleStockRecord> page, SampleStockRecord query) {
        IPage<SampleStockRecord> result = sampleStockRecordService.listPage(page, query);
        return AjaxResult.success(result);
    }
    @GetMapping("/listAll")
    @ApiOperation("List all by sample or experiment")
    public AjaxResult listAll(SampleStockRecord query) {
        return AjaxResult.success(sampleStockRecordService.listByExperimentOrSample(query));
    }
    @GetMapping("/{id}")
    @ApiOperation("Get by id")
    public AjaxResult getById(@PathVariable Long id) {
        return AjaxResult.success(sampleStockRecordService.getById(id));
    }
    @PostMapping("/add")
    @ApiOperation("Add")
    public AjaxResult add(@RequestBody SampleStockRecord entity) {
        return AjaxResult.success(sampleStockRecordService.save(entity));
    }
    @PostMapping("/update")
    @ApiOperation("Update")
    public AjaxResult update(@RequestBody SampleStockRecord entity) {
        return AjaxResult.success(sampleStockRecordService.updateById(entity));
    }
    @DeleteMapping("/delete")
    @ApiOperation("Batch delete")
    public AjaxResult delete(@RequestBody List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) return AjaxResult.error("ids required");
        return AjaxResult.success(sampleStockRecordService.removeBatchByIds(ids));
    }
}
src/main/java/com/ruoyi/lims/mapper/ExperimentResourceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.lims.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.lims.pojo.ExperimentResource;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ExperimentResourceMapper extends BaseMapper<ExperimentResource> {
}
src/main/java/com/ruoyi/lims/mapper/ExperimentStepMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.lims.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.lims.pojo.ExperimentStep;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ExperimentStepMapper extends BaseMapper<ExperimentStep> {
}
src/main/java/com/ruoyi/lims/mapper/InterfaceFieldMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.lims.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.lims.pojo.InterfaceField;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InterfaceFieldMapper extends BaseMapper<InterfaceField> {
}
src/main/java/com/ruoyi/lims/mapper/InterfaceLogMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.lims.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.lims.pojo.InterfaceLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InterfaceLogMapper extends BaseMapper<InterfaceLog> {
}
src/main/java/com/ruoyi/lims/mapper/SampleStatusLogMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.lims.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.lims.pojo.SampleStatusLog;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SampleStatusLogMapper extends BaseMapper<SampleStatusLog> {
}
src/main/java/com/ruoyi/lims/mapper/SampleStockRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,9 @@
package com.ruoyi.lims.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.lims.pojo.SampleStockRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface SampleStockRecordMapper extends BaseMapper<SampleStockRecord> {
}
src/main/java/com/ruoyi/lims/pojo/ExperimentResource.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
package com.ruoyi.lims.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("lims_experiment_resource")
public class ExperimentResource implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "Experiment id")
    private Long experimentId;
    @ApiModelProperty(value = "Experiment code")
    private String experimentCode;
    @ApiModelProperty(value = "Type: instrument/reagent/personnel")
    private String resourceType;
    @ApiModelProperty(value = "Resource id")
    private Long resourceId;
    @ApiModelProperty(value = "Resource code")
    private String resourceCode;
    @ApiModelProperty(value = "Resource name")
    private String resourceName;
    @ApiModelProperty(value = "Quantity")
    private BigDecimal quantity;
    @ApiModelProperty(value = "Unit")
    private String unit;
    @ApiModelProperty(value = "Usage time")
    private String usageTime;
    @ApiModelProperty(value = "Status: planned/inuse/returned/consumed")
    private String status;
    @ApiModelProperty(value = "Remark")
    private String remark;
    @ApiModelProperty(value = "Delete flag")
    private String delFlag;
    @ApiModelProperty(value = "Tenant id")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @ApiModelProperty(value = "Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty(value = "Create user")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
}
src/main/java/com/ruoyi/lims/pojo/ExperimentStep.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
package com.ruoyi.lims.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("lims_experiment_step")
public class ExperimentStep implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "Experiment id")
    private Long experimentId;
    @ApiModelProperty(value = "Experiment code")
    private String experimentCode;
    @ApiModelProperty(value = "Step number")
    private Integer stepNo;
    @ApiModelProperty(value = "Step name")
    private String stepName;
    @ApiModelProperty(value = "Step SOP content")
    private String stepContent;
    @ApiModelProperty(value = "Expected duration minutes")
    private Integer expectedDuration;
    @ApiModelProperty(value = "Executor")
    private String executor;
    @ApiModelProperty(value = "Plan start")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime planStartTime;
    @ApiModelProperty(value = "Plan end")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime planEndTime;
    @ApiModelProperty(value = "Actual start")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualStartTime;
    @ApiModelProperty(value = "Actual end")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime actualEndTime;
    @ApiModelProperty(value = "Status: pending/running/completed/skipped/failed")
    private String stepStatus;
    @ApiModelProperty(value = "Step result")
    private String stepResult;
    @ApiModelProperty(value = "Remark")
    private String remark;
    @ApiModelProperty(value = "Delete flag")
    private String delFlag;
    @ApiModelProperty(value = "Tenant id")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @ApiModelProperty(value = "Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty(value = "Create user")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
}
src/main/java/com/ruoyi/lims/pojo/InterfaceField.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,84 @@
package com.ruoyi.lims.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("lims_interface_field")
public class InterfaceField implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "Interface id")
    private Long interfaceId;
    @ApiModelProperty(value = "Interface code")
    private String interfaceCode;
    @ApiModelProperty(value = "Source field name")
    private String sourceField;
    @ApiModelProperty(value = "JSON path")
    private String sourcePath;
    @ApiModelProperty(value = "Target field")
    private String targetField;
    @ApiModelProperty(value = "Transform: none/multiply/offset/divide/map")
    private String transformType;
    @ApiModelProperty(value = "Transform value")
    private String transformValue;
    @ApiModelProperty(value = "Default value")
    private String defaultValue;
    @ApiModelProperty(value = "Sort")
    private Integer sortNo;
    @ApiModelProperty(value = "Remark")
    private String remark;
    @ApiModelProperty(value = "Delete flag")
    private String delFlag;
    @ApiModelProperty(value = "Tenant id")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @ApiModelProperty(value = "Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty(value = "Create user")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
}
src/main/java/com/ruoyi/lims/pojo/InterfaceLog.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,92 @@
package com.ruoyi.lims.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("lims_interface_log")
public class InterfaceLog implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "Interface id")
    private Long interfaceId;
    @ApiModelProperty(value = "Interface code")
    private String interfaceCode;
    @ApiModelProperty(value = "Interface name")
    private String interfaceName;
    @ApiModelProperty(value = "Type: test/manual/auto")
    private String invokeType;
    @ApiModelProperty(value = "Request")
    private String requestData;
    @ApiModelProperty(value = "Response")
    private String responseData;
    @ApiModelProperty(value = "Success 1/0")
    private String success;
    @ApiModelProperty(value = "Error message")
    private String errorMsg;
    @ApiModelProperty(value = "Duration ms")
    private Integer durationMs;
    @ApiModelProperty(value = "Record count")
    private Integer recordCount;
    @ApiModelProperty(value = "Operator")
    private String operator;
    @ApiModelProperty(value = "Invoke time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime invokeTime;
    @ApiModelProperty(value = "Delete flag")
    private String delFlag;
    @ApiModelProperty(value = "Tenant id")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @ApiModelProperty(value = "Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty(value = "Create user")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
}
src/main/java/com/ruoyi/lims/pojo/SampleStatusLog.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,80 @@
package com.ruoyi.lims.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("lims_sample_status_log")
public class SampleStatusLog implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "Sample id")
    private Long sampleId;
    @ApiModelProperty(value = "Sample code")
    private String sampleCode;
    @ApiModelProperty(value = "From status")
    private String fromStatus;
    @ApiModelProperty(value = "To status")
    private String toStatus;
    @ApiModelProperty(value = "Change reason")
    private String changeReason;
    @ApiModelProperty(value = "Operator")
    private String operator;
    @ApiModelProperty(value = "Operate time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime operateTime;
    @ApiModelProperty(value = "Remark")
    private String remark;
    @ApiModelProperty(value = "Delete flag")
    private String delFlag;
    @ApiModelProperty(value = "Tenant id")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @ApiModelProperty(value = "Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty(value = "Create user")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
}
src/main/java/com/ruoyi/lims/pojo/SampleStockRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,104 @@
package com.ruoyi.lims.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import org.springframework.format.annotation.DateTimeFormat;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Data
@TableName("lims_sample_stock_record")
public class SampleStockRecord implements Serializable {
    private static final long serialVersionUID = 1L;
    @ApiModelProperty(value = "ID")
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @ApiModelProperty(value = "Sample id")
    private Long sampleId;
    @ApiModelProperty(value = "Sample code")
    private String sampleCode;
    @ApiModelProperty(value = "Sample name")
    private String sampleName;
    @ApiModelProperty(value = "Type: in/out/destroy/return/transfer")
    private String recordType;
    @ApiModelProperty(value = "Quantity")
    private BigDecimal quantity;
    @ApiModelProperty(value = "Unit")
    private String unit;
    @ApiModelProperty(value = "Status before")
    private String beforeStatus;
    @ApiModelProperty(value = "Status after")
    private String afterStatus;
    @ApiModelProperty(value = "From location")
    private String fromLocation;
    @ApiModelProperty(value = "To location")
    private String toLocation;
    @ApiModelProperty(value = "Operator")
    private String operator;
    @ApiModelProperty(value = "Operate time")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime operateTime;
    @ApiModelProperty(value = "Recipient")
    private String recipient;
    @ApiModelProperty(value = "Purpose")
    private String purpose;
    @ApiModelProperty(value = "Related experiment")
    private String relatedExperiment;
    @ApiModelProperty(value = "Remark")
    private String remark;
    @ApiModelProperty(value = "Delete flag")
    private String delFlag;
    @ApiModelProperty(value = "Tenant id")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @ApiModelProperty(value = "Create time")
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @ApiModelProperty(value = "Create user")
    @TableField(fill = FieldFill.INSERT)
    private Integer createUser;
    @ApiModelProperty(value = "Update time")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    @ApiModelProperty(value = "Update user")
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Integer updateUser;
}
src/main/java/com/ruoyi/lims/service/ExperimentResourceService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.lims.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.lims.pojo.ExperimentResource;
import java.util.List;
public interface ExperimentResourceService extends IService<ExperimentResource> {
    IPage<ExperimentResource> listPage(Page<ExperimentResource> page, ExperimentResource query);
    List<ExperimentResource> listByExperimentOrSample(ExperimentResource query);
}
src/main/java/com/ruoyi/lims/service/ExperimentStepService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.lims.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.lims.pojo.ExperimentStep;
import java.util.List;
public interface ExperimentStepService extends IService<ExperimentStep> {
    IPage<ExperimentStep> listPage(Page<ExperimentStep> page, ExperimentStep query);
    List<ExperimentStep> listByExperimentOrSample(ExperimentStep query);
}
src/main/java/com/ruoyi/lims/service/InterfaceFieldService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.lims.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.lims.pojo.InterfaceField;
import java.util.List;
public interface InterfaceFieldService extends IService<InterfaceField> {
    IPage<InterfaceField> listPage(Page<InterfaceField> page, InterfaceField query);
    List<InterfaceField> listByExperimentOrSample(InterfaceField query);
}
src/main/java/com/ruoyi/lims/service/InterfaceLogService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.lims.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.lims.pojo.InterfaceLog;
import java.util.List;
public interface InterfaceLogService extends IService<InterfaceLog> {
    IPage<InterfaceLog> listPage(Page<InterfaceLog> page, InterfaceLog query);
    List<InterfaceLog> listByExperimentOrSample(InterfaceLog query);
}
src/main/java/com/ruoyi/lims/service/SampleStatusLogService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.lims.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.lims.pojo.SampleStatusLog;
import java.util.List;
public interface SampleStatusLogService extends IService<SampleStatusLog> {
    IPage<SampleStatusLog> listPage(Page<SampleStatusLog> page, SampleStatusLog query);
    List<SampleStatusLog> listByExperimentOrSample(SampleStatusLog query);
}
src/main/java/com/ruoyi/lims/service/SampleStockRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,14 @@
package com.ruoyi.lims.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.lims.pojo.SampleStockRecord;
import java.util.List;
public interface SampleStockRecordService extends IService<SampleStockRecord> {
    IPage<SampleStockRecord> listPage(Page<SampleStockRecord> page, SampleStockRecord query);
    List<SampleStockRecord> listByExperimentOrSample(SampleStockRecord query);
}
src/main/java/com/ruoyi/lims/service/impl/ExperimentResourceServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package com.ruoyi.lims.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lims.mapper.ExperimentResourceMapper;
import com.ruoyi.lims.pojo.ExperimentResource;
import com.ruoyi.lims.service.ExperimentResourceService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class)
public class ExperimentResourceServiceImpl extends ServiceImpl<ExperimentResourceMapper, ExperimentResource> implements ExperimentResourceService {
    @Override
    public IPage<ExperimentResource> listPage(Page<ExperimentResource> page, ExperimentResource query) {
        QueryWrapper<ExperimentResource> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getExperimentId() != null) { wrapper.eq("experiment_id", query.getExperimentId()); }
            wrapper.like(StringUtils.isNotEmpty(query.getExperimentCode()), "experiment_code", query.getExperimentCode());
            wrapper.like(StringUtils.isNotEmpty(query.getResourceType()), "resource_type", query.getResourceType());
            wrapper.like(StringUtils.isNotEmpty(query.getResourceCode()), "resource_code", query.getResourceCode());
            wrapper.like(StringUtils.isNotEmpty(query.getResourceName()), "resource_name", query.getResourceName());
            wrapper.like(StringUtils.isNotEmpty(query.getUnit()), "unit", query.getUnit());
            wrapper.like(StringUtils.isNotEmpty(query.getUsageTime()), "usage_time", query.getUsageTime());
            wrapper.like(StringUtils.isNotEmpty(query.getStatus()), "status", query.getStatus());
            wrapper.like(StringUtils.isNotEmpty(query.getRemark()), "remark", query.getRemark());
        }
        wrapper.orderByDesc("id");
        return baseMapper.selectPage(page, wrapper);
    }
    @Override
    public java.util.List<ExperimentResource> listByExperimentOrSample(ExperimentResource query) {
        QueryWrapper<ExperimentResource> w = new QueryWrapper<>();
        if (query != null && query.getExperimentId() != null) {
            w.eq("experiment_id", query.getExperimentId());
        }
        w.orderByAsc("id");
        return baseMapper.selectList(w);
    }
}
src/main/java/com/ruoyi/lims/service/impl/ExperimentStepServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package com.ruoyi.lims.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lims.mapper.ExperimentStepMapper;
import com.ruoyi.lims.pojo.ExperimentStep;
import com.ruoyi.lims.service.ExperimentStepService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class)
public class ExperimentStepServiceImpl extends ServiceImpl<ExperimentStepMapper, ExperimentStep> implements ExperimentStepService {
    @Override
    public IPage<ExperimentStep> listPage(Page<ExperimentStep> page, ExperimentStep query) {
        QueryWrapper<ExperimentStep> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getExperimentId() != null) { wrapper.eq("experiment_id", query.getExperimentId()); }
            wrapper.like(StringUtils.isNotEmpty(query.getExperimentCode()), "experiment_code", query.getExperimentCode());
            wrapper.like(StringUtils.isNotEmpty(query.getStepName()), "step_name", query.getStepName());
            wrapper.like(StringUtils.isNotEmpty(query.getStepContent()), "step_content", query.getStepContent());
            wrapper.like(StringUtils.isNotEmpty(query.getExecutor()), "executor", query.getExecutor());
            wrapper.like(StringUtils.isNotEmpty(query.getStepStatus()), "step_status", query.getStepStatus());
            wrapper.like(StringUtils.isNotEmpty(query.getStepResult()), "step_result", query.getStepResult());
            wrapper.like(StringUtils.isNotEmpty(query.getRemark()), "remark", query.getRemark());
        }
        wrapper.orderByDesc("id");
        return baseMapper.selectPage(page, wrapper);
    }
    @Override
    public java.util.List<ExperimentStep> listByExperimentOrSample(ExperimentStep query) {
        QueryWrapper<ExperimentStep> w = new QueryWrapper<>();
        if (query != null && query.getExperimentId() != null) {
            w.eq("experiment_id", query.getExperimentId());
        }
        w.orderByAsc("id");
        return baseMapper.selectList(w);
    }
}
src/main/java/com/ruoyi/lims/service/impl/InstrumentCalibrationServiceImpl.java
@@ -19,6 +19,8 @@
    public IPage<InstrumentCalibration> listPage(Page<InstrumentCalibration> page, InstrumentCalibration query) {
        QueryWrapper<InstrumentCalibration> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getInstrumentId() != null) { wrapper.eq("instrument_id", query.getInstrumentId()); }
        wrapper.like(StringUtils.isNotEmpty(query.getInstrumentCode()), "instrument_code", query.getInstrumentCode());
        wrapper.like(StringUtils.isNotEmpty(query.getInstrumentName()), "instrument_name", query.getInstrumentName());
        wrapper.like(StringUtils.isNotEmpty(query.getRecordType()), "record_type", query.getRecordType());
src/main/java/com/ruoyi/lims/service/impl/InterfaceFieldServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package com.ruoyi.lims.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lims.mapper.InterfaceFieldMapper;
import com.ruoyi.lims.pojo.InterfaceField;
import com.ruoyi.lims.service.InterfaceFieldService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class)
public class InterfaceFieldServiceImpl extends ServiceImpl<InterfaceFieldMapper, InterfaceField> implements InterfaceFieldService {
    @Override
    public IPage<InterfaceField> listPage(Page<InterfaceField> page, InterfaceField query) {
        QueryWrapper<InterfaceField> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getInterfaceId() != null) { wrapper.eq("interface_id", query.getInterfaceId()); }
            wrapper.like(StringUtils.isNotEmpty(query.getInterfaceCode()), "interface_code", query.getInterfaceCode());
            wrapper.like(StringUtils.isNotEmpty(query.getSourceField()), "source_field", query.getSourceField());
            wrapper.like(StringUtils.isNotEmpty(query.getSourcePath()), "source_path", query.getSourcePath());
            wrapper.like(StringUtils.isNotEmpty(query.getTargetField()), "target_field", query.getTargetField());
            wrapper.like(StringUtils.isNotEmpty(query.getTransformType()), "transform_type", query.getTransformType());
            wrapper.like(StringUtils.isNotEmpty(query.getTransformValue()), "transform_value", query.getTransformValue());
            wrapper.like(StringUtils.isNotEmpty(query.getDefaultValue()), "default_value", query.getDefaultValue());
            wrapper.like(StringUtils.isNotEmpty(query.getRemark()), "remark", query.getRemark());
        }
        wrapper.orderByDesc("id");
        return baseMapper.selectPage(page, wrapper);
    }
    @Override
    public java.util.List<InterfaceField> listByExperimentOrSample(InterfaceField query) {
        QueryWrapper<InterfaceField> w = new QueryWrapper<>();
        w.orderByDesc("id");
        w.last("limit 200");
        return baseMapper.selectList(w);
    }
}
src/main/java/com/ruoyi/lims/service/impl/InterfaceLogServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package com.ruoyi.lims.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lims.mapper.InterfaceLogMapper;
import com.ruoyi.lims.pojo.InterfaceLog;
import com.ruoyi.lims.service.InterfaceLogService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class)
public class InterfaceLogServiceImpl extends ServiceImpl<InterfaceLogMapper, InterfaceLog> implements InterfaceLogService {
    @Override
    public IPage<InterfaceLog> listPage(Page<InterfaceLog> page, InterfaceLog query) {
        QueryWrapper<InterfaceLog> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getInterfaceId() != null) { wrapper.eq("interface_id", query.getInterfaceId()); }
            wrapper.like(StringUtils.isNotEmpty(query.getInterfaceCode()), "interface_code", query.getInterfaceCode());
            wrapper.like(StringUtils.isNotEmpty(query.getInterfaceName()), "interface_name", query.getInterfaceName());
            wrapper.like(StringUtils.isNotEmpty(query.getInvokeType()), "invoke_type", query.getInvokeType());
            wrapper.like(StringUtils.isNotEmpty(query.getRequestData()), "request_data", query.getRequestData());
            wrapper.like(StringUtils.isNotEmpty(query.getResponseData()), "response_data", query.getResponseData());
            wrapper.like(StringUtils.isNotEmpty(query.getSuccess()), "success", query.getSuccess());
            wrapper.like(StringUtils.isNotEmpty(query.getErrorMsg()), "error_msg", query.getErrorMsg());
            wrapper.like(StringUtils.isNotEmpty(query.getOperator()), "operator", query.getOperator());
        }
        wrapper.orderByDesc("id");
        return baseMapper.selectPage(page, wrapper);
    }
    @Override
    public java.util.List<InterfaceLog> listByExperimentOrSample(InterfaceLog query) {
        QueryWrapper<InterfaceLog> w = new QueryWrapper<>();
        w.orderByDesc("id");
        w.last("limit 200");
        return baseMapper.selectList(w);
    }
}
src/main/java/com/ruoyi/lims/service/impl/ReagentRecordServiceImpl.java
@@ -19,6 +19,8 @@
    public IPage<ReagentRecord> listPage(Page<ReagentRecord> page, ReagentRecord query) {
        QueryWrapper<ReagentRecord> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getReagentId() != null) { wrapper.eq("reagent_id", query.getReagentId()); }
        wrapper.like(StringUtils.isNotEmpty(query.getReagentCode()), "reagent_code", query.getReagentCode());
        wrapper.like(StringUtils.isNotEmpty(query.getReagentName()), "reagent_name", query.getReagentName());
        wrapper.like(StringUtils.isNotEmpty(query.getRecordType()), "record_type", query.getRecordType());
src/main/java/com/ruoyi/lims/service/impl/SampleStatusLogServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package com.ruoyi.lims.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lims.mapper.SampleStatusLogMapper;
import com.ruoyi.lims.pojo.SampleStatusLog;
import com.ruoyi.lims.service.SampleStatusLogService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class)
public class SampleStatusLogServiceImpl extends ServiceImpl<SampleStatusLogMapper, SampleStatusLog> implements SampleStatusLogService {
    @Override
    public IPage<SampleStatusLog> listPage(Page<SampleStatusLog> page, SampleStatusLog query) {
        QueryWrapper<SampleStatusLog> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getSampleId() != null) { wrapper.eq("sample_id", query.getSampleId()); }
            wrapper.like(StringUtils.isNotEmpty(query.getSampleCode()), "sample_code", query.getSampleCode());
            wrapper.like(StringUtils.isNotEmpty(query.getFromStatus()), "from_status", query.getFromStatus());
            wrapper.like(StringUtils.isNotEmpty(query.getToStatus()), "to_status", query.getToStatus());
            wrapper.like(StringUtils.isNotEmpty(query.getChangeReason()), "change_reason", query.getChangeReason());
            wrapper.like(StringUtils.isNotEmpty(query.getOperator()), "operator", query.getOperator());
            wrapper.like(StringUtils.isNotEmpty(query.getRemark()), "remark", query.getRemark());
        }
        wrapper.orderByDesc("id");
        return baseMapper.selectPage(page, wrapper);
    }
    @Override
    public java.util.List<SampleStatusLog> listByExperimentOrSample(SampleStatusLog query) {
        QueryWrapper<SampleStatusLog> w = new QueryWrapper<>();
        if (query != null && query.getSampleId() != null) {
            w.eq("sample_id", query.getSampleId());
        }
        w.orderByDesc("id");
        return baseMapper.selectList(w);
    }
}
src/main/java/com/ruoyi/lims/service/impl/SampleStockRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,51 @@
package com.ruoyi.lims.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.lims.mapper.SampleStockRecordMapper;
import com.ruoyi.lims.pojo.SampleStockRecord;
import com.ruoyi.lims.service.SampleStockRecordService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional(rollbackFor = Exception.class)
public class SampleStockRecordServiceImpl extends ServiceImpl<SampleStockRecordMapper, SampleStockRecord> implements SampleStockRecordService {
    @Override
    public IPage<SampleStockRecord> listPage(Page<SampleStockRecord> page, SampleStockRecord query) {
        QueryWrapper<SampleStockRecord> wrapper = new QueryWrapper<>();
        if (query != null) {
            // ID filters
            if (query.getSampleId() != null) { wrapper.eq("sample_id", query.getSampleId()); }
            wrapper.like(StringUtils.isNotEmpty(query.getSampleCode()), "sample_code", query.getSampleCode());
            wrapper.like(StringUtils.isNotEmpty(query.getSampleName()), "sample_name", query.getSampleName());
            wrapper.like(StringUtils.isNotEmpty(query.getRecordType()), "record_type", query.getRecordType());
            wrapper.like(StringUtils.isNotEmpty(query.getUnit()), "unit", query.getUnit());
            wrapper.like(StringUtils.isNotEmpty(query.getBeforeStatus()), "before_status", query.getBeforeStatus());
            wrapper.like(StringUtils.isNotEmpty(query.getAfterStatus()), "after_status", query.getAfterStatus());
            wrapper.like(StringUtils.isNotEmpty(query.getFromLocation()), "from_location", query.getFromLocation());
            wrapper.like(StringUtils.isNotEmpty(query.getToLocation()), "to_location", query.getToLocation());
            wrapper.like(StringUtils.isNotEmpty(query.getOperator()), "operator", query.getOperator());
            wrapper.like(StringUtils.isNotEmpty(query.getRecipient()), "recipient", query.getRecipient());
            wrapper.like(StringUtils.isNotEmpty(query.getPurpose()), "purpose", query.getPurpose());
            wrapper.like(StringUtils.isNotEmpty(query.getRelatedExperiment()), "related_experiment", query.getRelatedExperiment());
            wrapper.like(StringUtils.isNotEmpty(query.getRemark()), "remark", query.getRemark());
        }
        wrapper.orderByDesc("id");
        return baseMapper.selectPage(page, wrapper);
    }
    @Override
    public java.util.List<SampleStockRecord> listByExperimentOrSample(SampleStockRecord query) {
        QueryWrapper<SampleStockRecord> w = new QueryWrapper<>();
        if (query != null && query.getSampleId() != null) {
            w.eq("sample_id", query.getSampleId());
        }
        w.orderByDesc("id");
        return baseMapper.selectList(w);
    }
}