巴青农资商城

商城数据统计技术方案.md 14KB

商城数据统计 — 技术方案

依据: 《商城数据统计功能需求.md》v1.0.4
关联: 《订单管理技术方案.md》v1.0.2、《商品分类功能需求》v1.5、《商城设置技术方案.md》v1.1
范围: 对外 只读 六项统计; 平台 Web 菜单;不新建 业务明细表,聚合查询 现有订单/分类/店铺/评价数据。
原则: 统计 不阻塞 交易写路径;准实时 + Redis 缓存;Token 会员 JWT 隔离


1. 技术架构

选型
基础框架 RuoYi v3.9.2springboot2 分支)
数据库 MySQL 5.7.39
ORM / 响应 MyBatis;AjaxResultcode / msg / data
缓存 Redis(统计结果 TTL,见 §5)
认证 AES-128-CBC 加密 UUID(请求头校验;不走 @PreAuthorize 登录态; Token 库表)
分词(词云) HanLPAnsj(二选一,应用层;结果缓存)
时区 Asia/Shanghai(统计日/年边界)

1.1 模块落位

文档目录: doc/平台后台/外部接口/
后端包: com.ruoyi.web.modules.openstats

openstats/
├── controller/MallStatsOpenController.java     # /api/open/stats/**
├── filter/OpenStatsTokenFilter.java            # Token 校验(仅匹配 open stats 路径)
├── service/IMallStatsOpenService.java
├── service/impl/MallStatsOpenServiceImpl.java
├── mapper/MallStatsOpenMapper.java
├── vo/                                         # 六项 VO + OverviewVO
├── support/
│   ├── OpenStatsTokenSupport.java              # Token 校验入口
│   ├── OpenStatsTokenCryptoSupport.java        # AES 解密 + UUID 格式校验
│   ├── AddressCityParser.java                  # consignee_address → 城市
│   ├── ReviewWordCloudSupport.java             # 分词 + 停用词 + Top50
│   └── StatsCacheKeys.java                     # Redis key 规范
└── constant/OpenStatsConstants.java            # Key/IV、缓存 TTL 等

sql/
└── (统计索引见 `sql/biz_order.sql` 中 `idx_stats_finish`)

与 C 端 @Anonymous 区别: 路径 /api/open/stats/** 携带有效 Token;Filter 先于 Controller;无效 Token 返回 401 进入业务。


2. 数据库设计

2.1 设计原则

原则 说明
不建统计事实表(v1) 六项指标 实时/缓存聚合;避免与订单写路径双写
只读关联 禁止 统计接口内 UPDATE 业务表
不建 Token 表 认证为 固定 AES 密钥 解密后校验 UUID; biz_open_api_token

2.2 依赖业务表(只读)

统计项 关键字段
biz_order 订单趋势、区域金额 order_id, order_status, create_time, finish_time, pay_amount, consignee_address
biz_order_item 品类销量、热销 order_id, goods_id, quantity
biz_goods 品类归并 goods_id, category_id(平台 二级
biz_goods_category 一级品类名 category_id, parent_id, category_level, shop_id IS NULL, category_name
biz_shop 店铺入驻 shop_id, create_time
biz_goods_review 词云 content, del_flag, show_flag

状态常量(对齐 OrderConstants):

含义 参与指标
3 已完成 §5.1、§5.2、§5.5(finish_time 非空)
0~4 非删除 §5.3(按 create_time
5 已删除 全部排除

品类归并 SQL 逻辑:

biz_order_item oi
  JOIN biz_order o ON o.order_id = oi.order_id
  JOIN biz_goods g ON g.goods_id = oi.goods_id
  JOIN biz_goods_category c2 ON c2.category_id = g.category_id AND c2.shop_id IS NULL
  JOIN biz_goods_category c1 ON c1.category_id = c2.parent_id AND c1.shop_id IS NULL
       → c1 为一级品类
  一级缺失 → category_id=0, category_name='未分类'(全接口统一)

2.3 统计索引(biz_order

脚本: sql/biz_order.sql — 建表已含 idx_stats_finishorder_status, finish_time);存量库见该文件 末尾升级注释


3. 核心查询口径(MyBatis)

3.1 农资品类销售 / 热销 Top5(共用数据集)

条件:

o.order_status = '3'
AND YEAR(o.finish_time) = #{statYear}   -- 当年,Asia/Shanghai
AND o.order_status <> '5'

聚合:

SELECT c1.category_id, c1.category_name, SUM(oi.quantity) AS qty
FROM biz_order o
INNER JOIN biz_order_item oi ON oi.order_id = o.order_id
INNER JOIN biz_goods g ON g.goods_id = oi.goods_id
INNER JOIN biz_goods_category c2 ON c2.category_id = g.category_id AND c2.shop_id IS NULL
INNER JOIN biz_goods_category c1 ON c1.category_id = c2.parent_id AND c1.shop_id IS NULL
WHERE ...
GROUP BY c1.category_id, c1.category_name
接口 Service 处理
品类销售 totalQty、各品类 ratio(1 位小数,误差归最大项)
热销 Top5 ORDER BY qty DESC, category_id ASC LIMIT 5(并列第 5 截断,对齐 ST-H2)

3.2 商城订单趋势

SELECT MONTH(o.create_time) AS m, COUNT(*) AS cnt
FROM biz_order o
WHERE YEAR(o.create_time) = #{statYear}
  AND o.order_status <> '5'
GROUP BY MONTH(o.create_time)

Service 补全 1~12 月,无数据月 cnt=0

3.3 店铺入驻

SELECT MONTH(create_time) AS m, COUNT(*) AS cnt
FROM biz_shop
WHERE YEAR(create_time) = #{statYear}
GROUP BY MONTH(create_time)

Service 算各月 占比yearTotal=0 则占比全空)。

3.4 消费区域 Top5

SELECT o.consignee_address, SUM(o.pay_amount) AS amount
FROM biz_order o
WHERE o.order_status = '3'
  AND YEAR(o.finish_time) = #{statYear}
GROUP BY ... -- 应用层 AddressCityParser 解析后 GROUP BY city
ORDER BY amount DESC
LIMIT 5
实现
城市 Java AddressCityParserconsignee_address 提取地级市;解析失败 → 未知
金额 amount / 10000保留 2 位小数

v1 不在库内加 consignee_city 字段;若地址格式不稳定,v1.1 可下单时冗余城市字段。

3.5 消费者评价词云

SELECT content FROM biz_goods_review
WHERE del_flag = '0' AND show_flag = '1'
  AND content IS NOT NULL AND TRIM(content) <> ''

应用层: ReviewWordCloudSupport.segment() → 去停用词 → 词频降序 → Top50全量结果 Redis 缓存 24h


4. 接口设计

基路径: /api/open/stats
认证: 请求头 X-Open-Token: {Base64密文}(见 §5.1)
权限: @Anonymous + OpenStatsTokenFilter(Filter 内校验,失败 401
响应: AjaxResultdata 为下表 VO。

4.1 接口一览

方法 路径 功能需求 缓存 TTL
GET /categorySales §5.1 品类销售 1 h
GET /hotCategoryRank §5.2 热销 Top5 品类 1 h
GET /orderTrend §5.3 订单趋势 1 h
GET /shopEntry §5.4 店铺入驻 1 h
GET /regionRank §5.5 区域 Top5 1 h
GET /reviewWordCloud §5.6 词云 Top50 24 h
GET /overview 组合 上述六项(大屏一次拉取) 取各子项最小 TTL

Query(可选,默认当年):

参数 适用 默认
statYear categorySales、hotCategoryRank、orderTrend、shopEntry、regionRank、overview 今年 yyyy

v1 不开放 任意历史区间(对齐功能需求非本期);参数 仅用于联调,生产大屏 可不传

4.2 响应结构(data

GET /categorySales

{
  "statYear": 2026,
  "totalQty": 1200,
  "items": [
    { "categoryId": 1, "categoryName": "兽药", "qty": 500, "ratio": 41.7 }
  ]
}

GET /hotCategoryRank

{
  "statYear": 2026,
  "items": [
    { "rank": 1, "categoryId": 1, "categoryName": "兽药", "qty": 500 }
  ]
}

GET /orderTrend

{
  "statYear": 2026,
  "items": [
    { "month": 1, "orderCount": 320 },
    { "month": 2, "orderCount": 0 }
  ]
}

GET /shopEntry

{
  "statYear": 2026,
  "yearTotal": 48,
  "items": [
    { "month": 1, "shopCount": 4, "ratio": 8.3 }
  ]
}

GET /regionRank

{
  "statYear": 2026,
  "items": [
    { "rank": 1, "city": "北京市", "amountWan": 12.35 }
  ]
}

GET /reviewWordCloud

{
  "items": [
    { "word": "质量好", "count": 86 }
  ]
}

GET /overview

{
  "categorySales": { },
  "hotCategoryRank": { },
  "orderTrend": { },
  "shopEntry": { },
  "regionRank": { },
  "reviewWordCloud": { }
}

4.3 错误码(示例)

HTTP code 场景
401 401 Token 缺失、Base64/AES 解密失败、或解密后 非标准 UUID
200 200 成功(含空数据 items: []
500 500 系统异常

5. 认证与性能

5.1 Token 校验流程

约定(调用方 ↔ 服务端):

明文 调用方生成 标准 UUID(如 550e8400-e29b-41d4-a716-446655440000
算法 AES-128-CBC,填充 PKCS5Padding
Key mdYJB5ENzTwEbql2(16 字节 UTF-8)
IV FU2GR30Iw76PjXbO(16 字节 UTF-8)
传输 密文 Base64 编码后放入请求头 X-Open-Token

调用方生成(示例逻辑):

uuid = UUID.randomUUID()
cipherBytes = AES-128-CBC-Encrypt(uuid UTF-8, key, iv)
token = Base64(cipherBytes)
Header: X-Open-Token: {token}

服务端校验:

请求 /api/open/stats/**
    → OpenStatsTokenFilter
        → 读 Header X-Open-Token
        → OpenStatsTokenCryptoSupport
            → Base64 解码
            → AES-128-CBC 解密(OpenStatsConstants 中 key/iv)
            → UUID.fromString 校验明文格式
            → 通过 → chain.doFilter
            → 失败 → 401 JSON,不访问 Controller
说明
查库、 校验 UUID 是否重复;每次请求可生成 新 UUID 再加密
Key/IV 变更须 服务端发版/改配置同步调用方 单 Token 吊销
后续可将 Key/IV 外置至 application.yml非本期必做

5.2 缓存策略

Redis Key 示例 TTL
openstats:cat:sales:{year} 1 h
openstats:cat:hot:{year} 1 h
openstats:order:month:{year} 1 h
openstats:shop:month:{year} 1 h
openstats:region:{year} 1 h
openstats:wordcloud 24 h
规则
Cache-Aside:先读 Redis,miss 再查 DB 并写入
年度指标捆绑刷新:同一 statYear5 项年度指标(品类销售、热销、订单趋势、入驻、区域)任一缓存 miss 或不全 时,一次性重建并写入全部 5 个 key,避免各接口数据时间点不一致
overview:在年度捆绑就绪且词云缓存存在时组装;任一缺失同步刷新年度捆绑 + 词云 后再返回
订单 确认收货/自动确认不主动删缓存;靠 TTL 准实时(对齐功能需求)
overview / 单接口复用 上述子 key,不再单独刷新部分指标

5.3 性能约束

要求
单次 SQL 禁止 无时间/状态条件的全表扫 biz_order_item必须 先过滤 biz_order
词云 禁止 每次请求全量分词;必须 走 24h 缓存
连接 统计走 从库只读(若架构有读写分离)为 可选优化

6. 业务规则映射

编号 实现
ST-R2 SQL order_status <> '5'
ST-R3 仅读 finish_time不读 ship_time
ST-C* / ST-H* §3.1 + Service 占比/Top5
ST-O* §3.2
ST-S* §3.3
ST-A* §3.4 + pay_amount
ST-RV* §3.5 + del_flag=0 AND show_flag=1
ST-T* Filter + AES 解密 + UUID 校验(OpenStatsTokenCryptoSupport
ST-TST3a 自动确认写入的 finish_time 参与 §3.1 统计年 过滤

7. 实施与 SQL 清单

阶段 内容
P1 Filter + AES Token 校验 + Controller 骨架
P2 五项 SQL 聚合 + Service + 单元测试(Mock 数据)
P3 Redis 缓存 + overview
P4 词云分词 + 停用词表资源文件
P5 可选索引 + 压测大屏并发
脚本 说明
sql/biz_order.sql idx_stats_finish;存量库执行文件末尾 ALTER 注释

8. 测试要点

编号 场景
OS-T1 无 Token / 明文 UUID / 错误密文 → 401
OS-T2 当年 3 品类完成单 → 占比合计 ≈100%
OS-T3 品类销量第 6 → 不在 hotCategoryRank
OS-T4 自动确认 finish_time当年 → 计入 categorySales
OS-T5 order_status=5 → 各接口均不计
OS-T6 overview 六项结构与单接口一致
OS-T7 第二次请求命中 Redis(集成环境可观测)

9. 修订记录

版本 说明
v1.0 首版:RuoYi3.9.2 + MySQL5.7;复用业务表聚合;六项 Open API + overview;Redis 缓存
v1.3 年度 5 项指标 捆绑刷新overview miss 时 同步更新 年度指标 + 词云,避免部分缓存过期
v1.2 §5.1/§5.2 改为 统计年YEAR(finish_time));接口参数统一 statYear;缓存 key/TTL 对齐年度指标
v1.1 Token 改为 AES-128-CBC 加密 UUID 校验;移除 biz_open_api_token 库表方案

文档版本:v1.3 · 关联《商城数据统计功能需求.md》v1.0.4 · 统计索引:sql/biz_order.sqlidx_stats_finish