This commit is contained in:
wangli 2026-01-23 00:13:39 +08:00
parent 17f557625b
commit 463b19a049
18 changed files with 1039 additions and 408 deletions

View File

@ -0,0 +1,33 @@
package top.biwin.xinayu.common.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 23:07
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AdminStatsResponse {
@JsonProperty("total_users")
private Long totalUsers;
@JsonProperty("total_cookies")
private Long totalCookies;
@JsonProperty("active_cookies")
private Long activeCookies;
@JsonProperty("total_cards")
private Long totalCards;
@JsonProperty("total_keywords")
private Long totalKeywords;
@JsonProperty("total_orders")
private Long totalOrders;
}

View File

@ -0,0 +1,20 @@
package top.biwin.xinayu.common.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.SuperBuilder;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 23:50
*/
@Data
@SuperBuilder
public class CheckDefaultPwdResponse extends BaseResponse {
@JsonProperty("using_default")
private Boolean usingDefault;
}

View File

@ -0,0 +1,57 @@
package top.biwin.xinayu.common.dto.response;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 23:26
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderResponse {
@JsonProperty("order_id")
private String orderId;
@JsonProperty("item_id")
private String itemId;
@JsonProperty("buyer_id")
private String buyerId;
@JsonProperty("spec_name")
private String specName;
@JsonProperty("spec_value")
private String specValue;
@JsonProperty("quantity")
private String quantity;
@JsonProperty("amount")
private String amount;
@JsonProperty("order_status")
private String orderStatus;
@JsonProperty("goofish_id")
private String goofish_id;
@JsonProperty("is_bargain")
private Integer isBargain = 0;
@JsonProperty("created_at")
private LocalDateTime createdAt;
@JsonProperty("updated_at")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,53 @@
package top.biwin.xianyu.core.entity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "ai_reply_settings")
public class AiReplySetting {
@Id
@Column(name = "goofish_id")
private Long goofishId;
@Column(name = "ai_enabled")
private Boolean aiEnabled = false;
@Column(name = "model_name")
private String modelName = "qwen-plus";
@Column(name = "api_key")
private String apiKey;
@Column(name = "base_url")
private String baseUrl = "https://dashscope.aliyuncs.com/compatible-mode/v1";
@Column(name = "max_discount_percent")
private Integer maxDiscountPercent = 10;
@Column(name = "max_discount_amount")
private Integer maxDiscountAmount = 100;
@Column(name = "max_bargain_rounds")
private Integer maxBargainRounds = 3;
@Column(name = "custom_prompts", columnDefinition = "TEXT")
private String customPrompts;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,64 @@
package top.biwin.xianyu.core.entity;
import jakarta.persistence.*;
import lombok.Data;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "cards")
public class Card {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String name;
@Column(nullable = false)
private String type; // api, text, data, image
@Column(name = "api_config", columnDefinition = "TEXT")
private String apiConfig;
@Column(name = "text_content", columnDefinition = "TEXT")
private String textContent;
@Column(name = "data_content", columnDefinition = "TEXT")
private String dataContent;
@Column(name = "image_url")
private String imageUrl;
@Column(columnDefinition = "TEXT")
private String description;
private Boolean enabled = true;
@Column(name = "delay_seconds")
private Integer delaySeconds = 0;
@Column(name = "is_multi_spec")
private Boolean isMultiSpec = false;
@Column(name = "spec_name")
private String specName;
@Column(name = "spec_value")
private String specValue;
@Column(name = "user_id", nullable = false)
private Long userId = 1L;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,30 @@
package top.biwin.xianyu.core.entity;
import jakarta.persistence.*;
import lombok.Data;
@Data
@Entity
@Table(name = "keywords")
public class Keyword {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // Synthetic ID, as original table didn't have one but needed for JPA
@Column(name = "cookie_id")
private String cookieId;
private String keyword;
private String reply;
@Column(name = "item_id")
private String itemId;
@Column(length = 20)
private String type = "text";
@Column(name = "image_url")
private String imageUrl;
}

View File

@ -0,0 +1,72 @@
package top.biwin.xianyu.core.entity;
import com.fasterxml.jackson.annotation.JsonProperty;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import lombok.Data;
import org.hibernate.annotations.ColumnDefault;
import org.hibernate.annotations.CreationTimestamp;
import org.hibernate.annotations.UpdateTimestamp;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "orders")
public class Order {
@Id
@Column(name = "order_id")
@JsonProperty("order_id")
private String orderId;
@Column(name = "item_id")
@JsonProperty("item_id")
private String itemId;
@Column(name = "buyer_id")
@JsonProperty("buyer_id")
private String buyerId;
@Column(name = "spec_name")
@JsonProperty("spec_name")
private String specName;
@Column(name = "spec_value")
@JsonProperty("spec_value")
private String specValue;
@Column(name = "quantity")
@JsonProperty("quantity")
private String quantity;
@Column(name = "amount")
@JsonProperty("amount")
private String amount;
@Column(name = "order_status")
@ColumnDefault("'unknown'")
@JsonProperty("order_status")
private String orderStatus;
@Column(name = "goofish_id")
@JsonProperty("goofish_id")
private Long goofishId;
@Column(name = "is_bargain")
@ColumnDefault("0")
@JsonProperty("is_bargain")
private Integer isBargain = 0;
@CreationTimestamp
@Column(name = "created_at", updatable = false)
@JsonProperty("created_at")
private LocalDateTime createdAt;
@UpdateTimestamp
@Column(name = "updated_at")
@JsonProperty("updated_at")
private LocalDateTime updatedAt;
}

View File

@ -0,0 +1,9 @@
package top.biwin.xianyu.core.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import top.biwin.xianyu.core.entity.AiReplySetting;
@Repository
public interface AiReplySettingRepository extends JpaRepository<AiReplySetting, Long> {
}

View File

@ -0,0 +1,12 @@
package top.biwin.xianyu.core.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import top.biwin.xianyu.core.entity.Card;
import java.util.List;
@Repository
public interface CardRepository extends JpaRepository<Card, Long> {
List<Card> findByUserId(Long userId);
}

View File

@ -10,4 +10,6 @@ import java.util.Optional;
public interface GoofishAccountRepository extends JpaRepository<GoofishAccount, Long> {
Optional<GoofishAccount> findByUserId(Long UserId);
long countByEnabled(Boolean enabled);
}

View File

@ -0,0 +1,35 @@
package top.biwin.xianyu.core.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import org.springframework.transaction.annotation.Transactional;
import top.biwin.xianyu.core.entity.Keyword;
import java.util.List;
@Repository
public interface KeywordRepository extends JpaRepository<Keyword, Long> {
List<Keyword> findByCookieId(String cookieId);
@Transactional
@Modifying
void deleteByCookieId(String cookieId);
@Transactional
@Modifying
@Query("DELETE FROM Keyword k WHERE k.cookieId = :cookieId AND (k.type IS NULL OR k.type = 'text')")
void deleteTextKeywordsByCookieId(@Param("cookieId") String cookieId);
// Conflict Check: Find image keywords that clash
// Python: SELECT type FROM keywords WHERE cookie_id = ? AND keyword = ? AND item_id = ? AND type = 'image'
@Query("SELECT k FROM Keyword k WHERE k.cookieId = :cookieId AND k.keyword = :keyword AND k.itemId = :itemId AND k.type = 'image'")
List<Keyword> findConflictImageKeywords(@Param("cookieId") String cookieId, @Param("keyword") String keyword, @Param("itemId") String itemId);
// Conflict Check for Generic (Null ItemId)
// Python: SELECT type FROM keywords WHERE cookie_id = ? AND keyword = ? AND (item_id IS NULL OR item_id = '') AND type = 'image'
@Query("SELECT k FROM Keyword k WHERE k.cookieId = :cookieId AND k.keyword = :keyword AND (k.itemId IS NULL OR k.itemId = '') AND k.type = 'image'")
List<Keyword> findConflictGenericImageKeywords(@Param("cookieId") String cookieId, @Param("keyword") String keyword);
}

View File

@ -0,0 +1,15 @@
package top.biwin.xianyu.core.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import top.biwin.xianyu.core.entity.Order;
import java.util.List;
@Repository
public interface OrderRepository extends JpaRepository<Order, String> {
List<Order> findByGoofishId(Long cookieId);
// For stats potentially
long count();
}

View File

@ -0,0 +1,76 @@
package top.biwin.xinayu.server.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.repository.*;
import top.biwin.xinayu.common.dto.response.AdminStatsResponse;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 22:58
*/
@Slf4j
@RestController
@RequestMapping("/admin")
public class AdminController {
@Autowired
private AdminUserRepository adminUserRepository;
@Autowired
private GoofishAccountRepository goofishAccountRepository;
@Autowired
private CardRepository cardRepository;
@Autowired
private KeywordRepository keywordRepository;
@Autowired
private OrderRepository orderRepository;
/**
* 获取系统统计信息管理员专用
* 对应 Python: @app.get('/admin/stats')
*/
@GetMapping("/stats")
public ResponseEntity<AdminStatsResponse> getStats(@RequestHeader(value = "Authorization", required = false) String token) {
log.info("查询系统统计信息");
// 1. 用户统计
long totalUsers = adminUserRepository.count();
// 2. Cookie 统计
long totalCookies = goofishAccountRepository.count();
// 3. 活跃账号统计启用状态的账号
long activeCookies = goofishAccountRepository.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);
}
return ResponseEntity.ok(AdminStatsResponse.builder()
.totalUsers(totalUsers)
.totalCookies(totalCookies)
.activeCookies(activeCookies)
.totalCards(totalCards)
.totalKeywords(totalKeywords)
.totalOrders(totalOrders)
.build());
}
}

View File

@ -5,15 +5,29 @@ import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.entity.AdminUser;
import top.biwin.xianyu.core.repository.AdminUserRepository;
import top.biwin.xinayu.common.dto.request.*;
import top.biwin.xinayu.common.dto.response.*;
import top.biwin.xinayu.common.dto.request.LoginCaptchaRequest;
import top.biwin.xinayu.common.dto.request.LoginRequest;
import top.biwin.xinayu.common.dto.request.RefreshRequest;
import top.biwin.xinayu.common.dto.request.SendCodeRequest;
import top.biwin.xinayu.common.dto.request.VerifyLoginCaptchaRequest;
import top.biwin.xinayu.common.dto.response.BaseResponse;
import top.biwin.xinayu.common.dto.response.CaptchaResponse;
import top.biwin.xinayu.common.dto.response.CheckDefaultPwdResponse;
import top.biwin.xinayu.common.dto.response.LoginResponse;
import top.biwin.xinayu.common.dto.response.RefreshResponse;
import top.biwin.xinayu.common.dto.response.SendCodeResponse;
import top.biwin.xinayu.server.security.JwtUtil;
import top.biwin.xinayu.server.service.AuthService;
import top.biwin.xinayu.server.service.CaptchaService;
import top.biwin.xinayu.server.service.EmailVerificationService;
import top.biwin.xinayu.server.util.CurrentUserUtil;
import java.util.HashMap;
import java.util.Map;
@ -33,234 +47,259 @@ import java.util.Optional;
* @since 2026-01-21
*/
@RestController
@RequestMapping("/auth")
@RequestMapping
public class AuthController {
private static final String ADMIN_USERNAME = "admin";
private static final String DEFAULT_ADMIN_PASSWORD = "123456";
@Autowired
private AuthService authService;
@Autowired
private AuthService authService;
@Autowired
private EmailVerificationService emailVerificationService;
@Autowired
private EmailVerificationService emailVerificationService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private AdminUserRepository adminUserRepository;
@Autowired
private AdminUserRepository adminUserRepository;
@Autowired
private CaptchaService captchaService;
@Autowired
private CaptchaService captchaService;
/**
* 用户登录接口
* <p>
* 请求示例
* POST /auth/login
* {
* "username": "admin",
* "password": "password123"
* }
* <p>
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
* <p>
* 错误响应
* - 401 Unauthorized: 用户名或密码错误
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 登录请求包含 username password
* @return ResponseEntity<LoginResponse> 登录响应
*/
@PostMapping("/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(response);
}
/**
* 刷新访问令牌接口
* <p>
* 请求示例
* POST /auth/refresh
* {
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* }
* <p>
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
* <p>
* 错误响应
* - 401 Unauthorized: 刷新令牌无效或已过期
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 刷新令牌请求包含 refreshToken
* @return ResponseEntity<RefreshResponse> 刷新令牌响应
*/
@PostMapping("/refresh")
public ResponseEntity<RefreshResponse> refresh(@RequestBody RefreshRequest request) {
RefreshResponse response = authService.refresh(request.getRefreshToken());
return ResponseEntity.ok(response);
}
/**
* 发送邮箱验证码接口
* <p>
*
* @param request 发送验证码请求包含 emailcaptchaIdcaptchaCode
* @return ResponseEntity<SendCodeResponse> 发送结果响应
*/
@PostMapping("/send-verification-code")
public ResponseEntity<SendCodeResponse> sendVerificationCode(@RequestBody SendCodeRequest request) {
Optional<AdminUser> optUser = adminUserRepository.findByEmail(request.getEmail());
SendCodeResponse response = SendCodeResponse.builder().build();
if (optUser.isEmpty()) {
response.setSuccess(false);
response.setMessage("该邮箱未注册账户");
} else {
// 验证通过发送邮箱验证码
emailVerificationService.generateAndSendCode(request.getEmail());
response.setSuccess(true);
response.setMessage("验证码已发送到您的邮箱,请注意查收");
response.setExpiresIn(300);
}
return ResponseEntity.ok(response);
}
/**
* 生成图形验证码接口
* <p>
* 生成4位字母数字混合验证码5分钟有效
* <p>
* 请求示例
* GET /auth/generate-captcha
* <p>
* 响应示例
* {
* "captchaId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
* "captchaImage": "data:image/png;base64,iVBORw0KGgo...",
* "expiresIn": 300
* }
*
* @return ResponseEntity<CaptchaResponse> 验证码响应
*/
@PostMapping("/generate-captcha")
public ResponseEntity<CaptchaResponse> generateCaptcha(@RequestBody LoginCaptchaRequest request) {
CaptchaResponse response = captchaService.generateCaptcha(request.getSessionId());
return ResponseEntity.ok(response);
}
/**
* 验证图形验证码
* 对应 Python: /verify-captcha
*/
@PostMapping("/verify-captcha")
public ResponseEntity<BaseResponse> verifyCaptcha(@RequestBody VerifyLoginCaptchaRequest request) {
// 先验证图形验证码
if (!captchaService.verifyCaptcha(request.getSessionId(), request.getCaptchaCode())) {
throw new BadCredentialsException("图形验证码错误或已过期");
/**
* 用户登录接口
* <p>
* 请求示例
* POST /auth/login
* {
* "username": "admin",
* "password": "password123"
* }
* <p>
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
* <p>
* 错误响应
* - 401 Unauthorized: 用户名或密码错误
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 登录请求包含 username password
* @return ResponseEntity<LoginResponse> 登录响应
*/
@PostMapping("/auth/login")
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request) {
LoginResponse response = authService.login(request);
return ResponseEntity.ok(response);
}
return ResponseEntity.ok(new BaseResponse("图形验证码验证成功", true));
}
/**
* 验证 Token 接口
* 检查 Access Token 是否有效并返回用户信息
* <p>
* 请求示例
* GET /auth/verify
* Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
* <p>
* 响应示例Token 有效
* {
* "authenticated": true,
* "user_id": 1,
* "username": "admin",
* "email": "admin@example.com",
* "is_admin": true
* }
* <p>
* 响应示例Token 无效
* {
* "authenticated": false
* }
*
* @param request HTTP 请求对象
* @return 验证结果和用户信息
*/
@GetMapping("/verify")
public ResponseEntity<Map<String, Object>> verify(HttpServletRequest request) {
Map<String, Object> response = new HashMap<>();
// 从请求头中提取 Token
String token = getTokenFromRequest(request);
if (token == null) {
response.put("authenticated", false);
response.put("message", "缺少 Authorization 头");
return ResponseEntity.ok(response);
/**
* 刷新访问令牌接口
* <p>
* 请求示例
* POST /auth/refresh
* {
* "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
* }
* <p>
* 响应示例
* {
* "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
* "expiresIn": 900,
* "tokenType": "Bearer"
* }
* <p>
* 错误响应
* - 401 Unauthorized: 刷新令牌无效或已过期
* - 500 Internal Server Error: 服务器内部错误
*
* @param request 刷新令牌请求包含 refreshToken
* @return ResponseEntity<RefreshResponse> 刷新令牌响应
*/
@PostMapping("/auth/refresh")
public ResponseEntity<RefreshResponse> refresh(@RequestBody RefreshRequest request) {
RefreshResponse response = authService.refresh(request.getRefreshToken());
return ResponseEntity.ok(response);
}
try {
// 使用 JwtUtil 验证 Token 是否有效检查签名和过期时间
if (jwtUtil.validateToken(token)) {
// Token 有效提取用户名
String username = jwtUtil.getUsernameFromToken(token);
/**
* 发送邮箱验证码接口
* <p>
*
* @param request 发送验证码请求包含 emailcaptchaIdcaptchaCode
* @return ResponseEntity<SendCodeResponse> 发送结果响应
*/
@PostMapping("/auth/send-verification-code")
public ResponseEntity<SendCodeResponse> sendVerificationCode(@RequestBody SendCodeRequest request) {
Optional<AdminUser> optUser = adminUserRepository.findByEmail(request.getEmail());
// 从数据库查询用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isPresent()) {
AdminUser user = userOpt.get();
// 返回用户信息
response.put("authenticated", true);
response.put("user_id", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("is_admin", true); // admin_user 表的用户都是管理员
response.put("is_active", user.getIsActive());
SendCodeResponse response = SendCodeResponse.builder().build();
if (optUser.isEmpty()) {
response.setSuccess(false);
response.setMessage("该邮箱未注册账户");
} else {
// Token 有效但用户不存在可能已被删除
response.put("authenticated", false);
response.put("message", "用户不存在");
// 验证通过发送邮箱验证码
emailVerificationService.generateAndSendCode(request.getEmail());
response.setSuccess(true);
response.setMessage("验证码已发送到您的邮箱,请注意查收");
response.setExpiresIn(300);
}
} else {
// Token 无效或已过期
response.put("authenticated", false);
response.put("message", "Token 无效或已过期");
}
} catch (Exception e) {
// Token 解析失败
response.put("authenticated", false);
response.put("message", "Token 格式错误");
return ResponseEntity.ok(response);
}
return ResponseEntity.ok(response);
}
/**
* 从请求头中提取 Token
*
* @param request HTTP 请求对象
* @return Token 字符串如果不存在则返回 null
*/
private String getTokenFromRequest(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StrUtil.isNotBlank(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
/**
* 生成图形验证码接口
* <p>
* 生成4位字母数字混合验证码5分钟有效
* <p>
* 请求示例
* GET /auth/generate-captcha
* <p>
* 响应示例
* {
* "captchaId": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
* "captchaImage": "data:image/png;base64,iVBORw0KGgo...",
* "expiresIn": 300
* }
*
* @return ResponseEntity<CaptchaResponse> 验证码响应
*/
@PostMapping("/auth/generate-captcha")
public ResponseEntity<CaptchaResponse> generateCaptcha(@RequestBody LoginCaptchaRequest request) {
CaptchaResponse response = captchaService.generateCaptcha(request.getSessionId());
return ResponseEntity.ok(response);
}
return null;
}
/**
* 验证图形验证码
* 对应 Python: /verify-captcha
*/
@PostMapping("/auth/verify-captcha")
public ResponseEntity<BaseResponse> verifyCaptcha(@RequestBody VerifyLoginCaptchaRequest request) {
// 先验证图形验证码
if (!captchaService.verifyCaptcha(request.getSessionId(), request.getCaptchaCode())) {
throw new BadCredentialsException("图形验证码错误或已过期");
}
return ResponseEntity.ok(new BaseResponse("图形验证码验证成功", true));
}
/**
* 验证 Token 接口
* 检查 Access Token 是否有效并返回用户信息
* <p>
* 请求示例
* GET /auth/verify
* Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
* <p>
* 响应示例Token 有效
* {
* "authenticated": true,
* "user_id": 1,
* "username": "admin",
* "email": "admin@example.com",
* "is_admin": true
* }
* <p>
* 响应示例Token 无效
* {
* "authenticated": false
* }
*
* @param request HTTP 请求对象
* @return 验证结果和用户信息
*/
@GetMapping("/auth/verify")
public ResponseEntity<Map<String, Object>> verify(HttpServletRequest request) {
Map<String, Object> response = new HashMap<>();
// 从请求头中提取 Token
String token = getTokenFromRequest(request);
if (token == null) {
response.put("authenticated", false);
response.put("message", "缺少 Authorization 头");
return ResponseEntity.ok(response);
}
try {
// 使用 JwtUtil 验证 Token 是否有效检查签名和过期时间
if (jwtUtil.validateToken(token)) {
// Token 有效提取用户名
String username = jwtUtil.getUsernameFromToken(token);
// 从数据库查询用户信息
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
if (userOpt.isPresent()) {
AdminUser user = userOpt.get();
// 返回用户信息
response.put("authenticated", true);
response.put("user_id", user.getId());
response.put("username", user.getUsername());
response.put("email", user.getEmail());
response.put("is_admin", true); // admin_user 表的用户都是管理员
response.put("is_active", user.getIsActive());
} else {
// Token 有效但用户不存在可能已被删除
response.put("authenticated", false);
response.put("message", "用户不存在");
}
} else {
// Token 无效或已过期
response.put("authenticated", false);
response.put("message", "Token 无效或已过期");
}
} catch (Exception e) {
// Token 解析失败
response.put("authenticated", false);
response.put("message", "Token 格式错误");
}
return ResponseEntity.ok(response);
}
/**
* 从请求头中提取 Token
*
* @param request HTTP 请求对象
* @return Token 字符串如果不存在则返回 null
*/
private String getTokenFromRequest(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (StrUtil.isNotBlank(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
/**
* 检查是否使用默认密码
* 对应 Python: /api/check-default-password
*/
@GetMapping("/api/check-default-password")
public ResponseEntity<CheckDefaultPwdResponse> checkDefaultPassword(HttpServletRequest httpRequest) {
CheckDefaultPwdResponse build = CheckDefaultPwdResponse.builder()
.usingDefault(false)
.success(true)
.build();
if(!CurrentUserUtil.isSuperAdmin()) {
return ResponseEntity.ok(build);
}
AdminUser adminUser = authService.verifyUserPassword(ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD);
build.setUsingDefault( adminUser != null);
return ResponseEntity.ok(build);
}
}

View File

@ -0,0 +1,15 @@
package top.biwin.xinayu.server.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 23:03
*/
@RestController
@RequestMapping("/card")
public class CardController {
}

View File

@ -0,0 +1,43 @@
package top.biwin.xinayu.server.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.entity.AiReplySetting;
import top.biwin.xianyu.core.entity.GoofishAccount;
import top.biwin.xianyu.core.repository.AiReplySettingRepository;
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 22:57
*/
@RestController
@RequestMapping()
public class KeywordController {
@Autowired
private GoofishAccountRepository goofishAccountRepository;
@Autowired
private AiReplySettingRepository aiReplySettingRepository;
@GetMapping("/ai-reply-settings")
public ResponseEntity<Map<Long, AiReplySetting>> getAllAiSettings() {
List<Long> goofishIds = goofishAccountRepository.findAll().stream()
.map(GoofishAccount::getId)
.toList();
List<AiReplySetting> settings = aiReplySettingRepository.findAllById(goofishIds);
return ResponseEntity.ok(settings.stream().collect(Collectors.toMap(AiReplySetting::getGoofishId, s -> s)));
}
}

View File

@ -0,0 +1,46 @@
package top.biwin.xinayu.server.controller;
import cn.hutool.core.bean.BeanUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import top.biwin.xianyu.core.entity.GoofishAccount;
import top.biwin.xianyu.core.entity.Order;
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
import top.biwin.xianyu.core.repository.OrderRepository;
import top.biwin.xinayu.common.dto.response.OrderResponse;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
/**
* TODO
*
* @author wangli
* @since 2026-01-22 22:56
*/
@RestController
@RequestMapping("/api/orders")
public class OrderController {
@Autowired
private GoofishAccountRepository goofishAccountRepository;
@Autowired
private OrderRepository orderRepository;
@GetMapping
public ResponseEntity<List<OrderResponse>> getAllOrder() {
List<Long> goofishIds = goofishAccountRepository.findAll().stream()
.map(GoofishAccount::getId)
.toList();
List<OrderResponse> result = new ArrayList<>();
goofishIds.forEach(i -> {
List<Order> order = orderRepository.findByGoofishId(i);
result.addAll(BeanUtil.copyToList(order, OrderResponse.class));
});
return ResponseEntity.ok(result);
}
}

View File

@ -34,221 +34,231 @@ import top.biwin.xinayu.server.security.JwtUtil;
@Service
public class AuthService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private AdminUserRepository adminUserRepository;
@Autowired
private AdminUserRepository adminUserRepository;
@Autowired
private EmailVerificationService emailVerificationService;
@Autowired
private EmailVerificationService emailVerificationService;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 用户登录支持多种方式
* <p>
* 登录方式自动判断
* - username + password -> 用户名密码登录
* - email + password -> 邮箱密码登录
* - email + verificationCode -> 邮箱验证码登录
*
* @param request 登录请求
* @return LoginResponse 登录响应包含 token 和用户信息
* @throws BadCredentialsException 认证失败时抛出
*/
public LoginResponse login(LoginRequest request) {
AdminUser user = null;
/**
* 用户登录支持多种方式
* <p>
* 登录方式自动判断
* - username + password -> 用户名密码登录
* - email + password -> 邮箱密码登录
* - email + verificationCode -> 邮箱验证码登录
*
* @param request 登录请求
* @return LoginResponse 登录响应包含 token 和用户信息
* @throws BadCredentialsException 认证失败时抛出
*/
public LoginResponse login(LoginRequest request) {
AdminUser user = null;
// TODO: 主人这里判断使用哪种登录方式
// 根据 request 中的字段判断登录类型然后调用对应的认证方法
// TODO: 主人这里判断使用哪种登录方式
// 根据 request 中的字段判断登录类型然后调用对应的认证方法
if (StringUtils.hasText(request.getUsername()) && StringUtils.hasText(request.getPassword())) {
// 方式1用户名 + 密码登录
user = authenticateByUsername(request.getUsername(), request.getPassword());
log.info("✅ 用户名登录成功: {}", request.getUsername());
if (StringUtils.hasText(request.getUsername()) && StringUtils.hasText(request.getPassword())) {
// 方式1用户名 + 密码登录
user = authenticateByUsername(request.getUsername(), request.getPassword());
log.info("✅ 用户名登录成功: {}", request.getUsername());
} else if (StringUtils.hasText(request.getEmail()) && StringUtils.hasText(request.getPassword())) {
// 方式2邮箱 + 密码登录
user = authenticateByEmail(request.getEmail(), request.getPassword());
log.info("✅ 邮箱密码登录成功: {}", request.getEmail());
} else if (StringUtils.hasText(request.getEmail()) && StringUtils.hasText(request.getPassword())) {
// 方式2邮箱 + 密码登录
user = authenticateByEmail(request.getEmail(), request.getPassword());
log.info("✅ 邮箱密码登录成功: {}", request.getEmail());
} else if (StringUtils.hasText(request.getEmail()) && StringUtils.hasText(request.getVerificationCode())) {
// 方式3邮箱 + 验证码登录
user = authenticateByEmailCode(request.getEmail(), request.getVerificationCode());
log.info("✅ 邮箱验证码登录成功: {}", request.getEmail());
} else if (StringUtils.hasText(request.getEmail()) && StringUtils.hasText(request.getVerificationCode())) {
// 方式3邮箱 + 验证码登录
user = authenticateByEmailCode(request.getEmail(), request.getVerificationCode());
log.info("✅ 邮箱验证码登录成功: {}", request.getEmail());
} else {
return LoginResponse.builder()
.success(false)
.message("无效的登录参数组合")
.build();
} else {
return LoginResponse.builder()
.success(false)
.message("无效的登录参数组合")
.build();
}
// TODO: 主人这里生成 JWT Token 并构建响应
// 1. 使用 user.getUsername() 生成 accessToken refreshToken
// 2. 调用 buildLoginResponse() 方法构建包含完整用户信息的响应
// 生成 JWT Token
String accessToken = jwtUtil.generateAccessToken(user.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(user.getUsername());
Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
// 构建响应包含完整用户信息
return buildLoginResponse(user, accessToken, refreshToken, expiresIn);
}
// TODO: 主人这里生成 JWT Token 并构建响应
// 1. 使用 user.getUsername() 生成 accessToken refreshToken
// 2. 调用 buildLoginResponse() 方法构建包含完整用户信息的响应
/**
* 用户名 + 密码认证
*
* @param username 用户名
* @param password 密码
* @return AdminUser 用户实体
*/
private AdminUser authenticateByUsername(String username, String password) {
// TODO: 主人这里使用 Spring Security AuthenticationManager 进行认证
// 步骤
// 1. 创建 UsernamePasswordAuthenticationToken
// 2. 调用 authenticationManager.authenticate(authToken)
// 3. 认证成功后从数据库查询用户信息并返回
// 生成 JWT Token
String accessToken = jwtUtil.generateAccessToken(user.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(user.getUsername());
Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authToken);
// 构建响应包含完整用户信息
return buildLoginResponse(user, accessToken, refreshToken, expiresIn);
}
/**
* 用户名 + 密码认证
*
* @param username 用户名
* @param password 密码
* @return AdminUser 用户实体
*/
private AdminUser authenticateByUsername(String username, String password) {
// TODO: 主人这里使用 Spring Security AuthenticationManager 进行认证
// 步骤
// 1. 创建 UsernamePasswordAuthenticationToken
// 2. 调用 authenticationManager.authenticate(authToken)
// 3. 认证成功后从数据库查询用户信息并返回
UsernamePasswordAuthenticationToken authToken =
new UsernamePasswordAuthenticationToken(username, password);
Authentication authentication = authenticationManager.authenticate(authToken);
// 认证成功查询用户完整信息
return adminUserRepository.findByUsername(username)
.orElseThrow(() -> new BadCredentialsException("用户不存在"));
}
/**
* 邮箱 + 密码认证
*
* @param email 邮箱
* @param password 密码
* @return AdminUser 用户实体
*/
private AdminUser authenticateByEmail(String email, String password) {
// TODO: 主人这里验证邮箱和密码
// 步骤
// 1. 通过邮箱查询用户adminUserRepository.findByEmail(email)
// 2. 如果用户不存在抛出 BadCredentialsException
// 3. 验证密码passwordEncoder.matches(password, user.getPasswordHash())
// 4. 如果密码错误抛出 BadCredentialsException
// 5. 检查账号是否激活
// 6. 返回用户实体
AdminUser user = adminUserRepository.findByEmail(email)
.orElseThrow(() -> new BadCredentialsException("邮箱或密码错误"));
// 验证密码
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new BadCredentialsException("邮箱或密码错误");
// 认证成功查询用户完整信息
return adminUserRepository.findByUsername(username)
.orElseThrow(() -> new BadCredentialsException("用户不存在"));
}
// 检查账号状态
if (!user.getIsActive()) {
throw new BadCredentialsException("账号已被禁用");
/**
* 邮箱 + 密码认证
*
* @param email 邮箱
* @param password 密码
* @return AdminUser 用户实体
*/
private AdminUser authenticateByEmail(String email, String password) {
// TODO: 主人这里验证邮箱和密码
// 步骤
// 1. 通过邮箱查询用户adminUserRepository.findByEmail(email)
// 2. 如果用户不存在抛出 BadCredentialsException
// 3. 验证密码passwordEncoder.matches(password, user.getPasswordHash())
// 4. 如果密码错误抛出 BadCredentialsException
// 5. 检查账号是否激活
// 6. 返回用户实体
AdminUser user = adminUserRepository.findByEmail(email)
.orElseThrow(() -> new BadCredentialsException("邮箱或密码错误"));
// 验证密码
if (!passwordEncoder.matches(password, user.getPasswordHash())) {
throw new BadCredentialsException("邮箱或密码错误");
}
// 检查账号状态
if (!user.getIsActive()) {
throw new BadCredentialsException("账号已被禁用");
}
return user;
}
return user;
}
/**
* 邮箱 + 验证码认证
*
* @param email 邮箱
* @param code 验证码
* @return AdminUser 用户实体
*/
private AdminUser authenticateByEmailCode(String email, String code) {
// TODO: 主人这里验证邮箱验证码
// 步骤
// 1. 调用 emailVerificationService.verifyCode(email, code) 验证验证码
// 2. 如果验证失败抛出 BadCredentialsException
// 3. 通过邮箱查询用户
// 4. 检查账号是否激活
// 5. 返回用户实体
/**
* 邮箱 + 验证码认证
*
* @param email 邮箱
* @param code 验证码
* @return AdminUser 用户实体
*/
private AdminUser authenticateByEmailCode(String email, String code) {
// TODO: 主人这里验证邮箱验证码
// 步骤
// 1. 调用 emailVerificationService.verifyCode(email, code) 验证验证码
// 2. 如果验证失败抛出 BadCredentialsException
// 3. 通过邮箱查询用户
// 4. 检查账号是否激活
// 5. 返回用户实体
// 验证验证码
if (!emailVerificationService.verifyCode(email, code)) {
throw new BadCredentialsException("验证码无效或已过期");
}
// 验证验证码
if (!emailVerificationService.verifyCode(email, code)) {
throw new BadCredentialsException("验证码无效或已过期");
// 查询用户
AdminUser user = adminUserRepository.findByEmail(email)
.orElseThrow(() -> new BadCredentialsException("该邮箱未注册"));
// 检查账号状态
if (!user.getIsActive()) {
throw new BadCredentialsException("账号已被禁用");
}
return user;
}
// 查询用户
AdminUser user = adminUserRepository.findByEmail(email)
.orElseThrow(() -> new BadCredentialsException("该邮箱未注册"));
/**
* 构建登录响应包含完整用户信息
*
* @param user 用户实体
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param expiresIn 过期时间
* @return LoginResponse
*/
private LoginResponse buildLoginResponse(AdminUser user, String accessToken,
String refreshToken, Long expiresIn) {
// TODO: 主人这里构建包含完整用户信息的响应
// user 实体的信息映射到 LoginResponse 的各个字段
// 检查账号状态
if (!user.getIsActive()) {
throw new BadCredentialsException("账号已被禁用");
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(expiresIn);
response.setTokenType("Bearer");
// 设置用户信息
response.setUserId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setIsActive(user.getIsActive());
response.setIsAdmin(true); // 所有 admin_user 表的用户都是管理员
// 设置角色信息
String roleName = user.getRole() != null ? user.getRole().name() : "ADMIN";
response.setRole(roleName);
response.setSuccess(true);
return response;
}
return user;
}
/**
* 构建登录响应包含完整用户信息
*
* @param user 用户实体
* @param accessToken 访问令牌
* @param refreshToken 刷新令牌
* @param expiresIn 过期时间
* @return LoginResponse
*/
private LoginResponse buildLoginResponse(AdminUser user, String accessToken,
String refreshToken, Long expiresIn) {
// TODO: 主人这里构建包含完整用户信息的响应
// user 实体的信息映射到 LoginResponse 的各个字段
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setExpiresIn(expiresIn);
response.setTokenType("Bearer");
// 设置用户信息
response.setUserId(user.getId());
response.setUsername(user.getUsername());
response.setEmail(user.getEmail());
response.setIsActive(user.getIsActive());
response.setIsAdmin(true); // 所有 admin_user 表的用户都是管理员
// 设置角色信息
String roleName = user.getRole() != null ? user.getRole().name() : "ADMIN";
response.setRole(roleName);
response.setSuccess(true);
return response;
}
/**
* 刷新访问令牌
* <p>
* 主人的自定义逻辑实现位置 #3
* <p>
* 💡 扩展提示
* - 可以实现 Refresh Token 轮换机制每次刷新都返回新的 refreshToken
* - 可以实现 Refresh Token 黑名单用户登出时将 refreshToken 加入黑名单
* - 可以限制单个 refreshToken 的使用次数
* - 可以检查用户是否仍然处于激活状态调用 AdminUserRepository 再次验证
*
* @param refreshToken 刷新令牌
* @return RefreshResponse 刷新响应包含新的 accessToken
* @throws BadCredentialsException 刷新令牌无效时抛出
*/
public RefreshResponse refresh(String refreshToken) {
if (!jwtUtil.validateToken(refreshToken)) {
throw new BadCredentialsException("刷新令牌无效或已过期");
/**
* 刷新访问令牌
* <p>
* 主人的自定义逻辑实现位置 #3
* <p>
* 💡 扩展提示
* - 可以实现 Refresh Token 轮换机制每次刷新都返回新的 refreshToken
* - 可以实现 Refresh Token 黑名单用户登出时将 refreshToken 加入黑名单
* - 可以限制单个 refreshToken 的使用次数
* - 可以检查用户是否仍然处于激活状态调用 AdminUserRepository 再次验证
*
* @param refreshToken 刷新令牌
* @return RefreshResponse 刷新响应包含新的 accessToken
* @throws BadCredentialsException 刷新令牌无效时抛出
*/
public RefreshResponse refresh(String refreshToken) {
if (!jwtUtil.validateToken(refreshToken)) {
throw new BadCredentialsException("刷新令牌无效或已过期");
}
String username = jwtUtil.getUsernameFromToken(refreshToken);
String newAccessToken = jwtUtil.generateAccessToken(username);
Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
return new RefreshResponse(newAccessToken, expiresIn, "Bearer");
}
public AdminUser verifyUserPassword(String username, String password) {
AdminUser user = null;
try {
user = authenticateByUsername(username, password);
} catch (Exception e) {
log.warn("未匹配上默认超管账号");
}
return user;
}
String username = jwtUtil.getUsernameFromToken(refreshToken);
String newAccessToken = jwtUtil.generateAccessToken(username);
Long expiresIn = jwtUtil.getAccessTokenExpirationInSeconds();
return new RefreshResponse(newAccessToken, expiresIn, "Bearer");
}
}