Compare commits

...

30 Commits

Author SHA1 Message Date
Chaos
d20e8513cc refactor(user): 优化用户列表接口参数注解及日志输出格式
- 统一@RequestParam注解中的name属性书写格式
- 优化日志输出格式,提升可读性
- 保持接口功能不变,仅调整代码细节
2025-12-02 13:20:33 +08:00
Chaos
50296e8fce feat(user): 增强用户列表分页查询功能
- 新增 keyword 参数支持用户名和昵称模糊搜索
- 新增 roleId 参数支持按角色过滤用户
- 优化 SQL 查询逻辑,使用 EXISTS 子查询关联用户与角色
- 更新 Mapper 接口以支持新的查询参数
- 调整 Service 和 Controller 层方法签名适配新参数
- 添加请求日志记录便于调试和监控
2025-12-01 17:27:20 +08:00
Chaos
9609f95e5e feat(device): add logging for device creation endpoint
- Added SLF4J logging annotation to DeviceController
- Implemented info level logging for create device requests
- Included request payload in log message for debugging purposes
2025-12-01 09:24:45 +08:00
Chaos
6e2de46157 refactor(user): 重构用户相关接口与实现
- 修改application.yaml中logic-not-delete-value为"null"
- 删除RoleController中无用的RoleResponse导入
- 修改RoleController的@GetMapping注解路径为/options
- UserController使用构造器注入替代@Autowired并添加@RequiredArgsConstructor
- UserController的@RequestMapping从/api/user改为/api/users
- UserController的获取用户列表接口路径从/all改为/并增加分页参数说明
- UserController新增根据ID获取用户信息接口GET /{id}
- UserController的重置用户密码接口路径从/setUserPassword改为/{userId}/password
- 删除UserController中已废弃的更新用户名接口/updateUsername
- UserController的获取当前用户信息接口路径从/profile改为/me
- UserController的更新用户信息接口路径从/profile改为/me
- UserController新增更新用户名接口PUT /me/username并标记为@Deprecated
- UserController的设置用户昵称接口路径改为/{userId}/nickname
- UserProfileService接口中的findUserWithRoles方法重命名为findUserWithRolesByUsername
- UserProfileService接口新增findUserWithRolesById方法定义
- UserProfileServiceImpl实现类中删除无用RoleResponse导入
- UserProfileServiceImpl中findUserWithRoles方法重命名为findUserWithRolesByUsername
- UserProfileServiceImpl中实现findUserWithRolesById方法逻辑并增加空值校验
2025-11-29 09:01:50 +08:00
Chaos
b479304687 feat(device): 实现设备创建时关联DNS映射配置
- 新增InterfaceDnsMapping实体及Mapper依赖注入
- 设备创建接口支持网络接口地址配置及DNS服务器绑定
- 优化事务管理,增强异常回滚机制
- 空值校验完善,避免空指针异常
- DNS服务查询逻辑优化,使用Map提高查找效率
- 增加数据库DNS记录不存在的错误码定义
2025-11-29 07:23:00 +08:00
chaos
1e09ce9f54 feat(device): 添加设备创建时关联DNS服务器功能
- 在DeviceServiceImpl中注入DnsServerService依赖
- 设备创建流程中增加DNS服务器名称列表的处理逻辑
- 调用DnsServerService根据DNS名称列表获取对应ID列表
- 新增DnsServerService接口及实现类DnsServerServiceImpl
- 实现根据单个或多个DNS名称查询对应服务器ID的方法
- 在ErrorCode中新增DNS相关错误码定义
2025-11-29 06:41:26 +08:00
Chaos
cc70d867c1 feat(device): 完善设备及网络接口管理功能
- 新增网络接口及地址配置相关实体类与映射
- 扩展 CreateDeviceRequest 支持嵌套接口与地址配置
- 调整设备类型实体类字段并增强逻辑删除支持
- 优化数据表结构,分离接口属性与地址配置
- 新增 DNS 服务器及相关映射实体支持
- 实现设备创建事务中同步保存接口与地址信息
- 调整 MyBatis Plus 逻辑删除配置与时间字段类型
- 重构 data.sql 初始化脚本,完善表间外键约束
2025-11-28 21:46:52 +08:00
Chaos
3f8dc871ab feat(role): remove admin authority check from getUserRole endpoint
- Removed @PreAuthorize annotation requiring 'admin' authority
- Made getUserRole endpoint publicly accessible
- Simplified role retrieval logic for all users
2025-11-28 17:03:22 +08:00
Chaos
f4070d1b99 docs(role): update user role retrieval comment
- Changed comment from "获取用户角色" to "获取用户角色选项"
- Updated documentation to reflect role options retrieval
- Maintained admin authority requirement for endpoint access
2025-11-28 17:02:31 +08:00
Chaos
647d5733ac refactor(role): refactor role management API and service
- Remove unused imports and simplify controller annotations
- Change request mapping path from /api/role to /api/roles
- Update assignRolesToUser endpoint to use POST /users
- Update revokeRolesFromUser endpoint to use DELETE /users
- Modify getAllRoles method to return OptionResponse instead of RoleResponse
- Simplify database query in RoleServiceImpl using lambda query wrapper
- Add proper authorization checks for all role management endpoints
- Remove redundant getUserRole method with duplicate logic
2025-11-28 17:01:02 +08:00
Chaos
f0d6279949 feat(device): add device type options endpoint
- Added new GET /options endpoint to retrieve device type options
- Implemented getDeviceTypeOptions method in DeviceTypeController
- Updated DeviceTypeServiceImpl to map device types to OptionResponse
- Removed unused LambdaQueryWrapper and todo comment
- Added Java documentation for the new endpoint
2025-11-28 14:06:12 +08:00
Chaos
af8959220a feat(device): refactor device management API and implement CRUD operations
- Rename CreateDriveRequest to CreateDeviceRequest
- Add DeviceQueryRequest for pagination and filtering
- Add UpdateDeviceRequest for device updates
- Refactor DeviceController with RESTful endpoints
- Implement getAllDevices with pagination and search
- Implement createDevice endpoint
- Implement updateDevice endpoint
- Implement deleteDevice endpoint
- Remove real delete endpoint
- Add DeviceResponse and OptionResponse DTOs
- Update DeviceService interface and implementation
- Add device type options endpoint
- Update device type controller mappings
2025-11-27 17:11:57 +08:00
Chaos
79ef40bd34 feat(role): 新增获取用户角色接口
- 在RoleController中新增getUserRole方法,用于获取所有用户角色
- 添加RoleService接口及其实现类RoleServiceImpl
- 创建RoleResponse DTO并优化UserProfileResponse中的角色映射逻辑
- 引入RoleService依赖并配置相关注解以支持新功能
- 实现角色数据的查询与转换,返回标准ApiResult格式结果
2025-11-26 06:25:31 +08:00
Chaos
2a94f493e6 feat(auth): implement custom user details and enhance JWT token payload
- Introduce CustomUserDetails to encapsulate user information and authorities
- Remove redundant UserProfileResponse from AuthTokenResponse
- Add userId, nickname, and avatar claims to JWT token generation
- Update UserDetailsServiceImpl to use CustomUserDetails
- Modify JwtTokenUtil to accept CustomUserDetails and include additional claims
- Add avatar field to User entity
- Update UserMapper to map avatar field in result mappings
- Add logging for successful and failed login attempts
2025-11-25 16:53:43 +08:00
Chaos
a5a23a6b52 feat(auth): add user profile to auth token response
- Add jwt configuration in application.yaml
- Include user profile information in authentication response
- Implement user profile response DTO with role mapping
- Fix typo in UserProfileService interface and implementation class names
- Add pagination support for user list endpoint
- Configure MyBatis Plus interceptor for MariaDB
- Add MyBatis Plus JSQLParser dependency
- Implement page-based user retrieval with role information
- Remove obsolete MyBatis configuration class
- Update controller to use paginated user data
- Replace manual user conversion with constructor mapping
- Rename UserProfileServcie to UserProfileService consistently
2025-11-24 17:10:01 +08:00
Chaos
ec49ea8e25 feat(auth): 添加请求参数校验支持
- 在 AuthController 中为注册和登录接口添加 @Validated 注解
- 更新 DeviceController、RoleController 和 UserController 中的相关接口以支持参数校验
- 修改 AuthLoginRequest 中用户名校验的错误提示信息
2025-11-24 07:18:42 +08:00
Chaos
4f0e0c163d refactor(auth): 优化认证服务异常处理逻辑
- 合并导入Spring Security认证相关类
- 增加对内部认证服务异常的捕获处理
- 保留业务异常的直接抛出
- 完善未知异常的日志记录,包含完整堆栈信息
- 调整设备响应DTO结构,增加字段分隔空行

fix(device): 调整设备响应对象结构

- 在设备响应DTO中添加ID字段上方空行以改善代码可读性
2025-11-23 21:59:59 +08:00
Chaos
b2c6cfe90a feat(user): implement user profile management functionality
- Add user nickname update endpoint with validation
- Introduce user profile update endpoint supporting nickname and password changes
- Create UserProfileResponse DTO with nickname field
- Add SetUserNicknameRequest DTO for nickname updates
- Implement UserProfileUpdateRequest DTO for profile modifications
- Refactor UserService to UserProfileService with enhanced capabilities
- Update UserMapper to include nickname in result mapping
- Add new error codes for nickname and profile update failures
- Modify CORS configuration to allow requests from localhost:5173
- Remove obsolete AppConfig RestTemplate bean definition
- Rename UserinfoResponse to UserProfileResponse for consistency
- Adjust controller endpoints to use updated service and DTOs
- Add transactional support for user profile updates
- Improve error handling for user-related operations
2025-11-21 17:14:50 +08:00
Chaos
d4bbaf6715 feat(device): 实现设备逻辑删除与真实删除功能
- 添加设备逻辑删除接口,更新查询SQL过滤已删除数据
- 添加设备真实删除接口,需管理员权限
- 在DeviceService中实现逻辑删除与真实删除方法
- 在DeviceMapper中添加逻辑删除SQL语句
- 新增设备相关错误码:删除失败、类型删除失败等
- 更新UserController中修改密码与更新用户名接口的请求方式为PUT
- 优化DeviceController中创建设备与查询设备接口的注解使用
- 引入Spring Security注解支持接口权限控制
2025-11-21 07:30:17 +08:00
Chaos
72a1e4d309 feat(device): 添加设备类型管理功能
- 在Device实体中添加了deviceType字段,并使用@TableField注解标记为非数据库字段
- 新增DeviceType实体类,用于表示设备类型信息
- 创建DeviceTypeController控制器,提供获取所有设备类型的接口
- 新增DeviceTypeService接口及其实现类DeviceTypeServiceImpl,实现设备类型相关业务逻辑
- 添加DriveTypeMapper接口,继承BaseMapper以支持对DeviceType的数据库操作
- 在ErrorCode常量类中增加了设备相关的错误码:DEVICE_NOT_FOUND和DEVICE_TYPE_NOT_FOUND
- 更新DeviceController,引入DeviceTypeResponse并优化代码结构
- 在DeviceMapper中新增selectOneWithTypeById方法,通过MyBatis注解实现关联查询设备及其类型信息
2025-11-21 06:54:12 +08:00
Chaos
e23434ab48 feat(device): 添加设备管理功能模块
- 新增设备实体类 Device 及其对应的数据传输对象 CreateDriveRequest 和 DeviceResponse
- 创建设备相关的控制器 DeviceController 并实现新建设备接口
- 实现设备服务接口 DeviceService 及其具体实现类 DeviceServiceImpl
- 添加设备数据访问层接口 DeviceMapper
- 在数据库初始化脚本中增加设备相关表结构定义,包括设备类型表、设备表和网络接口表
- 更新应用配置文件以激活开发环境配置
- 修复图片上传时获取原始文件名的问题
- 修改用户角色分配相关接口方法命名以提高语义清晰度
2025-11-21 06:29:50 +08:00
chaos
8dd0efa09e feat(database): 重构用户角色关联及权限体系
- 新增 BizException 构造函数支持自定义消息
- 优化 SQL 脚本结构,明确表设计最佳实践
- 修改 t_user、t_role 等表字段类型与索引策略
- 引入代理主键 id 到关联表 t_user_role 和 t_role_permission
- 更新 UserRole 实体类适配 MyBatis-Plus 主键策略
- 增强 UserRoleService 接口功能,支持分配和撤销角色
- 实现批量操作和事务控制提升数据一致性
- 添加安全注解 @PreAuthorize 控制接口访问权限
- 修正 Mapper 注解并优化参数命名提高可读性
- 扩展 ErrorCode 常量增强错误描述准确性
2025-11-20 23:01:30 +08:00
Chaos
e46b820fca 功能(auth):实现用户注册并分配默认角色
- 添加AppConfig类以启用事务管理
- 在application.yaml中配置默认角色代码
- 在AuthServiceImpl中注入RoleMapper和defRoleCode
- 在注册过程中为新用户分配默认角色
- 添加用户注册和登录事件的日志记录
- 在登录过程中处理异常并进行适当的错误日志记录
- 创建用户、角色、权限及关联关系的数据库表
- 添加用户角色实体类和用户-角色关系映射器
- 实现用户角色服务以管理用户角色分配
- 添加通过SetUserRoleRequest设置用户角色的API端点
- 更新用户实体类并添加适当的字段和注解
- 实现包含角色信息的用户信息查询方法
- 将用户转换逻辑重构为可复用方法
- 为SetUserRoleRequest DTO添加校验约束}```
2025-11-20 17:19:52 +08:00
Chaos
f3f92b52b8 fix(security): 优化安全配置及增加权限异常处理
- 在全局异常处理器中添加权限不足异常的处理方法
- 允许对 /api/public/* 路径的匿名访问,完善安全过滤链配置
- 确保其他请求需要认证,提升安全防护能力
- 维持无状态会话管理,禁用 CSRF 以适应前后端分离架构
2025-11-19 17:18:04 +08:00
Chaos
7dc0d26d9b feat(user): 添加更新用户名功能并优化安全配置
- 在 ErrorCode 中新增 USER_UPDATE_USERNAME_FAILED 错误码
- JwtAuthenticationTokenFilter 中增加当前用户名属性设置
- RestAuthenticationEntryPoint 返回状态码改为 403 并更新错误信息
- 新增 UpdateUsernameRequest DTO 用于接收用户名更新请求
- UserController 添加 updateUsername 接口支持修改用户名
- UserInfoService 和其实现类中增加 updateUsername 方法逻辑
- 引入 tokenHead 配置项以支持 JWT 相关操作
2025-11-18 22:32:51 +08:00
Chaos
7e754b19d4 feat(auth):重构认证登录接口返回结构
- 修改登录接口返回类型为 AuthTokenResponse
- 新增 AuthTokenResponse DTO 类封装 token 和 tokenHead
- 调整 AuthService 接口及实现类返回值类型
- 移除 Controller 层手动构造返回数据逻辑
- 完善异常处理逻辑,区分不同认证失败场景
- 新增用户未启用状态的错误码和处理
- 添加全局异常处理器对授权拒绝异常的处理
2025-11-18 17:18:09 +08:00
Chaos
0527602d1c feat(upload): 移除旧的分片上传HTML页面
- 删除了upload.html文件中的全部内容
- 移除了前端分片上传的示例代码- 清理了相关的CSS样式和JavaScript逻辑
- 移除了对axios库的引用
- 删除了文件选择、上传按钮和进度条的UI元素
- 移除了JWT token配置和相关API调用代码
2025-11-18 16:45:34 +08:00
Chaos
8fc7f6554d refactor(auth):重构认证模块并增强验证机制
- 重命名并调整 DTO 类结构,将 LoginRequest 和 RegisterRequest 迁移至 request 包- 引入 AuthLoginRequest 和 AuthRegisterRequest 并添加字段验证注解
- 更新 AuthController 使用新的 DTO 并增加 @Valid 参数校验
- 修改 AuthService 接口及实现类,接收 User 实体而非 RegisterRequest
- 添加全局异常处理器 GlobalExceptionHandler 处理参数校验和业务异常
- 新增 ErrorCode 枚举统一管理错误码和消息
- 引入 UserConverter 组件用于 DTO 到实体的转换
- 增强用户注册与登录逻辑,完善异常处理和错误提示
- 移除旧的 BadCredentialsException 捕获逻辑
- 调整 pom.xml 添加 spring-boot-starter-validation 和相关依赖
- 更新 User 实体类,添加完整字段映射和角色关联配置
- 新增 UserInfoService 及其实现,提供用户管理和密码设置功能
-优化 UserMapper 查询方法,支持联表查询用户及其角色信息
- 删除无用的 HLSController 控制器
- 完善 ImageController 文件上传逻辑并更新响应结构
- 添加用户名和密码格式验证工具类 PasswordValidate 和 UsernameValidate
2025-11-18 16:44:43 +08:00
Chaos
a22a369afa Merge remote-tracking branch 'origin/main' 2025-11-18 07:28:27 +08:00
Chaos
da1bdafbb2 feat(image): add image upload functionality
- Created FileUploadResponse DTO for image upload responses
- Implemented ImageController with upload endpoint
- Defined ImageService interface for image operations
- Added ImageServiceImpl with placeholder upload logic
- Updated pom.xml to include chaos_api_domain dependency
2025-11-18 07:27:28 +08:00
86 changed files with 2963 additions and 389 deletions

View File

@@ -29,5 +29,21 @@
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>cn.nopj</groupId>
<artifactId>chaos_api_domain</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,88 @@
package cn.nopj.chaos_api.common.constants;
import lombok.Getter;
/**
* 全局错误码枚举HTTP状态码 + 业务错误码)
* 规范:
* - 4xx: 客户端错误
* - 5xx: 服务端错误
* - 业务码分段:
* 100-199: 用户相关
* 200-299: 帖子/评论相关
* 300-399: 系统/第三方服务
*/
@Getter
public enum ErrorCode {
// ================== 用户相关 (100-199) ==================
USERNAME_EXISTS(409, "USER-101", "用户名已存在"),
USERNAME_FORMAT_ERROR(400, "USER-102", "用户名需5-20位字母数字组合"),
USERNAME_PATTERN_INVALID( 400, "USER-103" , "用户名格式无效,必须由字母、数字、下划线" ),
PASSWORD_FORMAT_ERROR(400, "USER-104", "密码需6-20位字符组合"),
USER_NOT_EXISTS(404, "USER-104", "用户不存在"),
USER_ID_INVALID(400, "USER-105", "用户ID无效"),
USER_NOT_EXISTS_OR_PASSWORD_WRONG(401, "USER-105", "用户名不存在或密码错误"),
USER_NOT_ENABLED(403, "USER-106", "用户未启用"),
USER_NOT_LOGIN(401, "USER-105", "请先登录"),
USER_BANNED(403, "USER-106", "账号已被封禁"),
EMAIL_EXISTS(409, "USER-107", "邮箱已注册"),
EMAIL_FORMAT_ERROR(400, "USER-108", "邮箱格式无效"),
PHONE_EXISTS(409, "USER-109", "手机号已注册"),
PHONE_FORMAT_ERROR(400, "USER-110", "手机号格式无效"),
USER_UPDATE_USERNAME_FAILED(400,"USER-111" , "用户名更新失败"),
USER_UPDATE_NICKNAME_FAILED( 400, "USER-112", "用户昵称更新失败" ),
USER_UPDATE_PROFILE_FAILED(400,"USER-113" , "用户信息更新失败" ),
// 用户组
ROLE_REVOKE_FAILED(400,"USER-112" ,"用户组删除失败" ),
ROLE_ID_EXISTS(400, "ROLE-100" , "角色ID已存在" ),
ROLE_ID_INVALID(400, "ROLE-101" , "角色ID无效" ),
ROLE_ID_NOT_EXISTS(400, "ROLE-102" , "角色ID不存在"),
// ================== 论坛内容相关 (200-299) ==================
POST_NOT_FOUND(404, "POST-201", "帖子不存在"),
POST_DELETED(410, "POST-202", "帖子已被删除"),
POST_TITLE_EMPTY(400, "POST-203", "标题不能为空"),
POST_CONTENT_EMPTY(400, "POST-204", "内容不能为空"),
COMMENT_NOT_FOUND(404, "POST-205", "评论不存在"),
COMMENT_TOO_LONG(400, "POST-206", "评论超过500字限制"),
// ================== 系统/第三方 (300-399) ==================
SYSTEM_ERROR(500, "SYS-300", "系统错误"),
CAPTCHA_ERROR(400, "SYS-301", "验证码错误"),
SMS_SEND_FAILED(500, "SYS-302", "短信发送失败"),
FILE_UPLOAD_FAILED(500, "SYS-303", "文件上传失败"),
RATE_LIMIT_EXCEEDED(429, "SYS-304", "操作过于频繁"),
// 设备
DEVICE_NOT_FOUND(404, "DEVICE-301", "设备不存在"),
DEVICE_TYPE_NOT_FOUND(404, "DEVICE-302", "设备类型不存在"),
DEVICE_DELETE_FAILED(500, "DEVICE-303", "设备删除失败"),
DEVICE_TYPE_DELETE_FAILED(500, "DEVICE-304", "设备类型删除失败"),
DEVICE_TYPE_NOT_EMPTY(400, "DEVICE-305", "设备类型不为空"),
DEVICE_DISABLED(403, "DEVICE-306", "设备已禁用"),
// DNS
DNS_NOT_DEFINED( 400, "DNS-401" , "未定义的DNS服务器" ),
DNS_NOT_FOUND(404, "DNS-402", "DNS服务器不存在"),
DNS_DELETE_FAILED(500, "DNS-403", "DNS服务器删除失败"),
DNS_NOT_EMPTY(400, "DNS-404", "DNS服务器不为空"),
DNS_DISABLED(403, "DNS-405", "DNS服务器已禁用"),
DNS_NOT_EXISTS(400, "DNS-406", "DNS服务器不存在"),
DNS_NOT_EXISTS_OR_PASSWORD_WRONG(401, "DNS-407", "DNS服务器不存在或密码错误"),
DNS_UPDATE_FAILED(400, "DNS-408", "DNS服务器更新失败"),
// 数据库
DB_DNS_NOT_FOUND( 404, "DB-401" , "数据库DNS服务器不存在" );
private final int httpStatus;
private final String code; // 业务错误码(领域-编号)
private final String message;
ErrorCode(int httpStatus, String code, String message) {
this.httpStatus = httpStatus;
this.code = code;
this.message = message;
}
}

View File

@@ -0,0 +1,18 @@
package cn.nopj.chaos_api.common.exceotion;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import lombok.Getter;
@Getter
public class BizException extends RuntimeException{
public final ErrorCode errorCode;
public BizException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
public BizException(String message) {
super(message);
this.errorCode = ErrorCode.SYSTEM_ERROR;
}
}

View File

@@ -0,0 +1,20 @@
package cn.nopj.chaos_api.converter;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.dto.request.AuthRegisterRequest;
import org.springframework.stereotype.Component;
@Component
public class UserConverter {
public User convert(AuthRegisterRequest request){
User user = new User();
user.setUsername(request.getUsername());
user.setPassword(request.getPassword());
user.setEnabled(true);
user.setAccountNonExpired(true);
user.setCredentialsNonExpired(true);
user.setAccountNonLocked(true);
return user;
}
}

View File

@@ -48,7 +48,11 @@
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>3.5.4</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-jsqlparser -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
</dependency>
</dependencies>
</project>

View File

@@ -1,9 +0,0 @@
package cn.nopj.chaos_api.config;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("cn.nopj.chaos_api.mapper")
public class MyBatisPlusConfig {
}

View File

@@ -0,0 +1,40 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.Device;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;
@Mapper
public interface DeviceMapper extends BaseMapper<Device> {
/**
* 根据id查询设备信息
* @param id 设备id
* @return 设备信息
*/
@Select("SELECT * FROM t_device WHERE id = #{id} AND deleted = 0 ")
@Results(id = "DeviceResultMap", value ={
@Result(id = true, property = "id", column = "id"),
@Result(property = "name", column = "name"),
@Result(property = "model", column = "model"),
@Result(property = "snmpCommunity", column = "snmp_community"),
@Result(property = "manufacturer", column = "manufacturer"),
@Result(property = "purchaseDate", column = "purchase_date"),
@Result(property = "status", column = "status"),
@Result(property = "remark", column = "remark"),
@Result(property = "createTime", column = "create_time"),
@Result(property = "updateTime", column = "update_time"),
@Result(property = "deviceType", column = "type_id",
one = @One(select = "cn.nopj.chaos_api.mapper.DriveTypeMapper.selectById"))
})
Device selectOneWithTypeById(Long id);
/**
* 逻辑删除设备
* @param id 设备id
* @return 逻辑删除影响行数
*/
@Update("UPDATE t_device SET deleted = 1 WHERE id = #{id}")
int deleteDevice(Long id);
}

View File

@@ -0,0 +1,9 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.DnsServer;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DnsServerMapper extends BaseMapper<DnsServer> {
}

View File

@@ -0,0 +1,11 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.DeviceType;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface DriveTypeMapper extends BaseMapper<DeviceType> {
}

View File

@@ -0,0 +1,9 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.InterfaceAddressConfig;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InterfaceAddressConfigMapper extends BaseMapper<InterfaceAddressConfig> {
}

View File

@@ -0,0 +1,9 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.InterfaceDnsMapping;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface InterfaceDnsMappingMapper extends BaseMapper<InterfaceDnsMapping> {
}

View File

@@ -0,0 +1,9 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.NetworkInterface;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface NetworkInterfaceMapper extends BaseMapper<NetworkInterface> {
}

View File

@@ -3,8 +3,16 @@ package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.Role;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface RoleMapper extends BaseMapper<Role> {
/**
* 根据角色编码查询角色信息
* @param code 角色编码
* @return 角色信息
*/
@Select("SELECT * FROM t_role WHERE code = #{code}")
Role selectByCode(String code);
}

View File

@@ -1,10 +1,12 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.Role;
import cn.nopj.chaos_api.domain.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.*;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@@ -32,9 +34,108 @@ public interface UserMapper extends BaseMapper<User> {
/**
* 插入用户角色关系
* @param id 用户id
* @param i 角色id
* 注意:已将注解由 @Select 修正为 @Insert并优化了参数命名。
* 由于 t_user_role 现在有自增主键 id只需插入 user_id 和 role_id
* @param userId 用户id
* @param roleId 角色id
*/
@Select("INSERT INTO t_user_role (user_id, role_id) VALUES (#{userId}, #{roleId})")
void insertUserRole(@Param("userId") Long id, @Param("roleId") Long i);
}
@Insert("INSERT INTO t_user_role (user_id, role_id) VALUES (#{userId}, #{roleId})")
void insertUserRole(@Param("userId") Long userId, @Param("roleId") Long roleId);
/**
* 根据用户名查询用户信息
* @param username 用户名
*/
@Select("SELECT * FROM t_user WHERE username = #{username}")
User selectByUsername(String username);
/**
* 根据用户ID查询用户信息和角色信息
*/
@Select("SELECT * FROM t_user WHERE id = #{id}")
@Results({
@Result(property = "roles", column = "id",
many = @Many(select = "findRolesByUserId"))
})
User findUserWithRolesById(Long id);
/**
* 查询所有用户信息
* @return 所有用户信息
**/
@Select("""
SELECT * FROM t_user
""")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "nickname", column = "nickname"),
@Result(property = "avatar", column = "avatar"),
@Result(property = "roles", column = "id",
many = @Many(select = "findRolesByUserId"))
})
List<User> findAllUserWithRoles();
/**
* 根据用户ID查询角色信息
*/
@Select("""
SELECT r.id, r.name ,r.code
FROM t_role r
JOIN t_user_role ur ON r.id = ur.role_id
WHERE ur.user_id = #{userId}
""")
List<Role> findRolesByUserId(@Param("userId") Long userId);
@Select("SELECT * FROM t_user WHERE username = #{username} ")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "nickname", column = "nickname"),
@Result(property = "avatar", column = "avatar"),
@Result(property = "roles", column = "id",
many = @Many(select = "findRolesByUserId"))
})
User findUserWithRolesByUsername(String username);
/**
* 分页查询用户,并携带角色信息
* 增加了 roleId 过滤
*/
@Select("""
<script>
SELECT * FROM t_user u
<where>
<if test='keyword != null and keyword != ""'>
AND (u.username LIKE CONCAT('%', #{keyword}, '%') OR u.nickname LIKE CONCAT('%', #{keyword}, '%'))
</if>
<if test='roleId != null'>
AND EXISTS (
SELECT 1
FROM t_user_role ur
WHERE ur.user_id = u.id
AND ur.role_id = #{roleId}
)
</if>
</where>
ORDER BY u.id ASC
</script>
""")
@Results({
@Result(id = true, property = "id", column = "id"),
@Result(property = "username", column = "username"),
@Result(property = "nickname", column = "nickname"),
@Result(property = "avatar", column = "avatar"),
@Result(property = "roles", column = "id",
many = @Many(select = "findRolesByUserId"))
})
IPage<User> selectPageWithRoles(Page<User> page,
@Param("keyword") String keyword,
@Param("roleId") Long roleId);
}

View File

@@ -0,0 +1,18 @@
package cn.nopj.chaos_api.mapper;
import cn.nopj.chaos_api.domain.entity.UserRole;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
@Mapper
public interface UserRoleMapper extends BaseMapper<UserRole> {
/**
* 删除用户角色关系
* @param userId 用户id
* @param roleId 角色id
*/
@Delete("DELETE FROM t_user_role WHERE user_id = #{userId} AND role_id = #{roleId}")
int delete(@Param("userId") Long userId,@Param("roleId") Long roleId);
}

View File

@@ -20,6 +20,10 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
@@ -29,5 +33,11 @@
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>3.0.2</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,30 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDate;
@Data
@TableName("t_device")
public class Device {
@TableId(value = "id",type = IdType.AUTO)
private Long id;
private String name;
private String model;
private Long typeId;
private Long locationId;
private String snmpCommunity;
private String manufacturer;
private LocalDate purchaseDate;
private int status;
private String remark;
private LocalDate createTime;
private LocalDate updateTime;
@TableLogic
private LocalDate deleteTime;
@TableField(exist = false)
private DeviceType deviceType;
}

View File

@@ -0,0 +1,51 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("t_device_type")
public class DeviceType {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 设备类型名称
*/
private String name;
/**
* 设备类型唯一编码
*/
private String code;
/**
* 父级类型ID (0表示顶级)
*/
private Long parentId;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除时间
*/
@TableLogic
private LocalDateTime deleteTime;
}

View File

@@ -0,0 +1,50 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* DNS 服务器实体
* 对应表: t_dns_server
*/
@Data
@TableName("t_dns_server")
public class DnsServer {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* DNS服务器IP地址
*/
private String dnsAddress;
/**
* DNS名称 (如: Google DNS)
*/
private String name;
/**
* 描述
*/
private String description;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除时间
*/
@TableLogic
private LocalDateTime deleteTime;
}

View File

@@ -0,0 +1,90 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 接口地址与VLAN配置实体 (核心配置)
* 对应表: t_interface_address_config
*/
@Data
@TableName("t_interface_address_config")
public class InterfaceAddressConfig {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 关联物理接口ID
*/
private Long interfaceId;
/**
* VLAN ID (0或NULL表示Native/Untagged)
*/
private Integer vlanId;
/**
* IP地址 (支持IPv4/IPv6)
*/
private String ipAddress;
/**
* 子网掩码/CIDR前缀
*/
private String subnetMask;
/**
* 网关IP地址
*/
private String gatewayIp;
/**
* 广播地址
*/
private String broadcastAddress;
/**
* 是否为主IP (true:主IP, false:从IP/Alias)
*/
private Boolean isPrimary;
/**
* 是否启用DHCP (true:启用, false:静态)
*/
private Boolean isDhcp;
/**
* MTU值
*/
private Integer mtu;
/**
* 配置状态 (1:启用, 0:禁用)
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除时间
*/
@TableLogic
private LocalDateTime deleteTime;
}

View File

@@ -0,0 +1,38 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 接口配置与DNS关联实体
* 对应表: t_interface_dns_mapping
*/
@Data
@TableName("t_interface_dns_mapping")
public class InterfaceDnsMapping {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 接口配置ID (关联 t_interface_address_config)
*/
private Long configId;
/**
* DNS服务器ID (关联 t_dns_server)
*/
private Long dnsServerId;
/**
* 优先级 (数值越小优先级越高)
*/
private Integer priority;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
}

View File

@@ -0,0 +1,80 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.*;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 网络接口基础实体 (物理/链路层属性)
* 对应表: t_network_interface
*/
@Data
@TableName("t_network_interface")
public class NetworkInterface {
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 设备ID
*/
private Long deviceId;
/**
* 父接口ID (用于子接口/聚合口成员)
*/
private Long parentId;
/**
* 接口名称 (如: eth0, GE0/0/1)
*/
private String name;
/**
* 接口类型 (1:物理口, 2:聚合口, 3:虚拟口)
*/
private Integer type;
/**
* MAC地址
*/
private String macAddress;
/**
* 物理端口速率 (Mbps)
*/
private Integer portSpeed;
/**
* 双工模式 (1:全双工, 2:半双工, 3:自适应)
*/
private Integer duplex;
/**
* 接口运行状态 (1:UP, 0:DOWN)
*/
private Integer status;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
/**
* 逻辑删除时间
*/
@TableLogic
private LocalDateTime deleteTime;
}

View File

@@ -1,21 +1,94 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableLogic;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.List;
/**
* 系统管理-用户表
* @TableName t_user
*/
@Data
@TableName("t_user")
public class User implements Serializable {
@TableName(value ="t_user")
public class User {
/**
* 主键ID
*/
@TableId
private Long id;
/**
* 用户名/登录名
*/
private String username;
/**
* 密码加密存储建议BCrypt
*/
private String password;
/**
* 用户昵称
*/
private String nickname;
/**
* 头像
*/
private String avatar;
/**
* 状态 (1:启用, 0:禁用)
*/
private Boolean enabled;
/**
* 账号未过期 (1:是, 0:否)
*/
private Boolean accountNonExpired;
/**
* 凭证未过期 (1:是, 0:否)
*/
private Boolean credentialsNonExpired;
/**
* 账号未锁定 (1:是, 0:否)
*/
private Boolean accountNonLocked;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}
/**
* 备注信息
*/
private String remark;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除时间
*/
@TableLogic
private LocalDateTime deleteTime;
@TableField(exist = false)
private List<Role> roles;
}

View File

@@ -0,0 +1,18 @@
package cn.nopj.chaos_api.domain.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@TableName("t_user_role")
@Data
public class UserRole {
@TableId(value = "id", type = IdType.AUTO)
private Long id; // 虚拟字段,数据库可不存
private Long userId;
private Long roleId;
}

View File

@@ -1,12 +0,0 @@
package cn.nopj.chaos_api.dto;
import lombok.Data;
/**
* 登录请求参数
*/
@Data
public class LoginRequest {
private String username;
private String password;
}

View File

@@ -1,12 +0,0 @@
package cn.nopj.chaos_api.dto;
import lombok.Data;
/**
* 注册请求参数
*/
@Data
public class RegisterRequest {
private String username;
private String password;
}

View File

@@ -0,0 +1,15 @@
package cn.nopj.chaos_api.dto.request;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* 登录请求参数
*/
@Data
public class AuthLoginRequest {
@Pattern(regexp = "^[a-zA-Z0-9_-]{5,16}$", message = "用户名校验失败")
private String username;
@Pattern(regexp = "^.{8,16}$", message = "密码长度需为8-16位")
private String password;
}

View File

@@ -0,0 +1,17 @@
package cn.nopj.chaos_api.dto.request;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
/**
* 注册请求参数
*/
@Data
public class AuthRegisterRequest {
@Pattern(regexp = "^[a-zA-Z0-9_-]{5,16}$", message = "用户名需为5-16位字母、数字、_或-")
private String username;
//密码不低于8位 不高于16位不限制字符
@Pattern(regexp = "^.{8,16}$", message = "密码长度需为8-16位")
private String password;
}

View File

@@ -0,0 +1,97 @@
package cn.nopj.chaos_api.dto.request;
import lombok.Data;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import java.time.LocalDate;
import java.util.List;
/**
* 创建设备请求对象
* <p>
* 对应您的命名规范CreateDeviceRequest
* 位于 request 包下
*/
@Data
public class CreateDeviceRequest {
// --- 设备基础信息 ---
@NotBlank(message = "设备名称不能为空")
private String name;
@NotNull(message = "设备类型ID不能为空")
private Long typeId;
private Long locationId;
@NotBlank(message = "设备型号不能为空")
private String model;
@NotBlank(message = "设备厂商不能为空")
private String manufacturer;
private String snmpCommunity;
private LocalDate purchaseDate;
private String remark;
@Valid
private List<NetworkInterfaceRequest> interfaces;
/**
* 网络接口请求参数
*/
@Data
public static class NetworkInterfaceRequest {
@NotBlank(message = "接口名称不能为空")
private String name;
@NotNull(message = "接口类型不能为空")
private Integer type; // 1:物理口, 2:聚合口, 3:虚拟口
@Pattern(regexp = "^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", message = "MAC地址格式不正确")
private String macAddress;
private Integer portSpeed;
private Integer duplex;
private String remark;
@Valid
private List<InterfaceAddressConfigRequest> addressConfigs;
}
/**
* 地址与VLAN配置请求参数
*/
@Data
public static class InterfaceAddressConfigRequest {
private Integer vlanId;
@Pattern(regexp = "^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", message = "IP地址格式不正确")
private String ipAddress;
private String subnetMask;
private String gatewayIp;
private String broadcastAddress;
private Boolean isPrimary;
private Boolean isDhcp;
private Integer mtu;
private List<String> dnsServers;
}
}

View File

@@ -0,0 +1,12 @@
package cn.nopj.chaos_api.dto.request;
import lombok.Data;
@Data
public class DeviceQueryRequest {
private Integer pageNum = 1;
private Integer pageSize = 10;
private Integer type;
private String keyword;
private String status;
}

View File

@@ -0,0 +1,11 @@
package cn.nopj.chaos_api.dto.request;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
@Data
public class SetUserNicknameRequest {
@NotBlank(message = "昵称不能为空")
private String nickname;
}

View File

@@ -0,0 +1,13 @@
package cn.nopj.chaos_api.dto.request;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Data
public class SetUserPasswordRequest {
//用户id为数字Long类型
@Pattern(regexp = "^[0-9]+$", message = "用户id格式错误")
private String userId;
@Pattern(regexp = "^.{8,16}$", message = "密码长度需为8-16位")
private String password;
}

View File

@@ -0,0 +1,14 @@
package cn.nopj.chaos_api.dto.request;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.util.List;
@Data
public class SetUserRoleRequest {
@Pattern(regexp = "^[0-9]+$", message = "用户id格式错误")
private Long userId;
@Pattern(regexp = "^[0-9]+$", message = "角色id格式错误")
private List<Long> rolesId;
}

View File

@@ -0,0 +1,22 @@
package cn.nopj.chaos_api.dto.request;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.time.LocalDate;
@Data
public class UpdateDeviceRequest {
@NotBlank(message = "设备id不能为空")
private Long id;
private String name;
private String model;
private Long typeId;
private Long locationId;
private String snmpCommunity;
private String manufacturer;
private LocalDate purchaseDate;
private int status;
private String remark;
}

View File

@@ -0,0 +1,10 @@
package cn.nopj.chaos_api.dto.request;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
@Data
public class UpdateUsernameRequest {
@Pattern(regexp = "^[a-zA-Z0-9_-]{5,16}$", message = "用户名需为5-16位字母、数字、_或-")
private String username;
}

View File

@@ -0,0 +1,9 @@
package cn.nopj.chaos_api.dto.request;
import lombok.Data;
@Data
public class UserProfileUpdateRequest {
private String nickname;
private String password;
}

View File

@@ -0,0 +1,9 @@
package cn.nopj.chaos_api.dto.response;
import lombok.Data;
@Data
public class AuthTokenResponse {
private String tokenHead;
private String token;
}

View File

@@ -0,0 +1,34 @@
package cn.nopj.chaos_api.dto.response;
import cn.nopj.chaos_api.domain.entity.Device;
import lombok.Data;
import java.time.LocalDate;
@Data
public class DeviceResponse {
private Long id;
private String name;
private String model;
private Long typeId;
private Long locationId;
private String snmpCommunity;
private String manufacturer;
private LocalDate purchaseDate;
private int status;
private String remark;
public DeviceResponse(Device device) {
this.id = device.getId();
this.name = device.getName();
this.model = device.getModel();
this.typeId = device.getTypeId();
this.locationId = device.getLocationId();
this.snmpCommunity = device.getSnmpCommunity();
this.manufacturer = device.getManufacturer();
this.purchaseDate = device.getPurchaseDate();
this.status = device.getStatus();
this.remark = device.getRemark();
}
}

View File

@@ -0,0 +1,27 @@
package cn.nopj.chaos_api.dto.response;
import cn.nopj.chaos_api.domain.entity.DeviceType;
import lombok.Data;
import java.time.LocalDateTime;
@Data
public class DeviceTypeResponse {
private Long id;
private String name;
private String code;
private Long parentId;
private String remark;
private LocalDateTime createTime;
private LocalDateTime updateTime;
public DeviceTypeResponse(DeviceType type){
this.id = type.getId();
this.name = type.getName();
this.code = type.getCode();
this.parentId = type.getParentId();
this.remark = type.getRemark();
this.createTime = type.getCreateTime();
this.updateTime = type.getUpdateTime();
}
}

View File

@@ -0,0 +1,13 @@
package cn.nopj.chaos_api.dto.response;
import lombok.Data;
@Data
public class FileUploadResponse {
private String fileName;
private String fileDownloadUri;
private String ossBucket;
private String fileOSSUri;
private String fileType;
private long size;
}

View File

@@ -0,0 +1,20 @@
package cn.nopj.chaos_api.dto.response;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class OptionResponse {
/**
* 选项标签
*/
private String label;
/**
* 值
*/
private String value;
}

View File

@@ -0,0 +1,15 @@
package cn.nopj.chaos_api.dto.response;
import cn.nopj.chaos_api.domain.entity.Role;
import lombok.Data;
@Data
public class RoleResponse {
private Long id;
private String name;
public RoleResponse(Role role){
this.id = role.getId();
this.name = role.getName();
}
}

View File

@@ -0,0 +1,23 @@
package cn.nopj.chaos_api.dto.response;
import cn.nopj.chaos_api.domain.entity.User;
import lombok.Data;
import java.util.List;
@Data
public class UserProfileResponse {
private Long id;
private String username;
private String nickname;
private String avatar;
private List<RoleResponse> roles;
public UserProfileResponse(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.nickname = user.getNickname();
this.avatar = user.getAvatar();
this.roles = user.getRoles().stream().map(RoleResponse::new).toList();
}
}

View File

@@ -25,6 +25,7 @@
<artifactId>chaos_api_domain</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>
<properties>

View File

@@ -0,0 +1,48 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.dto.request.CreateDeviceRequest;
import cn.nopj.chaos_api.dto.request.DeviceQueryRequest;
import cn.nopj.chaos_api.dto.request.UpdateDeviceRequest;
import cn.nopj.chaos_api.dto.response.DeviceResponse;
import com.baomidou.mybatisplus.core.metadata.IPage;
public interface DeviceService {
/**
* 新建设备信息
* @param createDeviceRequest 设备信息
* @return 新建设备信息结果
*/
DeviceResponse createDevice(CreateDeviceRequest createDeviceRequest);
/**
* 根据id查询设备信息
* @param id 设备id
* @return 设备信息
*/
DeviceResponse getDeviceById(Long id);
/**
* 删除设备信息 伪删除
* @param id 设备id
*/
void deleteDevice(Long id);
/**
* 删除设备信息 真删除
* @param id 设备id
*/
void deleteDeviceReal(Long id);
/**
* 查询所有设备信息
* @return 所有设备信息
*/
IPage<DeviceResponse> getAllDevices(DeviceQueryRequest request);
/**
* 更新设备信息
* @param updateDeviceRequest 设备信息
* @return 更新设备信息结果
*/
DeviceResponse updateDevice(UpdateDeviceRequest updateDeviceRequest);
}

View File

@@ -0,0 +1,20 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.dto.response.DeviceTypeResponse;
import cn.nopj.chaos_api.dto.response.OptionResponse;
import java.util.List;
public interface DeviceTypeService {
/**
* 获取所有设备类型
* @return 所有设备类型
*/
List<DeviceTypeResponse> getAllDeviceTypes();
/**
* 获取设备类型选项
* @return 设备类型选项
*/
List<OptionResponse> getDeviceTypeOptions();
}

View File

@@ -0,0 +1,21 @@
package cn.nopj.chaos_api.service;
import java.util.List;
public interface DnsServerService {
/**
* 根据域名获取DNS服务器ID
* @param dnsAddress 域名
* @return DNS服务器ID
*/
Long getDnsServerIdByName(String dnsAddress);
/**
* 根据域名列表获取DNS服务器ID列表
* @param nameList 域名列表
* @return DNS服务器ID列表
*/
List<Long> getDnsServerIdListByNameList(List<String> nameList);
}

View File

@@ -0,0 +1,17 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.dto.response.FileUploadResponse;
import java.io.InputStream;
public interface ImageService {
/**
* 上传图片
* @param fileName 文件名
* @param content 文件内容
* @return 文件路径
*/
FileUploadResponse uploadImage(String fileName, InputStream content);
}

View File

@@ -0,0 +1,14 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.dto.response.OptionResponse;
import cn.nopj.chaos_api.dto.response.RoleResponse;
import java.util.List;
public interface RoleService {
/**
* 获取所有角色
* @return 所有角色
*/
List<OptionResponse> getAllRoles();
}

View File

@@ -0,0 +1,101 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.dto.request.UserProfileUpdateRequest;
import cn.nopj.chaos_api.dto.response.UserProfileResponse;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import java.util.List;
/**
* 用户信息服务
*
* @author nopj
*/
public interface UserProfileService {
/**
* 设置用户密码
*
* @param id 用户id
* @param password 密码
*/
void setUserPassword(Long id, String password);
/**
* 获取所有用户信息
*
* @return 所有用户信息
*/
List<UserProfileResponse> getAllUsers();
/**
* 设置用户密码
*
* @param userId 用户id
* @param password 密码
*/
void setUserPassword(@Pattern(regexp = "^[0-9]+$", message = "用户id格式错误") String userId, @Pattern(regexp = "^.{8,16}$", message = "密码长度需为8-16位") String password);
/**
* 更新用户名
*
* @param username 用户名
* @param newUsername 新用户名
*/
void updateUsername(String username, String newUsername);
/**
* 根据用户名查询用户信息
*
* @param username 用户名
* @return 用户信息
*/
UserProfileResponse findUserWithRolesByUsername(String username);
/**
* 设置用户昵称
*
* @param username 用户名
* @param nickname 昵称
* @return 处理结果
*/
UserProfileResponse setUserNickname(@NotBlank(message = "用户账号不能为空") String username, @NotBlank(message = "昵称不能为空") String nickname);
/**
* 批量设置用户昵称
*
* @param userId 用户ID
* @param nickname 昵称
* @return 处理结果
*/
UserProfileResponse setUserNickname(@NotBlank(message = "用户ID不能为空") Long userId, @NotBlank(message = "昵称不能为空") String nickname);
/**
* 更新用户信息
*
* @param username 用户名
* @param request 用户信息
* @return 处理结果
*/
UserProfileResponse updateUserProfile(String username, UserProfileUpdateRequest request);
/**
* 获取所有用户信息
*
* @param pageNum 页码
* @param pageSize 页大小
* @return 所有用户信息
*/
IPage<UserProfileResponse> getAllUsers(Integer pageNum, Integer pageSize,String keyword,Long roleId);
/**
* 根据用户ID查询用户信息
*
* @param id 用户ID
* @return 用户信息
*/
UserProfileResponse findUserWithRolesById(Long id);
}

View File

@@ -0,0 +1,23 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.dto.request.SetUserRoleRequest;
import java.util.List;
/**
* 用户角色服务
*/
public interface UserRoleService {
/**
* 为用户设置角色
* @param request 请求参数
*/
void assignRolesToUser(SetUserRoleRequest request);
/**
* 取消用户角色
* @param request 角色id组
*/
int revokeRolesFromUser(SetUserRoleRequest request);
}

View File

@@ -50,5 +50,12 @@
<artifactId>spring-web</artifactId>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-crypto</artifactId>
<version>6.5.6</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>

View File

@@ -1,13 +0,0 @@
package cn.nopj.chaos_api.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
@Configuration
public class AppConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate ();
}
}

View File

@@ -0,0 +1,228 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.Device;
import cn.nopj.chaos_api.domain.entity.InterfaceAddressConfig;
import cn.nopj.chaos_api.domain.entity.InterfaceDnsMapping;
import cn.nopj.chaos_api.domain.entity.NetworkInterface;
import cn.nopj.chaos_api.dto.request.CreateDeviceRequest;
import cn.nopj.chaos_api.dto.request.DeviceQueryRequest;
import cn.nopj.chaos_api.dto.request.UpdateDeviceRequest;
import cn.nopj.chaos_api.dto.response.DeviceResponse;
import cn.nopj.chaos_api.mapper.DeviceMapper;
import cn.nopj.chaos_api.mapper.InterfaceAddressConfigMapper;
import cn.nopj.chaos_api.mapper.InterfaceDnsMappingMapper;
import cn.nopj.chaos_api.mapper.NetworkInterfaceMapper;
import cn.nopj.chaos_api.service.DeviceService;
import cn.nopj.chaos_api.service.DnsServerService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.util.List;
import java.util.Objects;
@Service
@Slf4j
public class DeviceServiceImpl extends ServiceImpl<DeviceMapper, Device> implements DeviceService {
@Autowired
NetworkInterfaceMapper networkInterfaceMapper;
@Autowired
InterfaceAddressConfigMapper interfaceAddressConfigMapper;
@Autowired
DnsServerService dnsServerService;
@Autowired
InterfaceDnsMappingMapper interfaceDnsMappingMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public DeviceResponse createDevice(CreateDeviceRequest createDeviceRequest) {
Device device = new Device();
BeanUtils.copyProperties(createDeviceRequest, device);
int rows = this.baseMapper.insert(device);
List<CreateDeviceRequest.NetworkInterfaceRequest> interfaces = createDeviceRequest.getInterfaces();
if (interfaces == null || interfaces.isEmpty()) {
return new DeviceResponse(device);
}
interfaces.forEach(i -> {
NetworkInterface networkInterface = new NetworkInterface();
networkInterface.setDeviceId(device.getId());
networkInterface.setName(i.getName());
networkInterface.setType(i.getType());
networkInterface.setMacAddress(i.getMacAddress());
networkInterface.setPortSpeed(i.getPortSpeed());
networkInterface.setDuplex(i.getDuplex());
networkInterface.setRemark(i.getRemark());
networkInterfaceMapper.insert(networkInterface);
List<CreateDeviceRequest.InterfaceAddressConfigRequest> addressConfigs = i.getAddressConfigs();
if (addressConfigs == null || addressConfigs.isEmpty()) {
return;
}
addressConfigs.forEach(a -> {
InterfaceAddressConfig iac = new InterfaceAddressConfig();
iac.setInterfaceId(networkInterface.getId());
iac.setVlanId(a.getVlanId());
iac.setIpAddress(a.getIpAddress());
iac.setGatewayIp(a.getGatewayIp());
iac.setSubnetMask(a.getSubnetMask());
iac.setBroadcastAddress(a.getBroadcastAddress());
iac.setIsPrimary(a.getIsPrimary());
iac.setIsDhcp(a.getIsDhcp());
iac.setMtu(a.getMtu());
interfaceAddressConfigMapper.insert(iac);
List<String> dnsServers = a.getDnsServers();
if (dnsServers == null || dnsServers.isEmpty()) {
return;
}
List<Long> dnsServerIdListByNameList = dnsServerService.getDnsServerIdListByNameList(dnsServers);
for (int j = 0; j < dnsServerIdListByNameList.size(); j++) {
InterfaceDnsMapping mapping = new InterfaceDnsMapping();
mapping.setConfigId(iac.getId());
mapping.setDnsServerId(dnsServerIdListByNameList.get(j));
mapping.setPriority(j);
interfaceDnsMappingMapper.insert(mapping);
}
});
});
if (rows > 0){
return new DeviceResponse(device);
}else {
throw new BizException("添加设备失败");
}
}
@Override
public DeviceResponse getDeviceById(Long id) {
Device device = this.baseMapper.selectOneWithTypeById(id);
if (device == null){
throw new BizException(ErrorCode.DEVICE_NOT_FOUND);
}
return new DeviceResponse(device);
}
@Override
@Transactional
public void deleteDevice(Long id) {
int i = this.baseMapper.deleteDevice(id);
if (i <= 0){
throw new BizException(ErrorCode.DEVICE_DELETE_FAILED);
}
}
@Override
@Transactional
public void deleteDeviceReal(Long id) {
log.info("删除设备id:{}",id);
int i = this.baseMapper.deleteById(id);
if (i <= 0){
throw new BizException(ErrorCode.DEVICE_DELETE_FAILED);
}
log.info("删除设备成功");
}
public IPage<DeviceResponse> getAllDevices(DeviceQueryRequest request) {
if (request == null) {
request = new DeviceQueryRequest();
}
long current = Objects.requireNonNullElse(request.getPageNum(), 1);
long size = Objects.requireNonNullElse(request.getPageSize(), 10);
Page<Device> page = new Page<>(current, size);
LambdaQueryWrapper<Device> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(request.getType() != null && request.getType() > 0, Device::getTypeId, request.getType());
if (StringUtils.hasText(request.getKeyword())) {
String keyword = request.getKeyword();
wrapper.and(w -> w
.like(Device::getName, keyword)
.or()
.like(Device::getModel, keyword)
.or()
.like(Device::getManufacturer, keyword)
.or()
.like(Device::getRemark, keyword)
.or()
.like(Device::getSnmpCommunity, keyword)
);
}
IPage<Device> deviceIPage = this.baseMapper.selectPage(page, wrapper);
return deviceIPage.convert(DeviceResponse::new);
}
@Override
public DeviceResponse updateDevice(UpdateDeviceRequest updateDeviceRequest) {
Device device = new Device();
//判断各字段是否为空 不为空再更新
if (updateDeviceRequest.getId() != null){
device.setId(updateDeviceRequest.getId());
}
if (updateDeviceRequest.getName() != null){
device.setName(updateDeviceRequest.getName());
}
if (updateDeviceRequest.getModel() != null){
device.setModel(updateDeviceRequest.getModel());
}
if (updateDeviceRequest.getTypeId() != null){
device.setTypeId(updateDeviceRequest.getTypeId());
}
if (updateDeviceRequest.getLocationId() != null){
device.setLocationId(updateDeviceRequest.getLocationId());
}
if (updateDeviceRequest.getSnmpCommunity() != null){
device.setSnmpCommunity(updateDeviceRequest.getSnmpCommunity());
}
if (updateDeviceRequest.getManufacturer() != null){
device.setManufacturer(updateDeviceRequest.getManufacturer());
}
if (updateDeviceRequest.getPurchaseDate() != null){
device.setPurchaseDate(updateDeviceRequest.getPurchaseDate());
}
if (updateDeviceRequest.getStatus() >= 0 && updateDeviceRequest.getStatus() <= 2){
device.setStatus(updateDeviceRequest.getStatus());
}
if (updateDeviceRequest.getRemark() != null){
device.setRemark(updateDeviceRequest.getRemark());
}
int rows = this.baseMapper.updateById(device);
if (rows > 0){
return new DeviceResponse(device);
}else {
throw new BizException("更新设备失败");
}
}
}

View File

@@ -0,0 +1,37 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.DeviceType;
import cn.nopj.chaos_api.dto.response.DeviceTypeResponse;
import cn.nopj.chaos_api.dto.response.OptionResponse;
import cn.nopj.chaos_api.mapper.DriveTypeMapper;
import cn.nopj.chaos_api.service.DeviceTypeService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class DeviceTypeServiceImpl extends ServiceImpl<DriveTypeMapper, DeviceType> implements DeviceTypeService {
@Override
public List<DeviceTypeResponse> getAllDeviceTypes() {
List<DeviceType> deviceTypes = this.baseMapper.selectList(null);
if (deviceTypes == null || deviceTypes.isEmpty()){
throw new BizException(ErrorCode.DEVICE_TYPE_NOT_FOUND);
}
return deviceTypes.stream().map(DeviceTypeResponse::new).toList();
}
@Override
public List<OptionResponse> getDeviceTypeOptions() {
return this.lambdaQuery()
.select(DeviceType::getId, DeviceType::getName)
.list()
.stream()
.map(deviceType -> new OptionResponse(deviceType.getName(), deviceType.getId().toString()))
.toList();
}
}

View File

@@ -0,0 +1,50 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.DnsServer;
import cn.nopj.chaos_api.mapper.DnsServerMapper;
import cn.nopj.chaos_api.service.DnsServerService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Service
public class DnsServerServiceImpl extends ServiceImpl<DnsServerMapper, DnsServer> implements DnsServerService {
@Override
public Long getDnsServerIdByName(String dnsAddress) {
if (dnsAddress == null || dnsAddress.isEmpty()){
throw new BizException(ErrorCode.DNS_NOT_FOUND);
}
DnsServer dnsServer = this.baseMapper.selectOne(new LambdaQueryWrapper<DnsServer>().eq(DnsServer::getName, dnsAddress));
if (dnsServer == null ){
throw new BizException(ErrorCode.DNS_NOT_DEFINED);
}
return dnsServer.getId();
}
@Override
public List<Long> getDnsServerIdListByNameList(List<String> nameList) {
List<DnsServer> dnsServers = this.baseMapper.selectList(null);
if (dnsServers == null || dnsServers.isEmpty() ){
throw new BizException(ErrorCode.DB_DNS_NOT_FOUND);
}
Map<String, Long> map = new HashMap<>();
for (DnsServer dnsServer : dnsServers) {
map.put(dnsServer.getName(), dnsServer.getId());
}
return nameList.stream().map(map::get).toList();
}
}

View File

@@ -0,0 +1,18 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.dto.response.FileUploadResponse;
import cn.nopj.chaos_api.service.ImageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
@Slf4j
@Service
public class ImageServiceImpl implements ImageService {
@Override
public FileUploadResponse uploadImage(String fileName, InputStream content) {
//todo 完成上传图片功能
return new FileUploadResponse();
}
}

View File

@@ -0,0 +1,24 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.domain.entity.Role;
import cn.nopj.chaos_api.dto.response.OptionResponse;
import cn.nopj.chaos_api.mapper.RoleMapper;
import cn.nopj.chaos_api.service.RoleService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements RoleService {
@Override
public List<OptionResponse> getAllRoles() {
return this.lambdaQuery()
.select(Role::getId, Role::getName)
.list()
.stream()
.map(role -> new OptionResponse(role.getName(), role.getId().toString()))
.toList();
}
}

View File

@@ -0,0 +1,179 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.dto.request.UserProfileUpdateRequest;
import cn.nopj.chaos_api.dto.response.UserProfileResponse;
import cn.nopj.chaos_api.mapper.UserMapper;
import cn.nopj.chaos_api.service.UserProfileService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Slf4j
@Service
public class UserProfileServiceImpl implements UserProfileService {
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
UserMapper userMapper;
@Override
public void setUserPassword(Long Id, String password) {
log.info("重置用户id: {}的密码", Id);
User user = userMapper.selectById(Id);
//查询用户是否存在
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
user.setPassword(passwordEncoder.encode(password));
int i = userMapper.updateById(user);
log.info("重置用户id: {}的密码结果: {}", Id, i);
}
@Override
public List<UserProfileResponse> getAllUsers() {
List<User> allUserWithRoles = userMapper.findAllUserWithRoles();
if (allUserWithRoles == null){
throw new RuntimeException("获取所有用户信息失败");
}
return allUserWithRoles.stream().map(UserProfileResponse::new).toList();
}
@Override
public void setUserPassword(String userId, String password) {
try{
Long Id = Long.parseLong(userId);
setUserPassword(Id, password);
}catch (NumberFormatException e) {
throw new BizException(ErrorCode.USER_ID_INVALID);
}
}
@Override
public void updateUsername(String username, String newUsername) {
if (newUsername == null || newUsername.isEmpty()){
throw new BizException(ErrorCode.USER_UPDATE_USERNAME_FAILED);
}
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
user.setUsername(newUsername);
int i = userMapper.updateById(user);
log.info("更新用户名结果: {}", i);
if (i == 0) {
throw new BizException(ErrorCode.USER_UPDATE_USERNAME_FAILED);
}
}
@Override
public UserProfileResponse findUserWithRolesByUsername(String username) {
if (username == null || username.isEmpty()){
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
log.info("查询用户名: {}", username);
User user = userMapper.findUserWithRolesByUsername(username);
log.info("查询用户名: {} 结果: {}", username, user);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
return new UserProfileResponse(user);
}
@Override
public UserProfileResponse setUserNickname(String username, String nickname) {
User user = userMapper.selectByUsername(username);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
user.setNickname(nickname);
int i = userMapper.updateById(user);
log.info("更新用户昵称结果: {}", i);
if (i == 0) {
throw new BizException(ErrorCode.USER_UPDATE_NICKNAME_FAILED);
}
return new UserProfileResponse(user);
}
@Override
public UserProfileResponse setUserNickname(Long userId, String nickname) {
User user = userMapper.selectById(userId);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
user.setNickname(nickname);
int i = userMapper.updateById(user);
log.info("更新用户昵称结果: {}", i);
if (i == 0) {
throw new BizException(ErrorCode.USER_UPDATE_NICKNAME_FAILED);
}
return new UserProfileResponse(user);
}
@Override
@Transactional
public UserProfileResponse updateUserProfile(String username, UserProfileUpdateRequest request) {
User user = userMapper.findUserWithRolesByUsername(username);
if (user == null) {
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
if (request.getNickname() != null){
user.setNickname(request.getNickname());
}
if (request.getPassword() != null){
user.setPassword(passwordEncoder.encode(request.getPassword()));
}
int i = userMapper.updateById(user);
log.info("更新用户信息结果: {}", i);
if (i == 0) {
throw new BizException(ErrorCode.USER_UPDATE_PROFILE_FAILED);
}
return new UserProfileResponse(user);
}
@Override
public IPage<UserProfileResponse> getAllUsers(Integer pageNum, Integer pageSize, String keyword, Long roleId) {
Page<User> page = new Page<>(pageNum, pageSize);
IPage<User> userPage = userMapper.selectPageWithRoles(page, keyword,roleId);
return userPage.convert(UserProfileResponse::new);
}
@Override
public UserProfileResponse findUserWithRolesById(Long id) {
if (id == null){
throw new BizException(ErrorCode.USER_ID_INVALID);
}
User userWithRolesById = userMapper.findUserWithRolesById(id);
if (userWithRolesById == null){
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
return new UserProfileResponse(userWithRolesById);
}
}

View File

@@ -0,0 +1,115 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.UserRole;
import cn.nopj.chaos_api.dto.request.SetUserRoleRequest;
import cn.nopj.chaos_api.mapper.UserRoleMapper;
import cn.nopj.chaos_api.service.UserRoleService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Service
// 继承 ServiceImpl<UserRoleMapper, UserRole> 以获得批量操作能力
public class UserRoleServiceImpl extends ServiceImpl<UserRoleMapper, UserRole> implements UserRoleService {
/**
* 批量分配角色给用户。
*/
@Transactional(rollbackFor = Exception.class)
@Override
public void assignRolesToUser(SetUserRoleRequest request) {
if (request == null || request.getUserId() == null || request.getUserId() <= 0L){
throw new BizException(ErrorCode.USER_ID_INVALID);
}
Long userId = request.getUserId();
if (request.getRolesId() == null || request.getRolesId().isEmpty()){
this.baseMapper.delete(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId,userId));
return;
}
List<Long> rolesId = request.getRolesId();
// 1. 数据预校验确保没有无效角色ID (0或null)
if (rolesId.stream().anyMatch(roleId -> roleId == null || roleId <= 0L)) {
throw new BizException(ErrorCode.ROLE_ID_INVALID);
}
// 2. 构建批量插入列表
List<UserRole> userRoleList = rolesId.stream()
.map(roleId -> {
var userRole = new UserRole();
userRole.setUserId(userId);
userRole.setRoleId(roleId);
return userRole;
})
.collect(Collectors.toList());
try {
// 替代:先删除用户所有旧角色,再插入新的角色集合 (常用授权模式)
this.remove(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, userId));
// 保持原有逻辑:只添加新的角色
boolean success = this.saveBatch(userRoleList);
if (!success) {
// saveBatch 返回 false通常是 MyBatis-Plus 内部错误或部分失败
throw new BizException("批量分配角色操作失败。");
}
} catch (DataIntegrityViolationException e) {
// 捕获数据完整性约束失败 (如外键约束失败: role_id 不存在)
log.error("分配角色时外键约束失败: userId={}, rolesId={}", userId, rolesId, e);
// 给出更具体的提示,可能需要检查是哪个 role_id 不存在
throw new BizException("分配角色失败列表中包含一个或多个不存在的角色ID。");
}
}
/**
* 批量解除用户的角色关联。
* 优化点: 使用 MyBatis-Plus 的批量删除,而不是在循环中单次删除。
* @param request 包含用户ID和角色ID列表
* @return 实际解除关联的记录数
*/
@Transactional(rollbackFor = Exception.class)
@Override
public int revokeRolesFromUser(SetUserRoleRequest request) {
if (request == null || request.getUserId() == null || request.getUserId() <= 0L){
throw new BizException(ErrorCode.USER_ID_INVALID);
}
if (request.getRolesId() == null || request.getRolesId().isEmpty()){
throw new BizException(ErrorCode.ROLE_ID_INVALID);
}
Long userId = request.getUserId();
List<Long> rolesId = request.getRolesId();
LambdaQueryWrapper<UserRole> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(UserRole::getUserId, userId)
.in(UserRole::getRoleId, rolesId); // 批量删除指定的角色ID
int count = this.baseMapper.delete(wrapper);
if (count == 0 && !rolesId.isEmpty()){
// 如果用户传入了角色ID列表但删除了 0 行,可能是这些角色未分配给该用户
log.warn("用户 {} 解除角色 {} 失败0条记录被删除。", userId, rolesId);
throw new BizException("解除角色失败,用户可能未分配这些角色。");
}
return count;
}
}

View File

@@ -44,7 +44,6 @@
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
@@ -69,7 +68,6 @@
<scope>provided</scope>
</dependency>
</dependencies>
<build>

View File

@@ -1,9 +1,11 @@
package cn.nopj.chaos_api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication(scanBasePackages = "cn.nopj.chaos_api")
@Slf4j
public class ChaosApiWebApplication {
public static void main(String[] args) {
SpringApplication.run(ChaosApiWebApplication.class, args);

View File

@@ -0,0 +1,9 @@
package cn.nopj.chaos_api.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Configuration
@EnableTransactionManagement
public class AppConfig {
}

View File

@@ -0,0 +1,21 @@
package cn.nopj.chaos_api.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@MapperScan("cn.nopj.chaos_api.mapper")
public class MybatisPlusConfig {
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MARIADB));
return interceptor;
}
}

View File

@@ -47,13 +47,13 @@ public class SecurityConfig {
return authenticationConfiguration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
// 允许所有对 /api/public/** 的匿名访问
.requestMatchers("/api/auth/login","/api/auth/register").permitAll()
.requestMatchers("/api/public/*","/api/auth/login","/api/auth/register").permitAll()
.anyRequest().authenticated()
)
// 禁用 CSRF因为现代前后端分离项目通常使用 Token
@@ -75,8 +75,7 @@ public class SecurityConfig {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList(
"http://localhost:63342", // 你的前端运行的地址
"http://localhost:8080", // 其他可能的前端地址
"http://localhost:5173", // 其他可能的前端地址
"http://127.0.0.1:5500", // 另一个常见的本地开发地址
"null" // 如果是从文件系统直接打开HTML文件Origin 会是 "null"。仅用于开发!
));

View File

@@ -38,10 +38,8 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws ServletException, IOException {
log.info("JWT 登录授权过滤器");
String authHeader = request.getHeader(this.tokenHeader);
log.info("authHeader: {}", authHeader);
if (authHeader != null && authHeader.startsWith(this.tokenHead)) {
String authToken = authHeader.substring(this.tokenHead.length()+1);
log.info("authToken={}", authToken);
@@ -61,7 +59,7 @@ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
log.info("authorities: {}", authorities);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(username, null, authorities);
request.setAttribute("currentUsername", username);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}

View File

@@ -18,7 +18,7 @@ public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ApiResult<Object> result = ApiResult.failed("未授权");
ApiResult<Object> result = ApiResult.failed(403,"权限不足");
String string = JSONObject.toJSONString(result);
response.getWriter().print(string);

View File

@@ -1,61 +1,50 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.LoginRequest;
import cn.nopj.chaos_api.dto.RegisterRequest;
import cn.nopj.chaos_api.converter.UserConverter;
import cn.nopj.chaos_api.dto.request.AuthLoginRequest;
import cn.nopj.chaos_api.dto.request.AuthRegisterRequest;
import cn.nopj.chaos_api.dto.response.AuthTokenResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.AuthService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
/**
* 认证管理
*
*/
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthService authService;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Autowired
private UserConverter userConverter;
/**
* 注册
*
* @param registerRequest 注册信息
* @param authRegisterRequest 注册信息
* @return 注册结果
*/
@PostMapping("/register")
public ApiResult<?> register(@RequestBody RegisterRequest registerRequest) {
if (authService.register(registerRequest) != null) {
return ApiResult.success("注册成功");
}
return ApiResult.failed("用户名已存在");
public ApiResult<?> register(@RequestBody @Validated AuthRegisterRequest authRegisterRequest) {
return ApiResult.success(authService.register(userConverter.convert(authRegisterRequest)));
}
/**
* 登录
*
* @param loginRequest 登录信息
* @param authLoginRequest 登录信息
* @return 登录结果
*/
@PostMapping("/login")
public ApiResult<?> login(@RequestBody LoginRequest loginRequest) {
try {
String token = authService.login(loginRequest.getUsername(), loginRequest.getPassword());
Map<String, String> tokenMap = new HashMap<>();
tokenMap.put("token", token);
tokenMap.put("tokenHead", tokenHead);
return ApiResult.success(tokenMap);
}catch (BadCredentialsException e){
return ApiResult.failed(e.getMessage());
}
public ApiResult<AuthTokenResponse> login(@RequestBody @Validated AuthLoginRequest authLoginRequest) {
return ApiResult.success(authService.login(authLoginRequest.getUsername(), authLoginRequest.getPassword()));
}
}

View File

@@ -0,0 +1,79 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.request.CreateDeviceRequest;
import cn.nopj.chaos_api.dto.request.DeviceQueryRequest;
import cn.nopj.chaos_api.dto.request.UpdateDeviceRequest;
import cn.nopj.chaos_api.dto.response.DeviceResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.DeviceService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 设备管理
*/
@Slf4j
@RestController
@RequestMapping("/api/devices")
public class DeviceController {
private final DeviceService deviceService;
public DeviceController(DeviceService deviceService) {
this.deviceService = deviceService;
}
/**
* 查询所有设备信息
* @return 所有设备信息
*/
@GetMapping
public ApiResult<IPage<DeviceResponse>> getAllDevices(DeviceQueryRequest request){
return ApiResult.success(deviceService.getAllDevices(request));
}
/**
* 新建设备信息
* @param createDeviceRequest 设备信息
* @return 新建设备信息结果
*/
@PostMapping
public ApiResult<DeviceResponse> createDevice(@RequestBody @Validated CreateDeviceRequest createDeviceRequest){
log.info("创建设备信息: {}", createDeviceRequest);
return ApiResult.success(deviceService.createDevice(createDeviceRequest));
}
/**
* 查询指定设备信息
* @return 指定设备信息
*/
@GetMapping("/{id}")
public ApiResult<DeviceResponse> getDeviceById(@PathVariable("id") Long id){
return ApiResult.success(deviceService.getDeviceById(id));
}
/**
* 删除指定设备信息
* @return 逻辑删除指定设备信息结果
*/
@DeleteMapping("/{id}")
public ApiResult<String> deleteDevice(@PathVariable("id") Long id){
deviceService.deleteDevice(id);
return ApiResult.success("删除成功");
}
/**
* 修改指定设备信息
* @return 修改指定设备信息结果
*/
@PutMapping({"/{id}"})
public ApiResult<DeviceResponse> updateDevice(@RequestBody @Validated UpdateDeviceRequest updateDeviceRequest){
return ApiResult.success(deviceService.updateDevice(updateDeviceRequest));
}
}

View File

@@ -0,0 +1,45 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.response.DeviceTypeResponse;
import cn.nopj.chaos_api.dto.response.OptionResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.DeviceTypeService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* 设备类型管理
*
* @author nopj
*/
@RestController
@RequestMapping("/api/device-types")
public class DeviceTypeController {
private final DeviceTypeService deviceTypeService;
public DeviceTypeController(DeviceTypeService deviceTypeService) {
this.deviceTypeService = deviceTypeService;
}
/**
* 获取所有设备类型
*
* @return 所有设备类型
*/
@GetMapping
public ApiResult<List<DeviceTypeResponse>> getAllDeviceTypes() {
return ApiResult.success(deviceTypeService.getAllDeviceTypes());
}
/**
* 获取所有设备类型选项
* @return 所有设备类型选项
*/
@GetMapping("/options")
public ApiResult<List<OptionResponse>> getDeviceTypeOptions() {
return ApiResult.success(deviceTypeService.getDeviceTypeOptions());
}
}

View File

@@ -1,20 +0,0 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/api/hls")
public class HLSController {
@PreAuthorize("hasAuthority('admin')")
@GetMapping("/")
ApiResult<String> getHLS(){
return ApiResult.success("HLS is radar");
}
}

View File

@@ -0,0 +1,46 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.response.FileUploadResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.ImageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.io.InputStream;
/**
* 图片管理
*/
@Slf4j
@RestController
@RequestMapping("/api/image")
public class ImageController {
@Autowired
private ImageService imageService;
/**
* 上传图片
* @param file 文件
* @return 上传结果
*/
@RequestMapping("/upload")
ApiResult<String> uploadImage(@RequestParam("file") MultipartFile file) {
if (file.isEmpty()){
return ApiResult.failed("上传文件不能为空");
}
try {
String fileName = file.getOriginalFilename();
InputStream inputStream = file.getInputStream();
FileUploadResponse response = imageService.uploadImage(fileName, inputStream);
return ApiResult.success(response.getFileDownloadUri());
} catch (IOException e) {
log.error("上传文件失败", e);
return ApiResult.failed("上传文件失败");
}
}
}

View File

@@ -1,13 +1,65 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.request.SetUserRoleRequest;
import cn.nopj.chaos_api.dto.response.OptionResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.RoleService;
import cn.nopj.chaos_api.service.UserRoleService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 角色管理
*/
@Slf4j
@RestController
@RequestMapping("/api/roles")
public class RoleController {
@Autowired
private UserRoleService userRoleService;
@Autowired
private RoleService roleService;
/**
* 获取用户角色选项
*/
@GetMapping("/options")
public ApiResult<List<OptionResponse>> getUserRole() {
return ApiResult.success(roleService.getAllRoles());
}
/**
* 设置用户角色
* @param request 请求参数
* @return 处理结果
*/
@PreAuthorize("hasAuthority('admin')")
@PostMapping("/users")
public ApiResult<String> assignRolesToUser(@RequestBody @Validated SetUserRoleRequest request) {
userRoleService.assignRolesToUser(request);
return ApiResult.success("用户角色设置成功");
}
/**
* 取消用户角色
* @param request 请求参数
* @return 处理结果
*/
@PreAuthorize("hasAuthority('admin')")
@DeleteMapping("/users")
public ApiResult<?> revokeRolesFromUser(@RequestBody @Validated SetUserRoleRequest request) {
return ApiResult.success("用户角色取消成功",userRoleService.revokeRolesFromUser(request));
}
}

View File

@@ -1,13 +1,108 @@
package cn.nopj.chaos_api.controller;
import cn.nopj.chaos_api.dto.request.SetUserNicknameRequest;
import cn.nopj.chaos_api.dto.request.SetUserPasswordRequest;
import cn.nopj.chaos_api.dto.request.UpdateUsernameRequest;
import cn.nopj.chaos_api.dto.request.UserProfileUpdateRequest;
import cn.nopj.chaos_api.dto.response.UserProfileResponse;
import cn.nopj.chaos_api.model.ApiResult;
import cn.nopj.chaos_api.service.UserProfileService;
import com.baomidou.mybatisplus.core.metadata.IPage;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 用户管理
*
* @author nopj
*/
@Slf4j
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserProfileService userProfileService;
/**
* 获取用户列表 (分页)
* URL: GET /api/users?pageNum=1&pageSize=10
*/
@PreAuthorize("hasAuthority('admin')")
@GetMapping
public ApiResult<IPage<UserProfileResponse>> listUsers(
@RequestParam(defaultValue = "1", name = "pageNum") Integer pageNum,
@RequestParam(defaultValue = "10", name = "pageSize") Integer pageSize,
@RequestParam(required = false,name = "keyword") String keyword,
@RequestParam(required = false,name = "roleId") Long roleId
) {
log.info("获取用户列表 (分页): pageNum={}, pageSize={}, keyword={}, roleId={}" , pageNum, pageSize, keyword, roleId);
return ApiResult.success(userProfileService.getAllUsers(pageNum, pageSize, keyword, roleId));
}
@GetMapping("/{id}")
public ApiResult<UserProfileResponse> getUserProfile(@PathVariable Long id) {
return ApiResult.success(userProfileService.findUserWithRolesById(id));
}
/**
* 管理员重置指定用户的密码
* URL: PUT /api/users/{userId}/password
*/
@PreAuthorize("hasAuthority('admin')")
@PutMapping("/{userId}/password")
public ApiResult<String> resetUserPassword(
@PathVariable Long userId,
@RequestBody @Validated SetUserPasswordRequest request) {
userProfileService.setUserPassword(userId, request.getPassword());
return ApiResult.success("用户密码修改成功");
}
/**
* 获取当前登录用户信息
* URL: GET /api/users/me (或 /api/users/profile)
*/
@GetMapping("/me")
public ApiResult<UserProfileResponse> getCurrentUserProfile(@RequestAttribute("currentUsername") String username) {
return ApiResult.success(userProfileService.findUserWithRolesByUsername(username));
}
/**
* 更新当前用户的基础信息
* URL: PATCH /api/users/me
*/
@PatchMapping("/me")
public ApiResult<UserProfileResponse> updateMyProfile(
@RequestAttribute("currentUsername") String username,
@RequestBody @Validated UserProfileUpdateRequest request) {
return ApiResult.success(userProfileService.updateUserProfile(username, request));
}
/**
* 更新当前用户的用户名
* URL: PUT /api/users/me/username
* @deprecated 修改用户名会导致 Token 失效
*/
@Deprecated
@PutMapping("/me/username")
public ApiResult<String> updateMyUsername(
@RequestAttribute("currentUsername") String username,
@RequestBody @Validated UpdateUsernameRequest request) {
userProfileService.updateUsername(username, request.getUsername());
return ApiResult.success("用户名更新成功");
}
/**
* 管理员修改用户昵称
* URL: PUT /api/users/{userId}/nickname
*/
@PreAuthorize("hasAuthority('admin')")
@PutMapping("/{userId}/nickname")
public ApiResult<UserProfileResponse> setUserNickname(
@PathVariable Long userId,
@RequestBody @Validated SetUserNicknameRequest request) {
return ApiResult.success(userProfileService.setUserNickname(userId, request.getNickname()));
}
}

View File

@@ -0,0 +1,53 @@
package cn.nopj.chaos_api.exception;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.model.ApiResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.Objects;
@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResult<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
// 提取第一条错误提示
String errorMsg = Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage();
// 返回统一的错误格式
return ApiResult.failed(errorMsg);
}
/**
* 处理自定义业务异常
*/
@ExceptionHandler(BizException.class)
public ApiResult<?> handleBizException(BizException ex) {
return ApiResult.failed(ex.getErrorCode().getHttpStatus(),ex.getMessage());
}
/**
* 兜底处理:处理所有未知的 RuntimeException
* 防止出现 500 错误页面,给前端返回友好的提示
*/
@ExceptionHandler(Exception.class)
public ApiResult<?> handleAllExceptions(Exception ex) {
log.error("服务器内部错误", ex);
return ApiResult.failed("服务器内部错误,请联系管理员");
}
/**
* 处理权限不足异常
*/
@ExceptionHandler(AuthorizationDeniedException.class)
public ApiResult<?> handleAuthorizationDeniedException(AuthorizationDeniedException ex) {
return ApiResult.failed("权限不足,请求已登记");
}
}

View File

@@ -0,0 +1,96 @@
package cn.nopj.chaos_api.security;
import cn.nopj.chaos_api.domain.entity.Role;
import cn.nopj.chaos_api.domain.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
public class CustomUserDetails implements UserDetails {
private final User user;
private final Collection<? extends GrantedAuthority> authorities;
public CustomUserDetails(User user){
this.user = user;
List<Role> roles = user.getRoles();
this.authorities = roles.stream().map(role -> new SimpleGrantedAuthority(role.getCode())).toList();
}
/**
* 获取该用户的权限集合
* Spring Security 会根据这些权限进行鉴权(如 @PreAuthorize
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
/**
* 账户是否未过期
* 返回 true 表示未过期false 表示已过期(无法登录)
*/
@Override
public boolean isAccountNonExpired() {
return user.getAccountNonExpired(); // 这里可以根据业务逻辑修改,例如判断 expire_time
}
/**
* 账户是否未锁定
* 返回 true 表示未锁定
*/
@Override
public boolean isAccountNonLocked() {
return user.getAccountNonLocked(); // 可根据由登录失败次数判断锁定逻辑
}
/**
* 凭证(密码)是否未过期
* 返回 true 表示未过期
*/
@Override
public boolean isCredentialsNonExpired() {
return user.getCredentialsNonExpired(); // 可根据密码最后修改时间判断
}
/**
* 账户是否可用
* 通常对应数据库中的 status 或 enabled 字段
*/
@Override
public boolean isEnabled() {
return user.getEnabled();
}
/**
* 获取用户ID
*/
public Long getUserId() {
return user.getId();
}
/**
* 获取用户昵称
*/
public String getNickname() {
return user.getNickname();
}
/**
* 获取用户头像
*/
public String getAvatar() {
return user.getAvatar();
}
}

View File

@@ -1,17 +1,17 @@
package cn.nopj.chaos_api.service;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.dto.RegisterRequest;
import cn.nopj.chaos_api.dto.response.AuthTokenResponse;
public interface AuthService {
/**
* 注册
*/
User register(RegisterRequest registerRequest);
User register(User user);
/**
* 登录
* @return 生成的 JWT token
*/
String login(String username, String password);
AuthTokenResponse login(String username, String password);
}

View File

@@ -1,25 +1,32 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.Role;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.dto.RegisterRequest;
import cn.nopj.chaos_api.dto.response.AuthTokenResponse;
import cn.nopj.chaos_api.dto.response.UserProfileResponse;
import cn.nopj.chaos_api.mapper.RoleMapper;
import cn.nopj.chaos_api.mapper.UserMapper;
import cn.nopj.chaos_api.security.CustomUserDetails;
import cn.nopj.chaos_api.service.AuthService;
import cn.nopj.chaos_api.util.JwtTokenUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@Service
@Slf4j
public class AuthServiceImpl implements AuthService {
@Autowired
private UserMapper userMapper;
@@ -29,37 +36,70 @@ public class AuthServiceImpl implements AuthService {
private JwtTokenUtil jwtTokenUtil;
@Autowired
private AuthenticationManager authenticationManager;
@Value("${jwt.tokenHead}")
private String tokenHead;
@Value("${auth.def-role-code}")
private String defRoleCode;
@Autowired
private RoleMapper roleMapper;
@Override
public User register(RegisterRequest registerRequest) {
@Transactional(rollbackFor = Exception.class)
public User register(User user) {
// 检查用户名是否已存在
if (userMapper.selectOne(new QueryWrapper<User>().eq("username", registerRequest.getUsername())) != null) {
if (userMapper.selectByUsername(user.getUsername()) != null) {
// 在实际项目中,这里应该抛出自定义异常
return null;
throw new BizException(ErrorCode.USERNAME_EXISTS);
}
User user = new User();
user.setUsername(registerRequest.getUsername());
// 使用 BCrypt 加密密码
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
user.setPassword(passwordEncoder.encode(user.getPassword()));
userMapper.insert(user);
// 你可以在这里为新用户分配默认角色
userMapper.insertUserRole(user.getId(), 2L);
Role role = roleMapper.selectByCode(defRoleCode);
userMapper.insertUserRole(user.getId(), role.getId());
log.info("用户注册成功: {}", user.getUsername());
return user;
}
@Override
public String login(String username, String password) {
public AuthTokenResponse login(String username, String password) {
log.info("用户登录: {}", username);
// 尝试进行身份验证
try{
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
log.info("用户登录成功: {}", username);
// 将认证结果保存在 SecurityContextHolder 中
SecurityContextHolder.getContext().setAuthentication(authentication);
CustomUserDetails userDetails = (CustomUserDetails)authentication.getPrincipal();
// 获取用户详情
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
if (!userDetails.isEnabled()){
return null;
if (!userDetails.isEnabled()) {
throw new BizException(ErrorCode.USER_NOT_ENABLED);
}
// 生成 JWT
return jwtTokenUtil.generateToken(userDetails);
AuthTokenResponse res = new AuthTokenResponse();
res.setToken(jwtTokenUtil.generateToken(userDetails));
res.setTokenHead(tokenHead);
return res;
}catch (BadCredentialsException | InternalAuthenticationServiceException e) {
log.error("用户登录失败",e);
throw new BizException(ErrorCode.USER_NOT_EXISTS_OR_PASSWORD_WRONG);
} catch (DisabledException e) {
throw new BizException(ErrorCode.USER_NOT_ENABLED);
} catch (BizException e){
throw e;
}
catch (Exception e) {
log.error("用户登录异常",e);
throw new BizException(ErrorCode.SYSTEM_ERROR);
}
}
}

View File

@@ -1,15 +1,17 @@
package cn.nopj.chaos_api.service.impl;
import cn.nopj.chaos_api.common.constants.ErrorCode;
import cn.nopj.chaos_api.common.exceotion.BizException;
import cn.nopj.chaos_api.domain.entity.User;
import cn.nopj.chaos_api.mapper.UserMapper;
import cn.nopj.chaos_api.security.CustomUserDetails;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import java.util.List;
@@ -22,31 +24,13 @@ public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
public CustomUserDetails loadUserByUsername(String username) {
// 1. 从数据库查询用户
User user = userMapper.selectOne(new QueryWrapper<User>().eq("username", username));
User user = userMapper.findUserWithRolesByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
throw new BizException(ErrorCode.USER_NOT_EXISTS);
}
// 2. 查询该用户的权限信息 (角色 + 权限)
List<String> authorities = userMapper.findAuthoritiesByUsername(username);
log.info("用户权限列表: {}", authorities);
// 3. 将权限字符串列表转换为 GrantedAuthority 集合
List<GrantedAuthority> grantedAuthorities = authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 4. 构建并返回 Spring Security 的 User 对象
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getEnabled(),
user.getAccountNonExpired(),
user.getCredentialsNonExpired(),
user.getAccountNonLocked(),
grantedAuthorities
);
return new CustomUserDetails(user);
}
}

View File

@@ -1,5 +1,6 @@
package cn.nopj.chaos_api.util;
import cn.nopj.chaos_api.security.CustomUserDetails;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
@@ -67,7 +68,7 @@ public class JwtTokenUtil {
/**
* 生成 Token
*/
public String generateToken(UserDetails userDetails) {
public String generateToken(CustomUserDetails userDetails) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
@@ -82,6 +83,9 @@ public class JwtTokenUtil {
.withIssuedAt(now)
.withExpiresAt(expiryDate)
.withClaim("authorities", authorities)
.withClaim("userId", userDetails.getUserId())
.withClaim("nickname", userDetails.getNickname())
.withClaim("avatar", userDetails.getAvatar())
.sign(algorithm);
}

View File

@@ -22,6 +22,8 @@ spring:
output:
ansi:
enabled: always
profiles:
active: dev
mybatis-plus:
mapper-locations: classpath*:/mapper/**/*.xml
@@ -29,14 +31,19 @@ mybatis-plus:
global-config:
db-config:
id-type: auto
logic-not-delete-value: "null"
logic-delete-value: "NOW()"
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
jwt:
tokenHeader: Authorization
tokenHead: Chaos
secret: zHANgcHao@1995!20250506
expiration: 604800
auth:
def-role-code: user

View File

@@ -0,0 +1,257 @@
/*
Navicat Premium Data Transfer
Source Schema : chaos
Target Server Type : MySQL
Target Server Version : 8.0+
Date: 2024-06-12
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- 1. 设备类型表 (保持原有结构)
-- ----------------------------
DROP TABLE IF EXISTS `t_device_type`;
CREATE TABLE `t_device_type` (
`id` bigint unsigned auto_increment comment '主键ID'
primary key,
`name` varchar(128) not null comment '设备类型名称',
`code` varchar(64) null comment '设备类型唯一编码',
`parent_id` bigint unsigned default 0 null comment '父级类型ID (0表示顶级)',
`remark` varchar(500) default '' null comment '备注',
`create_time` datetime default current_timestamp() not null comment '创建时间',
`update_time` datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
`delete_time` datetime null comment '逻辑删除时间',
constraint uk_code
unique (`code`),
constraint uk_name
unique (`name`)
) comment '资产管理-设备类型表';
create index idx_parent_id on `t_device_type` (`parent_id`);
-- ----------------------------
-- 2. 设备信息表 (保持原有结构)
-- ----------------------------
DROP TABLE IF EXISTS `t_device`;
CREATE TABLE `t_device` (
`id` bigint unsigned auto_increment comment '主键ID'
primary key,
`name` varchar(128) not null comment '设备名称',
`model` varchar(128) null comment '设备型号',
`type_id` bigint unsigned null comment '设备类型ID (外键关联设备类型表)',
`location_id` bigint unsigned null comment '位置ID (外键关联位置表)',
`snmp_community` varchar(128) null comment 'SNMP团体名 (建议加密或隐藏)',
`manufacturer` varchar(128) null comment '设备制造商',
`purchase_date` date null comment '采购日期',
`status` tinyint unsigned default 1 null comment '设备状态 (1:在线, 0:离线, 2:维护中)',
`remark` varchar(500) default '' null comment '备注',
`create_time` datetime default current_timestamp() not null comment '创建时间',
`update_time` datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
`delete_time` datetime null comment '逻辑删除时间',
constraint fk_dev_type_id
foreign key (`type_id`) references `t_device_type` (`id`)
on update cascade on delete set null
) comment '资产管理-设备信息表';
create index idx_location_id on `t_device` (`location_id`);
create index idx_type_id on `t_device` (`type_id`);
-- ----------------------------
-- 3. 网络接口表 (结构调整移除IP/VLAN仅保留物理/链路属性)
-- ----------------------------
DROP TABLE IF EXISTS `t_network_interface`;
CREATE TABLE `t_network_interface` (
`id` bigint unsigned auto_increment comment '主键ID'
primary key,
`device_id` bigint unsigned not null comment '设备ID',
`parent_id` bigint unsigned null comment '父接口ID (用于子接口/聚合口成员)',
`name` varchar(64) not null comment '接口名称 (如: eth0, GE0/0/1, Port-Channel1)',
`type` tinyint unsigned default 1 not null comment '接口类型 (1:物理口, 2:聚合口, 3:虚拟口)',
`mac_address` varchar(17) null comment 'MAC地址 (格式: AA:BB:CC:DD:EE:FF)',
`port_speed` int unsigned default 0 null comment '物理端口速率 (Mbps)',
`duplex` tinyint unsigned default 1 null comment '双工模式 (1:全双工, 2:半双工, 3:自适应)',
`status` tinyint unsigned default 1 null comment '接口运行状态 (1:UP, 0:DOWN)',
`remark` varchar(500) default '' null comment '备注',
`create_time` datetime default current_timestamp() not null comment '创建时间',
`update_time` datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
`delete_time` datetime null comment '逻辑删除时间',
constraint fk_ni_device_id
foreign key (`device_id`) references `t_device` (`id`)
on update cascade on delete cascade,
constraint fk_ni_parent_id
foreign key (`parent_id`) references `t_network_interface` (`id`)
on update cascade on delete set null
) comment '资产管理-网络接口基础表';
create index idx_ni_device_id on `t_network_interface` (`device_id`);
create index idx_ni_mac_address on `t_network_interface` (`mac_address`);
-- ----------------------------
-- 4. 接口地址配置表 (新增核心表支持多IP/多VLAN/广播地址)
-- ----------------------------
DROP TABLE IF EXISTS `t_interface_address_config`;
CREATE TABLE `t_interface_address_config` (
`id` bigint unsigned auto_increment comment '主键ID'
primary key,
`interface_id` bigint unsigned not null comment '关联物理接口ID',
`vlan_id` smallint unsigned default 0 null comment 'VLAN ID (0或NULL表示Native/Untagged)',
`ip_address` varchar(45) null comment 'IP地址 (支持IPv4/IPv6)',
`subnet_mask` varchar(45) null comment '子网掩码/CIDR前缀',
`gateway_ip` varchar(45) null comment '网关IP地址',
`broadcast_address` varchar(45) null comment '广播地址 (新增)',
`is_primary` tinyint unsigned default 1 not null comment '是否为主IP (1:主IP, 0:从IP/Alias)',
`is_dhcp` tinyint unsigned default 0 not null comment '是否启用DHCP (1:启用, 0:静态)',
`mtu` int unsigned default 1500 null comment 'MTU值',
`status` tinyint unsigned default 1 null comment '配置状态 (1:启用, 0:禁用)',
`remark` varchar(500) default '' null comment '备注',
`create_time` datetime default current_timestamp() not null comment '创建时间',
`update_time` datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
`delete_time` datetime null comment '逻辑删除时间',
constraint fk_iac_interface_id
foreign key (`interface_id`) references `t_network_interface` (`id`)
on update cascade on delete cascade
) comment '资产管理-接口地址与VLAN配置表';
create index idx_iac_interface_id on `t_interface_address_config` (`interface_id`);
create index idx_iac_ip_address on `t_interface_address_config` (`ip_address`);
-- ----------------------------
-- 5. DNS 服务器字典表 (新增复用DNS)
-- ----------------------------
DROP TABLE IF EXISTS `t_dns_server`;
CREATE TABLE `t_dns_server` (
`id` bigint unsigned auto_increment comment '主键ID'
primary key,
`dns_address` varchar(45) not null comment 'DNS服务器IP地址',
`name` varchar(64) null comment 'DNS名称 (如: Google DNS)',
`description` varchar(255) default '' null comment '描述',
`create_time` datetime default current_timestamp() not null comment '创建时间',
`update_time` datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
`delete_time` datetime null comment '逻辑删除时间',
constraint uk_dns_address
unique (`dns_address`)
) comment '资产管理-DNS服务器字典表';
-- ----------------------------
-- 6. 接口配置与DNS关联表 (新增:多对多关系)
-- ----------------------------
DROP TABLE IF EXISTS `t_interface_dns_mapping`;
CREATE TABLE `t_interface_dns_mapping` (
`id` bigint unsigned auto_increment comment '主键ID'
primary key,
`config_id` bigint unsigned not null comment '接口配置ID (关联 t_interface_address_config)',
`dns_server_id` bigint unsigned not null comment 'DNS服务器ID (关联 t_dns_server)',
`priority` int unsigned default 1 not null comment '优先级 (数值越小优先级越高)',
`create_time` datetime default current_timestamp() not null comment '创建时间',
constraint uk_config_dns
unique (`config_id`, `dns_server_id`),
constraint fk_idm_config_id
foreign key (`config_id`) references `t_interface_address_config` (`id`)
on update cascade on delete cascade,
constraint fk_idm_dns_id
foreign key (`dns_server_id`) references `t_dns_server` (`id`)
on update cascade on delete cascade
) comment '资产管理-接口配置DNS关联表';
create index idx_idm_config_id on `t_interface_dns_mapping` (`config_id`);
SET FOREIGN_KEY_CHECKS = 1;
create table chaos.t_permission
(
id bigint unsigned auto_increment comment '主键ID'
primary key,
parent_id bigint unsigned default 0 not null comment '父权限ID (0为顶级)',
name varchar(64) not null comment '权限名称',
code varchar(128) not null comment '权限标识/资源路径 (RESTful 风格或权限点)',
type tinyint unsigned default 1 not null comment '类型 (1:目录, 2:菜单, 3:按钮)',
sort_order int default 0 not null comment '排序 (数值越小越靠前)',
create_time datetime default current_timestamp() not null comment '创建时间',
update_time datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
delete_time datetime null,
constraint uk_code
unique (code)
)
comment '系统管理-权限表';
create index idx_parent_id
on chaos.t_permission (parent_id);
create table chaos.t_role
(
id bigint unsigned auto_increment comment '主键ID'
primary key,
name varchar(64) not null comment '角色名称 (如: 管理员)',
code varchar(64) not null comment '角色标识 (如: admin)',
status tinyint unsigned default 1 not null comment '状态 (1:正常, 0:停用)',
remark varchar(500) default '' null comment '备注',
create_time datetime default current_timestamp() not null comment '创建时间',
update_time datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
delete_time datetime null,
constraint uk_code
unique (code)
)
comment '系统管理-角色表';
create table chaos.t_role_permission
(
id bigint unsigned auto_increment comment '主键ID (代理键)'
primary key,
role_id bigint unsigned not null comment '角色ID',
permission_id bigint unsigned not null comment '权限ID',
constraint uk_role_permission
unique (role_id, permission_id),
constraint fk_rp_permission_id
foreign key (permission_id) references chaos.t_permission (id)
on update cascade on delete cascade,
constraint fk_rp_role_id
foreign key (role_id) references chaos.t_role (id)
on update cascade on delete cascade
)
comment '系统管理-角色权限关联表';
create table chaos.t_user
(
id bigint unsigned auto_increment comment '主键ID'
primary key,
username varchar(64) not null comment '用户名/登录名',
password varchar(255) not null comment '密码加密存储建议BCrypt或Argon2',
nickname varchar(64) default '' null comment '用户昵称',
avatar varchar(255) null comment '头像',
enabled tinyint unsigned default 1 not null comment '状态 (1:启用, 0:禁用)',
account_non_expired tinyint unsigned default 1 not null comment '账号未过期 (1:是, 0:否)',
credentials_non_expired tinyint unsigned default 1 not null comment '凭证未过期 (1:是, 0:否)',
account_non_locked tinyint unsigned default 1 not null comment '账号未锁定 (1:是, 0:否)',
remark varchar(500) default '' null comment '备注信息',
create_time datetime default current_timestamp() not null comment '创建时间',
update_time datetime default current_timestamp() not null on update current_timestamp() comment '更新时间',
delete_time datetime null,
constraint uk_username
unique (username)
)
comment '系统管理-用户表';
create table chaos.t_user_role
(
id bigint unsigned auto_increment comment '主键ID (代理键)'
primary key,
user_id bigint unsigned not null comment '用户ID',
role_id bigint unsigned not null comment '角色ID',
constraint uk_user_role
unique (user_id, role_id),
constraint fk_ur_role_id
foreign key (role_id) references chaos.t_role (id)
on update cascade on delete cascade,
constraint fk_ur_user_id
foreign key (user_id) references chaos.t_user (id)
on update cascade on delete cascade
)
comment '系统管理-用户角色关联表';

20
pom.xml
View File

@@ -119,6 +119,7 @@
<artifactId>logback-classic</artifactId>
<version>1.5.18</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-amqp -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
@@ -130,6 +131,25 @@
<artifactId>spring-web</artifactId>
<version>6.2.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<version>3.5.7</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework/spring-context -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.2.13</version>
</dependency>
<!-- https://mvnrepository.com/artifact/com.baomidou/mybatis-plus-jsqlparser -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser</artifactId>
<version>3.5.14</version>
</dependency>
</dependencies>
</dependencyManagement>
</project>

View File

@@ -1,218 +0,0 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>分片上传示例</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f0f2f5;
}
.container {
background: white;
padding: 2rem 3rem;
border-radius: 12px;
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
text-align: center;
width: 90%;
max-width: 500px;
}
h2 {
color: #333;
margin-bottom: 1.5rem;
}
#fileInput {
display: none;
}
.file-label {
display: inline-block;
padding: 10px 20px;
background-color: #e9ecef;
border: 2px dashed #ced4da;
border-radius: 8px;
cursor: pointer;
transition: background-color 0.3s, border-color 0.3s;
margin-bottom: 1rem;
}
.file-label:hover {
background-color: #dee2e6;
border-color: #adb5bd;
}
#fileName {
font-style: italic;
color: #6c757d;
margin-bottom: 1.5rem;
min-height: 20px;
}
#uploadButton {
width: 100%;
padding: 12px 20px;
border: none;
background: linear-gradient(45deg, #007bff, #0056b3);
color: white;
border-radius: 8px;
cursor: pointer;
font-size: 16px;
font-weight: bold;
transition: transform 0.2s, box-shadow 0.2s;
}
#uploadButton:disabled {
background: #adb5bd;
cursor: not-allowed;
}
#uploadButton:not(:disabled):hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 123, 255, 0.3);
}
.progress-bar {
width: 100%;
background-color: #e9ecef;
border-radius: 8px;
margin-top: 1.5rem;
overflow: hidden;
}
.progress {
width: 0%;
height: 24px;
background: linear-gradient(45deg, #28a745, #218838);
border-radius: 8px;
text-align: center;
color: white;
line-height: 24px;
font-weight: bold;
transition: width 0.4s ease-in-out;
}
#status {
margin-top: 1rem;
color: #212529;
font-weight: 500;
}
</style>
</head>
<body>
<div class="container">
<h2>视频分片上传</h2>
<label for="fileInput" class="file-label">选择文件</label>
<input type="file" id="fileInput" />
<div id="fileName">未选择文件</div>
<button id="uploadButton">上传</button>
<div class="progress-bar">
<div class="progress" id="progressBar">0%</div>
</div>
<p id="status"></p>
</div>
<script src="https://cdn.jsdelivr.net/npm/axios/dist/axios.min.js"></script>
<script>
const fileInput = document.getElementById('fileInput');
const uploadButton = document.getElementById('uploadButton');
const progressBar = document.getElementById('progressBar');
const statusEl = document.getElementById('status');
const fileNameEl = document.getElementById('fileName');
// =================================================================
// 【重要】请将这里替换为您通过登录接口获取到的真实JWT Token
const JWT_TOKEN = "Chaos "
+ "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJjaGFvcyIsImlhdCI6MTc1MjkwNjk4NCwiZXhwIjoxNzUzNTExNzg0fQ.fG8bSGqji9BfKVoSpMKy5GvSQuzXNdlc7Km94nkuUyPOPVBcGLSBafzSBONxn7ECYcGhS0jmmNK-_z207zy-UA"
// =================================================================
const api = axios.create({
baseURL: 'http://localhost:18888/api/video', // 确保这个基础URL与您的后端服务匹配
headers: { 'Authorization': JWT_TOKEN }
});
fileInput.addEventListener('change', () => {
if (fileInput.files.length > 0) {
fileNameEl.textContent = fileInput.files[0].name;
} else {
fileNameEl.textContent = '未选择文件';
}
});
uploadButton.addEventListener('click', async () => {
const file = fileInput.files[0];
if (!file) {
alert('请先选择文件!');
return;
}
uploadButton.disabled = true;
statusEl.textContent = '正在初始化上传...';
updateProgress(0);
try {
// 步骤 1: 初始化上传,获取 uploadId 和 chunkSize
const initResponse = await api.post('/init', {
fileName: file.name,
totalSize: file.size
});
const { uploadId, chunkSize } = initResponse.data.data;
statusEl.textContent = '初始化成功,开始上传分片...';
// 步骤 2: 分片并并发上传
const totalChunks = Math.ceil(file.size / chunkSize);
const uploadPromises = [];
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const formData = new FormData();
formData.append('chunk', chunk);
formData.append('uploadId', uploadId);
formData.append('chunkIndex', i);
const promise = api.post('/chunk', formData, {
onUploadProgress: (progressEvent) => {
// 这个回调是单个分片的进度,我们在这里简单更新总进度
const percentCompleted = Math.round(((i * chunkSize) + progressEvent.loaded) * 100 / file.size);
updateProgress(percentCompleted);
}
});
uploadPromises.push(promise);
}
// 等待所有分片上传完成
await Promise.all(uploadPromises);
statusEl.textContent = '所有分片上传完毕,正在合并文件...';
// 步骤 3: 通知后端合并文件
const mergeResponse = await api.post('/merge', {
uploadId: uploadId,
fileName: file.name
});
statusEl.textContent = mergeResponse.data.message || '文件上传成功,后台处理中!';
updateProgress(100);
} catch (error) {
console.error('上传失败:', error);
statusEl.textContent = '上传失败: ' + (error.response?.data?.message || error.message);
updateProgress(0, true); // 出错时重置进度条为红色
} finally {
uploadButton.disabled = false;
}
});
function updateProgress(percentage, isError = false) {
progressBar.style.width = percentage + '%';
progressBar.textContent = percentage + '%';
if (isError) {
progressBar.style.background = 'linear-gradient(45deg, #dc3545, #c82333)';
} else {
progressBar.style.background = 'linear-gradient(45deg, #28a745, #218838)';
}
}
</script>
</body>
</html>