Compare commits

...

19 Commits

Author SHA1 Message Date
litianxiang
2daec6b123 买家端接口fegin适配 2026-05-18 14:54:18 +08:00
litianxiang
7e19ba4d06 登录鉴权按照Source判断id来自于何处 2026-05-13 09:40:31 +08:00
litianxiang
58951ff9b6 买家端需要的获取商家主页和模糊搜索接口 2026-05-11 16:40:45 +08:00
litianxiang
9124256f01 log配置 2026-05-07 13:39:45 +08:00
litianxiang
7f69eebedf Revert "选中状态fix"
This reverts commit e00e7f3e5e.
2026-05-07 11:39:12 +08:00
litianxiang
e00e7f3e5e 选中状态fix 2026-05-07 11:38:18 +08:00
litianxiang
a5c0695488 日志
订单表字段改名
视频返回新增字段
2026-05-06 16:58:47 +08:00
litianxiang
c8d1bc6985 新工作流 2026-05-06 15:03:52 +08:00
litianxiang
3b11905b55 ws配置 2026-05-06 13:42:07 +08:00
litianxiang
94379b9a16 cors 2026-05-06 10:39:55 +08:00
litianxiang
e642dbf041 cors 2026-05-06 09:50:24 +08:00
litianxiang
e7ef16b8ab cors 2026-05-06 09:50:11 +08:00
litianxiang
32bd7c7808 cors 2026-05-05 17:31:00 +08:00
litianxiang
a98ba4222c cors 2026-05-05 17:29:46 +08:00
litianxiang
472c349220 工作流测试 2026-05-05 17:24:30 +08:00
litianxiang
2ebad70036 工作流测试 2026-05-05 17:09:19 +08:00
litianxiang
5ed0a0a288 工作流测试 2026-05-05 17:00:53 +08:00
litianxiang
ffd45a4f43 cors配置 2026-05-05 16:54:16 +08:00
litianxiang
1b744635d6 cors配置 2026-05-05 16:53:30 +08:00
8 changed files with 202 additions and 99 deletions

View File

@@ -4,7 +4,8 @@ on:
jobs: jobs:
build_and_deploy: build_and_deploy:
runs-on: ubuntu-latest runs-on: java21
outputs: outputs:
build_status: ${{ job.status }} build_status: ${{ job.status }}
build_url: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }} build_url: ${{ gitea.server_url }}/${{ gitea.repository.owner.name }}/${{ gitea.repository.name }}/actions/runs/${{ gitea.run_id }}
@@ -26,27 +27,20 @@ jobs:
with: with:
ref: master ref: master
- name: 2.设置 JDK 21 + Maven + 依赖缓存
uses: actions/setup-java@v5
with:
java-version: '21'
distribution: 'temurin'
cache: 'maven'
- name: 3.构建jar包 - name: 3.缓存 Maven 依赖
uses: actions/cache@v5
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: 4.构建项目
run: | run: |
echo "===== 开始构建JAR包 =====" java -version
# 新增:打印当前构建分支(两种方式双重确认) mvn -v
echo "当前工作目录分支:$(git branch --show-current)" mvn clean package -DskipTests
echo "Gitea检出分支:${{ github.ref_name }}"
echo "预期构建分支: master"
echo "========================"
mvn -B clean install -DskipTests -Pdev 2>&1
# 检查构建是否成功
if [ $? -ne 0 ]; then
echo "JAR包构建失败!"
exit 1
fi
- name: 5.生成Dockerfile - name: 5.生成Dockerfile
run: | run: |
@@ -78,49 +72,40 @@ jobs:
- ./uploads:/temp/uploads - ./uploads:/temp/uploads
ports: ports:
- '10094:10094' - '10094:10094'
networks:
- aida_java_net
restart: always restart: always
networks:
aida_java_net:
external: true
name: aida_java_net
EOF EOF
# 验证docker-compose.yml生成 # 验证docker-compose.yml生成
echo "docker-compose.yml内容:" echo "docker-compose.yml内容:"
cat docker-compose.yml cat docker-compose.yml
- name: 7.安装SSH工具 - name: 7.上传jar到远程服务器
run: | uses: appleboy/scp-action@master
$SUDO apt install -y sshpass openssh-client --no-install-recommends with:
# 配置SSH免密 host: ${{ secrets.SERVER_HOST }}
mkdir -p ~/.ssh port: 22
echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa username: ${{ secrets.SERVER_USER }}
chmod 600 ~/.ssh/id_rsa key: ${{ secrets.SSH_KEY }}
ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts source: "target/*.jar,Dockerfile,docker-compose.yml"
target: ${{ env.REMOTE_DEPLOY_PATH }}
preserve_host_directory_structure: false
- name: 8.同步文件到远程服务 - name: 8. 重启 Docker 服务
run: | uses: appleboy/ssh-action@master # 👈 专门执行命令的 action
echo "===== 同步文件到远程服务器 =====" with:
# 使用scp同步文件 host: ${{ secrets.SERVER_HOST }}
scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \ username: ${{ secrets.SERVER_USER }}
./target/*.jar ./Dockerfile ./docker-compose.yml \ key: ${{ secrets.SSH_KEY }}
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }}:${{ env.REMOTE_DEPLOY_PATH }} 2>&1 key_base64: true
script: |
- name: 9.部署和运行服务 echo "========= 进入部署目录 ========="
run: |
echo "===== 开始部署服务 ====="
# SSH执行部署命令
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null \
${{ secrets.SERVER_USER }}@${{ secrets.SERVER_HOST }} << 'EOF_SSH'
cd ${{ env.REMOTE_DEPLOY_PATH }} cd ${{ env.REMOTE_DEPLOY_PATH }}
echo "停止旧容器..." ls -l
docker compose down || true
echo "构建镜像..." echo "========= 停止旧服务 ========="
docker compose build --no-cache docker compose down
echo "启动服务..."
docker compose up -d echo "========= 启动新服务 ========="
echo "验证容器状态..." docker compose up -d --build
echo "========= 查看运行状态 ========="
docker compose ps docker compose ps
echo "部署完成!"
EOF_SSH

1
.gitignore vendored
View File

@@ -1,2 +1,3 @@
/.idea/ /.idea/
/target/ /target/
/log/

22
pom.xml
View File

@@ -20,8 +20,8 @@
<properties> <properties>
<java.version>21</java.version> <java.version>21</java.version>
<spring-cloud.version>2023.0.1</spring-cloud.version> <spring-cloud.version>2023.0.4</spring-cloud.version>
<spring-cloud-alibaba.version>2023.0.1.0</spring-cloud-alibaba.version> <spring-cloud-alibaba.version>2023.0.3.4</spring-cloud-alibaba.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -62,17 +62,11 @@
<scope>runtime</scope> <scope>runtime</scope>
</dependency> </dependency>
<!-- Jackson --> <!-- Hutool -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<!-- Hutool (aligned with aida_seller 5.8.26) -->
<dependency> <dependency>
<groupId>cn.hutool</groupId> <groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId> <artifactId>hutool-all</artifactId>
<version>5.8.26</version> <version>5.8.23</version>
</dependency> </dependency>
<!-- Redis (for token blacklist) --> <!-- Redis (for token blacklist) -->
@@ -100,6 +94,12 @@
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
</dependency> </dependency>
<!-- Spring Boot Logging显式引入确保 logback 正确初始化) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
<!-- Knife4j Gateway Aggregation --> <!-- Knife4j Gateway Aggregation -->
<dependency> <dependency>
<groupId>com.github.xiaoymin</groupId> <groupId>com.github.xiaoymin</groupId>
@@ -145,6 +145,8 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId> <artifactId>spring-boot-maven-plugin</artifactId>
<configuration> <configuration>
<!-- 强制工作目录为模块根目录,确保 ./log 指向项目目录而非 Maven 安装目录 -->
<workingDirectory>${project.basedir}</workingDirectory>
<excludes> <excludes>
<exclude> <exclude>
<groupId>org.projectlombok</groupId> <groupId>org.projectlombok</groupId>

View File

@@ -1,18 +0,0 @@
package com.aida.gateway.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.config.CorsRegistry;
import org.springframework.web.reactive.config.WebFluxConfigurer;
@Configuration
public class CorsConfig implements WebFluxConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "PUT", "DELETE")
.maxAge(3600);
}
}

View File

@@ -19,5 +19,13 @@ public class GatewayAuthProperties {
private List<String> ignorePaths; private List<String> ignorePaths;
/**
* 可选认证路径token 有则解析并写入下游请求头,无则放行。
* 与 ignorePaths 的区别ignorePaths 完全跳过认证逻辑;
* optionalAuthPaths 仍然尝试解析 token有 token 时正常写入 X-User-Id / X-User-Info
* 无 token 时才放行,确保已登录用户的信息能正确传递。
*/
private List<String> optionalAuthPaths;
private boolean blacklistEnabled = true; private boolean blacklistEnabled = true;
} }

View File

@@ -11,8 +11,10 @@ import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.data.redis.core.ReactiveRedisTemplate; import org.springframework.data.redis.core.ReactiveRedisTemplate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -42,7 +44,12 @@ import org.springframework.beans.factory.annotation.Qualifier;
@Slf4j @Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class GlobalAuthWebFilter implements WebFilter { public class GlobalAuthWebFilter implements WebFilter, Ordered {
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE - 1;
}
private final GatewayAuthProperties authProperties; private final GatewayAuthProperties authProperties;
@Qualifier("reactiveRedisTemplate") @Qualifier("reactiveRedisTemplate")
@@ -54,29 +61,47 @@ public class GlobalAuthWebFilter implements WebFilter {
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath(); String path = exchange.getRequest().getURI().getPath();
// 1. 放过 OPTIONS 预检请求,由全局 CORS 配置处理
if ("OPTIONS".equalsIgnoreCase(exchange.getRequest().getMethod().name())) { if ("OPTIONS".equalsIgnoreCase(exchange.getRequest().getMethod().name())) {
return chain.filter(exchange); return chain.filter(exchange);
} }
// 2. 白名单直接放行 // 2. 白名单直接放行(完全跳过认证)
if (isIgnoredPath(path)) { if (isIgnoredPath(path)) {
return chain.filter(exchange); return chain.filter(exchange);
} }
// 3. 提取 token // 3. 可选认证路径token 有则解析,无则放行
if (isOptionalAuthPath(path)) {
String rawHeader = exchange.getRequest().getHeaders()
.getFirst(authProperties.getJwtTokenHeader());
if (StrUtil.isBlank(rawHeader)) {
// 无 token直接放行不写任何 header
return chain.filter(exchange);
}
// 有 token正常走解析流程复用下面的验证逻辑
return processTokenWithAuthCheck(exchange, chain, rawHeader);
}
// 4. 其他路径:必须有 token
String rawHeader = exchange.getRequest().getHeaders() String rawHeader = exchange.getRequest().getHeaders()
.getFirst(authProperties.getJwtTokenHeader()); .getFirst(authProperties.getJwtTokenHeader());
if (StrUtil.isBlank(rawHeader)) { if (StrUtil.isBlank(rawHeader)) {
return writeUnauthorized(exchange, AuthConstants.MSG_MISSING_TOKEN); return writeUnauthorized(exchange, AuthConstants.MSG_MISSING_TOKEN);
} }
return processTokenWithAuthCheck(exchange, chain, rawHeader);
}
/**
* 统一的 token 解析与认证流程:解析 JWT → 黑名单检查 → 写入下游 header。
* 专供可选认证路径中有 token 的情况,以及普通路径的鉴权。
*/
private Mono<Void> processTokenWithAuthCheck(ServerWebExchange exchange, WebFilterChain chain, String rawHeader) {
String token = rawHeader; String token = rawHeader;
if (rawHeader.startsWith(authProperties.getJwtTokenPrefix())) { if (rawHeader.startsWith(authProperties.getJwtTokenPrefix())) {
token = rawHeader.substring(authProperties.getJwtTokenPrefix().length()); token = rawHeader.substring(authProperties.getJwtTokenPrefix().length());
} }
// 4. JWT 签名验证 // JWT 签名验证
Claims claims; Claims claims;
try { try {
claims = parseToken(token); claims = parseToken(token);
@@ -85,7 +110,7 @@ public class GlobalAuthWebFilter implements WebFilter {
return writeUnauthorized(exchange, AuthConstants.MSG_TOKEN_EXPIRED); return writeUnauthorized(exchange, AuthConstants.MSG_TOKEN_EXPIRED);
} }
// 5. 解析用户信息 // 解析用户信息
AuthPrincipalVo principal; AuthPrincipalVo principal;
try { try {
principal = objectMapper.readValue(claims.getSubject(), AuthPrincipalVo.class); principal = objectMapper.readValue(claims.getSubject(), AuthPrincipalVo.class);
@@ -98,7 +123,7 @@ public class GlobalAuthWebFilter implements WebFilter {
return writeUnauthorized(exchange, AuthConstants.MSG_INVALID_TOKEN); return writeUnauthorized(exchange, AuthConstants.MSG_INVALID_TOKEN);
} }
// 6. 黑名单检查(仅当启用时) // 黑名单检查
if (authProperties.isBlacklistEnabled()) { if (authProperties.isBlacklistEnabled()) {
String blacklistKey = AuthConstants.BLACKLIST_PREFIX + principal.getId(); String blacklistKey = AuthConstants.BLACKLIST_PREFIX + principal.getId();
return redisTemplate.hasKey(blacklistKey).flatMap(isBlacklisted -> { return redisTemplate.hasKey(blacklistKey).flatMap(isBlacklisted -> {
@@ -145,6 +170,18 @@ public class GlobalAuthWebFilter implements WebFilter {
return false; return false;
} }
private boolean isOptionalAuthPath(String requestUri) {
if (authProperties.getOptionalAuthPaths() == null) {
return false;
}
for (String pattern : authProperties.getOptionalAuthPaths()) {
if (pathMatcher.match(pattern, requestUri)) {
return true;
}
}
return false;
}
private Claims parseToken(String token) { private Claims parseToken(String token) {
SecretKey key = buildSigningKey(); SecretKey key = buildSigningKey();
return Jwts.parser() return Jwts.parser()
@@ -166,6 +203,11 @@ public class GlobalAuthWebFilter implements WebFilter {
ServerHttpResponse response = exchange.getResponse(); ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED); response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON); response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
String origin = exchange.getRequest().getHeaders().getFirst(HttpHeaders.ORIGIN);
if (origin != null) {
response.getHeaders().set(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, origin);
response.getHeaders().set(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
}
String body = String.format("{\"code\":401,\"message\":\"%s\"}", message); String body = String.format("{\"code\":401,\"message\":\"%s\"}", message);
DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8)); DataBuffer buffer = response.bufferFactory().wrap(body.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Mono.just(buffer)); return response.writeWith(Mono.just(buffer));

View File

@@ -11,7 +11,6 @@ spring:
name: aida-gateway name: aida-gateway
cloud: cloud:
gateway: gateway:
# ---------- 全局 CORS 配置 ----------
globalcors: globalcors:
cors-configurations: cors-configurations:
'[/**]': '[/**]':
@@ -24,9 +23,17 @@ spring:
- OPTIONS - OPTIONS
- PATCH - PATCH
allowed-headers: "*" allowed-headers: "*"
allow-credentials: true
max-age: 3600 max-age: 3600
default-filters:
- DedupeResponseHeader=Access-Control-Allow-Origin Access-Control-Allow-Credentials
# ---------- 路由配置 ---------- # ---------- 路由配置 ----------
routes: routes:
# 多实例部署时推送会失效升级多实例要注意ws改造
- id: aida-back-websocket
uri: lb://aida-back
predicates:
- Path=/notification/**
- id: aida-back - id: aida-back
uri: lb://aida-back uri: lb://aida-back
predicates: predicates:
@@ -43,6 +50,12 @@ spring:
uri: http://18.167.251.121:9994 uri: http://18.167.251.121:9994
predicates: predicates:
- Path=/python/** - Path=/python/**
- id: aida-buyer
uri: lb://aida-buyer
predicates:
- Path=/buyer/**
filters:
- StripPrefix=1
# ---------- Knife4j 网关聚合配置 ---------- # ---------- Knife4j 网关聚合配置 ----------
knife4j: knife4j:
gateway: gateway:
@@ -60,6 +73,11 @@ knife4j:
service-name: aida-seller service-name: aida-seller
context-path: /seller context-path: /seller
order: 2 order: 2
- name: 买家端服务 (Buyer)
url: /buyer/v3/api-docs
service-name: aida-buyer
context-path: /buyer
order: 3
# ---------- Gateway JWT 认证gateway 独有) ---------- # ---------- Gateway JWT 认证gateway 独有) ----------
gateway: gateway:
@@ -136,8 +154,8 @@ gateway:
- /aida/api/alipay-hk/trade/notify - /aida/api/alipay-hk/trade/notify
- /aida/api/stripe/trade/notify - /aida/api/stripe/trade/notify
# Notification # Notification
- /aida/notification/** - /notification/**
# buyer
logging: - /buyer/account/**
level: - /buyer/designer/shop/**
com.aida.gateway: debug - /buyer/designer/search

View File

@@ -0,0 +1,65 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<include resource="org/springframework/boot/logging/logback/defaults.xml" />
<!-- 日志存放路径 -->
<property name="log.path" value="./log" />
<!-- 日志输出格式 -->
<property name="log.pattern.console" value="${CONSOLE_LOG_PATTERN:-%clr(%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<property name="log.pattern.file" value="${FILE_LOG_PATTERN:-%d{${LOG_DATEFORMAT_PATTERN:-yyyy-MM-dd HH:mm:ss.SSS}} ${LOG_LEVEL_PATTERN:-%5p} ${PID:- } --- [%15t] %-40.40logger{39} : %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}" />
<!-- 控制台输出 -->
<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>${log.pattern.console}</pattern>
</encoder>
</appender>
<!-- Info 日志文件 -->
<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/aida-gateway-info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/aida-gateway-info.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern.file}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- Error 日志文件 -->
<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>${log.path}/aida-gateway-error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/aida-gateway-error.%d{yyyy-MM-dd}.log</fileNamePattern>
<maxHistory>60</maxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern.file}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 服务模块日志级别控制 -->
<logger name="com.aida.gateway" level="debug" />
<!-- Spring 日志级别控制 -->
<logger name="org.springframework" level="warn" />
<root level="info">
<appender-ref ref="console" />
</root>
<root level="info">
<appender-ref ref="file_info" />
<appender-ref ref="file_error" />
</root>
</configuration>