|
@@ -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
|
|
|
|
|
+> 维护者:开发团队
|