|
@@ -9,6 +9,7 @@ import org.springframework.data.domain.Page;
|
|
|
import org.springframework.data.domain.PageRequest;
|
|
import org.springframework.data.domain.PageRequest;
|
|
|
import org.springframework.data.domain.Sort;
|
|
import org.springframework.data.domain.Sort;
|
|
|
import org.springframework.data.jpa.domain.Specification;
|
|
import org.springframework.data.jpa.domain.Specification;
|
|
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
import org.springframework.scheduling.annotation.Async;
|
|
import org.springframework.scheduling.annotation.Async;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
|
|
|
|
@@ -28,9 +29,11 @@ public class AnalyticsService {
|
|
|
private static final Instant FAR_FUTURE = Instant.parse("2099-12-31T23:59:59Z");
|
|
private static final Instant FAR_FUTURE = Instant.parse("2099-12-31T23:59:59Z");
|
|
|
|
|
|
|
|
private final UserEventRepository repo;
|
|
private final UserEventRepository repo;
|
|
|
|
|
+ private final JdbcTemplate jdbc;
|
|
|
|
|
|
|
|
- public AnalyticsService(UserEventRepository repo) {
|
|
|
|
|
|
|
+ public AnalyticsService(UserEventRepository repo, JdbcTemplate jdbc) {
|
|
|
this.repo = repo;
|
|
this.repo = repo;
|
|
|
|
|
+ this.jdbc = jdbc;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/** 默认来源系统:当前业务系统 */
|
|
/** 默认来源系统:当前业务系统 */
|
|
@@ -307,6 +310,78 @@ public class AnalyticsService {
|
|
|
return systems;
|
|
return systems;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ==================== 问答会话(按 request_id 汇总耗时) ====================
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 浏览每次 AI 问答的耗时:以 chat_complete 事件为主,LEFT JOIN chat(取 query)和
|
|
|
|
|
+ * chat_stream_server(取服务端分段),按 request_id 关联,时间倒序分页。
|
|
|
|
|
+ * systemSource: null/空 → AI药典;"all" → 跨系统;其它 → 按系统过滤。
|
|
|
|
|
+ * minTotalMs: 只看耗时 ≥ 此值的(找慢请求),null/0 不过滤。
|
|
|
|
|
+ */
|
|
|
|
|
+ public Map<String, Object> queryQaSessions(String systemSource, String startDate, String endDate,
|
|
|
|
|
+ int page, int pageSize, Long minTotalMs) {
|
|
|
|
|
+ Instant startTime = parseDate(startDate) != null ? parseDate(startDate) : Instant.EPOCH;
|
|
|
|
|
+ Instant endTime = endDate != null && !endDate.isBlank()
|
|
|
|
|
+ ? LocalDate.parse(endDate).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant()
|
|
|
|
|
+ : FAR_FUTURE;
|
|
|
|
|
+ String systemFilter = normalizeSystemFilter(systemSource);
|
|
|
|
|
+
|
|
|
|
|
+ StringBuilder where = new StringBuilder(
|
|
|
|
|
+ "WHERE c.event_type = 'chat_complete' AND c.created_at >= ? AND c.created_at < ?");
|
|
|
|
|
+ List<Object> params = new ArrayList<>();
|
|
|
|
|
+ params.add(startTime);
|
|
|
|
|
+ params.add(endTime);
|
|
|
|
|
+ if (systemFilter != null) {
|
|
|
|
|
+ where.append(" AND c.system_source = ?");
|
|
|
|
|
+ params.add(systemFilter);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (minTotalMs != null && minTotalMs > 0) {
|
|
|
|
|
+ where.append(" AND (c.event_data->>'total_ms')::bigint >= ?");
|
|
|
|
|
+ params.add(minTotalMs);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Long total = jdbc.queryForObject(
|
|
|
|
|
+ "SELECT COUNT(*) FROM user_events c " + where, Long.class, params.toArray());
|
|
|
|
|
+
|
|
|
|
|
+ int offset = (page - 1) * pageSize;
|
|
|
|
|
+ List<Object> dataParams = new ArrayList<>(params);
|
|
|
|
|
+ dataParams.add(pageSize);
|
|
|
|
|
+ dataParams.add(offset);
|
|
|
|
|
+ String dataSql = "SELECT c.id, c.created_at, c.user_key, c.ip, "
|
|
|
|
|
+ + "c.event_data->>'request_id' AS request_id, "
|
|
|
|
|
+ + "q.event_data->>'query' AS query, "
|
|
|
|
|
+ + "c.event_data->>'intent' AS intent, "
|
|
|
|
|
+ + "c.event_data->>'status' AS status, "
|
|
|
|
|
+ + "(c.event_data->>'total_ms')::bigint AS total_ms, "
|
|
|
|
|
+ + "(c.event_data->>'ttft_ms')::bigint AS ttft_ms, "
|
|
|
|
|
+ + "(c.event_data->>'msg_len')::int AS msg_len, "
|
|
|
|
|
+ + "(c.event_data->>'has_media')::boolean AS has_media, "
|
|
|
|
|
+ + "c.event_data->>'conversation_id' AS conversation_id, "
|
|
|
|
|
+ + "s.event_data->>'endpoint' AS endpoint, "
|
|
|
|
|
+ + "(s.event_data->>'total_ms')::bigint AS server_total_ms, "
|
|
|
|
|
+ + "(s.event_data->>'search_rerank_ms')::bigint AS search_rerank_ms, "
|
|
|
|
|
+ + "(s.event_data->>'prompt_ms')::bigint AS prompt_ms, "
|
|
|
|
|
+ + "(s.event_data->>'llm_ms')::bigint AS llm_ms "
|
|
|
|
|
+ + "FROM user_events c "
|
|
|
|
|
+ + "LEFT JOIN user_events q ON q.event_type = 'chat' "
|
|
|
|
|
+ + " AND q.event_data->>'request_id' = c.event_data->>'request_id' "
|
|
|
|
|
+ + "LEFT JOIN user_events s ON s.event_type = 'chat_stream_server' "
|
|
|
|
|
+ + " AND s.event_data->>'request_id' = c.event_data->>'request_id' "
|
|
|
|
|
+ + where
|
|
|
|
|
+ + " ORDER BY c.created_at DESC LIMIT ? OFFSET ?";
|
|
|
|
|
+
|
|
|
|
|
+ List<Map<String, Object>> items = jdbc.queryForList(dataSql, dataParams.toArray());
|
|
|
|
|
+
|
|
|
|
|
+ Map<String, Object> response = new LinkedHashMap<>();
|
|
|
|
|
+ response.put("dimension", "qa_sessions");
|
|
|
|
|
+ response.put("system", systemFilter != null ? systemFilter : "all");
|
|
|
|
|
+ response.put("items", items);
|
|
|
|
|
+ response.put("total", total);
|
|
|
|
|
+ response.put("page", page);
|
|
|
|
|
+ response.put("page_size", pageSize);
|
|
|
|
|
+ return response;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ==================== Helpers ====================
|
|
// ==================== Helpers ====================
|
|
|
|
|
|
|
|
private Specification<UserEvent> buildTimeSpec(String eventType, String systemSource,
|
|
private Specification<UserEvent> buildTimeSpec(String eventType, String systemSource,
|