From c4c8911a80eb1402532977cbf74d69c8d441914f Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Wed, 22 Jul 2026 15:00:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=BC=BA=E5=8C=96=E5=87=BA=E5=BA=93?= =?UTF-8?q?=E5=8D=95=E4=BA=8B=E5=8A=A1=E4=B8=8E=E7=BC=96=E7=A0=81=E5=BC=82?= =?UTF-8?q?=E5=B8=B8=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/wms/enums/ErrorCodeConstants.java | 2 +- .../iostorinv/IostorInvServiceImpl.java | 17 ++- .../IostorInvServiceLocalSpringTest.java | 101 +++++++++++++++++- .../sql/iostorinv/create-outbound.sql | 2 +- 4 files changed, 112 insertions(+), 10 deletions(-) diff --git a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java index 955f2fdb..1e263ad5 100644 --- a/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java +++ b/nl-module-wms/nl-module-wms-api/src/main/java/cn/code/nl/module/wms/enums/ErrorCodeConstants.java @@ -12,7 +12,7 @@ public interface ErrorCodeConstants { // ================ 出入库表相关错误码 ================= ErrorCode IOSTOR_INV_NOT_EXISTS = new ErrorCode(4, "出入库单主表不存在"); - ErrorCode IOSTOR_INV_CODE_GENERATE_FAILED = new ErrorCode(7, "单据号生成失败"); + ErrorCode IOSTOR_INV_CODE_GENERATE_FAILED = new ErrorCode(6_000_4, "单据号生成失败"); ErrorCode IOSTORINV_DTL_NOT_EXISTS = new ErrorCode(5, "出入库单明细不存在"); diff --git a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java index ffccd527..60ec58d1 100644 --- a/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java +++ b/nl-module-wms/nl-module-wms-server/src/main/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceImpl.java @@ -2,6 +2,7 @@ package cn.code.nl.module.wms.service.iostorinv; import cn.hutool.core.collection.CollUtil; import cn.hutool.core.util.IdUtil; +import cn.hutool.core.util.StrUtil; import cn.code.nl.framework.common.pojo.CommonResult; import cn.code.nl.module.base.api.codegen.CodeGenApi; import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; @@ -53,12 +54,20 @@ public class IostorInvServiceImpl implements IostorInvService { @Override @Transactional(rollbackFor = Exception.class) public String createOutbound(IostorInvCreateReqVO reqVO) { + String ruleCode = "IO_CODE"; CodeGenerateReqDTO codeGenerateReqDTO = new CodeGenerateReqDTO(); - codeGenerateReqDTO.setRuleCode("IO_CODE"); - CommonResult codeResult = codeGenApi.generate(codeGenerateReqDTO); - if (codeResult == null || !codeResult.isSuccess() || codeResult.getData() == null) { + codeGenerateReqDTO.setRuleCode(ruleCode); + CommonResult codeResult; + try { + codeResult = codeGenApi.generate(codeGenerateReqDTO); + } catch (RuntimeException ex) { + log.error("[createOutbound][生成出库单号异常,规则编码:{}]", ruleCode, ex); + // 当前统一业务异常不支持 cause,远程异常堆栈已在此处完整记录。 + throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); + } + if (codeResult == null || !codeResult.isSuccess() || StrUtil.isBlank(codeResult.getData())) { String remoteMessage = codeResult == null ? "返回结果为空" : codeResult.getMsg(); - log.error("[createOutbound][生成出库单号失败,远程信息:{}]", remoteMessage); + log.error("[createOutbound][生成出库单号失败,规则编码:{},远程信息:{}]", ruleCode, remoteMessage); throw exception(IOSTOR_INV_CODE_GENERATE_FAILED); } diff --git a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java index 5b3f3365..3d68e44f 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java +++ b/nl-module-wms/nl-module-wms-server/src/test/java/cn/code/nl/module/wms/service/iostorinv/IostorInvServiceLocalSpringTest.java @@ -7,12 +7,17 @@ import cn.code.nl.module.base.api.codegen.dto.CodeGenerateReqDTO; import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO; import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; import jakarta.annotation.Resource; +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; import org.apache.ibatis.session.SqlSessionFactory; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mybatis.spring.annotation.MapperScan; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; +import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.DriverManagerDataSource; @@ -33,6 +38,7 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static cn.code.nl.module.wms.enums.ErrorCodeConstants.IOSTOR_INV_CODE_GENERATE_FAILED; @SpringJUnitConfig @ContextConfiguration(classes = IostorInvServiceLocalSpringTest.TestConfiguration.class) @@ -45,10 +51,14 @@ class IostorInvServiceLocalSpringTest { private JdbcTemplate jdbcTemplate; @Resource private TestCodeGenApi codeGenApi; + @Resource + private SecondDetailInsertFailureAspect detailInsertFailureAspect; @BeforeEach void resetCodeGenApi() { codeGenApi.setResult(CommonResult.success("OUT-TEST-0001")); + codeGenApi.setFailure(null); + detailInsertFailureAspect.disable(); } @Test @@ -104,17 +114,43 @@ class IostorInvServiceLocalSpringTest { () -> iostorInvService.createOutbound(buildRequest())); assertEquals("单据号生成失败", exception.getMessage()); + assertEquals(IOSTOR_INV_CODE_GENERATE_FAILED.getCode(), exception.getCode()); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); } @Test - void shouldRollbackHeaderAndFirstDetailWhenSecondDetailInsertFails() { - IostorInvCreateReqVO request = buildRequest(); - request.getDetails().get(1).setSourceBillCode(null); + void shouldRejectBlankGeneratedCodeWithoutInsertingAnything() { + codeGenApi.setResult(CommonResult.success(" ")); - assertThrows(RuntimeException.class, () -> iostorInvService.createOutbound(request)); + ServiceException exception = assertThrows(ServiceException.class, + () -> iostorInvService.createOutbound(buildRequest())); + assertEquals(IOSTOR_INV_CODE_GENERATE_FAILED.getCode(), exception.getCode()); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + + @Test + void shouldConvertRemoteCodeGenerationExceptionToBusinessError() { + codeGenApi.setFailure(new IllegalStateException("远程连接失败")); + + ServiceException exception = assertThrows(ServiceException.class, + () -> iostorInvService.createOutbound(buildRequest())); + + assertEquals(IOSTOR_INV_CODE_GENERATE_FAILED.getCode(), exception.getCode()); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); + assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); + } + + @Test + void shouldRollbackRealInsertsWhenInjectedSecondDetailFailureOccurs() { + detailInsertFailureAspect.failOnSecondInsert(); + + assertThrows(DataAccessResourceFailureException.class, + () -> iostorInvService.createOutbound(buildRequest())); + + assertEquals(1, detailInsertFailureAspect.getRowsVisibleBeforeFailure()); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class)); assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class)); } @@ -154,6 +190,7 @@ class IostorInvServiceLocalSpringTest { @Configuration(proxyBeanMethods = false) @EnableTransactionManagement + @EnableAspectJAutoProxy @MapperScan(basePackages = {"cn.code.nl.module.wms.dal.mysql.iostorinv", "cn.code.nl.module.wms.dal.mysql.iostorinvdtl"}) static class TestConfiguration { @@ -197,18 +234,31 @@ class IostorInvServiceLocalSpringTest { TestCodeGenApi codeGenApi() { return new TestCodeGenApi(); } + + @Bean + SecondDetailInsertFailureAspect detailInsertFailureAspect(JdbcTemplate jdbcTemplate) { + return new SecondDetailInsertFailureAspect(jdbcTemplate); + } } static class TestCodeGenApi implements CodeGenApi { private CommonResult result; + private RuntimeException failure; void setResult(CommonResult result) { this.result = result; } + void setFailure(RuntimeException failure) { + this.failure = failure; + } + @Override public CommonResult generate(CodeGenerateReqDTO reqDTO) { assertEquals("IO_CODE", reqDTO.getRuleCode()); + if (failure != null) { + throw failure; + } return result; } @@ -217,4 +267,47 @@ class IostorInvServiceLocalSpringTest { throw new UnsupportedOperationException(); } } + + /** 在第二次明细真实插入的调用点注入数据库访问故障,用于验证事务回滚。 */ + @Aspect + static class SecondDetailInsertFailureAspect { + private final JdbcTemplate jdbcTemplate; + private boolean enabled; + private int insertCount; + private int rowsVisibleBeforeFailure; + + SecondDetailInsertFailureAspect(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + void failOnSecondInsert() { + enabled = true; + insertCount = 0; + rowsVisibleBeforeFailure = 0; + } + + void disable() { + enabled = false; + insertCount = 0; + rowsVisibleBeforeFailure = 0; + } + + int getRowsVisibleBeforeFailure() { + return rowsVisibleBeforeFailure; + } + + @Around("bean(iostorinvDtlMapper) && execution(* insert(..))") + Object injectFailureOnSecondInsert(ProceedingJoinPoint joinPoint) throws Throwable { + if (!enabled) { + return joinPoint.proceed(); + } + insertCount++; + if (insertCount == 2) { + rowsVisibleBeforeFailure = jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class); + throw new DataAccessResourceFailureException("测试注入:第二条明细写入失败"); + } + return joinPoint.proceed(); + } + } } diff --git a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql index 564c3b5b..d684f115 100644 --- a/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql +++ b/nl-module-wms/nl-module-wms-server/src/test/resources/sql/iostorinv/create-outbound.sql @@ -16,7 +16,7 @@ CREATE TABLE wms_iostorinvdtl ( iostorinvdtl_id VARCHAR(64) PRIMARY KEY, iostorinv_id VARCHAR(64), seq_no INT, pcsn VARCHAR(128), bill_status VARCHAR(32), qty_unit_id VARCHAR(64), qty_unit_name VARCHAR(64), plan_qty DECIMAL(18, 3), real_qty DECIMAL(18, 3), source_billdtl_id VARCHAR(64), - source_bill_type VARCHAR(64), source_bill_code VARCHAR(64) NOT NULL, source_bill_table VARCHAR(128), + source_bill_type VARCHAR(64), source_bill_code VARCHAR(64), source_bill_table VARCHAR(128), remark VARCHAR(255), assign_qty DECIMAL(18, 3), unassign_qty DECIMAL(18, 3), material_code VARCHAR(64), source_load_port VARCHAR(64), callback_strategy VARCHAR(255), material_id VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP,