ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java SpringCloud微服务架构下的LIMS样本库设计与实现

Java SpringCloud微服务架构下的LIMS样本库设计与实现 简介一份基于Java SpringCloud实现的LIMS样本库实验室管理系统源代码面向需要落地微服务架构的Java工程师、实验室信息化项目开发者以及正在学习SpringCloud综合应用的中高级学习者。系统完整覆盖样本接收、存储、检测、分析、报告等全流程管理并集成权限控制、数据整合与外部系统对接能力。压缩包共199个文件、约11.41MB包含39个Java源文件与对应class文件、39个Jar依赖、31个XML配置以及JSP页面、JS/CSS前端资源、SQL脚本等其中XML兼顾Spring配置与MyBatis映射JSP/JS/CSS构成管理后台界面SQL脚本可辅助初始化数据库源码层级清晰已有206人学习下载。通过对Eureka服务发现、Zuul网关路由、用户服务、样本服务等核心模块的源码梳理读者可深入理解微服务拆分、业务解耦与权限控制的具体实现适合作为实验室管理系统二次开发及SpringCloud综合实战的参考范本。1. 用 Java SpringCloud 搭 LIMS 样本库先想清楚业务边界再动手做实验室系统的同行基本都遇到过这种场面样本登记靠 Excel 手工编号条码打印用第三方小工具样本入库出库查不到历史记录月底盘点时账实永远对不上。我接手过一家第三方医学检验所的样本库改造业务量一天两三千管样本旧系统是单机版 C/S 架构每管样本从登记到报告要经过五个岗位靠的是纸质交接和口头沟通。这套 Java SpringCloud 实现的 LIMS 样本库实验室管理系统源码解决的就是这类场景——把样本从登记、入库、检验任务分配、结果录入到报告生成的全流程串成一条有状态、可追踪、能审计的数据链路同时保证多机构多角色下的并发一致性和权限隔离。它适合三类人正在规划微服务架构的实验室内业开发、需要给现网 LIMS 补样本管理模块的 Java 工程师、以及想用真实业务场景练手 SpringCloud 生态的技术负责人。注意这套东西不是拿来就能跑的成品软件它是基于微服务思想实现的样本库核心源码需要你按自己的环境配置和二次开发。2. 微服务拆分与数据库设计样本库模块的边界到底划在哪2.1 服务划分把一个 LIMS 拆成六个可独立部署的服务LIMS 这类系统最常见的翻车点是把所有业务逻辑塞进一个单体 Spring Boot 工程然后发现样本登记、检测任务、报告模板之间耦合得根本拆不开。这套源码的服务边界划分思路值得直接抄——按业务域而不是按页面拆。我拆过几版之后稳定的划分方式是六个服务加一个网关。服务名端口核心职责关键表gateway8080路由转发、Token 鉴权、接口限流无system-service8101用户、角色、菜单、机构管理sys_user, sys_role, sys_orgsample-service8102样本登记、入库、出库、状态流转sample_info, sample_status_logtest-service8103检测任务分配、项目配置、结果录入test_task, test_item, test_resultreport-service8104报告生成、模板管理、审核签发report_info, report_templatestock-service8105样本库位、库存记录、库存预警stock_location, stock_record网关单独拆出来的原因很直接样本的登记和查询是高频接口而报告生成是低频重负载两者放在同一进程里报告导出时 GC 停顿会拖垮登记接口。拆开后各自水平扩展互不干扰。实际部署时常见做法是每个服务两个实例起 Docker 容器Nginx 指到网关网关再按路径前缀分发到各服务。这是这套源码里推荐的最小集群形态后续要加采集仪器对接服务也是往这个链路里塞一个新服务的事。2.2 样本表结构设计状态字段、关联关系、以及唯一约束样本库最核心的表是 sample_info设计得不好后面每个查询都难受。这份源码里的设计有几个值得记的点。首先是样本编号业务上要求一个样本一个号、扫码枪扫了就能定位所以 sample_no 上建了唯一索引而且编号规则是「机构代码 日期 六位流水号」。第二是状态字段 status用 tinyint 而不是 varchar代码里用枚举类映射避免魔法数字散落在业务代码里。CREATE TABLE sample_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, sample_no VARCHAR(32) NOT NULL COMMENT 样本编号唯一, org_id BIGINT NOT NULL COMMENT 机构ID, sample_type TINYINT NOT NULL COMMENT 样本类型1-血液 2-尿液 3-组织, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-已登记 2-在库 3-检测中 4-已出报告 5-已废弃, source TINYINT NOT NULL DEFAULT 1 COMMENT 来源1-门诊 2-住院 3-体检, patient_id BIGINT DEFAULT NULL COMMENT 关联患者ID, location_id BIGINT DEFAULT NULL COMMENT 库位ID, collect_time DATETIME DEFAULT NULL COMMENT 采样时间, receive_time DATETIME DEFAULT NULL COMMENT 实验室接收时间, out_time DATETIME DEFAULT NULL COMMENT 出库时间, create_by VARCHAR(32) NOT NULL COMMENT 创建人, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_sample_no (sample_no), KEY idx_status (status), KEY idx_org_receive (org_id, receive_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT样本主表;这个表结构的逻辑说明unique key 保证了并发登记时同一个编号不会写入两条这是幂等的最底层保障。idx_status 单独建索引是因为样本列表页最常用的筛选条件是状态这个查询频率远高于其他条件。idx_org_receive 联合索引是给机构维度的接收时间范围查询用的比如「查询某机构最近三天的登记样本」。注意这里没有用外键约束跨服务的表关联靠业务字段——org_id 关联 system-service 的机构表patient_id 关联患者主数据这是微服务架构下的常见做法为了服务自治放弃数据库外键换取的是拆分部署时不用处理库表之间的物理依赖。没有单独的样本扩展字段表是因为这类系统在第一次上线后很快会面临「同一个样本要多存几项结果」的需求。源码里预留了一个 JSON 字段 ext_info用 MySQL 5.7 的 JSON 类型存储特殊项目批次、冻融次数这些结构化程度不高的属性避免大面积改表。这个取舍很实用值得保留。2.3 Nacos 注册与配置服务发现和配置中心的落地写法微服务之间调用的第一步是让 sample-service 能找到 test-service。这套源码用的注册中心是 Nacos配置分两层bootstrap.yml 放 Nacos 地址和命名空间application.yml 放业务配置。注意 Nacos 的命名空间一定要建环境隔离就靠它——同一个 Nacos 集群上跑 dev、test、prod 三个命名空间DEVOPS 推广不下去的项目往往就是因为这套隔离没做。# bootstrap.yml spring: application: name: sample-service cloud: nacos: server-addr: 192.168.1.10:8848 username: nacos password: nacos namespace: prod-01 discovery: group: LIMS_GROUP config: group: LIMS_GROUP file-extension: yml# application.yml 在 Nacos 配置中心里维护的部分 server: port: 8102 spring: datasource: url: jdbc:mysql://192.168.1.20:3306/lims_sample?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: lims_app password: ENC(abc123...) driver-class-name: com.mysql.cj.jdbc.Driver redis: host: 192.168.1.30 port: 6379 mybatis-plus: mapper-locations: classpath*:/mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这段配置的关键点有两个。第一个是 group 保持一致服务的 discovery 和 config 都写了 LIMS_GROUP不同服务如果 group 不一致服务到服务之间会发现不了。第二个是密码加密配置中心里不落明文密码常见做法是 jasypt 加密后用 ENC() 包裹启动时通过 -Djasypt.encryptor.passwordxxx 注入密钥。每台服务器上的密钥放在环境变量里代码库里只保留密文避免配置文件泄露导致数据库权限丢失。这些看着是小事但生产环境出过乱子之后就会明白配置项的值错了服务起不来配置项的值泄露了整个库都保不住后者比前者可怕得多。3. 样本核心流程实现从登记到出库的状态机代码3.1 状态机定义为什么用状态码而不是直接改字段样本的整个生命周期本质上是一个状态机登记后进入待入库入库后变成在库检验科接单后变成检测中结果审核通过后变成已出报告不合格或超期就变成已废弃。这套源码把状态流转收敛到了一个地方而不是散落在各个 service 里随手 setStatus。这么做的好处是你可以写出一个完整的流转矩阵哪些路径合法、哪些路径非法一目了然。合法流转路径只有这几条1→2登记后入库、2→3在库样本被检测任务取走、3→4检测完成并出报告、2→5在库样本超期废弃、1→5登记作废。除此之外都是非法操作比如 3→2 就是样本还没有出结果就回到在库这在业务上意味着检测中断必须有强制备注才能执行。源码里用一个 StatusFlowValidator 组件承载这套规则每次更新状态前先校验合法性不合法直接抛业务异常。3.2 样本登记接口用策略模式处理三种样本类型样本登记在不同机构里采集信息不一样门诊样本要关联患者主索引体检样本要关联批次号科研样本要记录捐赠协议编号。如果用 if else 嵌套每加一种样本类型就改一遍主流程代码很快变成无人敢碰的状态。这套源码里做了策略模式把不同样本类型的登记逻辑拆成独立实现主流程只依赖接口。public interface SampleRegisterHandler { // 返回该策略支持的样本类型 SampleType type(); // 登记前的业务校验例如门诊样本校验患者是否存在 void validate(SampleRegisterDTO dto); // 执行入库逻辑 void handle(SampleRegisterDTO dto); }Component public class OutpatientSampleHandler implements SampleRegisterHandler { Override public SampleType type() { return SampleType.OUTPATIENT; } Override public void validate(SampleRegisterDTO dto) { // 门诊样本必须关联有效患者 if (dto.getPatientId() null) { throw new BizException(门诊样本必须关联患者); } } Override public void handle(SampleRegisterDTO dto) { // 生成样本编号机构代码日期流水 dto.setSampleNo(sampleNoGenerator.generate(dto.getOrgId())); // 初始化样本状态为已登记 saveSample(dto); // 写入状态流转日志记录初始状态 statusLogService.append(dto.getSampleNo(), SampleStatus.REGISTERED, null); } }调用侧把 Spring 容器里所有 SampleRegisterHandler 的实现注入到一个 Map 里key 是样本类型value 是处理器实例登记时直接按类型取。Service public class SampleRegisterService { private final MapSampleType, SampleRegisterHandler handlerMap; public SampleRegisterService(ListSampleRegisterHandler handlers) { this.handlerMap handlers.stream() .collect(Collectors.toMap(SampleRegisterHandler::type, Function.identity())); } Transactional(rollbackFor Exception.class) public void register(SampleRegisterDTO dto) { SampleRegisterHandler handler handlerMap.get(SampleType.of(dto.getSampleType())); if (handler null) { throw new BizException(不支持的样本类型); } handler.validate(dto); handler.handle(dto); } }这段实现里有两个点值得说明。第一handlerMap 用 Spring 注入 List 再转 Map以后新增一种样本类型时只加一个 Component 类不用改注册服务一行代码。第二 Transactional 加在注册服务上而不是单个 handler 上核心原因是一个样本登记动作可能涉及主表写入、状态日志写入、库存初始记录写入任何一个失败都要整体回滚。方法切面标注 rollbackFor Exception.class是为了让自定义业务异常也能触发回滚——Spring 默认只在 RuntimeException 上回滚如果你抛出 BizException 且它继承了 Exception 而不是 RuntimeException不指定这个参数就可能出现主表没写进去但状态日志写了的情况非常隐蔽。听起来像玄学但这种问题是真实发生过的血泪经验。3.3 入库与出库库存数量的事务控制样本入库不只是改样本状态还要在 stock_record 里增加一条「某样本从某库位入库」的记录出库则相反。这两个动作跨了 sample_info 主表和 stock_record 流水表必须在同一个事务里完成。此外同类样本在同一个库位的库存总数是实时查询统计出来的高频并发下很容易出现库存数量对不上的情况。源码里做了一个很实用的设计在库位的维度加一个 version 字段每次库存变更时比对版本号防止并发覆盖。Transactional(rollbackFor Exception.class) public void inbound(StockInboundDTO dto) { // 1. 更新样本状态 SampleInfo sample sampleMapper.selectByNo(dto.getSampleNo()); if (sample null) { throw new BizException(样本不存在); } // 2. 更新库位库存乐观锁控制并发 StockLocation location stockLocationMapper.selectByIdForUpdate(dto.getLocationId()); if (location null) { throw new BizException(库位不存在); } int updated stockLocationMapper.increaseStock( dto.getLocationId(), 1, location.getVersion()); if (updated 0) { throw new BizException(库位库存更新冲突请重试); } // 3. 写入库存流水 StockRecord record new StockRecord(); record.setSampleNo(dto.getSampleNo()); record.setLocationId(dto.getLocationId()); record.setAction(StockAction.INBOUND); stockRecordMapper.insert(record); }这个入库逻辑的细节在于 selectByIdForUpdate 和 increaseStock 的组合。selectByIdForUpdate 是悲观锁先锁住库位行保证后续两步操作之间没有其他事务插入适合单个库位的严格一致场景。increaseStock 在 SQL 层面用 version 做条件更新返回 0 表示版本已被其他事务修改直接抛异常让用户重试。业务上我倾向在样本入库这种低频但要求强一致的场景用悲观锁而在批量出库这种高频场景改成乐观锁两种策略都在源码里有对应实现根据接口调用频率灵活切换。如果你只用了乐观锁两管样本同时入同一个空库位后提交的事务会一直冲突直到放弃调用方体验就很差只用了悲观锁同一库位的并发扣减容易形成锁等待数据库连接池很快被占满两类锁搭配使用才是这个场景的正解。4. 批量导入与条码打印样本库运维效率的关键实现4.1 Excel 批量导入用 EasyExcel 做校验与幂等样本库上线三个月后最大的日常工作量是批量补录历史样本。手工一条条录一天能录三百条就算快而且极易在样本编号和采样时间上录错。源码用 EasyExcel 写了一个批量导入组件核心诉求是两件事格式错误提前拦截和重复导入不产生脏数据。public class SampleImportListener extends AnalysisEventListenerSampleImportRow { private final ListSampleImportRow validRows new ArrayList(); private final ListString errorMessages new ArrayList(); private final SetString existingNos; private static final int BATCH_SIZE 500; public SampleImportListener(SetString existingNos) { this.existingNos existingNos; } Override public void invoke(SampleImportRow row, AnalysisContext context) { // 行级校验必需字段不为空、日期格式合法、状态值在枚举范围内 if (row.getSampleNo() null || row.getSampleNo().trim().isEmpty()) { errorMessages.add(第 context.readRowHolder().getRowIndex() 行样本编号为空); return; } if (existingNos.contains(row.getSampleNo().trim())) { errorMessages.add(第 context.readRowHolder().getRowIndex() 行样本编号已存在); return; } validRows.add(row); } Override public void doAfterAllAnalysed(AnalysisContext context) { // 所有行解析完毕后统一入库 if (validRows.isEmpty()) { return; } for (int i 0; i validRows.size(); i BATCH_SIZE) { ListSampleImportRow batch validRows.subList(i, Math.min(i BATCH_SIZE, validRows.size())); sampleMapper.batchInsert(batch); } } }把这个监听器接入导入接口时existingNos 是提前从数据库查出的全部样本编号集合一次性放到内存里用于判重。文件量低于十万条时这种方案够用但如果你的库里有上百万历史样本一次性查全量编号会 OOM常见做法是改成分批查询每解析一百行查一次数据库查不到的才放行。这个组件里配的是一个可切换的判重器内存版和数据库版各有实现按数据规模选型。还有一点需要注意EasyExcel 解析时每行触发 invoke 方法如果你在 invoke 里直接调用 mapper 插入数据量大时事务会很大而且解析中断时已插入的数据无法回滚所以源码是在 doAfterAllAnalysed 里统一批量插入解析和入库两个阶段彻底分离出错时定位也更容易。4.2 条码生成扫码枪与标签打印的编码坑样本的条码在扫码枪扫出来的那一瞬间最容易出问题的是两个点字符集和解析协议。管子上贴的条码基本都是 Code128因为密度高、支持字母数字混合、扫码枪兼容性好。源码用 ZXing 生成一维条码图片然后按标签模板排版打印。生成条码时几个参数要设对宽度 300 像素、高度 80 像素、是否带可读字符设置为 true——这样下方会显示一列数字方便条码磨损后人工识别。public BufferedImage generateBarcode(String content) { // 样本编号是纯数字时用 CODE128B混合字符时用 CODE128C Code128Writer writer new Code128Writer(); MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.MARGIN, 10); BitMatrix bitMatrix writer.encode(content, BarcodeFormat.CODE_128, 300, 80, hints); BufferedImage image new BufferedImage(300, 80, BufferedImage.TYPE_INT_RGB); // 将 BitMatrix 渲染到 BufferedImage return image; }这里真正的坑在于样本编号如果包含汉字Code128 是编码不了的扫码枪扫出来会乱码。所以样本编号规则里只允许大写字母、数字和连字符从源头上规避解析问题。还有一个现场经常遇到的问题贴标机打印几十个标签后标签上的条码发虚扫码枪识别不了。排查看下来往往是图片按百分比缩放导致像素拉伸ZXing 生成的图片是 300x80打印驱动再用 72dpi 和 300dpi 转换条码线条就糊了。源码里直接把条码图片按 1:1 输出到打印机不走缩放路径这个问题就消失了。这些属于看着小、影响面却很大的细节做过打印标签的现场工程师应该都有共鸣。4.3 库存预警定时任务扫表的实现样本库最怕超期样本没人管血液样本常温下只能放几天组织样本冻存时间长了结果也会受影响。源码在 stock-service 里做了定时任务每天凌晨扫一次全库的在库样本把超期未出库的样本和低于下限的库位单独拉成预警清单第二天早上检验科上班第一眼就能看到。Component public class StockAlertJob { private final StockLocationMapper stockLocationMapper; private final SampleInfoMapper sampleInfoMapper; Scheduled(cron 0 30 2 * * ?) public void scanExpiredSamples() { // 查出所有在库样本 ListSampleInfo inStockSamples sampleInfoMapper.selectByStatus(SampleStatus.IN_STOCK); // 按库位分组统计对比库存下限 MapLong, Long stockCountMap inStockSamples.stream() .collect(Collectors.groupingBy(SampleInfo::getLocationId, Collectors.counting())); ListStockAlert alerts new ArrayList(); stockLocationMapper.selectAll().forEach(location - { long currentCount stockCountMap.getOrDefault(location.getId(), 0L); if (currentCount location.getMinThreshold()) { StockAlert alert new StockAlert(); alert.setLocationId(location.getId()); alert.setAlertType(StockAlertType.BELOW_MIN); alert.setCurrentCount(currentCount); alert.setMinThreshold(location.getMinThreshold()); alerts.add(alert); } }); // 批量插入预警记录 if (!alerts.isEmpty()) { stockAlertMapper.batchInsert(alerts); } } }定时任务本身不复杂但有几个参数值得细调。cron 表达式0 30 2 * * ?表示每天凌晨 2:30 执行选这个时间点是因为凌晨做完整库扫描对数据库的压力最大而这个时段业务几乎为零。如果你库里的样本量超过五十万条全表扫描和分组统计会造成长时间慢查询常见做法是改成增量扫描每天只扫前一天有状态变更的样本再配合一个每周一次的全量扫描做兜底。预警记录生成后接入企业微信或钉钉机器人推送是另一个扩展点源码里留了 Webhook 配置项往那边发一个 JSON POST 就可以收到样本超期和库位不足的通知现场运维非常依赖这个。5. SpringCloud 踩坑清单部署和读写样本数据时常见的五个问题5.1 现象服务间调用超时样本数据重复提交用户点一次「登记样本」按钮转了两分钟圈然后页面提示失败用户再点一次结果样本库里出现两条相同编号的记录。这种问题在微服务架构下特别常见。原因是 gateway 调用 sample-service、sample-service 调用 test-service 的链路中某个环节网络抖动OpenFeign 默认的读超时只有 1 秒连接超时 2 秒源码里如果忘记配置超时时间一次正常的批量登记接口可能因为 GC 停顿就被判定为超时。解决网关层面加全局超时配置sample-service 调用 test-service 的重试次数设置为 0改为由上游调用方做幂等控制。样本登记接口增加请求幂等键前端每次点击生成一个 requestId后端在 Redis 里按 requestId 判重同样的请求在 30 分钟内只处理一次。这是标准解法但注意重试次数不能盲目加大——样本登记不是纯查询重试可能导致一管样本在未出库时被重复扣减库存宁可失败让用户重试也不要自动重试造成资金或库存方面的对不上。5.2 现象数据库连接池被打满接口大面积超时样本批量导入功能上线第一天导入一万条记录时数据库连接池直接耗尽所有查询接口变成几十秒响应。原因是批量导入使用了线程池并发处理 Excel 分片每个线程都从连接池获取连接而连接池默认最大值是 10五十个线程同时跑就把连接池打满了。这是一个典型的「并发规划没配到系统资源上」的问题。解决按并发线程数调 HikariCP 参数。maximumPoolSize 调到线程池大小加 20 左右minimumIdle 保持 10 即可。另一个更稳的做法是把导入流程改成生产者消费者模式Excel 解析完成的行先写入本地消息表再由消费者单线程批量入库数据库压力完全可控。这套源码里两个模式都有小数据量直接用并发导入大数据量切到生产者消费者切换点在两万条。5.3 现象Nacos 配置修改了不生效重启也不行在 Nacos 控制台改了一个 MySQL 连接参数等了五分钟服务还是旧值重启也一样。最常见的原因是修改的 dataId 和服务实际读取的配置不对应。Nacos 的 dataId 命名规则是${spring.application.name}.${file-extension}如果你在控制台新建配置时写的是 sample-service.yaml而服务端的 spring.cloud.nacos.config.file-extension 配置的是 yml 后缀那这个配置永远不会被加载。还有一个隐蔽点如果你同时在 bootstrap.yml 里配了 config.group控制台新建配置时 group 必须一致标签页里默认的 DEFAULT_GROUP 往往被忽略。解决统一 dataId 命名规则所有环境都用${spring.application.name}.ymlgroup 固定为 LIMS_GROUP不要两种写法换着用。配置修改后先看日志里是否有 Fetching config from server 的记录确认服务确实拉取到了新配置再继续下一步。如果是敏感配置修改后不生效还要检查是否开了 spring.cloud.nacos.config.refresh-enabledfalse这个开关关了以后配置中心推送只是存下来不会触发上下文刷新。5.4 现象样本入库成功了但库存流水缺失跨服务调用时出现的数据不一致是微服务架构里最让人头疼的问题之一。样本入库需要在 sample-service 更新样本状态同时在 stock-service 记录库存流水如果两边的服务网络隔离或者其中一个服务事务回滚就会出现样本是「在库」状态但查不到入库流水。这种问题一旦发生后续所有库存统计都是错的且极难追溯。解决在跨服务的写操作之间引入分布式事务。源码里适配了 Seata AT 模式sample-service 执行本地事务时同时注册分支事务stock-service 的库存操作也作为分支事务参与全局事务任何一个分支失败都会触发逆向前置镜像回滚。如果你的业务允许最终一致也可以用本地消息表sample-service 写完样本状态后往消息表插一条待发送的流水事件然后定时把事件投递到 stock-service消费成功就更新消息状态重试到一定次数就触发人工核对。两种方案我都用过强一致场景直接 Seata重量级但可靠高吞吐场景用消息表注意消息表中的事件内容要包含完整的样本编号和库位 ID方便失败时人工补录。5.5 现象样本列表深分页越来越慢样本列表页翻到几百页之后接口响应从 100 毫秒涨到 3 秒。MySQL 中 limit 的深分页是已知的性能黑洞limit 100000, 20 会先扫描前 100020 行再丢弃前 100000 行代价巨大。业务上线初期没人会翻到这么深但当样本量冲到几百万时这个翻页问题就是必然出现的。解决改掉深分页换成三种常见方案。第一种是游标分页列表接口接受 lastId 参数查询条件变成 where id lastId order by id asc limit 20翻页性能不再受页数影响。第二种是限定最大翻页深度超过一百页强制用户使用时间范围过滤从业务上把深分页场景消灭掉。第三种是对于默认列表页直接限定只查最近一个月的数据历史数据走专门的查询入口。源码里默认用的是游标分页改造工作量不大却是样本库能长期扛住数据增长的保障。6. 进阶技巧用一个临时表和批量更新接口把库存校准做到分钟级盘库是样本库每个月固定要做的苦差事。实物数一遍系统里导出一遍两边对不上的地方全靠人工逐条核对几百条差异记录往往要核一整晚。这个技巧是把校准过程拆成一个「两阶段对账」把差异核对成本降到分钟级。第一阶段把所有库位和样本清单导入一张临时表 stock_count_actual字段就设三个location_id、sample_no、counted_status。盘点人员在手机上按库位扫码录入实物状态录完点击上传数据进临时表。第二阶段写一个批量对账接口把临时表和样本主表按照库位 ID join逐个比对样本编号集合和样本状态。两边一致的就跳过不一致的生成差异清单最后在页面上一键确认后再批量更新样本主表状态。Transactional(rollbackFor Exception.class) public ListStockDiff reconcile() { // 1. 从临时表读出实际库存按库位分组 ListActualStock actualList actualStockMapper.selectAllGroupByLocation(); // 2. 从样本主表读出系统库存按库位分组 ListSystemStock systemList sampleInfoMapper.selectInStockGroupByLocation(); // 3. 逐库位比对生成差异记录 ListStockDiff diffs new ArrayList(); for (SystemStock sys : systemList) { ActualStock actual findActual(actualList, sys.getLocationId()); if (actual null || actual.getCount() ! sys.getCount()) { StockDiff diff new StockDiff(); diff.setLocationId(sys.getLocationId()); diff.setSystemCount(sys.getCount()); diff.setActualCount(actual null ? 0 : actual.getCount()); diffs.add(diff); } } return diffs; }这里的核心不是 SQL 有多复杂而是把对账逻辑做成一个可反复执行的独立接口盘库人员随时可以触发差异清单直接在页面上标注「以实物为准」或「以系统为准」确认后批量更新样本状态。执行时注意两个验证步骤第一步先核对参与对账的样本范围一致——临时表里的样本编号必须全部存在于样本主表否则一盘库就会出现大量「系统没有但实物有」的记录原因往往是新到的样本还没入库就贴了条码上架第二步对账完成后跑一个统计查询连续三次核对「各库位总数 各状态样本数之和 差异数」对上了才算成功。这个校准接口上线后盘库时间从原来的四小时压到了半小时以内。从那以后我每次上线涉及样本状态变更的接口都会强制走一遍这个对账流程先导出一份变更前后的样本状态快照再模拟一次差异清单生成确认没有多改、漏改才敢发布。这套方法帮我在后面的三个实验室项目里少踩了无数次数据不一致的坑希望帮到你。本文还有配套的精品资源点击获取
返回列表