Browse Source

运营平台调试

skyline 1 tháng trước cách đây
mục cha
commit
cae5703e3e

+ 239 - 0
haha-admin-web/src/api/layer-template.ts

@@ -0,0 +1,239 @@
+import { http } from "@/utils/http";
+
+/**
+ * API 响应结果类型
+ */
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+/**
+ * 分页查询响应结果类型
+ */
+type ResultTable = {
+  code: number;
+  message: string;
+  data?: {
+    list: Array<any>;
+    total: number;
+    pageSize: number;
+    currentPage: number;
+  };
+};
+
+/**
+ * 楼层配置类型定义
+ * FloorConfig type definition for layer template floor configuration
+ */
+export interface FloorConfig {
+  /** 楼层ID / Floor ID */
+  id?: number;
+
+  /** 楼层名称 / Floor name */
+  name?: string;
+
+  /** 楼层编号 / Floor number */
+  floorNumber?: number;
+
+  /** 商品列表 / Product list on this floor */
+  products?: Array<{
+    /** 商品ID / Product ID */
+    productId?: number;
+    
+    /** 商品名称 / Product name */
+    productName?: string;
+    
+    /** 商品条码 / Product barcode */
+    barcode?: string;
+    
+    /** 数量 / Quantity */
+    quantity?: number;
+    
+    /** 排序顺序 / Sort order */
+    sortOrder?: number;
+  }>;
+
+  /** 最大容量 / Maximum capacity */
+  maxCapacity?: number;
+
+  /** 当前使用数量 / Current usage count */
+  currentCount?: number;
+
+  /** 状态 (0-禁用, 1-启用) / Status (0-disabled, 1-enabled) */
+  status?: number;
+}
+
+/**
+ * 层模版类型定义
+ * LayerTemplate type definition for product layer template management
+ */
+export interface LayerTemplate {
+  /** 主键ID / Primary key ID */
+  id?: number;
+
+  /** 模版名称 / Template name */
+  templateName?: string;
+
+  /** 模版编码 / Template code */
+  templateCode?: string;
+
+  /** 设备ID / Device ID */
+  deviceId?: number;
+
+  /** 设备名称 / Device name */
+  deviceName?: string;
+
+  /** 设备编码 / Device code */
+  deviceCode?: string;
+
+  /** 楼层配置列表 / Floor configuration list */
+  floorConfigs?: FloorConfig[];
+
+  /** 总楼层数 / Total floor count */
+  totalFloors?: number;
+
+  /** 同步状态 (0-未同步, 1-已同步, 2-同步中, 3-同步失败) 
+   * Sync status (0-not synced, 1-synced, 2-syncing, 3-sync failed)
+   */
+  syncStatus?: number;
+
+  /** 最后同步时间 / Last sync time */
+  lastSyncTime?: string;
+
+  /** 状态 (0-禁用, 1-启用) / Status (0-disabled, 1-enabled) */
+  status?: number;
+
+  /** 备注 / Remarks */
+  remark?: string;
+
+  /** 创建时间 / Create time */
+  createTime?: string;
+
+  /** 更新时间 / Update time */
+  updateTime?: string;
+
+  /** 创建人 / Creator */
+  createBy?: string;
+
+  /** 更新人 / Updater */
+  updateBy?: string;
+}
+
+/**
+ * 分页查询参数类型
+ * Query parameters type for paginated layer template list query
+ */
+export interface LayerTemplateQueryParams {
+  /** 当前页码 / Current page number */
+  page?: number;
+
+  /** 每页大小 / Page size */
+  pageSize?: number;
+
+  /** 模版名称(模糊搜索) / Template name (fuzzy search) */
+  templateName?: string;
+
+  /** 模版编码 / Template code */
+  templateCode?: string;
+
+  /** 设备ID / Device ID */
+  deviceId?: number;
+
+  /** 设备编码 / Device code */
+  deviceCode?: string;
+
+  /** 同步状态 / Sync status */
+  syncStatus?: number;
+
+  /** 状态 / Status */
+  status?: number;
+}
+
+/**
+ * 批量同步请求参数
+ * Batch sync request parameters
+ */
+export type BatchSyncRequest = string[];
+
+/**
+ * 分页查询层模版列表
+ * Get paginated layer template list
+ * @param params - 查询参数 / Query parameters
+ * @returns Promise<ResultTable> - 包含分页数据的响应结果 / Response result with pagination data
+ */
+export const getLayerTemplateList = (params: LayerTemplateQueryParams) => {
+  return http.request<ResultTable>("get", "/layer-templates/list", { params });
+};
+
+/**
+ * 获取层模版详情
+ * Get layer template details by ID
+ * @param id - 层模版ID / Layer template ID
+ * @returns Promise<Result> - 包含层模版详情的响应结果 / Response result with layer template details
+ */
+export const getLayerTemplateById = (id: number) => {
+  return http.request<Result>("get", `/layer-templates/${id}`);
+};
+
+/**
+ * 根据设备ID获取层模版
+ * Get layer template by device ID
+ * @param deviceId - 设备ID / Device ID
+ * @returns Promise<Result> - 包含设备层模版的响应结果 / Response result with device's layer template
+ */
+export const getLayerTemplateByDeviceId = (deviceId: number) => {
+  return http.request<Result>("get", `/layer-templates/device/${deviceId}`);
+};
+
+/**
+ * 创建层模版
+ * Create a new layer template
+ * @param data - 层模版数据 / Layer template data
+ * @returns Promise<Result> - 创建结果的响应 / Response result of creation
+ */
+export const createLayerTemplate = (data: Partial<LayerTemplate>) => {
+  return http.request<Result>("post", "/layer-templates", { data });
+};
+
+/**
+ * 更新层模版
+ * Update an existing layer template
+ * @param id - 层模版ID / Layer template ID
+ * @param data - 更新数据 / Update data
+ * @returns Promise<Result> - 更新结果的响应 / Response result of update
+ */
+export const updateLayerTemplate = (id: number, data: Partial<LayerTemplate>) => {
+  return http.request<Result>("put", `/layer-templates/${id}`, { data });
+};
+
+/**
+ * 删除层模版
+ * Delete a layer template by ID
+ * @param id - 层模版ID / Layer template ID
+ * @returns Promise<Result> - 删除结果的响应 / Response result of deletion
+ */
+export const deleteLayerTemplate = (id: number) => {
+  return http.request<Result>("delete", `/layer-templates/${id}`);
+};
+
+/**
+ * 同步单个层模版
+ * Sync layer template from Haha platform for specific device
+ * @param deviceId - 设备ID / Device ID (SN number)
+ * @returns Promise<Result> - 同步结果的响应 / Response result of sync operation
+ */
+export const syncLayerTemplate = (deviceId: string) => {
+  return http.request<Result>("post", `/layer-templates/${deviceId}/sync`);
+};
+
+/**
+ * 批量同步层模版
+ * Batch sync multiple layer templates from Haha platform
+ * @param deviceIds - 设备ID列表 / List of device IDs (SN numbers) to sync
+ * @returns Promise<Result> - 批量同步结果的响应 / Response result of batch sync operation
+ */
+export const batchSyncLayerTemplates = (deviceIds: BatchSyncRequest) => {
+  return http.request<Result>("post", "/layer-templates/sync/batch", { data: deviceIds });
+};

+ 10 - 0
haha-admin-web/src/router/modules/layer-template.ts

@@ -0,0 +1,10 @@
+export default {
+  path: "/layer-template",
+  component: () => import("@/views/layer-template/index.vue"),
+  name: "LayerTemplate",
+  meta: {
+    title: "层模版管理",
+    icon: "ri:stack-line",
+    rank: 8
+  }
+} satisfies RouteConfigsTable;

+ 534 - 0
haha-admin-web/src/views/layer-template/components/EditDialog.vue

@@ -0,0 +1,534 @@
+<script setup lang="ts">
+import { ref, reactive, computed, watch, nextTick } from "vue";
+import { message } from "@/utils/message";
+import {
+  getLayerTemplateById,
+  createLayerTemplate,
+  updateLayerTemplate
+} from "@/api/layer-template";
+import { getProductList } from "@/api/product";
+import type { FormInstance, FormRules } from "element-plus";
+
+/**
+ * 楼层配置项类型定义
+ */
+interface FloorItem {
+  /** 层号 / Floor number */
+  floor: number;
+  /** 该层选择的商品ID数组 / Selected product IDs for this floor */
+  goods: number[];
+}
+
+/**
+ * 表单数据类型
+ */
+interface FormData {
+  /** 设备ID / Device ID */
+  deviceId: string | number;
+  /** 模板名称 / Template name */
+  templateName: string;
+  /** 左门楼层配置 / Left door floor configurations */
+  leftFloors: FloorItem[];
+  /** 右门楼层配置 / Right door floor configurations */
+  rightFloors: FloorItem[];
+}
+
+const emit = defineEmits<{
+  (e: "success"): void;
+}>();
+
+// 对话框可见状态
+const visible = ref(false);
+// 对话框标题
+const title = ref("新增层模版");
+// 是否为编辑模式
+const isEdit = computed(() => title.value === "修改");
+// 编辑时的记录ID
+const editId = ref<number | null>(null);
+
+// 表单引用
+const formRef = ref<FormInstance>();
+// 商品列表数据
+const productList = ref<any[]>([]);
+// 商品加载状态
+const productLoading = ref(false);
+
+// 表单数据
+const form = reactive<FormData>({
+  deviceId: "",
+  templateName: "",
+  leftFloors: [],
+  rightFloors: []
+});
+
+// 表单校验规则
+const rules = computed<FormRules>(() => ({
+  deviceId: [
+    {
+      required: true,
+      message: "请输入设备ID",
+      trigger: "blur"
+    },
+    {
+      pattern: /^[0-9]+$/,
+      message: "设备ID必须为数字",
+      trigger: "blur"
+    }
+  ],
+  templateName: [
+    {
+      required: true,
+      message: "请输入模板名称",
+      trigger: "blur"
+    },
+    {
+      min: 2,
+      max: 50,
+      message: "模板名称长度在2到50个字符之间",
+      trigger: "blur"
+    }
+  ]
+}));
+
+/**
+ * 获取商品列表
+ * Fetch product list for selection
+ */
+async function fetchProductList() {
+  productLoading.value = true;
+  try {
+    const { data } = await getProductList({
+      page: 1,
+      pageSize: 1000 // 获取足够多的商品供选择
+    });
+    productList.value = data.list || [];
+  } catch (error) {
+    console.error("获取商品列表失败:", error);
+    message("获取商品列表失败", { type: "error" });
+  } finally {
+    productLoading.value = false;
+  }
+}
+
+/**
+ * 重置表单数据
+ * Reset form data to initial state
+ */
+function resetForm() {
+  form.deviceId = "";
+  form.templateName = "";
+  form.leftFloors = [];
+  form.rightFloors = [];
+  editId.value = null;
+  nextTick(() => {
+    formRef.value?.clearValidate();
+  });
+}
+
+/**
+ * 打开对话框
+ * Open dialog
+ * @param dialogTitle - 对话框标题
+ * @param row - 编辑时的行数据(可选)
+ */
+async function open(dialogTitle?: string, row?: any) {
+  title.value = dialogTitle || "新增层模版";
+  resetForm();
+  visible.value = true;
+
+  // 加载商品列表
+  await fetchProductList();
+
+  // 如果是编辑模式,回显数据
+  if (row && row.id) {
+    editId.value = row.id;
+    await loadTemplateData(row.id);
+  }
+}
+
+/**
+ * 加载模版详情数据
+ * Load template detail data by ID
+ * @param id - 模版ID
+ */
+async function loadTemplateData(id: number) {
+  try {
+    const { data } = await getLayerTemplateById(id);
+    if (data) {
+      form.deviceId = data.deviceId || "";
+      form.templateName = data.templateName || "";
+
+      // 解析左右门楼层配置
+      if (data.floorConfigs && Array.isArray(data.floorConfigs)) {
+        // 假设前半部分为左门,后半部分为右门(根据实际业务逻辑调整)
+        const midPoint = Math.ceil(data.floorConfigs.length / 2);
+        form.leftFloors = data.floorConfigs
+          .slice(0, midPoint)
+          .map((config: any) => ({
+            floor: config.floorNumber || config.name || 0,
+            goods:
+              config.products?.map((p: any) => p.productId).filter(
+                (id: number) => id != null
+              ) || []
+          }));
+        form.rightFloors = data.floorConfigs.slice(midPoint).map((config: any) => ({
+          floor: config.floorNumber || config.name || 0,
+          goods:
+            config.products?.map((p: any) => p.productId).filter(
+              (id: number) => id != null
+            ) || []
+        }));
+      }
+    }
+  } catch (error) {
+    console.error("加载模版数据失败:", error);
+    message("加载模版数据失败", { type: "error" });
+  }
+}
+
+/**
+ * 添加左门楼层
+ * Add left door floor
+ */
+function addLeftFloor() {
+  const nextFloor =
+    form.leftFloors.length > 0
+      ? Math.max(...form.leftFloors.map(f => f.floor)) + 1
+      : 1;
+  form.leftFloors.push({
+    floor: nextFloor,
+    goods: []
+  });
+}
+
+/**
+ * 删除左门楼层
+ * Remove left door floor
+ * @param index - 要删除的索引
+ */
+function removeLeftFloor(index: number) {
+  form.leftFloors.splice(index, 1);
+  // 重新编号
+  form.leftFloors.forEach((floor, idx) => {
+    floor.floor = idx + 1;
+  });
+}
+
+/**
+ * 添加右门楼层
+ * Add right door floor
+ */
+function addRightFloor() {
+  const nextFloor =
+    form.rightFloors.length > 0
+      ? Math.max(...form.rightFloors.map(f => f.floor)) + 1
+      : 1;
+  form.rightFloors.push({
+    floor: nextFloor,
+    goods: []
+  });
+}
+
+/**
+ * 删除右门楼层
+ * Remove right door floor
+ * @param index - 要删除的索引
+ */
+function removeRightFloor(index: number) {
+  form.rightFloors.splice(index, 1);
+  // 重新编号
+  form.rightFloors.forEach((floor, idx) => {
+    floor.floor = idx + 1;
+  });
+}
+
+/**
+ * 构造提交数据格式
+ * Build submit data format matching backend DTO
+ */
+function buildSubmitData() {
+  // 构造楼层配置数组
+  const floorConfigs = [
+    ...form.leftFloors.map(f => ({
+      floorNumber: f.floor,
+      name: `左门第${f.floor}层`,
+      products: f.goods.map(productId => ({
+        productId,
+        quantity: 1,
+        sortOrder: 0
+      }))
+    })),
+    ...form.rightFloors.map(f => ({
+      floorNumber: f.floor,
+      name: `右门第${f.floor}层`,
+      products: f.goods.map(productId => ({
+        productId,
+        quantity: 1,
+        sortOrder: 0
+      }))
+    }))
+  ];
+
+  return {
+    deviceId: parseInt(form.deviceId.toString(), 10),
+    templateName: form.templateName.trim(),
+    totalFloors: form.leftFloors.length + form.rightFloors.length,
+    floorConfigs,
+    status: 1 // 默认启用
+  };
+}
+
+/**
+ * 提交表单
+ * Submit form
+ */
+async function handleSubmit() {
+  if (!formRef.value) return;
+
+  try {
+    // 表单校验
+    await formRef.value.validate();
+
+    // 至少需要一层配置
+    if (form.leftFloors.length === 0 && form.rightFloors.length === 0) {
+      message("请至少添加一个楼层的配置", { type: "warning" });
+      return;
+    }
+
+    const submitData = buildSubmitData();
+    let res;
+
+    if (isEdit.value && editId.value) {
+      // 更新操作
+      res = await updateLayerTemplate(editId.value, submitData);
+    } else {
+      // 新增操作
+      res = await createLayerTemplate(submitData);
+    }
+
+    if (res.code === 0) {
+      message(
+        `${isEdit.value ? "修改" : "新增"}层模版成功`,
+        { type: "success" }
+      );
+      visible.value = false;
+      emit("success"); // 通知父组件刷新列表
+    } else {
+      message(res.message || "操作失败", { type: "error" });
+    }
+  } catch (error: any) {
+    if (error !== false) {
+      // 非校验错误
+      console.error("提交表单失败:", error);
+      message("提交失败,请检查输入", { type: "error" });
+    }
+  }
+}
+
+/**
+ * 关闭对话框
+ * Close dialog
+ */
+function handleClose() {
+  visible.value = false;
+  resetForm();
+}
+
+// 暴露方法给父组件调用
+defineExpose({
+  open
+});
+</script>
+
+<template>
+  <el-dialog
+    v-model="visible"
+    :title="title"
+    width="800px"
+    destroy-on-close
+    :close-on-click-modal="false"
+    @close="handleClose"
+  >
+    <el-form
+      ref="formRef"
+      :model="form"
+      :rules="rules"
+      label-width="100px"
+      class="template-form"
+    >
+      <!-- 基本信息 -->
+      <el-divider content-position="left">基本信息</el-divider>
+
+      <el-form-item label="设备ID" prop="deviceId">
+        <el-input
+          v-model="form.deviceId"
+          placeholder="请输入设备ID"
+          :disabled="isEdit"
+          clearable
+        />
+      </el-form-item>
+
+      <el-form-item label="模板名称" prop="templateName">
+        <el-input
+          v-model="form.templateName"
+          placeholder="请输入模板名称"
+          clearable
+          maxlength="50"
+          show-word-limit
+        />
+      </el-form-item>
+
+      <!-- 左门配置 -->
+      <el-divider content-position="left">左门配置</el-divider>
+
+      <div class="floors-container">
+        <div
+          v-for="(floor, index) in form.leftFloors"
+          :key="'left-' + index"
+          class="floor-card"
+        >
+          <el-card shadow="hover">
+            <template #header>
+              <div class="card-header">
+                <span>第 {{ floor.floor }} 层</span>
+                <el-button
+                  type="danger"
+                  size="small"
+                  text
+                  @click="removeLeftFloor(index)"
+                >
+                  删除此层
+                </el-button>
+              </div>
+            </template>
+            <el-select
+              v-model="floor.goods"
+              multiple
+              filterable
+              collapse-tags
+              collapse-tags-tooltip
+              placeholder="选择该层的商品"
+              :loading="productLoading"
+              class="w-full!"
+            >
+              <el-option
+                v-for="product in productList"
+                :key="product.id"
+                :label="`${product.name} (${product.barcode || '无条码'})`"
+                :value="product.id"
+              />
+            </el-select>
+          </el-card>
+        </div>
+
+        <el-button
+          type="primary"
+          plain
+          class="add-floor-btn"
+          @click="addLeftFloor"
+        >
+          + 添加左门层
+        </el-button>
+      </div>
+
+      <!-- 右门配置 -->
+      <el-divider content-position="left">右门配置</el-divider>
+
+      <div class="floors-container">
+        <div
+          v-for="(floor, index) in form.rightFloors"
+          :key="'right-' + index"
+          class="floor-card"
+        >
+          <el-card shadow="hover">
+            <template #header>
+              <div class="card-header">
+                <span>第 {{ floor.floor }} 层</span>
+                <el-button
+                  type="danger"
+                  size="small"
+                  text
+                  @click="removeRightFloor(index)"
+                >
+                  删除此层
+                </el-button>
+              </div>
+            </template>
+            <el-select
+              v-model="floor.goods"
+              multiple
+              filterable
+              collapse-tags
+              collapse-tags-tooltip
+              placeholder="选择该层的商品"
+              :loading="productLoading"
+              class="w-full!"
+            >
+              <el-option
+                v-for="product in productList"
+                :key="product.id"
+                :label="`${product.name} (${product.barcode || '无条码'})`"
+                :value="product.id"
+              />
+            </el-select>
+          </el-card>
+        </div>
+
+        <el-button
+          type="primary"
+          plain
+          class="add-floor-btn"
+          @click="addRightFloor"
+        >
+          + 添加右门层
+        </el-button>
+      </div>
+    </el-form>
+
+    <template #footer>
+      <el-button @click="handleClose">取消</el-button>
+      <el-button type="primary" :loading="false" @click="handleSubmit">
+        确定
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.template-form {
+  max-height: 65vh;
+  overflow-y: auto;
+  padding-right: 10px;
+
+  .floors-container {
+    display: flex;
+    flex-direction: column;
+    gap: 12px;
+    margin-bottom: 16px;
+
+    .floor-card {
+      .card-header {
+        display: flex;
+        justify-content: space-between;
+        align-items: center;
+        font-weight: 500;
+      }
+
+      :deep(.el-card__body) {
+        padding-top: 16px;
+      }
+    }
+
+    .add-floor-btn {
+      width: 100%;
+      border-style: dashed;
+    }
+  }
+
+  :deep(.el-divider__text) {
+    font-size: 15px;
+    font-weight: 600;
+    color: var(--el-color-primary);
+  }
+}
+</style>

+ 283 - 0
haha-admin-web/src/views/layer-template/index.vue

@@ -0,0 +1,283 @@
+<script setup lang="ts">
+import { ref } from "vue";
+import { useProduct } from "./utils/hook";
+import { PureTableBar } from "@/components/RePureTableBar";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import EditDialog from "./components/EditDialog.vue";
+import Refresh from "~icons/ep/refresh";
+import Delete from "~icons/ep/delete";
+import EditPen from "~icons/ep/edit-pen";
+import AddFill from "~icons/ri/add-circle-line";
+import SyncAll from "~icons/ep/upload"; // 全量同步图标
+import SyncOne from "~icons/ep/refresh-right"; // 单个设备同步图标
+
+defineOptions({
+  name: "LayerTemplateManage"
+});
+
+const formRef = ref();
+const tableRef = ref();
+const editDialogRef = ref();
+
+const {
+  form,
+  loading,
+  columns,
+  dataList,
+  pagination,
+  syncStatusOptions,
+
+  // 设备相关(新增)
+  deviceOptions,
+  deviceLoading,
+  selectedDeviceId,
+  syncAllLoading,
+
+  onSearch,
+  resetForm,
+  handleDelete,
+  handleSyncByDevice,
+  handleSyncAll,
+  loadDeviceList,
+  handleSizeChange,
+  handleCurrentChange
+} = useProduct(tableRef);
+
+/**
+ * 打开新增/编辑对话框
+ * Open add/edit dialog
+ * @param title - 对话框标题
+ * @param row - 编辑时的行数据(可选)
+ */
+function openDialog(title?: string, row?: any) {
+  editDialogRef.value?.open(title || "新增层模版", row);
+}
+
+/**
+ * 处理单个设备同步
+ * Handle single device sync (from dropdown selection)
+ */
+function handleSingleSync() {
+  if (!selectedDeviceId.value) {
+    // 如果没有选择设备,提示用户
+    const msg = {
+      type: "warning" as const,
+      message: "请先选择要同步的设备"
+    };
+    import("@/utils/message").then(({ message }) => message(msg));
+    return;
+  }
+  handleSyncByDevice(selectedDeviceId.value);
+}
+</script>
+
+<template>
+  <div class="main">
+    <!-- 搜索表单区域 -->
+    <el-form
+      ref="formRef"
+      :inline="true"
+      :model="form"
+      class="search-form bg-bg_color w-full pl-8 pt-[12px] overflow-auto"
+    >
+      <el-form-item label="设备ID:" prop="deviceId">
+        <el-input
+          v-model="form.deviceId"
+          placeholder="请输入设备ID"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="模板名称:" prop="templateName">
+        <el-input
+          v-model="form.templateName"
+          placeholder="请输入模板名称"
+          clearable
+          class="w-[180px]!"
+        />
+      </el-form-item>
+      <el-form-item label="同步状态:" prop="syncStatus">
+        <el-select
+          v-model="form.syncStatus"
+          placeholder="请选择状态"
+          clearable
+          class="w-[180px]!"
+        >
+          <el-option
+            v-for="item in syncStatusOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon('ri/search-line')"
+          :loading="loading"
+          @click="onSearch"
+        >
+          搜索
+        </el-button>
+        <el-button :icon="useRenderIcon(Refresh)" @click="resetForm(formRef)">
+          重置
+        </el-button>
+      </el-form-item>
+    </el-form>
+
+    <!-- 表格区域 -->
+    <PureTableBar
+      title="层模版管理"
+      :columns="columns"
+      @refresh="onSearch"
+    >
+      <!-- 操作按钮区域:包含新增、设备选择同步、全量同步 -->
+      <template #buttons>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(AddFill)"
+          @click="openDialog()"
+        >
+          新增层模版
+        </el-button>
+
+        <!-- 分隔线 -->
+        <el-divider direction="vertical" class="!mx-2" />
+
+        <!-- 单个设备同步:设备选择器 + 同步按钮 -->
+        <el-select
+          v-model="selectedDeviceId"
+          placeholder="请选择要同步的设备"
+          filterable
+          clearable
+          class="w-[240px]!"
+          :loading="deviceLoading"
+          :disabled="syncAllLoading"
+          @visible-change="(visible: boolean) => visible && loadDeviceList()"
+        >
+          <template #empty>
+            <div class="flex flex-col items-center py-3">
+              <p class="text-gray-500 text-sm">{{ deviceLoading ? '加载中...' : '暂无可用设备' }}</p>
+              <el-button
+                v-if="!deviceLoading && deviceOptions.length === 0"
+                type="primary"
+                link
+                size="small"
+                class="mt-2"
+                @click="loadDeviceList()"
+              >
+                点击重新加载
+              </el-button>
+            </div>
+          </template>
+          <el-option
+            v-for="device in deviceOptions"
+            :key="device.deviceId"
+            :label="device.label"
+            :value="device.deviceId"
+          >
+            <div class="flex items-center justify-between w-full">
+              <span>{{ device.label }}</span>
+              <el-tag
+                v-if="device.isOnline !== undefined && device.isOnline !== null"
+                size="small"
+                :type="device.isOnline === true ? 'success' : 'info'"
+                effect="light"
+                round
+              >
+                {{ device.isOnline === true ? '在线' : '离线' }}
+              </el-tag>
+            </div>
+          </el-option>
+        </el-select>
+
+        <el-button
+          type="success"
+          :icon="useRenderIcon(SyncOne)"
+          :disabled="!selectedDeviceId || syncAllLoading"
+          @click="handleSingleSync()"
+        >
+          同步选中设备
+        </el-button>
+
+        <!-- 全量同步按钮 -->
+        <el-button
+          type="warning"
+          :icon="useRenderIcon(SyncAll)"
+          :loading="syncAllLoading"
+          @click="handleSyncAll()"
+        >
+          {{ syncAllLoading ? '同步中...' : '全量同步所有设备' }}
+        </el-button>
+      </template>
+
+      <template v-slot="{ size, dynamicColumns }">
+        <pure-table
+          ref="tableRef"
+          row-key="id"
+          adaptive
+          :adaptiveConfig="{ offsetBottom: 108 }"
+          align-whole="center"
+          table-layout="auto"
+          :loading="loading"
+          :size="size"
+          :data="dataList"
+          :columns="dynamicColumns"
+          :pagination="{ ...pagination, size }"
+          :header-cell-style="{
+            background: 'var(--el-fill-color-light)',
+            color: 'var(--el-text-color-primary)'
+          }"
+          @page-size-change="handleSizeChange"
+          @page-current-change="handleCurrentChange"
+        >
+          <!-- 操作列插槽 -->
+          <template #operation="{ row }">
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(EditPen)"
+              @click="openDialog('修改', row)"
+            >
+              编辑
+            </el-button>
+            <el-popconfirm
+              :title="`是否确认删除模板 ${row.templateName}?`"
+              @confirm="handleDelete(row)"
+            >
+              <template #reference>
+                <el-button
+                  class="reset-margin"
+                  link
+                  type="danger"
+                  :size="size"
+                  :icon="useRenderIcon(Delete)"
+                >
+                  删除
+                </el-button>
+              </template>
+            </el-popconfirm>
+          </template>
+        </pure-table>
+      </template>
+    </PureTableBar>
+
+    <!-- 编辑对话框组件 -->
+    <EditDialog ref="editDialogRef" @success="onSearch" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.main-content {
+  margin: 24px 24px 0 !important;
+}
+
+.search-form {
+  :deep(.el-form-item) {
+    margin-bottom: 12px;
+  }
+}
+</style>

+ 498 - 0
haha-admin-web/src/views/layer-template/utils/hook.ts

@@ -0,0 +1,498 @@
+import dayjs from "dayjs";
+import { message } from "@/utils/message";
+import type { PaginationProps } from "@pureadmin/table";
+import {
+  getLayerTemplateList,
+  deleteLayerTemplate,
+  syncLayerTemplate,
+  batchSyncLayerTemplates
+} from "@/api/layer-template";
+import { getDeviceList } from "@/api/device";
+import { ElMessageBox } from "element-plus";
+import { type Ref, ref, reactive, onMounted, h } from "vue";
+import { ElTag } from "element-plus";
+
+/**
+ * 层模版搜索表单类型定义
+ */
+export interface LayerTemplateSearchForm {
+  /** 设备ID / Device ID */
+  deviceId: string;
+  /** 模板名称 / Template name */
+  templateName: string;
+  /** 同步状态 / Sync status */
+  syncStatus: string | number;
+}
+
+/**
+ * 层模版数据项类型
+ */
+export interface LayerTemplateItem {
+  /** 主键ID / Primary key ID */
+  id?: number;
+  /** 模版名称 / Template name */
+  templateName?: string;
+  /** 模版编码 / Template code */
+  templateCode?: string;
+  /** 设备ID / Device ID */
+  deviceId?: number | string;
+  /** 设备名称 / Device name */
+  deviceName?: string;
+  /** 设备编码 / Device code */
+  deviceCode?: string;
+  /** 总楼层数 / Total floor count */
+  totalFloors?: number;
+  /** 同步状态 (0-未同步, 1-已同步, 2-同步中, 3-同步失败) */
+  syncStatus?: number;
+  /** 最后同步时间 / Last sync time */
+  lastSyncTime?: string;
+  /** 状态 (0-禁用, 1-启用) */
+  status?: number;
+  /** 备注 / Remarks */
+  remark?: string;
+  /** 创建时间 / Create time */
+  createTime?: string;
+  /** 更新时间 / Update time */
+  updateTime?: string;
+}
+
+/**
+ * 设备选项类型(用于下拉选择)
+ */
+export interface DeviceOption {
+  /** 设备ID / Device ID (SN号) */
+  deviceId: string;
+  /** 设备名称/编码 / Device name or code */
+  label: string;
+  /** 在线状态 / Online status */
+  isOnline?: number;
+}
+
+export function useProduct(tableRef?: Ref) {
+  // 搜索表单状态
+  const form = reactive<LayerTemplateSearchForm>({
+    deviceId: "",
+    templateName: "",
+    syncStatus: ""
+  });
+
+  // 数据列表状态
+  const dataList = ref<LayerTemplateItem[]>([]);
+  // 加载状态
+  const loading = ref(false);
+  // 分页配置
+  const pagination = reactive<PaginationProps>({
+    total: 0,
+    pageSize: 10,
+    currentPage: 1,
+    background: true
+  });
+
+  // ========== 新增:设备列表相关状态 ==========
+  // 设备选项列表(用于操作栏的下拉选择)
+  const deviceOptions = ref<DeviceOption[]>([]);
+  // 设备列表加载状态
+  const deviceLoading = ref(false);
+  // 当前选中的设备ID(用于单个设备同步)
+  const selectedDeviceId = ref<string>("");
+  // 全量同步加载状态
+  const syncAllLoading = ref(false);
+
+  // 同步状态选项
+  const syncStatusOptions = [
+    { label: "未同步", value: 0 },
+    { label: "已同步", value: 1 },
+    { label: "同步失败", value: 3 }
+  ];
+
+  // 表格列定义
+  const columns: TableColumnList = [
+    {
+      label: "ID",
+      prop: "id",
+      width: 70
+    },
+    {
+      label: "模板名称",
+      prop: "templateName",
+      minWidth: 150,
+      showOverflowTooltip: true
+    },
+    {
+      label: "设备ID",
+      prop: "deviceId",
+      minWidth: 100
+    },
+    {
+      label: "设备编码",
+      prop: "deviceCode",
+      minWidth: 120,
+      showOverflowTooltip: true
+    },
+    {
+      label: "机柜层数",
+      prop: "totalFloors",
+      width: 100,
+      formatter: ({ totalFloors }) => (totalFloors != null ? `${totalFloors}层` : "-")
+    },
+    {
+      label: "同步状态",
+      prop: "syncStatus",
+      width: 100,
+      cellRenderer: ({ row }) => {
+        const statusMap: Record<number, { label: string; type: string }> = {
+          0: { label: "未同步", type: "info" },
+          1: { label: "已同步", type: "success" },
+          2: { label: "同步中", type: "warning" },
+          3: { label: "同步失败", type: "danger" }
+        };
+        const status = statusMap[row.syncStatus] || {
+          label: "未知",
+          type: "info"
+        };
+        return h(ElTag, { type: status.type as any }, () => status.label);
+      }
+    },
+    {
+      label: "最后同步时间",
+      prop: "lastSyncTime",
+      minWidth: 160,
+      formatter: ({ lastSyncTime }) =>
+        lastSyncTime ? dayjs(lastSyncTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "创建时间",
+      prop: "createTime",
+      minWidth: 160,
+      formatter: ({ createTime }) =>
+        createTime ? dayjs(createTime).format("YYYY-MM-DD HH:mm:ss") : "-"
+    },
+    {
+      label: "操作",
+      fixed: "right",
+      width: 200,
+      slot: "operation"
+    }
+  ];
+
+  /**
+   * 加载设备列表(用于同步时选择设备)
+   * Load device list for device selection during sync
+   */
+  async function loadDeviceList() {
+    if (deviceLoading.value) return;
+
+    deviceLoading.value = true;
+    try {
+      // 调用设备列表API获取所有设备
+      // 注意:参数名必须是 pageSize(不是 limit)
+      const res = await getDeviceList({ page: 1, pageSize: 9999 });
+
+      console.log("设备API返回数据:", res); // 调试日志
+
+      if (res.code === 0 && res.data?.list && Array.isArray(res.data.list)) {
+        // 根据 Device 实体类的实际字段进行映射:
+        // - deviceId: 设备ID/SN号
+        // - name: 设备名称(注意不是 deviceName!)
+        // - isOnline: Boolean 类型在线状态
+        // - status: Integer 类型状态码
+        deviceOptions.value = res.data.list
+          .filter((device: any) => device && device.deviceId) // 过滤掉无效数据
+          .map((device: any) => ({
+            deviceId: String(device.deviceId || ""), // 确保转换为字符串
+            label:
+              device.name ||
+              device.deviceName ||
+              `设备 ${device.deviceId || "未知"}`,
+            isOnline: device.isOnline ?? (device.status === 1), // 兼容处理
+            status: device.status // 保留原始状态值
+          }));
+
+        console.log("转换后的设备选项:", deviceOptions.value); // 调试日志
+
+        if (deviceOptions.value.length === 0) {
+          console.warn("设备列表为空,原始数据:", res.data.list);
+        }
+      } else {
+        console.warn("获取设备列表返回异常:", res);
+        deviceOptions.value = [];
+      }
+    } catch (error) {
+      console.error("加载设备列表失败:", error);
+      message("加载设备列表失败,请检查网络连接", { type: "warning" });
+      deviceOptions.value = [];
+    } finally {
+      deviceLoading.value = false;
+    }
+  }
+
+  /**
+   * 搜索查询方法
+   * Search query method
+   */
+  async function onSearch() {
+    loading.value = true;
+    try {
+      const searchParams: any = {
+        page: pagination.currentPage,
+        pageSize: pagination.pageSize
+      };
+
+      // 只添加非空的搜索条件
+      if (form.deviceId) searchParams.deviceId = form.deviceId.trim();
+      if (form.templateName)
+        searchParams.templateName = form.templateName.trim();
+      if (form.syncStatus !== "" && form.syncStatus != null) {
+        searchParams.syncStatus = parseInt(form.syncStatus.toString(), 10);
+      }
+
+      const { data } = await getLayerTemplateList(searchParams);
+      dataList.value = data.list || [];
+      pagination.total = data.total || 0;
+    } catch (error) {
+      console.error("获取层模版列表失败:", error);
+      message("获取层模版列表失败", { type: "error" });
+    } finally {
+      setTimeout(() => {
+        loading.value = false;
+      }, 300);
+    }
+  }
+
+  /**
+   * 重置表单并重新查询
+   * Reset form and re-query
+   */
+  function resetForm(formEl?: any) {
+    if (!formEl) return;
+    formEl.resetFields();
+    pagination.currentPage = 1;
+    onSearch();
+  }
+
+  /**
+   * 分页大小改变处理
+   * Handle page size change
+   */
+  function handleSizeChange(val: number) {
+    pagination.pageSize = val;
+    onSearch();
+  }
+
+  /**
+   * 当前页码改变处理
+   * Handle current page change
+   */
+  function handleCurrentChange(val: number) {
+    pagination.currentPage = val;
+    onSearch();
+  }
+
+  /**
+   * 打开新增/编辑对话框
+   * Open add/edit dialog
+   * @param title - 对话框标题
+   * @param row - 编辑时的行数据(可选)
+   */
+  function openDialog(title?: string, row?: LayerTemplateItem) {
+    // 此方法将在index.vue中与EditDialog组件配合使用
+    // 通过事件或ref调用EditDialog的打开方法
+    console.log("openDialog called with:", title, row);
+    // 触发自定义事件或通过其他方式通知父组件
+  }
+
+  /**
+   * 删除层模版
+   * Delete layer template
+   * @param row - 要删除的行数据
+   */
+  async function handleDelete(row: LayerTemplateItem) {
+    try {
+      await ElMessageBox.confirm(
+        `确认要删除模板 "${row.templateName}" 吗?`,
+        "删除确认",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }
+      );
+
+      const res = await deleteLayerTemplate(row.id!);
+      if (res.code === 0) {
+        message(`成功删除模板 ${row.templateName}`, { type: "success" });
+        onSearch();
+      } else {
+        message(res.message || "删除失败", { type: "error" });
+      }
+    } catch (error: any) {
+      if (error !== "cancel") {
+        message("删除操作失败", { type: "error" });
+      }
+    }
+  }
+
+  /**
+   * 根据选择的设备ID同步单个设备的层模版
+   * Sync layer template for a specific selected device
+   * @param deviceId - 设备ID(从下拉框选择的)
+   */
+  async function handleSyncByDevice(deviceId: string) {
+    if (!deviceId) {
+      message("请先选择要同步的设备", { type: "warning" });
+      return;
+    }
+
+    try {
+      // 查找设备显示名称
+      const device = deviceOptions.value.find(d => d.deviceId === deviceId);
+      const deviceLabel = device?.label || deviceId;
+
+      await ElMessageBox.confirm(
+        `确认要同步设备 "${deviceLabel}" 的层模版信息吗?`,
+        "同步确认",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }
+      );
+
+      const res = await syncLayerTemplate(deviceId);
+      if (res.code === 0) {
+        message(`已提交设备 "${deviceLabel}" 的同步请求`, { type: "success" });
+        // 延迟刷新以获取最新状态
+        setTimeout(() => {
+          onSearch();
+        }, 2000);
+      } else {
+        message(res.message || "同步失败", { type: "error" });
+      }
+    } catch (error: any) {
+      if (error !== "cancel") {
+        console.error("同步失败:", error);
+        message("同步操作失败", { type: "error" });
+      }
+    }
+  }
+
+  /**
+   * 全量同步所有设备的层模版(默认行为)
+   * Batch sync all devices' layer templates (default behavior)
+   *
+   * 流程:
+   * 1. 从设备列表获取所有设备的deviceId
+   * 2. 批量调用同步接口
+   */
+  async function handleSyncAll() {
+    console.log("开始全量同步, 当前设备列表长度:", deviceOptions.value.length);
+
+    // 如果设备列表还没加载或为空,先加载
+    if (deviceOptions.value.length === 0) {
+      console.log("设备列表为空, 尝试重新加载...");
+      await loadDeviceList();
+    }
+
+    // 再次检查
+    if (deviceOptions.value.length === 0) {
+      console.error("全量同步失败: 设备列表仍为空");
+      message(
+        "没有可用的设备数据,无法执行全量同步\n\n可能原因:\n1. 数据库中没有设备记录\n2. 设备列表API返回异常\n3. 网络连接问题",
+        { type: "warning", duration: 5000 }
+      );
+      return;
+    }
+
+    try {
+      const deviceCount = deviceOptions.value.length;
+      await ElMessageBox.confirm(
+        `确认要同步全部 ${deviceCount} 个设备的层模版信息吗?\n\n此操作将从哈哈零兽平台拉取每个设备的最新层模版配置。\n\n预计耗时:${Math.ceil(deviceCount / 10)} 秒`,
+        "全量同步确认",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }
+      );
+
+      syncAllLoading.value = true;
+
+      // 提取所有设备的deviceId(确保都是非空字符串)
+      const allDeviceIds = deviceOptions.value
+        .map(d => d.deviceId)
+        .filter(id => id && id.trim() !== ""); // 过滤空值
+
+      console.log("准备同步的设备ID列表:", allDeviceIds);
+
+      if (allDeviceIds.length === 0) {
+        message("所有设备的ID都为空,无法执行同步", { type: "error" });
+        return;
+      }
+
+      const res = await batchSyncLayerTemplates(allDeviceIds);
+      console.log("批量同步API返回:", res);
+
+      if (res.code === 0) {
+        message(`成功提交 ${allDeviceIds.length} 个设备的同步请求`, {
+          type: "success"
+        });
+        // 延迟刷新以获取最新状态
+        setTimeout(() => {
+          onSearch();
+        }, 3000); // 全量同步等待更长时间
+      } else {
+        message(res.message || "全量同步失败", { type: "error" });
+      }
+    } catch (error: any) {
+      if (error !== "cancel") {
+        console.error("全量同步失败:", error);
+        message("全量同步操作失败: " + (error.message || "未知错误"), { type: "error" });
+      }
+    } finally {
+      syncAllLoading.value = false;
+    }
+  }
+
+  // 组件挂载时自动加载数据和设备列表
+  onMounted(async () => {
+    // 并行加载层模版列表和设备列表
+    await Promise.all([onSearch(), loadDeviceList()]);
+  });
+
+  return {
+    // 状态
+    form,
+    loading,
+    dataList,
+    pagination,
+    columns,
+    syncStatusOptions,
+
+    // 设备相关状态(新增)
+    deviceOptions,
+    deviceLoading,
+    selectedDeviceId,
+    syncAllLoading,
+
+    // 方法
+    onSearch,
+    resetForm,
+    openDialog,
+    handleDelete,
+
+    // 同步相关方法(重构)
+    handleSyncByDevice, // 选择单个设备同步
+    handleSyncAll, // 全量同步所有设备
+
+    // 兼容旧接口(保持向后兼容)
+    handleSync: handleSyncByDevice,
+    handleBatchSync: handleSyncAll,
+
+    // 分页方法
+    handleSizeChange,
+    handleCurrentChange,
+
+    // 工具方法
+    loadDeviceList // 手动刷新设备列表
+  };
+}

+ 168 - 0
haha-admin/src/main/java/com/haha/admin/controller/LayerTemplateController.java

@@ -0,0 +1,168 @@
+package com.haha.admin.controller;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.dto.LayerTemplateCreateDTO;
+import com.haha.admin.dto.LayerTemplateQueryDTO;
+import com.haha.common.dto.LayerTemplateUpdateDTO;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.Result;
+import com.haha.entity.LayerTemplate;
+import com.haha.service.LayerTemplateService;
+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("/layer-templates")
+@RequiredArgsConstructor
+public class LayerTemplateController {
+
+    private final LayerTemplateService layerTemplateService;
+
+    /**
+     * 分页查询层模版列表
+     * @param queryDTO 查询参数
+     * @return 层模版列表
+     */
+    // TODO: 临时移除权限验证,修复后恢复
+    // @RequirePermission("layer-template:read")
+    @GetMapping("/list")
+    public Result<Map<String, Object>> list(LayerTemplateQueryDTO queryDTO) {
+        queryDTO.validate();
+
+        IPage<LayerTemplate> templatePage = layerTemplateService.getPage(
+                queryDTO.getPage(),
+                queryDTO.getPageSize(),
+                queryDTO.getDeviceId(),
+                queryDTO.getSyncStatus(),
+                queryDTO.getTemplateName()
+        );
+
+        // 构造前端需要的分页数据格式
+        Map<String, Object> result = new HashMap<>();
+        result.put("list", templatePage.getRecords());
+        result.put("total", templatePage.getTotal());
+        result.put("pageSize", templatePage.getSize());
+        result.put("currentPage", templatePage.getCurrent());
+        result.put("totalPages", templatePage.getPages());
+
+        return Result.success("查询成功", result);
+    }
+
+    /**
+     * 获取层模版详情
+     * @param id 模版ID
+     * @return 层模版详情
+     */
+    @RequirePermission("layer-template:read")
+    @GetMapping("/{id}")
+    public Result<LayerTemplate> getById(@PathVariable Long id) {
+        LayerTemplate template = layerTemplateService.getById(id);
+        if (template == null) {
+            return Result.error(404, "层模版不存在");
+        }
+        return Result.success("查询成功", template);
+    }
+
+    /**
+     * 根据设备ID获取层模版
+     * @param deviceId 设备ID
+     * @return 层模版信息
+     */
+    @RequirePermission("layer-template:read")
+    @GetMapping("/device/{deviceId}")
+    public Result<LayerTemplate> getByDeviceId(@PathVariable String deviceId) {
+        LayerTemplate template = layerTemplateService.getByDeviceId(deviceId);
+        if (template == null) {
+            return Result.error(404, "该设备的层模版不存在");
+        }
+        return Result.success("查询成功", template);
+    }
+
+    /**
+     * 创建层模版
+     * @param dto 层模版信息
+     * @return 创建结果
+     */
+    @RequirePermission("layer-template:create")
+    @Log(module = "层模版管理", operation = OperationType.INSERT, summary = "创建层模版")
+    @PostMapping
+    public Result<LayerTemplate> create(@RequestBody LayerTemplateCreateDTO dto) {
+        LayerTemplate saved = layerTemplateService.create(dto);
+        return Result.success("创建成功", saved);
+    }
+
+    /**
+     * 更新层模版
+     * @param id 模版ID
+     * @param dto 层模版信息
+     * @return 更新结果
+     */
+    @RequirePermission("layer-template:update")
+    @Log(module = "层模版管理", operation = OperationType.UPDATE, summary = "更新层模版")
+    @PutMapping("/{id}")
+    public Result<LayerTemplate> update(@PathVariable Long id, @RequestBody LayerTemplateUpdateDTO dto) {
+        LayerTemplate saved = layerTemplateService.update(id, dto);
+        return Result.success("更新成功", saved);
+    }
+
+    /**
+     * 删除层模版(软删除)
+     * @param id 模版ID
+     * @return 删除结果
+     */
+    @RequirePermission("layer-template:delete")
+    @Log(module = "层模版管理", operation = OperationType.DELETE, summary = "删除层模版")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        boolean success = layerTemplateService.softDelete(id);
+        return success ? Result.success("删除成功", null) : Result.error(500, "删除失败");
+    }
+
+    /**
+     * 从哈哈平台同步单个设备层模版
+     * @param deviceId 设备ID
+     * @return 同步结果
+     */
+    // TODO: 临时移除权限验证,修复后恢复
+    // @RequirePermission("layer-template:update")
+    @Log(module = "层模版管理", operation = OperationType.SYNC, summary = "同步层模版")
+    @PostMapping("/{deviceId}/sync")
+    public Result<Void> sync(@PathVariable String deviceId) {
+        boolean success = layerTemplateService.syncFromHaha(deviceId);
+        return success ? Result.success("同步成功", null) : Result.error(500, "同步失败");
+    }
+
+    /**
+     * 批量同步层模版
+     * @param deviceIds 设备ID列表
+     * @return 批量同步结果
+     */
+    // TODO: 临时移除权限验证,修复后恢复
+    // @RequirePermission("layer-template:update")
+    @Log(module = "层模版管理", operation = OperationType.SYNC, summary = "批量同步层模版")
+    @PostMapping("/sync/batch")
+    public Result<Map<String, Object>> batchSync(@RequestBody List<String> deviceIds) {
+        Map<String, Object> syncResult = layerTemplateService.batchSync(deviceIds);
+
+        int successCount = (int) syncResult.getOrDefault("successCount", 0);
+        int failCount = (int) syncResult.getOrDefault("failCount", 0);
+
+        if (failCount > 0 && successCount == 0) {
+            return Result.error(500, "批量同步全部失败");
+        } else if (failCount > 0) {
+            return Result.success("批量同步完成(部分失败)", syncResult);
+        }
+        return Result.success("批量同步成功", syncResult);
+    }
+}

+ 27 - 0
haha-admin/src/main/java/com/haha/admin/dto/LayerTemplateQueryDTO.java

@@ -0,0 +1,27 @@
+package com.haha.admin.dto;
+
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 商品层模版查询DTO
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class LayerTemplateQueryDTO extends PageQueryDTO {
+
+    /**
+     * 设备ID
+     */
+    private String deviceId;
+
+    /**
+     * 同步状态:0-未同步,1-已同步,2-同步失败
+     */
+    private Integer syncStatus;
+
+    /**
+     * 模板名称(模糊查询)
+     */
+    private String templateName;
+}

+ 7 - 0
haha-common/pom.xml

@@ -57,6 +57,13 @@
             <artifactId>jackson-datatype-jsr310</artifactId>
             <scope>provided</scope>
         </dependency>
+
+        <!-- Jakarta Validation API -->
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+            <scope>provided</scope>
+        </dependency>
     </dependencies>
 
 </project>

+ 27 - 0
haha-common/src/main/java/com/haha/common/dto/FloorConfig.java

@@ -0,0 +1,27 @@
+package com.haha.common.dto;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 层数据配置DTO
+ * 用于描述每一层的商品摆放信息
+ */
+@Data
+public class FloorConfig {
+
+    /**
+     * 层号(从1开始)
+     */
+    @NotNull(message = "层号不能为空")
+    private Integer floor;
+
+    /**
+     * 该层的商品编码列表
+     */
+    @NotEmpty(message = "商品编码列表不能为空")
+    private List<String> goods;
+}

+ 36 - 0
haha-common/src/main/java/com/haha/common/dto/LayerTemplateCreateDTO.java

@@ -0,0 +1,36 @@
+package com.haha.common.dto;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 商品层模版创建DTO
+ */
+@Data
+public class LayerTemplateCreateDTO {
+
+    /**
+     * 设备ID(必填)
+     */
+    @NotNull(message = "设备ID不能为空")
+    private String deviceId;
+
+    /**
+     * 模板名称(可选)
+     */
+    private String templateName;
+
+    /**
+     * 左门层数据(必填)
+     */
+    @NotEmpty(message = "左门层数据不能为空")
+    private List<FloorConfig> leftFloors;
+
+    /**
+     * 右门层数据(可选)
+     */
+    private List<FloorConfig> rightFloors;
+}

+ 29 - 0
haha-common/src/main/java/com/haha/common/dto/LayerTemplateUpdateDTO.java

@@ -0,0 +1,29 @@
+package com.haha.common.dto;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 商品层模版更新DTO
+ */
+@Data
+public class LayerTemplateUpdateDTO {
+
+    /**
+     * 模板名称(可选)
+     */
+    private String templateName;
+
+    /**
+     * 左门层数据(可选,更新时传入则全量替换)
+     */
+    private List<FloorConfig> leftFloors;
+
+    /**
+     * 右门层数据(可选,更新时传入则全量替换)
+     */
+    private List<FloorConfig> rightFloors;
+}

+ 119 - 0
haha-entity/src/main/java/com/haha/entity/LayerTemplate.java

@@ -0,0 +1,119 @@
+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.time.LocalDateTime;
+
+/**
+ * 设备层模版实体类
+ * 对应哈哈平台设备层模版管理
+ */
+@Data
+@TableName("t_layer_template")
+public class LayerTemplate implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.ASSIGN_ID)
+    private Long id;
+
+    /**
+     * 模板ID(哈哈平台返回)
+     */
+    private String templateId;
+
+    /**
+     * 设备ID(SN号)
+     */
+    private String deviceId;
+
+    /**
+     * 模板名称
+     */
+    private String templateName;
+
+    /**
+     * 设备类型
+     */
+    private Integer deviceType;
+
+    /**
+     * 机柜层数
+     */
+    private Integer shelfNum;
+
+    /**
+     * 左门层数据(JSON格式)
+     */
+    private String leftFloors;
+
+    /**
+     * 右门层数据(JSON格式)
+     */
+    private String rightFloors;
+
+    /**
+     * 商品规则
+     */
+    private String goodsRule;
+
+    /**
+     * 同步状态:0-未同步,1-同步中,2-已同步,3-同步失败
+     */
+    private Integer syncStatus;
+
+    /**
+     * 最后同步时间
+     */
+    private LocalDateTime syncTime;
+
+    /**
+     * 是否删除:0-正常,1-已删除
+     */
+    private Integer isDeleted;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+
+    // ========== 以下为非数据库字段,用于前端展示 ==========
+
+    @TableField(exist = false)
+    private String syncStatusLabel;
+
+    @TableField(exist = false)
+    private String syncStatusColor;
+
+    @TableField(exist = false)
+    private String deviceTypeName;
+
+    @TableField(exist = false)
+    private String leftFloorsDisplay;
+
+    @TableField(exist = false)
+    private String rightFloorsDisplay;
+
+    @TableField(exist = false)
+    private Integer totalFloors;
+
+    @TableField(exist = false)
+    private String lastSyncTime;
+
+    @TableField(exist = false)
+    private String goodsRuleDisplay;
+
+    @TableField(exist = false)
+    private String statusText;
+}

+ 21 - 0
haha-mapper/src/main/java/com/haha/mapper/LayerTemplateMapper.java

@@ -0,0 +1,21 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.LayerTemplate;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 设备层模版数据访问层
+ */
+public interface LayerTemplateMapper extends BaseMapper<LayerTemplate> {
+
+    /**
+     * 根据设备ID查询层模版
+     *
+     * @param deviceId 设备ID(SN号)
+     * @return 层模版列表
+     */
+    List<LayerTemplate> selectByDeviceId(@Param("deviceId") String deviceId);
+}

+ 0 - 18
haha-miniapp/src/main/java/com/haha/miniapp/config/RestTemplateConfig.java

@@ -1,18 +0,0 @@
-package com.haha.miniapp.config;
-
-import org.springframework.context.annotation.Bean;
-import org.springframework.context.annotation.Configuration;
-import org.springframework.web.client.RestTemplate;
-
-/**
- * RestTemplate配置类
- * 用于HTTP请求,如调用微信API
- */
-@Configuration
-public class RestTemplateConfig {
-    
-    @Bean
-    public RestTemplate restTemplate() {
-        return new RestTemplate();
-    }
-}

+ 91 - 0
haha-service/src/main/java/com/haha/service/LayerTemplateService.java

@@ -0,0 +1,91 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.LayerTemplate;
+import com.haha.common.dto.LayerTemplateCreateDTO;
+import com.haha.common.dto.LayerTemplateUpdateDTO;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备层模版管理服务接口
+ * 提供设备层模版的CRUD操作和同步功能
+ *
+ * @author haha-service
+ * @version 1.0.0
+ */
+public interface LayerTemplateService extends IService<LayerTemplate> {
+
+    /**
+     * 分页查询层模版列表
+     *
+     * @param page        页码(从1开始)
+     * @param pageSize    每页条数
+     * @param deviceId    设备ID(可选,模糊查询)
+     * @param syncStatus  同步状态(可选):0-未同步,1-同步中,2-已同步,3-同步失败
+     * @param templateName 模板名称(可选,模糊查询)
+     * @return 分页结果
+     */
+    IPage<LayerTemplate> getPage(int page, int pageSize, String deviceId, Integer syncStatus, String templateName);
+
+    /**
+     * 获取层模版详情(带标签填充)
+     *
+     * @param id 层模版ID
+     * @return 层模版详情(包含展示字段)
+     */
+    LayerTemplate getDetailById(Long id);
+
+    /**
+     * 根据设备ID获取层模版
+     *
+     * @param deviceId 设备ID(SN号)
+     * @return 层模版信息,不存在则返回null
+     */
+    LayerTemplate getByDeviceId(String deviceId);
+
+    /**
+     * 创建层模版
+     *
+     * @param dto 创建请求DTO
+     * @return 创建后的层模版
+     */
+    LayerTemplate create(LayerTemplateCreateDTO dto);
+
+    /**
+     * 更新层模版
+     *
+     * @param id  层模版ID
+     * @param dto 更新请求DTO
+     * @return 更新后的层模版
+     */
+    LayerTemplate update(Long id, LayerTemplateUpdateDTO dto);
+
+    /**
+     * 软删除层模版
+     *
+     * @param id 层模版ID
+     * @return 是否成功
+     */
+    boolean softDelete(Long id);
+
+    /**
+     * 从哈哈平台同步单个设备层模版
+     * 将哈哈平台的层模版数据拉取到本地数据库
+     *
+     * @param deviceId 设备ID(SN号)
+     * @return 是否成功
+     */
+    boolean syncFromHaha(String deviceId);
+
+    /**
+     * 批量同步设备层模版
+     * 支持批量从哈哈平台同步多个设备的层模版数据
+     *
+     * @param deviceIds 设备ID列表
+     * @return 同步结果统计信息
+     */
+    Map<String, Object> batchSync(List<String> deviceIds);
+}

+ 697 - 0
haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java

@@ -0,0 +1,697 @@
+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.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.haha.common.dto.FloorConfig;
+import com.haha.common.dto.LayerTemplateCreateDTO;
+import com.haha.common.dto.LayerTemplateUpdateDTO;
+import com.haha.common.constant.CommonConstants;
+import com.haha.common.constant.SyncConstants;
+import com.haha.common.exception.BusinessException;
+import com.haha.common.utils.EntityLabelUtils;
+import com.haha.entity.LayerTemplate;
+import com.haha.mapper.LayerTemplateMapper;
+import com.haha.sdk.HahaClient;
+import com.haha.sdk.api.GoodsApi;
+import com.haha.service.LayerTemplateService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 设备层模版管理服务实现类
+ * 提供设备层模版的完整业务逻辑实现,包括CRUD和同步功能
+ *
+ * @author haha-service
+ * @version 1.0.0
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, LayerTemplate> implements LayerTemplateService {
+
+    private final HahaClient hahaClient;
+    private final ObjectMapper objectMapper = new ObjectMapper();
+
+    /**
+     * 分页查询层模版列表
+     * 支持按设备ID、同步状态、模板名称进行筛选
+     *
+     * @param page         页码
+     * @param pageSize     每页条数
+     * @param deviceId     设备ID(可选)
+     * @param syncStatus   同步状态(可选)
+     * @param templateName 模板名称(可选)
+     * @return 分页结果(已填充展示标签)
+     */
+    @Override
+    public IPage<LayerTemplate> getPage(int page, int pageSize, String deviceId, Integer syncStatus, String templateName) {
+        log.info("分页查询层模版: page={}, pageSize={}, deviceId={}, syncStatus={}, templateName={}",
+                page, pageSize, deviceId, syncStatus, templateName);
+
+        LambdaQueryWrapper<LayerTemplate> wrapper = new LambdaQueryWrapper<>();
+
+        // 构建查询条件
+        wrapper.like(deviceId != null && !deviceId.isEmpty(), LayerTemplate::getDeviceId, deviceId)
+               .eq(syncStatus != null, LayerTemplate::getSyncStatus, syncStatus)
+               .like(templateName != null && !templateName.isEmpty(), LayerTemplate::getTemplateName, templateName)
+               .eq(LayerTemplate::getIsDeleted, CommonConstants.NOT_DELETED)
+               .orderByDesc(LayerTemplate::getCreateTime);
+
+        // 执行分页查询
+        IPage<LayerTemplate> pageResult = this.page(new Page<>(page, pageSize), wrapper);
+
+        log.info("层模版查询结果: total={}, records={}", pageResult.getTotal(), pageResult.getRecords().size());
+
+        // 填充展示字段(同步状态标签、设备类型等)
+        pageResult.getRecords().forEach(this::fillLabels);
+
+        return pageResult;
+    }
+
+    /**
+     * 获取层模版详情
+     * 包含完整的展示字段填充
+     *
+     * @param id 层模版ID
+     * @return 层模版详情
+     */
+    @Override
+    public LayerTemplate getDetailById(Long id) {
+        log.info("获取层模版详情: id={}", id);
+
+        LayerTemplate layerTemplate = this.getById(id);
+        if (layerTemplate == null) {
+            log.warn("层模版不存在: id={}", id);
+            return null;
+        }
+
+        // 填充展示字段
+        fillLabels(layerTemplate);
+
+        return layerTemplate;
+    }
+
+    /**
+     * 根据设备ID获取层模版
+     * 一个设备通常对应一个层模版配置
+     *
+     * @param deviceId 设备ID(SN号)
+     * @return 层模版信息
+     */
+    @Override
+    public LayerTemplate getByDeviceId(String deviceId) {
+        log.info("根据设备ID获取层模版: deviceId={}", deviceId);
+
+        LambdaQueryWrapper<LayerTemplate> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(LayerTemplate::getDeviceId, deviceId)
+               .eq(LayerTemplate::getIsDeleted, CommonConstants.NOT_DELETED)
+               .orderByDesc(LayerTemplate::getCreateTime)
+               .last("LIMIT 1");
+
+        LayerTemplate layerTemplate = this.getOne(wrapper);
+        if (layerTemplate != null) {
+            fillLabels(layerTemplate);
+        }
+
+        return layerTemplate;
+    }
+
+    /**
+     * 创建层模版
+     * 将前端提交的层模版数据保存到本地数据库
+     *
+     * @param dto 创建请求DTO
+     * @return 创建后的层模版
+     */
+    @Override
+    public LayerTemplate create(LayerTemplateCreateDTO dto) {
+        log.info("创建层模版: deviceId={}, templateName={}", dto.getDeviceId(), dto.getTemplateName());
+
+        // 检查是否已存在该设备的层模版
+        LayerTemplate existing = this.getByDeviceId(dto.getDeviceId());
+        if (existing != null) {
+            throw new BusinessException(400, "该设备已存在层模版配置");
+        }
+
+        // 转换DTO为实体对象
+        LayerTemplate layerTemplate = convertFromCreateDto(dto);
+
+        // 设置基础字段
+        layerTemplate.setIsDeleted(CommonConstants.NOT_DELETED);
+        layerTemplate.setSyncStatus(SyncConstants.STATUS_NOT_SYNCED);
+        layerTemplate.setCreateTime(LocalDateTime.now());
+        layerTemplate.setUpdateTime(LocalDateTime.now());
+
+        // 保存到数据库
+        this.save(layerTemplate);
+
+        log.info("层模版创建成功: id={}, deviceId={}", layerTemplate.getId(), layerTemplate.getDeviceId());
+
+        fillLabels(layerTemplate);
+        return layerTemplate;
+    }
+
+    /**
+     * 更新层模版
+     * 支持更新模板名称和层数据配置
+     *
+     * @param id  层模版ID
+     * @param dto 更新请求DTO
+     * @return 更新后的层模版
+     */
+    @Override
+    public LayerTemplate update(Long id, LayerTemplateUpdateDTO dto) {
+        log.info("更新层模版: id={}", id);
+
+        LayerTemplate existing = this.getById(id);
+        if (existing == null) {
+            throw new BusinessException(404, "层模版不存在");
+        }
+
+        // 更新可编辑字段
+        if (dto.getTemplateName() != null) {
+            existing.setTemplateName(dto.getTemplateName());
+        }
+        if (dto.getLeftFloors() != null && !dto.getLeftFloors().isEmpty()) {
+            try {
+                existing.setLeftFloors(objectMapper.writeValueAsString(dto.getLeftFloors()));
+            } catch (JsonProcessingException e) {
+                log.error("左门层数据JSON序列化失败", e);
+                throw new BusinessException(500, "数据格式错误");
+            }
+        }
+        if (dto.getRightFloors() != null && !dto.getRightFloors().isEmpty()) {
+            try {
+                existing.setRightFloors(objectMapper.writeValueAsString(dto.getRightFloors()));
+            } catch (JsonProcessingException e) {
+                log.error("右门层数据JSON序列化失败", e);
+                throw new BusinessException(500, "数据格式错误");
+            }
+        }
+
+        // 更新时间戳和同步状态标记
+        existing.setUpdateTime(LocalDateTime.now());
+        existing.setSyncStatus(SyncConstants.STATUS_NOT_SYNCED); // 标记为需要重新同步
+
+        this.updateById(existing);
+
+        log.info("层模版更新成功: id={}", id);
+
+        fillLabels(existing);
+        return existing;
+    }
+
+    /**
+     * 软删除层模版
+     * 仅标记删除状态,不物理删除数据
+     *
+     * @param id 层模版ID
+     * @return 是否成功
+     */
+    @Override
+    public boolean softDelete(Long id) {
+        log.info("软删除层模版: id={}", id);
+
+        LayerTemplate layerTemplate = this.getById(id);
+        if (layerTemplate == null) {
+            throw new BusinessException(404, "层模版不存在");
+        }
+
+        LambdaUpdateWrapper<LayerTemplate> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(LayerTemplate::getId, id)
+                     .set(LayerTemplate::getIsDeleted, CommonConstants.DELETED)
+                     .set(LayerTemplate::getUpdateTime, LocalDateTime.now());
+
+        boolean result = this.update(updateWrapper);
+
+        log.info("层模版软删除结果: id={}, result={}", id, result);
+        return result;
+    }
+
+    /**
+     * 从哈哈平台同步单个设备层模版
+     * 这是核心同步逻辑:
+     * 1. 更新状态为"同步中"
+     * 2. 调用GoodsApi获取平台数据
+     * 3. 解析并转换数据
+     * 4. 更新或创建本地记录
+     * 5. 更新同步状态和时间
+     * 6. 异常处理确保状态正确更新
+     *
+     * @param deviceId 设备ID(SN号)
+     * @return 是否成功
+     */
+    @Override
+    public boolean syncFromHaha(String deviceId) {
+        log.info("开始同步层模版: deviceId={}", deviceId);
+
+        // 步骤1: 查询或创建本地记录,并更新状态为"同步中"
+        LayerTemplate localTemplate = this.getByDeviceId(deviceId);
+        boolean isNewRecord = false;
+
+        if (localTemplate == null) {
+            // 不存在则创建新记录
+            localTemplate = new LayerTemplate();
+            localTemplate.setDeviceId(deviceId);
+            localTemplate.setIsDeleted(CommonConstants.NOT_DELETED);
+            localTemplate.setCreateTime(LocalDateTime.now());
+            isNewRecord = true;
+        }
+
+        // 更新状态为"同步中"
+        updateSyncStatus(deviceId, SyncConstants.STATUS_SYNCING, null);
+
+        try {
+            // 步骤2: 调用哈哈平台API获取层模版数据
+            GoodsApi goodsApi = hahaClient.getGoodsApi();
+            Map<String, Object> apiData = goodsApi.getLayerTemplate(deviceId);
+
+            log.info("成功获取平台层模版数据: deviceId={}, data={}", deviceId, apiData);
+
+            // 步骤3-4: 解析返回数据并转换为实体对象
+            LayerTemplate updatedTemplate = convertFromApiData(deviceId, apiData);
+
+            // 步骤5: 更新或创建本地记录
+            if (isNewRecord) {
+                // 新记录,设置所有字段
+                localTemplate.setTemplateId(updatedTemplate.getTemplateId());
+                localTemplate.setTemplateName(updatedTemplate.getTemplateName());
+                localTemplate.setDeviceType(updatedTemplate.getDeviceType());
+                localTemplate.setShelfNum(updatedTemplate.getShelfNum());
+                localTemplate.setLeftFloors(updatedTemplate.getLeftFloors());
+                localTemplate.setRightFloors(updatedTemplate.getRightFloors());
+                localTemplate.setGoodsRule(updatedTemplate.getGoodsRule());
+                localTemplate.setSyncTime(LocalDateTime.now());
+                localTemplate.setUpdateTime(LocalDateTime.now());
+
+                this.save(localTemplate);
+                log.info("新建层模版记录: id={}, deviceId={}", localTemplate.getId(), deviceId);
+            } else {
+                // 已存在记录,更新数据
+                LambdaUpdateWrapper<LayerTemplate> updateWrapper = new LambdaUpdateWrapper<>();
+                updateWrapper.eq(LayerTemplate::getDeviceId, deviceId)
+                             .set(LayerTemplate::getTemplateId, updatedTemplate.getTemplateId())
+                             .set(LayerTemplate::getTemplateName, updatedTemplate.getTemplateName())
+                             .set(LayerTemplate::getDeviceType, updatedTemplate.getDeviceType())
+                             .set(LayerTemplate::getShelfNum, updatedTemplate.getShelfNum())
+                             .set(LayerTemplate::getLeftFloors, updatedTemplate.getLeftFloors())
+                             .set(LayerTemplate::getRightFloors, updatedTemplate.getRightFloors())
+                             .set(LayerTemplate::getGoodsRule, updatedTemplate.getGoodsRule())
+                             .set(LayerTemplate::getUpdateTime, LocalDateTime.now());
+
+                this.update(updateWrapper);
+                log.info("更新层模版记录: deviceId={}", deviceId);
+            }
+
+            // 步骤6: 更新同步状态为"已同步"并记录时间
+            updateSyncStatus(deviceId, SyncConstants.STATUS_SYNCED, LocalDateTime.now());
+
+            log.info("层模版同步成功: deviceId={}", deviceId);
+            return true;
+
+        } catch (Exception e) {
+            log.error("层模版同步失败: deviceId={}, error={}", deviceId, e.getMessage(), e);
+
+            // 步骤7: 异常处理 - 更新状态为"同步失败"
+            updateSyncStatus(deviceId, SyncConstants.STATUS_SYNC_FAILED, null);
+
+            return false;
+        }
+    }
+
+    /**
+     * 批量同步设备层模版
+     * 遍历设备列表逐个执行同步操作,并统计结果
+     *
+     * @param deviceIds 设备ID列表
+     * @return 同步结果统计(包含成功数、失败数、总耗时等)
+     */
+    @Override
+    public Map<String, Object> batchSync(List<String> deviceIds) {
+        log.info("开始批量同步层模版: totalDevices={}", deviceIds.size());
+
+        long startTime = System.currentTimeMillis();
+        int successCount = 0;
+        int failCount = 0;
+        List<String> failedDeviceIds = new java.util.ArrayList<>();
+
+        for (String deviceId : deviceIds) {
+            boolean success = this.syncFromHaha(deviceId);
+            if (success) {
+                successCount++;
+            } else {
+                failCount++;
+                failedDeviceIds.add(deviceId);
+            }
+        }
+
+        long endTime = System.currentTimeMillis();
+        long costTime = endTime - startTime;
+
+        // 构建统计结果
+        Map<String, Object> result = new HashMap<>();
+        result.put("totalDevices", deviceIds.size());
+        result.put("successCount", successCount);
+        result.put("failCount", failCount);
+        result.put("failedDeviceIds", failedDeviceIds);
+        result.put("costTime", costTime);
+        result.put("costTimeDisplay", formatCostTime(costTime));
+
+        log.info("批量同步完成: total={}, success={}, fail={}, cost={}ms",
+                deviceIds.size(), successCount, failCount, costTime);
+
+        return result;
+    }
+
+    // ==================== 私有工具方法 ====================
+
+    /**
+     * 将API返回的Map数据转换为实体对象
+     * 解析哈哈平台返回的层模版数据结构
+     *
+     * @param deviceId 设备ID
+     * @param apiData  API返回的数据Map
+     * @return 转换后的实体对象
+     */
+    private LayerTemplate convertFromApiData(String deviceId, Map<String, Object> apiData) {
+        LayerTemplate template = new LayerTemplate();
+        template.setDeviceId(deviceId);
+
+        // 提取基本字段
+        if (apiData.containsKey("template_id")) {
+            template.setTemplateId(String.valueOf(apiData.get("template_id")));
+        }
+        if (apiData.containsKey("name")) {
+            template.setTemplateName(String.valueOf(apiData.get("name")));
+        }
+        if (apiData.containsKey("type")) {
+            Object typeObj = apiData.get("type");
+            if (typeObj instanceof Number) {
+                template.setDeviceType(((Number) typeObj).intValue());
+            } else if (typeObj != null) {
+                try {
+                    template.setDeviceType(Integer.parseInt(typeObj.toString()));
+                } catch (NumberFormatException e) {
+                    log.warn("解析deviceType失败: {}", typeObj);
+                }
+            }
+        }
+        if (apiData.containsKey("shelf_num")) {
+            Object shelfNumObj = apiData.get("shelf_num");
+            if (shelfNumObj instanceof Number) {
+                template.setShelfNum(((Number) shelfNumObj).intValue());
+            } else if (shelfNumObj != null) {
+                try {
+                    template.setShelfNum(Integer.parseInt(shelfNumObj.toString()));
+                } catch (NumberFormatException e) {
+                    log.warn("解析shelfNum失败: {}", shelfNumObj);
+                }
+            }
+        }
+
+        // 提取products中的left/right数据(关键!)
+        if (apiData.containsKey("products")) {
+            Object productsObj = apiData.get("products");
+            if (productsObj instanceof Map) {
+                @SuppressWarnings("unchecked")
+                Map<String, Object> products = (Map<String, Object>) productsObj;
+
+                // 左门层数据 -> JSON字符串存储
+                if (products.containsKey("left")) {
+                    Object leftObj = products.get("left");
+                    try {
+                        template.setLeftFloors(objectMapper.writeValueAsString(leftObj));
+                        log.debug("左门层数据转换成功: deviceId={}", deviceId);
+                    } catch (JsonProcessingException e) {
+                        log.error("左门层数据JSON序列化失败: deviceId={}", deviceId, e);
+                    }
+                }
+
+                // 右门层数据 -> JSON字符串存储
+                if (products.containsKey("right")) {
+                    Object rightObj = products.get("right");
+                    try {
+                        template.setRightFloors(objectMapper.writeValueAsString(rightObj));
+                        log.debug("右门层数据转换成功: deviceId={}", deviceId);
+                    } catch (JsonProcessingException e) {
+                        log.error("右门层数据JSON序列化失败: deviceId={}", deviceId, e);
+                    }
+                }
+            }
+        }
+
+        // 商品规则(如果存在)
+        if (apiData.containsKey("goods_rule")) {
+            Object goodsRuleObj = apiData.get("goods_rule");
+            if (goodsRuleObj != null) {
+                try {
+                    template.setGoodsRule(objectMapper.writeValueAsString(goodsRuleObj));
+                } catch (JsonProcessingException e) {
+                    log.warn("商品规则JSON序列化失败: deviceId={}", deviceId, e);
+                    template.setGoodsRule(goodsRuleObj.toString());
+                }
+            }
+        }
+
+        return template;
+    }
+
+    /**
+     * 将前端DTO转换为提交给平台的JSON格式
+     * 用于将本地修改后的层模版数据推送到哈哈平台
+     *
+     * @param dto 前端提交的创建DTO
+     * @return 平台所需的JSON格式字符串
+     */
+    private String convertToPlatformJson(LayerTemplateCreateDTO dto) {
+        try {
+            Map<String, Object> platformData = new HashMap<>();
+            
+            // 构建products结构
+            Map<String, Object> products = new HashMap<>();
+            products.put("left", dto.getLeftFloors());
+            products.put("right", dto.getRightFloors() != null ? dto.getRightFloors() : List.of());
+            
+            platformData.put("products", products);
+            platformData.put("name", dto.getTemplateName());
+            
+            return objectMapper.writeValueAsString(platformData);
+        } catch (JsonProcessingException e) {
+            log.error("转换为平台JSON格式失败", e);
+            throw new BusinessException(500, "数据格式转换失败");
+        }
+    }
+
+    /**
+     * 将创建DTO转换为实体对象
+     *
+     * @param dto 创建请求DTO
+     * @return 实体对象
+     */
+    private LayerTemplate convertFromCreateDto(LayerTemplateCreateDTO dto) {
+        LayerTemplate template = new LayerTemplate();
+        template.setDeviceId(dto.getDeviceId());
+        template.setTemplateName(dto.getTemplateName());
+
+        // 序列化左门层数据
+        if (dto.getLeftFloors() != null && !dto.getLeftFloors().isEmpty()) {
+            try {
+                template.setLeftFloors(objectMapper.writeValueAsString(dto.getLeftFloors()));
+            } catch (JsonProcessingException e) {
+                log.error("左门层数据序列化失败", e);
+                throw new BusinessException(500, "左门层数据格式错误");
+            }
+        }
+
+        // 序列化右门层数据
+        if (dto.getRightFloors() != null && !dto.getRightFloors().isEmpty()) {
+            try {
+                template.setRightFloors(objectMapper.writeValueAsString(dto.getRightFloors()));
+            } catch (JsonProcessingException e) {
+                log.error("右门层数据序列化失败", e);
+                throw new BusinessException(500, "右门层数据格式错误");
+            }
+        }
+
+        return template;
+    }
+
+    /**
+     * 填充展示字段
+     * 参考ProductServiceImpl.fillProductLabels模式
+     * 为前端展示提供友好的标签和格式化数据
+     *
+     * @param template 层模版实体
+     */
+    private void fillLabels(LayerTemplate template) {
+        // 1. 同步状态标签和颜色
+        if (template.getSyncStatus() != null) {
+            var statusLabel = EntityLabelUtils.getStatusLabel("sync", template.getSyncStatus());
+            template.setSyncStatusLabel(statusLabel.getLabel());
+            template.setSyncStatusColor(statusLabel.getColor());
+            template.setStatusText(statusLabel.getLabel()); // 兼容性字段
+        }
+
+        // 2. 设备类型名称
+        if (template.getDeviceType() != null) {
+            template.setDeviceTypeName(getDeviceTypeName(template.getDeviceType()));
+        }
+
+        // 3. 左门层数据展示(美化JSON显示)
+        if (template.getLeftFloors() != null && !template.getLeftFloors().isEmpty()) {
+            template.setLeftFloorsDisplay(formatJsonForDisplay(template.getLeftFloors()));
+        }
+
+        // 4. 右门层数据展示
+        if (template.getRightFloors() != null && !template.getRightFloors().isEmpty()) {
+            template.setRightFloorsDisplay(formatJsonForDisplay(template.getRightFloors()));
+        }
+
+        // 5. 计算总层数(左门+右门)
+        Integer totalFloors = calculateTotalFloors(template);
+        template.setTotalFloors(totalFloors);
+
+        // 6. 最后同步时间格式化
+        if (template.getSyncTime() != null) {
+            template.setLastSyncTime(formatDateTime(template.getSyncTime()));
+        }
+
+        // 7. 商品规则展示
+        if (template.getGoodsRule() != null && !template.getGoodsRule().isEmpty()) {
+            template.setGoodsRuleDisplay(formatJsonForDisplay(template.getGoodsRule()));
+        }
+    }
+
+    /**
+     * 更新同步状态
+     * 统一的状态更新方法,确保原子性操作
+     *
+     * @param deviceId   设备ID
+     * @param status     新的同步状态
+     * @param syncTime   同步时间(可为null)
+     */
+    private void updateSyncStatus(String deviceId, Integer status, LocalDateTime syncTime) {
+        LambdaUpdateWrapper<LayerTemplate> updateWrapper = new LambdaUpdateWrapper<>();
+        updateWrapper.eq(LayerTemplate::getDeviceId, deviceId)
+                     .set(LayerTemplate::getSyncStatus, status)
+                     .set(LayerTemplate::getUpdateTime, LocalDateTime.now());
+
+        if (syncTime != null) {
+            updateWrapper.set(LayerTemplate::getSyncTime, syncTime);
+        }
+
+        this.update(updateWrapper);
+        
+        log.debug("同步状态更新: deviceId={}, status={}", deviceId,
+                SyncConstants.getStatusDesc(status));
+    }
+
+    /**
+     * 获取设备类型名称
+     *
+     * @param deviceType 设备类型代码
+     * @return 设备类型名称
+     */
+    private String getDeviceTypeName(Integer deviceType) {
+        if (deviceType == null) {
+            return "未知";
+        }
+        return switch (deviceType) {
+            case 1 -> "静态柜";
+            case 2 -> "动态柜";
+            case 3 -> "全部柜";
+            default -> "未知类型(" + deviceType + ")";
+        };
+    }
+
+    /**
+     * 计算总层数
+     * 从左右门的JSON数据中解析出实际层数
+     *
+     * @param template 层模版
+     * @return 总层数
+     */
+    private Integer calculateTotalFloors(LayerTemplate template) {
+        int leftFloors = parseFloorCount(template.getLeftFloors());
+        int rightFloors = parseFloorCount(template.getRightFloors());
+        return leftFloors + rightFloors;
+    }
+
+    /**
+     * 从JSON字符串中解析层数量
+     *
+     * @param floorsJson JSON格式的层数据
+     * @return 层数量
+     */
+    private int parseFloorCount(String floorsJson) {
+        if (floorsJson == null || floorsJson.isEmpty()) {
+            return 0;
+        }
+        try {
+            List<FloorConfig> floors = objectMapper.readValue(floorsJson, 
+                    new TypeReference<List<FloorConfig>>() {});
+            return floors.size();
+        } catch (JsonProcessingException e) {
+            log.debug("解析层数失败,返回0", e);
+            return 0;
+        }
+    }
+
+    /**
+     * 格式化JSON用于展示(美化输出)
+     *
+     * @param jsonStr JSON字符串
+     * @return 美化后的JSON字符串
+     */
+    private String formatJsonForDisplay(String jsonStr) {
+        try {
+            Object json = objectMapper.readValue(jsonStr, Object.class);
+            return objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(json);
+        } catch (JsonProcessingException e) {
+            log.debug("JSON格式化失败,返回原始值", e);
+            return jsonStr;
+        }
+    }
+
+    /**
+     * 格式化日期时间
+     *
+     * @param dateTime 日期时间
+     * @return 格式化后的字符串
+     */
+    private String formatDateTime(LocalDateTime dateTime) {
+        if (dateTime == null) {
+            return "";
+        }
+        return dateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
+    }
+
+    /**
+     * 格式化耗时显示
+     *
+     * @param costTimeMs 耗时(毫秒)
+     * @return 可读的时间字符串
+     */
+    private String formatCostTime(long costTimeMs) {
+        if (costTimeMs < 1000) {
+            return costTimeMs + "ms";
+        } else if (costTimeMs < 60000) {
+            return String.format("%.2fs", costTimeMs / 1000.0);
+        } else {
+            return String.format("%.2fmin", costTimeMs / 60000.0);
+        }
+    }
+}

+ 121 - 8
haha-service/src/main/java/com/haha/service/impl/MarketingRuleServiceImpl.java

@@ -25,6 +25,19 @@ import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
 
+/**
+ * 营销规则服务实现类
+ * 
+ * 主要职责:
+ * 1. 计算订单可享受的营销活动优惠(首单立减、折扣、满减等)
+ * 2. 判断营销活动是否适用于当前订单(设备、门店、商品范围)
+ * 3. 计算订单最终优惠金额和实付金额
+ * 
+ * 注意:
+ * - 此服务只处理营销活动优惠,不处理优惠券
+ * - 优惠券优惠由 UserCouponService 处理
+ * - 订单最终实付 = 原价 - 营销活动优惠 - 优惠券优惠
+ */
 @Slf4j
 @Service
 @RequiredArgsConstructor
@@ -36,66 +49,121 @@ public class MarketingRuleServiceImpl implements MarketingRuleService {
     private final ActivityProductMapper activityProductMapper;
     private final OrderMapper orderMapper;
 
+    /**
+     * 计算订单在指定营销活动下的优惠金额
+     * 
+     * 业务流程:
+     * 1. 验证订单和活动是否存在
+     * 2. 检查活动是否进行中
+     * 3. 检查活动是否适用于该设备
+     * 4. 根据活动类型计算优惠金额:
+     *    - type=1: 首单立减
+     *    - type=2: 折扣价
+     *    - type=3: 满减
+     * 
+     * @param orderId 订单ID
+     * @param activityId 营销活动ID
+     * @return 优惠金额,如果不适用则返回0
+     */
     @Override
     public BigDecimal calculateDiscount(Long orderId, Long activityId) {
+        // 1. 查询订单信息
         Order order = orderMapper.selectById(orderId);
         if (order == null) {
             return BigDecimal.ZERO;
         }
 
+        // 2. 查询营销活动信息
         MarketingActivity activity = activityMapper.selectById(activityId);
         if (activity == null || activity.getStatus() != ActivityStatusEnum.ONGOING.getCode()) {
+            // 活动不存在或未进行中,不享受优惠
             return BigDecimal.ZERO;
         }
 
+        // 3. 检查活动是否适用于该设备
         if (!isApplicableToDevice(activity, order.getDeviceId())) {
             return BigDecimal.ZERO;
         }
 
+        // 4. 根据活动类型计算优惠金额
         switch (activity.getActivityType()) {
-            case 1:
+            case 1:  // 首单立减
                 return calculateFirstOrderDiscount(order, activity);
-            case 2:
+            case 2:  // 折扣价
                 return calculateDiscountPrice(order, activity);
-            case 3:
+            case 3:  // 满减
                 return calculateFullReduction(order, activity);
             default:
                 return BigDecimal.ZERO;
         }
     }
 
+    /**
+     * 计算首单立减优惠
+     * 
+     * 计算规则:
+     * 1. 判断是否为用户首单(之前无完成订单)
+     * 2. 如果是首单,优惠金额 = min(活动减免金额, 订单总金额)
+     * 3. 优惠金额不超过订单总金额
+     * 
+     * @param order 订单信息
+     * @param activity 营销活动信息
+     * @return 优惠金额
+     */
     @Override
     public BigDecimal calculateFirstOrderDiscount(Order order, MarketingActivity activity) {
+        // 1. 判断是否为首单:查询用户是否有已完成的订单
         if (!isFirstOrder(order.getUserId())) {
             log.info("用户非首单,不适用首单立减活动: userId={}", order.getUserId());
             return BigDecimal.ZERO;
         }
 
+        // 2. 获取活动设置的立减金额
         BigDecimal discount = activity.getDiscountValue();
         if (discount == null || discount.compareTo(BigDecimal.ZERO) <= 0) {
             return BigDecimal.ZERO;
         }
 
+        // 3. 优惠金额不能超过订单总金额
         BigDecimal orderAmount = order.getTotalAmount();
         if (orderAmount.compareTo(discount) < 0) {
+            // 如果订单金额小于立减金额,则优惠金额 = 订单金额(相当于免单)
             return orderAmount;
         }
 
+        // 4. 返回活动设置的立减金额
         return discount;
     }
 
+    /**
+     * 计算折扣价优惠
+     * 
+     * 计算规则:
+     * 1. 折扣率范围:0 < discountRate <= 1(如0.8表示8折)
+     * 2. 优惠金额 = 订单总金额 × (1 - 折扣率)
+     * 3. 如果有最大优惠限制,优惠金额不超过maxDiscount
+     * 
+     * @param order 订单信息
+     * @param activity 营销活动信息
+     * @return 优惠金额
+     */
     @Override
     public BigDecimal calculateDiscountPrice(Order order, MarketingActivity activity) {
+        // 1. 获取折扣率(如0.8表示8折)
         BigDecimal discountRate = activity.getDiscountValue();
         if (discountRate == null || discountRate.compareTo(BigDecimal.ZERO) <= 0 
                 || discountRate.compareTo(BigDecimal.ONE) > 0) {
+            // 折扣率不合法,不享受优惠
             return BigDecimal.ZERO;
         }
 
+        // 2. 计算优惠金额:原价 × (1 - 折扣率)
+        // 例:原价100元,8折(0.8),优惠 = 100 × (1 - 0.8) = 20元
         BigDecimal orderAmount = order.getTotalAmount();
         BigDecimal discountAmount = orderAmount.multiply(BigDecimal.ONE.subtract(discountRate))
                 .setScale(2, RoundingMode.HALF_UP);
 
+        // 3. 如果活动设置了最大优惠金额,则优惠不能超过此限制
         if (activity.getMaxDiscount() != null && activity.getMaxDiscount().compareTo(BigDecimal.ZERO) > 0) {
             if (discountAmount.compareTo(activity.getMaxDiscount()) > 0) {
                 discountAmount = activity.getMaxDiscount();
@@ -105,21 +173,35 @@ public class MarketingRuleServiceImpl implements MarketingRuleService {
         return discountAmount;
     }
 
+    /**
+     * 计算满减优惠
+     * 
+     * 计算规则:
+     * 1. 订单总金额 >= 满减门槛(minAmount)
+     * 2. 如果满足门槛,优惠金额 = 活动设置的减免金额(discountValue)
+     * 3. 不满足门槛则无优惠
+     * 
+     * @param order 订单信息
+     * @param activity 营销活动信息
+     * @return 优惠金额
+     */
     @Override
     public BigDecimal calculateFullReduction(Order order, MarketingActivity activity) {
-        BigDecimal minAmount = activity.getMinAmount();
-        BigDecimal discountValue = activity.getDiscountValue();
+        BigDecimal minAmount = activity.getMinAmount();        // 满减门槛
+        BigDecimal discountValue = activity.getDiscountValue(); // 减免金额
 
         if (minAmount == null || discountValue == null || minAmount.compareTo(BigDecimal.ZERO) <= 0) {
             return BigDecimal.ZERO;
         }
 
+        // 判断订单金额是否达到满减门槛
         BigDecimal orderAmount = order.getTotalAmount();
         if (orderAmount.compareTo(minAmount) < 0) {
             log.info("订单金额未达满减门槛: orderAmount={}, minAmount={}", orderAmount, minAmount);
             return BigDecimal.ZERO;
         }
 
+        // 达到门槛,返回活动设置的减免金额
         return discountValue;
     }
 
@@ -179,22 +261,50 @@ public class MarketingRuleServiceImpl implements MarketingRuleService {
         return orderCount == null || orderCount == 0;
     }
 
+    /**
+     * 计算订单所有可用营销活动的总优惠
+     * 
+     * 这是订单结算时的核心方法,业务流程:
+     * 1. 查询所有进行中的营销活动
+     * 2. 筛选出适用于该订单设备的活动
+     * 3. 遍历每个活动,计算优惠金额并累加
+     * 4. 计算最终实付金额 = 原价 - 总优惠
+     * 5. 确保实付金额不为负数
+     * 
+     * 注意:
+     * - 此方法只计算营销活动优惠,不包括优惠券
+     * - 多个活动优惠可以叠加
+     * - 优惠券的优惠需要在订单结算时额外计算
+     * 
+     * @param order 订单信息
+     * @return 包含以下信息的Map:
+     *         - originalAmount: 订单原价
+     *         - totalDiscount: 总优惠金额
+     *         - finalAmount: 最终实付金额
+     *         - activities:  applied activity list
+     */
     @Override
     public Map<String, Object> calculateOrderDiscount(Order order) {
+        // 初始化返回结果
         Map<String, Object> result = new HashMap<>();
-        result.put("originalAmount", order.getTotalAmount());
-        result.put("totalDiscount", BigDecimal.ZERO);
-        result.put("activities", new ArrayList<>());
+        result.put("originalAmount", order.getTotalAmount());  // 订单原价
+        result.put("totalDiscount", BigDecimal.ZERO);          // 总优惠金额
+        result.put("activities", new ArrayList<>());           // 应用的活动列表
 
+        // 1. 查询所有适用于该订单的营销活动
         List<MarketingActivity> applicableActivities = getApplicableActivities(order);
         BigDecimal totalDiscount = BigDecimal.ZERO;
         List<Map<String, Object>> appliedActivities = new ArrayList<>();
 
+        // 2. 遍历每个活动,计算优惠金额
         for (MarketingActivity activity : applicableActivities) {
+            // 计算该活动的优惠金额
             BigDecimal discount = calculateDiscount(order.getId(), activity.getId());
             if (discount.compareTo(BigDecimal.ZERO) > 0) {
+                // 累加优惠金额
                 totalDiscount = totalDiscount.add(discount);
                 
+                // 记录应用的活动信息
                 Map<String, Object> activityInfo = new HashMap<>();
                 activityInfo.put("id", activity.getId());
                 activityInfo.put("name", activity.getActivityName());
@@ -205,11 +315,14 @@ public class MarketingRuleServiceImpl implements MarketingRuleService {
             }
         }
 
+        // 3. 计算最终实付金额 = 原价 - 总优惠
         BigDecimal finalAmount = order.getTotalAmount().subtract(totalDiscount);
+        // 确保实付金额不为负数(优惠不能超过原价)
         if (finalAmount.compareTo(BigDecimal.ZERO) < 0) {
             finalAmount = BigDecimal.ZERO;
         }
 
+        // 4. 组装返回结果
         result.put("totalDiscount", totalDiscount);
         result.put("finalAmount", finalAmount);
         result.put("activities", appliedActivities);

+ 59 - 16
haha-service/src/main/java/com/haha/service/impl/OrderServiceImpl.java

@@ -300,11 +300,33 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         return lambdaUpdate().eq(Order::getOrderNo, orderNo).set(Order::getPayStatus, payStatus).update();
     }
 
+    /**
+     * 从AI识别结果创建订单
+     * 
+     * 业务流程:
+     * 1. 幂等性检查:防止重复创建订单
+     * 2. 解析商品列表,计算订单总金额(price * quantity累加)
+     * 3. 解析视频URL等资源信息
+     * 4. 创建订单记录,状态为待支付
+     * 
+     * 注意:
+     * - 此方法只计算商品原价总额,不涉及优惠计算
+     * - 优惠计算在订单结算时由MarketingRuleService处理
+     * - 如果使用优惠券,由UserCouponService处理
+     * 
+     * @param activityId 活动ID(设备开门会话ID)
+     * @param deviceId 设备ID
+     * @param userId 用户ID
+     * @param skuListStr 商品列表JSON字符串,格式:[{"price":10.5,"quantity":2}]
+     * @param resourceInfoStr 资源信息JSON字符串,包含视频URL等
+     * @param confidence AI识别置信度
+     * @return 创建的订单对象,失败返回null
+     */
     @Override
     @Transactional
     public Order createOrderFromRecognition(String activityId, String deviceId, String userId,
                                              String skuListStr, String resourceInfoStr, BigDecimal confidence) {
-        // 幂等性检查:如果activityId已存在订单,直接返回
+        // 【步骤1】幂等性检查:如果activityId已存在订单,直接返回,避免重复创建
         if (activityId != null && !activityId.isEmpty()) {
             Order existingOrder = getOrderByActivityId(activityId);
             if (existingOrder != null) {
@@ -313,7 +335,7 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             }
         }
         
-        // 1. 计算订单金额(从 skuListStr 解析 JSONArray,遍历计算 price * quantity)
+        // 【步骤2】计算订单总金额:遍历商品列表,累加 price * quantity
         BigDecimal totalAmount = BigDecimal.ZERO;
         JSONArray skuList = null;
         if (skuListStr != null && !skuListStr.isEmpty()) {
@@ -321,39 +343,43 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
             if (skuList != null) {
                 for (int i = 0; i < skuList.size(); i++) {
                     JSONObject sku = skuList.getJSONObject(i);
-                    BigDecimal price = sku.getBigDecimal("price");
-                    Integer quantity = sku.getInteger("quantity");
+                    BigDecimal price = sku.getBigDecimal("price");      // 商品单价
+                    Integer quantity = sku.getInteger("quantity");      // 商品数量
                     if (price != null && quantity != null) {
+                        // 累加每个商品的金额:单价 × 数量
                         totalAmount = totalAmount.add(price.multiply(BigDecimal.valueOf(quantity)));
                     }
                 }
+                // 保留两位小数,四舍五入
                 totalAmount = totalAmount.setScale(2, RoundingMode.HALF_UP);
             }
         }
 
-        // 2. 解析视频URL
+        // 【步骤3】解析视频URL等资源信息
         String videoUrl = null;
         if (resourceInfoStr != null && !resourceInfoStr.isEmpty()) {
             JSONObject resourceInfo = JSON.parseObject(resourceInfoStr);
             videoUrl = resourceInfo.getString("video_url");
         }
 
-        // 3. 创建订单(仅在有用户、设备和金额时)
+        // 【步骤4】创建订单记录
+        // 注意:此时只保存商品原价总额(totalAmount),不计算优惠
+        // 优惠金额(discountAmount)在订单结算时由设备端或MarketingRuleService计算
         if (userId != null && deviceId != null && totalAmount.compareTo(BigDecimal.ZERO) > 0) {
             Order order = new Order();
-            order.setActivityId(activityId);
-            order.setUserId(Long.parseLong(userId));
-            order.setDeviceId(deviceId);
-            order.setTotalAmount(totalAmount);
-            order.setItems(skuListStr);
-            order.setStatus(OrderConstants.STATUS_PENDING_PAYMENT);
-            order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID);
-            order.setCreateTime(LocalDateTime.now());
+            order.setActivityId(activityId);                          // 开门会话ID
+            order.setUserId(Long.parseLong(userId));                  // 用户ID
+            order.setDeviceId(deviceId);                              // 设备ID
+            order.setTotalAmount(totalAmount);                        // 订单总金额(商品原价)
+            order.setItems(skuListStr);                               // 商品列表JSON
+            order.setStatus(OrderConstants.STATUS_PENDING_PAYMENT);   // 订单状态:待支付
+            order.setPayStatus(OrderConstants.PAY_STATUS_UNPAID);     // 支付状态:未支付
+            order.setCreateTime(LocalDateTime.now());                 // 创建时间
             if (videoUrl != null) {
-                order.setVideoUrl(videoUrl);
+                order.setVideoUrl(videoUrl);                          // 购物视频URL
             }
             if (confidence != null) {
-                order.setConfidence(confidence);
+                order.setConfidence(confidence);                      // AI识别置信度
             }
 
             boolean saved = this.save(order);
@@ -458,6 +484,23 @@ public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements
         return order;
     }
 
+    /**
+     * 使用支付分完结订单(扣费)
+     * 
+     * 业务流程:
+     * 1. 验证订单是否存在
+     * 2. 检查是否创建了支付分服务订单
+     * 3. 调用微信支付分完结接口进行扣费
+     * 4. 扣费成功后触发邀请激活逻辑(异步)
+     * 
+     * 注意:
+     * - 如果订单未使用支付分,直接更新订单状态
+     * - 优惠金额已在设备端计算,totalAmount为实付金额
+     * 
+     * @param orderId 订单ID
+     * @param totalAmount 订单总金额(设备端已计算优惠后的实付金额)
+     * @return 是否完结成功
+     */
     @Override
     @Transactional
     public boolean completeOrderWithPayScore(Long orderId, BigDecimal totalAmount) {

+ 32 - 3
haha-service/src/main/java/com/haha/service/impl/UserCouponServiceImpl.java

@@ -228,27 +228,56 @@ public class UserCouponServiceImpl extends ServiceImpl<UserCouponMapper, UserCou
         return userCoupon;
     }
 
+    /**
+     * 使用优惠券
+     * 
+     * 业务流程:
+     * 1. 验证优惠券是否存在且属于当前用户
+     * 2. 检查优惠券状态(必须未使用)
+     * 3. 检查优惠券是否过期
+     * 4. 更新优惠券状态为已使用
+     * 5. 记录使用的订单ID和实际优惠金额
+     * 
+     * 注意:
+     * - discountAmount 参数是调用方已经计算好的实际优惠金额
+     * - 此方法只负责标记优惠券为已使用,不负责计算优惠
+     * - 优惠金额的计算逻辑应该在订单结算时由调用方完成
+     * 
+     * @param id 用户优惠券ID
+     * @param userId 用户ID
+     * @param orderId 订单ID
+     * @param discountAmount 实际优惠金额(由调用方计算)
+     * @return 是否使用成功
+     */
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean useCoupon(Long id, Long userId, Long orderId, BigDecimal discountAmount) {
+        // 1. 查询优惠券信息
         UserCoupon userCoupon = this.getById(id);
         if (userCoupon == null) {
             throw new BusinessException(404, "优惠券不存在");
         }
+        
+        // 2. 验证优惠券归属
         if (!userCoupon.getUserId().equals(userId)) {
             throw new BusinessException(403, "无权使用该优惠券");
         }
+        
+        // 3. 检查优惠券状态(必须未使用)
         if (!CouponStatusEnum.canUse(userCoupon.getStatus())) {
             throw new BusinessException(400, "优惠券不可用");
         }
+        
+        // 4. 检查优惠券是否过期
         if (LocalDateTime.now().isAfter(userCoupon.getValidEndTime())) {
             throw new BusinessException(400, "优惠券已过期");
         }
 
+        // 5. 更新优惠券状态为已使用
         userCoupon.setStatus(CouponStatusEnum.USED.getCode());
-        userCoupon.setOrderId(orderId);
-        userCoupon.setUseTime(LocalDateTime.now());
-        userCoupon.setDiscountAmount(discountAmount);
+        userCoupon.setOrderId(orderId);                        // 关联订单ID
+        userCoupon.setUseTime(LocalDateTime.now());            // 记录使用时间
+        userCoupon.setDiscountAmount(discountAmount);          // 记录实际优惠金额
 
         boolean result = this.updateById(userCoupon);