Ver Fonte

feat: ERP店铺同步优化+分页+过滤+异步+文档

- 店铺同步改为 @Async 异步后台执行,立即返回
- 同步时按名称过滤"自助售卖机"
- 解除绑定修复 LambdaUpdateWrapper 强制写 null
- 列表改为分页模式,支持搜索店铺名称/卖家账号
- 去掉店铺编码、授权状态列,卖家账号列宽与店铺名称一致
- Vite 代理路径修正 /erp-shop → /erp-shops
- 新增 ERP对接模块完整文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
skyline há 5 dias atrás
pai
commit
75cf58d6bc

+ 314 - 0
docs/ERP对接模块.md

@@ -0,0 +1,314 @@
+# ERP对接模块说明
+
+> 本文档描述企得宝 ERP 系统集成方案,涵盖 SDK 封装、数据同步、商品/店铺关联管理的完整架构。
+> 帮助开发人员理解 ERP 对接的设计决策和关键流程。
+
+---
+
+## 一、模块概述
+
+### 1.1 对接目标
+
+通过企得宝 ERP 开放平台 API(`https://api.7debao.com/router/api`),实现:
+
+- 将本地商品与 ERP 商品建立一对一关联
+- 将本地门店与 ERP 店铺建立一对一绑定
+- 后续补货单可推送至 ERP 生成采购单据
+
+### 1.2 架构总览
+
+```
+┌─────────────────────────────────────────────────────┐
+│                   haha-admin-web                     │
+│  erp-goods/index.vue     erp-shop/index.vue          │
+└──────────────────┬──────────────────────────────────┘
+                   │ HTTP API
+┌──────────────────▼──────────────────────────────────┐
+│                   haha-admin                         │
+│  ErpGoodsController     ErpShopController            │
+│  QdbSdkConfig (QdbClient Bean)                      │
+└──────────────────┬──────────────────────────────────┘
+                   │
+┌──────────────────▼──────────────────────────────────┐
+│                   haha-service                       │
+│  ErpGoodsService        ErpShopService               │
+│  (sync / bind / unbind / autoBind)                   │
+└──────────────────┬──────────────────────────────────┘
+                   │
+┌──────────────────▼──────────────────────────────────┐
+│                   qdb-sdk (子模块)                    │
+│  QdbClient → HttpUtil (签名 + HTTP)                  │
+│  GoodsApi     ShopApi     OrderApi                   │
+└──────────────────┬──────────────────────────────────┘
+                   │
+┌──────────────────▼──────────────────────────────────┐
+│              企得宝 ERP API                           │
+│          api.7debao.com/router/api                   │
+│  foonsu.erp.sysGoods.list    (系统商品查询)           │
+│  foonsu.erp.sysGoods.save    (系统商品上传)           │
+│  foonsu.erp.sysShop.list     (店铺查询)               │
+│  foonsu.erp.order.save       (订单上传)               │
+└─────────────────────────────────────────────────────┘
+```
+
+---
+
+## 二、数据库设计
+
+### 2.1 ER图(核心表)
+
+```
+t_product                    t_erp_goods              企得宝ERP
+┌──────────────┐            ┌──────────────────┐      ┌──────────┐
+│ id           │            │ id               │      │ goodsId  │
+│ code         │            │ erp_goods_id ◄───┼──────┤ goodsCode │
+│ name ◄───────┼────────────┤ goods_code       │      │ goodsName│
+│ erp_goods_id ├───────────►│ goods_name       │      │ specs    │
+│ barcode      │            │ specs            │      │ skUnit   │
+│ pic          │            │ sale_price       │      │ picPath  │
+│ ...          │            │ bar_code(JSON)   │      │ ...      │
+└──────────────┘            │ modify_date      │      └──────────┘
+                            │ status           │
+t_shop                      └──────────────────┘      企得宝ERP
+┌──────────────┐            t_erp_shop                 ┌──────────┐
+│ id           │            ┌──────────────────┐      │ shopId   │
+│ shop_code    │            │ id               │      │ shopCode │
+│ erp_shop_id  ├───────────►│ erp_shop_id ◄────┼──────┤ shopName │
+│ name         │            │ shop_code        │      │ ...      │
+│ ...          │            │ shop_name        │      └──────────┘
+└──────────────┘            │ status           │
+                            └──────────────────┘
+```
+
+### 2.2 关联关系
+
+- `t_product.erp_goods_id` → `t_erp_goods.erp_goods_id`:本地商品与 ERP 商品的**一对一关联**
+- `t_shop.erp_shop_id` → `t_erp_shop.erp_shop_id`:本地门店与 ERP 店铺的**一对一绑定**
+- `t_product.name` = `t_erp_goods.goods_code`:两者业务上的对应关系(商品名称即为 ERP 编码),是一键自动关联的依据
+
+### 2.3 DDL脚本
+
+| 脚本 | 用途 |
+|------|------|
+| `docs/database/create_erp_goods.sql` | 创建 `t_erp_goods` 缓存表 |
+| `docs/database/create_erp_shop.sql` | 创建 `t_erp_shop` 缓存表 |
+| `docs/database/add_erp_goods_id.sql` | `t_product` 增加 `erp_goods_id` 字段 |
+| `docs/database/add_erp_shop_id.sql` | `t_shop` 增加 `erp_shop_id` 字段 |
+| `docs/database/add_erp_goods_bar_code.sql` | `t_erp_goods` 增加 `bar_code`、`modify_date` |
+| `docs/database/menu_add_erp_goods.sql` | 插入"ERP商品关联"菜单 |
+| `docs/database/menu_add_erp_shop.sql` | 插入"ERP店铺管理"菜单 |
+| `docs/database/add_erp_permissions.sql` | 插入 ERP 功能权限数据 |
+
+---
+
+## 三、SDK集成 (qdb-sdk)
+
+### 3.1 模块定位
+
+`qdb-sdk` 是 `haha-parent` 的子模块(`com.haha:qdb-sdk`),统一版本管理。提供企得宝 ERP 开放 API 的 Java SDK 封装。
+
+### 3.2 核心类
+
+| 类 | 职责 |
+|----|------|
+| `QdbConfig` | SDK 配置(clientId、clientSecret、apiUrl、超时等) |
+| `QdbClient` | SDK 入口,持有各业务 API 实例 + `HttpUtil` |
+| `HttpUtil` | HTTP 请求封装(签名、OkHttp 调用) |
+| `SignUtil` | MD5 签名算法 |
+| `BaseQdbApi` | API 基类,提供 `execute()` 通用调用方法 |
+| `GoodsApi` | 商品管理 API(查询、上传) |
+| `ShopApi` | 店铺管理 API(查询) |
+| `OrderApi` | 订单管理 API(上传、查询) |
+
+### 3.3 Bean 配置
+
+`QdbSdkConfig`(haha-admin 模块):
+
+```java
+@Configuration
+public class QdbSdkConfig {
+    @Value("${qdb.client-id:}")
+    private String clientId;
+    @Value("${qdb.client-secret:}")
+    private String clientSecret;
+    @Value("${qdb.api-url:https://api.7debao.com/router/api}")
+    private String apiUrl;
+
+    @Bean
+    @ConditionalOnProperty(prefix = "qdb", name = {"client-id", "client-secret"})
+    public QdbClient qdbClient() { ... }
+}
+```
+
+配置项(`application.yml`):
+
+```yaml
+qdb:
+  client-id: fs2009263
+  client-secret: your_secret_here
+  api-url: https://api.7debao.com/router/api
+```
+
+**安全提醒**:`client-id` 和 `client-secret` 应使用环境变量或外部配置文件管理,避免提交到代码仓库。
+
+### 3.4 API 调用方式
+
+ERP API 统一 POST 到 `https://api.7debao.com/router/api`:
+- 公共参数(client_id, method, timestamp, sign...)拼接到 URL Query String
+- 业务参数以 JSON 放入 Request Body
+- 签名:公共参数按 key 字典序排列 → 拼接 `&key=clientSecret` → MD5
+
+---
+
+## 四、ERP商品同步
+
+### 4.1 同步策略
+
+**不是拉取 ERP 全量商品**,而是**以本地商品为基准逐条反查**。
+
+原因:ERP 中有多个客户的数据,全量拉取无法区分哪些是本系统需要的。系统只关心已在哈哈平台上架的商品。
+
+### 4.2 同步流程
+
+```
+① SELECT t_product WHERE is_deleted=0 AND name IS NOT NULL
+     → 获取所有本地商品(name 作为 ERP goodsCode)
+
+② 逐条调用 foonsu.erp.sysGoods.list
+     参数: { pageIndex:1, pageSize:1, goodsCode: product.name, status:1, combination:0 }
+     频率控制: 每 10 条暂停 200ms
+
+③ ERP 返回: { data: { total: N, data: [{ goodsId, goodsCode, goodsName, ... }] } }
+     → GoodsApi 自动拆包提取内层数组 → List<SysGoodsVO>
+
+④ 命中 → upsert 到 t_erp_goods(按 erp_goods_id 判断新增/更新)
+   未命中 → 记入日志,在同步完成后整齐打印清单
+```
+
+### 4.3 异步执行
+
+`ErpGoodsServiceImpl.syncFromErp()` 标注 `@Async("taskExecutor")`,接口 `POST /erp-goods/sync` 立即返回"同步任务已启动",后台线程池执行。
+
+### 4.4 保存字段
+
+同步时保存:`goodsId`, `goodsCode`, `goodsName`, `shortGoodsName`, `specs`, `skUnit`, `goodsKindCode`, `goodsKindName`, `brand`, `salePrice`, `costPrice`, `providerName`, `batchType`, `combination`, `articleNo`, `picPath`, `weight`, `barCodeList(JSON)`, `modifyDate`
+
+---
+
+## 五、商品关联
+
+### 5.1 手动关联/解除
+
+- `POST /erp-goods/bind` — 选择本地商品与 ERP 商品一对一关联
+- `POST /erp-goods/unbind` — 解除关联(使用 `LambdaUpdateWrapper.set(null)` 强制写 NULL,避免 MP 默认跳过 null 字段)
+
+### 5.2 一键自动关联
+
+- `POST /erp-goods/auto-bind` — 遍历所有 ERP 缓存商品,用 `goodsCode` 匹配本地 `product.name`
+- 唯一匹配 → 自动绑定;0 个或多于 1 个匹配 → 跳过
+- **批量优化**:
+  - 查询已绑定:1 次 `WHERE erp_goods_id IN (...)` 
+  - 查询匹配:每 100 个 code 一批 `WHERE name IN (...)`
+  - 批量 UPDATE:`CASE WHEN` SQL,每 100 条一批
+
+---
+
+## 六、ERP店铺管理
+
+### 6.1 同步
+
+- `POST /erp-shop/sync` — 从 ERP 查询店铺列表,缓存到 `t_erp_shop`
+- 当前实现为全量拉取(翻页),与商品同步类似采用异步执行
+
+### 6.2 绑定
+
+- `POST /erp-shop/bind` — 将本地门店与 ERP 店铺建立一对一绑定
+- `POST /erp-shop/unbind` — 解除绑定
+
+---
+
+## 七、权限配置
+
+### 7.1 权限码
+
+| 权限码 | 说明 | 类型 |
+|--------|------|------|
+| `erp:goods:sync` | ERP商品同步 | execute |
+| `erp:goods:read` | ERP商品查看 | read |
+| `erp:goods:bind` | ERP商品关联/解绑/一键关联 | update |
+| `erp:shop:sync` | ERP店铺同步 | execute |
+| `erp:shop:read` | ERP店铺查看 | read |
+| `erp:shop:bind` | ERP店铺绑定/解绑 | update |
+
+### 7.2 菜单
+
+| 菜单ID | 父菜单 | 路径 | 名称 |
+|--------|--------|------|------|
+| 404 | 商品管理(400) | /product/erp-goods | ERP商品关联 |
+| 305 | 运营管理(300) | /operation/erp-shop | ERP店铺管理 |
+
+### 7.3 权限校验机制
+
+- `@RequirePermission` 注解通过 AOP 切面(`PermissionAspect`)校验
+- 超级管理员(role_id=1)直接放行,不查权限表
+- 非超管角色需在 `t_data_permission` 表有对应权限码,且在 `t_role_menu` 表有菜单授权
+
+---
+
+## 八、前端页面
+
+### 8.1 ERP商品关联 `erp-goods/index.vue`
+
+| 功能 | 描述 |
+|------|------|
+| 列表 | 分页展示 ERP 商品(图片、编码、名称、规格、单位、售价、关联状态) |
+| 搜索 | 按商品名称/编码搜索 |
+| 同步 | 异步后台执行,完成后需手动刷新 |
+| 一键关联 | 按 goodsCode 自动匹配本地商品 |
+| 手动关联弹窗 | 展示 ERP 商品信息卡片 + 带缩略图的下拉选择框 |
+| 解除关联 | 确认弹窗后解除 |
+| 关联浮窗 | 悬停已关联标签时展示本地商品详情(含图片预览) |
+
+### 8.2 ERP店铺管理 `erp-shop/index.vue`
+
+类似商品关联页,提供店铺的同步、绑定、解除绑定功能。
+
+### 8.3 Vite 代理配置
+
+`/erp-goods` 和 `/erp-shop` 路径需在 `vite.config.ts` 的 `apiPaths` 数组中注册,否则开发环境请求不会被转发到后端。
+
+---
+
+## 九、关键设计决策
+
+### 9.1 为什么不用 GoodsApi 的泛型直接解析?
+
+ERP 的 `foonsu.erp.sysGoods.list` 返回的 `data` 字段是嵌套结构 `{ total: N, data: [...] }`,不是扁平数组。`GoodsApi.listSysGoods()` 内部用 `JSONObject` 接收原始 data,再手动提取内层数组,避免 fastjson2 泛型解析的类型不匹配。
+
+### 9.2 为什么同步用 goodsCode 逐条查而不是全量翻页?
+
+ERP 中有多客户的商品数据,全量拉取无法过滤。只有本地商品的 `name` 字段才对应 ERP 的 `goodsCode`,以此为 key 逐条精确查询是最准确的方式。
+
+### 9.3 为什么解除关联用 LambdaUpdateWrapper.set(null)?
+
+MyBatis-Plus 默认 `FieldStrategy.NOT_NULL`,`updateById(entity)` 会跳过 null 值字段。设置 `erpGoodsId = null` 时需要使用 `LambdaUpdateWrapper.set(Product::getErpGoodsId, null)` 强制写入。
+
+### 9.4 为什么 qdb-sdk 改为 haha-parent 子模块?
+
+原 `com.qdb:qdb-sdk:1.0.0` 是独立模块,与 `com.haha:qdb-sdk` 并存会导致 classpath 上有**两份** `GoodsApi.class`,旧版本优先加载 → NoSuchMethodError / ClassCastException。改为子模块统一 version,删除 Maven 本地旧 jar 后解决。
+
+---
+
+## 十、待完成
+
+| 项目 | 状态 | 说明 |
+|------|------|------|
+| 补货单推送 ERP | TODO | `ReplenishmentOrderServiceImpl.syncToErp()` 当前为 no-op |
+| ERP 店铺同步 | 已实现 | 待前端完善 |
+| 增量同步 | 未实现 | 每次全量遍历本地商品,无时间戳过滤 |
+| qdb-sdk 编译 | 已验证 | 需 `mvn clean install` 后 classpath 唯一 |
+
+---
+
+> 创建日期:2026-07-09
+> 维护者:开发团队

+ 3 - 3
haha-admin-web/src/api/erpShop.ts

@@ -10,9 +10,9 @@ type Result = {
 export const syncErpShops = () =>
   http.request<Result>("post", "/erp-shops/sync");
 
-/** 获取已缓存的 ERP 店铺列表 */
-export const getErpShopList = () =>
-  http.request<Result>("get", "/erp-shops");
+/** 分页获取已缓存的 ERP 店铺列表 */
+export const getErpShopList = (params: { keyword?: string; page: number; pageSize: number }) =>
+  http.request<Result>("get", "/erp-shops", { params });
 
 /** 绑定门店与 ERP 店铺 */
 export const bindErpShop = (shopId: number, erpShopId: number) =>

+ 62 - 23
haha-admin-web/src/views/erp-shop/index.vue

@@ -1,5 +1,5 @@
 <script setup lang="ts">
-import { ref, onMounted } from "vue";
+import { ref, reactive, onMounted } from "vue";
 import { ElMessage, ElMessageBox } from "element-plus";
 import { useRenderIcon } from "@/components/ReIcon/src/hooks";
 import Refresh from "~icons/ep/refresh";
@@ -19,7 +19,10 @@ defineOptions({
 
 const loading = ref(false);
 const syncing = ref(false);
+const keyword = ref("");
 const dataList = ref<any[]>([]);
+const total = ref(0);
+const pagination = reactive({ currentPage: 1, pageSize: 10 });
 const shopOptions = ref<any[]>([]);
 const bindDialogVisible = ref(false);
 const currentErpShop = ref<any>(null);
@@ -29,10 +32,14 @@ const bindLoading = ref(false);
 async function fetchList() {
   loading.value = true;
   try {
-    const res = await getErpShopList();
+    const res = await getErpShopList({
+      keyword: keyword.value || undefined,
+      page: pagination.currentPage,
+      pageSize: pagination.pageSize
+    });
     if (res.code === 200) {
-      dataList.value = res.data || [];
-      // 检查各 ERP 店铺的绑定状态
+      dataList.value = res.data?.list || [];
+      total.value = res.data?.total || 0;
       await fetchBoundStatus();
     }
   } finally {
@@ -40,6 +47,22 @@ async function fetchList() {
   }
 }
 
+function handlePageChange(page: number) {
+  pagination.currentPage = page;
+  fetchList();
+}
+
+function handleSizeChange(size: number) {
+  pagination.currentPage = 1;
+  pagination.pageSize = size;
+  fetchList();
+}
+
+function handleSearch() {
+  pagination.currentPage = 1;
+  fetchList();
+}
+
 async function fetchBoundStatus() {
   // 获取门店列表来展示绑定关系
   try {
@@ -61,11 +84,12 @@ async function handleSync() {
   try {
     const res = await syncErpShops();
     if (res.code === 200) {
-      ElMessage.success("同步成功");
-      await fetchList();
+      ElMessage.success(res.message || "同步任务已启动");
     } else {
       ElMessage.error(res.message || "同步失败");
     }
+  } catch (e) {
+    ElMessage.error("同步请求失败,请稍后重试");
   } finally {
     syncing.value = false;
   }
@@ -132,14 +156,24 @@ onMounted(() => {
   <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 class="flex items-center gap-3">
+        <el-input
+          v-model="keyword"
+          placeholder="搜索店铺名称或卖家账号"
+          clearable
+          class="w-[240px]!"
+          @keyup.enter="handleSearch"
+        />
+        <el-button @click="handleSearch">搜索</el-button>
+        <el-button
+          type="primary"
+          :icon="useRenderIcon(Refresh)"
+          :loading="syncing"
+          @click="handleSync"
+        >
+          从 ERP 同步
+        </el-button>
+      </div>
     </div>
 
     <div class="px-6 pt-4">
@@ -153,16 +187,8 @@ onMounted(() => {
       >
         <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="nickName" label="卖家账号" min-width="180" />
         <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">
@@ -196,6 +222,19 @@ onMounted(() => {
           </template>
         </el-table-column>
       </el-table>
+
+      <div class="flex justify-end mt-4">
+        <el-pagination
+          v-model:current-page="pagination.currentPage"
+          v-model:page-size="pagination.pageSize"
+          :total="total"
+          :page-sizes="[10, 20, 50]"
+          layout="total, sizes, prev, pager, next, jumper"
+          background
+          @current-change="handlePageChange"
+          @size-change="handleSizeChange"
+        />
+      </div>
     </div>
 
     <!-- 绑定对话框 -->

+ 1 - 1
haha-admin-web/vite.config.ts

@@ -45,7 +45,7 @@ export default async ({ mode }: ConfigEnv): Promise<UserConfigExport> => {
     "/upload",
     "/replenishment-orders",
     "/erp-goods",
-    "/erp-shop"
+    "/erp-shops"
   ];
   
   // 动态生成代理配置

+ 9 - 5
haha-admin/src/main/java/com/haha/admin/controller/ErpShopController.java

@@ -1,8 +1,10 @@
 package com.haha.admin.controller;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.haha.admin.annotation.RequirePermission;
 import com.haha.common.annotation.Log;
 import com.haha.common.enums.OperationType;
+import com.haha.common.vo.PageResult;
 import com.haha.common.vo.Result;
 import com.haha.entity.ErpShop;
 import com.haha.service.ErpShopService;
@@ -10,7 +12,6 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.web.bind.annotation.*;
 
-import java.util.List;
 import java.util.Map;
 
 /**
@@ -32,16 +33,19 @@ public class ErpShopController {
     @PostMapping("/sync")
     public Result<Void> syncFromErp() {
         erpShopService.syncFromErp();
-        return Result.success("同步成功");
+        return Result.success("同步任务已启动,正在后台执行,请稍后刷新查看结果");
     }
 
     /**
-     * 查询已缓存的 ERP 店铺列表
+     * 分页查询已缓存的 ERP 店铺列表
      */
     @RequirePermission("erp:shop:read")
     @GetMapping
-    public Result<List<ErpShop>> list() {
-        return Result.success("查询成功", erpShopService.listAll());
+    public Result<PageResult<ErpShop>> list(@RequestParam(required = false) String keyword,
+                                             @RequestParam(defaultValue = "1") int page,
+                                             @RequestParam(defaultValue = "10") int pageSize) {
+        IPage<ErpShop> pageResult = erpShopService.listAll(keyword, page, pageSize);
+        return Result.success("查询成功", PageResult.of(pageResult));
     }
 
     /**

+ 3 - 7
haha-service/src/main/java/com/haha/service/ErpShopService.java

@@ -1,24 +1,20 @@
 package com.haha.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
 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 店铺
+     * 分页查询已缓存的 ERP 店铺
      */
-    List<ErpShop> listAll();
+    IPage<ErpShop> listAll(String keyword, int page, int pageSize);
 
     /**
      * 将系统门店与 ERP 店铺一对一绑定

+ 26 - 10
haha-service/src/main/java/com/haha/service/impl/ErpShopServiceImpl.java

@@ -2,6 +2,8 @@ package com.haha.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.haha.common.exception.BusinessException;
 import com.haha.entity.ErpShop;
@@ -18,8 +20,10 @@ 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.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.StringUtils;
 
 import java.time.LocalDateTime;
 import java.util.List;
@@ -37,6 +41,7 @@ public class ErpShopServiceImpl extends ServiceImpl<ErpShopMapper, ErpShop> impl
     @Autowired(required = false)
     private QdbClient qdbClient;
 
+    @Async("taskExecutor")
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void syncFromErp() {
@@ -48,6 +53,7 @@ public class ErpShopServiceImpl extends ServiceImpl<ErpShopMapper, ErpShop> impl
         int pageSize = 50;
         boolean hasMore = true;
         int syncedCount = 0;
+        int filteredCount = 0;
 
         while (hasMore) {
             ShopQueryRequest request = ShopQueryRequest.builder()
@@ -65,11 +71,15 @@ public class ErpShopServiceImpl extends ServiceImpl<ErpShopMapper, ErpShop> impl
                 }
 
                 for (ShopInfo shopInfo : pageResult.getData()) {
+                    if (shopInfo.getShopName() == null
+                            || !shopInfo.getShopName().contains("自助售卖机")) {
+                        filteredCount++;
+                        continue;
+                    }
                     upsertErpShop(shopInfo);
                     syncedCount++;
                 }
 
-                // 判断是否还有下一页
                 Integer total = pageResult.getTotal();
                 if (total != null && pageNo * pageSize >= total) {
                     hasMore = false;
@@ -85,15 +95,20 @@ public class ErpShopServiceImpl extends ServiceImpl<ErpShopMapper, ErpShop> impl
             }
         }
 
-        log.info("ERP店铺同步完成: 共同步 {} 条", syncedCount);
+        log.info("ERP店铺同步完成: 协同步 {} 条, 过滤 {} 条", syncedCount, filteredCount);
     }
 
     @Override
-    public List<ErpShop> listAll() {
-        return lambdaQuery()
-                .eq(ErpShop::getStatus, 1)
-                .orderByAsc(ErpShop::getShopName)
-                .list();
+    public IPage<ErpShop> listAll(String keyword, int page, int pageSize) {
+        LambdaQueryWrapper<ErpShop> wrapper = new LambdaQueryWrapper<ErpShop>()
+                .eq(ErpShop::getStatus, 1);
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w
+                    .like(ErpShop::getShopName, keyword)
+                    .or()
+                    .like(ErpShop::getNickName, keyword));
+        }
+        return page(new Page<>(page, pageSize), wrapper.orderByAsc(ErpShop::getShopName));
     }
 
     @Override
@@ -134,9 +149,10 @@ public class ErpShopServiceImpl extends ServiceImpl<ErpShopMapper, ErpShop> impl
             throw new BusinessException(404, "门店不存在");
         }
 
-        shop.setErpShopId(null);
-        shop.setUpdateTime(LocalDateTime.now());
-        shopMapper.updateById(shop);
+        shopMapper.update(null, new LambdaUpdateWrapper<Shop>()
+                .set(Shop::getErpShopId, null)
+                .set(Shop::getUpdateTime, LocalDateTime.now())
+                .eq(Shop::getId, shopId));
 
         log.info("门店解绑ERP店铺: shopId={}, shopName={}", shopId, shop.getName());
     }