|
|
@@ -2,14 +2,18 @@ package com.pharmacopoeia.controller;
|
|
|
|
|
|
import com.pharmacopoeia.config.QwenProperties;
|
|
|
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 org.springframework.jdbc.core.JdbcTemplate;
|
|
|
-import org.springframework.http.HttpStatus;
|
|
|
import org.springframework.http.ResponseEntity;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
+import java.time.Instant;
|
|
|
+import java.time.temporal.ChronoUnit;
|
|
|
import java.util.LinkedHashMap;
|
|
|
-import java.util.List;
|
|
|
import java.util.Map;
|
|
|
|
|
|
@RestController
|
|
|
@@ -17,101 +21,80 @@ import java.util.Map;
|
|
|
public class AdminController {
|
|
|
|
|
|
private final QwenProperties qwenProperties;
|
|
|
- private final JdbcTemplate jdbc;
|
|
|
private final QACacheService qaCacheService;
|
|
|
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.jdbc = jdbc;
|
|
|
this.qaCacheService = qaCacheService;
|
|
|
this.authProperties = authProperties;
|
|
|
+ this.drugRepo = drugRepo;
|
|
|
+ this.chunkRepo = chunkRepo;
|
|
|
+ this.msgRepo = msgRepo;
|
|
|
+ this.userRepo = userRepo;
|
|
|
}
|
|
|
|
|
|
@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<>();
|
|
|
result.put("total_drugs", drugs);
|
|
|
result.put("total_chunks", chunks);
|
|
|
result.put("total_queries", queries);
|
|
|
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")
|
|
|
- public ResponseEntity<Map<String, Object>> listFeedback(
|
|
|
+ public ResponseEntity<ApiResponse<Map<String, Object>>> listFeedback(
|
|
|
@RequestParam(defaultValue = "1") int page,
|
|
|
@RequestParam(defaultValue = "20") int pageSize) {
|
|
|
- return ResponseEntity.ok(Map.of(
|
|
|
+ return ResponseEntity.ok(ApiResponse.success(Map.of(
|
|
|
"items", java.util.Collections.emptyList(),
|
|
|
"total", 0,
|
|
|
"page", page,
|
|
|
"page_size", pageSize
|
|
|
- ));
|
|
|
+ )));
|
|
|
}
|
|
|
|
|
|
@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<>();
|
|
|
result.put("total_drugs", drugs);
|
|
|
result.put("total_chunks", chunks);
|
|
|
- result.put("last_update", lastUpdate);
|
|
|
+ result.put("last_update", lastUpdate != null ? lastUpdate.toString() : "");
|
|
|
result.put("embedding_model", qwenProperties.getEmbeddingModel());
|
|
|
- return ResponseEntity.ok(result);
|
|
|
+ return ResponseEntity.ok(ApiResponse.success(result));
|
|
|
}
|
|
|
|
|
|
@DeleteMapping("/cache")
|
|
|
- public ResponseEntity<Map<String, Object>> clearCache(
|
|
|
+ public ResponseEntity<ApiResponse<Map<String, Object>>> clearCache(
|
|
|
@RequestParam(required = false) String keyword,
|
|
|
@RequestHeader(value = "Authorization", required = false) String authHeader) {
|
|
|
- // 运营端鉴权:必须提供 x-admin-token,或使用 adminToken Bearer
|
|
|
String adminToken = authProperties.getAdminToken();
|
|
|
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;
|
|
|
- // 方式1: Authorization: Bearer <admin-token>
|
|
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
|
String token = authHeader.substring(7);
|
|
|
if (adminToken.equals(token)) {
|
|
|
@@ -119,13 +102,10 @@ public class AdminController {
|
|
|
}
|
|
|
}
|
|
|
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);
|
|
|
- return ResponseEntity.ok(Map.of(
|
|
|
- "ok", true,
|
|
|
- "deleted", deleted
|
|
|
- ));
|
|
|
+ return ResponseEntity.ok(ApiResponse.success(Map.of("ok", true, "deleted", deleted)));
|
|
|
}
|
|
|
}
|