本文档描述企得宝 ERP 系统集成方案,涵盖 SDK 封装、数据同步、商品/店铺关联管理的完整架构。 帮助开发人员理解 ERP 对接的设计决策和关键流程。
通过企得宝 ERP 开放平台 API(https://api.7debao.com/router/api),实现:
┌─────────────────────────────────────────────────────┐
│ 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 (订单上传) │
└─────────────────────────────────────────────────────┘
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_device └──────────────────┘ 企得宝ERP
┌──────────────┐ t_erp_shop ┌──────────┐
│ id │ ┌──────────────────┐ │ shopId │
│ deviceId ◄───┼────────────┤ shop_code │ │ shopCode │
│ erp_shop_id ├───────────►│ erp_shop_id ◄────┼──────┤ shopName │
│ name │ │ shop_name │ │ ... │
│ ... │ │ nick_name │ └──────────┘
└──────────────┘ │ status │
└──────────────────┘
t_product.erp_goods_id → t_erp_goods.erp_goods_id:本地商品与 ERP 商品的一对一关联t_device.erp_shop_id → t_erp_shop.erp_shop_id:本地设备与 ERP 店铺的一对一绑定(ERP 店铺对应系统中的设备,ERP 的 shopCode = 设备的 deviceId)t_product.name = t_erp_goods.goods_code:商品名称即 ERP 编码,是一键自动关联的依据t_device.deviceId = t_erp_shop.shopCode:设备ID即 ERP 店铺编码,是一键自动绑定的依据| 脚本 | 用途 |
|---|---|
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_device_erp_shop_id.sql |
t_device 增加 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 功能权限数据 |
qdb-sdk 是 haha-parent 的子模块(com.haha:qdb-sdk),统一版本管理。提供企得宝 ERP 开放 API 的 Java SDK 封装。
| 类 | 职责 |
|---|---|
QdbConfig |
SDK 配置(clientId、clientSecret、apiUrl、超时等) |
QdbClient |
SDK 入口,持有各业务 API 实例 + HttpUtil |
HttpUtil |
HTTP 请求封装(签名、OkHttp 调用) |
SignUtil |
MD5 签名算法 |
BaseQdbApi |
API 基类,提供 execute() 通用调用方法 |
GoodsApi |
商品管理 API(查询、上传) |
ShopApi |
店铺管理 API(查询) |
OrderApi |
订单管理 API(上传、查询) |
QdbSdkConfig(haha-admin 模块):
@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):
qdb:
client-id: fs2009263
client-secret: your_secret_here
api-url: https://api.7debao.com/router/api
安全提醒:client-id 和 client-secret 应使用环境变量或外部配置文件管理,避免提交到代码仓库。
ERP API 统一 POST 到 https://api.7debao.com/router/api:
&key=clientSecret → MD5不是拉取 ERP 全量商品,而是以本地商品为基准逐条反查。
原因:ERP 中有多个客户的数据,全量拉取无法区分哪些是本系统需要的。系统只关心已在哈哈平台上架的商品。
① 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 判断新增/更新)
未命中 → 记入日志,在同步完成后整齐打印清单
ErpGoodsServiceImpl.syncFromErp() 标注 @Async("taskExecutor"),接口 POST /erp-goods/sync 立即返回"同步任务已启动",后台线程池执行。
同步时保存:goodsId, goodsCode, goodsName, shortGoodsName, specs, skUnit, goodsKindCode, goodsKindName, brand, salePrice, costPrice, providerName, batchType, combination, articleNo, picPath, weight, barCodeList(JSON), modifyDate
POST /erp-goods/bind — 选择本地商品与 ERP 商品一对一关联POST /erp-goods/unbind — 解除关联(使用 LambdaUpdateWrapper.set(null) 强制写 NULL,避免 MP 默认跳过 null 字段)POST /erp-goods/auto-bind — 遍历所有 ERP 缓存商品,用 goodsCode 匹配本地 product.nameWHERE erp_goods_id IN (...)WHERE name IN (...)CASE WHEN SQL,每 100 条一批POST /erp-shops/sync — 从 ERP 全量翻页查询店铺列表t_erp_shop@Async("taskExecutor"),接口立即返回syncedCount / filteredCountPOST /erp-shops/bind — 选择本地设备与 ERP 店铺一对一绑定POST /erp-shops/unbind — 解除绑定(使用 LambdaUpdateWrapper.set(null) 强制写 NULL)t_device),非门店。ERP 店铺的 shopCode 对应设备的 deviceIdPOST /erp-shops/auto-bind — 遍历所有 ERP 缓存店铺,用 shopCode 匹配设备 deviceIdWHERE erp_shop_id IN (...)WHERE device_id IN (...)CASE WHEN SQL,每 100 条一批| 权限码 | 说明 | 类型 |
|---|---|---|
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 |
| 菜单ID | 父菜单 | 路径 | 名称 |
|---|---|---|---|
| 404 | 商品管理(400) | /product/erp-goods | ERP商品关联 |
| 305 | 运营管理(300) | /operation/erp-shop | ERP店铺管理 |
@RequirePermission 注解通过 AOP 切面(PermissionAspect)校验t_data_permission 表有对应权限码,且在 t_role_menu 表有菜单授权erp-goods/index.vue| 功能 | 描述 |
|---|---|
| 列表 | 分页展示 ERP 商品(图片、编码、名称、规格、单位、售价、关联状态) |
| 搜索 | 按商品名称/编码搜索 |
| 同步 | 异步后台执行,完成后需手动刷新 |
| 一键关联 | 按 goodsCode 自动匹配本地商品 |
| 手动关联弹窗 | 展示 ERP 商品信息卡片 + 带缩略图的下拉选择框 |
| 解除关联 | 确认弹窗后解除 |
| 关联浮窗 | 悬停已关联标签时展示本地商品详情(含图片预览) |
erp-shop/index.vue| 功能 | 描述 |
|---|---|
| 列表 | 分页展示 ERP 店铺(店铺ID、名称、卖家账号、设备ID、绑定状态) |
| 搜索 | 按店铺名称/卖家账号搜索 |
| 同步 | 异步后台执行,过滤"自助售卖机"名称 |
| 一键绑定 | 按 shopCode 自动匹配设备 deviceId |
| 手动绑定弹窗 | 选择本地设备进行绑定 |
| 解除绑定 | 确认弹窗后解除 |
| 设备浮窗 | 悬停已绑定设备标签时展示设备详情(deviceId、名称、门店、地址) |
/erp-goods 和 /erp-shops 路径需在 vite.config.ts 的 apiPaths 数组中注册,否则开发环境请求不会被转发到后端。
ERP 的 foonsu.erp.sysGoods.list 返回的 data 字段是嵌套结构 { total: N, data: [...] },不是扁平数组。GoodsApi.listSysGoods() 内部用 JSONObject 接收原始 data,再手动提取内层数组,避免 fastjson2 泛型解析的类型不匹配。
ERP 中有多客户的商品数据,全量拉取无法过滤。只有本地商品的 name 字段才对应 ERP 的 goodsCode,以此为 key 逐条精确查询是最准确的方式。
MyBatis-Plus 默认 FieldStrategy.NOT_NULL,updateById(entity) 会跳过 null 值字段。设置 erpGoodsId = null 时需要使用 LambdaUpdateWrapper.set(Product::getErpGoodsId, null) 强制写入。
原 com.qdb:qdb-sdk:1.0.0 是独立模块,与 com.haha:qdb-sdk 并存会导致 classpath 上有两份 GoodsApi.class,旧版本优先加载 → NoSuchMethodError / ClassCastException。改为子模块统一 version,删除 Maven 本地旧 jar 后解决。
ERP 中的店铺(自助售卖机)在业务上对应的是系统中的一个设备(售货机)。ERP 的 shopCode 字段存储的是设备 ID(deviceId),因此绑定关系建立在 t_device 表上。一键绑定时用 shopCode ↔ deviceId 做精确匹配。
逐条 UPDATE 在数据量较大时性能很差(N 次网络往返)。CASE WHEN 将多条 UPDATE 合并为一条 SQL,每 100 行一批,大幅减少数据库交互次数。
| 项目 | 状态 | 说明 |
|---|---|---|
| 补货单推送 ERP | TODO | ReplenishmentOrderServiceImpl.syncToErp() 当前为 no-op |
| 商品增量同步 | 未实现 | 每次全量遍历本地商品,无时间戳过滤 |
| 店铺增量同步 | 未实现 | 每次全量翻页,无时间戳过滤 |
创建日期:2026-07-09 维护者:开发团队