fix(finance): key container finance by booking

This commit is contained in:
2026-09-08 21:27:23 +08:00
parent 55c36cf290
commit 1cf6722d6e
13 changed files with 99 additions and 50 deletions
@@ -27,7 +27,7 @@ public class PlatformFinanceContainerSettlementController {
+ "hasAuthority('platform:finance:settlement-ap:list')")
@Operation(summary = "按客户和柜次批量查询已结算柜费及成本分摊")
public ApiResponse<ContainerSettlementDetails> details(@RequestParam long customerId,
@RequestParam long containerId) {
return FinanceResponses.success(service.details(customerId, containerId));
@RequestParam long containerBookingId) {
return FinanceResponses.success(service.details(customerId, containerBookingId));
}
}
@@ -164,8 +164,8 @@ public class PlatformFinanceController {
@BusinessAudit(event = "FINANCE_COST_CREATE", resourceType = "FINANCE_COST")
public ApiResponse<?> cost(@AuthenticationPrincipal AuthenticatedUser user, @RequestHeader("Idempotency-Key") String key, @Valid @RequestBody CostRequest request) {
if (request.currency() != null && !"CNY".equals(request.currency())) throw new IllegalArgumentException("首期仅支持CNY");
var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null, request.containerId(), null, null, "MANUAL", request.expenseName(), request.amount(), request.voucherFileIds(), request.remark());
java.util.List<FinanceService.ContainerOrderVolume> volumes = java.util.List.of();
Long containerBookingId = null;
if ("CONTAINER_SHARED".equals(request.costType())) {
if (request.containerId() == null) throw new IllegalArgumentException("整柜成本必须指定柜次");
var response = costSnapshotClient.loadingPlan(fileToken, request.customerId(), request.containerId());
@@ -180,9 +180,13 @@ public class PlatformFinanceController {
|| snapshot.orders() == null || snapshot.orders().isEmpty()) {
throw new IllegalArgumentException("Firstleg未返回有效柜内订单体积快照");
}
containerBookingId = snapshot.containerBookingId();
volumes = snapshot.orders().stream().map(v -> new FinanceService.ContainerOrderVolume(v.orderId(),
v.inboundId(), snapshot.loadingPlanId(), snapshot.customerId(), v.volumeCbm())).toList();
v.inboundId(), snapshot.containerBookingId(), snapshot.customerId(), v.volumeCbm())).toList();
}
var internal = new FinanceService.CostRequest(null, request.costType(), request.orderId(), null,
containerBookingId, null, null, "MANUAL", request.expenseName(), request.amount(),
request.voucherFileIds(), request.remark());
return FinanceResponses.success(resourceView(service.createCost(request.customerId(), user.userId(), internal, volumes, key)));
}
@@ -9,7 +9,7 @@ public final class FinanceContainerSettlementDtos {
public record ContainerSettlementDetails(
long customerId,
long containerId,
long containerBookingId,
List<Long> settledSourceFeeIds,
List<ContainerCostDetail> costs) {}
@@ -20,7 +20,7 @@ public final class FinanceContainerSettlementDtos {
String costOrigin,
Long sourceFeeId,
Integer sourceFeeVersion,
long containerId,
long containerBookingId,
String supplierName,
String feeItemName,
BigDecimal amount,
@@ -13,14 +13,15 @@ public interface FinanceContainerSettlementQueryMapper {
@Select("""
SELECT COUNT(*) AS customerLinkCount
FROM (
SELECT customer_id, container_id FROM fin_fee_source
WHERE customer_id = #{customerId} AND container_id = #{containerId}
SELECT customer_id, container_booking_id AS container_id FROM fin_fee_source
WHERE customer_id = #{customerId} AND container_booking_id = #{containerBookingId}
UNION ALL
SELECT tenant_id AS customer_id, container_id FROM fin_cost
WHERE tenant_id = #{customerId} AND container_id = #{containerId}
WHERE tenant_id = #{customerId} AND container_id = #{containerBookingId}
) links
""")
OwnershipRow ownership(@Param("customerId") long customerId, @Param("containerId") long containerId);
OwnershipRow ownership(@Param("customerId") long customerId,
@Param("containerBookingId") long containerBookingId);
@Select("""
SELECT DISTINCT settled.source_fee_id
@@ -32,14 +33,14 @@ public interface FinanceContainerSettlementQueryMapper {
WHERE s.tenant_id = #{customerId}
AND s.status = 'PAID'
AND f.customer_id = #{customerId}
AND f.container_id = #{containerId}
AND f.container_booking_id = #{containerBookingId}
AND f.source_system = 'FIRSTLEG'
AND f.source_type = 'CONTAINER_FEE'
UNION ALL
SELECT c.source_fee_id
FROM fin_cost c
WHERE c.tenant_id = #{customerId}
AND c.container_id = #{containerId}
AND c.container_id = #{containerBookingId}
AND c.cost_origin = 'FIRSTLEG_DIRECT_PAID'
AND c.source_system = 'FIRSTLEG'
AND c.source_type = 'CONTAINER_FEE'
@@ -49,12 +50,12 @@ public interface FinanceContainerSettlementQueryMapper {
ORDER BY settled.source_fee_id
""")
List<Long> settledSourceFeeIds(@Param("customerId") long customerId,
@Param("containerId") long containerId);
@Param("containerBookingId") long containerBookingId);
@Select("""
SELECT c.id AS costId, c.cost_no AS costNo, c.cost_type AS costType, c.cost_origin AS costOrigin,
c.source_fee_id AS sourceFeeId, c.source_fee_version AS sourceFeeVersion,
c.container_id AS containerId, c.supplier_name_snapshot AS supplierName,
c.container_id AS containerBookingId, c.supplier_name_snapshot AS supplierName,
c.fee_item_name AS feeItemName, c.cost_amount AS amount, c.currency_code AS currency,
c.status,
a.id AS allocationId, a.order_id AS orderId, a.inbound_id AS inboundId,
@@ -63,11 +64,11 @@ public interface FinanceContainerSettlementQueryMapper {
a.rounding_adjustment AS roundingAdjustment
FROM fin_cost c
LEFT JOIN fin_cost_allocation a ON a.tenant_id = c.tenant_id AND a.cost_id = c.id
WHERE c.tenant_id = #{customerId} AND c.container_id = #{containerId}
WHERE c.tenant_id = #{customerId} AND c.container_id = #{containerBookingId}
ORDER BY c.created_at DESC, c.id DESC, a.id
""")
List<CostAllocationRow> costs(@Param("customerId") long customerId,
@Param("containerId") long containerId);
@Param("containerBookingId") long containerBookingId);
final class OwnershipRow {
private long customerLinkCount;
@@ -83,7 +84,7 @@ public interface FinanceContainerSettlementQueryMapper {
private String costOrigin;
private Long sourceFeeId;
private Integer sourceFeeVersion;
private long containerId;
private long containerBookingId;
private String supplierName;
private String feeItemName;
private BigDecimal amount;
@@ -110,8 +111,8 @@ public interface FinanceContainerSettlementQueryMapper {
public void setSourceFeeId(Long value) { this.sourceFeeId = value; }
public Integer getSourceFeeVersion() { return sourceFeeVersion; }
public void setSourceFeeVersion(Integer value) { this.sourceFeeVersion = value; }
public long getContainerId() { return containerId; }
public void setContainerId(long value) { this.containerId = value; }
public long getContainerBookingId() { return containerBookingId; }
public void setContainerBookingId(long value) { this.containerBookingId = value; }
public String getSupplierName() { return supplierName; }
public void setSupplierName(String value) { this.supplierName = value; }
public String getFeeItemName() { return feeItemName; }
@@ -22,21 +22,21 @@ public class FinanceContainerSettlementQueryService {
this.mapper = mapper;
}
public ContainerSettlementDetails details(long customerId, long containerId) {
public ContainerSettlementDetails details(long customerId, long containerBookingId) {
if (customerId <= 0) throw new IllegalArgumentException("客户范围无效");
if (containerId <= 0) throw new IllegalArgumentException("柜次范围无效");
if (containerBookingId <= 0) throw new IllegalArgumentException("货柜范围无效");
var ownership = mapper.ownership(customerId, containerId);
var ownership = mapper.ownership(customerId, containerBookingId);
if (ownership == null || ownership.getCustomerLinkCount() == 0) {
throw new BizException(ResultCode.NOT_FOUND, "柜次不存在或不属于客户");
}
List<Long> settledSourceFeeIds = mapper.settledSourceFeeIds(customerId, containerId).stream()
List<Long> settledSourceFeeIds = mapper.settledSourceFeeIds(customerId, containerBookingId).stream()
.filter(value -> value != null && value > 0)
.distinct()
.toList();
List<ContainerCostDetail> costs = groupCosts(mapper.costs(customerId, containerId));
return new ContainerSettlementDetails(customerId, containerId, settledSourceFeeIds, costs);
List<ContainerCostDetail> costs = groupCosts(mapper.costs(customerId, containerBookingId));
return new ContainerSettlementDetails(customerId, containerBookingId, settledSourceFeeIds, costs);
}
private static List<ContainerCostDetail> groupCosts(List<CostAllocationRow> rows) {
@@ -60,7 +60,7 @@ public class FinanceContainerSettlementQueryService {
private ContainerCostDetail view() {
return new ContainerCostDetail(cost.getCostId(), cost.getCostNo(), cost.getCostType(),
cost.getCostOrigin(), cost.getSourceFeeId(), cost.getSourceFeeVersion(), cost.getContainerId(),
cost.getCostOrigin(), cost.getSourceFeeId(), cost.getSourceFeeVersion(), cost.getContainerBookingId(),
cost.getSupplierName(), cost.getFeeItemName(), cost.getAmount(), cost.getCurrency(),
cost.getStatus(), List.copyOf(allocations));
}
@@ -86,10 +86,11 @@ public class FinanceFeeSourceEventService {
snapshot.sourceType(), snapshot.feeId());
jdbc.update("UPDATE fin_cost SET status='VOIDED',voided_at=CURRENT_TIMESTAMP(3),void_reason='来源版本已替代',version=version+1,updated_by=0 WHERE cost_origin='FIRSTLEG_DIRECT_PAID' AND source_system='FIRSTLEG' AND source_type=? AND source_fee_id=? AND status='CONFIRMED'",
snapshot.sourceType(), snapshot.feeId());
jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')",
jdbc.update("INSERT INTO fin_fee_source(source_system,source_fee_id,source_fee_version,customer_id,source_type,source_no,direction,order_id,inbound_id,container_id,container_booking_id,fee_item_code,fee_item_name,amount,currency_code,settlement_mode,paid_at,payment_method,payment_reference_no,payment_voucher_file_ids,source_hash,source_snapshot,status) VALUES ('FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'AVAILABLE')",
snapshot.feeId(), snapshot.feeVersion(), snapshot.customerId(), snapshot.sourceType(),
snapshot.inboundCode() == null || snapshot.inboundCode().isBlank() ? snapshot.containerNo() : snapshot.inboundCode(),
snapshot.direction(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(),
financeContainerBookingId(snapshot),
snapshot.feeItemCode(), snapshot.feeItemName(), snapshot.totalAmount(), snapshot.currency(),
snapshot.settlementMode(), snapshot.paidAt(), snapshot.paymentMethod(),
snapshot.paymentReferenceNo(), voucherJson(snapshot.paymentVoucherFileIds()),
@@ -116,7 +117,7 @@ public class FinanceFeeSourceEventService {
String costNo = "DIRECT-" + snapshot.sourceType() + "-" + snapshot.feeId() + "-V" + snapshot.feeVersion();
jdbc.update("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,cost_origin,source_system,source_type,source_fee_id,source_fee_version,source_hash,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,currency_code,cost_amount,voucher_file_ids,paid_at,payment_method,payment_reference_no,status,confirmed_at,created_by,created_by_name,updated_by) VALUES(?,?,'SOURCE_DIRECT_PAID','FIRSTLEG_DIRECT_PAID','FIRSTLEG',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP(3),0,'系统',0)",
snapshot.customerId(), costNo, snapshot.sourceType(), snapshot.feeId(), snapshot.feeVersion(),
snapshot.sourceHash(), snapshot.orderId(), snapshot.inboundId(), snapshot.containerId(),
snapshot.sourceHash(), snapshot.orderId(), snapshot.inboundId(), financeContainerBookingId(snapshot),
snapshot.supplierId(), snapshot.supplierName(), snapshot.feeItemCode(), snapshot.feeItemName(),
snapshot.currency(), snapshot.totalAmount(), voucherJsonOrEmpty(snapshot.paymentVoucherFileIds()),
snapshot.paidAt(), snapshot.paymentMethod(), snapshot.paymentReferenceNo());
@@ -184,7 +185,11 @@ public class FinanceFeeSourceEventService {
throw error("FIN-EVT-012", "应收费用不得携带结算方式或已付快照");
}
if ("INBOUND_FEE".equals(value.sourceType()) && (value.inboundId() == null || !String.valueOf(value.inboundId()).equals(event.payload().inboundId()))) throw error("FIN-EVT-013", "入库单归属不一致");
if ("CONTAINER_FEE".equals(value.sourceType()) && (value.containerId() == null || !String.valueOf(value.containerId()).equals(event.payload().containerId()))) throw error("FIN-EVT-014", "柜子归属不一致");
if ("CONTAINER_FEE".equals(value.sourceType()) && (value.containerId() == null
|| !String.valueOf(value.containerId()).equals(event.payload().containerId())
|| value.containerBookingId() == null || value.containerBookingId() <= 0)) {
throw error("FIN-EVT-014", "柜子归属不一致");
}
}
private void validatePayableSettlement(FirstlegFeeSourceClient.Snapshot value) {
@@ -216,6 +221,10 @@ public class FinanceFeeSourceEventService {
return values == null || values.isEmpty() ? "[]" : json(values);
}
private static Long financeContainerBookingId(FirstlegFeeSourceClient.Snapshot snapshot) {
return "CONTAINER_FEE".equals(snapshot.sourceType()) ? snapshot.containerBookingId() : null;
}
private String json(Object value) {
try { return objectMapper.writeValueAsString(value); }
catch (JsonProcessingException e) { throw error("FIN-EVT-001", "事件序列化失败"); }