doum
7 天以前 e46bfa3ff94a8a1b4daf37c7fcb79c2fab22a72c
新增智能电表、空调管理
已添加1个文件
已修改8个文件
469 ■■■■ 文件已修改
admin/src/views/business/ywelectricaldata.vue 137 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_admin/src/main/java/com/doumee/cloud/admin/YwElectricalDataCloudController.java 10 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/dao/business/dto/YwElectricalDataSyncDTO.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalBizService.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwElectricalDataService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/YwWorkDeskEnergyService.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalBizServiceImpl.java 160 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwElectricalDataServiceImpl.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
server/visits/dmvisit_service/src/main/java/com/doumee/service/business/impl/YwWorkDeskEnergyServiceImpl.java 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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>
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()));
    }
}
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;
}
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);
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);
}
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);
}
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) {
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;
    }
}
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,