Merge branch 'master' into feature/REQ-3540

# Conflicts:
#	nanopart-server/pom.xml
#	pom.xml
This commit is contained in:
yanglin 2025-03-20 16:33:52 +08:00
commit 59ff764e29
177 changed files with 9622 additions and 406 deletions

44
ess/ess-api/pom.xml Normal file
View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<artifactId>ess</artifactId>
<groupId>cn.axzo.nanopart</groupId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>ess-api</artifactId>
<packaging>jar</packaging>
<name>ess-api</name>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign-core</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-common-domain</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework.rocketmq</groupId>
<artifactId>axzo-common-rocketmq</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-httpclient</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,146 @@
package cn.axzo.nanopart.ess.api;
import java.util.List;
import javax.validation.Valid;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.ess.api.domain.EssOrgAndSealInfo;
import cn.axzo.nanopart.ess.api.domain.EssSealPersonInfo;
import cn.axzo.nanopart.ess.api.request.AddSealAuthorizationRequest;
import cn.axzo.nanopart.ess.api.request.AddSealPersonRequest;
import cn.axzo.nanopart.ess.api.request.AssignSignUrlRequest;
import cn.axzo.nanopart.ess.api.request.CreateConsoleLoginUrlRequest;
import cn.axzo.nanopart.ess.api.request.CreateContractByFileRequest;
import cn.axzo.nanopart.ess.api.request.DownloadSingedContractPdfRequest;
import cn.axzo.nanopart.ess.api.request.GetContractDetailByContractIdRequest;
import cn.axzo.nanopart.ess.api.request.GetEmbedWebUrlRequest;
import cn.axzo.nanopart.ess.api.request.GetOrgAuthStatesRequest;
import cn.axzo.nanopart.ess.api.request.GetPersonAuthStateRequest;
import cn.axzo.nanopart.ess.api.request.GetSealPersonRequest;
import cn.axzo.nanopart.ess.api.request.GetSealsRequest;
import cn.axzo.nanopart.ess.api.request.RemoveSealAuthorizationRequest;
import cn.axzo.nanopart.ess.api.request.RemoveSealPersonRequest;
import cn.axzo.nanopart.ess.api.request.RevokeContractRequest;
import cn.axzo.nanopart.ess.api.request.SaveContractSnapshotRequest;
import cn.axzo.nanopart.ess.api.response.AssignSignUrlResponse;
import cn.axzo.nanopart.ess.api.response.CreateConsoleLoginUrlResponse;
import cn.axzo.nanopart.ess.api.response.CreateContractByFileResponse;
import cn.axzo.nanopart.ess.api.response.DownloadSingedContractPdfResponse;
import cn.axzo.nanopart.ess.api.response.GetContractDetailByContractIdResponse;
import cn.axzo.nanopart.ess.api.response.GetEmbedWebUrlResponse;
import cn.axzo.nanopart.ess.api.response.GetOrgAuthStatesResponse;
import cn.axzo.nanopart.ess.api.response.GetPersonAuthStateResponse;
/**
* @author yanglin
*/
@FeignClient(name = "nanopart", url = "${axzo.service.nanopart:http://nanopart:8080}")
public interface EssApi {
/**
* 查询单位电子签开通情况
*/
@PostMapping("api/ess/getOrgAuthStates")
ApiResult<List<GetOrgAuthStatesResponse>> getOrgAuthStates(@RequestBody @Valid GetOrgAuthStatesRequest request);
/**
* 查询用户电子签开通情况(认证)
*/
@PostMapping("api/ess/getPersonAuthState")
ApiResult<GetPersonAuthStateResponse> getPersonAuthState(@RequestBody @Valid GetPersonAuthStateRequest request);
/**
* 获取电子签登录链接
*/
@PostMapping("api/ess/createConsoleLoginUrl")
ApiResult<CreateConsoleLoginUrlResponse> createConsoleLoginUrl(
@RequestBody @Valid CreateConsoleLoginUrlRequest request);
/**
* 获取内嵌页面链接, 有效期为5分钟
*/
@PostMapping("api/ess/getEmbedWebUrl")
ApiResult<GetEmbedWebUrlResponse> getEmbedWebUrl(@RequestBody @Valid GetEmbedWebUrlRequest request);
/**
* 查询单位已启用的印章以及授权人员列表等等
*/
@PostMapping("api/ess/getOrgEnabledSeals")
ApiResult<List<EssOrgAndSealInfo>> getOrgEnabledSeals(@RequestBody @Valid GetSealsRequest request);
/**
* 查询印章管理员
*/
@PostMapping("api/ess/getSealPersons")
ApiResult<List<EssSealPersonInfo>> getSealPersons(@RequestBody @Valid GetSealPersonRequest request);
/**
* 添加印章人员
*/
@PostMapping("api/ess/addSealPerson")
ApiResult<Boolean> addSealPerson(@RequestBody @Valid AddSealPersonRequest request);
/**
* 删除印章人员
*/
@PostMapping("api/ess/removeSealPerson")
ApiResult<Void> removeSealPerson(@RequestBody @Valid RemoveSealPersonRequest request);
/**
* 单个人印章授权(去授权)
*/
@PostMapping("api/ess/addSealAuthorization")
ApiResult<Void> addSealAuthorization(@RequestBody @Valid AddSealAuthorizationRequest request);
/**
* 单个人印章取消授权
*/
@PostMapping("api/ess/removeSealAuthorization")
ApiResult<Void> removeSealAuthorization(@RequestBody @Valid RemoveSealAuthorizationRequest request);
/**
* 通过文件创建合同
*/
@PostMapping("api/ess/createContractByFile")
ApiResult<CreateContractByFileResponse> createContractByFile(
@RequestBody @Valid CreateContractByFileRequest request);
/**
* 获取动态签署链接, 有效期为5分钟
*/
@PostMapping("api/ess/assignSignUrl")
ApiResult<AssignSignUrlResponse> assignSignUrl(@RequestBody @Valid AssignSignUrlRequest request);
/**
* 获取合同PDF文件地址. 下载链接有效期为5分钟
*/
@PostMapping("api/ess/getContractPDFUrl")
ApiResult<DownloadSingedContractPdfResponse> getContractPDFUrl(
@RequestBody @Valid DownloadSingedContractPdfRequest request);
/**
* 从腾讯电子签下载合同PDF文件
*/
@PostMapping("api/ess/saveContractSnapshot")
ApiResult<Void> saveContractSnapshot(@RequestBody @Valid SaveContractSnapshotRequest request);
/**
* 撤销合同
*/
@PostMapping("api/ess/revokeContract")
ApiResult<Void> revokeContract(@RequestBody @Valid RevokeContractRequest request);
/**
* 获取合同详情, 通过合同id
*/
@PostMapping("api/ess/getContractByContractId")
ApiResult<GetContractDetailByContractIdResponse> getContractByContractId(
@RequestBody @Valid GetContractDetailByContractIdRequest request);
}

View File

@ -0,0 +1,11 @@
package cn.axzo.nanopart.ess.api;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author yanglin
*/
@EnableFeignClients("cn.axzo.nanopart.ess.api")
public class EssAutoConfiguration {
}

View File

@ -0,0 +1,22 @@
package cn.axzo.nanopart.ess.api;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.ess.api.request.CallbackContent;
import cn.axzo.nanopart.ess.api.request.CallbackRequest;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import javax.validation.Valid;
/**
* @author yanglin
*/
@FeignClient(name = "nanopart", url = "${axzo.service.nanopart:http://nanopart:8080}")
public interface EssCallbackApi {
@PostMapping("api/ess/callback")
ApiResult<Object> callback(@RequestBody @Valid CallbackRequest request);
}

View File

@ -0,0 +1,23 @@
package cn.axzo.nanopart.ess.api.domain;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class CreateContractByFile extends CreateContractInfo {
/**
* 合同文件base64: Base64.getEncoder().encodeToString(file.getBytes())
*/
@NotEmpty(message = "base64Files不能为空")
private List<String> base64Files;
}

View File

@ -0,0 +1,74 @@
package cn.axzo.nanopart.ess.api.domain;
import cn.axzo.nanopart.ess.api.domain.contract.Approver;
import cn.axzo.nanopart.ess.api.enums.Constraint;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Range;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import static java.util.stream.Collectors.toSet;
/**
* @author yanglin
*/
@Setter
@Getter
public abstract class CreateContractInfo {
/**
* 合同名称
*/
@NotBlank(message = "contractName不能为空")
private String contractName;
/**
* 合同签署方
*/
@Valid
@NotEmpty(message = "approvers不能为空")
private List<Approver> approvers;
/**
* 合同签署方的签署约束种类
*/
@NotNull(message = "approverConstraint不能为空")
private Constraint constraint = Constraint.ONE_PERSON_PER_ORG;
/**
* 签署方签署控件印章/签名等的生成方式.
* 0: 在合同流程发起时由发起人指定签署方的签署控件的位置和数量
* 1: 签署方在签署时自行添加签署控件可以拖动位置和控制数量
*/
@Range(min = 0, max = 1, message = "signBeanTag必须是0或1")
private Long signBeanTag = 1L;
/**
* 是否按 approvers 中指定的顺序进行签署
*/
private boolean signOrdered = false;
/**
* 合同流程的签署截止时间格式为Unix标准时间戳, 透传给腾讯电子签
* 如果未设置签署截止时间则默认为合同流程创建后的365天时截止
* 如果在签署截止时间前未完成签署则合同状态会变为已过期导致合同作废
*/
private Long deadlineMs;
@JsonIgnore
public Set<Long> getApproverOuIds() {
if (approvers == null)
return Collections.emptySet();
return approvers.stream().map(Approver::getOuId).collect(toSet());
}
}

View File

@ -0,0 +1,32 @@
package cn.axzo.nanopart.ess.api.domain;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssOrgAndSealInfo {
/**
* 单位信息
*/
private EssOrgInfo org;
/**
* 印章信息
*/
private List<EssSealInfo> seals = new ArrayList<>();
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,39 @@
package cn.axzo.nanopart.ess.api.domain;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssOrgInfo {
/**
* 单位id
*/
private Long ouId;
/**
* 单位名称
*/
private String ouName;
/**
* 单位是否认证
*/
private boolean isOrgAuthorized;
/**
* 超级管理员人员id
*/
private Long superAdminPersonId;
/**
* 单位记录创建人id
*/
private Long createByPersonId;
}

View File

@ -0,0 +1,47 @@
package cn.axzo.nanopart.ess.api.domain;
import cn.axzo.nanopart.ess.api.enums.EssSealType;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssSealInfo {
/**
* 单位id
*/
private Long ouId;
/**
* 印章在腾讯的id
*/
private String essSealId;
/**
* 印章名称
*/
private String name;
/**
* 印章类型. OFFICIAL: 公章, CONTRACT: 合同专用章, FINANCE: 财务专用章, PERSONNEL: 人事专用章, INVOICE: 发票专用章, LEGAL_PERSON_SEAL: 法定代表人章, OTHER: 其它
*/
private EssSealType type;
/**
* 授权人员列表
*/
private List<EssSealPersonInfo> sealPersons = new ArrayList<>();
public void addSealPerson(EssSealPersonInfo person) {
sealPersons.add(person);
}
}

View File

@ -0,0 +1,54 @@
package cn.axzo.nanopart.ess.api.domain;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssSealPersonInfo {
/**
* 单位id
*/
private Long ouId;
/**
* 印章用户信息
*/
private PersonProfileInfo sealPerson = new PersonProfileInfo();
/**
* 印章授权人信息
*/
private PersonProfileInfo authorizedByPerson = new PersonProfileInfo();
/**
* 印章id
*/
private String essSealId;
/**
* 是否已经激活(实名认证)
*/
private boolean isPersonAuthorized;
/**
* 授权时间, unix时间戳
*/
private Long authorizeTimeMs;
/**
* 是否已经认证(印章授权)
*/
private boolean isSealAuthorized;
/**
* 是否是超级管理员
*/
private boolean isSuperAdmin;
}

View File

@ -0,0 +1,31 @@
package cn.axzo.nanopart.ess.api.domain;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class OperatorInfo implements OrgPerson {
/**
* 发起方单位id
*/
@NotNull(message = "ouId不能为空")
@Min(value = 1, message = "ouId必须大于0")
private Long ouId;
/**
* 发起方人员id
*/
@NotNull(message = "personId不能为空")
@Min(value = 1, message = "personId必须大于0")
private Long personId;
}

View File

@ -0,0 +1,32 @@
package cn.axzo.nanopart.ess.api.domain;
import cn.axzo.nanopart.ess.api.domain.contract.OrgPersonInfo;
import java.util.Objects;
/**
* @author yanglin
*/
public interface OrgPerson {
Long getOuId();
Long getPersonId();
default OrgPersonInfo toOrgPersonInfo() {
OrgPersonInfo orgPersonInfo = new OrgPersonInfo();
orgPersonInfo.setOuId(getOuId());
orgPersonInfo.setPersonId(getPersonId());
return orgPersonInfo;
}
static boolean equals(OrgPerson a, OrgPerson b) {
return Objects.equals(a.getOuId(), b.getOuId()) && Objects.equals(a.getPersonId(), b.getPersonId());
}
static String toString(OrgPerson person) {
return String.format("%d_%d", person.getOuId(), person.getPersonId());
}
}

View File

@ -0,0 +1,32 @@
package cn.axzo.nanopart.ess.api.domain;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.Collection;
import java.util.Objects;
import java.util.Optional;
/**
* @author yanglin
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class OrgPersons<T extends OrgPerson> {
private final Collection<T> persons;
public static <T extends OrgPerson> OrgPersons<T> wrap(Collection<T> persons) {
return new OrgPersons<>(persons);
}
public Optional<T> findByPerson(OrgPerson person) {
return persons.stream().filter(p -> Objects.equals(p.getOuId(), person.getOuId())
&& Objects.equals(p.getPersonId(), person.getPersonId())).findFirst();
}
public Optional<T> findByOrgId(Long ouId) {
return persons.stream().filter(p -> Objects.equals(p.getOuId(), ouId)).findFirst();
}
}

View File

@ -0,0 +1,29 @@
package cn.axzo.nanopart.ess.api.domain;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class PersonProfileInfo {
/**
* 自然人id
*/
private Long personId;
/**
* 姓名
*/
private String personName;
/**
* 头像
*/
private String avatar;
}

View File

@ -0,0 +1,95 @@
package cn.axzo.nanopart.ess.api.domain.contract;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.enums.EssApproverType;
import cn.axzo.nanopart.ess.api.enums.EssSealType;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.validator.constraints.Range;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class Approver implements OrgPerson {
/**
* 签署方人员信息, 如果是动态签署人, 则传null, 在获取签署链接时再指定签署人
*/
@Valid
private OrgPersonInfo signPerson;
/**
* 签署选项
*/
private SignOption signOption = new SignOption();
/**
* 在指定签署方时可选择企业B端或个人C端等不同的参与者类型
*/
@NotNull(message = "approverType不能为空")
private EssApproverType approverType = EssApproverType.ORGANIZATION;
/**
* 签署方在签署合同之前需要强制阅读合同的时长可指定为3秒至300秒之间的任意值
*/
@Range(min = 3, max = 300, message = "preReadTime必须在3-300之间")
@NotNull(message = "preReadTime不能为空")
private Long preReadTimeSeconds = 5L;
/**
* 指定盖章时, 使用的章类型
*/
private List<EssSealType> sealTypes;
/**
* 扩展信息, 内部使用
*/
private JSONObject internal;
public void setEssRecipientId(String essRecipientId) {
getOrCreateExtension().put("essRecipientId", essRecipientId);
}
@JsonIgnore
@JSONField(serialize = false)
public String getEssRecipientId() {
if (internal == null)
return null;
return internal.getString("essRecipientId");
}
@JsonIgnore
@JSONField(serialize = false)
public JSONObject getOrCreateExtension() {
if (internal == null)
internal = new JSONObject();
return internal;
}
public boolean isSignPersonPreset() {
return signPerson != null;
}
@JsonIgnore
@JSONField(serialize = false)
public Long getOuId() {
return signPerson == null ? 0L : signPerson.getOuId();
}
@JsonIgnore
@JSONField(serialize = false)
public Long getPersonId() {
return signPerson == null ? 0L : signPerson.getPersonId();
}
}

View File

@ -0,0 +1,64 @@
package cn.axzo.nanopart.ess.api.domain.contract;
import cn.axzo.nanopart.ess.api.enums.EssContractApproveState;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssApproveDetail {
/**
* 签署人, 动态签署人在没有签署(state=PENDING)的时候可能为null
*/
private OrgPersonInfo signPerson;
/**
* 签署编号
*/
private String recipientId;
/**
* 签署状态
*/
private EssContractApproveState state;
/**
* 签署顺序
*/
private long signOrder;
/**
* 对应签署人签署此合同的时间
*/
private long approveTimeMs;
/**
* 对应签署人状态的简单描述, : 已签署或者拒签的原因等
*/
private String message;
/**
* 对应签署人的签署截止时间
*/
private long approverDeadlineMs;
/**
* 单位id, 动态签署人在没有签署(state=PENDING)的时候可能为空
*/
private Long ouId;
/**
* 单位名称, 动态签署人在没有签署(state=PENDING)的时候可能为空
*/
private String ouName;
/**
* 对应签署人的手机号, 动态签署人在没有签署(state=PENDING)的时候可能为空
*/
private String phoneNumber;
}

View File

@ -0,0 +1,73 @@
package cn.axzo.nanopart.ess.api.domain.contract;
import cn.axzo.nanopart.ess.api.enums.EssContractState;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssContractInfo {
/**
* 发起合同的应用或业务场景
*/
private String appCode;
/**
* 业务编码
*/
private String bizCode;
/**
* 幂等编码
*/
private String idempotentCode;
/**
* 合同发起方单位id
*/
private Long creatorOuId;
/**
* 合同发起方人员id
*/
private Long creatorPersonId;
/**
* 合同名称
*/
private String contractName;
/**
* 电子签那边的合同id
*/
private String essContractId;
/**
* 合同状态. INIT: 合同创建, PART: 合同签署中, ALL: 合同签署完成, REJECT: 合同拒签, CANCEL: 合同撤回, WILLEXPIRE: 合同即将过期, DEADLINE: 合同流签(合同过期), RELIEVED: 解除协议已解除), INVALID: 合同已失效, EXCEPTION: 合同异常
*/
private EssContractState state;
/**
* 合并签署方信息, 如果是动态签署, 里面的签署人信息可能为nul
*/
private List<Approver> approvers;
/**
* 合同的签署方的签署情况
* <p/>
* 每次腾讯的回调都是给的全量签署情况数据, 如果业务依赖签署情况推荐业务状态, 需要自行做幂等处理
*/
private List<EssApproveDetail> approveDetails;
/**
* 腾讯电子签那边的消息
*/
private String essMessage;
}

View File

@ -0,0 +1,44 @@
package cn.axzo.nanopart.ess.api.domain.contract;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.context.request.async.StandardServletAsyncWebRequest;
import javax.validation.constraints.Min;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class OrgPersonInfo implements OrgPerson {
public static OrgPersonInfo create(OrgPerson person) {
return create(person.getOuId(), person.getPersonId());
}
public static OrgPersonInfo create(Long ouId, Long personId) {
OrgPersonInfo orgPersonInfo = new OrgPersonInfo();
orgPersonInfo.setOuId(ouId);
orgPersonInfo.setPersonId(personId);
return orgPersonInfo;
}
/**
* 单位id
*/
@NotNull(message = "ouId不能为空")
@Min(value = 1, message = "ouId必须大于0")
private Long ouId;
/**
* 人员id
*/
@NotNull(message = "personId不能为空")
@Min(value = 1, message = "personId必须大于0")
private Long personId;
}

View File

@ -0,0 +1,26 @@
package cn.axzo.nanopart.ess.api.domain.contract;
import lombok.Getter;
import lombok.Setter;
/**
* <a href="https://qian.tencent.com/developers/partnerApis/dataTypes/#approveroption">签署选项</a>
*
* @author yanglin
*/
@Setter
@Getter
public class SignOption {
/**
* 是否可以拒签. false-可以拒签, true-不可以拒签
*/
private boolean noRefuse = true;
/**
* 是否可以转发. false-可以转发 true-不可以转发
*/
private boolean noTransfer = false;
}

View File

@ -0,0 +1,12 @@
package cn.axzo.nanopart.ess.api.enums;
/**
* @author yanglin
*/
public enum Constraint {
// 一个子客只允许一个用户盖章
ONE_PERSON_PER_ORG
}

View File

@ -0,0 +1,12 @@
package cn.axzo.nanopart.ess.api.enums;
/**
* @author yanglin
*/
public enum EssApproverType {
// 企业/ 企业员工
ORGANIZATION,
// 企业/ 企业员工自动签
ENTERPRISESERVER
}

View File

@ -0,0 +1,27 @@
package cn.axzo.nanopart.ess.api.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor
public enum EssConsoleUrlEndpoint {
PC("PC", "(默认)web控制台链接, 需要在PC浏览器中打开"),
CHANNEL("CHANNEL", "H5跳转到电子签小程序链接, 一般用于发送短信中带的链接, 打开后进入腾讯电子签小程序"),
SHORT_URL("SHORT_URL", "H5跳转到电子签小程序链接的短链形式, 一般用于发送短信中带的链接, 打开后进入腾讯电子签小程序"),
WEIXIN_QRCODE_URL("WEIXIN_QRCODE_URL", "直接跳转至电子签小程序的二维码链接,无需通过中转页。您需要自行将其转换为二维码,使用微信扫码后可直接进入。请注意,直接点击链接是无效的。"),
APP("APP", "APP或小程序跳转电子签小程序链接, 一般用于贵方小程序或者APP跳转过来, 打开后进入腾讯电子签小程序"),
H5("H5", "H5长链接跳转H5链接, 一般用于贵方H5跳转过来, 打开后进入腾讯电子签H5页面"),
SHORT_H5("SHORT_H5", "H5短链跳转H5的短链形式, 一般用于发送短信中带的链接, 打开后进入腾讯电子签H5页面")
;
private final String code;
private final String description;
}

View File

@ -0,0 +1,39 @@
package cn.axzo.nanopart.ess.api.enums;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum EssContractApproveState {
PENDING("待签署"),
ACCEPT("已签署"),
REJECT("拒绝"),
DEADLINE("过期没人处理"),
CANCEL("流程已撤回"),
STOP("流程已终止"),
WAITPICKUP("待领取"),
FILLPENDING("待填写"),
FILLACCEPT("填写完成"),
FORWARD("已转他人处理"),
RELIEVED("解除协议(已解除)"),
FILLREJECT("拒绝填写"),
EXCEPTION("异常");
private final String description;
public static EssContractApproveState fromEssCode(String code) {
for (EssContractApproveState value : values()) {
if (value.name().equalsIgnoreCase(code)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,42 @@
package cn.axzo.nanopart.ess.api.enums;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author yanglin
*/
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum EssContractState {
INIT("合同创建", false),
PART("合同签署中", false),
ALL("合同签署完成", true),
REJECT("合同拒签", true),
CANCEL("合同撤回", true),
WILLEXPIRE("合同即将过期", false),
DEADLINE("合同流签(合同过期)", true),
RELIEVED("解除协议(已解除)", true),
INVALID("合同失效", true),
EXCEPTION("合同异常", true),
;
private final String description;
private final boolean finalState;
public static EssContractState fromEssCode(String code) {
for (EssContractState value : values()) {
if (value.name().equalsIgnoreCase(code)) {
return value;
}
}
return null;
}
}

View File

@ -0,0 +1,35 @@
package cn.axzo.nanopart.ess.api.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;
/**
* @author yanglin
*/
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
public enum EssEmbedType {
// 生成创建印章的嵌入页面
CREATE_SEAL(false, "CREATE_SEAL", EssSubject.SEAL),
// 生成预览印章列表的嵌入页面
PREVIEW_SEAL_LIST(false, "PREVIEW_SEAL_LIST", EssSubject.SEAL),
// 生成预览印章详情的嵌入页面
PREVIEW_SEAL_DETAIL(true, "PREVIEW_SEAL_DETAIL", EssSubject.SEAL),
// 生成预览合同文档的嵌入页面H5链接支持移动端的浏览器中打开
PREVIEW_CONTRACT(true, "PREVIEW_FLOW", EssSubject.CONTRACT),
// 生成预览合同详情的嵌入页面仅支持PC的浏览器中打开
PREVIEW_CONTRACT_DETAIL(true, "PREVIEW_FLOW_DETAIL", EssSubject.CONTRACT);
private final boolean businessIdRequired;
private final String essCode;
private final EssSubject essSubject;
public enum EssSubject {
SEAL, CONTRACT
}
}

View File

@ -0,0 +1,18 @@
package cn.axzo.nanopart.ess.api.enums;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum EssPersonState {
// 创建
CREATED,
// 认证
AUTHORIZED,
// 离职
RESIGNED
}

View File

@ -0,0 +1,21 @@
package cn.axzo.nanopart.ess.api.enums;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor
public enum EssSealState {
// 创建
CREATE,
// 删除
DELETED,
// 停用
DISABLED,
// 启用
ENABLED;
}

View File

@ -0,0 +1,40 @@
package cn.axzo.nanopart.ess.api.enums;
import lombok.Getter;
/**
* <a href="https://qian.tencent.com/developers/partnerApis/seals/ChannelDescribeOrganizationSeals/">印章类型</a>
*
* @author yanglin
*/
@Getter
public enum EssSealType {
OFFICIAL("公章", "OFFICIAL", "OFFICIAL"),
CONTRACT("合同专用章", "CONTRACT", "CONTRACT"),
FINANCE("财务专用章", "FINANCE", "FINANCE"),
PERSONNEL("人事专用章", "PERSONNEL", "PERSONNEL"),
OTHER("其他", "OTHER");
private final String description;
private final String useForLimitCode;
private final String[] essCodes;
EssSealType(String description, String useForLimitCode, String... essCodes) {
this.description = description;
this.useForLimitCode = useForLimitCode;
this.essCodes = essCodes;
}
public static EssSealType fromEssCode(String essCode) {
for (EssSealType value : values()) {
for (String code : value.essCodes) {
if (code.equals(essCode)) {
return value;
}
}
}
return OTHER;
}
}

View File

@ -0,0 +1,35 @@
package cn.axzo.nanopart.ess.api.enums;
import java.util.Arrays;
import cn.axzo.framework.rocketmq.Event;
import lombok.Getter;
/**
* @Author zr
* @Date 2024/3/19 18:01
* @Description
**/
@Getter
public enum MQEvent {
ESS_CONTRACT_STATE_CHANGE("nanopart", "ess-contract-state-change", "腾讯电子签合同状态变化"),
;
private final String model;
private final String tag;
private final String desc;
private final Event.EventCode eventCode;
MQEvent(String model, String tag, String desc) {
this.eventCode = Event.EventCode.builder().module(model).name(tag).build();
this.model = model;
this.tag = tag;
this.desc = desc;
}
public static MQEvent getByName(String name) {
return Arrays.stream(MQEvent.values()).filter(item -> item.name().equals(name)).findFirst().orElse(null);
}
}

View File

@ -0,0 +1,9 @@
package cn.axzo.nanopart.ess.api.enums;
/**
* @author yanglin
*/
public enum SignUrlEndpoint {
PC, WEIXIN_APP
}

View File

@ -0,0 +1,25 @@
package cn.axzo.nanopart.ess.api.mq;
import cn.axzo.nanopart.ess.api.domain.contract.EssContractInfo;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssContractDownloadPDFEvent extends MqMessage {
/**
* 合同信息
*/
private EssContractInfo contract;
/**
* 是否重复下载
*/
private boolean retryDownload;
}

View File

@ -0,0 +1,20 @@
package cn.axzo.nanopart.ess.api.mq;
import cn.axzo.nanopart.ess.api.domain.contract.EssContractInfo;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class EssContractStateChangeMessage extends MqMessage {
/**
* 合同信息, 处理签署详情时需要做幂等
*/
private EssContractInfo contract;
}

View File

@ -0,0 +1,34 @@
package cn.axzo.nanopart.ess.api.mq;
import cn.hutool.core.lang.UUID;
import lombok.Getter;
import lombok.Setter;
import java.io.Serializable;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* @author yanglin
*/
@Setter
@Getter
public abstract class MqMessage implements Serializable {
/**
* 消息唯一id
*/
private String messageId = UUID.randomUUID().toString();
/**
* 消息发送时间
*/
private Date messageSendTime = new Date();
/**
* 消息发送时间, 字符串格式
*/
private String messageSendTimeStr = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(messageSendTime);
}

View File

@ -0,0 +1,40 @@
package cn.axzo.nanopart.ess.api.request;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class AddSealAuthorizationRequest implements SealAndPersonRequest {
/**
* 印章id
*/
@NotBlank(message = "essSealId不能为空")
private String essSealId;
/**
* 人员id
*/
@NotNull(message = "personId不能为空")
private Long personId;
/**
* 前端忽略, yoke传递
*/
private Long operatorPersonId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,39 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class AddSealPersonRequest {
/**
* 印章id
*/
@NotBlank(message = "essSealId不能为空")
private String essSealId;
/**
* 人员id列表
*/
@NotNull(message = "personId不能为空")
private Long personId;
/**
* 前端忽略, yoke传递
*/
private Long operatorPersonId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,55 @@
package cn.axzo.nanopart.ess.api.request;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import com.alibaba.fastjson.JSON;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.enums.SignUrlEndpoint;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class AssignSignUrlRequest implements OrgPerson {
/**
* 合同id
*/
@NotBlank(message = "essContractId不能为空")
private String essContractId;
/**
* 单位id
*/
@NotNull(message = "ouId不能为空")
private Long ouId;
/**
* 人员id, 需要有印章权限
*/
@NotNull(message = "personId不能为空")
private Long personId;
/**
* PC: 电脑端, WEIXIN_APP: 小程序, H5: h5
*/
@NotNull(message = "endpoint不能为空")
private SignUrlEndpoint endpoint;
/**
* 签署方编号, 可以使用该编号指定动态签署方的信息, 该编号在创建合同的时候会返回
*/
@NotBlank(message = "recipientId不能为空")
private String recipientId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,20 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class CallbackContent {
private String encrypt;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,215 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class CallbackRequest {
@JsonProperty(value = "MsgId")
@JSONField(name = "MsgId")
private String MsgId;
@JsonProperty(value = "MsgType")
@JSONField(name = "MsgType")
private String MsgType;
@JsonProperty(value = "MsgVersion")
@JSONField(name = "MsgVersion")
private String MsgVersion;
@JsonProperty(value = "MsgData")
@JSONField(name = "MsgData")
private JSONObject MsgData;
public <T> T readMsgData(Class<T> clazz) {
return MsgData.toJavaObject(clazz);
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
@Setter
@Getter
public static class OrgAuthorizationFinish {
// 第三方应用的应用编号
private String ApplicationId;
// 第三方平台子客企业的唯一标识定义Agent中的ProxyOrganizationOpenId一样
private String ProxyOrganizationOpenId;
// 开通服务的第三方平台子客企业员工的唯一标识
private String ProxyOperatorOpenId;
// 子企业的ProxyAppId, 现在还没有地方使用此ProxyAppId, 可忽略
private String ProxyAppId;
// 是否开通, true表示开通, false表示未开通
private boolean OpenSuccess;
// 子企业的企业名字
private String OrganizationName;
// 子企业的企业社会统一信用代码
private String USCC;
// 子企业的法人的姓名
private String LegalName;
// 子企业的法人的OpenId
private String LegalOpenId;
// 子企业超管的姓名
private String AdminName;
// 子企业超管的手机号打码
private String AdminMobile;
}
@Setter
@Getter
public static class OrgPersonJoin {
// 第三方应用的应用编号
private String ApplicationId;
// 第三方平台子客企业的唯一标识定义Agent中的ProxyOrganizationOpenId一样, 可以参考Agent结构体
private String ProxyOrganizationOpenId;
// 加入企业的第三方平台子客企业员工的唯一标识
private String ProxyOperatorOpenId;
// 员工姓名
private String EmployeeName;
// 员工手机号打码
private String EmployeeMobile;
}
@Setter
@Getter
public static class SealOperate {
// 第三方应用的应用编号
private String ApplicationId;
// 第三方平台子客企业的唯一标识
private String ProxyOrganizationOpenId;
// 执行对应印章动作的第三方平台子客企业员工的唯一标识
private String ProxyOperatorOpenId;
/**
* 1. 如果印章授权给员工的行为, 此处为被授权员工的OpenId
* 2. 如果是取消某员工的印章授权, 此处为被取消授权员工的OpenId
* 3. 其他印章行为没有此字段
*/
private String AuthorizedOperatorOpenId;
// 对应的印章ID为32位字符串
private String SealId;
/**
* 对应的印章类型, 类型的值包括:
* OFFICIAL:企业公章
* CONTRACT:企业合同专用章
* ORGANIZATION_SEAL:客户本地上传的企业印章
* LEGAL_PERSON_SEAL:客户本地上传的法人印章
* FINANCE:财务专用章
* PERSONNEL:人事专用章
*/
private String SealType;
/**
* 印章创建 Create
* 印章删除 Delete
* 印章停用 Disable
* 印章启用 Enable
* 印章授权给某些员工 Valid
* 取消某些员工的印章授权 Invalid
*/
private String Operate;
/**
* 1. 如果印章启用或者停用行为, 是此印章权限的员工们的OpenId
* 2. 其他印章行为没有此字段
*/
private JSONArray AuthorizedUsers;
}
@Setter
@Getter
public static class ContractStateChanged {
// 第三方平台子客企业的唯一标识
private String ApplicationId;
// 第三方平台子客企业的唯一标识
private String ProxyOrganizationOpenId;
// 第三方平台子客企业员工的唯一标识
private String ProxyOperatorOpenId;
// 用PDF文件创建签署流程和用模板创建签署流程创建签署流程时候传递的CustomerData参数
private String CustomerData;
// 触发回调的合同流程ID为32位字符串
private String FlowId;
// 触发回调的合同流程的名称
private String FlowName;
// 触发回调的合同流程的类别分类
private String FlowType;
// 合同状态具体含义可以参考上述其他说明中的 会出现回调的合同状态
private String FlowStatus;
// 当合同流程状态为合同拒签, 合同撤回等状态时此字段为拒签或撤销原因其他状态时此字段为空值
private String FlowMessage;
// 合同流程的创建时间戳格式为Unix标准时间戳
private Integer CreateOn;
// 签署流程的签署截止时间格式为Unix标准时间戳
private Integer Deadline;
// 合同(流程)关注方信息列表, 结构体定义可以查看开发者中的CcInfo
private JSONArray CcInfo;
// 合同(流程)签署人信息列表, 结构体的定义可以参考下面的FlowApproverDetail
private List<FlowApproverDetail> FlowApproverInfo;
// 如果合同归属合同组, 此结构体为合同组的信息, 结构体的定义可以参考下面的FlowGroupMessageDetail
private JSONObject FlowGroupMessage;
// 此回调触发的时间,格式为Unix标准时间戳
private Integer OccurTime;
}
@Setter
@Getter
public static class FlowApproverDetail {
// 如果第三方子企业员工签署, 此字段为第三方子企业员工的唯一标识
private String ProxyOperatorOpenId;
// 如果第三方子企业员工签署, 此字段为第三方子企业的名称
private String ProxyOrganizationName;
// 如果saas企业员工签署, 此字段为saas企业的名称
private String OrganizationName;
// 如果第三方子企业员工签署, 此字段为第三方子企业的唯一标识
private String ProxyOrganizationOpenId;
// 对应签署人的手机号
private String PhoneNumber;
// 如果合同是顺序签署, SignOrder对应签署人的签署顺序
private long SignOrder;
// 对应签署人的参与角色ID
private String RecipientId;
// 对应签署人的名字
private String ApproveName;
// 对应签署人的状态, 具体含义可以参考上述其他说明中的 签署人状态
private String ApproveStatus;
// 对应签署人状态的简单描述, : 已签署或者拒签的原因等
private String ApproveMessage;
// 对应签署人签署此合同的时间格式为Unix标准时间戳
private long ApproveTime;
// 对应签署人签署此合同的证书信息, 此功能为白名单功能, 使用前请联系对接的客户经理沟通
private String CaSign;
// 对应签署人的签署截止时间格式为Unix标准时间戳
private long ApproverDeadline;
}
@Setter
@Getter
public static class SuperAdminChanged {
// 第三方应用的应用编号
private String ApplicationId;
// 第三方平台子客企业的唯一标识
private String ProxyOrganizationOpenId;
// 新的超管的企业的第三方平台子客企业员工的唯一标识
private String ChangeToUserOpenId;
// 新的超管的名字
private String ChangeToUserName;
// 新的超管的手机号
private String ChangeToUserMobile;
// 老的超管的企业的第三方平台子客企业员工的唯一标识
private String OldAdminOpenId;
// 老的超管的名字
private String OldAdminName;
// 老的超管的手机号
private String OldAdminMobile;
}
}

View File

@ -0,0 +1,53 @@
package cn.axzo.nanopart.ess.api.request;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.enums.EssConsoleUrlEndpoint;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class CreateConsoleLoginUrlRequest implements OrgPerson {
/**
* 单位id
*/
@NotNull(message = "单位id不能为空")
private Long ouId;
/**
* 人员id
*/
@NotNull(message = "人员id不能为空")
private Long personId;
/**
* 具体传啥问产品:
* PC: (默认)web控制台链接, 需要在PC浏览器中打开
* CHANNEL: H5跳转到电子签小程序链接, 一般用于发送短信中带的链接, 打开后进入腾讯电子签小程序
* SHORT_URL: H5跳转到电子签小程序链接的短链形式, 一般用于发送短信中带的链接, 打开后进入腾讯电子签小程序
* WEIXIN_QRCODE_URL: 直接跳转至电子签小程序的二维码链接无需通过中转页您需要自行将其转换为二维码使用微信扫码后可直接进入请注意直接点击链接是无效的
* APP: APP或小程序跳转电子签小程序链接, 一般用于贵方小程序或者APP跳转过来, 打开后进入腾讯电子签小程序
* H5: H5长链接跳转H5链接, 一般用于贵方H5跳转过来, 打开后进入腾讯电子签H5页面
* SHORT_H5: H5短链跳转H5的短链形式, 一般用于发送短信中带的链接, 打开后进入腾讯电子签H5页面
*/
@NotNull(message = "endpoint不能为空")
private EssConsoleUrlEndpoint endpoint;
/**
* 调试参数, 不填
*/
private transient boolean essCheckLoginPerson = true;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,40 @@
package cn.axzo.nanopart.ess.api.request;
import cn.axzo.nanopart.ess.api.domain.CreateContractByFile;
import cn.axzo.nanopart.ess.api.domain.CreateContractInfo;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.validation.Valid;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class CreateContractByFileRequest extends CreateContractRequest {
/**
* 通过上传PDF发起合同
*/
@Valid
@NotNull(message = "byFile不能为空")
private CreateContractByFile byFile;
@JsonIgnore
@JSONField(serialize = false)
public CreateContractInfo getContract() {
return byFile;
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,52 @@
package cn.axzo.nanopart.ess.api.request;
import cn.axzo.nanopart.ess.api.domain.CreateContractInfo;
import cn.axzo.nanopart.ess.api.domain.OperatorInfo;
import com.alibaba.fastjson.annotation.JSONField;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Getter;
import lombok.Setter;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
import java.util.UUID;
/**
* @author yanglin
*/
@Setter
@Getter
public abstract class CreateContractRequest {
/**
* 发起合同的应用或业务场景
*/
@NotBlank(message = "appCode不能为空")
private String appCode;
/**
* 业务编码
*/
@NotNull(message = "bizCode不能为空")
private String bizCode;
/**
* 幂等编码, 最大长度200. 使用appCode和idempotentCode的合同不能重复
*/
@NotBlank(message = "idempotentCode不能为空")
private String idempotentCode = UUID.randomUUID().toString();
/**
* 合同发起方信息
*/
@Valid
@NotNull(message = "creator不能为空")
private OperatorInfo creator;
@JsonIgnore
@JSONField(serialize = false)
public abstract CreateContractInfo getContract();
}

View File

@ -0,0 +1,27 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author yanglin
*/
@Setter
@Getter
public class DownloadSingedContractPdfRequest {
/**
* 合同id
*/
@NotBlank(message = "essContractId不能为空")
private String essContractId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,27 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetContractDetailByContractIdRequest {
/**
* 合同id
*/
@NotBlank(message = "essContractId不能为空")
private String essContractId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,43 @@
package cn.axzo.nanopart.ess.api.request;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.enums.SignUrlEndpoint;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetContractSignUrlRequest implements OrgPerson {
/**
* 合同id
*/
@NotBlank(message = "essContractId不能为空")
private String essContractId;
/**
* 单位id
*/
@NotNull(message = "ouId不能为空")
private Long ouId;
/**
* 人员id, 需要有印章权限
*/
@NotNull(message = "personId不能为空")
private Long personId;
/**
* PC: 电脑端, WEIXIN_APP: 小程序
*/
@NotNull(message = "urlType不能为空")
private SignUrlEndpoint urlType;
}

View File

@ -0,0 +1,47 @@
package cn.axzo.nanopart.ess.api.request;
import cn.axzo.nanopart.ess.api.enums.EssEmbedType;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetEmbedWebUrlRequest {
/**
* 单位id
*/
@NotNull(message = "单位id不能为空")
private Long ouId;
/**
* 内嵌类型.
* CREATE_SEAL: 生成创建印章的嵌入页面
* PREVIEW_CONTRACT: 生成预览合同文档的嵌入页面H5链接支持移动端的浏览器中打开
* PREVIEW_CONTRACT_DETAIL: 生成预览合同详情的嵌入页面仅支持PC的浏览器中打开
* PREVIEW_SEAL_LIST: 生成预览印章列表的嵌入页面
* PREVIEW_SEAL_DETAIL: 生成预览印章详情的嵌入页面
*/
@NotNull(message = "内嵌类型不能为空")
private EssEmbedType embedType;
/**
* 业务id.
* PREVIEW_CONTRACT: 传入合同id
* PREVIEW_CONTRACT_DETAIL: 传入合同id
* PREVIEW_SEAL_DETAIL: 传入印章id
*/
private String businessId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,29 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotEmpty;
import java.util.Set;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetOrgAuthStatesRequest {
/**
* 单位id
*/
@NotEmpty(message = "单位id不能为空")
private Set<Long> ouIds;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,33 @@
package cn.axzo.nanopart.ess.api.request;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetPersonAuthStateRequest implements OrgPerson {
/**
* 单位id
*/
@NotNull(message = "单位id不能为空")
private Long ouId;
/**
* 人员id
*/
@NotNull(message = "人员id不能为空")
private Long personId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,22 @@
package cn.axzo.nanopart.ess.api.request;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetSealPersonRequest {
/**
* 印章在腾讯的id
*/
@NotBlank(message = "essSealId不能为空")
private String essSealId;
}

View File

@ -0,0 +1,28 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotNull;
import java.util.Set;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetSealsRequest {
/**
* 单位列表
*/
@NotNull(message = "单位列表不能为空")
private Set<Long> ouIds;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,34 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class RemoveSealAuthorizationRequest implements SealAndPersonRequest {
/**
* 印章id
*/
@NotBlank(message = "essSealId不能为空")
private String essSealId;
/**
* 人员id
*/
@NotNull(message = "personId不能为空")
private Long personId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,34 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;
/**
* @author yanglin
*/
@Setter
@Getter
public class RemoveSealPersonRequest {
/**
* 印章id
*/
@NotBlank(message = "essSealId不能为空")
private String essSealId;
/**
* 人员id
*/
@NotNull(message = "personId不能为空")
private Long personId;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,32 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author yanglin
*/
@Setter
@Getter
public class RevokeContractRequest {
/**
* 合同id
*/
@NotBlank(message = "essContractId不能为空")
private String essContractId;
/**
* 撤销原因
*/
private String reason;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,33 @@
package cn.axzo.nanopart.ess.api.request;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.NotBlank;
/**
* @author yanglin
*/
@Setter
@Getter
public class SaveContractSnapshotRequest {
/**
* 合同id
*/
@NotBlank(message = "essContractId不能为空")
private String essContractId;
/**
* 是否重复下载, 即已经下载过了, 但是需要重新下载.
* 如果合同已经是终状 retryDownload=true 也不会重新下载
*/
private boolean retryDownload = false;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,13 @@
package cn.axzo.nanopart.ess.api.request;
/**
* @author yanglin
*/
public interface SealAndPersonRequest {
String getEssSealId();
Long getPersonId();
}

View File

@ -0,0 +1,24 @@
package cn.axzo.nanopart.ess.api.response;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class AssignSignUrlResponse {
/**
* 有效期为5分钟.
*/
private String url;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,24 @@
package cn.axzo.nanopart.ess.api.response;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class CreateConsoleLoginUrlResponse {
/**
* 腾讯电子签登录地址
*/
private String consoleLoginUrl;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,40 @@
package cn.axzo.nanopart.ess.api.response;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
public class CreateContractByFileResponse {
/**
* 合同id
*/
private String essContractId;
/**
* 签署方编号, 可以使用该编号指定动态签署方的信息, 顺序和请求中的approvers一致.
* <p/>
* 比如在使用了动态签署人的情况下, 就需要保存这个字段
* <p/>
* 在获取签署链接的时候回传到获取签署链接的请求中, 用以和创建合同的请求中的签署人的其它信息进行匹配
*/
private List<String> essRecipientIds;
/**
* 是否重复 (幂等)
*/
private boolean isDuplicate;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,24 @@
package cn.axzo.nanopart.ess.api.response;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class DownloadSingedContractPdfResponse {
/**
* 合同pdf下载地址. 下载链接有效期为5分钟
*/
private String pdfUrl;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,25 @@
package cn.axzo.nanopart.ess.api.response;
import cn.axzo.nanopart.ess.api.domain.contract.EssContractInfo;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetContractDetailByContractIdResponse {
/**
* 合同信息
*/
private EssContractInfo contract;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,21 @@
package cn.axzo.nanopart.ess.api.response;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetContractSignUrlResponse {
/**
* 签署地址或二维码地址, 有效期为5分钟
* 如果是PC端, 才是二维码地址
* 如果是小程序端, 则是小程序地址
*/
private String signOrQrUrl;
}

View File

@ -0,0 +1,24 @@
package cn.axzo.nanopart.ess.api.response;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetEmbedWebUrlResponse {
/**
* 内嵌页面地址, 有效期为5分钟
*/
private String embedWebUrl;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,29 @@
package cn.axzo.nanopart.ess.api.response;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetOrgAuthStatesResponse {
/**
* 单位id
*/
private Long ouId;
/**
* 是否开通了电子签
*/
private boolean isAuthorized;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,24 @@
package cn.axzo.nanopart.ess.api.response;
import com.alibaba.fastjson.JSON;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class GetPersonAuthStateResponse {
/**
* 是否认证
*/
private boolean isAuthorized;
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,34 @@
package cn.axzo.nanopart.ess.api.utils;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @author yanglin
*/
@Getter
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public enum YesOrNo {
YES("YES", ""), NO("NO", "");
private final String code;
private final String desc;
public static YesOrNo valueOf(boolean value) {
return value ? YES : NO;
}
public boolean booleanValue() {
return this == YES;
}
public boolean isYes() {
return this == YES;
}
public boolean isNo() {
return this == NO;
}
}

View File

@ -0,0 +1,2 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.axzo.nanopart.ess.api.EssAutoConfiguration

128
ess/ess-server/pom.xml Normal file
View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>cn.axzo.nanopart</groupId>
<artifactId>ess</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>ess-server</artifactId>
<packaging>jar</packaging>
<name>ess-server</name>
<dependencies>
<dependency>
<groupId>com.xuxueli</groupId>
<artifactId>xxl-job-core</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>com.tencentcloudapi</groupId>
<artifactId>tencentcloud-sdk-java</artifactId>
<version>3.1.1210</version>
</dependency>
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-web-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-consumer-spring-cloud-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-processor-spring-boot-starter</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-mybatisplus-spring-boot-starter</artifactId>
</dependency>
<!-- swagger-yapi -->
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-swagger-yapi-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework</groupId>
<artifactId>axzo-logger-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.framework.rocketmq</groupId>
<artifactId>axzo-common-rocketmq</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.pokonyan</groupId>
<artifactId>pokonyan</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.workflow</groupId>
<artifactId>workflow-engine-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.maokai</groupId>
<artifactId>maokai-api</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.nanopart</groupId>
<artifactId>ess-api</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.axzo.basics</groupId>
<artifactId>basics-profiles-api</artifactId>
</dependency>
<dependency>
<groupId>cn.axzo.maokai</groupId>
<artifactId>maokai-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.axzo</groupId>
<artifactId>thor-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.axzo.apollo</groupId>
<artifactId>apollo-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.axzo.basics</groupId>
<artifactId>basics-domainless-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>cn.axzo.org</groupId>
<artifactId>org-api</artifactId>
<version>1.0.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>cn.axzo.karma</groupId>
<artifactId>karma-api</artifactId>
<version>2.0.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,101 @@
package cn.axzo.nanopart.ess.server.dao;
import java.util.List;
import java.util.Optional;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Repository;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.axzo.nanopart.ess.api.domain.contract.Approver;
import cn.axzo.nanopart.ess.api.domain.contract.EssApproveDetail;
import cn.axzo.nanopart.ess.api.enums.EssContractState;
import cn.axzo.nanopart.ess.server.entity.EssContract;
import cn.axzo.nanopart.ess.server.mapper.EssContractMapper;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
/**
* @author yanglin
*/
@Repository("essContractDao")
public class EssContractDao extends ServiceImpl<EssContractMapper, EssContract> {
public Optional<EssContract> findByIdempotentCode(String appCode, String idempotentCode) {
return lambdaQuery() //
.eq(EssContract::getAppCode, appCode) //
.eq(EssContract::getIdempotentCode, idempotentCode) //
.oneOpt();
}
public EssContract getOrThrow(String essContractId) {
EssContract contract = findOrNull(essContractId);
BizAssertions.assertNotNull(contract, "找不到合同信息, essContractId={}", essContractId);
return contract;
}
public EssContract findOrNull(String essContractId) {
return find(essContractId, false).orElse(null);
}
public EssContract getForUpdateOrThrow(String essContractId) {
EssContract contract = find(essContractId, true).orElse(null);
BizAssertions.assertNotNull(contract, "找不到合同信息, 合同id={}", essContractId);
return contract;
}
private Optional<EssContract> find(String essContractId, boolean forUpdate) {
return lambdaQuery() //
.eq(EssContract::getEssContractId, essContractId) //
.last(forUpdate, "FOR UPDATE") //
.oneOpt();
}
public void setEssContractCreated(Long id, String flowId, List<String> essFileIds, List<Approver> approvers) {
lambdaUpdate() //
.eq(EssContract::getId, id) //
.set(EssContract::getEssFieldIds, JSON.toJSONString(essFileIds)) //
.set(EssContract::getEssContractId, flowId) //
.set(EssContract::getApprovers, JSON.toJSONString(approvers)) //
.update();
}
public void updateState(EssContract contract, EssContractState state,
List<EssApproveDetail> approveDetailsOrPreserve, String essMessage) {
lambdaUpdate() //
.eq(EssContract::getId, contract.getId()) //
.set(EssContract::getState, state) //
.set(StringUtils.isNotBlank(essMessage), EssContract::getEssMessage, essMessage) //
.set(CollectionUtils.isNotEmpty(approveDetailsOrPreserve), EssContract::getApproveDetails,
approveDetailsOrPreserve == null ? "[]" : JSON.toJSONString(approveDetailsOrPreserve)) //
.update();
}
public void setOssFileKey(EssContract contract, String fileKey) {
lambdaUpdate() //
.eq(EssContract::getId, contract.getId()) //
.set(EssContract::getOssFileKey, fileKey) //
.update();
}
public void updateAssigment(EssContract contract) {
if (contract.getAssignment() == null)
return;
lambdaUpdate() //
.eq(EssContract::getId, contract.getId()) //
.set(EssContract::getAssignment, JSON.toJSONString(contract.getAssignment())) //
.update();
}
public void updateExt(EssContract contract) {
if (contract.getRecordExt() == null)
return;
lambdaUpdate() //
.eq(EssContract::getId, contract.getId()) //
.set(EssContract::getRecordExt, JSON.toJSONString(contract.getRecordExt())) //
.update();
}
}

View File

@ -0,0 +1,48 @@
package cn.axzo.nanopart.ess.server.dao;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.axzo.nanopart.ess.server.entity.EssLog;
import cn.axzo.nanopart.ess.server.mapper.EssLogMapper;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
/**
* @author yanglin
*/
@RefreshScope
@Repository("essLogDao")
public class EssLogDao extends ServiceImpl<EssLogMapper, EssLog> {
@Value("${ess.enableRequestLog:true}")
private boolean enableRequestLog;
public void logRequest(String context, Object subject, Object request) {
if (enableRequestLog)
log(context, subject, "request", request);
}
public void log(String context, Object subject, Object... logContents) {
log(null, context, subject, logContents);
}
public void log(Exception exception, String context, Object subject, Object... logContents) {
EssLog log = new EssLog();
log.setContext(context);
log.setSubject(String.valueOf(subject));
log.setError(exception);
if (logContents != null && logContents.length > 0) {
BizAssertions.assertTrue(logContents.length % 2 == 0, "logContents must be even");
for (int i = 0; i < logContents.length; i += 2) {
BizAssertions.assertTrue(logContents[i] instanceof String, "logContents key must be String");
log.addLogContent((String) logContents[i], logContents[i + 1]);
}
}
save(log);
}
}

View File

@ -0,0 +1,63 @@
package cn.axzo.nanopart.ess.server.dao;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.utils.YesOrNo;
import cn.axzo.nanopart.ess.server.entity.EssOrg;
import cn.axzo.nanopart.ess.server.mapper.EssOrgMapper;
/**
* @author yanglin
*/
@Repository("essOrgDao")
public class EssOrgDao extends ServiceImpl<EssOrgMapper, EssOrg> {
public List<EssOrg> getByOuIds(Collection<Long> ouIds) {
if (CollectionUtils.isEmpty(ouIds))
return Collections.emptyList();
return lambdaQuery() //
.in(EssOrg::getOuId, ouIds) //
.list();
}
public EssOrg findOrNull(Long ouId) {
return find(ouId, false).orElse(null);
}
public EssOrg findForUpdateOrNull(Long ouId) {
return find(ouId, true).orElse(null);
}
private Optional<EssOrg> find(Long ouId, boolean forUpdate) {
return lambdaQuery() //
.eq(EssOrg::getOuId, ouId) //
.last(forUpdate, "FOR UPDATE") //
.oneOpt();
}
public void setOrgAuthorized(OrgPerson superAdmin) {
lambdaUpdate() //
.eq(EssOrg::getOuId, superAdmin.getOuId()) //
.set(EssOrg::getIsAuthorized, YesOrNo.YES) //
.set(EssOrg::getSuperAdminPersonId, superAdmin.getPersonId()) //
.update();
}
public void changeSuperAdmin(OrgPerson admin) {
lambdaUpdate() //
.eq(EssOrg::getOuId, admin.getOuId()) //
.set(EssOrg::getSuperAdminPersonId, admin.getPersonId()) //
.update();
}
}

View File

@ -0,0 +1,61 @@
package cn.axzo.nanopart.ess.server.dao;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.enums.EssPersonState;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.mapper.EssPersonMapper;
/**
* @author yanglin
*/
@Repository("essPersonDao")
public class EssPersonDao extends ServiceImpl<EssPersonMapper, EssPerson> {
public EssPerson findOrNull(OrgPerson person) {
return findOrNull(person.getOuId(), person.getPersonId());
}
public EssPerson findOrNull(Long ouId, Long personId) {
return find(ouId, personId, false).orElse(null);
}
public Optional<EssPerson> find(OrgPerson person, boolean forUpdate) {
return find(person.getOuId(), person.getPersonId(), forUpdate);
}
private Optional<EssPerson> find(Long ouId, Long personId, boolean forUpdate) {
return lambdaQuery() //
.eq(EssPerson::getOuId, ouId) //
.eq(EssPerson::getPersonId, personId) //
.last(forUpdate, "FOR UPDATE") //
.oneOpt();
}
public void setOrgPersonState(OrgPerson person, EssPersonState state) {
lambdaUpdate() //
.eq(EssPerson::getOuId, person.getOuId()) //
.eq(EssPerson::getPersonId, person.getPersonId()) //
.set(EssPerson::getState, state) //
.update();
}
public List<EssPerson> getByPersonIds(Collection<Long> personIds) {
if (CollectionUtils.isEmpty(personIds))
return Collections.emptyList();
return lambdaQuery() //
.in(EssPerson::getPersonId, personIds) //
.list();
}
}

View File

@ -0,0 +1,46 @@
package cn.axzo.nanopart.ess.server.dao;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.axzo.nanopart.ess.api.enums.EssSealState;
import cn.axzo.nanopart.ess.server.entity.EssSeal;
import cn.axzo.nanopart.ess.server.mapper.EssSealMapper;
/**
* @author yanglin
*/
@Repository("essSealDao")
public class EssSealDao extends ServiceImpl<EssSealMapper, EssSeal> {
public Optional<EssSeal> findByEssSealId(String essSealId) {
return lambdaQuery() //
.eq(EssSeal::getEssSealId, essSealId) //
.oneOpt();
}
public void updateState(String essSealId, EssSealState state) {
lambdaUpdate() //
.eq(EssSeal::getEssSealId, essSealId) //
.set(EssSeal::getState, state) //
.update();
}
public List<EssSeal> getByOuId(Long ouId) {
return lambdaQuery() //
.eq(EssSeal::getOuId, ouId) //
.list();
}
public void updateSealName(String essSealId, String sealName) {
lambdaUpdate() //
.eq(EssSeal::getEssSealId, essSealId) //
.set(EssSeal::getName, sealName) //
.update();
}
}

View File

@ -0,0 +1,81 @@
package cn.axzo.nanopart.ess.server.dao;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import org.springframework.stereotype.Repository;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.utils.YesOrNo;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import cn.axzo.nanopart.ess.server.mapper.EssSealPersonMapper;
/**
* @author yanglin
*/
@Repository("essSealPersonDao")
public class EssSealPersonDao extends ServiceImpl<EssSealPersonMapper, EssSealPerson> {
public void setPersonAuthorized(String essSealId, Long personId) {
lambdaUpdate() //
.eq(EssSealPerson::getEssSealId, essSealId) //
.eq(EssSealPerson::getPersonId, personId) //
.set(EssSealPerson::getIsAuthorized, YesOrNo.YES) //
.set(EssSealPerson::getAuthorizeTime, new Date()) //
.update();
}
public void setSealPersonAuthorizedBy(String essSealId, Long personId, Long authorizedByPersonId) {
lambdaUpdate() //
.eq(EssSealPerson::getEssSealId, essSealId) //
.eq(EssSealPerson::getPersonId, personId) //
.set(EssSealPerson::getAuthorizedByPersonId, authorizedByPersonId) //
.update();
}
public void removeSealAuthorization(String essSealId, Long personId) {
lambdaUpdate() //
.eq(EssSealPerson::getEssSealId, essSealId) //
.eq(EssSealPerson::getPersonId, personId) //
.set(EssSealPerson::getIsAuthorized, YesOrNo.NO) //
.update();
}
public Optional<EssSealPerson> find(String essSealId, Long personId) {
return find(essSealId, personId, false);
}
public Optional<EssSealPerson> find(String essSealId, Long personId, boolean forUpdate) {
return lambdaQuery() //
.eq(EssSealPerson::getEssSealId, essSealId) //
.eq(EssSealPerson::getPersonId, personId) //
.last(forUpdate, "FOR UPDATE") //
.oneOpt();
}
public List<EssSealPerson> getByPerson(OrgPerson person) {
return lambdaQuery() //
.eq(EssSealPerson::getOuId, person.getOuId()) //
.eq(EssSealPerson::getPersonId, person.getPersonId()) //
.list();
}
public List<EssSealPerson> get(String essSealId) {
return lambdaQuery() //
.eq(EssSealPerson::getEssSealId, essSealId) //
.orderByDesc(EssSealPerson::getId) //
.list();
}
public void deleteByPerson(OrgPerson person) {
lambdaUpdate()
.eq(EssSealPerson::getOuId, person.getOuId())
.eq(EssSealPerson::getPersonId, person.getPersonId())
.remove();
}
}

View File

@ -0,0 +1,203 @@
package cn.axzo.nanopart.ess.server.entity;
import cn.axzo.foundation.dao.support.mysql.type.BaseListTypeHandler;
import cn.axzo.nanopart.ess.api.domain.contract.Approver;
import cn.axzo.nanopart.ess.api.domain.contract.EssApproveDetail;
import cn.axzo.nanopart.ess.api.enums.Constraint;
import cn.axzo.nanopart.ess.api.enums.EssContractApproveState;
import cn.axzo.nanopart.ess.api.enums.EssContractState;
import cn.axzo.nanopart.ess.server.entity.domain.Assignment;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import lombok.Getter;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import java.util.List;
/**
* @author yanglin
*/
@Setter
@Getter
@TableName(value = "ess_contract", autoResultMap = true)
public class EssContract extends BaseEntity<EssContract> {
/**
* 发起合同的应用或业务场景
*/
private String appCode;
/**
* 业务编码
*/
private String bizCode;
/**
* 幂等编码
*/
private String idempotentCode;
/**
* 合同发起方单位id
*/
private Long creatorOuId;
/**
* 合同发起方人员id
*/
private Long creatorPersonId;
/**
* 合同名称
*/
private String contractName;
/**
* 电子签那边的合同id
*/
private String essContractId;
/**
* 腾讯电子签的资源id
*/
@TableField(typeHandler = StringListHandler.class)
private List<String> essFieldIds;
/**
* 合同状态. INIT: 合同创建, PART: 合同签署中, ALL: 合同签署完成, REJECT: 合同拒签, CANCEL: 合同撤回, WILLEXPIRE: 合同即将过期, DEADLINE: 合同流签(合同过期), RELIEVED: 解除协议已解除), INVALID: 合同已失效, EXCEPTION: 合同异常
*/
private EssContractState state;
/**
* 合并签署方信息
*/
@TableField(typeHandler = ApproverListHandler.class)
private List<Approver> approvers;
/**
* 签署信息(扩展字段)
*/
@TableField(typeHandler = FastjsonTypeHandler.class)
private Assignment assignment;
/**
* 签署方的签署情况
*/
@TableField(typeHandler = ApproveDetailListHandler.class)
private List<EssApproveDetail> approveDetails;
/**
* 腾讯电子签那边的消息
*/
private String essMessage;
/**
* 合同oss信息
*/
private String ossFileKey;
/**
* 扩展字段
*/
@TableField(typeHandler = FastjsonTypeHandler.class)
private RecordExt recordExt;
public boolean isUploadedToOss() {
return StringUtils.isNotBlank(ossFileKey);
}
public RecordExt getOrCreateExt() {
if (recordExt == null)
recordExt = new RecordExt();
return recordExt;
}
public Assignment getOrCreateAssignment() {
if (assignment == null)
assignment = new Assignment();
return assignment;
}
public boolean isOrgSigned(Long ouId) {
if (approveDetails == null)
return false;
for (EssApproveDetail detail : approveDetails) {
if (detail.getSignPerson() == null)
continue;
if (ouId.equals(detail.getSignPerson().getOuId()) && detail.getState() == EssContractApproveState.ACCEPT)
return true;
}
return false;
}
public int approverSize() {
return approvers == null ? 0 : approvers.size();
}
public Approver getApprover(Integer idx) {
BizAssertions.assertNotNull(approvers, "approver is null");
return approvers == null ? null : approvers.get(idx);
}
public Approver getApproverOrThrow(String recipientId) {
Approver approver = approvers.stream() //
.filter(a -> a.getEssRecipientId().equals(recipientId)) //
.findFirst() //
.orElse(null);
BizAssertions.assertNotNull(approver, "无效的签署编号: {}", recipientId);
return approver;
}
public EssApproveDetail getApproveDetailOrThrow(String recipientId) {
EssApproveDetail detail = approveDetails.stream() //
.filter(d -> d.getRecipientId().equals(recipientId)) //
.findFirst() //
.orElse(null);
BizAssertions.assertNotNull(detail, "无效的签署编号: {}", recipientId);
return detail;
}
public boolean isSignPersonsPreset() {
if (approvers == null)
return false;
return approvers.stream().allMatch(Approver::isSignPersonPreset);
}
public boolean shouldDownloadContract() {
return isFinalState() || getOrCreateExt().isSaveContractSnapshotArbitrarily();
}
public boolean isFinalState() {
return state.finalState();
}
public Constraint getConstraint() {
return assignment == null ? null : assignment.getConstraint();
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
@Setter
@Getter
public static class RecordExt {
private boolean saveContractSnapshotArbitrarily;
}
// @formatter:off
public static class StringListHandler
extends BaseListTypeHandler<String> {}
public static class ApproveDetailListHandler
extends BaseListTypeHandler<EssApproveDetail> {}
public static class ApproverListHandler
extends BaseListTypeHandler<Approver> {}
// @formatter:on
}

View File

@ -0,0 +1,77 @@
package cn.axzo.nanopart.ess.server.entity;
import org.apache.commons.lang3.StringUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import com.google.common.base.Throwables;
import cn.axzo.nanopart.ess.api.utils.YesOrNo;
import cn.axzo.nanopart.ess.server.http.RequestNoInterceptor;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
@TableName(value = "ess_log", autoResultMap = true)
public class EssLog extends BaseEntity<EssLog> {
/**
* 上下文
*/
private String context;
/**
* 主体
*/
private String subject;
/**
* 腾讯返回的请求id
*/
private String requestId;
/**
* 是否异常
*/
private YesOrNo isError;
/**
* 日志内容
*/
@TableField(typeHandler = FastjsonTypeHandler.class)
private JSONObject logContent;
public void addLogContent(String key, Object value) {
if (logContent == null)
initializeLogContent();
logContent.put(key, value);
}
private void initializeLogContent() {
logContent = new JSONObject();
String requestNo = RequestNoInterceptor.getRequestNo();
if (StringUtils.isNotBlank(requestNo))
logContent.put("requestNo", requestNo);
}
public void setError(Exception exception) {
if (exception == null)
return;
setIsError(YesOrNo.YES);
addLogContent("exception", Throwables.getStackTraceAsString(exception));
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,84 @@
package cn.axzo.nanopart.ess.server.entity;
import cn.axzo.nanopart.ess.api.utils.YesOrNo;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import lombok.Getter;
import lombok.Setter;
import java.util.Set;
/**
* @author yanglin
*/
@Setter
@Getter
@TableName(value = "ess_org", autoResultMap = true)
public class EssOrg extends BaseEntity<EssOrg> {
/**
* 单位id
*/
private Long ouId;
/**
* 单位名称
*/
private String ouName;
/**
* 是否已经认证. YES: 已认证, NO: 未认证
*/
private YesOrNo isAuthorized;
/**
* 超级管理员人员id
*/
private Long superAdminPersonId;
/**
* 单位记录创建人id
*/
private Long createByPersonId;
/**
* 扩展字段
*/
@TableField(typeHandler = FastjsonTypeHandler.class)
private RecordExt recordExt;
public boolean isSuperAdmin(Long personId) {
return superAdminPersonId != null && superAdminPersonId.equals(personId);
}
public RecordExt getOrCreateExt() {
if (recordExt == null)
recordExt = new RecordExt();
return recordExt;
}
public boolean isAuthorized() {
return isAuthorized.isYes();
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
@Setter
@Getter
public static class RecordExt {
private Set<Long> historySuperAdmins;
public boolean isPotentialSuperAdmin(Long personId) {
return historySuperAdmins != null && historySuperAdmins.contains(personId);
}
}
}

View File

@ -0,0 +1,65 @@
package cn.axzo.nanopart.ess.server.entity;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.enums.EssPersonState;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
@TableName(value = "ess_person", autoResultMap = true)
public class EssPerson extends BaseEntity<EssPerson> implements OrgPerson {
/**
* 单位id
*/
private Long ouId;
/**
* 人员id
*/
private Long personId;
/**
* 人员名称
*/
private String personName;
/**
* 是否已经认证
*/
private EssPersonState state;
/**
* 扩展字段
*/
@TableField(typeHandler = FastjsonTypeHandler.class)
public RecordExt recordExt;
public boolean isAuthorized() {
return state == EssPersonState.AUTHORIZED;
}
public boolean is(Long personId) {
return this.personId != null && this.personId.equals(personId);
}
@Setter
@Getter
public static class RecordExt {
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
}

View File

@ -0,0 +1,60 @@
package cn.axzo.nanopart.ess.server.entity;
import cn.axzo.nanopart.ess.api.enums.EssSealState;
import cn.axzo.nanopart.ess.api.enums.EssSealType;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
@TableName(value = "ess_seal", autoResultMap = true)
public class EssSeal extends BaseEntity<EssSeal> {
/**
* 单位id
*/
private Long ouId;
/**
* 印章在腾讯的id
*/
private String essSealId;
/**
* 印章名称
*/
private String name;
/**
* 印章状态. CREATE: 创建, DELETED: 删除, DISABLED: 停用, ENABLED: 启用
*/
private EssSealState state;
/**
* 印章类型. OFFICIAL: 公章, CONTRACT: 合同专用章, FINANCE: 财务专用章, PERSONNEL: 人事专用章, INVOICE: 发票专用章, LEGAL_PERSON_SEAL: 法定代表人章, OTHER: 其它
*/
private EssSealType type;
@TableField(typeHandler = FastjsonTypeHandler.class)
private RecordExt recordExt;
@Override
public String toString() {
return JSON.toJSONString(this);
}
@Setter
@Getter
public static class RecordExt {
}
}

View File

@ -0,0 +1,75 @@
package cn.axzo.nanopart.ess.server.entity;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.utils.YesOrNo;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.extension.handlers.FastjsonTypeHandler;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* @author yanglin
*/
@Setter
@Getter
@TableName(value = "ess_seal_person", autoResultMap = true)
public class EssSealPerson extends BaseEntity<EssSealPerson> implements OrgPerson {
/**
* 单位id
*/
private Long ouId;
/**
* 印章id
*/
private String essSealId;
/**
* 人员id
*/
private Long personId;
/**
* 是否授权. YES: , NO:
*/
private YesOrNo isAuthorized;
/**
* 授权人员id
*/
private Long authorizedByPersonId;
/**
* 授权时间
*/
private Date authorizeTime;
/**
* 扩展字段
*/
@TableField(typeHandler = FastjsonTypeHandler.class)
private RecordExt recordExt;
public boolean isAuthorized() {
return isAuthorized.isYes();
}
@Override
public String toString() {
return JSON.toJSONString(this);
}
@Setter
@Getter
public static class RecordExt {
}
}

View File

@ -0,0 +1,19 @@
package cn.axzo.nanopart.ess.server.entity.domain;
import cn.axzo.nanopart.ess.api.enums.Constraint;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class Assignment {
private Constraint constraint;
private SignPerOrgs signPerOrgs = new SignPerOrgs();
}

View File

@ -0,0 +1,41 @@
package cn.axzo.nanopart.ess.server.entity.domain;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.Objects;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class OuAndPersonId {
private final Long ouId;
private final Long personId;
public static OuAndPersonId create(Long ouId, Long personId) {
return new OuAndPersonId(ouId, personId);
}
public boolean is(Long ouId, Long personId) {
return this.ouId.equals(ouId) && this.personId.equals(personId);
}
@Override
public boolean equals(Object o) {
if (!(o instanceof OuAndPersonId))
return false;
OuAndPersonId that = (OuAndPersonId) o;
return Objects.equals(ouId, that.ouId) && Objects.equals(personId, that.personId);
}
@Override
public int hashCode() {
return Objects.hash(ouId, personId);
}
}

View File

@ -0,0 +1,15 @@
package cn.axzo.nanopart.ess.server.entity.domain;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter @Getter
public class SignPerOrg implements OrgPerson {
private Long ouId;
private Long personId;
private String recipientId;
}

View File

@ -0,0 +1,44 @@
package cn.axzo.nanopart.ess.server.entity.domain;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import lombok.Getter;
import lombok.Setter;
/**
* @author yanglin
*/
@Setter
@Getter
public class SignPerOrgs {
private List<SignPerOrg> signPerOrg = new ArrayList<>();
public Optional<SignPerOrg> find(Long ouId) {
return signPerOrg.stream() //
.filter(orgPersonInfo -> orgPersonInfo.getOuId().equals(ouId)) //
.findFirst();
}
public void upsert(OrgPerson person, String recipientId) {
remove(person.getOuId());
add(person, recipientId);
}
public void remove(Long ouId) {
signPerOrg.removeIf(org -> org.getOuId().equals(ouId));
}
public void add(OrgPerson person, String recipientId) {
SignPerOrg orgPersonInfo = new SignPerOrg();
orgPersonInfo.setOuId(person.getOuId());
orgPersonInfo.setPersonId(person.getPersonId());
orgPersonInfo.setRecipientId(recipientId);
signPerOrg.add(orgPersonInfo);
}
}

View File

@ -0,0 +1,114 @@
package cn.axzo.nanopart.ess.server.ess;
import static cn.axzo.nanopart.ess.server.utils.BizAssertions.fail;
import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.List;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.essbasic.v20210526.models.ApproverItem;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateFlowByFilesResponse;
import cn.axzo.nanopart.ess.api.domain.contract.EssApproveDetail;
import cn.axzo.nanopart.ess.api.enums.EssContractState;
import cn.axzo.nanopart.ess.api.request.CreateContractByFileRequest;
import cn.axzo.nanopart.ess.api.request.RevokeContractRequest;
import cn.axzo.nanopart.ess.api.response.CreateContractByFileResponse;
import cn.axzo.nanopart.ess.server.dao.EssContractDao;
import cn.axzo.nanopart.ess.server.dao.EssLogDao;
import cn.axzo.nanopart.ess.server.entity.EssContract;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.ess.domain.StringObject;
import cn.axzo.nanopart.ess.server.ess.mq.EssBroadcaster;
import cn.axzo.nanopart.ess.server.ess.support.ContractSupport;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import cn.axzo.nanopart.ess.server.utils.BizTransactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ContractManager {
public static final String CONTRACT_ID = "contractId";
private final OrgManager orgManager;
private final EssClient essClient;
private final EssContractDao essContractDao;
private final EssBroadcaster essBroadcaster;
private final ContractSupport contractSupport;
private final EssLogDao essLogDao;
@BizTransactional
public CreateContractByFileResponse createContractByFile(CreateContractByFileRequest request) {
orgManager.ensureOrgAuthorized(request.getCreator().getOuId(), "合同创建单位还未认证电子签");
contractSupport.validateCreateContract(request.getContract());
essLogDao.logRequest("createContractByFile", request.getBizCode(), request);
EssContract contract;
try {
contract = contractSupport.saveContractByFile(request);
}
catch (DuplicateKeyException e) {
return contractSupport.createDuplicateContractByFileResponse(request);
}
try {
EssPerson superAdmin = orgManager.getSuperAdminOrThrow(request.getCreator().getOuId());
List<String> essFileIds = essClient.uploadDocument(superAdmin, request.getByFile().getBase64Files());
ChannelCreateFlowByFilesResponse essResponse = essClient.createContractByFile(superAdmin, essFileIds,
request.getByFile(), StringObject.create().put(CONTRACT_ID, contract.getId()).toString());
List<String> essRecipientIds = Arrays.stream(essResponse.getApprovers()) //
.map(ApproverItem::getRecipientId) //
.collect(toList());
for (int i = 0; i < contract.approverSize(); i++)
contract.getApprover(i).setEssRecipientId(essRecipientIds.get(i));
essContractDao.setEssContractCreated(contract.getId(), essResponse.getFlowId(), essFileIds,
contract.getApprovers());
essBroadcaster.fireContractStateChanged(contract);
CreateContractByFileResponse response = new CreateContractByFileResponse();
response.setEssContractId(essResponse.getFlowId());
response.setEssRecipientIds(essRecipientIds);
return response;
}
catch (TencentCloudSDKException e) {
throw fail(e, "腾讯返回: {}", e.getMessage());
}
}
@BizTransactional
public void revokeContract(RevokeContractRequest request) {
essLogDao.logRequest("revokeContract", request.getEssContractId(), request);
// lock when updating state
EssContract contract = essContractDao.getForUpdateOrThrow(request.getEssContractId());
BizAssertions.assertFalse(contract.isFinalState(), "合同已是终态 {}, 无法撤销", contract.getState().description());
EssPerson superAdmin = getContractSuperAdmin(contract);
essClient.revokeContract(superAdmin, contract.getEssContractId(), request.getReason());
updateContractState(contract, EssContractState.CANCEL, null, request.getReason());
}
@BizTransactional
public void updateContractState(EssContract contract, EssContractState state, List<EssApproveDetail> approveDetails,
String essMessage) {
EssContract reload = essContractDao.getForUpdateOrThrow(contract.getEssContractId());
if (reload.isFinalState()) {
log.warn("合同[{}]已是最终状态[{}], 无法更新状态至{}", reload.getEssContractId(), reload.getState(), state.description());
}
else {
essContractDao.updateState(contract, state, approveDetails, essMessage);
essBroadcaster.fireContractStateChanged(contract);
}
}
public EssPerson getContractSuperAdmin(EssContract contract) {
return orgManager.getSuperAdminOrThrow(contract.getCreatorOuId());
}
}

View File

@ -0,0 +1,570 @@
package cn.axzo.nanopart.ess.server.ess;
import static cn.axzo.nanopart.ess.server.ess.EssClient.Func.func;
import static cn.axzo.nanopart.ess.server.utils.BizAssertions.fail;
import static cn.axzo.nanopart.ess.server.utils.IdBuilder.idbuilder;
import static java.util.stream.Collectors.toList;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.Nullable;
import javax.annotation.Resource;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.util.ReflectionUtils;
import com.google.common.base.Throwables;
import com.tencentcloudapi.common.Credential;
import com.tencentcloudapi.common.exception.TencentCloudSDKException;
import com.tencentcloudapi.common.profile.ClientProfile;
import com.tencentcloudapi.common.profile.HttpProfile;
import com.tencentcloudapi.essbasic.v20210526.EssbasicClient;
import com.tencentcloudapi.essbasic.v20210526.models.Agent;
import com.tencentcloudapi.essbasic.v20210526.models.ApproverOption;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCancelFlowRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateEmbedWebUrlRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateEmbedWebUrlResponse;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateFlowApproversRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateFlowByFilesRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateFlowByFilesResponse;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateFlowSignUrlRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateFlowSignUrlResponse;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateOrganizationBatchSignUrlRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateOrganizationBatchSignUrlResponse;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelCreateSealPolicyRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelDeleteSealPoliciesRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelDescribeEmployeesRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelDescribeEmployeesResponse;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelDescribeOrganizationSealsRequest;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelDescribeOrganizationSealsResponse;
import com.tencentcloudapi.essbasic.v20210526.models.ComponentLimit;
import com.tencentcloudapi.essbasic.v20210526.models.CreateConsoleLoginUrlRequest;
import com.tencentcloudapi.essbasic.v20210526.models.CreateConsoleLoginUrlResponse;
import com.tencentcloudapi.essbasic.v20210526.models.CreateFlowForwardsRequest;
import com.tencentcloudapi.essbasic.v20210526.models.CreateSignUrlsRequest;
import com.tencentcloudapi.essbasic.v20210526.models.CreateSignUrlsResponse;
import com.tencentcloudapi.essbasic.v20210526.models.DescribeResourceUrlsByFlowsRequest;
import com.tencentcloudapi.essbasic.v20210526.models.DescribeResourceUrlsByFlowsResponse;
import com.tencentcloudapi.essbasic.v20210526.models.FillApproverInfo;
import com.tencentcloudapi.essbasic.v20210526.models.FlowApproverInfo;
import com.tencentcloudapi.essbasic.v20210526.models.FlowForwardInfo;
import com.tencentcloudapi.essbasic.v20210526.models.OccupiedSeal;
import com.tencentcloudapi.essbasic.v20210526.models.ProxyOrganizationOperator;
import com.tencentcloudapi.essbasic.v20210526.models.SyncProxyOrganizationOperatorsRequest;
import com.tencentcloudapi.essbasic.v20210526.models.UploadFile;
import com.tencentcloudapi.essbasic.v20210526.models.UploadFilesRequest;
import com.tencentcloudapi.essbasic.v20210526.models.UploadFilesResponse;
import com.tencentcloudapi.essbasic.v20210526.models.UserInfo;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import cn.axzo.nanopart.ess.api.domain.CreateContractInfo;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.domain.contract.Approver;
import cn.axzo.nanopart.ess.api.enums.EssConsoleUrlEndpoint;
import cn.axzo.nanopart.ess.api.enums.EssEmbedType;
import cn.axzo.nanopart.ess.api.enums.EssSealType;
import cn.axzo.nanopart.ess.server.dao.EssLogDao;
import cn.axzo.nanopart.ess.server.entity.EssLog;
import cn.axzo.nanopart.ess.server.entity.EssOrg;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.entity.EssSeal;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import cn.axzo.nanopart.ess.server.ess.EssClient.Func.FuncBuilder;
import cn.axzo.nanopart.ess.server.ess.domain.OrgOpenId;
import cn.axzo.nanopart.ess.server.ess.domain.PersonOpenId;
import cn.axzo.nanopart.ess.server.ess.support.EssProps;
import cn.axzo.nanopart.ess.server.ess.support.EssSupport;
import cn.axzo.nanopart.ess.server.ess.support.OrgProfiles;
import cn.axzo.nanopart.ess.server.ess.support.PersonProfiles;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class EssClient implements InitializingBean {
private static final String NEW_TRANSACTION = "essLogTransactionTemplate";
private static final Pattern ESS_ERROR_REQUEST_ID_PATTERN = Pattern.compile("RequestId:([a-f0-9\\-]+)");
private final EssProps props;
private final EssLogDao essLogDao;
private final EssSupport essSupport;
@Resource(name = NEW_TRANSACTION)
private TransactionTemplate newTransaction;
private EssbasicClient manage;
private EssbasicClient file;
public String createConsoleLoginUrl(EssOrg org, EssPerson person, EssConsoleUrlEndpoint endpoint,
boolean essCheckLoginPerson) {
PersonProfileDto personProfile = essSupport.getPersonProfileOrThrow(person);
CreateConsoleLoginUrlRequest request = new CreateConsoleLoginUrlRequest();
request.setEndpoint(endpoint.getCode());
request.setEndpoint(request.getEndpoint());
request.setAgent(agent(person));
String orgName = org.getOuName() + (props.isTestEnv() ? "测试" : "");
if (props.isTestEnv())
orgName = orgName.replaceAll("中国", "zhongguo");
request.setProxyOrganizationName(orgName);
if (essCheckLoginPerson) {
request.setProxyOperatorName(personProfile.getRealName());
request.setProxyOperatorMobile(personProfile.getPhone());
}
CreateConsoleLoginUrlResponse response = exec(func() //
.context("CreateConsoleLoginUrl") //
.subject(idbuilder() //
.append(person.getOuId()) //
.append(person.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.CreateConsoleLoginUrl(request)));
return response.getConsoleUrl();
}
public String getEmbedWebUrl(EssPerson contextPerson, EssEmbedType embedType, String businessId) {
if (embedType.businessIdRequired())
BizAssertions.assertNotBlank(businessId, "内嵌类型为{}时业务ID不能为空", embedType.name());
ChannelCreateEmbedWebUrlRequest request = new ChannelCreateEmbedWebUrlRequest();
request.setAgent(agent(contextPerson));
request.setBusinessId(businessId);
request.setEmbedType(embedType.essCode());
request.setHiddenComponents(true);
ChannelCreateEmbedWebUrlResponse response = exec(func() //
.context("ChannelCreateEmbedWebUrl") //
.subject(idbuilder() //
.append(embedType.name()) //
.append(StringUtils.isBlank(businessId) ? contextPerson.getOuId() : businessId) //
.build()) //
.request(request) //
.command(() -> manage.ChannelCreateEmbedWebUrl(request)));
return response.getWebUrl();
}
@Nullable
public OccupiedSeal getSealInfo(EssPerson superAdmin, String sealId) {
ChannelDescribeOrganizationSealsRequest request = new ChannelDescribeOrganizationSealsRequest();
request.setAgent(agent(superAdmin));
request.setSealId(sealId);
request.setSealStatuses(new String[] { "ALL" });
request.setLimit(99L);
ChannelDescribeOrganizationSealsResponse response = exec(func() //
.context("ChannelDescribeOrganizationSeals") //
.subject(sealId) //
.request(request) //
.command(() -> manage.ChannelDescribeOrganizationSeals(request)));
OccupiedSeal[] seals = response.getSeals();
if (seals == null || seals.length == 0)
return null;
return seals[0];
}
public void addSealAuthorization(EssPerson superAdmin, EssSeal seal, EssSealPerson sealPerson) {
ChannelCreateSealPolicyRequest request = new ChannelCreateSealPolicyRequest();
request.setAgent(agent(superAdmin));
request.setSealId(seal.getEssSealId());
request.setUserIds(new String[] { PersonOpenId.create(sealPerson).toOpenId() });
exec(func() //
.context("ChannelCreateSealPolicy") //
.subject(idbuilder() //
.append(seal.getEssSealId()) //
.append(sealPerson.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.ChannelCreateSealPolicy(request)));
}
public void removeSealAuthorization(EssPerson superAdmin, EssSeal seal, EssSealPerson sealPerson) {
ChannelDeleteSealPoliciesRequest request = new ChannelDeleteSealPoliciesRequest();
request.setAgent(agent(superAdmin));
request.setSealId(seal.getEssSealId());
request.setUserIds(new String[] { PersonOpenId.create(sealPerson).toOpenId() });
exec(func() //
.context("ChannelDeleteSealPolicies") //
.subject(idbuilder() //
.append(seal.getEssSealId()) //
.append(sealPerson.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.ChannelDeleteSealPolicies(request)));
}
List<String> uploadDocument(EssPerson superAdmin, List<String> base64Files) throws TencentCloudSDKException {
return uploadFile(superAdmin, UploadFileBusinessType.DOCUMENT, base64Files);
}
List<String> uploadTemplate(EssPerson superAdmin, List<String> base64Files) throws TencentCloudSDKException {
return uploadFile(superAdmin, UploadFileBusinessType.TEMPLATE, base64Files);
}
private List<String> uploadFile(EssPerson superAdmin, UploadFileBusinessType businessType, List<String> base64Files)
throws TencentCloudSDKException {
BizAssertions.assertFalse(base64Files.isEmpty(), "文件不能为空");
ArrayList<UploadFile> files = new ArrayList<>();
for (String base64File : base64Files) {
UploadFile file = new UploadFile();
file.setFileBody(base64File);
files.add(file);
}
UploadFilesRequest request = new UploadFilesRequest();
request.setAgent(agent(superAdmin));
request.setFileInfos(files.toArray(new UploadFile[0]));
request.setBusinessType(businessType.name());
UploadFilesResponse response = call(Func.<UploadFilesResponse> func() //
.context("UploadFiles") //
.subject("") //
.subject2(r -> r.getFileIds()[0]) //
.request(request) //
.command(() -> file.UploadFiles(request)));
return Arrays.asList(response.getFileIds());
}
ChannelCreateFlowByFilesResponse createContractByFile(EssPerson superAdmin, List<String> contractFileIds,
CreateContractInfo contract, String customerData) throws TencentCloudSDKException {
OrgProfiles orgProfiles = OrgProfiles.wrap(essSupport.getOrgProfiles( //
contract.getApprovers().stream() //
.filter(Approver::isSignPersonPreset) //
.map(Approver::getOuId) //
.collect(toList())));
PersonProfiles personProfiles = PersonProfiles.wrap(essSupport.getPersonProfiles( //
contract.getApprovers().stream() //
.filter(Approver::isSignPersonPreset) //
.map(Approver::getPersonId) //
.collect(toList())));
ArrayList<FlowApproverInfo> approvers = new ArrayList<>();
for (Approver approver : contract.getApprovers()) {
FlowApproverInfo approverInfo = new FlowApproverInfo();
approvers.add(approverInfo);
ApproverOption approverOption = new ApproverOption();
if (approver.getSignPerson() == null) {
approverOption.setFillType(1L);
}
else {
OrganizationalUnitVO orgProfile = orgProfiles.getOrThrow(approver.getOuId());
PersonProfileDto personProfile = personProfiles.getOrThrow(approver.getPersonId());
approverInfo.setName(personProfile.getRealName());
approverInfo.setMobile(personProfile.getPhone());
approverInfo.setOpenId(PersonOpenId.create(approver).toOpenId());
approverInfo.setOrganizationName(orgProfile.getName());
approverInfo.setOrganizationOpenId(OrgOpenId.ofPerson(approver).toOpenId());
}
if (approver.getSignOption() != null) {
approverOption.setNoRefuse(approver.getSignOption().isNoRefuse());
approverOption.setNoTransfer(approver.getSignOption().isNoTransfer());
}
approverInfo.setApproverOption(approverOption);
approverInfo.setApproverType(approver.getApproverType().name());
approverInfo.setNotifyType("NONE");
approverInfo.setPreReadTime(approver.getPreReadTimeSeconds());
if (CollectionUtils.isNotEmpty(approver.getSealTypes())) {
ComponentLimit componentLimit = new ComponentLimit();
componentLimit.setComponentType("SIGN_SEAL");
componentLimit.setComponentValue( //
approver.getSealTypes().stream() //
.map(EssSealType::getUseForLimitCode) //
.distinct() //
.toArray(String[]::new));
approverInfo.setAddSignComponentsLimits(new ComponentLimit[] { componentLimit });
}
}
ChannelCreateFlowByFilesRequest request = new ChannelCreateFlowByFilesRequest();
request.setAgent(agent(superAdmin));
request.setSignBeanTag(contract.getSignBeanTag());
request.setFlowName(contract.getContractName());
request.setFileIds(contractFileIds.toArray(new String[0]));
request.setFlowApprovers(approvers.toArray(new FlowApproverInfo[0]));
request.setUnordered(!contract.isSignOrdered());
request.setCustomerData(customerData);
if (contract.getDeadlineMs() != null)
request.setDeadline(contract.getDeadlineMs() / 1000);
return call(Func.<ChannelCreateFlowByFilesResponse> func() //
.context("ChannelCreateFlowByFiles") //
.subject(contract.getContractName()) //
.subject2(ChannelCreateFlowByFilesResponse::getFlowId) //
.request(request) //
.command(() -> manage.ChannelCreateFlowByFiles(request)));
}
void revokeContract(EssPerson superAdmin, String essContractId, String reason) {
ChannelCancelFlowRequest request = new ChannelCancelFlowRequest();
request.setAgent(agent(superAdmin));
request.setFlowId(essContractId);
request.setCancelMessage(reason);
exec(func() //
.context("ChannelCancelFlow") //
.subject(essContractId) //
.request(request) //
.command(() -> manage.ChannelCancelFlow(request)));
}
public String getContractPDFUrl(EssPerson superAdmin, String essContractId) {
DescribeResourceUrlsByFlowsRequest request = new DescribeResourceUrlsByFlowsRequest();
request.setAgent(agent(superAdmin));
request.setFlowIds(new String[] { essContractId });
DescribeResourceUrlsByFlowsResponse response = exec(func() //
.context("DescribeResourceUrlsByFlows") //
.subject(essContractId) //
.request(request) //
.command(() -> manage.DescribeResourceUrlsByFlows(request)));
return response.getFlowResourceUrlInfos()[0].getResourceUrlInfos()[0].getUrl();
}
void createFlowApprovers(EssPerson superAdmin, String essContractId, String recipientId, EssPerson signPerson) {
OrganizationalUnitVO org = essSupport.getOrgProfileOrThrow(signPerson.getOuId());
FillApproverInfo approver = new FillApproverInfo();
approver.setRecipientId(recipientId);
approver.setOpenId(PersonOpenId.create(signPerson).toOpenId());
approver.setOrganizationOpenId(OrgOpenId.ofPerson(signPerson).toOpenId());
approver.setOrganizationName(org.getName());
ChannelCreateFlowApproversRequest request = new ChannelCreateFlowApproversRequest();
request.setAgent(agent(superAdmin));
request.setApprovers(new FillApproverInfo[] { approver });
request.setFlowId(essContractId);
request.setFillApproverType(1L);
exec(func() //
.context("ChannelCreateFlowApprovers") //
.subject(idbuilder() //
.append(essContractId) //
.append(signPerson.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.ChannelCreateFlowApprovers(request)));
}
public String createPcSignUrl(EssPerson superAdmin, String essContractId, EssPerson signPerson) {
ChannelCreateOrganizationBatchSignUrlRequest request = new ChannelCreateOrganizationBatchSignUrlRequest();
request.setAgent(agent(signPerson));
request.setFlowIds(new String[] { essContractId });
request.setOpenId(PersonOpenId.create(signPerson).toOpenId());
ChannelCreateOrganizationBatchSignUrlResponse response = exec(func() //
.context("ChannelCreateOrganizationBatchSignUrl").subject(idbuilder() //
.append(essContractId) //
.append(signPerson.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.ChannelCreateOrganizationBatchSignUrl(request)));
return response.getSignUrl();
}
public String createSignUrls(EssPerson superAdmin, String essContractId, String recipientId, OrgPerson signPerson) {
CreateSignUrlsRequest request = new CreateSignUrlsRequest();
request.setAgent(agent(superAdmin));
request.setFlowIds(new String[] { essContractId });
request.setOpenId(PersonOpenId.create(signPerson).toOpenId());
request.setOrganizationOpenId(OrgOpenId.ofPerson(signPerson).toOpenId());
request.setAutoJumpBack(true);
if (StringUtils.isNotBlank(recipientId)) {
request.setRecipientIds(new String[] { recipientId });
request.setGenerateType("RECIPIENT");
}
CreateSignUrlsResponse response = exec(func() //
.context("CreateSignUrls") //
.subject(idbuilder() //
.append(essContractId) //
.append(signPerson.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.CreateSignUrls(request)));
return response.getSignUrlInfos()[0].getSignUrl();
}
public String channelCreateFlowSignUrl(EssPerson superAdmin, String essContractId, EssPerson signPerson) {
FlowApproverInfo approver = new FlowApproverInfo();
approver.setOpenId(PersonOpenId.create(signPerson).toOpenId());
approver.setOrganizationOpenId(OrgOpenId.ofPerson(signPerson).toOpenId());
ChannelCreateFlowSignUrlRequest request = new ChannelCreateFlowSignUrlRequest();
request.setAgent(agent(superAdmin));
request.setFlowId(essContractId);
request.setFlowApproverInfos(new FlowApproverInfo[] { approver });
ChannelCreateFlowSignUrlResponse response = exec(func() //
.context("ChannelCreateFlowSignUrl") //
.subject(idbuilder() //
.append(essContractId) //
.append(signPerson.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.ChannelCreateFlowSignUrl(request)));
return response.getFlowApproverUrlInfos()[0].getSignUrl();
}
public void forward(EssPerson superAdmin, String essContractId, String recipientId, EssPerson signPerson) {
CreateFlowForwardsRequest request = new CreateFlowForwardsRequest();
request.setAgent(agent(superAdmin));
request.setTargetOpenId(PersonOpenId.create(signPerson).toOpenId());
FlowForwardInfo forward = new FlowForwardInfo();
forward.setFlowId(essContractId);
forward.setRecipientId(recipientId);
request.setFlowForwardInfos(new FlowForwardInfo[] { forward });
exec(func() //
.context("CreateFlowForwards") //
.subject(idbuilder() //
.append(essContractId) //
.append(signPerson.getPersonId()) //
.build()) //
.request(request) //
.command(() -> manage.CreateFlowForwards(request)));
}
public void setEmployeeResigned(EssPerson superAdmin, EssPerson person) {
ProxyOrganizationOperator operator = new ProxyOrganizationOperator();
operator.setId(PersonOpenId.create(person).toOpenId());
operator.setName(person.getPersonName());
SyncProxyOrganizationOperatorsRequest request = new SyncProxyOrganizationOperatorsRequest();
request.setAgent(agent(superAdmin));
request.setOperatorType("RESIGN");
request.setProxyOrganizationOperators(new ProxyOrganizationOperator[] { operator });
exec(func() //
.context("SyncProxyOrganizationOperators") //
.subject(idbuilder() //
.append(person.getOuId()) //
.append(person.getPersonId()) //
.append(request.getOperatorType()) //
.build()) //
.request(request) //
.command(() -> manage.SyncProxyOrganizationOperators(request)));
}
public ChannelDescribeEmployeesResponse getOrgPerson(EssPerson superAdmin, EssOrg org, Long offset) {
ChannelDescribeEmployeesRequest request = new ChannelDescribeEmployeesRequest();
request.setAgent(agent(superAdmin));
request.setOffset(offset);
request.setLimit(20L);
return exec(func() //
.context("ChannelDescribeEmployees") //
.subject(org.getOuId() + "") //
.request(request) //
.command(() -> manage.ChannelDescribeEmployees(request)));
}
private Agent agent(EssPerson person) {
Agent agent = new Agent();
UserInfo userInfo = new UserInfo();
agent.setAppId(props.getAppId());
agent.setProxyAppId("");
agent.setProxyOrganizationOpenId(OrgOpenId.ofPerson(person).toOpenId());
userInfo.setOpenId(PersonOpenId.create(person).toOpenId());
agent.setProxyOperator(userInfo);
return agent;
}
@Override
public void afterPropertiesSet() {
manage = createEssbasicClient(props.getApiEndPoint());
file = createEssbasicClient(props.getFileEndPoint());
}
private EssbasicClient createEssbasicClient(String apiEndPoint) {
Credential cred = new Credential(props.getSecretId(), props.getSecretKey());
HttpProfile httpProfile = new HttpProfile();
httpProfile.setEndpoint(apiEndPoint);
ClientProfile clientProfile = new ClientProfile();
clientProfile.setHttpProfile(httpProfile);
return new EssbasicClient(cred, "", clientProfile);
}
private <T> T exec(FuncBuilder<?> builder) {
try {
return call(builder);
}
catch (TencentCloudSDKException e) {
throw fail(e, "腾讯返回: {}", e.getMessage());
}
}
private <T> T call(FuncBuilder<?> builder) throws TencentCloudSDKException {
//noinspection unchecked
Func<Object> func = (Func<Object>) builder.build();
EssLog essLog = new EssLog();
essLog.setCreateAt(new Date());
essLog.setUpdateAt(new Date());
essLog.setContext(String.format("ess:api:%s", func.context));
essLog.addLogContent("essRequest", func.request);
essLog.setSubject(func.subject == null ? "" : func.subject);
T response = null;
TencentCloudSDKException exception = null;
try {
//noinspection unchecked
response = (T) func.command.exec();
essLog.addLogContent("essResponse", response);
if (func.subject2 != null) {
String newSubject = func.subject2.apply(response);
if (newSubject != null)
essLog.setSubject(newSubject);
}
if (response != null) {
Method method = ReflectionUtils.findMethod(response.getClass(), "getRequestId");
if (method != null) {
method.setAccessible(true);
String requestId = (String) ReflectionUtils.invokeMethod(method, response);
essLog.setRequestId(requestId);
}
}
}
catch (TencentCloudSDKException e) {
log.warn("腾讯云接口调用失败", e);
exception = e;
essLog.setError(e);
Matcher matcher = ESS_ERROR_REQUEST_ID_PATTERN.matcher(Throwables.getStackTraceAsString(e));
if (matcher.find()) {
String requestId = matcher.group(1);
essLog.setRequestId(requestId);
}
}
finally {
newTransaction.executeWithoutResult(unused -> essLogDao.save(essLog));
}
if (exception != null)
throw exception;
return response;
}
public enum UploadFileBusinessType {
DOCUMENT, TEMPLATE
}
@lombok.Builder
public static class Func<T> {
final String context;
final String subject;
final Function<T, String> subject2;
final Object request;
final TencentCloudFunc<T> command;
public static <T> FuncBuilder<T> func() {
return Func.builder();
}
}
private interface TencentCloudFunc<T> {
T exec() throws TencentCloudSDKException;
}
@Bean(NEW_TRANSACTION)
public TransactionTemplate transactionTemplate(PlatformTransactionManager transactionManager) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
return transactionTemplate;
}
}

View File

@ -0,0 +1,235 @@
package cn.axzo.nanopart.ess.server.ess;
import static cn.axzo.nanopart.ess.server.utils.IdBuilder.idbuilder;
import org.springframework.stereotype.Service;
import cn.axzo.nanopart.ess.api.domain.contract.Approver;
import cn.axzo.nanopart.ess.api.enums.Constraint;
import cn.axzo.nanopart.ess.api.enums.EssEmbedType.EssSubject;
import cn.axzo.nanopart.ess.api.enums.SignUrlEndpoint;
import cn.axzo.nanopart.ess.api.request.AddSealAuthorizationRequest;
import cn.axzo.nanopart.ess.api.request.AssignSignUrlRequest;
import cn.axzo.nanopart.ess.api.request.CreateConsoleLoginUrlRequest;
import cn.axzo.nanopart.ess.api.request.GetEmbedWebUrlRequest;
import cn.axzo.nanopart.ess.api.request.RemoveSealAuthorizationRequest;
import cn.axzo.nanopart.ess.api.request.SaveContractSnapshotRequest;
import cn.axzo.nanopart.ess.api.request.SealAndPersonRequest;
import cn.axzo.nanopart.ess.server.dao.EssContractDao;
import cn.axzo.nanopart.ess.server.dao.EssLogDao;
import cn.axzo.nanopart.ess.server.dao.EssPersonDao;
import cn.axzo.nanopart.ess.server.dao.EssSealDao;
import cn.axzo.nanopart.ess.server.dao.EssSealPersonDao;
import cn.axzo.nanopart.ess.server.entity.EssContract;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.entity.EssSeal;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import cn.axzo.nanopart.ess.server.ess.domain.OrgAndPerson;
import cn.axzo.nanopart.ess.server.ess.domain.SealAndPerson;
import cn.axzo.nanopart.ess.server.ess.mq.EssBroadcaster;
import cn.axzo.nanopart.ess.server.ess.support.OssService;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import cn.axzo.nanopart.ess.server.utils.BizTransactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class EssService {
private final OrgManager orgManager;
private final ContractManager contractManager;
private final EssClient essClient;
private final EssSealDao essSealDao;
private final EssSealPersonDao essSealPersonDao;
private final EssContractDao essContractDao;
private final EssPersonDao essPersonDao;
private final OssService ossService;
private final EssLogDao essLogDao;
private final EssBroadcaster essBroadcaster;
@BizTransactional
public String createConsoleLoginUrl(CreateConsoleLoginUrlRequest request) {
String subject = idbuilder() //
.append(request.getOuId()) //
.append(request.getPersonId()) //
.build();
essLogDao.logRequest("createConsoleLoginUrl", subject, request);
OrgAndPerson orgAndPerson = orgManager.createConsoleLoginUrl(request);
return essClient.createConsoleLoginUrl(orgAndPerson.getOrg(), orgAndPerson.getPerson(), request.getEndpoint(),
request.isEssCheckLoginPerson());
}
public String getEmbedWebUrl(GetEmbedWebUrlRequest request) {
EssPerson contextPerson;
if (request.getEmbedType().essSubject() == EssSubject.CONTRACT) {
BizAssertions.assertNotBlank(request.getBusinessId(), "合同ID不能为空");
EssContract contract = essContractDao.getOrThrow(request.getBusinessId());
contextPerson = contractManager.getContractSuperAdmin(contract);
}
else {
contextPerson = orgManager.getSuperAdminOrThrow(request.getOuId());
}
return essClient.getEmbedWebUrl(contextPerson, request.getEmbedType(), request.getBusinessId());
}
@BizTransactional
public void essAddSealAuthorization(AddSealAuthorizationRequest request) {
String subject = idbuilder() //
.append(request.getEssSealId()) //
.append(request.getPersonId()) //
.build();
essLogDao.logRequest("essAddSealAuthorization", subject, request);
SealAndPerson sealAndPerson = getSealAndPersonOrThrow(request);
EssSealPerson sealPerson = sealAndPerson.getSealPerson();
if (sealPerson.isAuthorized())
return;
EssPerson essPerson = essPersonDao.findOrNull(sealPerson);
BizAssertions.assertNotNull(essPerson, "人员不存在: {}", sealPerson.getPersonId());
BizAssertions.assertTrue(essPerson.isAuthorized(), "人员未加入单位, 无法授权");
EssPerson superAdmin = orgManager.getSuperAdminOrThrow(sealAndPerson.getSeal().getOuId());
essClient.addSealAuthorization(superAdmin, sealAndPerson.getSeal(), sealPerson);
orgManager.maybeAddPersonAndSetSealAuthorized(request.getEssSealId(), request.getPersonId());
Long operatorPersonId = request.getOperatorPersonId();
if (operatorPersonId != null && operatorPersonId > 0)
orgManager.setSealPersonAuthorizedBy(request.getEssSealId(), request.getPersonId(), operatorPersonId);
}
@BizTransactional
public void essRemoveSealAuthorization(RemoveSealAuthorizationRequest request) {
String subject = idbuilder() //
.append(request.getEssSealId()) //
.append(request.getPersonId()) //
.build();
essLogDao.logRequest("essRemoveSealAuthorization", subject, request);
SealAndPerson sealAndPerson = getSealAndPersonOrThrow(request);
if (!sealAndPerson.getSealPerson().isAuthorized())
return;
EssPerson superAdmin = orgManager.getSuperAdminOrThrow(sealAndPerson.getSeal().getOuId());
BizAssertions.assertFalse(superAdmin.is(request.getPersonId()), "不能取消超级管理员的印章授权");
essClient.removeSealAuthorization(superAdmin, sealAndPerson.getSeal(), sealAndPerson.getSealPerson());
orgManager.removeSealAuthorization(request.getEssSealId(), request.getPersonId());
}
private SealAndPerson getSealAndPersonOrThrow(SealAndPersonRequest request) {
EssSeal seal = essSealDao.findByEssSealId(request.getEssSealId()).orElse(null);
BizAssertions.assertNotNull(seal, "印章不存在: {}", request.getEssSealId());
EssSealPerson sealPerson = essSealPersonDao.find(request.getEssSealId(), request.getPersonId()).orElse(null);
BizAssertions.assertNotNull(sealPerson, "印章人员不存在: {}", request.getEssSealId(), request.getPersonId());
return new SealAndPerson(seal, sealPerson);
}
@BizTransactional
public String assignSignUrl(AssignSignUrlRequest request) {
String subject = idbuilder() //
.append(request.getEssContractId()) //
.append(request.getOuId()) //
.append(request.getPersonId()) //
.build();
essLogDao.logRequest("assignSignUrl", subject, request);
EssPerson signPerson = essPersonDao.findOrNull(request);
BizAssertions.assertNotNull(signPerson, "当前签署人员未加入单位, 无法签署");
EssContract contract = essContractDao.getForUpdateOrThrow(request.getEssContractId());
BizAssertions.assertFalse(contract.isFinalState(), "合同已是最终状态 {}, 无法签署", contract.getState());
Approver approver = contract.getApproverOrThrow(request.getRecipientId());
BizAssertions.assertFalse(approver.isSignPersonPreset(), "非动态签署人不能使用该接口签署");
if (contract.getConstraint() == Constraint.ONE_PERSON_PER_ORG) {
BizAssertions.assertFalse(contract.isOrgSigned(request.getOuId()), "该单位已签署, 无法再次签署");
contract.getOrCreateAssignment().getSignPerOrgs().find(request.getOuId()).ifPresent(org -> {
BizAssertions.assertEquals(org.getRecipientId(), request.getRecipientId(), "单位的签署编号不能变化");
if (!org.getPersonId().equals(request.getPersonId())) {
EssPerson signSuperAdmin = orgManager.getSuperAdminOrThrow(signPerson.getOuId());
try {
essClient.forward(signSuperAdmin, contract.getEssContractId(), request.getRecipientId(),
signPerson);
} catch (Exception e) {
essLogDao.log(e, "assignSignUrl", subject);
log.warn("转发签署人失败", e);
}
}
});
contract.getOrCreateAssignment().getSignPerOrgs().upsert(signPerson, request.getRecipientId());
essContractDao.updateAssigment(contract);
}
EssPerson superAdmin = contractManager.getContractSuperAdmin(contract);
if (request.getEndpoint() == SignUrlEndpoint.PC) {
essClient.createFlowApprovers(superAdmin, contract.getEssContractId(), request.getRecipientId(),
signPerson);
return essClient.createPcSignUrl(superAdmin, contract.getEssContractId(), signPerson);
}
else if (request.getEndpoint() == SignUrlEndpoint.WEIXIN_APP)
return essClient.createSignUrls(superAdmin, contract.getEssContractId(), request.getRecipientId(),
signPerson);
else
throw BizAssertions.fail("不支持的签署端点: {}", request.getEndpoint());
}
public String getContractPDFUrl(String essContractId) {
EssContract contract = essContractDao.getOrThrow(essContractId);
if (contract.isUploadedToOss())
return ossService.getOssUrl(contract.getOssFileKey());
// 尝试做补偿
maybeScheduleDownloadContractPDF(contract, false);
return getContractPDFUrlFromEss(contract);
}
@BizTransactional
public void saveContractSnapshot(SaveContractSnapshotRequest request) {
essLogDao.logRequest("saveContractSnapshot", request.getEssContractId(), request);
EssContract contract = essContractDao.getOrThrow(request.getEssContractId());
contract.getOrCreateExt().setSaveContractSnapshotArbitrarily(true);
essContractDao.updateExt(contract);
maybeScheduleDownloadContractPDF(contract, request.isRetryDownload());
}
public void maybeScheduleDownloadContractPDF(EssContract contract, boolean retryDownload) {
EssContract reload = essContractDao.findOrNull(contract.getEssContractId());
if (scheduleDownloadPDF(reload, retryDownload))
essBroadcaster.fireDownloadContractPDF(contract, retryDownload);
}
private boolean scheduleDownloadPDF(EssContract contract, boolean retryDownload) {
if (contract == null)
return false;
if (contract.isUploadedToOss() && !retryDownload)
return false;
return contract.shouldDownloadContract();
}
@BizTransactional
public void downloadContractPDF(EssContract contract, boolean retryDownload) {
if (!scheduleDownloadPDF(contract, retryDownload)) {
log.info("合同不需要下载PDF, contract={}", contract);
return;
}
// 终状的合同肯定无需重复下载
if (contract.isFinalState() && contract.isUploadedToOss()) {
log.info("合同已是终态且已下载PDF, contract={}", contract);
return;
}
try {
String pdfUrl = getContractPDFUrlFromEss(contract);
String fileName = String.format("%s.pdf", contract.getContractName());
String fileKey = ossService.uploadToOss(pdfUrl, fileName);
essContractDao.setOssFileKey(contract, fileKey);
essLogDao.log("uploadContractToOss", contract.getEssContractId(), "ossFileKey", fileKey, "retryDownload",
retryDownload);
log.info("上传合同到OSS成功, essContractId={}", contract.getEssContractId());
}
catch (Exception e) {
log.warn("上传合同到OSS失败", e);
essLogDao.log(e, "uploadContractToOss", contract.getEssContractId(), "retryDownload", retryDownload);
}
}
private String getContractPDFUrlFromEss(EssContract contract) {
EssPerson superAdmin = orgManager.getSuperAdminOrThrow(contract.getCreatorOuId());
return essClient.getContractPDFUrl(superAdmin, contract.getEssContractId());
}
}

View File

@ -0,0 +1,230 @@
package cn.axzo.nanopart.ess.server.ess;
import static cn.axzo.nanopart.ess.server.utils.IdBuilder.idbuilder;
import java.util.Optional;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import cn.axzo.nanopart.ess.api.domain.contract.OrgPersonInfo;
import cn.axzo.nanopart.ess.api.enums.EssPersonState;
import cn.axzo.nanopart.ess.api.enums.EssSealState;
import cn.axzo.nanopart.ess.api.enums.EssSealType;
import cn.axzo.nanopart.ess.api.request.CreateConsoleLoginUrlRequest;
import cn.axzo.nanopart.ess.api.request.RemoveSealPersonRequest;
import cn.axzo.nanopart.ess.api.utils.YesOrNo;
import cn.axzo.nanopart.ess.server.dao.EssLogDao;
import cn.axzo.nanopart.ess.server.dao.EssOrgDao;
import cn.axzo.nanopart.ess.server.dao.EssPersonDao;
import cn.axzo.nanopart.ess.server.dao.EssSealDao;
import cn.axzo.nanopart.ess.server.dao.EssSealPersonDao;
import cn.axzo.nanopart.ess.server.entity.EssOrg;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.entity.EssSeal;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import cn.axzo.nanopart.ess.server.ess.domain.OrgAndPerson;
import cn.axzo.nanopart.ess.server.ess.support.EssSupport;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import cn.axzo.nanopart.ess.server.utils.BizTransactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class OrgManager {
private final EssSupport essSupport;
private final EssOrgDao essOrgDao;
private final EssPersonDao essPersonDao;
private final EssSealDao essSealDao;
private final EssSealPersonDao essSealPersonDao;
private final EssLogDao essLogDao;
// !! login
@BizTransactional
public OrgAndPerson createConsoleLoginUrl(CreateConsoleLoginUrlRequest request) {
EssOrg org = essOrgDao.findForUpdateOrNull(request.getOuId());
if (org == null) {
org = essSupport.createOrg(request);
essOrgDao.save(org);
}
EssPerson person = getOrCreateOrgPerson(request);
return new OrgAndPerson(org, person);
}
// !! org and person
@BizTransactional
public void addAuthorizedOrgPerson(OrgPerson orgPerson) {
String subject = idbuilder() //
.append(orgPerson.getOuId()) //
.append(orgPerson.getPersonId()) //
.build();
essLogDao.log("personAuthorized", subject);
EssPerson person = getOrCreateOrgPerson(orgPerson);
essPersonDao.setOrgPersonState(person, EssPersonState.AUTHORIZED);
log.info("add authorized person: {}", person);
}
private EssPerson getOrCreateOrgPerson(OrgPerson orgPerson) {
EssPerson essPerson = essPersonDao.find(orgPerson, true).orElse(null);
if (essPerson == null) {
essPerson = essSupport.createPerson(orgPerson);
essPersonDao.save(essPerson);
essPerson = essPersonDao.find(orgPerson, true).orElseThrow(InternalError::new);
}
return essPerson;
}
@BizTransactional
public void setOrgAuthorized(OrgPerson superAdmin) {
essLogDao.log("orgAuthorized", superAdmin.getOuId(), "superAdminPersonId", superAdmin.getPersonId());
essOrgDao.setOrgAuthorized(superAdmin);
essLogDao.log("setOrgAuthorized", superAdmin.getOuId(), "superAdmin", superAdmin);
log.info("set org authorized, superAdmin={}", superAdmin);
}
@BizTransactional
public void changeSuperAdmin(OrgPerson admin) {
essOrgDao.changeSuperAdmin(admin);
log.info("change super admin: {}", OrgPerson.toString(admin));
}
public EssPerson getSuperAdminOrThrow(Long ouId) {
EssPerson superAdmin = findSuperAdmin(ouId).orElse(null);
BizAssertions.assertNotNull(superAdmin, "单位还未认证. 单位id={}", ouId);
return superAdmin;
}
public Optional<EssPerson> findSuperAdmin(Long ouId) {
EssOrg org = findOrgOrNull(ouId);
if (org == null)
return Optional.empty();
if (org.getSuperAdminPersonId() <= 0L)
return Optional.empty();
return Optional.ofNullable(essPersonDao.findOrNull(ouId, org.getSuperAdminPersonId()));
}
public EssOrg findOrgOrNull(Long ouId) {
return essOrgDao.findOrNull(ouId);
}
// !! seal
@BizTransactional
public boolean maybeAddSeal(Long ouId, String essSealId, EssSealType type, String name) {
EssSeal seal = essSealDao.findByEssSealId(essSealId).orElse(null);
if (seal != null)
return false;
seal = new EssSeal();
seal.setOuId(ouId);
seal.setEssSealId(essSealId);
seal.setName(name);
seal.setState(EssSealState.CREATE);
seal.setType(type);
try {
essSealDao.save(seal);
log.info("add seal: {}", seal);
return true;
}
catch (DuplicateKeyException ignored) {
return false;
}
}
public void updateSealState(String essSealId, EssSealState state) {
essSealDao.updateState(essSealId, state);
log.info("update seal state: {}, {}", essSealId, state);
}
@BizTransactional
public void maybeAddPersonAndSetSealAuthorized(String essSealId, Long personId) {
maybeAddSealPerson(essSealId, personId);
essSealPersonDao.setPersonAuthorized(essSealId, personId);
String subject = idbuilder().append(essSealId).append(personId).build();
essLogDao.log("maybeAddPersonAndSetSealAuthorized", subject, "personId", personId);
log.info("add seal authorization: essSealId={}, personId={}", essSealId, personId);
}
public void setSealPersonAuthorizedBy(String essSealId, Long personId, Long authorizedByPersonId) {
essSealPersonDao.setSealPersonAuthorizedBy(essSealId, personId, authorizedByPersonId);
}
@BizTransactional
public void removeSealAuthorization(String essSealId, Long personId) {
maybeAddSealPerson(essSealId, personId);
essSealPersonDao.removeSealAuthorization(essSealId, personId);
String subject = idbuilder().append(essSealId).append(personId).build();
essLogDao.log("removeSealAuthorization", subject, "personId", personId);
log.info("remove seal authorization: essSealId={}, personId={}", essSealId, personId);
}
@BizTransactional
public EssPerson addSealPerson(String essSealId, Long personId) {
EssSeal seal = essSealDao.findByEssSealId(essSealId).orElse(null);
BizAssertions.assertNotNull(seal, "印章不存在: {}", essSealId);
//noinspection DataFlowIssue
EssPerson essPerson = getOrCreateOrgPerson(OrgPersonInfo.create(seal.getOuId(), personId));
maybeAddSealPerson(essSealId, personId);
return essPerson;
}
/**
* should be called in transaction
*/
private void maybeAddSealPerson(String essSealId, Long personId) {
EssSeal seal = essSealDao.findByEssSealId(essSealId).orElse(null);
BizAssertions.assertNotNull(seal, "印章不存在: {}", essSealId);
EssSealPerson savedSealPerson = essSealPersonDao.find(essSealId, personId, true).orElse(null);
if (savedSealPerson != null)
return;
EssSealPerson sealPerson = new EssSealPerson();
//noinspection DataFlowIssue
sealPerson.setOuId(seal.getOuId());
sealPerson.setEssSealId(essSealId);
sealPerson.setPersonId(personId);
sealPerson.setIsAuthorized(YesOrNo.NO);
try {
essSealPersonDao.save(sealPerson);
}
catch (DuplicateKeyException ignored) {
}
}
@BizTransactional
public void tryRemoveSealPerson(RemoveSealPersonRequest request) {
EssSeal seal = essSealDao.findByEssSealId(request.getEssSealId()).orElse(null);
BizAssertions.assertNotNull(seal, "印章不存在: {}", request.getEssSealId());
//noinspection DataFlowIssue
EssSealPerson sealPerson = essSealPersonDao.find(seal.getEssSealId(), request.getPersonId(), true).orElse(null);
BizAssertions.assertNotNull(sealPerson, "印章人员不存在");
//noinspection DataFlowIssue
BizAssertions.assertTrue(!sealPerson.isAuthorized(), "请先取消授权再删除");
essSealPersonDao.removeById(sealPerson.getId());
log.info("remove seal person: {}", sealPerson);
}
public void setOrgPersonResign(EssPerson person) {
String subject = idbuilder() //
.append(person.getOuId()) //
.append(person.getPersonId()) //
.build();
essLogDao.log("personResign", subject);
essPersonDao.setOrgPersonState(person, EssPersonState.RESIGNED);
essSealPersonDao.deleteByPerson(person);
log.info("set person resigned: {}", person);
}
public void ensureOrgAuthorized(Long ouId, String message) {
EssOrg org = findOrgOrNull(ouId);
BizAssertions.assertTrue(org != null && org.isAuthorized(), message);
}
}

View File

@ -0,0 +1,181 @@
package cn.axzo.nanopart.ess.server.ess;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import static java.util.stream.Collectors.toSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.BiConsumer;
import org.springframework.stereotype.Service;
import com.google.common.collect.Lists;
import cn.axzo.basics.common.BeanMapper;
import cn.axzo.basics.profiles.dto.basic.PersonProfileDto;
import cn.axzo.nanopart.ess.api.domain.EssOrgAndSealInfo;
import cn.axzo.nanopart.ess.api.domain.EssOrgInfo;
import cn.axzo.nanopart.ess.api.domain.EssSealInfo;
import cn.axzo.nanopart.ess.api.domain.EssSealPersonInfo;
import cn.axzo.nanopart.ess.api.domain.PersonProfileInfo;
import cn.axzo.nanopart.ess.api.enums.EssSealState;
import cn.axzo.nanopart.ess.api.request.GetOrgAuthStatesRequest;
import cn.axzo.nanopart.ess.api.request.GetPersonAuthStateRequest;
import cn.axzo.nanopart.ess.api.request.GetSealPersonRequest;
import cn.axzo.nanopart.ess.api.request.GetSealsRequest;
import cn.axzo.nanopart.ess.api.response.GetOrgAuthStatesResponse;
import cn.axzo.nanopart.ess.api.response.GetPersonAuthStateResponse;
import cn.axzo.nanopart.ess.server.dao.EssOrgDao;
import cn.axzo.nanopart.ess.server.dao.EssPersonDao;
import cn.axzo.nanopart.ess.server.dao.EssSealDao;
import cn.axzo.nanopart.ess.server.dao.EssSealPersonDao;
import cn.axzo.nanopart.ess.server.entity.EssOrg;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.entity.EssSeal;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import cn.axzo.nanopart.ess.server.entity.domain.OuAndPersonId;
import cn.axzo.nanopart.ess.server.ess.domain.EssPersons;
import cn.axzo.nanopart.ess.server.ess.support.EssSupport;
import cn.axzo.nanopart.ess.server.ess.support.PersonProfiles;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import cn.axzo.pokonyan.config.mybatisplus.BaseEntity;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Service
@RequiredArgsConstructor
public class QueryService {
private final EssOrgDao essOrgDao;
private final EssPersonDao essPersonDao;
private final EssSealDao essSealDao;
private final EssSealPersonDao essSealPersonDao;
private final EssSupport essSupport;
public List<GetOrgAuthStatesResponse> getOrgAuthStates(GetOrgAuthStatesRequest request) {
List<EssOrg> orgs = essOrgDao.getByOuIds(request.getOuIds());
ArrayList<GetOrgAuthStatesResponse> responses = new ArrayList<>();
for (Long ouId : request.getOuIds()) {
GetOrgAuthStatesResponse response = new GetOrgAuthStatesResponse();
responses.add(response);
response.setOuId(ouId);
response.setAuthorized(orgs.stream() //
.filter(org -> org.getOuId().equals(ouId)) //
.map(EssOrg::isAuthorized) //
.findFirst() //
.orElse(false));
}
return responses;
}
public GetPersonAuthStateResponse getPersonAuthState(GetPersonAuthStateRequest request) {
EssPerson person = essPersonDao.findOrNull(request);
GetPersonAuthStateResponse response = new GetPersonAuthStateResponse();
response.setAuthorized(person != null && person.isAuthorized());
return response;
}
public List<EssOrgAndSealInfo> getOrgEnabledSeals(GetSealsRequest request) {
List<EssOrg> orgs = essOrgDao.lambdaQuery() //
.in(EssOrg::getOuId, request.getOuIds()) //
.list();
Map<Long, List<EssSeal>> ouId2Seals = essSealDao.lambdaQuery() //
.in(EssSeal::getOuId, request.getOuIds()) //
.eq(EssSeal::getState, EssSealState.ENABLED).list().stream() //
.collect(groupingBy(EssSeal::getOuId));
Set<String> essSealIds = ouId2Seals.values().stream() //
.flatMap(Collection::stream) //
.map(EssSeal::getEssSealId) //
.collect(toSet());
Map<String, List<EssSealPerson>> essSealId2Persons = Collections.emptyMap();
if (!essSealIds.isEmpty())
essSealId2Persons = essSealPersonDao.lambdaQuery() //
.in(EssSealPerson::getEssSealId, essSealIds).list().stream() //
.collect(groupingBy(EssSealPerson::getEssSealId));
HashSet<Long> personIds = new HashSet<>();
essSealId2Persons.values().stream().flatMap(Collection::stream).forEach(sealPerson -> {
personIds.add(sealPerson.getPersonId());
personIds.add(sealPerson.getAuthorizedByPersonId());
});
// ess person
EssPersons essPersons = EssPersons.wrap(essPersonDao.getByPersonIds(personIds));
// person profile
PersonProfiles personProfiles = PersonProfiles.wrap(
essSupport.getPersonProfiles(Lists.newArrayList(personIds)));
ArrayList<EssOrgAndSealInfo> essOrgAndSeals = new ArrayList<>();
for (EssOrg org : orgs) {
EssOrgAndSealInfo orgAndSeal = new EssOrgAndSealInfo();
essOrgAndSeals.add(orgAndSeal);
orgAndSeal.setOrg(BeanMapper.copyBean(org, EssOrgInfo.class));
orgAndSeal.getOrg().setOrgAuthorized(org.isAuthorized());
List<EssSeal> seals = ouId2Seals //
.getOrDefault(org.getOuId(), Collections.emptyList()).stream() //
.sorted(Comparator.comparingLong(BaseEntity::getId)) //
.collect(toList());
orgAndSeal.setSeals(BeanMapper.copyList(seals, EssSealInfo.class));
for (EssSealInfo sealInfo : orgAndSeal.getSeals()) {
List<EssSealPerson> persons = new ArrayList<>(
essSealId2Persons.getOrDefault(sealInfo.getEssSealId(), Collections.emptyList()));
persons.sort((p1, p2) -> -Long.compare(p1.getId(), p2.getId()));
for (EssSealPerson sealPerson : persons)
sealInfo.addSealPerson(createSealPersonInfo(essPersons, personProfiles, org, sealPerson));
}
}
return essOrgAndSeals;
}
public List<EssSealPersonInfo> getSealPersons(GetSealPersonRequest request) {
List<EssSealPerson> sealPersons = essSealPersonDao.get(request.getEssSealId());
if (sealPersons.isEmpty())
return Collections.emptyList();
HashSet<Long> personIds = new HashSet<>();
sealPersons.forEach(sealPerson -> {
personIds.add(sealPerson.getPersonId());
personIds.add(sealPerson.getAuthorizedByPersonId());
});
EssPersons essPersons = EssPersons.wrap(essPersonDao.getByPersonIds(personIds));
PersonProfiles personProfiles = PersonProfiles.wrap(
essSupport.getPersonProfiles(Lists.newArrayList(personIds)));
EssOrg org = essOrgDao.findOrNull(sealPersons.get(0).getOuId());
BizAssertions.assertNotNull(org, "org not found, ouId={}", sealPersons.get(0).getOuId());
ArrayList<EssSealPersonInfo> sealPersonsInfo = new ArrayList<>();
for (EssSealPerson sealPerson : sealPersons)
sealPersonsInfo.add(createSealPersonInfo(essPersons, personProfiles, org, sealPerson));
return sealPersonsInfo;
}
private EssSealPersonInfo createSealPersonInfo(EssPersons essPersons, PersonProfiles personProfiles, EssOrg org,
EssSealPerson sealPerson) {
EssPerson essPerson = essPersons.getOrThrow(
OuAndPersonId.create(sealPerson.getOuId(), sealPerson.getPersonId()));
EssSealPersonInfo sealPersonInfo = BeanMapper.copyBean(sealPerson, EssSealPersonInfo.class);
sealPersonInfo.setSuperAdmin(org.isSuperAdmin(sealPerson.getPersonId()));
sealPersonInfo.setSealAuthorized(sealPerson.isAuthorized());
sealPersonInfo.setPersonAuthorized(essPerson.isAuthorized());
sealPersonInfo.setAuthorizeTimeMs(
sealPerson.getAuthorizeTime() == null ? null : sealPerson.getAuthorizeTime().getTime());
BiConsumer<Long, PersonProfileInfo> builder = (personId, profileInfo) -> {
PersonProfileDto profile = personProfiles.findOrNull(personId);
if (profile == null)
return;
profileInfo.setPersonId(personId);
profileInfo.setPersonName(profile.getRealName());
profileInfo.setAvatar(profile.getAvatarUrl());
};
builder.accept(sealPerson.getPersonId(), sealPersonInfo.getSealPerson());
if (sealPersonInfo.isSealAuthorized())
builder.accept(sealPerson.getAuthorizedByPersonId(), sealPersonInfo.getAuthorizedByPerson());
return sealPersonInfo;
}
}

View File

@ -0,0 +1,184 @@
package cn.axzo.nanopart.ess.server.ess.controller;
import java.util.List;
import org.springframework.web.bind.annotation.RestController;
import cn.axzo.basics.common.BeanMapper;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.ess.api.EssApi;
import cn.axzo.nanopart.ess.api.domain.EssOrgAndSealInfo;
import cn.axzo.nanopart.ess.api.domain.EssSealPersonInfo;
import cn.axzo.nanopart.ess.api.domain.contract.EssContractInfo;
import cn.axzo.nanopart.ess.api.request.AddSealAuthorizationRequest;
import cn.axzo.nanopart.ess.api.request.AddSealPersonRequest;
import cn.axzo.nanopart.ess.api.request.AssignSignUrlRequest;
import cn.axzo.nanopart.ess.api.request.CreateConsoleLoginUrlRequest;
import cn.axzo.nanopart.ess.api.request.CreateContractByFileRequest;
import cn.axzo.nanopart.ess.api.request.DownloadSingedContractPdfRequest;
import cn.axzo.nanopart.ess.api.request.GetContractDetailByContractIdRequest;
import cn.axzo.nanopart.ess.api.request.GetEmbedWebUrlRequest;
import cn.axzo.nanopart.ess.api.request.GetOrgAuthStatesRequest;
import cn.axzo.nanopart.ess.api.request.GetPersonAuthStateRequest;
import cn.axzo.nanopart.ess.api.request.GetSealPersonRequest;
import cn.axzo.nanopart.ess.api.request.GetSealsRequest;
import cn.axzo.nanopart.ess.api.request.RemoveSealAuthorizationRequest;
import cn.axzo.nanopart.ess.api.request.RemoveSealPersonRequest;
import cn.axzo.nanopart.ess.api.request.RevokeContractRequest;
import cn.axzo.nanopart.ess.api.request.SaveContractSnapshotRequest;
import cn.axzo.nanopart.ess.api.response.AssignSignUrlResponse;
import cn.axzo.nanopart.ess.api.response.CreateConsoleLoginUrlResponse;
import cn.axzo.nanopart.ess.api.response.CreateContractByFileResponse;
import cn.axzo.nanopart.ess.api.response.DownloadSingedContractPdfResponse;
import cn.axzo.nanopart.ess.api.response.GetContractDetailByContractIdResponse;
import cn.axzo.nanopart.ess.api.response.GetEmbedWebUrlResponse;
import cn.axzo.nanopart.ess.api.response.GetOrgAuthStatesResponse;
import cn.axzo.nanopart.ess.api.response.GetPersonAuthStateResponse;
import cn.axzo.nanopart.ess.server.dao.EssContractDao;
import cn.axzo.nanopart.ess.server.entity.EssContract;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.ess.ContractManager;
import cn.axzo.nanopart.ess.server.ess.EssService;
import cn.axzo.nanopart.ess.server.ess.OrgManager;
import cn.axzo.nanopart.ess.server.ess.QueryService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
*/
@Slf4j
@RestController
@RequiredArgsConstructor
class ApiController implements EssApi {
private final OrgManager orgManager;
private final ContractManager contractManager;
private final QueryService queryService;
private final EssContractDao essContractDao;
private final EssService essService;
@Override
public ApiResult<List<GetOrgAuthStatesResponse>> getOrgAuthStates(GetOrgAuthStatesRequest request) {
log.info("getOrgAuthStates request={}", request);
return ApiResult.ok(queryService.getOrgAuthStates(request));
}
@Override
public ApiResult<GetPersonAuthStateResponse> getPersonAuthState(GetPersonAuthStateRequest request) {
log.info("getPersonAuthState request={}", request);
return ApiResult.ok(queryService.getPersonAuthState(request));
}
@Override
public ApiResult<CreateConsoleLoginUrlResponse> createConsoleLoginUrl(CreateConsoleLoginUrlRequest request) {
log.info("createConsoleLoginUrl request={}", request);
CreateConsoleLoginUrlResponse response = new CreateConsoleLoginUrlResponse();
response.setConsoleLoginUrl(essService.createConsoleLoginUrl(request));
return ApiResult.ok(response);
}
@Override
public ApiResult<GetEmbedWebUrlResponse> getEmbedWebUrl(GetEmbedWebUrlRequest request) {
log.info("getEmbedWebUrl request={}", request);
GetEmbedWebUrlResponse response = new GetEmbedWebUrlResponse();
response.setEmbedWebUrl(essService.getEmbedWebUrl(request));
return ApiResult.ok(response);
}
@Override
public ApiResult<List<EssOrgAndSealInfo>> getOrgEnabledSeals(GetSealsRequest request) {
log.info("getOrgEnabledSeals request={}", request);
return ApiResult.ok(queryService.getOrgEnabledSeals(request));
}
@Override
public ApiResult<List<EssSealPersonInfo>> getSealPersons(GetSealPersonRequest request) {
log.info("getSealPersons request={}", request);
return ApiResult.ok(queryService.getSealPersons(request));
}
@Override
public ApiResult<Boolean> addSealPerson(AddSealPersonRequest request) {
log.info("addSealPerson request={}", request);
EssPerson essPerson = orgManager.addSealPerson(request.getEssSealId(), request.getPersonId());
if (essPerson.isAuthorized()) {
AddSealAuthorizationRequest addSealAuthorizationRequest = new AddSealAuthorizationRequest();
addSealAuthorizationRequest.setPersonId(essPerson.getPersonId());
addSealAuthorizationRequest.setEssSealId(request.getEssSealId());
addSealAuthorizationRequest.setOperatorPersonId(request.getOperatorPersonId());
essService.essAddSealAuthorization(addSealAuthorizationRequest);
return ApiResult.ok(true);
}
return ApiResult.ok(false);
}
@Override
public ApiResult<Void> removeSealPerson(RemoveSealPersonRequest request) {
log.info("removeSealPerson request={}", request);
orgManager.tryRemoveSealPerson(request);
return ApiResult.ok();
}
@Override
public ApiResult<Void> addSealAuthorization(AddSealAuthorizationRequest request) {
log.info("addSealAuthorization request={}", request);
essService.essAddSealAuthorization(request);
return ApiResult.ok();
}
@Override
public ApiResult<Void> removeSealAuthorization(RemoveSealAuthorizationRequest request) {
log.info("removeSealAuthorization request={}", request);
essService.essRemoveSealAuthorization(request);
return ApiResult.ok();
}
@Override
public ApiResult<CreateContractByFileResponse> createContractByFile(CreateContractByFileRequest request) {
log.info("createContractByFile request={}", request);
return ApiResult.ok(contractManager.createContractByFile(request));
}
@Override
public ApiResult<AssignSignUrlResponse> assignSignUrl(AssignSignUrlRequest request) {
log.info("assignSignUrl request={}", request);
AssignSignUrlResponse response = new AssignSignUrlResponse();
response.setUrl(essService.assignSignUrl(request));
return ApiResult.ok(response);
}
@Override
public ApiResult<DownloadSingedContractPdfResponse> getContractPDFUrl(DownloadSingedContractPdfRequest request) {
log.info("getContractPDFUrl request={}", request);
DownloadSingedContractPdfResponse response = new DownloadSingedContractPdfResponse();
response.setPdfUrl(essService.getContractPDFUrl(request.getEssContractId()));
return ApiResult.ok(response);
}
@Override
public ApiResult<Void> saveContractSnapshot(SaveContractSnapshotRequest request) {
log.info("saveContractSnapshot request={}", request);
essService.saveContractSnapshot(request);
return ApiResult.ok();
}
@Override
public ApiResult<Void> revokeContract(RevokeContractRequest request) {
log.info("revokeContract request={}", request);
contractManager.revokeContract(request);
return ApiResult.ok();
}
@Override
public ApiResult<GetContractDetailByContractIdResponse> getContractByContractId(
GetContractDetailByContractIdRequest request) {
log.info("getContractByContractId request={}", request);
EssContract contract = essContractDao.getOrThrow(request.getEssContractId());
GetContractDetailByContractIdResponse response = new GetContractDetailByContractIdResponse();
response.setContract(BeanMapper.copyBean(contract, EssContractInfo.class));
return ApiResult.ok(response);
}
}

View File

@ -0,0 +1,294 @@
package cn.axzo.nanopart.ess.server.ess.controller;
import static cn.axzo.nanopart.ess.api.request.CallbackRequest.ContractStateChanged;
import static cn.axzo.nanopart.ess.api.request.CallbackRequest.OrgPersonJoin;
import static cn.axzo.nanopart.ess.api.request.CallbackRequest.SealOperate;
import static cn.axzo.nanopart.ess.server.utils.IdBuilder.idbuilder;
import static java.util.stream.Collectors.toList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSONObject;
import com.tencentcloudapi.essbasic.v20210526.models.OccupiedSeal;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.maokai.api.vo.response.OrganizationalUnitVO;
import cn.axzo.nanopart.ess.api.EssCallbackApi;
import cn.axzo.nanopart.ess.api.domain.contract.EssApproveDetail;
import cn.axzo.nanopart.ess.api.enums.EssContractApproveState;
import cn.axzo.nanopart.ess.api.enums.EssContractState;
import cn.axzo.nanopart.ess.api.enums.EssSealState;
import cn.axzo.nanopart.ess.api.enums.EssSealType;
import cn.axzo.nanopart.ess.api.request.AddSealAuthorizationRequest;
import cn.axzo.nanopart.ess.api.request.CallbackRequest;
import cn.axzo.nanopart.ess.api.request.CallbackRequest.OrgAuthorizationFinish;
import cn.axzo.nanopart.ess.api.request.CallbackRequest.SuperAdminChanged;
import cn.axzo.nanopart.ess.server.dao.EssContractDao;
import cn.axzo.nanopart.ess.server.dao.EssLogDao;
import cn.axzo.nanopart.ess.server.dao.EssSealPersonDao;
import cn.axzo.nanopart.ess.server.entity.EssContract;
import cn.axzo.nanopart.ess.server.entity.EssLog;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import cn.axzo.nanopart.ess.server.ess.ContractManager;
import cn.axzo.nanopart.ess.server.ess.EssClient;
import cn.axzo.nanopart.ess.server.ess.EssService;
import cn.axzo.nanopart.ess.server.ess.OrgManager;
import cn.axzo.nanopart.ess.server.ess.domain.OrgOpenId;
import cn.axzo.nanopart.ess.server.ess.domain.PersonOpenId;
import cn.axzo.nanopart.ess.server.ess.domain.StringObject;
import cn.axzo.nanopart.ess.server.ess.support.EssSupport;
import cn.axzo.nanopart.ess.server.ess.support.OrgProfiles;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
* @see <a href="https://qian.tencent.com/developers/partner/callback_types_staffs">腾讯回调</a>
*/
@Slf4j
@RestController
@RequiredArgsConstructor
class CallbackController implements EssCallbackApi, InitializingBean {
private final OrgManager orgManager;
private final EssService essService;
private final ContractManager contractManager;
private final EssLogDao essLogDao;
private final EssContractDao essContractDao;
private final EssSealPersonDao essSealPersonDao;
private final TransactionTemplate transactionTemplate;
private final EssSupport essSupport;
private final EssClient essClient;
private final Map<CallbackType, CallbackHandler> handlers = new HashMap<>();
@Override
public void afterPropertiesSet() {
// 企业开通电子签回调
registerHandler(CallbackType.ORG_AUTHORIZATION_FINISH, request -> {
OrgAuthorizationFinish result = request.readMsgData(OrgAuthorizationFinish.class);
PersonOpenId superAdmin = PersonOpenId.parse(result.getProxyOperatorOpenId());
if (result.isOpenSuccess()) {
orgManager.addAuthorizedOrgPerson(superAdmin);
orgManager.setOrgAuthorized(superAdmin);
}
return idbuilder() //
.append(superAdmin.getOuId()) //
.append(superAdmin.getPersonId()) //
.build();
});
// 员工加入子企业的时候发送此通知
registerHandler(CallbackType.ORG_PERSON_JOIN, request -> {
PersonOpenId person = PersonOpenId.parse(request.readMsgData(OrgPersonJoin.class).getProxyOperatorOpenId());
orgManager.addAuthorizedOrgPerson(person);
// 自动授权
for (EssSealPerson sealPerson : essSealPersonDao.getByPerson(person)) {
AddSealAuthorizationRequest addSealAuthorizationRequest = new AddSealAuthorizationRequest();
addSealAuthorizationRequest.setEssSealId(sealPerson.getEssSealId());
addSealAuthorizationRequest.setPersonId(person.getPersonId());
// 保留界面上的授权人
addSealAuthorizationRequest.setOperatorPersonId(0L);
try {
essService.essAddSealAuthorization(addSealAuthorizationRequest);
}
catch (Exception e) {
String subject = idbuilder() //
.append(sealPerson.getEssSealId()) //
.append(sealPerson.getPersonId()) //
.build();
essLogDao.log(e, "essAddSealAuthorization", subject);
log.warn("add seal authorization failed", e);
}
}
return idbuilder() //
.append(person.getOuId()) //
.append(person.getPersonId()) //
.build();
});
// 印章回调
registerHandler(CallbackType.SEAL_OPERATE, request -> {
SealOperate operate = request.readMsgData(SealOperate.class);
OrgOpenId ou = OrgOpenId.parse(operate.getProxyOrganizationOpenId());
PersonOpenId operator = null;
if (StringUtils.isNotBlank(operate.getProxyOperatorOpenId()))
operator = PersonOpenId.parse(operate.getProxyOperatorOpenId());
PersonOpenId authorized = StringUtils.isBlank(operate.getAuthorizedOperatorOpenId()) ? PersonOpenId.none()
: PersonOpenId.parse(operate.getAuthorizedOperatorOpenId());
String sealName = "";
boolean isDelete = "Delete".equals(operate.getOperate());
if (!isDelete && operator != null) {
EssPerson superAdmin = orgManager.getSuperAdminOrThrow(operator.getOuId());
OccupiedSeal sealInfo = essClient.getSealInfo(superAdmin, operate.getSealId());
if (sealInfo != null)
sealName = sealInfo.getSealName();
}
boolean sealCreated = orgManager.maybeAddSeal(ou.getOuId(), operate.getSealId(),
EssSealType.fromEssCode(operate.getSealType()), sealName);
// 1. 印章创建人会自动获得授权; 2. 避免miss create事件
if (sealCreated && operator != null) {
orgManager.maybeAddPersonAndSetSealAuthorized(operate.getSealId(), operator.getPersonId());
orgManager.setSealPersonAuthorizedBy(operate.getSealId(), operator.getPersonId(),
operator.getPersonId());
}
if (isDelete)
orgManager.updateSealState(operate.getSealId(), EssSealState.DELETED);
if ("Disable".equals(operate.getOperate()))
orgManager.updateSealState(operate.getSealId(), EssSealState.DISABLED);
if ("Enable".equals(operate.getOperate()))
orgManager.updateSealState(operate.getSealId(), EssSealState.ENABLED);
if ("Valid".equals(operate.getOperate())) {
orgManager.maybeAddPersonAndSetSealAuthorized(operate.getSealId(), authorized.getPersonId());
EssSealPerson savedPerson = essSealPersonDao //
.find(operate.getSealId(), authorized.getPersonId()) //
.orElseThrow(InternalError::new);
boolean authorizedByPreset = savedPerson != null && savedPerson.getAuthorizedByPersonId() > 0;
// 可能从控制台直接授权
if (!authorizedByPreset && operator != null)
orgManager.setSealPersonAuthorizedBy(operate.getSealId(), authorized.getPersonId(),
operator.getPersonId());
}
if ("Invalid".equals(operate.getOperate()))
orgManager.removeSealAuthorization(operate.getSealId(), authorized.getPersonId());
return operate.getSealId();
});
// 合同回调
registerHandler(CallbackType.CONTRACT_STATE_CHANGED, request -> {
ContractStateChanged changes = request.readMsgData(ContractStateChanged.class);
EssContractState state = EssContractState.fromEssCode(changes.getFlowStatus());
if (state == null) {
log.warn("unknown contract state: {}", changes.getFlowStatus());
return changes.getFlowId();
}
List<Long> ouIds = changes.getFlowApproverInfo().stream() //
.map(CallbackRequest.FlowApproverDetail::getProxyOrganizationOpenId) //
.map(this::parseLong) //
.filter(Objects::nonNull) //
.collect(toList());
OrgProfiles orgProfiles = OrgProfiles.wrap(essSupport.getOrgProfiles(ouIds));
List<EssApproveDetail> approveDetails = changes.getFlowApproverInfo().stream().map(info -> {
EssApproveDetail detail = new EssApproveDetail();
if (StringUtils.isNotBlank(info.getProxyOperatorOpenId())) {
PersonOpenId personOpenId = PersonOpenId.parse(info.getProxyOperatorOpenId());
detail.setSignPerson(personOpenId.toOrgPersonInfo());
}
detail.setRecipientId(info.getRecipientId());
detail.setState(EssContractApproveState.fromEssCode(info.getApproveStatus()));
detail.setSignOrder(info.getSignOrder());
detail.setApproveTimeMs(info.getApproveTime() * 1000);
detail.setMessage(info.getApproveMessage());
detail.setApproverDeadlineMs(info.getApproverDeadline() * 1000);
detail.setPhoneNumber(info.getPhoneNumber());
Long ouId = parseLong(info.getProxyOrganizationOpenId());
OrganizationalUnitVO org = orgProfiles.find(ouId).orElse(null);
if (org != null) {
detail.setOuName(org.getName());
detail.setOuId(org.getId());
}
return detail;
}).collect(toList());
EssContract contract = essContractDao.findOrNull(changes.getFlowId());
if (contract == null && StringUtils.isNotBlank(changes.getCustomerData())) {
JSONObject customData = StringObject.parse(changes.getCustomerData()).asJsonObject();
contract = essContractDao.getById(customData.getLong(ContractManager.CONTRACT_ID));
}
if (contract == null) {
log.warn("contract not found: {}", changes.getFlowId());
}
else {
contractManager.updateContractState(contract, state, approveDetails, changes.getFlowMessage());
essService.maybeScheduleDownloadContractPDF(contract, true);
}
return changes.getFlowId();
});
registerHandler(CallbackType.SUPER_ADMIN_CHANGED, request -> {
SuperAdminChanged changes = request.readMsgData(SuperAdminChanged.class);
PersonOpenId admin = PersonOpenId.parse(changes.getChangeToUserOpenId());
orgManager.changeSuperAdmin(admin);
return admin.getOuId();
});
}
private Long parseLong(String value) {
if (StringUtils.isBlank(value) || !NumberUtils.isDigits(value))
return null;
return Long.parseLong(value);
}
@Override
public ApiResult<Object> callback(CallbackRequest request) {
log.info("ess callback: {}", request);
Object subject = null;
CallbackType callbackType = CallbackType.parse(request.getMsgType()).orElse(null);
CallbackHandler handler = callbackType == null ? null : handlers.get(callbackType);
EssLog essLog = new EssLog();
essLog.addLogContent("interested", handler != null);
try {
if (handler != null) {
subject = transactionTemplate.execute(unused -> {
try {
return handler.handle(request);
}
catch (Exception e) {
essLog.setError(e);
log.warn("callback failed", e);
// wrap exception to make it throw to yoke
throw new RuntimeException(e);
}
});
}
else {
log.info("ignore callback: {}", request);
}
return ApiResult.ok("success");
}
finally {
String callbackTypeStr = callbackType == null ? "" : "[" + callbackType + "]";
essLog.setContext(String.format("ess:callback:%s%s", request.getMsgType(), callbackTypeStr));
essLog.setSubject(subject == null ? "" : String.valueOf(subject));
essLog.addLogContent("request", request);
essLogDao.save(essLog);
}
}
private void registerHandler(CallbackType callbackType, CallbackHandler handler) {
handlers.put(callbackType, handler);
}
private interface CallbackHandler {
Object handle(CallbackRequest request);
}
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
private enum CallbackType {
ORG_AUTHORIZATION_FINISH("OrgOpenTsignBiz"),
ORG_PERSON_JOIN("VerifyStaffInfo"),
SEAL_OPERATE("OperateSeal"),
CONTRACT_STATE_CHANGED("FlowStatusChange"),
SUPER_ADMIN_CHANGED("SuperAdminChange");
final String msgType;
static Optional<CallbackType> parse(String msgType) {
// @formatter:off
return Arrays.stream(CallbackType.values())
.filter(type -> type.msgType.equals(msgType))
.findFirst();
// @formatter:on
}
}
}

View File

@ -0,0 +1,51 @@
package cn.axzo.nanopart.ess.server.ess.controller;
import java.util.HashMap;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import com.tencentcloudapi.essbasic.v20210526.models.ChannelDescribeEmployeesResponse;
import com.tencentcloudapi.essbasic.v20210526.models.Staff;
import cn.axzo.framework.domain.web.result.ApiResult;
import cn.axzo.nanopart.ess.server.entity.EssOrg;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.ess.EssClient;
import cn.axzo.nanopart.ess.server.ess.OrgManager;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
*/
@Slf4j
@RestController("essPrivateController")
@RequiredArgsConstructor
public class PrivateController {
private final OrgManager orgManager;
private final EssClient essClient;
@PostMapping("/private/ess/getOrgPersons")
public ApiResult<?> getOrgPersons(@RequestParam("ouId") Long ouId) {
EssOrg org = orgManager.findOrgOrNull(ouId);
BizAssertions.assertNotNull(org, "电子签单位不存在");
EssPerson superAdmin = orgManager.getSuperAdminOrThrow(ouId);
HashMap<String, Staff> staffs = new HashMap<>();
long offset = 0L;
while (true) {
ChannelDescribeEmployeesResponse response = essClient.getOrgPerson(superAdmin, org, offset);
if (response.getEmployees() == null || response.getEmployees().length == 0)
break;
offset = response.getOffset() + 1;
for (Staff staff : response.getEmployees())
staffs.put(staff.getOpenId(), staff);
}
return ApiResult.ok(staffs.values());
}
}

View File

@ -0,0 +1,46 @@
package cn.axzo.nanopart.ess.server.ess.domain;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.entity.domain.OuAndPersonId;
import cn.axzo.nanopart.ess.server.utils.BizAssertions;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Optional;
/**
* @author yanglin
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class EssPersons {
private final List<EssPerson> persons;
public static EssPersons wrap(List<EssPerson> persons) {
return new EssPersons(persons);
}
public EssPerson getOrThrow(Long personId) {
EssPerson person = find(personId).orElse(null);
BizAssertions.assertNotNull(person, "person not found, personId={}", personId);
return person;
}
public Optional<EssPerson> find(Long personId) {
return persons.stream().filter(p -> p.getId().equals(personId)).findFirst();
}
public EssPerson getOrThrow(OuAndPersonId ouAndPersonId) {
EssPerson person = find(ouAndPersonId).orElse(null);
BizAssertions.assertNotNull(person, "person not found, ouId={}, personId={}", ouAndPersonId.getOuId(),
ouAndPersonId.getPersonId());
return person;
}
public Optional<EssPerson> find(OuAndPersonId ouAndPersonId) {
return persons.stream().filter(p -> ouAndPersonId.is(p.getOuId(), p.getPersonId())).findFirst();
}
}

View File

@ -0,0 +1,17 @@
package cn.axzo.nanopart.ess.server.ess.domain;
import cn.axzo.nanopart.ess.server.entity.EssOrg;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor
public class OrgAndPerson {
private final EssOrg org;
private final EssPerson person;
}

View File

@ -0,0 +1,39 @@
package cn.axzo.nanopart.ess.server.ess.domain;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class OrgOpenId {
private final Long ouId;
public static OrgOpenId ofPerson(OrgPerson person) {
return create(person.getOuId());
}
public static OrgOpenId create(Long ouId) {
return new OrgOpenId(ouId);
}
public static OrgOpenId parse(String openId) {
return new OrgOpenId(Long.parseLong(openId));
}
public String toOpenId() {
return ouId + "";
}
@Override
public String toString() {
return toOpenId();
}
}

View File

@ -0,0 +1,47 @@
package cn.axzo.nanopart.ess.server.ess.domain;
import cn.axzo.nanopart.ess.api.domain.OrgPerson;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class PersonOpenId implements OrgPerson {
private static final PersonOpenId NONE = create(0L, 0L);
private final Long ouId;
private final Long personId;
public static PersonOpenId none() {
return NONE;
}
public static PersonOpenId create(OrgPerson person) {
return create(person.getOuId(), person.getPersonId());
}
public static PersonOpenId create(Long ouId, Long personId) {
return new PersonOpenId(ouId, personId);
}
public static PersonOpenId parse(String openId) {
String[] split = openId.split("_");
return new PersonOpenId(Long.parseLong(split[0]), Long.parseLong(split[1]));
}
public String toOpenId() {
return ouId + "_" + personId;
}
@Override
public String toString() {
return toOpenId();
}
}

View File

@ -0,0 +1,17 @@
package cn.axzo.nanopart.ess.server.ess.domain;
import cn.axzo.nanopart.ess.server.entity.EssSeal;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@Getter
@RequiredArgsConstructor
public class SealAndPerson {
private final EssSeal seal;
private final EssSealPerson sealPerson;
}

View File

@ -0,0 +1,31 @@
package cn.axzo.nanopart.ess.server.ess.domain;
import cn.axzo.nanopart.ess.server.entity.EssSealPerson;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import java.util.List;
import java.util.Optional;
/**
* @author yanglin
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class SealPersons {
private final List<EssSealPerson> person;
public static SealPersons wrap(List<EssSealPerson> person) {
return new SealPersons(person);
}
public boolean containsPerson(Long personId) {
return person.stream().anyMatch(p -> p.getPersonId().equals(personId));
}
public Optional<EssSealPerson> find(Long ouId, Long personId) {
return person.stream().filter(p -> p.getOuId().equals(ouId) && p.getPersonId().equals(personId)).findFirst();
}
}

View File

@ -0,0 +1,38 @@
package cn.axzo.nanopart.ess.server.ess.domain;
import com.alibaba.fastjson.JSONObject;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
/**
* @author yanglin
*/
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class StringObject {
private final JSONObject obj;
public static StringObject create() {
return new StringObject(new JSONObject());
}
public static StringObject parse(String jsonStr) {
return new StringObject(JSONObject.parseObject(jsonStr));
}
public JSONObject asJsonObject() {
return obj;
}
public StringObject put(String key, Object value) {
obj.put(key, value);
return this;
}
@Override
public String toString() {
return obj.toJSONString();
}
}

View File

@ -0,0 +1,28 @@
package cn.axzo.nanopart.ess.server.ess.job;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.JSONObject;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.executor.XxlJobExecutor;
import com.xxl.job.core.handler.IJobHandler;
/**
* @author yanglin
*/
@RestController
public class JobHttpHandler {
@PostMapping("/jobs/{jobName}")
ReturnT<String> exec(@PathVariable String jobName,
@RequestBody(required = false) JSONObject paramObj) throws Exception {
IJobHandler jobHandler = XxlJobExecutor.loadJobHandler(jobName);
if (jobHandler == null)
return new ReturnT<>(ReturnT.FAIL_CODE, String.format("找不到job: %s", jobName));
return jobHandler.execute(paramObj == null ? null : paramObj.toJSONString());
}
}

View File

@ -0,0 +1,64 @@
package cn.axzo.nanopart.ess.server.ess.job;
import java.util.List;
import org.springframework.stereotype.Component;
import com.tencentcloudapi.essbasic.v20210526.models.OccupiedSeal;
import com.xxl.job.core.biz.model.ReturnT;
import com.xxl.job.core.handler.annotation.XxlJob;
import cn.axzo.nanopart.ess.api.enums.EssSealState;
import cn.axzo.nanopart.ess.api.utils.YesOrNo;
import cn.axzo.nanopart.ess.server.dao.EssOrgDao;
import cn.axzo.nanopart.ess.server.dao.EssSealDao;
import cn.axzo.nanopart.ess.server.entity.EssOrg;
import cn.axzo.nanopart.ess.server.entity.EssPerson;
import cn.axzo.nanopart.ess.server.entity.EssSeal;
import cn.axzo.nanopart.ess.server.ess.EssClient;
import cn.axzo.nanopart.ess.server.ess.OrgManager;
import cn.axzo.nanopart.ess.server.utils.RecordCursor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
* @author yanglin
*/
@Slf4j
@Component
@RequiredArgsConstructor
@SuppressWarnings("unused")
public class SyncSealNameJob {
private final OrgManager orgManager;
private final EssOrgDao essOrgDao;
private final EssSealDao essSealDao;
private final EssClient essClient;
@XxlJob("syncSealNameJob")
public ReturnT<String> execute(String s) {
RecordCursor<EssOrg> cursor = new RecordCursor<>(EssOrg::getId,
() -> essOrgDao.lambdaQuery().eq(EssOrg::getIsAuthorized, YesOrNo.YES));
for (List<EssOrg> orgs : cursor) {
for (EssOrg org : orgs) {
EssPerson superAdmin = orgManager.findSuperAdmin(org.getOuId()).orElse(null);
if (superAdmin == null) continue;
for (EssSeal seal : essSealDao.getByOuId(org.getOuId())) {
try {
if (seal.getState() == EssSealState.DELETED)
continue;
OccupiedSeal sealInfo = essClient.getSealInfo(superAdmin, seal.getEssSealId());
if (sealInfo == null) continue;
essSealDao.updateSealName(seal.getEssSealId(), sealInfo.getSealName());
log.info("update seal name success, sealId: {}, sealName: {}", seal.getEssSealId(), sealInfo.getSealName());
} catch (Exception e) {
log.warn("get seal info failed, sealId: {}", seal.getEssSealId(), e);
}
}
}
}
return ReturnT.SUCCESS;
}
}

View File

@ -0,0 +1,50 @@
package cn.axzo.nanopart.ess.server.ess.mq;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.stereotype.Component;
import com.alibaba.fastjson.JSON;
import cn.axzo.framework.rocketmq.Event;
import cn.axzo.framework.rocketmq.EventConsumer;
import cn.axzo.framework.rocketmq.EventHandler;
import cn.axzo.nanopart.ess.api.mq.EssContractDownloadPDFEvent;
import cn.axzo.nanopart.ess.server.dao.EssContractDao;
import cn.axzo.nanopart.ess.server.entity.EssContract;
import cn.axzo.nanopart.ess.server.ess.EssService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
/**
*
* IMPORTANT: 目前用的是visa注册的消费组
*
* @author yanglin
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class DownloadContractHandler implements EventHandler, InitializingBean {
private final EventConsumer eventConsumer;
private final EssContractDao essContractDao;
private final EssService essService;
@Override
public void onEvent(Event event, EventConsumer.Context context) {
EssContractDownloadPDFEvent message = event.normalizedData(EssContractDownloadPDFEvent.class);
log.info("receive download contract pdf event: {}", JSON.toJSONString(message));
EssContract contract = essContractDao.findOrNull(message.getContract().getEssContractId());
if (contract == null)
log.info("try download contract pdf but contract not found: {}", JSON.toJSONString(message));
else
essService.downloadContractPDF(contract, message.isRetryDownload());
}
@Override
public void afterPropertiesSet() {
eventConsumer.registerHandler(InternalMQEvent.ESS_CONTRACT_DOWNLOAD_PDF.getEventCode(), this);
}
}

Some files were not shown because too many files have changed in this diff Show More