Browse Source

feat: 全面接入haha-sdk未使用接口,补齐三层链路

梳理SDK中24个API方法,接入12个未使用接口,补齐Service→Controller→前端完整调用链路:
- 高优: setPayStatus(支付状态回传)、updateLayerTemplate(货道模板上行同步)、setStatus(上下架)、getDeviceGoodsList(设备商品列表)
- 中优: getOrderMedia(订单图像)、getNewGoodsInfo(新品状态查询)、queryOrder(订单对账补偿)、getIdMapping(商品ID映射)
- 低优: getDeviceStatus(设备详细状态)、getCallbackInfo(回调投递状态)、getLayerRecognize(静态柜分层识别)、shutdown(优雅关闭)

新增 CallbackController,4个Controller各新增端点,11个前端API函数,4个页面的Vue UI组件

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 2 tuần trước cách đây
mục cha
commit
2599537a7f
28 tập tin đã thay đổi với 703 bổ sung10 xóa
  1. 5 0
      haha-admin-web/src/api/device.ts
  2. 5 0
      haha-admin-web/src/api/newProductApply.ts
  3. 20 0
      haha-admin-web/src/api/order.ts
  4. 21 0
      haha-admin-web/src/api/product.ts
  5. 12 0
      haha-admin-web/src/views/device/index.vue
  6. 36 0
      haha-admin-web/src/views/device/utils/hook.tsx
  7. 9 0
      haha-admin-web/src/views/order/index.vue
  8. 50 1
      haha-admin-web/src/views/order/utils/hook.tsx
  9. 20 0
      haha-admin-web/src/views/product/index.vue
  10. 13 0
      haha-admin-web/src/views/product/new-apply/index.vue
  11. 19 1
      haha-admin-web/src/views/product/new-apply/utils/hook.tsx
  12. 106 1
      haha-admin-web/src/views/product/utils/hook.tsx
  13. 14 3
      haha-admin/src/main/java/com/haha/admin/config/HahaSdkConfig.java
  14. 36 0
      haha-admin/src/main/java/com/haha/admin/controller/CallbackController.java
  15. 12 0
      haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java
  16. 17 0
      haha-admin/src/main/java/com/haha/admin/controller/NewProductApplyController.java
  17. 38 0
      haha-admin/src/main/java/com/haha/admin/controller/OrderController.java
  18. 42 0
      haha-admin/src/main/java/com/haha/admin/controller/ProductController.java
  19. 12 1
      haha-miniapp/src/main/java/com/haha/miniapp/config/HahaSdkConfig.java
  20. 8 0
      haha-service/src/main/java/com/haha/service/DeviceService.java
  21. 20 0
      haha-service/src/main/java/com/haha/service/HahaCallbackService.java
  22. 9 0
      haha-service/src/main/java/com/haha/service/NewProductApplyService.java
  23. 19 0
      haha-service/src/main/java/com/haha/service/ProductService.java
  24. 13 1
      haha-service/src/main/java/com/haha/service/impl/DeviceServiceImpl.java
  25. 72 0
      haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java
  26. 20 0
      haha-service/src/main/java/com/haha/service/impl/LayerTemplateServiceImpl.java
  27. 13 1
      haha-service/src/main/java/com/haha/service/impl/NewProductApplyServiceImpl.java
  28. 42 1
      haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

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

@@ -82,3 +82,8 @@ export const bindDeviceReplenisher = (deviceId: string, data: { replenisherId: n
 export const unbindDeviceReplenisher = (deviceId: string, replenisherId: number) => {
   return http.request<Result>("delete", `/devices/${deviceId}/replenishers/${replenisherId}`);
 };
+
+// 查询设备详细状态(含门/锁/摄像头/网络状态)
+export const getDeviceDetailStatus = (deviceId: string) => {
+  return http.request<Result>("get", `/devices/${deviceId}/detail-status`);
+};

+ 5 - 0
haha-admin-web/src/api/newProductApply.ts

@@ -58,3 +58,8 @@ export const checkBarcode = (barcode: string) => {
     `/new-product-apply/check-barcode?barcode=${barcode}`
   );
 };
+
+// 主动刷新新品申请审核状态
+export const refreshApplyStatus = (id: number) => {
+  return http.request<Result>("post", `/new-product-apply/${id}/refresh-status`);
+};

+ 20 - 0
haha-admin-web/src/api/order.ts

@@ -76,4 +76,24 @@ export const cancelPayScoreOrder = (data: {
   reason?: string;
 }) => {
   return http.request<Result>("post", "/order/payscore/cancel", { data });
+};
+
+// 获取订单图像(开门前后图片/视频)
+export const getOrderMedia = (params: { orderId?: string; activityId?: string }) => {
+  return http.request<Result>("get", "/order/0/media", { params });
+};
+
+// 从哈哈平台同步查询订单
+export const queryOrderFromHaha = (params: { orderId?: string; activityId?: string }) => {
+  return http.request<Result>("get", "/order/query-from-haha", { params });
+};
+
+// 获取静态柜分层识别结果
+export const getLayerRecognize = (params: { orderId?: string; activityId?: string }) => {
+  return http.request<Result>("get", "/order/layer-recognize", { params });
+};
+
+// 查询回调投递状态
+export const getCallbackInfo = (params: { type: string; actId: string }) => {
+  return http.request<Result>("get", "/callback/info", { params });
 };

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

@@ -67,3 +67,24 @@ export const getMasterProductList = (params: {
 export const addToMerchantProducts = (data: { codeList: string }) => {
   return http.request<Result>("post", "/products/master/add-to-merchant", { data });
 };
+
+// 商品上下架控制
+export const setProductShelfStatus = (data: {
+  deviceId: string;
+  goodsCode: string;
+  salesType: string;
+  callType?: string;
+}) => {
+  return http.request<Result>("post", "/products/shelf-status", { data });
+};
+
+// 获取指定设备可售卖商品列表
+export const getDeviceProducts = (deviceId: string) => {
+  return http.request<Result>("get", `/products/device/${deviceId}`);
+};
+
+// 查询商品ID对应关系
+export const getIdMapping = (codeList?: string) => {
+  const params = codeList ? { codeList } : {};
+  return http.request<Result>("get", "/products/id-mapping", { params });
+};

+ 12 - 0
haha-admin-web/src/views/device/index.vue

@@ -9,6 +9,7 @@ import Temperature from "~icons/ri/temp-hot-line";
 import Volume from "~icons/ri/volume-up-line";
 import Document from "~icons/ep/document";
 import User from "~icons/ep/user";
+import Monitor from "~icons/ep/monitor";
 
 defineOptions({
   name: "DeviceManage"
@@ -51,6 +52,7 @@ const {
   replenisherAllSelected,
   replenisherSelectPartial,
   confirmReplenisherLoading,
+  handleDeviceDetailStatus,
   handleBindReplenisher,
   toggleReplenisherSelect,
   toggleAllReplenishers,
@@ -174,6 +176,16 @@ const {
             >
               音量
             </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(Monitor)"
+              @click="handleDeviceDetailStatus(row)"
+            >
+              详细状态
+            </el-button>
             <el-button
               class="reset-margin"
               link

+ 36 - 0
haha-admin-web/src/views/device/utils/hook.tsx

@@ -12,6 +12,7 @@ import {
   getDeviceReplenishers,
   bindDeviceReplenisher,
   unbindDeviceReplenisher,
+  getDeviceDetailStatus,
   type DoorRecordItem
 } from "@/api/device";
 import { getEnabledShops } from "@/api/shop";
@@ -481,6 +482,40 @@ export function useDevice(tableRef: Ref) {
     fetchDoorRecords();
   }
 
+  // 设备详细状态
+  async function handleDeviceDetailStatus(row: DeviceItem) {
+    try {
+      const { data } = await getDeviceDetailStatus(row.deviceId);
+      addDialog({
+        title: `设备详细状态 - ${row.deviceId}`,
+        width: "40%",
+        draggable: true,
+        closeOnClickModal: false,
+        contentRenderer: () => (
+          <div style="max-height: 60vh; overflow-y: auto;">
+            {data ? (
+              <div style="display: flex; flex-direction: column; gap: 8px;">
+                {Object.entries(data).map(([key, value]: [string, any]) => (
+                  <div style="display: flex; justify-content: space-between; padding: 8px 12px; background: #f9f9f9; border-radius: 6px;">
+                    <span style="font-weight: 500; text-transform: capitalize;">
+                      {key.replace(/_/g, " ")}
+                    </span>
+                    <span>{typeof value === "object" ? JSON.stringify(value) : String(value ?? "-")}</span>
+                  </div>
+                ))}
+              </div>
+            ) : (
+              <div style="text-align: center; padding: 40px; color: #999;">暂无状态数据</div>
+            )}
+          </div>
+        ),
+        hideFooter: true
+      });
+    } catch {
+      message("获取设备详细状态失败", { type: "error" });
+    }
+  }
+
   // ==================== 补货员绑定管理(勾选模式) ====================
   const replenisherDialogVisible = ref(false);
   const replenisherLoading = ref(false);
@@ -655,6 +690,7 @@ export function useDevice(tableRef: Ref) {
     replenisherAllSelected,
     replenisherSelectPartial,
     confirmReplenisherLoading,
+    handleDeviceDetailStatus,
     handleBindReplenisher,
     toggleReplenisherSelect,
     toggleAllReplenishers,

+ 9 - 0
haha-admin-web/src/views/order/index.vue

@@ -7,6 +7,7 @@ import Refresh from "~icons/ep/refresh";
 import View from "~icons/ep/view";
 import Refund from "~icons/ep/money";
 import Cancel from "~icons/ep/delete";
+import Search from "~icons/ep/search";
 
 defineOptions({
   name: "OrderManage"
@@ -26,6 +27,7 @@ const {
   handleDetail,
   handleRefund,
   handlePayScoreCancel,
+  handleQueryFromHaha,
   handleSizeChange,
   handleCurrentChange
 } = useOrder(tableRef);
@@ -139,6 +141,13 @@ async function doPayScoreCancel() {
         </el-button>
       </el-form-item>
       <el-form-item>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(Search)"
+          @click="handleQueryFromHaha()"
+        >
+          平台查询
+        </el-button>
         <el-button
           type="warning"
           :icon="useRenderIcon(Cancel)"

+ 50 - 1
haha-admin-web/src/views/order/utils/hook.tsx

@@ -7,7 +7,8 @@ import {
   getOrderList,
   getOrderById,
   refundOrder,
-  cancelPayScoreOrder
+  cancelPayScoreOrder,
+  queryOrderFromHaha
 } from "@/api/order";
 import { type Ref, ref, computed, reactive, onMounted } from "vue";
 import {
@@ -257,6 +258,53 @@ export function useOrder(tableRef: Ref) {
     handleCurrentPageChange(val, pagination, onSearch);
   }
 
+  // 从哈哈平台同步查询订单
+  async function handleQueryFromHaha(orderNo?: string, activityId?: string) {
+    const queryForm = reactive({ orderId: orderNo || "", activityId: activityId || "" });
+
+    addDialog({
+      title: "从哈哈平台查询订单",
+      width: "40%",
+      draggable: true,
+      closeOnClickModal: false,
+      contentRenderer: () => (
+        <div>
+          <div style="display: flex; gap: 8px; margin-bottom: 12px;">
+            <ElInput v-model={queryForm.orderId} placeholder="哈哈订单号" style="flex:1" />
+            <ElInput v-model={queryForm.activityId} placeholder="活动ID" style="flex:1" />
+          </div>
+        </div>
+      ),
+      beforeSure: async (done) => {
+        try {
+          const { data } = await queryOrderFromHaha({
+            orderId: queryForm.orderId || undefined,
+            activityId: queryForm.activityId || undefined
+          });
+          if (data) {
+            message("查询成功,订单信息已返回", { type: "success" });
+            // 展示查询结果
+            addDialog({
+              title: "哈哈平台订单详情",
+              width: "60%",
+              draggable: true,
+              closeOnClickModal: false,
+              contentRenderer: () => (
+                <pre style="max-height: 60vh; overflow-y: auto; padding: 12px; background: #f5f5f5; border-radius: 8px; font-size: 13px;">
+                  {JSON.stringify(data, null, 2)}
+                </pre>
+              ),
+              hideFooter: true
+            });
+          }
+          done();
+        } catch {
+          message("查询失败", { type: "error" });
+        }
+      }
+    });
+  }
+
   async function handleDetail(row: OrderItem) {
     try {
       const { data } = await getOrderById(row.id);
@@ -627,6 +675,7 @@ export function useOrder(tableRef: Ref) {
     handleDetail,
     handleRefund,
     handlePayScoreCancel,
+    handleQueryFromHaha,
     handleSizeChange,
     handleCurrentChange
   };

+ 20 - 0
haha-admin-web/src/views/product/index.vue

@@ -8,6 +8,8 @@ import Delete from "~icons/ep/delete";
 import EditPen from "~icons/ep/edit-pen";
 import AddFill from "~icons/ri/add-circle-line";
 import Sync from "~icons/ep/refresh";
+import Shelf from "~icons/ep/shop";
+import Box from "~icons/ep/box";
 
 defineOptions({
   name: "ProductManage"
@@ -28,6 +30,8 @@ const {
   openDialog,
   handleDelete,
   handleSync,
+  handleShelfStatus,
+  handleDeviceProducts,
   handleSizeChange,
   handleCurrentChange
 } = useProduct(tableRef);
@@ -111,6 +115,12 @@ const {
         >
           新增商品
         </el-button>
+        <el-button
+          :icon="useRenderIcon(Box)"
+          @click="handleDeviceProducts()"
+        >
+          设备商品
+        </el-button>
       </template>
       <template v-slot="{ size, dynamicColumns }">
         <pure-table
@@ -170,6 +180,16 @@ const {
             >
               同步
             </el-button>
+            <el-button
+              class="reset-margin"
+              link
+              type="primary"
+              :size="size"
+              :icon="useRenderIcon(Shelf)"
+              @click="handleShelfStatus(row)"
+            >
+              上下架
+            </el-button>
           </template>
         </pure-table>
       </template>

+ 13 - 0
haha-admin-web/src/views/product/new-apply/index.vue

@@ -9,6 +9,7 @@ import EditPen from "~icons/ep/edit-pen";
 import Refresh from "~icons/ep/refresh";
 import AddFill from "~icons/ri/add-circle-line";
 import View from "~icons/ep/view";
+import RefreshRight from "~icons/ep/refresh-right";
 
 defineOptions({
   name: "NewProductApply"
@@ -28,6 +29,7 @@ const {
   resetForm,
   openDialog,
   handleDelete,
+  handleRefreshStatus,
   handleSizeChange,
   handleCurrentChange
 } = useNewProductApply(tableRef);
@@ -147,6 +149,17 @@ const {
           @page-current-change="handleCurrentChange"
         >
           <template #operation="{ row }">
+            <el-button
+              v-if="row.status === 1"
+              class="reset-margin"
+              link
+              type="success"
+              :size="size"
+              :icon="useRenderIcon(RefreshRight)"
+              @click="handleRefreshStatus(row)"
+            >
+              刷新
+            </el-button>
             <el-button
               class="reset-margin"
               link

+ 19 - 1
haha-admin-web/src/views/product/new-apply/utils/hook.tsx

@@ -11,7 +11,8 @@ import {
   saveNewProductApplyDraft,
   updateNewProductApply,
   deleteNewProductApply,
-  checkBarcode
+  checkBarcode,
+  refreshApplyStatus
 } from "@/api/newProductApply";
 import {
   ElForm,
@@ -227,6 +228,22 @@ export function useNewProductApply(tableRef: Ref) {
     return null;
   }
 
+  // 主动刷新审核状态
+  async function handleRefreshStatus(row: any) {
+    try {
+      const { data, code } = await refreshApplyStatus(row.id);
+      if (code === 0 && data) {
+        message(`状态刷新成功`, { type: "success" });
+        onSearch();
+        getStatistics();
+      } else {
+        message("刷新失败", { type: "error" });
+      }
+    } catch {
+      message("刷新失败", { type: "error" });
+    }
+  }
+
   function handleSizeChange(val: number) {
     pagination.pageSize = val;
     onSearch();
@@ -255,6 +272,7 @@ export function useNewProductApply(tableRef: Ref) {
     handleDelete,
     handleSaveDraft,
     handleCheckBarcode,
+    handleRefreshStatus,
     handleSizeChange,
     handleCurrentChange
   };

+ 106 - 1
haha-admin-web/src/views/product/utils/hook.tsx

@@ -9,7 +9,9 @@ import {
   addProduct,
   updateProduct,
   deleteProduct,
-  syncProduct
+  syncProduct,
+  setProductShelfStatus,
+  getDeviceProducts
 } from "@/api/product";
 import { type Ref, ref, toRaw, reactive, onMounted } from "vue";
 import {
@@ -381,6 +383,107 @@ export function useProduct(tableRef: Ref) {
     }
   }
 
+  // 商品上下架
+  async function handleShelfStatus(row: ProductItem) {
+    const shelfForm = reactive({
+      deviceId: "",
+      goodsCode: row.code || "",
+      salesType: "IN"
+    });
+
+    addDialog({
+      title: `商品上下架 - ${row.name}`,
+      width: "30%",
+      draggable: true,
+      closeOnClickModal: false,
+      contentRenderer: () => (
+        <ElForm ref={ruleFormRef} model={shelfForm} label-width="100px">
+          <ElFormItem
+            label="设备ID"
+            prop="deviceId"
+            rules={[{ required: true, message: "请输入设备SN号", trigger: "blur" }]}
+          >
+            <ElInput v-model={shelfForm.deviceId} placeholder="设备SN号" clearable />
+          </ElFormItem>
+          <ElFormItem label="操作类型" prop="salesType">
+            <ElRadioGroup v-model={shelfForm.salesType}>
+              <ElRadioButton value="IN">上架</ElRadioButton>
+              <ElRadioButton value="OUT">下架</ElRadioButton>
+            </ElRadioGroup>
+          </ElFormItem>
+        </ElForm>
+      ),
+      beforeSure: async (done) => {
+        try {
+          const res = await setProductShelfStatus({
+            deviceId: shelfForm.deviceId,
+            goodsCode: shelfForm.goodsCode,
+            salesType: shelfForm.salesType
+          });
+          if (res.code === 0) {
+            const action = shelfForm.salesType === "IN" ? "上架" : "下架";
+            message(`商品 ${row.name} ${action}成功`, { type: "success" });
+            done();
+            onSearch();
+          } else {
+            message(res.message || "操作失败", { type: "error" });
+          }
+        } catch (error) {
+          message("操作失败", { type: "error" });
+        }
+      }
+    });
+  }
+
+  // 查看设备商品列表
+  async function handleDeviceProducts() {
+    const deviceForm = reactive({ deviceId: "" });
+    const deviceProductList: any = ref([]);
+
+    const loadDeviceProducts = async () => {
+      if (!deviceForm.deviceId) return;
+      try {
+        const { data } = await getDeviceProducts(deviceForm.deviceId);
+        deviceProductList.value = data || [];
+      } catch {
+        deviceProductList.value = [];
+      }
+    };
+
+    addDialog({
+      title: "设备商品列表",
+      width: "50%",
+      draggable: true,
+      closeOnClickModal: false,
+      contentRenderer: () => (
+        <div>
+          <div style="display: flex; gap: 8px; margin-bottom: 12px;">
+            <ElInput v-model={deviceForm.deviceId} placeholder="请输入设备SN号" style="flex:1" />
+            <el-button type="primary" onClick={loadDeviceProducts}>查询</el-button>
+          </div>
+          <div v-loading={false}>
+            {deviceProductList.value.length === 0 ? (
+              <div style="text-align: center; padding: 40px; color: #999;">
+                {deviceForm.deviceId ? "该设备暂无商品" : "请输入设备SN号查询"}
+              </div>
+            ) : (
+              <div style="display: flex; flex-direction: column; gap: 4px;">
+                {deviceProductList.value.map((item: any) => (
+                  <div style="display: flex; justify-content: space-between; align-items: center; padding: 8px 12px; background: #f9f9f9; border-radius: 6px;">
+                    <span style="font-weight: 500;">{item.name || "-"}</span>
+                    <span style="color: #999; font-size: 13px;">{item.barcode || "-"}</span>
+                    <span style="color: var(--el-color-primary);">¥{item.price || 0}</span>
+                  </div>
+                ))}
+              </div>
+            )}
+          </div>
+        </div>
+      ),
+      hideFooter: true
+    });
+  }
+
   onMounted(() => {
     onSearch();
   });
@@ -397,6 +500,8 @@ export function useProduct(tableRef: Ref) {
     openDialog,
     handleDelete,
     handleSync,
+    handleShelfStatus,
+    handleDeviceProducts,
     handleSizeChange,
     handleCurrentChange
   };

+ 14 - 3
haha-admin/src/main/java/com/haha/admin/config/HahaSdkConfig.java

@@ -2,6 +2,7 @@ package com.haha.admin.config;
 
 import com.haha.sdk.HahaClient;
 import com.haha.sdk.config.HahaConfig;
+import jakarta.annotation.PreDestroy;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -22,6 +23,8 @@ public class HahaSdkConfig {
     @Value("${haha.api.app-secret}")
     private String appSecret;
 
+    private HahaClient hahaClient;
+
     /**
      * 创建哈哈零售SDK客户端Bean
      * 注意:新版SDK包路径已更改为 com.haha.sdk.HahaClient
@@ -38,11 +41,19 @@ public class HahaSdkConfig {
                 .writeTimeout(30)
                 .enableLog(true)
                 .build();
-        
+
         // 校验配置
         config.validate();
-        
+
         // 创建客户端
-        return new HahaClient(config);
+        this.hahaClient = new HahaClient(config);
+        return this.hahaClient;
+    }
+
+    @PreDestroy
+    public void destroy() {
+        if (hahaClient != null) {
+            hahaClient.shutdown();
+        }
     }
 }

+ 36 - 0
haha-admin/src/main/java/com/haha/admin/controller/CallbackController.java

@@ -0,0 +1,36 @@
+package com.haha.admin.controller;
+
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.vo.Result;
+import com.haha.service.HahaCallbackService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Map;
+
+/**
+ * 哈哈平台回调管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/callback")
+@RequiredArgsConstructor
+public class CallbackController {
+
+    private final HahaCallbackService hahaCallbackService;
+
+    /**
+     * 查询回调投递状态
+     * @param type 识别类型:REC识别结果回调,ORDER订单结果回调
+     * @param actId 活动编号
+     * @return 回调状态信息
+     */
+    @RequirePermission("order:read")
+    @GetMapping("/info")
+    public Result<Map<String, Object>> getCallbackInfo(@RequestParam String type,
+                                                        @RequestParam String actId) {
+        Map<String, Object> result = hahaCallbackService.getCallbackInfo(type, actId);
+        return Result.success("查询成功", result);
+    }
+}

+ 12 - 0
haha-admin/src/main/java/com/haha/admin/controller/DeviceController.java

@@ -216,4 +216,16 @@ public class DeviceController {
         replenisherService.unbindDevice(replenisherId, deviceId);
         return Result.success("解绑成功");
     }
+
+    /**
+     * 查询设备详细状态(含门状态、锁状态、摄像头状态、网络状态)
+     * @param deviceId 设备SN号
+     * @return 设备详细状态
+     */
+    @RequirePermission("device:read")
+    @GetMapping("/{deviceId}/detail-status")
+    public Result<Map<String, Object>> getDetailStatus(@PathVariable String deviceId) {
+        Map<String, Object> status = deviceService.getDeviceDetailStatus(deviceId);
+        return Result.success("查询成功", status);
+    }
 }

+ 17 - 0
haha-admin/src/main/java/com/haha/admin/controller/NewProductApplyController.java

@@ -191,4 +191,21 @@ public class NewProductApplyController {
             return Result.error(e.getMessage());
         }
     }
+
+    /**
+     * 主动刷新新品申请审核状态(回调补偿机制)
+     * @param id 申请ID
+     * @return 最新的申请状态
+     */
+    @RequirePermission("product-apply:read")
+    @PostMapping("/{id}/refresh-status")
+    public Result<Map<String, Object>> refreshStatus(@PathVariable Long id) {
+        NewProductApply apply = newProductApplyService.getById(id);
+        if (apply == null) {
+            return Result.error(404, "申请记录不存在");
+        }
+        Map<String, Object> result = newProductApplyService.getNewGoodsInfo(
+                apply.getHahaCode(), apply.getHahaApplyId());
+        return Result.success("刷新成功", result);
+    }
 }

+ 38 - 0
haha-admin/src/main/java/com/haha/admin/controller/OrderController.java

@@ -12,6 +12,7 @@ import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.Order;
 import com.haha.service.OrderService;
+import com.haha.service.HahaCallbackService;
 import com.haha.service.payment.payscore.PayScoreCancelRequest;
 import com.haha.service.payment.payscore.PayScoreResult;
 import com.haha.service.payment.payscore.PayScoreStrategy;
@@ -34,6 +35,9 @@ public class OrderController {
 
     private final OrderService orderService;
 
+    @Autowired
+    private HahaCallbackService hahaCallbackService;
+
     @Autowired(required = false)
     private PayScoreStrategy payScoreStrategy;
 
@@ -143,4 +147,38 @@ public class OrderController {
             return Result.error(500, "取消异常: " + e.getMessage());
         }
     }
+
+    /**
+     * 获取订单图像(开门前后图片/视频)
+     */
+    @RequirePermission("order:read")
+    @GetMapping("/{id}/media")
+    public Result<Map<String, Object>> getOrderMedia(@PathVariable Long id,
+                                                      @RequestParam(required = false) String orderId,
+                                                      @RequestParam(required = false) String activityId) {
+        Map<String, Object> media = hahaCallbackService.getOrderMedia(orderId, activityId);
+        return Result.success("查询成功", media);
+    }
+
+    /**
+     * 从哈哈平台同步查询订单(对账补偿)
+     */
+    @RequirePermission("order:read")
+    @GetMapping("/query-from-haha")
+    public Result<Map<String, Object>> queryFromHaha(@RequestParam(required = false) String orderId,
+                                                      @RequestParam(required = false) String activityId) {
+        Map<String, Object> result = hahaCallbackService.queryOrderFromHaha(orderId, activityId);
+        return Result.success("查询成功", result);
+    }
+
+    /**
+     * 获取静态柜分层识别结果
+     */
+    @RequirePermission("order:read")
+    @GetMapping("/layer-recognize")
+    public Result<Map<String, Object>> getLayerRecognize(@RequestParam(required = false) String orderId,
+                                                          @RequestParam(required = false) String activityId) {
+        Map<String, Object> result = hahaCallbackService.getLayerRecognize(orderId, activityId);
+        return Result.success("查询成功", result);
+    }
 }

+ 42 - 0
haha-admin/src/main/java/com/haha/admin/controller/ProductController.java

@@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -171,4 +172,45 @@ public class ProductController {
         boolean success = productService.addToMerchant(dto.getCodeList());
         return success ? Result.success("添加成功", null) : Result.error(500, "添加失败");
     }
+
+    /**
+     * 商品上下架控制
+     * @param params 请求参数: deviceId, goodsCode, salesType(IN上架/OUT下架), callType(CHANGE/COVER)
+     * @return 操作结果
+     */
+    @RequirePermission("product:update")
+    @Log(module = "商品管理", operation = OperationType.UPDATE, summary = "商品上下架")
+    @PostMapping("/shelf-status")
+    public Result<Map<String, Object>> setShelfStatus(@RequestBody Map<String, String> params) {
+        Map<String, Object> result = productService.setProductShelfStatus(
+                params.get("deviceId"),
+                params.get("goodsCode"),
+                params.get("salesType"),
+                params.getOrDefault("callType", "CHANGE"));
+        return Result.success("操作成功", result);
+    }
+
+    /**
+     * 获取指定设备可售卖商品列表
+     * @param deviceId 设备SN号
+     * @return 商品列表
+     */
+    @RequirePermission("product:read")
+    @GetMapping("/device/{deviceId}")
+    public Result<List<Product>> getDeviceProducts(@PathVariable String deviceId) {
+        List<Product> list = productService.getProductListBydeviceId(deviceId);
+        return Result.success("查询成功", list);
+    }
+
+    /**
+     * 查询商品ID对应关系
+     * @param codeList 哈哈商品编号列表,多个用逗号隔开
+     * @return 对应关系
+     */
+    @RequirePermission("product:read")
+    @GetMapping("/id-mapping")
+    public Result<Map<String, Object>> getIdMapping(@RequestParam(required = false) String codeList) {
+        Map<String, Object> result = productService.getIdMapping(codeList);
+        return Result.success("查询成功", result);
+    }
 }

+ 12 - 1
haha-miniapp/src/main/java/com/haha/miniapp/config/HahaSdkConfig.java

@@ -2,6 +2,7 @@ package com.haha.miniapp.config;
 
 import com.haha.sdk.HahaClient;
 import com.haha.sdk.config.HahaConfig;
+import jakarta.annotation.PreDestroy;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
@@ -22,6 +23,8 @@ public class HahaSdkConfig {
     @Value("${haha.api.app-secret}")
     private String appSecret;
 
+    private HahaClient hahaClient;
+
     /**
      * 创建哈哈零售SDK客户端Bean
      * 注意:新版SDK包路径已更改为 com.haha.sdk.HahaClient
@@ -43,6 +46,14 @@ public class HahaSdkConfig {
         config.validate();
 
         // 创建客户端
-        return new HahaClient(config);
+        this.hahaClient = new HahaClient(config);
+        return this.hahaClient;
+    }
+
+    @PreDestroy
+    public void destroy() {
+        if (hahaClient != null) {
+            hahaClient.shutdown();
+        }
     }
 }

+ 8 - 0
haha-service/src/main/java/com/haha/service/DeviceService.java

@@ -140,4 +140,12 @@ public interface DeviceService extends IService<Device> {
      * @return 友好的错误描述
      */
     String getHahaErrorMessage(Integer errorCode, String defaultMsg);
+
+    /**
+     * 查询设备详细状态(含门状态、锁状态、摄像头状态、网络状态)
+     *
+     * @param deviceId 设备SN号
+     * @return 设备详细状态信息
+     */
+    Map<String, Object> getDeviceDetailStatus(String deviceId);
 }

+ 20 - 0
haha-service/src/main/java/com/haha/service/HahaCallbackService.java

@@ -55,4 +55,24 @@ public interface HahaCallbackService {
      * 验证回调签名
      */
     boolean validateSign(Map<String, Object> params);
+
+    /**
+     * 获取订单图像(开门前后图片/视频)
+     */
+    Map<String, Object> getOrderMedia(String orderId, String activityId);
+
+    /**
+     * 从哈哈平台查询订单详情(用于对账补偿)
+     */
+    Map<String, Object> queryOrderFromHaha(String orderId, String activityId);
+
+    /**
+     * 查询回调投递状态
+     */
+    Map<String, Object> getCallbackInfo(String type, String actId);
+
+    /**
+     * 获取静态柜分层识别结果
+     */
+    Map<String, Object> getLayerRecognize(String orderId, String activityId);
 }

+ 9 - 0
haha-service/src/main/java/com/haha/service/NewProductApplyService.java

@@ -57,4 +57,13 @@ public interface NewProductApplyService extends IService<NewProductApply> {
      * @return 商品信息(如果存在返回商品信息,否则返回null)
      */
     Map<String, Object> checkBarcodeInTotalList(String barcode);
+
+    /**
+     * 主动查询新品申请审核状态(回调补偿机制)
+     *
+     * @param code 商品code
+     * @param id   新品申请主键id
+     * @return 申请状态信息
+     */
+    Map<String, Object> getNewGoodsInfo(String code, String id);
 }

+ 19 - 0
haha-service/src/main/java/com/haha/service/ProductService.java

@@ -122,4 +122,23 @@ public interface ProductService extends IService<Product> {
      * @return 是否成功
      */
     boolean addToMerchant(String codeList);
+
+    /**
+     * 商品上下架控制
+     *
+     * @param deviceId  设备SN号
+     * @param goodsCode 商品code,多个用逗号隔开
+     * @param salesType 上下架类型:IN上架,OUT下架
+     * @param callType  更新方式:CHANGE更新模式,COVER覆盖模式
+     * @return 操作结果
+     */
+    Map<String, Object> setProductShelfStatus(String deviceId, String goodsCode, String salesType, String callType);
+
+    /**
+     * 查询商品ID对应关系
+     *
+     * @param codeList 哈哈商品编号列表,多个用逗号隔开
+     * @return 对应关系信息
+     */
+    Map<String, Object> getIdMapping(String codeList);
 }

+ 13 - 1
haha-service/src/main/java/com/haha/service/impl/DeviceServiceImpl.java

@@ -434,7 +434,7 @@ private final HahaClient hahaClient;
         if (errorCode == null) {
             return defaultMsg != null ? defaultMsg : "操作失败";
         }
-        
+
         switch (errorCode) {
             case -1:
                 return "参数错误,请检查设备ID是否正确";
@@ -455,4 +455,16 @@ private final HahaClient hahaClient;
                 return defaultMsg != null ? defaultMsg : "开门失败";
         }
     }
+
+    @Override
+    public Map<String, Object> getDeviceDetailStatus(String deviceId) {
+        try {
+            Map<String, Object> status = hahaClient.getDeviceApi().getDeviceStatus(deviceId);
+            log.info("查询设备详细状态 - deviceId: {}, status: {}", deviceId, status);
+            return status;
+        } catch (Exception e) {
+            log.error("查询设备详细状态失败 - deviceId: {}, error: {}", deviceId, e.getMessage());
+            return null;
+        }
+    }
 }

+ 72 - 0
haha-service/src/main/java/com/haha/service/impl/HahaCallbackServiceImpl.java

@@ -424,6 +424,12 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
         updateOrderFromRecognition(order, skuListStr, resourceInfoStr, confidence, activityId);
         logRecognizeResultDetails(resultStr, skuListStr, resourceInfoStr);
 
+        // 低置信度订单拉取开门前后图片供人工审核
+        if (confidence != null && confidence.compareTo(new BigDecimal("0.8")) < 0) {
+            log.info("识别置信度较低({}),拉取订单图像供人工审核 - activityId: {}", confidence, activityId);
+            getOrderMedia(null, activityId);
+        }
+
         // 更新关门状态
         doorRecordService.updateDoorClosed(activityId);
         log.info("AI识别结果处理完成 - activityId: {}, orderId: {}", activityId, order.getId());
@@ -1391,6 +1397,8 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
                 if (PayScoreState.DONE.getCode().equals(state)) {
                     log.info("支付分扣费成功 - orderId: {}, state: {}, outOrderNo: {}",
                             order.getId(), state, result.getOutOrderNo());
+                    // 通知哈哈平台支付已完成
+                    syncPayStatusToHaha(order);
                 } else if (PayScoreState.isPaying(state)) {
                     log.warn("支付分扣款已发起但用户待支付 - orderId: {}, state: {}, outOrderNo: {}",
                             order.getId(), state, result.getOutOrderNo());
@@ -1406,4 +1414,68 @@ public class HahaCallbackServiceImpl implements HahaCallbackService {
             log.error("处理支付分扣费异常 - orderId: {}", order.getId(), e);
         }
     }
+
+    /**
+     * 同步支付状态到哈哈平台
+     * 告知哈哈平台该订单已完成支付
+     */
+    private void syncPayStatusToHaha(Order order) {
+        try {
+            if (order.getOrderNo() != null && !order.getOrderNo().isEmpty()) {
+                hahaClient.getOrderApi().setPayStatus(order.getOrderNo());
+                log.info("支付状态已同步到哈哈平台 - orderNo: {}", order.getOrderNo());
+            }
+        } catch (Exception e) {
+            log.error("同步支付状态到哈哈平台失败 - orderNo: {}", order.getOrderNo(), e);
+        }
+    }
+
+    /**
+     * 获取订单图像用于人工审核
+     * 当识别置信度低于阈值时拉取开门前后图片
+     */
+    public Map<String, Object> getOrderMedia(String orderId, String activityId) {
+        try {
+            return hahaClient.getOrderApi().getOrderMedia(orderId, activityId);
+        } catch (Exception e) {
+            log.error("获取订单图像失败 - orderId: {}, activityId: {}", orderId, activityId, e);
+            return null;
+        }
+    }
+
+    /**
+     * 从哈哈平台查询订单详情(用于对账补偿)
+     */
+    public Map<String, Object> queryOrderFromHaha(String orderId, String activityId) {
+        try {
+            return hahaClient.getOrderApi().queryOrder(orderId, activityId);
+        } catch (Exception e) {
+            log.error("从哈哈平台查询订单失败 - orderId: {}, activityId: {}", orderId, activityId, e);
+            return null;
+        }
+    }
+
+    /**
+     * 查询回调投递状态
+     */
+    public Map<String, Object> getCallbackInfo(String type, String actId) {
+        try {
+            return hahaClient.getOrderApi().getCallbackInfo(type, actId);
+        } catch (Exception e) {
+            log.error("查询回调投递状态失败 - type: {}, actId: {}", type, actId, e);
+            return null;
+        }
+    }
+
+    /**
+     * 获取静态柜分层识别结果
+     */
+    public Map<String, Object> getLayerRecognize(String orderId, String activityId) {
+        try {
+            return hahaClient.getOrderApi().getLayerRecognize(orderId, activityId);
+        } catch (Exception e) {
+            log.error("获取静态柜分层识别结果失败 - orderId: {}, activityId: {}", orderId, activityId, e);
+            return null;
+        }
+    }
 }

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

@@ -853,9 +853,29 @@ public class LayerTemplateServiceImpl extends ServiceImpl<LayerTemplateMapper, L
         dto.setLeftFloors(leftFloors);
 
         update(template.getId(), dto);
+
+        // 推送层模板到哈哈平台
+        pushLayerTemplateToHaha(deviceId, leftFloors);
         return true;
     }
 
+    /**
+     * 推送层模板配置到哈哈平台
+     */
+    private void pushLayerTemplateToHaha(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);
+            updateSyncStatus(deviceId, SyncConstants.STATUS_SYNCED, LocalDateTime.now());
+            log.info("层模板已同步到哈哈平台 - deviceId: {}", deviceId);
+        } catch (Exception e) {
+            log.error("推送层模板到哈哈平台失败 - deviceId: {}, error: {}", deviceId, e.getMessage(), e);
+        }
+    }
+
     /**
      * 构建楼层标签(顶层/底层标识)
      */

+ 13 - 1
haha-service/src/main/java/com/haha/service/impl/NewProductApplyServiceImpl.java

@@ -220,7 +220,7 @@ public class NewProductApplyServiceImpl extends ServiceImpl<NewProductApplyMappe
         try {
             // 调用哈哈平台API查询商品总库
             Map<String, Object> result = hahaClient.getGoodsApi().getTotalList(1, 1, barcode, null, null, null);
-            
+
             if (result != null) {
                 List<Map<String, Object>> list = (List<Map<String, Object>>) result.get("data");
                 if (list != null && !list.isEmpty()) {
@@ -233,4 +233,16 @@ public class NewProductApplyServiceImpl extends ServiceImpl<NewProductApplyMappe
             return null;
         }
     }
+
+    @Override
+    public Map<String, Object> getNewGoodsInfo(String code, String id) {
+        try {
+            Map<String, Object> result = hahaClient.getGoodsApi().getNewGoodsInfo(code, id);
+            log.info("主动查询新品申请状态 - code: {}, id: {}, result: {}", code, id, result);
+            return result;
+        } catch (HahaException e) {
+            log.error("查询新品申请信息失败 - code: {}, id: {}, error: {}", code, id, e.getMessage());
+            return null;
+        }
+    }
 }

+ 42 - 1
haha-service/src/main/java/com/haha/service/impl/ProductServiceImpl.java

@@ -21,6 +21,7 @@ import java.time.LocalDateTime;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 
 @Slf4j
 @Service
@@ -134,7 +135,25 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
 
     @Override
     public List<Product> getProductListBydeviceId(String deviceId) {
-        // 这里需要实现根据设备SN获取商品列表的逻辑,暂时返回所有商品
+        try {
+            Map<String, Object> result = hahaClient.getGoodsApi().getDeviceGoodsList(deviceId, null);
+            if (result != null && result.containsKey("list")) {
+                @SuppressWarnings("unchecked")
+                List<Map<String, Object>> apiList = (List<Map<String, Object>>) result.get("list");
+                return apiList.stream().map(item -> {
+                    Product product = new Product();
+                    product.setCode((String) item.get("code"));
+                    product.setName((String) item.get("product_name"));
+                    product.setBarcode((String) item.get("bar_code"));
+                    if (item.get("price") != null) {
+                        product.setPrice(((Number) item.get("price")).doubleValue());
+                    }
+                    return product;
+                }).collect(Collectors.toList());
+            }
+        } catch (Exception e) {
+            log.error("获取设备商品列表失败 - deviceId: {}, error: {}", deviceId, e.getMessage());
+        }
         return list();
     }
 
@@ -205,6 +224,28 @@ public class ProductServiceImpl extends ServiceImpl<ProductMapper, Product> impl
         }
     }
 
+    @Override
+    public Map<String, Object> setProductShelfStatus(String deviceId, String goodsCode, String salesType, String callType) {
+        try {
+            Map<String, Object> result = hahaClient.getGoodsApi().setStatus(deviceId, goodsCode, salesType, callType);
+            log.info("商品上下架操作成功 - deviceId: {}, goodsCode: {}, salesType: {}", deviceId, goodsCode, salesType);
+            return result;
+        } catch (HahaException e) {
+            log.error("商品上下架操作失败: {}", e.getMessage());
+            throw new BusinessException(500, "商品上下架操作失败: " + e.getMessage());
+        }
+    }
+
+    @Override
+    public Map<String, Object> getIdMapping(String codeList) {
+        try {
+            return hahaClient.getGoodsApi().getIdMapping(codeList);
+        } catch (HahaException e) {
+            log.error("查询商品ID对应关系失败: {}", e.getMessage());
+            throw new BusinessException(500, "查询商品ID对应关系失败: " + e.getMessage());
+        }
+    }
+
     /**
      * 填充商品标签字段
      */