Selaa lähdekoodia

气密漏点模块

wangxichen 1 viikko sitten
vanhempi
commit
e129016329

BIN
bg/bcp.png


BIN
bg/kj.png


BIN
bg/zlcp.png


+ 58 - 0
db/ddl-leakage-point-stat.sql

@@ -0,0 +1,58 @@
+-- ============================================
+-- 气密泄露点位模块 DDL(从 T09 迁移过来)
+-- 目标库:mes_cloud_610
+-- 包含:
+--   1) 建表 mes_product_hf
+--   2) 追加菜单 生产管理/气密泄露点位
+-- ============================================
+
+-- --------------------------------------------
+-- 1) 工件焊缝/漏点原始记录表
+-- --------------------------------------------
+CREATE TABLE IF NOT EXISTS `mes_product_hf` (
+  `id`          varchar(64)  NOT NULL COMMENT 'id',
+  `sn`          varchar(255) DEFAULT NULL COMMENT '工件码',
+  `oprno`       varchar(64)  DEFAULT NULL COMMENT '工位号',
+  `line_sn`     varchar(64)  DEFAULT NULL COMMENT '产线编号',
+  `hf_flag`     varchar(64)  DEFAULT NULL COMMENT '焊缝/漏点标识',
+  `result`      varchar(64)  DEFAULT NULL COMMENT '结果',
+  `create_by`   varchar(64)  DEFAULT NULL COMMENT '创建人',
+  `create_date` datetime     DEFAULT NULL COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_mes_product_hf_sn` (`sn`),
+  KEY `idx_mes_product_hf_oprno` (`oprno`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='工件焊缝/漏点记录';
+
+
+-- --------------------------------------------
+-- 2) 追加菜单 生产管理/气密泄露点位(照抄 T09 那条)
+-- 前提:610 库"生产管理"父菜单 menu_code 与 T09 一致(=1672118505143263232)
+-- --------------------------------------------
+INSERT INTO `js_sys_menu` (
+  menu_code, parent_code, parent_codes, tree_sort, tree_sorts, tree_leaf, tree_level, tree_names,
+  menu_name, menu_type, menu_href, menu_target, menu_icon, menu_color, menu_title, permission,
+  weight, is_show, sys_code, module_codes, component, params, status,
+  create_by, create_date, update_by, update_date, remarks
+) VALUES (
+  '1780000000000000001',
+  '1672118505143263232',
+  '0,1672118505143263232,',
+  750,
+  '0000009290,0000000750,',
+  '1',
+  1,
+  '生产管理/气密泄露点位',
+  '气密泄露点位', '1', '/mes/mesProductLeakagePointStat/list', '', 'fa fa-map-marker', '', '', 'mes:mesProductQm:view',
+  40, '1', 'default', 'core', '', '', '0',
+  'system', NOW(), 'system', NOW(), '按泄露点位和工件号统计'
+);
+
+-- --------------------------------------------
+-- 结束
+-- 建完表和菜单后:
+--   1) 重启服务
+--   2) 用超管账号刷新菜单权限(jeesite 默认自动生效)
+--   3) 访问 /mes/mesProductLeakagePointStat 或从左侧菜单进入
+--   4) 数据来源:mes_product_hf.hf_flag + mes_product_qm + mes_product_record
+--      610 现场若气密仪暂未把漏点串写入 mes_product_hf,表会是空的,页面还能正常打开
+-- --------------------------------------------

+ 13 - 0
src/main/java/com/jeesite/modules/mes/dao/MesProductHfDao.java

@@ -0,0 +1,13 @@
+package com.jeesite.modules.mes.dao;
+
+import com.jeesite.common.dao.CrudDao;
+import com.jeesite.common.mybatis.annotation.MyBatisDao;
+import com.jeesite.modules.mes.entity.MesProductHf;
+
+/**
+ * 焊缝/漏点记录DAO接口
+ */
+@MyBatisDao
+public interface MesProductHfDao extends CrudDao<MesProductHf> {
+
+}

+ 119 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductHf.java

@@ -0,0 +1,119 @@
+package com.jeesite.modules.mes.entity;
+
+import javax.validation.constraints.Size;
+import com.jeesite.common.entity.DataEntity;
+import com.jeesite.common.mybatis.annotation.Column;
+import com.jeesite.common.mybatis.annotation.Table;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
+import com.jeesite.common.utils.excel.annotation.ExcelFields;
+
+import java.util.Date;
+
+/**
+ * 工件焊缝/漏点记录 Entity
+ * @author JinJuShi
+ * @version 2025-01-01
+ */
+@Table(name="mes_product_hf", alias="a", label="工件焊缝信息", columns={
+        @Column(name="id", attrName="id", label="id", isPK=true),
+        @Column(name="sn", attrName="sn", label="工件码"),
+        @Column(name="oprno", attrName="oprno", label="工位号"),
+        @Column(name="line_sn", attrName="lineSn", label="产线编号"),
+        @Column(name="hf_flag", attrName="hfFlag", label="焊缝标识"),
+        @Column(name="result", attrName="result", label="结果"),
+        @Column(name="create_by", attrName="createBy", label="创建人", isUpdate=false, isQuery=false),
+        @Column(name="create_date", attrName="createDate", label="创建时间", isUpdate=false, isQuery=false),
+}, orderBy="a.id DESC"
+)
+public class MesProductHf extends DataEntity<MesProductHf> {
+
+    private static final long serialVersionUID = 1L;
+    private String sn;		// 工件码
+    private String oprno;		// 工位号
+    private String lineSn;		// 产线编号
+    private String hfFlag;		// 焊缝/漏点标识
+    private String result;
+
+
+    @ExcelFields({
+            @ExcelField(title="工件码", attrName="sn", align=Align.CENTER, sort=10),
+            @ExcelField(title="工位号", attrName="oprno", align=Align.CENTER, sort=20),
+            @ExcelField(title="产线编号", attrName="lineSn", align=Align.CENTER, sort=30),
+            @ExcelField(title="焊缝/漏检标识", attrName="hfFlag", align=Align.CENTER, sort=40),
+            @ExcelField(title="结果", attrName="result", align=Align.CENTER, sort=40),
+            @ExcelField(title="创建人", attrName="createBy", align=Align.CENTER, sort=40),
+            @ExcelField(title="创建时间", attrName="createDate", align=Align.CENTER, sort=80, dataFormat="yyyy-MM-dd HH:mm:ss"),
+    })
+
+    public MesProductHf() {
+        this(null);
+    }
+
+    public MesProductHf(String id){
+        super(id);
+    }
+
+    @Size(min=0, max=255, message="工件码长度不能超过 255 个字符")
+    public String getSn() {
+        return sn;
+    }
+
+    public void setSn(String sn) {
+        this.sn = sn;
+    }
+
+    @Size(min=0, max=64, message="工位号长度不能超过 64 个字符")
+    public String getOprno() {
+        return oprno;
+    }
+
+    public void setOprno(String oprno) {
+        this.oprno = oprno;
+    }
+
+    @Size(min=0, max=64, message="产线编号长度不能超过 64 个字符")
+    public String getLineSn() {
+        return lineSn;
+    }
+
+    public void setLineSn(String lineSn) {
+        this.lineSn = lineSn;
+    }
+
+    @Size(min=0, max=64, message="焊缝标识长度不能超过 64 个字符")
+    public String getHfFlag() {
+        return hfFlag;
+    }
+
+    public void setHfFlag(String hfFlag) {
+        this.hfFlag = hfFlag;
+    }
+
+    @Size(min=0, max=64, message="结果长度不能超过 64 个字符")
+    public String getResult() {
+        return result;
+    }
+
+    public void setResult(String result) {
+        this.result = result;
+    }
+
+    public Date createDate_gte() {
+        return sqlMap.getWhere().getValue("create_date", QueryType.GTE);
+    }
+
+    public void setCreateDate_gte(Date createDate) {
+        sqlMap.getWhere().and("create_date", QueryType.GTE, createDate);
+    }
+
+    public Date getCreateDate_lte() {
+        return sqlMap.getWhere().getValue("create_date", QueryType.LTE);
+    }
+
+    public void setCreateDate_lte(Date createDate) {
+        sqlMap.getWhere().and("create_date", QueryType.LTE, createDate);
+    }
+
+}

+ 39 - 0
src/main/java/com/jeesite/modules/mes/entity/MesProductLeakagePointStat.java

@@ -0,0 +1,39 @@
+package com.jeesite.modules.mes.entity;
+
+import java.util.Date;
+
+import com.jeesite.common.utils.excel.annotation.ExcelField;
+import com.jeesite.common.utils.excel.annotation.ExcelField.Align;
+import com.jeesite.common.utils.excel.annotation.ExcelFields;
+
+/**
+ * 气密泄露点位按工件汇总的展示数据。
+ */
+public class MesProductLeakagePointStat {
+
+    private String leakagePoint;
+    private String sn;
+    private int occurrenceCount;
+    private Date firstOccurrenceTime;
+    private Date lastOccurrenceTime;
+
+    @ExcelFields({
+            @ExcelField(title = "泄露点位", attrName = "leakagePoint", align = Align.CENTER, sort = 10),
+            @ExcelField(title = "工件号", attrName = "sn", align = Align.CENTER, sort = 20),
+            @ExcelField(title = "出现次数", attrName = "occurrenceCount", align = Align.CENTER, sort = 30),
+            @ExcelField(title = "首次上报时间", attrName = "firstOccurrenceTime", align = Align.CENTER, sort = 40, dataFormat = "yyyy-MM-dd HH:mm:ss"),
+            @ExcelField(title = "最近上报时间", attrName = "lastOccurrenceTime", align = Align.CENTER, sort = 50, dataFormat = "yyyy-MM-dd HH:mm:ss")
+    })
+    public MesProductLeakagePointStat() { }
+
+    public String getLeakagePoint() { return leakagePoint; }
+    public void setLeakagePoint(String leakagePoint) { this.leakagePoint = leakagePoint; }
+    public String getSn() { return sn; }
+    public void setSn(String sn) { this.sn = sn; }
+    public int getOccurrenceCount() { return occurrenceCount; }
+    public void setOccurrenceCount(int occurrenceCount) { this.occurrenceCount = occurrenceCount; }
+    public Date getFirstOccurrenceTime() { return firstOccurrenceTime; }
+    public void setFirstOccurrenceTime(Date firstOccurrenceTime) { this.firstOccurrenceTime = firstOccurrenceTime; }
+    public Date getLastOccurrenceTime() { return lastOccurrenceTime; }
+    public void setLastOccurrenceTime(Date lastOccurrenceTime) { this.lastOccurrenceTime = lastOccurrenceTime; }
+}

+ 10 - 0
src/main/java/com/jeesite/modules/mes/service/MesProductHfService.java

@@ -0,0 +1,10 @@
+package com.jeesite.modules.mes.service;
+
+import com.jeesite.common.service.CrudService;
+import com.jeesite.modules.mes.dao.MesProductHfDao;
+import com.jeesite.modules.mes.entity.MesProductHf;
+import org.springframework.stereotype.Service;
+
+@Service
+public class MesProductHfService extends CrudService<MesProductHfDao, MesProductHf> {
+}

+ 516 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductLeakagePointStatController.java

@@ -0,0 +1,516 @@
+package com.jeesite.modules.mes.web;
+
+import com.jeesite.common.entity.Page;
+import com.jeesite.common.lang.StringUtils;
+import com.jeesite.common.mybatis.mapper.query.QueryType;
+import com.jeesite.common.web.BaseController;
+import com.jeesite.modules.mes.entity.MesProductHf;
+import com.jeesite.modules.mes.entity.MesProductQm;
+import com.jeesite.modules.mes.entity.MesProductRecord;
+import com.jeesite.modules.mes.entity.MesProductLeakagePointStat;
+import com.jeesite.modules.mes.req.MesSearchDateReq;
+import com.jeesite.modules.mes.service.MesProductHfService;
+import com.jeesite.modules.mes.service.MesProductQmService;
+import com.jeesite.modules.mes.service.MesProductRecordService;
+import com.jeesite.modules.mes.util.CommonUitl;
+import org.apache.shiro.authz.annotation.RequiresPermissions;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.ServletOutputStream;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.Date;
+import java.text.SimpleDateFormat;
+import java.net.URLEncoder;
+
+import org.apache.poi.ss.usermodel.BorderStyle;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellStyle;
+import org.apache.poi.ss.usermodel.HorizontalAlignment;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.ss.usermodel.Workbook;
+import org.apache.poi.ss.util.CellRangeAddress;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import java.util.HashMap;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/** 气密泄露点位统计 Controller。 */
+@Controller
+@RequestMapping(value = "${adminPath}/mes/mesProductLeakagePointStat")
+public class MesProductLeakagePointStatController extends BaseController {
+
+    @Autowired
+    private MesProductHfService mesProductHfService;
+
+    @Autowired
+    private MesProductQmService mesProductQmService;
+
+    @Autowired
+    private MesProductRecordService mesProductRecordService;
+
+    @RequiresPermissions("mes:mesProductQm:view")
+    @RequestMapping(value = {"list", ""})
+    public String list(Model model) {
+        return "redirect:" + adminPath + "/mes/mesProductLeakagePointStat/chart";
+    }
+
+    @RequiresPermissions("mes:mesProductQm:view")
+    @RequestMapping(value = "listData")
+    @ResponseBody
+    public Page<MesProductLeakagePointStat> listData(String leakagePoint, String sn, Date startDate,
+            Date endDate, HttpServletRequest request, HttpServletResponse response) {
+        List<MesProductLeakagePointStat> rows = buildStatistics(leakagePoint, sn, startDate, endDate, "all");
+        Page<MesProductLeakagePointStat> page = new Page<>(request, response);
+        page.setCount(rows.size());
+        int from = Math.max(0, (page.getPageNo() - 1) * page.getPageSize());
+        int to = Math.min(rows.size(), from + page.getPageSize());
+        page.setList(from < to ? rows.subList(from, to) : new ArrayList<MesProductLeakagePointStat>());
+        return page;
+    }
+
+    @RequiresPermissions("mes:mesProductQm:view")
+    @RequestMapping(value = "chart")
+    public String chart(Model model) {
+        MesSearchDateReq dateRange = CommonUitl.getSearchDate();
+        model.addAttribute("startDate", dateRange.getStartDate());
+        model.addAttribute("endDate", dateRange.getEndtDate());
+        return "modules/mes/mesProductLeakagePointStatChart";
+    }
+
+    @RequiresPermissions("mes:mesProductQm:view")
+    @RequestMapping(value = "chartData")
+    @ResponseBody
+    public Map<String, Object> chartData(String leakagePoint, String sn, String dataType, Date startDate, Date endDate) {
+        List<MesProductHf> records = findRecords(sn, startDate, endDate, dataType);
+        Map<String, Object> qualityData = buildQualityData(records, startDate, endDate, dataType);
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> occurrences = (List<Map<String, Object>>) qualityData.remove("defectOccurrences");
+        List<Map<String, Object>> points = new ArrayList<>();
+        Map<String, Integer> pointCounts = new LinkedHashMap<>();
+        @SuppressWarnings("unchecked")
+        List<Map<String, Object>> defectCategories = (List<Map<String, Object>>) qualityData.get("defectCategories");
+        for (Map<String, Object> category : defectCategories) {
+            String point = (String) category.get("label");
+            Integer count = (Integer) category.get("count");
+            if ("找不到漏点".equals(point) && count == 0) continue;
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("point", point);
+            row.put("count", count);
+            points.add(row);
+            pointCounts.put(point, count);
+        }
+        String selectedPoint = StringUtils.isNotBlank(leakagePoint) ? leakagePoint.trim()
+                : (points.isEmpty() ? "" : (String) points.get(0).get("point"));
+
+        Map<String, MesProductLeakagePointStat> detailsBySn = new LinkedHashMap<>();
+        Map<String, Integer> dailyCounts = new LinkedHashMap<>();
+        SimpleDateFormat dayFormat = new SimpleDateFormat("MM-dd");
+        for (Map<String, Object> occurrence : occurrences) {
+            if (!selectedPoint.equals(occurrence.get("point"))) continue;
+            String occurrenceSn = (String) occurrence.get("sn");
+            if (StringUtils.isBlank(occurrenceSn)) continue;
+            Date occurrenceTime = (Date) occurrence.get("time");
+            MesProductLeakagePointStat detail = detailsBySn.get(occurrenceSn);
+            if (detail == null) {
+                detail = new MesProductLeakagePointStat();
+                detail.setLeakagePoint(selectedPoint);
+                detail.setSn(occurrenceSn);
+                detail.setFirstOccurrenceTime(occurrenceTime);
+                detailsBySn.put(occurrenceSn, detail);
+            }
+            detail.setOccurrenceCount(detail.getOccurrenceCount() + 1);
+            if (occurrenceTime != null && (detail.getFirstOccurrenceTime() == null || occurrenceTime.before(detail.getFirstOccurrenceTime()))) {
+                detail.setFirstOccurrenceTime(occurrenceTime);
+            }
+            if (occurrenceTime != null && (detail.getLastOccurrenceTime() == null || occurrenceTime.after(detail.getLastOccurrenceTime()))) {
+                detail.setLastOccurrenceTime(occurrenceTime);
+            }
+            if (occurrenceTime != null) {
+                String day = dayFormat.format(occurrenceTime);
+                dailyCounts.put(day, dailyCounts.getOrDefault(day, 0) + 1);
+            }
+        }
+
+        List<MesProductLeakagePointStat> details = new ArrayList<>(detailsBySn.values());
+        details.sort(Comparator.comparing(MesProductLeakagePointStat::getOccurrenceCount, Comparator.reverseOrder())
+                .thenComparing(MesProductLeakagePointStat::getLastOccurrenceTime, Comparator.nullsLast(Comparator.reverseOrder())));
+        List<Map<String, Object>> trend = new ArrayList<>();
+        for (Map.Entry<String, Integer> entry : dailyCounts.entrySet()) {
+            Map<String, Object> row = new LinkedHashMap<>();
+            row.put("day", entry.getKey());
+            row.put("count", entry.getValue());
+            trend.add(row);
+        }
+        trend.sort(Comparator.comparing(row -> (String) row.get("day")));
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("points", points);
+        result.put("selectedPoint", selectedPoint);
+        result.put("details", details);
+        result.put("trend", trend);
+        result.put("totalCount", pointCounts.getOrDefault(selectedPoint, 0));
+        result.put("workpieceCount", details.size());
+        result.putAll(qualityData);
+        result.put("rawRecordCount", result.get("qualityTotal"));
+        return result;
+    }
+
+    private Map<String, Object> buildQualityData(List<MesProductHf> hfRecords, Date startDate, Date endDate, String dataType) {
+        MesProductQm query = new MesProductQm();
+        if (startDate != null) query.setTestTime_gte(startDate);
+        if (endDate != null) query.setTestTime_lte(endDate);
+        int qualified = 0;
+        int defective = 0;
+        int noPoint = 0;
+        Map<String, Integer> pointCounts = new LinkedHashMap<>();
+        List<Map<String, Object>> occurrences = new ArrayList<>();
+        Set<String> countedDefectOccurrences = new LinkedHashSet<>();
+        Map<String, Set<String>> leakagePointsByProduct = buildLeakagePointsByProduct(hfRecords);
+        // INSP030A/B 的生产记录是总结果的权威来源,优先于气密仪记录,避免重复计数。
+        List<MesProductRecord> productRecords = findProductRecords(startDate, endDate);
+        Set<String> productionRecordKeys = new LinkedHashSet<>();
+        for (MesProductRecord record : productRecords) {
+            if (matchesDataType(record.getOprno(), dataType)) {
+                productionRecordKeys.add(leakagePointKey(record.getSn(), record.getOprno()));
+            }
+        }
+        for (MesProductQm record : mesProductQmService.findList(query)) {
+            if (!matchesDataType(record.getOprno(), dataType)) continue;
+            if (productionRecordKeys.contains(leakagePointKey(record.getComponentNum(), record.getOprno()))) continue;
+            if ("OK".equalsIgnoreCase(record.getTestResult())) qualified++;
+            else {
+                Set<String> points = leakagePointsByProduct.get(leakagePointKey(record.getComponentNum(), record.getOprno()));
+                if (points == null || points.isEmpty()) {
+                    defective++;
+                    noPoint++;
+                    addDefectOccurrence(occurrences, "找不到漏点", record.getComponentNum(), record.getTestTime());
+                } else {
+                    defective += addUniqueLeakagePoints(record.getComponentNum(), record.getTestTime(), points,
+                            countedDefectOccurrences, pointCounts, occurrences);
+                }
+            }
+        }
+        for (MesProductRecord record : productRecords) {
+            if (!matchesDataType(record.getOprno(), dataType)) continue;
+            if ("OK".equalsIgnoreCase(record.getContent())) qualified++;
+            else {
+                Set<String> points = leakagePointsByProduct.get(leakagePointKey(record.getSn(), record.getOprno()));
+                if (points == null || points.isEmpty()) {
+                    defective++;
+                    noPoint++;
+                    addDefectOccurrence(occurrences, "找不到漏点", record.getSn(), record.getUpdateDate());
+                } else {
+                    defective += addUniqueLeakagePoints(record.getSn(), record.getUpdateDate(), points,
+                            countedDefectOccurrences, pointCounts, occurrences);
+                }
+            }
+        }
+        for (MesProductHf record : hfRecords) {
+            if ("OK".equalsIgnoreCase(record.getResult())) { qualified++; continue; }
+            Set<String> points = parseLeakagePoints(record.getHfFlag());
+            if (points == null || points.isEmpty()) {
+                defective++;
+                noPoint++;
+                addDefectOccurrence(occurrences, "找不到漏点", record.getSn(), record.getCreateDate());
+            } else {
+                defective += addUniqueLeakagePoints(record.getSn(), record.getCreateDate(), points,
+                        countedDefectOccurrences, pointCounts, occurrences);
+            }
+        }
+        List<Map<String, Object>> quality = new ArrayList<>();
+        Map<String, Object> ok = new LinkedHashMap<>(); ok.put("label", "合格数量"); ok.put("count", qualified); ok.put("color", "#2b8c70"); quality.add(ok);
+        Map<String, Object> ng = new LinkedHashMap<>(); ng.put("label", "不良数量"); ng.put("count", defective); ng.put("color", "#d65b43"); quality.add(ng);
+        List<Map<String, Object>> defects = new ArrayList<>();
+        String[] colors = {"#c94f3b", "#df8a43", "#3b94a1", "#6f8d4e", "#7d6aa3", "#5682b8"};
+        int colorIndex = 0;
+        for (Map.Entry<String, Integer> entry : pointCounts.entrySet()) {
+            Map<String, Object> item = new LinkedHashMap<>(); item.put("label", entry.getKey()); item.put("count", entry.getValue());
+            item.put("color", colors[colorIndex++ % colors.length]); defects.add(item);
+        }
+        Map<String, Object> missing = new LinkedHashMap<>(); missing.put("label", "找不到漏点"); missing.put("count", noPoint); missing.put("color", "#8998a3"); defects.add(missing);
+        defects.sort((a, b) -> Integer.compare((Integer) b.get("count"), (Integer) a.get("count")));
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("qualifiedCount", qualified); result.put("defectiveCount", defective); result.put("noPointCount", noPoint);
+        result.put("qualityCategories", quality); result.put("defectCategories", defects); result.put("qualityTotal", qualified + defective);
+        result.put("defectOccurrences", occurrences);
+        return result;
+    }
+
+    /** Matches the leakage-point enrichment used by the gas-tightness record page. */
+    private Map<String, Set<String>> buildLeakagePointsByProduct(List<MesProductHf> records) {
+        Map<String, Set<String>> pointsByProduct = new HashMap<>();
+        for (MesProductHf record : records) {
+            Set<String> points = pointsByProduct.computeIfAbsent(
+                    leakagePointKey(record.getSn(), record.getOprno()), key -> new LinkedHashSet<>());
+            points.addAll(parseLeakagePoints(record.getHfFlag()));
+        }
+        return pointsByProduct;
+    }
+
+    private boolean addLeakagePoints(Set<String> points, Map<String, Integer> pointCounts) {
+        if (points == null || points.isEmpty()) return false;
+        for (String point : points) pointCounts.put(point, pointCounts.getOrDefault(point, 0) + 1);
+        return true;
+    }
+
+    /** Keeps raw point occurrences for chart details, but counts repeat points once for quality rates. */
+    private int addUniqueLeakagePoints(String sn, Date time, Set<String> points, Set<String> countedOccurrences,
+            Map<String, Integer> pointCounts, List<Map<String, Object>> occurrences) {
+        int added = 0;
+        for (String point : points) {
+            // Point composition, point summary, and workpiece details use every original submission.
+            addDefectOccurrence(occurrences, point, sn, time);
+            pointCounts.put(point, pointCounts.getOrDefault(point, 0) + 1);
+            // Only the quality-result numerator/denominator deduplicates a workpiece-point pair.
+            if (!countedOccurrences.add(leakagePointOccurrenceKey(sn, point))) continue;
+            added++;
+        }
+        return added;
+    }
+
+    private Set<String> parseLeakagePoints(String value) {
+        Set<String> points = new LinkedHashSet<>();
+        for (String item : (value == null ? "" : value).split(",")) {
+            String point = normalizeLeakPoint(item);
+            if (StringUtils.isNotBlank(point)) points.add(point);
+        }
+        return points;
+    }
+
+    private void addDefectOccurrences(List<Map<String, Object>> occurrences, String sn, Date time, Set<String> points) {
+        for (String point : points) {
+            addDefectOccurrence(occurrences, point, sn, time);
+        }
+    }
+
+    private void addDefectOccurrence(List<Map<String, Object>> occurrences, String point, String sn, Date time) {
+        Map<String, Object> occurrence = new LinkedHashMap<>();
+        occurrence.put("point", point);
+        occurrence.put("sn", sn);
+        occurrence.put("time", time);
+        occurrences.add(occurrence);
+    }
+
+    private String leakagePointKey(String sn, String oprno) {
+        return StringUtils.defaultString(sn) + "|" + StringUtils.defaultString(oprno);
+    }
+
+    private String leakagePointOccurrenceKey(String sn, String point) {
+        return StringUtils.defaultString(sn) + "\u0000" + StringUtils.defaultString(point);
+    }
+
+    /** Overall air-tightness results from INSP030A/B are stored in the production-record table. */
+    private List<MesProductRecord> findProductRecords(Date startDate, Date endDate) {
+        MesProductRecord query = new MesProductRecord();
+        if (startDate != null) query.setUpdateDate_gte(startDate);
+        if (endDate != null) query.setUpdateDate_lte(endDate);
+        query.getSqlMap().getWhere()
+                .and("a.oprno", QueryType.IN, new String[] {"INSP030A", "INSP030B"})
+                .and("a.craft", QueryType.EQ, "100000");
+        return mesProductRecordService.findList(query);
+    }
+
+    @RequiresPermissions("mes:mesProductQm:view")
+    @RequestMapping(value = "chartExport")
+    public void chartExport(String sn, String dataType, Date startDate, Date endDate, HttpServletResponse response) throws Exception {
+        List<MesProductLeakagePointStat> rows = buildChartStatistics(sn, startDate, endDate, dataType);
+        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm");
+        String start = startDate == null ? "全部" : dateFormat.format(startDate);
+        String end = endDate == null ? "全部" : dateFormat.format(endDate);
+        String stationResult = dataTypeLabel(dataType);
+        String timeRange = start + " 至 " + end;
+        String fileName = "气密泄露点位汇总_" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + ".xlsx";
+
+        response.reset();
+        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
+        response.setHeader("Content-Disposition", "attachment; filename*=UTF-8''"
+                + URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"));
+        try (Workbook workbook = new XSSFWorkbook(); ServletOutputStream output = response.getOutputStream()) {
+            Sheet sheet = workbook.createSheet("泄露点位汇总");
+            int[] widths = {16, 38, 14, 36, 4, 22, 22};
+            for (int i = 0; i < widths.length; i++) sheet.setColumnWidth(i, widths[i] * 256);
+
+            CellStyle titleStyle = workbook.createCellStyle();
+            titleStyle.setAlignment(HorizontalAlignment.CENTER);
+            CellStyle headerStyle = workbook.createCellStyle();
+            headerStyle.setAlignment(HorizontalAlignment.CENTER);
+            headerStyle.setBorderBottom(BorderStyle.THIN);
+            headerStyle.setBorderTop(BorderStyle.THIN);
+            headerStyle.setBorderLeft(BorderStyle.THIN);
+            headerStyle.setBorderRight(BorderStyle.THIN);
+            CellStyle dataStyle = workbook.createCellStyle();
+            dataStyle.setBorderBottom(BorderStyle.THIN);
+            dataStyle.setBorderTop(BorderStyle.THIN);
+            dataStyle.setBorderLeft(BorderStyle.THIN);
+            dataStyle.setBorderRight(BorderStyle.THIN);
+
+            Row titleRow = sheet.createRow(0);
+            titleRow.setHeightInPoints(24);
+            Cell titleCell = titleRow.createCell(0);
+            titleCell.setCellValue("气密泄露点位汇总(工位结果:" + stationResult + ";统计范围:" + timeRange + ")");
+            titleCell.setCellStyle(titleStyle);
+            sheet.addMergedRegion(new CellRangeAddress(0, 0, 0, widths.length - 1));
+
+            String[] headers = {"工位结果", "统计时间段", "泄露点位", "工件号", "出现次数", "首次上报时间", "最近上报时间"};
+            Row headerRow = sheet.createRow(2);
+            headerRow.setHeightInPoints(21);
+            for (int i = 0; i < headers.length; i++) {
+                Cell cell = headerRow.createCell(i);
+                cell.setCellValue(headers[i]);
+                cell.setCellStyle(headerStyle);
+            }
+            SimpleDateFormat dateTimeFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+            int rowIndex = 3;
+            for (MesProductLeakagePointStat stat : rows) {
+                Row row = sheet.createRow(rowIndex++);
+                String[] values = {stationResult, timeRange, stat.getLeakagePoint(), stat.getSn(), String.valueOf(stat.getOccurrenceCount()),
+                        formatDateTime(stat.getFirstOccurrenceTime(), dateTimeFormat), formatDateTime(stat.getLastOccurrenceTime(), dateTimeFormat)};
+                for (int i = 0; i < values.length; i++) {
+                    Cell cell = row.createCell(i);
+                    cell.setCellValue(values[i]);
+                    cell.setCellStyle(dataStyle);
+                }
+            }
+            sheet.createFreezePane(0, 3);
+            workbook.write(output);
+        } catch (Exception e) {
+            throw new RuntimeException("导出气密泄露点位失败", e);
+        }
+    }
+
+    private String formatDateTime(Date date, SimpleDateFormat format) {
+        return date == null ? "" : format.format(date);
+    }
+
+    /** Uses the same NG leakage-point events as the chart and its workpiece details. */
+    @SuppressWarnings("unchecked")
+    private List<MesProductLeakagePointStat> buildChartStatistics(String sn, Date startDate, Date endDate, String dataType) {
+        List<MesProductHf> hfRecords = findRecords(sn, startDate, endDate, dataType);
+        Map<String, Object> qualityData = buildQualityData(hfRecords, startDate, endDate, dataType);
+        List<Map<String, Object>> occurrences = (List<Map<String, Object>>) qualityData.get("defectOccurrences");
+        Map<String, MesProductLeakagePointStat> statistics = new LinkedHashMap<>();
+        for (Map<String, Object> occurrence : occurrences) {
+            String point = (String) occurrence.get("point");
+            String occurrenceSn = (String) occurrence.get("sn");
+            if (StringUtils.isBlank(point) || StringUtils.isBlank(occurrenceSn)) continue;
+            Date occurrenceTime = (Date) occurrence.get("time");
+            String key = point + "\u0000" + occurrenceSn;
+            MesProductLeakagePointStat stat = statistics.get(key);
+            if (stat == null) {
+                stat = new MesProductLeakagePointStat();
+                stat.setLeakagePoint(point);
+                stat.setSn(occurrenceSn);
+                stat.setFirstOccurrenceTime(occurrenceTime);
+                statistics.put(key, stat);
+            }
+            stat.setOccurrenceCount(stat.getOccurrenceCount() + 1);
+            if (occurrenceTime != null && (stat.getFirstOccurrenceTime() == null || occurrenceTime.before(stat.getFirstOccurrenceTime()))) {
+                stat.setFirstOccurrenceTime(occurrenceTime);
+            }
+            if (occurrenceTime != null && (stat.getLastOccurrenceTime() == null || occurrenceTime.after(stat.getLastOccurrenceTime()))) {
+                stat.setLastOccurrenceTime(occurrenceTime);
+            }
+        }
+        List<MesProductLeakagePointStat> rows = new ArrayList<>(statistics.values());
+        rows.sort(Comparator.comparing(MesProductLeakagePointStat::getLeakagePoint)
+                .thenComparing(MesProductLeakagePointStat::getOccurrenceCount, Comparator.reverseOrder())
+                .thenComparing(MesProductLeakagePointStat::getSn));
+        return rows;
+    }
+
+    private List<MesProductLeakagePointStat> buildStatistics(String leakagePoint, String sn, Date startDate, Date endDate, String dataType) {
+        Map<String, MesProductLeakagePointStat> statistics = new LinkedHashMap<>();
+        for (MesProductHf record : findRecords(sn, startDate, endDate, dataType)) {
+            if (StringUtils.isBlank(record.getSn()) || StringUtils.isBlank(record.getHfFlag())) continue;
+            for (String item : record.getHfFlag().split(",")) {
+                String point = normalizeLeakPoint(item);
+                if (StringUtils.isBlank(point)
+                        || (StringUtils.isNotBlank(leakagePoint) && !point.contains(leakagePoint.trim()))) continue;
+                String key = point + "\u0000" + record.getSn();
+                MesProductLeakagePointStat stat = statistics.get(key);
+                if (stat == null) {
+                    stat = new MesProductLeakagePointStat();
+                    stat.setLeakagePoint(point);
+                    stat.setSn(record.getSn());
+                    stat.setFirstOccurrenceTime(record.getCreateDate());
+                    statistics.put(key, stat);
+                }
+                stat.setOccurrenceCount(stat.getOccurrenceCount() + 1);
+                if (record.getCreateDate() != null && (stat.getFirstOccurrenceTime() == null
+                        || record.getCreateDate().before(stat.getFirstOccurrenceTime()))) {
+                    stat.setFirstOccurrenceTime(record.getCreateDate());
+                }
+                if (record.getCreateDate() != null && (stat.getLastOccurrenceTime() == null
+                        || record.getCreateDate().after(stat.getLastOccurrenceTime()))) {
+                    stat.setLastOccurrenceTime(record.getCreateDate());
+                }
+            }
+        }
+
+        List<MesProductLeakagePointStat> rows = new ArrayList<>(statistics.values());
+        rows.sort(Comparator.comparing(MesProductLeakagePointStat::getLeakagePoint)
+                .thenComparing(MesProductLeakagePointStat::getOccurrenceCount, Comparator.reverseOrder())
+                .thenComparing(MesProductLeakagePointStat::getSn));
+        return rows;
+    }
+
+    private List<MesProductHf> findRecords(String sn, Date startDate, Date endDate, String dataType) {
+        MesProductHf query = new MesProductHf();
+        query.setSn(sn);
+        if (startDate != null) query.setCreateDate_gte(startDate);
+        if (endDate != null) query.setCreateDate_lte(endDate);
+        List<MesProductHf> records = mesProductHfService.findList(query);
+        if (StringUtils.isBlank(dataType) || "all".equals(dataType)) return records;
+        List<MesProductHf> filteredRecords = new ArrayList<>();
+        for (MesProductHf record : records) {
+            if (matchesDataType(record.getOprno(), dataType)) filteredRecords.add(record);
+        }
+        return filteredRecords;
+    }
+
+    /** Uses the same station groups as the title displayed on the gas-tightness record page. */
+    private boolean matchesDataType(String oprno, String dataType) {
+        if (StringUtils.isBlank(dataType) || "all".equals(dataType)) return true;
+        String station = CommonUitl.formatOprno(oprno);
+        if ("framework".equals(dataType)) return "INSP030".equals(station);
+        if ("semiFinished".equals(dataType)) return "INSP080".equals(station);
+        if ("finishedPackage".equals(dataType)) return "INSP090".equals(station);
+        if ("liquidCoolingPlate".equals(dataType)) return "INSP100".equals(station);
+        return false;
+    }
+
+    private String dataTypeLabel(String dataType) {
+        if ("framework".equals(dataType)) return "框架气密";
+        if ("semiFinished".equals(dataType)) return "半成品气密";
+        if ("finishedPackage".equals(dataType)) return "整包气密";
+        if ("liquidCoolingPlate".equals(dataType)) return "液冷板气密";
+        return "全部";
+    }
+
+    private String normalizeLeakPoint(String value) {
+        String point = value == null ? "" : value.trim();
+        if (!point.matches("\\d+")) return point;
+        int index;
+        try {
+            index = Integer.parseInt(point);
+        } catch (NumberFormatException ignored) {
+            return point;
+        }
+        if (index < 1 || index > 39) return point;
+        if (index <= 9) return "A" + index;
+        if (index <= 25) return "B" + (index - 9);
+        if (index <= 27) return "C" + (index - 25);
+        return "D" + (index - 27);
+    }
+}

+ 105 - 0
src/main/java/com/jeesite/modules/mes/web/MesProductRecordController.java

@@ -197,6 +197,10 @@ public class MesProductRecordController extends BaseController {
 	@Autowired
 	private FileUploadService fileUploadService;
 
+	// 气密检漏客户端(op110/qmzl)接口依赖
+	@Autowired
+	private MesProductHfService mesProductHfService;
+
 
 	/**
 	 * 获取数据
@@ -3807,4 +3811,105 @@ public class MesProductRecordController extends BaseController {
 		}
 	}
 
+	// ========== 气密检漏客户端(op110/qmzl)接口 ==========
+	// 加工前质量校验,客户端用参数 ucode 传用户(无登录 session)
+	@PostMapping(value = "pccheck")
+	@ResponseBody
+	public CommonResp pccheck(HttpServletRequest request) {
+		mesLogService.apisave(request);
+		CommonResp<String> resp = new CommonResp<>();
+		String sn = request.getParameter("sn");
+		String oprno = request.getParameter("oprno");
+		String lineSn = request.getParameter("lineSn");
+		String userCode = request.getParameter("ucode");
+		String oldOprno = oprno;
+		oprno = CommonUitl.formatOprno(oprno);
+		resp.setResult(Global.FALSE);
+
+		if(StringUtils.isEmpty(sn) || StringUtils.isEmpty(oprno) || StringUtils.isEmpty(lineSn)){
+			resp.setMessage("参数错误");
+			return resp;
+		}
+
+		String ret = checkQualityResult(oldOprno,sn,lineSn,userCode);
+		if(ret.equals("RSUD")){
+			// 开始工作,记录开工时间
+			MesDeviceTime mesDeviceTime = new MesDeviceTime();
+			mesDeviceTime.setOprno(oldOprno);
+			mesDeviceTime.setSn(sn);
+			mesDeviceTime.setStartDate(new Date());
+			mesDeviceTime.setProductCate(lineSn);
+			try{
+				mesDeviceTimeService.add(mesDeviceTime);
+			}catch (Exception e){
+				ret = "RSNS";
+			}
+		}
+
+		if(ret.equals("RSUD")){
+			resp.setMessage("工件可以加工");
+			resp.setResult(Global.TRUE);
+		}else{
+			String lx = ret.substring(0,2);
+			String processMsgRet = ret.substring(2,4);
+			String lmsg = ErrorMsg.getErrorMsg(processMsgRet,lx);
+			resp.setMessage(lmsg);
+		}
+
+		resp.setData(ret.replace("RS",""));
+		return resp;
+	}
+
+	// 结果+漏点上传,漏点索引串 ldFlag 存入 mes_product_hf 表
+	@PostMapping(value = "pcresult")
+	@ResponseBody
+	@Transactional
+	public String pcresult(HttpServletRequest request) {
+		mesLogService.apisave(request);
+		String sn = request.getParameter("sn");
+		String oprno = request.getParameter("oprno");
+		String lineSn = request.getParameter("lineSn");
+		String result = request.getParameter("result");
+		String userCode = request.getParameter("ucode");
+		String remark = request.getParameter("remark");
+		String craft = "100000";
+
+		if(StringUtils.isEmpty(sn) || StringUtils.isEmpty(oprno) || StringUtils.isEmpty(result) || StringUtils.isEmpty(lineSn)){
+			return renderResult(Global.FALSE, text("参数错误!"));
+		}
+		if(!result.equals("OK") && !result.equals("NG")){
+			return renderResult(Global.FALSE, text("结果错误!"));
+		}
+
+		try{
+			// 保存漏点:只要客户端提交了漏点索引,就按当前工位保存,不再限制工位白名单。
+			String ldFlag = request.getParameter("ldFlag");
+			if(!StringUtils.isEmpty(ldFlag)){
+				MesProductHf mesProductHf = new MesProductHf();
+				mesProductHf.setSn(sn);
+				mesProductHf.setOprno(oprno);
+				mesProductHf.setLineSn(lineSn);
+				mesProductHf.setHfFlag(ldFlag);
+				mesProductHf.setResult(result);
+				mesProductHfService.save(mesProductHf);
+			}
+
+			// 主结果落库,复用现有 updateRecordResult
+			List<ParamsResp> params = new ArrayList<>();
+			ParamsResp paramsResp1 = new ParamsResp();
+			paramsResp1.setCraft("400001");
+			paramsResp1.setResult("OK");
+			paramsResp1.setVal(userCode);
+			params.add(paramsResp1); // 绑定用户
+
+			List<BindMaterialResp> bmlists = mesLineProcessMaterialService.getBindMaterial(oprno, lineSn);
+			mesProductRecordService.updateRecordResult(sn,oprno,craft,"",result,userCode,params,lineSn,true,bmlists);
+		}catch (Exception e){
+			e.printStackTrace();
+			return renderResult(Global.FALSE, text("操作失败!"));
+		}
+
+		return renderResult(Global.TRUE, text("操作成功!"));
+	}
+
 }

Tiedoston diff-näkymää rajattu, sillä se on liian suuri
+ 30 - 0
src/main/resources/views/modules/mes/mesProductLeakagePointStatChart.html