Skip to content

统计分析 ​

当前实现范围 ​

官方前端的“应用 → 概览”页面用统计接口渲染四组卡片与曲线。四个官方统计接口在本项目的 Java 后端均已实现,数据取自业务表实时聚合,star_num、trample_num、customer_added_count 不再是占位值。

官方接口路径状态
对话相关统计趋势/api/application/{applicationId}/statistics/chat_record_aggregate_trend已实现
对话统计汇总/api/application/{applicationId}/statistics/chat_record_aggregate已实现
用户统计/api/application/{applicationId}/statistics/customer_count已实现
用户统计趋势/api/application/{applicationId}/statistics/customer_count_trend已实现

前端的“概览”只调用趋势接口,其余三个接口供对接方与后续页面使用。

趋势接口字段 ​

字段数据来源
day日期分组(yyyy-MM-dd)
chat_record_count当日问答记录数
customer_num当日有过会话的不同 client_id 数
tokens_num当日 message_tokens + coalesce(answer_tokens,0) 之和
star_num当日赞同数(vote_status='0')
trample_num当日反对数(vote_status='1')
customer_added_count当日新增访客数(公开访问建立访客记录)

汇总接口字段 ​

chat_record_aggregate 返回区间内一行汇总,另带 customer_added_count(区间内新增访客数)。customer_count 返回 customer_added_count 与 customer_today_added_count(当日新增访客数,今天不在区间内时为 0)。customer_count_trend 按天返回 day 与 customer_added_count,只返回有访客的日期,与官方实现一致。

日期参数为 yyyy-MM-dd,左闭右开区间(create_time >= 起日 and create_time < 止日 + 1)。参数缺失或格式错误返回 开始时间或结束时间格式不正确,起日晚于止日返回 开始时间不能晚于结束时间。普通用户只能查询自己拥有的应用,非本人应用返回 无权限访问应用统计;管理员 id=1 可查看全部应用。

数据来源 ​

统计不读取额外汇总表,全部按需查询业务表:

表用途
moss_kb_application_chat_record问答记录、问题与回答 token、vote_status、创建时间
moss_kb_application_chat会话归属的应用与 client_id
moss_kb_application_public_access_client公开访问访客记录,用于用户新增统计

问答记录在提问时写入 message_tokens,回答结束后回写 answer_tokens;vote_status 默认为 '-1'(未投票),赞同为 '0',反对为 '1'。因此 tokens_num 只统计已完成回答的记录,正在生成的会话当日 token 会偏小。

数据保留策略、会话归属与 token 口径见对话日志、问答模块。

统计趋势接口 ​

http
GET /api/application/{applicationId}/statistics/chat_record_aggregate_trend?start_time=2026-10-01&end_time=2026-10-03
Authorization: Bearer <JWT_TOKEN>

响应(数据来自本机验证库):

json
{
  "code": 200,
  "message": null,
  "data": [
    {
      "trample_num": 0,
      "customer_num": 0,
      "star_num": 0,
      "tokens_num": 0,
      "customer_added_count": 0,
      "day": "2026-10-01",
      "chat_record_count": 0
    },
    {
      "trample_num": 0,
      "customer_num": 3,
      "star_num": 0,
      "tokens_num": 94097,
      "customer_added_count": 5,
      "day": "2026-10-02",
      "chat_record_count": 10
    },
    {
      "trample_num": 0,
      "customer_num": 1,
      "star_num": 0,
      "tokens_num": 27903,
      "customer_added_count": 1,
      "day": "2026-10-03",
      "chat_record_count": 3
    }
  ]
}

区间内没有记录的日期补零行。

其它三个接口 ​

http
GET /api/application/{applicationId}/statistics/chat_record_aggregate?start_time=2026-10-01&end_time=2026-10-03
json
{
  "code": 200,
  "message": null,
  "data": {
    "trample_num": 0,
    "star_num": 0,
    "customer_num": 4,
    "tokens_num": 122000,
    "customer_added_count": 6,
    "chat_record_count": 13
  }
}
http
GET /api/application/{applicationId}/statistics/customer_count?start_time=2026-10-01&end_time=2026-10-03
GET /api/application/{applicationId}/statistics/customer_count_trend?start_time=2026-10-01&end_time=2026-10-03
json
{
  "code": 200,
  "message": null,
  "data": { "customer_added_count": 6, "customer_today_added_count": 1 }
}
json
{
  "code": 200,
  "message": null,
  "data": [
    { "customer_added_count": 5, "day": "2026-10-02" },
    { "customer_added_count": 1, "day": "2026-10-03" }
  ]
}

统计值必须是 JSON 数字 ​

统计字段在响应里必须是数字,不能是字符串。前端的 getSum 是拼接语义:

ts
export function getSum(array: Array<any>) {
  return array.reduce((total, item) => total + item, 0)
}

字段一旦是字符串,total + item 会逐项拼接,卡片上的“提问次数”会显示成 000,000,000,000,000,000,000,000,000,000,103 这种拼接结果(13 条记录 + 6 个新增用户拼出来的长数字),不是简单的显示偏差。

而框架的 JSON 写出为雪花 ID 开启了 WriteLongAsString(FastJson2.isLongToString()),Long 会被写成字符串:

java
// tio-utils FastJson2,Long 走字符串分支
JSONWriter.Feature[] features = { JSONWriter.Feature.WriteLongAsString, JSONWriter.Feature.WriteNulls };

因此统计值统一用 BigDecimal 输出,BigDecimal 不受该特性影响:

java
/** 统计值统一输出为 JSON 数字,前端 getSum 依赖数字而不是字符串。 */
private BigDecimal toNumber(Object value) {
  if (value instanceof Number) {
    return new BigDecimal(value.toString());
  }
  try {
    return value == null || "null".equals(value) ? BigDecimal.ZERO : new BigDecimal(value.toString().trim());
  } catch (RuntimeException e) {
    return BigDecimal.ZERO;
  }
}

空日期的占位值同样使用 BigDecimal.ZERO。排查时可以用 javap -p -c 看反编译结果确认 WriteLongAsString 的存在,别只按 JDBC 类型推断:驱动返回的是 Long,字符串是序列化阶段加上去的。

服务实现 ​

统计集中在 MossKbApplicationStatisticsService,控制器只做参数透传与权限入口:

java
@Get("/{applicationId}/statistics/chat_record_aggregate_trend")
public ResultVo statisticsOfCharRecordAggreagteTrend(Long applicationId, String start_time, String end_time) {
  return Aop.get(MossKbApplicationStatisticsService.class).chatRecordAggregateTrend(TioRequestContext.getUserIdLong(),
      applicationId, start_time, end_time);
}

对话统计趋势按天分组、一次查询取回全部日期,再与用户新增趋势按 day 合并,避免逐日查询:

java
private List<Map<String, Object>> queryChatRecordTrend(Long applicationId, LocalDate startDate, LocalDate endDate) {
  List<Row> rows = Db.find(
      "select r.create_time::date as day, count(r.id) as chat_record_count, count(distinct c.client_id) as customer_num,"
          + " coalesce(sum(r.message_tokens + coalesce(r.answer_tokens, 0)), 0) as tokens_num,"
          + " coalesce(sum(case when r.vote_status = '0' then 1 else 0 end), 0) as star_num,"
          + " coalesce(sum(case when r.vote_status = '1' then 1 else 0 end), 0) as trample_num"
          + " from moss_kb_application_chat_record r join moss_kb_application_chat c on c.id = r.chat_id"
          + " where c.application_id = ? and r.create_time >= ? and r.create_time < ? group by day order by day",
      applicationId, java.sql.Date.valueOf(startDate), java.sql.Date.valueOf(endDate.plusDays(1)));
  // ...
}

实现要点:

  • 权限使用 ApplicationAccess.owns,与知识库、文档接口同一套归属判断。
  • 时间过滤用左闭右开区间 create_time >= 起日 and create_time < 止日+1,避免 BETWEEN 在高精度时间戳上重复或遗漏边界记录。
  • 满意度、token 都在聚合 SQL 内用 coalesce 兜底,空区间返回 0 而不是 null。
  • 用户新增按应用与时间过滤,与官方 customer_count_trend.sql 口径一致;官方 customer_count 不过滤时间,这里按区间过滤,避免“区间统计”里混进区间外的访客。
  • 趋势查询固定 2 条 SQL(对话趋势 + 用户新增趋势),不随区间长度增长;原先逐日一条 SQL 的写法在 183 天区间要执行 183 次。

用户满意度投票 ​

满意度统计来自问答记录的 vote_status,写入接口在 ApiChatHistoryController:

http
PUT /api/application/{applicationId}/chat/{chatId}/chat_record/{recordId}/vote
Authorization: Bearer <JWT_TOKEN>
Content-Type: application/json

{"vote_status":"0"}

vote_status 取 '0'(赞同)、'1'(反对)、'-1'(取消),其他值直接拒绝;写入前校验应用与会话归属。

验证时的实际联动(同一条 10-03 的记录):

操作趋势接口 2026-10-03 返回
未投票star_num:0、trample_num:0
投赞同star_num:1、trample_num:0
改投反对star_num:0、trample_num:1

前端页面 ​

统计页面是官方 Vue 前端的“概览”,路由与接口调用如下:

位置说明
ui/src/router/modules/application.ts/application/:id/:type/overview 指向 views/application-overview/index.vue
ui/src/api/application-overview.tsgetStatistics 调用趋势接口
ui/src/views/application-overview/component/StatisticsCharts.vue用户总数、提问次数、Tokens 总数、用户满意度四组卡片与折线图

页面默认查最近 7 天,可切换最近 30、90、183 天或自定义区间,切换后重新请求趋势接口。四张卡片读取同一个数组:customer_num、customer_added_count、chat_record_count、tokens_num、star_num、trample_num。

构建注意 ​

统计服务的代码在 mosskb-business,而 mosskb-web 会把它的 jar 打进 fat jar,因此父 pom 里 mosskb-business 必须排在 mosskb-web 前面:

xml
<modules>
  <module>mosskb-business</module>
  <module>mosskb-web</module>
</modules>

顺序反了(原先即如此)会出现“改了 business 的代码、跑起来还是旧逻辑、日志里却写着 BUILD SUCCESS”:

  • mosskb-web 先构建,内嵌的是本地仓库/上一次构建的 mosskb-business-1.0.4.jar;
  • 之后 mosskb-business 才编译打包,产出的新 jar 没有任何模块再打包进去。

另一个只在 Windows 出现的坑:后端进程正在运行时会锁住 mosskb-web/target/mosskb-web-1.0.4.jar,maven-jar-plugin 替换失败但构建仍报成功,jar 的时间和内容都不变。重新打包前先停掉后端:

powershell
Get-NetTCPConnection -State Listen -LocalPort 10060 | Select-Object -ExpandProperty OwningProcess |
  Sort-Object -Unique | ForEach-Object { Stop-Process -Id $_ -Force }
mvn clean "-Dmaven.test.skip=true" "-Dmaven.javadoc.skip=true" "-Dgpg.skip=true" -Pproduction package

PowerShell 下 -Dmaven.test.skip=true 必须加引号,否则会被拆成 .test.skip=true,Maven 报 Unknown lifecycle phase ".test.skip=true"。

本机验证 ​

后端:mosskb-web 重新打包后的 mosskb-web-1.0.4.jar,监听 10060;前端:java-mosskb-ui 的 Vite 开发服务器,监听 3000(/api 代理到 10060);浏览器由 deepseek-browser-use 服务驱动。

接口验证 ​

用例结果
趋势接口 2026-10-01 ~ 2026-10-03200,10-02 为 10 条 / 3 用户 / 94097 tokens / 新增 5,10-03 为 3 条 / 1 用户 / 27903 tokens / 新增 1,10-01 补零行
汇总接口同区间200,13 条 / 122000 tokens / 4 用户 / 新增 6
用户统计同区间200,区间新增 6、当日新增 1
用户统计趋势同区间200,10-02 新增 5、10-03 新增 1
投票后重查趋势赞同与反对随 vote_status 变化,见上文联动表
非法日期 start_time=abc400,开始时间或结束时间格式不正确
起日晚于止日400,开始时间不能晚于结束时间

对照数据库直接执行等价 SQL 可复算同一组数字。

页面验证 ​

打开 http://127.0.0.1:3000/ui/#/application/{applicationId}/SIMPLE/overview,默认“过去 7 天”,直接读取卡片 DOM 文本:

卡片页面显示
用户总数4,新增 +6
提问次数13
Tokens 总数122.0k
用户满意度赞同 1、反对 0

满意度由投票接口写入后刷新得到,说明卡片读的是实时统计而非缓存。修复数字类型前同一页显示为 000,000,000,000,000,000,000,000,000,000,103 这类拼接长数字;改为 BigDecimal 输出后恢复正常。

切到“过去 30 天”,浏览器请求记录为 statistics/chat_record_aggregate_trend?start_time=2026-09-03&end_time=2026-10-03,返回 200 且为 31 天数据。

MIT Licensed