刘光辉
14 小时以前 34981c30a78e8bbd7791131059a9210f9928b62c
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
package jnpf.limsController;
 
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jnpf.base.UserInfo;
import jnpf.exception.DataException;
import jnpf.limsEntity.BaogaoShenheJibie;
import jnpf.limsEntity.BaogaoStatus;
import jnpf.limsEntity.LimsBaogaoSubmitParam;
import jnpf.limsEntity.LimsQingyandanEntity;
import jnpf.limsEntity.LimsSignEntity;
import jnpf.limsEntity.LimsSignUsageTarget;
import jnpf.limsEntity.SignOpType;
import jnpf.limsService.LimsBaogaoShouquanService;
import jnpf.limsService.LimsQingyandanReportService;
import jnpf.limsService.LimsQingyandanService;
import jnpf.limsService.LimsSignService;
import jnpf.util.R;
import jnpf.util.UserProvider;
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/baogao")
public class LimsBaogaoController {
 
    @Autowired
    private LimsQingyandanReportService limsQingyandanReportService;
 
    @Autowired
    private LimsQingyandanService limsQingyandanService;
 
    @Autowired
    private LimsSignService limsSignService;
 
    @Autowired
    private LimsBaogaoShouquanService limsBaogaoShouquanService;
 
    /**
     * 生成报告(status: created → pending_audit)。
     * 请求体:{ "id": "<reportId>", "baogao_riqi": "yyyy-MM-dd",
     *          "beizhu": "...", "biz_sign": "<lims_sign.f_id>" }
     * 项目全局 JacksonConfig 已配 SnakeCaseStrategy,前端 baogao_riqi / beizhu / biz_sign
     * 等 snake_case 字段会自动反序列化到驼峰命名属性。
     *
     * <p>授权:当前用户须在 lims_baogao_shouquan 中拥有
     * shenhe_jibie 包含 baogao_zhizhuo 且 shenhe_fenlei 包含报告 biz_type 的授权记录。
     * (shenhe_jibie 自 2026-05-21 起为多选 JSON 数组,一条授权可同时覆盖多个级别)
     *
     * <p>电子签名:与 audit_pass / approve / reject 同模式。
     * 调用方先走 /lims/biz/sign/verify 拿到 sign_id(op_type=baogao_submit, data_id=报告 id),
     * 再带入此处。本接口会校验 sign 真实性 / op_type / data_id / 签名人,全过才放行。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "生成报告")
    @PostMapping("/submit")
    public R<String> submit(@RequestBody LimsBaogaoSubmitParam param) {
        if (param == null || param.getId() == null || param.getId().isEmpty()) {
            return R.error("缺少报告 id");
        }
        if (param.getBaogaoRiqi() == null) {
            return R.error("请选择报告日期");
        }
        if (param.getBizSign() == null || param.getBizSign().isEmpty()) {
            return R.error("请添加电子签名");
        }
 
        LimsQingyandanEntity report = limsQingyandanService.getById(param.getId());
        if (report == null
                || (report.getDeleteMark() != null && report.getDeleteMark() == 1)) {
            return R.error("报告不存在或已删除");
        }
        R<String> authErr = checkAuthority(report, BaogaoShenheJibie.ZHIZHUO);
        if (authErr != null) {
            return authErr;
        }
        LimsSignEntity sign;
        try {
            sign = limsSignService.requireValidSign(
                    param.getBizSign(), SignOpType.BAOGAO_SUBMIT, param.getId());
        } catch (DataException e) {
            return R.error(e.getMessage());
        }
 
        boolean ok = limsQingyandanReportService.submitReport(param);
        if (!ok) {
            return R.error("当前报告不在待生成状态,无法重复生成");
        }
        recordReportSignUsage(sign, SignOpType.BAOGAO_SUBMIT, report);
        return R.success("已生成报告");
    }
 
    /**
     * 报告操作(按 action 分发):
     * <ul>
     *   <li>action=reject:退回 / 不通过(created / pending_audit / audited 都可退;
     *       报告软删;jianyan_xiang 由 created 退回时改 audit_rejected,
     *       由 pending_audit / audited 退回时改 pending_audit)</li>
     *   <li>action=audit_pass:审核通过(pending_audit → audited,写 shenhe_ren / shenhe_riqi)</li>
     *   <li>action=approve:批准(audited → approved,写 pizhun_ren / pizhun_riqi)</li>
     * </ul>
     * 请求体:{ "action": "...", "id": "<reportId>", "biz_sign": "<lims_sign.f_id>",
     *          "tuihuiyuanyin": "..."(仅 reject 必填) }
     *
     * <p>biz_sign 自 wave 2 起含义变更:从前端图形签名 base64 改为 lims_sign 主键。
     * 调用方必须先走 /lims/biz/sign/verify 拿到 sign_id(op_type 为对应动作 / data_id=reportId),
     * 再带入此处。本接口会校验 sign 真实性 / op_type / data_id / 签名人,全过才放行。
     *
     * <p>授权:每个动作均需对应 shenhe_jibie 的授权记录覆盖报告 biz_type:
     * audit_pass=baogao_shenhe / approve=baogao_qianfa /
     * reject 按当前状态映射(created→zhizhuo / pending_audit→shenhe / audited→qianfa)。
     */
    @Transactional(rollbackFor = Exception.class)
    @Operation(summary = "报告操作(退回项目等)")
    @PostMapping("/action")
    public R<String> baogaoAction(@RequestBody Map<String, String> params) {
        String action = params.get("action");
        String id = params.get("id");
        String tuihuiyuanyin = params.get("tuihuiyuanyin");
        String bizSign = params.get("biz_sign");
 
        if (action == null || action.isEmpty()) {
            return R.error("缺少 action");
        }
        if (id == null || id.isEmpty()) {
            return R.error("缺少报告 id");
        }
 
        LimsQingyandanEntity report = limsQingyandanService.getById(id);
        if (report == null
                || (report.getDeleteMark() != null && report.getDeleteMark() == 1)) {
            return R.error("报告不存在或已删除");
        }
 
        switch (action) {
            case "reject":
                if (tuihuiyuanyin == null || tuihuiyuanyin.isEmpty()) {
                    return R.error("请填写退回原因");
                }
                if (bizSign == null || bizSign.isEmpty()) {
                    return R.error("请添加电子签名");
                }
                BaogaoStatus currentStatus = parseBaogaoStatus(report.getBaogaoZhuangtai());
                BaogaoShenheJibie rejectJibie = BaogaoShenheJibie.forReject(currentStatus);
                if (rejectJibie == null) {
                    return R.error("当前报告状态不允许退回");
                }
                R<String> rejectAuthErr = checkAuthority(report, rejectJibie);
                if (rejectAuthErr != null) {
                    return rejectAuthErr;
                }
                LimsSignEntity rejectSign;
                try {
                    rejectSign = limsSignService.requireValidSign(
                            bizSign, SignOpType.BAOGAO_REJECT, id);
                } catch (DataException e) {
                    return R.error(e.getMessage());
                }
                boolean ok = limsQingyandanReportService.rejectReport(id, tuihuiyuanyin);
                if (!ok) {
                    return R.error("当前报告状态不允许退回");
                }
                recordReportSignUsage(rejectSign, SignOpType.BAOGAO_REJECT, report);
                return R.success("已退回");
 
            case "audit_pass":
                if (bizSign == null || bizSign.isEmpty()) {
                    return R.error("请添加电子签名");
                }
                R<String> auditAuthErr = checkAuthority(report, BaogaoShenheJibie.SHENHE);
                if (auditAuthErr != null) {
                    return auditAuthErr;
                }
                LimsSignEntity auditSign;
                try {
                    auditSign = limsSignService.requireValidSign(
                            bizSign, SignOpType.BAOGAO_AUDIT_PASS, id);
                } catch (DataException e) {
                    return R.error(e.getMessage());
                }
                if (!limsQingyandanReportService.auditPassReport(id)) {
                    return R.error("当前报告不在待审核状态,无法审核通过");
                }
                recordReportSignUsage(auditSign, SignOpType.BAOGAO_AUDIT_PASS, report);
                return R.success("已审核通过");
 
            case "approve":
                if (bizSign == null || bizSign.isEmpty()) {
                    return R.error("请添加电子签名");
                }
                R<String> approveAuthErr = checkAuthority(report, BaogaoShenheJibie.QIANFA);
                if (approveAuthErr != null) {
                    return approveAuthErr;
                }
                LimsSignEntity approveSign;
                try {
                    approveSign = limsSignService.requireValidSign(
                            bizSign, SignOpType.BAOGAO_APPROVE, id);
                } catch (DataException e) {
                    return R.error(e.getMessage());
                }
                if (!limsQingyandanReportService.approveReport(id)) {
                    return R.error("当前报告不在已审核状态,无法批准");
                }
                recordReportSignUsage(approveSign, SignOpType.BAOGAO_APPROVE, report);
                return R.success("已批准");
 
            default:
                return R.error("不支持的操作: " + action);
        }
    }
 
    /**
     * 当前用户对该报告的 jibie 是否有授权;无授权返回错误 R,已授权返回 null。
     * 报告 biz_type 为空 → 一律拒绝(不允许"无分类"报告绕过授权)。
     */
    private R<String> checkAuthority(LimsQingyandanEntity report, BaogaoShenheJibie jibie) {
        String bizType = report.getBizType();
        if (bizType == null || bizType.isEmpty()) {
            return R.error("报告缺少分类(biz_type),无法判定授权");
        }
        UserInfo userInfo = UserProvider.getUser();
        String userId = userInfo == null ? null : userInfo.getUserId();
        if (userId == null || userId.isEmpty()) {
            return R.error("无法获取当前用户");
        }
        if (!limsBaogaoShouquanService.hasAuthority(userId, jibie, bizType)) {
            return R.error("无权操作:当前用户未被授予该分类的"
                    + jibieLabel(jibie) + "权限");
        }
        return null;
    }
 
    private void recordReportSignUsage(LimsSignEntity sign,
                                       SignOpType opType,
                                       LimsQingyandanEntity report) {
        LimsSignUsageTarget target = LimsSignUsageTarget.builder()
                .targetTable("lims_qingyandan")
                .targetId(report.getId())
                .bizType("BAOGAO")
                .bizModule("LIMS-请验单")
                .bizCode(report.getJianyanLiushuihao())
                .build();
        limsSignService.recordSignUsage(sign, opType,
                Collections.singletonList(report.getId()), Collections.singletonList(target));
    }
 
    private String jibieLabel(BaogaoShenheJibie jibie) {
        switch (jibie) {
            case ZHIZHUO: return "报告制作";
            case SHENHE:  return "报告审核";
            case QIANFA:  return "报告签发";
            default:      return jibie.getCode();
        }
    }
 
    /** 状态字符串 → 枚举;未知值返回 null(reject 路径据此提示状态不允许)。 */
    private BaogaoStatus parseBaogaoStatus(String code) {
        if (code == null) {
            return null;
        }
        try {
            return BaogaoStatus.fromCode(code);
        } catch (IllegalArgumentException ex) {
            return null;
        }
    }
}