feat(fleet): enforce reviewed trips and original fee coverage for dispatch

This commit is contained in:
2026-10-03 04:18:52 +08:00
parent c50e9f97d1
commit 5e683da3b9
162 changed files with 9642 additions and 218 deletions
@@ -0,0 +1,31 @@
package com.baiyue.finance.controller;
import com.baiyue.common.core.api.ApiResponse;
import com.baiyue.finance.dto.DeliveryFeeCoverageModels;
import com.baiyue.finance.service.DeliveryFeeCoverageQueryService;
import com.baiyue.finance.service.FinanceEventException;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/internal/finance/v1/delivery-fee-coverage")
public class InternalDeliveryFeeCoverageController {
private final DeliveryFeeCoverageQueryService service;
@Value("${baiyue.security.internal-service-token}") private String expectedToken;
@PostMapping("/sources")
public ApiResponse<List<DeliveryFeeCoverageModels.Fee>> sources(
@RequestHeader(value = "X-Internal-Service-Token", required = false) String token,
@Valid @RequestBody DeliveryFeeCoverageModels.Query request) {
if (token == null || expectedToken == null || expectedToken.isBlank()
|| !MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8), expectedToken.getBytes(StandardCharsets.UTF_8)))
throw new FinanceEventException("FIN-EVT-401", "内部服务令牌无效");
return FinanceResponses.success(service.query(request));
}
}
@@ -0,0 +1,14 @@
package com.baiyue.finance.dto;
import jakarta.validation.constraints.*;
import java.math.BigDecimal;
import java.util.List;
public final class DeliveryFeeCoverageModels {
private DeliveryFeeCoverageModels() { }
public record Query(@NotNull @Positive Long customerId,
@NotEmpty @Size(max = 2000) List<@NotNull @Positive Long> inboundIds) { }
public record Fee(long financeFeeId, long sourceFeeId, String sourceType, int sourceVersion,
long customerId, long sourceInboundId, String inboundCode, String feeItemCode, String feeItemName,
String currency, BigDecimal amount, String sourceHash, String status) { }
}
@@ -0,0 +1,40 @@
package com.baiyue.finance.service;
import com.baiyue.finance.dto.DeliveryFeeCoverageModels;
import lombok.RequiredArgsConstructor;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Map;
/** Finance eligibility only. The business operator must explicitly confirm which services are included. */
@Service
@RequiredArgsConstructor
public class DeliveryFeeCoverageQueryService {
private final JdbcTemplate jdbc;
@Transactional(readOnly = true)
public List<DeliveryFeeCoverageModels.Fee> query(DeliveryFeeCoverageModels.Query request) {
return new NamedParameterJdbcTemplate(jdbc).query("""
SELECT f.id,f.source_fee_id,f.source_type,f.source_fee_version,f.customer_id,f.inbound_id,
CASE WHEN NULLIF(TRIM(f.inbound_code),'') IS NOT NULL THEN f.inbound_code
WHEN f.source_type='INBOUND_FEE' THEN f.source_no ELSE NULL END AS coverage_inbound_code,
f.fee_item_code,f.fee_item_name,f.currency_code,f.amount,f.source_hash,f.status
FROM fin_fee_source f
WHERE f.customer_id=:customerId AND f.inbound_id IN (:inboundIds)
AND f.source_system='FIRSTLEG' AND f.source_type IN ('INBOUND_FEE','CONTAINER_FEE')
AND f.direction='RECEIVABLE' AND f.review_status='APPROVED'
AND f.status IN ('AVAILABLE','SETTLED')
AND NOT EXISTS (SELECT 1 FROM fin_fee_source newer
WHERE newer.source_system=f.source_system AND newer.source_type=f.source_type
AND newer.source_fee_id=f.source_fee_id AND newer.source_fee_version>f.source_fee_version)
ORDER BY f.inbound_id,f.fee_item_code,f.source_fee_id
""", Map.of("customerId", request.customerId(), "inboundIds", request.inboundIds().stream().distinct().toList()),
(row, index) -> new DeliveryFeeCoverageModels.Fee(row.getLong("id"), row.getLong("source_fee_id"),
row.getString("source_type"), row.getInt("source_fee_version"), row.getLong("customer_id"), row.getLong("inbound_id"),
row.getString("coverage_inbound_code"), row.getString("fee_item_code"), row.getString("fee_item_name"),
row.getString("currency_code"), row.getBigDecimal("amount"), row.getString("source_hash"), row.getString("status")));
}
}
@@ -0,0 +1,48 @@
openapi: 3.0.3
info: { title: Delivery fee coverage sources, version: 1.0.0 }
paths:
/internal/finance/v1/delivery-fee-coverage/sources:
post:
summary: 按实际客户及头程入仓单批量读取可引用的原费用版本
description: 仅内部服务身份,Gateway不公开。只返回该客户、指定原入仓单、FIRSTLEG来源的已审核有效应收最新版本;AVAILABLE或SETTLED不代表自动涵盖任何尾程服务,服务范围由运营明确确认。查询不冻结资金、不变更账务、不新增收费。
parameters:
- { in: header, name: X-Internal-Service-Token, required: true, schema: { type: string } }
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [customerId, inboundIds]
properties:
customerId: { type: string, pattern: '^[1-9][0-9]*$' }
inboundIds: { type: array, minItems: 1, maxItems: 2000, items: { type: string, pattern: '^[1-9][0-9]*$' } }
responses:
'200':
description: 原费用不可变来源版本;没有符合记录返回空数组
content:
application/json:
schema:
type: object
properties:
data: { type: array, items: { $ref: '#/components/schemas/FeeSource' } }
'400': { description: 请求字段无效 }
'401': { description: 内部服务令牌无效 }
components:
schemas:
FeeSource:
type: object
properties:
financeFeeId: { type: string }
sourceFeeId: { type: string }
sourceType: { type: string, enum: [INBOUND_FEE, CONTAINER_FEE] }
sourceVersion: { type: integer }
customerId: { type: string }
sourceInboundId: { type: string }
inboundCode: { type: string, nullable: true }
feeItemCode: { type: string }
feeItemName: { type: string }
currency: { type: string }
amount: { type: number }
sourceHash: { type: string }
status: { type: string, enum: [AVAILABLE, SETTLED] }