# 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 ④ 命中 → 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 > 维护者:开发团队