刘光辉
昨天 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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package jnpf.limsController;
 
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.exception.DataException;
import jnpf.limsEntity.BizStatus;
import jnpf.limsEntity.LimsQingyandanEntity;
import jnpf.limsEntity.LimsSignEntity;
import jnpf.limsEntity.LimsSignUsageTarget;
import jnpf.limsEntity.SignOpType;
import jnpf.limsService.LimsQingyandanService;
import jnpf.limsService.LimsSignService;
import jnpf.util.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
 
import java.util.Collections;
import java.util.Map;
 
@Slf4j
@Tag(name = "lims请验单接口", description = "请验单提交 / 撤回 / 废弃 / 编辑")
@RestController
@RequestMapping("/lims/biz")
public class LimsQingyandanController {
 
    @Autowired
    private LimsQingyandanService limsQingyandanService;
    @Autowired
    private LimsSignService limsSignService;
 
    /**
     * 成品请验单提交 -> 复制数据到取样表
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "成品请验单提交")
    @PostMapping("/qingyandan/submit")
    public R<String> qingyandanSubmit(@RequestBody LimsQingyandanEntity formData) {
        LimsQingyandanEntity qingyandan = limsQingyandanService.findByFormData(formData);
        if (qingyandan == null) {
            return R.error("请验单数据不存在");
        }
 
        BizStatus currentStatus = BizStatus.fromCode(qingyandan.getBizStatus());
        if (currentStatus != BizStatus.CREATED && currentStatus != BizStatus.CANCELLED
                && currentStatus != BizStatus.REJECTED) {
            return R.error("当前状态[" + currentStatus.getLabel() + "]不允许提交");
        }
 
        LimsSignEntity sign;
        try {
            sign = requireQingyandanSign(qingyandan, formData, SignOpType.QINGYANDAN_SUBMIT);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        limsQingyandanService.submit(qingyandan);
        claimQingyandanSignUsageOnly(sign, SignOpType.QINGYANDAN_SUBMIT, qingyandan);
        return R.success("提交成功");
    }
 
    /**
     * 请验单操作:撤回 / 废弃
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "请验单操作(撤回/废弃)")
    @PostMapping("/qingyandan/action")
    public R<String> qingyandanAction(@RequestBody Map<String, String> params) {
        if (params == null) {
            return R.error("请求参数不能为空");
        }
        String action = params.get("action");
        String liushuihao = params.get("jianyan_liushuihao");
 
        if (liushuihao == null || liushuihao.isEmpty()) {
            return R.error("检验流水号不能为空");
        }
 
        LimsQingyandanEntity qingyandan = limsQingyandanService.findByLiushuihao(liushuihao);
        if (qingyandan == null) {
            return R.error("请验单数据不存在");
        }
 
        BizStatus currentStatus = BizStatus.fromCode(qingyandan.getBizStatus());
 
        switch (action) {
            case "cancel":
                if (currentStatus != BizStatus.CONFIRMED) {
                    return R.error("只有已提交状态的请验单才能撤回");
                }
                LimsSignEntity cancelSign;
                try {
                    cancelSign = requireQingyandanActionSign(
                            qingyandan, params, SignOpType.QINGYANDAN_CANCEL);
                } catch (DataException e) {
                    return R.error(e.getMessage());
                }
                if (!limsQingyandanService.cancel(qingyandan)) {
                    return R.error("请验单状态已变更,撤回未生效");
                }
                recordQingyandanSignUsage(
                        cancelSign, SignOpType.QINGYANDAN_CANCEL, qingyandan);
                return R.success("撤回成功");
 
            case "delete":
                if (currentStatus != BizStatus.CREATED && currentStatus != BizStatus.CANCELLED
                && currentStatus != BizStatus.REJECTED) {
                    return R.error("只有已创建或已撤回状态的请验单才能废弃");
                }
                limsQingyandanService.delete(qingyandan);
                return R.success("废弃成功");
 
            default:
                return R.error("不支持的操作: " + action);
        }
    }
 
    /**
     * 请验单编辑
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "请验单编辑")
    @PostMapping("/qingyandan/edit")
    public R<String> qingyandanEdit(@RequestBody LimsQingyandanEntity formData) {
        LimsQingyandanEntity qingyandan = limsQingyandanService.findByFormData(formData);
        if (qingyandan == null) {
            return R.error("请验单数据不存在");
        }
 
        BizStatus currentStatus = BizStatus.fromCode(qingyandan.getBizStatus());
        if (currentStatus != BizStatus.CREATED && currentStatus != BizStatus.CANCELLED
                && currentStatus != BizStatus.REJECTED) {
            return R.error("当前状态[" + currentStatus.getLabel() + "]不允许编辑");
        }
 
        LimsSignEntity sign;
        try {
            sign = requireQingyandanSign(qingyandan, formData, SignOpType.QINGYANDAN_SUBMIT);
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
        limsQingyandanService.edit(qingyandan, formData);
        claimQingyandanSignUsageOnly(sign, SignOpType.QINGYANDAN_SUBMIT, qingyandan);
        return R.success("编辑成功");
    }
 
    private LimsSignEntity requireQingyandanSign(LimsQingyandanEntity persisted,
                                                  LimsQingyandanEntity request,
                                                  SignOpType opType) {
        String signId = request == null ? null : request.getBizSign();
        if (signId == null || signId.trim().isEmpty()) {
            throw new DataException("请添加电子签名");
        }
        signId = signId.trim();
        persisted.setBizSign(signId);
        if (request != null) {
            request.setBizSign(signId);
        }
        return limsSignService.requireValidSign(
                signId, opType, persisted.getJianyanLiushuihao());
    }
 
    private LimsSignEntity requireQingyandanActionSign(LimsQingyandanEntity persisted,
                                                        Map<String, String> params,
                                                        SignOpType opType) {
        String signId = params.get("biz_sign");
        if (signId == null || signId.trim().isEmpty()) {
            signId = params.get("id");
        }
        if (signId == null || signId.trim().isEmpty()) {
            throw new DataException("请添加电子签名");
        }
        return limsSignService.requireValidSign(
                signId.trim(), opType, persisted.getJianyanLiushuihao());
    }
 
    /**
     * 认领签名并记一条 {@code SIGN_USED}。
     *
     * <p>🔴 <b>只剩撤回({@code QINGYANDAN_CANCEL})走这条路,一字未动</b>:撤回是列表按钮场景,
     * 层 0 根本不覆盖(实测撤回只产生层 2 + 层 3 两条事件,没有层 0 伴随事件)。
     * 一旦让它也改走只认领,那次签名就彻底没有任何记录。
     *
     * <p>提交 / 编辑改走 {@link #claimQingyandanSignUsageOnly}——见那里的说明。
     */
    private void recordQingyandanSignUsage(LimsSignEntity sign,
                                            SignOpType opType,
                                            LimsQingyandanEntity qingyandan) {
        limsSignService.recordSignUsage(sign, opType,
                Collections.singletonList(qingyandan.getId()),
                Collections.singletonList(qingyandanUsageTarget(qingyandan)));
    }
 
    /**
     * 只做签名的原子认领,<b>不发 {@code SIGN_USED} 事件</b>(2026-07-31,D8)。
     *
     * <p>提交 / 编辑走的是在线表单路径:表单保存本身已经产生层 0 事件,层 0 会据表单里的
     * {@code biz_sign} 字段补写一条签名事件;这里再发一条 {@code SIGN_USED} 就是同一次签名两条证据。
     *
     * <p><b>认领没有被删</b>:它是「签名单次消费」的防重放护栏(ed4d908),
     * 去掉等于同一个签名可以反复使用。本次改动只是把「认领」和「发事件」拆开。
     */
    private void claimQingyandanSignUsageOnly(LimsSignEntity sign,
                                               SignOpType opType,
                                               LimsQingyandanEntity qingyandan) {
        limsSignService.claimSignUsageOnly(sign, opType,
                Collections.singletonList(qingyandan.getId()),
                Collections.singletonList(qingyandanUsageTarget(qingyandan)));
    }
 
    private LimsSignUsageTarget qingyandanUsageTarget(LimsQingyandanEntity qingyandan) {
        return LimsSignUsageTarget.builder()
                .targetTable("lims_qingyandan")
                .targetId(qingyandan.getId())
                .bizType("QINGYANDAN")
                .bizModule("LIMS-请验单")
                .bizCode(qingyandan.getJianyanLiushuihao())
                .build();
    }
}