feat: 事务保存出库单及明细
This commit is contained in:
@@ -38,6 +38,13 @@ public class IostorInvController {
|
||||
@Resource
|
||||
private IostorInvService iostorInvService;
|
||||
|
||||
@PostMapping("/createOutbound")
|
||||
@Operation(summary = "创建出库单及明细")
|
||||
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:create')")
|
||||
public CommonResult<String> createOutbound(@Valid @RequestBody IostorInvCreateReqVO reqVO) {
|
||||
return success(iostorInvService.createOutbound(reqVO));
|
||||
}
|
||||
|
||||
@GetMapping("/availableInventoryPage")
|
||||
@Operation(summary = "获得可用库存分页")
|
||||
@PreAuthorize("@ss.hasPermission('wms:iostor-inv:query')")
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
package cn.code.nl.module.wms.service.iostorinv;
|
||||
|
||||
import cn.hutool.core.collection.CollUtil;
|
||||
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;
|
||||
import cn.code.nl.module.wms.dal.dataobject.iostorinvdtl.IostorinvDtlDO;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.*;
|
||||
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.*;
|
||||
import cn.code.nl.module.wms.dal.dataobject.iostorinv.IostorInvDO;
|
||||
@@ -28,6 +34,7 @@ import static cn.code.nl.module.wms.enums.ErrorCodeConstants.*;
|
||||
*/
|
||||
@Service
|
||||
@Validated
|
||||
@Slf4j
|
||||
public class IostorInvServiceImpl implements IostorInvService {
|
||||
|
||||
@Resource
|
||||
@@ -36,9 +43,59 @@ public class IostorInvServiceImpl implements IostorInvService {
|
||||
@Resource
|
||||
private IostorinvDtlMapper iostorinvDtlMapper;
|
||||
|
||||
@Resource
|
||||
private CodeGenApi codeGenApi;
|
||||
|
||||
/**
|
||||
* 创建出库单及其明细。
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public String createOutbound(IostorInvCreateReqVO reqVO) {
|
||||
throw new UnsupportedOperationException("出库单聚合保存功能尚未实现");
|
||||
CodeGenerateReqDTO codeGenerateReqDTO = new CodeGenerateReqDTO();
|
||||
codeGenerateReqDTO.setRuleCode("IO_CODE");
|
||||
CommonResult<String> codeResult = codeGenApi.generate(codeGenerateReqDTO);
|
||||
if (codeResult == null || !codeResult.isSuccess() || codeResult.getData() == null) {
|
||||
String remoteMessage = codeResult == null ? "返回结果为空" : codeResult.getMsg();
|
||||
log.error("[createOutbound][生成出库单号失败,远程信息:{}]", remoteMessage);
|
||||
throw exception(IOSTOR_INV_CODE_GENERATE_FAILED);
|
||||
}
|
||||
|
||||
BigDecimal totalWeight = reqVO.getDetails().stream()
|
||||
.map(IostorInvCreateReqVO.Detail::getPlanQty)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
IostorInvDO iostorInv = new IostorInvDO();
|
||||
iostorInv.setBillCode(codeResult.getData());
|
||||
iostorInv.setIoType("OUT");
|
||||
iostorInv.setBillType(reqVO.getBillType());
|
||||
iostorInv.setBizDate(reqVO.getBizDate());
|
||||
iostorInv.setStorId(reqVO.getStorId());
|
||||
iostorInv.setBillStatus("生成");
|
||||
iostorInv.setRemark(reqVO.getRemark());
|
||||
iostorInv.setDetailCount(reqVO.getDetails().size());
|
||||
iostorInv.setTotalWeight(totalWeight);
|
||||
iostorInvMapper.insert(iostorInv);
|
||||
|
||||
for (int index = 0; index < reqVO.getDetails().size(); index++) {
|
||||
IostorInvCreateReqVO.Detail detail = reqVO.getDetails().get(index);
|
||||
IostorinvDtlDO detailDO = new IostorinvDtlDO();
|
||||
detailDO.setIostorinvId(iostorInv.getIostorinvId());
|
||||
detailDO.setSeqNo(index + 1);
|
||||
detailDO.setMaterialCode(detail.getMaterialCode());
|
||||
detailDO.setMaterialId(detail.getMaterialId());
|
||||
detailDO.setPcsn(detail.getPcsn());
|
||||
detailDO.setPlanQty(detail.getPlanQty());
|
||||
detailDO.setUnassignQty(detail.getPlanQty());
|
||||
detailDO.setAssignQty(BigDecimal.ZERO);
|
||||
detailDO.setQtyUnitId(detail.getQtyUnitId());
|
||||
detailDO.setQtyUnitName(detail.getQtyUnitName());
|
||||
detailDO.setSourceBillCode(detail.getSourceBillCode());
|
||||
detailDO.setSourceBillType(detail.getSourceBillType());
|
||||
detailDO.setSourceBilldtlId(detail.getSourceBilldtlId());
|
||||
detailDO.setRemark(detail.getRemark());
|
||||
iostorinvDtlMapper.insert(detailDO);
|
||||
}
|
||||
return iostorInv.getIostorinvId();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -1,38 +1,200 @@
|
||||
package cn.code.nl.module.wms.service.iostorinv;
|
||||
|
||||
import cn.code.nl.framework.common.exception.ServiceException;
|
||||
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;
|
||||
import cn.code.nl.module.wms.controller.admin.iostorinv.vo.IostorInvCreateReqVO;
|
||||
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||
import com.baomidou.mybatisplus.extension.incrementer.H2KeyGenerator;
|
||||
import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
import org.apache.ibatis.session.SqlSessionFactory;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
|
||||
import org.springframework.jdbc.datasource.DriverManagerDataSource;
|
||||
import org.springframework.test.context.ContextConfiguration;
|
||||
import org.springframework.test.context.jdbc.Sql;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.transaction.PlatformTransactionManager;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
import org.springframework.validation.beanvalidation.MethodValidationPostProcessor;
|
||||
|
||||
import javax.sql.DataSource;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
@SpringBootTest
|
||||
@ActiveProfiles("local")
|
||||
@SpringJUnitConfig
|
||||
@ContextConfiguration(classes = IostorInvServiceLocalSpringTest.TestConfiguration.class)
|
||||
@Sql("/sql/iostorinv/create-outbound.sql")
|
||||
class IostorInvServiceLocalSpringTest {
|
||||
|
||||
@Resource
|
||||
private IostorInvService iostorInvService;
|
||||
@Resource
|
||||
private JdbcTemplate jdbcTemplate;
|
||||
@Resource
|
||||
private TestCodeGenApi codeGenApi;
|
||||
|
||||
@Test
|
||||
void createOutboundShouldRejectEmptyDetails() {
|
||||
IostorInvCreateReqVO reqVO = new IostorInvCreateReqVO();
|
||||
reqVO.setBillType("OUTBOUND");
|
||||
reqVO.setStorId("stor-1");
|
||||
reqVO.setBizDate(LocalDateTime.now());
|
||||
reqVO.setDetails(Collections.emptyList());
|
||||
|
||||
ConstraintViolationException exception = assertThrows(ConstraintViolationException.class,
|
||||
() -> iostorInvService.createOutbound(reqVO));
|
||||
|
||||
assertTrue(exception.getConstraintViolations().stream()
|
||||
.anyMatch(violation -> violation.getPropertyPath().toString().contains("details")));
|
||||
@BeforeEach
|
||||
void resetCodeGenApi() {
|
||||
codeGenApi.setResult(CommonResult.success("OUT-TEST-0001"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldCreateOutboundHeaderAndDetailsInOneTransaction() {
|
||||
String id = iostorInvService.createOutbound(buildRequest());
|
||||
|
||||
assertNotNull(id);
|
||||
jdbcTemplate.queryForObject("SELECT * FROM wms_iostorinv WHERE iostorinv_id = ?", (rs, rowNum) -> {
|
||||
assertEquals("OUT-TEST-0001", rs.getString("bill_code"));
|
||||
assertEquals("OUT", rs.getString("io_type"));
|
||||
assertEquals("销售出库", rs.getString("bill_type"));
|
||||
assertEquals("STOR-01", rs.getString("stor_id"));
|
||||
assertEquals("生成", rs.getString("bill_status"));
|
||||
assertEquals("主表备注", rs.getString("remark"));
|
||||
assertEquals(2, rs.getInt("detail_count"));
|
||||
assertEquals(new BigDecimal("19.750"), rs.getBigDecimal("total_weight"));
|
||||
return null;
|
||||
}, id);
|
||||
|
||||
List<DetailRow> details = jdbcTemplate.query(
|
||||
"SELECT * FROM wms_iostorinvdtl WHERE iostorinv_id = ? ORDER BY seq_no",
|
||||
(rs, rowNum) -> new DetailRow(rs.getInt("seq_no"), rs.getString("material_code"),
|
||||
rs.getString("material_id"), rs.getString("pcsn"), rs.getBigDecimal("plan_qty"),
|
||||
rs.getBigDecimal("assign_qty"), rs.getBigDecimal("unassign_qty"),
|
||||
rs.getString("qty_unit_id"), rs.getString("qty_unit_name"),
|
||||
rs.getString("source_bill_code"), rs.getString("source_bill_type"),
|
||||
rs.getString("source_billdtl_id"), rs.getString("remark")), id);
|
||||
assertEquals(List.of(
|
||||
new DetailRow(1, "MAT-01", "MID-01", "PCSN-01", new BigDecimal("12.500"),
|
||||
BigDecimal.ZERO.setScale(3), new BigDecimal("12.500"), "KG", "千克",
|
||||
"SRC-01", "ORDER", "SRC-DTL-01", "明细一"),
|
||||
new DetailRow(2, "MAT-02", "MID-02", "PCSN-02", new BigDecimal("7.250"),
|
||||
BigDecimal.ZERO.setScale(3), new BigDecimal("7.250"), "KG", "千克",
|
||||
"SRC-02", "ORDER", "SRC-DTL-02", "明细二")), details);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotInsertAnythingWhenCodeGenerationFails() {
|
||||
codeGenApi.setResult(CommonResult.error(500, "编码服务暂不可用"));
|
||||
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> iostorInvService.createOutbound(buildRequest()));
|
||||
|
||||
assertEquals("单据号生成失败", exception.getMessage());
|
||||
assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinv", Integer.class));
|
||||
assertEquals(0, jdbcTemplate.queryForObject("SELECT COUNT(*) FROM wms_iostorinvdtl", Integer.class));
|
||||
}
|
||||
|
||||
private IostorInvCreateReqVO buildRequest() {
|
||||
IostorInvCreateReqVO request = new IostorInvCreateReqVO();
|
||||
request.setBillType("销售出库");
|
||||
request.setStorId("STOR-01");
|
||||
request.setBizDate(LocalDateTime.of(2026, 7, 22, 0, 0));
|
||||
request.setRemark("主表备注");
|
||||
request.setDetails(List.of(buildDetail("01", "12.500", "明细一"),
|
||||
buildDetail("02", "7.250", "明细二")));
|
||||
return request;
|
||||
}
|
||||
|
||||
private IostorInvCreateReqVO.Detail buildDetail(String suffix, String planQty, String remark) {
|
||||
IostorInvCreateReqVO.Detail detail = new IostorInvCreateReqVO.Detail();
|
||||
detail.setMaterialCode("MAT-" + suffix);
|
||||
detail.setMaterialId("MID-" + suffix);
|
||||
detail.setPcsn("PCSN-" + suffix);
|
||||
detail.setPlanQty(new BigDecimal(planQty));
|
||||
detail.setQtyUnitId("KG");
|
||||
detail.setQtyUnitName("千克");
|
||||
detail.setSourceBillCode("SRC-" + suffix);
|
||||
detail.setSourceBillType("ORDER");
|
||||
detail.setSourceBilldtlId("SRC-DTL-" + suffix);
|
||||
detail.setRemark(remark);
|
||||
return detail;
|
||||
}
|
||||
|
||||
private record DetailRow(Integer seqNo, String materialCode, String materialId, String pcsn,
|
||||
BigDecimal planQty, BigDecimal assignQty, BigDecimal unassignQty,
|
||||
String qtyUnitId, String qtyUnitName, String sourceBillCode,
|
||||
String sourceBillType, String sourceBilldtlId, String remark) {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableTransactionManagement
|
||||
@MapperScan(basePackages = {"cn.code.nl.module.wms.dal.mysql.iostorinv",
|
||||
"cn.code.nl.module.wms.dal.mysql.iostorinvdtl"})
|
||||
static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
DataSource dataSource() {
|
||||
return new DriverManagerDataSource("jdbc:h2:mem:create_outbound;MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
}
|
||||
|
||||
@Bean
|
||||
SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
|
||||
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
|
||||
factory.setDataSource(dataSource);
|
||||
GlobalConfig.DbConfig dbConfig = new GlobalConfig.DbConfig();
|
||||
dbConfig.setKeyGenerators(List.of(new H2KeyGenerator()));
|
||||
factory.setGlobalConfig(new GlobalConfig().setDbConfig(dbConfig));
|
||||
org.apache.ibatis.session.Configuration configuration = new org.apache.ibatis.session.Configuration();
|
||||
configuration.setMapUnderscoreToCamelCase(true);
|
||||
factory.setConfiguration(configuration);
|
||||
return factory.getObject();
|
||||
}
|
||||
|
||||
@Bean
|
||||
PlatformTransactionManager transactionManager(DataSource dataSource) {
|
||||
return new DataSourceTransactionManager(dataSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
JdbcTemplate jdbcTemplate(DataSource dataSource) {
|
||||
return new JdbcTemplate(dataSource);
|
||||
}
|
||||
|
||||
@Bean
|
||||
static MethodValidationPostProcessor methodValidationPostProcessor() {
|
||||
return new MethodValidationPostProcessor();
|
||||
}
|
||||
|
||||
@Bean
|
||||
IostorInvService iostorInvService() {
|
||||
return new IostorInvServiceImpl();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestCodeGenApi codeGenApi() {
|
||||
return new TestCodeGenApi();
|
||||
}
|
||||
}
|
||||
|
||||
static class TestCodeGenApi implements CodeGenApi {
|
||||
private CommonResult<String> result;
|
||||
|
||||
void setResult(CommonResult<String> result) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<String> generate(CodeGenerateReqDTO reqDTO) {
|
||||
assertEquals("IO_CODE", reqDTO.getRuleCode());
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public CommonResult<String> preview(String ruleCode) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
DROP TABLE IF EXISTS wms_iostorinvdtl;
|
||||
DROP TABLE IF EXISTS wms_iostorinv;
|
||||
DROP SEQUENCE IF EXISTS wms_iostorinvdtl_seq;
|
||||
DROP SEQUENCE IF EXISTS wms_iostorinv_seq;
|
||||
|
||||
CREATE SEQUENCE wms_iostorinv_seq START WITH 1;
|
||||
CREATE SEQUENCE wms_iostorinvdtl_seq START WITH 1;
|
||||
|
||||
CREATE TABLE wms_iostorinv (
|
||||
iostorinv_id VARCHAR(64) PRIMARY KEY,
|
||||
bill_code VARCHAR(64), io_type VARCHAR(32), bill_type VARCHAR(64), biz_date TIMESTAMP,
|
||||
stor_id VARCHAR(64), stor_code VARCHAR(64), stor_name VARCHAR(128), source_id VARCHAR(64),
|
||||
source_name VARCHAR(128), source_type VARCHAR(64), total_qty DECIMAL(18, 3),
|
||||
total_weight DECIMAL(18, 3), detail_count INT, bill_status VARCHAR(32), remark VARCHAR(255),
|
||||
create_mode VARCHAR(32), dis_optid VARCHAR(64), dis_time TIMESTAMP, confirm_optid VARCHAR(64),
|
||||
confirm_time TIMESTAMP, sysdeptid VARCHAR(64), syscompanyid VARCHAR(64), is_upload VARCHAR(16),
|
||||
upload_optid VARCHAR(64), upload_time VARCHAR(64), create_time TIMESTAMP, update_time TIMESTAMP,
|
||||
creator VARCHAR(64), updater VARCHAR(64), deleted BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
|
||||
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), 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,
|
||||
creator VARCHAR(64), updater VARCHAR(64), deleted BOOLEAN DEFAULT FALSE
|
||||
);
|
||||
Reference in New Issue
Block a user