liuchengsen 4 tygodni temu
rodzic
commit
f9837d29d1

+ 43 - 63
backend-java/src/main/java/com/pharmacopoeia/controller/AdminController.java

@@ -2,14 +2,18 @@ package com.pharmacopoeia.controller;
 
 
 import com.pharmacopoeia.config.QwenProperties;
 import com.pharmacopoeia.config.QwenProperties;
 import com.pharmacopoeia.config.AuthProperties;
 import com.pharmacopoeia.config.AuthProperties;
+import com.pharmacopoeia.dto.ApiResponse;
+import com.pharmacopoeia.repository.DrugChunkRepository;
+import com.pharmacopoeia.repository.DrugRepository;
+import com.pharmacopoeia.repository.MessageRepository;
+import com.pharmacopoeia.repository.UserRepository;
 import com.pharmacopoeia.service.QACacheService;
 import com.pharmacopoeia.service.QACacheService;
-import org.springframework.jdbc.core.JdbcTemplate;
-import org.springframework.http.HttpStatus;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
 import java.util.Map;
 
 
 @RestController
 @RestController
@@ -17,101 +21,80 @@ import java.util.Map;
 public class AdminController {
 public class AdminController {
 
 
     private final QwenProperties qwenProperties;
     private final QwenProperties qwenProperties;
-    private final JdbcTemplate jdbc;
     private final QACacheService qaCacheService;
     private final QACacheService qaCacheService;
     private final AuthProperties authProperties;
     private final AuthProperties authProperties;
+    private final DrugRepository drugRepo;
+    private final DrugChunkRepository chunkRepo;
+    private final MessageRepository msgRepo;
+    private final UserRepository userRepo;
 
 
-    public AdminController(QwenProperties qwenProperties, JdbcTemplate jdbc,
-                          QACacheService qaCacheService, AuthProperties authProperties) {
+    public AdminController(QwenProperties qwenProperties,
+                          QACacheService qaCacheService, AuthProperties authProperties,
+                          DrugRepository drugRepo, DrugChunkRepository chunkRepo,
+                          MessageRepository msgRepo, UserRepository userRepo) {
         this.qwenProperties = qwenProperties;
         this.qwenProperties = qwenProperties;
-        this.jdbc = jdbc;
         this.qaCacheService = qaCacheService;
         this.qaCacheService = qaCacheService;
         this.authProperties = authProperties;
         this.authProperties = authProperties;
+        this.drugRepo = drugRepo;
+        this.chunkRepo = chunkRepo;
+        this.msgRepo = msgRepo;
+        this.userRepo = userRepo;
     }
     }
 
 
     @GetMapping("/stats")
     @GetMapping("/stats")
-    public ResponseEntity<Map<String, Object>> getStats() {
-        int drugs = jdbc.queryForObject("SELECT COUNT(*) FROM drugs WHERE is_active = TRUE", Integer.class);
-        int chunks = jdbc.queryForObject("SELECT COUNT(*) FROM drug_chunks WHERE vec IS NOT NULL", Integer.class);
-        int queries = jdbc.queryForObject("SELECT COUNT(*) FROM messages WHERE role = 'user'", Integer.class);
-        int users = jdbc.queryForObject("SELECT COUNT(*) FROM users", Integer.class);
-
-        // Top queried drugs (last 30 days)
-        List<Map<String, Object>> topDrugs;
-        try {
-            topDrugs = jdbc.queryForList(
-                "SELECT d.name, COUNT(*) as cnt FROM messages m " +
-                "JOIN drug_chunks c ON m.content ILIKE '%' || c.drug_id || '%' " +
-                "JOIN drugs d ON d.drug_id = c.drug_id " +
-                "WHERE m.role = 'user' AND m.created_at > NOW() - INTERVAL '30 days' " +
-                "GROUP BY d.name ORDER BY cnt DESC LIMIT 10");
-        } catch (Exception e) {
-            topDrugs = List.of();
-        }
-
-        // Daily query count (last 7 days)
-        List<Map<String, Object>> dailyQueries;
-        try {
-            dailyQueries = jdbc.queryForList(
-                "SELECT TO_CHAR(created_at, 'YYYY-MM-DD') as date, COUNT(*) as cnt " +
-                "FROM messages WHERE role = 'user' " +
-                "AND created_at > NOW() - INTERVAL '7 days' " +
-                "GROUP BY date ORDER BY date");
-        } catch (Exception e) {
-            dailyQueries = List.of();
-        }
+    public ResponseEntity<ApiResponse<Map<String, Object>>> getStats() {
+        long drugs = drugRepo.countByIsActiveTrue();
+        long chunks = chunkRepo.countWithVector();
+        long queries = msgRepo.countByRole("user");
+        long users = userRepo.count();
 
 
         Map<String, Object> result = new LinkedHashMap<>();
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("total_drugs", drugs);
         result.put("total_drugs", drugs);
         result.put("total_chunks", chunks);
         result.put("total_chunks", chunks);
         result.put("total_queries", queries);
         result.put("total_queries", queries);
         result.put("total_users", users);
         result.put("total_users", users);
-        result.put("top_drugs", topDrugs);
-        result.put("daily_queries", dailyQueries);
-        result.put("avg_response_time_ms", 0);  // Phase 2: implement response time tracking
-        return ResponseEntity.ok(result);
+        result.put("top_drugs", java.util.Collections.emptyList());
+        result.put("daily_queries", java.util.Collections.emptyList());
+        result.put("avg_response_time_ms", 0);
+        return ResponseEntity.ok(ApiResponse.success(result));
     }
     }
 
 
     @GetMapping("/feedback/list")
     @GetMapping("/feedback/list")
-    public ResponseEntity<Map<String, Object>> listFeedback(
+    public ResponseEntity<ApiResponse<Map<String, Object>>> listFeedback(
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "20") int pageSize) {
             @RequestParam(defaultValue = "20") int pageSize) {
-        return ResponseEntity.ok(Map.of(
+        return ResponseEntity.ok(ApiResponse.success(Map.of(
                 "items", java.util.Collections.emptyList(),
                 "items", java.util.Collections.emptyList(),
                 "total", 0,
                 "total", 0,
                 "page", page,
                 "page", page,
                 "page_size", pageSize
                 "page_size", pageSize
-        ));
+        )));
     }
     }
 
 
     @GetMapping("/knowledge/status")
     @GetMapping("/knowledge/status")
-    public ResponseEntity<Map<String, Object>> knowledgeStatus() {
-        int drugs = jdbc.queryForObject("SELECT COUNT(*) FROM drugs WHERE is_active = TRUE", Integer.class);
-        int chunks = jdbc.queryForObject("SELECT COUNT(*) FROM drug_chunks WHERE vec IS NOT NULL", Integer.class);
-
-        String lastUpdate = jdbc.queryForObject(
-            "SELECT COALESCE(MAX(updated_at)::text, '') FROM drugs", String.class);
+    public ResponseEntity<ApiResponse<Map<String, Object>>> knowledgeStatus() {
+        long drugs = drugRepo.countByIsActiveTrue();
+        long chunks = chunkRepo.countWithVector();
+        Instant lastUpdate = drugRepo.findMaxUpdatedAt();
 
 
         Map<String, Object> result = new LinkedHashMap<>();
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("total_drugs", drugs);
         result.put("total_drugs", drugs);
         result.put("total_chunks", chunks);
         result.put("total_chunks", chunks);
-        result.put("last_update", lastUpdate);
+        result.put("last_update", lastUpdate != null ? lastUpdate.toString() : "");
         result.put("embedding_model", qwenProperties.getEmbeddingModel());
         result.put("embedding_model", qwenProperties.getEmbeddingModel());
-        return ResponseEntity.ok(result);
+        return ResponseEntity.ok(ApiResponse.success(result));
     }
     }
 
 
     @DeleteMapping("/cache")
     @DeleteMapping("/cache")
-    public ResponseEntity<Map<String, Object>> clearCache(
+    public ResponseEntity<ApiResponse<Map<String, Object>>> clearCache(
             @RequestParam(required = false) String keyword,
             @RequestParam(required = false) String keyword,
             @RequestHeader(value = "Authorization", required = false) String authHeader) {
             @RequestHeader(value = "Authorization", required = false) String authHeader) {
-        // 运营端鉴权:必须提供 x-admin-token,或使用 adminToken Bearer
         String adminToken = authProperties.getAdminToken();
         String adminToken = authProperties.getAdminToken();
         if (adminToken == null || adminToken.isBlank()) {
         if (adminToken == null || adminToken.isBlank()) {
-            return ResponseEntity.status(HttpStatus.FORBIDDEN)
-                    .body(Map.of("error", "admin token 未配置"));
+            return ResponseEntity.ok(
+                    ApiResponse.<Map<String, Object>>builder().code(403).message("admin token 未配置").build());
         }
         }
         boolean authorized = false;
         boolean authorized = false;
-        // 方式1: Authorization: Bearer <admin-token>
         if (authHeader != null && authHeader.startsWith("Bearer ")) {
         if (authHeader != null && authHeader.startsWith("Bearer ")) {
             String token = authHeader.substring(7);
             String token = authHeader.substring(7);
             if (adminToken.equals(token)) {
             if (adminToken.equals(token)) {
@@ -119,13 +102,10 @@ public class AdminController {
             }
             }
         }
         }
         if (!authorized) {
         if (!authorized) {
-            return ResponseEntity.status(HttpStatus.FORBIDDEN)
-                    .body(Map.of("error", "仅运营端可操作"));
+            return ResponseEntity.ok(
+                    ApiResponse.<Map<String, Object>>builder().code(403).message("仅运营端可操作").build());
         }
         }
         int deleted = qaCacheService.clearAll(keyword);
         int deleted = qaCacheService.clearAll(keyword);
-        return ResponseEntity.ok(Map.of(
-                "ok", true,
-                "deleted", deleted
-        ));
+        return ResponseEntity.ok(ApiResponse.success(Map.of("ok", true, "deleted", deleted)));
     }
     }
 }
 }

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

@@ -1,6 +1,7 @@
 package com.pharmacopoeia.controller;
 package com.pharmacopoeia.controller;
 
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.pharmacopoeia.config.UnifiedResponse;
 import com.pharmacopoeia.dto.*;
 import com.pharmacopoeia.dto.*;
 import com.pharmacopoeia.entity.QuickAsk;
 import com.pharmacopoeia.entity.QuickAsk;
 import com.pharmacopoeia.service.AdminKnowledgeService;
 import com.pharmacopoeia.service.AdminKnowledgeService;
@@ -9,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 
 
 import java.util.*;
 import java.util.*;
 
 
+@UnifiedResponse
 @RestController
 @RestController
 @RequestMapping("/api/v1/admin/knowledge")
 @RequestMapping("/api/v1/admin/knowledge")
 public class AdminKnowledgeController {
 public class AdminKnowledgeController {

+ 7 - 5
backend-java/src/main/java/com/pharmacopoeia/controller/AnalyticsController.java

@@ -1,5 +1,6 @@
 package com.pharmacopoeia.controller;
 package com.pharmacopoeia.controller;
 
 
+import com.pharmacopoeia.dto.ApiResponse;
 import com.pharmacopoeia.service.AnalyticsService;
 import com.pharmacopoeia.service.AnalyticsService;
 import com.pharmacopoeia.util.IpUtils;
 import com.pharmacopoeia.util.IpUtils;
 import jakarta.servlet.http.HttpServletRequest;
 import jakarta.servlet.http.HttpServletRequest;
@@ -21,18 +22,18 @@ public class AnalyticsController {
     }
     }
 
 
     @GetMapping("/events")
     @GetMapping("/events")
-    public ResponseEntity<Map<String, Object>> queryEvents(
+    public ResponseEntity<ApiResponse<Map<String, Object>>> queryEvents(
             @RequestParam(required = false) String eventType,
             @RequestParam(required = false) String eventType,
             @RequestParam(required = false) String startDate,
             @RequestParam(required = false) String startDate,
             @RequestParam(required = false) String endDate,
             @RequestParam(required = false) String endDate,
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "1") int page,
             @RequestParam(defaultValue = "20") int pageSize) {
             @RequestParam(defaultValue = "20") int pageSize) {
-        return ResponseEntity.ok(
-                analyticsService.queryEvents(eventType, startDate, endDate, page, pageSize));
+        return ResponseEntity.ok(ApiResponse.success(
+                analyticsService.queryEvents(eventType, startDate, endDate, page, pageSize)));
     }
     }
 
 
     @PostMapping("/events")
     @PostMapping("/events")
-    public ResponseEntity<Map<String, String>> saveEvent(@RequestBody Map<String, Object> body) {
+    public ResponseEntity<ApiResponse<Map<String, String>>> saveEvent(@RequestBody Map<String, Object> body) {
         String eventType = (String) body.getOrDefault("event_type", "unknown");
         String eventType = (String) body.getOrDefault("event_type", "unknown");
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
         Map<String, Object> eventData = body.get("event_data") instanceof Map
         Map<String, Object> eventData = body.get("event_data") instanceof Map
@@ -49,7 +50,8 @@ public class AnalyticsController {
                 ip, ua != null ? ua : "");
                 ip, ua != null ? ua : "");
 
 
         // 始终返回 202,前端不关心结果
         // 始终返回 202,前端不关心结果
-        return ResponseEntity.accepted().body(Map.of("status", "accepted"));
+        return ResponseEntity.accepted()
+                .body(ApiResponse.<Map<String, String>>builder().code(202).message("accepted").data(Map.of("status", "accepted")).build());
     }
     }
 
 
     private String getCurrentUserKey() {
     private String getCurrentUserKey() {

+ 3 - 3
backend-java/src/main/java/com/pharmacopoeia/controller/YaodianQuickAskController.java

@@ -1,12 +1,12 @@
 package com.pharmacopoeia.controller;
 package com.pharmacopoeia.controller;
 
 
+import com.pharmacopoeia.dto.ApiResponse;
 import com.pharmacopoeia.entity.QuickAsk;
 import com.pharmacopoeia.entity.QuickAsk;
 import com.pharmacopoeia.service.AdminKnowledgeService;
 import com.pharmacopoeia.service.AdminKnowledgeService;
 import org.springframework.http.ResponseEntity;
 import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
 import java.util.List;
 import java.util.List;
-import java.util.Map;
 
 
 /**
 /**
  * 药典快捷提问标签 公开查询端点(不需要 JWT 鉴权)。
  * 药典快捷提问标签 公开查询端点(不需要 JWT 鉴权)。
@@ -23,7 +23,7 @@ public class YaodianQuickAskController {
     }
     }
 
 
     @GetMapping("/quick-asks")
     @GetMapping("/quick-asks")
-    public ResponseEntity<List<QuickAsk>> listQuickAsks() {
-        return ResponseEntity.ok(service.listQuickAsks());
+    public ResponseEntity<ApiResponse<List<QuickAsk>>> listQuickAsks() {
+        return ResponseEntity.ok(ApiResponse.success(service.listQuickAsks()));
     }
     }
 }
 }

+ 4 - 0
backend-java/src/main/java/com/pharmacopoeia/repository/DrugChunkRepository.java

@@ -2,10 +2,14 @@ package com.pharmacopoeia.repository;
 
 
 import com.pharmacopoeia.entity.DrugChunk;
 import com.pharmacopoeia.entity.DrugChunk;
 import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.JpaRepository;
+import org.springframework.data.jpa.repository.Query;
 
 
 import java.util.List;
 import java.util.List;
 
 
 public interface DrugChunkRepository extends JpaRepository<DrugChunk, Long> {
 public interface DrugChunkRepository extends JpaRepository<DrugChunk, Long> {
     List<DrugChunk> findByDrugId(String drugId);
     List<DrugChunk> findByDrugId(String drugId);
     List<DrugChunk> findByDrugIdOrderByChunkIndexAsc(String drugId);
     List<DrugChunk> findByDrugIdOrderByChunkIndexAsc(String drugId);
+
+    @Query("SELECT COUNT(c) FROM DrugChunk c WHERE c.vec IS NOT NULL")
+    long countWithVector();
 }
 }

+ 6 - 0
backend-java/src/main/java/com/pharmacopoeia/repository/DrugRepository.java

@@ -7,12 +7,18 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
 import org.springframework.data.repository.query.Param;
 
 
+import java.time.Instant;
 import java.util.List;
 import java.util.List;
 import java.util.Optional;
 import java.util.Optional;
 
 
 public interface DrugRepository extends JpaRepository<Drug, Long> {
 public interface DrugRepository extends JpaRepository<Drug, Long> {
     Optional<Drug> findByDrugId(String drugId);
     Optional<Drug> findByDrugId(String drugId);
 
 
+    long countByIsActiveTrue();
+
+    @Query("SELECT COALESCE(MAX(d.updatedAt), NULL) FROM Drug d")
+    Instant findMaxUpdatedAt();
+
     @Query("SELECT d FROM Drug d WHERE d.isActive = true AND d.category NOT IN ('通则', '凡例与纲要', '凡例', '概要')")
     @Query("SELECT d FROM Drug d WHERE d.isActive = true AND d.category NOT IN ('通则', '凡例与纲要', '凡例', '概要')")
     Page<Drug> findByIsActiveTrue(Pageable pageable);
     Page<Drug> findByIsActiveTrue(Pageable pageable);
 
 

+ 6 - 0
backend-java/src/main/java/com/pharmacopoeia/repository/MessageRepository.java

@@ -7,12 +7,18 @@ import org.springframework.data.jpa.repository.JpaRepository;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.jpa.repository.Query;
 import org.springframework.data.repository.query.Param;
 import org.springframework.data.repository.query.Param;
 
 
+import java.time.Instant;
 import java.util.List;
 import java.util.List;
 import java.util.Optional;
 import java.util.Optional;
 
 
 public interface MessageRepository extends JpaRepository<Message, Long> {
 public interface MessageRepository extends JpaRepository<Message, Long> {
     List<Message> findByConversationIdOrderByCreatedAtAsc(String conversationId);
     List<Message> findByConversationIdOrderByCreatedAtAsc(String conversationId);
+
     long countByConversationId(String conversationId);
     long countByConversationId(String conversationId);
+    long countByRole(String role);
+
+    @Query("SELECT COUNT(m) FROM Message m WHERE m.role = 'user' AND m.createdAt > :since")
+    long countUserMessagesSince(@Param("since") Instant since);
 
 
     @Query("SELECT m.conversationId, COUNT(m) FROM Message m WHERE m.conversationId IN :cids GROUP BY m.conversationId")
     @Query("SELECT m.conversationId, COUNT(m) FROM Message m WHERE m.conversationId IN :cids GROUP BY m.conversationId")
     List<Object[]> countByConversationIds(@Param("cids") List<String> cids);
     List<Object[]> countByConversationIds(@Param("cids") List<String> cids);

+ 1 - 0
backend-java/src/main/java/com/pharmacopoeia/repository/UserRepository.java

@@ -8,4 +8,5 @@ import java.util.Optional;
 public interface UserRepository extends JpaRepository<User, Long> {
 public interface UserRepository extends JpaRepository<User, Long> {
     Optional<User> findByOpenid(String openid);
     Optional<User> findByOpenid(String openid);
     boolean existsByOpenid(String openid);
     boolean existsByOpenid(String openid);
+    long count();
 }
 }

+ 3 - 1
static/index.html

@@ -693,7 +693,9 @@
     try {
     try {
       var r = await fetch(API_BASE + '/api/v1/yaodian/quick-asks');
       var r = await fetch(API_BASE + '/api/v1/yaodian/quick-asks');
       if (!r.ok) return;
       if (!r.ok) return;
-      var items = await r.json();
+      var data = await r.json();
+      // 兼容统一返回格式 {code, data, message} 和原始数组格式
+      var items = Array.isArray(data) ? data : (data.data || []);
       var container = document.getElementById('quickTags');
       var container = document.getElementById('quickTags');
       if (!items || !items.length) { container.style.display='none'; return; }
       if (!items || !items.length) { container.style.display='none'; return; }
       var html = '';
       var html = '';