This commit is contained in:
wangli 2026-01-18 23:16:21 +08:00
parent 5bec08604e
commit 729fad3615
12 changed files with 376 additions and 92 deletions

View File

@ -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");

View File

@ -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;
}
}

View File

@ -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("需要管理员权限");
}
}
}

View File

@ -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;
}
}

View File

@ -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");
}
}

View File

@ -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;
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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:

View File

@ -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>