From 36380d50a7719430b0dd70dd74993019cf7d2cb4 Mon Sep 17 00:00:00 2001 From: Toyas Date: Fri, 11 Apr 2025 00:26:30 +0800 Subject: [PATCH] feat(admin): Implement admin module foundation --- video-admin/pom.xml | 131 ++++++++ .../nuaa/video/admin/AdminApplication.java | 10 + .../video/admin/annotation/OperationLog.java | 16 + .../admin/aspect/OperationLogAspect.java | 220 +++++++++++++ .../admin/controller/AdminUserController.java | 111 +++++++ .../admin/controller/AuthController.java | 65 ++++ .../controller/ContentManageController.java | 119 +++++++ .../controller/UserManageController.java | 72 +++++ .../video/admin/dto/AdminPermissionDTO.java | 36 +++ .../nuaa/video/admin/dto/AdminRoleDTO.java | 44 +++ .../nuaa/video/admin/dto/AdminUserDTO.java | 69 ++++ .../com/nuaa/video/admin/dto/LoginDTO.java | 36 +++ .../nuaa/video/admin/dto/SystemConfigDTO.java | 36 +++ .../video/admin/entity/AdminOperationLog.java | 69 ++++ .../video/admin/entity/AdminPermission.java | 59 ++++ .../nuaa/video/admin/entity/AdminRole.java | 73 +++++ .../admin/entity/AdminRolePermission.java | 44 +++ .../video/admin/entity/AdminSystemConfig.java | 59 ++++ .../nuaa/video/admin/entity/AdminUser.java | 108 +++++++ .../video/admin/entity/AdminUserRole.java | 44 +++ .../video/admin/feign/CommentFeignClient.java | 45 +++ .../CommentFeignClientFallbackFactory.java | 42 +++ .../video/admin/feign/ContentFeignClient.java | 73 +++++ .../ContentFeignClientFallbackFactory.java | 62 ++++ .../video/admin/feign/UserFeignClient.java | 46 +++ .../feign/UserFeignClientFallbackFactory.java | 45 +++ .../admin/mapper/AdminOperationLogMapper.java | 33 ++ .../admin/mapper/AdminPermissionMapper.java | 31 ++ .../video/admin/mapper/AdminRoleMapper.java | 41 +++ .../mapper/AdminRolePermissionMapper.java | 25 ++ .../admin/mapper/AdminSystemConfigMapper.java | 21 ++ .../video/admin/mapper/AdminUserMapper.java | 39 +++ .../admin/mapper/AdminUserRoleMapper.java | 25 ++ .../admin/security/AdminUserDetails.java | 74 +++++ .../security/JwtAuthenticationFilter.java | 85 +++++ .../admin/security/JwtTokenProvider.java | 110 +++++++ .../video/admin/security/SecurityConfig.java | 54 ++++ .../video/admin/service/AdminUserService.java | 85 +++++ .../nuaa/video/admin/service/AuthService.java | 46 +++ .../service/impl/AdminUserServiceImpl.java | 302 ++++++++++++++++++ .../admin/service/impl/AuthServiceImpl.java | 278 ++++++++++++++++ .../video/admin/vo/AdminPermissionVO.java | 39 +++ .../com/nuaa/video/admin/vo/AdminRoleVO.java | 50 +++ .../com/nuaa/video/admin/vo/AdminUserVO.java | 75 +++++ .../com/nuaa/video/admin/vo/CaptchaVO.java | 27 ++ .../java/com/nuaa/video/admin/vo/LoginVO.java | 59 ++++ .../nuaa/video/admin/vo/SystemConfigVO.java | 44 +++ .../src/main/resources/application-dev.yml | 29 ++ .../src/main/resources/application.yml | 103 +++++- .../mapper/AdminOperationLogMapper.xml | 30 ++ .../mapper/AdminPermissionMapper.xml | 32 ++ .../main/resources/mapper/AdminRoleMapper.xml | 78 +++++ .../mapper/AdminRolePermissionMapper.xml | 13 + .../mapper/AdminSystemConfigMapper.xml | 15 + .../main/resources/mapper/AdminUserMapper.xml | 116 +++++++ .../resources/mapper/AdminUserRoleMapper.xml | 13 + .../auth/controller/AdminAuthController.java | 49 +++ .../video/auth/service/AuthUserService.java | 15 + .../service/impl/AuthUserServiceImpl.java | 108 +++++++ .../controller/AdminCommentController.java | 61 ++++ .../video/comment/service/CommentService.java | 20 ++ .../service/impl/CommentServiceImpl.java | 121 +++++++ .../controller/AdminCategoryController.java | 55 ++++ .../controller/AdminVideoController.java | 80 +++++ .../video/content/service/VideoService.java | 20 ++ .../service/impl/VideoServiceImpl.java | 193 +++++++++++ .../nuaa/video/content/vo/VideoInfoVO.java | 20 ++ .../user/controller/AdminUserController.java | 70 ++++ .../video/user/feign/AuthFeignClient.java | 20 +- .../video/user/service/UserInfoService.java | 25 ++ .../service/impl/UserInfoServiceImpl.java | 134 ++++++++ 71 files changed, 4682 insertions(+), 15 deletions(-) create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/annotation/OperationLog.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/aspect/OperationLogAspect.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/controller/AuthController.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/dto/AdminPermissionDTO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/dto/AdminUserDTO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/dto/LoginDTO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/dto/SystemConfigDTO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/entity/AdminOperationLog.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/entity/AdminPermission.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRole.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRolePermission.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/entity/AdminSystemConfig.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUser.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUserRole.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminOperationLogMapper.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminPermissionMapper.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRoleMapper.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRolePermissionMapper.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminSystemConfigMapper.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserMapper.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserRoleMapper.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/security/AdminUserDetails.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/security/JwtTokenProvider.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/security/SecurityConfig.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/service/AdminUserService.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/service/AuthService.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/vo/AdminPermissionVO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/vo/AdminRoleVO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/vo/AdminUserVO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/vo/CaptchaVO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/vo/LoginVO.java create mode 100644 video-admin/src/main/java/com/nuaa/video/admin/vo/SystemConfigVO.java create mode 100644 video-admin/src/main/resources/application-dev.yml create mode 100644 video-admin/src/main/resources/mapper/AdminOperationLogMapper.xml create mode 100644 video-admin/src/main/resources/mapper/AdminPermissionMapper.xml create mode 100644 video-admin/src/main/resources/mapper/AdminRoleMapper.xml create mode 100644 video-admin/src/main/resources/mapper/AdminRolePermissionMapper.xml create mode 100644 video-admin/src/main/resources/mapper/AdminSystemConfigMapper.xml create mode 100644 video-admin/src/main/resources/mapper/AdminUserMapper.xml create mode 100644 video-admin/src/main/resources/mapper/AdminUserRoleMapper.xml create mode 100644 video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java create mode 100644 video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java create mode 100644 video-content/src/main/java/com/nuaa/video/content/controller/AdminCategoryController.java create mode 100644 video-content/src/main/java/com/nuaa/video/content/controller/AdminVideoController.java create mode 100644 video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java diff --git a/video-admin/pom.xml b/video-admin/pom.xml index 09aa006..17d9a11 100644 --- a/video-admin/pom.xml +++ b/video-admin/pom.xml @@ -12,10 +12,141 @@ video-admin + com.nuaa.video video-common ${project.version} + + + + org.springframework.boot + spring-boot-starter-web + + + + + org.springframework.boot + spring-boot-starter-security + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-discovery + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-nacos-config + + + + + org.springframework.boot + spring-boot-starter-actuator + + + + + org.springframework.boot + spring-boot-starter-validation + + + + + org.springframework.cloud + spring-cloud-starter-openfeign + + + + + org.springframework.cloud + spring-cloud-starter-loadbalancer + + + + + com.alibaba.cloud + spring-cloud-starter-alibaba-sentinel + + + + + com.mysql + mysql-connector-j + runtime + + + + + com.baomidou + mybatis-plus-spring-boot3-starter + 3.5.11 + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + io.jsonwebtoken + jjwt-api + 0.12.3 + + + io.jsonwebtoken + jjwt-impl + 0.12.3 + runtime + + + io.jsonwebtoken + jjwt-jackson + 0.12.3 + runtime + + + + + org.springframework.boot + spring-boot-starter-aop + + + + + org.projectlombok + lombok + true + + + + + cn.hutool + hutool-all + 5.8.26 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/AdminApplication.java b/video-admin/src/main/java/com/nuaa/video/admin/AdminApplication.java index f8329fd..5b84a5e 100644 --- a/video-admin/src/main/java/com/nuaa/video/admin/AdminApplication.java +++ b/video-admin/src/main/java/com/nuaa/video/admin/AdminApplication.java @@ -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); diff --git a/video-admin/src/main/java/com/nuaa/video/admin/annotation/OperationLog.java b/video-admin/src/main/java/com/nuaa/video/admin/annotation/OperationLog.java new file mode 100644 index 0000000..f0fb8d6 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/annotation/OperationLog.java @@ -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 ""; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/aspect/OperationLogAspect.java b/video-admin/src/main/java/com/nuaa/video/admin/aspect/OperationLogAspect.java new file mode 100644 index 0000000..b880b04 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/aspect/OperationLogAspect.java @@ -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 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; + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java b/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java new file mode 100644 index 0000000..7b35fe3 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/controller/AdminUserController.java @@ -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> listUsers( + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "size", defaultValue = "10") Integer size, + @RequestParam(value = "keyword", required = false) String keyword) { + Page pageParam = new Page<>(page, size); + return adminUserService.listUsers(pageParam, keyword); + } + + /** + * 获取管理员详情 + */ + @PreAuthorize("hasAuthority('user:manage')") + @GetMapping("/{id}") + public Result getUser(@PathVariable Long id) { + return adminUserService.getUserDetail(id); + } + + /** + * 添加管理员 + */ + @OperationLog("添加管理员") + @PreAuthorize("hasAuthority('user:manage')") + @PostMapping + public Result addUser(@RequestBody @Valid AdminUserDTO adminUserDTO) { + return adminUserService.addUser(adminUserDTO, getCurrentUsername()); + } + + /** + * 更新管理员 + */ + @OperationLog("更新管理员") + @PreAuthorize("hasAuthority('user:manage')") + @PutMapping + public Result updateUser(@RequestBody @Valid AdminUserDTO adminUserDTO) { + return adminUserService.updateUser(adminUserDTO, getCurrentUsername()); + } + + /** + * 删除管理员 + */ + @OperationLog("删除管理员") + @PreAuthorize("hasAuthority('user:manage')") + @DeleteMapping("/{id}") + public Result deleteUser(@PathVariable Long id) { + return adminUserService.deleteUser(id); + } + + /** + * 启用/禁用管理员 + */ + @OperationLog("修改管理员状态") + @PreAuthorize("hasAuthority('user:manage')") + @PutMapping("/{id}/status/{status}") + public Result updateStatus(@PathVariable Long id, @PathVariable Integer status) { + return adminUserService.updateStatus(id, status, getCurrentUsername()); + } + + /** + * 重置密码 + */ + @OperationLog("重置管理员密码") + @PreAuthorize("hasAuthority('user:manage')") + @PutMapping("/{id}/reset-password") + public Result 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"; + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/controller/AuthController.java b/video-admin/src/main/java/com/nuaa/video/admin/controller/AuthController.java new file mode 100644 index 0000000..90a0de6 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/controller/AuthController.java @@ -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 getCaptcha() { + return authService.generateCaptcha(); + } + + /** + * 登录 + */ + @PostMapping("/login") + public Result login(@RequestBody @Valid LoginDTO loginDTO, HttpServletRequest request) { + return authService.login(loginDTO, request); + } + + /** + * 登出 + */ + @OperationLog("退出登录") + @PostMapping("/logout") + public Result 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 getCurrentUser(@AuthenticationPrincipal AdminUserDetails userDetails) { + return authService.getCurrentUser(userDetails); + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java b/video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java new file mode 100644 index 0000000..37113a5 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/controller/ContentManageController.java @@ -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> 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 getVideoDetail( + @PathVariable Long videoId, + @RequestHeader("Authorization") String token) { + return contentFeignClient.getVideoDetail(videoId, token); + } + + /** + * 审核视频 + */ + @OperationLog("审核视频") + @PreAuthorize("hasAuthority('video:manage')") + @PostMapping("/video/review/{videoId}") + public Result 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 disableVideo( + @PathVariable Long videoId, + @RequestHeader("Authorization") String token) { + return contentFeignClient.disableVideo(videoId, token); + } + + /** + * 获取分类列表 + */ + @PreAuthorize("hasAuthority('category:manage')") + @GetMapping("/category/list") + public Result getCategoryList(@RequestHeader("Authorization") String token) { + return contentFeignClient.getCategoryList(token); + } + + /** + * 添加分类 + */ + @OperationLog("添加分类") + @PreAuthorize("hasAuthority('category:manage')") + @PostMapping("/category") + public Result addCategory( + @RequestBody Object category, + @RequestHeader("Authorization") String token) { + return contentFeignClient.addCategory(category, token); + } + + /** + * 更新分类 + */ + @OperationLog("更新分类") + @PreAuthorize("hasAuthority('category:manage')") + @PutMapping("/category") + public Result updateCategory( + @RequestBody Object category, + @RequestHeader("Authorization") String token) { + return contentFeignClient.updateCategory(category, token); + } + + /** + * 删除分类 + */ + @OperationLog("删除分类") + @PreAuthorize("hasAuthority('category:manage')") + @DeleteMapping("/category/{categoryId}") + public Result deleteCategory( + @PathVariable Long categoryId, + @RequestHeader("Authorization") String token) { + return contentFeignClient.deleteCategory(categoryId, token); + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java b/video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java new file mode 100644 index 0000000..43f87bb --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/controller/UserManageController.java @@ -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> 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 getUserDetail( + @PathVariable Long userId, + @RequestHeader("Authorization") String token) { + return userFeignClient.getUserDetail(userId, token); + } + + /** + * 禁用/启用用户 + */ + @OperationLog("修改用户状态") + @PreAuthorize("hasAuthority('user:manage')") + @PutMapping("/{userId}/status/{status}") + public Result 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 resetUserPassword( + @PathVariable Long userId, + @RequestParam String newPassword, + @RequestHeader("Authorization") String token) { + return userFeignClient.resetUserPassword(userId, newPassword, token); + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminPermissionDTO.java b/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminPermissionDTO.java new file mode 100644 index 0000000..0a67296 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminPermissionDTO.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java b/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java new file mode 100644 index 0000000..4787379 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminRoleDTO.java @@ -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 permissionIds; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminUserDTO.java b/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminUserDTO.java new file mode 100644 index 0000000..843434e --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/dto/AdminUserDTO.java @@ -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 roleIds; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/dto/LoginDTO.java b/video-admin/src/main/java/com/nuaa/video/admin/dto/LoginDTO.java new file mode 100644 index 0000000..33c75b6 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/dto/LoginDTO.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/dto/SystemConfigDTO.java b/video-admin/src/main/java/com/nuaa/video/admin/dto/SystemConfigDTO.java new file mode 100644 index 0000000..4599660 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/dto/SystemConfigDTO.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminOperationLog.java b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminOperationLog.java new file mode 100644 index 0000000..ff04318 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminOperationLog.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminPermission.java b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminPermission.java new file mode 100644 index 0000000..efa2845 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminPermission.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRole.java b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRole.java new file mode 100644 index 0000000..1b72dbb --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRole.java @@ -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 permissionIds; + + /** + * 权限集合(非数据库字段) + */ + @TableField(exist = false) + private List permissions; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRolePermission.java b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRolePermission.java new file mode 100644 index 0000000..92010cb --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminRolePermission.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminSystemConfig.java b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminSystemConfig.java new file mode 100644 index 0000000..1ba1de4 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminSystemConfig.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUser.java b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUser.java new file mode 100644 index 0000000..c1ef6f0 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUser.java @@ -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 roleIds; + + /** + * 角色集合(非数据库字段) + */ + @TableField(exist = false) + private List roles; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUserRole.java b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUserRole.java new file mode 100644 index 0000000..550a014 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/entity/AdminUserRole.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java b/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java new file mode 100644 index 0000000..9ac5ed1 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClient.java @@ -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> 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 getCommentDetail(@PathVariable("commentId") Long commentId, + @RequestHeader("Authorization") String token); + + /** + * 删除评论 + */ + @DeleteMapping("/comment/admin/{commentId}") + Result deleteComment(@PathVariable("commentId") Long commentId, + @RequestHeader("Authorization") String token); + + /** + * 获取评论统计信息 + */ + @GetMapping("/comment/admin/stats") + Result getCommentStats(@RequestHeader("Authorization") String token); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java b/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java new file mode 100644 index 0000000..2e36d4c --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/feign/CommentFeignClientFallbackFactory.java @@ -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 { + + @Override + public CommentFeignClient create(Throwable throwable) { + log.error("CommentFeignClient调用失败,原因:", throwable); + + return new CommentFeignClient() { + @Override + public Result> getCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword, String token) { + return Result.error("获取评论列表失败:服务不可用"); + } + + @Override + public Result getCommentDetail(Long commentId, String token) { + return Result.error("获取评论详情失败:服务不可用"); + } + + @Override + public Result deleteComment(Long commentId, String token) { + return Result.error("删除评论失败:服务不可用"); + } + + @Override + public Result getCommentStats(String token) { + return Result.error("获取评论统计信息失败:服务不可用"); + } + }; + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java b/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java new file mode 100644 index 0000000..a1406b1 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClient.java @@ -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> 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 getVideoDetail(@PathVariable("videoId") Long videoId, + @RequestHeader("Authorization") String token); + + /** + * 审核视频 + */ + @PostMapping("/content/video/admin/review/{videoId}") + Result 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 disableVideo(@PathVariable("videoId") Long videoId, + @RequestHeader("Authorization") String token); + + /** + * 分类列表 + */ + @GetMapping("/content/category/admin/list") + Result getCategoryList(@RequestHeader("Authorization") String token); + + /** + * 添加分类 + */ + @PostMapping("/content/category/admin") + Result addCategory(@RequestBody Object category, @RequestHeader("Authorization") String token); + + /** + * 更新分类 + */ + @PutMapping("/content/category/admin") + Result updateCategory(@RequestBody Object category, @RequestHeader("Authorization") String token); + + /** + * 删除分类 + */ + @DeleteMapping("/content/category/admin/{categoryId}") + Result deleteCategory(@PathVariable("categoryId") Long categoryId, + @RequestHeader("Authorization") String token); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java b/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java new file mode 100644 index 0000000..6396586 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/feign/ContentFeignClientFallbackFactory.java @@ -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 { + + @Override + public ContentFeignClient create(Throwable throwable) { + log.error("ContentFeignClient调用失败,原因:", throwable); + + return new ContentFeignClient() { + @Override + public Result> getVideoList(Integer page, Integer size, String keyword, Long categoryId, Integer status, String token) { + return Result.error("获取视频列表失败:服务不可用"); + } + + @Override + public Result getVideoDetail(Long videoId, String token) { + return Result.error("获取视频详情失败:服务不可用"); + } + + @Override + public Result reviewVideo(Long videoId, Integer status, String reason, String token) { + return Result.error("审核视频失败:服务不可用"); + } + + @Override + public Result disableVideo(Long videoId, String token) { + return Result.error("下架视频失败:服务不可用"); + } + + @Override + public Result getCategoryList(String token) { + return Result.error("获取分类列表失败:服务不可用"); + } + + @Override + public Result addCategory(Object category, String token) { + return Result.error("添加分类失败:服务不可用"); + } + + @Override + public Result updateCategory(Object category, String token) { + return Result.error("更新分类失败:服务不可用"); + } + + @Override + public Result deleteCategory(Long categoryId, String token) { + return Result.error("删除分类失败:服务不可用"); + } + }; + } +} diff --git a/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java b/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java new file mode 100644 index 0000000..cfa358e --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClient.java @@ -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> 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 getUserDetail(@PathVariable("userId") Long userId, + @RequestHeader("Authorization") String token); + + /** + * 禁用/启用用户 + */ + @PutMapping("/user/profile/admin/status/{userId}/{status}") + Result updateUserStatus(@PathVariable("userId") Long userId, + @PathVariable("status") Integer status, + @RequestHeader("Authorization") String token); + + /** + * 重置用户密码 + */ + @PutMapping("/user/profile/admin/reset-password/{userId}") + Result resetUserPassword(@PathVariable("userId") Long userId, + @RequestParam String newPassword, + @RequestHeader("Authorization") String token); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java b/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java new file mode 100644 index 0000000..b8795c7 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/feign/UserFeignClientFallbackFactory.java @@ -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 { + + @Override + public UserFeignClient create(Throwable cause) { + log.error("用户服务调用失败:", cause); + return new UserFeignClient() { + @Override + public Result> getUserList(Integer page, Integer size, String keyword, String token) { + log.error("获取用户列表失败"); + return Result.error("获取用户列表失败,用户服务不可用"); + } + + @Override + public Result getUserDetail(Long userId, String token) { + log.error("获取用户详情失败,用户ID: {}", userId); + return Result.error("获取用户详情失败,用户服务不可用"); + } + + @Override + public Result updateUserStatus(Long userId, Integer status, String token) { + log.error("更新用户状态失败,用户ID: {}, 状态: {}", userId, status); + return Result.error("更新用户状态失败,用户服务不可用"); + } + + @Override + public Result resetUserPassword(Long userId, String newPassword, String token) { + log.error("重置用户密码失败,用户ID: {}", userId); + return Result.error("重置用户密码失败,用户服务不可用"); + } + }; + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminOperationLogMapper.java b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminOperationLogMapper.java new file mode 100644 index 0000000..c591598 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminOperationLogMapper.java @@ -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 { + + /** + * 分页查询操作日志 + * + * @param page 分页参数 + * @param username 用户名 + * @param operation 操作 + * @param status 状态 + * @param startTime 开始时间 + * @param endTime 结束时间 + * @return 操作日志列表 + */ + Page selectLogList( + Page page, + @Param("username") String username, + @Param("operation") String operation, + @Param("status") Integer status, + @Param("startTime") String startTime, + @Param("endTime") String endTime); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminPermissionMapper.java b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminPermissionMapper.java new file mode 100644 index 0000000..86f0717 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminPermissionMapper.java @@ -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 { + + /** + * 获取角色的权限列表 + * + * @param roleId 角色ID + * @return 权限列表 + */ + List selectPermissionsByRoleId(@Param("roleId") Long roleId); + + /** + * 获取管理员的权限列表 + * + * @param adminId 管理员ID + * @return 权限列表 + */ + List selectPermissionsByAdminId(@Param("adminId") Long adminId); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRoleMapper.java b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRoleMapper.java new file mode 100644 index 0000000..e1b3911 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRoleMapper.java @@ -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 { + + /** + * 查询角色列表(包括权限) + * + * @param page 分页参数 + * @param keyword 关键字 + * @return 角色列表 + */ + Page selectRoleList(Page page, @Param("keyword") String keyword); + + /** + * 获取角色详情(包括权限) + * + * @param id 角色ID + * @return 角色详情 + */ + AdminRole selectRoleDetail(@Param("id") Long id); + + /** + * 获取管理员的角色列表 + * + * @param adminId 管理员ID + * @return 角色列表 + */ + List selectRolesByAdminId(@Param("adminId") Long adminId); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRolePermissionMapper.java b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRolePermissionMapper.java new file mode 100644 index 0000000..9f27a14 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminRolePermissionMapper.java @@ -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 { + + /** + * 批量插入角色权限关联 + * + * @param roleId 角色ID + * @param permissionIds 权限ID列表 + * @param createdBy 创建人 + * @return 影响行数 + */ + int batchInsert(@Param("roleId") Long roleId, @Param("permissionIds") List permissionIds, @Param("createdBy") String createdBy); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminSystemConfigMapper.java b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminSystemConfigMapper.java new file mode 100644 index 0000000..b2525d1 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminSystemConfigMapper.java @@ -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 { + + /** + * 根据配置键获取配置值 + * + * @param configKey 配置键 + * @return 配置值 + */ + String getConfigValue(@Param("configKey") String configKey); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserMapper.java b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserMapper.java new file mode 100644 index 0000000..654957d --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserMapper.java @@ -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 { + + /** + * 根据用户名查询管理员信息(包括角色和权限) + * + * @param username 用户名 + * @return 管理员信息 + */ + AdminUser selectByUsername(@Param("username") String username); + + /** + * 查询管理员列表(包括角色) + * + * @param page 分页参数 + * @param keyword 关键字 + * @return 管理员列表 + */ + Page selectUserList(Page page, @Param("keyword") String keyword); + + /** + * 获取管理员详情(包括角色和权限) + * + * @param id 管理员ID + * @return 管理员详情 + */ + AdminUser selectUserDetail(@Param("id") Long id); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserRoleMapper.java b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserRoleMapper.java new file mode 100644 index 0000000..3c2be51 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/mapper/AdminUserRoleMapper.java @@ -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 { + + /** + * 批量插入用户角色关联 + * + * @param adminId 管理员ID + * @param roleIds 角色ID列表 + * @param createdBy 创建人 + * @return 影响行数 + */ + int batchInsert(@Param("adminId") Long adminId, @Param("roleIds") List roleIds, @Param("createdBy") String createdBy); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/security/AdminUserDetails.java b/video-admin/src/main/java/com/nuaa/video/admin/security/AdminUserDetails.java new file mode 100644 index 0000000..5480113 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/security/AdminUserDetails.java @@ -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 authorities; + + @Override + public Collection 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; + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java b/video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java new file mode 100644 index 0000000..2cae9f5 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/security/JwtAuthenticationFilter.java @@ -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 authorities = jwtTokenProvider.getAuthoritiesFromToken(token); + + // 转换为Spring Security的授权对象 + List 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; + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/security/JwtTokenProvider.java b/video-admin/src/main/java/com/nuaa/video/admin/security/JwtTokenProvider.java new file mode 100644 index 0000000..48a5ca8 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/security/JwtTokenProvider.java @@ -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 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 getClaimFromToken(String token, Function claimsResolver) { + Claims claims = Jwts.parser() + .verifyWith(getSignKey()) + .build() + .parseSignedClaims(token) + .getPayload(); + return claimsResolver.apply(claims); + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/security/SecurityConfig.java b/video-admin/src/main/java/com/nuaa/video/admin/security/SecurityConfig.java new file mode 100644 index 0000000..71de2a2 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/security/SecurityConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/service/AdminUserService.java b/video-admin/src/main/java/com/nuaa/video/admin/service/AdminUserService.java new file mode 100644 index 0000000..39d53b1 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/service/AdminUserService.java @@ -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 { + + /** + * 根据用户名查询管理员信息 + * + * @param username 用户名 + * @return 管理员信息 + */ + AdminUser getByUsername(String username); + + /** + * 分页查询管理员列表 + * + * @param page 分页参数 + * @param keyword 关键字 + * @return 管理员列表 + */ + Result> listUsers(Page page, String keyword); + + /** + * 获取管理员详情 + * + * @param id 管理员ID + * @return 管理员详情 + */ + Result getUserDetail(Long id); + + /** + * 添加管理员 + * + * @param adminUserDTO 管理员信息 + * @param operator 操作人 + * @return 结果 + */ + Result addUser(AdminUserDTO adminUserDTO, String operator); + + /** + * 更新管理员 + * + * @param adminUserDTO 管理员信息 + * @param operator 操作人 + * @return 结果 + */ + Result updateUser(AdminUserDTO adminUserDTO, String operator); + + /** + * 删除管理员 + * + * @param id 管理员ID + * @return 结果 + */ + Result deleteUser(Long id); + + /** + * 更新管理员状态 + * + * @param id 管理员ID + * @param status 状态 + * @param operator 操作人 + * @return 结果 + */ + Result updateStatus(Long id, Integer status, String operator); + + /** + * 重置密码 + * + * @param id 管理员ID + * @param newPassword 新密码 + * @param operator 操作人 + * @return 结果 + */ + Result resetPassword(Long id, String newPassword, String operator); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/service/AuthService.java b/video-admin/src/main/java/com/nuaa/video/admin/service/AuthService.java new file mode 100644 index 0000000..78eb063 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/service/AuthService.java @@ -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 generateCaptcha(); + + /** + * 登录 + * + * @param loginDTO 登录参数 + * @param request HTTP请求 + * @return 登录结果 + */ + Result login(LoginDTO loginDTO, HttpServletRequest request); + + /** + * 登出 + * + * @param token JWT令牌 + * @return 结果 + */ + Result logout(String token); + + /** + * 获取当前登录用户信息 + * + * @param adminUserDetails 用户详情 + * @return 用户信息 + */ + Result getCurrentUser(AdminUserDetails adminUserDetails); +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java b/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java new file mode 100644 index 0000000..2e4e98d --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AdminUserServiceImpl.java @@ -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 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> listUsers(Page page, String keyword) { + Page userPage = adminUserMapper.selectUserList(page, keyword); + + // 转换为VO + Page voPage = new Page<>(); + BeanUtils.copyProperties(userPage, voPage, "records"); + + List 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 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 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 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 addUser(AdminUserDTO adminUserDTO, String operator) { + // 检查用户名是否存在 + AdminUser existUser = getOne(new LambdaQueryWrapper() + .eq(AdminUser::getUsername, adminUserDTO.getUsername())); + + if (existUser != null) { + return Result.error("用户名已存在"); + } + + // 检查邮箱是否存在 + if (StringUtils.hasText(adminUserDTO.getEmail())) { + long count = count(new LambdaQueryWrapper() + .eq(AdminUser::getEmail, adminUserDTO.getEmail())); + if (count > 0) { + return Result.error("邮箱已存在"); + } + } + + // 检查手机号是否存在 + if (StringUtils.hasText(adminUserDTO.getPhone())) { + long count = count(new LambdaQueryWrapper() + .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 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() + .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() + .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() + .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() + .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 deleteUser(Long id) { + // 检查管理员是否存在 + AdminUser existUser = getById(id); + if (existUser == null) { + return Result.error("管理员不存在"); + } + + // 删除管理员 + removeById(id); + + // 删除角色关联 + adminUserRoleMapper.delete(new LambdaQueryWrapper() + .eq(AdminUserRole::getAdminId, id)); + + return Result.success(); + } + + @Override + public Result 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 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(); + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java b/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java new file mode 100644 index 0000000..b0c6ac8 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/service/impl/AuthServiceImpl.java @@ -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 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 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 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 logout(String token) { + if (StringUtils.hasText(token)) { + // 从Redis中删除token + redisTemplate.delete(TOKEN_KEY_PREFIX + token); + } + return Result.success(); + } + + @Override + public Result 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 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 roles = new ArrayList<>(); + List 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 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; + } +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminPermissionVO.java b/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminPermissionVO.java new file mode 100644 index 0000000..3387cba --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminPermissionVO.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminRoleVO.java b/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminRoleVO.java new file mode 100644 index 0000000..d784d45 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminRoleVO.java @@ -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 permissionIds; + + /** + * 权限列表 + */ + private List permissions; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminUserVO.java b/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminUserVO.java new file mode 100644 index 0000000..15bce79 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/vo/AdminUserVO.java @@ -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 roleIds; + + /** + * 角色列表 + */ + private List roles; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/vo/CaptchaVO.java b/video-admin/src/main/java/com/nuaa/video/admin/vo/CaptchaVO.java new file mode 100644 index 0000000..57c7016 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/vo/CaptchaVO.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/vo/LoginVO.java b/video-admin/src/main/java/com/nuaa/video/admin/vo/LoginVO.java new file mode 100644 index 0000000..4483267 --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/vo/LoginVO.java @@ -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 roles; + + /** + * 权限编码列表 + */ + private List permissions; +} \ No newline at end of file diff --git a/video-admin/src/main/java/com/nuaa/video/admin/vo/SystemConfigVO.java b/video-admin/src/main/java/com/nuaa/video/admin/vo/SystemConfigVO.java new file mode 100644 index 0000000..00a9d4a --- /dev/null +++ b/video-admin/src/main/java/com/nuaa/video/admin/vo/SystemConfigVO.java @@ -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; +} \ No newline at end of file diff --git a/video-admin/src/main/resources/application-dev.yml b/video-admin/src/main/resources/application-dev.yml new file mode 100644 index 0000000..7f1f180 --- /dev/null +++ b/video-admin/src/main/resources/application-dev.yml @@ -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 \ No newline at end of file diff --git a/video-admin/src/main/resources/application.yml b/video-admin/src/main/resources/application.yml index c293416..281305d 100644 --- a/video-admin/src/main/resources/application.yml +++ b/video-admin/src/main/resources/application.yml @@ -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: - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl \ No newline at end of file + 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 \ No newline at end of file diff --git a/video-admin/src/main/resources/mapper/AdminOperationLogMapper.xml b/video-admin/src/main/resources/mapper/AdminOperationLogMapper.xml new file mode 100644 index 0000000..ff1b0f0 --- /dev/null +++ b/video-admin/src/main/resources/mapper/AdminOperationLogMapper.xml @@ -0,0 +1,30 @@ + + + + + + + \ No newline at end of file diff --git a/video-admin/src/main/resources/mapper/AdminPermissionMapper.xml b/video-admin/src/main/resources/mapper/AdminPermissionMapper.xml new file mode 100644 index 0000000..e702ddc --- /dev/null +++ b/video-admin/src/main/resources/mapper/AdminPermissionMapper.xml @@ -0,0 +1,32 @@ + + + + + + + + + + \ No newline at end of file diff --git a/video-admin/src/main/resources/mapper/AdminRoleMapper.xml b/video-admin/src/main/resources/mapper/AdminRoleMapper.xml new file mode 100644 index 0000000..541c727 --- /dev/null +++ b/video-admin/src/main/resources/mapper/AdminRoleMapper.xml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/video-admin/src/main/resources/mapper/AdminRolePermissionMapper.xml b/video-admin/src/main/resources/mapper/AdminRolePermissionMapper.xml new file mode 100644 index 0000000..0169497 --- /dev/null +++ b/video-admin/src/main/resources/mapper/AdminRolePermissionMapper.xml @@ -0,0 +1,13 @@ + + + + + + + INSERT INTO admin_role_permission (role_id, permission_id, created_by, create_time) + VALUES + + (#{roleId}, #{permissionId}, #{createdBy}, NOW()) + + + \ No newline at end of file diff --git a/video-admin/src/main/resources/mapper/AdminSystemConfigMapper.xml b/video-admin/src/main/resources/mapper/AdminSystemConfigMapper.xml new file mode 100644 index 0000000..3470ffa --- /dev/null +++ b/video-admin/src/main/resources/mapper/AdminSystemConfigMapper.xml @@ -0,0 +1,15 @@ + + + + + + + \ No newline at end of file diff --git a/video-admin/src/main/resources/mapper/AdminUserMapper.xml b/video-admin/src/main/resources/mapper/AdminUserMapper.xml new file mode 100644 index 0000000..22c53fc --- /dev/null +++ b/video-admin/src/main/resources/mapper/AdminUserMapper.xml @@ -0,0 +1,116 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/video-admin/src/main/resources/mapper/AdminUserRoleMapper.xml b/video-admin/src/main/resources/mapper/AdminUserRoleMapper.xml new file mode 100644 index 0000000..2341956 --- /dev/null +++ b/video-admin/src/main/resources/mapper/AdminUserRoleMapper.xml @@ -0,0 +1,13 @@ + + + + + + + INSERT INTO admin_user_role (admin_id, role_id, created_by, create_time) + VALUES + + (#{adminId}, #{roleId}, #{createdBy}, NOW()) + + + \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java b/video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java new file mode 100644 index 0000000..79f2a79 --- /dev/null +++ b/video-auth/src/main/java/com/nuaa/video/auth/controller/AdminAuthController.java @@ -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 getLoginStats() { + return authUserService.getLoginStats(); + } + + /** + * 管理员重置用户密码 - 管理接口 + */ + @PutMapping("/reset-password/{userId}") + public Result resetPassword( + @PathVariable Long userId, + @RequestParam String newPassword) { + return authUserService.adminResetPassword(userId, newPassword); + } + + /** + * 锁定/解锁用户账号 - 管理接口 + */ + @PutMapping("/lock/{userId}/{locked}") + public Result lockUser( + @PathVariable Long userId, + @PathVariable Boolean locked) { + return authUserService.lockUser(userId, locked); + } +} \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java b/video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java index 84d0f7f..f575030 100644 --- a/video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java +++ b/video-auth/src/main/java/com/nuaa/video/auth/service/AuthUserService.java @@ -59,4 +59,19 @@ public interface AuthUserService extends IService { * @return 用户信息 */ Result getCurrentUser(Long userId); + + /** + * 获取登录统计 - 管理接口 + */ + Result getLoginStats(); + + /** + * 管理员重置用户密码 - 管理接口 + */ + Result adminResetPassword(Long userId, String newPassword); + + /** + * 锁定/解锁用户账号 - 管理接口 + */ + Result lockUser(Long userId, Boolean locked); } \ No newline at end of file diff --git a/video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java b/video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java index 897d3f8..6a582be 100644 --- a/video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java +++ b/video-auth/src/main/java/com/nuaa/video/auth/service/impl/AuthUserServiceImpl.java @@ -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 i return Result.success(user); } + @Override + public Result getLoginStats() { + Map 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 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 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 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()); + } + /** * 获取用户角色 * diff --git a/video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java b/video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java new file mode 100644 index 0000000..0db3037 --- /dev/null +++ b/video-comment/src/main/java/com/nuaa/video/comment/controller/AdminCommentController.java @@ -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> 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 getCommentDetail(@PathVariable Long commentId) { + return commentService.getAdminCommentDetail(commentId); + } + + /** + * 删除评论 - 管理接口 + */ + @DeleteMapping("/{commentId}") + public Result deleteComment(@PathVariable Long commentId) { + return commentService.adminDeleteComment(commentId); + } + + /** + * 获取评论统计信息 - 管理接口 + */ + @GetMapping("/stats") + public Result getCommentStats() { + return commentService.getCommentStats(); + } +} \ No newline at end of file diff --git a/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java b/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java index d40647f..3d6eda4 100644 --- a/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java +++ b/video-comment/src/main/java/com/nuaa/video/comment/service/CommentService.java @@ -95,4 +95,24 @@ public interface CommentService extends IService { * @return 统计信息 */ Result getCommentStat(Long videoId); + + /** + * 获取评论列表 - 管理接口 + */ + Result> getAdminCommentList(Integer page, Integer size, Long videoId, Long userId, String keyword); + + /** + * 获取评论详情 - 管理接口 + */ + Result getAdminCommentDetail(Long commentId); + + /** + * 管理员删除评论 - 管理接口 + */ + Result adminDeleteComment(Long commentId); + + /** + * 获取评论统计信息 - 管理接口 + */ + Result getCommentStats(); } \ No newline at end of file diff --git a/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java b/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java index 5bbdff8..5111855 100644 --- a/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java +++ b/video-comment/src/main/java/com/nuaa/video/comment/service/impl/CommentServiceImpl.java @@ -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 impl return Result.success(statVO); } + @Override + public Result> 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 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 commentPage = new Page<>(page, size); + commentPage = page(commentPage, queryWrapper); + + // 转换为VO + Page voPage = new Page<>(); + BeanUtils.copyProperties(commentPage, voPage, "records"); + + List 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 getAdminCommentDetail(Long commentId) { + // 查询评论信息 + Comment comment = getById(commentId); + if (comment == null) { + return Result.error("评论不存在"); + } + + // 转换为VO + CommentVO vo = convertToCommentVO(comment, null); + + // 获取回复列表 + Result> 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 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 getCommentStats() { + CommentStatVO statVO = new CommentStatVO(); + + // 获取各种统计数据 + // 1. 总评论数 + long totalComments = count(); + statVO.setCommentCount((int)totalComments); + + // 2. 今日新增评论数 + LocalDate today = LocalDate.now(); + long todayComments = count(new LambdaQueryWrapper() + .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 */ diff --git a/video-content/src/main/java/com/nuaa/video/content/controller/AdminCategoryController.java b/video-content/src/main/java/com/nuaa/video/content/controller/AdminCategoryController.java new file mode 100644 index 0000000..7525201 --- /dev/null +++ b/video-content/src/main/java/com/nuaa/video/content/controller/AdminCategoryController.java @@ -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> getCategoryList() { + return categoryService.getCategoryList(); + } + + /** + * 添加分类 - 管理接口 + */ + @PostMapping + public Result addCategory(@RequestBody ContentCategory category) { + return categoryService.addCategory(category); + } + + /** + * 更新分类 - 管理接口 + */ + @PutMapping + public Result updateCategory(@RequestBody ContentCategory category) { + return categoryService.updateCategory(category); + } + + /** + * 删除分类 - 管理接口 + */ + @DeleteMapping("/{categoryId}") + public Result deleteCategory(@PathVariable Long categoryId) { + return categoryService.deleteCategory(categoryId); + } +} \ No newline at end of file diff --git a/video-content/src/main/java/com/nuaa/video/content/controller/AdminVideoController.java b/video-content/src/main/java/com/nuaa/video/content/controller/AdminVideoController.java new file mode 100644 index 0000000..c32a780 --- /dev/null +++ b/video-content/src/main/java/com/nuaa/video/content/controller/AdminVideoController.java @@ -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> 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 pageParam = new Page<>(page, size); + return videoService.getAdminVideoList(pageParam, queryDTO); + } + + /** + * 获取视频详情 - 管理接口 + */ + @GetMapping("/{videoId}") + public Result getVideoDetail(@PathVariable Long videoId) { + return videoService.getAdminVideoDetail(videoId); + } + + /** + * 审核视频 - 管理接口 + */ + @PostMapping("/review/{videoId}") + public Result reviewVideo( + @PathVariable Long videoId, + @RequestParam Integer status, + @RequestParam(required = false) String reason) { + return videoService.reviewVideo(videoId, null, status, reason); + } + + /** + * 下架视频 - 管理接口 + */ + @PutMapping("/disable/{videoId}") + public Result disableVideo(@PathVariable Long videoId) { + return videoService.disableVideo(videoId); + } + + /** + * 获取视频统计信息 - 管理接口 + */ + @GetMapping("/stats") + public Result getVideoStats() { + return videoService.getVideoStats(); + } +} \ No newline at end of file diff --git a/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java b/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java index edb22fd..632a92a 100644 --- a/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java +++ b/video-content/src/main/java/com/nuaa/video/content/service/VideoService.java @@ -155,4 +155,24 @@ public interface VideoService extends IService { * @return 结果 */ Result reviewVideo(Long videoId, Long reviewerId, Integer status, String reason); + + /** + * 获取视频列表 - 管理接口 + */ + Result> getAdminVideoList(Page page, VideoQueryDTO queryDTO); + + /** + * 获取视频详情 - 管理接口 + */ + Result getAdminVideoDetail(Long videoId); + + /** + * 下架视频 - 管理接口 + */ + Result disableVideo(Long videoId); + + /** + * 获取视频统计信息 - 管理接口 + */ + Result getVideoStats(); } \ No newline at end of file diff --git a/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java b/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java index 54ed2b5..b35286d 100644 --- a/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java +++ b/video-content/src/main/java/com/nuaa/video/content/service/impl/VideoServiceImpl.java @@ -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> getAdminVideoList(Page page, VideoQueryDTO queryDTO) { + // 管理员可查看所有状态的视频,不需要过滤status + Page videoPage = videoMapper.selectVideoList(page, queryDTO); + + // 转换为VO + Page voPage = new Page<>(); + BeanUtils.copyProperties(videoPage, voPage, "records"); + + List 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> tagResult = tagService.getVideoTags(video.getId()); + if (tagResult.getCode() == 200 && tagResult.getData() != null) { + List 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 userResult = userFeignClient.getUserProfile(video.getUserId()); + if (userResult.getCode() == 200 && userResult.getData() != null) { + @SuppressWarnings("unchecked") + Map userData = (Map) 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 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> tagResult = tagService.getVideoTags(video.getId()); + if (tagResult.getCode() == 200 && tagResult.getData() != null) { + List 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 userResult = userFeignClient.getUserProfile(video.getUserId()); + if (userResult.getCode() == 200 && userResult.getData() != null) { + @SuppressWarnings("unchecked") + Map userData = (Map) 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() + .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 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 getVideoStats() { + // 收集视频统计数据 + Map stats = new HashMap<>(); + + // 总视频数 + long totalVideos = count(); + stats.put("totalVideos", totalVideos); + + // 今日新增视频 + LocalDate today = LocalDate.now(); + long todayNewVideos = count(new LambdaQueryWrapper() + .ge(ContentVideo::getCreateTime, today.atStartOfDay())); + stats.put("todayNewVideos", todayNewVideos); + + // 待审核视频数 + long pendingReviewVideos = count(new LambdaQueryWrapper() + .eq(ContentVideo::getStatus, 0)); + stats.put("pendingReviewVideos", pendingReviewVideos); + + // 已发布视频数 + long publishedVideos = count(new LambdaQueryWrapper() + .eq(ContentVideo::getStatus, 1)); + stats.put("publishedVideos", publishedVideos); + + // 已下架视频数 + long disabledVideos = count(new LambdaQueryWrapper() + .eq(ContentVideo::getStatus, 2)); + stats.put("disabledVideos", disabledVideos); + + // 审核未通过视频数 + long rejectedVideos = count(new LambdaQueryWrapper() + .eq(ContentVideo::getStatus, 3)); + stats.put("rejectedVideos", rejectedVideos); + + // 各分类视频数量分布 + Map categoryDistribution = new HashMap<>(); + List categories = categoryMapper.selectList(null); + for (ContentCategory category : categories) { + long count = count(new LambdaQueryWrapper() + .eq(ContentVideo::getCategoryId, category.getId())); + categoryDistribution.put(category.getName(), count); + } + stats.put("categoryDistribution", categoryDistribution); + + return Result.success(stats); + } } \ No newline at end of file diff --git a/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java b/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java index 7fa0f88..b53063d 100644 --- a/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java +++ b/video-content/src/main/java/com/nuaa/video/content/vo/VideoInfoVO.java @@ -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; } \ No newline at end of file diff --git a/video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java b/video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java new file mode 100644 index 0000000..05963bd --- /dev/null +++ b/video-user/src/main/java/com/nuaa/video/user/controller/AdminUserController.java @@ -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> getUserList( + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "size", defaultValue = "10") Integer size, + @RequestParam(value = "keyword", required = false) String keyword) { + Page pageParam = new Page<>(page, size); + return userInfoService.getAdminUserList(pageParam, keyword); + } + + /** + * 获取用户详情 - 管理接口 + */ + @GetMapping("/{userId}") + public Result getUserDetail(@PathVariable Long userId) { + return userInfoService.getAdminUserDetail(userId); + } + + /** + * 禁用/启用用户 - 管理接口 + */ + @PutMapping("/status/{userId}/{status}") + public Result updateUserStatus( + @PathVariable Long userId, + @PathVariable Integer status) { + return userInfoService.updateUserStatus(userId, status); + } + + /** + * 重置用户密码 - 管理接口 + */ + @PutMapping("/reset-password/{userId}") + public Result resetUserPassword( + @PathVariable Long userId, + @RequestParam String newPassword) { + return userInfoService.resetUserPassword(userId, newPassword); + } + + /** + * 获取用户统计信息 - 管理接口 + */ + @GetMapping("/stats") + public Result getUserStats() { + return userInfoService.getUserStats(); + } +} \ No newline at end of file diff --git a/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java b/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java index 7e86250..d72fb41 100644 --- a/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java +++ b/video-user/src/main/java/com/nuaa/video/user/feign/AuthFeignClient.java @@ -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 lockUser(@PathVariable Long userId, + @PathVariable Boolean locked); + + /** + * 管理员重置用户密码 - 管理接口 + */ + @PutMapping("/admin/reset-password/{userId}") + Result resetPassword( + @PathVariable Long userId, + @RequestParam String newPassword); } \ No newline at end of file diff --git a/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java b/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java index e1f61a6..bfdc4a1 100644 --- a/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java +++ b/video-user/src/main/java/com/nuaa/video/user/service/UserInfoService.java @@ -92,4 +92,29 @@ public interface UserInfoService extends IService { * @return 结果 */ Result increaseFavoriteCount(Long userId, Integer count); + + /** + * 获取用户列表 - 管理接口 + */ + Result> getAdminUserList(Page page, String keyword); + + /** + * 获取用户详情 - 管理接口 + */ + Result getAdminUserDetail(Long userId); + + /** + * 更新用户状态 - 管理接口 + */ + Result updateUserStatus(Long userId, Integer status); + + /** + * 重置用户密码 - 管理接口 + */ + Result resetUserPassword(Long userId, String newPassword); + + /** + * 获取用户统计信息 - 管理接口 + */ + Result getUserStats(); } \ No newline at end of file diff --git a/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java b/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java index bb803e7..4372aa5 100644 --- a/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java +++ b/video-user/src/main/java/com/nuaa/video/user/service/impl/UserInfoServiceImpl.java @@ -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 i private final UserSettingService userSettingService; private final MinioUtils minioUtils; private final UserStatDailyMapper userStatDailyMapper; + private final AuthFeignClient authFeignClient; @Override public Result getUserProfile(Long userId, Long currentUserId) { @@ -245,6 +252,133 @@ public class UserInfoServiceImpl extends ServiceImpl i return Result.success(); } + @Override + public Result> getAdminUserList(Page page, String keyword) { + LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>(); + + // 添加关键词搜索条件 + if (StringUtils.hasText(keyword)) { + queryWrapper.like(UserInfo::getNickname, keyword) + .or() + .like(UserInfo::getSignature, keyword); + } + + // 按创建时间降序排序 + queryWrapper.orderByDesc(UserInfo::getCreateTime); + + // 分页查询 + Page userPage = page(page, queryWrapper); + + // 转换为VO + Page voPage = new Page<>(); + BeanUtils.copyProperties(userPage, voPage, "records"); + + List 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 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 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 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 getUserStats() { + // 收集用户统计数据 + Map stats = new HashMap<>(); + + // 总用户数 + long totalUsers = count(); + stats.put("totalUsers", totalUsers); + + // 今日新增用户 + LocalDate today = LocalDate.now(); + long todayNewUsers = count(new LambdaQueryWrapper() + .ge(UserInfo::getCreateTime, today.atStartOfDay())); + stats.put("todayNewUsers", todayNewUsers); + + // 活跃用户数(最近7天有登录记录的用户) + // 此处需要结合用户登录记录表查询,简化处理 + stats.put("activeUsers", totalUsers * 0.3); // 示例数据 + + // 用户等级分布 + // 假设level字段代表用户等级 + Map levelDistribution = new HashMap<>(); + for (int i = 1; i <= 6; i++) { + int level = i; + long count = count(new LambdaQueryWrapper().eq(UserInfo::getLevel, level)); + levelDistribution.put("level" + i, count); + } + stats.put("levelDistribution", levelDistribution); + + return Result.success(stats); + } + /** * 更新用户每日统计数据 */