✨ 新增项目变更独立单据与MK审批回写

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 16:45:37 +08:00
parent adf782401a
commit 41a86fe335
11 changed files with 1379 additions and 0 deletions
@@ -0,0 +1,206 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 项目变更实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_project_change")
@Schema(description = "项目变更")
public class ProjectChange extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
@Schema(description = "原项目ID")
private Long projectId;
@Schema(description = "变更编号")
private String changeNo;
@Schema(description = "变更类型")
private String changeType;
@Schema(description = "变更日期")
private LocalDate changeDate;
@Schema(description = "变更内容")
private String changeContent;
@Schema(description = "变更原因")
private String changeReason;
@Schema(description = "变更前快照JSON")
private String beforeDataJson;
@Schema(description = "项目编号")
private String projectCode;
@Schema(description = "项目名称")
private String projectName;
@Schema(description = "项目简称")
private String projectShortName;
@Schema(description = "项目类型")
private String projectType;
@Schema(description = "业务部门ID")
private Long businessDeptId;
@Schema(description = "业务部门")
private String businessDeptName;
@Schema(description = "承办部门/平台公司ID,多个逗号分隔")
private String undertakeDeptId;
@Schema(description = "承办部门/平台公司,多个顿号分隔")
private String undertakeDeptName;
@Schema(description = "项目由来")
private String projectSource;
@Schema(description = "项目由来说明")
private String sourceRemark;
@Schema(description = "项目资金使用额度(万元)")
private BigDecimal fundLimit;
@Schema(description = "项目应收账款额度(万元)")
private BigDecimal receivableLimit;
@Schema(description = "应收账款回款期限(天)")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private Integer receivableDays;
@Schema(description = "回款账期(天)")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private Integer paymentDays;
@Schema(description = "货物类型")
private String cargoType;
@Schema(description = "预估货物数量")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private String cargoQuantity;
@Schema(description = "业务周期开始日期")
private LocalDate businessStartDate;
@Schema(description = "业务周期结束日期")
private LocalDate businessEndDate;
@Schema(description = "运输线路")
private String transportRoute;
@Schema(description = "运输类型")
private String transportType;
@Schema(description = "业务类型")
private String businessType;
@Schema(description = "业务模式")
private String businessMode;
@Schema(description = "项目规模(万元)")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal projectScale;
@Schema(description = "预计利润(万元)")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal estimatedProfit;
@Schema(description = "利润率(%)")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal profitRate;
@Schema(description = "资金需求(万元)")
@TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS)
private BigDecimal fundDemand;
@Schema(description = "结算方式")
private String settlementMode;
@Schema(description = "项目经办人ID")
private Long handlerUserId;
@Schema(description = "项目经办人")
private String handlerUserName;
@Schema(description = "项目负责人ID")
private Long principalUserId;
@Schema(description = "项目负责人")
private String principalUserName;
@Schema(description = "客户名称")
private String customerNames;
@Schema(description = "下游承运商")
private String carrierNames;
@Schema(description = "客户信息JSON")
private String customerJson;
@Schema(description = "承运商信息JSON")
private String carrierJson;
@Schema(description = "项目情况说明")
private String situationRemark;
@Schema(description = "项目附件JSON")
private String attachmentsJson;
@Schema(description = "审批状态")
private String approvalStatus;
@Schema(description = "当前节点")
private String currentNode;
@Schema(description = "当前处理人")
private String currentProcessor;
@Schema(description = "审核通过时间")
private LocalDateTime approvedTime;
@Schema(description = "同步状态")
private String syncStatus;
}
@@ -0,0 +1,84 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.vo;
import com.baomidou.mybatisplus.annotation.TableField;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.transport.pojo.entity.ProjectChange;
import java.io.Serial;
import java.util.List;
import java.util.Map;
/**
* 项目变更视图实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@Schema(description = "项目变更")
public class ProjectChangeVO extends ProjectChange {
@Serial
private static final long serialVersionUID = 1L;
@TableField(exist = false)
@Schema(description = "是否查看全部组织")
private Integer allDept;
@TableField(exist = false)
@Schema(description = "是否只读")
private Boolean readonly;
@TableField(exist = false)
@Schema(description = "创建人姓名")
private String createUserName;
@TableField(exist = false)
@Schema(description = "更新人姓名")
private String updateUserName;
@TableField(exist = false)
@Schema(description = "审批状态名称")
private String approvalStatusName;
@TableField(exist = false)
@Schema(description = "同步状态名称")
private String syncStatusName;
@TableField(exist = false)
@Schema(description = "货物类型字典,公开页回显使用")
private List<Map<String, String>> cargoTypeOptions;
@TableField(exist = false)
@Schema(description = "运输类型字典,公开页回显使用")
private List<Map<String, String>> transportTypeOptions;
@TableField(exist = false)
@Schema(description = "结算方式字典,公开页回显使用")
private List<Map<String, String>> settlementModeOptions;
}
@@ -0,0 +1,149 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.entity.ProjectChange;
import org.springblade.transport.pojo.vo.ProjectChangeVO;
import org.springblade.transport.service.IProjectChangeService;
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
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;
/**
* 项目变更 控制器
*
* @author Chill
*/
@RestController
@AllArgsConstructor
@PreAuth(menu = "project_change")
@RequestMapping("/project-change")
@Tag(name = "项目变更", description = "项目变更")
public class ProjectChangeController extends BladeController {
private final IProjectChangeService projectChangeService;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
@GetMapping("/detail")
@ApiOperationSupport(order = 1)
@Operation(summary = "详情", description = "传入id")
public R<ProjectChangeVO> detail(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.data(projectChangeService.detail(id));
}
@GetMapping("/list")
@ApiOperationSupport(order = 2)
@Operation(summary = "分页", description = "传入projectChange")
public R<IPage<ProjectChangeVO>> list(ProjectChangeVO projectChange, Query query) {
return R.data(projectChangeService.selectProjectChangePage(Condition.getPage(query), projectChange));
}
@PostMapping("/create-from-project")
@ApiOperationSupport(order = 3)
@Operation(summary = "基于项目创建变更草稿", description = "传入projectId、changeType")
public R<ProjectChangeVO> createFromProject(@Parameter(description = "项目ID", required = true) @RequestParam Long projectId,
@RequestParam(required = false) String changeType) {
return R.data(projectChangeService.createFromProject(projectId, changeType));
}
@PostMapping("/save-draft")
@ApiOperationSupport(order = 4)
@Operation(summary = "保存草稿", description = "传入projectChange")
public R saveDraft(@RequestBody ProjectChange projectChange) {
return R.status(projectChangeService.saveDraft(projectChange));
}
@PostMapping("/submit")
@ApiOperationSupport(order = 5)
@Operation(summary = "保存并提交审批", description = "传入projectChange")
public R submit(@RequestBody ProjectChange projectChange) {
return projectChangeService.submit(projectChange) ? R.data(projectChange) : R.fail("提交失败");
}
@PostMapping("/submit-approval")
@ApiOperationSupport(order = 6)
@Operation(summary = "提交审批", description = "传入id")
public R submitApproval(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(projectChangeService.submitApproval(id));
}
@PostMapping("/approve")
@ApiOperationSupport(order = 7)
@Operation(summary = "审批通过", description = "传入id,通过后回写原项目并推送合规")
public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) {
Long projectId = projectChangeService.approve(id);
hcsImmediatePushSupport.pushProjectAndWait(projectId);
String syncFailure = HcsImmediatePushSupport.pollSyncFailure();
if (syncFailure != null) {
ProjectChange projectChange = projectChangeService.getById(id);
if (projectChange != null) {
projectChange.setSyncStatus("failed");
projectChangeService.updateById(projectChange);
}
return R.data(Boolean.FALSE, "审批通过,但合规推送失败:" + syncFailure);
}
ProjectChange projectChange = projectChangeService.getById(id);
if (projectChange != null) {
projectChange.setSyncStatus("synced");
projectChangeService.updateById(projectChange);
}
return R.success("审批通过");
}
@PostMapping("/reject")
@ApiOperationSupport(order = 8)
@Operation(summary = "审批驳回", description = "传入id")
public R reject(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(projectChangeService.reject(id));
}
@PostMapping("/withdraw")
@ApiOperationSupport(order = 9)
@Operation(summary = "撤回审批", description = "传入id")
public R withdraw(@Parameter(description = "主键", required = true) @RequestParam Long id) {
return R.status(projectChangeService.withdraw(id));
}
@PostMapping("/remove")
@ApiOperationSupport(order = 10)
@Operation(summary = "逻辑删除", description = "传入ids")
public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) {
return R.status(projectChangeService.removeDraft(ids));
}
}
@@ -0,0 +1,34 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.transport.pojo.entity.ProjectChange;
/**
* 项目变更 Mapper 接口
*
* @author Chill
*/
public interface ProjectChangeMapper extends BaseMapper<ProjectChange> {
}
@@ -17,6 +17,7 @@ import org.springblade.transport.pojo.entity.InvoiceReceipt;
import org.springblade.transport.pojo.entity.PaymentApplication;
import org.springblade.transport.pojo.entity.PreSettlement;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.ProjectChange;
import org.springblade.transport.service.IBillPaymentService;
import org.springblade.transport.service.IContractManageService;
import org.springblade.transport.service.ICustomerArchiveService;
@@ -27,6 +28,7 @@ import org.springblade.transport.service.IPaymentApplicationService;
import org.springblade.transport.service.IPreSettlementService;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IProjectChangeService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.hcs.HcsImmediatePushSupport;
import org.springframework.stereotype.Component;
@@ -150,6 +152,78 @@ public final class MkProcessBizHandlers {
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
public static class ProjectChangeHandler implements IMkProcessBizHandler {
private final IProjectChangeService service;
private final HcsImmediatePushSupport hcsImmediatePushSupport;
@Override
public String bizType() {
return "project-change";
}
@Override
public Object publicDetail(Long id) {
return service.publicDetail(id);
}
@Override
public boolean syncFromProcessInfo(Long id, Object processInfo) {
ProjectChange entity = service.getById(id);
if (entity == null) {
return false;
}
String node = MkProcessNodeHelper.currentNode(processInfo);
String processor = MkProcessNodeHelper.currentProcessor(processInfo);
if (Func.isEmpty(node) && Func.isEmpty(processor)) {
return false;
}
if (Func.isNotEmpty(node)) {
entity.setCurrentNode(node);
}
if (Func.isNotEmpty(processor)) {
entity.setCurrentProcessor(processor);
}
entity.setApprovalStatus("reviewing");
return service.updateById(entity);
}
@Override
public boolean approveFromProcess(Long id, String processorName) {
ProjectChange entity = service.getById(id);
if (entity == null || "approved".equals(entity.getApprovalStatus())) {
return entity != null;
}
Long projectId = service.approve(id);
entity = service.getById(id);
if (entity != null) {
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(entity);
}
if (projectId != null) {
hcsImmediatePushSupport.pushProjectAfterCommit(projectId);
}
return true;
}
@Override
public boolean rejectFromProcess(Long id, String processorName) {
ProjectChange entity = service.getById(id);
if (entity == null || "rejected".equals(entity.getApprovalStatus())) {
return entity != null;
}
boolean rejected = service.reject(id);
entity = service.getById(id);
if (entity != null) {
entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统"));
service.updateById(entity);
}
return rejected;
}
}
@Component
@TenantIgnore
@RequiredArgsConstructor
@@ -26,6 +26,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.excel.ProjectApplyExcel;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.ProjectChange;
import org.springblade.transport.pojo.vo.ProjectApplyVO;
import java.util.List;
@@ -60,6 +61,15 @@ public interface IProjectApplyService extends BaseService<ProjectApply> {
boolean saveChange(ProjectApplyVO projectApply);
boolean submitChange(ProjectApplyVO projectApply);
boolean startChange(Long id, String changeContent, String changeReason);
/**
* 将独立项目变更单审批通过后的内容回写到原项目,并追加变更记录。
*
* @param change 已审批通过的项目变更单
* @return 是否成功
*/
boolean applyChangeFromRecord(ProjectChange change);
boolean removeDraft(String ids);
List<ProjectApplyExcel> exportProjectApply(ProjectApplyVO projectApply, String ids);
@@ -0,0 +1,70 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.entity.ProjectChange;
import org.springblade.transport.pojo.vo.ProjectChangeVO;
/**
* 项目变更 服务类
*
* @author Chill
*/
public interface IProjectChangeService extends BaseService<ProjectChange> {
IPage<ProjectChangeVO> selectProjectChangePage(IPage<ProjectChange> page, ProjectChangeVO projectChange);
ProjectChangeVO detail(Long id);
/**
* 公开详情(MK 回调/公开页)
*/
ProjectChangeVO publicDetail(Long id);
/**
* 基于正式项目创建变更草稿
*/
ProjectChangeVO createFromProject(Long projectId, String changeType);
boolean saveDraft(ProjectChange projectChange);
boolean submit(ProjectChange projectChange);
boolean submitApproval(Long id);
/**
* 审批通过:回写原项目并标记同步
*
* @return 原项目ID,供 Controller 推送合规
*/
Long approve(Long id);
boolean reject(Long id);
boolean withdraw(Long id);
boolean removeDraft(String ids);
}
@@ -44,6 +44,7 @@ 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.ProjectChange;
import org.springblade.transport.pojo.entity.FormalSettlement;
import org.springblade.transport.pojo.entity.ReceiptClaimSettlement;
import org.springblade.transport.pojo.entity.TemporaryCreditLimit;
@@ -307,6 +308,72 @@ public class ProjectApplyServiceImpl extends BaseServiceImpl<ProjectApplyMapper,
return updateById(projectApply);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean applyChangeFromRecord(ProjectChange change) {
if (change == null || Func.isEmpty(change.getProjectId())) {
throw new ServiceException("项目变更单无效");
}
ProjectApply projectApply = loadExists(change.getProjectId());
Map<String, Object> beforeData = buildProjectChangeSnapshot(projectApply);
copyChangeFields(projectApply, toProjectApplySnapshot(change));
Map<String, Object> afterData = buildProjectChangeSnapshot(projectApply);
String changeType = Func.isBlank(change.getChangeType()) ? CHANGE_TYPE_PROJECT : change.getChangeType();
appendProjectChangeRecord(projectApply, changeType, change.getChangeReason(),
STATUS_CHANGE_APPROVED, "变更审批通过", beforeData, afterData);
projectApply.setApprovalStatus(STATUS_CHANGE_APPROVED);
projectApply.setChangeContent(TransportBusinessSupport.trimToNull(change.getChangeContent()));
projectApply.setChangeReason(TransportBusinessSupport.trimToNull(change.getChangeReason()));
projectApply.setEffectiveType(EFFECTIVE_FORMAL);
projectApply.setCurrentNode("变更审批通过");
String processor = TransportBusinessSupport.trimToNull(AuthUtil.getUserName());
projectApply.setCurrentProcessor(processor == null ? "系统" : processor);
projectApply.setApprovedTime(LocalDateTime.now());
projectApply.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
return updateById(projectApply);
}
/**
* 将变更单可回写字段映射为临时 ProjectApply,供 copyChangeFields 复用。
*/
private ProjectApply toProjectApplySnapshot(ProjectChange change) {
ProjectApply snapshot = new ProjectApply();
snapshot.setBusinessDeptId(change.getBusinessDeptId());
snapshot.setBusinessDeptName(change.getBusinessDeptName());
snapshot.setUndertakeDeptId(change.getUndertakeDeptId());
snapshot.setUndertakeDeptName(change.getUndertakeDeptName());
snapshot.setFundLimit(change.getFundLimit());
snapshot.setReceivableLimit(change.getReceivableLimit());
snapshot.setReceivableDays(change.getReceivableDays());
snapshot.setPaymentDays(change.getPaymentDays());
snapshot.setCargoType(change.getCargoType());
snapshot.setCargoQuantity(change.getCargoQuantity());
snapshot.setBusinessStartDate(change.getBusinessStartDate());
snapshot.setBusinessEndDate(change.getBusinessEndDate());
snapshot.setTransportRoute(change.getTransportRoute());
snapshot.setTransportType(change.getTransportType());
snapshot.setBusinessType(change.getBusinessType());
snapshot.setBusinessMode(change.getBusinessMode());
snapshot.setProjectScale(change.getProjectScale());
snapshot.setSettlementMode(change.getSettlementMode());
snapshot.setEstimatedProfit(change.getEstimatedProfit());
snapshot.setProfitRate(change.getProfitRate());
snapshot.setFundDemand(change.getFundDemand());
snapshot.setHandlerUserId(change.getHandlerUserId());
snapshot.setHandlerUserName(change.getHandlerUserName());
snapshot.setPrincipalUserId(change.getPrincipalUserId());
snapshot.setPrincipalUserName(change.getPrincipalUserName());
snapshot.setCustomerNames(change.getCustomerNames());
snapshot.setCarrierNames(change.getCarrierNames());
snapshot.setCustomerJson(change.getCustomerJson());
snapshot.setCarrierJson(change.getCarrierJson());
snapshot.setSituationRemark(change.getSituationRemark());
snapshot.setAttachmentsJson(change.getAttachmentsJson());
snapshot.setChangeContent(change.getChangeContent());
snapshot.setChangeReason(change.getChangeReason());
return snapshot;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeDraft(String ids) {
@@ -0,0 +1,517 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.ProjectChangeMapper;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.entity.ProjectChange;
import org.springblade.transport.pojo.vo.ProjectApplyVO;
import org.springblade.transport.pojo.vo.ProjectChangeVO;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IProjectChangeService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.wrapper.ProjectChangeWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 项目变更 服务实现类
*
* @author Chill
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class ProjectChangeServiceImpl extends BaseServiceImpl<ProjectChangeMapper, ProjectChange> implements IProjectChangeService {
private static final String STATUS_DRAFT = "draft";
private static final String STATUS_REVIEWING = "reviewing";
private static final String STATUS_WITHDRAWN = "withdrawn";
private static final String STATUS_APPROVED = "approved";
private static final String STATUS_REJECTED = "rejected";
private static final String CHANGE_TYPE_PROJECT = "项目变更";
private static final String CHANGE_TYPE_RECORD = "项目备案调整";
private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd");
private final IProjectApplyService projectApplyService;
@Override
public IPage<ProjectChangeVO> selectProjectChangePage(IPage<ProjectChange> page, ProjectChangeVO projectChange) {
IPage<ProjectChange> entityPage = page(page, buildQuery(projectChange).orderByDesc(ProjectChange::getCreateTime));
IPage<ProjectChangeVO> voPage = ProjectChangeWrapper.build().pageVO(entityPage);
voPage.getRecords().forEach(this::fillReadonly);
return voPage;
}
@Override
public ProjectChangeVO detail(Long id) {
ProjectChangeVO projectChangeVO = ProjectChangeWrapper.build().entityVO(loadExists(id));
fillReadonly(projectChangeVO);
return projectChangeVO;
}
@Override
@TenantIgnore
public ProjectChangeVO publicDetail(Long id) {
ProjectChangeVO projectChangeVO = ProjectChangeWrapper.build().entityVO(loadExists(id));
projectChangeVO.setReadonly(Boolean.TRUE);
return projectChangeVO;
}
@Override
@Transactional(rollbackFor = Exception.class)
public ProjectChangeVO createFromProject(Long projectId, String changeType) {
ProjectApplyVO projectApply = projectApplyService.detail(projectId);
validateProjectChangeable(projectApply);
ProjectChange reviewing = findLatestByStatuses(projectId, List.of(STATUS_REVIEWING));
if (reviewing != null) {
throw new ServiceException("该项目已有审批中的变更单,请先完成后再发起");
}
ProjectChange unfinished = findLatestByStatuses(projectId, List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED));
if (unfinished != null) {
// 已有未完结变更单时复用,避免重复建单
return detail(unfinished.getId());
}
String resolvedType = Func.isBlank(changeType) ? CHANGE_TYPE_PROJECT : changeType.trim();
if (!List.of(CHANGE_TYPE_PROJECT, CHANGE_TYPE_RECORD).contains(resolvedType)) {
throw new ServiceException("请选择变更类型");
}
ProjectChange projectChange = copyFromProject(projectApply);
projectChange.setId(null);
projectChange.setProjectId(projectApply.getId());
projectChange.setChangeNo(nextChangeNo());
projectChange.setChangeType(resolvedType);
projectChange.setChangeDate(LocalDate.now());
projectChange.setChangeContent(null);
projectChange.setChangeReason(null);
projectChange.setBeforeDataJson(JsonUtil.toJson(buildSnapshot(projectApply)));
projectChange.setApprovalStatus(STATUS_DRAFT);
projectChange.setCurrentNode("草稿");
projectChange.setCurrentProcessor(AuthUtil.getUserName());
projectChange.setApprovedTime(null);
projectChange.setSyncStatus(null);
save(projectChange);
return detail(projectChange.getId());
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean saveDraft(ProjectChange projectChange) {
ProjectChange oldRecord = loadEditable(projectChange.getId());
prepareEditableFields(oldRecord, projectChange);
validateDraft(oldRecord);
if (Func.isEmpty(oldRecord.getApprovalStatus()) || STATUS_REVIEWING.equals(oldRecord.getApprovalStatus())) {
oldRecord.setApprovalStatus(STATUS_DRAFT);
}
if (List.of(STATUS_WITHDRAWN, STATUS_REJECTED).contains(oldRecord.getApprovalStatus())) {
// 驳回/撤回后编辑仍保持原状态,提交时再进入审批中
} else {
oldRecord.setApprovalStatus(STATUS_DRAFT);
oldRecord.setCurrentNode("草稿");
}
return updateById(oldRecord);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submit(ProjectChange projectChange) {
ProjectChange oldRecord = loadEditable(projectChange.getId());
prepareEditableFields(oldRecord, projectChange);
validateSubmit(oldRecord);
oldRecord.setApprovalStatus(STATUS_REVIEWING);
oldRecord.setCurrentNode(resolveApprovalNode(oldRecord));
oldRecord.setCurrentProcessor("待处理");
oldRecord.setApprovedTime(null);
return updateById(oldRecord);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean submitApproval(Long id) {
ProjectChange projectChange = loadEditable(id);
validateSubmit(projectChange);
projectChange.setApprovalStatus(STATUS_REVIEWING);
projectChange.setCurrentNode(resolveApprovalNode(projectChange));
projectChange.setCurrentProcessor("待处理");
projectChange.setApprovedTime(null);
return updateById(projectChange);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Long approve(Long id) {
ProjectChange projectChange = loadReviewing(id);
projectChange.setApprovalStatus(STATUS_APPROVED);
projectChange.setCurrentNode("审批通过");
String processor = TransportBusinessSupport.trimToNull(AuthUtil.getUserName());
projectChange.setCurrentProcessor(processor == null ? "系统" : processor);
projectChange.setApprovedTime(LocalDateTime.now());
projectChange.setSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED);
updateById(projectChange);
projectApplyService.applyChangeFromRecord(projectChange);
return projectChange.getProjectId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean reject(Long id) {
ProjectChange projectChange = loadReviewing(id);
projectChange.setApprovalStatus(STATUS_REJECTED);
projectChange.setCurrentNode("审核不通过");
String processor = TransportBusinessSupport.trimToNull(AuthUtil.getUserName());
projectChange.setCurrentProcessor(processor == null ? "系统" : processor);
return updateById(projectChange);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean withdraw(Long id) {
ProjectChange projectChange = loadReviewing(id);
projectChange.setApprovalStatus(STATUS_WITHDRAWN);
projectChange.setCurrentNode("已撤回");
projectChange.setCurrentProcessor(AuthUtil.getUserName());
return updateById(projectChange);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean removeDraft(String ids) {
List<Long> idList = Func.toLongList(ids);
if (Func.isEmpty(idList)) {
throw new ServiceException("请选择需要删除的数据");
}
for (ProjectChange projectChange : listByIds(idList)) {
if (!List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED).contains(projectChange.getApprovalStatus())) {
throw new ServiceException("仅草稿、已撤回、已驳回的项目变更允许删除");
}
}
return deleteLogic(idList);
}
private LambdaQueryWrapper<ProjectChange> buildQuery(ProjectChangeVO projectChange) {
TransportBusinessSupport.validateAllDept(projectChange.getAllDept(), "项目变更");
LambdaQueryWrapper<ProjectChange> queryWrapper = Wrappers.<ProjectChange>lambdaQuery().eq(ProjectChange::getIsDeleted, 0);
if (TransportBusinessSupport.isAdministrator()) {
if (Objects.equals(projectChange.getAllDept(), 1) && Func.isNotEmpty(projectChange.getUndertakeDeptId())) {
applyUndertakeDeptContains(queryWrapper, projectChange.getUndertakeDeptId());
}
} else if (!Objects.equals(projectChange.getAllDept(), 1)) {
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
Long currentUserId = AuthUtil.getUserId();
queryWrapper.and(wrapper -> {
if (currentDeptId != null) {
wrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", String.valueOf(currentDeptId));
}
wrapper.or().eq(ProjectChange::getCreateUser, currentUserId)
.or().eq(ProjectChange::getHandlerUserId, currentUserId)
.or().eq(ProjectChange::getPrincipalUserId, currentUserId);
});
} else if (Func.isNotEmpty(projectChange.getUndertakeDeptId())) {
applyUndertakeDeptContains(queryWrapper, projectChange.getUndertakeDeptId());
}
queryWrapper.like(Func.isNotEmpty(projectChange.getProjectCode()), ProjectChange::getProjectCode, projectChange.getProjectCode())
.like(Func.isNotEmpty(projectChange.getProjectName()), ProjectChange::getProjectName, projectChange.getProjectName())
.like(Func.isNotEmpty(projectChange.getUndertakeDeptName()), ProjectChange::getUndertakeDeptName, projectChange.getUndertakeDeptName())
.like(Func.isNotEmpty(projectChange.getPrincipalUserName()), ProjectChange::getPrincipalUserName, projectChange.getPrincipalUserName())
.eq(Func.isNotEmpty(projectChange.getProjectType()), ProjectChange::getProjectType, projectChange.getProjectType())
.eq(Func.isNotEmpty(projectChange.getApprovalStatus()), ProjectChange::getApprovalStatus, projectChange.getApprovalStatus())
.eq(Func.isNotEmpty(projectChange.getChangeType()), ProjectChange::getChangeType, projectChange.getChangeType())
.eq(Func.isNotEmpty(projectChange.getProjectId()), ProjectChange::getProjectId, projectChange.getProjectId())
.like(Func.isNotEmpty(projectChange.getChangeNo()), ProjectChange::getChangeNo, projectChange.getChangeNo());
return queryWrapper;
}
private void applyUndertakeDeptContains(LambdaQueryWrapper<ProjectChange> queryWrapper, String undertakeDeptId) {
for (String deptId : undertakeDeptId.split(",")) {
if (Func.isNotBlank(deptId)) {
queryWrapper.apply("FIND_IN_SET({0}, IFNULL(undertake_dept_id,''))", deptId.trim());
}
}
}
private ProjectChange copyFromProject(ProjectApply projectApply) {
ProjectChange projectChange = Objects.requireNonNull(BeanUtil.copyProperties(projectApply, ProjectChange.class));
projectChange.setId(null);
projectChange.setCreateUser(null);
projectChange.setCreateDept(null);
projectChange.setCreateTime(null);
projectChange.setUpdateUser(null);
projectChange.setUpdateTime(null);
projectChange.setStatus(1);
projectChange.setIsDeleted(0);
return projectChange;
}
private void prepareEditableFields(ProjectChange target, ProjectChange source) {
target.setChangeType(TransportBusinessSupport.trimToNull(source.getChangeType()));
target.setChangeReason(TransportBusinessSupport.trimToNull(source.getChangeReason()));
target.setChangeContent(TransportBusinessSupport.trimToNull(source.getChangeContent()));
target.setBusinessDeptId(source.getBusinessDeptId());
target.setBusinessDeptName(source.getBusinessDeptName());
normalizeUndertakeDept(source);
target.setUndertakeDeptId(source.getUndertakeDeptId());
target.setUndertakeDeptName(source.getUndertakeDeptName());
target.setFundLimit(source.getFundLimit());
target.setReceivableLimit(source.getReceivableLimit());
target.setReceivableDays(source.getReceivableDays());
target.setPaymentDays(source.getPaymentDays());
target.setCargoType(source.getCargoType());
target.setCargoQuantity(source.getCargoQuantity());
target.setBusinessStartDate(source.getBusinessStartDate());
target.setBusinessEndDate(source.getBusinessEndDate());
target.setTransportRoute(source.getTransportRoute());
target.setTransportType(source.getTransportType());
target.setBusinessType(source.getBusinessType());
target.setBusinessMode(source.getBusinessMode());
target.setProjectScale(source.getProjectScale());
target.setSettlementMode(source.getSettlementMode());
target.setEstimatedProfit(source.getEstimatedProfit());
target.setProfitRate(source.getProfitRate());
target.setFundDemand(source.getFundDemand());
target.setHandlerUserId(source.getHandlerUserId());
target.setHandlerUserName(source.getHandlerUserName());
target.setPrincipalUserId(source.getPrincipalUserId());
target.setPrincipalUserName(source.getPrincipalUserName());
target.setCustomerNames(source.getCustomerNames());
target.setCarrierNames(source.getCarrierNames());
target.setCustomerJson(source.getCustomerJson());
target.setCarrierJson(source.getCarrierJson());
target.setSituationRemark(source.getSituationRemark());
target.setAttachmentsJson(source.getAttachmentsJson());
if (source.getReceivableDays() != null && source.getReceivableDays() == -1) {
target.setReceivableDays(null);
}
if (source.getPaymentDays() != null && source.getPaymentDays() == -1) {
target.setPaymentDays(null);
}
if (source.getCargoQuantity() != null && "-1".equals(source.getCargoQuantity().trim())) {
target.setCargoQuantity(null);
}
}
private void normalizeUndertakeDept(ProjectChange projectChange) {
if (Func.isEmpty(projectChange.getUndertakeDeptId())) {
return;
}
String[] ids = projectChange.getUndertakeDeptId().split(",");
StringBuilder idBuilder = new StringBuilder();
for (String id : ids) {
if (Func.isBlank(id)) {
continue;
}
if (!idBuilder.isEmpty()) {
idBuilder.append(",");
}
idBuilder.append(id.trim());
}
projectChange.setUndertakeDeptId(idBuilder.toString());
}
private Map<String, Object> buildSnapshot(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("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());
return data;
}
private void validateProjectChangeable(ProjectApply projectApply) {
boolean approved = List.of("approved", "change_approved").contains(projectApply.getApprovalStatus());
if (!approved || !Objects.equals(projectApply.getEffectiveType(), "formal")) {
throw new ServiceException("仅正式生效且审批通过的项目允许发起变更");
}
if ("change_reviewing".equals(projectApply.getApprovalStatus())) {
throw new ServiceException("项目存在进行中的变更审批,请先完成后再发起");
}
}
private ProjectChange findLatestByStatuses(Long projectId, List<String> statuses) {
return getOne(Wrappers.<ProjectChange>lambdaQuery()
.eq(ProjectChange::getIsDeleted, 0)
.eq(ProjectChange::getProjectId, projectId)
.in(ProjectChange::getApprovalStatus, statuses)
.orderByDesc(ProjectChange::getCreateTime)
.last("limit 1"), false);
}
private void validateDraft(ProjectChange projectChange) {
TransportBusinessSupport.validateRequired(projectChange.getChangeType(), "请选择变更类型");
if (!List.of(CHANGE_TYPE_PROJECT, CHANGE_TYPE_RECORD).contains(projectChange.getChangeType())) {
throw new ServiceException("请选择变更类型");
}
TransportBusinessSupport.validateLength(projectChange.getChangeReason(), 2000, "变更原因不能超过2000个字");
TransportBusinessSupport.validateLength(projectChange.getChangeContent(), 2000, "变更内容不能超过2000个字");
}
private void validateSubmit(ProjectChange projectChange) {
validateDraft(projectChange);
TransportBusinessSupport.validateRequired(projectChange.getUndertakeDeptName(), "请选择承办部门");
TransportBusinessSupport.validateRequired(projectChange.getBusinessMode(), "请选择业务模式");
TransportBusinessSupport.validateRequired(projectChange.getCustomerNames(), "请选择客户");
TransportBusinessSupport.validateRequired(projectChange.getCarrierNames(), "请选择下游承运商");
validateAmount(projectChange.getFundLimit(), "项目资金使用额度", false);
validateAmount(projectChange.getReceivableLimit(), "项目应收账款额度", false);
validateDays(projectChange.getReceivableDays(), "应收账款回款期限");
validateDays(projectChange.getPaymentDays(), "回款账期");
validateAmount(projectChange.getProjectScale(), "项目规模", true);
validateAmount(projectChange.getEstimatedProfit(), "预计利润", true);
validateAmount(projectChange.getProfitRate(), "利润率", true);
validateAmount(projectChange.getFundDemand(), "资金需求", true);
TransportBusinessSupport.validateDateRange(projectChange.getBusinessStartDate(), projectChange.getBusinessEndDate(), "业务周期开始日期不能晚于结束日期");
if (Func.isEmpty(projectChange.getHandlerUserId()) || Func.isEmpty(projectChange.getPrincipalUserId())) {
throw new ServiceException("项目经办人和项目负责人不能为空");
}
TransportBusinessSupport.validateRequired(projectChange.getChangeReason(), "请输入变更原因");
}
private void validateAmount(BigDecimal value, String label, boolean nullable) {
if (!nullable && value == null) {
throw new ServiceException(label + "不能为空");
}
if (value == null) {
return;
}
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new ServiceException(label + "不能小于0");
}
if (value.scale() > 2) {
throw new ServiceException(label + "最多保留2位小数");
}
}
private void validateDays(Integer value, String label) {
if (value == null) {
return;
}
if (value < 0 || value > 999) {
throw new ServiceException(label + "范围为0到999天");
}
}
private String resolveApprovalNode(ProjectChange projectChange) {
return Func.isBlank(projectChange.getChangeType()) ? "项目变更审批" : projectChange.getChangeType() + "审批";
}
private synchronized String nextChangeNo() {
return "LXBG" + LocalDate.now().format(CODE_DATE) + String.format("%06d", count() + 1);
}
private ProjectChange loadExists(Long id) {
if (Func.isEmpty(id)) {
throw new ServiceException("主键不能为空");
}
ProjectChange projectChange = getById(id);
if (Func.isEmpty(projectChange) || Objects.equals(projectChange.getIsDeleted(), 1)) {
throw new ServiceException("项目变更不存在");
}
return projectChange;
}
private ProjectChange loadEditable(Long id) {
ProjectChange projectChange = loadExists(id);
if (!canCurrentUserOperate(projectChange)) {
throw new ServiceException("无权操作其他组织项目变更");
}
if (!List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED).contains(projectChange.getApprovalStatus())) {
throw new ServiceException("当前状态不允许编辑");
}
return projectChange;
}
private ProjectChange loadReviewing(Long id) {
ProjectChange projectChange = loadExists(id);
if (!Objects.equals(projectChange.getApprovalStatus(), STATUS_REVIEWING)) {
throw new ServiceException("仅审批中的项目变更允许该操作");
}
return projectChange;
}
private boolean canCurrentUserOperate(ProjectChange projectChange) {
if (TransportBusinessSupport.isAdministrator()) {
return true;
}
Long currentUserId = AuthUtil.getUserId();
if (Objects.equals(projectChange.getCreateUser(), currentUserId)
|| Objects.equals(projectChange.getHandlerUserId(), currentUserId)
|| Objects.equals(projectChange.getPrincipalUserId(), currentUserId)) {
return true;
}
Long currentDeptId = TransportBusinessSupport.currentDeptId("项目变更");
if (currentDeptId == null || Func.isBlank(projectChange.getUndertakeDeptId())) {
return false;
}
return List.of(projectChange.getUndertakeDeptId().split(",")).stream()
.map(String::trim)
.anyMatch(deptId -> Objects.equals(deptId, String.valueOf(currentDeptId)));
}
private void fillReadonly(ProjectChangeVO projectChangeVO) {
projectChangeVO.setReadonly(!canCurrentUserOperate(projectChangeVO));
}
}
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.ProjectChange;
import org.springblade.transport.pojo.vo.ProjectChangeVO;
import java.util.Objects;
/**
* 项目变更包装类
*
* @author Chill
*/
public class ProjectChangeWrapper extends BaseEntityWrapper<ProjectChange, ProjectChangeVO> {
public static ProjectChangeWrapper build() {
return new ProjectChangeWrapper();
}
@Override
public ProjectChangeVO entityVO(ProjectChange projectChange) {
ProjectChangeVO projectChangeVO = Objects.requireNonNull(BeanUtil.copyProperties(projectChange, ProjectChangeVO.class));
projectChangeVO.setCreateUserName(UserCache.getUserRealName(projectChange.getCreateUser()));
projectChangeVO.setUpdateUserName(UserCache.getUserRealName(projectChange.getUpdateUser()));
projectChangeVO.setApprovalStatusName(statusName(projectChange.getApprovalStatus()));
projectChangeVO.setSyncStatusName(syncStatusName(projectChange.getSyncStatus()));
return projectChangeVO;
}
private String statusName(String status) {
if (status == null) {
return "未知";
}
return switch (status) {
case "draft" -> "草稿";
case "reviewing" -> "审批中";
case "withdrawn" -> "已撤回";
case "approved" -> "审批通过";
case "rejected" -> "已驳回";
default -> "未知";
};
}
private String syncStatusName(String syncStatus) {
if (syncStatus == null || syncStatus.isBlank()) {
return "";
}
return switch (syncStatus) {
case "unsynced" -> "待推送";
case "synced" -> "已推送";
case "failed" -> "推送失败";
default -> syncStatus;
};
}
}
@@ -0,0 +1,88 @@
-- 项目变更独立单据 + 菜单
-- 审批通过后再回写 blade_project_apply
CREATE TABLE IF NOT EXISTS `blade_project_change` (
`id` bigint(20) NOT NULL COMMENT '主键',
`tenant_id` varchar(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT '000000' COMMENT '租户ID',
`create_user` bigint(20) DEFAULT NULL COMMENT '创建人',
`create_dept` bigint(20) DEFAULT NULL COMMENT '创建部门',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_user` bigint(20) DEFAULT NULL COMMENT '修改人',
`update_time` datetime DEFAULT NULL COMMENT '修改时间',
`status` int(11) DEFAULT '1' COMMENT '状态',
`is_deleted` int(11) DEFAULT '0' COMMENT '是否已删除',
`project_id` bigint(20) NOT NULL COMMENT '原项目ID',
`change_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更编号',
`change_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更类型',
`change_date` date DEFAULT NULL COMMENT '变更日期',
`change_content` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '变更内容',
`change_reason` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更原因',
`before_data_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '变更前快照JSON',
`project_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目编号',
`project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目名称',
`project_short_name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目简称',
`project_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目类型',
`business_dept_id` bigint(20) DEFAULT NULL COMMENT '业务部门ID',
`business_dept_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '业务部门',
`undertake_dept_id` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '承办部门/平台公司ID,多个逗号分隔',
`undertake_dept_name` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '承办部门/平台公司,多个顿号分隔',
`project_source` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目由来',
`source_remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目由来说明',
`fund_limit` decimal(18,2) DEFAULT NULL COMMENT '项目资金使用额度(万元)',
`receivable_limit` decimal(18,2) DEFAULT NULL COMMENT '项目应收账款额度(万元)',
`receivable_days` int(11) DEFAULT NULL COMMENT '应收账款回款期限(天)',
`payment_days` int(11) DEFAULT NULL COMMENT '回款账期(天)',
`cargo_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '货物类型',
`cargo_quantity` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '预估货物数量',
`business_start_date` date DEFAULT NULL COMMENT '业务周期开始日期',
`business_end_date` date DEFAULT NULL COMMENT '业务周期结束日期',
`transport_route` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '运输线路',
`transport_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '运输类型',
`business_type` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '业务类型',
`business_mode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '业务模式',
`project_scale` decimal(18,2) DEFAULT NULL COMMENT '项目规模(万元)',
`estimated_profit` decimal(18,2) DEFAULT NULL COMMENT '预计利润(万元)',
`profit_rate` decimal(18,2) DEFAULT NULL COMMENT '利润率(%)',
`fund_demand` decimal(18,2) DEFAULT NULL COMMENT '资金需求(万元)',
`settlement_mode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '结算方式',
`handler_user_id` bigint(20) DEFAULT NULL COMMENT '项目经办人ID',
`handler_user_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目经办人',
`principal_user_id` bigint(20) DEFAULT NULL COMMENT '项目负责人ID',
`principal_user_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '项目负责人',
`customer_names` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '客户名称',
`carrier_names` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '下游承运商',
`customer_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '客户信息JSON',
`carrier_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '承运商信息JSON',
`situation_remark` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '项目情况说明',
`attachments_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '项目附件JSON',
`approval_status` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '审批状态',
`current_node` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前节点',
`current_processor` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '当前处理人',
`approved_time` datetime DEFAULT NULL COMMENT '审核通过时间',
`sync_status` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '同步状态 unsynced/synced/failed',
PRIMARY KEY (`id`) USING BTREE,
KEY `idx_project_change_project` (`project_id`) USING BTREE,
KEY `idx_project_change_status` (`approval_status`) USING BTREE,
KEY `idx_project_change_type` (`change_type`) USING BTREE,
KEY `idx_project_change_create_time` (`create_time`) USING BTREE,
KEY `idx_project_change_no` (`change_no`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='项目变更';
-- 菜单:项目管理下增加「项目变更」
INSERT IGNORE INTO `blade_menu` (`id`, `parent_id`, `code`, `name`, `alias`, `path`, `source`, `sort`, `category`, `action`, `is_open`, `component`, `remark`, `is_deleted`) VALUES
(2100000000000000300, 2100000000000000000, 'project_change', '项目变更', 'project_change', '/business/project-change', 'iconfont icon-caidanguanli', 15, 1, 0, 1, NULL, '', 0),
(2100000000000000301, 2100000000000000300, 'project_change_view', '查看', 'project_change_view', '', '', 1, 2, 0, 1, NULL, '', 0),
(2100000000000000302, 2100000000000000300, 'project_change_add', '新增', 'project_change_add', '', '', 2, 2, 0, 1, NULL, '', 0),
(2100000000000000303, 2100000000000000300, 'project_change_edit', '编辑', 'project_change_edit', '', '', 3, 2, 0, 1, NULL, '', 0),
(2100000000000000304, 2100000000000000300, 'project_change_delete', '删除', 'project_change_delete', '', '', 4, 2, 0, 1, NULL, '', 0),
(2100000000000000305, 2100000000000000300, 'project_change_submit', '提交', 'project_change_submit', '', '', 5, 2, 0, 1, NULL, '', 0),
(2100000000000000306, 2100000000000000300, 'project_change_withdraw', '撤回', 'project_change_withdraw', '', '', 6, 2, 0, 1, NULL, '', 0),
(2100000000000000307, 2100000000000000300, 'project_change_approve', '审批通过', 'project_change_approve', '', '', 7, 2, 0, 1, NULL, '', 0),
(2100000000000000308, 2100000000000000300, 'project_change_reject', '审批驳回', 'project_change_reject', '', '', 8, 2, 0, 1, NULL, '', 0);
-- 业务字典 mk_template:提交项目变更审核流(dict_key 需按实际 MK 模板编码配置)
INSERT IGNORE INTO `blade_dict_biz` (`id`, `tenant_id`, `parent_id`, `code`, `dict_key`, `dict_value`, `sort`, `remark`, `is_sealed`, `status`, `is_deleted`)
SELECT 2100000000000000399, '000000', id, 'mk_template', 'PROJECT_CHANGE_TEMPLATE', '提交项目变更审核流', 99, '项目变更MK模板编码,请替换为真实 templateCode', 0, 1, 0
FROM `blade_dict_biz`
WHERE `code` = 'mk_template' AND `parent_id` = 0 AND `is_deleted` = 0
LIMIT 1;