init
This commit is contained in:
parent
5bec08604e
commit
729fad3615
@ -37,7 +37,7 @@ public class DataInitializerLogger implements ApplicationRunner {
|
||||
|
||||
if (Objects.isNull(systemSetting)) {
|
||||
// 如果没有初始化过,则执行data.sql中的脚本
|
||||
Resource resource = resourceLoader.getResource("classpath:data.sql");
|
||||
Resource resource = resourceLoader.getResource("classpath:init.sql");
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
String sqlScript = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
|
||||
// Split the SQL script into individual statements and execute them
|
||||
@ -53,7 +53,7 @@ public class DataInitializerLogger implements ApplicationRunner {
|
||||
}
|
||||
|
||||
// 在这里定义您的自定义日志内容
|
||||
logger.info("数据库首次初始化完成,默认数据已通过 data.sql 文件成功插入。");
|
||||
logger.info("数据库首次初始化完成,默认数据已通过 init.sql 文件成功插入。");
|
||||
|
||||
// 插入初始化标志,防止下次启动时再次执行
|
||||
jdbcTemplate.update("UPDATE system_settings SET value = ? WHERE `key` = ?", "true", "init_system");
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package com.xianyu.autoreply.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
|
||||
|
||||
/**
|
||||
* Jackson 全局配置
|
||||
* 解决前端 snake_case 与后端 camelCase 字段命名不匹配的问题
|
||||
*/
|
||||
@Configuration
|
||||
public class JacksonConfig {
|
||||
|
||||
/**
|
||||
* 配置 Jackson ObjectMapper
|
||||
* 自动处理 snake_case 和 camelCase 之间的转换
|
||||
*/
|
||||
@Bean
|
||||
public ObjectMapper objectMapper(Jackson2ObjectMapperBuilder builder) {
|
||||
ObjectMapper objectMapper = builder.createXmlMapper(false).build();
|
||||
|
||||
// 设置属性命名策略:将 JSON 的 snake_case 映射到 Java 的 camelCase
|
||||
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
|
||||
|
||||
return objectMapper;
|
||||
}
|
||||
}
|
||||
@ -1,26 +1,28 @@
|
||||
package com.xianyu.autoreply.controller;
|
||||
|
||||
import com.xianyu.autoreply.entity.User;
|
||||
import com.xianyu.autoreply.entity.Cookie;
|
||||
import com.xianyu.autoreply.repository.UserRepository;
|
||||
import com.xianyu.autoreply.entity.User;
|
||||
import com.xianyu.autoreply.repository.CardRepository;
|
||||
import com.xianyu.autoreply.repository.CookieRepository;
|
||||
import com.xianyu.autoreply.repository.KeywordRepository;
|
||||
import com.xianyu.autoreply.repository.OrderRepository;
|
||||
import com.xianyu.autoreply.repository.UserRepository;
|
||||
import com.xianyu.autoreply.service.TokenService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@ -33,19 +35,27 @@ public class AdminController {
|
||||
|
||||
private final UserRepository userRepository;
|
||||
private final CookieRepository cookieRepository;
|
||||
private final OrderRepository orderRepository;
|
||||
private final CardRepository cardRepository;
|
||||
private final KeywordRepository keywordRepository;
|
||||
private final TokenService tokenService;
|
||||
|
||||
// Log directory - adjust as needed for migration context
|
||||
private final String LOG_DIR = "logs";
|
||||
|
||||
private final OrderRepository orderRepository;
|
||||
private final String LOG_DIR = "logs";
|
||||
|
||||
@Autowired
|
||||
public AdminController(UserRepository userRepository,
|
||||
CookieRepository cookieRepository,
|
||||
OrderRepository orderRepository) {
|
||||
OrderRepository orderRepository,
|
||||
CardRepository cardRepository,
|
||||
KeywordRepository keywordRepository,
|
||||
TokenService tokenService) {
|
||||
this.userRepository = userRepository;
|
||||
this.cookieRepository = cookieRepository;
|
||||
this.orderRepository = orderRepository;
|
||||
this.cardRepository = cardRepository;
|
||||
this.keywordRepository = keywordRepository;
|
||||
this.tokenService = tokenService;
|
||||
}
|
||||
|
||||
// ------------------------- User Management -------------------------
|
||||
@ -55,14 +65,52 @@ public class AdminController {
|
||||
return userRepository.findAll();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统统计信息(管理员专用)
|
||||
* 对应 Python: @app.get('/admin/stats')
|
||||
*/
|
||||
@GetMapping("/admin/stats")
|
||||
public Map<String, Object> getStats() {
|
||||
return Map.of(
|
||||
"user_count", userRepository.count(),
|
||||
"cookie_count", cookieRepository.count(),
|
||||
"order_count", orderRepository.count(),
|
||||
"log_count", 0 // Mock log count or implement if needed
|
||||
public Map<String, Object> getStats(@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
// 管理员权限校验
|
||||
validateAdminPermission(token);
|
||||
|
||||
log.info("查询系统统计信息");
|
||||
|
||||
// 1. 用户统计
|
||||
long totalUsers = userRepository.count();
|
||||
|
||||
// 2. Cookie 统计
|
||||
long totalCookies = cookieRepository.count();
|
||||
|
||||
// 3. 活跃账号统计(启用状态的账号)
|
||||
long activeCookies = cookieRepository.countByEnabled(true);
|
||||
|
||||
// 4. 卡券统计
|
||||
long totalCards = cardRepository.count();
|
||||
|
||||
// 5. 关键词统计
|
||||
long totalKeywords = keywordRepository.count();
|
||||
|
||||
// 6. 订单统计
|
||||
long totalOrders = 0;
|
||||
try {
|
||||
totalOrders = orderRepository.count();
|
||||
} catch (Exception e) {
|
||||
// 兼容 Python 的异常处理
|
||||
log.warn("获取订单统计失败", e);
|
||||
}
|
||||
|
||||
Map<String, Object> stats = Map.of(
|
||||
"total_users", totalUsers,
|
||||
"total_cookies", totalCookies,
|
||||
"active_cookies", activeCookies,
|
||||
"total_cards", totalCards,
|
||||
"total_keywords", totalKeywords,
|
||||
"total_orders", totalOrders
|
||||
);
|
||||
|
||||
log.info("系统统计信息查询完成: {}", stats);
|
||||
return stats;
|
||||
}
|
||||
|
||||
@DeleteMapping("/admin/users/{userId}")
|
||||
@ -130,4 +178,26 @@ public class AdminController {
|
||||
return List.of("Error reading log file: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证管理员权限
|
||||
* 对应 Python: require_admin
|
||||
*/
|
||||
private void validateAdminPermission(String token) {
|
||||
if (token == null) {
|
||||
throw new RuntimeException("需要管理员权限");
|
||||
}
|
||||
|
||||
String rawToken = token.replace("Bearer ", "");
|
||||
TokenService.TokenInfo tokenInfo = tokenService.verifyToken(rawToken);
|
||||
|
||||
if (tokenInfo == null) {
|
||||
throw new RuntimeException("Token 无效");
|
||||
}
|
||||
|
||||
// 检查是否为 admin 用户
|
||||
if (!"admin".equals(tokenInfo.username)) {
|
||||
throw new RuntimeException("需要管理员权限");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -8,15 +8,21 @@ import com.xianyu.autoreply.service.TokenService;
|
||||
import com.xianyu.autoreply.service.XianyuClientService;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@ -85,6 +91,9 @@ public class CookieController {
|
||||
response.setAutoConfirm(cookie.getAutoConfirm());
|
||||
response.setRemark(cookie.getRemark() != null ? cookie.getRemark() : "");
|
||||
response.setPauseDuration(cookie.getPauseDuration() != null ? cookie.getPauseDuration() : 10);
|
||||
response.setUsername(cookie.getUsername());
|
||||
response.setLoginPassword(cookie.getPassword());
|
||||
response.setShowBrowser(cookie.getShowBrowser() == 1);
|
||||
return response;
|
||||
}).collect(Collectors.toList());
|
||||
}
|
||||
@ -291,5 +300,10 @@ public class CookieController {
|
||||
private String remark;
|
||||
@JsonProperty("pause_duration")
|
||||
private Integer pauseDuration;
|
||||
private String username;
|
||||
@JsonProperty("login_password")
|
||||
private String loginPassword;
|
||||
@JsonProperty("show_browser")
|
||||
private Boolean showBrowser;
|
||||
}
|
||||
}
|
||||
@ -1,25 +1,36 @@
|
||||
package com.xianyu.autoreply.controller;
|
||||
|
||||
import com.xianyu.autoreply.dto.KeywordWithItemIdRequest;
|
||||
import com.xianyu.autoreply.entity.AiReplySetting;
|
||||
import com.xianyu.autoreply.entity.Cookie;
|
||||
import com.xianyu.autoreply.entity.DefaultReply;
|
||||
import com.xianyu.autoreply.entity.Keyword;
|
||||
import com.xianyu.autoreply.model.req.KeywordWithItemIdRequest;
|
||||
import com.xianyu.autoreply.repository.AiReplySettingRepository;
|
||||
import com.xianyu.autoreply.repository.CookieRepository;
|
||||
import com.xianyu.autoreply.repository.DefaultReplyRecordRepository;
|
||||
import com.xianyu.autoreply.repository.DefaultReplyRepository;
|
||||
import com.xianyu.autoreply.repository.KeywordRepository;
|
||||
import com.xianyu.autoreply.service.AiReplyService;
|
||||
import com.xianyu.autoreply.service.TokenService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Set;
|
||||
import java.util.HashSet;
|
||||
import java.util.ArrayList;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@ -28,6 +39,7 @@ public class KeywordController {
|
||||
|
||||
private final KeywordRepository keywordRepository;
|
||||
private final DefaultReplyRepository defaultReplyRepository;
|
||||
private final DefaultReplyRecordRepository defaultReplyRecordRepository;
|
||||
private final AiReplySettingRepository aiReplySettingRepository;
|
||||
private final CookieRepository cookieRepository;
|
||||
private final AiReplyService aiReplyService;
|
||||
@ -36,12 +48,14 @@ public class KeywordController {
|
||||
@Autowired
|
||||
public KeywordController(KeywordRepository keywordRepository,
|
||||
DefaultReplyRepository defaultReplyRepository,
|
||||
DefaultReplyRecordRepository defaultReplyRecordRepository,
|
||||
AiReplySettingRepository aiReplySettingRepository,
|
||||
CookieRepository cookieRepository,
|
||||
AiReplyService aiReplyService,
|
||||
TokenService tokenService) {
|
||||
this.keywordRepository = keywordRepository;
|
||||
this.defaultReplyRepository = defaultReplyRepository;
|
||||
this.defaultReplyRecordRepository = defaultReplyRecordRepository;
|
||||
this.aiReplySettingRepository = aiReplySettingRepository;
|
||||
this.cookieRepository = cookieRepository;
|
||||
this.aiReplyService = aiReplyService;
|
||||
@ -52,36 +66,55 @@ public class KeywordController {
|
||||
|
||||
// 对应 Python: @app.get('/keywords-with-item-id/{cid}')
|
||||
@GetMapping("/keywords-with-item-id/{cid}")
|
||||
public List<Keyword> getKeywordsWithItemId(@PathVariable String cid, @RequestHeader(value = "Authorization", required = false) String token) {
|
||||
// Validate user ownership
|
||||
public List<Map<String, Object>> getKeywordsWithItemId(@PathVariable String cid, @RequestHeader(value = "Authorization", required = false) String token) {
|
||||
if (token != null) {
|
||||
String rawToken = token.replace("Bearer ", "");
|
||||
TokenService.TokenInfo tokenInfo = tokenService.verifyToken(rawToken);
|
||||
// Validate user ownership
|
||||
if (tokenInfo != null) {
|
||||
Long userId = tokenInfo.userId;
|
||||
Cookie cookie = cookieRepository.findById(cid).orElse(null);
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限访问该Cookie");
|
||||
if (!Objects.equals(userId, 1L)) {
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限访问该Cookie");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return keywordRepository.findByCookieId(cid);
|
||||
|
||||
// 获取关键词列表
|
||||
List<Keyword> keywords = keywordRepository.findByCookieId(cid);
|
||||
|
||||
// 转换为前端需要的格式(与 Python 实现一致)
|
||||
return keywords.stream()
|
||||
.map(k -> {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("keyword", k.getKeyword());
|
||||
result.put("reply", k.getReply() != null ? k.getReply() : "");
|
||||
result.put("item_id", k.getItemId() != null ? k.getItemId() : "");
|
||||
result.put("type", k.getType() != null ? k.getType() : "text");
|
||||
result.put("image_url", k.getImageUrl());
|
||||
return result;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
|
||||
// 对应 Python: @app.post('/keywords-with-item-id/{cid}')
|
||||
@PostMapping("/keywords-with-item-id/{cid}")
|
||||
public Map<String, Object> updateKeywordsWithItemId(@PathVariable String cid,
|
||||
@RequestBody KeywordWithItemIdRequest request,
|
||||
@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
public Map<String, Object> updateKeywordsWithItemId(@PathVariable String cid,
|
||||
@RequestBody KeywordWithItemIdRequest request,
|
||||
@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
// Validate user ownership
|
||||
if (token != null) {
|
||||
String rawToken = token.replace("Bearer ", "");
|
||||
TokenService.TokenInfo tokenInfo = tokenService.verifyToken(rawToken);
|
||||
if (tokenInfo != null) {
|
||||
Long userId = tokenInfo.userId;
|
||||
Cookie cookie = cookieRepository.findById(cid).orElse(null);
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限操作该Cookie");
|
||||
if (!Objects.equals(userId, 1L)) {
|
||||
Cookie cookie = cookieRepository.findById(cid).orElse(null);
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限操作该Cookie");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -95,7 +128,7 @@ public class KeywordController {
|
||||
throw new RuntimeException("关键词不能为空");
|
||||
}
|
||||
keywordStr = keywordStr.trim();
|
||||
|
||||
|
||||
String reply = (String) kwData.getOrDefault("reply", "");
|
||||
String itemId = (String) kwData.getOrDefault("item_id", "");
|
||||
if (itemId != null && itemId.trim().isEmpty()) itemId = null; // Normalize empty to null
|
||||
@ -107,7 +140,7 @@ public class KeywordController {
|
||||
throw new RuntimeException("关键词 '" + keywordStr + "' " + itemText + " 在当前提交中重复");
|
||||
}
|
||||
keywordSet.add(key);
|
||||
|
||||
|
||||
// Check conflict with image keywords in DB
|
||||
if (itemId != null) {
|
||||
if (!keywordRepository.findConflictImageKeywords(cid, keywordStr, itemId).isEmpty()) {
|
||||
@ -115,10 +148,10 @@ public class KeywordController {
|
||||
}
|
||||
} else {
|
||||
if (!keywordRepository.findConflictGenericImageKeywords(cid, keywordStr).isEmpty()) {
|
||||
throw new RuntimeException("关键词 '" + keywordStr + "' (通用关键词) 已存在(图片关键词),无法保存为文本关键词");
|
||||
throw new RuntimeException("关键词 '" + keywordStr + "' (通用关键词) 已存在(图片关键词),无法保存为文本关键词");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Keyword k = new Keyword();
|
||||
k.setCookieId(cid);
|
||||
k.setKeyword(keywordStr);
|
||||
@ -127,13 +160,13 @@ public class KeywordController {
|
||||
k.setType("text");
|
||||
keywordsToSave.add(k);
|
||||
}
|
||||
|
||||
|
||||
// Transactional update
|
||||
updateKeywordsTransactional(cid, keywordsToSave);
|
||||
|
||||
|
||||
return Map.of("msg", "updated", "count", keywordsToSave.size());
|
||||
}
|
||||
|
||||
|
||||
@Transactional
|
||||
protected void updateKeywordsTransactional(String cid, List<Keyword> newKeywords) {
|
||||
keywordRepository.deleteTextKeywordsByCookieId(cid);
|
||||
@ -145,14 +178,14 @@ public class KeywordController {
|
||||
public List<Keyword> getKeywords(@PathVariable String cid) {
|
||||
return keywordRepository.findByCookieId(cid);
|
||||
}
|
||||
|
||||
|
||||
// 对应 Python: @app.post('/keywords/{cid}')
|
||||
@PostMapping("/keywords/{cid}")
|
||||
public Keyword addKeyword(@PathVariable String cid, @RequestBody Keyword keyword) {
|
||||
keyword.setCookieId(cid);
|
||||
return keywordRepository.save(keyword);
|
||||
}
|
||||
|
||||
|
||||
// 对应 Python: @app.delete('/keywords/{cid}/{index}')
|
||||
// Python used index, Java uses ID.
|
||||
@DeleteMapping("/keywords/{cid}/{id}")
|
||||
@ -172,9 +205,9 @@ public class KeywordController {
|
||||
defaultReply.setCookieId(cid);
|
||||
return defaultReplyRepository.save(defaultReply);
|
||||
}
|
||||
|
||||
|
||||
// ------------------------- AI Settings -------------------------
|
||||
|
||||
|
||||
// GET /ai-reply-settings - Get all AI settings for current user (Aggregated)
|
||||
@GetMapping("/ai-reply-settings")
|
||||
public Map<String, AiReplySetting> getAllAiSettings() {
|
||||
@ -183,10 +216,10 @@ public class KeywordController {
|
||||
.collect(Collectors.toList());
|
||||
|
||||
List<AiReplySetting> settings = aiReplySettingRepository.findAllById(cookieIds);
|
||||
|
||||
|
||||
return settings.stream().collect(Collectors.toMap(AiReplySetting::getCookieId, s -> s));
|
||||
}
|
||||
|
||||
|
||||
@GetMapping("/ai-reply-settings/{cookieId}")
|
||||
public AiReplySetting getAiSetting(@PathVariable String cookieId) {
|
||||
return aiReplySettingRepository.findById(cookieId).orElse(null);
|
||||
@ -204,8 +237,148 @@ public class KeywordController {
|
||||
String userId = (String) testData.getOrDefault("user_id", "test_user");
|
||||
String itemId = (String) testData.getOrDefault("item_id", "test_item");
|
||||
String message = (String) testData.get("message");
|
||||
|
||||
|
||||
String reply = aiReplyService.generateReply(cookieId, chatId, userId, itemId, message);
|
||||
return java.util.Collections.singletonMap("reply", reply);
|
||||
}
|
||||
|
||||
// ------------------------- Default Reply Compatibility Routes -------------------------
|
||||
// 兼容前端使用 /default-reply/ 单数形式的请求
|
||||
|
||||
/**
|
||||
* 获取指定账号的默认回复设置(兼容路由)
|
||||
* 对应 Python: @app.get('/default-reply/{cid}')
|
||||
*/
|
||||
@GetMapping("/default-reply/{cid}")
|
||||
public Map<String, Object> getDefaultReplyCompat(@PathVariable String cid,
|
||||
@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
// 验证用户权限
|
||||
if (token != null) {
|
||||
String rawToken = token.replace("Bearer ", "");
|
||||
TokenService.TokenInfo tokenInfo = tokenService.verifyToken(rawToken);
|
||||
if (tokenInfo != null) {
|
||||
Long userId = tokenInfo.userId;
|
||||
if (!Objects.equals(userId, 1L)) {
|
||||
Cookie cookie = cookieRepository.findById(cid).orElse(null);
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限访问该Cookie");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 获取默认回复设置
|
||||
DefaultReply defaultReply = defaultReplyRepository.findById(cid).orElse(null);
|
||||
if (defaultReply == null) {
|
||||
// 如果没有设置,返回默认值(与 Python 实现一致)
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("enabled", false);
|
||||
result.put("reply_content", "");
|
||||
result.put("reply_once", false);
|
||||
result.put("reply_image_url", "");
|
||||
return result;
|
||||
}
|
||||
|
||||
// 返回结果(使用 Python 风格的字段名 snake_case)
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("enabled", defaultReply.getEnabled());
|
||||
result.put("reply_content", defaultReply.getReplyContent() != null ? defaultReply.getReplyContent() : "");
|
||||
result.put("reply_once", defaultReply.getReplyOnce());
|
||||
result.put("reply_image_url", defaultReply.getReplyImageUrl() != null ? defaultReply.getReplyImageUrl() : "");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定账号的默认回复设置(兼容路由)
|
||||
* 对应 Python: @app.put('/default-reply/{cid}')
|
||||
*/
|
||||
@PutMapping("/default-reply/{cid}")
|
||||
public Map<String, Object> updateDefaultReplyCompat(@PathVariable String cid,
|
||||
@RequestBody DefaultReply defaultReply,
|
||||
@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
// 验证用户权限
|
||||
if (token != null) {
|
||||
String rawToken = token.replace("Bearer ", "");
|
||||
TokenService.TokenInfo tokenInfo = tokenService.verifyToken(rawToken);
|
||||
if (tokenInfo != null) {
|
||||
Long userId = tokenInfo.userId;
|
||||
if (!Objects.equals(userId, 1L)) {
|
||||
Cookie cookie = cookieRepository.findById(cid).orElse(null);
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限操作该Cookie");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 设置 cookieId 并保存
|
||||
defaultReply.setCookieId(cid);
|
||||
DefaultReply saved = defaultReplyRepository.save(defaultReply);
|
||||
|
||||
// 返回结果(与 Python 实现一致)
|
||||
Map<String, Object> result = new java.util.HashMap<>();
|
||||
result.put("msg", "default reply updated");
|
||||
result.put("enabled", saved.getEnabled());
|
||||
result.put("reply_once", saved.getReplyOnce());
|
||||
result.put("reply_image_url", saved.getReplyImageUrl() != null ? saved.getReplyImageUrl() : "");
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除指定账号的默认回复设置(兼容路由)
|
||||
* 对应 Python: @app.delete('/default-reply/{cid}')
|
||||
*/
|
||||
@DeleteMapping("/default-reply/{cid}")
|
||||
public Map<String, String> deleteDefaultReplyCompat(@PathVariable String cid,
|
||||
@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
// 验证用户权限
|
||||
if (token != null) {
|
||||
String rawToken = token.replace("Bearer ", "");
|
||||
TokenService.TokenInfo tokenInfo = tokenService.verifyToken(rawToken);
|
||||
if (tokenInfo != null) {
|
||||
Long userId = tokenInfo.userId;
|
||||
if (!Objects.equals(userId, 1L)) {
|
||||
Cookie cookie = cookieRepository.findById(cid).orElse(null);
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限操作该Cookie");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 删除默认回复设置
|
||||
try {
|
||||
defaultReplyRepository.deleteById(cid);
|
||||
return Map.of("msg", "default reply deleted");
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException("删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空指定账号的默认回复记录(兼容路由)
|
||||
* 对应 Python: @app.post('/default-reply/{cid}/clear-records')
|
||||
*/
|
||||
@PostMapping("/default-reply/{cid}/clear-records")
|
||||
public Map<String, String> clearDefaultReplyRecordsCompat(@PathVariable String cid,
|
||||
@RequestHeader(value = "Authorization", required = false) String token) {
|
||||
// 验证用户权限
|
||||
if (token != null) {
|
||||
String rawToken = token.replace("Bearer ", "");
|
||||
TokenService.TokenInfo tokenInfo = tokenService.verifyToken(rawToken);
|
||||
if (tokenInfo != null) {
|
||||
Long userId = tokenInfo.userId;
|
||||
if(!Objects.equals(userId, 1L)) {
|
||||
Cookie cookie = cookieRepository.findById(cid).orElse(null);
|
||||
if (cookie != null && !cookie.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权限操作该Cookie");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 清空默认回复记录
|
||||
defaultReplyRecordRepository.deleteByCookieId(cid);
|
||||
return Map.of("msg", "default reply records cleared");
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
package com.xianyu.autoreply.dto;
|
||||
package com.xianyu.autoreply.model.req;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Data
|
||||
public class KeywordWithItemIdRequest {
|
||||
private List<Map<String, Object>> keywords;
|
||||
}
|
||||
}
|
||||
@ -9,4 +9,5 @@ import java.util.List;
|
||||
@Repository
|
||||
public interface CookieRepository extends JpaRepository<Cookie, String> {
|
||||
List<Cookie> findByUserId(Long userId);
|
||||
long countByEnabled(Boolean enabled);
|
||||
}
|
||||
|
||||
@ -8,4 +8,5 @@ import java.util.Optional;
|
||||
@Repository
|
||||
public interface DefaultReplyRecordRepository extends JpaRepository<DefaultReplyRecord, Long> {
|
||||
Optional<DefaultReplyRecord> findByCookieIdAndChatId(String cookieId, String chatId);
|
||||
void deleteByCookieId(String cookieId);
|
||||
}
|
||||
|
||||
@ -2089,7 +2089,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
*/
|
||||
private int calculateRetryDelay(int failures) {
|
||||
// 根据失败次数计算延迟:3秒 * 失败次数,最多30秒
|
||||
return Math.min(30 * failures, 120);
|
||||
return Math.min(3 * failures, 30);
|
||||
}
|
||||
|
||||
// ============== 消息发送方法 ==============
|
||||
@ -2536,7 +2536,7 @@ public class XianyuClient extends TextWebSocketHandler {
|
||||
// 对应Python: Line 7336-7391
|
||||
JSONObject message = decryptMessage(messageData);
|
||||
if (message == null) {
|
||||
log.error("【{}】消息解密失败或为空", cookieId);
|
||||
log.warn("【{}】消息解密失败或为空", cookieId);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -6,7 +6,7 @@ server:
|
||||
|
||||
spring:
|
||||
application:
|
||||
name: xianyu-auto-reply-backend
|
||||
name: xianyu-free
|
||||
|
||||
datasource:
|
||||
driver-class-name: org.sqlite.JDBC
|
||||
@ -22,10 +22,10 @@ spring:
|
||||
database-platform: org.hibernate.community.dialect.SQLiteDialect
|
||||
hibernate:
|
||||
ddl-auto: ${app.ddl-auto:update}
|
||||
show-sql: false # Set to false to disable SQL logging
|
||||
show-sql: true # Set to false to disable SQL logging
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: false # Set to false to disable SQL formatting
|
||||
format_sql: true # Set to false to disable SQL formatting
|
||||
|
||||
servlet:
|
||||
multipart:
|
||||
|
||||
@ -22,7 +22,7 @@ export function Accounts() {
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [accounts, setAccounts] = useState<AccountWithKeywordCount[]>([])
|
||||
const [activeModal, setActiveModal] = useState<ModalType>(null)
|
||||
|
||||
|
||||
// 默认密码检查状态
|
||||
const [usingDefaultPassword, setUsingDefaultPassword] = useState(false)
|
||||
const [showPasswordWarning, setShowPasswordWarning] = useState(false)
|
||||
@ -124,7 +124,7 @@ export function Accounts() {
|
||||
// 单独的 useEffect 检查默认密码
|
||||
useEffect(() => {
|
||||
if (!_hasHydrated || !isAuthenticated || !token || !user) return
|
||||
|
||||
|
||||
// 检查是否使用默认密码
|
||||
const checkPassword = async () => {
|
||||
if (user.is_admin) {
|
||||
@ -165,7 +165,7 @@ export function Accounts() {
|
||||
setShowPasswordWarning(true)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
setActiveModal('qrcode')
|
||||
setQrStatus('loading')
|
||||
try {
|
||||
@ -185,7 +185,7 @@ export function Accounts() {
|
||||
addToast({ type: 'error', message: '生成二维码失败' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 检查默认密码后打开弹窗
|
||||
const handleOpenModal = (modal: ModalType) => {
|
||||
if (usingDefaultPassword && (modal === 'password' || modal === 'manual')) {
|
||||
@ -394,11 +394,11 @@ export function Accounts() {
|
||||
}
|
||||
|
||||
// 更新登录信息
|
||||
const loginInfoChanged =
|
||||
const loginInfoChanged =
|
||||
editUsername !== (editingAccount.username || '') ||
|
||||
editLoginPassword !== (editingAccount.login_password || '') ||
|
||||
editShowBrowser !== (editingAccount.show_browser || false)
|
||||
|
||||
|
||||
if (loginInfoChanged) {
|
||||
promises.push(updateAccountLoginInfo(editingAccount.id, {
|
||||
username: editUsername,
|
||||
@ -424,7 +424,7 @@ export function Accounts() {
|
||||
setDefaultReplyContent('')
|
||||
setDefaultReplyImageUrl('')
|
||||
setActiveModal('default-reply')
|
||||
|
||||
|
||||
// 加载当前默认回复
|
||||
try {
|
||||
const result = await getDefaultReply(account.id)
|
||||
@ -437,7 +437,7 @@ export function Accounts() {
|
||||
|
||||
const handleSaveDefaultReply = async () => {
|
||||
if (!defaultReplyAccount) return
|
||||
|
||||
|
||||
try {
|
||||
setDefaultReplySaving(true)
|
||||
await updateDefaultReply(defaultReplyAccount.id, defaultReplyContent, true, false, defaultReplyImageUrl)
|
||||
@ -454,12 +454,12 @@ export function Accounts() {
|
||||
const handleUploadDefaultReplyImage = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0]
|
||||
if (!file) return
|
||||
|
||||
|
||||
try {
|
||||
setUploadingDefaultReplyImage(true)
|
||||
const formData = new FormData()
|
||||
formData.append('image', file)
|
||||
|
||||
|
||||
const response = await fetch('/upload-image', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
@ -467,7 +467,7 @@ export function Accounts() {
|
||||
},
|
||||
body: formData
|
||||
})
|
||||
|
||||
|
||||
const result = await response.json()
|
||||
if (result.image_url) {
|
||||
setDefaultReplyImageUrl(result.image_url)
|
||||
@ -491,7 +491,7 @@ export function Accounts() {
|
||||
const currentSettings = await getAIReplySettings(account.id)
|
||||
await updateAIReplySettings(account.id, {
|
||||
...currentSettings,
|
||||
enabled: newEnabled,
|
||||
ai_enabled: newEnabled,
|
||||
})
|
||||
setAccounts(prev => prev.map(a =>
|
||||
a.id === account.id ? { ...a, aiEnabled: newEnabled } : a,
|
||||
@ -526,7 +526,7 @@ export function Accounts() {
|
||||
try {
|
||||
setAiSettingsSaving(true)
|
||||
await updateAIReplySettings(aiSettingsAccount.id, {
|
||||
enabled: aiEnabled,
|
||||
ai_enabled: aiEnabled,
|
||||
max_discount_percent: aiMaxDiscountPercent,
|
||||
max_discount_amount: aiMaxDiscountAmount,
|
||||
max_bargain_rounds: aiMaxBargainRounds,
|
||||
@ -674,11 +674,10 @@ export function Accounts() {
|
||||
<td>
|
||||
<button
|
||||
onClick={() => handleToggleAI(account)}
|
||||
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors ${
|
||||
account.aiEnabled
|
||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-900/50'
|
||||
: 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-600'
|
||||
}`}
|
||||
className={`inline-flex items-center gap-1.5 px-2 py-1 rounded text-xs font-medium transition-colors ${account.aiEnabled
|
||||
? 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-300 hover:bg-purple-200 dark:hover:bg-purple-900/50'
|
||||
: 'bg-slate-100 text-slate-500 dark:bg-slate-700 dark:text-slate-400 hover:bg-slate-200 dark:hover:bg-slate-600'
|
||||
}`}
|
||||
title={account.aiEnabled ? '点击关闭AI回复' : '点击开启AI回复'}
|
||||
>
|
||||
<Bot className="w-3.5 h-3.5" />
|
||||
@ -991,14 +990,12 @@ export function Accounts() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditAutoConfirm(!editAutoConfirm)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
editAutoConfirm ? 'bg-blue-600' : 'bg-slate-300 dark:bg-slate-600'
|
||||
}`}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editAutoConfirm ? 'bg-blue-600' : 'bg-slate-300 dark:bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
editAutoConfirm ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editAutoConfirm ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
@ -1067,14 +1064,12 @@ export function Accounts() {
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setEditShowBrowser(!editShowBrowser)}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
|
||||
editShowBrowser ? 'bg-blue-600' : 'bg-slate-300 dark:bg-slate-600'
|
||||
}`}
|
||||
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${editShowBrowser ? 'bg-blue-600' : 'bg-slate-300 dark:bg-slate-600'
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
|
||||
editShowBrowser ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${editShowBrowser ? 'translate-x-6' : 'translate-x-1'
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user