✨ 新增合同变更独立单据与MK审批回写

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-27 18:34:09 +08:00
parent 41a86fe335
commit 4b9e117c9e
11 changed files with 1380 additions and 7 deletions
@@ -0,0 +1,228 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.pojo.entity;
import com.baomidou.mybatisplus.annotation.FieldStrategy;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springblade.core.tenant.mp.TenantEntity;
import java.io.Serial;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
* 合同变更实体类
*
* @author Chill
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("blade_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;
}
@@ -0,0 +1,70 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.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;
}
@@ -0,0 +1,157 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import org.springblade.core.boot.ctrl.BladeController;
import org.springblade.core.mp.support.Condition;
import org.springblade.core.mp.support.Query;
import org.springblade.core.secure.annotation.PreAuth;
import org.springblade.core.tool.api.R;
import org.springblade.transport.pojo.entity.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<ContractChangeVO> 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<IPage<ContractChangeVO>> 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<ContractChangeVO> 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));
}
}
@@ -0,0 +1,34 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.transport.pojo.entity.ContractChange;
/**
* 合同变更 Mapper 接口
*
* @author Chill
*/
public interface ContractChangeMapper extends BaseMapper<ContractChange> {
}
@@ -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
@@ -0,0 +1,75 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.entity.ContractChange;
import org.springblade.transport.pojo.vo.ContractChangeVO;
/**
* 合同变更 服务类
*
* @author Chill
*/
public interface IContractChangeService extends BaseService<ContractChange> {
IPage<ContractChangeVO> selectContractChangePage(IPage<ContractChange> 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);
}
@@ -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<ContractManage> {
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);
@@ -0,0 +1,461 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tenant.annotation.TenantIgnore;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.constant.HcsSyncConstant;
import org.springblade.transport.mapper.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<ContractChangeMapper, ContractChange> 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<ContractChangeVO> selectContractChangePage(IPage<ContractChange> page, ContractChangeVO contractChange) {
IPage<ContractChange> entityPage = page(page, buildQuery(contractChange).orderByDesc(ContractChange::getCreateTime));
IPage<ContractChangeVO> 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<Long> 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<Long> 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<ContractChange> buildQuery(ContractChangeVO contractChange) {
TransportBusinessSupport.validateAllDept(contractChange.getAllDept(), "合同变更");
LambdaQueryWrapper<ContractChange> queryWrapper = Wrappers.<ContractChange>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<String, Object> buildSnapshot(ContractManage contractManage) {
Map<String, Object> 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<String> statuses) {
return getOne(Wrappers.<ContractChange>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));
}
}
@@ -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<ContractManageMap
return updateById(source);
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean applyChangeFromRecord(ContractChange change) {
if (change == null || Func.isEmpty(change.getContractId())) {
throw new ServiceException("合同变更单无效");
}
ContractManage source = loadExists(change.getContractId());
if (!Objects.equals(source.getContractStage(), STAGE_FORMAL)
|| !List.of(STATUS_APPROVED, STATUS_CHANGE_APPROVED).contains(source.getApprovalStatus())) {
throw new ServiceException("当前合同状态不允许回写变更");
}
String changeType = resolveChangeType(change.getChangeType());
boolean settlementRuleOnly = Objects.equals(changeType, CHANGE_TYPE_SETTLEMENT);
String changeReason = TransportBusinessSupport.trimToNull(change.getChangeReason());
TransportBusinessSupport.validateLength(changeReason, 500, "变更原因不能超过500字");
ContractManage request = toContractManageRequest(change);
validateBillingPlans(request.getBillingPlanJson());
Map<String, Object> beforeData = buildContractChangeSnapshot(source);
if (settlementRuleOnly) {
applySettlementRuleChange(source, request);
} else {
applyContractInfoChange(source, request);
}
Map<String, Object> 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.<HcsContractExeStage>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<ContractManageMap
/**
* 结算规则变更:仅允许更新计费信息与结算单规则。
*/
private void applySettlementRuleChange(ContractManage source, ContractManage request) {
void applySettlementRuleChange(ContractManage source, ContractManage request) {
source.setBillingEnabled(request.getBillingEnabled());
source.setFeeGenerationMode(request.getFeeGenerationMode());
source.setBillingPlanJson(request.getBillingPlanJson());
@@ -370,7 +460,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
/**
* 合同信息变更:更新合同基本信息、计费/结算规则、付款比例及附件。
*/
private void applyContractInfoChange(ContractManage source, ContractManage request) {
void applyContractInfoChange(ContractManage source, ContractManage request) {
TransportBusinessSupport.validateRequired(request.getSettlementCurrency(), "请选择结算币种");
ContractManage candidate = new ContractManage();
BeanUtil.copyProperties(source, candidate);
@@ -379,6 +469,7 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
validatePaymentRatios(request.getPaymentRatioJson());
applyContractNo(source, request.getContractNo(), false);
source.setContractName(request.getContractName());
source.setSignType(request.getSignType());
source.setPartyB(request.getPartyB());
source.setStartDate(request.getStartDate());
source.setEndDate(request.getEndDate());
@@ -399,11 +490,6 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
normalizeOptionalIntegerFields(source);
validateContractAmountRequired(source);
validateAdditionalFields(source);
source.setBillingPlanJson(request.getBillingPlanJson()); source.setSettlementRuleJson(request.getSettlementRuleJson()); source.setPreSettlementConfigJson(request.getPreSettlementConfigJson()); source.setFormalSettlementConfigJson(request.getFormalSettlementConfigJson()); source.setPaymentRatioJson(request.getPaymentRatioJson());
source.setContractFileJson(request.getContractFileJson()); source.setAttachmentsJson(request.getAttachmentsJson());
source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(request.getChangeAttachmentsJson()));
// 高风险/黑名单客商管控:变更提交同样必须携带特批附件(与新增/暂存/转正式一致,外部客商,内部组织默认无风险)
validateSpecExplanationOnSubmit(source);
source.setBillingPlanJson(request.getBillingPlanJson());
source.setSettlementRuleJson(request.getSettlementRuleJson());
source.setPreSettlementConfigJson(request.getPreSettlementConfigJson());
@@ -411,6 +497,9 @@ public class ContractManageServiceImpl extends BaseServiceImpl<ContractManageMap
source.setPaymentRatioJson(request.getPaymentRatioJson());
source.setContractFileJson(request.getContractFileJson());
source.setAttachmentsJson(request.getAttachmentsJson());
source.setChangeAttachmentsJson(TransportBusinessSupport.trimToNull(request.getChangeAttachmentsJson()));
// 高风险/黑名单客商管控:变更提交同样必须携带特批附件(与新增/暂存/转正式一致,外部客商,内部组织默认无风险)
validateSpecExplanationOnSubmit(source);
}
@Override
@@ -0,0 +1,80 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
* <p>
* Use of this software is governed by the Commercial License Agreement
* obtained after purchasing a license from BladeX.
* <p>
* 1. This software is for development use only under a valid license
* from BladeX.
* <p>
* 2. Redistribution of this software's source code to any third party
* without a commercial license is strictly prohibited.
* <p>
* 3. Licensees may copyright their own code but cannot use segments
* from this software for such purposes. Copyright of this software
* remains with BladeX.
* <p>
* Using this software signifies agreement to this License, and the software
* must not be used for illegal purposes.
* <p>
* Author: Chill Zhuang (bladejava@qq.com)
*/
package org.springblade.transport.wrapper;
import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.ContractChange;
import org.springblade.transport.pojo.vo.ContractChangeVO;
import java.util.Objects;
/**
* 合同变更包装类
*
* @author Chill
*/
public class ContractChangeWrapper extends BaseEntityWrapper<ContractChange, ContractChangeVO> {
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;
};
}
}
@@ -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;