diff --git a/src/main/java/com/bao/dating/controller/AdminController.java b/src/main/java/com/bao/dating/controller/AdminController.java new file mode 100644 index 0000000..e7cf97f --- /dev/null +++ b/src/main/java/com/bao/dating/controller/AdminController.java @@ -0,0 +1,51 @@ +package com.bao.dating.controller; + +import com.bao.dating.common.Result; +import com.bao.dating.common.ResultCode; +import com.bao.dating.pojo.dto.UserBanDTO; +import com.bao.dating.pojo.entity.UserBan; +import com.bao.dating.service.UserBanService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +/** + * 管理员控制器 + * @author lenovo + */ +@RestController +@RequestMapping("/admin") +public class AdminController { + @Autowired + private UserBanService userBanService; + + /** + * 封禁用户 + */ + @PostMapping("/{userId}/ban") + public Result banUser(@PathVariable Long userId, + @RequestBody UserBanDTO userBanDTO) { + userBanDTO.setUserId(userId); + userBanService.banUser(userBanDTO); + return Result.success(ResultCode.SUCCESS, "封禁成功"); + } + + /** + * 解封用户 + */ + @PostMapping("/{userId}/unban") + public Result unbanUser(@PathVariable Long userId) { + + userBanService.unbanUser(userId); + return Result.success(ResultCode.SUCCESS, "解封成功"); + } + + /** + * 查询封禁状态 + */ + @GetMapping("/{userId}/banInfo") + public Result banInfo(@PathVariable Long userId) { + + UserBan ban = userBanService.getActiveBan(userId); + return Result.success(ResultCode.SUCCESS, "查询成功", ban); + } +} diff --git a/src/main/java/com/bao/dating/controller/UserController.java b/src/main/java/com/bao/dating/controller/UserController.java index d2bb7c8..8bb0789 100644 --- a/src/main/java/com/bao/dating/controller/UserController.java +++ b/src/main/java/com/bao/dating/controller/UserController.java @@ -204,4 +204,15 @@ public class UserController { return Result.success(ResultCode.SUCCESS,"用户登录成功",userLoginVO); } + /** + * 判断用户是否在线 + * @param userId 用户ID + * @return 用户是否在线 + */ + @GetMapping("/{userId}/online") + public Result isUserOnline(@PathVariable Long userId) { + + boolean online = userService.isUserOnline(userId); + return Result.success(ResultCode.SUCCESS, "查询成功", online); + } } diff --git a/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java b/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java index 3203fc9..2d976ec 100644 --- a/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java +++ b/src/main/java/com/bao/dating/interceptor/TokenInterceptor.java @@ -44,10 +44,10 @@ public class TokenInterceptor implements HandlerInterceptor { } // 从 header 获取 token String token = request.getHeader("token"); - + try { log.info("jwt校验: {}", token); - + // 验证 token 是否有效(包括是否过期) if (!JwtUtil.validateToken(token)) { log.error("Token无效或已过期"); @@ -66,10 +66,22 @@ public class TokenInterceptor implements HandlerInterceptor { response.getWriter().write("登录已失效, 请重新登录"); return false; } - + // 解析 token Long userId = Long.valueOf(JwtUtil.getSubjectFromToken(token)); + // 检查用户是否被封禁 + String banKey = "user:ban:" + userId; + if (Boolean.TRUE.equals(redisTemplate.hasKey(banKey))) { + String reason = String.valueOf(redisTemplate.opsForValue().get(banKey)); + log.error("用户 {} 已被封禁,原因:{}", userId, reason); + + response.setStatus(403); + response.setContentType("application/json;charset=UTF-8"); + response.getWriter().write("账号已被封禁:" + reason); + return false; + } + // 从Redis获取存储的token进行比对 Object redisTokenObj = redisTemplate.opsForValue().get("login:token:" + userId); String redisToken = redisTokenObj != null ? redisTokenObj.toString() : null; diff --git a/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java b/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java index a01153c..1e0e99a 100644 --- a/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java +++ b/src/main/java/com/bao/dating/interceptor/WsAuthInterceptor.java @@ -73,6 +73,14 @@ public class WsAuthInterceptor implements HandshakeInterceptor { Long userId = Long.valueOf(userIdStr); + // 检查用户是否被封禁 + String banKey = "user:ban:" + userId; + if (Boolean.TRUE.equals(redisTemplate.hasKey(banKey))) { + String reason = String.valueOf(redisTemplate.opsForValue().get(banKey)); + log.error("WebSocket拒绝:用户 {} 被封禁,原因:{}", userId, reason); + return false; + } + // 从Redis获取存储的token进行比对 String redisTokenKey = "login:token:" + userId; Object redisTokenObj = redisTemplate.opsForValue().get(redisTokenKey); diff --git a/src/main/java/com/bao/dating/mapper/UserBanMapper.java b/src/main/java/com/bao/dating/mapper/UserBanMapper.java new file mode 100644 index 0000000..a7d586c --- /dev/null +++ b/src/main/java/com/bao/dating/mapper/UserBanMapper.java @@ -0,0 +1,43 @@ +package com.bao.dating.mapper; + +import com.bao.dating.pojo.entity.UserBan; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +@Mapper +public interface UserBanMapper { + + /** + * 新增封禁记录 + * @param userBan 封禁记录 + * @return 影响行数 + */ + int insertBan(UserBan userBan); + + /** + * 查询是否存在生效中的封禁 + * @param userId 用户ID + * @return 存在返回1,不存在返回0 + */ + int existsActiveBan(@Param("userId") Long userId); + + /** + * 查询生效中的封禁记录 + * @param userId 用户ID + * @return 封禁记录 + */ + UserBan selectActiveBan(@Param("userId") Long userId); + + /** + * 解封用户 + * @param userId 用户ID + * @return 影响行数 + */ + int unbanUser(@Param("userId") Long userId); + + /** + * 定时任务:过期自动解封 + * @return 影响行数 + */ + int updateExpiredBans(); +} diff --git a/src/main/java/com/bao/dating/pojo/dto/UserBanDTO.java b/src/main/java/com/bao/dating/pojo/dto/UserBanDTO.java new file mode 100644 index 0000000..0f54079 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/dto/UserBanDTO.java @@ -0,0 +1,13 @@ +package com.bao.dating.pojo.dto; +import lombok.Data; + +/** + * 用户封禁数据传输对象 + * @author KilLze + */ +@Data +public class UserBanDTO { + private Long userId; + private String reason; + private Integer banDays; +} diff --git a/src/main/java/com/bao/dating/pojo/entity/UserBan.java b/src/main/java/com/bao/dating/pojo/entity/UserBan.java new file mode 100644 index 0000000..53bec33 --- /dev/null +++ b/src/main/java/com/bao/dating/pojo/entity/UserBan.java @@ -0,0 +1,30 @@ +package com.bao.dating.pojo.entity; + +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * 用户封禁记录 + * @author KilLze + */ +@Data +public class UserBan { + + private Long id; + + private Long userId; + + private String reason; + + private LocalDateTime banStartTime; + + private LocalDateTime banEndTime; + + /** + * 1:封禁中 0:已解封 + */ + private Integer status; + + private LocalDateTime createTime; +} diff --git a/src/main/java/com/bao/dating/pojo/vo/ChatSessionsVO.java b/src/main/java/com/bao/dating/pojo/vo/ChatSessionsVO.java index 3e28e5b..1be8926 100644 --- a/src/main/java/com/bao/dating/pojo/vo/ChatSessionsVO.java +++ b/src/main/java/com/bao/dating/pojo/vo/ChatSessionsVO.java @@ -29,4 +29,6 @@ public class ChatSessionsVO { private Integer topStatus; /** 免打扰状态 */ private Integer muteStatus; + /** 会话状态 */ + private Boolean online; } diff --git a/src/main/java/com/bao/dating/pojo/vo/UserInfoVO.java b/src/main/java/com/bao/dating/pojo/vo/UserInfoVO.java index 35c5c90..ec6ffa6 100644 --- a/src/main/java/com/bao/dating/pojo/vo/UserInfoVO.java +++ b/src/main/java/com/bao/dating/pojo/vo/UserInfoVO.java @@ -26,4 +26,5 @@ public class UserInfoVO implements Serializable { private LocalDateTime createdAt; private Double latitude; private Double longitude; + private Boolean online; } diff --git a/src/main/java/com/bao/dating/service/UserBanService.java b/src/main/java/com/bao/dating/service/UserBanService.java new file mode 100644 index 0000000..37a38de --- /dev/null +++ b/src/main/java/com/bao/dating/service/UserBanService.java @@ -0,0 +1,31 @@ +package com.bao.dating.service; + +import com.bao.dating.pojo.dto.UserBanDTO; +import com.bao.dating.pojo.entity.UserBan; + +/** + * 用户封禁服务接口 + * @author KilLze + */ +public interface UserBanService { + + /** + * 封禁用户 + * @param userBanDTO 用户封禁信息 + * + */ + void banUser(UserBanDTO userBanDTO); + + /** + * 解封用户 + * @param userId 用户ID + */ + void unbanUser(Long userId); + + /** + * 查询封禁信息 + * @param userId 用户ID + * @return 封禁信息 + */ + UserBan getActiveBan(Long userId); +} diff --git a/src/main/java/com/bao/dating/service/UserService.java b/src/main/java/com/bao/dating/service/UserService.java index 3e3fed1..ab23da1 100644 --- a/src/main/java/com/bao/dating/service/UserService.java +++ b/src/main/java/com/bao/dating/service/UserService.java @@ -92,4 +92,11 @@ public interface UserService { * @return 用户列表 */ List findNearbyUsers(double lat,double lng,double radiusKm); + + /** + * 判断用户是否在线 + * @param userId 用户ID + * @return 是否在线 + */ + boolean isUserOnline(Long userId); } diff --git a/src/main/java/com/bao/dating/service/impl/ChatServiceImpl.java b/src/main/java/com/bao/dating/service/impl/ChatServiceImpl.java index 517eb9c..ca27edf 100644 --- a/src/main/java/com/bao/dating/service/impl/ChatServiceImpl.java +++ b/src/main/java/com/bao/dating/service/impl/ChatServiceImpl.java @@ -236,6 +236,7 @@ public class ChatServiceImpl implements ChatService { vo.setSessionName("用户" + session.getTargetUserId()); vo.setAvatarUrl(null); } + vo.setOnline(userService.isUserOnline(vo.getTargetUserId())); return vo; }).collect(Collectors.toList()); } diff --git a/src/main/java/com/bao/dating/service/impl/UserBanServiceImpl.java b/src/main/java/com/bao/dating/service/impl/UserBanServiceImpl.java new file mode 100644 index 0000000..917e4dc --- /dev/null +++ b/src/main/java/com/bao/dating/service/impl/UserBanServiceImpl.java @@ -0,0 +1,74 @@ +package com.bao.dating.service.impl; + +import com.bao.dating.mapper.UserBanMapper; +import com.bao.dating.pojo.dto.UserBanDTO; +import com.bao.dating.pojo.entity.UserBan; +import com.bao.dating.service.UserBanService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.time.LocalDateTime; +import java.util.concurrent.TimeUnit; + +@Service +public class UserBanServiceImpl implements UserBanService { + + @Autowired + private UserBanMapper userBanMapper; + + @Autowired + private RedisTemplate redisTemplate; + + @Override + public void banUser(UserBanDTO userBanDTO) { + + // 已被封禁,直接拒绝 + if (userBanMapper.existsActiveBan(userBanDTO.getUserId()) > 0) { + throw new RuntimeException("用户已处于封禁状态"); + } + + LocalDateTime now = LocalDateTime.now(); + LocalDateTime endTime = userBanDTO.getBanDays() == null ? null : now.plusDays(userBanDTO.getBanDays()); + + // 1. 写数据库 + UserBan ban = new UserBan(); + ban.setUserId(userBanDTO.getUserId()); + ban.setReason(userBanDTO.getReason()); + ban.setBanStartTime(now); + ban.setBanEndTime(endTime); + ban.setStatus(1); + userBanMapper.insertBan(ban); + + // 2. 写 Redis + String key = "user:ban:" + userBanDTO.getUserId(); + if (userBanDTO.getBanDays() == null) { + redisTemplate.opsForValue().set(key, userBanDTO.getReason()); + } else { + redisTemplate.opsForValue().set(key, userBanDTO.getReason(), userBanDTO.getBanDays(), TimeUnit.DAYS); + } + + // 3. 踢下线 + redisTemplate.delete("login:token:" + userBanDTO.getUserId()); + } + + /** + * 解封用户 + */ + @Override + public void unbanUser(Long userId) { + // 更新数据库 + userBanMapper.unbanUser(userId); + + // 删除 Redis + redisTemplate.delete("user:ban:" + userId); + } + + /** + * 获取用户封禁信息 + */ + @Override + public UserBan getActiveBan(Long userId) { + return userBanMapper.selectActiveBan(userId); + } +} diff --git a/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java b/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java index e3210ed..3f3b9ec 100644 --- a/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java +++ b/src/main/java/com/bao/dating/service/impl/UserServiceImpl.java @@ -20,6 +20,7 @@ import com.bao.dating.util.CodeUtil; import com.bao.dating.util.FileUtil; import com.bao.dating.util.JwtUtil; import com.bao.dating.util.MD5Util; +import com.bao.dating.util.UserBanUtil; import io.jsonwebtoken.Claims; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; @@ -66,6 +67,9 @@ public class UserServiceImpl implements UserService { @Autowired private VerificationCodeService verificationCodeService; + @Autowired + private UserBanUtil userBanValidator; + /** * 用户登录 * @@ -92,6 +96,8 @@ public class UserServiceImpl implements UserService { if (!match) { throw new RuntimeException("密码错误"); } + // 用户封禁验证 + userBanValidator.validateUserNotBanned(user.getUserId()); // 生成token String token = JwtUtil.generateToken(String.valueOf(user.getUserId())); @@ -118,6 +124,9 @@ public class UserServiceImpl implements UserService { @Override public void logout(String token) { Claims claims = JwtUtil.getClaimsFromToken(token); + // 获取token信息 + String subject = claims.getSubject(); + // 获取token的过期时间 Date expiration = claims.getExpiration(); // 判断 token 是否已过期 long ttl = expiration.getTime() - System.currentTimeMillis(); @@ -126,6 +135,10 @@ public class UserServiceImpl implements UserService { return; } + // 从Redis中删除登录token记录 + String loginTokenKey = "login:token:" + subject; + redisTemplate.delete(loginTokenKey); + String logoutKey = "jwt:blacklist:" + token; redisTemplate.opsForValue().set( logoutKey, @@ -485,4 +498,23 @@ public class UserServiceImpl implements UserService { } return result; } -} + + @Override + public boolean isUserOnline(Long userId) { + if (userId == null) { + return false; + } + + // 1. 是否被封禁 + String banKey = "user:ban:" + userId; + if (Boolean.TRUE.equals(redisTemplate.hasKey(banKey))) { + return false; + } + + // 2. 是否存在登录 token + String tokenKey = "login:token:" + userId; + Boolean online = redisTemplate.hasKey(tokenKey); + + return Boolean.TRUE.equals(online); + } +} \ No newline at end of file diff --git a/src/main/java/com/bao/dating/task/UserBanScheduleTask.java b/src/main/java/com/bao/dating/task/UserBanScheduleTask.java new file mode 100644 index 0000000..3344e48 --- /dev/null +++ b/src/main/java/com/bao/dating/task/UserBanScheduleTask.java @@ -0,0 +1,26 @@ +package com.bao.dating.task; + +import com.bao.dating.mapper.UserBanMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.scheduling.annotation.EnableScheduling; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +@EnableScheduling +public class UserBanScheduleTask { + + @Autowired + private UserBanMapper userBanMapper; + + /** + * 每天凌晨 3 点同步过期封禁 + */ + @Scheduled(cron = "0 0 3 * * ?") + public void syncExpiredUserBan() { + int rows = userBanMapper.updateExpiredBans(); + log.info("封禁同步任务执行完成,解封 {} 个用户", rows); + } +} diff --git a/src/main/java/com/bao/dating/util/UserBanUtil.java b/src/main/java/com/bao/dating/util/UserBanUtil.java new file mode 100644 index 0000000..b53236a --- /dev/null +++ b/src/main/java/com/bao/dating/util/UserBanUtil.java @@ -0,0 +1,65 @@ +package com.bao.dating.util; + +import com.bao.dating.context.UserContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +import java.util.concurrent.TimeUnit; + +/** + * 用户封禁验证工具类 + * 提供统一的用户封禁状态检查功能 + * + * @author KilLze + */ +@Component +public class UserBanUtil { + + @Autowired + private RedisTemplate redisTemplate; + + /** + * 验证指定用户是否被封禁 + * + * @param userId 用户ID + * @throws RuntimeException 如果用户被封禁则抛出异常 + */ + public void validateUserNotBanned(Long userId) { + String banKey = "user:ban:" + userId; + if (Boolean.TRUE.equals(redisTemplate.hasKey(banKey))) { + String reason = (String) redisTemplate.opsForValue().get(banKey); + + // 获取剩余过期时间(秒) + Long ttlSeconds = redisTemplate.getExpire(banKey, TimeUnit.SECONDS); + String remainingTime = ""; + if (ttlSeconds != null && ttlSeconds > 0) { + long days = ttlSeconds / (24 * 3600); + long hours = (ttlSeconds % (24 * 3600)) / 3600; + long minutes = (ttlSeconds % 3600) / 60; + + if (days > 0) { + remainingTime = ",剩余时间:" + days + "天" + hours + "小时"; + } else if (hours > 0) { + remainingTime = ",剩余时间:" + hours + "小时" + minutes + "分钟"; + } else { + remainingTime = ",剩余时间:" + minutes + "分钟"; + } + } else { + remainingTime = ",永久封禁"; + } + + throw new RuntimeException("账号已被封禁,原因:" + reason + remainingTime); + } + } + + /** + * 验证当前登录用户是否被封禁 + * + * @throws RuntimeException 如果用户被封禁则抛出异常 + */ + public void validateCurrentUserNotBanned() { + Long userId = UserContext.getUserId(); + validateUserNotBanned(userId); + } +} \ No newline at end of file diff --git a/src/main/resources/com/bao/dating/mapper/UserBanMapper.xml b/src/main/resources/com/bao/dating/mapper/UserBanMapper.xml new file mode 100644 index 0000000..915d251 --- /dev/null +++ b/src/main/resources/com/bao/dating/mapper/UserBanMapper.xml @@ -0,0 +1,50 @@ + + + + + + + + INSERT INTO user_ban + (user_id, reason, ban_start_time, ban_end_time, status) + VALUES + (#{userId}, #{reason}, #{banStartTime}, #{banEndTime}, #{status}) + + + + + + + + + + + UPDATE user_ban + SET status = 0 + WHERE user_id = #{userId} + AND status = 1 + + + + + UPDATE user_ban + SET status = 0 + WHERE status = 1 + AND ban_end_time IS NOT NULL + AND ban_end_time < NOW() + + + \ No newline at end of file