18 KiB
需求概述
统计项目运营数据相关功能开发。
核心技术栈
- Java:OpenJDK21
- SpringBoot:3.2.9
- MybatisPlus:3.5.7
- RocketMQ:
- spring-boot-starter:2.2.3
- rocketmq-client:4.9.8
- MySQL:8.0+
- Redis:8.0+
目录说明
- 根目录:"E:\gitlab\mis-dev\mis-modules\mis-agent\src\main\java\com\lcfc\agent\stats"
- Controller:"<根目录>\api"
- Service:"<根目录>\service"
- 配置类:"<根目录>\conf"
- DTO:"<根目录>\domain\dto"
- VO:"<根目录>\domain\vo"
- Enum:"<根目录>\domain\enums"
- Entity:"<根目录>\domain\entity"
- Mapper:
- Java源文件:"<根目录>\domain\mapper"
- XML文件:"E:\gitlab\mis-dev\mis-modules\mis-agent\src\main\resources\mapper"
- 消息队列相关
- 消息载体:"<根目录>\mq\event"
- 监听器:"<根目录>\mq\consumer"
- SQL文件:"E:\gitlab\mis-dev\mis-modules\mis-agent\src\main\resources\sql-stats"
公共约定
- 枚举类、常量类、工具函数、VO、DTO均不能复用其它目录下面的,可以抄一个放到<目录说明>中对应的目录下,因为该模块未来可能会独立出去
示例
- 接口文档:https://git.jkwlstv.cn/doc/prompts/raw/branch/main/example_rest_api_doc.md
- MySQL建表语句:https://git.jkwlstv.cn/doc/prompts/raw/branch/main/example_mysql_ddl.md
- 实体类:https://git.jkwlstv.cn/doc/prompts/raw/branch/main/example_entity.md
- 枚举类:https://git.jkwlstv.cn/doc/prompts/raw/branch/main/example_enum.md
任务清单
生成表结构
- 数据库表结构字段名称、实体类字段名称,采用驼峰与下划线映射的方式。
- 查看目录下所有被
@TableName注解修饰的源文件,生成MySQL建表语句,建表语句存放到<SQL文件>目录下。 - 包含
project_id字段的表都需要对它建立普通索引。 - 在
project_stats_registry表中project_name和agent_id要创建唯一索引。 - 在
project_data_stats_daily表中project_id和daily要创建唯一索引。 - 实体类字段包含
@NotNull注解,你需要结合@Schema注解中的defaultValue属性判断是否需要设置默认值。 - 主键列是否需要用MySQL的自增序列,取决于实体类被
@TableId注解修饰的字段,要看其type属性,如果是IdType.AUTO则使用自增,否则不需要。 - 如果
@Schema注解指定了maxLength属性,按如下原则设置字段类型:- 数值小于或等于2000,使用VARCHAR
- 数值大于2000且小于10000,使用TEXT
- 数值大于10000,使用LONGTEXT
- 实体类的属性类型与表结构的字段类型映射关系:
- String类型默认使用VARCHAT(255)
- Long类型默认使用BIGINT
- Integer类型默认使用INT
- LocalDate类型默认使用DATE
- LocalDateTime类型默认使用DATETIME
@TableField注解中typeHandler等于JacksonTypeHandler.class时,表结构字段类型默认使用JSON
项目日志
清理过期错误日志
根据ProjectStatsLogConfig.java的cleanExpireErrorLogRedisKey属性,从Redis中查询是否存在该Key,如果存在不执行任何动作,不存在则视为今天没有清理过,需要执行清理动作,先将该Key存储到Redis中,值设置为"1",过期时间的计算公式:
public static long endDayOfMills() {
LocalDateTime endOfDay = LocalDateTime.of(LocalDate.now(), LocalTime.MAX);
LocalDateTime now = LocalDateTime.now();
return Duration.between(now, endOfDay).toMillis();
}
RedisKey存储成功后,开启一个虚拟线程,物理删除N天前的日志,N的数值取自ProjectStatsLogConfig.java的errorLogExpireDays属性。
创建错误日志
执行一遍<清理过期错误日志>然后创建ProjectStatsLog.java对象,然后调用Mapper接口相关方法保存,要求如下:
- 处理日志过程中发生异常,打印一条log即可,不要抛异常到外层。
- 用
ERROR填充logLevel字段 - 用当前时间填充
createTime字段 - 调用方至少要上报
description、exceptionStackTrace、keyParameters其中之一。如果都没上报,这条日志就不记了。 - 如果
description不为空,要看字符长度是否大于ProjectStatsLogConfig.java的descriptionMaxLength属性,超长则截断。 - 如果
exceptionStackTrace不为空,要看字符长度是否大于ProjectStatsLogConfig.java的exceptionStackTraceMaxLength属性,超长则截断。
创建普通日志
执行一遍<清理过期错误日志>然后创建ProjectStatsLog.java对象,然后调用Mapper接口相关方法保存,要求如下:
- 处理日志过程中发生异常,打印一条log即可,不要抛异常到外层。
- 用
INFO填充logLevel字段 - 用当前时间填充
createTime字段 - 调用方至少要上报
description、keyParameters其中之一。如果都没上报,这条日志就不记了。 - 如果
description不为空,要看字符长度是否大于ProjectStatsLogConfig.java的descriptionMaxLength属性,超长则截断。
监听器
按照业务逻辑实现各个监听器的onMessage方法。使用try-catch语法捕获异常,如果遇到异常,打印一条log,并参考<创建错误日志>生成1条错误日志即可。
匹配项目注册表
根据projectName和agentId字段去查询project_stats_registry表,查不到数据参考<创建错误日志>生成1条错误日志,description字段赋值"项目未注册",keyParameters赋值onMessage方法入参的对象。查得到数据视为满足要求,返回ProjectStatsRegistry对象,并继续执行业务逻辑。
ProjectAgentChatLifeCycleEventConsumer.java
先<匹配项目注册表>满足要求再按下述要求继续执行:
- 根据今天的日期,查看
project_data_stats_daily是否有数据,没数据insert否则update,请通过ON DUPLICATE KEY UPDATE实现。 - 把
inputToken、outputToken、callMcpToolCount、callSkillCount字段累加到project_data_stats_daily和project_data_stats相关字段。 - 如果
kbDocUseInfo不为空,要把相同的kbId合并成一条,把docIds去重合并。 - 如果
mcpUseInfo不为空,要把相同的mcpServerId合并成一条,把toolNames去重合并。 - 如果
skillUseInfo不为空,要把skillNames去重合并。 - 每收到1条数据,
project_data_stats_daily和project_data_stats中的chatNum字段+1。 - 把
userId去重合并到project_data_stats_daily和project_data_stats中的user_id,同时更新userNum字段,数值是userId的个数。 - 修改数据库表数据时,所有包含
updateTime的表,都要设置为当前时间。
ProjectInfoUpdateEventConsumer.java
如果ProjectInfoUpdatePayload.java中的before和after的字段其中1个是Null都不执行任何操作,否则按如下要求执行:
- 根据
before中的projectName和agentId字段查询project_stats_registry表。查不到数据执行第2步,然后结束。查到数据从第3步继续执行。 - 参考<创建错误日志>生成1条错误日志,
description赋值"项目未注册",keyParameters赋值ProjectInfoUpdatePayload.java对象。 - 参考<创建普通日志>生成1条普通日志,
description赋值"项目信息变更",keyParameters赋值ProjectInfoUpdatePayload.java对象。 - 用
after中的字段去覆盖第1步查出来数据,注意:只有不为空才更新!
ProjectUserDisLikeEventConsumer.java
先<匹配项目注册表>满足要求则执行:
- 从
dateTime获取LocalDate值,结合projectId、daily去查询ProjectDataStatsDaily,存在数据将其userDislikeCount字段原子化+1即可。 - 根据
projectId查询ProjectDataStats,存在数据将其userDislikeCount字段原子化+1即可。
ProjectUserDisLikeUndoEventConsumer.java
先<匹配项目注册表>满足要求则执行:
- 从
dateTime获取LocalDate值,结合projectId、daily去查询ProjectDataStatsDaily,存在数据将其userDislikeCount字段原子化-1即可。 - 根据
projectId查询ProjectDataStats,存在数据将其userDislikeCount字段原子化-1即可。
ProjectUserFeedbackEventConsumer.java
先<匹配项目注册表>满足要求则执行:
- 从
dateTime获取LocalDate值,结合projectId、daily去查询ProjectDataStatsDaily,存在数据将其userFeedbackCount字段原子化+1即可。 - 根据
projectId查询ProjectDataStats,存在数据将其userFeedbackCount字段原子化+1即可。
ProjectUserFeedbackUndoEventConsumer.java
先<匹配项目注册表>满足要求则执行:
- 从
dateTime获取LocalDate值,结合projectId、daily去查询ProjectDataStatsDaily,存在数据将其userFeedbackCount字段原子化-1即可。 - 根据
projectId查询ProjectDataStats,存在数据将其userFeedbackCount字段原子化-1即可。
ProjectStatsController.java
ProjectStatsControllerRules
-
分页接口参考
AgentPageController.java中的searchSingleAgent方法去实现。 -
数据图表请求参数中的
维度分为:日、月、年,字段请使用枚举类。与维度一同出现的参数还有begin和end,end的数值可以等于begin但不能小于它。
- 当
维度等于日,begin和end的上报格式:"yyyy-MM-dd" - 当
维度等于月,begin和end的上报格式:"yyyy-MM" - 当
维度等于年,begin和end的上报格式:"yyyy"
- 数据图表响应参数中的
x代表图表的横坐标,根据不同的维度填充不同的数值,规则如下:
- 当
维度等于日,begin到end之间的日期,包含begin和end,格式:"yyyy-MM-dd" - 当
维度等于月,begin到end之间的月份,包含begin和end,格式:"yyyy-MM" - 当
维度等于年,begin到end之间的年份,包含begin和end,格式:"yyyy"
-
数据图表在根据
月和年这2个维度查询图表数据的时候,涉及到计算的部分,不要在数据库进行,1个项目1年的数据也就300多条,按需查询对应字段,使用Java完成计算,优先使用Stream。 -
Token的显示规则与单位请参考
AgentChatMetricsServiceImpl.java中的getAgentMetricsLineChartToken处理逻辑
项目数据统计(需要分页)
- 请求方式:POST
- URL:/project/stats/page
- Content-Type:application/json
使用project_stats_registry与project_data_stats关联查询相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| pageSize | Integer | 分页参数,1页的条数,默认值:20 |
| pageNum | Integer | 分页参数,1页的条数,默认值:1 |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| 项目ID | String | 无 |
| 项目名称 | String | 无 |
| 用户数 | Long | 无 |
| 对话数 | Long | 无 |
| 消耗Token | String | 输入和输出token相加,显示规则参考 |
| 输入Token | String | 显示规则参考 |
| 输出Token | String | 显示规则参考 |
| 知识库文档数 | Long | 用kbDocNum |
| MCP工具调用次数 | Long | 无 |
| Skill调用次数 | Long | 无 |
累计数据
- 请求方式:GET
- URL:/project/stats/total?projectId={projectId}
根据projectId查询并计算project_data_stats相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| 累计用户数 | Long | 无 |
| 累计对话数 | Long | 无 |
| 累计消耗Token | String | 累计输入和累计输出token相加,显示规则参考 |
| 累计输入Token | String | 显示规则参考 |
| 累计输出Token | String | 显示规则参考 |
| 知识库文档数 | Long | 用kbDocNum |
| 知识库文档有效率 | String | 计算公式:调用的文档数量(去重)/ kbDocNum,精度:四舍五入保留两位小数 |
| 累计点踩数 | Long | 无 |
| 点踩率 | String | 计算公式=累计点踩数/累计对话数,精度:四舍五入保留两位小数 |
| 累计反馈数 | Long | 无 |
| 累计MCP工具调用个数 | Long | 计算所有mcpUseInfo中的toolNames总个数 |
| 累计MCP工具调用次数 | Long | 无 |
| 累计Skill调用个数 | Long | 提取skillUseInfo中的skillNames的个数 |
| 累计Skill调用次数 | Long | 无 |
数据图表(用户数)
- 请求方式:POST
- URL:/project/stats/chart/user
- Content-Type:application/json
查询并计算project_data_stats_daily相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
| 维度 | 枚举 | 参考 |
| begin | String | 参考 |
| end | String | 参考 |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| x | List | 参考 |
| y | List | 如果维度是日,直接使用userNum,否则把每一天数据里的userId合并去重后计算总个数 |
数据图表(对话数)
- 请求方式:POST
- URL:/project/stats/chart/chat
- Content-Type:application/json
查询并计算project_data_stats_daily相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
| 维度 | 枚举 | 参考 |
| begin | String | 参考 |
| end | String | 参考 |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| x | List | 参考 |
| y | List | 如果维度是日,直接使用chatNum,否则根据维度分组并求和 |
数据图表(消耗Token)
- 请求方式:POST
- URL:/project/stats/chart/token
- Content-Type:application/json
查询并计算project_data_stats_daily相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
| 维度 | 枚举 | 参考 |
| begin | String | 参考 |
| end | String | 参考 |
| type | 枚举 | 三种类型:all、input、output |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| x | List | 参考 |
| y | List | 如果请求参数的type的值是all,取inputToken和outputToken相加的值。如果是input取inputToken。如果是output取inputToken。如果维度是日,直接返回type对应的数值,否则根据维度分组并求和,显示规则参考 |
数据图表(对话点踩数)
- 请求方式:POST
- URL:/project/stats/chart/dislike
- Content-Type:application/json
查询并计算project_data_stats_daily相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
| 维度 | 枚举 | 参考 |
| begin | String | 参考 |
| end | String | 参考 |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| x | List | 参考 |
| y | List | 如果维度是日,直接使用userDislikeCount,否则根据维度分组并求和 |
数据图表(对话反馈数)
- 请求方式:POST
- URL:/project/stats/chart/feedback
- Content-Type:application/json
查询并计算project_data_stats_daily相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
| 维度 | 枚举 | 参考 |
| begin | String | 参考 |
| end | String | 参考 |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| x | List | 参考 |
| y | List | 如果维度是日,直接使用userFeedbackCount,否则根据维度分组并求和 |
数据图表(MCP使用情况)
- 请求方式:POST
- URL:/project/stats/chart/mcp/usage
- Content-Type:application/json
查询并计算project_data_stats_daily相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
| 维度 | 枚举 | 参考 |
| begin | String | 参考 |
| end | String | 参考 |
| type | 枚举值 | 两种类型,num=个数,count=次数 |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| x | List | 参考 |
| y | List | 如果请求参数中的type是num,则需要对mcpUseInfo中mcpServerId进行分组并去重toolNames,计算toolNames的个数,如果type是count,则使用callMcpToolCount。如果维度是日,直接返回type对应的数值,否则根据维度分组并求和 |
数据图表(Skill使用情况)
- 请求方式:POST
- URL:/project/stats/chart/skill/usage
- Content-Type:application/json
查询并计算project_data_stats_daily相关数据。
请求参数
| 参数名称 | 类型 | 说明 |
|---|---|---|
| projectId | String | 项目ID |
| 维度 | 枚举 | 参考 |
| begin | String | 参考 |
| end | String | 参考 |
| type | 枚举值 | 两种类型,num=个数,count=次数 |
响应参数
| 参数名称 | 类型 | 备注 |
|---|---|---|
| x | List | 参考 |
| y | List | 如果请求参数中的type是num,则需要对skillUseInfo中skillNames合并然后去重toolNames计算个数,如果type是count,则使用callSkillCount。如果维度是日,直接返回type对应的数值,否则根据维度分组并求和 |