签章位置固定功能
This commit is contained in:
+40
-252
@@ -6,11 +6,14 @@ import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchCollectSaveDto;
|
||||
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.ContractLaunchLawCallbackSampleDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
|
||||
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.utils.ContractLaunchConstants;
|
||||
import com.nbport.zgwl.entity.SysUserEntity;
|
||||
@@ -37,9 +40,6 @@ import java.util.Map;
|
||||
* 2. 模拟合同采集
|
||||
* 3. 合同归档
|
||||
* 4. 回写签章结果
|
||||
*
|
||||
* 这些测试接口在法务系统不可用期间便于你完整演示流程,
|
||||
* 后续接入真实系统后,保留或删除都很方便。
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@@ -49,302 +49,100 @@ public class ContractLaunchController {
|
||||
private final ContractLaunchService contractLaunchService;
|
||||
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")
|
||||
public R<PageResult<ContractLaunchDto>> queryList(@RequestBody ContractLaunchQueryDto queryDto) {
|
||||
return R.success(contractLaunchService.queryPage(queryDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询合同详情(含所有子表数据)。
|
||||
*
|
||||
* 返回合同基本信息 + 合同方列表 + 文件列表 + 履行计划 + 操作日志。
|
||||
*
|
||||
* 对应前端功能:合同详情页 / 编辑页面回显
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public R<ContractLaunchDto> getDetail(@PathVariable("id") Long id) {
|
||||
return R.success(contractLaunchService.getDetail(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录人的我方组织信息(当前先返回 mock 数据)。
|
||||
*
|
||||
* 用途:
|
||||
* 1. 发起合同页面只读展示“我方”信息
|
||||
* 2. 后续接入真实组织主数据时,前端不需要再改字段来源
|
||||
*/
|
||||
@GetMapping("/self-profile")
|
||||
public R<Object> getSelfProfile() {
|
||||
SysUserEntity currentUser = adminSecurityManage.getUser();
|
||||
return R.success(currentUser == null ? null : currentUser.getSysOrgEntity());
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建合同草稿。
|
||||
*
|
||||
* 流程:生成 contractSysId(CLS-xxx)和 contractCode → 保存主表 → 保存子表(合同方/文件/履行计划)→ 记录操作日志
|
||||
* 状态变为:draft(草稿)
|
||||
*
|
||||
* 必填校验项:合同名称、合同类型、合同类型编码、计价方式、币种、签订方式、合同期限类型、至少一个甲方+乙方
|
||||
*
|
||||
* 对应前端功能:新建合同 → 保存草稿
|
||||
*/
|
||||
@PostMapping
|
||||
public R<ContractLaunchDto> createDraft(@RequestBody ContractLaunchDto contract) {
|
||||
return R.success(contractLaunchService.createDraft(contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑合同草稿。
|
||||
*
|
||||
* 仅 draft(草稿)或 rejected(已驳回)状态允许编辑。
|
||||
* 逻辑:先删子表旧数据,再以当前数据重新插入(替换式更新)。
|
||||
* 保留原有的 contractSysId、contractCode、创建人/创建时间 不变。
|
||||
*
|
||||
* 对应前端功能:编辑草稿 → 保存
|
||||
*/
|
||||
@PutMapping
|
||||
public R<ContractLaunchDto> updateDraft(@RequestBody ContractLaunchDto contract) {
|
||||
return R.success(contractLaunchService.updateDraft(contract));
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制合同并生成新草稿。
|
||||
*
|
||||
* 业务设计:
|
||||
* 1. 允许从任意未删除合同复制出一份新的草稿,方便高频复用历史合同
|
||||
* 2. 保留主信息、合同方、正文附件、履行计划等业务内容
|
||||
* 3. 重置系统编号、合同编号、审批/签署/归档状态与相关痕迹,避免串流程数据
|
||||
*
|
||||
* 对应前端功能:列表页 → 复制合同
|
||||
*/
|
||||
@PostMapping("/{id}/copy")
|
||||
public R<ContractLaunchDto> copyDraft(@PathVariable("id") Long id) {
|
||||
return R.success(contractLaunchService.copyDraft(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 基于现有合同发起变更/附加合同草稿。
|
||||
*
|
||||
* 处理原则:
|
||||
* 1. 复制现有业务内容
|
||||
* 2. 自动关联主合同编码 mainContractCode
|
||||
* 3. 生成新的草稿,供业务在此基础上做变更补充
|
||||
*/
|
||||
@PostMapping("/{id}/change")
|
||||
public R<ContractLaunchDto> createChangeDraft(@PathVariable("id") Long id) {
|
||||
return R.success(contractLaunchService.createChangeDraft(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交法务审批。
|
||||
*
|
||||
* 状态流转:draft/rejected → reviewing
|
||||
* 调用结算平台 pushContract 接口将合同数据推送给法务系统开始审批流程。
|
||||
* 目前法务接口不可用,仅做状态变更 + 记录日志(TODO:联调时打开 settlementPushService.pushContract 调用)。
|
||||
*
|
||||
* 推送法务的报文包含:合同基本字段 + contractOpposites(相对方) + contractKxPlans(履行计划) + 附件等
|
||||
*
|
||||
* 对应前端功能:提交审批按钮
|
||||
*/
|
||||
@PostMapping("/{id}/submit")
|
||||
public R<ContractLaunchDto> submitToLaw(@PathVariable("id") Long id) {
|
||||
return R.success(contractLaunchService.submitToLaw(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 【测试按钮】模拟法务审批通过。
|
||||
*
|
||||
* 状态流转:reviewing → pending_sign(待签署)
|
||||
* 生成模拟的 lawContractCode 和 lawContractId 记录到合同上,以便后续流程使用。
|
||||
* 可在请求体中传入 approveMessage(审批意见)、lawContractCode、lawContractId 覆盖自动生成的值。
|
||||
*
|
||||
* 真实场景下,此方法应由法务系统的回调接口替代(见 lawApproveCallback)。
|
||||
*
|
||||
* 对应前端功能:测试按钮 → 模拟审批通过
|
||||
*/
|
||||
@PostMapping("/{id}/mock-approve")
|
||||
public R<ContractLaunchDto> mockApprove(@PathVariable("id") Long id,
|
||||
@RequestBody(required = false) ContractLaunchApproveDto approveDto) {
|
||||
return R.success(contractLaunchService.mockApprove(id, approveDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 【测试按钮】模拟法务审批驳回。
|
||||
*
|
||||
* 状态流转:reviewing → rejected(已驳回)
|
||||
* 可在请求体中传入 approveMessage(驳回原因)。
|
||||
* 驳回后合同可再次编辑和提交。
|
||||
*
|
||||
* 对应前端功能:测试按钮 → 模拟驳回
|
||||
*/
|
||||
@PostMapping("/{id}/mock-reject")
|
||||
public R<ContractLaunchDto> mockReject(@PathVariable("id") Long id,
|
||||
@RequestBody(required = false) ContractLaunchApproveDto approveDto) {
|
||||
return R.success(contractLaunchService.mockReject(id, approveDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取法务审批回调样例。
|
||||
*
|
||||
* 用途:
|
||||
* 1. 前端展示与法务协商的固定 data 结构
|
||||
* 2. 本地联调时复制到 Postman,直接调用 callback 接口
|
||||
*
|
||||
* 说明:
|
||||
* - 样例中的水印文件路径当前先复用本地合同正文路径
|
||||
* - 后续法务真正实现文件传输后,只需要替换 watermarkFile.filePath 来源
|
||||
*/
|
||||
@GetMapping("/{id}/callback/law/approve/sample")
|
||||
public R<ContractLaunchLawCallbackSampleDto> getLawApproveCallbackSample(@PathVariable("id") Long 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")
|
||||
public R<ContractLaunchDto> lawApproveCallback(@RequestBody ContractLaunchLawCallbackRequestDto callbackRequest) {
|
||||
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")
|
||||
public R<ContractLaunchDto> saveSealInfo(@PathVariable("id") Long id,
|
||||
@RequestBody ContractLaunchSealDto sealDto) {
|
||||
return R.success(contractLaunchService.saveSealInfo(id, sealDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 签署完成回调(浙江CA安全签/线下签署完成后调用)。
|
||||
*
|
||||
* 所有签署方完成签署后,将签署完成的合同文件信息回写。
|
||||
* 状态流转:signing → pending_collect(待采集)
|
||||
*
|
||||
* 请求体 ContractLaunchSealDto 字段:
|
||||
* sealContractStatus - 签署状态(2:签署完成)
|
||||
* signedFileName - 签署完成后的文件名
|
||||
* signedFilePath - 签署文件存储路径
|
||||
* signedFileType - 文件类型
|
||||
* signedFileSize - 文件大小
|
||||
* signedOfflineReason - 线下签署原因(如果未在线签署)
|
||||
*
|
||||
* 签署文件会自动保存到 file 子表(fileCategory = "signed"),先删旧签署文件再插入新的。
|
||||
*
|
||||
* 对应前端功能:签署完成 → 回写签署结果
|
||||
*/
|
||||
@PostMapping("/{id}/seal/finish")
|
||||
public R<ContractLaunchDto> finishSeal(@PathVariable("id") Long id,
|
||||
@RequestBody ContractLaunchSealDto sealDto) {
|
||||
return R.success(contractLaunchService.markSealCompleted(id, sealDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 【测试按钮】模拟合同采集。
|
||||
*
|
||||
* 合同签署完成后,调用数据采集 V3.0 的 collectContract 接口将合同信息推送到法务系统存档。
|
||||
* 目前法务接口不可用,仅做状态变更 + 记录日志(TODO:联调时打开 dataCollectionPushService.collectContract 调用)。
|
||||
*
|
||||
* 状态流转:pending_collect → pending_archive(待归档)
|
||||
*
|
||||
* 推送数据采集的报文包含:合同基本字段 + 用印信息(c_seal)+ 签订信息(c_sign)+ 附件等
|
||||
*
|
||||
* 对应前端功能:测试按钮 → 模拟采集
|
||||
*/
|
||||
@PostMapping("/{id}/mock-collect")
|
||||
public R<ContractLaunchDto> mockCollect(@PathVariable("id") Long 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")
|
||||
public R<ContractLaunchDto> saveCollectInfo(@PathVariable("id") Long id,
|
||||
@RequestBody ContractLaunchCollectSaveDto collectDto) {
|
||||
@RequestBody ContractLaunchCollectSaveDto collectDto) {
|
||||
return R.success(contractLaunchService.saveCollectInfo(id, collectDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对方签署确认状态。
|
||||
*/
|
||||
@GetMapping("/{id}/counterparty-confirm")
|
||||
public R<Map<String, Object>> getCounterpartyConfirm(@PathVariable("id") Long id) {
|
||||
return R.success(contractLaunchService.getCounterpartyConfirmFiltered(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 相对方确认已签署完成。
|
||||
*
|
||||
* 请求体:{"partyId": 123}
|
||||
* 后端自动获取当前登录用户信息写入确认记录。
|
||||
*/
|
||||
@PostMapping("/{id}/counterparty-confirm")
|
||||
public R<Map<String, Object>> confirmCounterparty(@PathVariable("id") Long id,
|
||||
@RequestBody Map<String, String> body) {
|
||||
@RequestBody Map<String, String> body) {
|
||||
String partyIdValue = body.get("partyId");
|
||||
if (partyIdValue == null || partyIdValue.isBlank()) {
|
||||
return R.error("partyId 不能为空");
|
||||
@@ -356,53 +154,51 @@ public class ContractLaunchController {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传法务水印合同文件。
|
||||
*
|
||||
* 当法务审批回调未返回带水印的合同文件时,允许用户手动上传。
|
||||
* 上传后的文件路径替换 contractTextFilePath,供后续在线签订使用。
|
||||
*
|
||||
* 请求方式:multipart/form-data
|
||||
* 参数:file(MultipartFile)
|
||||
*
|
||||
* 对应前端功能:签署准备页面的"上传带水印合同"区域
|
||||
*/
|
||||
@PostMapping("/{id}/seal-position/init")
|
||||
public R<ContractLaunchSealPositionPageDto> initSealPosition(@PathVariable("id") Long id) {
|
||||
return R.success(contractLaunchService.initSealPosition(id));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/seal-position/list")
|
||||
public R<ContractLaunchSealPositionPageDto> getSealPositionPage(@PathVariable("id") Long id) {
|
||||
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")
|
||||
public R<ContractLaunchDto> uploadWatermarkFile(@PathVariable("id") Long id,
|
||||
@RequestParam("file") MultipartFile file) {
|
||||
@RequestParam("file") MultipartFile 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")
|
||||
public R<ContractLaunchDto> archive(@PathVariable("id") Long id,
|
||||
@RequestBody(required = false) ContractLaunchArchiveDto archiveDto) {
|
||||
return R.success(contractLaunchService.archive(id, archiveDto));
|
||||
}
|
||||
|
||||
/**
|
||||
* 合同终止。
|
||||
*
|
||||
* 允许除 completed 外的任意环节执行终止。
|
||||
* 当前仅在本系统内变更状态并记录日志,法务系统终止需后续分别操作。
|
||||
*/
|
||||
@PostMapping("/{id}/terminate")
|
||||
public R<ContractLaunchDto> terminate(@PathVariable("id") Long id,
|
||||
@RequestBody(required = false) Map<String, Object> payload) {
|
||||
@@ -411,14 +207,6 @@ public class ContractLaunchController {
|
||||
return R.success(contractLaunchService.terminate(id, terminateReason));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除合同草稿(逻辑删除)。
|
||||
*
|
||||
* 仅 draft(草稿)或 rejected(已驳回)状态允许删除。
|
||||
* 逻辑:设置 isDeleted = 'Y',不物理删除数据。
|
||||
*
|
||||
* 对应前端功能:删除草稿按钮
|
||||
*/
|
||||
@DeleteMapping("/{id}")
|
||||
public R<String> deleteDraft(@PathVariable("id") Long id) {
|
||||
contractLaunchService.deleteDraft(id);
|
||||
|
||||
@@ -84,6 +84,12 @@ public class ContractLaunchDto implements Serializable {
|
||||
/** 签章平台合同ID(浙江CA安全签创建后返回) */
|
||||
private String sealContractId;
|
||||
|
||||
/** 签章位是否已配置(Y/N)。 */
|
||||
private String sealPosConfigured;
|
||||
|
||||
/** PDF 预览图片地址列表 JSON。 */
|
||||
private String sealPdfImageList;
|
||||
|
||||
/** 签章状态(1:签署中, 2:签署完成, 3:签署失败, null:未发起) */
|
||||
private Integer sealContractStatus;
|
||||
|
||||
@@ -402,4 +408,7 @@ public class ContractLaunchDto implements Serializable {
|
||||
|
||||
/** 操作日志列表 */
|
||||
private List<ContractLaunchLogDto> logList = new ArrayList<>();
|
||||
|
||||
/** 当前合同的签章位列表。 */
|
||||
private List<ContractLaunchSealPositionDto> sealPositionList = new ArrayList<>();
|
||||
}
|
||||
|
||||
+45
@@ -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;
|
||||
}
|
||||
}
|
||||
+24
@@ -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;
|
||||
}
|
||||
+42
@@ -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;
|
||||
}
|
||||
+24
@@ -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.extension.plugins.pagination.Page;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveEleDto;
|
||||
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.ContractLaunchLogDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealPositionDto;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
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;
|
||||
|
||||
public interface ContractLaunchMapper {
|
||||
|
||||
/** 获取主表序列(Oracle 主键生成) */
|
||||
Long selectMainSeqNextVal();
|
||||
|
||||
/** 获取合同方表序列 */
|
||||
Long selectPartySeqNextVal();
|
||||
|
||||
/** 获取文件表序列 */
|
||||
Long selectFileSeqNextVal();
|
||||
|
||||
/** 获取履行计划表序列 */
|
||||
Long selectPlanSeqNextVal();
|
||||
|
||||
/** 获取日志表序列 */
|
||||
Long selectLogSeqNextVal();
|
||||
|
||||
/** 获取纸质归档表序列 */
|
||||
Long selectArchivePaperSeqNextVal();
|
||||
|
||||
/** 获取电子归档表序列 */
|
||||
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);
|
||||
|
||||
/** 根据主键查询合同 */
|
||||
ContractLaunchDto selectContractById(@Param("id") Long id);
|
||||
|
||||
/** 根据主键并带权限条件查询合同 */
|
||||
ContractLaunchDto selectContractByIdWithScope(@Param("id") Long id, @Param("query") ContractLaunchQueryDto queryDto);
|
||||
|
||||
/** 根据 contractSysId 查询合同(法务回调时用于定位) */
|
||||
ContractLaunchDto selectContractByContractSysId(@Param("contractSysId") String contractSysId);
|
||||
|
||||
/** 根据 sealContractId 查询合同(签章平台回调时用于定位) */
|
||||
ContractLaunchDto selectContractBySealContractId(@Param("sealContractId") String sealContractId);
|
||||
|
||||
/** 新增合同 */
|
||||
int insertContract(ContractLaunchDto contract);
|
||||
|
||||
/** 修改合同 */
|
||||
int updateContract(ContractLaunchDto contract);
|
||||
|
||||
/** 逻辑删除合同 */
|
||||
int deleteContract(@Param("id") Long id, @Param("updateBy") String updateBy);
|
||||
|
||||
/** 批量插入合同方 */
|
||||
int batchInsertParty(@Param("list") List<ContractLaunchPartyDto> list,
|
||||
@Param("createBy") String createBy,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 批量插入文件 */
|
||||
int batchInsertFile(@Param("list") List<ContractLaunchFileDto> list,
|
||||
@Param("createBy") String createBy,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 批量插入履行计划 */
|
||||
int batchInsertPlan(@Param("list") List<ContractLaunchPlanDto> list,
|
||||
@Param("createBy") String createBy,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 批量插入纸质归档材料 */
|
||||
int batchInsertArchivePaper(@Param("list") List<ContractLaunchArchivePaperDto> list,
|
||||
@Param("createBy") String createBy,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 批量插入电子归档材料 */
|
||||
int batchInsertArchiveEle(@Param("list") List<ContractLaunchArchiveEleDto> list,
|
||||
@Param("createBy") String createBy,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 插入单条操作日志 */
|
||||
int insertLog(ContractLaunchLogDto logDto);
|
||||
|
||||
/** 删除合同下的所有合同方(逻辑删除) */
|
||||
int deletePartyByContractId(@Param("contractId") Long contractId,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 删除合同下的所有文件(逻辑删除) */
|
||||
int deleteFileByContractId(@Param("contractId") Long contractId,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 按分类删除合同下的文件(如重新上传正文时只删正文,不删其他附件) */
|
||||
int deleteFileByContractIdAndCategory(@Param("contractId") Long contractId,
|
||||
@Param("fileCategory") String fileCategory,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 回写法务附件路径缓存 */
|
||||
int updateFileLegalUrl(@Param("id") Long id,
|
||||
@Param("legalFileUrl") String legalFileUrl,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 删除合同下的所有履行计划(逻辑删除) */
|
||||
int deletePlanByContractId(@Param("contractId") Long contractId,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 删除合同下的纸质归档材料(逻辑删除) */
|
||||
int deleteArchivePaperByContractId(@Param("contractId") Long contractId,
|
||||
@Param("updateBy") String updateBy);
|
||||
|
||||
/** 删除合同下的电子归档材料(逻辑删除) */
|
||||
int deleteArchiveEleByContractId(@Param("contractId") Long contractId,
|
||||
@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);
|
||||
|
||||
/** 批量查询文件(按 contractIds) */
|
||||
List<ContractLaunchFileDto> selectFileByContractIds(@Param("contractIds") List<Long> contractIds);
|
||||
|
||||
/** 批量查询履行计划(按 contractIds) */
|
||||
List<ContractLaunchPlanDto> selectPlanByContractIds(@Param("contractIds") List<Long> contractIds);
|
||||
|
||||
/** 批量查询纸质归档材料(按 contractIds) */
|
||||
List<ContractLaunchArchivePaperDto> selectArchivePaperByContractIds(@Param("contractIds") List<Long> contractIds);
|
||||
|
||||
/** 批量查询电子归档材料(按 contractIds) */
|
||||
List<ContractLaunchArchiveEleDto> selectArchiveEleByContractIds(@Param("contractIds") List<Long> contractIds);
|
||||
|
||||
/** 批量查询操作日志(按 contractIds) */
|
||||
List<ContractLaunchLogDto> selectLogByContractIds(@Param("contractIds") List<Long> contractIds);
|
||||
}
|
||||
|
||||
+604
@@ -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;
|
||||
|
||||
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.contractlaunch.dto.ContractLaunchApproveDto;
|
||||
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.ContractLaunchLawCallbackDto;
|
||||
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.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 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.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public interface ContractLaunchService {
|
||||
|
||||
@@ -66,6 +102,24 @@ public interface ContractLaunchService {
|
||||
/** 相对方确认已签署完成 */
|
||||
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);
|
||||
|
||||
@@ -80,4 +134,544 @@ public interface ContractLaunchService {
|
||||
|
||||
/** 删除草稿(仅 draft/rejected 状态允许) */
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+203
-2
@@ -22,6 +22,11 @@ import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
|
||||
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto;
|
||||
import com.nbport.zgwl.contractlaunch.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.mapper.ContractLaunchMapper;
|
||||
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.utils.SealHttpClient;
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.alibaba.fastjson2.JSONArray;
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.nbport.zgwl.service.LocalFileService;
|
||||
import com.nbport.zgwl.entity.SysUserEntity;
|
||||
@@ -48,6 +54,12 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
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.format.DateTimeFormatter;
|
||||
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);
|
||||
String confirmKey = buildCounterpartyConfirmKey(targetParty);
|
||||
Map<String, Object> entry = new LinkedHashMap<>();
|
||||
@@ -713,6 +730,188 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
|
||||
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.setActionName(actionName);
|
||||
logDto.setActionStatus(actionStatus);
|
||||
logDto.setOperatorName(currentUser.getUsername());
|
||||
logDto.setOperatorPhone(currentUser.getMobilePhone());
|
||||
String operatorName = currentUser == null || currentUser.getUsername() == null || currentUser.getUsername().isBlank()
|
||||
? "system" : currentUser.getUsername();
|
||||
logDto.setOperatorName(operatorName);
|
||||
logDto.setOperatorPhone(currentUser == null ? null : currentUser.getMobilePhone());
|
||||
logDto.setActionTime(LocalDateTime.now());
|
||||
logDto.setActionContent(actionContent);
|
||||
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_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_FAILED = "failed";
|
||||
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_YES = "Y";
|
||||
}
|
||||
|
||||
@@ -18,9 +18,6 @@ public interface PartnerManageMapper {
|
||||
/** 获取相对方主表序列(Oracle 主键生成) */
|
||||
Long selectPartnerSeqNextVal();
|
||||
|
||||
/** 获取联系人表序列(Oracle 主键生成) */
|
||||
Long selectPartnerContactSeqNextVal();
|
||||
|
||||
/** 分页查询相对方列表 */
|
||||
IPage<PartnerManageDto> selectPartnerPage(Page<PartnerManageDto> page, @Param("query") PartnerQueryDto queryDto);
|
||||
|
||||
|
||||
@@ -331,19 +331,21 @@ public class PartnerManageServiceImpl implements PartnerManageService {
|
||||
|
||||
int sortOrder = 1;
|
||||
for (PartnerContactDto contact : contacts) {
|
||||
contact.setId(partnerManageMapper.selectPartnerContactSeqNextVal());
|
||||
contact.setPartnerRecordId(customerSysid);
|
||||
contact.setSortOrder(sortOrder++);
|
||||
contact.setDelFlag(PartnerManageConstants.CONTACT_DEL_FLAG_NORMAL);
|
||||
}
|
||||
|
||||
partnerManageMapper.batchInsertPartnerContacts(
|
||||
contacts,
|
||||
resolveOperatorName(),
|
||||
now,
|
||||
resolveOperatorName(),
|
||||
now,
|
||||
PartnerManageConstants.CONTACT_DEL_FLAG_NORMAL);
|
||||
String operator = resolveOperatorName();
|
||||
for (PartnerContactDto contact : contacts) {
|
||||
partnerManageMapper.batchInsertPartnerContacts(
|
||||
List.of(contact),
|
||||
operator,
|
||||
now,
|
||||
operator,
|
||||
now,
|
||||
PartnerManageConstants.CONTACT_DEL_FLAG_NORMAL);
|
||||
}
|
||||
}
|
||||
|
||||
private String ensureOppositeCode(String oppositeCode) {
|
||||
|
||||
@@ -2,12 +2,13 @@ package com.nbport.zgwl.seal.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSON;
|
||||
import com.nbport.zgwl.common.R;
|
||||
import com.nbport.zgwl.contractlaunch.service.ContractLaunchService;
|
||||
import com.nbport.zgwl.contractlaunch.utils.ContractLaunchConstants;
|
||||
import com.nbport.zgwl.seal.handler.SealWebSocketHandler;
|
||||
import com.nbport.zgwl.seal.service.SealLogService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
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.RestController;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 签章平台回调接口控制器
|
||||
*
|
||||
* 接收签章平台推送的回调通知,将关键回调写入合同操作日志表,
|
||||
* 实现签署过程全程留痕。
|
||||
*
|
||||
* @author xiaofengzhou
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/seal/callback")
|
||||
public class SealCallbackController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(SealCallbackController.class);
|
||||
|
||||
@Autowired
|
||||
private SealWebSocketHandler webSocketHandler;
|
||||
|
||||
@Autowired
|
||||
private SealLogService sealLogService;
|
||||
private final SealWebSocketHandler webSocketHandler;
|
||||
private final SealLogService sealLogService;
|
||||
private final ContractLaunchService contractLaunchService;
|
||||
|
||||
@PostMapping("/login")
|
||||
public R<String> loginCallback(@RequestBody Map<String, Object> params) {
|
||||
log.info("收到身份认证回调: {}", JSON.toJSONString(params));
|
||||
|
||||
try {
|
||||
String requestId = (String) params.get("requestId");
|
||||
String requestId = stringValue(params.get("requestId"));
|
||||
webSocketHandler.sendCallbackMessage(requestId, "login", params);
|
||||
// 身份认证回调记录日志(不关联具体合同)
|
||||
sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
|
||||
"身份认证回调", "身份认证回调已接收,requestId: " + requestId, params);
|
||||
return R.success("回调接收成功");
|
||||
@@ -60,20 +51,18 @@ public class SealCallbackController {
|
||||
@PostMapping("/sign")
|
||||
public R<String> signCallback(@RequestBody Map<String, Object> params) {
|
||||
log.info("收到签署回调: {}", JSON.toJSONString(params));
|
||||
|
||||
try {
|
||||
String contractId = (String) params.get("contractId");
|
||||
String requestId = (String) params.get("requestId");
|
||||
String contractId = stringValue(params.get("contractId"));
|
||||
String requestId = stringValue(params.get("requestId"));
|
||||
webSocketHandler.sendCallbackMessage(requestId, "sign", params);
|
||||
webSocketHandler.sendCallbackMessage(contractId, "contract_status", params);
|
||||
// 签署回调记录日志(关联到本系统合同)
|
||||
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
|
||||
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
|
||||
"签署回调", "收到签章平台签署回调,签章平台合同ID: " + contractId, params);
|
||||
return R.success("回调接收成功");
|
||||
} catch (Exception e) {
|
||||
log.error("处理签署回调失败", e);
|
||||
String contractId = (String) params.get("contractId");
|
||||
String contractId = stringValue(params.get("contractId"));
|
||||
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
|
||||
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
|
||||
"签署回调", ContractLaunchConstants.ACTION_STATUS_FAILED,
|
||||
@@ -85,36 +74,44 @@ public class SealCallbackController {
|
||||
@PostMapping("/presign")
|
||||
public R<String> presignCallback(@RequestBody Map<String, Object> params) {
|
||||
log.info("收到预盖章回调: {}", JSON.toJSONString(params));
|
||||
|
||||
try {
|
||||
String contractId = (String) params.get("contractId");
|
||||
String requestId = (String) params.get("requestId");
|
||||
String contractId = stringValue(params.get("contractId"));
|
||||
String requestId = stringValue(params.get("requestId"));
|
||||
webSocketHandler.sendCallbackMessage(requestId, "presign", params);
|
||||
webSocketHandler.sendCallbackMessage(contractId, "contract_status", params);
|
||||
// 预盖章回调记录日志(关联到本系统合同)
|
||||
contractLaunchService.handleSealPresignCallback(params);
|
||||
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
|
||||
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
|
||||
"预盖章回调", "收到签章平台预盖章回调,签章平台合同ID: " + contractId, params);
|
||||
return R.success("回调接收成功");
|
||||
} catch (Exception e) {
|
||||
log.error("处理预盖章回调失败", e);
|
||||
String contractId = (String) params.get("contractId");
|
||||
String contractId = stringValue(params.get("contractId"));
|
||||
Long internalContractId = sealLogService.findContractIdBySealContractId(contractId);
|
||||
sealLogService.logSealAction(internalContractId, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
|
||||
"预盖章回调", ContractLaunchConstants.ACTION_STATUS_FAILED,
|
||||
"处理预盖章回调失败: " + 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")
|
||||
public R<String> trusteeshipCallback(@RequestBody Map<String, Object> params) {
|
||||
log.info("收到托管章回调: {}", JSON.toJSONString(params));
|
||||
|
||||
try {
|
||||
String requestId = (String) params.get("requestId");
|
||||
String requestId = stringValue(params.get("requestId"));
|
||||
webSocketHandler.sendCallbackMessage(requestId, "trusteeship", params);
|
||||
// 托管章回调记录日志
|
||||
sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
|
||||
"托管章回调", "收到签章平台托管章回调,requestId: " + requestId, params);
|
||||
return R.success("回调接收成功");
|
||||
@@ -130,11 +127,9 @@ public class SealCallbackController {
|
||||
@PostMapping("/signQrCode")
|
||||
public R<String> signQrCodeCallback(@RequestBody Map<String, Object> params) {
|
||||
log.info("收到签名二维码回调: {}", JSON.toJSONString(params));
|
||||
|
||||
try {
|
||||
String requestId = (String) params.get("requestId");
|
||||
String requestId = stringValue(params.get("requestId"));
|
||||
webSocketHandler.sendCallbackMessage(requestId, "signQrCode", params);
|
||||
// 签名二维码回调记录日志
|
||||
sealLogService.logSealAction(null, ContractLaunchConstants.ACTION_SEAL_CALLBACK,
|
||||
"签名二维码回调", "收到签章平台签名二维码回调,requestId: " + requestId, params);
|
||||
return R.success("回调接收成功");
|
||||
@@ -149,7 +144,18 @@ public class SealCallbackController {
|
||||
|
||||
@GetMapping("/poll/{requestId}")
|
||||
public R<Map<String, Object>> pollCallback(@PathVariable String requestId) {
|
||||
Map<String, Object> callbackData = webSocketHandler.getCallbackData(requestId);
|
||||
return R.success(callbackData);
|
||||
return R.success(webSocketHandler.getCallbackData(requestId));
|
||||
}
|
||||
|
||||
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 java.io.IOException;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@@ -96,9 +97,13 @@ public class SealWebSocketHandler extends TextWebSocketHandler {
|
||||
return;
|
||||
}
|
||||
|
||||
Map<String, Object> cacheEntry = new ConcurrentHashMap<>();
|
||||
// 回调报文里可能出现 signerId 等可选字段为 null 的情况,
|
||||
// ConcurrentHashMap 不允许 null value,这里使用普通 Map 缓存原始回调内容。
|
||||
Map<String, Object> cacheEntry = new LinkedHashMap<>();
|
||||
cacheEntry.put("callbackType", callbackType);
|
||||
cacheEntry.putAll(data);
|
||||
if (data != null) {
|
||||
cacheEntry.putAll(data);
|
||||
}
|
||||
callbackCache.put(requestId, cacheEntry);
|
||||
|
||||
String sessionId = requestSessionMap.get(requestId);
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
<result property="approveMessage" column="APPROVE_MESSAGE"/>
|
||||
<result property="sealContractId" column="SEAL_CONTRACT_ID"/>
|
||||
<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="currencyName" column="CURRENCY_NAME"/>
|
||||
<result property="contractPeriodType" column="CONTRACT_PERIOD_TYPE"/>
|
||||
@@ -199,7 +201,7 @@
|
||||
select ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID,
|
||||
CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE,
|
||||
TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE,
|
||||
SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, CONTRACT_AMOUNT, CURRENCY_NAME,
|
||||
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,
|
||||
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,
|
||||
@@ -374,7 +376,7 @@
|
||||
ID, CONTRACT_SYS_ID, CONTRACT_CODE, LAW_CONTRACT_CODE, LAW_CONTRACT_ID,
|
||||
CONTRACT_NAME, CONTRACT_TYPE, CONTRACT_TYPE_CODE, CONTRACT_SOURCE, CREATION_MODE,
|
||||
TEMPLATE_ID, TEMPLATE_CODE, TEMPLATE_NAME, STATUS, APPROVE_STATUS, APPROVE_MESSAGE,
|
||||
SEAL_CONTRACT_ID, SEAL_CONTRACT_STATUS, CONTRACT_AMOUNT, CURRENCY_NAME,
|
||||
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,
|
||||
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,
|
||||
@@ -394,7 +396,7 @@
|
||||
#{id}, #{contractSysId}, #{contractCode}, #{lawContractCode}, #{lawContractId},
|
||||
#{contractName}, #{contractType}, #{contractTypeCode}, #{contractSource}, #{creationMode},
|
||||
#{templateId}, #{templateCode}, #{templateName}, #{status}, #{approveStatus}, #{approveMessage},
|
||||
#{sealContractId}, #{sealContractStatus}, #{contractAmount}, #{currencyName},
|
||||
#{sealContractId}, #{sealContractStatus}, #{sealPosConfigured}, #{sealPdfImageList}, #{contractAmount}, #{currencyName},
|
||||
#{contractPeriodType}, #{periodExplain}, #{effectiveStart}, #{effectiveEnd},
|
||||
#{paymentMethod}, #{primaryContent}, #{amountExplain}, #{valuationMode}, #{needAmendments}, #{estimatedAmount}, #{sealType}, #{sealQuantity},
|
||||
#{textSource}, #{mandateType}, #{cargoType}, #{assistDepartmentName}, #{executorAccount},
|
||||
@@ -431,6 +433,8 @@
|
||||
APPROVE_MESSAGE = #{approveMessage},
|
||||
SEAL_CONTRACT_ID = #{sealContractId},
|
||||
SEAL_CONTRACT_STATUS = #{sealContractStatus},
|
||||
SEAL_POS_CONFIGURED = #{sealPosConfigured},
|
||||
SEAL_PDF_IMAGE_LIST = #{sealPdfImageList},
|
||||
CONTRACT_AMOUNT = #{contractAmount},
|
||||
CURRENCY_NAME = #{currencyName},
|
||||
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">
|
||||
<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>
|
||||
|
||||
<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">
|
||||
<id property="customerSysid" column="CUSTOMER_SYSID"/>
|
||||
<result property="customerRegno" column="CUSTOMER_REGNO"/>
|
||||
@@ -243,7 +239,7 @@
|
||||
)
|
||||
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},
|
||||
#{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{item.remark}
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user