Sfoglia il codice sorgente

feat: qdb-sdk子模块化+ERP店铺商品关联管理+微信支付容错

- qdb-sdk 从独立模块改为 haha-parent 子模块,统一版本管理,新增 ShopApi 和分页查询
- ERP 店铺关联管理:t_erp_shop CRUD,门店绑定/解绑 ERP 店铺
- ERP 商品关联管理:t_erp_goods CRUD,商品绑定/解绑 ERP 商品
- 门店/商品 entity 新增 erpShopId/erpGoodsId 关联字段及下拉 VO
- 微信支付配置初始化失败时优雅降级(返回 null),不再阻塞应用启动
- syncToErp 暂时改为 no-op,等待后续真实对接

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline 1 settimana fa
parent
commit
5bc824a9cc
41 ha cambiato i file con 1722 aggiunte e 82 eliminazioni
  1. 6 0
      docs/database/add_erp_goods_id.sql
  2. 6 0
      docs/database/add_erp_shop_id.sql
  3. 31 0
      docs/database/create_erp_goods.sql
  4. 23 0
      docs/database/create_erp_shop.sql
  5. 10 0
      docs/database/menu_add_erp_goods.sql
  6. 10 0
      docs/database/menu_add_erp_shop.sql
  7. 23 0
      haha-admin-web/src/api/erpGoods.ts
  8. 23 0
      haha-admin-web/src/api/erpShop.ts
  9. 9 0
      haha-admin-web/src/router/modules/operation.ts
  10. 9 0
      haha-admin-web/src/router/modules/product.ts
  11. 260 0
      haha-admin-web/src/views/erp-goods/index.vue
  12. 238 0
      haha-admin-web/src/views/erp-shop/index.vue
  13. 7 0
      haha-admin/pom.xml
  14. 77 0
      haha-admin/src/main/java/com/haha/admin/controller/ErpGoodsController.java
  15. 77 0
      haha-admin/src/main/java/com/haha/admin/controller/ErpShopController.java
  16. 2 2
      haha-admin/src/main/resources/application.yml
  17. 1 0
      haha-common/src/main/java/com/haha/common/vo/ShopSelectVO.java
  18. 87 0
      haha-entity/src/main/java/com/haha/entity/ErpGoods.java
  19. 65 0
      haha-entity/src/main/java/com/haha/entity/ErpShop.java
  20. 9 0
      haha-entity/src/main/java/com/haha/entity/Product.java
  21. 12 0
      haha-entity/src/main/java/com/haha/entity/Shop.java
  22. 12 0
      haha-mapper/src/main/java/com/haha/mapper/ErpGoodsMapper.java
  23. 12 0
      haha-mapper/src/main/java/com/haha/mapper/ErpShopMapper.java
  24. 1 1
      haha-mapper/src/main/java/com/haha/mapper/ShopMapper.java
  25. 2 5
      haha-service/pom.xml
  26. 39 0
      haha-service/src/main/java/com/haha/service/ErpGoodsService.java
  27. 37 0
      haha-service/src/main/java/com/haha/service/ErpShopService.java
  28. 201 0
      haha-service/src/main/java/com/haha/service/impl/ErpGoodsServiceImpl.java
  29. 182 0
      haha-service/src/main/java/com/haha/service/impl/ErpShopServiceImpl.java
  30. 5 11
      haha-service/src/main/java/com/haha/service/impl/ReplenishmentOrderServiceImpl.java
  31. 1 0
      haha-service/src/main/java/com/haha/service/impl/ShopServiceImpl.java
  32. 21 3
      haha-service/src/main/java/com/haha/service/payment/config/WxPayConfig.java
  33. 6 0
      pom.xml
  34. 7 60
      qdb-sdk/pom.xml
  35. 5 0
      qdb-sdk/src/main/java/com/qdb/sdk/QdbClient.java
  36. 18 0
      qdb-sdk/src/main/java/com/qdb/sdk/api/GoodsApi.java
  37. 42 0
      qdb-sdk/src/main/java/com/qdb/sdk/api/ShopApi.java
  38. 43 0
      qdb-sdk/src/main/java/com/qdb/sdk/model/request/ShopQueryRequest.java
  39. 55 0
      qdb-sdk/src/main/java/com/qdb/sdk/model/response/ShopInfo.java
  40. 24 0
      qdb-sdk/src/main/java/com/qdb/sdk/model/response/ShopPageResult.java
  41. 24 0
      qdb-sdk/src/main/java/com/qdb/sdk/model/response/SysGoodsPageResult.java

+ 6 - 0
docs/database/add_erp_goods_id.sql

@@ -0,0 +1,6 @@
+-- t_product 增加 erp_goods_id 字段,用于与 ERP 商品建立一对一关联
+-- 创建日期: 2026-07-08
+
+ALTER TABLE t_product ADD COLUMN erp_goods_id BIGINT DEFAULT NULL COMMENT '关联的ERP商品ID(ERP内部goodsId)' AFTER code;
+
+CREATE INDEX idx_erp_goods_id ON t_product(erp_goods_id);

+ 6 - 0
docs/database/add_erp_shop_id.sql

@@ -0,0 +1,6 @@
+-- t_shop 增加 erp_shop_id 字段,用于与 ERP 店铺建立一对一绑定关系
+-- 创建日期: 2026-07-08
+
+ALTER TABLE t_shop ADD COLUMN erp_shop_id BIGINT DEFAULT NULL COMMENT '关联的ERP店铺ID(ERP内部shopId)' AFTER shop_code;
+
+CREATE INDEX idx_erp_shop_id ON t_shop(erp_shop_id);

+ 31 - 0
docs/database/create_erp_goods.sql

@@ -0,0 +1,31 @@
+-- 创建 ERP 商品信息缓存表
+-- 用于存储从企得宝 ERP 同步过来的系统商品数据
+-- 创建日期: 2026-07-08
+
+CREATE TABLE IF NOT EXISTS `t_erp_goods` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+    `erp_goods_id` BIGINT NOT NULL COMMENT 'ERP系统中的商品ID(goodsId)',
+    `goods_code` VARCHAR(100) DEFAULT NULL COMMENT '商品编码',
+    `goods_name` VARCHAR(255) DEFAULT NULL COMMENT '商品名称',
+    `short_goods_name` VARCHAR(100) DEFAULT NULL COMMENT '商品简称',
+    `specs` VARCHAR(255) DEFAULT NULL COMMENT '规格名称',
+    `sk_unit` VARCHAR(50) DEFAULT NULL COMMENT '单位名称',
+    `goods_kind_code` VARCHAR(50) DEFAULT NULL COMMENT '商品类别编码',
+    `goods_kind_name` VARCHAR(100) DEFAULT NULL COMMENT '商品类别名称',
+    `brand` VARCHAR(100) DEFAULT NULL COMMENT '品牌名称',
+    `sale_price` DECIMAL(10, 2) DEFAULT NULL COMMENT '标准售价',
+    `cost_price` DECIMAL(10, 2) DEFAULT NULL COMMENT '采购指导价',
+    `provider_name` VARCHAR(200) DEFAULT NULL COMMENT '供应商名称',
+    `batch_type` VARCHAR(10) DEFAULT 0 COMMENT '商品类型:0-普通商品;1-批次;2-唯一码',
+    `combination` VARCHAR(10) DEFAULT '0' COMMENT '组合商品:0-非组合;1-组合',
+    `article_no` VARCHAR(100) DEFAULT NULL COMMENT '商品货号',
+    `pic_path` VARCHAR(500) DEFAULT NULL COMMENT '图片路径',
+    `weight` DECIMAL(10, 2) DEFAULT NULL COMMENT '重量',
+    `status` TINYINT DEFAULT 1 COMMENT '状态:0-停用,1-启用',
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_erp_goods_id` (`erp_goods_id`),
+    KEY `idx_goods_code` (`goods_code`),
+    KEY `idx_goods_name` (`goods_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='ERP商品信息表';

+ 23 - 0
docs/database/create_erp_shop.sql

@@ -0,0 +1,23 @@
+-- 创建 ERP 店铺信息缓存表
+-- 用于存储从企得宝 ERP 同步过来的店铺数据
+-- 创建日期: 2026-07-08
+
+CREATE TABLE IF NOT EXISTS `t_erp_shop` (
+    `id` BIGINT NOT NULL COMMENT '主键ID(雪花算法)',
+    `erp_shop_id` BIGINT NOT NULL COMMENT 'ERP系统中的店铺ID(shopId)',
+    `out_shop_id` VARCHAR(64) DEFAULT NULL COMMENT '外部店铺ID',
+    `shop_name` VARCHAR(200) DEFAULT NULL COMMENT 'ERP店铺名称',
+    `shop_platform_code` VARCHAR(64) DEFAULT NULL COMMENT '平台店铺编码',
+    `nick_name` VARCHAR(200) DEFAULT NULL COMMENT '卖家账号',
+    `shop_code` VARCHAR(100) DEFAULT NULL COMMENT '店铺编码',
+    `platform_shop_id` VARCHAR(100) DEFAULT NULL COMMENT '平台店铺ID',
+    `access_token_expired_time` VARCHAR(32) DEFAULT NULL COMMENT '授权到期时间',
+    `service_expiration_time` VARCHAR(32) DEFAULT NULL COMMENT '服务到期时间',
+    `authorization_flag` VARCHAR(10) DEFAULT NULL COMMENT '授权标识:1-已授权',
+    `status` TINYINT DEFAULT 1 COMMENT '状态:0-停用,1-启用',
+    `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+    `update_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
+    PRIMARY KEY (`id`),
+    UNIQUE KEY `uk_erp_shop_id` (`erp_shop_id`),
+    KEY `idx_shop_name` (`shop_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='ERP门店信息表';

+ 10 - 0
docs/database/menu_add_erp_goods.sql

@@ -0,0 +1,10 @@
+-- 新增 ERP 商品关联菜单项
+-- 创建日期: 2026-07-08
+-- 父菜单: 商品管理 (id=400)
+-- rank=4,排在商品总库之后
+
+INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, icon, show_link, show_parent)
+VALUES (404, 400, 0, 'ERP商品关联', 'ErpGoodsList', '/product/erp-goods', '', 4, 'ri:links-line', 1, 0);
+
+-- 为超级管理员角色(role_id=1)授权
+INSERT INTO t_role_menu (id, role_id, menu_id) VALUES (90055, 1, 404);

+ 10 - 0
docs/database/menu_add_erp_shop.sql

@@ -0,0 +1,10 @@
+-- 新增 ERP 店铺管理菜单项
+-- 创建日期: 2026-07-08
+-- 父菜单: 运营管理 (id=300)
+-- rank=5,排在上货记录之后
+
+INSERT INTO t_menu (id, parent_id, menu_type, title, name, path, component, `rank`, icon, show_link, show_parent)
+VALUES (305, 300, 0, 'ERP店铺管理', 'ErpShopList', '/operation/erp-shop', '', 5, 'ri:store-2-line', 1, 0);
+
+-- 为超级管理员角色(role_id=1)授权
+INSERT INTO t_role_menu (id, role_id, menu_id) VALUES (90054, 1, 305);

+ 23 - 0
haha-admin-web/src/api/erpGoods.ts

@@ -0,0 +1,23 @@
+import { http } from "@/utils/http";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+/** 从 ERP 同步系统商品数据 */
+export const syncErpGoods = () =>
+  http.request<Result>("post", "/erp-goods/sync");
+
+/** 获取已缓存的 ERP 商品列表 */
+export const getErpGoodsList = (keyword?: string) =>
+  http.request<Result>("get", "/erp-goods", { params: { keyword } });
+
+/** 关联本地商品与 ERP 商品 */
+export const bindErpGoods = (productId: number, erpGoodsId: number) =>
+  http.request<Result>("post", "/erp-goods/bind", { data: { productId, erpGoodsId } });
+
+/** 解除本地商品与 ERP 商品的关联 */
+export const unbindErpGoods = (productId: number) =>
+  http.request<Result>("post", "/erp-goods/unbind", { data: { productId } });

+ 23 - 0
haha-admin-web/src/api/erpShop.ts

@@ -0,0 +1,23 @@
+import { http } from "@/utils/http";
+
+type Result = {
+  code: number;
+  message: string;
+  data?: any;
+};
+
+/** 从 ERP 同步店铺数据 */
+export const syncErpShops = () =>
+  http.request<Result>("post", "/erp-shops/sync");
+
+/** 获取已缓存的 ERP 店铺列表 */
+export const getErpShopList = () =>
+  http.request<Result>("get", "/erp-shops");
+
+/** 绑定门店与 ERP 店铺 */
+export const bindErpShop = (shopId: number, erpShopId: number) =>
+  http.request<Result>("post", "/erp-shops/bind", { data: { shopId, erpShopId } });
+
+/** 解除门店与 ERP 店铺的绑定 */
+export const unbindErpShop = (shopId: number) =>
+  http.request<Result>("post", "/erp-shops/unbind", { data: { shopId } });

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

@@ -43,6 +43,15 @@ export default {
         icon: "ri:user-star-line",
         title: "补货员管理"
       }
+    },
+    {
+      path: "/operation/erp-shop",
+      name: "ErpShopList",
+      component: () => import("@/views/erp-shop/index.vue"),
+      meta: {
+        icon: "ri:store-2-line",
+        title: "ERP店铺管理"
+      }
     }
   ]
 } satisfies RouteConfigsTable;

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

@@ -34,6 +34,15 @@ export default {
         icon: "ri:archive-line",
         title: "商品总库"
       }
+    },
+    {
+      path: "/product/erp-goods",
+      name: "ErpGoodsList",
+      component: () => import("@/views/erp-goods/index.vue"),
+      meta: {
+        icon: "ri:links-line",
+        title: "ERP商品关联"
+      }
     }
   ]
 } satisfies RouteConfigsTable;

+ 260 - 0
haha-admin-web/src/views/erp-goods/index.vue

@@ -0,0 +1,260 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import Refresh from "~icons/ep/refresh";
+import Link from "~icons/ep/link";
+import Remove from "~icons/ep/remove";
+import Search from "~icons/ep/search";
+import {
+  syncErpGoods,
+  getErpGoodsList,
+  bindErpGoods,
+  unbindErpGoods
+} from "@/api/erpGoods";
+import { getProductList } from "@/api/product";
+
+defineOptions({
+  name: "ErpGoodsList"
+});
+
+const loading = ref(false);
+const syncing = ref(false);
+const keyword = ref("");
+const dataList = ref<any[]>([]);
+const productOptions = ref<any[]>([]);
+const bindDialogVisible = ref(false);
+const currentErpGoods = ref<any>(null);
+const selectedProductId = ref<number | null>(null);
+const bindLoading = ref(false);
+
+async function fetchList() {
+  loading.value = true;
+  try {
+    const res = await getErpGoodsList(keyword.value || undefined);
+    if (res.code === 200) {
+      dataList.value = res.data || [];
+      // 检查各 ERP 商品的关联状态
+      await fetchBoundStatus();
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function fetchBoundStatus() {
+  try {
+    const res = await getProductList({ page: 1, pageSize: 1000 });
+    if (res.code === 200) {
+      const products = res.data?.list || [];
+      dataList.value.forEach((erp: any) => {
+        const bound = products.find((p: any) => p.erpGoodsId === erp.erpGoodsId);
+        erp._boundProduct = bound || null;
+      });
+    }
+  } catch (e) {
+    // ignore
+  }
+}
+
+async function handleSync() {
+  syncing.value = true;
+  try {
+    const res = await syncErpGoods();
+    if (res.code === 200) {
+      ElMessage.success("同步成功");
+      await fetchList();
+    } else {
+      ElMessage.error(res.message || "同步失败");
+    }
+  } finally {
+    syncing.value = false;
+  }
+}
+
+async function loadProductOptions() {
+  try {
+    const res = await getProductList({ page: 1, pageSize: 1000 });
+    if (res.code === 200) {
+      productOptions.value = res.data?.list || [];
+    }
+  } catch (e) {
+    // ignore
+  }
+}
+
+function openBindDialog(row: any) {
+  currentErpGoods.value = row;
+  selectedProductId.value = row._boundProduct?.id || null;
+  bindDialogVisible.value = true;
+  loadProductOptions();
+}
+
+async function handleBind() {
+  if (!selectedProductId.value) {
+    ElMessage.warning("请选择本地商品");
+    return;
+  }
+  bindLoading.value = true;
+  try {
+    const res = await bindErpGoods(selectedProductId.value, currentErpGoods.value.erpGoodsId);
+    if (res.code === 200) {
+      ElMessage.success("关联成功");
+      bindDialogVisible.value = false;
+      await fetchList();
+    } else {
+      ElMessage.error(res.message || "关联失败");
+    }
+  } finally {
+    bindLoading.value = false;
+  }
+}
+
+async function handleUnbind(row: any) {
+  if (!row._boundProduct) return;
+  try {
+    await ElMessageBox.confirm(
+      `确认解除本地商品「${row._boundProduct.name}」与 ERP 商品「${row.goodsName}」的关联?`,
+      "解除关联",
+      { type: "warning" }
+    );
+    const res = await unbindErpGoods(row._boundProduct.id);
+    if (res.code === 200) {
+      ElMessage.success("解除关联成功");
+      await fetchList();
+    } else {
+      ElMessage.error(res.message || "解除关联失败");
+    }
+  } catch (e) {
+    // 取消操作
+  }
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<template>
+  <div class="main">
+    <div class="px-6 pt-4 flex items-center justify-between">
+      <h2 class="text-lg font-semibold">ERP 商品关联</h2>
+      <div class="flex items-center gap-3">
+        <el-input
+          v-model="keyword"
+          placeholder="搜索商品名称或编码"
+          clearable
+          class="w-[240px]!"
+          @keyup.enter="fetchList"
+        >
+          <template #prefix>
+            <el-icon><component :is="useRenderIcon(Search)" /></el-icon>
+          </template>
+        </el-input>
+        <el-button @click="fetchList">搜索</el-button>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(Refresh)"
+          :loading="syncing"
+          @click="handleSync"
+        >
+          从 ERP 同步
+        </el-button>
+      </div>
+    </div>
+
+    <div class="px-6 pt-4">
+      <el-table
+        v-loading="loading"
+        :data="dataList"
+        border
+        stripe
+        style="width: 100%"
+        empty-text="暂无数据,请先同步ERP商品"
+      >
+        <el-table-column prop="erpGoodsId" label="ERP商品ID" width="150" />
+        <el-table-column prop="goodsCode" label="商品编码" width="140" />
+        <el-table-column prop="goodsName" label="商品名称" min-width="180" show-overflow-tooltip />
+        <el-table-column prop="specs" label="规格" width="120" show-overflow-tooltip />
+        <el-table-column prop="skUnit" label="单位" width="80" />
+        <el-table-column prop="brand" label="品牌" width="120" />
+        <el-table-column prop="salePrice" label="售价" width="100" />
+        <el-table-column label="关联本地商品" min-width="160">
+          <template #default="{ row }">
+            <el-tag v-if="row._boundProduct" type="success" size="small">
+              {{ row._boundProduct.name }}
+            </el-tag>
+            <span v-else class="text-gray-400">未关联</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="160" fixed="right">
+          <template #default="{ row }">
+            <el-button
+              v-if="!row._boundProduct"
+              link
+              type="primary"
+              :icon="useRenderIcon(Link)"
+              size="small"
+              @click="openBindDialog(row)"
+            >
+              关联
+            </el-button>
+            <el-button
+              v-else
+              link
+              type="danger"
+              :icon="useRenderIcon(Remove)"
+              size="small"
+              @click="handleUnbind(row)"
+            >
+              解除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 关联对话框 -->
+    <el-dialog
+      v-model="bindDialogVisible"
+      title="关联本地商品"
+      width="520px"
+      :close-on-click-modal="false"
+    >
+      <el-form label-width="100px">
+        <el-form-item label="ERP 商品">
+          <el-input
+            :model-value="
+              currentErpGoods?.goodsCode +
+              ' - ' +
+              currentErpGoods?.goodsName
+            "
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="选择本地商品">
+          <el-select
+            v-model="selectedProductId"
+            placeholder="请选择要关联的本地商品"
+            clearable
+            filterable
+            class="w-full!"
+          >
+            <el-option
+              v-for="p in productOptions"
+              :key="p.id"
+              :label="`[${p.code || '无编码'}] ${p.name}`"
+              :value="p.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="bindDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="bindLoading" @click="handleBind">
+          确认关联
+        </el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>

+ 238 - 0
haha-admin-web/src/views/erp-shop/index.vue

@@ -0,0 +1,238 @@
+<script setup lang="ts">
+import { ref, onMounted } from "vue";
+import { ElMessage, ElMessageBox } from "element-plus";
+import { useRenderIcon } from "@/components/ReIcon/src/hooks";
+import Refresh from "~icons/ep/refresh";
+import Link from "~icons/ep/link";
+import Remove from "~icons/ep/remove";
+import {
+  syncErpShops,
+  getErpShopList,
+  bindErpShop,
+  unbindErpShop
+} from "@/api/erpShop";
+import { getEnabledShops } from "@/api/shop";
+
+defineOptions({
+  name: "ErpShopList"
+});
+
+const loading = ref(false);
+const syncing = ref(false);
+const dataList = ref<any[]>([]);
+const shopOptions = ref<any[]>([]);
+const bindDialogVisible = ref(false);
+const currentErpShop = ref<any>(null);
+const selectedShopId = ref<number | null>(null);
+const bindLoading = ref(false);
+
+async function fetchList() {
+  loading.value = true;
+  try {
+    const res = await getErpShopList();
+    if (res.code === 200) {
+      dataList.value = res.data || [];
+      // 检查各 ERP 店铺的绑定状态
+      await fetchBoundStatus();
+    }
+  } finally {
+    loading.value = false;
+  }
+}
+
+async function fetchBoundStatus() {
+  // 获取门店列表来展示绑定关系
+  try {
+    const res = await getEnabledShops();
+    if (res.code === 200) {
+      const shops = res.data || [];
+      dataList.value.forEach((erp: any) => {
+        const bound = shops.find((s: any) => s.erpShopId === erp.erpShopId);
+        erp._boundShop = bound || null;
+      });
+    }
+  } catch (e) {
+    // ignore
+  }
+}
+
+async function handleSync() {
+  syncing.value = true;
+  try {
+    const res = await syncErpShops();
+    if (res.code === 200) {
+      ElMessage.success("同步成功");
+      await fetchList();
+    } else {
+      ElMessage.error(res.message || "同步失败");
+    }
+  } finally {
+    syncing.value = false;
+  }
+}
+
+function openBindDialog(row: any) {
+  currentErpShop.value = row;
+  selectedShopId.value = row._boundShop?.id || null;
+  bindDialogVisible.value = true;
+  // 加载门店下拉选项
+  getEnabledShops().then(res => {
+    if (res.code === 200) {
+      shopOptions.value = res.data || [];
+    }
+  });
+}
+
+async function handleBind() {
+  if (!selectedShopId.value) {
+    ElMessage.warning("请选择门店");
+    return;
+  }
+  bindLoading.value = true;
+  try {
+    const res = await bindErpShop(selectedShopId.value, currentErpShop.value.erpShopId);
+    if (res.code === 200) {
+      ElMessage.success("绑定成功");
+      bindDialogVisible.value = false;
+      await fetchList();
+    } else {
+      ElMessage.error(res.message || "绑定失败");
+    }
+  } finally {
+    bindLoading.value = false;
+  }
+}
+
+async function handleUnbind(row: any) {
+  if (!row._boundShop) return;
+  try {
+    await ElMessageBox.confirm(
+      `确认解除门店「${row._boundShop.name}」与 ERP 店铺「${row.shopName}」的绑定?`,
+      "解除绑定",
+      { type: "warning" }
+    );
+    const res = await unbindErpShop(row._boundShop.id);
+    if (res.code === 200) {
+      ElMessage.success("解绑成功");
+      await fetchList();
+    } else {
+      ElMessage.error(res.message || "解绑失败");
+    }
+  } catch (e) {
+    // 取消操作
+  }
+}
+
+onMounted(() => {
+  fetchList();
+});
+</script>
+
+<template>
+  <div class="main">
+    <div class="px-6 pt-4 flex items-center justify-between">
+      <h2 class="text-lg font-semibold">ERP 店铺管理</h2>
+      <el-button
+        type="primary"
+        :icon="useRenderIcon(Refresh)"
+        :loading="syncing"
+        @click="handleSync"
+      >
+        从 ERP 同步
+      </el-button>
+    </div>
+
+    <div class="px-6 pt-4">
+      <el-table
+        v-loading="loading"
+        :data="dataList"
+        border
+        stripe
+        style="width: 100%"
+        empty-text="暂无数据,请先同步ERP店铺"
+      >
+        <el-table-column prop="erpShopId" label="ERP店铺ID" width="160" />
+        <el-table-column prop="shopName" label="店铺名称" min-width="180" />
+        <el-table-column prop="shopCode" label="店铺编码" width="180" />
+        <el-table-column prop="nickName" label="卖家账号" width="140" />
+        <el-table-column prop="shopPlatformCode" label="平台编码" width="100" />
+        <el-table-column prop="authorizationFlag" label="授权状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="row.authorizationFlag === '1' ? 'success' : 'info'" size="small">
+              {{ row.authorizationFlag === '1' ? '已授权' : '未授权' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="绑定门店" min-width="160">
+          <template #default="{ row }">
+            <el-tag v-if="row._boundShop" type="success" size="small">
+              {{ row._boundShop.name }}
+            </el-tag>
+            <span v-else class="text-gray-400">未绑定</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="160" fixed="right">
+          <template #default="{ row }">
+            <el-button
+              v-if="!row._boundShop"
+              link
+              type="primary"
+              :icon="useRenderIcon(Link)"
+              size="small"
+              @click="openBindDialog(row)"
+            >
+              绑定
+            </el-button>
+            <el-button
+              v-else
+              link
+              type="danger"
+              :icon="useRenderIcon(Remove)"
+              size="small"
+              @click="handleUnbind(row)"
+            >
+              解绑
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </div>
+
+    <!-- 绑定对话框 -->
+    <el-dialog
+      v-model="bindDialogVisible"
+      title="绑定门店"
+      width="500px"
+      :close-on-click-modal="false"
+    >
+      <el-form label-width="100px">
+        <el-form-item label="ERP 店铺">
+          <el-input
+            :model-value="currentErpShop?.shopName + ' (' + currentErpShop?.erpShopId + ')'"
+            disabled
+          />
+        </el-form-item>
+        <el-form-item label="选择门店">
+          <el-select
+            v-model="selectedShopId"
+            placeholder="请选择要绑定的门店"
+            clearable
+            filterable
+            class="w-full!"
+          >
+            <el-option
+              v-for="s in shopOptions"
+              :key="s.id"
+              :label="s.name"
+              :value="s.id"
+            />
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="bindDialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="bindLoading" @click="handleBind">确认绑定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>

+ 7 - 0
haha-admin/pom.xml

@@ -22,6 +22,13 @@
             <artifactId>spring-boot-starter-actuator</artifactId>
         </dependency>
 
+        <!-- 企得宝 ERP SDK -->
+        <dependency>
+            <groupId>com.qdb</groupId>
+            <artifactId>qdb-sdk</artifactId>
+            <version>1.0.0</version>
+        </dependency>
+
         <!-- 依赖Service模块 -->
         <dependency>
             <groupId>com.haha</groupId>

+ 77 - 0
haha-admin/src/main/java/com/haha/admin/controller/ErpGoodsController.java

@@ -0,0 +1,77 @@
+package com.haha.admin.controller;
+
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.Result;
+import com.haha.entity.ErpGoods;
+import com.haha.service.ErpGoodsService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ERP 商品管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/erp-goods")
+@RequiredArgsConstructor
+public class ErpGoodsController {
+
+    private final ErpGoodsService erpGoodsService;
+
+    /**
+     * 从 ERP 同步系统商品数据
+     */
+    @RequirePermission("erp:goods:sync")
+    @Log(module = "ERP商品管理", operation = OperationType.SYNC, summary = "同步ERP商品数据")
+    @PostMapping("/sync")
+    public Result<Void> syncFromErp() {
+        erpGoodsService.syncFromErp();
+        return Result.success("同步成功");
+    }
+
+    /**
+     * 查询已缓存的 ERP 商品列表
+     */
+    @RequirePermission("erp:goods:read")
+    @GetMapping
+    public Result<List<ErpGoods>> list(@RequestParam(required = false) String keyword) {
+        return Result.success("查询成功", erpGoodsService.listAll(keyword));
+    }
+
+    /**
+     * 关联本地商品与 ERP 商品
+     */
+    @RequirePermission("erp:goods:bind")
+    @Log(module = "ERP商品管理", operation = OperationType.UPDATE, summary = "关联ERP商品")
+    @PostMapping("/bind")
+    public Result<Void> bind(@RequestBody Map<String, Long> params) {
+        Long productId = params.get("productId");
+        Long erpGoodsId = params.get("erpGoodsId");
+        if (productId == null || erpGoodsId == null) {
+            return Result.error("productId 和 erpGoodsId 不能为空");
+        }
+        erpGoodsService.bindGoods(productId, erpGoodsId);
+        return Result.success("关联成功");
+    }
+
+    /**
+     * 解除关联
+     */
+    @RequirePermission("erp:goods:bind")
+    @Log(module = "ERP商品管理", operation = OperationType.UPDATE, summary = "解除ERP商品关联")
+    @PostMapping("/unbind")
+    public Result<Void> unbind(@RequestBody Map<String, Long> params) {
+        Long productId = params.get("productId");
+        if (productId == null) {
+            return Result.error("productId 不能为空");
+        }
+        erpGoodsService.unbindGoods(productId);
+        return Result.success("解除关联成功");
+    }
+}

+ 77 - 0
haha-admin/src/main/java/com/haha/admin/controller/ErpShopController.java

@@ -0,0 +1,77 @@
+package com.haha.admin.controller;
+
+import com.haha.admin.annotation.RequirePermission;
+import com.haha.common.annotation.Log;
+import com.haha.common.enums.OperationType;
+import com.haha.common.vo.Result;
+import com.haha.entity.ErpShop;
+import com.haha.service.ErpShopService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * ERP 店铺管理控制器
+ */
+@Slf4j
+@RestController
+@RequestMapping("/erp-shops")
+@RequiredArgsConstructor
+public class ErpShopController {
+
+    private final ErpShopService erpShopService;
+
+    /**
+     * 从 ERP 同步店铺数据
+     */
+    @RequirePermission("erp:shop:sync")
+    @Log(module = "ERP店铺管理", operation = OperationType.SYNC, summary = "同步ERP店铺数据")
+    @PostMapping("/sync")
+    public Result<Void> syncFromErp() {
+        erpShopService.syncFromErp();
+        return Result.success("同步成功");
+    }
+
+    /**
+     * 查询已缓存的 ERP 店铺列表
+     */
+    @RequirePermission("erp:shop:read")
+    @GetMapping
+    public Result<List<ErpShop>> list() {
+        return Result.success("查询成功", erpShopService.listAll());
+    }
+
+    /**
+     * 绑定系统门店与 ERP 店铺
+     */
+    @RequirePermission("erp:shop:bind")
+    @Log(module = "ERP店铺管理", operation = OperationType.UPDATE, summary = "绑定ERP店铺")
+    @PostMapping("/bind")
+    public Result<Void> bind(@RequestBody Map<String, Long> params) {
+        Long shopId = params.get("shopId");
+        Long erpShopId = params.get("erpShopId");
+        if (shopId == null || erpShopId == null) {
+            return Result.error("shopId 和 erpShopId 不能为空");
+        }
+        erpShopService.bindShop(shopId, erpShopId);
+        return Result.success("绑定成功");
+    }
+
+    /**
+     * 解除绑定
+     */
+    @RequirePermission("erp:shop:bind")
+    @Log(module = "ERP店铺管理", operation = OperationType.UPDATE, summary = "解绑ERP店铺")
+    @PostMapping("/unbind")
+    public Result<Void> unbind(@RequestBody Map<String, Long> params) {
+        Long shopId = params.get("shopId");
+        if (shopId == null) {
+            return Result.error("shopId 不能为空");
+        }
+        erpShopService.unbindShop(shopId);
+        return Result.success("解绑成功");
+    }
+}

+ 2 - 2
haha-admin/src/main/resources/application.yml

@@ -122,8 +122,8 @@ haha:
 
 # 企得宝 ERP SDK 配置
 qdb:
-  client-id:
-  client-secret:
+  client-id: fs2009263
+  client-secret: zfmR7PEBUA3lpRVQjTn0KxlUmVnWo2OP
   api-url: https://api.7debao.com/router/api
 
 # 设备离线预警配置

+ 1 - 0
haha-common/src/main/java/com/haha/common/vo/ShopSelectVO.java

@@ -18,4 +18,5 @@ public class ShopSelectVO {
     private String address;
     private String contactName;
     private String contactPhone;
+    private Long erpShopId;
 }

+ 87 - 0
haha-entity/src/main/java/com/haha/entity/ErpGoods.java

@@ -0,0 +1,87 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 商品信息实体(从企得宝ERP同步的系统商品缓存数据)
+ */
+@Data
+@TableName("t_erp_goods")
+public class ErpGoods implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** ERP系统中的商品ID */
+    private Long erpGoodsId;
+
+    /** 商品编码 */
+    private String goodsCode;
+
+    /** 商品名称 */
+    private String goodsName;
+
+    /** 商品简称 */
+    private String shortGoodsName;
+
+    /** 规格名称 */
+    private String specs;
+
+    /** 单位名称 */
+    private String skUnit;
+
+    /** 商品类别编码 */
+    private String goodsKindCode;
+
+    /** 商品类别名称 */
+    private String goodsKindName;
+
+    /** 品牌名称 */
+    private String brand;
+
+    /** 标准售价 */
+    private BigDecimal salePrice;
+
+    /** 采购指导价 */
+    private BigDecimal costPrice;
+
+    /** 供应商名称 */
+    private String providerName;
+
+    /** 商品类型:0-普通商品;1-批次;2-唯一码 */
+    private String batchType;
+
+    /** 组合商品:0-非组合;1-组合 */
+    private String combination;
+
+    /** 商品货号 */
+    private String articleNo;
+
+    /** 图片路径 */
+    private String picPath;
+
+    /** 重量 */
+    private BigDecimal weight;
+
+    /** 状态:0-停用,1-启用 */
+    private Integer status;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+}

+ 65 - 0
haha-entity/src/main/java/com/haha/entity/ErpShop.java

@@ -0,0 +1,65 @@
+package com.haha.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * ERP 店铺信息实体(从企得宝ERP同步的店铺缓存数据)
+ */
+@Data
+@TableName("t_erp_shop")
+public class ErpShop implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.ASSIGN_ID)
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long id;
+
+    /** ERP系统中的店铺ID */
+    private Long erpShopId;
+
+    /** 外部店铺ID */
+    private String outShopId;
+
+    /** ERP店铺名称 */
+    private String shopName;
+
+    /** 平台店铺编码 */
+    private String shopPlatformCode;
+
+    /** 卖家账号 */
+    private String nickName;
+
+    /** 店铺编码 */
+    private String shopCode;
+
+    /** 平台店铺ID */
+    private String platformShopId;
+
+    /** 授权到期时间 */
+    private String accessTokenExpiredTime;
+
+    /** 服务到期时间 */
+    private String serviceExpirationTime;
+
+    /** 授权标识:1-已授权 */
+    private String authorizationFlag;
+
+    /** 状态:0-停用,1-启用 */
+    private Integer status;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime createTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai")
+    private LocalDateTime updateTime;
+}

+ 9 - 0
haha-entity/src/main/java/com/haha/entity/Product.java

@@ -36,6 +36,12 @@ public class Product implements Serializable {
      */
     private String code;
 
+    /**
+     * 关联的ERP商品ID(ERP内部goodsId)
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long erpGoodsId;
+
     /**
      * 商品条码
      */
@@ -146,4 +152,7 @@ public class Product implements Serializable {
 
     @TableField(exist = false)
     private LocalDateTime lastSyncTime;
+
+    @TableField(exist = false)
+    private String erpGoodsName;
 }

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

@@ -31,6 +31,12 @@ public class Shop implements Serializable {
      */
     private String shopCode;
 
+    /**
+     * 关联的ERP店铺ID(ERP内部shopId)
+     */
+    @JsonSerialize(using = ToStringSerializer.class)
+    private Long erpShopId;
+
     /**
      * 门店名称
      */
@@ -128,4 +134,10 @@ public class Shop implements Serializable {
      */
     @TableField(exist = false)
     private String statusColor;
+
+    /**
+     * 已绑定的ERP店铺名称
+     */
+    @TableField(exist = false)
+    private String erpShopName;
 }

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/ErpGoodsMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ErpGoods;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * ERP 商品信息 Mapper
+ */
+@Mapper
+public interface ErpGoodsMapper extends BaseMapper<ErpGoods> {
+}

+ 12 - 0
haha-mapper/src/main/java/com/haha/mapper/ErpShopMapper.java

@@ -0,0 +1,12 @@
+package com.haha.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.haha.entity.ErpShop;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * ERP 店铺信息 Mapper
+ */
+@Mapper
+public interface ErpShopMapper extends BaseMapper<ErpShop> {
+}

+ 1 - 1
haha-mapper/src/main/java/com/haha/mapper/ShopMapper.java

@@ -43,7 +43,7 @@ public interface ShopMapper extends BaseMapper<Shop> {
     /**
      * 查询所有启用的门店(用于下拉选择)
      */
-    @Select("SELECT id, name, address, contact_name, contact_phone " +
+    @Select("SELECT id, name, address, contact_name, contact_phone, erp_shop_id " +
             "FROM t_shop WHERE status = 1 ORDER BY name")
     List<Map<String, Object>> selectAllEnabledShops();
 }

+ 2 - 5
haha-service/pom.xml

@@ -34,14 +34,11 @@
             <artifactId>haha-sdk</artifactId>
         </dependency>
 
-        <!-- TODO: 企得宝 ERP SDK - 暂未对接,后续接入时取消注释 -->
-        <!--
+        <!-- 企得宝 ERP SDK -->
         <dependency>
-            <groupId>com.qdb</groupId>
+            <groupId>com.haha</groupId>
             <artifactId>qdb-sdk</artifactId>
-            <version>1.0.0</version>
         </dependency>
-        -->
 
         <!-- Spring Boot Web - provided scope -->
         <dependency>

+ 39 - 0
haha-service/src/main/java/com/haha/service/ErpGoodsService.java

@@ -0,0 +1,39 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.ErpGoods;
+
+import java.util.List;
+
+/**
+ * ERP 商品信息服务接口
+ */
+public interface ErpGoodsService extends IService<ErpGoods> {
+
+    /**
+     * 从 ERP 同步系统商品数据到本地缓存
+     */
+    void syncFromErp();
+
+    /**
+     * 查询已缓存的 ERP 商品列表
+     *
+     * @param keyword 搜索关键词(商品名称/编码,可选)
+     */
+    List<ErpGoods> listAll(String keyword);
+
+    /**
+     * 将本地商品与 ERP 商品一对一关联
+     *
+     * @param productId  本地商品ID
+     * @param erpGoodsId ERP商品主键ID(ERP内部goodsId)
+     */
+    void bindGoods(Long productId, Long erpGoodsId);
+
+    /**
+     * 解除本地商品与 ERP 商品的关联
+     *
+     * @param productId 本地商品ID
+     */
+    void unbindGoods(Long productId);
+}

+ 37 - 0
haha-service/src/main/java/com/haha/service/ErpShopService.java

@@ -0,0 +1,37 @@
+package com.haha.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.haha.entity.ErpShop;
+
+import java.util.List;
+
+/**
+ * ERP 店铺信息服务接口
+ */
+public interface ErpShopService extends IService<ErpShop> {
+
+    /**
+     * 从 ERP 同步店铺数据到本地缓存
+     */
+    void syncFromErp();
+
+    /**
+     * 查询所有已缓存的 ERP 店铺
+     */
+    List<ErpShop> listAll();
+
+    /**
+     * 将系统门店与 ERP 店铺一对一绑定
+     *
+     * @param shopId    系统门店ID
+     * @param erpShopId ERP店铺主键ID(ERP内部shopId)
+     */
+    void bindShop(Long shopId, Long erpShopId);
+
+    /**
+     * 解除系统门店与 ERP 店铺的绑定
+     *
+     * @param shopId 系统门店ID
+     */
+    void unbindShop(Long shopId);
+}

+ 201 - 0
haha-service/src/main/java/com/haha/service/impl/ErpGoodsServiceImpl.java

@@ -0,0 +1,201 @@
+package com.haha.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.ErpGoods;
+import com.haha.entity.Product;
+import com.haha.mapper.ErpGoodsMapper;
+import com.haha.mapper.ProductMapper;
+import com.haha.service.ErpGoodsService;
+import com.qdb.sdk.QdbClient;
+import com.qdb.sdk.QdbException;
+import com.qdb.sdk.model.request.SysGoodsQueryRequest;
+import com.qdb.sdk.model.response.QdbResponse;
+import com.qdb.sdk.model.response.SysGoodsPageResult;
+import com.qdb.sdk.model.response.SysGoodsVO;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * ERP 商品信息服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ErpGoodsServiceImpl extends ServiceImpl<ErpGoodsMapper, ErpGoods> implements ErpGoodsService {
+
+    private final ProductMapper productMapper;
+
+    @Autowired(required = false)
+    private QdbClient qdbClient;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncFromErp() {
+        if (qdbClient == null) {
+            throw new BusinessException(500, "QdbClient 未配置,无法同步ERP商品数据");
+        }
+
+        int pageNo = 1;
+        int pageSize = 50;
+        boolean hasMore = true;
+        int syncedCount = 0;
+
+        while (hasMore) {
+            SysGoodsQueryRequest request = SysGoodsQueryRequest.builder()
+                    .pageIndex(pageNo)
+                    .pageSize(pageSize)
+                    .status(1)
+                    .build();
+
+            try {
+                QdbResponse<SysGoodsPageResult> response = qdbClient.getGoodsApi().listSysGoodsPaged(request);
+                SysGoodsPageResult pageResult = response.getData();
+
+                if (pageResult == null || pageResult.getData() == null || pageResult.getData().isEmpty()) {
+                    hasMore = false;
+                    break;
+                }
+
+                for (SysGoodsVO goods : pageResult.getData()) {
+                    upsertErpGoods(goods);
+                    syncedCount++;
+                }
+
+                Integer total = pageResult.getTotal();
+                if (total != null && pageNo * pageSize >= total) {
+                    hasMore = false;
+                } else if (pageResult.getData().size() < pageSize) {
+                    hasMore = false;
+                } else {
+                    pageNo++;
+                }
+
+            } catch (QdbException e) {
+                log.error("同步ERP商品失败: pageNo={}, error={}", pageNo, e.getMessage());
+                throw new BusinessException(500, "ERP商品同步失败: " + e.getMessage());
+            }
+        }
+
+        log.info("ERP商品同步完成: 共同步 {} 条", syncedCount);
+    }
+
+    @Override
+    public List<ErpGoods> listAll(String keyword) {
+        LambdaQueryWrapper<ErpGoods> wrapper = new LambdaQueryWrapper<ErpGoods>()
+                .eq(ErpGoods::getStatus, 1);
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w
+                    .like(ErpGoods::getGoodsName, keyword)
+                    .or()
+                    .like(ErpGoods::getGoodsCode, keyword));
+        }
+        return list(wrapper.orderByAsc(ErpGoods::getGoodsName));
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void bindGoods(Long productId, Long erpGoodsId) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) {
+            throw new BusinessException(404, "商品不存在");
+        }
+
+        // 检查 ERP 商品是否存在
+        ErpGoods erpGoods = getOne(new LambdaQueryWrapper<ErpGoods>()
+                .eq(ErpGoods::getErpGoodsId, erpGoodsId));
+        if (erpGoods == null) {
+            throw new BusinessException(404, "ERP商品不存在,请先同步ERP商品数据");
+        }
+
+        // 检查是否已被其他本地商品绑定
+        List<Product> boundProducts = productMapper.selectList(new LambdaQueryWrapper<Product>()
+                .eq(Product::getErpGoodsId, erpGoodsId)
+                .eq(Product::getIsDeleted, 0));
+        if (!boundProducts.isEmpty() && !boundProducts.get(0).getId().equals(productId)) {
+            throw new BusinessException(400, "该ERP商品已关联到本地商品: " + boundProducts.get(0).getName());
+        }
+
+        product.setErpGoodsId(erpGoodsId);
+        product.setUpdateTime(LocalDateTime.now());
+        productMapper.updateById(product);
+
+        log.info("商品关联ERP商品: productId={}, productName={}, erpGoodsId={}, erpGoodsName={}",
+                productId, product.getName(), erpGoodsId, erpGoods.getGoodsName());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void unbindGoods(Long productId) {
+        Product product = productMapper.selectById(productId);
+        if (product == null) {
+            throw new BusinessException(404, "商品不存在");
+        }
+
+        product.setErpGoodsId(null);
+        product.setUpdateTime(LocalDateTime.now());
+        productMapper.updateById(product);
+
+        log.info("商品解除ERP商品关联: productId={}, productName={}", productId, product.getName());
+    }
+
+    /**
+     * 插入或更新 ERP 商品记录(按 erp_goods_id 判断)
+     */
+    private void upsertErpGoods(SysGoodsVO goods) {
+        ErpGoods existing = getOne(new LambdaQueryWrapper<ErpGoods>()
+                .eq(ErpGoods::getErpGoodsId, goods.getGoodsId()));
+
+        if (existing != null) {
+            existing.setGoodsCode(goods.getGoodsCode());
+            existing.setGoodsName(goods.getGoodsName());
+            existing.setShortGoodsName(goods.getShortGoodsName());
+            existing.setSpecs(goods.getSpecs());
+            existing.setSkUnit(goods.getSkUnit());
+            existing.setGoodsKindCode(goods.getGoodsKindCode());
+            existing.setGoodsKindName(goods.getGoodsKindName());
+            existing.setBrand(goods.getBrand());
+            existing.setSalePrice(goods.getSalePrice());
+            existing.setCostPrice(goods.getCostPrice());
+            existing.setProviderName(goods.getProviderName());
+            existing.setBatchType(goods.getBatchType());
+            existing.setCombination(goods.getCombination());
+            existing.setArticleNo(goods.getArticleNo());
+            existing.setPicPath(goods.getPicPath());
+            existing.setWeight(goods.getWeight());
+            existing.setUpdateTime(LocalDateTime.now());
+            updateById(existing);
+        } else {
+            ErpGoods erpGoods = new ErpGoods();
+            erpGoods.setErpGoodsId(goods.getGoodsId());
+            erpGoods.setGoodsCode(goods.getGoodsCode());
+            erpGoods.setGoodsName(goods.getGoodsName());
+            erpGoods.setShortGoodsName(goods.getShortGoodsName());
+            erpGoods.setSpecs(goods.getSpecs());
+            erpGoods.setSkUnit(goods.getSkUnit());
+            erpGoods.setGoodsKindCode(goods.getGoodsKindCode());
+            erpGoods.setGoodsKindName(goods.getGoodsKindName());
+            erpGoods.setBrand(goods.getBrand());
+            erpGoods.setSalePrice(goods.getSalePrice());
+            erpGoods.setCostPrice(goods.getCostPrice());
+            erpGoods.setProviderName(goods.getProviderName());
+            erpGoods.setBatchType(goods.getBatchType());
+            erpGoods.setCombination(goods.getCombination());
+            erpGoods.setArticleNo(goods.getArticleNo());
+            erpGoods.setPicPath(goods.getPicPath());
+            erpGoods.setWeight(goods.getWeight());
+            erpGoods.setStatus(1);
+            erpGoods.setCreateTime(LocalDateTime.now());
+            erpGoods.setUpdateTime(LocalDateTime.now());
+            save(erpGoods);
+        }
+    }
+}

+ 182 - 0
haha-service/src/main/java/com/haha/service/impl/ErpShopServiceImpl.java

@@ -0,0 +1,182 @@
+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.extension.service.impl.ServiceImpl;
+import com.haha.common.exception.BusinessException;
+import com.haha.entity.ErpShop;
+import com.haha.entity.Shop;
+import com.haha.mapper.ErpShopMapper;
+import com.haha.mapper.ShopMapper;
+import com.haha.service.ErpShopService;
+import com.qdb.sdk.QdbClient;
+import com.qdb.sdk.QdbException;
+import com.qdb.sdk.model.request.ShopQueryRequest;
+import com.qdb.sdk.model.response.QdbResponse;
+import com.qdb.sdk.model.response.ShopInfo;
+import com.qdb.sdk.model.response.ShopPageResult;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * ERP 店铺信息服务实现
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ErpShopServiceImpl extends ServiceImpl<ErpShopMapper, ErpShop> implements ErpShopService {
+
+    private final ShopMapper shopMapper;
+
+    @Autowired(required = false)
+    private QdbClient qdbClient;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void syncFromErp() {
+        if (qdbClient == null) {
+            throw new BusinessException(500, "QdbClient 未配置,无法同步ERP店铺数据");
+        }
+
+        int pageNo = 1;
+        int pageSize = 50;
+        boolean hasMore = true;
+        int syncedCount = 0;
+
+        while (hasMore) {
+            ShopQueryRequest request = ShopQueryRequest.builder()
+                    .pageIndex(pageNo)
+                    .pageSize(pageSize)
+                    .build();
+
+            try {
+                QdbResponse<ShopPageResult> response = qdbClient.getShopApi().queryShopInfo(request);
+                ShopPageResult pageResult = response.getData();
+
+                if (pageResult == null || pageResult.getData() == null || pageResult.getData().isEmpty()) {
+                    hasMore = false;
+                    break;
+                }
+
+                for (ShopInfo shopInfo : pageResult.getData()) {
+                    upsertErpShop(shopInfo);
+                    syncedCount++;
+                }
+
+                // 判断是否还有下一页
+                Integer total = pageResult.getTotal();
+                if (total != null && pageNo * pageSize >= total) {
+                    hasMore = false;
+                } else if (pageResult.getData().size() < pageSize) {
+                    hasMore = false;
+                } else {
+                    pageNo++;
+                }
+
+            } catch (QdbException e) {
+                log.error("同步ERP店铺失败: pageNo={}, error={}", pageNo, e.getMessage());
+                throw new BusinessException(500, "ERP店铺同步失败: " + e.getMessage());
+            }
+        }
+
+        log.info("ERP店铺同步完成: 共同步 {} 条", syncedCount);
+    }
+
+    @Override
+    public List<ErpShop> listAll() {
+        return lambdaQuery()
+                .eq(ErpShop::getStatus, 1)
+                .orderByAsc(ErpShop::getShopName)
+                .list();
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void bindShop(Long shopId, Long erpShopId) {
+        Shop shop = shopMapper.selectById(shopId);
+        if (shop == null) {
+            throw new BusinessException(404, "门店不存在");
+        }
+
+        // 检查 ERP 店铺是否存在
+        ErpShop erpShop = getOne(new LambdaQueryWrapper<ErpShop>()
+                .eq(ErpShop::getErpShopId, erpShopId));
+        if (erpShop == null) {
+            throw new BusinessException(404, "ERP店铺不存在,请先同步ERP店铺数据");
+        }
+
+        // 检查是否已被其他门店绑定
+        List<Shop> boundShops = shopMapper.selectList(new LambdaQueryWrapper<Shop>()
+                .eq(Shop::getErpShopId, erpShopId));
+        if (!boundShops.isEmpty() && !boundShops.get(0).getId().equals(shopId)) {
+            throw new BusinessException(400, "该ERP店铺已绑定到门店: " + boundShops.get(0).getName());
+        }
+
+        shop.setErpShopId(erpShopId);
+        shop.setUpdateTime(LocalDateTime.now());
+        shopMapper.updateById(shop);
+
+        log.info("门店绑定ERP店铺: shopId={}, shopName={}, erpShopId={}, erpShopName={}",
+                shopId, shop.getName(), erpShopId, erpShop.getShopName());
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void unbindShop(Long shopId) {
+        Shop shop = shopMapper.selectById(shopId);
+        if (shop == null) {
+            throw new BusinessException(404, "门店不存在");
+        }
+
+        shop.setErpShopId(null);
+        shop.setUpdateTime(LocalDateTime.now());
+        shopMapper.updateById(shop);
+
+        log.info("门店解绑ERP店铺: shopId={}, shopName={}", shopId, shop.getName());
+    }
+
+    /**
+     * 插入或更新 ERP 店铺记录(按 erp_shop_id 判断)
+     */
+    private void upsertErpShop(ShopInfo info) {
+        ErpShop existing = getOne(new LambdaQueryWrapper<ErpShop>()
+                .eq(ErpShop::getErpShopId, info.getShopId()));
+
+        if (existing != null) {
+            existing.setOutShopId(info.getOutShopId());
+            existing.setShopName(info.getShopName());
+            existing.setShopPlatformCode(info.getShopPlatformCode());
+            existing.setNickName(info.getNickName());
+            existing.setShopCode(info.getShopCode());
+            existing.setPlatformShopId(info.getPlatformShopId());
+            existing.setAccessTokenExpiredTime(info.getAccessTokenExpiredTime());
+            existing.setServiceExpirationTime(info.getServiceExpirationTime());
+            existing.setAuthorizationFlag(info.getAuthorizationFlag());
+            existing.setStatus(info.getStatus() != null ? Integer.parseInt(info.getStatus()) : 1);
+            existing.setUpdateTime(LocalDateTime.now());
+            updateById(existing);
+        } else {
+            ErpShop erpShop = new ErpShop();
+            erpShop.setErpShopId(info.getShopId());
+            erpShop.setOutShopId(info.getOutShopId());
+            erpShop.setShopName(info.getShopName());
+            erpShop.setShopPlatformCode(info.getShopPlatformCode());
+            erpShop.setNickName(info.getNickName());
+            erpShop.setShopCode(info.getShopCode());
+            erpShop.setPlatformShopId(info.getPlatformShopId());
+            erpShop.setAccessTokenExpiredTime(info.getAccessTokenExpiredTime());
+            erpShop.setServiceExpirationTime(info.getServiceExpirationTime());
+            erpShop.setAuthorizationFlag(info.getAuthorizationFlag());
+            erpShop.setStatus(info.getStatus() != null ? Integer.parseInt(info.getStatus()) : 1);
+            erpShop.setCreateTime(LocalDateTime.now());
+            erpShop.setUpdateTime(LocalDateTime.now());
+            save(erpShop);
+        }
+    }
+}

+ 5 - 11
haha-service/src/main/java/com/haha/service/impl/ReplenishmentOrderServiceImpl.java

@@ -20,11 +20,12 @@ import com.haha.mapper.*;
 import com.haha.service.ReplenishmentOrderService;
 // TODO: 企得宝 ERP SDK - 暂未对接,后续接入时取消注释
 // import com.qdb.sdk.QdbClient;
+// import com.qdb.sdk.QdbException;
 // import com.qdb.sdk.model.request.OrderGoods;
+// import com.qdb.sdk.model.request.OrderPay;
 // import com.qdb.sdk.model.request.OrderSaveRequest;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
@@ -53,7 +54,7 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
     private final DeviceInventoryMapper deviceInventoryMapper;
     private final ProductMapper productMapper;
 
-    private ObjectMapper objectMapper = new ObjectMapper();
+    private final ObjectMapper objectMapper = new ObjectMapper();
 
     // TODO: 企得宝 ERP SDK - 暂未对接,后续接入时取消注释
     // @Autowired(required = false)
@@ -318,19 +319,12 @@ public class ReplenishmentOrderServiceImpl extends ServiceImpl<ReplenishmentOrde
     @Override
     @Transactional(rollbackFor = Exception.class)
     public ReplenishmentOrder syncToErp(Long id) {
-        // TODO: 企得宝 ERP SDK - 暂未对接,后续接入时实现真实ERP推送
-        log.warn("syncToErp ERP对接尚未实现,仅更新状态: id={}", id);
+        // TODO: 企得宝 ERP SDK - 暂未对接,后续接入时实现
+        log.warn("syncToErp 暂未实现,企得宝 ERP SDK 尚未接入: id={}", id);
         ReplenishmentOrder order = getById(id);
         if (order == null) {
             throw new BusinessException(404, "补货单不存在");
         }
-        if (order.getStatus() != ReplenishmentOrderStatusEnum.SUBMITTED.getCode()) {
-            throw new BusinessException(400, "仅已提交状态的补货单可同步ERP");
-        }
-        order.setStatus(ReplenishmentOrderStatusEnum.SYNCED_TO_ERP.getCode());
-        order.setUpdateTime(LocalDateTime.now());
-        updateById(order);
-        log.info("补货单已同步ERP(状态变更): id={}, orderNo={}", id, order.getOrderNo());
         fillStatusLabel(order);
         return order;
     }

+ 1 - 0
haha-service/src/main/java/com/haha/service/impl/ShopServiceImpl.java

@@ -149,6 +149,7 @@ public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements Sh
                 .address((String) m.get("address"))
                 .contactName((String) m.get("contact_name"))
                 .contactPhone((String) m.get("contact_phone"))
+                .erpShopId((Long) m.get("erp_shop_id"))
                 .build()
         ).toList();
     }

+ 21 - 3
haha-service/src/main/java/com/haha/service/payment/config/WxPayConfig.java

@@ -20,12 +20,14 @@ import jakarta.annotation.PostConstruct;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import java.util.Objects;
 
 /**
  * 微信支付 V3 SDK 配置
  * 
  * 通过 wechat.pay.mch-id 配置项控制是否启用
- * 创建 RSAAutoCertificateConfig(自动管理平台证书)和核心服务 Bean
+ * 创建 RSAPublicKeyConfig(公钥模式)和核心服务 Bean
+ * 初始化失败时不会阻塞应用启动,仅日志警告
  */
 @Slf4j
 @Data
@@ -95,8 +97,8 @@ public class WxPayConfig {
             return config;
         } catch (Exception e) {
             log.error("微信支付 V3 SDK 配置初始化失败: {}", e.getMessage());
-            log.warn("微信支付功能将不可用,请检查配置参数和证书文件");
-            throw new IllegalStateException("微信支付配置初始化失败: " + e.getMessage(), e);
+            log.warn("微信支付功能将不可用(应用将继续启动),请检查配置参数和证书文件");
+            return null;
         }
     }
     
@@ -105,6 +107,10 @@ public class WxPayConfig {
      */
     @Bean
     public JsapiServiceExtension jsapiServiceExtension(Config wxPaySdkConfig) {
+        if (wxPaySdkConfig == null) {
+            log.warn("wxPaySdkConfig 未初始化,JsapiServiceExtension 跳过创建");
+            return null;
+        }
         return new JsapiServiceExtension.Builder().config(wxPaySdkConfig).build();
     }
     
@@ -113,6 +119,10 @@ public class WxPayConfig {
      */
     @Bean
     public RefundService wxRefundService(Config wxPaySdkConfig) {
+        if (wxPaySdkConfig == null) {
+            log.warn("wxPaySdkConfig 未初始化,RefundService 跳过创建");
+            return null;
+        }
         return new RefundService.Builder().config(wxPaySdkConfig).build();
     }
     
@@ -121,6 +131,10 @@ public class WxPayConfig {
      */
     @Bean
     public NotificationParser wxNotificationParser(Config wxPaySdkConfig) {
+        if (wxPaySdkConfig == null) {
+            log.warn("wxPaySdkConfig 未初始化,NotificationParser 跳过创建");
+            return null;
+        }
         return new NotificationParser((NotificationConfig) wxPaySdkConfig);
     }
     
@@ -163,6 +177,10 @@ public class WxPayConfig {
      */
     @Bean
     public com.wechat.pay.java.core.http.HttpClient wxPayHttpClient(Config wxPaySdkConfig) {
+        if (wxPaySdkConfig == null) {
+            log.warn("wxPaySdkConfig 未初始化,HttpClient 跳过创建");
+            return null;
+        }
         return new com.wechat.pay.java.core.http.DefaultHttpClientBuilder()
                 .config(wxPaySdkConfig)
                 .build();

+ 6 - 0
pom.xml

@@ -19,6 +19,7 @@
         <module>haha-miniapp</module>
         <module>haha-admin</module>
         <module>haha-sdk</module>
+        <module>qdb-sdk</module>
     </modules>
 
     <properties>
@@ -129,6 +130,11 @@
                 <artifactId>haha-sdk</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>com.haha</groupId>
+                <artifactId>qdb-sdk</artifactId>
+                <version>${project.version}</version>
+            </dependency>
 
             <!-- 微信支付 V3 官方 SDK -->
             <dependency>

+ 7 - 60
qdb-sdk/pom.xml

@@ -4,26 +4,19 @@
          xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
     <modelVersion>4.0.0</modelVersion>
 
-    <groupId>com.qdb</groupId>
+    <parent>
+        <groupId>com.haha</groupId>
+        <artifactId>haha-parent</artifactId>
+        <version>0.0.1-SNAPSHOT</version>
+        <relativePath>../pom.xml</relativePath>
+    </parent>
+
     <artifactId>qdb-sdk</artifactId>
-    <version>1.0.0</version>
     <packaging>jar</packaging>
 
     <name>qdb-sdk</name>
     <description>企得宝ERP开放平台 Java SDK</description>
 
-    <properties>
-        <java.version>21</java.version>
-        <maven.compiler.source>${java.version}</maven.compiler.source>
-        <maven.compiler.target>${java.version}</maven.compiler.target>
-        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
-        <okhttp.version>4.12.0</okhttp.version>
-        <fastjson2.version>2.0.53</fastjson2.version>
-        <lombok.version>1.18.36</lombok.version>
-        <slf4j.version>2.0.13</slf4j.version>
-        <junit.version>5.11.0</junit.version>
-    </properties>
-
     <dependencies>
         <!-- HTTP Client -->
         <dependency>
@@ -36,59 +29,13 @@
         <dependency>
             <groupId>com.alibaba.fastjson2</groupId>
             <artifactId>fastjson2</artifactId>
-            <version>${fastjson2.version}</version>
-        </dependency>
-
-        <!-- Lombok -->
-        <dependency>
-            <groupId>org.projectlombok</groupId>
-            <artifactId>lombok</artifactId>
-            <version>${lombok.version}</version>
-            <scope>provided</scope>
         </dependency>
 
         <!-- Logging -->
         <dependency>
             <groupId>org.slf4j</groupId>
             <artifactId>slf4j-api</artifactId>
-            <version>${slf4j.version}</version>
-        </dependency>
-        <dependency>
-            <groupId>org.slf4j</groupId>
-            <artifactId>slf4j-simple</artifactId>
-            <version>${slf4j.version}</version>
-            <scope>test</scope>
-        </dependency>
-
-        <!-- Test -->
-        <dependency>
-            <groupId>org.junit.jupiter</groupId>
-            <artifactId>junit-jupiter</artifactId>
-            <version>${junit.version}</version>
-            <scope>test</scope>
         </dependency>
     </dependencies>
 
-    <build>
-        <plugins>
-            <plugin>
-                <groupId>org.apache.maven.plugins</groupId>
-                <artifactId>maven-compiler-plugin</artifactId>
-                <version>3.11.0</version>
-                <configuration>
-                    <source>${java.version}</source>
-                    <target>${java.version}</target>
-                    <encoding>${project.build.sourceEncoding}</encoding>
-                    <parameters>true</parameters>
-                    <annotationProcessorPaths>
-                        <path>
-                            <groupId>org.projectlombok</groupId>
-                            <artifactId>lombok</artifactId>
-                            <version>${lombok.version}</version>
-                        </path>
-                    </annotationProcessorPaths>
-                </configuration>
-            </plugin>
-        </plugins>
-    </build>
 </project>

+ 5 - 0
qdb-sdk/src/main/java/com/qdb/sdk/QdbClient.java

@@ -3,6 +3,7 @@ package com.qdb.sdk;
 import com.qdb.sdk.api.GoodsApi;
 import com.qdb.sdk.api.OrderApi;
 import com.qdb.sdk.api.PurchaseApi;
+import com.qdb.sdk.api.ShopApi;
 import com.qdb.sdk.util.HttpUtil;
 import lombok.Getter;
 import lombok.extern.slf4j.Slf4j;
@@ -70,6 +71,9 @@ public class QdbClient {
     /** 商品管理 API */
     private final GoodsApi goodsApi;
 
+    /** 基础资料 - 店铺管理 API */
+    private final ShopApi shopApi;
+
     /**
      * 构造方法
      *
@@ -86,6 +90,7 @@ public class QdbClient {
         this.orderApi = new OrderApi(this);
         this.purchaseApi = new PurchaseApi(this);
         this.goodsApi = new GoodsApi(this);
+        this.shopApi = new ShopApi(this);
 
         log.info("企得宝ERP SDK 初始化成功, clientId: {}", config.getClientId());
     }

+ 18 - 0
qdb-sdk/src/main/java/com/qdb/sdk/api/GoodsApi.java

@@ -88,6 +88,8 @@ public class GoodsApi extends BaseQdbApi {
      * 系统商品查询
      * <p>
      * 查询系统商品信息(即【商品信息】、【组合商品】页面数据)。
+     * 注意:此方法解析 data 为扁平列表,不适用于分页场景。
+     * 分页场景请使用 {@link #listSysGoodsPaged(SysGoodsQueryRequest)}。
      * </p>
      *
      * @param request 查询请求参数(支持多种筛选条件)
@@ -99,6 +101,22 @@ public class GoodsApi extends BaseQdbApi {
                 new TypeReference<List<SysGoodsVO>>() {});
     }
 
+    /**
+     * 系统商品查询(带分页信息)
+     * <p>
+     * 查询系统商品信息,返回包含 total 和 data 的分页结果。
+     * 适用于需要分页遍历所有系统商品的场景(如同步到本地缓存)。
+     * </p>
+     *
+     * @param request 查询请求参数
+     * @return 响应对象,data 为包含 total 和商品列表的分页结果
+     * @throws QdbException 调用失败时抛出
+     */
+    public QdbResponse<SysGoodsPageResult> listSysGoodsPaged(SysGoodsQueryRequest request) throws QdbException {
+        return execute(METHOD_SYS_GOODS_LIST, request,
+                new TypeReference<SysGoodsPageResult>() {});
+    }
+
     /**
      * 批量新增店铺商品
      * <p>

+ 42 - 0
qdb-sdk/src/main/java/com/qdb/sdk/api/ShopApi.java

@@ -0,0 +1,42 @@
+package com.qdb.sdk.api;
+
+import com.alibaba.fastjson2.TypeReference;
+import com.qdb.sdk.QdbClient;
+import com.qdb.sdk.QdbException;
+import com.qdb.sdk.model.request.ShopQueryRequest;
+import com.qdb.sdk.model.response.QdbResponse;
+import com.qdb.sdk.model.response.ShopPageResult;
+
+/**
+ * 基础资料 - 店铺管理 API
+ * <p>
+ * 提供店铺查询等接口的调用封装。
+ * 对应文档:01-基础资料.md
+ * </p>
+ *
+ * @author qdb-sdk
+ * @version 1.0.0
+ */
+public class ShopApi extends BaseQdbApi {
+
+    /** 店铺查询 */
+    private static final String METHOD_SHOP_INFO = "foonsu.erp.shop.shopInfo";
+
+    public ShopApi(QdbClient client) {
+        super(client);
+    }
+
+    /**
+     * 查询店铺信息
+     * <p>
+     * 返回 ERP 系统中的店铺列表,支持分页和多种筛选条件。
+     * </p>
+     *
+     * @param request 查询请求参数
+     * @return 包含分页结果的响应对象
+     * @throws QdbException 调用失败时抛出
+     */
+    public QdbResponse<ShopPageResult> queryShopInfo(ShopQueryRequest request) throws QdbException {
+        return execute(METHOD_SHOP_INFO, request, new TypeReference<ShopPageResult>() {});
+    }
+}

+ 43 - 0
qdb-sdk/src/main/java/com/qdb/sdk/model/request/ShopQueryRequest.java

@@ -0,0 +1,43 @@
+package com.qdb.sdk.model.request;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+/**
+ * 店铺查询请求参数
+ * <p>
+ * 对应 Method: {@code foonsu.erp.shop.shopInfo}
+ * </p>
+ *
+ * @author qdb-sdk
+ * @version 1.0.0
+ */
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ShopQueryRequest {
+
+    /** 店铺ID */
+    private Long shopId;
+
+    /** 外部店铺ID集合,逗号分隔 */
+    private String outShopIds;
+
+    /** 页码,默认 1 */
+    private Integer pageIndex;
+
+    /** 每页大小,默认 20,最大不超过 50 */
+    private Integer pageSize;
+
+    /** 修改时间开始 yyyy-MM-dd HH:mm:ss */
+    private String modifyDateStart;
+
+    /** 修改时间结束 yyyy-MM-dd HH:mm:ss */
+    private String modifyDateEnd;
+
+    /** 平台店铺ID */
+    private String platformShopId;
+}

+ 55 - 0
qdb-sdk/src/main/java/com/qdb/sdk/model/response/ShopInfo.java

@@ -0,0 +1,55 @@
+package com.qdb.sdk.model.response;
+
+import lombok.Data;
+
+/**
+ * ERP 店铺信息
+ * <p>
+ * 对应店铺查询接口 {@code foonsu.erp.shop.shopInfo} 返回的 data 列表元素。
+ * </p>
+ *
+ * @author qdb-sdk
+ * @version 1.0.0
+ */
+@Data
+public class ShopInfo {
+
+    /** 店铺ID(ERP内部主键) */
+    private Long shopId;
+
+    /** 外部店铺ID */
+    private String outShopId;
+
+    /** 店铺名称 */
+    private String shopName;
+
+    /** 店铺平台编号 */
+    private String shopPlatformCode;
+
+    /** 卖家账号 */
+    private String nickName;
+
+    /** 授权到期时间 */
+    private String accessTokenExpiredTime;
+
+    /** 创建时间 */
+    private String createDate;
+
+    /** 状态:0-停用 1-启用 */
+    private String status;
+
+    /** 修改时间 */
+    private String modifyDate;
+
+    /** 店铺编码 */
+    private String shopCode;
+
+    /** 平台店铺ID */
+    private String platformShopId;
+
+    /** 授权标识:1-已授权 */
+    private String authorizationFlag;
+
+    /** 服务到期时间 */
+    private String serviceExpirationTime;
+}

+ 24 - 0
qdb-sdk/src/main/java/com/qdb/sdk/model/response/ShopPageResult.java

@@ -0,0 +1,24 @@
+package com.qdb.sdk.model.response;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 店铺查询分页结果
+ * <p>
+ * 对应店铺查询接口返回的 data 字段结构。
+ * </p>
+ *
+ * @author qdb-sdk
+ * @version 1.0.0
+ */
+@Data
+public class ShopPageResult {
+
+    /** 记录总数 */
+    private Integer total;
+
+    /** 店铺列表 */
+    private List<ShopInfo> data;
+}

+ 24 - 0
qdb-sdk/src/main/java/com/qdb/sdk/model/response/SysGoodsPageResult.java

@@ -0,0 +1,24 @@
+package com.qdb.sdk.model.response;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 系统商品查询分页结果
+ * <p>
+ * 对应 {@code foonsu.erp.sysGoods.list} 接口返回的 data 字段结构。
+ * </p>
+ *
+ * @author qdb-sdk
+ * @version 1.0.0
+ */
+@Data
+public class SysGoodsPageResult {
+
+    /** 记录总数 */
+    private Integer total;
+
+    /** 商品列表 */
+    private List<SysGoodsVO> data;
+}