优化
This commit is contained in:
@@ -2,7 +2,14 @@ package com.baiyue.warehouse.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baiyue.warehouse.entity.PalletEntity;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 托盘主数据 Mapper。 */
|
||||
public interface PalletMapper extends BaseMapper<PalletEntity> {
|
||||
/** 编码序列必须包含逻辑删除记录,避免历史编码被复用。 */
|
||||
@Select("SELECT pallet_code FROM wh_pallet WHERE tenant_id = #{tenantId} AND pallet_code LIKE 'TP-%'")
|
||||
List<String> selectAllCodes(@Param("tenantId") long tenantId);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@ public class PalletRepository {
|
||||
public PageResult<PalletEntity> page(Long tenantId, PalletRequests.PageQuery query) {
|
||||
LambdaQueryWrapper<PalletEntity> wrapper = new LambdaQueryWrapper<PalletEntity>()
|
||||
.eq(tenantId != null, PalletEntity::getTenantId, tenantId)
|
||||
.eq(PalletEntity::getDeleted, false)
|
||||
.eq(query.warehouseId() != null, PalletEntity::getWarehouseId, query.warehouseId())
|
||||
.like(StringUtils.hasText(query.palletCode()), PalletEntity::getPalletCode, query.palletCode())
|
||||
.eq(StringUtils.hasText(query.palletType()), PalletEntity::getPalletType, query.palletType())
|
||||
@@ -41,7 +40,6 @@ public class PalletRepository {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<PalletEntity>()
|
||||
.eq(PalletEntity::getTenantId, tenantId)
|
||||
.eq(PalletEntity::getWarehouseId, warehouseId)
|
||||
.eq(PalletEntity::getDeleted, false)
|
||||
.eq(PalletEntity::getId, id));
|
||||
}
|
||||
|
||||
@@ -50,7 +48,6 @@ public class PalletRepository {
|
||||
.eq(PalletEntity::getTenantId, tenantId)
|
||||
.eq(PalletEntity::getWarehouseId, warehouseId)
|
||||
.eq(PalletEntity::getPalletCode, code)
|
||||
.eq(PalletEntity::getDeleted, false)
|
||||
.ne(excludedId != null, PalletEntity::getId, excludedId)) > 0;
|
||||
}
|
||||
|
||||
@@ -59,7 +56,6 @@ public class PalletRepository {
|
||||
return mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
|
||||
.eq(PalletEntity::getTenantId, tenantId)
|
||||
.eq(PalletEntity::getWarehouseId, warehouseId)
|
||||
.eq(PalletEntity::getDeleted, false)
|
||||
.eq(PalletEntity::getStatus, "AVAILABLE")
|
||||
.eq(StringUtils.hasText(palletType), PalletEntity::getPalletType, palletType)
|
||||
.eq(capacityCbm != null, PalletEntity::getCapacityCbm, capacityCbm)
|
||||
@@ -67,15 +63,13 @@ public class PalletRepository {
|
||||
.last("LIMIT " + limit));
|
||||
}
|
||||
|
||||
public String nextCode(long tenantId, long warehouseId) {
|
||||
int maxSequence = mapper.selectList(new LambdaQueryWrapper<PalletEntity>()
|
||||
.select(PalletEntity::getPalletCode)
|
||||
.eq(PalletEntity::getTenantId, tenantId)
|
||||
.eq(PalletEntity::getWarehouseId, warehouseId)
|
||||
.likeRight(PalletEntity::getPalletCode, "TP-")
|
||||
.eq(PalletEntity::getDeleted, false))
|
||||
/**
|
||||
* 生成平台作用域内下一个编码。编码序列跨仓库共用,且包含逻辑删除记录,确保编码永不复用。
|
||||
*/
|
||||
public String nextCode(long tenantId) {
|
||||
List<String> existing = mapper.selectAllCodes(tenantId);
|
||||
int maxSequence = (existing == null ? List.<String>of() : existing)
|
||||
.stream()
|
||||
.map(PalletEntity::getPalletCode)
|
||||
.map(PALLET_CODE::matcher)
|
||||
.filter(Matcher::matches)
|
||||
.mapToInt(matcher -> Integer.parseInt(matcher.group(1)))
|
||||
|
||||
@@ -16,6 +16,8 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
/** 托盘主数据应用服务。 */
|
||||
@Service
|
||||
public class PalletApplicationService {
|
||||
private static final int CODE_INSERT_ATTEMPTS = 5;
|
||||
|
||||
@Resource
|
||||
private PalletRepository repository;
|
||||
@Resource
|
||||
@@ -34,19 +36,23 @@ public class PalletApplicationService {
|
||||
@Transactional
|
||||
@BusinessAudit(event = "PALLET_CREATE", resourceType = "PALLET", description = "新增托盘", captureBefore = false)
|
||||
public Long create(long tenantId, PalletRequests.SaveRequest request) {
|
||||
requireWarehouse(tenantId, request.warehouseId());
|
||||
requireWarehouse(tenantId, request.warehouseId(), true);
|
||||
PalletEntity entity = new PalletEntity();
|
||||
entity.setTenantId(tenantId);
|
||||
entity.setWarehouseId(request.warehouseId());
|
||||
entity.setPalletCode(repository.nextCode(tenantId, request.warehouseId()));
|
||||
copy(entity, request);
|
||||
entity.setStatus("AVAILABLE");
|
||||
try {
|
||||
repository.insert(entity);
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘编码已存在");
|
||||
String code = repository.nextCode(tenantId);
|
||||
for (int attempt = 0; attempt < CODE_INSERT_ATTEMPTS; attempt++) {
|
||||
entity.setPalletCode(code);
|
||||
try {
|
||||
repository.insert(entity);
|
||||
return entity.getId();
|
||||
} catch (DataIntegrityViolationException exception) {
|
||||
code = incrementCode(code);
|
||||
}
|
||||
}
|
||||
return entity.getId();
|
||||
throw new BizException(ResultCode.CONFLICT, "托盘编码生成冲突,请重试");
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -55,6 +61,7 @@ public class PalletApplicationService {
|
||||
if (warehouseId != request.warehouseId()) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "请求仓库与托盘所属仓库不一致");
|
||||
}
|
||||
requireWarehouse(tenantId, warehouseId, false);
|
||||
PalletEntity entity = owned(tenantId, warehouseId, id);
|
||||
copy(entity, request);
|
||||
try {
|
||||
@@ -83,10 +90,17 @@ public class PalletApplicationService {
|
||||
return entity;
|
||||
}
|
||||
|
||||
private void requireWarehouse(long tenantId, long warehouseId) {
|
||||
if (warehouseRepository.findOwned(tenantId, warehouseId) == null) {
|
||||
private void requireWarehouse(long tenantId, long warehouseId, boolean requireActive) {
|
||||
var warehouse = warehouseRepository.findOwned(tenantId, warehouseId);
|
||||
if (warehouse == null) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "仓库不存在或不属于目标租户");
|
||||
}
|
||||
if (!"DOMESTIC".equalsIgnoreCase(warehouse.getWarehouseType())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "托盘仅支持国内仓");
|
||||
}
|
||||
if (requireActive && !"ACTIVE".equalsIgnoreCase(warehouse.getStatus())) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "新增托盘仅支持启用中的国内仓");
|
||||
}
|
||||
}
|
||||
|
||||
private static void copy(PalletEntity entity, PalletRequests.SaveRequest request) {
|
||||
@@ -97,6 +111,11 @@ public class PalletApplicationService {
|
||||
entity.setRemark(request.remark());
|
||||
}
|
||||
|
||||
private static String incrementCode(String code) {
|
||||
int sequence = Integer.parseInt(code.substring("TP-".length())) + 1;
|
||||
return "TP-%03d".formatted(sequence);
|
||||
}
|
||||
|
||||
private static PalletRequests.View view(PalletEntity entity) {
|
||||
return new PalletRequests.View(entity.getId(), entity.getTenantId(), entity.getWarehouseId(),
|
||||
entity.getPalletCode(), entity.getPalletType(), entity.getCapacityCbm(), entity.getWeightKg(),
|
||||
|
||||
@@ -9,6 +9,7 @@ SET @sql = IF((SELECT COUNT(*) FROM information_schema.columns
|
||||
'ALTER TABLE wh_pallet ADD COLUMN weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''托盘重量kg'' AFTER capacity_cbm',
|
||||
'SELECT 1');
|
||||
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
|
||||
SET @sql = IF((SELECT COUNT(*) FROM information_schema.columns
|
||||
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet' AND column_name = 'color') = 0,
|
||||
'ALTER TABLE wh_pallet ADD COLUMN color VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''托盘颜色'' AFTER weight_kg',
|
||||
@@ -53,6 +54,35 @@ UPDATE wh_location SET tenant_id = 0 WHERE tenant_id <> 0;
|
||||
UPDATE wh_location_reservation SET tenant_id = 0 WHERE tenant_id <> 0;
|
||||
UPDATE wh_location_occupancy SET tenant_id = 0 WHERE tenant_id <> 0;
|
||||
|
||||
-- 托盘编码在平台公共作用域全局唯一,避免不同仓库并发生成重复编码。
|
||||
DROP PROCEDURE IF EXISTS assert_pallet_code_globally_unique;
|
||||
DELIMITER $$
|
||||
CREATE PROCEDURE assert_pallet_code_globally_unique()
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM wh_pallet
|
||||
GROUP BY tenant_id, pallet_code
|
||||
HAVING COUNT(*) > 1
|
||||
) THEN
|
||||
SIGNAL SQLSTATE '45000'
|
||||
SET MESSAGE_TEXT = '托盘编码迁移失败:不同仓库存在重复编码,请先清理';
|
||||
END IF;
|
||||
END$$
|
||||
DELIMITER ;
|
||||
CALL assert_pallet_code_globally_unique();
|
||||
DROP PROCEDURE IF EXISTS assert_pallet_code_globally_unique;
|
||||
|
||||
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet'
|
||||
AND index_name = 'uk_pallet_tenant_warehouse_code') > 0,
|
||||
'ALTER TABLE wh_pallet DROP INDEX uk_pallet_tenant_warehouse_code', 'SELECT 1');
|
||||
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = DATABASE() AND table_name = 'wh_pallet'
|
||||
AND index_name = 'uk_pallet_tenant_code') = 0,
|
||||
'ALTER TABLE wh_pallet ADD UNIQUE KEY uk_pallet_tenant_code (tenant_id, pallet_code)', 'SELECT 1');
|
||||
PREPARE statement FROM @sql; EXECUTE statement; DEALLOCATE PREPARE statement;
|
||||
|
||||
SET @sql = IF((SELECT COUNT(*) FROM information_schema.statistics
|
||||
WHERE table_schema = @schema_name
|
||||
AND table_name = 'wh_location_reservation'
|
||||
|
||||
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS wh_pallet (
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除标记',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_pallet_tenant_warehouse_code (tenant_id, warehouse_id, pallet_code),
|
||||
UNIQUE KEY uk_pallet_tenant_code (tenant_id, pallet_code),
|
||||
KEY idx_pallet_available (tenant_id, warehouse_id, status, deleted),
|
||||
KEY idx_pallet_type (tenant_id, warehouse_id, pallet_type, status, deleted),
|
||||
CONSTRAINT fk_pallet_warehouse FOREIGN KEY (warehouse_id) REFERENCES wh_warehouse (id),
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
-- 二期托盘初始化数据。执行顺序:warehouse-seed.sql、phase2-pallet-location-schema.sql、增量迁移、本文件。
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_warehouse;
|
||||
|
||||
-- 编码由平台维护接口自动生成;种子固定为 TP-001、TP-002。
|
||||
INSERT INTO wh_pallet(id,tenant_id,warehouse_id,pallet_code,pallet_type,capacity_cbm,weight_kg,color,status,remark,created_by,updated_by)
|
||||
VALUES
|
||||
(190000000000000101,0,190000000000000001,'TP-001','木托盘',1.200000,8.000,'黄色','AVAILABLE',NULL,0,0),
|
||||
(190000000000000102,0,190000000000000001,'TP-002','胶托盘',2.400000,12.000,'黑色','AVAILABLE',NULL,0,0)
|
||||
ON DUPLICATE KEY UPDATE
|
||||
tenant_id=0,
|
||||
warehouse_id=VALUES(warehouse_id),
|
||||
pallet_type=VALUES(pallet_type),
|
||||
capacity_cbm=VALUES(capacity_cbm),
|
||||
weight_kg=VALUES(weight_kg),
|
||||
color=VALUES(color),
|
||||
remark=VALUES(remark);
|
||||
@@ -11,13 +11,6 @@ INSERT INTO wh_service_capability(id,tenant_id,warehouse_id,capability_code,capa
|
||||
VALUES(190000000000000021,0,190000000000000001,'RECEIVING','头程收货',1,'16:00:00','工作日16点前预约可当日收货')
|
||||
ON DUPLICATE KEY UPDATE tenant_id=0,enabled=1,cutoff_time=VALUES(cutoff_time);
|
||||
|
||||
-- 二期托盘初始化数据。编码由平台维护接口自动生成,种子固定为 TP-001、TP-002。
|
||||
INSERT INTO wh_pallet(id,tenant_id,warehouse_id,pallet_code,pallet_type,capacity_cbm,weight_kg,color,status,remark,created_by,updated_by)
|
||||
VALUES
|
||||
(190000000000000101,0,190000000000000001,'TP-001','木托盘',1.200000,8.000,'黄色','AVAILABLE','无备注',0,0),
|
||||
(190000000000000102,0,190000000000000001,'TP-002','胶托盘',2.400000,12.000,'黑色','AVAILABLE','无备注',0,0)
|
||||
ON DUPLICATE KEY UPDATE tenant_id=0,warehouse_id=VALUES(warehouse_id),pallet_type=VALUES(pallet_type),capacity_cbm=VALUES(capacity_cbm),weight_kg=VALUES(weight_kg),color=VALUES(color),remark=VALUES(remark);
|
||||
|
||||
-- 一期联调快递公司主数据,供平台维护和订单履约回显验证。
|
||||
INSERT INTO wh_express_company(id,tenant_id,company_code,company_name,service_phone,website,tracking_url_template,status,remark)
|
||||
VALUES(192000000000000001,0,'SF','顺丰速运','95338','https://www.sf-express.com','https://www.sf-express.com/chn/sc/dynamic_function/waybill/#search/bill-number/{trackingNo}','ACTIVE','平台公共快递公司样本')
|
||||
|
||||
+3
@@ -43,6 +43,9 @@ class WarehouseSchemaContractTest {
|
||||
@Test
|
||||
void shouldHandleCase04() throws IOException {
|
||||
String sql = read("phase2-pallet-location-schema.sql");
|
||||
assertColumns(sql, "wh_pallet", "tenant_id", "warehouse_id", "pallet_code", "pallet_type",
|
||||
"capacity_cbm", "weight_kg", "color", "status", "remark", "deleted", "version");
|
||||
assertThat(sql).contains("uk_pallet_tenant_code (tenant_id, pallet_code)");
|
||||
assertColumns(sql, "wh_location", "tenant_id", "warehouse_id", "zone_code", "zone_name",
|
||||
"location_code", "location_type", "max_weight_kg", "shared_overflow", "status", "remark", "deleted", "version");
|
||||
assertColumns(sql, "wh_location_reservation", "idempotency_key", "business_id", "pallet_id",
|
||||
|
||||
+17
@@ -4,6 +4,7 @@ import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.api.PageResult;
|
||||
import com.baiyue.warehouse.dto.PalletRequests;
|
||||
import com.baiyue.warehouse.entity.PalletEntity;
|
||||
import com.baiyue.warehouse.entity.WarehouseEntity;
|
||||
import com.baiyue.warehouse.repository.PalletRepository;
|
||||
import com.baiyue.warehouse.repository.WarehouseRepository;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -16,6 +17,8 @@ import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@SpringJUnitConfig(PalletApplicationService.class)
|
||||
class PalletApplicationServiceTest {
|
||||
@MockBean private PalletRepository repository;
|
||||
@@ -46,6 +49,20 @@ class PalletApplicationServiceTest {
|
||||
verify(repository).page(null, query);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsOverseasWarehouseForCreate() {
|
||||
WarehouseEntity warehouse = new WarehouseEntity();
|
||||
warehouse.setWarehouseType("OVERSEAS");
|
||||
warehouse.setStatus("ACTIVE");
|
||||
when(warehouseRepository.findOwned(0, 20)).thenReturn(warehouse);
|
||||
PalletRequests.SaveRequest request = new PalletRequests.SaveRequest(
|
||||
20L, "木托盘", new BigDecimal("1.2"), new BigDecimal("8"), "黄色", null);
|
||||
|
||||
assertThatThrownBy(() -> service.create(0, request))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("仅支持国内仓");
|
||||
}
|
||||
|
||||
private static PalletEntity pallet(String status) {
|
||||
PalletEntity entity = new PalletEntity();
|
||||
entity.setId(30L);
|
||||
|
||||
Reference in New Issue
Block a user