chore: 更新Docker和CI配置

- 更新Woodpecker CI配置
- 更新Docker Compose配置
- 更新应用主类配置
- 更新网关路由服务
- 更新审计日志相关代码
This commit is contained in:
张翔
2026-04-15 23:38:03 +08:00
parent 38dc055a27
commit 60fb84e306
11 changed files with 398 additions and 204 deletions
+125 -71
View File
@@ -1,99 +1,153 @@
# Woodpecker CI/CD 流水线配置 # Woodpecker CI/CD 流水线配置 - 企业级质量门禁
# TDD工作流规范 - 质量门禁配置 # 基于Docker化部署的完整CI/CD流水线
pipeline: pipeline:
# 后端单元测试和集成测试 # 代码质量检查阶段
code-quality:
group: 质量检查
image: maven:3.9-openjdk-21
commands:
- echo "🔍 开始代码质量检查..."
- cd novalon-manage-api
- echo "📊 运行静态代码分析..."
- mvn spotbugs:check
- echo "📏 检查代码规范..."
- mvn checkstyle:check
- echo "📈 生成代码质量报告..."
- mvn pmd:check
- echo "✅ 代码质量检查完成"
when:
event: [push, pull_request]
# 后端测试阶段
test-backend: test-backend:
group: 后端测试
image: maven:3.9-openjdk-21 image: maven:3.9-openjdk-21
commands: commands:
- echo "🚀 开始后端测试..." - echo "🚀 开始后端测试..."
- cd novalon-manage-api - cd novalon-manage-api
- echo "🧪 运行单元测试..."
- mvn clean test jacoco:report - mvn clean test jacoco:report
- echo "✅ 后端测试完成,生成覆盖率报告" - echo "📊 生成测试覆盖率报告..."
- mvn jacoco:check
- echo "✅ 后端测试完成,覆盖率: $(cat target/site/jacoco/jacoco.xml | grep -oP 'lineCoverage=\"\K[0-9.]+')%"
when: when:
event: [push, pull_request] event: [push, pull_request]
# 构建后端JAR文件(用于E2E测试) # 前端测试阶段
build-backend-jar: test-frontend:
image: maven:3.9-openjdk-21 group: 前端测试
image: node:20
commands: commands:
- echo "📦 构建后端JAR文件..." - echo "🚀 开始前端测试..."
- cd novalon-manage-api/manage-app
- mvn clean package -DskipTests
- echo "✅ JAR文件构建完成: target/manage-app-1.0.0.jar"
when:
event: [push, pull_request]
# 前端单元测试
test-frontend-unit:
image: node:18
commands:
- echo "🚀 开始前端单元测试..."
- cd novalon-manage-web - cd novalon-manage-web
- echo "📦 安装依赖..."
- npm ci - npm ci
- echo "🧪 运行单元测试..."
- npm run test:unit - npm run test:unit
- echo "✅ 前端单元测试完成" - echo "📏 检查代码规范..."
- npm run lint
- echo "✅ 前端测试完成"
when: when:
event: [push, pull_request] event: [push, pull_request]
# 前端E2E测试 # Docker化构建阶段
test-frontend-e2e: docker-build:
image: mcr.microsoft.com/playwright:v1.40.0-jammy group: 容器化构建
environment: image: docker:24
- DISPLAY=:99 volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands: commands:
- echo "🚀 开始前端E2E测试..." - echo "🐳 开始Docker化构建..."
- echo "📦 构建后端镜像..."
- docker build -t novalon/backend:${CI_COMMIT_SHA:0:8} -f novalon-manage-api/Dockerfile ./novalon-manage-api
- echo "🌐 构建前端镜像..."
- docker build -t novalon/frontend:${CI_COMMIT_SHA:0:8} -f novalon-manage-web/Dockerfile ./novalon-manage-web
- echo "✅ Docker镜像构建完成"
when:
event: [push]
branch: [main, develop]
# 集成测试阶段(使用Docker Compose
integration-test:
group: 集成测试
image: docker:24
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- echo "🧪 开始集成测试..."
- echo "🐳 启动测试环境..."
- docker-compose -f docker-compose.test.yml up -d
- echo "⏳ 等待服务就绪..."
- sleep 60
- echo "🔍 检查服务健康状态..."
- curl -f http://localhost:8085/actuator/health || (docker-compose -f docker-compose.test.yml logs && exit 1)
- curl -f http://localhost:3002 || (docker-compose -f docker-compose.test.yml logs && exit 1)
- echo "✅ 集成测试环境就绪"
when:
event: [push]
branch: [main, develop]
# E2E测试阶段
e2e-test:
group: E2E测试
image: mcr.microsoft.com/playwright:v1.58.2-jammy
commands:
- echo "🎭 开始E2E测试..."
- cd novalon-manage-web - cd novalon-manage-web
- echo "📦 安装依赖..."
- npm ci - npm ci
- echo "🔧 安装浏览器..."
- npx playwright install --with-deps chromium - npx playwright install --with-deps chromium
- echo "🧪 运行E2E测试..."
- echo "📦 启动后端服务..." - npx playwright test --project=journeys --reporter=html,json,junit
- cd ../novalon-manage-api/manage-app
- java -jar target/manage-app-1.0.0.jar --spring.profiles.active=test &
- BACKEND_PID=$!
- cd ../../novalon-manage-web
- echo "⏳ 等待后端服务就绪..."
- |
for i in {1..60}; do
if curl -f http://localhost:8084/actuator/health > /dev/null 2>&1; then
echo "✅ 后端服务就绪"
break
fi
sleep 1
done
- echo "🎭 运行Playwright测试..."
- npx playwright test --project=chromium
- echo "🛑 停止后端服务..."
- kill $BACKEND_PID || true
- echo "✅ E2E测试完成" - echo "✅ E2E测试完成"
when: when:
event: [push, pull_request] event: [push]
branch: [main, develop]
# 质量门禁检查
quality-gates: # 安全扫描阶段
image: maven:3.9-openjdk-21 security-scan:
group: 安全扫描
image: aquasec/trivy:latest
commands: commands:
- echo "🔍 开始质量门禁检查..." - echo "🔒 开始安全扫描..."
- cd novalon-manage-api - echo "📊 扫描后端镜像..."
- mvn jacoco:check - trivy image novalon/backend:${CI_COMMIT_SHA:0:8}
- echo "✅ 测试覆盖率检查通过" - echo "📊 扫描前端镜像..."
- echo "✅ 所有测试用例通过" - trivy image novalon/frontend:${CI_COMMIT_SHA:0:8}
- echo "✅ 代码规范检查通过" - echo "✅ 安全扫描完成"
when: when:
event: [pull_request] event: [push]
branch: [main, develop]
# 构建阶段
build: # 部署阶段
image: maven:3.9-openjdk-21 deploy:
group: 部署
image: alpine:latest
commands: commands:
- echo "📦 开始构建..." - echo "🚀 开始部署..."
- cd novalon-manage-api - echo "📦 推送镜像到仓库..."
- mvn clean package -DskipTests - docker tag novalon/backend:${CI_COMMIT_SHA:0:8} ${DOCKER_REGISTRY}/novalon/backend:${CI_COMMIT_SHA:0:8}
- echo "✅ 构建成功" - docker tag novalon/frontend:${CI_COMMIT_SHA:0:8} ${DOCKER_REGISTRY}/novalon/frontend:${CI_COMMIT_SHA:0:8}
- docker push ${DOCKER_REGISTRY}/novalon/backend:${CI_COMMIT_SHA:0:8}
- docker push ${DOCKER_REGISTRY}/novalon/frontend:${CI_COMMIT_SHA:0:8}
- echo "✅ 部署完成"
when:
event: [push]
branch: [main]
# 清理阶段
cleanup:
group: 清理
image: docker:24
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- echo "🧹 开始清理..."
- docker-compose -f docker-compose.test.yml down -v
- docker system prune -f
- echo "✅ 清理完成"
when: when:
event: [push] event: [push]
branch: [main, develop] branch: [main, develop]
+83 -89
View File
@@ -1,118 +1,109 @@
version: '3.8' version: '3.8'
x-common-env: &common-env
TZ: Asia/Shanghai
LANG: zh_CN.UTF-8
services: services:
# PostgreSQL 数据库(用于测试 # PostgreSQL数据库服务(测试环境
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
container_name: novalon-test-db container_name: novalon-postgres-test
environment: environment:
POSTGRES_DB: novalon_test <<: *common-env
POSTGRES_USER: novalon POSTGRES_DB: manage_system_test
POSTGRES_PASSWORD: novalon123 POSTGRES_USER: novalon_test
POSTGRES_PASSWORD: novalon_test123
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=zh_CN.UTF-8"
ports: ports:
- "5432:5432" - "55433:5432"
volumes: volumes:
- postgres_test_data:/var/lib/postgresql/data - postgres_test_data:/var/lib/postgresql/data
- ./novalon-manage-api/manage-db/src/main/resources/db/migration:/docker-entrypoint-initdb.d
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U novalon -d novalon_test"] test: ["CMD-SHELL", "pg_isready -U novalon_test -d manage_system_test"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
networks:
- novalon-test-network
# Redis 缓存(可选)
redis:
image: redis:7-alpine
container_name: novalon-test-redis
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- novalon-test-network
# 后端服务
backend:
build:
context: ./novalon-manage-api
dockerfile: Dockerfile
container_name: novalon-test-backend
environment:
SPRING_PROFILES_ACTIVE: test
SPRING_R2DBC_URL: r2dbc:postgresql://postgres:5432/novalon_test
SPRING_R2DBC_USERNAME: novalon
SPRING_R2DBC_PASSWORD: novalon123
SPRING_FLYWAY_URL: jdbc:postgresql://postgres:5432/novalon_test
SPRING_FLYWAY_USER: novalon
SPRING_FLYWAY_PASSWORD: novalon123
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
ports:
- "8084:8084"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8084/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 60s
networks:
- novalon-test-network
# 网关服务
gateway:
build:
context: ./novalon-manage-api/manage-gateway
dockerfile: Dockerfile
container_name: novalon-test-gateway
environment:
SPRING_PROFILES_ACTIVE: test
BACKEND_SERVICE_URL: http://backend:8084
SPRING_DATA_REDIS_HOST: redis
SPRING_DATA_REDIS_PORT: 6379
ports:
- "8080:8080"
depends_on:
backend:
condition: service_healthy
redis:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 5
start_period: 30s start_period: 30s
networks: networks:
- novalon-test-network - novalon-test-network
# 前端服务(开发模式 # 后端API服务(测试环境
backend:
build:
context: ./novalon-manage-api
dockerfile: Dockerfile
args:
- BUILD_VERSION=test
container_name: novalon-backend-test
environment:
<<: *common-env
SPRING_PROFILES_ACTIVE: test
SPRING_R2DBC_URL: r2dbc:postgresql://postgres:5432/manage_system_test
SPRING_R2DBC_USERNAME: novalon_test
SPRING_R2DBC_PASSWORD: novalon_test123
SPRING_JACKSON_TIME_ZONE: Asia/Shanghai
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: health,info,metrics
ports:
- "8085:8084"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8084/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 60s
networks:
- novalon-test-network
# 前端Web服务(测试环境)
frontend: frontend:
build: build:
context: ./novalon-manage-web context: ./novalon-manage-web
dockerfile: Dockerfile.dev dockerfile: Dockerfile
container_name: novalon-test-frontend args:
environment: - BUILD_VERSION=test
VITE_API_BASE_URL: http://gateway:8080 container_name: novalon-frontend-test
ports: ports:
- "3002:3002" - "3002:80"
volumes:
- ./novalon-manage-web:/app
- /app/node_modules
depends_on: depends_on:
gateway: backend:
condition: service_healthy condition: service_healthy
environment:
<<: *common-env
- VITE_API_BASE_URL=http://backend:8084
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks: networks:
- novalon-test-network - novalon-test-network
# E2E测试服务
e2e-tests:
build:
context: ./novalon-manage-web
dockerfile: Dockerfile.playwright
container_name: novalon-e2e-tests
depends_on:
frontend:
condition: service_healthy
backend:
condition: service_healthy
environment:
<<: *common-env
TEST_BASE_URL: http://frontend:80
PLAYWRIGHT_HEADLESS: "true"
CI: "true"
networks:
- novalon-test-network
command: npm run test:e2e:journeys
volumes: volumes:
postgres_test_data: postgres_test_data:
driver: local driver: local
@@ -120,3 +111,6 @@ volumes:
networks: networks:
novalon-test-network: novalon-test-network:
driver: bridge driver: bridge
ipam:
config:
- subnet: 172.21.0.0/16
+57 -3
View File
@@ -1,37 +1,51 @@
version: '3.8' version: '3.8'
x-common-env: &common-env
TZ: Asia/Shanghai
LANG: zh_CN.UTF-8
services: services:
# PostgreSQL数据库服务 # PostgreSQL数据库服务
postgres: postgres:
image: postgres:15-alpine image: postgres:15-alpine
container_name: novalon-postgres container_name: novalon-postgres
environment: environment:
<<: *common-env
POSTGRES_DB: manage_system POSTGRES_DB: manage_system
POSTGRES_USER: novalon POSTGRES_USER: novalon
POSTGRES_PASSWORD: novalon123 POSTGRES_PASSWORD: novalon123
POSTGRES_INITDB_ARGS: "--encoding=UTF8 --locale=zh_CN.UTF-8"
ports: ports:
- "55432:5432" - "55432:5432"
volumes: volumes:
- postgres_data:/var/lib/postgresql/data - postgres_data:/var/lib/postgresql/data
- ./novalon-manage-api/manage-db/src/main/resources/db/migration:/docker-entrypoint-initdb.d
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U novalon -d manage_system"] test: ["CMD-SHELL", "pg_isready -U novalon -d manage_system"]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
start_period: 30s
networks: networks:
- novalon-network - novalon-network
restart: unless-stopped
# 后端API服务 # 后端API服务
backend: backend:
build: build:
context: ./novalon-manage-api context: ./novalon-manage-api
dockerfile: Dockerfile dockerfile: Dockerfile
args:
- BUILD_VERSION=${BUILD_VERSION:-latest}
container_name: novalon-backend container_name: novalon-backend
environment: environment:
<<: *common-env
SPRING_PROFILES_ACTIVE: docker SPRING_PROFILES_ACTIVE: docker
SPRING_R2DBC_URL: r2dbc:postgresql://postgres:5432/manage_system SPRING_R2DBC_URL: r2dbc:postgresql://postgres:5432/manage_system
SPRING_R2DBC_USERNAME: novalon SPRING_R2DBC_USERNAME: novalon
SPRING_R2DBC_PASSWORD: novalon123 SPRING_R2DBC_PASSWORD: novalon123
SPRING_JACKSON_TIME_ZONE: Asia/Shanghai
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: health,info,metrics
ports: ports:
- "8084:8084" - "8084:8084"
depends_on: depends_on:
@@ -42,15 +56,23 @@ services:
interval: 30s interval: 30s
timeout: 10s timeout: 10s
retries: 3 retries: 3
start_period: 40s start_period: 60s
networks: networks:
- novalon-network - novalon-network
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 前端Web服务 # 前端Web服务
frontend: frontend:
build: build:
context: ./novalon-manage-web context: ./novalon-manage-web
dockerfile: Dockerfile dockerfile: Dockerfile
args:
- BUILD_VERSION=${BUILD_VERSION:-latest}
container_name: novalon-frontend container_name: novalon-frontend
ports: ports:
- "3001:80" - "3001:80"
@@ -58,7 +80,8 @@ services:
backend: backend:
condition: service_healthy condition: service_healthy
environment: environment:
- VITE_API_BASE_URL=http://backend:8084 <<: *common-env
VITE_API_BASE_URL: http://backend:8084
healthcheck: healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"] test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 30s interval: 30s
@@ -67,11 +90,42 @@ services:
start_period: 40s start_period: 40s
networks: networks:
- novalon-network - novalon-network
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# Redis缓存服务(可选)
redis:
image: redis:7-alpine
container_name: novalon-redis
environment:
<<: *common-env
ports:
- "6379:6379"
command: redis-server --appendonly yes --requirepass novalon123
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 3s
retries: 5
networks:
- novalon-network
restart: unless-stopped
volumes: volumes:
postgres_data: postgres_data:
driver: local driver: local
redis_data:
driver: local
networks: networks:
novalon-network: novalon-network:
driver: bridge driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
+33 -6
View File
@@ -1,22 +1,49 @@
FROM maven:3.9-eclipse-temurin-17 AS builder # 多阶段构建优化Dockerfile
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app WORKDIR /app
# 复制Maven配置文件和源码
COPY pom.xml . COPY pom.xml .
COPY mvnw . COPY mvnw .
COPY mvnw.cmd . COPY mvnw.cmd .
COPY .mvn .mvn COPY .mvn .mvn
COPY src ./src
RUN chmod +x mvnw # 下载依赖(利用Docker缓存层)
RUN ./mvnw dependency:go-offline -B
# 复制源码并构建
COPY src ./src
RUN ./mvnw clean package -DskipTests RUN ./mvnw clean package -DskipTests
FROM openjdk:17-slim # 运行时镜像
FROM eclipse-temurin:21-jre-jammy
# 设置时区和语言环境
RUN apt-get update && apt-get install -y \
curl \
&& rm -rf /var/lib/apt/lists/*
# 创建非root用户运行应用
RUN groupadd -r novalon && useradd -r -g novalon novalon
WORKDIR /app WORKDIR /app
COPY --from=builder /app/target/*.jar app.jar # 复制构建产物
COPY --from=builder --chown=novalon:novalon /app/target/*.jar app.jar
# 设置JVM参数优化
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom"
# 暴露端口
EXPOSE 8084 EXPOSE 8084
ENTRYPOINT ["java", "-jar", "app.jar"] # 切换用户
USER novalon
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8084/actuator/health || exit 1
# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
@@ -9,9 +9,7 @@ import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
@SpringBootApplication(exclude = {ReactiveUserDetailsServiceAutoConfiguration.class}) @SpringBootApplication(scanBasePackages = "cn.novalon.manage", exclude = {ReactiveUserDetailsServiceAutoConfiguration.class})
@ConfigurationPropertiesScan(basePackages = "cn.novalon.manage")
@ComponentScan(basePackages = "cn.novalon.manage")
@EnableR2dbcRepositories(basePackages = {"cn.novalon.manage.db.dao", "cn.novalon.manage.sys.audit.repository"}) @EnableR2dbcRepositories(basePackages = {"cn.novalon.manage.db.dao", "cn.novalon.manage.sys.audit.repository"})
public class ManageApplication { public class ManageApplication {
@@ -20,6 +18,8 @@ public class ManageApplication {
public static void main(String[] args) { public static void main(String[] args) {
logger.info("应用程序启动中..."); logger.info("应用程序启动中...");
logger.info("包扫描路径: cn.novalon.manage"); logger.info("包扫描路径: cn.novalon.manage");
// 使用简单的启动方式,避免自动配置问题
SpringApplication.run(ManageApplication.class, args); SpringApplication.run(ManageApplication.class, args);
logger.info("应用程序启动完成"); logger.info("应用程序启动完成");
} }
@@ -20,7 +20,11 @@ spring:
password: ${DB_PASSWORD:postgres} password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
flyway: flyway:
enabled: false enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
baseline-version: 0
validate-on-migrate: true
security: security:
user: user:
name: disabled name: disabled
@@ -74,11 +74,9 @@ public class DynamicRouteService implements IDynamicRouteService {
logger.info("Adding route: {}", routeId); logger.info("Adding route: {}", routeId);
return routeDefinitionWriter.save(Mono.just(routeDefinition)) return routeDefinitionWriter.save(Mono.just(routeDefinition))
.then(Mono.fromRunnable(() -> { .then(Mono.fromRunnable(() -> routeCache.put(routeId, routeDefinition)))
routeCache.put(routeId, routeDefinition); .then(refreshRoutes())
refreshRoutes(); .then(Mono.fromRunnable(() -> logger.info("Route added successfully: {}", routeId)))
logger.info("Route added successfully: {}", routeId);
}))
.thenReturn(true) .thenReturn(true)
.onErrorResume(error -> { .onErrorResume(error -> {
logger.error("Failed to add route: {}", routeId, error); logger.error("Failed to add route: {}", routeId, error);
@@ -104,11 +102,9 @@ public class DynamicRouteService implements IDynamicRouteService {
return routeDefinitionWriter.delete(Mono.just(routeId)) return routeDefinitionWriter.delete(Mono.just(routeId))
.then(routeDefinitionWriter.save(Mono.just(routeDefinition))) .then(routeDefinitionWriter.save(Mono.just(routeDefinition)))
.then(Mono.fromRunnable(() -> { .then(Mono.fromRunnable(() -> routeCache.put(routeId, routeDefinition)))
routeCache.put(routeId, routeDefinition); .then(refreshRoutes())
refreshRoutes(); .then(Mono.fromRunnable(() -> logger.info("Route updated successfully: {}", routeId)))
logger.info("Route updated successfully: {}", routeId);
}))
.thenReturn(true) .thenReturn(true)
.onErrorResume(error -> { .onErrorResume(error -> {
logger.error("Failed to update route: {}", routeId, error); logger.error("Failed to update route: {}", routeId, error);
@@ -131,11 +127,9 @@ public class DynamicRouteService implements IDynamicRouteService {
logger.info("Deleting route: {}", routeId); logger.info("Deleting route: {}", routeId);
return routeDefinitionWriter.delete(Mono.just(routeId)) return routeDefinitionWriter.delete(Mono.just(routeId))
.then(Mono.fromRunnable(() -> { .then(Mono.fromRunnable(() -> routeCache.remove(routeId)))
routeCache.remove(routeId); .then(refreshRoutes())
refreshRoutes(); .then(Mono.fromRunnable(() -> logger.info("Route deleted successfully: {}", routeId)))
logger.info("Route deleted successfully: {}", routeId);
}))
.thenReturn(true) .thenReturn(true)
.onErrorResume(error -> { .onErrorResume(error -> {
logger.error("Failed to delete route: {}", routeId, error); logger.error("Failed to delete route: {}", routeId, error);
@@ -1,5 +1,6 @@
package cn.novalon.manage.gateway.route; package cn.novalon.manage.gateway.route;
import cn.novalon.manage.gateway.service.impl.DynamicRouteService;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -25,7 +26,7 @@ import static org.mockito.Mockito.*;
* 涉及业务:路由增删改查、路由刷新 * 涉及业务:路由增删改查、路由刷新
* *
* @author 张翔 * @author 张翔
* @date 2026-03-26 * @date 2026-04-14
*/ */
@ExtendWith(MockitoExtension.class) @ExtendWith(MockitoExtension.class)
class DynamicRouteServiceTest { class DynamicRouteServiceTest {
@@ -78,7 +79,15 @@ class DynamicRouteServiceTest {
@Test @Test
void testDeleteRoute_Success() { void testDeleteRoute_Success() {
String routeId = "test-route"; String routeId = "test-route";
RouteDefinition routeDefinition = createRouteDefinition(routeId);
// 先添加路由到缓存中
when(routeDefinitionWriter.save(any())).thenReturn(Mono.empty());
StepVerifier.create(dynamicRouteService.addRoute(routeDefinition))
.expectNext(true)
.verifyComplete();
// 然后删除路由
when(routeDefinitionWriter.delete(any())).thenReturn(Mono.empty()); when(routeDefinitionWriter.delete(any())).thenReturn(Mono.empty());
StepVerifier.create(dynamicRouteService.deleteRoute(routeId)) StepVerifier.create(dynamicRouteService.deleteRoute(routeId))
@@ -86,7 +95,7 @@ class DynamicRouteServiceTest {
.verifyComplete(); .verifyComplete();
verify(routeDefinitionWriter).delete(any()); verify(routeDefinitionWriter).delete(any());
verify(publisher).publishEvent(any(RefreshRoutesEvent.class)); verify(publisher, times(2)).publishEvent(any(RefreshRoutesEvent.class));
} }
@Test @Test
@@ -113,7 +122,7 @@ class DynamicRouteServiceTest {
.expectNext(true) .expectNext(true)
.verifyComplete(); .verifyComplete();
StepVerifier.create(dynamicRouteService.getAllRoutes().collectList()) StepVerifier.create(dynamicRouteService.getRoutes().collectList())
.assertNext(routes -> { .assertNext(routes -> {
assertNotNull(routes); assertNotNull(routes);
assertTrue(routes.size() >= 2); assertTrue(routes.size() >= 2);
@@ -131,7 +140,9 @@ class DynamicRouteServiceTest {
.expectNext(true) .expectNext(true)
.verifyComplete(); .verifyComplete();
assertTrue(dynamicRouteService.getRouteCount() >= 1); StepVerifier.create(dynamicRouteService.getRouteCount())
.assertNext(count -> assertTrue(count >= 1))
.verifyComplete();
} }
private RouteDefinition createRouteDefinition(String id) { private RouteDefinition createRouteDefinition(String id) {
@@ -47,9 +47,8 @@ public class AuditLogController {
public Flux<AuditLog> query(AuditLogQueryRequest request) { public Flux<AuditLog> query(AuditLogQueryRequest request) {
if (request.getEntityType() != null && request.getEntityId() != null) { if (request.getEntityType() != null && request.getEntityId() != null) {
return auditLogService.findByEntityTypeAndEntityId( return auditLogService.findByEntityTypeAndEntityId(
request.getEntityType(), request.getEntityType(),
request.getEntityId() request.getEntityId());
);
} else if (request.getEntityType() != null) { } else if (request.getEntityType() != null) {
return auditLogService.findByEntityType(request.getEntityType()); return auditLogService.findByEntityType(request.getEntityType());
} else if (request.getOperator() != null) { } else if (request.getOperator() != null) {
@@ -58,11 +57,10 @@ public class AuditLogController {
return auditLogService.findByOperationType(request.getOperationType()); return auditLogService.findByOperationType(request.getOperationType());
} else if (request.getStartTime() != null && request.getEndTime() != null) { } else if (request.getStartTime() != null && request.getEndTime() != null) {
return auditLogService.findByOperationTimeBetween( return auditLogService.findByOperationTimeBetween(
request.getStartTime(), request.getStartTime(),
request.getEndTime() request.getEndTime());
);
} }
return Flux.empty(); return Flux.empty();
} }
@@ -97,10 +95,8 @@ public class AuditLogController {
@GetMapping("/time-range") @GetMapping("/time-range")
@Operation(summary = "按时间范围查询", description = "根据时间范围查询审计日志") @Operation(summary = "按时间范围查询", description = "根据时间范围查询审计日志")
public Flux<AuditLog> findByTimeRange( public Flux<AuditLog> findByTimeRange(
@Parameter(description = "开始时间") @Parameter(description = "开始时间") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime,
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime startTime, @Parameter(description = "结束时间") @RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
@Parameter(description = "结束时间")
@RequestParam @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME) LocalDateTime endTime) {
return auditLogService.findByOperationTimeBetween(startTime, endTime); return auditLogService.findByOperationTimeBetween(startTime, endTime);
} }
@@ -108,7 +104,7 @@ public class AuditLogController {
@Operation(summary = "审计日志统计", description = "获取审计日志的统计信息") @Operation(summary = "审计日志统计", description = "获取审计日志的统计信息")
public Mono<AuditLogStatistics> getStatistics() { public Mono<AuditLogStatistics> getStatistics() {
AuditLogStatistics statistics = new AuditLogStatistics(); AuditLogStatistics statistics = new AuditLogStatistics();
return Mono.just(statistics); return Mono.just(statistics);
} }
@@ -138,4 +138,30 @@ public class AuditLog extends BaseDomain {
public void setDescription(String description) { public void setDescription(String description) {
this.description = description; this.description = description;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (!super.equals(o)) return false;
AuditLog auditLog = (AuditLog) o;
return java.util.Objects.equals(entityType, auditLog.entityType) &&
java.util.Objects.equals(entityId, auditLog.entityId) &&
java.util.Objects.equals(operationType, auditLog.operationType) &&
java.util.Objects.equals(operator, auditLog.operator) &&
java.util.Objects.equals(operationTime, auditLog.operationTime) &&
java.util.Objects.equals(beforeData, auditLog.beforeData) &&
java.util.Objects.equals(afterData, auditLog.afterData) &&
java.util.Arrays.equals(changedFields, auditLog.changedFields) &&
java.util.Objects.equals(ipAddress, auditLog.ipAddress) &&
java.util.Objects.equals(userAgent, auditLog.userAgent) &&
java.util.Objects.equals(description, auditLog.description);
}
@Override
public int hashCode() {
return java.util.Objects.hash(super.hashCode(), entityType, entityId, operationType, operator,
operationTime, beforeData, afterData, java.util.Arrays.hashCode(changedFields),
ipAddress, userAgent, description);
}
} }
@@ -100,4 +100,38 @@ public class AuditLogQueryRequest {
public void setSize(Integer size) { public void setSize(Integer size) {
this.size = size; this.size = size;
} }
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
AuditLogQueryRequest that = (AuditLogQueryRequest) o;
return java.util.Objects.equals(entityType, that.entityType) &&
java.util.Objects.equals(entityId, that.entityId) &&
java.util.Objects.equals(operationType, that.operationType) &&
java.util.Objects.equals(operator, that.operator) &&
java.util.Objects.equals(startTime, that.startTime) &&
java.util.Objects.equals(endTime, that.endTime) &&
java.util.Objects.equals(page, that.page) &&
java.util.Objects.equals(size, that.size);
}
@Override
public int hashCode() {
return java.util.Objects.hash(entityType, entityId, operationType, operator, startTime, endTime, page, size);
}
@Override
public String toString() {
return "AuditLogQueryRequest{" +
"entityType='" + entityType + '\'' +
", entityId=" + entityId +
", operationType='" + operationType + '\'' +
", operator='" + operator + '\'' +
", startTime=" + startTime +
", endTime=" + endTime +
", page=" + page +
", size=" + size +
'}';
}
} }