调整项目

This commit is contained in:
2026-09-13 01:01:09 +08:00
parent 2da4d824fd
commit 2262c39f60
7 changed files with 356 additions and 4 deletions
@@ -170,6 +170,10 @@ public class ProjectApply extends TenantEntity {
@Schema(description = "项目附件JSON") @Schema(description = "项目附件JSON")
private String attachmentsJson; private String attachmentsJson;
@Schema(description = "变更记录JSON")
@TableField(value = "change_record_json", insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private String changeRecordJson;
@Schema(description = "审批状态") @Schema(description = "审批状态")
private String approvalStatus; private String approvalStatus;
@@ -29,6 +29,7 @@ import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.ProjectApply;
import java.io.Serial; import java.io.Serial;
import java.math.BigDecimal;
/** /**
* 项目立项视图实体类 * 项目立项视图实体类
@@ -79,4 +80,24 @@ public class ProjectApplyVO extends ProjectApply {
@Schema(description = "是否仅查询可用于临时额度申请的项目") @Schema(description = "是否仅查询可用于临时额度申请的项目")
private Boolean temporaryCreditLimitSelectable; private Boolean temporaryCreditLimitSelectable;
@TableField(exist = false)
@Schema(description = "资金使用风险等级:high、medium、none")
private String fundUseRisk;
@TableField(exist = false)
@Schema(description = "资金使用风险名称")
private String fundUseRiskName;
@TableField(exist = false)
@Schema(description = "资金使用率(百分比)")
private BigDecimal fundUseRate;
@TableField(exist = false)
@Schema(description = "已使用资金金额")
private BigDecimal usedFundLimit;
@TableField(exist = false)
@Schema(description = "风险计算额度基数")
private BigDecimal maxFundLimit;
} }
@@ -48,6 +48,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 项目立项 控制器 * 项目立项 控制器
@@ -70,6 +71,22 @@ public class ProjectApplyController extends BladeController {
return R.data(projectApplyService.detail(id)); return R.data(projectApplyService.detail(id));
} }
@GetMapping("/fund-risk-stats")
@ApiOperationSupport(order = 2)
@Operation(summary = "资金使用风险统计", description = "传入项目筛选条件")
public R<Map<String, Integer>> fundRiskStats(ProjectApplyVO projectApply) {
return R.data(projectApplyService.fundRiskStats(projectApply));
}
@GetMapping("/change-record/detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "变更记录详情", description = "传入项目ID和变更记录序号")
public R<Map<String, Object>> changeRecordDetail(
@Parameter(description = "项目ID", required = true) @RequestParam Long id,
@Parameter(description = "变更记录序号,从0开始", required = true) @RequestParam Integer recordIndex) {
return R.data(projectApplyService.changeRecordDetail(id, recordIndex));
}
@GetMapping("/list") @GetMapping("/list")
@ApiOperationSupport(order = 2) @ApiOperationSupport(order = 2)
@Operation(summary = "分页", description = "传入projectApply") @Operation(summary = "分页", description = "传入projectApply")
@@ -29,6 +29,7 @@ import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.vo.ProjectApplyVO; import org.springblade.transport.pojo.vo.ProjectApplyVO;
import java.util.List; import java.util.List;
import java.util.Map;
/** /**
* 项目立项 服务类 * 项目立项 服务类
@@ -39,6 +40,8 @@ public interface IProjectApplyService extends BaseService<ProjectApply> {
IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply); IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply);
ProjectApplyVO detail(Long id); ProjectApplyVO detail(Long id);
Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply);
Map<String, Object> changeRecordDetail(Long id, Integer recordIndex);
boolean saveDraft(ProjectApply projectApply); boolean saveDraft(ProjectApply projectApply);
boolean submit(ProjectApply projectApply); boolean submit(ProjectApply projectApply);
boolean submitApproval(Long id); boolean submitApproval(Long id);
@@ -25,16 +25,24 @@ package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.springblade.core.log.exception.ServiceException; import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl; import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil; import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil; import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func; import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.UserCache; import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept; import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.excel.ProjectApplyExcel; import org.springblade.transport.excel.ProjectApplyExcel;
import org.springblade.transport.mapper.ProjectApplyMapper; import org.springblade.transport.mapper.ProjectApplyMapper;
import org.springblade.transport.mapper.FormalSettlementMapper;
import org.springblade.transport.mapper.ReceiptClaimSettlementMapper;
import org.springblade.transport.mapper.TemporaryCreditLimitMapper;
import org.springblade.transport.pojo.entity.ProjectApply; import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
import org.springblade.transport.pojo.entity.TemporaryCreditLimit;
import org.springblade.transport.pojo.vo.ProjectApplyVO; import org.springblade.transport.pojo.vo.ProjectApplyVO;
import org.springblade.transport.service.IProjectApplyService; import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.support.TransportBusinessSupport; import org.springblade.transport.support.TransportBusinessSupport;
@@ -47,9 +55,14 @@ import java.math.RoundingMode;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.stream.Collectors;
/** /**
* 项目立项 服务实现类 * 项目立项 服务实现类
@@ -57,6 +70,7 @@ import java.util.Objects;
* @author Chill * @author Chill
*/ */
@Service @Service
@RequiredArgsConstructor
public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper, ProjectApply> implements IProjectApplyService { public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper, ProjectApply> implements IProjectApplyService {
private static final String STATUS_DRAFT = "draft"; private static final String STATUS_DRAFT = "draft";
@@ -68,15 +82,21 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
private static final String STATUS_VOIDED = "voided"; private static final String STATUS_VOIDED = "voided";
private static final String EFFECTIVE_TEMPORARY = "temporary"; private static final String EFFECTIVE_TEMPORARY = "temporary";
private static final String EFFECTIVE_FORMAL = "formal"; private static final String EFFECTIVE_FORMAL = "formal";
private static final String STATUS_SETTLEMENT_APPROVED = "approved";
private static final String CHANGE_TYPE_PROJECT = "项目变更"; private static final String CHANGE_TYPE_PROJECT = "项目变更";
private static final String CHANGE_TYPE_RECORD = "项目备案调整"; private static final String CHANGE_TYPE_RECORD = "项目备案调整";
private static final String CHANGE_TYPE_DRAFT = "草稿编辑";
private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
private static final DateTimeFormatter PROJECT_YEAR = DateTimeFormatter.ofPattern("yyyy"); private static final DateTimeFormatter PROJECT_YEAR = DateTimeFormatter.ofPattern("yyyy");
private final TemporaryCreditLimitMapper temporaryCreditLimitMapper;
private final FormalSettlementMapper formalSettlementMapper;
private final ReceiptClaimSettlementMapper receiptClaimSettlementMapper;
@Override @Override
public IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply) { public IPage<ProjectApplyVO> selectProjectApplyPage(IPage<ProjectApply> page, ProjectApplyVO projectApply) {
IPage<ProjectApply> entityPage = page(page, buildQuery(projectApply)); IPage<ProjectApply> entityPage = page(page, buildQuery(projectApply));
IPage<ProjectApplyVO> voPage = ProjectApplyWrapper.build().pageVO(entityPage); IPage<ProjectApplyVO> voPage = ProjectApplyWrapper.build().pageVO(entityPage);
fillFundUseRisk(voPage.getRecords());
voPage.getRecords().forEach(this::fillReadonly); voPage.getRecords().forEach(this::fillReadonly);
return voPage; return voPage;
} }
@@ -89,25 +109,64 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
return projectApplyVO; return projectApplyVO;
} }
@Override
public Map<String, Integer> fundRiskStats(ProjectApplyVO projectApply) {
projectApply.setFundUseRisk(null);
List<ProjectApplyVO> records = list(buildQuery(projectApply)).stream()
.map(record -> ProjectApplyWrapper.build().entityVO(record))
.toList();
fillFundUseRisk(records);
Map<String, Integer> stats = new LinkedHashMap<>();
stats.put("high", (int) records.stream().filter(item -> "high".equals(item.getFundUseRisk())).count());
stats.put("medium", (int) records.stream().filter(item -> "medium".equals(item.getFundUseRisk())).count());
stats.put("none", (int) records.stream().filter(item -> "none".equals(item.getFundUseRisk())).count());
return stats;
}
@Override
public Map<String, Object> changeRecordDetail(Long id, Integer recordIndex) {
if (recordIndex == null || recordIndex < 0) {
throw new ServiceException("变更记录序号不能为空");
}
ProjectApply projectApply = loadExists(id);
List<Map<String, Object>> records = parseChangeRecords(projectApply.getChangeRecordJson());
if (recordIndex >= records.size()) {
throw new ServiceException("变更记录不存在");
}
return records.get(recordIndex);
}
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean saveDraft(ProjectApply projectApply) { public boolean saveDraft(ProjectApply projectApply) {
prepare(projectApply); ProjectApply beforeRecord = null;
if (Func.isNotEmpty(projectApply.getId())) { if (Func.isNotEmpty(projectApply.getId())) {
loadEditable(projectApply.getId(), true); beforeRecord = loadEditable(projectApply.getId(), true);
} }
prepare(projectApply);
prepareCreateOrUpdate(projectApply); prepareCreateOrUpdate(projectApply);
projectApply.setApprovalStatus(resolveEditableStatus(projectApply)); projectApply.setApprovalStatus(resolveEditableStatus(projectApply));
if (beforeRecord != null) {
projectApply.setChangeRecordJson(beforeRecord.getChangeRecordJson());
}
if (beforeRecord != null && Objects.equals(beforeRecord.getApprovalStatus(), STATUS_DRAFT)) {
appendProjectChangeRecord(projectApply, CHANGE_TYPE_DRAFT, null, STATUS_DRAFT, "草稿",
buildProjectChangeSnapshot(beforeRecord), buildProjectChangeSnapshot(projectApply));
}
return saveOrUpdate(projectApply); return saveOrUpdate(projectApply);
} }
@Override @Override
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean submit(ProjectApply projectApply) { public boolean submit(ProjectApply projectApply) {
ProjectApply beforeRecord = null;
if (Func.isNotEmpty(projectApply.getId())) {
beforeRecord = loadEditable(projectApply.getId(), true);
}
prepare(projectApply); prepare(projectApply);
validateSubmit(projectApply); validateSubmit(projectApply);
if (Func.isNotEmpty(projectApply.getId())) { if (beforeRecord != null) {
loadEditable(projectApply.getId(), true); projectApply.setChangeRecordJson(beforeRecord.getChangeRecordJson());
} }
prepareCreateOrUpdate(projectApply); prepareCreateOrUpdate(projectApply);
projectApply.setApprovalStatus(STATUS_REVIEWING); projectApply.setApprovalStatus(STATUS_REVIEWING);
@@ -186,10 +245,13 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean saveChange(ProjectApplyVO projectApply) { public boolean saveChange(ProjectApplyVO projectApply) {
ProjectApply oldRecord = loadChangeable(projectApply.getId()); ProjectApply oldRecord = loadChangeable(projectApply.getId());
Map<String, Object> beforeData = buildProjectChangeSnapshot(oldRecord);
prepare(projectApply); prepare(projectApply);
copyChangeFields(oldRecord, projectApply); copyChangeFields(oldRecord, projectApply);
validateSubmit(oldRecord); validateSubmit(oldRecord);
validateChangeLength(oldRecord); validateChangeLength(oldRecord);
Map<String, Object> afterData = buildProjectChangeSnapshot(oldRecord);
appendProjectChangeRecord(oldRecord, CHANGE_TYPE_PROJECT, oldRecord.getChangeReason(), "saved", "已保存", beforeData, afterData);
return updateById(oldRecord); return updateById(oldRecord);
} }
@@ -197,10 +259,14 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public boolean submitChange(ProjectApplyVO projectApply) { public boolean submitChange(ProjectApplyVO projectApply) {
ProjectApply oldRecord = loadChangeable(projectApply.getId()); ProjectApply oldRecord = loadChangeable(projectApply.getId());
Map<String, Object> beforeData = buildProjectChangeSnapshot(oldRecord);
prepare(projectApply); prepare(projectApply);
copyChangeFields(oldRecord, projectApply); copyChangeFields(oldRecord, projectApply);
validateSubmit(oldRecord); validateSubmit(oldRecord);
validateChange(oldRecord, projectApply.getChangeType()); validateChange(oldRecord, projectApply.getChangeType());
Map<String, Object> afterData = buildProjectChangeSnapshot(oldRecord);
appendProjectChangeRecord(oldRecord, projectApply.getChangeType(), oldRecord.getChangeReason(),
STATUS_CHANGE_REVIEWING, "变更审批中", beforeData, afterData);
oldRecord.setApprovalStatus(STATUS_CHANGE_REVIEWING); oldRecord.setApprovalStatus(STATUS_CHANGE_REVIEWING);
oldRecord.setCurrentNode(projectApply.getChangeType() + "审批"); oldRecord.setCurrentNode(projectApply.getChangeType() + "审批");
oldRecord.setCurrentProcessor("待处理"); oldRecord.setCurrentProcessor("待处理");
@@ -271,7 +337,10 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
queryWrapper.eq(ProjectApply::getUndertakeDeptId, projectApply.getUndertakeDeptId()); queryWrapper.eq(ProjectApply::getUndertakeDeptId, projectApply.getUndertakeDeptId());
} }
queryWrapper.like(Func.isNotEmpty(projectApply.getApplyNo()), ProjectApply::getApplyNo, projectApply.getApplyNo()) queryWrapper.like(Func.isNotEmpty(projectApply.getApplyNo()), ProjectApply::getApplyNo, projectApply.getApplyNo())
.like(Func.isNotEmpty(projectApply.getProjectCode()), ProjectApply::getProjectCode, projectApply.getProjectCode())
.like(Func.isNotEmpty(projectApply.getProjectName()), ProjectApply::getProjectName, projectApply.getProjectName()) .like(Func.isNotEmpty(projectApply.getProjectName()), ProjectApply::getProjectName, projectApply.getProjectName())
.like(Func.isNotEmpty(projectApply.getCustomerNames()), ProjectApply::getCustomerNames, projectApply.getCustomerNames())
.like(Func.isNotEmpty(projectApply.getCarrierNames()), ProjectApply::getCarrierNames, projectApply.getCarrierNames())
.like(Func.isNotEmpty(projectApply.getUndertakeDeptName()), ProjectApply::getUndertakeDeptName, projectApply.getUndertakeDeptName()) .like(Func.isNotEmpty(projectApply.getUndertakeDeptName()), ProjectApply::getUndertakeDeptName, projectApply.getUndertakeDeptName())
.like(Func.isNotEmpty(projectApply.getPrincipalUserName()), ProjectApply::getPrincipalUserName, projectApply.getPrincipalUserName()) .like(Func.isNotEmpty(projectApply.getPrincipalUserName()), ProjectApply::getPrincipalUserName, projectApply.getPrincipalUserName())
.eq(Func.isNotEmpty(projectApply.getProjectType()), ProjectApply::getProjectType, projectApply.getProjectType()) .eq(Func.isNotEmpty(projectApply.getProjectType()), ProjectApply::getProjectType, projectApply.getProjectType())
@@ -292,9 +361,84 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
queryWrapper.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL) queryWrapper.eq(ProjectApply::getEffectiveType, EFFECTIVE_FORMAL)
.in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved"); .in(ProjectApply::getApprovalStatus, STATUS_APPROVED, "change_approved");
} }
if (Func.isNotEmpty(projectApply.getFundUseRisk())) {
List<ProjectApplyVO> riskRecords = baseMapper.selectList(queryWrapper).stream()
.map(record -> ProjectApplyWrapper.build().entityVO(record))
.toList();
fillFundUseRisk(riskRecords);
List<Long> matchedIds = riskRecords.stream()
.filter(record -> Objects.equals(record.getFundUseRisk(), projectApply.getFundUseRisk()))
.map(ProjectApply::getId)
.toList();
queryWrapper.in(ProjectApply::getId, matchedIds.isEmpty() ? List.of(-1L) : matchedIds);
}
return queryWrapper; return queryWrapper;
} }
private void fillFundUseRisk(List<ProjectApplyVO> records) {
if (records == null || records.isEmpty()) {
return;
}
List<Long> projectIds = records.stream()
.map(ProjectApply::getId)
.filter(Objects::nonNull)
.distinct()
.toList();
if (projectIds.isEmpty()) {
return;
}
Map<Long, BigDecimal> approvedTemporaryLimits = new HashMap<>();
temporaryCreditLimitMapper.selectList(Wrappers.<TemporaryCreditLimit>lambdaQuery()
.in(TemporaryCreditLimit::getProjectId, projectIds)
.eq(TemporaryCreditLimit::getApprovalStatus, STATUS_SETTLEMENT_APPROVED)
.eq(TemporaryCreditLimit::getIsDeleted, 0))
.forEach(item -> approvedTemporaryLimits.merge(item.getProjectId(), nonNegative(item.getApplyLimit()), BigDecimal::add));
List<FormalSettlement> approvedReceivables = formalSettlementMapper.selectList(Wrappers.<FormalSettlement>lambdaQuery()
.in(FormalSettlement::getProjectId, projectIds)
.eq(FormalSettlement::getSettlementType, "receivable")
.eq(FormalSettlement::getApprovalStatus, STATUS_SETTLEMENT_APPROVED)
.eq(FormalSettlement::getIsDeleted, 0));
Map<Long, Long> settlementProjects = approvedReceivables.stream()
.filter(item -> item.getId() != null && item.getProjectId() != null)
.collect(Collectors.toMap(FormalSettlement::getId, FormalSettlement::getProjectId, (left, right) -> left));
Map<Long, BigDecimal> usedFundLimits = new HashMap<>();
if (!settlementProjects.isEmpty()) {
receiptClaimSettlementMapper.selectList(Wrappers.<ReceiptClaimSettlement>lambdaQuery()
.in(ReceiptClaimSettlement::getFormalSettlementId, settlementProjects.keySet())
.eq(ReceiptClaimSettlement::getStatus, 1)
.eq(ReceiptClaimSettlement::getIsDeleted, 0))
.forEach(item -> {
Long projectId = settlementProjects.get(item.getFormalSettlementId());
if (projectId != null) {
usedFundLimits.merge(projectId, nonNegative(item.getAllocatedReceiptAmount()), BigDecimal::add);
}
});
}
records.forEach(project -> {
BigDecimal projectLimit = nonNegative(project.getFundLimit());
BigDecimal temporaryLimit = approvedTemporaryLimits.getOrDefault(project.getId(), BigDecimal.ZERO);
BigDecimal maxFundLimit = projectLimit.max(temporaryLimit);
BigDecimal usedFundLimit = usedFundLimits.getOrDefault(project.getId(), BigDecimal.ZERO);
BigDecimal fundUseRate = maxFundLimit.signum() == 0
? BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP)
: usedFundLimit.multiply(BigDecimal.valueOf(100)).divide(maxFundLimit, 2, RoundingMode.HALF_UP);
String risk = fundUseRate.compareTo(new BigDecimal("90")) >= 0 ? "high"
: fundUseRate.compareTo(new BigDecimal("80")) >= 0 ? "medium" : "none";
project.setUsedFundLimit(usedFundLimit);
project.setMaxFundLimit(maxFundLimit);
project.setFundUseRate(fundUseRate);
project.setFundUseRisk(risk);
project.setFundUseRiskName("high".equals(risk) ? "高风险" : "medium".equals(risk) ? "中风险" : "无风险");
});
}
private BigDecimal nonNegative(BigDecimal value) {
return value == null || value.signum() < 0 ? BigDecimal.ZERO : value;
}
private void prepareCreateOrUpdate(ProjectApply projectApply) { private void prepareCreateOrUpdate(ProjectApply projectApply) {
if (Func.isEmpty(projectApply.getId())) { if (Func.isEmpty(projectApply.getId())) {
// 新增项目不产生变更记录,避免客户端误传变更字段导致记录回显。 // 新增项目不产生变更记录,避免客户端误传变更字段导致记录回显。
@@ -414,6 +558,146 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
target.setChangeReason(TransportBusinessSupport.trimToNull(source.getChangeReason())); target.setChangeReason(TransportBusinessSupport.trimToNull(source.getChangeReason()));
} }
private Map<String, Object> buildProjectChangeSnapshot(ProjectApply projectApply) {
Map<String, Object> data = new LinkedHashMap<>();
data.put("projectType", projectApply.getProjectType());
data.put("projectName", projectApply.getProjectName());
data.put("projectShortName", projectApply.getProjectShortName());
data.put("businessDeptName", projectApply.getBusinessDeptName());
data.put("undertakeDeptName", projectApply.getUndertakeDeptName());
data.put("projectSource", projectApply.getProjectSource());
data.put("sourceRemark", projectApply.getSourceRemark());
data.put("fundLimit", projectApply.getFundLimit());
data.put("receivableLimit", projectApply.getReceivableLimit());
data.put("receivableDays", projectApply.getReceivableDays());
data.put("paymentDays", projectApply.getPaymentDays());
data.put("cargoType", projectApply.getCargoType());
data.put("cargoQuantity", projectApply.getCargoQuantity());
data.put("businessStartDate", projectApply.getBusinessStartDate());
data.put("businessEndDate", projectApply.getBusinessEndDate());
data.put("transportRoute", projectApply.getTransportRoute());
data.put("transportType", projectApply.getTransportType());
data.put("businessType", projectApply.getBusinessType());
data.put("businessMode", projectApply.getBusinessMode());
data.put("projectScale", projectApply.getProjectScale());
data.put("settlementMode", projectApply.getSettlementMode());
data.put("estimatedProfit", projectApply.getEstimatedProfit());
data.put("profitRate", projectApply.getProfitRate());
data.put("fundDemand", projectApply.getFundDemand());
data.put("handlerUserName", projectApply.getHandlerUserName());
data.put("principalUserName", projectApply.getPrincipalUserName());
data.put("customerNames", projectApply.getCustomerNames());
data.put("carrierNames", projectApply.getCarrierNames());
data.put("situationRemark", normalizeProjectSnapshotJson(projectApply.getSituationRemark()));
data.put("attachmentsJson", normalizeProjectSnapshotJson(projectApply.getAttachmentsJson()));
return data;
}
private String normalizeProjectSnapshotJson(String value) {
if (Func.isEmpty(value)) {
return null;
}
String text = value.trim();
try {
Object parsed = JsonUtil.parse(text, Object.class);
if (parsed instanceof List<?> list && list.isEmpty()) {
return null;
}
if (parsed instanceof Map<?, ?> map && map.values().stream().allMatch(this::isEmptyProjectSnapshotValue)) {
return null;
}
} catch (Exception ignored) {
// 非JSON文本按原值参与差异比较。
}
return text;
}
private boolean isEmptyProjectSnapshotValue(Object value) {
if (value == null) {
return true;
}
if (value instanceof String text) {
return text.isBlank();
}
if (value instanceof List<?> list) {
return list.isEmpty();
}
if (value instanceof Map<?, ?> map) {
return map.isEmpty();
}
return false;
}
private void appendProjectChangeRecord(ProjectApply projectApply, String changeType, String changeReason,
String status, String statusName, Map<String, Object> beforeData,
Map<String, Object> afterData) {
retainChangedSnapshotFields(beforeData, afterData);
if (beforeData.isEmpty()) {
return;
}
List<Map<String, Object>> records = parseChangeRecords(projectApply.getChangeRecordJson());
Map<String, Object> record = new LinkedHashMap<>();
record.put("changeDate", LocalDate.now().toString());
record.put("handlerUserId", AuthUtil.getUserId());
record.put("handlerUserName", AuthUtil.getUserName());
record.put("changeType", changeType);
record.put("changeContent", buildProjectChangeContent(beforeData, afterData));
record.put("changeReason", TransportBusinessSupport.trimToNull(changeReason));
record.put("status", status);
record.put("statusName", statusName);
record.put("changedFields", new ArrayList<>(beforeData.keySet()));
record.put("beforeData", beforeData);
record.put("afterData", afterData);
records.add(record);
projectApply.setChangeRecordJson(JsonUtil.toJson(records));
}
private void retainChangedSnapshotFields(Map<String, Object> beforeData, Map<String, Object> afterData) {
List<String> unchangedFields = beforeData.entrySet().stream()
.filter(entry -> Objects.equals(entry.getValue(), afterData.get(entry.getKey())))
.map(Map.Entry::getKey)
.toList();
unchangedFields.forEach(field -> {
beforeData.remove(field);
afterData.remove(field);
});
}
private String buildProjectChangeContent(Map<String, Object> beforeData, Map<String, Object> afterData) {
return beforeData.keySet().stream()
.map(field -> "" + projectChangeFieldLabel(field) + "】从【"
+ formatProjectChangeValue(beforeData.get(field)) + "】调整为【"
+ formatProjectChangeValue(afterData.get(field)) + "")
.collect(Collectors.joining(""));
}
private String projectChangeFieldLabel(String field) {
Map<String, String> labels = Map.ofEntries(
Map.entry("projectType", "项目类型"), Map.entry("projectName", "项目名称"),
Map.entry("projectShortName", "项目简称"), Map.entry("businessDeptName", "业务部门"),
Map.entry("undertakeDeptName", "承办部门"), Map.entry("projectSource", "项目由来"),
Map.entry("sourceRemark", "项目由来说明"), Map.entry("fundLimit", "项目资金使用额度"),
Map.entry("receivableLimit", "项目应收账款额度"), Map.entry("receivableDays", "应收账款回款期限"),
Map.entry("paymentDays", "回款账期"), Map.entry("cargoType", "货物类型"),
Map.entry("cargoQuantity", "预估货物数量"), Map.entry("businessStartDate", "业务周期起"),
Map.entry("businessEndDate", "业务周期止"), Map.entry("transportRoute", "运输线路"),
Map.entry("transportType", "运输类型"), Map.entry("businessType", "业务类型"),
Map.entry("businessMode", "业务模式"), Map.entry("projectScale", "项目规模"),
Map.entry("settlementMode", "结算方式"), Map.entry("estimatedProfit", "预计利润"),
Map.entry("profitRate", "利润率"), Map.entry("fundDemand", "履约保证金"),
Map.entry("handlerUserName", "项目经办人"), Map.entry("principalUserName", "项目负责人"),
Map.entry("customerNames", "客户名称"), Map.entry("carrierNames", "下游承运商"),
Map.entry("situationRemark", "项目情况说明"), Map.entry("attachmentsJson", "项目材料"));
return labels.getOrDefault(field, field);
}
private String formatProjectChangeValue(Object value) {
if (value == null || (value instanceof String text && text.isBlank())) {
return "";
}
return String.valueOf(value);
}
private void validateDraft(ProjectApply projectApply) { private void validateDraft(ProjectApply projectApply) {
TransportBusinessSupport.validateRequired(projectApply.getProjectType(), "请选择项目类型"); TransportBusinessSupport.validateRequired(projectApply.getProjectType(), "请选择项目类型");
TransportBusinessSupport.validateRequired(projectApply.getProjectName(), "请输入项目名称"); TransportBusinessSupport.validateRequired(projectApply.getProjectName(), "请输入项目名称");
@@ -563,6 +847,25 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
return value == null ? null : value.setScale(2, RoundingMode.HALF_UP); return value == null ? null : value.setScale(2, RoundingMode.HALF_UP);
} }
@SuppressWarnings("unchecked")
private List<Map<String, Object>> parseChangeRecords(String value) {
if (Func.isEmpty(value)) {
return new ArrayList<>();
}
try {
Object records = JsonUtil.parse(value, Object.class);
if (records instanceof List<?> list) {
return (List<Map<String, Object>>) (List<?>) list;
}
if (records instanceof Map<?, ?> map) {
return new ArrayList<>(List.of((Map<String, Object>) map));
}
} catch (Exception ignored) {
// 历史记录JSON损坏时从空列表继续,避免影响项目保存。
}
return new ArrayList<>();
}
private void fillReadonly(ProjectApplyVO projectApplyVO) { private void fillReadonly(ProjectApplyVO projectApplyVO) {
projectApplyVO.setReadonly(!canCurrentUserOperate(projectApplyVO)); projectApplyVO.setReadonly(!canCurrentUserOperate(projectApplyVO));
} }
@@ -0,0 +1,3 @@
-- 项目立项增加变更记录JSON
ALTER TABLE `blade_project_apply`
ADD COLUMN `change_record_json` text DEFAULT NULL COMMENT '变更记录JSON' AFTER `attachments_json`;
@@ -49,6 +49,7 @@ CREATE TABLE `blade_project_apply` (
`carrier_json` text DEFAULT NULL COMMENT '承运商信息JSON', `carrier_json` text DEFAULT NULL COMMENT '承运商信息JSON',
`situation_remark` text DEFAULT NULL COMMENT '项目情况说明', `situation_remark` text DEFAULT NULL COMMENT '项目情况说明',
`attachments_json` text DEFAULT NULL COMMENT '项目附件JSON', `attachments_json` text DEFAULT NULL COMMENT '项目附件JSON',
`change_record_json` text DEFAULT NULL COMMENT '变更记录JSON',
`approval_status` varchar(50) DEFAULT NULL COMMENT '审批状态', `approval_status` varchar(50) DEFAULT NULL COMMENT '审批状态',
`current_node` varchar(100) DEFAULT NULL COMMENT '当前节点', `current_node` varchar(100) DEFAULT NULL COMMENT '当前节点',
`current_processor` varchar(100) DEFAULT NULL COMMENT '当前处理人', `current_processor` varchar(100) DEFAULT NULL COMMENT '当前处理人',