# 大屏 — 畜牧资源统计 — 技术方案 > 依据:同目录 `大屏畜牧资源功能需求.md` v1.2。数据源为 **AI 诊断** 既有表(会话、消息、使用用户台账)。本模块为**大屏只读看板**;**不**统计移动端「畜牧资源」资讯视图 `v_livestock_resource`。 --- ## 1. 技术架构 | 项 | 说明 | | --- | --- | | **后端** | RuoYi **v3.9.2**(**springboot2** 分支):JDK 8、Spring Boot 2.x、Spring MVC、MyBatis、Druid | | **数据库** | MySQL **5.7.39**,InnoDB,`utf8mb4` | | **前端** | `ruoyi-screen` 畜牧资源专题页(`/livestock-resources`);**切换 `statYear`** 拉取看板主数据(本期**单接口**) | | **代码包** | `com.ruoyi.web.modules.screen`(`LivestockResourceScreenController` / `ILivestockResourceScreenService` / `vo` / `support`) | **分层**:`Controller` → `ILivestockResourceScreenService`(口径编排、`statDate`/`statDateTime`)→ `LivestockResourceScreenMapper`(只读 SQL)+ `LivestockResourceScreenSupport`(7 日/24 小时补点、分类归一、占比、分档、关键词)。 | 场景 | 行为 | | --- | --- | | 进入 / 切换 `statYear` | 调用 **§3.1** | | 数据权限 | **全县**聚合;不按提问人/机构过滤 | | 统计日 `statDate` | 复用 `HomeScreenSupport.resolveStatDate(Y)`:`Y`=当前年 → 今日(`Asia/Shanghai`);历史年 → `Y-12-31` | | 统计时刻 `statDateTime` | 请求时 `LocalDateTime.now(Asia/Shanghai)`;响应回显用 | | 年内上界 | 会话 `create_time`;AI 消息 **`create_time`** ≤ `statDate 23:59:59`(当前年) | | 「今日」四项 | 仅当 `Y` = 当前自然年时按 `statDate` 自然日统计;否则固定 **0** | | 用户结构 | `biz_consult_user` **全表快照**,**不**随 `Y` 变 | | 7 日 / 分时 | 7 日以 `statDate` 滚动;分时为 **统计日 0~23 时**,**与 Y 无关** | | 已隐藏会话 | **不**过滤 `vet_visible`(对齐功能需求 **§2.18#2**) | | 性能 | 默认实时聚合;可选 **§2.3** 缓存表 | **依赖表(只读)** | 表 | 用途 | | --- | --- | | `biz_consult_session` | AI 会话数、用户去重、7 日/24 小时会话、分档分母 | | `biz_consult_message` | 提问量(AI 回复)、耗时、分类、状态、关键词语料 | | `biz_consult_user` | 新老用户结构 | **业务常量(与 `ConsultSessionRules` / `AiConsultConstants` 一致)** | 常量 | 值 | | --- | --- | | `consult_type` AI 问诊 | `2` | | `sender_role` 提问人 / AI | `1` / `3` | | AI 消息 `sender_user_id` | `0` | | `msg_type` 文本 | `1` | | 提问分类编码 | `1`~`4`(养殖技巧/疾病诊疗/用药指导/设备操作) | --- ## 2. 数据库设计 ### 2.1 本模块业务表 **无新增业务台账表。** DDL:`sql/biz_consult_session.sql`、`sql/biz_consult_message.sql`、`sql/biz_consult_user.sql`。 > 当前问诊表**无** `del_flag`;纳入条件为:`consult_type=2` 的会话及其消息。已隐藏(`vet_visible=0`)**仍统计**。 ### 2.2 时间边界(公共参数) 记 `statYear = Y`,`statDate`、`statDateTime` 见 **§1**。 | 边界 | 表达式 | | --- | --- | | 年区间起 | `Y-01-01 00:00:00` | | 年区间止(会话 `create_time`、AI 消息 `create_time`) | `statDate 23:59:59` | | 统计日区间 | `statDate 00:00:00` ~ `statDate 23:59:59` | | 近 7 日 | `[statDate − 6 日 00:00:00, statDate 23:59:59]` | | 统计日分时 | 统计日自然日;`HOUR(create_time)` / `HOUR(m.create_time)` 分 **24** 桶(0~23) | **会话别名(下文)**:`s` = `biz_consult_session`,条件 `s.consult_type = 2`。 **AI 消息别名**:`m_ai` = `biz_consult_message`,`m_ai.sender_role = 3 AND m_ai.sender_user_id = 0`,且 `EXISTS` 关联 `s.id = m_ai.session_id`。 **提问人消息别名**:`m_ask` = `biz_consult_message`,`m_ask.sender_role = 1`,且关联 AI 会话 `s`。 ### 2.3 可选表 `biz_livestock_resource_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 分钟**。 ### 2.4 查询口径(按表) #### 2.4.1 核心总览 | 指标 | SQL 要点 | | --- | --- | | 累计使用用户 | `COUNT(DISTINCT s.asker_user_id)`,`s.create_time` ∈ 年区间 | | 今日活跃用户 | `Y`=当前年:`COUNT(DISTINCT asker_user_id)`,`s.update_time` ∈ 统计日;否则 **0** | | 日新增用户 | `Y`=当前年:`create_time` 与 `update_time` 均在统计日,且 `asker_user_id` 不在 `create_time < 统计日` 的会话中;否则 **0** | | 累计会话数 | `COUNT(s.id)`,`s.create_time` ∈ 年区间 | | 今日会话数 | `Y`=当前年:`COUNT(s.id)`,`s.create_time` ∈ 统计日区间;否则 **0** | | 累计提问量 | `COUNT(m_ai.id)`,`m_ai.create_time` ∈ 年区间 | | 今日提问量 | `Y`=当前年:`COUNT(m_ai.id)`,`m_ai.create_time` ∈ 统计日;否则 **0** | | 平均响应时长(秒) | `AVG(m_ai.cost_time) / 1000.0`,年区间内且 `m_ai.cost_time IS NOT NULL`(**含 0**);无记录 → **0** | #### 2.4.2 近 7 日活跃度 | 维度 | SQL 要点 | | --- | --- | | 会话数/日 | `DATE(s.create_time)` 分组,`COUNT(s.id)` | | 提问量/日 | `DATE(m_ai.create_time)` 分组,`COUNT(m_ai.id)` | 应用层:固定输出 **7** 个点(`statDate−6` … `statDate`),缺日补 **0**。 #### 2.4.3 用户结构 `biz_consult_user` | 类型 | 条件 | | --- | --- | | 新用户 | `DATEDIFF(last_use_time, first_use_time) <= 180` | | 老用户 | `> 180` | `COUNT(*)` 各一类;占比 = 人数 / (新+老),**不**加 `Y` 条件。 #### 2.4.4 提问分类 / 模型调用 / 热点榜(同源) | 项 | 条件 | | --- | --- | | 统计行 | `m_ai`(`sender_role=3`、`sender_user_id=0`),`create_time` ∈ 年区间 | | 分类键 | 同会话内 `send_time ≤ m_ai.send_time` 的最近提问人消息 `ai_category`;空 → `UNCAT` | | 条数 | `GROUP BY` 分类键 `COUNT(*)`;`MAX(m_ai.create_time)` 供热点二次排序 | | 占比 | 分类 AI 回复条数 / **当年全部 AI 回复** × 100 | | 热点 Top5 | 条数降序;同分按 `lastEventTime` 降序;取 **5** | #### 2.4.5 统计日分时热度 | 维度 | SQL 要点 | | --- | --- | | 会话 | `s.create_time` ∈ 统计日,`GROUP BY HOUR(create_time)` | | 提问量 | `m_ai.create_time` ∈ 统计日,`GROUP BY HOUR(m_ai.create_time)` | 应用层:固定 **24** 桶 `00:00`~`23:00`;`hourlyHeat.windowEnd` = `statDate`(`yyyy-MM-dd`)。 #### 2.4.6 单次会话提问数量分布 子查询:按 `session_id` 统计年内 `m_ai` 条数 `reply_cnt`,再分档: | 档位 code | `reply_cnt` | | --- | --- | | `LIGHT` | 1~2 | | `NORMAL` | 3~4 | | `ACTIVE` | 5~10 | | `DEEP` | >10 | `COUNT(session_id)` / 年内会话总数 → 占比;四档固定返回,无数据为 0。 #### 2.4.7 会话状态分布 | 状态 | 条件 | | --- | --- | | `OK` 正常应答 | `m_ai.cost_time IS NOT NULL`(**含 0**) | | `FAIL` 调用失败 | `m_ai.cost_time IS NULL` | 年内 `COUNT(*)` 及占比;两者之和 = 年内 AI 消息总数。 #### 2.4.8 高频关键词 | 项 | 条件 | | --- | --- | | 语料 | `m_ask.msg_type = 1`,`m_ask.content` 非空,`m_ask.send_time` ∈ 年区间 | | 处理 | `LivestockResourceScreenSupport.extractTopKeywords(texts, 30)`:分词 + 停用词(`application.yml` 可配路径或内置短表) | ### 2.5 索引建议(既有表) | 表 | 建议 | | --- | --- | | `biz_consult_session` | `(consult_type, create_time)`、`(consult_type, update_time)` | | `biz_consult_message` | `(session_id, sender_role, create_time)`、`(create_time, sender_role)` | ### 2.6 Mapper 方法(建议) | Mapper | 方法 | 说明 | | --- | --- | --- | | `LivestockResourceScreenMapper` | `selectOverview(...)` | 总览 8 项(可拆 2~3 条 SQL) | | | `selectDailySessionCounts(start, end)` | 7 日会话 | | | `selectDailyAiReplyCounts(start, end)` | 7 日提问量 | | | `selectUserStructure()` | 新老用户 | | | `selectAskerCategoryCounts(yearStart, yearEnd)` | AI 回复按分类键聚合 | | | `selectAiReplyTotal(yearStart, yearEnd)` | 分类占比分母(年内 AI 回复总数) | | | `selectHourlySessionCounts(dayStart, dayEnd)` | 统计日 24 小时会话 | | | `selectHourlyAiReplyCounts(dayStart, dayEnd)` | 统计日 24 小时提问量 | | | `selectSessionReplyBuckets(yearStart, yearEnd)` | 四档会话数 | | | `selectYearSessionTotal(yearStart, yearEnd)` | 年内会话总数 | | | `selectAiReplyStatusCounts(yearStart, yearEnd)` | 正常/失败 | | | `selectTextAskerContents(yearStart, yearEnd)` | 关键词语料 | | | `selectDistinctSessionYears()` | 可选年份下拉 | XML:`mapper/screen/LivestockResourceScreenMapper.xml`。 --- ## 3. 接口设计 **统一响应**:`AjaxResult`(`code` / `msg` / `data`)。 **Base Path**:`/bigScreen/livestockResource` **权限标识(示例)**:`bigScreen:livestockResource:query` | # | 说明 | Method | URI | | --- | --- | --- | --- | | 3.1 | 看板主数据(十区块合一) | GET | `/bigScreen/livestockResource/dashboard` | 前端:进入页 / 切换 `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` | | `statDateTime` | string | `yyyy-MM-dd HH:mm:ss`,24h 窗口终点 | | `availableYears` | int[] | `biz_consult_session` 在 `consult_type=2` 下 `YEAR(create_time)` 并集 + 当前年,降序 | | `overview` | object | 核心总览 **§3.1.1** | | `activityTrend` | object | 活跃度趋势 **§3.1.2** | | `userStructure` | object | 用户结构 **§3.1.3** | | `categoryShare` | object | 提问分类占比 **§3.1.4** | | `modelCallAnalysis` | array | 模型调用分析(与分类同源) **§3.1.5** | | `hourlyHeat` | object | 分时热度 **§3.1.6** | | `sessionReplyDistribution` | array | 会话提问数量分布 **§3.1.7** | | `replyStatusDistribution` | object | 会话状态分布 **§3.1.8** | | `hotTopics` | array | 热点问题榜单 Top5 **§3.1.9** | | `topKeywords` | array | 高频关键词 Top30 **§3.1.10** | #### 3.1.1 `overview` | 字段 | 类型 | 说明 | | --- | --- | --- | | `totalUsers` | long | 累计使用用户 | | `todayActiveUsers` | long | 今日活跃用户 | | `todayNewUsers` | long | 日新增用户 | | `totalSessions` | long | 累计会话数 | | `todaySessions` | long | 今日会话数 | | `totalQuestionCount` | long | 累计提问量(AI 回复条数) | | `todayQuestionCount` | long | 今日提问量 | | `avgResponseSeconds` | decimal | 平均响应时长(秒),**1** 位小数 | #### 3.1.2 `activityTrend` | 字段 | 类型 | 说明 | | --- | --- | --- | | `dailySeries` | array | 固定 **7** 条 | `dailySeries[]`: | 字段 | 说明 | | --- | --- | | `date` | `yyyy-MM-dd` | | `sessionCount` | 会话数 | | `questionCount` | 提问量(AI 回复) | #### 3.1.3 `userStructure` | 字段 | 类型 | 说明 | | --- | --- | --- | | `newUserCount` | long | 新用户 | | `oldUserCount` | long | 老用户 | | `newUserRatio` | decimal | 占比 0~100 | | `oldUserRatio` | decimal | 占比 0~100 | #### 3.1.4 `categoryShare` | 字段 | 类型 | 说明 | | --- | --- | --- | | `totalAskerMessages` | long | 分母:年内全部 **AI 回复**(字段名沿用,与提问人无关) | | `items` | array | 分类项 | `items[]`:`categoryCode`、`categoryName`、`count`、`ratio`(0~100,1 位小数)。固定含 `1`~`4` + `UNCAT`(未分类),无数据 `count=0`。 #### 3.1.5 `modelCallAnalysis[]` 与 `categoryShare.items` **同源**(可复用引用):`categoryCode`、`categoryName`、`count`。展示为柱状图时前端取 `count`。 #### 3.1.6 `hourlyHeat` | 字段 | 类型 | 说明 | | --- | --- | --- | | `windowEnd` | string | 回显 `statDateTime` | | `hourlySeries` | array | 固定 **24** 条 | `hourlySeries[]`:`bucketStart`(`HH:mm`)、`sessionCount`、`questionCount`。 #### 3.1.7 `sessionReplyDistribution[]` 固定 **4** 条。 | 字段 | 说明 | | --- | --- | | `bandCode` | `LIGHT` / `NORMAL` / `ACTIVE` / `DEEP` | | `bandName` | 轻度 / 一般 / 活跃 / 深度 | | `sessionCount` | 会话数 | | `ratio` | 占年内会话总数 % | #### 3.1.8 `replyStatusDistribution` | 字段 | 说明 | | --- | --- | | `totalAiReplies` | 年内 AI 回复总数 | | `items` | 固定 2 条:`OK`(正常应答)、`FAIL`(调用失败),含 `count`、`ratio` | #### 3.1.9 `hotTopics[]` 最多 **5** 条:`categoryCode`、`categoryName`、`count`、`ratio`,按 `ratio` 或 `count` 降序。 #### 3.1.10 `topKeywords[]` 最多 **30** 条:`keyword`、`count`。 #### 响应示例(节选) ```json { "statYear": 2026, "statDate": "2026-05-20", "statDateTime": "2026-05-20 15:30:00", "overview": { "totalUsers": 120, "todayActiveUsers": 8, "todayNewUsers": 2, "totalSessions": 350, "todaySessions": 5, "totalQuestionCount": 980, "todayQuestionCount": 12, "avgResponseSeconds": 3.2 }, "activityTrend": { "dailySeries": [ { "date": "2026-05-14", "sessionCount": 3, "questionCount": 10 } ] }, "categoryShare": { "totalAskerMessages": 500, "items": [ { "categoryCode": "2", "categoryName": "疾病诊疗", "count": 200, "ratio": 40.0 } ] } } ``` #### 服务端逻辑(摘要) ``` 1. year = parseStatYear(statYear) 2. statDate = resolveStatDate(year); statDateTime = now(Shanghai) 3. yearStart/end, dayStart/end, sevenStart, window24Start = Support.bounds(...) 4. overview = Mapper 总览;avgResponseSeconds = Support.roundSeconds(avgMs/1000) 5. activityTrend = Mapper 7日 + buildDailySeries(7, statDate) 6. userStructure = Mapper 新老 + ratio 7. category = Mapper 分类 + total;categoryShare/modelCall/hotTopics = Support.buildCategoryViews(...) 8. hourlyHeat = Mapper 24h + buildHourlySeries(24, statDateTime) 9. sessionReplyDistribution = Mapper 分档 + ratio 10. replyStatusDistribution = Mapper OK/FAIL + ratio 11. topKeywords = Support.extractTopKeywords(Mapper 文本语料, 30) 12. availableYears = distinct session years ∪ current year 13. return AjaxResult.success(vo) ``` --- ## 4. 菜单与权限(示例) | 类型 | 名称 | 权限标识 | | --- | --- | --- | | 菜单 | 大屏畜牧资源 | `bigScreen:livestockResource:query` | SQL 示例:`sql/big_screen_livestock_resource_perm.sql`(挂载「大屏」父菜单下)。 --- ## 5. 交付清单 - [x] `LivestockResourceScreenController` + `ILivestockResourceScreenService` + VO + `LivestockResourceScreenSupport` - [x] `LivestockResourceScreenMapper` + XML(**§2.6**) - [ ] `ruoyi-screen` 对接 `GET /bigScreen/livestockResource/dashboard`(本期不改前端) - [x] 单元测试:历史年今日为 0、7 日补 0、统计日 24 桶、分类占比、耗时含 0 - [x] 接口测试:MockMvc `dashboard` - [ ] 菜单权限 SQL;依赖 `biz_consult_user`、消息 `cost_time`/`ai_category` 字段已上线 --- ## 6. 修订记录 | 版本 | 日期 | 说明 | | --- | --- | --- | | 1.0 | 2026-05-20 | 初稿:无新表;单接口十区块;对齐 `大屏畜牧资源功能需求.md` v1.1 | | 1.2 | 2026-05-20 | 对齐草稿:今日活跃 `update_time`、日新增双时间、AI 回复+分类归因、统计日 0~23 时分、`create_time` 消息归属 |