diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java index 75b05be0..7caf0be1 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CreditScoreQuantification.java @@ -32,6 +32,7 @@ import lombok.EqualsAndHashCode; import org.springblade.core.tenant.mp.TenantEntity; import java.io.Serial; +import java.math.BigDecimal; /** * 评分量化表实体类 @@ -62,5 +63,10 @@ public class CreditScoreQuantification extends TenantEntity { */ @Schema(description = "标准说明") private String standardDescription; + /** + * 减分项目最高扣分限制:客商评分时所有减分项合计不得超过该值,超出按该上限计分 + */ + @Schema(description = "减分项目最高扣分限制") + private BigDecimal maxMinusScore; } diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java index 6f54a69f..6162ec5c 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerCreditScoreVO.java @@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode; import org.springblade.transport.pojo.entity.CustomerCreditScore; import java.io.Serial; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @@ -57,4 +58,11 @@ public class CustomerCreditScoreVO extends CustomerCreditScore { @Schema(description = "评估标准") private List standards = new ArrayList<>(); + /** + * 来自评分量化表的减分项目最高扣分限制(非落库字段,供前端计分使用) + */ + @TableField(exist = false) + @Schema(description = "减分项目最高扣分限制") + private BigDecimal maxMinusScore; + } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java index cee7d476..b703f1ce 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/SettlementImportTemplateExporter.java @@ -96,8 +96,7 @@ public final class SettlementImportTemplateExporter { private SettlementImportTemplateExporter() { } - public static void export(HttpServletResponse response, List transportTypeOptions, - List feeItemNames) { + public static void export(HttpServletResponse response, List transportTypeOptions) { List headers = buildHeaders(); // 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死 List transportOptions = transportTypeOptions == null ? List.of() : transportTypeOptions.stream() @@ -106,7 +105,7 @@ public final class SettlementImportTemplateExporter { .distinct() .toList(); try (Workbook workbook = new XSSFWorkbook()) { - writeInstructionSheet(workbook, transportOptions, feeItemNames); + writeInstructionSheet(workbook); Sheet dataSheet = workbook.createSheet("导入结算单模板"); CellStyle headerStyle = createHeaderStyle(workbook); Row headerRow = dataSheet.createRow(0); @@ -137,8 +136,7 @@ public final class SettlementImportTemplateExporter { return headers; } - private static void writeInstructionSheet(Workbook workbook, List transportOptions, - List feeItemNames) { + private static void writeInstructionSheet(Workbook workbook) { Sheet sheet = workbook.createSheet("填写说明"); CellStyle titleStyle = workbook.createCellStyle(); Font titleFont = workbook.createFont(); @@ -147,24 +145,11 @@ public final class SettlementImportTemplateExporter { List lines = new ArrayList<>(); lines.add("导入结算单填写说明"); lines.add("1. 请不要修改「导入结算单模板」工作表的固定表头;带 * 号为必填项。"); - lines.add("2. 运输方式请从下拉项中选择,须与业务字典 transport_type 的字典名称一致。"); + lines.add("2. 运输方式请从下拉项中选择。"); lines.add("3. 日期请填写 yyyy-MM-dd(如 2026-01-01);Excel 日期单元格也可直接选择。"); lines.add("4. 里程(km) 可为空;填写时须为正数。"); lines.add("5. 「*运输费」之后、「*实际发货时间」之前可按需自行插入费用项列,列名须与系统已启用费用项名称完全一致;金额不为 0 时若不存在或已停用则无法导入。"); lines.add("6. 模板不含结算金额列;导入后系统按「运输费 + 各费用项」自动合计结算金额。"); - if (transportOptions != null && !transportOptions.isEmpty()) { - lines.add("7. 当前可选运输方式(业务字典 transport_type 名称):" - + String.join("、", transportOptions) + "。"); - } else { - lines.add("7. 运输方式请从业务字典 transport_type 维护的名称中选择。"); - } - if (feeItemNames != null && !feeItemNames.isEmpty()) { - lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用费用项一致;当前系统已启用费用项:" - + String.join("、", feeItemNames.stream().limit(30).toList()) - + (feeItemNames.size() > 30 ? " 等" : "") + "。"); - } else { - lines.add("8. 费用项请按实际需要在模板中自行增列,列名须与系统已启用的费用项主数据名称完全一致。"); - } for (int index = 0; index < lines.size(); index++) { Row row = sheet.createRow(index); Cell cell = row.createCell(0); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java index 25014d85..83035479 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CreditScoreQuantificationServiceImpl.java @@ -458,6 +458,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl queryWrapper = Wrappers.lambdaQuery() .eq(CreditScoreQuantification::getName, quantification.getName()) .eq(CreditScoreQuantification::getIsDeleted, 0); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index 39d96798..8293a856 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -280,6 +280,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl customerIds) { + // 逻辑删除保留历史评分明细;曾审核通过的旧评分在再次审核通过前仍通过主表等级/额度暂时生效 List scores = creditScoreMapper.selectList(Wrappers.lambdaQuery() .in(CustomerCreditScore::getCustomerId, customerIds) .and(wrapper -> wrapper.ne(CustomerCreditScore::getIsDeleted, 1).or().isNull(CustomerCreditScore::getIsDeleted))); @@ -1410,6 +1418,42 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl scores = loadScores(customer.getId()); + if (Func.isEmpty(scores)) { + return; + } + CustomerCreditScoreVO activeScore = scores.stream() + .filter(score -> "已完成".equals(score.getReviewStatus())) + .findFirst() + .orElse(scores.get(0)); + if (Func.isNotEmpty(activeScore.getCreditLevel())) { + customer.setCustomerLevel(activeScore.getCreditLevel()); + } + if (activeScore.getMaxCreditLimit() != null) { + customer.setMaxCreditLimit(activeScore.getMaxCreditLimit()); + customer.setApplyCreditLimit(activeScore.getMaxCreditLimit()); + } + } + private List loadContacts(Long customerId) { return contactMapper.selectList(Wrappers.lambdaQuery() .eq(CustomerContact::getCustomerId, customerId) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java index ccd9dee9..268ddb9c 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/LoadingManageServiceImpl.java @@ -190,8 +190,6 @@ public class LoadingManageServiceImpl extends BaseServiceImpl "运输方式不是公路,不可配载"; - case NOT_PENDING -> "当前状态不是待执行,不可配载"; - case ON_OTHER_MANIFEST -> "已关联其他配载单"; + case NOT_LOADABLE_STATUS -> "当前状态不是待执行或进行中,不可配载"; + case ON_OTHER_MANIFEST -> "已关联其他有效配载单"; }; } @@ -1087,6 +1087,27 @@ public class LoadingManageServiceImpl extends BaseServiceImpl + * 一张配载单的子运单同属一个项目,任一张需接单则整单待执行。 + */ + private String deriveLoadingBusinessStatus(List waybills) { + if (Func.isEmpty(waybills)) { + return STATUS_RUNNING; + } + Map liveConfigByProject = LiveProcessConfigSupport.byProjects(processConfigService, + waybills.stream().map(Waybill::getProjectId).toList()); + for (Waybill waybill : waybills) { + String processJson = LiveProcessConfigSupport.resolve( + liveConfigByProject.get(waybill.getProjectId()), waybill.getProcessJson()); + if (WaybillProcessSupport.requiresDriverAcceptConfirmation(processJson)) { + return STATUS_PENDING; + } + } + return STATUS_RUNNING; + } + /** * 子运单业务状态逐张按各自项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。 *

diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java index 227a73c7..f9a28ea3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/PaymentApplicationServiceImpl.java @@ -2131,21 +2131,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpllambdaQuery() .eq(PaymentApplicationSettlement::getPaymentApplicationId, entity.getId())); - boolean customAmounts = settlements.size() > 1 && lines != null && !lines.isEmpty(); + boolean customAmounts = this.useSpecifiedLineAmounts(lines); BigDecimal totalBase = settlements.stream() .map(item -> payableOfFormalSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal remainingApplied = money(entity.getAppliedAmount()); BigDecimal remainingPaid = money(entity.getPaidAmount()); if (customAmounts) { - BigDecimal lineTotal = BigDecimal.ZERO; for (FormalSettlement settlement : settlements) { BigDecimal payable = payableOfFormalSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); BigDecimal applied = this.specifiedAppliedAmount(lines, settlement.getId(), null); this.assertAppliedWithinPayable(settlement.getFormalSettlementNo(), applied, payable); - lineTotal = lineTotal.add(applied); } - this.assertLineTotalMatches(entity, lineTotal); } for (int index = 0; index < settlements.size(); index++) { FormalSettlement settlement = settlements.get(index); @@ -2243,20 +2240,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl 1 && lines != null && !lines.isEmpty(); + // 结算行付款金额独立于付款基本信息中的申请付款金额,有明细时按行保存 + boolean customAmounts = this.useSpecifiedLineAmounts(lines); BigDecimal totalBase = targetSettlements.stream() .map(item -> payableOfPreSettlement(item.getId(), item.getSettlementAmount(), entity.getId())) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal remainingApplied = money(entity.getAppliedAmount()); if (customAmounts) { - BigDecimal lineTotal = BigDecimal.ZERO; for (PreSettlement settlement : targetSettlements) { BigDecimal payable = payableOfPreSettlement(settlement.getId(), settlement.getSettlementAmount(), entity.getId()); BigDecimal applied = this.specifiedAppliedAmount(lines, null, settlement.getId()); this.assertAppliedWithinPayable(settlement.getPreSettlementNo(), applied, payable); - lineTotal = lineTotal.add(applied); } - this.assertLineTotalMatches(entity, lineTotal); } for (int index = 0; index < targetSettlements.size(); index++) { PreSettlement settlement = targetSettlements.get(index); @@ -2307,11 +2302,9 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl lines) { + return lines != null && !lines.isEmpty(); } private void validateQuota(PaymentApplication entity) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java index 0f5b680b..f7dd22e3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementAdjustmentServiceImpl.java @@ -119,8 +119,9 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl tailPaymentSettlementIds = tailPaymentSettlementIds(rows.stream() .map(FormalSettlement::getId).filter(Objects::nonNull).toList()); + // 仅排除已完成收/付款、已发起尾款付款的正式结算单。 + // 注意:sourceType=「预结算合并」是由预结算转正式的常规来源,不是尾款单,不能据此过滤。 rows = rows.stream().filter(row -> !isSettlementCompleted(row) - && !isTailSettlement(row) && !tailPaymentSettlementIds.contains(row.getId())).toList(); List> result = new ArrayList<>(); for (FormalSettlement row : rows) { @@ -467,9 +468,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl 0 && money(formal.getPaidAmount()).compareTo(money(formal.getSettlementAmount())) >= 0); } - private boolean isTailSettlement(FormalSettlement formal) { - return formal != null && "预结算合并".equals(formal.getSourceType()); - } private boolean hasTailPayment(Long formalSettlementId) { return formalSettlementId != null && tailPaymentSettlementIds(List.of(formalSettlementId)) .contains(formalSettlementId); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java index 31f0a8b0..6ec5bcd0 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/SettlementImportBatchServiceImpl.java @@ -76,6 +76,7 @@ import org.springblade.transport.service.IReceivablePayableDetailService; import org.springblade.transport.service.ISettlementImportBatchService; import org.springblade.transport.service.IWaybillImportBatchService; import org.springblade.transport.service.IWaybillService; +import com.fasterxml.jackson.core.type.TypeReference; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; @@ -220,12 +221,14 @@ public class SettlementImportBatchServiceImpl rows = buildRowsFromWaybills(listBatchWaybills(batch)); } vo.setRows(rows); + // 费用列名优先取库字段;若历史数据解析失败或缺失,从明细 feeItems 回补,避免草稿再打开丢列 + vo.setFeeItemNames(mergeFeeItemNames(vo.getFeeItemNames(), rows)); return vo; } @Override public void exportTemplate(HttpServletResponse response) { - SettlementImportTemplateExporter.export(response, loadTransportTypeLabels(), loadEnabledFeeItemNames()); + SettlementImportTemplateExporter.export(response, loadTransportTypeLabels()); } @Override @@ -867,21 +870,56 @@ public class SettlementImportBatchServiceImpl } private List resolveFeeItemNames(SettlementImportBatchRequest request) { + LinkedHashSet names = new LinkedHashSet<>(); if (request.getFeeItemNames() != null) { - return request.getFeeItemNames().stream().filter(Func::isNotEmpty).distinct().toList(); + request.getFeeItemNames().stream() + .filter(Func::isNotEmpty) + .map(String::trim) + .filter(Func::isNotEmpty) + .forEach(names::add); } - Set names = new LinkedHashSet<>(); if (Func.isNotEmpty(request.getRows())) { for (Map row : request.getRows()) { - Object feeItems = row.get("feeItems"); - if (feeItems instanceof Map raw) { - raw.keySet().stream().filter(Objects::nonNull).map(String::valueOf).forEach(names::add); - } + collectFeeItemNamesFromRow(row, names); } } return new ArrayList<>(names); } + /** 合并请求/库中的费用列名与明细行 feeItems 的键,保持插入顺序。 */ + private List mergeFeeItemNames(List preferred, List> rows) { + LinkedHashSet names = new LinkedHashSet<>(); + if (Func.isNotEmpty(preferred)) { + preferred.stream() + .filter(Func::isNotEmpty) + .map(String::trim) + .filter(Func::isNotEmpty) + .forEach(names::add); + } + if (Func.isNotEmpty(rows)) { + for (Map row : rows) { + collectFeeItemNamesFromRow(row, names); + } + } + return new ArrayList<>(names); + } + + private void collectFeeItemNamesFromRow(Map row, Set names) { + if (row == null || names == null) { + return; + } + Object feeItems = row.get("feeItems"); + if (!(feeItems instanceof Map raw)) { + return; + } + raw.keySet().stream() + .filter(Objects::nonNull) + .map(String::valueOf) + .map(String::trim) + .filter(Func::isNotEmpty) + .forEach(names::add); + } + private List> buildFeeRows(List> rows, List waybills) { List> primaryRows = primaryRows(rows); List> feeRows = new ArrayList<>(); @@ -1061,12 +1099,35 @@ public class SettlementImportBatchServiceImpl vo.setSettlementTypeName(settlementTypeName(batch.getSettlementType())); vo.setSettlementBillTypeName(settlementBillTypeName(batch.getSettlementBillType())); vo.setWaybillModeName(waybillModeName(batch.getWaybillMode())); - if (Func.isNotEmpty(batch.getFeeItemNamesJson())) { - vo.setFeeItemNames(JsonUtil.parseArray(batch.getFeeItemNamesJson(), String.class)); - } + vo.setFeeItemNames(parseFeeItemNamesJson(batch.getFeeItemNamesJson())); return vo; } + /** + * 解析费用列名 JSON。 + * 注意:不能用 JsonUtil.parseArray(..., String.class),其内部按 List<Map> 解析,字符串数组会失败导致草稿再打开丢费用列。 + */ + private List parseFeeItemNamesJson(String feeItemNamesJson) { + if (Func.isEmpty(feeItemNamesJson)) { + return List.of(); + } + try { + List names = JsonUtil.parse(feeItemNamesJson, new TypeReference>() { + }); + if (names == null || names.isEmpty()) { + return List.of(); + } + return names.stream() + .filter(Func::isNotEmpty) + .map(String::trim) + .filter(Func::isNotEmpty) + .distinct() + .toList(); + } catch (Exception exception) { + return List.of(); + } + } + private String importStatusName(String status) { if (STATUS_DRAFT.equals(status)) { return "草稿"; @@ -1350,20 +1411,6 @@ public class SettlementImportBatchServiceImpl return new ArrayList<>(labels); } - private List loadEnabledFeeItemNames() { - R> response = sysClient.getFeeItems(false); - if (response == null || !response.isSuccess() || response.getData() == null) { - return List.of(); - } - return response.getData().stream() - .filter(item -> item != null && (item.getStatus() == null || Objects.equals(item.getStatus(), 1))) - .map(FeeItem::getName) - .filter(Func::isNotEmpty) - .map(String::trim) - .distinct() - .toList(); - } - /** 历史已完成批次无 rows_json 时,按关联运单回填查看明细。 */ private List> buildRowsFromWaybills(List waybills) { if (Func.isEmpty(waybills)) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java index 34d9d35f..606e0c41 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/TransportPlanServiceImpl.java @@ -601,6 +601,13 @@ public class TransportPlanServiceImpl extends BaseServiceImpl internals = internalRows(id); List externals = externalRows(id); + internals.forEach(this::clearOptionalDecimalSentinels); + externals.forEach(this::normalizeExternalOptionalDecimals); vo.setInternalDetails(internals); vo.setExternalDetails(externals); vo.setFeeSummary(feeSummary(vo.getFormalSettlementId(), externals)); @@ -298,6 +300,7 @@ public class TransportReconciliationServiceImpl List validExternals = new ArrayList<>(); for (int index = 0; index < rows.size(); index++) { VehicleReconciliationExcel row = rows.get(index); + normalizeImportVehicleDecimals(row); List validationErrors = validateImportVehicle(row); if (Func.isNotEmpty(validationErrors)) { // 原因同时写回原行:异常携带的是原表 rows, @@ -342,6 +345,7 @@ public class TransportReconciliationServiceImpl external.setExternalLineNo(index + 1); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + normalizeExternalOptionalDecimals(external); Map feeItems = row.getFeeItems(); if (feeItems == null || feeItems.isEmpty()) { feeItems = new HashMap<>(); @@ -427,6 +431,7 @@ public class TransportReconciliationServiceImpl List validExternals = new ArrayList<>(); for (int index = 0; index < rows.size(); index++) { CargoReconciliationExcel row = rows.get(index); + normalizeImportCargoDecimals(row); List validationErrors = validateImportCargo(row); if (Func.isNotEmpty(validationErrors)) { // 原因同时写回原行:异常携带的是原表 rows, @@ -471,6 +476,7 @@ public class TransportReconciliationServiceImpl external.setExternalLineNo(index + 1); external.setActualDepartureTime(parseTime(row.getActualDepartureTime(), "实际发货时间")); external.setActualCompletionTime(parseTimeNullable(row.getActualCompletionTime(), "实际完成时间")); + normalizeExternalOptionalDecimals(external); Map feeItems = row.getFeeItems(); if (feeItems == null || feeItems.isEmpty()) { feeItems = new LinkedHashMap<>(); @@ -729,10 +735,12 @@ public class TransportReconciliationServiceImpl target.setModel(source.getModel()); target.setTransportQuantity(nonNegative(source.getTransportQuantity(), "运输量")); target.setQuantityUnit(source.getQuantityUnit()); - target.setMileage(source.getMileage()); + target.setMileage(normalizeOptionalDecimal(source.getMileage())); target.setBatchNo(source.getBatchNo()); - target.setUnitPrice(source.getUnitPrice() == null ? null : nonNegative(source.getUnitPrice(), "运输单价")); - target.setFreightAmount(source.getFreightAmount() == null ? null : nonNegative(source.getFreightAmount(), "运输费")); + BigDecimal unitPrice = normalizeOptionalDecimal(source.getUnitPrice()); + target.setUnitPrice(unitPrice == null ? null : nonNegative(unitPrice, "运输单价")); + BigDecimal freightAmount = normalizeOptionalDecimal(source.getFreightAmount()); + target.setFreightAmount(freightAmount == null ? null : nonNegative(freightAmount, "运输费")); target.setFeeItemsJson(source.getFeeItemsJson()); target.setSettlementAmount(nonNegative(source.getSettlementAmount(), "结算费用合计")); target.setMatchedInternalId(source.getMatchedInternalId()); @@ -824,7 +832,6 @@ public class TransportReconciliationServiceImpl applyAmount(bill, internal, external); copyExternalToInternal(internal, external); internalMapper.updateById(internal); - updateWaybill(internal, external); } refreshFormalSummaryFees(bill.getFormalSettlementId()); recalculateSettlement(bill); @@ -867,50 +874,14 @@ public class TransportReconciliationServiceImpl internal.setModel(external.getModel()); internal.setTransportQuantity(external.getTransportQuantity()); internal.setQuantityUnit(external.getQuantityUnit()); - internal.setMileage(external.getMileage()); + internal.setMileage(normalizeOptionalDecimal(external.getMileage())); internal.setBatchNo(external.getBatchNo()); - internal.setUnitPrice(external.getUnitPrice()); - internal.setFreightAmount(external.getFreightAmount()); + internal.setUnitPrice(normalizeOptionalDecimal(external.getUnitPrice())); + internal.setFreightAmount(normalizeOptionalDecimal(external.getFreightAmount())); internal.setFeeItemsJson(external.getFeeItemsJson()); internal.setSettlementAmount(external.getSettlementAmount()); } - private void updateWaybill(TransportReconciliationInternal internal, - TransportReconciliationExternal external) { - Long waybillId = null; - FormalSettlementDetail detail = internal.getFormalSettlementDetailId() == null ? null - : formalDetailMapper.selectById(internal.getFormalSettlementDetailId()); - if (detail != null) waybillId = detail.getWaybillId(); - if (waybillId == null && internal.getSourceDetailId() != null) { - ReceivablePayableDetail source = receivablePayableMapper.selectById(internal.getSourceDetailId()); - if (source != null) waybillId = source.getWaybillId(); - } - Waybill waybill = waybillId == null ? null : waybillMapper.selectById(waybillId); - if (waybill == null && Func.isNotEmpty(internal.getWaybillNo())) { - waybill = waybillMapper.selectOne(Wrappers.lambdaQuery() - .eq(Waybill::getWaybillNo, internal.getWaybillNo())); - } - if (waybill == null) return; - waybill.setVehicleNo(external.getVehicleNo()); - waybill.setDepartureAddress(external.getDepartureAddress()); - waybill.setArrivalAddress(external.getArrivalAddress()); - waybill.setStartDate(external.getActualDepartureTime() == null ? null : external.getActualDepartureTime().toLocalDate()); - waybill.setEndDate(external.getActualCompletionTime() == null ? null : external.getActualCompletionTime().toLocalDate()); - waybill.setTransportType(external.getTransportType()); - waybill.setCargoName(external.getCargoName()); - waybill.setCargoType(external.getCargoType()); - waybill.setSpecification(external.getSpecification()); - waybill.setModel(external.getModel()); - waybill.setQuantity(external.getTransportQuantity()); - waybill.setQuantityUnit(external.getQuantityUnit()); - waybill.setMileage(external.getMileage()); - waybill.setBatchNo(external.getBatchNo()); - waybill.setUnitPrice(external.getUnitPrice()); - waybill.setFreightJson(external.getFeeItemsJson()); - waybill.setOtherFeeTotal(money(external.getSettlementAmount()).subtract(money(external.getFreightAmount())).max(BigDecimal.ZERO)); - waybillMapper.updateById(waybill); - } - @Override @Transactional(rollbackFor = Exception.class) public void complete(Long id) { @@ -1508,6 +1479,52 @@ public class TransportReconciliationServiceImpl } private BigDecimal money(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } private BigDecimal nonNegative(BigDecimal value, String field) { if (value == null || value.compareTo(BigDecimal.ZERO) < 0) throw new ServiceException(field + "不能小于0"); return value; } + + /** + * BladeX 数字空值哨兵 -1 与 Excel 未填统一归一为 null,避免导入后单价显示成 -1。 + */ + private BigDecimal normalizeOptionalDecimal(BigDecimal value) { + if (value == null) { + return null; + } + return value.compareTo(BigDecimal.ONE.negate()) == 0 ? null : value; + } + + private void normalizeImportVehicleDecimals(VehicleReconciliationExcel row) { + if (row == null) { + return; + } + row.setUnitPrice(normalizeOptionalDecimal(row.getUnitPrice())); + row.setMileage(normalizeOptionalDecimal(row.getMileage())); + row.setFreightAmount(normalizeOptionalDecimal(row.getFreightAmount())); + } + + private void normalizeImportCargoDecimals(CargoReconciliationExcel row) { + if (row == null) { + return; + } + row.setUnitPrice(normalizeOptionalDecimal(row.getUnitPrice())); + row.setMileage(normalizeOptionalDecimal(row.getMileage())); + row.setFreightAmount(normalizeOptionalDecimal(row.getFreightAmount())); + } + + private void normalizeExternalOptionalDecimals(TransportReconciliationExternal external) { + if (external == null) { + return; + } + external.setUnitPrice(normalizeOptionalDecimal(external.getUnitPrice())); + external.setMileage(normalizeOptionalDecimal(external.getMileage())); + external.setFreightAmount(normalizeOptionalDecimal(external.getFreightAmount())); + } + + private void clearOptionalDecimalSentinels(TransportReconciliationInternal internal) { + if (internal == null) { + return; + } + internal.setUnitPrice(normalizeOptionalDecimal(internal.getUnitPrice())); + internal.setMileage(normalizeOptionalDecimal(internal.getMileage())); + internal.setFreightAmount(normalizeOptionalDecimal(internal.getFreightAmount())); + } private String limit(String value, int max) { if (value != null && value.length() > max) throw new ServiceException("备注不能超过" + max + "个字"); return value; } private BigDecimal sumInternalQuantity(List rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } private BigDecimal sumExternalQuantity(List rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java index e01c3fc7..3553602e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java @@ -36,7 +36,10 @@ import org.springblade.transport.service.IWaybillService; import org.springblade.transport.wrapper.VoucherManageWrapper; import org.springframework.stereotype.Service; import org.springframework.context.ApplicationEventPublisher; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; import io.minio.MinioClient; import io.minio.GetPresignedObjectUrlArgs; @@ -86,6 +89,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl minioClientProvider; + private final TransactionTemplate requiresNewTransactionTemplate; private final String minioBucketName; private final String minioRootDirectory; private final String minioTransformEndpoint; @@ -98,6 +102,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl minioClientProvider, + PlatformTransactionManager transactionManager, @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName, @Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory, @Value("${file.storage.minio.transform-endpoint:${minio.transform-endpoint:}}") String minioTransformEndpoint, @@ -111,6 +116,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl relatedWaybillIds = new HashSet<>(); + assertParsableZipArchive(voucher.getFileName(), voucher.getFileUrl()); Path archivePath = downloadSourceFile(voucher.getFileUrl()); try { + assertZipMagic(archivePath); Charset archiveCharset = detectArchiveCharset(archivePath); log.info("[凭证处理] 已识别压缩包文件名编码 voucherId={}, charset={}", voucherId, archiveCharset.name()); try (InputStream source = Files.newInputStream(archivePath); @@ -1235,6 +1245,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl { VoucherManage update = new VoucherManage(); - update.setId(voucher.getId()); + update.setId(voucherId); update.setProcessStatus("处理失败"); updateById(update); - log.error("凭证压缩包处理失败 voucherId:{}", voucherId, exception); - throw new ServiceException("凭证压缩包处理失败"); + }); + } + + /** + * 上传后缀为 7z 或非 zip 时直接判失败:后台仅支持标准 zip 解压。 + */ + private void assertParsableZipArchive(String fileName, String fileUrl) { + String lowerName = Func.toStr(fileName, "").trim().toLowerCase(Locale.ROOT); + String lowerUrl = Func.toStr(fileUrl, "").trim().toLowerCase(Locale.ROOT); + if (lowerName.endsWith(".7z") || lowerUrl.contains(".7z")) { + throw new ServiceException("暂不支持 7z 压缩包,请转换为 zip 后重新上传"); + } + boolean looksLikeZip = lowerName.endsWith(".zip") || lowerUrl.contains(".zip"); + if (!looksLikeZip) { + throw new ServiceException("仅支持 zip 压缩包,当前文件无法解析"); + } + } + + /** + * 校验本地临时文件是否为 zip(PK 头),排除随意改后缀的文档。 + */ + private void assertZipMagic(Path archivePath) throws Exception { + byte[] header = new byte[4]; + try (InputStream inputStream = Files.newInputStream(archivePath)) { + int read = inputStream.read(header); + if (read < 4 || header[0] != 'P' || header[1] != 'K') { + throw new ServiceException("文件不是有效的 zip 压缩包,无法解析"); + } } } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java index fa90287b..83b0c711 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillServiceImpl.java @@ -1059,8 +1059,9 @@ public class WaybillServiceImpl extends BaseServiceImpl waybill.setProcessJson(oldRecord.getProcessJson()); } } else { + // 新建手工运单不应带配载号;总单/计划调度写入的 masterNo、relationNo 必须保留, + // 否则总单进度按 masterNo 汇总会一直显示「已调度 0」。 waybill.setLoadingNo(null); - waybill.setMasterNo(null); waybill.setMileageRemark(null); } // 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java index eb7d9a43..ba6c7ddd 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/LoadableAdmissionSupport.java @@ -29,7 +29,7 @@ import java.util.List; import java.util.Locale; /** - * 可配载准入判据:公路 + 待执行 + 未关联其它配载单。 + * 可配载准入判据:公路 + 待执行/进行中 + 未关联其它配载单。 *

* 接单状态不进判据:拒单票照样可配载。配载提交会把它的接单事实清成待接单, * 否则新司机看不到这张票——司机端入口一律排除已拒绝的运单。 @@ -38,8 +38,11 @@ import java.util.Locale; */ public final class LoadableAdmissionSupport { - /** 可配载只放行待执行:已完成、已取消、草稿与调度中间态一律不可配载 */ - public static final String LOADABLE_BUSINESS_STATUS = WaybillProcessSupport.STATUS_PENDING; + /** 可配载业务状态:待执行、进行中;已完成、已取消、草稿与调度中间态一律不可配载 */ + public static final List LOADABLE_BUSINESS_STATUSES = List.of( + WaybillProcessSupport.STATUS_PENDING, + WaybillProcessSupport.STATUS_RUNNING + ); /** * 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。 @@ -56,7 +59,7 @@ public final class LoadableAdmissionSupport { } /** - * 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。 + * 不可配载的原因,按判定顺序排列:公路 → 待执行/进行中 → 未关联其它配载单。 *

* 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。 */ @@ -65,8 +68,8 @@ public final class LoadableAdmissionSupport { /** 运输方式不是公路 */ NOT_ROAD, - /** 业务状态不是待执行 */ - NOT_PENDING, + /** 业务状态不是待执行或进行中 */ + NOT_LOADABLE_STATUS, /** 已关联其它配载单 */ ON_OTHER_MANIFEST @@ -119,8 +122,8 @@ public final class LoadableAdmissionSupport { if (waybill == null || !isRoadTransport(waybill.getTransportType())) { return UnloadableReason.NOT_ROAD; } - if (!LOADABLE_BUSINESS_STATUS.equals(text(waybill.getBusinessStatus()))) { - return UnloadableReason.NOT_PENDING; + if (!LOADABLE_BUSINESS_STATUSES.contains(text(waybill.getBusinessStatus()))) { + return UnloadableReason.NOT_LOADABLE_STATUS; } String loadingNo = text(waybill.getLoadingNo()); if (loadingNo.isEmpty() || loadingNo.equals(text(ownLoadingNo))) { @@ -148,13 +151,13 @@ public final class LoadableAdmissionSupport { } /** - * 把整条可配载判据加到运单查询上:公路 + 待执行 + 未配载。 + * 把整条可配载判据加到运单查询上:公路 + 待执行/进行中 + 未配载。 * * @param wrapper 运单列表查询条件 */ public static void applyLoadableConditions(LambdaQueryWrapper wrapper) { applyRoadTransportCondition(wrapper); - wrapper.eq(Waybill::getBusinessStatus, LOADABLE_BUSINESS_STATUS); + wrapper.in(Waybill::getBusinessStatus, LOADABLE_BUSINESS_STATUSES); wrapper.and(nested -> nested.isNull(Waybill::getLoadingNo).or().eq(Waybill::getLoadingNo, "")); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java index 52ab19f9..6de2e6e7 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/LoadingManageChildStatusWriteTest.java @@ -139,13 +139,14 @@ class LoadingManageChildStatusWriteTest { } @Test - @DisplayName("不需接单的项目:提交时子单直接落进行中,不留待执行等读路径去提升") + @DisplayName("不需接单的项目:提交时配载单与子单均为进行中") void submitWritesRunningForProjectWithoutAcceptNode() { stubWaybills(waybill(NO_ACCEPT_WAYBILL_ID, NO_ACCEPT_PROJECT_ID)); stubConfigs(config(NO_ACCEPT_PROJECT_ID, NO_ACCEPT_JSON)); assertTrue(service.submit(loadingForm(NO_ACCEPT_WAYBILL_ID))); + assertEquals("running", capturedLoadingStatus()); CapturedUpdate statusUpdate = statusSyncUpdates().get(0); assertEquals("running", statusUpdate.setValue("business_status")); assertEquals(List.of(NO_ACCEPT_WAYBILL_ID), statusUpdate.whereIds()); @@ -153,13 +154,14 @@ class LoadingManageChildStatusWriteTest { } @Test - @DisplayName("需接单的项目:提交时子单落待执行,等新司机接单") + @DisplayName("需接单的项目:提交时配载单与子单均为待执行,等新司机接单") void submitWritesPendingForProjectWithAcceptNode() { stubWaybills(waybill(NEED_ACCEPT_WAYBILL_ID, NEED_ACCEPT_PROJECT_ID)); stubConfigs(config(NEED_ACCEPT_PROJECT_ID, NEED_ACCEPT_JSON)); assertTrue(service.submit(loadingForm(NEED_ACCEPT_WAYBILL_ID))); + assertEquals("pending", capturedLoadingStatus()); assertEquals("pending", statusSyncUpdates().get(0).setValue("business_status")); } @@ -367,12 +369,18 @@ class LoadingManageChildStatusWriteTest { waybill.setProjectId(projectId); waybill.setProcessJson(processJson); waybill.setDeptId(DEPT_ID_LONG); - // 可配载只放行公路 + 待执行 + 未配载,夹具的票都是这种票 + // 可配载放行公路 + 待执行/进行中 + 未配载,夹具默认待执行 waybill.setTransportType("公路运输"); waybill.setBusinessStatus("pending"); return waybill; } + private String capturedLoadingStatus() { + ArgumentCaptor captor = ArgumentCaptor.forClass(LoadingManage.class); + verify(service, atLeastOnce()).saveOrUpdate(captor.capture()); + return captor.getValue().getBusinessStatus(); + } + private void stubWaybills(Waybill... waybills) { when(waybillMapper.selectList(ArgumentMatchers.>any())).thenReturn(List.of(waybills)); } diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java index 54e6eacf..9a7b6813 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillLoadableQueryTest.java @@ -68,7 +68,7 @@ class WaybillLoadableQueryTest { } @Test - @DisplayName("开关开启:公路词表、待执行、未配载一次全部进 wrapper,接单状态不参与") + @DisplayName("开关开启:公路词表、待执行/进行中、未配载一次全部进 wrapper,接单状态不参与") void loadableSwitchAppliesWholePredicate() { LambdaQueryWrapper wrapper = capturedQuery(loadableQuery()); String sql = String.valueOf(wrapper.getTargetSql()); @@ -79,10 +79,8 @@ class WaybillLoadableQueryTest { assertTrue(params.contains("%" + keyword + "%"), "公路词表要整份进条件,只写 road 会漏掉中文取值的运单:" + params); } - assertTrue(sql.contains("business_status =") && params.contains("pending"), - "只放行待执行:" + sql + " " + params); - assertFalse(params.contains("running"), - "进行中一律不可配载,不能沿用「待执行与进行中都可配载」的旧口径:" + params); + assertTrue(sql.contains("business_status") && params.contains("pending") && params.contains("running"), + "放行待执行与进行中:" + sql + " " + params); assertTrue(sql.contains("loading_no IS NULL") && sql.contains("loading_no ="), "未配载要同时覆盖 NULL 与空串:" + sql); assertFalse(sql.contains("driver_accept_status"), diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java index 9b5c72ad..cb6cb597 100644 --- a/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/support/LoadableAdmissionSupportTest.java @@ -50,10 +50,12 @@ class LoadableAdmissionSupportTest { // ------------------------------------------------------------ 单张运单准入 @Test - @DisplayName("公路 + 待执行 + 未配载才可配载") + @DisplayName("公路 + 待执行/进行中 + 未配载才可配载") void loadableOnlyWhenAllConditionsHold() { assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "pending", null, "pending"), null)); assertTrue(LoadableAdmissionSupport.isLoadable(waybill("公路运输", "pending", "", null), null)); + assertTrue(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null), + "进行中也可配载"); } @Test @@ -64,12 +66,10 @@ class LoadableAdmissionSupportTest { } @Test - @DisplayName("非公路、非待执行、已关联配载单各自单独即可拒绝") + @DisplayName("非公路、非待执行/进行中、已关联配载单各自单独即可拒绝") void eachConditionRejectsOnItsOwn() { assertFalse(LoadableAdmissionSupport.isLoadable(waybill("railway", "pending", null, "pending"), null), "非公路不可配载"); - assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "running", null, null), null), - "进行中不可配载:不需要接单的项目保存后即为进行中,那是过程配置的产物,不代表可配载"); assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "draft", null, null), null)); assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "completed", null, null), null)); assertFalse(LoadableAdmissionSupport.isLoadable(waybill("road", "cancelled", null, null), null)); @@ -79,18 +79,19 @@ class LoadableAdmissionSupportTest { } @Test - @DisplayName("不可配载原因按 公路 → 待执行 → 未关联其它配载单 取第一条") + @DisplayName("不可配载原因按 公路 → 待执行/进行中 → 未关联其它配载单 取第一条") void unloadableReasonFollowsPredicateOrder() { assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_ROAD, LoadableAdmissionSupport.unloadableReason( - waybill("railway", "running", "PZ20260101-002", null), "PZ20260101-001")); - assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_PENDING, + waybill("railway", "completed", "PZ20260101-002", null), "PZ20260101-001")); + assertEquals(LoadableAdmissionSupport.UnloadableReason.NOT_LOADABLE_STATUS, LoadableAdmissionSupport.unloadableReason( - waybill("road", "running", "PZ20260101-002", null), "PZ20260101-001")); + waybill("road", "completed", "PZ20260101-002", null), "PZ20260101-001")); assertEquals(LoadableAdmissionSupport.UnloadableReason.ON_OTHER_MANIFEST, LoadableAdmissionSupport.unloadableReason( waybill("road", "pending", "PZ20260101-002", null), "PZ20260101-001")); assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "pending", null, null), null)); + assertNull(LoadableAdmissionSupport.unloadableReason(waybill("road", "running", null, null), null)); assertNull(LoadableAdmissionSupport.unloadableReason( waybill("road", "pending", "PZ20260101-001", null), "PZ20260101-001")); } diff --git a/doc/sql/transport/blade_credit_score_quantification.sql b/doc/sql/transport/blade_credit_score_quantification.sql index ed9a8bdd..4bc6df40 100644 --- a/doc/sql/transport/blade_credit_score_quantification.sql +++ b/doc/sql/transport/blade_credit_score_quantification.sql @@ -8,6 +8,7 @@ CREATE TABLE `blade_credit_score_quantification` ( `name` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '评定表名称', `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '备注', `standard_description` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '标准说明', + `max_minus_score` decimal(10,2) NULL DEFAULT NULL COMMENT '减分项目最高扣分限制', `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', `create_time` datetime NULL DEFAULT NULL COMMENT '创建时间', diff --git a/doc/sql/transport/blade_credit_score_quantification_export_insert.sql b/doc/sql/transport/blade_credit_score_quantification_export_insert.sql new file mode 100644 index 00000000..d15ae98d --- /dev/null +++ b/doc/sql/transport/blade_credit_score_quantification_export_insert.sql @@ -0,0 +1,421 @@ +-- ============================================================================ +-- 评分量化表跨环境复制:按 ID 生成 INSERT 脚本 +-- ============================================================================ +-- 用法(在 DEV 库执行): +-- 1. 修改下方 @quantification_id 为要复制的评分量化表主键 +-- 2. 整段执行,结果集中每一行是一条可直接在 UAT/PROD 执行的 SQL +-- 3. 先复制「清理」语句,再复制各表 INSERT,按结果顺序在目标库执行 +-- +-- 覆盖表(按外键依赖顺序): +-- blade_credit_score_quantification +-- blade_credit_score_category +-- blade_credit_score_item +-- blade_credit_score_item_option +-- blade_credit_rating_standard +-- +-- 说明: +-- - 保留原主键与关联 ID,便于 UAT/PROD 与 DEV 配置一致 +-- - 仅导出 is_deleted = 0 的有效数据 +-- - 目标库若已存在同名评定表(tenant_id + name + is_deleted 唯一),请先处理冲突 +-- ============================================================================ + +-- >>> 请修改为要导出的评分量化表 ID <<< +SET @quantification_id := 0; + +-- ---------------------------------------------------------------------------- +-- 0. 导出前校验:确认主表存在及子表数量 +-- ---------------------------------------------------------------------------- +SELECT + q.id, + q.tenant_id, + q.name, + q.status, + q.max_minus_score, + (SELECT COUNT(1) FROM blade_credit_score_category c + WHERE c.quantification_id = q.id AND c.is_deleted = 0) AS category_count, + (SELECT COUNT(1) FROM blade_credit_score_item i + WHERE i.quantification_id = q.id AND i.is_deleted = 0) AS item_count, + (SELECT COUNT(1) FROM blade_credit_score_item_option o + WHERE o.quantification_id = q.id AND o.is_deleted = 0) AS option_count, + (SELECT COUNT(1) FROM blade_credit_rating_standard s + WHERE s.quantification_id = q.id AND s.is_deleted = 0) AS standard_count +FROM blade_credit_score_quantification q +WHERE q.id = @quantification_id + AND q.is_deleted = 0; + +-- ---------------------------------------------------------------------------- +-- 1. 目标库清理语句(先删子表再删主表;结果复制到 UAT/PROD 优先执行) +-- ---------------------------------------------------------------------------- +SELECT CONCAT( + '-- ========== 目标库清理(quantification_id=', @quantification_id, ') ==========\n', + 'DELETE FROM `blade_credit_score_item_option` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_score_item` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_score_category` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_rating_standard` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_score_quantification` WHERE `id` = ', @quantification_id, ';\n', + '-- 若目标库存在同名不同 ID 的评定表,可按名称清理(按需取消注释):\n', + '-- DELETE o FROM `blade_credit_score_item_option` o\n', + '-- INNER JOIN `blade_credit_score_quantification` q ON q.id = o.quantification_id\n', + '-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.is_deleted = 0;\n', + '-- DELETE i FROM `blade_credit_score_item` i\n', + '-- INNER JOIN `blade_credit_score_quantification` q ON q.id = i.quantification_id\n', + '-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.is_deleted = 0;\n', + '-- DELETE c FROM `blade_credit_score_category` c\n', + '-- INNER JOIN `blade_credit_score_quantification` q ON q.id = c.quantification_id\n', + '-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.is_deleted = 0;\n', + '-- DELETE s FROM `blade_credit_rating_standard` s\n', + '-- INNER JOIN `blade_credit_score_quantification` q ON q.id = s.quantification_id\n', + '-- WHERE q.tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND q.is_deleted = 0;\n', + '-- DELETE FROM `blade_credit_score_quantification`\n', + '-- WHERE tenant_id = ', IFNULL(QUOTE((SELECT tenant_id FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND name = ', IFNULL(QUOTE((SELECT name FROM blade_credit_score_quantification WHERE id = @quantification_id LIMIT 1)), 'NULL'), + ' AND is_deleted = 0;' +) AS sql_text; + +-- ---------------------------------------------------------------------------- +-- 2. 主表 blade_credit_score_quantification +-- ---------------------------------------------------------------------------- +SELECT CONCAT( + 'INSERT INTO `blade_credit_score_quantification` (', + '`id`, `tenant_id`, `name`, `remark`, `standard_description`, `max_minus_score`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + IFNULL(QUOTE(name), 'NULL'), ', ', + IFNULL(QUOTE(remark), 'NULL'), ', ', + IFNULL(QUOTE(standard_description), 'NULL'), ', ', + IFNULL(CAST(max_minus_score AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' +) AS sql_text +FROM blade_credit_score_quantification +WHERE id = @quantification_id + AND is_deleted = 0; + +-- ---------------------------------------------------------------------------- +-- 3. 分类 blade_credit_score_category +-- ---------------------------------------------------------------------------- +SELECT CONCAT( + 'INSERT INTO `blade_credit_score_category` (', + '`id`, `tenant_id`, `quantification_id`, `category_code`, `category_name`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + IFNULL(QUOTE(category_code), 'NULL'), ', ', + IFNULL(QUOTE(category_name), 'NULL'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' +) AS sql_text +FROM blade_credit_score_category +WHERE quantification_id = @quantification_id + AND is_deleted = 0 +ORDER BY sort, id; + +-- ---------------------------------------------------------------------------- +-- 4. 评分项目 blade_credit_score_item +-- ---------------------------------------------------------------------------- +SELECT CONCAT( + 'INSERT INTO `blade_credit_score_item` (', + '`id`, `tenant_id`, `quantification_id`, `category_id`, `category_code`, `item_name`, ', + '`option_type`, `base_value`, `score`, `score_description`, `option_description`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + category_id, ', ', + IFNULL(QUOTE(category_code), 'NULL'), ', ', + IFNULL(QUOTE(item_name), 'NULL'), ', ', + IFNULL(QUOTE(option_type), 'NULL'), ', ', + IFNULL(CAST(base_value AS CHAR), 'NULL'), ', ', + IFNULL(CAST(score AS CHAR), '0.00'), ', ', + IFNULL(QUOTE(score_description), 'NULL'), ', ', + IFNULL(QUOTE(option_description), 'NULL'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' +) AS sql_text +FROM blade_credit_score_item +WHERE quantification_id = @quantification_id + AND is_deleted = 0 +ORDER BY sort, id; + +-- ---------------------------------------------------------------------------- +-- 5. 评分项目选项 blade_credit_score_item_option +-- ---------------------------------------------------------------------------- +SELECT CONCAT( + 'INSERT INTO `blade_credit_score_item_option` (', + '`id`, `tenant_id`, `quantification_id`, `item_id`, `option_name`, ', + '`change_type`, `change_value`, `change_unit`, `score_type`, `score`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + item_id, ', ', + IFNULL(QUOTE(option_name), 'NULL'), ', ', + IFNULL(QUOTE(change_type), 'NULL'), ', ', + IFNULL(CAST(change_value AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(change_unit), 'NULL'), ', ', + IFNULL(QUOTE(score_type), 'NULL'), ', ', + IFNULL(CAST(score AS CHAR), '0.00'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' +) AS sql_text +FROM blade_credit_score_item_option +WHERE quantification_id = @quantification_id + AND is_deleted = 0 +ORDER BY sort, id; + +-- ---------------------------------------------------------------------------- +-- 6. 信用等级评估标准 blade_credit_rating_standard +-- ---------------------------------------------------------------------------- +SELECT CONCAT( + 'INSERT INTO `blade_credit_rating_standard` (', + '`id`, `tenant_id`, `quantification_id`, `credit_level`, ', + '`score_rate_lower`, `score_rate_upper`, `score_rate_increase`, ', + '`credit_limit_lower`, `credit_limit_upper`, `credit_limit_increase`, ', + '`standard_description`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + IFNULL(QUOTE(credit_level), 'NULL'), ', ', + IFNULL(CAST(score_rate_lower AS CHAR), 'NULL'), ', ', + IFNULL(CAST(score_rate_upper AS CHAR), 'NULL'), ', ', + IFNULL(CAST(score_rate_increase AS CHAR), '1'), ', ', + IFNULL(CAST(credit_limit_lower AS CHAR), '0.00'), ', ', + IFNULL(CAST(credit_limit_upper AS CHAR), '0.00'), ', ', + IFNULL(CAST(credit_limit_increase AS CHAR), '0.00'), ', ', + IFNULL(QUOTE(standard_description), 'NULL'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' +) AS sql_text +FROM blade_credit_rating_standard +WHERE quantification_id = @quantification_id + AND is_deleted = 0 +ORDER BY sort, id; + +-- ---------------------------------------------------------------------------- +-- 7. 汇总:一次性输出全部 SQL(便于整段复制;MySQL 客户端需允许 GROUP_CONCAT 长度) +-- ---------------------------------------------------------------------------- +SET SESSION group_concat_max_len = 1024 * 1024 * 16; + +SELECT CONCAT_WS( + '\n', + -- 清理 + CONCAT( + '-- ========== 目标库清理(quantification_id=', @quantification_id, ') ==========\n', + 'DELETE FROM `blade_credit_score_item_option` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_score_item` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_score_category` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_rating_standard` WHERE `quantification_id` = ', @quantification_id, ';\n', + 'DELETE FROM `blade_credit_score_quantification` WHERE `id` = ', @quantification_id, ';' + ), + '', + '-- ========== 主表 ==========', + (SELECT GROUP_CONCAT( + CONCAT( + 'INSERT INTO `blade_credit_score_quantification` (', + '`id`, `tenant_id`, `name`, `remark`, `standard_description`, `max_minus_score`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + IFNULL(QUOTE(name), 'NULL'), ', ', + IFNULL(QUOTE(remark), 'NULL'), ', ', + IFNULL(QUOTE(standard_description), 'NULL'), ', ', + IFNULL(CAST(max_minus_score AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' + ) + SEPARATOR '\n' + ) + FROM blade_credit_score_quantification + WHERE id = @quantification_id AND is_deleted = 0), + '', + '-- ========== 分类 ==========', + IFNULL((SELECT GROUP_CONCAT( + CONCAT( + 'INSERT INTO `blade_credit_score_category` (', + '`id`, `tenant_id`, `quantification_id`, `category_code`, `category_name`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + IFNULL(QUOTE(category_code), 'NULL'), ', ', + IFNULL(QUOTE(category_name), 'NULL'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' + ) + ORDER BY sort, id + SEPARATOR '\n' + ) + FROM blade_credit_score_category + WHERE quantification_id = @quantification_id AND is_deleted = 0), ''), + '', + '-- ========== 评分项目 ==========', + IFNULL((SELECT GROUP_CONCAT( + CONCAT( + 'INSERT INTO `blade_credit_score_item` (', + '`id`, `tenant_id`, `quantification_id`, `category_id`, `category_code`, `item_name`, ', + '`option_type`, `base_value`, `score`, `score_description`, `option_description`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + category_id, ', ', + IFNULL(QUOTE(category_code), 'NULL'), ', ', + IFNULL(QUOTE(item_name), 'NULL'), ', ', + IFNULL(QUOTE(option_type), 'NULL'), ', ', + IFNULL(CAST(base_value AS CHAR), 'NULL'), ', ', + IFNULL(CAST(score AS CHAR), '0.00'), ', ', + IFNULL(QUOTE(score_description), 'NULL'), ', ', + IFNULL(QUOTE(option_description), 'NULL'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' + ) + ORDER BY sort, id + SEPARATOR '\n' + ) + FROM blade_credit_score_item + WHERE quantification_id = @quantification_id AND is_deleted = 0), ''), + '', + '-- ========== 评分项目选项 ==========', + IFNULL((SELECT GROUP_CONCAT( + CONCAT( + 'INSERT INTO `blade_credit_score_item_option` (', + '`id`, `tenant_id`, `quantification_id`, `item_id`, `option_name`, ', + '`change_type`, `change_value`, `change_unit`, `score_type`, `score`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + item_id, ', ', + IFNULL(QUOTE(option_name), 'NULL'), ', ', + IFNULL(QUOTE(change_type), 'NULL'), ', ', + IFNULL(CAST(change_value AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(change_unit), 'NULL'), ', ', + IFNULL(QUOTE(score_type), 'NULL'), ', ', + IFNULL(CAST(score AS CHAR), '0.00'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' + ) + ORDER BY sort, id + SEPARATOR '\n' + ) + FROM blade_credit_score_item_option + WHERE quantification_id = @quantification_id AND is_deleted = 0), ''), + '', + '-- ========== 信用等级评估标准 ==========', + IFNULL((SELECT GROUP_CONCAT( + CONCAT( + 'INSERT INTO `blade_credit_rating_standard` (', + '`id`, `tenant_id`, `quantification_id`, `credit_level`, ', + '`score_rate_lower`, `score_rate_upper`, `score_rate_increase`, ', + '`credit_limit_lower`, `credit_limit_upper`, `credit_limit_increase`, ', + '`standard_description`, `sort`, ', + '`create_user`, `create_dept`, `create_time`, `update_user`, `update_time`, `status`, `is_deleted`', + ') VALUES (', + id, ', ', + IFNULL(QUOTE(tenant_id), 'NULL'), ', ', + quantification_id, ', ', + IFNULL(QUOTE(credit_level), 'NULL'), ', ', + IFNULL(CAST(score_rate_lower AS CHAR), 'NULL'), ', ', + IFNULL(CAST(score_rate_upper AS CHAR), 'NULL'), ', ', + IFNULL(CAST(score_rate_increase AS CHAR), '1'), ', ', + IFNULL(CAST(credit_limit_lower AS CHAR), '0.00'), ', ', + IFNULL(CAST(credit_limit_upper AS CHAR), '0.00'), ', ', + IFNULL(CAST(credit_limit_increase AS CHAR), '0.00'), ', ', + IFNULL(QUOTE(standard_description), 'NULL'), ', ', + IFNULL(CAST(sort AS CHAR), '0'), ', ', + IFNULL(CAST(create_user AS CHAR), 'NULL'), ', ', + IFNULL(CAST(create_dept AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(create_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(update_user AS CHAR), 'NULL'), ', ', + IFNULL(QUOTE(DATE_FORMAT(update_time, '%Y-%m-%d %H:%i:%s')), 'NULL'), ', ', + IFNULL(CAST(status AS CHAR), 'NULL'), ', ', + IFNULL(CAST(is_deleted AS CHAR), '0'), + ');' + ) + ORDER BY sort, id + SEPARATOR '\n' + ) + FROM blade_credit_rating_standard + WHERE quantification_id = @quantification_id AND is_deleted = 0), '') +) AS full_export_sql; diff --git a/doc/sql/transport/blade_credit_score_quantification_max_minus_score_patch_20261023.sql b/doc/sql/transport/blade_credit_score_quantification_max_minus_score_patch_20261023.sql new file mode 100644 index 00000000..9425b13a --- /dev/null +++ b/doc/sql/transport/blade_credit_score_quantification_max_minus_score_patch_20261023.sql @@ -0,0 +1,5 @@ +-- 评分量化表:减分项目最高扣分限制 +-- 客商自评/复评时,所有减分项合计扣分不得超过该值;超出则按该上限计分 + +ALTER TABLE `blade_credit_score_quantification` + ADD COLUMN `max_minus_score` decimal(10,2) NULL DEFAULT NULL COMMENT '减分项目最高扣分限制' AFTER `standard_description`;