From 463b19a049f7b2ccdd9f58b2392d24d91e2e4277 Mon Sep 17 00:00:00 2001 From: wangli Date: Fri, 23 Jan 2026 00:13:39 +0800 Subject: [PATCH] init --- .../dto/response/AdminStatsResponse.java | 33 ++ .../dto/response/CheckDefaultPwdResponse.java | 20 + .../common/dto/response/OrderResponse.java | 57 +++ .../xianyu/core/entity/AiReplySetting.java | 53 ++ .../top/biwin/xianyu/core/entity/Card.java | 64 +++ .../top/biwin/xianyu/core/entity/Keyword.java | 30 ++ .../top/biwin/xianyu/core/entity/Order.java | 72 +++ .../repository/AiReplySettingRepository.java | 9 + .../core/repository/CardRepository.java | 12 + .../repository/GoofishAccountRepository.java | 2 + .../core/repository/KeywordRepository.java | 35 ++ .../core/repository/OrderRepository.java | 15 + .../server/controller/AdminController.java | 76 +++ .../server/controller/AuthController.java | 473 ++++++++++-------- .../server/controller/CardController.java | 15 + .../server/controller/KeywordController.java | 43 ++ .../server/controller/OrderController.java | 46 ++ .../xinayu/server/service/AuthService.java | 392 ++++++++------- 18 files changed, 1039 insertions(+), 408 deletions(-) create mode 100644 xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/AdminStatsResponse.java create mode 100644 xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/CheckDefaultPwdResponse.java create mode 100644 xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/OrderResponse.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/entity/AiReplySetting.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Card.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Keyword.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Order.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/repository/AiReplySettingRepository.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/repository/CardRepository.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/repository/KeywordRepository.java create mode 100644 xianyu-core/src/main/java/top/biwin/xianyu/core/repository/OrderRepository.java create mode 100644 xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AdminController.java create mode 100644 xianyu-server/src/main/java/top/biwin/xinayu/server/controller/CardController.java create mode 100644 xianyu-server/src/main/java/top/biwin/xinayu/server/controller/KeywordController.java create mode 100644 xianyu-server/src/main/java/top/biwin/xinayu/server/controller/OrderController.java diff --git a/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/AdminStatsResponse.java b/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/AdminStatsResponse.java new file mode 100644 index 0000000..1269ed4 --- /dev/null +++ b/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/AdminStatsResponse.java @@ -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; +} diff --git a/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/CheckDefaultPwdResponse.java b/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/CheckDefaultPwdResponse.java new file mode 100644 index 0000000..0161472 --- /dev/null +++ b/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/CheckDefaultPwdResponse.java @@ -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; +} diff --git a/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/OrderResponse.java b/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/OrderResponse.java new file mode 100644 index 0000000..0ec8c91 --- /dev/null +++ b/xianyu-common/src/main/java/top/biwin/xinayu/common/dto/response/OrderResponse.java @@ -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; +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/AiReplySetting.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/AiReplySetting.java new file mode 100644 index 0000000..336c6ad --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/AiReplySetting.java @@ -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; +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Card.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Card.java new file mode 100644 index 0000000..6db1ce7 --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Card.java @@ -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; +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Keyword.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Keyword.java new file mode 100644 index 0000000..2195163 --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Keyword.java @@ -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; +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Order.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Order.java new file mode 100644 index 0000000..f2c7471 --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/entity/Order.java @@ -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; +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/AiReplySettingRepository.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/AiReplySettingRepository.java new file mode 100644 index 0000000..0b2dcb9 --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/AiReplySettingRepository.java @@ -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 { +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/CardRepository.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/CardRepository.java new file mode 100644 index 0000000..323e505 --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/CardRepository.java @@ -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 { + List findByUserId(Long userId); +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/GoofishAccountRepository.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/GoofishAccountRepository.java index efdb359..b9e7b5d 100644 --- a/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/GoofishAccountRepository.java +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/GoofishAccountRepository.java @@ -10,4 +10,6 @@ import java.util.Optional; public interface GoofishAccountRepository extends JpaRepository { Optional findByUserId(Long UserId); + + long countByEnabled(Boolean enabled); } diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/KeywordRepository.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/KeywordRepository.java new file mode 100644 index 0000000..aed606d --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/KeywordRepository.java @@ -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 { + List 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 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 findConflictGenericImageKeywords(@Param("cookieId") String cookieId, @Param("keyword") String keyword); +} diff --git a/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/OrderRepository.java b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/OrderRepository.java new file mode 100644 index 0000000..03f7a50 --- /dev/null +++ b/xianyu-core/src/main/java/top/biwin/xianyu/core/repository/OrderRepository.java @@ -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 { + List findByGoofishId(Long cookieId); + + // For stats potentially + long count(); +} diff --git a/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AdminController.java b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AdminController.java new file mode 100644 index 0000000..e6ff83b --- /dev/null +++ b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AdminController.java @@ -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 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()); + } +} diff --git a/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AuthController.java b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AuthController.java index 93308ad..23b7a15 100644 --- a/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AuthController.java +++ b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/AuthController.java @@ -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; - - /** - * 用户登录接口 - *

- * 请求示例: - * POST /auth/login - * { - * "username": "admin", - * "password": "password123" - * } - *

- * 响应示例: - * { - * "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - * "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - * "expiresIn": 900, - * "tokenType": "Bearer" - * } - *

- * 错误响应: - * - 401 Unauthorized: 用户名或密码错误 - * - 500 Internal Server Error: 服务器内部错误 - * - * @param request 登录请求(包含 username 和 password) - * @return ResponseEntity 登录响应 - */ - @PostMapping("/login") - public ResponseEntity login(@RequestBody LoginRequest request) { - LoginResponse response = authService.login(request); - return ResponseEntity.ok(response); - } - - /** - * 刷新访问令牌接口 - *

- * 请求示例: - * POST /auth/refresh - * { - * "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." - * } - *

- * 响应示例: - * { - * "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", - * "expiresIn": 900, - * "tokenType": "Bearer" - * } - *

- * 错误响应: - * - 401 Unauthorized: 刷新令牌无效或已过期 - * - 500 Internal Server Error: 服务器内部错误 - * - * @param request 刷新令牌请求(包含 refreshToken) - * @return ResponseEntity 刷新令牌响应 - */ - @PostMapping("/refresh") - public ResponseEntity refresh(@RequestBody RefreshRequest request) { - RefreshResponse response = authService.refresh(request.getRefreshToken()); - return ResponseEntity.ok(response); - } - - /** - * 发送邮箱验证码接口 - *

- * - * @param request 发送验证码请求(包含 email、captchaId、captchaCode) - * @return ResponseEntity 发送结果响应 - */ - @PostMapping("/send-verification-code") - public ResponseEntity sendVerificationCode(@RequestBody SendCodeRequest request) { - Optional 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); - } - - /** - * 生成图形验证码接口 - *

- * 生成4位字母数字混合验证码,5分钟有效 - *

- * 请求示例: - * GET /auth/generate-captcha - *

- * 响应示例: - * { - * "captchaId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - * "captchaImage": "data:image/png;base64,iVBORw0KGgo...", - * "expiresIn": 300 - * } - * - * @return ResponseEntity 验证码响应 - */ - @PostMapping("/generate-captcha") - public ResponseEntity generateCaptcha(@RequestBody LoginCaptchaRequest request) { - CaptchaResponse response = captchaService.generateCaptcha(request.getSessionId()); - return ResponseEntity.ok(response); - } - - /** - * 验证图形验证码 - * 对应 Python: /verify-captcha - */ - @PostMapping("/verify-captcha") - public ResponseEntity verifyCaptcha(@RequestBody VerifyLoginCaptchaRequest request) { - - // 先验证图形验证码 - if (!captchaService.verifyCaptcha(request.getSessionId(), request.getCaptchaCode())) { - throw new BadCredentialsException("图形验证码错误或已过期"); + /** + * 用户登录接口 + *

+ * 请求示例: + * POST /auth/login + * { + * "username": "admin", + * "password": "password123" + * } + *

+ * 响应示例: + * { + * "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + * "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + * "expiresIn": 900, + * "tokenType": "Bearer" + * } + *

+ * 错误响应: + * - 401 Unauthorized: 用户名或密码错误 + * - 500 Internal Server Error: 服务器内部错误 + * + * @param request 登录请求(包含 username 和 password) + * @return ResponseEntity 登录响应 + */ + @PostMapping("/auth/login") + public ResponseEntity login(@RequestBody LoginRequest request) { + LoginResponse response = authService.login(request); + return ResponseEntity.ok(response); } - return ResponseEntity.ok(new BaseResponse("图形验证码验证成功", true)); - } - - /** - * 验证 Token 接口 - * 检查 Access Token 是否有效,并返回用户信息 - *

- * 请求示例: - * GET /auth/verify - * Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... - *

- * 响应示例(Token 有效): - * { - * "authenticated": true, - * "user_id": 1, - * "username": "admin", - * "email": "admin@example.com", - * "is_admin": true - * } - *

- * 响应示例(Token 无效): - * { - * "authenticated": false - * } - * - * @param request HTTP 请求对象 - * @return 验证结果和用户信息 - */ - @GetMapping("/verify") - public ResponseEntity> verify(HttpServletRequest request) { - Map response = new HashMap<>(); - - // 从请求头中提取 Token - String token = getTokenFromRequest(request); - - if (token == null) { - response.put("authenticated", false); - response.put("message", "缺少 Authorization 头"); - return ResponseEntity.ok(response); + /** + * 刷新访问令牌接口 + *

+ * 请求示例: + * POST /auth/refresh + * { + * "refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..." + * } + *

+ * 响应示例: + * { + * "accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...", + * "expiresIn": 900, + * "tokenType": "Bearer" + * } + *

+ * 错误响应: + * - 401 Unauthorized: 刷新令牌无效或已过期 + * - 500 Internal Server Error: 服务器内部错误 + * + * @param request 刷新令牌请求(包含 refreshToken) + * @return ResponseEntity 刷新令牌响应 + */ + @PostMapping("/auth/refresh") + public ResponseEntity 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); + /** + * 发送邮箱验证码接口 + *

+ * + * @param request 发送验证码请求(包含 email、captchaId、captchaCode) + * @return ResponseEntity 发送结果响应 + */ + @PostMapping("/auth/send-verification-code") + public ResponseEntity sendVerificationCode(@RequestBody SendCodeRequest request) { + Optional optUser = adminUserRepository.findByEmail(request.getEmail()); - // 从数据库查询用户信息 - Optional 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); + /** + * 生成图形验证码接口 + *

+ * 生成4位字母数字混合验证码,5分钟有效 + *

+ * 请求示例: + * GET /auth/generate-captcha + *

+ * 响应示例: + * { + * "captchaId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + * "captchaImage": "data:image/png;base64,iVBORw0KGgo...", + * "expiresIn": 300 + * } + * + * @return ResponseEntity 验证码响应 + */ + @PostMapping("/auth/generate-captcha") + public ResponseEntity 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 verifyCaptcha(@RequestBody VerifyLoginCaptchaRequest request) { + + // 先验证图形验证码 + if (!captchaService.verifyCaptcha(request.getSessionId(), request.getCaptchaCode())) { + throw new BadCredentialsException("图形验证码错误或已过期"); + } + + return ResponseEntity.ok(new BaseResponse("图形验证码验证成功", true)); + } + + /** + * 验证 Token 接口 + * 检查 Access Token 是否有效,并返回用户信息 + *

+ * 请求示例: + * GET /auth/verify + * Headers: Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + *

+ * 响应示例(Token 有效): + * { + * "authenticated": true, + * "user_id": 1, + * "username": "admin", + * "email": "admin@example.com", + * "is_admin": true + * } + *

+ * 响应示例(Token 无效): + * { + * "authenticated": false + * } + * + * @param request HTTP 请求对象 + * @return 验证结果和用户信息 + */ + @GetMapping("/auth/verify") + public ResponseEntity> verify(HttpServletRequest request) { + Map 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 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 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); + } + + } diff --git a/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/CardController.java b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/CardController.java new file mode 100644 index 0000000..0a77fbb --- /dev/null +++ b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/CardController.java @@ -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 { +} diff --git a/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/KeywordController.java b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/KeywordController.java new file mode 100644 index 0000000..c9c4889 --- /dev/null +++ b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/KeywordController.java @@ -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> getAllAiSettings() { + List goofishIds = goofishAccountRepository.findAll().stream() + .map(GoofishAccount::getId) + .toList(); + + List settings = aiReplySettingRepository.findAllById(goofishIds); + + return ResponseEntity.ok(settings.stream().collect(Collectors.toMap(AiReplySetting::getGoofishId, s -> s))); + } +} diff --git a/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/OrderController.java b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/OrderController.java new file mode 100644 index 0000000..d60024c --- /dev/null +++ b/xianyu-server/src/main/java/top/biwin/xinayu/server/controller/OrderController.java @@ -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> getAllOrder() { + List goofishIds = goofishAccountRepository.findAll().stream() + .map(GoofishAccount::getId) + .toList(); + List result = new ArrayList<>(); + goofishIds.forEach(i -> { + List order = orderRepository.findByGoofishId(i); + result.addAll(BeanUtil.copyToList(order, OrderResponse.class)); + }); + return ResponseEntity.ok(result); + } +} diff --git a/xianyu-server/src/main/java/top/biwin/xinayu/server/service/AuthService.java b/xianyu-server/src/main/java/top/biwin/xinayu/server/service/AuthService.java index 79d407f..b569c4c 100644 --- a/xianyu-server/src/main/java/top/biwin/xinayu/server/service/AuthService.java +++ b/xianyu-server/src/main/java/top/biwin/xinayu/server/service/AuthService.java @@ -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; - /** - * 用户登录(支持多种方式) - *

- * 登录方式自动判断: - * - username + password -> 用户名密码登录 - * - email + password -> 邮箱密码登录 - * - email + verificationCode -> 邮箱验证码登录 - * - * @param request 登录请求 - * @return LoginResponse 登录响应(包含 token 和用户信息) - * @throws BadCredentialsException 认证失败时抛出 - */ - public LoginResponse login(LoginRequest request) { - AdminUser user = null; + /** + * 用户登录(支持多种方式) + *

+ * 登录方式自动判断: + * - 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; - } - - /** - * 刷新访问令牌 - *

- * ⭐️ 主人的自定义逻辑实现位置 #3 - *

- * 💡 扩展提示: - * - 可以实现 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("刷新令牌无效或已过期"); + /** + * 刷新访问令牌 + *

+ * ⭐️ 主人的自定义逻辑实现位置 #3 + *

+ * 💡 扩展提示: + * - 可以实现 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"); - } }