refactor(project): Reorganize frontend and backend directories

This commit is contained in:
2026-03-18 10:45:15 +08:00
parent cc9eaf4cdf
commit 92a139e470
513 changed files with 28251 additions and 287 deletions
+9
View File
@@ -0,0 +1,9 @@
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY target/video-gateway-*.jar app.jar
EXPOSE 9000
ENTRYPOINT ["java", "-jar", "app.jar"]
+107
View File
@@ -0,0 +1,107 @@
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>video-platform</artifactId>
<groupId>com.nuaa.video</groupId>
<version>1.0.0</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>video-gateway</artifactId>
<dependencies>
<!-- Spring Cloud Gateway -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- 添加负载均衡依赖 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Sentinel Gateway适配 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Commons IO -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.15.1</version>
</dependency>
<!-- JJWT (JSON Web Token) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,13 @@
package com.nuaa.video.gateway;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
@SpringBootApplication
@EnableDiscoveryClient
public class GatewayApplication {
public static void main(String[] args) {
SpringApplication.run(GatewayApplication.class, args);
}
}
@@ -0,0 +1,30 @@
package com.nuaa.video.gateway.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;
/**
* 跨域配置
*/
@Configuration
public class CorsConfig {
@Bean
public CorsWebFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOriginPattern("*"); // 允许所有来源
config.addAllowedHeader("*"); // 允许所有请求头
config.addAllowedMethod("*"); // 允许所有方法
config.setAllowCredentials(true); // 允许携带凭证
config.setMaxAge(3600L); // 预检请求的有效期,单位为秒
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
source.registerCorsConfiguration("/**", config);
return new CorsWebFilter(source);
}
}
@@ -0,0 +1,70 @@
package com.nuaa.video.gateway.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.event.RefreshRoutesEvent;
import org.springframework.cloud.gateway.route.RouteDefinition;
import org.springframework.cloud.gateway.route.RouteDefinitionWriter;
import org.springframework.cloud.gateway.support.NotFoundException;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import jakarta.annotation.Resource;
/**
* 动态路由配置
*/
@Slf4j
@Component
public class DynamicRouteConfig implements ApplicationEventPublisherAware {
@Resource
private RouteDefinitionWriter routeDefinitionWriter;
private ApplicationEventPublisher publisher;
@Override
public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
this.publisher = applicationEventPublisher;
}
/**
* 添加路由
*/
public void addRoute(RouteDefinition definition) {
try {
routeDefinitionWriter.save(Mono.just(definition)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("成功添加路由: {}", definition.getId());
} catch (Exception e) {
log.error("添加路由失败: {}", definition.getId(), e);
}
}
/**
* 更新路由
*/
public void updateRoute(RouteDefinition definition) {
try {
this.deleteRoute(definition.getId());
this.addRoute(definition);
log.info("成功更新路由: {}", definition.getId());
} catch (Exception e) {
log.error("更新路由失败: {}", definition.getId(), e);
}
}
/**
* 删除路由
*/
public void deleteRoute(String id) {
try {
this.routeDefinitionWriter.delete(Mono.just(id)).subscribe();
this.publisher.publishEvent(new RefreshRoutesEvent(this));
log.info("成功删除路由: {}", id);
} catch (Exception e) {
log.error("删除路由失败: {}", id, e);
}
}
}
@@ -0,0 +1,78 @@
package com.nuaa.video.gateway.config;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayFlowRule;
import com.alibaba.csp.sentinel.adapter.gateway.common.rule.GatewayRuleManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.GatewayCallbackManager;
import com.alibaba.csp.sentinel.adapter.gateway.sc.callback.BlockRequestHandler;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.server.ServerResponse;
import jakarta.annotation.PostConstruct;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
/**
* 网关降级配置
*/
@Configuration
public class GatewayFallbackConfig {
/**
* 初始化自定义限流响应和规则
*/
@PostConstruct
public void doInit() {
// 自定义限流响应
initBlockHandlers();
// 初始化QPS限流规则
initQpsLimitRule();
}
/**
* 自定义限流响应
*/
private void initBlockHandlers() {
BlockRequestHandler blockRequestHandler = (serverWebExchange, throwable) -> {
Map<String, Object> response = new HashMap<>();
response.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
response.put("message", "请求过于频繁,请稍后再试");
return ServerResponse.status(HttpStatus.TOO_MANY_REQUESTS)
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(response));
};
GatewayCallbackManager.setBlockHandler(blockRequestHandler);
}
/**
* 初始化QPS限流规则
*/
private void initQpsLimitRule() {
Set<GatewayFlowRule> rules = new HashSet<>();
// 对每个API路径设置限流
// 用户服务限流
rules.add(new GatewayFlowRule("video-user")
.setCount(100)
.setIntervalSec(1));
// 内容服务限流
rules.add(new GatewayFlowRule("video-content")
.setCount(200)
.setIntervalSec(1));
// 认证服务限流
rules.add(new GatewayFlowRule("video-auth")
.setCount(50)
.setIntervalSec(1));
// 加载限流规则
GatewayRuleManager.loadRules(rules);
}
}
@@ -0,0 +1,16 @@
package com.nuaa.video.gateway.config;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;
@Configuration
public class LoadBalancerConfig {
@Bean
@LoadBalanced
public WebClient.Builder loadBalancedWebClientBuilder() {
return WebClient.builder();
}
}
@@ -0,0 +1,144 @@
package com.nuaa.video.gateway.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* JWT认证过滤器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class AuthGlobalFilter implements GlobalFilter, Ordered {
@Value("${jwt.secret:video-platform-secret-key-2024-nuaa}")
private String secret;
@Value("${jwt.token-header:Authorization}")
private String tokenHeader;
@Value("${gateway.ignore.urls:/**/auth/login,/**/auth/register,/**/auth/captcha,/content/video/public/**,/content/category/tree,/content/category/list,/comment/reply/public/**,/comment/public/**,/admin/auth/captcha,/admin/auth/login}")
private List<String> ignoreUrls;
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
// 跳过不需要验证的路径
if (isIgnorePath(path)) {
return chain.filter(exchange);
}
// 获取token
String token = getToken(request);
if (!StringUtils.hasText(token)) {
return unauthorized(exchange, "缺少token");
}
try {
// 验证token
Claims claims = parseToken(token);
// 将用户信息传递到下游服务
ServerHttpRequest.Builder mutate = request.mutate();
mutate.header("userId", claims.getSubject());
mutate.header("username", claims.get("username", String.class));
return chain.filter(exchange.mutate().request(mutate.build()).build());
} catch (Exception e) {
log.error("Token验证失败", e);
return unauthorized(exchange, "Token验证失败");
}
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
/**
* 检查是否为忽略路径
*/
private boolean isIgnorePath(String path) {
return ignoreUrls.stream().anyMatch(ignore -> pathMatcher.match(ignore, path));
}
/**
* 从请求中获取token
*/
private String getToken(ServerHttpRequest request) {
String token = request.getHeaders().getFirst(tokenHeader);
if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
return token.substring(7);
}
return null;
}
/**
* 解析token
*/
private Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(getSigningKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
/**
* 获取签名密钥
*/
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 返回未授权响应
*/
private Mono<Void> unauthorized(ServerWebExchange exchange, String message) {
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
Map<String, Object> result = new HashMap<>();
result.put("code", HttpStatus.UNAUTHORIZED.value());
result.put("message", message);
try {
byte[] bytes = objectMapper.writeValueAsBytes(result);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
} catch (JsonProcessingException e) {
log.error("响应JSON转换异常", e);
return response.setComplete();
}
}
}
@@ -0,0 +1,49 @@
package com.nuaa.video.gateway.filter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.List;
import java.util.Map;
/**
* 请求日志过滤器
*/
@Slf4j
@Component
public class RequestLogFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
String method = request.getMethod().name();
String host = request.getRemoteAddress().getHostString();
// 记录请求日志
log.info("Request: {} {} from {}", method, uri, host);
// 记录请求头信息(生产环境中可以根据需要调整日志级别)
if (log.isDebugEnabled()) {
Map<String, List<String>> headers = request.getHeaders();
headers.forEach((name, values) ->
log.debug("Header: {} = {}", name, String.join(",", values)));
}
// 执行后续过滤器
return chain.filter(exchange).then(Mono.fromRunnable(() ->
log.info("Response for: {} {}", method, uri)));
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE + 10;
}
}
@@ -0,0 +1,77 @@
package com.nuaa.video.gateway.handler;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode; // 导入HttpStatusCode
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
/**
* 全局异常处理器
*/
@Slf4j
@Order(-1)
@Component
@RequiredArgsConstructor
public class GlobalExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
// 设置响应的状态码
if (response.isCommitted()) {
return Mono.error(ex);
}
HttpStatusCode status; // 修改为HttpStatusCode类型
String message;
int statusCode; // 添加一个int类型的状态码变量
// 根据异常类型设置响应状态和消息
if (ex instanceof ResponseStatusException) {
ResponseStatusException responseStatusException = (ResponseStatusException) ex;
status = responseStatusException.getStatusCode();
message = responseStatusException.getReason();
statusCode = status.value(); // 获取状态码的数值
} else {
status = HttpStatus.INTERNAL_SERVER_ERROR;
message = "服务器内部错误";
statusCode = HttpStatus.INTERNAL_SERVER_ERROR.value();
}
log.error("网关异常: {}", message, ex);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
response.setStatusCode(status);
// 构建响应体
Map<String, Object> result = new HashMap<>();
result.put("code", statusCode); // 使用状态码数值
result.put("message", message);
try {
byte[] bytes = objectMapper.writeValueAsBytes(result);
DataBuffer buffer = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(buffer));
} catch (JsonProcessingException e) {
log.error("响应JSON转换异常", e);
return response.setComplete();
}
}
}
@@ -0,0 +1,109 @@
server:
port: 9000
spring:
application:
name: video-gateway
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
namespace: public
group: DEFAULT_GROUP
# 移除以下健康检查配置
# heart-beat-interval: 5000
# heart-beat-timeout: 15000
register-enabled: true
watch-delay: 30000
watch.enabled: true
loadbalancer:
ribbon:
enabled: false
cache:
enabled: true
caffeine:
spec: initialCapacity=500,expireAfterWrite=5s
# 添加重试机制
gateway:
discovery:
locator:
enabled: true
lower-case-service-id: true
url-expression: "'lb://'+serviceId"
# 添加重试配置
httpclient:
connect-timeout: 5000
response-timeout: 10000
routes:
# 保持原有路由配置不变,但移除StripPrefix过滤器
- id: video-auth
uri: lb://video-auth
predicates:
- Path=/auth/**
filters:
# 移除 StripPrefix=1 过滤器
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
backoff:
firstBackoff: 1000ms
maxBackoff: 5000ms
factor: 2
basedOnPreviousValue: false
- id: video-user
uri: lb://video-user
predicates:
- Path=/user/**
filters:
# 移除 StripPrefix=1 过滤器
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
- id: video-content
uri: lb://video-content
predicates:
- Path=/content/**
filters:
# 移除 StripPrefix=1 过滤器
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
- id: video-comment
uri: lb://video-comment
predicates:
- Path=/comment/**
filters:
# 移除 StripPrefix=1 过滤器
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
- id: video-admin
uri: lb://video-admin
predicates:
- Path=/admin/**
filters:
# 移除 StripPrefix=1 过滤器
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
- id: video-recommend
uri: lb://video-recommend
predicates:
- Path=/recommend/**
filters:
# 移除 StripPrefix=1 过滤器
- name: Retry
args:
retries: 3
statuses: BAD_GATEWAY,SERVICE_UNAVAILABLE
methods: GET,POST
@@ -0,0 +1,39 @@
server:
port: 9000
spring:
application:
name: video-gateway
profiles:
active: ${SPRING_PROFILES_ACTIVE:dev}
config:
import:
- optional:nacos:common.yaml?refreshEnabled=true
- optional:nacos:${spring.application.name}.yaml?refreshEnabled=true
cloud:
nacos:
discovery:
server-addr: ${NACOS_SERVER_ADDR:${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}}
namespace: ${NACOS_NAMESPACE:}
group: ${NACOS_GROUP:DEFAULT_GROUP}
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
ip: ${SPRING_CLOUD_NACOS_DISCOVERY_IP:}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
file-extension: yaml
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
gateway:
enabled: true
@@ -0,0 +1,26 @@
spring:
application:
name: video-gateway
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
namespace: public
group: DEFAULT_GROUP
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
# 确保服务能够被发现
ephemeral: true # 临时实例,适合容器环境
# 设置正确的注册IP
ip: ${GATEWAY_IP:}
port: ${server.port:9000}
# 增加重试次数
fail-fast: true
register-enabled: true
config:
server-addr: ${NACOS_HOST:nacos}:${NACOS_PORT:8848}
namespace: public
group: DEFAULT_GROUP
username: ${NACOS_USERNAME:nacos}
password: ${NACOS_PASSWORD:nacos}
file-extension: yaml
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml"/>
<include resource="org/springframework/boot/logging/logback/console-appender.xml"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
<!-- 将DEBUG日志级别改为INFO,减少日志输出 -->
<logger name="com.alibaba.nacos.client.naming" level="INFO"/>
<logger name="org.springframework.cloud.gateway" level="INFO"/>
<logger name="org.springframework.cloud.loadbalancer" level="INFO"/>
</configuration>