diff --git a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java index a024a6f8..48c5d9ea 100644 --- a/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java +++ b/blade-service-api/blade-system-api/src/main/java/org/springblade/system/pojo/entity/Dept.java @@ -139,9 +139,9 @@ public class Dept extends TenantEntity { private String deptCode; /** - * 金蝶部门代码 + * 金蝶公司代码(平台公司维护;字段名 kingdeeDeptCode 为历史遗留) */ - @Schema(description = "金蝶部门代码") + @Schema(description = "金蝶公司代码") private String kingdeeDeptCode; /** diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java index e35d1051..dab1be7e 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerArchive.java @@ -126,6 +126,10 @@ public class CustomerArchive extends TenantEntity { @TableField(updateStrategy = FieldStrategy.ALWAYS) private LocalDate businessEndDate; + @Schema(description = "下次评定日期") + @TableField(updateStrategy = FieldStrategy.ALWAYS) + private LocalDate nextEvaluateDate; + @Schema(description = "注册资金(万元)") @TableField(updateStrategy = FieldStrategy.ALWAYS) private BigDecimal registeredCapital; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScore.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScore.java index c7beeefd..d28c1f37 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScore.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerCreditScore.java @@ -56,9 +56,15 @@ public class CustomerCreditScore extends TenantEntity { @Schema(description = "评分量化表ID") private Long quantificationId; + @Schema(description = "评估方式:online线上评估,offline线下评估") + private String evaluateMode; + @Schema(description = "评分日期") private LocalDate scoreDate; + @Schema(description = "下次评定日期") + private LocalDate nextEvaluateDate; + @Schema(description = "自评得分") private BigDecimal selfScore; diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java index 807d5a72..71468fe8 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/CustomerArchiveVO.java @@ -106,6 +106,10 @@ public class CustomerArchiveVO extends CustomerArchive { @Schema(description = "已付金额合计(元)") private BigDecimal usedFundLimit; + @TableField(exist = false) + @Schema(description = "到期筛查:all全部/soon三十天内到期/expired已过期/license_soon执照即将到期/evaluate_soon评定即将到期") + private String expireStatus; + @TableField(exist = false) @Schema(description = "客商风险筛选值:blacklist黑名单/high高风险/middle中风险/low低风险") private String customerRisk; diff --git a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/BusinessDeptConfigServiceImpl.java b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/BusinessDeptConfigServiceImpl.java index 733a45f5..38f91fe6 100644 --- a/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/BusinessDeptConfigServiceImpl.java +++ b/blade-service/blade-system/src/main/java/org/springblade/system/service/impl/BusinessDeptConfigServiceImpl.java @@ -112,9 +112,15 @@ public class BusinessDeptConfigServiceImpl extends BaseServiceImpl PREFIX_MAX_LENGTH) { throw new ServiceException("合同编码前缀不能超过64字"); } - if (Func.isEmpty(businessDeptConfig.getKingdeeCompanyCode())) { - throw new ServiceException("请输入金蝶公司代码"); - } if (Func.isNotEmpty(businessDeptConfig.getPlatformCompanyName()) && businessDeptConfig.getPlatformCompanyName().length() > NAME_MAX_LENGTH) { throw new ServiceException("平台公司名称不能超过100字"); @@ -144,7 +147,8 @@ public class BusinessDeptConfigServiceImpl extends BaseServiceImpl NAME_MAX_LENGTH) { throw new ServiceException("业务部门名称不能超过100字"); } - if (businessDeptConfig.getKingdeeCompanyCode().length() > CODE_MAX_LENGTH) { + if (Func.isNotEmpty(businessDeptConfig.getKingdeeCompanyCode()) + && businessDeptConfig.getKingdeeCompanyCode().length() > CODE_MAX_LENGTH) { throw new ServiceException("金蝶公司代码不能超过64字"); } if (Func.isNotEmpty(businessDeptConfig.getKingdeeDeptCode()) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java index c2c0b7ab..55a71a1a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/CustomerArchiveController.java @@ -32,6 +32,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.http.HttpServletResponse; import lombok.AllArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.springblade.common.excel.ImportFailureExcelUtil; +import org.springblade.common.excel.ImportFailureException; import org.springblade.core.boot.ctrl.BladeController; import org.springblade.core.excel.util.ExcelUtil; import org.springblade.core.mp.support.Condition; @@ -44,6 +46,7 @@ import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.transport.excel.CustomerArchiveExcel; +import org.springblade.transport.excel.CustomerCreditScoreExcel; import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.vo.CustomerArchiveVO; import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; @@ -55,6 +58,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.multipart.MultipartFile; import java.util.List; @@ -239,6 +243,52 @@ public class CustomerArchiveController extends BladeController { return R.data(customerArchiveService.buildScoreTemplate(quantificationId)); } + /** + * 线下评分 Excel 模板下载 + */ + @GetMapping("/score-excel-template") + @ApiOperationSupport(order = 10) + @Operation(summary = "线下评分Excel模板", description = "按评分量化表ID导出可填写模板") + public void scoreExcelTemplate(@RequestParam Long quantificationId, HttpServletResponse response) { + List list = customerArchiveService.buildScoreExcelTemplate(quantificationId); + ExcelUtil.export(response, "客商评分明细模板" + DateUtil.time(), "评分明细", list, CustomerCreditScoreExcel.class); + } + + /** + * 线下评分 Excel 导入(解析回填评分明细,失败导出失败明细) + */ + @PostMapping("/score-import") + @ApiOperationSupport(order = 10) + @Operation(summary = "线下评分Excel导入", description = "解析回填评分明细;失败时导出导入失败明细") + public R scoreImport(@RequestParam Long quantificationId, + MultipartFile file, + HttpServletResponse response) { + if (file == null || file.isEmpty()) { + return R.fail("上传文件不能为空"); + } + String fileName = Func.toStrWithEmpty(file.getOriginalFilename(), "").toLowerCase(); + if (!fileName.endsWith(".xls") && !fileName.endsWith(".xlsx")) { + return R.fail("请上传 .xls,.xlsx 标准格式文件"); + } + try { + CustomerCreditScoreVO score = customerArchiveService.importScoreExcel( + quantificationId, ExcelUtil.read(file, CustomerCreditScoreExcel.class)); + return R.data(score); + } catch (ImportFailureException exception) { + ImportFailureExcelUtil.export(response, + "客商评分导入失败明细" + DateUtil.time(), "导入失败明细", + exception.getFailureList(), CustomerCreditScoreExcel.class); + return null; + } catch (Exception readException) { + if (ImportFailureExcelUtil.exportReadFailure( + response, "客商评分导入失败明细" + DateUtil.time(), "导入失败明细", + file, CustomerCreditScoreExcel.class, readException)) { + return null; + } + throw readException; + } + } + /** * 导出客商档案 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerCreditScoreExcel.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerCreditScoreExcel.java new file mode 100644 index 00000000..69aeab80 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/excel/CustomerCreditScoreExcel.java @@ -0,0 +1,70 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

+ * Use of this software is governed by the Commercial License Agreement + * obtained after purchasing a license from BladeX. + *

+ * 1. This software is for development use only under a valid license + * from BladeX. + *

+ * 2. Redistribution of this software's source code to any third party + * without a commercial license is strictly prohibited. + *

+ * 3. Licensees may copyright their own code but cannot use segments + * from this software for such purposes. Copyright of this software + * remains with BladeX. + *

+ * Using this software signifies agreement to this License, and the software + * must not be used for illegal purposes. + *

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.excel; + +import cn.idev.excel.annotation.ExcelIgnore; +import cn.idev.excel.annotation.ExcelProperty; +import cn.idev.excel.annotation.write.style.ColumnWidth; +import cn.idev.excel.annotation.write.style.ContentRowHeight; +import cn.idev.excel.annotation.write.style.HeadRowHeight; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 客商线下评分 Excel(模板下载与导入共用) + * + * @author Chill + */ +@Data +@ColumnWidth(24) +@HeadRowHeight(20) +@ContentRowHeight(18) +public class CustomerCreditScoreExcel implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @ExcelProperty("分类") + private String categoryName; + + @ExcelProperty("*评分项目") + private String itemName; + + @ExcelProperty("选项类型") + private String optionTypeName; + + @ExcelProperty("可选选项说明") + private String optionHint; + + @ExcelProperty("*填写值") + private String fillValue; + + @ExcelProperty("自评得分") + private String selfScore; + + @ExcelIgnore + private String errorMessage; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml index 077b5a75..d02e9049 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/CustomerArchiveMapper.xml @@ -35,6 +35,7 @@ + @@ -95,6 +96,7 @@ business_term_type, business_start_date, business_end_date, + next_evaluate_date, registered_capital, paid_in_capital, principal, @@ -198,6 +200,38 @@ + + + + + AND IFNULL(business_term_type, '') <> '长期' + AND business_end_date IS NOT NULL + AND business_end_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) + + + AND next_evaluate_date IS NOT NULL + AND next_evaluate_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY) + + + AND ( + (IFNULL(business_term_type, '') <> '长期' + AND business_end_date IS NOT NULL + AND business_end_date < CURDATE()) + OR (next_evaluate_date IS NOT NULL AND next_evaluate_date < CURDATE()) + ) + + + + AND ( + (IFNULL(business_term_type, '') <> '长期' + AND business_end_date IS NOT NULL + AND business_end_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)) + OR (next_evaluate_date IS NOT NULL + AND next_evaluate_date <= DATE_ADD(CURDATE(), INTERVAL 30 DAY)) + ) + + + ORDER BY create_time DESC diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java index 89317ca9..be114d15 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/KingdeeReceiptFlowMapper.java @@ -41,11 +41,11 @@ import java.util.List; public interface KingdeeReceiptFlowMapper extends BaseMapper { /** - * 业务部门配置中已维护的金蝶公司代码列表(去重) + * 组织管理(blade_dept)中的平台公司金蝶公司代码列表(是否平台公司=是 且 已维护金蝶编码) * - * @return 金蝶公司代码 + * @return 平台公司金蝶公司代码 */ - @Select("SELECT DISTINCT kingdee_company_code FROM blade_business_dept_config " - + "WHERE kingdee_company_code IS NOT NULL AND kingdee_company_code != '' AND is_deleted = 0") + @Select("SELECT kingdee_dept_code FROM blade_dept WHERE is_platform_company = 1 " + + "AND kingdee_dept_code IS NOT NULL AND kingdee_dept_code != '' AND is_deleted = 0") List selectPlatformOrgNumbers(); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java index c608c664..eda5109f 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/ICustomerArchiveService.java @@ -252,6 +252,24 @@ public interface ICustomerArchiveService extends BaseService { */ CustomerCreditScoreVO buildScoreTemplate(Long quantificationId); + /** + * 线下评分 Excel 模板(按量化表项目生成) + * + * @param quantificationId 评分量化表ID + * @return 模板行 + */ + List buildScoreExcelTemplate(Long quantificationId); + + /** + * 线下评分 Excel 导入:解析并回填评分明细,失败抛出 ImportFailureException + * + * @param quantificationId 评分量化表ID + * @param rows Excel 行 + * @return 已回填的评分记录(含明细) + */ + CustomerCreditScoreVO importScoreExcel(Long quantificationId, + List rows); + /** * 导出客商档案 * diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index 7a58c17f..2930cca1 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -906,10 +906,11 @@ public class ContractManageServiceImpl extends BaseServiceImpl buildScoreExcelTemplate(Long quantificationId) { + List details = loadScoreTemplateDetails(quantificationId); + List rows = new ArrayList<>(); + for (CustomerCreditScoreDetailVO detail : details) { + CustomerCreditScoreExcel excel = new CustomerCreditScoreExcel(); + excel.setCategoryName(detail.getCategoryName()); + excel.setItemName(detail.getItemName()); + boolean scoreType = "score".equalsIgnoreCase(detail.getOptionType()); + excel.setOptionTypeName(scoreType ? "分值" : "选项"); + excel.setOptionHint(scoreType + ? Func.toStrWithEmpty(detail.getOptionDescription(), "请按基准数值填写实际数值") + : buildOptionHint(detail.getOptionsJson())); + excel.setFillValue(""); + excel.setSelfScore(""); + rows.add(excel); + } + return rows; + } + + @Override + public CustomerCreditScoreVO importScoreExcel(Long quantificationId, List rows) { + if (Func.isEmpty(quantificationId)) { + throw new ServiceException("请先选择评分量化表"); + } + if (Func.isEmpty(rows)) { + throw new ServiceException("导入数据不能为空"); + } + CustomerCreditScoreVO score = buildScoreTemplate(quantificationId); + score.setEvaluateMode("offline"); + Map detailByName = score.getDetails().stream() + .filter(detail -> Func.isNotEmpty(detail.getItemName())) + .collect(Collectors.toMap(detail -> detail.getItemName().trim(), detail -> detail, (left, right) -> left, LinkedHashMap::new)); + boolean hasError = false; + Set filledItems = new HashSet<>(); + for (int index = 0; index < rows.size(); index++) { + CustomerCreditScoreExcel row = rows.get(index); + List errors = new ArrayList<>(); + String itemName = Func.toStrWithEmpty(row.getItemName(), "").trim(); + String fillValue = Func.toStrWithEmpty(row.getFillValue(), "").trim(); + if (Func.isEmpty(itemName)) { + ImportFailureExcelUtil.addValidationError(errors, true, "评分项目不能为空"); + } + CustomerCreditScoreDetailVO detail = Func.isEmpty(itemName) ? null : detailByName.get(itemName); + if (Func.isNotEmpty(itemName) && detail == null) { + ImportFailureExcelUtil.addValidationError(errors, true, "评分项目[" + itemName + "]不在当前评分量化表中"); + } + if (detail != null && filledItems.contains(itemName)) { + ImportFailureExcelUtil.addValidationError(errors, true, "评分项目[" + itemName + "]重复"); + } + if (detail != null && Func.isEmpty(fillValue)) { + ImportFailureExcelUtil.addValidationError(errors, true, "填写值不能为空"); + } + if (detail != null && Func.isNotEmpty(fillValue) && errors.isEmpty()) { + try { + applyImportedScoreDetail(detail, fillValue, Func.toStrWithEmpty(row.getSelfScore(), "").trim()); + filledItems.add(itemName); + } catch (ServiceException exception) { + ImportFailureExcelUtil.addValidationError(errors, true, exception.getMessage()); + } + } + if (!errors.isEmpty()) { + hasError = true; + row.setErrorMessage(ImportFailureExcelUtil.formatErrorMessage(errors)); + } + } + if (hasError) { + throw new ImportFailureException(rows); + } + List missing = detailByName.keySet().stream().filter(name -> !filledItems.contains(name)).toList(); + if (!missing.isEmpty()) { + throw new ServiceException("评分项目未全部填写:" + String.join("、", missing)); + } + return score; + } + + /** 将 Excel 填写值回填到评分明细:选项型匹配选项,分值型写入 scoreInput/selfScore */ + private void applyImportedScoreDetail(CustomerCreditScoreDetailVO detail, String fillValue, String selfScoreText) { + if ("score".equalsIgnoreCase(detail.getOptionType())) { + BigDecimal scoreInput; + try { + scoreInput = new BigDecimal(fillValue); + } catch (NumberFormatException exception) { + throw new ServiceException("填写值必须为数字"); + } + detail.setScoreInput(scoreInput); + if (Func.isNotEmpty(selfScoreText)) { + try { + detail.setSelfScore(new BigDecimal(selfScoreText)); + } catch (NumberFormatException exception) { + throw new ServiceException("自评得分必须为数字"); + } + } + return; + } + Map matched = matchScoreOption(detail.getOptionsJson(), fillValue); + if (matched == null) { + throw new ServiceException("填写值[" + fillValue + "]不在可选选项中"); + } + Object optionValue = matched.get("value"); + if (optionValue == null) { + optionValue = matched.get("label"); + } + if (optionValue == null) { + optionValue = matched.get("optionName"); + } + detail.setSelectedOption(optionValue == null ? fillValue : String.valueOf(optionValue)); + BigDecimal optionScore = resolveOptionScore(matched); + detail.setSelfScore(optionScore == null ? BigDecimal.ZERO : optionScore); + if (Func.isNotEmpty(selfScoreText)) { + try { + detail.setSelfScore(new BigDecimal(selfScoreText)); + } catch (NumberFormatException exception) { + throw new ServiceException("自评得分必须为数字"); + } + } + } + + private Map matchScoreOption(String optionsJson, String fillValue) { + if (Func.isEmpty(optionsJson) || Func.isEmpty(fillValue)) { + return null; + } + try { + Object parsed = JsonUtil.parse(optionsJson, List.class); + if (!(parsed instanceof List options)) { + return null; + } + for (Object option : options) { + if (!(option instanceof Map optionMap)) { + continue; + } + Map normalized = new LinkedHashMap<>(); + optionMap.forEach((key, value) -> normalized.put(String.valueOf(key), value)); + List candidates = Stream.of( + normalized.get("label"), + normalized.get("value"), + normalized.get("optionName")) + .filter(Objects::nonNull) + .map(value -> String.valueOf(value).trim()) + .filter(Func::isNotEmpty) + .toList(); + if (candidates.stream().anyMatch(fillValue::equals)) { + return normalized; + } + } + } catch (Exception ignored) { + return null; + } + return null; + } + + private String buildOptionHint(String optionsJson) { + if (Func.isEmpty(optionsJson)) { + return ""; + } + try { + Object parsed = JsonUtil.parse(optionsJson, List.class); + if (!(parsed instanceof List options)) { + return ""; + } + List labels = new ArrayList<>(); + for (Object option : options) { + if (!(option instanceof Map optionMap)) { + continue; + } + Object label = optionMap.get("label"); + if (label == null) { + label = optionMap.get("optionName"); + } + if (label == null) { + label = optionMap.get("value"); + } + if (label != null && Func.isNotEmpty(String.valueOf(label).trim())) { + labels.add(String.valueOf(label).trim()); + } + } + return String.join(" / ", labels); + } catch (Exception exception) { + return ""; + } + } + @Override public List exportCustomerArchive(Wrapper queryWrapper) { return list(queryWrapper).stream().map(this::buildExcel).toList(); @@ -1250,10 +1434,32 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl 1) { throw new ServiceException("评分记录只能有一条"); } + // 仅客商分类含「客户」时强制评分;纯承运商不强制 + if (!requiresCustomerScore(customer.getCustomerType())) { + return; + } boolean selfCompleted = scores.size() == 1 && "已完成".equals(scores.get(0).getSelfStatus()); if (!selfCompleted) { - throw new ServiceException("提交审批前必须完成自评"); + throw new ServiceException("客户提交审批前必须完成评分自评"); } + CustomerCreditScoreVO score = scores.get(0); + if (Func.isEmpty(score.getNextEvaluateDate())) { + throw new ServiceException("请填写下次评定日期"); + } + if (Func.isEmpty(score.getQuantificationId())) { + throw new ServiceException("请选择评分量化表"); + } + } + + /** 客商分类是否包含客户(字典值「客户」或历史值 customer) */ + private boolean requiresCustomerScore(String customerType) { + if (Func.isEmpty(customerType)) { + return false; + } + return Func.toStrList(customerType.replace(",", ",")).stream() + .map(String::trim) + .filter(Func::isNotEmpty) + .anyMatch(item -> "客户".equals(item) || "customer".equalsIgnoreCase(item)); } private void validateApproval(CustomerArchiveVO customer, boolean temporaryToFormal) { @@ -1499,6 +1705,9 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl loadContacts(Long customerId) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java index 529b40e2..6a8450df 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ReceiptFlowServiceImpl.java @@ -463,10 +463,10 @@ public class ReceiptFlowServiceImpl extends BaseServiceImpl orgNumbers = baseMapper.selectPlatformOrgNumbers(); if (Func.isEmpty(orgNumbers)) { - throw new ServiceException("业务部门配置中未维护金蝶公司代码,无法同步金蝶收款流水,请先检查业务部门配置"); + throw new ServiceException("组织管理中未维护平台公司金蝶公司代码,无法同步金蝶收款流水,请先检查组织数据"); } log.info("收款流水同步按组织过滤,组织编码:{}", orgNumbers); RecBillSyncState state = new RecBillSyncState(); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java index 88cc24ac..97fb92d3 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/support/KingdeeApBillSyncSupport.java @@ -583,9 +583,9 @@ public class KingdeeApBillSyncSupport { String feeType = Func.toStr(summaryFee.getFeeType()).trim(); String feeItem = Func.toStr(summaryFee.getFeeItem()).trim(); String sourceDesc = "结算合计[" + feeType + "-" + feeItem + "]"; - // 物料:运费类按项目业务模式/运输方式映射,仓储大类(档案编码 A05)映射仓储物料,其余非运费类映射装卸物料 + // 物料:运费类按项目业务模式/运输方式映射,仓储大类(档案编码/费用类型 A05)映射仓储物料,其余非运费类映射装卸物料 FeeCategory feeCategory = isFreightCategory(feeType, feeItem, context.feeCategoryByName()) ? FeeCategory.FREIGHT - : isWarehouseCategory(feeItem, context.feeCategoryByName()) ? FeeCategory.WAREHOUSE : FeeCategory.LOADING; + : isWarehouseCategory(feeType, feeItem, context.feeCategoryByName()) ? FeeCategory.WAREHOUSE : FeeCategory.LOADING; String materialNumber = resolveMaterialNumber(feeCategory, context.project(), fallbackTransportType, sourceDesc); // 结算合计费用项行无运输量,数量默认 1 BigDecimal quantity = BigDecimal.ONE; @@ -773,12 +773,15 @@ public class KingdeeApBillSyncSupport { } /** - * 仓储大类判定(手动添加结算合计费用行物料映射用):费用项档案维护的费用大类编码为 A05 归仓储大类, - * 无名称关键字兜底,分类以档案维护为准 + * 仓储大类判定(手动添加结算合计费用行物料映射用):费用项档案维护的费用大类编码为 A05 归仓储大类; + * 档案未维护该费用项时回退行费用类型编码 A05;无名称关键字兜底,分类以档案/费用类型维护为准 */ - static boolean isWarehouseCategory(String feeItemName, Map feeCategoryByName) { + public static boolean isWarehouseCategory(String feeTypeCode, String feeItemName, Map feeCategoryByName) { String categoryCode = feeItemName == null || feeCategoryByName == null ? null : feeCategoryByName.get(feeItemName); - return FEE_CATEGORY_WAREHOUSE.equals(categoryCode); + if (Func.isNotBlank(categoryCode)) { + return FEE_CATEGORY_WAREHOUSE.equals(categoryCode); + } + return FEE_CATEGORY_WAREHOUSE.equals(feeTypeCode); } /** 装卸大类费用项判定(关键字兜底):费用项名称含“装卸” */ @@ -1030,7 +1033,7 @@ public class KingdeeApBillSyncSupport { } /** - * 合同金蝶组织编码:取业务部门配置中的金蝶公司代码(平台公司 + 业务部门) + * 合同金蝶组织编码:合同关联项目 → 项目业务部门沿部门树向上反查唯一平台公司 → 平台公司金蝶公司代码 * * @param contract 合同实体 * @param context 金蝶同步上下文(缓存载体) @@ -1040,14 +1043,28 @@ public class KingdeeApBillSyncSupport { if (contract == null) { throw new ServiceException("推送金蝶失败,合同不存在,无法确定金蝶组织编码"); } - return this.kingdeeCompanyNumber(contract, contract.getProjectId(), context); + // 优先取合同平台公司金蝶代码;未维护时回退项目业务部门上溯 + if (contract.getPlatformCompanyId() != null) { + Dept platformCompany = SysCache.getDept(contract.getPlatformCompanyId()); + if (platformCompany == null) { + throw new ServiceException("推送金蝶失败,合同平台公司不存在,无法确定金蝶组织编码"); + } + if (Func.isBlank(platformCompany.getKingdeeDeptCode())) { + throw new ServiceException("推送金蝶失败,平台公司[" + platformCompany.getDeptName() + "]未维护金蝶公司代码,请检查组织数据"); + } + return platformCompany.getKingdeeDeptCode(); + } + if (contract.getProjectId() == null) { + throw new ServiceException("推送金蝶失败,合同[" + Func.toStr(contract.getContractNo()) + "]未关联项目,无法确定金蝶组织编码"); + } + return this.kingdeeOrgNumber(contract.getProjectId(), context); } /** - * 金蝶组织编码:指定部门沿部门树向上反查平台公司,再按「平台公司 + 业务部门」取业务部门配置中的金蝶公司代码。 + * 金蝶组织编码:指定部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶公司代码。 * 先票后款链路收票登记不挂合同/项目,结算组织由页面手选部门派生(A303-2 org_number) * - * @param deptId 手选部门id(收票登记 deptId,须为业务部门) + * @param deptId 手选部门id(收票登记 deptId) * @param context 金蝶同步上下文(缓存载体) * @return 组织编码 */ @@ -1059,78 +1076,38 @@ public class KingdeeApBillSyncSupport { if (dept == null) { throw new ServiceException("推送金蝶失败,所选结算组织[" + deptId + "]不存在,无法确定金蝶组织编码"); } - if (!Objects.equals(dept.getIsBusinessDept(), 1)) { - throw new ServiceException("推送金蝶失败,所选结算组织[" + dept.getDeptName() + "]不是业务部门,无法确定金蝶组织编码"); - } - Long platformCompanyId = this.resolvePlatformCompanyId(dept); - if (platformCompanyId == null) { - throw new ServiceException("推送金蝶失败,部门[" + Func.toStr(dept.getDeptName()) + "]未归属平台公司,无法确定金蝶组织编码"); - } - return this.requireKingdeeCompanyCode(platformCompanyId, deptId); + return this.platformCompanyOrgNumber(dept, dept.getDeptName()); } /** - * 金蝶组织编码:按项目业务部门配置中的金蝶公司代码推送 + * 金蝶组织编码:项目业务部门沿部门树向上反查唯一平台公司(是否平台公司=是),取该平台公司的金蝶公司代码; + * 项目未关联/未维护业务部门/业务部门未归属平台公司/平台公司未维护金蝶公司代码时拦截 * * @param projectId 项目id * @param context 金蝶同步上下文(缓存载体) * @return 组织编码 */ public String kingdeeOrgNumber(Long projectId, KingdeeSyncContext context) { - return this.kingdeeCompanyNumber(null, projectId, context); + ProjectApply project = this.requireOrgProject(projectId, context); + Dept dept = SysCache.getDept(project.getBusinessDeptId()); + if (dept == null) { + throw new ServiceException("推送金蝶失败,项目[" + Func.toStr(project.getProjectName()) + "]业务部门不存在,无法确定金蝶组织编码"); + } + return this.platformCompanyOrgNumber(dept, project.getBusinessDeptName()); } - /** - * 金蝶公司代码:按合同平台公司下业务部门配置推送 - * - * @param contract 合同(优先取其平台公司、合同负责部门) - * @param projectId 项目id(合同未带业务部门时回退项目业务部门) - * @param context 金蝶同步上下文(缓存载体) - * @return 公司编码 - */ - private String kingdeeCompanyNumber(ContractManage contract, Long projectId, KingdeeSyncContext context) { - Long platformCompanyId = contract == null ? null : contract.getPlatformCompanyId(); - Long businessDeptId = contract == null ? null : contract.getOrganizationId(); - if (businessDeptId == null && projectId != null) { - ProjectApply project = this.requireOrgProject(projectId, context); - businessDeptId = project.getBusinessDeptId(); - } - if (platformCompanyId == null && businessDeptId != null) { - Dept businessDept = SysCache.getDept(businessDeptId); - if (businessDept == null) { - throw new ServiceException("推送金蝶失败,业务部门不存在,无法确定金蝶组织编码"); - } - platformCompanyId = this.resolvePlatformCompanyId(businessDept); - if (platformCompanyId == null) { - throw new ServiceException("推送金蝶失败,部门[" + Func.toStr(businessDept.getDeptName()) - + "]未归属平台公司,无法确定金蝶组织编码"); - } - } - if (platformCompanyId == null || businessDeptId == null) { - throw new ServiceException("业务部门对应的金蝶公司代码未配置,单据无法推送,请联系管理员处理。"); - } - return this.requireKingdeeCompanyCode(platformCompanyId, businessDeptId); - } - - /** 读取业务部门配置中的金蝶公司代码,未配置时拦截 */ - private String requireKingdeeCompanyCode(Long platformCompanyId, Long businessDeptId) { - org.springblade.system.pojo.entity.BusinessDeptConfig config = - SysCache.getBusinessDeptConfig(platformCompanyId, businessDeptId); - if (config == null || Func.isBlank(config.getKingdeeCompanyCode())) { - throw new ServiceException("业务部门对应的金蝶公司代码未配置,单据无法推送,请联系管理员处理。"); - } - return config.getKingdeeCompanyCode().trim(); - } - - /** 部门树向上反查平台公司ID(上限 20 级防脏数据成环死循环) */ - private Long resolvePlatformCompanyId(Dept dept) { + /** 部门树向上反查平台公司金蝶公司代码(上限 20 级防脏数据成环死循环),未归属平台公司时拦截 */ + private String platformCompanyOrgNumber(Dept dept, String startDeptName) { for (int depth = 0; dept != null && depth < 20; depth++) { if (Objects.equals(dept.getIsPlatformCompany(), 1)) { - return dept.getId(); + if (Func.isBlank(dept.getKingdeeDeptCode())) { + throw new ServiceException("推送金蝶失败,平台公司[" + dept.getDeptName() + "]未维护金蝶公司代码,请检查组织数据"); + } + return dept.getKingdeeDeptCode(); } dept = dept.getParentId() == null ? null : SysCache.getDept(dept.getParentId()); } - return null; + throw new ServiceException("推送金蝶失败,部门[" + Func.toStr(startDeptName) + "]未归属平台公司,无法确定金蝶组织编码"); } /** diff --git a/doc/sql/bladex/bladex.mysql.all.create.sql b/doc/sql/bladex/bladex.mysql.all.create.sql index 0d8125ed..99909415 100644 --- a/doc/sql/bladex/bladex.mysql.all.create.sql +++ b/doc/sql/bladex/bladex.mysql.all.create.sql @@ -303,7 +303,7 @@ CREATE TABLE `blade_dept` ( `dept_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门名', `full_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '部门全称', `dept_code` varchar(30) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织编码', - `kingdee_dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '金蝶部门代码', + `kingdee_dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '金蝶公司代码', `contract_code_prefix` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '合同编码前缀', `contract_initial_serial` int NULL DEFAULT NULL COMMENT '合同初始流水号', `short_name` varchar(45) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '组织简称', diff --git a/doc/sql/transport/blade_business_dept_config_kingdee_company_code_revert_20261104.sql b/doc/sql/transport/blade_business_dept_config_kingdee_company_code_revert_20261104.sql new file mode 100644 index 00000000..4b74e2c1 --- /dev/null +++ b/doc/sql/transport/blade_business_dept_config_kingdee_company_code_revert_20261104.sql @@ -0,0 +1,11 @@ +-- 金蝶公司代码改回组织(平台公司)维护:业务部门配置仅作展示镜像 +-- 1)用平台公司组织上的金蝶编码回填业务部门配置展示字段 +UPDATE `blade_business_dept_config` c +INNER JOIN `blade_dept` d ON d.id = c.platform_company_id AND d.is_deleted = 0 +SET c.kingdee_company_code = d.kingdee_dept_code +WHERE d.kingdee_dept_code IS NOT NULL + AND d.kingdee_dept_code != ''; + +-- 2)字段注释改为组织回填展示口径 +ALTER TABLE `blade_business_dept_config` + MODIFY COLUMN `kingdee_company_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '金蝶公司代码(来自平台公司组织,仅展示)'; diff --git a/doc/sql/transport/blade_customer_score_evaluate_mode_20261104.sql b/doc/sql/transport/blade_customer_score_evaluate_mode_20261104.sql new file mode 100644 index 00000000..27460018 --- /dev/null +++ b/doc/sql/transport/blade_customer_score_evaluate_mode_20261104.sql @@ -0,0 +1,44 @@ +-- 客商评分双模式与下次评定日期:评分记录增加评估方式/下次评定日期,档案主表增加下次评定日期供列表展示 +-- 幂等可重复执行;排序规则 utf8mb4_general_ci + +-- ① 评分记录:评估方式 online/offline +SET @score_mode_col := ( + SELECT COUNT(1) FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'blade_customer_credit_score' + AND column_name = 'evaluate_mode' +); +SET @score_mode_sql := IF(@score_mode_col = 0, + 'ALTER TABLE `blade_customer_credit_score` ADD COLUMN `evaluate_mode` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ''online'' COMMENT ''评估方式:online线上评估,offline线下评估'' AFTER `quantification_id`', + 'SELECT 1'); +PREPARE score_mode_stmt FROM @score_mode_sql; +EXECUTE score_mode_stmt; +DEALLOCATE PREPARE score_mode_stmt; + +-- ② 评分记录:下次评定日期 +SET @score_next_col := ( + SELECT COUNT(1) FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'blade_customer_credit_score' + AND column_name = 'next_evaluate_date' +); +SET @score_next_sql := IF(@score_next_col = 0, + 'ALTER TABLE `blade_customer_credit_score` ADD COLUMN `next_evaluate_date` date NULL DEFAULT NULL COMMENT ''下次评定日期'' AFTER `score_date`', + 'SELECT 1'); +PREPARE score_next_stmt FROM @score_next_sql; +EXECUTE score_next_stmt; +DEALLOCATE PREPARE score_next_stmt; + +-- ③ 客商档案:下次评定日期(审批通过后由评分写回) +SET @archive_next_col := ( + SELECT COUNT(1) FROM information_schema.columns + WHERE table_schema = DATABASE() + AND table_name = 'blade_customer_archive' + AND column_name = 'next_evaluate_date' +); +SET @archive_next_sql := IF(@archive_next_col = 0, + 'ALTER TABLE `blade_customer_archive` ADD COLUMN `next_evaluate_date` date NULL DEFAULT NULL COMMENT ''下次评定日期'' AFTER `business_end_date`', + 'SELECT 1'); +PREPARE archive_next_stmt FROM @archive_next_sql; +EXECUTE archive_next_stmt; +DEALLOCATE PREPARE archive_next_stmt; diff --git a/doc/sql/transport/blade_dept_kingdee_company_code_comment_20261104.sql b/doc/sql/transport/blade_dept_kingdee_company_code_comment_20261104.sql new file mode 100644 index 00000000..4f005a05 --- /dev/null +++ b/doc/sql/transport/blade_dept_kingdee_company_code_comment_20261104.sql @@ -0,0 +1,3 @@ +-- 组织表:平台公司金蝶编码字段注释由「金蝶部门代码」改为「金蝶公司代码」(字段名不变) +ALTER TABLE `blade_dept` + MODIFY COLUMN `kingdee_dept_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '金蝶公司代码';