优化
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
package com.baiyue.finance.client;
|
||||
|
||||
import com.baiyue.common.core.api.ApiResponse;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@FeignClient(name = "warehouse-service", contextId = "financeWarehouseSupplierClient",
|
||||
url = "${baiyue.clients.warehouse.url:}", path = "/internal/warehouse/v1/suppliers")
|
||||
public interface WarehouseSupplierClient {
|
||||
@GetMapping("/{supplierId}")
|
||||
ApiResponse<SupplierSnapshot> get(@RequestHeader("X-Internal-Service-Token") String token,
|
||||
@PathVariable long supplierId,
|
||||
@RequestParam(defaultValue = "true") boolean requireActive,
|
||||
@RequestParam String category);
|
||||
|
||||
record SupplierSnapshot(long supplierId, String name, String category, String scope, String status) {}
|
||||
}
|
||||
@@ -28,6 +28,7 @@ public class FinanceFeeSourceEventService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final FirstlegFeeSourceClient firstleg;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final FinanceSupplierValidator supplierValidator;
|
||||
@Value("${baiyue.security.internal-service-token}")
|
||||
private String internalToken;
|
||||
|
||||
@@ -126,6 +127,13 @@ public class FinanceFeeSourceEventService {
|
||||
if (!"CNY".equals(value.currency()) || value.totalAmount() == null || value.totalAmount().signum() <= 0) throw error("FIN-EVT-010", "费用金额或币种无效");
|
||||
if (!Set.of("RECEIVABLE", "PAYABLE").contains(value.direction())) throw error("FIN-EVT-011", "费用方向无效");
|
||||
if (!Set.of("INBOUND_FEE", "CONTAINER_FEE").contains(value.sourceType()) || blank(value.feeItemCode()) || blank(value.feeItemName()) || blank(value.sourceHash())) throw error("FIN-EVT-012", "费用来源快照字段不完整");
|
||||
if ("PAYABLE".equals(value.direction())) {
|
||||
if (value.partnerId() == null || value.partnerId() <= 0 || blank(value.partnerName())) {
|
||||
throw error("FIN-EVT-012", "应付费用来源供应商快照不完整");
|
||||
}
|
||||
String category = supplierValidator.categoryFor(value.feeItemCode(), value.feeItemName());
|
||||
supplierValidator.requireActive(value.partnerId(), category);
|
||||
}
|
||||
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", "柜子归属不一致");
|
||||
}
|
||||
|
||||
@@ -27,11 +27,14 @@ public class FinanceService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final FinanceVoucherValidator voucherValidator;
|
||||
private final FinanceSupplierValidator supplierValidator;
|
||||
|
||||
public FinanceService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator) {
|
||||
public FinanceService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator,
|
||||
FinanceSupplierValidator supplierValidator) {
|
||||
this.jdbc = jdbc;
|
||||
this.objectMapper = objectMapper;
|
||||
this.voucherValidator = voucherValidator;
|
||||
this.supplierValidator = supplierValidator;
|
||||
}
|
||||
|
||||
public Map<String, Object> customerTerms(long tenantId) {
|
||||
@@ -265,14 +268,23 @@ public class FinanceService {
|
||||
if (replay != null) return jdbc.queryForMap("SELECT * FROM fin_cost WHERE id=? AND tenant_id=?", replay, tenantId);
|
||||
if (!"SINGLE_ORDER".equals(request.costType()) && !"CONTAINER_SHARED".equals(request.costType())) throw bad("成本类型只能是SINGLE_ORDER或CONTAINER_SHARED");
|
||||
if (request.amount() == null || request.amount().signum() <= 0) throw bad("成本金额必须大于0");
|
||||
String supplierName = null;
|
||||
if (request.supplierId() != null) {
|
||||
if (request.supplierId() <= 0) throw bad("供应商ID无效");
|
||||
String category = supplierValidator.categoryFor(request.feeItemCode(), request.feeItemName());
|
||||
supplierName = supplierValidator.requireActive(request.supplierId(), category).name();
|
||||
} else if (org.springframework.util.StringUtils.hasText(request.supplierName())) {
|
||||
throw bad("供应商名称必须来自Warehouse供应商主数据");
|
||||
}
|
||||
validateVoucherFiles(tenantId, request.voucherFileIds());
|
||||
if ("SINGLE_ORDER".equals(request.costType()) && (request.orderId() == null || request.containerId() != null)) throw bad("单票成本必须指定订单且不能指定柜次");
|
||||
if ("CONTAINER_SHARED".equals(request.costType()) && (request.containerId() == null || containerOrders == null || containerOrders.isEmpty())) throw bad("整柜成本必须指定柜次及柜内有效订单体积");
|
||||
if ("SINGLE_ORDER".equals(request.costType())) validateSingleOrder(tenantId, request.orderId());
|
||||
validateContainerOrders(tenantId, request.containerId(), containerOrders);
|
||||
String supplierNameSnapshot = supplierName;
|
||||
KeyHolder holder = new GeneratedKeyHolder();
|
||||
jdbc.update(connection -> { PreparedStatement ps = connection.prepareStatement("INSERT INTO fin_cost(tenant_id,cost_no,cost_type,order_id,inbound_id,container_id,supplier_id,supplier_name_snapshot,fee_item_code,fee_item_name,cost_amount,voucher_file_ids,remark,created_by,updated_by) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)", Statement.RETURN_GENERATED_KEYS);
|
||||
ps.setLong(1, tenantId); ps.setString(2, temporaryNo("COST")); ps.setString(3, request.costType()); ps.setObject(4, request.orderId()); ps.setObject(5, request.inboundId()); ps.setObject(6, request.containerId()); ps.setObject(7, request.supplierId()); ps.setString(8, request.supplierName()); ps.setString(9, request.feeItemCode()); ps.setString(10, request.feeItemName()); ps.setBigDecimal(11, request.amount()); ps.setString(12, json(request.voucherFileIds())); ps.setString(13, request.remark()); ps.setLong(14, operatorId); ps.setLong(15, operatorId); return ps; }, holder);
|
||||
ps.setLong(1, tenantId); ps.setString(2, temporaryNo("COST")); ps.setString(3, request.costType()); ps.setObject(4, request.orderId()); ps.setObject(5, request.inboundId()); ps.setObject(6, request.containerId()); ps.setObject(7, request.supplierId()); ps.setString(8, supplierNameSnapshot); ps.setString(9, request.feeItemCode()); ps.setString(10, request.feeItemName()); ps.setBigDecimal(11, request.amount()); ps.setString(12, json(request.voucherFileIds())); ps.setString(13, request.remark()); ps.setLong(14, operatorId); ps.setLong(15, operatorId); return ps; }, holder);
|
||||
long costId = holder.getKey().longValue();
|
||||
jdbc.update("UPDATE fin_cost SET cost_no=? WHERE id=?", stableNo("COST", costId), costId);
|
||||
if ("CONTAINER_SHARED".equals(request.costType())) {
|
||||
|
||||
@@ -46,11 +46,14 @@ public class FinanceSettlementService {
|
||||
private final JdbcTemplate jdbc;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final FinanceVoucherValidator voucherValidator;
|
||||
private final FinanceSupplierValidator supplierValidator;
|
||||
|
||||
public FinanceSettlementService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator) {
|
||||
public FinanceSettlementService(JdbcTemplate jdbc, ObjectMapper objectMapper, FinanceVoucherValidator voucherValidator,
|
||||
FinanceSupplierValidator supplierValidator) {
|
||||
this.jdbc = jdbc;
|
||||
this.objectMapper = objectMapper;
|
||||
this.voucherValidator = voucherValidator;
|
||||
this.supplierValidator = supplierValidator;
|
||||
}
|
||||
|
||||
@Transactional
|
||||
@@ -111,7 +114,7 @@ public class FinanceSettlementService {
|
||||
List<Object> args = new ArrayList<>();
|
||||
args.addAll(ids);
|
||||
List<Map<String, Object>> sources = jdbc.queryForList(
|
||||
"SELECT id,customer_id,direction,currency_code,status,amount,source_snapshot," +
|
||||
"SELECT id,customer_id,direction,currency_code,status,amount,fee_item_code,fee_item_name,source_snapshot," +
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(source_snapshot,'$.reviewStatus')) review_status," +
|
||||
"JSON_UNQUOTE(JSON_EXTRACT(source_snapshot,'$.settlementIncluded')) settlement_included," +
|
||||
"CAST(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(source_snapshot,'$.supplierId'))," +
|
||||
@@ -137,8 +140,11 @@ public class FinanceSettlementService {
|
||||
}
|
||||
BigDecimal total = sources.stream().map(row -> decimal(row, "amount")).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
if (total.signum() <= 0) throw error("FIN_400", "应付结算金额必须大于0");
|
||||
String supplierName = sources.stream().map(row -> text(row, "supplier_name"))
|
||||
.filter(StringUtils::hasText).findFirst().orElse("供应商" + request.supplierId());
|
||||
Set<String> supplierCategories = sources.stream()
|
||||
.map(row -> supplierValidator.categoryFor(text(row, "fee_item_code"), text(row, "fee_item_name")))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (supplierCategories.size() != 1) throw error("FIN_409", "应付结算费用来源必须属于同一供应商类别");
|
||||
String supplierName = supplierValidator.requireActive(request.supplierId(), supplierCategories.iterator().next()).name();
|
||||
String customerName = sources.stream().map(row -> jsonText(parseTree(text(row, "source_snapshot")), "customerName"))
|
||||
.filter(StringUtils::hasText).findFirst().orElse("客户" + customerId);
|
||||
LocalDate generatedDate = LocalDate.now();
|
||||
@@ -215,7 +221,7 @@ public class FinanceSettlementService {
|
||||
CreateSettlementPaymentRequest request) {
|
||||
validatePaymentRequest(request);
|
||||
Map<String, Object> settlement = one(
|
||||
"SELECT id,tenant_id,direction,currency_code,total_amount,paid_amount,outstanding_amount,status,version " +
|
||||
"SELECT id,tenant_id,direction,currency_code,total_amount,paid_amount,outstanding_amount,status,version,supplier_id " +
|
||||
"FROM fin_settlement WHERE id=? FOR UPDATE", "结算单不存在", settlementId);
|
||||
long customerId = number(settlement, "tenant_id").longValue();
|
||||
Long replay = reserve(customerId, operatorId, "SETTLEMENT_PAYMENT_CREATE", key, "POST",
|
||||
@@ -229,6 +235,8 @@ public class FinanceSettlementService {
|
||||
if (number(settlement, "version").intValue() != request.expectedVersion()) throw error("FIN_409", "结算单版本冲突");
|
||||
BigDecimal outstanding = decimal(settlement, "outstanding_amount");
|
||||
if (request.amount().compareTo(outstanding) > 0) throw error("FIN_409", "付款金额不得超过未付余额");
|
||||
String supplierCategory = payableSupplierCategory(customerId, settlementId);
|
||||
supplierValidator.requireActive(number(settlement, "supplier_id").longValue(), supplierCategory);
|
||||
validateVoucherFiles(customerId, request.voucherFileIds());
|
||||
|
||||
insertPayment(customerId, settlementId, request, operatorId);
|
||||
@@ -353,6 +361,18 @@ public class FinanceSettlementService {
|
||||
voucherValidator.validate(customerId, fileIds, "付款凭证");
|
||||
}
|
||||
|
||||
private String payableSupplierCategory(long customerId, long settlementId) {
|
||||
Set<String> categories = jdbc.queryForList(
|
||||
"SELECT f.fee_item_code,f.fee_item_name FROM fin_settlement_line sl " +
|
||||
"JOIN fin_fee_source f ON f.id=sl.fee_source_id AND f.customer_id=sl.tenant_id " +
|
||||
"WHERE sl.tenant_id=? AND sl.settlement_id=?",
|
||||
customerId, settlementId).stream()
|
||||
.map(row -> supplierValidator.categoryFor(text(row, "fee_item_code"), text(row, "fee_item_name")))
|
||||
.collect(java.util.stream.Collectors.toSet());
|
||||
if (categories.size() != 1) throw error("FIN_409", "结算单费用来源供应商类别不一致");
|
||||
return categories.iterator().next();
|
||||
}
|
||||
|
||||
private Long reserve(long customerId, long operatorId, String operation, String key,
|
||||
String method, String path, Object body) {
|
||||
if (!StringUtils.hasText(key) || key.length() > 128) throw error("FIN_400", "必须提供有效的Idempotency-Key");
|
||||
@@ -417,10 +437,13 @@ public class FinanceSettlementService {
|
||||
BigDecimal amount = decimal(row, "amount");
|
||||
BigDecimal unitPrice = quantity == null || quantity.signum() == 0 ? null
|
||||
: amount.divide(quantity, 6, java.math.RoundingMode.HALF_UP);
|
||||
Long supplierId = jsonLong(snapshot, "supplierId");
|
||||
if (supplierId == null) supplierId = jsonLong(snapshot, "partnerId");
|
||||
return new SettlementLine(number(row, "fee_source_id").longValue(), text(row, "source_type"),
|
||||
number(row, "source_fee_id").longValue(), number(row, "source_fee_version").intValue(),
|
||||
text(row, "direction"), number(row, "customer_id").longValue(), jsonText(snapshot, "customerName"),
|
||||
jsonLong(snapshot, "supplierId"), firstText(jsonText(snapshot, "supplierName"), settlement.supplierName()),
|
||||
supplierId, firstText(jsonText(snapshot, "supplierName"),
|
||||
firstText(jsonText(snapshot, "partnerName"), settlement.supplierName())),
|
||||
nullableNumber(row, "inbound_id"), jsonText(snapshot, "inboundCode"), nullableNumber(row, "container_id"),
|
||||
jsonText(snapshot, "containerNo"), jsonDate(snapshot, "businessDate"), text(row, "fee_item_name"),
|
||||
quantity, unitPrice, amount, text(row, "currency_code"), firstText(jsonText(snapshot, "reviewStatus"), "PENDING"),
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.baiyue.finance.service;
|
||||
|
||||
import com.baiyue.finance.client.WarehouseSupplierClient;
|
||||
import feign.FeignException;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Locale;
|
||||
import java.util.Set;
|
||||
|
||||
@Service
|
||||
public class FinanceSupplierValidator {
|
||||
private static final Set<String> CATEGORIES = Set.of("TRUCKING", "CUSTOMS", "BOOKING", "CERTIFICATE");
|
||||
|
||||
private final WarehouseSupplierClient client;
|
||||
private final String internalToken;
|
||||
|
||||
public FinanceSupplierValidator(WarehouseSupplierClient client,
|
||||
@Value("${baiyue.security.internal-service-token:}") String internalToken) {
|
||||
this.client = client;
|
||||
this.internalToken = internalToken;
|
||||
}
|
||||
|
||||
public WarehouseSupplierClient.SupplierSnapshot requireActive(long supplierId, String category) {
|
||||
String normalizedCategory = normalizeCategory(category);
|
||||
try {
|
||||
var response = client.get(internalToken, supplierId, true, normalizedCategory);
|
||||
var supplier = response == null ? null : response.data();
|
||||
if (response == null || !response.successful() || supplier == null
|
||||
|| supplier.supplierId() != supplierId || !"ACTIVE".equals(supplier.status())
|
||||
|| !normalizedCategory.equals(supplier.category()) || !StringUtils.hasText(supplier.name())) {
|
||||
throw new FinanceEventException("FIN_409", "供应商不存在、未启用或类别与费用来源不匹配");
|
||||
}
|
||||
return supplier;
|
||||
} catch (FeignException exception) {
|
||||
if (exception.status() == 400 || exception.status() == 404 || exception.status() == 409) {
|
||||
throw new FinanceEventException("FIN_409", "供应商不存在、未启用或类别与费用来源不匹配");
|
||||
}
|
||||
throw new FinanceEventException("FIN_503", "供应商主数据服务暂不可用");
|
||||
}
|
||||
}
|
||||
|
||||
public String categoryFor(String feeItemCode, String feeItemName) {
|
||||
String value = ((feeItemCode == null ? "" : feeItemCode) + " "
|
||||
+ (feeItemName == null ? "" : feeItemName)).toUpperCase(Locale.ROOT);
|
||||
if (containsAny(value, "TRUCK", "TRAILER", "DRAYAGE", "拖车", "卡车")) return "TRUCKING";
|
||||
if (containsAny(value, "CUSTOMS", "CLEARANCE", "DECLARATION", "报关", "清关")) return "CUSTOMS";
|
||||
if (containsAny(value, "CERTIFICATE", "CERTIFICATION", "CERT_", "认证", "证书")) return "CERTIFICATE";
|
||||
if (containsAny(value, "BOOKING", "FREIGHT", "OCEAN", "SHIPPING", "订舱", "海运", "运费")) return "BOOKING";
|
||||
throw new FinanceEventException("FIN_409", "费用来源缺少可验证的供应商类别");
|
||||
}
|
||||
|
||||
private static boolean containsAny(String value, String... candidates) {
|
||||
for (String candidate : candidates) if (value.contains(candidate)) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static String normalizeCategory(String category) {
|
||||
String normalized = category == null ? "" : category.trim().toUpperCase(Locale.ROOT);
|
||||
if (!CATEGORIES.contains(normalized)) {
|
||||
throw new FinanceEventException("FIN_409", "费用来源供应商类别无效");
|
||||
}
|
||||
return normalized;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user