刘光辉
昨天 bb638871a7fb692d80f1b7a758f991dc0879002c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
package jnpf.limsService;
 
import jnpf.base.service.SuperService;
import jnpf.limsEntity.LimsModifySignPasswordParam;
import jnpf.limsEntity.LimsSignEntity;
import jnpf.limsEntity.LimsSignUsageTarget;
import jnpf.limsEntity.LimsSignVerifyParam;
import jnpf.limsEntity.SignOpType;
 
import java.util.Collection;
 
/**
 * LIMS 电子签名服务。
 * 校验账户 + 签名密码后落一条 lims_sign,返回主键作为 biz_sign 凭证给前端。
 */
public interface LimsSignService extends SuperService<LimsSignEntity> {
 
    /**
     * 校验签名并落 lims_sign 一条,返回新记录的主键 id(即给前端的 biz_sign)。
     *
     * <p>校验链:
     * <ol>
     *   <li>opType 必须在 {@link jnpf.limsEntity.SignOpType} 枚举内</li>
     *   <li>accountName 必须等于当前 session 用户的 account(防代签)</li>
     *   <li>note 非空(前端可填 "N/A")</li>
     *   <li>查 lims_user_sign_password by user_id;无记录 → 抛 "未初始化签名密码"</li>
     *   <li>password_hash = Md5(password + secretkey.toLowerCase()) 比对</li>
     *   <li>校验全通过后插入 lims_sign,返回 id</li>
     * </ol>
     *
     * @return lims_sign 主键 id
     * @throws jnpf.exception.DataException 校验失败
     */
    String verifyAndRecord(LimsSignVerifyParam param);
 
    /**
     * 校验指定用户签名并落 lims_sign 一条。
     *
     * <p>与 {@link #verifyAndRecord(LimsSignVerifyParam)} 的差异:
     * <ul>
     *   <li>allowSelf 不为 yes 时,accountName 对应的用户不能是当前 session 用户</li>
     *   <li>按 accountName 查询目标用户,并校验目标用户的签名密码</li>
     *   <li>签名记录的 creatorUserId 写入目标用户 id</li>
     * </ul>
     *
     * @return 校验通过后新建的签名记录
     */
    LimsSignEntity verifyUserAndRecord(LimsSignVerifyParam param);
 
    /**
     * 给所有未软删 base_user 批量初始化签名密码 = "123456"。
     * 已有 lims_user_sign_password 记录的跳过。每个用户独立生成 secretkey。
     *
     * @return [插入条数, 跳过条数]
     */
    int[] initAllUsers();
 
    /**
     * 修改 / 初始化当前登录用户的签名密码。
     *
     * <p>校验链:
     * <ol>
     *   <li>code + timestamp 命中图形验证码缓存(不区分大小写)</li>
     *   <li>当前 session 用户存在</li>
     *   <li>accountPassword 通过 Md5(accountPassword + secretkey) 比对 base_user.f_password</li>
     *   <li>password / repeatPassword 均非空且相等</li>
     *   <li>生成新的 secretkey 与 hash,写入 / 更新 lims_user_sign_password,刷新 last_reset_time</li>
     * </ol>
     *
     * <p>用户首次设置签名密码(未存在 lims_user_sign_password 记录)和后续修改走同一接口,
     * 行为分别为 INSERT 与 UPDATE。
     *
     * @throws jnpf.exception.DataException 任一校验失败
     */
    void modifySignPassword(LimsModifySignPasswordParam param);
 
    /**
     * 校验业务侧传入的 biz_sign(即 lims_sign 主键)真实有效。
     *
     * <p>op_type 与 data_id 各自独立支持 lazy / eager(自方案 C 起):
     * <ul>
     *   <li>字段为空 → 仅在返回实体中补全业务期望值,暂不写库</li>
     *   <li>字段已有值 → 严格校验与业务期望匹配,不符抛 DataException</li>
     * </ul>
     * 真正认领由 {@link #recordSignUsage} 在业务成功后原子完成,避免状态冲突时提前消费签名。
     * 已有 {@code extra_json} 的签名视为已消费,必须重新签名,不能跨业务周期复用。
     *
     * <p>expectedDataId 传 null = 业务接口不需要 data_id 锚点(如 jiance-task 批量类),
     * 此时完全跳过 data_id 校验,保留 verify 时的原值不动。
     *
     * <p>签名人始终校验:必须等于当前 session 用户(防别人签的被偷用)。
     *
     * @param signId         前端回传的 biz_sign(lims_sign 主键)
     * @param expectedOpType 业务期望的操作类型
     * @param expectedDataId 业务期望的 data_id;传 null 表示不锚 data_id
     * @return 校验通过的签名记录
     */
    LimsSignEntity requireValidSign(String signId, SignOpType expectedOpType, String expectedDataId);
 
    /**
     * 在业务写库成功后记录签名被实际使用的事实。
     *
     * <p>只接受业务 service 返回的实际成功目标;请求中因状态、权限或并发被跳过的目标不得传入。
     * 实际目标为空时不认领签名、不更新 {@code lims_sign.extra_json},也不生成 {@code SIGN_USED}。
     * 非空时先通过条件更新一次性原子认领签名,再将事件交给 AuditTxHolder 在事务提交后发布;
     * 已消费签名、认领冲突或结果保存失败会标记当前事务回滚。
     */
    void recordSignUsage(LimsSignEntity sign,
                         SignOpType opType,
                         Collection<String> requestedTargetIds,
                         Collection<LimsSignUsageTarget> actualTargets);
 
    /**
     * 只做签名的原子认领,<b>不发审计事件</b>。
     *
     * <p>给「签名事实由别处记录」的调用方用——当前是在线表单路径:表单保存本身已经产生层 0
     * 事件,层 0 会据表单里的签名字段补写一条签名事件;这里再发一条 {@code SIGN_USED}
     * 就是同一次签名两条证据。
     *
     * <p><b>认领逻辑必须保留</b>:它是「签名单次消费」的防重放护栏(ed4d908),
     * 去掉等于同一个签名可以反复使用。
     *
     * <p>与 {@link #recordSignUsage} 的关系是「同一段认领 + 是否再发事件」:
     * {@code recordSignUsage} 内部就调本方法,两处认领逻辑只有一份,不会各自漂移。
     * 认领的全部前置条件(空目标不认领、认领冲突标记事务回滚)与 {@code recordSignUsage} 完全一致。
     */
    void claimSignUsageOnly(LimsSignEntity sign,
                            SignOpType opType,
                            Collection<String> requestedTargetIds,
                            Collection<LimsSignUsageTarget> actualTargets);
 
    /**
     * 把任意结构数据序列化为 JSON 后回写到 lims_sign.extra_json。
     *
     * <p>典型用法:
     * <ul>
     *   <li>jiance-task 批量类把 ids 包成 {"targets":[...]} 落库</li>
     *   <li>业务想加结构化操作日志:{"targets":[...],"comments":"..."}</li>
     * </ul>
     *
     * <p>extra 为 null 时不更新。重复调用会覆盖(业务最新一次最权威)。
     *
     * @param signId 签名记录主键(即 biz_sign)
     * @param extra  任意可序列化对象(Map / List / POJO 都可)
     */
    void attachExtra(String signId, Object extra);
}