diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerChangeRecord.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerChangeRecord.java index dbda316..ee1ab86 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerChangeRecord.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/CustomerChangeRecord.java @@ -57,6 +57,15 @@ public class CustomerChangeRecord extends TenantEntity { @Schema(description = "变更内容") private String changeContent; + @Schema(description = "变更字段,多个字段以逗号分隔") + private String changedFields; + + @Schema(description = "变更前数据JSON") + private String beforeData; + + @Schema(description = "变更后数据JSON") + private String afterData; + @Schema(description = "变更账号") private String changeUserName; 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 93046d6..6b1a126 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 @@ -43,6 +43,7 @@ import org.springblade.core.tool.utils.Func; import org.springblade.transport.excel.CustomerArchiveExcel; import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.vo.CustomerArchiveVO; +import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; import org.springblade.transport.pojo.vo.CustomerCreditScoreVO; import org.springblade.transport.service.ICustomerArchiveService; import org.springframework.web.bind.annotation.GetMapping; @@ -89,6 +90,17 @@ public class CustomerArchiveController extends BladeController { return R.data(pages); } + /** + * 变更记录分页 + */ + @GetMapping("/change-record/list") + @ApiOperationSupport(order = 3) + @Operation(summary = "客商变更记录分页", description = "传入客商ID") + public R> changeRecordList( + @Parameter(description = "客商ID", required = true) @RequestParam Long customerId, Query query) { + return R.data(customerArchiveService.selectChangeRecordPage(Condition.getPage(query), customerId)); + } + /** * 新增或修改 */ diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java index 37c633f..41e399d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectApplyController.java @@ -144,8 +144,8 @@ public class ProjectApplyController extends BladeController { @ApiOperationSupport(order = 12) @Operation(summary = "发起变更", description = "传入id、changeContent和changeReason") public R startChange(@Parameter(description = "主键", required = true) @RequestParam Long id, - @RequestParam String changeContent, - @RequestParam String changeReason) { + @RequestParam(required = false) String changeContent, + @RequestParam(required = false) String changeReason) { return R.status(projectApplyService.startChange(id, changeContent, changeReason)); } 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 ffabc83..64b88d0 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 @@ -28,6 +28,7 @@ import org.springblade.core.mp.base.BaseService; import org.springblade.transport.excel.CustomerArchiveExcel; import org.springblade.transport.pojo.entity.CustomerArchive; import org.springblade.transport.pojo.vo.CustomerArchiveVO; +import org.springblade.transport.pojo.vo.CustomerChangeRecordVO; import org.springblade.transport.pojo.vo.CustomerCreditScoreVO; import java.util.List; @@ -48,6 +49,15 @@ public interface ICustomerArchiveService extends BaseService { */ IPage selectCustomerArchivePage(IPage page, CustomerArchiveVO customer); + /** + * 客商变更记录分页 + * + * @param page 分页参数 + * @param customerId 客商ID + * @return 变更记录分页 + */ + IPage selectChangeRecordPage(IPage page, Long customerId); + /** * 聚合详情 * diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java index b3c9818..975cc7d 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/CustomerArchiveServiceImpl.java @@ -27,6 +27,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.IdWorker; import com.baomidou.mybatisplus.core.toolkit.Wrappers; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; import org.springblade.core.log.exception.ServiceException; import org.springblade.core.mp.base.BaseServiceImpl; @@ -122,33 +123,46 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl selectChangeRecordPage(IPage page, Long customerId) { + if (Func.isEmpty(customerId)) { + throw new ServiceException("客商ID不能为空"); + } + ensureCustomerAccessible(customerId); + IPage recordPage = changeRecordMapper.selectPage( + new Page<>(page.getCurrent(), page.getSize()), + Wrappers.lambdaQuery() + .eq(CustomerChangeRecord::getCustomerId, customerId) + .eq(CustomerChangeRecord::getIsDeleted, 0) + .orderByDesc(CustomerChangeRecord::getChangeTime)); + page.setTotal(recordPage.getTotal()); + return page.setRecords(recordPage.getRecords().stream() + .map(record -> Objects.requireNonNull(BeanUtil.copyProperties(record, CustomerChangeRecordVO.class))) + .toList()); + } + @Override public CustomerArchiveVO detail(Long id) { if (Func.isEmpty(id)) { throw new ServiceException("主键不能为空"); } - CustomerArchive customer = getById(id); - if (Func.isEmpty(customer) || Objects.equals(customer.getIsDeleted(), 1)) { - throw new ServiceException("客商档案不存在"); - } - if (baseMapper.selectCustomerPermissionCount(id, AuthUtil.getUserId()) == 0) { - throw new ServiceException("无权访问该客商档案"); - } + CustomerArchive customer = ensureCustomerAccessible(id); CustomerArchiveVO detail = Objects.requireNonNull(BeanUtil.copyProperties(customer, CustomerArchiveVO.class)); detail.setContacts(loadContacts(id)); detail.setReceiptAccounts(loadReceiptAccounts(id)); detail.setInvoices(loadInvoices(id)); detail.setScores(loadScores(id)); - detail.setChangeRecords(loadChangeRecords(id)); return detail; } @Override @Transactional(rollbackFor = Exception.class) public boolean submit(CustomerArchiveVO customer) { + CustomerArchiveVO beforeDetail = Func.isEmpty(customer.getId()) ? null : detail(customer.getId()); prepare(customer); validateBase(customer); boolean created = Func.isEmpty(customer.getId()); + CustomerArchive before = created ? null : ensureCustomerAccessible(customer.getId()); if (created) { customer.setCustomerCode(nextCustomerCode()); } @@ -159,7 +173,9 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl details = scoreVO.getDetails() == null ? new ArrayList<>() : scoreVO.getDetails(); + if (Func.isEmpty(details)) { + throw new ServiceException("评分明细不能为空,请重新读取评分量化表后再保存"); + } for (int detailIndex = 0; detailIndex < details.size(); detailIndex++) { CustomerCreditScoreDetailVO detailVO = details.get(detailIndex); if (Func.isEmpty(detailVO.getScore())) { @@ -484,6 +495,7 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl loadChangeRecords(Long customerId) { - return changeRecordMapper.selectList(Wrappers.lambdaQuery() - .eq(CustomerChangeRecord::getCustomerId, customerId) - .eq(CustomerChangeRecord::getIsDeleted, 0) - .orderByDesc(CustomerChangeRecord::getChangeTime)) - .stream().map(record -> Objects.requireNonNull(BeanUtil.copyProperties(record, CustomerChangeRecordVO.class))).toList(); - } - private CreditScoreQuantification resolveQuantification(Long quantificationId) { CreditScoreQuantification quantification; if (Func.isNotEmpty(quantificationId)) { @@ -763,17 +767,125 @@ public class CustomerArchiveServiceImpl extends BaseServiceImpl beforeSnapshot = customerSnapshot(before); + Map afterSnapshot = customerSnapshot(after); + addDetailSnapshot(beforeSnapshot, null); + addDetailSnapshot(afterSnapshot, null); + writeChangeRecord(customerId, content, beforeSnapshot, afterSnapshot); + } + + private void addChangeRecord(Long customerId, String content, CustomerArchive before, CustomerArchive after, + CustomerArchiveVO beforeDetail, CustomerArchiveVO afterDetail) { + Map beforeSnapshot = customerSnapshot(before); + Map afterSnapshot = customerSnapshot(after); + addDetailSnapshot(beforeSnapshot, beforeDetail); + addDetailSnapshot(afterSnapshot, afterDetail); + writeChangeRecord(customerId, content, beforeSnapshot, afterSnapshot); + } + + private void writeChangeRecord(Long customerId, String content, Map beforeSnapshot, + Map afterSnapshot) { + for (String field : afterSnapshot.keySet()) { + beforeSnapshot.putIfAbsent(field, null); + } + Map beforeData = new LinkedHashMap<>(); + Map afterData = new LinkedHashMap<>(); + List changedFields = new ArrayList<>(); + for (String field : beforeSnapshot.keySet()) { + if (!afterSnapshot.containsKey(field)) { + afterSnapshot.put(field, null); + } + Object beforeValue = beforeSnapshot.get(field); + Object afterValue = afterSnapshot.get(field); + if (!Objects.equals(beforeValue, afterValue)) { + changedFields.add(field); + beforeData.put(field, beforeValue); + afterData.put(field, afterValue); + } + } + if (changedFields.isEmpty()) { + return; + } CustomerChangeRecord record = new CustomerChangeRecord(); record.setId(IdWorker.getId()); record.setCustomerId(customerId); record.setChangeTime(LocalDateTime.now()); record.setChangeContent(content); + record.setChangedFields(String.join("、", changedFields)); + record.setBeforeData(JsonUtil.toJson(beforeData)); + record.setAfterData(JsonUtil.toJson(afterData)); record.setChangeUserName(AuthUtil.getUserName()); record.setStatus(STATUS_ENABLED); changeRecordMapper.insert(record); } + private void addDetailSnapshot(Map snapshot, CustomerArchiveVO detail) { + if (detail == null) { + return; + } + snapshot.put("联系人信息", JsonUtil.toJson(detail.getContacts())); + snapshot.put("收款信息", JsonUtil.toJson(detail.getReceiptAccounts())); + snapshot.put("发票信息", JsonUtil.toJson(detail.getInvoices())); + snapshot.put("评分信息", JsonUtil.toJson(detail.getScores())); + } + + private Map customerSnapshot(CustomerArchive customer) { + Map snapshot = new LinkedHashMap<>(); + if (customer == null) { + return snapshot; + } + snapshot.put("客商编号", customer.getCustomerCode()); + snapshot.put("客商简称", customer.getShortName()); + snapshot.put("客商全称", customer.getFullName()); + snapshot.put("客商性质", customer.getCustomerNature()); + snapshot.put("统一社会信用代码", customer.getUnifiedCreditCode()); + snapshot.put("客商类型", customer.getCustomerType()); + snapshot.put("所属项目", customer.getProjectName()); + snapshot.put("注册/实际经营地址", customer.getRegisteredAddress()); + snapshot.put("注册地址行政区划", customer.getRegisteredRegionName()); + snapshot.put("注册地址详细地址", customer.getRegisteredDetailAddress()); + snapshot.put("法人/负责人", customer.getLegalPerson()); + snapshot.put("联系电话", customer.getContactPhone()); + snapshot.put("所属组织ID", customer.getDeptId()); + snapshot.put("所属组织ID集合", customer.getDeptIds()); + snapshot.put("所属组织", customer.getDeptName()); + snapshot.put("开票税点", customer.getInvoiceTaxRate()); + snapshot.put("经营范围", customer.getBusinessScope()); + snapshot.put("营业期限类型", customer.getBusinessTermType()); + snapshot.put("营业期限截止日", customer.getBusinessEndDate()); + snapshot.put("注册资金(万元)", customer.getRegisteredCapital()); + snapshot.put("客商负责人", customer.getPrincipal()); + snapshot.put("助记码", customer.getMnemonicCode()); + snapshot.put("客户等级", customer.getCustomerLevel()); + snapshot.put("最大资金使用额度(万元)", customer.getMaxCreditLimit()); + snapshot.put("申请总资金使用额度(万元)", customer.getApplyCreditLimit()); + snapshot.put("备注", customer.getRemark()); + snapshot.put("客商材料", customer.getQualificationAttachments()); + snapshot.put("准入类型", customer.getAccessType()); + snapshot.put("审批状态", customer.getApprovalStatus()); + snapshot.put("当前节点", customer.getCurrentNode()); + snapshot.put("当前处理人", customer.getCurrentProcessor()); + snapshot.put("审核通过时间", customer.getApprovedTime()); + snapshot.put("状态", customer.getStatus()); + return snapshot; + } + private CustomerArchiveExcel buildExcel(CustomerArchive customer) { CustomerArchiveExcel excel = new CustomerArchiveExcel(); excel.setCustomerCode(customer.getCustomerCode()); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java index 68d00ed..95907d4 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectApplyServiceImpl.java @@ -214,8 +214,6 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl } else { TransportBusinessSupport.validateRequired(waybill.getDriverName(), "司机不能为空"); TransportBusinessSupport.validateRequired(waybill.getDriverPhone(), "司机手机号不能为空"); - TransportBusinessSupport.validateRequired(waybill.getTrailerVehicleNo(), "挂车车牌号不能为空"); - TransportBusinessSupport.validateRequired(waybill.getEscortName(), "押运人不能为空"); - TransportBusinessSupport.validateRequired(waybill.getEscortPhone(), "押运人手机号不能为空"); - if (Func.isEmpty(waybill.getMileage())) { - throw new ServiceException("里程不能为空"); + if (!isRoadTransport(waybill.getTransportType())) { + TransportBusinessSupport.validateRequired(waybill.getTrailerVehicleNo(), "挂车车牌号不能为空"); + TransportBusinessSupport.validateRequired(waybill.getEscortName(), "押运人不能为空"); + TransportBusinessSupport.validateRequired(waybill.getEscortPhone(), "押运人手机号不能为空"); + if (Func.isEmpty(waybill.getMileage())) { + throw new ServiceException("里程不能为空"); + } } } TransportBusinessSupport.validateRequired(waybill.getCarrierJson(), "承运信息不能为空"); @@ -492,6 +495,7 @@ public class WaybillServiceImpl extends BaseServiceImpl TransportBusinessSupport.validateNonNegative(waybill.getMileage(), "里程"); TransportBusinessSupport.validateNonNegative(waybill.getUnitPrice(), "单价"); TransportBusinessSupport.validateNonNegative(waybill.getOtherFeeTotal(), "其他费用合计"); + validateRoadTaskInfo(waybill); TransportBusinessSupport.validateDateRange(waybill.getStartDate(), waybill.getEndDate(), "开始日期不能晚于结束日期"); if (waybill.getEstimatedStartTime() != null && waybill.getEstimatedEndTime() != null @@ -518,6 +522,31 @@ public class WaybillServiceImpl extends BaseServiceImpl return false; } + private void validateRoadTaskInfo(Waybill waybill) { + if (!isRoadTransport(waybill.getTransportType())) { + return; + } + TransportBusinessSupport.validateLength(waybill.getDriverName(), 20, "司机姓名不能超过20字"); + TransportBusinessSupport.validateLength(waybill.getDriverPhone(), 20, "司机手机号不能超过20字"); + TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 30, "车牌号不能超过30字"); + TransportBusinessSupport.validateLength(waybill.getTrailerVehicleNo(), 30, "挂车车牌号不能超过30字"); + TransportBusinessSupport.validateLength(waybill.getEscortName(), 20, "押运人不能超过20字"); + TransportBusinessSupport.validateLength(waybill.getEscortPhone(), 20, "押运人手机号不能超过20字"); + BigDecimal mileage = waybill.getMileage(); + if (mileage == null) { + return; + } + BigDecimal normalizedMileage = mileage.stripTrailingZeros(); + if (normalizedMileage.signum() <= 0 || normalizedMileage.scale() > 0 || normalizedMileage.precision() > 10) { + throw new ServiceException("里程必须为不超过10位的正整数"); + } + } + + private boolean isRoadTransport(String transportType) { + String value = transportType == null ? "" : transportType.toLowerCase(); + return value.contains("公路") || value.contains("道路") || value.contains("road") || "gl".equals(value); + } + private void fillJsonFromFlatFields(Waybill waybill) { if (Func.isEmpty(waybill.getTaskEntryMode())) { waybill.setTaskEntryMode("simple"); diff --git a/doc/sql/transport/blade_customer_archive.sql b/doc/sql/transport/blade_customer_archive.sql index 2104eed..036a419 100644 --- a/doc/sql/transport/blade_customer_archive.sql +++ b/doc/sql/transport/blade_customer_archive.sql @@ -221,6 +221,9 @@ CREATE TABLE `blade_customer_change_record` ( `customer_id` bigint NOT NULL COMMENT '客商ID', `change_time` datetime NULL DEFAULT NULL COMMENT '变更日期', `change_content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '变更内容', + `changed_fields` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '变更字段', + `before_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '变更前数据JSON', + `after_data` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL COMMENT '变更后数据JSON', `change_user_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '变更账号', `create_user` bigint NULL DEFAULT NULL COMMENT '创建人', `create_dept` bigint NULL DEFAULT NULL COMMENT '创建部门', diff --git a/doc/sql/transport/blade_customer_change_record_patch_20260810.sql b/doc/sql/transport/blade_customer_change_record_patch_20260810.sql new file mode 100644 index 0000000..d0faa85 --- /dev/null +++ b/doc/sql/transport/blade_customer_change_record_patch_20260810.sql @@ -0,0 +1,6 @@ +-- 客商变更记录:增加字段变更审计明细 + +ALTER TABLE `blade_customer_change_record` + ADD COLUMN `changed_fields` varchar(1000) DEFAULT NULL COMMENT '变更字段' AFTER `change_content`, + ADD COLUMN `before_data` text COMMENT '变更前数据JSON' AFTER `changed_fields`, + ADD COLUMN `after_data` text COMMENT '变更后数据JSON' AFTER `before_data`; diff --git a/doc/sql/transport/blade_customer_credit_score_detail_repair_20260807.sql b/doc/sql/transport/blade_customer_credit_score_detail_repair_20260807.sql new file mode 100644 index 0000000..407de53 --- /dev/null +++ b/doc/sql/transport/blade_customer_credit_score_detail_repair_20260807.sql @@ -0,0 +1,9 @@ +-- 修复评分明细未写入 tenant_id 导致被多租户拦截器过滤的问题。 +-- 明细租户与所属评分主记录保持一致;仅更新 tenant_id 为空的历史数据。 + +UPDATE `blade_customer_credit_score_detail` detail +INNER JOIN `blade_customer_credit_score` score ON score.`id` = detail.`score_id` +SET detail.`tenant_id` = score.`tenant_id` +WHERE (detail.`tenant_id` IS NULL OR TRIM(detail.`tenant_id`) = '') + AND score.`tenant_id` IS NOT NULL + AND TRIM(score.`tenant_id`) <> '';