新增海外仓阶梯费用

This commit is contained in:
2026-08-05 11:20:28 +08:00
parent 9678ee0f1f
commit cac377de96
20 changed files with 801 additions and 10 deletions
@@ -0,0 +1,93 @@
package com.baiyue.warehouse.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
import com.baiyue.warehouse.service.OverseasStorageFeeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.Min;
import org.springdoc.core.annotations.ParameterObject;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
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;
/** 平台海外仓阶梯费用管理接口。 */
@Validated
@RestController
@RequestMapping("/api/platform/v1/overseas-storage-fees")
@Tag(name = "海外仓阶梯费用", description = "按客户维护海外仓仓储费阶梯")
public class PlatformOverseasStorageFeeController {
@Resource
private OverseasStorageFeeService service;
@GetMapping
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:list')")
@Operation(summary = "分页查询海外仓阶梯费用")
public ApiResponse<PageResult<OverseasStorageFeeDtos.View>> page(
@AuthenticationPrincipal AuthenticatedUser user,
@Valid @ParameterObject OverseasStorageFeeDtos.PageQuery query) {
requirePlatform(user);
return ApiResponse.success(service.page(query));
}
@GetMapping("/{id}")
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:list')")
@Operation(summary = "查询海外仓阶梯费用详情")
public ApiResponse<OverseasStorageFeeDtos.View> detail(
@AuthenticationPrincipal AuthenticatedUser user, @PathVariable @Min(1) long id) {
requirePlatform(user);
return ApiResponse.success(service.detail(id));
}
@PostMapping
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:create')")
@Operation(summary = "新增海外仓阶梯费用")
public ApiResponse<Long> create(@AuthenticationPrincipal AuthenticatedUser user,
@Valid @RequestBody OverseasStorageFeeDtos.CreateRequest request) {
requirePlatform(user);
return ApiResponse.success(service.create(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:edit')")
@Operation(summary = "编辑海外仓阶梯费用")
public ApiResponse<Void> update(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Min(1) long id,
@Valid @RequestBody OverseasStorageFeeDtos.UpdateRequest request) {
requirePlatform(user);
service.update(id, request);
return ApiResponse.success(null);
}
@DeleteMapping("/{id}")
@PreAuthorize("hasAuthority('platform:overseas-storage-fee:delete')")
@Operation(summary = "删除海外仓阶梯费用")
public ApiResponse<Void> delete(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable @Min(1) long id,
@RequestParam @Min(0) int version) {
requirePlatform(user);
service.delete(id, version);
return ApiResponse.success(null);
}
private static void requirePlatform(AuthenticatedUser user) {
if (user == null || !"PLATFORM".equals(user.appCode())) {
throw new BizException(ResultCode.FORBIDDEN, "仅允许平台端访问");
}
}
}
@@ -0,0 +1,86 @@
package com.baiyue.warehouse.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Digits;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
/** 海外仓阶梯费用接口契约。 */
public final class OverseasStorageFeeDtos {
private OverseasStorageFeeDtos() {
}
@Schema(name = "OverseasStorageFeePageQuery", description = "海外仓阶梯费用分页条件")
public record PageQuery(
@Min(1) long page,
@Min(1) @Max(200) long size,
@Schema(description = "客户ID,不传时查询全部客户") @Positive Long customerId,
@Schema(description = "客户公司名称关键字") @Size(max = 128) String customerName
) {
public PageQuery {
if (page == 0) {
page = 1;
}
if (size == 0) {
size = 20;
}
}
}
@Schema(name = "OverseasStorageFeeTierRequest", description = "仓储费阶梯")
public record TierRequest(
@NotNull @Min(1) @Max(36500) Integer startDay,
@NotNull @Min(1) @Max(36500) Integer endDay,
@NotNull @DecimalMin(value = "0", inclusive = false) @Digits(integer = 12, fraction = 6)
BigDecimal unitPrice
) {
}
@Schema(name = "OverseasStorageFeeCreateRequest", description = "新增海外仓阶梯费用")
public record CreateRequest(
@NotNull @Positive Long customerId,
@NotBlank @Size(max = 128) String customerName,
@NotBlank @Pattern(regexp = "^[A-Z]{3}$") String currency,
@NotEmpty @Size(max = 100) List<@Valid TierRequest> tiers
) {
}
@Schema(name = "OverseasStorageFeeUpdateRequest", description = "编辑海外仓阶梯费用")
public record UpdateRequest(
@NotNull @Positive Long customerId,
@NotBlank @Size(max = 128) String customerName,
@NotBlank @Pattern(regexp = "^[A-Z]{3}$") String currency,
@NotEmpty @Size(max = 100) List<@Valid TierRequest> tiers,
@NotNull @Min(0) Integer version
) {
}
@Schema(name = "OverseasStorageFeeTierView", description = "仓储费阶梯明细")
public record TierView(Long id, Integer startDay, Integer endDay, BigDecimal unitPrice, Integer sortNo) {
}
@Schema(name = "OverseasStorageFeeView", description = "海外仓阶梯费用详情")
public record View(
Long id,
Long customerId,
String customerName,
String currency,
List<TierView> tiers,
Integer version,
LocalDateTime createdAt,
LocalDateTime updatedAt
) {
}
}
@@ -0,0 +1,20 @@
package com.baiyue.warehouse.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;
/** 客户维度的海外仓阶梯仓储费主记录。 */
@Getter
@Setter
@TableName("wh_overseas_storage_fee_rule")
public class OverseasStorageFeeRuleEntity extends BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private String customerName;
private String currency;
}
@@ -0,0 +1,25 @@
package com.baiyue.warehouse.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("wh_overseas_storage_fee_tier")
public class OverseasStorageFeeTierEntity extends BaseEntity {
@TableId(type = IdType.ASSIGN_ID)
private Long id;
private Long tenantId;
private Long ruleId;
private Integer startDay;
private Integer endDay;
private BigDecimal unitPrice;
private Integer sortNo;
}
@@ -0,0 +1,8 @@
package com.baiyue.warehouse.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
/** 海外仓阶梯费用主记录 Mapper。 */
public interface OverseasStorageFeeRuleMapper extends BaseMapper<OverseasStorageFeeRuleEntity> {
}
@@ -0,0 +1,8 @@
package com.baiyue.warehouse.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
/** 海外仓阶梯费用明细 Mapper。 */
public interface OverseasStorageFeeTierMapper extends BaseMapper<OverseasStorageFeeTierEntity> {
}
@@ -0,0 +1,83 @@
package com.baiyue.warehouse.repository;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
import com.baiyue.warehouse.mapper.OverseasStorageFeeRuleMapper;
import com.baiyue.warehouse.mapper.OverseasStorageFeeTierMapper;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.List;
/** 海外仓阶梯费用持久化仓储。 */
@Repository
public class OverseasStorageFeeRepository {
@Resource
private OverseasStorageFeeRuleMapper ruleMapper;
@Resource
private OverseasStorageFeeTierMapper tierMapper;
public PageResult<OverseasStorageFeeRuleEntity> page(OverseasStorageFeeDtos.PageQuery query) {
LambdaQueryWrapper<OverseasStorageFeeRuleEntity> wrapper =
new LambdaQueryWrapper<OverseasStorageFeeRuleEntity>()
.eq(query.customerId() != null, OverseasStorageFeeRuleEntity::getTenantId, query.customerId())
.like(StringUtils.hasText(query.customerName()),
OverseasStorageFeeRuleEntity::getCustomerName, query.customerName())
.orderByDesc(OverseasStorageFeeRuleEntity::getUpdatedAt)
.orderByDesc(OverseasStorageFeeRuleEntity::getId);
return PageResult.from(ruleMapper.selectPage(new Page<>(query.page(), query.size()), wrapper));
}
public OverseasStorageFeeRuleEntity find(long id) {
return ruleMapper.selectById(id);
}
public boolean existsCustomer(long customerId, Long excludedId) {
return ruleMapper.selectCount(new LambdaQueryWrapper<OverseasStorageFeeRuleEntity>()
.eq(OverseasStorageFeeRuleEntity::getTenantId, customerId)
.ne(excludedId != null, OverseasStorageFeeRuleEntity::getId, excludedId)) > 0;
}
public List<OverseasStorageFeeTierEntity> findTiers(Collection<Long> ruleIds) {
if (ruleIds == null || ruleIds.isEmpty()) {
return List.of();
}
return tierMapper.selectList(new LambdaQueryWrapper<OverseasStorageFeeTierEntity>()
.in(OverseasStorageFeeTierEntity::getRuleId, ruleIds)
.orderByAsc(OverseasStorageFeeTierEntity::getRuleId)
.orderByAsc(OverseasStorageFeeTierEntity::getSortNo)
.orderByAsc(OverseasStorageFeeTierEntity::getId));
}
public void insertRule(OverseasStorageFeeRuleEntity entity) {
ruleMapper.insert(entity);
}
public int updateRule(OverseasStorageFeeRuleEntity entity) {
return ruleMapper.updateById(entity);
}
public void insertTiers(List<OverseasStorageFeeTierEntity> tiers) {
tiers.forEach(tierMapper::insert);
}
public void deleteTiers(long ruleId) {
tierMapper.delete(new LambdaQueryWrapper<OverseasStorageFeeTierEntity>()
.eq(OverseasStorageFeeTierEntity::getRuleId, ruleId));
}
public int deleteRule(long id, int version) {
return ruleMapper.update(null, new LambdaUpdateWrapper<OverseasStorageFeeRuleEntity>()
.eq(OverseasStorageFeeRuleEntity::getId, id)
.eq(OverseasStorageFeeRuleEntity::getVersion, version)
.set(OverseasStorageFeeRuleEntity::getDeleted, true)
.set(OverseasStorageFeeRuleEntity::getVersion, version + 1));
}
}
@@ -0,0 +1,161 @@
package com.baiyue.warehouse.service;
import com.baiyue.common.audit.BusinessAudit;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.api.ResultCode;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
import com.baiyue.warehouse.repository.OverseasStorageFeeRepository;
import jakarta.annotation.Resource;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/** 海外仓阶梯费用管理服务。 */
@Service
public class OverseasStorageFeeService {
@Resource
private OverseasStorageFeeRepository repository;
public PageResult<OverseasStorageFeeDtos.View> page(OverseasStorageFeeDtos.PageQuery query) {
PageResult<OverseasStorageFeeRuleEntity> source = repository.page(query);
Map<Long, List<OverseasStorageFeeTierEntity>> tiers = repository.findTiers(
source.records().stream().map(OverseasStorageFeeRuleEntity::getId).toList())
.stream().collect(Collectors.groupingBy(OverseasStorageFeeTierEntity::getRuleId));
return new PageResult<>(source.records().stream()
.map(rule -> view(rule, tiers.getOrDefault(rule.getId(), List.of())))
.toList(), source.page(), source.size(), source.total(), source.pages());
}
public OverseasStorageFeeDtos.View detail(long id) {
OverseasStorageFeeRuleEntity rule = required(id);
return view(rule, repository.findTiers(List.of(id)));
}
@Transactional
@BusinessAudit(event = "OVERSEAS_STORAGE_FEE_CREATE", resourceType = "OVERSEAS_STORAGE_FEE",
description = "新增海外仓阶梯费用", captureBefore = false)
public Long create(OverseasStorageFeeDtos.CreateRequest request) {
ensureCustomerUnique(request.customerId(), null);
List<OverseasStorageFeeDtos.TierRequest> tiers = normalize(request.tiers());
OverseasStorageFeeRuleEntity rule = new OverseasStorageFeeRuleEntity();
copy(rule, request.customerId(), request.customerName(), request.currency());
try {
repository.insertRule(rule);
repository.insertTiers(tierEntities(rule.getId(), request.customerId(), tiers));
} catch (DataIntegrityViolationException exception) {
throw new BizException(ResultCode.CONFLICT, "该客户已配置海外仓阶梯费用或阶梯区间重复");
}
return rule.getId();
}
@Transactional
@BusinessAudit(event = "OVERSEAS_STORAGE_FEE_UPDATE", resourceType = "OVERSEAS_STORAGE_FEE",
resourceId = "#id", description = "编辑海外仓阶梯费用")
public void update(long id, OverseasStorageFeeDtos.UpdateRequest request) {
OverseasStorageFeeRuleEntity rule = required(id);
if (!rule.getVersion().equals(request.version())) {
throw new BizException(ResultCode.CONFLICT, "费用配置已被修改,请刷新后重试");
}
ensureCustomerUnique(request.customerId(), id);
List<OverseasStorageFeeDtos.TierRequest> tiers = normalize(request.tiers());
copy(rule, request.customerId(), request.customerName(), request.currency());
try {
if (repository.updateRule(rule) != 1) {
throw new BizException(ResultCode.CONFLICT, "费用配置已被并发修改,请刷新后重试");
}
repository.deleteTiers(id);
repository.insertTiers(tierEntities(id, request.customerId(), tiers));
} catch (DataIntegrityViolationException exception) {
throw new BizException(ResultCode.CONFLICT, "该客户已配置海外仓阶梯费用或阶梯区间重复");
}
}
@Transactional
@BusinessAudit(event = "OVERSEAS_STORAGE_FEE_DELETE", resourceType = "OVERSEAS_STORAGE_FEE",
resourceId = "#id", description = "删除海外仓阶梯费用")
public void delete(long id, int version) {
required(id);
repository.deleteTiers(id);
if (repository.deleteRule(id, version) != 1) {
throw new BizException(ResultCode.CONFLICT, "费用配置已被并发修改,请刷新后重试");
}
}
private OverseasStorageFeeRuleEntity required(long id) {
OverseasStorageFeeRuleEntity rule = repository.find(id);
if (rule == null) {
throw new BizException(ResultCode.NOT_FOUND, "海外仓阶梯费用不存在");
}
return rule;
}
private void ensureCustomerUnique(long customerId, Long excludedId) {
if (repository.existsCustomer(customerId, excludedId)) {
throw new BizException(ResultCode.CONFLICT, "该客户已配置海外仓阶梯费用");
}
}
private static List<OverseasStorageFeeDtos.TierRequest> normalize(
List<OverseasStorageFeeDtos.TierRequest> source) {
if (source == null || source.isEmpty() || source.size() > 100) {
throw new BizException(ResultCode.VALIDATION_FAILED, "阶梯费用明细必须为1至100组");
}
if (source.stream().anyMatch(tier -> tier == null || tier.startDay() == null
|| tier.endDay() == null || tier.unitPrice() == null
|| tier.unitPrice().signum() <= 0)) {
throw new BizException(ResultCode.VALIDATION_FAILED, "阶梯天数和仓储费标准必须为有效正数");
}
List<OverseasStorageFeeDtos.TierRequest> tiers = new ArrayList<>(source);
tiers.sort(Comparator.comparingInt(OverseasStorageFeeDtos.TierRequest::startDay));
int expectedStart = 1;
for (OverseasStorageFeeDtos.TierRequest tier : tiers) {
if (tier.startDay() != expectedStart || tier.endDay() < tier.startDay()) {
throw new BizException(ResultCode.VALIDATION_FAILED,
"阶梯必须从第1天开始,且区间连续、不能重叠或倒置");
}
expectedStart = tier.endDay() + 1;
}
return List.copyOf(tiers);
}
private static void copy(OverseasStorageFeeRuleEntity rule, long customerId,
String customerName, String currency) {
rule.setTenantId(customerId);
rule.setCustomerName(customerName.trim());
rule.setCurrency(currency);
}
private static List<OverseasStorageFeeTierEntity> tierEntities(
long ruleId, long customerId, List<OverseasStorageFeeDtos.TierRequest> source) {
List<OverseasStorageFeeTierEntity> result = new ArrayList<>(source.size());
for (int index = 0; index < source.size(); index++) {
OverseasStorageFeeDtos.TierRequest request = source.get(index);
OverseasStorageFeeTierEntity tier = new OverseasStorageFeeTierEntity();
tier.setTenantId(customerId);
tier.setRuleId(ruleId);
tier.setStartDay(request.startDay());
tier.setEndDay(request.endDay());
tier.setUnitPrice(request.unitPrice());
tier.setSortNo(index + 1);
result.add(tier);
}
return result;
}
private static OverseasStorageFeeDtos.View view(OverseasStorageFeeRuleEntity rule,
List<OverseasStorageFeeTierEntity> tiers) {
return new OverseasStorageFeeDtos.View(rule.getId(), rule.getTenantId(), rule.getCustomerName(),
rule.getCurrency(), tiers.stream().map(tier -> new OverseasStorageFeeDtos.TierView(
tier.getId(), tier.getStartDay(), tier.getEndDay(), tier.getUnitPrice(), tier.getSortNo()))
.toList(), rule.getVersion(), rule.getCreatedAt(), rule.getUpdatedAt());
}
}
@@ -22,7 +22,9 @@ public class WarehouseSchemaReadinessService {
"SELECT id, tenant_id, company_code, company_name, status, deleted FROM wh_express_company LIMIT 0",
"SELECT id, tenant_id, warehouse_id, zone_code, zone_name, location_code, location_type, max_weight_kg, shared_overflow, status, deleted, version FROM wh_location LIMIT 0",
"SELECT id, tenant_id, warehouse_id, pallet_id, location_id, shared_overflow, reservation_status, expires_at, version FROM wh_location_reservation LIMIT 0",
"SELECT id, tenant_id, warehouse_id, pallet_id, location_id, shared_overflow, occupancy_status, active_location_occupancy, active_pallet_occupancy, version FROM wh_location_occupancy LIMIT 0"
"SELECT id, tenant_id, warehouse_id, pallet_id, location_id, shared_overflow, occupancy_status, active_location_occupancy, active_pallet_occupancy, version FROM wh_location_occupancy LIMIT 0",
"SELECT id, tenant_id, customer_name, currency, version FROM wh_overseas_storage_fee_rule LIMIT 0",
"SELECT id, tenant_id, rule_id, start_day, end_day, unit_price, sort_no, version FROM wh_overseas_storage_fee_tier LIMIT 0"
);
@Resource
@@ -0,0 +1,36 @@
package com.baiyue.warehouse.controller;
import org.junit.jupiter.api.Test;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import static org.assertj.core.api.Assertions.assertThat;
class PlatformOverseasStorageFeeControllerContractTest {
@Test
void exposesCrud() {
assertThat(PlatformOverseasStorageFeeController.class.getAnnotation(RequestMapping.class).value())
.containsExactly("/api/platform/v1/overseas-storage-fees");
assertMethod("page", GetMapping.class, "platform:overseas-storage-fee:list");
assertMethod("detail", GetMapping.class, "platform:overseas-storage-fee:list");
assertMethod("create", PostMapping.class, "platform:overseas-storage-fee:create");
assertMethod("update", PutMapping.class, "platform:overseas-storage-fee:edit");
assertMethod("delete", DeleteMapping.class, "platform:overseas-storage-fee:delete");
}
private static void assertMethod(String name, Class<? extends Annotation> mapping, String authority) {
Method method = Arrays.stream(PlatformOverseasStorageFeeController.class.getDeclaredMethods())
.filter(candidate -> candidate.getName().equals(name)).findFirst().orElseThrow();
assertThat(method.getAnnotation(mapping)).isNotNull();
assertThat(method.getAnnotation(PreAuthorize.class).value())
.isEqualTo("hasAuthority('" + authority + "')");
}
}
@@ -0,0 +1,31 @@
package com.baiyue.warehouse.repository;
import org.junit.jupiter.api.Test;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import static org.assertj.core.api.Assertions.assertThat;
class OverseasStorageFeeSqlContractTest {
@Test
void containsSchemaAndPermissions() throws IOException {
String sql = Files.readString(Path.of("..", "deploy", "sql", "头程数据库统一增量-MySQL8.sql"));
assertThat(sql)
.contains("CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_rule")
.contains("CREATE TABLE IF NOT EXISTS wh_overseas_storage_fee_tier")
.contains("uk_overseas_fee_active_customer")
.contains("idx_overseas_fee_page")
.contains("idx_overseas_fee_customer_name")
.contains("uk_overseas_fee_tier_start")
.contains("idx_overseas_fee_tier_rule")
.contains("platform:overseas-storage-fee:list")
.contains("platform:overseas-storage-fee:create")
.contains("platform:overseas-storage-fee:edit")
.contains("platform:overseas-storage-fee:delete")
.contains("CALL unified_assert_overseas_storage_fee()")
.contains("海外仓阶梯费用表、索引和权限校验通过");
}
}
@@ -0,0 +1,110 @@
package com.baiyue.warehouse.service;
import com.baiyue.common.core.api.PageResult;
import com.baiyue.common.core.exception.BizException;
import com.baiyue.warehouse.dto.OverseasStorageFeeDtos;
import com.baiyue.warehouse.entity.OverseasStorageFeeRuleEntity;
import com.baiyue.warehouse.entity.OverseasStorageFeeTierEntity;
import com.baiyue.warehouse.repository.OverseasStorageFeeRepository;
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 java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
@SpringJUnitConfig(OverseasStorageFeeService.class)
class OverseasStorageFeeServiceTest {
@MockBean
private OverseasStorageFeeRepository repository;
@Resource
private OverseasStorageFeeService service;
@Test
void createsRule() {
doAnswer(invocation -> {
OverseasStorageFeeRuleEntity rule = invocation.getArgument(0);
rule.setId(10L);
return null;
}).when(repository).insertRule(any());
Long id = service.create(new OverseasStorageFeeDtos.CreateRequest(
10001L, "示例客户", "USD", List.of(tier(8, 30, "0.8"), tier(1, 7, "0.5"))));
assertThat(id).isEqualTo(10L);
verify(repository).insertTiers(any());
}
@Test
void rejectsTierGap() {
OverseasStorageFeeDtos.CreateRequest request = new OverseasStorageFeeDtos.CreateRequest(
10001L, "示例客户", "USD", List.of(tier(1, 7, "0.5"), tier(9, 30, "0.8")));
assertThatThrownBy(() -> service.create(request)).isInstanceOf(BizException.class);
verify(repository, never()).insertRule(any());
}
@Test
void rejectsDuplicateCustomer() {
when(repository.existsCustomer(10001L, null)).thenReturn(true);
OverseasStorageFeeDtos.CreateRequest request = new OverseasStorageFeeDtos.CreateRequest(
10001L, "示例客户", "USD", List.of(tier(1, 30, "0.8")));
assertThatThrownBy(() -> service.create(request)).isInstanceOf(BizException.class);
}
@Test
void pagesWithTiers() {
OverseasStorageFeeRuleEntity rule = rule();
OverseasStorageFeeTierEntity tier = new OverseasStorageFeeTierEntity();
tier.setId(20L);
tier.setRuleId(10L);
tier.setStartDay(1);
tier.setEndDay(30);
tier.setUnitPrice(new BigDecimal("0.8"));
tier.setSortNo(1);
OverseasStorageFeeDtos.PageQuery query = new OverseasStorageFeeDtos.PageQuery(1, 20, null, null);
when(repository.page(query)).thenReturn(new PageResult<>(List.of(rule), 1, 20, 1, 1));
when(repository.findTiers(List.of(10L))).thenReturn(List.of(tier));
OverseasStorageFeeDtos.View result = service.page(query).records().get(0);
assertThat(result.customerId()).isEqualTo(10001L);
assertThat(result.tiers()).singleElement().extracting(OverseasStorageFeeDtos.TierView::unitPrice)
.isEqualTo(new BigDecimal("0.8"));
}
@Test
void detectsStaleUpdate() {
OverseasStorageFeeRuleEntity rule = rule();
when(repository.find(10L)).thenReturn(rule);
OverseasStorageFeeDtos.UpdateRequest request = new OverseasStorageFeeDtos.UpdateRequest(
10001L, "示例客户", "USD", List.of(tier(1, 30, "0.8")), 2);
assertThatThrownBy(() -> service.update(10L, request)).isInstanceOf(BizException.class);
verify(repository, never()).updateRule(any());
}
private static OverseasStorageFeeDtos.TierRequest tier(int start, int end, String price) {
return new OverseasStorageFeeDtos.TierRequest(start, end, new BigDecimal(price));
}
private static OverseasStorageFeeRuleEntity rule() {
OverseasStorageFeeRuleEntity rule = new OverseasStorageFeeRuleEntity();
rule.setId(10L);
rule.setTenantId(10001L);
rule.setCustomerName("示例客户");
rule.setCurrency("USD");
rule.setVersion(1);
return rule;
}
}