feat(admin): Implement admin module foundation

This commit is contained in:
2025-04-11 00:26:30 +08:00
parent 2f49bed861
commit 36380d50a7
71 changed files with 4682 additions and 15 deletions
+131
View File
@@ -12,10 +12,141 @@
<artifactId>video-admin</artifactId>
<dependencies>
<!-- 引入公共模块 -->
<dependency>
<groupId>com.nuaa.video</groupId>
<artifactId>video-common</artifactId>
<version>${project.version}</version>
</dependency>
<!-- Spring Boot Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Discovery -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Nacos Config -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!-- Spring Boot Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<!-- Spring Boot Validation -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- Spring Cloud OpenFeign -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<!-- Spring Cloud LoadBalancer -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>
<!-- Spring Cloud Alibaba Sentinel -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<!-- MySQL Connector -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MyBatis Plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
<version>3.5.11</version>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- JWT相关 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- Hutool工具包 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -1,11 +1,21 @@
package com.nuaa.video.admin;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* 管理后台启动类
*/
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
@MapperScan("com.nuaa.video.admin.map" +
"" +
"" +
"per")
public class AdminApplication {
public static void main(String[] args) {
SpringApplication.run(AdminApplication.class, args);
@@ -0,0 +1,16 @@
package com.nuaa.video.admin.annotation;
import java.lang.annotation.*;
/**
* 操作日志注解
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface OperationLog {
/**
* 操作描述
*/
String value() default "";
}
@@ -0,0 +1,220 @@
package com.nuaa.video.admin.aspect;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.nuaa.video.admin.annotation.OperationLog;
import com.nuaa.video.admin.entity.AdminOperationLog;
import com.nuaa.video.admin.mapper.AdminOperationLogMapper;
import com.nuaa.video.admin.security.AdminUserDetails;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;
/**
* 操作日志切面
*/
@Slf4j
@Aspect
@Component
@RequiredArgsConstructor
public class OperationLogAspect {
private final AdminOperationLogMapper operationLogMapper;
private final ObjectMapper objectMapper;
@Value("${operation-log.enabled:true}")
private boolean enabled;
@Value("#{'${operation-log.exclude-paths:}'.split(',')}")
private List<String> excludePaths;
/**
* 切入点
*/
@Pointcut("@annotation(com.nuaa.video.admin.annotation.OperationLog)")
public void operationLogPointcut() {
}
/**
* 处理完请求后执行
*
* @param joinPoint 切点
* @param result 返回值
*/
@AfterReturning(pointcut = "operationLogPointcut()", returning = "result")
public void doAfterReturning(JoinPoint joinPoint, Object result) {
if (!enabled) {
return;
}
handleLog(joinPoint, null, result);
}
/**
* 拦截异常操作
*
* @param joinPoint 切点
* @param e 异常
*/
@AfterThrowing(value = "operationLogPointcut()", throwing = "e")
public void doAfterThrowing(JoinPoint joinPoint, Exception e) {
if (!enabled) {
return;
}
handleLog(joinPoint, e, null);
}
/**
* 处理日志
*/
private void handleLog(JoinPoint joinPoint, Exception e, Object result) {
try {
// 获取请求信息
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return;
}
HttpServletRequest request = attributes.getRequest();
String requestURI = request.getRequestURI();
// 检查是否需要排除
if (isExclude(requestURI)) {
return;
}
// 获取注解信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
OperationLog annotation = method.getAnnotation(OperationLog.class);
// 如果未配置注解,则不记录日志
if (annotation == null) {
return;
}
// 获取当前登录用户
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Long adminId = null;
String username = "unknown";
if (authentication != null && authentication.getPrincipal() instanceof AdminUserDetails) {
AdminUserDetails userDetails = (AdminUserDetails) authentication.getPrincipal();
adminId = userDetails.getId();
username = userDetails.getUsername();
} else if (authentication != null && authentication.getPrincipal() instanceof String) {
username = (String) authentication.getPrincipal();
}
// 获取操作
String operation = annotation.value();
// 获取请求方法
String methodName = joinPoint.getTarget().getClass().getName() + "." + method.getName();
// 获取请求参数
String params = getParams(joinPoint);
// 获取IP地址
String ip = getIpAddress(request);
// 创建日志对象
AdminOperationLog operationLog = new AdminOperationLog();
operationLog.setAdminId(adminId);
operationLog.setUsername(username);
operationLog.setOperation(operation);
operationLog.setMethod(methodName);
operationLog.setParams(params);
operationLog.setIp(ip);
operationLog.setOperationTime(LocalDateTime.now());
// 处理异常和结果
if (e != null) {
operationLog.setStatus(0); // 失败
operationLog.setErrorMsg(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : e.toString());
} else {
operationLog.setStatus(1); // 成功
}
// 保存日志
operationLogMapper.insert(operationLog);
} catch (Exception ex) {
log.error("记录操作日志失败", ex);
}
}
/**
* 获取请求参数
*/
private String getParams(JoinPoint joinPoint) {
try {
Object[] args = joinPoint.getArgs();
// 过滤掉一些不需要记录的参数类型
args = Arrays.stream(args)
.filter(arg -> !(arg instanceof HttpServletRequest || arg instanceof HttpServletRequest))
.toArray();
return objectMapper.writeValueAsString(args);
} catch (Exception e) {
log.error("获取请求参数失败", e);
return "获取参数失败";
}
}
/**
* 获取请求的IP地址
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况,第一个IP为客户端真实IP
if (ip != null && ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
return ip;
}
/**
* 判断是否需要排除请求
*/
private boolean isExclude(String requestURI) {
if (excludePaths != null && !excludePaths.isEmpty()) {
for (String pattern : excludePaths) {
if (StringUtils.hasText(pattern) && requestURI.matches(pattern.trim())) {
return true;
}
}
}
return false;
}
}
@@ -0,0 +1,111 @@
package com.nuaa.video.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.admin.annotation.OperationLog;
import com.nuaa.video.admin.dto.AdminUserDTO;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.service.AdminUserService;
import com.nuaa.video.admin.vo.AdminUserVO;
import com.nuaa.video.common.model.vo.Result;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
/**
* 管理员控制器
*/
@Slf4j
@RestController
@RequestMapping("/admin/user")
@RequiredArgsConstructor
public class AdminUserController {
private final AdminUserService adminUserService;
/**
* 获取管理员列表
*/
@PreAuthorize("hasAuthority('user:manage')")
@GetMapping("/list")
public Result<Page<AdminUserVO>> listUsers(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword) {
Page<AdminUser> pageParam = new Page<>(page, size);
return adminUserService.listUsers(pageParam, keyword);
}
/**
* 获取管理员详情
*/
@PreAuthorize("hasAuthority('user:manage')")
@GetMapping("/{id}")
public Result<AdminUserVO> getUser(@PathVariable Long id) {
return adminUserService.getUserDetail(id);
}
/**
* 添加管理员
*/
@OperationLog("添加管理员")
@PreAuthorize("hasAuthority('user:manage')")
@PostMapping
public Result<Void> addUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
return adminUserService.addUser(adminUserDTO, getCurrentUsername());
}
/**
* 更新管理员
*/
@OperationLog("更新管理员")
@PreAuthorize("hasAuthority('user:manage')")
@PutMapping
public Result<Void> updateUser(@RequestBody @Valid AdminUserDTO adminUserDTO) {
return adminUserService.updateUser(adminUserDTO, getCurrentUsername());
}
/**
* 删除管理员
*/
@OperationLog("删除管理员")
@PreAuthorize("hasAuthority('user:manage')")
@DeleteMapping("/{id}")
public Result<Void> deleteUser(@PathVariable Long id) {
return adminUserService.deleteUser(id);
}
/**
* 启用/禁用管理员
*/
@OperationLog("修改管理员状态")
@PreAuthorize("hasAuthority('user:manage')")
@PutMapping("/{id}/status/{status}")
public Result<Void> updateStatus(@PathVariable Long id, @PathVariable Integer status) {
return adminUserService.updateStatus(id, status, getCurrentUsername());
}
/**
* 重置密码
*/
@OperationLog("重置管理员密码")
@PreAuthorize("hasAuthority('user:manage')")
@PutMapping("/{id}/reset-password")
public Result<Void> resetPassword(@PathVariable Long id, @RequestParam String newPassword) {
return adminUserService.resetPassword(id, newPassword, getCurrentUsername());
}
/**
* 获取当前登录用户名
*/
private String getCurrentUsername() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication.getName();
}
return "system";
}
}
@@ -0,0 +1,65 @@
package com.nuaa.video.admin.controller;
import com.nuaa.video.admin.annotation.OperationLog;
import com.nuaa.video.admin.dto.LoginDTO;
import com.nuaa.video.admin.security.AdminUserDetails;
import com.nuaa.video.admin.service.AuthService;
import com.nuaa.video.admin.vo.CaptchaVO;
import com.nuaa.video.admin.vo.LoginVO;
import com.nuaa.video.common.model.vo.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
/**
* 认证控制器
*/
@Slf4j
@RestController
@RequestMapping("/admin/auth")
@RequiredArgsConstructor
public class AuthController {
private final AuthService authService;
/**
* 获取验证码
*/
@GetMapping("/captcha")
public Result<CaptchaVO> getCaptcha() {
return authService.generateCaptcha();
}
/**
* 登录
*/
@PostMapping("/login")
public Result<LoginVO> login(@RequestBody @Valid LoginDTO loginDTO, HttpServletRequest request) {
return authService.login(loginDTO, request);
}
/**
* 登出
*/
@OperationLog("退出登录")
@PostMapping("/logout")
public Result<Void> logout(@RequestHeader("Authorization") String authorization) {
// 从请求头中提取token
String token = null;
if (authorization != null && authorization.startsWith("Bearer ")) {
token = authorization.substring(7);
}
return authService.logout(token);
}
/**
* 获取当前用户信息
*/
@GetMapping("/info")
public Result<LoginVO> getCurrentUser(@AuthenticationPrincipal AdminUserDetails userDetails) {
return authService.getCurrentUser(userDetails);
}
}
@@ -0,0 +1,119 @@
package com.nuaa.video.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.admin.annotation.OperationLog;
import com.nuaa.video.admin.feign.ContentFeignClient;
import com.nuaa.video.common.model.vo.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 内容管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/admin/platform/content")
@RequiredArgsConstructor
public class ContentManageController {
private final ContentFeignClient contentFeignClient;
/**
* 获取视频列表
*/
@PreAuthorize("hasAuthority('video:manage')")
@GetMapping("/video/list")
public Result<Page<Object>> getVideoList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "status", required = false) Integer status,
@RequestHeader("Authorization") String token) {
return contentFeignClient.getVideoList(page, size, keyword, categoryId, status, token);
}
/**
* 获取视频详情
*/
@PreAuthorize("hasAuthority('video:manage')")
@GetMapping("/video/{videoId}")
public Result<Object> getVideoDetail(
@PathVariable Long videoId,
@RequestHeader("Authorization") String token) {
return contentFeignClient.getVideoDetail(videoId, token);
}
/**
* 审核视频
*/
@OperationLog("审核视频")
@PreAuthorize("hasAuthority('video:manage')")
@PostMapping("/video/review/{videoId}")
public Result<Void> reviewVideo(
@PathVariable Long videoId,
@RequestParam Integer status,
@RequestParam(required = false) String reason,
@RequestHeader("Authorization") String token) {
return contentFeignClient.reviewVideo(videoId, status, reason, token);
}
/**
* 下架视频
*/
@OperationLog("下架视频")
@PreAuthorize("hasAuthority('video:manage')")
@PutMapping("/video/disable/{videoId}")
public Result<Void> disableVideo(
@PathVariable Long videoId,
@RequestHeader("Authorization") String token) {
return contentFeignClient.disableVideo(videoId, token);
}
/**
* 获取分类列表
*/
@PreAuthorize("hasAuthority('category:manage')")
@GetMapping("/category/list")
public Result<Object> getCategoryList(@RequestHeader("Authorization") String token) {
return contentFeignClient.getCategoryList(token);
}
/**
* 添加分类
*/
@OperationLog("添加分类")
@PreAuthorize("hasAuthority('category:manage')")
@PostMapping("/category")
public Result<Void> addCategory(
@RequestBody Object category,
@RequestHeader("Authorization") String token) {
return contentFeignClient.addCategory(category, token);
}
/**
* 更新分类
*/
@OperationLog("更新分类")
@PreAuthorize("hasAuthority('category:manage')")
@PutMapping("/category")
public Result<Void> updateCategory(
@RequestBody Object category,
@RequestHeader("Authorization") String token) {
return contentFeignClient.updateCategory(category, token);
}
/**
* 删除分类
*/
@OperationLog("删除分类")
@PreAuthorize("hasAuthority('category:manage')")
@DeleteMapping("/category/{categoryId}")
public Result<Void> deleteCategory(
@PathVariable Long categoryId,
@RequestHeader("Authorization") String token) {
return contentFeignClient.deleteCategory(categoryId, token);
}
}
@@ -0,0 +1,72 @@
package com.nuaa.video.admin.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.admin.annotation.OperationLog;
import com.nuaa.video.admin.feign.UserFeignClient;
import com.nuaa.video.common.model.vo.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
/**
* 用户管理控制器
*/
@Slf4j
@RestController
@RequestMapping("/admin/platform/user")
@RequiredArgsConstructor
public class UserManageController {
private final UserFeignClient userFeignClient;
/**
* 获取用户列表
*/
@PreAuthorize("hasAuthority('user:manage')")
@GetMapping("/list")
public Result<Page<Object>> getUserList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestHeader("Authorization") String token) {
return userFeignClient.getUserList(page, size, keyword, token);
}
/**
* 获取用户详情
*/
@PreAuthorize("hasAuthority('user:manage')")
@GetMapping("/{userId}")
public Result<Object> getUserDetail(
@PathVariable Long userId,
@RequestHeader("Authorization") String token) {
return userFeignClient.getUserDetail(userId, token);
}
/**
* 禁用/启用用户
*/
@OperationLog("修改用户状态")
@PreAuthorize("hasAuthority('user:manage')")
@PutMapping("/{userId}/status/{status}")
public Result<Void> updateUserStatus(
@PathVariable Long userId,
@PathVariable Integer status,
@RequestHeader("Authorization") String token) {
return userFeignClient.updateUserStatus(userId, status, token);
}
/**
* 重置用户密码
*/
@OperationLog("重置用户密码")
@PreAuthorize("hasAuthority('user:manage')")
@PutMapping("/{userId}/reset-password")
public Result<Void> resetUserPassword(
@PathVariable Long userId,
@RequestParam String newPassword,
@RequestHeader("Authorization") String token) {
return userFeignClient.resetUserPassword(userId, newPassword, token);
}
}
@@ -0,0 +1,36 @@
package com.nuaa.video.admin.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 权限DTO
*/
@Data
public class AdminPermissionDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 权限ID
*/
private Long id;
/**
* 权限名称
*/
@NotBlank(message = "权限名称不能为空")
private String name;
/**
* 权限编码
*/
@NotBlank(message = "权限编码不能为空")
private String code;
/**
* 权限描述
*/
private String description;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.admin.dto;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 角色DTO
*/
@Data
public class AdminRoleDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 角色ID
*/
private Long id;
/**
* 角色名称
*/
@NotBlank(message = "角色名称不能为空")
private String name;
/**
* 角色编码
*/
@NotBlank(message = "角色编码不能为空")
@Pattern(regexp = "^ADMIN_[A-Z_]+$", message = "角色编码必须以ADMIN_开头")
private String code;
/**
* 角色描述
*/
private String description;
/**
* 权限ID集合
*/
private List<Long> permissionIds;
}
@@ -0,0 +1,69 @@
package com.nuaa.video.admin.dto;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 管理员DTO
*/
@Data
public class AdminUserDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 管理员ID
*/
private Long id;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
@Size(min = 5, max = 20, message = "用户名长度必须在5-20之间")
private String username;
/**
* 密码
*/
@Size(min = 6, max = 20, message = "密码长度必须在6-20之间")
private String password;
/**
* 真实姓名
*/
@NotBlank(message = "真实姓名不能为空")
private String realName;
/**
* 头像URL
*/
private String avatar;
/**
* 邮箱
*/
@Email(message = "邮箱格式不正确")
private String email;
/**
* 手机号
*/
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
private String phone;
/**
* 状态(0:禁用,1:正常)
*/
private Integer status;
/**
* 角色ID集合
*/
private List<Long> roleIds;
}
@@ -0,0 +1,36 @@
package com.nuaa.video.admin.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 登录DTO
*/
@Data
public class LoginDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 用户名
*/
@NotBlank(message = "用户名不能为空")
private String username;
/**
* 密码
*/
@NotBlank(message = "密码不能为空")
private String password;
/**
* 验证码
*/
private String captcha;
/**
* 验证码Key
*/
private String captchaKey;
}
@@ -0,0 +1,36 @@
package com.nuaa.video.admin.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import java.io.Serializable;
/**
* 系统配置DTO
*/
@Data
public class SystemConfigDTO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 配置ID
*/
private Long id;
/**
* 配置键
*/
@NotBlank(message = "配置键不能为空")
private String configKey;
/**
* 配置值
*/
@NotBlank(message = "配置值不能为空")
private String configValue;
/**
* 配置描述
*/
private String description;
}
@@ -0,0 +1,69 @@
package com.nuaa.video.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 操作日志实体
*/
@Data
@TableName("admin_operation_log")
public class AdminOperationLog implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 管理员ID
*/
private Long adminId;
/**
* 用户名
*/
private String username;
/**
* 操作
*/
private String operation;
/**
* 方法名
*/
private String method;
/**
* 参数
*/
private String params;
/**
* IP地址
*/
private String ip;
/**
* 状态(0:失败,1:成功)
*/
private Integer status;
/**
* 错误消息
*/
private String errorMsg;
/**
* 操作时间
*/
private LocalDateTime operationTime;
}
@@ -0,0 +1,59 @@
package com.nuaa.video.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 管理员权限实体
*/
@Data
@TableName("admin_permission")
public class AdminPermission implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 权限ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 权限名称
*/
private String name;
/**
* 权限编码
*/
private String code;
/**
* 权限描述
*/
private String description;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,73 @@
package com.nuaa.video.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 管理员角色实体
*/
@Data
@TableName("admin_role")
public class AdminRole implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 角色ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 角色名称
*/
private String name;
/**
* 角色编码
*/
private String code;
/**
* 角色描述
*/
private String description;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 权限ID集合(非数据库字段)
*/
@TableField(exist = false)
private List<Long> permissionIds;
/**
* 权限集合(非数据库字段)
*/
@TableField(exist = false)
private List<AdminPermission> permissions;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 角色权限关联实体
*/
@Data
@TableName("admin_role_permission")
public class AdminRolePermission implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 角色ID
*/
private Long roleId;
/**
* 权限ID
*/
private Long permissionId;
/**
* 创建人
*/
private String createdBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,59 @@
package com.nuaa.video.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统配置实体
*/
@Data
@TableName("admin_system_config")
public class AdminSystemConfig implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 配置键
*/
private String configKey;
/**
* 配置值
*/
private String configValue;
/**
* 配置描述
*/
private String description;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,108 @@
package com.nuaa.video.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 管理员实体
*/
@Data
@TableName("admin_user")
public class AdminUser implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 管理员ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码(加密)
*/
private String password;
/**
* 加密盐值
*/
private String salt;
/**
* 真实姓名
*/
private String realName;
/**
* 头像URL
*/
private String avatar;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phone;
/**
* 状态(0:禁用,1:正常)
*/
private Integer status;
/**
* 最后登录时间
*/
private LocalDateTime lastLoginTime;
/**
* 最后登录IP
*/
private String lastLoginIp;
/**
* 创建人
*/
private String createdBy;
/**
* 更新人
*/
private String updatedBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 角色ID集合(非数据库字段)
*/
@TableField(exist = false)
private List<Long> roleIds;
/**
* 角色集合(非数据库字段)
*/
@TableField(exist = false)
private List<AdminRole> roles;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.admin.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 管理员角色关联实体
*/
@Data
@TableName("admin_user_role")
public class AdminUserRole implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 管理员ID
*/
private Long adminId;
/**
* 角色ID
*/
private Long roleId;
/**
* 创建人
*/
private String createdBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,45 @@
package com.nuaa.video.admin.feign;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* 评论服务Feign客户端
*/
@FeignClient(name = "video-comment", fallbackFactory = CommentFeignClientFallbackFactory.class)
public interface CommentFeignClient {
/**
* 获取评论列表
*/
@GetMapping("/comment/admin/list")
Result<Page<Object>> getCommentList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "videoId", required = false) Long videoId,
@RequestParam(value = "userId", required = false) Long userId,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestHeader("Authorization") String token);
/**
* 获取评论详情
*/
@GetMapping("/comment/admin/{commentId}")
Result<Object> getCommentDetail(@PathVariable("commentId") Long commentId,
@RequestHeader("Authorization") String token);
/**
* 删除评论
*/
@DeleteMapping("/comment/admin/{commentId}")
Result<Void> deleteComment(@PathVariable("commentId") Long commentId,
@RequestHeader("Authorization") String token);
/**
* 获取评论统计信息
*/
@GetMapping("/comment/admin/stats")
Result<Object> getCommentStats(@RequestHeader("Authorization") String token);
}
@@ -0,0 +1,42 @@
package com.nuaa.video.admin.feign;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* 评论服务Feign客户端降级工厂
*/
@Slf4j
@Component
public class CommentFeignClientFallbackFactory implements FallbackFactory<CommentFeignClient> {
@Override
public CommentFeignClient create(Throwable throwable) {
log.error("CommentFeignClient调用失败,原因:", throwable);
return new CommentFeignClient() {
@Override
public Result<Page<Object>> getCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword, String token) {
return Result.error("获取评论列表失败:服务不可用");
}
@Override
public Result<Object> getCommentDetail(Long commentId, String token) {
return Result.error("获取评论详情失败:服务不可用");
}
@Override
public Result<Void> deleteComment(Long commentId, String token) {
return Result.error("删除评论失败:服务不可用");
}
@Override
public Result<Object> getCommentStats(String token) {
return Result.error("获取评论统计信息失败:服务不可用");
}
};
}
}
@@ -0,0 +1,73 @@
package com.nuaa.video.admin.feign;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* 内容服务Feign客户端
*/
@FeignClient(name = "video-content", fallbackFactory = ContentFeignClientFallbackFactory.class)
public interface ContentFeignClient {
/**
* 获取视频列表
*/
@GetMapping("/content/video/admin/list")
Result<Page<Object>> getVideoList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "status", required = false) Integer status,
@RequestHeader("Authorization") String token);
/**
* 获取视频详情
*/
@GetMapping("/content/video/admin/{videoId}")
Result<Object> getVideoDetail(@PathVariable("videoId") Long videoId,
@RequestHeader("Authorization") String token);
/**
* 审核视频
*/
@PostMapping("/content/video/admin/review/{videoId}")
Result<Void> reviewVideo(@PathVariable("videoId") Long videoId,
@RequestParam("status") Integer status,
@RequestParam(value = "reason", required = false) String reason,
@RequestHeader("Authorization") String token);
/**
* 下架视频
*/
@PutMapping("/content/video/admin/disable/{videoId}")
Result<Void> disableVideo(@PathVariable("videoId") Long videoId,
@RequestHeader("Authorization") String token);
/**
* 分类列表
*/
@GetMapping("/content/category/admin/list")
Result<Object> getCategoryList(@RequestHeader("Authorization") String token);
/**
* 添加分类
*/
@PostMapping("/content/category/admin")
Result<Void> addCategory(@RequestBody Object category, @RequestHeader("Authorization") String token);
/**
* 更新分类
*/
@PutMapping("/content/category/admin")
Result<Void> updateCategory(@RequestBody Object category, @RequestHeader("Authorization") String token);
/**
* 删除分类
*/
@DeleteMapping("/content/category/admin/{categoryId}")
Result<Void> deleteCategory(@PathVariable("categoryId") Long categoryId,
@RequestHeader("Authorization") String token);
}
@@ -0,0 +1,62 @@
package com.nuaa.video.admin.feign;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* 内容服务Feign客户端降级工厂
*/
@Slf4j
@Component
public class ContentFeignClientFallbackFactory implements FallbackFactory<ContentFeignClient> {
@Override
public ContentFeignClient create(Throwable throwable) {
log.error("ContentFeignClient调用失败,原因:", throwable);
return new ContentFeignClient() {
@Override
public Result<Page<Object>> getVideoList(Integer page, Integer size, String keyword, Long categoryId, Integer status, String token) {
return Result.error("获取视频列表失败:服务不可用");
}
@Override
public Result<Object> getVideoDetail(Long videoId, String token) {
return Result.error("获取视频详情失败:服务不可用");
}
@Override
public Result<Void> reviewVideo(Long videoId, Integer status, String reason, String token) {
return Result.error("审核视频失败:服务不可用");
}
@Override
public Result<Void> disableVideo(Long videoId, String token) {
return Result.error("下架视频失败:服务不可用");
}
@Override
public Result<Object> getCategoryList(String token) {
return Result.error("获取分类列表失败:服务不可用");
}
@Override
public Result<Void> addCategory(Object category, String token) {
return Result.error("添加分类失败:服务不可用");
}
@Override
public Result<Void> updateCategory(Object category, String token) {
return Result.error("更新分类失败:服务不可用");
}
@Override
public Result<Void> deleteCategory(Long categoryId, String token) {
return Result.error("删除分类失败:服务不可用");
}
};
}
}
@@ -0,0 +1,46 @@
package com.nuaa.video.admin.feign;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
/**
* 用户服务Feign客户端
*/
@FeignClient(name = "video-user", fallbackFactory = UserFeignClientFallbackFactory.class)
public interface UserFeignClient {
/**
* 获取用户列表
*/
@GetMapping("/user/profile/admin/list")
Result<Page<Object>> getUserList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestHeader("Authorization") String token);
/**
* 获取用户详情
*/
@GetMapping("/user/profile/admin/{userId}")
Result<Object> getUserDetail(@PathVariable("userId") Long userId,
@RequestHeader("Authorization") String token);
/**
* 禁用/启用用户
*/
@PutMapping("/user/profile/admin/status/{userId}/{status}")
Result<Void> updateUserStatus(@PathVariable("userId") Long userId,
@PathVariable("status") Integer status,
@RequestHeader("Authorization") String token);
/**
* 重置用户密码
*/
@PutMapping("/user/profile/admin/reset-password/{userId}")
Result<Void> resetUserPassword(@PathVariable("userId") Long userId,
@RequestParam String newPassword,
@RequestHeader("Authorization") String token);
}
@@ -0,0 +1,45 @@
package com.nuaa.video.admin.feign;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.openfeign.FallbackFactory;
import org.springframework.stereotype.Component;
/**
* 用户服务Feign客户端降级工厂
*/
@Slf4j
@Component
public class UserFeignClientFallbackFactory implements FallbackFactory<UserFeignClient> {
@Override
public UserFeignClient create(Throwable cause) {
log.error("用户服务调用失败:", cause);
return new UserFeignClient() {
@Override
public Result<Page<Object>> getUserList(Integer page, Integer size, String keyword, String token) {
log.error("获取用户列表失败");
return Result.error("获取用户列表失败,用户服务不可用");
}
@Override
public Result<Object> getUserDetail(Long userId, String token) {
log.error("获取用户详情失败,用户ID: {}", userId);
return Result.error("获取用户详情失败,用户服务不可用");
}
@Override
public Result<Void> updateUserStatus(Long userId, Integer status, String token) {
log.error("更新用户状态失败,用户ID: {}, 状态: {}", userId, status);
return Result.error("更新用户状态失败,用户服务不可用");
}
@Override
public Result<Void> resetUserPassword(Long userId, String newPassword, String token) {
log.error("重置用户密码失败,用户ID: {}", userId);
return Result.error("重置用户密码失败,用户服务不可用");
}
};
}
}
@@ -0,0 +1,33 @@
package com.nuaa.video.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.admin.entity.AdminOperationLog;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 操作日志Mapper
*/
@Mapper
public interface AdminOperationLogMapper extends BaseMapper<AdminOperationLog> {
/**
* 分页查询操作日志
*
* @param page 分页参数
* @param username 用户名
* @param operation 操作
* @param status 状态
* @param startTime 开始时间
* @param endTime 结束时间
* @return 操作日志列表
*/
Page<AdminOperationLog> selectLogList(
Page<AdminOperationLog> page,
@Param("username") String username,
@Param("operation") String operation,
@Param("status") Integer status,
@Param("startTime") String startTime,
@Param("endTime") String endTime);
}
@@ -0,0 +1,31 @@
package com.nuaa.video.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.admin.entity.AdminPermission;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 权限Mapper
*/
@Mapper
public interface AdminPermissionMapper extends BaseMapper<AdminPermission> {
/**
* 获取角色的权限列表
*
* @param roleId 角色ID
* @return 权限列表
*/
List<AdminPermission> selectPermissionsByRoleId(@Param("roleId") Long roleId);
/**
* 获取管理员的权限列表
*
* @param adminId 管理员ID
* @return 权限列表
*/
List<AdminPermission> selectPermissionsByAdminId(@Param("adminId") Long adminId);
}
@@ -0,0 +1,41 @@
package com.nuaa.video.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.admin.entity.AdminRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 角色Mapper
*/
@Mapper
public interface AdminRoleMapper extends BaseMapper<AdminRole> {
/**
* 查询角色列表(包括权限)
*
* @param page 分页参数
* @param keyword 关键字
* @return 角色列表
*/
Page<AdminRole> selectRoleList(Page<AdminRole> page, @Param("keyword") String keyword);
/**
* 获取角色详情(包括权限)
*
* @param id 角色ID
* @return 角色详情
*/
AdminRole selectRoleDetail(@Param("id") Long id);
/**
* 获取管理员的角色列表
*
* @param adminId 管理员ID
* @return 角色列表
*/
List<AdminRole> selectRolesByAdminId(@Param("adminId") Long adminId);
}
@@ -0,0 +1,25 @@
package com.nuaa.video.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.admin.entity.AdminRolePermission;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 角色权限关联Mapper
*/
@Mapper
public interface AdminRolePermissionMapper extends BaseMapper<AdminRolePermission> {
/**
* 批量插入角色权限关联
*
* @param roleId 角色ID
* @param permissionIds 权限ID列表
* @param createdBy 创建人
* @return 影响行数
*/
int batchInsert(@Param("roleId") Long roleId, @Param("permissionIds") List<Long> permissionIds, @Param("createdBy") String createdBy);
}
@@ -0,0 +1,21 @@
package com.nuaa.video.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.admin.entity.AdminSystemConfig;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 系统配置Mapper
*/
@Mapper
public interface AdminSystemConfigMapper extends BaseMapper<AdminSystemConfig> {
/**
* 根据配置键获取配置值
*
* @param configKey 配置键
* @return 配置值
*/
String getConfigValue(@Param("configKey") String configKey);
}
@@ -0,0 +1,39 @@
package com.nuaa.video.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.admin.entity.AdminUser;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 管理员Mapper
*/
@Mapper
public interface AdminUserMapper extends BaseMapper<AdminUser> {
/**
* 根据用户名查询管理员信息(包括角色和权限)
*
* @param username 用户名
* @return 管理员信息
*/
AdminUser selectByUsername(@Param("username") String username);
/**
* 查询管理员列表(包括角色)
*
* @param page 分页参数
* @param keyword 关键字
* @return 管理员列表
*/
Page<AdminUser> selectUserList(Page<AdminUser> page, @Param("keyword") String keyword);
/**
* 获取管理员详情(包括角色和权限)
*
* @param id 管理员ID
* @return 管理员详情
*/
AdminUser selectUserDetail(@Param("id") Long id);
}
@@ -0,0 +1,25 @@
package com.nuaa.video.admin.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.nuaa.video.admin.entity.AdminUserRole;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 用户角色关联Mapper
*/
@Mapper
public interface AdminUserRoleMapper extends BaseMapper<AdminUserRole> {
/**
* 批量插入用户角色关联
*
* @param adminId 管理员ID
* @param roleIds 角色ID列表
* @param createdBy 创建人
* @return 影响行数
*/
int batchInsert(@Param("adminId") Long adminId, @Param("roleIds") List<Long> roleIds, @Param("createdBy") String createdBy);
}
@@ -0,0 +1,74 @@
package com.nuaa.video.admin.security;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
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;
/**
* Spring Security用户详情
*/
@Data
public class AdminUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
/**
* 管理员ID
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
@JsonIgnore
private String password;
/**
* 授权列表
*/
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
@@ -0,0 +1,85 @@
package com.nuaa.video.admin.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.List;
import java.util.stream.Collectors;
/**
* JWT认证过滤器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider jwtTokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
// 从请求头获取JWT令牌
String token = getJwtFromRequest(request);
// 验证令牌
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
// 从令牌中获取用户信息
Long adminId = jwtTokenProvider.getAdminIdFromToken(token);
String username = jwtTokenProvider.getUsernameFromToken(token);
List<String> authorities = jwtTokenProvider.getAuthoritiesFromToken(token);
// 转换为Spring Security的授权对象
List<SimpleGrantedAuthority> simpleGrantedAuthorities = authorities.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
// 创建认证对象
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
username, null, simpleGrantedAuthorities);
// 设置用户详情
AdminUserDetails userDetails = new AdminUserDetails();
userDetails.setId(adminId);
userDetails.setUsername(username);
userDetails.setAuthorities(simpleGrantedAuthorities);
authentication.setDetails(userDetails);
// 设置认证信息到安全上下文
SecurityContextHolder.getContext().setAuthentication(authentication);
// 将管理员ID和用户名设置到请求属性中,便于后续使用
request.setAttribute("adminId", adminId);
request.setAttribute("username", username);
}
} catch (Exception e) {
log.error("无法设置用户认证", e);
SecurityContextHolder.clearContext();
}
filterChain.doFilter(request, response);
}
/**
* 从请求头中提取JWT令牌
*/
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
@@ -0,0 +1,110 @@
package com.nuaa.video.admin.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.function.Function;
/**
* JWT令牌提供者
*/
@Slf4j
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
/**
* 生成JWT令牌
*/
public String generateToken(AdminUserDetails userDetails) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
return Jwts.builder()
.subject(userDetails.getId().toString())
.claim("username", userDetails.getUsername())
.claim("authorities", userDetails.getAuthorities().stream()
.map(auth -> auth.getAuthority())
.toList())
.issuedAt(now)
.expiration(expiryDate)
.signWith(getSignKey())
.compact();
}
/**
* 验证JWT令牌
*/
public boolean validateToken(String token) {
try {
Jwts.parser().verifyWith(getSignKey()).build().parseSignedClaims(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("无效的JWT令牌: {}", e.getMessage());
return false;
}
}
/**
* 从JWT令牌中获取管理员ID
*/
public Long getAdminIdFromToken(String token) {
String subject = getClaimFromToken(token, Claims::getSubject);
return Long.parseLong(subject);
}
/**
* 从JWT令牌中获取用户名
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, claims -> claims.get("username", String.class));
}
/**
* 从JWT令牌中获取权限列表
*/
@SuppressWarnings("unchecked")
public List<String> getAuthoritiesFromToken(String token) {
try {
return getClaimFromToken(token, claims -> claims.get("authorities", List.class));
} catch (Exception e) {
log.error("获取权限列表失败", e);
return new ArrayList<>();
}
}
/**
* 获取JWT令牌的签名密钥
*/
private SecretKey getSignKey() {
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
return Keys.hmacShaKeyFor(keyBytes);
}
/**
* 从JWT令牌中获取特定声明
*/
private <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
Claims claims = Jwts.parser()
.verifyWith(getSignKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claimsResolver.apply(claims);
}
}
@@ -0,0 +1,54 @@
package com.nuaa.video.admin.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* 安全配置类
*/
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.cors(cors -> cors.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/admin/auth/login", "/admin/auth/captcha", "/admin/auth/logout", "/actuator/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
@@ -0,0 +1,85 @@
package com.nuaa.video.admin.service;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.nuaa.video.admin.dto.AdminUserDTO;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.vo.AdminUserVO;
import com.nuaa.video.common.model.vo.Result;
/**
* 管理员服务接口
*/
public interface AdminUserService extends IService<AdminUser> {
/**
* 根据用户名查询管理员信息
*
* @param username 用户名
* @return 管理员信息
*/
AdminUser getByUsername(String username);
/**
* 分页查询管理员列表
*
* @param page 分页参数
* @param keyword 关键字
* @return 管理员列表
*/
Result<Page<AdminUserVO>> listUsers(Page<AdminUser> page, String keyword);
/**
* 获取管理员详情
*
* @param id 管理员ID
* @return 管理员详情
*/
Result<AdminUserVO> getUserDetail(Long id);
/**
* 添加管理员
*
* @param adminUserDTO 管理员信息
* @param operator 操作人
* @return 结果
*/
Result<Void> addUser(AdminUserDTO adminUserDTO, String operator);
/**
* 更新管理员
*
* @param adminUserDTO 管理员信息
* @param operator 操作人
* @return 结果
*/
Result<Void> updateUser(AdminUserDTO adminUserDTO, String operator);
/**
* 删除管理员
*
* @param id 管理员ID
* @return 结果
*/
Result<Void> deleteUser(Long id);
/**
* 更新管理员状态
*
* @param id 管理员ID
* @param status 状态
* @param operator 操作人
* @return 结果
*/
Result<Void> updateStatus(Long id, Integer status, String operator);
/**
* 重置密码
*
* @param id 管理员ID
* @param newPassword 新密码
* @param operator 操作人
* @return 结果
*/
Result<Void> resetPassword(Long id, String newPassword, String operator);
}
@@ -0,0 +1,46 @@
package com.nuaa.video.admin.service;
import com.nuaa.video.admin.dto.LoginDTO;
import com.nuaa.video.admin.security.AdminUserDetails;
import com.nuaa.video.admin.vo.CaptchaVO;
import com.nuaa.video.admin.vo.LoginVO;
import com.nuaa.video.common.model.vo.Result;
import jakarta.servlet.http.HttpServletRequest;
/**
* 认证服务接口
*/
public interface AuthService {
/**
* 生成验证码
*
* @return 验证码数据
*/
Result<CaptchaVO> generateCaptcha();
/**
* 登录
*
* @param loginDTO 登录参数
* @param request HTTP请求
* @return 登录结果
*/
Result<LoginVO> login(LoginDTO loginDTO, HttpServletRequest request);
/**
* 登出
*
* @param token JWT令牌
* @return 结果
*/
Result<Void> logout(String token);
/**
* 获取当前登录用户信息
*
* @param adminUserDetails 用户详情
* @return 用户信息
*/
Result<LoginVO> getCurrentUser(AdminUserDetails adminUserDetails);
}
@@ -0,0 +1,302 @@
package com.nuaa.video.admin.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nuaa.video.admin.dto.AdminUserDTO;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.entity.AdminUserRole;
import com.nuaa.video.admin.mapper.AdminUserMapper;
import com.nuaa.video.admin.mapper.AdminUserRoleMapper;
import com.nuaa.video.admin.service.AdminUserService;
import com.nuaa.video.admin.vo.AdminRoleVO;
import com.nuaa.video.admin.vo.AdminUserVO;
import com.nuaa.video.common.model.vo.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
import java.util.stream.Collectors;
/**
* 管理员服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AdminUserServiceImpl extends ServiceImpl<AdminUserMapper, AdminUser> implements AdminUserService {
private final AdminUserMapper adminUserMapper;
private final AdminUserRoleMapper adminUserRoleMapper;
private final PasswordEncoder passwordEncoder;
@Override
public AdminUser getByUsername(String username) {
return adminUserMapper.selectByUsername(username);
}
@Override
public Result<Page<AdminUserVO>> listUsers(Page<AdminUser> page, String keyword) {
Page<AdminUser> userPage = adminUserMapper.selectUserList(page, keyword);
// 转换为VO
Page<AdminUserVO> voPage = new Page<>();
BeanUtils.copyProperties(userPage, voPage, "records");
List<AdminUserVO> userVOList = new ArrayList<>();
for (AdminUser user : userPage.getRecords()) {
AdminUserVO vo = new AdminUserVO();
BeanUtils.copyProperties(user, vo);
// 设置角色ID列表
if (user.getRoles() != null && !user.getRoles().isEmpty()) {
vo.setRoleIds(user.getRoles().stream().map(role -> role.getId()).collect(Collectors.toList()));
// 设置角色列表
List<AdminRoleVO> roleVOList = user.getRoles().stream().map(role -> {
AdminRoleVO roleVO = new AdminRoleVO();
BeanUtils.copyProperties(role, roleVO);
return roleVO;
}).collect(Collectors.toList());
vo.setRoles(roleVOList);
}
userVOList.add(vo);
}
voPage.setRecords(userVOList);
return Result.success(voPage);
}
@Override
public Result<AdminUserVO> getUserDetail(Long id) {
AdminUser user = adminUserMapper.selectUserDetail(id);
if (user == null) {
return Result.error("管理员不存在");
}
// 转换为VO
AdminUserVO vo = new AdminUserVO();
BeanUtils.copyProperties(user, vo);
// 设置角色ID列表
if (user.getRoles() != null && !user.getRoles().isEmpty()) {
vo.setRoleIds(user.getRoles().stream().map(role -> role.getId()).collect(Collectors.toList()));
// 设置角色列表
List<AdminRoleVO> roleVOList = user.getRoles().stream().map(role -> {
AdminRoleVO roleVO = new AdminRoleVO();
BeanUtils.copyProperties(role, roleVO);
return roleVO;
}).collect(Collectors.toList());
vo.setRoles(roleVOList);
}
return Result.success(vo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> addUser(AdminUserDTO adminUserDTO, String operator) {
// 检查用户名是否存在
AdminUser existUser = getOne(new LambdaQueryWrapper<AdminUser>()
.eq(AdminUser::getUsername, adminUserDTO.getUsername()));
if (existUser != null) {
return Result.error("用户名已存在");
}
// 检查邮箱是否存在
if (StringUtils.hasText(adminUserDTO.getEmail())) {
long count = count(new LambdaQueryWrapper<AdminUser>()
.eq(AdminUser::getEmail, adminUserDTO.getEmail()));
if (count > 0) {
return Result.error("邮箱已存在");
}
}
// 检查手机号是否存在
if (StringUtils.hasText(adminUserDTO.getPhone())) {
long count = count(new LambdaQueryWrapper<AdminUser>()
.eq(AdminUser::getPhone, adminUserDTO.getPhone()));
if (count > 0) {
return Result.error("手机号已存在");
}
}
// 创建管理员
AdminUser adminUser = new AdminUser();
BeanUtils.copyProperties(adminUserDTO, adminUser);
// 生成盐值
String salt = UUID.randomUUID().toString().replace("-", "");
adminUser.setSalt(salt);
// 加密密码
if (StringUtils.hasText(adminUserDTO.getPassword())) {
adminUser.setPassword(passwordEncoder.encode(adminUserDTO.getPassword()));
} else {
// 默认密码为123456
adminUser.setPassword(passwordEncoder.encode("123456"));
}
// 设置默认状态
if (adminUser.getStatus() == null) {
adminUser.setStatus(1); // 正常状态
}
adminUser.setCreatedBy(operator);
adminUser.setUpdatedBy(operator);
adminUser.setCreateTime(LocalDateTime.now());
adminUser.setUpdateTime(LocalDateTime.now());
// 保存管理员
save(adminUser);
// 保存角色关联
if (adminUserDTO.getRoleIds() != null && !adminUserDTO.getRoleIds().isEmpty()) {
adminUserRoleMapper.batchInsert(adminUser.getId(), adminUserDTO.getRoleIds(), operator);
}
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> updateUser(AdminUserDTO adminUserDTO, String operator) {
// 检查管理员是否存在
AdminUser existUser = getById(adminUserDTO.getId());
if (existUser == null) {
return Result.error("管理员不存在");
}
// 检查用户名是否重复
if (!existUser.getUsername().equals(adminUserDTO.getUsername())) {
AdminUser userWithSameName = getOne(new LambdaQueryWrapper<AdminUser>()
.eq(AdminUser::getUsername, adminUserDTO.getUsername()));
if (userWithSameName != null) {
return Result.error("用户名已存在");
}
}
// 检查邮箱是否重复
if (StringUtils.hasText(adminUserDTO.getEmail()) && !adminUserDTO.getEmail().equals(existUser.getEmail())) {
long count = count(new LambdaQueryWrapper<AdminUser>()
.eq(AdminUser::getEmail, adminUserDTO.getEmail())
.ne(AdminUser::getId, adminUserDTO.getId()));
if (count > 0) {
return Result.error("邮箱已存在");
}
}
// 检查手机号是否重复
if (StringUtils.hasText(adminUserDTO.getPhone()) && !adminUserDTO.getPhone().equals(existUser.getPhone())) {
long count = count(new LambdaQueryWrapper<AdminUser>()
.eq(AdminUser::getPhone, adminUserDTO.getPhone())
.ne(AdminUser::getId, adminUserDTO.getId()));
if (count > 0) {
return Result.error("手机号已存在");
}
}
// 更新管理员信息
AdminUser adminUser = new AdminUser();
BeanUtils.copyProperties(adminUserDTO, adminUser);
// 如果密码不为空,则更新密码
if (StringUtils.hasText(adminUserDTO.getPassword())) {
adminUser.setPassword(passwordEncoder.encode(adminUserDTO.getPassword()));
} else {
// 不更新密码
adminUser.setPassword(null);
}
adminUser.setUpdatedBy(operator);
adminUser.setUpdateTime(LocalDateTime.now());
// 保存管理员
updateById(adminUser);
// 更新角色关联
if (adminUserDTO.getRoleIds() != null) {
// 删除原有角色关联
adminUserRoleMapper.delete(new LambdaQueryWrapper<AdminUserRole>()
.eq(AdminUserRole::getAdminId, adminUser.getId()));
// 添加新的角色关联
if (!adminUserDTO.getRoleIds().isEmpty()) {
adminUserRoleMapper.batchInsert(adminUser.getId(), adminUserDTO.getRoleIds(), operator);
}
}
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> deleteUser(Long id) {
// 检查管理员是否存在
AdminUser existUser = getById(id);
if (existUser == null) {
return Result.error("管理员不存在");
}
// 删除管理员
removeById(id);
// 删除角色关联
adminUserRoleMapper.delete(new LambdaQueryWrapper<AdminUserRole>()
.eq(AdminUserRole::getAdminId, id));
return Result.success();
}
@Override
public Result<Void> updateStatus(Long id, Integer status, String operator) {
// 检查管理员是否存在
AdminUser existUser = getById(id);
if (existUser == null) {
return Result.error("管理员不存在");
}
// 更新状态
AdminUser adminUser = new AdminUser();
adminUser.setId(id);
adminUser.setStatus(status);
adminUser.setUpdatedBy(operator);
adminUser.setUpdateTime(LocalDateTime.now());
updateById(adminUser);
return Result.success();
}
@Override
public Result<Void> resetPassword(Long id, String newPassword, String operator) {
// 检查管理员是否存在
AdminUser existUser = getById(id);
if (existUser == null) {
return Result.error("管理员不存在");
}
// 更新密码
AdminUser adminUser = new AdminUser();
adminUser.setId(id);
adminUser.setPassword(passwordEncoder.encode(newPassword));
adminUser.setUpdatedBy(operator);
adminUser.setUpdateTime(LocalDateTime.now());
updateById(adminUser);
return Result.success();
}
}
@@ -0,0 +1,278 @@
package com.nuaa.video.admin.service.impl;
import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.LineCaptcha;
import cn.hutool.core.util.IdUtil;
import com.nuaa.video.admin.dto.LoginDTO;
import com.nuaa.video.admin.entity.AdminPermission;
import com.nuaa.video.admin.entity.AdminRole;
import com.nuaa.video.admin.entity.AdminUser;
import com.nuaa.video.admin.security.AdminUserDetails;
import com.nuaa.video.admin.security.JwtTokenProvider;
import com.nuaa.video.admin.service.AdminUserService;
import com.nuaa.video.admin.service.AuthService;
import com.nuaa.video.admin.vo.CaptchaVO;
import com.nuaa.video.admin.vo.LoginVO;
import com.nuaa.video.common.model.vo.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
/**
* 认证服务实现类
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class AuthServiceImpl implements AuthService {
private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final AdminUserService adminUserService;
private final RedisTemplate<String, Object> redisTemplate;
@Value("${login.retry-count:5}")
private int maxRetryCount;
@Value("${login.lock-time:30}")
private int lockTime;
private static final String CAPTCHA_KEY_PREFIX = "admin:captcha:";
private static final String TOKEN_KEY_PREFIX = "admin:token:";
private static final String LOGIN_RETRY_KEY_PREFIX = "admin:login:retry:";
private static final String LOGIN_LOCK_KEY_PREFIX = "admin:login:lock:";
@Override
public Result<CaptchaVO> generateCaptcha() {
// 生成验证码
LineCaptcha captcha = CaptchaUtil.createLineCaptcha(120, 40, 4, 100);
String code = captcha.getCode();
String imageBase64 = captcha.getImageBase64();
// 生成验证码key
String captchaKey = IdUtil.fastSimpleUUID();
// 将验证码存入Redis,有效期5分钟
redisTemplate.opsForValue().set(CAPTCHA_KEY_PREFIX + captchaKey, code, 5, TimeUnit.MINUTES);
// 构建返回数据
CaptchaVO captchaVO = new CaptchaVO();
captchaVO.setCaptchaKey(captchaKey);
captchaVO.setCaptchaImage("data:image/png;base64," + imageBase64);
return Result.success(captchaVO);
}
@Override
public Result<LoginVO> login(LoginDTO loginDTO, HttpServletRequest request) {
String username = loginDTO.getUsername();
String password = loginDTO.getPassword();
// 检查账户是否被锁定
Boolean isLocked = (Boolean) redisTemplate.opsForValue().get(LOGIN_LOCK_KEY_PREFIX + username);
if (Boolean.TRUE.equals(isLocked)) {
return Result.error("账户已被锁定,请稍后再试");
}
// 验证验证码
if (StringUtils.hasText(loginDTO.getCaptchaKey()) && StringUtils.hasText(loginDTO.getCaptcha())) {
String captchaKey = CAPTCHA_KEY_PREFIX + loginDTO.getCaptchaKey();
String captchaValue = (String) redisTemplate.opsForValue().get(captchaKey);
if (!StringUtils.hasText(captchaValue) || !captchaValue.equalsIgnoreCase(loginDTO.getCaptcha())) {
return Result.error("验证码错误");
}
// 验证通过后删除验证码
redisTemplate.delete(captchaKey);
}
try {
// 进行认证
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password));
// 认证成功,清除登录失败记录
redisTemplate.delete(LOGIN_RETRY_KEY_PREFIX + username);
// 获取用户详情
AdminUser adminUser = adminUserService.getByUsername(username);
// 设置最后登录时间和IP
AdminUser updateUser = new AdminUser();
updateUser.setId(adminUser.getId());
updateUser.setLastLoginTime(LocalDateTime.now());
updateUser.setLastLoginIp(getIpAddress(request));
adminUserService.updateById(updateUser);
// 构建用户详情
AdminUserDetails userDetails = buildUserDetails(adminUser);
// 生成token
String token = jwtTokenProvider.generateToken(userDetails);
// 将token存入Redis,有效期等同于token过期时间
redisTemplate.opsForValue().set(TOKEN_KEY_PREFIX + token, token, 24, TimeUnit.HOURS);
// 构建登录返回数据
LoginVO loginVO = buildLoginVO(adminUser, token);
return Result.success(loginVO);
} catch (BadCredentialsException e) {
// 认证失败,记录登录失败次数
Integer retryCount = (Integer) redisTemplate.opsForValue().get(LOGIN_RETRY_KEY_PREFIX + username);
if (retryCount == null) {
retryCount = 0;
}
retryCount++;
// 如果超过最大重试次数,锁定账户
if (retryCount >= maxRetryCount) {
redisTemplate.opsForValue().set(LOGIN_LOCK_KEY_PREFIX + username, true, lockTime, TimeUnit.MINUTES);
return Result.error("账户已被锁定,请" + lockTime + "分钟后再试");
}
// 保存重试次数
redisTemplate.opsForValue().set(LOGIN_RETRY_KEY_PREFIX + username, retryCount, 1, TimeUnit.HOURS);
return Result.error("用户名或密码错误,还剩" + (maxRetryCount - retryCount) + "次机会");
} catch (Exception e) {
log.error("登录异常", e);
return Result.error("登录失败:" + e.getMessage());
}
}
@Override
public Result<Void> logout(String token) {
if (StringUtils.hasText(token)) {
// 从Redis中删除token
redisTemplate.delete(TOKEN_KEY_PREFIX + token);
}
return Result.success();
}
@Override
public Result<LoginVO> getCurrentUser(AdminUserDetails adminUserDetails) {
// 获取用户信息
AdminUser adminUser = adminUserService.getById(adminUserDetails.getId());
if (adminUser == null) {
return Result.error("用户不存在");
}
// 构建登录返回数据
LoginVO loginVO = buildLoginVO(adminUser, null);
return Result.success(loginVO);
}
/**
* 构建用户详情
*/
private AdminUserDetails buildUserDetails(AdminUser adminUser) {
AdminUserDetails userDetails = new AdminUserDetails();
userDetails.setId(adminUser.getId());
userDetails.setUsername(adminUser.getUsername());
userDetails.setPassword(adminUser.getPassword());
// 设置权限
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// 添加角色权限
if (adminUser.getRoles() != null && !adminUser.getRoles().isEmpty()) {
for (AdminRole role : adminUser.getRoles()) {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getCode()));
// 添加权限
if (role.getPermissions() != null && !role.getPermissions().isEmpty()) {
for (AdminPermission permission : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(permission.getCode()));
}
}
}
}
userDetails.setAuthorities(authorities);
return userDetails;
}
/**
* 构建登录返回数据
*/
private LoginVO buildLoginVO(AdminUser adminUser, String token) {
LoginVO loginVO = new LoginVO();
loginVO.setToken(token);
loginVO.setTokenType("Bearer");
loginVO.setExpiresIn(24 * 60 * 60L); // 24小时
loginVO.setAdminId(adminUser.getId());
loginVO.setUsername(adminUser.getUsername());
loginVO.setRealName(adminUser.getRealName());
loginVO.setAvatar(adminUser.getAvatar());
// 设置角色和权限
List<String> roles = new ArrayList<>();
List<String> permissions = new ArrayList<>();
if (adminUser.getRoles() != null && !adminUser.getRoles().isEmpty()) {
// 设置角色
roles = adminUser.getRoles().stream()
.map(AdminRole::getCode)
.collect(Collectors.toList());
// 设置权限
for (AdminRole role : adminUser.getRoles()) {
if (role.getPermissions() != null && !role.getPermissions().isEmpty()) {
List<String> rolePermissions = role.getPermissions().stream()
.map(AdminPermission::getCode)
.collect(Collectors.toList());
permissions.addAll(rolePermissions);
}
}
}
loginVO.setRoles(roles);
loginVO.setPermissions(permissions);
return loginVO;
}
/**
* 获取请求的IP地址
*/
private String getIpAddress(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("Proxy-Client-IP");
}
if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getHeader("WL-Proxy-Client-IP");
}
if (!StringUtils.hasText(ip) || "unknown".equalsIgnoreCase(ip)) {
ip = request.getRemoteAddr();
}
// 多个代理的情况,第一个IP为客户端真实IP
if (ip != null && ip.indexOf(",") > 0) {
ip = ip.substring(0, ip.indexOf(","));
}
return ip;
}
}
@@ -0,0 +1,39 @@
package com.nuaa.video.admin.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 权限VO
*/
@Data
public class AdminPermissionVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 权限ID
*/
private Long id;
/**
* 权限名称
*/
private String name;
/**
* 权限编码
*/
private String code;
/**
* 权限描述
*/
private String description;
/**
* 创建时间
*/
private LocalDateTime createTime;
}
@@ -0,0 +1,50 @@
package com.nuaa.video.admin.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 角色VO
*/
@Data
public class AdminRoleVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 角色ID
*/
private Long id;
/**
* 角色名称
*/
private String name;
/**
* 角色编码
*/
private String code;
/**
* 角色描述
*/
private String description;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 权限ID集合
*/
private List<Long> permissionIds;
/**
* 权限列表
*/
private List<AdminPermissionVO> permissions;
}
@@ -0,0 +1,75 @@
package com.nuaa.video.admin.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.List;
/**
* 管理员VO
*/
@Data
public class AdminUserVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 管理员ID
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 真实姓名
*/
private String realName;
/**
* 头像URL
*/
private String avatar;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phone;
/**
* 状态(0:禁用,1:正常)
*/
private Integer status;
/**
* 最后登录时间
*/
private LocalDateTime lastLoginTime;
/**
* 最后登录IP
*/
private String lastLoginIp;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 角色ID集合
*/
private List<Long> roleIds;
/**
* 角色列表
*/
private List<AdminRoleVO> roles;
}
@@ -0,0 +1,27 @@
package com.nuaa.video.admin.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* 验证码VO
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CaptchaVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 验证码Key
*/
private String captchaKey;
/**
* 验证码图片(Base64编码)
*/
private String captchaImage;
}
@@ -0,0 +1,59 @@
package com.nuaa.video.admin.vo;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
/**
* 登录结果VO
*/
@Data
public class LoginVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 访问令牌
*/
private String token;
/**
* 令牌类型
*/
private String tokenType;
/**
* 过期时间(秒)
*/
private Long expiresIn;
/**
* 管理员ID
*/
private Long adminId;
/**
* 用户名
*/
private String username;
/**
* 真实姓名
*/
private String realName;
/**
* 头像URL
*/
private String avatar;
/**
* 角色编码列表
*/
private List<String> roles;
/**
* 权限编码列表
*/
private List<String> permissions;
}
@@ -0,0 +1,44 @@
package com.nuaa.video.admin.vo;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
* 系统配置VO
*/
@Data
public class SystemConfigVO implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 配置ID
*/
private Long id;
/**
* 配置键
*/
private String configKey;
/**
* 配置值
*/
private String configValue;
/**
* 配置描述
*/
private String description;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}
@@ -0,0 +1,29 @@
spring:
datasource:
dynamic:
primary: master
strict: false
datasource:
master:
url: jdbc:mysql://5.180.253.115:3306/video_admin?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root
password: sty@G458221
driver-class-name: com.mysql.cj.jdbc.Driver
data:
redis:
database: 5
host: 5.180.253.115
port: 6379
password: redis_EsJ8FX
timeout: 10000
# 开发环境启用操作日志记录
operation-log:
enabled: true
# 开发环境日志配置
logging:
level:
com.nuaa.video: debug
org.springframework.security: debug
+91 -10
View File
@@ -4,30 +4,111 @@ server:
spring:
application:
name: video-admin
profiles:
active: dev
config:
import:
- optional:nacos:${spring.application.name}.yaml
cloud:
nacos:
discovery:
server-addr: 5.180.253.115:8848
namespace: public
group: DEFAULT_GROUP
server-addr: ${NACOS_HOST:5.180.253.115}:${NACOS_PORT:8848}
namespace: ${NACOS_NAMESPACE:public}
group: ${NACOS_GROUP:DEFAULT_GROUP}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
namespace: ${spring.cloud.nacos.discovery.namespace}
group: ${spring.cloud.nacos.discovery.group}
file-extension: yaml
sentinel:
transport:
dashboard: 5.180.253.115:8080
dashboard: ${SENTINEL_DASHBOARD_HOST:5.180.253.115}:${SENTINEL_DASHBOARD_PORT:8080}
port: 8725
eager: true
datasource:
url: jdbc:mysql://5.180.253.115:3306/video_admin?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai
username: root
password: sty@G458221
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:5.180.253.115}:${MYSQL_PORT:3306}/video_admin?useUnicode=true&characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: ${MYSQL_USERNAME:root}
password: ${MYSQL_PASSWORD:sty@G458221}
hikari:
minimum-idle: 5
maximum-pool-size: 20
idle-timeout: 30000
max-lifetime: 1200000
connection-timeout: 30000
connection-test-query: SELECT 1
data:
redis:
host: 5.180.253.115
port: 6379
password: redis_EsJ8FX
host: ${REDIS_HOST:5.180.253.115}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:redis_EsJ8FX}
database: 5
timeout: 10000
lettuce:
pool:
max-active: 200
max-wait: -1
max-idle: 10
min-idle: 0
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.nuaa.video.admin.entity
configuration:
map-underscore-to-camel-case: true
cache-enabled: false
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
global-config:
db-config:
id-type: auto
logic-delete-field: status
logic-delete-value: 0
logic-not-delete-value: 1
# JWT配置
jwt:
secret: video-platform-admin-secret-key-2024-nuaa
expiration: 86400 # 24小时
token-header: Authorization
token-prefix: "Bearer "
# 登录配置
login:
# 最大重试次数
retry-count: 5
# 锁定时间(分钟)
lock-time: 30
# 操作日志配置
operation-log:
enabled: true
exclude-paths:
- /admin/auth/login
- /admin/auth/logout
- /admin/auth/captcha
- /actuator/**
management:
endpoints:
web:
exposure:
include: '*'
endpoint:
health:
show-details: always
feign:
client:
config:
default:
connectTimeout: 5000
readTimeout: 5000
circuitbreaker:
enabled: true
logging:
level:
com.nuaa.video: info
org.springframework.security: info
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.admin.mapper.AdminOperationLogMapper">
<!-- 分页查询操作日志 -->
<select id="selectLogList" resultType="com.nuaa.video.admin.entity.AdminOperationLog">
SELECT
*
FROM
admin_operation_log
<where>
<if test="username != null and username != ''">
AND username LIKE CONCAT('%', #{username}, '%')
</if>
<if test="operation != null and operation != ''">
AND operation LIKE CONCAT('%', #{operation}, '%')
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null and startTime != ''">
AND operation_time &gt;= #{startTime}
</if>
<if test="endTime != null and endTime != ''">
AND operation_time &lt;= #{endTime}
</if>
</where>
ORDER BY operation_time DESC
</select>
</mapper>
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.admin.mapper.AdminPermissionMapper">
<!-- 获取角色的权限列表 -->
<select id="selectPermissionsByRoleId" resultType="com.nuaa.video.admin.entity.AdminPermission">
SELECT
p.*
FROM
admin_permission p
INNER JOIN
admin_role_permission rp ON p.id = rp.permission_id
WHERE
rp.role_id = #{roleId}
</select>
<!-- 获取管理员的权限列表 -->
<select id="selectPermissionsByAdminId" resultType="com.nuaa.video.admin.entity.AdminPermission">
SELECT DISTINCT
p.*
FROM
admin_permission p
INNER JOIN
admin_role_permission rp ON p.id = rp.permission_id
INNER JOIN
admin_role r ON rp.role_id = r.id
INNER JOIN
admin_user_role ur ON r.id = ur.role_id
WHERE
ur.admin_id = #{adminId}
</select>
</mapper>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.admin.mapper.AdminRoleMapper">
<!-- 分页查询角色列表 -->
<select id="selectRoleList" resultMap="roleMap">
SELECT
r.*,
p.id AS perm_id,
p.name AS perm_name,
p.code AS perm_code,
p.description AS perm_description
FROM
admin_role r
LEFT JOIN
admin_role_permission rp ON r.id = rp.role_id
LEFT JOIN
admin_permission p ON rp.permission_id = p.id
<where>
<if test="keyword != null and keyword != ''">
AND (r.name LIKE CONCAT('%', #{keyword}, '%')
OR r.code LIKE CONCAT('%', #{keyword}, '%')
OR r.description LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
ORDER BY r.id ASC
</select>
<!-- 获取角色详情 -->
<select id="selectRoleDetail" resultMap="roleMap">
SELECT
r.*,
p.id AS perm_id,
p.name AS perm_name,
p.code AS perm_code,
p.description AS perm_description
FROM
admin_role r
LEFT JOIN
admin_role_permission rp ON r.id = rp.role_id
LEFT JOIN
admin_permission p ON rp.permission_id = p.id
WHERE
r.id = #{id}
</select>
<!-- 获取管理员的角色列表 -->
<select id="selectRolesByAdminId" resultType="com.nuaa.video.admin.entity.AdminRole">
SELECT
r.*
FROM
admin_role r
INNER JOIN
admin_user_role ur ON r.id = ur.role_id
WHERE
ur.admin_id = #{adminId}
</select>
<!-- 结果映射 -->
<resultMap id="roleMap" type="com.nuaa.video.admin.entity.AdminRole">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="code" column="code"/>
<result property="description" column="description"/>
<result property="createdBy" column="created_by"/>
<result property="updatedBy" column="updated_by"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<!-- 权限集合 -->
<collection property="permissions" ofType="com.nuaa.video.admin.entity.AdminPermission">
<id property="id" column="perm_id"/>
<result property="name" column="perm_name"/>
<result property="code" column="perm_code"/>
<result property="description" column="perm_description"/>
</collection>
</resultMap>
</mapper>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.admin.mapper.AdminRolePermissionMapper">
<!-- 批量插入角色权限关联 -->
<insert id="batchInsert">
INSERT INTO admin_role_permission (role_id, permission_id, created_by, create_time)
VALUES
<foreach collection="permissionIds" item="permissionId" separator=",">
(#{roleId}, #{permissionId}, #{createdBy}, NOW())
</foreach>
</insert>
</mapper>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.admin.mapper.AdminSystemConfigMapper">
<!-- 根据配置键获取配置值 -->
<select id="getConfigValue" resultType="java.lang.String">
SELECT
config_value
FROM
admin_system_config
WHERE
config_key = #{configKey}
LIMIT 1
</select>
</mapper>
@@ -0,0 +1,116 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.admin.mapper.AdminUserMapper">
<!-- 根据用户名查询管理员信息 -->
<select id="selectByUsername" resultMap="adminUserMap">
SELECT
au.*,
ar.id AS role_id,
ar.name AS role_name,
ar.code AS role_code,
ar.description AS role_description,
ap.id AS perm_id,
ap.name AS perm_name,
ap.code AS perm_code,
ap.description AS perm_description
FROM
admin_user au
LEFT JOIN
admin_user_role aur ON au.id = aur.admin_id
LEFT JOIN
admin_role ar ON aur.role_id = ar.id
LEFT JOIN
admin_role_permission arp ON ar.id = arp.role_id
LEFT JOIN
admin_permission ap ON arp.permission_id = ap.id
WHERE
au.username = #{username}
</select>
<!-- 分页查询管理员列表 -->
<select id="selectUserList" resultMap="adminUserMap">
SELECT
au.*,
ar.id AS role_id,
ar.name AS role_name,
ar.code AS role_code
FROM
admin_user au
LEFT JOIN
admin_user_role aur ON au.id = aur.admin_id
LEFT JOIN
admin_role ar ON aur.role_id = ar.id
<where>
<if test="keyword != null and keyword != ''">
AND (au.username LIKE CONCAT('%', #{keyword}, '%')
OR au.real_name LIKE CONCAT('%', #{keyword}, '%')
OR au.email LIKE CONCAT('%', #{keyword}, '%')
OR au.phone LIKE CONCAT('%', #{keyword}, '%'))
</if>
</where>
GROUP BY au.id
ORDER BY au.id ASC
</select>
<!-- 获取管理员详情 -->
<select id="selectUserDetail" resultMap="adminUserMap">
SELECT
au.*,
ar.id AS role_id,
ar.name AS role_name,
ar.code AS role_code,
ar.description AS role_description,
ap.id AS perm_id,
ap.name AS perm_name,
ap.code AS perm_code,
ap.description AS perm_description
FROM
admin_user au
LEFT JOIN
admin_user_role aur ON au.id = aur.admin_id
LEFT JOIN
admin_role ar ON aur.role_id = ar.id
LEFT JOIN
admin_role_permission arp ON ar.id = arp.role_id
LEFT JOIN
admin_permission ap ON arp.permission_id = ap.id
WHERE
au.id = #{id}
</select>
<!-- 结果映射 -->
<resultMap id="adminUserMap" type="com.nuaa.video.admin.entity.AdminUser">
<id property="id" column="id"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<result property="salt" column="salt"/>
<result property="realName" column="real_name"/>
<result property="avatar" column="avatar"/>
<result property="email" column="email"/>
<result property="phone" column="phone"/>
<result property="status" column="status"/>
<result property="lastLoginTime" column="last_login_time"/>
<result property="lastLoginIp" column="last_login_ip"/>
<result property="createdBy" column="created_by"/>
<result property="updatedBy" column="updated_by"/>
<result property="createTime" column="create_time"/>
<result property="updateTime" column="update_time"/>
<!-- 角色集合 -->
<collection property="roles" ofType="com.nuaa.video.admin.entity.AdminRole">
<id property="id" column="role_id"/>
<result property="name" column="role_name"/>
<result property="code" column="role_code"/>
<result property="description" column="role_description"/>
<!-- 权限集合 -->
<collection property="permissions" ofType="com.nuaa.video.admin.entity.AdminPermission">
<id property="id" column="perm_id"/>
<result property="name" column="perm_name"/>
<result property="code" column="perm_code"/>
<result property="description" column="perm_description"/>
</collection>
</collection>
</resultMap>
</mapper>
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.nuaa.video.admin.mapper.AdminUserRoleMapper">
<!-- 批量插入用户角色关联 -->
<insert id="batchInsert">
INSERT INTO admin_user_role (admin_id, role_id, created_by, create_time)
VALUES
<foreach collection="roleIds" item="roleId" separator=",">
(#{adminId}, #{roleId}, #{createdBy}, NOW())
</foreach>
</insert>
</mapper>
@@ -0,0 +1,49 @@
package com.nuaa.video.auth.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.auth.entity.AuthUser;
import com.nuaa.video.auth.service.AuthUserService;
import com.nuaa.video.common.model.vo.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 认证管理控制器 - 供管理后台调用
*/
@Slf4j
@RestController
@RequestMapping("/auth/admin")
@RequiredArgsConstructor
public class AdminAuthController {
private final AuthUserService authUserService;
/**
* 获取登录统计 - 管理接口
*/
@GetMapping("/login/stats")
public Result<Object> getLoginStats() {
return authUserService.getLoginStats();
}
/**
* 管理员重置用户密码 - 管理接口
*/
@PutMapping("/reset-password/{userId}")
public Result<Void> resetPassword(
@PathVariable Long userId,
@RequestParam String newPassword) {
return authUserService.adminResetPassword(userId, newPassword);
}
/**
* 锁定/解锁用户账号 - 管理接口
*/
@PutMapping("/lock/{userId}/{locked}")
public Result<Void> lockUser(
@PathVariable Long userId,
@PathVariable Boolean locked) {
return authUserService.lockUser(userId, locked);
}
}
@@ -59,4 +59,19 @@ public interface AuthUserService extends IService<AuthUser> {
* @return 用户信息
*/
Result<AuthUser> getCurrentUser(Long userId);
/**
* 获取登录统计 - 管理接口
*/
Result<Object> getLoginStats();
/**
* 管理员重置用户密码 - 管理接口
*/
Result<Void> adminResetPassword(Long userId, String newPassword);
/**
* 锁定/解锁用户账号 - 管理接口
*/
Result<Void> lockUser(Long userId, Boolean locked);
}
@@ -1,5 +1,6 @@
package com.nuaa.video.auth.service.impl;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -24,10 +25,14 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 用户认证服务实现类
@@ -283,6 +288,109 @@ public class AuthUserServiceImpl extends ServiceImpl<AuthUserMapper, AuthUser> i
return Result.success(user);
}
@Override
public Result<Object> getLoginStats() {
Map<String, Object> stats = new HashMap<>();
// 这里是模拟数据,实际项目中需要从数据库或缓存中获取
// 总用户数
long totalUsers = count();
stats.put("totalUsers", totalUsers);
// 今日登录用户数
LocalDate today = LocalDate.now();
long todayLoginUsers = 0; // 实际需要查询登录记录表
stats.put("todayLoginUsers", todayLoginUsers);
// 当前在线用户数(可从Redis中获取)
long onlineUsers = 0; // 实际需要从Redis或其他存储中获取
stats.put("onlineUsers", onlineUsers);
// 最近7天每日登录用户数
Map<String, Long> dailyLoginUsers = new HashMap<>();
for (int i = 6; i >= 0; i--) {
LocalDate date = today.minusDays(i);
String dateStr = date.toString();
long count = 0; // 实际需要查询登录记录表
dailyLoginUsers.put(dateStr, count);
}
stats.put("dailyLoginUsers", dailyLoginUsers);
return Result.success(stats);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> adminResetPassword(Long userId, String newPassword) {
// 查询用户是否存在
AuthUser user = getById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 生成新的盐值
String salt = generateSalt();
// 加密密码
String encryptedPassword = encryptPassword(newPassword, salt);
// 更新密码和盐值
AuthUser updateUser = new AuthUser();
updateUser.setId(userId);
updateUser.setPassword(encryptedPassword);
updateUser.setSalt(salt);
updateUser.setUpdatedBy("admin");
updateUser.setUpdateTime(LocalDateTime.now());
updateById(updateUser);
// 清除用户token
String tokenKey = "auth:token:" + userId;
redisUtils.del(tokenKey);
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> lockUser(Long userId, Boolean locked) {
// 查询用户是否存在
AuthUser user = getById(userId);
if (user == null) {
return Result.error("用户不存在");
}
// 更新用户状态
AuthUser updateUser = new AuthUser();
updateUser.setId(userId);
updateUser.setStatus(locked ? 0 : 1); // 0-禁用,1-正常
updateUser.setUpdatedBy("admin");
updateUser.setUpdateTime(LocalDateTime.now());
updateById(updateUser);
// 如果锁定用户,清除用户token
if (locked) {
String tokenKey = "auth:token:" + userId;
redisUtils.del(tokenKey);
}
return Result.success();
}
/**
* 生成盐值
*/
private String generateSalt() {
return UUID.randomUUID().toString().replace("-", "");
}
/**
* 加密密码
*/
private String encryptPassword(String password, String salt) {
// 实际项目中应该使用加密算法如BCrypt
return DigestUtils.md5DigestAsHex((password + salt).getBytes());
}
/**
* 获取用户角色
*
@@ -0,0 +1,61 @@
package com.nuaa.video.comment.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.comment.entity.Comment;
import com.nuaa.video.comment.service.CommentService;
import com.nuaa.video.comment.vo.CommentStatVO;
import com.nuaa.video.comment.vo.CommentVO;
import com.nuaa.video.common.model.vo.Result;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 评论管理控制器 - 供管理后台调用
*/
@Slf4j
@RestController
@RequestMapping("/comment/admin")
@RequiredArgsConstructor
public class AdminCommentController {
private final CommentService commentService;
/**
* 获取评论列表 - 管理接口
*/
@GetMapping("/list")
public Result<Page<CommentVO>> getCommentList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "videoId", required = false) Long videoId,
@RequestParam(value = "userId", required = false) Long userId,
@RequestParam(value = "keyword", required = false) String keyword) {
return commentService.getAdminCommentList(page, size, videoId, userId, keyword);
}
/**
* 获取评论详情 - 管理接口
*/
@GetMapping("/{commentId}")
public Result<CommentVO> getCommentDetail(@PathVariable Long commentId) {
return commentService.getAdminCommentDetail(commentId);
}
/**
* 删除评论 - 管理接口
*/
@DeleteMapping("/{commentId}")
public Result<Void> deleteComment(@PathVariable Long commentId) {
return commentService.adminDeleteComment(commentId);
}
/**
* 获取评论统计信息 - 管理接口
*/
@GetMapping("/stats")
public Result<CommentStatVO> getCommentStats() {
return commentService.getCommentStats();
}
}
@@ -95,4 +95,24 @@ public interface CommentService extends IService<Comment> {
* @return 统计信息
*/
Result<CommentStatVO> getCommentStat(Long videoId);
/**
* 获取评论列表 - 管理接口
*/
Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword);
/**
* 获取评论详情 - 管理接口
*/
Result<CommentVO> getAdminCommentDetail(Long commentId);
/**
* 管理员删除评论 - 管理接口
*/
Result<Void> adminDeleteComment(Long commentId);
/**
* 获取评论统计信息 - 管理接口
*/
Result<CommentStatVO> getCommentStats();
}
@@ -25,7 +25,9 @@ import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@@ -362,6 +364,125 @@ public class CommentServiceImpl extends ServiceImpl<CommentMapper, Comment> impl
return Result.success(statVO);
}
@Override
public Result<Page<CommentVO>> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword) {
if (page == null || page < 1) {
page = 1;
}
if (size == null || size < 1) {
size = 10;
}
// 构建查询条件
LambdaQueryWrapper<Comment> queryWrapper = new LambdaQueryWrapper<>();
// 管理员可以查看所有状态的评论,包括已删除的
// 按视频ID筛选
if (videoId != null) {
queryWrapper.eq(Comment::getVideoId, videoId);
}
// 按用户ID筛选
if (userId != null) {
queryWrapper.eq(Comment::getUserId, userId);
}
// 按关键词筛选
if (StringUtils.hasText(keyword)) {
queryWrapper.like(Comment::getContent, keyword);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(Comment::getCreateTime);
// 分页查询
Page<Comment> commentPage = new Page<>(page, size);
commentPage = page(commentPage, queryWrapper);
// 转换为VO
Page<CommentVO> voPage = new Page<>();
BeanUtils.copyProperties(commentPage, voPage, "records");
List<CommentVO> commentVOList = new ArrayList<>();
for (Comment comment : commentPage.getRecords()) {
CommentVO vo = convertToCommentVO(comment, null);
commentVOList.add(vo);
}
voPage.setRecords(commentVOList);
return Result.success(voPage);
}
@Override
public Result<CommentVO> getAdminCommentDetail(Long commentId) {
// 查询评论信息
Comment comment = getById(commentId);
if (comment == null) {
return Result.error("评论不存在");
}
// 转换为VO
CommentVO vo = convertToCommentVO(comment, null);
// 获取回复列表
Result<List<ReplyVO>> repliesResult = commentReplyService.getHotReplies(commentId, 10, null);
if (repliesResult.getCode() == 200 && repliesResult.getData() != null) {
vo.setHotReplies(repliesResult.getData());
} else {
vo.setHotReplies(new ArrayList<>());
}
return Result.success(vo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> adminDeleteComment(Long commentId) {
// 查询评论是否存在
Comment comment = getById(commentId);
if (comment == null) {
return Result.error("评论不存在");
}
// 逻辑删除评论(设置状态为0)
comment.setStatus(0);
comment.setUpdatedBy("admin");
comment.setUpdateTime(LocalDateTime.now());
updateById(comment);
// 更新视频评论数
contentFeignClient.increaseCommentCount(comment.getVideoId(), -1, "Bearer ");
return Result.success();
}
@Override
public Result<CommentStatVO> getCommentStats() {
CommentStatVO statVO = new CommentStatVO();
// 获取各种统计数据
// 1. 总评论数
long totalComments = count();
statVO.setCommentCount((int)totalComments);
// 2. 今日新增评论数
LocalDate today = LocalDate.now();
long todayComments = count(new LambdaQueryWrapper<Comment>()
.ge(Comment::getCreateTime, today.atStartOfDay()));
// 3. 回复总数
long totalReplies = commentReplyMapper.selectCount(null);
statVO.setReplyCount((int)totalReplies);
// 4. 总点赞数
long totalLikes = commentLikeMapper.selectCount(null);
statVO.setLikeCount((int)totalLikes);
return Result.success(statVO);
}
/**
* 将评论实体转换为VO
*/
@@ -0,0 +1,55 @@
package com.nuaa.video.content.controller;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentCategory;
import com.nuaa.video.content.service.CategoryService;
import com.nuaa.video.content.vo.CategoryVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 分类管理控制器 - 供管理后台调用
*/
@Slf4j
@RestController
@RequestMapping("/content/category/admin")
@RequiredArgsConstructor
public class AdminCategoryController {
private final CategoryService categoryService;
/**
* 获取分类列表 - 管理接口
*/
@GetMapping("/list")
public Result<List<CategoryVO>> getCategoryList() {
return categoryService.getCategoryList();
}
/**
* 添加分类 - 管理接口
*/
@PostMapping
public Result<Void> addCategory(@RequestBody ContentCategory category) {
return categoryService.addCategory(category);
}
/**
* 更新分类 - 管理接口
*/
@PutMapping
public Result<Void> updateCategory(@RequestBody ContentCategory category) {
return categoryService.updateCategory(category);
}
/**
* 删除分类 - 管理接口
*/
@DeleteMapping("/{categoryId}")
public Result<Void> deleteCategory(@PathVariable Long categoryId) {
return categoryService.deleteCategory(categoryId);
}
}
@@ -0,0 +1,80 @@
package com.nuaa.video.content.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.content.entity.ContentVideo;
import com.nuaa.video.content.dto.VideoQueryDTO;
import com.nuaa.video.content.service.VideoService;
import com.nuaa.video.content.vo.VideoInfoVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 视频管理控制器 - 供管理后台调用
*/
@Slf4j
@RestController
@RequestMapping("/content/video/admin")
@RequiredArgsConstructor
public class AdminVideoController {
private final VideoService videoService;
/**
* 获取视频列表 - 管理接口
*/
@GetMapping("/list")
public Result<Page<VideoInfoVO>> getVideoList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "status", required = false) Integer status) {
VideoQueryDTO queryDTO = new VideoQueryDTO();
queryDTO.setKeyword(keyword);
queryDTO.setCategoryId(categoryId);
queryDTO.setStatus(status);
queryDTO.setPageNum(page);
queryDTO.setPageSize(size);
Page<ContentVideo> pageParam = new Page<>(page, size);
return videoService.getAdminVideoList(pageParam, queryDTO);
}
/**
* 获取视频详情 - 管理接口
*/
@GetMapping("/{videoId}")
public Result<VideoInfoVO> getVideoDetail(@PathVariable Long videoId) {
return videoService.getAdminVideoDetail(videoId);
}
/**
* 审核视频 - 管理接口
*/
@PostMapping("/review/{videoId}")
public Result<Void> reviewVideo(
@PathVariable Long videoId,
@RequestParam Integer status,
@RequestParam(required = false) String reason) {
return videoService.reviewVideo(videoId, null, status, reason);
}
/**
* 下架视频 - 管理接口
*/
@PutMapping("/disable/{videoId}")
public Result<Void> disableVideo(@PathVariable Long videoId) {
return videoService.disableVideo(videoId);
}
/**
* 获取视频统计信息 - 管理接口
*/
@GetMapping("/stats")
public Result<Object> getVideoStats() {
return videoService.getVideoStats();
}
}
@@ -155,4 +155,24 @@ public interface VideoService extends IService<ContentVideo> {
* @return 结果
*/
Result<Void> reviewVideo(Long videoId, Long reviewerId, Integer status, String reason);
/**
* 获取视频列表 - 管理接口
*/
Result<Page<VideoInfoVO>> getAdminVideoList(Page<ContentVideo> page, VideoQueryDTO queryDTO);
/**
* 获取视频详情 - 管理接口
*/
Result<VideoInfoVO> getAdminVideoDetail(Long videoId);
/**
* 下架视频 - 管理接口
*/
Result<Void> disableVideo(Long videoId);
/**
* 获取视频统计信息 - 管理接口
*/
Result<Object> getVideoStats();
}
@@ -27,8 +27,10 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@@ -724,4 +726,195 @@ public class VideoServiceImpl extends ServiceImpl<ContentVideoMapper, ContentVid
return Result.success();
}
// 在 com.nuaa.video.content.service.impl.VideoServiceImpl 类中添加:
@Override
public Result<Page<VideoInfoVO>> getAdminVideoList(Page<ContentVideo> page, VideoQueryDTO queryDTO) {
// 管理员可查看所有状态的视频,不需要过滤status
Page<ContentVideo> videoPage = videoMapper.selectVideoList(page, queryDTO);
// 转换为VO
Page<VideoInfoVO> voPage = new Page<>();
BeanUtils.copyProperties(videoPage, voPage, "records");
List<VideoInfoVO> voList = new ArrayList<>();
for (ContentVideo video : videoPage.getRecords()) {
VideoInfoVO vo = new VideoInfoVO();
BeanUtils.copyProperties(video, vo);
// 获取分类名称
if (video.getCategoryId() != null) {
ContentCategory category = categoryMapper.selectById(video.getCategoryId());
if (category != null) {
vo.setCategoryName(category.getName());
}
}
// 获取标签
Result<List<TagVO>> tagResult = tagService.getVideoTags(video.getId());
if (tagResult.getCode() == 200 && tagResult.getData() != null) {
List<TagVO> tags = tagResult.getData();
vo.setTagIds(tags.stream().map(TagVO::getId).collect(Collectors.toList()));
vo.setTagNames(tags.stream().map(TagVO::getName).collect(Collectors.toList()));
}
// 获取用户信息
try {
Result<Object> userResult = userFeignClient.getUserProfile(video.getUserId());
if (userResult.getCode() == 200 && userResult.getData() != null) {
@SuppressWarnings("unchecked")
Map<String, Object> userData = (Map<String, Object>) userResult.getData();
vo.setUserNickname((String) userData.get("nickname"));
vo.setUserAvatar((String) userData.get("avatar"));
}
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage());
}
voList.add(vo);
}
voPage.setRecords(voList);
return Result.success(voPage);
}
@Override
public Result<VideoInfoVO> getAdminVideoDetail(Long videoId) {
// 查询视频信息
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 转换为VO
VideoInfoVO vo = new VideoInfoVO();
BeanUtils.copyProperties(video, vo);
// 获取分类名称
if (video.getCategoryId() != null) {
ContentCategory category = categoryMapper.selectById(video.getCategoryId());
if (category != null) {
vo.setCategoryName(category.getName());
}
}
// 获取标签
Result<List<TagVO>> tagResult = tagService.getVideoTags(video.getId());
if (tagResult.getCode() == 200 && tagResult.getData() != null) {
List<TagVO> tags = tagResult.getData();
vo.setTagIds(tags.stream().map(TagVO::getId).collect(Collectors.toList()));
vo.setTagNames(tags.stream().map(TagVO::getName).collect(Collectors.toList()));
}
// 获取用户信息
try {
Result<Object> userResult = userFeignClient.getUserProfile(video.getUserId());
if (userResult.getCode() == 200 && userResult.getData() != null) {
@SuppressWarnings("unchecked")
Map<String, Object> userData = (Map<String, Object>) userResult.getData();
vo.setUserNickname((String) userData.get("nickname"));
vo.setUserAvatar((String) userData.get("avatar"));
}
} catch (Exception e) {
log.error("获取用户信息失败: {}", e.getMessage());
}
// 获取审核记录
ContentVideoReview latestReview = videoReviewMapper.selectOne(
new LambdaQueryWrapper<ContentVideoReview>()
.eq(ContentVideoReview::getVideoId, videoId)
.orderByDesc(ContentVideoReview::getCreateTime)
.last("LIMIT 1")
);
// 可以将审核记录信息添加到VO中返回
if (latestReview != null) {
vo.setReviewStatus(latestReview.getStatus());
vo.setReviewReason(latestReview.getReason());
vo.setReviewerId(latestReview.getReviewerId());
vo.setReviewTime(latestReview.getCreateTime());
}
return Result.success(vo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> disableVideo(Long videoId) {
// 查询视频是否存在
ContentVideo video = getById(videoId);
if (video == null) {
return Result.error("视频不存在");
}
// 更新视频状态为已下架
ContentVideo updateVideo = new ContentVideo();
updateVideo.setId(videoId);
updateVideo.setStatus(2); // 已下架
updateVideo.setUpdatedBy("admin");
updateVideo.setUpdateTime(LocalDateTime.now());
updateById(updateVideo);
// 记录操作日志
ContentVideoReview review = new ContentVideoReview();
review.setVideoId(videoId);
review.setReviewerId(0L); // 系统/管理员ID
review.setStatus(2); // 下架
review.setReason("管理员下架操作");
review.setCreatedBy("admin");
review.setCreateTime(LocalDateTime.now());
videoReviewMapper.insert(review);
return Result.success();
}
@Override
public Result<Object> getVideoStats() {
// 收集视频统计数据
Map<String, Object> stats = new HashMap<>();
// 总视频数
long totalVideos = count();
stats.put("totalVideos", totalVideos);
// 今日新增视频
LocalDate today = LocalDate.now();
long todayNewVideos = count(new LambdaQueryWrapper<ContentVideo>()
.ge(ContentVideo::getCreateTime, today.atStartOfDay()));
stats.put("todayNewVideos", todayNewVideos);
// 待审核视频数
long pendingReviewVideos = count(new LambdaQueryWrapper<ContentVideo>()
.eq(ContentVideo::getStatus, 0));
stats.put("pendingReviewVideos", pendingReviewVideos);
// 已发布视频数
long publishedVideos = count(new LambdaQueryWrapper<ContentVideo>()
.eq(ContentVideo::getStatus, 1));
stats.put("publishedVideos", publishedVideos);
// 已下架视频数
long disabledVideos = count(new LambdaQueryWrapper<ContentVideo>()
.eq(ContentVideo::getStatus, 2));
stats.put("disabledVideos", disabledVideos);
// 审核未通过视频数
long rejectedVideos = count(new LambdaQueryWrapper<ContentVideo>()
.eq(ContentVideo::getStatus, 3));
stats.put("rejectedVideos", rejectedVideos);
// 各分类视频数量分布
Map<String, Long> categoryDistribution = new HashMap<>();
List<ContentCategory> categories = categoryMapper.selectList(null);
for (ContentCategory category : categories) {
long count = count(new LambdaQueryWrapper<ContentVideo>()
.eq(ContentVideo::getCategoryId, category.getId()));
categoryDistribution.put(category.getName(), count);
}
stats.put("categoryDistribution", categoryDistribution);
return Result.success(stats);
}
}
@@ -142,4 +142,24 @@ public class VideoInfoVO implements Serializable {
* 发布时间
*/
private LocalDateTime publishTime;
/**
* 审核员ID
*/
private Long reviewerId;
/**
* 审核状态(1:通过,2:不通过)
*/
private Integer reviewStatus;
/**
* 审核原因
*/
private String reviewReason;
/**
* 创建时间
*/
private LocalDateTime reviewTime;
}
@@ -0,0 +1,70 @@
package com.nuaa.video.user.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.nuaa.video.common.model.vo.Result;
import com.nuaa.video.user.entity.UserInfo;
import com.nuaa.video.user.service.UserInfoService;
import com.nuaa.video.user.vo.UserProfileVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
/**
* 用户管理控制器 - 供管理后台调用
*/
@Slf4j
@RestController
@RequestMapping("/user/profile/admin")
@RequiredArgsConstructor
public class AdminUserController {
private final UserInfoService userInfoService;
/**
* 获取用户列表 - 管理接口
*/
@GetMapping("/list")
public Result<Page<UserProfileVO>> getUserList(
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "size", defaultValue = "10") Integer size,
@RequestParam(value = "keyword", required = false) String keyword) {
Page<UserInfo> pageParam = new Page<>(page, size);
return userInfoService.getAdminUserList(pageParam, keyword);
}
/**
* 获取用户详情 - 管理接口
*/
@GetMapping("/{userId}")
public Result<UserProfileVO> getUserDetail(@PathVariable Long userId) {
return userInfoService.getAdminUserDetail(userId);
}
/**
* 禁用/启用用户 - 管理接口
*/
@PutMapping("/status/{userId}/{status}")
public Result<Void> updateUserStatus(
@PathVariable Long userId,
@PathVariable Integer status) {
return userInfoService.updateUserStatus(userId, status);
}
/**
* 重置用户密码 - 管理接口
*/
@PutMapping("/reset-password/{userId}")
public Result<Void> resetUserPassword(
@PathVariable Long userId,
@RequestParam String newPassword) {
return userInfoService.resetUserPassword(userId, newPassword);
}
/**
* 获取用户统计信息 - 管理接口
*/
@GetMapping("/stats")
public Result<Object> getUserStats() {
return userInfoService.getUserStats();
}
}
@@ -1,11 +1,8 @@
// com.nuaa.video.user.feign.AuthFeignClient.java
package com.nuaa.video.user.feign;
import com.nuaa.video.common.model.vo.Result;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.*;
/**
* 认证服务Feign客户端
@@ -30,4 +27,19 @@ public interface AuthFeignClient {
*/
@GetMapping("/username/{userId}")
String getUsernameById(@PathVariable("userId") Long userId);
/**
* 锁定/解锁用户账号 - 管理接口
*/
@PutMapping("/admin/lock/{userId}/{locked}")
Result<Void> lockUser(@PathVariable Long userId,
@PathVariable Boolean locked);
/**
* 管理员重置用户密码 - 管理接口
*/
@PutMapping("/admin/reset-password/{userId}")
Result<Void> resetPassword(
@PathVariable Long userId,
@RequestParam String newPassword);
}
@@ -92,4 +92,29 @@ public interface UserInfoService extends IService<UserInfo> {
* @return 结果
*/
Result<Void> increaseFavoriteCount(Long userId, Integer count);
/**
* 获取用户列表 - 管理接口
*/
Result<Page<UserProfileVO>> getAdminUserList(Page<UserInfo> page, String keyword);
/**
* 获取用户详情 - 管理接口
*/
Result<UserProfileVO> getAdminUserDetail(Long userId);
/**
* 更新用户状态 - 管理接口
*/
Result<Void> updateUserStatus(Long userId, Integer status);
/**
* 重置用户密码 - 管理接口
*/
Result<Void> resetUserPassword(Long userId, String newPassword);
/**
* 获取用户统计信息 - 管理接口
*/
Result<Object> getUserStats();
}
@@ -9,6 +9,7 @@ import com.nuaa.video.common.utils.MinioUtils;
import com.nuaa.video.user.dto.UserProfileDTO;
import com.nuaa.video.user.entity.UserInfo;
import com.nuaa.video.user.entity.UserStatDaily;
import com.nuaa.video.user.feign.AuthFeignClient;
import com.nuaa.video.user.mapper.UserFollowMapper;
import com.nuaa.video.user.mapper.UserInfoMapper;
import com.nuaa.video.user.mapper.UserStatDailyMapper;
@@ -20,10 +21,15 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* 用户信息服务实现类
@@ -38,6 +44,7 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
private final UserSettingService userSettingService;
private final MinioUtils minioUtils;
private final UserStatDailyMapper userStatDailyMapper;
private final AuthFeignClient authFeignClient;
@Override
public Result<UserProfileVO> getUserProfile(Long userId, Long currentUserId) {
@@ -245,6 +252,133 @@ public class UserInfoServiceImpl extends ServiceImpl<UserInfoMapper, UserInfo> i
return Result.success();
}
@Override
public Result<Page<UserProfileVO>> getAdminUserList(Page<UserInfo> page, String keyword) {
LambdaQueryWrapper<UserInfo> queryWrapper = new LambdaQueryWrapper<>();
// 添加关键词搜索条件
if (StringUtils.hasText(keyword)) {
queryWrapper.like(UserInfo::getNickname, keyword)
.or()
.like(UserInfo::getSignature, keyword);
}
// 按创建时间降序排序
queryWrapper.orderByDesc(UserInfo::getCreateTime);
// 分页查询
Page<UserInfo> userPage = page(page, queryWrapper);
// 转换为VO
Page<UserProfileVO> voPage = new Page<>();
BeanUtils.copyProperties(userPage, voPage, "records");
List<UserProfileVO> voList = userPage.getRecords().stream()
.map(user -> {
UserProfileVO vo = new UserProfileVO();
BeanUtils.copyProperties(user, vo);
return vo;
})
.collect(Collectors.toList());
voPage.setRecords(voList);
return Result.success(voPage);
}
@Override
public Result<UserProfileVO> getAdminUserDetail(Long userId) {
// 查询用户信息
UserInfo userInfo = getById(userId);
if (userInfo == null) {
return Result.error("用户不存在");
}
// 转换为VO
UserProfileVO vo = new UserProfileVO();
BeanUtils.copyProperties(userInfo, vo);
// 查询用户统计信息
// 例如:历史发布视频、历史评论等
return Result.success(vo);
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> updateUserStatus(Long userId, Integer status) {
// 查询用户是否存在
UserInfo userInfo = getById(userId);
if (userInfo == null) {
return Result.error("用户不存在");
}
// 更新用户状态
UserInfo updateUser = new UserInfo();
updateUser.setId(userId);
updateUser.setUpdateTime(LocalDateTime.now());
updateUser.setUpdatedBy("admin");
updateById(updateUser);
// 当禁用用户,需要清除用户相关的登录态等
if (status == 0) {
// 发送事件通知auth服务禁用该用户账号
//TODO 可以优化为消息队列
authFeignClient.lockUser(userId, true);
}
return Result.success();
}
@Override
@Transactional(rollbackFor = Exception.class)
public Result<Void> resetUserPassword(Long userId, String newPassword) {
// 查询用户是否存在
UserInfo userInfo = getById(userId);
if (userInfo == null) {
return Result.error("用户不存在");
}
// 调用auth服务重置密码(实际项目中通过Feign或消息队列)
authFeignClient.resetPassword(userId, newPassword);
log.info("重置用户密码: userId={}, newPassword=***", userId);
return Result.success();
}
@Override
public Result<Object> getUserStats() {
// 收集用户统计数据
Map<String, Object> stats = new HashMap<>();
// 总用户数
long totalUsers = count();
stats.put("totalUsers", totalUsers);
// 今日新增用户
LocalDate today = LocalDate.now();
long todayNewUsers = count(new LambdaQueryWrapper<UserInfo>()
.ge(UserInfo::getCreateTime, today.atStartOfDay()));
stats.put("todayNewUsers", todayNewUsers);
// 活跃用户数(最近7天有登录记录的用户)
// 此处需要结合用户登录记录表查询,简化处理
stats.put("activeUsers", totalUsers * 0.3); // 示例数据
// 用户等级分布
// 假设level字段代表用户等级
Map<String, Object> levelDistribution = new HashMap<>();
for (int i = 1; i <= 6; i++) {
int level = i;
long count = count(new LambdaQueryWrapper<UserInfo>().eq(UserInfo::getLevel, level));
levelDistribution.put("level" + i, count);
}
stats.put("levelDistribution", levelDistribution);
return Result.success(stats);
}
/**
* 更新用户每日统计数据
*/