Commit bcbcf7c9e127e10e16954e840e67d775a8b6884f

Authored by 杨鸣坤
1 parent 0fbac06e

feat: 新增设备利用率与OEE数据同步及统计查询功能

... ... @@ -36,4 +36,32 @@ public class HealthController {
36 36 public Map<String, Object> deviceStats() {
37 37 return deviceSearchService.queryDeviceStats();
38 38 }
  39 +
  40 + @GetMapping("/device/lampData")
  41 + public Map<String, Object> lampData(
  42 + @RequestParam String dtuSn,
  43 + @RequestParam String date) {
  44 + return deviceSearchService.queryLampData(dtuSn, date);
  45 + }
  46 +
  47 + @GetMapping("/device/syncDevUtil")
  48 + public String syncDevUtil() {
  49 + devicePullService.pullDevUtilAndSave();
  50 + return "设备利用率数据同步完成";
  51 + }
  52 +
  53 + @GetMapping("/device/syncOee")
  54 + public String syncOee() {
  55 + devicePullService.pullOeeAndSave();
  56 + return "OEE时序数据同步完成";
  57 + }
  58 +
  59 + @GetMapping("/device/oeeStats")
  60 + public Map<String, Object> oeeStats(
  61 + @RequestParam String dtuSn,
  62 + @RequestParam(defaultValue = "day") String type,
  63 + @RequestParam(required = false) String startDate,
  64 + @RequestParam(required = false) String endDate) {
  65 + return deviceSearchService.queryOeeStats(dtuSn, type, startDate, endDate);
  66 + }
39 67 }
... ...
... ... @@ -34,6 +34,8 @@ import java.text.SimpleDateFormat;
34 34 import java.util.*;
35 35 import java.util.concurrent.TimeUnit;
36 36
  37 +import org.springframework.scheduling.annotation.Scheduled;
  38 +
37 39 @Slf4j
38 40 @Service
39 41 public class DevicePullService {
... ... @@ -57,6 +59,12 @@ public class DevicePullService {
57 59 private String deviceCorpCode;
58 60 @Value("${device.db.tableName}")
59 61 private String deviceTableName;
  62 + @Value("${device.db.devUtilTableName}")
  63 + private String devUtilTableName;
  64 + @Value("${device.db.oeeTableName}")
  65 + private String oeeTableName;
  66 + @Value("${device.lamp.url}")
  67 + private String deviceLampUrl;
60 68
61 69 @Resource
62 70 private RedisTemplate<String, String> redisTemplate;
... ... @@ -172,15 +180,14 @@ public class DevicePullService {
172 180 return deviceInfoDetail;
173 181 }
174 182
175   - public String getDtuSnRateOfAction(String dtuSn) {
  183 + public String getDtuSnRateOfAction(String dtuSn, String startDate, String endDate) {
176 184 String accessToken = getAccessToken();
177 185 Map<String, String> headerMap = new HashMap<>(1);
178 186 headerMap.put("Authorization", "Bearer " + accessToken);
179 187
180 188 Map<String, String> paramsMap = new HashMap<>();
181   - String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
182   - paramsMap.put("startDate", today);
183   - paramsMap.put("endDate", today);
  189 + paramsMap.put("startDate", startDate);
  190 + paramsMap.put("endDate", endDate);
184 191 paramsMap.put("dtuSn", dtuSn);
185 192
186 193 String rateResult = sendRequestGet(deviceSnRateUrl, paramsMap, headerMap);
... ... @@ -198,7 +205,8 @@ public class DevicePullService {
198 205 }
199 206
200 207 public String getUtilizationRate(String dtuSn) {
201   - String rateResult = getDtuSnRateOfAction(dtuSn);
  208 + String today = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
  209 + String rateResult = getDtuSnRateOfAction(dtuSn, today, today);
202 210 if (StringUtils.isBlank(rateResult)) {
203 211 return "0%";
204 212 }
... ... @@ -231,6 +239,18 @@ public class DevicePullService {
231 239 return String.format("%.2f%%", rate);
232 240 }
233 241
  242 + public String getLampData(String dtuSn, String date) {
  243 + String accessToken = getAccessToken();
  244 + Map<String, String> headerMap = new HashMap<>(1);
  245 + headerMap.put("Authorization", "Bearer " + accessToken);
  246 +
  247 + Map<String, String> paramsMap = new HashMap<>();
  248 + paramsMap.put("dtuSn", dtuSn);
  249 + paramsMap.put("date", date);
  250 +
  251 + return sendRequestGet(deviceLampUrl, paramsMap, headerMap);
  252 + }
  253 +
234 254 public void saveOrUpdateDevice(String projectState, String projectType, String deviceName, String dtuId,
235 255 String deviceId, String dtuSn, String lampState, String startTime,
236 256 String duration, String utilizationRate) {
... ... @@ -259,6 +279,289 @@ public class DevicePullService {
259 279 }
260 280 }
261 281
  282 + /**
  283 + * 首次全量同步:本年1月1日 ~ 昨天(手动触发一次)
  284 + */
  285 + public void pullDevUtilAndSave() {
  286 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  287 + Calendar cal = Calendar.getInstance();
  288 + cal.add(Calendar.DAY_OF_MONTH, -1); // 昨天
  289 + String endDate = sdf.format(cal.getTime());
  290 + cal.set(cal.get(Calendar.YEAR), Calendar.JANUARY, 1);
  291 + String startDate = sdf.format(cal.getTime());
  292 +
  293 + log.info("【全量同步】设备利用率,日期范围: {} ~ {}", startDate, endDate);
  294 + doSyncDevUtil(startDate, endDate);
  295 + }
  296 +
  297 + /**
  298 + * 每日增量同步:仅同步昨天的数据(定时任务自动触发)
  299 + */
  300 +// @Scheduled(cron = "${scheduler.devUtil.cron:0 30 2 * * ?}")
  301 + public void pullDevUtilDaily() {
  302 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  303 + Calendar cal = Calendar.getInstance();
  304 + cal.add(Calendar.DAY_OF_MONTH, -1);
  305 + String yesterday = sdf.format(cal.getTime());
  306 +
  307 + log.info("【每日增量】同步设备利用率,日期: {}", yesterday);
  308 + doSyncDevUtil(yesterday, yesterday);
  309 + }
  310 +
  311 + /**
  312 + * 核心同步逻辑:获取设备列表 → 按每31天分批调用 dtuSnRateOfAction → 写入/更新 t_auto_ymk_iot_dev_util
  313 + */
  314 + private void doSyncDevUtil(String startDate, String endDate) {
  315 + String deviceResult = getDeviceInfo();
  316 + if (StringUtils.isBlank(deviceResult)) {
  317 + log.error("获取设备列表失败");
  318 + return;
  319 + }
  320 + Map<String, Object> deviceInfos = JSON.parseObject(deviceResult, new TypeReference<>() {});
  321 + JSONArray deviceInfoList = (JSONArray) deviceInfos.get("data");
  322 + if (CollectionUtils.isEmpty(deviceInfoList)) {
  323 + log.warn("设备列表为空");
  324 + return;
  325 + }
  326 +
  327 + // 按最多31天拆分为多个日期区间
  328 + List<DateRange> batches = splitDateRange(startDate, endDate);
  329 + log.info("开始同步设备利用率数据,设备数: {}, 日期范围: {} ~ {}, 共分{}批次",
  330 + deviceInfoList.size(), startDate, endDate, batches.size());
  331 + int totalSaved = 0;
  332 +
  333 + for (Object o : deviceInfoList) {
  334 + JSONObject deviceInfoJson = (JSONObject) o;
  335 + String dtuSn = deviceInfoJson.getString("dtuSn");
  336 +
  337 + // 每个设备按31天一批次逐批调用
  338 + for (DateRange batch : batches) {
  339 + try {
  340 + String rateResult = getDtuSnRateOfAction(dtuSn, batch.start, batch.end);
  341 + if (StringUtils.isBlank(rateResult)) continue;
  342 +
  343 + Map<String, Object> rateMap = JSON.parseObject(rateResult, new TypeReference<>() {});
  344 + Integer code = (Integer) rateMap.get("code");
  345 + if (code == null || code != 200) continue;
  346 +
  347 + JSONArray dataList = (JSONArray) rateMap.get("data");
  348 + if (CollectionUtils.isEmpty(dataList)) continue;
  349 +
  350 + for (int i = 0; i < dataList.size(); i++) {
  351 + JSONObject dayData = (JSONObject) dataList.get(i);
  352 + String dateStr = dayData.getString("date");
  353 + JSONArray realRateList = dayData.getJSONArray("realRate");
  354 + if (CollectionUtils.isEmpty(realRateList)) continue;
  355 +
  356 + JSONObject rateObj = (JSONObject) realRateList.get(0);
  357 + double state0 = rateObj.getDoubleValue("0");
  358 + double state1 = rateObj.getDoubleValue("1");
  359 + double state2 = rateObj.getDoubleValue("2");
  360 + double state3 = rateObj.getDoubleValue("3");
  361 + double state4 = rateObj.getDoubleValue("4");
  362 +
  363 + saveOrUpdateDevUtil(dtuSn, dateStr, state0, state1, state2, state3, state4);
  364 + totalSaved++;
  365 + }
  366 + } catch (Exception e) {
  367 + log.error("处理设备 {} 数据异常 - 批次:{}~{}", dtuSn, batch.start, batch.end, e);
  368 + }
  369 + }
  370 + }
  371 + log.info("设备利用率数据同步完成,共保存 {} 条记录", totalSaved);
  372 + }
  373 +
  374 + /**
  375 + * 按最大31天将日期范围拆分为多段
  376 + */
  377 + private static List<DateRange> splitDateRange(String startDate, String endDate) {
  378 + List<DateRange> ranges = new ArrayList<>();
  379 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  380 + try {
  381 + Calendar cur = Calendar.getInstance();
  382 + cur.setTime(sdf.parse(startDate));
  383 + Calendar endCal = Calendar.getInstance();
  384 + endCal.setTime(sdf.parse(endDate));
  385 +
  386 + while (!cur.after(endCal)) {
  387 + DateRange range = new DateRange(sdf.format(cur.getTime()), null);
  388 +
  389 + // 向后推进30天(即当前日+30,共31天)
  390 + Calendar batchEnd = Calendar.getInstance();
  391 + batchEnd.setTime(cur.getTime());
  392 + batchEnd.add(Calendar.DAY_OF_MONTH, 30);
  393 +
  394 + if (!batchEnd.after(endCal)) {
  395 + range.end = sdf.format(batchEnd.getTime());
  396 + } else {
  397 + range.end = sdf.format(endCal.getTime());
  398 + }
  399 +
  400 + ranges.add(range);
  401 + cur.add(Calendar.DAY_OF_MONTH, 31);
  402 + }
  403 + } catch (Exception e) {
  404 + throw new RuntimeException("日期解析失败: " + startDate + " ~ " + endDate, e);
  405 + }
  406 + return ranges;
  407 + }
  408 +
  409 + private static class DateRange {
  410 + String start;
  411 + String end;
  412 +
  413 + DateRange(String start, String end) {
  414 + this.start = start;
  415 + this.end = end;
  416 + }
  417 + }
  418 +
  419 + private void saveOrUpdateDevUtil(String dtuSn, String dateUtil,
  420 + double s0, double s1, double s2, double s3, double s4) {
  421 + List<Map<String, Object>> existList = jdbcTemplate.queryForList(
  422 + "SELECT id FROM " + devUtilTableName + " WHERE corp_code = ? AND dtuSn = ? AND date_util = ?",
  423 + deviceCorpCode, dtuSn, dateUtil);
  424 +
  425 + Date now = new Date();
  426 + if (!existList.isEmpty()) {
  427 + jdbcTemplate.update(
  428 + "UPDATE " + devUtilTableName + " SET `0`=?,`1`=?,`2`=?,`3`=?,`4`=?,updated_at=? WHERE dtuSn=? AND date_util=?",
  429 + s0, s1, s2, s3, s4, now, dtuSn, dateUtil);
  430 + } else {
  431 + String id = UUID.randomUUID().toString().replace("-", "");
  432 + jdbcTemplate.update(
  433 + "INSERT INTO " + devUtilTableName + " (id,corp_code,created_at,created_by,updated_at,updated_by,date_util,dtuSn,`0`,`1`,`2`,`3`,`4`) " +
  434 + "VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)",
  435 + id, deviceCorpCode, now, "system", now, "system", dateUtil, dtuSn, s0, s1, s2, s3, s4);
  436 + }
  437 + }
  438 +
  439 + // ==================== OEE 时序数据同步 ====================
  440 +
  441 + /**
  442 + * 首次全量同步 OEE:本年1月1日 ~ 昨天(手动触发一次)
  443 + */
  444 + public void pullOeeAndSave() {
  445 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  446 + Calendar cal = Calendar.getInstance();
  447 + cal.add(Calendar.DAY_OF_MONTH, -1);
  448 + String endDate = sdf.format(cal.getTime());
  449 + cal.set(cal.get(Calendar.YEAR), Calendar.JANUARY, 1);
  450 + String startDate = sdf.format(cal.getTime());
  451 +
  452 + log.info("【全量同步】OEE时序数据,日期范围: {} ~ {}", startDate, endDate);
  453 + doSyncOee(startDate, endDate);
  454 + }
  455 +
  456 + /**
  457 + * 每日增量同步 OEE:仅同步昨天(定时任务自动触发)
  458 + */
  459 +// @Scheduled(cron = "${scheduler.oee.cron:0 35 2 * * ?}")
  460 + public void pullOeeDaily() {
  461 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  462 + Calendar cal = Calendar.getInstance();
  463 + cal.add(Calendar.DAY_OF_MONTH, -1);
  464 + String yesterday = sdf.format(cal.getTime());
  465 +
  466 + log.info("【每日增量】同步OEE时序数据,日期: {}", yesterday);
  467 + doSyncOee(yesterday, yesterday);
  468 + }
  469 +
  470 + /**
  471 + * OEE 核心同步:获取设备列表 → 每个设备每天调用 /triColorLamp/dtuSn → lampData JSON 存入 description
  472 + */
  473 + private void doSyncOee(String startDate, String endDate) {
  474 + String deviceResult = getDeviceInfo();
  475 + if (StringUtils.isBlank(deviceResult)) {
  476 + log.error("获取设备列表失败");
  477 + return;
  478 + }
  479 + Map<String, Object> deviceInfos = JSON.parseObject(deviceResult, new TypeReference<>() {});
  480 + JSONArray deviceInfoList = (JSONArray) deviceInfos.get("data");
  481 + if (CollectionUtils.isEmpty(deviceInfoList)) {
  482 + log.warn("设备列表为空");
  483 + return;
  484 + }
  485 +
  486 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  487 + List<String> dateList = new ArrayList<>();
  488 + try {
  489 + Date start = sdf.parse(startDate);
  490 + Date end = sdf.parse(endDate);
  491 + Calendar cur = Calendar.getInstance();
  492 + cur.setTime(start);
  493 + while (!cur.getTime().after(end)) {
  494 + dateList.add(sdf.format(cur.getTime()));
  495 + cur.add(Calendar.DAY_OF_MONTH, 1);
  496 + }
  497 + } catch (Exception e) {
  498 + log.error("日期解析失败: {} ~ {}", startDate, endDate, e);
  499 + return;
  500 + }
  501 +
  502 + log.info("开始同步OEE时序数据,设备数: {}, 天数: {}", deviceInfoList.size(), dateList.size());
  503 + int totalSaved = 0;
  504 +
  505 + for (Object o : deviceInfoList) {
  506 + JSONObject deviceInfoJson = (JSONObject) o;
  507 + String dtuSn = deviceInfoJson.getString("dtuSn");
  508 +
  509 + for (String dateStr : dateList) {
  510 + try {
  511 + String lampResult = getLampData(dtuSn, dateStr);
  512 + if (StringUtils.isBlank(lampResult)) continue;
  513 +
  514 + Map<String, Object> lampMap = JSON.parseObject(lampResult, new TypeReference<>() {});
  515 + Integer code = (Integer) lampMap.get("code");
  516 + if (code == null || code != 200) continue;
  517 +
  518 + JSONArray dataList = (JSONArray) lampMap.get("data");
  519 + if (CollectionUtils.isEmpty(dataList)) continue;
  520 +
  521 + JSONObject dataObj = (JSONObject) dataList.get(0);
  522 + JSONArray lampDataArr = dataObj.getJSONArray("lampData");
  523 + String description = lampDataArr != null ? lampDataArr.toJSONString() : "[]";
  524 +
  525 + saveOrUpdateOee(dtuSn, dateStr, description);
  526 + totalSaved++;
  527 + } catch (Exception e) {
  528 + log.error("处理OEE设备 {} 数据异常 - date:{}", dtuSn, dateStr, e);
  529 + }
  530 + }
  531 + }
  532 + log.info("OEE时序数据同步完成,共保存 {} 条记录", totalSaved);
  533 + }
  534 +
  535 + private void saveOrUpdateOee(String dtuSn, String oeeDate, String description) {
  536 + int maxLen = 60000;
  537 + String lamp1 = "";
  538 + String lamp2 = "";
  539 +
  540 + if (description.length() > maxLen) {
  541 + lamp1 = description.substring(0, maxLen);
  542 + lamp2 = description.substring(maxLen);
  543 + } else {
  544 + lamp1 = description;
  545 + }
  546 +
  547 + List<Map<String, Object>> existList = jdbcTemplate.queryForList(
  548 + "SELECT id FROM " + oeeTableName + " WHERE corp_code = ? AND dtuSn = ? AND oee_date = ?",
  549 + deviceCorpCode, dtuSn, oeeDate);
  550 +
  551 + Date now = new Date();
  552 + if (!existList.isEmpty()) {
  553 + jdbcTemplate.update(
  554 + "UPDATE " + oeeTableName + " SET triColorLamp1=?, triColorLamp2=?, updated_at=? WHERE dtuSn=? AND oee_date=?",
  555 + lamp1, lamp2, now, dtuSn, oeeDate);
  556 + } else {
  557 + String id = UUID.randomUUID().toString().replace("-", "");
  558 + jdbcTemplate.update(
  559 + "INSERT INTO " + oeeTableName + " (id,corp_code,created_at,created_by,updated_at,updated_by,dtuSn,oee_date,triColorLamp1,triColorLamp2) " +
  560 + "VALUES (?,?,?,?,?,?,?,?,?,?)",
  561 + id, deviceCorpCode, now, "system", now, "system", dtuSn, oeeDate, lamp1, lamp2);
  562 + }
  563 + }
  564 +
262 565 public String getEnergyInfo() {
263 566 String accessToken = getAccessToken();
264 567 Map<String, String> headerMap = new HashMap<>(1);
... ...
1 1 package com.iot.scheduler.service;
2 2
  3 +import com.alibaba.fastjson.JSONArray;
  4 +import com.alibaba.fastjson.JSONObject;
3 5 import lombok.extern.slf4j.Slf4j;
4 6 import org.springframework.beans.factory.annotation.Value;
5 7 import org.springframework.jdbc.core.JdbcTemplate;
... ... @@ -7,8 +9,9 @@ import org.springframework.stereotype.Service;
7 9 import org.springframework.util.StringUtils;
8 10
9 11 import jakarta.annotation.Resource;
10   -import java.util.List;
11   -import java.util.Map;
  12 +
  13 +import java.text.SimpleDateFormat;
  14 +import java.util.*;
12 15
13 16 @Slf4j
14 17 @Service
... ... @@ -18,9 +21,13 @@ public class DeviceSearchService {
18 21 private String deviceCorpCode;
19 22 @Value("${device.db.tableName}")
20 23 private String deviceTableName;
  24 + @Value("${device.db.oeeTableName}")
  25 + private String oeeTableName;
21 26
22 27 @Resource
23 28 private JdbcTemplate jdbcTemplate;
  29 + @Resource
  30 + private DevicePullService devicePullService;
24 31
25 32 public Map<String, Object> queryDeviceList(String deviceName, String lampState, Integer pageNo, Integer pageSize) {
26 33 StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM " + deviceTableName + " WHERE corp_code = ?");
... ... @@ -85,4 +92,462 @@ public class DeviceSearchService {
85 92 "off", off
86 93 );
87 94 }
  95 +
  96 + public Map<String, Object> queryLampData(String dtuSn, String date) {
  97 + String result = devicePullService.getLampData(dtuSn, date);
  98 + Map<String, Object> res = com.alibaba.fastjson.JSON.parseObject(result, new com.alibaba.fastjson.TypeReference<>() {});
  99 + Integer code = (Integer) res.get("code");
  100 + if (code == null || code != 200) {
  101 + return Map.of("lampDurationStats", Map.of(), "list", List.of());
  102 + }
  103 +
  104 + JSONArray dataList = (JSONArray) res.get("data");
  105 + if (dataList == null || dataList.isEmpty()) {
  106 + return Map.of("lampDurationStats", Map.of(), "list", List.of());
  107 + }
  108 +
  109 + // 统计各灯状态时长
  110 + long offTotal = 0, redTotal = 0, yellowTotal = 0, greenTotal = 0, blueTotal = 0;
  111 + for (int i = 0; i < dataList.size(); i++) {
  112 + JSONObject item = (JSONObject) dataList.get(i);
  113 + JSONArray lampDataList = item.getJSONArray("lampData");
  114 + if (lampDataList == null) continue;
  115 +
  116 + for (int j = 0; j < lampDataList.size(); j++) {
  117 + JSONObject lamp = (JSONObject) lampDataList.get(j);
  118 + int state = lamp.getIntValue("lampState");
  119 + long duration = lamp.getLongValue("duration");
  120 + switch (state) {
  121 + case 0 -> offTotal += duration;
  122 + case 1 -> redTotal += duration;
  123 + case 2 -> yellowTotal += duration;
  124 + case 3 -> greenTotal += duration;
  125 + case 4 -> blueTotal += duration;
  126 + }
  127 + }
  128 + }
  129 +
  130 + Map<String, String> stats = new LinkedHashMap<>();
  131 + stats.put("off", formatDuration(offTotal));
  132 + stats.put("red", formatDuration(redTotal));
  133 + stats.put("yellow", formatDuration(yellowTotal));
  134 + stats.put("green", formatDuration(greenTotal));
  135 + stats.put("blue", formatDuration(blueTotal));
  136 +
  137 + return Map.of(
  138 + "lampDurationStats", stats,
  139 + "list", dataList
  140 + );
  141 + }
  142 +
  143 + private String formatDuration(long totalSeconds) {
  144 + long hours = totalSeconds / 3600;
  145 + long minutes = (totalSeconds % 3600) / 60;
  146 + long seconds = totalSeconds % 60;
  147 +
  148 + if (hours > 0) {
  149 + return hours + "时" + minutes + "分" + seconds + "秒";
  150 + }
  151 + if (minutes > 0) {
  152 + return minutes + "分" + seconds + "秒";
  153 + }
  154 + return seconds + "秒";
  155 + }
  156 +
  157 + /**
  158 + * 稼动率/OEE统计查询
  159 + * @param dtuSn 设备序列号(可选,为空查全部)
  160 + * @param type 查询类型:day-日(按日期段), week-周(今年第1周~本周), month-月(今年1月~本月)
  161 + * @param startDate 日模式下的开始日期(yyyy-MM-dd)
  162 + * @param endDate 日模式下的结束日期(yyyy-MM-dd)
  163 + */
  164 + public Map<String, Object> queryOeeStats(String dtuSn, String type, String startDate, String endDate) {
  165 + // 1. 根据类型确定日期范围
  166 + List<String> dates = buildDateRange(type, startDate, endDate);
  167 + log.info("OEE查询 - type:{}, dtuSn:{}, 日期范围:{}天, 起始:{}, 结束:{}", type, dtuSn, dates.size(), dates.get(0), dates.get(dates.size() - 1));
  168 +
  169 + // 判断是否包含今天
  170 + String todayStr = new SimpleDateFormat("yyyy-MM-dd").format(new Date());
  171 + boolean includeToday = dates.contains(todayStr);
  172 +
  173 + // 2. 从数据库查询 oee 表数据
  174 + List<Map<String, Object>> dbRecords = queryOeeFromDb(dtuSn, dates, includeToday ? todayStr : null);
  175 + log.info("OEE查询 - DB返回记录数:{}, 排除today:{}", dbRecords.size(), includeToday ? todayStr : "无");
  176 +
  177 + Map<String, JSONObject> dbDataMap = new LinkedHashMap<>();
  178 + for (Map<String, Object> record : dbRecords) {
  179 + String oeeDate = String.valueOf(record.get("oee_date"));
  180 + // 截断时间部分,只保留日期 yyyy-MM-dd
  181 + if (oeeDate.length() > 10) {
  182 + oeeDate = oeeDate.substring(0, 10);
  183 + }
  184 + String lamp1 = (String) record.get("triColorLamp1");
  185 + String lamp2 = (String) record.get("triColorLamp2");
  186 + String fullJson = (lamp1 != null ? lamp1 : "") + (lamp2 != null ? lamp2 : "");
  187 + if (!fullJson.isEmpty()) {
  188 + try {
  189 + JSONArray arr = JSONArray.parseArray(fullJson);
  190 + dbDataMap.put(oeeDate, new JSONObject().fluentPut("lampData", arr));
  191 + } catch (Exception e) {
  192 + log.warn("解析OEE JSON异常 - date:{}, dtuSn:{}", oeeDate, dtuSn, e);
  193 + }
  194 + }
  195 + }
  196 +
  197 + // 3. 如果包含今天且数据库没有今天的实时数据,则调用接口获取
  198 + if (includeToday && !dbDataMap.containsKey(todayStr)) {
  199 + if (StringUtils.hasText(dtuSn)) {
  200 + String apiResult = devicePullService.getLampData(dtuSn, todayStr);
  201 + if (StringUtils.hasText(apiResult)) {
  202 + Map<String, Object> res = com.alibaba.fastjson.JSON.parseObject(apiResult,
  203 + new com.alibaba.fastjson.TypeReference<>() {});
  204 + Integer code = (Integer) res.get("code");
  205 + if (code != null && code == 200) {
  206 + JSONArray dataList = (JSONArray) res.get("data");
  207 + if (dataList != null && !dataList.isEmpty()) {
  208 + JSONObject dataObj = (JSONObject) dataList.get(0);
  209 + dbDataMap.put(todayStr, dataObj);
  210 + }
  211 + }
  212 + }
  213 + }
  214 + }
  215 + log.info("OEE查询 - 最终有效数据日期数:{}, 日期列表:{}", dbDataMap.size(), dbDataMap.keySet());
  216 +
  217 + // 4. 根据 type 决定统计粒度
  218 + List<Map<String, Object>> statsList;
  219 + Map<String, Object> summary;
  220 +
  221 + if ("day".equals(type)) {
  222 + Map<String, Object> result = aggregateByDay(dates, dbDataMap);
  223 + statsList = (List<Map<String, Object>>) result.get("list");
  224 + summary = (Map<String, Object>) result.get("summary");
  225 + } else if ("week".equals(type)) {
  226 + Map<String, Object> result = aggregateByWeek(dates, dbDataMap);
  227 + statsList = (List<Map<String, Object>>) result.get("list");
  228 + summary = (Map<String, Object>) result.get("summary");
  229 + } else {
  230 + Map<String, Object> result = aggregateByMonth(dates, dbDataMap);
  231 + statsList = (List<Map<String, Object>>) result.get("list");
  232 + summary = (Map<String, Object>) result.get("summary");
  233 + }
  234 +
  235 + return Map.of(
  236 + "summary", summary,
  237 + "list", statsList
  238 + );
  239 + }
  240 +
  241 + /**
  242 + * 按天统计
  243 + */
  244 + private Map<String, Object> aggregateByDay(List<String> dates, Map<String, JSONObject> dbDataMap) {
  245 + List<Map<String, Object>> dailyStats = new ArrayList<>();
  246 + long totalOffDur = 0, totalRedDur = 0, totalYellowDur = 0, totalGreenDur = 0, totalBlueDur = 0;
  247 + int totalOffCnt = 0, totalRedCnt = 0, totalYellowCnt = 0, totalGreenCnt = 0, totalBlueCnt = 0;
  248 +
  249 + for (String d : dates) {
  250 + long[] counts = countLampData(dbDataMap.get(d));
  251 +
  252 + totalOffDur += counts[0]; totalRedDur += counts[2]; totalYellowDur += counts[4];
  253 + totalGreenDur += counts[6]; totalBlueDur += counts[8];
  254 + totalOffCnt += (int)counts[1]; totalRedCnt += (int)counts[3]; totalYellowCnt += (int)counts[5];
  255 + totalGreenCnt += (int)counts[7]; totalBlueCnt += (int)counts[9];
  256 +
  257 + Map<String, Object> dayStat = new LinkedHashMap<>();
  258 + dayStat.put("label", d);
  259 + dayStat.put("date", d);
  260 + dayStat.put("off", Map.of("duration", formatDuration(counts[0]), "seconds", counts[0], "count", (int)counts[1]));
  261 + dayStat.put("red", Map.of("duration", formatDuration(counts[2]), "seconds", counts[2], "count", (int)counts[3]));
  262 + dayStat.put("yellow", Map.of("duration", formatDuration(counts[4]), "seconds", counts[4], "count", (int)counts[5]));
  263 + dayStat.put("green", Map.of("duration", formatDuration(counts[6]), "seconds", counts[6], "count", (int)counts[7]));
  264 + dayStat.put("blue", Map.of("duration", formatDuration(counts[8]), "seconds", counts[8], "count", (int)counts[9]));
  265 + dailyStats.add(dayStat);
  266 + }
  267 +
  268 + Map<String, Object> summary = buildSummary(totalOffDur, totalRedDur, totalYellowDur, totalGreenDur, totalBlueDur,
  269 + totalOffCnt, totalRedCnt, totalYellowCnt, totalGreenCnt, totalBlueCnt);
  270 +
  271 + return Map.of("list", dailyStats, "summary", summary);
  272 + }
  273 +
  274 + /**
  275 + * 按周统计
  276 + */
  277 + private Map<String, Object> aggregateByWeek(List<String> dates, Map<String, JSONObject> dbDataMap) {
  278 + // 按自然周(周一~周日)分组,第一周从1月1日开始(可能不足7天)
  279 + List<List<String>> weekBuckets = new ArrayList<>();
  280 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  281 +
  282 + // 确定年份和该年第一个周一的日期
  283 + String yearStr = dates.get(0).substring(0, 4);
  284 + int year = Integer.parseInt(yearStr);
  285 + Calendar jan1 = Calendar.getInstance();
  286 + jan1.set(year, Calendar.JANUARY, 1);
  287 + int jan1Dow = jan1.get(Calendar.DAY_OF_WEEK);
  288 +
  289 + // 找到第一个周一:如果1月1日就是周一,则firstMonday=1月1日;否则往后推到周一
  290 + Calendar firstMonday = Calendar.getInstance();
  291 + firstMonday.setTime(jan1.getTime());
  292 + int daysToAdd = jan1Dow == Calendar.MONDAY ? 0 : (Calendar.MONDAY - jan1Dow + 7) % 7;
  293 + firstMonday.add(Calendar.DATE, daysToAdd);
  294 +
  295 + for (String d : dates) {
  296 + try {
  297 + Date date = sdf.parse(d);
  298 + Calendar cal = Calendar.getInstance();
  299 + cal.setTime(date);
  300 + cal.set(Calendar.HOUR_OF_DAY, 0);
  301 + cal.set(Calendar.MINUTE, 0);
  302 + cal.set(Calendar.SECOND, 0);
  303 + cal.set(Calendar.MILLISECOND, 0);
  304 +
  305 + int bucketIndex;
  306 + if (!cal.after(firstMonday)) {
  307 + // 在第一个周一之前(含当天),属于第1周(从1月1日开始)
  308 + bucketIndex = 0;
  309 + } else {
  310 + // 第一个周一之后,按完整自然周计算
  311 + long diffMs = cal.getTimeInMillis() - firstMonday.getTimeInMillis();
  312 + long diffDays = diffMs / (1000 * 60 * 60 * 24);
  313 + bucketIndex = 1 + (int)(diffDays / 7); // 第2周、第3周...
  314 + }
  315 +
  316 + while (weekBuckets.size() <= bucketIndex) {
  317 + weekBuckets.add(new ArrayList<>());
  318 + }
  319 + weekBuckets.get(bucketIndex).add(d);
  320 + } catch (Exception e) {
  321 + log.warn("解析日期失败: {}", d);
  322 + }
  323 + }
  324 +
  325 + List<Map<String, Object>> weeklyStats = new ArrayList<>();
  326 + long tOffD=0, tRedD=0, tYelD=0, tGrnD=0, tBluD=0;
  327 + int tOffC=0, tRedC=0, tYelC=0, tGrnC=0, tBluC=0;
  328 +
  329 + for (int i = 0; i < weekBuckets.size(); i++) {
  330 + List<String> daysInWeek = weekBuckets.get(i);
  331 + if (daysInWeek.isEmpty()) continue;
  332 + long wOffD=0, wRedD=0, wYelD=0, wGrnD=0, wBluD=0;
  333 + int wOffC=0, wRedC=0, wYelC=0, wGrnC=0, wBluC=0;
  334 +
  335 + for (String d : daysInWeek) {
  336 + long[] cnt = countLampData(dbDataMap.get(d));
  337 + wOffD += cnt[0]; wRedD += cnt[2]; wYelD += cnt[4]; wGrnD += cnt[6]; wBluD += cnt[8];
  338 + wOffC += (int)cnt[1]; wRedC += (int)cnt[3]; wYelC += (int)cnt[5]; wGrnC += (int)cnt[7]; wBluC += (int)cnt[9];
  339 + }
  340 +
  341 + tOffD+=wOffD; tRedD+=wRedD; tYelD+=wYelD; tGrnD+=wGrnD; tBluD+=wBluD;
  342 + tOffC+=wOffC; tRedC+=wRedC; tYelC+=wYelC; tGrnC+=wGrnC; tBluC+=wBluC;
  343 +
  344 + Collections.sort(daysInWeek);
  345 + Map<String, Object> ws = new LinkedHashMap<>();
  346 + ws.put("label", (i + 1) + "周");
  347 + ws.put("week", yearStr + "-W" + String.format("%02d", i + 1));
  348 + ws.put("startDate", daysInWeek.get(0));
  349 + ws.put("endDate", daysInWeek.get(daysInWeek.size() - 1));
  350 + ws.put("off", Map.of("duration", formatDuration(wOffD), "seconds", wOffD, "count", wOffC));
  351 + ws.put("red", Map.of("duration", formatDuration(wRedD), "seconds", wRedD, "count", wRedC));
  352 + ws.put("yellow", Map.of("duration", formatDuration(wYelD), "seconds", wYelD, "count", wYelC));
  353 + ws.put("green", Map.of("duration", formatDuration(wGrnD), "seconds", wGrnD, "count", wGrnC));
  354 + ws.put("blue", Map.of("duration", formatDuration(wBluD), "seconds", wBluD, "count", wBluC));
  355 + weeklyStats.add(ws);
  356 + }
  357 +
  358 + Map<String, Object> summary = buildSummary(tOffD, tRedD, tYelD, tGrnD, tBluD,
  359 + tOffC, tRedC, tYelC, tGrnC, tBluC);
  360 + return Map.of("list", weeklyStats, "summary", summary);
  361 + }
  362 +
  363 + /**
  364 + * 按月统计
  365 + */
  366 + private Map<String, Object> aggregateByMonth(List<String> dates, Map<String, JSONObject> dbDataMap) {
  367 + // 按 年-月 分组
  368 + Map<String, List<String>> monthGroups = new LinkedHashMap<>();
  369 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  370 +
  371 + for (String d : dates) {
  372 + String monthKey = d.substring(0, 7); // yyyy-MM
  373 + monthGroups.computeIfAbsent(monthKey, k -> new ArrayList<>()).add(d);
  374 + }
  375 +
  376 + List<Map<String, Object>> monthlyStats = new ArrayList<>();
  377 + long tOffD=0, tRedD=0, tYelD=0, tGrnD=0, tBluD=0;
  378 + int tOffC=0, tRedC=0, tYelC=0, tGrnC=0, tBluC=0;
  379 +
  380 + for (Map.Entry<String, List<String>> entry : monthGroups.entrySet()) {
  381 + String monthKey = entry.getKey();
  382 + List<String> daysInMonth = entry.getValue();
  383 + long mOffD=0, mRedD=0, mYelD=0, mGrnD=0, mBluD=0;
  384 + int mOffC=0, mRedC=0, mYelC=0, mGrnC=0, mBluC=0;
  385 +
  386 + for (String d : daysInMonth) {
  387 + long[] cnt = countLampData(dbDataMap.get(d));
  388 + mOffD += cnt[0]; mRedD += cnt[2]; mYelD += cnt[4]; mGrnD += cnt[6]; mBluD += cnt[8];
  389 + mOffC += (int)cnt[1]; mRedC += (int)cnt[3]; mYelC += (int)cnt[5]; mGrnC += (int)cnt[7]; mBluC += (int)cnt[9];
  390 + }
  391 +
  392 + tOffD+=mOffD; tRedD+=mRedD; tYelD+=mYelD; tGrnD+=mGrnD; tBluD+=mBluD;
  393 + tOffC+=mOffC; tRedC+=mRedC; tYelC+=mYelC; tGrnC+=mGrnC; tBluC+=mBluC;
  394 +
  395 + String monthNum = monthKey.substring(5); // "01", "02" ...
  396 + Map<String, Object> ms = new LinkedHashMap<>();
  397 + ms.put("label", Integer.parseInt(monthNum) + "月");
  398 + ms.put("month", monthKey);
  399 + ms.put("off", Map.of("duration", formatDuration(mOffD), "seconds", mOffD, "count", mOffC));
  400 + ms.put("red", Map.of("duration", formatDuration(mRedD), "seconds", mRedD, "count", mRedC));
  401 + ms.put("yellow", Map.of("duration", formatDuration(mYelD), "seconds", mYelD, "count", mYelC));
  402 + ms.put("green", Map.of("duration", formatDuration(mGrnD), "seconds", mGrnD, "count", mGrnC));
  403 + ms.put("blue", Map.of("duration", formatDuration(mBluD), "seconds", mBluD, "count", mBluC));
  404 + monthlyStats.add(ms);
  405 + }
  406 +
  407 + Map<String, Object> summary = buildSummary(tOffD, tRedD, tYelD, tGrnD, tBluD,
  408 + tOffC, tRedC, tYelC, tGrnC, tBluC);
  409 + return Map.of("list", monthlyStats, "summary", summary);
  410 + }
  411 +
  412 + /**
  413 + * 统计单日 lampData 各状态时长和次数
  414 + * 返回 [offDur, offCnt, redDur, redCnt, yelDur, yelCnt, grnDur, grnCnt, bluDur, bluCnt]
  415 + */
  416 + private long[] countLampData(JSONObject dayData) {
  417 + long[] result = new long[10];
  418 + if (dayData != null) {
  419 + JSONArray lampArr = dayData.getJSONArray("lampData");
  420 + if (lampArr != null) {
  421 + for (int i = 0; i < lampArr.size(); i++) {
  422 + JSONObject item = lampArr.getJSONObject(i);
  423 + int state = item.getIntValue("lampState");
  424 + long dur = item.getLongValue("duration");
  425 + switch (state) {
  426 + case 0 -> { result[0] += dur; result[1]++; }
  427 + case 1 -> { result[2] += dur; result[3]++; }
  428 + case 2 -> { result[4] += dur; result[5]++; }
  429 + case 3 -> { result[6] += dur; result[7]++; }
  430 + case 4 -> { result[8] += dur; result[9]++; }
  431 + }
  432 + }
  433 + }
  434 + }
  435 + return result;
  436 + }
  437 +
  438 + /**
  439 + * 构建 summary 汇总对象
  440 + */
  441 + private Map<String, Object> buildSummary(long offD, long redD, long yelD, long grnD, long bluD,
  442 + int offC, int redC, int yelC, int grnC, int bluC) {
  443 + Map<String, Object> summary = new LinkedHashMap<>();
  444 + summary.put("off", Map.of("duration", formatDuration(offD), "seconds", offD, "count", offC));
  445 + summary.put("red", Map.of("duration", formatDuration(redD), "seconds", redD, "count", redC));
  446 + summary.put("yellow", Map.of("duration", formatDuration(yelD), "seconds", yelD, "count", yelC));
  447 + summary.put("green", Map.of("duration", formatDuration(grnD), "seconds", grnD, "count", grnC));
  448 + summary.put("blue", Map.of("duration", formatDuration(bluD), "seconds", bluD, "count", bluC));
  449 + return summary;
  450 + }
  451 +
  452 + /**
  453 + * 根据查询类型构建日期列表
  454 + */
  455 + private List<String> buildDateRange(String type, String startDate, String endDate) {
  456 + List<String> result = new ArrayList<>();
  457 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  458 +
  459 + switch (type) {
  460 + case "day": {
  461 + // 指定日期范围
  462 + if (StringUtils.hasText(startDate) && StringUtils.hasText(endDate)) {
  463 + try {
  464 + Date start = sdf.parse(startDate);
  465 + Date end = sdf.parse(endDate);
  466 + Calendar cur = Calendar.getInstance();
  467 + cur.setTime(start);
  468 + while (!cur.getTime().after(end)) {
  469 + result.add(sdf.format(cur.getTime()));
  470 + cur.add(Calendar.DAY_OF_MONTH, 1);
  471 + }
  472 + } catch (Exception e) {
  473 + log.error("日期解析失败: {} ~ {}", startDate, endDate, e);
  474 + }
  475 + }
  476 + break;
  477 + }
  478 + case "week": {
  479 + // 今年1月1日 ~ 今天所在周的周日(自然周,不跨年)
  480 + Calendar now = Calendar.getInstance();
  481 + int currentYear = now.get(Calendar.YEAR);
  482 +
  483 + Calendar startCal = Calendar.getInstance();
  484 + startCal.set(currentYear, Calendar.JANUARY, 1);
  485 +
  486 + // 本周结束(周日)
  487 + Calendar endCal = (Calendar) now.clone();
  488 + int todayDow = endCal.get(Calendar.DAY_OF_WEEK);
  489 + int toSunday = todayDow == Calendar.SUNDAY ? 0 : (7 - todayDow);
  490 + endCal.add(Calendar.DATE, toSunday);
  491 +
  492 + Calendar cur = Calendar.getInstance();
  493 + cur.setTime(startCal.getTime());
  494 + while (!cur.after(endCal)) {
  495 + result.add(sdf.format(cur.getTime()));
  496 + cur.add(Calendar.DAY_OF_MONTH, 1);
  497 + }
  498 + break;
  499 + }
  500 + case "month": {
  501 + // 今年1月 ~ 本月最后一天
  502 + Calendar now = Calendar.getInstance();
  503 + int year = now.get(Calendar.YEAR);
  504 + int thisMonth = now.get(Calendar.MONTH); // 0-indexed
  505 +
  506 + for (int m = 0; m <= thisMonth; m++) {
  507 + Calendar cal = Calendar.getInstance();
  508 + cal.set(year, m, 1);
  509 + int lastDay = cal.getActualMaximum(Calendar.DAY_OF_MONTH);
  510 + for (int d = 1; d <= lastDay; d++) {
  511 + cal.set(year, m, d);
  512 + result.add(sdf.format(cal.getTime()));
  513 + }
  514 + }
  515 + break;
  516 + }
  517 + }
  518 + return result;
  519 + }
  520 +
  521 + /**
  522 + * 从 oee 表查询指定日期范围的数据
  523 + */
  524 + private List<Map<String, Object>> queryOeeFromDb(String dtuSn, List<String> dates, String excludeToday) {
  525 + if (dates.isEmpty()) return Collections.emptyList();
  526 +
  527 + StringBuilder sql = new StringBuilder(
  528 + "SELECT oee_date, triColorLamp1, triColorLamp2 FROM " + oeeTableName + " WHERE corp_code = ?");
  529 + List<Object> params = new ArrayList<>();
  530 + params.add(deviceCorpCode);
  531 +
  532 + if (StringUtils.hasText(dtuSn)) {
  533 + sql.append(" AND dtuSn = ?");
  534 + params.add(dtuSn);
  535 + }
  536 +
  537 + sql.append(" AND oee_date IN (");
  538 + List<Object> dateParams = new ArrayList<>();
  539 + for (String d : dates) {
  540 + if (d.equals(excludeToday)) continue; // 排除今天,今天走接口
  541 + sql.append("?,");
  542 + dateParams.add(d);
  543 + }
  544 + if (dateParams.isEmpty()) {
  545 + return Collections.emptyList(); // 所有日期都是今天
  546 + }
  547 + sql.deleteCharAt(sql.length() - 1).append(")");
  548 + params.addAll(dateParams);
  549 + sql.append(" ORDER BY oee_date ASC");
  550 +
  551 + return jdbcTemplate.queryForList(sql.toString(), params.toArray());
  552 + }
88 553 }
... ...
... ... @@ -38,6 +38,10 @@ scheduler:
38 38 panji:
39 39 pull: "0 0/5 * * * ?"
40 40 push: "0 0/10 * * * ?"
  41 + devUtil:
  42 + cron: "0 30 2 * * ?" # 每日凌晨 2:30 增量同步昨天数据
  43 + oee:
  44 + cron: "0 35 2 * * ?" # 每日凌晨 2:35 增量同步昨天OEE数据
41 45
42 46 device:
43 47 token:
... ... @@ -57,3 +61,7 @@ device:
57 61 db:
58 62 corpCode: "ymk"
59 63 tableName: "t_auto_ymk_iot_device"
  64 + devUtilTableName: "t_auto_ymk_iot_dev_util"
  65 + oeeTableName: "t_auto_ymk_iot_dev_oee"
  66 + lamp:
  67 + url: "https://iotgc.cniot.vip/triColorLamp/dtuSn"
... ...