This commit is contained in:
wangli 2026-01-19 00:25:36 +08:00
parent 729fad3615
commit 72e067c9c1
22 changed files with 650 additions and 203 deletions

View File

@ -31,15 +31,14 @@ import java.util.stream.Stream;
@Slf4j
@RestController
@RequestMapping
public class AdminController {
public class AdminController extends BaseController {
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";
@ -50,12 +49,12 @@ public class AdminController {
CardRepository cardRepository,
KeywordRepository keywordRepository,
TokenService tokenService) {
super(tokenService);
this.userRepository = userRepository;
this.cookieRepository = cookieRepository;
this.orderRepository = orderRepository;
this.cardRepository = cardRepository;
this.keywordRepository = keywordRepository;
this.tokenService = tokenService;
}
// ------------------------- User Management -------------------------

View File

@ -17,18 +17,18 @@ import java.util.Map;
@Slf4j
@RestController
@RequestMapping
public class AuthController {
public class AuthController extends BaseController {
private final AuthService authService;
private final TokenService tokenService;
private static final String ADMIN_USERNAME = "admin";
private static final String DEFAULT_ADMIN_PASSWORD = "admin123";
@Autowired
public AuthController(AuthService authService, TokenService tokenService) {
public AuthController(AuthService authService,
TokenService tokenService) {
super(tokenService);
this.authService = authService;
this.tokenService = tokenService;
}
/**
@ -69,7 +69,7 @@ public class AuthController {
loginType = "用户名/密码";
log.info("【{}】尝试用户名登录", request.getUsername());
user = authService.verifyUserPassword(request.getUsername(), request.getPassword());
}
}
// 2. 邮箱/密码登录
else if (StrUtil.isNotBlank(request.getEmail()) && StrUtil.isNotBlank(request.getPassword())) {
loginType = "邮箱/密码";
@ -95,15 +95,15 @@ public class AuthController {
if (user != null) {
boolean isAdmin = ADMIN_USERNAME.equals(user.getUsername());
String token = tokenService.generateToken(user, isAdmin);
log.info("【{}#{}】{}登录成功{}", user.getUsername(), user.getId(), loginType, isAdmin ? "(管理员)" : "");
return new LoginResponse(true, token, "登录成功", user.getId(), user.getUsername(), isAdmin);
} else {
log.warn("{}登录失败", loginType);
if (loginType.contains("验证码")) {
// 这个分支其实上面已经处理了这里是兜底逻辑
return new LoginResponse(false, "用户不存在");
return new LoginResponse(false, "用户不存在");
}
return new LoginResponse(false, "用户名或密码错误"); // 或邮箱或密码错误
}
@ -117,7 +117,7 @@ public class AuthController {
public Map<String, Object> verify(HttpServletRequest request) {
String token = getTokenFromRequest(request);
TokenService.TokenInfo info = tokenService.verifyToken(token);
Map<String, Object> response = new HashMap<>();
if (info != null) {
response.put("authenticated", true);
@ -142,7 +142,7 @@ public class AuthController {
response.put("message", "已登出");
return response;
}
/**
* 修改管理员密码接口
* 对应 Python: /change-admin-password
@ -151,12 +151,12 @@ public class AuthController {
public Map<String, Object> changeAdminPassword(@RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) {
String token = getTokenFromRequest(httpRequest);
TokenService.TokenInfo info = tokenService.verifyToken(token);
Map<String, Object> response = new HashMap<>();
if (info == null || !info.isAdmin) {
response.put("success", false);
response.put("message", "未授权访问或非管理员");
return response;
response.put("success", false);
response.put("message", "未授权访问或非管理员");
return response;
}
User user = authService.verifyUserPassword(ADMIN_USERNAME, request.getCurrent_password());
@ -177,7 +177,7 @@ public class AuthController {
}
return response;
}
/**
* 普通用户修改密码接口
* 对应 Python: /change-password
@ -186,19 +186,19 @@ public class AuthController {
public Map<String, Object> changeUserPassword(@RequestBody ChangePasswordRequest request, HttpServletRequest httpRequest) {
String token = getTokenFromRequest(httpRequest);
TokenService.TokenInfo info = tokenService.verifyToken(token);
Map<String, Object> response = new HashMap<>();
if (info == null) {
response.put("success", false);
response.put("message", "无法获取用户信息");
return response;
response.put("success", false);
response.put("message", "无法获取用户信息");
return response;
}
User user = authService.verifyUserPassword(info.username, request.getCurrent_password());
if (user == null) {
response.put("success", false);
response.put("message", "当前密码错误");
return response;
response.put("success", false);
response.put("message", "当前密码错误");
return response;
}
boolean success = authService.updateUserPassword(info.username, request.getNew_password());
@ -212,7 +212,7 @@ public class AuthController {
}
return response;
}
/**
* 检查是否使用默认密码
* 对应 Python: /api/check-default-password
@ -221,13 +221,13 @@ public class AuthController {
public Map<String, Boolean> checkDefaultPassword(HttpServletRequest httpRequest) {
String token = getTokenFromRequest(httpRequest);
TokenService.TokenInfo info = tokenService.verifyToken(token);
Map<String, Boolean> response = new HashMap<>();
if (info == null || !info.isAdmin) {
response.put("using_default", false);
return response;
response.put("using_default", false);
return response;
}
User adminUser = authService.verifyUserPassword(ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD);
response.put("using_default", adminUser != null);
return response;
@ -272,7 +272,7 @@ public class AuthController {
this.is_admin = isAdmin;
}
}
@Data
public static class ChangePasswordRequest {
private String current_password;

View File

@ -0,0 +1,34 @@
package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.service.TokenService;
import lombok.Data;
import java.util.Objects;
/**
* 用于处理 Admin 账号的特殊逻辑
*
* @author wangli
* @since 2026-01-18 23:21
*/
@Data
public abstract class BaseController {
protected final TokenService tokenService;
protected boolean isAdmin(String token) {
return isAdmin(getUserId(token));
}
protected boolean isAdmin(Long userId) {
return Objects.equals(1L, userId);
}
// Helper to get user ID
protected Long getUserId(String token) {
if (token == null) throw new RuntimeException("Unauthorized");
String rawToken = token.replace("Bearer ", "");
TokenService.TokenInfo info = tokenService.verifyToken(rawToken);
if (info == null) throw new RuntimeException("Unauthorized");
return info.userId;
}
}

View File

@ -2,6 +2,7 @@ package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.entity.CaptchaCode;
import com.xianyu.autoreply.repository.CaptchaCodeRepository;
import com.xianyu.autoreply.service.TokenService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -15,12 +16,14 @@ import java.util.Map;
@Slf4j
@RestController
// 注意移除了类级别的 @RequestMapping("/api/captcha")改用方法级别的根路径映射
public class CaptchaController {
public class CaptchaController extends BaseController {
private final CaptchaCodeRepository captchaCodeRepository;
@Autowired
public CaptchaController(CaptchaCodeRepository captchaCodeRepository) {
public CaptchaController(CaptchaCodeRepository captchaCodeRepository,
TokenService tokenService) {
super(tokenService);
this.captchaCodeRepository = captchaCodeRepository;
}

View File

@ -1,6 +1,7 @@
package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.service.CaptchaSessionService;
import com.xianyu.autoreply.service.TokenService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
@ -13,12 +14,14 @@ import java.util.Map;
@RestController
@RequestMapping("/api/captcha")
public class CaptchaRemoteController {
public class CaptchaRemoteController extends BaseController {
private final CaptchaSessionService sessionService;
@Autowired
public CaptchaRemoteController(CaptchaSessionService sessionService) {
public CaptchaRemoteController(CaptchaSessionService sessionService,
TokenService tokenService) {
super(tokenService);
this.sessionService = sessionService;
}
@ -30,10 +33,10 @@ public class CaptchaRemoteController {
map.put("session_id", id);
map.put("completed", session.isCompleted());
// has_websocket check would require exposing wsConnections from Handler, skipped for now
map.put("has_websocket", true);
map.put("has_websocket", true);
sessions.add(map);
});
return Map.of("count", sessions.size(), "sessions", sessions);
}
@ -41,7 +44,7 @@ public class CaptchaRemoteController {
public Map<String, Object> getSessionInfo(@PathVariable String sessionId) {
CaptchaSessionService.CaptchaSession session = sessionService.getSession(sessionId);
if (session == null) throw new RuntimeException("会话不存在");
Map<String, Object> resp = new HashMap<>();
resp.put("session_id", sessionId);
resp.put("screenshot", session.getScreenshot());
@ -55,23 +58,23 @@ public class CaptchaRemoteController {
public Map<String, String> getScreenshot(@PathVariable String sessionId) {
CaptchaSessionService.CaptchaSession session = sessionService.getSession(sessionId);
if (session == null) throw new RuntimeException("会话不存在");
return Map.of("screenshot", session.getScreenshot());
}
@PostMapping("/mouse_event")
public Map<String, Object> handleMouseEvent(@RequestBody MouseEventRequest request) {
boolean success = sessionService.handleMouseEvent(
request.getSession_id(),
request.getEvent_type(),
request.getX(),
request.getSession_id(),
request.getEvent_type(),
request.getX(),
request.getY()
);
if (!success) throw new RuntimeException("处理失败");
boolean completed = sessionService.checkCompletion(request.getSession_id());
return Map.of("success", true, "completed", completed);
}
@ -87,7 +90,7 @@ public class CaptchaRemoteController {
sessionService.closeSession(sessionId);
return Map.of("success", true);
}
// HTML Control Page serving could be done by Thymeleaf or Static Resource,
// here returning simple string or checking static folder.
// Python served specific HTML file.

View File

@ -27,34 +27,27 @@ import java.util.stream.Collectors;
@RestController
@RequestMapping("/cookies")
public class CookieController {
public class CookieController extends BaseController {
private final CookieRepository cookieRepository;
private final XianyuClientService xianyuClientService;
private final BrowserService browserService;
private final TokenService tokenService;
@Autowired
public CookieController(CookieRepository cookieRepository,
XianyuClientService xianyuClientService,
BrowserService browserService,
TokenService tokenService) {
super(tokenService);
this.cookieRepository = cookieRepository;
this.xianyuClientService = xianyuClientService;
this.browserService = browserService;
this.tokenService = tokenService;
}
// Helper to get user ID
private Long getUserId(String token) {
if (token == null) throw new RuntimeException("Unauthorized");
String rawToken = token.replace("Bearer ", "");
TokenService.TokenInfo info = tokenService.verifyToken(rawToken);
if (info == null) throw new RuntimeException("Unauthorized");
return info.userId;
}
private void checkOwnership(Cookie cookie, Long userId) {
if (isAdmin(userId)) return;
if (cookie != null && !cookie.getUserId().equals(userId)) {
throw new RuntimeException("Forbidden: You do not own this cookie");
}
@ -63,6 +56,9 @@ public class CookieController {
@GetMapping
public List<Cookie> listCookies(@RequestHeader(value = "Authorization", required = false) String token) {
Long userId = getUserId(token);
if (isAdmin(userId)) {
return cookieRepository.findAll();
}
// Repository needs a method findByUserId. Assuming it exists.
return cookieRepository.findByUserId(userId);
}

View File

@ -1,6 +1,7 @@
package com.xianyu.autoreply.controller;
import cn.hutool.json.JSONObject;
import com.xianyu.autoreply.service.TokenService;
import com.xianyu.autoreply.utils.GeetestLib;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
@ -13,12 +14,14 @@ import java.util.Map;
@Slf4j
@RestController
@RequestMapping("/geetest")
public class GeetestController {
public class GeetestController extends BaseController {
private final GeetestLib geetestLib;
@Autowired
public GeetestController(GeetestLib geetestLib) {
public GeetestController(GeetestLib geetestLib,
TokenService tokenService) {
super(tokenService);
this.geetestLib = geetestLib;
}
@ -30,25 +33,25 @@ public class GeetestController {
// 必传参数
// digestmod: 加密算法"md5", "sha256", "hmac-sha256"
GeetestLib.GeetestResult result = geetestLib.register(GeetestLib.DigestMod.MD5, null, null);
Map<String, Object> response = new HashMap<>();
if (result.getStatus() == 1 || (result.getData() != null && result.getData().contains("\"success\": 0"))) {
// status 1 means full success
// or if it fallback mode (status might be 0 in Lib but we treat as success HTTP response with offline data)
// Check GeetestLib: logic. It sets status=0 if logic fails?
// GeetestLib: "初始化接口失败,后续流程走宕机模式" sets status=0.
// But for the frontend, getting the offline parameters IS a successful API call.
response.put("success", true);
response.put("code", 200);
response.put("message", "获取成功");
response.put("data", result.toJsonObject());
// status 1 means full success
// or if it fallback mode (status might be 0 in Lib but we treat as success HTTP response with offline data)
// Check GeetestLib: logic. It sets status=0 if logic fails?
// GeetestLib: "初始化接口失败,后续流程走宕机模式" sets status=0.
// But for the frontend, getting the offline parameters IS a successful API call.
response.put("success", true);
response.put("code", 200);
response.put("message", "获取成功");
response.put("data", result.toJsonObject());
} else {
response.put("success", false);
response.put("code", 500);
response.put("message", "获取验证参数失败: " + result.getMsg());
response.put("success", false);
response.put("code", 500);
response.put("message", "获取验证参数失败: " + result.getMsg());
}
return response;
}
@ -58,20 +61,20 @@ public class GeetestController {
@PostMapping("/validate")
public Map<String, Object> validate(@RequestBody ValidateRequest request) {
GeetestLib.GeetestResult result;
// 这里的逻辑需要根据 register 返回的 new_captcha (gt_server_status) 来判断走 normal 还是 fail 模式
// 但是在 Python SDK 的使用中这个状态通常维护在 Session
// 简单实现如果不判断状态默认尝试走 successValidate (正常模式)
// 也可以让前端传回来或者像 Python demo 那样存 session
// Python reply_server.py 其实并没有展示完整的 validate 逻辑
// 这里我们按照 Standard Flow 实现
result = geetestLib.successValidate(
request.getChallenge(),
request.getValidate(),
request.getSeccode(),
null,
request.getChallenge(),
request.getValidate(),
request.getSeccode(),
null,
null
);
@ -85,7 +88,7 @@ public class GeetestController {
response.put("code", 400);
response.put("message", "验证失败: " + result.getMsg());
}
return response;
}

View File

@ -3,6 +3,8 @@ package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.entity.ItemInfo;
import com.xianyu.autoreply.repository.CookieRepository;
import com.xianyu.autoreply.repository.ItemInfoRepository;
import com.xianyu.autoreply.service.TokenService;
import com.xianyu.autoreply.service.XianyuClient;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
@ -20,13 +22,16 @@ import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping
public class ItemController {
public class ItemController extends BaseController {
private final ItemInfoRepository itemInfoRepository;
private final CookieRepository cookieRepository;
@Autowired
public ItemController(ItemInfoRepository itemInfoRepository, CookieRepository cookieRepository) {
public ItemController(ItemInfoRepository itemInfoRepository,
CookieRepository cookieRepository,
TokenService tokenService) {
super(tokenService);
this.itemInfoRepository = itemInfoRepository;
this.cookieRepository = cookieRepository;
}
@ -35,19 +40,20 @@ public class ItemController {
// GET /items - Get all items for current user (Aggregated)
@GetMapping("/items")
public Map<String, Object> getAllItems() {
public Map<String, Object> getAllItems(@RequestHeader(value = "Authorization") String token) {
// Migration assumption: Single user or Admin view, so we fetch all cookies first.
List<String> cookieIds = cookieRepository.findAll().stream()
.map(com.xianyu.autoreply.entity.Cookie::getId)
.collect(Collectors.toList());
List<ItemInfo> allItems = new ArrayList<>();
if (!cookieIds.isEmpty()) {
for (String cid : cookieIds) {
allItems.addAll(itemInfoRepository.findByCookieId(cid));
}
for (String cid : cookieIds) {
allItems.addAll(itemInfoRepository.findByCookieId(cid));
}
}
return Map.of("items", allItems);
}
@ -55,7 +61,7 @@ public class ItemController {
public List<ItemInfo> getItems(@PathVariable String cid) {
return itemInfoRepository.findByCookieId(cid);
}
// Alias for consistency
@GetMapping("/items/cookie/{cookie_id}")
public List<ItemInfo> getItemsAlias(@PathVariable String cookie_id) {
@ -69,22 +75,23 @@ public class ItemController {
}
@PutMapping("/items/{cookie_id}/{item_id}")
public Map<String, Object> updateItem(@PathVariable String cookie_id,
@PathVariable String item_id,
public Map<String, Object> updateItem(@PathVariable String cookie_id,
@PathVariable String item_id,
@RequestBody ItemInfo itemUpdate) {
ItemInfo item = itemInfoRepository.findByCookieIdAndItemId(cookie_id, item_id)
.orElseThrow(() -> new RuntimeException("Item not found"));
if (itemUpdate.getItemTitle() != null) item.setItemTitle(itemUpdate.getItemTitle());
if (itemUpdate.getItemDescription() != null) item.setItemDescription(itemUpdate.getItemDescription());
if (itemUpdate.getItemPrice() != null) item.setItemPrice(itemUpdate.getItemPrice());
if (itemUpdate.getItemDetail() != null) item.setItemDetail(itemUpdate.getItemDetail());
if (itemUpdate.getItemCategory() != null) item.setItemCategory(itemUpdate.getItemCategory());
// Specific flags
if (itemUpdate.getIsMultiSpec() != null) item.setIsMultiSpec(itemUpdate.getIsMultiSpec());
if (itemUpdate.getMultiQuantityDelivery() != null) item.setMultiQuantityDelivery(itemUpdate.getMultiQuantityDelivery());
if (itemUpdate.getMultiQuantityDelivery() != null)
item.setMultiQuantityDelivery(itemUpdate.getMultiQuantityDelivery());
itemInfoRepository.save(item);
return Map.of("success", true, "msg", "Item updated", "data", item);
}
@ -92,8 +99,8 @@ public class ItemController {
@Transactional
@DeleteMapping("/items/{cookie_id}/{item_id}")
public Map<String, Object> deleteItem(@PathVariable String cookie_id, @PathVariable String item_id) {
itemInfoRepository.deleteByCookieIdAndItemId(cookie_id, item_id);
return Map.of("success", true, "msg", "Item deleted");
itemInfoRepository.deleteByCookieIdAndItemId(cookie_id, item_id);
return Map.of("success", true, "msg", "Item deleted");
}
// ------------------------- Batch Operations -------------------------
@ -122,13 +129,13 @@ public class ItemController {
@PostMapping("/items/search_multiple")
public Map<String, Object> searchItemsMultiple(@RequestBody MultiSearchRequest request) {
if (request.getCookie_ids() == null || request.getCookie_ids().isEmpty()) {
return Map.of("success", false, "message", "No cookie IDs provided");
return Map.of("success", false, "message", "No cookie IDs provided");
}
String keyword = request.getKeyword() != null ? request.getKeyword() : "";
List<ItemInfo> items = itemInfoRepository.findByCookieIdInAndItemTitleContainingIgnoreCase(
request.getCookie_ids(), keyword);
return Map.of("success", true, "data", items);
}
@ -139,23 +146,23 @@ public class ItemController {
try {
int page = request.getPage_number() > 0 ? request.getPage_number() - 1 : 0;
int size = request.getPage_size() > 0 ? request.getPage_size() : 20;
Pageable pageable = PageRequest.of(page, size, Sort.by("updatedAt").descending());
Page<ItemInfo> pageResult;
if (request.getKeyword() != null && !request.getKeyword().isEmpty()) {
pageResult = itemInfoRepository.findByCookieIdAndItemTitleContainingIgnoreCase(
request.getCookie_id(), request.getKeyword(), pageable);
} else {
pageResult = itemInfoRepository.findByCookieId(request.getCookie_id(), pageable);
}
Map<String, Object> data = new HashMap<>();
data.put("items", pageResult.getContent());
data.put("total", pageResult.getTotalElements());
data.put("current_page", request.getPage_number());
data.put("total_pages", pageResult.getTotalPages());
return Map.of("success", true, "data", data);
} catch (Exception e) {
log.error("Error getting items by page", e);
@ -166,12 +173,12 @@ public class ItemController {
// ------------------------- Specific Feature Updates -------------------------
@PutMapping("/items/{cookie_id}/{item_id}/multi-spec")
public Map<String, Object> updateMultiSpec(@PathVariable String cookie_id,
@PathVariable String item_id,
@RequestBody Map<String, Boolean> body) {
public Map<String, Object> updateMultiSpec(@PathVariable String cookie_id,
@PathVariable String item_id,
@RequestBody Map<String, Boolean> body) {
ItemInfo item = itemInfoRepository.findByCookieIdAndItemId(cookie_id, item_id)
.orElseThrow(() -> new RuntimeException("Item not found"));
Boolean enabled = body.get("enabled");
if (enabled != null) {
item.setIsMultiSpec(enabled);
@ -181,12 +188,12 @@ public class ItemController {
}
@PutMapping("/items/{cookie_id}/{item_id}/multi-quantity-delivery")
public Map<String, Object> updateMultiQuantityDelivery(@PathVariable String cookie_id,
@PathVariable String item_id,
@RequestBody Map<String, Boolean> body) {
public Map<String, Object> updateMultiQuantityDelivery(@PathVariable String cookie_id,
@PathVariable String item_id,
@RequestBody Map<String, Boolean> body) {
ItemInfo item = itemInfoRepository.findByCookieIdAndItemId(cookie_id, item_id)
.orElseThrow(() -> new RuntimeException("Item not found"));
Boolean enabled = body.get("enabled");
if (enabled != null) {
item.setMultiQuantityDelivery(enabled);
@ -197,18 +204,52 @@ public class ItemController {
// ------------------------- Sync (Stub/Trigger) -------------------------
/**
* 从账号获取所有商品真实实现
* 对应Python: @app.post("/items/get-all-from-account")
*/
@PostMapping("/items/get-all-from-account")
public Map<String, Object> getAllFromAccount(@RequestBody Map<String, String> body) {
// In Python this triggers a background crawler task.
// We will log this action and return success.
// Real implementation requires bridging to the crawler service (Python/Node/Java).
String cookieId = body.get("cookie_id");
log.info("Triggering item sync for cookie: {}", cookieId);
// Logic to clear existing items logic if needed or it's an upsert process
// For now, assuming external crawler pushes data to DB
return Map.of("success", true, "message", "Sync task started (Backend Received)");
if (cookieId == null || cookieId.isEmpty()) {
return Map.of("success", false, "message", "缺少cookie_id参数");
}
log.info("触发商品同步任务cookieId: {}", cookieId);
try {
// 从全局实例字典获取XianyuClient实例
XianyuClient client = XianyuClient.getInstance(cookieId);
if (client == null) {
return Map.of("success", false, "message", "未找到该账号的活跃连接,请确保账号已启用");
}
// 调用getAllItems方法获取所有商品
Map<String, Object> result = client.getAllItems(20, null);
if (Boolean.TRUE.equals(result.get("success"))) {
int totalCount = (int) result.get("total_count");
int totalPages = (int) result.get("total_pages");
int savedCount = (int) result.get("total_saved");
return Map.of(
"success", true,
"message", String.format("成功获取商品,共 %d 件,保存 %d 件", totalCount, savedCount),
"total_count", totalCount,
"total_pages", totalPages,
"saved_count", savedCount
);
} else {
String error = (String) result.getOrDefault("error", "未知错误");
return Map.of("success", false, "message", "获取商品失败: " + error);
}
} catch (Exception e) {
log.error("获取账号商品信息异常: {}", e.getMessage(), e);
return Map.of("success", false, "message", "获取商品信息异常: " + e.getMessage());
}
}
// ------------------------- DTOs -------------------------
@Data

View File

@ -35,7 +35,7 @@ import java.util.stream.Collectors;
@RestController
@RequestMapping
public class KeywordController {
public class KeywordController extends BaseController {
private final KeywordRepository keywordRepository;
private final DefaultReplyRepository defaultReplyRepository;
@ -43,7 +43,6 @@ public class KeywordController {
private final AiReplySettingRepository aiReplySettingRepository;
private final CookieRepository cookieRepository;
private final AiReplyService aiReplyService;
private final TokenService tokenService;
@Autowired
public KeywordController(KeywordRepository keywordRepository,
@ -53,13 +52,13 @@ public class KeywordController {
CookieRepository cookieRepository,
AiReplyService aiReplyService,
TokenService tokenService) {
super(tokenService);
this.keywordRepository = keywordRepository;
this.defaultReplyRepository = defaultReplyRepository;
this.defaultReplyRecordRepository = defaultReplyRecordRepository;
this.aiReplySettingRepository = aiReplySettingRepository;
this.cookieRepository = cookieRepository;
this.aiReplyService = aiReplyService;
this.tokenService = tokenService;
}
// ------------------------- Keywords -------------------------

View File

@ -1,5 +1,7 @@
package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.repository.SystemSettingRepository;
import com.xianyu.autoreply.service.TokenService;
import com.xianyu.autoreply.service.XianyuClient;
import com.xianyu.autoreply.service.XianyuClientService;
import lombok.Data;
@ -11,17 +13,19 @@ import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/send-message")
public class MessageController {
public class MessageController extends BaseController {
private final XianyuClientService xianyuClientService;
private final com.xianyu.autoreply.repository.SystemSettingRepository systemSettingRepository;
private final SystemSettingRepository systemSettingRepository;
// Default key matching Python's API_SECRET_KEY
private static final String DEFAULT_API_KEY = "xianyu_api_secret_2024";
@Autowired
public MessageController(XianyuClientService xianyuClientService,
com.xianyu.autoreply.repository.SystemSettingRepository systemSettingRepository) {
SystemSettingRepository systemSettingRepository,
TokenService tokenService) {
super(tokenService);
this.xianyuClientService = xianyuClientService;
this.systemSettingRepository = systemSettingRepository;
}

View File

@ -8,6 +8,7 @@ import com.xianyu.autoreply.entity.Cookie;
import com.xianyu.autoreply.repository.CookieRepository;
import com.xianyu.autoreply.repository.MessageNotificationRepository;
import com.xianyu.autoreply.repository.NotificationChannelRepository;
import com.xianyu.autoreply.service.TokenService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
@ -22,7 +23,7 @@ import java.util.stream.Collectors;
@RestController
@RequestMapping
public class NotificationController {
public class NotificationController extends BaseController {
private final NotificationChannelRepository channelRepository;
private final MessageNotificationRepository notificationRepository;
@ -31,7 +32,9 @@ public class NotificationController {
@Autowired
public NotificationController(NotificationChannelRepository channelRepository,
MessageNotificationRepository notificationRepository,
CookieRepository cookieRepository) {
CookieRepository cookieRepository,
TokenService tokenService) {
super(tokenService);
this.channelRepository = channelRepository;
this.notificationRepository = notificationRepository;
this.cookieRepository = cookieRepository;

View File

@ -4,6 +4,7 @@ import com.xianyu.autoreply.entity.Cookie;
import com.xianyu.autoreply.entity.Order;
import com.xianyu.autoreply.repository.CookieRepository;
import com.xianyu.autoreply.repository.OrderRepository;
import com.xianyu.autoreply.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@ -16,13 +17,16 @@ import java.util.stream.Collectors;
@Slf4j
@RestController
@RequestMapping("/api/orders")
public class OrderController {
public class OrderController extends BaseController {
private final OrderRepository orderRepository;
private final CookieRepository cookieRepository;
@Autowired
public OrderController(OrderRepository orderRepository, CookieRepository cookieRepository) {
public OrderController(OrderRepository orderRepository,
CookieRepository cookieRepository,
TokenService tokenService) {
super(tokenService);
this.orderRepository = orderRepository;
this.cookieRepository = cookieRepository;
}
@ -33,11 +37,11 @@ public class OrderController {
// For simple migration assuming "admin" or checking cookies.
// Python logic iterates user cookies and fetches orders.
// Here we mock "current user" context by fetching all cookies (User 1 assumption again)
List<String> cookieIds = cookieRepository.findAll().stream()
.map(Cookie::getId)
.collect(Collectors.toList());
List<Order> result = new ArrayList<>();
for (String cid : cookieIds) {
result.addAll(orderRepository.findByCookieId(cid));
@ -50,14 +54,14 @@ public class OrderController {
// Python checks ownership. We will just check existence first.
Order order = orderRepository.findById(orderId)
.orElseThrow(() -> new RuntimeException("订单不存在"));
return Map.of("success", true, "data", order);
}
@DeleteMapping("/{orderId}")
public Map<String, Object> deleteOrder(@PathVariable String orderId) {
if (!orderRepository.existsById(orderId)) {
throw new RuntimeException("订单不存在");
throw new RuntimeException("订单不存在");
}
orderRepository.deleteById(orderId);
return Map.of("success", true, "message", "删除成功");

View File

@ -1,6 +1,7 @@
package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.service.BrowserService;
import com.xianyu.autoreply.service.TokenService;
import lombok.Data;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@ -9,15 +10,15 @@ import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@RestController
public class PasswordLoginController {
public class PasswordLoginController extends BaseController {
private final BrowserService browserService;
private final com.xianyu.autoreply.service.TokenService tokenService;
@Autowired
public PasswordLoginController(BrowserService browserService, com.xianyu.autoreply.service.TokenService tokenService) {
public PasswordLoginController(BrowserService browserService,
TokenService tokenService) {
super(tokenService);
this.browserService = browserService;
this.tokenService = tokenService;
}
@PostMapping("/password-login")

View File

@ -1,6 +1,7 @@
package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.service.QrLoginService;
import com.xianyu.autoreply.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@ -10,12 +11,14 @@ import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class QrLoginController {
public class QrLoginController extends BaseController {
private final QrLoginService qrLoginService;
@Autowired
public QrLoginController(QrLoginService qrLoginService) {
public QrLoginController(QrLoginService qrLoginService,
TokenService tokenService) {
super(tokenService);
this.qrLoginService = qrLoginService;
}

View File

@ -2,6 +2,7 @@ package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.entity.UserStats;
import com.xianyu.autoreply.repository.UserStatsRepository;
import com.xianyu.autoreply.service.TokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@ -15,12 +16,14 @@ import java.util.stream.Collectors;
@Slf4j
@RestController
// Removing class level @RequestMapping to support root paths /statistics
public class StatsController {
public class StatsController extends BaseController {
private final UserStatsRepository userStatsRepository;
@Autowired
public StatsController(UserStatsRepository userStatsRepository) {
public StatsController(UserStatsRepository userStatsRepository,
TokenService tokenService) {
super(tokenService);
this.userStatsRepository = userStatsRepository;
}
@ -28,20 +31,20 @@ public class StatsController {
public Map<String, Object> receiveUserStats(@RequestBody UserStatsDto data) {
try {
if (data.anonymous_id == null) {
return Map.of("status", "error", "message", "Missing anonymous_id");
return Map.of("status", "error", "message", "Missing anonymous_id");
}
String os = "unknown";
String version = "2.2.0";
if (data.info != null) {
os = (String) data.info.getOrDefault("os", "unknown");
version = (String) data.info.getOrDefault("version", "2.2.0");
}
UserStats stats = userStatsRepository.findByAnonymousId(data.anonymous_id)
.orElse(new UserStats());
if (stats.getId() == null) {
stats.setAnonymousId(data.anonymous_id);
stats.setFirstSeen(LocalDateTime.now());
@ -53,12 +56,12 @@ public class StatsController {
stats.setOs(os);
stats.setVersion(version);
stats.setInfo(data.info);
userStatsRepository.save(stats);
log.info("Received user stats: {}", data.anonymous_id);
return Map.of("status", "success", "message", "User stats received");
} catch (Exception e) {
log.error("Error saving stats", e);
return Map.of("status", "error", "message", "Error saving stats");
@ -71,36 +74,36 @@ public class StatsController {
long totalUsers = userStatsRepository.count();
long dailyActive = userStatsRepository.countActiveUsersSince(LocalDateTime.now().minusDays(1));
long weeklyActive = userStatsRepository.countActiveUsersSince(LocalDateTime.now().minusDays(7));
List<UserStats> all = userStatsRepository.findAll();
Map<String, Long> osDistribution = all.stream()
.collect(Collectors.groupingBy(u -> u.getOs() == null ? "unknown" : u.getOs(), Collectors.counting()));
Map<String, Long> versionDistribution = all.stream()
.collect(Collectors.groupingBy(u -> u.getVersion() == null ? "unknown" : u.getVersion(), Collectors.counting()));
return Map.of(
"total_users", totalUsers,
"daily_active_users", dailyActive,
"weekly_active_users", weeklyActive,
"os_distribution", osDistribution,
"version_distribution", versionDistribution,
"last_updated", LocalDateTime.now().toString()
"total_users", totalUsers,
"daily_active_users", dailyActive,
"weekly_active_users", weeklyActive,
"os_distribution", osDistribution,
"version_distribution", versionDistribution,
"last_updated", LocalDateTime.now().toString()
);
} catch (Exception e) {
return Map.of("error", e.getMessage());
return Map.of("error", e.getMessage());
}
}
@GetMapping("/stats/recent")
public Map<String, Object> getRecentUsers() {
List<UserStats> recent = userStatsRepository.findTop20ByOrderByLastSeenDesc();
List<Map<String, Object>> mapped = recent.stream().map(u -> {
String maskedId = u.getAnonymousId();
if (maskedId.length() > 8) maskedId = maskedId.substring(0, 8) + "****";
// Need to return specific keys
Map<String, Object> m = new HashMap<>();
m.put("anonymous_id", maskedId);
@ -111,10 +114,10 @@ public class StatsController {
m.put("total_reports", u.getTotalReports());
return m;
}).collect(Collectors.toList());
return Map.of("recent_users", mapped);
}
// DTO class
public static class UserStatsDto {
public String anonymous_id;

View File

@ -2,6 +2,7 @@ package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.entity.SystemSetting;
import com.xianyu.autoreply.repository.SystemSettingRepository;
import com.xianyu.autoreply.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@ -16,12 +17,14 @@ import java.util.Map;
@RestController
@RequestMapping("/api/system")
public class SystemController {
public class SystemController extends BaseController {
private final SystemSettingRepository systemSettingRepository;
@Autowired
public SystemController(SystemSettingRepository systemSettingRepository) {
public SystemController(SystemSettingRepository systemSettingRepository,
TokenService tokenService) {
super(tokenService);
this.systemSettingRepository = systemSettingRepository;
}
@ -41,10 +44,10 @@ public class SystemController {
// In real app, fetch from DB. For now, mock or fetch if safe.
// Python code filters keys: registration_enabled, show_default_login_info, login_captcha_enabled
// We can reuse getSettings() and filter.
List<SystemSetting> all = systemSettingRepository.findAll();
Map<String, String> publicSettings = new java.util.HashMap<>();
// Defaults
publicSettings.put("registration_enabled", "true");
publicSettings.put("show_default_login_info", "true");
@ -52,9 +55,9 @@ public class SystemController {
for (SystemSetting setting : all) {
String key = setting.getKey();
if ("registration_enabled".equals(key) ||
"show_default_login_info".equals(key) ||
"login_captcha_enabled".equals(key)) {
if ("registration_enabled".equals(key) ||
"show_default_login_info".equals(key) ||
"login_captcha_enabled".equals(key)) {
publicSettings.put(key, setting.getValue());
}
}
@ -66,7 +69,7 @@ public class SystemController {
public Map<String, Object> checkVersion() {
// In Python this calls an external URL.
// We return a dummy response or implement the HTTP call using Hutool if needed.
return Collections.singletonMap("version", "1.0.0");
return Collections.singletonMap("version", "1.0.0");
}
@GetMapping("/version/changelog")

View File

@ -2,6 +2,7 @@ package com.xianyu.autoreply.controller;
import com.xianyu.autoreply.entity.SystemSetting;
import com.xianyu.autoreply.repository.SystemSettingRepository;
import com.xianyu.autoreply.service.TokenService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@ -19,12 +20,14 @@ import java.util.stream.Collectors;
*/
@RestController
@RequestMapping("/system-settings")
public class SystemSettingController {
public class SystemSettingController extends BaseController {
private final SystemSettingRepository systemSettingRepository;
@Autowired
public SystemSettingController(SystemSettingRepository systemSettingRepository) {
public SystemSettingController(SystemSettingRepository systemSettingRepository,
TokenService tokenService) {
super(tokenService);
this.systemSettingRepository = systemSettingRepository;
}

View File

@ -6,9 +6,11 @@ import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.xianyu.autoreply.entity.Cookie;
import com.xianyu.autoreply.entity.ItemInfo;
import com.xianyu.autoreply.model.ItemDetailCache;
import com.xianyu.autoreply.model.LockHoldInfo;
import com.xianyu.autoreply.repository.CookieRepository;
import com.xianyu.autoreply.repository.ItemInfoRepository;
import com.xianyu.autoreply.service.captcha.CaptchaHandler;
import com.xianyu.autoreply.utils.XianyuUtils;
import jakarta.websocket.ContainerProvider;
@ -97,6 +99,7 @@ public class XianyuClient extends TextWebSocketHandler {
private final BrowserService browserService;
private final PauseManager pauseManager; // 暂停管理器
private final OrderStatusHandler orderStatusHandler; // 订单状态处理器
private final ItemInfoRepository itemInfoRepository; // 商品信息存储库
private String cookiesStr; // Cookie字符串
private Map<String, String> cookies; // Cookie字典
@ -217,7 +220,8 @@ public class XianyuClient extends TextWebSocketHandler {
public XianyuClient(String cookieId, CookieRepository cookieRepository,
ReplyService replyService, CaptchaHandler captchaHandler,
BrowserService browserService, PauseManager pauseManager,
OrderStatusHandler orderStatusHandler) {
OrderStatusHandler orderStatusHandler,
ItemInfoRepository itemInfoRepository) {
this.cookieId = cookieId;
this.cookieRepository = cookieRepository;
this.replyService = replyService;
@ -225,6 +229,7 @@ public class XianyuClient extends TextWebSocketHandler {
this.browserService = browserService;
this.pauseManager = pauseManager;
this.orderStatusHandler = orderStatusHandler;
this.itemInfoRepository = itemInfoRepository;
// 创建HTTP客户端
this.httpClient = new OkHttpClient.Builder()
@ -3496,9 +3501,326 @@ public class XianyuClient extends TextWebSocketHandler {
return false;
}
}
// ============== 商品信息获取相关方法对应Python XianyuLive.get_all_items系列方法==============
/**
* 获取所有商品信息自动分页
* 对应Python: async def get_all_items(self, page_size=20, max_pages=None)
*
* @param pageSize 每页数量默认20
* @param maxPages 最大页数限制null表示无限制
* @return 包含所有商品信息的Map
*/
public Map<String, Object> getAllItems(int pageSize, Integer maxPages) {
log.info("【{}】开始获取所有商品信息,每页{}条", cookieId, pageSize);
int pageNumber = 1;
int totalSaved = 0;
int totalCount = 0;
while (true) {
if (maxPages != null && pageNumber > maxPages) {
log.info("【{}】达到最大页数限制 {},停止获取", cookieId, maxPages);
break;
}
log.info("【{}】正在获取第 {} 页...", cookieId, pageNumber);
Map<String, Object> result = getItemListInfo(pageNumber, pageSize, 0);
if (!Boolean.TRUE.equals(result.get("success"))) {
log.error("【{}】获取第 {} 页失败: {}", cookieId, pageNumber, result.get("error"));
return Map.of(
"success", false,
"error", result.getOrDefault("error", "获取商品失败"),
"total_pages", pageNumber - 1,
"total_count", totalCount,
"total_saved", totalSaved
);
}
@SuppressWarnings("unchecked")
java.util.List<Map<String, Object>> currentItems = (java.util.List<Map<String, Object>>) result.get("items");
if (currentItems == null || currentItems.isEmpty()) {
log.info("【{}】第 {} 页没有数据,获取完成", cookieId, pageNumber);
break;
}
totalCount += currentItems.size();
Integer savedCount = (Integer) result.get("saved_count");
if (savedCount != null) {
totalSaved += savedCount;
}
log.info("【{}】第 {} 页获取到 {} 个商品", cookieId, pageNumber, currentItems.size());
// 如果当前页商品数量少于页面大小说明已经是最后一页
if (currentItems.size() < pageSize) {
log.info("【{}】第 {} 页商品数量({})少于页面大小({}),获取完成",
cookieId, pageNumber, currentItems.size(), pageSize);
break;
}
pageNumber++;
// 添加延迟避免请求过快
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("【{}】获取商品时被中断", cookieId);
break;
}
}
log.info("【{}】所有商品获取完成,共 {} 个商品,保存了 {} 个", cookieId, totalCount, totalSaved);
return Map.of(
"success", true,
"total_pages", pageNumber,
"total_count", totalCount,
"total_saved", totalSaved
);
}
/**
* 获取商品列表信息单页
* 对应Python: async def get_item_list_info(self, page_number=1, page_size=20, retry_count=0)
*
* @param pageNumber 页码从1开始
* @param pageSize 每页数量
* @param retryCount 重试次数内部使用
* @return 包含商品列表的Map
*/
private Map<String, Object> getItemListInfo(int pageNumber, int pageSize, int retryCount) {
if (retryCount >= 4) {
log.error("【{}】获取商品信息失败,重试次数过多", cookieId);
return Map.of("error", "获取商品信息失败,重试次数过多");
}
try {
// 构建请求参数
long timestamp = System.currentTimeMillis();
Map<String, String> params = new HashMap<>();
params.put("jsv", "2.7.2");
params.put("appKey", API_APP_KEY);
params.put("t", String.valueOf(timestamp));
params.put("sign", "");
params.put("v", "1.0");
params.put("type", "originaljson");
params.put("accountSite", "xianyu");
params.put("dataType", "json");
params.put("timeout", "20000");
params.put("api", "mtop.idle.web.xyh.item.list");
params.put("sessionOption", "AutoLoginOnly");
params.put("spm_cnt", "a21ybx.im.0.0");
params.put("spm_pre", "a21ybx.collection.menu.1.272b5141NafCNK");
// 构建数据
Map<String, Object> dataMap = new HashMap<>();
dataMap.put("needGroupInfo", false);
dataMap.put("pageNumber", pageNumber);
dataMap.put("pageSize", pageSize);
dataMap.put("groupName", "在售");
dataMap.put("groupId", "58877261");
dataMap.put("defaultGroup", true);
dataMap.put("userId", myId);
String dataVal = JSON.toJSONString(dataMap);
// 从cookie中获取token
String mh5tk = cookies.get("_m_h5_tk");
String token = "";
if (mh5tk != null && mh5tk.contains("_")) {
token = mh5tk.split("_")[0];
}
log.warn("【{}】准备获取商品列表token: {}", cookieId, token);
// 生成签名
String sign = XianyuUtils.generateSign(String.valueOf(timestamp), token, dataVal);
params.put("sign", sign);
// 发送HTTP请求
String url = "https://h5api.m.goofish.com/h5/mtop.idle.web.xyh.item.list/1.0/";
cn.hutool.http.HttpRequest request = HttpRequest.post(url)
.form("data", dataVal)
.cookie(cookiesStr);
// 添加所有params参数
for (Map.Entry<String, String> entry : params.entrySet()) {
request.form(entry.getKey(), entry.getValue());
}
String responseBody = request.execute().body();
JSONObject resJson = JSON.parseObject(responseBody);
log.info("【{}】商品信息获取响应: {}", cookieId, resJson.toJSONString());
// 检查响应是否成功
JSONArray retArray = resJson.getJSONArray("ret");
if (retArray != null && !retArray.isEmpty() && "SUCCESS::调用成功".equals(retArray.getString(0))) {
JSONObject itemsData = resJson.getJSONObject("data");
JSONArray cardList = itemsData.getJSONArray("cardList");
// 解析商品信息
java.util.List<Map<String, Object>> itemsList = new java.util.ArrayList<>();
if (cardList != null) {
for (int i = 0; i < cardList.size(); i++) {
JSONObject card = cardList.getJSONObject(i);
JSONObject cardData = card.getJSONObject("cardData");
if (cardData != null) {
Map<String, Object> itemInfo = new HashMap<>();
itemInfo.put("id", cardData.getString("id"));
itemInfo.put("title", cardData.getString("title"));
JSONObject priceInfo = cardData.getJSONObject("priceInfo");
if (priceInfo != null) {
itemInfo.put("price", priceInfo.getString("price"));
String priceText = (priceInfo.getString("preText") != null ? priceInfo.getString("preText") : "") +
(priceInfo.getString("price") != null ? priceInfo.getString("price") : "");
itemInfo.put("price_text", priceText);
} else {
itemInfo.put("price", "");
itemInfo.put("price_text", "");
}
itemInfo.put("category_id", cardData.getString("categoryId"));
itemInfo.put("auction_type", cardData.getString("auctionType"));
itemInfo.put("item_status", cardData.getInteger("itemStatus"));
itemInfo.put("detail_url", cardData.getString("detailUrl"));
itemInfo.put("pic_info", cardData.getJSONObject("picInfo"));
itemInfo.put("detail_params", cardData.getJSONObject("detailParams"));
itemInfo.put("track_params", cardData.getJSONObject("trackParams"));
itemInfo.put("item_label_data", cardData.getJSONObject("itemLabelDataVO"));
itemInfo.put("card_type", card.getInteger("cardType"));
itemsList.add(itemInfo);
}
}
}
log.info("【{}】成功获取到 {} 个商品", cookieId, itemsList.size());
// 打印商品详细信息到控制台
System.out.println("\n" + "=".repeat(80));
System.out.println(String.format("📦 账号 %s 的商品列表 (第%d页%d 个商品)", myId, pageNumber, itemsList.size()));
System.out.println("=".repeat(80));
for (int i = 0; i < itemsList.size(); i++) {
Map<String, Object> item = itemsList.get(i);
System.out.println(String.format("\n🔸 商品 %d:", i + 1));
System.out.println(String.format(" 商品ID: %s", item.get("id")));
System.out.println(String.format(" 商品标题: %s", item.get("title")));
System.out.println(String.format(" 价格: %s", item.get("price_text")));
System.out.println(String.format(" 分类ID: %s", item.get("category_id")));
System.out.println(String.format(" 商品状态: %s", item.get("item_status")));
System.out.println(String.format(" 拍卖类型: %s", item.get("auction_type")));
System.out.println(String.format(" 详情链接: %s", item.get("detail_url")));
}
System.out.println("\n" + "=".repeat(80));
System.out.println("✅ 商品列表获取完成");
System.out.println("=".repeat(80));
// 自动保存商品信息到数据库
int savedCount = 0;
if (!itemsList.isEmpty()) {
savedCount = saveItemsToDatabase(itemsList);
log.info("【{}】已将 {} 个商品信息保存到数据库", cookieId, savedCount);
}
return Map.of(
"success", true,
"page_number", pageNumber,
"page_size", pageSize,
"current_count", itemsList.size(),
"items", itemsList,
"saved_count", savedCount
);
} else {
// 检查是否是token失效
String errorMsg = retArray != null && !retArray.isEmpty() ? retArray.getString(0) : "";
if (errorMsg.contains("FAIL_SYS_TOKEN_EXOIRED") || errorMsg.toLowerCase().contains("token")) {
log.warn("【{}】Token失效准备重试: {}", cookieId, errorMsg);
try {
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getItemListInfo(pageNumber, pageSize, retryCount + 1);
} else {
log.error("【{}】获取商品信息失败: {}", cookieId, resJson.toJSONString());
return Map.of("error", "获取商品信息失败: " + errorMsg);
}
}
} catch (Exception e) {
log.error("【{}】商品信息API请求异常", cookieId, e);
try {
Thread.sleep(500);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
}
return getItemListInfo(pageNumber, pageSize,retryCount + 1);
}
}
/**
* 保存商品列表到数据库
* 对应Python: async def save_items_list_to_db(self, items_list)
*
* @param itemsList 商品列表
* @return 保存的商品数量
*/
private int saveItemsToDatabase(java.util.List<Map<String, Object>> itemsList) {
int savedCount = 0;
try {
for (Map<String, Object> itemData : itemsList) {
try {
String itemId = (String) itemData.get("id");
if (itemId == null || itemId.isEmpty()) {
log.warn("【{}】跳过保存商品ID为空", cookieId);
continue;
}
// 查找或创建商品实体
ItemInfo itemInfo = itemInfoRepository.findByCookieIdAndItemId(cookieId, itemId)
.orElse(new ItemInfo());
// 设置字段
itemInfo.setCookieId(cookieId);
itemInfo.setItemId(itemId);
itemInfo.setItemTitle((String) itemData.get("title"));
itemInfo.setItemPrice((String) itemData.get("price"));
// 尝试从 detail_params 中提取分类信息
@SuppressWarnings("unchecked")
Map<String, Object> detailParams = (Map<String, Object>) itemData.get("detail_params");
if (detailParams != null) {
Object categoryName = detailParams.get("categoryName");
if (categoryName != null) {
itemInfo.setItemCategory(categoryName.toString());
}
}
// 保存到数据库
itemInfoRepository.save(itemInfo);
savedCount++;
} catch (Exception e) {
log.error("【{}】保存商品信息失败: {}", cookieId, itemData.get("id"), e);
}
}
log.info("【{}】成功保存 {} 个商品到数据库", cookieId, savedCount);
} catch (Exception e) {
log.error("【{}】批量保存商品信息时出错", cookieId, e);
}
return savedCount;
}
}

View File

@ -2,6 +2,7 @@ package com.xianyu.autoreply.service;
import com.xianyu.autoreply.entity.Cookie;
import com.xianyu.autoreply.repository.CookieRepository;
import com.xianyu.autoreply.repository.ItemInfoRepository;
import com.xianyu.autoreply.service.captcha.CaptchaHandler;
import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
@ -23,17 +24,19 @@ public class XianyuClientService {
private final PauseManager pauseManager;
private final OrderStatusHandler orderStatusHandler;
private final Map<String, XianyuClient> clients = new ConcurrentHashMap<>();
private final ItemInfoRepository itemInfoRepository;
@Autowired
public XianyuClientService(CookieRepository cookieRepository, ReplyService replyService,
public XianyuClientService(CookieRepository cookieRepository, ReplyService replyService,
CaptchaHandler captchaHandler, BrowserService browserService,
PauseManager pauseManager, OrderStatusHandler orderStatusHandler) {
PauseManager pauseManager, OrderStatusHandler orderStatusHandler, ItemInfoRepository itemInfoRepository) {
this.cookieRepository = cookieRepository;
this.replyService = replyService;
this.captchaHandler = captchaHandler;
this.browserService = browserService;
this.pauseManager = pauseManager;
this.orderStatusHandler = orderStatusHandler;
this.itemInfoRepository = itemInfoRepository;
}
@PostConstruct
@ -52,8 +55,9 @@ public class XianyuClientService {
log.warn("Client {} already running", cookieId);
return;
}
XianyuClient client = new XianyuClient(cookieId, cookieRepository, replyService,
captchaHandler, browserService, pauseManager, orderStatusHandler);
XianyuClient client = new XianyuClient(cookieId, cookieRepository, replyService,
captchaHandler, browserService, pauseManager, orderStatusHandler,
itemInfoRepository);
clients.put(cookieId, client);
client.start();
}
@ -64,7 +68,7 @@ public class XianyuClientService {
client.stop();
}
}
public XianyuClient getClient(String cookieId) {
return clients.get(cookieId);
}

View File

@ -1,17 +1,32 @@
import { get, post, put, del } from '@/utils/request'
import type { Account, AccountDetail, ApiResponse } from '@/types'
// 获取账号列表(返回账号ID数组)
// 获取账号列表(返回账号对象数组)
export const getAccounts = async (): Promise<Account[]> => {
const ids: string[] = await get('/cookies')
// 后端返回的是账号ID数组转换为Account对象数组
return ids.map(id => ({
id,
cookie: '',
enabled: true,
interface BackendAccount {
id: string
value: string
remark?: string
username?: string
password?: string
enabled: boolean
created_at?: string
updated_at?: string
user_id?: number
auto_confirm: boolean
pause_duration?: number
show_browser?: boolean
}
const data = await get<BackendAccount[]>('/cookies')
// 后端返回的是完整账号对象数组转换为前端Account格式
return data.map(item => ({
id: item.id,
cookie: item.value || '',
remark: item.remark,
enabled: item.enabled,
use_ai_reply: false,
use_default_reply: false,
auto_confirm: false
auto_confirm: item.auto_confirm
}))
}
@ -33,6 +48,7 @@ export const getAccountDetails = async (): Promise<AccountDetail[]> => {
return data.map((item) => ({
id: item.id,
cookie: item.value,
remark: item.remark,
enabled: item.enabled,
auto_confirm: item.auto_confirm,
note: item.remark,

View File

@ -637,6 +637,7 @@ export function Accounts() {
<thead>
<tr>
<th>ID</th>
<th></th>
<th></th>
<th></th>
<th>AI回复</th>
@ -658,6 +659,7 @@ export function Accounts() {
accounts.map((account) => (
<tr key={account.id}>
<td className="font-medium text-blue-600 dark:text-blue-400">{account.id}</td>
<td className="font-medium text-blue-600 dark:text-blue-400">{account.remark}</td>
<td>
<span className="inline-flex items-center gap-1.5 text-sm">
<MessageSquare className="w-3.5 h-3.5 text-blue-500" />

View File

@ -26,6 +26,7 @@ export interface LoginResponse {
export interface Account {
id: string
cookie: string
remark?: string
enabled: boolean
use_ai_reply: boolean
use_default_reply: boolean
@ -109,14 +110,14 @@ export interface Order {
updated_at?: string
}
export type OrderStatus =
| 'processing'
export type OrderStatus =
| 'processing'
| 'pending_ship'
| 'processed'
| 'shipped'
| 'completed'
| 'processed'
| 'shipped'
| 'completed'
| 'refunding'
| 'cancelled'
| 'cancelled'
| 'unknown'
// 卡券相关类型