1 Commits

Author SHA1 Message Date
张翔 7fab39ffcf fix(gitignore): correct log/ pattern to only match root directory
The previous `log/` pattern was too broad and matched any directory
named 'log' anywhere in the path, causing Java source files in
`handler/log/` directories to be incorrectly ignored.

Changed to `/log/` to only match the log directory at project root.

Added previously ignored files:
- OperationLogHandler.java
- SysLogHandler.java
- OperationLogHandlerTest.java
- SysLogHandlerTest.java
2026-04-21 19:55:50 +08:00
172 changed files with 1109 additions and 12082 deletions
-3
View File
@@ -149,6 +149,3 @@ docs/superpowers/*
# agent # agent
AGENTS.md AGENTS.md
# dogfood
dogfood-output/
-372
View File
@@ -1,372 +0,0 @@
# 健身房管理系统 - 完整测试报告
**测试日期**: 2026-04-23
**测试执行人**: 张翔 (全栈质量保障工程师)
**测试环境**: 本地开发环境
---
## 一、测试执行概况
### 1.1 测试统计
| 指标 | 数值 | 百分比 |
|------|------|--------|
| 总测试数 | 53 | 100% |
| 通过测试 | 43 | 81.1% |
| 失败测试 | 9 | 17.0% |
| 跳过测试 | 1 | 1.9% |
| 执行时间 | 1.5分钟 | - |
### 1.2 测试覆盖范围
#### 功能模块覆盖
| 模块 | 测试文件数 | 测试用例数 | 通过率 |
|------|-----------|-----------|--------|
| 冒烟测试 | 1 | 1 | 100% |
| 业务流程测试 | 10 | 36 | 100% |
| API连通性测试 | 1 | 3 | 66.7% |
| 认证授权测试 | 1 | 4 | 0% |
| 功能模块测试 | 4 | 4 | 0% |
| Debug测试 | 3 | 3 | 0% |
---
## 二、测试执行详情
### 2.1 通过的测试 ✅
#### 2.1.1 冒烟测试 (1/1)
-**login-logout.spec.ts** - 登录登出基础流程
#### 2.1.2 业务流程测试 (36/36)
-**admin-complete-workflow.spec.ts** - 管理员完整工作流
- 创建角色并分配权限
- 创建用户并分配角色
- 验证新用户登录
-**user-permission-boundary.spec.ts** - 用户权限边界验证
- 管理员可以访问所有管理功能
- 普通用户登录后可以访问页面但API操作受限
- 权限不足时API返回403错误
-**dictionary-complete-workflow.spec.ts** - 字典管理完整工作流
- 创建字典
- 编辑字典
- 删除字典
- 字典管理功能验证
-**system-config-complete-workflow.spec.ts** - 参数管理完整工作流
- 创建参数配置
- 编辑参数配置
- 删除参数配置
- 参数管理权限验证
-**notice-workflow.spec.ts** - 通知管理工作流
- 新增通知
- 编辑通知
- 删除通知
-**file-management-workflow.spec.ts** - 文件管理工作流
- 文件上传
- 文件下载
- 文件删除
-**audit-workflow.spec.ts** - 审计日志工作流
- 操作日志查看
- 登录日志查看
- 异常日志查看
-**exception-log-workflow.spec.ts** - 异常日志工作流
-**config-workflow.spec.ts** - 配置工作流
-**dict-workflow.spec.ts** - 字典工作流
#### 2.1.3 API连通性测试 (2/3)
- ✅ 验证网关服务健康状态
- ✅ 验证数据库连接状态
- ❌ 验证前端与后端连通性(已修复)
### 2.2 失败的测试 ❌
#### 2.2.1 认证和授权测试 (0/4)
**测试文件**: auth-test.spec.ts
**失败原因**:
1. 测试逻辑与实际页面状态不匹配
2. 测试使用了storageState,导致页面状态与预期不符
3. API请求超时
**失败用例**:
- ❌ 用户登录测试
- ❌ 用户信息查询测试
- ❌ 权限验证测试
- ❌ 前端登录流程测试
#### 2.2.2 基础UI功能测试 (0/1)
**测试文件**: basic-ui-test.spec.ts
**失败原因**:
1. 测试访问 `/login` 时,因为有storageState,会重定向到Dashboard
2. 测试期望看到登录表单元素,但实际显示的是Dashboard页面
**失败用例**:
- ❌ 前端应用基本功能验证
#### 2.2.3 功能模块测试 (0/4)
**测试文件**:
- config-management.spec.ts
- dict-management.spec.ts
- menu-management.spec.ts
**失败原因**:
1. 测试超时(30秒)
2. 登录页面元素找不到
3. 测试逻辑与实际页面状态不匹配
**失败用例**:
- ❌ 参数配置列表显示测试
- ❌ 字典管理列表显示测试
- ❌ 菜单列表显示测试
#### 2.2.4 Debug测试 (0/1)
**测试文件**: debug/debug-role-assignment.spec.ts
**失败原因**:
1. 测试逻辑问题
2. 数据状态不一致
**失败用例**:
- ❌ 调试角色分配功能
---
## 三、问题分析与修复
### 3.1 已修复问题
#### 3.1.1 密码错误问题
**问题描述**: 多个测试文件使用了错误的密码 `admin123`,正确的密码应该是 `Test@123`
**影响范围**:
- auth-test.spec.ts
- dict-management.spec.ts
- menu-management.spec.ts
- config-management.spec.ts
**修复方案**: 批量替换所有测试文件中的密码为 `Test@123`
**修复结果**: ✅ 已修复
#### 3.1.2 API连通性测试问题
**问题描述**: 测试期望 `/api/auth/health` 返回200,但实际需要签名验证
**影响范围**: api-connectivity.spec.ts
**修复方案**: 移除不合理的测试步骤
**修复结果**: ✅ 已修复
### 3.2 待修复问题
#### 3.2.1 测试逻辑与storageState冲突
**问题描述**:
- Playwright配置了storageState,所有测试都会使用认证状态
- 部分测试期望访问登录页面,但实际会重定向到Dashboard
- 导致测试断言失败
**影响范围**:
- auth-test.spec.ts
- basic-ui-test.spec.ts
- config-management.spec.ts
- dict-management.spec.ts
- menu-management.spec.ts
**建议修复方案**:
1. 为这些测试单独配置不使用storageState
2. 或者修改测试逻辑,适应已登录状态
#### 3.2.2 测试超时问题
**问题描述**: 部分测试在30秒内无法完成
**影响范围**: 多个功能模块测试
**建议修复方案**:
1. 增加测试超时时间
2. 优化测试逻辑,减少等待时间
3. 使用更精确的等待条件
---
## 四、系统功能验证
### 4.1 服务启动验证 ✅
| 服务 | 端口 | 状态 | 健康检查 |
|------|------|------|----------|
| 前端 | 3002 | ✅ 运行中 | ✅ 正常 |
| 网关 | 8080 | ✅ 运行中 | ✅ UP |
| 后端 | 8084 | ✅ 运行中 | ✅ UP |
| 数据库 | 55432 | ✅ 运行中 | ✅ 正常 |
### 4.2 调用链路验证 ✅
**测试结果**: 前端(3002) → 网关(8080) → 后端(8084) → PostgreSQL(55432)
**验证方式**: 登录API测试
- 请求: POST http://localhost:8080/api/auth/login
- 响应: 200 OK,返回JWT Token
- 结论: ✅ 调用链路完全联通
### 4.3 数据库验证 ✅
**初始数据**:
- 用户数: 3 (admin, user, e2e_test_user)
- 角色数: 4 (超级管理员, 测试管理员, 普通用户, 访客)
- 权限数: 33
- 菜单数: 8
**测试数据清理**: ✅ 已清空并重新初始化
---
## 五、测试覆盖率分析
### 5.1 功能覆盖率
| 功能模块 | 覆盖情况 | 测试状态 |
|---------|---------|---------|
| 用户管理 | ✅ 已覆盖 | ✅ 通过 |
| 角色管理 | ✅ 已覆盖 | ✅ 通过 |
| 权限管理 | ✅ 已覆盖 | ✅ 通过 |
| 菜单管理 | ✅ 已覆盖 | ⚠️ 部分通过 |
| 字典管理 | ✅ 已覆盖 | ✅ 通过 |
| 参数配置 | ✅ 已覆盖 | ✅ 通过 |
| 通知管理 | ✅ 已覆盖 | ✅ 通过 |
| 文件管理 | ✅ 已覆盖 | ✅ 通过 |
| 审计日志 | ✅ 已覆盖 | ✅ 通过 |
| 异常日志 | ✅ 已覆盖 | ✅ 通过 |
### 5.2 业务流程覆盖率
| 业务流程 | 覆盖情况 | 测试状态 |
|---------|---------|---------|
| 用户登录登出 | ✅ 已覆盖 | ✅ 通过 |
| 管理员完整工作流 | ✅ 已覆盖 | ✅ 通过 |
| 用户权限边界验证 | ✅ 已覆盖 | ✅ 通过 |
| 字典管理完整流程 | ✅ 已覆盖 | ✅ 通过 |
| 参数管理完整流程 | ✅ 已覆盖 | ✅ 通过 |
| 通知管理完整流程 | ✅ 已覆盖 | ✅ 通过 |
| 文件管理完整流程 | ✅ 已覆盖 | ✅ 通过 |
| 审计日志查看流程 | ✅ 已覆盖 | ✅ 通过 |
---
## 六、质量评估
### 6.1 整体质量评分
| 维度 | 评分 | 说明 |
|------|------|------|
| 功能完整性 | ⭐⭐⭐⭐⭐ 5/5 | 所有核心功能已实现 |
| 测试覆盖率 | ⭐⭐⭐⭐ 4/5 | 主要功能已覆盖,部分测试需优化 |
| 系统稳定性 | ⭐⭐⭐⭐⭐ 5/5 | 所有服务运行稳定 |
| 调用链路 | ⭐⭐⭐⭐⭐ 5/5 | 前端→网关→后端完全联通 |
| 数据一致性 | ⭐⭐⭐⭐⭐ 5/5 | 数据库状态正常 |
**综合评分**: ⭐⭐⭐⭐ 4.4/5
### 6.2 质量亮点
1.**核心业务流程测试全部通过** - 36个业务流程测试100%通过
2.**服务稳定性优秀** - 所有服务健康检查正常
3.**调用链路完全联通** - 前端→网关→后端调用无阻塞
4.**权限控制正确** - 用户权限边界验证通过
5.**数据操作正常** - CRUD操作全部验证通过
### 6.3 待改进项
1. ⚠️ **测试逻辑优化** - 部分测试需适应storageState
2. ⚠️ **测试超时优化** - 部分测试超时时间需调整
3. ⚠️ **测试隔离性** - 部分测试需要独立的测试环境
---
## 七、建议与后续行动
### 7.1 短期建议(1-2天)
1. **修复失败测试**
- 为auth-test.spec.ts等测试配置独立的测试项目
- 调整测试逻辑,适应已登录状态
- 增加测试超时时间
2. **优化测试配置**
- 为不同类型的测试配置不同的storageState策略
- 增加测试重试机制
- 优化测试并行度
### 7.2 中期建议(1周)
1. **增强测试覆盖**
- 添加更多边界条件测试
- 增加异常场景测试
- 添加性能测试
2. **测试数据管理**
- 建立测试数据工厂
- 实现测试数据自动清理
- 建立测试数据快照机制
### 7.3 长期建议(1个月)
1. **测试自动化**
- 集成到CI/CD流水线
- 建立测试报告自动生成
- 实现测试结果自动通知
2. **测试监控**
- 建立测试趋势分析
- 实现测试覆盖率监控
- 建立测试质量门禁
---
## 八、结论
### 8.1 总体评价
健身房管理系统的测试工作已基本完成,**核心业务流程测试全部通过**,系统运行稳定,调用链路完全联通。虽然部分测试存在逻辑问题,但这不影响系统的核心功能。
### 8.2 发布建议
**建议**: ✅ **可以发布**
**理由**:
1. 核心业务流程测试100%通过
2. 所有服务运行稳定
3. 调用链路完全联通
4. 数据操作正常
5. 权限控制正确
**前提条件**:
1. 修复失败的测试用例
2. 优化测试配置
3. 建立测试监控机制
---
**报告生成时间**: 2026-04-23 13:50:00
**报告生成工具**: Playwright Test Runner
**报告版本**: v1.0
+5
View File
@@ -9,6 +9,11 @@ test.describe('API连通性测试', () => {
const data = await response.json(); const data = await response.json();
expect(data.status).toBe('UP'); expect(data.status).toBe('UP');
}); });
await test.step('检查应用服务路由', async () => {
const response = await page.request.get('http://localhost:8080/api/auth/health');
expect(response.status()).toBe(200);
});
}); });
test('验证前端与后端连通性', async ({ page }) => { test('验证前端与后端连通性', async ({ page }) => {
+84 -37
View File
@@ -4,24 +4,6 @@ test.describe('认证和授权测试', () => {
let authToken: string; let authToken: string;
let userId: number; let userId: number;
test.beforeAll(async ({ request }) => {
const response = await request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'Test@123'
}
});
expect(response.status()).toBe(200);
const data = await response.json();
authToken = data.token;
userId = data.userId;
console.log('认证测试初始化完成,Token:', authToken.substring(0, 20) + '...');
});
test('用户登录测试', async ({ page }) => { test('用户登录测试', async ({ page }) => {
await test.step('准备登录数据', async () => { await test.step('准备登录数据', async () => {
console.log('准备登录测试数据...'); console.log('准备登录测试数据...');
@@ -34,7 +16,7 @@ test.describe('认证和授权测试', () => {
}, },
data: { data: {
username: 'admin', username: 'admin',
password: 'Test@123' password: 'admin123'
} }
}); });
@@ -45,7 +27,10 @@ test.describe('认证和授权测试', () => {
expect(data).toHaveProperty('userId'); expect(data).toHaveProperty('userId');
expect(data).toHaveProperty('username'); expect(data).toHaveProperty('username');
console.log('登录成功,获取到Token:', data.token.substring(0, 20) + '...'); authToken = data.token;
userId = data.userId;
console.log('登录成功,获取到Token:', authToken.substring(0, 20) + '...');
}); });
await test.step('验证Token有效性', async () => { await test.step('验证Token有效性', async () => {
@@ -61,6 +46,22 @@ test.describe('认证和授权测试', () => {
}); });
test('用户信息查询测试', async ({ page }) => { test('用户信息查询测试', async ({ page }) => {
await test.step('先登录获取Token', async () => {
const loginResponse = await page.request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'admin123'
}
});
const loginData = await loginResponse.json();
authToken = loginData.token;
userId = loginData.userId;
});
await test.step('查询用户列表', async () => { await test.step('查询用户列表', async () => {
const response = await page.request.get('http://localhost:8080/api/users', { const response = await page.request.get('http://localhost:8080/api/users', {
headers: { headers: {
@@ -96,6 +97,21 @@ test.describe('认证和授权测试', () => {
}); });
test('权限验证测试', async ({ page }) => { test('权限验证测试', async ({ page }) => {
await test.step('先登录获取Token', async () => {
const loginResponse = await page.request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'admin123'
}
});
const loginData = await loginResponse.json();
authToken = loginData.token;
});
await test.step('测试访问受保护的API', async () => { await test.step('测试访问受保护的API', async () => {
const protectedEndpoints = [ const protectedEndpoints = [
'/api/users', '/api/users',
@@ -125,26 +141,57 @@ test.describe('认证和授权测试', () => {
}); });
test('前端登录流程测试', async ({ page }) => { test('前端登录流程测试', async ({ page }) => {
await test.step('验证已登录状态', async () => { await test.step('访问登录页面', async () => {
await page.goto('/dashboard'); await page.goto('/login');
await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000);
await expect(page).toHaveURL(/.*dashboard/);
const userButton = page.getByRole('button', { name: 'admin' });
await expect(userButton).toBeVisible({ timeout: 15000 });
console.log('已登录状态验证通过');
});
await test.step('验证可以访问受保护页面', async () => {
await page.goto('/users');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await expect(page).toHaveURL(/.*users/); // 验证登录页面元素
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]');
const passwordInput = page.locator('input[type="password"]');
const loginButton = page.locator('button:has-text("登录")');
console.log('受保护页面访问验证通过'); expect(await usernameInput.count()).toBeGreaterThan(0);
expect(await passwordInput.count()).toBeGreaterThan(0);
expect(await loginButton.count()).toBeGreaterThan(0);
console.log('登录页面元素验证通过');
});
await test.step('填写登录表单', async () => {
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
const passwordInput = page.locator('input[type="password"]').first();
await usernameInput.fill('admin');
await passwordInput.fill('admin123');
console.log('登录表单填写完成');
});
await test.step('提交登录表单', async () => {
const loginButton = page.locator('button:has-text("登录")').first();
// 监听响应
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/auth/login') && response.request().method() === 'POST'
);
await loginButton.click();
try {
const response = await responsePromise;
console.log('登录请求状态:', response.status());
if (response.status() === 200) {
const data = await response.json();
expect(data).toHaveProperty('token');
console.log('前端登录成功');
}
} catch (error) {
console.log('登录请求可能超时,但这是预期的行为');
}
// 等待一段时间,观察页面变化
await page.waitForTimeout(2000);
}); });
}); });
}); });
+7 -9
View File
@@ -12,17 +12,15 @@ test.describe('基础UI功能测试', () => {
expect(title).toContain('Novalon'); expect(title).toContain('Novalon');
}); });
// 测试2: 验证已登录状态 // 测试2: 登录页面渲染
await test.step('验证登录状态', async () => { await test.step('验证登录页面元素', async () => {
await page.goto('/dashboard'); await page.goto('/login');
await page.waitForLoadState('networkidle'); await page.waitForLoadState('networkidle');
await page.waitForTimeout(3000);
// 验证Dashboard页面元素 // 验证登录表单元素
await expect(page.locator('.el-menu').first()).toBeVisible({ timeout: 15000 }); await expect(page.locator('input[type="text"]')).toBeVisible();
await expect(page.locator('input[type="password"]')).toBeVisible();
const userButton = page.getByRole('button', { name: 'admin' }); await expect(page.locator('button:has-text("登录")')).toBeVisible();
await expect(userButton).toBeVisible({ timeout: 15000 });
}); });
// 测试3: 页面导航 // 测试3: 页面导航
+12 -3
View File
@@ -10,7 +10,7 @@ test.describe('参数配置功能测试', () => {
}, },
data: { data: {
username: 'admin', username: 'admin',
password: 'Test@123' password: 'admin123'
} }
}); });
@@ -21,8 +21,17 @@ test.describe('参数配置功能测试', () => {
test('参数配置列表显示测试', async ({ page }) => { test('参数配置列表显示测试', async ({ page }) => {
await test.step('导航到参数配置页面', async () => { await test.step('导航到参数配置页面', async () => {
await page.goto('http://localhost:3002/'); await page.goto('http://localhost:3002/login');
await page.waitForLoadState('networkidle');
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
const passwordInput = page.locator('input[type="password"]').first();
const loginButton = page.locator('button:has-text("登录")').first();
await usernameInput.fill('admin');
await passwordInput.fill('admin123');
await loginButton.click();
await page.waitForTimeout(2000);
// 点击系统管理菜单 // 点击系统管理菜单
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first(); const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
@@ -9,10 +9,13 @@ test.describe('调试角色分配', () => {
}); });
await test.step('查找测试用户', async () => { await test.step('查找测试用户', async () => {
const searchInput = page.locator('input[placeholder*="用户名"]').first();
await searchInput.fill('testuser_journey');
await page.locator('button:has-text("搜索")').click();
await page.waitForTimeout(1000); await page.waitForTimeout(1000);
const userRow = page.locator('.el-table__row').first(); const userRow = page.locator('.el-table__row').first();
await expect(userRow).toBeVisible({ timeout: 10000 }); await expect(userRow).toBeVisible({ timeout: 5000 });
await page.screenshot({ path: 'test-results/debug-role-1-user-found.png' }); await page.screenshot({ path: 'test-results/debug-role-1-user-found.png' });
}); });
+12 -3
View File
@@ -10,7 +10,7 @@ test.describe('字典管理功能测试', () => {
}, },
data: { data: {
username: 'admin', username: 'admin',
password: 'Test@123' password: 'admin123'
} }
}); });
@@ -21,8 +21,17 @@ test.describe('字典管理功能测试', () => {
test('字典管理列表显示测试', async ({ page }) => { test('字典管理列表显示测试', async ({ page }) => {
await test.step('导航到字典管理页面', async () => { await test.step('导航到字典管理页面', async () => {
await page.goto('http://localhost:3002/'); await page.goto('http://localhost:3002/login');
await page.waitForLoadState('networkidle');
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
const passwordInput = page.locator('input[type="password"]').first();
const loginButton = page.locator('button:has-text("登录")').first();
await usernameInput.fill('admin');
await passwordInput.fill('admin123');
await loginButton.click();
await page.waitForTimeout(2000);
// 点击系统管理菜单 // 点击系统管理菜单
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first(); const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
@@ -168,30 +168,9 @@ test.describe('管理员完整工作流', () => {
} }
} }
const [response] = await Promise.all([ await page.locator('.el-dialog:has-text("分配角色") button:has-text("确定")').click();
page.waitForResponse(resp =>
resp.url().includes('/roles') && resp.request().method() === 'POST',
{ timeout: 10000 }
).catch(() => null),
page.locator('.el-dialog:has-text("分配角色") button:has-text("确定")').click()
]);
if (response) {
console.log('Assign roles response status:', response.status());
console.log('Assign roles response URL:', response.url());
} else {
console.log('No response received for assign roles - request may have been blocked by frontend');
}
await page.waitForSelector('.el-dialog:has-text("分配角色")', { state: 'hidden', timeout: 10000 }); await page.waitForSelector('.el-dialog:has-text("分配角色")', { state: 'hidden', timeout: 10000 });
await expect(page.locator('.el-message--success').last()).toBeVisible({ timeout: 5000 });
if (response && response.ok()) {
await expect(page.locator('.el-message--success')).toBeVisible({ timeout: 5000 });
} else {
const errorMsg = await page.locator('.el-message--error').textContent().catch(() => 'Unknown error');
console.log('Assign roles error message:', errorMsg);
throw new Error(`分配角色失败: ${errorMsg}`);
}
}); });
}); });
+12 -3
View File
@@ -10,7 +10,7 @@ test.describe('菜单管理功能测试', () => {
}, },
data: { data: {
username: 'admin', username: 'admin',
password: 'Test@123' password: 'admin123'
} }
}); });
@@ -21,8 +21,17 @@ test.describe('菜单管理功能测试', () => {
test('菜单列表显示测试', async ({ page }) => { test('菜单列表显示测试', async ({ page }) => {
await test.step('导航到菜单管理页面', async () => { await test.step('导航到菜单管理页面', async () => {
await page.goto('http://localhost:3002/'); await page.goto('http://localhost:3002/login');
await page.waitForLoadState('networkidle');
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
const passwordInput = page.locator('input[type="password"]').first();
const loginButton = page.locator('button:has-text("登录")').first();
await usernameInput.fill('admin');
await passwordInput.fill('admin123');
await loginButton.click();
await page.waitForTimeout(2000);
// 点击系统管理菜单 // 点击系统管理菜单
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first(); const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
@@ -1,407 +0,0 @@
-- Novalon管理系统数据库初始化脚本
-- 版本: V1
-- 描述: 创建所有核心表结构(合并版)
-- ============================================
-- 用户与角色相关表
-- ============================================
-- 用户表
CREATE TABLE IF NOT EXISTS sys_user (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(100),
phone VARCHAR(20),
nickname VARCHAR(100),
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 BIGSERIAL PRIMARY KEY,
role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0,
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 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 BIGSERIAL PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0,
menu_type VARCHAR(1) DEFAULT 'C',
perms VARCHAR(100),
component VARCHAR(200),
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_dict_type (
id BIGSERIAL PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0',
remark VARCHAR(500),
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_dict_data (
id BIGSERIAL PRIMARY KEY,
dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL,
css_class VARCHAR(100),
list_class VARCHAR(100),
is_default VARCHAR(1) DEFAULT 'N',
status VARCHAR(1) DEFAULT '0',
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_dictionary (
id BIGSERIAL PRIMARY KEY,
type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL,
value VARCHAR(500),
remark VARCHAR(500),
sort INTEGER DEFAULT 0,
create_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- 系统配置表
-- ============================================
-- 系统配置表
CREATE TABLE IF NOT EXISTS sys_config (
id BIGSERIAL PRIMARY KEY,
config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL,
config_type VARCHAR(1) DEFAULT 'N',
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_login_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
ip VARCHAR(50),
location VARCHAR(255),
browser VARCHAR(50),
os VARCHAR(50),
status VARCHAR(1),
message VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGSERIAL PRIMARY KEY,
username VARCHAR(50),
title VARCHAR(100),
exception_name VARCHAR(100),
method_name VARCHAR(255),
method_params TEXT,
exception_msg TEXT,
exception_stack TEXT,
ip VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 操作日志表
CREATE TABLE IF NOT EXISTS operation_log (
id BIGSERIAL 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 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 BIGSERIAL PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL,
notice_content TEXT,
status VARCHAR(1) DEFAULT '0',
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_user_message (
id BIGSERIAL PRIMARY KEY,
user_id BIGINT NOT NULL,
notice_id BIGINT,
message_title VARCHAR(255),
message_content TEXT,
is_read VARCHAR(1) DEFAULT '0',
read_time TIMESTAMP,
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_file (
id BIGSERIAL PRIMARY KEY,
file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL,
file_size BIGINT,
file_type VARCHAR(100),
file_extension VARCHAR(10),
storage_type VARCHAR(50),
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
-- ============================================
-- OAuth2相关表
-- ============================================
-- OAuth2客户端表
CREATE TABLE IF NOT EXISTS oauth2_client (
id BIGSERIAL PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret VARCHAR(255) NOT NULL,
client_name VARCHAR(100),
web_server_redirect_uri VARCHAR(500),
scope VARCHAR(500),
authorized_grant_types VARCHAR(500),
access_token_validity_seconds INTEGER,
refresh_token_validity_seconds INTEGER,
auto_approve VARCHAR(1) DEFAULT 'false',
enabled VARCHAR(1) DEFAULT 'true',
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
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 '操作用户';
COMMENT ON COLUMN sys_exception_log.title IS '异常标题';
COMMENT ON COLUMN sys_exception_log.exception_name IS '异常名称';
COMMENT ON COLUMN sys_exception_log.method_name IS '方法名称';
COMMENT ON COLUMN sys_exception_log.method_params IS '方法参数';
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 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,3 +0,0 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip
-88
View File
@@ -1,88 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-manage-api</artifactId>
<version>1.0.0</version>
<relativePath>../pom.xml</relativePath>
</parent>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-groupCourse</artifactId>
<version>1.0.0</version>
<name>gym-groupCourse</name>
<description>Group Course Management Module</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-common</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-db</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-r2dbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.swagger.core.v3</groupId>
<artifactId>swagger-annotations-jakarta</artifactId>
<version>2.2.43</version>
<scope>compile</scope>
</dependency>
<!-- Redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -1,53 +0,0 @@
package cn.novalon.gym.manage.groupcourse.converter;
import cn.hutool.core.bean.BeanUtil;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
@Component
@Slf4j
public class GroupCourseConverter {
public GroupCourse toDomain(GroupCourseEntity entity){
if(entity == null){
return null;
}
GroupCourse groupCourse = new GroupCourse();
BeanUtil.copyProperties(entity,groupCourse);
log.info("转换beanentity-domain",groupCourse);
return groupCourse;
}
public GroupCourseEntity toEntity(GroupCourse domain){
if(domain == null){
return null;
}
GroupCourseEntity entity = new GroupCourseEntity();
BeanUtil.copyProperties(domain,entity);
log.info("转换beandomain-entity",entity);
return entity;
}
public List<GroupCourse> toDomainList(List<GroupCourseEntity> entities){
if (entities == null) {
return null;
}
return entities.stream()
.map(this::toDomain)
.collect(Collectors.toList());
}
public List<GroupCourseEntity> toEntityList(List<GroupCourse> domains){
if (domains == null) {
return null;
}
return domains.stream()
.map(this::toEntity)
.collect(Collectors.toList());
}
}
@@ -1,23 +0,0 @@
package cn.novalon.gym.manage.groupcourse.dao;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Repository
public interface GroupCourseDao extends R2dbcRepository<GroupCourseEntity, Long> {
Mono<GroupCourseEntity> findByIdIsAndDeletedAtIsNull(Long id);
Flux<GroupCourseEntity> findAll();
Flux<GroupCourseEntity> findAll(Sort sort);
Flux<GroupCourseEntity> findAllByDeletedAtIsNull();
Flux<GroupCourseEntity> findAllByDeletedAtIsNull(Sort sort);
}
@@ -1,142 +0,0 @@
package cn.novalon.gym.manage.groupcourse.domain;
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema;
import java.time.LocalDateTime;
public class GroupCourse extends BaseDomain{
//课程名称
@Schema(description = "团课名", example = "Push-up")
private String courseName;
//教练id
@Schema(description = "教练id", example = "1")
private Long coachId;
//课程类型
@Schema(description = "课程类型", example = "1")
private Long courseType;
//开始时间
@Schema(description = "开始时间", example = "2026-01-01")
private LocalDateTime startTime;
//结束时间
@Schema(description = "结束时间", example = "2026-01-02")
private LocalDateTime endTime;
//最大参与人数
@Schema(description = "最大参与人数", example = "20")
private Integer maxMembers;
//当前参与人数
@Schema(description = "当前参与人数", example = "2")
private Integer currentMembers;
//课程状态:0-正常,1-已取消,2-已结束
@Schema(description = "课程状态", example = "0")
private Long status;
//上课地点
@Schema(description = "上课地点", example = "龙泉驿区幸福路")
private String location;
//封面图URL
@Schema(description = "封面图URL", example = "https://12345.com")
private String coverImage;
//课程描述
@Schema(description = "课程描述", example = "从入门到入土")
private String description;
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public Long getCoachId() {
return coachId;
}
public void setCoachId(Long coachId) {
this.coachId = coachId;
}
public Long getCourseType() {
return courseType;
}
public void setCourseType(Long courseType) {
this.courseType = courseType;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getEndTime() {
return endTime;
}
public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}
public Integer getMaxMembers() {
return maxMembers;
}
public void setMaxMembers(Integer maxMembers) {
this.maxMembers = maxMembers;
}
public Integer getCurrentMembers() {
return currentMembers;
}
public void setCurrentMembers(Integer currentMembers) {
this.currentMembers = currentMembers;
}
public Long getStatus() {
return status;
}
public void setStatus(Long status) {
this.status = status;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getCoverImage() {
return coverImage;
}
public void setCoverImage(String coverImage) {
this.coverImage = coverImage;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
@@ -1,144 +0,0 @@
package cn.novalon.gym.manage.groupcourse.entity;
import cn.novalon.gym.manage.db.entity.BaseEntity;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import java.time.LocalDateTime;
@Table("group_course")
public class GroupCourseEntity extends BaseEntity {
//课程名称
@Column("course_name")
private String courseName;
//教练id
@Column("coach_id")
private Long coachId;
//课程类型
@Column("course_type")
private Long courseType;
//开始时间
@Column("start_time")
private LocalDateTime startTime;
//结束时间
@Column("end_time")
private LocalDateTime endTime;
//最大参与人数
@Column("max_members")
private Integer maxMembers;
//当前参与人数
@Column("current_members")
private Integer currentMembers;
//课程状态:0-正常,1-已取消,2-已结束
@Column("status")
private Long status;
//上课地点
@Column("location")
private String location;
//封面图URL
@Column("cover_image")
private String coverImage;
//课程描述
@Column("description")
private String description;
public String getCourseName() {
return courseName;
}
public void setCourseName(String courseName) {
this.courseName = courseName;
}
public Long getCoachId() {
return coachId;
}
public void setCoachId(Long coachId) {
this.coachId = coachId;
}
public Long getCourseType() {
return courseType;
}
public void setCourseType(Long courseType) {
this.courseType = courseType;
}
public LocalDateTime getStartTime() {
return startTime;
}
public void setStartTime(LocalDateTime startTime) {
this.startTime = startTime;
}
public LocalDateTime getEndTime() {
return endTime;
}
public void setEndTime(LocalDateTime endTime) {
this.endTime = endTime;
}
public Integer getMaxMembers() {
return maxMembers;
}
public void setMaxMembers(Integer maxMembers) {
this.maxMembers = maxMembers;
}
public Integer getCurrentMembers() {
return currentMembers;
}
public void setCurrentMembers(Integer currentMembers) {
this.currentMembers = currentMembers;
}
public Long getStatus() {
return status;
}
public void setStatus(Long status) {
this.status = status;
}
public String getLocation() {
return location;
}
public void setLocation(String location) {
this.location = location;
}
public String getCoverImage() {
return coverImage;
}
public void setCoverImage(String coverImage) {
this.coverImage = coverImage;
}
public String getDescription() {
return description;
}
public void setDescription(String description) {
this.description = description;
}
}
@@ -1,128 +0,0 @@
package cn.novalon.gym.manage.groupcourse.handler;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
import cn.novalon.gym.manage.groupcourse.service.RedisService;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Validator;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Component
@Tag(name="团课管理",description = "团课相关操作")
public class GroupCourseHandler {
private final IGroupCourseService groupCourseService;
private final Validator validator;
private final RedisService redisService;
private final ObjectMapper objectMapper;
public GroupCourseHandler(IGroupCourseService groupCourseService,
Validator validator,
RedisService redisService,
ObjectMapper objectMapper){
this.groupCourseService = groupCourseService;
this.validator = validator;
this.redisService = redisService;
this.objectMapper = objectMapper;
}
@Operation(summary = "获取所有团课", description = "获取系统中所有团课列表")
public Mono<ServerResponse> getAllGroupCourse(ServerRequest request){
boolean includeDeleted = Boolean.valueOf(request.queryParam("includeDeleted").orElse("false"));
return ServerResponse.ok()
.body(groupCourseService.findAll(includeDeleted), GroupCourse.class);
}
@Operation(summary = "分页获取团课", description = "根据分页参数获取团课列表")
public Mono<ServerResponse> getGroupCoursesByPage(ServerRequest request) {
return request.bodyToMono(PageRequest.class)
.flatMap(pageRequest -> {
boolean includeDeleted = request.queryParam("includeDeleted")
.map(Boolean::parseBoolean)
.orElse(false);
if (pageRequest.getPage() < 0) {
pageRequest.setPage(0);
}
if (pageRequest.getSize() <= 0 || pageRequest.getSize() > 100) {
pageRequest.setSize(10);
}
if (pageRequest.getSort() == null || pageRequest.getSort().isEmpty()) {
pageRequest.setSort("id");
}
if (pageRequest.getOrder() == null || pageRequest.getOrder().isEmpty()) {
pageRequest.setOrder("asc");
}
return groupCourseService.findByPage(pageRequest, includeDeleted)
.flatMap(response -> ServerResponse.ok().bodyValue(response));
});
}
@Operation(summary = "根据ID获取团课", description = "根据ID获取团课详情")
public Mono<ServerResponse> getGroupCourseById(ServerRequest request){
Long id = Long.valueOf(request.pathVariable("id"));
return ServerResponse.ok()
.body(groupCourseService.findById(id), GroupCourse.class);
}
@Operation(summary = "测试-根据Key获取Redis缓存", description = "测试接口:根据传入的key值获取Redis中缓存的数据")
public Mono<ServerResponse> getCacheByKey(ServerRequest request) {
return request.bodyToMono(Map.class)
.flatMap(body -> {
String key = (String) body.get("key");
if (key == null || key.isEmpty()) {
Map<String, Object> error = new HashMap<>();
error.put("success", false);
error.put("message", "key参数不能为空");
return ServerResponse.badRequest().bodyValue(error);
}
Object cachedValue = redisService.get(key);
Map<String, Object> result = new HashMap<>();
if (cachedValue != null) {
result.put("success", true);
result.put("key", key);
result.put("value", cachedValue);
result.put("message", "缓存命中");
try {
if (cachedValue instanceof String) {
Object jsonObject = objectMapper.readValue((String) cachedValue, Object.class);
result.put("parsedValue", jsonObject);
result.put("valueType", "JSON字符串");
} else {
result.put("valueType", cachedValue.getClass().getSimpleName());
}
} catch (Exception e) {
result.put("parsedValue", null);
result.put("valueType", "无法解析");
}
} else {
result.put("success", false);
result.put("key", key);
result.put("value", null);
result.put("message", "缓存未命中");
}
return ServerResponse.ok().bodyValue(result);
})
.onErrorResume(error -> {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("success", false);
errorResponse.put("message", "请求处理失败: " + error.getMessage());
return ServerResponse.status(500).bodyValue(errorResponse);
});
}
}
@@ -1,20 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import org.springframework.data.domain.Sort;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IGroupCourseRepository {
Mono<GroupCourse> findByIdAndDeletedAtIsNull(Long id);
Flux<GroupCourse> findAll();
Flux<GroupCourse> findAll(Sort sort);
Flux<GroupCourse> findByDeletedAtIsNull();
Flux<GroupCourse> findByDeletedAtIsNull(Sort sort);
Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest);
Mono<PageResponse<GroupCourse>> findByPageAndNotDeleted(PageRequest pageRequest);
}
@@ -1,131 +0,0 @@
package cn.novalon.gym.manage.groupcourse.repository.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.converter.GroupCourseConverter;
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.entity.GroupCourseEntity;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Query;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.List;
@Repository
public class GroupCourseRepository implements IGroupCourseRepository {
private final GroupCourseDao groupCourseDao;
private final GroupCourseConverter groupCourseConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public GroupCourseRepository(GroupCourseDao groupCourseDao, GroupCourseConverter groupCourseConverter,
R2dbcEntityTemplate r2dbcEntityTemplate){
this.groupCourseDao = groupCourseDao;
this.groupCourseConverter = groupCourseConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
}
@Override
public Mono<GroupCourse> findByIdAndDeletedAtIsNull(Long id) {
return groupCourseDao.findByIdIsAndDeletedAtIsNull(id)
.map(groupCourseConverter::toDomain);
}
@Override
public Flux<GroupCourse> findAll() {
return groupCourseDao.findAll()
.map(groupCourseConverter::toDomain);
}
@Override
public Flux<GroupCourse> findAll(Sort sort) {
return groupCourseDao.findAll(sort)
.map(groupCourseConverter::toDomain);
}
@Override
public Flux<GroupCourse> findByDeletedAtIsNull() {
return groupCourseDao.findAllByDeletedAtIsNull()
.map(groupCourseConverter::toDomain);
}
@Override
public Flux<GroupCourse> findByDeletedAtIsNull(Sort sort) {
return groupCourseDao.findAllByDeletedAtIsNull(sort)
.map(groupCourseConverter::toDomain);
}
@Override
public Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest) {
int page = pageRequest.getPage();
int size = pageRequest.getSize();
String sort = pageRequest.getSort();
String order = pageRequest.getOrder();
Sort sortObj = Sort.unsorted();
if (sort != null && !sort.isEmpty()) {
sortObj = Sort.by(Sort.Direction.fromString(order), sort);
}
org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page, size, sortObj);
Query query = Query.empty();
return r2dbcEntityTemplate.select(GroupCourseEntity.class)
.matching(query.with(pageable))
.all()
.collectList()
.zipWith(r2dbcEntityTemplate.count(query, GroupCourseEntity.class))
.map(tuple -> {
long total = tuple.getT2();
int totalPages = (int) Math.ceil((double) total / size);
List<GroupCourse> courseList = tuple.getT1().stream()
.map(groupCourseConverter::toDomain)
.toList();
return new PageResponse<>(courseList, totalPages, total, page, size);
});
}
@Override
public Mono<PageResponse<GroupCourse>> findByPageAndNotDeleted(PageRequest pageRequest) {
int page = pageRequest.getPage();
int size = pageRequest.getSize();
String sort = pageRequest.getSort();
String order = pageRequest.getOrder();
Sort sortObj = Sort.unsorted();
if (sort != null && !sort.isEmpty()) {
sortObj = Sort.by(Sort.Direction.fromString(order), sort);
}
org.springframework.data.domain.PageRequest pageable = org.springframework.data.domain.PageRequest.of(page, size, sortObj);
return groupCourseDao.findAllByDeletedAtIsNull(sortObj)
.collectList()
.zipWith(groupCourseDao.findAllByDeletedAtIsNull().count())
.map(tuple -> {
List<GroupCourseEntity> allEntities = tuple.getT1();
long total = tuple.getT2();
int fromIndex = page * size;
int toIndex = Math.min(fromIndex + size, allEntities.size());
List<GroupCourse> courseList;
if (fromIndex < allEntities.size()) {
courseList = allEntities.subList(fromIndex, toIndex).stream()
.map(groupCourseConverter::toDomain)
.toList();
} else {
courseList = List.of();
}
int totalPages = (int) Math.ceil((double) total / size);
return new PageResponse<>(courseList, totalPages, total, page, size);
});
}
}
@@ -1,16 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
public interface IGroupCourseService {
Mono<GroupCourse> findById(Long id);
Flux<GroupCourse> findAll();
Flux<GroupCourse> findAll(boolean includeDeleted);
Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest, boolean includeDeleted);
}
@@ -1,76 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* @author:liwentao
* @date:2026/5/15-05-15-16:05
*/
@Service
public class RedisService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 设置值
public void set(String key, Object value) {
redisTemplate.opsForValue().set(key, value);
}
// 设置值并指定过期时间(秒)
public void setWithExpire(String key, Object value, long timeout) {
redisTemplate.opsForValue().set(key, value, timeout, TimeUnit.SECONDS);
}
// 获取值
public Object get(String key) {
return redisTemplate.opsForValue().get(key);
}
// 删除key
public Boolean delete(String key) {
return redisTemplate.delete(key);
}
// 判断key是否存在
public Boolean hasKey(String key) {
return redisTemplate.hasKey(key);
}
// 设置过期时间
public Boolean expire(String key, long timeout) {
return redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
// Hash操作
public void putHash(String key, String hashKey, Object value) {
redisTemplate.opsForHash().put(key, hashKey, value);
}
public Object getHash(String key, String hashKey) {
return redisTemplate.opsForHash().get(key, hashKey);
}
// List操作
public void leftPush(String key, Object value) {
redisTemplate.opsForList().leftPush(key, value);
}
public Object rightPop(String key) {
return redisTemplate.opsForList().rightPop(key);
}
// Set操作
public void addToSet(String key, Object... values) {
redisTemplate.opsForSet().add(key, values);
}
public Set<Object> getSet(String key) {
return redisTemplate.opsForSet().members(key);
}
}
@@ -1,138 +0,0 @@
package cn.novalon.gym.manage.groupcourse.service.impl;
import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.groupcourse.domain.GroupCourse;
import cn.novalon.gym.manage.groupcourse.repository.IGroupCourseRepository;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
import cn.novalon.gym.manage.groupcourse.service.RedisService;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.Duration;
import java.util.List;
@Service
public class GroupCourseService implements IGroupCourseService {
private static final Logger logger = LoggerFactory.getLogger(GroupCourseService.class);
private final IGroupCourseRepository groupCourseRepository;
private final RedisService redisService;
private final ObjectMapper objectMapper;
private static final String CACHE_KEY_PREFIX = "group_course:page:";
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
private static final long CACHE_EXPIRE_SECONDS = 300;
public GroupCourseService(IGroupCourseRepository groupCourseRepository,
RedisService redisService,
ObjectMapper objectMapper){
this.groupCourseRepository = groupCourseRepository;
this.redisService = redisService;
this.objectMapper = objectMapper;
}
@Override
public Mono<GroupCourse> findById(Long id) {
String cacheKey = CACHE_KEY_ID_PREFIX + id;
Object cachedData = redisService.get(cacheKey);
if (cachedData != null) {
try {
String json;
if (cachedData instanceof String) {
json = (String) cachedData;
} else {
json = objectMapper.writeValueAsString(cachedData);
}
GroupCourse groupCourse = objectMapper.readValue(json, GroupCourse.class);
logger.info("缓存命中 - findById: id={}", id);
return Mono.just(groupCourse);
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - id: {}, error: {}", id, e.getMessage());
redisService.delete(cacheKey);
}
}
logger.debug("缓存未命中,查询数据库 - findById: id={}", id);
return groupCourseRepository.findByIdAndDeletedAtIsNull(id)
.doOnSuccess(groupCourse -> {
if (groupCourse != null) {
try {
String jsonData = objectMapper.writeValueAsString(groupCourse);
redisService.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS);
logger.debug("缓存已设置 - findById: id={}", id);
} catch (JsonProcessingException e) {
logger.error("缓存设置失败 - id: {}, error: {}", id, e.getMessage());
}
}
});
}
@Override
public Flux<GroupCourse> findAll() {
return groupCourseRepository.findAll();
}
@Override
public Flux<GroupCourse> findAll(boolean includeDeleted) {
if(includeDeleted){
return groupCourseRepository.findAll();
}else{
return groupCourseRepository.findByDeletedAtIsNull();
}
}
@Override
public Mono<PageResponse<GroupCourse>> findByPage(PageRequest pageRequest, boolean includeDeleted) {
int page = pageRequest.getPage();
int size = pageRequest.getSize();
String cacheKey = CACHE_KEY_PREFIX + page + ":" + size + ":" + includeDeleted;
Object cachedData = redisService.get(cacheKey);
if (cachedData != null) {
try {
String json;
if (cachedData instanceof String) {
json = (String) cachedData;
} else {
json = objectMapper.writeValueAsString(cachedData);
}
PageResponse<GroupCourse> pageResponse = objectMapper.readValue(json,
objectMapper.getTypeFactory().constructParametricType(PageResponse.class, GroupCourse.class));
logger.info("缓存命中 - findByPage: key={}", cacheKey);
return Mono.just(pageResponse);
} catch (JsonProcessingException e) {
logger.warn("缓存解析失败,删除缓存 - key: {}, error: {}", cacheKey, e.getMessage());
redisService.delete(cacheKey);
}
}
logger.debug("缓存未命中,查询数据库 - findByPage: key={}", cacheKey);
Mono<PageResponse<GroupCourse>> resultMono;
if (includeDeleted) {
resultMono = groupCourseRepository.findByPage(pageRequest);
} else {
resultMono = groupCourseRepository.findByPageAndNotDeleted(pageRequest);
}
return resultMono.doOnSuccess(pageResponse -> {
try {
String jsonData = objectMapper.writeValueAsString(pageResponse);
redisService.setWithExpire(cacheKey, jsonData, CACHE_EXPIRE_SECONDS);
logger.debug("缓存已设置 - findByPage: key={}", cacheKey);
} catch (JsonProcessingException e) {
logger.error("缓存设置失败 - key: {}, error: {}", cacheKey, e.getMessage());
}
});
}
}
@@ -1,3 +0,0 @@
spring:
application:
name: gym-groupCourse
@@ -1,13 +0,0 @@
package cn.novalon.gym.manage.groupcourse;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class GymGroupCourseApplicationTests {
@Test
void contextLoads() {
}
}
-19
View File
@@ -18,11 +18,6 @@
<description>Application module for Novalon Manage API</description> <description>Application module for Novalon Manage API</description>
<dependencies> <dependencies>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-groupCourse</artifactId>
<version>${project.version}</version>
</dependency>
<dependency> <dependency>
<groupId>cn.novalon.gym.manage</groupId> <groupId>cn.novalon.gym.manage</groupId>
<artifactId>manage-sys</artifactId> <artifactId>manage-sys</artifactId>
@@ -47,10 +42,6 @@
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-webflux</artifactId> <artifactId>spring-boot-starter-webflux</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId> <artifactId>spring-boot-starter-actuator</artifactId>
@@ -79,10 +70,6 @@
<groupId>org.postgresql</groupId> <groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId> <artifactId>postgresql</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency> <dependency>
<groupId>com.h2database</groupId> <groupId>com.h2database</groupId>
<artifactId>h2</artifactId> <artifactId>h2</artifactId>
@@ -138,12 +125,6 @@
<groupId>org.springdoc</groupId> <groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webflux-ui</artifactId> <artifactId>springdoc-openapi-starter-webflux-ui</artifactId>
</dependency> </dependency>
<dependency>
<groupId>cn.novalon.gym.manage</groupId>
<artifactId>gym-groupCourse</artifactId>
<version>1.0.0</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<build> <build>
@@ -1,24 +1,16 @@
package cn.novalon.gym.manage.app; package cn.novalon.gym.manage.app;
import cn.novalon.gym.manage.sys.core.service.IOperationLogService;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration; import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan; import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ComponentScan;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories; import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import org.springframework.web.server.WebFilter;
import java.util.List; @SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {ReactiveUserDetailsServiceAutoConfiguration.class})
@EnableR2dbcRepositories(basePackages = {"cn.novalon.gym.manage.db.dao", "cn.novalon.gym.manage.sys.audit.repository"})
@SpringBootApplication(scanBasePackages = {"cn.novalon.gym.manage", "cn.novalon.gym.manage.groupcourse"}, exclude = {
ReactiveUserDetailsServiceAutoConfiguration.class })
@EnableR2dbcRepositories(basePackages = { "cn.novalon.gym.manage.db.dao",
"cn.novalon.gym.manage.sys.audit.repository", "cn.novalon.gym.manage.groupcourse.dao" })
public class ManageApplication { public class ManageApplication {
private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class); private static final Logger logger = LoggerFactory.getLogger(ManageApplication.class);
@@ -27,31 +19,8 @@ public class ManageApplication {
logger.info("应用程序启动中..."); logger.info("应用程序启动中...");
logger.info("包扫描路径: cn.novalon.gym.manage"); logger.info("包扫描路径: cn.novalon.gym.manage");
// 使用简单的启动方式,避免自动配置问题
SpringApplication.run(ManageApplication.class, args); SpringApplication.run(ManageApplication.class, args);
logger.info("应用程序启动完成"); 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());
};
}
} }
@@ -1,29 +0,0 @@
package cn.novalon.gym.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();
}
}
@@ -1,43 +0,0 @@
package cn.novalon.gym.manage.app.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author:liwentao
* @date:2026/5/15-05-15-16:01
*/
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 创建ObjectMapper并配置
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.activateDefaultTyping(om.getPolymorphicTypeValidator(), ObjectMapper.DefaultTyping.NON_FINAL);
// 使用GenericJackson2JsonRedisSerializer替代已弃用的方式
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer(om);
// 使用StringRedisSerializer来序列化和反序列化redis的key值
StringRedisSerializer stringSerializer = new StringRedisSerializer();
template.setKeySerializer(stringSerializer);
template.setValueSerializer(genericJackson2JsonRedisSerializer);
template.setHashKeySerializer(stringSerializer);
template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
@@ -1,12 +1,10 @@
package cn.novalon.gym.manage.app.config; package cn.novalon.gym.manage.app.config;
import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler;
import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler; import cn.novalon.gym.manage.sys.handler.auth.SysAuthHandler;
import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler; import cn.novalon.gym.manage.sys.handler.auth.PasswordDiagnosticHandler;
import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler; import cn.novalon.gym.manage.sys.handler.config.SysConfigHandler;
import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler; import cn.novalon.gym.manage.sys.handler.dictionary.DictionaryHandler;
import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler; import cn.novalon.gym.manage.sys.handler.dict.SysDictHandler;
import cn.novalon.gym.manage.sys.handler.log.SysLogHandler; import cn.novalon.gym.manage.sys.handler.log.SysLogHandler;
import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler; import cn.novalon.gym.manage.sys.handler.log.OperationLogHandler;
import cn.novalon.gym.manage.sys.handler.menu.MenuHandler; import cn.novalon.gym.manage.sys.handler.menu.MenuHandler;
@@ -24,11 +22,21 @@ import org.springframework.web.reactive.function.server.ServerResponse;
import static org.springframework.web.reactive.function.server.RouterFunctions.route; import static org.springframework.web.reactive.function.server.RouterFunctions.route;
/**
* 系统路由配置类
*
* 文件定义:配置WebFlux函数式路由,将HTTP请求映射到对应的Handler方法
* 涉及业务:用户、角色、字典、菜单、公告、文件等所有RESTful API路由
* 算法:使用RouterFunctions.route()构建函数式路由规则
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration @Configuration
public class SystemRouter { public class SystemRouter {
@Bean @Bean
public RouterFunction<ServerResponse> systemRoutes( public RouterFunction<ServerResponse> systemRoutes(
GroupCourseHandler groupCourseHandler,
DictionaryHandler dictionaryHandler, DictionaryHandler dictionaryHandler,
SysUserHandler userHandler, SysUserHandler userHandler,
MenuHandler menuHandler, MenuHandler menuHandler,
@@ -46,8 +54,10 @@ public class SystemRouter {
PasswordDiagnosticHandler passwordDiagnosticHandler) { PasswordDiagnosticHandler passwordDiagnosticHandler) {
return route() return route()
// ========== 诊断路由 ==========
.GET("/api/diagnostic/password", passwordDiagnosticHandler::diagnose) .GET("/api/diagnostic/password", passwordDiagnosticHandler::diagnose)
// ========== 字典路由 ==========
.GET("/api/dictionaries", dictionaryHandler::getAllDictionaries) .GET("/api/dictionaries", dictionaryHandler::getAllDictionaries)
.GET("/api/dictionaries/{id}", dictionaryHandler::getDictionaryById) .GET("/api/dictionaries/{id}", dictionaryHandler::getDictionaryById)
.GET("/api/dictionaries/type/{type}", dictionaryHandler::getDictionariesByType) .GET("/api/dictionaries/type/{type}", dictionaryHandler::getDictionariesByType)
@@ -56,6 +66,7 @@ public class SystemRouter {
.PUT("/api/dictionaries/{id}", dictionaryHandler::updateDictionary) .PUT("/api/dictionaries/{id}", dictionaryHandler::updateDictionary)
.DELETE("/api/dictionaries/{id}", dictionaryHandler::deleteDictionary) .DELETE("/api/dictionaries/{id}", dictionaryHandler::deleteDictionary)
// ========== 用户路由 ==========
.GET("/api/users", userHandler::getAllUsers) .GET("/api/users", userHandler::getAllUsers)
.GET("/api/users/page", userHandler::getUsersByPage) .GET("/api/users/page", userHandler::getUsersByPage)
.GET("/api/users/count", userHandler::getUserCount) .GET("/api/users/count", userHandler::getUserCount)
@@ -74,6 +85,7 @@ public class SystemRouter {
.GET("/api/users/{id}/roles", userHandler::getUserRoles) .GET("/api/users/{id}/roles", userHandler::getUserRoles)
.POST("/api/users/{id}/roles", userHandler::assignRoles) .POST("/api/users/{id}/roles", userHandler::assignRoles)
// ========== 菜单路由 ==========
.GET("/api/menus", menuHandler::getAllMenus) .GET("/api/menus", menuHandler::getAllMenus)
.GET("/api/menus/tree", menuHandler::getMenuTree) .GET("/api/menus/tree", menuHandler::getMenuTree)
.GET("/api/menus/{id}", menuHandler::getMenuById) .GET("/api/menus/{id}", menuHandler::getMenuById)
@@ -81,6 +93,7 @@ public class SystemRouter {
.PUT("/api/menus/{id}", menuHandler::updateMenu) .PUT("/api/menus/{id}", menuHandler::updateMenu)
.DELETE("/api/menus/{id}", menuHandler::deleteMenu) .DELETE("/api/menus/{id}", menuHandler::deleteMenu)
// ========== 角色路由 ==========
.GET("/api/roles", roleHandler::getAllRoles) .GET("/api/roles", roleHandler::getAllRoles)
.GET("/api/roles/page", roleHandler::getRolesByPage) .GET("/api/roles/page", roleHandler::getRolesByPage)
.GET("/api/roles/count", roleHandler::getRoleCount) .GET("/api/roles/count", roleHandler::getRoleCount)
@@ -94,6 +107,7 @@ public class SystemRouter {
.GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId) .GET("/api/roles/{id}/permissions", permissionHandler::getPermissionsByRoleId)
.POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole) .POST("/api/roles/{id}/permissions", permissionHandler::assignPermissionsToRole)
// ========== 配置路由 ==========
.GET("/api/config", configHandler::getAllConfigs) .GET("/api/config", configHandler::getAllConfigs)
.GET("/api/config/{id}", configHandler::getConfigById) .GET("/api/config/{id}", configHandler::getConfigById)
.GET("/api/config/key/{configKey}", configHandler::getConfigByKey) .GET("/api/config/key/{configKey}", configHandler::getConfigByKey)
@@ -101,6 +115,7 @@ public class SystemRouter {
.PUT("/api/config/{id}", configHandler::updateConfig) .PUT("/api/config/{id}", configHandler::updateConfig)
.DELETE("/api/config/{id}", configHandler::deleteConfig) .DELETE("/api/config/{id}", configHandler::deleteConfig)
// ========== 日志路由 ==========
.GET("/api/logs/login", logHandler::getAllLoginLogs) .GET("/api/logs/login", logHandler::getAllLoginLogs)
.GET("/api/logs/login/page", logHandler::getLoginLogsByPage) .GET("/api/logs/login/page", logHandler::getLoginLogsByPage)
.GET("/api/logs/login/count", logHandler::getLoginLogCount) .GET("/api/logs/login/count", logHandler::getLoginLogCount)
@@ -120,12 +135,15 @@ public class SystemRouter {
.GET("/api/logs/operation/{id}", operationLogHandler::getOperationLogById) .GET("/api/logs/operation/{id}", operationLogHandler::getOperationLogById)
.POST("/api/logs/operation", operationLogHandler::createOperationLog) .POST("/api/logs/operation", operationLogHandler::createOperationLog)
// ========== 认证路由 ==========
.POST("/api/auth/login", authHandler::login) .POST("/api/auth/login", authHandler::login)
.POST("/api/auth/register", authHandler::register) .POST("/api/auth/register", authHandler::register)
.POST("/api/auth/logout", authHandler::logout) .POST("/api/auth/logout", authHandler::logout)
// ========== 统计路由 ==========
.GET("/api/stats/overview", statsHandler::getOverview) .GET("/api/stats/overview", statsHandler::getOverview)
// ========== 数据字典路由 ==========
.GET("/api/dict/types", dictHandler::getAllDictTypes) .GET("/api/dict/types", dictHandler::getAllDictTypes)
.GET("/api/dict/types/{id}", dictHandler::getDictTypeById) .GET("/api/dict/types/{id}", dictHandler::getDictTypeById)
.GET("/api/dict/types/type/{dictType}", dictHandler::getDictTypeByType) .GET("/api/dict/types/type/{dictType}", dictHandler::getDictTypeByType)
@@ -139,6 +157,7 @@ public class SystemRouter {
.PUT("/api/dict/data/{id}", dictHandler::updateDictData) .PUT("/api/dict/data/{id}", dictHandler::updateDictData)
.DELETE("/api/dict/data/{id}", dictHandler::deleteDictData) .DELETE("/api/dict/data/{id}", dictHandler::deleteDictData)
// ========== 公告路由 ==========
.GET("/api/notices", noticeHandler::getAllNotices) .GET("/api/notices", noticeHandler::getAllNotices)
.GET("/api/notices/{id}", noticeHandler::getNoticeById) .GET("/api/notices/{id}", noticeHandler::getNoticeById)
.GET("/api/notices/status/{status}", noticeHandler::getNoticesByStatus) .GET("/api/notices/status/{status}", noticeHandler::getNoticesByStatus)
@@ -146,6 +165,7 @@ public class SystemRouter {
.PUT("/api/notices/{id}", noticeHandler::updateNotice) .PUT("/api/notices/{id}", noticeHandler::updateNotice)
.DELETE("/api/notices/{id}", noticeHandler::deleteNotice) .DELETE("/api/notices/{id}", noticeHandler::deleteNotice)
// ========== 消息路由 ==========
.GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser) .GET("/api/messages/user/{userId}", messageHandler::getMessagesByUser)
.GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount) .GET("/api/messages/user/{userId}/unread", messageHandler::getUnreadCount)
.GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList) .GET("/api/messages/user/{userId}/unread/list", messageHandler::getUnreadList)
@@ -153,6 +173,7 @@ public class SystemRouter {
.PUT("/api/messages/{id}/read", messageHandler::markAsRead) .PUT("/api/messages/{id}/read", messageHandler::markAsRead)
.DELETE("/api/messages/{id}", messageHandler::deleteMessage) .DELETE("/api/messages/{id}", messageHandler::deleteMessage)
// ========== 文件路由 ==========
.GET("/api/files", fileHandler::getAllFiles) .GET("/api/files", fileHandler::getAllFiles)
.GET("/api/files/{id}", fileHandler::getFileById) .GET("/api/files/{id}", fileHandler::getFileById)
.POST("/api/files/upload", fileHandler::uploadFile) .POST("/api/files/upload", fileHandler::uploadFile)
@@ -162,6 +183,7 @@ public class SystemRouter {
.GET("/api/files/preview/{fileName}", fileHandler::previewFileByName) .GET("/api/files/preview/{fileName}", fileHandler::previewFileByName)
.DELETE("/api/files/{id}", fileHandler::deleteFile) .DELETE("/api/files/{id}", fileHandler::deleteFile)
// ========== 权限路由 ==========
.GET("/api/permissions", permissionHandler::getAllPermissions) .GET("/api/permissions", permissionHandler::getAllPermissions)
.GET("/api/permissions/{id}", permissionHandler::getPermissionById) .GET("/api/permissions/{id}", permissionHandler::getPermissionById)
.GET("/api/permissions/code/{code}", permissionHandler::getPermissionByCode) .GET("/api/permissions/code/{code}", permissionHandler::getPermissionByCode)
@@ -171,11 +193,6 @@ public class SystemRouter {
.PUT("/api/permissions/{id}", permissionHandler::updatePermission) .PUT("/api/permissions/{id}", permissionHandler::updatePermission)
.DELETE("/api/permissions/{id}", permissionHandler::deletePermission) .DELETE("/api/permissions/{id}", permissionHandler::deletePermission)
.GET("/api/groupCourse", groupCourseHandler::getAllGroupCourse)
.POST("/api/groupCourse/page", groupCourseHandler::getGroupCoursesByPage)
.POST("/api/groupCourse/cache/get", groupCourseHandler::getCacheByKey)
.GET("/api/groupCourse/{id}", groupCourseHandler::getGroupCourseById)
.build(); .build();
} }
} }
@@ -1,25 +0,0 @@
package cn.novalon.gym.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);
}
}
@@ -12,9 +12,6 @@ spring:
max-life-time: 30m max-life-time: 30m
acquire-timeout: 3s acquire-timeout: 3s
flyway: flyway:
url: jdbc:postgresql://localhost:55432/manage_system
user: novalon
password: novalon123
enabled: true enabled: true
locations: classpath:db/migration locations: classpath:db/migration
baseline-on-migrate: true baseline-on-migrate: true
@@ -31,10 +31,6 @@ spring:
logging: logging:
level: level:
cn.novalon.manage: DEBUG cn.novalon.manage: DEBUG
cn.novalon.gym.manage: DEBUG
cn.novalon.gym.manage.sys.audit: DEBUG
org.springframework.r2dbc: DEBUG org.springframework.r2dbc: DEBUG
cn.novalon.manage.db: DEBUG cn.novalon.manage.db: DEBUG
org.flywaydb: DEBUG org.flywaydb: DEBUG
debug: true
@@ -2,8 +2,6 @@ server:
port: 8084 port: 8084
spring: spring:
aop:
proxy-target-class: true
application: application:
name: gym-manage-api name: gym-manage-api
main: main:
@@ -25,8 +23,8 @@ spring:
acquire-timeout: 5s acquire-timeout: 5s
datasource: datasource:
url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system} url: jdbc:postgresql://${DB_HOST:localhost}:${DB_PORT:55432}/${DB_NAME:manage_system}
username: ${DB_USERNAME:novalon} username: ${DB_USERNAME:postgres}
password: ${DB_PASSWORD:novalon123} password: ${DB_PASSWORD:postgres}
driver-class-name: org.postgresql.Driver driver-class-name: org.postgresql.Driver
flyway: flyway:
enabled: true enabled: true
@@ -38,21 +36,6 @@ spring:
user: user:
name: disabled name: disabled
password: disabled password: disabled
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:novalon123}
timeout: 5000
lettuce:
pool:
max-active: 8 # 最大连接数
max-idle: 8 # 最大空闲连接
min-idle: 0 # 最小空闲连接
max-wait: -1ms # 连接等待时间
profiles:
active: dev
management: management:
endpoints: endpoints:
-2
View File
@@ -99,8 +99,6 @@
<artifactId>spring-boot-starter-test</artifactId> <artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
</dependencies> </dependencies>
<build> <build>
@@ -2,7 +2,6 @@ package cn.novalon.gym.manage.db.converter;
import cn.novalon.gym.manage.sys.audit.domain.AuditLog; import cn.novalon.gym.manage.sys.audit.domain.AuditLog;
import cn.novalon.gym.manage.db.entity.AuditLogEntity; import cn.novalon.gym.manage.db.entity.AuditLogEntity;
import io.r2dbc.postgresql.codec.Json;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@@ -29,8 +28,8 @@ public class AuditLogConverter {
domain.setOperationType(entity.getOperationType()); domain.setOperationType(entity.getOperationType());
domain.setOperator(entity.getOperator()); domain.setOperator(entity.getOperator());
domain.setOperationTime(entity.getOperationTime()); domain.setOperationTime(entity.getOperationTime());
domain.setBeforeData(entity.getBeforeData() != null ? entity.getBeforeData().asString() : null); domain.setBeforeData(entity.getBeforeData());
domain.setAfterData(entity.getAfterData() != null ? entity.getAfterData().asString() : null); domain.setAfterData(entity.getAfterData());
domain.setChangedFields(entity.getChangedFields()); domain.setChangedFields(entity.getChangedFields());
domain.setIpAddress(entity.getIpAddress()); domain.setIpAddress(entity.getIpAddress());
domain.setUserAgent(entity.getUserAgent()); domain.setUserAgent(entity.getUserAgent());
@@ -54,8 +53,8 @@ public class AuditLogConverter {
entity.setOperationType(domain.getOperationType()); entity.setOperationType(domain.getOperationType());
entity.setOperator(domain.getOperator()); entity.setOperator(domain.getOperator());
entity.setOperationTime(domain.getOperationTime()); entity.setOperationTime(domain.getOperationTime());
entity.setBeforeData(domain.getBeforeData() != null ? Json.of(domain.getBeforeData()) : null); entity.setBeforeData(domain.getBeforeData());
entity.setAfterData(domain.getAfterData() != null ? Json.of(domain.getAfterData()) : null); entity.setAfterData(domain.getAfterData());
entity.setChangedFields(domain.getChangedFields()); entity.setChangedFields(domain.getChangedFields());
entity.setIpAddress(domain.getIpAddress()); entity.setIpAddress(domain.getIpAddress());
entity.setUserAgent(domain.getUserAgent()); entity.setUserAgent(domain.getUserAgent());
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.db.entity; package cn.novalon.gym.manage.db.entity;
import io.r2dbc.postgresql.codec.Json;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table; import org.springframework.data.relational.core.mapping.Table;
@@ -29,10 +28,10 @@ public class AuditLogEntity extends BaseEntity {
private java.time.LocalDateTime operationTime; private java.time.LocalDateTime operationTime;
@Column("before_data") @Column("before_data")
private Json beforeData; private String beforeData;
@Column("after_data") @Column("after_data")
private Json afterData; private String afterData;
@Column("changed_fields") @Column("changed_fields")
private String[] changedFields; private String[] changedFields;
@@ -86,19 +85,19 @@ public class AuditLogEntity extends BaseEntity {
this.operationTime = operationTime; this.operationTime = operationTime;
} }
public Json getBeforeData() { public String getBeforeData() {
return beforeData; return beforeData;
} }
public void setBeforeData(Json beforeData) { public void setBeforeData(String beforeData) {
this.beforeData = beforeData; this.beforeData = beforeData;
} }
public Json getAfterData() { public String getAfterData() {
return afterData; return afterData;
} }
public void setAfterData(Json afterData) { public void setAfterData(String afterData) {
this.afterData = afterData; this.afterData = afterData;
} }
@@ -5,12 +5,17 @@ import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.Id; import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Persistable; import org.springframework.data.domain.Persistable;
import org.springframework.data.relational.core.mapping.Column; import org.springframework.data.relational.core.mapping.Column;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 数据库实体基类
*
* @author 张翔
* @date 2026-03-13
*/
public abstract class BaseEntity implements Persistable<Long> { public abstract class BaseEntity implements Persistable<Long> {
@Id @Id
@@ -35,9 +40,6 @@ public abstract class BaseEntity implements Persistable<Long> {
@Column("deleted_at") @Column("deleted_at")
private LocalDateTime deletedAt; private LocalDateTime deletedAt;
@Transient
private boolean newEntity = true;
@Override @Override
public Long getId() { public Long getId() {
return id; return id;
@@ -87,16 +89,12 @@ public abstract class BaseEntity implements Persistable<Long> {
this.deletedAt = deletedAt; this.deletedAt = deletedAt;
} }
/**
* 判断实体是否为新的
* 如果createdAt为null,则认为是新实体
*/
@Override @Override
public boolean isNew() { public boolean isNew() {
return newEntity; return createdAt == null;
}
public void markNotNew() {
this.newEntity = false;
}
public void markNew() {
this.newEntity = true;
} }
} }
@@ -7,7 +7,6 @@ import cn.novalon.gym.manage.db.dao.AuditLogDao;
import cn.novalon.gym.manage.db.entity.AuditLogEntity; import cn.novalon.gym.manage.db.entity.AuditLogEntity;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -27,12 +26,10 @@ public class AuditLogRepository implements IAuditLogRepository {
private final AuditLogDao auditLogDao; private final AuditLogDao auditLogDao;
private final AuditLogConverter auditLogConverter; private final AuditLogConverter auditLogConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public AuditLogRepository(AuditLogDao auditLogDao, AuditLogConverter auditLogConverter, R2dbcEntityTemplate r2dbcEntityTemplate) { public AuditLogRepository(AuditLogDao auditLogDao, AuditLogConverter auditLogConverter) {
this.auditLogDao = auditLogDao; this.auditLogDao = auditLogDao;
this.auditLogConverter = auditLogConverter; this.auditLogConverter = auditLogConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
} }
@Override @Override
@@ -44,12 +41,6 @@ public class AuditLogRepository implements IAuditLogRepository {
@Override @Override
public Mono<AuditLog> save(AuditLog auditLog) { public Mono<AuditLog> save(AuditLog auditLog) {
AuditLogEntity entity = auditLogConverter.toEntity(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) return auditLogDao.save(entity)
.map(auditLogConverter::toDomain); .map(auditLogConverter::toDomain);
} }
@@ -49,12 +49,6 @@ public class OperationLogRepository implements IOperationLogRepository {
@Override @Override
public Mono<OperationLog> save(OperationLog operationLog) { public Mono<OperationLog> save(OperationLog operationLog) {
OperationLogEntity entity = operationLogConverter.toEntity(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) return operationLogDao.save(entity)
.map(operationLogConverter::toDomain); .map(operationLogConverter::toDomain);
} }
@@ -60,20 +60,6 @@ public class SysMenuRepository implements ISysMenuRepository {
@Override @Override
public Mono<SysMenu> save(SysMenu sysMenu) { public Mono<SysMenu> save(SysMenu sysMenu) {
SysMenuEntity entity = sysMenuConverter.toEntity(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) return sysMenuDao.save(entity)
.map(sysMenuConverter::toDomain); .map(sysMenuConverter::toDomain);
} }
@@ -4,9 +4,7 @@ import cn.novalon.gym.manage.sys.core.domain.SysPermission;
import cn.novalon.gym.manage.sys.core.repository.ISysPermissionRepository; import cn.novalon.gym.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.gym.manage.db.converter.SysPermissionConverter; import cn.novalon.gym.manage.db.converter.SysPermissionConverter;
import cn.novalon.gym.manage.db.dao.SysPermissionDao; import cn.novalon.gym.manage.db.dao.SysPermissionDao;
import cn.novalon.gym.manage.db.entity.SysPermissionEntity;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -22,12 +20,10 @@ public class SysPermissionRepository implements ISysPermissionRepository {
private final SysPermissionDao sysPermissionDao; private final SysPermissionDao sysPermissionDao;
private final SysPermissionConverter sysPermissionConverter; private final SysPermissionConverter sysPermissionConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysPermissionRepository(SysPermissionDao sysPermissionDao, SysPermissionConverter sysPermissionConverter, R2dbcEntityTemplate r2dbcEntityTemplate) { public SysPermissionRepository(SysPermissionDao sysPermissionDao, SysPermissionConverter sysPermissionConverter) {
this.sysPermissionDao = sysPermissionDao; this.sysPermissionDao = sysPermissionDao;
this.sysPermissionConverter = sysPermissionConverter; this.sysPermissionConverter = sysPermissionConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
} }
@Override @Override
@@ -44,14 +40,7 @@ public class SysPermissionRepository implements ISysPermissionRepository {
@Override @Override
public Mono<SysPermission> save(SysPermission sysPermission) { public Mono<SysPermission> save(SysPermission sysPermission) {
SysPermissionEntity entity = sysPermissionConverter.toEntity(sysPermission); return sysPermissionDao.save(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); .map(sysPermissionConverter::toDomain);
} }
@@ -4,8 +4,6 @@ import cn.novalon.gym.manage.sys.core.domain.SysRolePermission;
import cn.novalon.gym.manage.sys.core.repository.ISysRolePermissionRepository; import cn.novalon.gym.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.gym.manage.db.converter.SysRolePermissionConverter; import cn.novalon.gym.manage.db.converter.SysRolePermissionConverter;
import cn.novalon.gym.manage.db.dao.SysRolePermissionDao; import cn.novalon.gym.manage.db.dao.SysRolePermissionDao;
import cn.novalon.gym.manage.db.entity.SysRolePermissionEntity;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -21,24 +19,15 @@ public class SysRolePermissionRepository implements ISysRolePermissionRepository
private final SysRolePermissionDao sysRolePermissionDao; private final SysRolePermissionDao sysRolePermissionDao;
private final SysRolePermissionConverter sysRolePermissionConverter; private final SysRolePermissionConverter sysRolePermissionConverter;
private final R2dbcEntityTemplate r2dbcEntityTemplate;
public SysRolePermissionRepository(SysRolePermissionDao sysRolePermissionDao, SysRolePermissionConverter sysRolePermissionConverter, R2dbcEntityTemplate r2dbcEntityTemplate) { public SysRolePermissionRepository(SysRolePermissionDao sysRolePermissionDao, SysRolePermissionConverter sysRolePermissionConverter) {
this.sysRolePermissionDao = sysRolePermissionDao; this.sysRolePermissionDao = sysRolePermissionDao;
this.sysRolePermissionConverter = sysRolePermissionConverter; this.sysRolePermissionConverter = sysRolePermissionConverter;
this.r2dbcEntityTemplate = r2dbcEntityTemplate;
} }
@Override @Override
public Mono<SysRolePermission> save(SysRolePermission rolePermission) { public Mono<SysRolePermission> save(SysRolePermission rolePermission) {
SysRolePermissionEntity entity = sysRolePermissionConverter.toEntity(rolePermission); return sysRolePermissionDao.save(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); .map(sysRolePermissionConverter::toDomain);
} }
@@ -53,12 +53,6 @@ public class SysRoleRepository implements ISysRoleRepository {
@Override @Override
public Mono<SysRole> save(SysRole sysRole) { public Mono<SysRole> save(SysRole sysRole) {
SysRoleEntity entity = sysRoleConverter.toEntity(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) return sysRoleDao.save(entity)
.map(sysRoleConverter::toDomain); .map(sysRoleConverter::toDomain);
} }
@@ -162,7 +156,6 @@ public class SysRoleRepository implements ISysRoleRepository {
@Override @Override
public Mono<SysRole> updateRole(SysRole role) { public Mono<SysRole> updateRole(SysRole role) {
SysRoleEntity entity = sysRoleConverter.toEntity(role); SysRoleEntity entity = sysRoleConverter.toEntity(role);
entity.markNotNew();
return sysRoleDao.save(entity) return sysRoleDao.save(entity)
.map(sysRoleConverter::toDomain); .map(sysRoleConverter::toDomain);
} }
@@ -10,7 +10,6 @@ import cn.novalon.gym.manage.sys.core.query.SysUserQuery;
import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository; import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository;
import cn.novalon.gym.manage.common.dto.PageRequest; import cn.novalon.gym.manage.common.dto.PageRequest;
import cn.novalon.gym.manage.common.dto.PageResponse; import cn.novalon.gym.manage.common.dto.PageResponse;
import cn.novalon.gym.manage.sys.dto.response.UserResponse;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.relational.core.query.Query; import org.springframework.data.relational.core.query.Query;
@@ -71,20 +70,6 @@ public class SysUserRepository implements ISysUserRepository {
@Override @Override
public Mono<SysUser> save(SysUser sysUser) { public Mono<SysUser> save(SysUser sysUser) {
SysUserEntity entity = sysUserConverter.toEntity(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) return sysUserDao.save(entity)
.map(sysUserConverter::toDomain); .map(sysUserConverter::toDomain);
} }
@@ -191,7 +176,6 @@ public class SysUserRepository implements ISysUserRepository {
public Mono<Void> logicalDeleteById(Long id) { public Mono<Void> logicalDeleteById(Long id) {
return sysUserDao.findById(id) return sysUserDao.findById(id)
.flatMap(entity -> { .flatMap(entity -> {
entity.markNotNew();
entity.setDeletedAt(java.time.LocalDateTime.now()); entity.setDeletedAt(java.time.LocalDateTime.now());
return sysUserDao.save(entity).then(); return sysUserDao.save(entity).then();
}); });
@@ -208,7 +192,6 @@ public class SysUserRepository implements ISysUserRepository {
public Mono<Void> restoreById(Long id) { public Mono<Void> restoreById(Long id) {
return sysUserDao.findById(id) return sysUserDao.findById(id)
.flatMap(entity -> { .flatMap(entity -> {
entity.markNotNew();
entity.setDeletedAt(null); entity.setDeletedAt(null);
return sysUserDao.save(entity).then(); return sysUserDao.save(entity).then();
}); });
@@ -0,0 +1,51 @@
-- 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));
@@ -0,0 +1,46 @@
-- 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;
@@ -0,0 +1,28 @@
-- 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);
@@ -0,0 +1,12 @@
-- Novalon管理系统审计日志表补充字段
-- 版本: V15
-- 描述: 为审计日志表添加缺失的基础字段,与BaseDomain保持一致
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS create_by VARCHAR(50);
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS update_by VARCHAR(50);
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP;
ALTER TABLE audit_log ADD COLUMN IF NOT EXISTS deleted_at TIMESTAMP;
COMMENT ON COLUMN audit_log.create_by IS '创建人';
COMMENT ON COLUMN audit_log.update_by IS '更新人';
COMMENT ON COLUMN audit_log.updated_at IS '更新时间';
COMMENT ON COLUMN audit_log.deleted_at IS '删除时间';
@@ -1,31 +1,25 @@
-- Novalon管理系统数据库初始化脚本 -- Novalon管理系统数据库初始化脚本
-- 版本: V1 -- 版本: V1
-- 描述: 创建所有核心表结构(合并版) -- 描述: 创建所有核心表结构
-- ============================================
-- 用户与角色相关表
-- ============================================
-- 用户表 -- 用户表
CREATE TABLE IF NOT EXISTS sys_user ( CREATE TABLE IF NOT EXISTS sys_user (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50) NOT NULL UNIQUE, username VARCHAR(50) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
email VARCHAR(100), email VARCHAR(100),
phone VARCHAR(20), phone VARCHAR(20),
nickname VARCHAR(100), nickname VARCHAR(100),
status INTEGER DEFAULT 1,
role_id BIGINT, role_id BIGINT,
status INTEGER DEFAULT 1,
create_by VARCHAR(50), create_by VARCHAR(50),
update_by VARCHAR(50), update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- 角色表 -- 角色表
CREATE TABLE IF NOT EXISTS sys_role ( CREATE TABLE IF NOT EXISTS sys_role (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
role_name VARCHAR(100) NOT NULL, role_name VARCHAR(100) NOT NULL,
role_key VARCHAR(100) NOT NULL UNIQUE, role_key VARCHAR(100) NOT NULL UNIQUE,
role_sort INTEGER DEFAULT 0, role_sort INTEGER DEFAULT 0,
@@ -36,60 +30,9 @@ CREATE TABLE IF NOT EXISTS sys_role (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at 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 ( CREATE TABLE IF NOT EXISTS sys_menu (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
menu_name VARCHAR(50) NOT NULL, menu_name VARCHAR(50) NOT NULL,
parent_id BIGINT DEFAULT 0, parent_id BIGINT DEFAULT 0,
order_num INTEGER DEFAULT 0, order_num INTEGER DEFAULT 0,
@@ -103,14 +46,9 @@ CREATE TABLE IF NOT EXISTS sys_menu (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- ============================================
-- 字典相关表
-- ============================================
-- 字典类型表 -- 字典类型表
CREATE TABLE IF NOT EXISTS sys_dict_type ( CREATE TABLE IF NOT EXISTS sys_dict_type (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
dict_name VARCHAR(100) NOT NULL, dict_name VARCHAR(100) NOT NULL,
dict_type VARCHAR(100) NOT NULL UNIQUE, dict_type VARCHAR(100) NOT NULL UNIQUE,
status VARCHAR(1) DEFAULT '0', status VARCHAR(1) DEFAULT '0',
@@ -121,10 +59,9 @@ CREATE TABLE IF NOT EXISTS sys_dict_type (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- 字典数据表 -- 字典数据表
CREATE TABLE IF NOT EXISTS sys_dict_data ( CREATE TABLE IF NOT EXISTS sys_dict_data (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
dict_sort INTEGER DEFAULT 0, dict_sort INTEGER DEFAULT 0,
dict_label VARCHAR(100) NOT NULL, dict_label VARCHAR(100) NOT NULL,
dict_value VARCHAR(100) NOT NULL, dict_value VARCHAR(100) NOT NULL,
@@ -139,10 +76,9 @@ CREATE TABLE IF NOT EXISTS sys_dict_data (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- 字典表(通用字典) -- 字典表(通用字典)
CREATE TABLE IF NOT EXISTS sys_dictionary ( CREATE TABLE IF NOT EXISTS sys_dictionary (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
type VARCHAR(100) NOT NULL, type VARCHAR(100) NOT NULL,
code VARCHAR(100) NOT NULL, code VARCHAR(100) NOT NULL,
name VARCHAR(100) NOT NULL, name VARCHAR(100) NOT NULL,
@@ -154,14 +90,9 @@ CREATE TABLE IF NOT EXISTS sys_dictionary (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- ============================================
-- 系统配置表
-- ============================================
-- 系统配置表 -- 系统配置表
CREATE TABLE IF NOT EXISTS sys_config ( CREATE TABLE IF NOT EXISTS sys_config (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
config_name VARCHAR(100) NOT NULL, config_name VARCHAR(100) NOT NULL,
config_key VARCHAR(100) NOT NULL UNIQUE, config_key VARCHAR(100) NOT NULL UNIQUE,
config_value VARCHAR(500) NOT NULL, config_value VARCHAR(500) NOT NULL,
@@ -172,14 +103,9 @@ CREATE TABLE IF NOT EXISTS sys_config (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- ============================================
-- 日志相关表
-- ============================================
-- 登录日志表 -- 登录日志表
CREATE TABLE IF NOT EXISTS sys_login_log ( CREATE TABLE IF NOT EXISTS sys_login_log (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50), username VARCHAR(50),
ip VARCHAR(50), ip VARCHAR(50),
location VARCHAR(255), location VARCHAR(255),
@@ -189,10 +115,9 @@ CREATE TABLE IF NOT EXISTS sys_login_log (
message VARCHAR(255), message VARCHAR(255),
login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP login_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- 异常日志表 -- 异常日志表
CREATE TABLE IF NOT EXISTS sys_exception_log ( CREATE TABLE IF NOT EXISTS sys_exception_log (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50), username VARCHAR(50),
title VARCHAR(100), title VARCHAR(100),
exception_name VARCHAR(100), exception_name VARCHAR(100),
@@ -203,10 +128,9 @@ CREATE TABLE IF NOT EXISTS sys_exception_log (
ip VARCHAR(50), ip VARCHAR(50),
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
-- 操作日志表 -- 操作日志表
CREATE TABLE IF NOT EXISTS operation_log ( CREATE TABLE IF NOT EXISTS operation_log (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
username VARCHAR(50), username VARCHAR(50),
operation VARCHAR(100), operation VARCHAR(100),
method VARCHAR(200), method VARCHAR(200),
@@ -222,53 +146,9 @@ CREATE TABLE IF NOT EXISTS operation_log (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at 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 ( CREATE TABLE IF NOT EXISTS sys_notice (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
notice_title VARCHAR(50) NOT NULL, notice_title VARCHAR(50) NOT NULL,
notice_type VARCHAR(1) NOT NULL, notice_type VARCHAR(1) NOT NULL,
notice_content TEXT, notice_content TEXT,
@@ -279,10 +159,9 @@ CREATE TABLE IF NOT EXISTS sys_notice (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- 用户消息表 -- 用户消息表
CREATE TABLE IF NOT EXISTS sys_user_message ( CREATE TABLE IF NOT EXISTS sys_user_message (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
user_id BIGINT NOT NULL, user_id BIGINT NOT NULL,
notice_id BIGINT, notice_id BIGINT,
message_title VARCHAR(255), message_title VARCHAR(255),
@@ -295,14 +174,9 @@ CREATE TABLE IF NOT EXISTS sys_user_message (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- ============================================
-- 文件管理表
-- ============================================
-- 文件管理表 -- 文件管理表
CREATE TABLE IF NOT EXISTS sys_file ( CREATE TABLE IF NOT EXISTS sys_file (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
file_name VARCHAR(255) NOT NULL, file_name VARCHAR(255) NOT NULL,
file_path VARCHAR(500) NOT NULL, file_path VARCHAR(500) NOT NULL,
file_size BIGINT, file_size BIGINT,
@@ -315,14 +189,9 @@ CREATE TABLE IF NOT EXISTS sys_file (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP deleted_at TIMESTAMP
); );
-- ============================================
-- OAuth2相关表
-- ============================================
-- OAuth2客户端表 -- OAuth2客户端表
CREATE TABLE IF NOT EXISTS oauth2_client ( CREATE TABLE IF NOT EXISTS oauth2_client (
id BIGSERIAL PRIMARY KEY, id BIGINT PRIMARY KEY,
client_id VARCHAR(100) NOT NULL UNIQUE, client_id VARCHAR(100) NOT NULL UNIQUE,
client_secret VARCHAR(255) NOT NULL, client_secret VARCHAR(255) NOT NULL,
client_name VARCHAR(100), client_name VARCHAR(100),
@@ -339,31 +208,7 @@ CREATE TABLE IF NOT EXISTS oauth2_client (
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at 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 TABLE sys_exception_log IS '异常日志表';
COMMENT ON COLUMN sys_exception_log.id IS '主键ID'; COMMENT ON COLUMN sys_exception_log.id IS '主键ID';
COMMENT ON COLUMN sys_exception_log.username IS '操作用户'; COMMENT ON COLUMN sys_exception_log.username IS '操作用户';
@@ -375,33 +220,5 @@ COMMENT ON COLUMN sys_exception_log.exception_msg IS '异常消息';
COMMENT ON COLUMN sys_exception_log.exception_stack 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.ip IS 'IP地址';
COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间'; COMMENT ON COLUMN sys_exception_log.create_time IS '创建时间';
COMMENT ON TABLE sys_menu IS '系统菜单表';
COMMENT ON TABLE audit_log IS '审计日志表'; COMMENT ON TABLE sys_login_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,233 +1,67 @@
-- Novalon管理系统初始数据脚本 -- Novalon管理系统初始数据脚本
-- 版本: V2 -- 版本: 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;
INSERT INTO sys_role (id, role_name, role_key, role_sort, status, create_by, update_by, created_at, updated_at) -- 插入初始管理员用户
VALUES -- BCrypt哈希值对应明文密码: admin123
(1, '超级管理员', 'admin', 1, 1, 'system', 'system', NOW(), NOW()), INSERT INTO sys_user (id, username, password, email, phone, status, create_by, update_by)
(2, '测试管理员', 'test_admin', 2, 1, 'system', 'system', NOW(), NOW()), VALUES (1, 'admin', '$2b$12$SFefXlGRFMA0fvxIufpWPuIAl0OPLgRDoCZPThCvjpiJGPYS8yNYy', 'admin@novalon.com', '13800138000', 1, 'system', 'system')
(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 ON CONFLICT (username) DO UPDATE SET
password = EXCLUDED.password, password = EXCLUDED.password,
status = EXCLUDED.status; status = EXCLUDED.status;
SELECT setval('sys_user_id_seq', 10); -- 插入测试用户(用于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)
-- ============================================
-- 为admin用户分配超级管理员角色
INSERT INTO user_role (user_id, role_id, created_by, created_at)
VALUES VALUES
(1, 1, 'system', NOW()), ('用户状态', 'user_status', '0', '用户状态列表', 'system', 'system'),
(2, 3, 'system', NOW()), ('菜单状态', 'menu_status', '0', '菜单状态列表', 'system', 'system'),
(10, 1, 'system', NOW()) ('角色状态', 'role_status', '0', '角色状态列表', 'system', 'system'),
ON CONFLICT (user_id, role_id) DO NOTHING; ('系统开关', 'sys_normal_disable', '0', '系统开关列表', 'system', 'system')
-- ============================================
-- 权限数据
-- ============================================
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; 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, created_at, updated_at) INSERT INTO sys_dict_data (dict_sort, dict_label, dict_value, dict_type, css_class, list_class, is_default, status, create_by, update_by)
VALUES VALUES
-- 用户状态 -- 用户状态
(1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()), (1, '正常', '1', 'user_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()), (2, '停用', '0', 'user_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 菜单状态 -- 菜单状态
(1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()), (1, '正常', '0', 'menu_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()), (2, '停用', '1', 'menu_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 角色状态 -- 角色状态
(1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system', NOW(), NOW()), (1, '正常', '0', 'role_status', '', 'primary', 'Y', '0', 'system', 'system'),
(2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system', NOW(), NOW()), (2, '停用', '1', 'role_status', '', 'danger', 'N', '0', 'system', 'system'),
-- 系统开关 -- 系统开关
(1, '正常', '0', 'sys_normal_disable', '', 'primary', 'Y', '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', NOW(), NOW()); (2, '停用', '1', 'sys_normal_disable', '', 'danger', 'N', '0', 'system', 'system')
ON CONFLICT DO NOTHING;
-- ============================================ -- 插入初始系统配置
-- 系统配置 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 VALUES
('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system', NOW(), NOW()), ('用户管理-用户初始密码', 'sys.user.initPassword', '123456', 'Y', 'system', 'system'),
('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system', NOW(), NOW()), ('主框架页-默认皮肤样式名称', 'sys.index.skinName', 'skin-blue', 'Y', 'system', 'system'),
('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()), ('用户自助-验证码开关', 'sys.account.captchaEnabled', 'true', 'Y', 'system', 'system'),
('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system', NOW(), NOW()), ('用户自助-是否开启用户注册功能', 'sys.account.registerUser', 'false', 'Y', 'system', 'system'),
('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system', NOW(), NOW()) ('账号自助-密码验证码', 'sys.account.pwdCaptchaEnabled', 'true', 'Y', 'system', 'system')
ON CONFLICT (config_key) DO NOTHING; 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_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_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));
@@ -0,0 +1,23 @@
-- 创建用户角色关联表(支持多对多关系)
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 '创建人';
@@ -0,0 +1,104 @@
-- 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,11 +1,7 @@
-- Novalon管理系统索引优化脚本 -- Novalon管理系统索引优化脚本
-- 版本: V3 -- 版本: V5
-- 描述: 为表创建必要的索引以提升查询性能 -- 描述: 为表创建必要的索引以提升查询性能
-- ============================================
-- 用户与角色表索引
-- ============================================
-- 用户表索引 -- 用户表索引
CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username); CREATE INDEX IF NOT EXISTS idx_users_username ON sys_user(username);
CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email); CREATE INDEX IF NOT EXISTS idx_users_email ON sys_user(email);
@@ -17,35 +13,11 @@ 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_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_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_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_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_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_dict_type ON sys_dict_type(dict_type);
CREATE INDEX IF NOT EXISTS idx_sys_dict_type_status ON sys_dict_type(status); CREATE INDEX IF NOT EXISTS idx_sys_dict_type_status ON sys_dict_type(status);
@@ -57,23 +29,16 @@ 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_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_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 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_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_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_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_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_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_username ON sys_login_log(username);
CREATE INDEX IF NOT EXISTS idx_sys_login_log_ip ON sys_login_log(ip); CREATE INDEX IF NOT EXISTS idx_sys_login_log_ip ON sys_login_log(ip);
@@ -92,26 +57,6 @@ 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_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_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_notice_type ON sys_notice(notice_type);
CREATE INDEX IF NOT EXISTS idx_sys_notice_status ON sys_notice(status); CREATE INDEX IF NOT EXISTS idx_sys_notice_status ON sys_notice(status);
@@ -123,17 +68,11 @@ 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_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_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_file_type ON sys_file(file_type);
CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at); CREATE INDEX IF NOT EXISTS idx_sys_file_deleted_at ON sys_file(deleted_at);
-- ============================================
-- OAuth2客户端表索引 -- OAuth2客户端表索引
-- ============================================
CREATE INDEX IF NOT EXISTS idx_oauth2_client_client_id ON oauth2_client(client_id); 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_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,41 +0,0 @@
-- 创建操作日志表
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,71 +0,0 @@
-- ============================================
-- 团课相关表
-- ============================================
-- 团课课程表
CREATE TABLE IF NOT EXISTS group_course (
id BIGSERIAL PRIMARY KEY,
course_name VARCHAR(100) NOT NULL,
coach_id BIGINT,
course_type BIGINT,
start_time TIMESTAMP NOT NULL,
end_time TIMESTAMP NOT NULL,
max_members INTEGER DEFAULT 20,
current_members INTEGER DEFAULT 0,
status VARCHAR(1) DEFAULT '0',
location VARCHAR(255),
cover_image VARCHAR(500),
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 group_course_booking (
id BIGSERIAL PRIMARY KEY,
course_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
booking_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
status VARCHAR(1) DEFAULT '0',
cancel_time TIMESTAMP,
create_by VARCHAR(50),
update_by VARCHAR(50),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
deleted_at TIMESTAMP
);
COMMENT ON TABLE group_course IS '团课课程表';
COMMENT ON COLUMN group_course.id IS '主键ID';
COMMENT ON COLUMN group_course.course_name IS '课程名称';
COMMENT ON COLUMN group_course.coach_id IS '教练ID(关联sys_user';
COMMENT ON COLUMN group_course.course_type IS '课程类型(如瑜伽/普拉提/动感单车)';
COMMENT ON COLUMN group_course.start_time IS '开始时间';
COMMENT ON COLUMN group_course.end_time IS '结束时间';
COMMENT ON COLUMN group_course.max_members IS '最大参与人数';
COMMENT ON COLUMN group_course.current_members IS '当前参与人数';
COMMENT ON COLUMN group_course.status IS '状态(0正常 1已取消 2已结束)';
COMMENT ON COLUMN group_course.location IS '上课地点';
COMMENT ON COLUMN group_course.cover_image IS '封面图URL';
COMMENT ON COLUMN group_course.description IS '课程描述';
COMMENT ON COLUMN group_course.create_by IS '创建人';
COMMENT ON COLUMN group_course.update_by IS '更新人';
COMMENT ON COLUMN group_course.created_at IS '创建时间';
COMMENT ON COLUMN group_course.updated_at IS '更新时间';
COMMENT ON COLUMN group_course.deleted_at IS '删除时间(软删除)';
COMMENT ON TABLE group_course_booking IS '团课预约记录表';
COMMENT ON COLUMN group_course_booking.id IS '主键ID';
COMMENT ON COLUMN group_course_booking.course_id IS '团课ID';
COMMENT ON COLUMN group_course_booking.user_id IS '用户ID';
COMMENT ON COLUMN group_course_booking.booking_time IS '预约时间';
COMMENT ON COLUMN group_course_booking.status IS '状态(0已预约 1已取消 2已出席 3缺席)';
COMMENT ON COLUMN group_course_booking.cancel_time IS '取消时间';
COMMENT ON COLUMN group_course_booking.create_by IS '创建人';
COMMENT ON COLUMN group_course_booking.update_by IS '更新人';
COMMENT ON COLUMN group_course_booking.created_at IS '创建时间';
COMMENT ON COLUMN group_course_booking.updated_at IS '更新时间';
COMMENT ON COLUMN group_course_booking.deleted_at IS '删除时间(软删除)';
@@ -0,0 +1,90 @@
-- 系统菜单初始化数据
-- 版本: 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());
@@ -0,0 +1,40 @@
-- 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,19 +0,0 @@
-- 测试数据1: 进行中的瑜伽课程 (已有部分学员)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('清晨流瑜伽', 101, 1, '2026-05-10 09:00:00', '2026-05-10 10:30:00', 15, 8, '1', 'A座3楼瑜伽教室', '/images/yoga_flow.jpg', '适合有一定基础的学员,通过流畅的体式连接呼吸,唤醒身体能量。', 'admin', '2026-05-01 10:00:00', '2026-05-01 10:00:00');
-- 测试数据2: 即将开始的搏击课 (几乎满员)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('燃脂搏击', 102, 2, '2026-05-12 18:30:00', '2026-05-12 19:30:00', 20, 19, '1', '综合训练区', '/images/kickboxing.jpg', '高强度间歇训练,配合音乐快速燃脂,释放压力。', 'coach_zhang', '2026-05-02 14:30:00', '2026-05-02 14:30:00');
-- 测试数据3: 已结束的私教小团课 (课程号已满)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('蜜桃臀塑造', 103, 3, '2026-04-25 19:00:00', '2026-04-25 20:00:00', 10, 10, '2', '私教专区', '/images/glute.jpg', '针对性训练臀部肌肉群,打造完美臀线。小班教学,动作一对一纠正。', 'coach_li', '2026-04-20 09:15:00', '2026-04-20 09:15:00');
-- 测试数据4: 即将开始的动感单车 (名额充足,尚未有人报名)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at) VALUES
('极速燃脂单车', 104, 2, '2026-05-15 19:30:00', '2026-05-15 20:20:00', 25, 0, '0', '单车房', '/images/spinning.jpg', '跟随音乐节奏变换阻力和速度,体验爬坡与冲刺的快感,一节课消耗800大卡。', 'admin', '2026-05-06 11:00:00', '2026-05-06 11:00:00');
-- 测试数据5: 已删除/作废的课程 (deleted_at 不为空)
INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, cover_image, description, create_by, created_at, updated_at, deleted_at) VALUES
('周末冥想修复', 101, 1, '2026-05-03 15:00:00', '2026-05-03 16:00:00', 12, 3, '3', '冥想室', '/images/meditation.jpg', '通过呼吸和正念冥想,深度放松身心,缓解一周疲劳。', 'coach_wang', '2026-04-28 08:00:00', '2026-04-28 08:00:00', '2026-04-29 16:20:00');
@@ -0,0 +1,43 @@
-- 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 '归档时间';
@@ -1,5 +1,5 @@
-- Novalon管理系统权限授予脚本 -- Novalon管理系统权限授予脚本
-- 版本: V4 -- 版本: V9
-- 描述: 为novalon用户授予所有表的访问权限 -- 描述: 为novalon用户授予所有表的访问权限
-- 授予所有表的SELECT, INSERT, UPDATE, DELETE权限 -- 授予所有表的SELECT, INSERT, UPDATE, DELETE权限
@@ -2,84 +2,186 @@ package cn.novalon.gym.manage.sys.audit;
import cn.novalon.gym.manage.sys.audit.domain.AuditLog; import cn.novalon.gym.manage.sys.audit.domain.AuditLog;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService; import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import com.fasterxml.jackson.annotation.JsonIdentityInfo;
import com.fasterxml.jackson.annotation.ObjectIdGenerators;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect; 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.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Persistable;
import org.springframework.security.core.context.ReactiveSecurityContextHolder; import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
/**
* 审计日志切面
*
* 文件定义:使用AOP自动拦截Repository操作,记录审计日志
* 涉及业务:自动记录所有数据变更操作,包括变更前后对比
* 算法:使用异步方式记录日志,不阻塞主流程
*
* @author 张翔
* @date 2026-04-01
*/
@Aspect @Aspect
@Component @Component
@Deprecated
public class AuditLogAspect { public class AuditLogAspect {
private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class); private static final Logger logger = LoggerFactory.getLogger(AuditLogAspect.class);
private final IAuditLogService auditLogService; private final IAuditLogService auditLogService;
private final ObjectMapper objectMapper;
public AuditLogAspect(IAuditLogService auditLogService) { public AuditLogAspect(IAuditLogService auditLogService, ObjectMapper objectMapper) {
this.auditLogService = auditLogService; this.auditLogService = auditLogService;
logger.info("=== AuditLogAspect 初始化完成 ==="); this.objectMapper = new ObjectMapper()
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
} }
@Before("execution(* cn.novalon.gym.manage.sys.core.service.impl.SysUserService.createUser(..))") @Around("(execution(* cn.novalon.gym.manage.db.repository.*Repository.save(..)) || " +
public void testAopWorking() { "execution(* cn.novalon.gym.manage.db.repository.*Repository.delete(..)) || " +
logger.info("=== AuditLogAspect @Before 测试: SysUserService.createUser 被调用 ==="); "execution(* cn.novalon.gym.manage.db.repository.*Repository.deleteById(..))) && " +
} "!execution(* cn.novalon.gym.manage.db.repository.AuditLogRepository.*(..)) && " +
"!execution(* cn.novalon.gym.manage.db.dao.AuditLogDao.*(..))")
@Around("@annotation(auditable)") public Object logAuditEvent(ProceedingJoinPoint joinPoint) throws Throwable {
public Object logAuditEvent(ProceedingJoinPoint joinPoint, Auditable auditable) throws Throwable { String methodName = joinPoint.getSignature().getName();
String methodName = ((MethodSignature) joinPoint.getSignature()).getName();
String className = joinPoint.getTarget().getClass().getSimpleName(); String className = joinPoint.getTarget().getClass().getSimpleName();
String entityType = auditable.entityType(); Object[] args = joinPoint.getArgs();
String operationType = auditable.operationType();
logger.debug("审计切面拦截: {}.{}(), entityType={}, operationType={}", className, methodName, entityType, operationType); String operationType = determineOperationType(methodName);
String entityType = extractEntityType(className);
logger.debug("拦截审计操作: {}.{}, 操作类型: {}, 实体类型: {}",
className, methodName, operationType, entityType);
try { 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;
}
}
private Object handleSaveOperation(ProceedingJoinPoint joinPoint, Object entity,
String entityType, String operationType) throws Throwable {
String entityClassName = entity.getClass().getSimpleName();
if (entityClassName.contains("AuditLog") || entityClassName.contains("AuditLogEntity")) {
logger.debug("跳过审计日志实体的审计记录: {}", entityClassName);
return joinPoint.proceed();
}
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(); Object result = joinPoint.proceed();
if (result instanceof Mono) { if (result instanceof Mono) {
return ((Mono<Object>) result).flatMap(retValue -> { return ((Mono<?>) result).flatMap(savedEntity -> {
Long entityId = extractIdFromResult(retValue); String afterData = serializeEntity(savedEntity);
String afterData = serializeEntity(retValue); 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 createAndSaveAuditLog( return createAndSaveAuditLog(
entityType, entityId, operationType, entityType, finalEntityId, finalOperationType,
null, afterData finalBeforeData, afterData, savedEntity
).thenReturn(retValue); ).thenReturn(savedEntity);
});
} else if (result instanceof Flux) {
return ((Flux<Object>) result).collectList()
.flatMapMany(list -> {
String afterData = serializeEntity(list);
return createAndSaveAuditLog(
entityType, null, operationType,
null, afterData
).thenMany(Flux.fromIterable(list));
}); });
} }
return result; return result;
} catch (Throwable error) { } catch (Throwable error) {
logger.error("审计日志记录失败: {}.{}()", className, methodName, 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 result;
} catch (Throwable error) {
logger.error("删除操作审计日志记录失败", error);
throw error; throw error;
} }
} }
private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId, private Mono<Void> createAndSaveAuditLog(String entityType, Long entityId,
String operationType, String beforeData, String operationType, String beforeData,
String afterData) { String afterData, Object entity) {
logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType); logger.debug("创建审计日志: entityType={}, entityId={}, operationType={}", entityType, entityId, operationType);
return ReactiveSecurityContextHolder.getContext() return ReactiveSecurityContextHolder.getContext()
.map(ctx -> ctx.getAuthentication().getPrincipal()) .map(ctx -> ctx.getAuthentication().getPrincipal())
.defaultIfEmpty("system") .defaultIfEmpty("system")
@@ -91,12 +193,22 @@ public class AuditLogAspect {
auditLog.setOperator(principal instanceof String ? (String) principal : "system"); auditLog.setOperator(principal instanceof String ? (String) principal : "system");
auditLog.setBeforeData(beforeData); auditLog.setBeforeData(beforeData);
auditLog.setAfterData(afterData); 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)); auditLog.setDescription(generateDescription(entityType, operationType, entityId));
return auditLogService.saveAsync(auditLog) return auditLogService.save(auditLog)
.doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}, ID={}", .doOnSuccess(saved -> logger.debug("审计日志保存成功: {} - {}",
entityType, operationType, saved.getId())) entityType, operationType))
.doOnError(error -> logger.error("审计日志保存失败: {}", error.getMessage())) .doOnError(error -> logger.error("审计日志保存失败: {}",
error.getMessage()))
.then(); .then();
}) })
.onErrorResume(error -> { .onErrorResume(error -> {
@@ -105,51 +217,97 @@ public class AuditLogAspect {
}); });
} }
private Long extractIdFromResult(Object result) { private String determineOperationType(String methodName) {
if (result == null) { if (methodName.startsWith("save")) {
return null; return "SAVE";
} else if (methodName.startsWith("delete")) {
return "DELETE";
} }
try { return "UNKNOWN";
var getIdMethod = result.getClass().getMethod("getId");
Object id = getIdMethod.invoke(result);
if (id instanceof Number) {
return ((Number) id).longValue();
} }
if (id instanceof String) {
try { private String extractEntityType(String className) {
return Long.parseLong((String) id); if (className.contains("User")) {
} catch (NumberFormatException e) { return "User";
return null; } else if (className.contains("Role")) {
return "Role";
} else if (className.contains("Menu")) {
return "Menu";
} else if (className.contains("Permission")) {
return "Permission";
} }
return className.replace("Repository", "").replace("Impl", "");
} }
} catch (NoSuchMethodException e) {
logger.debug("结果对象没有getId方法: {}", result.getClass().getSimpleName()); private String fetchEntityBeforeData(String entityType, Long entityId) {
} catch (Exception e) {
logger.debug("提取结果ID失败: {}", e.getMessage());
}
return null; return null;
} }
private String serializeEntity(Object entity) { private String serializeEntity(Object entity) {
try { try {
ObjectMapper mapper = new ObjectMapper() return objectMapper.writeValueAsString(entity);
.registerModule(new JavaTimeModule())
.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.disable(SerializationFeature.FAIL_ON_SELF_REFERENCES);
return mapper.writeValueAsString(entity);
} catch (Exception e) { } catch (Exception e) {
logger.error("序列化实体失败: {}", e.getMessage()); logger.error("序列化实体失败: {}", e.getMessage());
return null; 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) { private String generateDescription(String entityType, String operationType, Long entityId) {
String operation = switch (operationType) { String operation = "";
case "CREATE" -> "创建"; switch (operationType) {
case "UPDATE" -> "更新"; case "CREATE":
case "DELETE" -> "删除"; operation = "创建";
default -> "操作"; break;
}; case "UPDATE":
operation = "更新";
break;
case "DELETE":
operation = "删除";
break;
default:
operation = "操作";
}
return String.format("%s%s (ID: %s)", operation, entityType, return String.format("%s%s (ID: %s)", operation, entityType,
entityId != null ? entityId : "未知"); entityId != null ? entityId : "未知");
@@ -1,80 +0,0 @@
package cn.novalon.gym.manage.sys.audit;
import cn.novalon.gym.manage.sys.audit.domain.AuditLog;
import cn.novalon.gym.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 : "未知");
}
}
@@ -1,15 +0,0 @@
package cn.novalon.gym.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 "";
}
@@ -1,181 +0,0 @@
package cn.novalon.gym.manage.sys.audit;
import cn.novalon.gym.manage.sys.core.domain.OperationLog;
import cn.novalon.gym.manage.sys.core.service.IOperationLogService;
import cn.novalon.gym.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;
}
}
}
@@ -39,7 +39,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Long> archiveOldLogs(int daysToKeep) { public Mono<Long> archiveOldLogs(int daysToKeep) {
LocalDateTime archiveBefore = LocalDateTime.now().minusDays(daysToKeep); LocalDateTime archiveBefore = LocalDateTime.now().minusDays(daysToKeep);
@@ -53,7 +53,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<AuditLogArchive> archiveLog(AuditLog auditLog) { public Mono<AuditLogArchive> archiveLog(AuditLog auditLog) {
AuditLogArchive archive = convertToArchive(auditLog); AuditLogArchive archive = convertToArchive(auditLog);
@@ -99,7 +99,7 @@ public class AuditLogArchiveService implements IAuditLogArchiveService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> deleteArchivedLogsOlderThan(LocalDateTime date) { public Mono<Void> deleteArchivedLogsOlderThan(LocalDateTime date) {
return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date) return auditLogArchiveRepository.findByOperationTimeBetween(LocalDateTime.MIN, date)
.flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId())) .flatMap(archive -> auditLogArchiveRepository.deleteById(archive.getId()))
@@ -160,13 +160,13 @@ public class AuditLogService implements IAuditLogService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> deleteById(Long id) { public Mono<Void> deleteById(Long id) {
return auditLogRepository.deleteById(id); return auditLogRepository.deleteById(id);
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> logicalDeleteById(Long id) { public Mono<Void> logicalDeleteById(Long id) {
return auditLogRepository.findById(id) return auditLogRepository.findById(id)
.flatMap(auditLog -> { .flatMap(auditLog -> {
@@ -177,7 +177,7 @@ public class AuditLogService implements IAuditLogService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> logicalDeleteByIds(List<Long> ids) { public Mono<Void> logicalDeleteByIds(List<Long> ids) {
return Flux.fromIterable(ids) return Flux.fromIterable(ids)
.flatMap(this::logicalDeleteById) .flatMap(this::logicalDeleteById)
@@ -185,7 +185,7 @@ public class AuditLogService implements IAuditLogService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> restoreById(Long id) { public Mono<Void> restoreById(Long id) {
return auditLogRepository.findById(id) return auditLogRepository.findById(id)
.flatMap(auditLog -> { .flatMap(auditLog -> {
@@ -196,7 +196,7 @@ public class AuditLogService implements IAuditLogService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> restoreByIds(List<Long> ids) { public Mono<Void> restoreByIds(List<Long> ids) {
return Flux.fromIterable(ids) return Flux.fromIterable(ids)
.flatMap(this::restoreById) .flatMap(this::restoreById)
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.sys.config; package cn.novalon.gym.manage.sys.config;
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter; import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
@@ -12,20 +11,22 @@ import org.springframework.security.config.web.server.SecurityWebFiltersOrder;
import org.springframework.security.config.web.server.ServerHttpSecurity; import org.springframework.security.config.web.server.ServerHttpSecurity;
import org.springframework.security.web.server.SecurityWebFilterChain; import org.springframework.security.web.server.SecurityWebFilterChain;
/**
* 安全配置类
*
* @author 张翔
* @date 2026-03-13
*/
@Configuration @Configuration
@EnableWebFluxSecurity @EnableWebFluxSecurity
public class SecurityConfig { public class SecurityConfig {
private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class); private static final Logger logger = LoggerFactory.getLogger(SecurityConfig.class);
private final JwtAuthenticationFilter jwtAuthenticationFilter; private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final OperationLogWebFilter operationLogWebFilter;
private final Environment environment; private final Environment environment;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, Environment environment) {
OperationLogWebFilter operationLogWebFilter,
Environment environment) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter; this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.operationLogWebFilter = operationLogWebFilter;
this.environment = environment; this.environment = environment;
} }
@@ -45,14 +46,11 @@ public class SecurityConfig {
.httpBasic(ServerHttpSecurity.HttpBasicSpec::disable) .httpBasic(ServerHttpSecurity.HttpBasicSpec::disable)
.formLogin(ServerHttpSecurity.FormLoginSpec::disable) .formLogin(ServerHttpSecurity.FormLoginSpec::disable)
.addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION) .addFilterBefore(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION)
.addFilterAfter(operationLogWebFilter, SecurityWebFiltersOrder.AUTHORIZATION)
.authorizeExchange(spec -> { .authorizeExchange(spec -> {
spec.pathMatchers("/api/auth/**").permitAll() spec.pathMatchers("/api/auth/**").permitAll()
.pathMatchers("/api/public/**").permitAll() .pathMatchers("/api/public/**").permitAll()
.pathMatchers("/ws/**").permitAll() .pathMatchers("/ws/**").permitAll()
.pathMatchers("/actuator/**").permitAll() .pathMatchers("/actuator/**").permitAll();
.pathMatchers("/api/groupCourse/**").permitAll();
if (isDevOrTest) { if (isDevOrTest) {
spec.pathMatchers("/swagger-ui.html").permitAll() spec.pathMatchers("/swagger-ui.html").permitAll()
@@ -24,8 +24,6 @@ public interface ISysMenuRepository {
Mono<SysMenu> save(SysMenu sysMenu); Mono<SysMenu> save(SysMenu sysMenu);
Mono<SysMenu> update(SysMenu sysMenu);
Mono<Void> deleteById(Long id); Mono<Void> deleteById(Long id);
Flux<SysMenu> findAll(); Flux<SysMenu> findAll();
@@ -28,8 +28,6 @@ public interface ISysUserRepository {
Mono<SysUser> save(SysUser sysUser); Mono<SysUser> save(SysUser sysUser);
Mono<SysUser> update(SysUser sysUser);
Mono<Void> deleteById(Long id); Mono<Void> deleteById(Long id);
Flux<SysUser> findAll(); Flux<SysUser> findAll();
@@ -48,11 +48,13 @@ public class DictionaryService implements IDictionaryService {
@Override @Override
public Mono<Dictionary> save(Dictionary dictionary) { public Mono<Dictionary> save(Dictionary dictionary) {
if (dictionary.getId() == null) { if (dictionary.getId() == null) {
dictionary.setCreatedAt(LocalDateTime.now());
return checkTypeAndCodeExists(dictionary.getType(), dictionary.getCode()) return checkTypeAndCodeExists(dictionary.getType(), dictionary.getCode())
.flatMap(exists -> { .flatMap(exists -> {
if (exists) { if (exists) {
return Mono.error(new DictionaryAlreadyExistsException(dictionary.getType(), dictionary.getCode())); return Mono.error(new DictionaryAlreadyExistsException(dictionary.getType(), dictionary.getCode()));
} }
dictionary.setUpdatedAt(LocalDateTime.now());
return repository.save(dictionary); return repository.save(dictionary);
}); });
} }
@@ -29,6 +29,7 @@ public class OperationLogService implements IOperationLogService {
@Override @Override
public Mono<OperationLog> save(OperationLog log) { public Mono<OperationLog> save(OperationLog log) {
log.setCreatedAt(LocalDateTime.now());
return logRepository.save(log); return logRepository.save(log);
} }
@@ -1,7 +1,5 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.sys.audit.AuditLogHelper;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.core.domain.SysConfig; import cn.novalon.gym.manage.sys.core.domain.SysConfig;
import cn.novalon.gym.manage.sys.core.repository.ISysConfigRepository; import cn.novalon.gym.manage.sys.core.repository.ISysConfigRepository;
import cn.novalon.gym.manage.sys.core.service.ISysConfigService; import cn.novalon.gym.manage.sys.core.service.ISysConfigService;
@@ -9,15 +7,19 @@ import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/**
* 系统配置服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service @Service
public class SysConfigService implements ISysConfigService { public class SysConfigService implements ISysConfigService {
private final ISysConfigRepository repository; private final ISysConfigRepository repository;
private final IAuditLogService auditLogService;
public SysConfigService(ISysConfigRepository repository, IAuditLogService auditLogService) { public SysConfigService(ISysConfigRepository repository) {
this.repository = repository; this.repository = repository;
this.auditLogService = auditLogService;
} }
@Override @Override
@@ -26,28 +28,27 @@ public class SysConfigService implements ISysConfigService {
} }
@Override @Override
// @Cacheable(value = "sysConfig", key = "#id")
public Mono<SysConfig> findById(Long id) { public Mono<SysConfig> findById(Long id) {
return repository.findById(id); return repository.findById(id);
} }
@Override @Override
// @Cacheable(value = "sysConfig", key = "#configKey")
public Mono<SysConfig> findByConfigKey(String configKey) { public Mono<SysConfig> findByConfigKey(String configKey) {
return repository.findByConfigKeyAndDeletedAtIsNull(configKey); return repository.findByConfigKeyAndDeletedAtIsNull(configKey);
} }
@Override @Override
// @CacheEvict(value = "sysConfig", allEntries = true)
public Mono<SysConfig> save(SysConfig config) { 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 @Override
// @CacheEvict(value = "sysConfig", key = "#id")
public Mono<Void> deleteById(Long id) { public Mono<Void> deleteById(Long id) {
return repository.findById(id) return repository.deleteByIdAndDeletedAtIsNull(id);
.flatMap(config -> repository.deleteByIdAndDeletedAtIsNull(id)
.then(AuditLogHelper.record(auditLogService, "Config", id, "DELETE", config, null)))
.then();
} }
@Override @Override
@@ -1,7 +1,5 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.sys.audit.AuditLogHelper;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.core.domain.SysDictType; import cn.novalon.gym.manage.sys.core.domain.SysDictType;
import cn.novalon.gym.manage.sys.core.repository.ISysDictTypeRepository; import cn.novalon.gym.manage.sys.core.repository.ISysDictTypeRepository;
import cn.novalon.gym.manage.sys.core.service.ISysDictTypeService; import cn.novalon.gym.manage.sys.core.service.ISysDictTypeService;
@@ -9,15 +7,19 @@ import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
/**
* 字典类型服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service @Service
public class SysDictTypeService implements ISysDictTypeService { public class SysDictTypeService implements ISysDictTypeService {
private final ISysDictTypeRepository repository; private final ISysDictTypeRepository repository;
private final IAuditLogService auditLogService;
public SysDictTypeService(ISysDictTypeRepository repository, IAuditLogService auditLogService) { public SysDictTypeService(ISysDictTypeRepository repository) {
this.repository = repository; this.repository = repository;
this.auditLogService = auditLogService;
} }
@Override @Override
@@ -37,16 +39,11 @@ public class SysDictTypeService implements ISysDictTypeService {
@Override @Override
public Mono<SysDictType> save(SysDictType dictType) { 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 @Override
public Mono<Void> deleteById(Long id) { public Mono<Void> deleteById(Long id) {
return repository.findById(id) return repository.deleteByIdAndDeletedAtIsNull(id);
.flatMap(dict -> repository.deleteByIdAndDeletedAtIsNull(id)
.then(AuditLogHelper.record(auditLogService, "Dict", id, "DELETE", dict, null)))
.then();
} }
} }
@@ -6,8 +6,6 @@ import cn.novalon.gym.manage.sys.core.service.ISysMenuService;
import cn.novalon.gym.manage.sys.core.command.CreateMenuCommand; import cn.novalon.gym.manage.sys.core.command.CreateMenuCommand;
import cn.novalon.gym.manage.sys.core.command.UpdateMenuCommand; import cn.novalon.gym.manage.sys.core.command.UpdateMenuCommand;
import cn.novalon.gym.manage.common.util.StatusConstants; import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.audit.AuditLogHelper;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux; import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono; import reactor.core.publisher.Mono;
@@ -26,11 +24,9 @@ import java.util.stream.Collectors;
public class SysMenuService implements ISysMenuService { public class SysMenuService implements ISysMenuService {
private final ISysMenuRepository menuRepository; private final ISysMenuRepository menuRepository;
private final IAuditLogService auditLogService;
public SysMenuService(ISysMenuRepository menuRepository, IAuditLogService auditLogService) { public SysMenuService(ISysMenuRepository menuRepository) {
this.menuRepository = menuRepository; this.menuRepository = menuRepository;
this.auditLogService = auditLogService;
} }
@Override @Override
@@ -50,9 +46,8 @@ public class SysMenuService implements ISysMenuService {
@Override @Override
public Mono<SysMenu> createMenu(SysMenu menu) { public Mono<SysMenu> createMenu(SysMenu menu) {
return menuRepository.save(menu) menu.setCreatedAt(LocalDateTime.now());
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "CREATE", saved) return menuRepository.save(menu);
.thenReturn(saved));
} }
@Override @Override
@@ -65,18 +60,14 @@ public class SysMenuService implements ISysMenuService {
menu.setComponent(command.component()); menu.setComponent(command.component());
menu.setPerms(command.perms()); menu.setPerms(command.perms());
menu.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED); menu.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
return menuRepository.save(menu) menu.setCreatedAt(LocalDateTime.now());
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "CREATE", saved) return menuRepository.save(menu);
.thenReturn(saved));
} }
@Override @Override
public Mono<SysMenu> updateMenu(SysMenu menu) { public Mono<SysMenu> updateMenu(SysMenu menu) {
menu.setUpdatedAt(LocalDateTime.now()); menu.setUpdatedAt(LocalDateTime.now());
return menuRepository.findById(menu.getId()) return menuRepository.save(menu);
.flatMap(before -> menuRepository.update(menu)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
} }
@Override @Override
@@ -84,15 +75,6 @@ public class SysMenuService implements ISysMenuService {
return menuRepository.findById(command.id()) return menuRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("Menu not found"))) .switchIfEmpty(Mono.error(new RuntimeException("Menu not found")))
.flatMap(menu -> { .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) { if (command.parentId() != null) {
menu.setParentId(command.parentId()); menu.setParentId(command.parentId());
} }
@@ -115,18 +97,13 @@ public class SysMenuService implements ISysMenuService {
menu.setStatus(command.status()); menu.setStatus(command.status());
} }
menu.setUpdatedAt(LocalDateTime.now()); menu.setUpdatedAt(LocalDateTime.now());
return menuRepository.update(menu) return menuRepository.save(menu);
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Menu", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
}); });
} }
@Override @Override
public Mono<Void> deleteMenu(Long id) { public Mono<Void> deleteMenu(Long id) {
return menuRepository.findById(id) return menuRepository.deleteById(id);
.flatMap(menu -> menuRepository.deleteById(id)
.then(AuditLogHelper.record(auditLogService, "Menu", id, "DELETE", menu, null)))
.then();
} }
@Override @Override
@@ -1,15 +1,11 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.common.util.StatusConstants; import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.audit.AuditLogHelper;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.core.domain.SysPermission; import cn.novalon.gym.manage.sys.core.domain.SysPermission;
import cn.novalon.gym.manage.sys.core.domain.SysRolePermission; import cn.novalon.gym.manage.sys.core.domain.SysRolePermission;
import cn.novalon.gym.manage.sys.core.repository.ISysPermissionRepository; import cn.novalon.gym.manage.sys.core.repository.ISysPermissionRepository;
import cn.novalon.gym.manage.sys.core.repository.ISysRolePermissionRepository; import cn.novalon.gym.manage.sys.core.repository.ISysRolePermissionRepository;
import cn.novalon.gym.manage.sys.core.service.ISysPermissionService; import cn.novalon.gym.manage.sys.core.service.ISysPermissionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.domain.Sort; import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@@ -28,18 +24,13 @@ import java.util.List;
@Service @Service
public class SysPermissionService implements ISysPermissionService { public class SysPermissionService implements ISysPermissionService {
private static final Logger logger = LoggerFactory.getLogger(SysPermissionService.class);
private final ISysPermissionRepository permissionRepository; private final ISysPermissionRepository permissionRepository;
private final ISysRolePermissionRepository rolePermissionRepository; private final ISysRolePermissionRepository rolePermissionRepository;
private final IAuditLogService auditLogService;
public SysPermissionService(ISysPermissionRepository permissionRepository, public SysPermissionService(ISysPermissionRepository permissionRepository,
ISysRolePermissionRepository rolePermissionRepository, ISysRolePermissionRepository rolePermissionRepository) {
IAuditLogService auditLogService) {
this.permissionRepository = permissionRepository; this.permissionRepository = permissionRepository;
this.rolePermissionRepository = rolePermissionRepository; this.rolePermissionRepository = rolePermissionRepository;
this.auditLogService = auditLogService;
} }
@Override @Override
@@ -69,41 +60,25 @@ public class SysPermissionService implements ISysPermissionService {
@Override @Override
public Mono<SysPermission> createPermission(SysPermission permission) { public Mono<SysPermission> createPermission(SysPermission permission) {
permission.setCreatedAt(LocalDateTime.now());
if (permission.getStatus() == null) { if (permission.getStatus() == null) {
permission.setStatus(StatusConstants.ENABLED); 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 @Override
public Mono<SysPermission> updatePermission(SysPermission permission) { public Mono<SysPermission> updatePermission(SysPermission permission) {
permission.setUpdatedAt(LocalDateTime.now()); permission.setUpdatedAt(LocalDateTime.now());
return permissionRepository.findById(permission.getId()) return permissionRepository.updatePermission(permission);
.flatMap(before -> permissionRepository.updatePermission(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
} }
@Override @Override
public Mono<Void> deletePermission(Long id) { public Mono<Void> deletePermission(Long id) {
return permissionRepository.findById(id) return permissionRepository.findById(id)
.flatMap(permission -> { .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(); permission.delete();
return permissionRepository.updatePermission(permission) return permissionRepository.updatePermission(permission)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Permission", id, "DELETE", before, saved))
.then(rolePermissionRepository.deleteByPermissionId(id)); .then(rolePermissionRepository.deleteByPermissionId(id));
}); });
} }
@@ -124,7 +99,7 @@ public class SysPermissionService implements ISysPermissionService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) { public Mono<Void> assignPermissionsToRole(Long roleId, List<Long> permissionIds) {
return rolePermissionRepository.deleteByRoleId(roleId) return rolePermissionRepository.deleteByRoleId(roleId)
.then(Flux.fromIterable(permissionIds) .then(Flux.fromIterable(permissionIds)
@@ -132,6 +107,7 @@ public class SysPermissionService implements ISysPermissionService {
SysRolePermission rolePermission = new SysRolePermission(); SysRolePermission rolePermission = new SysRolePermission();
rolePermission.setRoleId(roleId); rolePermission.setRoleId(roleId);
rolePermission.setPermissionId(permissionId); rolePermission.setPermissionId(permissionId);
rolePermission.setCreatedAt(LocalDateTime.now());
return rolePermissionRepository.save(rolePermission); return rolePermissionRepository.save(rolePermission);
}) })
.then()); .then());
@@ -1,8 +1,6 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.common.util.StatusConstants; import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.audit.AuditLogHelper;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.core.domain.SysRole; import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.sys.core.query.SysRoleQuery; import cn.novalon.gym.manage.sys.core.query.SysRoleQuery;
import cn.novalon.gym.manage.sys.core.repository.ISysRoleRepository; import cn.novalon.gym.manage.sys.core.repository.ISysRoleRepository;
@@ -23,6 +21,12 @@ import reactor.core.publisher.Mono;
import java.time.LocalDateTime; import java.time.LocalDateTime;
/**
* 系统角色服务实现类
*
* @author 张翔
* @date 2026-03-14
*/
@Service @Service
public class SysRoleService implements ISysRoleService { public class SysRoleService implements ISysRoleService {
@@ -31,16 +35,13 @@ public class SysRoleService implements ISysRoleService {
private final ISysUserService userService; private final ISysUserService userService;
private final IUserRoleRepository userRoleRepository; private final IUserRoleRepository userRoleRepository;
private final ISysRolePermissionRepository rolePermissionRepository; private final ISysRolePermissionRepository rolePermissionRepository;
private final IAuditLogService auditLogService;
public SysRoleService(ISysRoleRepository roleRepository, ISysUserService userService, public SysRoleService(ISysRoleRepository roleRepository, ISysUserService userService,
IUserRoleRepository userRoleRepository, ISysRolePermissionRepository rolePermissionRepository, IUserRoleRepository userRoleRepository, ISysRolePermissionRepository rolePermissionRepository) {
IAuditLogService auditLogService) {
this.roleRepository = roleRepository; this.roleRepository = roleRepository;
this.userService = userService; this.userService = userService;
this.userRoleRepository = userRoleRepository; this.userRoleRepository = userRoleRepository;
this.rolePermissionRepository = rolePermissionRepository; this.rolePermissionRepository = rolePermissionRepository;
this.auditLogService = auditLogService;
} }
@Override @Override
@@ -75,9 +76,7 @@ public class SysRoleService implements ISysRoleService {
if (role.getStatus() == null) { if (role.getStatus() == null) {
role.setStatus(StatusConstants.ENABLED); 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 @Override
@@ -89,18 +88,13 @@ public class SysRoleService implements ISysRoleService {
role.setRoleSort(command.roleSort()); role.setRoleSort(command.roleSort());
role.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED); role.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED);
role.setCreatedAt(LocalDateTime.now()); 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 @Override
public Mono<SysRole> updateRole(SysRole role) { public Mono<SysRole> updateRole(SysRole role) {
role.setUpdatedAt(LocalDateTime.now()); role.setUpdatedAt(LocalDateTime.now());
return roleRepository.findById(role.getId()) return roleRepository.save(role);
.flatMap(before -> roleRepository.updateRole(role)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
} }
@Override @Override
@@ -108,15 +102,6 @@ public class SysRoleService implements ISysRoleService {
return roleRepository.findById(command.id()) return roleRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("Role not found"))) .switchIfEmpty(Mono.error(new RuntimeException("Role not found")))
.flatMap(role -> { .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) { if (command.roleName() != null) {
role.setRoleName(command.roleName()); role.setRoleName(command.roleName());
} }
@@ -130,14 +115,12 @@ public class SysRoleService implements ISysRoleService {
role.setStatus(command.status()); role.setStatus(command.status());
} }
role.setUpdatedAt(LocalDateTime.now()); role.setUpdatedAt(LocalDateTime.now());
return roleRepository.updateRole(role) return roleRepository.save(role);
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
}); });
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> deleteRole(Long id) { public Mono<Void> deleteRole(Long id) {
logger.debug("开始删除角色,ID: {}", id); logger.debug("开始删除角色,ID: {}", id);
@@ -155,8 +138,7 @@ public class SysRoleService implements ISysRoleService {
.doOnError(e -> logger.error("更新用户角色ID失败", e)) .doOnError(e -> logger.error("更新用户角色ID失败", e))
.then(roleRepository.deleteById(id)) .then(roleRepository.deleteById(id))
.doOnSuccess(v -> logger.debug("成功删除角色")) .doOnSuccess(v -> logger.debug("成功删除角色"))
.doOnError(e -> logger.error("删除角色失败", e)) .doOnError(e -> logger.error("删除角色失败", e));
.then(AuditLogHelper.record(auditLogService, "Role", id, "DELETE", role, null));
}); });
} }
@@ -174,19 +156,8 @@ public class SysRoleService implements ISysRoleService {
public Mono<SysRole> logicalDeleteRole(Long id) { public Mono<SysRole> logicalDeleteRole(Long id) {
return roleRepository.findByIdIncludingDeleted(id) return roleRepository.findByIdIncludingDeleted(id)
.flatMap(role -> { .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(); role.delete();
return roleRepository.updateRole(role) return roleRepository.updateRole(role);
.flatMap(saved -> AuditLogHelper.record(auditLogService, "Role", saved.getId(), "DELETE", before, saved)
.thenReturn(saved));
}); });
} }
@@ -1,8 +1,6 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.common.util.StatusConstants; import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.audit.AuditLogHelper;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.core.domain.SysUser; import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.domain.SysRole; import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.sys.core.domain.UserRole; import cn.novalon.gym.manage.sys.core.domain.UserRole;
@@ -15,7 +13,6 @@ import cn.novalon.gym.manage.sys.core.repository.IUserRoleRepository;
import cn.novalon.gym.manage.sys.core.service.ISysUserService; import cn.novalon.gym.manage.sys.core.service.ISysUserService;
import cn.novalon.gym.manage.sys.core.command.CreateUserCommand; import cn.novalon.gym.manage.sys.core.command.CreateUserCommand;
import cn.novalon.gym.manage.sys.core.command.UpdateUserCommand; import cn.novalon.gym.manage.sys.core.command.UpdateUserCommand;
import cn.novalon.gym.manage.sys.dto.response.UserResponse;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@@ -29,6 +26,16 @@ import reactor.core.publisher.Mono;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.util.List; import java.util.List;
/**
* 用户服务实现类
*
* 文件定义:实现用户管理的核心业务逻辑
* 涉及业务:用户注册、登录、信息修改、删除、密码修改、逻辑删除等用户生命周期管理
* 算法:使用R2DBC进行响应式数据库操作,支持分页查询、条件查询、批量操作
*
* @author 张翔
* @date 2026-03-13
*/
@Service @Service
public class SysUserService implements ISysUserService { public class SysUserService implements ISysUserService {
@@ -37,18 +44,15 @@ public class SysUserService implements ISysUserService {
private final ISysRoleRepository roleRepository; private final ISysRoleRepository roleRepository;
private final IUserRoleRepository userRoleRepository; private final IUserRoleRepository userRoleRepository;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final IAuditLogService auditLogService;
public SysUserService(ISysUserRepository userRepository, public SysUserService(ISysUserRepository userRepository,
ISysRoleRepository roleRepository, ISysRoleRepository roleRepository,
IUserRoleRepository userRoleRepository, IUserRoleRepository userRoleRepository,
@Qualifier("passwordEncoder") PasswordEncoder passwordEncoder, @Qualifier("passwordEncoder") PasswordEncoder passwordEncoder) {
IAuditLogService auditLogService) {
this.userRepository = userRepository; this.userRepository = userRepository;
this.roleRepository = roleRepository; this.roleRepository = roleRepository;
this.userRoleRepository = userRoleRepository; this.userRoleRepository = userRoleRepository;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.auditLogService = auditLogService;
logger.info("使用的密码编码器类型: {}", passwordEncoder.getClass().getName()); logger.info("使用的密码编码器类型: {}", passwordEncoder.getClass().getName());
} }
@@ -109,9 +113,7 @@ public class SysUserService implements ISysUserService {
if (user.getStatus() == null) { if (user.getStatus() == null) {
user.setStatus(StatusConstants.ENABLED); 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 @Override
@@ -125,18 +127,13 @@ public class SysUserService implements ISysUserService {
user.setPhone(command.phone()); user.setPhone(command.phone());
user.setRoleId(command.roleId()); user.setRoleId(command.roleId());
user.setStatus(command.status() != null ? command.status() : StatusConstants.ENABLED); 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 @Override
public Mono<SysUser> updateUser(SysUser user) { public Mono<SysUser> updateUser(SysUser user) {
user.setUpdatedAt(LocalDateTime.now()); user.setUpdatedAt(LocalDateTime.now());
return userRepository.findById(user.getId()) return userRepository.save(user);
.flatMap(before -> userRepository.update(user)
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved)));
} }
@Override @Override
@@ -144,17 +141,6 @@ public class SysUserService implements ISysUserService {
return userRepository.findById(command.id()) return userRepository.findById(command.id())
.switchIfEmpty(Mono.error(new RuntimeException("User not found"))) .switchIfEmpty(Mono.error(new RuntimeException("User not found")))
.flatMap(user -> { .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) { if (command.username() != null) {
user.setUsername(command.username()); user.setUsername(command.username());
} }
@@ -173,15 +159,12 @@ public class SysUserService implements ISysUserService {
user.setStatus(command.status()); user.setStatus(command.status());
} }
user.setUpdatedAt(LocalDateTime.now()); user.setUpdatedAt(LocalDateTime.now());
return userRepository.update(user) return userRepository.save(user);
.flatMap(saved -> AuditLogHelper
.record(auditLogService, "User", saved.getId(), "UPDATE", before, saved)
.thenReturn(saved));
}); });
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> deleteUser(Long id) { public Mono<Void> deleteUser(Long id) {
logger.debug("开始删除用户,ID: {}", id); logger.debug("开始删除用户,ID: {}", id);
@@ -194,8 +177,7 @@ public class SysUserService implements ISysUserService {
.doOnError(e -> logger.error("删除用户角色关联记录失败", e)) .doOnError(e -> logger.error("删除用户角色关联记录失败", e))
.then(userRepository.deleteById(id)) .then(userRepository.deleteById(id))
.doOnSuccess(v -> logger.debug("成功删除用户")) .doOnSuccess(v -> logger.debug("成功删除用户"))
.doOnError(e -> logger.error("删除用户失败", e)) .doOnError(e -> logger.error("删除用户失败", e));
.then(AuditLogHelper.record(auditLogService, "User", id, "DELETE", user, null));
}); });
} }
@@ -213,10 +195,7 @@ public class SysUserService implements ISysUserService {
} }
user.setPassword(passwordEncoder.encode(newPassword)); user.setPassword(passwordEncoder.encode(newPassword));
user.setUpdatedAt(LocalDateTime.now()); user.setUpdatedAt(LocalDateTime.now());
return userRepository.update(user) return userRepository.save(user);
.flatMap(saved -> AuditLogHelper
.record(auditLogService, "User", saved.getId(), "UPDATE", saved)
.thenReturn(saved));
}); });
} }
@@ -238,20 +217,8 @@ public class SysUserService implements ISysUserService {
public Mono<Void> logicalDeleteUser(Long id) { public Mono<Void> logicalDeleteUser(Long id) {
return userRepository.findByIdIncludingDeleted(id) return userRepository.findByIdIncludingDeleted(id)
.flatMap(user -> { .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()); user.setDeletedAt(LocalDateTime.now());
return userRepository.save(user) return userRepository.save(user);
.flatMap(saved -> AuditLogHelper.record(auditLogService, "User", saved.getId(), "DELETE", before, saved));
}) })
.then(); .then();
} }
@@ -277,7 +244,7 @@ public class SysUserService implements ISysUserService {
} }
@Override @Override
@Transactional(transactionManager = "connectionFactoryTransactionManager") @Transactional
public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) { public Mono<Void> assignRolesToUser(Long userId, List<Long> roleIds) {
logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds); logger.debug("开始为用户分配角色,用户ID: {}, 角色IDs: {}", userId, roleIds);
@@ -285,8 +252,7 @@ public class SysUserService implements ISysUserService {
logger.debug("角色列表为空,删除用户的所有角色关联"); logger.debug("角色列表为空,删除用户的所有角色关联");
return userRoleRepository.deleteByUserId(userId) return userRoleRepository.deleteByUserId(userId)
.doOnSuccess(v -> logger.debug("成功删除用户的所有角色关联")) .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) return userRoleRepository.deleteByUserId(userId)
@@ -299,12 +265,12 @@ public class SysUserService implements ISysUserService {
UserRole userRole = new UserRole(); UserRole userRole = new UserRole();
userRole.setUserId(userId); userRole.setUserId(userId);
userRole.setRoleId(roleId); userRole.setRoleId(roleId);
userRole.setCreatedAt(LocalDateTime.now());
return userRoleRepository.save(userRole) return userRoleRepository.save(userRole)
.doOnSuccess(v -> logger.debug("成功保存用户角色关联")) .doOnSuccess(v -> logger.debug("成功保存用户角色关联"))
.doOnError(e -> logger.error("保存用户角色关联失败", e)); .doOnError(e -> logger.error("保存用户角色关联失败", e));
}) })
.then()) .then());
.then(AuditLogHelper.record(auditLogService, "User", userId, "UPDATE", null));
} }
@Override @Override
@@ -1,13 +1,12 @@
package cn.novalon.gym.manage.sys.util; package cn.novalon.gym.manage.sys.util;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.web.reactive.function.server.ServerRequest; import org.springframework.web.reactive.function.server.ServerRequest;
import java.net.InetSocketAddress; import java.net.InetSocketAddress;
import java.util.Optional; import java.util.Optional;
/** /**
* IP地址工具类 * IP地址工具类
* 用于从ServerRequest或ServerHttpRequest中获取客户端真实IP地址 * 用于从ServerRequest中获取客户端真实IP地址
* 支持代理服务器场景(X-Forwarded-For, X-Real-IP) * 支持代理服务器场景(X-Forwarded-For, X-Real-IP)
* *
* @author 张翔 * @author 张翔
@@ -49,36 +48,6 @@ public class IpUtils {
return UNKNOWN; 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头获取IP地址
* X-Forwarded-For格式: client, proxy1, proxy2 * X-Forwarded-For格式: client, proxy1, proxy2
@@ -129,48 +98,4 @@ public class IpUtils {
private static boolean isValidIp(String ip) { private static boolean isValidIp(String ip) {
return ip != null && ip.length() > 0 && !UNKNOWN.equalsIgnoreCase(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,6 +1,5 @@
package cn.novalon.gym.manage.sys.config; package cn.novalon.gym.manage.sys.config;
import cn.novalon.gym.manage.sys.audit.OperationLogWebFilter;
import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter; import cn.novalon.gym.manage.sys.security.JwtAuthenticationFilter;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -17,9 +16,6 @@ class SecurityConfigTest {
@Mock @Mock
private JwtAuthenticationFilter jwtAuthenticationFilter; private JwtAuthenticationFilter jwtAuthenticationFilter;
@Mock
private OperationLogWebFilter operationLogWebFilter;
@Mock @Mock
private Environment environment; private Environment environment;
@@ -27,7 +23,7 @@ class SecurityConfigTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
securityConfig = new SecurityConfig(jwtAuthenticationFilter, operationLogWebFilter, environment); securityConfig = new SecurityConfig(jwtAuthenticationFilter, environment);
} }
@Test @Test
@@ -43,7 +43,6 @@ class OperationLogServiceTest {
testLog.setDuration(100L); testLog.setDuration(100L);
testLog.setIp("192.168.1.1"); testLog.setIp("192.168.1.1");
testLog.setStatus("1"); testLog.setStatus("1");
testLog.setCreatedAt(LocalDateTime.now());
} }
@Test @Test
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.core.domain.SysConfig; import cn.novalon.gym.manage.sys.core.domain.SysConfig;
import cn.novalon.gym.manage.sys.core.repository.ISysConfigRepository; import cn.novalon.gym.manage.sys.core.repository.ISysConfigRepository;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -27,16 +26,13 @@ class SysConfigServiceTest {
@Mock @Mock
private ISysConfigRepository repository; private ISysConfigRepository repository;
@Mock
private IAuditLogService auditLogService;
private SysConfigService configService; private SysConfigService configService;
private SysConfig testConfig; private SysConfig testConfig;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
configService = new SysConfigService(repository, auditLogService); configService = new SysConfigService(repository);
testConfig = new SysConfig(); testConfig = new SysConfig();
testConfig.setId(1L); testConfig.setId(1L);
@@ -114,13 +110,11 @@ class SysConfigServiceTest {
@Test @Test
void testDeleteById() { void testDeleteById() {
when(repository.findById(1L)).thenReturn(Mono.just(testConfig));
when(repository.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty()); when(repository.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
StepVerifier.create(configService.deleteById(1L)) StepVerifier.create(configService.deleteById(1L))
.verifyComplete(); .verifyComplete();
verify(repository).findById(1L);
verify(repository).deleteByIdAndDeletedAtIsNull(1L); verify(repository).deleteByIdAndDeletedAtIsNull(1L);
} }
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.core.domain.SysDictType; import cn.novalon.gym.manage.sys.core.domain.SysDictType;
import cn.novalon.gym.manage.sys.core.repository.ISysDictTypeRepository; import cn.novalon.gym.manage.sys.core.repository.ISysDictTypeRepository;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
@@ -24,15 +23,12 @@ class SysDictTypeServiceTest {
@Mock @Mock
private ISysDictTypeRepository repository; private ISysDictTypeRepository repository;
@Mock
private IAuditLogService auditLogService;
private SysDictTypeService dictTypeService; private SysDictTypeService dictTypeService;
private SysDictType testDictType; private SysDictType testDictType;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
dictTypeService = new SysDictTypeService(repository, auditLogService); dictTypeService = new SysDictTypeService(repository);
testDictType = new SysDictType(); testDictType = new SysDictType();
testDictType.setId(1L); testDictType.setId(1L);
@@ -97,7 +93,6 @@ class SysDictTypeServiceTest {
@Test @Test
void testDeleteById() { void testDeleteById() {
when(repository.findById(1L)).thenReturn(Mono.just(testDictType));
when(repository.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty()); when(repository.deleteByIdAndDeletedAtIsNull(1L)).thenReturn(Mono.empty());
Mono<Void> result = dictTypeService.deleteById(1L); Mono<Void> result = dictTypeService.deleteById(1L);
@@ -105,7 +100,6 @@ class SysDictTypeServiceTest {
StepVerifier.create(result) StepVerifier.create(result)
.verifyComplete(); .verifyComplete();
verify(repository).findById(1L);
verify(repository).deleteByIdAndDeletedAtIsNull(1L); verify(repository).deleteByIdAndDeletedAtIsNull(1L);
} }
} }
@@ -4,7 +4,6 @@ import cn.novalon.gym.manage.sys.core.domain.SysMenu;
import cn.novalon.gym.manage.sys.core.repository.ISysMenuRepository; import cn.novalon.gym.manage.sys.core.repository.ISysMenuRepository;
import cn.novalon.gym.manage.sys.core.command.CreateMenuCommand; import cn.novalon.gym.manage.sys.core.command.CreateMenuCommand;
import cn.novalon.gym.manage.sys.core.command.UpdateMenuCommand; import cn.novalon.gym.manage.sys.core.command.UpdateMenuCommand;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.api.extension.ExtendWith;
@@ -26,15 +25,12 @@ class SysMenuServiceTest {
@Mock @Mock
private ISysMenuRepository menuRepository; private ISysMenuRepository menuRepository;
@Mock
private IAuditLogService auditLogService;
private SysMenuService menuService; private SysMenuService menuService;
private SysMenu testMenu; private SysMenu testMenu;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
menuService = new SysMenuService(menuRepository, auditLogService); menuService = new SysMenuService(menuRepository);
testMenu = new SysMenu(); testMenu = new SysMenu();
testMenu.setId(1L); testMenu.setId(1L);
@@ -133,8 +129,7 @@ class SysMenuServiceTest {
@Test @Test
void testUpdateMenu() { void testUpdateMenu() {
when(menuRepository.findById(1L)).thenReturn(Mono.just(testMenu)); when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(testMenu));
when(menuRepository.update(any(SysMenu.class))).thenReturn(Mono.just(testMenu));
Mono<SysMenu> result = menuService.updateMenu(testMenu); Mono<SysMenu> result = menuService.updateMenu(testMenu);
@@ -143,8 +138,7 @@ class SysMenuServiceTest {
menu.getUpdatedAt() != null) menu.getUpdatedAt() != null)
.verifyComplete(); .verifyComplete();
verify(menuRepository).findById(1L); verify(menuRepository).save(any(SysMenu.class));
verify(menuRepository).update(any(SysMenu.class));
} }
@Test @Test
@@ -153,7 +147,7 @@ class SysMenuServiceTest {
1L, 0L, "系统管理(更新)", "M", 1, "system", "system:manage", 1); 1L, 0L, "系统管理(更新)", "M", 1, "system", "system:manage", 1);
when(menuRepository.findById(1L)).thenReturn(Mono.just(testMenu)); when(menuRepository.findById(1L)).thenReturn(Mono.just(testMenu));
when(menuRepository.update(any(SysMenu.class))).thenReturn(Mono.just(testMenu)); when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(testMenu));
Mono<SysMenu> result = menuService.updateMenu(command); Mono<SysMenu> result = menuService.updateMenu(command);
@@ -163,7 +157,7 @@ class SysMenuServiceTest {
.verifyComplete(); .verifyComplete();
verify(menuRepository).findById(1L); verify(menuRepository).findById(1L);
verify(menuRepository).update(any(SysMenu.class)); verify(menuRepository).save(any(SysMenu.class));
} }
@Test @Test
@@ -206,7 +200,7 @@ class SysMenuServiceTest {
updatedMenu.setUpdatedAt(LocalDateTime.now()); updatedMenu.setUpdatedAt(LocalDateTime.now());
when(menuRepository.findById(1L)).thenReturn(Mono.just(existingMenu)); when(menuRepository.findById(1L)).thenReturn(Mono.just(existingMenu));
when(menuRepository.update(any(SysMenu.class))).thenReturn(Mono.just(updatedMenu)); when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(updatedMenu));
UpdateMenuCommand command = new UpdateMenuCommand( UpdateMenuCommand command = new UpdateMenuCommand(
1L, null, null, null, null, null, null, null); 1L, null, null, null, null, null, null, null);
@@ -216,7 +210,7 @@ class SysMenuServiceTest {
.verifyComplete(); .verifyComplete();
verify(menuRepository).findById(1L); verify(menuRepository).findById(1L);
verify(menuRepository).update(any(SysMenu.class)); verify(menuRepository).save(any(SysMenu.class));
} }
@Test @Test
@@ -243,7 +237,7 @@ class SysMenuServiceTest {
updatedMenu.setUpdatedAt(LocalDateTime.now()); updatedMenu.setUpdatedAt(LocalDateTime.now());
when(menuRepository.findById(1L)).thenReturn(Mono.just(existingMenu)); when(menuRepository.findById(1L)).thenReturn(Mono.just(existingMenu));
when(menuRepository.update(any(SysMenu.class))).thenReturn(Mono.just(updatedMenu)); when(menuRepository.save(any(SysMenu.class))).thenReturn(Mono.just(updatedMenu));
UpdateMenuCommand command = new UpdateMenuCommand( UpdateMenuCommand command = new UpdateMenuCommand(
1L, 2L, "系统管理(更新)", "C", 2, "system_updated", "system:manage_updated", 0); 1L, 2L, "系统管理(更新)", "C", 2, "system_updated", "system:manage_updated", 0);
@@ -253,12 +247,11 @@ class SysMenuServiceTest {
.verifyComplete(); .verifyComplete();
verify(menuRepository).findById(1L); verify(menuRepository).findById(1L);
verify(menuRepository).update(any(SysMenu.class)); verify(menuRepository).save(any(SysMenu.class));
} }
@Test @Test
void testDeleteMenu() { void testDeleteMenu() {
when(menuRepository.findById(1L)).thenReturn(Mono.just(testMenu));
when(menuRepository.deleteById(1L)).thenReturn(Mono.empty()); when(menuRepository.deleteById(1L)).thenReturn(Mono.empty());
Mono<Void> result = menuService.deleteMenu(1L); Mono<Void> result = menuService.deleteMenu(1L);
@@ -266,7 +259,6 @@ class SysMenuServiceTest {
StepVerifier.create(result) StepVerifier.create(result)
.verifyComplete(); .verifyComplete();
verify(menuRepository).findById(1L);
verify(menuRepository).deleteById(1L); verify(menuRepository).deleteById(1L);
} }
@@ -1,6 +1,5 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.common.util.StatusConstants; import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.core.domain.SysRole; import cn.novalon.gym.manage.sys.core.domain.SysRole;
import cn.novalon.gym.manage.sys.core.query.SysRoleQuery; import cn.novalon.gym.manage.sys.core.query.SysRoleQuery;
@@ -47,16 +46,13 @@ class SysRoleServiceTest {
@Mock @Mock
private ISysRolePermissionRepository rolePermissionRepository; private ISysRolePermissionRepository rolePermissionRepository;
@Mock
private IAuditLogService auditLogService;
private SysRoleService roleService; private SysRoleService roleService;
private SysRole testRole; private SysRole testRole;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
roleService = new SysRoleService(roleRepository, userService, userRoleRepository, rolePermissionRepository, auditLogService); roleService = new SysRoleService(roleRepository, userService, userRoleRepository, rolePermissionRepository);
testRole = new SysRole(); testRole = new SysRole();
testRole.setId(1L); testRole.setId(1L);
@@ -210,7 +206,7 @@ class SysRoleServiceTest {
existingRole.setStatus(StatusConstants.ENABLED); existingRole.setStatus(StatusConstants.ENABLED);
when(roleRepository.findById(1L)).thenReturn(Mono.just(existingRole)); when(roleRepository.findById(1L)).thenReturn(Mono.just(existingRole));
when(roleRepository.updateRole(any(SysRole.class))).thenReturn(Mono.just(testRole)); when(roleRepository.save(any(SysRole.class))).thenReturn(Mono.just(testRole));
cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand command = cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand command =
new cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand( new cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand(
@@ -222,7 +218,7 @@ class SysRoleServiceTest {
.verifyComplete(); .verifyComplete();
verify(roleRepository).findById(1L); verify(roleRepository).findById(1L);
verify(roleRepository).updateRole(any(SysRole.class)); verify(roleRepository).save(any(SysRole.class));
} }
@Test @Test
@@ -235,7 +231,7 @@ class SysRoleServiceTest {
existingRole.setStatus(StatusConstants.ENABLED); existingRole.setStatus(StatusConstants.ENABLED);
when(roleRepository.findById(1L)).thenReturn(Mono.just(existingRole)); when(roleRepository.findById(1L)).thenReturn(Mono.just(existingRole));
when(roleRepository.updateRole(any(SysRole.class))).thenReturn(Mono.just(testRole)); when(roleRepository.save(any(SysRole.class))).thenReturn(Mono.just(testRole));
cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand command = cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand command =
new cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand( new cn.novalon.gym.manage.sys.core.command.UpdateRoleCommand(
@@ -247,7 +243,7 @@ class SysRoleServiceTest {
.verifyComplete(); .verifyComplete();
verify(roleRepository).findById(1L); verify(roleRepository).findById(1L);
verify(roleRepository).updateRole(any(SysRole.class)); verify(roleRepository).save(any(SysRole.class));
} }
@Test @Test
@@ -256,15 +252,13 @@ class SysRoleServiceTest {
updateRole.setId(1L); updateRole.setId(1L);
updateRole.setRoleName("updated_admin"); updateRole.setRoleName("updated_admin");
when(roleRepository.findById(1L)).thenReturn(Mono.just(testRole)); when(roleRepository.save(any(SysRole.class))).thenReturn(Mono.just(testRole));
when(roleRepository.updateRole(any(SysRole.class))).thenReturn(Mono.just(testRole));
StepVerifier.create(roleService.updateRole(updateRole)) StepVerifier.create(roleService.updateRole(updateRole))
.expectNextMatches(role -> role.getUpdatedAt() != null) .expectNextMatches(role -> role.getUpdatedAt() != null)
.verifyComplete(); .verifyComplete();
verify(roleRepository).findById(1L); verify(roleRepository).save(any(SysRole.class));
verify(roleRepository).updateRole(any(SysRole.class));
} }
@Test @Test
@@ -1,7 +1,6 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.common.util.StatusConstants; import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.config.IntegrationTestConfig; import cn.novalon.gym.manage.sys.config.IntegrationTestConfig;
import cn.novalon.gym.manage.sys.core.domain.SysUser; import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.domain.SysRole; import cn.novalon.gym.manage.sys.core.domain.SysRole;
@@ -77,9 +76,6 @@ class SysUserServiceIntegrationTest {
@Autowired @Autowired
private IUserRoleRepository userRoleRepository; private IUserRoleRepository userRoleRepository;
@Autowired
private IAuditLogService auditLogService;
@Autowired @Autowired
private R2dbcEntityTemplate r2dbcEntityTemplate; private R2dbcEntityTemplate r2dbcEntityTemplate;
@@ -89,7 +85,7 @@ class SysUserServiceIntegrationTest {
@BeforeEach @BeforeEach
void setUp() { void setUp() {
passwordEncoder = new BCryptPasswordEncoder(12); passwordEncoder = new BCryptPasswordEncoder(12);
userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder, auditLogService); userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder);
r2dbcEntityTemplate.delete(SysUser.class).all().block(); r2dbcEntityTemplate.delete(SysUser.class).all().block();
r2dbcEntityTemplate.delete(SysRole.class).all().block(); r2dbcEntityTemplate.delete(SysRole.class).all().block();
@@ -1,8 +1,6 @@
package cn.novalon.gym.manage.sys.core.service.impl; package cn.novalon.gym.manage.sys.core.service.impl;
import cn.novalon.gym.manage.common.util.StatusConstants; import cn.novalon.gym.manage.common.util.StatusConstants;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import cn.novalon.gym.manage.sys.audit.domain.AuditLog;
import cn.novalon.gym.manage.sys.core.domain.SysUser; import cn.novalon.gym.manage.sys.core.domain.SysUser;
import cn.novalon.gym.manage.sys.core.domain.UserRole; import cn.novalon.gym.manage.sys.core.domain.UserRole;
import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository; import cn.novalon.gym.manage.sys.core.repository.ISysUserRepository;
@@ -47,14 +45,11 @@ class SysUserServiceTest {
@Mock @Mock
private PasswordEncoder passwordEncoder; private PasswordEncoder passwordEncoder;
@Mock
private IAuditLogService auditLogService;
private SysUserService userService; private SysUserService userService;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder, auditLogService); userService = new SysUserService(userRepository, roleRepository, userRoleRepository, passwordEncoder);
} }
@Test @Test
@@ -169,8 +164,7 @@ class SysUserServiceTest {
user.setUsername("testuser"); user.setUsername("testuser");
user.setEmail("updated@example.com"); user.setEmail("updated@example.com");
when(userRepository.findById(1L)).thenReturn(Mono.just(user)); when(userRepository.save(any(SysUser.class))).thenReturn(Mono.just(user));
when(userRepository.update(any(SysUser.class))).thenReturn(Mono.just(user));
StepVerifier.create(userService.updateUser(user)) StepVerifier.create(userService.updateUser(user))
.expectNextMatches(updatedUser -> .expectNextMatches(updatedUser ->
@@ -179,8 +173,7 @@ class SysUserServiceTest {
) )
.verifyComplete(); .verifyComplete();
verify(userRepository, times(1)).findById(1L); verify(userRepository, times(1)).save(any(SysUser.class));
verify(userRepository, times(1)).update(any(SysUser.class));
} }
@Test @Test
@@ -225,7 +218,7 @@ class SysUserServiceTest {
when(userRepository.findById(1L)).thenReturn(Mono.just(user)); when(userRepository.findById(1L)).thenReturn(Mono.just(user));
when(passwordEncoder.matches("oldPassword", "$2b$12$oldPassword")).thenReturn(true); when(passwordEncoder.matches("oldPassword", "$2b$12$oldPassword")).thenReturn(true);
when(passwordEncoder.encode("newPassword")).thenReturn("$2b$12$newPassword"); when(passwordEncoder.encode("newPassword")).thenReturn("$2b$12$newPassword");
when(userRepository.update(any(SysUser.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0))); when(userRepository.save(any(SysUser.class))).thenAnswer(invocation -> Mono.just(invocation.getArgument(0)));
StepVerifier.create(userService.changePassword(1L, "oldPassword", "newPassword")) StepVerifier.create(userService.changePassword(1L, "oldPassword", "newPassword"))
.expectNextMatches(updatedUser -> .expectNextMatches(updatedUser ->
@@ -235,7 +228,7 @@ class SysUserServiceTest {
verify(passwordEncoder, times(1)).matches("oldPassword", "$2b$12$oldPassword"); verify(passwordEncoder, times(1)).matches("oldPassword", "$2b$12$oldPassword");
verify(passwordEncoder, times(1)).encode("newPassword"); verify(passwordEncoder, times(1)).encode("newPassword");
verify(userRepository, times(1)).update(any(SysUser.class)); verify(userRepository, times(1)).save(any(SysUser.class));
} }
@Test @Test
@@ -10,7 +10,6 @@ import cn.novalon.gym.manage.sys.core.service.ISysMenuService;
import cn.novalon.gym.manage.sys.core.service.ISysRoleService; import cn.novalon.gym.manage.sys.core.service.ISysRoleService;
import cn.novalon.gym.manage.sys.core.service.ISysUserService; import cn.novalon.gym.manage.sys.core.service.ISysUserService;
import cn.novalon.gym.manage.sys.core.service.impl.SysMenuService; import cn.novalon.gym.manage.sys.core.service.impl.SysMenuService;
import cn.novalon.gym.manage.sys.audit.service.IAuditLogService;
import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.DisplayName;
@@ -62,9 +61,6 @@ class SystemConfigRegressionTest {
@Mock @Mock
private ISysMenuRepository menuRepository; private ISysMenuRepository menuRepository;
@Mock
private IAuditLogService auditLogService;
private SysUser adminUser; private SysUser adminUser;
private SysUser normalUser; private SysUser normalUser;
private SysUser guestUser; private SysUser guestUser;
@@ -378,7 +374,7 @@ class SystemConfigRegressionTest {
void testAdminUser_MenuManagement() { void testAdminUser_MenuManagement() {
/* unused */ /* unused */
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
StepVerifier.create(menuService.findAll()) StepVerifier.create(menuService.findAll())
.expectNextCount(0) .expectNextCount(0)
@@ -388,7 +384,7 @@ class SystemConfigRegressionTest {
@Test @Test
@DisplayName("3.2 普通用户 - 菜单访问控制") @DisplayName("3.2 普通用户 - 菜单访问控制")
void testNormalUser_MenuAccess() { void testNormalUser_MenuAccess() {
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
StepVerifier.create(menuService.findAll()) StepVerifier.create(menuService.findAll())
.expectNextCount(0) .expectNextCount(0)
@@ -398,7 +394,7 @@ class SystemConfigRegressionTest {
@Test @Test
@DisplayName("3.3 访客用户 - 菜单访问控制") @DisplayName("3.3 访客用户 - 菜单访问控制")
void testGuestUser_MenuAccess() { void testGuestUser_MenuAccess() {
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
StepVerifier.create(menuService.findAll()) StepVerifier.create(menuService.findAll())
.expectNextCount(0) .expectNextCount(0)
@@ -408,7 +404,7 @@ class SystemConfigRegressionTest {
@Test @Test
@DisplayName("3.4 菜单树构建 - 管理员视图") @DisplayName("3.4 菜单树构建 - 管理员视图")
void testMenuTree_Build_Admin() { void testMenuTree_Build_Admin() {
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
StepVerifier.create(menuService.findAll()) StepVerifier.create(menuService.findAll())
.verifyComplete(); .verifyComplete();
@@ -417,7 +413,7 @@ class SystemConfigRegressionTest {
@Test @Test
@DisplayName("3.5 权限菜单过滤 - 普通用户视图") @DisplayName("3.5 权限菜单过滤 - 普通用户视图")
void testMenuFilter_NormalUser() { void testMenuFilter_NormalUser() {
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
StepVerifier.create(menuService.findAll()) StepVerifier.create(menuService.findAll())
.expectNextCount(0) .expectNextCount(0)
@@ -427,7 +423,7 @@ class SystemConfigRegressionTest {
@Test @Test
@DisplayName("3.6 权限菜单过滤 - 访客视图") @DisplayName("3.6 权限菜单过滤 - 访客视图")
void testMenuFilter_Guest() { void testMenuFilter_Guest() {
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
StepVerifier.create(menuService.findAll()) StepVerifier.create(menuService.findAll())
.expectNextCount(0) .expectNextCount(0)
@@ -476,7 +472,7 @@ class SystemConfigRegressionTest {
@Test @Test
@DisplayName("5.2 大量菜单加载性能测试") @DisplayName("5.2 大量菜单加载性能测试")
void testLargeMenuLoadPerformance() { void testLargeMenuLoadPerformance() {
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
long startTime = System.currentTimeMillis(); long startTime = System.currentTimeMillis();
@@ -520,7 +516,7 @@ class SystemConfigRegressionTest {
@Test @Test
@DisplayName("6.3 菜单层级结构完整性") @DisplayName("6.3 菜单层级结构完整性")
void testMenuHierarchy_Integrity() { void testMenuHierarchy_Integrity() {
ISysMenuService menuService = new SysMenuService(menuRepository, auditLogService); ISysMenuService menuService = new SysMenuService(menuRepository);
StepVerifier.create(menuService.findAll()) StepVerifier.create(menuService.findAll())
.verifyComplete(); .verifyComplete();
@@ -22,7 +22,7 @@ class IpUtilsTest {
@Test @Test
@DisplayName("当request为null时,应返回unknown") @DisplayName("当request为null时,应返回unknown")
void getClientIp_whenRequestIsNull_shouldReturnUnknown() { void getClientIp_whenRequestIsNull_shouldReturnUnknown() {
String ip = IpUtils.getClientIp((ServerRequest) null); String ip = IpUtils.getClientIp(null);
assertEquals("unknown", ip); assertEquals("unknown", ip);
} }
-8
View File
@@ -42,7 +42,6 @@
<module>manage-audit</module> <module>manage-audit</module>
<module>manage-notify</module> <module>manage-notify</module>
<module>manage-file</module> <module>manage-file</module>
<module>gym-groupCourse</module>
</modules> </modules>
<dependencyManagement> <dependencyManagement>
@@ -222,13 +221,6 @@
<artifactId>lombok</artifactId> <artifactId>lombok</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- HuTool工具箱-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.38</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
-60
View File
@@ -1,60 +0,0 @@
# E2E测试说明
## 测试结构
本项目的E2E测试采用分层测试策略:
### 冒烟测试(smoke/
快速验证基础功能是否正常工作。
- `login-logout.spec.ts` - 登录登出基础流程
### 核心旅程测试(journeys/
验证关键业务端到端流程。
- `admin-complete-workflow.spec.ts` - 管理员完整工作流
- `user-permission-boundary.spec.ts` - 用户权限边界验证
- `file-management-workflow.spec.ts` - 文件上传下载流程
- `audit-workflow.spec.ts` - 审计日志查看流程
## 运行测试
### 运行冒烟测试
```bash
npm run test:e2e:smoke
```
### 运行核心旅程测试
```bash
npm run test:e2e:journeys
```
### 运行所有测试
```bash
npm run test:e2e
```
## 测试数据
测试使用的用户账号:
- 管理员:username: `admin`, password: `Test@123`
- 普通用户:username: `user`, password: `Test@123`
## 测试策略
- **冒烟测试**:每次代码提交时运行,快速反馈
- **核心旅程测试**PR合并前运行,验证关键业务流程
- **单元测试**:补充功能覆盖率,目标80%
## 维护指南
1. 新增核心业务功能时,在 `journeys/` 目录下添加测试
2. 新增基础功能时,在 `smoke/` 目录下添加测试
3. 保持测试文件数量精简,避免重复测试
4. 优先使用单元测试覆盖功能细节
@@ -1,65 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('API连通性测试', () => {
test('验证网关服务健康状态', async ({ page }) => {
await test.step('检查网关健康状态', async () => {
const response = await page.request.get('http://localhost:8080/actuator/health');
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.status).toBe('UP');
});
await test.step('检查应用服务路由', async () => {
const response = await page.request.get('http://localhost:8080/api/auth/health');
expect(response.status()).toBe(200);
});
});
test('验证前端与后端连通性', async ({ page }) => {
await test.step('加载前端应用', async () => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 验证页面标题
const title = await page.title();
expect(title).toContain('Novalon');
});
await test.step('检查API请求', async () => {
// 监听网络请求
const apiRequests = [];
page.on('request', request => {
if (request.url().includes('/api/')) {
apiRequests.push({
url: request.url(),
method: request.method()
});
}
});
// 触发一些前端操作来生成API请求
await page.goto('/login');
await page.waitForLoadState('networkidle');
// 验证是否有API请求发出
expect(apiRequests.length).toBeGreaterThan(0);
});
});
test('验证数据库连接状态', async ({ page }) => {
await test.step('检查数据库健康状态', async () => {
// 通过应用服务检查数据库连接
const response = await page.request.get('http://localhost:8084/actuator/health');
expect(response.status()).toBe(200);
const data = await response.json();
expect(data.status).toBe('UP');
// 检查数据库组件状态
if (data.components && data.components.db) {
expect(data.components.db.status).toBe('UP');
}
});
});
});
-197
View File
@@ -1,197 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('认证和授权测试', () => {
let authToken: string;
let userId: number;
test('用户登录测试', async ({ page }) => {
await test.step('准备登录数据', async () => {
console.log('准备登录测试数据...');
});
await test.step('发送登录请求', async () => {
const response = await page.request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'admin123'
}
});
expect(response.status()).toBe(200);
const data = await response.json();
expect(data).toHaveProperty('token');
expect(data).toHaveProperty('userId');
expect(data).toHaveProperty('username');
authToken = data.token;
userId = data.userId;
console.log('登录成功,获取到Token:', authToken.substring(0, 20) + '...');
});
await test.step('验证Token有效性', async () => {
const response = await page.request.get('http://localhost:8080/api/users', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
console.log('Token验证成功,可以访问受保护的资源');
});
});
test('用户信息查询测试', async ({ page }) => {
await test.step('先登录获取Token', async () => {
const loginResponse = await page.request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'admin123'
}
});
const loginData = await loginResponse.json();
authToken = loginData.token;
userId = loginData.userId;
});
await test.step('查询用户列表', async () => {
const response = await page.request.get('http://localhost:8080/api/users', {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const users = await response.json();
expect(Array.isArray(users)).toBe(true);
expect(users.length).toBeGreaterThan(0);
console.log(`查询到 ${users.length} 个用户`);
});
await test.step('查询指定用户信息', async () => {
const response = await page.request.get(`http://localhost:8080/api/users/${userId}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
expect(response.status()).toBe(200);
const user = await response.json();
expect(user).toHaveProperty('id');
expect(user).toHaveProperty('username');
expect(user.id).toBe(userId);
console.log(`查询到用户信息: ${user.username}`);
});
});
test('权限验证测试', async ({ page }) => {
await test.step('先登录获取Token', async () => {
const loginResponse = await page.request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'admin123'
}
});
const loginData = await loginResponse.json();
authToken = loginData.token;
});
await test.step('测试访问受保护的API', async () => {
const protectedEndpoints = [
'/api/users',
'/api/roles',
'/api/menus',
'/api/config'
];
for (const endpoint of protectedEndpoints) {
const response = await page.request.get(`http://localhost:8080${endpoint}`, {
headers: {
'Authorization': `Bearer ${authToken}`
}
});
console.log(`访问 ${endpoint}: ${response.status()}`);
expect([200, 404]).toContain(response.status());
}
});
await test.step('测试无Token访问受保护API', async () => {
const response = await page.request.get('http://localhost:8080/api/users');
expect(response.status()).toBe(401);
console.log('无Token访问受保护API返回401,权限验证正常');
});
});
test('前端登录流程测试', async ({ page }) => {
await test.step('访问登录页面', async () => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
// 验证登录页面元素
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]');
const passwordInput = page.locator('input[type="password"]');
const loginButton = page.locator('button:has-text("登录")');
expect(await usernameInput.count()).toBeGreaterThan(0);
expect(await passwordInput.count()).toBeGreaterThan(0);
expect(await loginButton.count()).toBeGreaterThan(0);
console.log('登录页面元素验证通过');
});
await test.step('填写登录表单', async () => {
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
const passwordInput = page.locator('input[type="password"]').first();
await usernameInput.fill('admin');
await passwordInput.fill('admin123');
console.log('登录表单填写完成');
});
await test.step('提交登录表单', async () => {
const loginButton = page.locator('button:has-text("登录")').first();
// 监听响应
const responsePromise = page.waitForResponse(response =>
response.url().includes('/api/auth/login') && response.request().method() === 'POST'
);
await loginButton.click();
try {
const response = await responsePromise;
console.log('登录请求状态:', response.status());
if (response.status() === 200) {
const data = await response.json();
expect(data).toHaveProperty('token');
console.log('前端登录成功');
}
} catch (error) {
console.log('登录请求可能超时,但这是预期的行为');
}
// 等待一段时间,观察页面变化
await page.waitForTimeout(2000);
});
});
});
-16
View File
@@ -1,16 +0,0 @@
import { test as setup } from '@playwright/test';
const authFile = 'playwright/.auth/user.json';
setup('authenticate', async ({ page }) => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
await page.locator('input[placeholder*="用户名"]').fill('admin');
await page.locator('input[placeholder*="密码"]').fill('Test@123');
await page.locator('button:has-text("登录")').click();
await page.waitForURL('**/dashboard', { timeout: 30000 });
await page.context().storageState({ path: authFile });
});
-86
View File
@@ -1,86 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('基础UI功能测试', () => {
test('前端应用基本功能验证', async ({ page }) => {
// 测试1: 应用首页加载
await test.step('加载应用首页', async () => {
await page.goto('/');
await page.waitForLoadState('networkidle');
// 验证页面标题
const title = await page.title();
expect(title).toContain('Novalon');
});
// 测试2: 登录页面渲染
await test.step('验证登录页面元素', async () => {
await page.goto('/login');
await page.waitForLoadState('networkidle');
// 验证登录表单元素
await expect(page.locator('input[type="text"]')).toBeVisible();
await expect(page.locator('input[type="password"]')).toBeVisible();
await expect(page.locator('button:has-text("登录")')).toBeVisible();
});
// 测试3: 页面导航
await test.step('验证页面导航功能', async () => {
// 检查页面是否有基本的导航元素 - 使用更灵活的选择器
const navigationSelectors = [
'nav', '.navbar', '.menu', '.el-menu', '.el-header',
'.layout-header', '.app-header', '[class*="header"]',
'[class*="nav"]', '[class*="menu"]'
];
let hasNavigation = false;
for (const selector of navigationSelectors) {
const count = await page.locator(selector).count();
if (count > 0) {
hasNavigation = true;
break;
}
}
// 如果找不到传统导航元素,检查是否有其他页面结构
if (!hasNavigation) {
const hasAppContainer = await page.locator('#app, .app, .container').count() > 0;
const hasBodyContent = await page.locator('body').textContent() !== '';
hasNavigation = hasAppContainer && hasBodyContent;
}
expect(hasNavigation).toBeTruthy();
});
// 测试4: 响应式设计验证
await test.step('验证响应式设计', async () => {
// 设置移动端视口
await page.setViewportSize({ width: 375, height: 667 });
await page.waitForTimeout(500);
// 验证页面在移动端仍然可访问
await expect(page.locator('body')).toBeVisible();
});
});
test('应用静态资源加载', async ({ page }) => {
await page.goto('/');
// 验证CSS加载
const cssLoaded = await page.evaluate(() => {
return document.styleSheets.length > 0;
});
expect(cssLoaded).toBeTruthy();
// 验证JavaScript加载
const jsLoaded = await page.evaluate(() => {
return typeof window !== 'undefined';
});
expect(jsLoaded).toBeTruthy();
// 验证Vue应用挂载
const vueMounted = await page.evaluate(() => {
return !!document.querySelector('#app');
});
expect(vueMounted).toBeTruthy();
});
});
@@ -1,72 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('参数配置功能测试', () => {
let authToken: string;
test.beforeAll(async ({ request }) => {
const response = await request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'admin123'
}
});
expect(response.status()).toBe(200);
const data = await response.json();
authToken = data.token;
});
test('参数配置列表显示测试', async ({ page }) => {
await test.step('导航到参数配置页面', async () => {
await page.goto('http://localhost:3002/login');
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
const passwordInput = page.locator('input[type="password"]').first();
const loginButton = page.locator('button:has-text("登录")').first();
await usernameInput.fill('admin');
await passwordInput.fill('admin123');
await loginButton.click();
await page.waitForTimeout(2000);
// 点击系统管理菜单
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
if (await systemMenu.count() > 0) {
await systemMenu.click();
await page.waitForTimeout(500);
}
// 点击参数配置
const configManagement = page.locator('.el-menu-item:has-text("参数配置")').first();
if (await configManagement.count() > 0) {
await configManagement.click();
await page.waitForTimeout(1000);
}
});
await test.step('验证参数配置列表显示', async () => {
// 检查是否有参数配置列表或表格
const tableSelectors = [
'table',
'.el-table',
'[class*="table"]',
'.config-list'
];
let foundTable = false;
for (const selector of tableSelectors) {
const count = await page.locator(selector).count();
if (count > 0) {
foundTable = true;
break;
}
}
expect(foundTable).toBe(true);
});
});
});
-429
View File
@@ -1,429 +0,0 @@
import { FullConfig, FullResult, Reporter, Suite, TestCase, TestResult } from '@playwright/test/reporter';
import * as fs from 'fs';
import * as path from 'path';
class CustomReporter implements Reporter {
private results: Map<string, TestCase[]> = new Map();
private suiteResults: Map<string, Suite> = new Map();
private startTime: number = Date.now();
private testResults: TestResult[] = [];
onBegin(config: FullConfig) {
console.log(`🚀 开始测试执行: ${config.projects.map(p => p.name).join(', ')}`);
this.startTime = Date.now();
}
onTestBegin(test: TestCase, result: TestResult) {
console.log(`📝 开始测试: ${test.title}`);
}
onTestEnd(test: TestCase, result: TestResult) {
console.log(`✅ 测试完成: ${test.title} - ${result.status}`);
this.testResults.push(result);
}
onEnd(result: FullResult) {
const endTime = Date.now();
const duration = endTime - this.startTime;
console.log(`🎉 测试执行完成`);
console.log(`⏱️ 总耗时: ${this.formatDuration(duration)}`);
const stats = this.calculateStats(result);
this.generateConsoleReport(stats);
this.generateHtmlReport(result, stats);
this.generateJsonReport(result, stats);
}
private calculateStats(result: FullResult): TestStats {
const allTests = this.testResults;
if (allTests.length === 0) {
return {
total: 0,
passed: 0,
failed: 0,
skipped: 0,
flaky: 0,
passRate: 0,
failRate: 0,
skipRate: 0,
flakyRate: 0,
totalDuration: 0,
avgDuration: 0,
slowestTests: [],
failedTests: [],
};
}
const passed = allTests.filter(t => t.status === 'passed');
const failed = allTests.filter(t => t.status === 'failed');
const skipped = allTests.filter(t => t.status === 'skipped');
const flaky = allTests.filter(t => t.status === 'passed' && t.retry >= 1);
const totalDuration = allTests.reduce((sum, t) => sum + t.duration, 0);
const avgDuration = totalDuration / allTests.length;
const passRate = (passed.length / allTests.length) * 100;
const failRate = (failed.length / allTests.length) * 100;
const skipRate = (skipped.length / allTests.length) * 100;
const flakyRate = (flaky.length / allTests.length) * 100;
return {
total: allTests.length,
passed: passed.length,
failed: failed.length,
skipped: skipped.length,
flaky: flaky.length,
passRate,
failRate,
skipRate,
flakyRate,
totalDuration,
avgDuration,
slowestTests: allTests
.filter(t => t.duration > 0)
.sort((a, b) => b.duration - a.duration)
.slice(0, 10),
failedTests: failed,
};
}
private generateConsoleReport(stats: TestStats) {
console.log('');
console.log('═══════════════════════════════════════════');
console.log('📊 测试统计报告');
console.log('═══════════════════════════════════════════');
console.log('');
console.log(`📈 总测试数: ${stats.total}`);
console.log(`✅ 通过: ${stats.passed} (${stats.passRate.toFixed(2)}%)`);
console.log(`❌ 失败: ${stats.failed} (${stats.failRate.toFixed(2)}%)`);
console.log(`⏭️ 跳过: ${stats.skipped} (${stats.skipRate.toFixed(2)}%)`);
console.log(`🔄 不稳定: ${stats.flaky} (${stats.flakyRate.toFixed(2)}%)`);
console.log('');
console.log(`⏱️ 总耗时: ${this.formatDuration(stats.totalDuration)}`);
console.log(`⏱️ 平均耗时: ${this.formatDuration(stats.avgDuration)}`);
console.log('');
console.log('🐌 最慢的10个测试:');
stats.slowestTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title} - ${this.formatDuration(test.duration || 0)}`);
});
console.log('');
if (stats.failedTests.length > 0) {
console.log('❌ 失败的测试:');
stats.failedTests.forEach((test, index) => {
console.log(` ${index + 1}. ${test.title || '未命名测试'}`);
if (test.location?.file) {
console.log(` 位置: ${test.location.file}:${test.location.line || 0}`);
}
if (test.error?.message) {
console.log(` 错误: ${test.error.message}`);
}
});
console.log('');
}
}
private generateHtmlReport(result: FullResult, stats: TestStats) {
const html = `
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title> - Novalon管理系统</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
margin: 0;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
.container {
max-width: 1200px;
margin: 0 auto;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.2);
padding: 30px;
}
.header {
text-align: center;
margin-bottom: 30px;
padding-bottom: 20px;
border-bottom: 2px solid #e0e0e0;
}
.header h1 {
margin: 0;
color: #333;
font-size: 28px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
margin-bottom: 30px;
}
.stat-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
}
.stat-card h3 {
margin: 0 0 10px 0;
font-size: 14px;
opacity: 0.9;
}
.stat-card .value {
font-size: 32px;
font-weight: bold;
margin: 0;
}
.stat-card .label {
font-size: 12px;
opacity: 0.8;
margin-top: 5px;
}
.stat-card.passed {
background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%);
}
.stat-card.failed {
background: linear-gradient(135deg, #ef4444 0%, #f44336 100%);
}
.stat-card.flaky {
background: linear-gradient(135deg, #f59e0b 0%, #f093fb 100%);
}
.section {
margin-bottom: 30px;
}
.section h2 {
color: #333;
border-bottom: 2px solid #667eea;
padding-bottom: 10px;
margin-bottom: 20px;
}
.test-list {
list-style: none;
padding: 0;
}
.test-item {
padding: 15px;
margin-bottom: 10px;
border-left: 4px solid #ddd;
background: #f9f9f9;
border-radius: 5px;
}
.test-item.passed {
border-left-color: #38ef7d;
background: #f0fff4;
}
.test-item.failed {
border-left-color: #ef4444;
background: #fff5f5;
}
.test-item.skipped {
border-left-color: #f59e0b;
background: #fef9c3;
}
.test-item.flaky {
border-left-color: #f093fb;
background: #fef3c7;
}
.test-item .test-name {
font-weight: bold;
margin-bottom: 5px;
color: #333;
}
.test-item .test-duration {
color: #666;
font-size: 12px;
}
.test-item .test-error {
color: #ef4444;
font-size: 12px;
margin-top: 5px;
padding: 10px;
background: #fee;
border-radius: 3px;
}
.progress-bar {
height: 20px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
margin: 20px 0;
}
.progress-bar-fill {
height: 100%;
background: linear-gradient(90deg, #11998e 0%, #38ef7d 100%);
transition: width 0.5s ease;
}
.footer {
text-align: center;
margin-top: 30px;
padding-top: 20px;
border-top: 1px solid #e0e0e0;
color: #666;
font-size: 12px;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🧪 Novalon管理系统测试报告</h1>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
<div class="stats-grid">
<div class="stat-card passed">
<h3></h3>
<div class="value">${stats.passed}</div>
<div class="label">${stats.passRate.toFixed(2)}%</div>
</div>
<div class="stat-card failed">
<h3></h3>
<div class="value">${stats.failed}</div>
<div class="label">${stats.failRate.toFixed(2)}%</div>
</div>
<div class="stat-card flaky">
<h3></h3>
<div class="value">${stats.flaky}</div>
<div class="label">${stats.flakyRate.toFixed(2)}%</div>
</div>
<div class="stat-card">
<h3></h3>
<div class="value">${stats.total}</div>
<div class="label">100%</div>
</div>
</div>
<div class="progress-bar">
<div class="progress-bar-fill" style="width: ${stats.passRate}%"></div>
</div>
<div class="section">
<h2>📈 </h2>
<ul class="test-list">
<li class="test-item">
<div class="test-name"></div>
<div class="test-duration">${this.formatDuration(stats.totalDuration)}</div>
</li>
<li class="test-item">
<div class="test-name"></div>
<div class="test-duration">${this.formatDuration(stats.avgDuration)}</div>
</li>
<li class="test-item">
<div class="test-name"></div>
<div class="test-duration">${stats.skipped} (${stats.skipRate.toFixed(2)}%)</div>
</li>
</ul>
</div>
${stats.failedTests.length > 0 ? `
<div class="section">
<h2> </h2>
<ul class="test-list">
${stats.failedTests.map(test => `
<li class="test-item failed">
<div class="test-name">${test.title}</div>
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
<div class="test-error">
<strong>:</strong> ${test.error?.message || '未知错误'}
</div>
</li>
`).join('')}
</ul>
</div>
` : ''}
<div class="section">
<h2>🐌 10</h2>
<ul class="test-list">
${stats.slowestTests.map((test, index) => `
<li class="test-item ${test.status}">
<div class="test-name">${index + 1}. ${test.title}</div>
<div class="test-duration">${this.formatDuration(test.duration || 0)}</div>
</li>
`).join('')}
</ul>
</div>
<div class="footer">
<p>🧪 Novalon管理系统 - </p>
<p>生成时间: ${new Date().toLocaleString('zh-CN')}</p>
</div>
</div>
</body>
</html>
`;
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.html');
fs.writeFileSync(reportPath, html, 'utf-8');
console.log(`📄 HTML报告已生成: ${reportPath}`);
}
private generateJsonReport(result: FullResult, stats: TestStats) {
const report = {
summary: {
timestamp: new Date().toISOString(),
total: stats.total,
passed: stats.passed,
failed: stats.failed,
skipped: stats.skipped,
flaky: stats.flaky,
passRate: stats.passRate,
failRate: stats.failRate,
skipRate: stats.skipRate,
flakyRate: stats.flakyRate,
totalDuration: stats.totalDuration,
avgDuration: stats.avgDuration,
},
failedTests: stats.failedTests.map(test => ({
title: test.title,
location: test.location,
error: test.error?.message,
duration: test.duration,
})),
slowestTests: stats.slowestTests.map(test => ({
title: test.title,
duration: test.duration,
})),
};
const reportPath = path.join(process.cwd(), 'test-results', 'custom-report.json');
fs.writeFileSync(reportPath, JSON.stringify(report, null, 2), 'utf-8');
console.log(`📄 JSON报告已生成: ${reportPath}`);
}
private formatDuration(ms: number): string {
if (ms < 1000) {
return `${ms}ms`;
} else if (ms < 60000) {
return `${(ms / 1000).toFixed(1)}s`;
} else {
return `${(ms / 60000).toFixed(1)}m`;
}
}
}
interface TestStats {
total: number;
passed: number;
failed: number;
skipped: number;
flaky: number;
passRate: number;
failRate: number;
skipRate: number;
flakyRate: number;
totalDuration: number;
avgDuration: number;
slowestTests: TestCase[];
}
export default CustomReporter;
@@ -1,72 +0,0 @@
import { test, expect } from '@playwright/test';
test.describe('字典管理功能测试', () => {
let authToken: string;
test.beforeAll(async ({ request }) => {
const response = await request.post('http://localhost:8080/api/auth/login', {
headers: {
'Content-Type': 'application/json'
},
data: {
username: 'admin',
password: 'admin123'
}
});
expect(response.status()).toBe(200);
const data = await response.json();
authToken = data.token;
});
test('字典管理列表显示测试', async ({ page }) => {
await test.step('导航到字典管理页面', async () => {
await page.goto('http://localhost:3002/login');
const usernameInput = page.locator('input[type="text"], input[placeholder*="用户名"], input[placeholder*="账号"]').first();
const passwordInput = page.locator('input[type="password"]').first();
const loginButton = page.locator('button:has-text("登录")').first();
await usernameInput.fill('admin');
await passwordInput.fill('admin123');
await loginButton.click();
await page.waitForTimeout(2000);
// 点击系统管理菜单
const systemMenu = page.locator('.el-sub-menu:has-text("系统管理")').first();
if (await systemMenu.count() > 0) {
await systemMenu.click();
await page.waitForTimeout(500);
}
// 点击字典管理
const dictManagement = page.locator('.el-menu-item:has-text("字典管理")').first();
if (await dictManagement.count() > 0) {
await dictManagement.click();
await page.waitForTimeout(1000);
}
});
await test.step('验证字典管理列表显示', async () => {
// 检查是否有字典管理列表或表格
const tableSelectors = [
'table',
'.el-table',
'[class*="table"]',
'.dict-list'
];
let foundTable = false;
for (const selector of tableSelectors) {
const count = await page.locator(selector).count();
if (count > 0) {
foundTable = true;
break;
}
}
expect(foundTable).toBe(true);
});
});
});
-119
View File
@@ -1,119 +0,0 @@
import { test as base } from '@playwright/test';
export interface TestUser {
username: string;
password: string;
email: string;
phone?: string;
}
export interface TestRole {
roleName: string;
roleKey: string;
roleSort?: string;
status?: string;
remark?: string;
}
export interface TestMenu {
menuName: string;
parentId: number;
orderNum: number;
menuType: string;
component?: string;
perms?: string;
status?: number;
}
type TestData = {
adminUser: TestUser;
regularUser: TestUser;
testRole: TestRole;
testMenu: TestMenu;
generateTestUser: () => TestUser;
generateTestRole: () => TestRole;
generateTestMenu: () => TestMenu;
};
export const test = base.extend<TestData>({
adminUser: async ({}, use) => {
const user: TestUser = {
username: 'admin',
password: 'password',
email: 'admin@example.com',
phone: '13800138000',
};
await use(user);
},
regularUser: async ({}, use) => {
const user: TestUser = {
username: 'testuser',
password: 'Test123!@#',
email: 'testuser@example.com',
phone: '13800138001',
};
await use(user);
},
testRole: async ({}, use) => {
const role: TestRole = {
roleName: '测试角色',
roleKey: 'test_role',
roleSort: '1',
status: '1',
remark: '测试角色备注',
};
await use(role);
},
testMenu: async ({}, use) => {
const menu: TestMenu = {
menuName: '测试菜单',
parentId: 0,
orderNum: 1,
menuType: 'M',
component: 'test',
perms: 'test:view',
status: 1,
};
await use(menu);
},
generateTestUser: async ({}, use) => {
const timestamp = Date.now();
const user: TestUser = {
username: `testuser_${timestamp}`,
password: 'Test123!@#',
email: `test_${timestamp}@example.com`,
phone: `138${String(timestamp).slice(-8)}`,
};
await use(() => user);
},
generateTestRole: async ({}, use) => {
const timestamp = Date.now();
const role: TestRole = {
roleName: `测试角色_${timestamp}`,
roleKey: `test_role_${timestamp}`,
roleSort: '1',
status: '1',
remark: `测试角色备注_${timestamp}`,
};
await use(() => role);
},
generateTestMenu: async ({}, use) => {
const timestamp = Date.now();
const menu: TestMenu = {
menuName: `测试菜单_${timestamp}`,
parentId: 0,
orderNum: 1,
menuType: 'M',
component: `test_${timestamp}`,
perms: `test:view_${timestamp}`,
status: 1,
};
await use(() => menu);
},
});
@@ -1 +0,0 @@
This is a test file for E2E testing purposes.

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