feat(db): 迁移数据库迁移脚本 V1-V5(任务 T1.1) #5

Merged
zhangxiang merged 21 commits from dev into main 2026-05-01 21:59:04 +08:00
189 changed files with 1947 additions and 1723 deletions
Vendored
BIN
View File
Binary file not shown.
+13 -1
View File
@@ -165,4 +165,16 @@ nbdist/
.trae/
# git worktrees
.worktrees/
.worktrees/
# docs
docs/
# macOS specific
.DS_Store
Thumbs.db
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
-94
View File
@@ -1,94 +0,0 @@
# Woodpecker CI/CD 配置 - E2E/UAT测试集成
# 集成Python pytest测试套件
pipeline:
# E2E/UAT测试阶段
test-e2e-uat:
image: python:3.11
environment:
- BASE_URL=http://localhost:8084
- FRONTEND_URL=http://localhost:3000
- ENV=test
- DATABASE=h2
commands:
- echo "开始E2E/UAT测试..."
- cd test-suite
- pip install -r requirements.txt
- pip install pytest-xdist pytest-rerunfailures
- python3 run_tests.py --parallel --reruns 2 --coverage
- echo "✅ E2E/UAT测试完成"
when:
event: [push, pull_request]
# 生成测试报告
generate-report:
image: python:3.11
commands:
- echo "生成测试报告..."
- cd test-suite
- pip install -r requirements.txt
- pip install allure-pytest
- pytest tests/ --alluredir=allure-results
- echo "✅ 报告生成完成"
when:
event: [push, pull_request]
# 质量门禁
quality-gates:
image: python:3.11
commands:
- echo "开始质量门禁检查..."
- cd test-suite
- pip install -r requirements.txt
- pytest tests/ --cov=. --cov-report=term-missing --cov-fail-under=80
- echo "✅ 质量门禁检查通过"
when:
event: [pull_request]
# 工作流配置
workflows:
# 开发分支工作流
develop:
when:
event: [push]
branch: [develop]
steps:
- test-e2e-uat
- generate-report
# 主分支工作流
main:
when:
event: [push]
branch: [main]
steps:
- test-e2e-uat
- quality-gates
- generate-report
# Pull Request工作流
pull-request:
when:
event: [pull_request]
steps:
- test-e2e-uat
- quality-gates
# 通知配置
notifications:
slack:
webhook: ${SLACK_WEBHOOK_URL}
channel: '#ci-cd'
on_success: true
on_failure: true
# 环境变量
environment:
- PYTHONUNBUFFERED=1
- PYTHONDONTWRITEBYTECODE=1
# 缓存配置
cache:
paths:
- ~/.pip/cache
- test-suite/.pytest_cache
-155
View File
@@ -1,155 +0,0 @@
# Woodpecker CI/CD - 测试套件专用流水线
# 用途: 执行系统性的测试套件(E2E、UAT、性能、安全测试)
pipeline:
# 环境准备阶段
prepare:
image: python:3.11-slim
commands:
- echo "准备测试环境..."
- cd test-suite
- pip install -r requirements.txt
- echo "✅ 测试环境准备完成"
when:
event: [push, pull_request]
# 集成测试阶段
test-integration:
image: python:3.11-slim
commands:
- echo "开始集成测试..."
- cd test-suite
- pytest tests/integration/ -v --tb=short --cov=. --cov-report=xml --alluredir=allure-results/integration
- echo "✅ 集成测试完成"
when:
event: [push, pull_request]
# E2E测试阶段
test-e2e:
image: python:3.11-slim
commands:
- echo "开始E2E测试..."
- cd test-suite
- pytest tests/e2e/ -v --tb=short --cov=. --cov-report=xml --alluredir=allure-results/e2e -m "e2e"
- echo "✅ E2E测试完成"
when:
event: [push, pull_request]
# UAT验收测试阶段
test-uat:
image: python:3.11-slim
commands:
- echo "开始UAT验收测试..."
- cd test-suite
- pytest tests/uat/ -v --tb=short --cov=. --cov-report=xml --alluredir=allure-results/uat -m "uat"
- echo "✅ UAT测试完成"
when:
event: [push, pull_request]
# 性能测试阶段
test-performance:
image: python:3.11-slim
commands:
- echo "开始性能测试..."
- cd test-suite
- pytest tests/performance/ -v --tb=short --cov=. --cov-report=xml --alluredir=allure-results/performance -m "performance"
- echo "✅ 性能测试完成"
when:
event: [push]
branch: [main, develop]
# 安全测试阶段
test-security:
image: python:3.11-slim
commands:
- echo "开始安全测试..."
- cd test-suite
- pytest tests/security/ -v --tb=short --cov=. --cov-report=xml --alluredir=allure-results/security -m "security"
- echo "✅ 安全测试完成"
when:
event: [pull_request]
# 测试报告生成
generate-reports:
image: python:3.11-slim
commands:
- echo "生成测试报告..."
- cd test-suite
- mkdir -p reports
- cp -r htmlcov reports/
- cp -r allure-results reports/
- echo "✅ 测试报告生成完成"
when:
event: [push, pull_request]
status: [success, failure]
# 质量门禁检查
quality-gates:
image: python:3.11-slim
commands:
- echo "开始质量门禁检查..."
- cd test-suite
- |
# 检查测试覆盖率
if [ -f coverage.xml ]; then
coverage_percent=$(python -c "import xml.etree.ElementTree as ET; tree = ET.parse('coverage.xml'); root = tree.getroot(); print(float(root.attrib['line-rate']) * 100)")
echo "测试覆盖率: ${coverage_percent}%"
if (( $(echo "$coverage_percent < 80" | bc -l) )); then
echo "❌ 测试覆盖率不足80%"
exit 1
fi
fi
- echo "✅ 测试覆盖率检查通过"
- echo "✅ 所有测试用例通过"
- echo "✅ 质量门禁检查通过"
when:
event: [pull_request]
# 工作流配置
workflows:
# 完整测试工作流(主分支)
full-test:
when:
event: [push]
branch: [main, develop]
steps:
- prepare
- test-integration
- test-e2e
- test-uat
- test-performance
- generate-reports
# 快速测试工作流(Pull Request)
quick-test:
when:
event: [pull_request]
steps:
- prepare
- test-integration
- test-e2e
- test-uat
- test-security
- quality-gates
- generate-reports
# 通知配置
notifications:
slack:
webhook: ${SLACK_WEBHOOK_URL}
channel: '#test-reports'
on_success: true
on_failure: true
on_start: false
# 环境变量
environment:
- PYTHONPATH=/woodpecker/src/github.com/novalon/novalon-manage-system/test-suite
- TEST_ENV=ci
# 缓存配置
cache:
paths:
- test-suite/.pytest_cache
- test-suite/htmlcov
- test-suite/allure-results
-263
View File
@@ -1,263 +0,0 @@
# Woodpecker CI/CD 流水线配置 - 企业级质量门禁
# 基于Docker化部署的完整CI/CD流水线
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:
group: 后端测试
image: maven:3.9-openjdk-21
commands:
- echo "🚀 开始后端测试..."
- cd novalon-manage-api
- echo "🧪 运行单元测试..."
- mvn clean test jacoco:report
- echo "📊 生成测试覆盖率报告..."
- mvn jacoco:check
- echo "✅ 后端测试完成,覆盖率: $(cat target/site/jacoco/jacoco.xml | grep -oP 'lineCoverage=\"\K[0-9.]+')%"
when:
event: [push, pull_request]
# 前端测试阶段
test-frontend:
group: 前端测试
image: node:20
commands:
- echo "🚀 开始前端测试..."
- cd novalon-manage-web
- echo "📦 安装依赖..."
- npm ci
- echo "🧪 运行单元测试..."
- npm run test:unit
- echo "📏 检查代码规范..."
- npm run lint
- echo "✅ 前端测试完成"
when:
event: [push, pull_request]
# Docker化构建阶段
docker-build:
group: 容器化构建
image: docker:24
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- 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
- echo "📦 安装依赖..."
- npm ci
- echo "🔧 安装浏览器..."
- npx playwright install --with-deps chromium
- echo "🧪 运行E2E测试..."
- npx playwright test --project=journeys --reporter=html,json,junit
- echo "✅ E2E测试完成"
when:
event: [push]
branch: [main, develop]
# 安全扫描阶段
security-scan:
group: 安全扫描
image: aquasec/trivy:latest
commands:
- echo "🔒 开始安全扫描..."
- echo "📊 扫描后端镜像..."
- trivy image novalon/backend:${CI_COMMIT_SHA:0:8}
- echo "📊 扫描前端镜像..."
- trivy image novalon/frontend:${CI_COMMIT_SHA:0:8}
- echo "✅ 安全扫描完成"
when:
event: [push]
branch: [main, develop]
# 部署阶段
deploy:
group: 部署
image: alpine:latest
commands:
- echo "🚀 开始部署..."
- echo "📦 推送镜像到仓库..."
- docker tag novalon/backend:${CI_COMMIT_SHA:0:8} ${DOCKER_REGISTRY}/novalon/backend:${CI_COMMIT_SHA:0:8}
- 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:
event: [push]
branch: [main, develop]
# 安全扫描
security-scan:
image: aquasec/trivy:latest
commands:
- echo "🔒 开始安全漏洞扫描..."
- trivy filesystem --severity HIGH,CRITICAL --exit-code 1 .
- echo "✅ 安全扫描通过"
when:
event: [pull_request]
# 发布测试报告
publish-test-reports:
image: alpine:latest
commands:
- echo "📊 发布测试报告..."
- mkdir -p reports
- cp -r novalon-manage-api/target/site/jacoco reports/backend-coverage || true
- cp -r novalon-manage-web/playwright-report reports/e2e-report || true
- echo "✅ 测试报告已发布到 reports/"
when:
event: [push, pull_request]
status: [success, failure]
# 部署到测试环境
deploy-staging:
image: alpine/k8s:1.29
commands:
- echo "🚀 部署到测试环境..."
- kubectl apply -f k8s/staging/
- echo "✅ 测试环境部署完成"
when:
event: [push]
branch: [develop]
# 部署到生产环境
deploy-production:
image: alpine/k8s:1.29
commands:
- echo "🚀 部署到生产环境..."
- kubectl apply -f k8s/production/
- echo "✅ 生产环境部署完成"
when:
event: [push]
branch: [main]
# 工作流配置
workflows:
# 开发分支工作流
develop:
when:
event: [push]
branch: [develop]
steps:
- test-backend
- build-backend-jar
- test-frontend-unit
- test-frontend-e2e
- publish-test-reports
- build
- deploy-staging
# 主分支工作流
main:
when:
event: [push]
branch: [main]
steps:
- test-backend
- build-backend-jar
- test-frontend-unit
- test-frontend-e2e
- publish-test-reports
- security-scan
- build
- deploy-production
# Pull Request工作流
pull-request:
when:
event: [pull_request]
steps:
- test-backend
- build-backend-jar
- test-frontend-unit
- test-frontend-e2e
- publish-test-reports
- quality-gates
- security-scan
# 通知配置
notifications:
slack:
webhook: ${SLACK_WEBHOOK_URL}
channel: '#ci-cd'
on_success: true
on_failure: true
on_start: false
# 环境变量
environment:
- JAVA_HOME=/usr/lib/jvm/java-21-openjdk
- NODE_ENV=test
- SPRING_PROFILES_ACTIVE=test
# 缓存配置
cache:
paths:
- ~/.m2/repository
- novalon-manage-web/node_modules
+75
View File
@@ -0,0 +1,75 @@
# 全局 Agent 规则
本文件用于约束自动化代理在本机工作区中的默认工作方式,并将 Superpowers 作为主工作流体系按需激活。
## 指令优先级
- 默认以 **Superpowers** 作为主工作流体系,但不默认启用 full Superpowers。
- 只读分析任务可不进入完整实现流程,但结论必须清晰、可追溯。
- 若用户明确要求 `continue nonstop`,默认持续推进,直到满足验收标准或出现真实阻塞。
## 默认原则
### 最短路径与并行轻重分流
- 默认采用“满足质量要求的最短路径”。
- 能直接完成并验证的,不升级为更重流程。
- 能用轻量 planning 解决的小任务,不升级为重文档流程。
- 能用单一专项 skill 解决的问题,不扩展为 full Superpowers。
### 轻量任务默认策略(Codex / Superpowers
- 轻量任务:单文件或小范围修改、明确 bug 修复、配置 / 文案调整、小测试补充、局部
文档修改。
- 默认可跳过完整 `brainstorming``writing-plans``using-git-worktrees` 与重 review 链,直接实现并做定向验证;仅在关键不确定且无法从当前对话、项目上下文、`AGENTS.md`、现有代码回答时才提问。
- 总原则:将 Superpowers 视为可调节的工程纪律层——小任务走轻量路径,中任务保留简短 brainstorming 与短计划,大任务再启用完整流程。
### 流程升级 / 降级
- 升级到更重流程:影响边界超出初始判断、涉及公共 API / schema / 持久化 / 并发 /
共享逻辑、需求仍不清晰、验证覆盖不足、任务演变为中大型实现或重构。
- 降级到更轻流程:改动局部且边界清晰、不涉及共享核心逻辑、验证直接、补长计划或补
测试的成本明显高于收益、问题已收敛为单点修复。
### Step by Step Reasoning Workflow
### 执行原则
1. 先澄清,再实现;先缩小边界,再扩展范围。
2. 优先局部修改与最小充分实现,避免无关扩张。
3. 若复杂度上升,及时升级流程,而不是硬撑轻流程。
4. 若任务已收敛为局部改动,及时降级流程。
### 编码质量原则(Karpathy Guidelines
在编写、审查或重构代码时,遵循以下原则:
1. **编码前先思考** — 明确假设,不隐藏困惑,展示权衡
2. **简单优先** — 只写解决问题的最小代码,拒绝过度抽象
3. **精准修改** — 只触碰必须修改的部分,不"改进"相邻代码
4. **目标驱动执行** — 定义可验证的成功标准,循环直到验证通过
## 技能协同迭代项目工作流
### 技能组合策略
基于 Superpowers-ZH 技能框架,推荐以下技能协同组合用于复杂项目迭代:
#### 核心技能组合
- **gsd** - 综合性项目管理系统,适用于个人开发者使用 Claude 代理进行任务管理、进度跟踪和项目规划
- **gstack-workflow-assistant** - 工作流助手技能,提供结构化的工作流程支持,适用于团队协作、任务分配和项目分工管理
- **superpower-zh** - 技能框架,提供 27 个专业技能的集成管理
- **karpathy-guidelines** - 编码质量指南,避免过度复杂化,确保代码简洁有效
#### 协同工作流程
```
项目规划 (gsd) → 工作流管理 (gstack-workflow-assistant) → 技能执行 (superpower-zh) → 质量检查 (karpathy)
```
### 适用场景
- 复杂软件项目开发
- 需要严格质量控制的迭代过程
- 跨团队协作项目
- 长期维护的项目
### 使用建议
1. **项目启动阶段**:使用 gsd 进行项目规划和任务分解
2. **团队协作阶段**:使用 gstack-workflow-assistant 进行任务分配和分工管理
3. **开发执行阶段**:使用 superpower-zh 中的具体技能(如 TDD、代码审查等)
4. **质量保障阶段**:使用 karpathy 进行代码质量检查
### 技能安装与更新
- 所有技能已全局安装,支持 Trae、Trae CN 等 45 个代理
- 技能列表维护在 `.trae/rules/superpowers-zh.md`
- 定期使用 `npx skills check` 检查技能更新
### 协同优势
- **完整闭环**:形成项目管理→协作→执行→质量保障的完整开发闭环
- **质量保障**:通过技能协同确保代码质量和项目进度
- **效率提升**:系统化的工作流程减少重复劳动和错误
- **团队协作**:支持多人协作和任务分工管理
-21
View File
@@ -1,21 +0,0 @@
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
public class PasswordTest {
public static void main(String[] args) {
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
String hash = "$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C";
// 测试常见密码
String[] passwords = {"admin", "Admin@123", "Test@123", "password", "123456", "admin123"};
for (String password : passwords) {
boolean matches = encoder.matches(password, hash);
System.out.println(password + ": " + matches);
}
// 生成新的哈希
String newHash = encoder.encode("Test@123");
System.out.println("\nNew hash for 'Test@123': " + newHash);
}
}
+35 -2
View File
@@ -30,6 +30,39 @@ services:
- novalon-network
restart: unless-stopped
# 网关服务
gateway:
build:
context: ./novalon-manage-api
dockerfile: manage-gateway/Dockerfile
args:
- BUILD_VERSION=${BUILD_VERSION:-latest}
container_name: novalon-gateway
environment:
<<: *common-env
SPRING_PROFILES_ACTIVE: docker
USER_SERVICE_URL: http://backend:8084
SPRING_CLOUD_GATEWAY_ROUTES_0_URI: http://backend:8084
ports:
- "8080:8080"
depends_on:
backend:
condition: service_healthy
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- novalon-network
restart: unless-stopped
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
# 后端API服务
backend:
build:
@@ -77,11 +110,11 @@ services:
ports:
- "3001:80"
depends_on:
backend:
gateway:
condition: service_healthy
environment:
<<: *common-env
VITE_API_BASE_URL: http://backend:8084
VITE_API_BASE_URL: http://gateway:8080
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:80"]
interval: 30s
+3 -21
View File
@@ -1,22 +1,4 @@
# 多阶段构建优化Dockerfile
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /app
# 复制Maven配置文件和源码
COPY pom.xml .
COPY mvnw .
COPY mvnw.cmd .
COPY .mvn .mvn
# 下载依赖(利用Docker缓存层)
RUN ./mvnw dependency:go-offline -B
# 复制源码并构建
COPY src ./src
RUN ./mvnw clean package -DskipTests
# 运行时镜像
# 化Dockerfile - 使用本地编译好的jar文件
FROM eclipse-temurin:21-jre-jammy
# 设置时区和语言环境
@@ -30,7 +12,7 @@ RUN groupadd -r novalon && useradd -r -g novalon novalon
WORKDIR /app
# 复制构建产物
COPY --from=builder --chown=novalon:novalon /app/target/*.jar app.jar
COPY manage-app/target/manage-app-1.0.0.jar app.jar
# 设置JVM参数优化
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom"
@@ -46,4 +28,4 @@ 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"]
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
@@ -1,16 +1,24 @@
package cn.novalon.manage.app;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.web.server.WebFilter;
@SpringBootApplication(scanBasePackages = "cn.novalon.manage", exclude = {ReactiveUserDetailsServiceAutoConfiguration.class})
@EnableR2dbcRepositories(basePackages = {"cn.novalon.manage.db.dao", "cn.novalon.manage.sys.audit.repository"})
import java.util.List;
@SpringBootApplication(scanBasePackages = "cn.novalon.manage", exclude = {
ReactiveUserDetailsServiceAutoConfiguration.class })
@EnableR2dbcRepositories(basePackages = { "cn.novalon.manage.db.dao",
"cn.novalon.manage.sys.audit.repository" })
public class ManageApplication {
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
@@ -18,9 +26,32 @@ public class ManageApplication {
public static void main(String[] args) {
logger.info("应用程序启动中...");
logger.info("包扫描路径: cn.novalon.manage");
// 使用简单的启动方式,避免自动配置问题
SpringApplication.run(ManageApplication.class, args);
logger.info("应用程序启动完成");
}
@Bean
public CommandLineRunner checkWebFilters(List<WebFilter> webFilters) {
return args -> {
logger.info("=== 检查已注册的 WebFilter ===");
logger.info("WebFilter 总数: {}", webFilters.size());
for (WebFilter filter : webFilters) {
logger.info(" - {} (Order: {})",
filter.getClass().getName(),
filter.getClass().getAnnotation(org.springframework.core.annotation.Order.class) != null
? filter.getClass().getAnnotation(org.springframework.core.annotation.Order.class)
.value()
: "");
}
};
}
@Bean
public CommandLineRunner checkOperationLogService(IOperationLogService service) {
return args -> {
logger.info("=== 检查 IOperationLogService ===");
logger.info("IOperationLogService 实现: {}", service.getClass().getName());
};
}
}
@@ -0,0 +1,29 @@
package cn.novalon.manage.app.config;
import com.zaxxer.hikari.HikariDataSource;
import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import javax.sql.DataSource;
@Configuration
public class DataSourceConfig {
@Bean
@Primary
@ConfigurationProperties("spring.datasource")
public DataSourceProperties dataSourceProperties() {
return new DataSourceProperties();
}
@Bean
@Primary
public DataSource dataSource(DataSourceProperties properties) {
return properties.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}
}
@@ -3,6 +3,8 @@ package cn.novalon.manage.app.config;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
@@ -39,6 +41,12 @@ public class JacksonConfig {
javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(formatter));
objectMapper.registerModule(javaTimeModule);
SimpleModule longModule = new SimpleModule();
longModule.addSerializer(Long.class, ToStringSerializer.instance);
longModule.addSerializer(Long.TYPE, ToStringSerializer.instance);
objectMapper.registerModule(longModule);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
@@ -0,0 +1,25 @@
package cn.novalon.manage.app.config;
import io.r2dbc.spi.ConnectionFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.transaction.ReactiveTransactionManager;
import org.springframework.transaction.reactive.TransactionalOperator;
import org.springframework.r2dbc.connection.R2dbcTransactionManager;
@Configuration
public class TransactionManagerConfig {
@Bean(name = "connectionFactoryTransactionManager")
@Primary
public ReactiveTransactionManager reactiveTransactionManager(ConnectionFactory connectionFactory) {
return new R2dbcTransactionManager(connectionFactory);
}
@Bean
@Primary
public TransactionalOperator transactionalOperator(ReactiveTransactionManager reactiveTransactionManager) {
return TransactionalOperator.create(reactiveTransactionManager);
}
}
@@ -1,14 +1,26 @@
spring:
cache:
type: none
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
pool:
initial-size: 5
max-size: 20
max-idle-time: 10m
max-life-time: 30m
acquire-timeout: 3s
flyway:
enabled: true
locations: classpath:db/migration
baseline-on-migrate: true
validate-on-migrate: true
jwt:
secret: novalon-novalon-manage-jwt-secret-key-for-development-only-2026
expiration: 86400000
rate:
limit:
limit-for-period: 10000
@@ -6,7 +6,7 @@ spring:
r2dbc:
url: r2dbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
password: 123456
pool:
initial-size: 5
max-size: 20
@@ -16,7 +16,7 @@ spring:
datasource:
url: jdbc:postgresql://localhost:55432/manage_system
username: novalon
password: novalon123
password: 123456
driver-class-name: org.postgresql.Driver
flyway:
enabled: true
@@ -31,6 +31,10 @@ spring:
logging:
level:
cn.novalon.manage: DEBUG
cn.novalon.novalon.manage: DEBUG
cn.novalon.novalon.manage.sys.audit: DEBUG
org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG
org.flywaydb: DEBUG
org.flywaydb: DEBUG
debug: true
@@ -2,8 +2,17 @@ server:
port: 8084
spring:
aop:
proxy-target-class: true
application:
name: manage-app
main:
allow-bean-definition-overriding: true
cache:
type: none
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
r2dbc:
url: r2dbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:postgres}
@@ -65,4 +74,4 @@ springdoc:
operations-sorter: alpha
show-actuator: false
default-consumes-media-type: application/json
default-produces-media-type: application/json
default-produces-media-type: application/json
@@ -14,7 +14,10 @@ import reactor.test.StepVerifier;
* @author 张翔
* @date 2026-04-03
*/
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@SpringBootTest(
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
classes = cn.novalon.manage.app.ManageApplication.class
)
@ActiveProfiles("test")
class ManualTableCreationTest {
@@ -25,7 +28,7 @@ class ManualTableCreationTest {
void setUp() {
r2dbcEntityTemplate.getDatabaseClient()
.sql("CREATE TABLE IF NOT EXISTS operation_log (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
"id BIGSERIAL PRIMARY KEY, " +
"username VARCHAR(50), " +
"operation VARCHAR(100), " +
"method VARCHAR(200), " +
@@ -1,36 +0,0 @@
package cn.novalon.manage.common.config;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
/**
* 缓存配置类
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(caffeineCacheBuilder());
return cacheManager;
}
private Caffeine<Object, Object> caffeineCacheBuilder() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(500)
.expireAfterWrite(30, TimeUnit.MINUTES)
.recordStats();
}
}
@@ -1,2 +1 @@
cn.novalon.manage.common.config.CacheConfig
cn.novalon.manage.common.config.JwtProperties
cn.novalon.manage.common.config.JwtProperties
@@ -2,6 +2,7 @@ package cn.novalon.manage.db.converter;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.db.entity.AuditLogEntity;
import io.r2dbc.postgresql.codec.Json;
import org.springframework.stereotype.Component;
@@ -28,8 +29,8 @@ public class AuditLogConverter {
domain.setOperationType(entity.getOperationType());
domain.setOperator(entity.getOperator());
domain.setOperationTime(entity.getOperationTime());
domain.setBeforeData(entity.getBeforeData());
domain.setAfterData(entity.getAfterData());
domain.setBeforeData(entity.getBeforeData() != null ? entity.getBeforeData().asString() : null);
domain.setAfterData(entity.getAfterData() != null ? entity.getAfterData().asString() : null);
domain.setChangedFields(entity.getChangedFields());
domain.setIpAddress(entity.getIpAddress());
domain.setUserAgent(entity.getUserAgent());
@@ -53,8 +54,8 @@ public class AuditLogConverter {
entity.setOperationType(domain.getOperationType());
entity.setOperator(domain.getOperator());
entity.setOperationTime(domain.getOperationTime());
entity.setBeforeData(domain.getBeforeData());
entity.setAfterData(domain.getAfterData());
entity.setBeforeData(domain.getBeforeData() != null ? Json.of(domain.getBeforeData()) : null);
entity.setAfterData(domain.getAfterData() != null ? Json.of(domain.getAfterData()) : null);
entity.setChangedFields(domain.getChangedFields());
entity.setIpAddress(domain.getIpAddress());
entity.setUserAgent(domain.getUserAgent());
@@ -1,5 +1,6 @@
package cn.novalon.manage.db.entity;
import io.r2dbc.postgresql.codec.Json;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
@@ -28,10 +29,10 @@ public class AuditLogEntity extends BaseEntity {
private java.time.LocalDateTime operationTime;
@Column("before_data")
private String beforeData;
private Json beforeData;
@Column("after_data")
private String afterData;
private Json afterData;
@Column("changed_fields")
private String[] changedFields;
@@ -85,19 +86,19 @@ public class AuditLogEntity extends BaseEntity {
this.operationTime = operationTime;
}
public String getBeforeData() {
public Json getBeforeData() {
return beforeData;
}
public void setBeforeData(String beforeData) {
public void setBeforeData(Json beforeData) {
this.beforeData = beforeData;
}
public String getAfterData() {
public Json getAfterData() {
return afterData;
}
public void setAfterData(String afterData) {
public void setAfterData(Json afterData) {
this.afterData = afterData;
}
@@ -5,17 +5,12 @@ import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Persistable;
import org.springframework.data.relational.core.mapping.Column;
import java.time.LocalDateTime;
/**
* 数据库实体基类
*
* @author 张翔
* @date 2026-03-13
*/
public abstract class BaseEntity implements Persistable<Long> {
@Id
@@ -40,6 +35,9 @@ public abstract class BaseEntity implements Persistable<Long> {
@Column("deleted_at")
private LocalDateTime deletedAt;
@Transient
private boolean newEntity = true;
@Override
public Long getId() {
return id;
@@ -89,12 +87,16 @@ public abstract class BaseEntity implements Persistable<Long> {
this.deletedAt = deletedAt;
}
/**
* 判断实体是否为新的
* 如果createdAt为null,则认为是新实体
*/
@Override
public boolean isNew() {
return createdAt == null;
return newEntity;
}
public void markNotNew() {
this.newEntity = false;
}
public void markNew() {
this.newEntity = true;
}
}
@@ -7,6 +7,7 @@ import cn.novalon.manage.db.dao.AuditLogDao;
import cn.novalon.manage.db.entity.AuditLogEntity;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -26,10 +27,12 @@ public class AuditLogRepository implements IAuditLogRepository {
private final AuditLogDao auditLogDao;
private final AuditLogConverter auditLogConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public AuditLogRepository(AuditLogDao auditLogDao, AuditLogConverter auditLogConverter) {
public AuditLogRepository(AuditLogDao auditLogDao, AuditLogConverter auditLogConverter, R2dbcEntityTemplate r2dbcEntityTemplate) {
this.auditLogDao = auditLogDao;
this.auditLogConverter = auditLogConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
@@ -41,6 +44,12 @@ public class AuditLogRepository implements IAuditLogRepository {
@Override
public Mono<AuditLog> save(AuditLog auditLog) {
AuditLogEntity entity = auditLogConverter.toEntity(auditLog);
if (entity.isNew()) {
return r2dbcEntityTemplate.insert(AuditLogEntity.class)
.using(entity)
.doOnNext(e -> e.markNotNew())
.map(auditLogConverter::toDomain);
}
return auditLogDao.save(entity)
.map(auditLogConverter::toDomain);
}
@@ -49,6 +49,12 @@ public class OperationLogRepository implements IOperationLogRepository {
@Override
public Mono<OperationLog> save(OperationLog operationLog) {
OperationLogEntity entity = operationLogConverter.toEntity(operationLog);
if (entity.isNew()) {
return r2dbcEntityTemplate.insert(OperationLogEntity.class)
.using(entity)
.doOnNext(e -> e.markNotNew())
.map(operationLogConverter::toDomain);
}
return operationLogDao.save(entity)
.map(operationLogConverter::toDomain);
}
@@ -60,6 +60,20 @@ public class SysMenuRepository implements ISysMenuRepository {
@Override
public Mono<SysMenu> save(SysMenu sysMenu) {
SysMenuEntity entity = sysMenuConverter.toEntity(sysMenu);
if (entity.isNew()) {
return r2dbcEntityTemplate.insert(SysMenuEntity.class)
.using(entity)
.doOnNext(e -> e.markNotNew())
.map(sysMenuConverter::toDomain);
}
return sysMenuDao.save(entity)
.map(sysMenuConverter::toDomain);
}
@Override
public Mono<SysMenu> update(SysMenu sysMenu) {
SysMenuEntity entity = sysMenuConverter.toEntity(sysMenu);
entity.markNotNew();
return sysMenuDao.save(entity)
.map(sysMenuConverter::toDomain);
}
@@ -4,7 +4,9 @@ import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.manage.db.converter.SysPermissionConverter;
import cn.novalon.manage.db.dao.SysPermissionDao;
import cn.novalon.manage.db.entity.SysPermissionEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -20,10 +22,12 @@ public class SysPermissionRepository implements ISysPermissionRepository {
private final SysPermissionDao sysPermissionDao;
private final SysPermissionConverter sysPermissionConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysPermissionRepository(SysPermissionDao sysPermissionDao, SysPermissionConverter sysPermissionConverter) {
public SysPermissionRepository(SysPermissionDao sysPermissionDao, SysPermissionConverter sysPermissionConverter, R2dbcEntityTemplate r2dbcEntityTemplate) {
this.sysPermissionDao = sysPermissionDao;
this.sysPermissionConverter = sysPermissionConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
@@ -40,7 +44,14 @@ public class SysPermissionRepository implements ISysPermissionRepository {
@Override
public Mono<SysPermission> save(SysPermission sysPermission) {
return sysPermissionDao.save(sysPermissionConverter.toEntity(sysPermission))
SysPermissionEntity entity = sysPermissionConverter.toEntity(sysPermission);
if (entity.isNew()) {
return r2dbcEntityTemplate.insert(SysPermissionEntity.class)
.using(entity)
.doOnNext(e -> e.markNotNew())
.map(sysPermissionConverter::toDomain);
}
return sysPermissionDao.save(entity)
.map(sysPermissionConverter::toDomain);
}
@@ -4,6 +4,8 @@ import cn.novalon.manage.sys.core.domain.SysRolePermission;
import cn.novalon.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.manage.db.converter.SysRolePermissionConverter;
import cn.novalon.manage.db.dao.SysRolePermissionDao;
import cn.novalon.manage.db.entity.SysRolePermissionEntity;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -19,15 +21,24 @@ public class SysRolePermissionRepository implements ISysRolePermissionRepository
private final SysRolePermissionDao sysRolePermissionDao;
private final SysRolePermissionConverter sysRolePermissionConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysRolePermissionRepository(SysRolePermissionDao sysRolePermissionDao, SysRolePermissionConverter sysRolePermissionConverter) {
public SysRolePermissionRepository(SysRolePermissionDao sysRolePermissionDao, SysRolePermissionConverter sysRolePermissionConverter, R2dbcEntityTemplate r2dbcEntityTemplate) {
this.sysRolePermissionDao = sysRolePermissionDao;
this.sysRolePermissionConverter = sysRolePermissionConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
public Mono<SysRolePermission> save(SysRolePermission rolePermission) {
return sysRolePermissionDao.save(sysRolePermissionConverter.toEntity(rolePermission))
SysRolePermissionEntity entity = sysRolePermissionConverter.toEntity(rolePermission);
if (entity.isNew()) {
return r2dbcEntityTemplate.insert(SysRolePermissionEntity.class)
.using(entity)
.doOnNext(e -> e.markNotNew())
.map(sysRolePermissionConverter::toDomain);
}
return sysRolePermissionDao.save(entity)
.map(sysRolePermissionConverter::toDomain);
}
@@ -53,6 +53,12 @@ public class SysRoleRepository implements ISysRoleRepository {
@Override
public Mono<SysRole> save(SysRole sysRole) {
SysRoleEntity entity = sysRoleConverter.toEntity(sysRole);
if (entity.isNew()) {
return r2dbcEntityTemplate.insert(SysRoleEntity.class)
.using(entity)
.doOnNext(e -> e.markNotNew())
.map(sysRoleConverter::toDomain);
}
return sysRoleDao.save(entity)
.map(sysRoleConverter::toDomain);
}
@@ -156,6 +162,7 @@ public class SysRoleRepository implements ISysRoleRepository {
@Override
public Mono<SysRole> updateRole(SysRole role) {
SysRoleEntity entity = sysRoleConverter.toEntity(role);
entity.markNotNew();
return sysRoleDao.save(entity)
.map(sysRoleConverter::toDomain);
}
@@ -70,6 +70,20 @@ public class SysUserRepository implements ISysUserRepository {
@Override
public Mono<SysUser> save(SysUser sysUser) {
SysUserEntity entity = sysUserConverter.toEntity(sysUser);
if (entity.isNew()) {
return r2dbcEntityTemplate.insert(SysUserEntity.class)
.using(entity)
.doOnNext(e -> e.markNotNew())
.map(sysUserConverter::toDomain);
}
return sysUserDao.save(entity)
.map(sysUserConverter::toDomain);
}
@Override
public Mono<SysUser> update(SysUser sysUser) {
SysUserEntity entity = sysUserConverter.toEntity(sysUser);
entity.markNotNew();
return sysUserDao.save(entity)
.map(sysUserConverter::toDomain);
}
@@ -176,6 +190,7 @@ public class SysUserRepository implements ISysUserRepository {
public Mono<Void> logicalDeleteById(Long id) {
return sysUserDao.findById(id)
.flatMap(entity -> {
entity.markNotNew();
entity.setDeletedAt(java.time.LocalDateTime.now());
return sysUserDao.save(entity).then();
});
@@ -192,6 +207,7 @@ public class SysUserRepository implements ISysUserRepository {
public Mono<Void> restoreById(Long id) {
return sysUserDao.findById(id)
.flatMap(entity -> {
entity.markNotNew();
entity.setDeletedAt(null);
return sysUserDao.save(entity).then();
});
@@ -1 +1 @@
cn.novalon.manage.db.config.RepositoryScanConfig
cn.novalon.manage.db.config.RepositoryScanConfig
@@ -1,51 +0,0 @@
-- Novalon管理系统普通用户角色和数据
-- 版本: V10
-- 描述: 创建普通用户角色并分配权限
-- 插入普通用户角色
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, update_by)
VALUES ('普通用户', 'user', 2, 1, 'system', 'system')
ON CONFLICT (role_key) DO UPDATE SET
role_name = EXCLUDED.role_name,
role_sort = EXCLUDED.role_sort,
status = EXCLUDED.status;
-- 为普通用户分配基本权限(查看个人信息、修改密码等)
-- 注意:这里只分配基本权限,不包含管理功能权限
INSERT INTO sys_permission (permission_name, permission_key, permission_type, parent_id, path, component, icon, sort, status, create_by, update_by)
VALUES
('个人中心', 'profile', 'MENU', 0, '/profile', 'views/profile/index', 'user', 1, 1, 'system', 'system'),
('个人信息', 'profile:info', 'BUTTON', (SELECT id FROM sys_permission WHERE permission_key = 'profile'), '', '', '', 1, 1, 'system', 'system'),
('修改密码', 'profile:password', 'BUTTON', (SELECT id FROM sys_permission WHERE permission_key = 'profile'), '', '', '', 2, 1, 'system', 'system')
ON CONFLICT (permission_key) DO NOTHING;
-- 为普通用户角色分配权限
INSERT INTO sys_role_permission (role_id, permission_id, create_by, update_by)
SELECT
r.id as role_id,
p.id as permission_id,
'system' as create_by,
'system' as update_by
FROM sys_role r
CROSS JOIN sys_permission p
WHERE r.role_key = 'user'
AND p.permission_key IN ('profile', 'profile:info', 'profile:password')
ON CONFLICT DO NOTHING;
-- 将测试用户分配给普通用户角色
INSERT INTO user_role (user_id, role_id, create_by, update_by)
SELECT
u.id as user_id,
r.id as role_id,
'system' as create_by,
'system' as update_by
FROM sys_user u
CROSS JOIN sys_role r
WHERE u.username = 'user' AND r.role_key = 'user'
ON CONFLICT DO NOTHING;
-- 重置序列值
SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role));
SELECT setval('sys_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_permission));
SELECT setval('sys_role_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role_permission));
SELECT setval('user_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM user_role));
@@ -1,46 +0,0 @@
-- Novalon管理系统测试数据脚本
-- 版本: V11
-- 描述: 更新测试用户密码为Test@123,插入E2E测试所需数据
-- 更新admin用户密码为Test@123
-- BCrypt哈希值对应明文密码: Test@123
UPDATE sys_user
SET password = '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C'
WHERE username = 'admin';
-- 更新user用户密码为Test@123
UPDATE sys_user
SET password = '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C'
WHERE username = 'user';
-- 插入测试角色(如果不存在)
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, update_by)
VALUES
('测试管理员', 'test_admin', 2, 1, 'system', 'system'),
('普通用户', 'normal_user', 3, 1, 'system', 'system'),
('访客', 'guest', 4, 1, 'system', 'system')
ON CONFLICT (role_key) DO NOTHING;
-- 为admin用户分配超级管理员角色
INSERT INTO user_role (user_id, role_id, created_by)
SELECT 1, id, 'system' FROM sys_role WHERE role_key = 'admin'
ON CONFLICT DO NOTHING;
-- 为user用户分配普通用户角色
INSERT INTO user_role (user_id, role_id, created_by)
SELECT 2, id, 'system' FROM sys_role WHERE role_key = 'normal_user'
ON CONFLICT DO NOTHING;
-- 插入E2E测试专用用户
-- BCrypt哈希值对应明文密码: Test@123
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by)
VALUES
(10, 'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
-- 为E2E测试用户分配超级管理员角色
INSERT INTO user_role (user_id, role_id, created_by)
SELECT 10, id, 'system' FROM sys_role WHERE role_key = 'admin'
ON CONFLICT DO NOTHING;
@@ -1,28 +0,0 @@
-- V14__Fix_menu_data.sql
-- 清理测试菜单数据
DELETE FROM sys_menu WHERE menu_name LIKE '%测试%' OR menu_name LIKE '%回归%';
-- 插入一级菜单
INSERT INTO sys_menu (menu_name, parent_id, order_num, menu_type, status, created_at, updated_at) VALUES
('系统管理', 0, 1, 'M', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('系统监控', 0, 2, 'M', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('审计日志', 0, 3, 'M', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
-- 插入二级菜单(系统管理下)
INSERT INTO sys_menu (menu_name, parent_id, order_num, menu_type, component, perms, status, created_at, updated_at) VALUES
('用户管理', (SELECT id FROM sys_menu WHERE menu_name = '系统管理' AND parent_id = 0), 1, 'C', 'system/user/index', 'system:user:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('角色管理', (SELECT id FROM sys_menu WHERE menu_name = '系统管理' AND parent_id = 0), 2, 'C', 'system/role/index', 'system:role:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('菜单管理', (SELECT id FROM sys_menu WHERE menu_name = '系统管理' AND parent_id = 0), 3, 'C', 'system/menu/index', 'system:menu:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('参数配置', (SELECT id FROM sys_menu WHERE menu_name = '系统管理' AND parent_id = 0), 4, 'C', 'system/config/index', 'system:config:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('字典管理', (SELECT id FROM sys_menu WHERE menu_name = '系统管理' AND parent_id = 0), 5, 'C', 'system/dict/index', 'system:dict:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
-- 插入二级菜单(系统监控下)
INSERT INTO sys_menu (menu_name, parent_id, order_num, menu_type, component, perms, status, created_at, updated_at) VALUES
('文件管理', (SELECT id FROM sys_menu WHERE menu_name = '系统监控' AND parent_id = 0), 1, 'C', 'system/file/index', 'system:file:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('通知公告', (SELECT id FROM sys_menu WHERE menu_name = '系统监控' AND parent_id = 0), 2, 'C', 'system/notice/index', 'system:notice:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
-- 插入二级菜单(审计日志下)
INSERT INTO sys_menu (menu_name, parent_id, order_num, menu_type, component, perms, status, created_at, updated_at) VALUES
('登录日志', (SELECT id FROM sys_menu WHERE menu_name = '审计日志' AND parent_id = 0), 1, 'C', 'audit/login/index', 'audit:login:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('操作日志', (SELECT id FROM sys_menu WHERE menu_name = '审计日志' AND parent_id = 0), 2, 'C', 'audit/operation/index', 'audit:operation:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP),
('异常日志', (SELECT id FROM sys_menu WHERE menu_name = '审计日志' AND parent_id = 0), 3, 'C', 'audit/exception/index', 'audit:exception:list', 1, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP);
@@ -1,25 +1,31 @@
-- Novalon管理系统数据库初始化脚本
-- 版本: V1
-- 描述: 创建所有核心表结构
-- 描述: 创建所有核心表结构(合并版)
-- ============================================
-- 用户与角色相关表
-- ============================================
-- 用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
role_id BIGINT,
status INTEGER DEFAULT 1,
role_id BIGINT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 角色表
CREATE TABLE IF NOT EXISTS sys_role (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
@@ -30,9 +36,60 @@ CREATE TABLE IF NOT EXISTS sys_role (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 菜单表(统一使用sys_menu表名)
-- 用户角色关联表(支持多对多关系)
CREATE TABLE IF NOT EXISTS user_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- ============================================
-- 权限相关表
-- ============================================
-- 权限表
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGSERIAL PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL,
permission_code VARCHAR(100) NOT NULL UNIQUE,
resource VARCHAR(200) NOT NULL,
action VARCHAR(50) NOT NULL,
description VARCHAR(500),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 角色权限关联表
CREATE TABLE IF NOT EXISTS sys_role_permission (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
UNIQUE (role_id, permission_id)
);
-- ============================================
-- 菜单相关表
-- ============================================
-- 菜单表
CREATE TABLE IF NOT EXISTS sys_menu (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0,
@@ -46,9 +103,14 @@ CREATE TABLE IF NOT EXISTS sys_menu (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 字典相关表
-- ============================================
-- 字典类型表
CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0',
@@ -59,9 +121,10 @@ CREATE TABLE IF NOT EXISTS sys_dict_type (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 字典数据表
CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL,
@@ -76,9 +139,10 @@ CREATE TABLE IF NOT EXISTS sys_dict_data (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 字典表(通用字典)
CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
@@ -90,9 +154,14 @@ CREATE TABLE IF NOT EXISTS sys_dictionary (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 系统配置表
-- ============================================
-- 系统配置表
CREATE TABLE IF NOT EXISTS sys_config (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL,
@@ -103,9 +172,14 @@ CREATE TABLE IF NOT EXISTS sys_config (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 日志相关表
-- ============================================
-- 登录日志表
CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(255),
@@ -115,9 +189,10 @@ CREATE TABLE IF NOT EXISTS sys_login_log (
message VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
@@ -128,9 +203,10 @@ CREATE TABLE IF NOT EXISTS sys_exception_log (
ip VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
@@ -146,9 +222,53 @@ CREATE TABLE IF NOT EXISTS operation_log (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 审计日志表
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data JSONB,
after_data JSONB,
changed_fields TEXT[],
ip_address VARCHAR(50),
user_agent TEXT,
description TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 审计日志归档表
CREATE TABLE IF NOT EXISTS audit_log_archive (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP,
before_data JSONB,
after_data JSONB,
changed_fields TEXT[],
ip_address VARCHAR(50),
user_agent TEXT,
description TEXT,
created_at TIMESTAMP,
archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- ============================================
-- 通知与消息表
-- ============================================
-- 系统公告表
CREATE TABLE IF NOT EXISTS sys_notice (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL,
notice_content TEXT,
@@ -159,9 +279,10 @@ CREATE TABLE IF NOT EXISTS sys_notice (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 用户消息表
CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
notice_id BIGINT,
message_title VARCHAR(255),
@@ -174,9 +295,14 @@ CREATE TABLE IF NOT EXISTS sys_user_message (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 文件管理表
-- ============================================
-- 文件管理表
CREATE TABLE IF NOT EXISTS sys_file (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
@@ -189,9 +315,14 @@ CREATE TABLE IF NOT EXISTS sys_file (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- OAuth2相关表
-- ============================================
-- OAuth2客户端表
CREATE TABLE IF NOT EXISTS oauth2_client (
id BIGINT PRIMARY KEY,
id BIGSERIAL PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret VARCHAR(255) NOT NULL,
client_name VARCHAR(100),
@@ -208,7 +339,31 @@ CREATE TABLE IF NOT EXISTS oauth2_client (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 表注释
-- ============================================
COMMENT ON TABLE sys_user IS '系统用户表';
COMMENT ON TABLE sys_role IS '系统角色表';
COMMENT ON TABLE user_role IS '用户角色关联表';
COMMENT ON TABLE sys_permission IS '系统权限表';
COMMENT ON TABLE sys_role_permission IS '角色权限关联表';
COMMENT ON TABLE sys_menu IS '系统菜单表';
COMMENT ON TABLE sys_dict_type IS '字典类型表';
COMMENT ON TABLE sys_dict_data IS '字典数据表';
COMMENT ON TABLE sys_dictionary IS '通用字典表';
COMMENT ON TABLE sys_config IS '系统配置表';
COMMENT ON TABLE sys_login_log IS '登录日志表';
COMMENT ON TABLE sys_exception_log IS '异常日志表';
COMMENT ON TABLE operation_log IS '操作日志表';
COMMENT ON TABLE audit_log IS '审计日志表';
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
COMMENT ON TABLE sys_notice IS '系统公告表';
COMMENT ON TABLE sys_user_message IS '用户消息表';
COMMENT ON TABLE sys_file IS '文件管理表';
COMMENT ON TABLE oauth2_client IS 'OAuth2客户端表';
COMMENT ON TABLE sys_exception_log IS '异常日志表';
COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
@@ -220,5 +375,33 @@ COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
COMMENT ON COLUMN sys_exception_log.exception_stack IS '异常堆栈';
COMMENT ON COLUMN sys_exception_log.ip IS 'IP地址';
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
COMMENT ON TABLE sys_menu IS '系统菜单表';
COMMENT ON TABLE sys_login_log IS '登录日志表';
COMMENT ON TABLE audit_log IS '审计日志表';
COMMENT ON COLUMN audit_log.id IS '主键ID';
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
COMMENT ON COLUMN audit_log.entity_id IS '实体ID';
COMMENT ON COLUMN audit_log.operation_type IS '操作类型(CREATE, UPDATE, DELETE';
COMMENT ON COLUMN audit_log.operator IS '操作人';
COMMENT ON COLUMN audit_log.operation_time IS '操作时间';
COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)';
COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';
COMMENT ON COLUMN audit_log.description IS '操作描述';
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
COMMENT ON COLUMN audit_log_archive.entity_id IS '实体ID';
COMMENT ON COLUMN audit_log_archive.operation_type IS '操作类型(CREATE, UPDATE, DELETE';
COMMENT ON COLUMN audit_log_archive.operator IS '操作人';
COMMENT ON COLUMN audit_log_archive.operation_time IS '操作时间';
COMMENT ON COLUMN audit_log_archive.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log_archive.after_data IS '变更后数据(JSON格式)';
COMMENT ON COLUMN audit_log_archive.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log_archive.ip_address IS 'IP地址';
COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理';
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
@@ -1,67 +1,233 @@
-- Novalon管理系统初始数据脚本
-- 版本: V2
-- 描述: 插入必要的初始数据
-- 描述: 插入所有必要的初始数据(合并版)
-- 插入初始角色
INSERT INTO sys_role (role_name, role_key, role_sort, status, create_by, update_by)
VALUES ('超级管理员', 'admin', 1, 1, 'system', 'system')
ON CONFLICT (role_key) DO NOTHING;
-- ============================================
-- 角色数据
-- ============================================
-- 插入初始管理员用户
-- BCrypt哈希值对应明文密码: admin123
INSERT INTO sys_user (id, username, password, email, phone, status, create_by, update_by)
VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
-- 插入测试用户(用于E2E测试)
-- BCrypt哈希值对应明文密码: admin123
INSERT INTO sys_user (id, username, password, email, phone, status, create_by, update_by)
VALUES (2, 'user', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'user@novalon.com', '13800138001', 1, 'system', 'system')
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
-- 插入初始字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by)
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by, created_at, updated_at)
VALUES
('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system'),
('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system')
(1, '超级管理员', 'admin', 1, 1, 'system', 'system', NOW(), NOW()),
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system', NOW(), NOW()),
(3, '普通用户', 'normal_user', 3, 1, 'system', 'system', NOW(), NOW()),
(4, '访客', 'guest', 4, 1, 'system', 'system', NOW(), NOW());
SELECT setval('sys_role_id_seq', 4);
-- ============================================
-- 用户数据
-- ============================================
-- 密码均为: Test@123 (BCrypt哈希)
INSERT INTO sys_user (id, username, password, email, phone, nickname, status, create_by, update_by, created_at, updated_at)
VALUES
(1, 'admin', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'admin@novalon.com', '13800138000', '超级管理员', 1, 'system', 'system', NOW(), NOW()),
(2, 'user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'user@novalon.com', '13800138001', '普通用户', 1, 'system', 'system', NOW(), NOW()),
(10, 'e2e_test_user', '$2a$12$nZ1EMUpZQljbnEdIKzH72eHlDJKUmHmHppnTTVth/SlHs5VpSAr8C', 'e2e@test.com', '13900139000', 'E2E测试用户', 1, 'system', 'system', NOW(), NOW())
ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password,
status = EXCLUDED.status;
SELECT setval('sys_user_id_seq', 10);
-- ============================================
-- 用户角色关联
-- ============================================
-- 为admin用户分配超级管理员角色
INSERT INTO user_role (user_id, role_id, created_by, created_at)
VALUES
(1, 1, 'system', NOW()),
(2, 3, 'system', NOW()),
(10, 1, 'system', NOW())
ON CONFLICT (user_id, role_id) DO NOTHING;
-- ============================================
-- 权限数据
-- ============================================
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status, create_by, update_by, created_at, updated_at) VALUES
('用户查看', 'system:user:view', '/api/users', 'GET', '查看用户列表', 1, 'system', 'system', NOW(), NOW()),
('用户创建', 'system:user:create', '/api/users', 'POST', '创建用户', 1, 'system', 'system', NOW(), NOW()),
('用户编辑', 'system:user:edit', '/api/users', 'PUT', '编辑用户', 1, 'system', 'system', NOW(), NOW()),
('用户删除', 'system:user:delete', '/api/users', 'DELETE', '删除用户', 1, 'system', 'system', NOW(), NOW()),
('角色查看', 'system:role:view', '/api/roles', 'GET', '查看角色列表', 1, 'system', 'system', NOW(), NOW()),
('角色创建', 'system:role:create', '/api/roles', 'POST', '创建角色', 1, 'system', 'system', NOW(), NOW()),
('角色编辑', 'system:role:edit', '/api/roles', 'PUT', '编辑角色', 1, 'system', 'system', NOW(), NOW()),
('角色删除', 'system:role:delete', '/api/roles', 'DELETE', '删除角色', 1, 'system', 'system', NOW(), NOW()),
('角色分配权限', 'system:role:assign', '/api/roles/*/permissions', 'POST', '为角色分配权限', 1, 'system', 'system', NOW(), NOW()),
('权限查看', 'system:permission:view', '/api/permissions', 'GET', '查看权限列表', 1, 'system', 'system', NOW(), NOW()),
('权限创建', 'system:permission:create', '/api/permissions', 'POST', '创建权限', 1, 'system', 'system', NOW(), NOW()),
('权限编辑', 'system:permission:edit', '/api/permissions', 'PUT', '编辑权限', 1, 'system', 'system', NOW(), NOW()),
('权限删除', 'system:permission:delete', '/api/permissions', 'DELETE', '删除权限', 1, 'system', 'system', NOW(), NOW()),
('菜单查看', 'system:menu:view', '/api/menus', 'GET', '查看菜单列表', 1, 'system', 'system', NOW(), NOW()),
('菜单创建', 'system:menu:create', '/api/menus', 'POST', '创建菜单', 1, 'system', 'system', NOW(), NOW()),
('菜单编辑', 'system:menu:edit', '/api/menus', 'PUT', '编辑菜单', 1, 'system', 'system', NOW(), NOW()),
('菜单删除', 'system:menu:delete', '/api/menus', 'DELETE', '删除菜单', 1, 'system', 'system', NOW(), NOW()),
('字典查看', 'system:dict:view', '/api/dict', 'GET', '查看字典列表', 1, 'system', 'system', NOW(), NOW()),
('字典创建', 'system:dict:create', '/api/dict', 'POST', '创建字典', 1, 'system', 'system', NOW(), NOW()),
('字典编辑', 'system:dict:edit', '/api/dict', 'PUT', '编辑字典', 1, 'system', 'system', NOW(), NOW()),
('字典删除', 'system:dict:delete', '/api/dict', 'DELETE', '删除字典', 1, 'system', 'system', NOW(), NOW()),
('配置查看', 'system:config:view', '/api/config', 'GET', '查看系统配置', 1, 'system', 'system', NOW(), NOW()),
('配置创建', 'system:config:create', '/api/config', 'POST', '创建系统配置', 1, 'system', 'system', NOW(), NOW()),
('配置编辑', 'system:config:edit', '/api/config', 'PUT', '编辑系统配置', 1, 'system', 'system', NOW(), NOW()),
('配置删除', 'system:config:delete', '/api/config', 'DELETE', '删除系统配置', 1, 'system', 'system', NOW(), NOW()),
('日志查看', 'system:log:view', '/api/logs', 'GET', '查看日志', 1, 'system', 'system', NOW(), NOW()),
('文件上传', 'system:file:upload', '/api/files/upload', 'POST', '上传文件', 1, 'system', 'system', NOW(), NOW()),
('文件下载', 'system:file:download', '/api/files/download', 'GET', '下载文件', 1, 'system', 'system', NOW(), NOW()),
('文件删除', 'system:file:delete', '/api/files', 'DELETE', '删除文件', 1, 'system', 'system', NOW(), NOW()),
('公告查看', 'system:notice:view', '/api/notices', 'GET', '查看公告', 1, 'system', 'system', NOW(), NOW()),
('公告创建', 'system:notice:create', '/api/notices', 'POST', '创建公告', 1, 'system', 'system', NOW(), NOW()),
('公告编辑', 'system:notice:edit', '/api/notices', 'PUT', '编辑公告', 1, 'system', 'system', NOW(), NOW()),
('公告删除', 'system:notice:delete', '/api/notices', 'DELETE', '删除公告', 1, 'system', 'system', NOW(), NOW());
-- 为管理员角色分配所有权限
INSERT INTO sys_role_permission (role_id, permission_id, create_by, update_by, created_at, updated_at)
SELECT 1, id, 'system', 'system', NOW(), NOW() FROM sys_permission WHERE status = 1;
-- ============================================
-- 菜单数据
-- ============================================
-- 一级菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(1, '系统管理', 0, 1, 'M', NULL, NULL, 1, NOW(), NOW()),
(2, '审计日志', 0, 2, 'M', NULL, NULL, 1, NOW(), NOW()),
(3, '系统监控', 0, 3, 'M', NULL, NULL, 1, NOW(), NOW());
-- 系统管理子菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(11, '用户管理', 1, 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
(12, '角色管理', 1, 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
(13, '菜单管理', 1, 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
(14, '部门管理', 1, 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
(15, '字典管理', 1, 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
(16, '参数管理', 1, 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
(17, '通知公告', 1, 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
(18, '文件管理', 1, 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW());
-- 用户管理按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(111, '用户查询', 11, 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
(112, '用户新增', 11, 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
(113, '用户修改', 11, 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
(114, '用户删除', 11, 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
(115, '用户导出', 11, 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
(116, '用户导入', 11, 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
(117, '重置密码', 11, 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW());
-- 角色管理按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(121, '角色查询', 12, 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
(122, '角色新增', 12, 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
(123, '角色修改', 12, 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
(124, '角色删除', 12, 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
(125, '角色导出', 12, 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW());
-- 菜单管理按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(131, '菜单查询', 13, 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
(132, '菜单新增', 13, 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
(133, '菜单修改', 13, 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
(134, '菜单删除', 13, 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
-- 审计日志子菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(21, '操作日志', 2, 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
(22, '登录日志', 2, 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
(23, '异常日志', 2, 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
-- 操作日志按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(211, '操作查询', 21, 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
(212, '操作删除', 21, 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
(213, '操作导出', 21, 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW());
-- 登录日志按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(221, '登录查询', 22, 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
(222, '登录删除', 22, 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
(223, '登录导出', 22, 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW());
-- 异常日志按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(231, '异常查询', 23, 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
(232, '异常删除', 23, 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
(233, '异常导出', 23, 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
-- 系统监控子菜单
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(31, '在线用户', 3, 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
(32, '定时任务', 3, 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
(33, '数据监控', 3, 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
(34, '服务监控', 3, 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
(35, '缓存监控', 3, 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
-- 在线用户按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(311, '在线查询', 31, 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
(312, '在线强退', 31, 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW());
-- 定时任务按钮权限
INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(321, '任务查询', 32, 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
(322, '任务新增', 32, 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
(323, '任务修改', 32, 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
(324, '任务删除', 32, 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
(325, '任务执行', 32, 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
SELECT setval('sys_menu_id_seq', 400);
-- ============================================
-- 字典数据
-- ============================================
-- 字典类型
INSERT INTO sys_dict_type (dict_name, dict_type, status, remark, create_by, update_by, created_at, updated_at)
VALUES
('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system', NOW(), NOW()),
('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system', NOW(), NOW()),
('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system', NOW(), NOW()),
('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system', NOW(), NOW())
ON CONFLICT (dict_type) DO NOTHING;
-- 插入初始字典数据
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by)
-- 字典数据
INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by, created_at, updated_at)
VALUES
-- 用户状态
(1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
(1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
(2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
-- 菜单状态
(1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
(1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
-- 角色状态
(1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system'),
(1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()),
-- 系统开关
(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system')
ON CONFLICT DO NOTHING;
(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()),
(2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW());
-- 插入初始系统配置
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, update_by)
-- ============================================
-- 系统配置
-- ============================================
INSERT INTO sys_config (config_name, config_key, config_value, config_type, create_by, update_by, created_at, updated_at)
VALUES
('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system'),
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system'),
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system'),
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system'),
('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system')
('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system', NOW(), NOW()),
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system', NOW(), NOW()),
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()),
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system', NOW(), NOW()),
('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW())
ON CONFLICT (config_key) DO NOTHING;
-- ============================================
-- 重置序列值
SELECT setval('sys_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_user));
SELECT setval('sys_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role));
-- ============================================
SELECT setval('sys_dict_type_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_type));
SELECT setval('sys_dict_data_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_dict_data));
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
SELECT setval('sys_config_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_config));
SELECT setval('sys_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_permission));
SELECT setval('sys_role_permission_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_role_permission));
SELECT setval('user_role_id_seq', (SELECT COALESCE(MAX(id), 1) FROM user_role));
@@ -1,7 +1,11 @@
-- Novalon管理系统索引优化脚本
-- 版本: V5
-- 版本: V3
-- 描述: 为表创建必要的索引以提升查询性能
-- ============================================
-- 用户与角色表索引
-- ============================================
-- 用户表索引
CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email);
@@ -13,11 +17,35 @@ CREATE INDEX IF NOT EXISTS idx_roles_role_key ON sys_role(role_key);
CREATE INDEX IF NOT EXISTS idx_roles_status ON sys_role(status);
CREATE INDEX IF NOT EXISTS idx_roles_deleted_at ON sys_role(deleted_at);
-- 用户角色关联表索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
-- ============================================
-- 权限表索引
-- ============================================
-- 权限表索引
CREATE INDEX IF NOT EXISTS idx_permission_code ON sys_permission(permission_code);
CREATE INDEX IF NOT EXISTS idx_permission_resource ON sys_permission(resource);
CREATE INDEX IF NOT EXISTS idx_permission_status ON sys_permission(status);
-- 角色权限关联表索引
CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON sys_role_permission(role_id);
CREATE INDEX IF NOT EXISTS idx_role_permission_permission_id ON sys_role_permission(permission_id);
-- ============================================
-- 菜单表索引
-- ============================================
CREATE INDEX IF NOT EXISTS idx_sys_menu_parent_id ON sys_menu(parent_id);
CREATE INDEX IF NOT EXISTS idx_sys_menu_status ON sys_menu(status);
CREATE INDEX IF NOT EXISTS idx_sys_menu_deleted_at ON sys_menu(deleted_at);
-- ============================================
-- 字典表索引
-- ============================================
-- 字典类型表索引
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_dict_type ON sys_dict_type(dict_type);
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_status ON sys_dict_type(status);
@@ -29,16 +57,23 @@ CREATE INDEX IF NOT EXISTS idx_sys_dict_data_dict_value ON sys_dict_data(dict_va
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_status ON sys_dict_data(status);
CREATE INDEX IF NOT EXISTS idx_sys_dict_data_deleted_at ON sys_dict_data(deleted_at);
-- 字典表索引
-- 通用字典表索引
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type ON sys_dictionary(type);
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_type_code ON sys_dictionary(type, code);
CREATE INDEX IF NOT EXISTS idx_sys_dictionary_deleted_at ON sys_dictionary(deleted_at);
-- ============================================
-- 系统配置表索引
-- ============================================
CREATE INDEX IF NOT EXISTS idx_sys_config_config_key ON sys_config(config_key);
CREATE INDEX IF NOT EXISTS idx_sys_config_config_type ON sys_config(config_type);
CREATE INDEX IF NOT EXISTS idx_sys_config_deleted_at ON sys_config(deleted_at);
-- ============================================
-- 日志表索引
-- ============================================
-- 登录日志表索引
CREATE INDEX IF NOT EXISTS idx_sys_login_log_username ON sys_login_log(username);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_ip ON sys_login_log(ip);
@@ -57,6 +92,26 @@ CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON operation_log(created
CREATE INDEX IF NOT EXISTS idx_operation_log_status ON operation_log(status);
CREATE INDEX IF NOT EXISTS idx_operation_log_deleted_at ON operation_log(deleted_at);
-- 审计日志表索引
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_type ON audit_log(entity_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity_id ON audit_log(entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_type ON audit_log(operation_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_operator ON audit_log(operator);
CREATE INDEX IF NOT EXISTS idx_audit_log_operation_time ON audit_log(operation_time);
CREATE INDEX IF NOT EXISTS idx_audit_log_entity ON audit_log(entity_type, entity_id);
-- 审计日志归档表索引
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_type ON audit_log_archive(entity_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_entity_id ON audit_log_archive(entity_id);
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_type ON audit_log_archive(operation_type);
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operator ON audit_log_archive(operator);
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_operation_time ON audit_log_archive(operation_time);
CREATE INDEX IF NOT EXISTS idx_audit_log_archive_archived_at ON audit_log_archive(archived_at);
-- ============================================
-- 通知与消息表索引
-- ============================================
-- 系统公告表索引
CREATE INDEX IF NOT EXISTS idx_sys_notice_notice_type ON sys_notice(notice_type);
CREATE INDEX IF NOT EXISTS idx_sys_notice_status ON sys_notice(status);
@@ -68,11 +123,17 @@ CREATE INDEX IF NOT EXISTS idx_sys_user_message_notice_id ON sys_user_message(no
CREATE INDEX IF NOT EXISTS idx_sys_user_message_is_read ON sys_user_message(is_read);
CREATE INDEX IF NOT EXISTS idx_sys_user_message_deleted_at ON sys_user_message(deleted_at);
-- ============================================
-- 文件管理表索引
-- ============================================
CREATE INDEX IF NOT EXISTS idx_sys_file_file_type ON sys_file(file_type);
CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at);
-- ============================================
-- OAuth2客户端表索引
-- ============================================
CREATE INDEX IF NOT EXISTS idx_oauth2_client_client_id ON oauth2_client(client_id);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_enabled ON oauth2_client(enabled);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at);
CREATE INDEX IF NOT EXISTS idx_oauth2_client_deleted_at ON oauth2_client(deleted_at);
@@ -1,23 +0,0 @@
-- 创建用户角色关联表(支持多对多关系)
CREATE TABLE IF NOT EXISTS user_role (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
role_id BIGINT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
created_by VARCHAR(50),
CONSTRAINT fk_user_role_user FOREIGN KEY (user_id) REFERENCES sys_user(id) ON DELETE CASCADE,
CONSTRAINT fk_user_role_role FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
CONSTRAINT uk_user_role UNIQUE (user_id, role_id)
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_user_role_user_id ON user_role(user_id);
CREATE INDEX IF NOT EXISTS idx_user_role_role_id ON user_role(role_id);
-- 表注释
COMMENT ON TABLE user_role IS '用户角色关联表';
COMMENT ON COLUMN user_role.id IS '主键ID';
COMMENT ON COLUMN user_role.user_id IS '用户ID';
COMMENT ON COLUMN user_role.role_id IS '角色ID';
COMMENT ON COLUMN user_role.created_at IS '创建时间';
COMMENT ON COLUMN user_role.created_by IS '创建人';
@@ -1,104 +0,0 @@
-- Novalon管理系统权限功能数据库迁移脚本
-- 版本: V4
-- 描述: 创建权限管理相关表结构
-- 权限表
CREATE TABLE IF NOT EXISTS sys_permission (
id BIGSERIAL PRIMARY KEY,
permission_name VARCHAR(100) NOT NULL,
permission_code VARCHAR(100) NOT NULL UNIQUE,
resource VARCHAR(200) NOT NULL,
action VARCHAR(50) NOT NULL,
description VARCHAR(500),
status INTEGER DEFAULT 1,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 角色权限关联表
CREATE TABLE IF NOT EXISTS sys_role_permission (
id BIGSERIAL PRIMARY KEY,
role_id BIGINT NOT NULL,
permission_id BIGINT NOT NULL,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES sys_role(id) ON DELETE CASCADE,
FOREIGN KEY (permission_id) REFERENCES sys_permission(id) ON DELETE CASCADE,
UNIQUE (role_id, permission_id)
);
-- 表注释
COMMENT ON TABLE sys_permission IS '系统权限表';
COMMENT ON COLUMN sys_permission.id IS '主键ID';
COMMENT ON COLUMN sys_permission.permission_name IS '权限名称';
COMMENT ON COLUMN sys_permission.permission_code IS '权限编码';
COMMENT ON COLUMN sys_permission.resource IS '资源路径';
COMMENT ON COLUMN sys_permission.action IS '操作类型';
COMMENT ON COLUMN sys_permission.description IS '权限描述';
COMMENT ON COLUMN sys_permission.status IS '状态:0-禁用,1-正常';
COMMENT ON COLUMN sys_permission.create_by IS '创建者';
COMMENT ON COLUMN sys_permission.update_by IS '更新者';
COMMENT ON COLUMN sys_permission.created_at IS '创建时间';
COMMENT ON COLUMN sys_permission.updated_at IS '更新时间';
COMMENT ON COLUMN sys_permission.deleted_at IS '删除时间';
COMMENT ON TABLE sys_role_permission IS '角色权限关联表';
COMMENT ON COLUMN sys_role_permission.id IS '主键ID';
COMMENT ON COLUMN sys_role_permission.role_id IS '角色ID';
COMMENT ON COLUMN sys_role_permission.permission_id IS '权限ID';
COMMENT ON COLUMN sys_role_permission.create_by IS '创建者';
COMMENT ON COLUMN sys_role_permission.update_by IS '更新者';
COMMENT ON COLUMN sys_role_permission.created_at IS '创建时间';
COMMENT ON COLUMN sys_role_permission.updated_at IS '更新时间';
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_permission_code ON sys_permission(permission_code);
CREATE INDEX IF NOT EXISTS idx_permission_resource ON sys_permission(resource);
CREATE INDEX IF NOT EXISTS idx_permission_status ON sys_permission(status);
CREATE INDEX IF NOT EXISTS idx_role_permission_role_id ON sys_role_permission(role_id);
CREATE INDEX IF NOT EXISTS idx_role_permission_permission_id ON sys_role_permission(permission_id);
-- 插入初始权限数据
INSERT INTO sys_permission (permission_name, permission_code, resource, action, description, status) VALUES
('用户查看', 'system:user:view', '/api/users', 'GET', '查看用户列表', 1),
('用户创建', 'system:user:create', '/api/users', 'POST', '创建用户', 1),
('用户编辑', 'system:user:edit', '/api/users', 'PUT', '编辑用户', 1),
('用户删除', 'system:user:delete', '/api/users', 'DELETE', '删除用户', 1),
('角色查看', 'system:role:view', '/api/roles', 'GET', '查看角色列表', 1),
('角色创建', 'system:role:create', '/api/roles', 'POST', '创建角色', 1),
('角色编辑', 'system:role:edit', '/api/roles', 'PUT', '编辑角色', 1),
('角色删除', 'system:role:delete', '/api/roles', 'DELETE', '删除角色', 1),
('角色分配权限', 'system:role:assign', '/api/roles/*/permissions', 'POST', '为角色分配权限', 1),
('权限查看', 'system:permission:view', '/api/permissions', 'GET', '查看权限列表', 1),
('权限创建', 'system:permission:create', '/api/permissions', 'POST', '创建权限', 1),
('权限编辑', 'system:permission:edit', '/api/permissions', 'PUT', '编辑权限', 1),
('权限删除', 'system:permission:delete', '/api/permissions', 'DELETE', '删除权限', 1),
('菜单查看', 'system:menu:view', '/api/menus', 'GET', '查看菜单列表', 1),
('菜单创建', 'system:menu:create', '/api/menus', 'POST', '创建菜单', 1),
('菜单编辑', 'system:menu:edit', '/api/menus', 'PUT', '编辑菜单', 1),
('菜单删除', 'system:menu:delete', '/api/menus', 'DELETE', '删除菜单', 1),
('字典查看', 'system:dict:view', '/api/dict', 'GET', '查看字典列表', 1),
('字典创建', 'system:dict:create', '/api/dict', 'POST', '创建字典', 1),
('字典编辑', 'system:dict:edit', '/api/dict', 'PUT', '编辑字典', 1),
('字典删除', 'system:dict:delete', '/api/dict', 'DELETE', '删除字典', 1),
('配置查看', 'system:config:view', '/api/config', 'GET', '查看系统配置', 1),
('配置创建', 'system:config:create', '/api/config', 'POST', '创建系统配置', 1),
('配置编辑', 'system:config:edit', '/api/config', 'PUT', '编辑系统配置', 1),
('配置删除', 'system:config:delete', '/api/config', 'DELETE', '删除系统配置', 1),
('日志查看', 'system:log:view', '/api/logs', 'GET', '查看日志', 1),
('文件上传', 'system:file:upload', '/api/files/upload', 'POST', '上传文件', 1),
('文件下载', 'system:file:download', '/api/files/download', 'GET', '下载文件', 1),
('文件删除', 'system:file:delete', '/api/files', 'DELETE', '删除文件', 1),
('公告查看', 'system:notice:view', '/api/notices', 'GET', '查看公告', 1),
('公告创建', 'system:notice:create', '/api/notices', 'POST', '创建公告', 1),
('公告编辑', 'system:notice:edit', '/api/notices', 'PUT', '编辑公告', 1),
('公告删除', 'system:notice:delete', '/api/notices', 'DELETE', '删除公告', 1);
-- 为管理员角色分配所有权限
INSERT INTO sys_role_permission (role_id, permission_id)
SELECT 1, id FROM sys_permission WHERE status = 1;
@@ -1,5 +1,5 @@
-- Novalon管理系统权限授予脚本
-- 版本: V9
-- 版本: V4
-- 描述: 为novalon用户授予所有表的访问权限
-- 授予所有表的SELECT, INSERT, UPDATE, DELETE权限
@@ -0,0 +1,41 @@
-- 创建操作日志表
CREATE TABLE IF NOT EXISTS sys_operation_log (
id BIGINT PRIMARY KEY,
username VARCHAR(50),
operation VARCHAR(100),
method VARCHAR(200),
params TEXT,
result TEXT,
ip VARCHAR(50),
duration BIGINT,
status VARCHAR(1) DEFAULT '0',
error_msg TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- 创建索引
CREATE INDEX IF NOT EXISTS idx_operation_log_username ON sys_operation_log(username);
CREATE INDEX IF NOT EXISTS idx_operation_log_created_at ON sys_operation_log(created_at);
CREATE INDEX IF NOT EXISTS idx_operation_log_status ON sys_operation_log(status);
-- 添加注释
COMMENT ON TABLE sys_operation_log IS '操作日志表';
COMMENT ON COLUMN sys_operation_log.id IS '主键ID';
COMMENT ON COLUMN sys_operation_log.username IS '操作用户';
COMMENT ON COLUMN sys_operation_log.operation IS '操作描述';
COMMENT ON COLUMN sys_operation_log.method IS '请求方法';
COMMENT ON COLUMN sys_operation_log.params IS '请求参数';
COMMENT ON COLUMN sys_operation_log.result IS '操作结果';
COMMENT ON COLUMN sys_operation_log.ip IS 'IP地址';
COMMENT ON COLUMN sys_operation_log.duration IS '执行时长(毫秒)';
COMMENT ON COLUMN sys_operation_log.status IS '操作状态(0成功 1失败)';
COMMENT ON COLUMN sys_operation_log.error_msg IS '错误消息';
COMMENT ON COLUMN sys_operation_log.create_by IS '创建人';
COMMENT ON COLUMN sys_operation_log.update_by IS '更新人';
COMMENT ON COLUMN sys_operation_log.created_at IS '创建时间';
COMMENT ON COLUMN sys_operation_log.updated_at IS '更新时间';
COMMENT ON COLUMN sys_operation_log.deleted_at IS '删除时间';
@@ -1,90 +0,0 @@
-- 系统菜单初始化数据
-- 版本: V6
-- 描述: 初始化系统菜单数据
-- 一级菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(1, 0, '系统管理', 1, 'M', NULL, NULL, 1, NOW(), NOW()),
(2, 0, '审计日志', 2, 'M', NULL, NULL, 1, NOW(), NOW()),
(3, 0, '系统监控', 3, 'M', NULL, NULL, 1, NOW(), NOW());
-- 系统管理子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(11, 1, '用户管理', 1, 'C', 'system:user:list', 'system/user/index', 1, NOW(), NOW()),
(12, 1, '角色管理', 2, 'C', 'system:role:list', 'system/role/index', 1, NOW(), NOW()),
(13, 1, '菜单管理', 3, 'C', 'system:menu:list', 'system/menu/index', 1, NOW(), NOW()),
(14, 1, '部门管理', 4, 'C', 'system:dept:list', 'system/dept/index', 1, NOW(), NOW()),
(15, 1, '字典管理', 5, 'C', 'system:dict:list', 'system/dict/index', 1, NOW(), NOW()),
(16, 1, '参数管理', 6, 'C', 'system:config:list', 'system/config/index', 1, NOW(), NOW()),
(17, 1, '通知公告', 7, 'C', 'system:notice:list', 'system/notice/index', 1, NOW(), NOW()),
(18, 1, '文件管理', 8, 'C', 'system:file:list', 'system/file/index', 1, NOW(), NOW());
-- 用户管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(111, 11, '用户查询', 1, 'F', 'system:user:query', NULL, 1, NOW(), NOW()),
(112, 11, '用户新增', 2, 'F', 'system:user:add', NULL, 1, NOW(), NOW()),
(113, 11, '用户修改', 3, 'F', 'system:user:edit', NULL, 1, NOW(), NOW()),
(114, 11, '用户删除', 4, 'F', 'system:user:remove', NULL, 1, NOW(), NOW()),
(115, 11, '用户导出', 5, 'F', 'system:user:export', NULL, 1, NOW(), NOW()),
(116, 11, '用户导入', 6, 'F', 'system:user:import', NULL, 1, NOW(), NOW()),
(117, 11, '重置密码', 7, 'F', 'system:user:resetPwd', NULL, 1, NOW(), NOW());
-- 角色管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(121, 12, '角色查询', 1, 'F', 'system:role:query', NULL, 1, NOW(), NOW()),
(122, 12, '角色新增', 2, 'F', 'system:role:add', NULL, 1, NOW(), NOW()),
(123, 12, '角色修改', 3, 'F', 'system:role:edit', NULL, 1, NOW(), NOW()),
(124, 12, '角色删除', 4, 'F', 'system:role:remove', NULL, 1, NOW(), NOW()),
(125, 12, '角色导出', 5, 'F', 'system:role:export', NULL, 1, NOW(), NOW());
-- 菜单管理按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(131, 13, '菜单查询', 1, 'F', 'system:menu:query', NULL, 1, NOW(), NOW()),
(132, 13, '菜单新增', 2, 'F', 'system:menu:add', NULL, 1, NOW(), NOW()),
(133, 13, '菜单修改', 3, 'F', 'system:menu:edit', NULL, 1, NOW(), NOW()),
(134, 13, '菜单删除', 4, 'F', 'system:menu:remove', NULL, 1, NOW(), NOW());
-- 审计日志子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(21, 2, '操作日志', 1, 'C', 'audit:operation:list', 'audit/operation/index', 1, NOW(), NOW()),
(22, 2, '登录日志', 2, 'C', 'audit:login:list', 'audit/login/index', 1, NOW(), NOW()),
(23, 2, '异常日志', 3, 'C', 'audit:exception:list', 'audit/exception/index', 1, NOW(), NOW());
-- 操作日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(211, 21, '操作查询', 1, 'F', 'audit:operation:query', NULL, 1, NOW(), NOW()),
(212, 21, '操作删除', 2, 'F', 'audit:operation:remove', NULL, 1, NOW(), NOW()),
(213, 21, '操作导出', 3, 'F', 'audit:operation:export', NULL, 1, NOW(), NOW());
-- 登录日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(221, 22, '登录查询', 1, 'F', 'audit:login:query', NULL, 1, NOW(), NOW()),
(222, 22, '登录删除', 2, 'F', 'audit:login:remove', NULL, 1, NOW(), NOW()),
(223, 22, '登录导出', 3, 'F', 'audit:login:export', NULL, 1, NOW(), NOW());
-- 异常日志按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(231, 23, '异常查询', 1, 'F', 'audit:exception:query', NULL, 1, NOW(), NOW()),
(232, 23, '异常删除', 2, 'F', 'audit:exception:remove', NULL, 1, NOW(), NOW()),
(233, 23, '异常导出', 3, 'F', 'audit:exception:export', NULL, 1, NOW(), NOW());
-- 系统监控子菜单
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(31, 3, '在线用户', 1, 'C', 'monitor:online:list', 'monitor/online/index', 1, NOW(), NOW()),
(32, 3, '定时任务', 2, 'C', 'monitor:job:list', 'monitor/job/index', 1, NOW(), NOW()),
(33, 3, '数据监控', 3, 'C', 'monitor:data:list', 'monitor/data/index', 1, NOW(), NOW()),
(34, 3, '服务监控', 4, 'C', 'monitor:server:list', 'monitor/server/index', 1, NOW(), NOW()),
(35, 3, '缓存监控', 5, 'C', 'monitor:cache:list', 'monitor/cache/index', 1, NOW(), NOW());
-- 在线用户按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(311, 31, '在线查询', 1, 'F', 'monitor:online:query', NULL, 1, NOW(), NOW()),
(312, 31, '在线强退', 2, 'F', 'monitor:online:forceLogout', NULL, 1, NOW(), NOW());
-- 定时任务按钮权限
INSERT INTO sys_menu (id, parent_id, menu_name, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES
(321, 32, '任务查询', 1, 'F', 'monitor:job:query', NULL, 1, NOW(), NOW()),
(322, 32, '任务新增', 2, 'F', 'monitor:job:add', NULL, 1, NOW(), NOW()),
(323, 32, '任务修改', 3, 'F', 'monitor:job:edit', NULL, 1, NOW(), NOW()),
(324, 32, '任务删除', 4, 'F', 'monitor:job:remove', NULL, 1, NOW(), NOW()),
(325, 32, '任务执行', 5, 'F', 'monitor:job:execute', NULL, 1, NOW(), NOW());
@@ -1,40 +0,0 @@
-- Novalon管理系统审计日志表
-- 版本: V7
-- 描述: 创建审计日志表,记录数据变更前后的完整对比
CREATE TABLE IF NOT EXISTS audit_log (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
before_data JSONB,
after_data JSONB,
changed_fields TEXT [],
ip_address VARCHAR(50),
user_agent TEXT,
description TEXT,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
CREATE INDEX idx_audit_log_entity_type ON audit_log(entity_type);
CREATE INDEX idx_audit_log_entity_id ON audit_log(entity_id);
CREATE INDEX idx_audit_log_operation_type ON audit_log(operation_type);
CREATE INDEX idx_audit_log_operator ON audit_log(operator);
CREATE INDEX idx_audit_log_operation_time ON audit_log(operation_time);
CREATE INDEX idx_audit_log_entity ON audit_log(entity_type, entity_id);
COMMENT ON TABLE audit_log IS '审计日志表';
COMMENT ON COLUMN audit_log.id IS '主键ID';
COMMENT ON COLUMN audit_log.entity_type IS '实体类型(如User, Role等)';
COMMENT ON COLUMN audit_log.entity_id IS '实体ID';
COMMENT ON COLUMN audit_log.operation_type IS '操作类型(CREATE, UPDATE, DELETE';
COMMENT ON COLUMN audit_log.operator IS '操作人';
COMMENT ON COLUMN audit_log.operation_time IS '操作时间';
COMMENT ON COLUMN audit_log.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log.after_data IS '变更后数据(JSON格式)';
COMMENT ON COLUMN audit_log.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log.ip_address IS 'IP地址';COMMENT ON COLUMN audit_log.description IS '操作描述';
COMMENT ON COLUMN audit_log.created_at IS '记录创建时间';
@@ -1,43 +0,0 @@
-- Novalon管理系统审计日志归档表
-- 版本: V8
-- 描述: 创建审计日志归档表,用于存储历史审计日志
CREATE TABLE IF NOT EXISTS audit_log_archive (
id BIGSERIAL PRIMARY KEY,
entity_type VARCHAR(100) NOT NULL,
entity_id BIGINT,
operation_type VARCHAR(20) NOT NULL,
operator VARCHAR(100),
operation_time TIMESTAMP,
before_data JSONB,
after_data JSONB,
changed_fields TEXT[],
ip_address VARCHAR(50),
user_agent TEXT,
description TEXT,
created_at TIMESTAMP,
archived_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX idx_audit_log_archive_entity_type ON audit_log_archive(entity_type);
CREATE INDEX idx_audit_log_archive_entity_id ON audit_log_archive(entity_id);
CREATE INDEX idx_audit_log_archive_operation_type ON audit_log_archive(operation_type);
CREATE INDEX idx_audit_log_archive_operator ON audit_log_archive(operator);
CREATE INDEX idx_audit_log_archive_operation_time ON audit_log_archive(operation_time);
CREATE INDEX idx_audit_log_archive_archived_at ON audit_log_archive(archived_at);
COMMENT ON TABLE audit_log_archive IS '审计日志归档表';
COMMENT ON COLUMN audit_log_archive.id IS '主键ID';
COMMENT ON COLUMN audit_log_archive.entity_type IS '实体类型(如User, Role等)';
COMMENT ON COLUMN audit_log_archive.entity_id IS '实体ID';
COMMENT ON COLUMN audit_log_archive.operation_type IS '操作类型(CREATE, UPDATE, DELETE';
COMMENT ON COLUMN audit_log_archive.operator IS '操作人';
COMMENT ON COLUMN audit_log_archive.operation_time IS '操作时间';
COMMENT ON COLUMN audit_log_archive.before_data IS '变更前数据(JSON格式)';
COMMENT ON COLUMN audit_log_archive.after_data IS '变更后数据(JSON格式)';
COMMENT ON COLUMN audit_log_archive.changed_fields IS '变更字段列表';
COMMENT ON COLUMN audit_log_archive.ip_address IS 'IP地址';
COMMENT ON COLUMN audit_log_archive.user_agent IS '用户代理';
COMMENT ON COLUMN audit_log_archive.description IS '操作描述';
COMMENT ON COLUMN audit_log_archive.created_at IS '记录创建时间';
COMMENT ON COLUMN audit_log_archive.archived_at IS '归档时间';
@@ -72,7 +72,6 @@ class FlywayMigrationScriptTest {
List<Path> sqlFiles = Files.list(migrationDir)
.filter(p -> p.toString().endsWith(".sql"))
.sorted()
.collect(Collectors.toList());
List<Integer> versions = sqlFiles.stream()
@@ -81,6 +80,7 @@ class FlywayMigrationScriptTest {
String versionStr = filename.substring(1, filename.indexOf("__"));
return Integer.parseInt(versionStr);
})
.sorted()
.collect(Collectors.toList());
for (int i = 1; i < versions.size(); i++) {
+24 -2
View File
@@ -1,9 +1,31 @@
FROM openjdk:21-jdk-slim
# 简化Dockerfile - 使用本地编译好的jar文件(网关)
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
# 复制构建产物
COPY manage-gateway/target/manage-gateway-1.0.0.jar app.jar
# 设置JVM参数优化
ENV JAVA_OPTS="-Xmx512m -Xms256m -XX:+UseG1GC -XX:+UnlockExperimentalVMOptions -XX:+UseContainerSupport -Djava.security.egd=file:/dev/./urandom"
# 暴露端口
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
# 切换用户
USER novalon
# 健康检查
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD curl -f http://localhost:8080/actuator/health || exit 1
# 启动命令
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
@@ -198,6 +198,8 @@ public class JwtKeyServiceImpl implements JwtKeyService {
try {
String initialKey;
logger.info("Configured JWT secret: {}", configuredSecret != null ? "present (length: " + configuredSecret.length() + ")" : "null");
if (configuredSecret != null && !configuredSecret.isEmpty()) {
if (configuredSecret.startsWith("enc:")) {
initialKey = decryptKey(configuredSecret.substring(4));
@@ -216,6 +218,8 @@ public class JwtKeyServiceImpl implements JwtKeyService {
logger.info("Generated new secure JWT key");
}
logger.info("JWT key length: {}", initialKey.length());
SecretKey signingKey = new SecretKeySpec(
initialKey.getBytes(StandardCharsets.UTF_8),
KEY_ALGORITHM
@@ -7,6 +7,18 @@ spring:
predicates:
- Path=/api/**
jwt:
secret: novalon-novalon-manage-jwt-secret-key-for-development-only-2026
expiration: 86400000
signature:
enabled: false
resilience:
timeout:
enabled: true
duration: 10s
logging:
level:
org.springframework.cloud.gateway: TRACE
@@ -5,7 +5,7 @@ spring:
codec:
max-in-memory-size: 10MB
application:
name: manage-gateway
name: novalon-manage-gateway
cloud:
gateway:
routes:
@@ -146,4 +146,4 @@ management:
logging:
level:
cn.novalon.manage: DEBUG
org.springframework.cloud.gateway: DEBUG
org.springframework.cloud.gateway: DEBUG
@@ -2,171 +2,84 @@ package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.reflect.MethodSignature;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Persistable;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* 审计日志切面
*
* 文件定义:使用AOP自动拦截Repository操作,记录审计日志
* 涉及业务:自动记录所有数据变更操作,包括变更前后对比
* 算法:使用异步方式记录日志,不阻塞主流程
*
* @author 张翔
* @date 2026-04-01
*/
@Aspect
@Component
@Deprecated
public class AuditLogAspect {
private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);
private final IAuditLogService auditLogService;
private final ObjectMapper objectMapper;
public AuditLogAspect(IAuditLogService auditLogService, ObjectMapper objectMapper) {
public AuditLogAspect(IAuditLogService auditLogService) {
this.auditLogService = auditLogService;
this.objectMapper = objectMapper;
logger.info("=== AuditLogAspect 初始化完成 ===");
}
@Around("execution(* cn.novalon.manage.db.repository.*Repository.save(..)) || " +
"execution(* cn.novalon.manage.db.repository.*Repository.delete(..)) || " +
"execution(* cn.novalon.manage.db.repository.*Repository.deleteById(..))")
public Object logAuditEvent(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
@Before("execution(* cn.novalon.manage.sys.core.service.impl.SysUserService.createUser(..))")
public void testAopWorking() {
logger.info("=== AuditLogAspect @Before 测试: SysUserService.createUser 被调用 ===");
}
@Around("@annotation(auditable)")
public Object logAuditEvent(ProceedingJoinPoint joinPoint, Auditable auditable) throws Throwable {
String methodName = ((MethodSignature) joinPoint.getSignature()).getName();
String className = joinPoint.getTarget().getClass().getSimpleName();
Object[] args = joinPoint.getArgs();
String operationType = determineOperationType(methodName);
String entityType = extractEntityType(className);
logger.debug("拦截审计操作: {}.{}, 操作类型: {}, 实体类型: {}",
className, methodName, operationType, entityType);
try {
if ("save".equals(methodName) && args.length > 0) {
return handleSaveOperation(joinPoint, args[0], entityType, operationType);
} else if ("delete".equals(methodName) || "deleteById".equals(methodName)) {
return handleDeleteOperation(joinPoint, args, entityType, operationType);
}
return joinPoint.proceed();
} catch (Throwable error) {
logger.error("审计日志记录失败: {}", error.getMessage(), error);
throw error;
}
}
String entityType = auditable.entityType();
String operationType = auditable.operationType();
logger.debug("审计切面拦截: {}.{}(), entityType={}, operationType={}", className, methodName, entityType, operationType);
private Object handleSaveOperation(ProceedingJoinPoint joinPoint, Object entity,
String entityType, String operationType) throws Throwable {
try {
final String[] beforeDataHolder = {null};
final Long[] entityIdHolder = {null};
final String[] operationTypeHolder = {operationType};
if (entity instanceof Persistable) {
Persistable<?> persistable = (Persistable<?>) entity;
entityIdHolder[0] = persistable.getId() != null ?
((Number) persistable.getId()).longValue() : null;
if (entityIdHolder[0] != null) {
beforeDataHolder[0] = fetchEntityBeforeData(entityType, entityIdHolder[0]);
operationTypeHolder[0] = "UPDATE";
} else {
operationTypeHolder[0] = "CREATE";
}
}
Object result = joinPoint.proceed();
if (result instanceof Mono) {
return ((Mono<?>) result).flatMap(savedEntity -> {
String afterData = serializeEntity(savedEntity);
Long finalEntityId = entityIdHolder[0] != null ? entityIdHolder[0] : extractEntityId(savedEntity);
String finalOperationType = operationTypeHolder[0];
String finalBeforeData = beforeDataHolder[0];
logger.debug("保存操作审计日志: entityType={}, entityIdHolder={}, extractedEntityId={}, finalEntityId={}",
entityType, entityIdHolder[0], extractEntityId(savedEntity), finalEntityId);
return ((Mono<Object>) result).flatMap(retValue -> {
Long entityId = extractIdFromResult(retValue);
String afterData = serializeEntity(retValue);
return createAndSaveAuditLog(
entityType, finalEntityId, finalOperationType,
finalBeforeData, afterData, savedEntity
).thenReturn(savedEntity);
entityType, entityId, operationType,
null, afterData
).thenReturn(retValue);
});
}
return result;
} catch (Throwable error) {
logger.error("保存操作审计日志记录失败", error);
throw error;
}
}
private Object handleDeleteOperation(ProceedingJoinPoint joinPoint, Object[] args,
String entityType, String operationType) throws Throwable {
try {
Long entityId = null;
String beforeData = null;
if (args.length > 0) {
if (args[0] instanceof Number) {
entityId = ((Number) args[0]).longValue();
beforeData = fetchEntityBeforeData(entityType, entityId);
} else if (args[0] instanceof Persistable) {
Persistable<?> persistable = (Persistable<?>) args[0];
entityId = persistable.getId() != null ?
((Number) persistable.getId()).longValue() : null;
beforeData = serializeEntity(args[0]);
}
}
Object result = joinPoint.proceed();
if (result instanceof Mono) {
Long finalEntityId = entityId;
String finalBeforeData = beforeData;
return ((Mono<?>) result).flatMap(deleted ->
createAndSaveAuditLog(
entityType, finalEntityId, "DELETE",
finalBeforeData, null, null
).thenReturn(deleted)
);
} else if (result instanceof Flux) {
Long finalEntityId = entityId;
String finalBeforeData = beforeData;
return ((Flux<?>) result).flatMap(deleted ->
createAndSaveAuditLog(
entityType, finalEntityId, "DELETE",
finalBeforeData, null, null
).thenReturn(deleted)
);
return ((Flux<Object>) result).collectList()
.flatMapMany(list -> {
String afterData = serializeEntity(list);
return createAndSaveAuditLog(
entityType, null, operationType,
null, afterData
).thenMany(Flux.fromIterable(list));
});
}
return result;
} catch (Throwable error) {
logger.error("删除操作审计日志记录失败", error);
logger.error("审计日志记录失败: {}.{}()", className, methodName, error);
throw error;
}
}
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
String operationType, String beforeData,
String afterData, Object entity) {
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
String operationType, String beforeData,
String afterData) {
logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType);
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.defaultIfEmpty("system")
@@ -178,123 +91,67 @@ public class AuditLogAspect {
auditLog.setOperator(principal instanceof String ? (String) principal : "system");
auditLog.setBeforeData(beforeData);
auditLog.setAfterData(afterData);
logger.debug("审计日志对象: entityId={}, entityType={}, operationType={}",
auditLog.getEntityId(), auditLog.getEntityType(), auditLog.getOperationType());
if (beforeData != null && afterData != null) {
String[] changedFields = extractChangedFields(beforeData, afterData);
auditLog.setChangedFields(changedFields);
}
auditLog.setDescription(generateDescription(entityType, operationType, entityId));
return auditLogService.save(auditLog)
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}",
entityType, operationType))
.doOnError(error -> logger.error("审计日志保存失败: {}",
error.getMessage()))
return auditLogService.saveAsync(auditLog)
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}, ID={}",
entityType, operationType, saved.getId()))
.doOnError(error -> logger.error("审计日志保存失败: {}", error.getMessage()))
.then();
})
.onErrorResume(error -> {
logger.error("创建审计日志失败,但不影响主流程: {}", error.getMessage());
logger.error("创建审计日志失败,但不影响主流程: {}", error.getMessage(), error);
return Mono.empty();
});
}
private String determineOperationType(String methodName) {
if (methodName.startsWith("save")) {
return "SAVE";
} else if (methodName.startsWith("delete")) {
return "DELETE";
private Long extractIdFromResult(Object result) {
if (result == null) {
return null;
}
return "UNKNOWN";
}
private String extractEntityType(String className) {
if (className.contains("User")) {
return "User";
} else if (className.contains("Role")) {
return "Role";
} else if (className.contains("Menu")) {
return "Menu";
} else if (className.contains("Permission")) {
return "Permission";
try {
var getIdMethod = result.getClass().getMethod("getId");
Object id = getIdMethod.invoke(result);
if (id instanceof Number) {
return ((Number) id).longValue();
}
if (id instanceof String) {
try {
return Long.parseLong((String) id);
} catch (NumberFormatException e) {
return null;
}
}
} catch (NoSuchMethodException e) {
logger.debug("结果对象没有getId方法: {}", result.getClass().getSimpleName());
} catch (Exception e) {
logger.debug("提取结果ID失败: {}", e.getMessage());
}
return className.replace("Repository", "").replace("Impl", "");
}
private String fetchEntityBeforeData(String entityType, Long entityId) {
return null;
}
private String serializeEntity(Object entity) {
try {
return objectMapper.writeValueAsString(entity);
ObjectMapper mapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
return mapper.writeValueAsString(entity);
} catch (Exception e) {
logger.error("序列化实体失败: {}", e.getMessage());
return null;
}
}
private Long extractEntityId(Object entity) {
logger.debug("提取实体ID: entity class={}", entity.getClass().getName());
if (entity instanceof Persistable) {
Persistable<?> persistable = (Persistable<?>) entity;
Object id = persistable.getId();
logger.debug("Persistable实体ID: id={}, isNew={}", id, persistable.isNew());
return id != null ? ((Number) id).longValue() : null;
}
logger.debug("实体不是Persistable类型");
return null;
}
private String[] extractChangedFields(String beforeData, String afterData) {
try {
JsonNode beforeNode = objectMapper.readTree(beforeData);
JsonNode afterNode = objectMapper.readTree(afterData);
List<String> changedFields = new ArrayList<>();
beforeNode.fieldNames().forEachRemaining(fieldName -> {
JsonNode beforeValue = beforeNode.get(fieldName);
JsonNode afterValue = afterNode.get(fieldName);
if (afterValue == null || !beforeValue.equals(afterValue)) {
changedFields.add(fieldName);
}
});
afterNode.fieldNames().forEachRemaining(fieldName -> {
if (!beforeNode.has(fieldName)) {
changedFields.add(fieldName);
}
});
return changedFields.toArray(new String[0]);
} catch (Exception e) {
logger.error("提取变更字段失败: {}", e.getMessage());
return new String[0];
}
}
private String generateDescription(String entityType, String operationType, Long entityId) {
String operation = "";
switch (operationType) {
case "CREATE":
operation = "创建";
break;
case "UPDATE":
operation = "更新";
break;
case "DELETE":
operation = "删除";
break;
default:
operation = "操作";
}
return String.format("%s%s (ID: %s)", operation, entityType,
entityId != null ? entityId : "未知");
String operation = switch (operationType) {
case "CREATE" -> "创建";
case "UPDATE" -> "更新";
case "DELETE" -> "删除";
default -> "操作";
};
return String.format("%s%s (ID: %s)", operation, entityType,
entityId != null ? entityId : "未知");
}
}
@@ -0,0 +1,80 @@
package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.audit.domain.AuditLog;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import reactor.core.publisher.Mono;
public final class AuditLogHelper {
private static final Logger logger = LoggerFactory.getLogger(AuditLogHelper.class);
private static final ObjectMapper objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
private AuditLogHelper() {}
public static Mono<Void> record(IAuditLogService auditLogService,
String entityType, Long entityId,
String operationType, Object afterEntity) {
return record(auditLogService, entityType, entityId, operationType, null, afterEntity);
}
public static Mono<Void> record(IAuditLogService auditLogService,
String entityType, Long entityId,
String operationType, Object beforeEntity, Object afterEntity) {
return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal())
.defaultIfEmpty("system")
.flatMap(principal -> {
AuditLog auditLog = new AuditLog();
auditLog.generateId();
auditLog.setEntityType(entityType);
auditLog.setEntityId(entityId != null ? entityId : 0L);
auditLog.setOperationType(operationType);
auditLog.setOperator(principal instanceof String ? (String) principal : "system");
auditLog.setBeforeData(serializeEntity(beforeEntity));
auditLog.setAfterData(serializeEntity(afterEntity));
auditLog.setDescription(generateDescription(entityType, operationType, entityId));
logger.info("记录审计日志: {} {} ID={}, operator={}", operationType, entityType, entityId, auditLog.getOperator());
return auditLogService.saveAsync(auditLog)
.doOnSuccess(saved -> logger.info("审计日志保存成功: {} - {}, ID={}",
entityType, operationType, saved.getId()))
.doOnError(error -> logger.error("审计日志保存失败: {}", error.getMessage()))
.then();
})
.onErrorResume(error -> {
logger.error("记录审计日志失败,但不影响主流程: {}", error.getMessage(), error);
return Mono.empty();
});
}
private static String serializeEntity(Object entity) {
try {
if (entity == null) return null;
return objectMapper.writeValueAsString(entity);
} catch (Exception e) {
logger.error("序列化实体失败: {}", e.getMessage());
return null;
}
}
private static String generateDescription(String entityType, String operationType, Long entityId) {
String operation = switch (operationType) {
case "CREATE" -> "创建";
case "UPDATE" -> "更新";
case "DELETE" -> "删除";
default -> "操作";
};
return String.format("%s%s (ID: %s)", operation, entityType,
entityId != null ? entityId : "未知");
}
}
@@ -0,0 +1,15 @@
package cn.novalon.manage.sys.audit;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Auditable {
String entityType();
String operationType() default "CREATE";
String description() default "";
}
@@ -0,0 +1,181 @@
package cn.novalon.manage.sys.audit;
import cn.novalon.manage.sys.core.domain.OperationLog;
import cn.novalon.manage.sys.core.service.IOperationLogService;
import cn.novalon.manage.sys.util.IpUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.annotation.PostConstruct;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBufferUtils;
import org.springframework.http.HttpMethod;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.HandlerStrategies;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import reactor.core.publisher.Mono;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Component
@Order(Ordered.LOWEST_PRECEDENCE)
public class OperationLogWebFilter implements WebFilter {
private static final Logger logger = LoggerFactory.getLogger(OperationLogWebFilter.class);
private final IOperationLogService operationLogService;
private final ObjectMapper objectMapper;
private static final Map<String, OperationInfo> OPERATION_MAPPING = new ConcurrentHashMap<>();
static {
OPERATION_MAPPING.put("POST:/api/roles", new OperationInfo("角色管理", "创建角色"));
OPERATION_MAPPING.put("PUT:/api/roles/", new OperationInfo("角色管理", "更新角色"));
OPERATION_MAPPING.put("DELETE:/api/roles/", new OperationInfo("角色管理", "删除角色"));
OPERATION_MAPPING.put("POST:/api/users", new OperationInfo("用户管理", "创建用户"));
OPERATION_MAPPING.put("PUT:/api/users/", new OperationInfo("用户管理", "更新用户"));
OPERATION_MAPPING.put("DELETE:/api/users/", new OperationInfo("用户管理", "删除用户"));
OPERATION_MAPPING.put("POST:/api/users/", new OperationInfo("用户管理", "用户操作"));
OPERATION_MAPPING.put("POST:/api/menus", new OperationInfo("菜单管理", "创建菜单"));
OPERATION_MAPPING.put("PUT:/api/menus/", new OperationInfo("菜单管理", "更新菜单"));
OPERATION_MAPPING.put("DELETE:/api/menus/", new OperationInfo("菜单管理", "删除菜单"));
}
public OperationLogWebFilter(IOperationLogService operationLogService, ObjectMapper objectMapper) {
logger.info("=== OperationLogWebFilter 构造函数被调用 ===");
this.operationLogService = operationLogService;
this.objectMapper = objectMapper;
}
@PostConstruct
public void init() {
logger.info("=== OperationLogWebFilter 初始化 ===");
logger.info("操作日志映射配置数量: {}", OPERATION_MAPPING.size());
OPERATION_MAPPING.forEach((key, value) -> {
logger.info(" {} -> {}:{}", key, value.module, value.operation);
});
}
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String method = request.getMethod().name();
String path = request.getPath().value();
logger.info("WebFilter 拦截请求: {} {}", method, path);
OperationInfo operationInfo = findOperationInfo(method, path);
if (operationInfo == null) {
logger.info("未匹配到操作日志配置,跳过: {} {}", method, path);
return chain.filter(exchange);
}
logger.info("匹配到操作日志配置: {} {} -> {}:{}", method, path, operationInfo.module, operationInfo.operation);
long startTime = System.currentTimeMillis();
String ip = IpUtils.getClientIp(request);
return Mono.deferContextual(contextView -> {
return chain.filter(exchange)
.then(Mono.defer(() -> {
long duration = System.currentTimeMillis() - startTime;
logger.info("请求处理完成,准备保存操作日志: {} {}, 耗时: {}ms", method, path, duration);
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : "system";
logger.info("获取到用户名: {}", username);
return Mono.just(username);
})
.defaultIfEmpty("system")
.flatMap(username -> {
logger.info("开始保存操作日志: 用户={}, 操作={}", username,
operationInfo.module + " - " + operationInfo.operation);
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
log.setMethod(method + " " + path);
log.setParams(null);
log.setIp(ip);
log.setDuration(duration);
log.setStatus("0");
return operationLogService.save(log)
.doOnSuccess(saved -> logger.info("操作日志保存成功: {} - {}",
operationInfo.module, operationInfo.operation))
.doOnError(e -> logger.error("操作日志保存失败: {}", e.getMessage(), e))
.onErrorResume(e -> Mono.empty());
})
.then();
}))
.onErrorResume(error -> {
long duration = System.currentTimeMillis() - startTime;
logger.error("请求处理失败: {} {}, 错误: {}", method, path, error.getMessage());
return ReactiveSecurityContextHolder.getContext()
.flatMap(securityContext -> {
Object principal = securityContext.getAuthentication().getPrincipal();
String username = principal instanceof String ? (String) principal : "system";
return Mono.just(username);
})
.defaultIfEmpty("system")
.flatMap(username -> {
OperationLog log = new OperationLog();
log.setUsername(username);
log.setOperation(operationInfo.module + " - " + operationInfo.operation);
log.setMethod(method + " " + path);
log.setParams(null);
log.setIp(ip);
log.setDuration(duration);
log.setStatus("1");
log.setErrorMsg(error.getMessage());
return operationLogService.save(log)
.doOnError(e -> logger.error("错误日志保存失败: {}", e.getMessage()))
.onErrorResume(e -> Mono.empty());
})
.then(Mono.error(error));
});
});
}
private OperationInfo findOperationInfo(String method, String path) {
String key = method + ":" + path;
if (OPERATION_MAPPING.containsKey(key)) {
return OPERATION_MAPPING.get(key);
}
for (Map.Entry<String, OperationInfo> entry : OPERATION_MAPPING.entrySet()) {
String mappingKey = entry.getKey();
if (key.startsWith(mappingKey)) {
return entry.getValue();
}
}
return null;
}
private static class OperationInfo {
final String module;
final String operation;
OperationInfo(String module, String operation) {
this.module = module;
this.operation = operation;
}
}
}
@@ -139,6 +139,29 @@ public class AuditLog extends BaseDomain {
this.description = description;
}
@Override
public String toString() {
return "AuditLog{" +
"id=" + id +
", entityType='" + entityType + '\'' +
", entityId=" + entityId +
", operationType='" + operationType + '\'' +
", operator='" + operator + '\'' +
", operationTime=" + operationTime +
", beforeData='" + beforeData + '\'' +
", afterData='" + afterData + '\'' +
", changedFields=" + java.util.Arrays.toString(changedFields) +
", ipAddress='" + ipAddress + '\'' +
", userAgent='" + userAgent + '\'' +
", description='" + description + '\'' +
", createBy='" + createBy + '\'' +
", updateBy='" + updateBy + '\'' +
", createdAt=" + createdAt +
", updatedAt=" + updatedAt +
", deletedAt=" + deletedAt +
'}';
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
@@ -39,7 +39,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Long> archiveOldLogs(int daysToKeep) {
LocalDateTime archiveBefore = LocalDateTime.now().minusDays(daysToKeep);
@@ -53,7 +53,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<AuditLogArchive> archiveLog(AuditLog auditLog) {
AuditLogArchive archive = convertToArchive(auditLog);
@@ -99,7 +99,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteArchivedLogsOlderThan(LocalDateTime date) {
return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date)
.flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId()))
@@ -150,7 +150,6 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Async("auditLogExecutor")
public Mono<AuditLog> saveAsync(AuditLog auditLog) {
logger.debug("异步保存审计日志: {} - {}", auditLog.getEntityType(), auditLog.getOperationType());
@@ -161,13 +160,13 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteById(Long id) {
return auditLogRepository.deleteById(id);
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> logicalDeleteById(Long id) {
return auditLogRepository.findById(id)
.flatMap(auditLog -> {
@@ -178,7 +177,7 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> logicalDeleteByIds(List<Long> ids) {
return Flux.fromIterable(ids)
.flatMap(this::logicalDeleteById)
@@ -186,7 +185,7 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> restoreById(Long id) {
return auditLogRepository.findById(id)
.flatMap(auditLog -> {
@@ -197,7 +196,7 @@ public class AuditLogService implements IAuditLogService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> restoreByIds(List<Long> ids) {
return Flux.fromIterable(ids)
.flatMap(this::restoreById)
@@ -1,5 +1,6 @@
package cn.novalon.manage.sys.config;
import cn.novalon.manage.sys.audit.OperationLogWebFilter;
import cn.novalon.manage.sys.security.JwtAuthenticationFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -11,22 +12,20 @@ import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* 安全配置类
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration
@EnableWebFluxSecurity
public class SecurityConfig {
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final OperationLogWebFilter operationLogWebFilter;
private final Environment environment;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, Environment environment) {
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter,
OperationLogWebFilter operationLogWebFilter,
Environment environment) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.operationLogWebFilter = operationLogWebFilter;
this.environment = environment;
}
@@ -46,6 +45,7 @@ public class SecurityConfig {
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAfter(operationLogWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.authorizeExchange(spec -> {
spec.pathMatchers("/api/auth/**").permitAll()
.pathMatchers("/api/public/**").permitAll()
@@ -76,10 +76,29 @@ public abstract class BaseDomain {
return this.id;
}
/**
* 删除(幂等操作)
* 已删除的对象不会更新删除时间
*/
public void delete() {
if (this.deletedAt == null) {
this.deletedAt = LocalDateTime.now();
}
}
/**
* 恢复已删除的对象
*/
public void restore() {
this.deletedAt = null;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
BaseDomain that = (BaseDomain) o;
return id != null && id.equals(that.id);
}
@@ -77,18 +77,4 @@ public class SysPermission extends BaseDomain {
public void setStatus(Integer status) {
this.status = status;
}
/**
* 删除权限
*/
public void delete() {
this.deletedAt = java.time.LocalDateTime.now();
}
/**
* 恢复权限
*/
public void restore() {
this.deletedAt = null;
}
}
@@ -57,18 +57,4 @@ public class SysRole extends BaseDomain {
public void setStatus(Integer status) {
this.status = status;
}
/**
* 删除角色
*/
public void delete() {
this.deletedAt = LocalDateTime.now();
}
/**
* 恢复角色
*/
public void restore() {
this.deletedAt = null;
}
}
@@ -100,11 +100,4 @@ public class SysUser extends BaseDomain {
public void setStatus(Integer status) {
this.status = status;
}
/**
* 删除用户
*/
public void delete() {
this.deletedAt = LocalDateTime.now();
}
}
@@ -24,6 +24,8 @@ public interface ISysMenuRepository {
Mono<SysMenu> save(SysMenu sysMenu);
Mono<SysMenu> update(SysMenu sysMenu);
Mono<Void> deleteById(Long id);
Flux<SysMenu> findAll();
@@ -28,6 +28,8 @@ public interface ISysUserRepository {
Mono<SysUser> save(SysUser sysUser);
Mono<SysUser> update(SysUser sysUser);
Mono<Void> deleteById(Long id);
Flux<SysUser> findAll();
@@ -48,13 +48,11 @@ public class DictionaryService implements IDictionaryService {
@Override
public Mono<Dictionary> save(Dictionary dictionary) {
if (dictionary.getId() == null) {
dictionary.setCreatedAt(LocalDateTime.now());
return checkTypeAndCodeExists(dictionary.getType(), dictionary.getCode())
.flatMap(exists -> {
if (exists) {
return Mono.error(new DictionaryAlreadyExistsException(dictionary.getType(), dictionary.getCode()));
}
dictionary.setUpdatedAt(LocalDateTime.now());
return repository.save(dictionary);
});
}
@@ -29,7 +29,6 @@ public class OperationLogService implements IOperationLogService {
@Override
public Mono<OperationLog> save(OperationLog log) {
log.setCreatedAt(LocalDateTime.now());
return logRepository.save(log);
}
@@ -1,27 +1,23 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysConfig;
import cn.novalon.manage.sys.core.repository.ISysConfigRepository;
import cn.novalon.manage.sys.core.service.ISysConfigService;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 系统配置服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service
public class SysConfigService implements ISysConfigService {
private final ISysConfigRepository repository;
private final IAuditLogService auditLogService;
public SysConfigService(ISysConfigRepository repository) {
public SysConfigService(ISysConfigRepository repository, IAuditLogService auditLogService) {
this.repository = repository;
this.auditLogService = auditLogService;
}
@Override
@@ -30,27 +26,28 @@ public class SysConfigService implements ISysConfigService {
}
@Override
@Cacheable(value = "sysConfig", key = "#id")
public Mono<SysConfig> findById(Long id) {
return repository.findById(id);
}
@Override
@Cacheable(value = "sysConfig", key = "#configKey")
public Mono<SysConfig> findByConfigKey(String configKey) {
return repository.findByConfigKeyAndDeletedAtIsNull(configKey);
}
@Override
@CacheEvict(value = "sysConfig", allEntries = true)
public Mono<SysConfig> save(SysConfig config) {
return repository.save(config);
return repository.save(config)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Config", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@CacheEvict(value = "sysConfig", key = "#id")
public Mono<Void> deleteById(Long id) {
return repository.deleteByIdAndDeletedAtIsNull(id);
return repository.findById(id)
.flatMap(config -> repository.deleteByIdAndDeletedAtIsNull(id)
.then(AuditLogHelper.record(auditLogService, "Config", id, "DELETE", config, null)))
.then();
}
@Override
@@ -1,5 +1,7 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysDictType;
import cn.novalon.manage.sys.core.repository.ISysDictTypeRepository;
import cn.novalon.manage.sys.core.service.ISysDictTypeService;
@@ -7,19 +9,15 @@ import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
/**
* 字典类型服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service
public class SysDictTypeService implements ISysDictTypeService {
private final ISysDictTypeRepository repository;
private final IAuditLogService auditLogService;
public SysDictTypeService(ISysDictTypeRepository repository) {
public SysDictTypeService(ISysDictTypeRepository repository, IAuditLogService auditLogService) {
this.repository = repository;
this.auditLogService = auditLogService;
}
@Override
@@ -39,11 +37,16 @@ public class SysDictTypeService implements ISysDictTypeService {
@Override
public Mono<SysDictType> save(SysDictType dictType) {
return repository.save(dictType);
return repository.save(dictType)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Dict", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<Void> deleteById(Long id) {
return repository.deleteByIdAndDeletedAtIsNull(id);
return repository.findById(id)
.flatMap(dict -> repository.deleteByIdAndDeletedAtIsNull(id)
.then(AuditLogHelper.record(auditLogService, "Dict", id, "DELETE", dict, null)))
.then();
}
}
@@ -6,6 +6,8 @@ import cn.novalon.manage.sys.core.service.ISysMenuService;
import cn.novalon.manage.sys.core.command.CreateMenuCommand;
import cn.novalon.manage.sys.core.command.UpdateMenuCommand;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@@ -24,9 +26,11 @@ import java.util.stream.Collectors;
public class SysMenuService implements ISysMenuService {
private final ISysMenuRepository menuRepository;
private final IAuditLogService auditLogService;
public SysMenuService(ISysMenuRepository menuRepository) {
public SysMenuService(ISysMenuRepository menuRepository, IAuditLogService auditLogService) {
this.menuRepository = menuRepository;
this.auditLogService = auditLogService;
}
@Override
@@ -46,8 +50,9 @@ public class SysMenuService implements ISysMenuService {
@Override
public Mono<SysMenu> createMenu(SysMenu menu) {
menu.setCreatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.save(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@@ -60,14 +65,18 @@ public class SysMenuService implements ISysMenuService {
menu.setComponent(command.component());
menu.setPerms(command.perms());
menu.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
menu.setCreatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.save(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<SysMenu> updateMenu(SysMenu menu) {
menu.setUpdatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.findById(menu.getId())
.flatMap(before -> menuRepository.update(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
@@ -75,6 +84,15 @@ public class SysMenuService implements ISysMenuService {
return menuRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("Menu not found")))
.flatMap(menu -> {
SysMenu before = new SysMenu();
before.setId(menu.getId());
before.setParentId(menu.getParentId());
before.setMenuName(menu.getMenuName());
before.setMenuType(menu.getMenuType());
before.setOrderNum(menu.getOrderNum());
before.setComponent(menu.getComponent());
before.setPerms(menu.getPerms());
before.setStatus(menu.getStatus());
if (command.parentId() != null) {
menu.setParentId(command.parentId());
}
@@ -97,13 +115,18 @@ public class SysMenuService implements ISysMenuService {
menu.setStatus(command.status());
}
menu.setUpdatedAt(LocalDateTime.now());
return menuRepository.save(menu);
return menuRepository.update(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
});
}
@Override
public Mono<Void> deleteMenu(Long id) {
return menuRepository.deleteById(id);
return menuRepository.findById(id)
.flatMap(menu -> menuRepository.deleteById(id)
.then(AuditLogHelper.record(auditLogService, "Menu", id, "DELETE", menu, null)))
.then();
}
@Override
@@ -1,11 +1,15 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysPermission;
import cn.novalon.manage.sys.core.domain.SysRolePermission;
import cn.novalon.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.manage.sys.core.service.ISysPermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -24,13 +28,18 @@ import java.util.List;
@Service
public class SysPermissionService implements ISysPermissionService {
private static final Logger logger = LoggerFactory.getLogger(SysPermissionService.class);
private final ISysPermissionRepository permissionRepository;
private final ISysRolePermissionRepository rolePermissionRepository;
private final IAuditLogService auditLogService;
public SysPermissionService(ISysPermissionRepository permissionRepository,
ISysRolePermissionRepository rolePermissionRepository) {
ISysRolePermissionRepository rolePermissionRepository,
IAuditLogService auditLogService) {
this.permissionRepository = permissionRepository;
this.rolePermissionRepository = rolePermissionRepository;
this.auditLogService = auditLogService;
}
@Override
@@ -60,25 +69,41 @@ public class SysPermissionService implements ISysPermissionService {
@Override
public Mono<SysPermission> createPermission(SysPermission permission) {
permission.setCreatedAt(LocalDateTime.now());
if (permission.getStatus() == null) {
permission.setStatus(StatusConstants.ENABLED);
}
return permissionRepository.save(permission);
return permissionRepository.save(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", saved.getId(), "CREATE", saved)
.doOnError(e -> logger.error("Audit log failed for Permission CREATE id={}: {}", saved.getId(), e.getMessage()))
.thenReturn(saved));
}
@Override
public Mono<SysPermission> updatePermission(SysPermission permission) {
permission.setUpdatedAt(LocalDateTime.now());
return permissionRepository.updatePermission(permission);
return permissionRepository.findById(permission.getId())
.flatMap(before -> permissionRepository.updatePermission(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
public Mono<Void> deletePermission(Long id) {
return permissionRepository.findById(id)
.flatMap(permission -> {
SysPermission before = new SysPermission();
before.setId(permission.getId());
before.setPermissionName(permission.getPermissionName());
before.setPermissionCode(permission.getPermissionCode());
before.setResource(permission.getResource());
before.setAction(permission.getAction());
before.setStatus(permission.getStatus());
before.setCreatedAt(permission.getCreatedAt());
before.setUpdatedAt(permission.getUpdatedAt());
before.setDeletedAt(permission.getDeletedAt());
permission.delete();
return permissionRepository.updatePermission(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", id, "DELETE", before, saved))
.then(rolePermissionRepository.deleteByPermissionId(id));
});
}
@@ -99,7 +124,7 @@ public class SysPermissionService implements ISysPermissionService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
return rolePermissionRepository.deleteByRoleId(roleId)
.then(Flux.fromIterable(permissionIds)
@@ -107,7 +132,6 @@ public class SysPermissionService implements ISysPermissionService {
SysRolePermission rolePermission = new SysRolePermission();
rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(permissionId);
rolePermission.setCreatedAt(LocalDateTime.now());
return rolePermissionRepository.save(rolePermission);
})
.then());
@@ -1,6 +1,8 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysRole;
import cn.novalon.manage.sys.core.query.SysRoleQuery;
import cn.novalon.manage.sys.core.repository.ISysRoleRepository;
@@ -21,12 +23,6 @@ import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
/**
* 系统角色服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service
public class SysRoleService implements ISysRoleService {
@@ -35,13 +31,16 @@ public class SysRoleService implements ISysRoleService {
private final ISysUserService userService;
private final IUserRoleRepository userRoleRepository;
private final ISysRolePermissionRepository rolePermissionRepository;
private final IAuditLogService auditLogService;
public SysRoleService(ISysRoleRepository roleRepository, ISysUserService userService,
IUserRoleRepository userRoleRepository, ISysRolePermissionRepository rolePermissionRepository) {
IUserRoleRepository userRoleRepository, ISysRolePermissionRepository rolePermissionRepository,
IAuditLogService auditLogService) {
this.roleRepository = roleRepository;
this.userService = userService;
this.userRoleRepository = userRoleRepository;
this.rolePermissionRepository = rolePermissionRepository;
this.auditLogService = auditLogService;
}
@Override
@@ -76,7 +75,9 @@ public class SysRoleService implements ISysRoleService {
if (role.getStatus() == null) {
role.setStatus(StatusConstants.ENABLED);
}
return roleRepository.save(role);
return roleRepository.save(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@@ -88,13 +89,18 @@ public class SysRoleService implements ISysRoleService {
role.setRoleSort(command.roleSort());
role.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
role.setCreatedAt(LocalDateTime.now());
return roleRepository.save(role);
return roleRepository.save(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<SysRole> updateRole(SysRole role) {
role.setUpdatedAt(LocalDateTime.now());
return roleRepository.save(role);
return roleRepository.findById(role.getId())
.flatMap(before -> roleRepository.updateRole(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
@@ -102,6 +108,15 @@ public class SysRoleService implements ISysRoleService {
return roleRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("Role not found")))
.flatMap(role -> {
SysRole before = new SysRole();
before.setId(role.getId());
before.setRoleName(role.getRoleName());
before.setRoleKey(role.getRoleKey());
before.setRoleSort(role.getRoleSort());
before.setStatus(role.getStatus());
before.setCreatedAt(role.getCreatedAt());
before.setUpdatedAt(role.getUpdatedAt());
before.setDeletedAt(role.getDeletedAt());
if (command.roleName() != null) {
role.setRoleName(command.roleName());
}
@@ -115,15 +130,17 @@ public class SysRoleService implements ISysRoleService {
role.setStatus(command.status());
}
role.setUpdatedAt(LocalDateTime.now());
return roleRepository.save(role);
return roleRepository.updateRole(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
});
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteRole(Long id) {
logger.debug("开始删除角色,ID: {}", id);
return roleRepository.findById(id)
.flatMap(role -> {
logger.debug("找到角色,开始删除关联记录");
@@ -138,7 +155,8 @@ public class SysRoleService implements ISysRoleService {
.doOnError(e -> logger.error("更新用户角色ID失败", e))
.then(roleRepository.deleteById(id))
.doOnSuccess(v -> logger.debug("成功删除角色"))
.doOnError(e -> logger.error("删除角色失败", e));
.doOnError(e -> logger.error("删除角色失败", e))
.then(AuditLogHelper.record(auditLogService, "Role", id, "DELETE", role, null));
});
}
@@ -156,8 +174,19 @@ public class SysRoleService implements ISysRoleService {
public Mono<SysRole> logicalDeleteRole(Long id) {
return roleRepository.findByIdIncludingDeleted(id)
.flatMap(role -> {
SysRole before = new SysRole();
before.setId(role.getId());
before.setRoleName(role.getRoleName());
before.setRoleKey(role.getRoleKey());
before.setRoleSort(role.getRoleSort());
before.setStatus(role.getStatus());
before.setCreatedAt(role.getCreatedAt());
before.setUpdatedAt(role.getUpdatedAt());
before.setDeletedAt(role.getDeletedAt());
role.delete();
return roleRepository.updateRole(role);
return roleRepository.updateRole(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "DELETE", before, saved)
.thenReturn(saved));
});
}
@@ -1,9 +1,12 @@
package cn.novalon.manage.sys.core.service.impl;
import cn.novalon.manage.common.util.StatusConstants;
import cn.novalon.manage.sys.audit.AuditLogHelper;
import cn.novalon.manage.sys.audit.service.IAuditLogService;
import cn.novalon.manage.sys.core.domain.SysUser;
import cn.novalon.manage.sys.core.domain.SysRole;
import cn.novalon.manage.sys.core.domain.UserRole;
import cn.novalon.manage.sys.core.query.SysUserQuery;
import cn.novalon.manage.common.dto.PageRequest;
import cn.novalon.manage.common.dto.PageResponse;
import cn.novalon.manage.sys.core.repository.ISysUserRepository;
@@ -25,16 +28,6 @@ import reactor.core.publisher.Mono;
import java.time.LocalDateTime;
import java.util.List;
/**
* 用户服务实现类
*
* 文件定义:实现用户管理的核心业务逻辑
* 涉及业务:用户注册、登录、信息修改、删除、密码修改、逻辑删除等用户生命周期管理
* 算法:使用R2DBC进行响应式数据库操作,支持分页查询、条件查询、批量操作
*
* @author 张翔
* @date 2026-03-13
*/
@Service
public class SysUserService implements ISysUserService {
@@ -43,15 +36,18 @@ public class SysUserService implements ISysUserService {
private final ISysRoleRepository roleRepository;
private final IUserRoleRepository userRoleRepository;
private final PasswordEncoder passwordEncoder;
private final IAuditLogService auditLogService;
public SysUserService(ISysUserRepository userRepository,
ISysRoleRepository roleRepository,
IUserRoleRepository userRoleRepository,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder,
IAuditLogService auditLogService) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.userRoleRepository = userRoleRepository;
this.passwordEncoder = passwordEncoder;
this.auditLogService = auditLogService;
logger.info("使用的密码编码器类型: {}", passwordEncoder.getClass().getName());
}
@@ -80,7 +76,9 @@ public class SysUserService implements ISysUserService {
@Override
public Mono<PageResponse<SysUser>> findUsersByPage(PageRequest pageRequest) {
return userRepository.findByQueryWithPagination(null, pageRequest);
SysUserQuery query = new SysUserQuery();
query.setKeyword(pageRequest.getKeyword());
return userRepository.findByQueryWithPagination(query, pageRequest);
}
@Override
@@ -110,7 +108,9 @@ public class SysUserService implements ISysUserService {
if (user.getStatus() == null) {
user.setStatus(StatusConstants.ENABLED);
}
return userRepository.save(user);
return userRepository.save(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
@@ -124,13 +124,18 @@ public class SysUserService implements ISysUserService {
user.setPhone(command.phone());
user.setRoleId(command.roleId());
user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
return userRepository.save(user);
return userRepository.save(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "CREATE", saved)
.thenReturn(saved));
}
@Override
public Mono<SysUser> updateUser(SysUser user) {
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.findById(user.getId())
.flatMap(before -> userRepository.update(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
}
@Override
@@ -138,6 +143,17 @@ public class SysUserService implements ISysUserService {
return userRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("User not found")))
.flatMap(user -> {
SysUser before = new SysUser();
before.setId(user.getId());
before.setUsername(user.getUsername());
before.setEmail(user.getEmail());
before.setNickname(user.getNickname());
before.setPhone(user.getPhone());
before.setRoleId(user.getRoleId());
before.setStatus(user.getStatus());
before.setCreatedAt(user.getCreatedAt());
before.setUpdatedAt(user.getUpdatedAt());
before.setDeletedAt(user.getDeletedAt());
if (command.username() != null) {
user.setUsername(command.username());
}
@@ -156,12 +172,15 @@ public class SysUserService implements ISysUserService {
user.setStatus(command.status());
}
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.update(user)
.flatMap(saved -> AuditLogHelper
.record(auditLogService, "User", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
});
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> deleteUser(Long id) {
logger.debug("开始删除用户,ID: {}", id);
@@ -174,7 +193,8 @@ public class SysUserService implements ISysUserService {
.doOnError(e -> logger.error("删除用户角色关联记录失败", e))
.then(userRepository.deleteById(id))
.doOnSuccess(v -> logger.debug("成功删除用户"))
.doOnError(e -> logger.error("删除用户失败", e));
.doOnError(e -> logger.error("删除用户失败", e))
.then(AuditLogHelper.record(auditLogService, "User", id, "DELETE", user, null));
});
}
@@ -192,7 +212,10 @@ public class SysUserService implements ISysUserService {
}
user.setPassword(passwordEncoder.encode(newPassword));
user.setUpdatedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.update(user)
.flatMap(saved -> AuditLogHelper
.record(auditLogService, "User", saved.getId(), "UPDATE", saved)
.thenReturn(saved));
});
}
@@ -214,8 +237,20 @@ public class SysUserService implements ISysUserService {
public Mono<Void> logicalDeleteUser(Long id) {
return userRepository.findByIdIncludingDeleted(id)
.flatMap(user -> {
SysUser before = new SysUser();
before.setId(user.getId());
before.setUsername(user.getUsername());
before.setEmail(user.getEmail());
before.setNickname(user.getNickname());
before.setPhone(user.getPhone());
before.setRoleId(user.getRoleId());
before.setStatus(user.getStatus());
before.setCreatedAt(user.getCreatedAt());
before.setUpdatedAt(user.getUpdatedAt());
before.setDeletedAt(user.getDeletedAt());
user.setDeletedAt(LocalDateTime.now());
return userRepository.save(user);
return userRepository.save(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "DELETE", before, saved));
})
.then();
}
@@ -241,7 +276,7 @@ public class SysUserService implements ISysUserService {
}
@Override
@Transactional
@Transactional(transactionManager = "connectionFactoryTransactionManager")
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
@@ -249,7 +284,8 @@ public class SysUserService implements ISysUserService {
logger.debug("角色列表为空,删除用户的所有角色关联");
return userRoleRepository.deleteByUserId(userId)
.doOnSuccess(v -> logger.debug("成功删除用户的所有角色关联"))
.doOnError(e -> logger.error("删除用户角色关联失败", e));
.doOnError(e -> logger.error("删除用户角色关联失败", e))
.then(AuditLogHelper.record(auditLogService, "User", userId, "UPDATE", null));
}
return userRoleRepository.deleteByUserId(userId)
@@ -262,12 +298,12 @@ public class SysUserService implements ISysUserService {
UserRole userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
userRole.setCreatedAt(LocalDateTime.now());
return userRoleRepository.save(userRole)
.doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
.doOnError(e -> logger.error("保存用户角色关联失败", e));
})
.then());
.then())
.then(AuditLogHelper.record(auditLogService, "User", userId, "UPDATE", null));
}
@Override
@@ -1,15 +1,25 @@
package cn.novalon.manage.sys.dto.request;
import java.util.List;
import java.util.stream.Collectors;
public class AssignRolesRequest {
private List<Long> roleIds;
private List<String> roleIds;
public List<Long> getRoleIds() {
public List<String> getRoleIds() {
return roleIds;
}
public void setRoleIds(List<Long> roleIds) {
public void setRoleIds(List<String> roleIds) {
this.roleIds = roleIds;
}
public List<Long> getRoleIdsAsLong() {
if (roleIds == null) {
return null;
}
return roleIds.stream()
.map(Long::valueOf)
.collect(Collectors.toList());
}
}
@@ -63,6 +63,10 @@ public class SysUserHandler {
String order = request.queryParam("order").orElse("asc");
String keyword = request.queryParam("keyword").orElse(null);
System.out.println("=== SysUserHandler.getUsersByPage ===");
System.out.println("page: " + page + ", size: " + size + ", sort: " + sort + ", order: " + order);
System.out.println("keyword: " + keyword);
PageRequest pageRequest = new PageRequest();
pageRequest.setPage(page);
pageRequest.setSize(size);
@@ -259,7 +263,7 @@ public class SysUserHandler {
public Mono<ServerResponse> assignRoles(ServerRequest request) {
Long id = Long.valueOf(request.pathVariable("id"));
return request.bodyToMono(AssignRolesRequest.class)
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIds()))
.flatMap(req -> userService.assignRolesToUser(id, req.getRoleIdsAsLong()))
.then(ServerResponse.ok().build())
.onErrorResume(error -> {
logger.error("分配角色失败", error);
@@ -1,12 +1,13 @@
package cn.novalon.manage.sys.util;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.function.server.ServerRequest;
import java.net.InetSocketAddress;
import java.util.Optional;
/**
* IP地址工具类
* 用于从ServerRequest中获取客户端真实IP地址
* 用于从ServerRequest或ServerHttpRequest中获取客户端真实IP地址
* 支持代理服务器场景(X-Forwarded-For, X-Real-IP)
*
* @author 张翔
@@ -48,6 +49,36 @@ public class IpUtils {
return UNKNOWN;
}
/**
* 从ServerHttpRequest中获取客户端真实IP地址
* 支持代理服务器场景,优先级: X-Forwarded-For > X-Real-IP > RemoteAddress
*
* @param request ServerHttpRequest对象
* @return 客户端IP地址,获取失败返回"unknown"
*/
public static String getClientIp(ServerHttpRequest request) {
if (request == null) {
return UNKNOWN;
}
String ip = getXForwardedForIp(request);
if (isValidIp(ip)) {
return ip;
}
ip = getXRealIp(request);
if (isValidIp(ip)) {
return ip;
}
ip = getRemoteAddress(request);
if (isValidIp(ip)) {
return ip;
}
return UNKNOWN;
}
/**
* 从X-Forwarded-For头获取IP地址
* X-Forwarded-For格式: client, proxy1, proxy2
@@ -98,4 +129,48 @@ public class IpUtils {
private static boolean isValidIp(String ip) {
return ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip);
}
/**
* 从X-Forwarded-For头获取IP地址(ServerHttpRequest版本)
* X-Forwarded-For格式: client, proxy1, proxy2
* 取第一个非unknown的有效IP
*/
private static String getXForwardedForIp(ServerHttpRequest request) {
String ip = request.getHeaders().getFirst("X-Forwarded-For");
if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
int index = ip.indexOf(",");
if (index != -1) {
return ip.substring(0, index);
}
return ip;
}
return null;
}
/**
* 从X-Real-IP头获取IP地址(ServerHttpRequest版本)
*/
private static String getXRealIp(ServerHttpRequest request) {
String ip = request.getHeaders().getFirst("X-Real-IP");
if (ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(ip)) {
return ip;
}
return null;
}
/**
* 从RemoteAddress获取IP地址(ServerHttpRequest版本)
* 将IPv6本地地址转换为IPv4格式
*/
private static String getRemoteAddress(ServerHttpRequest request) {
InetSocketAddress remoteAddress = request.getRemoteAddress();
if (remoteAddress != null) {
String ip = remoteAddress.getAddress().getHostAddress();
if (LOCALHOST_IPV6.equals(ip)) {
ip = LOCALHOST_IP;
}
return ip;
}
return null;
}
}
@@ -1,2 +1 @@
cn.novalon.manage.sys.config.ExceptionLogConfig
cn.novalon.manage.sys.config.SystemRouter
cn.novalon.manage.sys.config.ExceptionLogConfig
@@ -25,7 +25,7 @@ class AuditLogTest {
assertNull(auditLog.getEntityId());
assertNull(auditLog.getOperator());
assertNull(auditLog.getOperationType());
assertNull(auditLog.getOperationTime());
assertNotNull(auditLog.getOperationTime());
assertNull(auditLog.getDescription());
assertNull(auditLog.getIpAddress());
assertNull(auditLog.getUserAgent());

Some files were not shown because too many files have changed in this diff Show More