费用提交

This commit is contained in:
2026-10-26 13:41:43 +08:00
parent 0fe1d10566
commit 761fe3686e
171 changed files with 8750 additions and 386 deletions
@@ -7,11 +7,16 @@ import com.baiyue.finance.dto.FinanceWalletDtos.CreditLimitRequest;
import com.baiyue.finance.dto.FinanceWalletDtos.RechargeReviewRequest;
import com.baiyue.finance.dto.FinancePhase1Dtos.PageEnvelope;
import com.baiyue.finance.dto.FinanceWalletDtos.RechargeRequest;
import com.baiyue.finance.dto.FinanceWalletDtos.WalletRefundRequest;
import com.baiyue.finance.service.FinanceWalletService;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;
@RestController
@RequestMapping("/api/platform/finance/v1")
@@ -62,4 +67,25 @@ public class PlatformFinanceWalletController {
@GetMapping("/recharge-requests/{requestId}/voucher")
@PreAuthorize("hasAuthority('platform:finance:customer-account:list')")
public ResponseEntity<byte[]> voucher(@PathVariable long requestId) { return service.voucher(requestId); }
/** 退款必须由平台财务针对原消费发起,复用资金服务的账户锁及累计可退上限。 */
@PostMapping("/accounts/{customerId}/ledger/{ledgerId}/refunds")
@PreAuthorize("hasAuthority('platform:finance:customer-account:adjust')")
public ApiResponse<?> refund(@AuthenticationPrincipal AuthenticatedUser user,
@PathVariable long customerId, @PathVariable long ledgerId,
@RequestHeader("Idempotency-Key") String idempotencyKey,
@RequestBody WalletRefundRequest request) {
if (user == null || user.userId() == null || !"PLATFORM".equals(user.appCode()) || !Long.valueOf(0L).equals(user.tenantId())) {
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅平台财务可操作退款");
}
if (request == null || idempotencyKey == null || idempotencyKey.isBlank()
|| idempotencyKey.trim().length() > 128) throw new IllegalArgumentException("退款请求及幂等键必填且幂等键不超过128字符");
if (request.reason() == null || request.reason().isBlank() || request.reason().length() > 500)
throw new IllegalArgumentException("退款原因必填且不超过500字符");
// 技术退款引用由客户和幂等键确定;资金流水号仍由正式 AC 编号生成器分配。
String refundNo = "MANUAL_REFUND:" + UUID.nameUUIDFromBytes(
(customerId + ":" + idempotencyKey.trim()).getBytes(StandardCharsets.UTF_8));
return FinanceResponses.success(service.refundConsumption(customerId, ledgerId, refundNo,
idempotencyKey.trim(), request.amount(), user.userId(), user.displayName(), request.reason().trim()));
}
}
@@ -54,6 +54,9 @@ public final class FinanceWalletDtos {
public record WalletHoldActionRequest(String remark) { }
/** 关联原消费退款,原因必填,原流水保持不可变。 */
public record WalletRefundRequest(BigDecimal amount, String reason) { }
public record WalletHold(long holdId, long customerId, String businessType,
String businessNo, BigDecimal amount,
BigDecimal actualFrozenAmount, BigDecimal creditFrozenAmount,
@@ -135,6 +135,7 @@ public class BusinessFeeSourceEventService {
positive(value.walletSettlement().holdId(), "walletSettlement.holdId");
positive(value.walletSettlement().ledgerId(), "walletSettlement.ledgerId");
if (blank(value.walletSettlement().commandEventId())) throw error("FIN-EVT-012", "缺少原资金命令引用");
walletIdentity(value);
} else if ("OVERSEAS_PROCUREMENT_FEE".equals(value.sourceType()) && "RECEIVABLE".equals(value.direction())) {
throw error("FIN-EVT-012", "采购应收必须引用成功实扣资金,不能进入待收费池");
}
@@ -154,7 +155,7 @@ public class BusinessFeeSourceEventService {
throw error("FIN-EVT-012", "应付来源供应商快照不一致");
validateSettlement(value);
} else if (!blank(value.supplierId()) || !blank(value.supplierName()) || !blank(value.supplierCategory())
|| !blank(value.settlementMode()) || value.paidAt() != null
|| value.settlementMode() != null && !Set.of("BILL", "WALLET").contains(value.settlementMode()) || value.paidAt() != null
|| !blank(value.paymentMethod()) || !blank(value.paymentReferenceNo())
|| (value.paymentVoucherFileIds() != null && !value.paymentVoucherFileIds().isEmpty())) {
throw error("FIN-EVT-012", "应收来源不得携带供应商或付款事实");
@@ -202,9 +203,10 @@ public class BusinessFeeSourceEventService {
long customerId = positive(source.customerId(), "customerId");
long holdId = positive(reference.holdId(), "holdId");
long ledgerId = positive(reference.ledgerId(), "ledgerId");
String[] identity = walletIdentity(source);
List<Map<String, Object>> rows = jdbc.queryForList(
"SELECT l.amount,c.result_json FROM fin_wallet_hold h JOIN fin_wallet_ledger l ON l.id=h.action_ledger_id AND l.customer_id=h.customer_id JOIN fin_wallet_command_inbox c ON c.event_id=? AND c.status='SUCCEEDED' WHERE h.id=? AND l.id=? AND h.customer_id=? AND h.business_type=? AND h.business_no=? AND h.status='SETTLED' AND l.change_type='CONSUMPTION' FOR UPDATE",
reference.commandEventId(), holdId, ledgerId, customerId, source.businessType(), source.businessNo());
reference.commandEventId(), holdId, ledgerId, customerId, identity[0], identity[1]);
if (rows.size() != 1 || !(rows.get(0).get("amount") instanceof java.math.BigDecimal amount)
|| amount.negate().compareTo(source.totalAmount()) != 0) {
throw error("FIN-EVT-017", "费用来源与成功扣款的客户、业务或金额不匹配");
@@ -218,8 +220,8 @@ public class BusinessFeeSourceEventService {
|| !reference.holdId().equals(result.path("walletHoldId").asText())
|| !reference.ledgerId().equals(result.path("walletLedgerId").asText())
|| result.path("customerId").asLong() != customerId
|| !source.businessType().equals(result.path("businessType").asText())
|| !source.businessNo().equals(result.path("businessNo").asText())
|| !identity[0].equals(result.path("businessType").asText())
|| !identity[1].equals(result.path("businessNo").asText())
|| result.path("amount").decimalValue().compareTo(source.totalAmount()) != 0) {
throw error("FIN-EVT-017", "费用来源引用的资金结果不匹配");
}
@@ -234,6 +236,25 @@ public class BusinessFeeSourceEventService {
feeSourceId);
}
/** 独立锁款仅接收已约定的人工费用或仓租身份,不能借此挂接任意业务扣款。 */
private String[] walletIdentity(BusinessFeeSourceChangedV1 source) {
var reference = source.walletSettlement();
String type = reference.walletBusinessType(), no = reference.walletBusinessNo();
if (type == null && no == null) return new String[]{source.businessType(), source.businessNo()};
if (blank(type) || blank(no) || !"LASTLEG".equals(source.sourceSystem()))
throw error("FIN-EVT-017", "资金业务引用不完整");
if ("OVERSEAS_MANUAL_FEE".equals(type)) {
if ("FEE_TYPE_077".equals(source.feeItemCode())
|| !no.matches(java.util.regex.Pattern.quote(source.sourceFeeId()) + "-[1-9][0-9]*"))
throw error("FIN-EVT-017", "人工费用资金引用与来源不匹配");
} else if ("STORAGE_RENT".equals(type)) {
if (!"FEE_TYPE_077".equals(source.feeItemCode()))
throw error("FIN-EVT-017", "仓租资金引用与费用项目不匹配");
positive(no, "walletBusinessNo");
} else throw error("FIN-EVT-017", "不支持的独立资金业务引用");
return new String[]{type, no};
}
private void materializeDirectPaidCost(String system, long feeId, BusinessFeeSourceChangedV1 value) {
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,warehouse_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','BUSINESS_DIRECT_PAID',?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,'CONFIRMED',CURRENT_TIMESTAMP(3),0,'系统',0)",
positive(value.customerId(), "customerId"), "DIRECT-" + system + '-' + value.sourceType() + '-' + feeId + "-V" + value.sourceVersion(),