From e46bfa3ff94a8a1b4daf37c7fcb79c2fab22a72c Mon Sep 17 00:00:00 2001
From: doum <doum>
Date: 星期五, 29 五月 2026 17:10:00 +0800
Subject: [PATCH] 新增智能电表、空调管理

---
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java |   32 +++
 server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalDataSyncDTO.java          |   13 +
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java          |    5 
 server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java         |   10 +
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java           |    3 
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java          |    5 
 admin/src/views/business/ywelectricaldata.vue                                                                 |  137 +++++++++++++++--
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java |  104 +++++++++---
 server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java  |  160 ++++++++++++++++++-
 9 files changed, 406 insertions(+), 63 deletions(-)

diff --git a/admin/src/views/business/ywelectricaldata.vue b/admin/src/views/business/ywelectricaldata.vue
index 7dc1ab5..3235191 100644
--- a/admin/src/views/business/ywelectricaldata.vue
+++ b/admin/src/views/business/ywelectricaldata.vue
@@ -80,6 +80,33 @@
         :pagination="tableData.pagination"
       />
     </template>
+    <el-dialog
+      title="绔嬪嵆鎶勮〃"
+      :visible.sync="syncDialogVisible"
+      width="520px"
+      append-to-body
+      @closed="resetSyncForm"
+    >
+      <el-form ref="syncFormRef" :model="syncForm" :rules="syncRules" label-width="110px">
+        <el-form-item label="鎶勮〃鏃堕棿娈�" prop="readTimeRange">
+          <el-date-picker
+            v-model="syncForm.readTimeRange"
+            type="datetimerange"
+            value-format="yyyy-MM-dd HH:mm:ss"
+            range-separator="-"
+            start-placeholder="寮�濮嬫椂闂�"
+            end-placeholder="缁撴潫鏃堕棿"
+            :picker-options="syncPickerOptions"
+            style="width: 100%"
+          />
+        </el-form-item>
+        <p class="sync-tip">鏃堕棿娈垫渶闀夸笉瓒呰繃7澶�</p>
+      </el-form>
+      <template v-slot:footer>
+        <el-button @click="syncDialogVisible = false">鍙栨秷</el-button>
+        <el-button type="primary" :loading="isReading" @click="submitSync">纭鎶勮〃</el-button>
+      </template>
+    </el-dialog>
   </TableLayout>
 </template>
 
@@ -108,7 +135,32 @@
         readTimeRange: []
       },
       roomOptions: [],
-      isReading: false
+      isReading: false,
+      syncDialogVisible: false,
+      syncMinDate: null,
+      syncForm: {
+        readTimeRange: []
+      },
+      syncRules: {
+        readTimeRange: [{ required: true, message: '璇烽�夋嫨鎶勮〃鏃堕棿娈�', trigger: 'change' }]
+      }
+    }
+  },
+  computed: {
+    syncPickerOptions () {
+      return {
+        onPick: ({ minDate, maxDate }) => {
+          this.syncMinDate = minDate && !maxDate ? minDate : null
+        },
+        disabledDate: (time) => {
+          if (!this.syncMinDate) return false
+          const anchor = dayjs(this.syncMinDate)
+          const min = anchor.subtract(7, 'day').startOf('day')
+          const max = anchor.add(7, 'day').endOf('day')
+          const current = dayjs(time)
+          return current.isBefore(min) || current.isAfter(max)
+        }
+      }
     }
   },
   created () {
@@ -181,22 +233,69 @@
       this.search()
     },
     handleReadNow () {
-      this.$dialog.actionConfirm('纭绔嬪嵆浠庣涓夋柟骞冲彴鍚屾鎶勮〃鏁版嵁鍚楋紵', '鎿嶄綔纭鎻愰啋')
-        .then(() => {
-          this.isReading = true
-          dataApi.syncAll({})
-            .then(res => {
-              this.$tip.apiSuccess(res || '鎶勮〃鍚屾鎴愬姛')
-              this.search()
-            })
-            .catch(e => {
-              this.$tip.apiFailed(e)
-            })
-            .finally(() => {
-              this.isReading = false
-            })
+      this.syncForm.readTimeRange = this.defaultSyncRange()
+      this.syncDialogVisible = true
+      this.$nextTick(() => {
+        if (this.$refs.syncFormRef) {
+          this.$refs.syncFormRef.clearValidate()
+        }
+      })
+    },
+    defaultSyncRange () {
+      const end = dayjs()
+      const start = end.subtract(24, 'hour')
+      return [start.format('YYYY-MM-DD HH:mm:ss'), end.format('YYYY-MM-DD HH:mm:ss')]
+    },
+    resetSyncForm () {
+      this.syncMinDate = null
+      this.syncForm.readTimeRange = []
+      if (this.$refs.syncFormRef) {
+        this.$refs.syncFormRef.resetFields()
+      }
+    },
+    validateSyncRange (range) {
+      if (!range || range.length !== 2) {
+        this.$tip.warning('璇烽�夋嫨鎶勮〃鏃堕棿娈�')
+        return false
+      }
+      const start = dayjs(range[0])
+      const end = dayjs(range[1])
+      if (!start.isValid() || !end.isValid()) {
+        this.$tip.warning('鎶勮〃鏃堕棿鏍煎紡涓嶆纭�')
+        return false
+      }
+      if (!end.isAfter(start)) {
+        this.$tip.warning('鎶勮〃寮�濮嬫椂闂村繀椤绘棭浜庣粨鏉熸椂闂�')
+        return false
+      }
+      if (end.diff(start, 'day', true) > 7) {
+        this.$tip.warning('鎶勮〃鏃堕棿娈典笉鑳借秴杩�7澶�')
+        return false
+      }
+      return true
+    },
+    submitSync () {
+      this.$refs.syncFormRef.validate(valid => {
+        if (!valid) return
+        const range = this.syncForm.readTimeRange
+        if (!this.validateSyncRange(range)) return
+        this.isReading = true
+        dataApi.syncAll({
+          readTimeBegin: range[0],
+          readTimeEnd: range[1]
         })
-        .catch(() => {})
+          .then(res => {
+            this.$tip.apiSuccess(res || '鎶勮〃鍚屾鎴愬姛')
+            this.syncDialogVisible = false
+            this.search()
+          })
+          .catch(e => {
+            this.$tip.apiFailed(e)
+          })
+          .finally(() => {
+            this.isReading = false
+          })
+      })
     },
     formatJsfs (val) {
       if (val == null || val === '') return '-'
@@ -235,4 +334,10 @@
 
 <style scoped>
 .red { color: #f56c6c; }
+.sync-tip {
+  margin: 0 0 0 110px;
+  color: #909399;
+  font-size: 12px;
+  line-height: 1.5;
+}
 </style>
diff --git a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java
index babb5eb..13df3ec 100644
--- a/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java
+++ b/server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java
@@ -7,6 +7,7 @@
 import com.doumee.core.model.ApiResponse;
 import com.doumee.core.model.PageData;
 import com.doumee.core.model.PageWrap;
+import com.doumee.dao.business.dto.YwElectricalDataSyncDTO;
 import com.doumee.dao.business.model.Device;
 import com.doumee.dao.business.model.YwElectricalData;
 import com.doumee.service.business.YwElectricalDataService;
@@ -14,6 +15,7 @@
 import com.doumee.config.annotation.CloudRequiredPermission;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
+import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 import javax.servlet.http.HttpServletResponse;
@@ -94,7 +96,11 @@
     @ApiOperation("绔嬪嵆鎶勮〃锛堝悓姝ョ涓夋柟鎶勮〃鏁版嵁锛�")
     @PostMapping("/syncAll")
     @CloudRequiredPermission("business:ywelectricaldata:sync")
-    public ApiResponse<String> syncAll(@RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
-        return ApiResponse.success(ywElectricalDataService.syncFromPlatform());
+    public ApiResponse<String> syncAll(@RequestBody(required = false) YwElectricalDataSyncDTO dto,
+                                       @RequestHeader(Constants.HEADER_USER_TOKEN) String token) {
+        if (dto == null || (StringUtils.isBlank(dto.getReadTimeBegin()) && StringUtils.isBlank(dto.getReadTimeEnd()))) {
+            return ApiResponse.success(ywElectricalDataService.syncFromPlatform());
+        }
+        return ApiResponse.success(ywElectricalDataService.syncFromPlatform(dto.getReadTimeBegin(), dto.getReadTimeEnd()));
     }
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalDataSyncDTO.java b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalDataSyncDTO.java
new file mode 100644
index 0000000..34eabee
--- /dev/null
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalDataSyncDTO.java
@@ -0,0 +1,13 @@
+package com.doumee.dao.business.dto;
+
+import lombok.Data;
+
+@Data
+public class YwElectricalDataSyncDTO {
+
+    /** 鎶勮〃寮�濮嬫椂闂� yyyy-MM-dd HH:mm:ss */
+    private String readTimeBegin;
+
+    /** 鎶勮〃缁撴潫鏃堕棿 yyyy-MM-dd HH:mm:ss */
+    private String readTimeEnd;
+}
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java
index 540e88c..c924826 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java
@@ -35,6 +35,9 @@
     /** 鎵嬪姩浠庣涓夋柟骞冲彴鎷夊彇鎶勮〃鏁版嵁鍏ュ簱 */
     String syncMeterDataFromPlatform();
 
+    /** 鎵嬪姩浠庣涓夋柟骞冲彴鎷夊彇鎸囧畾鏃堕棿娈垫妱琛ㄦ暟鎹叆搴� */
+    String syncMeterDataFromPlatform(String readTimeBegin, String readTimeEnd);
+
     void cleanLogBeforeThreeMonths();
 
     void enrichList(List<YwElectrical> list);
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java
index 73a7488..b616aad 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java
@@ -113,4 +113,9 @@
      * 浠庣涓夋柟骞冲彴鍚屾鎶勮〃鏁版嵁
      */
     String syncFromPlatform();
+
+    /**
+     * 浠庣涓夋柟骞冲彴鍚屾鎸囧畾鏃堕棿娈垫妱琛ㄦ暟鎹�
+     */
+    String syncFromPlatform(String readTimeBegin, String readTimeEnd);
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java
index dd64e4c..1d5f75d 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java
@@ -12,4 +12,9 @@
     List<DailyEnergyStatVO> electricalDailyStats();
 
     List<DailyEnergyStatVO> conditionerDailyStats();
+
+    /**
+     * 鎶勮〃鍚屾鍚庯紝鎸夋妱琛ㄦ椂闂存鍓嶅悗鍚勬墿灞曚竴澶╋紝鍒锋柊姣忔棩鐢甸噺/鐢佃垂缁熻
+     */
+    String refreshElectricalDailyStatsForRange(String readTimeBegin, String readTimeEnd);
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
index 8ab9565..2a76435 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java
@@ -64,6 +64,12 @@
     private static final int ELE_READ_TYPE_METER = 3;
     /** queryData 鐢佃〃鐘舵�佽鎯呭姛鑳� ID */
     private static final String QUERY_DATA_FUNCTION_METER_STATUS = "253";
+    /** DataRequest 鍗曟鏈�澶ф潯鏁帮紙鎺ュ彛 limit 涓婇檺 1000锛� */
+    private static final int QUERY_DATA_PAGE_SIZE = 500;
+    /** 鎶勮〃/DataRequest 鏌ヨ鏃堕棿璺ㄥ害涓婇檺锛堝ぉ锛� */
+    private static final int MAX_METER_QUERY_DAYS = 7;
+    /** 鍒嗛〉 total 鍚堢悊涓婇檺锛岄伩鍏嶈鎶婃椂闂存埑褰撲綔鎬绘潯鏁� */
+    private static final int QUERY_DATA_TOTAL_SANITY_MAX = 100_000;
 
     @Autowired
     private YwElectricalMapper ywElectricalMapper;
@@ -829,7 +835,7 @@
     @Override
     public void syncMeterDataScheduled() {
         try {
-            syncMeterDataInternal();
+            syncMeterDataInternal(null, null);
         } catch (Exception e) {
             log.warn("syncMeterDataScheduled failed", e);
         }
@@ -837,8 +843,36 @@
 
     @Override
     public String syncMeterDataFromPlatform() {
-        MeterDataSyncStats stats = syncMeterDataInternal();
+        MeterDataSyncStats stats = syncMeterDataInternal(null, null);
         return "鎶勮〃鍚屾瀹屾垚锛氭柊澧炪��" + stats.addCount + "銆戞潯锛岃烦杩囬噸澶嶃��" + stats.skipCount + "銆戞潯";
+    }
+
+    @Override
+    public String syncMeterDataFromPlatform(String readTimeBegin, String readTimeEnd) {
+        if (StringUtils.isBlank(readTimeBegin) || StringUtils.isBlank(readTimeEnd)) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "璇烽�夋嫨鎶勮〃鏃堕棿娈�");
+        }
+        String startTime = readTimeBegin.trim();
+        String endTime = readTimeEnd.trim();
+        validateManualSyncTimeRange(startTime, endTime);
+        String expandedStart = expandStartByOneDay(startTime);
+        MeterDataSyncStats stats = syncMeterDataInternal(expandedStart, endTime);
+        return "鎶勮〃鍚屾瀹屾垚锛氭柊澧炪��" + stats.addCount + "銆戞潯锛岃烦杩囬噸澶嶃��" + stats.skipCount + "銆戞潯";
+    }
+
+    private String expandStartByOneDay(String startTime) {
+        try {
+            Date start = DateUtil.StringToDate(startTime, "yyyy-MM-dd HH:mm:ss");
+            if (start == null) {
+                return startTime;
+            }
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(start);
+            cal.add(Calendar.DAY_OF_MONTH, -1);
+            return DateUtil.formatDate(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+        } catch (Exception e) {
+            return startTime;
+        }
     }
 
     private static class MeterDataSyncStats {
@@ -846,12 +880,14 @@
         private int skipCount;
     }
 
-    private MeterDataSyncStats syncMeterDataInternal() {
+    private MeterDataSyncStats syncMeterDataInternal(String startTime, String endTime) {
         MeterDataSyncStats stats = new MeterDataSyncStats();
-        String startTime = resolveSyncStartTime();
-        QueryDataRequest param = buildQueryDataRequest(startTime, DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
-        log.info("sync meter data, start_time={}, end_time={}", startTime, param.getEnd_time());
-        List<QueryDataInfoResponse> list = fetchQueryDataList(param);
+        String resolvedStart = StringUtils.isNotBlank(startTime) ? startTime : resolveSyncStartTime();
+        String resolvedEnd = StringUtils.isNotBlank(endTime) ? endTime : DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss");
+        resolvedStart = capQueryStartTime(resolvedStart, resolvedEnd, MAX_METER_QUERY_DAYS);
+        QueryDataRequest param = buildQueryDataRequest(resolvedStart, resolvedEnd);
+        log.info("sync meter data, start_time={}, end_time={}", resolvedStart, param.getEnd_time());
+        List<QueryDataInfoResponse> list = fetchAllQueryDataList(param);
         if (CollectionUtils.isEmpty(list)) {
             return stats;
         }
@@ -875,6 +911,51 @@
         return stats;
     }
 
+    private void validateManualSyncTimeRange(String startTime, String endTime) {
+        if (!startTime.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")
+                || !endTime.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}")) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "鎶勮〃鏃堕棿鏍煎紡涓嶆纭�");
+        }
+        Date start;
+        Date end;
+        try {
+            start = DateUtil.StringToDate(startTime, "yyyy-MM-dd HH:mm:ss");
+            end = DateUtil.StringToDate(endTime, "yyyy-MM-dd HH:mm:ss");
+        } catch (Exception e) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "鎶勮〃鏃堕棿鏍煎紡涓嶆纭�");
+        }
+        if (start == null || end == null || !start.before(end)) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "鎶勮〃寮�濮嬫椂闂村繀椤绘棭浜庣粨鏉熸椂闂�");
+        }
+        long diffMs = end.getTime() - start.getTime();
+        if (diffMs > (long) MAX_METER_QUERY_DAYS * 24 * 60 * 60 * 1000) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "鎶勮〃鏃堕棿娈典笉鑳借秴杩�" + MAX_METER_QUERY_DAYS + "澶�");
+        }
+    }
+
+    private String capQueryStartTime(String startTime, String endTime, int maxDays) {
+        if (StringUtils.isBlank(startTime) || StringUtils.isBlank(endTime)) {
+            return startTime;
+        }
+        try {
+            Date start = DateUtil.StringToDate(startTime, "yyyy-MM-dd HH:mm:ss");
+            Date end = DateUtil.StringToDate(endTime, "yyyy-MM-dd HH:mm:ss");
+            if (start == null || end == null || !start.before(end)) {
+                return startTime;
+            }
+            long maxMs = (long) maxDays * 24 * 60 * 60 * 1000;
+            if (end.getTime() - start.getTime() <= maxMs) {
+                return startTime;
+            }
+            Calendar cal = Calendar.getInstance();
+            cal.setTime(end);
+            cal.add(Calendar.DAY_OF_MONTH, -maxDays);
+            return DateUtil.formatDate(cal.getTime(), "yyyy-MM-dd HH:mm:ss");
+        } catch (Exception e) {
+            return startTime;
+        }
+    }
+
     /** 鍗曡〃鎶勮〃鍚庝粠绗笁鏂规媺鍙栨渶鏂版暟鎹叆搴擄紝杩斿洖鏄惁鏈夋柊璁板綍 */
     private boolean syncMeterDataForElectrical(YwElectrical e) {
         if (e == null || StringUtils.isBlank(e.getAddress())) {
@@ -887,7 +968,7 @@
                 DateUtil.formatDate(new Date(), "yyyy-MM-dd HH:mm:ss"));
         List<QueryDataInfoResponse> list;
         try {
-            list = fetchQueryDataList(param);
+            list = fetchAllQueryDataList(param);
         } catch (BusinessException ex) {
             log.warn("sync meter data for electricalId={} failed: {}", e.getId(), ex.getMessage());
             return false;
@@ -918,17 +999,66 @@
         param.setStart_time(startTime);
         param.setEnd_time(endTime);
         param.setOffset(0);
-        param.setLimit(500);
+        param.setLimit(QUERY_DATA_PAGE_SIZE);
         return param;
     }
 
-    private List<QueryDataInfoResponse> fetchQueryDataList(QueryDataRequest param) {
-        ElectronicDataResponse response = ElectronicToolUtil.queryDataRequest(param);
-        if (!ElectronicToolUtil.isDataApiSuccess(response)) {
-            throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(),
-                    ElectronicToolUtil.dataApiErrorMessage(response, "鎶勮〃鏁版嵁鍚屾澶辫触"));
+    /** 鎸� DataRequest 鍒嗛〉 total 寰幆鎷夊彇鍏ㄩ儴鎶勮〃鏁版嵁 */
+    private List<QueryDataInfoResponse> fetchAllQueryDataList(QueryDataRequest param) {
+        if (param == null) {
+            return Collections.emptyList();
         }
-        return parseQueryDataList(response);
+        int limit = param.getLimit() > 0 ? param.getLimit() : QUERY_DATA_PAGE_SIZE;
+        List<QueryDataInfoResponse> all = new ArrayList<>();
+        int offset = 0;
+        Integer total = null;
+        int pageNo = 0;
+        while (true) {
+            pageNo++;
+            param.setOffset(offset);
+            param.setLimit(limit);
+            ElectronicDataResponse response = ElectronicToolUtil.queryDataRequest(param);
+            if (!ElectronicToolUtil.isDataApiSuccess(response)) {
+                throw new BusinessException(ResponseStatus.SERVER_ERROR.getCode(),
+                        ElectronicToolUtil.dataApiErrorMessage(response, "鎶勮〃鏁版嵁鍚屾澶辫触"));
+            }
+            List<QueryDataInfoResponse> page = parseQueryDataList(response);
+            if (!CollectionUtils.isEmpty(page)) {
+                all.addAll(page);
+            }
+            if (total == null) {
+                total = resolveQueryTotal(response);
+            }
+            log.info("sync meter data page={}, offset={}, pageSize={}, accumulated={}, total={}",
+                    pageNo, offset, page.size(), all.size(), total);
+            if (total != null && total > 0) {
+                offset += limit;
+                if (offset >= total) {
+                    break;
+                }
+                continue;
+            }
+            if (CollectionUtils.isEmpty(page) || page.size() < limit) {
+                break;
+            }
+            offset += limit;
+            if (pageNo >= 200) {
+                log.warn("sync meter data pagination exceeded safety page limit, accumulated={}", all.size());
+                break;
+            }
+        }
+        return all;
+    }
+
+    private Integer resolveQueryTotal(ElectronicDataResponse response) {
+        if (response == null || response.getTotal() == null) {
+            return null;
+        }
+        int total = response.getTotal();
+        if (total <= 0 || total > QUERY_DATA_TOTAL_SANITY_MAX) {
+            return null;
+        }
+        return total;
     }
 
     private List<QueryDataInfoResponse> parseQueryDataList(ElectronicDataResponse response) {
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java
index b05ad19..0f265b1 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java
@@ -15,12 +15,14 @@
 import com.doumee.dao.business.model.YwElectricalRoom;
 import com.doumee.service.business.YwElectricalBizService;
 import com.doumee.service.business.YwElectricalDataService;
+import com.doumee.service.business.YwWorkDeskEnergyService;
 import com.github.yulichang.wrapper.MPJLambdaWrapper;
 import org.apache.commons.lang3.StringUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 
+import java.text.SimpleDateFormat;
 import java.util.List;
 
 /**
@@ -33,6 +35,8 @@
     private YwElectricalDataMapper ywElectricalDataMapper;
     @Autowired
     private YwElectricalBizService ywElectricalBizService;
+    @Autowired
+    private YwWorkDeskEnergyService ywWorkDeskEnergyService;
 
     @Override
     public Integer create(YwElectricalData ywElectricalData) {
@@ -137,13 +141,28 @@
                     .eq(YwElectricalRoom::getRoomId, model.getRoomId());
         }
         if (model.getReadTimeBegin() != null) {
-            queryWrapper.ge(YwElectricalData::getCreateDate, Utils.Date.getStart(model.getReadTimeBegin()));
+            String begin = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+                    .format(Utils.Date.getStart(model.getReadTimeBegin()));
+            queryWrapper.and(w -> w.and(w1 -> w1.isNotNull(YwElectricalData::getAddTime)
+                            .ne(YwElectricalData::getAddTime, "")
+                            .ge(YwElectricalData::getAddTime, begin))
+                    .or(w2 -> w2.and(w3 -> w3.isNull(YwElectricalData::getAddTime)
+                                    .or().eq(YwElectricalData::getAddTime, ""))
+                            .ge(YwElectricalData::getCreateDate, Utils.Date.getStart(model.getReadTimeBegin()))));
         }
         if (model.getReadTimeEnd() != null) {
-            queryWrapper.le(YwElectricalData::getCreateDate, Utils.Date.getEnd(model.getReadTimeEnd()));
+            String end = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
+                    .format(Utils.Date.getEnd(model.getReadTimeEnd()));
+            queryWrapper.and(w -> w.and(w1 -> w1.isNotNull(YwElectricalData::getAddTime)
+                            .ne(YwElectricalData::getAddTime, "")
+                            .le(YwElectricalData::getAddTime, end))
+                    .or(w2 -> w2.and(w3 -> w3.isNull(YwElectricalData::getAddTime)
+                                    .or().eq(YwElectricalData::getAddTime, ""))
+                            .le(YwElectricalData::getCreateDate, Utils.Date.getEnd(model.getReadTimeEnd()))));
         }
 
-        queryWrapper.orderByDesc(YwElectricalData::getCreateDate)
+        queryWrapper.orderByDesc(YwElectricalData::getAddTime)
+                .orderByDesc(YwElectricalData::getCreateDate)
                 .orderByDesc(YwElectricalData::getId);
         IPage<YwElectricalData> result = ywElectricalDataMapper.selectJoinPage(page, YwElectricalData.class, queryWrapper);
         PageData<YwElectricalData> pageData = PageData.from(result);
@@ -161,4 +180,11 @@
     public String syncFromPlatform() {
         return ywElectricalBizService.syncMeterDataFromPlatform();
     }
+
+    @Override
+    public String syncFromPlatform(String readTimeBegin, String readTimeEnd) {
+        String syncMsg = ywElectricalBizService.syncMeterDataFromPlatform(readTimeBegin, readTimeEnd);
+        String statsMsg = ywWorkDeskEnergyService.refreshElectricalDailyStatsForRange(readTimeBegin, readTimeEnd);
+        return syncMsg + "锛�" + statsMsg;
+    }
 }
diff --git a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java
index aa2d11c..5d7812b 100644
--- a/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java
+++ b/server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java
@@ -1,7 +1,10 @@
 package com.doumee.service.business.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.doumee.core.constants.ResponseStatus;
+import com.doumee.core.exception.BusinessException;
 import com.doumee.core.utils.Constants;
+import com.doumee.core.utils.DateUtil;
 import com.doumee.dao.business.YwConditionerUsageMapper;
 import com.doumee.dao.business.YwElectricalDataMapper;
 import com.doumee.dao.business.model.YwConditionerUsage;
@@ -18,6 +21,7 @@
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.HashMap;
@@ -42,13 +46,54 @@
     public List<DailyEnergyStatVO> electricalDailyStats() {
         LocalDate end = LocalDate.now();
         LocalDate start = end.minusDays(DAYS - 1L);
-        LocalDate queryStart = start.minusDays(1L);
+        return buildElectricalDailyStats(start, end);
+    }
+
+    @Override
+    public String refreshElectricalDailyStatsForRange(String readTimeBegin, String readTimeEnd) {
+        if (StringUtils.isBlank(readTimeBegin) || StringUtils.isBlank(readTimeEnd)) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "鎶勮〃鏃堕棿娈典笉鑳戒负绌�");
+        }
+        LocalDate start = parseDateTime(readTimeBegin.trim()).minusDays(1);
+        LocalDate end = parseDateTime(readTimeEnd.trim()).plusDays(1);
+        if (start.isAfter(end)) {
+            throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "鎶勮〃鏃堕棿娈垫棤鏁�");
+        }
+        buildElectricalDailyStats(start, end);
+        return "宸插埛鏂版瘡鏃ョ數閲�/鐢佃垂缁熻锛�" + start.format(DATE_FMT) + " ~ " + end.format(DATE_FMT) + "锛�";
+    }
+
+    @Override
+    public List<DailyEnergyStatVO> conditionerDailyStats() {
+        LocalDate end = LocalDate.now();
+        LocalDate start = end.minusDays(DAYS - 1L);
         Map<String, DailyEnergyStatVO> bucket = initDailyBucket(start, end);
 
-        List<YwElectricalData> rows = ywElectricalDataMapper.selectList(new QueryWrapper<YwElectricalData>().lambda()
-                .eq(YwElectricalData::getIsdeleted, Constants.ZERO)
-                .ge(YwElectricalData::getCreateDate, java.sql.Date.valueOf(queryStart))
-                .le(YwElectricalData::getCreateDate, java.sql.Date.valueOf(end.plusDays(1))));
+        List<YwConditionerUsage> rows = ywConditionerUsageMapper.selectList(new QueryWrapper<YwConditionerUsage>().lambda()
+                .eq(YwConditionerUsage::getIsdeleted, Constants.ZERO)
+                .ge(YwConditionerUsage::getUsageDate, java.sql.Date.valueOf(start))
+                .le(YwConditionerUsage::getUsageDate, java.sql.Date.valueOf(end)));
+
+        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
+        for (YwConditionerUsage row : rows) {
+            if (row.getUsageDate() == null) {
+                continue;
+            }
+            String dayKey = sdf.format(row.getUsageDate());
+            if (!bucket.containsKey(dayKey)) {
+                continue;
+            }
+            DailyEnergyStatVO stat = bucket.get(dayKey);
+            stat.setTotalKwh(stat.getTotalKwh().add(nullToZero(row.getSumDl())));
+            stat.setTotalFee(stat.getTotalFee().add(nullToZero(row.getSumDf())));
+        }
+        return normalizeBucket(bucket);
+    }
+
+    private List<DailyEnergyStatVO> buildElectricalDailyStats(LocalDate start, LocalDate end) {
+        LocalDate queryStart = start.minusDays(1L);
+        Map<String, DailyEnergyStatVO> bucket = initDailyBucket(start, end);
+        List<YwElectricalData> rows = loadElectricalRowsForStats(queryStart, end);
 
         SimpleDateFormat dayFmt = new SimpleDateFormat("yyyy-MM-dd");
         SimpleDateFormat readTimeFmt = new SimpleDateFormat(READ_TIME_PATTERN);
@@ -93,31 +138,36 @@
         return normalizeBucket(bucket);
     }
 
-    @Override
-    public List<DailyEnergyStatVO> conditionerDailyStats() {
-        LocalDate end = LocalDate.now();
-        LocalDate start = end.minusDays(DAYS - 1L);
-        Map<String, DailyEnergyStatVO> bucket = initDailyBucket(start, end);
+    private List<YwElectricalData> loadElectricalRowsForStats(LocalDate queryStart, LocalDate end) {
+        String queryStartStr = queryStart.format(DATE_FMT) + " 00:00:00";
+        String queryEndStr = end.format(DATE_FMT) + " 23:59:59";
+        return ywElectricalDataMapper.selectList(new QueryWrapper<YwElectricalData>().lambda()
+                .eq(YwElectricalData::getIsdeleted, Constants.ZERO)
+                .and(w -> w.and(w1 -> w1.isNotNull(YwElectricalData::getAddTime)
+                                .ne(YwElectricalData::getAddTime, "")
+                                .ge(YwElectricalData::getAddTime, queryStartStr)
+                                .le(YwElectricalData::getAddTime, queryEndStr))
+                        .or(w2 -> w2.and(w3 -> w3.isNull(YwElectricalData::getAddTime)
+                                        .or().eq(YwElectricalData::getAddTime, ""))
+                                .ge(YwElectricalData::getCreateDate, java.sql.Date.valueOf(queryStart))
+                                .le(YwElectricalData::getCreateDate, java.sql.Date.valueOf(end.plusDays(1))))));
+    }
 
-        List<YwConditionerUsage> rows = ywConditionerUsageMapper.selectList(new QueryWrapper<YwConditionerUsage>().lambda()
-                .eq(YwConditionerUsage::getIsdeleted, Constants.ZERO)
-                .ge(YwConditionerUsage::getUsageDate, java.sql.Date.valueOf(start))
-                .le(YwConditionerUsage::getUsageDate, java.sql.Date.valueOf(end)));
-
-        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
-        for (YwConditionerUsage row : rows) {
-            if (row.getUsageDate() == null) {
-                continue;
+    private static LocalDate parseDateTime(String text) {
+        if (text.length() >= 10) {
+            try {
+                return LocalDate.parse(text.substring(0, 10), DATE_FMT);
+            } catch (DateTimeParseException ignored) {
             }
-            String dayKey = sdf.format(row.getUsageDate());
-            if (!bucket.containsKey(dayKey)) {
-                continue;
-            }
-            DailyEnergyStatVO stat = bucket.get(dayKey);
-            stat.setTotalKwh(stat.getTotalKwh().add(nullToZero(row.getSumDl())));
-            stat.setTotalFee(stat.getTotalFee().add(nullToZero(row.getSumDf())));
         }
-        return normalizeBucket(bucket);
+        try {
+            Date date = DateUtil.StringToDate(text, READ_TIME_PATTERN);
+            if (date != null) {
+                return date.toInstant().atZone(java.time.ZoneId.systemDefault()).toLocalDate();
+            }
+        } catch (Exception ignored) {
+        }
+        throw new BusinessException(ResponseStatus.NOT_ALLOWED.getCode(), "鎶勮〃鏃堕棿鏍煎紡涓嶆纭�");
     }
 
     private static void upsertLatestReading(Map<String, Map<String, MeterDayReading>> meterDayLatest,

--
Gitblit v1.9.3