From a4c9c4c91913dfd9f150fd53fb4f988ca8345a1e Mon Sep 17 00:00:00 2001 From: zhouxiaofeng <2946471396@qq.com> Date: Fri, 29 May 2026 15:25:25 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/nbport/zgwl/ZgwlEndApplication.java | 3 +- .../controller/ContractLaunchController.java | 268 +++++ .../dto/ContractLaunchApproveDto.java | 34 + .../dto/ContractLaunchArchiveDto.java | 30 + .../contractlaunch/dto/ContractLaunchDto.java | 359 +++++++ .../dto/ContractLaunchFileDto.java | 43 + .../dto/ContractLaunchLogDto.java | 52 + .../dto/ContractLaunchPartyDto.java | 88 ++ .../dto/ContractLaunchPlanDto.java | 62 ++ .../dto/ContractLaunchQueryDto.java | 37 + .../dto/ContractLaunchSealDto.java | 40 + .../mapper/ContractLaunchMapper.java | 96 ++ .../service/ContractLaunchService.java | 52 + .../service/ContractLaunchServiceImpl.java | 937 ++++++++++++++++++ .../utils/ContractLaunchCodeUtils.java | 42 + .../utils/ContractLaunchConstants.java | 51 + .../com/nbport/zgwl/entity/SysOrgEntity.java | 25 + .../com/nbport/zgwl/entity/SysUserEntity.java | 8 + .../manifest/utils/ManifestCodeUtils.java | 32 + .../zgwl/utils/AdminSecurityManage.java | 30 + .../contractlaunch/ContractLaunchMapper.xml | 532 ++++++++++ .../manifest/ContractTemplateMapper.xml | 4 +- .../mapper/partner/PartnerManageMapper.xml | 9 +- .../2a478b9d74b3485ab696ef172cb5db3e.docx | Bin 0 -> 10324 bytes .../56ffed2147d943adae16d1c966d1a48f.docx | Bin 0 -> 10324 bytes .../97a2c5ece28b48f9843d1ae27656a09f.docx | Bin 0 -> 10324 bytes zgwl_contract_launch_field_patch_oracle.sql | 33 + zgwl_contract_launch_oracle.sql | 455 +++++++++ 28 files changed, 3318 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/controller/ContractLaunchController.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchApproveDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchArchiveDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchFileDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchLogDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPartyDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPlanDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchQueryDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/mapper/ContractLaunchMapper.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchService.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchCodeUtils.java create mode 100644 src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchConstants.java create mode 100644 src/main/java/com/nbport/zgwl/entity/SysOrgEntity.java create mode 100644 src/main/java/com/nbport/zgwl/utils/AdminSecurityManage.java create mode 100644 src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml create mode 100644 upload-files/20260529/2a478b9d74b3485ab696ef172cb5db3e.docx create mode 100644 upload-files/20260529/56ffed2147d943adae16d1c966d1a48f.docx create mode 100644 upload-files/20260529/97a2c5ece28b48f9843d1ae27656a09f.docx create mode 100644 zgwl_contract_launch_field_patch_oracle.sql create mode 100644 zgwl_contract_launch_oracle.sql diff --git a/src/main/java/com/nbport/zgwl/ZgwlEndApplication.java b/src/main/java/com/nbport/zgwl/ZgwlEndApplication.java index d1efab7..3a8e11c 100644 --- a/src/main/java/com/nbport/zgwl/ZgwlEndApplication.java +++ b/src/main/java/com/nbport/zgwl/ZgwlEndApplication.java @@ -7,7 +7,8 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; @MapperScan({ "com.nbport.zgwl.mapper", "com.nbport.zgwl.partner.mapper", - "com.nbport.zgwl.manifest.mapper" + "com.nbport.zgwl.manifest.mapper", + "com.nbport.zgwl.contractlaunch.mapper" }) @SpringBootApplication public class ZgwlEndApplication { diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/controller/ContractLaunchController.java b/src/main/java/com/nbport/zgwl/contractlaunch/controller/ContractLaunchController.java new file mode 100644 index 0000000..c5d0b5e --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/controller/ContractLaunchController.java @@ -0,0 +1,268 @@ +package com.nbport.zgwl.contractlaunch.controller; + +import com.nbport.zgwl.common.PageResult; +import com.nbport.zgwl.common.R; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto; +import com.nbport.zgwl.contractlaunch.service.ContractLaunchService; +import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.DeleteMapping; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.PutMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +/** + * 合同发起 Controller + * + * 这里除了正常 CRUD,还额外提供了几类“测试按钮”接口: + * 1. 模拟法务审批通过 / 驳回 + * 2. 模拟合同采集 + * 3. 合同归档 + * 4. 回写签章结果 + * + * 这些测试接口在法务系统不可用期间便于你完整演示流程, + * 后续接入真实系统后,保留或删除都很方便。 + */ +@RestController +@RequiredArgsConstructor +@RequestMapping(ContractLaunchConstants.REQUEST_PREFIX) +public class ContractLaunchController { + + private final ContractLaunchService contractLaunchService; + + /** + * 分页查询合同列表。 + * + * 支持按合同名称、合同编号、合同类型、状态、创建时间范围过滤。 + * 返回结果附带合同方摘要、文件列表、履行计划(不含日志,列表页日志量大)。 + * + * 对应前端功能:合同列表页 + * + * 请求体 ContractLaunchQueryDto 字段: + * contractName - 合同名称(模糊) + * contractCode - 合同编号 + * contractType - 合同类型 + * status - 状态(draft/reviewing/rejected/pending_sign/signing/...) + * dateStart - 创建时间范围-开始(yyyy-MM-dd) + * dateEnd - 创建时间范围-结束(yyyy-MM-dd) + */ + @PostMapping("/list") + public R> queryList(@RequestBody ContractLaunchQueryDto queryDto) { + return R.success(contractLaunchService.queryPage(queryDto)); + } + + /** + * 查询合同详情(含所有子表数据)。 + * + * 返回合同基本信息 + 合同方列表 + 文件列表 + 履行计划 + 操作日志。 + * + * 对应前端功能:合同详情页 / 编辑页面回显 + */ + @GetMapping("/{id}") + public R getDetail(@PathVariable("id") Long id) { + return R.success(contractLaunchService.getDetail(id)); + } + + /** + * 创建合同草稿。 + * + * 流程:生成 contractSysId(CLS-xxx)和 contractCode → 保存主表 → 保存子表(合同方/文件/履行计划)→ 记录操作日志 + * 状态变为:draft(草稿) + * + * 必填校验项:合同名称、合同类型、合同类型编码、计价方式、币种、签订方式、合同期限类型、至少一个甲方+乙方 + * + * 对应前端功能:新建合同 → 保存草稿 + */ + @PostMapping + public R createDraft(@RequestBody ContractLaunchDto contract) { + return R.success(contractLaunchService.createDraft(contract)); + } + + /** + * 编辑合同草稿。 + * + * 仅 draft(草稿)或 rejected(已驳回)状态允许编辑。 + * 逻辑:先删子表旧数据,再以当前数据重新插入(替换式更新)。 + * 保留原有的 contractSysId、contractCode、创建人/创建时间 不变。 + * + * 对应前端功能:编辑草稿 → 保存 + */ + @PutMapping + public R updateDraft(@RequestBody ContractLaunchDto contract) { + return R.success(contractLaunchService.updateDraft(contract)); + } + + /** + * 提交法务审批。 + * + * 状态流转:draft/rejected → reviewing + * 调用结算平台 pushContract 接口将合同数据推送给法务系统开始审批流程。 + * 目前法务接口不可用,仅做状态变更 + 记录日志(TODO:联调时打开 settlementPushService.pushContract 调用)。 + * + * 推送法务的报文包含:合同基本字段 + contractOpposites(相对方) + contractKxPlans(履行计划) + 附件等 + * + * 对应前端功能:提交审批按钮 + */ + @PostMapping("/{id}/submit") + public R submitToLaw(@PathVariable("id") Long id) { + return R.success(contractLaunchService.submitToLaw(id)); + } + + /** + * 【测试按钮】模拟法务审批通过。 + * + * 状态流转:reviewing → pending_sign(待签署) + * 生成模拟的 lawContractCode 和 lawContractId 记录到合同上,以便后续流程使用。 + * 可在请求体中传入 approveMessage(审批意见)、lawContractCode、lawContractId 覆盖自动生成的值。 + * + * 真实场景下,此方法应由法务系统的回调接口替代(见 lawApproveCallback)。 + * + * 对应前端功能:测试按钮 → 模拟审批通过 + */ + @PostMapping("/{id}/mock-approve") + public R mockApprove(@PathVariable("id") Long id, + @RequestBody(required = false) ContractLaunchApproveDto approveDto) { + return R.success(contractLaunchService.mockApprove(id, approveDto)); + } + + /** + * 【测试按钮】模拟法务审批驳回。 + * + * 状态流转:reviewing → rejected(已驳回) + * 可在请求体中传入 approveMessage(驳回原因)。 + * 驳回后合同可再次编辑和提交。 + * + * 对应前端功能:测试按钮 → 模拟驳回 + */ + @PostMapping("/{id}/mock-reject") + public R mockReject(@PathVariable("id") Long id, + @RequestBody(required = false) ContractLaunchApproveDto approveDto) { + return R.success(contractLaunchService.mockReject(id, approveDto)); + } + + /** + * 法务审批结果回调入口(供法务系统调用)。 + * + * submitToLaw 时推给法务的 pushUrl 默认指向此接口:POST /contract/launch/callback/law/approve + * 法务审批完成后,会往 pushUrl POST 审批结果。 + * + * 当前法务接口不可用,但此回调接口已预先实现,可接受测试请求验证流程。 + * 请求体使用 Map 接收以兼容不同版本的字段名,内部通过 getFirstText 按多个 key 依次尝试提取。 + * + * 识别合同方式:从报文中取 contractSysId 定位到本系统的合同 + * 判断通过/驳回:从 approveStatus/status/result 等多个字段名中取值判断 + * + * 状态流转:reviewing → pending_sign(通过) / rejected(驳回) + * + * 对应前端功能:法务系统审批完成后自动回调(目前用法务系统暂未对接) + */ + @PostMapping("/callback/law/approve") + public R lawApproveCallback(@RequestBody Map callbackPayload) { + return R.success(contractLaunchService.handleLawApproveCallback(callbackPayload)); + } + + /** + * 签署发起回调(浙江CA安全签调用)。 + * + * 在浙江CA安全签平台创建签署合同后,回写 sealContractId 到本系统合同。 + * 状态流转:pending_sign → signing(签署中) + * + * 请求体 ContractLaunchSealDto 字段: + * sealContractId - 签章平台生成的合同ID(必填) + * sealContractStatus - 签署状态(1:签署中) + * + * 对应前端功能:调起浙江CA签署后,回写签章合同ID + */ + @PostMapping("/{id}/seal/save") + public R saveSealInfo(@PathVariable("id") Long id, + @RequestBody ContractLaunchSealDto sealDto) { + return R.success(contractLaunchService.saveSealInfo(id, sealDto)); + } + + /** + * 签署完成回调(浙江CA安全签/线下签署完成后调用)。 + * + * 所有签署方完成签署后,将签署完成的合同文件信息回写。 + * 状态流转:signing → pending_collect(待采集) + * + * 请求体 ContractLaunchSealDto 字段: + * sealContractStatus - 签署状态(2:签署完成) + * signedFileName - 签署完成后的文件名 + * signedFilePath - 签署文件存储路径 + * signedFileType - 文件类型 + * signedFileSize - 文件大小 + * signedOfflineReason - 线下签署原因(如果未在线签署) + * + * 签署文件会自动保存到 file 子表(fileCategory = "signed"),先删旧签署文件再插入新的。 + * + * 对应前端功能:签署完成 → 回写签署结果 + */ + @PostMapping("/{id}/seal/finish") + public R finishSeal(@PathVariable("id") Long id, + @RequestBody ContractLaunchSealDto sealDto) { + return R.success(contractLaunchService.markSealCompleted(id, sealDto)); + } + + /** + * 【测试按钮】模拟合同采集。 + * + * 合同签署完成后,调用数据采集 V3.0 的 collectContract 接口将合同信息推送到法务系统存档。 + * 目前法务接口不可用,仅做状态变更 + 记录日志(TODO:联调时打开 dataCollectionPushService.collectContract 调用)。 + * + * 状态流转:pending_collect → pending_archive(待归档) + * + * 推送数据采集的报文包含:合同基本字段 + 用印信息(c_seal)+ 签订信息(c_sign)+ 附件等 + * + * 对应前端功能:测试按钮 → 模拟采集 + */ + @PostMapping("/{id}/mock-collect") + public R mockCollect(@PathVariable("id") Long id) { + return R.success(contractLaunchService.mockCollect(id)); + } + + /** + * 合同归档。 + * + * 合同全生命周期最后一步,状态变为 completed(已完成)。 + * 同时将归档信息推送到法务系统存档(TODO:联调时打开 settlementPushService.pushArchive 调用)。 + * + * 请求体 ContractLaunchArchiveDto 字段: + * archiveKeeper - 档案保管人手机号 + * archiveKeeperUnit - 档案保管单位名称 + * archiveKeeperUnitCode - 档案保管单位编码 + * archiveFiles - 归档附件列表 + * + * 归档附件自动保存到 file 子表(fileCategory = "archive")。 + * + * 对应前端功能:归档按钮 → 合同完结 + */ + @PostMapping("/{id}/archive") + public R archive(@PathVariable("id") Long id, + @RequestBody(required = false) ContractLaunchArchiveDto archiveDto) { + return R.success(contractLaunchService.archive(id, archiveDto)); + } + + /** + * 删除合同草稿(逻辑删除)。 + * + * 仅 draft(草稿)或 rejected(已驳回)状态允许删除。 + * 逻辑:设置 isDeleted = 'Y',不物理删除数据。 + * + * 对应前端功能:删除草稿按钮 + */ + @DeleteMapping("/{id}") + public R deleteDraft(@PathVariable("id") Long id) { + contractLaunchService.deleteDraft(id); + return R.success("删除成功"); + } +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchApproveDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchApproveDto.java new file mode 100644 index 0000000..0d800cf --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchApproveDto.java @@ -0,0 +1,34 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 模拟审批 / 驳回参数 + * + * 当前法务接口不可用,通过「测试按钮」手动审批时携带此参数。 + * 真实联调后,此 DTO 可由法务回调接口的报文替代。 + */ +@Data +public class ContractLaunchApproveDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 审批意见 */ + private String approveMessage; + + /** + * 法务回调返回的合同编号。 + * 模拟时自动生成,真实联调时由法务系统返回。 + */ + private String lawContractCode; + + /** + * 法务回调返回的合同ID。 + * 模拟时自动生成,真实联调时由法务系统返回。 + */ + private String lawContractId; +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchArchiveDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchArchiveDto.java new file mode 100644 index 0000000..ca7d96a --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchArchiveDto.java @@ -0,0 +1,30 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.ArrayList; +import java.util.List; + +/** + * 合同归档参数 + */ +@Data +public class ContractLaunchArchiveDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 档案保管人手机号 */ + private String archiveKeeper; + + /** 档案保管单位名称 */ + private String archiveKeeperUnit; + + /** 档案保管单位编码 */ + private String archiveKeeperUnitCode; + + /** 归档附件列表(纸质或电子档案材料) */ + private List archiveFiles = new ArrayList<>(); +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java new file mode 100644 index 0000000..55a3999 --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchDto.java @@ -0,0 +1,359 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; + +/** + * 合同发起主对象 + * + * 这里尽量把前后端会直接用到的字段都放在一个 DTO 里, + * 方便你后续排查和修改,不做过深的分层。 + */ +@Data +public class ContractLaunchDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + // ==================== 主键与编号 ==================== + + /** 主键ID */ + private Long id; + + /** 合同系统唯一编码(本系统生成,格式 CLS-时间戳-随机数) */ + private String contractSysId; + + /** 业务合同编号(如 MM-20260522-1234,前端可手动输入,不传则自动生成) */ + private String contractCode; + + /** 法务系统生成的合同编号(审批通过后法务返回) */ + private String lawContractCode; + + /** 法务系统返回的合同ID(审批通过后法务返回) */ + private String lawContractId; + + // ==================== 合同基本信息 ==================== + + /** 合同名称 */ + private String contractName; + + /** 合同类型中文名(如 买卖合同、服务合同) */ + private String contractType; + + /** 合同类型编码(如 MM/GY/ZL,见文档附录字典) */ + private String contractTypeCode; + + /** 合同来源(ECP:结算平台, DSSP:大宗平台) */ + private String contractSource; + + /** 创建方式(template:从模板创建, manual:手动创建) */ + private String creationMode; + + /** 选择的模板ID */ + private Long templateId; + + /** 选择的模板编号 */ + private String templateCode; + + /** 选择的模板名称 */ + private String templateName; + + // ==================== 流程状态 ==================== + + /** + * 合同状态流转: + * draft → reviewing → (approved→pending_sign / rejected) + * pending_sign → signing → pending_collect → pending_archive → completed + */ + private String status; + + /** 审批状态(pending:待审批, approved:已通过, rejected:已驳回) */ + private String approveStatus; + + /** 审批意见/消息 */ + private String approveMessage; + + // ==================== 签章状态 ==================== + + /** 签章平台合同ID(浙江CA安全签创建后返回) */ + private String sealContractId; + + /** 签章状态(1:签署中, 2:签署完成, 3:签署失败, null:未发起) */ + private Integer sealContractStatus; + + // ==================== 金额与币种 ==================== + + /** 合同标的额(元) */ + private Double contractAmount; + + /** 币种(RMB/USD/HKD/EUR/IDR) */ + private String currencyName; + + /** 合同期限类型(0:固定期限, 1:无固定期限) */ + private String contractPeriodType; + + /** 期限说明(无固定期限时必填) */ + private String periodExplain; + + /** 合同开始日期 */ + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private LocalDateTime effectiveStart; + + /** 合同结束日期 */ + @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8") + private LocalDateTime effectiveEnd; + + /** 付款方式 */ + private String paymentMethod; + + /** 合同主要内容 */ + private String primaryContent; + + /** 金额说明(计价方式为无金额时填写) */ + private String amountExplain; + + /** 计价方式(0:无金额, 1:固定总价, 2:预估总价, 3:无法预估总价, 4:固定单价) */ + private String valuationMode; + + // ==================== 签署相关 ==================== + + /** 签订方式(GZ:公章, HTZYZ:合同专用章, 等) */ + private String sealType; + + /** 文本来源(standardText:推荐文本, exampleText:示范文本, draftBySelf:自行起草, otherText:对方文本) */ + private String textSource; + + /** 授权方式(当签订方式为"公章/合同专用章+被授权人签字"时必填) */ + private String mandateType; + + /** 货类 */ + private String cargoType; + + /** 协助部门名称 */ + private String assistDepartmentName; + + /** 执行人账号(手机号) */ + private String executorAccount; + + /** 创建人手机号 */ + private String creatorPhone; + + /** 二级单位合同编码 */ + private String selfCode; + + /** 补录主合同编码(用于关联主合同,多个用逗号分割) */ + private String mainContractCode; + + /** + * 签约主体编码,推送法务字段 signingSubjectCode。 + * 与 orgId 分开保存,避免被当前登录人的组织ID覆盖。 + */ + private String signingSubjectCode; + + /** + * 固定期间日期类型,推送法务字段 dateType:0-年,1-月,2-日 + */ + private String dateType; + + // ==================== 业务标识 ==================== + + /** 是否补录合同(1:补录, 2:非补录) */ + private Integer isAddit; + + /** 签署方式(0:纸质合同, 1:电子合同,默认1) */ + private String isElectron; + + /** 收支方向(1:付款, 2:收款, 3:收支双向, 0:无) */ + private String paymentDirection; + + /** + * 回调推送地址,法务审批完成后回调本系统使用。 + * 真实地址后续可以由配置文件统一生成。 + */ + private String pushUrl; + + /** 是否重大合同(1:是, 0:否) */ + private Integer isMajorContract; + + /** 是否公开招标(1:是, 0:否) */ + private Integer isOpenBidding; + + /** 是否列入当年预算(1:是, 0:否) */ + private Integer isYearBudget; + + /** 是否共享(1:是, 0:否) */ + private String isShare; + + /** 签约主体是否涉外(1:是, 0:否) */ + private String contractNature; + + /** 签约主体是否涉港澳台(1:是, 0:否) */ + private String isSgat; + + /** 是否正常 */ + private String isNormal; + + /** 是否终止 */ + private String isTerminated; + + /** 是否单方协议(Y/N) */ + private String isSingleAgreement; + + /** 招标编号 */ + private String biddingNo; + + /** 公开招标说明 */ + private String openBiddingRemark; + + /** 签订依据说明 */ + private String contractGistRemark; + + /** + * 额外金额类型,多个用逗号拼接:DJ-定金,YJ-押金,YFK-预付款,BZJ-保证金 + */ + private String ewAmountType; + + /** 定金金额 */ + private Double djAmount; + + /** 押金金额 */ + private Double yjAmount; + + /** 预付款金额 */ + private Double yfkAmount; + + /** 保证金金额 */ + private Double bzjAmount; + + /** 正文/摘要 */ + private String bodySummary; + + // ==================== 文件信息 ==================== + + /** 合同正文文件名 */ + private String contractTextFileName; + + /** 合同正文文件路径 */ + private String contractTextFilePath; + + /** 合同正文文件类型 */ + private String contractTextFileType; + + /** 合同正文文件大小(字节) */ + private Long contractTextFileSize; + + // ==================== 签署完成信息 ==================== + + /** 签署完成后的合同文件名 */ + private String signedFileName; + + /** 签署完成后的合同文件路径 */ + private String signedFilePath; + + /** 签署完成后的合同文件类型 */ + private String signedFileType; + + /** 签署完成后的合同文件大小(字节) */ + private Long signedFileSize; + + /** 未在线签署的原因(线下签署时填写) */ + private String signedOfflineReason; + + // ==================== 签署与归档时间 ==================== + + /** 签署日期 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime signDate; + + /** 合同生效日期 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime activeDate; + + /** 归档日期 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime archiveDate; + + /** 档案保管人手机号 */ + private String archiveKeeper; + + /** 档案保管单位 */ + private String archiveKeeperUnit; + + /** 档案保管单位编码 */ + private String archiveKeeperUnitCode; + + // ==================== 业务扩展字段(财务提醒) ==================== + + /** 开票时间(用于账单开票提醒,提前3天提醒) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime invoiceDate; + + /** 收款时间(用于应收账款提醒,提前3天提醒) */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime receiveDate; + + /** 提前提醒天数 */ + private Integer remindDays; + + // ==================== 创建信息 ==================== + + /** 发起人 */ + private String initiator; + + /** 发起人部门 */ + private String initiatorDept; + + /** 公司名称(当前登录人所在公司) */ + private String companyName; + + /** 公司/组织编码 */ + private String orgId; + + /** 查询范围(personal:个人, company:公司, admin:管理员) */ + private String queryScope; + + /** 创建人 */ + private String createBy; + + /** 创建时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime createTime; + + /** 更新人 */ + private String updateBy; + + /** 更新时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime updateTime; + + /** 删除标记(N:未删除, Y:已删除) */ + private String isDeleted; + + /** 备注 */ + private String remark; + + // ==================== 子表数据(非数据库字段) ==================== + + /** + * 前端表格直接展示用的相对方摘要 + */ + private String counterpartySummary; + + /** 合同方列表(甲方/乙方/丙方等) */ + private List partyList = new ArrayList<>(); + + /** 附件文件列表(正文/签订依据/其他附件/签署文件/归档文件) */ + private List fileList = new ArrayList<>(); + + /** 履行计划列表 */ + private List performancePlans = new ArrayList<>(); + + /** 操作日志列表 */ + private List logList = new ArrayList<>(); +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchFileDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchFileDto.java new file mode 100644 index 0000000..145709b --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchFileDto.java @@ -0,0 +1,43 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 合同文件信息 + */ +@Data +public class ContractLaunchFileDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + private Long id; + + /** 关联合同主表 ID */ + private Long contractId; + + /** 文件分类:body-正文, gist-签订依据, other-其他附件, signed-签署文件, archive-归档文件 */ + private String fileCategory; + + /** 文件名 */ + private String fileName; + + /** 文件路径(上传到法务系统后返回的 path,或本系统存储路径) */ + private String filePath; + + /** 文件类型(docx/pdf 等) */ + private String fileType; + + /** 文件大小(字节) */ + private Long fileSize; + + /** 排序号 */ + private Integer sortOrder; + + /** 备注 */ + private String remark; +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchLogDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchLogDto.java new file mode 100644 index 0000000..dbc4bdc --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchLogDto.java @@ -0,0 +1,52 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 合同操作日志 + */ +@Data +public class ContractLaunchLogDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + private Long id; + + /** 关联合同主表 ID */ + private Long contractId; + + /** 操作类型(save/submit/approve/reject/seal_start/seal_finish/collect/archive/delete) */ + private String actionType; + + /** 操作名称(中文描述,如"保存草稿"、"提交法务") */ + private String actionName; + + /** 操作状态(success/failed/pending) */ + private String actionStatus; + + /** 操作人姓名 */ + private String operatorName; + + /** 操作人手机号 */ + private String operatorPhone; + + /** 操作时间 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime actionTime; + + /** 操作内容描述 */ + private String actionContent; + + /** 回调/操作的原始报文(JSON 文本存储,用于追溯) */ + private String callbackPayload; + + /** 备注 */ + private String remark; +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPartyDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPartyDto.java new file mode 100644 index 0000000..dc80865 --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPartyDto.java @@ -0,0 +1,88 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 合同方信息 + */ +@Data +public class ContractLaunchPartyDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + private Long id; + + /** 关联合同主表 ID */ + private Long contractId; + + /** 角色编码(A:甲方/我方, B:乙方/相对方, C:丙方 等) */ + private String roleCode; + + /** 角色名称(甲方/乙方/丙方) */ + private String roleName; + + /** 排序号 */ + private Integer sortOrder; + + /** 关联相对方主表 ID(partner 模块的 customerSysid) */ + private Long partnerId; + + /** 法务系统的相对方ID(jtId,采集后返回) */ + private String oppositeId; + + /** 公司全称 */ + private String companyName; + + /** 公司类型(client/supplier/Partner/clientAndSup/other) */ + private String companyType; + + /** 相对方性质(1:境内组织,2:境内个人,3:境外组织,4:境外个人) */ + private String oppositeCharacter; + + /** 联系人姓名 */ + private String contactName; + + /** 联系人电话 */ + private String contactPhone; + + /** 联系人邮箱 */ + private String contactEmail; + + /** 联系地址 */ + private String contactAddress; + + /** 注册地址 */ + private String registeredAddress; + + /** 法定代表人 */ + private String legalRepresentative; + + /** 统一社会信用代码 */ + private String creditCode; + + /** 开户银行 */ + private String bankName; + + /** 银行账户 */ + private String bankAccount; + + /** 自然人证件类型 */ + private String naturalPersonIdType; + + /** 自然人证件号码 */ + private String naturalPersonIdNumber; + + /** 是否集团内(Y/N) */ + private String isGroupInternal; + + /** 是否涉港澳台(Y/N) */ + private String isHongKongMacaoTaiwan; + + /** 备注 */ + private String remark; +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPlanDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPlanDto.java new file mode 100644 index 0000000..cce5fd3 --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchPlanDto.java @@ -0,0 +1,62 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.time.LocalDateTime; + +/** + * 履行计划 + * + * 对应结算平台推送合同接口的 contractKxPlans 字段。 + * 与法务接口文档中的款项计划一一映射。 + */ +@Data +public class ContractLaunchPlanDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 主键ID */ + private Long id; + + /** 关联合同主表 ID */ + private Long contractId; + + /** 履行事项(如:第一笔付款) */ + private String planMatter; + + /** 履约条件(如:达到xx条件) */ + private String planWhere; + + /** 节点日期 */ + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") + private LocalDateTime planTime; + + /** 提前提醒天数 */ + private Integer planDay; + + /** 款项金额 */ + private Double planMoney; + + /** + * 所属主体编码,推送法务字段 planSubjectCode。 + */ + private String planSubjectCode; + + /** + * 所属主体名称,推送法务字段 planSubjectName。 + */ + private String planSubjectName; + + /** 备注 */ + private String planDetail; + + /** 履行状态(0:待履行, 1:已履行, 2:逾期) */ + private String planStatus; + + /** 备注 */ + private String remark; +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchQueryDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchQueryDto.java new file mode 100644 index 0000000..ba57dcf --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchQueryDto.java @@ -0,0 +1,37 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import com.nbport.zgwl.dto.BasePageDto; +import lombok.Data; +import lombok.EqualsAndHashCode; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 合同发起列表查询参数 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class ContractLaunchQueryDto extends BasePageDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 合同名称(模糊搜索) */ + private String contractName; + + /** 合同编号 */ + private String contractCode; + + /** 合同类型 */ + private String contractType; + + /** 状态(draft/reviewing/rejected/pending_sign/signing/pending_collect/pending_archive/completed) */ + private String status; + + /** 创建时间范围-开始 yyyy-MM-dd */ + private String dateStart; + + /** 创建时间范围-结束 yyyy-MM-dd */ + private String dateEnd; +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java new file mode 100644 index 0000000..b978f8f --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/dto/ContractLaunchSealDto.java @@ -0,0 +1,40 @@ +package com.nbport.zgwl.contractlaunch.dto; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 签章回写参数 + * + * 签署完成后,浙江CA安全签平台通过回调通知我方, + * 我方把签署结果回写到合同的 seal_* 字段。 + */ +@Data +public class ContractLaunchSealDto implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 签章平台生成的合同ID(用于后续查询签署进度) */ + private String sealContractId; + + /** 签章状态(1:签署中, 2:签署完成, 3:签署失败) */ + private Integer sealContractStatus; + + /** 签署完成后的合同文件名 */ + private String signedFileName; + + /** 签署完成后的合同文件路径(我方存储) */ + private String signedFilePath; + + /** 签署完成后的合同文件类型 */ + private String signedFileType; + + /** 签署完成后的合同文件大小 */ + private Long signedFileSize; + + /** 未在线签署的原因(线下签署时填写) */ + private String signedOfflineReason; +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/mapper/ContractLaunchMapper.java b/src/main/java/com/nbport/zgwl/contractlaunch/mapper/ContractLaunchMapper.java new file mode 100644 index 0000000..fb90bec --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/mapper/ContractLaunchMapper.java @@ -0,0 +1,96 @@ +package com.nbport.zgwl.contractlaunch.mapper; + +import com.baomidou.mybatisplus.core.metadata.IPage; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchFileDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLogDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; +import org.apache.ibatis.annotations.Param; + +import java.util.List; + +public interface ContractLaunchMapper { + + /** 获取主表序列(Oracle 主键生成) */ + Long selectMainSeqNextVal(); + + /** 获取合同方表序列 */ + Long selectPartySeqNextVal(); + + /** 获取文件表序列 */ + Long selectFileSeqNextVal(); + + /** 获取履行计划表序列 */ + Long selectPlanSeqNextVal(); + + /** 获取日志表序列 */ + Long selectLogSeqNextVal(); + + /** 分页查询合同列表 */ + IPage selectContractPage(Page page, @Param("query") ContractLaunchQueryDto queryDto); + + /** 根据主键查询合同 */ + ContractLaunchDto selectContractById(@Param("id") Long id); + + /** 根据 contractSysId 查询合同(法务回调时用于定位) */ + ContractLaunchDto selectContractByContractSysId(@Param("contractSysId") String contractSysId); + + /** 新增合同 */ + int insertContract(ContractLaunchDto contract); + + /** 修改合同 */ + int updateContract(ContractLaunchDto contract); + + /** 逻辑删除合同 */ + int deleteContract(@Param("id") Long id, @Param("updateBy") String updateBy); + + /** 批量插入合同方 */ + int batchInsertParty(@Param("list") List list, + @Param("createBy") String createBy, + @Param("updateBy") String updateBy); + + /** 批量插入文件 */ + int batchInsertFile(@Param("list") List list, + @Param("createBy") String createBy, + @Param("updateBy") String updateBy); + + /** 批量插入履行计划 */ + int batchInsertPlan(@Param("list") List list, + @Param("createBy") String createBy, + @Param("updateBy") String updateBy); + + /** 插入单条操作日志 */ + int insertLog(ContractLaunchLogDto logDto); + + /** 删除合同下的所有合同方(逻辑删除) */ + int deletePartyByContractId(@Param("contractId") Long contractId, + @Param("updateBy") String updateBy); + + /** 删除合同下的所有文件(逻辑删除) */ + int deleteFileByContractId(@Param("contractId") Long contractId, + @Param("updateBy") String updateBy); + + /** 按分类删除合同下的文件(如重新上传正文时只删正文,不删其他附件) */ + int deleteFileByContractIdAndCategory(@Param("contractId") Long contractId, + @Param("fileCategory") String fileCategory, + @Param("updateBy") String updateBy); + + /** 删除合同下的所有履行计划(逻辑删除) */ + int deletePlanByContractId(@Param("contractId") Long contractId, + @Param("updateBy") String updateBy); + + /** 批量查询合同方(按 contractIds) */ + List selectPartyByContractIds(@Param("contractIds") List contractIds); + + /** 批量查询文件(按 contractIds) */ + List selectFileByContractIds(@Param("contractIds") List contractIds); + + /** 批量查询履行计划(按 contractIds) */ + List selectPlanByContractIds(@Param("contractIds") List contractIds); + + /** 批量查询操作日志(按 contractIds) */ + List selectLogByContractIds(@Param("contractIds") List contractIds); +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchService.java b/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchService.java new file mode 100644 index 0000000..3dd31ec --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchService.java @@ -0,0 +1,52 @@ +package com.nbport.zgwl.contractlaunch.service; + +import com.nbport.zgwl.common.PageResult; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto; + +import java.util.Map; + +public interface ContractLaunchService { + + /** 分页查询合同列表 */ + PageResult queryPage(ContractLaunchQueryDto queryDto); + + /** 查询合同详情(含合同方/文件/履行计划/日志) */ + ContractLaunchDto getDetail(Long id); + + /** 创建合同草稿 */ + ContractLaunchDto createDraft(ContractLaunchDto contract); + + /** 编辑合同草稿 */ + ContractLaunchDto updateDraft(ContractLaunchDto contract); + + /** 提交法务审批(调用结算平台 pushContract 接口) */ + ContractLaunchDto submitToLaw(Long id); + + /** 模拟审批通过(测试按钮,TODO:替换为真实法务回调) */ + ContractLaunchDto mockApprove(Long id, ContractLaunchApproveDto approveDto); + + /** 模拟审批驳回(测试按钮,TODO:替换为真实法务回调) */ + ContractLaunchDto mockReject(Long id, ContractLaunchApproveDto approveDto); + + /** 接收法务审批回调(处理审批结果,更新合同状态) */ + ContractLaunchDto handleLawApproveCallback(Map callbackPayload); + + /** 回写签章信息(签署发起时调用,记录 sealContractId) */ + ContractLaunchDto saveSealInfo(Long id, ContractLaunchSealDto sealDto); + + /** 签署完成回调(回写签署文件路径,状态变更为待采集) */ + ContractLaunchDto markSealCompleted(Long id, ContractLaunchSealDto sealDto); + + /** 模拟合同采集(调用数据采集 collectContract 接口,TODO:替换为真实调用) */ + ContractLaunchDto mockCollect(Long id); + + /** 合同归档(状态变更为已完成,TODO:同时推送归档到法务) */ + ContractLaunchDto archive(Long id, ContractLaunchArchiveDto archiveDto); + + /** 删除草稿(仅 draft/rejected 状态允许) */ + void deleteDraft(Long id); +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java b/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java new file mode 100644 index 0000000..c86dc48 --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/service/ContractLaunchServiceImpl.java @@ -0,0 +1,937 @@ +package com.nbport.zgwl.contractlaunch.service; + +import com.alibaba.fastjson2.JSON; +import com.baomidou.mybatisplus.extension.plugins.pagination.Page; +import com.nbport.zgwl.common.PageResult; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchFileDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLogDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; +import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto; +import com.nbport.zgwl.contractlaunch.mapper.ContractLaunchMapper; +import com.nbport.zgwl.contractlaunch.utils.ContractLaunchCodeUtils; +import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants; +import com.nbport.zgwl.contractpush.dto.PushPayloadDTO; +import com.nbport.zgwl.contractpush.service.DataCollectionPushService; +import com.nbport.zgwl.contractpush.service.SettlementPushService; +import com.nbport.zgwl.entity.SysUserEntity; +import com.nbport.zgwl.exception.ServiceException; +import com.nbport.zgwl.utils.AdminSecurityManage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * 合同发起主服务 + * + * 这里尽量把核心流程都写在一个 Service 里, + * 方便你后续自己看和改,不额外再拆很多 handler。 + */ +@Slf4j +@Service +@RequiredArgsConstructor +public class ContractLaunchServiceImpl implements ContractLaunchService { + + private final ContractLaunchMapper contractLaunchMapper; + private final AdminSecurityManage adminSecurityManage; + private final SettlementPushService settlementPushService; + private final DataCollectionPushService dataCollectionPushService; + + /** 分页查询,附带合同方/文件/履行计划(列表页不查日志) */ + @Override + public PageResult queryPage(ContractLaunchQueryDto queryDto) { + Page page = new Page<>(queryDto.getPageNo(), queryDto.getPageSize()); + Page resultPage = (Page) contractLaunchMapper.selectContractPage(page, queryDto); + PageResult pageResult = new PageResult<>(resultPage); + fillChildren(pageResult.getData(), false); + return pageResult; + } + + /** 查询详情,含日志 */ + @Override + public ContractLaunchDto getDetail(Long id) { + ContractLaunchDto contract = contractLaunchMapper.selectContractById(id); + if (contract == null) { + throw new ServiceException("合同不存在"); + } + fillChildren(Collections.singletonList(contract), true); + return contract; + } + + /** + * 创建合同草稿。 + * + * 完整流程: + * 1. 获取当前登录用户信息 + * 2. 数据清洗(normalizeContract:去空格、补默认值) + * 3. 数据校验(validateDraft:必填项检查) + * 4. 生成主键ID 和 contractSysId(CLS-时间戳)、contractCode(类型编码-日期-随机数) + * 5. 填充创建人/创建时间/公司/部门/状态 draft + * 6. 保存主表 + 子表(合同方/文件/履行计划) + * 7. 记录操作日志 + * + * 注意:此时仅在本系统保存,未推送到法务系统。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto createDraft(ContractLaunchDto contract) { + SysUserEntity currentUser = adminSecurityManage.getUser(); + normalizeContract(contract); + validateDraft(contract); + + LocalDateTime now = LocalDateTime.now(); + contract.setId(contractLaunchMapper.selectMainSeqNextVal()); + contract.setContractSysId(ContractLaunchCodeUtils.buildContractSysId()); + if (contract.getContractCode() == null || contract.getContractCode().isBlank()) { + contract.setContractCode(ContractLaunchCodeUtils.buildContractCode(contract.getContractTypeCode())); + } + contract.setContractSource(contract.getContractSource() == null || contract.getContractSource().isBlank() ? "ECP" : contract.getContractSource()); + contract.setStatus(ContractLaunchConstants.STATUS_DRAFT); + contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_PENDING); + contract.setInitiator(currentUser.getUsername()); + contract.setInitiatorDept(currentUser.getSysOrgEntity() == null ? "" : currentUser.getSysOrgEntity().getOrgName()); + contract.setCompanyName(currentUser.getSysOrgEntity() == null ? "" : currentUser.getSysOrgEntity().getOrgName()); + contract.setOrgId(currentUser.getSysOrgEntity() == null ? "" : currentUser.getSysOrgEntity().getId()); + contract.setCreatorPhone(currentUser.getMobilePhone()); + contract.setCreateBy(currentUser.getUsername()); + contract.setCreateTime(now); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(now); + contract.setIsDeleted(ContractLaunchConstants.IS_DELETED_NO); + contract.setQueryScope("personal"); + contractLaunchMapper.insertContract(contract); + saveChildren(contract, currentUser.getUsername()); + insertLog(contract.getId(), ContractLaunchConstants.ACTION_SAVE, "保存草稿", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "合同草稿创建成功", null, currentUser); + return getDetail(contract.getId()); + } + + /** + * 编辑合同草稿。 + * + * 仅 draft(草稿)或 rejected(已驳回)可编辑。 + * 采用替换式更新:先删子表(合同方/文件/履行计划)旧数据,再以当前数据重新插入。 + * 保留以下字段不变:contractSysId、contractCode、lawContractCode、lawContractId、 + * 状态、审批状态、发起人、公司、orgId、创建人/创建时间。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto updateDraft(ContractLaunchDto contract) { + if (contract.getId() == null) { + throw new ServiceException("修改时 id 不能为空"); + } + ContractLaunchDto dbContract = getDetail(contract.getId()); + if (!canEdit(dbContract.getStatus())) { + throw new ServiceException("当前状态不允许编辑"); + } + + SysUserEntity currentUser = adminSecurityManage.getUser(); + normalizeContract(contract); + validateDraft(contract); + contract.setContractSysId(dbContract.getContractSysId()); + contract.setContractCode(dbContract.getContractCode()); + contract.setLawContractCode(dbContract.getLawContractCode()); + contract.setLawContractId(dbContract.getLawContractId()); + contract.setStatus(dbContract.getStatus()); + contract.setApproveStatus(dbContract.getApproveStatus()); + contract.setInitiator(dbContract.getInitiator()); + contract.setInitiatorDept(dbContract.getInitiatorDept()); + contract.setCompanyName(dbContract.getCompanyName()); + contract.setOrgId(dbContract.getOrgId()); + contract.setCreatorPhone(dbContract.getCreatorPhone()); + contract.setCreateBy(dbContract.getCreateBy()); + contract.setCreateTime(dbContract.getCreateTime()); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contract.setIsDeleted(ContractLaunchConstants.IS_DELETED_NO); + contractLaunchMapper.updateContract(contract); + replaceChildren(contract, currentUser.getUsername()); + insertLog(contract.getId(), ContractLaunchConstants.ACTION_SAVE, "编辑草稿", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "合同草稿修改成功", null, currentUser); + return getDetail(contract.getId()); + } + + /** + * 提交法务审批。 + * + * 状态流转:draft/rejected → reviewing(待审批) + * + * 主要做两件事: + * 1. 更新合同状态为 reviewing + * 2. 构建结算平台 pushContract 接口的完整报文(含合同方、履行计划、附件等) + * 3. 记录提交日志(日志中保存了完整的推送报文,方便排查) + * + * TODO(真实联调时打开): + * PushPayloadDTO requestDTO = new PushPayloadDTO(); + * requestDTO.setPayload(settlementPayload); + * settlementPushService.pushContract(requestDTO); + * + * 法务审批完成后会通过 pushUrl 回调本系统的 /callback/law/approve 接口。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto submitToLaw(Long id) { + ContractLaunchDto contract = getDetail(id); + if (!canSubmit(contract.getStatus())) { + throw new ServiceException("当前状态不允许提交法务"); + } + SysUserEntity currentUser = adminSecurityManage.getUser(); + contract.setStatus(ContractLaunchConstants.STATUS_REVIEWING); + contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_PENDING); + contract.setApproveMessage("已提交法务,等待审批"); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + + Map settlementPayload = buildSettlementPushPayload(contract); + insertLog(id, ContractLaunchConstants.ACTION_SUBMIT, "提交法务", ContractLaunchConstants.ACTION_STATUS_PENDING, "合同已提交法务接口,当前先保留 mock 流程", settlementPayload, currentUser); + + // TODO: 法务接口当前不可用,真实联调时打开下面调用即可 + // PushPayloadDTO requestDTO = new PushPayloadDTO(); + // requestDTO.setPayload(settlementPayload); + // Map response = settlementPushService.pushContract(requestDTO); + + return getDetail(id); + } + + /** + * 【测试按钮】模拟法务审批通过。 + * + * 状态流转:reviewing → pending_sign(待签署) + * + * 模拟法务系统返回 lawContractCode 和 lawContractId,供后续签署流程使用。 + * 如果 approveDto 中传入了值则使用传入值,否则自动生成模拟值: + * - lawContractCode = "LAW-" + 合同类型编码-日期-随机数 + * - lawContractId = "LAWID-" + 合同ID + * + * 真实场景下此逻辑由法务回调接口 handleLawApproveCallback 替代。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto mockApprove(Long id, ContractLaunchApproveDto approveDto) { + ContractLaunchDto contract = getDetail(id); + if (!ContractLaunchConstants.STATUS_REVIEWING.equals(contract.getStatus())) { + throw new ServiceException("仅审批中的合同可模拟通过"); + } + SysUserEntity currentUser = adminSecurityManage.getUser(); + contract.setStatus(ContractLaunchConstants.STATUS_PENDING_SIGN); + contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_APPROVED); + contract.setApproveMessage(approveDto == null || approveDto.getApproveMessage() == null || approveDto.getApproveMessage().isBlank() + ? "法务审批通过(测试按钮模拟)" + : approveDto.getApproveMessage()); + contract.setLawContractCode(approveDto == null || approveDto.getLawContractCode() == null || approveDto.getLawContractCode().isBlank() + ? "LAW-" + ContractLaunchCodeUtils.buildContractCode(contract.getContractTypeCode()) + : approveDto.getLawContractCode()); + contract.setLawContractId(approveDto == null || approveDto.getLawContractId() == null || approveDto.getLawContractId().isBlank() + ? "LAWID-" + id + : approveDto.getLawContractId()); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + insertLog(id, ContractLaunchConstants.ACTION_APPROVE, "模拟审批通过", ContractLaunchConstants.ACTION_STATUS_SUCCESS, contract.getApproveMessage(), approveDto, currentUser); + return getDetail(id); + } + + /** + * 【测试按钮】模拟法务审批驳回。 + * + * 状态流转:reviewing → rejected(已驳回) + * 驳回后合同回到草稿状态,可以再次编辑后重新提交。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto mockReject(Long id, ContractLaunchApproveDto approveDto) { + ContractLaunchDto contract = getDetail(id); + if (!ContractLaunchConstants.STATUS_REVIEWING.equals(contract.getStatus())) { + throw new ServiceException("仅审批中的合同可模拟驳回"); + } + SysUserEntity currentUser = adminSecurityManage.getUser(); + contract.setStatus(ContractLaunchConstants.STATUS_REJECTED); + contract.setApproveStatus(ContractLaunchConstants.APPROVE_STATUS_REJECTED); + contract.setApproveMessage(approveDto == null || approveDto.getApproveMessage() == null || approveDto.getApproveMessage().isBlank() + ? "法务审批驳回(测试按钮模拟)" + : approveDto.getApproveMessage()); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + insertLog(id, ContractLaunchConstants.ACTION_REJECT, "模拟审批驳回", ContractLaunchConstants.ACTION_STATUS_SUCCESS, contract.getApproveMessage(), approveDto, currentUser); + return getDetail(id); + } + + /** + * 接收法务审批回调。 + * + * 法务系统审批完成后,通过 pushUrl POST 审批结果到本接口。 + * + * 处理逻辑: + * 1. 从回调报文中提取 contractSysId 定位到本系统合同 + * 2. 判断审批结果(通过/驳回) + * - 通过:status → pending_sign(待签署),记录法务返回的合同编号和合同ID + * - 驳回:status → rejected(已驳回),记录驳回原因 + * 3. 记录操作日志 + * + * 兼容设计: + * - 使用 getFirstText 按多个 key 依次尝试提取字段(如 approveStatus/status/result) + * - 审批通过状态兼容:1/true/success/approved/pass/agree/审批通过 + * - 合同编号兼容:lawContractCode/contractCode/htCode/code + * - 合同ID兼容:lawContractId/contractId/id/businessId + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto handleLawApproveCallback(Map callbackPayload) { + ContractLaunchDto contract = getContractByLawCallback(callbackPayload); + SysUserEntity currentUser = adminSecurityManage.getUser(); + + boolean approved = isLawCallbackApproved(callbackPayload); + contract.setApproveStatus(approved + ? ContractLaunchConstants.APPROVE_STATUS_APPROVED + : ContractLaunchConstants.APPROVE_STATUS_REJECTED); + contract.setStatus(approved + ? ContractLaunchConstants.STATUS_PENDING_SIGN + : ContractLaunchConstants.STATUS_REJECTED); + contract.setApproveMessage(getFirstText(callbackPayload, + "approveMessage", "message", "msg", "opinion", "remark")); + + String lawContractCode = getFirstText(callbackPayload, + "lawContractCode", "contractCode", "htCode", "code"); + String lawContractId = getFirstText(callbackPayload, + "lawContractId", "contractId", "id", "businessId"); + if (lawContractCode != null && !lawContractCode.isBlank()) { + contract.setLawContractCode(lawContractCode); + } + if (lawContractId != null && !lawContractId.isBlank()) { + contract.setLawContractId(lawContractId); + } + + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + insertLog( + contract.getId(), + approved ? ContractLaunchConstants.ACTION_APPROVE : ContractLaunchConstants.ACTION_REJECT, + approved ? "法务审批回调通过" : "法务审批回调驳回", + ContractLaunchConstants.ACTION_STATUS_SUCCESS, + contract.getApproveMessage(), + callbackPayload, + currentUser + ); + return getDetail(contract.getId()); + } + + /** + * 回写签章信息(签署发起时调用)。 + * + * 浙江CA安全签平台创建签署合同后,回调此方法回写 sealContractId。 + * 状态流转:pending_sign → signing(签署中) + * + * sealContractId 由浙江CA安全签平台生成,后续通过此 ID 查询签署进度。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto saveSealInfo(Long id, ContractLaunchSealDto sealDto) { + ContractLaunchDto contract = getDetail(id); + SysUserEntity currentUser = adminSecurityManage.getUser(); + contract.setSealContractId(sealDto.getSealContractId()); + contract.setSealContractStatus(sealDto.getSealContractStatus()); + contract.setStatus(ContractLaunchConstants.STATUS_SIGNING); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + insertLog(id, ContractLaunchConstants.ACTION_SEAL_START, "开始签署", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "签章平台合同已创建", sealDto, currentUser); + return getDetail(id); + } + + /** + * 签署完成回调。 + * + * 所有签署方签署完成后(或在浙江CA签署完成、或线下签署后手动上传), + * 回写签署完成的合同文件信息。 + * + * 状态流转:signing → pending_collect(待采集) + * + * 执行步骤: + * 1. 回写 signedFileName/signedFilePath/signedFileType/signedFileSize + * 2. 如果有线下签署原因(signedOfflineReason),一并保存 + * 3. 将签署文件保存到 file 子表(fileCategory = "signed") + * 4. 记录操作日志 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto markSealCompleted(Long id, ContractLaunchSealDto sealDto) { + ContractLaunchDto contract = getDetail(id); + SysUserEntity currentUser = adminSecurityManage.getUser(); + contract.setSealContractId(sealDto.getSealContractId() == null || sealDto.getSealContractId().isBlank() ? contract.getSealContractId() : sealDto.getSealContractId()); + contract.setSealContractStatus(sealDto.getSealContractStatus()); + contract.setSignedFileName(sealDto.getSignedFileName()); + contract.setSignedFilePath(sealDto.getSignedFilePath()); + contract.setSignedFileType(sealDto.getSignedFileType()); + contract.setSignedFileSize(sealDto.getSignedFileSize()); + contract.setSignedOfflineReason(sealDto.getSignedOfflineReason()); + contract.setStatus(ContractLaunchConstants.STATUS_PENDING_COLLECT); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + saveSignedFile(id, sealDto, currentUser.getUsername()); + insertLog(id, ContractLaunchConstants.ACTION_SEAL_FINISH, "签署完成", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "合同签署已完成,待采集", sealDto, currentUser); + return getDetail(id); + } + + /** + * 【测试按钮】模拟合同采集。 + * + * 合同签署完成后,将合同最新信息(含用印信息和签订信息)推送到法务系统。 + * + * 状态流转:pending_collect → pending_archive(待归档) + * + * 构建数据采集 V3.0 collectContract 接口的报文,包含: + * - 合同基本字段(c_name, c_code, c_type 等) + * - 用印信息 c_seal(签章类型、签章日期等) + * - 签订信息 c_sign(签署日期、生效日期、签署附件等) + * - 履行计划 c_lxPlan + * - 附件(正文 c_zw / 其他 c_attachs / 签署文件 c_signAttachs) + * + * TODO(真实联调时打开): + * PushPayloadDTO requestDTO = new PushPayloadDTO(); + * requestDTO.setPayload(payload); + * dataCollectionPushService.collectContract(requestDTO); + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto mockCollect(Long id) { + ContractLaunchDto contract = getDetail(id); + if (!ContractLaunchConstants.STATUS_PENDING_COLLECT.equals(contract.getStatus())) { + throw new ServiceException("当前状态不允许执行采集"); + } + SysUserEntity currentUser = adminSecurityManage.getUser(); + Map payload = buildDataCollectionContractPayload(contract); + insertLog(id, ContractLaunchConstants.ACTION_COLLECT, "模拟合同采集", ContractLaunchConstants.ACTION_STATUS_PENDING, "合同采集接口当前先注释,使用测试按钮流转", payload, currentUser); + + // TODO: 法务接口当前不可用,真实联调时打开下面调用即可 + // PushPayloadDTO requestDTO = new PushPayloadDTO(); + // requestDTO.setPayload(payload); + // dataCollectionPushService.collectContract(requestDTO); + + contract.setStatus(ContractLaunchConstants.STATUS_PENDING_ARCHIVE); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + return getDetail(id); + } + + /** + * 合同归档。 + * + * 合同全生命周期最后一步。将归档信息(保管人、保管单位、归档附件)保存到合同, + * 同时推送到法务系统存档。 + * + * 状态流转:pending_archive → completed(已完成) + * + * 执行步骤: + * 1. 填写归档日期、保管人信息 + * 2. 保存归档附件到 file 子表(fileCategory = "archive") + * 3. 构建归档报文 pushArchive 接口的 payload + * 4. 记录操作日志 + * + * TODO(真实联调时打开): + * settlementPushService.pushArchive(requestDTO) + * dataCollectionPushService.collectContractArchive(requestDTO) + */ + @Override + @Transactional(rollbackFor = Exception.class) + public ContractLaunchDto archive(Long id, ContractLaunchArchiveDto archiveDto) { + ContractLaunchDto contract = getDetail(id); + if (!ContractLaunchConstants.STATUS_PENDING_ARCHIVE.equals(contract.getStatus())) { + throw new ServiceException("当前状态不允许归档"); + } + SysUserEntity currentUser = adminSecurityManage.getUser(); + contract.setArchiveDate(LocalDateTime.now()); + contract.setArchiveKeeper(archiveDto == null ? null : archiveDto.getArchiveKeeper()); + contract.setArchiveKeeperUnit(archiveDto == null ? null : archiveDto.getArchiveKeeperUnit()); + contract.setArchiveKeeperUnitCode(archiveDto == null ? null : archiveDto.getArchiveKeeperUnitCode()); + contract.setStatus(ContractLaunchConstants.STATUS_COMPLETED); + contract.setUpdateBy(currentUser.getUsername()); + contract.setUpdateTime(LocalDateTime.now()); + contractLaunchMapper.updateContract(contract); + + if (archiveDto != null && archiveDto.getArchiveFiles() != null && !archiveDto.getArchiveFiles().isEmpty()) { + for (ContractLaunchFileDto fileDto : archiveDto.getArchiveFiles()) { + fileDto.setId(contractLaunchMapper.selectFileSeqNextVal()); + fileDto.setContractId(id); + fileDto.setFileCategory(ContractLaunchConstants.FILE_CATEGORY_ARCHIVE); + } + contractLaunchMapper.batchInsertFile(archiveDto.getArchiveFiles(), currentUser.getUsername(), currentUser.getUsername()); + } + + Map payload = buildArchivePayload(contract, archiveDto); + insertLog(id, ContractLaunchConstants.ACTION_ARCHIVE, "合同归档", ContractLaunchConstants.ACTION_STATUS_PENDING, "合同归档接口当前先注释,使用测试按钮流转", payload, currentUser); + + // TODO: 法务接口当前不可用,真实联调时打开下面调用即可 + // PushPayloadDTO requestDTO = new PushPayloadDTO(); + // requestDTO.setPayload(payload); + // settlementPushService.pushArchive(requestDTO); + // dataCollectionPushService.collectContractArchive(requestDTO); + + return getDetail(id); + } + + /** + * 删除合同草稿(逻辑删除)。 + * + * 仅 draft(草稿)或 rejected(已驳回)状态允许删除。 + * 设置 isDeleted = 'Y',不物理删除数据。 + * 已提交法务审批或进行中的合同不允许删除。 + */ + @Override + @Transactional(rollbackFor = Exception.class) + public void deleteDraft(Long id) { + ContractLaunchDto contract = getDetail(id); + if (!canDelete(contract.getStatus())) { + throw new ServiceException("仅草稿或驳回状态允许删除"); + } + SysUserEntity currentUser = adminSecurityManage.getUser(); + contractLaunchMapper.deleteContract(id, currentUser.getUsername()); + insertLog(id, ContractLaunchConstants.ACTION_DELETE, "删除合同", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "合同已逻辑删除", null, currentUser); + } + + /** + * 批量填充子表数据(合同方/文件/履行计划/日志)。 + * + * 一次性查完所有子表再按 contractId 分组赋值,避免 N+1 查询。 + * includeLogs: 列表页不查日志(日志数据量大),详情页才查。 + */ + private void fillChildren(List list, boolean includeLogs) { + if (list == null || list.isEmpty()) { + return; + } + List ids = list.stream().map(ContractLaunchDto::getId).filter(Objects::nonNull).collect(Collectors.toList()); + if (ids.isEmpty()) { + return; + } + Map> partyMap = contractLaunchMapper.selectPartyByContractIds(ids).stream() + .collect(Collectors.groupingBy(ContractLaunchPartyDto::getContractId)); + Map> fileMap = contractLaunchMapper.selectFileByContractIds(ids).stream() + .collect(Collectors.groupingBy(ContractLaunchFileDto::getContractId)); + Map> planMap = contractLaunchMapper.selectPlanByContractIds(ids).stream() + .collect(Collectors.groupingBy(ContractLaunchPlanDto::getContractId)); + Map> logMap = includeLogs + ? contractLaunchMapper.selectLogByContractIds(ids).stream().collect(Collectors.groupingBy(ContractLaunchLogDto::getContractId)) + : Collections.emptyMap(); + + list.forEach(item -> { + item.setPartyList(new ArrayList<>(partyMap.getOrDefault(item.getId(), Collections.emptyList()))); + item.setFileList(new ArrayList<>(fileMap.getOrDefault(item.getId(), Collections.emptyList()))); + item.setPerformancePlans(new ArrayList<>(planMap.getOrDefault(item.getId(), Collections.emptyList()))); + item.setLogList(new ArrayList<>(logMap.getOrDefault(item.getId(), Collections.emptyList()))); + item.setCounterpartySummary( + item.getPartyList().stream() + .filter(party -> !"A".equalsIgnoreCase(party.getRoleCode())) + .map(ContractLaunchPartyDto::getCompanyName) + .filter(name -> name != null && !name.isBlank()) + .collect(Collectors.joining(";")) + ); + }); + } + + /** 保存子表数据(合同方/文件/履行计划,插入前生成主键 ID) */ + private void saveChildren(ContractLaunchDto contract, String operator) { + if (contract.getPartyList() != null) { + for (int i = 0; i < contract.getPartyList().size(); i++) { + ContractLaunchPartyDto party = contract.getPartyList().get(i); + party.setId(contractLaunchMapper.selectPartySeqNextVal()); + party.setContractId(contract.getId()); + party.setSortOrder(i + 1); + } + if (!contract.getPartyList().isEmpty()) { + contractLaunchMapper.batchInsertParty(contract.getPartyList(), operator, operator); + } + } + if (contract.getFileList() != null) { + for (int i = 0; i < contract.getFileList().size(); i++) { + ContractLaunchFileDto file = contract.getFileList().get(i); + file.setId(contractLaunchMapper.selectFileSeqNextVal()); + file.setContractId(contract.getId()); + file.setSortOrder(i + 1); + } + if (!contract.getFileList().isEmpty()) { + contractLaunchMapper.batchInsertFile(contract.getFileList(), operator, operator); + } + } + if (contract.getPerformancePlans() != null) { + for (ContractLaunchPlanDto plan : contract.getPerformancePlans()) { + plan.setId(contractLaunchMapper.selectPlanSeqNextVal()); + plan.setContractId(contract.getId()); + if (plan.getPlanStatus() == null || plan.getPlanStatus().isBlank()) { + plan.setPlanStatus("0"); + } + } + if (!contract.getPerformancePlans().isEmpty()) { + contractLaunchMapper.batchInsertPlan(contract.getPerformancePlans(), operator, operator); + } + } + } + + /** 替换子表数据(先删旧数据,再保存新数据) */ + private void replaceChildren(ContractLaunchDto contract, String operator) { + contractLaunchMapper.deletePartyByContractId(contract.getId(), operator); + contractLaunchMapper.deleteFileByContractId(contract.getId(), operator); + contractLaunchMapper.deletePlanByContractId(contract.getId(), operator); + saveChildren(contract, operator); + } + + /** 保存签署文件到 file 子表(先删旧签署文件,再插入新的) */ + private void saveSignedFile(Long contractId, ContractLaunchSealDto sealDto, String operator) { + if (sealDto.getSignedFilePath() == null || sealDto.getSignedFilePath().isBlank()) { + return; + } + contractLaunchMapper.deleteFileByContractIdAndCategory(contractId, ContractLaunchConstants.FILE_CATEGORY_SIGNED, operator); + ContractLaunchFileDto fileDto = new ContractLaunchFileDto(); + fileDto.setId(contractLaunchMapper.selectFileSeqNextVal()); + fileDto.setContractId(contractId); + fileDto.setFileCategory(ContractLaunchConstants.FILE_CATEGORY_SIGNED); + fileDto.setFileName(sealDto.getSignedFileName() == null || sealDto.getSignedFileName().isBlank() + ? "已签署合同文件" + : sealDto.getSignedFileName()); + fileDto.setFilePath(sealDto.getSignedFilePath()); + fileDto.setFileType(sealDto.getSignedFileType()); + fileDto.setFileSize(sealDto.getSignedFileSize()); + fileDto.setSortOrder(1); + contractLaunchMapper.batchInsertFile(Collections.singletonList(fileDto), operator, operator); + } + + /** 写入操作日志 */ + private void insertLog(Long contractId, String actionType, String actionName, String actionStatus, String actionContent, Object payload, SysUserEntity currentUser) { + ContractLaunchLogDto logDto = new ContractLaunchLogDto(); + logDto.setId(contractLaunchMapper.selectLogSeqNextVal()); + logDto.setContractId(contractId); + logDto.setActionType(actionType); + logDto.setActionName(actionName); + logDto.setActionStatus(actionStatus); + logDto.setOperatorName(currentUser.getUsername()); + logDto.setOperatorPhone(currentUser.getMobilePhone()); + logDto.setActionTime(LocalDateTime.now()); + logDto.setActionContent(actionContent); + logDto.setCallbackPayload(payload == null ? null : JSON.toJSONString(payload)); + contractLaunchMapper.insertLog(logDto); + } + + /** 数据清洗:去空格、补默认值 */ + private void normalizeContract(ContractLaunchDto contract) { + if (contract == null) { + throw new ServiceException("合同数据不能为空"); + } + contract.setContractName(trim(contract.getContractName())); + contract.setContractCode(trim(contract.getContractCode())); + contract.setContractType(trim(contract.getContractType())); + contract.setContractTypeCode(trim(contract.getContractTypeCode())); + contract.setPaymentMethod(trim(contract.getPaymentMethod())); + contract.setPrimaryContent(trim(contract.getPrimaryContent())); + contract.setAmountExplain(trim(contract.getAmountExplain())); + contract.setAssistDepartmentName(trim(contract.getAssistDepartmentName())); + contract.setExecutorAccount(trim(contract.getExecutorAccount())); + contract.setPeriodExplain(trim(contract.getPeriodExplain())); + contract.setContractGistRemark(trim(contract.getContractGistRemark())); + contract.setBodySummary(trim(contract.getBodySummary())); + contract.setSigningSubjectCode(trim(contract.getSigningSubjectCode())); + contract.setDateType(trim(contract.getDateType())); + contract.setPushUrl(trim(contract.getPushUrl())); + contract.setEwAmountType(trim(contract.getEwAmountType())); + if (contract.getDateType() == null || contract.getDateType().isBlank()) { + contract.setDateType("0"); + } + if (contract.getTextSource() == null || contract.getTextSource().isBlank()) { + contract.setTextSource("standardText"); + } + if (contract.getIsElectron() == null || contract.getIsElectron().isBlank()) { + contract.setIsElectron("1"); + } + if (contract.getPaymentDirection() == null || contract.getPaymentDirection().isBlank()) { + contract.setPaymentDirection("0"); + } + if (contract.getPushUrl() == null || contract.getPushUrl().isBlank()) { + contract.setPushUrl("/contract/launch/callback/law/approve"); + } + if (contract.getPartyList() == null) { + contract.setPartyList(new ArrayList<>()); + } + if (contract.getFileList() == null) { + contract.setFileList(new ArrayList<>()); + } + if (contract.getPerformancePlans() == null) { + contract.setPerformancePlans(new ArrayList<>()); + } + } + + /** 校验草稿必填项 */ + private void validateDraft(ContractLaunchDto contract) { + if (contract.getContractName() == null || contract.getContractName().isBlank()) { + throw new ServiceException("合同名称不能为空"); + } + if (contract.getContractTypeCode() == null || contract.getContractTypeCode().isBlank()) { + throw new ServiceException("合同类型编码不能为空"); + } + if (contract.getContractType() == null || contract.getContractType().isBlank()) { + throw new ServiceException("合同类型不能为空"); + } + if (contract.getValuationMode() == null || contract.getValuationMode().isBlank()) { + throw new ServiceException("计价方式不能为空"); + } + if (contract.getCurrencyName() == null || contract.getCurrencyName().isBlank()) { + throw new ServiceException("币种不能为空"); + } + if (contract.getSealType() == null || contract.getSealType().isBlank()) { + throw new ServiceException("签订方式不能为空"); + } + if (contract.getContractPeriodType() == null || contract.getContractPeriodType().isBlank()) { + throw new ServiceException("合同期限类型不能为空"); + } + if ("0".equals(contract.getContractPeriodType()) + && (contract.getEffectiveStart() == null || contract.getEffectiveEnd() == null)) { + throw new ServiceException("固定期限合同必须填写开始和结束时间"); + } + if ("1".equals(contract.getContractPeriodType()) + && (contract.getPeriodExplain() == null || contract.getPeriodExplain().isBlank())) { + throw new ServiceException("无固定期限合同必须填写期限说明"); + } + if (contract.getPartyList() == null || contract.getPartyList().isEmpty()) { + throw new ServiceException("至少需要维护一个合同方"); + } + boolean hasPartyA = contract.getPartyList().stream().anyMatch(item -> "A".equalsIgnoreCase(item.getRoleCode())); + boolean hasCounterparty = contract.getPartyList().stream().anyMatch(item -> !"A".equalsIgnoreCase(item.getRoleCode())); + if (!hasPartyA) { + throw new ServiceException("必须维护我方信息"); + } + if (!hasCounterparty) { + throw new ServiceException("至少维护一个相对方"); + } + } + + private boolean canEdit(String status) { + return ContractLaunchConstants.STATUS_DRAFT.equals(status) || ContractLaunchConstants.STATUS_REJECTED.equals(status); + } + + private boolean canSubmit(String status) { + return ContractLaunchConstants.STATUS_DRAFT.equals(status) || ContractLaunchConstants.STATUS_REJECTED.equals(status); + } + + private boolean canDelete(String status) { + return ContractLaunchConstants.STATUS_DRAFT.equals(status) || ContractLaunchConstants.STATUS_REJECTED.equals(status); + } + + /** 通过法务回调报文查找对应合同(先取 contractSysId 再查库) */ + private ContractLaunchDto getContractByLawCallback(Map payload) { + if (payload == null || payload.isEmpty()) { + throw new ServiceException("法务回调报文不能为空"); + } + String contractSysId = getFirstText(payload, "contractSysId", "businessNo", "thirdContractId"); + if (contractSysId == null || contractSysId.isBlank()) { + throw new ServiceException("法务回调缺少 contractSysId,无法定位合同"); + } + ContractLaunchDto contract = contractLaunchMapper.selectContractByContractSysId(contractSysId); + if (contract == null) { + throw new ServiceException("未找到法务回调对应的合同:" + contractSysId); + } + fillChildren(Collections.singletonList(contract), true); + return contract; + } + + /** 判断法务回调是否审批通过(兼容多种字段名和值) */ + private boolean isLawCallbackApproved(Map payload) { + String status = getFirstText(payload, "approveStatus", "status", "result", "auditStatus"); + if (status == null || status.isBlank()) { + return true; + } + String normalized = status.trim().toLowerCase(); + return normalized.equals("1") + || normalized.equals("true") + || normalized.equals("success") + || normalized.equals("approved") + || normalized.equals("pass") + || normalized.equals("agree") + || normalized.equals("通过") + || normalized.equals("审批通过"); + } + + /** 从回调报文中提取指定 key 的文本值(多个 key 依次尝试,取第一个非空) */ + private String getFirstText(Map payload, String... keys) { + if (payload == null) { + return null; + } + for (String key : keys) { + Object value = payload.get(key); + if (value == null) { + continue; + } + String text = String.valueOf(value).trim(); + if (!text.isBlank()) { + return text; + } + } + return null; + } + + /** 构建结算平台推送合同报文(pushContract 接口的 payload) */ + private Map buildSettlementPushPayload(ContractLaunchDto contract) { + Map payload = new LinkedHashMap<>(); + payload.put("contractSource", contract.getContractSource()); + payload.put("isAddit", contract.getIsAddit() == null ? 2 : contract.getIsAddit()); + payload.put("operate", "create"); + payload.put("mainContractCode", contract.getMainContractCode()); + payload.put("periodExplain", contract.getPeriodExplain()); + payload.put("valuationMode", contract.getValuationMode()); + payload.put("isMajorContract", contract.getIsMajorContract() == null ? 0 : contract.getIsMajorContract()); + payload.put("sealType", contract.getSealType()); + payload.put("creatorPhone", contract.getCreatorPhone()); + payload.put("dateType", contract.getDateType()); + payload.put("currencyName", contract.getCurrencyName()); + payload.put("contractGistRemark", contract.getContractGistRemark()); + payload.put("contractSysId", contract.getContractSysId()); + payload.put("selfCode", contract.getSelfCode()); + payload.put("contractName", contract.getContractName()); + payload.put("startTime", contract.getEffectiveStart()); + payload.put("endTime", contract.getEffectiveEnd()); + payload.put("amountExplain", contract.getAmountExplain()); + payload.put("isText", contract.getTextSource()); + payload.put("primaryContent", contract.getPrimaryContent()); + payload.put("isElectron", contract.getIsElectron()); + payload.put("paymentDirection", contract.getPaymentDirection()); + payload.put("contractTypeCode", contract.getContractTypeCode()); + payload.put("bodySummary", contract.getBodySummary()); + payload.put("signingSubjectCode", + contract.getSigningSubjectCode() == null || contract.getSigningSubjectCode().isBlank() + ? contract.getOrgId() + : contract.getSigningSubjectCode()); + payload.put("executorAccount", contract.getExecutorAccount()); + payload.put("contractPeriod", contract.getContractPeriodType()); + payload.put("contractAmount", contract.getContractAmount()); + payload.put("isSgat", contract.getIsSgat()); + payload.put("isOpenBidding", contract.getIsOpenBidding() == null ? 0 : contract.getIsOpenBidding()); + payload.put("pushUrl", contract.getPushUrl()); + payload.put("isYearBudget", contract.getIsYearBudget() == null ? 0 : contract.getIsYearBudget()); + payload.put("isShare", contract.getIsShare()); + payload.put("assistDepartmentName", contract.getAssistDepartmentName()); + payload.put("contractNature", contract.getContractNature()); + payload.put("contractCode", contract.getContractCode()); + payload.put("ewAmountType", contract.getEwAmountType()); + payload.put("djAmount", contract.getDjAmount()); + payload.put("yjAmount", contract.getYjAmount()); + payload.put("yfkAmount", contract.getYfkAmount()); + payload.put("bzjAmount", contract.getBzjAmount()); + payload.put("contractOpposites", contract.getPartyList().stream() + .filter(item -> !"A".equalsIgnoreCase(item.getRoleCode())) + .map(item -> { + Map map = new LinkedHashMap<>(); + map.put("oppCharacter", "2".equals(item.getOppositeCharacter()) ? "ZZR" : "QY"); + map.put("credirCode", item.getCreditCode()); + map.put("idCard", item.getNaturalPersonIdNumber()); + map.put("oppositeName", item.getCompanyName()); + return map; + }).collect(Collectors.toList())); + payload.put("contractKxPlans", contract.getPerformancePlans().stream().map(plan -> { + Map map = new LinkedHashMap<>(); + map.put("planWhere", plan.getPlanWhere()); + map.put("planMoney", plan.getPlanMoney()); + map.put("planDetail", plan.getPlanDetail()); + map.put("planTime", plan.getPlanTime()); + map.put("planMatter", plan.getPlanMatter()); + map.put("planDay", plan.getPlanDay()); + map.put("planSubjectCode", plan.getPlanSubjectCode()); + map.put("planSubjectName", plan.getPlanSubjectName()); + return map; + }).collect(Collectors.toList())); + payload.put("contractText", buildFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_BODY)); + payload.put("otherAttach", buildFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_OTHER)); + payload.put("contractGistFile", buildFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_GIST)); + return payload; + } + + /** 构建数据采集合同报文(collectContract 接口的 payload) */ + private Map buildDataCollectionContractPayload(ContractLaunchDto contract) { + Map payload = new LinkedHashMap<>(); + payload.put("c_name", contract.getContractName()); + payload.put("c_code", contract.getLawContractCode() == null || contract.getLawContractCode().isBlank() ? contract.getContractCode() : contract.getLawContractCode()); + payload.put("c_weNames", contract.getCompanyName()); + payload.put("c_oppositeNames", contract.getCounterpartySummary()); + payload.put("c_textSource", contract.getTextSource()); + payload.put("c_mainContent", contract.getPrimaryContent()); + payload.put("c_direction", contract.getPaymentDirection()); + payload.put("c_method", contract.getValuationMode()); + payload.put("c_subjectAMT", contract.getContractAmount()); + payload.put("c_currency", contract.getCurrencyName()); + payload.put("c_amountExplain", contract.getAmountExplain()); + payload.put("c_type", contract.getContractTypeCode()); + payload.put("c_termType", contract.getContractPeriodType()); + payload.put("c_termExplain", contract.getPeriodExplain()); + payload.put("c_termStart", contract.getEffectiveStart()); + payload.put("c_termEnd", contract.getEffectiveEnd()); + payload.put("c_qyztGAT", contract.getIsSgat()); + payload.put("c_executor", contract.getExecutorAccount()); + payload.put("c_openBidding", contract.getIsOpenBidding()); + payload.put("c_currentBudget", contract.getIsYearBudget()); + payload.put("c_major", contract.getIsMajorContract()); + payload.put("c_signMethod", contract.getSealType()); + payload.put("c_creator", contract.getInitiator()); + payload.put("c_creatorPhone", contract.getCreatorPhone()); + payload.put("c_createDept", contract.getInitiatorDept()); + payload.put("c_zw", buildFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_BODY)); + payload.put("c_attachs", buildFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_OTHER)); + payload.put("c_signAttachs", buildFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_SIGNED)); + payload.put("c_lxPlan", contract.getPerformancePlans()); + Map seal = new LinkedHashMap<>(); + seal.put("seal_isSeal", "1"); + seal.put("seal_approveSame", "1"); + seal.put("seal_type", contract.getSealType()); + seal.put("seal_date", contract.getSignDate()); + payload.put("c_seal", seal); + Map sign = new LinkedHashMap<>(); + sign.put("sign_isSign", "1"); + sign.put("sign_date", contract.getSignDate()); + sign.put("sign_activeDate", contract.getActiveDate()); + sign.put("sign_attachs", buildFilePayloadList(contract, ContractLaunchConstants.FILE_CATEGORY_SIGNED)); + sign.put("sign_executor", contract.getExecutorAccount()); + payload.put("c_sign", sign); + return payload; + } + + /** 构建归档报文(pushArchive 接口的 payload) */ + private Map buildArchivePayload(ContractLaunchDto contract, ContractLaunchArchiveDto archiveDto) { + Map payload = new LinkedHashMap<>(); + payload.put("contractCode", contract.getLawContractCode()); + payload.put("contractSysId", contract.getContractSysId()); + payload.put("Keeper", archiveDto == null ? null : archiveDto.getArchiveKeeper()); + payload.put("KeeperUnit", archiveDto == null ? null : archiveDto.getArchiveKeeperUnit()); + payload.put("KeeperUnitCode", archiveDto == null ? null : archiveDto.getArchiveKeeperUnitCode()); + payload.put("archivePapers", List.of()); + payload.put("archiveEles", (archiveDto == null || archiveDto.getArchiveFiles() == null) ? List.of() : archiveDto.getArchiveFiles()); + return payload; + } + + /** 按 fileCategory 提取文件列表,转为法务接口要求的附件格式 */ + private List> buildFilePayloadList(ContractLaunchDto contract, String fileCategory) { + return contract.getFileList().stream() + .filter(item -> fileCategory.equals(item.getFileCategory())) + .map(item -> { + Map map = new LinkedHashMap<>(); + map.put("fileName", item.getFileName()); + map.put("filePath", item.getFilePath()); + map.put("fileSize", item.getFileSize()); + map.put("fileType", item.getFileType()); + return map; + }).collect(Collectors.toList()); + } + + private String trim(String value) { + return value == null ? null : value.trim(); + } +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchCodeUtils.java b/src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchCodeUtils.java new file mode 100644 index 0000000..c8358a5 --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchCodeUtils.java @@ -0,0 +1,42 @@ +package com.nbport.zgwl.contractlaunch.utils; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 合同编号工具 + * + * contractSysId:本系统合同唯一标识,格式 CLS-yyyyMMddHHmmss-随机4位 + * contractCode:业务合同编号,格式 合同类型编码-yyyyMMdd-随机4位(如 MM-20260522-1234) + */ +public final class ContractLaunchCodeUtils { + + private ContractLaunchCodeUtils() { + } + + /** 生成合同系统唯一编码(CLS-时间戳-随机4位) */ + public static String buildContractSysId() { + return "CLS-" + formatNow("yyyyMMddHHmmss") + "-" + randomDigits(4); + } + + /** 生成业务合同编号(合同类型编码-日期-随机4位,如 MM-20260522-1234) */ + public static String buildContractCode(String contractTypeCode) { + String typeCode = contractTypeCode == null || contractTypeCode.isBlank() + ? "QT" + : contractTypeCode; + return typeCode + "-" + formatNow("yyyyMMdd") + "-" + randomDigits(4); + } + + private static String formatNow(String pattern) { + return new SimpleDateFormat(pattern).format(new Date()); + } + + private static String randomDigits(int length) { + StringBuilder builder = new StringBuilder(); + for (int i = 0; i < length; i++) { + builder.append(ThreadLocalRandom.current().nextInt(10)); + } + return builder.toString(); + } +} diff --git a/src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchConstants.java b/src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchConstants.java new file mode 100644 index 0000000..fd60d2c --- /dev/null +++ b/src/main/java/com/nbport/zgwl/contractlaunch/utils/ContractLaunchConstants.java @@ -0,0 +1,51 @@ +package com.nbport.zgwl.contractlaunch.utils; + +/** + * 合同发起模块常量 + * + * 这里把状态值集中放一起,方便你后续改状态文案或补真实流程。 + */ +public final class ContractLaunchConstants { + + private ContractLaunchConstants() { + } + + public static final String REQUEST_PREFIX = "/contract/launch"; + + public static final String STATUS_DRAFT = "draft"; + public static final String STATUS_REVIEWING = "reviewing"; + public static final String STATUS_REJECTED = "rejected"; + public static final String STATUS_PENDING_SIGN = "pending_sign"; + public static final String STATUS_SIGNING = "signing"; + public static final String STATUS_PENDING_COLLECT = "pending_collect"; + public static final String STATUS_PENDING_ARCHIVE = "pending_archive"; + public static final String STATUS_COMPLETED = "completed"; + public static final String STATUS_TERMINATED = "terminated"; + + public static final String APPROVE_STATUS_PENDING = "pending"; + public static final String APPROVE_STATUS_APPROVED = "approved"; + public static final String APPROVE_STATUS_REJECTED = "rejected"; + + public static final String FILE_CATEGORY_BODY = "body"; + public static final String FILE_CATEGORY_OTHER = "other"; + public static final String FILE_CATEGORY_GIST = "gist"; + public static final String FILE_CATEGORY_SIGNED = "signed"; + public static final String FILE_CATEGORY_ARCHIVE = "archive"; + + public static final String ACTION_SAVE = "save"; + public static final String ACTION_SUBMIT = "submit"; + public static final String ACTION_APPROVE = "approve"; + public static final String ACTION_REJECT = "reject"; + public static final String ACTION_SEAL_START = "seal_start"; + public static final String ACTION_SEAL_FINISH = "seal_finish"; + public static final String ACTION_COLLECT = "collect"; + public static final String ACTION_ARCHIVE = "archive"; + public static final String ACTION_DELETE = "delete"; + + public static final String ACTION_STATUS_SUCCESS = "success"; + public static final String ACTION_STATUS_FAILED = "failed"; + public static final String ACTION_STATUS_PENDING = "pending"; + + public static final String IS_DELETED_NO = "N"; + public static final String IS_DELETED_YES = "Y"; +} diff --git a/src/main/java/com/nbport/zgwl/entity/SysOrgEntity.java b/src/main/java/com/nbport/zgwl/entity/SysOrgEntity.java new file mode 100644 index 0000000..404c71c --- /dev/null +++ b/src/main/java/com/nbport/zgwl/entity/SysOrgEntity.java @@ -0,0 +1,25 @@ +package com.nbport.zgwl.entity; + +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; + +/** + * 组织信息 + * + * 当前项目里先作为一个简单对象使用, + * 后续接入真实权限系统时,可以直接替换成真实组织实体。 + */ +@Data +public class SysOrgEntity implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** 组织ID */ + private String id; + + /** 组织名称 */ + private String orgName; +} diff --git a/src/main/java/com/nbport/zgwl/entity/SysUserEntity.java b/src/main/java/com/nbport/zgwl/entity/SysUserEntity.java index 6577d67..6e11b6e 100644 --- a/src/main/java/com/nbport/zgwl/entity/SysUserEntity.java +++ b/src/main/java/com/nbport/zgwl/entity/SysUserEntity.java @@ -28,4 +28,12 @@ public class SysUserEntity extends BaseEntity { @TableField("email") private String email; + + /** + * 当前登录人的所属组织 + * + * 这里不映射数据库字段,只用于业务代码里取组织信息。 + */ + @TableField(exist = false) + private SysOrgEntity sysOrgEntity; } diff --git a/src/main/java/com/nbport/zgwl/manifest/utils/ManifestCodeUtils.java b/src/main/java/com/nbport/zgwl/manifest/utils/ManifestCodeUtils.java index 754ccb6..aee943f 100644 --- a/src/main/java/com/nbport/zgwl/manifest/utils/ManifestCodeUtils.java +++ b/src/main/java/com/nbport/zgwl/manifest/utils/ManifestCodeUtils.java @@ -16,6 +16,38 @@ import java.util.concurrent.ThreadLocalRandom; public final class ManifestCodeUtils { private static final Map CONTRACT_TYPE_CODE_MAP = Map.ofEntries( + Map.entry("MM", "MM"), + Map.entry("GY", "GY"), + Map.entry("ZY", "ZY"), + Map.entry("JK", "JK"), + Map.entry("BZ", "BZ"), + Map.entry("ZL", "ZL"), + Map.entry("RZ", "RZ"), + Map.entry("BL", "BL"), + Map.entry("CL", "CL"), + Map.entry("GC", "GC"), + Map.entry("YS", "YS"), + Map.entry("JS", "JS"), + Map.entry("BG", "BG"), + Map.entry("CC", "CC"), + Map.entry("WT", "WT"), + Map.entry("WY", "WY"), + Map.entry("HJ", "HJ"), + Map.entry("ZJ", "ZJ"), + Map.entry("HH", "HH"), + Map.entry("YG", "YG"), + Map.entry("YW", "YW"), + Map.entry("QT", "QT"), + Map.entry("YWBFP", "YWBFP"), + Map.entry("YWBFS", "YWBFS"), + Map.entry("YWIOP", "YWIOP"), + Map.entry("YWIOS", "YWIOS"), + Map.entry("YWDOP", "YWDOP"), + Map.entry("YWDOS", "YWDOS"), + Map.entry("YWBCP", "YWBCP"), + Map.entry("YWBCS", "YWBCS"), + Map.entry("YWOTP", "YWOTP"), + Map.entry("YWOTS", "YWOTS"), Map.entry("买卖合同", "MM"), Map.entry("采购合同", "CG"), Map.entry("销售合同", "XS"), diff --git a/src/main/java/com/nbport/zgwl/utils/AdminSecurityManage.java b/src/main/java/com/nbport/zgwl/utils/AdminSecurityManage.java new file mode 100644 index 0000000..fab800f --- /dev/null +++ b/src/main/java/com/nbport/zgwl/utils/AdminSecurityManage.java @@ -0,0 +1,30 @@ +package com.nbport.zgwl.utils; + +import com.nbport.zgwl.entity.SysOrgEntity; +import com.nbport.zgwl.entity.SysUserEntity; +import org.springframework.stereotype.Component; + +/** + * 登录用户获取组件 + * + * 当前系统还没有真正接入统一认证, + * 所以这里先返回一个 mock 用户,便于业务代码先跑通。 + * 后续接入真实系统时,直接把这里替换成真正的登录态获取逻辑即可。 + */ +@Component +public class AdminSecurityManage { + + public SysUserEntity getUser() { + SysOrgEntity orgEntity = new SysOrgEntity(); + orgEntity.setId("ORG001"); + orgEntity.setOrgName("浙港物流平台有限公司"); + + SysUserEntity user = new SysUserEntity(); + user.setId(1L); + user.setUsername("admin"); + user.setMobilePhone("13800000000"); + user.setEmail("admin@test.com"); + user.setSysOrgEntity(orgEntity); + return user; + } +} diff --git a/src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml b/src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml new file mode 100644 index 0000000..b2cd5c0 --- /dev/null +++ b/src/main/resources/mapper/contractlaunch/ContractLaunchMapper.xml @@ -0,0 +1,532 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + select ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID, + CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE, + TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE, + SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, CONTRACT_AMOUNT, CURRENCY_NAME, + CONTRACT_PERIOD_TYPE, PERIOD_EXPLAIN, EFFECTIVE_START, EFFECTIVE_END, + PAYMENT_METHOD, PRIMARY_CONTENT, AMOUNT_EXPLAIN, VALUATION_MODE, SEAL_TYPE, + TEXT_SOURCE, MANDATE_TYPE, CARGO_TYPE, ASSIST_DEPARTMENT_NAME, EXECUTOR_ACCOUNT, + CREATOR_PHONE, SELF_CODE, MAIN_CONTRACT_CODE, SIGNING_SUBJECT_CODE, DATE_TYPE, IS_ADDIT, IS_ELECTRON, + PAYMENT_DIRECTION, PUSH_URL, IS_MAJOR_CONTRACT, IS_OPEN_BIDDING, IS_YEAR_BUDGET, IS_SHARE, + CONTRACT_NATURE, IS_SGAT, IS_NORMAL, IS_TERMINATED, IS_SINGLE_AGREEMENT, + BIDDING_NO, OPEN_BIDDING_REMARK, CONTRACT_GIST_REMARK, + EW_AMOUNT_TYPE, DJ_AMOUNT, YJ_AMOUNT, YFK_AMOUNT, BZJ_AMOUNT, BODY_SUMMARY, + CONTRACT_TEXT_FILE_NAME, CONTRACT_TEXT_FILE_PATH, CONTRACT_TEXT_FILE_TYPE, CONTRACT_TEXT_FILE_SIZE, + SIGNED_FILE_NAME, SIGNED_FILE_PATH, SIGNED_FILE_TYPE, SIGNED_FILE_SIZE, SIGNED_OFFLINE_REASON, + SIGN_DATE, ACTIVE_DATE, ARCHIVE_DATE, ARCHIVE_KEEPER, ARCHIVE_KEEPER_UNIT, + ARCHIVE_KEEPER_UNIT_CODE, INVOICE_DATE, RECEIVE_DATE, REMIND_DAYS, + INITIATOR, INITIATOR_DEPT, COMPANY_NAME, ORG_ID, QUERY_SCOPE, + CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK + from SEAL_CONTRACT_LAUNCH_MAIN + + + + + + + + + + + + + + + + insert into SEAL_CONTRACT_LAUNCH_MAIN ( + ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID, + CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE, + TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE, + SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, CONTRACT_AMOUNT, CURRENCY_NAME, + CONTRACT_PERIOD_TYPE, PERIOD_EXPLAIN, EFFECTIVE_START, EFFECTIVE_END, + PAYMENT_METHOD, PRIMARY_CONTENT, AMOUNT_EXPLAIN, VALUATION_MODE, SEAL_TYPE, + TEXT_SOURCE, MANDATE_TYPE, CARGO_TYPE, ASSIST_DEPARTMENT_NAME, EXECUTOR_ACCOUNT, + CREATOR_PHONE, SELF_CODE, MAIN_CONTRACT_CODE, SIGNING_SUBJECT_CODE, DATE_TYPE, IS_ADDIT, IS_ELECTRON, + PAYMENT_DIRECTION, PUSH_URL, IS_MAJOR_CONTRACT, IS_OPEN_BIDDING, IS_YEAR_BUDGET, IS_SHARE, + CONTRACT_NATURE, IS_SGAT, IS_NORMAL, IS_TERMINATED, IS_SINGLE_AGREEMENT, + BIDDING_NO, OPEN_BIDDING_REMARK, CONTRACT_GIST_REMARK, + EW_AMOUNT_TYPE, DJ_AMOUNT, YJ_AMOUNT, YFK_AMOUNT, BZJ_AMOUNT, BODY_SUMMARY, + CONTRACT_TEXT_FILE_NAME, CONTRACT_TEXT_FILE_PATH, CONTRACT_TEXT_FILE_TYPE, CONTRACT_TEXT_FILE_SIZE, + SIGNED_FILE_NAME, SIGNED_FILE_PATH, SIGNED_FILE_TYPE, SIGNED_FILE_SIZE, SIGNED_OFFLINE_REASON, + SIGN_DATE, ACTIVE_DATE, ARCHIVE_DATE, ARCHIVE_KEEPER, ARCHIVE_KEEPER_UNIT, + ARCHIVE_KEEPER_UNIT_CODE, INVOICE_DATE, RECEIVE_DATE, REMIND_DAYS, + INITIATOR, INITIATOR_DEPT, COMPANY_NAME, ORG_ID, QUERY_SCOPE, + CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK + ) values ( + #{id}, #{contractSysId}, #{contractCode}, #{lawContractCode}, #{lawContractId}, + #{contractName}, #{contractType}, #{contractTypeCode}, #{contractSource}, #{creationMode}, + #{templateId}, #{templateCode}, #{templateName}, #{status}, #{approveStatus}, #{approveMessage}, + #{sealContractId}, #{sealContractStatus}, #{contractAmount}, #{currencyName}, + #{contractPeriodType}, #{periodExplain}, #{effectiveStart}, #{effectiveEnd}, + #{paymentMethod}, #{primaryContent}, #{amountExplain}, #{valuationMode}, #{sealType}, + #{textSource}, #{mandateType}, #{cargoType}, #{assistDepartmentName}, #{executorAccount}, + #{creatorPhone}, #{selfCode}, #{mainContractCode}, #{signingSubjectCode}, #{dateType}, #{isAddit}, #{isElectron}, + #{paymentDirection}, #{pushUrl}, #{isMajorContract}, #{isOpenBidding}, #{isYearBudget}, #{isShare}, + #{contractNature}, #{isSgat}, #{isNormal}, #{isTerminated}, #{isSingleAgreement}, + #{biddingNo}, #{openBiddingRemark}, #{contractGistRemark}, + #{ewAmountType}, #{djAmount}, #{yjAmount}, #{yfkAmount}, #{bzjAmount}, #{bodySummary}, + #{contractTextFileName}, #{contractTextFilePath}, #{contractTextFileType}, #{contractTextFileSize}, + #{signedFileName}, #{signedFilePath}, #{signedFileType}, #{signedFileSize}, #{signedOfflineReason}, + #{signDate}, #{activeDate}, #{archiveDate}, #{archiveKeeper}, #{archiveKeeperUnit}, + #{archiveKeeperUnitCode}, #{invoiceDate}, #{receiveDate}, #{remindDays}, + #{initiator}, #{initiatorDept}, #{companyName}, #{orgId}, #{queryScope}, + #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{isDeleted}, #{remark} + ) + + + + update SEAL_CONTRACT_LAUNCH_MAIN + + LAW_CONTRACT_CODE = #{lawContractCode}, + LAW_CONTRACT_ID = #{lawContractId}, + CONTRACT_NAME = #{contractName}, + CONTRACT_TYPE = #{contractType}, + CONTRACT_TYPE_CODE = #{contractTypeCode}, + CONTRACT_SOURCE = #{contractSource}, + CREATION_MODE = #{creationMode}, + TEMPLATE_ID = #{templateId}, + TEMPLATE_CODE = #{templateCode}, + TEMPLATE_NAME = #{templateName}, + STATUS = #{status}, + APPROVE_STATUS = #{approveStatus}, + APPROVE_MESSAGE = #{approveMessage}, + SEAL_CONTRACT_ID = #{sealContractId}, + SEAL_CONTRACT_STATUS = #{sealContractStatus}, + CONTRACT_AMOUNT = #{contractAmount}, + CURRENCY_NAME = #{currencyName}, + CONTRACT_PERIOD_TYPE = #{contractPeriodType}, + PERIOD_EXPLAIN = #{periodExplain}, + EFFECTIVE_START = #{effectiveStart}, + EFFECTIVE_END = #{effectiveEnd}, + PAYMENT_METHOD = #{paymentMethod}, + PRIMARY_CONTENT = #{primaryContent}, + AMOUNT_EXPLAIN = #{amountExplain}, + VALUATION_MODE = #{valuationMode}, + SEAL_TYPE = #{sealType}, + TEXT_SOURCE = #{textSource}, + MANDATE_TYPE = #{mandateType}, + CARGO_TYPE = #{cargoType}, + ASSIST_DEPARTMENT_NAME = #{assistDepartmentName}, + EXECUTOR_ACCOUNT = #{executorAccount}, + CREATOR_PHONE = #{creatorPhone}, + SELF_CODE = #{selfCode}, + MAIN_CONTRACT_CODE = #{mainContractCode}, + SIGNING_SUBJECT_CODE = #{signingSubjectCode}, + DATE_TYPE = #{dateType}, + IS_ADDIT = #{isAddit}, + IS_ELECTRON = #{isElectron}, + PAYMENT_DIRECTION = #{paymentDirection}, + PUSH_URL = #{pushUrl}, + IS_MAJOR_CONTRACT = #{isMajorContract}, + IS_OPEN_BIDDING = #{isOpenBidding}, + IS_YEAR_BUDGET = #{isYearBudget}, + IS_SHARE = #{isShare}, + CONTRACT_NATURE = #{contractNature}, + IS_SGAT = #{isSgat}, + IS_NORMAL = #{isNormal}, + IS_TERMINATED = #{isTerminated}, + IS_SINGLE_AGREEMENT = #{isSingleAgreement}, + BIDDING_NO = #{biddingNo}, + OPEN_BIDDING_REMARK = #{openBiddingRemark}, + CONTRACT_GIST_REMARK = #{contractGistRemark}, + EW_AMOUNT_TYPE = #{ewAmountType}, + DJ_AMOUNT = #{djAmount}, + YJ_AMOUNT = #{yjAmount}, + YFK_AMOUNT = #{yfkAmount}, + BZJ_AMOUNT = #{bzjAmount}, + BODY_SUMMARY = #{bodySummary}, + CONTRACT_TEXT_FILE_NAME = #{contractTextFileName}, + CONTRACT_TEXT_FILE_PATH = #{contractTextFilePath}, + CONTRACT_TEXT_FILE_TYPE = #{contractTextFileType}, + CONTRACT_TEXT_FILE_SIZE = #{contractTextFileSize}, + SIGNED_FILE_NAME = #{signedFileName}, + SIGNED_FILE_PATH = #{signedFilePath}, + SIGNED_FILE_TYPE = #{signedFileType}, + SIGNED_FILE_SIZE = #{signedFileSize}, + SIGNED_OFFLINE_REASON = #{signedOfflineReason}, + SIGN_DATE = #{signDate}, + ACTIVE_DATE = #{activeDate}, + ARCHIVE_DATE = #{archiveDate}, + ARCHIVE_KEEPER = #{archiveKeeper}, + ARCHIVE_KEEPER_UNIT = #{archiveKeeperUnit}, + ARCHIVE_KEEPER_UNIT_CODE = #{archiveKeeperUnitCode}, + INVOICE_DATE = #{invoiceDate}, + RECEIVE_DATE = #{receiveDate}, + REMIND_DAYS = #{remindDays}, + INITIATOR = #{initiator}, + INITIATOR_DEPT = #{initiatorDept}, + COMPANY_NAME = #{companyName}, + ORG_ID = #{orgId}, + QUERY_SCOPE = #{queryScope}, + UPDATE_BY = #{updateBy}, + UPDATE_TIME = #{updateTime}, + IS_DELETED = #{isDeleted}, + REMARK = #{remark}, + + where ID = #{id} + and IS_DELETED = 'N' + + + + update SEAL_CONTRACT_LAUNCH_MAIN + set IS_DELETED = 'Y', + UPDATE_BY = #{updateBy}, + UPDATE_TIME = SYSDATE + where ID = #{id} + and IS_DELETED = 'N' + + + + INSERT ALL + + INTO SEAL_CONTRACT_LAUNCH_PARTY ( + ID, CONTRACT_ID, ROLE_CODE, ROLE_NAME, SORT_ORDER, PARTNER_ID, OPPOSITE_ID, + COMPANY_NAME, COMPANY_TYPE, OPPOSITE_CHARACTER, CONTACT_NAME, CONTACT_PHONE, + CONTACT_EMAIL, CONTACT_ADDRESS, REGISTERED_ADDRESS, LEGAL_REPRESENTATIVE, + CREDIT_CODE, BANK_NAME, BANK_ACCOUNT, NATURAL_PERSON_ID_TYPE, NATURAL_PERSON_ID_NUMBER, + IS_GROUP_INTERNAL, IS_HONG_KONG_MACAO_TAIWAN, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK + ) VALUES ( + #{item.id}, #{item.contractId}, #{item.roleCode}, #{item.roleName}, #{item.sortOrder}, #{item.partnerId}, #{item.oppositeId}, + #{item.companyName}, #{item.companyType}, #{item.oppositeCharacter}, #{item.contactName}, #{item.contactPhone}, + #{item.contactEmail}, #{item.contactAddress}, #{item.registeredAddress}, #{item.legalRepresentative}, + #{item.creditCode}, #{item.bankName}, #{item.bankAccount}, #{item.naturalPersonIdType}, #{item.naturalPersonIdNumber}, + #{item.isGroupInternal}, #{item.isHongKongMacaoTaiwan}, #{createBy}, SYSDATE, #{updateBy}, SYSDATE, 'N', #{item.remark} + ) + + SELECT 1 FROM DUAL + + + + INSERT ALL + + INTO SEAL_CONTRACT_LAUNCH_FILE ( + ID, CONTRACT_ID, FILE_CATEGORY, FILE_NAME, FILE_PATH, FILE_TYPE, FILE_SIZE, + SORT_ORDER, CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK + ) VALUES ( + #{item.id}, #{item.contractId}, #{item.fileCategory}, #{item.fileName}, #{item.filePath}, #{item.fileType}, #{item.fileSize}, + #{item.sortOrder}, #{createBy}, SYSDATE, #{updateBy}, SYSDATE, 'N', #{item.remark} + ) + + SELECT 1 FROM DUAL + + + + INSERT ALL + + INTO SEAL_CONTRACT_LAUNCH_PLAN ( + ID, CONTRACT_ID, PLAN_MATTER, PLAN_WHERE, PLAN_TIME, PLAN_DAY, PLAN_MONEY, + PLAN_SUBJECT_CODE, PLAN_SUBJECT_NAME, PLAN_DETAIL, PLAN_STATUS, + CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK + ) VALUES ( + #{item.id}, #{item.contractId}, #{item.planMatter}, #{item.planWhere}, #{item.planTime}, #{item.planDay}, #{item.planMoney}, + #{item.planSubjectCode}, #{item.planSubjectName}, #{item.planDetail}, #{item.planStatus}, + #{createBy}, SYSDATE, #{updateBy}, SYSDATE, 'N', #{item.remark} + ) + + SELECT 1 FROM DUAL + + + + insert into SEAL_CONTRACT_LAUNCH_LOG ( + ID, CONTRACT_ID, ACTION_TYPE, ACTION_NAME, ACTION_STATUS, OPERATOR_NAME, OPERATOR_PHONE, + ACTION_TIME, ACTION_CONTENT, CALLBACK_PAYLOAD, REMARK + ) values ( + #{id}, #{contractId}, #{actionType}, #{actionName}, #{actionStatus}, #{operatorName}, #{operatorPhone}, + #{actionTime}, #{actionContent}, #{callbackPayload}, #{remark} + ) + + + + update SEAL_CONTRACT_LAUNCH_PARTY + set IS_DELETED = 'Y', + UPDATE_BY = #{updateBy}, + UPDATE_TIME = SYSDATE + where CONTRACT_ID = #{contractId} + and IS_DELETED = 'N' + + + + update SEAL_CONTRACT_LAUNCH_FILE + set IS_DELETED = 'Y', + UPDATE_BY = #{updateBy}, + UPDATE_TIME = SYSDATE + where CONTRACT_ID = #{contractId} + and IS_DELETED = 'N' + + + + update SEAL_CONTRACT_LAUNCH_FILE + set IS_DELETED = 'Y', + UPDATE_BY = #{updateBy}, + UPDATE_TIME = SYSDATE + where CONTRACT_ID = #{contractId} + and FILE_CATEGORY = #{fileCategory} + and IS_DELETED = 'N' + + + + update SEAL_CONTRACT_LAUNCH_PLAN + set IS_DELETED = 'Y', + UPDATE_BY = #{updateBy}, + UPDATE_TIME = SYSDATE + where CONTRACT_ID = #{contractId} + and IS_DELETED = 'N' + + + + + + + + + + + diff --git a/src/main/resources/mapper/manifest/ContractTemplateMapper.xml b/src/main/resources/mapper/manifest/ContractTemplateMapper.xml index e214ae7..db8246d 100644 --- a/src/main/resources/mapper/manifest/ContractTemplateMapper.xml +++ b/src/main/resources/mapper/manifest/ContractTemplateMapper.xml @@ -30,7 +30,7 @@ - select id, template_code, template_name, source, contract_type, self_party_role, + select id, template_code, template_name, source, contract_type, version, status, variable_count, description, usage_tip, template_file_name, template_file_path, publish_org_ids_json, publish_org_names_json, variable_mappings_json, @@ -73,7 +73,7 @@ insert into contract_template_manage ( - id, template_code, template_name, source, contract_type, self_party_role, + id, template_code, template_name, source, contract_type, version, status, variable_count, description, usage_tip, template_file_name, template_file_path, publish_org_ids_json, publish_org_names_json, variable_mappings_json, diff --git a/src/main/resources/mapper/partner/PartnerManageMapper.xml b/src/main/resources/mapper/partner/PartnerManageMapper.xml index 0807db2..55c7038 100644 --- a/src/main/resources/mapper/partner/PartnerManageMapper.xml +++ b/src/main/resources/mapper/partner/PartnerManageMapper.xml @@ -12,8 +12,14 @@ + + + + + + @@ -60,7 +66,8 @@ - select CUSTOMER_SYSID, CUSTOMER_NAME_C, CUSTOMER_TAXNO, CUSTOMER_REGADDR, + select CUSTOMER_SYSID, CUSTOMER_REGNO, CUSTOMER_NO, CUSTOMER_NAME_C, CUSTOMER_TAXNO, + CUSTOMER_ADDRESS, OFFICEPHONE, CUSTOMER_BANK, CUSTOMER_BANKACCOUNT, CUSTOMER_REGADDR, EMAIL_ADDRESS, OPPOSITE_ID, OPPOSITE_CHARACTER, GAT_FLAG, GROUP_FLAG, COUNTRY, COUNTRY_NAME, LEGAL_PERSON_NAME, CERTIFICATE_TYPE, CERTIFICATE_NUM, TIN_CODE, REGISTERED_CAPITAL, REG_CAPITAL, diff --git a/upload-files/20260529/2a478b9d74b3485ab696ef172cb5db3e.docx b/upload-files/20260529/2a478b9d74b3485ab696ef172cb5db3e.docx new file mode 100644 index 0000000000000000000000000000000000000000..8dbb93782d6f981a5ee0435f364e68bdbe40ad06 GIT binary patch literal 10324 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