Ver Fonte

refactor: 设备层模版管理 → 设备模版管理,支持层模版(LAYER)和柜模版(CABINET)

- 新增 template_type 字段区分模版类型
- syncFromHaha 自动检测: 先调 4.5.1 层模版API,失败/数据无效则降级 4.1 柜模版API
- 柜模版数据转换: 4.1 扁平商品列表映射为单层结构
- pushTemplateToHaha 根据模版类型路由到不同API (LAYER→4.5.2, CABINET→4.5)
- 前端: 列表页新增模版类型列和筛选,编辑页支持 LAYER/CABINET 切换
- 增量 SQL: layer_template_upgrade_v2.sql

Co-Authored-By: Claude <noreply@anthropic.com>
skyline há 2 dias atrás
pai
commit
fa5d7c0911

+ 2 - 2
docs/database/seed_menu.sql

@@ -172,10 +172,10 @@ INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `ran
 (1008, 1000, 0, '菜单管理', 'SystemMenu', '/system/menu', '', 8, 1, 0);
 
 -- =====================================================
--- 11. 设备层模版管理 (Layer Template) - rank 8
+-- 11. 设备模版管理 (Device Template) - rank 8
 -- =====================================================
 INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, icon, show_link, show_parent, keep_alive) VALUES
-(1100, 0, 0, '设备模版管理', 'LayerTemplate', '/layer-template', '', 8, 'ri:stack-line', 1, 0, 1);
+(1100, 0, 0, '设备模版管理', 'LayerTemplate', '/layer-template', '', 8, 'ri:stack-line', 1, 0, 1);
 
 -- =====================================================
 -- 角色菜单关联:admin 角色(role_id=1)拥有所有菜单

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

@@ -109,6 +109,12 @@ export interface LayerTemplate {
   /** 同步状态颜色 / Sync status color */
   syncStatusColor?: string;
 
+  /** 模版类型: LAYER-层模版, CABINET-柜模版 / Template type: LAYER or CABINET */
+  templateType?: string;
+
+  /** 模版类型展示标签 / Template type display label */
+  templateTypeLabel?: string;
+
   /** 设备类型名称 / Device type name */
   deviceTypeName?: string;
 
@@ -163,6 +169,9 @@ export interface LayerTemplateQueryParams {
   /** 同步状态 / Sync status */
   syncStatus?: number;
 
+  /** 模版类型: LAYER-层模版, CABINET-柜模版 / Template type */
+  templateType?: string;
+
   /** 状态 / Status */
   status?: number;
 }

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

@@ -3,7 +3,7 @@ export default {
   component: () => import("@/views/layer-template/index.vue"),
   name: "LayerTemplate",
   meta: {
-    title: "设备模版管理",
+    title: "设备模版管理",
     icon: "ri:layout-grid-line",
     rank: 11,
     roles: ["admin"]

+ 12 - 10
haha-admin-web/src/views/layer-template/components/EditDialog.vue

@@ -17,8 +17,8 @@ interface FormData {
   templateName: string;
   /** 设备类型: 1-静态柜(单门), 2-动态柜(双门) */
   deviceType: number;
-  /** 模板类型: 1-层模板 */
-  templateType: number;
+  /** 模版类型: LAYER-层模版, CABINET-柜模版 */
+  templateType: string;
   /** 设备层数 */
   shelfNum: number;
   /** 左门楼层配置 */
@@ -34,7 +34,7 @@ const emit = defineEmits<{
 // 对话框可见状态
 const visible = ref(false);
 // 对话框标题
-const title = ref("新增模版");
+const title = ref("新增设备模版");
 // 是否为编辑模式
 const isEdit = computed(() => title.value === "修改");
 // 编辑时的记录ID
@@ -53,7 +53,7 @@ const activeRightFloorTab = ref("right-1");
 const form = reactive<FormData>({
   templateName: "",
   deviceType: 1,
-  templateType: 1,
+  templateType: "LAYER",
   shelfNum: 5,
   leftFloors: [],
   rightFloors: []
@@ -94,7 +94,7 @@ const isDoubleDoor = computed(() => form.deviceType === 2);
 function resetForm() {
   form.templateName = "";
   form.deviceType = 1;
-  form.templateType = 1;
+  form.templateType = "LAYER";
   form.shelfNum = 5;
   form.leftFloors = [];
   form.rightFloors = [];
@@ -175,7 +175,7 @@ watch(
  * 打开对话框
  */
 async function open(dialogTitle?: string, row?: any) {
-  title.value = dialogTitle || "新增模版";
+  title.value = dialogTitle || "新增设备模版";
   resetForm();
   visible.value = true;
 
@@ -200,6 +200,7 @@ async function loadTemplateData(id: number) {
     if (data) {
       form.templateName = data.templateName || "";
       form.deviceType = data.deviceType || 1;
+      form.templateType = data.templateType || "LAYER";
       form.shelfNum = data.shelfNum || 5;
 
       // 解析左门层数据(JSON字符串 -> FloorConfig数组)
@@ -307,6 +308,7 @@ function buildSubmitData() {
     deviceId: editDeviceId.value,
     templateName: form.templateName.trim(),
     deviceType: form.deviceType,
+    templateType: form.templateType,
     shelfNum: form.shelfNum,
     leftFloors: form.leftFloors,
     rightFloors: isDoubleDoor.value ? form.rightFloors : [],
@@ -341,7 +343,7 @@ async function handleSubmit() {
 
     if (res.code === 200 || res.code === 0) {
       message(
-        `${isEdit.value ? "修改" : "新增"}模版成功`,
+        `${isEdit.value ? "修改" : "新增"}设备模版成功`,
         { type: "success" }
       );
       visible.value = false;
@@ -426,10 +428,10 @@ defineExpose({
           </el-form-item>
         </el-col>
         <el-col :span="12">
-          <el-form-item label="模类型">
+          <el-form-item label="模类型">
             <el-radio-group v-model="form.templateType">
-              <el-radio :value="1">层模板</el-radio>
-              <el-radio :value="2">柜模板</el-radio>
+              <el-radio value="LAYER">层模版</el-radio>
+              <el-radio value="CABINET">柜模版</el-radio>
             </el-radio-group>
           </el-form-item>
         </el-col>

+ 18 - 2
haha-admin-web/src/views/layer-template/index.vue

@@ -26,6 +26,7 @@ const {
   dataList,
   pagination,
   syncStatusOptions,
+  templateTypeOptions,
 
   // 设备相关(新增)
   deviceOptions,
@@ -111,6 +112,21 @@ function handleSingleSync() {
           />
         </el-select>
       </el-form-item>
+      <el-form-item label="模版类型:" prop="templateType">
+        <el-select
+          v-model="form.templateType"
+          placeholder="请选择模版类型"
+          clearable
+          class="w-[180px]!"
+        >
+          <el-option
+            v-for="item in templateTypeOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </el-form-item>
       <el-form-item>
         <el-button
           type="primary"
@@ -128,7 +144,7 @@ function handleSingleSync() {
 
     <!-- 表格区域 -->
     <PureTableBar
-      title="设备模版管理"
+      title="设备模版管理"
       :columns="columns"
       @refresh="onSearch"
     >
@@ -139,7 +155,7 @@ function handleSingleSync() {
           :icon="useRenderIcon(AddFill)"
           @click="openDialog()"
         >
-          新增模版
+          新增设备模版
         </el-button>
 
         <!-- 分隔线 -->

+ 45 - 12
haha-admin-web/src/views/layer-template/utils/hook.ts

@@ -13,7 +13,7 @@ import { type Ref, ref, reactive, onMounted, h } from "vue";
 import { ElTag } from "element-plus";
 
 /**
- * 模版搜索表单类型定义
+ * 设备模版搜索表单类型定义
  */
 export interface LayerTemplateSearchForm {
   /** 设备ID / Device ID */
@@ -22,10 +22,12 @@ export interface LayerTemplateSearchForm {
   templateName: string;
   /** 同步状态 / Sync status */
   syncStatus: string | number;
+  /** 模版类型 / Template type */
+  templateType: string;
 }
 
 /**
- * 模版数据项类型
+ * 设备模版数据项类型
  */
 export interface LayerTemplateItem {
   /** 主键ID / Primary key ID */
@@ -38,6 +40,10 @@ export interface LayerTemplateItem {
   deviceId?: number | string;
   /** 设备名称 / Device name */
   deviceName?: string;
+  /** 模版类型: LAYER-设备模版, CABINET-柜模版 */
+  templateType?: string;
+  /** 模版类型展示标签 */
+  templateTypeLabel?: string;
   /** 总楼层数 / Total floor count */
   totalFloors?: number;
   /** 同步状态 (0-未同步, 1-同步中, 2-已同步, 3-同步失败) */
@@ -73,7 +79,8 @@ export function useProduct(tableRef?: Ref) {
   const form = reactive<LayerTemplateSearchForm>({
     deviceId: "",
     templateName: "",
-    syncStatus: ""
+    syncStatus: "",
+    templateType: ""
   });
 
   // 数据列表状态
@@ -106,6 +113,12 @@ export function useProduct(tableRef?: Ref) {
     { label: "同步失败", value: 3 }
   ];
 
+  // 模版类型选项
+  const templateTypeOptions = [
+    { label: "层模版", value: "LAYER" },
+    { label: "柜模版", value: "CABINET" }
+  ];
+
   // 表格列定义
   const columns: TableColumnList = [
     {
@@ -134,12 +147,30 @@ export function useProduct(tableRef?: Ref) {
       minWidth: 100,
       align: "center"
     },
+    {
+      label: "模版类型",
+      prop: "templateType",
+      width: 100,
+      align: "center",
+      cellRenderer: ({ row }) => {
+        const typeMap: Record<string, { label: string; type: string }> = {
+          LAYER: { label: "层模版", type: "primary" },
+          CABINET: { label: "柜模版", type: "success" }
+        };
+        const info = typeMap[row.templateType] || {
+          label: row.templateTypeLabel || row.templateType || "-",
+          type: "info"
+        };
+        return h(ElTag, { type: info.type as any }, () => info.label);
+      }
+    },
     {
       label: "机柜层数",
       prop: "totalFloors",
       width: 100,
       align: "center",
-      formatter: ({ totalFloors }) => (totalFloors != null ? `${totalFloors}层` : "-")
+      formatter: ({ totalFloors, templateType }) =>
+        templateType === "CABINET" ? "-" : (totalFloors != null ? `${totalFloors}层` : "-")
     },
     {
       label: "同步状态",
@@ -256,13 +287,14 @@ export function useProduct(tableRef?: Ref) {
       if (form.syncStatus !== "" && form.syncStatus != null) {
         searchParams.syncStatus = parseInt(form.syncStatus.toString(), 10);
       }
+      if (form.templateType) searchParams.templateType = form.templateType;
 
       const { data } = await getLayerTemplateList(searchParams);
       dataList.value = data.list || [];
       pagination.total = Number(data.total) || 0;
     } catch (error) {
-      console.error("获取模版列表失败:", error);
-      message("获取模版列表失败", { type: "error" });
+      console.error("获取设备模版列表失败:", error);
+      message("获取设备模版列表失败", { type: "error" });
     } finally {
       setTimeout(() => {
         loading.value = false;
@@ -313,7 +345,7 @@ export function useProduct(tableRef?: Ref) {
   }
 
   /**
-   * 删除模版
+   * 删除设备模版
    * Delete layer template
    * @param row - 要删除的行数据
    */
@@ -344,7 +376,7 @@ export function useProduct(tableRef?: Ref) {
   }
 
   /**
-   * 根据选择的设备ID同步单个设备的模版
+   * 根据选择的设备ID同步单个设备的设备模版
    * Sync layer template for a specific selected device
    * @param deviceId - 设备ID(从下拉框选择的)
    */
@@ -360,7 +392,7 @@ export function useProduct(tableRef?: Ref) {
       const deviceLabel = device?.label || deviceId;
 
       await ElMessageBox.confirm(
-        `确认要同步设备 "${deviceLabel}" 的模版信息吗?`,
+        `确认要同步设备 "${deviceLabel}" 的设备模版信息吗?`,
         "同步确认",
         {
           confirmButtonText: "确定",
@@ -388,7 +420,7 @@ export function useProduct(tableRef?: Ref) {
   }
 
   /**
-   * 全量同步所有设备的模版(默认行为)
+   * 全量同步所有设备的设备模版(默认行为)
    * Batch sync all devices' layer templates (default behavior)
    *
    * 流程:
@@ -417,7 +449,7 @@ export function useProduct(tableRef?: Ref) {
     try {
       const deviceCount = deviceOptions.value.length;
       await ElMessageBox.confirm(
-        `确认要同步全部 ${deviceCount} 个设备的层模版信息吗?\n\n此操作将从哈哈零兽平台拉取每个设备的最新层模版配置。\n\n预计耗时:${Math.ceil(deviceCount / 10)} 秒`,
+        `确认要同步全部 ${deviceCount} 个设备的设备模版信息吗?\n\n此操作将从哈哈零兽平台拉取每个设备的最新设备模版配置。\n\n预计耗时:${Math.ceil(deviceCount / 10)} 秒`,
         "全量同步确认",
         {
           confirmButtonText: "确定",
@@ -467,7 +499,7 @@ export function useProduct(tableRef?: Ref) {
 
   // 组件挂载时自动加载数据和设备列表
   onMounted(async () => {
-    // 并行加载模版列表和设备列表
+    // 并行加载设备模版列表和设备列表
     await Promise.all([onSearch(), loadDeviceList()]);
   });
 
@@ -479,6 +511,7 @@ export function useProduct(tableRef?: Ref) {
     pagination,
     columns,
     syncStatusOptions,
+    templateTypeOptions,
 
     // 设备相关状态(新增)
     deviceOptions,

+ 24 - 22
haha-admin/src/main/java/com/haha/admin/controller/LayerTemplateController.java

@@ -19,7 +19,8 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 设备层模版管理控制器
+ * 设备模版管理控制器
+ * 支持设备模版(LAYER)和柜模版(CABINET)两种类型
  */
 @Slf4j
 @RestController
@@ -30,9 +31,9 @@ public class LayerTemplateController {
     private final LayerTemplateService layerTemplateService;
 
     /**
-     * 分页查询模版列表
+     * 分页查询设备模版列表
      * @param queryDTO 查询参数
-     * @return 模版列表
+     * @return 设备模版列表
      */
     @RequirePermission("layer-template:read")
     @GetMapping("/list")
@@ -44,7 +45,8 @@ public class LayerTemplateController {
                 queryDTO.getPageSize(),
                 queryDTO.getDeviceId(),
                 queryDTO.getSyncStatus(),
-                queryDTO.getTemplateName()
+                queryDTO.getTemplateName(),
+                queryDTO.getTemplateType()
         );
 
         // 构造前端需要的分页数据格式
@@ -59,42 +61,42 @@ public class LayerTemplateController {
     }
 
     /**
-     * 获取模版详情
+     * 获取设备模版详情
      * @param id 模版ID
-     * @return 模版详情
+     * @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.error(404, "设备模版不存在");
         }
         return Result.success("查询成功", template);
     }
 
     /**
-     * 根据设备ID获取模版
+     * 根据设备ID获取设备模版
      * @param deviceId 设备ID
-     * @return 模版信息
+     * @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.error(404, "该设备的设备模版不存在");
         }
         return Result.success("查询成功", template);
     }
 
     /**
-     * 创建模版
-     * @param dto 模版信息
+     * 创建设备模版
+     * @param dto 设备模版信息
      * @return 创建结果
      */
     @RequirePermission("layer-template:create")
-    @Log(module = "设备层模版管理", operation = OperationType.INSERT, summary = "创建层模版")
+    @Log(module = "设备设备模版管理", operation = OperationType.INSERT, summary = "创建设备模版")
     @PostMapping
     public Result<LayerTemplate> create(@RequestBody LayerTemplateCreateDTO dto) {
         LayerTemplate saved = layerTemplateService.create(dto);
@@ -102,13 +104,13 @@ public class LayerTemplateController {
     }
 
     /**
-     * 更新模版
+     * 更新设备模版
      * @param id 模版ID
-     * @param dto 模版信息
+     * @param dto 设备模版信息
      * @return 更新结果
      */
     @RequirePermission("layer-template:update")
-    @Log(module = "设备层模版管理", operation = OperationType.UPDATE, summary = "更新层模版")
+    @Log(module = "设备设备模版管理", operation = OperationType.UPDATE, summary = "更新设备模版")
     @PutMapping("/{id}")
     public Result<LayerTemplate> update(@PathVariable Long id, @RequestBody LayerTemplateUpdateDTO dto) {
         LayerTemplate saved = layerTemplateService.update(id, dto);
@@ -116,12 +118,12 @@ public class LayerTemplateController {
     }
 
     /**
-     * 删除模版(软删除)
+     * 删除设备模版(软删除)
      * @param id 模版ID
      * @return 删除结果
      */
     @RequirePermission("layer-template:delete")
-    @Log(module = "设备层模版管理", operation = OperationType.DELETE, summary = "删除层模版")
+    @Log(module = "设备设备模版管理", operation = OperationType.DELETE, summary = "删除设备模版")
     @DeleteMapping("/{id}")
     public Result<Void> delete(@PathVariable Long id) {
         boolean success = layerTemplateService.softDelete(id);
@@ -129,12 +131,12 @@ public class LayerTemplateController {
     }
 
     /**
-     * 从哈哈平台同步单个设备模版
+     * 从哈哈平台同步单个设备设备模版
      * @param deviceId 设备ID
      * @return 同步结果
      */
     @RequirePermission("layer-template:update")
-    @Log(module = "设备层模版管理", operation = OperationType.SYNC, summary = "同步层模版")
+    @Log(module = "设备设备模版管理", operation = OperationType.SYNC, summary = "同步设备模版")
     @PostMapping("/{deviceId}/sync")
     public Result<Void> sync(@PathVariable String deviceId) {
         boolean success = layerTemplateService.syncFromHaha(deviceId);
@@ -142,12 +144,12 @@ public class LayerTemplateController {
     }
 
     /**
-     * 批量同步模版
+     * 批量同步设备模版
      * @param deviceIds 设备ID列表
      * @return 批量同步结果
      */
     @RequirePermission("layer-template:update")
-    @Log(module = "设备层模版管理", operation = OperationType.SYNC, summary = "批量同步层模版")
+    @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);

+ 1 - 0
haha-admin/src/main/resources/sql/layer_template.sql

@@ -4,6 +4,7 @@ CREATE TABLE IF NOT EXISTS `t_layer_template` (
   `device_id` VARCHAR(64) NOT NULL COMMENT '设备ID(SN号)',
   `template_name` VARCHAR(256) DEFAULT NULL COMMENT '模板名称',
   `device_type` INT DEFAULT NULL COMMENT '设备类型',
+  `template_type` VARCHAR(20) DEFAULT 'LAYER' COMMENT '模版类型: LAYER-层模版, CABINET-柜模版',
   `shelf_num` INT DEFAULT NULL COMMENT '机柜层数',
   `left_floors` JSON DEFAULT NULL COMMENT '左门层数据(JSON格式)',
   `right_floors` JSON DEFAULT NULL COMMENT '右门层数据(JSON格式)',

+ 11 - 10
haha-admin/src/main/resources/sql/layer_template_init.sql

@@ -1,7 +1,7 @@
 -- =====================================================
--- 设备模版管理功能 - 数据库初始化脚本
+-- 设备模版管理功能 - 数据库初始化脚本
 -- 创建时间: 2026-04-12
--- 说明: 用于初始化设备模版管理功能所需的数据库表和权限
+-- 说明: 用于初始化设备模版管理功能所需的数据库表和权限
 -- =====================================================
 
 -- 1. 创建层模版表
@@ -11,6 +11,7 @@ CREATE TABLE IF NOT EXISTS `t_layer_template` (
   `device_id` VARCHAR(64) NOT NULL COMMENT '设备ID(SN号)',
   `template_name` VARCHAR(256) DEFAULT NULL COMMENT '模板名称',
   `device_type` INT DEFAULT NULL COMMENT '设备类型',
+  `template_type` VARCHAR(20) DEFAULT 'LAYER' COMMENT '模版类型: LAYER-层模版, CABINET-柜模版',
   `shelf_num` INT DEFAULT NULL COMMENT '机柜层数',
   `left_floors` JSON DEFAULT NULL COMMENT '左门层数据(JSON格式)',
   `right_floors` JSON DEFAULT NULL COMMENT '右门层数据(JSON格式)',
@@ -26,20 +27,20 @@ CREATE TABLE IF NOT EXISTS `t_layer_template` (
   KEY `idx_template_id` (`template_id`)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备层模版表';
 
--- 2. 添加设备模版管理模块的权限
--- 2. 添加设备模版管理模块的权限
+-- 2. 添加设备模版管理模块的权限
+-- 2. 添加设备模版管理模块的权限
 INSERT INTO t_data_permission (id,permission_code, permission_name, module_code, module_name, operation_type, description, sort_order) VALUES
-(68,'layer-template:create', '创建层模版', 'layer-template', '设备层模版管理', 'create', '添加新层模版', 151),
-(69,'layer-template:read', '查询层模版', 'layer-template', '设备层模版管理', 'read', '查看层模版列表和详情', 152),
-(70,'layer-template:update', '修改层模版', 'layer-template', '设备层模版管理', 'update', '编辑层模版信息和同步操作', 153),
-(71,'layer-template:delete', '删除层模版', 'layer-template', '设备层模版管理', 'delete', '删除层模版', 154);
--- 3. 为超级管理员角色(role.id=1)分配设备模版管理权限
+(68,'layer-template:create', '创建设备模版', 'layer-template', '设备模版管理', 'create', '添加新设备模版', 151),
+(69,'layer-template:read', '查询设备模版', 'layer-template', '设备模版管理', 'read', '查看设备模版列表和详情', 152),
+(70,'layer-template:update', '修改设备模版', 'layer-template', '设备模版管理', 'update', '编辑设备模版信息和同步操作', 153),
+(71,'layer-template:delete', '删除设备模版', 'layer-template', '设备模版管理', 'delete', '删除设备模版', 154);
+-- 3. 为超级管理员角色(role.id=1)分配设备模版管理权限
 INSERT IGNORE INTO t_role_permission (role_id, permission_id)
 SELECT 1, id FROM t_data_permission WHERE module_code = 'layer-template' AND status = 1;
 
 -- =====================================================
 -- 执行完成提示
 -- =====================================================
-SELECT '✅ 设备模版管理功能数据库初始化完成!' AS message;
+SELECT '✅ 设备模版管理功能数据库初始化完成!' AS message;
 SELECT COUNT(*) AS table_exists FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 't_layer_template';
 SELECT COUNT(*) AS permission_count FROM t_data_permission WHERE module_code = 'layer-template';

+ 91 - 0
haha-admin/src/main/resources/sql/layer_template_upgrade_v2.sql

@@ -0,0 +1,91 @@
+-- =====================================================
+-- 设备模版管理 — 增量升级脚本 (v1 → v2)
+-- 执行时间: 2026-07-23
+-- 说明: 从"设备层模版管理"升级为"设备模版管理"
+--       支持 LAYER(层模版) 和 CABINET(柜模版) 两种类型
+-- 注意: 本脚本幂等,可重复执行
+-- =====================================================
+
+-- =========================================================
+-- 1. t_layer_template 表结构变更
+-- =========================================================
+
+-- 1.1 添加 template_type 列(如果不存在则添加)
+--     存量数据全部标记为 LAYER(旧版只有层模版)
+SELECT COUNT(*) INTO @col_exists
+FROM information_schema.columns
+WHERE table_schema = DATABASE()
+  AND table_name = 't_layer_template'
+  AND column_name = 'template_type';
+
+SET @sql = IF(@col_exists = 0,
+    'ALTER TABLE `t_layer_template` ADD COLUMN `template_type` VARCHAR(20) DEFAULT ''LAYER'' COMMENT ''模版类型: LAYER-层模版, CABINET-柜模版'' AFTER `device_type`',
+    'SELECT ''template_type 列已存在,跳过'' AS message');
+PREPARE stmt FROM @sql;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
+
+-- 1.2 存量数据回填(将 NULL 值设为默认值 LAYER)
+UPDATE `t_layer_template` SET `template_type` = 'LAYER' WHERE `template_type` IS NULL;
+
+-- 1.3 更新表注释
+ALTER TABLE `t_layer_template` COMMENT = '设备模版表(支持层模版和柜模版)';
+
+-- =========================================================
+-- 2. 菜单表变更
+-- =========================================================
+
+-- 2.1 菜单名称更新
+UPDATE `t_menu`
+SET `title` = '设备模版管理'
+WHERE `name` = 'LayerTemplate'
+  AND `title` = '设备层模版管理'
+  AND `deleted` = 0;
+
+-- =========================================================
+-- 3. 权限表变更
+-- =========================================================
+
+-- 3.1 权限名称 & 描述更新
+UPDATE `t_data_permission`
+SET `permission_name` = '创建设备模版',
+    `module_name`     = '设备模版管理',
+    `description`     = '添加新设备模版'
+WHERE `permission_code` = 'layer-template:create'
+  AND `permission_name` = '创建层模版';
+
+UPDATE `t_data_permission`
+SET `permission_name` = '查询设备模版',
+    `module_name`     = '设备模版管理',
+    `description`     = '查看设备模版列表和详情'
+WHERE `permission_code` = 'layer-template:read'
+  AND `permission_name` = '查询层模版';
+
+UPDATE `t_data_permission`
+SET `permission_name` = '修改设备模版',
+    `module_name`     = '设备模版管理',
+    `description`     = '编辑设备模版信息和同步操作'
+WHERE `permission_code` = 'layer-template:update'
+  AND `permission_name` = '修改层模版';
+
+UPDATE `t_data_permission`
+SET `permission_name` = '删除设备模版',
+    `module_name`     = '设备模版管理',
+    `description`     = '删除设备模版'
+WHERE `permission_code` = 'layer-template:delete'
+  AND `permission_name` = '删除层模版';
+
+-- =====================================================
+-- 执行完成
+-- =====================================================
+SELECT '✅ 设备模版管理升级 v1→v2 执行完成!' AS message;
+
+-- 验证
+SELECT '--- 表结构 ---' AS '';
+SHOW COLUMNS FROM `t_layer_template` WHERE Field = 'template_type';
+SELECT '--- 存量数据的 template_type 分布 ---' AS '';
+SELECT `template_type`, COUNT(*) AS cnt FROM `t_layer_template` WHERE `is_deleted` = 0 GROUP BY `template_type`;
+SELECT '--- 菜单标题 ---' AS '';
+SELECT `id`, `title` FROM `t_menu` WHERE `name` = 'LayerTemplate' AND `deleted` = 0;
+SELECT '--- 权限名称 ---' AS '';
+SELECT `permission_code`, `permission_name`, `module_name` FROM `t_data_permission` WHERE `module_code` = 'layer-template';

+ 3 - 3
haha-admin/src/main/resources/sql/menu_fix_ranks_and_icons.sql

@@ -8,9 +8,9 @@ UPDATE t_menu SET `rank` = 7  WHERE id = 700 AND deleted = 0;  -- 分销管理:
 UPDATE t_menu SET `rank` = 8  WHERE id = 800 AND deleted = 0;  -- 营销中心: 7→8
 UPDATE t_menu SET `rank` = 9  WHERE id = 900 AND deleted = 0;  -- 统计报表: 8→9
 UPDATE t_menu SET `rank` = 10 WHERE id = 1000 AND deleted = 0; -- 系统设置: 8→10
-UPDATE t_menu SET `rank` = 11 WHERE id = 1100 AND deleted = 0; -- 设备模版管理: 8→11
+UPDATE t_menu SET `rank` = 11 WHERE id = 1100 AND deleted = 0; -- 设备模版管理: 8→11
 
--- 2. 修复设备模版图标与库存管理重复 (ri:stack-line → ri:layout-grid-line)
+-- 2. 修复设备模版图标与库存管理重复 (ri:stack-line → ri:layout-grid-line)
 UPDATE t_menu SET icon = 'ri:layout-grid-line' WHERE id = 1100 AND deleted = 0;
 
 -- 最终一级菜单排序:
@@ -24,4 +24,4 @@ UPDATE t_menu SET icon = 'ri:layout-grid-line' WHERE id = 1100 AND deleted = 0;
 --  8: 营销中心
 --  9: 统计报表
 -- 10: 系统设置
--- 11: 设备模版管理
+-- 11: 设备模版管理

+ 21 - 0
haha-common/src/main/java/com/haha/common/constant/SyncConstants.java

@@ -29,6 +29,27 @@ public final class SyncConstants {
      */
     public static final int STATUS_SYNC_FAILED = 3;
     
+    // ==================== 模版类型 ====================
+
+    /**
+     * 模版类型:层模版(动态柜,使用 4.5.1/4.5.2 API)
+     */
+    public static final String TEMPLATE_TYPE_LAYER = "LAYER";
+
+    /**
+     * 模版类型:柜模版(静态柜,使用 4.1/4.5 API)
+     */
+    public static final String TEMPLATE_TYPE_CABINET = "CABINET";
+
+    /**
+     * 获取模版类型描述
+     */
+    public static String getTemplateTypeDesc(String templateType) {
+        if (TEMPLATE_TYPE_LAYER.equals(templateType)) return "层模版";
+        if (TEMPLATE_TYPE_CABINET.equals(templateType)) return "柜模版";
+        return "未知";
+    }
+
     // ==================== 状态描述 ====================
     
     /**

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

@@ -33,4 +33,9 @@ public class LayerTemplateCreateDTO {
      * 右门层数据(可选)
      */
     private List<FloorConfig> rightFloors;
+
+    /**
+     * 模版类型: LAYER-层模版, CABINET-柜模版(可选,默认LAYER)
+     */
+    private String templateType;
 }

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

@@ -26,4 +26,9 @@ public class LayerTemplateUpdateDTO {
      * 右门层数据(可选,更新时传入则全量替换)
      */
     private List<FloorConfig> rightFloors;
+
+    /**
+     * 模版类型: LAYER-层模版, CABINET-柜模版(可选)
+     */
+    private String templateType;
 }

+ 11 - 2
haha-entity/src/main/java/com/haha/entity/LayerTemplate.java

@@ -11,8 +11,9 @@ import java.io.Serializable;
 import java.time.LocalDateTime;
 
 /**
- * 设备层模版实体类
- * 对应哈哈平台设备设备层模版管理
+ * 设备模版实体类
+ * 支持层模版(LAYER)和柜模版(CABINET)两种类型
+ * 对应哈哈平台设备模版管理
  */
 @Data
 @TableName("t_layer_template")
@@ -46,6 +47,11 @@ public class LayerTemplate implements Serializable {
      */
     private Integer deviceType;
 
+    /**
+     * 模版类型: LAYER-层模版(动态柜), CABINET-柜模版(静态柜)
+     */
+    private String templateType;
+
     /**
      * 机柜层数
      */
@@ -102,6 +108,9 @@ public class LayerTemplate implements Serializable {
     @TableField(exist = false)
     private String deviceTypeName;
 
+    @TableField(exist = false)
+    private String templateTypeLabel;
+
     @TableField(exist = false)
     private String leftFloorsDisplay;
 

+ 6 - 1
haha-entity/src/main/java/com/haha/entity/dto/LayerTemplateQueryDTO.java

@@ -4,7 +4,7 @@ import lombok.Data;
 import lombok.EqualsAndHashCode;
 
 /**
- * 商品层模版查询DTO
+ * 设备模版查询DTO
  */
 @Data
 @EqualsAndHashCode(callSuper = true)
@@ -24,4 +24,9 @@ public class LayerTemplateQueryDTO extends PageQueryDTO {
      * 模板名称(模糊查询)
      */
     private String templateName;
+
+    /**
+     * 模版类型: LAYER-层模版, CABINET-柜模版
+     */
+    private String templateType;
 }

+ 31 - 2
haha-sdk/src/main/java/com/haha/sdk/api/GoodsApi.java

@@ -128,8 +128,10 @@ public class GoodsApi extends BaseApi {
     }
     
     /**
-     * 商品上下架
-     * 
+     * 商品上下架(修改设备柜模版)
+     * 给指定设备配置可售卖的商品,对应哈哈 API 4.5
+     * 对于柜模版(CABINET)设备,此方法用于更新设备商品配置
+     *
      * @param deviceId 设备id,sn号
      * @param goodsCode 商品code,多个商品code用逗号(,)隔开
      * @param salesType 上下架类型:IN上架,OUT下架。call_type='CHANGE'时必传。
@@ -152,6 +154,33 @@ public class GoodsApi extends BaseApi {
         Map<String, Object> result = post("/systemtemplate/deviceGoods", params);
         return (Map<String, Object>) result.get("data");
     }
+ 
+    /**
+     * 获取设备柜模版数据
+     * 获取静态柜设备的可售卖商品列表,对应哈哈 API 4.1
+     * 对于柜模版(CABINET)设备,此方法用于获取设备商品配置
+     *
+     * @param deviceId 设备ID
+     * @return 柜模版商品数据
+     * @throws HahaException 调用失败时抛出
+     */
+    public Map<String, Object> getCabinetTemplate(String deviceId) throws HahaException {
+        return getDeviceGoodsList(deviceId, null);
+    }
+
+    /**
+     * 更新设备柜模版
+     * 覆盖式更新静态柜设备的商品配置,对应哈哈 API 4.5
+     *
+     * @param deviceId 设备ID
+     * @param goodsCode 商品code列表,多个用逗号隔开
+     * @param callType 请求更新方式:CHANGE更新模式,COVER覆盖模式
+     * @return 更新结果
+     * @throws HahaException 调用失败时抛出
+     */
+    public Map<String, Object> updateCabinetTemplate(String deviceId, String goodsCode, String callType) throws HahaException {
+        return setStatus(deviceId, goodsCode, null, callType);
+    }
     
     /**
      * 获取设备层模板

+ 7 - 6
haha-service/src/main/java/com/haha/service/LayerTemplateService.java

@@ -19,16 +19,17 @@ import java.util.Map;
 public interface LayerTemplateService extends IService<LayerTemplate> {
 
     /**
-     * 分页查询模版列表
+     * 分页查询设备模版列表
      *
-     * @param page        页码(从1开始)
-     * @param pageSize    每页条数
-     * @param deviceId    设备ID(可选,模糊查询)
-     * @param syncStatus  同步状态(可选):0-未同步,1-同步中,2-已同步,3-同步失败
+     * @param page         页码(从1开始)
+     * @param pageSize     每页条数
+     * @param deviceId     设备ID(可选,模糊查询)
+     * @param syncStatus   同步状态(可选):0-未同步,1-同步中,2-已同步,3-同步失败
      * @param templateName 模板名称(可选,模糊查询)
+     * @param templateType 模版类型(可选):LAYER-层模版, CABINET-柜模版
      * @return 分页结果
      */
-    IPage<LayerTemplate> getPage(int page, int pageSize, String deviceId, Integer syncStatus, String templateName);
+    IPage<LayerTemplate> getPage(int page, int pageSize, String deviceId, Integer syncStatus, String templateName, String templateType);
 
     /**
      * 获取层模版详情(带标签填充)

+ 208 - 30
haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java

@@ -43,11 +43,12 @@ import java.util.List;
 import java.util.Map;
 
 /**
- * 设备设备层模版管理服务实现类
- * 提供设备层模版的完整业务逻辑实现,包括CRUD和同步功能
+ * 设备模版管理服务实现类
+ * 支持层模版(LAYER)和柜模版(CABINET)两种类型
+ * 提供设备模版的CRUD和同步功能,同步时自动检测模版类型
  *
  * @author haha-service
- * @version 1.0.0
+ * @version 2.0.0
  */
 @Slf4j
 @Service
@@ -78,9 +79,9 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
      * @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);
+    public IPage<LayerTemplate> getPage(int page, int pageSize, String deviceId, Integer syncStatus, String templateName, String templateType) {
+        log.info("分页查询设备模版: page={}, pageSize={}, deviceId={}, syncStatus={}, templateName={}, templateType={}",
+                page, pageSize, deviceId, syncStatus, templateName, templateType);
 
         LambdaQueryWrapper<LayerTemplate> wrapper = new LambdaQueryWrapper<>();
 
@@ -88,13 +89,14 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         wrapper.like(deviceId != null && !deviceId.isEmpty(), LayerTemplate::getDeviceId, deviceId)
                .eq(syncStatus != null, LayerTemplate::getSyncStatus, syncStatus)
                .like(templateName != null && !templateName.isEmpty(), LayerTemplate::getTemplateName, templateName)
+               .eq(templateType != null && !templateType.isEmpty(), LayerTemplate::getTemplateType, templateType)
                .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());
+        log.info("设备模版查询结果: total={}, records={}", pageResult.getTotal(), pageResult.getRecords().size());
 
         // 填充展示字段(同步状态标签、设备类型等)
         pageResult.getRecords().forEach(this::fillLabels);
@@ -213,6 +215,9 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         if (dto.getTemplateName() != null) {
             existing.setTemplateName(dto.getTemplateName());
         }
+        if (dto.getTemplateType() != null) {
+            existing.setTemplateType(dto.getTemplateType());
+        }
         if (dto.getLeftFloors() != null && !dto.getLeftFloors().isEmpty()) {
             try {
                 existing.setLeftFloors(objectMapper.writeValueAsString(dto.getLeftFloors()));
@@ -318,14 +323,52 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         updateSyncStatus(deviceId, SyncConstants.STATUS_SYNCING, null);
 
         try {
-            // 步骤2: 调用哈哈平台API获取层模版数据
+            // 步骤2: 自动检测模版类型并调用对应API
             GoodsApi goodsApi = hahaClient.getGoodsApi();
-            Map<String, Object> apiData = goodsApi.getLayerTemplate(deviceId);
+            Map<String, Object> apiData = null;
+            String detectedTemplateType = null;
+
+            // 先尝试层模版 API (4.5.1),失败或数据无效则尝试柜模版 API (4.1)
+            // 注意:BaseApi.post() 只在 code != 1 时抛 HahaException
+            // 如果平台对柜模版设备返回 code=1 但 data 为空,抛异常也不会触发
+            // 所以必须同时验证返回数据是否包含层模版特有字段
+            boolean layerValid = false;
+            try {
+                apiData = goodsApi.getLayerTemplate(deviceId);
+                // 验证返回数据是否为有效的层模版
+                // products 必须是 Map 且非空,不能是空字符串(柜模版设备会返回 products:"")
+                layerValid = apiData != null
+                        && apiData.get("products") instanceof Map
+                        && !((Map<?, ?>) apiData.get("products")).isEmpty();
+                if (layerValid) {
+                    detectedTemplateType = SyncConstants.TEMPLATE_TYPE_LAYER;
+                    log.info("设备 {} 检测为层模版(LAYER)类型,数据: {}", deviceId, apiData);
+                } else {
+                    log.info("层模版API返回成功但数据为空/无效(缺少products字段),降级为柜模版: deviceId={}", deviceId);
+                }
+            } catch (Exception layerEx) {
+                log.info("层模版API调用失败(code!=1),尝试柜模版API: deviceId={}, error={}", deviceId, layerEx.getMessage());
+            }
 
-            log.info("成功获取平台层模版数据: deviceId={}, data={}", deviceId, apiData);
+            if (!layerValid) {
+                try {
+                    apiData = goodsApi.getCabinetTemplate(deviceId);
+                    detectedTemplateType = SyncConstants.TEMPLATE_TYPE_CABINET;
+                    log.info("设备 {} 检测为柜模版(CABINET)类型,数据: {}", deviceId, apiData);
+                } catch (Exception cabinetEx) {
+                    log.error("柜模版API也调用失败: deviceId={}, error={}", deviceId, cabinetEx.getMessage());
+                    throw new BusinessException(500, "无法获取设备模版数据,层模版和柜模版API均失败");
+                }
+            }
 
-            // 步骤3-4: 解析返回数据并转换为实体对象
-            LayerTemplate updatedTemplate = convertFromApiData(deviceId, apiData);
+            // 步骤3-4: 根据模版类型解析数据并转换为实体对象
+            LayerTemplate updatedTemplate;
+            if (SyncConstants.TEMPLATE_TYPE_CABINET.equals(detectedTemplateType)) {
+                updatedTemplate = convertCabinetFromApiData(deviceId, apiData);
+            } else {
+                updatedTemplate = convertFromApiData(deviceId, apiData);
+            }
+            updatedTemplate.setTemplateType(detectedTemplateType);
 
             // 步骤5: 更新或创建本地记录
             if (isNewRecord) {
@@ -337,11 +380,13 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
                 localTemplate.setLeftFloors(updatedTemplate.getLeftFloors());
                 localTemplate.setRightFloors(updatedTemplate.getRightFloors());
                 localTemplate.setGoodsRule(updatedTemplate.getGoodsRule());
+                localTemplate.setTemplateType(detectedTemplateType);
                 localTemplate.setSyncTime(LocalDateTime.now());
                 localTemplate.setUpdateTime(LocalDateTime.now());
 
                 this.save(localTemplate);
-                log.info("新建层模版记录: id={}, deviceId={}", localTemplate.getId(), deviceId);
+                log.info("新建设备模版记录: id={}, deviceId={}, templateType={}",
+                        localTemplate.getId(), deviceId, detectedTemplateType);
             } else {
                 // 已存在记录,更新数据
                 LambdaUpdateWrapper<LayerTemplate> updateWrapper = new LambdaUpdateWrapper<>();
@@ -353,10 +398,11 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
                              .set(LayerTemplate::getLeftFloors, updatedTemplate.getLeftFloors())
                              .set(LayerTemplate::getRightFloors, updatedTemplate.getRightFloors())
                              .set(LayerTemplate::getGoodsRule, updatedTemplate.getGoodsRule())
+                             .set(LayerTemplate::getTemplateType, detectedTemplateType)
                              .set(LayerTemplate::getUpdateTime, LocalDateTime.now());
 
                 this.update(updateWrapper);
-                log.info("更新层模版记录: deviceId={}", deviceId);
+                log.info("更新设备模版记录: deviceId={}, templateType={}", deviceId, detectedTemplateType);
             }
 
             // 步骤6: 从层模版数据初始化库存(仅对尚不存在的商品创建 stock=0 记录)
@@ -376,7 +422,7 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
             // 步骤7: 更新同步状态为"已同步"并记录时间
             updateSyncStatus(deviceId, SyncConstants.STATUS_SYNCED, LocalDateTime.now());
 
-            log.info("层模版同步成功: deviceId={}", deviceId);
+            log.info("设备模版同步成功: deviceId={}, templateType={}", deviceId, detectedTemplateType);
             return true;
 
         } catch (Exception e) {
@@ -526,6 +572,79 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         return template;
     }
 
+    /**
+     * 将柜模版 API (4.1) 返回数据转换为实体对象
+     * 柜模版没有层的概念,将4.1返回的分类商品列表展平为单层结构
+     *
+     * @param deviceId 设备ID
+     * @param apiData  API返回的柜模版数据
+     * @return 转换后的实体对象
+     */
+    private LayerTemplate convertCabinetFromApiData(String deviceId, Map<String, Object> apiData) {
+        LayerTemplate template = new LayerTemplate();
+        template.setDeviceId(deviceId);
+        template.setTemplateType(SyncConstants.TEMPLATE_TYPE_CABINET);
+
+        // 柜模版无层数概念
+        template.setShelfNum(null);
+
+        // 将 dataInfo 中的所有商品展平为单个虚拟层
+        List<FloorConfig> floors = new ArrayList<>();
+        FloorConfig singleFloor = new FloorConfig();
+        singleFloor.setFloor(1);
+        List<GoodsItem> goodsList = new ArrayList<>();
+
+        if (apiData.containsKey("dataInfo")) {
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> dataInfoList = (List<Map<String, Object>>) apiData.get("dataInfo");
+            if (dataInfoList != null) {
+                for (Map<String, Object> category : dataInfoList) {
+                    @SuppressWarnings("unchecked")
+                    List<Map<String, Object>> goods = (List<Map<String, Object>>) category.get("goods");
+                    if (goods != null) {
+                        for (Map<String, Object> item : goods) {
+                            GoodsItem goodsItem = new GoodsItem();
+                            goodsItem.setKey(String.valueOf(item.getOrDefault("code", "")));
+                            goodsItem.setLabel(String.valueOf(item.getOrDefault("name", "")));
+                            goodsItem.setPic(String.valueOf(item.getOrDefault("pic", "")));
+                            goodsItem.setRec_pic(String.valueOf(item.getOrDefault("rec_pic", "")));
+                            Object cPrice = item.get("c_price");
+                            if (cPrice != null) {
+                                goodsItem.setC_price(String.valueOf(cPrice));
+                            }
+                            Object discount = item.get("discount");
+                            if (discount != null) {
+                                goodsItem.setDiscount(String.valueOf(discount));
+                            }
+                            Object disPrice = item.get("dis_price");
+                            if (disPrice != null && !String.valueOf(disPrice).isEmpty()) {
+                                goodsItem.setDis_price(String.valueOf(disPrice));
+                            }
+                            goodsItem.setType(String.valueOf(item.getOrDefault("type", "")));
+                            goodsItem.setStock(0); // 柜模版中stock始终为0,由实际库存决定
+                            goodsList.add(goodsItem);
+                        }
+                    }
+                }
+            }
+        }
+
+        singleFloor.setGoods(goodsList);
+        floors.add(singleFloor);
+
+        try {
+            template.setLeftFloors(objectMapper.writeValueAsString(floors));
+        } catch (JsonProcessingException e) {
+            log.error("柜模版左门层数据JSON序列化失败: deviceId={}", deviceId, e);
+        }
+
+        // 柜模版没有右门
+        template.setRightFloors(null);
+
+        log.info("柜模版数据转换完成: deviceId={}, 商品数={}", deviceId, goodsList.size());
+        return template;
+    }
+
     /**
      * 将前端DTO转换为提交给平台的JSON格式
      * 用于将本地修改后的层模版数据推送到哈哈平台
@@ -562,6 +681,7 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         LayerTemplate template = new LayerTemplate();
         template.setDeviceId(dto.getDeviceId());
         template.setTemplateName(dto.getTemplateName());
+        template.setTemplateType(dto.getTemplateType() != null ? dto.getTemplateType() : SyncConstants.TEMPLATE_TYPE_LAYER);
 
         // 序列化左门层数据
         if (dto.getLeftFloors() != null && !dto.getLeftFloors().isEmpty()) {
@@ -607,6 +727,11 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
             template.setDeviceTypeName(getDeviceTypeName(template.getDeviceType()));
         }
 
+        // 2.5. 模版类型标签
+        if (template.getTemplateType() != null) {
+            template.setTemplateTypeLabel(SyncConstants.getTemplateTypeDesc(template.getTemplateType()));
+        }
+
         // 3. 左门层数据展示(美化JSON显示)
         if (template.getLeftFloors() != null && !template.getLeftFloors().isEmpty()) {
             template.setLeftFloorsDisplay(formatJsonForDisplay(template.getLeftFloors()));
@@ -918,25 +1043,37 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
 
         update(template.getId(), dto);
 
-        // 推送层模板到哈哈平台
-        pushLayerTemplateToHaha(deviceId, leftFloors);
+        // 推送模版到哈哈平台(根据模版类型路由到不同API)
+        pushTemplateToHaha(deviceId, leftFloors);
         return true;
     }
 
     /**
-     * 推送层模板配置到哈哈平台
+     * 推送模版配置到哈哈平台
+     * 根据设备的模版类型自动选择对应API:
+     * - LAYER层模版: 使用 4.5.2 (postLayerTypeInfo)
+     * - CABINET柜模版: 使用 4.5 (systemtemplate/deviceGoods)
      */
-    private void pushLayerTemplateToHaha(String deviceId, List<FloorConfig> leftFloors) {
+    private void pushTemplateToHaha(String deviceId, List<FloorConfig> leftFloors) {
         try {
-            Map<String, Object> products = new HashMap<>();
-            products.put("left", leftFloors);
-            products.put("right", List.of());
-            String productsJson = objectMapper.writeValueAsString(products);
-            hahaClient.getGoodsApi().updateLayerTemplate(deviceId, productsJson);
+            LayerTemplate template = getByDeviceId(deviceId);
+            if (template != null && SyncConstants.TEMPLATE_TYPE_CABINET.equals(template.getTemplateType())) {
+                // 柜模版: 提取所有商品编码,使用 4.5 API 覆盖更新
+                String goodsCodes = extractProductCodesFromFloors(leftFloors);
+                hahaClient.getGoodsApi().updateCabinetTemplate(deviceId, goodsCodes, "COVER");
+                log.info("柜模版已同步到哈哈平台 - deviceId: {}, goodsCodes: {}", deviceId, goodsCodes);
+            } else {
+                // 层模版: 使用 4.5.2 API — 原有逻辑
+                Map<String, Object> products = new HashMap<>();
+                products.put("left", leftFloors);
+                products.put("right", List.of());
+                String productsJson = objectMapper.writeValueAsString(products);
+                hahaClient.getGoodsApi().updateLayerTemplate(deviceId, productsJson);
+                log.info("层模版已同步到哈哈平台 - deviceId: {}", deviceId);
+            }
             updateSyncStatus(deviceId, SyncConstants.STATUS_SYNCED, LocalDateTime.now());
-            log.info("层模板已同步到哈哈平台 - deviceId: {}", deviceId);
         } catch (Exception e) {
-            log.error("推送层模板到哈哈平台失败 - deviceId: {}, error: {}", deviceId, e.getMessage(), e);
+            log.error("推送模版到哈哈平台失败 - deviceId: {}, error: {}", deviceId, e.getMessage(), e);
         }
     }
 
@@ -971,26 +1108,47 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
     }
 
     /**
-     * 从层模版API数据中提取商品编码,为不存在的商品创建库存记录(stock=0)
+     * 从模版API数据中提取商品编码,为不存在的商品创建库存记录(stock=0)
+     * 支持层模版(4.5.1)和柜模版(4.1)两种数据格式
      * 仅在库存记录不存在时创建,已存在的记录不受影响
      */
     private void initInventoryFromTemplate(String deviceId, Map<String, Object> apiData) {
         // 提取所有商品编码
         java.util.Set<String> productCodes = new java.util.HashSet<>();
 
-        if (apiData.containsKey("products")) {
+        // 层模版格式: products.left / products.right(必须 instanceof Map,柜模版可能返回 products:"")
+        if (apiData.containsKey("products") && apiData.get("products") instanceof Map) {
             @SuppressWarnings("unchecked")
             Map<String, Object> products = (Map<String, Object>) apiData.get("products");
             extractCodesFromFloors(products.get("left"), productCodes);
             extractCodesFromFloors(products.get("right"), productCodes);
         }
+        // 柜模版格式: dataInfo[].goods[].code
+        else if (apiData.containsKey("dataInfo")) {
+            @SuppressWarnings("unchecked")
+            List<Map<String, Object>> dataInfoList = (List<Map<String, Object>>) apiData.get("dataInfo");
+            if (dataInfoList != null) {
+                for (Map<String, Object> category : dataInfoList) {
+                    @SuppressWarnings("unchecked")
+                    List<Map<String, Object>> goods = (List<Map<String, Object>>) category.get("goods");
+                    if (goods != null) {
+                        for (Map<String, Object> item : goods) {
+                            Object code = item.get("code");
+                            if (code != null && !code.toString().isEmpty()) {
+                                productCodes.add(code.toString());
+                            }
+                        }
+                    }
+                }
+            }
+        }
 
         if (productCodes.isEmpty()) {
-            log.info("层模版中无商品配置,跳过库存初始化: deviceId={}", deviceId);
+            log.info("设备模版中无商品配置,跳过库存初始化: deviceId={}", deviceId);
             return;
         }
 
-        log.info("从层模版提取到 {} 个商品编码,开始初始化库存: deviceId={}", productCodes.size(), deviceId);
+        log.info("从设备模版提取到 {} 个商品编码,开始初始化库存: deviceId={}", productCodes.size(), deviceId);
         int created = 0;
         int skipped = 0;
 
@@ -1057,4 +1215,24 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
             log.warn("解析层商品数据失败: {}", e.getMessage());
         }
     }
+
+    /**
+     * 从 FloorConfig 列表中提取所有商品编码,用逗号拼接
+     * 用于柜模版更新时传递给 4.5 API
+     */
+    private String extractProductCodesFromFloors(List<FloorConfig> floors) {
+        java.util.Set<String> codes = new java.util.LinkedHashSet<>();
+        if (floors != null) {
+            for (FloorConfig floor : floors) {
+                if (floor.getGoods() != null) {
+                    for (GoodsItem goods : floor.getGoods()) {
+                        if (goods.getKey() != null && !goods.getKey().isEmpty()) {
+                            codes.add(goods.getKey());
+                        }
+                    }
+                }
+            }
+        }
+        return String.join(",", codes);
+    }
 }