依据: 《商城数据统计功能需求.md》v1.0.4
关联: 《订单管理技术方案.md》v1.0.2、《商品分类功能需求》v1.5、《商城设置技术方案.md》v1.1
范围: 对外 只读 六项统计;无 平台 Web 菜单;不新建 业务明细表,聚合查询 现有订单/分类/店铺/评价数据。
原则: 统计 不阻塞 交易写路径;准实时 + Redis 缓存;Token 与 会员 JWT 隔离。
| 项 | 选型 |
|---|---|
| 基础框架 | RuoYi v3.9.2(springboot2 分支) |
| 数据库 | MySQL 5.7.39 |
| ORM / 响应 | MyBatis;AjaxResult(code / msg / data) |
| 缓存 | Redis(统计结果 TTL,见 §5) |
| 认证 | AES-128-CBC 加密 UUID(请求头校验;不走 @PreAuthorize 登录态;无 Token 库表) |
| 分词(词云) | HanLP 或 Ansj(二选一,应用层;结果缓存) |
| 时区 | Asia/Shanghai(统计日/年边界) |
文档目录: 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,不 进入业务。
| 原则 | 说明 |
|---|---|
| 不建统计事实表(v1) | 六项指标 实时/缓存聚合;避免与订单写路径双写 |
| 只读关联 | 禁止 统计接口内 UPDATE 业务表 |
| 不建 Token 表 | 认证为 固定 AES 密钥 解密后校验 UUID;无 biz_open_api_token |
| 表 | 统计项 | 关键字段 |
|---|---|---|
| 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='未分类'(全接口统一)
biz_order)脚本: sql/biz_order.sql — 建表已含 idx_stats_finish(order_status, finish_time);存量库见该文件 末尾升级注释。
条件:
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) |
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。
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 则占比全空)。
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 AddressCityParser 从 consignee_address 提取地级市;解析失败 → 未知 |
| 金额 | amount / 10000,保留 2 位小数 |
v1 不在库内加
consignee_city字段;若地址格式不稳定,v1.1 可下单时冗余城市字段。
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。
基路径: /api/open/stats
认证: 请求头 X-Open-Token: {Base64密文}(见 §5.1)
权限: @Anonymous + OpenStatsTokenFilter(Filter 内校验,失败 401)
响应: AjaxResult;data 为下表 VO。
| 方法 | 路径 | 功能需求 | 缓存 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 不开放 任意历史区间(对齐功能需求非本期);参数 仅用于联调,生产大屏 可不传。
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": { }
}
| HTTP | code | 场景 |
|---|---|---|
| 401 | 401 | Token 缺失、Base64/AES 解密失败、或解密后 非标准 UUID |
| 200 | 200 | 成功(含空数据 items: []) |
| 500 | 500 | 系统异常 |
约定(调用方 ↔ 服务端):
| 项 | 值 |
|---|---|
| 明文 | 调用方生成 标准 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(非本期必做) |
| 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 并写入 |
年度指标捆绑刷新:同一 statYear 下 5 项年度指标(品类销售、热销、订单趋势、入驻、区域)任一缓存 miss 或不全 时,一次性重建并写入全部 5 个 key,避免各接口数据时间点不一致 |
overview:在年度捆绑就绪且词云缓存存在时组装;任一缺失 时 同步刷新年度捆绑 + 词云 后再返回 |
| 订单 确认收货/自动确认 后 不主动删缓存;靠 TTL 准实时(对齐功能需求) |
overview / 单接口 均 复用 上述子 key,不再单独刷新部分指标 |
| 项 | 要求 |
|---|---|
| 单次 SQL | 禁止 无时间/状态条件的全表扫 biz_order_item;必须 先过滤 biz_order |
| 词云 | 禁止 每次请求全量分词;必须 走 24h 缓存 |
| 连接 | 统计走 从库只读(若架构有读写分离)为 可选优化 |
| 编号 | 实现 |
|---|---|
| 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 统计年 过滤 |
| 阶段 | 内容 |
|---|---|
| P1 | Filter + AES Token 校验 + Controller 骨架 |
| P2 | 五项 SQL 聚合 + Service + 单元测试(Mock 数据) |
| P3 | Redis 缓存 + overview |
| P4 | 词云分词 + 停用词表资源文件 |
| P5 | 可选索引 + 压测大屏并发 |
| 脚本 | 说明 |
|---|---|
sql/biz_order.sql |
含 idx_stats_finish;存量库执行文件末尾 ALTER 注释 |
| 编号 | 场景 |
|---|---|
| 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(集成环境可观测) |
| 版本 | 说明 |
|---|---|
| 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.sql → idx_stats_finish