新增后台管理系统(答辩用

This commit is contained in:
2026-06-24 05:07:32 +08:00
parent 2251f31524
commit 1f209e6761
98 changed files with 25099 additions and 0 deletions
+189
View File
@@ -0,0 +1,189 @@
# 认证管理模块 API 文档
> **文档版本**: v1.0
> **创建日期**: 2026-06-16
> **作者**: 张翔
> **状态**: 正式发布
---
## 目录
1. [概述](#概述)
2. [基础路径](#基础路径)
3. [认证接口](#认证接口)
- [用户名+密码登录](#用户名密码登录)
- [获取用户信息](#获取用户信息)
4. [数据模型](#数据模型)
- [LoginRequest](#loginrequest)
- [LoginResponse](#loginresponse)
- [UserInfo](#userinfo)
5. [响应码说明](#响应码说明)
---
## 概述
认证管理模块提供用户登录认证和用户信息查询功能。采用 Spring WebFlux 响应式编程,支持高并发场景。
## 基础路径
所有接口的基础路径为: `http://{host}:{port}/api/auth`
---
## 认证接口
### 用户名+密码登录
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/auth/login` |
| **所属文件** | `AuthHandler.java` |
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| username | string | 是 | - | 用户名 |
| password | string | 是 | - | 密码 |
**成功响应** (200 OK):
```json
{
"code": 200,
"message": "登录成功",
"data": {
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"refreshToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"tokenType": "Bearer",
"expiresIn": 7200,
"userInfo": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"phone": "13800138000",
"nickname": "超级管理员",
"status": 1,
"roleId": 1
}
}
}
```
**失败响应** (400 Bad Request):
```json
{
"code": 400,
"message": "用户名或密码错误"
}
```
---
### 获取用户信息
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/auth/users/{id}` |
| **所属文件** | `AuthHandler.java` |
**路径参数**:
| 参数名 | 类型 | 必填 | 说明 |
|--------|------|------|------|
| id | Long | 是 | 用户ID |
**成功响应** (200 OK):
```json
{
"code": 200,
"message": "获取用户信息成功",
"data": {
"id": 1,
"username": "admin",
"email": "admin@example.com",
"phone": "13800138000",
"nickname": "超级管理员",
"status": 1,
"roleId": 1
}
}
```
**失败响应** (400 Bad Request):
```json
{
"code": 400,
"message": "无效的用户ID"
}
```
---
## 数据模型
### LoginRequest
用户登录请求对象。
| 字段 | 类型 | 必填 | 说明 | 示例 |
|------|------|------|------|------|
| username | string | 是 | 用户名 | admin |
| password | string | 是 | 密码 | Test@123 |
### LoginResponse
用户登录响应对象。
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| accessToken | string | 访问令牌 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
| refreshToken | string | 刷新令牌 | eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... |
| tokenType | string | 令牌类型 | Bearer |
| expiresIn | Long | 过期时间(秒) | 7200 |
| userInfo | UserInfo | 用户信息 | - |
### UserInfo
用户信息对象。
| 字段 | 类型 | 说明 | 示例 |
|------|------|------|------|
| id | Long | 用户ID | 1 |
| username | string | 用户名 | admin |
| email | string | 邮箱 | admin@example.com |
| phone | string | 手机号 | 13800138000 |
| nickname | string | 昵称 | 超级管理员 |
| status | Integer | 状态:0-禁用,1-正常 | 1 |
| roleId | Long | 角色ID | 1 |
---
## 响应码说明
| 响应码 | 说明 |
|--------|------|
| 200 | 成功 |
| 400 | 请求参数错误(如用户名密码错误、无效的用户ID) |
| 401 | 未授权(认证失败) |
| 403 | 禁止访问 |
| 404 | 资源未找到 |
| 409 | 冲突(如重复数据) |
| 500 | 服务器内部错误 |
---
## 业务规则
1. 登录成功后返回的 `accessToken` 用于后续接口的身份认证
2. `accessToken` 有效期为 2 小时(7200 秒)
3. `refreshToken` 用于刷新 `accessToken`,有效期为 7 天
4. 用户状态 `status` 为 0 时表示禁用,无法登录
5. 所有需要认证的接口需要在请求头中携带 `Authorization: Bearer {accessToken}`
+343
View File
@@ -0,0 +1,343 @@
# 员工管理模块 API 文档
> **文档版本**: v1.0
> **创建日期**: 2026-06-20
> **作者**: AI Assistant
> **状态**: 正式发布
---
## 目录
1. [概述](#概述)
2. [基础路径](#基础路径)
3. [员工管理接口](#员工管理接口)
- [分页获取员工列表(含角色信息)](#分页获取员工列表含角色信息)
4. [用户管理接口(复用)](#用户管理接口复用)
- [创建员工账号](#创建员工账号)
- [获取员工详情](#获取员工详情)
- [更新员工信息](#更新员工信息)
- [逻辑删除员工](#逻辑删除员工)
- [修改密码](#修改密码)
- [为用户分配角色](#为用户分配角色)
- [获取用户的角色](#获取用户的角色)
5. [角色管理接口(复用)](#角色管理接口复用)
- [获取所有角色](#获取所有角色)
6. [数据模型](#数据模型)
- [UserRegisterRequest](#userregisterrequest)
- [EmployeePageResponse](#employeepageresponse)
- [RoleInfo](#roleinfo)
---
## 概述
员工管理模块为店长(超级管理员 admin)提供新员工账号创建和权限分配功能。该模块复用已有的用户管理、角色管理接口,并新增了带角色信息的员工分页查询接口。
## 基础路径
所有接口的基础路径为: `http://{host}:{port}/api`
---
## 员工管理接口
### 分页获取员工列表(含角色信息)
> **新增接口**:该接口在已有 `/api/users/page` 基础上,额外返回每个员工的角色详细信息。
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/employees/page` |
| **所属文件** | `SysUserHandler.java` |
**请求参数**:
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| page | int | 否 | 0 | 页码(从0开始) |
| size | int | 否 | 10 | 每页数量 |
| sort | string | 否 | id | 排序字段 |
| order | string | 否 | asc | 排序方向(asc/desc |
| keyword | string | 否 | - | 搜索关键字(匹配用户名/昵称) |
**成功响应** (200 OK):
```json
{
"content": [
{
"id": 1,
"username": "admin",
"nickname": "超级管理员",
"email": "admin@novalon.com",
"phone": "13800138000",
"avatar": null,
"status": 1,
"roles": [
{
"id": 1,
"roleName": "超级管理员",
"roleKey": "admin",
"roleSort": 1
}
],
"createdAt": "2026-03-13T10:00:00",
"updatedAt": "2026-03-13T10:00:00"
}
],
"totalPages": 1,
"totalElements": 1,
"currentPage": 0,
"pageSize": 10,
"first": true,
"last": true
}
```
---
## 用户管理接口(复用)
以下接口为 `SysUserHandler` 中已有的用户管理接口,可直接用于员工管理。
### 创建员工账号
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/users` |
| **所属文件** | `SysUserHandler.java` |
**请求体**:
```json
{
"username": "newstaff",
"password": "Staff@123",
"nickname": "新员工",
"email": "staff@novalon.com",
"phone": "13900139001",
"roles": [2, 3]
}
```
**成功响应** (201 Created):
```json
{
"id": 11,
"username": "newstaff",
"nickname": "新员工",
"email": "staff@novalon.com",
"phone": "13900139001",
"status": 1,
"createdAt": "2026-06-20T10:00:00",
"updatedAt": "2026-06-20T10:00:00"
}
```
**校验规则**:
- `username`: 3-50位,只能包含字母、数字、下划线和横线
- `password`: 8-20位,必须包含大小写字母和数字
- `email`: 合法邮箱格式
- `phone`: 中国大陆手机号格式(1[3-9]开头的11位数字)
### 获取员工详情
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/users/{id}` |
| **所属文件** | `SysUserHandler.java` |
**成功响应** (200 OK):
```json
{
"id": 1,
"username": "admin",
"nickname": "超级管理员",
"email": "admin@novalon.com",
"phone": "13800138000",
"avatar": null,
"status": 1,
"roles": [1],
"createdAt": "2026-03-13T10:00:00",
"updatedAt": "2026-03-13T10:00:00"
}
```
### 更新员工信息
| 属性 | 值 |
|------|-----|
| **HTTP方法** | PUT |
| **接口路径** | `/api/users/{id}` |
| **所属文件** | `SysUserHandler.java` |
**请求体**:
```json
{
"email": "newemail@novalon.com",
"roleId": 2,
"status": 1
}
```
### 逻辑删除员工
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/users/{id}/action/logical-delete` |
| **所属文件** | `SysUserHandler.java` |
### 修改密码
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/users/{id}/action/change-password` |
| **所属文件** | `SysUserHandler.java` |
**请求体**:
```json
{
"oldPassword": "Old@123",
"newPassword": "New@456"
}
```
### 为用户分配角色
| 属性 | 值 |
|------|-----|
| **HTTP方法** | POST |
| **接口路径** | `/api/users/{id}/roles` |
| **所属文件** | `SysUserHandler.java` |
**请求体**:
```json
{
"roleIds": ["2", "3"]
}
```
### 获取用户的角色
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/users/{id}/roles` |
| **所属文件** | `SysUserHandler.java` |
---
## 角色管理接口(复用)
### 获取所有角色
| 属性 | 值 |
|------|-----|
| **HTTP方法** | GET |
| **接口路径** | `/api/roles` |
| **所属文件** | `SysRoleHandler.java` |
**成功响应** (200 OK):
```json
[
{
"id": 1,
"roleName": "超级管理员",
"roleKey": "admin",
"roleSort": 1,
"status": 1,
"createdAt": "2026-03-13T10:00:00",
"updatedAt": "2026-03-13T10:00:00"
},
{
"id": 2,
"roleName": "测试管理员",
"roleKey": "test_admin",
"roleSort": 2,
"status": 1,
"createdAt": "2026-03-13T10:00:00",
"updatedAt": "2026-03-13T10:00:00"
},
{
"id": 3,
"roleName": "普通用户",
"roleKey": "normal_user",
"roleSort": 3,
"status": 1,
"createdAt": "2026-03-13T10:00:00",
"updatedAt": "2026-03-13T10:00:00"
},
{
"id": 4,
"roleName": "访客",
"roleKey": "guest",
"roleSort": 4,
"status": 1,
"createdAt": "2026-03-13T10:00:00",
"updatedAt": "2026-03-13T10:00:00"
}
]
```
---
## 数据模型
### UserRegisterRequest
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| username | String | 是 | 用户名(3-50位字母数字下划线横线) |
| password | String | 是 | 密码(8-20位含大小写字母和数字) |
| nickname | String | 否 | 昵称 |
| email | String | 是 | 邮箱 |
| phone | String | 是 | 手机号(中国大陆11位) |
| roles | List\<Long\> | 否 | 角色ID列表 |
### EmployeePageResponse
| 字段 | 类型 | 说明 |
|------|------|------|
| content | List\<EmployeeInfo\> | 员工列表 |
| totalPages | int | 总页数 |
| totalElements | long | 总记录数 |
| currentPage | int | 当前页码 |
| pageSize | int | 每页数量 |
| first | boolean | 是否第一页 |
| last | boolean | 是否最后一页 |
### EmployeeInfo
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 用户ID |
| username | String | 用户名 |
| nickname | String | 昵称 |
| email | String | 邮箱 |
| phone | String | 手机号 |
| avatar | String | 头像URL |
| status | Integer | 状态:0-禁用, 1-正常 |
| roles | List\<RoleInfo\> | 角色列表 |
| createdAt | String | 创建时间 |
| updatedAt | String | 更新时间 |
### RoleInfo
| 字段 | 类型 | 说明 |
|------|------|------|
| id | Long | 角色ID |
| roleName | String | 角色名称 |
| roleKey | String | 角色标识 |
| roleSort | Integer | 排序号 |
+51
View File
@@ -0,0 +1,51 @@
# soybean-admin-mock
## Docs
- [🦊一分钟,了解 Apifox ](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/doc-2825188.md):
## API Docs
- Auth [用户名+密码登录](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-100526985.md):
- Auth [获取用户信息](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-120399825.md):
- Auth [刷新token](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-120415125.md):
- Auth [自定义后端错误](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-158477619.md):
- 前端路由 [获取用户路由数据](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-120415303.md):
- 前端路由 [路由是否存在](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-120415373.md): 当用户访问一个路由失败时,有可能是没有该路由的权限,或者没有该路由,所以需要判断路由是否存在
- 前端路由 [获取固定的路由数据(不需要权限)](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-158516240.md):
- 前端路由 [获取react用户路由](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-273820808.md):
- 调试 [debug](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-125438392.md):
- 调试 [debug post](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-134021864.md):
- 系统管理 [系统管理 - 获取角色列表](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-145344387.md):
- 系统管理 [系统管理 - 获取用户列表](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-149754769.md):
- 系统管理 [系统管理 - 获取用户列表(废弃)](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-145387463.md):
- 系统管理 [获取所有角色](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-145409928.md):
- 系统管理 [获取菜单列表](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-145421249.md):
- 系统管理 [系统管理 - 获取菜单列表](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-157945109.md):
- 系统管理 [获取所有页面组件](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-157988353.md):
- 系统管理 [获取菜单树](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-158414289.md):
- 项目配置 [获取用户配置](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-229492806.md):
- 项目配置 [保存用户配置](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-229493074.md):
- 用户 [postApiUsersLogin](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003433.md): 用户登录
- 用户 [postApiUsersRegister](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003434.md): 用户注册
- 用户 [getApiUsersProfile](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003435.md): 获取用户个人信息
- 用户 [putApiUsersPassword](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003436.md): 更新用户密码
- 用户 [postApiUsersLogout](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003437.md): 用户登出
- 角色 [getApiRoles](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003438.md): 获取所有角色
- 角色 [postApiRoles](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003439.md): 创建角色
- 角色 [getApiRolesById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003440.md): 获取角色详情
- 角色 [putApiRolesById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003441.md): 更新角色
- 角色 [deleteApiRolesById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003442.md): 删除角色
- 权限 [getApiPermissions](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003443.md): 获取所有权限
- 权限 [postApiPermissions](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003444.md): 创建权限
- 权限 [getApiPermissionsById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003445.md): 获取权限详情
- 权限 [putApiPermissionsById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003446.md): 更新权限
- 权限 [deleteApiPermissionsById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003447.md): 删除权限
- 菜单 [getApiMenusUser](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003448.md): 获取用户菜单
- 菜单 [getApiMenus](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003449.md): 获取所有菜单
- 菜单 [postApiMenus](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003450.md): 创建菜单
- 菜单 [getApiMenusById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003451.md): 获取菜单详情
- 菜单 [putApiMenusById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003452.md): 更新菜单
- 菜单 [deleteApiMenusById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003453.md): 删除菜单
- 文件 [postApiFilesUpload](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003454.md): 上传文件
- 文件 [getApiFilesById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003455.md): 获取文件信息
- 文件 [deleteApiFilesById](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003456.md): 删除文件
- 文件 [getApiFilesUser](https://s.apifox.cn/35c8727a-d3ab-47e9-8863-ef8e37df6887/api-281003457.md): 获取用户文件列表
@@ -0,0 +1,44 @@
package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 会员每日签到明细
*
* @author system
* @date 2026-06-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberDailySignIn {
/**
* 会员ID
*/
private Long memberId;
/**
* 签到时间
*/
private String signInTime;
/**
* 签到状态:SUCCESS / FAILED
*/
private String signInStatus;
/**
* 签到方式:QR_CODE / MANUAL / FACE
*/
private String signInType;
/**
* 失败原因(仅失败时)
*/
private String failReason;
}
@@ -0,0 +1,49 @@
package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 会员月度签到统计
*
* @author system
* @date 2026-06-17
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberMonthlySignIn {
/**
* 会员ID
*/
private Long memberId;
/**
* 会员姓名
*/
private String memberName;
/**
* 统计月份(格式:yyyy-MM)
*/
private String month;
/**
* 当月签到总次数
*/
private Long totalSignIns;
/**
* 当月成功签到次数
*/
private Long successSignIns;
/**
* 当月失败签到次数
*/
private Long failedSignIns;
}
@@ -0,0 +1,54 @@
package cn.novalon.gym.manage.datacount.domain;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 会员签到明细
*
* @author system
* @date 2026-06-16
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MemberSignInDetail {
/**
* 会员ID
*/
private Long memberId;
/**
* 会员姓名
*/
private String memberName;
/**
* 会员手机号
*/
private String memberPhone;
/**
* 签到总次数
*/
private Long totalSignIns;
/**
* 成功签到次数
*/
private Long successSignIns;
/**
* 失败签到次数
*/
private Long failedSignIns;
/**
* 最近签到时间
*/
private String lastSignInTime;
}
@@ -0,0 +1,67 @@
package cn.novalon.gym.manage.groupcourse.scheduler;
import cn.novalon.gym.manage.groupcourse.service.IGroupCourseService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 团课超时与自动结课定时任务
*
* 功能:
* 1. 检查已超时未开课的课程,标记为超时并返还用户权益
* 2. 检查已超时未结课的课程,自动结课并发送教练警告
*
* @author 张翔
* @date 2026-06-23
*/
@Component
public class GroupCourseTimeoutScheduler {
private static final Logger logger = LoggerFactory.getLogger(GroupCourseTimeoutScheduler.class);
private final IGroupCourseService groupCourseService;
public GroupCourseTimeoutScheduler(IGroupCourseService groupCourseService) {
this.groupCourseService = groupCourseService;
}
/**
* 每分钟检查一次超时未开课的课程
* 超时条件:status='0' 且 actual_start_time IS NULL 且 start_time + 10分钟 < 当前时间
*/
@Scheduled(fixedRate = 60000)
public void checkAndHandleTimeoutCourses() {
logger.debug("定时任务开始检查超时未开课的团课");
groupCourseService.processTimeoutCourses()
.subscribe(
count -> {
if (count > 0) {
logger.warn("定时任务完成,处理了 {} 个超时团课,已返还用户权益", count);
}
},
error -> logger.error("超时团课处理定时任务执行失败:{}", error.getMessage(), error)
);
}
/**
* 每分钟检查一次需要自动结课的课程
* 自动结课条件:status='3'(进行中) 且 actual_end_time IS NULL 且 end_time + 10分钟 < 当前时间
*/
@Scheduled(fixedRate = 60000)
public void checkAndHandleAutoEndCourses() {
logger.debug("定时任务开始检查需要自动结课的团课");
groupCourseService.processAutoEndCourses()
.subscribe(
count -> {
if (count > 0) {
logger.warn("定时任务完成,自动结课 {} 个团课,请通知相关教练", count);
}
},
error -> logger.error("自动结课定时任务执行失败:{}", error.getMessage(), error)
);
}
}
@@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
+114
View File
@@ -0,0 +1,114 @@
<?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-manage-cuit</artifactId>
<version>1.0.0</version>
<name>gym-manage-cuit</name>
<description>Auth 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-sys</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>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JWT -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,63 @@
package cn.novalon.gym.manage.auth.converter;
import cn.novalon.gym.manage.auth.domain.User;
import cn.novalon.gym.manage.auth.dto.UserInfo;
import cn.novalon.gym.manage.auth.entity.UserEntity;
public class UserConverter {
public static User toDomain(UserEntity entity) {
if (entity == null) {
return null;
}
User user = new User();
user.setId(entity.getId());
user.setUsername(entity.getUsername());
user.setEmail(entity.getEmail());
user.setPhone(entity.getPhone());
user.setNickname(entity.getNickname());
user.setStatus(entity.getStatus());
user.setRoleId(entity.getRoleId());
user.setCreateBy(entity.getCreateBy());
user.setUpdateBy(entity.getUpdateBy());
user.setCreatedAt(entity.getCreatedAt());
user.setUpdatedAt(entity.getUpdatedAt());
user.setDeletedAt(entity.getDeletedAt());
return user;
}
public static UserEntity toEntity(User domain) {
if (domain == null) {
return null;
}
UserEntity entity = new UserEntity();
entity.setId(domain.getId());
entity.setUsername(domain.getUsername());
entity.setEmail(domain.getEmail());
entity.setPhone(domain.getPhone());
entity.setNickname(domain.getNickname());
entity.setStatus(domain.getStatus());
entity.setRoleId(domain.getRoleId());
entity.setCreateBy(domain.getCreateBy());
entity.setUpdateBy(domain.getUpdateBy());
entity.setCreatedAt(domain.getCreatedAt());
entity.setUpdatedAt(domain.getUpdatedAt());
entity.setDeletedAt(domain.getDeletedAt());
return entity;
}
public static UserInfo toUserInfo(User user) {
if (user == null) {
return null;
}
UserInfo userInfo = new UserInfo();
userInfo.setId(user.getId());
userInfo.setUsername(user.getUsername());
userInfo.setEmail(user.getEmail());
userInfo.setPhone(user.getPhone());
userInfo.setNickname(user.getNickname());
userInfo.setStatus(user.getStatus());
userInfo.setRoleId(user.getRoleId());
return userInfo;
}
}
@@ -0,0 +1,17 @@
package cn.novalon.gym.manage.auth.dao;
import cn.novalon.gym.manage.auth.entity.UserEntity;
import org.springframework.data.r2dbc.repository.Query;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import reactor.core.publisher.Mono;
@Repository
public interface UserDao extends R2dbcRepository<UserEntity, Long> {
@Query("SELECT * FROM sys_user WHERE username = :username AND deleted_at IS NULL")
Mono<UserEntity> findByUsername(String username);
@Query("SELECT * FROM sys_user WHERE id = :id AND deleted_at IS NULL")
Mono<UserEntity> findByIdAndDeletedAtIsNull(Long id);
}
@@ -0,0 +1,73 @@
package cn.novalon.gym.manage.auth.domain;
import cn.novalon.gym.manage.sys.core.domain.BaseDomain;
import io.swagger.v3.oas.annotations.media.Schema;
public class User extends BaseDomain {
@Schema(description = "用户名", example = "admin")
private String username;
@Schema(description = "邮箱", example = "admin@example.com")
private String email;
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "昵称", example = "超级管理员")
private String nickname;
@Schema(description = "状态:0-禁用,1-正常", example = "1")
private Integer status;
@Schema(description = "角色ID", example = "1")
private Long roleId;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
}
@@ -0,0 +1,32 @@
package cn.novalon.gym.manage.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
@Schema(description = "用户登录请求")
public class LoginRequest {
@NotBlank(message = "用户名不能为空")
@Schema(description = "用户名", example = "admin", required = true)
private String username;
@NotBlank(message = "密码不能为空")
@Schema(description = "密码", example = "Test@123", required = true)
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
@@ -0,0 +1,62 @@
package cn.novalon.gym.manage.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "用户登录响应")
public class LoginResponse {
@Schema(description = "访问令牌")
private String accessToken;
@Schema(description = "刷新令牌")
private String refreshToken;
@Schema(description = "令牌类型", example = "Bearer")
private String tokenType;
@Schema(description = "过期时间(秒)", example = "7200")
private Long expiresIn;
@Schema(description = "用户信息")
private UserInfo userInfo;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
public Long getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(Long expiresIn) {
this.expiresIn = expiresIn;
}
public UserInfo getUserInfo() {
return userInfo;
}
public void setUserInfo(UserInfo userInfo) {
this.userInfo = userInfo;
}
}
@@ -0,0 +1,84 @@
package cn.novalon.gym.manage.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "用户信息")
public class UserInfo {
@Schema(description = "用户ID", example = "1")
private Long id;
@Schema(description = "用户名", example = "admin")
private String username;
@Schema(description = "邮箱", example = "admin@example.com")
private String email;
@Schema(description = "手机号", example = "13800138000")
private String phone;
@Schema(description = "昵称", example = "超级管理员")
private String nickname;
@Schema(description = "状态:0-禁用,1-正常", example = "1")
private Integer status;
@Schema(description = "角色ID", example = "1")
private Long roleId;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
}
@@ -0,0 +1,86 @@
package cn.novalon.gym.manage.auth.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;
@Table("sys_user")
public class UserEntity extends BaseEntity {
@Column("username")
private String username;
@Column("password")
private String password;
@Column("email")
private String email;
@Column("phone")
private String phone;
@Column("nickname")
private String nickname;
@Column("status")
private Integer status;
@Column("role_id")
private Long roleId;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public Long getRoleId() {
return roleId;
}
public void setRoleId(Long roleId) {
this.roleId = roleId;
}
}
@@ -0,0 +1,83 @@
package cn.novalon.gym.manage.auth.handler;
import cn.novalon.gym.manage.auth.dto.LoginRequest;
import cn.novalon.gym.manage.auth.dto.LoginResponse;
import cn.novalon.gym.manage.auth.dto.UserInfo;
import cn.novalon.gym.manage.auth.service.IAuthService;
import cn.novalon.gym.manage.auth.util.ResponseCode;
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 AuthHandler {
private final IAuthService authService;
private final Validator validator;
public AuthHandler(IAuthService authService, Validator validator) {
this.authService = authService;
this.validator = validator;
}
@Operation(summary = "用户名+密码登录", description = "使用用户名和密码进行登录认证")
public Mono<ServerResponse> login(ServerRequest request) {
return request.bodyToMono(LoginRequest.class)
.flatMap(loginRequest -> {
return authService.login(loginRequest)
.flatMap(response -> {
Map<String, Object> result = new HashMap<>();
result.put("code", ResponseCode.SUCCESS);
result.put("message", "登录成功");
result.put("data", response);
return ServerResponse.ok().bodyValue(result);
})
.onErrorResume(error -> {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("code", ResponseCode.BAD_REQUEST);
errorResponse.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(errorResponse);
});
})
.onErrorResume(error -> {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("code", ResponseCode.BAD_REQUEST);
errorResponse.put("message", "请求参数错误: " + error.getMessage());
return ServerResponse.badRequest().bodyValue(errorResponse);
});
}
@Operation(summary = "获取用户信息", description = "根据用户ID获取用户详细信息")
public Mono<ServerResponse> getUserInfo(ServerRequest request) {
try {
Long userId = Long.valueOf(request.pathVariable("id"));
return authService.getUserInfo(userId)
.flatMap(userInfo -> {
Map<String, Object> result = new HashMap<>();
result.put("code", ResponseCode.SUCCESS);
result.put("message", "获取用户信息成功");
result.put("data", userInfo);
return ServerResponse.ok().bodyValue(result);
})
.onErrorResume(error -> {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("code", ResponseCode.BAD_REQUEST);
errorResponse.put("message", error.getMessage());
return ServerResponse.badRequest().bodyValue(errorResponse);
});
} catch (NumberFormatException e) {
Map<String, Object> errorResponse = new HashMap<>();
errorResponse.put("code", ResponseCode.BAD_REQUEST);
errorResponse.put("message", "无效的用户ID");
return ServerResponse.badRequest().bodyValue(errorResponse);
}
}
}
@@ -0,0 +1,14 @@
package cn.novalon.gym.manage.auth.repository;
import cn.novalon.gym.manage.auth.domain.User;
import cn.novalon.gym.manage.auth.dto.LoginRequest;
import reactor.core.publisher.Mono;
public interface AuthRepository {
Mono<User> findByUsername(String username);
Mono<User> findById(Long id);
Mono<User> validateLogin(LoginRequest request);
}
@@ -0,0 +1,50 @@
package cn.novalon.gym.manage.auth.repository.impl;
import cn.novalon.gym.manage.auth.converter.UserConverter;
import cn.novalon.gym.manage.auth.dao.UserDao;
import cn.novalon.gym.manage.auth.domain.User;
import cn.novalon.gym.manage.auth.dto.LoginRequest;
import cn.novalon.gym.manage.auth.repository.AuthRepository;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component
public class AuthRepositoryImpl implements AuthRepository {
private final UserDao userDao;
private final PasswordEncoder passwordEncoder;
public AuthRepositoryImpl(UserDao userDao, PasswordEncoder passwordEncoder) {
this.userDao = userDao;
this.passwordEncoder = passwordEncoder;
}
@Override
public Mono<User> findByUsername(String username) {
return userDao.findByUsername(username)
.map(UserConverter::toDomain);
}
@Override
public Mono<User> findById(Long id) {
return userDao.findByIdAndDeletedAtIsNull(id)
.map(UserConverter::toDomain);
}
@Override
public Mono<User> validateLogin(LoginRequest request) {
return userDao.findByUsername(request.getUsername())
.flatMap(userEntity -> {
if (userEntity.getStatus() == 0) {
return Mono.error(new RuntimeException("用户已被禁用"));
}
if (passwordEncoder.matches(request.getPassword(), userEntity.getPassword())) {
return Mono.just(UserConverter.toDomain(userEntity));
} else {
return Mono.error(new RuntimeException("用户名或密码错误"));
}
})
.switchIfEmpty(Mono.error(new RuntimeException("用户名或密码错误")));
}
}
@@ -0,0 +1,13 @@
package cn.novalon.gym.manage.auth.service;
import cn.novalon.gym.manage.auth.dto.LoginRequest;
import cn.novalon.gym.manage.auth.dto.LoginResponse;
import cn.novalon.gym.manage.auth.dto.UserInfo;
import reactor.core.publisher.Mono;
public interface IAuthService {
Mono<LoginResponse> login(LoginRequest request);
Mono<UserInfo> getUserInfo(Long userId);
}
@@ -0,0 +1,48 @@
package cn.novalon.gym.manage.auth.service.impl;
import cn.novalon.gym.manage.auth.converter.UserConverter;
import cn.novalon.gym.manage.auth.dto.LoginRequest;
import cn.novalon.gym.manage.auth.dto.LoginResponse;
import cn.novalon.gym.manage.auth.dto.UserInfo;
import cn.novalon.gym.manage.auth.repository.AuthRepository;
import cn.novalon.gym.manage.auth.service.IAuthService;
import cn.novalon.gym.manage.auth.util.JwtUtil;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
@Service
public class AuthServiceImpl implements IAuthService {
private final AuthRepository authRepository;
private final JwtUtil jwtUtil;
public AuthServiceImpl(AuthRepository authRepository, JwtUtil jwtUtil) {
this.authRepository = authRepository;
this.jwtUtil = jwtUtil;
}
@Override
public Mono<LoginResponse> login(LoginRequest request) {
return authRepository.validateLogin(request)
.flatMap(user -> {
String accessToken = jwtUtil.generateToken(user.getId(), user.getUsername());
String refreshToken = jwtUtil.generateRefreshToken(user.getId(), user.getUsername());
LoginResponse response = new LoginResponse();
response.setAccessToken(accessToken);
response.setRefreshToken(refreshToken);
response.setTokenType("Bearer");
response.setExpiresIn(7200L);
response.setUserInfo(UserConverter.toUserInfo(user));
return Mono.just(response);
});
}
@Override
public Mono<UserInfo> getUserInfo(Long userId) {
return authRepository.findById(userId)
.map(UserConverter::toUserInfo)
.switchIfEmpty(Mono.error(new RuntimeException("用户不存在")));
}
}
@@ -0,0 +1,80 @@
package cn.novalon.gym.manage.auth.util;
import cn.novalon.gym.manage.common.config.JwtProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
@Component
public class JwtUtil {
private final JwtProperties jwtProperties;
public JwtUtil(JwtProperties jwtProperties) {
this.jwtProperties = jwtProperties;
}
private SecretKey getSigningKey() {
return Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8));
}
public String generateToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration()))
.signWith(getSigningKey())
.compact();
}
public String generateRefreshToken(Long userId, String username) {
Map<String, Object> claims = new HashMap<>();
claims.put("userId", userId);
claims.put("username", username);
return Jwts.builder()
.setClaims(claims)
.setSubject(username + "_refresh")
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtProperties.getExpiration() * 7))
.signWith(getSigningKey())
.compact();
}
public Claims parseToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
public Long getUserIdFromToken(String token) {
return parseToken(token).get("userId", Long.class);
}
public String getUsernameFromToken(String token) {
return parseToken(token).getSubject();
}
public boolean validateToken(String token) {
try {
parseToken(token);
return true;
} catch (Exception e) {
return false;
}
}
}
@@ -0,0 +1,34 @@
package cn.novalon.gym.manage.auth.util;
/**
* HTTP 响应状态码常量类
*
* @author 张翔
* @date 2026-06-16
*/
public class ResponseCode {
private ResponseCode() {
}
/** 成功 */
public static final int SUCCESS = 200;
/** 客户端错误:请求参数有误 */
public static final int BAD_REQUEST = 400;
/** 客户端错误:未授权 */
public static final int UNAUTHORIZED = 401;
/** 客户端错误:禁止访问 */
public static final int FORBIDDEN = 403;
/** 客户端错误:资源未找到 */
public static final int NOT_FOUND = 404;
/** 客户端错误:冲突(如重复数据) */
public static final int CONFLICT = 409;
/** 服务器错误:内部服务器错误 */
public static final int INTERNAL_SERVER_ERROR = 500;
}
@@ -0,0 +1,16 @@
# JWT Configuration
jwt:
secret: mySecretKeyForJwtTokenGenerationThatIsLongEnoughForHS256Algorithm
expiration: 7200 # 2 hours in seconds
refresh-expiration: 604800 # 7 days in seconds
# Server Configuration
server:
port: 8080
# Spring Configuration
spring:
application:
name: gym-manage-cuit
profiles:
active: dev
@@ -0,0 +1,15 @@
package cn.novalon.gym.manage.cuit.gymmanagecuit;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
@Disabled("模块无独立SpringBootApplication,需通过manage-app运行集成测试")
class GymManageCuitApplicationTests {
@Test
void contextLoads() {
}
}