✨ 评分量化减分累计与最高扣分限制,并修复运输对账与结算调整单等问题
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+6
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
+8
@@ -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;
|
||||
|
||||
}
|
||||
|
||||
+4
-19
@@ -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);
|
||||
|
||||
+8
@@ -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);
|
||||
|
||||
+44
@@ -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)
|
||||
|
||||
+25
-4
@@ -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>
|
||||
|
||||
+6
-13
@@ -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) {
|
||||
|
||||
+2
-7
@@ -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);
|
||||
|
||||
+71
-24
@@ -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<Map> 解析,字符串数组会失败导致草稿再打开丢费用列。
|
||||
*/
|
||||
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)) {
|
||||
|
||||
+7
@@ -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());
|
||||
|
||||
+60
-43
@@ -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); }
|
||||
|
||||
+57
-3
@@ -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 压缩包,无法解析");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -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);
|
||||
}
|
||||
// 新建若已带业务状态(如复制提交已完成)则在过程校正后回写,保证与原运单一致
|
||||
|
||||
+13
-10
@@ -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, ""));
|
||||
}
|
||||
|
||||
|
||||
+11
-3
@@ -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));
|
||||
}
|
||||
|
||||
+3
-5
@@ -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"),
|
||||
|
||||
+9
-8
@@ -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`;
|
||||
Reference in New Issue
Block a user