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": "", "baogao_riqi": "yyyy-MM-dd", * "beizhu": "...", "biz_sign": "" } * 项目全局 JacksonConfig 已配 SnakeCaseStrategy,前端 baogao_riqi / beizhu / biz_sign * 等 snake_case 字段会自动反序列化到驼峰命名属性。 * *

授权:当前用户须在 lims_baogao_shouquan 中拥有 * shenhe_jibie 包含 baogao_zhizhuo 且 shenhe_fenlei 包含报告 biz_type 的授权记录。 * (shenhe_jibie 自 2026-05-21 起为多选 JSON 数组,一条授权可同时覆盖多个级别) * *

电子签名:与 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 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 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 分发): *

* 请求体:{ "action": "...", "id": "", "biz_sign": "", * "tuihuiyuanyin": "..."(仅 reject 必填) } * *

biz_sign 自 wave 2 起含义变更:从前端图形签名 base64 改为 lims_sign 主键。 * 调用方必须先走 /lims/biz/sign/verify 拿到 sign_id(op_type 为对应动作 / data_id=reportId), * 再带入此处。本接口会校验 sign 真实性 / op_type / data_id / 签名人,全过才放行。 * *

授权:每个动作均需对应 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 baogaoAction(@RequestBody Map 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 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 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 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 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; } } }