法务回调审批样例

This commit is contained in:
2026-06-09 09:20:06 +08:00
parent c67fdcfc41
commit ef7f559490
7 changed files with 270 additions and 57 deletions
@@ -5,6 +5,9 @@ import com.nbport.zgwl.common.R;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.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.service.ContractLaunchService;
@@ -194,25 +197,47 @@ public class ContractLaunchController {
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 审批结果。
*
* 当前法务接口不可用,但此回调接口已预先实现,可接受测试请求验证流程。
* 请求体使用 Map 接收以兼容不同版本的字段名,内部通过 getFirstText 按多个 key 依次尝试提取。
* 当前约定请求体结构固定为:
* {
* "data": {
* "contractSysId": "...",
* "approveStatus": "approved|rejected",
* ...
* }
* }
*
* 识别合同方式:从报文中取 contractSysId 定位到本系统的合同
* 判断通过/驳回:从 approveStatus/status/result 等多个字段名中取值判断
* 这样前后端、法务、Postman 联调都统一只看 data 这一套结构,
* 不再做多字段兼容判断,便于后续正式协商定稿。
*
* 状态流转:reviewing → pending_sign(通过) / rejected(驳回)
*
* 对应前端功能:法务系统审批完成后自动回调(目前用法务系统暂未对接)
*/
@PostMapping("/callback/law/approve")
public R<ContractLaunchDto> lawApproveCallback(@RequestBody Map<String, Object> callbackPayload) {
return R.success(contractLaunchService.handleLawApproveCallback(callbackPayload));
public R<ContractLaunchDto> lawApproveCallback(@RequestBody ContractLaunchLawCallbackRequestDto callbackRequest) {
return R.success(contractLaunchService.handleLawApproveCallback(callbackRequest == null ? null : callbackRequest.getData()));
}
/**
@@ -0,0 +1,57 @@
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;
/**
* 约定给法务的固定审批回调结构。
*
* 设计原则:
* 1. 只保留法务系统可持有或可回写的字段
* 2. 业务系统依赖 contractSysId 唯一定位合同
* 3. approveStatus 仅约定 approved / rejected 两种值,不做多值兼容
*/
@Data
public class ContractLaunchLawCallbackDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 法务回调唯一流水号,后续若做幂等可直接使用 */
private String callbackId;
/** 结算平台推送给法务的唯一合同编码,用于回调时唯一定位合同 */
private String contractSysId;
/** 业务合同编号(推送给法务时已带出,便于双方排查) */
private String contractCode;
/** 二级单位合同编码(若推送时有值,法务可原样带回) */
private String selfCode;
/** 合同名称(便于接口日志和联调排查) */
private String contractName;
/** 审批状态,仅允许 approved / rejected */
private String approveStatus;
/** 审批意见 */
private String approveMessage;
/** 法务审批完成时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
private LocalDateTime approveTime;
/** 法务系统合同编号 */
private String lawContractCode;
/** 法务系统合同ID */
private String lawContractId;
/** 带法务水印的合同正文文件 */
private ContractLaunchLawCallbackFileDto watermarkFile;
}
@@ -0,0 +1,31 @@
package com.nbport.zgwl.contractlaunch.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 法务审批回调中的带水印文件信息。
*
* 当前联调前先沿用本地合同正文文件路径,
* 后续法务真正传文件时可直接复用这组字段。
*/
@Data
public class ContractLaunchLawCallbackFileDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 文件名 */
private String fileName;
/** 文件路径(当前本地模拟先回传本系统保存的正文路径) */
private String filePath;
/** 文件类型 */
private String fileType;
/** 文件大小 */
private Long fileSize;
}
@@ -0,0 +1,21 @@
package com.nbport.zgwl.contractlaunch.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 法务审批回调请求体。
*
* 按双方约定固定包一层 data,
* 便于和现有法务接口文档风格保持一致。
*/
@Data
public class ContractLaunchLawCallbackRequestDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
private ContractLaunchLawCallbackDto data;
}
@@ -0,0 +1,31 @@
package com.nbport.zgwl.contractlaunch.dto;
import lombok.Data;
import java.io.Serial;
import java.io.Serializable;
/**
* 法务审批回调样例。
*
* 前端可直接展示这两个 data 结构,
* 便于复制到 Postman 调用回调接口。
*/
@Data
public class ContractLaunchLawCallbackSampleDto implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/** 回调地址 */
private String callbackUrl;
/** 请求方式,固定 POST */
private String callbackMethod;
/** 审批通过样例 */
private ContractLaunchLawCallbackDto approvedData;
/** 审批驳回样例 */
private ContractLaunchLawCallbackDto rejectedData;
}
@@ -4,11 +4,11 @@ import com.nbport.zgwl.common.PageResult;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchApproveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchArchiveDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackSampleDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchQueryDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchSealDto;
import java.util.Map;
public interface ContractLaunchService {
/** 分页查询合同列表 */
@@ -38,8 +38,11 @@ public interface ContractLaunchService {
/** 模拟审批驳回(测试按钮,TODO:替换为真实法务回调) */
ContractLaunchDto mockReject(Long id, ContractLaunchApproveDto approveDto);
/** 获取法务审批回调样例(用于本地联调和前端展示) */
ContractLaunchLawCallbackSampleDto buildLawApproveCallbackSample(Long id);
/** 接收法务审批回调(处理审批结果,更新合同状态) */
ContractLaunchDto handleLawApproveCallback(Map<String, Object> callbackPayload);
ContractLaunchDto handleLawApproveCallback(ContractLaunchLawCallbackDto callbackData);
/** 回写签章信息(签署发起时调用,记录 sealContractId */
ContractLaunchDto saveSealInfo(Long id, ContractLaunchSealDto sealDto);
@@ -10,6 +10,9 @@ 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.ContractLaunchLawCallbackDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackFileDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLawCallbackSampleDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchLogDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPartyDto;
import com.nbport.zgwl.contractlaunch.dto.ContractLaunchPlanDto;
@@ -44,6 +47,7 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.stream.Collectors;
/**
@@ -296,6 +300,17 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
return getDetail(id);
}
@Override
public ContractLaunchLawCallbackSampleDto buildLawApproveCallbackSample(Long id) {
ContractLaunchDto contract = getDetail(id);
ContractLaunchLawCallbackSampleDto sample = new ContractLaunchLawCallbackSampleDto();
sample.setCallbackUrl(ContractLaunchConstants.REQUEST_PREFIX + "/callback/law/approve");
sample.setCallbackMethod("POST");
sample.setApprovedData(buildLawCallbackData(contract, true));
sample.setRejectedData(buildLawCallbackData(contract, false));
return sample;
}
/**
* 接收法务审批回调。
*
@@ -308,38 +323,33 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
* - 驳回:status → rejected(已驳回),记录驳回原因
* 3. 记录操作日志
*
* 兼容设计:
* - 使用 getFirstText 按多个 key 依次尝试提取字段(如 approveStatus/status/result
* - 审批通过状态兼容:1/true/success/approved/pass/agree/审批通过
* - 合同编号兼容:lawContractCode/contractCode/htCode/code
* - 合同ID兼容:lawContractId/contractId/id/businessId
*/
@Override
@Transactional(rollbackFor = Exception.class)
public ContractLaunchDto handleLawApproveCallback(Map<String, Object> callbackPayload) {
ContractLaunchDto contract = getContractByLawCallback(callbackPayload);
public ContractLaunchDto handleLawApproveCallback(ContractLaunchLawCallbackDto callbackData) {
ContractLaunchDto contract = getContractByLawCallback(callbackData);
SysUserEntity currentUser = adminSecurityManage.getUser();
boolean approved = isLawCallbackApproved(callbackPayload);
boolean approved = isLawCallbackApproved(callbackData);
contract.setApproveStatus(approved
? ContractLaunchConstants.APPROVE_STATUS_APPROVED
: ContractLaunchConstants.APPROVE_STATUS_REJECTED);
contract.setStatus(approved
? ContractLaunchConstants.STATUS_PENDING_SIGN
: ContractLaunchConstants.STATUS_REJECTED);
contract.setApproveMessage(getFirstText(callbackPayload,
"approveMessage", "message", "msg", "opinion", "remark"));
contract.setApproveMessage(trim(callbackData.getApproveMessage()));
String lawContractCode = getFirstText(callbackPayload,
"lawContractCode", "contractCode", "htCode", "code");
String lawContractId = getFirstText(callbackPayload,
"lawContractId", "contractId", "id", "businessId");
String lawContractCode = trim(callbackData.getLawContractCode());
String lawContractId = trim(callbackData.getLawContractId());
if (lawContractCode != null && !lawContractCode.isBlank()) {
contract.setLawContractCode(lawContractCode);
}
if (lawContractId != null && !lawContractId.isBlank()) {
contract.setLawContractId(lawContractId);
}
if (approved) {
applyLawWatermarkFile(contract, callbackData.getWatermarkFile());
}
contract.setUpdateBy(currentUser.getUsername());
contract.setUpdateTime(LocalDateTime.now());
@@ -350,7 +360,7 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
approved ? "法务审批回调通过" : "法务审批回调驳回",
ContractLaunchConstants.ACTION_STATUS_SUCCESS,
contract.getApproveMessage(),
callbackPayload,
callbackData,
currentUser
);
return getDetail(contract.getId());
@@ -1038,11 +1048,11 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
}
/** 通过法务回调报文查找对应合同(先取 contractSysId 再查库) */
private ContractLaunchDto getContractByLawCallback(Map<String, Object> payload) {
if (payload == null || payload.isEmpty()) {
private ContractLaunchDto getContractByLawCallback(ContractLaunchLawCallbackDto payload) {
if (payload == null) {
throw new ServiceException("法务回调报文不能为空");
}
String contractSysId = getFirstText(payload, "contractSysId", "businessNo", "thirdContractId");
String contractSysId = trim(payload.getContractSysId());
if (contractSysId == null || contractSysId.isBlank()) {
throw new ServiceException("法务回调缺少 contractSysId,无法定位合同");
}
@@ -1054,39 +1064,19 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
return contract;
}
/** 判断法务回调是否审批通过(兼容多种字段名和值 */
private boolean isLawCallbackApproved(Map<String, Object> payload) {
String status = getFirstText(payload, "approveStatus", "status", "result", "auditStatus");
if (status == null || status.isBlank()) {
/** 判断法务回调是否审批通过(固定值:approved / rejected */
private boolean isLawCallbackApproved(ContractLaunchLawCallbackDto payload) {
if (payload == null || payload.getApproveStatus() == null || payload.getApproveStatus().isBlank()) {
throw new ServiceException("法务回调缺少 approveStatus");
}
String normalized = payload.getApproveStatus().trim().toLowerCase();
if ("approved".equals(normalized)) {
return true;
}
String normalized = status.trim().toLowerCase();
return normalized.equals("1")
|| normalized.equals("true")
|| normalized.equals("success")
|| normalized.equals("approved")
|| normalized.equals("pass")
|| normalized.equals("agree")
|| normalized.equals("通过")
|| normalized.equals("审批通过");
if ("rejected".equals(normalized)) {
return false;
}
/** 从回调报文中提取指定 key 的文本值(多个 key 依次尝试,取第一个非空) */
private String getFirstText(Map<String, Object> payload, String... keys) {
if (payload == null) {
return null;
}
for (String key : keys) {
Object value = payload.get(key);
if (value == null) {
continue;
}
String text = String.valueOf(value).trim();
if (!text.isBlank()) {
return text;
}
}
return null;
throw new ServiceException("法务回调 approveStatus 仅允许 approved 或 rejected");
}
/** 构建结算平台推送合同报文(pushContract 接口的 payload */
@@ -1270,6 +1260,61 @@ public class ContractLaunchServiceImpl implements ContractLaunchService {
}).collect(Collectors.toList());
}
private ContractLaunchLawCallbackDto buildLawCallbackData(ContractLaunchDto contract, boolean approved) {
ContractLaunchLawCallbackDto data = new ContractLaunchLawCallbackDto();
data.setCallbackId("LAW-CB-" + UUID.randomUUID().toString().replace("-", ""));
data.setContractSysId(contract.getContractSysId());
data.setContractCode(contract.getContractCode());
data.setSelfCode(contract.getSelfCode());
data.setContractName(contract.getContractName());
data.setApproveStatus(approved ? "approved" : "rejected");
data.setApproveMessage(approved ? "法务审批通过" : "法务审批驳回,请修改后重新提交");
data.setApproveTime(LocalDateTime.now());
data.setLawContractCode(buildSampleLawContractCode(contract));
data.setLawContractId(buildSampleLawContractId(contract));
data.setWatermarkFile(buildLawWatermarkFile(contract));
return data;
}
private String buildSampleLawContractCode(ContractLaunchDto contract) {
if (contract.getLawContractCode() != null && !contract.getLawContractCode().isBlank()) {
return contract.getLawContractCode();
}
return "LAW-" + ContractLaunchCodeUtils.buildContractCode(contract.getContractTypeCode());
}
private String buildSampleLawContractId(ContractLaunchDto contract) {
if (contract.getLawContractId() != null && !contract.getLawContractId().isBlank()) {
return contract.getLawContractId();
}
return "LAWID-" + contract.getId();
}
private ContractLaunchLawCallbackFileDto buildLawWatermarkFile(ContractLaunchDto contract) {
ContractLaunchLawCallbackFileDto file = new ContractLaunchLawCallbackFileDto();
file.setFileName(contract.getContractTextFileName());
file.setFilePath(contract.getContractTextFilePath());
file.setFileType(contract.getContractTextFileType());
file.setFileSize(contract.getContractTextFileSize());
return file;
}
/**
* 当前联调阶段先把法务回调中的水印文件回写到合同正文字段。
*
* 这样后续“在线签订”仍读取同一套正文路径;
* 现在法务未传真实文件时,这里通常还是你本地原正文路径。
*/
private void applyLawWatermarkFile(ContractLaunchDto contract, ContractLaunchLawCallbackFileDto watermarkFile) {
if (watermarkFile == null || watermarkFile.getFilePath() == null || watermarkFile.getFilePath().isBlank()) {
return;
}
contract.setContractTextFileName(trim(watermarkFile.getFileName()));
contract.setContractTextFilePath(trim(watermarkFile.getFilePath()));
contract.setContractTextFileType(trim(watermarkFile.getFileType()));
contract.setContractTextFileSize(watermarkFile.getFileSize());
}
private boolean requiresAuthAttachment(String sealType) {
return "GZBSQRQZ".equals(sealType) || "HTZYZBSQRQZ".equals(sealType);
}