feat(admin): 添加用户管理相关文件

添加用户管理视图、API和状态管理文件
This commit is contained in:
张翔
2026-03-28 14:37:29 +08:00
commit 08ea5fbe98
1643 changed files with 255646 additions and 0 deletions
@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>io.destiny</groupId>
<artifactId>everything-is-suitable-api</artifactId>
<version>1.0.0</version>
</parent>
<artifactId>everything-is-suitable-client-api</artifactId>
<packaging>jar</packaging>
<name>Everything Is Suitable Client API</name>
<description>Client API layer for Everything Is Suitable API</description>
<dependencies>
<dependency>
<groupId>io.destiny</groupId>
<artifactId>everything-is-suitable-client</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.destiny</groupId>
<artifactId>everything-is-suitable-biz</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>io.destiny</groupId>
<artifactId>everything-is-suitable-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,59 @@
package io.destiny.client.api.config;
import io.destiny.client.api.handler.ClientAuthHandler;
import io.destiny.client.api.handler.UnifiedLoginHandler;
import org.springdoc.core.annotations.RouterOperation;
import org.springdoc.core.annotations.RouterOperations;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RequestPredicates.*;
import static org.springframework.web.reactive.function.server.RouterFunctions.route;
@Configuration
public class ClientRouterConfig {
@Bean
@RouterOperations({
@RouterOperation(path = "/client/auth/register", method = RequestMethod.POST, beanClass = ClientAuthHandler.class, beanMethod = "register"),
@RouterOperation(path = "/client/auth/logout/{userId}", method = RequestMethod.POST, beanClass = ClientAuthHandler.class, beanMethod = "logout"),
@RouterOperation(path = "/client/auth/status/{userId}", method = RequestMethod.GET, beanClass = ClientAuthHandler.class, beanMethod = "checkLoginStatus")
})
public RouterFunction<ServerResponse> clientAuthRouter(ClientAuthHandler clientAuthHandler) {
return route()
.path("/client/auth", builder -> builder
.POST("/register", accept(MediaType.APPLICATION_JSON),
clientAuthHandler::register)
.POST("/logout/{userId}", clientAuthHandler::logout)
.GET("/status/{userId}", clientAuthHandler::checkLoginStatus))
.build();
}
@Bean
@RouterOperations({
@RouterOperation(path = "/client/login/unified", method = RequestMethod.POST, beanClass = UnifiedLoginHandler.class, beanMethod = "unifiedLogin"),
@RouterOperation(path = "/client/login/password", method = RequestMethod.POST, beanClass = UnifiedLoginHandler.class, beanMethod = "passwordLogin"),
@RouterOperation(path = "/client/login/wechat", method = RequestMethod.POST, beanClass = UnifiedLoginHandler.class, beanMethod = "wechatLogin"),
@RouterOperation(path = "/client/login/douyin", method = RequestMethod.POST, beanClass = UnifiedLoginHandler.class, beanMethod = "douyinLogin"),
@RouterOperation(path = "/client/login/sms", method = RequestMethod.POST, beanClass = UnifiedLoginHandler.class, beanMethod = "smsLogin")
})
public RouterFunction<ServerResponse> unifiedLoginRouter(UnifiedLoginHandler unifiedLoginHandler) {
return route()
.path("/client/login", builder -> builder
.POST("/unified", accept(MediaType.APPLICATION_JSON),
unifiedLoginHandler::unifiedLogin)
.POST("/password", accept(MediaType.APPLICATION_JSON),
unifiedLoginHandler::passwordLogin)
.POST("/wechat", accept(MediaType.APPLICATION_JSON),
unifiedLoginHandler::wechatLogin)
.POST("/douyin", accept(MediaType.APPLICATION_JSON),
unifiedLoginHandler::douyinLogin)
.POST("/sms", accept(MediaType.APPLICATION_JSON),
unifiedLoginHandler::smsLogin))
.build();
}
}
@@ -0,0 +1,187 @@
package io.destiny.client.api.handler;
import io.destiny.client.core.domain.ClientUser;
import io.destiny.client.core.service.ClientUserAuthService;
import io.destiny.client.core.strategy.LoginStrategyProxy;
import io.destiny.client.dto.ClientUserLoginRequest;
import io.destiny.client.dto.ClientUserRegisterRequest;
import io.destiny.client.dto.ClientUserResponse;
import io.destiny.client.dto.LoginRequest;
import io.destiny.common.exception.ApplicationException;
import io.destiny.common.response.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Component
@Tag(name = "客户端认证", description = "客户端用户认证相关接口")
public class ClientAuthHandler {
private static final Logger logger = LoggerFactory.getLogger(ClientAuthHandler.class);
private final ClientUserAuthService clientUserAuthService;
private final LoginStrategyProxy loginStrategyProxy;
public ClientAuthHandler(ClientUserAuthService clientUserAuthService,
LoginStrategyProxy loginStrategyProxy) {
this.clientUserAuthService = clientUserAuthService;
this.loginStrategyProxy = loginStrategyProxy;
}
@Operation(summary = "用户注册", description = "新用户注册,需要提供用户名、密码、邮箱和手机号")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "注册成功", content = @Content(schema = @Schema(implementation = ClientUserResponse.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> register(ServerRequest request) {
return request.bodyToMono(ClientUserRegisterRequest.class)
.flatMap(registerRequest -> {
logger.info("Registering user: {}", registerRequest.getUsername());
ClientUser clientUser = registerRequest.toDomain();
return clientUserAuthService.register(clientUser)
.map(ClientUserResponse::from);
})
.map(response -> Result.success("User registered successfully", response))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(ApplicationException.class, e -> {
logger.error("Registration failed: {}", e.getMessage());
String code = e.getExceptionCode() != null ? e.getExceptionCode().getCode()
: "REGISTRATION_ERROR";
return ServerResponse.badRequest()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(Result.error(code, e.getMessage()));
})
.onErrorResume(Exception.class, e -> {
logger.error("Unexpected error during registration", e);
return ServerResponse.status(500)
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(Result.error("REGISTRATION_ERROR",
"Registration failed"));
});
}
@Operation(summary = "用户登录", description = "用户登录获取访问令牌和刷新令牌")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功", content = @Content(schema = @Schema(implementation = ClientUserResponse.class))),
@ApiResponse(responseCode = "401", description = "用户名或密码错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> login(ServerRequest request) {
return request.bodyToMono(ClientUserLoginRequest.class)
.map(loginRequest -> {
String clientIp = getClientIp(request);
loginRequest.setIpAddress(clientIp);
return loginRequest;
})
.flatMap(loginRequest -> {
logger.info("User login attempt: {} from IP: {}", loginRequest.getUsername(),
loginRequest.getIpAddress());
LoginRequest loginRequestWrapper = new LoginRequest();
loginRequestWrapper.setLoginType("password");
loginRequestWrapper.setIpAddress(loginRequest.getIpAddress());
Map<String, Object> data = new HashMap<>();
data.put("username", loginRequest.getUsername());
data.put("password", loginRequest.getPassword());
loginRequestWrapper.setData(data);
return loginStrategyProxy.login(loginRequestWrapper);
})
.map(response -> Result.success("Login successful", response))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(ApplicationException.class, e -> {
logger.error("Login failed: {}", e.getMessage());
String code = e.getExceptionCode() != null ? e.getExceptionCode().getCode()
: "LOGIN_ERROR";
return ServerResponse.status(401)
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(Result.error(code, e.getMessage()));
})
.onErrorResume(Exception.class, e -> {
logger.error("Unexpected error during login", e);
return ServerResponse.status(500)
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(Result.error("LOGIN_ERROR", "Login failed"));
});
}
private String getClientIp(ServerRequest request) {
String xForwardedFor = request.headers().firstHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.headers().firstHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
return request.remoteAddress()
.map(address -> address.getAddress().getHostAddress())
.orElse("unknown");
}
@Operation(summary = "用户登出", description = "用户登出,清除登录状态")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登出成功"),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> logout(ServerRequest request) {
Long userId = Long.parseLong(request.pathVariable("userId"));
logger.info("User logout: {}", userId);
return clientUserAuthService.logout(userId)
.then(Mono.defer(() -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(Result.success(null, "Logout successful"))))
.onErrorResume(Exception.class, e -> {
logger.error("Logout failed for user: {}", userId, e);
return ServerResponse.status(500)
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(Result.error("LOGOUT_ERROR", "Logout failed"));
});
}
@Operation(summary = "检查登录状态", description = "检查用户是否已登录")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "查询成功", content = @Content(schema = @Schema(implementation = Boolean.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> checkLoginStatus(ServerRequest request) {
Long userId = Long.parseLong(request.pathVariable("userId"));
logger.info("Checking login status for user: {}", userId);
return clientUserAuthService.checkLoginStatus(userId)
.map(isLoggedIn -> Result.success(isLoggedIn))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(Exception.class, e -> {
logger.error("Failed to check login status for user: {}", userId, e);
return ServerResponse.status(500)
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(Result.error("STATUS_CHECK_ERROR",
"Failed to check login status"));
});
}
}
@@ -0,0 +1,37 @@
package io.destiny.client.api.handler;
import io.destiny.biz.dto.DailyFortuneRequest;
import io.destiny.biz.dto.DailyFortuneResponse;
import io.destiny.biz.service.IFortuneAnalysisService;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
public class ClientFortuneHandler {
private final IFortuneAnalysisService fortuneService;
public ClientFortuneHandler(IFortuneAnalysisService fortuneService) {
this.fortuneService = fortuneService;
}
public Mono<ServerResponse> getDailyFortune(ServerRequest request) {
String userId = request.queryParam("userId").orElse("");
String date = request.queryParam("date").orElse("");
return ServerResponse.ok()
.bodyValue("{\"message\":\"Daily fortune - to be implemented\"}");
}
public Mono<ServerResponse> getMonthlyFortune(ServerRequest request) {
return ServerResponse.ok()
.bodyValue("{\"message\":\"Monthly fortune - to be implemented\"}");
}
public Mono<ServerResponse> getYearlyFortune(ServerRequest request) {
return ServerResponse.ok()
.bodyValue("{\"message\":\"Yearly fortune - to be implemented\"}");
}
}
@@ -0,0 +1,205 @@
package io.destiny.client.api.handler;
import io.destiny.client.core.exception.GlobalExceptionHandler;
import io.destiny.client.core.strategy.LoginStrategyProxy;
import io.destiny.client.dto.LoginRequest;
import io.destiny.client.dto.WechatLoginRequest;
import io.destiny.client.dto.DouyinLoginRequest;
import io.destiny.client.dto.SmsLoginRequest;
import io.destiny.client.dto.PasswordLoginRequest;
import io.destiny.common.response.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
@Component
@Tag(name = "统一登录", description = "统一登录相关接口")
public class UnifiedLoginHandler {
private static final Logger logger = LoggerFactory.getLogger(UnifiedLoginHandler.class);
private final LoginStrategyProxy loginStrategyProxy;
private final GlobalExceptionHandler globalExceptionHandler;
public UnifiedLoginHandler(LoginStrategyProxy loginStrategyProxy, GlobalExceptionHandler globalExceptionHandler) {
this.loginStrategyProxy = loginStrategyProxy;
this.globalExceptionHandler = globalExceptionHandler;
}
@Operation(summary = "统一登录", description = "支持多种登录方式的统一登录接口")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功", content = @Content(schema = @Schema(implementation = Result.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> unifiedLogin(ServerRequest request) {
return request.bodyToMono(LoginRequest.class)
.flatMap(loginRequest -> {
String clientIp = getClientIp(request);
loginRequest.setIpAddress(clientIp);
logger.info("Unified login request - Type: {}, IP: {}",
loginRequest.getLoginType(), clientIp);
return loginStrategyProxy.login(loginRequest);
})
.map(response -> Result.success("Login successful", response))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(ex -> {
try {
return globalExceptionHandler.handleException(ex, request.exchange());
} catch (IllegalStateException e) {
return globalExceptionHandler.handleException(ex, null);
}
});
}
@Operation(summary = "微信登录", description = "使用微信授权码登录")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功", content = @Content(schema = @Schema(implementation = Result.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> wechatLogin(ServerRequest request) {
return request.bodyToMono(WechatLoginRequest.class)
.flatMap(wechatRequest -> {
String clientIp = getClientIp(request);
LoginRequest loginRequest = new LoginRequest();
loginRequest.setLoginType("wechat");
loginRequest.setIpAddress(clientIp);
loginRequest.setData(wechatRequest);
logger.info("WeChat login request - IP: {}", clientIp);
return loginStrategyProxy.login(loginRequest);
})
.map(response -> Result.success("WeChat login successful", response))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(ex -> {
try {
return globalExceptionHandler.handleException(ex, request.exchange());
} catch (IllegalStateException e) {
return globalExceptionHandler.handleException(ex, null);
}
});
}
@Operation(summary = "抖音登录", description = "使用抖音授权码登录")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功", content = @Content(schema = @Schema(implementation = Result.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> douyinLogin(ServerRequest request) {
return request.bodyToMono(DouyinLoginRequest.class)
.flatMap(douyinRequest -> {
String clientIp = getClientIp(request);
LoginRequest loginRequest = new LoginRequest();
loginRequest.setLoginType("douyin");
loginRequest.setIpAddress(clientIp);
loginRequest.setData(douyinRequest);
logger.info("Douyin login request - IP: {}", clientIp);
return loginStrategyProxy.login(loginRequest);
})
.map(response -> Result.success("Douyin login successful", response))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(ex -> {
try {
return globalExceptionHandler.handleException(ex, request.exchange());
} catch (IllegalStateException e) {
return globalExceptionHandler.handleException(ex, null);
}
});
}
@Operation(summary = "短信验证码登录", description = "使用手机号和短信验证码登录")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功", content = @Content(schema = @Schema(implementation = Result.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> smsLogin(ServerRequest request) {
return request.bodyToMono(SmsLoginRequest.class)
.flatMap(smsRequest -> {
String clientIp = getClientIp(request);
LoginRequest loginRequest = new LoginRequest();
loginRequest.setLoginType("sms");
loginRequest.setIpAddress(clientIp);
loginRequest.setData(smsRequest);
logger.info("SMS login request - Phone: {}, IP: {}",
smsRequest.getPhone(), clientIp);
return loginStrategyProxy.login(loginRequest);
})
.map(response -> Result.success("SMS login successful", response))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(ex -> globalExceptionHandler.handleException(ex, request.exchange()));
}
@Operation(summary = "密码登录", description = "使用用户名和密码登录")
@ApiResponses(value = {
@ApiResponse(responseCode = "200", description = "登录成功", content = @Content(schema = @Schema(implementation = Result.class))),
@ApiResponse(responseCode = "400", description = "请求参数错误"),
@ApiResponse(responseCode = "500", description = "服务器内部错误")
})
public Mono<ServerResponse> passwordLogin(ServerRequest request) {
return request.bodyToMono(PasswordLoginRequest.class)
.flatMap(passwordRequest -> {
String clientIp = getClientIp(request);
LoginRequest loginRequest = new LoginRequest();
loginRequest.setLoginType("password");
loginRequest.setIpAddress(clientIp);
loginRequest.setData(passwordRequest);
logger.info("Password login request - Username: {}, IP: {}",
passwordRequest.getUsername(), clientIp);
return loginStrategyProxy.login(loginRequest);
})
.map(response -> Result.success("Password login successful", response))
.flatMap(result -> ServerResponse.ok()
.contentType(org.springframework.http.MediaType.APPLICATION_JSON)
.bodyValue(result))
.onErrorResume(ex -> globalExceptionHandler.handleException(ex, request.exchange()));
}
private String getClientIp(ServerRequest request) {
String xForwardedFor = request.headers().firstHeader("X-Forwarded-For");
if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
return xForwardedFor.split(",")[0].trim();
}
String xRealIp = request.headers().firstHeader("X-Real-IP");
if (xRealIp != null && !xRealIp.isEmpty()) {
return xRealIp;
}
return request.remoteAddress()
.map(address -> address.getAddress().getHostAddress())
.orElse("unknown");
}
}