Commit bcbcf7c9e127e10e16954e840e67d775a8b6884f

Authored by 杨鸣坤
1 parent 0fbac06e

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

@@ -36,4 +36,32 @@ public class HealthController { @@ -36,4 +36,32 @@ public class HealthController {
36 public Map<String, Object> deviceStats() { 36 public Map<String, Object> deviceStats() {
37 return deviceSearchService.queryDeviceStats(); 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,6 +34,8 @@ import java.text.SimpleDateFormat;
34 import java.util.*; 34 import java.util.*;
35 import java.util.concurrent.TimeUnit; 35 import java.util.concurrent.TimeUnit;
36 36
  37 +import org.springframework.scheduling.annotation.Scheduled;
  38 +
37 @Slf4j 39 @Slf4j
38 @Service 40 @Service
39 public class DevicePullService { 41 public class DevicePullService {
@@ -57,6 +59,12 @@ public class DevicePullService { @@ -57,6 +59,12 @@ public class DevicePullService {
57 private String deviceCorpCode; 59 private String deviceCorpCode;
58 @Value("${device.db.tableName}") 60 @Value("${device.db.tableName}")
59 private String deviceTableName; 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 @Resource 69 @Resource
62 private RedisTemplate<String, String> redisTemplate; 70 private RedisTemplate<String, String> redisTemplate;
@@ -172,15 +180,14 @@ public class DevicePullService { @@ -172,15 +180,14 @@ public class DevicePullService {
172 return deviceInfoDetail; 180 return deviceInfoDetail;
173 } 181 }
174 182
175 - public String getDtuSnRateOfAction(String dtuSn) { 183 + public String getDtuSnRateOfAction(String dtuSn, String startDate, String endDate) {
176 String accessToken = getAccessToken(); 184 String accessToken = getAccessToken();
177 Map<String, String> headerMap = new HashMap<>(1); 185 Map<String, String> headerMap = new HashMap<>(1);
178 headerMap.put("Authorization", "Bearer " + accessToken); 186 headerMap.put("Authorization", "Bearer " + accessToken);
179 187
180 Map<String, String> paramsMap = new HashMap<>(); 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 paramsMap.put("dtuSn", dtuSn); 191 paramsMap.put("dtuSn", dtuSn);
185 192
186 String rateResult = sendRequestGet(deviceSnRateUrl, paramsMap, headerMap); 193 String rateResult = sendRequestGet(deviceSnRateUrl, paramsMap, headerMap);
@@ -198,7 +205,8 @@ public class DevicePullService { @@ -198,7 +205,8 @@ public class DevicePullService {
198 } 205 }
199 206
200 public String getUtilizationRate(String dtuSn) { 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 if (StringUtils.isBlank(rateResult)) { 210 if (StringUtils.isBlank(rateResult)) {
203 return "0%"; 211 return "0%";
204 } 212 }
@@ -231,6 +239,18 @@ public class DevicePullService { @@ -231,6 +239,18 @@ public class DevicePullService {
231 return String.format("%.2f%%", rate); 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 public void saveOrUpdateDevice(String projectState, String projectType, String deviceName, String dtuId, 254 public void saveOrUpdateDevice(String projectState, String projectType, String deviceName, String dtuId,
235 String deviceId, String dtuSn, String lampState, String startTime, 255 String deviceId, String dtuSn, String lampState, String startTime,
236 String duration, String utilizationRate) { 256 String duration, String utilizationRate) {
@@ -259,6 +279,289 @@ public class DevicePullService { @@ -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 public String getEnergyInfo() { 565 public String getEnergyInfo() {
263 String accessToken = getAccessToken(); 566 String accessToken = getAccessToken();
264 Map<String, String> headerMap = new HashMap<>(1); 567 Map<String, String> headerMap = new HashMap<>(1);
1 package com.iot.scheduler.service; 1 package com.iot.scheduler.service;
2 2
  3 +import com.alibaba.fastjson.JSONArray;
  4 +import com.alibaba.fastjson.JSONObject;
3 import lombok.extern.slf4j.Slf4j; 5 import lombok.extern.slf4j.Slf4j;
4 import org.springframework.beans.factory.annotation.Value; 6 import org.springframework.beans.factory.annotation.Value;
5 import org.springframework.jdbc.core.JdbcTemplate; 7 import org.springframework.jdbc.core.JdbcTemplate;
@@ -7,8 +9,9 @@ import org.springframework.stereotype.Service; @@ -7,8 +9,9 @@ import org.springframework.stereotype.Service;
7 import org.springframework.util.StringUtils; 9 import org.springframework.util.StringUtils;
8 10
9 import jakarta.annotation.Resource; 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 @Slf4j 16 @Slf4j
14 @Service 17 @Service
@@ -18,9 +21,13 @@ public class DeviceSearchService { @@ -18,9 +21,13 @@ public class DeviceSearchService {
18 private String deviceCorpCode; 21 private String deviceCorpCode;
19 @Value("${device.db.tableName}") 22 @Value("${device.db.tableName}")
20 private String deviceTableName; 23 private String deviceTableName;
  24 + @Value("${device.db.oeeTableName}")
  25 + private String oeeTableName;
21 26
22 @Resource 27 @Resource
23 private JdbcTemplate jdbcTemplate; 28 private JdbcTemplate jdbcTemplate;
  29 + @Resource
  30 + private DevicePullService devicePullService;
24 31
25 public Map<String, Object> queryDeviceList(String deviceName, String lampState, Integer pageNo, Integer pageSize) { 32 public Map<String, Object> queryDeviceList(String deviceName, String lampState, Integer pageNo, Integer pageSize) {
26 StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM " + deviceTableName + " WHERE corp_code = ?"); 33 StringBuilder countSql = new StringBuilder("SELECT COUNT(*) FROM " + deviceTableName + " WHERE corp_code = ?");
@@ -85,4 +92,462 @@ public class DeviceSearchService { @@ -85,4 +92,462 @@ public class DeviceSearchService {
85 "off", off 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,6 +38,10 @@ scheduler:
38 panji: 38 panji:
39 pull: "0 0/5 * * * ?" 39 pull: "0 0/5 * * * ?"
40 push: "0 0/10 * * * ?" 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 device: 46 device:
43 token: 47 token:
@@ -57,3 +61,7 @@ device: @@ -57,3 +61,7 @@ device:
57 db: 61 db:
58 corpCode: "ymk" 62 corpCode: "ymk"
59 tableName: "t_auto_ymk_iot_device" 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"