package com.movie.websocket; import java.io.IOException; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Component; import jakarta.websocket.OnClose; import jakarta.websocket.OnMessage; import jakarta.websocket.OnOpen; import jakarta.websocket.Session; import jakarta.websocket.server.PathParam; import jakarta.websocket.server.ServerEndpoint; /** * WebSocket 服务端 * 前端连接地址: ws://localhost:8080/ws/{userId} */ @Component @ServerEndpoint("/ws/{userId}") public class WebSocketServer { // 静态变量,用来记录当前在线连接数。应该把它设计成线程安全的。 private static ConcurrentHashMap sessionMap = new ConcurrentHashMap<>(); public static ConcurrentHashMap getSessionMap() { return sessionMap; } public static void setSessionMap(ConcurrentHashMap sessionMap) { WebSocketServer.sessionMap = sessionMap; } /** * 连接建立成功调用的方法 */ @OnOpen public void onOpen(Session session, @PathParam("userId") String userId) { sessionMap.put(userId, session); System.out.println("用户上线:" + userId + ", 当前在线人数:" + sessionMap.size()); } /** * 连接关闭调用的方法 */ @OnClose public void onClose(@PathParam("userId") String userId) { sessionMap.remove(userId); System.out.println("用户下线:" + userId + ", 当前在线人数:" + sessionMap.size()); } /** * 收到客户端消息后调用的方法 (可选) */ @OnMessage public void onMessage(String message, Session session) { System.out.println("收到消息:" + message); } /** * 自定义发送消息方法 (服务器 -> 客户端) */ @SuppressWarnings("CallToPrintStackTrace") public static void sendInfo(String userId, String message) { Session session = sessionMap.get(userId); if (session != null && session.isOpen()) { try { session.getBasicRemote().sendText(message); System.out.println("推送消息给用户 " + userId + " : " + message); } catch (IOException e) { e.printStackTrace(); } } else { System.out.println("用户 " + userId + " 不在线,消息未发送"); } } /** * 群发消息 */ @SuppressWarnings("CallToPrintStackTrace") public static void sendAll(String message) { for (Session session : sessionMap.values()) { if (session.isOpen()) { try { session.getBasicRemote().sendText(message); } catch (IOException e) { e.printStackTrace(); } } } System.out.println("群发消息完成: " + message); } } package com.movie.utils; import com.movie.entity.User; public class UserHolder { private static final ThreadLocal tl = new ThreadLocal<>(); public static void saveUser(User user){ tl.set(user); } public static User getUser(){ return tl.get(); } public static void removeUser(){ tl.remove(); } }package com.movie.task; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import com.movie.mapper.OrderMapper; @Component public class OrderTask { @Autowired private OrderMapper orderMapper; /** * 每分钟执行一次 (Cron表达式: 秒 分 时 日 月 周) * 检查是否有电影已经放完了,如果有,把订单改成已观影 */ @Scheduled(cron = "0 0/1 * * * ?") public void processWatchedOrders() { // System.out.println("执行定时任务:更新已观影状态 - " + LocalDateTime.now()); orderMapper.updateWatchedStatus(); } }package com.movie.repository; import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; import com.movie.doc.MovieDoc; public interface MovieRepository extends ElasticsearchRepository { // Spring Data Elasticsearch 会自动帮我们实现 CRUD 方法 }package com.movie.mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.movie.entity.ReviewReply; import com.movie.vo.ReplyVO; public interface ReviewReplyMapper extends BaseMapper { // 查询某条评论下的所有回复 (连表查用户) @Select("SELECT r.*, u.nickname, u.avatar_url, tu.nickname as target_nickname " + "FROM review_reply r " + "LEFT JOIN sys_user u ON r.user_id = u.id " + "LEFT JOIN sys_user tu ON r.target_user_id = tu.id " + "WHERE r.review_id = #{reviewId} AND r.is_deleted = 0 " + "ORDER BY r.create_time ASC") Page findRepliesByReviewId(Page page, @Param("reviewId") Long reviewId); }package com.movie.mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.movie.entity.Review; import com.movie.vo.ReviewVO; public interface ReviewMapper extends BaseMapper { // 自定义分页连表查询 @Select("SELECT r.*, u.nickname, u.avatar_url " + "FROM movie_review r " + "LEFT JOIN sys_user u ON r.user_id = u.id " + "WHERE r.movie_id = #{movieId} AND r.is_deleted = 0 " + "ORDER BY r.create_time DESC") Page findReviewsByMovieId(Page page, @Param("movieId") Long movieId); // 使用 ") Page findAdminReviews(Page page, @Param("keyword") String keyword); @Update("UPDATE movie_review SET like_count = like_count + 1 WHERE id = #{id}") void increaseLikeCount(Long id); @Update("UPDATE movie_review SET like_count = like_count - 1 WHERE id = #{id} AND like_count > 0") void decreaseLikeCount(Long id); }package com.movie.mapper; import java.math.BigDecimal; import org.apache.ibatis.annotations.Select; import org.apache.ibatis.annotations.Update; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.movie.entity.Order; /** *

* 购票订单表 Mapper 接口 *

* * @author Liu * @since 2025-12-25 */ public interface OrderMapper extends BaseMapper { // 统计总票房 (只算已支付 status=1 的) @Select("SELECT IFNULL(SUM(total_price), 0) FROM movie_order WHERE status = 1") BigDecimal sumTotalBoxOffice(); // 统计今日票房 (只算已支付 + 支付时间是今天) @Select("SELECT IFNULL(SUM(total_price), 0) FROM movie_order WHERE status = 1 AND DATE(pay_time) = CURDATE()") BigDecimal sumTodayBoxOffice(); // 定时任务专用:把所有 "已支付(1)" 且 "结束时间小于当前时间" 的订单,改为 "已观影(4)" // 这里用了连表更新 (UPDATE JOIN) @Update("UPDATE movie_order o " + "INNER JOIN movie_schedule s ON o.schedule_id = s.id " + "SET o.status = 4 " + "WHERE o.status = 1 AND s.end_time < NOW()") void updateWatchedStatus(); } package com.movie.listener; import java.util.List; import java.util.stream.Collectors; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.movie.config.RabbitMQConfig; import com.movie.entity.Order; import com.movie.service.IOrderService; @Component public class OrderTimeoutListener { @Autowired private IOrderService orderService; @Autowired private StringRedisTemplate redisTemplate; // 监听死信队列 @RabbitListener(queues = RabbitMQConfig.ORDER_RELEASE_QUEUE) public void listenOrderTimeout(String orderNo) { if (orderNo == null) return; // 1. 根据订单号查询订单 QueryWrapper query = new QueryWrapper<>(); query.eq("order_no", orderNo); Order order = orderService.getOne(query); if (order == null) return; // 2. 检查订单状态是否仍为“待支付” if (order.getStatus() == 0) { // 3. 状态仍为 0,说明超时未支付,关闭订单 order.setStatus(2); // 2-已取消 orderService.updateById(order); // 4. 释放 Redis 中的座位锁 String lockKeyPrefix = "lock:seat:" + order.getScheduleId() + ":"; List seats = List.of(order.getSeatInfo().split(",")); List lockKeys = seats.stream().map(seat -> lockKeyPrefix + seat).collect(Collectors.toList()); redisTemplate.delete(lockKeys); } } }package com.movie.listener; import org.springframework.amqp.rabbit.annotation.RabbitListener; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.movie.config.RabbitMQConfig; import com.movie.dto.LikeMsgDTO; import com.movie.entity.Review; import com.movie.entity.ReviewReply; import com.movie.entity.UserLikeRecord; import com.movie.service.IReviewReplyService; import com.movie.service.IReviewService; import com.movie.service.IUserLikeRecordService; @Component public class LikeCountListener { @Autowired private StringRedisTemplate redisTemplate; @Autowired private IReviewService reviewService; @Autowired private IReviewReplyService replyService; @Autowired private IUserLikeRecordService userLikeRecordService; // 记得创建这个Service @RabbitListener(queues = RabbitMQConfig.LIKE_COUNT_QUEUE) public void handleLikeMessage(LikeMsgDTO msg) { if (msg == null) return; // 1. 维护 MySQL 的点赞记录表 (持久化) if (msg.getIsLike()) { // 插入记录 (如果已存在可能会报错,建议加 try-catch 或 ignore) try { UserLikeRecord record = new UserLikeRecord(); record.setUserId(msg.getUserId()); record.setTargetId(msg.getTargetId()); record.setType(msg.getType()); // 1或2 userLikeRecordService.save(record); } catch (Exception e) { } } else { // 删除记录 QueryWrapper query = new QueryWrapper<>(); query.eq("user_id", msg.getUserId()) .eq("target_id", msg.getTargetId()) .eq("type", msg.getType()); userLikeRecordService.remove(query); } // 2. 更新 Redis 里的总数 -> 同步到 MySQL 计数列 String redisKey = (msg.getType() == 1 ? "review:like:" : "reply:like:") + msg.getTargetId(); Long size = redisTemplate.opsForSet().size(redisKey); int count = size != null ? size.intValue() : 0; if (msg.getType() == 1) { // 更新影评表 Review review = new Review(); review.setId(msg.getTargetId()); review.setLikeCount(count); reviewService.updateById(review); } else { // 更新回复表 ReviewReply reply = new ReviewReply(); reply.setId(msg.getTargetId()); reply.setLikeCount(count); replyService.updateById(reply); } } }package com.movie.interceptor; import java.util.concurrent.TimeUnit; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.HandlerInterceptor; import com.movie.entity.User; import com.movie.utils.UserHolder; import cn.hutool.core.util.StrUtil; import cn.hutool.json.JSONUtil; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public class LoginInterceptor implements HandlerInterceptor { @SuppressWarnings("FieldMayBeFinal") private StringRedisTemplate stringRedisTemplate; public LoginInterceptor(StringRedisTemplate stringRedisTemplate) { this.stringRedisTemplate = stringRedisTemplate; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 获取请求头中的 token String token = request.getHeader("authorization"); if (StrUtil.isBlank(token)) { response.setStatus(401); // 401 Unauthorized return false; } // 2. 基于 token 获取 redis 中的用户 String key = "login_token:" + token; String userJson = stringRedisTemplate.opsForValue().get(key); if (StrUtil.isBlank(userJson)) { response.setStatus(401); return false; } // 3. 将查询到的 Hash 数据转为 User 对象 User user = JSONUtil.toBean(userJson, User.class); // 4. 保存用户信息到 ThreadLocal UserHolder.saveUser(user); // 5. 刷新 token 有效期(用户只要在操作,token 就应该续期) stringRedisTemplate.expire(key, 24, TimeUnit.HOURS); // 6. 放行 return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { // 请求结束后,移除用户,防止内存泄漏 UserHolder.removeUser(); } }package com.movie.interceptor; import org.springframework.web.servlet.HandlerInterceptor; import com.movie.entity.User; import com.movie.utils.UserHolder; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; public class AdminAuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 从 ThreadLocal 获取用户信息 (LoginInterceptor 已经塞进去了) User user = UserHolder.getUser(); // 2. 检查用户是否存在,以及是否是管理员 if (user == null || Boolean.FALSE.equals(user.getIsAdmin())) { response.setStatus(403); // 403 Forbidden - 你有身份,但没权限 return false; } // 3. 是管理员,放行 return true; } }package com.movie.doc; import java.util.List; import org.springframework.data.annotation.Id; import org.springframework.data.elasticsearch.annotations.Document; import org.springframework.data.elasticsearch.annotations.Field; import org.springframework.data.elasticsearch.annotations.FieldType; @Document(indexName = "movie_index") // 定义 ES 索引名 public class MovieDoc { @Id private Long id; // 对应 movie_info 表的 ID // ik_max_word 是中文分词器,ik_smart 是更智能的 // analyzer 是写入时分词,searchAnalyzer 是搜索时分词 @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private String title; @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private String originalTitle; @Field(type = FieldType.Keyword) // Keyword 不分词,精确匹配 private String genre; @Field(type = FieldType.Keyword) private String country; @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private String synopsis; @Field(type = FieldType.Keyword) private String posterUrl; // 海报地址 @Field(type = FieldType.Double) private Double rating; // 评分 @Field(type = FieldType.Keyword) private String releaseDate; // 上映日期(存字符串即可,方便展示) // --- 关键:把关联信息也存进来 --- @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private List actors; // 演员名列表 ["成龙", "吴彦祖"] @Field(type = FieldType.Text, analyzer = "ik_max_word", searchAnalyzer = "ik_smart") private List directors; // 导演名列表 // --- 请手动生成 Getter / Setter --- public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getOriginalTitle() { return originalTitle; } public void setOriginalTitle(String originalTitle) { this.originalTitle = originalTitle; } public String getGenre() { return genre; } public void setGenre(String genre) { this.genre = genre; } public String getCountry() { return country; } public void setCountry(String country) { this.country = country; } public String getSynopsis() { return synopsis; } public void setSynopsis(String synopsis) { this.synopsis = synopsis; } public List getActors() { return actors; } public void setActors(List actors) { this.actors = actors; } public List getDirectors() { return directors; } public void setDirectors(List directors) { this.directors = directors; } public String getPosterUrl() { return posterUrl; } public void setPosterUrl(String posterUrl) { this.posterUrl = posterUrl; } public Double getRating() { return rating; } public void setRating(Double rating) { this.rating = rating; } public String getReleaseDate() { return releaseDate; } public void setReleaseDate(String releaseDate) { this.releaseDate = releaseDate; } }package com.movie.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.socket.server.standard.ServerEndpointExporter; @Configuration public class WebSocketConfig { /** * 注入 ServerEndpointExporter, * 这个 bean 会自动注册使用了 @ServerEndpoint 注解声明的 Websocket endpoint */ @Bean public ServerEndpointExporter serverEndpointExporter() { return new ServerEndpointExporter(); } }package com.movie.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.amqp.core.QueueBuilder; import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; import org.springframework.amqp.support.converter.MessageConverter; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class RabbitMQConfig { // --- 点赞队列 (不变) --- public static final String LIKE_COUNT_QUEUE = "like.count.queue.v2"; @Bean public Queue likeCountQueue() { return new Queue(LIKE_COUNT_QUEUE, true); } // --- 新增:订单超时处理 --- public static final String ORDER_TTL_QUEUE = "order.delay.queue"; // 延迟队列 public static final String ORDER_DLX_EXCHANGE = "order.dlx.exchange"; // 死信交换机 public static final String ORDER_RELEASE_QUEUE = "order.release.queue"; // 死信接收队列 public static final String ORDER_DLX_ROUTING_KEY = "order.release.key"; // 死信路由键 // 1. 定义死信交换机 @Bean public DirectExchange orderDlxExchange() { return new DirectExchange(ORDER_DLX_EXCHANGE); } // 2. 定义延迟队列 (消息过期后会进入死信交换机) @Bean public Queue orderTtlQueue() { return QueueBuilder.durable(ORDER_TTL_QUEUE) .ttl(900000) // 15分钟 TTL (15 * 60 * 1000) .deadLetterExchange(ORDER_DLX_EXCHANGE) // 绑定死信交换机 .deadLetterRoutingKey(ORDER_DLX_ROUTING_KEY) // 绑定死信路由键 .build(); } // 3. 定义死信队列 (真正消费消息的队列) @Bean public Queue orderReleaseQueue() { return new Queue(ORDER_RELEASE_QUEUE, true); } // 4. 绑定死信队列和死信交换机 @Bean public Binding orderDlxBinding() { return BindingBuilder.bind(orderReleaseQueue()).to(orderDlxExchange()).with(ORDER_DLX_ROUTING_KEY); } @Bean public MessageConverter messageConverter() { return new Jackson2JsonMessageConverter(); } }package com.movie.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; @Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 核心:如果不加这个拦截器,所有的 Page 对象都会失效,直接查全表 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }package com.movie.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import com.movie.interceptor.AdminAuthInterceptor; import com.movie.interceptor.LoginInterceptor; @Configuration public class MvcConfig implements WebMvcConfigurer { @Autowired private StringRedisTemplate stringRedisTemplate; @Override public void addInterceptors(InterceptorRegistry registry) { // 注册登录拦截器 registry.addInterceptor(new LoginInterceptor(stringRedisTemplate)) .addPathPatterns("/**") // 拦截所有 .excludePathPatterns( // --- 排除所有公开路径 --- // 用户认证 "/user/login", "/user/register", // 文件上传 "/file/upload", // 公开内容查询 "/movie/search", // ES 搜索 "/movie/top10", // 热门电影 "/movie/detail/**", // 电影详情 "/actorInfo/list", // 演员列表 "/actorInfo/{id}", // 演员详情 "/directorInfo/list", // 导演列表 "/directorInfo/{id}", // 导演详情 "/schedule/list/**", // 某电影的排片 "/review/list/**", // 某电影的评论 "/reply/list/**", "/cinema/list", "/hall/list/**" // 某评论的回复 ).order(1); registry.addInterceptor(new AdminAuthInterceptor()) .addPathPatterns( // --- 把所有管理员接口都加到这里 --- "/movie/add", "/movie/update", "/movie/delete/**", "/actor/**", "/director/**", "/cinema/**", "/hall/**", "/schedule/**", "/report/**", "/log/**", "/review/delete/**", "/review/admin/**", "/user/status/**", "/reply/delete/**" ) .excludePathPatterns( "/schedule/list/**", "/cinema/list/**" ,// 允许普通用户访问排片列表,不触发管理员检查 "/hall/list/**" // 允许普通用户访问影厅列表,不触发管理员检查 ) .order(2); } }package com.movie.config; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import io.minio.MinioClient; /** * MinIO 配置类 * 作用:读取 application.yml 中的 minio 配置,并注册 MinioClient 到 Spring 容器 */ @Configuration @ConfigurationProperties(prefix = "minio") // 告诉 Spring 读取 minio 开头的配置 public class MinioConfig { private String endpoint; private String accessKey; private String secretKey; private String bucketName; /** * 注入 MinioClient 客户端 * 以后在代码里直接 @Autowired MinioClient minioClient 就能用了 */ @Bean public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } public String getEndpoint() { return endpoint; } public void setEndpoint(String endpoint) { this.endpoint = endpoint; } public String getAccessKey() { return accessKey; } public void setAccessKey(String accessKey) { this.accessKey = accessKey; } public String getSecretKey() { return secretKey; } public void setSecretKey(String secretKey) { this.secretKey = secretKey; } public String getBucketName() { return bucketName; } public void setBucketName(String bucketName) { this.bucketName = bucketName; } }package com.movie.common; public class Result { private Integer code; // 200成功,500失败 private String message; private T data; public static Result success(T data) { Result r = new Result<>(); r.code = 200; r.message = "success"; r.data = data; return r; } public static Result error(String msg) { Result r = new Result<>(); r.code = 500; r.message = msg; return r; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public T getData() { return data; } public void setData(T data) { this.data = data; } }package com.movie.aspect; import java.lang.reflect.Method; import java.time.LocalDateTime; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.context.request.RequestContextHolder; import org.springframework.web.context.request.ServletRequestAttributes; import com.movie.annotation.SysLog; import com.movie.entity.Log; import com.movie.entity.User; import com.movie.service.ILogService; import com.movie.utils.UserHolder; import cn.hutool.json.JSONUtil; import jakarta.servlet.http.HttpServletRequest; @Aspect @Component public class LogAspect { @Autowired private ILogService logService; @Around("@annotation(com.movie.annotation.SysLog)") public Object saveLog(ProceedingJoinPoint point) throws Throwable { long beginTime = System.currentTimeMillis(); // 1. 执行目标方法 Object result = point.proceed(); // 2. 计算执行时长 long time = System.currentTimeMillis() - beginTime; // 3. 异步保存日志 recordLog(point, time); return result; } private void recordLog(ProceedingJoinPoint point, long time) { MethodSignature signature = (MethodSignature) point.getSignature(); Method method = signature.getMethod(); Log sysLog = new Log(); // --- 1. 设置操作内容 (Action) --- SysLog logAnnotation = method.getAnnotation(SysLog.class); if (logAnnotation != null) { sysLog.setAction(logAnnotation.value()); } // --- 2. 设置方法名 (Method) --- String className = point.getTarget().getClass().getName(); String methodName = signature.getName(); sysLog.setMethod(className + "." + methodName + "()"); // --- 3. 设置参数 (Params) --- // 使用 Hutool 工具类序列化参数,并截取过长内容防止数据库报错 try { Object[] args = point.getArgs(); String params = JSONUtil.toJsonStr(args); if (params.length() > 500) { params = params.substring(0, 500) + "..."; } sysLog.setParams(params); } catch (Exception e) { // 参数解析失败不影响日志记录 } // --- 4. 设置操作人 (Username) --- User user = UserHolder.getUser(); if (user != null) { sysLog.setUsername(user.getUsername()); } else { sysLog.setUsername("游客/未登录"); } // --- 5. 设置 IP 地址 (核心优化) --- // 获取 Request 对象 ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); if (attributes != null) { HttpServletRequest request = attributes.getRequest(); String ip = getIpAddr(request); // 使用增强版 IP 获取方法 sysLog.setIp(ip); } // --- 6. 设置耗时 (Time) - 对应你的数据库字段 --- sysLog.setTime(time); // --- 7. 设置创建时间 --- sysLog.setCreateTime(LocalDateTime.now()); // 保存到数据库 logService.save(sysLog); } /** * 获取客户端真实IP地址 (处理代理和本地IPv6问题) */ private String getIpAddr(HttpServletRequest request) { String ip = request.getHeader("x-forwarded-for"); if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getHeader("WL-Proxy-Client-IP"); } if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) { ip = request.getRemoteAddr(); } // 处理本地开发环境的 IPv6 地址 0:0:0:0:0:0:0:1 转为 127.0.0.1 if ("0:0:0:0:0:0:0:1".equals(ip)) { return "127.0.0.1"; } return ip; } }package com.movie.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Target(ElementType.METHOD) // 作用在方法上 @Retention(RetentionPolicy.RUNTIME) @Documented public @interface SysLog { String value() default ""; // 操作描述,例如 "删除电影" }