init
This commit is contained in:
parent
14c753af24
commit
4e5162e804
@ -25,7 +25,6 @@
|
||||
<dependency>
|
||||
<groupId>org.msgpack</groupId>
|
||||
<artifactId>jackson-dataformat-msgpack</artifactId>
|
||||
<version>0.9.6</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
@ -1,16 +1,21 @@
|
||||
package top.biwin.xinayu.common.dto.request;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 登录请求 DTO
|
||||
* 用于接收客户端的登录请求参数
|
||||
* <p>
|
||||
* 注意:使用 @JsonIgnoreProperties(ignoreUnknown = true) 注解
|
||||
* 允许前端传入额外字段而不会导致反序列化失败
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Data
|
||||
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||
public class LoginRequest {
|
||||
/**
|
||||
* 用户名
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
package top.biwin.xinayu.common.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-22 21:52
|
||||
*/
|
||||
@Data
|
||||
public class CookieDetailsResponse {
|
||||
private Long id;
|
||||
private String cookie;
|
||||
private Boolean enabled;
|
||||
@JsonProperty("auto_confirm")
|
||||
private Integer autoConfirm;
|
||||
private String remark;
|
||||
@JsonProperty("pause_duration")
|
||||
private Integer pauseDuration;
|
||||
private String username;
|
||||
@JsonProperty("login_password")
|
||||
private String loginPassword;
|
||||
@JsonProperty("show_browser")
|
||||
private Boolean showBrowser;
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package top.biwin.xinayu.common.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-22 21:52
|
||||
*/
|
||||
@Data
|
||||
public class GoofishAccountResponse {
|
||||
private Long id;
|
||||
private String cookie;
|
||||
private Boolean enabled;
|
||||
@JsonProperty("auto_confirm")
|
||||
private Integer autoConfirm;
|
||||
private String remark;
|
||||
@JsonProperty("pause_duration")
|
||||
private Integer pauseDuration;
|
||||
private String username;
|
||||
@JsonProperty("login_password")
|
||||
private String loginPassword;
|
||||
@JsonProperty("show_browser")
|
||||
private Boolean showBrowser;
|
||||
}
|
||||
@ -0,0 +1,37 @@
|
||||
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-23 21:53
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class KeywordResponse {
|
||||
|
||||
private Long id; // Synthetic ID, as original table didn't have one but needed for JPA
|
||||
|
||||
@JsonProperty("goofish_id")
|
||||
private Long goofishId;
|
||||
|
||||
private String keyword;
|
||||
|
||||
private String reply;
|
||||
|
||||
@JsonProperty("item_id")
|
||||
private String itemId;
|
||||
|
||||
private String type;
|
||||
|
||||
@JsonProperty("image_url")
|
||||
private String imageUrl;
|
||||
}
|
||||
@ -0,0 +1,24 @@
|
||||
package top.biwin.xinayu.common.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-23 23:02
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@SuperBuilder
|
||||
public class QrLoginResponse extends BaseResponse {
|
||||
|
||||
@JsonProperty("session_id")
|
||||
private String sessionId;
|
||||
|
||||
@JsonProperty("qr_code_url")
|
||||
private String qrCodeUrl;
|
||||
}
|
||||
@ -15,7 +15,7 @@
|
||||
<dependency>
|
||||
<groupId>top.biwin</groupId>
|
||||
<artifactId>xianyu-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@ -19,7 +19,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "admin_user")
|
||||
public class AdminUser {
|
||||
public class AdminUserEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@ -13,7 +13,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "ai_reply_settings")
|
||||
public class AiReplySetting {
|
||||
public class AiReplySettingEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "goofish_id")
|
||||
@ -10,7 +10,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "cards")
|
||||
public class Card {
|
||||
public class CardEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@ -16,7 +16,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "email_verification_code")
|
||||
public class EmailVerificationCode {
|
||||
public class EmailVerificationCodeEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@ -21,7 +21,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "goofish_account")
|
||||
public class GoofishAccount {
|
||||
public class GoofishAccountEntity {
|
||||
@Id
|
||||
private Long id;
|
||||
|
||||
@ -6,14 +6,14 @@ import lombok.Data;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "keywords")
|
||||
public class Keyword {
|
||||
public class KeywordEntity {
|
||||
|
||||
@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;
|
||||
@Column(name = "goofish_id")
|
||||
private Long goofishId;
|
||||
|
||||
private String keyword;
|
||||
|
||||
@ -15,7 +15,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "orders")
|
||||
public class Order {
|
||||
public class OrderEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "order_id")
|
||||
@ -12,7 +12,7 @@ import java.time.LocalDateTime;
|
||||
@Data
|
||||
@Entity
|
||||
@Table(name = "system_settings")
|
||||
public class SystemSetting {
|
||||
public class SystemSettingEntity {
|
||||
|
||||
@Id
|
||||
@Column(name = "key", unique = true, nullable = false)
|
||||
@ -2,7 +2,7 @@ package top.biwin.xianyu.core.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import top.biwin.xianyu.core.entity.AdminUser;
|
||||
import top.biwin.xianyu.core.entity.AdminUserEntity;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@ -14,7 +14,7 @@ import java.util.Optional;
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Repository
|
||||
public interface AdminUserRepository extends JpaRepository<AdminUser, Long> {
|
||||
public interface AdminUserRepository extends JpaRepository<AdminUserEntity, Long> {
|
||||
|
||||
/**
|
||||
* 根据用户名查询用户
|
||||
@ -22,7 +22,7 @@ public interface AdminUserRepository extends JpaRepository<AdminUser, Long> {
|
||||
* @param username 用户名
|
||||
* @return Optional<AdminUser>
|
||||
*/
|
||||
Optional<AdminUser> findByUsername(String username);
|
||||
Optional<AdminUserEntity> findByUsername(String username);
|
||||
|
||||
/**
|
||||
* 根据邮箱查询用户
|
||||
@ -30,5 +30,5 @@ public interface AdminUserRepository extends JpaRepository<AdminUser, Long> {
|
||||
* @param email 邮箱
|
||||
* @return Optional<AdminUser>
|
||||
*/
|
||||
Optional<AdminUser> findByEmail(String email);
|
||||
Optional<AdminUserEntity> findByEmail(String email);
|
||||
}
|
||||
|
||||
@ -2,8 +2,8 @@ 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;
|
||||
import top.biwin.xianyu.core.entity.AiReplySettingEntity;
|
||||
|
||||
@Repository
|
||||
public interface AiReplySettingRepository extends JpaRepository<AiReplySetting, Long> {
|
||||
public interface AiReplySettingRepository extends JpaRepository<AiReplySettingEntity, Long> {
|
||||
}
|
||||
|
||||
@ -2,11 +2,11 @@ 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 top.biwin.xianyu.core.entity.CardEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface CardRepository extends JpaRepository<Card, Long> {
|
||||
List<Card> findByUserId(Long userId);
|
||||
public interface CardRepository extends JpaRepository<CardEntity, Long> {
|
||||
List<CardEntity> findByUserId(Long userId);
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ package top.biwin.xianyu.core.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import top.biwin.xianyu.core.entity.EmailVerificationCode;
|
||||
import top.biwin.xianyu.core.entity.EmailVerificationCodeEntity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Optional;
|
||||
@ -15,7 +15,7 @@ import java.util.Optional;
|
||||
* @since 2026-01-21
|
||||
*/
|
||||
@Repository
|
||||
public interface EmailVerificationCodeRepository extends JpaRepository<EmailVerificationCode, Long> {
|
||||
public interface EmailVerificationCodeRepository extends JpaRepository<EmailVerificationCodeEntity, Long> {
|
||||
|
||||
/**
|
||||
* 根据邮箱和验证码查询(用于验证)
|
||||
@ -26,7 +26,7 @@ public interface EmailVerificationCodeRepository extends JpaRepository<EmailVeri
|
||||
* @param now 当前时间
|
||||
* @return Optional<EmailVerificationCode>
|
||||
*/
|
||||
Optional<EmailVerificationCode> findByEmailAndCodeAndIsUsedFalseAndExpiresAtAfter(
|
||||
Optional<EmailVerificationCodeEntity> findByEmailAndCodeAndIsUsedFalseAndExpiresAtAfter(
|
||||
String email, String code, LocalDateTime now);
|
||||
|
||||
/**
|
||||
@ -35,7 +35,7 @@ public interface EmailVerificationCodeRepository extends JpaRepository<EmailVeri
|
||||
* @param email 邮箱地址
|
||||
* @return Optional<EmailVerificationCode>
|
||||
*/
|
||||
Optional<EmailVerificationCode> findFirstByEmailOrderByCreatedAtDesc(String email);
|
||||
Optional<EmailVerificationCodeEntity> findFirstByEmailOrderByCreatedAtDesc(String email);
|
||||
|
||||
/**
|
||||
* 删除过期的验证码(定时清理任务使用)
|
||||
|
||||
@ -2,14 +2,14 @@ package top.biwin.xianyu.core.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import top.biwin.xianyu.core.entity.GoofishAccount;
|
||||
import top.biwin.xianyu.core.entity.GoofishAccountEntity;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface GoofishAccountRepository extends JpaRepository<GoofishAccount, Long> {
|
||||
public interface GoofishAccountRepository extends JpaRepository<GoofishAccountEntity, Long> {
|
||||
|
||||
Optional<GoofishAccount> findByUserId(Long UserId);
|
||||
Optional<GoofishAccountEntity> findByUserId(Long UserId);
|
||||
|
||||
long countByEnabled(Boolean enabled);
|
||||
}
|
||||
|
||||
@ -6,30 +6,28 @@ 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 top.biwin.xianyu.core.entity.KeywordEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface KeywordRepository extends JpaRepository<Keyword, Long> {
|
||||
List<Keyword> findByCookieId(String cookieId);
|
||||
public interface KeywordRepository extends JpaRepository<KeywordEntity, Long> {
|
||||
List<KeywordEntity> findByGoofishId(Long goofishId);
|
||||
|
||||
@Transactional
|
||||
@Modifying
|
||||
void deleteByCookieId(String cookieId);
|
||||
void deleteByGoofishId(Long goofishId);
|
||||
|
||||
@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);
|
||||
@Query("DELETE FROM KeywordEntity k WHERE k.goofishId = :goofishId AND (k.type IS NULL OR k.type = 'text')")
|
||||
void deleteTextKeywordsByGoofishId(@Param("goofishId") Long goofishId);
|
||||
|
||||
// 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);
|
||||
@Query("SELECT k FROM KeywordEntity k WHERE k.goofishId = :goofishId AND k.keyword = :keyword AND k.itemId = :itemId AND k.type = 'image'")
|
||||
List<KeywordEntity> findConflictImageKeywords(@Param("goofishId") Long goofishId, @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);
|
||||
@Query("SELECT k FROM KeywordEntity k WHERE k.goofishId = :goofishId AND k.keyword = :keyword AND (k.itemId IS NULL OR k.itemId = '') AND k.type = 'image'")
|
||||
List<KeywordEntity> findConflictGenericImageKeywords(@Param("goofishId") Long goofishId, @Param("keyword") String keyword);
|
||||
}
|
||||
|
||||
@ -2,13 +2,13 @@ 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 top.biwin.xianyu.core.entity.OrderEntity;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Repository
|
||||
public interface OrderRepository extends JpaRepository<Order, String> {
|
||||
List<Order> findByGoofishId(Long cookieId);
|
||||
public interface OrderRepository extends JpaRepository<OrderEntity, String> {
|
||||
List<OrderEntity> findByGoofishId(Long cookieId);
|
||||
|
||||
// For stats potentially
|
||||
long count();
|
||||
|
||||
@ -2,11 +2,11 @@ package top.biwin.xianyu.core.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import top.biwin.xianyu.core.entity.SystemSetting;
|
||||
import top.biwin.xianyu.core.entity.SystemSettingEntity;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Repository
|
||||
public interface SystemSettingRepository extends JpaRepository<SystemSetting, String> {
|
||||
Optional<SystemSetting> findByKey(String key);
|
||||
public interface SystemSettingRepository extends JpaRepository<SystemSettingEntity, String> {
|
||||
Optional<SystemSettingEntity> findByKey(String key);
|
||||
}
|
||||
|
||||
@ -11,4 +11,31 @@
|
||||
|
||||
<artifactId>xianyu-goofish</artifactId>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>top.biwin</groupId>
|
||||
<artifactId>xianyu-core</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.msgpack</groupId>
|
||||
<artifactId>jackson-dataformat-msgpack</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.zxing</groupId>
|
||||
<artifactId>javase</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
@ -0,0 +1,33 @@
|
||||
package top.biwin.xianyu.goofish.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-23 22:47
|
||||
*/
|
||||
@Data
|
||||
public class QrLoginSessionDto {
|
||||
private String sessionId;
|
||||
private String status = "waiting"; // waiting, scanned, success, expired, cancelled, verification_required
|
||||
private String qrCodeUrl;
|
||||
private String qrContent;
|
||||
private String unb;
|
||||
private long createdTime = System.currentTimeMillis();
|
||||
private long expireTime = 300 * 1000; // 5 mins
|
||||
private String verificationUrl;
|
||||
private Map<String, String> params = new HashMap<>(); // Store login params (t, ck, etc.)
|
||||
private Map<String, String> cookies = new HashMap<>();
|
||||
private String accountId; // 保存处理后的账号ID
|
||||
private boolean isNewAccount; // 是否为新账号
|
||||
private boolean realCookieRefreshed; // 是否成功刷新真实Cookie
|
||||
|
||||
public boolean isExpired() {
|
||||
return System.currentTimeMillis() - createdTime > expireTime;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,756 @@
|
||||
package top.biwin.xianyu.goofish.qrcode;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.google.zxing.BarcodeFormat;
|
||||
import com.google.zxing.client.j2se.MatrixToImageWriter;
|
||||
import com.google.zxing.common.BitMatrix;
|
||||
import com.google.zxing.qrcode.QRCodeWriter;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import okhttp3.Cookie;
|
||||
import okhttp3.CookieJar;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.HttpUrl;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.Response;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
|
||||
import top.biwin.xianyu.goofish.dto.QrLoginSessionDto;
|
||||
import top.biwin.xinayu.common.dto.response.QrLoginResponse;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
public class QrLoginService {
|
||||
|
||||
@Autowired
|
||||
private GoofishAccountRepository goofishAccountRepository;
|
||||
// private final BrowserService browserService;
|
||||
@Autowired
|
||||
private OkHttpClient okHttpClient;
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
private final Map<String, QrLoginSessionDto> sessions = new ConcurrentHashMap<>();
|
||||
|
||||
// 并发锁机制 - 防止同一session的并发处理
|
||||
private final Map<String, Object> qrCheckLocks = new ConcurrentHashMap<>();
|
||||
// 已处理记录 - 记录已完成处理的session
|
||||
private final Map<String, ProcessedRecord> qrCheckProcessed = new ConcurrentHashMap<>();
|
||||
|
||||
// --- Session Classes ---
|
||||
|
||||
@Data
|
||||
public static class ProcessedRecord {
|
||||
private boolean processed;
|
||||
private long timestamp;
|
||||
|
||||
public ProcessedRecord(boolean processed, long timestamp) {
|
||||
this.processed = processed;
|
||||
this.timestamp = timestamp;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Core Methods ---
|
||||
|
||||
public QrLoginResponse generateQrCode() {
|
||||
String sessionId = UUID.randomUUID().toString();
|
||||
log.info("【QR Login】Generating new QR code session: {}", sessionId);
|
||||
|
||||
QrLoginSessionDto session = new QrLoginSessionDto();
|
||||
session.setSessionId(sessionId);
|
||||
|
||||
try {
|
||||
// 1. Get m_h5_tk
|
||||
// getMh5tk(session);
|
||||
log.info("【QR Login】Got m_h5_tk for session: {}", sessionId);
|
||||
|
||||
// 2. Get Login Params
|
||||
Map<String, String> loginParams = getLoginParams(session);
|
||||
log.info("【QR Login】Got login params for session: {}", sessionId);
|
||||
|
||||
// 3. Generate QR Code Data
|
||||
// Construct URL: https://passport.goofish.com/newlogin/qrcode/generate.do
|
||||
HttpUrl.Builder urlBuilder = HttpUrl.parse("https://passport.goofish.com/newlogin/qrcode/generate.do").newBuilder();
|
||||
for (Map.Entry<String, String> entry : loginParams.entrySet()) {
|
||||
urlBuilder.addQueryParameter(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(urlBuilder.build())
|
||||
.headers(generateHeaders())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = okHttpClient.newCall(request).execute()) {
|
||||
String responseBody = response.body().string();
|
||||
log.debug("【QR Login Debug】Generate QR raw response: {}", responseBody);
|
||||
|
||||
Map<String, Object> result = objectMapper.readValue(responseBody, Map.class);
|
||||
Map<String, Object> content = (Map<String, Object>) result.get("content");
|
||||
Boolean success = (Boolean) content.get("success");
|
||||
|
||||
if (success != null && success) {
|
||||
Map<String, Object> data = (Map<String, Object>) content.get("data");
|
||||
|
||||
// Update session with t and ck
|
||||
session.getParams().put("t", String.valueOf(data.get("t")));
|
||||
session.getParams().put("ck", (String) data.get("ck"));
|
||||
|
||||
String qrContent = (String) data.get("codeContent");
|
||||
session.setQrContent(qrContent);
|
||||
|
||||
String qrBase64 = generateQrImageBase64(qrContent);
|
||||
String qrDataUrl = "data:image/png;base64," + qrBase64;
|
||||
|
||||
session.setQrCodeUrl(qrDataUrl);
|
||||
sessions.put(sessionId, session);
|
||||
|
||||
log.info("【QR Login】QR Code generated successfully: {}", sessionId);
|
||||
return QrLoginResponse.builder()
|
||||
.success(true)
|
||||
.sessionId(sessionId)
|
||||
.qrCodeUrl(qrDataUrl)
|
||||
.build();
|
||||
} else {
|
||||
throw new RuntimeException("Failed to generate QR code from API");
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】Error generating QR code", e);
|
||||
return QrLoginResponse.builder()
|
||||
.success(false)
|
||||
.message("生成二维码失败: " + e.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> checkQrCodeStatus(String sessionId) {
|
||||
try {
|
||||
// 1. 清理过期记录
|
||||
cleanupQrCheckRecords();
|
||||
|
||||
// 2. 检查是否已经处理过
|
||||
ProcessedRecord processedRecord = qrCheckProcessed.get(sessionId);
|
||||
if (processedRecord != null && processedRecord.isProcessed()) {
|
||||
log.debug("【QR Login】扫码登录session {} 已处理过,直接返回", sessionId);
|
||||
return Map.of("status", "already_processed", "message", "该会话已处理完成");
|
||||
}
|
||||
|
||||
// 3. 获取或创建该session的锁对象
|
||||
Object sessionLock = qrCheckLocks.computeIfAbsent(sessionId, k -> new Object());
|
||||
|
||||
// 4. 尝试获取锁(使用tryLock模式,避免阻塞)
|
||||
boolean lockAcquired = false;
|
||||
synchronized (sessionLock) {
|
||||
// 检查锁状态(在Java中我们用一个简单的标记)
|
||||
// 如果已经有线程在处理,直接返回processing
|
||||
if (Thread.holdsLock(sessionLock)) {
|
||||
lockAcquired = true;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用synchronized块确保同一session不会被并发处理
|
||||
synchronized (sessionLock) {
|
||||
// 5. 双重检查 - 再次确认是否已处理
|
||||
processedRecord = qrCheckProcessed.get(sessionId);
|
||||
if (processedRecord != null && processedRecord.isProcessed()) {
|
||||
log.debug("【QR Login】扫码登录session {} 在获取锁后发现已处理,直接返回", sessionId);
|
||||
return Map.of("status", "already_processed", "message", "该会话已处理完成");
|
||||
}
|
||||
|
||||
// 6. 清理过期会话
|
||||
cleanupExpiredSessions();
|
||||
|
||||
// 7. 获取会话状态
|
||||
Map<String, Object> statusInfo = getSessionStatus(sessionId);
|
||||
log.info("【QR Login】获取扫码状态信息: {}", statusInfo);
|
||||
|
||||
String status = (String) statusInfo.get("status");
|
||||
|
||||
// 8. 如果登录成功,处理Cookie
|
||||
if ("success".equals(status)) {
|
||||
log.info("【QR Login】扫码成功的状态信息: {}", statusInfo);
|
||||
|
||||
// 获取会话Cookie信息
|
||||
Map<String, String> cookiesInfo = getSessionCookies(sessionId);
|
||||
log.info("【QR Login】获取会话Cookie: {}", cookiesInfo);
|
||||
|
||||
if (cookiesInfo != null && !cookiesInfo.isEmpty()) {
|
||||
// 处理扫码登录Cookie
|
||||
Map<String, Object> accountInfo = processQrLoginCookies(
|
||||
cookiesInfo.get("cookies"),
|
||||
cookiesInfo.get("unb")
|
||||
);
|
||||
|
||||
// 将账号信息添加到返回结果中
|
||||
statusInfo.put("account_info", accountInfo);
|
||||
|
||||
log.info("【QR Login】扫码登录处理完成: {}, 账号: {}",
|
||||
sessionId, accountInfo.get("account_id"));
|
||||
|
||||
// 9. 标记该session已处理
|
||||
qrCheckProcessed.put(sessionId, new ProcessedRecord(true, System.currentTimeMillis()));
|
||||
}
|
||||
}
|
||||
|
||||
return statusInfo;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】检查扫码登录状态异常: {}", e.getMessage(), e);
|
||||
Map<String, Object> errorResult = new HashMap<>();
|
||||
errorResult.put("status", "error");
|
||||
errorResult.put("message", e.getMessage());
|
||||
return errorResult;
|
||||
}
|
||||
}
|
||||
|
||||
private void processLoginSuccess(QrLoginSessionDto session) {
|
||||
/*String unb = session.getUnb();
|
||||
if (unb == null) {
|
||||
throw new RuntimeException("Logged in but UNB is missing!");
|
||||
}
|
||||
|
||||
// 1. Determine AccountId
|
||||
String accountId = unb; // Default to UNB
|
||||
boolean isNewAccount = true;
|
||||
|
||||
if (goofishAccountRepository.existsById(unb)) {
|
||||
isNewAccount = false;
|
||||
log.info("【QR Login】Found existing account by ID: {}", unb);
|
||||
} else {
|
||||
log.info("【QR Login】New account detected for UNB: {}", unb);
|
||||
}
|
||||
|
||||
// 2. Verify and Refresh Cookies via BrowserService
|
||||
Map<String, String> verifiedCookies = browserService.verifyQrLoginCookies(session.getCookies(), accountId);
|
||||
|
||||
if (verifiedCookies != null && !verifiedCookies.isEmpty()) {
|
||||
log.info("【QR Login】Browser verification SUCCESS. Cookies verified: {}", verifiedCookies.size());
|
||||
|
||||
// Build cookie string
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : verifiedCookies.entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
String finalCookieStr = sb.toString();
|
||||
|
||||
// 3. Save to DB
|
||||
com.xianyu.autoreply.entity.Cookie cookieEntity = cookieRepository.findById(accountId)
|
||||
.orElse(new com.xianyu.autoreply.entity.Cookie());
|
||||
|
||||
cookieEntity.setId(accountId);
|
||||
cookieEntity.setValue(finalCookieStr);
|
||||
if (isNewAccount) {
|
||||
cookieEntity.setUsername("TB_" + unb); // Placeholder
|
||||
cookieEntity.setPassword("QR_LOGIN"); // Placeholder
|
||||
cookieEntity.setUserId(0L);
|
||||
}
|
||||
cookieEntity.setEnabled(true);
|
||||
cookieRepository.save(cookieEntity);
|
||||
|
||||
log.info("【QR Login】Account saved to DB: {}", accountId);
|
||||
|
||||
// Update session
|
||||
session.setCookies(verifiedCookies);
|
||||
|
||||
} else {
|
||||
log.warn("【QR Login】Browser verification FAILED. Falling back to simple API cookies.");
|
||||
// Fallback: Save original API cookies
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : session.getCookies().entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
String finalCookieStr = sb.toString();
|
||||
|
||||
com.xianyu.autoreply.entity.Cookie cookieEntity = cookieRepository.findById(accountId)
|
||||
.orElse(new com.xianyu.autoreply.entity.Cookie());
|
||||
cookieEntity.setId(accountId);
|
||||
cookieEntity.setValue(finalCookieStr);
|
||||
cookieEntity.setEnabled(true);
|
||||
cookieRepository.save(cookieEntity);
|
||||
log.info("【QR Login】Fallback: Original API cookies saved for {}", accountId);
|
||||
}*/
|
||||
}
|
||||
|
||||
private Map<String, Object> buildSuccessResult(QrLoginSessionDto session) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "success");
|
||||
result.put("session_id", session.getSessionId());
|
||||
result.put("unb", session.getUnb());
|
||||
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : session.getCookies().entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
result.put("cookies", sb.toString());
|
||||
return result;
|
||||
}
|
||||
|
||||
// --- Helper Methods ---
|
||||
|
||||
/**
|
||||
* Refresh existing cookies using browser (Keep-alive)
|
||||
*/
|
||||
public Map<String, String> refreshCookie(String accountId) {
|
||||
/*log.info("【QR Login】Triggering cookie refresh for account: {}", accountId);
|
||||
return browserService.refreshCookies(accountId);*/
|
||||
return null;
|
||||
}
|
||||
|
||||
private void getMh5tk(QrLoginSessionDto session) throws IOException {
|
||||
String apiH5Tk = "https://h5api.m.goofish.com/h5/mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get/1.0/";
|
||||
String appKey = "34839810";
|
||||
String dataStr = "{\"bizScene\":\"home\"}";
|
||||
String t = String.valueOf(System.currentTimeMillis());
|
||||
|
||||
// 1. Initial Get to get m_h5_tk cookie
|
||||
Request initialRequest = new Request.Builder()
|
||||
.url(apiH5Tk)
|
||||
.headers(generateHeaders())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
okHttpClient.newCall(initialRequest).execute().close(); // Cookies handled by cookieJar
|
||||
|
||||
// Extract m_h5_tk from cookie jar
|
||||
String mh5tk = getCookieValue("m_h5_tk");
|
||||
String token = mh5tk.split("_")[0];
|
||||
|
||||
// 2. Sign
|
||||
String signInput = token + "&" + t + "&" + appKey + "&" + dataStr;
|
||||
String sign = md5(signInput);
|
||||
|
||||
// 3. Post with sign
|
||||
HttpUrl url = HttpUrl.parse(apiH5Tk).newBuilder()
|
||||
.addQueryParameter("jsv", "2.7.2")
|
||||
.addQueryParameter("appKey", appKey)
|
||||
.addQueryParameter("t", t)
|
||||
.addQueryParameter("sign", sign)
|
||||
.addQueryParameter("v", "1.0")
|
||||
.addQueryParameter("type", "originaljson")
|
||||
.addQueryParameter("dataType", "json")
|
||||
.addQueryParameter("api", "mtop.gaia.nodejs.gaia.idle.data.gw.v2.index.get")
|
||||
.addQueryParameter("data", dataStr)
|
||||
.build();
|
||||
|
||||
Request postRequest = new Request.Builder()
|
||||
.url(url)
|
||||
.headers(generateHeaders())
|
||||
.get() // Note: Python code used POST but query params seem to be in URL? Let's check Python code. Line 138: client.post(..., params=params). params in httpx POST usually go to query string? No, httpx params are query, data/json is body. But here Python used `post` with `params`.
|
||||
// Ah, Python code: client.post(self.api_h5_tk, params=params, headers=self.headers, cookies=session.cookies)
|
||||
// Wait, if it is a POST without body?? Usually mtop requires GET or POST. Let's stick to what Python did.
|
||||
// Re-reading Python: `params` argument in `client.post` adds to URL query string.
|
||||
.post(RequestBody.create(new byte[0], null)) // Empty body POST
|
||||
.build();
|
||||
|
||||
okHttpClient.newCall(postRequest).execute().close();
|
||||
}
|
||||
|
||||
private Map<String, String> getLoginParams(QrLoginSessionDto session) throws IOException {
|
||||
HttpUrl url = HttpUrl.parse("https://passport.goofish.com/mini_login.htm").newBuilder()
|
||||
.addQueryParameter("lang", "zh_cn")
|
||||
.addQueryParameter("appName", "xianyu")
|
||||
.addQueryParameter("appEntrance", "web")
|
||||
.addQueryParameter("styleType", "vertical")
|
||||
.addQueryParameter("bizParams", "")
|
||||
.addQueryParameter("notLoadSsoView", "false")
|
||||
.addQueryParameter("notKeepLogin", "false")
|
||||
.addQueryParameter("isMobile", "false")
|
||||
.addQueryParameter("qrCodeFirst", "false")
|
||||
.addQueryParameter("stie", "77")
|
||||
.addQueryParameter("rnd", String.valueOf(Math.random()))
|
||||
.build();
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.headers(generateHeaders())
|
||||
.get()
|
||||
.build();
|
||||
|
||||
try (Response response = okHttpClient.newCall(request).execute()) {
|
||||
String html = response.body().string();
|
||||
Pattern pattern = Pattern.compile("window\\.viewData\\s*=\s*(\\{.*?\\});");
|
||||
Matcher matcher = pattern.matcher(html);
|
||||
if (matcher.find()) {
|
||||
String jsonStr = matcher.group(1);
|
||||
Map<String, Object> viewData = objectMapper.readValue(jsonStr, Map.class);
|
||||
Map<String, Object> loginFormData = (Map<String, Object>) viewData.get("loginFormData");
|
||||
|
||||
Map<String, String> params = new HashMap<>();
|
||||
if (loginFormData != null) {
|
||||
for (Map.Entry<String, Object> entry : loginFormData.entrySet()) {
|
||||
params.put(entry.getKey(), String.valueOf(entry.getValue()));
|
||||
}
|
||||
params.put("umidTag", "SERVER");
|
||||
session.getParams().putAll(params);
|
||||
return params;
|
||||
}
|
||||
}
|
||||
}
|
||||
throw new RuntimeException("Could not find login params in mini_login.htm");
|
||||
}
|
||||
|
||||
private void pollQrCodeStatus(QrLoginSessionDto session) throws IOException {
|
||||
String apiScanStatus = "https://passport.goofish.com/newlogin/qrcode/query.do";
|
||||
|
||||
FormBody.Builder formBuilder = new FormBody.Builder();
|
||||
for (Map.Entry<String, String> entry : session.getParams().entrySet()) {
|
||||
formBuilder.add(entry.getKey(), entry.getValue());
|
||||
}
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(apiScanStatus)
|
||||
.headers(generateHeaders())
|
||||
// In Python: client.post(api, data=session.params). `data` means FORM body.
|
||||
.post(formBuilder.build())
|
||||
.build();
|
||||
|
||||
try (Response response = okHttpClient.newCall(request).execute()) {
|
||||
String body = response.body().string();
|
||||
Map<String, Object> result = objectMapper.readValue(body, Map.class);
|
||||
|
||||
// Capture cookies from response
|
||||
List<Cookie> cookies = Cookie.parseAll(request.url(), response.headers());
|
||||
for (Cookie c : cookies) {
|
||||
session.getCookies().put(c.name(), c.value());
|
||||
if ("unb".equals(c.name())) {
|
||||
session.setUnb(c.value());
|
||||
}
|
||||
}
|
||||
|
||||
Map<String, Object> content = (Map<String, Object>) result.get("content");
|
||||
Map<String, Object> data = (Map<String, Object>) content.get("data");
|
||||
|
||||
String qrCodeStatus = (String) data.get("qrCodeStatus");
|
||||
|
||||
if ("CONFIRMED".equals(qrCodeStatus)) {
|
||||
Boolean iframeRedirect = (Boolean) data.get("iframeRedirect");
|
||||
if (iframeRedirect != null && iframeRedirect) {
|
||||
session.setStatus("verification_required");
|
||||
session.setVerificationUrl((String) data.get("iframeRedirectUrl"));
|
||||
log.warn("【QR Login】Risk control triggered: {}", session.getSessionId());
|
||||
} else {
|
||||
session.setStatus("success");
|
||||
log.info("【QR Login】Success! UNB: {}", session.getUnb());
|
||||
}
|
||||
} else if ("NEW".equals(qrCodeStatus)) {
|
||||
// waiting
|
||||
} else if ("EXPIRED".equals(qrCodeStatus)) {
|
||||
session.setStatus("expired");
|
||||
} else if ("SCANED".equals(qrCodeStatus)) {
|
||||
session.setStatus("scanned");
|
||||
} else {
|
||||
session.setStatus("cancelled");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String generateQrImageBase64(String content) throws Exception {
|
||||
QRCodeWriter qrCodeWriter = new QRCodeWriter();
|
||||
BitMatrix bitMatrix = qrCodeWriter.encode(content, BarcodeFormat.QR_CODE, 200, 200);
|
||||
ByteArrayOutputStream pngOutputStream = new ByteArrayOutputStream();
|
||||
MatrixToImageWriter.writeToStream(bitMatrix, "PNG", pngOutputStream);
|
||||
byte[] pngData = pngOutputStream.toByteArray();
|
||||
return Base64.getEncoder().encodeToString(pngData);
|
||||
}
|
||||
|
||||
private Headers generateHeaders() {
|
||||
return new Headers.Builder()
|
||||
.add("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.0.0 Safari/537.36 Edg/143.0.0.0")
|
||||
.add("Accept", "application/json, text/plain, */*")
|
||||
.add("Accept-Language", "zh-CN,zh;q=0.9,en;q=0.8")
|
||||
.add("Referer", "https://passport.goofish.com/")
|
||||
.add("Origin", "https://passport.goofish.com")
|
||||
.build();
|
||||
}
|
||||
|
||||
private String md5(String input) {
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("MD5");
|
||||
byte[] messageDigest = md.digest(input.getBytes());
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (byte b : messageDigest) {
|
||||
sb.append(String.format("%02x", b));
|
||||
}
|
||||
return sb.toString();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
private String getCookieValue(String name) {
|
||||
// InMemoryCookieJar implementation detail - retrieving for specific host
|
||||
// Since we know the host
|
||||
List<Cookie> cookies = okHttpClient.cookieJar().loadForRequest(HttpUrl.parse("https://h5api.m.goofish.com"));
|
||||
for (Cookie c : cookies) {
|
||||
if (c.name().equals(name)) return c.value();
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
// Simple custom InMemoryCookieJar
|
||||
private static class InMemoryCookieJar implements CookieJar {
|
||||
private final List<Cookie> cookies = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void saveFromResponse(HttpUrl url, List<Cookie> cookies) {
|
||||
this.cookies.addAll(cookies);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Cookie> loadForRequest(HttpUrl url) {
|
||||
List<Cookie> validCookies = new ArrayList<>();
|
||||
for (Cookie cookie : cookies) {
|
||||
validCookies.add(cookie);
|
||||
}
|
||||
return validCookies;
|
||||
}
|
||||
}
|
||||
|
||||
// --- 清理和工具方法 ---
|
||||
|
||||
/**
|
||||
* 清理过期的扫码检查记录(超过1小时)
|
||||
*/
|
||||
private void cleanupQrCheckRecords() {
|
||||
long currentTime = System.currentTimeMillis();
|
||||
List<String> expiredSessions = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, ProcessedRecord> entry : qrCheckProcessed.entrySet()) {
|
||||
// 清理超过1小时的记录
|
||||
if (currentTime - entry.getValue().getTimestamp() > 3600 * 1000) {
|
||||
expiredSessions.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for (String sessionId : expiredSessions) {
|
||||
qrCheckProcessed.remove(sessionId);
|
||||
qrCheckLocks.remove(sessionId);
|
||||
log.debug("【QR Login】清理过期的扫码检查记录: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理过期的登录会话
|
||||
*/
|
||||
private void cleanupExpiredSessions() {
|
||||
List<String> expiredSessions = new ArrayList<>();
|
||||
|
||||
for (Map.Entry<String, QrLoginSessionDto> entry : sessions.entrySet()) {
|
||||
if (entry.getValue().isExpired()) {
|
||||
expiredSessions.add(entry.getKey());
|
||||
}
|
||||
}
|
||||
|
||||
for (String sessionId : expiredSessions) {
|
||||
sessions.remove(sessionId);
|
||||
log.info("【QR Login】清理过期会话: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话状态(包含轮询API)
|
||||
*/
|
||||
private Map<String, Object> getSessionStatus(String sessionId) {
|
||||
QrLoginSessionDto session = sessions.get(sessionId);
|
||||
if (session == null) {
|
||||
return Map.of("status", "not_found", "message", "会话不存在或已过期");
|
||||
}
|
||||
|
||||
if (session.isExpired() && !"success".equals(session.getStatus())) {
|
||||
session.setStatus("expired");
|
||||
return Map.of("status", "expired", "session_id", sessionId);
|
||||
}
|
||||
|
||||
// 如果已经成功,直接返回
|
||||
if ("success".equals(session.getStatus()) && session.getUnb() != null) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", "success");
|
||||
result.put("session_id", sessionId);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 轮询状态
|
||||
try {
|
||||
pollQrCodeStatus(session);
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】轮询状态失败 for {}", sessionId, e);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("status", session.getStatus());
|
||||
result.put("session_id", sessionId);
|
||||
|
||||
if ("verification_required".equals(session.getStatus())) {
|
||||
result.put("verification_url", session.getVerificationUrl());
|
||||
result.put("message", "账号被风控,需要手机验证");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取会话Cookie信息
|
||||
*/
|
||||
private Map<String, String> getSessionCookies(String sessionId) {
|
||||
QrLoginSessionDto session = sessions.get(sessionId);
|
||||
if (session != null && "success".equals(session.getStatus())) {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
|
||||
// 将Cookie转换为字符串
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : session.getCookies().entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
|
||||
result.put("cookies", sb.toString());
|
||||
result.put("unb", session.getUnb());
|
||||
return result;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理扫码登录Cookie - 对应Python的process_qr_login_cookies方法
|
||||
*/
|
||||
private Map<String, Object> processQrLoginCookies(String cookies, String unb) {
|
||||
/*try {
|
||||
log.info("【QR Login】开始处理扫码登录Cookie, UNB: {}", unb);
|
||||
|
||||
// 1. 检查是否已存在相同unb的账号
|
||||
String existingAccountId = null;
|
||||
boolean isNewAccount = true;
|
||||
|
||||
// 遍历数据库中的所有Cookie,查找是否有相同的unb
|
||||
Iterable<com.xianyu.autoreply.entity.Cookie> allCookies = cookieRepository.findAll();
|
||||
for (com.xianyu.autoreply.entity.Cookie cookieEntity : allCookies) {
|
||||
String cookieValue = cookieEntity.getValue();
|
||||
if (cookieValue != null && cookieValue.contains("unb=" + unb)) {
|
||||
existingAccountId = cookieEntity.getId();
|
||||
isNewAccount = false;
|
||||
log.info("【QR Login】扫码登录找到现有账号: {}, UNB: {}", existingAccountId, unb);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 确定账号ID
|
||||
String accountId;
|
||||
if (existingAccountId != null) {
|
||||
accountId = existingAccountId;
|
||||
} else {
|
||||
// 创建新账号,使用unb作为账号ID
|
||||
accountId = unb;
|
||||
|
||||
// 确保账号ID唯一
|
||||
int counter = 1;
|
||||
String originalAccountId = accountId;
|
||||
while (cookieRepository.existsById(accountId)) {
|
||||
accountId = originalAccountId + "_" + counter;
|
||||
counter++;
|
||||
}
|
||||
|
||||
log.info("【QR Login】扫码登录准备创建新账号: {}, UNB: {}", accountId, unb);
|
||||
}
|
||||
|
||||
// 3. 使用浏览器服务验证并刷新Cookie(获取真实Cookie)
|
||||
log.info("【QR Login】开始使用扫码cookie获取真实cookie: {}", accountId);
|
||||
|
||||
boolean realCookieRefreshed = false;
|
||||
String finalCookieStr = cookies;
|
||||
|
||||
try {
|
||||
// 调用BrowserService验证并获取真实Cookie
|
||||
Map<String, String> verifiedCookies = browserService.verifyQrLoginCookies(
|
||||
parseCookieString(cookies),
|
||||
accountId
|
||||
);
|
||||
|
||||
if (verifiedCookies != null && !verifiedCookies.isEmpty()) {
|
||||
log.info("【QR Login】浏览器验证成功,获取到真实Cookie,数量: {}", verifiedCookies.size());
|
||||
|
||||
// 将Cookie转换为字符串
|
||||
StringBuilder sb = new StringBuilder();
|
||||
for (Map.Entry<String, String> entry : verifiedCookies.entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("; ");
|
||||
}
|
||||
finalCookieStr = sb.toString();
|
||||
realCookieRefreshed = true;
|
||||
} else {
|
||||
log.warn("【QR Login】浏览器验证失败,使用原始扫码Cookie");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】获取真实Cookie异常: {}", e.getMessage(), e);
|
||||
log.warn("【QR Login】降级处理 - 使用原始扫码Cookie");
|
||||
}
|
||||
|
||||
// 4. 保存Cookie到数据库
|
||||
com.xianyu.autoreply.entity.Cookie cookieEntity = cookieRepository.findById(accountId)
|
||||
.orElse(new com.xianyu.autoreply.entity.Cookie());
|
||||
|
||||
cookieEntity.setId(accountId);
|
||||
cookieEntity.setValue(finalCookieStr);
|
||||
|
||||
if (isNewAccount) {
|
||||
cookieEntity.setUsername("TB_" + unb);
|
||||
cookieEntity.setPassword("QR_LOGIN");
|
||||
cookieEntity.setUserId(0L);
|
||||
}
|
||||
|
||||
cookieEntity.setEnabled(true);
|
||||
cookieRepository.save(cookieEntity);
|
||||
|
||||
log.info("【QR Login】Cookie已保存到数据库: {}, 是否新账号: {}, 真实Cookie刷新: {}",
|
||||
accountId, isNewAccount, realCookieRefreshed);
|
||||
|
||||
// 5. 构建返回结果
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("account_id", accountId);
|
||||
result.put("is_new_account", isNewAccount);
|
||||
result.put("real_cookie_refreshed", realCookieRefreshed);
|
||||
result.put("cookie_length", finalCookieStr.length());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("【QR Login】处理扫码登录Cookie失败: {}", e.getMessage(), e);
|
||||
throw new RuntimeException("处理扫码登录Cookie失败: " + e.getMessage(), e);
|
||||
}*/
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析Cookie字符串为Map
|
||||
*/
|
||||
private Map<String, String> parseCookieString(String cookieStr) {
|
||||
Map<String, String> cookieMap = new HashMap<>();
|
||||
if (cookieStr != null && !cookieStr.isEmpty()) {
|
||||
String[] pairs = cookieStr.split(";\\s*");
|
||||
for (String pair : pairs) {
|
||||
String[] kv = pair.split("=", 2);
|
||||
if (kv.length == 2) {
|
||||
cookieMap.put(kv[0].trim(), kv[1].trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
return cookieMap;
|
||||
}
|
||||
}
|
||||
@ -15,12 +15,17 @@
|
||||
<dependency>
|
||||
<groupId>top.biwin</groupId>
|
||||
<artifactId>xianyu-common</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>top.biwin</groupId>
|
||||
<artifactId>xianyu-core</artifactId>
|
||||
<version>${project.version}</version>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>top.biwin</groupId>
|
||||
<artifactId>xianyu-goofish</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
||||
@ -15,7 +15,7 @@ import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
* @author wangli
|
||||
* @since 2026-01-20 23:51
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@SpringBootApplication(scanBasePackages = "top.biwin")
|
||||
@EntityScan(basePackages = "top.biwin.xianyu.core.entity")
|
||||
@EnableJpaRepositories(basePackages = "top.biwin.xianyu.core.repository")
|
||||
public class XianyuFreedomApplication {
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
package top.biwin.xinayu.server.config;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import okhttp3.ConnectionPool;
|
||||
import okhttp3.OkHttpClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* HTTP 客户端配置类
|
||||
* 提供 OkHttpClient 和 ObjectMapper 的 Bean 配置
|
||||
* <p>
|
||||
* 配置要点:
|
||||
* 1. OkHttpClient - 配置连接池、超时时间、重试策略
|
||||
* 2. ObjectMapper - JSON 序列化/反序列化
|
||||
* <p>
|
||||
* 性能优化:
|
||||
* - 连接池:最多 10 个空闲连接,保持 5 分钟
|
||||
* - 连接超时:10 秒
|
||||
* - 读取超时:30 秒
|
||||
* - 写入超时:30 秒
|
||||
* - 自动重试:启用
|
||||
* - 重定向跟随:启用
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-23
|
||||
*/
|
||||
@Configuration
|
||||
public class HttpClientConfig {
|
||||
|
||||
/**
|
||||
* OkHttpClient Bean 配置
|
||||
* <p>
|
||||
* 生产级配置项:
|
||||
* 1. ConnectionPool:连接池管理,提升性能,减少握手开销
|
||||
* - maxIdleConnections: 10(最大空闲连接数)
|
||||
* - keepAliveDuration: 5分钟(连接保持时间)
|
||||
* 2. Timeout 配置:
|
||||
* - connectTimeout: 10秒(连接超时)
|
||||
* - readTimeout: 30秒(读取超时)
|
||||
* - writeTimeout: 30秒(写入超时)
|
||||
* 3. 失败重试:retryOnConnectionFailure = true
|
||||
* 4. 重定向:followRedirects = true
|
||||
*
|
||||
* @return OkHttpClient 实例(单例)
|
||||
*/
|
||||
@Bean
|
||||
public OkHttpClient okHttpClient() {
|
||||
// 配置连接池:最多10个空闲连接,每个连接保持5分钟
|
||||
ConnectionPool connectionPool = new ConnectionPool(
|
||||
10, // 最大空闲连接数
|
||||
5, // 保持时间
|
||||
TimeUnit.MINUTES // 时间单位
|
||||
);
|
||||
|
||||
return new OkHttpClient.Builder()
|
||||
// 连接池配置
|
||||
.connectionPool(connectionPool)
|
||||
|
||||
// 超时配置
|
||||
.connectTimeout(10, TimeUnit.SECONDS) // 连接超时:10秒
|
||||
.readTimeout(30, TimeUnit.SECONDS) // 读取超时:30秒
|
||||
.writeTimeout(30, TimeUnit.SECONDS) // 写入超时:30秒
|
||||
|
||||
// 失败重试配置
|
||||
.retryOnConnectionFailure(true) // 连接失败时自动重试
|
||||
|
||||
// 重定向配置
|
||||
.followRedirects(true) // 自动跟随重定向
|
||||
.followSslRedirects(true) // 自动跟随 HTTPS 重定向
|
||||
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* ObjectMapper Bean 配置
|
||||
* 提供全局的 JSON 序列化/反序列化工具
|
||||
* <p>
|
||||
* 注意:Spring Boot 会自动配置 ObjectMapper,
|
||||
* 这里显式定义是为了确保在所有场景下都可用
|
||||
*
|
||||
* @return ObjectMapper 实例(单例)
|
||||
*/
|
||||
@Bean
|
||||
public ObjectMapper objectMapper() {
|
||||
return new ObjectMapper();
|
||||
}
|
||||
}
|
||||
@ -10,7 +10,7 @@ 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.entity.AdminUserEntity;
|
||||
import top.biwin.xianyu.core.repository.AdminUserRepository;
|
||||
import top.biwin.xinayu.common.dto.request.LoginCaptchaRequest;
|
||||
import top.biwin.xinayu.common.dto.request.LoginRequest;
|
||||
@ -135,7 +135,7 @@ public class AuthController {
|
||||
*/
|
||||
@PostMapping("/auth/send-verification-code")
|
||||
public ResponseEntity<SendCodeResponse> sendVerificationCode(@RequestBody SendCodeRequest request) {
|
||||
Optional<AdminUser> optUser = adminUserRepository.findByEmail(request.getEmail());
|
||||
Optional<AdminUserEntity> optUser = adminUserRepository.findByEmail(request.getEmail());
|
||||
|
||||
SendCodeResponse response = SendCodeResponse.builder().build();
|
||||
if (optUser.isEmpty()) {
|
||||
@ -234,10 +234,10 @@ public class AuthController {
|
||||
String username = jwtUtil.getUsernameFromToken(token);
|
||||
|
||||
// 从数据库查询用户信息
|
||||
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
|
||||
Optional<AdminUserEntity> userOpt = adminUserRepository.findByUsername(username);
|
||||
|
||||
if (userOpt.isPresent()) {
|
||||
AdminUser user = userOpt.get();
|
||||
AdminUserEntity user = userOpt.get();
|
||||
|
||||
// 返回用户信息
|
||||
response.put("authenticated", true);
|
||||
@ -296,8 +296,8 @@ public class AuthController {
|
||||
return ResponseEntity.ok(build);
|
||||
}
|
||||
|
||||
AdminUser adminUser = authService.verifyUserPassword(ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD);
|
||||
build.setUsingDefault( adminUser != null);
|
||||
AdminUserEntity adminUserEntity = authService.verifyUserPassword(ADMIN_USERNAME, DEFAULT_ADMIN_PASSWORD);
|
||||
build.setUsingDefault( adminUserEntity != null);
|
||||
return ResponseEntity.ok(build);
|
||||
}
|
||||
|
||||
|
||||
@ -5,9 +5,9 @@ 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.GoofishAccountEntity;
|
||||
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
|
||||
import top.biwin.xinayu.common.dto.response.CookieDetailsResponse;
|
||||
import top.biwin.xinayu.common.dto.response.GoofishAccountResponse;
|
||||
import top.biwin.xinayu.server.util.CurrentUserUtil;
|
||||
|
||||
import java.util.ArrayList;
|
||||
@ -22,7 +22,7 @@ import java.util.stream.Collectors;
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/goofish")
|
||||
public class GoofishController {
|
||||
public class GoofishAccountController {
|
||||
@Autowired
|
||||
private GoofishAccountRepository goofishAccountRepository;
|
||||
|
||||
@ -31,18 +31,18 @@ public class GoofishController {
|
||||
* 对应Python的 get_cookies_details 接口
|
||||
*/
|
||||
@GetMapping("/details")
|
||||
public ResponseEntity<List<CookieDetailsResponse>> getAllCookiesDetails() {
|
||||
List<GoofishAccount> goofishAccounts = new ArrayList<>();
|
||||
public ResponseEntity<List<GoofishAccountResponse>> getAllGoofishAccountDetails() {
|
||||
List<GoofishAccountEntity> goofishAccountEntities = new ArrayList<>();
|
||||
if (CurrentUserUtil.isSuperAdmin()) {
|
||||
goofishAccounts.addAll(goofishAccountRepository.findAll());
|
||||
goofishAccountEntities.addAll(goofishAccountRepository.findAll());
|
||||
} else {
|
||||
goofishAccountRepository.findByUserId(CurrentUserUtil.getCurrentUserId())
|
||||
.ifPresent(goofishAccounts::add);
|
||||
.ifPresent(goofishAccountEntities::add);
|
||||
}
|
||||
|
||||
// 构建详细信息响应
|
||||
return ResponseEntity.ok(goofishAccounts.stream().map(account -> {
|
||||
CookieDetailsResponse response = new CookieDetailsResponse();
|
||||
return ResponseEntity.ok(goofishAccountEntities.stream().map(account -> {
|
||||
GoofishAccountResponse response = new GoofishAccountResponse();
|
||||
response.setId(account.getId());
|
||||
response.setCookie(account.getCookie());
|
||||
response.setEnabled(account.getEnabled());
|
||||
@ -3,12 +3,16 @@ 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.PathVariable;
|
||||
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.entity.AiReplySettingEntity;
|
||||
import top.biwin.xianyu.core.entity.GoofishAccountEntity;
|
||||
import top.biwin.xianyu.core.entity.KeywordEntity;
|
||||
import top.biwin.xianyu.core.repository.AiReplySettingRepository;
|
||||
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
|
||||
import top.biwin.xianyu.core.repository.KeywordRepository;
|
||||
import top.biwin.xinayu.common.dto.response.KeywordResponse;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@ -28,16 +32,36 @@ public class KeywordController {
|
||||
private GoofishAccountRepository goofishAccountRepository;
|
||||
@Autowired
|
||||
private AiReplySettingRepository aiReplySettingRepository;
|
||||
@Autowired
|
||||
private KeywordRepository keywordRepository;
|
||||
|
||||
|
||||
@GetMapping("/ai-reply-settings")
|
||||
public ResponseEntity<Map<Long, AiReplySetting>> getAllAiSettings() {
|
||||
public ResponseEntity<Map<Long, AiReplySettingEntity>> getAllAiSettings() {
|
||||
List<Long> goofishIds = goofishAccountRepository.findAll().stream()
|
||||
.map(GoofishAccount::getId)
|
||||
.map(GoofishAccountEntity::getId)
|
||||
.toList();
|
||||
|
||||
List<AiReplySetting> settings = aiReplySettingRepository.findAllById(goofishIds);
|
||||
List<AiReplySettingEntity> settings = aiReplySettingRepository.findAllById(goofishIds);
|
||||
|
||||
return ResponseEntity.ok(settings.stream().collect(Collectors.toMap(AiReplySetting::getGoofishId, s -> s)));
|
||||
return ResponseEntity.ok(settings.stream().collect(Collectors.toMap(AiReplySettingEntity::getGoofishId, s -> s)));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/keywords-with-item-id/{gid}")
|
||||
public ResponseEntity<List<KeywordResponse>> getKeywordsWithItemId(@PathVariable Long gid) {
|
||||
|
||||
// 获取关键词列表
|
||||
List<KeywordEntity> keywordEntities = keywordRepository.findByGoofishId(gid);
|
||||
|
||||
// 转换为前端需要的格式(与 Python 实现一致)
|
||||
return ResponseEntity.ok(keywordEntities.stream()
|
||||
.map(k -> new KeywordResponse(k.getId(),
|
||||
k.getGoofishId(),
|
||||
k.getKeyword(),
|
||||
k.getReply(),
|
||||
k.getItemId(),
|
||||
k.getType(),
|
||||
k.getImageUrl()))
|
||||
.collect(Collectors.toList()));
|
||||
}
|
||||
}
|
||||
@ -6,8 +6,8 @@ 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.entity.GoofishAccountEntity;
|
||||
import top.biwin.xianyu.core.entity.OrderEntity;
|
||||
import top.biwin.xianyu.core.repository.GoofishAccountRepository;
|
||||
import top.biwin.xianyu.core.repository.OrderRepository;
|
||||
import top.biwin.xinayu.common.dto.response.OrderResponse;
|
||||
@ -33,12 +33,12 @@ public class OrderController {
|
||||
@GetMapping
|
||||
public ResponseEntity<List<OrderResponse>> getAllOrder() {
|
||||
List<Long> goofishIds = goofishAccountRepository.findAll().stream()
|
||||
.map(GoofishAccount::getId)
|
||||
.map(GoofishAccountEntity::getId)
|
||||
.toList();
|
||||
List<OrderResponse> result = new ArrayList<>();
|
||||
goofishIds.forEach(i -> {
|
||||
List<Order> order = orderRepository.findByGoofishId(i);
|
||||
result.addAll(BeanUtil.copyToList(order, OrderResponse.class));
|
||||
List<OrderEntity> orderEntity = orderRepository.findByGoofishId(i);
|
||||
result.addAll(BeanUtil.copyToList(orderEntity, OrderResponse.class));
|
||||
});
|
||||
return ResponseEntity.ok(result);
|
||||
}
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
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.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import top.biwin.xianyu.goofish.qrcode.QrLoginService;
|
||||
import top.biwin.xinayu.common.dto.response.QrLoginResponse;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-23 22:37
|
||||
*/
|
||||
@RestController
|
||||
public class QrLoginController {
|
||||
@Autowired
|
||||
private QrLoginService qrLoginService;
|
||||
|
||||
@PostMapping("/qr-login/generate")
|
||||
public ResponseEntity<QrLoginResponse> generateQrCode() {
|
||||
return ResponseEntity.ok(qrLoginService.generateQrCode());
|
||||
}
|
||||
|
||||
@GetMapping("/qr-login/check/{sessionId}")
|
||||
public Map<String, Object> checkQrCodeStatus(@PathVariable String sessionId) {
|
||||
return qrLoginService.checkQrCodeStatus(sessionId);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.SystemSetting;
|
||||
import top.biwin.xianyu.core.entity.SystemSettingEntity;
|
||||
import top.biwin.xianyu.core.repository.SystemSettingRepository;
|
||||
|
||||
import java.util.HashMap;
|
||||
@ -33,7 +33,7 @@ public class SystemSettingController {
|
||||
"login_captcha_enabled"
|
||||
);
|
||||
|
||||
List<SystemSetting> allHelper = systemSettingRepository.findAll();
|
||||
List<SystemSettingEntity> allHelper = systemSettingRepository.findAll();
|
||||
Map<String, String> result = new HashMap<>();
|
||||
|
||||
// 默认值 (Python logic)
|
||||
@ -41,7 +41,7 @@ public class SystemSettingController {
|
||||
result.put("show_default_login_info", "true");
|
||||
result.put("login_captcha_enabled", "true");
|
||||
|
||||
for (SystemSetting setting : allHelper) {
|
||||
for (SystemSettingEntity setting : allHelper) {
|
||||
if (publicKeys.contains(setting.getKey())) {
|
||||
result.put(setting.getKey(), setting.getValue());
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ 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.AdminUser;
|
||||
import top.biwin.xianyu.core.entity.AdminUserEntity;
|
||||
import top.biwin.xinayu.common.enums.UserRole;
|
||||
import top.biwin.xinayu.server.util.CurrentUserUtil;
|
||||
|
||||
@ -27,7 +27,7 @@ public class CurrentUserDemoController {
|
||||
*/
|
||||
@GetMapping("/full-info")
|
||||
public ResponseEntity<Map<String, Object>> getFullUserInfo() {
|
||||
AdminUser user = CurrentUserUtil.getCurrentUser();
|
||||
AdminUserEntity user = CurrentUserUtil.getCurrentUser();
|
||||
|
||||
if (user == null) {
|
||||
return ResponseEntity.ok(Map.of("message", "未登录"));
|
||||
@ -70,7 +70,7 @@ public class CurrentUserDemoController {
|
||||
*/
|
||||
@GetMapping("/personalized-greeting")
|
||||
public ResponseEntity<Map<String, Object>> personalizedGreeting() {
|
||||
AdminUser user = CurrentUserUtil.getCurrentUser();
|
||||
AdminUserEntity user = CurrentUserUtil.getCurrentUser();
|
||||
|
||||
if (user == null) {
|
||||
return ResponseEntity.ok(Map.of("greeting", "欢迎访客!"));
|
||||
@ -107,7 +107,7 @@ public class CurrentUserDemoController {
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
|
||||
// 方法1:获取完整对象
|
||||
AdminUser user = CurrentUserUtil.getCurrentUser();
|
||||
AdminUserEntity user = CurrentUserUtil.getCurrentUser();
|
||||
if (user != null) {
|
||||
response.put("method1_fullObject", Map.of(
|
||||
"description", "通过 getCurrentUser() 获取完整对象",
|
||||
|
||||
@ -8,7 +8,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
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.AdminUser;
|
||||
import top.biwin.xianyu.core.entity.AdminUserEntity;
|
||||
import top.biwin.xianyu.core.repository.AdminUserRepository;
|
||||
import top.biwin.xinayu.server.util.CurrentUserUtil;
|
||||
|
||||
@ -47,13 +47,13 @@ public class UserInfoController {
|
||||
}
|
||||
|
||||
// 根据用户名查询完整用户信息
|
||||
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
|
||||
Optional<AdminUserEntity> userOpt = adminUserRepository.findByUsername(username);
|
||||
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.ok(Map.of("message", "用户不存在"));
|
||||
}
|
||||
|
||||
AdminUser user = userOpt.get();
|
||||
AdminUserEntity user = userOpt.get();
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("userId", user.getId());
|
||||
response.put("username", user.getUsername());
|
||||
@ -81,13 +81,13 @@ public class UserInfoController {
|
||||
String username = userDetails.getUsername();
|
||||
|
||||
// 根据用户名查询完整用户信息
|
||||
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
|
||||
Optional<AdminUserEntity> userOpt = adminUserRepository.findByUsername(username);
|
||||
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.ok(Map.of("message", "用户不存在"));
|
||||
}
|
||||
|
||||
AdminUser user = userOpt.get();
|
||||
AdminUserEntity user = userOpt.get();
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("userId", user.getId());
|
||||
response.put("username", user.getUsername());
|
||||
@ -114,13 +114,13 @@ public class UserInfoController {
|
||||
String username = authentication.getName();
|
||||
|
||||
// 根据用户名查询完整用户信息
|
||||
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
|
||||
Optional<AdminUserEntity> userOpt = adminUserRepository.findByUsername(username);
|
||||
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.ok(Map.of("message", "用户不存在"));
|
||||
}
|
||||
|
||||
AdminUser user = userOpt.get();
|
||||
AdminUserEntity user = userOpt.get();
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("userId", user.getId());
|
||||
response.put("username", user.getUsername());
|
||||
@ -148,13 +148,13 @@ public class UserInfoController {
|
||||
String username = principal.getName();
|
||||
|
||||
// 根据用户名查询完整用户信息
|
||||
Optional<AdminUser> userOpt = adminUserRepository.findByUsername(username);
|
||||
Optional<AdminUserEntity> userOpt = adminUserRepository.findByUsername(username);
|
||||
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.ok(Map.of("message", "用户不存在"));
|
||||
}
|
||||
|
||||
AdminUser user = userOpt.get();
|
||||
AdminUserEntity user = userOpt.get();
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("userId", user.getId());
|
||||
response.put("username", user.getUsername());
|
||||
|
||||
@ -11,7 +11,7 @@ import org.springframework.jdbc.core.BeanPropertyRowMapper;
|
||||
import org.springframework.jdbc.core.JdbcTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import top.biwin.xianyu.core.entity.SystemSetting;
|
||||
import top.biwin.xianyu.core.entity.SystemSettingEntity;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
@ -32,10 +32,10 @@ public class DataInitializerLogger implements ApplicationRunner {
|
||||
@Override
|
||||
public void run(ApplicationArguments args) throws Exception {
|
||||
// 检查初始化标志
|
||||
List<SystemSetting> systemSettings = jdbcTemplate.query("SELECT `key`, `value` FROM system_settings WHERE `key` = 'init_system'", new BeanPropertyRowMapper<>(SystemSetting.class));
|
||||
SystemSetting systemSetting = systemSettings.isEmpty() ? null : systemSettings.get(0);
|
||||
List<SystemSettingEntity> systemSettingEntities = jdbcTemplate.query("SELECT `key`, `value` FROM system_settings WHERE `key` = 'init_system'", new BeanPropertyRowMapper<>(SystemSettingEntity.class));
|
||||
SystemSettingEntity systemSettingEntity = systemSettingEntities.isEmpty() ? null : systemSettingEntities.get(0);
|
||||
|
||||
if (Objects.isNull(systemSetting)) {
|
||||
if (Objects.isNull(systemSettingEntity)) {
|
||||
// 如果没有初始化过,则执行data.sql中的脚本
|
||||
Resource resource = resourceLoader.getResource("classpath:META-INF/init.sql");
|
||||
try (InputStream inputStream = resource.getInputStream()) {
|
||||
|
||||
@ -7,7 +7,7 @@ import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||
import org.springframework.stereotype.Service;
|
||||
import top.biwin.xianyu.core.entity.AdminUser;
|
||||
import top.biwin.xianyu.core.entity.AdminUserEntity;
|
||||
import top.biwin.xianyu.core.repository.AdminUserRepository;
|
||||
import top.biwin.xinayu.common.enums.UserRole;
|
||||
|
||||
@ -46,24 +46,24 @@ public class AdminUserDetailsService implements UserDetailsService {
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
// 1. 从数据库查询用户
|
||||
AdminUser adminUser = adminUserRepository.findByUsername(username)
|
||||
AdminUserEntity adminUserEntity = adminUserRepository.findByUsername(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("用户不存在: " + username));
|
||||
|
||||
// 2. 检查用户是否被禁用
|
||||
if (!adminUser.getIsActive()) {
|
||||
if (!adminUserEntity.getIsActive()) {
|
||||
throw new RuntimeException("用户已被禁用: " + username);
|
||||
}
|
||||
|
||||
// 3. 获取用户角色,如果为空则默认为 ADMIN
|
||||
UserRole role = adminUser.getRole() != null ? adminUser.getRole() : UserRole.ADMIN;
|
||||
UserRole role = adminUserEntity.getRole() != null ? adminUserEntity.getRole() : UserRole.ADMIN;
|
||||
|
||||
// 4. 将角色转换为 Spring Security 的 GrantedAuthority
|
||||
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role.getAuthority());
|
||||
|
||||
// 5. 构建并返回 UserDetails 对象
|
||||
return User.builder()
|
||||
.username(adminUser.getUsername())
|
||||
.password(adminUser.getPasswordHash())
|
||||
.username(adminUserEntity.getUsername())
|
||||
.password(adminUserEntity.getPasswordHash())
|
||||
.authorities(Collections.singletonList(authority))
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -9,7 +9,7 @@ import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import top.biwin.xianyu.core.entity.AdminUser;
|
||||
import top.biwin.xianyu.core.entity.AdminUserEntity;
|
||||
import top.biwin.xianyu.core.repository.AdminUserRepository;
|
||||
import top.biwin.xinayu.common.dto.request.LoginRequest;
|
||||
import top.biwin.xinayu.common.dto.response.LoginResponse;
|
||||
@ -62,7 +62,7 @@ public class AuthService {
|
||||
* @throws BadCredentialsException 认证失败时抛出
|
||||
*/
|
||||
public LoginResponse login(LoginRequest request) {
|
||||
AdminUser user = null;
|
||||
AdminUserEntity user = null;
|
||||
|
||||
if (StringUtils.hasText(request.getUsername()) && StringUtils.hasText(request.getPassword())) {
|
||||
// 方式1:用户名 + 密码登录
|
||||
@ -102,7 +102,7 @@ public class AuthService {
|
||||
* @param password 密码
|
||||
* @return AdminUser 用户实体
|
||||
*/
|
||||
private AdminUser authenticateByUsername(String username, String password) {
|
||||
private AdminUserEntity authenticateByUsername(String username, String password) {
|
||||
UsernamePasswordAuthenticationToken authToken =
|
||||
new UsernamePasswordAuthenticationToken(username, password);
|
||||
Authentication authentication = authenticationManager.authenticate(authToken);
|
||||
@ -119,8 +119,8 @@ public class AuthService {
|
||||
* @param password 密码
|
||||
* @return AdminUser 用户实体
|
||||
*/
|
||||
private AdminUser authenticateByEmail(String email, String password) {
|
||||
AdminUser user = adminUserRepository.findByEmail(email)
|
||||
private AdminUserEntity authenticateByEmail(String email, String password) {
|
||||
AdminUserEntity user = adminUserRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new BadCredentialsException("邮箱或密码错误"));
|
||||
|
||||
// 验证密码
|
||||
@ -143,14 +143,14 @@ public class AuthService {
|
||||
* @param code 验证码
|
||||
* @return AdminUser 用户实体
|
||||
*/
|
||||
private AdminUser authenticateByEmailCode(String email, String code) {
|
||||
private AdminUserEntity authenticateByEmailCode(String email, String code) {
|
||||
// 验证验证码
|
||||
if (!emailVerificationService.verifyCode(email, code)) {
|
||||
throw new BadCredentialsException("验证码无效或已过期");
|
||||
}
|
||||
|
||||
// 查询用户
|
||||
AdminUser user = adminUserRepository.findByEmail(email)
|
||||
AdminUserEntity user = adminUserRepository.findByEmail(email)
|
||||
.orElseThrow(() -> new BadCredentialsException("该邮箱未注册"));
|
||||
|
||||
// 检查账号状态
|
||||
@ -170,7 +170,7 @@ public class AuthService {
|
||||
* @param expiresIn 过期时间
|
||||
* @return LoginResponse
|
||||
*/
|
||||
private LoginResponse buildLoginResponse(AdminUser user, String accessToken,
|
||||
private LoginResponse buildLoginResponse(AdminUserEntity user, String accessToken,
|
||||
String refreshToken, Long expiresIn) {
|
||||
// 将 user 实体的信息映射到 LoginResponse 的各个字段
|
||||
|
||||
@ -221,8 +221,8 @@ public class AuthService {
|
||||
return new RefreshResponse(newAccessToken, expiresIn, "Bearer");
|
||||
}
|
||||
|
||||
public AdminUser verifyUserPassword(String username, String password) {
|
||||
AdminUser user = null;
|
||||
public AdminUserEntity verifyUserPassword(String username, String password) {
|
||||
AdminUserEntity user = null;
|
||||
try {
|
||||
user = authenticateByUsername(username, password);
|
||||
} catch (Exception e) {
|
||||
|
||||
@ -8,7 +8,7 @@ import org.springframework.mail.javamail.JavaMailSenderImpl;
|
||||
import org.springframework.scheduling.annotation.Async;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
import top.biwin.xianyu.core.entity.SystemSetting;
|
||||
import top.biwin.xianyu.core.entity.SystemSettingEntity;
|
||||
import top.biwin.xianyu.core.repository.SystemSettingRepository;
|
||||
|
||||
import java.util.Properties;
|
||||
@ -89,7 +89,7 @@ public class EmailService {
|
||||
*/
|
||||
private String getSettingValue(String key) {
|
||||
return systemSettingRepository.findByKey(key)
|
||||
.map(SystemSetting::getValue)
|
||||
.map(SystemSettingEntity::getValue)
|
||||
.orElse("");
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import top.biwin.xianyu.core.entity.EmailVerificationCode;
|
||||
import top.biwin.xianyu.core.entity.EmailVerificationCodeEntity;
|
||||
import top.biwin.xianyu.core.repository.EmailVerificationCodeRepository;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
@ -58,7 +58,7 @@ public class EmailVerificationService {
|
||||
@Transactional
|
||||
public void generateAndSendCode(String email) {
|
||||
// 防止频繁发送
|
||||
Optional<EmailVerificationCode> latestCode = codeRepository.findFirstByEmailOrderByCreatedAtDesc(email);
|
||||
Optional<EmailVerificationCodeEntity> latestCode = codeRepository.findFirstByEmailOrderByCreatedAtDesc(email);
|
||||
if (latestCode.isPresent()) {
|
||||
LocalDateTime lastSentTime = latestCode.get().getCreatedAt();
|
||||
long secondsSinceLastSent = java.time.Duration.between(lastSentTime, LocalDateTime.now()).getSeconds();
|
||||
@ -73,7 +73,7 @@ public class EmailVerificationService {
|
||||
String code = generateCode();
|
||||
|
||||
// 创建验证码记录
|
||||
EmailVerificationCode verificationCode = new EmailVerificationCode();
|
||||
EmailVerificationCodeEntity verificationCode = new EmailVerificationCodeEntity();
|
||||
verificationCode.setEmail(email);
|
||||
verificationCode.setCode(code);
|
||||
verificationCode.setExpiresAt(LocalDateTime.now().plusMinutes(expirationMinutes));
|
||||
@ -98,7 +98,7 @@ public class EmailVerificationService {
|
||||
public boolean verifyCode(String email, String code) {
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
Optional<EmailVerificationCode> verificationCodeOpt =
|
||||
Optional<EmailVerificationCodeEntity> verificationCodeOpt =
|
||||
codeRepository.findByEmailAndCodeAndIsUsedFalseAndExpiresAtAfter(email, code, now);
|
||||
|
||||
if (verificationCodeOpt.isEmpty()) {
|
||||
@ -107,7 +107,7 @@ public class EmailVerificationService {
|
||||
}
|
||||
|
||||
// 标记为已使用
|
||||
EmailVerificationCode verificationCode = verificationCodeOpt.get();
|
||||
EmailVerificationCodeEntity verificationCode = verificationCodeOpt.get();
|
||||
verificationCode.setIsUsed(true);
|
||||
codeRepository.save(verificationCode);
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ package top.biwin.xinayu.server.util;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import top.biwin.xianyu.core.entity.AdminUserEntity;
|
||||
|
||||
/**
|
||||
* 当前用户工具类
|
||||
@ -201,7 +202,7 @@ public class CurrentUserUtil {
|
||||
*
|
||||
* @return AdminUser 对象,如果未登录或用户不存在则返回 null
|
||||
*/
|
||||
public static top.biwin.xianyu.core.entity.AdminUser getCurrentUser() {
|
||||
public static AdminUserEntity getCurrentUser() {
|
||||
// 1. 获取当前用户名
|
||||
String username = getCurrentUsername();
|
||||
if (username == null) {
|
||||
@ -232,7 +233,7 @@ public class CurrentUserUtil {
|
||||
* @return 用户 ID,如果未登录则返回 null
|
||||
*/
|
||||
public static Long getCurrentUserId() {
|
||||
top.biwin.xianyu.core.entity.AdminUser user = getCurrentUser();
|
||||
AdminUserEntity user = getCurrentUser();
|
||||
return user != null ? user.getId() : null;
|
||||
}
|
||||
|
||||
@ -247,7 +248,7 @@ public class CurrentUserUtil {
|
||||
* @return 用户邮箱,如果未登录则返回 null
|
||||
*/
|
||||
public static String getCurrentUserEmail() {
|
||||
top.biwin.xianyu.core.entity.AdminUser user = getCurrentUser();
|
||||
AdminUserEntity user = getCurrentUser();
|
||||
return user != null ? user.getEmail() : null;
|
||||
}
|
||||
|
||||
@ -265,7 +266,7 @@ public class CurrentUserUtil {
|
||||
* @return 用户角色枚举,如果未登录则返回 null
|
||||
*/
|
||||
public static top.biwin.xinayu.common.enums.UserRole getCurrentUserRole() {
|
||||
top.biwin.xianyu.core.entity.AdminUser user = getCurrentUser();
|
||||
AdminUserEntity user = getCurrentUser();
|
||||
return user != null ? user.getRole() : null;
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user