From 233981a98a8b8171b1cc1e757a441dc3274ee32f Mon Sep 17 00:00:00 2001 From: 8ga Date: Thu, 23 Jul 2026 14:57:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=9B=B4=E6=96=B0=20a2s-v1=5F2-project=5Fstats?= =?UTF-8?q?.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- a2s-v1_2-project_stats.md | 821 +++++++++++++++++++------------------- 1 file changed, 414 insertions(+), 407 deletions(-) diff --git a/a2s-v1_2-project_stats.md b/a2s-v1_2-project_stats.md index 6cffd51..ac77752 100644 --- a/a2s-v1_2-project_stats.md +++ b/a2s-v1_2-project_stats.md @@ -1,408 +1,415 @@ -# 需求概述 - -统计项目运营数据相关功能开发。 - -## 核心技术栈 - -- 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建表语句,建表语句存放到目录下。 -- 包含`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 - -### 项目日志 - -#### 清理过期错误日志 - -根据`ProjectStatsLogConfig.java`的`cleanExpireErrorLogRedisKey`属性,从Redis中查询是否存在该Key,如果存在不执行任何动作,不存在则视为今天没有清理过,需要执行清理动作,先将该Key存储到Redis中,值设置为"1",过期时间的计算公式: - -```java -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 - -先<匹配项目注册表>满足要求再按下述要求继续执行: - -1. 根据今天的日期,查看`project_data_stats_daily`是否有数据,没数据`insert`否则`update`,请通过`ON DUPLICATE KEY UPDATE`实现。 -2. 把`inputToken`、`outputToken`、`callMcpToolCount`、`callSkillCount`字段累加到`project_data_stats_daily`和`project_data_stats`相关字段。 -3. 如果`kbDocUseInfo`不为空,要把相同的`kbId`合并成一条,把`docIds`去重合并。 -4. 如果`mcpUseInfo`不为空,要把相同的`mcpServerId`合并成一条,把`toolNames`去重合并。 -5. 如果`skillUseInfo`不为空,要把`skillNames`去重合并。 -6. 每收到1条数据,`project_data_stats_daily`和`project_data_stats`中的`chatNum`字段+1。 -7. 把`userId`去重合并到`project_data_stats_daily`和`project_data_stats`中的`user_id`,同时更新`userNum`字段,数值是`userId`的个数。 -8. 修改数据库表数据时,所有包含`updateTime`的表,都要设置为当前时间。 - -#### ProjectInfoUpdateEventConsumer.java - -如果`ProjectInfoUpdatePayload.java`中的`before`和`after`的字段其中1个是Null都不执行任何操作,否则按如下要求执行: - -1. 根据`before`中的`projectName`和`agentId`字段查询`project_stats_registry`表。查不到数据执行第2步,然后结束。查到数据从第3步继续执行。 -2. 参考<创建错误日志>生成1条错误日志,`description`赋值"项目未注册",`keyParameters`赋值`ProjectInfoUpdatePayload.java`对象。 -3. 参考<创建普通日志>生成1条普通日志,`description`赋值"项目信息变更",`keyParameters`赋值`ProjectInfoUpdatePayload.java`对象。 -4. 用`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 - -1. 分页接口参考`AgentPageController.java`中的`searchSingleAgent`方法去实现。 - -2. 数据图表请求参数中的`维度`分为:日、月、年,字段请使用枚举类。与维度一同出现的参数还有`begin`和`end`,`end`的数值可以等于`begin`但不能小于它。 -- 当`维度`等于`日`,`begin`和`end`的上报格式:"yyyy-MM-dd" -- 当`维度`等于`月`,`begin`和`end`的上报格式:"yyyy-MM" -- 当`维度`等于`年`,`begin`和`end`的上报格式:"yyyy" - -3. 数据图表响应参数中的`x`代表图表的横坐标,根据不同的`维度`填充不同的数值,规则如下: -- 当`维度`等于`日`,`begin`到`end`之间的日期,包含`begin`和`end`,格式:"yyyy-MM-dd" -- 当`维度`等于`月`,`begin`到`end`之间的月份,包含`begin`和`end`,格式:"yyyy-MM" -- 当`维度`等于`年`,`begin`到`end`之间的年份,包含`begin`和`end`,格式:"yyyy" - -4. 数据图表在根据`月`和`年`这2个维度查询图表数据的时候,涉及到计算的部分,不要在数据库进行,1个项目1年的数据也就300多条,按需查询对应字段,使用Java完成计算,优先使用`Stream`。 - -5. 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|参考| +# 需求概述 + +统计项目运营数据相关功能开发。 + +## 核心技术栈 + +- 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建表语句,建表语句存放到目录下。 +- 包含`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",过期时间的计算公式: + +```java +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 + +先<匹配项目注册表>满足要求再按下述要求继续执行: + +1. 根据今天的日期,查看`project_data_stats_daily`是否有数据,没数据`insert`否则`update`,请通过`ON DUPLICATE KEY UPDATE`实现。 +2. 把`inputToken`、`outputToken`、`callMcpToolCount`、`callSkillCount`字段累加到`project_data_stats_daily`和`project_data_stats`相关字段。 +3. 如果`kbDocUseInfo`不为空,要把相同的`kbId`合并成一条,把`docIds`去重合并。 +4. 如果`mcpUseInfo`不为空,要把相同的`mcpServerId`合并成一条,把`toolNames`去重合并。 +5. 如果`skillUseInfo`不为空,要把`skillNames`去重合并。 +6. 每收到1条数据,`project_data_stats_daily`和`project_data_stats`中的`chatNum`字段+1。 +7. 把`userId`去重合并到`project_data_stats_daily`和`project_data_stats`中的`user_id`,同时更新`userNum`字段,数值是`userId`的个数。 +8. 修改数据库表数据时,所有包含`updateTime`的表,都要设置为当前时间。 + +#### ProjectInfoUpdateEventConsumer.java + +如果`ProjectInfoUpdatePayload.java`中的`before`和`after`的字段其中1个是Null都不执行任何操作,否则按如下要求执行: + +1. 根据`before`中的`projectName`和`agentId`字段查询`project_stats_registry`表。查不到数据执行第2步,然后结束。查到数据从第3步继续执行。 +2. 参考<创建错误日志>生成1条错误日志,`description`赋值"项目未注册",`keyParameters`赋值`ProjectInfoUpdatePayload.java`对象。 +3. 参考<创建普通日志>生成1条普通日志,`description`赋值"项目信息变更",`keyParameters`赋值`ProjectInfoUpdatePayload.java`对象。 +4. 用`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 + +1. 分页接口参考`AgentPageController.java`中的`searchSingleAgent`方法去实现。 + +2. 数据图表请求参数中的`维度`分为:日、月、年,字段请使用枚举类。与维度一同出现的参数还有`begin`和`end`,`end`的数值可以等于`begin`但不能小于它。 +- 当`维度`等于`日`,`begin`和`end`的上报格式:"yyyy-MM-dd" +- 当`维度`等于`月`,`begin`和`end`的上报格式:"yyyy-MM" +- 当`维度`等于`年`,`begin`和`end`的上报格式:"yyyy" + +3. 数据图表响应参数中的`x`代表图表的横坐标,根据不同的`维度`填充不同的数值,规则如下: +- 当`维度`等于`日`,`begin`到`end`之间的日期,包含`begin`和`end`,格式:"yyyy-MM-dd" +- 当`维度`等于`月`,`begin`到`end`之间的月份,包含`begin`和`end`,格式:"yyyy-MM" +- 当`维度`等于`年`,`begin`到`end`之间的年份,包含`begin`和`end`,格式:"yyyy" + +4. 数据图表在根据`月`和`年`这2个维度查询图表数据的时候,涉及到计算的部分,不要在数据库进行,1个项目1年的数据也就300多条,按需查询对应字段,使用Java完成计算,优先使用`Stream`。 + +5. 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`对应的数值,否则根据维度分组并求和| \ No newline at end of file