feat(finance): add container settlement aggregate
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
package com.baiyue.finance;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
@@ -7,6 +9,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
|
||||
|
||||
/** Finance 独立服务启动入口。首期只承载人工账单、收款和成本台账。 */
|
||||
@SpringBootApplication
|
||||
@MapperScan(basePackages = "com.baiyue.finance", annotationClass = Mapper.class)
|
||||
@EnableFeignClients(basePackages = "com.baiyue.finance.client")
|
||||
@EnableScheduling
|
||||
public class BaiyueFinanceApplication {
|
||||
|
||||
+33
@@ -0,0 +1,33 @@
|
||||
package com.baiyue.finance.controller;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import com.baiyue.finance.dto.FinanceContainerSettlementDtos.ContainerSettlementDetails;
|
||||
import com.baiyue.finance.service.FinanceContainerSettlementQueryService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
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/finance/v1")
|
||||
public class PlatformFinanceContainerSettlementController {
|
||||
private final FinanceContainerSettlementQueryService service;
|
||||
|
||||
public PlatformFinanceContainerSettlementController(FinanceContainerSettlementQueryService service) {
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
@GetMapping("/container-settlement-details")
|
||||
@PreAuthorize("hasAuthority('platform:finance:cost:view') and "
|
||||
+ "hasAuthority('platform:finance:settlement-ar:list') and "
|
||||
+ "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));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.baiyue.finance.dto;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/** Read-only container finance projection for the loading-list workbench. */
|
||||
public final class FinanceContainerSettlementDtos {
|
||||
private FinanceContainerSettlementDtos() {}
|
||||
|
||||
public record ContainerSettlementDetails(
|
||||
long customerId,
|
||||
long containerId,
|
||||
List<Long> settledSourceFeeIds,
|
||||
List<ContainerCostDetail> costs) {}
|
||||
|
||||
public record ContainerCostDetail(
|
||||
long costId,
|
||||
String costNo,
|
||||
String costType,
|
||||
String costOrigin,
|
||||
Long sourceFeeId,
|
||||
Integer sourceFeeVersion,
|
||||
long containerId,
|
||||
String supplierName,
|
||||
String feeItemName,
|
||||
BigDecimal amount,
|
||||
String currency,
|
||||
String status,
|
||||
List<ContainerCostAllocation> allocations) {}
|
||||
|
||||
public record ContainerCostAllocation(
|
||||
long allocationId,
|
||||
long orderId,
|
||||
Long inboundId,
|
||||
BigDecimal volumeCbm,
|
||||
BigDecimal totalVolumeCbm,
|
||||
BigDecimal allocationRate,
|
||||
BigDecimal allocatedAmount,
|
||||
boolean roundingAdjustment) {}
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package com.baiyue.finance.mapper;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
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}
|
||||
UNION ALL
|
||||
SELECT tenant_id AS customer_id, container_id FROM fin_cost
|
||||
WHERE tenant_id = #{customerId} AND container_id = #{containerId}
|
||||
) links
|
||||
""")
|
||||
OwnershipRow ownership(@Param("customerId") long customerId, @Param("containerId") long containerId);
|
||||
|
||||
@Select("""
|
||||
SELECT DISTINCT settled.source_fee_id
|
||||
FROM (
|
||||
SELECT f.source_fee_id
|
||||
FROM fin_settlement s
|
||||
JOIN fin_settlement_line sl ON sl.tenant_id = s.tenant_id AND sl.settlement_id = s.id
|
||||
JOIN fin_fee_source f ON f.id = sl.fee_source_id AND f.customer_id = sl.tenant_id
|
||||
WHERE s.tenant_id = #{customerId}
|
||||
AND s.status = 'PAID'
|
||||
AND f.customer_id = #{customerId}
|
||||
AND f.container_id = #{containerId}
|
||||
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.cost_origin = 'FIRSTLEG_DIRECT_PAID'
|
||||
AND c.source_system = 'FIRSTLEG'
|
||||
AND c.source_type = 'CONTAINER_FEE'
|
||||
AND c.status = 'CONFIRMED'
|
||||
AND c.source_fee_id IS NOT NULL
|
||||
) settled
|
||||
ORDER BY settled.source_fee_id
|
||||
""")
|
||||
List<Long> settledSourceFeeIds(@Param("customerId") long customerId,
|
||||
@Param("containerId") long containerId);
|
||||
|
||||
@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.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,
|
||||
a.volume_cbm AS volumeCbm, a.total_volume_cbm AS totalVolumeCbm,
|
||||
a.allocation_rate AS allocationRate, a.allocated_amount AS allocatedAmount,
|
||||
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}
|
||||
ORDER BY c.created_at DESC, c.id DESC, a.id
|
||||
""")
|
||||
List<CostAllocationRow> costs(@Param("customerId") long customerId,
|
||||
@Param("containerId") long containerId);
|
||||
|
||||
final class OwnershipRow {
|
||||
private long customerLinkCount;
|
||||
|
||||
public long getCustomerLinkCount() { return customerLinkCount; }
|
||||
public void setCustomerLinkCount(long value) { this.customerLinkCount = value; }
|
||||
}
|
||||
|
||||
final class CostAllocationRow {
|
||||
private long costId;
|
||||
private String costNo;
|
||||
private String costType;
|
||||
private String costOrigin;
|
||||
private Long sourceFeeId;
|
||||
private Integer sourceFeeVersion;
|
||||
private long containerId;
|
||||
private String supplierName;
|
||||
private String feeItemName;
|
||||
private BigDecimal amount;
|
||||
private String currency;
|
||||
private String status;
|
||||
private Long allocationId;
|
||||
private Long orderId;
|
||||
private Long inboundId;
|
||||
private BigDecimal volumeCbm;
|
||||
private BigDecimal totalVolumeCbm;
|
||||
private BigDecimal allocationRate;
|
||||
private BigDecimal allocatedAmount;
|
||||
private boolean roundingAdjustment;
|
||||
|
||||
public long getCostId() { return costId; }
|
||||
public void setCostId(long value) { this.costId = value; }
|
||||
public String getCostNo() { return costNo; }
|
||||
public void setCostNo(String value) { this.costNo = value; }
|
||||
public String getCostType() { return costType; }
|
||||
public void setCostType(String value) { this.costType = value; }
|
||||
public String getCostOrigin() { return costOrigin; }
|
||||
public void setCostOrigin(String value) { this.costOrigin = value; }
|
||||
public Long getSourceFeeId() { return sourceFeeId; }
|
||||
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 String getSupplierName() { return supplierName; }
|
||||
public void setSupplierName(String value) { this.supplierName = value; }
|
||||
public String getFeeItemName() { return feeItemName; }
|
||||
public void setFeeItemName(String value) { this.feeItemName = value; }
|
||||
public BigDecimal getAmount() { return amount; }
|
||||
public void setAmount(BigDecimal value) { this.amount = value; }
|
||||
public String getCurrency() { return currency; }
|
||||
public void setCurrency(String value) { this.currency = value; }
|
||||
public String getStatus() { return status; }
|
||||
public void setStatus(String value) { this.status = value; }
|
||||
public Long getAllocationId() { return allocationId; }
|
||||
public void setAllocationId(Long value) { this.allocationId = value; }
|
||||
public Long getOrderId() { return orderId; }
|
||||
public void setOrderId(Long value) { this.orderId = value; }
|
||||
public Long getInboundId() { return inboundId; }
|
||||
public void setInboundId(Long value) { this.inboundId = value; }
|
||||
public BigDecimal getVolumeCbm() { return volumeCbm; }
|
||||
public void setVolumeCbm(BigDecimal value) { this.volumeCbm = value; }
|
||||
public BigDecimal getTotalVolumeCbm() { return totalVolumeCbm; }
|
||||
public void setTotalVolumeCbm(BigDecimal value) { this.totalVolumeCbm = value; }
|
||||
public BigDecimal getAllocationRate() { return allocationRate; }
|
||||
public void setAllocationRate(BigDecimal value) { this.allocationRate = value; }
|
||||
public BigDecimal getAllocatedAmount() { return allocatedAmount; }
|
||||
public void setAllocatedAmount(BigDecimal value) { this.allocatedAmount = value; }
|
||||
public boolean isRoundingAdjustment() { return roundingAdjustment; }
|
||||
public void setRoundingAdjustment(boolean value) { this.roundingAdjustment = value; }
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package com.baiyue.finance.service;
|
||||
|
||||
import com.baiyue.common.core.api.ResultCode;
|
||||
import com.baiyue.common.core.exception.BizException;
|
||||
import com.baiyue.finance.dto.FinanceContainerSettlementDtos.ContainerCostAllocation;
|
||||
import com.baiyue.finance.dto.FinanceContainerSettlementDtos.ContainerCostDetail;
|
||||
import com.baiyue.finance.dto.FinanceContainerSettlementDtos.ContainerSettlementDetails;
|
||||
import com.baiyue.finance.mapper.FinanceContainerSettlementQueryMapper;
|
||||
import com.baiyue.finance.mapper.FinanceContainerSettlementQueryMapper.CostAllocationRow;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Service
|
||||
public class FinanceContainerSettlementQueryService {
|
||||
private final FinanceContainerSettlementQueryMapper mapper;
|
||||
|
||||
public FinanceContainerSettlementQueryService(FinanceContainerSettlementQueryMapper mapper) {
|
||||
this.mapper = mapper;
|
||||
}
|
||||
|
||||
public ContainerSettlementDetails details(long customerId, long containerId) {
|
||||
if (customerId <= 0) throw new IllegalArgumentException("客户范围无效");
|
||||
if (containerId <= 0) throw new IllegalArgumentException("柜次范围无效");
|
||||
|
||||
var ownership = mapper.ownership(customerId, containerId);
|
||||
if (ownership == null || ownership.getCustomerLinkCount() == 0) {
|
||||
throw new BizException(ResultCode.NOT_FOUND, "柜次不存在或不属于客户");
|
||||
}
|
||||
|
||||
List<Long> settledSourceFeeIds = mapper.settledSourceFeeIds(customerId, containerId).stream()
|
||||
.filter(value -> value != null && value > 0)
|
||||
.distinct()
|
||||
.toList();
|
||||
List<ContainerCostDetail> costs = groupCosts(mapper.costs(customerId, containerId));
|
||||
return new ContainerSettlementDetails(customerId, containerId, settledSourceFeeIds, costs);
|
||||
}
|
||||
|
||||
private static List<ContainerCostDetail> groupCosts(List<CostAllocationRow> rows) {
|
||||
Map<Long, CostAccumulator> grouped = new LinkedHashMap<>();
|
||||
for (CostAllocationRow row : rows) {
|
||||
CostAccumulator accumulator = grouped.computeIfAbsent(row.getCostId(), ignored -> new CostAccumulator(row));
|
||||
if (row.getAllocationId() != null) {
|
||||
accumulator.allocations.add(new ContainerCostAllocation(row.getAllocationId(), row.getOrderId(),
|
||||
row.getInboundId(), row.getVolumeCbm(), row.getTotalVolumeCbm(), row.getAllocationRate(),
|
||||
row.getAllocatedAmount(), row.isRoundingAdjustment()));
|
||||
}
|
||||
}
|
||||
return grouped.values().stream().map(CostAccumulator::view).toList();
|
||||
}
|
||||
|
||||
private static final class CostAccumulator {
|
||||
private final CostAllocationRow cost;
|
||||
private final List<ContainerCostAllocation> allocations = new ArrayList<>();
|
||||
|
||||
private CostAccumulator(CostAllocationRow cost) { this.cost = cost; }
|
||||
|
||||
private ContainerCostDetail view() {
|
||||
return new ContainerCostDetail(cost.getCostId(), cost.getCostNo(), cost.getCostType(),
|
||||
cost.getCostOrigin(), cost.getSourceFeeId(), cost.getSourceFeeVersion(), cost.getContainerId(),
|
||||
cost.getSupplierName(), cost.getFeeItemName(), cost.getAmount(), cost.getCurrency(),
|
||||
cost.getStatus(), List.copyOf(allocations));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user