package jnpf.limsService.print; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import jnpf.exception.DataException; import jnpf.limsEntity.JianyanXiangmuGuanliEntity; import jnpf.limsEntity.LimsJianyanXiangEntity; import jnpf.limsEntity.LimsPrintTemplateEntity; import jnpf.limsEntity.LimsQingyandanEntity; import jnpf.limsMapper.JianyanXiangmuGuanliMapper; import jnpf.limsService.LimsJianyanXiangService; import jnpf.limsService.LimsQingyandanService; import jnpf.permission.UserApi; import jnpf.permission.entity.UserEntity; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.stream.Collectors; /** * "成品质量报告"模板渲染器(template_id = jianyan_baogao)。 * *

输入:lims_qingyandan.f_id;报告数据与表头都在这张行上(baogao_* 列)。 * 输出包含表头、检测项分组(按 fenxi_leixing 聚合 lims_jianyan_xiang)、 * 签名区(报告人/复核人/QC 经理)、依据 / 结论 / 备注(来自模板 config)。 */ @Component public class JianyanBaogaoRenderer implements PrintTemplateRenderer { public static final String TEMPLATE_ID = "jianyan_baogao"; @Autowired private LimsQingyandanService qingyandanService; @Autowired private LimsJianyanXiangService jianyanXiangService; @Autowired private JianyanXiangmuGuanliMapper xiangmuGuanliMapper; @Autowired private UserApi usersApi; @Autowired private ObjectMapper objectMapper; @Override public String getTemplateId() { return TEMPLATE_ID; } @Override public Map render(String qingyandanId, LimsPrintTemplateEntity tpl) { if (qingyandanId == null || qingyandanId.isEmpty()) { throw new DataException("缺少请验单 id"); } // 1. 请验单(报告数据 + 表头都在这一行上,必须存在 + 未软删) LimsQingyandanEntity qingyandan = qingyandanService.getById(qingyandanId); if (qingyandan == null || (qingyandan.getDeleteMark() != null && qingyandan.getDeleteMark() == 1)) { throw new DataException("请验单不存在或已删除: " + qingyandanId); } // 2. 该流水号下未软删的所有 jianyan_xiang QueryWrapper wrapper = new QueryWrapper<>(); wrapper.eq("jianyan_liushuihao", qingyandan.getJianyanLiushuihao()); wrapper.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); wrapper.orderByAsc("f_id"); List rows = jianyanXiangService.list(wrapper); // 3.1 排除 lims_jianyan_xiang.shifou_chuju_baogao = 'no' 的检验项。 // 仅显式 "no" 才会被剔除;NULL / 其它值按"出报告"处理(保守不误伤)。 // 该字段在 buildJianyanXiang / buildWaijianJianyanXiang 阶段就从 jiancexiang 快照过来, // 渲染时不再回查 lims_qingyandan_jiancexiang。 rows = rows.stream() .filter(r -> !"no".equalsIgnoreCase(r.getShifouChujuBaogao())) .collect(Collectors.toList()); // 4. 解析模板 config Map cfg = parseConfig(tpl); Map labels = asMap(cfg.get("labels")); // 5. 拼装返回结构(保持 LinkedHashMap 顺序便于阅读) // 顶层 companyName / docNumber / reportBookNumber 命名与前端模板 // jianyan_baogao.html 内 templateData.* 的读取键一致,避免前端再做映射。 // // docNumber 的真实业务语义是"产品质量标准文档编号 + 版次"(如 // STP-QAD-Z003-MP-R01-06),应跟随产品/产品标准维护。当前业务尚未 // 在主数据里建立该字段,**临时**用 jianyan_liushuihao(每次请验唯一) // 占位,避免页头空白。等 lims_quality_manager 等主数据补齐 doc_number // 后切回 cfg.doc_number 或新源即可(cfg.doc_number 字段保留不删)。 Map data = new LinkedHashMap<>(); data.put("template_id", tpl.getTemplateId()); data.put("template_name", tpl.getTemplateName()); data.put("companyName", asString(cfg.get("company_name"))); data.put("docNumber", asString(qingyandan.getJianyanLiushuihao())); data.put("reportBookNumber", asString(qingyandan.getBaogaoBianhao())); data.put("yiju", asString(cfg.get("yiju"))); data.put("header", buildHeader(qingyandan)); data.put("groups", buildGroups(rows)); data.put("jielun", asString(cfg.get("jielun"))); data.put("signatures", buildSignatures(qingyandan, labels)); data.put("beizhu", asString(cfg.get("beizhu"))); return data; } private Map buildHeader(LimsQingyandanEntity q) { Map h = new LinkedHashMap<>(); h.put("chanpin_mingcheng", q.getChanpinMingcheng()); h.put("pihao", q.getPihao()); h.put("laiyuan_bianma", q.getLaiyuanBianma()); h.put("chanpin_bianma", q.getChanpinBianma()); h.put("guige", q.getGuige()); h.put("shengchan_riqi", q.getShengchanRiqi()); h.put("piliang", q.getPiliang()); h.put("piliang_danwei", q.getPiliangDanwei()); h.put("youxiaoqi", q.getYouxiaoqi()); return h; } /** * 按"质量属性中文名"分组,组内按 jianyan_xiang 自然顺序(f_id 升序)。 * *

"质量属性"取自检验项目主数据 jianyan_xiangmu_guanli: *

    *
  1. 用 lims_jianyan_xiang.jianyan_xiangmu_bianma 反查主数据,得到 self;
  2. *
  3. self.fenxi_leixing 为空 → 质量属性 = self.mingcheng(自身既是质量属性又是检验项目);
  4. *
  5. self.fenxi_leixing 不空 → 用其值再次反查主数据 bianma 得到 parent, * 质量属性 = parent.mingcheng。
  6. *
* *

主数据缺失的容错:self 缺 → 用 lims_jianyan_xiang.jianyan_xiangmu_mingcheng 兜底; * parent 缺 → 用 self.mingcheng 兜底;都没有就空字符串。 * *

下发字段 key 仍叫 "fenxi_leixing",与历史前端模板兼容;其值的语义已变成 * "质量属性中文名"(不再是分类编码)。 */ private List> buildGroups(List rows) { if (rows.isEmpty()) { return new ArrayList<>(); } // 1. 一次性把这批 jianyan_xiang 涉及到的"自身 bianma"的主数据捞出来 Set selfBianmas = rows.stream() .map(LimsJianyanXiangEntity::getJianyanXiangmuBianma) .filter(s -> s != null && !s.isEmpty()) .collect(Collectors.toSet()); Map selfMap = fetchXiangmuByBianmas(selfBianmas); // 2. 在 self 集合里找需要进一步反查的 parent bianma Set parentBianmas = selfMap.values().stream() .map(JianyanXiangmuGuanliEntity::getFenxiLeixing) .filter(s -> s != null && !s.isEmpty()) .collect(Collectors.toSet()); Map parentMap = fetchXiangmuByBianmas(parentBianmas); // 3. 按"质量属性中文名"分组,保留出现顺序 Map> grouped = new LinkedHashMap<>(); for (LimsJianyanXiangEntity r : rows) { String key = resolveZhiliangShuxing(r, selfMap, parentMap); grouped.computeIfAbsent(key, k -> new ArrayList<>()).add(r); } // 4. 拼装 groups List> groups = new ArrayList<>(); for (Map.Entry> e : grouped.entrySet()) { Map g = new LinkedHashMap<>(); g.put("fenxi_leixing", e.getKey()); List> items = new ArrayList<>(); for (LimsJianyanXiangEntity r : e.getValue()) { Map item = new LinkedHashMap<>(); item.put("jianyan_xiangmu_mingcheng", r.getJianyanXiangmuMingcheng()); item.put("biaozhun_guiding", r.getBiaozhunGuiding()); item.put("jianyan_jieguo", r.getJianyanJieguo()); items.add(item); } g.put("items", items); groups.add(g); } return groups; } /** 批量按 bianma 反查主数据(自动过滤软删 + 多租户拦截器自动加 tenant 条件)。 */ private Map fetchXiangmuByBianmas(Set bianmas) { if (bianmas == null || bianmas.isEmpty()) { return new HashMap<>(); } QueryWrapper qw = new QueryWrapper<>(); qw.in("bianma", bianmas); qw.and(w -> w.isNull("f_delete_mark").or().ne("f_delete_mark", 1)); Map map = new HashMap<>(); for (JianyanXiangmuGuanliEntity e : xiangmuGuanliMapper.selectList(qw)) { if (e != null && e.getBianma() != null) { map.put(e.getBianma(), e); } } return map; } private String resolveZhiliangShuxing(LimsJianyanXiangEntity r, Map selfMap, Map parentMap) { JianyanXiangmuGuanliEntity self = selfMap.get(r.getJianyanXiangmuBianma()); if (self == null) { return r.getJianyanXiangmuMingcheng() != null ? r.getJianyanXiangmuMingcheng() : ""; } if (self.getFenxiLeixing() == null || self.getFenxiLeixing().isEmpty()) { return self.getMingcheng() != null ? self.getMingcheng() : ""; } JianyanXiangmuGuanliEntity parent = parentMap.get(self.getFenxiLeixing()); if (parent == null || parent.getMingcheng() == null) { return self.getMingcheng() != null ? self.getMingcheng() : ""; } return parent.getMingcheng(); } private Map buildSignatures(LimsQingyandanEntity r, Map labels) { // 三个签名人 id 一次批量查回真实姓名 List userIds = Arrays.asList(r.getBaogaoRen(), r.getBaogaoShenheRen(), r.getBaogaoPizhunRen()) .stream() .filter(s -> s != null && !s.isEmpty()) .distinct() .collect(Collectors.toList()); Map idToRealName = new HashMap<>(); if (!userIds.isEmpty()) { List users = usersApi.getUserName(userIds); if (users != null) { for (UserEntity u : users) { if (u != null && u.getId() != null) { idToRealName.put(u.getId(), u.getRealName()); } } } } Map sig = new LinkedHashMap<>(); sig.put("baogaoren", makeSignatureBlock(asString(labels.get("baogaoren"), "报告人/日期"), r.getBaogaoRen(), r.getBaogaoRiqi(), idToRealName)); sig.put("fuheren", makeSignatureBlock(asString(labels.get("fuheren"), "复核人/日期"), r.getBaogaoShenheRen(), r.getBaogaoShenheRiqi(), idToRealName)); sig.put("pizhunren", makeSignatureBlock(asString(labels.get("pizhunren"), "QC经理/日期"), r.getBaogaoPizhunRen(), r.getBaogaoPizhunRiqi(), idToRealName)); return sig; } private Map makeSignatureBlock(String label, String userId, Date date, Map idToRealName) { Map m = new LinkedHashMap<>(); m.put("label", label); m.put("user_id", userId); m.put("name", userId != null ? idToRealName.get(userId) : null); m.put("date", date); return m; } private Map parseConfig(LimsPrintTemplateEntity tpl) { String json = tpl.getConfig(); if (json == null || json.isEmpty()) { return Collections.emptyMap(); } try { return objectMapper.readValue(json, new TypeReference>() {}); } catch (Exception e) { throw new DataException("打印模板 config JSON 解析失败: " + e.getMessage()); } } private static String asString(Object v) { return v == null ? "" : v.toString(); } private static String asString(Object v, String fallback) { return v == null ? fallback : v.toString(); } @SuppressWarnings("unchecked") private static Map asMap(Object v) { if (v instanceof Map) { return (Map) v; } return Collections.emptyMap(); } }