电子签名

This commit is contained in:
2026-05-14 16:10:51 +08:00
parent 1c9cbfae59
commit 8f9d286bec
9 changed files with 671 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
# 电子签名后端对接说明
## 目标
支持前端在“点击使用电子签名”时,自动获取当前登录人员已配置的电子签名并回填。
当当前人员未配置电子签名时,后端返回提示:
`暂未设置电子签名,请在个人资料内设置后重试或使用手动签名`
## 数据设计
### 表名
`sys_user_electronic_signature`
### 核心设计说明
- 一条记录对应“当前登录人员”的一份电子签名配置
- 人员唯一性通过 `tenant_id + person_key` 控制
- `person_key` 生成规则:`loginPort:companyId:businessUserId`
-`businessUserId` 为空时,回退为 `loginPort:companyId:userId`
- 签名文件本体继续复用现有 OSS 表,不重复存文件内容,只存 `sign_oss_id`
- 支持“清空签名”:保留记录,将 `status` 置为 `0`,并清空 `sign_oss_id`
### 主要字段
- `person_key`:当前登录人员唯一键
- `user_id`:系统用户 ID
- `business_user_id`:业务人员 ID例如驾驶员/安全员 ID
- `company_id`:当前企业 ID
- `login_port`:当前登录端口
- `sign_oss_id`:电子签名图片 OSS ID
- `sign_name`:签署人名称快照
- `status``1=已设置``0=未设置`
建表脚本见:
- `sql/1289/3_user_electronic_signature.sql`
## 接口列表
统一前缀:
- `/system/user/profile`
### 1. 查询当前登录人员电子签名信息
- 方法:`GET`
- 路径:`/system/user/profile/electronic-signature/info`
- 用途:个人资料页进入时查询当前签名配置状态
- 特点:即使未配置,也返回成功,前端通过 `data.configured` 判断
响应示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"id": 1,
"configured": true,
"personKey": "port-driver:1001:DRV0001",
"userId": 20001,
"businessUserId": "DRV0001",
"companyId": 1001,
"loginPort": "port-driver",
"signOssId": 98765,
"signUrl": "https://xxx/signature.png",
"signName": "张三",
"status": "1"
}
}
```
未配置时示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"configured": false,
"personKey": "port-driver:1001:DRV0001",
"userId": 20001,
"businessUserId": "DRV0001",
"companyId": 1001,
"loginPort": "port-driver",
"signOssId": null,
"signUrl": null,
"signName": "张三",
"status": "0"
}
}
```
### 2. 业务页获取当前电子签名并回填
- 方法:`GET`
- 路径:`/system/user/profile/electronic-signature/current`
- 用途:点击“使用电子签名”时直接调用
- 特点:已配置时返回签名信息;未配置时直接返回失败提示,前端弹出 `msg` 即可
已配置响应示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"configured": true,
"signOssId": 98765,
"signUrl": "https://xxx/signature.png",
"signName": "张三"
}
}
```
未配置响应示例:
```json
{
"code": 500,
"msg": "暂未设置电子签名,请在个人资料内设置后重试或使用手动签名",
"data": null
}
```
前端建议处理:
- 当接口成功时,将 `data.signUrl` 回填到下方签名展示区域
- 同时保留 `data.signOssId`,提交业务表单时直接透传
- 当接口失败时,直接提示后端返回的 `msg`
### 3. 保存当前登录人员电子签名
- 方法:`PUT`
- 路径:`/system/user/profile/electronic-signature`
- 用途:个人资料页设置或更换电子签名
请求参数:
```json
{
"signOssId": 98765
}
```
说明:
- `signOssId` 必填
- 文件需要先通过现有 OSS 上传接口上传成功
- 后端会校验该文件是否存在,且是否为图片格式
成功响应示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": {
"configured": true,
"signOssId": 98765,
"signUrl": "https://xxx/signature.png",
"signName": "张三",
"status": "1"
}
}
```
### 4. 清空当前登录人员电子签名
- 方法:`DELETE`
- 路径:`/system/user/profile/electronic-signature`
- 用途:个人资料页删除当前电子签名配置
成功响应示例:
```json
{
"code": 200,
"msg": "操作成功",
"data": null
}
```
## 与前端交互建议
### 个人资料页
1. 页面加载时调用 `GET /electronic-signature/info`
2.`configured = true`,展示当前签名图片
3. 用户更换签名时,先调用现有 OSS 上传接口,再把返回的 `ossId` 提交到保存接口
4. 用户删除签名时,调用清空接口
### 业务签署页
1. 点击“使用电子签名”
2. 调用 `GET /electronic-signature/current`
3. 若成功:
- 回填 `signUrl`
- 保存 `signOssId`
4. 若失败:
- 直接提示 `msg`
- 保留“手动签名”入口
## 依赖的现有上传接口
上传签名图片时继续复用现有 OSS 上传能力:
- 方法:`POST`
- 路径:`/resource/oss/upload`
- 表单字段:`file`
返回示例中的 `data.ossId` 即为后续保存电子签名所需的 `signOssId`

View File

@@ -0,0 +1,25 @@
CREATE TABLE IF NOT EXISTS `sys_user_electronic_signature` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`tenant_id` varchar(20) NOT NULL DEFAULT '000000' COMMENT '租户编号',
`person_key` varchar(128) NOT NULL COMMENT '人员唯一标识规则loginPort:companyId:businessUserId业务ID为空时回退为 userId',
`user_id` bigint NOT NULL COMMENT '系统用户ID',
`business_user_id` varchar(64) DEFAULT NULL COMMENT '业务人员ID',
`company_id` bigint DEFAULT NULL COMMENT '企业ID',
`login_port` varchar(64) NOT NULL COMMENT '登录端口',
`sign_oss_id` bigint DEFAULT NULL COMMENT '电子签名图片OSS ID',
`sign_name` varchar(64) DEFAULT NULL COMMENT '签署人名称快照',
`status` char(1) NOT NULL DEFAULT '1' COMMENT '状态1=已设置 0=未设置',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
`create_dept` bigint DEFAULT NULL COMMENT '创建部门',
`create_by` bigint DEFAULT NULL COMMENT '创建人',
`create_by_name` varchar(64) DEFAULT NULL COMMENT '创建人名称',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新人',
`update_by_name` varchar(64) DEFAULT NULL COMMENT '更新人名称',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_tenant_person_key` (`tenant_id`, `person_key`),
KEY `idx_user_id` (`user_id`),
KEY `idx_business_user_id` (`business_user_id`),
KEY `idx_company_id` (`company_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='用户电子签名配置表';

View File

@@ -17,11 +17,14 @@ import org.dromara.common.mybatis.helper.DataPermissionHelper;
import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.common.web.core.BaseController; import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.bo.SysUserBo; import org.dromara.system.domain.bo.SysUserBo;
import org.dromara.system.domain.bo.SysUserElectronicSignatureBo;
import org.dromara.system.domain.bo.SysUserPasswordBo; import org.dromara.system.domain.bo.SysUserPasswordBo;
import org.dromara.system.domain.bo.SysUserProfileBo; import org.dromara.system.domain.bo.SysUserProfileBo;
import org.dromara.system.domain.vo.ProfileUserVo; import org.dromara.system.domain.vo.ProfileUserVo;
import org.dromara.system.domain.vo.SysOssVo; import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.domain.vo.SysUserElectronicSignatureVo;
import org.dromara.system.domain.vo.SysUserVo; import org.dromara.system.domain.vo.SysUserVo;
import org.dromara.system.service.ISysUserElectronicSignatureService;
import org.dromara.system.service.ISysOssService; import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.ISysUserService; import org.dromara.system.service.ISysUserService;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
@@ -45,6 +48,7 @@ public class SysProfileController extends BaseController {
private final ISysUserService userService; private final ISysUserService userService;
private final ISysOssService ossService; private final ISysOssService ossService;
private final ISysUserLoginPortService sysUserLoginPortService; private final ISysUserLoginPortService sysUserLoginPortService;
private final ISysUserElectronicSignatureService userElectronicSignatureService;
/** /**
* 个人信息 * 个人信息
@@ -93,6 +97,46 @@ public class SysProfileController extends BaseController {
return R.fail("修改个人信息异常,请联系管理员"); return R.fail("修改个人信息异常,请联系管理员");
} }
/**
* 当前登录人员电子签名信息
*/
@GetMapping("/electronic-signature/info")
public R<SysUserElectronicSignatureVo> electronicSignatureInfo() {
return R.ok(userElectronicSignatureService.getCurrentUserSignature());
}
/**
* 获取当前登录人员可直接用于业务回填的电子签名
*/
@GetMapping("/electronic-signature/current")
public R<SysUserElectronicSignatureVo> currentElectronicSignature() {
SysUserElectronicSignatureVo vo = userElectronicSignatureService.getCurrentUserSignature();
if (Boolean.FALSE.equals(vo.getConfigured())) {
return R.fail("暂未设置电子签名,请在个人资料内设置后重试或使用手动签名");
}
return R.ok(vo);
}
/**
* 保存当前登录人员电子签名
*/
@RepeatSubmit
@Log(title = "电子签名", businessType = BusinessType.UPDATE)
@PutMapping("/electronic-signature")
public R<SysUserElectronicSignatureVo> saveElectronicSignature(@Validated @RequestBody SysUserElectronicSignatureBo bo) {
return R.ok(userElectronicSignatureService.saveCurrentUserSignature(bo));
}
/**
* 清空当前登录人员电子签名
*/
@RepeatSubmit
@Log(title = "电子签名", businessType = BusinessType.UPDATE)
@DeleteMapping("/electronic-signature")
public R<Void> clearElectronicSignature() {
return toAjax(userElectronicSignatureService.clearCurrentUserSignature());
}
/** /**
* 重置密码 * 重置密码
* *

View File

@@ -0,0 +1,74 @@
package org.dromara.system.domain;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.dromara.common.tenant.core.TenantEntity;
import java.io.Serial;
/**
* 用户电子签名配置对象 sys_user_electronic_signature
*
* @author shihongwei
*/
@Data
@EqualsAndHashCode(callSuper = true)
@TableName("sys_user_electronic_signature")
public class SysUserElectronicSignature extends TenantEntity {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id")
private Long id;
/**
* 人员唯一标识
*/
private String personKey;
/**
* 系统用户ID
*/
private Long userId;
/**
* 业务人员ID
*/
private String businessUserId;
/**
* 企业ID
*/
private Long companyId;
/**
* 登录端口
*/
private String loginPort;
/**
* 电子签名图片OSS ID
*/
private Long signOssId;
/**
* 签署人名称快照
*/
private String signName;
/**
* 状态1=已设置 0=未设置
*/
private String status;
/**
* 备注
*/
private String remark;
}

View File

@@ -0,0 +1,19 @@
package org.dromara.system.domain.bo;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 当前登录人员电子签名保存参数
*
* @author shihongwei
*/
@Data
public class SysUserElectronicSignatureBo {
/**
* 电子签名图片OSS ID
*/
@NotNull(message = "电子签名图片不能为空")
private Long signOssId;
}

View File

@@ -0,0 +1,79 @@
package org.dromara.system.domain.vo;
import io.github.linpeilie.annotations.AutoMapper;
import lombok.Data;
import org.dromara.common.translation.annotation.Translation;
import org.dromara.common.translation.constant.TransConstant;
import org.dromara.system.domain.SysUserElectronicSignature;
import java.io.Serial;
import java.io.Serializable;
/**
* 当前登录人员电子签名信息
*
* @author shihongwei
*/
@Data
@AutoMapper(target = SysUserElectronicSignature.class)
public class SysUserElectronicSignatureVo implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
private Long id;
/**
* 是否已配置电子签名
*/
private Boolean configured;
/**
* 人员唯一标识
*/
private String personKey;
/**
* 系统用户ID
*/
private Long userId;
/**
* 业务人员ID
*/
private String businessUserId;
/**
* 企业ID
*/
private Long companyId;
/**
* 登录端口
*/
private String loginPort;
/**
* 电子签名图片OSS ID
*/
private Long signOssId;
/**
* 电子签名图片URL
*/
@Translation(type = TransConstant.OSS_ID_TO_URL, mapper = "signOssId")
private String signUrl;
/**
* 签署人名称
*/
private String signName;
/**
* 状态1=已设置 0=未设置
*/
private String status;
}

View File

@@ -0,0 +1,15 @@
package org.dromara.system.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
import org.dromara.system.domain.SysUserElectronicSignature;
import org.dromara.system.domain.vo.SysUserElectronicSignatureVo;
/**
* 用户电子签名配置Mapper接口
*
* @author shihongwei
*/
@Mapper
public interface SysUserElectronicSignatureMapper extends BaseMapperPlus<SysUserElectronicSignature, SysUserElectronicSignatureVo> {
}

View File

@@ -0,0 +1,34 @@
package org.dromara.system.service;
import org.dromara.system.domain.bo.SysUserElectronicSignatureBo;
import org.dromara.system.domain.vo.SysUserElectronicSignatureVo;
/**
* 当前登录人员电子签名服务
*
* @author shihongwei
*/
public interface ISysUserElectronicSignatureService {
/**
* 获取当前登录人员电子签名信息
*
* @return 电子签名信息
*/
SysUserElectronicSignatureVo getCurrentUserSignature();
/**
* 保存当前登录人员电子签名
*
* @param bo 保存参数
* @return 保存后的电子签名信息
*/
SysUserElectronicSignatureVo saveCurrentUserSignature(SysUserElectronicSignatureBo bo);
/**
* 清空当前登录人员电子签名
*
* @return 是否成功
*/
Boolean clearCurrentUserSignature();
}

View File

@@ -0,0 +1,166 @@
package org.dromara.system.service.impl;
import cn.hutool.core.io.FileUtil;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.model.LoginUser;
import org.dromara.common.core.exception.ServiceException;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.core.utils.file.MimeTypeUtils;
import org.dromara.common.satoken.utils.LoginHelper;
import org.dromara.system.domain.SysUserElectronicSignature;
import org.dromara.system.domain.bo.SysUserElectronicSignatureBo;
import org.dromara.system.domain.vo.SysOssVo;
import org.dromara.system.domain.vo.SysUserElectronicSignatureVo;
import org.dromara.system.mapper.SysUserElectronicSignatureMapper;
import org.dromara.system.service.ISysOssService;
import org.dromara.system.service.ISysUserElectronicSignatureService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 当前登录人员电子签名服务实现
*
* @author shihongwei
*/
@RequiredArgsConstructor
@Service
public class SysUserElectronicSignatureServiceImpl implements ISysUserElectronicSignatureService {
private static final String STATUS_ENABLED = "1";
private static final String STATUS_DISABLED = "0";
private final SysUserElectronicSignatureMapper baseMapper;
private final ISysOssService ossService;
@Override
public SysUserElectronicSignatureVo getCurrentUserSignature() {
LoginUser loginUser = getRequiredLoginUser();
String personKey = buildPersonKey(loginUser);
SysUserElectronicSignature entity = baseMapper.selectOne(
Wrappers.<SysUserElectronicSignature>lambdaQuery()
.eq(SysUserElectronicSignature::getTenantId, loginUser.getTenantId())
.eq(SysUserElectronicSignature::getPersonKey, personKey)
.last("limit 1")
);
if (entity == null) {
return buildEmptyVo(loginUser, personKey);
}
SysUserElectronicSignatureVo vo = MapstructUtils.convert(entity, SysUserElectronicSignatureVo.class);
vo.setConfigured(isConfigured(entity));
if (StringUtils.isBlank(vo.getSignName())) {
vo.setSignName(resolveSignName(loginUser));
}
return vo;
}
@Override
@Transactional(rollbackFor = Exception.class)
public SysUserElectronicSignatureVo saveCurrentUserSignature(SysUserElectronicSignatureBo bo) {
LoginUser loginUser = getRequiredLoginUser();
validateSignatureOss(bo.getSignOssId());
String personKey = buildPersonKey(loginUser);
SysUserElectronicSignature entity = baseMapper.selectOne(
Wrappers.<SysUserElectronicSignature>lambdaQuery()
.eq(SysUserElectronicSignature::getTenantId, loginUser.getTenantId())
.eq(SysUserElectronicSignature::getPersonKey, personKey)
.last("limit 1")
);
if (entity == null) {
entity = new SysUserElectronicSignature();
entity.setTenantId(loginUser.getTenantId());
entity.setPersonKey(personKey);
}
entity.setUserId(loginUser.getUserId());
entity.setBusinessUserId(loginUser.getBusinessUserId());
entity.setCompanyId(loginUser.getCompanyId());
entity.setLoginPort(defaultLoginPort(loginUser));
entity.setSignOssId(bo.getSignOssId());
entity.setSignName(resolveSignName(loginUser));
entity.setStatus(STATUS_ENABLED);
if (entity.getId() == null) {
baseMapper.insert(entity);
} else {
baseMapper.updateById(entity);
}
return getCurrentUserSignature();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Boolean clearCurrentUserSignature() {
LoginUser loginUser = getRequiredLoginUser();
String personKey = buildPersonKey(loginUser);
SysUserElectronicSignature entity = baseMapper.selectOne(
Wrappers.<SysUserElectronicSignature>lambdaQuery()
.eq(SysUserElectronicSignature::getTenantId, loginUser.getTenantId())
.eq(SysUserElectronicSignature::getPersonKey, personKey)
.last("limit 1")
);
if (entity == null) {
return true;
}
entity.setUserId(loginUser.getUserId());
entity.setBusinessUserId(loginUser.getBusinessUserId());
entity.setCompanyId(loginUser.getCompanyId());
entity.setLoginPort(defaultLoginPort(loginUser));
entity.setSignOssId(null);
entity.setSignName(resolveSignName(loginUser));
entity.setStatus(STATUS_DISABLED);
return baseMapper.updateById(entity) > 0;
}
private boolean isConfigured(SysUserElectronicSignature entity) {
return entity != null
&& STATUS_ENABLED.equals(entity.getStatus())
&& entity.getSignOssId() != null;
}
private SysUserElectronicSignatureVo buildEmptyVo(LoginUser loginUser, String personKey) {
SysUserElectronicSignatureVo vo = new SysUserElectronicSignatureVo();
vo.setConfigured(false);
vo.setPersonKey(personKey);
vo.setUserId(loginUser.getUserId());
vo.setBusinessUserId(loginUser.getBusinessUserId());
vo.setCompanyId(loginUser.getCompanyId());
vo.setLoginPort(defaultLoginPort(loginUser));
vo.setSignName(resolveSignName(loginUser));
vo.setStatus(STATUS_DISABLED);
return vo;
}
private void validateSignatureOss(Long signOssId) {
SysOssVo ossVo = ossService.getById(signOssId);
if (ossVo == null) {
throw new ServiceException("电子签名文件不存在,请重新上传后再试");
}
String fileName = StringUtils.defaultIfBlank(ossVo.getOriginalName(), ossVo.getFileName());
String extension = FileUtil.extName(fileName);
if (!StringUtils.equalsAnyIgnoreCase(extension, MimeTypeUtils.IMAGE_EXTENSION)) {
throw new ServiceException("电子签名必须为图片格式");
}
}
private LoginUser getRequiredLoginUser() {
LoginUser loginUser = LoginHelper.getLoginUser();
if (loginUser == null || loginUser.getUserId() == null) {
throw new ServiceException("用户未登录");
}
return loginUser;
}
private String buildPersonKey(LoginUser loginUser) {
String businessUserId = StringUtils.defaultIfBlank(loginUser.getBusinessUserId(), String.valueOf(loginUser.getUserId()));
String companyId = loginUser.getCompanyId() == null ? "0" : String.valueOf(loginUser.getCompanyId());
return defaultLoginPort(loginUser) + ":" + companyId + ":" + businessUserId;
}
private String defaultLoginPort(LoginUser loginUser) {
return StringUtils.defaultIfBlank(loginUser.getLoginPort(), "default");
}
private String resolveSignName(LoginUser loginUser) {
return StringUtils.defaultIfBlank(loginUser.getNickname(), loginUser.getUsername());
}
}