Переглянути джерело

埋点系统的信息统计接口更新等操作

liuchengsen 4 тижнів тому
батько
коміт
41eb0a01cf

+ 0 - 7
backend-java/src/main/java/com/pharmacopoeia/controller/AdminKnowledgeController.java

@@ -203,13 +203,6 @@ public class AdminKnowledgeController {
         ));
     }
 
-    // ==================== 统计 ====================
-
-    @GetMapping("/stats")
-    public ResponseEntity<Map<String, Object>> knowledgeStats() {
-        return ResponseEntity.ok(service.knowledgeStats());
-    }
-
     // ==================== 快捷提问标签管理(管理端,需要 JWT) ====================
 
     @GetMapping("/quick-asks")

+ 16 - 0
backend-java/src/main/java/com/pharmacopoeia/controller/AnalyticsController.java

@@ -41,6 +41,22 @@ public class AnalyticsController {
         return ResponseEntity.ok(ApiResponse.success(detail));
     }
 
+    @GetMapping("/stats")
+    public ResponseEntity<ApiResponse<Map<String, Object>>> queryStats(
+            @RequestParam(required = false) String startDate,
+            @RequestParam(required = false) String endDate,
+            @RequestParam(required = false) String dimension,
+            @RequestParam(required = false) String range) {
+        // 没传时间范围默认统计今天
+        if ((startDate == null || startDate.isBlank()) && (endDate == null || endDate.isBlank())) {
+            String today = java.time.LocalDate.now().toString();
+            return ResponseEntity.ok(ApiResponse.success(
+                    analyticsService.queryStats(today, today, dimension, range)));
+        }
+        return ResponseEntity.ok(ApiResponse.success(
+                analyticsService.queryStats(startDate, endDate, dimension, range)));
+    }
+
     @PostMapping("/events")
     public ResponseEntity<ApiResponse<Map<String, String>>> saveEvent(@RequestBody Map<String, Object> body) {
         String eventType = (String) body.getOrDefault("event_type", "unknown");

+ 60 - 0
backend-java/src/main/java/com/pharmacopoeia/repository/UserEventRepository.java

@@ -3,9 +3,69 @@ package com.pharmacopoeia.repository;
 import com.pharmacopoeia.entity.UserEvent;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;
 import org.springframework.stereotype.Repository;
 
+import java.time.Instant;
+import java.util.List;
+
 @Repository
 public interface UserEventRepository extends JpaRepository<UserEvent, Long>,
         JpaSpecificationExecutor<UserEvent> {
+
+    /** 总数 */
+    @Query("SELECT COUNT(e) FROM UserEvent e " +
+           "WHERE (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime)")
+    long countTotal(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
+
+    /** 去重活跃用户数 */
+    @Query("SELECT COUNT(DISTINCT e.userKey) FROM UserEvent e " +
+           "WHERE (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime)")
+    long countDistinctUsers(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
+
+    /** 按类型统计数量 */
+    @Query("SELECT COUNT(e) FROM UserEvent e " +
+           "WHERE e.eventType = :eventType " +
+           "AND (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime)")
+    long countByType(@Param("eventType") String eventType,
+                     @Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
+
+    /** 按 IP 分组统计 */
+    @Query("SELECT e.ip as key, COUNT(e) as cnt FROM UserEvent e " +
+           "WHERE (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime) " +
+           "GROUP BY e.ip ORDER BY cnt DESC")
+    List<Object[]> countByIp(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
+
+    /** 按 page_url 分组统计 */
+    @Query("SELECT e.pageUrl as key, COUNT(e) as cnt FROM UserEvent e " +
+           "WHERE (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime) " +
+           "GROUP BY e.pageUrl ORDER BY cnt DESC")
+    List<Object[]> countByPageUrl(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
+
+    /** 按 event_type 分组统计 */
+    @Query("SELECT e.eventType as key, COUNT(e) as cnt FROM UserEvent e " +
+           "WHERE (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime) " +
+           "GROUP BY e.eventType ORDER BY cnt DESC")
+    List<Object[]> countByEventType(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
+
+    /** 按 user_key 分组统计(去重用户) */
+    @Query("SELECT e.userKey as key, COUNT(e) as cnt FROM UserEvent e " +
+           "WHERE (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime) " +
+           "GROUP BY e.userKey ORDER BY cnt DESC")
+    List<Object[]> countByUserKey(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
+
+    /** 用户+IP 混合维度:每个用户关联的 IP 列表 */
+    @Query("SELECT e.userKey, e.ip, COUNT(e) as cnt FROM UserEvent e " +
+           "WHERE (:startTime IS NULL OR e.createdAt >= :startTime) " +
+           "AND (:endTime IS NULL OR e.createdAt < :endTime) " +
+           "GROUP BY e.userKey, e.ip ORDER BY e.userKey, cnt DESC")
+    List<Object[]> countByUserIp(@Param("startTime") Instant startTime, @Param("endTime") Instant endTime);
 }

+ 0 - 53
backend-java/src/main/java/com/pharmacopoeia/service/AdminKnowledgeService.java

@@ -27,7 +27,6 @@ import com.pharmacopoeia.util.IpUtils;
 
 import java.util.*;
 import java.util.function.Consumer;
-import java.time.Instant;
 
 @Service
 public class AdminKnowledgeService {
@@ -298,58 +297,6 @@ public class AdminKnowledgeService {
         return r;
     }
 
-    // ==================== Statistics ====================
-
-    public Map<String, Object> knowledgeStats() {
-        int totalDrugs = (int) drugRepository.countByIsActiveTrue();
-        int drugsWithChunks = (int) drugChunkRepository.countDistinctDrugsWithVector();
-
-        int totalKPs = (int) knowledgePointRepository.count();
-        List<Map<String, Object>> kpBySubject = knowledgePointRepository.countBySubjectGrouped()
-                .stream()
-                .map(row -> {
-                    Map<String, Object> m = new LinkedHashMap<>();
-                    m.put("subject", row[0]);
-                    m.put("cnt", row[1]);
-                    return m;
-                })
-                .collect(java.util.stream.Collectors.toList());
-
-        int totalQs = (int) questionRepository.count();
-        int auditedQs = (int) questionRepository.countByAuditedTrue();
-        int pendingQs = (int) questionRepository.countByAuditedFalse();
-
-        Instant lastUpdate = drugRepository.findMaxUpdatedAt();
-        String lastUpdateStr = lastUpdate != null ? lastUpdate.toString() : "";
-
-        Map<String, Object> drugsStats = new LinkedHashMap<>();
-        drugsStats.put("total", totalDrugs);
-        drugsStats.put("active", totalDrugs);
-        drugsStats.put("with_chunks", drugsWithChunks);
-
-        Map<String, Object> kpStats = new LinkedHashMap<>();
-        kpStats.put("total", totalKPs);
-        kpStats.put("by_subject", kpBySubject);
-
-        Map<String, Object> qStats = new LinkedHashMap<>();
-        qStats.put("total", totalQs);
-        qStats.put("audited", auditedQs);
-        qStats.put("pending", pendingQs);
-
-        Map<String, Object> viStats = new LinkedHashMap<>();
-        viStats.put("status", "active");
-        viStats.put("collection", "drug_chunks");
-        viStats.put("last_reindex", lastUpdateStr);
-
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("drugs", drugsStats);
-        result.put("knowledge_points", kpStats);
-        result.put("questions", qStats);
-        result.put("vector_index", viStats);
-        result.put("last_data_update", lastUpdateStr);
-        return result;
-    }
-
     // ==================== 快捷提问标签管理 ====================
 
     /** 获取所有激活的标签,按 sort_order 排序(公开接口,无需鉴权) */

+ 89 - 0
backend-java/src/main/java/com/pharmacopoeia/service/AnalyticsService.java

@@ -119,4 +119,93 @@ public class AnalyticsService {
         if (date == null || date.isBlank()) return null;
         return LocalDate.parse(date).atStartOfDay(ZoneOffset.UTC).toInstant();
     }
+
+    // ==================== 统计概览 ====================
+
+    /** 概览数字:支持三种场景——累计、今天、最近7天 */
+    public Map<String, Object> queryOverview(String range) {
+        String today = LocalDate.now().toString();
+        Instant todayStart = parseDate(today);
+        Instant todayEnd = LocalDate.parse(today).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant();
+
+        Instant startTime = null;
+        Instant endTime = null;
+        String label;
+
+        if ("today".equals(range)) {
+            startTime = todayStart;
+            endTime = todayEnd;
+            label = "today";
+        } else if ("week".equals(range)) {
+            startTime = todayStart.minus(6, java.time.temporal.ChronoUnit.DAYS);
+            endTime = todayEnd;
+            label = "week";
+        } else {
+            label = "total";
+        }
+
+        Map<String, Object> overview = new LinkedHashMap<>();
+        overview.put("range", label);
+        overview.put("total_events", repo.countTotal(startTime, endTime));
+        overview.put("active_users", repo.countDistinctUsers(startTime, endTime));
+        overview.put("page_views", repo.countByType("page_view", startTime, endTime));
+        overview.put("searches", repo.countByType("search", startTime, endTime));
+        overview.put("ai_qa", repo.countByType("chat", startTime, endTime));
+        return overview;
+    }
+
+    // ==================== 统计维度 ====================
+
+    /** 五个维度统计,支持按维度切换,概览支持 range=today/week/total;
+     *  如果日期都不传,调用方默认填了当天 */
+    public Map<String, Object> queryStats(String startDate, String endDate, String dimension,
+                                          String range) {
+        Instant startTime = parseDate(startDate);
+        Instant endTime = endDate != null && !endDate.isBlank()
+                ? LocalDate.parse(endDate).plusDays(1).atStartOfDay(ZoneOffset.UTC).toInstant()
+                : null;
+
+        Map<String, Object> result = new LinkedHashMap<>();
+        // 概览数字(range 控制累计/今天/7天)
+        result.put("overview", queryOverview(range));
+
+        // 指定维度只查一个,不查全部
+        boolean all = (dimension == null || dimension.isBlank());
+        if (all || "ip".equals(dimension)) {
+            result.put("by_ip", mapGroupResult(repo.countByIp(startTime, endTime)));
+        }
+        if (all || "url".equals(dimension)) {
+            result.put("by_url", mapGroupResult(repo.countByPageUrl(startTime, endTime)));
+        }
+        if (all || "type".equals(dimension)) {
+            result.put("by_type", mapGroupResult(repo.countByEventType(startTime, endTime)));
+        }
+        if (all || "user".equals(dimension)) {
+            List<Map<String, Object>> byUser = mapGroupResult(repo.countByUserKey(startTime, endTime));
+            result.put("by_user", byUser);
+            result.put("total_users", byUser.size());
+        }
+        if (all || "user_ip".equals(dimension)) {
+            result.put("by_user_ip", mapUserIpResult(repo.countByUserIp(startTime, endTime)));
+        }
+        return result;
+    }
+
+    private List<Map<String, Object>> mapGroupResult(List<Object[]> rows) {
+        return rows.stream()
+                .map(row -> Map.of("key", row[0] != null ? row[0] : "", "count", (Long) row[1]))
+                .collect(Collectors.toList());
+    }
+
+    private List<Map<String, Object>> mapUserIpResult(List<Object[]> rows) {
+        return rows.stream()
+                .map(row -> {
+                    Map<String, Object> m = new LinkedHashMap<>();
+                    m.put("user_key", row[0] != null ? row[0] : "");
+                    m.put("ip", row[1] != null ? row[1] : "");
+                    m.put("count", (Long) row[2]);
+                    return m;
+                })
+                .collect(Collectors.toList());
+    }
 }

+ 2 - 0
database/user_events.sql

@@ -19,5 +19,7 @@ CREATE TABLE IF NOT EXISTS public.user_events (
 CREATE INDEX IF NOT EXISTS ix_user_events_user_key ON user_events(user_key);
 CREATE INDEX IF NOT EXISTS ix_user_events_type ON user_events(event_type);
 CREATE INDEX IF NOT EXISTS ix_user_events_created_at ON user_events(created_at DESC);
+CREATE INDEX IF NOT EXISTS ix_user_events_ip ON user_events(ip);
+CREATE INDEX IF NOT EXISTS ix_user_events_page_url ON user_events(page_url);
 
 COMMIT;