feat(lastleg): add overseas container fee sources

This commit is contained in:
2026-09-08 21:45:57 +08:00
parent 1cf6722d6e
commit e3a5ea7341
9 changed files with 325 additions and 11 deletions
@@ -4,6 +4,7 @@ import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.common.security.AuthenticatedUser;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ActionRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.AdjustRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ReviewRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.VoidRequest;
@@ -37,8 +38,19 @@ public class PlatformOverseasFeeSourceController {
public ApiResponse<List<OverseasFeeSource>> list(@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam(required = false) String customerId,
@RequestParam(required = false) String businessType,
@RequestParam(required = false) String businessNo) {
requirePlatform(user); return ApiResponse.success(service.list(customerId, businessType, businessNo));
@RequestParam(required = false) String businessNo,
@RequestParam(required = false) Long containerBookingId) {
requirePlatform(user);
return ApiResponse.success(service.list(customerId, businessType, businessNo, containerBookingId));
}
@GetMapping("/container-customers")
@PreAuthorize("hasAuthority('platform:finance:customer-arap:list')")
public ApiResponse<List<ContainerCustomerOption>> listContainerCustomers(
@AuthenticationPrincipal AuthenticatedUser user,
@RequestParam long containerBookingId) {
requirePlatform(user);
return ApiResponse.success(service.listContainerCustomers(containerBookingId));
}
@PostMapping
@@ -15,11 +15,13 @@ import java.util.List;
public final class OverseasFeeSourceDtos {
private OverseasFeeSourceDtos() { }
public record ContainerCustomerOption(String customerId, String customerName) { }
public record SaveRequest(
@NotBlank @Size(max = 128) String customerId,
@Size(max = 200) String customerName,
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND_FEE|OVERSEAS_PROCUREMENT_FEE|OVERSEAS_TRUCK_FEE|OVERSEAS_LOOSEBOARD_FEE|OVERSEAS_WAREHOUSE_FEE") String sourceType,
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND|OVERSEAS_PROCUREMENT|US_TRUCK|LOCAL_LOOSEBOARD|WAREHOUSE_SERVICE") String businessType,
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND_FEE|OVERSEAS_PROCUREMENT_FEE|OVERSEAS_TRUCK_FEE|OVERSEAS_LOOSEBOARD_FEE|OVERSEAS_WAREHOUSE_FEE|OVERSEAS_CONTAINER_FEE") String sourceType,
@NotBlank @Pattern(regexp = "OVERSEAS_INBOUND|OVERSEAS_PROCUREMENT|US_TRUCK|LOCAL_LOOSEBOARD|WAREHOUSE_SERVICE|OVERSEAS_CONTAINER") String businessType,
@Min(1) Long businessId,
@NotBlank @Size(max = 128) String businessNo,
@Min(1) Long orderId,
@@ -1,18 +1,37 @@
package com.baiyue.lastleg.financefee.service;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import java.util.List;
@Component
@RequiredArgsConstructor
public class OverseasFeeBusinessGuard {
private final JdbcTemplate jdbc;
public List<ContainerCustomerOption> listContainerCustomers(long containerBookingId) {
List<ContainerCustomerOption> customers = jdbc.query(
"SELECT customer_id,CASE WHEN COUNT(DISTINCT NULLIF(TRIM(customer_name),''))=1 "
+ "THEN MAX(NULLIF(TRIM(customer_name),'')) ELSE NULL END AS customer_name "
+ "FROM ll_order WHERE source_container_booking_id=? GROUP BY customer_id ORDER BY customer_id",
(result, rowNum) -> new ContainerCustomerOption(
result.getString("customer_id"), result.getString("customer_name")),
String.valueOf(containerBookingId));
if (customers.isEmpty()) throw bad("真实货柜不存在或尚无尾程订单");
return customers;
}
public void requireOwned(SaveRequest fee) {
if ("OVERSEAS_CONTAINER".equals(fee.businessType())) {
requireOwnedContainer(fee);
return;
}
if (fee.businessId() == null) {
if (!"WAREHOUSE_SERVICE".equals(fee.businessType()))
throw bad("业务记录ID不能为空");
@@ -38,6 +57,32 @@ public class OverseasFeeBusinessGuard {
}
}
private void requireOwnedContainer(SaveRequest fee) {
if (fee.businessId() == null || fee.containerBookingId() == null
|| !fee.businessId().equals(fee.containerBookingId()))
throw bad("海外货柜费用必须以真实来源柜ID同时关联业务和订舱柜");
if (blank(fee.containerNo()) || !fee.businessNo().trim().equals(fee.containerNo().trim()))
throw bad("海外货柜费用的业务编号必须与真实集装箱号一致");
String bookingId = String.valueOf(fee.containerBookingId());
Integer total = jdbc.queryForObject(
"SELECT COUNT(*) FROM ll_order WHERE source_container_booking_id=?",
Integer.class, bookingId);
if (total == null || total < 1) throw bad("费用关联的真实货柜不存在");
Integer inconsistent = jdbc.queryForObject(
"SELECT COUNT(*) FROM ll_order WHERE source_container_booking_id=? "
+ "AND (container_no IS NULL OR TRIM(container_no)='' OR BINARY TRIM(container_no)<>BINARY ?)",
Integer.class, bookingId, fee.containerNo().trim());
if (inconsistent == null || inconsistent > 0)
throw bad("真实货柜的集装箱号缺失或不一致");
Integer owned = jdbc.queryForObject(
"SELECT COUNT(*) FROM ll_order WHERE source_container_booking_id=? AND customer_id=?",
Integer.class, bookingId, fee.customerId().trim());
if (owned == null || owned < 1) throw bad("该客户不属于费用关联的真实货柜");
}
private static boolean blank(String value) { return value == null || value.isBlank(); }
private static ResponseStatusException bad(String message) {
return new ResponseStatusException(HttpStatus.BAD_REQUEST, message);
@@ -3,6 +3,7 @@ package com.baiyue.lastleg.financefee.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.AdjustRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ContainerCustomerOption;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.ReviewRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.SaveRequest;
import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos.VoidRequest;
@@ -26,26 +27,36 @@ import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HexFormat;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@Service
@RequiredArgsConstructor
public class OverseasFeeSourceService {
static final Set<String> OVERSEAS_CONTAINER_FEE_ITEMS = Set.of(
"PORT_FEE", "STORAGE_FEE", "CONTAINER_REPAIR_FEE", "CRANE_FEE", "TERMINAL_DRAYAGE_FEE", "OTHER");
private final OverseasFeeSourceMapper mapper;
private final OverseasFeeCommandMapper commands;
private final OverseasFeeBusinessGuard businessGuard;
private final OverseasFeeOutboxService outbox;
private final ObjectMapper objectMapper;
public List<OverseasFeeSource> list(String customerId, String businessType, String businessNo) {
public List<OverseasFeeSource> list(String customerId, String businessType, String businessNo,
Long containerBookingId) {
return mapper.selectList(new LambdaQueryWrapper<OverseasFeeSource>()
.eq(text(customerId) != null, OverseasFeeSource::getCustomerId, text(customerId))
.eq(text(businessType) != null, OverseasFeeSource::getBusinessType, text(businessType))
.like(text(businessNo) != null, OverseasFeeSource::getBusinessNo, text(businessNo))
.eq(containerBookingId != null, OverseasFeeSource::getContainerBookingId, containerBookingId)
.inSql(OverseasFeeSource::getId, "SELECT MAX(v.id) FROM ll_finance_fee_source v GROUP BY v.source_fee_id")
.orderByDesc(OverseasFeeSource::getCreatedAt).orderByDesc(OverseasFeeSource::getId));
}
public List<ContainerCustomerOption> listContainerCustomers(long containerBookingId) {
return businessGuard.listContainerCustomers(containerBookingId);
}
@Transactional
public OverseasFeeSource create(String operatorId, String key, SaveRequest request) {
OverseasFeeCommand replay = replay(operatorId, "CREATE", key, request);
@@ -99,6 +110,13 @@ public class OverseasFeeSourceService {
validate(request.fee()); businessGuard.requireOwned(request.fee());
if (!old.getCustomerId().equals(request.fee().customerId().trim()))
throw conflict("调整不得改变费用所属客户");
if (!old.getSourceType().equals(request.fee().sourceType())
|| !old.getBusinessType().equals(request.fee().businessType())
|| !Objects.equals(old.getBusinessId(), request.fee().businessId())
|| !old.getBusinessNo().equals(request.fee().businessNo().trim())
|| !Objects.equals(old.getContainerBookingId(), request.fee().containerBookingId())
|| !Objects.equals(old.getContainerNo(), text(request.fee().containerNo())))
throw conflict("调整不得改变费用来源或业务归属");
old.setSourceStatus("SUPERSEDED"); old.setUpdatedBy(operatorId); old.setUpdatedAt(now()); mapper.updateById(old);
OverseasFeeSource next = build(IdWorker.getId(), sourceFeeId, old.getSourceVersion() + 1, operatorId, request.fee());
next.setReviewStatus("PENDING");
@@ -147,9 +165,13 @@ public class OverseasFeeSourceService {
case "US_TRUCK" -> "OVERSEAS_TRUCK_FEE";
case "LOCAL_LOOSEBOARD" -> "OVERSEAS_LOOSEBOARD_FEE";
case "WAREHOUSE_SERVICE" -> "OVERSEAS_WAREHOUSE_FEE";
case "OVERSEAS_CONTAINER" -> "OVERSEAS_CONTAINER_FEE";
default -> "";
};
if (!expected.equals(value.sourceType())) throw bad("来源类型与业务类型不匹配");
if ("OVERSEAS_CONTAINER".equals(value.businessType())
&& !OVERSEAS_CONTAINER_FEE_ITEMS.contains(value.feeItemCode().trim()))
throw bad("海外货柜费用项目不受支持");
boolean payable = "PAYABLE".equals(value.direction());
boolean files = value.paymentVoucherFileIds() != null && !value.paymentVoucherFileIds().isEmpty();
if (!payable) {