This commit is contained in:
2026-11-05 11:55:23 +08:00
parent 94a36b8c4c
commit 5a8918e2e4
10 changed files with 418 additions and 27 deletions
@@ -0,0 +1,107 @@
package com.baiyue.finance.service;
import java.util.Locale;
import java.util.Map;
/** 导出业务枚举沿用财务页面中文口径,不改动筛选值、币种及业务编号。 */
final class FinanceExportLabels {
/** 中文导出映射仅提供静态转换。 */
private FinanceExportLabels() { }
private static final Map<String, String> STATUSES = Map.ofEntries(
Map.entry("ACTIVE", "正常"),
Map.entry("DISABLED", "停用"),
Map.entry("INACTIVE", "停用"),
Map.entry("DRAFT", "草稿"),
Map.entry("PENDING", "待审核"),
Map.entry("PENDING_REVIEW", "待审核"),
Map.entry("APPROVED", "已审核"),
Map.entry("ISSUED", "已出账"),
Map.entry("REJECTED", "已驳回"),
Map.entry("UNPAID", "未支付"),
Map.entry("PARTIALLY_PAID", "部分结清"),
Map.entry("PAID", "已结清"),
Map.entry("VOIDED", "已作废"),
Map.entry("AVAILABLE", "待结算"),
Map.entry("LOCKED", "结算锁定"),
Map.entry("BILLED", "已入账"),
Map.entry("SETTLED", "已结算"),
Map.entry("RECORDED", "已登记"),
Map.entry("CONFIRMED", "已确认"),
Map.entry("PENDING_CONFIRMATION", "待客户确认"),
Map.entry("NOT_REQUIRED", "无需确认"));
private static final Map<String, String> SOURCES = Map.ofEntries(
Map.entry("INBOUND_FEE", "入库费用"),
Map.entry("ORDER_FEE", "订单费用"),
Map.entry("CONTAINER_FEE", "装柜费用"),
Map.entry("CONTAINER_COST", "整柜成本"),
Map.entry("MANUAL_COST", "人工成本"),
Map.entry("STORAGE_FEE", "仓储费用"),
Map.entry("OVERSEAS_INBOUND_FEE", "海外入仓费用"),
Map.entry("OVERSEAS_PROCUREMENT_FEE", "海外采购费用"),
Map.entry("OVERSEAS_TRUCK_FEE", "本土卡车费用"),
Map.entry("OVERSEAS_LOOSEBOARD_FEE", "本土散板费用"),
Map.entry("OVERSEAS_WAREHOUSE_FEE", "海外仓服务费"),
Map.entry("OVERSEAS_CONTAINER_FEE", "海外柜子费用"),
Map.entry("OVERSEAS_ORDER_FEE", "尾程订单费用"),
Map.entry("OVERSEAS_DELIVERY_FEE", "尾程派送费用"),
Map.entry("OVERSEAS_RELABEL_FEE", "换标费用"));
private static final Map<String, String> BUSINESSES = Map.ofEntries(
Map.entry("RECHARGE", "充值"),
Map.entry("DEDUCTION", "扣款"),
Map.entry("CREDIT_LIMIT", "授信额度调整"),
Map.entry("FREEZE", "冻结"),
Map.entry("UNFREEZE", "解冻"),
Map.entry("CONSUMPTION", "消费扣款"),
Map.entry("REFUND", "退款"),
Map.entry("RECONCILIATION_CREATED", "生成对账单"),
Map.entry("SETTLEMENT_ISSUED", "结算单生效"),
Map.entry("RECEIPT", "收款"),
Map.entry("PAYMENT", "付款"),
Map.entry("SUPPLIER_DIRECT_PAID", "供应商线下直付"),
Map.entry("SUPPLIER_ON_ACCOUNT_PAYMENT", "供应商账期付款"),
Map.entry("ADJUSTMENT", "账务调整"),
Map.entry("VOID", "作废"),
Map.entry("BILL", "对账单"),
Map.entry("COST", "成本"),
Map.entry("SETTLEMENT", "结算"),
Map.entry("CREATE", "创建"),
Map.entry("APPROVE", "审核通过"),
Map.entry("REJECT", "审核驳回"),
Map.entry("UPDATE", "更新"),
Map.entry("FEE_SOURCE_EDIT", "编辑费用"),
Map.entry("FEE_SOURCE_SUBMIT", "提交审核"),
Map.entry("FEE_SOURCE_REVIEW_APPROVE", "审核通过"),
Map.entry("FEE_SOURCE_REVIEW_REJECT", "审核驳回"),
Map.entry("FEE_SOURCE_ADJUST", "调整费用"),
Map.entry("FEE_SOURCE_VOID", "作废费用"),
Map.entry("FEE_SOURCE_PUBLISHED", "费用同步"),
Map.entry("FEE_SOURCE_VERSION_ADJUST", "费用调整同步"),
Map.entry("RECONCILIATION_CREATE", "生成对账单"),
Map.entry("RECONCILIATION_ADJUST", "调整对账单"),
Map.entry("BILL_CREATE", "创建账单"),
Map.entry("BILL_ISSUE", "出账"),
Map.entry("BILL_ADJUST", "调整账单"),
Map.entry("BILL_VOID", "作废账单"),
Map.entry("SETTLEMENT_AR_CREATE", "生成应收结算单"),
Map.entry("SETTLEMENT_AP_CREATE", "生成应付结算单"),
Map.entry("SETTLEMENT_PAYMENT_CREATE", "登记收付款"),
Map.entry("RECEIPT_CREATE", "登记收款"),
Map.entry("COST_CREATE", "登记成本"),
Map.entry("REOPEN", "重开"));
/** 转换账户、审核及结算状态。 */
static String status(String value) { return label(value, STATUSES, "未知状态"); }
/** 转换费用来源类型。 */
static String source(String value) { return label(value, SOURCES, "未知来源类型"); }
/** 转换账务及资金流水业务类型。 */
static String business(String value) { return label(value, BUSINESSES, "未知业务类型"); }
/** 转换应收应付方向。 */
static String direction(String value) { return label(value, Map.of("RECEIVABLE", "应收", "PAYABLE", "应付"), "未知方向"); }
/** 空值保持空白,已是中文的历史快照保持原文;新增英文枚举不直接泄露到业务报表。 */
private static String label(String value, Map<String, String> labels, String fallback) {
if (value == null || value.isBlank()) return value;
String code = value.trim().toUpperCase(Locale.ROOT);
return labels.getOrDefault(code, value.matches(".*[一-鿿].*") ? value : fallback);
}
}
@@ -339,35 +339,39 @@ public class FinanceExportProviderService {
} catch (GeneralSecurityException exception) { throw new IllegalStateException("HmacSHA256 is unavailable", exception); }
}
/** 导出账户摘要,状态使用中文展示。 */
private Map<String, Object> accountRow(AccountSummary value) {
return row("customerCode", value.customerCode(), "customerName", value.customerName(), "contactName", value.contactName(),
"contactPhone", value.contactPhone(), "outstandingReceivable", value.outstandingReceivable(), "pendingBilling", value.pendingBilling(),
"receivedInPeriod", value.receivedInPeriod(), "totalCost", value.totalCost(), "paidCost", value.paidCost(),
"outstandingPayable", value.outstandingPayable(), "status", value.status(), "lastChangedAt", value.lastChangedAt());
"outstandingPayable", value.outstandingPayable(), "status", FinanceExportLabels.status(value.status()), "lastChangedAt", value.lastChangedAt());
}
/** 导出账户流水,转换业务类型并保留金额和编号。 */
private Map<String, Object> ledgerRow(LedgerEntry value) {
return row("ledgerId", String.valueOf(value.ledgerId()), "occurredAt", value.occurredAt(), "businessType", value.businessType(),
return row("ledgerId", String.valueOf(value.ledgerId()), "occurredAt", value.occurredAt(), "businessType", FinanceExportLabels.business(value.businessType()),
"documentNo", value.documentNo(), "receivableChange", value.receivableChange(), "payableChange", value.payableChange(),
"outstandingReceivableAfter", value.outstandingReceivableAfter(), "operatorName", value.operatorName(), "remark", value.remark());
}
/** 费用方向、来源类型及审核和结算状态统一展示中文。 */
private Map<String, Object> feeRow(FeeSourceSummary source) {
return row("customerName", source.customerName(), "direction", source.direction(), "sourceType", source.sourceType(),
return row("customerName", source.customerName(), "direction", FinanceExportLabels.direction(source.direction()), "sourceType", FinanceExportLabels.source(source.sourceType()),
"sourceFeeId", String.valueOf(source.sourceFeeId()), "sourceVersion", source.sourceVersion(), "inboundCode", source.inboundCode(),
"containerNo", source.containerNo(), "businessDate", source.businessDate(), "feeItemName", source.feeItemName(),
"quantity", source.quantity(), "unitPrice", source.unitPrice(), "amount", source.amount(),
"reviewStatus", source.reviewStatus(), "sourceStatus", source.sourceStatus(), "statementGenerated", source.statementGenerated());
"reviewStatus", FinanceExportLabels.status(source.reviewStatus()), "sourceStatus", FinanceExportLabels.status(source.sourceStatus()), "statementGenerated", source.statementGenerated());
}
/** 对账单导出沿用页面状态口径,是否结清仍传递布尔值。 */
private Map<String, Object> statementRow(ReconciliationDetail value) {
return row("statementNo", value.statementNo(), "customerName", value.customerName(), "periodStart", value.periodStart(),
"periodEnd", value.periodEnd(), "creditDays", value.creditDays(), "currency", value.currency(), "totalReceivable", value.totalReceivable(),
"receivedAmount", value.receivedAmount(), "outstandingAmount", value.outstandingAmount(), "settled", value.settled(), "status", value.status(),
"receivedAmount", value.receivedAmount(), "outstandingAmount", value.outstandingAmount(), "settled", value.settled(), "status", FinanceExportLabels.status(value.status()),
"remark", value.remark(), "createdByName", value.createdByName(), "createdAt", value.createdAt(), "updatedByName", value.updatedByName(), "updatedAt", value.updatedAt());
}
/** 结算单列表导出使用中文状态,便于财务人员直接阅读。 */
private Map<String, Object> settlementRow(SettlementSummary value) {
return row("settlementNo", value.settlementNo(), "customerName", value.customerName(), "supplierName", value.supplierName(),
"currency", value.currency(), "totalAmount", value.totalAmount(), "paidAmount", value.paidAmount(), "outstandingAmount", value.outstandingAmount(),
"businessPeriod", value.businessPeriod(), "generatedDate", value.generatedDate(), "dueDate", value.dueDate(), "status", settlementStatusLabel(value.status()));
"businessPeriod", value.businessPeriod(), "generatedDate", value.generatedDate(), "dueDate", value.dueDate(), "status", FinanceExportLabels.status(value.status()));
}
/** 明细导出与列表保持相同的中文结算状态。 */
private Map<String, Object> settlementDetailRow(SettlementSummary summary, SettlementLine line) {
@@ -375,18 +379,7 @@ public class FinanceExportProviderService {
"feeItemName", line == null ? null : line.feeItemName(), "inboundCode", line == null ? null : line.inboundCode(),
"containerNo", line == null ? null : line.containerNo(), "businessDate", line == null ? null : line.businessDate(),
"lineAmount", line == null ? null : line.amount(), "totalAmount", summary.totalAmount(), "paidAmount", summary.paidAmount(),
"outstandingAmount", summary.outstandingAmount(), "status", settlementStatusLabel(summary.status()));
}
/** 转换正式结算状态,未知值保留原值以便排查。 */
private String settlementStatusLabel(String status) {
if (status == null) return null;
return switch (status) {
case "ISSUED" -> "已出账";
case "PARTIALLY_PAID" -> "部分结清";
case "PAID" -> "已结清";
case "VOIDED" -> "已作废";
default -> status;
};
"outstandingAmount", summary.outstandingAmount(), "status", FinanceExportLabels.status(summary.status()));
}
private Map<String, Object> receivableStatisticsRow(YearMonth month, ReceivableStatisticsRow value) {
return row("statisticsMonth", month.toString(), "customerName", value.customerName(),
@@ -0,0 +1,34 @@
package com.baiyue.finance.service;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class FinanceExportLabelsTest {
@Test void translatesStatusSourceAndLedgerFamiliesWithoutLeakingCodes() {
assertThat(FinanceExportLabels.status("PARTIALLY_PAID")).isEqualTo("部分结清");
assertThat(FinanceExportLabels.status("SETTLED")).isEqualTo("已结算");
for (String code : List.of("ACTIVE", "DISABLED", "DRAFT", "PENDING", "PENDING_REVIEW", "APPROVED", "REJECTED", "ISSUED", "PARTIALLY_PAID", "PAID", "VOIDED", "AVAILABLE", "LOCKED", "BILLED")) {
assertThat(FinanceExportLabels.status(code)).matches(".*[一-鿿].*").doesNotContain("未知");
}
for (String code : List.of("INBOUND_FEE", "ORDER_FEE", "CONTAINER_FEE", "CONTAINER_COST", "MANUAL_COST", "STORAGE_FEE", "OVERSEAS_INBOUND_FEE", "OVERSEAS_PROCUREMENT_FEE", "OVERSEAS_TRUCK_FEE", "OVERSEAS_LOOSEBOARD_FEE", "OVERSEAS_WAREHOUSE_FEE", "OVERSEAS_CONTAINER_FEE", "OVERSEAS_ORDER_FEE", "OVERSEAS_DELIVERY_FEE", "OVERSEAS_RELABEL_FEE")) {
assertThat(FinanceExportLabels.source(code)).matches(".*[一-鿿].*").doesNotContain("未知");
}
for (String code : List.of("FEE_SOURCE_EDIT", "FEE_SOURCE_SUBMIT", "FEE_SOURCE_REVIEW_APPROVE", "FEE_SOURCE_REVIEW_REJECT", "FEE_SOURCE_ADJUST", "FEE_SOURCE_VOID", "RECONCILIATION_CREATED", "SETTLEMENT_ISSUED", "RECEIPT", "PAYMENT", "SUPPLIER_DIRECT_PAID", "SUPPLIER_ON_ACCOUNT_PAYMENT", "ADJUSTMENT", "VOID")) {
assertThat(FinanceExportLabels.business(code)).matches(".*[一-鿿].*").doesNotContain("未知");
}
assertThat(FinanceExportLabels.direction("RECEIVABLE")).isEqualTo("应收");
for (String code : List.of("RECHARGE", "DEDUCTION", "CREDIT_LIMIT", "FREEZE", "UNFREEZE", "CONSUMPTION", "REFUND")) {
assertThat(FinanceExportLabels.business(code)).matches(".*[一-鿿].*").doesNotContain("未知");
}
assertThat(FinanceExportLabels.direction("PAYABLE")).isEqualTo("应付");
}
@Test void handlesEmptyChineseSnapshotsAndUnknownValues() {
assertThat(FinanceExportLabels.status(null)).isNull();
assertThat(FinanceExportLabels.status("")).isEmpty();
assertThat(FinanceExportLabels.status("已结清")).isEqualTo("已结清");
assertThat(FinanceExportLabels.status("NEW_INTERNAL_STATUS")).isEqualTo("未知状态");
assertThat(FinanceExportLabels.status(" partially_paid ")).isEqualTo("部分结清");
}
}
@@ -45,6 +45,19 @@ class FinanceExportProviderServiceTest {
service.initializeSigningKey();
}
@Test
void exportsAccountAndLedgerEnumsAsChineseWithoutChangingFiltersOrReferences() throws Exception {
var mapper = new ObjectMapper().findAndRegisterModules();
var account = mapper.convertValue(Map.of("customerId", 10001L, "customerCode", "C-10001", "status", "ACTIVE"), com.baiyue.finance.dto.FinancePhase1Dtos.AccountSummary.class);
var ledger = mapper.convertValue(Map.of("ledgerId", 1L, "businessType", "SUPPLIER_ON_ACCOUNT_PAYMENT", "documentNo", "AP-1"), com.baiyue.finance.dto.FinancePhase1Dtos.LedgerEntry.class);
when(accountService.list(1, 100, null, "ACTIVE", 10001L)).thenReturn(PageEnvelope.success(List.of(account), 1, 100, 1));
when(operationService.listDerivedCustomerAccountLedger(10001L, 1, 100, "SUPPLIER_ON_ACCOUNT_PAYMENT", null, null)).thenReturn(PageEnvelope.success(List.of(ledger), 1, 100, 1));
var accounts = service.exportBatch(FinanceExportProviderService.CUSTOMER_ACCOUNT_LIST, request(Map.of("customerId", "10001", "status", "ACTIVE"), null, null));
var ledgers = service.exportBatch(FinanceExportProviderService.CUSTOMER_ACCOUNT_LEDGER, request(Map.of("customerId", "10001", "businessType", "SUPPLIER_ON_ACCOUNT_PAYMENT"), null, null));
assertThat(accounts.rows().get(0)).containsEntry("status", "正常").containsEntry("customerCode", "C-10001");
assertThat(ledgers.rows().get(0)).containsEntry("businessType", "供应商账期付款").containsEntry("documentNo", "AP-1");
}
@Test
/** 应付结算列表和详情导出均使用中文状态,包括无明细的单据。 */
void exportsSettlementStatusesInChinese() {
@@ -98,7 +111,7 @@ class FinanceExportProviderServiceTest {
assertThat(first.columns()).isNotEmpty();
assertThat(first.rows()).hasSize(1);
assertThat(first.rows().get(0)).containsEntry("statementNo", "REC-501").containsEntry("customerName", "客户A");
assertThat(first.rows().get(0)).containsEntry("statementNo", "REC-501").containsEntry("customerName", "客户A").containsEntry("status", "已出账");
assertThat(first.totalRows()).isEqualTo(1L);
assertThat(continued.columns()).isEmpty();
assertThat(continued.rows()).isEmpty();
@@ -119,7 +132,7 @@ class FinanceExportProviderServiceTest {
var result = service.exportBatch(FinanceExportProviderService.FEE_SOURCE_LIST,
request(Map.of("direction", "PAYABLE", "status", "PAID"), null, null));
assertThat(result.rows()).singleElement().satisfies(row -> {
assertThat(row).containsEntry("sourceStatus", "PAID").containsEntry("sourceFeeId", "9001");
assertThat(row).containsEntry("sourceStatus", "已结清").containsEntry("reviewStatus", "已审核").containsEntry("direction", "应付").containsEntry("sourceType", "装柜费用").containsEntry("sourceFeeId", "9001");
assertThat(row.get("amount")).isEqualTo(new BigDecimal("300"));
});
org.mockito.Mockito.verifyNoInteractions(queryService);