签章位置固定功能

This commit is contained in:
2026-07-10 17:03:39 +08:00
parent f7b995ed3a
commit 0cce9d3acc
20 changed files with 1946 additions and 347 deletions
+50
View File
@@ -0,0 +1,50 @@
-- 合同签章位能力补丁
-- 执行前请先确认正式库对象名与本测试库一致。
ALTER TABLE SEAL_CONTRACT_LAUNCH_MAIN ADD (
SEAL_POS_CONFIGURED CHAR(1),
SEAL_PDF_IMAGE_LIST CLOB
);
COMMENT ON COLUMN SEAL_CONTRACT_LAUNCH_MAIN.SEAL_POS_CONFIGURED IS '签章位是否已配置(Y/N)';
COMMENT ON COLUMN SEAL_CONTRACT_LAUNCH_MAIN.SEAL_PDF_IMAGE_LIST IS '签章平台PDF转图片列表JSON';
CREATE TABLE SEAL_CONTRACT_LAUNCH_SEAL_POS (
ID NUMBER(20) PRIMARY KEY,
CONTRACT_ID NUMBER(20) NOT NULL,
PARTY_ID NUMBER(20) NOT NULL,
LABEL VARCHAR2(100) NOT NULL,
REQUIRED CHAR(1) DEFAULT 'Y' NOT NULL,
PAGE_NUM NUMBER(10) NOT NULL,
X NUMBER(12,2) NOT NULL,
Y NUMBER(12,2) NOT NULL,
WIDTH NUMBER(12,2) NOT NULL,
HEIGHT NUMBER(12,2) NOT NULL,
SEAL_ID VARCHAR2(100),
SEAL_NAME VARCHAR2(200),
REQUEST_ID VARCHAR2(100),
SIGNED_STATUS NUMBER(2) DEFAULT 0 NOT NULL,
SIGNED_BY_NAME VARCHAR2(100),
SIGNED_BY_ID VARCHAR2(100),
SIGNED_TIME TIMESTAMP,
CREATE_BY VARCHAR2(100),
CREATE_TIME TIMESTAMP DEFAULT SYSTIMESTAMP,
UPDATE_BY VARCHAR2(100),
UPDATE_TIME TIMESTAMP DEFAULT SYSTIMESTAMP,
IS_DELETED CHAR(1) DEFAULT 'N' NOT NULL,
REMARK VARCHAR2(500)
);
COMMENT ON TABLE SEAL_CONTRACT_LAUNCH_SEAL_POS IS '合同签章位配置表';
COMMENT ON COLUMN SEAL_CONTRACT_LAUNCH_SEAL_POS.PARTY_ID IS '签署方子表ID';
COMMENT ON COLUMN SEAL_CONTRACT_LAUNCH_SEAL_POS.REQUIRED IS '是否必签(Y/N)';
COMMENT ON COLUMN SEAL_CONTRACT_LAUNCH_SEAL_POS.REQUEST_ID IS '最近一次预盖章二维码请求ID';
COMMENT ON COLUMN SEAL_CONTRACT_LAUNCH_SEAL_POS.SIGNED_STATUS IS '0空框 1已选章 2待回调 3已签 4失败';
CREATE INDEX IDX_SC_LAUNCH_SEAL_POS_CONTRACT ON SEAL_CONTRACT_LAUNCH_SEAL_POS(CONTRACT_ID, IS_DELETED);
CREATE INDEX IDX_SC_LAUNCH_SEAL_POS_REQUEST ON SEAL_CONTRACT_LAUNCH_SEAL_POS(REQUEST_ID, IS_DELETED);
CREATE SEQUENCE SEQ_SEAL_CONTRACT_LAUNCH_SEAL_POS
START WITH 1
INCREMENT BY 1
NOCACHE;
@@ -6,11 +6,14 @@ import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchCollectSaveDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchCollectSaveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackRequestDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackRequestDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackSampleDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackSampleDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionBatchStatusDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionCheckResultDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionPageDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionSaveDto;
import com.nbport.zgwl.contractlaunch.service.ContractLaunchService; import com.nbport.zgwl.contractlaunch.service.ContractLaunchService;
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants; import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants;
import com.nbport.zgwl.entity.SysUserEntity; import com.nbport.zgwl.entity.SysUserEntity;
@@ -37,9 +40,6 @@ import java.util.Map;
* 2. 模拟合同采集 * 2. 模拟合同采集
* 3. 合同归档 * 3. 合同归档
* 4. 回写签章结果 * 4. 回写签章结果
*
* 这些测试接口在法务系统不可用期间便于你完整演示流程,
* 后续接入真实系统后,保留或删除都很方便。
*/ */
@RestController @RestController
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -49,299 +49,97 @@ public class ContractLaunchController {
private final ContractLaunchService contractLaunchService; private final ContractLaunchService contractLaunchService;
private final AdminSecurityManage adminSecurityManage; private final AdminSecurityManage adminSecurityManage;
/**
* 分页查询合同列表。
*
* 支持按合同名称、合同编号、合同类型、状态、创建时间范围过滤。
* 返回结果附带合同方摘要、文件列表、履行计划(不含日志,列表页日志量大)。
*
* 对应前端功能:合同列表页
*
* 请求体 ContractLaunchQueryDto 字段:
* contractName - 合同名称(模糊)
* contractCode - 合同编号
* contractType - 合同类型
* status - 状态(draft/reviewing/rejected/pending_sign/signing/...
* dateStart - 创建时间范围-开始(yyyy-MM-dd
* dateEnd - 创建时间范围-结束(yyyy-MM-dd
*/
@PostMapping("/list") @PostMapping("/list")
public R<PageResult<ContractLaunchDto>> queryList(@RequestBody ContractLaunchQueryDto queryDto) { public R<PageResult<ContractLaunchDto>> queryList(@RequestBody ContractLaunchQueryDto queryDto) {
return R.success(contractLaunchService.queryPage(queryDto)); return R.success(contractLaunchService.queryPage(queryDto));
} }
/**
* 查询合同详情(含所有子表数据)。
*
* 返回合同基本信息 + 合同方列表 + 文件列表 + 履行计划 + 操作日志。
*
* 对应前端功能:合同详情页 / 编辑页面回显
*/
@GetMapping("/{id}") @GetMapping("/{id}")
public R<ContractLaunchDto> getDetail(@PathVariable("id") Long id) { public R<ContractLaunchDto> getDetail(@PathVariable("id") Long id) {
return R.success(contractLaunchService.getDetail(id)); return R.success(contractLaunchService.getDetail(id));
} }
/**
* 获取当前登录人的我方组织信息(当前先返回 mock 数据)。
*
* 用途:
* 1. 发起合同页面只读展示“我方”信息
* 2. 后续接入真实组织主数据时,前端不需要再改字段来源
*/
@GetMapping("/self-profile") @GetMapping("/self-profile")
public R<Object> getSelfProfile() { public R<Object> getSelfProfile() {
SysUserEntity currentUser = adminSecurityManage.getUser(); SysUserEntity currentUser = adminSecurityManage.getUser();
return R.success(currentUser == null ? null : currentUser.getSysOrgEntity()); return R.success(currentUser == null ? null : currentUser.getSysOrgEntity());
} }
/**
* 创建合同草稿。
*
* 流程:生成 contractSysIdCLS-xxx)和 contractCode → 保存主表 → 保存子表(合同方/文件/履行计划)→ 记录操作日志
* 状态变为:draft(草稿)
*
* 必填校验项:合同名称、合同类型、合同类型编码、计价方式、币种、签订方式、合同期限类型、至少一个甲方+乙方
*
* 对应前端功能:新建合同 → 保存草稿
*/
@PostMapping @PostMapping
public R<ContractLaunchDto> createDraft(@RequestBody ContractLaunchDto contract) { public R<ContractLaunchDto> createDraft(@RequestBody ContractLaunchDto contract) {
return R.success(contractLaunchService.createDraft(contract)); return R.success(contractLaunchService.createDraft(contract));
} }
/**
* 编辑合同草稿。
*
* 仅 draft(草稿)或 rejected(已驳回)状态允许编辑。
* 逻辑:先删子表旧数据,再以当前数据重新插入(替换式更新)。
* 保留原有的 contractSysId、contractCode、创建人/创建时间 不变。
*
* 对应前端功能:编辑草稿 → 保存
*/
@PutMapping @PutMapping
public R<ContractLaunchDto> updateDraft(@RequestBody ContractLaunchDto contract) { public R<ContractLaunchDto> updateDraft(@RequestBody ContractLaunchDto contract) {
return R.success(contractLaunchService.updateDraft(contract)); return R.success(contractLaunchService.updateDraft(contract));
} }
/**
* 复制合同并生成新草稿。
*
* 业务设计:
* 1. 允许从任意未删除合同复制出一份新的草稿,方便高频复用历史合同
* 2. 保留主信息、合同方、正文附件、履行计划等业务内容
* 3. 重置系统编号、合同编号、审批/签署/归档状态与相关痕迹,避免串流程数据
*
* 对应前端功能:列表页 → 复制合同
*/
@PostMapping("/{id}/copy") @PostMapping("/{id}/copy")
public R<ContractLaunchDto> copyDraft(@PathVariable("id") Long id) { public R<ContractLaunchDto> copyDraft(@PathVariable("id") Long id) {
return R.success(contractLaunchService.copyDraft(id)); return R.success(contractLaunchService.copyDraft(id));
} }
/**
* 基于现有合同发起变更/附加合同草稿。
*
* 处理原则:
* 1. 复制现有业务内容
* 2. 自动关联主合同编码 mainContractCode
* 3. 生成新的草稿,供业务在此基础上做变更补充
*/
@PostMapping("/{id}/change") @PostMapping("/{id}/change")
public R<ContractLaunchDto> createChangeDraft(@PathVariable("id") Long id) { public R<ContractLaunchDto> createChangeDraft(@PathVariable("id") Long id) {
return R.success(contractLaunchService.createChangeDraft(id)); return R.success(contractLaunchService.createChangeDraft(id));
} }
/**
* 提交法务审批。
*
* 状态流转:draft/rejected → reviewing
* 调用结算平台 pushContract 接口将合同数据推送给法务系统开始审批流程。
* 目前法务接口不可用,仅做状态变更 + 记录日志(TODO:联调时打开 settlementPushService.pushContract 调用)。
*
* 推送法务的报文包含:合同基本字段 + contractOpposites(相对方) + contractKxPlans(履行计划) + 附件等
*
* 对应前端功能:提交审批按钮
*/
@PostMapping("/{id}/submit") @PostMapping("/{id}/submit")
public R<ContractLaunchDto> submitToLaw(@PathVariable("id") Long id) { public R<ContractLaunchDto> submitToLaw(@PathVariable("id") Long id) {
return R.success(contractLaunchService.submitToLaw(id)); return R.success(contractLaunchService.submitToLaw(id));
} }
/**
* 【测试按钮】模拟法务审批通过。
*
* 状态流转:reviewing → pending_sign(待签署)
* 生成模拟的 lawContractCode 和 lawContractId 记录到合同上,以便后续流程使用。
* 可在请求体中传入 approveMessage(审批意见)、lawContractCode、lawContractId 覆盖自动生成的值。
*
* 真实场景下,此方法应由法务系统的回调接口替代(见 lawApproveCallback)。
*
* 对应前端功能:测试按钮 → 模拟审批通过
*/
@PostMapping("/{id}/mock-approve") @PostMapping("/{id}/mock-approve")
public R<ContractLaunchDto> mockApprove(@PathVariable("id") Long id, public R<ContractLaunchDto> mockApprove(@PathVariable("id") Long id,
@RequestBody(required = false) ContractLaunchApproveDto approveDto) { @RequestBody(required = false) ContractLaunchApproveDto approveDto) {
return R.success(contractLaunchService.mockApprove(id, approveDto)); return R.success(contractLaunchService.mockApprove(id, approveDto));
} }
/**
* 【测试按钮】模拟法务审批驳回。
*
* 状态流转:reviewing → rejected(已驳回)
* 可在请求体中传入 approveMessage(驳回原因)。
* 驳回后合同可再次编辑和提交。
*
* 对应前端功能:测试按钮 → 模拟驳回
*/
@PostMapping("/{id}/mock-reject") @PostMapping("/{id}/mock-reject")
public R<ContractLaunchDto> mockReject(@PathVariable("id") Long id, public R<ContractLaunchDto> mockReject(@PathVariable("id") Long id,
@RequestBody(required = false) ContractLaunchApproveDto approveDto) { @RequestBody(required = false) ContractLaunchApproveDto approveDto) {
return R.success(contractLaunchService.mockReject(id, approveDto)); return R.success(contractLaunchService.mockReject(id, approveDto));
} }
/**
* 获取法务审批回调样例。
*
* 用途:
* 1. 前端展示与法务协商的固定 data 结构
* 2. 本地联调时复制到 Postman,直接调用 callback 接口
*
* 说明:
* - 样例中的水印文件路径当前先复用本地合同正文路径
* - 后续法务真正实现文件传输后,只需要替换 watermarkFile.filePath 来源
*/
@GetMapping("/{id}/callback/law/approve/sample") @GetMapping("/{id}/callback/law/approve/sample")
public R<ContractLaunchLawCallbackSampleDto> getLawApproveCallbackSample(@PathVariable("id") Long id) { public R<ContractLaunchLawCallbackSampleDto> getLawApproveCallbackSample(@PathVariable("id") Long id) {
return R.success(contractLaunchService.buildLawApproveCallbackSample(id)); return R.success(contractLaunchService.buildLawApproveCallbackSample(id));
} }
/**
* 法务审批结果回调入口(供法务系统调用)。
*
* submitToLaw 时推给法务的 pushUrl 默认指向此接口:POST /contract/launch/callback/law/approve
* 法务审批完成后,会往 pushUrl POST 审批结果。
*
* 当前约定请求体结构固定为:
* {
* "data": {
* "contractSysId": "...",
* "approveStatus": "approved|rejected",
* ...
* }
* }
*
* 这样前后端、法务、Postman 联调都统一只看 data 这一套结构,
* 不再做多字段兼容判断,便于后续正式协商定稿。
*
* 状态流转:reviewing → pending_sign(通过) / rejected(驳回)
*
* 对应前端功能:法务系统审批完成后自动回调(目前用法务系统暂未对接)
*/
@PostMapping("/callback/law/approve") @PostMapping("/callback/law/approve")
public R<ContractLaunchDto> lawApproveCallback(@RequestBody ContractLaunchLawCallbackRequestDto callbackRequest) { public R<ContractLaunchDto> lawApproveCallback(@RequestBody ContractLaunchLawCallbackRequestDto callbackRequest) {
return R.success(contractLaunchService.handleLawApproveCallback(callbackRequest == null ? null : callbackRequest.getData())); return R.success(contractLaunchService.handleLawApproveCallback(callbackRequest == null ? null : callbackRequest.getData()));
} }
/**
* 签署发起回调(浙江CA安全签调用)。
*
* 在浙江CA安全签平台创建签署合同后,回写 sealContractId 到本系统合同。
* 状态流转:pending_sign → signing(签署中)
*
* 请求体 ContractLaunchSealDto 字段:
* sealContractId - 签章平台生成的合同ID(必填)
* sealContractStatus - 签署状态(1:签署中)
*
* 对应前端功能:调起浙江CA签署后,回写签章合同ID
*/
@PostMapping("/{id}/seal/save") @PostMapping("/{id}/seal/save")
public R<ContractLaunchDto> saveSealInfo(@PathVariable("id") Long id, public R<ContractLaunchDto> saveSealInfo(@PathVariable("id") Long id,
@RequestBody ContractLaunchSealDto sealDto) { @RequestBody ContractLaunchSealDto sealDto) {
return R.success(contractLaunchService.saveSealInfo(id, 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") @PostMapping("/{id}/seal/finish")
public R<ContractLaunchDto> finishSeal(@PathVariable("id") Long id, public R<ContractLaunchDto> finishSeal(@PathVariable("id") Long id,
@RequestBody ContractLaunchSealDto sealDto) { @RequestBody ContractLaunchSealDto sealDto) {
return R.success(contractLaunchService.markSealCompleted(id, 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") @PostMapping("/{id}/mock-collect")
public R<ContractLaunchDto> mockCollect(@PathVariable("id") Long id) { public R<ContractLaunchDto> mockCollect(@PathVariable("id") Long id) {
return R.success(contractLaunchService.mockCollect(id)); return R.success(contractLaunchService.mockCollect(id));
} }
/**
* 保存采集信息(用印信息 + 签订信息)。
*
* 签署完成后,在「合同采集」阶段(pending_collect),
* 用户通过采集弹窗补充/修改用印信息和签订信息后保存到合同记录。
* 与 mock-collect 分开,允许用户在触发采集前先保存草稿。
*
* 请求体 ContractLaunchCollectSaveDto 字段:
* sealIsSeal - 是否用印
* sealApproveSame - 用印和审批文本是否一致
* sealQuantity - 用印数量
* sealDate - 用印时间
* signDate - 签订日期
* activeDate - 合同生效日期
* executorAccount - 合同履行执行人
* signedFileName - 签署文件名
* signedFilePath - 签署文件路径
* signedFileType - 签署文件类型
* signedFileSize - 签署文件大小
*/
@PostMapping("/{id}/collect/save") @PostMapping("/{id}/collect/save")
public R<ContractLaunchDto> saveCollectInfo(@PathVariable("id") Long id, public R<ContractLaunchDto> saveCollectInfo(@PathVariable("id") Long id,
@RequestBody ContractLaunchCollectSaveDto collectDto) { @RequestBody ContractLaunchCollectSaveDto collectDto) {
return R.success(contractLaunchService.saveCollectInfo(id, collectDto)); return R.success(contractLaunchService.saveCollectInfo(id, collectDto));
} }
/**
* 获取相对方签署确认状态。
*/
@GetMapping("/{id}/counterparty-confirm") @GetMapping("/{id}/counterparty-confirm")
public R<Map<String, Object>> getCounterpartyConfirm(@PathVariable("id") Long id) { public R<Map<String, Object>> getCounterpartyConfirm(@PathVariable("id") Long id) {
return R.success(contractLaunchService.getCounterpartyConfirmFiltered(id)); return R.success(contractLaunchService.getCounterpartyConfirmFiltered(id));
} }
/**
* 相对方确认已签署完成。
*
* 请求体:{"partyId": 123}
* 后端自动获取当前登录用户信息写入确认记录。
*/
@PostMapping("/{id}/counterparty-confirm") @PostMapping("/{id}/counterparty-confirm")
public R<Map<String, Object>> confirmCounterparty(@PathVariable("id") Long id, public R<Map<String, Object>> confirmCounterparty(@PathVariable("id") Long id,
@RequestBody Map<String, String> body) { @RequestBody Map<String, String> body) {
@@ -356,53 +154,51 @@ public class ContractLaunchController {
} }
} }
/** @PostMapping("/{id}/seal-position/init")
* 上传法务水印合同文件。 public R<ContractLaunchSealPositionPageDto> initSealPosition(@PathVariable("id") Long id) {
* return R.success(contractLaunchService.initSealPosition(id));
* 当法务审批回调未返回带水印的合同文件时,允许用户手动上传。 }
* 上传后的文件路径替换 contractTextFilePath,供后续在线签订使用。
* @GetMapping("/{id}/seal-position/list")
* 请求方式:multipart/form-data public R<ContractLaunchSealPositionPageDto> getSealPositionPage(@PathVariable("id") Long id) {
* 参数:fileMultipartFile return R.success(contractLaunchService.getSealPositionPage(id));
* }
* 对应前端功能:签署准备页面的"上传带水印合同"区域
*/ @PostMapping("/{id}/seal-position/save")
public R<ContractLaunchSealPositionPageDto> saveSealPositions(@PathVariable("id") Long id,
@RequestBody(required = false) ContractLaunchSealPositionSaveDto saveDto) {
return R.success(contractLaunchService.saveSealPositions(id, saveDto == null ? new ContractLaunchSealPositionSaveDto() : saveDto));
}
@PostMapping("/{id}/seal-position/batch-status")
public R<ContractLaunchSealPositionPageDto> batchUpdateSealPositionStatus(@PathVariable("id") Long id,
@RequestBody ContractLaunchSealPositionBatchStatusDto batchDto) {
return R.success(contractLaunchService.batchUpdateSealPositionStatus(id, batchDto));
}
@PostMapping("/{id}/seal-position/check")
public R<ContractLaunchSealPositionCheckResultDto> checkSealPositions(@PathVariable("id") Long id,
@RequestBody(required = false) Map<String, Object> body) {
Long partyId = null;
Object partyIdValue = body == null ? null : body.get("partyId");
if (partyIdValue != null && !String.valueOf(partyIdValue).isBlank()) {
partyId = Long.valueOf(String.valueOf(partyIdValue));
}
return R.success(contractLaunchService.checkSealPositions(id, partyId));
}
@PostMapping("/{id}/watermark/upload") @PostMapping("/{id}/watermark/upload")
public R<ContractLaunchDto> uploadWatermarkFile(@PathVariable("id") Long id, public R<ContractLaunchDto> uploadWatermarkFile(@PathVariable("id") Long id,
@RequestParam("file") MultipartFile file) { @RequestParam("file") MultipartFile file) {
return R.success(contractLaunchService.uploadWatermarkFile(id, file)); return R.success(contractLaunchService.uploadWatermarkFile(id, file));
} }
/**
* 合同归档。
*
* 合同全生命周期最后一步,状态变为 completed(已完成)。
* 同时将归档信息按数据采集 V3.0 的合同归档接口推送到法务系统存档
* TODO:联调时打开 dataCollectionPushService.collectContractArchive 调用)。
*
* 请求体 ContractLaunchArchiveDto 字段:
* archiveReceiveDept - 合同归档接收部门编码
* archiveTransferor - 移交人账号
* archivePapers - 纸质档案材料列表
* archiveEles - 电子档案材料列表(含 approvalAttachs
*
* 归档附件自动保存到 file 子表(fileCategory = "archive"),
* 纸质/电子档案明细分别保存到独立子表。
*
* 对应前端功能:归档按钮 → 合同完结
*/
@PostMapping("/{id}/archive") @PostMapping("/{id}/archive")
public R<ContractLaunchDto> archive(@PathVariable("id") Long id, public R<ContractLaunchDto> archive(@PathVariable("id") Long id,
@RequestBody(required = false) ContractLaunchArchiveDto archiveDto) { @RequestBody(required = false) ContractLaunchArchiveDto archiveDto) {
return R.success(contractLaunchService.archive(id, archiveDto)); return R.success(contractLaunchService.archive(id, archiveDto));
} }
/**
* 合同终止。
*
* 允许除 completed 外的任意环节执行终止。
* 当前仅在本系统内变更状态并记录日志,法务系统终止需后续分别操作。
*/
@PostMapping("/{id}/terminate") @PostMapping("/{id}/terminate")
public R<ContractLaunchDto> terminate(@PathVariable("id") Long id, public R<ContractLaunchDto> terminate(@PathVariable("id") Long id,
@RequestBody(required = false) Map<String, Object> payload) { @RequestBody(required = false) Map<String, Object> payload) {
@@ -411,14 +207,6 @@ public class ContractLaunchController {
return R.success(contractLaunchService.terminate(id, terminateReason)); return R.success(contractLaunchService.terminate(id, terminateReason));
} }
/**
* 删除合同草稿(逻辑删除)。
*
* 仅 draft(草稿)或 rejected(已驳回)状态允许删除。
* 逻辑:设置 isDeleted = 'Y',不物理删除数据。
*
* 对应前端功能:删除草稿按钮
*/
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
public R<String> deleteDraft(@PathVariable("id") Long id) { public R<String> deleteDraft(@PathVariable("id") Long id) {
contractLaunchService.deleteDraft(id); contractLaunchService.deleteDraft(id);
@@ -84,6 +84,12 @@ public class ContractLaunchDto implements Serializable {
/** 签章平台合同ID(浙江CA安全签创建后返回) */ /** 签章平台合同ID(浙江CA安全签创建后返回) */
private String sealContractId; private String sealContractId;
/** 签章位是否已配置(Y/N)。 */
private String sealPosConfigured;
/** PDF 预览图片地址列表 JSON。 */
private String sealPdfImageList;
/** 签章状态(1:签署中, 2:签署完成, 3:签署失败, null:未发起) */ /** 签章状态(1:签署中, 2:签署完成, 3:签署失败, null:未发起) */
private Integer sealContractStatus; private Integer sealContractStatus;
@@ -402,4 +408,7 @@ public class ContractLaunchDto implements Serializable {
/** 操作日志列表 */ /** 操作日志列表 */
private List<ContractLaunchLogDto> logList = new ArrayList<>(); private List<ContractLaunchLogDto> logList = new ArrayList<>();
/** 当前合同的签章位列表。 */
private List<ContractLaunchSealPositionDto> sealPositionList = new ArrayList<>();
} }
@@ -0,0 +1,45 @@
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 ContractLaunchSealPositionBatchStatusDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 当前操作代表哪一方签。 */
private Long partyId;
/** 目标状态:0空框 1已选章 2待回调 3已签 4失败。 */
private Integer signedStatus;
private String requestId;
private String signedByName;
private String signedById;
private List<Item> items = new ArrayList<>();
@Data
public static class Item implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
private String sealId;
private String sealName;
}
}
@@ -0,0 +1,24 @@
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 ContractLaunchSealPositionCheckResultDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private boolean passed;
private int missingCount;
private List<ContractLaunchSealPositionDto> missingPositions = new ArrayList<>();
}
@@ -0,0 +1,73 @@
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;
/**
* 合同签章位 DTO。
*
* 一份合同可配置多个签章位,
* 每个签章位绑定到具体合同方(partyId)。
*/
@Data
public class ContractLaunchSealPositionDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long id;
private Long contractId;
private Long partyId;
private String label;
/** Y/N,是否必签。 */
private String required;
private Integer pageNum;
private Double x;
private Double y;
private Double width;
private Double height;
private String sealId;
private String sealName;
/** 最近一次二维码请求ID。 */
private String requestId;
/** 0空框 1已选章 2待回调 3已签 4失败。 */
private Integer signedStatus;
private String signedByName;
private String signedById;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime signedTime;
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;
private String isDeleted;
private String remark;
}
@@ -0,0 +1,42 @@
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;
/**
* 签章位页面返回对象。
*
* 同时返回:
* 1. 签章平台合同ID
* 2. PDF 预览信息
* 3. 当前合同的签章位列表
* 4. 当前用户可代表签署的 partyId 列表
*/
@Data
public class ContractLaunchSealPositionPageDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private Long contractId;
private String sealContractId;
private String sealPosConfigured;
private Integer imageWidth;
private Integer imageHeight;
private List<String> pdfToImageList = new ArrayList<>();
private List<ContractLaunchSealPositionDto> positions = new ArrayList<>();
private List<Long> availablePartyIds = new ArrayList<>();
private Long preferredPartyId;
}
@@ -0,0 +1,24 @@
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;
/**
* 批量保存签章位入参。
*
* 第一版采用替换式保存:
* 前端传当前整份合同的全部签章位,
* 后端先删后插,避免逐条 diff。
*/
@Data
public class ContractLaunchSealPositionSaveDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private List<ContractLaunchSealPositionDto> positions = new ArrayList<>();
}
@@ -2,138 +2,221 @@ package com.nbport.zgwl.contractlaunch.mapper;
import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveEleDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveEleDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchivePaperDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchivePaperDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchFileDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchFileDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLogDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLogDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionDto;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.util.List; import java.util.List;
public interface ContractLaunchMapper { public interface ContractLaunchMapper {
/** 获取主表序列(Oracle 主键生成) */
Long selectMainSeqNextVal(); Long selectMainSeqNextVal();
/** 获取合同方表序列 */
Long selectPartySeqNextVal(); Long selectPartySeqNextVal();
/** 获取文件表序列 */
Long selectFileSeqNextVal(); Long selectFileSeqNextVal();
/** 获取履行计划表序列 */
Long selectPlanSeqNextVal(); Long selectPlanSeqNextVal();
/** 获取日志表序列 */
Long selectLogSeqNextVal(); Long selectLogSeqNextVal();
/** 获取纸质归档表序列 */
Long selectArchivePaperSeqNextVal(); Long selectArchivePaperSeqNextVal();
/** 获取电子归档表序列 */
Long selectArchiveEleSeqNextVal(); Long selectArchiveEleSeqNextVal();
/** 分页查询合同列表 */ @Select("select SEQ_SEAL_CONTRACT_LAUNCH_SEAL_POS.NEXTVAL from dual")
Long selectSealPositionSeqNextVal();
IPage<ContractLaunchDto> selectContractPage(Page<ContractLaunchDto> page, @Param("query") ContractLaunchQueryDto queryDto); IPage<ContractLaunchDto> selectContractPage(Page<ContractLaunchDto> page, @Param("query") ContractLaunchQueryDto queryDto);
/** 根据主键查询合同 */
ContractLaunchDto selectContractById(@Param("id") Long id); ContractLaunchDto selectContractById(@Param("id") Long id);
/** 根据主键并带权限条件查询合同 */
ContractLaunchDto selectContractByIdWithScope(@Param("id") Long id, @Param("query") ContractLaunchQueryDto queryDto); ContractLaunchDto selectContractByIdWithScope(@Param("id") Long id, @Param("query") ContractLaunchQueryDto queryDto);
/** 根据 contractSysId 查询合同(法务回调时用于定位) */
ContractLaunchDto selectContractByContractSysId(@Param("contractSysId") String contractSysId); ContractLaunchDto selectContractByContractSysId(@Param("contractSysId") String contractSysId);
/** 根据 sealContractId 查询合同(签章平台回调时用于定位) */
ContractLaunchDto selectContractBySealContractId(@Param("sealContractId") String sealContractId); ContractLaunchDto selectContractBySealContractId(@Param("sealContractId") String sealContractId);
/** 新增合同 */
int insertContract(ContractLaunchDto contract); int insertContract(ContractLaunchDto contract);
/** 修改合同 */
int updateContract(ContractLaunchDto contract); int updateContract(ContractLaunchDto contract);
/** 逻辑删除合同 */
int deleteContract(@Param("id") Long id, @Param("updateBy") String updateBy); int deleteContract(@Param("id") Long id, @Param("updateBy") String updateBy);
/** 批量插入合同方 */
int batchInsertParty(@Param("list") List<ContractLaunchPartyDto> list, int batchInsertParty(@Param("list") List<ContractLaunchPartyDto> list,
@Param("createBy") String createBy, @Param("createBy") String createBy,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 批量插入文件 */
int batchInsertFile(@Param("list") List<ContractLaunchFileDto> list, int batchInsertFile(@Param("list") List<ContractLaunchFileDto> list,
@Param("createBy") String createBy, @Param("createBy") String createBy,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 批量插入履行计划 */
int batchInsertPlan(@Param("list") List<ContractLaunchPlanDto> list, int batchInsertPlan(@Param("list") List<ContractLaunchPlanDto> list,
@Param("createBy") String createBy, @Param("createBy") String createBy,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 批量插入纸质归档材料 */
int batchInsertArchivePaper(@Param("list") List<ContractLaunchArchivePaperDto> list, int batchInsertArchivePaper(@Param("list") List<ContractLaunchArchivePaperDto> list,
@Param("createBy") String createBy, @Param("createBy") String createBy,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 批量插入电子归档材料 */
int batchInsertArchiveEle(@Param("list") List<ContractLaunchArchiveEleDto> list, int batchInsertArchiveEle(@Param("list") List<ContractLaunchArchiveEleDto> list,
@Param("createBy") String createBy, @Param("createBy") String createBy,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 插入单条操作日志 */
int insertLog(ContractLaunchLogDto logDto); int insertLog(ContractLaunchLogDto logDto);
/** 删除合同下的所有合同方(逻辑删除) */
int deletePartyByContractId(@Param("contractId") Long contractId, int deletePartyByContractId(@Param("contractId") Long contractId,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 删除合同下的所有文件(逻辑删除) */
int deleteFileByContractId(@Param("contractId") Long contractId, int deleteFileByContractId(@Param("contractId") Long contractId,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 按分类删除合同下的文件(如重新上传正文时只删正文,不删其他附件) */
int deleteFileByContractIdAndCategory(@Param("contractId") Long contractId, int deleteFileByContractIdAndCategory(@Param("contractId") Long contractId,
@Param("fileCategory") String fileCategory, @Param("fileCategory") String fileCategory,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 回写法务附件路径缓存 */
int updateFileLegalUrl(@Param("id") Long id, int updateFileLegalUrl(@Param("id") Long id,
@Param("legalFileUrl") String legalFileUrl, @Param("legalFileUrl") String legalFileUrl,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 删除合同下的所有履行计划(逻辑删除) */
int deletePlanByContractId(@Param("contractId") Long contractId, int deletePlanByContractId(@Param("contractId") Long contractId,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 删除合同下的纸质归档材料(逻辑删除) */
int deleteArchivePaperByContractId(@Param("contractId") Long contractId, int deleteArchivePaperByContractId(@Param("contractId") Long contractId,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 删除合同下的电子归档材料(逻辑删除) */
int deleteArchiveEleByContractId(@Param("contractId") Long contractId, int deleteArchiveEleByContractId(@Param("contractId") Long contractId,
@Param("updateBy") String updateBy); @Param("updateBy") String updateBy);
/** 批量查询合同方(按 contractIds */ @Delete("""
delete from SEAL_CONTRACT_LAUNCH_SEAL_POS
where CONTRACT_ID = #{contractId}
""")
int deleteSealPositionByContractId(@Param("contractId") Long contractId,
@Param("updateBy") String updateBy);
@Insert("""
<script>
INSERT ALL
<foreach collection='list' item='item'>
INTO SEAL_CONTRACT_LAUNCH_SEAL_POS (
ID, CONTRACT_ID, PARTY_ID, LABEL, REQUIRED, PAGE_NUM, X, Y, WIDTH, HEIGHT,
SEAL_ID, SEAL_NAME, REQUEST_ID, SIGNED_STATUS, SIGNED_BY_NAME, SIGNED_BY_ID, SIGNED_TIME,
CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK
) VALUES (
SEQ_SEAL_CONTRACT_LAUNCH_SEAL_POS.NEXTVAL, #{item.contractId}, #{item.partyId}, #{item.label}, #{item.required}, #{item.pageNum}, #{item.x}, #{item.y}, #{item.width}, #{item.height},
#{item.sealId}, #{item.sealName}, #{item.requestId}, #{item.signedStatus}, #{item.signedByName}, #{item.signedById}, #{item.signedTime},
#{createBy}, SYSDATE, #{updateBy}, SYSDATE, 'N', #{item.remark}
)
</foreach>
SELECT 1 FROM DUAL
</script>
""")
int batchInsertSealPosition(@Param("list") List<ContractLaunchSealPositionDto> list,
@Param("createBy") String createBy,
@Param("updateBy") String updateBy);
@Select("""
select ID, CONTRACT_ID, PARTY_ID, LABEL, REQUIRED, PAGE_NUM, X, Y, WIDTH, HEIGHT,
SEAL_ID, SEAL_NAME, REQUEST_ID, SIGNED_STATUS, SIGNED_BY_NAME, SIGNED_BY_ID, SIGNED_TIME,
CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK
from SEAL_CONTRACT_LAUNCH_SEAL_POS
where CONTRACT_ID = #{contractId}
and IS_DELETED = 'N'
order by PAGE_NUM asc, ID asc
""")
@Results(id = "sealPositionResult", value = {
@Result(property = "id", column = "ID", id = true),
@Result(property = "contractId", column = "CONTRACT_ID"),
@Result(property = "partyId", column = "PARTY_ID"),
@Result(property = "label", column = "LABEL"),
@Result(property = "required", column = "REQUIRED"),
@Result(property = "pageNum", column = "PAGE_NUM"),
@Result(property = "x", column = "X"),
@Result(property = "y", column = "Y"),
@Result(property = "width", column = "WIDTH"),
@Result(property = "height", column = "HEIGHT"),
@Result(property = "sealId", column = "SEAL_ID"),
@Result(property = "sealName", column = "SEAL_NAME"),
@Result(property = "requestId", column = "REQUEST_ID"),
@Result(property = "signedStatus", column = "SIGNED_STATUS"),
@Result(property = "signedByName", column = "SIGNED_BY_NAME"),
@Result(property = "signedById", column = "SIGNED_BY_ID"),
@Result(property = "signedTime", column = "SIGNED_TIME"),
@Result(property = "createBy", column = "CREATE_BY"),
@Result(property = "createTime", column = "CREATE_TIME"),
@Result(property = "updateBy", column = "UPDATE_BY"),
@Result(property = "updateTime", column = "UPDATE_TIME"),
@Result(property = "isDeleted", column = "IS_DELETED"),
@Result(property = "remark", column = "REMARK")
})
List<ContractLaunchSealPositionDto> selectSealPositionByContractId(@Param("contractId") Long contractId);
@Select("""
select ID, CONTRACT_ID, PARTY_ID, LABEL, REQUIRED, PAGE_NUM, X, Y, WIDTH, HEIGHT,
SEAL_ID, SEAL_NAME, REQUEST_ID, SIGNED_STATUS, SIGNED_BY_NAME, SIGNED_BY_ID, SIGNED_TIME,
CREATE_BY, CREATE_TIME, UPDATE_BY, UPDATE_TIME, IS_DELETED, REMARK
from SEAL_CONTRACT_LAUNCH_SEAL_POS
where REQUEST_ID = #{requestId}
and IS_DELETED = 'N'
order by ID asc
""")
@ResultMap("sealPositionResult")
List<ContractLaunchSealPositionDto> selectSealPositionByRequestId(@Param("requestId") String requestId);
@Update("""
<script>
update SEAL_CONTRACT_LAUNCH_SEAL_POS
<trim prefix='set' suffixOverrides=','>
PARTY_ID = #{partyId},
LABEL = #{label},
REQUIRED = #{required},
PAGE_NUM = #{pageNum},
X = #{x},
Y = #{y},
WIDTH = #{width},
HEIGHT = #{height},
SEAL_ID = #{sealId},
SEAL_NAME = #{sealName},
REQUEST_ID = #{requestId},
SIGNED_STATUS = #{signedStatus},
SIGNED_BY_NAME = #{signedByName},
SIGNED_BY_ID = #{signedById},
SIGNED_TIME = #{signedTime},
UPDATE_BY = #{updateBy},
UPDATE_TIME = #{updateTime},
IS_DELETED = #{isDeleted},
REMARK = #{remark},
</trim>
where ID = #{id}
and IS_DELETED = 'N'
</script>
""")
int updateSealPosition(ContractLaunchSealPositionDto position);
List<ContractLaunchPartyDto> selectPartyByContractIds(@Param("contractIds") List<Long> contractIds); List<ContractLaunchPartyDto> selectPartyByContractIds(@Param("contractIds") List<Long> contractIds);
/** 批量查询文件(按 contractIds */
List<ContractLaunchFileDto> selectFileByContractIds(@Param("contractIds") List<Long> contractIds); List<ContractLaunchFileDto> selectFileByContractIds(@Param("contractIds") List<Long> contractIds);
/** 批量查询履行计划(按 contractIds */
List<ContractLaunchPlanDto> selectPlanByContractIds(@Param("contractIds") List<Long> contractIds); List<ContractLaunchPlanDto> selectPlanByContractIds(@Param("contractIds") List<Long> contractIds);
/** 批量查询纸质归档材料(按 contractIds */
List<ContractLaunchArchivePaperDto> selectArchivePaperByContractIds(@Param("contractIds") List<Long> contractIds); List<ContractLaunchArchivePaperDto> selectArchivePaperByContractIds(@Param("contractIds") List<Long> contractIds);
/** 批量查询电子归档材料(按 contractIds */
List<ContractLaunchArchiveEleDto> selectArchiveEleByContractIds(@Param("contractIds") List<Long> contractIds); List<ContractLaunchArchiveEleDto> selectArchiveEleByContractIds(@Param("contractIds") List<Long> contractIds);
/** 批量查询操作日志(按 contractIds */
List<ContractLaunchLogDto> selectLogByContractIds(@Param("contractIds") List<Long> contractIds); List<ContractLaunchLogDto> selectLogByContractIds(@Param("contractIds") List<Long> contractIds);
} }
@@ -0,0 +1,604 @@
package com.nbport.zgwl.contractlaunch.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.nbport.zgwl.auth.AuthManager;
import com.nbport.zgwl.auth.SecurityManager;
import com.nbport.zgwl.auth.SysEnum;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionBatchStatusDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionPageDto;
import com.nbport.zgwl.contractlaunch.mapper.ContractLaunchMapper;
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants;
import com.nbport.zgwl.entity.SysUserEntity;
import com.nbport.zgwl.exception.ServiceException;
import com.nbport.zgwl.partner.dto.PartnerManageDto;
import com.nbport.zgwl.partner.service.PartnerManageService;
import com.nbport.zgwl.seal.config.SealConfig;
import com.nbport.zgwl.seal.service.SealTokenService;
import com.nbport.zgwl.seal.utils.SealHttpClient;
import com.nbport.zgwl.service.LocalFileService;
import com.nbport.zgwl.utils.AdminSecurityManage;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 签章位相关的辅助能力。
*
* 这部分逻辑后补到现有 Service 时,尽量不去拆散原来的大类,
* 因此单独抽一个父类承接:
* 1. 签章平台预览初始化
* 2. 签章位坐标校验与状态流转
* 3. 当前用户可代表的签署方识别
*/
abstract class ContractLaunchSealPositionSupport {
protected static final int DEFAULT_IMAGE_WIDTH = 793;
protected static final int DEFAULT_IMAGE_HEIGHT = 1122;
protected void assertSealPositionConfigAccess(ContractLaunchDto contract) {
if (contract == null || contract.getId() == null) {
throw new ServiceException("合同不存在");
}
if (!"1".equals(defaultIfBlank(contract.getIsElectron(), "1"))) {
throw new ServiceException("仅电子合同支持配置签章位");
}
SysUserEntity currentUser = adminSecurityManage().getUser();
if (currentUser == null) {
throw new ServiceException("当前用户未登录");
}
if (securityManager().isSuperAdmin()) {
return;
}
if (!authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.MANAGE)) {
throw new ServiceException("仅管理端用户可配置签章位");
}
}
protected void assertSealPositionViewAccess(ContractLaunchDto contract) {
if (contract == null || contract.getId() == null) {
throw new ServiceException("合同不存在");
}
if (!"1".equals(defaultIfBlank(contract.getIsElectron(), "1"))) {
throw new ServiceException("仅电子合同支持在线签署");
}
SysUserEntity currentUser = adminSecurityManage().getUser();
if (currentUser == null) {
throw new ServiceException("当前用户未登录");
}
if (securityManager().isSuperAdmin()) {
return;
}
boolean canManage = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.MANAGE);
boolean canBusiness = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.BUSINESS);
if (!canManage && !canBusiness) {
throw new ServiceException("当前用户无权访问在线签署");
}
}
protected SealPreviewData startSealPreview(ContractLaunchDto contract) {
if (contract == null) {
throw new ServiceException("合同不存在");
}
String filePath = trim(contract.getContractTextFilePath());
if (isBlank(filePath)) {
throw new ServiceException("请先上传水印文件");
}
byte[] fileBytes = localFileService().readFileBytes(filePath);
String fileName = defaultIfBlank(trim(contract.getContractTextFileName()), defaultIfBlank(trim(contract.getContractName()), "合同正文") + ".pdf");
String contentType = resolveContentType(fileName, contract.getContractTextFileType());
MultipartFile multipartFile = new SimpleMultipartFile(fileBytes, fileName, contentType);
String token = sealTokenService().getAccessToken();
String apiUrl = sealConfig().getBaseUrl() + "/saas/api/contract/start";
String response = SealHttpClient.postMultipart(apiUrl, multipartFile, baseFileName(contract.getContractName(), fileName), "seal-position-init", token);
JSONObject result = JSON.parseObject(response);
if (result == null || !result.getBooleanValue("isSuccess")) {
throw new ServiceException(result == null ? "初始化签署文件失败" : defaultIfBlank(result.getString("msg"), "初始化签署文件失败"));
}
SealPreviewData previewData = parsePreviewData(result.get("data"));
if (isBlank(previewData.contractId())) {
throw new ServiceException("签章平台未返回合同ID");
}
return previewData;
}
protected SealPreviewData previewSealContract(String sealContractId, String cachedImageListJson) {
List<String> cachedImageList = parseCachedImageList(cachedImageListJson);
if (isBlank(sealContractId)) {
return new SealPreviewData(null, DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT, cachedImageList);
}
try {
JSONObject body = new JSONObject();
body.put("contractId", sealContractId);
String response = SealHttpClient.post(
sealConfig().getBaseUrl() + "/saas/api/contract/preview",
body.toJSONString(),
sealTokenService().getAccessToken(),
"application/json"
);
JSONObject result = JSON.parseObject(response);
if (result != null && result.getBooleanValue("isSuccess")) {
SealPreviewData previewData = parsePreviewData(result.get("data"));
if (!previewData.pdfToImageList().isEmpty()) {
return previewData;
}
}
} catch (Exception ex) {
if (cachedImageList.isEmpty()) {
throw new ServiceException("获取签章预览失败: " + ex.getMessage());
}
}
return new SealPreviewData(sealContractId, DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT, cachedImageList);
}
protected ContractLaunchSealPositionPageDto buildSealPositionPage(ContractLaunchDto contract, SealPreviewData previewData) {
ContractLaunchSealPositionPageDto pageDto = new ContractLaunchSealPositionPageDto();
pageDto.setContractId(contract.getId());
pageDto.setSealContractId(defaultIfBlank(contract.getSealContractId(), previewData == null ? null : previewData.contractId()));
pageDto.setSealPosConfigured(defaultIfBlank(contract.getSealPosConfigured(), ContractLaunchConstants.FLAG_NO));
pageDto.setImageWidth(previewData == null ? DEFAULT_IMAGE_WIDTH : previewData.imageWidth());
pageDto.setImageHeight(previewData == null ? DEFAULT_IMAGE_HEIGHT : previewData.imageHeight());
pageDto.setPdfToImageList(previewData == null ? new ArrayList<>() : new ArrayList<>(previewData.pdfToImageList()));
pageDto.setPositions(new ArrayList<>(contractLaunchMapper().selectSealPositionByContractId(contract.getId())));
List<Long> availablePartyIds = resolveAvailablePartyIds(contract, adminSecurityManage().getUser());
pageDto.setAvailablePartyIds(availablePartyIds);
pageDto.setPreferredPartyId(availablePartyIds.size() == 1 ? availablePartyIds.get(0) : null);
return pageDto;
}
protected void validateSealPositionForSave(ContractLaunchSealPositionDto item, Map<Long, ContractLaunchPartyDto> partyMap) {
if (item == null) {
throw new ServiceException("签章位数据不能为空");
}
if (item.getPartyId() == null) {
throw new ServiceException("签章位必须绑定签署方");
}
if (partyMap == null || !partyMap.containsKey(item.getPartyId())) {
throw new ServiceException("签章位绑定的签署方不存在");
}
if (isBlank(item.getLabel())) {
throw new ServiceException("签章位标签不能为空");
}
if (item.getPageNum() == null || item.getPageNum() <= 0) {
throw new ServiceException("签章位页码必须大于0");
}
if (item.getX() == null || item.getY() == null || item.getWidth() == null || item.getHeight() == null) {
throw new ServiceException("签章位坐标不能为空");
}
if (item.getWidth() <= 0 || item.getHeight() <= 0) {
throw new ServiceException("签章位宽高必须大于0");
}
}
protected String normalizeRequiredFlag(String required) {
String value = trim(required);
if (value == null) {
return ContractLaunchConstants.FLAG_YES;
}
String normalized = value.toUpperCase(Locale.ROOT);
if ("N".equals(normalized) || "0".equals(normalized) || "FALSE".equals(normalized)) {
return ContractLaunchConstants.FLAG_NO;
}
return ContractLaunchConstants.FLAG_YES;
}
protected Double roundCoordinate(Double value) {
if (value == null) {
return null;
}
return Math.round(value * 100D) / 100D;
}
protected List<Long> resolveAvailablePartyIds(ContractLaunchDto contract, SysUserEntity currentUser) {
if (contract == null || contract.getPartyList() == null || contract.getPartyList().isEmpty() || currentUser == null) {
return new ArrayList<>();
}
if (securityManager().isSuperAdmin()) {
return contract.getPartyList().stream()
.map(ContractLaunchPartyDto::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
boolean canManage = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.MANAGE);
boolean canBusiness = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.BUSINESS);
String currentOrgId = currentUser.getSysOrgEntity() == null ? null : trim(currentUser.getSysOrgEntity().getId());
String currentAccount = trim(currentUser.getUsername());
String currentMobile = trim(currentUser.getMobilePhone());
Set<Long> partyIds = new LinkedHashSet<>();
for (ContractLaunchPartyDto party : contract.getPartyList()) {
if (party == null || party.getId() == null) {
continue;
}
if (canManage && isOurParty(party) && Objects.equals(currentOrgId, trim(party.getSubjectCode()))) {
partyIds.add(party.getId());
continue;
}
if (canBusiness && isOppositeParty(party) && isBusinessPartyMatch(party, currentOrgId, currentAccount, currentMobile)) {
partyIds.add(party.getId());
}
}
return new ArrayList<>(partyIds);
}
protected Long resolveTargetPartyId(Long partyId, List<Long> availablePartyIds) {
if (partyId != null) {
if (availablePartyIds != null && !availablePartyIds.isEmpty() && !availablePartyIds.contains(partyId)) {
throw new ServiceException("当前用户不可操作该签署方");
}
return partyId;
}
if (availablePartyIds == null || availablePartyIds.isEmpty()) {
throw new ServiceException("当前用户未匹配到可签署方,请选择后重试");
}
if (availablePartyIds.size() > 1) {
throw new ServiceException("当前用户匹配到多个签署方,请选择后重试");
}
return availablePartyIds.get(0);
}
protected void applySealPositionStatus(ContractLaunchSealPositionDto position,
ContractLaunchSealPositionBatchStatusDto batchDto,
ContractLaunchSealPositionBatchStatusDto.Item item,
SysUserEntity currentUser,
LocalDateTime now) {
Integer signedStatus = batchDto.getSignedStatus();
if (signedStatus == null) {
throw new ServiceException("签章位状态不能为空");
}
position.setUpdateBy(currentUser == null ? "system" : currentUser.getUsername());
position.setUpdateTime(now);
if (signedStatus == ContractLaunchConstants.SEAL_POS_STATUS_EMPTY) {
position.setSealId(null);
position.setSealName(null);
position.setRequestId(null);
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_EMPTY);
return;
}
if (signedStatus == ContractLaunchConstants.SEAL_POS_STATUS_SELECTED) {
if (item == null || isBlank(item.getSealId())) {
throw new ServiceException("已选章状态必须传印章ID");
}
position.setSealId(item.getSealId());
position.setSealName(item.getSealName());
position.setRequestId(null);
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_SELECTED);
return;
}
if (signedStatus == ContractLaunchConstants.SEAL_POS_STATUS_SIGNING) {
String sealId = item == null ? null : item.getSealId();
if (!isBlank(sealId)) {
position.setSealId(sealId);
}
if (item != null && !isBlank(item.getSealName())) {
position.setSealName(item.getSealName());
}
if (isBlank(position.getSealId())) {
throw new ServiceException("提交预盖章前请先选择印章");
}
if (isBlank(batchDto.getRequestId())) {
throw new ServiceException("二维码请求ID不能为空");
}
position.setRequestId(batchDto.getRequestId());
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_SIGNING);
return;
}
if (signedStatus == ContractLaunchConstants.SEAL_POS_STATUS_SIGNED) {
if (item != null && !isBlank(item.getSealId())) {
position.setSealId(item.getSealId());
}
if (item != null && !isBlank(item.getSealName())) {
position.setSealName(item.getSealName());
}
if (!isBlank(batchDto.getRequestId())) {
position.setRequestId(batchDto.getRequestId());
}
position.setSignedByName(defaultIfBlank(batchDto.getSignedByName(), currentUser == null ? null : currentUser.getUsername()));
position.setSignedById(defaultIfBlank(batchDto.getSignedById(), currentUser == null ? null : String.valueOf(currentUser.getId())));
position.setSignedTime(now);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_SIGNED);
return;
}
if (signedStatus == ContractLaunchConstants.SEAL_POS_STATUS_FAILED) {
position.setRequestId(null);
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_FAILED);
return;
}
throw new ServiceException("不支持的签章位状态: " + signedStatus);
}
protected boolean isSealPresignFailed(Map<String, Object> callbackData) {
String contractStatus = stringValue(callbackData == null ? null : callbackData.get("contractStatus"));
if (!isBlank(contractStatus)) {
if ("20".equals(contractStatus) || "2000".equals(contractStatus)) {
return false;
}
if ("0".equals(contractStatus) || contractStatus.startsWith("-")) {
return true;
}
}
Object successValue = callbackData == null ? null : (callbackData.containsKey("isSuccess") ? callbackData.get("isSuccess") : callbackData.get("success"));
if (successValue != null) {
return !Boolean.parseBoolean(String.valueOf(successValue));
}
String statusText = (stringValue(callbackData == null ? null : callbackData.get("status")) + " "
+ stringValue(callbackData == null ? null : callbackData.get("msg"))).toLowerCase(Locale.ROOT);
return statusText.contains("fail") || statusText.contains("cancel") || statusText.contains("timeout") || statusText.contains("reject");
}
protected String stringValue(Object value) {
return value == null ? null : trim(String.valueOf(value));
}
protected boolean isBlank(String value) {
return value == null || value.isBlank();
}
protected String defaultIfBlank(String value, String defaultValue) {
return isBlank(value) ? defaultValue : value;
}
protected String trim(String value) {
return value == null ? null : value.trim();
}
protected boolean isOurParty(ContractLaunchPartyDto party) {
return party != null && "OUR".equalsIgnoreCase(trim(party.getRoleCode()));
}
protected boolean isOppositeParty(ContractLaunchPartyDto party) {
return party != null && "OPPOSITE".equalsIgnoreCase(trim(party.getRoleCode()));
}
protected boolean isSingleAgreement(ContractLaunchDto contract) {
return contract != null && "1".equals(defaultIfBlank(contract.getIsSingleAgreement(), "0"));
}
private boolean isBusinessPartyMatch(ContractLaunchPartyDto party, String currentOrgId, String currentAccount, String currentMobile) {
if (Objects.equals(currentOrgId, trim(party.getSubjectCode()))) {
return true;
}
if (party.getPartnerId() == null) {
return false;
}
try {
PartnerManageDto partner = partnerManageService().selectPartnerById(party.getPartnerId());
if (partner == null) {
return false;
}
if (partner.getLinkedOrgId() != null && Objects.equals(currentOrgId, String.valueOf(partner.getLinkedOrgId()))) {
return true;
}
String linkedUserAccount = trim(partner.getLinkedUserAccount());
return !isBlank(linkedUserAccount) && (linkedUserAccount.equalsIgnoreCase(defaultIfBlank(currentAccount, ""))
|| linkedUserAccount.equalsIgnoreCase(defaultIfBlank(currentMobile, "")));
} catch (Exception ignored) {
return false;
}
}
private SealPreviewData parsePreviewData(Object dataObject) {
if (dataObject == null) {
return new SealPreviewData(null, DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT, new ArrayList<>());
}
JSONObject data = dataObject instanceof JSONObject
? (JSONObject) dataObject
: JSON.parseObject(JSON.toJSONString(dataObject));
if (data == null) {
return new SealPreviewData(null, DEFAULT_IMAGE_WIDTH, DEFAULT_IMAGE_HEIGHT, new ArrayList<>());
}
List<String> imageList = new ArrayList<>();
Object imageObject = data.get("pdfToImageList");
if (imageObject instanceof JSONArray array) {
for (int i = 0; i < array.size(); i++) {
String item = stringValue(array.get(i));
if (!isBlank(item)) {
imageList.add(item);
}
}
} else if (imageObject instanceof List<?> list) {
for (Object item : list) {
String imageUrl = stringValue(item);
if (!isBlank(imageUrl)) {
imageList.add(imageUrl);
}
}
} else if (imageObject != null) {
imageList.addAll(parseCachedImageList(String.valueOf(imageObject)));
}
Integer imageWidth = data.getInteger("imageWidth");
Integer imageHeight = data.getInteger("imageHeight");
return new SealPreviewData(
stringValue(data.get("contractId")),
imageWidth == null || imageWidth <= 0 ? DEFAULT_IMAGE_WIDTH : imageWidth,
imageHeight == null || imageHeight <= 0 ? DEFAULT_IMAGE_HEIGHT : imageHeight,
imageList
);
}
private List<String> parseCachedImageList(String json) {
if (isBlank(json)) {
return new ArrayList<>();
}
try {
JSONArray array = JSON.parseArray(json);
List<String> imageList = new ArrayList<>();
for (int i = 0; i < array.size(); i++) {
String item = stringValue(array.get(i));
if (!isBlank(item)) {
imageList.add(item);
}
}
return imageList;
} catch (Exception ex) {
return new ArrayList<>();
}
}
private String baseFileName(String contractName, String fileName) {
String value = defaultIfBlank(trim(contractName), trim(fileName));
if (isBlank(value)) {
return "合同正文";
}
int index = value.lastIndexOf('.');
return index > 0 ? value.substring(0, index) : value;
}
private String resolveContentType(String fileName, String fileType) {
String suffix = trim(fileType);
if (isBlank(suffix) && !isBlank(fileName) && fileName.contains(".")) {
suffix = fileName.substring(fileName.lastIndexOf('.') + 1);
}
suffix = defaultIfBlank(suffix, "pdf").toLowerCase(Locale.ROOT);
return switch (suffix) {
case "doc" -> "application/msword";
case "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "pdf" -> "application/pdf";
default -> "application/octet-stream";
};
}
protected ContractLaunchMapper contractLaunchMapper() {
return readField("contractLaunchMapper", ContractLaunchMapper.class);
}
protected AdminSecurityManage adminSecurityManage() {
return readField("adminSecurityManage", AdminSecurityManage.class);
}
protected AuthManager authManager() {
return readField("authManager", AuthManager.class);
}
protected SecurityManager securityManager() {
return readField("securityManager", SecurityManager.class);
}
protected LocalFileService localFileService() {
return readField("localFileService", LocalFileService.class);
}
protected SealTokenService sealTokenService() {
return readField("sealTokenService", SealTokenService.class);
}
protected SealConfig sealConfig() {
return readField("sealConfig", SealConfig.class);
}
protected PartnerManageService partnerManageService() {
return readField("partnerManageService", PartnerManageService.class);
}
protected <T> T readField(String fieldName, Class<T> type) {
try {
Field field = findField(getClass(), fieldName);
field.setAccessible(true);
Object value = field.get(this);
return type.cast(value);
} catch (Exception ex) {
throw new IllegalStateException("读取字段失败: " + fieldName, ex);
}
}
private Field findField(Class<?> clazz, String fieldName) throws NoSuchFieldException {
Class<?> current = clazz;
while (current != null) {
try {
return current.getDeclaredField(fieldName);
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(fieldName);
}
protected record SealPreviewData(String contractId, Integer imageWidth, Integer imageHeight, List<String> pdfToImageList) {
}
private static class SimpleMultipartFile implements MultipartFile {
private final byte[] content;
private final String originalFilename;
private final String contentType;
private SimpleMultipartFile(byte[] content, String originalFilename, String contentType) {
this.content = content == null ? new byte[0] : content;
this.originalFilename = originalFilename;
this.contentType = contentType;
}
@Override
public String getName() {
return "file";
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return content.length == 0;
}
@Override
public long getSize() {
return content.length;
}
@Override
public byte[] getBytes() {
return content;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(content);
}
@Override
public void transferTo(File dest) throws IOException {
try (FileOutputStream outputStream = new FileOutputStream(dest)) {
outputStream.write(content);
}
}
}
}
@@ -1,5 +1,11 @@
package com.nbport.zgwl.contractlaunch.service; package com.nbport.zgwl.contractlaunch.service;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.nbport.zgwl.auth.AuthManager;
import com.nbport.zgwl.auth.SecurityManager;
import com.nbport.zgwl.auth.SysEnum;
import com.nbport.zgwl.common.PageResult; import com.nbport.zgwl.common.PageResult;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto;
@@ -7,11 +13,41 @@ import com.nbport.zgwl.contractlaunch.dto.ContractLaunchCollectSaveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackSampleDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackSampleDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionBatchStatusDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionCheckResultDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionPageDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionSaveDto;
import com.nbport.zgwl.contractlaunch.mapper.ContractLaunchMapper;
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants;
import com.nbport.zgwl.entity.SysUserEntity;
import com.nbport.zgwl.exception.ServiceException;
import com.nbport.zgwl.partner.dto.PartnerManageDto;
import com.nbport.zgwl.partner.service.PartnerManageService;
import com.nbport.zgwl.seal.config.SealConfig;
import com.nbport.zgwl.seal.service.SealTokenService;
import com.nbport.zgwl.seal.utils.SealHttpClient;
import com.nbport.zgwl.service.LocalFileService;
import com.nbport.zgwl.utils.AdminSecurityManage;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
public interface ContractLaunchService { public interface ContractLaunchService {
@@ -66,6 +102,24 @@ public interface ContractLaunchService {
/** 相对方确认已签署完成 */ /** 相对方确认已签署完成 */
Map<String, Object> confirmCounterparty(Long id, Long partyId); Map<String, Object> confirmCounterparty(Long id, Long partyId);
/** 初始化合同签章位配置所需的签章平台预览 */
ContractLaunchSealPositionPageDto initSealPosition(Long id);
/** 查询合同签章位与预览信息 */
ContractLaunchSealPositionPageDto getSealPositionPage(Long id);
/** 保存合同签章位配置 */
ContractLaunchSealPositionPageDto saveSealPositions(Long id, ContractLaunchSealPositionSaveDto saveDto);
/** 批量更新签章位状态 */
ContractLaunchSealPositionPageDto batchUpdateSealPositionStatus(Long id, ContractLaunchSealPositionBatchStatusDto batchDto);
/** 校验签章位是否已全部签署 */
ContractLaunchSealPositionCheckResultDto checkSealPositions(Long id, Long partyId);
/** 处理签章平台预盖章回调 */
void handleSealPresignCallback(Map<String, Object> callbackData);
/** 上传法务水印合同文件(当审批回调未带回水印文件时,手动上传代替) */ /** 上传法务水印合同文件(当审批回调未带回水印文件时,手动上传代替) */
ContractLaunchDto uploadWatermarkFile(Long id, MultipartFile file); ContractLaunchDto uploadWatermarkFile(Long id, MultipartFile file);
@@ -80,4 +134,544 @@ public interface ContractLaunchService {
/** 删除草稿(仅 draft/rejected 状态允许) */ /** 删除草稿(仅 draft/rejected 状态允许) */
void deleteDraft(Long id); void deleteDraft(Long id);
default void assertSealPositionConfigAccess(ContractLaunchDto contract) {
if (contract == null || contract.getId() == null) {
throw new ServiceException("合同不存在");
}
if (!"1".equals(defaultIfBlank(contract.getIsElectron(), "1"))) {
throw new ServiceException("仅电子合同支持配置签章位");
}
SysUserEntity currentUser = adminSecurityManage().getUser();
if (currentUser == null) {
throw new ServiceException("当前用户未登录");
}
if (securityManager().isSuperAdmin()) {
return;
}
if (!authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.MANAGE)) {
throw new ServiceException("仅管理端用户可配置签章位");
}
}
default void assertSealPositionViewAccess(ContractLaunchDto contract) {
if (contract == null || contract.getId() == null) {
throw new ServiceException("合同不存在");
}
if (!"1".equals(defaultIfBlank(contract.getIsElectron(), "1"))) {
throw new ServiceException("仅电子合同支持在线签署");
}
SysUserEntity currentUser = adminSecurityManage().getUser();
if (currentUser == null) {
throw new ServiceException("当前用户未登录");
}
if (securityManager().isSuperAdmin()) {
return;
}
boolean canManage = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.MANAGE);
boolean canBusiness = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.BUSINESS);
if (!canManage && !canBusiness) {
throw new ServiceException("当前用户无权访问在线签署");
}
}
default SealPreviewData startSealPreview(ContractLaunchDto contract) {
String filePath = trimText(contract == null ? null : contract.getContractTextFilePath());
if (isBlank(filePath)) {
throw new ServiceException("请先上传水印文件");
}
byte[] fileBytes = localFileService().readFileBytes(filePath);
String fileName = defaultIfBlank(trimText(contract.getContractTextFileName()), defaultIfBlank(trimText(contract.getContractName()), "合同正文") + ".pdf");
MultipartFile multipartFile = new SealPositionMultipartFile(fileBytes, fileName, resolveSealContentType(fileName, contract.getContractTextFileType()));
String response = SealHttpClient.postMultipart(
sealConfig().getBaseUrl() + "/saas/api/contract/start",
multipartFile,
buildSealPreviewContractName(contract, fileName),
"seal-position-init",
sealTokenService().getAccessToken()
);
JSONObject result = JSON.parseObject(response);
if (result == null || !result.getBooleanValue("isSuccess")) {
throw new ServiceException(result == null ? "初始化签署文件失败" : defaultIfBlank(result.getString("msg"), "初始化签署文件失败"));
}
SealPreviewData previewData = parseSealPreviewData(result.get("data"));
if (isBlank(previewData.contractId())) {
throw new ServiceException("签章平台未返回合同ID");
}
return previewData;
}
default SealPreviewData previewSealContract(String sealContractId, String cachedImageListJson) {
List<String> cachedImageList = parseSealImageListJson(cachedImageListJson);
if (isBlank(sealContractId)) {
return new SealPreviewData(null, 793, 1122, cachedImageList);
}
try {
JSONObject body = new JSONObject();
body.put("contractId", sealContractId);
String response = SealHttpClient.post(
sealConfig().getBaseUrl() + "/saas/api/contract/preview",
body.toJSONString(),
sealTokenService().getAccessToken(),
"application/json"
);
JSONObject result = JSON.parseObject(response);
if (result != null && result.getBooleanValue("isSuccess")) {
SealPreviewData previewData = parseSealPreviewData(result.get("data"));
if (!previewData.pdfToImageList().isEmpty()) {
return previewData;
}
}
} catch (Exception ex) {
if (cachedImageList.isEmpty()) {
throw new ServiceException("获取签章预览失败: " + ex.getMessage());
}
}
return new SealPreviewData(sealContractId, 793, 1122, cachedImageList);
}
default ContractLaunchSealPositionPageDto buildSealPositionPage(ContractLaunchDto contract, SealPreviewData previewData) {
ContractLaunchSealPositionPageDto pageDto = new ContractLaunchSealPositionPageDto();
pageDto.setContractId(contract.getId());
pageDto.setSealContractId(defaultIfBlank(contract.getSealContractId(), previewData == null ? null : previewData.contractId()));
pageDto.setSealPosConfigured(defaultIfBlank(contract.getSealPosConfigured(), ContractLaunchConstants.FLAG_NO));
pageDto.setImageWidth(previewData == null ? 793 : previewData.imageWidth());
pageDto.setImageHeight(previewData == null ? 1122 : previewData.imageHeight());
pageDto.setPdfToImageList(previewData == null ? new ArrayList<>() : previewData.pdfToImageList());
pageDto.setPositions(new ArrayList<>(contractLaunchMapper().selectSealPositionByContractId(contract.getId())));
List<Long> availablePartyIds = resolveAvailablePartyIds(contract, adminSecurityManage().getUser());
pageDto.setAvailablePartyIds(availablePartyIds);
pageDto.setPreferredPartyId(availablePartyIds.size() == 1 ? availablePartyIds.get(0) : null);
return pageDto;
}
default void validateSealPositionForSave(ContractLaunchSealPositionDto item, Map<Long, ContractLaunchPartyDto> partyMap) {
if (item == null) {
throw new ServiceException("签章位数据不能为空");
}
if (item.getPartyId() == null) {
throw new ServiceException("签章位必须绑定签署方");
}
if (partyMap == null || !partyMap.containsKey(item.getPartyId())) {
throw new ServiceException("签章位绑定的签署方不存在");
}
if (isBlank(item.getLabel())) {
throw new ServiceException("签章位标签不能为空");
}
if (item.getPageNum() == null || item.getPageNum() <= 0) {
throw new ServiceException("签章位页码必须大于0");
}
if (item.getX() == null || item.getY() == null || item.getWidth() == null || item.getHeight() == null) {
throw new ServiceException("签章位坐标不能为空");
}
if (item.getWidth() <= 0 || item.getHeight() <= 0) {
throw new ServiceException("签章位宽高必须大于0");
}
}
default String normalizeRequiredFlag(String required) {
String value = trimText(required);
if (value == null) {
return ContractLaunchConstants.FLAG_YES;
}
String normalized = value.toUpperCase(Locale.ROOT);
if ("N".equals(normalized) || "0".equals(normalized) || "FALSE".equals(normalized)) {
return ContractLaunchConstants.FLAG_NO;
}
return ContractLaunchConstants.FLAG_YES;
}
default Double roundCoordinate(Double value) {
if (value == null) {
return null;
}
return Math.round(value * 100D) / 100D;
}
default List<Long> resolveAvailablePartyIds(ContractLaunchDto contract, SysUserEntity currentUser) {
if (contract == null || contract.getPartyList() == null || contract.getPartyList().isEmpty() || currentUser == null) {
return new ArrayList<>();
}
if (securityManager().isSuperAdmin()) {
return contract.getPartyList().stream()
.map(ContractLaunchPartyDto::getId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
boolean canManage = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.MANAGE);
boolean canBusiness = authManager().isUserCanAccessSys(currentUser.getId(), SysEnum.BUSINESS);
String currentOrgId = currentUser.getSysOrgEntity() == null ? null : trimText(currentUser.getSysOrgEntity().getId());
String currentAccount = trimText(currentUser.getUsername());
String currentMobile = trimText(currentUser.getMobilePhone());
Set<Long> partyIds = new LinkedHashSet<>();
for (ContractLaunchPartyDto party : contract.getPartyList()) {
if (party == null || party.getId() == null) {
continue;
}
if (canManage && isOurParty(party) && Objects.equals(currentOrgId, trimText(party.getSubjectCode()))) {
partyIds.add(party.getId());
continue;
}
if (canBusiness && isOppositeParty(party) && isMatchedBusinessParty(party, currentOrgId, currentAccount, currentMobile)) {
partyIds.add(party.getId());
}
}
return new ArrayList<>(partyIds);
}
default Long resolveTargetPartyId(Long partyId, List<Long> availablePartyIds) {
if (partyId != null) {
if (availablePartyIds == null || availablePartyIds.isEmpty()) {
throw new ServiceException("当前用户未匹配到可签署方,请手动选择后重试");
}
if (!availablePartyIds.contains(partyId)) {
throw new ServiceException("当前用户不可操作该签署方");
}
return partyId;
}
if (availablePartyIds == null || availablePartyIds.isEmpty()) {
throw new ServiceException("当前用户未匹配到可签署方,请手动选择后重试");
}
if (availablePartyIds.size() > 1) {
throw new ServiceException("当前用户匹配到多个签署方,请手动选择后重试");
}
return availablePartyIds.get(0);
}
default void applySealPositionStatus(ContractLaunchSealPositionDto position,
ContractLaunchSealPositionBatchStatusDto batchDto,
ContractLaunchSealPositionBatchStatusDto.Item item,
SysUserEntity currentUser,
java.time.LocalDateTime now) {
Integer signedStatus = batchDto.getSignedStatus();
if (signedStatus == null) {
throw new ServiceException("签章位状态不能为空");
}
position.setUpdateBy(currentUser == null ? "system" : currentUser.getUsername());
position.setUpdateTime(now);
if (Objects.equals(signedStatus, ContractLaunchConstants.SEAL_POS_STATUS_EMPTY)) {
position.setSealId(null);
position.setSealName(null);
position.setRequestId(null);
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_EMPTY);
return;
}
if (Objects.equals(signedStatus, ContractLaunchConstants.SEAL_POS_STATUS_SELECTED)) {
if (item == null || isBlank(item.getSealId())) {
throw new ServiceException("已选章状态必须传印章ID");
}
position.setSealId(item.getSealId());
position.setSealName(item.getSealName());
position.setRequestId(null);
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_SELECTED);
return;
}
if (Objects.equals(signedStatus, ContractLaunchConstants.SEAL_POS_STATUS_SIGNING)) {
if (item != null && !isBlank(item.getSealId())) {
position.setSealId(item.getSealId());
}
if (item != null && !isBlank(item.getSealName())) {
position.setSealName(item.getSealName());
}
if (isBlank(position.getSealId())) {
throw new ServiceException("提交预盖章前请先选择印章");
}
if (isBlank(batchDto.getRequestId())) {
throw new ServiceException("二维码请求ID不能为空");
}
position.setRequestId(batchDto.getRequestId());
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_SIGNING);
return;
}
if (Objects.equals(signedStatus, ContractLaunchConstants.SEAL_POS_STATUS_SIGNED)) {
if (item != null && !isBlank(item.getSealId())) {
position.setSealId(item.getSealId());
}
if (item != null && !isBlank(item.getSealName())) {
position.setSealName(item.getSealName());
}
if (!isBlank(batchDto.getRequestId())) {
position.setRequestId(batchDto.getRequestId());
}
position.setSignedByName(defaultIfBlank(batchDto.getSignedByName(), currentUser == null ? null : currentUser.getUsername()));
position.setSignedById(defaultIfBlank(batchDto.getSignedById(), currentUser == null ? null : String.valueOf(currentUser.getId())));
position.setSignedTime(now);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_SIGNED);
return;
}
if (Objects.equals(signedStatus, ContractLaunchConstants.SEAL_POS_STATUS_FAILED)) {
position.setRequestId(null);
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_FAILED);
return;
}
throw new ServiceException("不支持的签章位状态: " + signedStatus);
}
default boolean isSealPresignFailed(Map<String, Object> callbackData) {
String contractStatus = stringValue(callbackData == null ? null : callbackData.get("contractStatus"));
if (!isBlank(contractStatus)) {
if ("20".equals(contractStatus) || "2000".equals(contractStatus)) {
return false;
}
if ("0".equals(contractStatus) || contractStatus.startsWith("-")) {
return true;
}
}
Object successValue = callbackData == null ? null : (callbackData.containsKey("isSuccess") ? callbackData.get("isSuccess") : callbackData.get("success"));
if (successValue != null) {
return !Boolean.parseBoolean(String.valueOf(successValue));
}
String statusText = (defaultIfBlank(stringValue(callbackData == null ? null : callbackData.get("status")), "") + " "
+ defaultIfBlank(stringValue(callbackData == null ? null : callbackData.get("msg")), "")).toLowerCase(Locale.ROOT);
return statusText.contains("fail") || statusText.contains("cancel") || statusText.contains("timeout") || statusText.contains("reject");
}
default String stringValue(Object value) {
return value == null ? null : trimText(String.valueOf(value));
}
default boolean isBlank(String value) {
return value == null || value.isBlank();
}
default String defaultIfBlank(String value, String defaultValue) {
return isBlank(value) ? defaultValue : value;
}
private boolean isMatchedBusinessParty(ContractLaunchPartyDto party, String currentOrgId, String currentAccount, String currentMobile) {
if (Objects.equals(currentOrgId, trimText(party.getSubjectCode()))) {
return true;
}
if (party.getPartnerId() == null) {
return false;
}
try {
PartnerManageDto partner = partnerManageService().selectPartnerById(party.getPartnerId());
if (partner == null) {
return false;
}
if (partner.getLinkedOrgId() != null && Objects.equals(currentOrgId, String.valueOf(partner.getLinkedOrgId()))) {
return true;
}
String linkedUserAccount = trimText(partner.getLinkedUserAccount());
return !isBlank(linkedUserAccount) && (linkedUserAccount.equalsIgnoreCase(defaultIfBlank(currentAccount, ""))
|| linkedUserAccount.equalsIgnoreCase(defaultIfBlank(currentMobile, "")));
} catch (Exception ignored) {
return false;
}
}
private SealPreviewData parseSealPreviewData(Object dataObject) {
if (dataObject == null) {
return new SealPreviewData(null, 793, 1122, new ArrayList<>());
}
JSONObject data = dataObject instanceof JSONObject
? (JSONObject) dataObject
: JSON.parseObject(JSON.toJSONString(dataObject));
if (data == null) {
return new SealPreviewData(null, 793, 1122, new ArrayList<>());
}
List<String> imageList = new ArrayList<>();
Object imageObject = data.get("pdfToImageList");
if (imageObject instanceof JSONArray array) {
for (int i = 0; i < array.size(); i++) {
String item = stringValue(array.get(i));
if (!isBlank(item)) {
imageList.add(item);
}
}
} else if (imageObject instanceof List<?> list) {
for (Object item : list) {
String imageUrl = stringValue(item);
if (!isBlank(imageUrl)) {
imageList.add(imageUrl);
}
}
} else if (imageObject != null) {
imageList.addAll(parseSealImageListJson(String.valueOf(imageObject)));
}
Integer imageWidth = data.getInteger("imageWidth");
Integer imageHeight = data.getInteger("imageHeight");
return new SealPreviewData(
stringValue(data.get("contractId")),
imageWidth == null || imageWidth <= 0 ? 793 : imageWidth,
imageHeight == null || imageHeight <= 0 ? 1122 : imageHeight,
imageList
);
}
private List<String> parseSealImageListJson(String json) {
if (isBlank(json)) {
return new ArrayList<>();
}
try {
JSONArray array = JSON.parseArray(json);
List<String> imageList = new ArrayList<>();
for (int i = 0; i < array.size(); i++) {
String item = stringValue(array.get(i));
if (!isBlank(item)) {
imageList.add(item);
}
}
return imageList;
} catch (Exception ex) {
return new ArrayList<>();
}
}
private String buildSealPreviewContractName(ContractLaunchDto contract, String fileName) {
String value = defaultIfBlank(trimText(contract == null ? null : contract.getContractName()), trimText(fileName));
if (isBlank(value)) {
return "合同正文";
}
int index = value.lastIndexOf('.');
return index > 0 ? value.substring(0, index) : value;
}
private String resolveSealContentType(String fileName, String fileType) {
String suffix = trimText(fileType);
if (isBlank(suffix) && !isBlank(fileName) && fileName.contains(".")) {
suffix = fileName.substring(fileName.lastIndexOf('.') + 1);
}
suffix = defaultIfBlank(suffix, "pdf").toLowerCase(Locale.ROOT);
return switch (suffix) {
case "doc" -> "application/msword";
case "docx" -> "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
case "pdf" -> "application/pdf";
default -> "application/octet-stream";
};
}
private ContractLaunchMapper contractLaunchMapper() {
return readField("contractLaunchMapper", ContractLaunchMapper.class);
}
private AdminSecurityManage adminSecurityManage() {
return readField("adminSecurityManage", AdminSecurityManage.class);
}
private AuthManager authManager() {
return readField("authManager", AuthManager.class);
}
private SecurityManager securityManager() {
return readField("securityManager", SecurityManager.class);
}
private LocalFileService localFileService() {
return readField("localFileService", LocalFileService.class);
}
private SealTokenService sealTokenService() {
return readField("sealTokenService", SealTokenService.class);
}
private SealConfig sealConfig() {
return readField("sealConfig", SealConfig.class);
}
private PartnerManageService partnerManageService() {
return readField("partnerManageService", PartnerManageService.class);
}
private <T> T readField(String fieldName, Class<T> type) {
try {
Field field = findField(this.getClass(), fieldName);
field.setAccessible(true);
return type.cast(field.get(this));
} catch (Exception ex) {
throw new IllegalStateException("读取字段失败: " + fieldName, ex);
}
}
private Field findField(Class<?> type, String fieldName) throws NoSuchFieldException {
Class<?> current = type;
while (current != null) {
try {
return current.getDeclaredField(fieldName);
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
}
}
throw new NoSuchFieldException(fieldName);
}
private boolean isOurParty(ContractLaunchPartyDto party) {
return party != null && "OUR".equalsIgnoreCase(trimText(party.getRoleCode()));
}
private boolean isOppositeParty(ContractLaunchPartyDto party) {
return party != null && "OPPOSITE".equalsIgnoreCase(trimText(party.getRoleCode()));
}
private String trimText(String value) {
return value == null ? null : value.trim();
}
class SealPositionMultipartFile implements MultipartFile {
private final byte[] content;
private final String originalFilename;
private final String contentType;
public SealPositionMultipartFile(byte[] content, String originalFilename, String contentType) {
this.content = content == null ? new byte[0] : content;
this.originalFilename = originalFilename;
this.contentType = contentType;
}
@Override
public String getName() {
return "file";
}
@Override
public String getOriginalFilename() {
return originalFilename;
}
@Override
public String getContentType() {
return contentType;
}
@Override
public boolean isEmpty() {
return content.length == 0;
}
@Override
public long getSize() {
return content.length;
}
@Override
public byte[] getBytes() {
return content;
}
@Override
public InputStream getInputStream() {
return new ByteArrayInputStream(content);
}
@Override
public void transferTo(File dest) throws IOException {
try (FileOutputStream outputStream = new FileOutputStream(dest)) {
outputStream.write(content);
}
}
}
} }
@@ -22,6 +22,11 @@ import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto; import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionBatchStatusDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionCheckResultDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionPageDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionSaveDto;
import com.nbport.zgwl.contractlaunch.service.ContractLaunchLegalFileBridgeService; import com.nbport.zgwl.contractlaunch.service.ContractLaunchLegalFileBridgeService;
import com.nbport.zgwl.contractlaunch.mapper.ContractLaunchMapper; import com.nbport.zgwl.contractlaunch.mapper.ContractLaunchMapper;
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchCodeUtils; import com.nbport.zgwl.contractlaunch.utils.ContractLaunchCodeUtils;
@@ -36,6 +41,7 @@ import com.nbport.zgwl.seal.service.SealTokenService;
import com.nbport.zgwl.seal.config.SealConfig; import com.nbport.zgwl.seal.config.SealConfig;
import com.nbport.zgwl.seal.utils.SealHttpClient; import com.nbport.zgwl.seal.utils.SealHttpClient;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject; import com.alibaba.fastjson2.JSONObject;
import com.nbport.zgwl.service.LocalFileService; import com.nbport.zgwl.service.LocalFileService;
import com.nbport.zgwl.entity.SysUserEntity; import com.nbport.zgwl.entity.SysUserEntity;
@@ -48,6 +54,12 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatter;
import java.util.ArrayList; import java.util.ArrayList;
@@ -688,6 +700,11 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
} }
} }
ContractLaunchSealPositionCheckResultDto checkResult = checkSealPositions(id, targetParty.getId());
if (checkResult != null && !checkResult.isPassed()) {
throw new ServiceException("该相对方还有 " + checkResult.getMissingCount() + " 个必签位置未完成签署,请先完成签章后再确认");
}
Map<String, Object> confirmMap = getCounterpartyConfirmMap(id); Map<String, Object> confirmMap = getCounterpartyConfirmMap(id);
String confirmKey = buildCounterpartyConfirmKey(targetParty); String confirmKey = buildCounterpartyConfirmKey(targetParty);
Map<String, Object> entry = new LinkedHashMap<>(); Map<String, Object> entry = new LinkedHashMap<>();
@@ -713,6 +730,188 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
return getCounterpartyConfirmFiltered(id); return getCounterpartyConfirmFiltered(id);
} }
@Override
@Transactional(rollbackFor = Exception.class)
public ContractLaunchSealPositionPageDto initSealPosition(Long id) {
ContractLaunchDto contract = getDetail(id);
assertSealPositionConfigAccess(contract);
SealPreviewData previewData;
SysUserEntity currentUser = adminSecurityManage.getUser();
if (isBlank(contract.getSealContractId())) {
previewData = startSealPreview(contract);
contract.setSealContractId(previewData.contractId());
contract.setSealPdfImageList(JsonUtils.toJson(previewData.pdfToImageList()));
contract.setSealPosConfigured(defaultIfBlank(contract.getSealPosConfigured(), ContractLaunchConstants.FLAG_NO));
contract.setUpdateBy(currentUser.getUsername());
contract.setUpdateTime(LocalDateTime.now());
contractLaunchMapper.updateContract(contract);
insertLog(id, ContractLaunchConstants.ACTION_SEAL_CREATE, "初始化签署文件", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "已基于水印文件创建签章平台合同", previewData, currentUser);
} else {
previewData = previewSealContract(contract.getSealContractId(), contract.getSealPdfImageList());
if (!previewData.pdfToImageList().isEmpty()) {
contract.setSealPdfImageList(JsonUtils.toJson(previewData.pdfToImageList()));
contract.setUpdateBy(currentUser.getUsername());
contract.setUpdateTime(LocalDateTime.now());
contractLaunchMapper.updateContract(contract);
}
}
return buildSealPositionPage(getDetail(id), previewData);
}
@Override
public ContractLaunchSealPositionPageDto getSealPositionPage(Long id) {
ContractLaunchDto contract = getDetail(id);
assertSealPositionViewAccess(contract);
return buildSealPositionPage(contract, previewSealContract(contract.getSealContractId(), contract.getSealPdfImageList()));
}
@Override
@Transactional(rollbackFor = Exception.class)
public ContractLaunchSealPositionPageDto saveSealPositions(Long id, ContractLaunchSealPositionSaveDto saveDto) {
ContractLaunchDto contract = getDetail(id);
assertSealPositionConfigAccess(contract);
if (!ContractLaunchConstants.STATUS_PENDING_SIGN.equals(contract.getStatus())) {
throw new ServiceException("仅待签署状态允许配置签章位");
}
if (isBlank(contract.getSealContractId())) {
throw new ServiceException("请先初始化签署文件");
}
SysUserEntity currentUser = adminSecurityManage.getUser();
String operator = currentUser.getUsername();
Map<Long, ContractLaunchPartyDto> partyMap = contract.getPartyList() == null ? Map.of() : contract.getPartyList().stream()
.collect(Collectors.toMap(ContractLaunchPartyDto::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
List<ContractLaunchSealPositionDto> newPositions = new ArrayList<>();
if (saveDto != null && saveDto.getPositions() != null) {
for (ContractLaunchSealPositionDto item : saveDto.getPositions()) {
validateSealPositionForSave(item, partyMap);
ContractLaunchSealPositionDto position = new ContractLaunchSealPositionDto();
position.setContractId(id);
position.setPartyId(item.getPartyId());
position.setLabel(trim(item.getLabel()));
position.setRequired(normalizeRequiredFlag(item.getRequired()));
position.setPageNum(item.getPageNum());
position.setX(roundCoordinate(item.getX()));
position.setY(roundCoordinate(item.getY()));
position.setWidth(roundCoordinate(item.getWidth()));
position.setHeight(roundCoordinate(item.getHeight()));
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_EMPTY);
position.setCreateBy(operator);
position.setUpdateBy(operator);
position.setIsDeleted(ContractLaunchConstants.IS_DELETED_NO);
newPositions.add(position);
}
}
contractLaunchMapper.deleteSealPositionByContractId(id, operator);
if (!newPositions.isEmpty()) {
// Oracle 的 INSERT ALL 在一条语句里插多行时会让 NEXTVAL 行为异常,这里改成逐条插入更稳。
for (ContractLaunchSealPositionDto position : newPositions) {
contractLaunchMapper.batchInsertSealPosition(List.of(position), operator, operator);
}
}
contract.setSealPosConfigured(newPositions.isEmpty() ? ContractLaunchConstants.FLAG_NO : ContractLaunchConstants.FLAG_YES);
contract.setUpdateBy(operator);
contract.setUpdateTime(LocalDateTime.now());
contractLaunchMapper.updateContract(contract);
insertLog(id, ContractLaunchConstants.ACTION_SEAL_POSITION_CONFIG, "保存签章位", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "已保存签章位配置,共 " + newPositions.size() + "", saveDto, currentUser);
return getSealPositionPage(id);
}
@Override
@Transactional(rollbackFor = Exception.class)
public ContractLaunchSealPositionPageDto batchUpdateSealPositionStatus(Long id, ContractLaunchSealPositionBatchStatusDto batchDto) {
ContractLaunchDto contract = getDetail(id);
assertSealPositionViewAccess(contract);
if (batchDto == null || batchDto.getItems() == null || batchDto.getItems().isEmpty()) {
throw new ServiceException("签章位状态更新数据不能为空");
}
SysUserEntity currentUser = adminSecurityManage.getUser();
List<Long> availablePartyIds = resolveAvailablePartyIds(contract, currentUser);
Long targetPartyId = resolveTargetPartyId(batchDto.getPartyId(), availablePartyIds);
Map<Long, ContractLaunchSealPositionDto> positionMap = contractLaunchMapper.selectSealPositionByContractId(id).stream()
.collect(Collectors.toMap(ContractLaunchSealPositionDto::getId, item -> item, (a, b) -> a, LinkedHashMap::new));
LocalDateTime now = LocalDateTime.now();
for (ContractLaunchSealPositionBatchStatusDto.Item item : batchDto.getItems()) {
ContractLaunchSealPositionDto position = positionMap.get(item.getId());
if (position == null) {
throw new ServiceException("签章位不存在: " + item.getId());
}
if (!Objects.equals(position.getPartyId(), targetPartyId)) {
throw new ServiceException("签章位不属于当前签署方");
}
applySealPositionStatus(position, batchDto, item, currentUser, now);
contractLaunchMapper.updateSealPosition(position);
}
if (Objects.equals(batchDto.getSignedStatus(), ContractLaunchConstants.SEAL_POS_STATUS_SIGNING)
&& ContractLaunchConstants.STATUS_PENDING_SIGN.equals(contract.getStatus())) {
contract.setStatus(ContractLaunchConstants.STATUS_SIGNING);
contract.setSealContractStatus(20);
contract.setUpdateBy(currentUser.getUsername());
contract.setUpdateTime(now);
contractLaunchMapper.updateContract(contract);
}
insertLog(id, ContractLaunchConstants.ACTION_SEAL_POSITION_STATUS, "更新签章位状态", ContractLaunchConstants.ACTION_STATUS_SUCCESS, "已批量更新签章位状态", batchDto, currentUser);
return getSealPositionPage(id);
}
@Override
public ContractLaunchSealPositionCheckResultDto checkSealPositions(Long id, Long partyId) {
ContractLaunchDto contract = getDetail(id);
assertSealPositionViewAccess(contract);
SysUserEntity currentUser = adminSecurityManage.getUser();
List<Long> availablePartyIds = resolveAvailablePartyIds(contract, currentUser);
if (partyId != null && !availablePartyIds.isEmpty() && !availablePartyIds.contains(partyId) && !securityManager.isSuperAdmin()) {
throw new ServiceException("当前用户不可校验该签署方");
}
List<ContractLaunchSealPositionDto> positions = contractLaunchMapper.selectSealPositionByContractId(id).stream()
.filter(item -> ContractLaunchConstants.FLAG_YES.equals(normalizeRequiredFlag(item.getRequired())))
.filter(item -> partyId == null || Objects.equals(item.getPartyId(), partyId))
.filter(item -> !Objects.equals(item.getSignedStatus(), ContractLaunchConstants.SEAL_POS_STATUS_SIGNED))
.collect(Collectors.toList());
ContractLaunchSealPositionCheckResultDto result = new ContractLaunchSealPositionCheckResultDto();
result.setPassed(positions.isEmpty());
result.setMissingCount(positions.size());
result.setMissingPositions(positions);
return result;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void handleSealPresignCallback(Map<String, Object> callbackData) {
String requestId = stringValue(callbackData.get("requestId"));
if (isBlank(requestId)) {
return;
}
List<ContractLaunchSealPositionDto> positions = contractLaunchMapper.selectSealPositionByRequestId(requestId);
if (positions == null || positions.isEmpty()) {
return;
}
boolean success = !isSealPresignFailed(callbackData);
LocalDateTime now = LocalDateTime.now();
String signer = stringValue(callbackData.get("signer"));
String signerId = stringValue(callbackData.get("signerId"));
String operator = isBlank(signer) ? "seal_callback" : signer;
Long contractId = positions.get(0).getContractId();
for (ContractLaunchSealPositionDto position : positions) {
if (success) {
position.setSignedStatus(ContractLaunchConstants.SEAL_POS_STATUS_SIGNED);
position.setSignedByName(signer);
position.setSignedById(signerId);
position.setSignedTime(now);
} else {
position.setSignedStatus(isBlank(position.getSealId()) ? ContractLaunchConstants.SEAL_POS_STATUS_EMPTY : ContractLaunchConstants.SEAL_POS_STATUS_SELECTED);
position.setRequestId(null);
position.setSignedByName(null);
position.setSignedById(null);
position.setSignedTime(null);
}
position.setUpdateBy(operator);
position.setUpdateTime(now);
contractLaunchMapper.updateSealPosition(position);
}
insertLog(contractId, ContractLaunchConstants.ACTION_SEAL_POSITION_STATUS, success ? "预盖章回调成功" : "预盖章回调失败", success ? ContractLaunchConstants.ACTION_STATUS_SUCCESS : ContractLaunchConstants.ACTION_STATUS_FAILED, success ? "签章位已根据回调更新为已签" : "签章位已根据回调回滚为可重试状态", callbackData, adminSecurityManage.getUser());
}
/** /**
* 【测试按钮】模拟合同采集。 * 【测试按钮】模拟合同采集。
*/ */
@@ -1228,8 +1427,10 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
logDto.setActionType(actionType); logDto.setActionType(actionType);
logDto.setActionName(actionName); logDto.setActionName(actionName);
logDto.setActionStatus(actionStatus); logDto.setActionStatus(actionStatus);
logDto.setOperatorName(currentUser.getUsername()); String operatorName = currentUser == null || currentUser.getUsername() == null || currentUser.getUsername().isBlank()
logDto.setOperatorPhone(currentUser.getMobilePhone()); ? "system" : currentUser.getUsername();
logDto.setOperatorName(operatorName);
logDto.setOperatorPhone(currentUser == null ? null : currentUser.getMobilePhone());
logDto.setActionTime(LocalDateTime.now()); logDto.setActionTime(LocalDateTime.now());
logDto.setActionContent(actionContent); logDto.setActionContent(actionContent);
logDto.setCallbackPayload(payload == null ? null : JSON.toJSONString(payload)); logDto.setCallbackPayload(payload == null ? null : JSON.toJSONString(payload));
@@ -0,0 +1,41 @@
package com.nbport.zgwl.contractlaunch.service;
import java.util.ArrayList;
import java.util.List;
/**
* 签章平台预览结果
*
* 不用 record保持和项目现有 Java 风格更一致
* 同时保留 record 风格的 accessor 命名方便 ServiceImpl 直接调用
*/
public class SealPreviewData {
private final String contractId;
private final Integer imageWidth;
private final Integer imageHeight;
private final List<String> pdfToImageList;
public SealPreviewData(String contractId, Integer imageWidth, Integer imageHeight, List<String> pdfToImageList) {
this.contractId = contractId;
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
this.pdfToImageList = pdfToImageList == null ? new ArrayList<>() : new ArrayList<>(pdfToImageList);
}
public String contractId() {
return contractId;
}
public Integer imageWidth() {
return imageWidth;
}
public Integer imageHeight() {
return imageHeight;
}
public List<String> pdfToImageList() {
return new ArrayList<>(pdfToImageList);
}
}
@@ -65,11 +65,22 @@ public final class ContractLaunchConstants {
public static final String ACTION_SEAL_UPLOAD = "seal_upload"; public static final String ACTION_SEAL_UPLOAD = "seal_upload";
/** 签章平台二维码生成(等待扫码确认) */ /** 签章平台二维码生成(等待扫码确认) */
public static final String ACTION_SEAL_QRCODE = "seal_qrcode"; public static final String ACTION_SEAL_QRCODE = "seal_qrcode";
public static final String ACTION_SEAL_POSITION_CONFIG = "seal_position_config";
public static final String ACTION_SEAL_POSITION_STATUS = "seal_position_status";
public static final String ACTION_STATUS_SUCCESS = "success"; public static final String ACTION_STATUS_SUCCESS = "success";
public static final String ACTION_STATUS_FAILED = "failed"; public static final String ACTION_STATUS_FAILED = "failed";
public static final String ACTION_STATUS_PENDING = "pending"; public static final String ACTION_STATUS_PENDING = "pending";
public static final String FLAG_YES = "Y";
public static final String FLAG_NO = "N";
public static final int SEAL_POS_STATUS_EMPTY = 0;
public static final int SEAL_POS_STATUS_SELECTED = 1;
public static final int SEAL_POS_STATUS_SIGNING = 2;
public static final int SEAL_POS_STATUS_SIGNED = 3;
public static final int SEAL_POS_STATUS_FAILED = 4;
public static final String IS_DELETED_NO = "N"; public static final String IS_DELETED_NO = "N";
public static final String IS_DELETED_YES = "Y"; public static final String IS_DELETED_YES = "Y";
} }
@@ -18,9 +18,6 @@ public interface PartnerManageMapper {
/** 获取相对方主表序列(Oracle 主键生成) */ /** 获取相对方主表序列(Oracle 主键生成) */
Long selectPartnerSeqNextVal(); Long selectPartnerSeqNextVal();
/** 获取联系人表序列(Oracle 主键生成) */
Long selectPartnerContactSeqNextVal();
/** 分页查询相对方列表 */ /** 分页查询相对方列表 */
IPage<PartnerManageDto> selectPartnerPage(Page<PartnerManageDto> page, @Param("query") PartnerQueryDto queryDto); IPage<PartnerManageDto> selectPartnerPage(Page<PartnerManageDto> page, @Param("query") PartnerQueryDto queryDto);
@@ -331,20 +331,22 @@ public class PartnerManageServiceImpl implements PartnerManageService {
int sortOrder = 1; int sortOrder = 1;
for (PartnerContactDto contact : contacts) { for (PartnerContactDto contact : contacts) {
contact.setId(partnerManageMapper.selectPartnerContactSeqNextVal());
contact.setPartnerRecordId(customerSysid); contact.setPartnerRecordId(customerSysid);
contact.setSortOrder(sortOrder++); contact.setSortOrder(sortOrder++);
contact.setDelFlag(PartnerManageConstants.CONTACT_DEL_FLAG_NORMAL); contact.setDelFlag(PartnerManageConstants.CONTACT_DEL_FLAG_NORMAL);
} }
String operator = resolveOperatorName();
for (PartnerContactDto contact : contacts) {
partnerManageMapper.batchInsertPartnerContacts( partnerManageMapper.batchInsertPartnerContacts(
contacts, List.of(contact),
resolveOperatorName(), operator,
now, now,
resolveOperatorName(), operator,
now, now,
PartnerManageConstants.CONTACT_DEL_FLAG_NORMAL); PartnerManageConstants.CONTACT_DEL_FLAG_NORMAL);
} }
}
private String ensureOppositeCode(String oppositeCode) { private String ensureOppositeCode(String oppositeCode) {
return ensureOppositeCode(oppositeCode, null); return ensureOppositeCode(oppositeCode, null);
@@ -2,12 +2,13 @@ package com.nbport.zgwl.seal.controller;
import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSON;
import com.nbport.zgwl.common.R; import com.nbport.zgwl.common.R;
import com.nbport.zgwl.contractlaunch.service.ContractLaunchService;
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants; import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants;
import com.nbport.zgwl.seal.handler.SealWebSocketHandler; import com.nbport.zgwl.seal.handler.SealWebSocketHandler;
import com.nbport.zgwl.seal.service.SealLogService; import com.nbport.zgwl.seal.service.SealLogService;
import lombok.RequiredArgsConstructor;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
@@ -15,36 +16,26 @@ import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
/**
* 签章平台回调接口控制器
*
* 接收签章平台推送的回调通知将关键回调写入合同操作日志表
* 实现签署过程全程留痕
*
* @author xiaofengzhou
*/
@RestController @RestController
@RequiredArgsConstructor
@RequestMapping("/seal/callback") @RequestMapping("/seal/callback")
public class SealCallbackController { public class SealCallbackController {
private static final Logger log = LoggerFactory.getLogger(SealCallbackController.class); private static final Logger log = LoggerFactory.getLogger(SealCallbackController.class);
@Autowired private final SealWebSocketHandler webSocketHandler;
private SealWebSocketHandler webSocketHandler; private final SealLogService sealLogService;
private final ContractLaunchService contractLaunchService;
@Autowired
private SealLogService sealLogService;
@PostMapping("/login") @PostMapping("/login")
public R<String> loginCallback(@RequestBody Map<String, Object> params) { public R<String> loginCallback(@RequestBody Map<String, Object> params) {
log.info("收到身份认证回调: {}", JSON.toJSONString(params)); log.info("收到身份认证回调: {}", JSON.toJSONString(params));
try { try {
String requestId = (String) params.get("requestId"); String requestId = stringValue(params.get("requestId"));
webSocketHandler.sendCallbackMessage(requestId, "login", params); webSocketHandler.sendCallbackMessage(requestId, "login", params);
// 身份认证回调记录日志不关联具体合同
sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK, sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
"身份认证回调", "身份认证回调已接收,requestId: " + requestId, params); "身份认证回调", "身份认证回调已接收,requestId: " + requestId, params);
return R.success("回调接收成功"); return R.success("回调接收成功");
@@ -60,20 +51,18 @@ public class SealCallbackController {
@PostMapping("/sign") @PostMapping("/sign")
public R<String> signCallback(@RequestBody Map<String, Object> params) { public R<String> signCallback(@RequestBody Map<String, Object> params) {
log.info("收到签署回调: {}", JSON.toJSONString(params)); log.info("收到签署回调: {}", JSON.toJSONString(params));
try { try {
String contractId = (String) params.get("contractId"); String contractId = stringValue(params.get("contractId"));
String requestId = (String) params.get("requestId"); String requestId = stringValue(params.get("requestId"));
webSocketHandler.sendCallbackMessage(requestId, "sign", params); webSocketHandler.sendCallbackMessage(requestId, "sign", params);
webSocketHandler.sendCallbackMessage(contractId, "contract_status", params); webSocketHandler.sendCallbackMessage(contractId, "contract_status", params);
// 签署回调记录日志关联到本系统合同
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId); Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK, sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
"签署回调", "收到签章平台签署回调,签章平台合同ID: " + contractId, params); "签署回调", "收到签章平台签署回调,签章平台合同ID: " + contractId, params);
return R.success("回调接收成功"); return R.success("回调接收成功");
} catch (Exception e) { } catch (Exception e) {
log.error("处理签署回调失败", e); log.error("处理签署回调失败", e);
String contractId = (String) params.get("contractId"); String contractId = stringValue(params.get("contractId"));
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId); Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK, sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
"签署回调", ContractLaunchConstants.ACTION_STATUS_FAILED, "签署回调", ContractLaunchConstants.ACTION_STATUS_FAILED,
@@ -85,36 +74,44 @@ public class SealCallbackController {
@PostMapping("/presign") @PostMapping("/presign")
public R<String> presignCallback(@RequestBody Map<String, Object> params) { public R<String> presignCallback(@RequestBody Map<String, Object> params) {
log.info("收到预盖章回调: {}", JSON.toJSONString(params)); log.info("收到预盖章回调: {}", JSON.toJSONString(params));
try { try {
String contractId = (String) params.get("contractId"); String contractId = stringValue(params.get("contractId"));
String requestId = (String) params.get("requestId"); String requestId = stringValue(params.get("requestId"));
webSocketHandler.sendCallbackMessage(requestId, "presign", params); webSocketHandler.sendCallbackMessage(requestId, "presign", params);
webSocketHandler.sendCallbackMessage(contractId, "contract_status", params); webSocketHandler.sendCallbackMessage(contractId, "contract_status", params);
// 预盖章回调记录日志关联到本系统合同 contractLaunchService.handleSealPresignCallback(params);
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId); Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK, sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
"预盖章回调", "收到签章平台预盖章回调,签章平台合同ID: " + contractId, params); "预盖章回调", "收到签章平台预盖章回调,签章平台合同ID: " + contractId, params);
return R.success("回调接收成功"); return R.success("回调接收成功");
} catch (Exception e) { } catch (Exception e) {
log.error("处理预盖章回调失败", e); log.error("处理预盖章回调失败", e);
String contractId = (String) params.get("contractId"); String contractId = stringValue(params.get("contractId"));
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId); Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK, sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
"预盖章回调", ContractLaunchConstants.ACTION_STATUS_FAILED, "预盖章回调", ContractLaunchConstants.ACTION_STATUS_FAILED,
"处理预盖章回调失败: " + e.getMessage(), params); "处理预盖章回调失败: " + e.getMessage(), params);
return R.error("回调处理失败: " + e.getMessage()); return R.error("回调处理失败: " + buildErrorMessage(e));
} }
} }
@PostMapping("/presign/mock-success")
public R<String> mockPresignSuccess(@RequestBody Map<String, Object> body) {
Map<String, Object> payload = new LinkedHashMap<>();
payload.put("requestId", body.get("requestId"));
payload.put("contractId", body.get("contractId"));
payload.put("signer", body.get("signer"));
payload.put("signerId", body.get("signerId"));
payload.put("contractStatus", body.getOrDefault("contractStatus", 20));
return presignCallback(payload);
}
@PostMapping("/trusteeship") @PostMapping("/trusteeship")
public R<String> trusteeshipCallback(@RequestBody Map<String, Object> params) { public R<String> trusteeshipCallback(@RequestBody Map<String, Object> params) {
log.info("收到托管章回调: {}", JSON.toJSONString(params)); log.info("收到托管章回调: {}", JSON.toJSONString(params));
try { try {
String requestId = (String) params.get("requestId"); String requestId = stringValue(params.get("requestId"));
webSocketHandler.sendCallbackMessage(requestId, "trusteeship", params); webSocketHandler.sendCallbackMessage(requestId, "trusteeship", params);
// 托管章回调记录日志
sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK, sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
"托管章回调", "收到签章平台托管章回调,requestId: " + requestId, params); "托管章回调", "收到签章平台托管章回调,requestId: " + requestId, params);
return R.success("回调接收成功"); return R.success("回调接收成功");
@@ -130,11 +127,9 @@ public class SealCallbackController {
@PostMapping("/signQrCode") @PostMapping("/signQrCode")
public R<String> signQrCodeCallback(@RequestBody Map<String, Object> params) { public R<String> signQrCodeCallback(@RequestBody Map<String, Object> params) {
log.info("收到签名二维码回调: {}", JSON.toJSONString(params)); log.info("收到签名二维码回调: {}", JSON.toJSONString(params));
try { try {
String requestId = (String) params.get("requestId"); String requestId = stringValue(params.get("requestId"));
webSocketHandler.sendCallbackMessage(requestId, "signQrCode", params); webSocketHandler.sendCallbackMessage(requestId, "signQrCode", params);
// 签名二维码回调记录日志
sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK, sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
"签名二维码回调", "收到签章平台签名二维码回调,requestId: " + requestId, params); "签名二维码回调", "收到签章平台签名二维码回调,requestId: " + requestId, params);
return R.success("回调接收成功"); return R.success("回调接收成功");
@@ -149,7 +144,18 @@ public class SealCallbackController {
@GetMapping("/poll/{requestId}") @GetMapping("/poll/{requestId}")
public R<Map<String, Object>> pollCallback(@PathVariable String requestId) { public R<Map<String, Object>> pollCallback(@PathVariable String requestId) {
Map<String, Object> callbackData = webSocketHandler.getCallbackData(requestId); return R.success(webSocketHandler.getCallbackData(requestId));
return R.success(callbackData); }
private String buildErrorMessage(Exception e) {
if (e == null) {
return "未知异常";
}
String message = e.getMessage();
return message == null || message.isBlank() ? e.toString() : message;
}
private String stringValue(Object value) {
return value == null ? null : String.valueOf(value).trim();
} }
} }
@@ -10,6 +10,7 @@ import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler; import org.springframework.web.socket.handler.TextWebSocketHandler;
import java.io.IOException; import java.io.IOException;
import java.util.LinkedHashMap;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
@@ -96,9 +97,13 @@ public class SealWebSocketHandler extends TextWebSocketHandler {
return; return;
} }
Map<String, Object> cacheEntry = new ConcurrentHashMap<>(); // 回调报文里可能出现 signerId 等可选字段为 null 的情况
// ConcurrentHashMap 不允许 null value这里使用普通 Map 缓存原始回调内容
Map<String, Object> cacheEntry = new LinkedHashMap<>();
cacheEntry.put("callbackType", callbackType); cacheEntry.put("callbackType", callbackType);
if (data != null) {
cacheEntry.putAll(data); cacheEntry.putAll(data);
}
callbackCache.put(requestId, cacheEntry); callbackCache.put(requestId, cacheEntry);
String sessionId = requestSessionMap.get(requestId); String sessionId = requestSessionMap.get(requestId);
@@ -21,6 +21,8 @@
<result property="approveMessage" column="APPROVE_MESSAGE"/> <result property="approveMessage" column="APPROVE_MESSAGE"/>
<result property="sealContractId" column="SEAL_CONTRACT_ID"/> <result property="sealContractId" column="SEAL_CONTRACT_ID"/>
<result property="sealContractStatus" column="SEAL_CONTRACT_STATUS"/> <result property="sealContractStatus" column="SEAL_CONTRACT_STATUS"/>
<result property="sealPosConfigured" column="SEAL_POS_CONFIGURED"/>
<result property="sealPdfImageList" column="SEAL_PDF_IMAGE_LIST"/>
<result property="contractAmount" column="CONTRACT_AMOUNT"/> <result property="contractAmount" column="CONTRACT_AMOUNT"/>
<result property="currencyName" column="CURRENCY_NAME"/> <result property="currencyName" column="CURRENCY_NAME"/>
<result property="contractPeriodType" column="CONTRACT_PERIOD_TYPE"/> <result property="contractPeriodType" column="CONTRACT_PERIOD_TYPE"/>
@@ -199,7 +201,7 @@
select ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID, select ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID,
CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE, CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE,
TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE, TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE,
SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, CONTRACT_AMOUNT, CURRENCY_NAME, SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, SEAL_POS_CONFIGURED, SEAL_PDF_IMAGE_LIST, CONTRACT_AMOUNT, CURRENCY_NAME,
CONTRACT_PERIOD_TYPE, PERIOD_EXPLAIN, EFFECTIVE_START, EFFECTIVE_END, CONTRACT_PERIOD_TYPE, PERIOD_EXPLAIN, EFFECTIVE_START, EFFECTIVE_END,
PAYMENT_METHOD, PRIMARY_CONTENT, AMOUNT_EXPLAIN, VALUATION_MODE, NEED_AMENDMENTS, ESTIMATED_AMOUNT, SEAL_TYPE, SEAL_QUANTITY, PAYMENT_METHOD, PRIMARY_CONTENT, AMOUNT_EXPLAIN, VALUATION_MODE, NEED_AMENDMENTS, ESTIMATED_AMOUNT, SEAL_TYPE, SEAL_QUANTITY,
TEXT_SOURCE, MANDATE_TYPE, CARGO_TYPE, ASSIST_DEPARTMENT_NAME, EXECUTOR_ACCOUNT, TEXT_SOURCE, MANDATE_TYPE, CARGO_TYPE, ASSIST_DEPARTMENT_NAME, EXECUTOR_ACCOUNT,
@@ -374,7 +376,7 @@
ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID, ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID,
CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE, CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE,
TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE, TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE,
SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, CONTRACT_AMOUNT, CURRENCY_NAME, SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, SEAL_POS_CONFIGURED, SEAL_PDF_IMAGE_LIST, CONTRACT_AMOUNT, CURRENCY_NAME,
CONTRACT_PERIOD_TYPE, PERIOD_EXPLAIN, EFFECTIVE_START, EFFECTIVE_END, CONTRACT_PERIOD_TYPE, PERIOD_EXPLAIN, EFFECTIVE_START, EFFECTIVE_END,
PAYMENT_METHOD, PRIMARY_CONTENT, AMOUNT_EXPLAIN, VALUATION_MODE, NEED_AMENDMENTS, ESTIMATED_AMOUNT, SEAL_TYPE, SEAL_QUANTITY, PAYMENT_METHOD, PRIMARY_CONTENT, AMOUNT_EXPLAIN, VALUATION_MODE, NEED_AMENDMENTS, ESTIMATED_AMOUNT, SEAL_TYPE, SEAL_QUANTITY,
TEXT_SOURCE, MANDATE_TYPE, CARGO_TYPE, ASSIST_DEPARTMENT_NAME, EXECUTOR_ACCOUNT, TEXT_SOURCE, MANDATE_TYPE, CARGO_TYPE, ASSIST_DEPARTMENT_NAME, EXECUTOR_ACCOUNT,
@@ -394,7 +396,7 @@
#{id}, #{contractSysId}, #{contractCode}, #{lawContractCode}, #{lawContractId}, #{id}, #{contractSysId}, #{contractCode}, #{lawContractCode}, #{lawContractId},
#{contractName}, #{contractType}, #{contractTypeCode}, #{contractSource}, #{creationMode}, #{contractName}, #{contractType}, #{contractTypeCode}, #{contractSource}, #{creationMode},
#{templateId}, #{templateCode}, #{templateName}, #{status}, #{approveStatus}, #{approveMessage}, #{templateId}, #{templateCode}, #{templateName}, #{status}, #{approveStatus}, #{approveMessage},
#{sealContractId}, #{sealContractStatus}, #{contractAmount}, #{currencyName}, #{sealContractId}, #{sealContractStatus}, #{sealPosConfigured}, #{sealPdfImageList}, #{contractAmount}, #{currencyName},
#{contractPeriodType}, #{periodExplain}, #{effectiveStart}, #{effectiveEnd}, #{contractPeriodType}, #{periodExplain}, #{effectiveStart}, #{effectiveEnd},
#{paymentMethod}, #{primaryContent}, #{amountExplain}, #{valuationMode}, #{needAmendments}, #{estimatedAmount}, #{sealType}, #{sealQuantity}, #{paymentMethod}, #{primaryContent}, #{amountExplain}, #{valuationMode}, #{needAmendments}, #{estimatedAmount}, #{sealType}, #{sealQuantity},
#{textSource}, #{mandateType}, #{cargoType}, #{assistDepartmentName}, #{executorAccount}, #{textSource}, #{mandateType}, #{cargoType}, #{assistDepartmentName}, #{executorAccount},
@@ -431,6 +433,8 @@
APPROVE_MESSAGE = #{approveMessage}, APPROVE_MESSAGE = #{approveMessage},
SEAL_CONTRACT_ID = #{sealContractId}, SEAL_CONTRACT_ID = #{sealContractId},
SEAL_CONTRACT_STATUS = #{sealContractStatus}, SEAL_CONTRACT_STATUS = #{sealContractStatus},
SEAL_POS_CONFIGURED = #{sealPosConfigured},
SEAL_PDF_IMAGE_LIST = #{sealPdfImageList},
CONTRACT_AMOUNT = #{contractAmount}, CONTRACT_AMOUNT = #{contractAmount},
CURRENCY_NAME = #{currencyName}, CURRENCY_NAME = #{currencyName},
CONTRACT_PERIOD_TYPE = #{contractPeriodType}, CONTRACT_PERIOD_TYPE = #{contractPeriodType},
@@ -2,14 +2,10 @@
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nbport.zgwl.partner.mapper.PartnerManageMapper"> <mapper namespace="com.nbport.zgwl.partner.mapper.PartnerManageMapper">
<select id="selectPartnerSeqNextVal" resultType="java.lang.Long"> <select id="selectPartnerSeqNextVal" resultType="java.lang.Long" useCache="false" flushCache="true">
select SEQ_ILDM_FEE_CUSTOMER_INFO.NEXTVAL from dual select SEQ_ILDM_FEE_CUSTOMER_INFO.NEXTVAL from dual
</select> </select>
<select id="selectPartnerContactSeqNextVal" resultType="java.lang.Long">
select SEQ_SEAL_PARTNER_CONTACT.NEXTVAL from dual
</select>
<resultMap id="PartnerManageResult" type="com.nbport.zgwl.partner.dto.PartnerManageDto"> <resultMap id="PartnerManageResult" type="com.nbport.zgwl.partner.dto.PartnerManageDto">
<id property="customerSysid" column="CUSTOMER_SYSID"/> <id property="customerSysid" column="CUSTOMER_SYSID"/>
<result property="customerRegno" column="CUSTOMER_REGNO"/> <result property="customerRegno" column="CUSTOMER_REGNO"/>
@@ -243,7 +239,7 @@
) )
VALUES VALUES
( (
#{item.id}, #{item.partnerRecordId}, #{item.contactName}, #{item.contactPhone}, SEQ_SEAL_PARTNER_CONTACT.NEXTVAL, #{item.partnerRecordId}, #{item.contactName}, #{item.contactPhone},
#{item.postalAddress}, #{item.email}, #{item.sortOrder}, #{item.postalAddress}, #{item.email}, #{item.sortOrder},
#{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{item.remark} #{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{item.remark}
) )