diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java new file mode 100644 index 00000000..936ec485 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/ContractChange.java @@ -0,0 +1,228 @@ +/** + * 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_contract_change") +@Schema(description = "合同变更") +public class ContractChange extends TenantEntity { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "原合同ID") + private Long contractId; + + @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 changeAttachmentsJson; + + @Schema(description = "变更前快照JSON") + private String beforeDataJson; + + @Schema(description = "合同编号") + private String contractNo; + + @Schema(description = "合同名称") + private String contractName; + + @Schema(description = "所属项目ID") + private Long projectId; + + @Schema(description = "所属项目") + private String projectName; + + @Schema(description = "合同负责部门ID") + private Long organizationId; + + @Schema(description = "合同负责部门") + private String organizationName; + + @Schema(description = "合同类别") + private String contractCategory; + + @Schema(description = "签约类型") + private String signType; + + @Schema(description = "甲方") + private String partyA; + + @Schema(description = "乙方") + private String partyB; + + @Schema(description = "开始日期") + private LocalDate startDate; + + @Schema(description = "结束日期") + private LocalDate endDate; + + @Schema(description = "临时效力起") + private LocalDate temporaryStartDate; + + @Schema(description = "临时效力止") + private LocalDate temporaryEndDate; + + @Schema(description = "经办人ID") + private Long handlerUserId; + + @Schema(description = "经办人") + private String handlerUserName; + + @Schema(description = "签订日期") + private LocalDate signDate; + + @Schema(description = "申请日期") + private LocalDate applyDate; + + @Schema(description = "结算方式") + private String settlementMode; + + @Schema(description = "合同格式") + private String contractFormat; + + @Schema(description = "是否需要加盖法人章") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer legalSealFlag; + + @Schema(description = "一式份数") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer copyCount; + + @Schema(description = "结算币种") + private String settlementCurrency; + + @Schema(description = "开票周期(天)") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer invoiceCycle; + + @Schema(description = "回款账期(天)") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer paymentDays; + + @Schema(description = "合同金额") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private BigDecimal contractAmount; + + @Schema(description = "是否范本") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer templateFlag; + + @Schema(description = "原件合同编号") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private String originalContractNo; + + @Schema(description = "是否电子章") + @TableField(insertStrategy = FieldStrategy.ALWAYS, updateStrategy = FieldStrategy.ALWAYS) + private Integer electronicSealFlag; + + @Schema(description = "计费信息开关") + private Integer billingEnabled; + + @Schema(description = "费用生成模式") + private String feeGenerationMode; + + @Schema(description = "合同主文件JSON") + private String contractFileJson; + + @Schema(description = "其它附件JSON") + private String attachmentsJson; + + @Schema(description = "计费方案JSON") + private String billingPlanJson; + + @Schema(description = "结算生成规则JSON") + private String settlementRuleJson; + + @Schema(description = "预结算配置JSON") + private String preSettlementConfigJson; + + @Schema(description = "正式结算配置JSON") + private String formalSettlementConfigJson; + + @Schema(description = "付款比例设置JSON") + private String paymentRatioJson; + + @Schema(description = "对账配置JSON") + private String reconciliationJson; + + @Schema(description = "合同主要内容") + private String mainContent; + + @Schema(description = "其他事项说明") + private String remark; + + @Schema(description = "合规客商风险等级(展示用)") + private String hcsRiskLevel; + + @Schema(description = "合规客商黑名单状态(展示用)") + private String hcsBlackStatus; + + @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/ContractChangeVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractChangeVO.java new file mode 100644 index 00000000..086d498e --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/ContractChangeVO.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.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.ContractChange; + +import java.io.Serial; + +/** + * 合同变更视图实体类 + * + * @author Chill + */ +@Data +@EqualsAndHashCode(callSuper = true) +@Schema(description = "合同变更") +public class ContractChangeVO extends ContractChange { + + @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; + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractChangeController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractChangeController.java new file mode 100644 index 00000000..89ddf9e0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ContractChangeController.java @@ -0,0 +1,157 @@ +/** + * 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.ContractChange; +import org.springblade.transport.pojo.vo.ContractChangeVO; +import org.springblade.transport.service.IContractChangeService; +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 = "contract_change") +@RequestMapping("/contract-change") +@Tag(name = "合同变更", description = "合同变更") +public class ContractChangeController extends BladeController { + + private final IContractChangeService contractChangeService; + 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(contractChangeService.detail(id)); + } + + @GetMapping("/list") + @ApiOperationSupport(order = 2) + @Operation(summary = "分页", description = "传入contractChange") + public R> list(ContractChangeVO contractChange, Query query) { + return R.data(contractChangeService.selectContractChangePage(Condition.getPage(query), contractChange)); + } + + @PostMapping("/create-from-contract") + @ApiOperationSupport(order = 3) + @Operation(summary = "基于合同创建变更草稿", description = "传入contractId、changeType") + public R createFromContract(@Parameter(description = "合同ID", required = true) @RequestParam Long contractId, + @RequestParam(required = false) String changeType) { + return R.data(contractChangeService.createFromContract(contractId, changeType)); + } + + @PostMapping("/save-draft") + @ApiOperationSupport(order = 4) + @Operation(summary = "保存草稿", description = "传入contractChange") + public R saveDraft(@RequestBody ContractChange contractChange) { + return R.status(contractChangeService.saveDraft(contractChange)); + } + + @PostMapping("/submit") + @ApiOperationSupport(order = 5) + @Operation(summary = "保存并提交审批", description = "传入contractChange") + public R submit(@RequestBody ContractChange contractChange) { + return contractChangeService.submit(contractChange) ? R.data(contractChange) : 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(contractChangeService.submitApproval(id)); + } + + @PostMapping("/approve") + @ApiOperationSupport(order = 7) + @Operation(summary = "审批通过", description = "传入id,通过后回写原合同并推送合规") + public R approve(@Parameter(description = "主键", required = true) @RequestParam Long id) { + Long contractId = contractChangeService.approve(id); + hcsImmediatePushSupport.pushContractAndWait(contractId); + String syncFailure = HcsImmediatePushSupport.pollSyncFailure(); + if (syncFailure != null) { + ContractChange contractChange = contractChangeService.getById(id); + if (contractChange != null) { + contractChange.setSyncStatus("failed"); + contractChangeService.updateById(contractChange); + } + return R.data(Boolean.FALSE, "审批通过,但合规推送失败:" + syncFailure); + } + ContractChange contractChange = contractChangeService.getById(id); + if (contractChange != null) { + contractChange.setSyncStatus("synced"); + contractChangeService.updateById(contractChange); + } + 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(contractChangeService.reject(id)); + } + + @PostMapping("/withdraw") + @ApiOperationSupport(order = 9) + @Operation(summary = "撤回审批", description = "传入id") + public R withdraw(@Parameter(description = "主键", required = true) @RequestParam Long id) { + return R.status(contractChangeService.withdraw(id)); + } + + @PostMapping("/remove") + @ApiOperationSupport(order = 10) + @Operation(summary = "逻辑删除", description = "传入ids") + public R remove(@Parameter(description = "主键集合", required = true) @RequestParam String ids) { + return R.status(contractChangeService.removeDraft(ids)); + } + + @PostMapping("/mark-sync-status") + @ApiOperationSupport(order = 11) + @Operation(summary = "标记同步状态", description = "传入ids和syncStatus") + public R markSyncStatus(@Parameter(description = "主键集合", required = true) @RequestParam String ids, + @Parameter(description = "同步状态", required = true) @RequestParam String syncStatus) { + return R.status(contractChangeService.markSyncStatus(ids, syncStatus)); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ContractChangeMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ContractChangeMapper.java new file mode 100644 index 00000000..5fd4321e --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/ContractChangeMapper.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.ContractChange; + +/** + * 合同变更 Mapper 接口 + * + * @author Chill + */ +public interface ContractChangeMapper 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 482a817e..296dfbfc 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 @@ -10,6 +10,7 @@ import org.springblade.transport.pojo.dto.InvoiceApplicationStatusRequest; import org.springblade.transport.pojo.dto.InvoiceReceiptStatusRequest; import org.springblade.transport.pojo.dto.PaymentApplicationStatusRequest; import org.springblade.transport.pojo.entity.BillPayment; +import org.springblade.transport.pojo.entity.ContractChange; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.FormalSettlement; import org.springblade.transport.pojo.entity.InvoiceApplication; @@ -19,6 +20,7 @@ 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.IContractChangeService; import org.springblade.transport.service.IContractManageService; import org.springblade.transport.service.ICustomerArchiveService; import org.springblade.transport.service.IFormalSettlementService; @@ -224,6 +226,78 @@ public final class MkProcessBizHandlers { } } + @Component + @TenantIgnore + @RequiredArgsConstructor + public static class ContractChangeHandler implements IMkProcessBizHandler { + private final IContractChangeService service; + private final HcsImmediatePushSupport hcsImmediatePushSupport; + + @Override + public String bizType() { + return "contract-change"; + } + + @Override + public Object publicDetail(Long id) { + return service.publicDetail(id); + } + + @Override + public boolean syncFromProcessInfo(Long id, Object processInfo) { + ContractChange 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) { + ContractChange entity = service.getById(id); + if (entity == null || "approved".equals(entity.getApprovalStatus())) { + return entity != null; + } + Long contractId = service.approve(id); + entity = service.getById(id); + if (entity != null) { + entity.setCurrentProcessor(MkProcessNodeHelper.text(processorName, "系统")); + service.updateById(entity); + } + if (contractId != null) { + hcsImmediatePushSupport.pushContractAfterCommit(contractId); + } + return true; + } + + @Override + public boolean rejectFromProcess(Long id, String processorName) { + ContractChange 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/IContractChangeService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractChangeService.java new file mode 100644 index 00000000..8dab1de0 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractChangeService.java @@ -0,0 +1,75 @@ +/** + * 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.ContractChange; +import org.springblade.transport.pojo.vo.ContractChangeVO; + +/** + * 合同变更 服务类 + * + * @author Chill + */ +public interface IContractChangeService extends BaseService { + + IPage selectContractChangePage(IPage page, ContractChangeVO contractChange); + + ContractChangeVO detail(Long id); + + /** + * 公开详情(MK 回调/公开页) + */ + ContractChangeVO publicDetail(Long id); + + /** + * 基于正式合同创建变更草稿 + */ + ContractChangeVO createFromContract(Long contractId, String changeType); + + boolean saveDraft(ContractChange contractChange); + + boolean submit(ContractChange contractChange); + + boolean submitApproval(Long id); + + /** + * 审批通过:回写原合同并标记同步 + * + * @return 原合同ID,供 Controller 推送合规 + */ + Long approve(Long id); + + boolean reject(Long id); + + boolean withdraw(Long id); + + boolean removeDraft(String ids); + + /** + * 批量标记同步状态(用于列表批量同步后回写) + */ + boolean markSyncStatus(String ids, String syncStatus); + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java index 33806995..d4f481b9 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IContractManageService.java @@ -25,6 +25,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; import org.springblade.transport.excel.ContractManageExcel; +import org.springblade.transport.pojo.entity.ContractChange; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.vo.ContractManageVO; @@ -57,6 +58,15 @@ public interface IContractManageService extends BaseService { boolean withdraw(Long id); boolean startChange(Long id, String changeContent, String changeReason); boolean submitChange(ContractManage contractManage); + + /** + * 将独立合同变更单回写到原合同(审批通过后调用,不改写原合同为 change_reviewing) + * + * @param change 合同变更单 + * @return 是否成功 + */ + boolean applyChangeFromRecord(ContractChange change); + boolean updateAttachments(ContractManage contractManage); boolean terminate(Long id, String reason); boolean removeDraft(String ids); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java new file mode 100644 index 00000000..a663c015 --- /dev/null +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractChangeServiceImpl.java @@ -0,0 +1,461 @@ +/** + * 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.ContractChangeMapper; +import org.springblade.transport.pojo.entity.ContractChange; +import org.springblade.transport.pojo.entity.ContractManage; +import org.springblade.transport.pojo.vo.ContractChangeVO; +import org.springblade.transport.pojo.vo.ContractManageVO; +import org.springblade.transport.service.IContractChangeService; +import org.springblade.transport.service.IContractManageService; +import org.springblade.transport.support.TransportBusinessSupport; +import org.springblade.transport.wrapper.ContractChangeWrapper; +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 ContractChangeServiceImpl extends BaseServiceImpl implements IContractChangeService { + + 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 STAGE_FORMAL = "formal"; + private static final String CHANGE_TYPE_INFO = "合同信息变更"; + private static final String CHANGE_TYPE_SETTLEMENT = "结算规则变更"; + private static final DateTimeFormatter CODE_DATE = DateTimeFormatter.ofPattern("yyyyMMdd"); + + private final IContractManageService contractManageService; + + @Override + public IPage selectContractChangePage(IPage page, ContractChangeVO contractChange) { + IPage entityPage = page(page, buildQuery(contractChange).orderByDesc(ContractChange::getCreateTime)); + IPage voPage = ContractChangeWrapper.build().pageVO(entityPage); + voPage.getRecords().forEach(this::fillReadonly); + return voPage; + } + + @Override + public ContractChangeVO detail(Long id) { + ContractChangeVO contractChangeVO = ContractChangeWrapper.build().entityVO(loadExists(id)); + fillReadonly(contractChangeVO); + return contractChangeVO; + } + + @Override + @TenantIgnore + public ContractChangeVO publicDetail(Long id) { + ContractChangeVO contractChangeVO = ContractChangeWrapper.build().entityVO(loadExists(id)); + contractChangeVO.setReadonly(Boolean.TRUE); + return contractChangeVO; + } + + @Override + @Transactional(rollbackFor = Exception.class) + public ContractChangeVO createFromContract(Long contractId, String changeType) { + ContractManageVO contractManage = contractManageService.detail(contractId); + validateContractChangeable(contractManage); + ContractChange reviewing = findLatestByStatuses(contractId, List.of(STATUS_REVIEWING)); + if (reviewing != null) { + throw new ServiceException("该合同已有审批中的变更单,请先完成后再发起"); + } + ContractChange unfinished = findLatestByStatuses(contractId, List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED)); + if (unfinished != null) { + // 已有未完结变更单时复用,避免重复建单 + return detail(unfinished.getId()); + } + String resolvedType = Func.isBlank(changeType) ? CHANGE_TYPE_INFO : changeType.trim(); + if (!List.of(CHANGE_TYPE_INFO, CHANGE_TYPE_SETTLEMENT).contains(resolvedType)) { + throw new ServiceException("请选择变更类型"); + } + ContractChange contractChange = copyFromContract(contractManage); + contractChange.setId(null); + contractChange.setContractId(contractManage.getId()); + contractChange.setChangeNo(nextChangeNo()); + contractChange.setChangeType(resolvedType); + contractChange.setChangeDate(LocalDate.now()); + contractChange.setChangeContent(null); + contractChange.setChangeReason(null); + contractChange.setChangeAttachmentsJson(null); + contractChange.setBeforeDataJson(JsonUtil.toJson(buildSnapshot(contractManage))); + contractChange.setApprovalStatus(STATUS_DRAFT); + contractChange.setCurrentNode("草稿"); + contractChange.setCurrentProcessor(AuthUtil.getUserName()); + contractChange.setApprovedTime(null); + contractChange.setSyncStatus(null); + save(contractChange); + return detail(contractChange.getId()); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean saveDraft(ContractChange contractChange) { + ContractChange oldRecord = loadEditable(contractChange.getId()); + prepareEditableFields(oldRecord, contractChange); + 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(ContractChange contractChange) { + ContractChange oldRecord = loadEditable(contractChange.getId()); + prepareEditableFields(oldRecord, contractChange); + 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) { + ContractChange contractChange = loadEditable(id); + validateSubmit(contractChange); + contractChange.setApprovalStatus(STATUS_REVIEWING); + contractChange.setCurrentNode(resolveApprovalNode(contractChange)); + contractChange.setCurrentProcessor("待处理"); + contractChange.setApprovedTime(null); + return updateById(contractChange); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public Long approve(Long id) { + ContractChange contractChange = loadReviewing(id); + contractChange.setApprovalStatus(STATUS_APPROVED); + contractChange.setCurrentNode("审批通过"); + String processor = TransportBusinessSupport.trimToNull(AuthUtil.getUserName()); + contractChange.setCurrentProcessor(processor == null ? "系统" : processor); + contractChange.setApprovedTime(LocalDateTime.now()); + contractChange.setSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + updateById(contractChange); + contractManageService.applyChangeFromRecord(contractChange); + return contractChange.getContractId(); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean reject(Long id) { + ContractChange contractChange = loadReviewing(id); + contractChange.setApprovalStatus(STATUS_REJECTED); + contractChange.setCurrentNode("审核不通过"); + String processor = TransportBusinessSupport.trimToNull(AuthUtil.getUserName()); + contractChange.setCurrentProcessor(processor == null ? "系统" : processor); + return updateById(contractChange); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean withdraw(Long id) { + ContractChange contractChange = loadReviewing(id); + contractChange.setApprovalStatus(STATUS_WITHDRAWN); + contractChange.setCurrentNode("已撤回"); + contractChange.setCurrentProcessor(AuthUtil.getUserName()); + return updateById(contractChange); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean removeDraft(String ids) { + List idList = Func.toLongList(ids); + if (Func.isEmpty(idList)) { + throw new ServiceException("请选择需要删除的数据"); + } + for (ContractChange contractChange : listByIds(idList)) { + if (!List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED).contains(contractChange.getApprovalStatus())) { + throw new ServiceException("仅草稿、已撤回、已驳回的合同变更允许删除"); + } + } + return deleteLogic(idList); + } + + @Override + @Transactional(rollbackFor = Exception.class) + public boolean markSyncStatus(String ids, String syncStatus) { + List idList = Func.toLongList(ids); + if (Func.isEmpty(idList)) { + throw new ServiceException("请选择需要同步的数据"); + } + if (!List.of(HcsSyncConstant.HCS_STATUS_UNSYNCED, HcsSyncConstant.HCS_STATUS_SYNCED, HcsSyncConstant.HCS_STATUS_FAILED) + .contains(syncStatus)) { + throw new ServiceException("同步状态不合法"); + } + for (ContractChange contractChange : listByIds(idList)) { + if (!Objects.equals(contractChange.getApprovalStatus(), STATUS_APPROVED)) { + throw new ServiceException("仅审批通过的合同变更允许更新同步状态"); + } + contractChange.setSyncStatus(syncStatus); + updateById(contractChange); + } + return true; + } + + private LambdaQueryWrapper buildQuery(ContractChangeVO contractChange) { + TransportBusinessSupport.validateAllDept(contractChange.getAllDept(), "合同变更"); + LambdaQueryWrapper queryWrapper = Wrappers.lambdaQuery().eq(ContractChange::getIsDeleted, 0); + if (TransportBusinessSupport.isAdministrator()) { + if (Objects.equals(contractChange.getAllDept(), 1) && Func.isNotEmpty(contractChange.getOrganizationId())) { + queryWrapper.eq(ContractChange::getOrganizationId, contractChange.getOrganizationId()); + } + } else if (!Objects.equals(contractChange.getAllDept(), 1)) { + Long currentDeptId = TransportBusinessSupport.currentDeptId("合同变更"); + Long currentUserId = AuthUtil.getUserId(); + queryWrapper.and(wrapper -> wrapper.eq(ContractChange::getOrganizationId, currentDeptId) + .or().eq(ContractChange::getCreateUser, currentUserId) + .or().eq(ContractChange::getHandlerUserId, currentUserId)); + } else if (Func.isNotEmpty(contractChange.getOrganizationId())) { + queryWrapper.eq(ContractChange::getOrganizationId, contractChange.getOrganizationId()); + } + queryWrapper.eq(Func.isNotEmpty(contractChange.getProjectId()), ContractChange::getProjectId, contractChange.getProjectId()) + .like(Func.isNotEmpty(contractChange.getContractNo()), ContractChange::getContractNo, contractChange.getContractNo()) + .like(Func.isNotEmpty(contractChange.getContractName()), ContractChange::getContractName, contractChange.getContractName()) + .like(Func.isNotEmpty(contractChange.getProjectName()) && Func.isEmpty(contractChange.getProjectId()), + ContractChange::getProjectName, contractChange.getProjectName()) + .like(Func.isNotEmpty(contractChange.getOrganizationName()), ContractChange::getOrganizationName, contractChange.getOrganizationName()) + .eq(Func.isNotEmpty(contractChange.getContractCategory()), ContractChange::getContractCategory, contractChange.getContractCategory()) + .eq(Func.isNotEmpty(contractChange.getSignType()), ContractChange::getSignType, contractChange.getSignType()) + .eq(Func.isNotEmpty(contractChange.getApprovalStatus()), ContractChange::getApprovalStatus, contractChange.getApprovalStatus()) + .eq(Func.isNotEmpty(contractChange.getChangeType()), ContractChange::getChangeType, contractChange.getChangeType()) + .eq(Func.isNotEmpty(contractChange.getContractId()), ContractChange::getContractId, contractChange.getContractId()) + .like(Func.isNotEmpty(contractChange.getChangeNo()), ContractChange::getChangeNo, contractChange.getChangeNo()); + return queryWrapper; + } + + private ContractChange copyFromContract(ContractManage contractManage) { + ContractChange contractChange = Objects.requireNonNull(BeanUtil.copyProperties(contractManage, ContractChange.class)); + contractChange.setId(null); + contractChange.setCreateUser(null); + contractChange.setCreateDept(null); + contractChange.setCreateTime(null); + contractChange.setUpdateUser(null); + contractChange.setUpdateTime(null); + contractChange.setStatus(1); + contractChange.setIsDeleted(0); + return contractChange; + } + + private void prepareEditableFields(ContractChange target, ContractChange source) { + target.setChangeType(TransportBusinessSupport.trimToNull(source.getChangeType())); + target.setChangeReason(TransportBusinessSupport.trimToNull(source.getChangeReason())); + target.setChangeContent(TransportBusinessSupport.trimToNull(source.getChangeContent())); + target.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(source.getChangeAttachmentsJson())); + target.setSignType(source.getSignType()); + target.setContractNo(source.getContractNo()); + target.setContractName(source.getContractName()); + target.setPartyB(source.getPartyB()); + target.setStartDate(source.getStartDate()); + target.setEndDate(source.getEndDate()); + target.setContractFormat(source.getContractFormat()); + target.setSettlementMode(source.getSettlementMode()); + target.setLegalSealFlag(source.getLegalSealFlag()); + target.setCopyCount(source.getCopyCount()); + target.setSettlementCurrency(source.getSettlementCurrency()); + target.setInvoiceCycle(source.getInvoiceCycle()); + target.setPaymentDays(source.getPaymentDays()); + target.setContractAmount(source.getContractAmount()); + target.setTemplateFlag(source.getTemplateFlag()); + target.setOriginalContractNo(source.getOriginalContractNo()); + target.setElectronicSealFlag(source.getElectronicSealFlag()); + target.setBillingEnabled(source.getBillingEnabled()); + target.setFeeGenerationMode(source.getFeeGenerationMode()); + target.setContractFileJson(source.getContractFileJson()); + target.setAttachmentsJson(source.getAttachmentsJson()); + target.setBillingPlanJson(source.getBillingPlanJson()); + target.setSettlementRuleJson(source.getSettlementRuleJson()); + target.setPreSettlementConfigJson(source.getPreSettlementConfigJson()); + target.setFormalSettlementConfigJson(source.getFormalSettlementConfigJson()); + target.setPaymentRatioJson(source.getPaymentRatioJson()); + target.setReconciliationJson(source.getReconciliationJson()); + target.setMainContent(source.getMainContent()); + target.setRemark(source.getRemark()); + } + + private Map buildSnapshot(ContractManage contractManage) { + Map data = new LinkedHashMap<>(); + data.put("contractNo", contractManage.getContractNo()); + data.put("contractName", contractManage.getContractName()); + data.put("signType", contractManage.getSignType()); + data.put("partyB", contractManage.getPartyB()); + data.put("startDate", contractManage.getStartDate()); + data.put("endDate", contractManage.getEndDate()); + data.put("contractFormat", contractManage.getContractFormat()); + data.put("settlementMode", contractManage.getSettlementMode()); + data.put("legalSealFlag", contractManage.getLegalSealFlag()); + data.put("copyCount", contractManage.getCopyCount()); + data.put("settlementCurrency", contractManage.getSettlementCurrency()); + data.put("invoiceCycle", contractManage.getInvoiceCycle()); + data.put("paymentDays", contractManage.getPaymentDays()); + data.put("contractAmount", contractManage.getContractAmount()); + data.put("templateFlag", contractManage.getTemplateFlag()); + data.put("originalContractNo", contractManage.getOriginalContractNo()); + data.put("electronicSealFlag", contractManage.getElectronicSealFlag()); + data.put("mainContent", contractManage.getMainContent()); + data.put("remark", contractManage.getRemark()); + data.put("feeGenerationMode", contractManage.getFeeGenerationMode()); + data.put("billingPlanJson", contractManage.getBillingPlanJson()); + data.put("settlementRuleJson", contractManage.getSettlementRuleJson()); + data.put("preSettlementConfigJson", contractManage.getPreSettlementConfigJson()); + data.put("formalSettlementConfigJson", contractManage.getFormalSettlementConfigJson()); + data.put("paymentRatioJson", contractManage.getPaymentRatioJson()); + data.put("contractFileJson", contractManage.getContractFileJson()); + data.put("attachmentsJson", contractManage.getAttachmentsJson()); + return data; + } + + private void validateContractChangeable(ContractManage contractManage) { + boolean approved = List.of("approved", "change_approved").contains(contractManage.getApprovalStatus()); + if (!approved || !Objects.equals(contractManage.getContractStage(), STAGE_FORMAL)) { + throw new ServiceException("仅正式合同且审批通过允许发起变更"); + } + } + + private ContractChange findLatestByStatuses(Long contractId, List statuses) { + return getOne(Wrappers.lambdaQuery() + .eq(ContractChange::getIsDeleted, 0) + .eq(ContractChange::getContractId, contractId) + .in(ContractChange::getApprovalStatus, statuses) + .orderByDesc(ContractChange::getCreateTime) + .last("limit 1"), false); + } + + private void validateDraft(ContractChange contractChange) { + TransportBusinessSupport.validateRequired(contractChange.getChangeType(), "请选择变更类型"); + if (!List.of(CHANGE_TYPE_INFO, CHANGE_TYPE_SETTLEMENT).contains(contractChange.getChangeType())) { + throw new ServiceException("请选择变更类型"); + } + TransportBusinessSupport.validateLength(contractChange.getChangeReason(), 500, "变更原因不能超过500个字"); + TransportBusinessSupport.validateLength(contractChange.getChangeContent(), 2000, "变更内容不能超过2000个字"); + } + + private void validateSubmit(ContractChange contractChange) { + validateDraft(contractChange); + TransportBusinessSupport.validateRequired(contractChange.getChangeReason(), "请输入变更原因"); + if (Objects.equals(contractChange.getChangeType(), CHANGE_TYPE_SETTLEMENT)) { + return; + } + TransportBusinessSupport.validateRequired(contractChange.getContractName(), "请输入合同名称"); + TransportBusinessSupport.validateRequired(contractChange.getSettlementCurrency(), "请选择结算币种"); + if (contractChange.getContractAmount() != null && contractChange.getContractAmount().compareTo(BigDecimal.ZERO) < 0) { + throw new ServiceException("合同金额不能小于0"); + } + } + + private String resolveApprovalNode(ContractChange contractChange) { + return Func.isBlank(contractChange.getChangeType()) ? "合同变更审批" : contractChange.getChangeType() + "审批"; + } + + private synchronized String nextChangeNo() { + return "HTBG" + LocalDate.now().format(CODE_DATE) + String.format("%06d", count() + 1); + } + + private ContractChange loadExists(Long id) { + if (Func.isEmpty(id)) { + throw new ServiceException("主键不能为空"); + } + ContractChange contractChange = getById(id); + if (Func.isEmpty(contractChange) || Objects.equals(contractChange.getIsDeleted(), 1)) { + throw new ServiceException("合同变更不存在"); + } + return contractChange; + } + + private ContractChange loadEditable(Long id) { + ContractChange contractChange = loadExists(id); + if (!canCurrentUserOperate(contractChange)) { + throw new ServiceException("无权操作其他组织合同变更"); + } + if (!List.of(STATUS_DRAFT, STATUS_WITHDRAWN, STATUS_REJECTED).contains(contractChange.getApprovalStatus())) { + throw new ServiceException("当前状态不允许编辑"); + } + return contractChange; + } + + private ContractChange loadReviewing(Long id) { + ContractChange contractChange = loadExists(id); + if (!Objects.equals(contractChange.getApprovalStatus(), STATUS_REVIEWING)) { + throw new ServiceException("仅审批中的合同变更允许该操作"); + } + return contractChange; + } + + private boolean canCurrentUserOperate(ContractChange contractChange) { + if (TransportBusinessSupport.isAdministrator()) { + return true; + } + Long currentUserId = AuthUtil.getUserId(); + if (Objects.equals(contractChange.getCreateUser(), currentUserId) + || Objects.equals(contractChange.getHandlerUserId(), currentUserId)) { + return true; + } + Long currentDeptId = TransportBusinessSupport.currentDeptId("合同变更"); + return currentDeptId != null && Objects.equals(contractChange.getOrganizationId(), currentDeptId); + } + + private void fillReadonly(ContractChangeVO contractChangeVO) { + contractChangeVO.setReadonly(!canCurrentUserOperate(contractChangeVO)); + } + +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java index bb9d57d0..3d2ac007 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/ContractManageServiceImpl.java @@ -41,6 +41,7 @@ import org.springblade.transport.constant.HcsSyncConstant; import org.springblade.transport.excel.ContractManageExcel; import org.springblade.transport.mapper.ContractManageMapper; import org.springblade.transport.mapper.HcsContractExeStageMapper; +import org.springblade.transport.pojo.entity.ContractChange; import org.springblade.transport.pojo.entity.ContractManage; import org.springblade.transport.pojo.entity.HcsContractExeStage; import org.springblade.transport.pojo.vo.ContractManageVO; @@ -341,6 +342,95 @@ public class ContractManageServiceImpl extends BaseServiceImpl beforeData = buildContractChangeSnapshot(source); + if (settlementRuleOnly) { + applySettlementRuleChange(source, request); + } else { + applyContractInfoChange(source, request); + } + Map afterData = buildContractChangeSnapshot(source); + retainChangedSnapshotFields(beforeData, afterData); + source.setChangeContent(TransportBusinessSupport.trimToNull(change.getChangeContent())); + source.setChangeReason(changeReason); + source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(change.getChangeAttachmentsJson())); + appendChangeRecord(source, changeType, changeReason, STATUS_CHANGE_APPROVED, "变更审批通过", beforeData, afterData); + source.setAttachmentsJson(mergeAttachmentJson(source.getAttachmentsJson(), source.getChangeAttachmentsJson())); + source.setChangeAttachmentsJson(null); + source.setChangeContent(null); + source.setChangeReason(null); + source.setContractFileJson(markContractFilesApproved(source.getContractFileJson())); + refreshArchiveStatusOnApprove(source); + source.setApprovalStatus(STATUS_CHANGE_APPROVED); + source.setCurrentNode("变更审批通过"); + String processor = TransportBusinessSupport.trimToNull(AuthUtil.getUserName()); + source.setCurrentProcessor(processor == null ? "系统" : processor); + source.setApprovedTime(LocalDateTime.now()); + source.setHcsSyncStatus(HcsSyncConstant.HCS_STATUS_UNSYNCED); + contractExeStageService.createDefaultRowOnApprove(source); + hcsContractExeStageMapper.update(null, Wrappers.lambdaUpdate() + .eq(HcsContractExeStage::getContractId, source.getId()) + .isNull(HcsContractExeStage::getHcsStageFdId) + .set(HcsContractExeStage::getRowOp, "add")); + return updateById(source); + } + + /** + * 将变更单可回写字段映射为临时 ContractManage,供 apply*Change 复用。 + */ + private ContractManage toContractManageRequest(ContractChange change) { + ContractManage request = new ContractManage(); + request.setChangeType(change.getChangeType()); + request.setChangeContent(change.getChangeContent()); + request.setChangeReason(change.getChangeReason()); + request.setChangeAttachmentsJson(change.getChangeAttachmentsJson()); + request.setSignType(change.getSignType()); + request.setContractNo(change.getContractNo()); + request.setContractName(change.getContractName()); + request.setPartyB(change.getPartyB()); + request.setStartDate(change.getStartDate()); + request.setEndDate(change.getEndDate()); + request.setContractFormat(change.getContractFormat()); + request.setSettlementMode(change.getSettlementMode()); + request.setLegalSealFlag(change.getLegalSealFlag()); + request.setCopyCount(change.getCopyCount()); + request.setSettlementCurrency(change.getSettlementCurrency()); + request.setInvoiceCycle(change.getInvoiceCycle()); + request.setPaymentDays(change.getPaymentDays()); + request.setContractAmount(change.getContractAmount()); + request.setTemplateFlag(change.getTemplateFlag()); + request.setOriginalContractNo(change.getOriginalContractNo()); + request.setElectronicSealFlag(change.getElectronicSealFlag()); + request.setRemark(change.getRemark()); + request.setMainContent(change.getMainContent()); + request.setBillingEnabled(change.getBillingEnabled()); + request.setFeeGenerationMode(change.getFeeGenerationMode()); + request.setBillingPlanJson(change.getBillingPlanJson()); + request.setSettlementRuleJson(change.getSettlementRuleJson()); + request.setPreSettlementConfigJson(change.getPreSettlementConfigJson()); + request.setFormalSettlementConfigJson(change.getFormalSettlementConfigJson()); + request.setPaymentRatioJson(change.getPaymentRatioJson()); + request.setContractFileJson(change.getContractFileJson()); + request.setAttachmentsJson(change.getAttachmentsJson()); + return request; + } + /** * 解析并校验合同变更类型,兼容历史未传类型的请求。 */ @@ -358,7 +448,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl + * 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.ContractChange; +import org.springblade.transport.pojo.vo.ContractChangeVO; + +import java.util.Objects; + +/** + * 合同变更包装类 + * + * @author Chill + */ +public class ContractChangeWrapper extends BaseEntityWrapper { + + public static ContractChangeWrapper build() { + return new ContractChangeWrapper(); + } + + @Override + public ContractChangeVO entityVO(ContractChange contractChange) { + ContractChangeVO contractChangeVO = Objects.requireNonNull(BeanUtil.copyProperties(contractChange, ContractChangeVO.class)); + contractChangeVO.setCreateUserName(UserCache.getUserRealName(contractChange.getCreateUser())); + contractChangeVO.setUpdateUserName(UserCache.getUserRealName(contractChange.getUpdateUser())); + contractChangeVO.setApprovalStatusName(statusName(contractChange.getApprovalStatus())); + contractChangeVO.setSyncStatusName(syncStatusName(contractChange.getSyncStatus())); + return contractChangeVO; + } + + 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_contract_change_20261027.sql b/doc/sql/transport/blade_contract_change_20261027.sql new file mode 100644 index 00000000..d897655d --- /dev/null +++ b/doc/sql/transport/blade_contract_change_20261027.sql @@ -0,0 +1,95 @@ +-- 合同变更独立单据 + 菜单 +-- 审批通过后再回写 blade_contract_manage + +CREATE TABLE IF NOT EXISTS `blade_contract_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 '是否已删除', + `contract_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(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '变更原因', + `change_attachments_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '变更附件JSON', + `before_data_json` longtext CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '变更前快照JSON', + `contract_no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同编号', + `contract_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同名称', + `project_id` bigint(20) DEFAULT NULL COMMENT '所属项目ID', + `project_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '所属项目', + `organization_id` bigint(20) DEFAULT NULL COMMENT '合同负责部门ID', + `organization_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同负责部门', + `contract_category` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同类别', + `sign_type` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '签约类型', + `party_a` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '甲方', + `party_b` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '乙方', + `start_date` date DEFAULT NULL COMMENT '开始日期', + `end_date` date DEFAULT NULL COMMENT '结束日期', + `temporary_start_date` date DEFAULT NULL COMMENT '临时效力起', + `temporary_end_date` date 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 '经办人', + `sign_date` date DEFAULT NULL COMMENT '签订日期', + `apply_date` date DEFAULT NULL COMMENT '申请日期', + `settlement_mode` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '结算方式', + `contract_format` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同格式', + `legal_seal_flag` int(11) DEFAULT NULL COMMENT '是否需要加盖法人章', + `copy_count` int(11) DEFAULT NULL COMMENT '一式份数', + `settlement_currency` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '结算币种', + `invoice_cycle` int(11) DEFAULT NULL COMMENT '开票周期(天)', + `payment_days` int(11) DEFAULT NULL COMMENT '回款账期(天)', + `contract_amount` decimal(18,2) DEFAULT NULL COMMENT '合同金额', + `template_flag` int(11) DEFAULT NULL COMMENT '是否范本', + `original_contract_no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '原件合同编号', + `electronic_seal_flag` int(11) DEFAULT NULL COMMENT '是否电子章', + `billing_enabled` int(11) DEFAULT NULL COMMENT '计费信息开关', + `fee_generation_mode` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '费用生成模式', + `contract_file_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '合同主文件JSON', + `attachments_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '其它附件JSON', + `billing_plan_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '计费方案JSON', + `settlement_rule_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '结算生成规则JSON', + `pre_settlement_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '预结算配置JSON', + `formal_settlement_config_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '正式结算配置JSON', + `payment_ratio_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '付款比例设置JSON', + `reconciliation_json` text CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci COMMENT '对账配置JSON', + `main_content` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合同主要内容', + `remark` varchar(2000) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '其他事项说明', + `hcs_risk_level` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规客商风险等级(展示用)', + `hcs_black_status` varchar(8) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '合规客商黑名单状态(展示用)', + `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_contract_change_contract` (`contract_id`) USING BTREE, + KEY `idx_contract_change_status` (`approval_status`) USING BTREE, + KEY `idx_contract_change_type` (`change_type`) USING BTREE, + KEY `idx_contract_change_create_time` (`create_time`) USING BTREE, + KEY `idx_contract_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 +(2110000000000000200, 2110000000000000000, 'contract_change', '合同变更', 'contract_change', '/business/contract-change', 'iconfont icon-caidanguanli', 20, 1, 0, 1, NULL, '', 0), +(2110000000000000201, 2110000000000000200, 'contract_change_view', '查看', 'contract_change_view', '', '', 1, 2, 0, 1, NULL, '', 0), +(2110000000000000202, 2110000000000000200, 'contract_change_add', '新增', 'contract_change_add', '', '', 2, 2, 0, 1, NULL, '', 0), +(2110000000000000203, 2110000000000000200, 'contract_change_edit', '编辑', 'contract_change_edit', '', '', 3, 2, 0, 1, NULL, '', 0), +(2110000000000000204, 2110000000000000200, 'contract_change_delete', '删除', 'contract_change_delete', '', '', 4, 2, 0, 1, NULL, '', 0), +(2110000000000000205, 2110000000000000200, 'contract_change_submit', '提交', 'contract_change_submit', '', '', 5, 2, 0, 1, NULL, '', 0), +(2110000000000000206, 2110000000000000200, 'contract_change_withdraw', '撤回', 'contract_change_withdraw', '', '', 6, 2, 0, 1, NULL, '', 0), +(2110000000000000207, 2110000000000000200, 'contract_change_approve', '审批通过', 'contract_change_approve', '', '', 7, 2, 0, 1, NULL, '', 0), +(2110000000000000208, 2110000000000000200, 'contract_change_reject', '审批驳回', 'contract_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 2110000000000000299, '000000', id, 'mk_template', 'CONTRACT_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;