浏览代码

feat: 站点月表增加手动重新同步功能,支持指定月份重新计算覆盖统计数据

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 3 周之前
父节点
当前提交
6753b83eb9

+ 56 - 0
admin-web/src/views/admin/station/stat/index.vue

@@ -67,8 +67,31 @@
           <SvgIcon name="ele-Search"/>
           查询
         </el-button>
+
+        <el-button v-auth="'stationStatMonth.modify'" plain size="default" type="warning" @click="openResyncDialog">
+          <SvgIcon name="ele-Refresh"/>
+          重新同步
+        </el-button>
       </el-form>
 
+      <el-dialog v-model="resyncDialog.visible" title="重新同步月统计数据" width="400px" :close-on-click-modal="false">
+        <el-form label-width="80px">
+          <el-form-item label="选择月份">
+            <el-date-picker
+              v-model="resyncDialog.month"
+              type="month"
+              value-format="YYYY-MM"
+              placeholder="请选择要重新同步的月份"
+              :disabled-date="(date) => date >= new Date(new Date().getFullYear(), new Date().getMonth(), 1)"
+            />
+          </el-form-item>
+        </el-form>
+        <template #footer>
+          <el-button @click="resyncDialog.visible = false">取消</el-button>
+          <el-button type="primary" :loading="resyncDialog.loading" @click="doResync">确定同步</el-button>
+        </template>
+      </el-dialog>
+
       <el-table
           border
           stripe="stripe"
@@ -210,6 +233,39 @@ onBeforeUnmount(() => {
 
 
 //region 方法区
+
+const resyncDialog = reactive({
+  visible: false,
+  month: '',
+  loading: false,
+});
+
+const openResyncDialog = () => {
+  const now = new Date();
+  now.setMonth(now.getMonth() - 1);
+  resyncDialog.month = now.getFullYear() + '-' + String(now.getMonth() + 1).padStart(2, '0');
+  resyncDialog.visible = true;
+};
+
+const doResync = () => {
+  if (!resyncDialog.month) {
+    Msg.message('请选择月份', 'warning');
+    return;
+  }
+  Msg.confirm(`确认重新同步 ${resyncDialog.month} 的站点统计数据吗?此操作将覆盖该月已有数据。`).then(() => {
+    resyncDialog.loading = true;
+    $body('/stat/resyncStationStatMonth?statMonth=' + resyncDialog.month, {}).then((res: any) => {
+      Msg.message(res.msg || '同步完成', 'success');
+      resyncDialog.visible = false;
+      loadData(true);
+    }).catch(() => {
+      Msg.message('同步失败', 'error');
+    }).finally(() => {
+      resyncDialog.loading = false;
+    });
+  });
+};
+
 // 初始化表格数据
 const handleCreateStatements = (statMonth) => {
   $get(`statements/create/${statMonth.id}`).then(()=>{

+ 18 - 1
admin/src/main/java/com/kym/admin/controller/StatController.java

@@ -1,5 +1,6 @@
 package com.kym.admin.controller;
 
+import com.kym.admin.jobs.StationStatJob;
 import com.kym.common.R;
 import com.kym.entity.admin.StationStatMonth;
 import com.kym.entity.admin.queryParams.StatQueryParam;
@@ -19,10 +20,12 @@ public class StatController {
 
     private final ChargeOrderService chargeOrderService;
     private final StationStatMonthService stationStatMonthService;
+    private final StationStatJob stationStatJob;
 
-    public StatController(ChargeOrderService chargeOrderService, StationStatMonthService stationStatMonthService) {
+    public StatController(ChargeOrderService chargeOrderService, StationStatMonthService stationStatMonthService, StationStatJob stationStatJob) {
         this.chargeOrderService = chargeOrderService;
         this.stationStatMonthService = stationStatMonthService;
+        this.stationStatJob = stationStatJob;
     }
 
     /**
@@ -91,4 +94,18 @@ public class StatController {
         return R.success();
     }
 
+    /**
+     * 重新同步指定月份的站点月统计数据
+     * @param statMonth 格式: "yyyy-MM"
+     */
+    @PostMapping("/resyncStationStatMonth")
+    R<?> resyncStationStatMonth(String statMonth) {
+        try {
+            int count = stationStatJob.resyncMonth(statMonth);
+            return R.success("重新同步完成,共更新 " + count + " 条记录");
+        } catch (IllegalArgumentException e) {
+            return R.failed(400, e.getMessage());
+        }
+    }
+
 }

+ 29 - 10
admin/src/main/java/com/kym/admin/jobs/StationStatJob.java

@@ -1,6 +1,5 @@
 package com.kym.admin.jobs;
 
-import cn.hutool.core.date.DateUtil;
 import com.baomidou.dynamic.datasource.toolkit.DynamicDataSourceContextHolder;
 import com.kym.common.utils.CommUtil;
 import com.kym.entity.admin.ConnectorInfo;
@@ -19,6 +18,7 @@ import org.springframework.stereotype.Component;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.LocalTime;
+import java.time.YearMonth;
 import java.time.format.DateTimeFormatter;
 import java.time.temporal.TemporalAdjusters;
 import java.util.*;
@@ -61,8 +61,8 @@ public class StationStatJob {
         var startTime = LocalDateTime.of(statDay.toLocalDate(), LocalTime.MIN);
         var endTime = LocalDateTime.of(statDay.toLocalDate(), LocalTime.MAX);
         var chargeOrderList = getChargeOrders(startTime, endTime);
-        dayService.replaceBatch((Collection<StationStatDay>) getStationStat(chargeOrderList, true));
-        dayService.saveBatch((Collection<StationStatDay>) getStationStat(chargeOrderList, true));
+        dayService.replaceBatch((Collection<StationStatDay>) getStationStat(chargeOrderList, true, LocalDate.now().minusDays(1)));
+        dayService.saveBatch((Collection<StationStatDay>) getStationStat(chargeOrderList, true, LocalDate.now().minusDays(1)));
         log.info("执行站点日统计定时任务-结束");
     }
 
@@ -77,11 +77,30 @@ public class StationStatJob {
         var startTime = statMonth.with(TemporalAdjusters.firstDayOfMonth()).with(LocalTime.MIN);
         var endTime = statMonth.with(TemporalAdjusters.lastDayOfMonth()).with(LocalTime.MAX);
         var chargeOrderList = getChargeOrders(startTime, endTime);
-        monthService.replaceBatch((Collection<StationStatMonth>) getStationStat(chargeOrderList, false));
-//        monthService.saveBatch((Collection<StationStatMonth>) getStationStat(chargeOrderList, false));
+        monthService.replaceBatch((Collection<StationStatMonth>) getStationStat(chargeOrderList, false, LocalDate.now().minusMonths(1)));
+//        monthService.saveBatch((Collection<StationStatMonth>) getStationStat(chargeOrderList, false, LocalDate.now().minusMonths(1)));
         log.info("执行站点月统计定时任务-结束");
     }
 
+    /**
+     * 手动重新同步指定月份的站点统计数据
+     * @param yearMonth 格式: "yyyy-MM"
+     * @return 生成的记录数
+     */
+    public int resyncMonth(String yearMonth) {
+        var ym = YearMonth.parse(yearMonth, DateTimeFormatter.ofPattern("yyyy-MM"));
+        if (!ym.isBefore(YearMonth.now())) {
+            throw new IllegalArgumentException("只允许同步本月之前的数据");
+        }
+        var startTime = ym.atDay(1).atTime(LocalTime.MIN);
+        var endTime = ym.atEndOfMonth().atTime(LocalTime.MAX);
+        var targetDate = ym.atDay(1);
+        var chargeOrderList = getChargeOrders(startTime, endTime);
+        var stats = (Collection<StationStatMonth>) getStationStat(chargeOrderList, false, targetDate);
+        monthService.replaceBatch(stats);
+        return stats.size();
+    }
+
     // 只执行一次
 //    @PostConstruct
     private void init() {
@@ -107,7 +126,7 @@ public class StationStatJob {
         log.info("执行站点初始化定时任务-结束");
     }
 
-    private List<ChargeOrder> getChargeOrders(LocalDateTime startTime, LocalDateTime endTime) {
+    List<ChargeOrder> getChargeOrders(LocalDateTime startTime, LocalDateTime endTime) {
         // 通过charge_app.t_charge_order表统计start_time为前一天且充电金额大于0的有效充电订单
         DynamicDataSourceContextHolder.push("db-miniapp");
         var res = chargeOrderService.lambdaQuery()
@@ -119,7 +138,7 @@ public class StationStatJob {
         return res;
     }
 
-    private Collection<?> getStationStat(List<ChargeOrder> chargeOrderList, boolean isDayStat) {
+    Collection<?> getStationStat(List<ChargeOrder> chargeOrderList, boolean isDayStat, LocalDate targetDate) {
         // 统计每个站点的connector_id数量
         var stationConnectorCountMap = connectorInfoService.list().stream().collect(Collectors.groupingBy(ConnectorInfo::getStationId, Collectors.counting()));
         // 将chargeOrderList按照stationId分组
@@ -145,7 +164,7 @@ public class StationStatJob {
                     return isDayStat
                             ? new StationStatDay()
                             .setStationId(entry.getKey())
-                            .setStatDay(DateUtil.format(LocalDateTime.now().minusDays(1), "yyyy-MM-dd"))
+                            .setStatDay(targetDate.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")))
                             .setChargeUsers(chargeUsers)
                             .setValidOrders(chargeOrders.size())
                             .setTotalMoney(totalMoney)
@@ -162,7 +181,7 @@ public class StationStatJob {
 
                             : new StationStatMonth()
                             .setStationId(entry.getKey())
-                            .setStatMonth(DateUtil.format(LocalDateTime.now().minusMonths(1), "yyyy-MM"))
+                            .setStatMonth(targetDate.format(DateTimeFormatter.ofPattern("yyyy-MM")))
                             .setChargeUsers(chargeUsers)
                             .setValidOrders(chargeOrders.size())
                             .setTotalMoney(totalMoney)
@@ -173,7 +192,7 @@ public class StationStatJob {
                             .setTotalPower(totalPower)
                             .setAvgOrderElec(avgPower)
                             .setAvgOrderMoney((int) avgOrderMoney)
-                            .setAvgConnectorElec(totalPower / (stationConnectorCountMap.get(entry.getKey()) * (LocalDate.now().minusMonths(1).lengthOfMonth())));
+                            .setAvgConnectorElec(totalPower / (stationConnectorCountMap.get(entry.getKey()) * (targetDate.lengthOfMonth())));
                 })).values();
     }
 }