diff --git a/docs/electronic_signature_api.md b/docs/electronic_signature_api.md new file mode 100644 index 0000000..72e85b2 --- /dev/null +++ b/docs/electronic_signature_api.md @@ -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`。 diff --git a/sql/1289/3_user_electronic_signature.sql b/sql/1289/3_user_electronic_signature.sql new file mode 100644 index 0000000..5e958b6 --- /dev/null +++ b/sql/1289/3_user_electronic_signature.sql @@ -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='用户电子签名配置表'; diff --git a/src/main/java/org/dromara/system/controller/system/SysProfileController.java b/src/main/java/org/dromara/system/controller/system/SysProfileController.java index ef4c18b..a1793c0 100644 --- a/src/main/java/org/dromara/system/controller/system/SysProfileController.java +++ b/src/main/java/org/dromara/system/controller/system/SysProfileController.java @@ -17,11 +17,14 @@ import org.dromara.common.mybatis.helper.DataPermissionHelper; import org.dromara.common.satoken.utils.LoginHelper; import org.dromara.common.web.core.BaseController; 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.SysUserProfileBo; import org.dromara.system.domain.vo.ProfileUserVo; 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.service.ISysUserElectronicSignatureService; import org.dromara.system.service.ISysOssService; import org.dromara.system.service.ISysUserService; import org.springframework.http.MediaType; @@ -45,6 +48,7 @@ public class SysProfileController extends BaseController { private final ISysUserService userService; private final ISysOssService ossService; private final ISysUserLoginPortService sysUserLoginPortService; + private final ISysUserElectronicSignatureService userElectronicSignatureService; /** * 个人信息 @@ -93,6 +97,46 @@ public class SysProfileController extends BaseController { return R.fail("修改个人信息异常,请联系管理员"); } + /** + * 当前登录人员电子签名信息 + */ + @GetMapping("/electronic-signature/info") + public R electronicSignatureInfo() { + return R.ok(userElectronicSignatureService.getCurrentUserSignature()); + } + + /** + * 获取当前登录人员可直接用于业务回填的电子签名 + */ + @GetMapping("/electronic-signature/current") + public R 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 saveElectronicSignature(@Validated @RequestBody SysUserElectronicSignatureBo bo) { + return R.ok(userElectronicSignatureService.saveCurrentUserSignature(bo)); + } + + /** + * 清空当前登录人员电子签名 + */ + @RepeatSubmit + @Log(title = "电子签名", businessType = BusinessType.UPDATE) + @DeleteMapping("/electronic-signature") + public R clearElectronicSignature() { + return toAjax(userElectronicSignatureService.clearCurrentUserSignature()); + } + /** * 重置密码 * diff --git a/src/main/java/org/dromara/system/domain/SysUserElectronicSignature.java b/src/main/java/org/dromara/system/domain/SysUserElectronicSignature.java new file mode 100644 index 0000000..68ee91d --- /dev/null +++ b/src/main/java/org/dromara/system/domain/SysUserElectronicSignature.java @@ -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; +} diff --git a/src/main/java/org/dromara/system/domain/bo/SysUserElectronicSignatureBo.java b/src/main/java/org/dromara/system/domain/bo/SysUserElectronicSignatureBo.java new file mode 100644 index 0000000..5f07250 --- /dev/null +++ b/src/main/java/org/dromara/system/domain/bo/SysUserElectronicSignatureBo.java @@ -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; +} diff --git a/src/main/java/org/dromara/system/domain/vo/SysUserElectronicSignatureVo.java b/src/main/java/org/dromara/system/domain/vo/SysUserElectronicSignatureVo.java new file mode 100644 index 0000000..2148511 --- /dev/null +++ b/src/main/java/org/dromara/system/domain/vo/SysUserElectronicSignatureVo.java @@ -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; +} diff --git a/src/main/java/org/dromara/system/mapper/SysUserElectronicSignatureMapper.java b/src/main/java/org/dromara/system/mapper/SysUserElectronicSignatureMapper.java new file mode 100644 index 0000000..f2fc549 --- /dev/null +++ b/src/main/java/org/dromara/system/mapper/SysUserElectronicSignatureMapper.java @@ -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 { +} diff --git a/src/main/java/org/dromara/system/service/ISysUserElectronicSignatureService.java b/src/main/java/org/dromara/system/service/ISysUserElectronicSignatureService.java new file mode 100644 index 0000000..20c2931 --- /dev/null +++ b/src/main/java/org/dromara/system/service/ISysUserElectronicSignatureService.java @@ -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(); +} diff --git a/src/main/java/org/dromara/system/service/impl/SysUserElectronicSignatureServiceImpl.java b/src/main/java/org/dromara/system/service/impl/SysUserElectronicSignatureServiceImpl.java new file mode 100644 index 0000000..1237984 --- /dev/null +++ b/src/main/java/org/dromara/system/service/impl/SysUserElectronicSignatureServiceImpl.java @@ -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.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.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.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()); + } +}