esign-integration-guide.md 18 KB

e签宝电子签署集成实现文档

一、概述

本文档详细说明如何在贷款管理系统中集成e签宝电子签署功能,实现合同的在线电子签署。系统采用前后端分离架构,后端使用Spring Boot,前端使用uniapp开发微信小程序。

二、准备工作

2.1 注册e签宝账号

  1. 访问e签宝官网:https://www.esign.cn
  2. 注册企业账号并完成实名认证
  3. 创建应用,获取以下信息:

2.2 配置回调地址

在e签宝控制台配置回调地址:

  • 回调URL:https://your-domain.com/api/wechat/esign/callback
  • 回调方式:HTTP POST
  • 回调内容:JSON格式

三、后端实现

3.1 依赖配置

已在pom.xml中添加e签宝SDK依赖:

<dependency>
    <groupId>com.esign</groupId>
    <artifactId>esign-sdk-java</artifactId>
    <version>3.6.0</version>
</dependency>

3.2 配置文件

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

3.3 核心类说明

3.3.1 实体类

EsignFlow (src/main/java/com/loan/system/domain/entity/EsignFlow.java)

  • 存储签署流程信息
  • 包含流程ID、合同ID、签署状态等字段

3.3.2 服务类

EsignService (src/main/java/com/loan/system/service/EsignService.java)

  • 提供签署流程的创建、启动、查询等核心功能

EsignServiceImpl (src/main/java/com/loan/system/service/Impl/EsignServiceImpl.java)

  • 实现e签宝API调用逻辑
  • 处理签署流程状态同步

3.3.3 控制器

EsignController (src/main/java/com/loan/system/controller/wechat/EsignController.java)

  • 提供RESTful API接口
  • 供前端调用

3.4 数据库表结构

系统会自动创建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;

四、前端实现(uniapp)

4.1 签署流程页面

创建签署页面 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>

4.2 WebView页面

创建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>

4.3 创建签署流程

在合同详情页面,添加创建签署流程的功能:

// 创建签署流程
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);
  }
}

五、完整签署流程

5.1 流程步骤

  1. 创建签署流程

    • 前端调用 /wechat/esign/flow/create 接口
    • 后端创建e签宝签署流程,返回flowId
    • 保存流程信息到数据库
  2. 启动签署流程

    • 前端调用 /wechat/esign/flow/{flowId}/start 接口
    • 后端调用e签宝API启动流程
  3. 获取签署链接

    • 前端调用 /wechat/esign/flow/{flowId}/customer/sign-url 接口
    • 后端返回签署链接
  4. 打开签署页面

    • 前端在WebView中打开签署链接
    • 用户在e签宝页面完成签署
  5. 查询签署状态

    • 前端轮询调用 /wechat/esign/flow/{flowId}/status 接口
    • 后端同步e签宝流程状态
  6. 签署完成

    • e签宝回调 /wechat/esign/callback 接口
    • 后端更新流程状态
    • 前端显示签署完成
  7. 下载已签署文档

    • 前端调用 /wechat/esign/flow/{flowId}/download 接口
    • 后端返回文档下载地址

5.2 时序图

前端                后端                e签宝
 |                  |                   |
 |--创建流程-------->|                   |
 |                  |--创建流程--------->|
 |                  |<--返回flowId------|
 |<--返回flowId------|                   |
 |                  |                   |
 |--启动流程-------->|                   |
 |                  |--启动流程--------->|
 |                  |<--启动成功--------|
 |<--启动成功--------|                   |
 |                  |                   |
 |--获取签署链接---->|                   |
 |                  |--获取签署链接---->|
 |                  |<--返回签署链接----|
 |<--返回签署链接----|                   |
 |                  |                   |
 |--打开WebView-----|                   |
 |                  |                   |
 |                  |<--用户签署--------|
 |                  |                   |
 |                  |--回调通知--------->|
 |                  |                   |
 |--查询状态-------->|                   |
 |                  |--查询状态--------->|
 |                  |<--返回状态--------|
 |<--返回状态--------|                   |
 |                  |                   |
 |--下载文档-------->|                   |
 |                  |--获取下载地址----->|
 |                  |<--返回下载地址----|
 |<--返回下载地址----|                   |

六、API接口说明

6.1 创建签署流程

接口地址: 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"
}

6.2 启动签署流程

接口地址: POST /wechat/esign/flow/{flowId}/start

返回结果:

{
  "code": 200,
  "msg": "启动签署流程成功"
}

6.3 获取客户签署链接

接口地址: GET /wechat/esign/flow/{flowId}/customer/sign-url

请求参数:

  • mobile: 手机号(必填)
  • name: 姓名(必填)
  • accountId: 账号ID(可选)

返回结果:

{
  "code": 200,
  "msg": "获取成功",
  "data": "https://esign.cn/sign/xxx"
}

6.4 查询流程状态

接口地址: GET /wechat/esign/flow/{flowId}/status

返回结果:

{
  "code": 200,
  "msg": "查询成功",
  "data": {
    "flowStatus": 2,
    "flowStatusDesc": "签署完成",
    "signers": [...]
  }
}

6.5 下载已签署文档

接口地址: GET /wechat/esign/flow/{flowId}/download

返回结果:

{
  "code": 200,
  "msg": "获取下载地址成功",
  "data": "https://esign.cn/download/xxx"
}

6.6 撤销签署流程

接口地址: POST /wechat/esign/flow/{flowId}/revoke

请求参数:

  • revokeReason: 撤销原因(必填)

返回结果:

{
  "code": 200,
  "msg": "撤销成功"
}

6.7 e签宝回调接口

接口地址: POST /wechat/esign/callback

请求参数(e签宝回调):

{
  "flowId": "flowId123456",
  "action": "SIGN_FLOW_FINISH",
  "flowStatus": 2,
  "signerAccountId": "accountId123",
  "timestamp": 1640995200000
}

七、注意事项

7.1 合同文件上传

在创建签署流程前,需要先将合同文件上传到e签宝:

  1. 调用e签宝文件上传接口
  2. 获取文件ID(fileId)
  3. 使用fileId创建签署流程

7.2 签署位置配置

签署位置(坐标)需要根据实际合同模板调整:

  • EsignServiceImpl.createSignFields()方法中配置
  • 支持多页文档,需要指定页码
  • 支持多个签署区域

7.3 错误处理

  1. 网络错误:实现重试机制
  2. API错误:记录错误日志,返回友好提示
  3. 签署超时:设置合理的签署截止时间
  4. 回调失败:实现回调重试机制

7.4 安全性

  1. 接口鉴权:所有接口需要JWT认证
  2. 参数校验:验证手机号、身份证号格式
  3. 回调验证:验证e签宝回调签名
  4. 数据加密:敏感信息加密存储

7.5 性能优化

  1. 状态轮询:使用合理的轮询间隔(建议3-5秒)
  2. 缓存机制:缓存访问令牌,避免频繁获取
  3. 异步处理:回调处理使用异步方式
  4. 数据库索引:为flowId、contractId等字段建立索引

八、测试建议

8.1 单元测试

  • 测试服务类方法
  • 测试API接口
  • 测试异常情况

8.2 集成测试

  • 测试完整签署流程
  • 测试回调处理
  • 测试并发场景

8.3 沙箱环境

  • 使用e签宝沙箱环境进行测试
  • 验证各种签署场景
  • 测试异常情况处理

九、常见问题

9.1 签署链接无法打开

  • 检查URL是否正确编码
  • 检查小程序是否配置了业务域名
  • 检查网络连接

9.2 签署状态不同步

  • 检查回调接口是否正常
  • 检查轮询逻辑是否正确
  • 检查数据库更新是否成功

9.3 文档下载失败

  • 检查文档是否已签署完成
  • 检查下载地址是否有效
  • 检查文件权限

十、参考资料


文档版本: v1.0
最后更新: 2024-01-01
维护人员: 开发团队