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;
|
}
|
}
|
}
|