fix: 强化出库单事务与编码异常处理
This commit is contained in:
@@ -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, "出入库单明细不存在");
|
||||
|
||||
|
||||
@@ -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<String> codeResult = codeGenApi.generate(codeGenerateReqDTO);
|
||||
if (codeResult == null || !codeResult.isSuccess() || codeResult.getData() == null) {
|
||||
codeGenerateReqDTO.setRuleCode(ruleCode);
|
||||
CommonResult<String> 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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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<String> result;
|
||||
private RuntimeException failure;
|
||||
|
||||
void setResult(CommonResult<String> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
void setFailure(RuntimeException failure) {
|
||||
this.failure = failure;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<String> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user