Merge branch 'test' of http://8.163.30.128:10082/java-project/baiyue-firstleg into test
This commit is contained in:
@@ -3093,7 +3093,7 @@ CREATE TABLE fl_loading_cabinet_evidence (
|
|||||||
seal_no varchar(64) DEFAULT NULL COMMENT '带锁码照对应锁码',
|
seal_no varchar(64) DEFAULT NULL COMMENT '带锁码照对应锁码',
|
||||||
remark varchar(1000) DEFAULT NULL COMMENT '证据备注',
|
remark varchar(1000) DEFAULT NULL COMMENT '证据备注',
|
||||||
status varchar(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
status varchar(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
||||||
active_flag tinyint unsigned DEFAULT '1' COMMENT '有效记录为1,作废记录为NULL,保证每柜每类型仅一条有效事实',
|
active_flag tinyint unsigned DEFAULT '1' COMMENT '有效记录为1,作废记录为NULL,同类允许多张有效照片',
|
||||||
operation_key varchar(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
operation_key varchar(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
||||||
request_hash char(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
request_hash char(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
||||||
void_reason varchar(500) DEFAULT NULL COMMENT '作废原因',
|
void_reason varchar(500) DEFAULT NULL COMMENT '作废原因',
|
||||||
@@ -3109,7 +3109,7 @@ CREATE TABLE fl_loading_cabinet_evidence (
|
|||||||
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
||||||
UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
KEY idx_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
||||||
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
||||||
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
||||||
CONSTRAINT fk_cabinet_evidence_booking FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking (id),
|
CONSTRAINT fk_cabinet_evidence_booking FOREIGN KEY (container_booking_id) REFERENCES fl_container_booking (id),
|
||||||
|
|||||||
@@ -174,7 +174,8 @@ public class GenericXlsxExportRunner {
|
|||||||
case STRING -> cell.setCellValue(safeString(String.valueOf(value), column.key(), warnedTruncatedColumns));
|
case STRING -> cell.setCellValue(safeString(String.valueOf(value), column.key(), warnedTruncatedColumns));
|
||||||
case INTEGER -> cell.setCellValue(new BigDecimal(String.valueOf(value)).longValueExact());
|
case INTEGER -> cell.setCellValue(new BigDecimal(String.valueOf(value)).longValueExact());
|
||||||
case DECIMAL -> cell.setCellValue(new BigDecimal(String.valueOf(value)).doubleValue());
|
case DECIMAL -> cell.setCellValue(new BigDecimal(String.valueOf(value)).doubleValue());
|
||||||
case BOOLEAN -> cell.setCellValue(value instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(value)));
|
// 面向业务人员的是否字段统一显示中文,Provider 仍传输布尔类型。
|
||||||
|
case BOOLEAN -> cell.setCellValue((value instanceof Boolean b ? b : Boolean.parseBoolean(String.valueOf(value))) ? "是" : "否");
|
||||||
case DATE -> { cell.setCellValue(LocalDate.parse(String.valueOf(value))); cell.setCellStyle(styles.date); }
|
case DATE -> { cell.setCellValue(LocalDate.parse(String.valueOf(value))); cell.setCellStyle(styles.date); }
|
||||||
case DATETIME -> { cell.setCellValue(LocalDateTime.parse(String.valueOf(value))); cell.setCellStyle(styles.dateTime); }
|
case DATETIME -> { cell.setCellValue(LocalDateTime.parse(String.valueOf(value))); cell.setCellStyle(styles.dateTime); }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class GenericXlsxExportRunnerTest {
|
|||||||
}
|
}
|
||||||
@Test void writesRealTypedWorkbookAndCarriesSnapshotAcrossBatches() throws Exception {
|
@Test void writesRealTypedWorkbookAndCarriesSnapshotAcrossBatches() throws Exception {
|
||||||
List<ExportColumn> columns=List.of(new ExportColumn("text","文本",ExportColumnType.STRING,18),new ExportColumn("integer","整数",ExportColumnType.INTEGER,12),new ExportColumn("decimal","金额",ExportColumnType.DECIMAL,12),new ExportColumn("flag","布尔",ExportColumnType.BOOLEAN,10),new ExportColumn("date","日期",ExportColumnType.DATE,12),new ExportColumn("time","时间",ExportColumnType.DATETIME,20));
|
List<ExportColumn> columns=List.of(new ExportColumn("text","文本",ExportColumnType.STRING,18),new ExportColumn("integer","整数",ExportColumnType.INTEGER,12),new ExportColumn("decimal","金额",ExportColumnType.DECIMAL,12),new ExportColumn("flag","布尔",ExportColumnType.BOOLEAN,10),new ExportColumn("date","日期",ExportColumnType.DATE,12),new ExportColumn("time","时间",ExportColumnType.DATETIME,20));
|
||||||
List<Map<String,Object>> firstRows=IntStream.range(0,100).mapToObj(index -> Map.<String,Object>of("text",index==0?"=SUM(1,2)":"row-"+index,"integer",12,"decimal","3.50","flag",true,"date","2026-08-03","time","2026-08-03T21:00:00")).toList();
|
List<Map<String,Object>> firstRows=IntStream.range(0,100).mapToObj(index -> Map.<String,Object>of("text",index==0?"=SUM(1,2)":"row-"+index,"integer",12,"decimal","3.50","flag",index % 2 == 0,"date","2026-08-03","time","2026-08-03T21:00:00")).toList();
|
||||||
var first=new ExportBatchResponse(1,"sha256:test","snapshot-9",columns,firstRows,"10",true,101L);
|
var first=new ExportBatchResponse(1,"sha256:test","snapshot-9",columns,firstRows,"10",true,101L);
|
||||||
var second=new ExportBatchResponse(1,"sha256:test","snapshot-9",null,List.of(Map.of("text","\n=SUM(1,2)","integer",13,"decimal",4.5,"flag",false,"date","2026-08-04","time","2026-08-04T21:00:00")),null,false,101L);
|
var second=new ExportBatchResponse(1,"sha256:test","snapshot-9",null,List.of(Map.of("text","\n=SUM(1,2)","integer",13,"decimal",4.5,"flag",false,"date","2026-08-04","time","2026-08-04T21:00:00")),null,false,101L);
|
||||||
when(provider.batch(anyString(),eq("CUSTOMER_LIST"),any(),any())).thenReturn(ApiResponse.success(first),ApiResponse.success(second));
|
when(provider.batch(anyString(),eq("CUSTOMER_LIST"),any(),any())).thenReturn(ApiResponse.success(first),ApiResponse.success(second));
|
||||||
@@ -58,7 +58,7 @@ class GenericXlsxExportRunnerTest {
|
|||||||
assertEquals(101,result.rows());
|
assertEquals(101,result.rows());
|
||||||
try(var workbook=new XSSFWorkbook(storage.load(result.objectKey()).getInputStream())){
|
try(var workbook=new XSSFWorkbook(storage.load(result.objectKey()).getInputStream())){
|
||||||
var row=workbook.getSheetAt(0).getRow(1);assertEquals("'=SUM(1,2)",row.getCell(0).getStringCellValue());
|
var row=workbook.getSheetAt(0).getRow(1);assertEquals("'=SUM(1,2)",row.getCell(0).getStringCellValue());
|
||||||
assertEquals(CellType.NUMERIC,row.getCell(1).getCellType());assertEquals(CellType.NUMERIC,row.getCell(2).getCellType());assertEquals(CellType.BOOLEAN,row.getCell(3).getCellType());assertEquals(CellType.NUMERIC,row.getCell(4).getCellType());assertEquals(CellType.NUMERIC,row.getCell(5).getCellType());
|
assertEquals(CellType.NUMERIC,row.getCell(1).getCellType());assertEquals(CellType.NUMERIC,row.getCell(2).getCellType());assertEquals(CellType.STRING,row.getCell(3).getCellType());assertEquals("是",row.getCell(3).getStringCellValue());assertEquals("否",workbook.getSheetAt(0).getRow(2).getCell(3).getStringCellValue());assertEquals(CellType.NUMERIC,row.getCell(4).getCellType());assertEquals(CellType.NUMERIC,row.getCell(5).getCellType());
|
||||||
assertEquals("'\n=SUM(1,2)",workbook.getSheetAt(0).getRow(101).getCell(0).getStringCellValue());
|
assertEquals("'\n=SUM(1,2)",workbook.getSheetAt(0).getRow(101).getCell(0).getStringCellValue());
|
||||||
assertTrue(workbook.getNumCellStyles()<10,"日期样式必须复用,不能按单元格创建");
|
assertTrue(workbook.getNumCellStyles()<10,"日期样式必须复用,不能按单元格创建");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
-18
@@ -339,35 +339,39 @@ public class FinanceExportProviderService {
|
|||||||
} catch (GeneralSecurityException exception) { throw new IllegalStateException("HmacSHA256 is unavailable", exception); }
|
} catch (GeneralSecurityException exception) { throw new IllegalStateException("HmacSHA256 is unavailable", exception); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 导出账户摘要,状态使用中文展示。 */
|
||||||
private Map<String, Object> accountRow(AccountSummary value) {
|
private Map<String, Object> accountRow(AccountSummary value) {
|
||||||
return row("customerCode", value.customerCode(), "customerName", value.customerName(), "contactName", value.contactName(),
|
return row("customerCode", value.customerCode(), "customerName", value.customerName(), "contactName", value.contactName(),
|
||||||
"contactPhone", value.contactPhone(), "outstandingReceivable", value.outstandingReceivable(), "pendingBilling", value.pendingBilling(),
|
"contactPhone", value.contactPhone(), "outstandingReceivable", value.outstandingReceivable(), "pendingBilling", value.pendingBilling(),
|
||||||
"receivedInPeriod", value.receivedInPeriod(), "totalCost", value.totalCost(), "paidCost", value.paidCost(),
|
"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) {
|
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(),
|
"documentNo", value.documentNo(), "receivableChange", value.receivableChange(), "payableChange", value.payableChange(),
|
||||||
"outstandingReceivableAfter", value.outstandingReceivableAfter(), "operatorName", value.operatorName(), "remark", value.remark());
|
"outstandingReceivableAfter", value.outstandingReceivableAfter(), "operatorName", value.operatorName(), "remark", value.remark());
|
||||||
}
|
}
|
||||||
|
/** 费用方向、来源类型及审核和结算状态统一展示中文。 */
|
||||||
private Map<String, Object> feeRow(FeeSourceSummary source) {
|
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(),
|
"sourceFeeId", String.valueOf(source.sourceFeeId()), "sourceVersion", source.sourceVersion(), "inboundCode", source.inboundCode(),
|
||||||
"containerNo", source.containerNo(), "businessDate", source.businessDate(), "feeItemName", source.feeItemName(),
|
"containerNo", source.containerNo(), "businessDate", source.businessDate(), "feeItemName", source.feeItemName(),
|
||||||
"quantity", source.quantity(), "unitPrice", source.unitPrice(), "amount", source.amount(),
|
"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) {
|
private Map<String, Object> statementRow(ReconciliationDetail value) {
|
||||||
return row("statementNo", value.statementNo(), "customerName", value.customerName(), "periodStart", value.periodStart(),
|
return row("statementNo", value.statementNo(), "customerName", value.customerName(), "periodStart", value.periodStart(),
|
||||||
"periodEnd", value.periodEnd(), "creditDays", value.creditDays(), "currency", value.currency(), "totalReceivable", value.totalReceivable(),
|
"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());
|
"remark", value.remark(), "createdByName", value.createdByName(), "createdAt", value.createdAt(), "updatedByName", value.updatedByName(), "updatedAt", value.updatedAt());
|
||||||
}
|
}
|
||||||
/** 结算单列表导出使用中文状态,便于财务人员直接阅读。 */
|
/** 结算单列表导出使用中文状态,便于财务人员直接阅读。 */
|
||||||
private Map<String, Object> settlementRow(SettlementSummary value) {
|
private Map<String, Object> settlementRow(SettlementSummary value) {
|
||||||
return row("settlementNo", value.settlementNo(), "customerName", value.customerName(), "supplierName", value.supplierName(),
|
return row("settlementNo", value.settlementNo(), "customerName", value.customerName(), "supplierName", value.supplierName(),
|
||||||
"currency", value.currency(), "totalAmount", value.totalAmount(), "paidAmount", value.paidAmount(), "outstandingAmount", value.outstandingAmount(),
|
"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) {
|
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(),
|
"feeItemName", line == null ? null : line.feeItemName(), "inboundCode", line == null ? null : line.inboundCode(),
|
||||||
"containerNo", line == null ? null : line.containerNo(), "businessDate", line == null ? null : line.businessDate(),
|
"containerNo", line == null ? null : line.containerNo(), "businessDate", line == null ? null : line.businessDate(),
|
||||||
"lineAmount", line == null ? null : line.amount(), "totalAmount", summary.totalAmount(), "paidAmount", summary.paidAmount(),
|
"lineAmount", line == null ? null : line.amount(), "totalAmount", summary.totalAmount(), "paidAmount", summary.paidAmount(),
|
||||||
"outstandingAmount", summary.outstandingAmount(), "status", settlementStatusLabel(summary.status()));
|
"outstandingAmount", summary.outstandingAmount(), "status", FinanceExportLabels.status(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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
private Map<String, Object> receivableStatisticsRow(YearMonth month, ReceivableStatisticsRow value) {
|
private Map<String, Object> receivableStatisticsRow(YearMonth month, ReceivableStatisticsRow value) {
|
||||||
return row("statisticsMonth", month.toString(), "customerName", value.customerName(),
|
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("部分结清");
|
||||||
|
}
|
||||||
|
}
|
||||||
+15
-2
@@ -45,6 +45,19 @@ class FinanceExportProviderServiceTest {
|
|||||||
service.initializeSigningKey();
|
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
|
@Test
|
||||||
/** 应付结算列表和详情导出均使用中文状态,包括无明细的单据。 */
|
/** 应付结算列表和详情导出均使用中文状态,包括无明细的单据。 */
|
||||||
void exportsSettlementStatusesInChinese() {
|
void exportsSettlementStatusesInChinese() {
|
||||||
@@ -98,7 +111,7 @@ class FinanceExportProviderServiceTest {
|
|||||||
|
|
||||||
assertThat(first.columns()).isNotEmpty();
|
assertThat(first.columns()).isNotEmpty();
|
||||||
assertThat(first.rows()).hasSize(1);
|
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(first.totalRows()).isEqualTo(1L);
|
||||||
assertThat(continued.columns()).isEmpty();
|
assertThat(continued.columns()).isEmpty();
|
||||||
assertThat(continued.rows()).isEmpty();
|
assertThat(continued.rows()).isEmpty();
|
||||||
@@ -119,7 +132,7 @@ class FinanceExportProviderServiceTest {
|
|||||||
var result = service.exportBatch(FinanceExportProviderService.FEE_SOURCE_LIST,
|
var result = service.exportBatch(FinanceExportProviderService.FEE_SOURCE_LIST,
|
||||||
request(Map.of("direction", "PAYABLE", "status", "PAID"), null, null));
|
request(Map.of("direction", "PAYABLE", "status", "PAID"), null, null));
|
||||||
assertThat(result.rows()).singleElement().satisfies(row -> {
|
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"));
|
assertThat(row.get("amount")).isEqualTo(new BigDecimal("300"));
|
||||||
});
|
});
|
||||||
org.mockito.Mockito.verifyNoInteractions(queryService);
|
org.mockito.Mockito.verifyNoInteractions(queryService);
|
||||||
|
|||||||
+3
@@ -254,6 +254,9 @@ public class ContainerCustomsService {
|
|||||||
throw conflict("当前柜尚未完成首次发送报关行资料");
|
throw conflict("当前柜尚未完成首次发送报关行资料");
|
||||||
}
|
}
|
||||||
CompletionRow completion = mapper.lockCompletion(documentId);
|
CompletionRow completion = mapper.lockCompletion(documentId);
|
||||||
|
if (completion != null && "CONFIRMED".equals(completion.customerConfirmStatus())) {
|
||||||
|
throw conflict("客户已确认预录单,不可再次上传");
|
||||||
|
}
|
||||||
CustomsFileClient.Metadata metadata = upload(source.tenantId(), RECEIPT_FILE_TYPE, Long.toString(container.loadingPlanId()), operatorId, file);
|
CustomsFileClient.Metadata metadata = upload(source.tenantId(), RECEIPT_FILE_TYPE, Long.toString(container.loadingPlanId()), operatorId, file);
|
||||||
long completionId;
|
long completionId;
|
||||||
int version;
|
int version;
|
||||||
|
|||||||
+1
-1
@@ -14,7 +14,7 @@ public interface CabinetLoadingEvidenceMapper extends BaseMapper<CabinetLoadingE
|
|||||||
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} ORDER BY evidence_type,id")
|
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} ORDER BY evidence_type,id")
|
||||||
List<CabinetLoadingEvidenceEntity> byContainer(@Param("containerBookingId") long containerBookingId);
|
List<CabinetLoadingEvidenceEntity> byContainer(@Param("containerBookingId") long containerBookingId);
|
||||||
|
|
||||||
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} AND evidence_type=#{type} AND active_flag=1 LIMIT 1 FOR UPDATE")
|
@Select("SELECT * FROM fl_loading_cabinet_evidence WHERE container_booking_id=#{containerBookingId} AND evidence_type=#{type} AND active_flag=1 ORDER BY id DESC LIMIT 1 FOR UPDATE")
|
||||||
CabinetLoadingEvidenceEntity activeForUpdate(@Param("containerBookingId") long containerBookingId,
|
CabinetLoadingEvidenceEntity activeForUpdate(@Param("containerBookingId") long containerBookingId,
|
||||||
@Param("type") String type);
|
@Param("type") String type);
|
||||||
|
|
||||||
|
|||||||
+1
-4
@@ -74,8 +74,6 @@ public class CabinetLoadingEvidenceService {
|
|||||||
if (current != null && !Objects.equals(current.getVersion(), expected)) throw conflict("装柜证据版本已变化,请刷新后重试");
|
if (current != null && !Objects.equals(current.getVersion(), expected)) throw conflict("装柜证据版本已变化,请刷新后重试");
|
||||||
if (current != null && Objects.equals(current.getFileId(), request.fileId())
|
if (current != null && Objects.equals(current.getFileId(), request.fileId())
|
||||||
&& Objects.equals(current.getSealNo(), sealNo)) return view(scope);
|
&& Objects.equals(current.getSealNo(), sealNo)) return view(scope);
|
||||||
if (current != null && mapper.voidActive(current.getId(), scope.containerBookingId(), current.getVersion(),
|
|
||||||
"REPLACED", operatorId, operatorName) != 1) throw conflict("装柜证据版本已变化,请刷新后重试");
|
|
||||||
CabinetLoadingEvidenceEntity value = new CabinetLoadingEvidenceEntity();
|
CabinetLoadingEvidenceEntity value = new CabinetLoadingEvidenceEntity();
|
||||||
value.setContainerBookingId(scope.containerBookingId()); value.setSourceTaskId(taskId);
|
value.setContainerBookingId(scope.containerBookingId()); value.setSourceTaskId(taskId);
|
||||||
value.setEvidenceType(type); value.setFileId(request.fileId()); value.setFileName(file.fileName());
|
value.setEvidenceType(type); value.setFileId(request.fileId()); value.setFileName(file.fileName());
|
||||||
@@ -84,8 +82,7 @@ public class CabinetLoadingEvidenceService {
|
|||||||
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
value.setCreatedBy(operatorId); value.setCreatedByName(operatorName);
|
||||||
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName);
|
value.setUpdatedBy(operatorId); value.setUpdatedByName(operatorName);
|
||||||
try { mapper.insert(value); } catch (DuplicateKeyException exception) {
|
try { mapper.insert(value); } catch (DuplicateKeyException exception) {
|
||||||
// A replacement may already have voided the old row in this transaction.
|
// The task-scoped operation key protects concurrent duplicate uploads.
|
||||||
// Roll it back; a subsequent same-key request can read the committed winner.
|
|
||||||
throw conflict("装柜证据发生并发变更,请刷新后重试");
|
throw conflict("装柜证据发生并发变更,请刷新后重试");
|
||||||
}
|
}
|
||||||
return view(scope);
|
return view(scope);
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- Existing databases: allow multiple active photos per evidence type. No photo rows are changed.
|
||||||
|
USE baiyue_firstleg;
|
||||||
|
SET @evidence_ddl = IF(EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
|
||||||
|
AND table_name='fl_loading_cabinet_evidence' AND index_name='uk_cabinet_evidence_active'
|
||||||
|
), 'ALTER TABLE fl_loading_cabinet_evidence DROP INDEX uk_cabinet_evidence_active', 'SELECT 1');
|
||||||
|
PREPARE evidence_ddl FROM @evidence_ddl;
|
||||||
|
EXECUTE evidence_ddl;
|
||||||
|
DEALLOCATE PREPARE evidence_ddl;
|
||||||
|
SET @evidence_ddl = IF(EXISTS(
|
||||||
|
SELECT 1 FROM information_schema.statistics WHERE table_schema=DATABASE()
|
||||||
|
AND table_name='fl_loading_cabinet_evidence' AND index_name='idx_cabinet_evidence_active'
|
||||||
|
), 'SELECT 1', 'ALTER TABLE fl_loading_cabinet_evidence ADD INDEX idx_cabinet_evidence_active (container_booking_id,evidence_type,active_flag)');
|
||||||
|
PREPARE evidence_ddl FROM @evidence_ddl;
|
||||||
|
EXECUTE evidence_ddl;
|
||||||
|
DEALLOCATE PREPARE evidence_ddl;
|
||||||
@@ -11,7 +11,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence (
|
|||||||
seal_no VARCHAR(64) NULL COMMENT '带锁码照对应锁码',
|
seal_no VARCHAR(64) NULL COMMENT '带锁码照对应锁码',
|
||||||
remark VARCHAR(1000) NULL COMMENT '证据备注',
|
remark VARCHAR(1000) NULL COMMENT '证据备注',
|
||||||
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
status VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT '证据状态:ACTIVE有效、VOID作废',
|
||||||
active_flag TINYINT UNSIGNED NULL DEFAULT 1 COMMENT '有效唯一标识;作废后置NULL',
|
active_flag TINYINT UNSIGNED NULL DEFAULT 1 COMMENT '有效标识;同类型允许多张,作废后置NULL',
|
||||||
operation_key VARCHAR(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
operation_key VARCHAR(128) NOT NULL COMMENT '任务范围内幂等操作键',
|
||||||
request_hash CHAR(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
request_hash CHAR(64) NOT NULL COMMENT '规范化请求SHA-256摘要',
|
||||||
void_reason VARCHAR(500) NULL COMMENT '作废原因',
|
void_reason VARCHAR(500) NULL COMMENT '作废原因',
|
||||||
@@ -26,7 +26,7 @@ CREATE TABLE IF NOT EXISTS fl_loading_cabinet_evidence (
|
|||||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
|
||||||
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后更新时间',
|
||||||
PRIMARY KEY (id),
|
PRIMARY KEY (id),
|
||||||
UNIQUE KEY uk_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
KEY idx_cabinet_evidence_active (container_booking_id,evidence_type,active_flag),
|
||||||
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
UNIQUE KEY uk_cabinet_evidence_operation (source_task_id,operation_key),
|
||||||
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
KEY idx_cabinet_evidence_container (container_booking_id,status),
|
||||||
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
KEY idx_cabinet_evidence_seal (seal_no,active_flag),
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ paths:
|
|||||||
responses: {'200': {description: 仅platform:firstleg:pda:work且任务必须由当前用户领取}}
|
responses: {'200': {description: 仅platform:firstleg:pda:work且任务必须由当前用户领取}}
|
||||||
post:
|
post:
|
||||||
summary: 通过Firstleg代理上传并登记当前柜次装柜证据
|
summary: 通过Firstleg代理上传并登记当前柜次装柜证据
|
||||||
|
description: 同类型照片追加保留,不替换已有有效照片。expectedVersion取当前同类型最新有效照片版本,无照片时为0;重复幂等键不追加。
|
||||||
parameters:
|
parameters:
|
||||||
- {$ref: '#/components/parameters/TaskId'}
|
- {$ref: '#/components/parameters/TaskId'}
|
||||||
- {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
- {name: Idempotency-Key, in: header, required: true, schema: {type: string, maxLength: 128}}
|
||||||
|
|||||||
+13
@@ -304,6 +304,19 @@ class ContainerCustomsServiceTest {
|
|||||||
verify(mapper,never()).insertBrokerSubmission(anyLong(),anyLong(),anyLong(),anyString(),anyInt(),anyString(),anyLong(),anyString());
|
verify(mapper,never()).insertBrokerSubmission(anyLong(),anyLong(),anyLong(),anyString(),anyInt(),anyString(),anyLong(),anyString());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test void receiptUploadRejectsAfterCustomerConfirmationBeforeBrokerExecution(){
|
||||||
|
var source=document(1L);var declaring=container(ContainerCustomsService.DECLARING,3);
|
||||||
|
when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_COMPLETION_1"),eq("confirmed"),anyString())).thenReturn(1);
|
||||||
|
when(mapper.document(1L)).thenReturn(source);when(mapper.lockContainerById(2L)).thenReturn(declaring);
|
||||||
|
when(mapper.lockDocument(1L)).thenReturn(source);when(mapper.brokerSubmission(2L)).thenReturn(brokerSubmission());
|
||||||
|
when(mapper.lockCompletion(1L)).thenReturn(new ContainerCustomsMapper.CompletionRow(21L,1L,99L,
|
||||||
|
"receipt.pdf","b".repeat(64),"application/pdf",1L,5L,"admin",java.time.LocalDateTime.now(),1,"CONFIRMED",null));
|
||||||
|
assertThatThrownBy(()->service.uploadCompletion(1L,5L,"admin","confirmed",1,
|
||||||
|
new MockMultipartFile("file","receipt.pdf","application/pdf",new byte[]{1})))
|
||||||
|
.isInstanceOf(BizException.class).hasMessageContaining("客户已确认预录单");
|
||||||
|
verifyNoInteractions(files);
|
||||||
|
}
|
||||||
|
|
||||||
@Test void receiptUploadRejectsWhenInitialBrokerSubmissionIsMissing(){
|
@Test void receiptUploadRejectsWhenInitialBrokerSubmissionIsMissing(){
|
||||||
var source=document(1L);var container=container(ContainerCustomsService.DECLARING,3);
|
var source=document(1L);var container=container(ContainerCustomsService.DECLARING,3);
|
||||||
when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_COMPLETION_1"),eq("before-send"),anyString())).thenReturn(1);
|
when(mapper.claimCommand(anyLong(),eq(5L),eq("UPLOAD_COMPLETION_1"),eq("before-send"),anyString())).thenReturn(1);
|
||||||
|
|||||||
+5
-5
@@ -58,7 +58,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
|||||||
}).collect(Collectors.joining(","));
|
}).collect(Collectors.joining(","));
|
||||||
jdbc.execute("CREATE TABLE fl_loading_cabinet_evidence(" + columns + ")");
|
jdbc.execute("CREATE TABLE fl_loading_cabinet_evidence(" + columns + ")");
|
||||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT uq_operation UNIQUE(source_task_id,operation_key)");
|
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT uq_operation UNIQUE(source_task_id,operation_key)");
|
||||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT uq_seal UNIQUE(seal_no,active_flag)");
|
jdbc.execute("CREATE INDEX idx_active ON fl_loading_cabinet_evidence(container_booking_id,evidence_type,active_flag)");
|
||||||
String taskColumns = Arrays.stream(LoadingPickTaskEntity.class.getDeclaredFields())
|
String taskColumns = Arrays.stream(LoadingPickTaskEntity.class.getDeclaredFields())
|
||||||
.filter(field -> !Modifier.isStatic(field.getModifiers())).map(field ->
|
.filter(field -> !Modifier.isStatic(field.getModifiers())).map(field ->
|
||||||
field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase() + " "
|
field.getName().replaceAll("([a-z0-9])([A-Z])", "$1_$2").toLowerCase() + " "
|
||||||
@@ -98,7 +98,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void replacementInsertFailureRollsBackOldActiveRowAndSameRequestCanRetry() {
|
void appendInsertFailureRetainsOldActiveRowAndSameRequestCanRetry() {
|
||||||
nextFile = 301; upload("original");
|
nextFile = 301; upload("original");
|
||||||
var before = snapshot();
|
var before = snapshot();
|
||||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT reject_new_file CHECK(file_id <> 302)");
|
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence ADD CONSTRAINT reject_new_file CHECK(file_id <> 302)");
|
||||||
@@ -108,11 +108,11 @@ class CabinetLoadingEvidenceTransactionTest {
|
|||||||
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence DROP CONSTRAINT reject_new_file");
|
jdbc.execute("ALTER TABLE fl_loading_cabinet_evidence DROP CONSTRAINT reject_new_file");
|
||||||
upload("replacement");
|
upload("replacement");
|
||||||
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
||||||
.singleElement().satisfies(row -> assertThat(row.getFileId()).isEqualTo(302L));
|
.extracting(CabinetLoadingEvidenceEntity::getFileId).containsExactly(301L, 302L);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void duplicateKeyAfterStaleReplayReadsMustRollBackPriorVoid() {
|
void duplicateKeyAfterStaleReplayReadsRetainsAllPhotos() {
|
||||||
nextFile = 301; upload("original");
|
nextFile = 301; upload("original");
|
||||||
nextFile = 302; upload("winner");
|
nextFile = 302; upload("winner");
|
||||||
var before = snapshot();
|
var before = snapshot();
|
||||||
@@ -167,7 +167,7 @@ class CabinetLoadingEvidenceTransactionTest {
|
|||||||
boolean secondPassed = second.get(20, java.util.concurrent.TimeUnit.SECONDS);
|
boolean secondPassed = second.get(20, java.util.concurrent.TimeUnit.SECONDS);
|
||||||
assertThat(firstPassed || secondPassed).isTrue();
|
assertThat(firstPassed || secondPassed).isTrue();
|
||||||
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
assertThat(mapper.byContainer(99)).hasSize(2).filteredOn(row -> "ACTIVE".equals(row.getStatus()))
|
||||||
.singleElement().satisfies(row -> assertThat(row.getOperationKey()).isEqualTo("concurrent"));
|
.extracting(CabinetLoadingEvidenceEntity::getOperationKey).containsExactly("original", "concurrent");
|
||||||
var committed = snapshot();
|
var committed = snapshot();
|
||||||
upload("concurrent");
|
upload("concurrent");
|
||||||
assertThat(snapshot()).isEqualTo(committed);
|
assertThat(snapshot()).isEqualTo(committed);
|
||||||
|
|||||||
@@ -0,0 +1,169 @@
|
|||||||
|
package com.baiyue.lastleg.export;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/** 导出按任务类型及字段转换中文;仓码、SKU、币种与自由文本原样保留。 */
|
||||||
|
final class LastlegExportLabels {
|
||||||
|
/** 中文导出映射仅提供静态转换。 */
|
||||||
|
private LastlegExportLabels() { }
|
||||||
|
private static final Map<String, String> WORKFLOW = Map.ofEntries(
|
||||||
|
Map.entry("PENDING_INBOUND", "待入库"),
|
||||||
|
Map.entry("PENDING_REVIEW", "待审核"),
|
||||||
|
Map.entry("PENDING_RECEIPT", "待签收入库"),
|
||||||
|
Map.entry("PARTIALLY_RECEIVED", "部分入库"),
|
||||||
|
Map.entry("INBOUND_COMPLETED", "全部入库"),
|
||||||
|
Map.entry("WAIT_PDA_RECEIVING", "入库测量中"),
|
||||||
|
Map.entry("INBOUND_MEASURING", "入库测量中"),
|
||||||
|
Map.entry("WAIT_CUSTOMER_CONFIRMATION", "待客户确认实测"),
|
||||||
|
Map.entry("APPROVED", "审核通过"),
|
||||||
|
Map.entry("PENDING_ASSIGNMENT", "待分配"),
|
||||||
|
Map.entry("WAIT_RELABEL", "待换标"),
|
||||||
|
Map.entry("WAIT_FBA_DESTINATION", "待确认目的仓"),
|
||||||
|
Map.entry("READY_FOR_DISPATCH", "待创建出库单"),
|
||||||
|
Map.entry("WAIT_OUTBOUND", "待出库"),
|
||||||
|
Map.entry("ALLOCATED", "已分配库存"),
|
||||||
|
Map.entry("OUTBOUNDED", "已出库"),
|
||||||
|
Map.entry("RECEIVED", "已收货"),
|
||||||
|
Map.entry("IN_TRANSIT", "运输中"),
|
||||||
|
Map.entry("DESTINATION_ASSIGNED", "已分配目的仓"),
|
||||||
|
Map.entry("INBOUND_PLANNED", "已生成入库计划"),
|
||||||
|
Map.entry("PENDING_PROCESSING", "待处理"),
|
||||||
|
Map.entry("PROCESSING", "处理中"),
|
||||||
|
Map.entry("COMPLETED", "已完成"),
|
||||||
|
Map.entry("REJECTED", "已驳回"),
|
||||||
|
Map.entry("CANCELLED", "已取消"),
|
||||||
|
Map.entry("DRAFT", "草稿"),
|
||||||
|
Map.entry("PARTIAL_INBOUND", "部分入库"),
|
||||||
|
Map.entry("SIGNED", "已签收"),
|
||||||
|
Map.entry("PICKING", "拣货中"),
|
||||||
|
Map.entry("PICKED", "已拣货"),
|
||||||
|
Map.entry("SHIPPED", "已出库"),
|
||||||
|
Map.entry("WAIT_DELIVERY", "待派送"),
|
||||||
|
Map.entry("PENDING_DELIVERY", "待派送"),
|
||||||
|
Map.entry("DELIVERING", "派送中"),
|
||||||
|
Map.entry("DELIVERED", "派送完成"),
|
||||||
|
Map.entry("RETURNED", "已退回"),
|
||||||
|
Map.entry("CUSTOMER_CONFIRM_PENDING", "待客户确认"),
|
||||||
|
Map.entry("READY_FOR_OUTBOUND", "待出库"),
|
||||||
|
Map.entry("NOT_REQUIRED", "无需换标"));
|
||||||
|
private static final Map<String, String> INVENTORY_BUSINESS = Map.ofEntries(
|
||||||
|
Map.entry("IN_TRANSIT", "在途入账"),
|
||||||
|
Map.entry("STOCK_IN", "入库"),
|
||||||
|
Map.entry("STOCK_IN_TRANSIT_RELEASE", "在途库存转入"),
|
||||||
|
Map.entry("HOLD", "库存暂扣"),
|
||||||
|
Map.entry("RELEASE", "解除暂扣"),
|
||||||
|
Map.entry("ALLOCATE", "库存分配"),
|
||||||
|
Map.entry("UNALLOCATE", "取消分配"),
|
||||||
|
Map.entry("PICK", "拣货"),
|
||||||
|
Map.entry("SHIP", "出库"),
|
||||||
|
Map.entry("ADJUST", "库存调整"),
|
||||||
|
Map.entry("STOCKTAKE_ADJUST", "盘点调整"),
|
||||||
|
Map.entry("RECEIVING_WAREHOUSE_CHANGED", "变更接仓点"),
|
||||||
|
Map.entry("INTERNAL_MOVE", "库内移库"),
|
||||||
|
Map.entry("RELABEL_PALLET_DISASSEMBLE", "换标拆托"),
|
||||||
|
Map.entry("PALLET_ASSEMBLE", "组托"),
|
||||||
|
Map.entry("PALLET_DISASSEMBLE", "拆托"),
|
||||||
|
Map.entry("PALLET_SWAP", "换托"),
|
||||||
|
Map.entry("PALLET_UNBIND", "解托"),
|
||||||
|
Map.entry("RETURN_STOCK_IN", "退回入库"),
|
||||||
|
Map.entry("DISCARD", "货物废弃"));
|
||||||
|
private static final Map<String, String> ORDER_TYPES = Map.ofEntries(
|
||||||
|
Map.entry("ONE_CLICK_DROPSHIP", "一件代发"),
|
||||||
|
Map.entry("FIRST_LEG", "头程订单"),
|
||||||
|
Map.entry("RELABEL_APPLICATION", "换标下单"),
|
||||||
|
Map.entry("WAREHOUSE_TRANSFER", "仓储转运"),
|
||||||
|
Map.entry("RETURN_RELABEL", "退货换标"),
|
||||||
|
Map.entry("DELIVERY_RETURN", "派送异常退回"),
|
||||||
|
Map.entry("LOCAL_LOOSEBOARD", "本土散板"),
|
||||||
|
Map.entry("CARGO_DISCARD", "货物废弃订单"),
|
||||||
|
Map.entry("REJECTED", "拒收订单"),
|
||||||
|
Map.entry("EXPRESS", "快递订单"));
|
||||||
|
private static final Map<String, String> NODES = Map.ofEntries(
|
||||||
|
Map.entry("WAIT_LOADING", "待装柜"),
|
||||||
|
Map.entry("LOADING", "装柜中"),
|
||||||
|
Map.entry("WAIT_CUSTOMS", "待出口报关"),
|
||||||
|
Map.entry("WAIT_OUTBOUND", "待国内出仓"),
|
||||||
|
Map.entry("DEPARTED_WAREHOUSE", "已离开国内仓库"),
|
||||||
|
Map.entry("IN_TRANSIT", "海运途中"),
|
||||||
|
Map.entry("ARRIVED_DESTINATION_PORT", "船舶已到目的港"),
|
||||||
|
Map.entry("WAITING_FOR_ARRIVAL", "待到港"),
|
||||||
|
Map.entry("ARRIVED", "已到港"),
|
||||||
|
Map.entry("PICKUP_READY", "已清关、可提柜"),
|
||||||
|
Map.entry("PICKUP_SCHEDULED", "已预约提柜"),
|
||||||
|
Map.entry("PICKED_UP", "已提柜"),
|
||||||
|
Map.entry("UNLOADED", "待还柜"),
|
||||||
|
Map.entry("EMPTY_RETURNED", "已还柜"),
|
||||||
|
Map.entry("AT_ORIGIN_PORT", "重箱已进入起运港"),
|
||||||
|
Map.entry("LOADED_ON_VESSEL", "已装船"),
|
||||||
|
Map.entry("DEPARTED_ORIGIN_PORT", "船舶已离开起运港"),
|
||||||
|
Map.entry("DISCHARGED_FROM_VESSEL", "已卸船、未清关"));
|
||||||
|
private static final Map<String, String> INVENTORY_STATUS = Map.ofEntries(
|
||||||
|
Map.entry("OUTSIDE", "仓外"),
|
||||||
|
Map.entry("UNBOUND", "未绑托"),
|
||||||
|
Map.entry("IN_TRANSIT", "在途"),
|
||||||
|
Map.entry("RECEIVING", "收货中"),
|
||||||
|
Map.entry("AVAILABLE", "可用"),
|
||||||
|
Map.entry("HOLD", "暂扣"),
|
||||||
|
Map.entry("HELD", "暂扣"),
|
||||||
|
Map.entry("ALLOCATED", "已分配"),
|
||||||
|
Map.entry("PICKED", "已拣货"),
|
||||||
|
Map.entry("SHIPPED", "已出库"),
|
||||||
|
Map.entry("DISCARDED", "已废弃"),
|
||||||
|
Map.entry("STOCK_IN", "已入库"),
|
||||||
|
Map.entry("EMPTY", "空置"),
|
||||||
|
Map.entry("OCCUPIED", "占用"));
|
||||||
|
|
||||||
|
/** 只转换明确的业务枚举列,避免将编号、币种和备注误当作状态。 */
|
||||||
|
static Object value(String type, String key, Object value) {
|
||||||
|
if (!(value instanceof String text) || text.isBlank()) return value;
|
||||||
|
return switch (type) {
|
||||||
|
case "LAST_MILE_ORDER_LIST" -> switch (key) {
|
||||||
|
case "currentStatus" -> label(text, WORKFLOW, "未知状态");
|
||||||
|
case "orderTypeName", "businessSource.label" -> label(text, ORDER_TYPES, "未知业务类型");
|
||||||
|
case "containerNode" -> label(text, NODES, "未知货柜节点");
|
||||||
|
case "relabelStatus" -> label(text, WORKFLOW, "未知换标状态");
|
||||||
|
case "palletMergeFlag" -> label(text, Map.of("REQUIRED", "待合托", "MERGED", "已合托"), "未标记");
|
||||||
|
case "subOrderStatus" -> subOrders(text);
|
||||||
|
default -> value;
|
||||||
|
};
|
||||||
|
case "OVERSEAS_INVENTORY_LEDGER" -> switch (key) {
|
||||||
|
case "businessType" -> label(text, INVENTORY_BUSINESS, "未知业务类型");
|
||||||
|
case "statusFrom", "statusTo" -> label(text, INVENTORY_STATUS, "未知库存状态");
|
||||||
|
default -> value;
|
||||||
|
};
|
||||||
|
case "OVERSEAS_QUOTE_LIST", "MONTHLY_EXCHANGE_RATE_LIST", "FLEET_QUOTE_LIST" -> switch (key) {
|
||||||
|
case "status" -> label(text, Map.of("ACTIVE", "启用", "INACTIVE", "停用", "DELETED", "已删除"), "未知状态");
|
||||||
|
case "details.kind" -> label(text, Map.of("PURCHASE", "车队采购报价", "SALE", "外接销售报价"), "未知用途");
|
||||||
|
default -> value;
|
||||||
|
};
|
||||||
|
case "VEHICLE_LIST" -> switch (key) {
|
||||||
|
case "status" -> label(text, Map.of("IN_SERVICE", "在役", "DISPATCHED", "外派中", "MAINTENANCE", "维修中", "INACTIVE", "停用", "RETURNED", "已归还"), "未知状态");
|
||||||
|
case "ownership" -> label(text, Map.of("OWNED", "自有", "DISPATCHED", "外派"), "未知车辆归属");
|
||||||
|
case "region" -> label(text, Map.of("DOMESTIC", "国内", "OVERSEAS", "海外"), "未知区域");
|
||||||
|
default -> value;
|
||||||
|
};
|
||||||
|
case "TRIP_OPERATIONS_LIST" -> "status".equals(key)
|
||||||
|
? label(text, Map.of("PLANNED", "计划中", "IN_TRANSIT", "运输中", "COMPLETED", "货物已结案", "CANCELLED", "已取消"), "未知状态") : value;
|
||||||
|
default -> value;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 子单状态可能由多个“业务类型:状态”组成,分别翻译而不破坏分组。 */
|
||||||
|
private static String subOrders(String value) {
|
||||||
|
Map<String, String> kinds = Map.of("INBOUND_PLAN", "入库单", "OUTBOUND", "出库单", "DELIVERY", "派送单", "RELABEL", "换标单", "WAREHOUSE_TRANSFER", "仓储转运", "RETURN_RELABEL", "退货换标");
|
||||||
|
return Arrays.stream(value.split("[;;]"))
|
||||||
|
.map(part -> {
|
||||||
|
String[] pair = part.split(":", 2);
|
||||||
|
return pair.length == 2 ? label(pair[0], kinds, "子单") + ":" + label(pair[1], WORKFLOW, "未知状态")
|
||||||
|
: label(part, WORKFLOW, "未知状态");
|
||||||
|
}).collect(Collectors.joining(";"));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 中文历史快照保持原文,未知英文枚举使用中文占位。 */
|
||||||
|
private static String label(String value, Map<String, String> labels, String fallback) {
|
||||||
|
return labels.getOrDefault(value.trim().toUpperCase(Locale.ROOT), value.matches(".*[一-鿿].*") ? value : fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
+5
-4
@@ -47,6 +47,7 @@ public class LastlegExportProviderService {
|
|||||||
@Resource private Validator validator;
|
@Resource private Validator validator;
|
||||||
@Value("${baiyue.security.internal-service-token}") private String signingKey;
|
@Value("${baiyue.security.internal-service-token}") private String signingKey;
|
||||||
|
|
||||||
|
/** 校验平台导出快照,复用列表筛选并生成中文业务数据批次。 */
|
||||||
public ExportBatchResponse batch(String type, ExportBatchRequest request) {
|
public ExportBatchResponse batch(String type, ExportBatchRequest request) {
|
||||||
if (request == null || !"PLATFORM".equals(request.appCode()) || !"0".equals(request.tenantId())) {
|
if (request == null || !"PLATFORM".equals(request.appCode()) || !"0".equals(request.tenantId())) {
|
||||||
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅支持平台导出");
|
throw new ResponseStatusException(HttpStatus.FORBIDDEN, "仅支持平台导出");
|
||||||
@@ -81,7 +82,7 @@ public class LastlegExportProviderService {
|
|||||||
case "TRIP_OPERATIONS_LIST" -> operations.report(query(parameters, TripOperationsModels.Query.class), costVisible).page();
|
case "TRIP_OPERATIONS_LIST" -> operations.report(query(parameters, TripOperationsModels.Query.class), costVisible).page();
|
||||||
default -> throw invalid("不支持该导出类型");
|
default -> throw invalid("不支持该导出类型");
|
||||||
};
|
};
|
||||||
List<Map<String, Object>> rows = page.records().stream().map(value -> row(value, columns)).toList();
|
List<Map<String, Object>> rows = page.records().stream().map(value -> row(type, value, columns)).toList();
|
||||||
int next = offset + rows.size();
|
int next = offset + rows.size();
|
||||||
if (rows.isEmpty() && next < page.total()) throw invalid("导出分页未推进");
|
if (rows.isEmpty() && next < page.total()) throw invalid("导出分页未推进");
|
||||||
return new ExportBatchResponse(1, hash(columns), snapshot, first ? columns : List.of(), rows,
|
return new ExportBatchResponse(1, hash(columns), snapshot, first ? columns : List.of(), rows,
|
||||||
@@ -98,7 +99,8 @@ public class LastlegExportProviderService {
|
|||||||
} catch (IllegalArgumentException e) { throw invalid("导出筛选条件无效"); }
|
} catch (IllegalArgumentException e) { throw invalid("导出筛选条件无效"); }
|
||||||
}
|
}
|
||||||
|
|
||||||
private Map<String, Object> row(Object value, List<ExportColumn> columns) {
|
/** 按显式列投影业务数据,仅对指定枚举字段进行中文转换。 */
|
||||||
|
private Map<String, Object> row(String type, Object value, List<ExportColumn> columns) {
|
||||||
JsonNode source = json.valueToTree(value);
|
JsonNode source = json.valueToTree(value);
|
||||||
Map<String, Object> result = new LinkedHashMap<>();
|
Map<String, Object> result = new LinkedHashMap<>();
|
||||||
for (ExportColumn column : columns) {
|
for (ExportColumn column : columns) {
|
||||||
@@ -106,12 +108,11 @@ public class LastlegExportProviderService {
|
|||||||
for (String part : column.key().split("\\.")) node = node.path(part);
|
for (String part : column.key().split("\\.")) node = node.path(part);
|
||||||
Object cell = node.isMissingNode() || node.isNull() ? null
|
Object cell = node.isMissingNode() || node.isNull() ? null
|
||||||
: node.isBoolean() ? node.booleanValue() : node.isNumber() ? node.decimalValue() : node.asText();
|
: node.isBoolean() ? node.booleanValue() : node.isNumber() ? node.decimalValue() : node.asText();
|
||||||
result.put(column.key(), cell);
|
result.put(column.key(), LastlegExportLabels.value(type, column.key(), cell));
|
||||||
}
|
}
|
||||||
if (value instanceof com.baiyue.lastleg.dto.LastlegOrderSummary order) {
|
if (value instanceof com.baiyue.lastleg.dto.LastlegOrderSummary order) {
|
||||||
result.put("abnormal", Boolean.TRUE.equals(order.abnormal()) ? "是" : "否");
|
result.put("abnormal", Boolean.TRUE.equals(order.abnormal()) ? "是" : "否");
|
||||||
result.put("detained", Boolean.TRUE.equals(order.detained()) ? "是" : "否");
|
result.put("detained", Boolean.TRUE.equals(order.detained()) ? "是" : "否");
|
||||||
result.put("currentStatus", com.baiyue.lastleg.model.LastlegOrderStatus.labelOf(order.currentStatus()));
|
|
||||||
}
|
}
|
||||||
if (value instanceof TripOperationsModels.Row trip) {
|
if (value instanceof TripOperationsModels.Row trip) {
|
||||||
result.put("destinations", trip.destinations().stream().map(TripOperationsModels.Destination::label).collect(Collectors.joining(" / ")));
|
result.put("destinations", trip.destinations().stream().map(TripOperationsModels.Destination::label).collect(Collectors.joining(" / ")));
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package com.baiyue.lastleg.export;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class LastlegExportLabelsTest {
|
||||||
|
@Test void translatesEveryEnumColumnOfAllLastlegTaskTypes() {
|
||||||
|
Object[][] examples = {
|
||||||
|
{"LAST_MILE_ORDER_LIST", "currentStatus", "WAIT_PDA_RECEIVING", "入库测量中"},
|
||||||
|
{"LAST_MILE_ORDER_LIST", "orderTypeName", "FIRST_LEG", "头程订单"},
|
||||||
|
{"LAST_MILE_ORDER_LIST", "businessSource.label", "LOCAL_LOOSEBOARD", "本土散板"},
|
||||||
|
{"LAST_MILE_ORDER_LIST", "containerNode", "PICKUP_READY", "已清关、可提柜"},
|
||||||
|
{"LAST_MILE_ORDER_LIST", "relabelStatus", "CUSTOMER_CONFIRM_PENDING", "待客户确认"},
|
||||||
|
{"LAST_MILE_ORDER_LIST", "palletMergeFlag", "REQUIRED", "待合托"},
|
||||||
|
{"OVERSEAS_INVENTORY_LEDGER", "businessType", "RETURN_STOCK_IN", "退回入库"},
|
||||||
|
{"OVERSEAS_INVENTORY_LEDGER", "statusFrom", "PICKED", "已拣货"},
|
||||||
|
{"OVERSEAS_INVENTORY_LEDGER", "statusTo", "SHIPPED", "已出库"},
|
||||||
|
{"OVERSEAS_QUOTE_LIST", "status", "ACTIVE", "启用"},
|
||||||
|
{"MONTHLY_EXCHANGE_RATE_LIST", "status", "INACTIVE", "停用"},
|
||||||
|
{"FLEET_QUOTE_LIST", "status", "ACTIVE", "启用"},
|
||||||
|
{"FLEET_QUOTE_LIST", "details.kind", "PURCHASE", "车队采购报价"},
|
||||||
|
{"FLEET_QUOTE_LIST", "details.kind", "SALE", "外接销售报价"},
|
||||||
|
{"VEHICLE_LIST", "status", "DISPATCHED", "外派中"},
|
||||||
|
{"VEHICLE_LIST", "ownership", "DISPATCHED", "外派"},
|
||||||
|
{"VEHICLE_LIST", "region", "OVERSEAS", "海外"},
|
||||||
|
{"TRIP_OPERATIONS_LIST", "status", "COMPLETED", "货物已结案"}
|
||||||
|
};
|
||||||
|
for (Object[] example : examples) {
|
||||||
|
assertEquals(example[3], LastlegExportLabels.value((String) example[0], (String) example[1], example[2]), example[0] + ":" + example[1]);
|
||||||
|
assertTrue(LastlegExportColumns.columns((String) example[0]).stream().anyMatch(c -> c.key().equals(example[1])), "映射必须对应实际导出列");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void translatesCompositeSubOrdersAndAllVehicleStatuses() {
|
||||||
|
assertEquals("出库单:已出库;派送单:待派送", LastlegExportLabels.value("LAST_MILE_ORDER_LIST", "subOrderStatus", "OUTBOUND:OUTBOUNDED;DELIVERY:WAIT_DELIVERY"));
|
||||||
|
assertEquals("入库单:部分入库", LastlegExportLabels.value("LAST_MILE_ORDER_LIST", "subOrderStatus", "INBOUND_PLAN:PARTIAL_INBOUND"));
|
||||||
|
for (String code : List.of("IN_SERVICE", "DISPATCHED", "MAINTENANCE", "INACTIVE", "RETURNED")) {
|
||||||
|
String label = (String) LastlegExportLabels.value("VEHICLE_LIST", "status", code);
|
||||||
|
assertTrue(label.matches(".*[一-鿿].*")); assertFalse(label.contains("未知"));
|
||||||
|
}
|
||||||
|
for (String code : List.of("OUTSIDE", "UNBOUND", "IN_TRANSIT", "RECEIVING", "AVAILABLE", "HOLD", "ALLOCATED", "PICKED", "SHIPPED")) {
|
||||||
|
assertFalse(LastlegExportLabels.value("OVERSEAS_INVENTORY_LEDGER", "statusTo", code).toString().contains("未知"));
|
||||||
|
}
|
||||||
|
for (String code : List.of("RECEIVING_WAREHOUSE_CHANGED", "INTERNAL_MOVE", "RELABEL_PALLET_DISASSEMBLE")) {
|
||||||
|
assertFalse(LastlegExportLabels.value("OVERSEAS_INVENTORY_LEDGER", "businessType", code).toString().contains("未知"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test void preservesBusinessIdentifiersAmountsDatesCurrenciesAndFreeText() {
|
||||||
|
for (String type : List.of("LAST_MILE_ORDER_LIST", "OVERSEAS_INVENTORY_BALANCE", "OVERSEAS_INVENTORY_LEDGER", "OVERSEAS_QUOTE_LIST", "MONTHLY_EXCHANGE_RATE_LIST", "VEHICLE_LIST", "FLEET_QUOTE_LIST", "TRIP_OPERATIONS_LIST", "SELFOP_INVENTORY_TURNOVER")) {
|
||||||
|
assertEquals("ACTIVE", LastlegExportLabels.value(type, "remark", "ACTIVE"));
|
||||||
|
assertEquals("ONT8", LastlegExportLabels.value(type, "warehouseCode", "ONT8"));
|
||||||
|
assertEquals("USD", LastlegExportLabels.value(type, "currency", "USD"));
|
||||||
|
assertEquals(BigDecimal.TEN, LastlegExportLabels.value(type, "amount", BigDecimal.TEN));
|
||||||
|
assertNull(LastlegExportLabels.value(type, "status", null));
|
||||||
|
}
|
||||||
|
assertEquals("已完成", LastlegExportLabels.value("LAST_MILE_ORDER_LIST", "currentStatus", "已完成"));
|
||||||
|
assertEquals("未知状态", LastlegExportLabels.value("TRIP_OPERATIONS_LIST", "status", "NEW_INTERNAL_STATE"));
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -46,6 +46,17 @@ class LastlegExportProviderServiceTest {
|
|||||||
}
|
}
|
||||||
@AfterEach void close() { validators.close(); }
|
@AfterEach void close() { validators.close(); }
|
||||||
|
|
||||||
|
@Test void rowProjectionTranslatesOnlyBusinessEnumsBeforeWritingBatches() {
|
||||||
|
Map<String, Object> row = ReflectionTestUtils.invokeMethod(service, "row", "FLEET_QUOTE_LIST",
|
||||||
|
Map.of("status", "ACTIVE", "quoteNo", "ACTIVE", "details", Map.of("kind", "SALE", "currency", "USD", "remark", "SALE")),
|
||||||
|
LastlegExportColumns.columns("FLEET_QUOTE_LIST"));
|
||||||
|
assertEquals("启用", row.get("status"));
|
||||||
|
assertEquals("外接销售报价", row.get("details.kind"));
|
||||||
|
assertEquals("ACTIVE", row.get("quoteNo"));
|
||||||
|
assertEquals("USD", row.get("details.currency"));
|
||||||
|
assertEquals("SALE", row.get("details.remark"));
|
||||||
|
}
|
||||||
|
|
||||||
@Test void batchesAllRowsAndBindsSnapshotToFiltersRequesterAndAttempt() {
|
@Test void batchesAllRowsAndBindsSnapshotToFiltersRequesterAndAttempt() {
|
||||||
var row = new MonthlyExchangeRateDtos.Response("2026-10", "USD", "CNY", new BigDecimal("7.12"), "ACTIVE", 1, "operator", LocalDateTime.of(2026,10,8,12,0), null);
|
var row = new MonthlyExchangeRateDtos.Response("2026-10", "USD", "CNY", new BigDecimal("7.12"), "ACTIVE", 1, "operator", LocalDateTime.of(2026,10,8,12,0), null);
|
||||||
when(rates.page(any())).thenAnswer(call -> {
|
when(rates.page(any())).thenAnswer(call -> {
|
||||||
|
|||||||
Reference in New Issue
Block a user