# 大屏 — 交易销售统计 — 技术方案 > 依据:同目录 `大屏交易销售功能需求.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) | #### 响应示例(节选) ```json { "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` ```yaml 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. 交付清单 - [x] `TradeSalesScreenController` + `ITradeSalesScreenService` + VO + `TradeSalesScreenSupport` - [x] `TradeSalesScreenMapper` + XML(**§2.6**) - [x] `application.yml` 默认行情市场名 + **§4.1** 商城对接项 - [x] `MallStatsOpenApiClient` + Properties + Token + `mallStatsRestTemplate` - [x] 前端 `ruoyi-screen/src/views/tradeSales` 右栏六区块图表 - [x] 单元测试:牦牛口径 + 商城合并(`TradeSalesScreenServiceImplTest`、`MallStatsOpenTokenSupportTest` 等) - [ ] 集成测试:联调真实商城 Open API(可选) - [ ] 菜单权限 SQL;依赖 `biz_trade_order_line.grade_code`、`biz_grade_weight_config` 已执行 --- ## 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)`) |