优化
This commit is contained in:
+6
-6
@@ -3,7 +3,7 @@ package com.baiyue.firstleg.controller;
|
||||
import com.baiyue.common.audit.BusinessAudit;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.security.AuthenticatedUser;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.Response;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.GroupResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.UpdateRequest;
|
||||
import com.baiyue.firstleg.service.CustomerDimensionConfigService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
@@ -30,8 +30,8 @@ public class PlatformCustomerDimensionConfigController {
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('firstleg:dimension-config:view')")
|
||||
@Operation(summary = "查询客户尺寸计算配置")
|
||||
public ApiResponse<Response> get(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId) {
|
||||
public ApiResponse<GroupResponse> get(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId) {
|
||||
return ApiResponse.success(service.get(FirstlegScopeGuard.platform(user, tenantId)));
|
||||
}
|
||||
|
||||
@@ -41,9 +41,9 @@ public class PlatformCustomerDimensionConfigController {
|
||||
resourceType = "DIMENSION_CONFIG", resourceId = "#tenantId",
|
||||
description = "平台编辑客户尺寸计算配置")
|
||||
@Operation(summary = "编辑客户尺寸计算配置")
|
||||
public ApiResponse<Response> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@Valid @RequestBody UpdateRequest request) {
|
||||
public ApiResponse<GroupResponse> update(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@Valid @RequestBody UpdateRequest request) {
|
||||
long scope = FirstlegScopeGuard.platform(user, tenantId);
|
||||
return ApiResponse.success(service.update(scope, user.userId(), request));
|
||||
}
|
||||
|
||||
+17
@@ -21,6 +21,7 @@ import jakarta.validation.Valid;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
@@ -167,6 +168,22 @@ public class PlatformInboundMeasurementController {
|
||||
tenantId, inboundId, resultId, user.userId(), user.username(), request));
|
||||
}
|
||||
|
||||
@BusinessAudit(event = "INBOUND_WEIGHT_IMAGE_DELETE", resourceType = "INBOUND_WEIGHT_RESULT",
|
||||
resourceId = "#resultId", description = "删除称重图片及重量结果")
|
||||
@Operation(summary = "删除称重图片", description = "删除重量结果和文件;已确认图片删除后重新汇总实际总毛重。")
|
||||
@DeleteMapping("/weight-results/{resultId}")
|
||||
@PreAuthorize("hasAuthority('platform:firstleg:inbound:weight:edit')")
|
||||
public ApiResponse<Void> deleteWeightResult(
|
||||
@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId,
|
||||
@RequestParam int version,
|
||||
@PathVariable long inboundId,
|
||||
@PathVariable long resultId) {
|
||||
requirePlatformTenant(user, tenantId);
|
||||
service.deleteWeightResult(tenantId, inboundId, resultId, version, user.userId());
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private static void requirePlatformTenant(AuthenticatedUser user, long tenantId) {
|
||||
FirstlegScopeGuard.platform(user, tenantId);
|
||||
if (user.userId() == null || user.userId() <= 0) {
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
package com.baiyue.firstleg.domain;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 平台固定维护的四种客户箱规,不允许扩展或改名。 */
|
||||
public enum CustomerDimensionBoxType {
|
||||
SMALL("小箱", "30.000", "20.000", "20.000", "标准小箱"),
|
||||
MEDIUM("中箱", "40.000", "30.000", "30.000", "标准中箱"),
|
||||
LARGE("大箱", "50.000", "40.000", "40.000", "标准大箱"),
|
||||
EXTRA_LARGE("特大箱", "60.000", "50.000", "50.000", "特大箱");
|
||||
|
||||
private final String displayName;
|
||||
private final BigDecimal defaultLengthCm;
|
||||
private final BigDecimal defaultWidthCm;
|
||||
private final BigDecimal defaultHeightCm;
|
||||
private final String defaultRemark;
|
||||
|
||||
CustomerDimensionBoxType(String displayName, String defaultLengthCm, String defaultWidthCm,
|
||||
String defaultHeightCm, String defaultRemark) {
|
||||
this.displayName = displayName;
|
||||
this.defaultLengthCm = new BigDecimal(defaultLengthCm);
|
||||
this.defaultWidthCm = new BigDecimal(defaultWidthCm);
|
||||
this.defaultHeightCm = new BigDecimal(defaultHeightCm);
|
||||
this.defaultRemark = defaultRemark;
|
||||
}
|
||||
|
||||
public String displayName() {
|
||||
return displayName;
|
||||
}
|
||||
|
||||
public BigDecimal defaultLengthCm() {
|
||||
return defaultLengthCm;
|
||||
}
|
||||
|
||||
public BigDecimal defaultWidthCm() {
|
||||
return defaultWidthCm;
|
||||
}
|
||||
|
||||
public BigDecimal defaultHeightCm() {
|
||||
return defaultHeightCm;
|
||||
}
|
||||
|
||||
public String defaultRemark() {
|
||||
return defaultRemark;
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Digits;
|
||||
@@ -10,21 +12,40 @@ import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
public final class CustomerDimensionConfigDtos {
|
||||
private CustomerDimensionConfigDtos() { }
|
||||
|
||||
public record UpdateRequest(
|
||||
@NotNull @Size(min = 4, max = 4) List<@Valid RuleUpdateRequest> rules
|
||||
) { }
|
||||
|
||||
public record RuleUpdateRequest(
|
||||
@NotNull CustomerDimensionBoxType boxType,
|
||||
@NotNull @DecimalMin("0.001") @Digits(integer = 6, fraction = 3) BigDecimal innerLengthCm,
|
||||
@NotNull @DecimalMin("0.001") @Digits(integer = 6, fraction = 3) BigDecimal innerWidthCm,
|
||||
@NotNull @DecimalMin("0.001") @Digits(integer = 6, fraction = 3) BigDecimal innerHeightCm,
|
||||
@NotNull @DecimalMin("0.001") @DecimalMax("100.000") @Digits(integer = 3, fraction = 3)
|
||||
BigDecimal edgeAdditionCm,
|
||||
@Size(max = 500) String remark,
|
||||
@NotNull @Min(0) Integer version
|
||||
) { }
|
||||
|
||||
public record Response(
|
||||
public record GroupResponse(Long tenantId, List<RuleResponse> rules) { }
|
||||
|
||||
public record RuleResponse(
|
||||
Long id,
|
||||
Long tenantId,
|
||||
CustomerDimensionBoxType boxType,
|
||||
String boxTypeName,
|
||||
BigDecimal innerLengthCm,
|
||||
BigDecimal innerWidthCm,
|
||||
BigDecimal innerHeightCm,
|
||||
BigDecimal edgeAdditionCm,
|
||||
BigDecimal outerLengthCm,
|
||||
BigDecimal outerWidthCm,
|
||||
BigDecimal outerHeightCm,
|
||||
BigDecimal volumeCbm,
|
||||
String remark,
|
||||
Integer version,
|
||||
LocalDateTime createdAt,
|
||||
|
||||
+6
-1
@@ -4,12 +4,13 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import com.baiyue.common.core.model.BaseEntity;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 客户填写真实尺寸后使用的每侧加宽配置。 */
|
||||
/** 客户固定箱规的内径和每侧加宽配置。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_customer_dimension_config")
|
||||
@@ -17,6 +18,10 @@ public class CustomerDimensionConfigEntity extends BaseEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private CustomerDimensionBoxType boxType;
|
||||
private BigDecimal innerLengthCm;
|
||||
private BigDecimal innerWidthCm;
|
||||
private BigDecimal innerHeightCm;
|
||||
private BigDecimal edgeAdditionCm;
|
||||
private String remark;
|
||||
private String initializationKey;
|
||||
|
||||
+43
-14
@@ -5,33 +5,62 @@ import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
public interface CustomerDimensionConfigMapper extends BaseMapper<CustomerDimensionConfigEntity> {
|
||||
@Insert("""
|
||||
<script>
|
||||
INSERT IGNORE INTO fl_customer_dimension_config
|
||||
(id, tenant_id, edge_addition_cm, remark, initialization_key, created_by, created_at,
|
||||
(id, tenant_id, box_type, inner_length_cm, inner_width_cm, inner_height_cm,
|
||||
edge_addition_cm, remark, initialization_key, created_by, created_at,
|
||||
updated_by, updated_at, deleted, version)
|
||||
VALUES
|
||||
(#{id}, #{tenantId}, #{edgeAdditionCm}, #{remark}, #{initializationKey}, #{operatorId}, CURRENT_TIMESTAMP(3),
|
||||
#{operatorId}, CURRENT_TIMESTAMP(3), 0, 0)
|
||||
<foreach collection="rows" item="row" separator=",">
|
||||
(#{row.id}, #{row.tenantId}, #{row.boxType}, #{row.innerLengthCm}, #{row.innerWidthCm},
|
||||
#{row.innerHeightCm}, #{row.edgeAdditionCm}, #{row.remark}, #{row.initializationKey},
|
||||
#{operatorId}, CURRENT_TIMESTAMP(3), #{operatorId}, CURRENT_TIMESTAMP(3), 0, 0)
|
||||
</foreach>
|
||||
</script>
|
||||
""")
|
||||
int insertDefault(@Param("id") long id, @Param("tenantId") long tenantId,
|
||||
@Param("edgeAdditionCm") BigDecimal edgeAdditionCm,
|
||||
@Param("remark") String remark, @Param("initializationKey") String initializationKey,
|
||||
@Param("operatorId") long operatorId);
|
||||
int insertDefaults(@Param("rows") List<CustomerDimensionConfigEntity> rows,
|
||||
@Param("operatorId") long operatorId);
|
||||
|
||||
@Select("""
|
||||
SELECT id, tenant_id, box_type, inner_length_cm, inner_width_cm, inner_height_cm,
|
||||
edge_addition_cm, remark, initialization_key, created_by, created_at,
|
||||
updated_by, updated_at, deleted, version
|
||||
FROM fl_customer_dimension_config
|
||||
WHERE tenant_id = #{tenantId}
|
||||
AND initialization_key = #{initializationKey}
|
||||
AND deleted = 0
|
||||
FOR UPDATE
|
||||
""")
|
||||
List<CustomerDimensionConfigEntity> selectDefaultsForCompensation(
|
||||
@Param("tenantId") long tenantId, @Param("initializationKey") String initializationKey);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM fl_customer_dimension_config
|
||||
WHERE id = #{id}
|
||||
AND tenant_id = #{tenantId}
|
||||
WHERE tenant_id = #{tenantId}
|
||||
AND initialization_key = #{initializationKey}
|
||||
AND edge_addition_cm = #{edgeAdditionCm}
|
||||
AND version = 0
|
||||
AND deleted = 0
|
||||
AND box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')
|
||||
AND inner_length_cm = CASE box_type
|
||||
WHEN 'SMALL' THEN 30.000 WHEN 'MEDIUM' THEN 40.000
|
||||
WHEN 'LARGE' THEN 50.000 WHEN 'EXTRA_LARGE' THEN 60.000 END
|
||||
AND inner_width_cm = CASE box_type
|
||||
WHEN 'SMALL' THEN 20.000 WHEN 'MEDIUM' THEN 30.000
|
||||
WHEN 'LARGE' THEN 40.000 WHEN 'EXTRA_LARGE' THEN 50.000 END
|
||||
AND inner_height_cm = CASE box_type
|
||||
WHEN 'SMALL' THEN 20.000 WHEN 'MEDIUM' THEN 30.000
|
||||
WHEN 'LARGE' THEN 40.000 WHEN 'EXTRA_LARGE' THEN 50.000 END
|
||||
AND edge_addition_cm = 2.000
|
||||
AND remark = CASE box_type
|
||||
WHEN 'SMALL' THEN '标准小箱' WHEN 'MEDIUM' THEN '标准中箱'
|
||||
WHEN 'LARGE' THEN '标准大箱' WHEN 'EXTRA_LARGE' THEN '特大箱' END
|
||||
""")
|
||||
int deleteUnchangedDefault(@Param("tenantId") long tenantId, @Param("id") long id,
|
||||
@Param("initializationKey") String initializationKey,
|
||||
@Param("edgeAdditionCm") BigDecimal edgeAdditionCm);
|
||||
int deleteUnchangedDefaults(@Param("tenantId") long tenantId,
|
||||
@Param("initializationKey") String initializationKey);
|
||||
}
|
||||
|
||||
+12
-9
@@ -6,29 +6,32 @@ import com.baiyue.firstleg.mapper.CustomerDimensionConfigMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public class CustomerDimensionConfigRepository {
|
||||
@Resource
|
||||
private CustomerDimensionConfigMapper mapper;
|
||||
|
||||
public CustomerDimensionConfigEntity find(long tenantId) {
|
||||
return mapper.selectOne(new LambdaQueryWrapper<CustomerDimensionConfigEntity>()
|
||||
public List<CustomerDimensionConfigEntity> findAll(long tenantId) {
|
||||
return mapper.selectList(new LambdaQueryWrapper<CustomerDimensionConfigEntity>()
|
||||
.eq(CustomerDimensionConfigEntity::getTenantId, tenantId));
|
||||
}
|
||||
|
||||
public boolean insertDefault(long id, long tenantId, BigDecimal edgeAdditionCm,
|
||||
String remark, String initializationKey, long operatorId) {
|
||||
return mapper.insertDefault(id, tenantId, edgeAdditionCm, remark, initializationKey, operatorId) == 1;
|
||||
public int insertDefaults(List<CustomerDimensionConfigEntity> rows, long operatorId) {
|
||||
return mapper.insertDefaults(rows, operatorId);
|
||||
}
|
||||
|
||||
public int update(CustomerDimensionConfigEntity value) {
|
||||
return mapper.updateById(value);
|
||||
}
|
||||
|
||||
public int deleteUnchangedDefault(long tenantId, long id, String initializationKey,
|
||||
BigDecimal edgeAdditionCm) {
|
||||
return mapper.deleteUnchangedDefault(tenantId, id, initializationKey, edgeAdditionCm);
|
||||
public List<CustomerDimensionConfigEntity> findDefaultsForCompensation(long tenantId,
|
||||
String initializationKey) {
|
||||
return mapper.selectDefaultsForCompensation(tenantId, initializationKey);
|
||||
}
|
||||
|
||||
public int deleteUnchangedDefaults(long tenantId, String initializationKey) {
|
||||
return mapper.deleteUnchangedDefaults(tenantId, initializationKey);
|
||||
}
|
||||
}
|
||||
|
||||
+8
@@ -157,6 +157,14 @@ public class InboundMeasurementRepository {
|
||||
.eq(InboundWeightResultEntity::getId, weight.getId())) == 1;
|
||||
}
|
||||
|
||||
public boolean deleteWeight(long tenantId, long inboundId, long resultId, int expectedVersion) {
|
||||
return weights.delete(new LambdaQueryWrapper<InboundWeightResultEntity>()
|
||||
.eq(InboundWeightResultEntity::getTenantId, tenantId)
|
||||
.eq(InboundWeightResultEntity::getInboundId, inboundId)
|
||||
.eq(InboundWeightResultEntity::getId, resultId)
|
||||
.eq(InboundWeightResultEntity::getVersion, expectedVersion)) == 1;
|
||||
}
|
||||
|
||||
|
||||
public void insertRevision(InboundWeightRevisionEntity revision) {
|
||||
revisions.insert(revision);
|
||||
|
||||
+184
-41
@@ -3,8 +3,11 @@ package com.baiyue.firstleg.service;
|
||||
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.GroupResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.Response;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.RuleResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.RuleUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.UpdateRequest;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
@@ -13,75 +16,215 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/** 管理每个客户唯一的尺寸加边配置,不参与当前订单或计费计算。 */
|
||||
/** 管理每个客户固定四种箱规的尺寸配置,不参与当前订单或计费计算。 */
|
||||
@Service
|
||||
public class CustomerDimensionConfigService {
|
||||
public static final BigDecimal DEFAULT_EDGE_ADDITION_CM = new BigDecimal("2.000");
|
||||
private static final String DEFAULT_REMARK = "默认每侧加宽 2cm";
|
||||
private static final BigDecimal TWO = new BigDecimal("2");
|
||||
private static final BigDecimal CUBIC_CENTIMETERS_PER_CBM = new BigDecimal("1000000");
|
||||
|
||||
@Resource
|
||||
private CustomerDimensionConfigRepository repository;
|
||||
|
||||
public Response get(long tenantId) {
|
||||
CustomerDimensionConfigEntity value = repository.find(tenantId);
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "客户尺寸计算配置不存在");
|
||||
}
|
||||
return response(value);
|
||||
public GroupResponse get(long tenantId) {
|
||||
return response(tenantId, requireCompleteRules(repository.findAll(tenantId)));
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Response update(long tenantId, long operatorId, UpdateRequest request) {
|
||||
CustomerDimensionConfigEntity value = repository.find(tenantId);
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "客户尺寸计算配置不存在");
|
||||
public GroupResponse update(long tenantId, long operatorId, UpdateRequest request) {
|
||||
Map<CustomerDimensionBoxType, RuleUpdateRequest> requested = requireFixedRuleSet(request);
|
||||
List<CustomerDimensionConfigEntity> existing = requireCompleteRules(repository.findAll(tenantId));
|
||||
Map<CustomerDimensionBoxType, CustomerDimensionConfigEntity> persisted = index(existing);
|
||||
|
||||
for (CustomerDimensionBoxType type : CustomerDimensionBoxType.values()) {
|
||||
CustomerDimensionConfigEntity value = persisted.get(type);
|
||||
if (!value.getVersion().equals(requested.get(type).version())) {
|
||||
throw staleConfiguration();
|
||||
}
|
||||
}
|
||||
if (!value.getVersion().equals(request.version())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "尺寸计算配置已被修改,请刷新后重试");
|
||||
|
||||
for (CustomerDimensionBoxType type : CustomerDimensionBoxType.values()) {
|
||||
CustomerDimensionConfigEntity value = persisted.get(type);
|
||||
RuleUpdateRequest update = requested.get(type);
|
||||
value.setInnerLengthCm(update.innerLengthCm());
|
||||
value.setInnerWidthCm(update.innerWidthCm());
|
||||
value.setInnerHeightCm(update.innerHeightCm());
|
||||
value.setEdgeAdditionCm(update.edgeAdditionCm());
|
||||
value.setRemark(normalizeRemark(update.remark()));
|
||||
value.setUpdatedBy(operatorId);
|
||||
if (repository.update(value) != 1) {
|
||||
throw staleConfiguration();
|
||||
}
|
||||
}
|
||||
value.setEdgeAdditionCm(request.edgeAdditionCm());
|
||||
value.setRemark(normalizeRemark(request.remark()));
|
||||
value.setUpdatedBy(operatorId);
|
||||
if (repository.update(value) != 1) {
|
||||
throw new BizException(ResultCode.CONFLICT, "尺寸计算配置已被修改,请刷新后重试");
|
||||
}
|
||||
return response(value);
|
||||
return response(tenantId, existing);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public InitializeResponse initializeDefault(long tenantId, long operatorId, String initializationKey) {
|
||||
requireTenant(tenantId);
|
||||
String normalizedKey = requireInitializationKey(initializationKey);
|
||||
CustomerDimensionConfigEntity existing = repository.find(tenantId);
|
||||
if (existing != null) {
|
||||
return new InitializeResponse(existing.getId(),
|
||||
normalizedKey.equals(existing.getInitializationKey()));
|
||||
List<CustomerDimensionConfigEntity> existing = repository.findAll(tenantId);
|
||||
if (!existing.isEmpty()) {
|
||||
List<CustomerDimensionConfigEntity> complete = requireCompleteRules(existing);
|
||||
return initializationResponse(complete, normalizedKey);
|
||||
}
|
||||
|
||||
long id = IdWorker.getId();
|
||||
boolean created = repository.insertDefault(id, tenantId, DEFAULT_EDGE_ADDITION_CM,
|
||||
DEFAULT_REMARK, normalizedKey, operatorId);
|
||||
CustomerDimensionConfigEntity persisted = repository.find(tenantId);
|
||||
if (persisted == null) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户尺寸计算配置初始化失败");
|
||||
}
|
||||
boolean ownedByKey = normalizedKey.equals(persisted.getInitializationKey());
|
||||
return new InitializeResponse(persisted.getId(), created || ownedByKey);
|
||||
repository.insertDefaults(defaultRows(tenantId, normalizedKey), operatorId);
|
||||
List<CustomerDimensionConfigEntity> persisted = requireCompleteRules(repository.findAll(tenantId));
|
||||
return initializationResponse(persisted, normalizedKey);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void compensateDefault(long tenantId, String initializationKey) {
|
||||
requireTenant(tenantId);
|
||||
String normalizedKey = requireInitializationKey(initializationKey);
|
||||
CustomerDimensionConfigEntity existing = repository.find(tenantId);
|
||||
if (existing == null || !normalizedKey.equals(existing.getInitializationKey())) return;
|
||||
repository.deleteUnchangedDefault(tenantId, existing.getId(), normalizedKey, DEFAULT_EDGE_ADDITION_CM);
|
||||
List<CustomerDimensionConfigEntity> locked =
|
||||
repository.findDefaultsForCompensation(tenantId, normalizedKey);
|
||||
if (!isUnchangedDefaultGroup(locked, normalizedKey)) return;
|
||||
if (repository.deleteUnchangedDefaults(tenantId, normalizedKey) != CustomerDimensionBoxType.values().length) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户箱规默认配置补偿删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
private static Response response(CustomerDimensionConfigEntity value) {
|
||||
return new Response(value.getId(), value.getTenantId(), value.getEdgeAdditionCm(), value.getRemark(),
|
||||
value.getVersion(), value.getCreatedAt(), value.getUpdatedAt());
|
||||
private static Map<CustomerDimensionBoxType, RuleUpdateRequest> requireFixedRuleSet(UpdateRequest request) {
|
||||
if (request == null || request.rules() == null || request.rules().size() != CustomerDimensionBoxType.values().length) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须完整提交固定四种箱规");
|
||||
}
|
||||
Map<CustomerDimensionBoxType, RuleUpdateRequest> values = new EnumMap<>(CustomerDimensionBoxType.class);
|
||||
for (RuleUpdateRequest rule : request.rules()) {
|
||||
if (rule == null || rule.boxType() == null || values.put(rule.boxType(), rule) != null) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "箱规类型重复或无效");
|
||||
}
|
||||
requirePositive(rule.innerLengthCm(), "箱规内长");
|
||||
requirePositive(rule.innerWidthCm(), "箱规内宽");
|
||||
requirePositive(rule.innerHeightCm(), "箱规内高");
|
||||
requirePositive(rule.edgeAdditionCm(), "箱规单侧加宽");
|
||||
if (rule.version() == null || rule.version() < 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "箱规版本号无效");
|
||||
}
|
||||
}
|
||||
if (values.size() != CustomerDimensionBoxType.values().length) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "必须完整提交固定四种箱规");
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
private static List<CustomerDimensionConfigEntity> requireCompleteRules(
|
||||
List<CustomerDimensionConfigEntity> values) {
|
||||
if (values == null || values.isEmpty()) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "客户箱规尺寸配置不存在");
|
||||
}
|
||||
Map<CustomerDimensionBoxType, CustomerDimensionConfigEntity> indexed = index(values);
|
||||
if (values.size() != CustomerDimensionBoxType.values().length
|
||||
|| indexed.size() != CustomerDimensionBoxType.values().length) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户箱规尺寸配置不完整");
|
||||
}
|
||||
List<CustomerDimensionConfigEntity> ordered = new ArrayList<>(CustomerDimensionBoxType.values().length);
|
||||
for (CustomerDimensionBoxType type : CustomerDimensionBoxType.values()) {
|
||||
CustomerDimensionConfigEntity value = indexed.get(type);
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户箱规尺寸配置不完整");
|
||||
}
|
||||
ordered.add(value);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
private static Map<CustomerDimensionBoxType, CustomerDimensionConfigEntity> index(
|
||||
List<CustomerDimensionConfigEntity> values) {
|
||||
Map<CustomerDimensionBoxType, CustomerDimensionConfigEntity> indexed =
|
||||
new EnumMap<>(CustomerDimensionBoxType.class);
|
||||
for (CustomerDimensionConfigEntity value : values) {
|
||||
if (value == null || value.getBoxType() == null || indexed.put(value.getBoxType(), value) != null) {
|
||||
throw new BizException(ResultCode.INTERNAL_ERROR, "客户箱规尺寸配置不完整");
|
||||
}
|
||||
}
|
||||
return indexed;
|
||||
}
|
||||
|
||||
private static List<CustomerDimensionConfigEntity> defaultRows(long tenantId, String initializationKey) {
|
||||
List<CustomerDimensionConfigEntity> rows = new ArrayList<>(CustomerDimensionBoxType.values().length);
|
||||
for (CustomerDimensionBoxType type : CustomerDimensionBoxType.values()) {
|
||||
CustomerDimensionConfigEntity value = new CustomerDimensionConfigEntity();
|
||||
value.setId(IdWorker.getId());
|
||||
value.setTenantId(tenantId);
|
||||
value.setBoxType(type);
|
||||
value.setInnerLengthCm(type.defaultLengthCm());
|
||||
value.setInnerWidthCm(type.defaultWidthCm());
|
||||
value.setInnerHeightCm(type.defaultHeightCm());
|
||||
value.setEdgeAdditionCm(DEFAULT_EDGE_ADDITION_CM);
|
||||
value.setRemark(type.defaultRemark());
|
||||
value.setInitializationKey(initializationKey);
|
||||
rows.add(value);
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
private static boolean isUnchangedDefaultGroup(List<CustomerDimensionConfigEntity> values,
|
||||
String initializationKey) {
|
||||
if (values == null || values.size() != CustomerDimensionBoxType.values().length) return false;
|
||||
Map<CustomerDimensionBoxType, CustomerDimensionConfigEntity> indexed;
|
||||
try {
|
||||
indexed = index(values);
|
||||
} catch (BizException ignored) {
|
||||
return false;
|
||||
}
|
||||
for (CustomerDimensionBoxType type : CustomerDimensionBoxType.values()) {
|
||||
CustomerDimensionConfigEntity value = indexed.get(type);
|
||||
if (value == null || !initializationKey.equals(value.getInitializationKey())
|
||||
|| value.getVersion() == null || value.getVersion() != 0
|
||||
|| value.getInnerLengthCm() == null || value.getInnerWidthCm() == null
|
||||
|| value.getInnerHeightCm() == null || value.getEdgeAdditionCm() == null
|
||||
|| value.getInnerLengthCm().compareTo(type.defaultLengthCm()) != 0
|
||||
|| value.getInnerWidthCm().compareTo(type.defaultWidthCm()) != 0
|
||||
|| value.getInnerHeightCm().compareTo(type.defaultHeightCm()) != 0
|
||||
|| value.getEdgeAdditionCm().compareTo(DEFAULT_EDGE_ADDITION_CM) != 0
|
||||
|| !type.defaultRemark().equals(value.getRemark())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static InitializeResponse initializationResponse(List<CustomerDimensionConfigEntity> values,
|
||||
String initializationKey) {
|
||||
boolean ownedByKey = values.stream()
|
||||
.allMatch(value -> initializationKey.equals(value.getInitializationKey()));
|
||||
return new InitializeResponse(values.get(0).getId(), ownedByKey);
|
||||
}
|
||||
|
||||
private static GroupResponse response(long tenantId, List<CustomerDimensionConfigEntity> values) {
|
||||
return new GroupResponse(tenantId, values.stream().map(CustomerDimensionConfigService::ruleResponse).toList());
|
||||
}
|
||||
|
||||
private static RuleResponse ruleResponse(CustomerDimensionConfigEntity value) {
|
||||
BigDecimal edgePair = value.getEdgeAdditionCm().multiply(TWO);
|
||||
BigDecimal outerLength = value.getInnerLengthCm().add(edgePair);
|
||||
BigDecimal outerWidth = value.getInnerWidthCm().add(edgePair);
|
||||
BigDecimal outerHeight = value.getInnerHeightCm().add(edgePair);
|
||||
BigDecimal volume = outerLength.multiply(outerWidth).multiply(outerHeight)
|
||||
.divide(CUBIC_CENTIMETERS_PER_CBM)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
return new RuleResponse(value.getId(), value.getBoxType(), value.getBoxType().displayName(),
|
||||
value.getInnerLengthCm(), value.getInnerWidthCm(), value.getInnerHeightCm(),
|
||||
value.getEdgeAdditionCm(), outerLength, outerWidth, outerHeight, volume,
|
||||
value.getRemark(), value.getVersion(), value.getCreatedAt(), value.getUpdatedAt());
|
||||
}
|
||||
|
||||
private static void requirePositive(BigDecimal value, String fieldName) {
|
||||
if (value == null || value.compareTo(new BigDecimal("0.001")) < 0) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, fieldName + "必须大于0");
|
||||
}
|
||||
}
|
||||
|
||||
private static BizException staleConfiguration() {
|
||||
return new BizException(ResultCode.CONFLICT, "箱规尺寸配置已被修改,请刷新后重试");
|
||||
}
|
||||
|
||||
private static String normalizeRemark(String value) {
|
||||
|
||||
@@ -3,7 +3,9 @@ package com.baiyue.firstleg.service;
|
||||
import jakarta.annotation.Resource;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||
import com.baiyue.firstleg.dto.MeasurementBatchCommand;
|
||||
@@ -17,6 +19,8 @@ import com.baiyue.firstleg.repository.InboundMeasurementRepository;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import feign.FeignException;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
@@ -32,6 +36,8 @@ public class InboundMeasurementService {
|
||||
private static final String WEIGHT_CONFIRMED = "CONFIRMED";
|
||||
private static final String WEIGHT_PENDING_CONFIRM = "PENDING_CONFIRM";
|
||||
private static final String WEIGHT_NEEDS_MANUAL = "NEEDS_MANUAL";
|
||||
private static final Set<String> WEIGHT_EDITABLE_STATUSES = Set.of(
|
||||
"WAIT_MEASURE", "MEASURING", "MEASURE_REJECTED");
|
||||
/** 测量完成后的默认普通托盘类型;实际拆托和库位占用由提交编排服务执行。 */
|
||||
private static final String RECOMMENDED_PALLET_TYPE = "STANDARD";
|
||||
private static final BigDecimal LB_TO_KG = new BigDecimal("0.45359237");
|
||||
@@ -40,6 +46,10 @@ public class InboundMeasurementService {
|
||||
private InboundMeasurementRepository repository;
|
||||
@Resource
|
||||
private MeasurementBatchService batchService;
|
||||
@Resource
|
||||
private MeasurementImageClient imageClient;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
public InboundMeasurementResponses.Detail detail(long tenantId, long inboundId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
@@ -231,13 +241,45 @@ public class InboundMeasurementService {
|
||||
return toWeight(weight);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void deleteWeightResult(long tenantId, long inboundId, long resultId,
|
||||
int version, long operatorId) {
|
||||
InboundOrderEntity inbound = requireInbound(tenantId, inboundId);
|
||||
if (!WEIGHT_EDITABLE_STATUSES.contains(inbound.getProcessStatus())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "当前入库状态不允许删除称重图片");
|
||||
}
|
||||
InboundWeightResultEntity weight = requireWeight(tenantId, inboundId, resultId);
|
||||
requireVersion(version, weight.getVersion());
|
||||
if (!repository.deleteWeight(tenantId, inboundId, resultId, version)) {
|
||||
throw concurrentModification();
|
||||
}
|
||||
if (WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus())) {
|
||||
recalculateConfirmedWeight(inbound, operatorId);
|
||||
}
|
||||
try {
|
||||
ApiResponse<Void> response = imageClient.delete(
|
||||
internalToken, operatorId, tenantId, inboundId, weight.getFileId());
|
||||
if (response == null || !response.successful()) {
|
||||
String message = response == null || !StringUtils.hasText(response.message())
|
||||
? "称重图片删除失败" : response.message();
|
||||
throw new BizException(response == null ? ResultCode.SERVICE_UNAVAILABLE
|
||||
: ResultCode.of(response.code()), message);
|
||||
}
|
||||
} catch (FeignException exception) {
|
||||
throw new BizException(ResultCode.SERVICE_UNAVAILABLE, "文件服务暂不可用,称重图片未删除");
|
||||
}
|
||||
}
|
||||
|
||||
private void recalculateConfirmedWeight(InboundOrderEntity inbound, long operatorId) {
|
||||
BigDecimal total = repository.listWeights(inbound.getTenantId(), inbound.getId()).stream()
|
||||
List<InboundWeightResultEntity> confirmedWeights = repository
|
||||
.listWeights(inbound.getTenantId(), inbound.getId()).stream()
|
||||
.filter(weight -> WEIGHT_CONFIRMED.equals(weight.getRecognitionStatus()))
|
||||
.toList();
|
||||
BigDecimal total = confirmedWeights.stream()
|
||||
.map(InboundWeightResultEntity::getFinalWeightKg)
|
||||
.reduce(BigDecimal.ZERO, BigDecimal::add)
|
||||
.setScale(3, RoundingMode.HALF_UP);
|
||||
inbound.setActualGrossWeightKg(total);
|
||||
inbound.setActualGrossWeightKg(confirmedWeights.isEmpty() ? null : total);
|
||||
if ("WAIT_MEASURE".equals(inbound.getProcessStatus())) {
|
||||
inbound.setProcessStatus("MEASURING");
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
SET NAMES utf8mb4;
|
||||
USE baiyue_firstleg;
|
||||
|
||||
-- 每个客户唯一的尺寸计算配置;当前只保存每侧加宽值,不接入订单或计费。
|
||||
-- 每个客户固定四条箱规尺寸配置;当前只维护配置事实,不接入订单或计费。
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE',
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
@@ -15,7 +19,12 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customer_dimension_config_tenant (tenant_id),
|
||||
UNIQUE KEY uk_customer_dimension_config_tenant_box (tenant_id, box_type),
|
||||
KEY idx_customer_dimension_config_initialization (tenant_id, initialization_key),
|
||||
CONSTRAINT chk_customer_dimension_box_type
|
||||
CHECK (box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')),
|
||||
CONSTRAINT chk_customer_dimension_inner_size
|
||||
CHECK (inner_length_cm >= 0.001 AND inner_width_cm >= 0.001 AND inner_height_cm >= 0.001),
|
||||
CONSTRAINT chk_customer_dimension_edge_addition
|
||||
CHECK (edge_addition_cm >= 0.001 AND edge_addition_cm <= 100.000)
|
||||
) ENGINE=InnoDB COMMENT='客户尺寸计算配置';
|
||||
) ENGINE=InnoDB COMMENT='客户固定四箱规尺寸配置';
|
||||
|
||||
@@ -48,3 +48,30 @@ CREATE TABLE IF NOT EXISTS fl_consumed_event (
|
||||
consumed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次成功消费时间',
|
||||
PRIMARY KEY (event_id)
|
||||
) ENGINE=InnoDB COMMENT='头程消息消费幂等表:记录已处理事件并按事件ID去重,防止RocketMQ重复投递造成重复建单;不是消息发布表';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE',
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
created_by BIGINT UNSIGNED NOT NULL COMMENT '创建人ID',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||
updated_by BIGINT UNSIGNED NOT NULL COMMENT '最后修改人ID',
|
||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customer_dimension_config_tenant_box (tenant_id, box_type),
|
||||
KEY idx_customer_dimension_config_initialization (tenant_id, initialization_key),
|
||||
CONSTRAINT chk_customer_dimension_box_type
|
||||
CHECK (box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')),
|
||||
CONSTRAINT chk_customer_dimension_inner_size
|
||||
CHECK (inner_length_cm >= 0.001 AND inner_width_cm >= 0.001 AND inner_height_cm >= 0.001),
|
||||
CONSTRAINT chk_customer_dimension_edge_addition
|
||||
CHECK (edge_addition_cm >= 0.001 AND edge_addition_cm <= 100.000)
|
||||
) ENGINE=InnoDB COMMENT='客户固定四箱规尺寸配置';
|
||||
|
||||
@@ -733,6 +733,10 @@ CREATE TABLE IF NOT EXISTS fl_inbound_idempotency (
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
box_type VARCHAR(24) NOT NULL COMMENT '固定箱规类型:SMALL/MEDIUM/LARGE/EXTRA_LARGE',
|
||||
inner_length_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内长(厘米)',
|
||||
inner_width_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内宽(厘米)',
|
||||
inner_height_cm DECIMAL(9,3) NOT NULL COMMENT '箱规内高(厘米)',
|
||||
edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000 COMMENT '每个尺寸方向的单侧加宽厘米数',
|
||||
remark VARCHAR(500) NULL COMMENT '备注',
|
||||
initialization_key VARCHAR(128) NOT NULL COMMENT '创建流程幂等键及补偿归属',
|
||||
@@ -743,7 +747,12 @@ CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
deleted TINYINT(1) NOT NULL DEFAULT 0 COMMENT '逻辑删除',
|
||||
version INT NOT NULL DEFAULT 0 COMMENT '乐观锁版本号',
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE KEY uk_customer_dimension_config_tenant (tenant_id),
|
||||
UNIQUE KEY uk_customer_dimension_config_tenant_box (tenant_id, box_type),
|
||||
KEY idx_customer_dimension_config_initialization (tenant_id, initialization_key),
|
||||
CONSTRAINT chk_customer_dimension_box_type
|
||||
CHECK (box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')),
|
||||
CONSTRAINT chk_customer_dimension_inner_size
|
||||
CHECK (inner_length_cm >= 0.001 AND inner_width_cm >= 0.001 AND inner_height_cm >= 0.001),
|
||||
CONSTRAINT chk_customer_dimension_edge_addition
|
||||
CHECK (edge_addition_cm >= 0.001 AND edge_addition_cm <= 100.000)
|
||||
) ENGINE=InnoDB COMMENT='客户尺寸计算配置';
|
||||
) ENGINE=InnoDB COMMENT='客户固定四箱规尺寸配置';
|
||||
|
||||
@@ -8,8 +8,8 @@ security:
|
||||
paths:
|
||||
/internal/firstleg/v1/customer-dimension-configs/defaults:
|
||||
post:
|
||||
summary: 幂等初始化客户尺寸计算默认配置
|
||||
description: 仅供 IAM 在客户创建或注册成功编排中调用;同一客户和 initializationKey 重试稳定返回 createdByThisInitialization=true。
|
||||
summary: 幂等初始化客户四箱规默认配置
|
||||
description: 原子创建 SMALL、MEDIUM、LARGE、EXTRA_LARGE 四行;同一客户和 initializationKey 重试稳定返回 createdByThisInitialization=true,configId 为 SMALL 行 ID。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
requestBody:
|
||||
@@ -20,8 +20,8 @@ paths:
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,data 返回 configId 和 createdByThisInitialization}
|
||||
delete:
|
||||
summary: 补偿删除本次新建且尚未修改的默认配置
|
||||
description: 仅供 IAM 创建客户事务补偿使用;同时匹配 initializationKey、默认2cm和version=0才物理删除。
|
||||
summary: 补偿删除本次新建且尚未修改的四箱规默认配置
|
||||
description: 仅当四行均匹配 initializationKey、默认尺寸、默认2cm和version=0时整组物理删除,不会部分删除。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
parameters:
|
||||
@@ -31,16 +31,16 @@ paths:
|
||||
'200': {description: 幂等补偿完成}
|
||||
/api/platform/v1/first-leg/customer-dimension-config:
|
||||
get:
|
||||
summary: 查询客户尺寸计算配置
|
||||
description: 需要 firstleg:dimension-config:view 权限;配置只描述真实尺寸的每侧加宽值,当前不参与订单或计费。
|
||||
summary: 查询客户固定四箱规尺寸配置
|
||||
description: 需要 firstleg:dimension-config:view 权限;按 SMALL、MEDIUM、LARGE、EXTRA_LARGE 固定顺序返回,当前不参与订单或计费。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
'200': {description: 成功时 data 为 CustomerDimensionConfigResponse}
|
||||
'200': {description: 成功时 data 为 CustomerDimensionConfigGroupResponse}
|
||||
'403': {description: 无查询权限}
|
||||
put:
|
||||
summary: 编辑客户尺寸计算配置
|
||||
description: 需要 firstleg:dimension-config:edit 权限;使用 version 执行乐观锁更新。
|
||||
summary: 整组编辑客户固定四箱规尺寸配置
|
||||
description: 需要 firstleg:dimension-config:edit 权限;请求必须恰好包含四种固定 boxType,四行逐行乐观锁并在同一事务原子保存。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
requestBody:
|
||||
@@ -49,7 +49,7 @@ paths:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CustomerDimensionConfigUpdateRequest'}
|
||||
responses:
|
||||
'200': {description: 成功时 data 为更新后的 CustomerDimensionConfigResponse}
|
||||
'200': {description: 成功时 data 为更新后的 CustomerDimensionConfigGroupResponse}
|
||||
'403': {description: 无编辑权限}
|
||||
/internal/firstleg/v1/inbounds/order-approved:
|
||||
post:
|
||||
@@ -288,6 +288,20 @@ paths:
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 无确认权限,或修改重量时无编辑权限}
|
||||
/api/platform/v1/first-leg/inbounds/{inboundId}/weight-results/{resultId}:
|
||||
delete:
|
||||
summary: 删除称重图片及重量结果
|
||||
description: >-
|
||||
需要 platform:firstleg:inbound:weight:edit 权限;仅允许在可编辑的测量状态删除。
|
||||
删除已确认图片后重新汇总剩余已确认图片的实际总毛重,删除最后一张时总毛重恢复为空。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: version, in: query, required: true, schema: {type: integer, format: int32, minimum: 0}}
|
||||
- {name: inboundId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: resultId, in: path, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,按响应体 code 判断}
|
||||
'403': {description: 无重量编辑权限}
|
||||
/api/platform/v1/inbounds/{inboundId}/measurements/approve:
|
||||
post:
|
||||
summary: 审核通过测量
|
||||
@@ -405,24 +419,56 @@ components:
|
||||
schemas:
|
||||
CustomerDimensionConfigUpdateRequest:
|
||||
type: object
|
||||
required: [edgeAdditionCm, version]
|
||||
required: [rules]
|
||||
properties:
|
||||
rules:
|
||||
type: array
|
||||
minItems: 4
|
||||
maxItems: 4
|
||||
description: 必须恰好包含 SMALL、MEDIUM、LARGE、EXTRA_LARGE 各一行
|
||||
items: {$ref: '#/components/schemas/CustomerDimensionConfigRuleUpdateRequest'}
|
||||
CustomerDimensionConfigRuleUpdateRequest:
|
||||
type: object
|
||||
required: [boxType, innerLengthCm, innerWidthCm, innerHeightCm, edgeAdditionCm, version]
|
||||
properties:
|
||||
boxType: {type: string, enum: [SMALL, MEDIUM, LARGE, EXTRA_LARGE]}
|
||||
innerLengthCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
innerWidthCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
innerHeightCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
edgeAdditionCm:
|
||||
type: number
|
||||
minimum: 0.001
|
||||
maximum: 100
|
||||
multipleOf: 0.001
|
||||
description: 真实长、宽、高三个方向分别使用的每侧加宽厘米数
|
||||
description: 箱规长、宽、高三个方向分别使用的每侧加宽厘米数
|
||||
example: 2.000
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
version: {type: integer, minimum: 0, description: 乐观锁版本号}
|
||||
CustomerDimensionConfigResponse:
|
||||
CustomerDimensionConfigGroupResponse:
|
||||
type: object
|
||||
required: [id, tenantId, edgeAdditionCm, version]
|
||||
required: [tenantId, rules]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
rules:
|
||||
type: array
|
||||
minItems: 4
|
||||
maxItems: 4
|
||||
items: {$ref: '#/components/schemas/CustomerDimensionConfigRuleResponse'}
|
||||
CustomerDimensionConfigRuleResponse:
|
||||
type: object
|
||||
required: [id, boxType, boxTypeName, innerLengthCm, innerWidthCm, innerHeightCm, edgeAdditionCm, outerLengthCm, outerWidthCm, outerHeightCm, volumeCbm, version]
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
boxType: {type: string, enum: [SMALL, MEDIUM, LARGE, EXTRA_LARGE]}
|
||||
boxTypeName: {type: string, enum: [小箱, 中箱, 大箱, 特大箱]}
|
||||
innerLengthCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
innerWidthCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
innerHeightCm: {type: number, minimum: 0.001, multipleOf: 0.001}
|
||||
edgeAdditionCm: {type: number, minimum: 0.001, maximum: 100, example: 2.000}
|
||||
outerLengthCm: {type: number, minimum: 0.001, description: innerLengthCm + edgeAdditionCm * 2}
|
||||
outerWidthCm: {type: number, minimum: 0.001, description: innerWidthCm + edgeAdditionCm * 2}
|
||||
outerHeightCm: {type: number, minimum: 0.001, description: innerHeightCm + edgeAdditionCm * 2}
|
||||
volumeCbm: {type: number, multipleOf: 0.001, description: 使用未舍入外径完整计算后四舍五入保留3位}
|
||||
remark: {type: string, nullable: true}
|
||||
version: {type: integer, minimum: 0}
|
||||
createdAt: {type: string, format: date-time, nullable: true}
|
||||
|
||||
+2
@@ -39,6 +39,8 @@ class PlatformInboundMeasurementControllerContractTest {
|
||||
+ " and (!#request.hasAdjustment() or hasAuthority('platform:firstleg:inbound:weight:edit'))",
|
||||
AuthenticatedUser.class, long.class, long.class, long.class,
|
||||
InboundMeasurementRequests.ConfirmWeightResultRequest.class);
|
||||
assertAuthority("deleteWeightResult", "hasAuthority('platform:firstleg:inbound:weight:edit')",
|
||||
AuthenticatedUser.class, long.class, int.class, long.class, long.class);
|
||||
}
|
||||
|
||||
private void assertAuthority(String methodName, String expected, Class<?>... parameterTypes)
|
||||
|
||||
+112
-35
@@ -1,65 +1,142 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.mapper.CustomerDimensionConfigMapper;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class CustomerDimensionConfigSqlContractTest {
|
||||
@Test
|
||||
void moduleMigrationDefinesUniqueCustomerConfigAndDefault() throws IOException {
|
||||
String sql = new ClassPathResource("db/customer-dimension-config.sql")
|
||||
.getContentAsString(StandardCharsets.UTF_8);
|
||||
void allFourDatabaseEntrypointsDefineTheSameFourRowStructure() throws Exception {
|
||||
List<String> sqlEntrypoints = List.of(
|
||||
resource("db/customer-dimension-config.sql"),
|
||||
resource("db/firstleg.sql"),
|
||||
resource("db/phase2-inbound-schema.sql"),
|
||||
Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8)
|
||||
);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS fl_customer_dimension_config")
|
||||
.contains("edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000")
|
||||
.contains("initialization_key VARCHAR(128) NOT NULL")
|
||||
.contains("UNIQUE KEY uk_customer_dimension_config_tenant (tenant_id)");
|
||||
for (String sql : sqlEntrypoints) {
|
||||
assertThat(sql)
|
||||
.contains("fl_customer_dimension_config")
|
||||
.contains("box_type VARCHAR(24) NOT NULL")
|
||||
.contains("inner_length_cm DECIMAL(9,3) NOT NULL")
|
||||
.contains("inner_width_cm DECIMAL(9,3) NOT NULL")
|
||||
.contains("inner_height_cm DECIMAL(9,3) NOT NULL")
|
||||
.contains("edge_addition_cm DECIMAL(6,3) NOT NULL DEFAULT 2.000")
|
||||
.contains("initialization_key VARCHAR(128) NOT NULL")
|
||||
.contains("created_by BIGINT UNSIGNED NOT NULL")
|
||||
.contains("updated_by BIGINT UNSIGNED NOT NULL")
|
||||
.contains("version INT NOT NULL DEFAULT 0")
|
||||
.contains("UNIQUE KEY uk_customer_dimension_config_tenant_box (tenant_id, box_type)")
|
||||
.contains("CHECK (box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE'))")
|
||||
.doesNotContain("UNIQUE KEY uk_customer_dimension_config_tenant (tenant_id)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationIncludesConfigAndFinalAssertion() throws IOException {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
void fixedDefaultsAndBatchInitializationCoverExactlyFourBoxTypes() throws Exception {
|
||||
assertThat(CustomerDimensionBoxType.values()).extracting(CustomerDimensionBoxType::name)
|
||||
.containsExactly("SMALL", "MEDIUM", "LARGE", "EXTRA_LARGE");
|
||||
assertThat(List.of(CustomerDimensionBoxType.values()))
|
||||
.extracting(CustomerDimensionBoxType::defaultLengthCm)
|
||||
.containsExactly(new java.math.BigDecimal("30.000"), new java.math.BigDecimal("40.000"),
|
||||
new java.math.BigDecimal("50.000"), new java.math.BigDecimal("60.000"));
|
||||
|
||||
Insert annotation = CustomerDimensionConfigMapper.class
|
||||
.getDeclaredMethod("insertDefaults", List.class, long.class)
|
||||
.getAnnotation(Insert.class);
|
||||
String sql = String.join(" ", annotation.value());
|
||||
assertThat(sql)
|
||||
.contains("CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_customer_dimension_config")
|
||||
.contains("客户尺寸计算配置表未创建")
|
||||
.contains("uk_customer_dimension_config_tenant")
|
||||
.contains("CREATE PROCEDURE unified_guard_dimension_config_permissions()")
|
||||
.contains("id = 5102111 AND COALESCE(permission_code, '') <> 'firstleg:dimension-config:view'")
|
||||
.contains("permission_code = 'firstleg:dimension-config:view' AND id <> 5102111")
|
||||
.contains("id = 5102112 AND COALESCE(permission_code, '') <> 'firstleg:dimension-config:edit'")
|
||||
.contains("permission_code = 'firstleg:dimension-config:edit' AND id <> 5102112")
|
||||
.contains("(5102111, 0, 'PLATFORM', 'BUTTON', 5100010")
|
||||
.contains("(5102112, 0, 'PLATFORM', 'BUTTON', 5100010")
|
||||
.contains("r.role_code = 'PLATFORM_ADMIN'")
|
||||
.contains("r.role_code = 'ORDER_OPERATOR'")
|
||||
.contains("客户尺寸计算配置权限父节点企业客户不存在")
|
||||
.contains("客户尺寸计算配置权限码、固定ID或父节点不正确")
|
||||
.contains("平台管理员未获得客户尺寸计算配置读写权限")
|
||||
.contains("订单运营角色的客户尺寸计算配置权限不是只读");
|
||||
.contains("INSERT IGNORE INTO fl_customer_dimension_config")
|
||||
.contains("box_type, inner_length_cm, inner_width_cm, inner_height_cm")
|
||||
.contains("<foreach collection=\"rows\"");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationDeleteRequiresExactKeyDefaultValueAndUnmodifiedVersion() throws Exception {
|
||||
Delete annotation = com.baiyue.firstleg.mapper.CustomerDimensionConfigMapper.class
|
||||
.getDeclaredMethod("deleteUnchangedDefault", long.class, long.class,
|
||||
String.class, java.math.BigDecimal.class)
|
||||
void compensationLocksThenDeletesOnlyExactDefaultRows() throws Exception {
|
||||
Select select = CustomerDimensionConfigMapper.class
|
||||
.getDeclaredMethod("selectDefaultsForCompensation", long.class, String.class)
|
||||
.getAnnotation(Select.class);
|
||||
assertThat(String.join(" ", select.value()))
|
||||
.contains("tenant_id = #{tenantId}")
|
||||
.contains("initialization_key = #{initializationKey}")
|
||||
.contains("FOR UPDATE");
|
||||
|
||||
Delete annotation = CustomerDimensionConfigMapper.class
|
||||
.getDeclaredMethod("deleteUnchangedDefaults", long.class, String.class)
|
||||
.getAnnotation(Delete.class);
|
||||
String sql = String.join(" ", annotation.value());
|
||||
|
||||
assertThat(sql)
|
||||
.contains("tenant_id = #{tenantId}")
|
||||
.contains("initialization_key = #{initializationKey}")
|
||||
.contains("edge_addition_cm = #{edgeAdditionCm}")
|
||||
.contains("version = 0");
|
||||
.contains("version = 0")
|
||||
.contains("box_type IN ('SMALL', 'MEDIUM', 'LARGE', 'EXTRA_LARGE')")
|
||||
.contains("inner_length_cm = CASE box_type")
|
||||
.contains("edge_addition_cm = 2.000")
|
||||
.contains("remark = CASE box_type");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationKeepsPermissionGuardsAndAssertsFinalFourRowSchema() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
assertThat(sql)
|
||||
.contains("CREATE PROCEDURE unified_guard_dimension_config_permissions()")
|
||||
.contains("(5102111, 0, 'PLATFORM', 'BUTTON', 5100010")
|
||||
.contains("r.role_code = 'PLATFORM_ADMIN'")
|
||||
.contains("r.role_code = 'ORDER_OPERATOR'")
|
||||
.contains("index_name = 'uk_customer_dimension_config_tenant_box'")
|
||||
.contains("客户四箱规尺寸配置字段不完整")
|
||||
.contains("客户尺寸计算配置权限码、固定ID或父节点不正确");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationUpgradesOnlyRecognizedLegacyTableAndBackfillsIdempotently() throws Exception {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
int upgrade = sql.indexOf("CREATE PROCEDURE unified_upgrade_legacy_customer_dimension_config()");
|
||||
int create = sql.indexOf("CREATE TABLE IF NOT EXISTS baiyue_firstleg.fl_customer_dimension_config");
|
||||
int backfill = sql.indexOf("INSERT INTO baiyue_firstleg.fl_customer_dimension_config (", create);
|
||||
assertThat(upgrade).isGreaterThanOrEqualTo(0);
|
||||
assertThat(create).isGreaterThan(upgrade);
|
||||
assertThat(backfill).isGreaterThan(create);
|
||||
assertThat(sql)
|
||||
.contains("AND column_name = 'box_type') = 0")
|
||||
.contains("AND column_name IN ('id', 'tenant_id', 'edge_addition_cm', 'remark', 'initialization_key'")
|
||||
.contains("AND index_name = 'uk_customer_dimension_config_tenant'")
|
||||
.contains("DROP TABLE baiyue_firstleg.fl_customer_dimension_config")
|
||||
.contains("客户尺寸配置表不是可识别的旧单行结构")
|
||||
.contains("SELECT id AS tenant_id, created_by")
|
||||
.contains("FROM baiyue_iam.iam_tenant")
|
||||
.contains("WHERE tenant_type = 'MERCHANT' AND deleted = 0 AND id > 0")
|
||||
.contains("CONCAT('unified-four-box-default-v1:', tenant.tenant_id)")
|
||||
.contains("ON DUPLICATE KEY UPDATE id = id")
|
||||
.contains("现有企业客户未完整持有固定四箱规尺寸配置");
|
||||
|
||||
String repeatSafeClause = sql.substring(backfill,
|
||||
sql.indexOf("DROP PROCEDURE IF EXISTS unified_assert_final_extensions", backfill));
|
||||
assertThat(repeatSafeClause)
|
||||
.doesNotContain("edge_addition_cm = VALUES")
|
||||
.doesNotContain("inner_length_cm = VALUES")
|
||||
.doesNotContain("remark = VALUES")
|
||||
.doesNotContain("version = VALUES");
|
||||
}
|
||||
|
||||
private static String resource(String path) throws Exception {
|
||||
return new ClassPathResource(path).getContentAsString(StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
|
||||
+141
-48
@@ -1,22 +1,30 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.domain.CustomerDimensionBoxType;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.GroupResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.RuleUpdateRequest;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.UpdateRequest;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.repository.CustomerDimensionConfigRepository;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -28,82 +36,167 @@ class CustomerDimensionConfigServiceTest {
|
||||
private CustomerDimensionConfigService service;
|
||||
|
||||
@Test
|
||||
void initializesTwoCentimeterDefaultExactlyOnce() {
|
||||
CustomerDimensionConfigEntity persisted = config(91L, 8L, "2.000", 0, "registration-1");
|
||||
when(repository.find(8L)).thenReturn(null, persisted);
|
||||
when(repository.insertDefault(anyLong(), eq(8L), eq(new BigDecimal("2.000")),
|
||||
eq("默认每侧加宽 2cm"), eq("registration-1"), eq(100L))).thenReturn(true);
|
||||
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
|
||||
|
||||
assertThat(result.createdByThisInitialization()).isTrue();
|
||||
assertThat(result.configId()).isEqualTo(91L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryAfterLostResponseKeepsCreationOwnershipForSameKey() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "registration-1"));
|
||||
void initializesFourDefaultsAtomicallyInFixedOrder() {
|
||||
List<CustomerDimensionConfigEntity> persisted = defaultConfigs("registration-1");
|
||||
when(repository.findAll(8L)).thenReturn(List.of(), persisted);
|
||||
when(repository.insertDefaults(any(), eq(100L))).thenReturn(4);
|
||||
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
|
||||
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, true));
|
||||
verify(repository, never()).insertDefault(anyLong(), anyLong(),
|
||||
eq(new BigDecimal("2.000")), eq("默认每侧加宽 2cm"),
|
||||
eq("registration-1"), anyLong());
|
||||
@SuppressWarnings("unchecked")
|
||||
ArgumentCaptor<List<CustomerDimensionConfigEntity>> captor = ArgumentCaptor.forClass(List.class);
|
||||
verify(repository).insertDefaults(captor.capture(), eq(100L));
|
||||
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getBoxType)
|
||||
.containsExactly(CustomerDimensionBoxType.values());
|
||||
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getInnerLengthCm)
|
||||
.containsExactly(new BigDecimal("30.000"), new BigDecimal("40.000"),
|
||||
new BigDecimal("50.000"), new BigDecimal("60.000"));
|
||||
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getInnerWidthCm)
|
||||
.containsExactly(new BigDecimal("20.000"), new BigDecimal("30.000"),
|
||||
new BigDecimal("40.000"), new BigDecimal("50.000"));
|
||||
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getInnerHeightCm)
|
||||
.containsExactly(new BigDecimal("20.000"), new BigDecimal("30.000"),
|
||||
new BigDecimal("40.000"), new BigDecimal("50.000"));
|
||||
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getEdgeAdditionCm)
|
||||
.containsOnly(new BigDecimal("2.000"));
|
||||
assertThat(captor.getValue()).extracting(CustomerDimensionConfigEntity::getRemark)
|
||||
.containsExactly("标准小箱", "标准中箱", "标准大箱", "特大箱");
|
||||
}
|
||||
|
||||
@Test
|
||||
void retryAfterLostResponseKeepsCreationOwnershipForSameKey() {
|
||||
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
|
||||
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-1");
|
||||
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, true));
|
||||
verify(repository, never()).insertDefaults(any(), eq(100L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentInitializationKeyDoesNotTakeOwnershipOrOverwrite() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "registration-1"));
|
||||
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
|
||||
|
||||
InitializeResponse result = service.initializeDefault(8L, 100L, "registration-2");
|
||||
|
||||
assertThat(result).isEqualTo(new InitializeResponse(91L, false));
|
||||
verify(repository, never()).insertDefault(anyLong(), anyLong(),
|
||||
eq(new BigDecimal("2.000")), eq("默认每侧加宽 2cm"),
|
||||
eq("registration-2"), anyLong());
|
||||
verify(repository, never()).insertDefaults(any(), eq(100L));
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleUpdateVersion() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 3, "registration-1"));
|
||||
void returnsFixedOrderCalculatedOuterDimensionsAndThreeDecimalVolume() {
|
||||
List<CustomerDimensionConfigEntity> shuffled = new ArrayList<>(defaultConfigs("registration-1"));
|
||||
Collections.reverse(shuffled);
|
||||
when(repository.findAll(8L)).thenReturn(shuffled);
|
||||
|
||||
assertThatThrownBy(() -> service.update(8L, 100L,
|
||||
new UpdateRequest(new BigDecimal("2.500"), "新配置", 2)))
|
||||
GroupResponse result = service.get(8L);
|
||||
|
||||
assertThat(result.rules()).extracting(rule -> rule.boxType())
|
||||
.containsExactly(CustomerDimensionBoxType.values());
|
||||
assertThat(result.rules().get(0).boxTypeName()).isEqualTo("小箱");
|
||||
assertThat(result.rules().get(0).outerLengthCm()).isEqualByComparingTo("34.000");
|
||||
assertThat(result.rules().get(0).outerWidthCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(result.rules().get(0).outerHeightCm()).isEqualByComparingTo("24.000");
|
||||
assertThat(result.rules().get(0).volumeCbm()).isEqualByComparingTo("0.020");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsMissingOrDuplicateBoxTypes() {
|
||||
List<RuleUpdateRequest> rules = new ArrayList<>(updateRequest(0).rules());
|
||||
rules.set(3, rules.get(0));
|
||||
|
||||
assertThatThrownBy(() -> service.update(8L, 100L, new UpdateRequest(rules)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("重复");
|
||||
verify(repository, never()).findAll(8L);
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsAnyStaleVersionBeforeWritingRows() {
|
||||
List<CustomerDimensionConfigEntity> persisted = defaultConfigs("registration-1");
|
||||
persisted.forEach(value -> value.setVersion(3));
|
||||
when(repository.findAll(8L)).thenReturn(persisted);
|
||||
UpdateRequest request = updateRequest(3);
|
||||
List<RuleUpdateRequest> rules = new ArrayList<>(request.rules());
|
||||
RuleUpdateRequest stale = rules.get(2);
|
||||
rules.set(2, new RuleUpdateRequest(stale.boxType(), stale.innerLengthCm(), stale.innerWidthCm(),
|
||||
stale.innerHeightCm(), stale.edgeAdditionCm(), stale.remark(), 2));
|
||||
|
||||
assertThatThrownBy(() -> service.update(8L, 100L, new UpdateRequest(rules)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("刷新后重试");
|
||||
verify(repository, never()).update(org.mockito.ArgumentMatchers.any());
|
||||
verify(repository, never()).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationUsesTenantKeyDefaultValueAndUnmodifiedVersion() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "registration-1"));
|
||||
void abortsWholeTransactionalUpdateWhenAnyOptimisticWriteFails() {
|
||||
when(repository.findAll(8L)).thenReturn(defaultConfigs("registration-1"));
|
||||
when(repository.update(any())).thenReturn(1, 0);
|
||||
|
||||
service.compensateDefault(8L, "registration-1");
|
||||
|
||||
verify(repository).deleteUnchangedDefault(
|
||||
8L, 91L, "registration-1", new BigDecimal("2.000"));
|
||||
assertThatThrownBy(() -> service.update(8L, 100L, updateRequest(0)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("刷新后重试");
|
||||
verify(repository, times(2)).update(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void editedConfigIsNotCompensatedByDelayedRollback() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.500", 1, "registration-1"));
|
||||
void compensationLocksAndDeletesExactlyFourUnchangedDefaults() {
|
||||
when(repository.findDefaultsForCompensation(8L, "registration-1"))
|
||||
.thenReturn(defaultConfigs("registration-1"));
|
||||
when(repository.deleteUnchangedDefaults(8L, "registration-1")).thenReturn(4);
|
||||
|
||||
service.compensateDefault(8L, "registration-1");
|
||||
|
||||
verify(repository).deleteUnchangedDefault(
|
||||
8L, 91L, "registration-1", new BigDecimal("2.000"));
|
||||
verify(repository).deleteUnchangedDefaults(8L, "registration-1");
|
||||
}
|
||||
|
||||
private static CustomerDimensionConfigEntity config(long id, long tenantId,
|
||||
String edgeAddition, int version,
|
||||
String initializationKey) {
|
||||
CustomerDimensionConfigEntity value = new CustomerDimensionConfigEntity();
|
||||
value.setId(id);
|
||||
value.setTenantId(tenantId);
|
||||
value.setEdgeAdditionCm(new BigDecimal(edgeAddition));
|
||||
value.setVersion(version);
|
||||
value.setInitializationKey(initializationKey);
|
||||
return value;
|
||||
@Test
|
||||
void compensationDoesNotDeleteAnEditedGroup() {
|
||||
List<CustomerDimensionConfigEntity> values = defaultConfigs("registration-1");
|
||||
values.get(2).setVersion(1);
|
||||
when(repository.findDefaultsForCompensation(8L, "registration-1")).thenReturn(values);
|
||||
|
||||
service.compensateDefault(8L, "registration-1");
|
||||
|
||||
verify(repository, never()).deleteUnchangedDefaults(8L, "registration-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationRollsBackInsteadOfAcceptingPartialDelete() {
|
||||
when(repository.findDefaultsForCompensation(8L, "registration-1"))
|
||||
.thenReturn(defaultConfigs("registration-1"));
|
||||
when(repository.deleteUnchangedDefaults(8L, "registration-1")).thenReturn(3);
|
||||
|
||||
assertThatThrownBy(() -> service.compensateDefault(8L, "registration-1"))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("补偿删除失败");
|
||||
}
|
||||
|
||||
private static UpdateRequest updateRequest(int version) {
|
||||
return new UpdateRequest(List.of(CustomerDimensionBoxType.values()).stream()
|
||||
.map(type -> new RuleUpdateRequest(type, type.defaultLengthCm(), type.defaultWidthCm(),
|
||||
type.defaultHeightCm(), new BigDecimal("2.000"), type.defaultRemark(), version))
|
||||
.toList());
|
||||
}
|
||||
|
||||
private static List<CustomerDimensionConfigEntity> defaultConfigs(String initializationKey) {
|
||||
List<CustomerDimensionConfigEntity> values = new ArrayList<>();
|
||||
long id = 91L;
|
||||
for (CustomerDimensionBoxType type : CustomerDimensionBoxType.values()) {
|
||||
CustomerDimensionConfigEntity value = new CustomerDimensionConfigEntity();
|
||||
value.setId(id++);
|
||||
value.setTenantId(8L);
|
||||
value.setBoxType(type);
|
||||
value.setInnerLengthCm(type.defaultLengthCm());
|
||||
value.setInnerWidthCm(type.defaultWidthCm());
|
||||
value.setInnerHeightCm(type.defaultHeightCm());
|
||||
value.setEdgeAdditionCm(new BigDecimal("2.000"));
|
||||
value.setRemark(type.defaultRemark());
|
||||
value.setVersion(0);
|
||||
value.setInitializationKey(initializationKey);
|
||||
values.add(value);
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.firstleg.client.MeasurementImageClient;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementRequests;
|
||||
import com.baiyue.firstleg.dto.InboundMeasurementResponses;
|
||||
import com.baiyue.firstleg.entity.InboundMeasurementEntity;
|
||||
@@ -18,6 +20,7 @@ import org.junit.jupiter.params.provider.CsvSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
@@ -27,17 +30,21 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.assertj.core.api.Assertions.tuple;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.anyLong;
|
||||
import static org.mockito.ArgumentMatchers.anyInt;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.times;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.verifyNoInteractions;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@DisplayName("平台入库测量应用服务")
|
||||
@SpringJUnitConfig(InboundMeasurementService.class)
|
||||
@TestPropertySource(properties = "baiyue.security.internal-service-token=internal-token")
|
||||
class InboundMeasurementServiceTest {
|
||||
@MockBean private InboundMeasurementRepository repository;
|
||||
@MockBean private MeasurementBatchService batchService;
|
||||
@MockBean private MeasurementImageClient imageClient;
|
||||
@Resource private InboundMeasurementService service;
|
||||
|
||||
@Test
|
||||
@@ -357,6 +364,58 @@ class InboundMeasurementServiceTest {
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletingPendingWeightRemovesResultAndOwnedImage() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 1);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.deleteWeight(1L, 10L, 20L, 1)).thenReturn(true);
|
||||
when(imageClient.delete("internal-token", 9L, 1L, 10L, 120L))
|
||||
.thenReturn(ApiResponse.success(null));
|
||||
|
||||
service.deleteWeightResult(1L, 10L, 20L, 1, 9L);
|
||||
|
||||
verify(repository).deleteWeight(1L, 10L, 20L, 1);
|
||||
verify(imageClient).delete("internal-token", 9L, 1L, 10L, 120L);
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletingLastConfirmedWeightClearsInboundTotal() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
inbound.setActualGrossWeightKg(new BigDecimal("285.500"));
|
||||
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "CONFIRMED", 2);
|
||||
weight.setFinalWeightKg(new BigDecimal("285.500"));
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.deleteWeight(1L, 10L, 20L, 2)).thenReturn(true);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of());
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
when(imageClient.delete("internal-token", 9L, 1L, 10L, 120L))
|
||||
.thenReturn(ApiResponse.success(null));
|
||||
|
||||
service.deleteWeightResult(1L, 10L, 20L, 2, 9L);
|
||||
|
||||
assertThat(inbound.getActualGrossWeightKg()).isNull();
|
||||
verify(repository).updateInbound(inbound);
|
||||
}
|
||||
|
||||
@Test
|
||||
void deletingWeightRejectsStaleVersionBeforeCallingFileService() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 2);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
|
||||
assertThatThrownBy(() -> service.deleteWeightResult(1L, 10L, 20L, 1, 9L))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("版本已变化");
|
||||
|
||||
verify(repository, never()).deleteWeight(anyLong(), anyLong(), anyLong(), anyInt());
|
||||
verifyNoInteractions(imageClient);
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitAdvancesOnlyToMeasurePendingAfterAllWeightsConfirmed() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
|
||||
Reference in New Issue
Block a user