package jnpf.limsService.impl;
|
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import jnpf.audit.model.AuditEventDTO;
|
import jnpf.audit.sdk.AuditClient;
|
import jnpf.audit.sdk.annotation.AuditLog;
|
import jnpf.audit.AuditConsts;
|
import jnpf.base.UserInfo;
|
import jnpf.base.service.SuperServiceImpl;
|
import jnpf.exception.DataException;
|
import jnpf.limsEntity.LimsModifySignPasswordParam;
|
import jnpf.limsEntity.LimsSignEntity;
|
import jnpf.limsEntity.LimsSignUsageTarget;
|
import jnpf.limsEntity.LimsSignVerifyParam;
|
import jnpf.limsEntity.LimsUserSignPasswordEntity;
|
import jnpf.limsEntity.SignOpType;
|
import jnpf.limsMapper.LimsSignMapper;
|
import jnpf.limsMapper.LimsUserSignPasswordMapper;
|
import jnpf.limsService.LimsSignService;
|
import jnpf.limsService.support.LimsSignUsageSupport;
|
import jnpf.permission.UserApi;
|
import jnpf.permission.entity.UserEntity;
|
import jnpf.util.JsonUtil;
|
import jnpf.util.Md5Util;
|
import jnpf.util.RandomUtil;
|
import jnpf.util.RedisUtil;
|
import jnpf.util.UserProvider;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.NoTransactionException;
|
import org.springframework.transaction.interceptor.TransactionAspectSupport;
|
import org.springframework.transaction.support.TransactionSynchronizationManager;
|
|
import java.util.Collection;
|
import java.util.Date;
|
import java.util.HashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
|
@Service
|
public class LimsSignServiceImpl
|
extends SuperServiceImpl<LimsSignMapper, LimsSignEntity>
|
implements LimsSignService {
|
|
private static final String DEFAULT_INIT_PASSWORD = "123456";
|
|
@Autowired
|
private LimsUserSignPasswordMapper userSignPasswordMapper;
|
|
@Autowired
|
private UserApi userApi;
|
|
@Autowired
|
private RedisUtil redisUtil;
|
|
/** audit.enabled=false 时 AuditClient Bean 不存在,签名业务仍须可启动。 */
|
@Autowired
|
private ObjectProvider<AuditClient> auditClientProvider;
|
|
/**
|
* 电子签名(当前登录用户)。层 2 埋点,成功 {@code SIGN} / 失败 {@code SIGN_FAIL}。
|
*
|
* <p>三个非默认的注解项,每个都是被实据逼出来的,别随手改回默认:
|
* <ul>
|
* <li>{@code bizCodeRequired=false}——签名**常常先于业务对象存在**(先签名拿 biz_sign、
|
* 再创建请验单,此时 dataId 为空)。留默认 true 会让签名事件被静默跳过,
|
* 只在日志里留一行 WARN。</li>
|
* <li>{@code failureAction="SIGN_FAIL"}——失败必须留痕:签名失败在合规上是独立关注对象
|
* (连续失败可能是有人在试密码)。本方法的全部失败路径都是从 service 抛
|
* {@code DataException}("签名密码错误"/"账户与当前登录用户不符"/…),
|
* 切面 catch 得到,异常 message 自动落 {@code reason}。</li>
|
* <li>{@code password: '***'}——密码字段**呈现但不记值**,用的是全局约定
|
* {@link jnpf.audit.diff.AuditFieldDiff#MASKED}(与层 0 快照、层 1
|
* masked-columns 同一个常量)。审计表 append-only 保留 10 年,明文进去洗不掉。</li>
|
* </ul>
|
*
|
* <p>{@code targetIdExpr="#result"} 只在成功路径求值出 signId(= {@code lims_sign.f_id});
|
* 失败时 result 为 null,切面逐字段兜底后该列留空,事件照记。
|
*
|
* <p><b>2026-07-31 起成功不再当场记录</b>({@code recordSuccess=false}):验签成功后业务若失败,
|
* 库里会留下一条无对应业务动作的孤儿签名事件——技术上自洽,药厂合规上不接受
|
* (签名 + 业务是一个整体)。成功事实改由层 0 在表单保存成功后,据 {@code biz_sign}
|
* 等签名字段回读本表补写,与业务事件共用 operationId。失败侧 {@code SIGN_FAIL} 一字未动。
|
*/
|
@Override
|
@AuditLog(
|
eventType = AuditConsts.TYPE_E_SIGNATURE,
|
action = "SIGN",
|
label = "电子签名",
|
failureAction = "SIGN_FAIL",
|
failureLabel = "电子签名 失败",
|
recordSuccess = false, // 成功事实由层 0 在业务保存成功后补写,见 spec §6
|
bizType = "SIGN",
|
bizCodeExpr = "#param?.dataId",
|
bizCodeRequired = false,
|
targetTable = "lims_sign",
|
targetIdExpr = "#result",
|
reasonExpr = "#param?.note",
|
extraExpr = "{'accountName': #param?.accountName, 'opType': #param?.opType,"
|
+ " 'password': T(jnpf.audit.diff.AuditFieldDiff).MASKED}"
|
)
|
public String verifyAndRecord(LimsSignVerifyParam param) {
|
LimsSignEntity sign = verifyCurrentUserAndRecord(param);
|
return sign.getId();
|
}
|
|
private LimsSignEntity verifyCurrentUserAndRecord(LimsSignVerifyParam param) {
|
if (param == null) {
|
throw new DataException("入参不能为空");
|
}
|
validateVerifyParam(param);
|
|
UserInfo userInfo = UserProvider.getUser();
|
if (userInfo == null || userInfo.getUserId() == null) {
|
throw new DataException("当前未登录");
|
}
|
if (!param.getAccountName().equals(userInfo.getUserAccount())) {
|
throw new DataException("账户与当前登录用户不符");
|
}
|
|
verifySignPassword(userInfo.getUserId(), param.getPassword(), "您还未设置签名密码,请先到「个人中心 → 修改签名密码」初始化");
|
return saveSign(param, userInfo.getUserId());
|
}
|
|
/**
|
* 电子签名(指定用户,非当前登录人)。埋点口径同 {@link #verifyAndRecord},两处差异:
|
* <ul>
|
* <li>{@code targetIdExpr="#result?.id"} 用**安全导航**——本方法返回实体而非 String,
|
* 失败时 result 为 null,非安全导航会抛 SpelEvaluationException(切面虽有逐字段兜底,
|
* 但让意图显式比依赖兜底好)。</li>
|
* <li>extra 多记 {@code allowSelf}——它决定"是否允许给自己签名",是这个端点独有的授权开关,
|
* 事后追责时需要知道当时传的是什么。</li>
|
* </ul>
|
*
|
* <p><b>2026-07-31 起成功不再当场记录</b>({@code recordSuccess=false}):验签成功后业务若失败,
|
* 库里会留下一条无对应业务动作的孤儿签名事件——技术上自洽,药厂合规上不接受
|
* (签名 + 业务是一个整体)。成功事实改由层 0 在表单保存成功后,据 {@code biz_sign}
|
* 等签名字段回读本表补写,与业务事件共用 operationId。失败侧 {@code SIGN_FAIL} 一字未动。
|
*/
|
@Override
|
@AuditLog(
|
eventType = AuditConsts.TYPE_E_SIGNATURE,
|
action = "SIGN",
|
label = "电子签名(指定用户)",
|
failureAction = "SIGN_FAIL",
|
failureLabel = "电子签名(指定用户)失败",
|
recordSuccess = false, // 成功事实由层 0 在业务保存成功后补写,见 spec §6
|
bizType = "SIGN",
|
bizCodeExpr = "#param?.dataId",
|
bizCodeRequired = false,
|
targetTable = "lims_sign",
|
targetIdExpr = "#result?.id",
|
reasonExpr = "#param?.note",
|
extraExpr = "{'accountName': #param?.accountName, 'opType': #param?.opType,"
|
+ " 'allowSelf': #param?.allowSelf,"
|
+ " 'password': T(jnpf.audit.diff.AuditFieldDiff).MASKED}"
|
)
|
public LimsSignEntity verifyUserAndRecord(LimsSignVerifyParam param) {
|
if (param == null) {
|
throw new DataException("入参不能为空");
|
}
|
validateVerifyParam(param);
|
|
UserInfo userInfo = UserProvider.getUser();
|
if (userInfo == null || userInfo.getUserId() == null) {
|
throw new DataException("当前未登录");
|
}
|
boolean allowSelf = isYes(param.getAllowSelf());
|
if (!allowSelf && param.getAccountName().equals(userInfo.getUserAccount())) {
|
throw new DataException("不允许验证当前登录用户");
|
}
|
|
UserEntity signUser = userApi.getInfoByAccount(param.getAccountName());
|
if (signUser == null || signUser.getId() == null || signUser.getId().isEmpty()) {
|
throw new DataException("签名账户不存在");
|
}
|
if (!allowSelf && Objects.equals(userInfo.getUserId(), signUser.getId())) {
|
throw new DataException("不允许验证当前登录用户");
|
}
|
|
verifySignPassword(signUser.getId(), param.getPassword(), "该用户还未设置签名密码");
|
return saveSign(param, signUser.getId());
|
}
|
|
private void validateVerifyParam(LimsSignVerifyParam param) {
|
String opTypeCode = param.getOpType();
|
|
// op_type 自方案 C 起改为可选:
|
// - 传了:必须是合法 SignOpType(eager 模式,业务接口仍按枚举校验)
|
// - 没传:lazy 模式,业务接口收到 biz_sign 时再回写 op_type
|
if (opTypeCode != null && !opTypeCode.isEmpty() && !SignOpType.isValidCode(opTypeCode)) {
|
throw new DataException("非法的签名操作类型: " + opTypeCode);
|
}
|
if (param.getAccountName() == null || param.getAccountName().isEmpty()) {
|
throw new DataException("账户名称不能为空");
|
}
|
if (param.getPassword() == null || param.getPassword().isEmpty()) {
|
throw new DataException("签名密码不能为空");
|
}
|
if (param.getNote() == null || param.getNote().isEmpty()) {
|
throw new DataException("说明 / 原因不能为空");
|
}
|
}
|
|
private void verifySignPassword(String userId, String password, String unsetMessage) {
|
QueryWrapper<LimsUserSignPasswordEntity> pq = new QueryWrapper<>();
|
pq.eq("user_id", userId);
|
pq.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
LimsUserSignPasswordEntity pwdRow = userSignPasswordMapper.selectOne(pq);
|
if (pwdRow == null || pwdRow.getPasswordHash() == null || pwdRow.getPasswordHash().isEmpty()) {
|
throw new DataException(unsetMessage);
|
}
|
|
String inputHash = Md5Util.getStringMd5(password + pwdRow.getSecretkey().toLowerCase());
|
if (!inputHash.equals(pwdRow.getPasswordHash())) {
|
throw new DataException("签名密码错误");
|
}
|
}
|
|
private boolean isYes(String value) {
|
return "yes".equalsIgnoreCase(value == null ? "" : value.trim());
|
}
|
|
private LimsSignEntity saveSign(LimsSignVerifyParam param, String creatorUserId) {
|
LimsSignEntity sign = new LimsSignEntity();
|
sign.setId(RandomUtil.uuId());
|
sign.setDataId(param.getDataId());
|
sign.setOpType(param.getOpType());
|
sign.setAccountName(param.getAccountName());
|
sign.setNote(param.getNote());
|
sign.setMetaData(param.getMetaData());
|
sign.setCreatorUserId(creatorUserId);
|
// tenantId / creatorTime 由 MetaObjectHandler 填
|
this.save(sign);
|
return sign;
|
}
|
|
@Override
|
public LimsSignEntity requireValidSign(String signId, SignOpType expectedOpType, String expectedDataId) {
|
if (signId == null || signId.isEmpty()) {
|
throw new DataException("缺少电子签名");
|
}
|
if (expectedOpType == null) {
|
throw new DataException("业务未指定签名操作类型");
|
}
|
UserInfo userInfo = UserProvider.getUser();
|
if (userInfo == null || userInfo.getUserId() == null) {
|
throw new DataException("当前未登录");
|
}
|
|
QueryWrapper<LimsSignEntity> q = new QueryWrapper<>();
|
q.eq("f_id", signId);
|
q.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
// 不限定列:见 commit 54c8a52 — selectList 限定列 + 多租户拦截器会让 entity 为 null
|
LimsSignEntity sign = this.getBaseMapper().selectOne(q);
|
if (sign == null) {
|
throw new DataException("电子签名不存在或已失效");
|
}
|
// 签名人始终校验
|
if (!userInfo.getUserId().equals(sign.getCreatorUserId())) {
|
throw new DataException("电子签名非当前用户所签,禁止使用");
|
}
|
if (sign.getExtraJson() != null && !sign.getExtraJson().isEmpty()) {
|
throw new DataException("电子签名已被使用,请重新签名");
|
}
|
|
// op_type / data_id 各自独立支持 lazy / eager:
|
// 字段为空 → 仅在内存中补全,业务成功后由 recordSignUsage 原子认领
|
// 字段已有值 → 严格校验匹配
|
String existingOpType = sign.getOpType();
|
if (existingOpType == null || existingOpType.isEmpty()) {
|
sign.setOpType(expectedOpType.getCode());
|
} else if (!expectedOpType.getCode().equals(existingOpType)) {
|
throw new DataException("电子签名操作类型不符(需 " + expectedOpType.getLabel() + ")");
|
}
|
|
// expectedDataId 为 null = 业务接口不需要锚 data_id(如 jiance-task 批量类),
|
// 这种情况完全不校验也不回写 sign.data_id(保留 verify 时的原值)。
|
if (expectedDataId != null) {
|
String existingDataId = sign.getDataId();
|
if (existingDataId == null || existingDataId.isEmpty()) {
|
sign.setDataId(expectedDataId);
|
} else if (!expectedDataId.equals(existingDataId)) {
|
throw new DataException("电子签名业务对象不符");
|
}
|
}
|
return sign;
|
}
|
|
/**
|
* 「原子认领 + 发 SIGN_USED」的组合入口,语义与拆分前完全一致。
|
*
|
* <p>2026-07-31 拆成两步({@link #claimSignUsageOnly} + {@link #publishSignUsedEvents}):
|
* 在线表单路径只需要前半步——表单保存本身已产生层 0 事件,层 0 会据表单里的签名字段补写
|
* 一条签名事件,这里再发 {@code SIGN_USED} 就是同一次签名两条证据(D8)。
|
* <b>顺序不可调换</b>:认领失败会标记事务回滚,必须先认领成功才允许发事件。
|
*/
|
@Override
|
public void recordSignUsage(LimsSignEntity sign,
|
SignOpType opType,
|
Collection<String> requestedTargetIds,
|
Collection<LimsSignUsageTarget> actualTargets) {
|
claimSignUsageOnly(sign, opType, requestedTargetIds, actualTargets);
|
publishSignUsedEvents(sign, opType, actualTargets);
|
}
|
|
@Override
|
public void claimSignUsageOnly(LimsSignEntity sign,
|
SignOpType opType,
|
Collection<String> requestedTargetIds,
|
Collection<LimsSignUsageTarget> actualTargets) {
|
List<LimsSignUsageTarget> normalized = LimsSignUsageSupport.normalizeTargets(actualTargets);
|
if (!signUsageApplicable(sign, opType, normalized)) {
|
return;
|
}
|
String usageJson = JsonUtil.getObjectToString(
|
LimsSignUsageSupport.signExtra(requestedTargetIds, normalized));
|
claimSignUsage(sign, opType, usageJson);
|
}
|
|
private void publishSignUsedEvents(LimsSignEntity sign,
|
SignOpType opType,
|
Collection<LimsSignUsageTarget> actualTargets) {
|
List<LimsSignUsageTarget> normalized = LimsSignUsageSupport.normalizeTargets(actualTargets);
|
if (!signUsageApplicable(sign, opType, normalized)) {
|
return;
|
}
|
for (LimsSignUsageSupport.UsageGroup group : LimsSignUsageSupport.groupByBizCode(normalized)) {
|
AuditEventDTO event = AuditEventDTO.builder()
|
.clientEventId(LimsSignUsageSupport.clientEventId(sign.getId(), opType, group))
|
.eventType(AuditConsts.TYPE_E_SIGNATURE)
|
.actionCode(LimsSignUsageSupport.ACTION_SIGN_USED)
|
.actionLabel(opType.getLabel() + " · 电子签名")
|
.sourceLayer(3)
|
.targetTable("lims_sign")
|
.targetId(sign.getId())
|
.bizType(group.getBizType())
|
.bizModule(group.getBizModule())
|
.bizCode(group.getBizCode())
|
.reason(sign.getNote())
|
.extra(JsonUtil.getObjectToString(
|
LimsSignUsageSupport.eventExtra(sign, opType, group)))
|
.build();
|
auditClientProvider.ifAvailable(client -> client.record(event));
|
}
|
}
|
|
/**
|
* 认领与发事件共用的前置判定:任一不满足则本次「签名被使用」视为没有发生,
|
* 既不认领也不发事件。两侧共用一份,避免"认领了但不发事件"这类不对称。
|
*/
|
private boolean signUsageApplicable(LimsSignEntity sign,
|
SignOpType opType,
|
List<LimsSignUsageTarget> normalized) {
|
return sign != null && sign.getId() != null && !sign.getId().isEmpty()
|
&& opType != null && !normalized.isEmpty();
|
}
|
|
private void claimSignUsage(LimsSignEntity sign, SignOpType opType, String usageJson) {
|
UpdateWrapper<LimsSignEntity> update = new UpdateWrapper<>();
|
update.eq("f_id", sign.getId());
|
update.eq("f_creator_user_id", sign.getCreatorUserId());
|
update.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
update.and(w -> w.isNull("op_type").or().eq("op_type", "")
|
.or().eq("op_type", opType.getCode()));
|
update.and(w -> w.isNull("extra_json").or().eq("extra_json", ""));
|
update.set("op_type", opType.getCode());
|
update.set("extra_json", usageJson);
|
|
String dataId = sign.getDataId();
|
if (dataId == null || dataId.isEmpty()) {
|
update.and(w -> w.isNull("data_id").or().eq("data_id", ""));
|
} else {
|
update.and(w -> w.isNull("data_id").or().eq("data_id", "")
|
.or().eq("data_id", dataId));
|
update.set("data_id", dataId);
|
}
|
|
if (this.getBaseMapper().update(null, update) != 1) {
|
throw signUsageFailure("电子签名已被其他业务使用或状态已变更");
|
}
|
sign.setOpType(opType.getCode());
|
sign.setExtraJson(usageJson);
|
}
|
|
private DataException signUsageFailure(String message) {
|
if (TransactionSynchronizationManager.isActualTransactionActive()) {
|
try {
|
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
|
} catch (NoTransactionException ignored) {
|
// 调用方不是由 @Transactional 代理进入时,仍向上抛错阻止继续记录事件。
|
}
|
}
|
return new DataException(message);
|
}
|
|
@Override
|
public int[] initAllUsers() {
|
UserInfo userInfo = UserProvider.getUser();
|
if (userInfo == null) {
|
throw new DataException("当前未登录");
|
}
|
|
// 1. 取所有未软删的 base_user
|
List<Map<String, Object>> users = userSignPasswordMapper.selectAllActiveBaseUsers();
|
if (users == null || users.isEmpty()) {
|
return new int[]{0, 0};
|
}
|
|
// 2. 取已初始化的 user_id 集合(一次查避免循环里 N+1)
|
Set<String> existed = new HashSet<>();
|
QueryWrapper<LimsUserSignPasswordEntity> exq = new QueryWrapper<>();
|
exq.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
List<LimsUserSignPasswordEntity> exList = userSignPasswordMapper.selectList(exq);
|
if (exList != null) {
|
for (LimsUserSignPasswordEntity r : exList) {
|
if (r != null && r.getUserId() != null) {
|
existed.add(r.getUserId());
|
}
|
}
|
}
|
|
// 3. 过滤后批量插入
|
int inserted = 0;
|
int skipped = 0;
|
Date now = new Date();
|
for (Map<String, Object> u : users) {
|
String uid = u.get("id") == null ? null : u.get("id").toString();
|
String tenantId = u.get("tenantId") == null ? "0" : u.get("tenantId").toString();
|
if (uid == null || uid.isEmpty()) {
|
continue;
|
}
|
if (existed.contains(uid)) {
|
skipped++;
|
continue;
|
}
|
// RandomUtil 仅提供 uuId / enUuId,无 randomString。截 8 位作为独立盐,足够区分。
|
String secretkey = RandomUtil.uuId().substring(0, 8);
|
String hash = Md5Util.getStringMd5(DEFAULT_INIT_PASSWORD + secretkey.toLowerCase());
|
|
LimsUserSignPasswordEntity row = new LimsUserSignPasswordEntity();
|
row.setId(RandomUtil.uuId());
|
row.setTenantId(tenantId);
|
row.setUserId(uid);
|
row.setPasswordHash(hash);
|
row.setSecretkey(secretkey);
|
row.setCreatorTime(now);
|
row.setCreatorUserId(userInfo.getUserId());
|
userSignPasswordMapper.insert(row);
|
inserted++;
|
}
|
return new int[]{inserted, skipped};
|
}
|
|
/**
|
* 修改签名密码。**动作码与签名本身分开**({@code SIGN_PWD_CHANGE})——它不是"签了一次名",
|
* 而是"改变了此后所有签名的凭据",合规上是独立的关键动作,混进 SIGN 会污染签名计数。
|
*
|
* <p>入参三个字段全是密码(accountPassword / password / repeatPassword),
|
* 因此 extra 里**三个一律 MASKED**:记"这次提交了哪些凭据字段"而不记值。
|
* 本方法没有 note 字段,故 {@code reasonExpr} 不配,reason 留空——
|
* 失败的技术原因("验证码错误或已过期"/"账户密码错误"/…)由切面落 extra.failureReason。
|
*
|
* <p>{@code bizCodeRequired=false}:改密码天然没有业务单号。
|
*/
|
@Override
|
@AuditLog(
|
eventType = AuditConsts.TYPE_E_SIGNATURE,
|
action = "SIGN_PWD_CHANGE",
|
label = "修改签名密码",
|
failureAction = "SIGN_PWD_CHANGE_FAIL",
|
failureLabel = "修改签名密码 失败",
|
bizType = "SIGN",
|
bizCodeExpr = "",
|
bizCodeRequired = false,
|
targetTable = "lims_user_sign_password",
|
extraExpr = "{'accountPassword': T(jnpf.audit.diff.AuditFieldDiff).MASKED,"
|
+ " 'password': T(jnpf.audit.diff.AuditFieldDiff).MASKED,"
|
+ " 'repeatPassword': T(jnpf.audit.diff.AuditFieldDiff).MASKED}"
|
)
|
public void modifySignPassword(LimsModifySignPasswordParam param) {
|
if (param == null) {
|
throw new DataException("入参不能为空");
|
}
|
|
// 1. 验证码
|
if (param.getTimestamp() == null || param.getTimestamp().isEmpty()
|
|| param.getCode() == null || param.getCode().isEmpty()) {
|
throw new DataException("请输入验证码");
|
}
|
Object cached = redisUtil.getString(param.getTimestamp());
|
String captcha = cached == null ? "" : String.valueOf(cached);
|
if (captcha.isEmpty() || !param.getCode().equalsIgnoreCase(captcha)) {
|
throw new DataException("验证码错误或已过期");
|
}
|
|
// 2. 当前用户
|
UserInfo userInfo = UserProvider.getUser();
|
if (userInfo == null || userInfo.getUserId() == null) {
|
throw new DataException("当前未登录");
|
}
|
|
// 3. 账户密码(与 /Users/Current/Actions/ModifyPassword 校验逻辑一致:
|
// 前端 MD5(明文),后端 Md5(md5_password + secretkey) == base_user.f_password)
|
if (param.getAccountPassword() == null || param.getAccountPassword().isEmpty()) {
|
throw new DataException("请输入账户密码");
|
}
|
UserEntity user = userApi.getInfoById(userInfo.getUserId());
|
if (user == null) {
|
throw new DataException("用户不存在");
|
}
|
String expectAccountHash = Md5Util.getStringMd5(
|
param.getAccountPassword().toLowerCase() + user.getSecretkey().toLowerCase());
|
if (!expectAccountHash.equals(user.getPassword())) {
|
throw new DataException("账户密码错误");
|
}
|
|
// 4. 新签名密码
|
if (param.getPassword() == null || param.getPassword().isEmpty()) {
|
throw new DataException("新签名密码不能为空");
|
}
|
if (!param.getPassword().equals(param.getRepeatPassword())) {
|
throw new DataException("两次输入的新签名密码不一致");
|
}
|
|
// 5. 写入 / 更新 lims_user_sign_password
|
QueryWrapper<LimsUserSignPasswordEntity> pq = new QueryWrapper<>();
|
pq.eq("user_id", userInfo.getUserId());
|
pq.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
|
LimsUserSignPasswordEntity existing = userSignPasswordMapper.selectOne(pq);
|
|
String secretkey = RandomUtil.uuId().substring(0, 8);
|
String hash = Md5Util.getStringMd5(param.getPassword() + secretkey.toLowerCase());
|
Date now = new Date();
|
if (existing == null) {
|
LimsUserSignPasswordEntity row = new LimsUserSignPasswordEntity();
|
row.setId(RandomUtil.uuId());
|
row.setUserId(userInfo.getUserId());
|
row.setPasswordHash(hash);
|
row.setSecretkey(secretkey);
|
row.setLastResetTime(now);
|
// tenantId / creatorUserId / creatorTime 由 MetaObjectHandler 填
|
userSignPasswordMapper.insert(row);
|
} else {
|
existing.setPasswordHash(hash);
|
existing.setSecretkey(secretkey);
|
existing.setLastResetTime(now);
|
// tenantId / lastModifyUserId / lastModifyTime 由 MetaObjectHandler 填
|
userSignPasswordMapper.updateById(existing);
|
}
|
|
// 消耗已用验证码,防止重放
|
redisUtil.remove(param.getTimestamp());
|
}
|
|
@Override
|
public void attachExtra(String signId, Object extra) {
|
if (signId == null || signId.isEmpty() || extra == null) {
|
return;
|
}
|
UpdateWrapper<LimsSignEntity> uw = new UpdateWrapper<>();
|
uw.eq("f_id", signId);
|
uw.set("extra_json", JsonUtil.getObjectToString(extra));
|
this.update(null, uw);
|
}
|
}
|