刘光辉
昨天 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
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
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.limsEntity.BaogaoStatus;
import jnpf.limsEntity.JianyanJielun;
import jnpf.limsEntity.JianyanStatus;
import jnpf.limsEntity.LimsBaogaoSubmitParam;
import jnpf.limsEntity.LimsJianyanXiangEntity;
import jnpf.limsEntity.LimsQingyandanEntity;
import jnpf.limsEntity.bizlog.BizLogAction;
import jnpf.limsEntity.bizlog.BizLogType;
import jnpf.limsService.LimsJianyanXiangService;
import jnpf.limsService.LimsQingyandanReportService;
import jnpf.limsService.LimsQingyandanService;
import jnpf.util.JsonUtil;
import jnpf.util.UserProvider;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import java.text.SimpleDateFormat;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Objects;
 
/**
 * 报告生命周期服务实现。报告已合并进 lims_qingyandan,所有动作按请验单 id 在该表的
 * baogao_* 列上流转,不再读写 lims_qingyandan_report。
 */
@Service
public class LimsQingyandanReportServiceImpl implements LimsQingyandanReportService {
 
    @Autowired
    private LimsQingyandanService limsQingyandanService;
 
    @Autowired
    private LimsJianyanXiangService limsJianyanXiangService;
 
    @Autowired
    private LimsOperationalMessagePublisher operationalMessagePublisher;
 
    /**
     * 层 3 显式审计入口。**必须用 {@link ObjectProvider} 注入**:{@code audit.enabled=false} 时
     * 整个自动配置类连同 AuditClient Bean 一起缺席,普通 @Autowired 会让本服务起不来。
     */
    @Autowired
    private ObjectProvider<AuditClient> auditClientProvider;
 
    @Override
    public boolean generateReportData(String qingyandanId) {
        if (qingyandanId == null || qingyandanId.isEmpty()) {
            return false;
        }
 
        // 1. 源请验单(主键查)。不存在 / 已软删直接跳过(不抛异常,避免回滚上游的审核动作)
        LimsQingyandanEntity qingyandan = limsQingyandanService.getById(qingyandanId);
        if (qingyandan == null
                || (qingyandan.getDeleteMark() != null && qingyandan.getDeleteMark() == 1)) {
            return false;
        }
        // 幂等:baogao_zhuangtai 非空串即视为已生成(含已提交/审核/批准),跳过
        if (qingyandan.getBaogaoZhuangtai() != null && !qingyandan.getBaogaoZhuangtai().isEmpty()) {
            return false;
        }
 
        // 2. 该请验单下所有未软删的检验项:判"全 audited" + 算 shifou_hege
        List<LimsJianyanXiangEntity> xiangRows = loadXiangRowsByQingyandanId(qingyandanId);
        if (xiangRows.isEmpty() || hasUnauditedXiang(xiangRows)) {
            return false;
        }
 
        // 3. 在请验单行上落报告初始数据(仅当 baogao_zhuangtai 仍为空串,防并发)
        UpdateWrapper<LimsQingyandanEntity> uw = new UpdateWrapper<>();
        uw.eq("f_id", qingyandanId);
        uw.eq("baogao_zhuangtai", "");
        uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
        uw.set("baogao_zhuangtai", BaogaoStatus.CREATED.getCode());
        uw.set("baogao_shifou_hege", computeShifouHege(xiangRows));
        // 报告书编号首次生成后保持稳定(退回不清空),避免重新生成时编号被复用
        if (qingyandan.getBaogaoBianhao() == null || qingyandan.getBaogaoBianhao().isEmpty()) {
            uw.set("baogao_bianhao", generateBaogaoBianhao(qingyandan.getChanpinBianma()));
        }
        return limsQingyandanService.update(null, uw);
    }
 
    /** 拿该请验单下所有未软删的检验项。不要用 select 限定列,JNPF 拦截器会把缺系统列的行整体置 null。 */
    private List<LimsJianyanXiangEntity> loadXiangRowsByQingyandanId(String qingyandanId) {
        QueryWrapper<LimsJianyanXiangEntity> wrapper = new QueryWrapper<>();
        wrapper.eq("qingyandan_id", qingyandanId);
        wrapper.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
        return limsJianyanXiangService.list(wrapper);
    }
 
    /** 所有检验项(含外检)必须 audited 才就绪,任意一行不就绪即 true。 */
    private boolean hasUnauditedXiang(List<LimsJianyanXiangEntity> rows) {
        return rows.stream()
                .filter(Objects::nonNull)
                .anyMatch(r -> !JianyanStatus.AUDITED.getCode().equals(r.getJianyanZhuangtai()));
    }
 
    /** 任一检验项结论 non_compliant → "no",否则 "yes"。 */
    private String computeShifouHege(List<LimsJianyanXiangEntity> rows) {
        boolean anyNonCompliant = rows.stream()
                .filter(Objects::nonNull)
                .anyMatch(r -> JianyanJielun.NON_COMPLIANT.getCode().equals(r.getJianyanJielun()));
        return anyNonCompliant ? "no" : "yes";
    }
 
    /**
     * 生成报告书编号:{chanpin_bianma}-{yyyyDDD}A{NN},例:CT001-2026005A01。
     * NN 取同 (chanpin_bianma, yyyyDDD) 前缀的已分配编号数 + 1,2 位补零。
     * 编号一经分配不再清空(退回保留),计数单调,避免复用。
     */
    private String generateBaogaoBianhao(String chanpinBianma) {
        String yyyyDDD = new SimpleDateFormat("yyyyDDD").format(new Date());
        String prefix = (chanpinBianma == null ? "" : chanpinBianma) + "-" + yyyyDDD + "A";
        QueryWrapper<LimsQingyandanEntity> wrapper = new QueryWrapper<>();
        wrapper.likeRight("baogao_bianhao", prefix);
        long n = limsQingyandanService.count(wrapper) + 1;
        return prefix + String.format("%02d", n);
    }
 
    @Override
    @AuditLog(
            action = "REPORT_GENERATE", label = "生成报告", bizType = "BAOGAO",
            bizCodeExpr = "#result ? #param.id : ''",
            targetTable = "lims_qingyandan", targetIdExpr = "#param.id",
            diff = true, entityClass = LimsQingyandanEntity.class
    )
    public boolean submitReport(LimsBaogaoSubmitParam param) {
        UserInfo userInfo = UserProvider.getUser();
        LimsQingyandanEntity report = limsQingyandanService.getById(param.getId());
        if (report == null) {
            return false;
        }
        Date submittedAt = new Date();
        UpdateWrapper<LimsQingyandanEntity> uw = new UpdateWrapper<>();
        uw.eq("f_id", param.getId());
        // 仅 created 可被"生成报告",防并发 / 防重复点击
        uw.eq("baogao_zhuangtai", BaogaoStatus.CREATED.getCode());
        uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
        uw.set("baogao_ren", userInfo.getUserId());
        uw.set("baogao_riqi", param.getBaogaoRiqi());
        uw.set("baogao_beizhu", param.getBeizhu());
        uw.set("baogao_zhuangtai", BaogaoStatus.PENDING_AUDIT.getCode());
        // 重新生成报告时清空上一次退回痕迹
        uw.set("baogao_tuihui_yuanyin", null);
        uw.set("baogao_tuihui_ren", null);
        uw.set("baogao_tuihui_riqi", null);
        boolean updated = limsQingyandanService.update(null, uw);
        if (updated) {
            operationalMessagePublisher.reportPendingReview(report, userInfo, submittedAt);
        }
        return updated;
    }
 
    @Override
    @AuditLog(
            action = "AUDIT_PASS", label = "报告审核通过", bizType = "BAOGAO",
            bizCodeExpr = "#result ? #qingyandanId : ''",
            targetTable = "lims_qingyandan", targetIdExpr = "#qingyandanId",
            diff = true, entityClass = LimsQingyandanEntity.class
    )
    public boolean auditPassReport(String qingyandanId) {
        UserInfo userInfo = UserProvider.getUser();
        LimsQingyandanEntity report = limsQingyandanService.getById(qingyandanId);
        if (report == null || Objects.equals(report.getBaogaoRen(), userInfo.getUserId())) {
            return false;
        }
        Date reviewedAt = new Date();
        UpdateWrapper<LimsQingyandanEntity> uw = new UpdateWrapper<>();
        uw.eq("f_id", qingyandanId);
        uw.eq("baogao_zhuangtai", BaogaoStatus.PENDING_AUDIT.getCode());
        uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
        uw.set("baogao_shenhe_ren", userInfo.getUserId());
        uw.set("baogao_shenhe_riqi", reviewedAt);
        uw.set("baogao_zhuangtai", BaogaoStatus.AUDITED.getCode());
        boolean updated = limsQingyandanService.update(null, uw);
        if (updated) {
            operationalMessagePublisher.reportPendingApproval(report, userInfo, reviewedAt);
        }
        return updated;
    }
 
    @Override
    @AuditLog(
            action = "APPROVE", label = "报告批准", bizType = "BAOGAO",
            bizCodeExpr = "#result ? #qingyandanId : ''",
            targetTable = "lims_qingyandan", targetIdExpr = "#qingyandanId",
            diff = true, entityClass = LimsQingyandanEntity.class
    )
    public boolean approveReport(String qingyandanId) {
        UserInfo userInfo = UserProvider.getUser();
        LimsQingyandanEntity report = limsQingyandanService.getById(qingyandanId);
        if (report == null || Objects.equals(report.getBaogaoRen(), userInfo.getUserId())
                || Objects.equals(report.getBaogaoShenheRen(), userInfo.getUserId())) {
            return false;
        }
        UpdateWrapper<LimsQingyandanEntity> uw = new UpdateWrapper<>();
        uw.eq("f_id", qingyandanId);
        uw.eq("baogao_zhuangtai", BaogaoStatus.AUDITED.getCode());
        uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
        uw.set("baogao_pizhun_ren", userInfo.getUserId());
        uw.set("baogao_pizhun_riqi", new Date());
        uw.set("baogao_zhuangtai", BaogaoStatus.APPROVED.getCode());
        return limsQingyandanService.update(null, uw);
    }
 
    @Override
    public boolean rejectReport(String qingyandanId, String tuihuiYuanyin) {
        UserInfo userInfo = UserProvider.getUser();
 
        LimsQingyandanEntity qingyandan = limsQingyandanService.getById(qingyandanId);
        if (qingyandan == null
                || (qingyandan.getDeleteMark() != null && qingyandan.getDeleteMark() == 1)) {
            return false;
        }
        String currentStatus = qingyandan.getBaogaoZhuangtai();
        boolean rejectable = BaogaoStatus.CREATED.getCode().equals(currentStatus)
                || BaogaoStatus.PENDING_AUDIT.getCode().equals(currentStatus)
                || BaogaoStatus.AUDITED.getCode().equals(currentStatus);
        if (!rejectable) {
            return false;
        }
 
        Date now = new Date();
        List<LimsJianyanXiangEntity> affectedItems = loadXiangRowsByQingyandanId(qingyandanId);
        // 重置报告阶段字段回初始(baogao_bianhao 保留,便于重新生成时沿用旧编号),
        // 写退回三件套;带状态守卫防并发。
        UpdateWrapper<LimsQingyandanEntity> uw = new UpdateWrapper<>();
        uw.eq("f_id", qingyandanId);
        uw.eq("baogao_zhuangtai", currentStatus);
        uw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
        uw.set("baogao_zhuangtai", "");
        uw.set("baogao_ren", null);
        uw.set("baogao_riqi", null);
        uw.set("baogao_beizhu", null);
        uw.set("baogao_shenhe_ren", null);
        uw.set("baogao_shenhe_riqi", null);
        uw.set("baogao_pizhun_ren", null);
        uw.set("baogao_pizhun_riqi", null);
        uw.set("baogao_tuihui_yuanyin", tuihuiYuanyin);
        uw.set("baogao_tuihui_ren", userInfo.getUserId());
        uw.set("baogao_tuihui_riqi", now);
        if (!limsQingyandanService.update(null, uw)) {
            return false;
        }
 
        // 检验项按报告当前状态推回:
        //   created → AUDIT_REJECTED(重新录入 → 复核 → 触发新一轮生成)
        //   pending_audit / audited → PENDING_AUDIT(重新 audit-pass → 触发新一轮生成)
        String xiangTargetStatus = BaogaoStatus.CREATED.getCode().equals(currentStatus)
                ? JianyanStatus.AUDIT_REJECTED.getCode()
                : JianyanStatus.PENDING_AUDIT.getCode();
        UpdateWrapper<LimsJianyanXiangEntity> xw = new UpdateWrapper<>();
        xw.eq("jianyan_liushuihao", qingyandan.getJianyanLiushuihao());
        xw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1));
        xw.set("jianyan_zhuangtai", xiangTargetStatus);
        limsJianyanXiangService.update(null, xw);
 
        recordRejectReportEvent(qingyandan.getJianyanLiushuihao(), qingyandanId, tuihuiYuanyin);
        operationalMessagePublisher.reportRejected(
                qingyandan, affectedItems, currentStatus, tuihuiYuanyin, userInfo, now);
        return true;
    }
 
    /**
     * 层 3 显式写一条「报告退回」审计事件。
     *
     * <p>为什么走层 3 而不是方法级 {@code @AuditLog}:本事件的 bizCode 是
     * {@code qingyandan.getJianyanLiushuihao()},而 {@code qingyandan} 是 {@link #rejectReport}
     * 方法体内 {@code getById} 查出来的**局部变量**——方法级 SpEL 只能取方法参数
     * ({@code qingyandanId} / {@code tuihuiYuanyin})与 {@code #result},够不着它。
     *
     * <p>{@code operationId} **本方法不传**,交 {@code AuditTxHolder} 单源补齐,理由同
     * {@code LimsJianceTaskServiceImpl#recordAuditFailEvent}——同处也记了「原注释说留空」的勘误。
     * 实测本事件的 operation_id 非空,且与同一批 AUDIT_FAIL 那次不同(两次操作各自成域)。
     *
     * <p>extra 用 {@code reason} 键,与层 2 @AuditLog 的 {@code extraExpr="{'reason': …}"} 对齐,
     * 不沿用旧表的 {@code note}——否则同一 actionCode 在 audit_events 里出现两种键名。
     */
    private void recordRejectReportEvent(String jianyanLiushuihao, String qingyandanId, String tuihuiYuanyin) {
        auditClientProvider.ifAvailable(client -> client.record(AuditEventDTO.builder()
                .eventType(AuditConsts.TYPE_BIZ_ACTION)
                .actionCode(BizLogAction.RETURN.getCode())
                .actionLabel("报告退回")
                .bizType(BizLogType.BAOGAO.getCode())
                .bizCode(jianyanLiushuihao)
                .targetTable("lims_qingyandan")
                .targetId(qingyandanId)
                .sourceLayer(3)
                .extra(JsonUtil.getObjectToString(Collections.singletonMap("reason", tuihuiYuanyin)))
                .build()));
    }
}