Initial commit: Integral Shop CRMEB Project
Backend: Spring Boot 2.2.6 + MyBatis Plus - crmeb-admin: Admin API module - crmeb-service: Business logic - crmeb-common: Common utilities - crmeb-front: Frontend API Frontend: Vue 2.6.10 + Element UI 2.13.0 - Admin management system - 454 Vue components Analyzed by Miao Agent on 2026-02-28 Project Score: 5.5/10 Security: High Risk (JWT bypass, API over-permission)
This commit is contained in:
75
crmeb_22miao/.cursor/plans/fix_cors_error_e714db6e.plan.md
Normal file
75
crmeb_22miao/.cursor/plans/fix_cors_error_e714db6e.plan.md
Normal file
@@ -0,0 +1,75 @@
|
||||
---
|
||||
name: Fix CORS Error
|
||||
overview: Fix the CORS error on `http://127.0.0.1:8080/api/admin/platform/getLoginPic` by adding a webpack dev proxy in the frontend and hardening the backend CORS + security configuration.
|
||||
todos:
|
||||
- id: proxy
|
||||
content: Add /api proxy in mer_plat_admin/vue.config.js pointing to http://127.0.0.1:8080
|
||||
status: completed
|
||||
- id: cors-config
|
||||
content: Change addAllowedOrigin('*') to addAllowedOriginPattern('*') in MER-2.2_2601 CorsConfig.java
|
||||
status: completed
|
||||
- id: security-config
|
||||
content: Remove duplicate corsFilter addFilterBefore calls and uncomment OPTIONS permitAll in MER-2.2_2601 WebSecurityConfig.java
|
||||
status: completed
|
||||
- id: verify-base-url
|
||||
content: Check frontend base URL config and update if needed to use relative path when proxy is active
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Fix CORS Error: `mer_plat_admin` → `mer_java`
|
||||
|
||||
## Problem
|
||||
|
||||
Frontend (`localhost:9527`) makes direct cross-origin requests to the backend (`127.0.0.1:8080`). Two issues:
|
||||
|
||||
1. No dev proxy → browser enforces CORS on every API call
|
||||
2. `WebSecurityConfig` calls `http.cors()` AND `http.addFilterBefore(corsFilter, ...)` — both register the same `CorsFilter` bean, causing Spring Security 5.2.x to deduplicate/misposition it, resulting in no `Access-Control-Allow-Origin` header in the response. The OPTIONS preflight `permitAll` is also commented out.
|
||||
|
||||
## Fix 1 — Frontend: Add Dev Proxy
|
||||
|
||||
**File**: `[MER-2.2_2601/mer_plat_admin/vue.config.js](MER-2.2_2601/mer_plat_admin/vue.config.js)`
|
||||
|
||||
Add a `proxy` block inside `devServer` so all `/api` requests are transparently forwarded to the backend — no CORS issue at all in development:
|
||||
|
||||
```js
|
||||
devServer: {
|
||||
port: port,
|
||||
open: false,
|
||||
overlay: { warnings: false, errors: true },
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://127.0.0.1:8080',
|
||||
changeOrigin: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
Then update the frontend base URL so it uses a relative path (remove the hardcoded `http://127.0.0.1:8080`).
|
||||
|
||||
## Fix 2 — Backend: Clean Up CORS Configuration
|
||||
|
||||
**File**: `[MER-2.2_2601/mer_java/crmeb-admin/src/main/java/com/zbkj/admin/config/CorsConfig.java](MER-2.2_2601/mer_java/crmeb-admin/src/main/java/com/zbkj/admin/config/CorsConfig.java)`
|
||||
|
||||
- Change `addAllowedOrigin("*")` → `addAllowedOriginPattern("*")` (required for Spring 5.3+ compatibility and avoids ambiguity with credentials)
|
||||
|
||||
## Fix 3 — Backend: Reconcile Spring Security CORS Wiring
|
||||
|
||||
**File**: `[MER-2.2_2601/mer_java/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java](MER-2.2_2601/mer_java/crmeb-admin/src/main/java/com/zbkj/admin/config/WebSecurityConfig.java)`
|
||||
|
||||
Two changes:
|
||||
|
||||
- Remove the redundant `http.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class)` / `http.addFilterBefore(corsFilter, LogoutFilter.class)` calls — `http.cors()` already registers the `CorsFilter` bean automatically when it detects it in the context. Registering it twice causes deduplication issues.
|
||||
- Uncomment the OPTIONS preflight `permitAll` so pre-flight requests never get blocked:
|
||||
|
||||
```java
|
||||
.antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
```
|
||||
|
||||
## Files Changed
|
||||
|
||||
- `MER-2.2_2601/mer_plat_admin/vue.config.js` — add proxy
|
||||
- `MER-2.2_2601/mer_java/crmeb-admin/.../CorsConfig.java` — `addAllowedOriginPattern`
|
||||
- `MER-2.2_2601/mer_java/crmeb-admin/.../WebSecurityConfig.java` — remove duplicate filter registration, uncomment OPTIONS permitAll
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
---
|
||||
name: Fix eb_user id insert error
|
||||
overview: 数据库 eb_user 表 id 字段已去除自增,需在应用层在每次插入前生成并设置 id,保证 INSERT 语句包含 id 字段。
|
||||
todos:
|
||||
- id: next-user-id
|
||||
content: UserDao 增加 getMaxId,UserMapper.xml 增加对应 SQL;UserService 增加 getNextUserId()
|
||||
status: completed
|
||||
- id: register-phone
|
||||
content: UserServiceImpl.registerPhone 在 save(user) 前调用 getNextUserId() 并 user.setId(...)
|
||||
status: completed
|
||||
- id: wa-sync
|
||||
content: WaUserSyncServiceImpl 在 insert 前若 user.getId() 为 null 则设置 user.setId(getNextUserId())
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Fix: Field 'id' doesn't have a default value (eb_user insert)
|
||||
|
||||
## 前提
|
||||
|
||||
**数据库 `eb_user` 表 `id` 字段已去除自增**,不再使用数据库自增,因此必须在应用层在每次插入前为 `id` 赋值。
|
||||
|
||||
## 原因简述
|
||||
|
||||
- 实体 [User](MER-2.2_2601/mer_java/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java) 使用 `@TableId(type = IdType.INPUT)`,插入时若 `id == null`,MyBatis-Plus 生成的 INSERT 不包含 `id` 列。
|
||||
- 表结构无默认值且无自增时,MySQL 报错:`Field 'id' doesn't have a default value`。
|
||||
|
||||
因此需要在**所有执行 User 插入的路径**上,在插入前保证 `user.setId(...)` 已设置非空值。
|
||||
|
||||
## 新方案:应用层生成 id
|
||||
|
||||
保持 `IdType.INPUT`,在插入前统一通过“取当前最大 id + 1”的方式生成新 id,并在各插入点设置到实体上。
|
||||
|
||||
### 1. 提供“下一个用户 id”能力
|
||||
|
||||
**1.1 UserDao**
|
||||
文件:[mer_java/crmeb-service/src/main/java/com/zbkj/service/dao/UserDao.java](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/dao/UserDao.java)
|
||||
|
||||
- 新增方法:`Integer getMaxId();`
|
||||
|
||||
**1.2 UserMapper.xml**
|
||||
文件:[mer_java/crmeb-service/src/main/resources/mapper/user/UserMapper.xml](MER-2.2_2601/mer_java/crmeb-service/src/main/resources/mapper/user/UserMapper.xml)
|
||||
|
||||
- 新增 SQL,例如:
|
||||
```xml
|
||||
<select id="getMaxId" resultType="java.lang.Integer">
|
||||
SELECT COALESCE(MAX(id), 0) FROM eb_user
|
||||
</select>
|
||||
```
|
||||
|
||||
**1.3 UserService / UserServiceImpl**
|
||||
|
||||
- 在 [UserService](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/UserService.java) 接口中新增:`Integer getNextUserId();`
|
||||
- 在 [UserServiceImpl](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserServiceImpl.java) 中实现:调用 `dao.getMaxId()`,返回 `maxId == null ? 1 : maxId + 1`。
|
||||
- **并发说明**:若注册/同步并发较高,建议在“获取 nextUserId + 插入”同一事务内执行,或后续改为序列表/分布式 id 方案;当前先保证单机下逻辑正确。
|
||||
|
||||
### 2. 插入前设置 id 的调用点
|
||||
|
||||
**2.1 手机号注册(无 id)**
|
||||
文件:[UserServiceImpl](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/impl/UserServiceImpl.java)
|
||||
|
||||
- 在 `registerPhone` 中,在执行 `save(user)` 之前(例如在 `transactionTemplate.execute` 内、`save(user)` 之前)增加:
|
||||
- `user.setId(getNextUserId());`
|
||||
|
||||
这样 INSERT 会带上 `id`,与“无自增”表结构一致。
|
||||
|
||||
**2.2 用户同步(可能无 id)**
|
||||
文件:[WaUserSyncServiceImpl](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/WaUserSyncServiceImpl.java)
|
||||
|
||||
- 当前已有 `user.setId(waUser.getId())`;若 `waUser.getId()` 可能为 null,则在 `userDao.insert(user)` 前增加判断:
|
||||
- 若 `user.getId() == null`,则 `user.setId(userService.getNextUserId())`(需注入 `UserService`)。
|
||||
|
||||
其他直接或间接插入 `User` 的地方(若有),同样在 insert/save 前保证 `user.setId(...)` 已赋值为非 null。
|
||||
|
||||
### 3. 不改动部分
|
||||
|
||||
- [User](MER-2.2_2601/mer_java/crmeb-common/src/main/java/com/zbkj/common/model/user/User.java) 保持 `@TableId(value = "id", type = IdType.INPUT)`,无需改为 AUTO。
|
||||
- 数据库无需恢复自增;表结构保持“无自增、无默认值”,由应用始终显式写入 `id`。
|
||||
|
||||
## 小结
|
||||
|
||||
|
||||
| 项目 | 操作 |
|
||||
| ----------------------------- | ------------------------------------------------------------------------------ |
|
||||
| UserDao | 新增 `Integer getMaxId();` |
|
||||
| UserMapper.xml | 新增 `getMaxId` 的 SELECT(COALESCE(MAX(id),0)) |
|
||||
| UserService / UserServiceImpl | 新增并实现 `getNextUserId()` |
|
||||
| UserServiceImpl.registerPhone | 在 `save(user)` 前 `user.setId(getNextUserId())` |
|
||||
| WaUserSyncServiceImpl | 在 insert 前若 `user.getId() == null` 则 `user.setId(userService.getNextUserId())` |
|
||||
|
||||
|
||||
按上述修改后,所有对 `eb_user` 的 INSERT 都会包含 `id` 字段,与“id 已去除自增”的数据库设计一致,错误可消除。
|
||||
@@ -0,0 +1,90 @@
|
||||
---
|
||||
name: Fix Nginx static 404
|
||||
overview: 服务器站点路径与 Nginx 已确认为 /www/wwwroot/jfplat.suzhouyuqi.com,root 与 static 目录均正确,nginx -t 已通过。若仍 404,按本文档「文件名一致性」与「location /static/ 显式送文件」等步骤继续排查。
|
||||
todos: []
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# 解决 jfplat 部署后 /static/ 资源 404 的排查与修复
|
||||
|
||||
## 当前已确认状态
|
||||
|
||||
- **站点根目录**:`/www/wwwroot/jfplat.suzhouyuqi.com/`
|
||||
- **static 目录**:已存在,含子目录 `css`、`js`、`tinymce4.7.5` 等(权限 755/root)
|
||||
- **Nginx 配置**(宝塔「配置文件」):
|
||||
- `root /www/wwwroot/jfplat.suzhouyuqi.com/;`
|
||||
- `location / { try_files $uri $uri/ /index.html; }`
|
||||
- `location /static/ { expires 7d; add_header Cache-Control "public, immutable"; }`
|
||||
- **Nginx 语法与重载**:`nginx -t && nginx -s reload` 已成功
|
||||
|
||||
在上述均正确的前提下若仍出现 404,按下面步骤继续排查。
|
||||
|
||||
---
|
||||
|
||||
## 第一步:确认 index.html 与 static 内文件名一致(哈希一致)
|
||||
|
||||
Vue 构建会对资源加哈希(如 `chunk-libs.a95f79e0.css`)。若服务器上的 `index.html` 与 `static/` 不是**同一次**构建产物,会出现「HTML 里引用的文件名在服务器上不存在」。
|
||||
|
||||
在服务器上执行:
|
||||
|
||||
```bash
|
||||
# 看 index.html 里引用的 CSS/JS 文件名
|
||||
grep -oE '/static/[^"]+\.(css|js)' /www/wwwroot/jfplat.suzhouyuqi.com/index.html
|
||||
|
||||
# 看 static/css 下实际存在的 .css 文件
|
||||
ls /www/wwwroot/jfplat.suzhouyuqi.com/static/css/
|
||||
|
||||
# 看 static/js 下实际存在的 .js 文件
|
||||
ls /www/wwwroot/jfplat.suzhouyuqi.com/static/js/
|
||||
```
|
||||
|
||||
若 `grep` 出来的文件名(如 `chunk-libs.a95f79e0.css`)在 `ls` 结果里不存在,说明部署不同步。
|
||||
|
||||
**处理**:用本地**同一次** `npm run build:prod` 生成的整个 `dist` 目录重新上传覆盖,保证 `index.html` 与 `static/` 内文件一一对应。
|
||||
|
||||
---
|
||||
|
||||
## 第二步:为 location /static/ 显式指定送文件(可选)
|
||||
|
||||
当前 `location /static/` 只有 `expires` 和 `add_header`,未显式指定如何送文件。个别环境下可导致静态请求未按 root 寻址。可在该块内加上 `try_files`,确保按 root 查找文件:
|
||||
|
||||
在宝塔 → 该站点 → 配置文件 → 找到 `location /static/` 块,改为:
|
||||
|
||||
```nginx
|
||||
location /static/ {
|
||||
try_files $uri =404;
|
||||
expires 7d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
```
|
||||
|
||||
保存后再次执行:`nginx -t && nginx -s reload`。
|
||||
|
||||
---
|
||||
|
||||
## 第三步:在服务器本机用 curl 自检
|
||||
|
||||
确认请求是否由本机 Nginx 正确响应:
|
||||
|
||||
```bash
|
||||
curl -I http://127.0.0.1/static/css/chunk-libs.a95f79e0.css -H "Host: jfplat.suzhouyuqi.com"
|
||||
```
|
||||
|
||||
若返回 `200 OK`,说明 Nginx 与 root 正常,问题可能在 CDN/反向代理或浏览器缓存;若返回 `404`,说明仍是 Nginx 寻址或文件缺失,回到第一步核对文件名与部署。
|
||||
|
||||
---
|
||||
|
||||
## 第四步:权限与访问
|
||||
|
||||
- Nginx 进程用户(如 `www` 或 `nginx`)需能遍历并读 root 及 static 下文件。
|
||||
- 在服务器上:`ls -la /www/wwwroot/jfplat.suzhouyuqi.com/` 与 `ls -la /www/wwwroot/jfplat.suzhouyuqi.com/static/` 不应出现 Permission denied。
|
||||
- 若启用 SELinux,可临时执行 `setenforce 0` 测试是否为 SELinux 拦截(仅作排查,测试后按需恢复)。
|
||||
|
||||
---
|
||||
|
||||
## 小结
|
||||
|
||||
- **已确认**:root 为 `/www/wwwroot/jfplat.suzhouyuqi.com/`,static 目录存在,配置语法正确并已重载。
|
||||
- **优先排查**:index.html 中引用的带哈希文件名是否与 `static/css/`、`static/js/`、`static/tinymce4.7.5/` 下实际文件一致;不一致则用同一次构建的完整 dist 重新部署。
|
||||
- **可选优化**:在 `location /static/` 中增加 `try_files $uri =404;`,再重载 Nginx。
|
||||
- **自检**:用 `curl -I` 在服务器本机带 Host 头请求具体静态 URL,区分是 Nginx 问题还是前端/CDN/缓存问题。
|
||||
@@ -0,0 +1,556 @@
|
||||
---
|
||||
name: Integral API Migration Plan
|
||||
overview: 将积分商城模块从单商户后端迁移到多商户后端。核心策略:在多商户后端新增兼容接口匹配前端现有 API 路径,前端 JS 文件保持不变,后端缺失参数采用默认值补充。
|
||||
todos:
|
||||
- id: backend-compat-product
|
||||
content: 多商户后端新增商品兼容接口:GET products(商品列表)、GET category(分类列表)、GET product/detail/{id}(商品详情路径参数兼容)、POST cart/save(加入购物车兼容)
|
||||
status: completed
|
||||
- id: backend-compat-cart
|
||||
content: 多商户后端新增购物车兼容接口:POST cart/num 支持 query string 参数 id/number、POST cart/delete 支持 query string 参数 ids
|
||||
status: completed
|
||||
- id: backend-compat-order
|
||||
content: 多商户后端新增订单兼容接口:GET order/data(订单统计)、POST order/cancel(body传id)、POST order/take(body传id)、POST order/del(body传id)
|
||||
status: completed
|
||||
- id: backend-compat-user
|
||||
content: 多商户后端新增用户兼容接口:POST loginV2(无密码登录)、GET user(用户信息映射到/info)、GET address/default(默认地址映射到/get/default)
|
||||
status: completed
|
||||
- id: backend-compat-integral
|
||||
content: 多商户后端新增积分兼容接口:GET integral/list(积分记录)、GET integral/user/account(按account查询积分信息)
|
||||
status: completed
|
||||
- id: backend-wa-controllers
|
||||
content: 多商户后端新增WA寄卖相关控制器:WaUserController(POST wa/user/info)、WaSelfbonusController(GET wa/selfbonus/list),从单商户版迁移
|
||||
status: completed
|
||||
- id: backend-response-compat
|
||||
content: 确保新增兼容接口返回的数据结构与单商户版本一致,必要时在 Service 层做字段映射/补充(新增适配方法,不修改原有方法)
|
||||
status: completed
|
||||
- id: verify-integration
|
||||
content: 联调验证:逐页面测试所有26个接口是否正常工作,确认前端无需任何修改
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# 积分商城模块 API 接口迁移方案:单商户 -> 多商户
|
||||
|
||||
## 核心迁移原则
|
||||
|
||||
1. **后端只增不改**:在多商户后端新增兼容接口,不修改原有 Controller 方法
|
||||
2. **字段只增不删**:涉及字段调整时,新增属性/参数,不删除原有字段
|
||||
3. **前端零修改**:保持 `api/store.js`、`api/order.js`、`api/user.js` 等 JS 文件不变,前端缺少的参数由后端接口补充默认值
|
||||
4. **优先后端适配**:通过在多商户后端新增与单商户路径一致的兼容接口,内部委托给多商户已有的 Service 层实现
|
||||
|
||||
---
|
||||
|
||||
## 一、积分模块实际使用的 API 接口清单
|
||||
|
||||
通过分析 `pages/integral/` 下 9 个 Vue 文件,排除仅导入但未实际调用的接口(`collectIntegralGoods`、`loadPreOrderApi`),以及整个 `api/integral.js` 文件(未被任何页面引用),共梳理出 **26 个实际使用的 API 接口**,分布在 4 个 API 文件中:
|
||||
|
||||
### 来源 `api/store.js`(4个)
|
||||
|
||||
- `getProductslist` — GET `products`
|
||||
- `getCategoryList` — GET `category`
|
||||
- `getProductDetail` — GET `product/detail/{id}?type=1`
|
||||
- `postCartAdd` — POST `cart/save`
|
||||
|
||||
### 来源 `api/order.js`(13个)
|
||||
|
||||
- `getCartCounts` — GET `cart/count?numType=true&type=total`
|
||||
- `getCartList` — GET `cart/list`
|
||||
- `changeCartNum` — POST `cart/num?id={id}&number={num}`
|
||||
- `cartDel` — POST `cart/delete?ids={ids}`
|
||||
- `preOrderApi` — POST `order/pre/order`
|
||||
- `orderCreate` — POST `order/create`
|
||||
- `orderPay` — POST `pay/payment`
|
||||
- `orderData` — GET `order/data`
|
||||
- `getOrderList` — GET `order/list`
|
||||
- `getOrderDetail` — GET `order/detail/{orderId}`
|
||||
- `orderCancel` — POST `order/cancel`(body: {id})
|
||||
- `orderTake` — POST `order/take`(body: {id})
|
||||
- `orderDel` — POST `order/del`(body: {id})
|
||||
|
||||
### 来源 `api/user.js`(9个)
|
||||
|
||||
- `loginV2` — POST `loginV2`
|
||||
- `getUserInfo` — GET `user`
|
||||
- `getAddressDefault` — GET `address/default`
|
||||
- `getAddressList` — GET `address/list`
|
||||
- `getAddressDetail` — GET `address/detail/{id}`
|
||||
- `getIntegralUserByAccount` — GET `integral/user/account`(query: {account})
|
||||
- `getIntegralList` — GET `integral/list`
|
||||
- `getWaUserInfo` — POST `wa/user/info`
|
||||
- `getWaSelfBonusList` — GET `wa/selfbonus/list`
|
||||
|
||||
### 各页面使用情况
|
||||
|
||||
- **index.vue**(6个):`loginV2`, `getUserInfo`, `getIntegralUserByAccount`, `getCartCounts`, `getCategoryList`, `getProductslist`
|
||||
- **detail.vue**(3个):`getProductDetail`, `postCartAdd`, `getCartCounts`
|
||||
- **cart.vue**(5个):`getIntegralUserByAccount`, `getCartCounts`, `getCartList`, `changeCartNum`, `cartDel`
|
||||
- **confirm.vue**(9个):`getUserInfo`, `getCartList`, `getProductDetail`, `getAddressDefault`, `getAddressList`, `getAddressDetail`, `preOrderApi`, `orderCreate`, `orderPay`
|
||||
- **orders.vue**(6个):`orderData`, `getCartCounts`, `getOrderList`, `orderCancel`, `orderTake`, `orderDel`
|
||||
- **order-detail.vue**(3个):`getOrderDetail`, `orderCancel`, `orderTake`
|
||||
- **points.vue**(6个):`loginV2`, `getUserInfo`, `getIntegralUserByAccount`, `getWaUserInfo`, `getWaSelfBonusList`, `getIntegralList`
|
||||
- **rules.vue / search.vue**:无 API 调用
|
||||
|
||||
---
|
||||
|
||||
## 二、多商户版本后端现有 API 对应关系
|
||||
|
||||
多商户版本所有前端 API 路径前缀为 `api/front/`(与单商户一致,由 `request.js` 自动拼接)。
|
||||
|
||||
以下为多商户版本关键 Controller 文件位置(现有接口,不做修改):
|
||||
|
||||
- [ProductController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/ProductController.java) — `api/front/product`
|
||||
- [ProductCategoryController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/ProductCategoryController.java) — `api/front/product/category`
|
||||
- [CartController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/CartController.java) — `api/front/cart`
|
||||
- [OrderController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/OrderController.java) — `api/front/order`
|
||||
- [PayController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/PayController.java) — `api/front/pay`
|
||||
- [UserController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/UserController.java) — `api/front/user`
|
||||
- [UserAddressController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/UserAddressController.java) — `api/front/address`
|
||||
- [UserCenterController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/UserCenterController.java) — `api/front/user/center`
|
||||
- [LoginController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/LoginController.java) — `api/front/login`
|
||||
- [IntegralShoppingController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/IntegralShoppingController.java) — `api/front/integral/shopping`
|
||||
|
||||
---
|
||||
|
||||
## 三、后端新增兼容接口详细方案
|
||||
|
||||
**总体思路**:在多商户后端 `crmeb-front` 模块中新建一个兼容控制器 `IntegralCompatController`(路径 `api/front`),以及在现有 Controller 中新增方法,用于接收前端原有的 URL 路径和参数格式,内部委托给多商户已有 Service 实现。
|
||||
|
||||
### A. 商品相关(4个需新增兼容接口)
|
||||
|
||||
**1. `GET products` — 商品列表**
|
||||
|
||||
- 前端调用:`request.get('products', {page, limit, cid, type}, {noAuth: true})`
|
||||
- 多商户已有:`GET product/list` -> `ProductController.getList(ProductFrontSearchRequest)`
|
||||
- 新增方式:在 `ProductController` 中新增方法
|
||||
|
||||
```java
|
||||
@ApiOperation(value = "商品列表(兼容单商户)")
|
||||
@RequestMapping(value = "/products", method = RequestMethod.GET)
|
||||
// 注意:此方法需放在一个 @RequestMapping("api/front") 的兼容控制器中
|
||||
// 因为现有 ProductController 的前缀是 api/front/product
|
||||
```
|
||||
|
||||
由于 `api/front/products` 与 `api/front/product/list` 前缀不同,需要在**新建的兼容控制器**中处理:
|
||||
|
||||
```java
|
||||
// 新建文件:IntegralCompatController.java
|
||||
// 路径:api/front
|
||||
@RestController
|
||||
@RequestMapping("api/front")
|
||||
public class IntegralCompatController {
|
||||
|
||||
@Autowired
|
||||
private FrontProductService productService;
|
||||
|
||||
@ApiOperation(value = "商品列表(兼容单商户路径)")
|
||||
@GetMapping("/products")
|
||||
public CommonResult<CommonPage<ProductFrontResponse>> getProductList(
|
||||
@ModelAttribute ProductFrontSearchRequest request) {
|
||||
// 委托给已有的 productService,前端缺少的参数使用默认值
|
||||
return CommonResult.success(CommonPage.restPage(productService.getList(request)));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- 前端传 `{page, limit, cid, type}`,后端 `ProductFrontSearchRequest` 多余字段使用默认值
|
||||
|
||||
**2. `GET category` — 分类列表**
|
||||
|
||||
- 前端调用:`request.get('category', {}, {noAuth: true})`
|
||||
- 多商户已有:`GET product/category/get/tree` -> `ProductCategoryController.getCategory()`
|
||||
- 新增方式:在 `IntegralCompatController` 中新增
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private ProductCategoryService productCategoryService;
|
||||
|
||||
@ApiOperation(value = "分类列表(兼容单商户路径)")
|
||||
@GetMapping("/category")
|
||||
public CommonResult<List<ProCategoryCacheVo>> getCategory() {
|
||||
return CommonResult.success(productCategoryService.getMerchantCacheTree());
|
||||
}
|
||||
```
|
||||
|
||||
- 注意:返回的树形结构 `ProCategoryCacheVo` 与单商户版可能有差异,需确认前端 `index.vue` 解析逻辑是否兼容。如果字段不同,需在 Service 层做适配转换(新增适配方法)
|
||||
|
||||
**3. `GET product/detail/{id}` — 商品详情**
|
||||
|
||||
- 前端调用:`request.get('product/detail/' + id + '?type=' + type, {}, {noAuth: true})`
|
||||
- 多商户已有:`GET product/detail` (query params) -> `ProductController.getDetail(ProductFrontDetailRequest)`
|
||||
- 新增方式:在现有 `ProductController` 中**新增**一个路径参数方法
|
||||
|
||||
```java
|
||||
// 在 ProductController.java 中新增(不修改原有 getDetail 方法)
|
||||
@ApiOperation(value = "商品详情(兼容单商户路径参数)")
|
||||
@RequestMapping(value = "/detail/{id}", method = RequestMethod.GET)
|
||||
public CommonResult<ProductDetailResponse> getDetailById(
|
||||
@PathVariable Integer id,
|
||||
@RequestParam(defaultValue = "normal") String type) {
|
||||
// marketingType、groupActivityId、groupBuyRecordId 使用默认值
|
||||
return CommonResult.success(productService.getDetail_V1_7(id, type, null, null, null));
|
||||
}
|
||||
```
|
||||
|
||||
- 前端传 `type=1`,后端补充 `marketingType=null`, `groupActivityId=null`, `groupBuyRecordId=null` 默认值
|
||||
|
||||
**4. `POST cart/save` — 加入购物车**
|
||||
|
||||
- 前端调用:`request.post('cart/save', data, {})`
|
||||
- 多商户已有:`POST cart/add` -> `CartController.add(CartRequest)`
|
||||
- 新增方式:在现有 `CartController` 中**新增**方法
|
||||
|
||||
```java
|
||||
// 在 CartController.java 中新增(不修改原有 add 方法)
|
||||
@ApiOperation(value = "添加购物车(兼容单商户路径)")
|
||||
@RequestMapping(value = "/save", method = RequestMethod.POST)
|
||||
public CommonResult<String> save(@RequestBody @Validated CartRequest request) {
|
||||
if (cartService.add(request)) {
|
||||
return CommonResult.success();
|
||||
}
|
||||
return CommonResult.failed();
|
||||
}
|
||||
```
|
||||
|
||||
- 注意:需确认 `CartRequest` 字段是否兼容前端传入的 `{productId, cartNum, isNew, productAttrUnique}`
|
||||
|
||||
### B. 购物车相关(2个需新增兼容接口)
|
||||
|
||||
**5. `POST cart/num?id={id}&number={num}` — 修改购物车数量**
|
||||
|
||||
- 前端调用:`request.post('cart/num?id=${cartId}&number=${number}', {}, opt, 1)`
|
||||
- 多商户已有:`POST cart/num` -> `CartController.update(CartUpdateNumRequest body)`
|
||||
- 问题:前端通过 **query string** 传参且 body 为空,多商户接口要求 **RequestBody**
|
||||
- 新增方式:在 `CartController` 中**新增**一个兼容方法,同时支持 query string 参数
|
||||
|
||||
```java
|
||||
// 在 CartController.java 中新增(不修改原有 update 方法)
|
||||
@ApiOperation(value = "修改购物车数量(兼容query参数)")
|
||||
@RequestMapping(value = "/num", method = RequestMethod.POST, params = {"id", "number"})
|
||||
public CommonResult<String> updateByQuery(
|
||||
@RequestParam("id") Long id,
|
||||
@RequestParam("number") Integer number) {
|
||||
if (cartService.updateCartNum(id, number)) {
|
||||
return CommonResult.success();
|
||||
}
|
||||
return CommonResult.failed();
|
||||
}
|
||||
```
|
||||
|
||||
- 使用 `params = {"id", "number"}` 注解条件区分两个同路径方法:有 query 参数走兼容方法,body 传参走原方法
|
||||
- 如果 Spring 路径冲突,可改为**在原方法中同时支持两种传参**(新增参数,保留原有)
|
||||
|
||||
**6. `POST cart/delete?ids={ids}` — 删除购物车**
|
||||
|
||||
- 前端调用:`request.post('cart/delete?ids=${ids}', {}, opt, 1)`(ids 为逗号分隔字符串)
|
||||
- 多商户已有:`POST cart/delete` -> `CartController.delete(CartDeleteRequest body)`(body: `{ids: [1,2,3]}`)
|
||||
- 新增方式:在 `CartController` 中**新增**兼容方法
|
||||
|
||||
```java
|
||||
// 在 CartController.java 中新增
|
||||
@ApiOperation(value = "删除购物车(兼容query参数)")
|
||||
@RequestMapping(value = "/delete", method = RequestMethod.POST, params = "ids")
|
||||
public CommonResult<String> deleteByQuery(@RequestParam("ids") String ids) {
|
||||
List<Long> idList = Arrays.stream(ids.split(","))
|
||||
.map(Long::parseLong)
|
||||
.collect(Collectors.toList());
|
||||
if (cartService.deleteCartByIds(idList)) {
|
||||
return CommonResult.success();
|
||||
}
|
||||
return CommonResult.failed();
|
||||
}
|
||||
```
|
||||
|
||||
- 前端传逗号分隔字符串 `"1,2,3"`,后端解析为 `List<Long>`
|
||||
|
||||
### C. 订单相关(4个需新增兼容接口)
|
||||
|
||||
**7. `GET order/data` — 订单统计数据**
|
||||
|
||||
- 前端调用:`request.get('order/data')`
|
||||
- 多商户已有:`GET user/center/order/num` -> `UserCenterController.getUserCenterOrderNum()` -> `OrderCenterNumResponse`
|
||||
- 多商户无 `order/data` 接口
|
||||
- 新增方式:在 `OrderController` 中**新增**方法
|
||||
|
||||
```java
|
||||
// 在 OrderController.java 中新增
|
||||
@Autowired
|
||||
private UserCenterService userCenterService;
|
||||
|
||||
@ApiOperation(value = "订单统计数据(兼容单商户路径)")
|
||||
@RequestMapping(value = "/data", method = RequestMethod.GET)
|
||||
public CommonResult<OrderCenterNumResponse> orderData() {
|
||||
return CommonResult.success(userCenterService.getUserCenterOrderNum());
|
||||
}
|
||||
```
|
||||
|
||||
- 注意:`OrderCenterNumResponse` 的字段需要与单商户原有的订单统计数据结构保持兼容。如果不兼容,新增一个适配方法返回单商户格式的数据
|
||||
|
||||
**8. `POST order/cancel` (body: {id}) — 取消订单**
|
||||
|
||||
- 前端调用:`request.post('order/cancel', {id: id}, {}, 1)`
|
||||
- 多商户已有:`POST order/cancel/{orderNo}` -> 路径参数
|
||||
- 新增方式:在 `OrderController` 中**新增**方法,接收 body 中的 id
|
||||
|
||||
```java
|
||||
// 在 OrderController.java 中新增(不修改原有 cancel 方法)
|
||||
@ApiOperation(value = "取消订单(兼容body传参)")
|
||||
@RequestMapping(value = "/cancel", method = RequestMethod.POST)
|
||||
public CommonResult<Boolean> cancelByBody(@RequestBody Map<String, String> params) {
|
||||
String orderNo = params.get("id");
|
||||
if (orderService.cancel(orderNo)) {
|
||||
return CommonResult.success();
|
||||
}
|
||||
return CommonResult.failed();
|
||||
}
|
||||
```
|
||||
|
||||
- 前端传 `{id: orderNo}`,后端从 body 中取出 `id` 作为 `orderNo` 使用
|
||||
- 关键前提:前端传入的 `id` 值实际上需要是 `orderNo`(字符串订单号)。需确认单商户版本的订单列表/详情返回数据中,前端取的是 `id` 还是 `orderNo`
|
||||
|
||||
**9. `POST order/take` (body: {id}) — 确认收货**
|
||||
|
||||
- 前端调用:`request.post('order/take', {id: uni}, {}, 1)`
|
||||
- 多商户已有:`POST order/take/delivery/{orderNo}` -> 路径参数
|
||||
- 新增方式:在 `OrderController` 中**新增**
|
||||
|
||||
```java
|
||||
// 在 OrderController.java 中新增
|
||||
@ApiOperation(value = "确认收货(兼容body传参)")
|
||||
@RequestMapping(value = "/take", method = RequestMethod.POST)
|
||||
public CommonResult<String> takeByBody(@RequestBody Map<String, String> params) {
|
||||
String orderNo = params.get("id");
|
||||
if (orderService.takeDelivery(orderNo)) {
|
||||
return CommonResult.success("订单收货成功");
|
||||
}
|
||||
return CommonResult.failed("订单收货失败");
|
||||
}
|
||||
```
|
||||
|
||||
**10. `POST order/del` (body: {id}) — 删除订单**
|
||||
|
||||
- 前端调用:`request.post('order/del', {id: uni}, {}, 1)`
|
||||
- 多商户已有:`POST order/delete/{orderNo}` -> 路径参数
|
||||
- 新增方式:在 `OrderController` 中**新增**
|
||||
|
||||
```java
|
||||
// 在 OrderController.java 中新增
|
||||
@ApiOperation(value = "删除订单(兼容body传参)")
|
||||
@RequestMapping(value = "/del", method = RequestMethod.POST)
|
||||
public CommonResult<Boolean> deleteByBody(@RequestBody Map<String, String> params) {
|
||||
String orderNo = params.get("id");
|
||||
if (orderService.delete(orderNo)) {
|
||||
return CommonResult.success();
|
||||
}
|
||||
return CommonResult.failed();
|
||||
}
|
||||
```
|
||||
|
||||
### D. 用户/登录相关(3个需新增兼容接口)
|
||||
|
||||
**11. `POST loginV2` — 用户登录**
|
||||
|
||||
- 前端调用:`request.post("loginV2", {account, password, spread_spid}, {noAuth: true})`
|
||||
- 多商户已有:`POST login/mobile/password` -> `LoginController.phonePasswordLogin(LoginPasswordRequest)`
|
||||
- 新增方式:在 `IntegralCompatController`(路径 `api/front`)中新增
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private LoginService loginService;
|
||||
|
||||
@ApiOperation(value = "账号密码登录(兼容单商户loginV2)")
|
||||
@RequestMapping(value = "/loginV2", method = RequestMethod.POST)
|
||||
public CommonResult<LoginResponse> loginV2(@RequestBody Map<String, String> params) {
|
||||
// 从前端参数 account/password 映射到多商户的 LoginPasswordRequest
|
||||
LoginPasswordRequest loginRequest = new LoginPasswordRequest();
|
||||
loginRequest.setPhone(params.get("account"));
|
||||
loginRequest.setPassword(params.get("password"));
|
||||
// spread_spid 如有需要可在 Service 层处理,此处忽略或新增字段
|
||||
return CommonResult.success(loginService.phonePasswordLogin(loginRequest));
|
||||
}
|
||||
```
|
||||
|
||||
- 注意:`LoginPasswordRequest` 需确认字段名(`phone` vs `account`)。如果字段差异大,可在此方法中做转换
|
||||
- `spread_spid` 参数在多商户版可能无对应字段,后端忽略即可
|
||||
|
||||
**12. `GET user` — 获取用户信息**
|
||||
|
||||
- 前端调用:`request.get('user')`
|
||||
- 多商户已有:`GET user/info` -> `UserController.getUserCenter()` -> `UserInfoResponse`
|
||||
- 问题:`UserController` 的 `@RequestMapping` 前缀是 `api/front/user`,前端请求 `api/front/user`(无后缀)
|
||||
- 新增方式:在 `UserController` 中新增一个空路径映射
|
||||
|
||||
```java
|
||||
// 在 UserController.java 中新增(不修改原有 /info 方法)
|
||||
@ApiOperation(value = "用户信息(兼容单商户路径)")
|
||||
@RequestMapping(value = "", method = RequestMethod.GET)
|
||||
public CommonResult<UserInfoResponse> getUserInfoCompat() {
|
||||
return CommonResult.success(userService.getUserInfo());
|
||||
}
|
||||
```
|
||||
|
||||
- 这样 `GET api/front/user` 和 `GET api/front/user/info` 都能正常访问
|
||||
|
||||
**13. `GET address/default` — 获取默认地址**
|
||||
|
||||
- 前端调用:`request.get('address/default')`
|
||||
- 多商户已有:`GET address/get/default` -> `UserAddressController.getDefault()`
|
||||
- 新增方式:在 `UserAddressController` 中新增
|
||||
|
||||
```java
|
||||
// 在 UserAddressController.java 中新增(不修改原有 /get/default 方法)
|
||||
@ApiOperation(value = "获取默认地址(兼容单商户路径)")
|
||||
@RequestMapping(value = "/default", method = RequestMethod.GET)
|
||||
public CommonResult<UserAddress> getDefaultCompat() {
|
||||
return CommonResult.success(userAddressService.getDefault());
|
||||
}
|
||||
```
|
||||
|
||||
### E. 积分相关(2个需新增兼容接口)
|
||||
|
||||
**14. `GET integral/list` — 积分记录**
|
||||
|
||||
- 前端调用:`request.get("integral/list", {page, limit})`
|
||||
- 多商户已有:`GET user/center/integral/list` -> `UserCenterController.getIntegralList(PageParamRequest)`
|
||||
- 新增方式:在 `IntegralCompatController` 中新增
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private UserCenterService userCenterService;
|
||||
|
||||
@ApiOperation(value = "积分记录(兼容单商户路径)")
|
||||
@GetMapping("/integral/list")
|
||||
public CommonResult<CommonPage<UserIntegralRecord>> getIntegralList(
|
||||
@Validated PageParamRequest pageParamRequest) {
|
||||
return CommonResult.success(
|
||||
CommonPage.restPage(userCenterService.getUserIntegralRecordList(pageParamRequest)));
|
||||
}
|
||||
```
|
||||
|
||||
**15. `GET integral/user/account` — 根据账户获取用户积分信息**
|
||||
|
||||
- 前端调用:`request.get("integral/user/account", {account: account}, {noAuth: true})`
|
||||
- 多商户已有:`GET integral/shopping/get/user/integral`(仅获取当前登录用户积分,无 account 参数)
|
||||
- 差异:单商户版通过外部 `account` 参数查询指定用户积分,多商户版无此功能
|
||||
- 新增方式:在 `IntegralCompatController` 中**新增完整接口**
|
||||
|
||||
```java
|
||||
@Autowired
|
||||
private IntegralShoppingService integralShoppingService;
|
||||
// 可能需要注入 UserService 来根据 account 查找用户
|
||||
|
||||
@ApiOperation(value = "根据账户获取用户积分信息(兼容单商户)")
|
||||
@GetMapping("/integral/user/account")
|
||||
public CommonResult<Map<String, Object>> getIntegralUserByAccount(
|
||||
@RequestParam(value = "account", required = false) String account) {
|
||||
// 方案一:如果积分商城只需当前登录用户积分,直接调用已有方法
|
||||
return CommonResult.success(integralShoppingService.getUserIntegralInfo());
|
||||
// 方案二:如果确实需要按 account 查询,需在 Service 层新增方法
|
||||
}
|
||||
```
|
||||
|
||||
- 需确认业务:前端传的 `account` 是当前登录用户自己的 account 还是别人的。如果是自己的,方案一即可;如果需要查询他人,需在 Service 层新增按 account 查询的方法
|
||||
|
||||
### F. WA 寄卖相关(2个全新接口 + Service/Mapper)
|
||||
|
||||
**16. `POST wa/user/info` — WA 寄卖商城用户信息**
|
||||
|
||||
- 多商户版**完全不存在**此功能
|
||||
- 需从单商户版 [WaUserController.java](crmeb_22miao/crmeb-front/src/main/java/com/zbkj/front/controller/WaUserController.java) 迁移
|
||||
- 新增文件:
|
||||
- `WaUserController.java`(路径 `api/front/wa/user`)
|
||||
- 对应的 Service、ServiceImpl、Mapper、Model(如需数据库操作)
|
||||
|
||||
**17. `GET wa/selfbonus/list` — 个人奖金记录列表**
|
||||
|
||||
- 多商户版**完全不存在**此功能
|
||||
- 需从单商户版 [WaSelfbonusController.java](crmeb_22miao/crmeb-front/src/main/java/com/zbkj/front/controller/WaSelfbonusController.java) 迁移
|
||||
- 新增文件:
|
||||
- `WaSelfbonusController.java`(路径 `api/front/wa/selfbonus`)
|
||||
- 对应的 Service、ServiceImpl、Mapper、Model
|
||||
|
||||
### G. 已兼容、无需改动的接口(9个)
|
||||
|
||||
以下接口路径和参数格式在多商户版本中已经兼容(或仅有 query string 拼接方式差异但不影响):
|
||||
|
||||
- `getCartCounts` — GET `cart/count?numType=true&type=total`(query string 会自动映射到 `CartNumRequest` 对象属性,无需改动)
|
||||
- `getCartList` — GET `cart/list`(路径完全一致)
|
||||
- `preOrderApi` — POST `order/pre/order`(路径完全一致)
|
||||
- `orderCreate` — POST `order/create`(路径完全一致)
|
||||
- `orderPay` — POST `pay/payment`(路径完全一致)
|
||||
- `getOrderList` — GET `order/list`(路径完全一致)
|
||||
- `getOrderDetail` — GET `order/detail/{orderId}`(路径格式一致,多商户用 `{orderNo}` 但 String 类型兼容)
|
||||
- `getAddressList` — GET `address/list`(完全一致)
|
||||
- `getAddressDetail` — GET `address/detail/{id}`(完全一致)
|
||||
|
||||
---
|
||||
|
||||
## 四、关键注意事项
|
||||
|
||||
### 1. 返回数据结构兼容性(重点排查)
|
||||
|
||||
虽然前端 JS 文件不修改,但多商户版本接口返回的数据结构可能与单商户版不同。需在**后端 Service 层**做适配,确保返回字段兼容:
|
||||
|
||||
- **购物车列表** `cart/list`:多商户返回 `List<CartMerchantResponse>`(按商户分组),单商户版返回扁平列表。需确认前端 `cart.vue` 的数据解析逻辑,必要时在后端新增适配方法将分组数据打平
|
||||
- **分类列表** `category`:多商户返回 `ProCategoryCacheVo` 树形结构,需确认与单商户版 `category` 返回的字段是否一致(如 `id`, `name`, `children` 等字段名)
|
||||
- **订单统计** `order/data`:需确认 `OrderCenterNumResponse` 的字段名(如 `unPaidCount`, `unShippedCount` 等)与单商户版订单统计的字段名是否一致
|
||||
- **用户信息** `user`:`UserInfoResponse` 字段可能新增了商户相关内容,但只增不删即可兼容
|
||||
- **订单列表/详情**:`OrderFrontDataResponse` 和 `OrderFrontDetailResponse` 可能包含新增的商户字段,前端未使用的字段不会产生影响
|
||||
|
||||
### 2. 订单标识 id vs orderNo
|
||||
|
||||
- 单商户版前端在订单操作时传的 `id` 值,实际内容是什么?(数字 ID 还是订单号字符串?)
|
||||
- 多商户版统一使用 `orderNo`(字符串)
|
||||
- **需排查确认**:单商户版订单列表接口返回的数据中,前端取用的 `id` 字段的实际值。如果单商户版的 `id` 就是订单号字符串,则兼容无问题;如果是数字 ID,需要在后端兼容接口中做转换(根据 id 查 orderNo)
|
||||
|
||||
### 3. 前端请求方式特殊处理
|
||||
|
||||
- `changeCartNum` 和 `cartDel` 使用了 query string 拼接在 URL 中 + 空 body 的方式(`request.post('cart/num?id=1&number=2', {}, opt, 1)`)
|
||||
- 后端兼容接口需用 `@RequestParam` 接收而非 `@RequestBody`
|
||||
- 如果 Spring MVC 因同路径 `/num` 产生冲突(一个接收 RequestBody,一个接收 RequestParam),可考虑:
|
||||
- 方案 A:使用 `params` 条件区分(`@RequestMapping(params = "id")`)
|
||||
- 方案 B:修改原有方法,同时支持 query param 和 body(通过方法参数设置 `required = false`)
|
||||
- 方案 C:在兼容方法中判断参数来源,统一处理
|
||||
|
||||
### 4. loginV2 的 spread_spid 参数
|
||||
|
||||
- 单商户前端传 `{account, password, spread_spid}`
|
||||
- 多商户 `LoginPasswordRequest` 中可能无 `spread_spid` 字段
|
||||
- 处理方式:在 `LoginPasswordRequest` 中**新增** `spreadSpid` 属性(只增不删),或在兼容接口中忽略此参数
|
||||
|
||||
### 5. 新增兼容接口不影响多商户原有前端
|
||||
|
||||
- 所有新增的兼容接口都是额外的 URL 映射,不修改原有方法签名和逻辑
|
||||
- 多商户原有前端(如有)仍然调用原路径,不受影响
|
||||
- 仅积分商城前端通过旧路径访问兼容接口
|
||||
|
||||
---
|
||||
|
||||
## 五、改动汇总
|
||||
|
||||
### 前端(零修改)
|
||||
|
||||
- `api/store.js` — 不修改
|
||||
- `api/order.js` — 不修改
|
||||
- `api/user.js` — 不修改
|
||||
- `pages/integral/*.vue` — 不修改
|
||||
|
||||
### 后端新增文件
|
||||
|
||||
- **新建** `IntegralCompatController.java`(路径 `api/front`):承载 `GET /products`、`GET /category`、`POST /loginV2`、`GET /integral/list`、`GET /integral/user/account` 共 5 个兼容接口
|
||||
- **新建** `WaUserController.java`(路径 `api/front/wa/user`):从单商户迁移
|
||||
- **新建** `WaSelfbonusController.java`(路径 `api/front/wa/selfbonus`):从单商户迁移
|
||||
- **新建** WA 相关 Service/ServiceImpl/Mapper/Model 文件
|
||||
|
||||
### 后端现有文件新增方法(不修改原有方法)
|
||||
|
||||
- [ProductController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/ProductController.java):新增 `GET /detail/{id}` 兼容方法
|
||||
- [CartController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/CartController.java):新增 `POST /save`、`POST /num`(query param)、`POST /delete`(query param)共 3 个兼容方法
|
||||
- [OrderController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/OrderController.java):新增 `GET /data`、`POST /cancel`(body)、`POST /take`(body)、`POST /del`(body)共 4 个兼容方法
|
||||
- [UserController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/UserController.java):新增 `GET ""` 空路径兼容方法
|
||||
- [UserAddressController.java](MER-2.2_2601/mer_java/crmeb-front/src/main/java/com/zbkj/front/controller/UserAddressController.java):新增 `GET /default` 兼容方法
|
||||
|
||||
### 后端可能需要新增的 Service 适配方法
|
||||
|
||||
- 如果返回数据字段不兼容,在对应 Service 中新增适配方法(如 `getListCompat()`),不修改原有方法
|
||||
- 如果 `integral/user/account` 需要按 account 查询功能,在 `IntegralShoppingService` 中新增方法
|
||||
|
||||
510
crmeb_22miao/.cursor/plans/kafka_sync_test_plan_acd87e7e.plan.md
Normal file
510
crmeb_22miao/.cursor/plans/kafka_sync_test_plan_acd87e7e.plan.md
Normal file
@@ -0,0 +1,510 @@
|
||||
---
|
||||
name: Kafka Sync Test Plan
|
||||
overview: Comprehensive test plan covering unit tests, integration tests, end-to-end verification, and edge case/error scenarios for the Kafka-based order/user sync system between single-merchant (crmeb_22miao) and multi-merchant (MER-2.2_2601) platforms.
|
||||
todos:
|
||||
- id: fix-bug-sourceid
|
||||
content: 修复 OrderSyncProducerServiceImpl.convertStatusToMessage 中 msg.setSourceId(msg.getSourceId()) 改为 msg.setSourceId(sourceId)
|
||||
status: completed
|
||||
- id: env-setup
|
||||
content: 执行 sync_staging_tables.sql 建表,插入 eb_sync_merchant_config 测试数据,创建 Kafka 4个 topic,清理 Redis sync key
|
||||
status: completed
|
||||
- id: unit-test-mapping
|
||||
content: 对 OrderStagingProcessServiceImpl 的 mapOrderStatus/mapPayChannel/mapSecondType 等方法编写 JUnit 单元测试
|
||||
status: completed
|
||||
- id: producer-integration-test
|
||||
content: 触发 syncOrders()/syncUsers(),用 kafka-console-consumer 验证消息内容和 Redis 时间戳更新
|
||||
status: completed
|
||||
- id: consumer-integration-test
|
||||
content: 手动向 Kafka topic 发送测试消息,验证 4 张 staging 表数据写入(sync_status=0)
|
||||
status: completed
|
||||
- id: e2e-test
|
||||
content: 完整端到端测试:user→order→detail→status 全链路,验证 eb_order/eb_merchant_order/eb_order_detail/eb_order_flow_record 字段正确性
|
||||
status: completed
|
||||
- id: error-scenario-test
|
||||
content: 测试异常场景:用户不存在时订单 staging 变 failed、重复消息幂等性、重试逻辑
|
||||
status: completed
|
||||
- id: sync-log-verify
|
||||
content: 验证 eb_sync_log 每批次记录完整,status/success_count/fail_count 统计正确
|
||||
status: completed
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Kafka 订单同步系统测试方案
|
||||
|
||||
## 测试范围
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph P [Producer - crmeb_22miao]
|
||||
PT[OrderSyncProducerTask]
|
||||
PS[OrderSyncProducerServiceImpl]
|
||||
Redis[(Redis\nlast_sync_time)]
|
||||
end
|
||||
|
||||
subgraph K [Kafka Broker\n118.31.75.148:9092]
|
||||
K1[order-sync-topic]
|
||||
K2[order-detail-sync-topic]
|
||||
K3[order-status-sync-topic]
|
||||
K4[user-sync-topic]
|
||||
end
|
||||
|
||||
subgraph C [Consumer - MER-2.2_2601]
|
||||
OC[OrderSyncConsumer]
|
||||
subgraph ST [Staging Tables]
|
||||
S1[eb_sync_order_staging]
|
||||
S2[eb_sync_order_detail_staging]
|
||||
S3[eb_sync_order_status_staging]
|
||||
S4[eb_sync_user_staging]
|
||||
end
|
||||
OPT[OrderStagingProcessTask]
|
||||
OPS[OrderStagingProcessServiceImpl]
|
||||
subgraph TT [Target Tables]
|
||||
T1[eb_order]
|
||||
T2[eb_merchant_order]
|
||||
T3[eb_order_detail]
|
||||
T4[eb_order_flow_record]
|
||||
T5[eb_user]
|
||||
end
|
||||
end
|
||||
|
||||
PT --> PS --> Redis
|
||||
PS --> K1 & K2 & K3 & K4
|
||||
K1 --> OC --> S1
|
||||
K2 --> OC --> S2
|
||||
K3 --> OC --> S3
|
||||
K4 --> OC --> S4
|
||||
OPT --> OPS --> TT
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 零、测试环境
|
||||
|
||||
### 连接信息汇总
|
||||
|
||||
|
||||
| 系统 | MySQL Host | DB Name | 用户名 | Redis Host | Redis DB |
|
||||
| --------------------------- | --------------------- | ---------------- | ---------------- | --------------------- | -------- |
|
||||
| shop_a (profile: miaoay161) | `101.201.54.161:3306` | `yangtangyoupin` | `yangtangyoupin` | `101.201.54.161:6379` | `2` |
|
||||
| shop_b (profile: miao33) | `39.106.63.33:3306` | `yangtangyoupin` | `yangtangyoupin` | `39.106.63.33:6379` | `2` |
|
||||
| 多商户 (profile: dev) | `118.31.75.148:3306` | `shop22` | `shop22` | `118.31.75.148:6379` | `2` |
|
||||
|
||||
|
||||
> **注意**:shop_a 和 shop_b 使用**相同的数据库名** `yangtangyoupin`,仅服务器不同。因此 `source_id` 前缀(`S_shop_a_` / `S_shop_b_`)是防止目标库订单号冲突的唯一隔离手段,需重点验证。
|
||||
|
||||
MySQL 密码均为 `5Fn8eWrbYFtAhCZw`,Redis 密码均为 `123456`。
|
||||
|
||||
### MySQL 客户端连接命令
|
||||
|
||||
```bash
|
||||
# 多商户 DB(执行 staging 建表、验证 SQL)
|
||||
mysql -h118.31.75.148 -P3306 -ushop22 -p5Fn8eWrbYFtAhCZw shop22
|
||||
|
||||
# 单商户 shop_a DB(查询源订单/用户)
|
||||
mysql -h101.201.54.161 -P3306 -uyangtangyoupin -p5Fn8eWrbYFtAhCZw yangtangyoupin
|
||||
|
||||
# 单商户 shop_b DB
|
||||
mysql -h39.106.63.33 -P3306 -uyangtangyoupin -p5Fn8eWrbYFtAhCZw yangtangyoupin
|
||||
```
|
||||
|
||||
### Kafka & 服务端口
|
||||
|
||||
- Kafka Broker: `118.31.75.148:9092`(与多商户同一服务器)
|
||||
- 多商户服务端口: `20800`
|
||||
- shop_a 服务端口: `30032`(profile: miaoay161)
|
||||
- shop_b 服务端口: `30032`(profile: miao33,需与 shop_a 分开部署)
|
||||
|
||||
---
|
||||
|
||||
## 一、已知 Bug(测试前需先修复)
|
||||
|
||||
### Bug 1: `convertStatusToMessage` 中 `sourceId` 未设置
|
||||
|
||||
- 文件: `[crmeb_22miao/crmeb-service/.../OrderSyncProducerServiceImpl.java](crmeb_22miao/crmeb-service/src/main/java/com/zbkj/service/service/sync/impl/OrderSyncProducerServiceImpl.java)`
|
||||
- 问题: `msg.setSourceId(msg.getSourceId())` 应改为 `msg.setSourceId(sourceId)`
|
||||
- 影响: 订单状态消息的 `sourceId` 始终为 null,consumer 写入 staging 时 `source_id` 为空,导致 `eb_sync_order_status_staging` 中所有状态记录无法关联来源商户
|
||||
|
||||
---
|
||||
|
||||
## 二、环境准备
|
||||
|
||||
### 2.1 数据库初始化
|
||||
|
||||
连接多商户数据库(`118.31.75.148:3306`,库名 `shop22`)执行建表脚本:
|
||||
|
||||
```bash
|
||||
mysql -h118.31.75.148 -P3306 -ushop22 -p5Fn8eWrbYFtAhCZw shop22 < db/sync_staging_tables.sql
|
||||
```
|
||||
|
||||
插入测试商户配置(shop_a 对应商户ID=1,shop_b 对应商户ID=2,请按多商户系统实际 `eb_merchant.id` 调整):
|
||||
|
||||
```sql
|
||||
-- 连接多商户 DB 后执行
|
||||
INSERT IGNORE INTO eb_sync_merchant_config (source_id, source_name, target_mer_id, status)
|
||||
VALUES
|
||||
('shop_a', '单商户A(101.201.54.161)', 1, 1),
|
||||
('shop_b', '单商户B(39.106.63.33)', 2, 1);
|
||||
|
||||
-- 验证
|
||||
SELECT * FROM eb_sync_merchant_config;
|
||||
```
|
||||
|
||||
### 2.2 Redis 状态清除(每次测试前)
|
||||
|
||||
每个单商户系统有独立的 Redis 实例,需分别清除。各系统均使用 DB 2:
|
||||
|
||||
```bash
|
||||
# shop_a 的 Redis(101.201.54.161,DB 2)
|
||||
redis-cli -h 101.201.54.161 -a 123456 -n 2 DEL sync:last_order_time:shop_a
|
||||
redis-cli -h 101.201.54.161 -a 123456 -n 2 DEL sync:last_user_time:shop_a
|
||||
|
||||
# shop_b 的 Redis(39.106.63.33,DB 2)
|
||||
redis-cli -h 39.106.63.33 -a 123456 -n 2 DEL sync:last_order_time:shop_b
|
||||
redis-cli -h 39.106.63.33 -a 123456 -n 2 DEL sync:last_user_time:shop_b
|
||||
```
|
||||
|
||||
验证清除结果:
|
||||
|
||||
```bash
|
||||
redis-cli -h 101.201.54.161 -a 123456 -n 2 EXISTS sync:last_order_time:shop_a
|
||||
# 期望返回 0
|
||||
```
|
||||
|
||||
### 2.3 Kafka Topic 确认
|
||||
|
||||
```bash
|
||||
kafka-topics.sh --bootstrap-server 118.31.75.148:9092 --list
|
||||
# 确认存在以下4个 topic:
|
||||
# order-sync-topic
|
||||
# order-detail-sync-topic
|
||||
# order-status-sync-topic
|
||||
# user-sync-topic
|
||||
```
|
||||
|
||||
若不存在,手动创建:
|
||||
|
||||
```bash
|
||||
kafka-topics.sh --bootstrap-server 118.31.75.148:9092 --create \
|
||||
--topic order-sync-topic --partitions 3 --replication-factor 1
|
||||
# 其余3个 topic 同理
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、单元测试(字段映射逻辑)
|
||||
|
||||
针对 `OrderStagingProcessServiceImpl` 中的映射方法编写 JUnit 测试,无需启动 Spring 容器。
|
||||
|
||||
### 3.1 订单状态映射 `mapOrderStatus(paid, status)`
|
||||
|
||||
|
||||
| paid | status | 预期 multi status | 描述 |
|
||||
| ---- | ------ | --------------- | --- |
|
||||
| 0 | 任意 | 0 | 待支付 |
|
||||
| 1 | 0 | 1 | 待发货 |
|
||||
| 1 | 1 | 4 | 待收货 |
|
||||
| 1 | 2 | 5 | 已收货 |
|
||||
| 1 | 3 | 6 | 已完成 |
|
||||
|
||||
|
||||
### 3.2 支付渠道映射 `mapPayChannel(isChannel)`
|
||||
|
||||
|
||||
| isChannel | 预期 payChannel |
|
||||
| --------- | ------------- |
|
||||
| 0 | "public" |
|
||||
| 1 | "mini" |
|
||||
| 2 | "yue" |
|
||||
| null/其他 | 默认值 |
|
||||
|
||||
|
||||
### 3.3 订单号前缀规则
|
||||
|
||||
- 输入: `sourceId="shop_a"`, `orderNo="202502240001"`
|
||||
- 预期 target order_no: `"S_shop_a_202502240001"`
|
||||
- 验证: `eb_order.order_no` 与 `eb_merchant_order.order_no` 均为该值
|
||||
|
||||
### 3.4 `eb_order.level` 和 `plat_order_no`
|
||||
|
||||
- `eb_order.level` 必须为 `0`(平台单)
|
||||
- `eb_order.plat_order_no` 必须等于 `eb_order.order_no`(自引用)
|
||||
|
||||
---
|
||||
|
||||
## 四、集成测试(单模块)
|
||||
|
||||
### 4.1 Producer 测试
|
||||
|
||||
**目标**: 验证 `OrderSyncProducerServiceImpl.syncOrders()` 正确从 DB 读取并发送消息
|
||||
|
||||
**环境**: 以 shop_a 为例,服务使用 profile `miaoay161`,源 DB 为 `yangtangyoupin@101.201.54.161`
|
||||
|
||||
步骤:
|
||||
|
||||
1. 在 shop_a 单商户 DB 准备 3 条测试订单(不同状态: paid=0, paid=1/status=0, paid=1/status=3):
|
||||
|
||||
```bash
|
||||
mysql -h101.201.54.161 -P3306 -uyangtangyoupin -p5Fn8eWrbYFtAhCZw yangtangyoupin
|
||||
```
|
||||
|
||||
1. 清除 shop_a 的 Redis 时间戳(确保全量同步):
|
||||
|
||||
```bash
|
||||
redis-cli -h 101.201.54.161 -a 123456 -n 2 DEL sync:last_order_time:shop_a
|
||||
redis-cli -h 101.201.54.161 -a 123456 -n 2 DEL sync:last_user_time:shop_a
|
||||
```
|
||||
|
||||
1. 调用 `syncOrders()`(等待定时任务触发,或通过 Swagger 手动调用)
|
||||
2. 使用 kafka-console-consumer 验证消息:
|
||||
|
||||
```bash
|
||||
kafka-console-consumer.sh --bootstrap-server 118.31.75.148:9092 \
|
||||
--topic order-sync-topic --from-beginning --max-messages 10
|
||||
```
|
||||
|
||||
验证要点:
|
||||
|
||||
- 消息 JSON 包含 `sourceId="shop_a"`, `targetMerId=1`
|
||||
- `orderNo` 字段与 DB 中 `order_id` 一致
|
||||
- 同时发送对应的 detail 和 status 消息到各自 topic
|
||||
- Redis 时间戳已更新:
|
||||
|
||||
```bash
|
||||
redis-cli -h 101.201.54.161 -a 123456 -n 2 GET sync:last_order_time:shop_a
|
||||
# 期望返回非空时间戳(毫秒)
|
||||
```
|
||||
|
||||
### 4.2 增量同步测试
|
||||
|
||||
步骤:
|
||||
|
||||
1. 执行一次 `syncOrders()`,记录 Redis 中的时间戳 T1:
|
||||
|
||||
```bash
|
||||
redis-cli -h 101.201.54.161 -a 123456 -n 2 GET sync:last_order_time:shop_a
|
||||
```
|
||||
|
||||
1. 在 shop_a DB(`yangtangyoupin@101.201.54.161`)新增 1 条订单(`update_time > T1`)
|
||||
2. 再次执行 `syncOrders()`
|
||||
3. 验证 Kafka 中只有新增的 1 条订单消息(不重复发旧数据)
|
||||
|
||||
### 4.3 Consumer 测试
|
||||
|
||||
**目标**: 验证消息正确写入 staging 表
|
||||
|
||||
步骤:
|
||||
|
||||
1. 向 `order-sync-topic` 手动发送一条测试消息:
|
||||
|
||||
```bash
|
||||
kafka-console-producer.sh --bootstrap-server 118.31.75.148:9092 \
|
||||
--topic order-sync-topic
|
||||
# 输入 JSON (见下方模板)
|
||||
```
|
||||
|
||||
测试消息模板:
|
||||
|
||||
```json
|
||||
{
|
||||
"sourceId": "shop_a", "targetMerId": 1,
|
||||
"sourceOrderId": 9999, "orderNo": "TEST202502240001",
|
||||
"uid": 1, "totalPrice": 99.00, "payPrice": 89.00,
|
||||
"paid": true, "status": 0, "isChannel": 1,
|
||||
"realName": "测试用户", "userPhone": "13800138000"
|
||||
}
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
```sql
|
||||
SELECT * FROM eb_sync_order_staging
|
||||
WHERE order_no = 'TEST202502240001';
|
||||
-- sync_status 应为 0, source_id='shop_a', target_mer_id=1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、端到端测试(全链路)
|
||||
|
||||
### 5.1 正常订单同步全链路
|
||||
|
||||
**前置条件**: 用户先同步(order processing 依赖 user 存在)
|
||||
|
||||
**源库**: shop_a,`yangtangyoupin@101.201.54.161:3306`,profile `miaoay161`
|
||||
**目标库**: 多商户,`shop22@118.31.75.148:3306`,profile `dev`
|
||||
|
||||
步骤:
|
||||
|
||||
1. 在 shop_a 单商户 DB 准备用户数据(phone: `13900139001`)和关联订单:
|
||||
|
||||
```bash
|
||||
mysql -h101.201.54.161 -P3306 -uyangtangyoupin -p5Fn8eWrbYFtAhCZw yangtangyoupin
|
||||
```
|
||||
|
||||
1. 触发 `syncUsers()` → 等待 consumer 写入 `eb_sync_user_staging`
|
||||
2. 触发多商户 `processUsers()` → 验证 `eb_user` 已创建
|
||||
3. 触发 `syncOrders()` → 等待 consumer 写入 3 个 staging 表
|
||||
4. 触发多商户 `processOrders()` → 触发 `processOrderDetails()` → 触发 `processOrderStatuses()`
|
||||
|
||||
**验证 SQL(在多商户 DB `shop22@118.31.75.148` 执行)**:
|
||||
|
||||
```sql
|
||||
-- 1. 检查订单主表
|
||||
SELECT order_no, status, pay_channel, level, plat_order_no, mer_id
|
||||
FROM eb_order WHERE order_no LIKE 'S_shop_a_%';
|
||||
|
||||
-- 2. 检查商户订单
|
||||
SELECT order_no, mer_id, delivery_type
|
||||
FROM eb_merchant_order WHERE order_no LIKE 'S_shop_a_%';
|
||||
|
||||
-- 3. 检查订单详情
|
||||
SELECT order_no, product_name, pay_num, gain_integral
|
||||
FROM eb_order_detail WHERE order_no LIKE 'S_shop_a_%';
|
||||
|
||||
-- 4. 检查流水记录
|
||||
SELECT order_no, flow_type, flow_message, operator_type
|
||||
FROM eb_order_flow_record WHERE order_no LIKE 'S_shop_a_%';
|
||||
|
||||
-- 5. 检查同步状态全部为已完成
|
||||
SELECT sync_status, COUNT(*) FROM eb_sync_order_staging GROUP BY sync_status;
|
||||
SELECT sync_status, COUNT(*) FROM eb_sync_order_detail_staging GROUP BY sync_status;
|
||||
SELECT sync_status, COUNT(*) FROM eb_sync_order_status_staging GROUP BY sync_status;
|
||||
SELECT sync_status, COUNT(*) FROM eb_sync_user_staging GROUP BY sync_status;
|
||||
```
|
||||
|
||||
**字段级验证(抽查)**:
|
||||
|
||||
```sql
|
||||
-- 验证 paid=1,status=1 的订单 -> eb_order.status=4
|
||||
SELECT o.order_no, o.status
|
||||
FROM eb_order o
|
||||
JOIN eb_sync_order_staging s ON o.order_no = CONCAT('S_shop_a_', s.order_no)
|
||||
WHERE s.paid = 1 AND s.status = 1;
|
||||
-- 期望 o.status = 4
|
||||
|
||||
-- 验证 is_channel=1 -> pay_channel='mini'
|
||||
SELECT o.order_no, o.pay_channel
|
||||
FROM eb_order o
|
||||
JOIN eb_sync_order_staging s ON o.order_no = CONCAT('S_shop_a_', s.order_no)
|
||||
WHERE s.is_channel = 1;
|
||||
-- 期望 o.pay_channel = 'mini'
|
||||
```
|
||||
|
||||
### 5.2 多商户来源测试(shop_b)
|
||||
|
||||
**源库**: shop_b,`yangtangyoupin@39.106.63.33:3306`,profile `miao33`
|
||||
|
||||
> **重要**: shop_b 与 shop_a 使用**同一数据库名** `yangtangyoupin`,只是 MySQL 和 Redis 服务器不同(`39.106.63.33`)。两个系统的订单 ID、用户 ID 可能存在数值重叠,`source_id` 前缀(`S_shop_a_` vs `S_shop_b_`)是目标库中唯一的隔离手段,必须验证其正确性。
|
||||
|
||||
步骤:
|
||||
|
||||
1. 使用 profile `miao33` 启动 shop_b 单商户服务,该 profile 中 `sync.source-id=shop_b`, `sync.target-mer-id=2`(按 `application.yml` 中 sync 配置确认)
|
||||
2. 在 shop_b DB(`yangtangyoupin@39.106.63.33`)准备测试数据,订单号可与 shop_a 相同以验证隔离效果:
|
||||
|
||||
```bash
|
||||
mysql -h39.106.63.33 -P3306 -uyangtangyoupin -p5Fn8eWrbYFtAhCZw yangtangyoupin
|
||||
```
|
||||
|
||||
1. 清除 shop_b Redis 时间戳:
|
||||
|
||||
```bash
|
||||
redis-cli -h 39.106.63.33 -a 123456 -n 2 DEL sync:last_order_time:shop_b
|
||||
redis-cli -h 39.106.63.33 -a 123456 -n 2 DEL sync:last_user_time:shop_b
|
||||
```
|
||||
|
||||
1. 触发 shop_b 的 `syncOrders()`,重复 5.1 全链路
|
||||
2. 验证两套数据前缀不同、互不干扰(在多商户 DB `shop22@118.31.75.148` 执行):
|
||||
|
||||
```sql
|
||||
-- 两组数据应各自独立
|
||||
SELECT order_no FROM eb_order WHERE order_no LIKE 'S_shop_a_%';
|
||||
SELECT order_no FROM eb_order WHERE order_no LIKE 'S_shop_b_%';
|
||||
|
||||
-- 重点:若 shop_a 和 shop_b 源库中有相同原始 order_no(如 '20250101001'),
|
||||
-- 目标库应生成两条不同记录:S_shop_a_20250101001 和 S_shop_b_20250101001
|
||||
SELECT order_no FROM eb_order
|
||||
WHERE order_no IN ('S_shop_a_20250101001', 'S_shop_b_20250101001');
|
||||
-- 期望:返回2条,各自独立
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、异常与重试测试
|
||||
|
||||
### 6.1 用户不存在时订单处理失败
|
||||
|
||||
步骤:
|
||||
|
||||
1. 清空 `eb_user`(或确保目标用户不存在)
|
||||
2. 触发订单处理 `processOrders()`
|
||||
3. 验证:
|
||||
|
||||
```sql
|
||||
-- staging 记录应变为 failed(3),有错误信息
|
||||
SELECT sync_status, sync_error, retry_count
|
||||
FROM eb_sync_order_staging WHERE sync_status = 3;
|
||||
```
|
||||
|
||||
### 6.2 重复消费(幂等性)
|
||||
|
||||
步骤:
|
||||
|
||||
1. 向 Kafka 发送同一订单消息两次(相同 `order_no`)
|
||||
2. 触发 consumer 消费
|
||||
3. 触发 `processOrders()`
|
||||
4. 验证 `eb_order` 中该 `order_no` 只有一条记录(处理层有重复检查)
|
||||
|
||||
### 6.3 Kafka 断开后重连
|
||||
|
||||
步骤:
|
||||
|
||||
1. 暂停 Kafka broker(或模拟网络中断)
|
||||
2. 触发 `syncOrders()`,观察日志是否有重试(`retries: 3` 已配置)
|
||||
3. 恢复 Kafka,验证数据最终落入 staging 表
|
||||
|
||||
### 6.4 Producer 日志验证
|
||||
|
||||
订单同步完成后,日志应包含:
|
||||
|
||||
```
|
||||
订单同步完成: orders=N, details=M, statuses=K
|
||||
```
|
||||
|
||||
staging 处理完成后,日志应包含:
|
||||
|
||||
```
|
||||
订单中间表处理完成: total=N, success=N, fail=0, skip=0
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、同步日志验证
|
||||
|
||||
每批次处理后,`eb_sync_log` 应有记录:
|
||||
|
||||
```sql
|
||||
SELECT source_id, sync_type, total_count, success_count, fail_count, status, start_time, end_time
|
||||
FROM eb_sync_log ORDER BY id DESC LIMIT 20;
|
||||
```
|
||||
|
||||
验证:
|
||||
|
||||
- `status=1`(完成)
|
||||
- `success_count + fail_count + skip_count = total_count`
|
||||
- `end_time > start_time`
|
||||
|
||||
---
|
||||
|
||||
## 八、测试顺序建议
|
||||
|
||||
1. 修复 Bug 1(`convertStatusToMessage` 中的 `sourceId`)
|
||||
2. 执行建表 SQL,插入商户配置
|
||||
3. 单元测试(映射逻辑,无需启动服务)
|
||||
4. 启动服务,验证 Producer 集成测试(4.1, 4.2)
|
||||
5. 验证 Consumer 集成测试(4.3)
|
||||
6. 执行全链路端到端测试(5.1)
|
||||
7. 执行多来源测试(5.2)
|
||||
8. 执行异常场景测试(6.1, 6.2)
|
||||
|
||||
@@ -0,0 +1,332 @@
|
||||
---
|
||||
name: Order Data Kafka Sync
|
||||
overview: Design and implement a Kafka-based data synchronization system to continuously sync user and order data from multiple single-merchant CRMEB systems into a multi-merchant platform, using intermediate staging tables for data transformation and merchant ID assignment.
|
||||
todos:
|
||||
- id: sql-staging
|
||||
content: Create SQL file with 6 staging/config/log tables (eb_sync_merchant_config, eb_sync_order_staging, eb_sync_order_detail_staging, eb_sync_order_status_staging, eb_sync_user_staging, eb_sync_log)
|
||||
status: completed
|
||||
- id: kafka-deps
|
||||
content: Add spring-kafka dependency to both projects' pom.xml files (root + crmeb-admin + crmeb-service)
|
||||
status: completed
|
||||
- id: message-dtos
|
||||
content: "Create Kafka message DTOs: SyncOrderMessage, SyncOrderDetailMessage, SyncOrderStatusMessage, SyncUserMessage in crmeb-common/model/sync/"
|
||||
status: completed
|
||||
- id: producer-service
|
||||
content: Implement OrderSyncProducerService (interface + impl) in single-merchant crmeb-service - query changed records, send to Kafka
|
||||
status: completed
|
||||
- id: producer-task
|
||||
content: Create OrderSyncProducerTask scheduled task in single-merchant crmeb-admin/task/sync/
|
||||
status: completed
|
||||
- id: producer-config
|
||||
content: Add Kafka producer config + sync properties to single-merchant application.yml
|
||||
status: completed
|
||||
- id: staging-models
|
||||
content: Create staging table entity models (SyncOrderStaging, SyncOrderDetailStaging, etc.) + SyncMerchantConfig + SyncLog in multi-merchant crmeb-common/model/sync/
|
||||
status: completed
|
||||
- id: staging-daos
|
||||
content: Create DAO interfaces + mapper XMLs for all staging tables in multi-merchant crmeb-service/dao/sync/
|
||||
status: completed
|
||||
- id: kafka-consumer
|
||||
content: Implement OrderSyncConsumer with @KafkaListener in multi-merchant crmeb-admin/consumer/
|
||||
status: completed
|
||||
- id: consumer-config
|
||||
content: Add Kafka consumer config to multi-merchant application.yml
|
||||
status: completed
|
||||
- id: process-service
|
||||
content: Implement OrderStagingProcessService (interface + impl) - core field mapping and transformation logic from staging tables to eb_order + eb_merchant_order + eb_order_detail + eb_order_flow_record
|
||||
status: in_progress
|
||||
- id: process-task
|
||||
content: Create OrderStagingProcessTask scheduled task in multi-merchant crmeb-admin/task/sync/
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Multi-Merchant Order Data Sync via Kafka
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph sources [Single-Merchant Systems]
|
||||
SMA["System A\n(source_id=shop_a)"]
|
||||
SMB["System B\n(source_id=shop_b)"]
|
||||
SMC["System C\n(source_id=shop_c)"]
|
||||
end
|
||||
|
||||
subgraph kafka [Kafka Cluster]
|
||||
T1[order-sync-topic]
|
||||
T2[order-detail-sync-topic]
|
||||
T3[order-status-sync-topic]
|
||||
T4[user-sync-topic]
|
||||
end
|
||||
|
||||
subgraph multi [Multi-Merchant Platform]
|
||||
subgraph staging [Staging Tables]
|
||||
S1[eb_sync_order_staging]
|
||||
S2[eb_sync_order_detail_staging]
|
||||
S3[eb_sync_order_status_staging]
|
||||
S4[eb_sync_user_staging]
|
||||
end
|
||||
subgraph target [Target Tables]
|
||||
T_O[eb_order]
|
||||
T_MO[eb_merchant_order]
|
||||
T_OD[eb_order_detail]
|
||||
T_FR[eb_order_flow_record]
|
||||
T_U[eb_user]
|
||||
end
|
||||
ProcessTask["OrderStagingProcessTask\n(Scheduled)"]
|
||||
end
|
||||
|
||||
SMA --> T1 & T2 & T3 & T4
|
||||
SMB --> T1 & T2 & T3 & T4
|
||||
SMC --> T1 & T2 & T3 & T4
|
||||
|
||||
T1 --> S1
|
||||
T2 --> S2
|
||||
T3 --> S3
|
||||
T4 --> S4
|
||||
|
||||
S1 & S2 & S3 & S4 --> ProcessTask
|
||||
ProcessTask --> T_O & T_MO & T_OD & T_FR & T_U
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Data Flow
|
||||
|
||||
1. **Producer (Single-Merchant)**: Scheduled task scans for new/updated records by `update_time > last_sync_time`, serializes to JSON, sends to Kafka topic with `source_id` and `target_mer_id` in message headers
|
||||
2. **Consumer (Multi-Merchant)**: Kafka listener receives messages, writes to staging tables with `sync_status=0` (pending)
|
||||
3. **Processor (Multi-Merchant)**: Scheduled task reads pending staging records, transforms fields, inserts into target tables, updates `sync_status=2` (synced)
|
||||
|
||||
---
|
||||
|
||||
## 1. SQL: Staging Tables + Config Table
|
||||
|
||||
Create file: [db/sync_staging_tables.sql](db/sync_staging_tables.sql)
|
||||
|
||||
### `eb_sync_merchant_config` - Source merchant mapping
|
||||
|
||||
- `id`, `source_id` (unique identifier for each single-merchant system), `source_name`, `target_mer_id` (corresponding `eb_merchant.id` in multi-merchant), `last_order_sync_time`, `last_user_sync_time`, `status`
|
||||
|
||||
### `eb_sync_order_staging` - Mirrors `eb_store_order` + sync control
|
||||
|
||||
- All fields from single-merchant `eb_store_order` prefixed where needed (e.g., `source_order_id`)
|
||||
- Added fields: `source_id`, `target_mer_id`, `sync_status` (0=pending, 1=processing, 2=synced, 3=failed), `sync_time`, `sync_error`, `retry_count`
|
||||
|
||||
### `eb_sync_order_detail_staging` - Mirrors `eb_store_order_info` + sync control
|
||||
|
||||
- All fields from single-merchant `eb_store_order_info`
|
||||
- Added fields: `source_id`, `target_mer_id`, `source_order_id`, `uid`, `sync_status`, `sync_error`
|
||||
|
||||
### `eb_sync_order_status_staging` - Mirrors `eb_store_order_status` + sync control
|
||||
|
||||
- All fields from single-merchant `eb_store_order_status`
|
||||
- Added fields: `source_id`, `order_no`, `sync_status`
|
||||
|
||||
### `eb_sync_user_staging` - Mirrors `eb_user` (single) + sync control
|
||||
|
||||
- All fields from single-merchant `eb_user`
|
||||
- Added fields: `source_id`, `sync_status`, `sync_error`
|
||||
|
||||
### `eb_sync_log` - Audit trail
|
||||
|
||||
- `id`, `source_id`, `sync_type` (order/user/detail/status), `batch_id`, `total_count`, `success_count`, `fail_count`, `start_time`, `end_time`, `status`, `error_msg`
|
||||
|
||||
---
|
||||
|
||||
## 2. Key Field Mappings
|
||||
|
||||
### Order Status Mapping (Single -> Multi)
|
||||
|
||||
|
||||
| Single (`paid` + `status`) | Multi `eb_order.status` |
|
||||
| -------------------------- | ----------------------- |
|
||||
| `paid=0` | 0 (待支付) |
|
||||
| `paid=1, status=0` | 1 (待发货) |
|
||||
| `paid=1, status=1` | 4 (待收货) |
|
||||
| `paid=1, status=2` | 5 (已收货) |
|
||||
| `paid=1, status=3` | 6 (已完成) |
|
||||
|
||||
|
||||
### Pay Channel Mapping
|
||||
|
||||
|
||||
| Single `is_channel` | Multi `pay_channel` |
|
||||
| ------------------- | ------------------- |
|
||||
| 0 | "public" |
|
||||
| 1 | "mini" |
|
||||
| 2 | "yue" |
|
||||
|
||||
|
||||
### Single `eb_store_order` -> Multi `eb_order` + `eb_merchant_order`
|
||||
|
||||
One single-merchant order generates **two** records in multi-merchant:
|
||||
|
||||
- `eb_order` (platform-level, `level=0`): pricing, status, payment info
|
||||
- `eb_merchant_order` (merchant-level): shipping, delivery, merchant-specific info
|
||||
|
||||
Both share the same `order_no`. The multi-merchant `eb_order` record carries `plat_order_no` = itself.
|
||||
|
||||
### Single `eb_store_order_info` -> Multi `eb_order_detail`
|
||||
|
||||
Key differences:
|
||||
|
||||
- Single uses `order_id` (int FK) -> Multi uses `order_no` (varchar FK)
|
||||
- Multi adds: `mer_id`, `uid`, `freight_fee`, `coupon_price`, `use_integral`, `integral_price`, `gain_integral`, `sub_brokerage_type`, `pay_price`
|
||||
- Single `give_integral` -> Multi `gain_integral`
|
||||
- Single `is_sub` -> Multi `sub_brokerage_type`
|
||||
|
||||
### Single `eb_store_order_status` -> Multi `eb_order_flow_record`
|
||||
|
||||
- Single `oid` (int) -> lookup `order_no` from order mapping
|
||||
- Single `change_type` -> Multi `flow_type`
|
||||
- Single `change_message` -> Multi `flow_message`
|
||||
- Multi adds: `operator_id=0`, `operator_type=0` (system)
|
||||
|
||||
---
|
||||
|
||||
## 3. Dependencies (Both Projects)
|
||||
|
||||
Add to root `pom.xml` in both [crmeb_22miao/pom.xml](crmeb_22miao/pom.xml) and [MER-2.2_2601/mer_java/pom.xml](MER-2.2_2601/mer_java/pom.xml):
|
||||
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>org.springframework.kafka</groupId>
|
||||
<artifactId>spring-kafka</artifactId>
|
||||
</dependency>
|
||||
```
|
||||
|
||||
Spring Boot 2.2.6 manages `spring-kafka` version automatically (2.3.x).
|
||||
|
||||
Add to `crmeb-admin/pom.xml` and `crmeb-service/pom.xml` in both projects.
|
||||
|
||||
---
|
||||
|
||||
## 4. Producer Side (Single-Merchant: `crmeb_22miao`)
|
||||
|
||||
### Configuration
|
||||
|
||||
- [crmeb-admin/src/main/resources/application.yml](crmeb_22miao/crmeb-admin/src/main/resources/application.yml): Add `spring.kafka.bootstrap-servers`, producer serializer config, `sync.source-id`, `sync.target-mer-id`
|
||||
|
||||
### New Files
|
||||
|
||||
- `**crmeb-common/.../common/model/sync/SyncOrderMessage.java**` - DTO for Kafka order message (all `eb_store_order` fields + `sourceId` + `targetMerId`)
|
||||
- `**crmeb-common/.../common/model/sync/SyncOrderDetailMessage.java**` - DTO for order detail message
|
||||
- `**crmeb-common/.../common/model/sync/SyncOrderStatusMessage.java**` - DTO for order status message
|
||||
- `**crmeb-common/.../common/model/sync/SyncUserMessage.java**` - DTO for user message
|
||||
- `**crmeb-service/.../service/service/sync/OrderSyncProducerService.java**` - Interface
|
||||
- `**crmeb-service/.../service/service/sync/impl/OrderSyncProducerServiceImpl.java**` - Implementation: queries `eb_store_order` WHERE `update_time > lastSyncTime`, converts to `SyncOrderMessage`, sends to Kafka topic. Uses `eb_sync_merchant_config` or local config for `source_id`/`target_mer_id`.
|
||||
- `**crmeb-admin/.../admin/task/sync/OrderSyncProducerTask.java**` - Scheduled task: invokes `OrderSyncProducerService` to scan and send orders/details/statuses. Tracks `lastSyncTime` in Redis key `sync:last_order_time:{sourceId}`.
|
||||
|
||||
### Sync Logic (Producer)
|
||||
|
||||
```java
|
||||
// Pseudocode
|
||||
List<StoreOrder> newOrders = storeOrderDao.selectList(
|
||||
new LambdaQueryWrapper<StoreOrder>()
|
||||
.gt(StoreOrder::getUpdateTime, lastSyncTime)
|
||||
.orderByAsc(StoreOrder::getUpdateTime)
|
||||
.last("LIMIT 500")
|
||||
);
|
||||
for (StoreOrder order : newOrders) {
|
||||
SyncOrderMessage msg = convertToMessage(order, sourceId, targetMerId);
|
||||
kafkaTemplate.send("order-sync-topic", order.getOrderId(), JSON.toJSONString(msg));
|
||||
// Also send order details and status for this order
|
||||
}
|
||||
// Update lastSyncTime in Redis
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Consumer Side (Multi-Merchant: `MER-2.2_2601`)
|
||||
|
||||
### Configuration
|
||||
|
||||
- [crmeb-admin/src/main/resources/application.yml](MER-2.2_2601/mer_java/crmeb-admin/src/main/resources/application.yml): Add Kafka consumer config, consumer group `order-sync-group`
|
||||
|
||||
### New Model/Entity Files (in `crmeb-common`)
|
||||
|
||||
- `**common/model/sync/SyncOrderStaging.java**` - `@TableName("eb_sync_order_staging")` entity
|
||||
- `**common/model/sync/SyncOrderDetailStaging.java**` - `@TableName("eb_sync_order_detail_staging")` entity
|
||||
- `**common/model/sync/SyncOrderStatusStaging.java**` - `@TableName("eb_sync_order_status_staging")` entity
|
||||
- `**common/model/sync/SyncUserStaging.java**` - `@TableName("eb_sync_user_staging")` entity
|
||||
- `**common/model/sync/SyncMerchantConfig.java**` - `@TableName("eb_sync_merchant_config")` entity
|
||||
- `**common/model/sync/SyncLog.java**` - `@TableName("eb_sync_log")` entity
|
||||
|
||||
### New DAO Files (in `crmeb-service`)
|
||||
|
||||
- `**service/dao/sync/SyncOrderStagingDao.java**` - extends `BaseMapper<SyncOrderStaging>`
|
||||
- `**service/dao/sync/SyncOrderDetailStagingDao.java**`
|
||||
- `**service/dao/sync/SyncOrderStatusStagingDao.java**`
|
||||
- `**service/dao/sync/SyncUserStagingDao.java**`
|
||||
- `**service/dao/sync/SyncMerchantConfigDao.java**`
|
||||
- `**service/dao/sync/SyncLogDao.java**`
|
||||
- Corresponding empty mapper XMLs in `resources/mapper/sync/`
|
||||
|
||||
### Kafka Consumer
|
||||
|
||||
- `**crmeb-admin/.../admin/consumer/OrderSyncConsumer.java**` - `@KafkaListener` on topics `order-sync-topic`, `order-detail-sync-topic`, `order-status-sync-topic`, `user-sync-topic`. Deserializes JSON messages, creates staging records with `sync_status=0`, inserts via staging DAOs.
|
||||
|
||||
### Processing Service
|
||||
|
||||
- `**crmeb-service/.../service/service/sync/OrderStagingProcessService.java**` - Interface with methods: `processOrderStaging()`, `processOrderDetailStaging()`, `processOrderStatusStaging()`, `processUserStaging()`
|
||||
- `**crmeb-service/.../service/service/sync/impl/OrderStagingProcessServiceImpl.java**` - Core transformation logic:
|
||||
|
||||
**Order Processing Logic:**
|
||||
|
||||
```java
|
||||
// 1. Select pending staging records (sync_status=0, LIMIT 200)
|
||||
// 2. For each staging record:
|
||||
// a. Map status: combinePaidAndStatus(staging.paid, staging.status) -> multiStatus
|
||||
// b. Map payChannel: mapPayChannel(staging.isChannel) -> payChannel
|
||||
// c. Create eb_order record (platform level, level=0)
|
||||
// d. Create eb_merchant_order record (merchant level)
|
||||
// e. Create eb_order_profit_sharing record
|
||||
// f. Insert all in transaction
|
||||
// g. Update staging sync_status=2
|
||||
// 3. On error: update staging sync_status=3, sync_error=msg
|
||||
```
|
||||
|
||||
**Key transformations in `OrderStagingProcessServiceImpl`:**
|
||||
|
||||
- Order number: prefix with `source_id` to avoid conflicts, e.g., `"S_{sourceId}_{originalOrderNo}"`
|
||||
- User ID mapping: lookup by `phone` in `eb_user` table (since users were already synced via `wa_users`)
|
||||
- Status mapping: combine `paid` + `status` fields as described above
|
||||
- `eb_order.level = 0` (platform order), `eb_merchant_order` gets `mer_id = target_mer_id`
|
||||
- `eb_order.plat_order_no = order_no` (self-reference for platform orders)
|
||||
- `eb_order.second_type`: map from single's `type` field (0->0 普通, 1->4 视频号)
|
||||
- `eb_merchant_order.delivery_type`: map from single's `delivery_type` / `shipping_type`
|
||||
|
||||
### Sync Processing Task
|
||||
|
||||
- `**crmeb-admin/.../admin/task/sync/OrderStagingProcessTask.java**` - Scheduled task (every 2 minutes): calls `OrderStagingProcessService.processOrderStaging()` etc.
|
||||
|
||||
---
|
||||
|
||||
## 6. File Summary
|
||||
|
||||
### SQL (1 file)
|
||||
|
||||
- `db/sync_staging_tables.sql` - 6 tables: config, order staging, detail staging, status staging, user staging, sync log
|
||||
|
||||
### Single-Merchant Producer (7 files)
|
||||
|
||||
- 2 pom.xml changes (root + crmeb-admin)
|
||||
- 1 application.yml change
|
||||
- 4 message DTOs in `crmeb-common/model/sync/`
|
||||
- 1 producer service interface + 1 impl in `crmeb-service/service/sync/`
|
||||
- 1 producer task in `crmeb-admin/task/sync/`
|
||||
|
||||
### Multi-Merchant Consumer (18+ files)
|
||||
|
||||
- 2 pom.xml changes (root + crmeb-admin)
|
||||
- 1 application.yml change
|
||||
- 6 entity models in `crmeb-common/model/sync/`
|
||||
- 4 message DTOs (shared with producer, or copy)
|
||||
- 6 DAO interfaces in `crmeb-service/dao/sync/`
|
||||
- 6 mapper XMLs in `resources/mapper/sync/`
|
||||
- 1 Kafka consumer in `crmeb-admin/consumer/`
|
||||
- 1 processing service interface + 1 impl in `crmeb-service/service/sync/`
|
||||
- 1 processing task in `crmeb-admin/task/sync/`
|
||||
|
||||
Total: ~30 new files + 6 modified files
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
name: Order Print Product Details
|
||||
overview: Supplement the product detail table in the order print page (`orderDetailPrint.vue`) with product image, product ID, and reward points columns, plus a totals summary row.
|
||||
todos:
|
||||
- id: add-image-col
|
||||
content: 在商品列表表格新增「商品图片」列(第一列,显示缩略图)
|
||||
status: in_progress
|
||||
- id: add-product-id
|
||||
content: 在商品名称列下方补充显示商品编号(item.productId)
|
||||
status: pending
|
||||
- id: add-integral-col
|
||||
content: 新增「赠送积分」列,仅当订单中存在积分时条件显示
|
||||
status: pending
|
||||
- id: add-total-row
|
||||
content: 在商品表格底部添加合计汇总行(数量合计 + 金额合计)
|
||||
status: pending
|
||||
- id: update-copy-text
|
||||
content: 同步更新 formatOrderInfo() 复制文本,补充商品编号和赠送积分信息
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# Order Print Page — 补充商品明细信息
|
||||
|
||||
## 目标文件
|
||||
|
||||
`[single_admin22miao/src/views/order/orderDetailPrint.vue](single_admin22miao/src/views/order/orderDetailPrint.vue)`
|
||||
|
||||
## 当前商品列表表格(第 142–163 行)
|
||||
|
||||
|
||||
| 列 | 字段 |
|
||||
| ---- | ----------------------- |
|
||||
| 商品名称 | `item.info.productName` |
|
||||
| 规格 | `item.info.sku` |
|
||||
| 单价 | `item.info.price` |
|
||||
| 数量 | `item.info.payNum` |
|
||||
| 小计 | 计算值 |
|
||||
|
||||
|
||||
## 补充内容
|
||||
|
||||
### 1. 商品图片列(新增第一列)
|
||||
|
||||
- 来源:`item.info.image`
|
||||
- 显示为小图(width: 50px),打印时保留缩略图
|
||||
|
||||
### 2. 商品编号列(商品名称列内或独立列)
|
||||
|
||||
- 来源:`item.productId`
|
||||
- 在商品名称下方以小字显示(`编号: xxx`)
|
||||
|
||||
### 3. 赠送积分列(条件显示)
|
||||
|
||||
- 来源:`item.info.giveIntegral`
|
||||
- 仅当订单中任意商品有积分时显示该列(`v-if` 控制表头和单元格)
|
||||
|
||||
### 4. 合计汇总行(表格底部)
|
||||
|
||||
- 数量合计:所有 `payNum` 求和
|
||||
- 金额合计:与 `orderDatalist.proTotalPrice` 一致
|
||||
|
||||
### 5. 同步更新 `formatOrderInfo()` 复制文本
|
||||
|
||||
- 在复制文本中补充商品编号和赠送积分字段
|
||||
|
||||
## 数据结构参考
|
||||
|
||||
`orderDatalist.orderInfoList` 中每条 item 的字段(来自 `StoreOrderInfoOldVo` + `OrderInfoDetailVo`):
|
||||
|
||||
- `item.productId` — 商品编号
|
||||
- `item.info.image` — 商品图片
|
||||
- `item.info.productName` — 商品名称
|
||||
- `item.info.sku` — 规格
|
||||
- `item.info.price` — 单价
|
||||
- `item.info.payNum` — 数量
|
||||
- `item.info.giveIntegral` — 赠送积分
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
---
|
||||
name: Staging table scheduled sync
|
||||
overview: 在多商户侧已存在 OrderStagingProcessTask 与 OrderStagingProcessService,可将中间表数据写入目标表。本方案在原基础上补充三个关键问题:(1) 单商户 uid 与多商户 eb_user.id 的映射;(2) 用户同步时写入 eb_merchant_user 建立用户-商户关系;(3) 重复用户/订单跳过时写入 eb_sync_log 失败记录便于排查。最后通过 eb_schedule_job 注册 4 个定时任务完成自动化。
|
||||
todos:
|
||||
- id: fix-uid-mapping
|
||||
content: 修改 processUserStaging:写入 eb_user 时设置 uid=staging.sourceUid(单商户原始uid),id=自增(多商户id);修改 processOneOrder:按 phone 查 eb_user.id 替换 staging.uid,确保订单关联的是多商户 uid
|
||||
status: in_progress
|
||||
- id: add-merchant-user
|
||||
content: 修改 processUserStaging:用户写入 eb_user 后,根据 staging.sourceId 查 eb_sync_merchant_config 获取 target_mer_id,向 eb_merchant_user 插入 (uid=新用户id, mer_id=target_mer_id) 记录
|
||||
status: pending
|
||||
- id: add-skip-fail-log
|
||||
content: 修改 processUserStaging 和 processOrderStaging:对重复跳过的用户/订单,在 eb_sync_log 或 staging 表中记录 skip 原因(如 已存在的 phone/order_no),将 log.debug 改为 log.info 并写入 sync_error 字段
|
||||
status: pending
|
||||
- id: insert-schedule-job
|
||||
content: 新增 SQL 文件向 eb_schedule_job 插入 4 条定时任务记录(processUsers/processOrders/processOrderDetails/processOrderStatuses)
|
||||
status: pending
|
||||
isProject: false
|
||||
---
|
||||
|
||||
# 中间表数据定时同步到用户与订单等相关表
|
||||
|
||||
## 现状
|
||||
|
||||
- **中间表**([db/sync_staging_tables.sql](db/sync_staging_tables.sql)):Kafka 消费者写入 `eb_sync_user_staging`、`eb_sync_order_staging`、`eb_sync_order_detail_staging`、`eb_sync_order_status_staging`,`sync_status=0` 表示待处理。
|
||||
- **处理逻辑**(多商户 MER-2.2_2601):
|
||||
- [OrderStagingProcessServiceImpl](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/sync/impl/OrderStagingProcessServiceImpl.java):从中间表读取待处理数据,转换后写入 `eb_user`、`eb_order`、`eb_merchant_order`、`eb_order_detail`、`eb_order_flow_record`。
|
||||
- [OrderStagingProcessTask](MER-2.2_2601/mer_java/crmeb-admin/src/main/java/com/zbkj/admin/task/sync/OrderStagingProcessTask.java):提供 4 个无参方法,但**未绑定定时执行**。
|
||||
- **多商户定时体系**:Quartz + `eb_schedule_job` 表,启动时自动加载。
|
||||
|
||||
---
|
||||
|
||||
## 需要解决的三个关键问题
|
||||
|
||||
### 问题 1:单商户 uid 与多商户用户 id 的对应关系
|
||||
|
||||
**现状**:多商户 `eb_user` 表有两个 ID 字段(见 [多商户eb_user.sql](MER-2.2_2601/mer_java/sql/多商户eb_user.sql)):
|
||||
|
||||
|
||||
| 字段 | 含义 |
|
||||
| ----- | ------------------------------------ |
|
||||
| `id` | 多商户平台自增用户 ID(其他表如 eb_order.uid 引用此值) |
|
||||
| `uid` | 单商户原始用户 ID(用于追溯来源) |
|
||||
|
||||
|
||||
联合主键:`PRIMARY KEY (id, uid)`
|
||||
|
||||
**当前代码问题**:
|
||||
|
||||
1. `processUserStaging()` 中写入 `eb_user` 时只设了 `user.setId(nextUserId)`,**未设置 `uid**`(单商户原始 uid),导致 `uid` 列为默认值 0,无法追溯来源。
|
||||
2. `processOneOrder()` 中直接使用 `staging.getUid()`(单商户 uid)赋给 `order.setUid()` 和 `merchantOrder.setUid()`,但 `eb_order.uid` 和 `eb_merchant_order.uid` 期望的是**多商户 `eb_user.id**`,导致关联错误。
|
||||
|
||||
**修复方案**:
|
||||
|
||||
在 [OrderStagingProcessServiceImpl.java](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/sync/impl/OrderStagingProcessServiceImpl.java) 中:
|
||||
|
||||
**(a) processUserStaging() -- 写入时同时设 id 和 uid:**
|
||||
|
||||
```java
|
||||
// 现有代码(约 L463-464)
|
||||
user.setId(nextUserId);
|
||||
// 需增加:
|
||||
user.setUid(staging.getSourceUid().longValue());
|
||||
```
|
||||
|
||||
**(b) processOneOrder() -- 按 phone 查 eb_user.id 替代直接用 staging.uid:**
|
||||
|
||||
```java
|
||||
// 在 processOneOrder() 开头,staging.getUserPhone() 或 staging 中的 phone 信息
|
||||
// 查找多商户 eb_user 中的真实 id
|
||||
LambdaQueryWrapper<User> userQuery = new LambdaQueryWrapper<>();
|
||||
userQuery.eq(User::getPhone, staging.getUserPhone());
|
||||
User multiUser = userDao.selectOne(userQuery);
|
||||
if (multiUser == null) {
|
||||
throw new RuntimeException("用户不存在,无法创建订单: phone=" + staging.getUserPhone()
|
||||
+ ", sourceUid=" + staging.getUid());
|
||||
}
|
||||
int multiUid = multiUser.getId();
|
||||
|
||||
// 后续使用 multiUid 替代 staging.getUid()
|
||||
order.setUid(multiUid); // L125
|
||||
merchantOrder.setUid(multiUid); // L172
|
||||
```
|
||||
|
||||
**uid 映射关系图:**
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph single [单商户]
|
||||
SU["eb_user\nuid=100"]
|
||||
SO["eb_store_order\nuid=100"]
|
||||
end
|
||||
|
||||
subgraph staging [中间表]
|
||||
SUS["eb_sync_user_staging\nsource_uid=100\nphone=138xxx"]
|
||||
SOS["eb_sync_order_staging\nuid=100\nuser_phone=138xxx"]
|
||||
end
|
||||
|
||||
subgraph multi [多商户]
|
||||
MU["eb_user\nid=5, uid=100\nphone=138xxx"]
|
||||
MO["eb_order\nuid=5"]
|
||||
MMU["eb_merchant_user\nuid=5, mer_id=1"]
|
||||
end
|
||||
|
||||
SU --> SUS
|
||||
SO --> SOS
|
||||
SUS -->|"processUsers()\nid=自增, uid=source_uid"| MU
|
||||
SOS -->|"processOrders()\n按phone查eb_user.id=5"| MO
|
||||
MU -->|"同时插入"| MMU
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
### 问题 2:补充用户所属商户关系(eb_merchant_user)
|
||||
|
||||
**现状**:多商户系统用 `eb_merchant_user` 表记录用户与商户的关联:
|
||||
|
||||
```sql
|
||||
-- eb_merchant_user(见 shop22多商户V2.2+寄卖.sql L1387)
|
||||
CREATE TABLE eb_merchant_user (
|
||||
id INT UNSIGNED AUTO_INCREMENT,
|
||||
uid INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户uid(多商户 eb_user.id)',
|
||||
mer_id INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '商户id',
|
||||
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (id)
|
||||
);
|
||||
```
|
||||
|
||||
当前 `processUserStaging()` 只往 `eb_user` 写数据,**未写 `eb_merchant_user**`,导致用户不关联任何商户,在商户后台的「用户管理」中看不到同步过来的用户。
|
||||
|
||||
**修复方案**:
|
||||
|
||||
在 [OrderStagingProcessServiceImpl.java](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/sync/impl/OrderStagingProcessServiceImpl.java) 中:
|
||||
|
||||
**(a) 注入 MerchantUserDao(若未注入)**
|
||||
|
||||
**(b) processUserStaging() 中用户插入成功后,追加 eb_merchant_user 记录:**
|
||||
|
||||
```java
|
||||
userDao.insert(user);
|
||||
|
||||
// 根据 staging.sourceId 查 eb_sync_merchant_config 获取 target_mer_id
|
||||
LambdaQueryWrapper<SyncMerchantConfig> configQuery = new LambdaQueryWrapper<>();
|
||||
configQuery.eq(SyncMerchantConfig::getSourceId, staging.getSourceId());
|
||||
SyncMerchantConfig config = syncMerchantConfigDao.selectOne(configQuery);
|
||||
if (config != null) {
|
||||
// 检查是否已存在
|
||||
LambdaQueryWrapper<MerchantUser> muCheck = new LambdaQueryWrapper<>();
|
||||
muCheck.eq(MerchantUser::getUid, user.getId());
|
||||
muCheck.eq(MerchantUser::getMerId, config.getTargetMerId());
|
||||
if (merchantUserDao.selectCount(muCheck) == 0) {
|
||||
MerchantUser mu = new MerchantUser();
|
||||
mu.setUid(user.getId());
|
||||
mu.setMerId(config.getTargetMerId());
|
||||
mu.setCreateTime(new Date());
|
||||
merchantUserDao.insert(mu);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**(c) 对于用户已存在(跳过)的情况**,也要确保 `eb_merchant_user` 关联存在:
|
||||
|
||||
```java
|
||||
if (existingUser != null) {
|
||||
// 用户已存在,但仍需确保与该来源商户的关联
|
||||
// 查 config 获取 target_mer_id,检查并补充 eb_merchant_user
|
||||
ensureMerchantUserRelation(existingUser.getId(), staging.getSourceId());
|
||||
markUserStagingSynced(staging.getId());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 问题 3:重复用户/订单跳过时写入失败日志
|
||||
|
||||
**现状**:
|
||||
|
||||
- 用户重复跳过时只有 `log.debug`(约 L455),不写 `sync_error`,staging 直接标为 `synced(2)`。
|
||||
- 订单重复跳过时(约 L79-82),也是直接标 `synced(2)` 无任何记录。
|
||||
- 异常失败会写 `sync_error` + `retry_count`,但跳过的不会。
|
||||
|
||||
审计困难:无法区分一条 staging 记录是「新同步成功」还是「因重复而跳过」。
|
||||
|
||||
**修复方案**:
|
||||
|
||||
**(a) 在 staging 表中增加跳过标记**
|
||||
|
||||
将重复跳过的记录标记为 `sync_status=2` 但在 `sync_error` 中写入原因:
|
||||
|
||||
```java
|
||||
// processUserStaging() 用户已存在时(约 L454-458)
|
||||
if (existingUser != null) {
|
||||
String skipReason = "用户已存在: phone=" + staging.getPhone()
|
||||
+ ", existingId=" + existingUser.getId();
|
||||
log.info("用户跳过: sourceUid={}, reason={}", staging.getSourceUid(), skipReason);
|
||||
markUserStagingSkipped(staging.getId(), skipReason);
|
||||
ensureMerchantUserRelation(existingUser.getId(), staging.getSourceId());
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// processOrderStaging() 订单已存在时(约 L79-82)
|
||||
if (existCount != null && existCount > 0) {
|
||||
String skipReason = "订单已存在: orderNo=" + targetOrderNo;
|
||||
log.info("订单跳过: sourceOrderId={}, reason={}", staging.getSourceOrderId(), skipReason);
|
||||
markOrderStagingSkipped(staging.getId(), skipReason);
|
||||
skipCount++;
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
**(b) 增加 markXxxStagingSkipped 方法**
|
||||
|
||||
与现有 `markXxxStagingSynced` 类似,但额外写入 `sync_error`:
|
||||
|
||||
```java
|
||||
private void markUserStagingSkipped(Long id, String reason) {
|
||||
LambdaUpdateWrapper<SyncUserStaging> w = new LambdaUpdateWrapper<>();
|
||||
w.eq(SyncUserStaging::getId, id);
|
||||
w.set(SyncUserStaging::getSyncStatus, 2);
|
||||
w.set(SyncUserStaging::getSyncTime, new Date());
|
||||
w.set(SyncUserStaging::getSyncError, reason);
|
||||
syncUserStagingDao.update(null, w);
|
||||
}
|
||||
```
|
||||
|
||||
订单跳过同理。
|
||||
|
||||
**(c) eb_sync_log 汇总日志中已有 skipCount**
|
||||
|
||||
现有 `completeSyncLog()` 已将 `skipCount` 写入 `eb_sync_log.skip_count`,无需改动。只需确保上述 skipReason 写入 staging 的 `sync_error` 字段即可。
|
||||
|
||||
---
|
||||
|
||||
## 实现步骤
|
||||
|
||||
### 步骤 1:修改 OrderStagingProcessServiceImpl.java
|
||||
|
||||
在 [OrderStagingProcessServiceImpl](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/sync/impl/OrderStagingProcessServiceImpl.java) 中:
|
||||
|
||||
1. **processUserStaging()**:
|
||||
- `user.setUid(staging.getSourceUid().longValue())` -- 写入单商户原始 uid
|
||||
- 插入 `eb_user` 后追加 `eb_merchant_user` 记录
|
||||
- 用户已存在跳过时,补充 `eb_merchant_user` 关系 + 写 `sync_error` 跳过原因
|
||||
2. **processOneOrder()**:
|
||||
- 开头按 `staging.getUserPhone()` 查 `eb_user` 获取多商户 `id`
|
||||
- 用查到的 `multiUid` 替代 `staging.getUid()` 设入 `order.setUid()` 和 `merchantOrder.setUid()`
|
||||
- 未查到用户则抛异常(staging 会被标为 failed)
|
||||
3. **增加辅助方法**:
|
||||
- `ensureMerchantUserRelation(int userId, String sourceId)`
|
||||
- `markUserStagingSkipped(Long id, String reason)`
|
||||
- `markOrderStagingSkipped(Long id, String reason)`
|
||||
4. **注入 `MerchantUserDao**`(若尚未注入)和 `SyncMerchantConfigDao`(已注入)
|
||||
|
||||
### 步骤 2:新增 SQL 文件注册 4 个定时任务
|
||||
|
||||
新增 [MER-2.2_2601/mer_java/sql/order_sync_staging_schedule_job.sql](MER-2.2_2601/mer_java/sql/order_sync_staging_schedule_job.sql):
|
||||
|
||||
```sql
|
||||
INSERT INTO eb_schedule_job (bean_name, method_name, params, cron_expression, status, remark, is_delete, create_time)
|
||||
SELECT 'OrderStagingProcessTask', 'processUsers', '', '0 */3 * * * ?', 0, '同步用户中间表到eb_user+eb_merchant_user', 0, NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM eb_schedule_job WHERE bean_name='OrderStagingProcessTask' AND method_name='processUsers' AND is_delete=0
|
||||
);
|
||||
|
||||
INSERT INTO eb_schedule_job (bean_name, method_name, params, cron_expression, status, remark, is_delete, create_time)
|
||||
SELECT 'OrderStagingProcessTask', 'processOrders', '', '0 1/3 * * * ?', 0, '同步订单中间表到eb_order/eb_merchant_order', 0, NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM eb_schedule_job WHERE bean_name='OrderStagingProcessTask' AND method_name='processOrders' AND is_delete=0
|
||||
);
|
||||
|
||||
INSERT INTO eb_schedule_job (bean_name, method_name, params, cron_expression, status, remark, is_delete, create_time)
|
||||
SELECT 'OrderStagingProcessTask', 'processOrderDetails', '', '0 2/3 * * * ?', 0, '同步订单详情中间表到eb_order_detail', 0, NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM eb_schedule_job WHERE bean_name='OrderStagingProcessTask' AND method_name='processOrderDetails' AND is_delete=0
|
||||
);
|
||||
|
||||
INSERT INTO eb_schedule_job (bean_name, method_name, params, cron_expression, status, remark, is_delete, create_time)
|
||||
SELECT 'OrderStagingProcessTask', 'processOrderStatuses', '', '0 2/3 * * * ?', 0, '同步订单状态中间表到eb_order_flow_record', 0, NOW()
|
||||
FROM DUAL WHERE NOT EXISTS (
|
||||
SELECT 1 FROM eb_schedule_job WHERE bean_name='OrderStagingProcessTask' AND method_name='processOrderStatuses' AND is_delete=0
|
||||
);
|
||||
```
|
||||
|
||||
### 步骤 3:部署与生效
|
||||
|
||||
1. 在多商户数据库执行 `order_sync_staging_schedule_job.sql`。
|
||||
2. 重启多商户应用(`ScheduleJobServiceImpl.init()` 从 `eb_schedule_job` 加载并注册 Quartz 任务)。
|
||||
3. 之后 4 个方法按 cron 每 3 分钟自动执行。
|
||||
|
||||
---
|
||||
|
||||
## 数据流总览
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph staging [中间表]
|
||||
U[eb_sync_user_staging]
|
||||
O[eb_sync_order_staging]
|
||||
D[eb_sync_order_detail_staging]
|
||||
S[eb_sync_order_status_staging]
|
||||
end
|
||||
|
||||
subgraph quartz [Quartz 定时]
|
||||
T1["processUsers()"]
|
||||
T2["processOrders()"]
|
||||
T3["processOrderDetails()"]
|
||||
T4["processOrderStatuses()"]
|
||||
end
|
||||
|
||||
subgraph target [目标表]
|
||||
EU["eb_user\n(id=多商户, uid=单商户)"]
|
||||
MU[eb_merchant_user]
|
||||
EO[eb_order]
|
||||
MO[eb_merchant_order]
|
||||
OD[eb_order_detail]
|
||||
FR[eb_order_flow_record]
|
||||
end
|
||||
|
||||
subgraph logs [日志]
|
||||
SL[eb_sync_log]
|
||||
SE["staging.sync_error\n(跳过原因)"]
|
||||
end
|
||||
|
||||
T1 --> U --> EU
|
||||
T1 --> U --> MU
|
||||
T2 --> O -->|"按phone查eb_user.id"| EO
|
||||
T2 --> O --> MO
|
||||
T3 --> D --> OD
|
||||
T4 --> S --> FR
|
||||
|
||||
T1 -.->|"skip/fail"| SE
|
||||
T2 -.->|"skip/fail"| SE
|
||||
T1 -.-> SL
|
||||
T2 -.-> SL
|
||||
T3 -.-> SL
|
||||
T4 -.-> SL
|
||||
```
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
## 涉及文件
|
||||
|
||||
|
||||
| 文件 | 操作 |
|
||||
| --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
|
||||
| [OrderStagingProcessServiceImpl.java](MER-2.2_2601/mer_java/crmeb-service/src/main/java/com/zbkj/service/service/sync/impl/OrderStagingProcessServiceImpl.java) | 修改:uid 映射、eb_merchant_user 插入、skip 日志 |
|
||||
| [MER-2.2_2601/mer_java/sql/order_sync_staging_schedule_job.sql](MER-2.2_2601/mer_java/sql/order_sync_staging_schedule_job.sql) | 新增:4 条 eb_schedule_job INSERT |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user