大屏 — 交易销售统计 — 技术方案
依据:同目录 大屏交易销售功能需求.md v1.3。
牦牛交易:交易市场平台既有表(订单、明细、供应商、承销商、日度行情、等级重量配置等),本服务 MyBatis 只读聚合。
农资商城:不直连商城库表;经 MallStatsOpenApiClient 调用农资商城 Open API GET /api/open/stats/overview(口径见商城项目 doc/平台后台/外部接口/商城数据统计技术方案.md)。
本模块为大屏只读看板;看板主接口仍为单接口 GET /bigScreen/tradeSales/dashboard。
1. 技术架构
| 项 |
说明 |
| 后端 |
RuoYi v3.9.2(springboot2 分支):JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid |
| 数据库 |
MySQL 5.7.39,InnoDB,utf8mb4 |
| 前端 |
大屏交易销售专题页;切换 statYear 拉取看板主数据(单接口) |
| 代码包(牦牛) |
com.ruoyi.web.modules.screen(TradeSalesScreenController / ITradeSalesScreenService / vo / support) |
| 代码包(商城) |
com.ruoyi.web.modules.screen.mallstats(MallStatsOpenApiClient / MallStatsOpenProperties / Token / HTTP) |
分层(牦牛):Controller → ITradeSalesScreenService(口径编排、statDate 计算)→ TradeSalesScreenMapper(只读 SQL)+ TradeSalesScreenSupport(行情 7 日聚合、滚动 12 月趋势、占比)。
分层(商城):TradeSalesScreenServiceImpl.mergeMallStats → MallStatsOpenApiClient.fetchOverview(HTTP + 本地 Redis 缓存)→ 写入 TradeSalesDashboardVo 农资字段;失败降级空态,不阻断牦牛区块。
| 场景 |
行为 |
进入 / 切换 statYear |
调用 §3.1(牦牛 SQL + 商城 overview 一次 HTTP) |
| 商城未启用 / 调用失败 |
mallStatsAvailable=false;右栏空态;左栏正常 |
| 商城性能 |
优先 overview 一次拉六项;本地 Redis TTL 默认 5 min(bigscreen.mall-stats.cache-ttl-minutes);商城侧另有 Redis |
| 数据权限 |
全县聚合;不按供应商/用户过滤 |
统计日 statDate |
Y = 当前年 → 今日(Asia/Shanghai);Y 为历史年 → Y-12-31 |
| 年内订单上界 |
finish_time ≤ statDate 23:59:59(当前年);历史年 ≤ Y-12-31 23:59:59 |
| 入驻商铺数 |
biz_supplier 实时快照,不随 statYear 变 |
| 产地行情近 7 日 |
以 statDate 为终点 [statDate−6, statDate],与 Y 无关 |
| 按月趋势图 |
滚动最近 12 月(牦牛交易、商城订单);锚点 HomeScreenSupport.currentYearMonth();不随 statYear 变 |
| 性能 |
牦牛:默认实时聚合;可选 §2.3 缓存表;商城:overview + 本地 Redis |
依赖表(只读 — 牦牛侧)
| 表 |
用途 |
biz_trade_order |
已完成订单汇总、按月趋势、销售去向关联 |
biz_trade_order_line |
品质等级(grade_code)按行统计 |
biz_supplier |
入驻商铺数(有效供应商) |
biz_distributor |
销售去向 sales_destination(1 本市 / 2 本省 / 3 外省) |
biz_market_daily_quote |
产地行情近 7 日 |
biz_trade_market |
解析默认行情市场(名称「巴青牦牛交易市场」) |
biz_grade_weight_config |
等级编码 → 展示名称 |
外部依赖(只读 — 农资商城 Open API)
| 项 |
说明 |
| 基路径 |
{bigscreen.mall-stats.base-url}/api/open/stats |
| 聚合接口 |
GET /overview?statYear=(可选,默认当年;无 statDate 参数) |
| 认证 |
请求头 X-Open-Token(AES-128-CBC 加密 UUID;Key/IV 见 §4.1) |
| 响应 |
商城 AjaxResult;data 含六项 VO(与单接口字段一致) |
| 口径文档 |
商城项目 doc/平台后台/外部接口/商城数据统计技术方案.md v1.2(品类/热销/区域按 统计年 YEAR(finish_time)) |
不包含:本服务直连农资商城订单/店铺/品类/评价等任意表。
2. 数据库设计
2.1 本模块业务表
无新增业务台账表。 业务 DDL 以 sql/biz_trade_order.sql、sql/biz_supplier.sql、sql/biz_distributor.sql、sql/biz_market_daily_quote.sql、sql/biz_trade_market.sql、sql/biz_grade_weight_config.sql 为准。
2.2 公共参数
记 statYear = Y,statDate 见 §1。
| 边界 |
表达式 |
| 年区间起 |
Y-01-01 00:00:00 |
| 年区间止(已完成订单) |
statDate 23:59:59(见 §1) |
| 行情 7 日起 |
statDate − 6 日(含) |
| 行情 7 日止 |
statDate(含) |
| 按月分组键 |
MONTH(finish_time),应用层补全 1~12 |
默认行情市场(配置)
| 项 |
说明 |
| 优先 |
application.yml:bigscreen.trade-sales.default-market-name(默认 巴青牦牛交易市场) |
| 解析 |
biz_trade_market:del_flag='0' 且 market_name 等于配置值 → 取 id 作为 trade_market_id 过滤条件 |
| 未匹配 |
hasQuoteData=false,行情区块空态;日志告警 |
2.3 可选表 biz_trade_sales_screen_cache(性能优化)
非本期必建。
| 字段 |
类型 |
说明 |
id |
bigint(20) |
主键 |
stat_year |
int(11) |
统计年份 |
payload_json |
mediumtext |
§3.1 响应 JSON |
expire_time |
datetime |
过期时间 |
create_time |
datetime |
写入时间 |
索引:UNIQUE uk_stat_year (stat_year)。TTL 建议 1~5 分钟(与功能需求 §7.3 一致)。
2.4 查询口径(按表)
交易订单 biz_trade_order(对齐功能需求 §2.5)
| 项 |
条件 |
| 纳入 |
del_flag='0' AND order_status='1'(已完成) |
| 按年 |
finish_time ∈ [年区间起, 年区间止] |
| 总览 |
COUNT(*) 订单笔数;SUM(total_heads);SUM(total_amount) |
| 按月 |
GROUP BY MONTH(finish_time) → 当月 SUM(total_heads)、SUM(total_amount) |
销售去向(对齐 §2.8)
| 项 |
条件 |
| 关联 |
biz_trade_order o JOIN biz_distributor d ON o.distributor_id = d.id AND d.del_flag='0' |
| 订单 |
同 §2.4 已完成且在 Y 年内 |
| 分组 |
d.sales_destination(1 / 2 / 3) |
| 指标 |
SUM(o.total_heads)(头);占比分母 = 当年已完成订单 SUM(total_heads) |
应用层固定返回 3 档(本市 / 本省 / 外省),无数据为 0 头、ratio=0。
品质等级 biz_trade_order_line(对齐 §2.9)
| 项 |
条件 |
| 关联 |
line JOIN biz_trade_order o(同上已完成、年内) |
| 分组 |
line.grade_code |
| 名称 |
聚合时 max(line.grade_name);无快照时回退 biz_grade_weight_config.grade_name |
| 指标 |
COUNT(*) 明细行(头);占比分母 = 当年已完成订单明细总行数 |
| 空等级 |
grade_code IS NULL 的行不计入品质占比;若分母为 0 → hasGradeData=false |
应用层按配置表 sort_order 输出各等级;配置表有而当年无数据的等级 count=0、ratio=0(可选,与产品约定;默认仅返回 count>0 的等级 + 固定四档补 0,推荐固定返回配置表全部等级便于饼图)。
入驻商铺 biz_supplier(对齐 §2.10)
| 项 |
条件 |
| 指标 |
COUNT(*) |
| 条件 |
del_flag='0' |
| 与 Y |
无年份条件 |
日度行情 biz_market_daily_quote(对齐 §2.6、§2.7)
| 项 |
条件 |
| 范围 |
del_flag='0' AND trade_market_id = {默认市场ID} AND quote_date BETWEEN 7 日起止 |
| 原始行 |
窗口内全部日度记录(含多维度:公母、重量档等) |
应用层按 quote_date 聚合(TradeSalesScreenSupport)
| 聚合项 |
规则 |
| 当日最低价 |
该日各条 price_min 的 MIN |
| 当日最高价 |
该日各条 price_max 的 MAX |
| 当日均价 |
该日各条 avg_price 的 算术平均 |
| 7 日卡片 |
7 个自然日「当日均价」有效值的平均;7 日内「当日最低价」的最小、「当日最高价」的最大;lastQuoteDate = 窗口内最大 quote_date |
| 价格单位 |
窗口内 quote_date 最大那天的记录中 create_time 最新一条的 price_unit(1 元/头 / 2 元/斤) |
| 缺日 |
7 日横轴仍输出该日;minPrice/maxPrice/avgPrice 均为 0(对齐功能需求 §5.3.2 默认) |
2.5 索引建议(既有表)
| 表 |
建议 |
biz_trade_order |
(order_status, del_flag, finish_time) 或 idx_finish_time |
biz_trade_order_line |
(order_id)(已有 idx_order_id) |
biz_market_daily_quote |
(trade_market_id, quote_date, del_flag) |
biz_distributor |
沿用 idx_sales_destination |
2.6 Mapper 扩展(建议)
| Mapper |
方法 |
说明 |
TradeSalesScreenMapper |
selectTradeOverviewByYear(y, endTime) |
总览三项 |
TradeSalesScreenMapper |
selectMonthlyTradeByYear(y, endTime) |
按月头数、金额 |
TradeSalesScreenMapper |
selectSalesDestinationByYear(y, endTime) |
三档头数 |
TradeSalesScreenMapper |
selectQualityGradeByYear(y, endTime) |
按 grade_code 计数 |
TradeSalesScreenMapper |
selectQuotesInWindow(marketId, startDate, endDate) |
行情原始行 |
TradeSalesScreenMapper |
selectDistinctFinishYears() |
可选年份 |
BizSupplierMapper |
countForScreen() |
可复用首页 countForScreen() |
XML:mapper/screen/TradeSalesScreenMapper.xml。
3. 接口设计
统一响应:AjaxResult(code / msg / data)。
权限标识(示例):bigScreen:tradeSales:query
Base Path:/bigScreen/tradeSales
| # |
说明 |
Method |
URI |
权限 |
| 3.1 |
看板主数据(牦牛五区块 + 农资六区块合一) |
GET |
/bigScreen/tradeSales/dashboard |
bigScreen:tradeSales:query |
前端:进入页 / 切换 statYear / 手动刷新 → 仅调用 3.1。
3.1 看板主数据
Query
| 参数 |
类型 |
必填 |
说明 |
statYear |
string |
Y |
四位年份;非法 → 业务异常(复用 AchievementReportValidation.parseStatYear) |
forceRefresh |
boolean |
N |
默认 false;true 时跳过 §2.3 缓存 |
响应 data(顶层)
| 字段 |
类型 |
说明 |
statYear |
int |
回显 |
statDate |
string |
统计时点 yyyy-MM-dd |
availableYears |
int[] |
biz_trade_order 已完成订单 YEAR(finish_time) 并集 + 当前年,降序 |
tradeOverview |
object |
交易总览 §3.1.1 |
originQuote |
object |
产地行情 §3.1.2 |
tradeMonthlyTrend |
array |
数额趋势 §3.1.3 |
salesDestination |
array |
销售去向 §3.1.4 |
qualityGrade |
object |
品质等级 §3.1.5 |
mallStatsAvailable |
boolean |
是否成功接入商城统计 |
categorySales |
object |
农资品类销售 §3.1.6 |
hotCategoryRank |
object |
热销农资 Top5 §3.1.7 |
mallOrderTrend |
object |
商城订单趋势 §3.1.8 |
shopEntry |
object |
店铺入驻 §3.1.9 |
regionRank |
object |
消费区域 Top5 §3.1.10 |
reviewWordCloud |
object |
消费者评价词云 §3.1.11 |
3.1.1 tradeOverview
| 字段 |
类型 |
说明 |
orderCount |
long |
牦牛交易订单数(笔) |
tradeHeads |
long |
牦牛交易量(头) |
tradeAmount |
decimal |
牦牛交易总额(元) |
supplierCount |
long |
入驻商铺数(家),实时(牦牛供应商快照) |
agriOrderCount |
long |
农资订单量(单);滚动 12 月 mallOrderTrend 各月 orderCount 求和;未接入时为 null |
agriSalesAmount |
decimal |
农资销售额(元);商城 Open API v1 未提供年度总额,未接入或未返回时为 null,前端显示 — |
3.1.2 originQuote
| 字段 |
类型 |
说明 |
hasQuoteData |
boolean |
7 日内是否存在行情登记 |
avgPrice7d |
decimal |
7 日均价;无数据可为 null |
minPrice7d |
decimal |
7 日最低价 |
maxPrice7d |
decimal |
7 日最高价 |
lastQuoteDate |
string |
最近行情日 yyyy-MM-dd;无则 null |
priceUnit |
int |
1 元/头 / 2 元/斤 |
priceUnitLabel |
string |
如 元/头 |
dailyTrend |
array |
固定 7 条,按日期升序 |
dailyTrend[]:
| 字段 |
说明 |
quoteDate |
yyyy-MM-dd |
minPrice / maxPrice / avgPrice |
当日聚合价;缺日为 0 |
3.1.3 tradeMonthlyTrend[]
固定 12 条;滚动最近 12 自然月(buildRolling12MonthTradeTrend)。
| 字段 |
说明 |
statYear |
自然年 |
month |
1~12 |
tradeHeads |
该月已完成订单总头数 |
tradeAmount |
该月已完成订单总金额(元) |
查询窗口:finish_time ∈ [anchor−11 月 1 日, rollingWindowEnd];按月 YEAR+MONTH 聚合。
3.1.4 salesDestination[]
固定 3 条。
| 字段 |
说明 |
salesDestination |
1 / 2 / 3 |
salesDestinationName |
本市 / 本省 / 外省 |
tradeHeads |
头数 |
ratio |
占比 0~100 数值(如 33.3 表示 33.3%),保留 1 位小数 |
3.1.5 qualityGrade
| 字段 |
说明 |
hasGradeData |
当年是否存在 grade_code 非空的已完成明细 |
totalHeads |
参与占比计算的明细总行数 |
items |
数组 |
items[]:
| 字段 |
说明 |
gradeCode |
A / B / C / D |
gradeName |
一级 / 二级等 |
tradeHeads |
头数 |
ratio |
占比 0~100,保留 1 位小数 |
3.1.6 categorySales(商城 Open API 透传)
| 字段 |
说明 |
statYear |
统计年(当年已完成单按 finish_time 归属 Y) |
totalQty |
当年总销量(件) |
items[] |
categoryId、categoryName、qty、ratio |
3.1.7 hotCategoryRank
| 字段 |
说明 |
statYear |
同 §3.1.6 |
items[] |
rank、categoryId、categoryName、qty(Top5) |
3.1.8 mallOrderTrend
| 字段 |
说明 |
statYear |
锚点年(回显) |
items[] |
固定 12 条:滚动最近 12 月;每项含 statYear、month、orderCount |
跨年时分别拉取商城 Open API 两年 orderTrend 并合并,再 buildRolling12MonthMallOrderTrend。
3.1.9 shopEntry
| 字段 |
说明 |
statYear |
统计年 |
yearTotal |
当年入驻店铺总数 |
items[] |
month、shopCount、ratio(各月占 yearTotal 百分比) |
3.1.10 regionRank
| 字段 |
说明 |
statYear |
统计年 |
items[] |
rank、city、amountWan(万元,2 位小数) |
3.1.11 reviewWordCloud
| 字段 |
说明 |
items[] |
word、count(Top50) |
响应示例(节选)
{
"statYear": 2026,
"statDate": "2026-05-20",
"availableYears": [2026, 2025],
"tradeOverview": {
"orderCount": 12,
"tradeHeads": 48,
"tradeAmount": 960000.00,
"supplierCount": 5,
"agriOrderCount": 320,
"agriSalesAmount": null
},
"originQuote": {
"hasQuoteData": true,
"avgPrice7d": 32.5,
"minPrice7d": 30.0,
"maxPrice7d": 35.0,
"lastQuoteDate": "2026-05-20",
"priceUnit": 2,
"priceUnitLabel": "元/斤",
"dailyTrend": [
{ "quoteDate": "2026-05-14", "minPrice": 31.0, "maxPrice": 33.0, "avgPrice": 32.0 }
]
},
"tradeMonthlyTrend": [
{ "month": 1, "tradeHeads": 10, "tradeAmount": 200000.00 },
{ "month": 12, "tradeHeads": 0, "tradeAmount": 0 }
],
"salesDestination": [
{ "salesDestination": 1, "salesDestinationName": "本市", "tradeHeads": 20, "ratio": 41.7 }
],
"qualityGrade": {
"hasGradeData": true,
"totalHeads": 48,
"items": [
{ "gradeCode": "B", "gradeName": "二级", "tradeHeads": 30, "ratio": 62.5 }
]
},
"mallStatsAvailable": true,
"categorySales": { "statYear": 2026, "totalQty": 1200, "items": [] },
"hotCategoryRank": { "statYear": 2026, "items": [] },
"mallOrderTrend": { "statYear": 2026, "items": [{ "month": 1, "orderCount": 320 }] },
"shopEntry": { "statYear": 2026, "yearTotal": 48, "items": [] },
"regionRank": { "statYear": 2026, "items": [] },
"reviewWordCloud": { "items": [{ "word": "质量好", "count": 86 }] }
}
服务端逻辑(摘要)
1. year = parseStatYear(statYear); statDate = resolveStatDate(year); endTime = statDate 23:59:59
2. tradeOverview = orderMapper 总览(year, endTime) + supplierMapper.countForScreen()
3. tradeMonthlyTrend = orderMapper 按月 + buildMonthlySeries(12, year, statDate)
4. salesDestination = orderMapper JOIN distributor + fill3Buckets + ratio
5. qualityGrade = lineMapper 分组 + join grade_config + ratio; hasGradeData
6. marketId = resolveDefaultMarketId(configName)
7. rows = quoteMapper 7日窗口(marketId); originQuote = Support.aggregateQuote7d(rows, statDate)
8. availableYears = distinct finish years ∪ current year
9. mall = MallStatsOpenApiClient.fetchOverview(year)
→ 请求商城 `GET /overview?statYear={year}`(**不传** statDate)
→ 成功:Support.applyMallStats(vo, mall) + enrichOverviewWithMall(tradeOverview, mall)
→ 失败/未启用:Support.applyEmptyMallStats(vo)
10. return AjaxResult.success(vo)
4. 农资商城对接配置
4.1 application.yml
bigscreen:
trade-sales:
default-market-name: 巴青牦牛交易市场
mall-stats:
enabled: false # 生产改为 true
base-url: "" # 商城服务根地址,如 http://shop-host:8080
aes-key: mdYJB5ENzTwEbql2
aes-iv: FU2GR30Iw76PjXbO
connect-timeout-ms: 5000
read-timeout-ms: 15000
cache-ttl-minutes: 5 # 本地 Redis:bigscreen:mallstats:overview:{statYear}
| 项 |
说明 |
| Token |
每次请求生成 UUID → AES-128-CBC 加密 → Base64 → 头 X-Open-Token |
| 降级 |
enabled=false 或 base-url 空或 HTTP/401/500 → mallStatsAvailable=false,不抛错 |
| 缓存 |
Cache-Aside;与商城侧 Redis 叠加,减少跨服务调用 |
5. 菜单与权限(示例)
| 类型 |
名称 |
权限标识 |
| 菜单 |
大屏交易销售 |
bigScreen:tradeSales:query |
SQL 示例:sql/big_screen_trade_sales_perm.sql(挂载「大屏」父菜单下)。
6. 交付清单
7. 修订记录
| 版本 |
日期 |
说明 |
| 1.0 |
2026-05-20 |
初稿:无新表;单接口五区块;农资商城不纳入 |
| 1.1 |
2026-05-20 |
增刊农资商城:Open API overview 对接 + 本地 Redis;dashboard 扩展六区块;agriOrderCount;agriSalesAmount 待商城 API 补年度总额 |
| 1.2 |
2026-05-20 |
对齐商城 Open API v1.2:overview 仅传 statYear;categorySales / hotCategoryRank 响应字段改为 statYear;品类/热销/区域口径为统计年(YEAR(finish_time)) |