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:
2026-02-28 04:10:52 +08:00
commit 0b3d8b6be6
2278 changed files with 310929 additions and 0 deletions

View 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

View File

@@ -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 增加 getMaxIdUserMapper.xml 增加对应 SQLUserService 增加 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` 的 SELECTCOALESCE(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 已去除自增”的数据库设计一致,错误可消除。

View File

@@ -0,0 +1,90 @@
---
name: Fix Nginx static 404
overview: 服务器站点路径与 Nginx 已确认为 /www/wwwroot/jfplat.suzhouyuqi.comroot 与 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/缓存问题。

View File

@@ -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/cancelbody传id、POST order/takebody传id、POST order/delbody传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` 中新增方法

View 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` 始终为 nullconsumer 写入 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=1shop_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 的 Redis101.201.54.161DB 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 的 Redis39.106.63.33DB 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

View File

@@ -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

View File

@@ -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)`
## 当前商品列表表格(第 142163 行)
| 列 | 字段 |
| ---- | ----------------------- |
| 商品名称 | `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` — 赠送积分

View File

@@ -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单商户原始uidid=自增多商户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 |