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