diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectChange.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectChange.java new file mode 100644 index 00000000..ad3776a1 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ProjectChange.java @@ -0,0 +1,206 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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; + +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectChangeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectChangeVO.java new file mode 100644 index 00000000..efe5a566 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ProjectChangeVO.java @@ -0,0 +1,84 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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> cargoTypeOptions; + + @TableField(exist = false) + @Schema(description = "运输类型字典,公开页回显使用") + private List> transportTypeOptions; + + @TableField(exist = false) + @Schema(description = "结算方式字典,公开页回显使用") + private List> settlementModeOptions; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectChangeController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectChangeController.java new file mode 100644 index 00000000..c697286c --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProjectChangeController.java @@ -0,0 +1,149 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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 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> 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 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)); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ProjectChangeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ProjectChangeMapper.java new file mode 100644 index 00000000..d9687ac6 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ProjectChangeMapper.java @@ -0,0 +1,34 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import org.springblade.transport.pojo.entity.ProjectChange; + +/** + * 项目变更 Mapper 接口 + * + * @author Chill + */ +public interface ProjectChangeMapper extends BaseMapper { +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java index 668d232a..482a817e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mk/MkProcessBizHandlers.java @@ -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 diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java index 601e20d0..bf977a69 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectApplyService.java @@ -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 { 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 exportProjectApply(ProjectApplyVO projectApply, String ids); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectChangeService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectChangeService.java new file mode 100644 index 00000000..1cbafa62 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IProjectChangeService.java @@ -0,0 +1,70 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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 { + + IPage selectProjectChangePage(IPage 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); + +} 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 cf107fbd..c8427238 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 @@ -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 beforeData = buildProjectChangeSnapshot(projectApply); + copyChangeFields(projectApply, toProjectApplySnapshot(change)); + Map 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) { diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectChangeServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectChangeServiceImpl.java new file mode 100644 index 00000000..313573b9 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ProjectChangeServiceImpl.java @@ -0,0 +1,517 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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 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 selectProjectChangePage(IPage page, ProjectChangeVO projectChange) { + IPage entityPage = page(page, buildQuery(projectChange).orderByDesc(ProjectChange::getCreateTime)); + IPage 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 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 buildQuery(ProjectChangeVO projectChange) { + TransportBusinessSupport.validateAllDept(projectChange.getAllDept(), "项目变更"); + LambdaQueryWrapper queryWrapper = Wrappers.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 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 buildSnapshot(ProjectApply projectApply) { + Map 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 statuses) { + return getOne(Wrappers.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)); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ProjectChangeWrapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ProjectChangeWrapper.java new file mode 100644 index 00000000..aec00665 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/wrapper/ProjectChangeWrapper.java @@ -0,0 +1,80 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + *

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

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

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

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

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

+ * Author: Chill Zhuang (bladejava@qq.com) + */ +package org.springblade.transport.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 { + + 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; + }; + } + +} diff --git a/doc/sql/transport/blade_project_change_20261027.sql b/doc/sql/transport/blade_project_change_20261027.sql new file mode 100644 index 00000000..10a89675 --- /dev/null +++ b/doc/sql/transport/blade_project_change_20261027.sql @@ -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;