✨ 评分量化减分累计与最高扣分限制,并修复运输对账与结算调整单等问题

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-23 04:28:19 +08:00
parent dea72691de
commit 49c149c675
20 changed files with 763 additions and 140 deletions
@@ -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;
}
@@ -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<CreditRatingStandardVO> standards = new ArrayList<>();
/**
* 来自评分量化表的减分项目最高扣分限制(非落库字段,供前端计分使用)
*/
@TableField(exist = false)
@Schema(description = "减分项目最高扣分限制")
private BigDecimal maxMinusScore;
}
@@ -96,8 +96,7 @@ public final class SettlementImportTemplateExporter {
private SettlementImportTemplateExporter() {
}
public static void export(HttpServletResponse response, List<String> transportTypeOptions,
List<String> feeItemNames) {
public static void export(HttpServletResponse response, List<String> transportTypeOptions) {
List<String> headers = buildHeaders();
// 运输方式由调用方传入业务字典 transport_type 的字典名称,禁止写死
List<String> 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<String> transportOptions,
List<String> 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<String> 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);
@@ -458,6 +458,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
quantification.setName(trimToEmpty(quantification.getName()));
quantification.setRemark(trimToNull(quantification.getRemark()));
quantification.setStandardDescription(trimToNull(quantification.getStandardDescription()));
if (quantification.getMaxMinusScore() != null
&& quantification.getMaxMinusScore().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("减分项目最高扣分限制不能小于0");
}
if (Func.isEmpty(quantification.getStatus())) {
quantification.setStatus(STATUS_DRAFT);
}
@@ -472,6 +476,10 @@ public class CreditScoreQuantificationServiceImpl extends BaseServiceImpl<Credit
validateLength(quantification.getName(), NAME_MAX_LENGTH, "评定表名称最多30字符");
validateLength(quantification.getRemark(), REMARK_MAX_LENGTH, "备注最多500字符");
validateLength(quantification.getStandardDescription(), REMARK_MAX_LENGTH, "标准说明最多500字符");
if (quantification.getMaxMinusScore() != null
&& quantification.getMaxMinusScore().compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException("减分项目最高扣分限制不能小于0");
}
LambdaQueryWrapper<CreditScoreQuantification> queryWrapper = Wrappers.<CreditScoreQuantification>lambdaQuery()
.eq(CreditScoreQuantification::getName, quantification.getName())
.eq(CreditScoreQuantification::getIsDeleted, 0);
@@ -280,6 +280,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
customer.setCustomerCode(nextCustomerCode());
}
CustomerArchive entity = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchive.class));
// 曾审核通过后的评分替换:保存/再提交时仍沿用原生效等级与额度,待再次审核通过后再切换
this.keepEffectiveCreditUntilReapproved(before, entity);
boolean result = saveOrUpdate(entity);
customer.setId(entity.getId());
replaceDetail(entity.getId(), customer);
@@ -420,6 +422,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setCurrentNode("审核通过");
after.setCurrentProcessor(Func.isEmpty(processorName) ? "系统" : processorName.trim());
after.setApprovedTime(LocalDateTime.now());
// 审核通过后按当前未删除评分刷新生效等级/额度(替换旧评分场景在此切换生效)
this.applyCreditFromActiveScore(after);
addChangeRecord(id, "客商准入审核通过", before, after);
boolean updated = updateById(after);
log.info("流程结束审核通过 id={} processor={} result={}", id, after.getCurrentProcessor(), updated);
@@ -484,6 +488,8 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
after.setCurrentNode("审核通过");
after.setCurrentProcessor(AuthUtil.getUserName());
after.setApprovedTime(LocalDateTime.now());
// 审核通过后按当前未删除评分刷新生效等级/额度(替换旧评分场景在此切换生效)
this.applyCreditFromActiveScore(after);
addChangeRecord(id, "客商准入审核通过", before, after);
return updateById(after);
}
@@ -943,6 +949,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
score.setScoreDate(LocalDate.now());
score.setSelfStatus("未完成");
score.setReviewStatus("未完成");
score.setMaxMinusScore(quantification.getMaxMinusScore());
score.setDetails(loadScoreTemplateDetails(quantification.getId()));
return score;
}
@@ -1400,6 +1407,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl<CustomerArchiveM
}
private void deleteScores(List<Long> customerIds) {
// 逻辑删除保留历史评分明细;曾审核通过的旧评分在再次审核通过前仍通过主表等级/额度暂时生效
List<CustomerCreditScore> scores = creditScoreMapper.selectList(Wrappers.<CustomerCreditScore>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<CustomerArchiveM
}
}
/**
* 客商曾审核通过后,保存或再次提审时不覆盖主表已生效的等级/额度。
*/
private void keepEffectiveCreditUntilReapproved(CustomerArchive before, CustomerArchive entity) {
if (before == null || before.getApprovedTime() == null || entity == null) {
return;
}
entity.setCustomerLevel(before.getCustomerLevel());
entity.setMaxCreditLimit(before.getMaxCreditLimit());
entity.setApplyCreditLimit(before.getApplyCreditLimit());
}
/**
* 按当前未删除评分刷新客商生效等级与额度;优先取复评已完成的评分。
*/
private void applyCreditFromActiveScore(CustomerArchive customer) {
if (customer == null || customer.getId() == null) {
return;
}
List<CustomerCreditScoreVO> 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<CustomerContactVO> loadContacts(Long customerId) {
return contactMapper.selectList(Wrappers.<CustomerContact>lambdaQuery()
.eq(CustomerContact::getCustomerId, customerId)
@@ -190,8 +190,6 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setDeptName(oldRecord.getDeptName());
}
prepareCreateOrUpdate(loadingManage);
// 进行中编辑:保持 running;草稿/新建确认后统一为待执行
loadingManage.setBusinessStatus(runningEdit ? STATUS_RUNNING : STATUS_PENDING);
if (Func.isEmpty(loadingManage.getLoadingNo())) {
loadingManage.setLoadingNo(nextCode());
}
@@ -200,6 +198,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
if (runningEdit && oldRecord != null) {
validateRunningEdit(oldRecord, loadingManage, waybills, oldWaybillIds);
}
// 进行中编辑保持 running;新建/草稿确认按项目过程配置:需接单→待执行,无需接单→进行中
loadingManage.setBusinessStatus(runningEdit ? STATUS_RUNNING : deriveLoadingBusinessStatus(waybills));
boolean result = saveOrUpdate(loadingManage);
// 进行中编辑:保留原有子单状态与接单事实,仅对新加入子单做派车同步
if (runningEdit) {
@@ -872,8 +872,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
String prefix = "运单" + waybill.getWaybillNo();
return prefix + switch (reason) {
case NOT_ROAD -> "运输方式不是公路,不可配载";
case NOT_PENDING -> "当前状态不是待执行,不可配载";
case ON_OTHER_MANIFEST -> "已关联其他配载单";
case NOT_LOADABLE_STATUS -> "当前状态不是待执行或进行中,不可配载";
case ON_OTHER_MANIFEST -> "已关联其他有效配载单";
};
}
@@ -1087,6 +1087,27 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.set(Waybill::getDriverRejectReason, null));
}
/**
* 配载单业务状态按子运单所属项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。
* <p>
* 一张配载单的子运单同属一个项目,任一张需接单则整单待执行。
*/
private String deriveLoadingBusinessStatus(List<Waybill> waybills) {
if (Func.isEmpty(waybills)) {
return STATUS_RUNNING;
}
Map<Long, String> 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;
}
/**
* 子运单业务状态逐张按各自项目的实时过程配置推导:需要接单 → 待执行,不需要 → 进行中。
* <p>
@@ -2131,21 +2131,18 @@ public class PaymentApplicationServiceImpl extends BaseServiceImpl<PaymentApplic
if (settlements.isEmpty()) return;
settlementRelationMapper.delete(Wrappers.<PaymentApplicationSettlement>lambdaQuery()
.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<PaymentApplic
return;
}
// 单选/多选均落关联行,生成即占用可付款额度(删除草稿或审核不通过后释放)
boolean customAmounts = targetSettlements.size() > 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<PaymentApplic
}
}
private void assertLineTotalMatches(PaymentApplication entity, BigDecimal lineTotal) {
BigDecimal appliedAmount = money(entity.getAppliedAmount()).setScale(2, RoundingMode.HALF_UP);
if (lineTotal.setScale(2, RoundingMode.HALF_UP).compareTo(appliedAmount) != 0) {
throw new ServiceException("申请付款金额须等于各结算单付款金额合计");
}
/** 前端传入结算行付款金额时,按行落库,不再要求合计等于申请付款金额。 */
private boolean useSpecifiedLineAmounts(List<PaymentApplicationSettlementLineRequest> lines) {
return lines != null && !lines.isEmpty();
}
private void validateQuota(PaymentApplication entity) {
@@ -119,8 +119,9 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
.orderByDesc(FormalSettlement::getCreateTime));
Set<Long> 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<Map<String, Object>> result = new ArrayList<>();
for (FormalSettlement row : rows) {
@@ -467,9 +468,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
if (isSettlementCompleted(formal)) {
throw new ServiceException("该正式结算单已完成收/付款,不能进行结算调整");
}
if (isTailSettlement(formal)) {
throw new ServiceException("尾款结算正式结算单不能进行结算调整");
}
if (hasTailPayment(formal.getId())) {
throw new ServiceException("该正式结算单已发起尾款结算,不能进行结算调整");
}
@@ -480,9 +478,6 @@ public class SettlementAdjustmentServiceImpl extends BaseServiceImpl<SettlementA
|| (money(formal.getSettlementAmount()).signum() > 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);
@@ -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<String> resolveFeeItemNames(SettlementImportBatchRequest request) {
LinkedHashSet<String> 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<String> names = new LinkedHashSet<>();
if (Func.isNotEmpty(request.getRows())) {
for (Map<String, Object> 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<String> mergeFeeItemNames(List<String> preferred, List<Map<String, Object>> rows) {
LinkedHashSet<String> 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<String, Object> row : rows) {
collectFeeItemNamesFromRow(row, names);
}
}
return new ArrayList<>(names);
}
private void collectFeeItemNamesFromRow(Map<String, Object> row, Set<String> 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<Map<String, Object>> buildFeeRows(List<Map<String, Object>> rows, List<Waybill> waybills) {
List<Map<String, Object>> primaryRows = primaryRows(rows);
List<Map<String, Object>> 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&lt;Map&gt; 解析,字符串数组会失败导致草稿再打开丢费用列。
*/
private List<String> parseFeeItemNamesJson(String feeItemNamesJson) {
if (Func.isEmpty(feeItemNamesJson)) {
return List.of();
}
try {
List<String> names = JsonUtil.parse(feeItemNamesJson, new TypeReference<List<String>>() {
});
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<String> loadEnabledFeeItemNames() {
R<List<FeeItem>> 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<Map<String, Object>> buildRowsFromWaybills(List<Waybill> waybills) {
if (Func.isEmpty(waybills)) {
@@ -601,6 +601,13 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
waybill.setContractId(plan.getContractId());
waybill.setContractName(plan.getContractName());
waybill.setCustomerName(plan.getCustomerName());
// 计划单若来自总单,调度运单须继承总单号与段号,供总单进度汇总
if (Func.isEmpty(waybill.getMasterNo()) && Func.isNotEmpty(plan.getMasterNo())) {
waybill.setMasterNo(plan.getMasterNo());
}
if (Func.isEmpty(waybill.getRelationNo()) && Func.isNotEmpty(plan.getRelationNo())) {
waybill.setRelationNo(plan.getRelationNo());
}
if (Func.isEmpty(waybill.getDepartureAddress())) {
waybill.setDepartureAddressId(plan.getDepartureAddressId());
waybill.setDepartureName(plan.getDepartureName());
@@ -205,6 +205,8 @@ public class TransportReconciliationServiceImpl
}
List<TransportReconciliationInternal> internals = internalRows(id);
List<TransportReconciliationExternal> 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<TransportReconciliationExternal> validExternals = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
VehicleReconciliationExcel row = rows.get(index);
normalizeImportVehicleDecimals(row);
List<String> 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<String, BigDecimal> feeItems = row.getFeeItems();
if (feeItems == null || feeItems.isEmpty()) {
feeItems = new HashMap<>();
@@ -427,6 +431,7 @@ public class TransportReconciliationServiceImpl
List<TransportReconciliationExternal> validExternals = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
CargoReconciliationExcel row = rows.get(index);
normalizeImportCargoDecimals(row);
List<String> 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<String, BigDecimal> 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.<Waybill>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<TransportReconciliationInternal> rows) { return rows.stream().map(TransportReconciliationInternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
private BigDecimal sumExternalQuantity(List<TransportReconciliationExternal> rows) { return rows.stream().map(TransportReconciliationExternal::getTransportQuantity).map(this::money).reduce(BigDecimal.ZERO, BigDecimal::add); }
@@ -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<VoucherManageMappe
private final LoadingManageMapper loadingManageMapper;
private final ApplicationEventPublisher eventPublisher;
private final ObjectProvider<MinioClient> 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<VoucherManageMappe
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
LoadingManageMapper loadingManageMapper,
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> 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<VoucherManageMappe
this.loadingManageMapper = loadingManageMapper;
this.eventPublisher = eventPublisher;
this.minioClientProvider = minioClientProvider;
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
this.requiresNewTransactionTemplate = transactionTemplate;
this.minioBucketName = minioBucketName;
this.minioRootDirectory = minioRootDirectory;
this.minioTransformEndpoint = minioTransformEndpoint;
@@ -1156,8 +1164,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
int imageCount = 0;
int matchedImageCount = 0;
Set<Long> 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<VoucherManageMappe
}
}
}
if (fileCount == 0) {
throw new ServiceException("压缩包无法解析或内没有找到有效文件,请检查后重新上传");
}
log.info("[凭证处理] 进度 90%:文件解压上传完成 voucherId={}, fileCount={}, imageCount={}, matchedImageCount={}",
voucherId, fileCount, imageCount, matchedImageCount);
VoucherManage update = new VoucherManage();
@@ -1256,12 +1269,53 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
deleteTempArchive(archivePath);
}
} catch (Exception exception) {
// 独立事务落「处理失败」,避免本方法事务回滚把失败状态冲掉后一直卡在「处理中」。
markProcessFailed(voucher.getId());
log.error("凭证压缩包处理失败 voucherId:{}", voucherId, exception);
// 不向上抛:解析失败属业务终态,MQ 消费确认后不再重试。
}
}
/**
* 在新事务中写入处理失败,确保即使当前解析事务回滚,列表也能看到失败态并允许重新上传。
*/
private void markProcessFailed(Long voucherId) {
if (voucherId == null) {
return;
}
requiresNewTransactionTemplate.executeWithoutResult(status -> {
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 压缩包,无法解析");
}
}
}
@@ -1059,8 +1059,9 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
waybill.setProcessJson(oldRecord.getProcessJson());
}
} else {
// 新建手工运单不应带配载号;总单/计划调度写入的 masterNo、relationNo 必须保留,
// 否则总单进度按 masterNo 汇总会一直显示「已调度 0」。
waybill.setLoadingNo(null);
waybill.setMasterNo(null);
waybill.setMileageRemark(null);
}
// 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致
@@ -29,7 +29,7 @@ import java.util.List;
import java.util.Locale;
/**
* 可配载准入判据:公路 + 待执行 + 未关联其它配载单。
* 可配载准入判据:公路 + 待执行/进行中 + 未关联其它配载单。
* <p>
* 接单状态不进判据:拒单票照样可配载。配载提交会把它的接单事实清成待接单,
* 否则新司机看不到这张票——司机端入口一律排除已拒绝的运单。
@@ -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<String> LOADABLE_BUSINESS_STATUSES = List.of(
WaybillProcessSupport.STATUS_PENDING,
WaybillProcessSupport.STATUS_RUNNING
);
/**
* 公路运输方式的同义写法,按「包含」比对,与前端运单管理页的识别口径一致。
@@ -56,7 +59,7 @@ public final class LoadableAdmissionSupport {
}
/**
* 不可配载的原因,按判定顺序排列:公路 → 待执行 → 未关联其它配载单。
* 不可配载的原因,按判定顺序排列:公路 → 待执行/进行中 → 未关联其它配载单。
* <p>
* 判据只产出原因,中文文案由调用方映射——面向用户的措辞不进这一层。
*/
@@ -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<Waybill> 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, ""));
}
@@ -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<LoadingManage> captor = ArgumentCaptor.forClass(LoadingManage.class);
verify(service, atLeastOnce()).saveOrUpdate(captor.capture());
return captor.getValue().getBusinessStatus();
}
private void stubWaybills(Waybill... waybills) {
when(waybillMapper.selectList(ArgumentMatchers.<Wrapper<Waybill>>any())).thenReturn(List.of(waybills));
}
@@ -68,7 +68,7 @@ class WaybillLoadableQueryTest {
}
@Test
@DisplayName("开关开启:公路词表、待执行、未配载一次全部进 wrapper,接单状态不参与")
@DisplayName("开关开启:公路词表、待执行/进行中、未配载一次全部进 wrapper,接单状态不参与")
void loadableSwitchAppliesWholePredicate() {
LambdaQueryWrapper<Waybill> 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"),
@@ -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"));
}
@@ -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 '创建时间',
@@ -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;
@@ -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`;