本文档详细说明如何在贷款管理系统中集成e签宝电子签署功能,实现合同的在线电子签署。系统采用前后端分离架构,后端使用Spring Boot,前端使用uniapp开发微信小程序。
在e签宝控制台配置回调地址:
https://your-domain.com/api/wechat/esign/callback已在pom.xml中添加e签宝SDK依赖:
<dependency>
<groupId>com.esign</groupId>
<artifactId>esign-sdk-java</artifactId>
<version>3.6.0</version>
</dependency>
在application-dev.yaml中配置e签宝参数:
system:
esign:
app-id: your-app-id
app-secret: your-app-secret
api-url: https://smlopenapi.esign.cn
project-id: your-project-id
enabled: true
EsignFlow (src/main/java/com/loan/system/domain/entity/EsignFlow.java)
EsignService (src/main/java/com/loan/system/service/EsignService.java)
EsignServiceImpl (src/main/java/com/loan/system/service/Impl/EsignServiceImpl.java)
EsignController (src/main/java/com/loan/system/controller/wechat/EsignController.java)
系统会自动创建esign_flow表,主要字段:
CREATE TABLE `esign_flow` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`flow_id` varchar(100) NOT NULL COMMENT 'e签宝流程ID',
`contract_id` bigint(20) COMMENT '合同ID',
`case_id` bigint(20) COMMENT '案件ID',
`flow_status` int(11) COMMENT '流程状态:0-草稿 1-签署中 2-签署完成 3-签署失败 4-已撤销 5-已过期 6-已拒签',
`flow_status_desc` varchar(50) COMMENT '流程状态描述',
`document_id` varchar(100) COMMENT '签署文档ID',
`document_url` varchar(500) COMMENT '签署文档下载地址',
`document_name` varchar(200) COMMENT '签署文档名称',
`customer_account_id` varchar(100) COMMENT '客户签署人账号ID',
`business_account_id` varchar(100) COMMENT '业务方签署人账号ID',
`customer_sign_status` int(11) COMMENT '客户签署状态:0-待签署 1-已签署 2-已拒签 3-已过期',
`business_sign_status` int(11) COMMENT '业务方签署状态',
`customer_sign_time` varchar(50) COMMENT '客户签署时间',
`business_sign_time` varchar(50) COMMENT '业务方签署时间',
`create_time` varchar(50) COMMENT '创建时间',
`update_time` varchar(50) COMMENT '更新时间',
`is_delete` tinyint(1) DEFAULT 0 COMMENT '是否删除',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_flow_id` (`flow_id`),
KEY `idx_contract_id` (`contract_id`),
KEY `idx_case_id` (`case_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
创建签署页面 pages/contract/sign.vue:
<template>
<view class="sign-container">
<view class="contract-info">
<text class="title">合同签署</text>
<text class="contract-name">{{ contractName }}</text>
</view>
<view class="sign-status">
<text>签署状态:{{ signStatusText }}</text>
</view>
<view class="sign-buttons" v-if="showSignButton">
<button class="sign-btn" @click="handleSign">开始签署</button>
</view>
<view class="sign-progress" v-if="flowDetail">
<view class="progress-item">
<text>客户签署:{{ customerSignStatusText }}</text>
</view>
<view class="progress-item">
<text>业务方签署:{{ businessSignStatusText }}</text>
</view>
</view>
</view>
</template>
<script>
export default {
data() {
return {
contractId: null,
flowId: null,
contractName: '',
signStatusText: '待签署',
showSignButton: true,
flowDetail: null,
customerSignStatusText: '待签署',
businessSignStatusText: '待签署'
}
},
onLoad(options) {
this.contractId = options.contractId;
this.loadFlowDetail();
},
methods: {
// 加载签署流程详情
async loadFlowDetail() {
try {
const res = await this.$http.get(`/wechat/esign/flow/contract/${this.contractId}`);
if (res.code === 200) {
this.flowDetail = res.data;
this.flowId = res.data.flowId;
this.updateSignStatus(res.data);
}
} catch (error) {
console.error('加载流程详情失败', error);
uni.showToast({
title: '加载失败',
icon: 'none'
});
}
},
// 更新签署状态
updateSignStatus(flowDetail) {
const statusMap = {
0: '草稿',
1: '签署中',
2: '签署完成',
3: '签署失败',
4: '已撤销',
5: '已过期',
6: '已拒签'
};
this.signStatusText = statusMap[flowDetail.flowStatus] || '未知';
const signStatusMap = {
0: '待签署',
1: '已签署',
2: '已拒签',
3: '已过期'
};
this.customerSignStatusText = signStatusMap[flowDetail.customerSignStatus] || '未知';
this.businessSignStatusText = signStatusMap[flowDetail.businessSignStatus] || '未知';
// 如果流程已完成或已撤销,隐藏签署按钮
if (flowDetail.flowStatus === 2 || flowDetail.flowStatus === 4) {
this.showSignButton = false;
}
},
// 处理签署
async handleSign() {
try {
// 获取用户信息
const userInfo = uni.getStorageSync('userInfo');
// 获取签署链接
const res = await this.$http.get(`/wechat/esign/flow/${this.flowId}/customer/sign-url`, {
params: {
mobile: userInfo.mobile,
name: userInfo.name
}
});
if (res.code === 200) {
const signUrl = res.data;
// 在小程序webview中打开签署页面
uni.navigateTo({
url: `/pages/webview/webview?url=${encodeURIComponent(signUrl)}&title=合同签署`
});
} else {
uni.showToast({
title: res.msg || '获取签署链接失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取签署链接失败', error);
uni.showToast({
title: '获取签署链接失败',
icon: 'none'
});
}
},
// 轮询查询签署状态
startPolling() {
this.pollTimer = setInterval(async () => {
if (this.flowId) {
try {
const res = await this.$http.get(`/wechat/esign/flow/${this.flowId}/status`);
if (res.code === 200) {
this.updateSignStatus(res.data);
// 如果签署完成,停止轮询
if (res.data.flowStatus === 2) {
clearInterval(this.pollTimer);
uni.showToast({
title: '签署完成',
icon: 'success'
});
// 返回合同列表
setTimeout(() => {
uni.navigateBack();
}, 1500);
}
}
} catch (error) {
console.error('查询状态失败', error);
}
}
}, 3000); // 每3秒查询一次
}
},
onShow() {
// 页面显示时开始轮询
if (this.flowId) {
this.startPolling();
}
},
onHide() {
// 页面隐藏时停止轮询
if (this.pollTimer) {
clearInterval(this.pollTimer);
}
}
}
</script>
<style scoped>
.sign-container {
padding: 20rpx;
}
.contract-info {
background: #fff;
padding: 30rpx;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.title {
font-size: 36rpx;
font-weight: bold;
display: block;
margin-bottom: 20rpx;
}
.contract-name {
font-size: 28rpx;
color: #666;
}
.sign-status {
background: #fff;
padding: 20rpx 30rpx;
border-radius: 10rpx;
margin-bottom: 20rpx;
}
.sign-buttons {
margin: 40rpx 0;
}
.sign-btn {
width: 100%;
background: #007aff;
color: #fff;
border-radius: 10rpx;
padding: 25rpx 0;
}
.sign-progress {
background: #fff;
padding: 30rpx;
border-radius: 10rpx;
}
.progress-item {
padding: 15rpx 0;
border-bottom: 1px solid #f0f0f0;
}
.progress-item:last-child {
border-bottom: none;
}
</style>
创建WebView页面用于显示签署页面 pages/webview/webview.vue:
<template>
<view class="webview-container">
<web-view :src="url"></web-view>
</view>
</template>
<script>
export default {
data() {
return {
url: ''
}
},
onLoad(options) {
this.url = decodeURIComponent(options.url || '');
if (options.title) {
uni.setNavigationBarTitle({
title: decodeURIComponent(options.title)
});
}
}
}
</script>
<style scoped>
.webview-container {
width: 100%;
height: 100vh;
}
</style>
在合同详情页面,添加创建签署流程的功能:
// 创建签署流程
async createSignFlow() {
try {
uni.showLoading({
title: '创建中...'
});
// 获取合同信息
const contractInfo = this.contractDetail;
// 获取客户信息
const customerInfo = this.customerInfo;
const params = {
contractId: this.contractId,
caseId: this.caseId,
documentUrl: contractInfo.filePath, // 合同文件URL
documentName: contractInfo.contractName,
customerName: customerInfo.name,
customerMobile: customerInfo.mobile,
customerIdNumber: customerInfo.idNumber,
businessName: '业务方名称', // 从配置或用户信息获取
businessMobile: '业务方手机号',
signDeadline: Date.now() + 7 * 24 * 60 * 60 * 1000 // 7天后过期
};
const res = await this.$http.post('/wechat/esign/flow/create', params);
uni.hideLoading();
if (res.code === 200) {
uni.showToast({
title: '创建成功',
icon: 'success'
});
// 启动签署流程
await this.startSignFlow(res.data);
// 跳转到签署页面
uni.navigateTo({
url: `/pages/contract/sign?contractId=${this.contractId}`
});
} else {
uni.showToast({
title: res.msg || '创建失败',
icon: 'none'
});
}
} catch (error) {
uni.hideLoading();
console.error('创建签署流程失败', error);
uni.showToast({
title: '创建失败',
icon: 'none'
});
}
},
// 启动签署流程
async startSignFlow(flowId) {
try {
const res = await this.$http.post(`/wechat/esign/flow/${flowId}/start`);
if (res.code !== 200) {
console.error('启动签署流程失败', res);
}
} catch (error) {
console.error('启动签署流程失败', error);
}
}
创建签署流程
/wechat/esign/flow/create 接口启动签署流程
/wechat/esign/flow/{flowId}/start 接口获取签署链接
/wechat/esign/flow/{flowId}/customer/sign-url 接口打开签署页面
查询签署状态
/wechat/esign/flow/{flowId}/status 接口签署完成
/wechat/esign/callback 接口下载已签署文档
/wechat/esign/flow/{flowId}/download 接口前端 后端 e签宝
| | |
|--创建流程-------->| |
| |--创建流程--------->|
| |<--返回flowId------|
|<--返回flowId------| |
| | |
|--启动流程-------->| |
| |--启动流程--------->|
| |<--启动成功--------|
|<--启动成功--------| |
| | |
|--获取签署链接---->| |
| |--获取签署链接---->|
| |<--返回签署链接----|
|<--返回签署链接----| |
| | |
|--打开WebView-----| |
| | |
| |<--用户签署--------|
| | |
| |--回调通知--------->|
| | |
|--查询状态-------->| |
| |--查询状态--------->|
| |<--返回状态--------|
|<--返回状态--------| |
| | |
|--下载文档-------->| |
| |--获取下载地址----->|
| |<--返回下载地址----|
|<--返回下载地址----| |
接口地址: POST /wechat/esign/flow/create
请求参数:
{
"contractId": 123,
"caseId": 456,
"documentUrl": "https://example.com/contract.pdf",
"documentName": "借款合同",
"customerName": "张三",
"customerMobile": "13800138000",
"customerIdNumber": "110101199001011234",
"businessName": "业务方",
"businessMobile": "13900139000",
"businessIdNumber": "110101199001011235",
"signDeadline": 1640995200000,
"remark": "备注信息"
}
返回结果:
{
"code": 200,
"msg": "创建签署流程成功",
"data": "flowId123456"
}
接口地址: POST /wechat/esign/flow/{flowId}/start
返回结果:
{
"code": 200,
"msg": "启动签署流程成功"
}
接口地址: GET /wechat/esign/flow/{flowId}/customer/sign-url
请求参数:
mobile: 手机号(必填)name: 姓名(必填)accountId: 账号ID(可选)返回结果:
{
"code": 200,
"msg": "获取成功",
"data": "https://esign.cn/sign/xxx"
}
接口地址: GET /wechat/esign/flow/{flowId}/status
返回结果:
{
"code": 200,
"msg": "查询成功",
"data": {
"flowStatus": 2,
"flowStatusDesc": "签署完成",
"signers": [...]
}
}
接口地址: GET /wechat/esign/flow/{flowId}/download
返回结果:
{
"code": 200,
"msg": "获取下载地址成功",
"data": "https://esign.cn/download/xxx"
}
接口地址: POST /wechat/esign/flow/{flowId}/revoke
请求参数:
revokeReason: 撤销原因(必填)返回结果:
{
"code": 200,
"msg": "撤销成功"
}
接口地址: POST /wechat/esign/callback
请求参数(e签宝回调):
{
"flowId": "flowId123456",
"action": "SIGN_FLOW_FINISH",
"flowStatus": 2,
"signerAccountId": "accountId123",
"timestamp": 1640995200000
}
在创建签署流程前,需要先将合同文件上传到e签宝:
签署位置(坐标)需要根据实际合同模板调整:
EsignServiceImpl.createSignFields()方法中配置文档版本: v1.0
最后更新: 2024-01-01
维护人员: 开发团队