优化
This commit is contained in:
+61
@@ -0,0 +1,61 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeRequest;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeResponse;
|
||||
import com.baiyue.firstleg.service.CustomerDimensionConfigService;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/internal/firstleg/v1/customer-dimension-configs")
|
||||
public class InternalCustomerDimensionConfigController {
|
||||
@Resource
|
||||
private CustomerDimensionConfigService service;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
private byte[] expectedToken;
|
||||
|
||||
@PostConstruct
|
||||
void initializeToken() {
|
||||
expectedToken = internalToken.getBytes(StandardCharsets.UTF_8);
|
||||
}
|
||||
|
||||
@PostMapping("/defaults")
|
||||
public ApiResponse<InitializeResponse> initializeDefault(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@Valid @RequestBody InitializeRequest request) {
|
||||
requireToken(token);
|
||||
return ApiResponse.success(service.initializeDefault(
|
||||
request.tenantId(), request.operatorId(), request.initializationKey()));
|
||||
}
|
||||
|
||||
@DeleteMapping("/defaults")
|
||||
public ApiResponse<Void> compensateDefault(
|
||||
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
|
||||
@RequestParam long tenantId, @RequestParam String initializationKey) {
|
||||
requireToken(token);
|
||||
service.compensateDefault(tenantId, initializationKey);
|
||||
return ApiResponse.success(null);
|
||||
}
|
||||
|
||||
private void requireToken(String token) {
|
||||
if (token == null || !MessageDigest.isEqual(expectedToken, token.getBytes(StandardCharsets.UTF_8))) {
|
||||
throw new BizException(ResultCode.FORBIDDEN, "内部服务凭证无效");
|
||||
}
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
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.UpdateRequest;
|
||||
import com.baiyue.firstleg.service.CustomerDimensionConfigService;
|
||||
import com.baiyue.firstleg.service.FirstlegScopeGuard;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.annotation.Resource;
|
||||
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.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@Tag(name = "客户尺寸计算配置")
|
||||
@RestController
|
||||
@RequestMapping("/api/platform/v1/first-leg/customer-dimension-config")
|
||||
public class PlatformCustomerDimensionConfigController {
|
||||
@Resource
|
||||
private CustomerDimensionConfigService service;
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasAuthority('firstleg:dimension-config:view')")
|
||||
@Operation(summary = "查询客户尺寸计算配置")
|
||||
public ApiResponse<Response> get(@AuthenticationPrincipal AuthenticatedUser user,
|
||||
@RequestParam long tenantId) {
|
||||
return ApiResponse.success(service.get(FirstlegScopeGuard.platform(user, tenantId)));
|
||||
}
|
||||
|
||||
@PutMapping
|
||||
@PreAuthorize("hasAuthority('firstleg:dimension-config:edit')")
|
||||
@BusinessAudit(event = "CUSTOMER_DIMENSION_CONFIG_UPDATE",
|
||||
resourceType = "DIMENSION_CONFIG", resourceId = "#tenantId",
|
||||
description = "平台编辑客户尺寸计算配置")
|
||||
@Operation(summary = "编辑客户尺寸计算配置")
|
||||
public ApiResponse<Response> 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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.constraints.DecimalMax;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Digits;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
public final class CustomerDimensionConfigDtos {
|
||||
private CustomerDimensionConfigDtos() { }
|
||||
|
||||
public record UpdateRequest(
|
||||
@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(
|
||||
Long id,
|
||||
Long tenantId,
|
||||
BigDecimal edgeAdditionCm,
|
||||
String remark,
|
||||
Integer version,
|
||||
LocalDateTime createdAt,
|
||||
LocalDateTime updatedAt
|
||||
) { }
|
||||
|
||||
public record InitializeRequest(
|
||||
@NotNull @Min(1) Long tenantId,
|
||||
@NotNull @Min(0) Long operatorId,
|
||||
@NotBlank @Size(max = 128) String initializationKey
|
||||
) { }
|
||||
|
||||
public record InitializeResponse(Long configId, boolean createdByThisInitialization) { }
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.baiyue.firstleg.entity;
|
||||
|
||||
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 lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 客户填写真实尺寸后使用的每侧加宽配置。 */
|
||||
@Getter
|
||||
@Setter
|
||||
@TableName("fl_customer_dimension_config")
|
||||
public class CustomerDimensionConfigEntity extends BaseEntity {
|
||||
@TableId(type = IdType.ASSIGN_ID)
|
||||
private Long id;
|
||||
private Long tenantId;
|
||||
private BigDecimal edgeAdditionCm;
|
||||
private String remark;
|
||||
private String initializationKey;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.baiyue.firstleg.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
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 java.math.BigDecimal;
|
||||
|
||||
public interface CustomerDimensionConfigMapper extends BaseMapper<CustomerDimensionConfigEntity> {
|
||||
@Insert("""
|
||||
INSERT IGNORE INTO fl_customer_dimension_config
|
||||
(id, tenant_id, 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)
|
||||
""")
|
||||
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);
|
||||
|
||||
@Delete("""
|
||||
DELETE FROM fl_customer_dimension_config
|
||||
WHERE id = #{id}
|
||||
AND tenant_id = #{tenantId}
|
||||
AND initialization_key = #{initializationKey}
|
||||
AND edge_addition_cm = #{edgeAdditionCm}
|
||||
AND version = 0
|
||||
AND deleted = 0
|
||||
""")
|
||||
int deleteUnchangedDefault(@Param("tenantId") long tenantId, @Param("id") long id,
|
||||
@Param("initializationKey") String initializationKey,
|
||||
@Param("edgeAdditionCm") BigDecimal edgeAdditionCm);
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baiyue.firstleg.entity.CustomerDimensionConfigEntity;
|
||||
import com.baiyue.firstleg.mapper.CustomerDimensionConfigMapper;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
@Repository
|
||||
public class CustomerDimensionConfigRepository {
|
||||
@Resource
|
||||
private CustomerDimensionConfigMapper mapper;
|
||||
|
||||
public CustomerDimensionConfigEntity find(long tenantId) {
|
||||
return mapper.selectOne(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 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);
|
||||
}
|
||||
}
|
||||
+102
@@ -0,0 +1,102 @@
|
||||
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.dto.CustomerDimensionConfigDtos.InitializeResponse;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.Response;
|
||||
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.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/** 管理每个客户唯一的尺寸加边配置,不参与当前订单或计费计算。 */
|
||||
@Service
|
||||
public class CustomerDimensionConfigService {
|
||||
public static final BigDecimal DEFAULT_EDGE_ADDITION_CM = new BigDecimal("2.000");
|
||||
private static final String DEFAULT_REMARK = "默认每侧加宽 2cm";
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public Response update(long tenantId, long operatorId, UpdateRequest request) {
|
||||
CustomerDimensionConfigEntity value = repository.find(tenantId);
|
||||
if (value == null) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "客户尺寸计算配置不存在");
|
||||
}
|
||||
if (!value.getVersion().equals(request.version())) {
|
||||
throw new BizException(ResultCode.CONFLICT, "尺寸计算配置已被修改,请刷新后重试");
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@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()));
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
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 String normalizeRemark(String value) {
|
||||
if (value == null || value.isBlank()) return null;
|
||||
return value.trim();
|
||||
}
|
||||
|
||||
private static void requireTenant(long tenantId) {
|
||||
if (tenantId <= 0) throw new BizException(ResultCode.BAD_REQUEST, "客户ID无效");
|
||||
}
|
||||
|
||||
private static String requireInitializationKey(String value) {
|
||||
if (value == null || value.isBlank() || value.trim().length() > 128) {
|
||||
throw new BizException(ResultCode.BAD_REQUEST, "初始化幂等键无效");
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
@@ -199,6 +199,9 @@ public class InboundMeasurementService {
|
||||
requireVersion(request.version(), weight.getVersion());
|
||||
int currentVersion = request.version();
|
||||
if (request.hasAdjustment()) {
|
||||
requireCompleteAdjustment(request.adjustedWeightValue(), request.adjustedWeightUnit(),
|
||||
request.adjustmentReason());
|
||||
requireConfirmableKg(request.adjustedWeightValue(), request.adjustedWeightUnit());
|
||||
currentVersion = applyWeightCorrection(weight, currentVersion,
|
||||
request.adjustedWeightValue(), request.adjustedWeightUnit(),
|
||||
request.adjustmentReason(), operatorId, operatorName);
|
||||
@@ -211,7 +214,7 @@ public class InboundMeasurementService {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED, "OCR 重量无法确认,请先人工修正数值和单位");
|
||||
}
|
||||
String unit = normalizeUnit(rawUnit);
|
||||
BigDecimal finalKg = toKg(value, unit);
|
||||
BigDecimal finalKg = requireConfirmableKg(value, unit);
|
||||
weight.setFinalWeightKg(finalKg);
|
||||
weight.setRecognitionStatus(WEIGHT_CONFIRMED);
|
||||
weight.setConfirmedBy(operatorId);
|
||||
@@ -418,6 +421,15 @@ public class InboundMeasurementService {
|
||||
return kg.setScale(3, RoundingMode.HALF_UP);
|
||||
}
|
||||
|
||||
private BigDecimal requireConfirmableKg(BigDecimal value, String unit) {
|
||||
BigDecimal finalKg = toKg(value, normalizeUnit(unit));
|
||||
if (!isPositive(finalKg)) {
|
||||
throw new BizException(ResultCode.VALIDATION_FAILED,
|
||||
"换算后重量不能小于 0.001 KG");
|
||||
}
|
||||
return finalKg;
|
||||
}
|
||||
|
||||
private BigDecimal effectiveValue(InboundWeightResultEntity weight) {
|
||||
return weight.getAdjustedWeightValue() == null
|
||||
? weight.getOcrWeightValue() : weight.getAdjustedWeightValue();
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
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',
|
||||
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 (tenant_id),
|
||||
CONSTRAINT chk_customer_dimension_edge_addition
|
||||
CHECK (edge_addition_cm >= 0.001 AND edge_addition_cm <= 100.000)
|
||||
) ENGINE=InnoDB COMMENT='客户尺寸计算配置';
|
||||
@@ -729,3 +729,21 @@ CREATE TABLE IF NOT EXISTS fl_inbound_idempotency (
|
||||
UNIQUE KEY uk_inbound_idempotency (tenant_id, operation, operator_id, idempotency_key),
|
||||
KEY idx_inbound_idempotency_created (created_at)
|
||||
) ENGINE=InnoDB COMMENT='第三方测量、审核、费用发送和入库写操作幂等记录';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS fl_customer_dimension_config (
|
||||
id BIGINT UNSIGNED NOT NULL COMMENT '配置ID',
|
||||
tenant_id BIGINT UNSIGNED NOT NULL COMMENT '客户隔离ID',
|
||||
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 (tenant_id),
|
||||
CONSTRAINT chk_customer_dimension_edge_addition
|
||||
CHECK (edge_addition_cm >= 0.001 AND edge_addition_cm <= 100.000)
|
||||
) ENGINE=InnoDB COMMENT='客户尺寸计算配置';
|
||||
|
||||
@@ -6,6 +6,51 @@ info:
|
||||
security:
|
||||
- bearerAuth: []
|
||||
paths:
|
||||
/internal/firstleg/v1/customer-dimension-configs/defaults:
|
||||
post:
|
||||
summary: 幂等初始化客户尺寸计算默认配置
|
||||
description: 仅供 IAM 在客户创建或注册成功编排中调用;同一客户和 initializationKey 重试稳定返回 createdByThisInitialization=true。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CustomerDimensionConfigInitializeRequest'}
|
||||
responses:
|
||||
'200': {description: 成功或业务失败,data 返回 configId 和 createdByThisInitialization}
|
||||
delete:
|
||||
summary: 补偿删除本次新建且尚未修改的默认配置
|
||||
description: 仅供 IAM 创建客户事务补偿使用;同时匹配 initializationKey、默认2cm和version=0才物理删除。
|
||||
security:
|
||||
- internalServiceToken: []
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
- {name: initializationKey, in: query, required: true, schema: {type: string, minLength: 1, maxLength: 128}}
|
||||
responses:
|
||||
'200': {description: 幂等补偿完成}
|
||||
/api/platform/v1/first-leg/customer-dimension-config:
|
||||
get:
|
||||
summary: 查询客户尺寸计算配置
|
||||
description: 需要 firstleg:dimension-config:view 权限;配置只描述真实尺寸的每侧加宽值,当前不参与订单或计费。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
responses:
|
||||
'200': {description: 成功时 data 为 CustomerDimensionConfigResponse}
|
||||
'403': {description: 无查询权限}
|
||||
put:
|
||||
summary: 编辑客户尺寸计算配置
|
||||
description: 需要 firstleg:dimension-config:edit 权限;使用 version 执行乐观锁更新。
|
||||
parameters:
|
||||
- {name: tenantId, in: query, required: true, schema: {type: string, pattern: '^[1-9][0-9]*$'}}
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema: {$ref: '#/components/schemas/CustomerDimensionConfigUpdateRequest'}
|
||||
responses:
|
||||
'200': {description: 成功时 data 为更新后的 CustomerDimensionConfigResponse}
|
||||
'403': {description: 无编辑权限}
|
||||
/internal/firstleg/v1/inbounds/order-approved:
|
||||
post:
|
||||
summary: 订单审核通过后幂等创建入库单
|
||||
@@ -358,6 +403,43 @@ components:
|
||||
in: query
|
||||
schema: {type: integer, format: int64, minimum: 1, maximum: 200, default: 20}
|
||||
schemas:
|
||||
CustomerDimensionConfigUpdateRequest:
|
||||
type: object
|
||||
required: [edgeAdditionCm, version]
|
||||
properties:
|
||||
edgeAdditionCm:
|
||||
type: number
|
||||
minimum: 0.001
|
||||
maximum: 100
|
||||
multipleOf: 0.001
|
||||
description: 真实长、宽、高三个方向分别使用的每侧加宽厘米数
|
||||
example: 2.000
|
||||
remark: {type: string, maxLength: 500, nullable: true}
|
||||
version: {type: integer, minimum: 0, description: 乐观锁版本号}
|
||||
CustomerDimensionConfigResponse:
|
||||
type: object
|
||||
required: [id, tenantId, edgeAdditionCm, version]
|
||||
properties:
|
||||
id: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
edgeAdditionCm: {type: number, minimum: 0.001, maximum: 100, example: 2.000}
|
||||
remark: {type: string, nullable: true}
|
||||
version: {type: integer, minimum: 0}
|
||||
createdAt: {type: string, format: date-time, nullable: true}
|
||||
updatedAt: {type: string, format: date-time, nullable: true}
|
||||
CustomerDimensionConfigInitializeRequest:
|
||||
type: object
|
||||
required: [tenantId, operatorId, initializationKey]
|
||||
properties:
|
||||
tenantId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
operatorId: {type: string, pattern: '^[0-9]+$'}
|
||||
initializationKey: {type: string, minLength: 1, maxLength: 128}
|
||||
CustomerDimensionConfigInitializeResponse:
|
||||
type: object
|
||||
required: [configId, createdByThisInitialization]
|
||||
properties:
|
||||
configId: {type: string, pattern: '^[1-9][0-9]*$'}
|
||||
createdByThisInitialization: {type: boolean}
|
||||
ConfirmWeightResultRequest:
|
||||
type: object
|
||||
required: [version]
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package com.baiyue.firstleg.controller;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeRequest;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeResponse;
|
||||
import com.baiyue.firstleg.service.CustomerDimensionConfigService;
|
||||
import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(InternalCustomerDimensionConfigController.class)
|
||||
class CustomerDimensionConfigControllerContractTest {
|
||||
@Resource
|
||||
private InternalCustomerDimensionConfigController internalController;
|
||||
@MockBean
|
||||
private CustomerDimensionConfigService service;
|
||||
|
||||
@BeforeEach
|
||||
void initializeToken() {
|
||||
ReflectionTestUtils.setField(internalController, "internalToken", "test-token");
|
||||
internalController.initializeToken();
|
||||
}
|
||||
|
||||
@Test
|
||||
void internalInitializationRequiresTokenAndReturnsCreationIdentity() {
|
||||
InitializeRequest request = new InitializeRequest(8L, 100L, "registration-1");
|
||||
when(service.initializeDefault(8L, 100L, "registration-1"))
|
||||
.thenReturn(new InitializeResponse(91L, true));
|
||||
|
||||
assertThatThrownBy(() -> internalController.initializeDefault("wrong", request))
|
||||
.isInstanceOf(BizException.class);
|
||||
assertThat(internalController.initializeDefault("test-token", request).data())
|
||||
.isEqualTo(new InitializeResponse(91L, true));
|
||||
verify(service).initializeDefault(8L, 100L, "registration-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void platformControllerKeepsScopeAndExistingPermissions() throws Exception {
|
||||
RequestMapping mapping = PlatformCustomerDimensionConfigController.class
|
||||
.getAnnotation(RequestMapping.class);
|
||||
assertThat(mapping.value()).containsExactly("/api/platform/v1/first-leg/customer-dimension-config");
|
||||
assertThat(PlatformCustomerDimensionConfigController.class
|
||||
.getDeclaredMethod("get", com.baiyue.common.security.AuthenticatedUser.class, long.class)
|
||||
.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('firstleg:dimension-config:view')");
|
||||
assertThat(PlatformCustomerDimensionConfigController.class
|
||||
.getDeclaredMethod("update", com.baiyue.common.security.AuthenticatedUser.class,
|
||||
long.class, com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.UpdateRequest.class)
|
||||
.getAnnotation(PreAuthorize.class).value())
|
||||
.isEqualTo("hasAuthority('firstleg:dimension-config:edit')");
|
||||
}
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
package com.baiyue.firstleg.dto;
|
||||
|
||||
import jakarta.validation.Validation;
|
||||
import jakarta.validation.Validator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.Arguments;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
class ConfirmWeightResultRequestValidationTest {
|
||||
private final Validator validator = Validation.buildDefaultValidatorFactory().getValidator();
|
||||
|
||||
@Test
|
||||
void acceptsConfirmationWithoutAdjustment() {
|
||||
var request = new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
0, null, null, null);
|
||||
|
||||
assertThat(validator.validate(request)).isEmpty();
|
||||
assertThat(request.hasAdjustment()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
void acceptsDocumentedWeightBoundaries() {
|
||||
var minimum = new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
0, new BigDecimal("0.001"), "KG", "最小值");
|
||||
var maximum = new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
Integer.MAX_VALUE, new BigDecimal("999999999999999.999"), "LB", "最大值");
|
||||
|
||||
assertThat(validator.validate(minimum)).isEmpty();
|
||||
assertThat(validator.validate(maximum)).isEmpty();
|
||||
assertThat(minimum.hasAdjustment()).isTrue();
|
||||
assertThat(maximum.hasAdjustment()).isTrue();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("invalidRequests")
|
||||
void rejectsInvalidFieldBoundaries(
|
||||
InboundMeasurementRequests.ConfirmWeightResultRequest request,
|
||||
String invalidField) {
|
||||
assertThat(validator.validate(request))
|
||||
.extracting(violation -> violation.getPropertyPath().toString())
|
||||
.contains(invalidField);
|
||||
}
|
||||
|
||||
private static Stream<Arguments> invalidRequests() {
|
||||
return Stream.of(
|
||||
Arguments.of(request(-1, "1.000", "KG", "原因"), "version"),
|
||||
Arguments.of(request(0, "0.000", "KG", "原因"), "adjustedWeightValue"),
|
||||
Arguments.of(request(0, "-1.000", "KG", "原因"), "adjustedWeightValue"),
|
||||
Arguments.of(request(0, "1.0001", "KG", "原因"), "adjustedWeightValue"),
|
||||
Arguments.of(request(0, "1000000000000000.000", "KG", "原因"), "adjustedWeightValue"),
|
||||
Arguments.of(request(0, "1.000", "KILOGRAMS", "原因"), "adjustedWeightUnit"),
|
||||
Arguments.of(request(0, "1.000", "KG", "x".repeat(501)), "adjustmentReason")
|
||||
);
|
||||
}
|
||||
|
||||
private static InboundMeasurementRequests.ConfirmWeightResultRequest request(
|
||||
int version, String value, String unit, String reason) {
|
||||
return new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
version, new BigDecimal(value), unit, reason);
|
||||
}
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package com.baiyue.firstleg.repository;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
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 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);
|
||||
|
||||
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)");
|
||||
}
|
||||
|
||||
@Test
|
||||
void unifiedMigrationIncludesConfigAndFinalAssertion() throws IOException {
|
||||
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"),
|
||||
StandardCharsets.UTF_8);
|
||||
|
||||
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("订单运营角色的客户尺寸计算配置权限不是只读");
|
||||
}
|
||||
|
||||
@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)
|
||||
.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");
|
||||
}
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
package com.baiyue.firstleg.service;
|
||||
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.firstleg.dto.CustomerDimensionConfigDtos.InitializeResponse;
|
||||
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.springframework.boot.test.mock.mockito.MockBean;
|
||||
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
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.eq;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@SpringJUnitConfig(CustomerDimensionConfigService.class)
|
||||
class CustomerDimensionConfigServiceTest {
|
||||
@MockBean
|
||||
private CustomerDimensionConfigRepository repository;
|
||||
@Resource
|
||||
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"));
|
||||
|
||||
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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void differentInitializationKeyDoesNotTakeOwnershipOrOverwrite() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "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());
|
||||
}
|
||||
|
||||
@Test
|
||||
void rejectsStaleUpdateVersion() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 3, "registration-1"));
|
||||
|
||||
assertThatThrownBy(() -> service.update(8L, 100L,
|
||||
new UpdateRequest(new BigDecimal("2.500"), "新配置", 2)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("刷新后重试");
|
||||
verify(repository, never()).update(org.mockito.ArgumentMatchers.any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void compensationUsesTenantKeyDefaultValueAndUnmodifiedVersion() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.000", 0, "registration-1"));
|
||||
|
||||
service.compensateDefault(8L, "registration-1");
|
||||
|
||||
verify(repository).deleteUnchangedDefault(
|
||||
8L, 91L, "registration-1", new BigDecimal("2.000"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void editedConfigIsNotCompensatedByDelayedRollback() {
|
||||
when(repository.find(8L)).thenReturn(config(91L, 8L, "2.500", 1, "registration-1"));
|
||||
|
||||
service.compensateDefault(8L, "registration-1");
|
||||
|
||||
verify(repository).deleteUnchangedDefault(
|
||||
8L, 91L, "registration-1", new BigDecimal("2.000"));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
+184
@@ -13,6 +13,8 @@ import jakarta.annotation.Resource;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
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;
|
||||
@@ -123,6 +125,107 @@ class InboundMeasurementServiceTest {
|
||||
verify(repository).insertRevision(any(InboundWeightRevisionEntity.class));
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@CsvSource({
|
||||
"kg,2.000",
|
||||
"KGS,2.000",
|
||||
"lb,0.907",
|
||||
"LBS,0.907",
|
||||
"' lbs ',0.907"
|
||||
})
|
||||
void confirmingAcceptsSupportedUnitAliases(String unit, String expectedKg) {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundWeightResultEntity weight = weight(20L, "1.000", "KG", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.updateWeight(weight)).thenReturn(true);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
|
||||
InboundMeasurementResponses.WeightResult response = service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
0, new BigDecimal("2.000"), unit, "单位边界测试"));
|
||||
|
||||
assertThat(response.adjustedWeightUnit()).isEqualTo(
|
||||
unit.trim().toUpperCase().startsWith("LB") ? "LB" : "KG");
|
||||
assertThat(response.finalWeightKg()).isEqualByComparingTo(expectedKg);
|
||||
assertThat(response.version()).isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingAcceptsSmallestRepresentableKgWeight() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundWeightResultEntity weight = weight(20L, "0.001", "KG", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.updateWeight(weight)).thenReturn(true);
|
||||
when(repository.listWeights(1L, 10L)).thenReturn(List.of(weight));
|
||||
when(repository.updateInbound(inbound)).thenReturn(true);
|
||||
|
||||
InboundMeasurementResponses.WeightResult response = service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null));
|
||||
|
||||
assertThat(response.finalWeightKg()).isEqualByComparingTo("0.001");
|
||||
assertThat(inbound.getActualGrossWeightKg()).isEqualByComparingTo("0.001");
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingRejectsLbWeightThatRoundsToZeroKgBeforePersisting() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundWeightResultEntity weight = weight(20L, "1.000", "KG", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
0, new BigDecimal("0.001"), "LB", "极小重量边界测试")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("换算后重量不能小于 0.001 KG");
|
||||
|
||||
verify(repository, never()).updateWeight(any());
|
||||
verify(repository, never()).insertRevision(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingRejectsOcrLbWeightThatRoundsToZeroKg() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundWeightResultEntity weight = weight(20L, "0.001", "LB", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("换算后重量不能小于 0.001 KG");
|
||||
|
||||
verify(repository, never()).updateWeight(any());
|
||||
verify(repository, never()).insertRevision(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingRejectsUnsupportedUnitBeforePersisting() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
0, new BigDecimal("286.125"), "G", "错误单位测试")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("重量单位仅支持 KG 或 LB");
|
||||
|
||||
verify(repository, never()).updateWeight(any());
|
||||
verify(repository, never()).insertRevision(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingAdjustedWeightWritesCorrectionAndConfirmationInOneTransaction() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
@@ -173,6 +276,87 @@ class InboundMeasurementServiceTest {
|
||||
verify(repository, never()).insertRevision(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingRejectsEveryPartialAdjustmentCombinationBeforePersisting() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
List<InboundMeasurementRequests.ConfirmWeightResultRequest> partialRequests = List.of(
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, "KG", null),
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(0, null, null, "人工修正"),
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
0, new BigDecimal("286.125"), " ", " ")
|
||||
);
|
||||
|
||||
partialRequests.forEach(request -> assertThatThrownBy(() -> service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员", request))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("有效重量、单位和修改原因"));
|
||||
|
||||
verify(repository, never()).updateWeight(any());
|
||||
verify(repository, never()).insertRevision(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingAnAlreadyConfirmedWeightWithCurrentVersionIsIdempotent() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
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);
|
||||
|
||||
InboundMeasurementResponses.WeightResult response = service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(2, null, null, null));
|
||||
|
||||
assertThat(response.recognitionStatus()).isEqualTo("CONFIRMED");
|
||||
assertThat(response.version()).isEqualTo(2);
|
||||
verify(repository, never()).updateWeight(any());
|
||||
verify(repository, never()).insertRevision(any());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingWithStaleVersionDoesNotTreatRequestAsIdempotent() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
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);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(1, null, null, null)))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("版本已变化");
|
||||
|
||||
verify(repository, never()).updateWeight(any());
|
||||
verify(repository, never()).insertRevision(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void confirmingAdjustedWeightStopsBeforeRecalculationWhenConfirmUpdateConflicts() {
|
||||
InboundOrderEntity inbound = inbound("WAIT_MEASURE");
|
||||
InboundWeightResultEntity weight = weight(20L, "285.500", "KG", "PENDING_CONFIRM", 0);
|
||||
when(repository.findInbound(1L, 10L)).thenReturn(inbound);
|
||||
when(repository.findWeight(1L, 10L, 20L)).thenReturn(weight);
|
||||
when(repository.updateWeight(weight)).thenReturn(true, false);
|
||||
|
||||
assertThatThrownBy(() -> service.confirmWeightResult(
|
||||
1L, 10L, 20L, 9L, "操作员",
|
||||
new InboundMeasurementRequests.ConfirmWeightResultRequest(
|
||||
0, new BigDecimal("286.125"), "KG", "并发冲突测试")))
|
||||
.isInstanceOf(BizException.class)
|
||||
.hasMessageContaining("版本已变化");
|
||||
|
||||
verify(repository, times(2)).updateWeight(weight);
|
||||
verify(repository, times(1)).insertRevision(any(InboundWeightRevisionEntity.class));
|
||||
verify(repository, never()).listWeights(anyLong(), anyLong());
|
||||
verify(repository, never()).updateInbound(any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void submitAdvancesOnlyToMeasurePendingAfterAllWeightsConfirmed() {
|
||||
InboundOrderEntity inbound = inbound("MEASURING");
|
||||
|
||||
Reference in New Issue
Block a user