init
This commit is contained in:
parent
17f557625b
commit
463b19a049
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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> {
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -10,4 +10,6 @@ import java.util.Optional;
|
||||
public interface GoofishAccountRepository extends JpaRepository<GoofishAccount, Long> {
|
||||
|
||||
Optional<GoofishAccount> findByUserId(Long UserId);
|
||||
|
||||
long countByEnabled(Boolean enabled);
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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 发送验证码请求(包含 email、captchaId、captchaCode)
|
||||
* @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 发送验证码请求(包含 email、captchaId、captchaCode)
|
||||
* @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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
}
|
||||
@ -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)));
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user