Pārlūkot izejas kodu

feat: 日统计重新同步支持选定时间范围批量同步(最多30天)

- 后端新增 POST /daily-stat/batch-sync 接口,接受 startDate/endDate
- 前端日期选择器改为 daterange 类型,默认选中昨天
- 限制最多30天、禁止选择今天及以后
- 同步按钮增加 loading 状态

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 nedēļu atpakaļ
vecāks
revīzija
8b09c84bf4

+ 5 - 0
admin-web-new/src/api/stat.ts

@@ -35,6 +35,11 @@ export const syncDailyStat = (data: { statDate: string }) => {
   return http.request<any>("post", "/daily-stat/sync", { data });
 };
 
+/** 批量同步日统计 */
+export const syncDailyStatBatch = (data: { startDate: string; endDate: string }) => {
+  return http.request<any>("post", "/daily-stat/batch-sync", { data });
+};
+
 /** 月统计列表 */
 export const getMonthStatList = (data?: object) => {
   return http.request<any>("post", "/month-stat/list", { data });

+ 36 - 15
admin-web-new/src/views/admin/statistics/daily-stat.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import { reactive, onMounted, ref, nextTick } from "vue";
-import { getDailyStatList, syncDailyStat } from "@/api/stat";
+import { getDailyStatList, syncDailyStatBatch } from "@/api/stat";
 import { getStationList } from "@/api/station";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import { ElMessage, ElMessageBox } from "element-plus";
@@ -10,7 +10,8 @@ defineOptions({ name: "AdminDailyStat" });
 const queryRef = ref();
 
 const state = reactive({
-  syncDate: "",
+  syncDateRange: [] as string[],
+  syncing: false,
   formQuery: {
     stationId: "",
     startDate: "",
@@ -47,7 +48,8 @@ const state = reactive({
 onMounted(() => {
   const yesterday = new Date();
   yesterday.setDate(yesterday.getDate() - 1);
-  state.syncDate = yesterday.toISOString().split("T")[0];
+  const yesterdayStr = yesterday.toISOString().split("T")[0];
+  state.syncDateRange = [yesterdayStr, yesterdayStr];
 
   loadStations();
   loadData();
@@ -105,26 +107,42 @@ const handleReset = () => {
   loadData(true);
 };
 
+const disabledSyncDate = (date: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  return date.getTime() >= today.getTime();
+};
+
 const handleSyncDailyStat = () => {
-  if (!state.syncDate) {
-    ElMessage.warning("请选择同步日期");
+  if (!state.syncDateRange || state.syncDateRange.length !== 2) {
+    ElMessage.warning("请选择同步日期范围");
+    return;
+  }
+  const [startDate, endDate] = state.syncDateRange;
+  const days = Math.floor((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000) + 1;
+  if (days > 30) {
+    ElMessage.warning("同步日期范围不能超过30天");
     return;
   }
+  const rangeLabel = startDate === endDate ? startDate : `${startDate} ~ ${endDate}`;
   ElMessageBox.confirm(
-    `确认重新同步 ${state.syncDate} 的统计数据吗?将同步所有站点的数据。`,
-    "重新同步日统计",
+    `确认重新同步 ${rangeLabel}(共${days}天)的统计数据吗?将同步所有站点的数据。`,
+    "批量同步日统计",
     { confirmButtonText: "确定", cancelButtonText: "取消", type: "warning" }
   ).then(() => {
-    const loading = ElMessage({ message: "同步中...", type: "info", duration: 0 });
-    syncDailyStat({ statDate: state.syncDate })
+    state.syncing = true;
+    const loading = ElMessage({ message: `批量同步中(共${days}天)...`, type: "info", duration: 0 });
+    syncDailyStatBatch({ startDate, endDate })
       .then(() => {
         loading.close();
-        ElMessage.success("同步完成");
+        state.syncing = false;
+        ElMessage.success(`同步完成(共${days}天)`);
         loadData(true);
       })
       .catch(() => {
         loading.close();
-        ElMessage.error("同步失败");
+        state.syncing = false;
+        ElMessage.error("批量同步失败");
       });
   }).catch(() => {});
 };
@@ -194,15 +212,18 @@ const moneyFields = ["totalIncome", "totalRefund", "crossIncome", "crossExpend",
         </el-form-item>
         <el-form-item>
           <el-date-picker
-            v-model="state.syncDate"
-            type="date"
-            placeholder="同步日期"
+            v-model="state.syncDateRange"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
             value-format="YYYY-MM-DD"
-            style="width: 200px"
+            :disabled-date="disabledSyncDate"
+            style="width: 260px"
           />
           <el-button
             type="warning"
             :icon="useRenderIcon('ri/loop-right-line')"
+            :loading="state.syncing"
             @click="handleSyncDailyStat"
             style="margin-left: 8px"
           >

+ 37 - 13
admin-web/src/views/admin/statistics/daily-stat.vue

@@ -59,13 +59,15 @@
         </el-button>
 
         <el-date-picker
-            v-model="state.syncDate"
-            type="date"
-            placeholder="同步日期"
+            v-model="state.syncDateRange"
+            type="daterange"
+            start-placeholder="开始日期"
+            end-placeholder="结束日期"
             value-format="YYYY-MM-DD"
-            class="wd180 ml10"/>
+            :disabled-date="disabledSyncDate"
+            class="wd260 ml10"/>
 
-        <el-button class="ml10" plain size="default" type="warning" @click="handleSyncDailyStat">
+        <el-button class="ml10" plain size="default" type="warning" @click="handleSyncDailyStat" :loading="state.syncing">
           <SvgIcon name="ele-RefreshRight"/>
           重新同步
         </el-button>
@@ -121,7 +123,8 @@ import ExtSelect from "/@/components/form/ExtSelect.vue";
 const queryRef = ref();
 
 const state = reactive({
-  syncDate: '',
+  syncDateRange: [] as string[],
+  syncing: false,
   formQuery: {
     stationId: '',
     startDate: '',
@@ -158,7 +161,8 @@ onMounted(() => {
   // 同步日期默认昨天
   const yesterday = new Date();
   yesterday.setDate(yesterday.getDate() - 1);
-  state.syncDate = yesterday.toISOString().split('T')[0];
+  const yesterdayStr = yesterday.toISOString().split('T')[0];
+  state.syncDateRange = [yesterdayStr, yesterdayStr];
 
   loadData();
 
@@ -185,19 +189,39 @@ const loadData = (refresh: boolean = false) => {
   })
 };
 
+const disabledSyncDate = (date: Date) => {
+  const today = new Date();
+  today.setHours(0, 0, 0, 0);
+  // 不允许选择今天及以后
+  return date.getTime() >= today.getTime();
+};
+
 const handleSyncDailyStat = () => {
-  if (!state.syncDate) {
-    Msg.message('请选择同步日期', 'warning');
+  if (!state.syncDateRange || state.syncDateRange.length !== 2) {
+    Msg.message('请选择同步日期范围', 'warning');
+    return;
+  }
+  const [startDate, endDate] = state.syncDateRange;
+  const days = Math.floor((new Date(endDate).getTime() - new Date(startDate).getTime()) / 86400000) + 1;
+  if (days > 30) {
+    Msg.message('同步日期范围不能超过30天', 'warning');
     return;
   }
-  Msg.confirm(`确认重新同步 ${state.syncDate} 的统计数据吗?将同步所有站点的数据。`, '重新同步日统计').then(() => {
-    Msg.showLoading('同步中...');
-    $body(`/daily-stat/sync`, {statDate: state.syncDate}).then(() => {
-      Msg.message("同步完成");
+  const rangeLabel = startDate === endDate ? startDate : `${startDate} ~ ${endDate}`;
+  Msg.confirm(
+    `确认重新同步 ${rangeLabel}(共${days}天)的统计数据吗?将同步所有站点的数据。`,
+    '批量同步日统计'
+  ).then(() => {
+    state.syncing = true;
+    Msg.showLoading(`批量同步中(共${days}天)...`);
+    $body(`/daily-stat/batch-sync`, { startDate, endDate }).then(() => {
+      Msg.message(`同步完成(共${days}天)`);
       Msg.hideLoading();
+      state.syncing = false;
       loadData(true);
     }).catch(e => {
       Msg.hideLoading();
+      state.syncing = false;
     })
   })
 };

+ 10 - 0
car-wash-admin/src/main/java/com/kym/admin/controller/DailyStatController.java

@@ -39,4 +39,14 @@ public class DailyStatController {
         dailyStatService.syncDailyStat(statDate);
         return R.success();
     }
+
+    @SaCheckPermission("dailyStat.sync")
+    @SysLog("批量同步日统计")
+    @PostMapping("/batch-sync")
+    public R<?> batchSync(@RequestBody Map<String, String> body) {
+        String startDate = body.get("startDate");
+        String endDate = body.get("endDate");
+        dailyStatService.syncDailyStatBatch(startDate, endDate);
+        return R.success();
+    }
 }

+ 2 - 0
car-wash-service/src/main/java/com/kym/service/DailyStatService.java

@@ -17,4 +17,6 @@ public interface DailyStatService extends MyBaseService<DailyStat> {
     PageBean<DailyStatVo> listDailyStat(DailyStatQueryParam params);
 
     void syncDailyStat(String statDate);
+
+    void syncDailyStatBatch(String startDate, String endDate);
 }

+ 18 - 0
car-wash-service/src/main/java/com/kym/service/impl/DailyStatServiceImpl.java

@@ -169,4 +169,22 @@ public class DailyStatServiceImpl extends MyBaseServiceImpl<DailyStatMapper, Dai
         }
         log.info("日统计同步完成: {}, 站点数: {}", statDate, statList.size());
     }
+
+    @Override
+    public void syncDailyStatBatch(String startDate, String endDate) {
+        LocalDate start = LocalDate.parse(startDate, DateTimeFormatter.ISO_LOCAL_DATE);
+        LocalDate end = LocalDate.parse(endDate, DateTimeFormatter.ISO_LOCAL_DATE);
+        if (end.isBefore(start)) {
+            throw new IllegalArgumentException("结束日期不能早于开始日期");
+        }
+        long days = start.until(end).getDays() + 1;
+        if (days > 30) {
+            throw new IllegalArgumentException("批量同步最多支持30天");
+        }
+        log.info("批量同步日统计: {} ~ {} (共{}天)", startDate, endDate, days);
+        for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) {
+            syncDailyStat(d.format(DateTimeFormatter.ISO_LOCAL_DATE));
+        }
+        log.info("批量同步日统计完成: {} ~ {}", startDate, endDate);
+    }
 }