Procházet zdrojové kódy

智能柜项目提交

skyline před 2 měsíci
rodič
revize
66898c770e
35 změnil soubory, kde provedl 3450 přidání a 224 odebrání
  1. 91 0
      haha-admin-web/src/api/timedDiscount.ts
  2. 44 0
      haha-admin-web/src/components/AmapWithSearch/src/index.vue
  3. 9 0
      haha-admin-web/src/router/modules/marketing.ts
  4. 4 4
      haha-admin-web/src/router/modules/operation.ts
  5. 286 0
      haha-admin-web/src/views/distribution/index.vue
  6. 0 220
      haha-admin-web/src/views/map-test/index.vue
  7. 267 0
      haha-admin-web/src/views/timed-discount/index.vue
  8. 785 0
      haha-admin-web/src/views/timed-discount/utils/hook.tsx
  9. 140 0
      haha-admin-web/src/views/timed-discount/utils/types.ts
  10. 5 0
      haha-admin-web/vite.config.ts
  11. 147 0
      haha-admin/src/main/java/com/haha/admin/controller/TimedDiscountController.java
  12. 48 0
      haha-admin/src/main/java/com/haha/admin/task/TimedDiscountTask.java
  13. 150 0
      haha-admin/src/main/resources/sql/timed_discount.sql
  14. 60 0
      haha-common/src/main/java/com/haha/common/enums/DiscountTypeEnum.java
  15. 60 0
      haha-common/src/main/java/com/haha/common/enums/ExecuteStatusEnum.java
  16. 59 0
      haha-common/src/main/java/com/haha/common/enums/NightDiscountStatusEnum.java
  17. 60 0
      haha-common/src/main/java/com/haha/common/enums/RestoreStatusEnum.java
  18. 73 0
      haha-entity/src/main/java/com/haha/entity/PriceAdjustmentLog.java
  19. 91 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountActivity.java
  20. 24 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountProduct.java
  21. 63 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountRecord.java
  22. 24 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountShop.java
  23. 56 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountStatistics.java
  24. 45 0
      haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountCreateDTO.java
  25. 15 0
      haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountQueryDTO.java
  26. 23 0
      haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountRecordQueryDTO.java
  27. 47 0
      haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountUpdateDTO.java
  28. 9 0
      haha-mapper/src/main/java/com/haha/mapper/PriceAdjustmentLogMapper.java
  29. 9 0
      haha-mapper/src/main/java/com/haha/mapper/TimedDiscountActivityMapper.java
  30. 9 0
      haha-mapper/src/main/java/com/haha/mapper/TimedDiscountProductMapper.java
  31. 9 0
      haha-mapper/src/main/java/com/haha/mapper/TimedDiscountRecordMapper.java
  32. 9 0
      haha-mapper/src/main/java/com/haha/mapper/TimedDiscountShopMapper.java
  33. 9 0
      haha-mapper/src/main/java/com/haha/mapper/TimedDiscountStatisticsMapper.java
  34. 54 0
      haha-service/src/main/java/com/haha/service/TimedDiscountService.java
  35. 666 0
      haha-service/src/main/java/com/haha/service/impl/TimedDiscountServiceImpl.java

+ 91 - 0
haha-admin-web/src/api/timedDiscount.ts

@@ -0,0 +1,91 @@
+import { http } from "@/utils/http";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+
+export const getTimedDiscountList = (params?: {
+  page?: number;
+  pageSize?: number;
+  activityName?: string;
+  status?: number;
+  discountType?: number;
+}) => {
+  return http.request<ResultTable>("get", "/timed-discount/list", { params });
+};
+
+export const getTimedDiscountById = (id: number) => {
+  return http.request<Result>("get", `/timed-discount/${id}`);
+};
+
+export const createTimedDiscount = (data: any) => {
+  return http.request<Result>("post", "/timed-discount", { data });
+};
+
+export const updateTimedDiscount = (id: number, data: any) => {
+  return http.request<Result>("put", `/timed-discount/${id}`, { data });
+};
+
+export const updateTimedDiscountStatus = (id: number, status: number) => {
+  return http.request<Result>("put", `/timed-discount/${id}/status`, { params: { status } });
+};
+
+export const deleteTimedDiscount = (id: number) => {
+  return http.request<Result>("delete", `/timed-discount/${id}`);
+};
+
+export const executeTimedDiscount = (id: number) => {
+  return http.request<Result>("post", `/timed-discount/${id}/execute`);
+};
+
+export const getTimedDiscountRecords = (params?: {
+  page?: number;
+  pageSize?: number;
+  activityId?: number;
+  executeDateStart?: string;
+  executeDateEnd?: string;
+  shopId?: number;
+  deviceId?: string;
+  status?: number;
+}) => {
+  return http.request<ResultTable>("get", "/timed-discount/records", { params });
+};
+
+export const getTimedDiscountRecordById = (recordId: number) => {
+  return http.request<Result>("get", `/timed-discount/records/${recordId}`);
+};
+
+export const getPriceAdjustmentLogs = (recordId: number, params?: {
+  page?: number;
+  pageSize?: number;
+}) => {
+  return http.request<ResultTable>("get", `/timed-discount/records/${recordId}/logs`, { params });
+};
+
+export const getTimedDiscountStatistics = (id: number) => {
+  return http.request<Result>("get", `/timed-discount/${id}/statistics`);
+};
+
+export const getTimedDiscountDailyStatistics = (id: number, params?: {
+  page?: number;
+  pageSize?: number;
+}) => {
+  return http.request<ResultTable>("get", `/timed-discount/${id}/statistics/daily`, { params });
+};
+
+export const getEnabledTimedDiscounts = () => {
+  return http.request<Result>("get", "/timed-discount/enabled");
+};

+ 44 - 0
haha-admin-web/src/components/AmapWithSearch/src/index.vue

@@ -84,6 +84,49 @@ const addMarker = (lng: number, lat: number) => {
   map.setCenter([lng, lat]);
 };
 
+// 添加自定义标记(支持点击事件和自定义颜色)
+const addCustomMarker = (lng: number, lat: number, title: string, onClick?: () => void, iconColor: string = 'red') => {
+  if (!map || !AMap) {
+    console.error('[addCustomMarker] map or AMap not initialized');
+    return null;
+  }
+
+  // 创建标记点
+  const customMarker = new AMap.Marker({
+    position: [lng, lat],
+    title: title,
+    cursor: "pointer", // 设置鼠标样式为 pointer
+    animation: "AMAP_ANIMATION_DROP"
+  });
+
+  // 设置红色图标(使用高德地图官方图标)
+  if (iconColor === 'red') {
+    try {
+      const icon = new AMap.Icon({
+        image: 'https://webapi.amap.com/theme/v1.3/markers/n_red_b.png',
+        size: new AMap.Size(25, 34),
+        imageSize: new AMap.Size(25, 34),
+        anchor: 'bottom-center'
+      });
+      customMarker.setIcon(icon);
+      console.log('[addCustomMarker] 红色图标设置成功');
+    } catch (error) {
+      console.error('[addCustomMarker] 设置红色图标失败:', error);
+    }
+  }
+
+  // 添加点击事件
+  if (onClick) {
+    customMarker.on("click", onClick);
+  }
+
+  // 添加到地图
+  map.add(customMarker);
+  
+  console.log('[addCustomMarker] 标记点添加成功:', { lng, lat, title });
+  return customMarker;
+};
+
 const reloadMap = () => {
   if (map) {
     map.destroy();
@@ -442,6 +485,7 @@ onBeforeUnmount(() => {
 defineExpose({
   reloadMap,
   addMarker,
+  addCustomMarker, // 暴露添加自定义标记的方法
   searchAddress // 暴露搜索方法
 });
 </script>

+ 9 - 0
haha-admin-web/src/router/modules/marketing.ts

@@ -25,6 +25,15 @@ export default {
         title: "优惠券管理"
       }
     },
+    {
+      path: "/marketing/timed-discount",
+      name: "TimedDiscount",
+      component: () => import("@/views/timed-discount/index.vue"),
+      meta: {
+        icon: "ri:discount-percent-line",
+        title: "定时折扣"
+      }
+    },
     {
       path: "/marketing/statistics",
       name: "MarketingStatistics",

+ 4 - 4
haha-admin-web/src/router/modules/operation.ts

@@ -26,12 +26,12 @@ export default {
       }
     },
     {
-      path: "/map-test",
-      name: "MapTest",
-      component: () => import("@/views/map-test/index.vue"),
+      path: "/distribution",
+      name: "DistributionMap",
+      component: () => import("@/views/distribution/index.vue"),
       meta: {
         icon: "ri:map-pin-line",
-        title: "地图功能测试"
+        title: "门店分布地图"
       }
     }
   ]

+ 286 - 0
haha-admin-web/src/views/distribution/index.vue

@@ -0,0 +1,286 @@
+<template>
+  <div class="shop-distribution-page">
+    <!-- 页面头部 -->
+    <div class="page-header">
+      <div class="header-left">
+        <h2 class="page-title">🗺️ 门店分布地图</h2>
+        <el-tag type="success" effect="dark" size="large">
+          已显示 {{ shopList.length }} 个门店
+        </el-tag>
+      </div>
+      <div class="header-right">
+        <el-input
+          v-model="searchAddress"
+          placeholder="请输入地址搜索"
+          clearable
+          style="width: 300px"
+          @keyup.enter="handleAddressSearch"
+        >
+          <template #append>
+            <el-button @click="handleAddressSearch">
+              <el-icon><Search /></el-icon>
+              搜索
+            </el-button>
+          </template>
+        </el-input>
+      </div>
+    </div>
+
+    <!-- 地图容器 - 占满剩余空间 -->
+    <div class="map-container">
+      <AmapWithSearch
+        ref="mapComponentRef"
+        :longitude="centerLongitude"
+        :latitude="centerLatitude"
+        height="calc(100vh - 200px)"
+        :zoom="12"
+        :enable-click-mark="false"
+        :show-search="false"
+      />
+      
+      <!-- 门店信息浮层 -->
+      <div v-if="selectedShop" class="shop-info-panel" :style="infoPanelStyle">
+        <div class="panel-header">
+          <h3>{{ selectedShop.name }}</h3>
+          <el-button type="info" size="small" circle @click="closeShopInfo">✕</el-button>
+        </div>
+        <div class="panel-content">
+          <p><strong>📍 地址:</strong>{{ selectedShop.province }}{{ selectedShop.city }}{{ selectedShop.district }}{{ selectedShop.address }}</p>
+          <p><strong>🔢 坐标:</strong>经度 {{ selectedShop.longitude }}, 纬度 {{ selectedShop.latitude }}</p>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, computed, onMounted, nextTick } from 'vue';
+import { AmapWithSearch } from '@/components/AmapWithSearch';
+import { getShopList } from '@/api/shop';
+import { Search } from '@element-plus/icons-vue';
+
+// 高德地图全局对象
+let AMap: any = null;
+
+// 地图组件引用
+const mapComponentRef = ref<any>(null);
+
+// 门店数据
+const shopList = ref<any[]>([]);
+const loading = ref(false);
+const searchAddress = ref('');
+
+// 地图中心点(默认深圳)
+const centerLongitude = ref(114.057868);
+const centerLatitude = ref(22.677079);
+
+// 当前选中的门店
+const selectedShop = ref<any>(null);
+// 标记点位置(用于计算信息面板位置)
+const markerPosition = ref<{ lng: number; lat: number } | null>(null);
+
+// 加载门店列表并在地图上标记
+const loadShopList = async () => {
+  loading.value = true;
+  try {
+    const res = await getShopList({ page: 1, pageSize: 100 });
+    console.log('门店列表 API 响应:', JSON.stringify(res));
+    
+    if (res.code === 200 || res.code === 0) {
+      shopList.value = res.data?.list || [];
+      console.log('加载门店列表成功,数量:', shopList.value.length);
+      
+      // 不立即添加标记,等待用户手动触发或地图组件准备好后再添加
+      if (shopList.value.length > 0 && shopList.value[0].longitude && shopList.value[0].latitude) {
+        centerLongitude.value = Number(shopList.value[0].longitude);
+        centerLatitude.value = Number(shopList.value[0].latitude);
+      }
+    } else {
+      console.warn('门店列表 API 返回异常 code:', res.code, 'message:', res.message);
+    }
+  } catch (error) {
+    console.error('加载门店列表失败:', error);
+  } finally {
+    loading.value = false;
+  }
+};
+
+// 获取 AMap 实例(从全局 window 对象)
+const getAMapInstance = () => {
+  if (!AMap && (window as any).AMap) {
+    AMap = (window as any).AMap;
+  }
+  return AMap;
+};
+
+// 在地图上添加所有门店的标记
+const addShopMarkers = async () => {
+  if (!mapComponentRef.value) {
+    console.error('地图组件未初始化');
+    return;
+  }
+
+  // 等待地图实例初始化
+  await nextTick();
+
+  console.log('[addShopMarkers] 开始添加门店标记...');
+  
+  const mapInstance = mapComponentRef.value;
+  
+  shopList.value.forEach((shop, index) => {
+    if (shop.longitude && shop.latitude) {
+      const lng = Number(shop.longitude);
+      const lat = Number(shop.latitude);
+      
+      console.log(`添加门店标记 ${index + 1}:`, shop.name, lng, lat);
+      
+      // 使用组件暴露的 addCustomMarker 方法添加带点击事件的红色标记
+      if (typeof mapInstance.addCustomMarker === 'function') {
+        const result = mapInstance.addCustomMarker(lng, lat, shop.name, () => {
+          console.log('点击门店标记:', shop.name);
+          // 设置选中的门店并计算面板位置
+          selectedShop.value = shop;
+          markerPosition.value = { lng, lat };
+        }, 'red'); // 使用红色标记
+        
+        if (result) {
+          console.log(`[addShopMarkers] 标记点 ${shop.name} 添加成功`);
+        } else {
+          console.error(`[addShopMarkers] 标记点 ${shop.name} 添加失败`);
+        }
+      } else {
+        console.error('[addShopMarkers] addCustomMarker 方法不存在');
+      }
+    } else {
+      console.warn(`门店 ${shop.name} 没有有效的经纬度`);
+    }
+  });
+  
+  console.log('[addShopMarkers] 门店标记添加完成');
+};
+
+// 关闭门店信息面板
+const closeShopInfo = () => {
+  selectedShop.value = null;
+  markerPosition.value = null;
+};
+
+// 处理地址搜索
+const handleAddressSearch = async () => {
+  if (!searchAddress.value.trim()) {
+    return;
+  }
+  
+  if (mapComponentRef.value && typeof mapComponentRef.value.searchAddress === 'function') {
+    await mapComponentRef.value.searchAddress(searchAddress.value);
+  }
+};
+
+// 计算信息面板样式(显示在标记点右上方)
+const infoPanelStyle = computed(() => {
+  // 简单方案:始终显示在地图容器的右上角
+  // 这样更稳定,不受地图缩放和移动影响
+  return {
+    position: 'absolute' as const,
+    top: '20px',
+    right: '20px',
+    maxWidth: '320px',
+    zIndex: 1000
+  };
+});
+
+// 初始化加载门店列表
+onMounted(() => {
+  loadShopList();
+  // 等待地图组件初始化完成后再添加标记
+  setTimeout(() => {
+    addShopMarkers();
+  }, 1000); // 延迟 1 秒确保地图已初始化
+});
+</script>
+
+<style scoped>
+.shop-distribution-page {
+  display: flex;
+  flex-direction: column;
+  height: 100vh; /* 占满整个视口高度 */
+  overflow: hidden;
+}
+
+.page-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 16px 20px;
+  background: white;
+  border-bottom: 1px solid #e4e7ed;
+  flex-shrink: 0; /* 不压缩头部 */
+}
+
+.header-left {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+}
+
+.page-title {
+  margin: 0;
+  font-size: 20px;
+  color: #303133;
+  font-weight: 600;
+}
+
+.map-container {
+  flex: 1; /* 占满剩余空间 */
+  position: relative;
+  background: white;
+  overflow: hidden;
+  min-height: 500px; /* 确保最小高度 */
+}
+
+.shop-info-panel {
+  position: absolute;
+  top: 20px;
+  right: 20px;
+  background: white;
+  border-radius: 8px;
+  box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
+  z-index: 1000;
+  min-width: 300px;
+  max-width: 400px;
+  border: 1px solid #e4e7ed;
+}
+
+.panel-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 12px 16px;
+  border-bottom: 1px solid #e4e7ed;
+  background: #f5f7fa;
+  border-radius: 8px 8px 0 0;
+}
+
+.panel-header h3 {
+  margin: 0;
+  font-size: 16px;
+  color: #303133;
+  font-weight: 600;
+}
+
+.panel-content {
+  padding: 16px;
+}
+
+.panel-content p {
+  margin: 8px 0;
+  font-size: 13px;
+  color: #606266;
+  line-height: 1.6;
+}
+
+.panel-content strong {
+  color: #303133;
+  font-weight: 600;
+}
+</style>

+ 0 - 220
haha-admin-web/src/views/map-test/index.vue

@@ -1,220 +0,0 @@
-<template>
-  <div class="map-test-page">
-    <h2>高德地图测试页面</h2>
-    
-    <!-- 基础地图测试 -->
-    <div class="test-section">
-      <h3>基础地图显示测试</h3>
-      <Amap 
-        :longitude="116.397428" 
-        :latitude="39.90923" 
-        height="400px" 
-        :zoom="15"
-        :enable-click-mark="false"
-      />
-    </div>
-
-    <!-- 可交互地图测试 -->
-    <div class="test-section">
-      <h3>可交互地图测试(点击选择位置)</h3>
-      <Amap 
-        v-model:longitude="testLongitude"
-        v-model:latitude="testLatitude"
-        height="400px" 
-        :zoom="15"
-        :enable-click-mark="true"
-        @markerMoved="handleMarkerMoved"
-      />
-      <div class="coordinates-info">
-        <p>当前坐标:</p>
-        <p>经度: {{ testLongitude }}</p>
-        <p>纬度: {{ testLatitude }}</p>
-      </div>
-    </div>
-
-    <!-- 错误处理测试 -->
-    <div class="test-section">
-      <h3>错误处理测试</h3>
-      <Amap 
-        :longitude="0" 
-        :latitude="0" 
-        height="300px" 
-        :zoom="15"
-        :enable-click-mark="false"
-      />
-    </div>
-
-    <!-- 门店定位测试 -->
-    <div class="test-section">
-      <h3>门店定位测试(模拟点击查看)</h3>
-      <div class="shop-selector">
-        <el-select v-model="selectedShopId" placeholder="请选择门店">
-          <el-option
-            v-for="shop in shopList"
-            :key="shop.id"
-            :label="shop.name"
-            :value="shop.id"
-          />
-        </el-select>
-        <el-button type="primary" @click="loadShopLocation" :loading="loading">加载位置</el-button>
-      </div>
-      <AmapWithSearch
-        v-if="showMap"
-        :longitude="shopLongitude"
-        :latitude="shopLatitude"
-        height="400px"
-        :zoom="15"
-        :enable-click-mark="false"
-        show-search
-      />
-      <div v-if="selectedShop" class="shop-info">
-        <p><strong>门店名称:</strong>{{ selectedShop.name }}</p>
-        <p><strong>详细地址:</strong>{{ selectedShop.address }}</p>
-        <p><strong>经度:</strong>{{ shopLongitude }}</p>
-        <p><strong>纬度:</strong>{{ shopLatitude }}</p>
-      </div>
-    </div>
-  </div>
-</template>
-
-<script setup lang="ts">
-import { ref, computed, onMounted } from 'vue';
-import { Amap } from '@/components/Amap';
-import { AmapWithSearch } from '@/components/AmapWithSearch';
-import { getShopList, getShopById } from '@/api/shop';
-
-const testLongitude = ref(116.397428);
-const testLatitude = ref(39.90923);
-
-// 门店定位相关
-const selectedShopId = ref<number | null>(null);
-const shopList = ref<any[]>([]);
-const shopLongitude = ref(0);
-const shopLatitude = ref(0);
-const loading = ref(false);
-const showMap = ref(false);
-
-// 加载门店列表
-const loadShopList = async () => {
-  try {
-    const res = await getShopList({ page: 1, pageSize: 100 });
-    console.log('门店列表 API 响应:', JSON.stringify(res));
-    
-    // API 返回格式可能是 { code: 200, message: '查询成功', data: { list: [], total: 0 } }
-    if (res.code === 200 || res.code === 0) {
-      shopList.value = res.data?.list || [];
-      console.log('加载门店列表成功,数量:', shopList.value.length);
-      if (shopList.value.length > 0) {
-        console.log('第一个门店:', shopList.value[0]);
-      }
-    } else {
-      console.warn('门店列表 API 返回异常 code:', res.code, 'message:', res.message);
-    }
-  } catch (error) {
-    console.error('加载门店列表失败:', error);
-  }
-};
-
-// 加载门店位置
-const loadShopLocation = async () => {
-  if (!selectedShopId.value) {
-    alert('请先选择门店');
-    return;
-  }
-
-  loading.value = true;
-  try {
-    const res = await getShopById(selectedShopId.value);
-    console.log('门店详情 API 响应:', JSON.stringify(res));
-    
-    // API 返回格式可能是 { code: 200, message: '查询成功', data: {...} }
-    if (res.code === 200 || res.code === 0) {
-      shopLongitude.value = Number(res.data?.longitude) || 0;
-      shopLatitude.value = Number(res.data?.latitude) || 0;
-      showMap.value = true;
-      console.log('加载门店位置成功:', {
-        name: res.data?.name,
-        longitude: shopLongitude.value,
-        latitude: shopLatitude.value
-      });
-    } else {
-      console.error('门店详情 API 返回异常 code:', res.code, 'message:', res.message);
-      alert(`加载门店信息失败:${res.message || '未知错误'}`);
-    }
-  } catch (error) {
-    console.error('加载门店位置失败:', error);
-    alert('加载失败,请重试');
-  } finally {
-    loading.value = false;
-  }
-};
-
-// 当前选中的门店
-const selectedShop = computed(() => {
-  return shopList.value.find(shop => shop.id === selectedShopId.value);
-});
-
-// 初始化加载门店列表
-onMounted(() => {
-  loadShopList();
-});
-
-const handleMarkerMoved = (lng: number, lat: number) => {
-  console.log('标记点移动到:', lng, lat);
-  testLongitude.value = lng;
-  testLatitude.value = lat;
-};
-</script>
-
-<style scoped>
-.map-test-page {
-  padding: 20px;
-  max-width: 1200px;
-  margin: 0 auto;
-}
-
-.test-section {
-  margin-bottom: 40px;
-  padding: 20px;
-  border: 1px solid #ddd;
-  border-radius: 8px;
-}
-
-.test-section h3 {
-  margin-top: 0;
-  color: #333;
-}
-
-.coordinates-info {
-  margin-top: 15px;
-  padding: 10px;
-  background: #f5f5f5;
-  border-radius: 4px;
-}
-
-.coordinates-info p {
-  margin: 5px 0;
-  font-size: 14px;
-}
-
-.shop-selector {
-  display: flex;
-  gap: 10px;
-  margin-bottom: 15px;
-  align-items: center;
-}
-
-.shop-info {
-  margin-top: 15px;
-  padding: 15px;
-  background: #f9f9f9;
-  border-radius: 4px;
-  border-left: 4px solid #409EFF;
-}
-
-.shop-info p {
-  margin: 8px 0;
-  font-size: 14px;
-  color: #333;
-}
-</style>

+ 267 - 0
haha-admin-web/src/views/timed-discount/index.vue

@@ -0,0 +1,267 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useTimedDiscount, useTimedDiscountRecord } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+
+defineOptions({
+  name: "TimedDiscount"
+});
+
+const activeTab = ref("activity");
+const formRef = ref();
+const recordFormRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  discountTypeMap,
+  statusMap,
+  onSearch,
+  resetForm,
+  handleAdd,
+  handleEdit,
+  handleViewDetail,
+  handleDelete,
+  handleToggleStatus,
+  handleExecute,
+  handleSizeChange,
+  handleCurrentChange
+} = useTimedDiscount();
+
+const {
+  form: recordForm,
+  loading: recordLoading,
+  columns: recordColumns,
+  dataList: recordDataList,
+  pagination: recordPagination,
+  executeStatusMap,
+  onSearch: onRecordSearch,
+  resetForm: resetRecordForm,
+  handleViewDetail: handleRecordDetail,
+  handleSizeChange: handleRecordSizeChange,
+  handleCurrentChange: handleRecordCurrentChange
+} = useTimedDiscountRecord();
+</script>
+
+<template>
+  <div class="main">
+    <el-tabs v-model="activeTab" class="demo-tabs">
+      <el-tab-pane label="活动配置" name="activity">
+        <PureTableBar title="定时折扣活动" :columns="columns" @refresh="onSearch">
+          <template #buttons>
+            <el-button type="primary" @click="handleAdd">
+              <IconifyIconOffline icon="add" class="mr-1" />
+              新增活动
+            </el-button>
+          </template>
+          <template #form>
+            <el-form
+              ref="formRef"
+              :inline="true"
+              :model="form"
+              class="search-form bg-bg_color px-4 pt-4 pb-2"
+            >
+              <el-form-item label="活动名称" prop="activityName">
+                <el-input
+                  v-model="form.activityName"
+                  placeholder="请输入活动名称"
+                  clearable
+                  class="!w-[200px]"
+                  @keyup.enter="onSearch"
+                />
+              </el-form-item>
+              <el-form-item label="折扣类型" prop="discountType">
+                <el-select
+                  v-model="form.discountType"
+                  placeholder="请选择"
+                  clearable
+                  class="!w-[150px]"
+                >
+                  <el-option
+                    v-for="(item, key) in discountTypeMap"
+                    :key="key"
+                    :label="item.text"
+                    :value="Number(key)"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item label="状态" prop="status">
+                <el-select
+                  v-model="form.status"
+                  placeholder="请选择"
+                  clearable
+                  class="!w-[150px]"
+                >
+                  <el-option
+                    v-for="(item, key) in statusMap"
+                    :key="key"
+                    :label="item.text"
+                    :value="Number(key)"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" @click="onSearch">搜索</el-button>
+                <el-button @click="resetForm(formRef)">重置</el-button>
+              </el-form-item>
+            </el-form>
+          </template>
+          <template v-slot="{ size, dynamicColumns }">
+            <pure-table
+              :loading="loading"
+              :data="dataList"
+              :columns="dynamicColumns"
+              :size="size"
+              adaptive
+              :adaptive-config="{ offsetBottom: 32 }"
+              :pagination="{
+                currentPage: pagination.currentPage,
+                pageSize: pagination.pageSize,
+                total: pagination.total,
+                pageSizes: [10, 20, 50, 100],
+                onSizeChange: handleSizeChange,
+                onCurrentChange: handleCurrentChange
+              }"
+            >
+              <template #operation="{ row }">
+                <el-button
+                  link
+                  type="primary"
+                  size="small"
+                  @click="handleViewDetail(row)"
+                >
+                  详情
+                </el-button>
+                <el-button
+                  link
+                  type="primary"
+                  size="small"
+                  @click="handleEdit(row)"
+                >
+                  编辑
+                </el-button>
+                <el-button
+                  link
+                  :type="row.status === 1 ? 'warning' : 'success'"
+                  size="small"
+                  @click="handleToggleStatus(row)"
+                >
+                  {{ row.status === 1 ? "禁用" : "启用" }}
+                </el-button>
+                <el-button
+                  link
+                  type="success"
+                  size="small"
+                  @click="handleExecute(row)"
+                >
+                  执行
+                </el-button>
+                <el-popconfirm
+                  title="确定要删除该活动吗?"
+                  @confirm="handleDelete(row)"
+                >
+                  <template #reference>
+                    <el-button link type="danger" size="small">删除</el-button>
+                  </template>
+                </el-popconfirm>
+              </template>
+            </pure-table>
+          </template>
+        </PureTableBar>
+      </el-tab-pane>
+
+      <el-tab-pane label="执行记录" name="records">
+        <PureTableBar title="执行记录" :columns="recordColumns" @refresh="onRecordSearch">
+          <template #form>
+            <el-form
+              ref="recordFormRef"
+              :inline="true"
+              :model="recordForm"
+              class="search-form bg-bg_color px-4 pt-4 pb-2"
+            >
+              <el-form-item label="执行日期" prop="executeDateStart">
+                <el-date-picker
+                  v-model="recordForm.executeDateStart"
+                  type="date"
+                  placeholder="开始日期"
+                  value-format="YYYY-MM-DD"
+                  class="!w-[150px]"
+                />
+              </el-form-item>
+              <el-form-item prop="executeDateEnd">
+                <el-date-picker
+                  v-model="recordForm.executeDateEnd"
+                  type="date"
+                  placeholder="结束日期"
+                  value-format="YYYY-MM-DD"
+                  class="!w-[150px]"
+                />
+              </el-form-item>
+              <el-form-item label="状态" prop="status">
+                <el-select
+                  v-model="recordForm.status"
+                  placeholder="请选择"
+                  clearable
+                  class="!w-[150px]"
+                >
+                  <el-option
+                    v-for="(item, key) in executeStatusMap"
+                    :key="key"
+                    :label="item.text"
+                    :value="Number(key)"
+                  />
+                </el-select>
+              </el-form-item>
+              <el-form-item>
+                <el-button type="primary" @click="onRecordSearch">搜索</el-button>
+                <el-button @click="resetRecordForm(recordFormRef)">重置</el-button>
+              </el-form-item>
+            </el-form>
+          </template>
+          <template v-slot="{ size, dynamicColumns }">
+            <pure-table
+              :loading="recordLoading"
+              :data="recordDataList"
+              :columns="dynamicColumns"
+              :size="size"
+              adaptive
+              :adaptive-config="{ offsetBottom: 32 }"
+              :pagination="{
+                currentPage: recordPagination.currentPage,
+                pageSize: recordPagination.pageSize,
+                total: recordPagination.total,
+                pageSizes: [10, 20, 50, 100],
+                onSizeChange: handleRecordSizeChange,
+                onCurrentChange: handleRecordCurrentChange
+              }"
+            >
+              <template #operation="{ row }">
+                <el-button
+                  link
+                  type="primary"
+                  size="small"
+                  @click="handleRecordDetail(row)"
+                >
+                  查看详情
+                </el-button>
+              </template>
+            </pure-table>
+          </template>
+        </PureTableBar>
+      </el-tab-pane>
+    </el-tabs>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.main {
+  padding: 20px;
+}
+
+.search-form {
+  border-radius: 4px;
+}
+</style>

+ 785 - 0
haha-admin-web/src/views/timed-discount/utils/hook.tsx

@@ -0,0 +1,785 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import { addDialog } from "@/components/ReDialog";
+import {
+  getTimedDiscountList,
+  getTimedDiscountById,
+  createTimedDiscount,
+  updateTimedDiscount,
+  updateTimedDiscountStatus,
+  deleteTimedDiscount,
+  executeTimedDiscount,
+  getTimedDiscountRecords,
+  getTimedDiscountRecordById,
+  getPriceAdjustmentLogs,
+  getTimedDiscountStatistics,
+  getTimedDiscountDailyStatistics
+} from "@/api/timedDiscount";
+import type { PaginationProps } from "@pureadmin/table";
+import { deviceDetection } from "@pureadmin/utils";
+import { onMounted, reactive, ref, toRaw, h } from "vue";
+import {
+  ElForm,
+  ElInput,
+  ElFormItem,
+  ElSelect,
+  ElOption,
+  ElTimePicker,
+  ElInputNumber,
+  ElSwitch,
+  ElDescriptions,
+  ElDescriptionsItem,
+  ElTable,
+  ElTableColumn,
+  ElTag,
+  ElTabs,
+  ElTabPane,
+  ElDatePicker,
+  ElStatistic
+} from "element-plus";
+import { 
+  initPagination, 
+  handlePageSizeChange, 
+  handleCurrentPageChange, 
+  resetPagination,
+  updatePaginationData 
+} from "@/utils/paginationHelper";
+import type { SearchFormProps, RecordSearchFormProps, FormDataProps, TimedDiscountActivity, TimedDiscountRecord, PriceAdjustmentLog } from "./types";
+
+const discountTypeMap = {
+  1: { text: "固定折扣", type: "primary" },
+  2: { text: "阶梯折扣", type: "success" },
+  3: { text: "固定价格", type: "warning" }
+};
+
+const statusMap = {
+  0: { text: "已禁用", type: "danger" },
+  1: { text: "已启用", type: "success" }
+};
+
+const executeStatusMap = {
+  1: { text: "成功", type: "success" },
+  2: { text: "部分成功", type: "warning" },
+  3: { text: "失败", type: "danger" }
+};
+
+const restoreStatusMap = {
+  0: { text: "未恢复", type: "warning" },
+  1: { text: "已恢复", type: "success" },
+  2: { text: "已售出", type: "primary" }
+};
+
+const productTypeOptions = [
+  { label: "自制餐品", value: "自制餐品" },
+  { label: "盒饭", value: "盒饭" },
+  { label: "水饮", value: "水饮" },
+  { label: "副食", value: "副食" },
+  { label: "零食", value: "零食" },
+  { label: "饮料", value: "饮料" }
+];
+
+export function useTimedDiscount() {
+  const form = reactive<SearchFormProps>({
+    activityName: "",
+    status: undefined,
+    discountType: undefined
+  });
+  const loading = ref(true);
+  const dataList = ref<TimedDiscountActivity[]>([]);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  const columns: TableColumnList = [
+    {
+      label: "活动ID",
+      prop: "id",
+      width: 80
+    },
+    {
+      label: "活动名称",
+      prop: "activityName",
+      minWidth: 150
+    },
+    {
+      label: "折扣类型",
+      prop: "discountType",
+      minWidth: 100,
+      cellRenderer: ({ row }) => {
+        const item = discountTypeMap[row.discountType] || { text: "未知", type: "info" };
+        return <el-tag type={item.type}>{item.text}</el-tag>;
+      }
+    },
+    {
+      label: "折扣值",
+      prop: "discountValue",
+      minWidth: 100,
+      formatter: ({ discountType, discountValue }) => {
+        if (discountType === 1) {
+          return `${(discountValue * 100).toFixed(0)}%`;
+        } else if (discountType === 3) {
+          return `¥${discountValue}`;
+        }
+        return discountValue;
+      }
+    },
+    {
+      label: "开始时间",
+      prop: "startTime",
+      minWidth: 100,
+      formatter: ({ startTime }) => startTime || ""
+    },
+    {
+      label: "结束时间",
+      prop: "endTime",
+      minWidth: 100,
+      formatter: ({ endTime }) => endTime || "次日营业"
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 80,
+      cellRenderer: ({ row }) => {
+        const item = statusMap[row.status] || { text: "未知", type: "info" };
+        return <el-tag type={item.type}>{item.text}</el-tag>;
+      }
+    },
+    {
+      label: "自动执行",
+      prop: "autoExecute",
+      minWidth: 80,
+      cellRenderer: ({ row }) => (
+        <el-tag type={row.autoExecute === 1 ? "success" : "info"}>
+          {row.autoExecute === 1 ? "是" : "否"}
+        </el-tag>
+      )
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : ""
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 280,
+      slot: "operation"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+      
+      if (form.activityName) searchParams.activityName = form.activityName;
+      if (form.status !== undefined) searchParams.status = form.status;
+      if (form.discountType !== undefined) searchParams.discountType = form.discountType;
+      
+      const { data } = await getTimedDiscountList(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        updatePaginationData(pagination, data);
+      }
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function resetForm(formEl) {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  }
+
+  async function handleDelete(row: TimedDiscountActivity) {
+    const { code } = await deleteTimedDiscount(row.id);
+    if (code === 200) {
+      message("删除成功", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handleToggleStatus(row: TimedDiscountActivity) {
+    const newStatus = row.status === 1 ? 0 : 1;
+    const { code } = await updateTimedDiscountStatus(row.id, newStatus);
+    if (code === 200) {
+      message(newStatus === 1 ? "已启用" : "已禁用", { type: "success" });
+      onSearch();
+    }
+  }
+
+  async function handleExecute(row: TimedDiscountActivity) {
+    const { code } = await executeTimedDiscount(row.id);
+    if (code === 200) {
+      message("执行成功", { type: "success" });
+    }
+  }
+
+  function handleViewDetail(row: TimedDiscountActivity) {
+    addDialog({
+      title: "活动详情",
+      width: "900px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => (
+        <div>
+          <ElDescriptions column={2} border>
+            <ElDescriptionsItem label="活动名称">{row.activityName}</ElDescriptionsItem>
+            <ElDescriptionsItem label="折扣类型">
+              <el-tag type={discountTypeMap[row.discountType]?.type}>
+                {discountTypeMap[row.discountType]?.text}
+              </el-tag>
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="开始时间">{row.startTime}</ElDescriptionsItem>
+            <ElDescriptionsItem label="结束时间">{row.endTime || "次日营业开始"}</ElDescriptionsItem>
+            <ElDescriptionsItem label="折扣值">
+              {row.discountType === 1 ? `${(row.discountValue * 100).toFixed(0)}%` : `¥${row.discountValue}`}
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="最低折扣价">
+              {row.minDiscountPrice ? `¥${row.minDiscountPrice}` : "不限制"}
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="适用范围">{row.applyScopeLabel}</ElDescriptionsItem>
+            <ElDescriptionsItem label="商品范围">{row.productScopeLabel}</ElDescriptionsItem>
+            <ElDescriptionsItem label="适用商品分类" span={2}>
+              {row.productTypes || "全部"}
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="优先商品分类" span={2}>
+              {row.priorityProductTypes || "无"}
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="最低库存阈值">{row.minStock}</ElDescriptionsItem>
+            <ElDescriptionsItem label="最高库存阈值">{row.maxStock || "不限制"}</ElDescriptionsItem>
+            <ElDescriptionsItem label="状态">
+              <el-tag type={statusMap[row.status]?.type}>{statusMap[row.status]?.text}</el-tag>
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="自动执行">{row.autoExecute === 1 ? "是" : "否"}</ElDescriptionsItem>
+            <ElDescriptionsItem label="活动说明" span={2}>{row.activityDesc || "暂无说明"}</ElDescriptionsItem>
+          </ElDescriptions>
+        </div>
+      )
+    });
+  }
+
+  function handleEdit(row: TimedDiscountActivity) {
+    const formData = reactive<FormDataProps>({
+      id: row.id,
+      activityName: row.activityName,
+      activityDesc: row.activityDesc || "",
+      startTime: row.startTime,
+      endTime: row.endTime || "",
+      discountType: row.discountType,
+      discountValue: row.discountValue,
+      minDiscountPrice: row.minDiscountPrice || null,
+      applyScope: row.applyScope,
+      productScope: row.productScope,
+      productTypes: row.productTypes || "",
+      priorityProductTypes: row.priorityProductTypes || "",
+      minStock: row.minStock || 1,
+      maxStock: row.maxStock || null,
+      autoExecute: row.autoExecute,
+      notifyOnExecute: row.notifyOnExecute || 0,
+      shopIds: row.shopIds || [],
+      productIds: row.productIds || []
+    });
+
+    addDialog({
+      title: "编辑活动",
+      width: "700px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => renderForm(formData),
+      beforeSure: async (done) => {
+        if (!validateForm(formData)) return;
+        try {
+          const { code } = await updateTimedDiscount(formData.id!, toRaw(formData));
+          if (code === 200) {
+            message("编辑成功", { type: "success" });
+            done();
+            onSearch();
+          }
+        } catch (error) {
+          message("编辑失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  function handleAdd() {
+    const formData = reactive<FormDataProps>({
+      activityName: "",
+      activityDesc: "",
+      startTime: "20:00:00",
+      endTime: "23:59:59",
+      discountType: 1,
+      discountValue: 0.5,
+      minDiscountPrice: 1,
+      applyScope: 1,
+      productScope: 2,
+      productTypes: "自制餐品,盒饭",
+      priorityProductTypes: "自制餐品,盒饭",
+      minStock: 1,
+      maxStock: null,
+      autoExecute: 1,
+      notifyOnExecute: 0,
+      shopIds: [],
+      productIds: []
+    });
+
+    addDialog({
+      title: "新增活动",
+      width: "700px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => renderForm(formData),
+      beforeSure: async (done) => {
+        if (!validateForm(formData)) return;
+        try {
+          const { code } = await createTimedDiscount(toRaw(formData));
+          if (code === 200) {
+            message("新增成功", { type: "success" });
+            done();
+            onSearch();
+          }
+        } catch (error) {
+          message("新增失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  function renderForm(formData: FormDataProps) {
+    return (
+      <ElForm label-width="120px">
+        <ElFormItem label="活动名称" required>
+          <ElInput v-model={formData.activityName} placeholder="请输入活动名称" clearable />
+        </ElFormItem>
+        <ElFormItem label="活动说明">
+          <ElInput v-model={formData.activityDesc} type="textarea" rows={2} placeholder="请输入活动说明" />
+        </ElFormItem>
+        <ElFormItem label="开始时间" required>
+          <ElTimePicker
+            v-model={formData.startTime}
+            placeholder="选择开始时间"
+            format="HH:mm:ss"
+            value-format="HH:mm:ss"
+            class="w-full"
+          />
+        </ElFormItem>
+        <ElFormItem label="结束时间">
+          <ElTimePicker
+            v-model={formData.endTime}
+            placeholder="选择结束时间(空表示次日营业)"
+            format="HH:mm:ss"
+            value-format="HH:mm:ss"
+            class="w-full"
+            clearable
+          />
+        </ElFormItem>
+        <ElFormItem label="折扣类型" required>
+          <ElSelect v-model={formData.discountType} placeholder="请选择折扣类型" class="w-full">
+            <ElOption label="固定折扣" value={1} />
+            <ElOption label="阶梯折扣" value={2} />
+            <ElOption label="固定价格" value={3} />
+          </ElSelect>
+        </ElFormItem>
+        <ElFormItem label="折扣值" required>
+          <ElInputNumber
+            v-model={formData.discountValue}
+            min={0}
+            max={formData.discountType === 1 ? 1 : 9999}
+            step={formData.discountType === 1 ? 0.1 : 1}
+            precision={formData.discountType === 1 ? 2 : 0}
+            class="w-full"
+            placeholder={formData.discountType === 1 ? "折扣比例(如0.5表示5折)" : "固定价格"}
+          />
+        </ElFormItem>
+        <ElFormItem label="最低折扣价">
+          <ElInputNumber
+            v-model={formData.minDiscountPrice}
+            min={0}
+            precision={2}
+            class="w-full"
+            placeholder="最低折扣价格(防止价格过低)"
+          />
+        </ElFormItem>
+        <ElFormItem label="适用范围">
+          <ElSelect v-model={formData.applyScope} placeholder="请选择适用范围" class="w-full">
+            <ElOption label="全部门店" value={1} />
+            <ElOption label="指定门店" value={2} />
+          </ElSelect>
+        </ElFormItem>
+        <ElFormItem label="商品范围">
+          <ElSelect v-model={formData.productScope} placeholder="请选择商品范围" class="w-full">
+            <ElOption label="全部商品" value={1} />
+            <ElOption label="指定分类" value={2} />
+            <ElOption label="指定商品" value={3} />
+          </ElSelect>
+        </ElFormItem>
+        {formData.productScope === 2 && (
+          <ElFormItem label="适用商品分类">
+            <ElSelect
+              v-model={formData.productTypes}
+              multiple
+              filterable
+              allow-create
+              placeholder="请选择商品分类"
+              class="w-full"
+            >
+              {productTypeOptions.map(item => (
+                <ElOption key={item.value} label={item.label} value={item.value} />
+              ))}
+            </ElSelect>
+          </ElFormItem>
+        )}
+        <ElFormItem label="优先商品分类">
+          <ElSelect
+            v-model={formData.priorityProductTypes}
+            multiple
+            filterable
+            allow-create
+            placeholder="请选择优先商品分类(自制餐品等)"
+            class="w-full"
+          >
+            {productTypeOptions.map(item => (
+              <ElOption key={item.value} label={item.label} value={item.value} />
+            ))}
+          </ElSelect>
+        </ElFormItem>
+        <ElFormItem label="最低库存阈值">
+          <ElInputNumber v-model={formData.minStock} min={1} class="w-full" placeholder="低于此数量不参与优惠" />
+        </ElFormItem>
+        <ElFormItem label="最高库存阈值">
+          <ElInputNumber v-model={formData.maxStock} min={1} class="w-full" placeholder="高于此数量不参与优惠(空不限制)" clearable />
+        </ElFormItem>
+        <ElFormItem label="自动执行">
+          <ElSwitch v-model={formData.autoExecute} active-value={1} inactive-value={0} />
+        </ElFormItem>
+      </ElForm>
+    );
+  }
+
+  function validateForm(formData: FormDataProps): boolean {
+    if (!formData.activityName) {
+      message("请输入活动名称", { type: "warning" });
+      return false;
+    }
+    if (!formData.startTime) {
+      message("请选择开始时间", { type: "warning" });
+      return false;
+    }
+    if (!formData.discountType) {
+      message("请选择折扣类型", { type: "warning" });
+      return false;
+    }
+    if (!formData.discountValue) {
+      message("请输入折扣值", { type: "warning" });
+      return false;
+    }
+    return true;
+  }
+
+  function handleSizeChange(val: number) {
+    handlePageSizeChange(val, pagination, onSearch);
+  }
+
+  function handleCurrentChange(val: number) {
+    handleCurrentPageChange(val, pagination, onSearch);
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    discountTypeMap,
+    statusMap,
+    onSearch,
+    resetForm,
+    handleAdd,
+    handleEdit,
+    handleViewDetail,
+    handleDelete,
+    handleToggleStatus,
+    handleExecute,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}
+
+export function useTimedDiscountRecord() {
+  const form = reactive<RecordSearchFormProps>({
+    activityId: undefined,
+    executeDateStart: "",
+    executeDateEnd: "",
+    shopId: undefined,
+    deviceId: "",
+    status: undefined
+  });
+  const loading = ref(true);
+  const dataList = ref<TimedDiscountRecord[]>([]);
+  const pagination = reactive<PaginationProps>(initPagination());
+
+  const columns: TableColumnList = [
+    {
+      label: "记录ID",
+      prop: "id",
+      width: 80
+    },
+    {
+      label: "活动名称",
+      prop: "activityName",
+      minWidth: 120
+    },
+    {
+      label: "执行日期",
+      prop: "executeDate",
+      minWidth: 100
+    },
+    {
+      label: "执行时间",
+      prop: "executeTime",
+      minWidth: 160,
+      formatter: ({ executeTime }) =>
+        executeTime ? dayjs(executeTime).format("YYYY-MM-DD HH:mm:ss") : ""
+    },
+    {
+      label: "门店",
+      prop: "shopName",
+      minWidth: 120
+    },
+    {
+      label: "设备",
+      prop: "deviceName",
+      minWidth: 120
+    },
+    {
+      label: "商品数",
+      prop: "totalProducts",
+      minWidth: 80
+    },
+    {
+      label: "优先商品",
+      prop: "priorityProducts",
+      minWidth: 80
+    },
+    {
+      label: "总库存",
+      prop: "totalStock",
+      minWidth: 80
+    },
+    {
+      label: "原价总值",
+      prop: "originalValue",
+      minWidth: 100,
+      formatter: ({ originalValue }) => `¥${originalValue?.toFixed(2) || "0.00"}`
+    },
+    {
+      label: "优惠金额",
+      prop: "savedValue",
+      minWidth: 100,
+      formatter: ({ savedValue }) => `¥${savedValue?.toFixed(2) || "0.00"}`
+    },
+    {
+      label: "状态",
+      prop: "status",
+      minWidth: 80,
+      cellRenderer: ({ row }) => {
+        const item = executeStatusMap[row.status] || { text: "未知", type: "info" };
+        return <el-tag type={item.type}>{item.text}</el-tag>;
+      }
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 120,
+      slot: "operation"
+    }
+  ];
+
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+      
+      if (form.activityId) searchParams.activityId = form.activityId;
+      if (form.executeDateStart) searchParams.executeDateStart = form.executeDateStart;
+      if (form.executeDateEnd) searchParams.executeDateEnd = form.executeDateEnd;
+      if (form.shopId) searchParams.shopId = form.shopId;
+      if (form.deviceId) searchParams.deviceId = form.deviceId;
+      if (form.status !== undefined) searchParams.status = form.status;
+      
+      const { data } = await getTimedDiscountRecords(searchParams);
+      if (data) {
+        dataList.value = data.list || [];
+        updatePaginationData(pagination, data);
+      }
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  function resetForm(formEl) {
+    if (!formEl) return;
+    formEl.resetFields();
+    resetPagination(pagination, onSearch);
+  }
+
+  function handleViewDetail(row: TimedDiscountRecord) {
+    addDialog({
+      title: "执行记录详情",
+      width: "1000px",
+      draggable: true,
+      fullscreen: deviceDetection(),
+      contentRenderer: () => (
+        <div>
+          <ElDescriptions column={3} border class="mb-4">
+            <ElDescriptionsItem label="活动名称">{row.activityName}</ElDescriptionsItem>
+            <ElDescriptionsItem label="执行日期">{row.executeDate}</ElDescriptionsItem>
+            <ElDescriptionsItem label="执行时间">
+              {dayjs(row.executeTime).format("YYYY-MM-DD HH:mm:ss")}
+            </ElDescriptionsItem>
+            <ElDescriptionsItem label="门店">{row.shopName || "全部"}</ElDescriptionsItem>
+            <ElDescriptionsItem label="设备">{row.deviceName || "全部"}</ElDescriptionsItem>
+            <ElDescriptionsItem label="状态">
+              <el-tag type={executeStatusMap[row.status]?.type}>
+                {executeStatusMap[row.status]?.text}
+              </el-tag>
+            </ElDescriptionsItem>
+          </ElDescriptions>
+          
+          <div class="grid grid-cols-4 gap-4 mb-4">
+            <ElStatistic title="处理商品数" value={row.totalProducts} />
+            <ElStatistic title="优先商品数" value={row.priorityProducts} />
+            <ElStatistic title="总库存" value={row.totalStock} />
+            <ElStatistic title="优惠金额" value={row.savedValue} prefix="¥" precision={2} />
+          </div>
+          
+          <h4 class="mb-2">价格调整明细</h4>
+          <PriceAdjustmentTable recordId={row.id} />
+        </div>
+      )
+    });
+  }
+
+  function handleSizeChange(val: number) {
+    handlePageSizeChange(val, pagination, onSearch);
+  }
+
+  function handleCurrentChange(val: number) {
+    handleCurrentPageChange(val, pagination, onSearch);
+  }
+
+  onMounted(() => {
+    onSearch();
+  });
+
+  return {
+    form,
+    loading,
+    columns,
+    dataList,
+    pagination,
+    executeStatusMap,
+    onSearch,
+    resetForm,
+    handleViewDetail,
+    handleSizeChange,
+    handleCurrentChange
+  };
+}
+
+function PriceAdjustmentTable(props: { recordId: number }) {
+  const loading = ref(true);
+  const dataList = ref<PriceAdjustmentLog[]>([]);
+  const pagination = reactive({ currentPage: 1, pageSize: 10, total: 0 });
+
+  async function loadData() {
+    loading.value = true;
+    try {
+      const { data } = await getPriceAdjustmentLogs(props.recordId, {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      });
+      if (data) {
+        dataList.value = data.list || [];
+        pagination.total = data.total || 0;
+      }
+    } finally {
+      loading.value = false;
+    }
+  }
+
+  onMounted(loadData);
+
+  const columns: TableColumnList = [
+    { label: "商品名称", prop: "productName", minWidth: 120 },
+    { label: "商品分类", prop: "productType", minWidth: 80 },
+    { label: "库存", prop: "stock", width: 60 },
+    { 
+      label: "原价", 
+      prop: "originalPrice", 
+      width: 80,
+      formatter: ({ originalPrice }) => `¥${originalPrice?.toFixed(2)}`
+    },
+    { 
+      label: "折扣价", 
+      prop: "discountedPrice", 
+      width: 80,
+      formatter: ({ discountedPrice }) => `¥${discountedPrice?.toFixed(2)}`
+    },
+    { 
+      label: "优先", 
+      prop: "isPriority", 
+      width: 60,
+      cellRenderer: ({ row }) => (
+        <el-tag type={row.isPriority === 1 ? "warning" : "info"} size="small">
+          {row.isPriority === 1 ? "是" : "否"}
+        </el-tag>
+      )
+    },
+    { 
+      label: "恢复状态", 
+      prop: "restoreStatus", 
+      width: 80,
+      cellRenderer: ({ row }) => (
+        <el-tag type={restoreStatusMap[row.restoreStatus]?.type} size="small">
+          {restoreStatusMap[row.restoreStatus]?.text}
+        </el-tag>
+      )
+    },
+    { 
+      label: "售出数量", 
+      prop: "saleQuantity", 
+      width: 80 
+    }
+  ];
+
+  return () => (
+    <ElTable
+      data={dataList.value}
+      v-loading={loading.value}
+      columns={columns}
+      style={{ width: "100%" }}
+      pagination={{
+        currentPage: pagination.currentPage,
+        pageSize: pagination.pageSize,
+        total: pagination.total,
+        onCurrentChange: (val: number) => {
+          pagination.currentPage = val;
+          loadData();
+        }
+      }}
+    />
+  );
+}

+ 140 - 0
haha-admin-web/src/views/timed-discount/utils/types.ts

@@ -0,0 +1,140 @@
+export interface TimedDiscountActivity {
+  id: number;
+  activityName: string;
+  activityDesc: string;
+  startTime: string;
+  endTime: string;
+  discountType: number;
+  discountValue: number;
+  minDiscountPrice: number;
+  applyScope: number;
+  productScope: number;
+  productTypes: string;
+  priorityProductTypes: string;
+  minStock: number;
+  maxStock: number;
+  status: number;
+  autoExecute: number;
+  notifyOnExecute: number;
+  creatorId: number;
+  creatorName: string;
+  createTime: string;
+  updateTime: string;
+  statusLabel: string;
+  statusColor: string;
+  discountTypeLabel: string;
+  applyScopeLabel: string;
+  productScopeLabel: string;
+  shopIds: number[];
+  productIds: number[];
+  productTypeList: string[];
+  priorityProductTypeList: string[];
+}
+
+export interface TimedDiscountRecord {
+  id: number;
+  activityId: number;
+  activityName: string;
+  executeDate: string;
+  executeTime: string;
+  shopId: number;
+  shopName: string;
+  deviceId: string;
+  deviceName: string;
+  totalProducts: number;
+  priorityProducts: number;
+  totalStock: number;
+  originalValue: number;
+  discountValue: number;
+  savedValue: number;
+  status: number;
+  errorMessage: string;
+  executeDuration: number;
+  createTime: string;
+  statusLabel: string;
+  statusColor: string;
+}
+
+export interface PriceAdjustmentLog {
+  id: number;
+  recordId: number;
+  activityId: number;
+  shopId: number;
+  shopName: string;
+  deviceId: string;
+  deviceName: string;
+  productId: number;
+  productName: string;
+  productType: string;
+  stock: number;
+  originalPrice: number;
+  discountType: number;
+  discountValue: number;
+  discountedPrice: number;
+  isPriority: number;
+  adjustTime: string;
+  restoreTime: string;
+  restoreStatus: number;
+  saleQuantity: number;
+  saleAmount: number;
+  restoreStatusLabel: string;
+  restoreStatusColor: string;
+  discountTypeLabel: string;
+}
+
+export interface TimedDiscountStatistics {
+  id: number;
+  activityId: number;
+  statDate: string;
+  shopId: number;
+  shopName: string;
+  totalAdjustedProducts: number;
+  priorityProducts: number;
+  totalAdjustedStock: number;
+  totalSoldQuantity: number;
+  prioritySoldQuantity: number;
+  originalValue: number;
+  actualSales: number;
+  savedAmount: number;
+  wasteReductionRate: number;
+  sellThroughRate: number;
+  createTime: string;
+  updateTime: string;
+  activityName: string;
+}
+
+export interface SearchFormProps {
+  activityName: string;
+  status: number | undefined;
+  discountType: number | undefined;
+}
+
+export interface RecordSearchFormProps {
+  activityId: number | undefined;
+  executeDateStart: string;
+  executeDateEnd: string;
+  shopId: number | undefined;
+  deviceId: string;
+  status: number | undefined;
+}
+
+export interface FormDataProps {
+  id?: number;
+  activityName: string;
+  activityDesc: string;
+  startTime: string;
+  endTime: string;
+  discountType: number;
+  discountValue: number;
+  minDiscountPrice: number;
+  applyScope: number;
+  productScope: number;
+  productTypes: string;
+  priorityProductTypes: string;
+  minStock: number;
+  maxStock: number;
+  autoExecute: number;
+  notifyOnExecute: number;
+  shopIds: number[];
+  productIds: number[];
+}

+ 5 - 0
haha-admin-web/vite.config.ts

@@ -112,6 +112,11 @@ export default ({ mode }: ConfigEnv): UserConfigExport => {
           target: "http://localhost:8080/admin",
           changeOrigin: true,
           rewrite: path => path.replace(/^\/dict/, "/dict")
+        },
+        "/timed-discount": {
+          target: "http://localhost:8080/admin",
+          changeOrigin: true,
+          rewrite: path => path.replace(/^\/timed-discount/, "/timed-discount")
         }
       },
       // 预热文件以提前转换和缓存结果,降低启动期间的初始页面加载时长并防止转换瀑布

+ 147 - 0
haha-admin/src/main/java/com/haha/admin/controller/TimedDiscountController.java

@@ -0,0 +1,147 @@
+package com.haha.admin.controller;
+
+import cn.dev33.satoken.stp.StpUtil;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
+import com.haha.common.vo.Result;
+import com.haha.entity.TimedDiscountActivity;
+import com.haha.entity.TimedDiscountRecord;
+import com.haha.entity.TimedDiscountStatistics;
+import com.haha.entity.PriceAdjustmentLog;
+import com.haha.entity.dto.TimedDiscountCreateDTO;
+import com.haha.entity.dto.TimedDiscountQueryDTO;
+import com.haha.entity.dto.TimedDiscountRecordQueryDTO;
+import com.haha.entity.dto.TimedDiscountUpdateDTO;
+import com.haha.service.TimedDiscountService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@Slf4j
+@RestController
+@RequestMapping("/timed-discount")
+@RequiredArgsConstructor
+public class TimedDiscountController {
+
+    private final TimedDiscountService timedDiscountService;
+
+    @RequirePermission("timed-discount:read")
+    @GetMapping("/list")
+    public Result<PageResult<TimedDiscountActivity>> list(TimedDiscountQueryDTO queryDTO) {
+        IPage<TimedDiscountActivity> page = timedDiscountService.getPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("timed-discount:read")
+    @GetMapping("/{id}")
+    public Result<Map<String, Object>> getById(@PathVariable Long id) {
+        TimedDiscountActivity activity = timedDiscountService.getDetail(id);
+        List<Long> shopIds = timedDiscountService.getActivityShopIds(id);
+        List<Long> productIds = timedDiscountService.getActivityProductIds(id);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("activity", activity);
+        result.put("shopIds", shopIds);
+        result.put("productIds", productIds);
+
+        return Result.success("查询成功", result);
+    }
+
+    @RequirePermission("timed-discount:create")
+    @Log(module = "定时折扣", operation = OperationType.INSERT, summary = "创建活动")
+    @PostMapping
+    public Result<TimedDiscountActivity> create(@RequestBody TimedDiscountCreateDTO dto) {
+        Long creatorId = StpUtil.getLoginIdAsLong();
+        String creatorName = (String) StpUtil.getSession().get("name");
+
+        TimedDiscountActivity activity = timedDiscountService.create(dto, creatorId, creatorName);
+        return Result.success("创建成功", activity);
+    }
+
+    @RequirePermission("timed-discount:edit")
+    @Log(module = "定时折扣", operation = OperationType.UPDATE, summary = "编辑活动")
+    @PutMapping("/{id}")
+    public Result<TimedDiscountActivity> update(@PathVariable Long id, @RequestBody TimedDiscountUpdateDTO dto) {
+        dto.setId(id);
+        TimedDiscountActivity activity = timedDiscountService.update(dto);
+        return Result.success("更新成功", activity);
+    }
+
+    @RequirePermission("timed-discount:edit")
+    @Log(module = "定时折扣", operation = OperationType.UPDATE, summary = "更新状态")
+    @PutMapping("/{id}/status")
+    public Result<Void> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
+        timedDiscountService.updateStatus(id, status);
+        return Result.success("状态更新成功", null);
+    }
+
+    @RequirePermission("timed-discount:delete")
+    @Log(module = "定时折扣", operation = OperationType.DELETE, summary = "删除活动")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        timedDiscountService.deleteActivity(id);
+        return Result.success("删除成功", null);
+    }
+
+    @RequirePermission("timed-discount:execute")
+    @Log(module = "定时折扣", operation = OperationType.UPDATE, summary = "手动执行活动")
+    @PostMapping("/{id}/execute")
+    public Result<Void> execute(@PathVariable Long id) {
+        timedDiscountService.executeDiscountActivity(id);
+        return Result.success("执行成功", null);
+    }
+
+    @RequirePermission("timed-discount:read")
+    @GetMapping("/records")
+    public Result<PageResult<TimedDiscountRecord>> getRecords(TimedDiscountRecordQueryDTO queryDTO) {
+        IPage<TimedDiscountRecord> page = timedDiscountService.getRecordPage(queryDTO);
+        return Result.success("查询成功", PageResult.of(page));
+    }
+
+    @RequirePermission("timed-discount:read")
+    @GetMapping("/records/{recordId}")
+    public Result<TimedDiscountRecord> getRecordDetail(@PathVariable Long recordId) {
+        TimedDiscountRecord record = timedDiscountService.getRecordDetail(recordId);
+        return Result.success("查询成功", record);
+    }
+
+    @RequirePermission("timed-discount:read")
+    @GetMapping("/records/{recordId}/logs")
+    public Result<PageResult<PriceAdjustmentLog>> getPriceAdjustmentLogs(
+            @PathVariable Long recordId,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int pageSize) {
+        IPage<PriceAdjustmentLog> pageResult = timedDiscountService.getPriceAdjustmentLogs(recordId, page, pageSize);
+        return Result.success("查询成功", PageResult.of(pageResult));
+    }
+
+    @RequirePermission("timed-discount:read")
+    @GetMapping("/{id}/statistics")
+    public Result<Map<String, Object>> getStatistics(@PathVariable Long id) {
+        Map<String, Object> stats = timedDiscountService.getActivityStatistics(id);
+        return Result.success("查询成功", stats);
+    }
+
+    @RequirePermission("timed-discount:read")
+    @GetMapping("/{id}/statistics/daily")
+    public Result<PageResult<TimedDiscountStatistics>> getDailyStatistics(
+            @PathVariable Long id,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "20") int pageSize) {
+        IPage<TimedDiscountStatistics> pageResult = timedDiscountService.getStatisticsPage(id, page, pageSize);
+        return Result.success("查询成功", PageResult.of(pageResult));
+    }
+
+    @GetMapping("/enabled")
+    public Result<List<TimedDiscountActivity>> getEnabledActivities() {
+        List<TimedDiscountActivity> activities = timedDiscountService.getEnabledActivities();
+        return Result.success("查询成功", activities);
+    }
+}

+ 48 - 0
haha-admin/src/main/java/com/haha/admin/task/TimedDiscountTask.java

@@ -0,0 +1,48 @@
+package com.haha.admin.task;
+
+import com.haha.service.TimedDiscountService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class TimedDiscountTask {
+
+    private final TimedDiscountService timedDiscountService;
+
+    @Scheduled(cron = "0 0/5 * * * ?")
+    public void executeTimedDiscount() {
+        log.info("开始检查定时折扣活动执行");
+        try {
+            timedDiscountService.executeAllEnabledActivities();
+            log.info("定时折扣活动检查完成");
+        } catch (Exception e) {
+            log.error("定时折扣活动执行失败", e);
+        }
+    }
+
+    @Scheduled(cron = "0 0 6 * * ?")
+    public void restorePrices() {
+        log.info("开始执行价格恢复任务");
+        try {
+            timedDiscountService.restorePrices();
+            log.info("价格恢复任务执行完成");
+        } catch (Exception e) {
+            log.error("价格恢复任务执行失败", e);
+        }
+    }
+
+    @Scheduled(cron = "0 5 * * * ?")
+    public void updateSalesStatistics() {
+        log.info("开始执行销售统计更新任务");
+        try {
+            timedDiscountService.updateSalesStatistics();
+            log.info("销售统计更新任务执行完成");
+        } catch (Exception e) {
+            log.error("销售统计更新任务执行失败", e);
+        }
+    }
+}

+ 150 - 0
haha-admin/src/main/resources/sql/timed_discount.sql

@@ -0,0 +1,150 @@
+-- 定时折扣活动模块数据库表
+-- 创建时间:2026-03-19
+-- 功能说明:针对售卖机中白天未售罄的餐品(特别是自制餐品)在指定时间自动打折销售
+
+-- 1. 定时折扣活动配置表
+CREATE TABLE IF NOT EXISTS `t_timed_discount_activity` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_name` varchar(100) NOT NULL COMMENT '活动名称',
+  `activity_desc` varchar(500) DEFAULT NULL COMMENT '活动描述',
+  `start_time` time NOT NULL COMMENT '每日开始时间(如20:00:00)',
+  `end_time` time DEFAULT NULL COMMENT '每日结束时间(如23:59:59,NULL表示到次日营业开始)',
+  `discount_type` tinyint NOT NULL DEFAULT 1 COMMENT '折扣类型:1-固定折扣 2-阶梯折扣 3-固定价格',
+  `discount_value` decimal(10,2) NOT NULL COMMENT '折扣值(折扣比例如0.7表示7折,或固定价格)',
+  `min_discount_price` decimal(10,2) DEFAULT NULL COMMENT '最低折扣价(防止价格过低)',
+  `apply_scope` tinyint NOT NULL DEFAULT 1 COMMENT '适用范围:1-全部门店 2-指定门店',
+  `product_scope` tinyint NOT NULL DEFAULT 1 COMMENT '商品范围:1-全部商品 2-指定分类 3-指定商品',
+  `product_types` varchar(500) DEFAULT NULL COMMENT '适用商品分类(逗号分隔,如:自制餐品,盒饭)',
+  `priority_product_types` varchar(500) DEFAULT NULL COMMENT '优先商品分类(自制餐品等,逗号分隔)',
+  `min_stock` int NOT NULL DEFAULT 1 COMMENT '最低库存阈值(低于此数量不参与优惠)',
+  `max_stock` int DEFAULT NULL COMMENT '最高库存阈值(高于此数量不参与优惠,NULL不限制)',
+  `status` tinyint NOT NULL DEFAULT 1 COMMENT '状态:0-禁用 1-启用',
+  `auto_execute` tinyint NOT NULL DEFAULT 1 COMMENT '是否自动执行:0-否 1-是',
+  `notify_on_execute` tinyint NOT NULL DEFAULT 0 COMMENT '执行时是否通知:0-否 1-是',
+  `creator_id` bigint NOT NULL COMMENT '创建人ID',
+  `creator_name` varchar(50) DEFAULT NULL COMMENT '创建人姓名',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  `deleted` tinyint NOT NULL DEFAULT 0 COMMENT '删除标记:0-正常 1-已删除',
+  PRIMARY KEY (`id`),
+  KEY `idx_status` (`status`),
+  KEY `idx_time` (`start_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时折扣活动配置表';
+
+-- 2. 定时折扣活动门店关联表
+CREATE TABLE IF NOT EXISTS `t_timed_discount_shop` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_id` bigint NOT NULL COMMENT '活动ID',
+  `shop_id` bigint NOT NULL COMMENT '门店ID',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_activity_shop` (`activity_id`, `shop_id`),
+  KEY `idx_shop` (`shop_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时折扣活动门店关联表';
+
+-- 3. 定时折扣活动商品关联表
+CREATE TABLE IF NOT EXISTS `t_timed_discount_product` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_id` bigint NOT NULL COMMENT '活动ID',
+  `product_id` bigint NOT NULL COMMENT '商品ID',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_activity_product` (`activity_id`, `product_id`),
+  KEY `idx_product` (`product_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时折扣活动商品关联表';
+
+-- 4. 定时折扣执行记录表
+CREATE TABLE IF NOT EXISTS `t_timed_discount_record` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_id` bigint NOT NULL COMMENT '活动ID',
+  `activity_name` varchar(100) NOT NULL COMMENT '活动名称',
+  `execute_date` date NOT NULL COMMENT '执行日期',
+  `execute_time` datetime NOT NULL COMMENT '执行时间',
+  `shop_id` bigint DEFAULT NULL COMMENT '门店ID',
+  `shop_name` varchar(100) DEFAULT NULL COMMENT '门店名称',
+  `device_id` varchar(50) DEFAULT NULL COMMENT '设备ID',
+  `device_name` varchar(100) DEFAULT NULL COMMENT '设备名称',
+  `total_products` int NOT NULL DEFAULT 0 COMMENT '处理商品总数',
+  `priority_products` int NOT NULL DEFAULT 0 COMMENT '优先商品数(自制餐品等)',
+  `total_stock` int NOT NULL DEFAULT 0 COMMENT '总库存数量',
+  `original_value` decimal(12,2) NOT NULL DEFAULT 0 COMMENT '原价总价值',
+  `discount_value` decimal(12,2) NOT NULL DEFAULT 0 COMMENT '折扣后总价值',
+  `saved_value` decimal(12,2) NOT NULL DEFAULT 0 COMMENT '优惠金额',
+  `status` tinyint NOT NULL DEFAULT 1 COMMENT '执行状态:1-成功 2-部分成功 3-失败',
+  `error_message` varchar(500) DEFAULT NULL COMMENT '错误信息',
+  `execute_duration` int DEFAULT NULL COMMENT '执行耗时(毫秒)',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_activity` (`activity_id`),
+  KEY `idx_date` (`execute_date`),
+  KEY `idx_shop` (`shop_id`),
+  KEY `idx_device` (`device_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时折扣执行记录表';
+
+-- 5. 价格调整明细表
+CREATE TABLE IF NOT EXISTS `t_price_adjustment_log` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `record_id` bigint NOT NULL COMMENT '执行记录ID',
+  `activity_id` bigint NOT NULL COMMENT '活动ID',
+  `shop_id` bigint DEFAULT NULL COMMENT '门店ID',
+  `shop_name` varchar(100) DEFAULT NULL COMMENT '门店名称',
+  `device_id` varchar(50) NOT NULL COMMENT '设备ID',
+  `device_name` varchar(100) DEFAULT NULL COMMENT '设备名称',
+  `product_id` bigint NOT NULL COMMENT '商品ID',
+  `product_name` varchar(100) NOT NULL COMMENT '商品名称',
+  `product_type` varchar(50) DEFAULT NULL COMMENT '商品分类',
+  `stock` int NOT NULL COMMENT '当前库存',
+  `original_price` decimal(10,2) NOT NULL COMMENT '原价',
+  `discount_type` tinyint NOT NULL COMMENT '折扣类型',
+  `discount_value` decimal(10,2) NOT NULL COMMENT '折扣值',
+  `discounted_price` decimal(10,2) NOT NULL COMMENT '折扣后价格',
+  `is_priority` tinyint NOT NULL DEFAULT 0 COMMENT '是否优先商品(自制餐品等)',
+  `adjust_time` datetime NOT NULL COMMENT '调整时间',
+  `restore_time` datetime DEFAULT NULL COMMENT '恢复时间',
+  `restore_status` tinyint NOT NULL DEFAULT 0 COMMENT '恢复状态:0-未恢复 1-已恢复 2-已售出',
+  `sale_quantity` int NOT NULL DEFAULT 0 COMMENT '售出数量',
+  `sale_amount` decimal(12,2) DEFAULT NULL COMMENT '销售金额',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  KEY `idx_record` (`record_id`),
+  KEY `idx_activity` (`activity_id`),
+  KEY `idx_device_product` (`device_id`, `product_id`),
+  KEY `idx_adjust_time` (`adjust_time`),
+  KEY `idx_restore_status` (`restore_status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='价格调整明细表';
+
+-- 6. 定时折扣销售统计表
+CREATE TABLE IF NOT EXISTS `t_timed_discount_statistics` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `activity_id` bigint NOT NULL COMMENT '活动ID',
+  `stat_date` date NOT NULL COMMENT '统计日期',
+  `shop_id` bigint DEFAULT NULL COMMENT '门店ID',
+  `shop_name` varchar(100) DEFAULT NULL COMMENT '门店名称',
+  `total_adjusted_products` int NOT NULL DEFAULT 0 COMMENT '调价商品总数',
+  `priority_products` int NOT NULL DEFAULT 0 COMMENT '优先商品数(自制餐品)',
+  `total_adjusted_stock` int NOT NULL DEFAULT 0 COMMENT '调价库存总量',
+  `total_sold_quantity` int NOT NULL DEFAULT 0 COMMENT '售出总量',
+  `priority_sold_quantity` int NOT NULL DEFAULT 0 COMMENT '优先商品售出量',
+  `original_value` decimal(12,2) NOT NULL DEFAULT 0 COMMENT '原价总价值',
+  `actual_sales` decimal(12,2) NOT NULL DEFAULT 0 COMMENT '实际销售额',
+  `saved_amount` decimal(12,2) NOT NULL DEFAULT 0 COMMENT '优惠金额',
+  `waste_reduction_rate` decimal(5,2) DEFAULT NULL COMMENT '浪费减少率(%)',
+  `sell_through_rate` decimal(5,2) DEFAULT NULL COMMENT '售罄率(%)',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_activity_date_shop` (`activity_id`, `stat_date`, `shop_id`),
+  KEY `idx_stat_date` (`stat_date`),
+  KEY `idx_shop` (`shop_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='定时折扣销售统计表';
+
+-- 插入默认活动配置示例
+INSERT INTO `t_timed_discount_activity` 
+(`activity_name`, `activity_desc`, `start_time`, `end_time`, `discount_type`, `discount_value`, 
+ `min_discount_price`, `apply_scope`, `product_scope`, `product_types`, `priority_product_types`, 
+ `min_stock`, `status`, `auto_execute`, `creator_id`, `creator_name`)
+VALUES 
+('晚间餐品优惠', '每日晚间对未售罄餐品进行折扣销售,减少浪费', '20:00:00', '23:59:59', 1, 0.50, 
+ 1.00, 1, 2, '自制餐品,盒饭,水饮,副食', '自制餐品,盒饭', 
+ 1, 1, 1, 1, '系统管理员');

+ 60 - 0
haha-common/src/main/java/com/haha/common/enums/DiscountTypeEnum.java

@@ -0,0 +1,60 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum DiscountTypeEnum {
+
+    FIXED_DISCOUNT(1, "固定折扣", "primary"),
+    TIERED_DISCOUNT(2, "阶梯折扣", "success"),
+    FIXED_PRICE(3, "固定价格", "warning");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    DiscountTypeEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (DiscountTypeEnum type : values()) {
+            if (type.code == code) {
+                return type.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static DiscountTypeEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (DiscountTypeEnum type : values()) {
+            if (type.code == code) {
+                return type;
+            }
+        }
+        return null;
+    }
+}

+ 60 - 0
haha-common/src/main/java/com/haha/common/enums/ExecuteStatusEnum.java

@@ -0,0 +1,60 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum ExecuteStatusEnum {
+
+    SUCCESS(1, "成功", "success"),
+    PARTIAL_SUCCESS(2, "部分成功", "warning"),
+    FAILED(3, "失败", "danger");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    ExecuteStatusEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (ExecuteStatusEnum status : values()) {
+            if (status.code == code) {
+                return status.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static ExecuteStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (ExecuteStatusEnum status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 59 - 0
haha-common/src/main/java/com/haha/common/enums/NightDiscountStatusEnum.java

@@ -0,0 +1,59 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum NightDiscountStatusEnum {
+
+    DISABLED(0, "已禁用", "danger"),
+    ENABLED(1, "已启用", "success");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    NightDiscountStatusEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (NightDiscountStatusEnum status : values()) {
+            if (status.code == code) {
+                return status.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static NightDiscountStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (NightDiscountStatusEnum status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 60 - 0
haha-common/src/main/java/com/haha/common/enums/RestoreStatusEnum.java

@@ -0,0 +1,60 @@
+package com.haha.common.enums;
+
+import com.haha.common.vo.StatusLabel;
+
+public enum RestoreStatusEnum {
+
+    NOT_RESTORED(0, "未恢复", "warning"),
+    RESTORED(1, "已恢复", "success"),
+    SOLD_OUT(2, "已售出", "primary");
+
+    private final int code;
+    private final String label;
+    private final String color;
+
+    RestoreStatusEnum(int code, String label, String color) {
+        this.code = code;
+        this.label = label;
+        this.color = color;
+    }
+
+    public int getCode() {
+        return code;
+    }
+
+    public String getLabel() {
+        return label;
+    }
+
+    public String getColor() {
+        return color;
+    }
+
+    public StatusLabel getStatusLabel() {
+        return new StatusLabel(label, color);
+    }
+
+    public static StatusLabel getLabelByCode(Integer code) {
+        if (code == null) {
+            return StatusLabel.info("未知");
+        }
+        for (RestoreStatusEnum status : values()) {
+            if (status.code == code) {
+                return status.getStatusLabel();
+            }
+        }
+        return StatusLabel.info("未知");
+    }
+
+    public static RestoreStatusEnum getByCode(Integer code) {
+        if (code == null) {
+            return null;
+        }
+        for (RestoreStatusEnum status : values()) {
+            if (status.code == code) {
+                return status;
+            }
+        }
+        return null;
+    }
+}

+ 73 - 0
haha-entity/src/main/java/com/haha/entity/PriceAdjustmentLog.java

@@ -0,0 +1,73 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_price_adjustment_log")
+public class PriceAdjustmentLog implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long recordId;
+
+    private Long activityId;
+
+    private Long shopId;
+
+    private String shopName;
+
+    private String deviceId;
+
+    private String deviceName;
+
+    private Long productId;
+
+    private String productName;
+
+    private String productType;
+
+    private Integer stock;
+
+    private BigDecimal originalPrice;
+
+    private Integer discountType;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal discountedPrice;
+
+    private Integer isPriority;
+
+    private LocalDateTime adjustTime;
+
+    private LocalDateTime restoreTime;
+
+    private Integer restoreStatus;
+
+    private Integer saleQuantity;
+
+    private BigDecimal saleAmount;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    @TableField(exist = false)
+    private String restoreStatusLabel;
+
+    @TableField(exist = false)
+    private String restoreStatusColor;
+
+    @TableField(exist = false)
+    private String discountTypeLabel;
+}

+ 91 - 0
haha-entity/src/main/java/com/haha/entity/TimedDiscountActivity.java

@@ -0,0 +1,91 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.List;
+
+@Data
+@TableName("t_timed_discount_activity")
+public class TimedDiscountActivity implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String activityName;
+
+    private String activityDesc;
+
+    private LocalTime startTime;
+
+    private LocalTime endTime;
+
+    private Integer discountType;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal minDiscountPrice;
+
+    private Integer applyScope;
+
+    private Integer productScope;
+
+    private String productTypes;
+
+    private String priorityProductTypes;
+
+    private Integer minStock;
+
+    private Integer maxStock;
+
+    private Integer status;
+
+    private Integer autoExecute;
+
+    private Integer notifyOnExecute;
+
+    private Long creatorId;
+
+    private String creatorName;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    private Integer deleted;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String statusColor;
+
+    @TableField(exist = false)
+    private String discountTypeLabel;
+
+    @TableField(exist = false)
+    private String applyScopeLabel;
+
+    @TableField(exist = false)
+    private String productScopeLabel;
+
+    @TableField(exist = false)
+    private List<Long> shopIds;
+
+    @TableField(exist = false)
+    private List<Long> productIds;
+
+    @TableField(exist = false)
+    private List<String> productTypeList;
+
+    @TableField(exist = false)
+    private List<String> priorityProductTypeList;
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/TimedDiscountProduct.java

@@ -0,0 +1,24 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_timed_discount_product")
+public class TimedDiscountProduct implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long activityId;
+
+    private Long productId;
+
+    private LocalDateTime createTime;
+}

+ 63 - 0
haha-entity/src/main/java/com/haha/entity/TimedDiscountRecord.java

@@ -0,0 +1,63 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_timed_discount_record")
+public class TimedDiscountRecord implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long activityId;
+
+    private String activityName;
+
+    private LocalDate executeDate;
+
+    private LocalDateTime executeTime;
+
+    private Long shopId;
+
+    private String shopName;
+
+    private String deviceId;
+
+    private String deviceName;
+
+    private Integer totalProducts;
+
+    private Integer priorityProducts;
+
+    private Integer totalStock;
+
+    private BigDecimal originalValue;
+
+    private BigDecimal discountValue;
+
+    private BigDecimal savedValue;
+
+    private Integer status;
+
+    private String errorMessage;
+
+    private Integer executeDuration;
+
+    private LocalDateTime createTime;
+
+    @TableField(exist = false)
+    private String statusLabel;
+
+    @TableField(exist = false)
+    private String statusColor;
+}

+ 24 - 0
haha-entity/src/main/java/com/haha/entity/TimedDiscountShop.java

@@ -0,0 +1,24 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_timed_discount_shop")
+public class TimedDiscountShop implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long activityId;
+
+    private Long shopId;
+
+    private LocalDateTime createTime;
+}

+ 56 - 0
haha-entity/src/main/java/com/haha/entity/TimedDiscountStatistics.java

@@ -0,0 +1,56 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Data
+@TableName("t_timed_discount_statistics")
+public class TimedDiscountStatistics implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long activityId;
+
+    private LocalDate statDate;
+
+    private Long shopId;
+
+    private String shopName;
+
+    private Integer totalAdjustedProducts;
+
+    private Integer priorityProducts;
+
+    private Integer totalAdjustedStock;
+
+    private Integer totalSoldQuantity;
+
+    private Integer prioritySoldQuantity;
+
+    private BigDecimal originalValue;
+
+    private BigDecimal actualSales;
+
+    private BigDecimal savedAmount;
+
+    private BigDecimal wasteReductionRate;
+
+    private BigDecimal sellThroughRate;
+
+    private LocalDateTime createTime;
+
+    private LocalDateTime updateTime;
+
+    @TableField(exist = false)
+    private String activityName;
+}

+ 45 - 0
haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountCreateDTO.java

@@ -0,0 +1,45 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalTime;
+import java.util.List;
+
+@Data
+public class TimedDiscountCreateDTO {
+    
+    private String activityName;
+    
+    private String activityDesc;
+    
+    private LocalTime startTime;
+    
+    private LocalTime endTime;
+    
+    private Integer discountType;
+    
+    private BigDecimal discountValue;
+    
+    private BigDecimal minDiscountPrice;
+    
+    private Integer applyScope;
+    
+    private Integer productScope;
+    
+    private String productTypes;
+    
+    private String priorityProductTypes;
+    
+    private Integer minStock;
+    
+    private Integer maxStock;
+    
+    private Integer autoExecute;
+    
+    private Integer notifyOnExecute;
+    
+    private List<Long> shopIds;
+    
+    private List<Long> productIds;
+}

+ 15 - 0
haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountQueryDTO.java

@@ -0,0 +1,15 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TimedDiscountQueryDTO extends PageQueryDTO {
+    
+    private String activityName;
+    
+    private Integer status;
+    
+    private Integer discountType;
+}

+ 23 - 0
haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountRecordQueryDTO.java

@@ -0,0 +1,23 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.time.LocalDate;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class TimedDiscountRecordQueryDTO extends PageQueryDTO {
+    
+    private Long activityId;
+    
+    private LocalDate executeDateStart;
+    
+    private LocalDate executeDateEnd;
+    
+    private Long shopId;
+    
+    private String deviceId;
+    
+    private Integer status;
+}

+ 47 - 0
haha-entity/src/main/java/com/haha/entity/dto/TimedDiscountUpdateDTO.java

@@ -0,0 +1,47 @@
+package com.haha.entity.dto;
+
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalTime;
+import java.util.List;
+
+@Data
+public class TimedDiscountUpdateDTO {
+    
+    private Long id;
+    
+    private String activityName;
+    
+    private String activityDesc;
+    
+    private LocalTime startTime;
+    
+    private LocalTime endTime;
+    
+    private Integer discountType;
+    
+    private BigDecimal discountValue;
+    
+    private BigDecimal minDiscountPrice;
+    
+    private Integer applyScope;
+    
+    private Integer productScope;
+    
+    private String productTypes;
+    
+    private String priorityProductTypes;
+    
+    private Integer minStock;
+    
+    private Integer maxStock;
+    
+    private Integer autoExecute;
+    
+    private Integer notifyOnExecute;
+    
+    private List<Long> shopIds;
+    
+    private List<Long> productIds;
+}

+ 9 - 0
haha-mapper/src/main/java/com/haha/mapper/PriceAdjustmentLogMapper.java

@@ -0,0 +1,9 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.PriceAdjustmentLog;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface PriceAdjustmentLogMapper extends BaseMapper<PriceAdjustmentLog> {
+}

+ 9 - 0
haha-mapper/src/main/java/com/haha/mapper/TimedDiscountActivityMapper.java

@@ -0,0 +1,9 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.TimedDiscountActivity;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TimedDiscountActivityMapper extends BaseMapper<TimedDiscountActivity> {
+}

+ 9 - 0
haha-mapper/src/main/java/com/haha/mapper/TimedDiscountProductMapper.java

@@ -0,0 +1,9 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.TimedDiscountProduct;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TimedDiscountProductMapper extends BaseMapper<TimedDiscountProduct> {
+}

+ 9 - 0
haha-mapper/src/main/java/com/haha/mapper/TimedDiscountRecordMapper.java

@@ -0,0 +1,9 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.TimedDiscountRecord;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TimedDiscountRecordMapper extends BaseMapper<TimedDiscountRecord> {
+}

+ 9 - 0
haha-mapper/src/main/java/com/haha/mapper/TimedDiscountShopMapper.java

@@ -0,0 +1,9 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.TimedDiscountShop;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TimedDiscountShopMapper extends BaseMapper<TimedDiscountShop> {
+}

+ 9 - 0
haha-mapper/src/main/java/com/haha/mapper/TimedDiscountStatisticsMapper.java

@@ -0,0 +1,9 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.TimedDiscountStatistics;
+import org.apache.ibatis.annotations.Mapper;
+
+@Mapper
+public interface TimedDiscountStatisticsMapper extends BaseMapper<TimedDiscountStatistics> {
+}

+ 54 - 0
haha-service/src/main/java/com/haha/service/TimedDiscountService.java

@@ -0,0 +1,54 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.TimedDiscountActivity;
+import com.haha.entity.TimedDiscountRecord;
+import com.haha.entity.TimedDiscountStatistics;
+import com.haha.entity.PriceAdjustmentLog;
+import com.haha.entity.dto.TimedDiscountCreateDTO;
+import com.haha.entity.dto.TimedDiscountQueryDTO;
+import com.haha.entity.dto.TimedDiscountRecordQueryDTO;
+import com.haha.entity.dto.TimedDiscountUpdateDTO;
+
+import java.util.List;
+import java.util.Map;
+
+public interface TimedDiscountService extends IService<TimedDiscountActivity> {
+
+    IPage<TimedDiscountActivity> getPage(TimedDiscountQueryDTO queryDTO);
+
+    TimedDiscountActivity getDetail(Long id);
+
+    TimedDiscountActivity create(TimedDiscountCreateDTO dto, Long creatorId, String creatorName);
+
+    TimedDiscountActivity update(TimedDiscountUpdateDTO dto);
+
+    boolean updateStatus(Long id, Integer status);
+
+    boolean deleteActivity(Long id);
+
+    List<Long> getActivityShopIds(Long activityId);
+
+    List<Long> getActivityProductIds(Long activityId);
+
+    List<TimedDiscountActivity> getEnabledActivities();
+
+    IPage<TimedDiscountRecord> getRecordPage(TimedDiscountRecordQueryDTO queryDTO);
+
+    TimedDiscountRecord getRecordDetail(Long recordId);
+
+    IPage<PriceAdjustmentLog> getPriceAdjustmentLogs(Long recordId, int page, int pageSize);
+
+    IPage<TimedDiscountStatistics> getStatisticsPage(Long activityId, int page, int pageSize);
+
+    Map<String, Object> getActivityStatistics(Long activityId);
+
+    void executeDiscountActivity(Long activityId);
+
+    void executeAllEnabledActivities();
+
+    void restorePrices();
+
+    void updateSalesStatistics();
+}

+ 666 - 0
haha-service/src/main/java/com/haha/service/impl/TimedDiscountServiceImpl.java

@@ -0,0 +1,666 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.enums.*;
+import com.haha.common.exception.BusinessException;
+import com.haha.common.vo.StatusLabel;
+import com.haha.entity.*;
+import com.haha.entity.dto.TimedDiscountCreateDTO;
+import com.haha.entity.dto.TimedDiscountQueryDTO;
+import com.haha.entity.dto.TimedDiscountRecordQueryDTO;
+import com.haha.entity.dto.TimedDiscountUpdateDTO;
+import com.haha.mapper.*;
+import com.haha.service.TimedDiscountService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TimedDiscountServiceImpl extends ServiceImpl<TimedDiscountActivityMapper, TimedDiscountActivity> implements TimedDiscountService {
+
+    private final TimedDiscountActivityMapper activityMapper;
+    private final TimedDiscountShopMapper discountShopMapper;
+    private final TimedDiscountProductMapper discountProductMapper;
+    private final TimedDiscountRecordMapper recordMapper;
+    private final PriceAdjustmentLogMapper priceAdjustmentLogMapper;
+    private final TimedDiscountStatisticsMapper statisticsMapper;
+    private final DeviceInventoryMapper deviceInventoryMapper;
+    private final ProductMapper productMapper;
+    private final DeviceMapper deviceMapper;
+    private final ShopMapper shopMapper;
+
+    @Override
+    public IPage<TimedDiscountActivity> getPage(TimedDiscountQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<TimedDiscountActivity> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<TimedDiscountActivity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StringUtils.hasText(queryDTO.getActivityName()), TimedDiscountActivity::getActivityName, queryDTO.getActivityName());
+        wrapper.eq(queryDTO.getStatus() != null, TimedDiscountActivity::getStatus, queryDTO.getStatus());
+        wrapper.eq(queryDTO.getDiscountType() != null, TimedDiscountActivity::getDiscountType, queryDTO.getDiscountType());
+        wrapper.eq(TimedDiscountActivity::getDeleted, 0);
+        wrapper.orderByDesc(TimedDiscountActivity::getCreateTime);
+
+        IPage<TimedDiscountActivity> result = this.page(page, wrapper);
+        result.getRecords().forEach(this::fillLabels);
+
+        return result;
+    }
+
+    @Override
+    public TimedDiscountActivity getDetail(Long id) {
+        TimedDiscountActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+        fillLabels(activity);
+        activity.setShopIds(getActivityShopIds(id));
+        activity.setProductIds(getActivityProductIds(id));
+        
+        if (StringUtils.hasText(activity.getProductTypes())) {
+            activity.setProductTypeList(Arrays.asList(activity.getProductTypes().split(",")));
+        }
+        if (StringUtils.hasText(activity.getPriorityProductTypes())) {
+            activity.setPriorityProductTypeList(Arrays.asList(activity.getPriorityProductTypes().split(",")));
+        }
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public TimedDiscountActivity create(TimedDiscountCreateDTO dto, Long creatorId, String creatorName) {
+        TimedDiscountActivity activity = new TimedDiscountActivity();
+        activity.setActivityName(dto.getActivityName());
+        activity.setActivityDesc(dto.getActivityDesc());
+        activity.setStartTime(dto.getStartTime());
+        activity.setEndTime(dto.getEndTime());
+        activity.setDiscountType(dto.getDiscountType());
+        activity.setDiscountValue(dto.getDiscountValue());
+        activity.setMinDiscountPrice(dto.getMinDiscountPrice());
+        activity.setApplyScope(dto.getApplyScope());
+        activity.setProductScope(dto.getProductScope());
+        activity.setProductTypes(dto.getProductTypes());
+        activity.setPriorityProductTypes(dto.getPriorityProductTypes());
+        activity.setMinStock(dto.getMinStock() != null ? dto.getMinStock() : 1);
+        activity.setMaxStock(dto.getMaxStock());
+        activity.setStatus(NightDiscountStatusEnum.ENABLED.getCode());
+        activity.setAutoExecute(dto.getAutoExecute() != null ? dto.getAutoExecute() : 1);
+        activity.setNotifyOnExecute(dto.getNotifyOnExecute() != null ? dto.getNotifyOnExecute() : 0);
+        activity.setCreatorId(creatorId);
+        activity.setCreatorName(creatorName);
+        activity.setDeleted(0);
+
+        this.save(activity);
+
+        saveActivityShops(activity.getId(), dto.getShopIds(), dto.getApplyScope());
+        saveActivityProducts(activity.getId(), dto.getProductIds(), dto.getProductScope());
+
+        log.info("创建定时折扣活动成功: id={}, name={}", activity.getId(), activity.getActivityName());
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public TimedDiscountActivity update(TimedDiscountUpdateDTO dto) {
+        TimedDiscountActivity activity = this.getById(dto.getId());
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        activity.setActivityName(dto.getActivityName());
+        activity.setActivityDesc(dto.getActivityDesc());
+        activity.setStartTime(dto.getStartTime());
+        activity.setEndTime(dto.getEndTime());
+        activity.setDiscountType(dto.getDiscountType());
+        activity.setDiscountValue(dto.getDiscountValue());
+        activity.setMinDiscountPrice(dto.getMinDiscountPrice());
+        activity.setApplyScope(dto.getApplyScope());
+        activity.setProductScope(dto.getProductScope());
+        activity.setProductTypes(dto.getProductTypes());
+        activity.setPriorityProductTypes(dto.getPriorityProductTypes());
+        activity.setMinStock(dto.getMinStock());
+        activity.setMaxStock(dto.getMaxStock());
+        activity.setAutoExecute(dto.getAutoExecute());
+        activity.setNotifyOnExecute(dto.getNotifyOnExecute());
+
+        this.updateById(activity);
+
+        deleteActivityShops(activity.getId());
+        deleteActivityProducts(activity.getId());
+        saveActivityShops(activity.getId(), dto.getShopIds(), dto.getApplyScope());
+        saveActivityProducts(activity.getId(), dto.getProductIds(), dto.getProductScope());
+
+        log.info("更新定时折扣活动成功: id={}", activity.getId());
+        return activity;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateStatus(Long id, Integer status) {
+        TimedDiscountActivity activity = this.getById(id);
+        if (activity == null || activity.getDeleted() == 1) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        activity.setStatus(status);
+        boolean result = this.updateById(activity);
+
+        if (result) {
+            log.info("更新定时折扣活动状态成功: id={}, status={}", id, status);
+        }
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteActivity(Long id) {
+        TimedDiscountActivity activity = this.getById(id);
+        if (activity == null) {
+            throw new BusinessException(404, "活动不存在");
+        }
+
+        activity.setDeleted(1);
+        boolean result = this.updateById(activity);
+
+        if (result) {
+            deleteActivityShops(id);
+            deleteActivityProducts(id);
+            log.info("删除定时折扣活动成功: id={}", id);
+        }
+        return result;
+    }
+
+    @Override
+    public List<Long> getActivityShopIds(Long activityId) {
+        LambdaQueryWrapper<TimedDiscountShop> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountShop::getActivityId, activityId);
+        return discountShopMapper.selectList(wrapper).stream()
+                .map(TimedDiscountShop::getShopId)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<Long> getActivityProductIds(Long activityId) {
+        LambdaQueryWrapper<TimedDiscountProduct> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountProduct::getActivityId, activityId);
+        return discountProductMapper.selectList(wrapper).stream()
+                .map(TimedDiscountProduct::getProductId)
+                .collect(Collectors.toList());
+    }
+
+    @Override
+    public List<TimedDiscountActivity> getEnabledActivities() {
+        LambdaQueryWrapper<TimedDiscountActivity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountActivity::getStatus, NightDiscountStatusEnum.ENABLED.getCode());
+        wrapper.eq(TimedDiscountActivity::getDeleted, 0);
+        wrapper.eq(TimedDiscountActivity::getAutoExecute, 1);
+        return this.list(wrapper);
+    }
+
+    @Override
+    public IPage<TimedDiscountRecord> getRecordPage(TimedDiscountRecordQueryDTO queryDTO) {
+        queryDTO.validate();
+        Page<TimedDiscountRecord> page = new Page<>(queryDTO.getPage(), queryDTO.getPageSize());
+
+        LambdaQueryWrapper<TimedDiscountRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(queryDTO.getActivityId() != null, TimedDiscountRecord::getActivityId, queryDTO.getActivityId());
+        wrapper.ge(queryDTO.getExecuteDateStart() != null, TimedDiscountRecord::getExecuteDate, queryDTO.getExecuteDateStart());
+        wrapper.le(queryDTO.getExecuteDateEnd() != null, TimedDiscountRecord::getExecuteDate, queryDTO.getExecuteDateEnd());
+        wrapper.eq(queryDTO.getShopId() != null, TimedDiscountRecord::getShopId, queryDTO.getShopId());
+        wrapper.eq(StringUtils.hasText(queryDTO.getDeviceId()), TimedDiscountRecord::getDeviceId, queryDTO.getDeviceId());
+        wrapper.eq(queryDTO.getStatus() != null, TimedDiscountRecord::getStatus, queryDTO.getStatus());
+        wrapper.orderByDesc(TimedDiscountRecord::getExecuteTime);
+
+        IPage<TimedDiscountRecord> result = recordMapper.selectPage(page, wrapper);
+        result.getRecords().forEach(this::fillRecordLabels);
+
+        return result;
+    }
+
+    @Override
+    public TimedDiscountRecord getRecordDetail(Long recordId) {
+        TimedDiscountRecord record = recordMapper.selectById(recordId);
+        if (record != null) {
+            fillRecordLabels(record);
+        }
+        return record;
+    }
+
+    @Override
+    public IPage<PriceAdjustmentLog> getPriceAdjustmentLogs(Long recordId, int page, int pageSize) {
+        Page<PriceAdjustmentLog> pageObj = new Page<>(page, pageSize);
+        LambdaQueryWrapper<PriceAdjustmentLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PriceAdjustmentLog::getRecordId, recordId);
+        wrapper.orderByAsc(PriceAdjustmentLog::getAdjustTime);
+
+        IPage<PriceAdjustmentLog> result = priceAdjustmentLogMapper.selectPage(pageObj, wrapper);
+        result.getRecords().forEach(this::fillPriceLogLabels);
+
+        return result;
+    }
+
+    @Override
+    public IPage<TimedDiscountStatistics> getStatisticsPage(Long activityId, int page, int pageSize) {
+        Page<TimedDiscountStatistics> pageObj = new Page<>(page, pageSize);
+        LambdaQueryWrapper<TimedDiscountStatistics> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountStatistics::getActivityId, activityId);
+        wrapper.orderByDesc(TimedDiscountStatistics::getStatDate);
+
+        return statisticsMapper.selectPage(pageObj, wrapper);
+    }
+
+    @Override
+    public Map<String, Object> getActivityStatistics(Long activityId) {
+        Map<String, Object> stats = new HashMap<>();
+        
+        LambdaQueryWrapper<TimedDiscountStatistics> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountStatistics::getActivityId, activityId);
+        List<TimedDiscountStatistics> statList = statisticsMapper.selectList(wrapper);
+
+        int totalAdjustedProducts = statList.stream().mapToInt(TimedDiscountStatistics::getTotalAdjustedProducts).sum();
+        int totalSoldQuantity = statList.stream().mapToInt(TimedDiscountStatistics::getTotalSoldQuantity).sum();
+        BigDecimal totalOriginalValue = statList.stream().map(TimedDiscountStatistics::getOriginalValue).reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalActualSales = statList.stream().map(TimedDiscountStatistics::getActualSales).reduce(BigDecimal.ZERO, BigDecimal::add);
+        BigDecimal totalSavedAmount = statList.stream().map(TimedDiscountStatistics::getSavedAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        stats.put("totalAdjustedProducts", totalAdjustedProducts);
+        stats.put("totalSoldQuantity", totalSoldQuantity);
+        stats.put("totalOriginalValue", totalOriginalValue);
+        stats.put("totalActualSales", totalActualSales);
+        stats.put("totalSavedAmount", totalSavedAmount);
+        stats.put("executionCount", statList.size());
+
+        return stats;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void executeDiscountActivity(Long activityId) {
+        TimedDiscountActivity activity = this.getById(activityId);
+        if (activity == null || activity.getStatus() != NightDiscountStatusEnum.ENABLED.getCode()) {
+            log.warn("活动不存在或未启用: activityId={}", activityId);
+            return;
+        }
+
+        LocalTime now = LocalTime.now();
+        if (now.isBefore(activity.getStartTime())) {
+            log.info("未到活动开始时间: activityId={}, startTime={}", activityId, activity.getStartTime());
+            return;
+        }
+
+        long startTime = System.currentTimeMillis();
+        log.info("开始执行定时折扣活动: activityId={}, name={}", activityId, activity.getActivityName());
+
+        try {
+            List<Long> shopIds = getTargetShopIds(activity);
+            List<String> productTypes = getProductTypes(activity);
+            List<String> priorityTypes = getPriorityProductTypes(activity);
+
+            int totalProducts = 0;
+            int priorityProducts = 0;
+            int totalStock = 0;
+            BigDecimal originalValue = BigDecimal.ZERO;
+            BigDecimal discountValue = BigDecimal.ZERO;
+
+            for (Long shopId : shopIds) {
+                Shop shop = shopMapper.selectById(shopId);
+                String shopName = shop != null ? shop.getName() : "";
+
+                List<Device> devices = getDevicesByShop(shopId);
+                for (Device device : devices) {
+                    List<Map<String, Object>> inventoryList = getEligibleInventory(device.getDeviceId(), activity, productTypes);
+
+                    for (Map<String, Object> inventory : inventoryList) {
+                        Long productId = (Long) inventory.get("productId");
+                        Integer stock = (Integer) inventory.get("stock");
+                        Product product = productMapper.selectById(productId);
+
+                        if (product == null || product.getRetailPrice() == null) {
+                            continue;
+                        }
+
+                        BigDecimal originalPrice = BigDecimal.valueOf(product.getRetailPrice());
+                        BigDecimal discountedPrice = calculateDiscountedPrice(originalPrice, activity);
+                        boolean isPriority = isPriorityProduct(product.getType(), priorityTypes);
+
+                        PriceAdjustmentLog logEntry = new PriceAdjustmentLog();
+                        logEntry.setActivityId(activityId);
+                        logEntry.setShopId(shopId);
+                        logEntry.setShopName(shopName);
+                        logEntry.setDeviceId(device.getDeviceId());
+                        logEntry.setDeviceName(device.getName());
+                        logEntry.setProductId(productId);
+                        logEntry.setProductName(product.getName());
+                        logEntry.setProductType(product.getType());
+                        logEntry.setStock(stock);
+                        logEntry.setOriginalPrice(originalPrice);
+                        logEntry.setDiscountType(activity.getDiscountType());
+                        logEntry.setDiscountValue(activity.getDiscountValue());
+                        logEntry.setDiscountedPrice(discountedPrice);
+                        logEntry.setIsPriority(isPriority ? 1 : 0);
+                        logEntry.setAdjustTime(LocalDateTime.now());
+                        logEntry.setRestoreStatus(RestoreStatusEnum.NOT_RESTORED.getCode());
+                        logEntry.setSaleQuantity(0);
+
+                        priceAdjustmentLogMapper.insert(logEntry);
+
+                        totalProducts++;
+                        if (isPriority) {
+                            priorityProducts++;
+                        }
+                        totalStock += stock;
+                        originalValue = originalValue.add(originalPrice.multiply(BigDecimal.valueOf(stock)));
+                        discountValue = discountValue.add(discountedPrice.multiply(BigDecimal.valueOf(stock)));
+                    }
+                }
+            }
+
+            TimedDiscountRecord record = new TimedDiscountRecord();
+            record.setActivityId(activityId);
+            record.setActivityName(activity.getActivityName());
+            record.setExecuteDate(LocalDate.now());
+            record.setExecuteTime(LocalDateTime.now());
+            record.setTotalProducts(totalProducts);
+            record.setPriorityProducts(priorityProducts);
+            record.setTotalStock(totalStock);
+            record.setOriginalValue(originalValue);
+            record.setDiscountValue(discountValue);
+            record.setSavedValue(originalValue.subtract(discountValue));
+            record.setStatus(ExecuteStatusEnum.SUCCESS.getCode());
+            record.setExecuteDuration((int) (System.currentTimeMillis() - startTime));
+
+            recordMapper.insert(record);
+
+            log.info("定时折扣活动执行完成: activityId={}, totalProducts={}, savedValue={}", 
+                    activityId, totalProducts, record.getSavedValue());
+
+        } catch (Exception e) {
+            log.error("定时折扣活动执行失败: activityId={}", activityId, e);
+            
+            TimedDiscountRecord record = new TimedDiscountRecord();
+            record.setActivityId(activityId);
+            record.setActivityName(activity.getActivityName());
+            record.setExecuteDate(LocalDate.now());
+            record.setExecuteTime(LocalDateTime.now());
+            record.setStatus(ExecuteStatusEnum.FAILED.getCode());
+            record.setErrorMessage(e.getMessage());
+            record.setExecuteDuration((int) (System.currentTimeMillis() - startTime));
+            recordMapper.insert(record);
+        }
+    }
+
+    @Override
+    public void executeAllEnabledActivities() {
+        List<TimedDiscountActivity> activities = getEnabledActivities();
+        for (TimedDiscountActivity activity : activities) {
+            try {
+                executeDiscountActivity(activity.getId());
+            } catch (Exception e) {
+                log.error("执行定时折扣活动异常: activityId={}", activity.getId(), e);
+            }
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void restorePrices() {
+        log.info("开始恢复商品价格");
+
+        LambdaQueryWrapper<PriceAdjustmentLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PriceAdjustmentLog::getRestoreStatus, RestoreStatusEnum.NOT_RESTORED.getCode());
+        wrapper.lt(PriceAdjustmentLog::getAdjustTime, LocalDateTime.now().minusHours(12));
+
+        List<PriceAdjustmentLog> logs = priceAdjustmentLogMapper.selectList(wrapper);
+        for (PriceAdjustmentLog logEntry : logs) {
+            logEntry.setRestoreStatus(RestoreStatusEnum.RESTORED.getCode());
+            logEntry.setRestoreTime(LocalDateTime.now());
+            priceAdjustmentLogMapper.updateById(logEntry);
+        }
+
+        log.info("商品价格恢复完成: count={}", logs.size());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void updateSalesStatistics() {
+        log.info("开始更新销售统计");
+
+        LocalDate today = LocalDate.now();
+        LambdaQueryWrapper<TimedDiscountRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountRecord::getExecuteDate, today);
+        List<TimedDiscountRecord> records = recordMapper.selectList(wrapper);
+
+        for (TimedDiscountRecord record : records) {
+            updateRecordStatistics(record);
+        }
+
+        log.info("销售统计更新完成");
+    }
+
+    private void updateRecordStatistics(TimedDiscountRecord record) {
+        LambdaQueryWrapper<PriceAdjustmentLog> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(PriceAdjustmentLog::getRecordId, record.getId());
+        List<PriceAdjustmentLog> logs = priceAdjustmentLogMapper.selectList(wrapper);
+
+        int totalSold = logs.stream().mapToInt(PriceAdjustmentLog::getSaleQuantity).sum();
+        BigDecimal totalSales = logs.stream()
+                .filter(l -> l.getSaleAmount() != null)
+                .map(PriceAdjustmentLog::getSaleAmount)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+
+        TimedDiscountStatistics stats = new TimedDiscountStatistics();
+        stats.setActivityId(record.getActivityId());
+        stats.setStatDate(record.getExecuteDate());
+        stats.setShopId(record.getShopId());
+        stats.setShopName(record.getShopName());
+        stats.setTotalAdjustedProducts(record.getTotalProducts());
+        stats.setPriorityProducts(record.getPriorityProducts());
+        stats.setTotalAdjustedStock(record.getTotalStock());
+        stats.setTotalSoldQuantity(totalSold);
+        stats.setOriginalValue(record.getOriginalValue());
+        stats.setActualSales(totalSales);
+        stats.setSavedAmount(record.getSavedValue());
+
+        if (record.getTotalStock() > 0) {
+            BigDecimal sellThroughRate = BigDecimal.valueOf(totalSold)
+                    .divide(BigDecimal.valueOf(record.getTotalStock()), 4, RoundingMode.HALF_UP)
+                    .multiply(BigDecimal.valueOf(100));
+            stats.setSellThroughRate(sellThroughRate);
+        }
+
+        LambdaQueryWrapper<TimedDiscountStatistics> existWrapper = new LambdaQueryWrapper<>();
+        existWrapper.eq(TimedDiscountStatistics::getActivityId, record.getActivityId());
+        existWrapper.eq(TimedDiscountStatistics::getStatDate, record.getExecuteDate());
+        existWrapper.eq(TimedDiscountStatistics::getShopId, record.getShopId());
+        TimedDiscountStatistics exist = statisticsMapper.selectOne(existWrapper);
+
+        if (exist != null) {
+            stats.setId(exist.getId());
+            statisticsMapper.updateById(stats);
+        } else {
+            statisticsMapper.insert(stats);
+        }
+    }
+
+    private List<Long> getTargetShopIds(TimedDiscountActivity activity) {
+        if (activity.getApplyScope() == ApplyScopeEnum.ALL_SHOP.getCode()) {
+            LambdaQueryWrapper<Shop> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(Shop::getStatus, 1);
+            return shopMapper.selectList(wrapper).stream().map(Shop::getId).collect(Collectors.toList());
+        }
+        return getActivityShopIds(activity.getId());
+    }
+
+    private List<String> getProductTypes(TimedDiscountActivity activity) {
+        if (activity.getProductScope() == 1) {
+            return null;
+        }
+        if (StringUtils.hasText(activity.getProductTypes())) {
+            return Arrays.asList(activity.getProductTypes().split(","));
+        }
+        return Collections.emptyList();
+    }
+
+    private List<String> getPriorityProductTypes(TimedDiscountActivity activity) {
+        if (StringUtils.hasText(activity.getPriorityProductTypes())) {
+            return Arrays.asList(activity.getPriorityProductTypes().split(","));
+        }
+        return Collections.emptyList();
+    }
+
+    private List<Device> getDevicesByShop(Long shopId) {
+        LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Device::getShopId, shopId);
+        return deviceMapper.selectList(wrapper);
+    }
+
+    private List<Map<String, Object>> getEligibleInventory(String deviceId, TimedDiscountActivity activity, List<String> productTypes) {
+        LambdaQueryWrapper<DeviceInventory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(DeviceInventory::getDeviceId, deviceId);
+        wrapper.ge(DeviceInventory::getStock, activity.getMinStock());
+        if (activity.getMaxStock() != null) {
+            wrapper.le(DeviceInventory::getStock, activity.getMaxStock());
+        }
+
+        List<DeviceInventory> inventoryList = deviceInventoryMapper.selectList(wrapper);
+        List<Map<String, Object>> result = new ArrayList<>();
+
+        for (DeviceInventory inventory : inventoryList) {
+            Product product = productMapper.selectById(inventory.getProductId());
+            if (product == null) {
+                continue;
+            }
+
+            if (productTypes != null && !productTypes.isEmpty()) {
+                if (!productTypes.contains(product.getType())) {
+                    continue;
+                }
+            }
+
+            Map<String, Object> map = new HashMap<>();
+            map.put("productId", inventory.getProductId());
+            map.put("stock", inventory.getStock());
+            result.add(map);
+        }
+
+        return result;
+    }
+
+    private BigDecimal calculateDiscountedPrice(BigDecimal originalPrice, TimedDiscountActivity activity) {
+        BigDecimal discountedPrice;
+
+        if (activity.getDiscountType() == DiscountTypeEnum.FIXED_DISCOUNT.getCode()) {
+            discountedPrice = originalPrice.multiply(activity.getDiscountValue());
+        } else if (activity.getDiscountType() == DiscountTypeEnum.FIXED_PRICE.getCode()) {
+            discountedPrice = activity.getDiscountValue();
+        } else {
+            discountedPrice = originalPrice.multiply(activity.getDiscountValue());
+        }
+
+        discountedPrice = discountedPrice.setScale(2, RoundingMode.HALF_UP);
+
+        if (activity.getMinDiscountPrice() != null) {
+            if (discountedPrice.compareTo(activity.getMinDiscountPrice()) < 0) {
+                discountedPrice = activity.getMinDiscountPrice();
+            }
+        }
+
+        return discountedPrice;
+    }
+
+    private boolean isPriorityProduct(String productType, List<String> priorityTypes) {
+        if (priorityTypes == null || priorityTypes.isEmpty()) {
+            return false;
+        }
+        return priorityTypes.contains(productType);
+    }
+
+    private void saveActivityShops(Long activityId, List<Long> shopIds, Integer applyScope) {
+        if (applyScope != null && applyScope == ApplyScopeEnum.SPECIFIED_SHOP.getCode()
+                && shopIds != null && !shopIds.isEmpty()) {
+            for (Long shopId : shopIds) {
+                TimedDiscountShop discountShop = new TimedDiscountShop();
+                discountShop.setActivityId(activityId);
+                discountShop.setShopId(shopId);
+                discountShopMapper.insert(discountShop);
+            }
+        }
+    }
+
+    private void saveActivityProducts(Long activityId, List<Long> productIds, Integer productScope) {
+        if (productScope != null && productScope == 3
+                && productIds != null && !productIds.isEmpty()) {
+            for (Long productId : productIds) {
+                TimedDiscountProduct discountProduct = new TimedDiscountProduct();
+                discountProduct.setActivityId(activityId);
+                discountProduct.setProductId(productId);
+                discountProductMapper.insert(discountProduct);
+            }
+        }
+    }
+
+    private void deleteActivityShops(Long activityId) {
+        LambdaQueryWrapper<TimedDiscountShop> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountShop::getActivityId, activityId);
+        discountShopMapper.delete(wrapper);
+    }
+
+    private void deleteActivityProducts(Long activityId) {
+        LambdaQueryWrapper<TimedDiscountProduct> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TimedDiscountProduct::getActivityId, activityId);
+        discountProductMapper.delete(wrapper);
+    }
+
+    private void fillLabels(TimedDiscountActivity activity) {
+        StatusLabel statusLabel = NightDiscountStatusEnum.getLabelByCode(activity.getStatus());
+        activity.setStatusLabel(statusLabel.getLabel());
+        activity.setStatusColor(statusLabel.getColor());
+
+        StatusLabel discountTypeLabel = DiscountTypeEnum.getLabelByCode(activity.getDiscountType());
+        activity.setDiscountTypeLabel(discountTypeLabel.getLabel());
+
+        StatusLabel applyScopeLabel = ApplyScopeEnum.getLabelByCode(activity.getApplyScope());
+        activity.setApplyScopeLabel(applyScopeLabel.getLabel());
+
+        if (activity.getProductScope() != null) {
+            if (activity.getProductScope() == 1) {
+                activity.setProductScopeLabel("全部商品");
+            } else if (activity.getProductScope() == 2) {
+                activity.setProductScopeLabel("指定分类");
+            } else {
+                activity.setProductScopeLabel("指定商品");
+            }
+        }
+    }
+
+    private void fillRecordLabels(TimedDiscountRecord record) {
+        StatusLabel statusLabel = ExecuteStatusEnum.getLabelByCode(record.getStatus());
+        record.setStatusLabel(statusLabel.getLabel());
+        record.setStatusColor(statusLabel.getColor());
+    }
+
+    private void fillPriceLogLabels(PriceAdjustmentLog log) {
+        StatusLabel restoreStatusLabel = RestoreStatusEnum.getLabelByCode(log.getRestoreStatus());
+        log.setRestoreStatusLabel(restoreStatusLabel.getLabel());
+        log.setRestoreStatusColor(restoreStatusLabel.getColor());
+
+        StatusLabel discountTypeLabel = DiscountTypeEnum.getLabelByCode(log.getDiscountType());
+        log.setDiscountTypeLabel(discountTypeLabel.getLabel());
+    }
+}