Browse Source

日期格式优化,开关门记录

skyline 1 month ago
parent
commit
ad75507725
53 changed files with 1366 additions and 60 deletions
  1. 21 0
      haha-admin-web/src/api/device.ts
  2. 36 1
      haha-admin-web/src/views/device/index.vue
  3. 222 14
      haha-admin-web/src/views/device/utils/hook.tsx
  4. 56 0
      haha-admin/src/main/java/com/haha/admin/config/JacksonConfig.java
  5. 1 1
      haha-admin/src/main/java/com/haha/admin/config/WebMvcConfig.java
  6. 20 1
      haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java
  7. 21 0
      haha-common/pom.xml
  8. 3 0
      haha-common/src/main/java/com/haha/common/vo/CouponTemplateVO.java
  9. 3 0
      haha-common/src/main/java/com/haha/common/vo/CouponVO.java
  10. 3 0
      haha-common/src/main/java/com/haha/common/vo/OrderVO.java
  11. 6 0
      haha-entity/pom.xml
  12. 3 0
      haha-entity/src/main/java/com/haha/entity/Account.java
  13. 4 0
      haha-entity/src/main/java/com/haha/entity/Admin.java
  14. 5 0
      haha-entity/src/main/java/com/haha/entity/Announcement.java
  15. 5 0
      haha-entity/src/main/java/com/haha/entity/CheckinRecord.java
  16. 5 0
      haha-entity/src/main/java/com/haha/entity/CouponTemplate.java
  17. 4 0
      haha-entity/src/main/java/com/haha/entity/Device.java
  18. 3 0
      haha-entity/src/main/java/com/haha/entity/DictData.java
  19. 2 0
      haha-entity/src/main/java/com/haha/entity/DictLog.java
  20. 3 0
      haha-entity/src/main/java/com/haha/entity/DictType.java
  21. 100 0
      haha-entity/src/main/java/com/haha/entity/DoorRecord.java
  22. 3 1
      haha-entity/src/main/java/com/haha/entity/FundLog.java
  23. 2 0
      haha-entity/src/main/java/com/haha/entity/InventoryLog.java
  24. 5 0
      haha-entity/src/main/java/com/haha/entity/MarketingActivity.java
  25. 5 0
      haha-entity/src/main/java/com/haha/entity/NewProductApply.java
  26. 2 0
      haha-entity/src/main/java/com/haha/entity/OperationLog.java
  27. 6 0
      haha-entity/src/main/java/com/haha/entity/Order.java
  28. 3 0
      haha-entity/src/main/java/com/haha/entity/OrderItem.java
  29. 5 0
      haha-entity/src/main/java/com/haha/entity/PriceAdjustmentLog.java
  30. 3 0
      haha-entity/src/main/java/com/haha/entity/Role.java
  31. 3 0
      haha-entity/src/main/java/com/haha/entity/Shop.java
  32. 4 0
      haha-entity/src/main/java/com/haha/entity/StatCategoryDaily.java
  33. 4 0
      haha-entity/src/main/java/com/haha/entity/StatDeviceDaily.java
  34. 4 0
      haha-entity/src/main/java/com/haha/entity/StatProductDaily.java
  35. 4 0
      haha-entity/src/main/java/com/haha/entity/StatShopDaily.java
  36. 6 0
      haha-entity/src/main/java/com/haha/entity/StatUserRepurchase.java
  37. 5 0
      haha-entity/src/main/java/com/haha/entity/StockRecord.java
  38. 2 0
      haha-entity/src/main/java/com/haha/entity/SyncLog.java
  39. 4 0
      haha-entity/src/main/java/com/haha/entity/SyncRecord.java
  40. 5 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountActivity.java
  41. 4 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountRecord.java
  42. 4 0
      haha-entity/src/main/java/com/haha/entity/TimedDiscountStatistics.java
  43. 3 0
      haha-entity/src/main/java/com/haha/entity/User.java
  44. 7 0
      haha-entity/src/main/java/com/haha/entity/UserCoupon.java
  45. 85 0
      haha-mapper/src/main/java/com/haha/mapper/DoorRecordMapper.java
  46. 69 0
      haha-mapper/src/main/resources/mapper/DoorRecordMapper.xml
  47. 56 0
      haha-miniapp/src/main/java/com/haha/miniapp/config/JacksonConfig.java
  48. 33 0
      haha-miniapp/src/main/resources/sql/door_record.sql
  49. 27 5
      haha-sdk/src/main/java/com/haha/sdk/api/DeviceApi.java
  50. 107 0
      haha-service/src/main/java/com/haha/service/DoorRecordService.java
  51. 91 35
      haha-service/src/main/java/com/haha/service/impl/DeviceServiceImpl.java
  52. 234 0
      haha-service/src/main/java/com/haha/service/impl/DoorRecordServiceImpl.java
  53. 45 2
      haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

+ 21 - 0
haha-admin-web/src/api/device.ts

@@ -47,3 +47,24 @@ export const setTemperature = (id: number, data: { temperature: number }) => {
 export const setVolume = (id: number, data: { volume: number }) => {
   return http.request<Result>("put", `/devices/${id}/volume`, { data });
 };
+
+export interface DoorRecordItem {
+  id: number;
+  activityId: string;
+  deviceId: string;
+  userId: number;
+  doorIndex?: string;
+  openType: string;
+  doorStatus: string;
+  nobuy: number;
+  orderId?: number;
+  openTime: string;
+  closeTime?: string;
+  duration?: number;
+  source?: string;
+  remark?: string;
+}
+
+export const getDoorRecords = (deviceId: string, params: { page?: number; pageSize?: number }) => {
+  return http.request<ResultTable>("get", `/devices/${deviceId}/door-records`, { params });
+};

+ 36 - 1
haha-admin-web/src/views/device/index.vue

@@ -7,6 +7,7 @@ import Refresh from "~icons/ep/refresh";
 import Unlock from "~icons/ep/unlock";
 import Temperature from "~icons/ri/temp-hot-line";
 import Volume from "~icons/ri/volume-up-line";
+import Document from "~icons/ep/document";
 
 defineOptions({
   name: "DeviceManage"
@@ -28,7 +29,9 @@ const {
   handleSetTemperature,
   handleSetVolume,
   handleSizeChange,
-  handleCurrentChange
+  handleCurrentChange,
+  operatingIds,
+  showDoorRecords
 } = useDevice(tableRef);
 </script>
 
@@ -121,6 +124,7 @@ const {
               type="primary"
               :size="size"
               :icon="useRenderIcon(Unlock)"
+              :loading="operatingIds.has(row.id)"
               @click="handleOpenDoor(row)"
             >
               开门
@@ -131,6 +135,7 @@ const {
               type="primary"
               :size="size"
               :icon="useRenderIcon(Temperature)"
+              :loading="operatingIds.has(row.id)"
               @click="handleSetTemperature(row)"
             >
               温度
@@ -141,14 +146,44 @@ const {
               type="primary"
               :size="size"
               :icon="useRenderIcon(Volume)"
+              :loading="operatingIds.has(row.id)"
               @click="handleSetVolume(row)"
             >
               音量
             </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(Document)"
+              @click="showDoorRecords(row)"
+            >
+              开关门记录
+            </el-button>
           </template>
         </pure-table>
       </template>
     </PureTableBar>
+
+    <!-- 开关门记录弹窗 -->
+    <el-dialog
+      v-model="dialogVisible"
+      title="开关门记录"
+      width="90%"
+      :close-on-click-modal="false"
+      destroy-on-close
+    >
+      <pure-table
+        row-key="id"
+        :loading="recordLoading"
+        :data="recordList"
+        :columns="recordColumns"
+        :pagination="{ ...recordPagination, size: 'default' }"
+        @page-size-change="handleRecordSizeChange"
+        @page-current-change="handleRecordCurrentChange"
+      />
+    </el-dialog>
   </div>
 </template>
 

+ 222 - 14
haha-admin-web/src/views/device/utils/hook.tsx

@@ -7,7 +7,9 @@ import {
   getDeviceList,
   openDoor,
   setTemperature,
-  setVolume
+  setVolume,
+  getDoorRecords,
+  type DoorRecordItem
 } from "@/api/device";
 import { getEnabledShops } from "@/api/shop";
 import { type Ref, ref, toRaw, reactive, onMounted } from "vue";
@@ -34,6 +36,21 @@ export function useDevice(tableRef: Ref) {
   const dataList = ref([]);
   const loading = ref(true);
   const shopOptions = ref([]);
+  // 操作按钮的加载状态
+  const operatingIds = ref<Set<number>>(new Set());
+  
+  // 开关门记录相关
+  const dialogVisible = ref(false);
+  const recordLoading = ref(false);
+  const recordList = ref<DoorRecordItem[]>([]);
+  const currentDeviceId = ref<string>("");
+  const recordPagination = reactive<PaginationProps>({
+    total: 0,
+    pageSize: 10,
+    currentPage: 1,
+    background: true
+  });
+  
   const pagination = reactive<PaginationProps>({
     total: 0,
     pageSize: 10,
@@ -158,29 +175,44 @@ export function useDevice(tableRef: Ref) {
   // 远程开门
   async function handleOpenDoor(row: DeviceItem) {
     try {
-      await ElMessageBox.confirm(`确认要远程开启设备 ${row.deviceName} 的门吗?`, "系统提示", {
+      const deviceName = row.deviceName || row.storeName || `设备${row.deviceId}`;
+      
+      await ElMessageBox.confirm(`确认要远程开启 ${deviceName} 的门吗?`, "系统提示", {
         confirmButtonText: "确定",
         cancelButtonText: "取消",
         type: "warning"
       });
-
-      const res = await openDoor(row.id);
+  
+      // 添加 loading 状态
+      operatingIds.value.add(row.id);
+        
+      // 传递 doorIndex 参数,默认为 A 门
+      const res = await openDoor(row.id, { doorIndex: "A" });
       if (res.code === 0) {
-        message(`已发送开门指令到设备 ${row.deviceName}`, { type: "success" });
+        message(`已发送开门指令到设备 ${deviceName}`, { type: "success" });
+        // 开门成功后刷新列表,更新门状态
+        await onSearch();
       } else {
         message(res.message || "开门失败", { type: "error" });
       }
     } catch (error) {
       // 用户取消或请求失败
+      if (error !== "cancel") {
+        console.error("开门失败:", error);
+        message("操作失败", { type: "error" });
+      }
+    } finally {
+      // 移除 loading 状态
+      operatingIds.value.delete(row.id);
     }
   }
 
   // 设置温度
   const tempForm = reactive({ temperature: 0 });
-
+  
   function handleSetTemperature(row: DeviceItem) {
     tempForm.temperature = row.temperature;
-
+  
     addDialog({
       title: `设置 ${row.deviceName} 的温度`,
       width: "30%",
@@ -190,31 +222,51 @@ export function useDevice(tableRef: Ref) {
       contentRenderer: () => (
         <ElForm ref={ruleFormRef} model={tempForm} label-width="80px">
           <ElFormItem
-            label="温度(℃)"
+            label="温度 (℃)"
             prop="temperature"
-            rules={[{ required: true, message: "请输入温度", trigger: "blur" }]}
+            rules={[
+              { required: true, message: "请输入温度", trigger: "blur" },
+              { 
+                type: "number", 
+                min: -30, 
+                max: 30, 
+                message: "温度范围为 -30℃ 到 30℃", 
+                trigger: "blur" 
+              }
+            ]}
           >
             <ElInputNumber
               v-model={tempForm.temperature}
               min={-30}
               max={30}
+              step={0.5}
+              precision={1}
               class="w-full!"
+              placeholder="请输入温度值"
             />
           </ElFormItem>
         </ElForm>
       ),
       beforeSure: async (done) => {
         try {
+          // 添加 loading 状态
+          operatingIds.value.add(row.id);
+          
           const res = await setTemperature(row.id, { temperature: tempForm.temperature });
           if (res.code === 0) {
-            row.temperature = tempForm.temperature;
             message(`已设置温度为 ${tempForm.temperature}℃`, { type: "success" });
             done();
+            // 温度设置成功后刷新列表,更新温度显示
+            await onSearch();
           } else {
             message(res.message || "设置失败", { type: "error" });
           }
         } catch (error) {
+          console.error("设置温度失败:", error);
           message("设置失败", { type: "error" });
+        } finally {
+          // 移除 loading 状态
+          operatingIds.value.delete(row.id);
         }
       }
     });
@@ -237,35 +289,181 @@ export function useDevice(tableRef: Ref) {
           <ElFormItem
             label="音量"
             prop="volume"
-            rules={[{ required: true, message: "请输入音量", trigger: "blur" }]}
+            rules={[
+              { required: true, message: "请输入音量", trigger: "blur" },
+              { 
+                type: "number", 
+                min: 0, 
+                max: 100, 
+                message: "音量范围为 0 到 100", 
+                trigger: "blur" 
+              }
+            ]}
           >
             <ElInputNumber
               v-model={volumeForm.volume}
               min={0}
               max={100}
+              step={5}
               class="w-full!"
+              placeholder="请输入音量值 (0-100)"
             />
           </ElFormItem>
         </ElForm>
       ),
       beforeSure: async (done) => {
         try {
+          // 添加 loading 状态
+          operatingIds.value.add(row.id);
+          
           const res = await setVolume(row.id, { volume: volumeForm.volume });
           if (res.code === 0) {
-            row.volume = volumeForm.volume;
             message(`已设置音量为 ${volumeForm.volume}`, { type: "success" });
             done();
+            // 音量设置成功后刷新列表,更新音量显示
+            await onSearch();
           } else {
             message(res.message || "设置失败", { type: "error" });
           }
         } catch (error) {
+          console.error("设置音量失败:", error);
           message("设置失败", { type: "error" });
+        } finally {
+          // 移除 loading 状态
+          operatingIds.value.delete(row.id);
         }
       }
     });
   }
 
-  // 获取门店列表
+  const recordColumns: TableColumnList = [
+    {
+      label: "活动 ID",
+      prop: "activityId",
+      minWidth: 180
+    },
+    {
+      label: "用户 ID",
+      prop: "userId",
+      minWidth: 100
+    },
+    {
+      label: "门索引",
+      prop: "doorIndex",
+      minWidth: 80
+    },
+    {
+      label: "类型",
+      prop: "openType",
+      minWidth: 80,
+      cellRenderer: ({ row }) => (
+        <el-tag type={row.openType === "IN" ? "warning" : "info"}>
+          {row.openType === "IN" ? "上货" : "消费"}
+        </el-tag>
+      )
+    },
+    {
+      label: "门状态",
+      prop: "doorStatus",
+      minWidth: 100,
+      cellRenderer: ({ row }) => (
+        <el-tag 
+          type={
+            row.doorStatus === "OPENED" ? "danger" : 
+            row.doorStatus === "CLOSED" ? "success" : "info"
+          }
+        >
+          {
+            row.doorStatus === "OPENED" ? "已开门" : 
+            row.doorStatus === "CLOSED" ? "已关门" : "异常"
+          }
+        </el-tag>
+      )
+    },
+    {
+      label: "是否有消费",
+      prop: "nobuy",
+      minWidth: 100,
+      cellRenderer: ({ row }) => (
+        <el-tag type={row.nobuy === 0 ? "success" : "info"}>
+          {row.nobuy === 0 ? "有消费" : "无消费"}
+        </el-tag>
+      )
+    },
+    {
+      label: "开门时间",
+      prop: "openTime",
+      minWidth: 160,
+      formatter: ({ openTime }) => dayjs(openTime).format("YYYY-MM-DD HH:mm:ss")
+    },
+    {
+      label: "关门时间",
+      prop: "closeTime",
+      minWidth: 160,
+      formatter: ({ closeTime }) => 
+        closeTime ? dayjs(closeTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "持续时长 (秒)",
+      prop: "duration",
+      minWidth: 100
+    },
+    {
+      label: "来源",
+      prop: "source",
+      minWidth: 100,
+      cellRenderer: ({ row }) => (
+        <el-tag type={row.source === "MINIAPP" ? "primary" : "info"} size="small">
+          {row.source === "MINIAPP" ? "小程序" : row.source === "ADMIN" ? "管理后台" : row.source || "-"}
+        </el-tag>
+      )
+    }
+  ];
+  
+  // 查询开关门记录
+  async function fetchDoorRecords() {
+    if (!currentDeviceId.value) return;
+    
+    recordLoading.value = true;
+    try {
+      const res = await getDoorRecords(currentDeviceId.value, {
+        page: recordPagination.currentPage,
+        pageSize: recordPagination.pageSize
+      });
+      
+      if (res.code === 0 && res.data) {
+        recordList.value = res.data.list || [];
+        recordPagination.total = res.data.total || 0;
+      }
+    } catch (error) {
+      console.error("查询开关门记录失败:", error);
+      message("查询失败", { type: "error" });
+    } finally {
+      recordLoading.value = false;
+    }
+  }
+  
+  // 显示开关门记录弹窗
+  function showDoorRecords(row: DeviceItem) {
+    currentDeviceId.value = row.deviceId;
+    recordPagination.currentPage = 1;
+    recordPagination.pageSize = 10;
+    dialogVisible.value = true;
+    fetchDoorRecords();
+  }
+  
+  // 记录分页大小变化
+  function handleRecordSizeChange(val: number) {
+    recordPagination.pageSize = val;
+    recordPagination.currentPage = 1;
+    fetchDoorRecords();
+  }
+  
+  // 记录页码变化
+  function handleRecordCurrentChange(val: number) {
+    recordPagination.currentPage = val;
+    fetchDoorRecords();
+  }
   async function fetchShopOptions() {
     try {
       const { data } = await getEnabledShops();
@@ -293,6 +491,16 @@ export function useDevice(tableRef: Ref) {
     handleSetTemperature,
     handleSetVolume,
     handleSizeChange,
-    handleCurrentChange
+    handleCurrentChange,
+    operatingIds,
+    // 开关门记录相关
+    dialogVisible,
+    recordLoading,
+    recordList,
+    recordColumns,
+    recordPagination,
+    showDoorRecords,
+    handleRecordSizeChange,
+    handleRecordCurrentChange
   };
 }

+ 56 - 0
haha-admin/src/main/java/com/haha/admin/config/JacksonConfig.java

@@ -0,0 +1,56 @@
+package com.haha.admin.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+@Configuration
+public class JacksonConfig {
+
+    private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+    private static final String DATE_FORMAT = "yyyy-MM-dd";
+    private static final String TIME_FORMAT = "HH:mm:ss";
+
+    @Bean
+    @Primary
+    public ObjectMapper objectMapper() {
+        ObjectMapper mapper = new ObjectMapper();
+        
+        JavaTimeModule javaTimeModule = new JavaTimeModule();
+        javaTimeModule.addSerializer(LocalDateTime.class, 
+            new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
+        javaTimeModule.addDeserializer(LocalDateTime.class, 
+            new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
+        
+        javaTimeModule.addSerializer(LocalDate.class, 
+            new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
+        javaTimeModule.addDeserializer(LocalDate.class, 
+            new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
+        
+        javaTimeModule.addSerializer(LocalTime.class, 
+            new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
+        javaTimeModule.addDeserializer(LocalTime.class, 
+            new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
+
+        mapper.registerModule(javaTimeModule);
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
+        return mapper;
+    }
+}

+ 1 - 1
haha-admin/src/main/java/com/haha/admin/config/WebMvcConfig.java

@@ -14,7 +14,7 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  *
  * @author haha
  */
-@Configuration
+@Configuration(proxyBeanMethods = false)
 @RequiredArgsConstructor
 public class WebMvcConfig implements WebMvcConfigurer {
 

+ 20 - 1
haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java

@@ -9,6 +9,7 @@ import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.Device;
 import com.haha.service.DeviceService;
+import com.haha.service.DoorRecordService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
@@ -25,6 +26,7 @@ import java.util.Map;
 public class DeviceController {
 
     private final DeviceService deviceService;
+    private final DoorRecordService doorRecordService;
 
     /**
      * 分页查询设备列表
@@ -104,7 +106,7 @@ public class DeviceController {
 
     /**
      * 设置音量
-     * @param id 设备ID
+     * @param id 设备 ID
      * @param params 音量参数
      * @return 操作结果
      */
@@ -115,4 +117,21 @@ public class DeviceController {
         boolean success = deviceService.setVolume(id, volume);
         return success ? Result.success("音量设置成功", null) : Result.error(500, "设置失败");
     }
+
+    /**
+     * 查询设备的开关门记录
+     * @param deviceId 设备 ID
+     * @param page 页码
+     * @param pageSize 每页数量
+     * @return 开关门记录列表
+     */
+    @RequirePermission("device:read")
+    @GetMapping("/{deviceId}/door-records")
+    public Result<PageResult<com.haha.entity.DoorRecord>> getDoorRecords(
+            @PathVariable String deviceId,
+            @RequestParam(defaultValue = "1") int page,
+            @RequestParam(defaultValue = "10") int pageSize) {
+        IPage<com.haha.entity.DoorRecord> recordPage = doorRecordService.getDeviceRecordsByPage(deviceId, page, pageSize);
+        return Result.success("查询成功", PageResult.of(recordPage));
+    }
 }

+ 21 - 0
haha-common/pom.xml

@@ -36,6 +36,27 @@
             <artifactId>jakarta.servlet-api</artifactId>
             <scope>provided</scope>
         </dependency>
+
+        <!-- Jackson Annotations -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Jackson Databind -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Jackson Java 8 Time Module -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.datatype</groupId>
+            <artifactId>jackson-datatype-jsr310</artifactId>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 3 - 0
haha-common/src/main/java/com/haha/common/vo/CouponTemplateVO.java

@@ -1,5 +1,6 @@
 package com.haha.common.vo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -32,8 +33,10 @@ public class CouponTemplateVO {
 
     private String validTypeLabel;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validStartTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validEndTime;
 
     private Integer validDays;

+ 3 - 0
haha-common/src/main/java/com/haha/common/vo/CouponVO.java

@@ -1,5 +1,6 @@
 package com.haha.common.vo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.math.BigDecimal;
@@ -32,8 +33,10 @@ public class CouponVO {
 
     private String statusColor;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validStartTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validEndTime;
 
     private Boolean isExpired;

+ 3 - 0
haha-common/src/main/java/com/haha/common/vo/OrderVO.java

@@ -1,5 +1,6 @@
 package com.haha.common.vo;
 
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.math.BigDecimal;
 import java.time.LocalDateTime;
@@ -21,7 +22,9 @@ public class OrderVO {
     private String payStatusLabel;
     private Integer status;
     private String statusText;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime payTime;
     private String videoUrl;
     private BigDecimal confidence;

+ 6 - 0
haha-entity/pom.xml

@@ -27,6 +27,12 @@
             <groupId>jakarta.validation</groupId>
             <artifactId>jakarta.validation-api</artifactId>
         </dependency>
+
+        <!-- Jackson Annotations -->
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-annotations</artifactId>
+        </dependency>
     </dependencies>
 
 </project>

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/Account.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -28,7 +29,9 @@ public class Account implements Serializable {
 
     private Integer points;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 4 - 0
haha-entity/src/main/java/com/haha/entity/Admin.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -69,6 +70,7 @@ public class Admin implements Serializable {
     /**
      * 最后登录时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime lastLoginTime;
 
     /**
@@ -79,10 +81,12 @@ public class Admin implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
      * 更新时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/Announcement.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -55,11 +56,13 @@ public class Announcement implements Serializable {
     /**
      * 发布时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime publishTime;
 
     /**
      * 下线时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime offlineTime;
 
     /**
@@ -80,11 +83,13 @@ public class Announcement implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
      * 更新时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     // ========== 以下为非数据库字段,用于前端展示 ==========

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/CheckinRecord.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -103,20 +104,24 @@ public class CheckinRecord implements Serializable {
     /**
      * 同步时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime syncedAt;
 
     /**
      * 签到时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime checkinTime;
 
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
      * 更新时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/CouponTemplate.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -38,8 +39,10 @@ public class CouponTemplate implements Serializable {
 
     private Integer validType;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validStartTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validEndTime;
 
     private Integer validDays;
@@ -56,8 +59,10 @@ public class CouponTemplate implements Serializable {
 
     private String creatorName;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     private Integer deleted;

+ 4 - 0
haha-entity/src/main/java/com/haha/entity/Device.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -33,8 +34,10 @@ public class Device implements Serializable {
      */
     private String doorStatus;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     // ========== 以下为非数据库字段,用于前端展示 ==========
@@ -82,5 +85,6 @@ public class Device implements Serializable {
     private Integer orderCount;
 
     @TableField(exist = false)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime lastOnlineTime;
 }

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/DictData.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -40,10 +41,12 @@ public class DictData implements Serializable {
 
     private String createBy;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     private String updateBy;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     private String remark;

+ 2 - 0
haha-entity/src/main/java/com/haha/entity/DictLog.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -36,5 +37,6 @@ public class DictLog implements Serializable {
 
     private String ip;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 }

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/DictType.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -32,10 +33,12 @@ public class DictType implements Serializable {
 
     private String createBy;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     private String updateBy;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     private String remark;

+ 100 - 0
haha-entity/src/main/java/com/haha/entity/DoorRecord.java

@@ -0,0 +1,100 @@
+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.time.LocalDateTime;
+
+/**
+ * 设备开关门记录实体
+ * 用于记录用户每次开关门的详细流水信息
+ * 
+ * @author haha
+ */
+@Data
+@TableName("t_door_record")
+public class DoorRecord {
+
+    /**
+     * 主键 ID
+     */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 活动 ID(哈哈平台生成)
+     */
+    private String activityId;
+
+    /**
+     * 设备 ID
+     */
+    private String deviceId;
+
+    /**
+     * 用户 ID
+     */
+    private Long userId;
+
+    /**
+     * 门索引(A/B/C/D)
+     */
+    private String doorIndex;
+
+    /**
+     * 开门类型:IN-上货,OUT-消费
+     */
+    private String openType;
+
+    /**
+     * 门状态:OPENED-已开门,CLOSED-已关门
+     */
+    private String doorStatus;
+
+    /**
+     * 是否有消费:0-有消费,1-无消费
+     */
+    private Integer nobuy;
+
+    /**
+     * 订单 ID(如果有消费则关联订单)
+     */
+    private Long orderId;
+
+    /**
+     * 开门时间
+     */
+    private LocalDateTime openTime;
+
+    /**
+     * 关门时间
+     */
+    private LocalDateTime closeTime;
+
+    /**
+     * 持续时长(秒)
+     */
+    private Integer duration;
+
+    /**
+     * 记录来源:MINIAPP-小程序,ADMIN-管理后台
+     */
+    private String source;
+
+    /**
+     * 备注信息
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 3 - 1
haha-entity/src/main/java/com/haha/entity/FundLog.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -19,7 +20,7 @@ public class FundLog implements Serializable {
     private Long userId;
 
     /**
-     * 浜ゆ槗绫诲瀷锛?-鍏呭€硷紝2-娑堣垂锛?-閫€娆撅紝4-鎻愮幇
+     * 交易类型:1-充值,2-消费,3-退款,4-提现
      */
     private Integer type;
 
@@ -33,5 +34,6 @@ public class FundLog implements Serializable {
 
     private String remark;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 }

+ 2 - 0
haha-entity/src/main/java/com/haha/entity/InventoryLog.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -95,5 +96,6 @@ public class InventoryLog implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 }

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/MarketingActivity.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -25,8 +26,10 @@ public class MarketingActivity implements Serializable {
 
     private String activityDesc;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime startTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime endTime;
 
     private Integer status;
@@ -51,8 +54,10 @@ public class MarketingActivity implements Serializable {
 
     private String creatorName;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     private Integer deleted;

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/NewProductApply.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -206,20 +207,24 @@ public class NewProductApply implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
      * 更新时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     /**
      * 提交时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime submitTime;
 
     /**
      * 审核时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime auditTime;
 }

+ 2 - 0
haha-entity/src/main/java/com/haha/entity/OperationLog.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -111,6 +112,7 @@ public class OperationLog implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**

+ 6 - 0
haha-entity/src/main/java/com/haha/entity/Order.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -49,8 +50,10 @@ public class Order implements Serializable {
 
     private String items;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime payTime;
 
     /** 退款状态 */
@@ -60,6 +63,7 @@ public class Order implements Serializable {
     private BigDecimal refundAmount;
 
     /** 退款时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime refundTime;
 
     /** 退款原因 */
@@ -75,9 +79,11 @@ public class Order implements Serializable {
     private String serviceId;
 
     /** 服务开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime serviceStartTime;
 
     /** 服务结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime serviceEndTime;
 
     private Integer status;

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/OrderItem.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -44,7 +45,9 @@ public class OrderItem implements Serializable {
 
     private Long userId;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime payTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 }

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

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -48,8 +49,10 @@ public class PriceAdjustmentLog implements Serializable {
 
     private Integer isPriority;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime adjustTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime restoreTime;
 
     private Integer restoreStatus;
@@ -58,8 +61,10 @@ public class PriceAdjustmentLog implements Serializable {
 
     private BigDecimal saleAmount;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     @TableField(exist = false)

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/Role.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -46,10 +47,12 @@ public class Role implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
      * 更新时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/Shop.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -90,11 +91,13 @@ public class Shop implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
      * 更新时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     // ========== 以下为非数据库字段,用于前端展示 ==========

+ 4 - 0
haha-entity/src/main/java/com/haha/entity/StatCategoryDaily.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -17,6 +18,7 @@ public class StatCategoryDaily implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate statDate;
 
     private String category;
@@ -33,7 +35,9 @@ public class StatCategoryDaily implements Serializable {
 
     private BigDecimal profitAmount;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 4 - 0
haha-entity/src/main/java/com/haha/entity/StatDeviceDaily.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -17,6 +18,7 @@ public class StatDeviceDaily implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate statDate;
 
     private String deviceId;
@@ -39,7 +41,9 @@ public class StatDeviceDaily implements Serializable {
 
     private BigDecimal profitAmount;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 4 - 0
haha-entity/src/main/java/com/haha/entity/StatProductDaily.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -17,6 +18,7 @@ public class StatProductDaily implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate statDate;
 
     private Long productId;
@@ -41,7 +43,9 @@ public class StatProductDaily implements Serializable {
 
     private BigDecimal profitAmount;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 4 - 0
haha-entity/src/main/java/com/haha/entity/StatShopDaily.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -17,6 +18,7 @@ public class StatShopDaily implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate statDate;
 
     private Long shopId;
@@ -47,7 +49,9 @@ public class StatShopDaily implements Serializable {
 
     private BigDecimal profitRate;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 6 - 0
haha-entity/src/main/java/com/haha/entity/StatUserRepurchase.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.math.BigDecimal;
@@ -17,6 +18,7 @@ public class StatUserRepurchase implements Serializable {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate statDate;
 
     private Long userId;
@@ -27,8 +29,10 @@ public class StatUserRepurchase implements Serializable {
 
     private Integer totalOrderCount;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate firstOrderDate;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate lastOrderDate;
 
     private BigDecimal totalAmount;
@@ -37,7 +41,9 @@ public class StatUserRepurchase implements Serializable {
 
     private Integer repurchaseDays;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 5 - 0
haha-entity/src/main/java/com/haha/entity/StockRecord.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -66,11 +67,13 @@ public class StockRecord implements Serializable {
     /**
      * 开始时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime startTime;
 
     /**
      * 结束时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime endTime;
 
     /**
@@ -81,10 +84,12 @@ public class StockRecord implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     /**
      * 更新时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 }

+ 2 - 0
haha-entity/src/main/java/com/haha/entity/SyncLog.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -59,5 +60,6 @@ public class SyncLog implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 }

+ 4 - 0
haha-entity/src/main/java/com/haha/entity/SyncRecord.java

@@ -3,6 +3,7 @@ package com.haha.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -49,11 +50,13 @@ public class SyncRecord implements Serializable {
     /**
      * 开始时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime startTime;
 
     /**
      * 结束时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime endTime;
 
     /**
@@ -79,5 +82,6 @@ public class SyncRecord implements Serializable {
     /**
      * 创建时间
      */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 }

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

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -24,8 +25,10 @@ public class TimedDiscountActivity implements Serializable {
 
     private String activityDesc;
 
+    @JsonFormat(pattern = "HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalTime startTime;
 
+    @JsonFormat(pattern = "HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalTime endTime;
 
     private Integer discountType;
@@ -58,8 +61,10 @@ public class TimedDiscountActivity implements Serializable {
 
     private String creatorName;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     private Integer deleted;

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

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -23,8 +24,10 @@ public class TimedDiscountRecord implements Serializable {
 
     private String activityName;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate executeDate;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime executeTime;
 
     private Long shopId;
@@ -53,6 +56,7 @@ public class TimedDiscountRecord implements Serializable {
 
     private Integer executeDuration;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
     @TableField(exist = false)

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

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -21,6 +22,7 @@ public class TimedDiscountStatistics implements Serializable {
 
     private Long activityId;
 
+    @JsonFormat(pattern = "yyyy-MM-dd", timezone = "Asia/Shanghai")
     private LocalDate statDate;
 
     private Long shopId;
@@ -47,8 +49,10 @@ public class TimedDiscountStatistics implements Serializable {
 
     private BigDecimal sellThroughRate;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     @TableField(exist = false)

+ 3 - 0
haha-entity/src/main/java/com/haha/entity/User.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 import java.io.Serializable;
 import java.time.LocalDateTime;
@@ -33,8 +34,10 @@ public class User implements Serializable {
      */
     private Integer payscoreEnabled;
     
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
     
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
     
     // ========== 以下为非数据库字段,用于前端展示 ==========

+ 7 - 0
haha-entity/src/main/java/com/haha/entity/UserCoupon.java

@@ -4,6 +4,7 @@ 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 com.fasterxml.jackson.annotation.JsonFormat;
 import lombok.Data;
 
 import java.io.Serializable;
@@ -28,18 +29,24 @@ public class UserCoupon implements Serializable {
 
     private Integer status;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime receiveTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime useTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validStartTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime validEndTime;
 
     private BigDecimal discountAmount;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime createTime;
 
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
     private LocalDateTime updateTime;
 
     @TableField(exist = false)

+ 85 - 0
haha-mapper/src/main/java/com/haha/mapper/DoorRecordMapper.java

@@ -0,0 +1,85 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.DoorRecord;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 设备开关门记录 Mapper 接口
+ * 
+ * @author haha
+ */
+@Mapper
+public interface DoorRecordMapper extends BaseMapper<DoorRecord> {
+
+    /**
+     * 根据活动 ID 查询开门记录
+     * 
+     * @param activityId 活动 ID
+     * @return 开门记录
+     */
+    DoorRecord getByActivityId(@Param("activityId") String activityId);
+
+    /**
+     * 根据用户 ID 查询最近的开门记录
+     * 
+     * @param userId 用户 ID
+     * @param deviceId 设备 ID
+     * @param limit 限制数量
+     * @return 开门记录列表
+     */
+    List<DoorRecord> getRecentRecords(
+        @Param("userId") Long userId, 
+        @Param("deviceId") String deviceId,
+        @Param("limit") int limit
+    );
+
+    /**
+     * 更新门状态
+     * 
+     * @param activityId 活动 ID
+     * @param doorStatus 门状态
+     * @param closeTime 关门时间
+     * @return 影响行数
+     */
+    int updateDoorStatus(
+        @Param("activityId") String activityId,
+        @Param("doorStatus") String doorStatus,
+        @Param("closeTime") LocalDateTime closeTime
+    );
+
+    /**
+     * 关联订单 ID
+     * 
+     * @param activityId 活动 ID
+     * @param orderId 订单 ID
+     * @return 影响行数
+     */
+    int linkOrderId(@Param("activityId") String activityId, @Param("orderId") Long orderId);
+
+    /**
+     * 标记无消费记录
+     * 
+     * @param activityId 活动 ID
+     * @return 影响行数
+     */
+    int markNoConsume(@Param("activityId") String activityId);
+
+    /**
+     * 统计指定时间范围内的开门次数
+     * 
+     * @param deviceId 设备 ID
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 开门次数
+     */
+    int countByTimeRange(
+        @Param("deviceId") String deviceId,
+        @Param("startTime") LocalDateTime startTime,
+        @Param("endTime") LocalDateTime endTime
+    );
+}

+ 69 - 0
haha-mapper/src/main/resources/mapper/DoorRecordMapper.xml

@@ -0,0 +1,69 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.haha.mapper.DoorRecordMapper">
+
+    <resultMap id="DoorRecordResult" type="com.haha.entity.DoorRecord">
+        <id property="id" column="id"/>
+        <result property="activityId" column="activity_id"/>
+        <result property="deviceId" column="device_id"/>
+        <result property="userId" column="user_id"/>
+        <result property="doorIndex" column="door_index"/>
+        <result property="openType" column="open_type"/>
+        <result property="doorStatus" column="door_status"/>
+        <result property="nobuy" column="nobuy"/>
+        <result property="orderId" column="order_id"/>
+        <result property="openTime" column="open_time"/>
+        <result property="closeTime" column="close_time"/>
+        <result property="duration" column="duration"/>
+        <result property="source" column="source"/>
+        <result property="remark" column="remark"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <!-- 根据活动 ID 查询开门记录 -->
+    <select id="getByActivityId" resultMap="DoorRecordResult">
+        SELECT * 
+        FROM t_door_record 
+        WHERE activity_id = #{activityId}
+        LIMIT 1
+    </select>
+
+    <!-- 根据用户 ID 查询最近的开门记录 -->
+    <select id="getRecentRecords" resultMap="DoorRecordResult">
+        SELECT * 
+        FROM t_door_record 
+        WHERE user_id = #{userId}
+        AND device_id = #{deviceId}
+        ORDER BY open_time DESC
+        LIMIT #{limit}
+    </select>
+
+    <!-- 更新门状态 -->
+    <update id="updateDoorStatus">
+        UPDATE t_door_record 
+        SET door_status = #{doorStatus},
+            close_time = #{closeTime},
+            duration = TIMESTAMPDIFF(SECOND, open_time, #{closeTime}),
+            update_time = NOW()
+        WHERE activity_id = #{activityId}
+    </update>
+
+    <!-- 关联订单 ID -->
+    <update id="linkOrderId">
+        UPDATE t_door_record 
+        SET order_id = #{orderId},
+            update_time = NOW()
+        WHERE activity_id = #{activityId}
+    </update>
+
+    <!-- 标记无消费记录 -->
+    <update id="markNoConsume">
+        UPDATE t_door_record 
+        SET nobuy = 1,
+            remark = '用户开门但未消费',
+            update_time = NOW()
+        WHERE activity_id = #{activityId}
+    </update>
+
+</mapper>

+ 56 - 0
haha-miniapp/src/main/java/com/haha/miniapp/config/JacksonConfig.java

@@ -0,0 +1,56 @@
+package com.haha.miniapp.config;
+
+import com.fasterxml.jackson.databind.DeserializationFeature;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.SerializationFeature;
+import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
+import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
+
+@Configuration
+public class JacksonConfig {
+
+    private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+    private static final String DATE_FORMAT = "yyyy-MM-dd";
+    private static final String TIME_FORMAT = "HH:mm:ss";
+
+    @Bean
+    @Primary
+    public ObjectMapper objectMapper() {
+        ObjectMapper mapper = new ObjectMapper();
+        
+        JavaTimeModule javaTimeModule = new JavaTimeModule();
+        javaTimeModule.addSerializer(LocalDateTime.class, 
+            new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
+        javaTimeModule.addDeserializer(LocalDateTime.class, 
+            new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DATE_TIME_FORMAT)));
+        
+        javaTimeModule.addSerializer(LocalDate.class, 
+            new LocalDateSerializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
+        javaTimeModule.addDeserializer(LocalDate.class, 
+            new LocalDateDeserializer(DateTimeFormatter.ofPattern(DATE_FORMAT)));
+        
+        javaTimeModule.addSerializer(LocalTime.class, 
+            new LocalTimeSerializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
+        javaTimeModule.addDeserializer(LocalTime.class, 
+            new LocalTimeDeserializer(DateTimeFormatter.ofPattern(TIME_FORMAT)));
+
+        mapper.registerModule(javaTimeModule);
+        mapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
+        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
+
+        return mapper;
+    }
+}

+ 33 - 0
haha-miniapp/src/main/resources/sql/door_record.sql

@@ -0,0 +1,33 @@
+-- 设备开关门记录表
+-- 用于记录用户每次开关门的详细流水信息,包括开门时间、关门时间、持续时长、是否有消费等
+
+CREATE TABLE IF NOT EXISTS `t_door_record` (
+  `id` BIGINT(20) NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
+  `activity_id` VARCHAR(64) NOT NULL COMMENT '活动 ID(哈哈平台生成)',
+  `device_id` VARCHAR(32) NOT NULL COMMENT '设备 ID',
+  `user_id` BIGINT(20) NOT NULL COMMENT '用户 ID',
+  `door_index` VARCHAR(10) DEFAULT NULL COMMENT '门索引(A/B/C/D)',
+  `open_type` VARCHAR(10) NOT NULL COMMENT '开门类型:IN-上货,OUT-消费',
+  `door_status` VARCHAR(20) NOT NULL COMMENT '门状态:OPENED-已开门,CLOSED-已关门',
+  `nobuy` TINYINT(1) DEFAULT 0 COMMENT '是否有消费:0-有消费,1-无消费',
+  `order_id` BIGINT(20) DEFAULT NULL COMMENT '订单 ID(如果有消费则关联订单)',
+  `open_time` DATETIME NOT NULL COMMENT '开门时间',
+  `close_time` DATETIME DEFAULT NULL COMMENT '关门时间',
+  `duration` INT(11) DEFAULT NULL COMMENT '持续时长(秒)',
+  `source` VARCHAR(20) DEFAULT NULL COMMENT '记录来源:MINIAPP-小程序,ADMIN-管理后台',
+  `remark` VARCHAR(500) 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_id` (`activity_id`),
+  KEY `idx_user_id` (`user_id`),
+  KEY `idx_device_id` (`device_id`),
+  KEY `idx_order_id` (`order_id`),
+  KEY `idx_open_time` (`open_time`),
+  KEY `idx_create_time` (`create_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备开关门记录表';
+
+-- 数据初始化说明
+-- 1. 该表从新部署时自动创建
+-- 2. 历史数据可以通过定时任务或脚本进行补充(如果需要)
+-- 3. 建议定期归档旧数据(如超过 1 年的记录)

+ 27 - 5
haha-sdk/src/main/java/com/haha/sdk/api/DeviceApi.java

@@ -120,26 +120,48 @@ public class DeviceApi extends BaseApi {
     }
     
     /**
-     * 设置设备音量(仅限动态柜)
+     * 设置设备音量 (仅限动态柜)
      * 
-     * @param deviceId 设备ID
+     * @param deviceId 设备 ID
      * @param voice 音量大小值 0~15
      * @return 是否成功
      * @throws HahaException 调用失败时抛出
      */
     public boolean setVoice(String deviceId, int voice) throws HahaException {
         if (voice < 0 || voice > 15) {
-            throw new IllegalArgumentException("音量值必须在0-15之间");
+            throw new IllegalArgumentException("音量值必须在 0-15 之间");
         }
-        
+            
         Map<String, String> params = new HashMap<>();
         params.put("access_token", client.getAccessToken());
         params.put("device_id", deviceId);
         params.put("voice", String.valueOf(voice));
-        
+            
         post("/device/voiceControl", params);
         return true;
     }
+        
+    /**
+     * 设置设备温度 (仅限动态柜)
+     * 
+     * @param deviceId 设备 ID
+     * @param temperature 温度值,范围 -30~30℃
+     * @return 是否成功
+     * @throws HahaException 调用失败时抛出
+     */
+    public boolean setTemperature(String deviceId, double temperature) throws HahaException {
+        if (temperature < -30 || temperature > 30) {
+            throw new IllegalArgumentException("温度值必须在 -30~30℃之间");
+        }
+            
+        Map<String, String> params = new HashMap<>();
+        params.put("access_token", client.getAccessToken());
+        params.put("device_id", deviceId);
+        params.put("temperature", String.valueOf(temperature));
+            
+        post("/device/setTemperature", params);
+        return true;
+    }
     
     /**
      * 查询设备和锁的状态

+ 107 - 0
haha-service/src/main/java/com/haha/service/DoorRecordService.java

@@ -0,0 +1,107 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.DoorRecord;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 设备开关门记录服务接口
+ * 
+ * @author haha
+ */
+public interface DoorRecordService extends IService<DoorRecord> {
+
+    /**
+     * 保存开门记录(Redis + 数据库双存储)
+     * 
+     * @param deviceId 设备 ID
+     * @param userId 用户 ID
+     * @param activityId 活动 ID
+     * @param doorIndex 门索引
+     * @param openType 开门类型
+     * @param source 记录来源
+     * @return 开门记录
+     */
+    DoorRecord saveDoorOpenRecord(
+        String deviceId, 
+        Long userId, 
+        String activityId,
+        String doorIndex,
+        String openType,
+        String source
+    );
+
+    /**
+     * 根据活动 ID 获取开门记录
+     * 
+     * @param activityId 活动 ID
+     * @return 开门记录
+     */
+    DoorRecord getByActivityId(String activityId);
+
+    /**
+     * 更新门状态为已关门
+     * 
+     * @param activityId 活动 ID
+     * @return 是否成功
+     */
+    boolean updateDoorClosed(String activityId);
+
+    /**
+     * 标记无消费记录
+     * 
+     * @param activityId 活动 ID
+     * @return 是否成功
+     */
+    boolean markNoConsume(String activityId);
+
+    /**
+     * 关联订单 ID
+     * 
+     * @param activityId 活动 ID
+     * @param orderId 订单 ID
+     * @return 是否成功
+     */
+    boolean linkOrderId(String activityId, Long orderId);
+
+    /**
+     * 获取用户最近的开门记录
+     * 
+     * @param userId 用户 ID
+     * @param deviceId 设备 ID
+     * @param limit 限制数量
+     * @return 开门记录列表
+     */
+    List<DoorRecord> getRecentRecords(Long userId, String deviceId, int limit);
+
+    /**
+     * 统计指定时间范围内的开门次数
+     * 
+     * @param deviceId 设备 ID
+     * @param startTime 开始时间
+     * @param endTime 结束时间
+     * @return 开门次数
+     */
+    int countByTimeRange(String deviceId, LocalDateTime startTime, LocalDateTime endTime);
+
+    /**
+     * 清理过期的 Redis 开门记录(保留最近 30 分钟)
+     */
+    void cleanupExpiredRedisRecords();
+
+    /**
+     * 分页查询设备的开关门记录
+     * 
+     * @param deviceId 设备 ID
+     * @param page 页码
+     * @param pageSize 每页数量
+     * @return 开门记录分页数据
+     */
+    com.baomidou.mybatisplus.core.metadata.IPage<DoorRecord> getDeviceRecordsByPage(
+        String deviceId, 
+        int page, 
+        int pageSize
+    );
+}

+ 91 - 35
haha-service/src/main/java/com/haha/service/impl/DeviceServiceImpl.java

@@ -21,6 +21,7 @@ import com.haha.mapper.ShopMapper;
 import com.haha.service.DeviceService;
 import com.haha.service.OrderService;
 import com.haha.service.UserService;
+import com.haha.service.DoorRecordService;
 import com.haha.common.utils.OrderUtils;
 import com.haha.common.vo.OpenDoorVO;
 import com.haha.sdk.HahaClient;
@@ -49,6 +50,7 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
     private final ShopMapper shopMapper;
     private final DeviceMapper deviceMapper;
     private final UserService userService;
+    private final DoorRecordService doorRecordService;
 
     @Override
     public IPage<Device> getPage(int page, int pageSize, String deviceId, Long shopId, Integer status) {
@@ -108,12 +110,42 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
         if (device == null) {
             throw new BusinessException(404, "设备不存在");
         }
-        
-        log.info("远程开门: deviceId={}, doorIndex={}", device.getDeviceId(), doorIndex);
-        
-        // TODO: 调用SDK发送开门指令
-        
-        return true;
+            
+        log.info("远程开门:deviceId={}, doorIndex={}", device.getDeviceId(), doorIndex);
+            
+        try {
+            // 调用 SDK 发送开门指令
+            // 注意:管理后台开门默认为上货模式 (IN)
+            // outUserId 必须是 6-32 位数字或字母,不能包含特殊字符
+            String outUserId = "ADMIN" + System.currentTimeMillis(); // 生成临时用户编号(纯数字)
+            
+            // 检查是否多门单开设备
+            String finalDoorIndex = null;
+            Integer multiDoorType = hahaClient.getDeviceApi().isMultiDoorUnique(device.getDeviceId());
+            
+            // 如果是多门单开设备,使用传入的 doorIndex;否则不传递门索引
+            if (multiDoorType != null && multiDoorType > 0) {
+                finalDoorIndex = doorIndex != null ? doorIndex : DeviceConstants.DOOR_A;
+                log.info("设备 {} 是多门单开设备,类型:{},将打开 {} 门", device.getDeviceId(), multiDoorType, finalDoorIndex);
+            } else {
+                log.info("设备 {} 是单门设备或不支持多门单开,不传递门索引参数", device.getDeviceId());
+            }
+            
+            OpenDoorResult result = hahaClient.getDeviceApi().openDoor(
+                device.getDeviceId(), 
+                outUserId,
+                OpenType.IN.getCode(), // 管理后台开门为上货模式
+                finalDoorIndex,
+                "ADMIN"
+            );
+                
+            log.info("开门成功 - 设备:{}, 活动 ID: {}, 用户 ID: {}", 
+                    device.getDeviceId(), result.getActivityId(), result.getUserId());
+            return true;
+        } catch (Exception e) {
+            log.error("开门失败 - 设备:{}, error: {}", device.getDeviceId(), e.getMessage());
+            throw new BusinessException(500, "开门失败:" + e.getMessage());
+        }
     }
 
     @Override
@@ -122,12 +154,24 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
         if (device == null) {
             throw new BusinessException(404, "设备不存在");
         }
-        
-        log.info("设置温度: deviceId={}, temperature={}", device.getDeviceId(), temperature);
-        
-        // TODO: 调用SDK设置温度
-        
-        return true;
+            
+        log.info("设置温度:deviceId={}, temperature={}", device.getDeviceId(), temperature);
+            
+        try {
+            // 调用 SDK 设置温度
+            hahaClient.getDeviceApi().setTemperature(device.getDeviceId(), temperature);
+                
+            // 更新本地设备温度 (注意:temperature 字段是 String 类型)
+            device.setTemperature(String.valueOf(temperature));
+            device.setUpdateTime(LocalDateTime.now());
+            this.updateById(device);
+                
+            log.info("温度设置成功 - 设备:{}, 温度:{}℃", device.getDeviceId(), temperature);
+            return true;
+        } catch (Exception e) {
+            log.error("温度设置失败 - 设备:{}, error: {}", device.getDeviceId(), e.getMessage());
+            throw new BusinessException(500, "温度设置失败:" + e.getMessage());
+        }
     }
 
     @Override
@@ -136,12 +180,29 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
         if (device == null) {
             throw new BusinessException(404, "设备不存在");
         }
-        
-        log.info("设置音量: deviceId={}, volume={}", device.getDeviceId(), volume);
-        
-        // TODO: 调用SDK设置音量
-        
-        return true;
+            
+        log.info("设置音量:deviceId={}, volume={}", device.getDeviceId(), volume);
+            
+        try {
+            // 调用 SDK 设置音量 (SDK 使用 voice 参数,范围 0-15)
+            // 前端传入的 volume 范围是 0-100,需要转换
+            int voiceLevel = (int) Math.round(volume / 100.0 * 15);
+            voiceLevel = Math.max(0, Math.min(15, voiceLevel)); // 确保在 0-15 范围内
+                
+            hahaClient.getDeviceApi().setVoice(device.getDeviceId(), voiceLevel);
+                
+            // 更新本地设备音量
+            device.setVolume(volume);
+            device.setUpdateTime(LocalDateTime.now());
+            this.updateById(device);
+                
+            log.info("音量设置成功 - 设备:{}, 音量值:{} (SDK 级别:{})", 
+                    device.getDeviceId(), volume, voiceLevel);
+            return true;
+        } catch (Exception e) {
+            log.error("音量设置失败 - 设备:{}, error: {}", device.getDeviceId(), e.getMessage());
+            throw new BusinessException(500, "音量设置失败:" + e.getMessage());
+        }
     }
 
     @Override
@@ -223,24 +284,19 @@ public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> impleme
         String source = "MINIAPP";
 
         OpenDoorResult openResult = hahaClient.getDeviceApi().openDoor(deviceId, String.valueOf(userId), openType, doorIndex, source);
-
-        // 4. 创建本地订单记录
-        Order order = new Order();
-        order.setOrderNo(OrderUtils.getOrderNo());
-        order.setActivityId(openResult.getActivityId());
-        order.setUserId(userId);
-        order.setDeviceId(deviceId);
-        order.setStatus(OrderStatus.PENDING_PAYMENT.getCode());
-        order.setPayStatus(PayStatus.UNPAID.getCode());
-        order.setCreateTime(LocalDateTime.now());
-
-        boolean saved = orderService.save(order);
-        if (!saved) {
-            log.warn("订单 {} 保存失败,但开门已成功", order.getOrderNo());
-        }
-
+        
+        // 4. 【修改】保存开门记录到数据库和 Redis(不再立即创建订单)
+        doorRecordService.saveDoorOpenRecord(
+            deviceId, 
+            userId, 
+            openResult.getActivityId(),
+            doorIndex,
+            openType,
+            source
+        );
+        
         // 5. 返回成功结果
-        log.info("开门成功 - 设备: {}, 活动ID: {}, 用户ID: {}", deviceId, openResult.getActivityId(), openResult.getUserId());
+        log.info("开门成功 - 设备:{}, 活动 ID: {}, 用户 ID: {}", deviceId, openResult.getActivityId(), openResult.getUserId());
 
         return OpenDoorVO.builder()
             .doorOpened(true)

+ 234 - 0
haha-service/src/main/java/com/haha/service/impl/DoorRecordServiceImpl.java

@@ -0,0 +1,234 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.entity.DoorRecord;
+import com.haha.mapper.DoorRecordMapper;
+import com.haha.service.DoorRecordService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 设备开关门记录服务实现类
+ * 
+ * @author haha
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DoorRecordServiceImpl extends ServiceImpl<DoorRecordMapper, DoorRecord> implements DoorRecordService {
+
+    private final StringRedisTemplate stringRedisTemplate;
+    private static final String DOOR_RECORD_REDIS_KEY = "haha:door:open:";
+    private static final long REDIS_EXPIRE_MINUTES = 30; // Redis 过期时间 30 分钟
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public DoorRecord saveDoorOpenRecord(String deviceId, Long userId, String activityId, 
+                                         String doorIndex, String openType, String source) {
+        try {
+            LocalDateTime now = LocalDateTime.now();
+            
+            // 1. 保存到数据库
+            DoorRecord record = new DoorRecord();
+            record.setActivityId(activityId);
+            record.setDeviceId(deviceId);
+            record.setUserId(userId);
+            record.setDoorIndex(doorIndex);
+            record.setOpenType(openType);
+            record.setDoorStatus("OPENED");
+            record.setNobuy(0); // 默认假设有消费,后续 AI 识别会更新
+            record.setOpenTime(now);
+            record.setSource(source);
+            
+            boolean saved = this.save(record);
+            if (saved) {
+                log.info("保存开门记录到数据库 - activityId: {}, deviceId: {}, userId: {}", 
+                        activityId, deviceId, userId);
+            } else {
+                log.error("保存开门记录到数据库失败 - activityId: {}", activityId);
+            }
+
+            // 2. 同时保存到 Redis(用于快速查询和临时状态)
+            saveToRedis(activityId, deviceId, userId, doorIndex, openType, now, source);
+
+            return record;
+            
+        } catch (Exception e) {
+            log.error("保存开门记录失败 - activityId: {}", activityId, e);
+            throw new RuntimeException("保存开门记录失败", e);
+        }
+    }
+
+    @Override
+    public DoorRecord getByActivityId(String activityId) {
+        // 优先从数据库查询
+        return baseMapper.getByActivityId(activityId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean updateDoorClosed(String activityId) {
+        try {
+            DoorRecord record = getByActivityId(activityId);
+            if (record == null) {
+                log.warn("关门记录不存在 - activityId: {}", activityId);
+                return false;
+            }
+
+            LocalDateTime closeTime = LocalDateTime.now();
+            int result = baseMapper.updateDoorStatus(activityId, "CLOSED", closeTime);
+            
+            if (result > 0) {
+                log.info("更新门状态为已关门 - activityId: {}, 持续时长:{}秒", 
+                        activityId, record.getDuration());
+                
+                // 同时更新 Redis
+                updateRedisDoorStatus(activityId, "CLOSED", closeTime);
+                return true;
+            } else {
+                log.error("更新门状态失败 - activityId: {}", activityId);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("更新门状态失败 - activityId: {}", activityId, e);
+            return false;
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean markNoConsume(String activityId) {
+        try {
+            int result = baseMapper.markNoConsume(activityId);
+            if (result > 0) {
+                log.info("标记无消费记录 - activityId: {}", activityId);
+                
+                // 同时更新 Redis
+                updateRedisNobuy(activityId, 1);
+                return true;
+            } else {
+                log.error("标记无消费记录失败 - activityId: {}", activityId);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("标记无消费记录失败 - activityId: {}", activityId, e);
+            return false;
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean linkOrderId(String activityId, Long orderId) {
+        try {
+            int result = baseMapper.linkOrderId(activityId, orderId);
+            if (result > 0) {
+                log.info("关联订单 ID - activityId: {}, orderId: {}", activityId, orderId);
+                
+                // 同时更新 Redis
+                updateRedisOrderId(activityId, orderId);
+                return true;
+            } else {
+                log.error("关联订单 ID 失败 - activityId: {}", activityId);
+                return false;
+            }
+        } catch (Exception e) {
+            log.error("关联订单 ID 失败 - activityId: {}", activityId, e);
+            return false;
+        }
+    }
+
+    @Override
+    public List<DoorRecord> getRecentRecords(Long userId, String deviceId, int limit) {
+        return baseMapper.getRecentRecords(userId, deviceId, limit);
+    }
+
+    @Override
+    public int countByTimeRange(String deviceId, LocalDateTime startTime, LocalDateTime endTime) {
+        return baseMapper.countByTimeRange(deviceId, startTime, endTime);
+    }
+
+    @Override
+    public void cleanupExpiredRedisRecords() {
+        // 这个方法可以定时调用,清理过期的 Redis 记录
+        // 由于数据库有唯一索引,不用担心数据丢失
+        log.info("清理过期 Redis 开门记录任务已执行");
+    }
+
+    @Override
+    public com.baomidou.mybatisplus.core.metadata.IPage<DoorRecord> getDeviceRecordsByPage(
+            String deviceId, int page, int pageSize) {
+        LambdaQueryWrapper<DoorRecord> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(DoorRecord::getDeviceId, deviceId)
+               .orderByDesc(DoorRecord::getOpenTime);
+        
+        return this.page(new com.baomidou.mybatisplus.extension.plugins.pagination.Page<>(page, pageSize), wrapper);
+    }
+
+    // ==================== Redis 操作方法 ====================
+
+    private void saveToRedis(String activityId, String deviceId, Long userId, 
+                            String doorIndex, String openType, LocalDateTime openTime, String source) {
+        try {
+            String key = DOOR_RECORD_REDIS_KEY + activityId;
+            Map<String, String> record = new HashMap<>();
+            record.put("activityId", activityId);
+            record.put("deviceId", deviceId);
+            record.put("userId", String.valueOf(userId));
+            record.put("doorIndex", doorIndex != null ? doorIndex : "");
+            record.put("openType", openType);
+            record.put("openTime", String.valueOf(openTime.toEpochSecond(java.time.ZoneOffset.UTC)));
+            record.put("source", source != null ? source : "");
+            record.put("status", "OPENED");
+            
+            stringRedisTemplate.opsForHash().putAll(key, record);
+            stringRedisTemplate.expire(key, REDIS_EXPIRE_MINUTES, TimeUnit.MINUTES);
+            
+            log.debug("保存开门记录到 Redis: activityId={}", activityId);
+        } catch (Exception e) {
+            log.error("保存开门记录到 Redis 失败 - activityId: {}", activityId, e);
+            // Redis 失败不影响数据库操作
+        }
+    }
+
+    private void updateRedisDoorStatus(String activityId, String doorStatus, LocalDateTime closeTime) {
+        try {
+            String key = DOOR_RECORD_REDIS_KEY + activityId;
+            stringRedisTemplate.opsForHash().put(key, "doorStatus", doorStatus);
+            stringRedisTemplate.opsForHash().put(key, "closeTime", 
+                String.valueOf(closeTime.toEpochSecond(java.time.ZoneOffset.UTC)));
+            log.debug("更新 Redis 门状态 - activityId: {}, status: {}", activityId, doorStatus);
+        } catch (Exception e) {
+            log.error("更新 Redis 门状态失败 - activityId: {}", activityId, e);
+        }
+    }
+
+    private void updateRedisNobuy(String activityId, int nobuy) {
+        try {
+            String key = DOOR_RECORD_REDIS_KEY + activityId;
+            stringRedisTemplate.opsForHash().put(key, "nobuy", String.valueOf(nobuy));
+            log.debug("更新 Redis 无消费标记 - activityId: {}", activityId);
+        } catch (Exception e) {
+            log.error("更新 Redis 无消费标记失败 - activityId: {}", activityId, e);
+        }
+    }
+
+    private void updateRedisOrderId(String activityId, Long orderId) {
+        try {
+            String key = DOOR_RECORD_REDIS_KEY + activityId;
+            stringRedisTemplate.opsForHash().put(key, "orderId", String.valueOf(orderId));
+            log.debug("更新 Redis 订单 ID - activityId: {}, orderId: {}", activityId, orderId);
+        } catch (Exception e) {
+            log.error("更新 Redis 订单 ID 失败 - activityId: {}", activityId, e);
+        }
+    }
+}

+ 45 - 2
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -10,12 +10,14 @@ import com.haha.common.enums.ProductAuditStatus;
 import com.haha.common.enums.RecognizeConsumeType;
 import com.haha.entity.Order;
 import com.haha.entity.OrderGoods;
+import com.haha.entity.DoorRecord;
 import com.haha.mapper.DeviceMapper;
 import com.haha.mapper.OrderGoodsMapper;
 import com.haha.sdk.HahaClient;
 import com.haha.service.HahaCallbackService;
 import com.haha.service.NewProductApplyService;
 import com.haha.service.OrderService;
+import com.haha.service.DoorRecordService;
 import com.haha.service.payment.payscore.PayScoreResult;
 import com.haha.service.payment.payscore.PayScoreService;
 import org.springframework.data.redis.core.StringRedisTemplate;
@@ -63,6 +65,9 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
     @Autowired
     private com.haha.common.config.CommonConfig commonConfig;
 
+    @Autowired
+    private DoorRecordService doorRecordService;
+
     private static final String DEVICE_STATUS_KEY = "haha:device:status:";
     private static final String RECOGNIZE_RESULT_KEY = "haha:recognize:result:";
     private static final String ORDER_INFO_KEY = "haha:order:info:";
@@ -121,7 +126,11 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
                         log.info("设备 {} 开门成功", deviceId);
                         break;
                     case CLOSED:
-                        log.info("设备 {} 关门成功,等待AI识别", deviceId);
+                        log.info("设备 {} 关门成功,等待 AI 识别", deviceId);
+                        // 更新开门记录为已关门状态
+                        if (activityId != null && !activityId.isEmpty()) {
+                            doorRecordService.updateDoorClosed(activityId);
+                        }
                         break;
                     case ERROR:
                         log.error("设备 {} 开门失败", deviceId);
@@ -275,19 +284,53 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
 
             saveRecognizeResultToRedis(activityId, params);
 
+            // 判断是否消费
             if (RecognizeConsumeType.isNoConsume(nobuy)) {
-                log.info("用户打开柜门但未消费,无需处理");
+                log.info("用户打开柜门但未消费,无需创建订单");
+                // 标记无消费记录
+                doorRecordService.markNoConsume(activityId);
+                // 更新关门状态
+                doorRecordService.updateDoorClosed(activityId);
                 return;
             }
 
             // 识别结果置信度
             BigDecimal confidence = parseBigDecimal(params.get("confidence"));
 
+            // 【修改】先尝试从开门记录获取信息创建订单
             Order existingOrder = orderService.getOrderByActivityId(activityId);
+            if (existingOrder == null) {
+                // 尝试从数据库获取开门记录并创建订单
+                DoorRecord record = doorRecordService.getByActivityId(activityId);
+                if (record != null) {
+                    log.info("根据开门记录创建订单 - activityId: {}, deviceId: {}, userId: {}", 
+                            activityId, record.getDeviceId(), record.getUserId());
+                    
+                    existingOrder = orderService.createOrderFromRecognition(
+                        activityId, 
+                        record.getDeviceId(), 
+                        String.valueOf(record.getUserId()),
+                        skuListStr, 
+                        resourceInfoStr, 
+                        confidence
+                    );
+                    
+                    // 关联开门记录和订单
+                    if (existingOrder != null) {
+                        doorRecordService.linkOrderId(activityId, existingOrder.getId());
+                    }
+                } else {
+                    log.warn("未找到开门记录 - activityId: {}", activityId);
+                }
+            }
+            
             if (existingOrder != null) {
                 log.info("更新订单信息 - activityId: {}, orderId: {}", activityId, existingOrder.getId());
                 updateOrderFromRecognition(existingOrder, skuListStr, resourceInfoStr, confidence, activityId);
                 logRecognizeResultDetails(resultStr, skuListStr, resourceInfoStr);
+                
+                // 更新关门状态和关联订单
+                doorRecordService.updateDoorClosed(activityId);
             }
 
         } catch (Exception e) {