Commit eb3a615802c59f9d193847341be28dff8e2d6f2a

Authored by 杨鸣坤
1 parent bcbcf7c9

feat: 新增OEE时序图分页查询接口

@@ -64,4 +64,22 @@ public class HealthController { @@ -64,4 +64,22 @@ public class HealthController {
64 @RequestParam(required = false) String endDate) { 64 @RequestParam(required = false) String endDate) {
65 return deviceSearchService.queryOeeStats(dtuSn, type, startDate, endDate); 65 return deviceSearchService.queryOeeStats(dtuSn, type, startDate, endDate);
66 } 66 }
  67 +
  68 + /**
  69 + * OEE时序图分页查询(含稼动率)
  70 + * 查询所有设备在指定日期范围内的OEE时序数据,分页返回(每页最多20条)
  71 + *
  72 + * @param startDate 开始日期 yyyy-MM-dd
  73 + * @param endDate 结束日期 yyyy-MM-dd
  74 + * @param pageNo 页码,默认1
  75 + * @param pageSize 每页条数,最大20,默认20
  76 + */
  77 + @GetMapping("/device/oeeTimeline")
  78 + public Map<String, Object> oeeTimeline(
  79 + @RequestParam String startDate,
  80 + @RequestParam String endDate,
  81 + @RequestParam(defaultValue = "1") Integer pageNo,
  82 + @RequestParam(defaultValue = "20") Integer pageSize) {
  83 + return deviceSearchService.queryOeeTimeline(startDate, endDate, pageNo, pageSize);
  84 + }
67 } 85 }
1 package com.iot.scheduler.service; 1 package com.iot.scheduler.service;
2 2
  3 +import com.alibaba.fastjson.JSON;
3 import com.alibaba.fastjson.JSONArray; 4 import com.alibaba.fastjson.JSONArray;
4 import com.alibaba.fastjson.JSONObject; 5 import com.alibaba.fastjson.JSONObject;
5 import lombok.extern.slf4j.Slf4j; 6 import lombok.extern.slf4j.Slf4j;
@@ -550,4 +551,381 @@ public class DeviceSearchService { @@ -550,4 +551,381 @@ public class DeviceSearchService {
550 551
551 return jdbcTemplate.queryForList(sql.toString(), params.toArray()); 552 return jdbcTemplate.queryForList(sql.toString(), params.toArray());
552 } 553 }
  554 +
  555 + // ==================== OEE 时序图分页查询(含稼动率计算) ====================
  556 +
  557 + /**
  558 + * 按设备分页查询所有设备的OEE时序数据,并计算稼动率
  559 + *
  560 + * @param startDate 开始日期 yyyy-MM-dd
  561 + * @param endDate 结束日期 yyyy-MM-dd
  562 + * @param pageNo 页码,从1开始
  563 + * @param pageSize 每页设备数,最大20
  564 + */
  565 + public Map<String, Object> queryOeeTimeline(String startDate, String endDate, Integer pageNo, Integer pageSize) {
  566 + log.info("========== [OEE时序图查询-按设备分页] startDate={}, endDate={}, pageNo={}, pageSize={} ==========",
  567 + startDate, endDate, pageNo, pageSize);
  568 +
  569 + // 参数校验与默认值
  570 + if (!StringUtils.hasText(startDate) || !StringUtils.hasText(endDate)) {
  571 + return Map.of("total", 0, "pageNo", pageNo, "pageSize", pageSize, "list", List.of());
  572 + }
  573 + int ps = Math.min(pageSize != null ? pageSize : 20, 20);
  574 + int pn = pageNo != null && pageNo > 0 ? pageNo : 1;
  575 +
  576 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  577 + String todayStr = sdf.format(new Date());
  578 + boolean includeToday = isDateInRange(todayStr, startDate, endDate);
  579 + log.info("包含今日({}): {}", todayStr, includeToday);
  580 +
  581 + // 1. 从设备表查询所有 dtuSn 及设备名称
  582 + Map<String, String> deviceNameMap = queryAllDtuSnWithName();
  583 + List<String> allDtuSns = new ArrayList<>(deviceNameMap.keySet());
  584 + int deviceTotal = allDtuSns.size();
  585 + log.info("设备总数: {}", deviceTotal);
  586 +
  587 + if (allDtuSns.isEmpty()) {
  588 + return buildPageResult(0, pn, ps, List.of());
  589 + }
  590 +
  591 + // 2. 构建日期范围列表
  592 + List<String> dateList = buildDayList(startDate, endDate);
  593 + log.info("日期范围共 {} 天: {} ~ {}", dateList.size(), dateList.get(0), dateList.get(dateList.size() - 1));
  594 +
  595 + // 3. 批量从数据库查询全部设备的 OEE 数据
  596 + List<OeeRecord> allRecords = queryOeeBatch(allDtuSns, dateList, includeToday ? todayStr : null);
  597 +
  598 + // 4. 如果包含今天,补充调用接口获取今天的实时数据
  599 + if (includeToday) {
  600 + supplementTodayData(allDtuSns, todayStr, allRecords);
  601 + }
  602 +
  603 + // 5. 按 dtuSn 分组,每个设备包含该日期范围内所有天的数据
  604 + Map<String, List<OeeRecord>> deviceMap = new LinkedHashMap<>();
  605 + for (OeeRecord r : allRecords) {
  606 + deviceMap.computeIfAbsent(r.dtuSn, k -> new ArrayList<>()).add(r);
  607 + }
  608 + // 确保没有数据的设备也有空列表(保持顺序一致)
  609 + for (String sn : allDtuSns) {
  610 + if (!deviceMap.containsKey(sn)) {
  611 + deviceMap.put(sn, new ArrayList<>());
  612 + }
  613 + }
  614 +
  615 + // 6. 设备维度分页
  616 + List<Map<String, Object>> pageList;
  617 + int offset = (pn - 1) * ps;
  618 + List<String> pagedDevices = new ArrayList<>(deviceMap.keySet()).subList(
  619 + Math.min(offset, deviceTotal), Math.min(offset + ps, deviceTotal));
  620 + pageList = convertDeviceGroupToResponse(pagedDevices, deviceMap, dateList, deviceNameMap);
  621 +
  622 + return buildPageResult(deviceTotal, pn, ps, pageList);
  623 + }
  624 +
  625 + /** 查询设备表所有dtuSn及对应设备名称 */
  626 + private Map<String, String> queryAllDtuSnWithName() {
  627 + String sql = "SELECT dtuSn, deviceName FROM " + deviceTableName + " WHERE corp_code = ? ORDER BY dtuSn";
  628 + List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, deviceCorpCode);
  629 + Map<String, String> result = new LinkedHashMap<>(rows.size());
  630 + for (Map<String, Object> row : rows) {
  631 + result.put((String) row.get("dtuSn"), (String) row.get("deviceName"));
  632 + }
  633 + return result;
  634 + }
  635 +
  636 + /** 判断某日期是否在范围内 */
  637 + private boolean isDateInRange(String dateStr, String start, String end) {
  638 + try {
  639 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  640 + Date target = sdf.parse(dateStr);
  641 + Date s = sdf.parse(start);
  642 + Date e = sdf.parse(end);
  643 + return !target.before(s) && !target.after(e);
  644 + } catch (Exception e) {
  645 + return false;
  646 + }
  647 + }
  648 +
  649 + /** 构建连续日期列表 */
  650 + private List<String> buildDayList(String startDate, String endDate) {
  651 + List<String> result = new ArrayList<>();
  652 + try {
  653 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  654 + Calendar cur = Calendar.getInstance();
  655 + cur.setTime(sdf.parse(startDate));
  656 + Calendar endCal = Calendar.getInstance();
  657 + endCal.setTime(sdf.parse(endDate));
  658 + while (!cur.after(endCal)) {
  659 + result.add(sdf.format(cur.getTime()));
  660 + cur.add(Calendar.DAY_OF_MONTH, 1);
  661 + }
  662 + } catch (Exception e) {
  663 + log.error("日期解析失败: {} ~ {}", startDate, endDate, e);
  664 + }
  665 + return result;
  666 + }
  667 +
  668 + /** OEE内部记录结构 */
  669 + private static class OeeRecord {
  670 + String dtuSn;
  671 + String oeeDate;
  672 + JSONArray lampData; // 该日lampData数组
  673 + double availabilityRate; // 稼动率 (%)
  674 +
  675 + OeeRecord(String dtuSn, String oeeDate) {
  676 + this.dtuSn = dtuSn;
  677 + this.oeeDate = oeeDate;
  678 + this.availabilityRate = 0.0;
  679 + }
  680 + }
  681 +
  682 + /** 批量从数据库查询OEE数据 */
  683 + private List<OeeRecord> queryOeeBatch(List<String> dtuSns, List<String> dateList, String excludeToday) {
  684 + List<OeeRecord> records = new ArrayList<>();
  685 +
  686 + if (dateList.isEmpty()) return records;
  687 +
  688 + StringBuilder sql = new StringBuilder(
  689 + "SELECT dtuSn, oee_date, triColorLamp1, triColorLamp2 FROM " + oeeTableName +
  690 + " WHERE corp_code = ? AND dtuSn IN (");
  691 + List<Object> params = new ArrayList<>();
  692 + params.add(deviceCorpCode);
  693 +
  694 + // dtuSn IN 条件
  695 + for (String sn : dtuSns) { sql.append("?,"); params.add(sn); }
  696 + sql.deleteCharAt(sql.length() - 1).append(")");
  697 +
  698 + // 日期条件(排除今天)
  699 + sql.append(" AND oee_date IN (");
  700 + List<Object> dateParams = new ArrayList<>();
  701 + for (String d : dateList) {
  702 + if (d.equals(excludeToday)) continue;
  703 + sql.append("?,"); dateParams.add(d);
  704 + }
  705 + if (dateParams.isEmpty()) return records; // 全是今天
  706 + sql.deleteCharAt(sql.length() - 1).append(")");
  707 + params.addAll(dateParams);
  708 +
  709 + List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql.toString(), params.toArray());
  710 + log.info("DB批量查询返回 {} 行OEE数据", rows.size());
  711 +
  712 + for (Map<String, Object> row : rows) {
  713 + String sn = (String) row.get("dtuSn");
  714 + String oeeDate = String.valueOf(row.get("oee_date"));
  715 + if (oeeDate.length() > 10) oeeDate = oeeDate.substring(0, 10);
  716 + String lamp1 = (String) row.get("triColorLamp1");
  717 + String lamp2 = (String) row.get("triColorLamp2");
  718 +
  719 + OeeRecord record = new OeeRecord(sn, oeeDate);
  720 + try {
  721 + String fullJson = (lamp1 != null ? lamp1 : "") + (lamp2 != null ? lamp2 : "");
  722 + if (!fullJson.isEmpty()) {
  723 + record.lampData = JSONArray.parseArray(fullJson);
  724 + } else {
  725 + record.lampData = new JSONArray();
  726 + }
  727 + } catch (Exception e) {
  728 + log.warn("解析OEE JSON异常 - dtuSn:{}, date:{}", sn, oeeDate, e);
  729 + record.lampData = new JSONArray();
  730 + }
  731 +
  732 + record.availabilityRate = calcAvailabilityRate(record.lampData);
  733 + records.add(record);
  734 + }
  735 +
  736 + return records;
  737 + }
  738 +
  739 + /** 补充今天的实时接口数据 */
  740 + private void supplementTodayData(List<String> dtuSns, String todayStr, List<OeeRecord> records) {
  741 + log.info("开始补充今日({})实时OEE数据...", todayStr);
  742 + int apiCount = 0;
  743 + for (String dtuSn : dtuSns) {
  744 + // 先检查是否已有该设备今天的数据
  745 + boolean exists = false;
  746 + for (OeeRecord r : records) {
  747 + if (r.dtuSn.equals(dtuSn) && r.oeeDate.equals(todayStr)) {
  748 + exists = true; break;
  749 + }
  750 + }
  751 + if (exists) continue;
  752 +
  753 + try {
  754 + String apiResult = devicePullService.getLampData(dtuSn, todayStr);
  755 + if (!StringUtils.hasText(apiResult)) continue;
  756 +
  757 + Map<String, Object> res = JSON.parseObject(apiResult, new com.alibaba.fastjson.TypeReference<>() {});
  758 + Integer code = (Integer) res.get("code");
  759 + if (code == null || code != 200) continue;
  760 +
  761 + JSONArray dataList = (JSONArray) res.get("data");
  762 + if (dataList == null || dataList.isEmpty()) continue;
  763 +
  764 + JSONObject dataObj = (JSONObject) dataList.get(0);
  765 + OeeRecord record = new OeeRecord(dtuSn, todayStr);
  766 + record.lampData = dataObj.getJSONArray("lampData");
  767 + if (record.lampData == null) record.lampData = new JSONArray();
  768 + record.availabilityRate = calcAvailabilityRate(record.lampData);
  769 + records.add(record);
  770 + apiCount++;
  771 + } catch (Exception e) {
  772 + log.error("获取今日OEE数据异常 - dtuSn:{}", dtuSn, e);
  773 + }
  774 + }
  775 + log.info("今日实时数据补充完成, 新增 {} 条", apiCount);
  776 + }
  777 +
  778 + /**
  779 + * 计算稼动率
  780 + * 稼动率 = 绿灯(state=3)时长 / 总时长 * 100%
  781 + */
  782 + private double calcAvailabilityRate(JSONArray lampData) {
  783 + if (lampData == null || lampData.isEmpty()) return 0.0;
  784 +
  785 + long totalDuration = 0;
  786 + long greenDuration = 0;
  787 +
  788 + for (int i = 0; i < lampData.size(); i++) {
  789 + JSONObject item = lampData.getJSONObject(i);
  790 + if (item == null) continue;
  791 + int state = item.getIntValue("lampState");
  792 + long dur = item.getLongValue("duration");
  793 + totalDuration += dur;
  794 + if (state == 3) greenDuration += dur;
  795 + }
  796 +
  797 + if (totalDuration == 0) return 0.0;
  798 + return Math.round(greenDuration * 10000.0 / totalDuration) / 100.0; // 保留2位小数
  799 + }
  800 +
  801 + /** 按设备分组转换为响应格式:每个设备一条记录,包含lampData和设备名称 */
  802 + private List<Map<String, Object>> convertDeviceGroupToResponse(List<String> pagedDevices,
  803 + Map<String, List<OeeRecord>> deviceMap,
  804 + List<String> dateList,
  805 + Map<String, String> deviceNameMap) {
  806 + List<Map<String, Object>> list = new ArrayList<>(pagedDevices.size());
  807 +
  808 + for (String dtuSn : pagedDevices) {
  809 + Map<String, Object> item = new LinkedHashMap<>();
  810 + item.put("dtuSn", dtuSn);
  811 + item.put("deviceName", deviceNameMap.getOrDefault(dtuSn, ""));
  812 +
  813 + List<OeeRecord> dayRecords = deviceMap.getOrDefault(dtuSn, Collections.emptyList());
  814 + // 构建日期→record的快速查找map
  815 + Map<String, OeeRecord> recordByDate = new LinkedHashMap<>();
  816 + for (OeeRecord r : dayRecords) {
  817 + recordByDate.put(r.oeeDate, r);
  818 + }
  819 +
  820 + // 汇总计算综合稼动率
  821 + long totalDur = 0, greenDur = 0;
  822 + long offD = 0, redD = 0, yellowD = 0, greenD = 0, blueD = 0;
  823 + int offC = 0, redC = 0, yellowC = 0, greenC = 0, blueC = 0;
  824 +
  825 + // 拼接所有天的 lampData 为一个数组(按日期排序)
  826 + JSONArray allLampData = new JSONArray();
  827 + List<Map<String, Object>> dailyDetails = new ArrayList<>();
  828 +
  829 + for (String d : dateList) {
  830 + OeeRecord rec = recordByDate.get(d);
  831 + if (rec != null && rec.lampData != null) {
  832 + for (int i = 0; i < rec.lampData.size(); i++) {
  833 + JSONObject lamp = rec.lampData.getJSONObject(i);
  834 + if (lamp == null) continue;
  835 + int state = lamp.getIntValue("lampState");
  836 + long dur = lamp.getLongValue("duration");
  837 + switch (state) {
  838 + case 0 -> { offD += dur; offC++; }
  839 + case 1 -> { redD += dur; redC++; }
  840 + case 2 -> { yellowD += dur; yellowC++; }
  841 + case 3 -> { greenD += dur; greenC++; }
  842 + case 4 -> { blueD += dur; blueC++; }
  843 + }
  844 + totalDur += dur;
  845 + if (state == 3) greenDur += dur;
  846 + }
  847 + allLampData.addAll(rec.lampData);
  848 + }
  849 + double dayRate = (rec != null) ? rec.availabilityRate : 0.0;
  850 + dailyDetails.add(Map.of(
  851 + "oeeDate", d,
  852 + "availabilityRate", dayRate,
  853 + "hasData", rec != null
  854 + ));
  855 + }
  856 +
  857 + double overallRate = (totalDur > 0) ? Math.round(greenDur * 10000.0 / totalDur) / 100.0 : 0.0;
  858 +
  859 + // 扁平化字段,对齐截图格式
  860 + item.put("availabilityRatio", String.format("%.2f%%", overallRate));
  861 + item.put("offDuration", formatDuration(offD));
  862 + item.put("redDuration", formatDuration(redD));
  863 + item.put("yellowDuration", formatDuration(yellowD));
  864 + item.put("greenDuration", formatDuration(greenD));
  865 + item.put("blueDuration", formatDuration(blueD));
  866 + item.put("dataDays", recordByDate.size());
  867 + item.put("totalDays", dateList.size());
  868 + item.put("lampData", allLampData);
  869 + item.put("dailyDetails", dailyDetails);
  870 +
  871 + list.add(item);
  872 + }
  873 +
  874 + return list;
  875 + }
  876 +
  877 + /** 转换为响应格式 */
  878 + private List<Map<String, Object>> convertToResponse(List<OeeRecord> records) {
  879 + List<Map<String, Object>> list = new ArrayList<>(records.size());
  880 +
  881 + for (OeeRecord r : records) {
  882 + Map<String, Object> item = new LinkedHashMap<>();
  883 + item.put("dtuSn", r.dtuSn);
  884 + item.put("oeeDate", r.oeeDate);
  885 + item.put("availabilityRate", r.availabilityRate); // 稼动率
  886 + item.put("availabilityRateStr", r.availabilityRate + "%");
  887 +
  888 + // 各状态时长统计
  889 + long offDur = 0, redDur = 0, yellowDur = 0, greenDur = 0, blueDur = 0;
  890 + int offCnt = 0, redCnt = 0, yellowCnt = 0, greenCnt = 0, blueCnt = 0;
  891 +
  892 + if (r.lampData != null) {
  893 + for (int i = 0; i < r.lampData.size(); i++) {
  894 + JSONObject lamp = r.lampData.getJSONObject(i);
  895 + if (lamp == null) continue;
  896 + int state = lamp.getIntValue("lampState");
  897 + long dur = lamp.getLongValue("duration");
  898 + switch (state) {
  899 + case 0 -> { offDur += dur; offCnt++; }
  900 + case 1 -> { redDur += dur; redCnt++; }
  901 + case 2 -> { yellowDur += dur; yellowCnt++; }
  902 + case 3 -> { greenDur += dur; greenCnt++; }
  903 + case 4 -> { blueDur += dur; blueCnt++; }
  904 + }
  905 + }
  906 + }
  907 +
  908 + item.put("off", Map.of("duration", formatDuration(offDur), "seconds", offDur, "count", offCnt));
  909 + item.put("red", Map.of("duration", formatDuration(redDur), "seconds", redDur, "count", redCnt));
  910 + item.put("yellow", Map.of("duration", formatDuration(yellowDur), "seconds", yellowDur, "count", yellowCnt));
  911 + item.put("green", Map.of("duration", formatDuration(greenDur), "seconds", greenDur, "count", greenCnt));
  912 + item.put("blue", Map.of("duration", formatDuration(blueDur), "seconds", blueDur, "count", blueCnt));
  913 + item.put("lampData", r.lampData != null ? r.lampData : new JSONArray());
  914 +
  915 + list.add(item);
  916 + }
  917 + return list;
  918 + }
  919 +
  920 + /** 构建分页结果(标准分页格式) */
  921 + private Map<String, Object> buildPageResult(long total, int pageNo, int pageSize, List<Map<String, Object>> list) {
  922 + return Map.of(
  923 + "data", Map.of(
  924 + "total", total,
  925 + "size", pageSize,
  926 + "current", pageNo,
  927 + "records", list
  928 + )
  929 + );
  930 + }
553 } 931 }
@@ -39,9 +39,9 @@ scheduler: @@ -39,9 +39,9 @@ scheduler:
39 pull: "0 0/5 * * * ?" 39 pull: "0 0/5 * * * ?"
40 push: "0 0/10 * * * ?" 40 push: "0 0/10 * * * ?"
41 devUtil: 41 devUtil:
42 - cron: "0 30 2 * * ?" # 每日凌晨 2:30 增量同步昨天数据 42 + cron: "0 5 11 * * ?" # 每日凌晨 2:30 增量同步昨天数据
43 oee: 43 oee:
44 - cron: "0 35 2 * * ?" # 每日凌晨 2:35 增量同步昨天OEE数据 44 + cron: "0 5 11 * * ?" # 每日凌晨 2:35 增量同步昨天OEE数据
45 45
46 device: 46 device:
47 token: 47 token: