init
This commit is contained in:
@@ -2,12 +2,22 @@ package com.timeline.user.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package com.timeline.user.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* STOMP 拦截器,用于在 STOMP CONNECT 时设置 Principal
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class StompPrincipalInterceptor implements ChannelInterceptor {
|
||||
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
|
||||
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
// 从session attributes中获取userId
|
||||
Map<String, Object> sessionAttrs = accessor.getSessionAttributes();
|
||||
if (sessionAttrs != null) {
|
||||
Object userId = sessionAttrs.get("userId");
|
||||
if (userId != null && accessor.getUser() == null) {
|
||||
String userIdStr = userId.toString();
|
||||
log.info("STOMP CONNECT: 从session attributes设置Principal为userId: {}", userIdStr);
|
||||
Principal principal = new XUserIdHandshakeInterceptor.UserPrincipal(userIdStr);
|
||||
accessor.setUser(principal);
|
||||
}
|
||||
}
|
||||
|
||||
// 如果还没有Principal,尝试从请求头获取
|
||||
if (accessor.getUser() == null) {
|
||||
String userId = accessor.getFirstNativeHeader("X-User-Id");
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
log.info("STOMP CONNECT: 从请求头设置Principal为userId: {}", userId);
|
||||
Principal principal = new XUserIdHandshakeInterceptor.UserPrincipal(userId);
|
||||
accessor.setUser(principal);
|
||||
}
|
||||
}
|
||||
|
||||
// 打印调试信息
|
||||
Principal principal = accessor.getUser();
|
||||
if (principal != null) {
|
||||
log.info("STOMP CONNECT: Principal已设置,用户: {}", principal.getName());
|
||||
} else {
|
||||
log.warn("STOMP CONNECT: 未设置Principal,sessionAttributes: {}", sessionAttrs);
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.timeline.user.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.support.DefaultHandshakeHandler;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class UserIdPrincipalHandshakeHandler extends DefaultHandshakeHandler {
|
||||
|
||||
@Override
|
||||
protected Principal determineUser(ServerHttpRequest request, WebSocketHandler wsHandler, Map<String, Object> attributes) {
|
||||
// 首先尝试从attributes中获取userId(由XUserIdHandshakeInterceptor放入)
|
||||
Object userId = attributes.get("userId");
|
||||
if (userId != null) {
|
||||
String userIdStr = userId.toString();
|
||||
log.info("WebSocket握手:设置Principal为userId: {},attributes: {}", userIdStr, attributes.keySet());
|
||||
// 使用 UserPrincipal 类而不是 lambda,确保 Principal 对象正确
|
||||
return new XUserIdHandshakeInterceptor.UserPrincipal(userIdStr);
|
||||
}
|
||||
|
||||
// 如果没有userId,则尝试获取username
|
||||
Object username = attributes.get("username");
|
||||
if (username != null) {
|
||||
String usernameStr = username.toString();
|
||||
log.info("WebSocket握手:设置Principal为username: {}", usernameStr);
|
||||
return new XUserIdHandshakeInterceptor.UserPrincipal(usernameStr);
|
||||
}
|
||||
|
||||
// 如果都没有,则使用默认实现
|
||||
log.warn("WebSocket握手:未找到userId或username,attributes: {},使用默认Principal", attributes.keySet());
|
||||
Principal defaultPrincipal = super.determineUser(request, wsHandler, attributes);
|
||||
if (defaultPrincipal != null) {
|
||||
log.info("WebSocket握手:使用默认Principal: {}", defaultPrincipal.getName());
|
||||
}
|
||||
return defaultPrincipal;
|
||||
}
|
||||
}
|
||||
@@ -1,21 +1,31 @@
|
||||
package com.timeline.user.config;
|
||||
|
||||
import com.timeline.user.interceptor.UserContextInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.servlet.config.annotation.ContentNegotiationConfigurer;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private UserContextInterceptor userContextInterceptor;
|
||||
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
registry.addInterceptor(userContextInterceptor)
|
||||
.addPathPatterns("/api/**")
|
||||
.excludePathPatterns("/api/auth/login", "/api/auth/register");
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
// 为SockJS添加静态资源映射,处理/info等端点
|
||||
registry.addResourceHandler("/user/ws/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
|
||||
// 添加对测试页面的静态资源映射
|
||||
registry.addResourceHandler("/test/**")
|
||||
.addResourceLocations("classpath:/static/");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
|
||||
// 配置内容协商策略,确保能正确处理JSON响应
|
||||
configurer.defaultContentType(MediaType.APPLICATION_JSON)
|
||||
.favorParameter(false)
|
||||
.favorPathExtension(false)
|
||||
.ignoreAcceptHeader(false);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
package com.timeline.user.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@Slf4j
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
private StompPrincipalInterceptor stompPrincipalInterceptor;
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/user/ws")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.addInterceptors(new XUserIdHandshakeInterceptor(applicationContext))
|
||||
.setHandshakeHandler(new UserIdPrincipalHandshakeHandler())
|
||||
.withSockJS();
|
||||
log.info("WebSocket 端点已注册: /user/ws");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 启用简单的消息代理,支持 /topic 和 /queue 前缀
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
// 设置应用程序目标前缀
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
// 设置用户目标前缀,用于点对点消息
|
||||
registry.setUserDestinationPrefix("/user");
|
||||
log.info("WebSocket 消息代理已配置: /topic, /queue, /app, /user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
// 添加STOMP拦截器,用于在CONNECT时设置Principal
|
||||
registration.interceptors(stompPrincipalInterceptor);
|
||||
log.info("已配置客户端入站通道拦截器");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package com.timeline.user.config;
|
||||
|
||||
import com.timeline.user.utils.JwtUtils;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import java.net.URI;
|
||||
import java.security.Principal;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
public class XUserIdHandshakeInterceptor implements HandshakeInterceptor {
|
||||
|
||||
// 从配置中获取JWT密钥
|
||||
@Value("${jwt.secret:timelineSecretKey}")
|
||||
private String jwtSecret = "timelineSecretKey";
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
public XUserIdHandshakeInterceptor(ApplicationContext applicationContext) {
|
||||
this.applicationContext = applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response,
|
||||
@NonNull WebSocketHandler wsHandler,
|
||||
@NonNull Map<String, Object> attributes) {
|
||||
if (request instanceof ServletServerHttpRequest servletRequest) {
|
||||
HttpServletRequest httpServletRequest = servletRequest.getServletRequest();
|
||||
|
||||
// 首先尝试从X-User-Id请求头获取userId(来自网关转发)
|
||||
String userId = httpServletRequest.getHeader("X-User-Id");
|
||||
String username = httpServletRequest.getHeader("X-Username");
|
||||
|
||||
// 如果没有从请求头获取到,则尝试从查询参数中获取token并解析
|
||||
if (userId == null || userId.isEmpty()) {
|
||||
URI uri = request.getURI();
|
||||
String query = uri.getRawQuery();
|
||||
if (query != null) {
|
||||
String[] params = query.split("&");
|
||||
String token = null;
|
||||
for (String param : params) {
|
||||
// 支持 token=xxx 格式
|
||||
if (param.startsWith("token=")) {
|
||||
token = param.substring(6);
|
||||
break;
|
||||
}
|
||||
// 支持 Authorization=Bearer xxx 或 Authorization=xxx 格式
|
||||
if (param.startsWith("Authorization=")) {
|
||||
String authValue = param.substring(14);
|
||||
if (authValue.startsWith("Bearer ")) {
|
||||
token = authValue.substring(7);
|
||||
} else {
|
||||
token = authValue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 解析 token
|
||||
if (token != null && !token.isEmpty()) {
|
||||
try {
|
||||
// URL 解码
|
||||
token = java.net.URLDecoder.decode(token, java.nio.charset.StandardCharsets.UTF_8);
|
||||
Claims claims = JwtUtils.parseToken(token, jwtSecret);
|
||||
if (claims != null) {
|
||||
userId = claims.get("userId", String.class);
|
||||
username = claims.getSubject();
|
||||
log.info("WebSocket握手:从查询参数解析token获取到userId: {}", userId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("WebSocket握手:解析token失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果获取到userId,则将其放入attributes中供后续使用
|
||||
if (userId != null && !userId.isEmpty()) {
|
||||
attributes.put("userId", userId);
|
||||
servletRequest.getServletRequest().setAttribute("userId", userId);
|
||||
log.info("WebSocket握手:从请求头获取到userId: {}", userId);
|
||||
} else {
|
||||
log.warn("WebSocket握手:未获取到userId,可能无法正确建立连接");
|
||||
}
|
||||
|
||||
// 如果获取到username,也将其存储
|
||||
if (username != null && !username.isEmpty()) {
|
||||
attributes.put("username", username);
|
||||
servletRequest.getServletRequest().setAttribute("username", username);
|
||||
log.debug("WebSocket握手:获取到username: {}", username);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(@NonNull ServerHttpRequest request,
|
||||
@org.springframework.lang.Nullable ServerHttpResponse response,
|
||||
@NonNull WebSocketHandler wsHandler,
|
||||
@org.springframework.lang.Nullable Exception exception) {
|
||||
// 在握手完成后,检查userId是否已设置
|
||||
if (request instanceof ServletServerHttpRequest servletRequest) {
|
||||
String userId = (String) servletRequest.getServletRequest().getAttribute("userId");
|
||||
if (userId != null) {
|
||||
log.info("WebSocket握手完成,userId: {}", userId);
|
||||
} else {
|
||||
log.warn("WebSocket握手完成,但未找到userId");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class UserPrincipal implements Principal {
|
||||
private final String name;
|
||||
|
||||
public UserPrincipal(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +1,105 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.user.dto.LoginRequest;
|
||||
import com.timeline.user.dto.LoginResponse;
|
||||
import com.timeline.user.dto.RegisterRequest;
|
||||
import com.timeline.user.dto.RefreshRequest;
|
||||
import com.timeline.user.entity.User;
|
||||
import com.timeline.user.service.UserAuthService;
|
||||
import com.timeline.user.service.UserService;
|
||||
import com.timeline.user.utils.JwtUtils;
|
||||
import com.timeline.common.utils.RedisUtils;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequestMapping("/user/auth")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
private UserAuthService userAuthService;
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
private static final String TOKEN_BLACKLIST_PREFIX = "auth:token:blacklist:";
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest loginRequest) {
|
||||
log.info("用户登录请求: {}", loginRequest.getUsername());
|
||||
LoginResponse response = userService.login(loginRequest);
|
||||
LoginResponse response = userAuthService.login(loginRequest);
|
||||
return ResponseEntity.success(response);
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<User> register(@RequestBody RegisterRequest registerRequest) {
|
||||
log.info("用户注册请求: {}", registerRequest.getUsername());
|
||||
User user = userService.register(registerRequest);
|
||||
User user = userAuthService.register(registerRequest);
|
||||
return ResponseEntity.success(user);
|
||||
}
|
||||
|
||||
@PostMapping("/refresh")
|
||||
public ResponseEntity<LoginResponse> refresh(@RequestBody RefreshRequest request) {
|
||||
String refreshToken = request.getRefreshToken();
|
||||
if (refreshToken == null || !jwtUtils.validateToken(refreshToken) || jwtUtils.isTokenExpired(refreshToken)) {
|
||||
return ResponseEntity.error(ResponseEnum.UNAUTHORIZED, "无效的刷新令牌");
|
||||
}
|
||||
if (!"refresh".equals(jwtUtils.getTokenType(refreshToken))) {
|
||||
return ResponseEntity.error(ResponseEnum.UNAUTHORIZED, "令牌类型错误");
|
||||
}
|
||||
String userId = jwtUtils.getUserIdFromToken(refreshToken);
|
||||
String username = jwtUtils.getUsernameFromToken(refreshToken);
|
||||
String newAccess = jwtUtils.generateAccessToken(userId, username);
|
||||
String newRefresh = jwtUtils.generateRefreshToken(userId, username);
|
||||
LoginResponse resp = new LoginResponse(newAccess, newRefresh, jwtUtils.getAccessExpirationSeconds(), userId, username);
|
||||
return ResponseEntity.success(resp);
|
||||
}
|
||||
|
||||
@PostMapping("/logout")
|
||||
public ResponseEntity<String> logout(@RequestHeader(value = "Authorization", required = false) String authHeader,
|
||||
@RequestBody(required = false) RefreshRequest request) {
|
||||
String accessToken = extractToken(authHeader);
|
||||
String refreshToken = request != null ? request.getRefreshToken() : null;
|
||||
|
||||
boolean hasToken = false;
|
||||
if (accessToken != null) {
|
||||
blacklist(accessToken);
|
||||
hasToken = true;
|
||||
}
|
||||
if (refreshToken != null && jwtUtils.validateToken(refreshToken)) {
|
||||
blacklist(refreshToken);
|
||||
hasToken = true;
|
||||
}
|
||||
if (!hasToken) {
|
||||
return ResponseEntity.error(ResponseEnum.BAD_REQUEST, "缺少可注销的令牌");
|
||||
}
|
||||
return ResponseEntity.success("已退出登录");
|
||||
}
|
||||
|
||||
@SuppressWarnings("null")
|
||||
private void blacklist(@NonNull String token) {
|
||||
long ttlSeconds = jwtUtils.getRemainingSeconds(token);
|
||||
if (ttlSeconds <= 0) {
|
||||
return;
|
||||
}
|
||||
Duration ttl = Duration.ofSeconds(ttlSeconds);
|
||||
redisUtils.set(TOKEN_BLACKLIST_PREFIX + token, "1", ttl);
|
||||
}
|
||||
|
||||
private String extractToken(String authHeader) {
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
return authHeader.substring(7);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.user.dto.FriendRequestDto;
|
||||
import com.timeline.user.dto.FriendUserDto;
|
||||
import com.timeline.user.entity.Friend;
|
||||
import com.timeline.user.entity.FriendNotify;
|
||||
import com.timeline.user.service.FriendService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/user/friend")
|
||||
public class FriendController {
|
||||
|
||||
@Autowired
|
||||
private FriendService friendService;
|
||||
|
||||
@PostMapping("/request")
|
||||
public ResponseEntity<String> requestFriend(@RequestBody FriendRequestDto dto) {
|
||||
friendService.requestFriend(dto.getFriendId());
|
||||
return ResponseEntity.success("请求已发送");
|
||||
}
|
||||
|
||||
@PostMapping("/accept")
|
||||
public ResponseEntity<String> accept(@RequestBody FriendRequestDto dto) {
|
||||
friendService.acceptFriend(dto.getFriendId());
|
||||
return ResponseEntity.success("已接受");
|
||||
}
|
||||
|
||||
@PostMapping("/reject")
|
||||
public ResponseEntity<String> reject(@RequestBody FriendRequestDto dto) {
|
||||
friendService.rejectFriend(dto.getFriendId());
|
||||
return ResponseEntity.success("已拒绝");
|
||||
}
|
||||
|
||||
@GetMapping("/list")
|
||||
public ResponseEntity<List<FriendUserDto>> list() {
|
||||
return ResponseEntity.success(friendService.listFriends());
|
||||
}
|
||||
|
||||
/* @GetMapping("/ids")
|
||||
public ResponseEntity<List<String>> friendIds() {
|
||||
return ResponseEntity.success(friendService.listFriendIds());
|
||||
} */
|
||||
|
||||
@GetMapping("/pending")
|
||||
public ResponseEntity<List<Friend>> pending() {
|
||||
return ResponseEntity.success(friendService.listPending());
|
||||
}
|
||||
|
||||
@GetMapping("/notify/unread")
|
||||
public ResponseEntity<List<FriendNotify>> unreadNotify() {
|
||||
return ResponseEntity.success(friendService.listUnreadNotify());
|
||||
}
|
||||
|
||||
@PostMapping("/notify/read/{id}")
|
||||
public ResponseEntity<String> markRead(@PathVariable Long id) {
|
||||
friendService.markNotifyRead(id);
|
||||
return ResponseEntity.success("已读");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.user.service.UserMessageService;
|
||||
import com.timeline.user.ws.WsNotifyService;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/user/message")
|
||||
public class MessagePushController {
|
||||
|
||||
@Autowired
|
||||
private WsNotifyService wsNotifyService;
|
||||
|
||||
@Autowired
|
||||
private UserMessageService userMessageService;
|
||||
|
||||
/**
|
||||
* 向指定用户推送自定义消息
|
||||
*
|
||||
* @param toUserId 目标用户ID
|
||||
* @param destination 消息目的地
|
||||
* @param message 消息内容
|
||||
* @return 响应结果
|
||||
*/
|
||||
@PostMapping("/push")
|
||||
public ResponseEntity<String> pushMessageToUser(
|
||||
@RequestParam String toUserId,
|
||||
@RequestParam String destination,
|
||||
@RequestBody String message) {
|
||||
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("message", message);
|
||||
payload.put("timestamp", System.currentTimeMillis());
|
||||
payload.put("type", "custom");
|
||||
|
||||
wsNotifyService.pushMessageToUser(toUserId, destination, payload);
|
||||
log.info("向用户 {} 推送消息到 {}: {}", toUserId, destination, message);
|
||||
|
||||
return ResponseEntity.success("消息已推送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户推送通知
|
||||
*
|
||||
* @param toUserId 目标用户ID
|
||||
* @param request 通知内容
|
||||
* @return 响应结果
|
||||
*/
|
||||
@PostMapping("/notify")
|
||||
public ResponseEntity<String> sendNotificationToUser(
|
||||
@RequestParam String toUserId,
|
||||
@RequestBody NotificationRequest request) {
|
||||
|
||||
NotificationPayload payload = new NotificationPayload();
|
||||
payload.setTitle(request.getTitle());
|
||||
payload.setContent(request.getContent());
|
||||
payload.setType(request.getType());
|
||||
payload.setTimestamp(System.currentTimeMillis());
|
||||
|
||||
wsNotifyService.sendNotificationToUser(toUserId, payload);
|
||||
|
||||
// 同时存储为未读消息
|
||||
userMessageService.addUnreadMessage(toUserId, Map.of(
|
||||
"type", "notification",
|
||||
"title", request.getTitle(),
|
||||
"content", request.getContent(),
|
||||
"notificationType", request.getType(),
|
||||
"timestamp", System.currentTimeMillis()
|
||||
));
|
||||
|
||||
log.info("向用户 {} 发送通知: {}", toUserId, request.getTitle());
|
||||
|
||||
return ResponseEntity.success("通知已发送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户添加未读消息(不会立即推送,只在下次连接时推送)
|
||||
*
|
||||
* @param toUserId 目标用户ID
|
||||
* @param request 消息内容
|
||||
* @return 响应结果
|
||||
*/
|
||||
@PostMapping("/unread")
|
||||
public ResponseEntity<String> addUnreadMessage(
|
||||
@RequestParam String toUserId,
|
||||
@RequestBody UnreadMessageRequest request) {
|
||||
|
||||
userMessageService.addUnreadMessage(toUserId, Map.of(
|
||||
"type", request.getType(),
|
||||
"title", request.getTitle(),
|
||||
"content", request.getContent(),
|
||||
"timestamp", System.currentTimeMillis()
|
||||
));
|
||||
|
||||
log.info("为用户 {} 添加未读消息: {}", toUserId, request.getTitle());
|
||||
|
||||
return ResponseEntity.success("未读消息已添加");
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class NotificationRequest {
|
||||
private String title;
|
||||
private String content;
|
||||
private String type; // info, warning, error
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class NotificationPayload {
|
||||
private String title;
|
||||
private String content;
|
||||
private String type;
|
||||
private long timestamp;
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class UnreadMessageRequest {
|
||||
private String title;
|
||||
private String content;
|
||||
private String type;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/permission")
|
||||
@RequestMapping("/user/permission")
|
||||
public class PermissionController {
|
||||
|
||||
@Autowired
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.user.listener.StompSubscriptionListener;
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 订阅信息控制器,用于查询用户的STOMP订阅状态
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/user/subscription")
|
||||
public class SubscriptionController {
|
||||
|
||||
@Autowired
|
||||
private StompSubscriptionListener stompSubscriptionListener;
|
||||
|
||||
/**
|
||||
* 获取指定用户的订阅信息
|
||||
* @param userId 用户ID
|
||||
* @return 订阅信息
|
||||
*/
|
||||
@GetMapping("/{userId}")
|
||||
public ResponseEntity<Set<String>> getUserSubscription(@PathVariable String userId) {
|
||||
Set<String> subscriptions = stompSubscriptionListener.getUserSubscriptions(userId);
|
||||
return ResponseEntity.success(subscriptions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户的订阅信息
|
||||
* @return 所有用户的订阅信息
|
||||
*/
|
||||
@GetMapping("/all")
|
||||
public ResponseEntity<Map<String, Set<String>>> getAllSubscriptions() {
|
||||
Map<String, Set<String>> subscriptions = stompSubscriptionListener.getAllSubscriptions();
|
||||
return ResponseEntity.success(subscriptions);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.user.ws.WsNotifyService;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 测试消息控制器,用于手动发送测试WebSocket消息
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/user/test-message")
|
||||
public class TestNotificationPayload {
|
||||
|
||||
@Autowired
|
||||
private WsNotifyService wsNotifyService;
|
||||
|
||||
/**
|
||||
* 发送测试好友通知
|
||||
*
|
||||
* @param toUserId 目标用户ID
|
||||
* @return 响应结果
|
||||
*/
|
||||
@PostMapping("/friend")
|
||||
public ResponseEntity<String> sendFriendTestMessage(@RequestParam String toUserId) {
|
||||
Map<String, Object> payload = buildMessagePayload(
|
||||
"test_friend_notification",
|
||||
"这是一条测试好友通知"
|
||||
);
|
||||
|
||||
wsNotifyService.sendFriendNotify(toUserId, payload);
|
||||
log.info("已发送测试好友通知给用户: {}", toUserId);
|
||||
|
||||
return ResponseEntity.success("测试好友通知已发送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试好友通知到所有频道
|
||||
*
|
||||
* @param toUserId 目标用户ID
|
||||
* @return 响应结果
|
||||
*/
|
||||
@PostMapping("/friend-all")
|
||||
public ResponseEntity<String> sendFriendTestMessageToAllChannels(@RequestParam String toUserId) {
|
||||
Map<String, Object> payload = buildMessagePayload(
|
||||
"test_friend_notification_all",
|
||||
"这是一条发送到所有频道的测试好友通知"
|
||||
);
|
||||
|
||||
wsNotifyService.sendFriendNotifyToAllChannels(toUserId, payload);
|
||||
log.info("已发送测试好友通知到所有频道给用户: {}", toUserId);
|
||||
|
||||
return ResponseEntity.success("测试好友通知已发送到所有频道");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试聊天消息
|
||||
*
|
||||
* @param toUserId 目标用户ID
|
||||
* @return 响应结果
|
||||
*/
|
||||
@PostMapping("/chat")
|
||||
public ResponseEntity<String> sendChatTestMessage(@RequestParam String toUserId) {
|
||||
Map<String, Object> payload = buildMessagePayload(
|
||||
"test_chat_message",
|
||||
"这是一条测试聊天消息"
|
||||
);
|
||||
|
||||
wsNotifyService.sendChatMessage(toUserId, payload);
|
||||
log.info("已发送测试聊天消息给用户: {}", toUserId);
|
||||
|
||||
return ResponseEntity.success("测试聊天消息已发送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测试通知
|
||||
*
|
||||
* @param toUserId 目标用户ID
|
||||
* @return 响应结果
|
||||
*/
|
||||
@PostMapping("/notification")
|
||||
public ResponseEntity<String> sendNotificationTestMessage(@RequestParam String toUserId) {
|
||||
Map<String, Object> payload = buildMessagePayload(
|
||||
"test_notification",
|
||||
"这是一条测试通知消息"
|
||||
);
|
||||
// 添加通知特有的字段
|
||||
payload.put("title", "测试通知");
|
||||
payload.put("type", "info");
|
||||
|
||||
wsNotifyService.sendNotificationToUser(toUserId, payload);
|
||||
log.info("已发送测试通知给用户: {}", toUserId);
|
||||
|
||||
return ResponseEntity.success("测试通知已发送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建通用消息负载
|
||||
*
|
||||
* @param type 消息类型
|
||||
* @param message 消息内容
|
||||
* @return 消息负载Map
|
||||
*/
|
||||
private Map<String, Object> buildMessagePayload(String type, String message) {
|
||||
Map<String, Object> payload = new HashMap<>();
|
||||
payload.put("type", type);
|
||||
payload.put("message", message);
|
||||
payload.put("timestamp", System.currentTimeMillis());
|
||||
return payload;
|
||||
}
|
||||
}
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.user.dto.RegisterRequest;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.user.dto.UpdateUser;
|
||||
import com.timeline.user.entity.User;
|
||||
import com.timeline.user.service.UserService;
|
||||
import com.timeline.user.utils.UserContext;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/user")
|
||||
@RequestMapping("/user")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
@@ -19,23 +25,28 @@ public class UserController {
|
||||
|
||||
@GetMapping("/info")
|
||||
public ResponseEntity<User> getCurrentUserInfo() {
|
||||
String userId = UserContext.getCurrentUserId();
|
||||
log.info("获取当前用户信息: {}", userId);
|
||||
User user = userService.getCurrentUser();
|
||||
return ResponseEntity.success(user);
|
||||
}
|
||||
/**
|
||||
* 查询指定用户信息
|
||||
* @param userId
|
||||
* @return
|
||||
*/
|
||||
@GetMapping("/{userId}")
|
||||
public ResponseEntity<User> updateUserInfo(@PathVariable String userId) {
|
||||
User user = userService.getUserByUserId(userId);
|
||||
return ResponseEntity.success(user);
|
||||
}
|
||||
|
||||
@PutMapping("/info")
|
||||
public ResponseEntity<User> updateUserInfo(@RequestBody RegisterRequest updateRequest) {
|
||||
String userId = UserContext.getCurrentUserId();
|
||||
log.info("更新用户信息: {}", userId);
|
||||
User user = userService.updateUserInfo(userId, updateRequest);
|
||||
return ResponseEntity.success(user);
|
||||
@GetMapping("/search")
|
||||
public ResponseEntity<List<User>> getMethodName(User user) {
|
||||
log.info(user.toString());
|
||||
return ResponseEntity.success(userService.searchUsers(user));
|
||||
}
|
||||
|
||||
|
||||
@DeleteMapping
|
||||
public ResponseEntity<String> deleteUser() {
|
||||
String userId = UserContext.getCurrentUserId();
|
||||
String userId = UserContextUtils.getCurrentUserId();
|
||||
log.info("删除用户: {}", userId);
|
||||
userService.deleteUser(userId);
|
||||
return ResponseEntity.success("用户删除成功");
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.user.dao.FriendNotifyMapper;
|
||||
import com.timeline.user.dto.UserMessageDto;
|
||||
import com.timeline.user.entity.FriendNotify;
|
||||
import com.timeline.user.service.UserMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户消息查询接口:未读消息 & 历史消息摘要
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/user/message")
|
||||
public class UserMessageController {
|
||||
|
||||
@Autowired
|
||||
private UserMessageService userMessageService;
|
||||
|
||||
@Autowired
|
||||
private FriendNotifyMapper friendNotifyMapper;
|
||||
|
||||
private String currentUserId() {
|
||||
return UserContextUtils.getCurrentUserId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的未读消息(包含系统/通知 + 好友请求等)
|
||||
*/
|
||||
@GetMapping("/unread")
|
||||
public ResponseEntity<List<UserMessageDto>> unreadMessages() {
|
||||
String userId = currentUserId();
|
||||
List<UserMessageDto> result = new ArrayList<>();
|
||||
|
||||
// 1. 来自 UserMessageService 的未读消息(内存/缓存)
|
||||
List<Map<String, Object>> unread = userMessageService.getUnreadMessages(userId);
|
||||
long now = System.currentTimeMillis();
|
||||
for (Map<String, Object> m : unread) {
|
||||
UserMessageDto dto = new UserMessageDto();
|
||||
dto.setCategory((String) m.getOrDefault("category", "notification"));
|
||||
dto.setType((String) m.getOrDefault("type", "generic"));
|
||||
dto.setFromUserId((String) m.get("fromUserId"));
|
||||
dto.setToUserId((String) m.getOrDefault("toUserId", userId));
|
||||
dto.setTitle((String) m.get("title"));
|
||||
dto.setContent((String) m.getOrDefault("content", m.get("message")));
|
||||
Object ts = m.get("timestamp");
|
||||
dto.setTimestamp(ts instanceof Number ? ((Number) ts).longValue() : now);
|
||||
dto.setStatus((String) m.getOrDefault("status", "unread"));
|
||||
result.add(dto);
|
||||
}
|
||||
|
||||
// 2. 未读好友通知(来自 friend_notify 表)
|
||||
List<FriendNotify> friendNotifies = friendNotifyMapper.selectUnread(userId);
|
||||
for (FriendNotify fn : friendNotifies) {
|
||||
UserMessageDto dto = new UserMessageDto();
|
||||
dto.setId(fn.getId());
|
||||
dto.setCategory("friend");
|
||||
dto.setType("friend_" + fn.getType()); // request / accept / reject -> friend_request 等
|
||||
dto.setFromUserId(fn.getFromUserId());
|
||||
dto.setToUserId(fn.getToUserId());
|
||||
dto.setTitle("好友通知");
|
||||
dto.setContent(fn.getContent());
|
||||
dto.setTimestamp(fn.getCreateTime() != null ? fn.getCreateTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli() : now);
|
||||
dto.setStatus(fn.getStatus());
|
||||
result.add(dto);
|
||||
}
|
||||
|
||||
// 简单按时间倒序
|
||||
result.sort((a, b) -> Long.compare(
|
||||
b.getTimestamp() != null ? b.getTimestamp() : 0L,
|
||||
a.getTimestamp() != null ? a.getTimestamp() : 0L
|
||||
));
|
||||
|
||||
log.info("用户 {} 未读消息数量: {}", userId, result.size());
|
||||
return ResponseEntity.success(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 历史消息摘要:目前返回最近的好友通知(不限已读/未读)
|
||||
* 如需更完整的历史,可扩展独立的消息表。
|
||||
*/
|
||||
@GetMapping("/history/friend")
|
||||
public ResponseEntity<List<UserMessageDto>> friendHistory() {
|
||||
String userId = currentUserId();
|
||||
// 复用 friend_notify 表,这里按“每个好友一条记录(取最新状态)”进行归并
|
||||
List<FriendNotify> all = friendNotifyMapper.selectAllByUser(userId);
|
||||
List<UserMessageDto> result = new ArrayList<>();
|
||||
long now = System.currentTimeMillis();
|
||||
|
||||
// key: fromUserId(谁发起/操作好友关系),value: 该好友的最新一条通知
|
||||
java.util.Map<String, FriendNotify> latestByFromUser = new java.util.HashMap<>();
|
||||
for (FriendNotify fn : all) {
|
||||
String from = fn.getFromUserId();
|
||||
if (from == null) {
|
||||
continue;
|
||||
}
|
||||
FriendNotify exist = latestByFromUser.get(from);
|
||||
if (exist == null) {
|
||||
latestByFromUser.put(from, fn);
|
||||
} else {
|
||||
// 按 create_time 取最新一条
|
||||
java.time.LocalDateTime existTime = exist.getCreateTime();
|
||||
java.time.LocalDateTime curTime = fn.getCreateTime();
|
||||
if (curTime != null && (existTime == null || curTime.isAfter(existTime))) {
|
||||
latestByFromUser.put(from, fn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 将每个好友的最新一条记录转换为摘要 DTO
|
||||
for (FriendNotify fn : latestByFromUser.values()) {
|
||||
UserMessageDto dto = new UserMessageDto();
|
||||
dto.setId(fn.getId());
|
||||
dto.setCategory("friend");
|
||||
String rawType = fn.getType(); // request / accept / reject
|
||||
dto.setType("friend_" + rawType);
|
||||
dto.setFromUserId(fn.getFromUserId());
|
||||
dto.setToUserId(fn.getToUserId());
|
||||
dto.setTitle("好友通知");
|
||||
|
||||
// 如果 content 为空,根据类型给出默认文案
|
||||
String content = fn.getContent();
|
||||
if (content == null || content.isEmpty()) {
|
||||
if ("request".equals(rawType)) {
|
||||
content = "向你发送了好友请求";
|
||||
} else if ("accept".equals(rawType)) {
|
||||
content = "已接受你的好友请求";
|
||||
} else if ("reject".equals(rawType)) {
|
||||
content = "已拒绝你的好友请求";
|
||||
} else {
|
||||
content = "好友关系发生变更";
|
||||
}
|
||||
}
|
||||
dto.setContent(content);
|
||||
|
||||
dto.setTimestamp(fn.getCreateTime() != null
|
||||
? fn.getCreateTime().atZone(java.time.ZoneId.systemDefault()).toInstant().toEpochMilli()
|
||||
: now);
|
||||
dto.setStatus(fn.getStatus());
|
||||
result.add(dto);
|
||||
}
|
||||
|
||||
result.sort((a, b) -> Long.compare(
|
||||
b.getTimestamp() != null ? b.getTimestamp() : 0L,
|
||||
a.getTimestamp() != null ? a.getTimestamp() : 0L
|
||||
));
|
||||
log.info("用户 {} 好友通知历史数量: {}", userId, result.size());
|
||||
return ResponseEntity.success(result);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package com.timeline.user.controller;
|
||||
|
||||
import com.timeline.common.response.ResponseEntity;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.user.ws.WsNotifyService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* WebSocket 测试控制器,用于测试消息推送功能
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/user/ws/test")
|
||||
public class WebSocketTestController {
|
||||
|
||||
@Autowired
|
||||
private WsNotifyService wsNotifyService;
|
||||
|
||||
/**
|
||||
* 测试推送通知消息给当前用户
|
||||
*/
|
||||
@PostMapping("/notify/self")
|
||||
public ResponseEntity<String> testNotifySelf(@RequestBody Map<String, Object> message) {
|
||||
String userId = UserContextUtils.getCurrentUserId();
|
||||
if (userId == null || userId.isEmpty()) {
|
||||
return ResponseEntity.error(401, "未获取到用户身份");
|
||||
}
|
||||
|
||||
log.info("测试推送通知给用户: {}", userId);
|
||||
// 确保消息包含必要字段
|
||||
if (!message.containsKey("timestamp")) {
|
||||
message.put("timestamp", System.currentTimeMillis());
|
||||
}
|
||||
if (!message.containsKey("type")) {
|
||||
message.put("type", "test");
|
||||
}
|
||||
wsNotifyService.sendNotificationToUser(userId, message);
|
||||
return ResponseEntity.success("通知已推送给用户: " + userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试推送通知消息给指定用户
|
||||
*/
|
||||
@PostMapping("/notify/{targetUserId}")
|
||||
public ResponseEntity<String> testNotifyUser(
|
||||
@PathVariable String targetUserId,
|
||||
@RequestBody Map<String, Object> message) {
|
||||
log.info("测试推送通知给用户: {}", targetUserId);
|
||||
wsNotifyService.sendNotificationToUser(targetUserId, message);
|
||||
return ResponseEntity.success("通知已推送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试推送好友通知
|
||||
*/
|
||||
@PostMapping("/friend/{targetUserId}")
|
||||
public ResponseEntity<String> testFriendNotify(
|
||||
@PathVariable String targetUserId,
|
||||
@RequestBody Map<String, Object> message) {
|
||||
log.info("测试推送好友通知给用户: {}", targetUserId);
|
||||
wsNotifyService.sendFriendNotifyToAllChannels(targetUserId, message);
|
||||
return ResponseEntity.success("好友通知已推送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试推送聊天消息
|
||||
*/
|
||||
@PostMapping("/chat/{targetUserId}")
|
||||
public ResponseEntity<String> testChatMessage(
|
||||
@PathVariable String targetUserId,
|
||||
@RequestBody Map<String, Object> message) {
|
||||
log.info("测试推送聊天消息给用户: {}", targetUserId);
|
||||
wsNotifyService.sendChatMessage(targetUserId, message);
|
||||
return ResponseEntity.success("聊天消息已推送");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.timeline.user.dao;
|
||||
|
||||
import com.timeline.user.dto.FriendUserDto;
|
||||
import com.timeline.user.entity.Friend;
|
||||
import com.timeline.user.entity.Friendship;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface FriendMapper {
|
||||
void insert(Friendship friendship);
|
||||
|
||||
Friendship selectByUsers(@Param("userId") String userId, @Param("friendId") String friendId);
|
||||
|
||||
void updateStatus(@Param("userId") String userId,
|
||||
@Param("friendId") String friendId,
|
||||
@Param("status") String status);
|
||||
|
||||
List<FriendUserDto> selectFriends(@Param("userId") String userId);
|
||||
|
||||
List<Friend> selectPending(@Param("toUserId") String toUserId);
|
||||
|
||||
// List<FriendUserDto> selectFriendUsers(FriendUserDto userDto);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.timeline.user.dao;
|
||||
|
||||
import com.timeline.user.entity.FriendNotify;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Mapper
|
||||
public interface FriendNotifyMapper {
|
||||
void insert(FriendNotify notify);
|
||||
List<FriendNotify> selectUnread(@Param("toUserId") String toUserId);
|
||||
void markRead(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 查询某个用户的全部好友通知,按时间倒序(用于历史记录)
|
||||
*/
|
||||
List<FriendNotify> selectAllByUser(@Param("toUserId") String toUserId);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package com.timeline.user.dao;
|
||||
|
||||
import com.timeline.user.entity.User;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
|
||||
@Mapper
|
||||
@@ -9,6 +12,7 @@ public interface UserMapper {
|
||||
User selectById(Long id);
|
||||
User selectByUserId(String userId);
|
||||
User selectByUsername(String username);
|
||||
List<User> searchUsers(User user);
|
||||
void update(User user);
|
||||
void deleteByUserId(String userId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.timeline.user.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class ChatMessage {
|
||||
/**
|
||||
* 接收方用户 ID
|
||||
*/
|
||||
private String toUserId;
|
||||
|
||||
/**
|
||||
* 发送方用户 ID(由服务端在 WebSocket 会话中填充)
|
||||
*/
|
||||
private String fromUserId;
|
||||
|
||||
/**
|
||||
* 发送方用户名(可选)
|
||||
*/
|
||||
private String fromUsername;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发送时间戳(毫秒)
|
||||
*/
|
||||
private Long timestamp;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
package com.timeline.user.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FriendNotifyPayload {
|
||||
/**
|
||||
* 消息大类,固定为 friend
|
||||
*/
|
||||
private String category = "friend";
|
||||
|
||||
/**
|
||||
* 好友操作类型:request / accept / reject
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 发送方用户 ID
|
||||
*/
|
||||
private String fromUserId;
|
||||
|
||||
/**
|
||||
* 发送方用户名(可选,用于前端展示)
|
||||
*/
|
||||
private String fromUsername;
|
||||
|
||||
/**
|
||||
* 提示文案或说明
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 事件时间戳(毫秒)
|
||||
*/
|
||||
private Long timestamp;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.timeline.user.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class FriendRequestDto {
|
||||
private String friendId;
|
||||
private String remark;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.timeline.user.dto;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import com.timeline.user.entity.User;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper=false)
|
||||
public class FriendUserDto extends User{
|
||||
private LocalDateTime createFriendTime;
|
||||
private String remark;
|
||||
private String friendStatus;
|
||||
}
|
||||
@@ -2,11 +2,15 @@ package com.timeline.user.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class LoginResponse {
|
||||
private String token;
|
||||
private String accessToken;
|
||||
private String refreshToken;
|
||||
private Long accessTokenExpiresInSeconds;
|
||||
private String userId;
|
||||
private String username;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
package com.timeline.user.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class RefreshRequest {
|
||||
private String refreshToken;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.timeline.user.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
public class UpdateUser {
|
||||
private String username;
|
||||
private String nickname;
|
||||
private String email;
|
||||
private String phone;
|
||||
private String avatar;
|
||||
private String description;
|
||||
private String location;
|
||||
private String tag;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.timeline.user.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 通用用户消息 DTO,用于 WebSocket 推送和历史记录查询
|
||||
*/
|
||||
@Data
|
||||
public class UserMessageDto {
|
||||
/**
|
||||
* 数据库主键(如果来源于持久化表,可以为 null)
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 消息大类:system / notification / friend / chat 等
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 业务类型:如 friend_request / friend_accepted / friend_rejected / connection_established 等
|
||||
*/
|
||||
private String type;
|
||||
|
||||
/**
|
||||
* 发送方用户 ID
|
||||
*/
|
||||
private String fromUserId;
|
||||
|
||||
/**
|
||||
* 接收方用户 ID
|
||||
*/
|
||||
private String toUserId;
|
||||
|
||||
/**
|
||||
* 可选标题(用于通知类消息)
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 文本内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 发送时间戳(毫秒)
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
/**
|
||||
* 状态:unread / read 等
|
||||
*/
|
||||
private String status;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.timeline.user.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class Friend {
|
||||
private Long id;
|
||||
private String userId;
|
||||
private String friendId;
|
||||
private Integer status; // 0 pending, 1 accepted, 2 rejected/blocked
|
||||
private String remark;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.timeline.user.entity;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@Data
|
||||
public class FriendNotify {
|
||||
private Long id;
|
||||
private String fromUserId;
|
||||
private String toUserId;
|
||||
private String type; // request / accept / reject
|
||||
private String status; // unread / read
|
||||
private String content;
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime readTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.timeline.user.entity;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Data;
|
||||
@Data
|
||||
public class Friendship {
|
||||
private Long id;
|
||||
private String userId;
|
||||
private String friendId;
|
||||
private Integer sortKey;
|
||||
private String status; // 0 pending, 1 accepted, 2 rejected/blocked
|
||||
private LocalDateTime createTime;
|
||||
private LocalDateTime updateTime;
|
||||
}
|
||||
@@ -15,6 +15,10 @@ public class User {
|
||||
private String phone;
|
||||
private Integer status; // 0-正常,1-禁用
|
||||
private Integer isDeleted; // 0-未删除,1-已删除
|
||||
private String avatar;
|
||||
private String description;
|
||||
private String location;
|
||||
private String tag;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
package com.timeline.user.interceptor;
|
||||
|
||||
import com.timeline.user.entity.User;
|
||||
import com.timeline.user.service.UserService;
|
||||
import com.timeline.user.utils.JwtUtils;
|
||||
import com.timeline.user.utils.UserContext;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerInterceptor;
|
||||
|
||||
@Component
|
||||
public class UserContextInterceptor implements HandlerInterceptor {
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Override
|
||||
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
|
||||
String token = request.getHeader("Authorization");
|
||||
if (token != null && token.startsWith("Bearer ")) {
|
||||
token = token.substring(7);
|
||||
if (jwtUtils.validateToken(token) && !jwtUtils.isTokenExpired(token)) {
|
||||
String userId = jwtUtils.getUserIdFromToken(token);
|
||||
User user = userService.getUserByUserId(userId);
|
||||
if (user != null) {
|
||||
UserContext.setUser(user);
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
|
||||
UserContext.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.timeline.user.listener;
|
||||
|
||||
import com.timeline.user.service.UserMessageService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class ApplicationStartupListener implements ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
@Autowired
|
||||
private UserMessageService userMessageService;
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
// 应用启动完成后初始化测试消息
|
||||
userMessageService.initializeTestMessages();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package com.timeline.user.listener;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
|
||||
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
|
||||
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* STOMP订阅监听器,用于跟踪用户的订阅信息
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class StompSubscriptionListener {
|
||||
|
||||
// 存储用户订阅信息:userId -> destinations
|
||||
private final Map<String, Set<String>> userSubscriptions = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 监听STOMP订阅事件
|
||||
* @param event 订阅事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketSubscribeListener(SessionSubscribeEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
|
||||
String userId = getUserId(headerAccessor);
|
||||
String destination = headerAccessor.getDestination();
|
||||
String subscriptionId = headerAccessor.getSubscriptionId();
|
||||
|
||||
if (userId != null && destination != null) {
|
||||
userSubscriptions.computeIfAbsent(userId, k -> new HashSet<>()).add(destination);
|
||||
log.info("用户 {} 订阅了 {}", userId, destination);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听STOMP取消订阅事件
|
||||
* @param event 取消订阅事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketUnsubscribeListener(SessionUnsubscribeEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
|
||||
String userId = getUserId(headerAccessor);
|
||||
String subscriptionId = headerAccessor.getSubscriptionId();
|
||||
|
||||
if (userId != null) {
|
||||
log.info("用户 {} 取消订阅 (subscriptionId: {})", userId, subscriptionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从headerAccessor中提取用户ID
|
||||
* @param headerAccessor STOMP头部访问器
|
||||
* @return 用户ID
|
||||
*/
|
||||
private String getUserId(StompHeaderAccessor headerAccessor) {
|
||||
// 从simpUser属性中获取用户ID
|
||||
if (headerAccessor.getUser() != null) {
|
||||
return headerAccessor.getUser().getName();
|
||||
}
|
||||
|
||||
// 如果没有从simpUser获取到,尝试从session属性中获取
|
||||
Object userIdAttr = headerAccessor.getSessionAttributes().get("userId");
|
||||
if (userIdAttr instanceof String) {
|
||||
return (String) userIdAttr;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的订阅信息
|
||||
* @param userId 用户ID
|
||||
* @return 订阅的目标地址集合
|
||||
*/
|
||||
public Set<String> getUserSubscriptions(String userId) {
|
||||
return userSubscriptions.getOrDefault(userId, new HashSet<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有用户的订阅信息
|
||||
* @return 所有用户的订阅信息
|
||||
*/
|
||||
public Map<String, Set<String>> getAllSubscriptions() {
|
||||
return userSubscriptions;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.timeline.user.service;
|
||||
|
||||
import com.timeline.user.dto.FriendUserDto;
|
||||
import com.timeline.user.entity.Friend;
|
||||
import com.timeline.user.entity.FriendNotify;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public interface FriendService {
|
||||
void requestFriend(String targetUserId);
|
||||
|
||||
void acceptFriend(String targetUserId);
|
||||
|
||||
void rejectFriend(String targetUserId);
|
||||
|
||||
List<FriendUserDto> listFriends();
|
||||
|
||||
List<Friend> listPending();
|
||||
|
||||
List<FriendNotify> listUnreadNotify();
|
||||
|
||||
void markNotifyRead(Long id);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
package com.timeline.user.service;
|
||||
|
||||
import com.timeline.user.dto.LoginRequest;
|
||||
import com.timeline.user.dto.LoginResponse;
|
||||
import com.timeline.user.dto.RegisterRequest;
|
||||
import com.timeline.user.entity.User;
|
||||
|
||||
public interface UserAuthService {
|
||||
LoginResponse login(LoginRequest loginRequest);
|
||||
User register(RegisterRequest registerRequest);
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package com.timeline.user.service;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import org.springframework.stereotype.Service;
|
||||
import java.util.Collections;
|
||||
|
||||
/**
|
||||
* 用户消息服务,用于管理用户的未读消息
|
||||
*/
|
||||
@Service
|
||||
public class UserMessageService {
|
||||
|
||||
// 模拟存储用户未读消息的数据结构
|
||||
// 在实际应用中,这应该存储在数据库或缓存中
|
||||
private final Map<String, List<Map<String, Object>>> userUnreadMessages = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 为用户添加一条未读消息(统一结构建议):
|
||||
* category/type/fromUserId/toUserId/title/content/timestamp/status 等
|
||||
* @param userId 用户ID
|
||||
* @param message 消息内容
|
||||
*/
|
||||
public void addUnreadMessage(String userId, Map<String, Object> message) {
|
||||
userUnreadMessages.computeIfAbsent(userId, k -> new ArrayList<>()).add(message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户的所有未读消息
|
||||
* @param userId 用户ID
|
||||
* @return 未读消息列表
|
||||
*/
|
||||
public List<Map<String, Object>> getUnreadMessages(String userId) {
|
||||
return userUnreadMessages.getOrDefault(userId, Collections.emptyList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除用户的所有未读消息
|
||||
* @param userId 用户ID
|
||||
*/
|
||||
public void clearUnreadMessages(String userId) {
|
||||
userUnreadMessages.remove(userId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化一些测试消息
|
||||
*/
|
||||
public void initializeTestMessages() {
|
||||
// 添加一些测试消息
|
||||
addUnreadMessage("testUser1", Map.of(
|
||||
"type", "notification",
|
||||
"title", "欢迎使用系统",
|
||||
"content", "感谢您注册我们的系统",
|
||||
"timestamp", System.currentTimeMillis()
|
||||
));
|
||||
|
||||
addUnreadMessage("testUser1", Map.of(
|
||||
"type", "friend_request",
|
||||
"title", "好友请求",
|
||||
"content", "用户John Doe想要添加您为好友",
|
||||
"timestamp", System.currentTimeMillis() - 3600000 // 1小时前
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,17 @@
|
||||
package com.timeline.user.service;
|
||||
|
||||
import com.timeline.user.entity.User;
|
||||
import com.timeline.user.dto.LoginRequest;
|
||||
import com.timeline.user.dto.RegisterRequest;
|
||||
import com.timeline.user.dto.LoginResponse;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import com.timeline.user.dto.UpdateUser;
|
||||
|
||||
public interface UserService {
|
||||
LoginResponse login(LoginRequest loginRequest);
|
||||
User register(RegisterRequest registerRequest);
|
||||
User getUserByUserId(String userId);
|
||||
User updateUserInfo(String userId, RegisterRequest updateRequest);
|
||||
User getCurrentUser();
|
||||
User updateUserInfo(UpdateUser updateUser);
|
||||
void deleteUser(String userId);
|
||||
boolean checkUserPermission(String userId, String requiredPermission);
|
||||
List<User> searchUsers(User user);
|
||||
User getUserInfo(String userId);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,225 @@
|
||||
package com.timeline.user.service.impl;
|
||||
|
||||
import com.timeline.common.constants.CommonConstants;
|
||||
import com.timeline.common.exception.CustomException;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.user.dao.FriendMapper;
|
||||
import com.timeline.user.dao.FriendNotifyMapper;
|
||||
import com.timeline.user.dao.UserMapper;
|
||||
import com.timeline.user.dto.FriendUserDto;
|
||||
import com.timeline.user.dto.FriendNotifyPayload;
|
||||
import com.timeline.user.entity.Friend;
|
||||
import com.timeline.user.entity.FriendNotify;
|
||||
import com.timeline.user.entity.Friendship;
|
||||
import com.timeline.user.entity.User;
|
||||
import com.timeline.user.service.FriendService;
|
||||
import com.timeline.user.service.UserMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class FriendServiceImpl implements FriendService {
|
||||
|
||||
@Autowired
|
||||
private FriendMapper friendMapper;
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
@Autowired
|
||||
private FriendNotifyMapper friendNotifyMapper;
|
||||
@Autowired
|
||||
private com.timeline.user.ws.WsNotifyService wsNotifyService;
|
||||
@Autowired
|
||||
private UserMessageService userMessageService;
|
||||
|
||||
private String currentUser() {
|
||||
String uid = UserContextUtils.getCurrentUserId();
|
||||
if (uid == null || uid.isEmpty()) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份");
|
||||
}
|
||||
return uid;
|
||||
}
|
||||
private String currentUsername() {
|
||||
String username = UserContextUtils.getCurrentUsername();
|
||||
if (username == null || username.isEmpty()) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "未获取到用户身份");
|
||||
}
|
||||
return username;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void requestFriend(String targetUserId) {
|
||||
String uid = currentUser();
|
||||
log.info("用户 {} 向用户 {} 发送好友请求", uid, targetUserId);
|
||||
User targetUser = userMapper.selectByUserId(targetUserId);
|
||||
if (targetUser == null) {
|
||||
throw new CustomException(ResponseEnum.BAD_REQUEST, "目标用户不存在");
|
||||
}
|
||||
if (uid.equals(targetUserId)) {
|
||||
throw new CustomException(ResponseEnum.BAD_REQUEST, "不能添加自己为好友");
|
||||
}
|
||||
Friendship exist = friendMapper.selectByUsers(uid, targetUserId);
|
||||
if (exist != null && exist.getStatus() == CommonConstants.FRIENDSHIP_ACCEPTED) {
|
||||
throw new CustomException(ResponseEnum.BAD_REQUEST, "已是好友");
|
||||
}
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
if (exist == null) {
|
||||
Friendship f = new Friendship();
|
||||
f.setUserId(uid);
|
||||
f.setFriendId(targetUserId);
|
||||
f.setStatus(CommonConstants.FRIENDSHIP_PENDING);
|
||||
f.setSortKey(CommonConstants.DEFAULT_SORT_KEY);
|
||||
f.setCreateTime(now);
|
||||
f.setUpdateTime(now);
|
||||
friendMapper.insert(f);
|
||||
} else {
|
||||
friendMapper.updateStatus(uid, targetUserId, CommonConstants.FRIENDSHIP_PENDING);
|
||||
}
|
||||
FriendNotify notify = new FriendNotify();
|
||||
notify.setFromUserId(uid);
|
||||
notify.setToUserId(targetUserId);
|
||||
notify.setType("request");
|
||||
notify.setStatus("unread");
|
||||
notify.setCreateTime(now);
|
||||
friendNotifyMapper.insert(notify);
|
||||
|
||||
FriendNotifyPayload payload = new FriendNotifyPayload();
|
||||
payload.setType("request");
|
||||
payload.setFromUserId(uid);
|
||||
payload.setFromUsername(currentUsername());
|
||||
payload.setContent("向你发送了好友请求");
|
||||
payload.setTimestamp(System.currentTimeMillis());
|
||||
log.info("准备发送好友请求通知给用户: {}", targetUserId);
|
||||
wsNotifyService.sendFriendNotifyToAllChannels(targetUserId, payload);
|
||||
|
||||
// 存储未读消息,以便用户下次连接时能收到
|
||||
userMessageService.addUnreadMessage(targetUserId, Map.of(
|
||||
"category", "friend",
|
||||
"type", "friend_request",
|
||||
"fromUserId", uid,
|
||||
"fromUsername", currentUsername(),
|
||||
"toUserId", targetUserId,
|
||||
"title", "好友请求",
|
||||
"content", "您收到了一个好友请求",
|
||||
"timestamp", System.currentTimeMillis(),
|
||||
"status", "unread"
|
||||
));
|
||||
log.info("好友请求已处理完毕");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptFriend(String targetUserId) {
|
||||
String uid = currentUser();
|
||||
log.info("用户 {} 接受了用户 {} 的好友请求", uid, targetUserId);
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
friendMapper.updateStatus(targetUserId, uid, CommonConstants.FRIENDSHIP_ACCEPTED); // 请求方记录
|
||||
|
||||
Friendship reverse = friendMapper.selectByUsers(uid, targetUserId);
|
||||
if (reverse == null) {
|
||||
Friendship f = new Friendship();
|
||||
f.setUserId(uid);
|
||||
f.setFriendId(targetUserId);
|
||||
f.setStatus(CommonConstants.FRIENDSHIP_ACCEPTED);
|
||||
f.setCreateTime(now);
|
||||
f.setUpdateTime(now);
|
||||
friendMapper.insert(f);
|
||||
} else {
|
||||
friendMapper.updateStatus(uid, targetUserId, CommonConstants.FRIENDSHIP_ACCEPTED);
|
||||
}
|
||||
|
||||
FriendNotify notify = new FriendNotify();
|
||||
notify.setFromUserId(uid);
|
||||
notify.setToUserId(targetUserId);
|
||||
notify.setType("accept");
|
||||
notify.setStatus("unread");
|
||||
notify.setCreateTime(now);
|
||||
friendNotifyMapper.insert(notify);
|
||||
|
||||
FriendNotifyPayload payload = new FriendNotifyPayload();
|
||||
payload.setType("accept");
|
||||
payload.setFromUserId(uid);
|
||||
payload.setFromUsername(currentUsername());
|
||||
payload.setContent("接受了你的好友请求");
|
||||
payload.setTimestamp(System.currentTimeMillis());
|
||||
log.info("准备发送好友接受通知给用户: {}", targetUserId);
|
||||
wsNotifyService.sendFriendNotifyToAllChannels(targetUserId, payload);
|
||||
|
||||
// 存储未读消息,以便用户下次连接时能收到
|
||||
userMessageService.addUnreadMessage(targetUserId, Map.of(
|
||||
"category", "friend",
|
||||
"type", "friend_accepted",
|
||||
"fromUserId", uid,
|
||||
"fromUsername", currentUsername(),
|
||||
"toUserId", targetUserId,
|
||||
"title", "好友请求已通过",
|
||||
"content", "您的好友请求已被接受",
|
||||
"timestamp", System.currentTimeMillis(),
|
||||
"status", "unread"
|
||||
));
|
||||
log.info("好友接受已处理完毕");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void rejectFriend(String targetUserId) {
|
||||
String uid = currentUser();
|
||||
log.info("用户 {} 拒绝了用户 {} 的好友请求", uid, targetUserId);
|
||||
|
||||
friendMapper.updateStatus(targetUserId, uid, CommonConstants.FRIENDSHIP_REJECTED);
|
||||
FriendNotify notify = new FriendNotify();
|
||||
notify.setFromUserId(uid);
|
||||
notify.setToUserId(targetUserId);
|
||||
notify.setType("reject");
|
||||
notify.setStatus("unread");
|
||||
notify.setCreateTime(LocalDateTime.now());
|
||||
friendNotifyMapper.insert(notify);
|
||||
|
||||
FriendNotifyPayload payload = new FriendNotifyPayload();
|
||||
payload.setType("reject");
|
||||
payload.setFromUserId(uid);
|
||||
payload.setContent("拒绝了你的好友请求");
|
||||
payload.setTimestamp(System.currentTimeMillis());
|
||||
log.info("准备发送好友拒绝通知给用户: {}", targetUserId);
|
||||
wsNotifyService.sendFriendNotifyToAllChannels(targetUserId, payload);
|
||||
|
||||
// 存储未读消息,以便用户下次连接时能收到
|
||||
userMessageService.addUnreadMessage(targetUserId, Map.of(
|
||||
"category", "friend",
|
||||
"type", "friend_rejected",
|
||||
"fromUserId", uid,
|
||||
"toUserId", targetUserId,
|
||||
"title", "好友请求被拒绝",
|
||||
"content", "您的好友请求已被拒绝",
|
||||
"timestamp", System.currentTimeMillis(),
|
||||
"status", "unread"
|
||||
));
|
||||
log.info("好友拒绝已处理完毕");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FriendUserDto> listFriends() {
|
||||
return friendMapper.selectFriends(currentUser());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Friend> listPending() {
|
||||
return friendMapper.selectPending(currentUser());
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public List<FriendNotify> listUnreadNotify() {
|
||||
return friendNotifyMapper.selectUnread(currentUser());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void markNotifyRead(Long id) {
|
||||
friendNotifyMapper.markRead(id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package com.timeline.user.service.impl;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.timeline.common.constants.CommonConstants;
|
||||
import com.timeline.common.exception.CustomException;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.common.utils.IdUtils;
|
||||
import com.timeline.common.utils.RedisUtils;
|
||||
import com.timeline.user.dao.UserMapper;
|
||||
import com.timeline.user.dto.LoginRequest;
|
||||
import com.timeline.user.dto.LoginResponse;
|
||||
import com.timeline.user.dto.RegisterRequest;
|
||||
import com.timeline.user.entity.User;
|
||||
import com.timeline.user.service.UserAuthService;
|
||||
import com.timeline.user.utils.JwtUtils;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class UserAuthServiceImpl implements UserAuthService {
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@Autowired
|
||||
private RedisUtils redisUtils;
|
||||
|
||||
@SuppressWarnings("null")
|
||||
@Override
|
||||
public LoginResponse login(LoginRequest loginRequest) {
|
||||
try {
|
||||
User user = userMapper.selectByUsername(loginRequest.getUsername());
|
||||
if (user == null) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户名或密码错误");
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户名或密码错误");
|
||||
}
|
||||
|
||||
if (user.getStatus() == 1) {
|
||||
throw new CustomException(ResponseEnum.FORBIDDEN, "用户已被禁用");
|
||||
}
|
||||
|
||||
String accessToken = jwtUtils.generateAccessToken(user.getUserId(), user.getUsername());
|
||||
String refreshToken = jwtUtils.generateRefreshToken(user.getUserId(), user.getUsername());
|
||||
redisUtils.set(loginRequest.getUsername(), refreshToken, jwtUtils.getAccessExpirationSeconds());
|
||||
return new LoginResponse(accessToken, refreshToken, jwtUtils.getAccessExpirationSeconds(), user.getUserId(), user.getUsername());
|
||||
} catch (CustomException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("用户登录失败", e);
|
||||
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "登录失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public User register(RegisterRequest registerRequest) {
|
||||
try {
|
||||
// 检查用户名是否已存在
|
||||
User existingUser = userMapper.selectByUsername(registerRequest.getUsername());
|
||||
if (existingUser != null) {
|
||||
throw new CustomException(ResponseEnum.BAD_REQUEST, "用户名已存在");
|
||||
}
|
||||
|
||||
User user = new User();
|
||||
user.setUserId(IdUtils.randomUuidUpper());
|
||||
user.setUsername(registerRequest.getUsername());
|
||||
user.setNickname(registerRequest.getNickname());
|
||||
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
|
||||
user.setEmail(registerRequest.getEmail());
|
||||
user.setPhone(registerRequest.getPhone());
|
||||
user.setStatus(CommonConstants.USER_STATUS_NORMAL); // 正常状态
|
||||
user.setIsDeleted(CommonConstants.NOT_DELETED);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
userMapper.insert(user);
|
||||
return user;
|
||||
} catch (CustomException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("用户注册失败", e);
|
||||
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "注册失败");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,22 +1,21 @@
|
||||
package com.timeline.user.service.impl;
|
||||
|
||||
import com.timeline.common.constants.CommonConstants;
|
||||
import com.timeline.common.exception.CustomException;
|
||||
import com.timeline.common.response.ResponseEnum;
|
||||
import com.timeline.common.utils.IdUtils;
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.user.dao.UserMapper;
|
||||
import com.timeline.user.entity.User;
|
||||
import com.timeline.user.dto.LoginRequest;
|
||||
import com.timeline.user.dto.RegisterRequest;
|
||||
import com.timeline.user.dto.LoginResponse;
|
||||
import com.timeline.user.dto.UpdateUser;
|
||||
import com.timeline.user.service.UserService;
|
||||
import com.timeline.user.utils.JwtUtils;
|
||||
import com.timeline.user.ws.WsNotifyService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@@ -24,88 +23,60 @@ public class UserServiceImpl implements UserService {
|
||||
|
||||
@Autowired
|
||||
private UserMapper userMapper;
|
||||
|
||||
|
||||
@Autowired
|
||||
private PasswordEncoder passwordEncoder;
|
||||
private WsNotifyService wsNotifyService;
|
||||
|
||||
@Autowired
|
||||
private JwtUtils jwtUtils;
|
||||
|
||||
@Override
|
||||
public LoginResponse login(LoginRequest loginRequest) {
|
||||
try {
|
||||
User user = userMapper.selectByUsername(loginRequest.getUsername());
|
||||
if (user == null) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户名或密码错误");
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(loginRequest.getPassword(), user.getPassword())) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED, "用户名或密码错误");
|
||||
}
|
||||
|
||||
if (user.getStatus() == 1) {
|
||||
throw new CustomException(ResponseEnum.FORBIDDEN, "用户已被禁用");
|
||||
}
|
||||
|
||||
String token = jwtUtils.generateToken(user.getUserId(), user.getUsername());
|
||||
return new LoginResponse(token, user.getUserId(), user.getUsername());
|
||||
} catch (CustomException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("用户登录失败", e);
|
||||
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "登录失败");
|
||||
private String getCurrentUserId() {
|
||||
String uid = UserContextUtils.getCurrentUserId();
|
||||
if (uid == null) {
|
||||
throw new CustomException(ResponseEnum.UNAUTHORIZED);
|
||||
}
|
||||
return uid;
|
||||
}
|
||||
|
||||
@Override
|
||||
public User register(RegisterRequest registerRequest) {
|
||||
try {
|
||||
// 检查用户名是否已存在
|
||||
User existingUser = userMapper.selectByUsername(registerRequest.getUsername());
|
||||
if (existingUser != null) {
|
||||
throw new CustomException(ResponseEnum.BAD_REQUEST, "用户名已存在");
|
||||
}
|
||||
|
||||
User user = new User();
|
||||
user.setUserId(IdUtils.randomUuidUpper());
|
||||
user.setUsername(registerRequest.getUsername());
|
||||
user.setNickname(registerRequest.getNickname());
|
||||
user.setPassword(passwordEncoder.encode(registerRequest.getPassword()));
|
||||
user.setEmail(registerRequest.getEmail());
|
||||
user.setPhone(registerRequest.getPhone());
|
||||
user.setStatus(CommonConstants.USER_STATUS_NORMAL); // 正常状态
|
||||
user.setIsDeleted(CommonConstants.NOT_DELETED);
|
||||
user.setCreateTime(LocalDateTime.now());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
userMapper.insert(user);
|
||||
return user;
|
||||
} catch (CustomException e) {
|
||||
throw e;
|
||||
} catch (Exception e) {
|
||||
log.error("用户注册失败", e);
|
||||
throw new CustomException(ResponseEnum.INTERNAL_SERVER_ERROR, "注册失败");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public User getUserByUserId(String userId) {
|
||||
return userMapper.selectByUserId(userId);
|
||||
}
|
||||
@Override
|
||||
public User getCurrentUser() {
|
||||
String userId = getCurrentUserId();
|
||||
log.info("获取当前用户信息: {}",userId);
|
||||
return userMapper.selectByUserId(userId);
|
||||
}
|
||||
@Override
|
||||
public User getUserInfo(String userId) {
|
||||
log.info("获取指定用户信息: {}",userId);
|
||||
return userMapper.selectByUserId(userId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public User updateUserInfo(String userId, RegisterRequest updateRequest) {
|
||||
public User updateUserInfo(UpdateUser updateUser) {
|
||||
try {
|
||||
String userId = getCurrentUserId();
|
||||
User user = userMapper.selectByUserId(userId);
|
||||
if (user == null) {
|
||||
throw new CustomException(ResponseEnum.NOT_FOUND, "用户不存在");
|
||||
}
|
||||
|
||||
user.setEmail(updateRequest.getEmail());
|
||||
user.setPhone(updateRequest.getPhone());
|
||||
user.setNickname(updateUser.getNickname());
|
||||
user.setEmail(updateUser.getEmail());
|
||||
user.setPhone(updateUser.getPhone());
|
||||
user.setAvatar(updateUser.getAvatar());
|
||||
user.setDescription(updateUser.getDescription());
|
||||
user.setLocation(updateUser.getLocation());
|
||||
user.setTag(updateUser.getTag());
|
||||
user.setUpdateTime(LocalDateTime.now());
|
||||
|
||||
userMapper.update(user);
|
||||
|
||||
// 用户信息更新后,通过WebSocket推送通知
|
||||
Map<String, Object> notification = new HashMap<>();
|
||||
notification.put("type", "user_profile_updated");
|
||||
notification.put("message", "您的个人信息已成功更新");
|
||||
notification.put("timestamp", System.currentTimeMillis());
|
||||
wsNotifyService.sendNotificationToUser(userId, notification);
|
||||
|
||||
return user;
|
||||
} catch (CustomException e) {
|
||||
throw e;
|
||||
@@ -139,4 +110,9 @@ public class UserServiceImpl implements UserService {
|
||||
User user = userMapper.selectByUserId(userId);
|
||||
return user != null && user.getStatus() == 0; // 用户存在且未被禁用
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<User> searchUsers(User user) {
|
||||
return userMapper.searchUsers(user);
|
||||
}
|
||||
}
|
||||
@@ -3,12 +3,15 @@ package com.timeline.user.utils;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
public class JwtUtils {
|
||||
@@ -16,25 +19,39 @@ public class JwtUtils {
|
||||
@Value("${jwt.secret:timelineSecretKey}")
|
||||
private String secret;
|
||||
|
||||
@Value("${jwt.expiration:86400}")
|
||||
private Long expiration;
|
||||
@Value("${jwt.access-expiration:900}")
|
||||
private Long accessExpirationSeconds;
|
||||
|
||||
public String generateToken(String userId, String username) {
|
||||
@Value("${jwt.refresh-expiration:604800}")
|
||||
private Long refreshExpirationSeconds;
|
||||
|
||||
public String generateAccessToken(String userId, String username) {
|
||||
return buildToken(userId, username, "access", accessExpirationSeconds);
|
||||
}
|
||||
|
||||
public String generateRefreshToken(String userId, String username) {
|
||||
return buildToken(userId, username, "refresh", refreshExpirationSeconds);
|
||||
}
|
||||
|
||||
private String buildToken(String userId, String username, String tokenType, long expiresInSeconds) {
|
||||
Map<String, Object> claims = new HashMap<>();
|
||||
claims.put("userId", userId);
|
||||
claims.put("username", username);
|
||||
claims.put("tokenType", tokenType);
|
||||
long now = System.currentTimeMillis();
|
||||
return Jwts.builder()
|
||||
.setClaims(claims)
|
||||
.setSubject(username)
|
||||
.setIssuedAt(new Date())
|
||||
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
|
||||
.signWith(SignatureAlgorithm.HS512, secret)
|
||||
.setIssuedAt(new Date(now))
|
||||
.setExpiration(new Date(now + expiresInSeconds * 1000))
|
||||
.signWith(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)), SignatureAlgorithm.HS512)
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Claims getClaimsFromToken(String token) {
|
||||
return Jwts.parser()
|
||||
.setSigningKey(secret)
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
}
|
||||
@@ -47,9 +64,20 @@ public class JwtUtils {
|
||||
return getClaimsFromToken(token).getSubject();
|
||||
}
|
||||
|
||||
public String getTokenType(String token) {
|
||||
return getClaimsFromToken(token).get("tokenType", String.class);
|
||||
}
|
||||
|
||||
public Long getAccessExpirationSeconds() {
|
||||
return accessExpirationSeconds;
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
Jwts.parser().setSigningKey(secret).parseClaimsJws(token);
|
||||
Jwts.parserBuilder()
|
||||
.setSigningKey(Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)))
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
return false;
|
||||
@@ -60,4 +88,29 @@ public class JwtUtils {
|
||||
Date expiration = getClaimsFromToken(token).getExpiration();
|
||||
return expiration.before(new Date());
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算 token 剩余有效期(秒),过期返回 0。
|
||||
*/
|
||||
public long getRemainingSeconds(String token) {
|
||||
try {
|
||||
Date exp = getClaimsFromToken(token).getExpiration();
|
||||
long diff = exp.getTime() - System.currentTimeMillis();
|
||||
return diff > 0 ? TimeUnit.MILLISECONDS.toSeconds(diff) : 0L;
|
||||
} catch (Exception e) {
|
||||
return 0L;
|
||||
}
|
||||
}
|
||||
|
||||
public static Claims parseToken(String token, String jwtSecret) {
|
||||
try {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(Keys.hmacShaKeyFor(jwtSecret.getBytes(StandardCharsets.UTF_8)))
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,29 +0,0 @@
|
||||
package com.timeline.user.utils;
|
||||
|
||||
import com.timeline.user.entity.User;
|
||||
|
||||
public class UserContext {
|
||||
private static final ThreadLocal<User> userHolder = new ThreadLocal<>();
|
||||
|
||||
public static void setUser(User user) {
|
||||
userHolder.set(user);
|
||||
}
|
||||
|
||||
public static User getCurrentUser() {
|
||||
return userHolder.get();
|
||||
}
|
||||
|
||||
public static String getCurrentUserId() {
|
||||
User user = getCurrentUser();
|
||||
return user != null ? user.getUserId() : null;
|
||||
}
|
||||
|
||||
public static String getCurrentUsername() {
|
||||
User user = getCurrentUser();
|
||||
return user != null ? user.getUsername() : null;
|
||||
}
|
||||
|
||||
public static void clear() {
|
||||
userHolder.remove();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.timeline.user.ws;
|
||||
|
||||
import com.timeline.common.utils.UserContextUtils;
|
||||
import com.timeline.user.dto.ChatMessage;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
public class ChatWsController {
|
||||
|
||||
@Autowired
|
||||
private WsNotifyService wsNotifyService;
|
||||
|
||||
@MessageMapping("/chat/send")
|
||||
public void send(@Payload ChatMessage msg, Principal principal) {
|
||||
if (principal == null) {
|
||||
log.warn("未认证用户尝试发送聊天消息");
|
||||
return;
|
||||
}
|
||||
String fromUserId = principal.getName();
|
||||
msg.setFromUserId(fromUserId);
|
||||
msg.setFromUsername(UserContextUtils.getCurrentUsername());
|
||||
msg.setTimestamp(System.currentTimeMillis());
|
||||
log.info("用户 {}({}) 向 {} 发送消息: {}", fromUserId, msg.getFromUsername(), msg.getToUserId(), msg.getContent());
|
||||
wsNotifyService.sendChatMessage(msg.getToUserId(), msg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
package com.timeline.user.ws;
|
||||
|
||||
import com.timeline.user.service.UserMessageService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.messaging.SessionConnectedEvent;
|
||||
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
|
||||
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
|
||||
import java.security.Principal;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WebSocketEventListener {
|
||||
|
||||
@Autowired
|
||||
@SuppressWarnings("unused")
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired
|
||||
private UserMessageService userMessageService;
|
||||
|
||||
@Autowired
|
||||
private WsNotifyService wsNotifyService;
|
||||
|
||||
/**
|
||||
* WebSocket 连接建立事件(STOMP CONNECT)
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
Principal principal = headerAccessor.getUser();
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
StompCommand command = headerAccessor.getCommand();
|
||||
|
||||
log.info("WebSocket 连接建立事件,会话ID: {},STOMP命令: {},Principal: {}",
|
||||
sessionId, command, principal != null ? principal.getName() : "null");
|
||||
|
||||
// 打印所有session attributes
|
||||
Map<String, Object> sessionAttrs = headerAccessor.getSessionAttributes();
|
||||
log.info("会话属性: {}", sessionAttrs);
|
||||
|
||||
if (principal != null) {
|
||||
String userId = principal.getName();
|
||||
log.info("WebSocket 连接建立,会话ID: {},用户ID: {}", sessionId, userId);
|
||||
|
||||
// 检查用户是否在注册表中
|
||||
org.springframework.messaging.simp.user.SimpUser user = userRegistry.getUser(userId);
|
||||
if (user != null) {
|
||||
log.info("用户 {} 已在注册表中,会话数: {}", userId, user.getSessions().size());
|
||||
} else {
|
||||
log.warn("用户 {} 不在注册表中!", userId);
|
||||
}
|
||||
} else {
|
||||
log.warn("WebSocket 连接建立,会话ID: {},但未获取到用户身份", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WebSocket 断开连接事件
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
Principal principal = headerAccessor.getUser();
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
|
||||
if (principal != null) {
|
||||
String userId = principal.getName();
|
||||
log.info("WebSocket 连接断开,用户ID: {},会话ID: {}", userId, sessionId);
|
||||
// 清除初始消息标记,允许下次连接时重新推送
|
||||
initialMessageSent.remove(userId);
|
||||
} else {
|
||||
log.warn("断开连接事件中未获取到用户身份,会话ID: {}", sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
// 用于跟踪已推送初始消息的用户,避免重复推送
|
||||
private final java.util.Set<String> initialMessageSent = java.util.concurrent.ConcurrentHashMap.newKeySet();
|
||||
|
||||
@Autowired
|
||||
private org.springframework.messaging.simp.user.SimpUserRegistry userRegistry;
|
||||
|
||||
/**
|
||||
* 订阅事件 - 当用户订阅某个频道时触发
|
||||
*/
|
||||
@EventListener
|
||||
public void handleSubscribeEvent(SessionSubscribeEvent event) {
|
||||
StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage());
|
||||
Principal principal = headerAccessor.getUser();
|
||||
String destination = headerAccessor.getDestination();
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
|
||||
if (principal != null) {
|
||||
String userId = principal.getName();
|
||||
log.info("用户 {} 订阅了频道: {},会话ID: {}", userId, destination, sessionId);
|
||||
|
||||
// 检查用户是否在注册表中
|
||||
org.springframework.messaging.simp.user.SimpUser user = userRegistry.getUser(userId);
|
||||
if (user != null) {
|
||||
log.info("用户 {} 已在注册表中,会话数: {}", userId, user.getSessions().size());
|
||||
} else {
|
||||
log.warn("用户 {} 不在注册表中!当前注册用户: {}", userId,
|
||||
userRegistry.getUsers().stream()
|
||||
.map(org.springframework.messaging.simp.user.SimpUser::getName)
|
||||
.toList());
|
||||
}
|
||||
|
||||
// 如果是第一次订阅(任意频道),推送初始消息
|
||||
if (!initialMessageSent.contains(userId)) {
|
||||
initialMessageSent.add(userId);
|
||||
log.info("用户 {} 首次订阅,准备推送初始消息", userId);
|
||||
CompletableFuture.delayedExecutor(500, TimeUnit.MILLISECONDS).execute(() -> {
|
||||
pushInitialMessages(userId);
|
||||
});
|
||||
}
|
||||
|
||||
// 如果订阅的是通知频道,立即推送未读消息
|
||||
if (destination != null && destination.contains("/queue/notification")) {
|
||||
CompletableFuture.delayedExecutor(300, TimeUnit.MILLISECONDS).execute(() -> {
|
||||
pushUnreadNotifications(userId);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
log.warn("订阅事件中未获取到用户身份,会话ID: {},目标: {},sessionAttributes: {}",
|
||||
sessionId, destination, headerAccessor.getSessionAttributes());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送初始消息给刚连接的用户
|
||||
*/
|
||||
private void pushInitialMessages(String userId) {
|
||||
try {
|
||||
log.info("开始推送初始消息给用户: {}", userId);
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
// 1. 推送连接成功的欢迎消息(统一结构)
|
||||
Map<String, Object> welcomeMsg = Map.of(
|
||||
"category", "system",
|
||||
"type", "connection_established",
|
||||
"fromUserId", "system",
|
||||
"toUserId", userId,
|
||||
"title", "连接成功",
|
||||
"content", "WebSocket 连接已建立",
|
||||
"timestamp", now,
|
||||
"status", "unread"
|
||||
);
|
||||
wsNotifyService.sendNotificationToUser(userId, welcomeMsg);
|
||||
log.info("已推送欢迎消息给用户: {}", userId);
|
||||
|
||||
// 2. 推送未读通知
|
||||
pushUnreadNotifications(userId);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("推送初始消息失败,用户ID: {}", userId, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送未读通知
|
||||
*/
|
||||
private void pushUnreadNotifications(String userId) {
|
||||
try {
|
||||
List<Map<String, Object>> unreadMessages = userMessageService.getUnreadMessages(userId);
|
||||
log.info("用户 {} 有 {} 条未读消息", userId, unreadMessages.size());
|
||||
|
||||
for (Map<String, Object> message : unreadMessages) {
|
||||
wsNotifyService.sendNotificationToUser(userId, message);
|
||||
log.debug("已推送未读消息给用户 {}: {}", userId, message);
|
||||
}
|
||||
|
||||
// 推送完成后清除未读消息
|
||||
if (!unreadMessages.isEmpty()) {
|
||||
userMessageService.clearUnreadMessages(userId);
|
||||
log.info("已清除用户 {} 的未读消息", userId);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("推送未读通知失败,用户ID: {}", userId, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.timeline.user.ws;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.messaging.simp.user.SimpUser;
|
||||
import org.springframework.messaging.simp.user.SimpUserRegistry;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* WebSocket 会话注册表,用于调试和管理用户会话
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class WebSocketSessionRegistry {
|
||||
|
||||
private final SimpUserRegistry userRegistry;
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
public WebSocketSessionRegistry(SimpUserRegistry userRegistry, SimpMessagingTemplate messagingTemplate) {
|
||||
this.userRegistry = userRegistry;
|
||||
this.messagingTemplate = messagingTemplate;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有已连接的用户
|
||||
*/
|
||||
public Set<String> getConnectedUsers() {
|
||||
return userRegistry.getUsers().stream()
|
||||
.map(SimpUser::getName)
|
||||
.collect(Collectors.toSet());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否在线
|
||||
*/
|
||||
public boolean isUserOnline(String userId) {
|
||||
SimpUser user = userRegistry.getUser(userId);
|
||||
boolean online = user != null;
|
||||
log.info("用户 {} 在线状态: {}", userId, online);
|
||||
if (online) {
|
||||
log.info("用户 {} 的会话数: {}", userId, user.getSessions().size());
|
||||
}
|
||||
return online;
|
||||
}
|
||||
|
||||
/**
|
||||
* 打印所有在线用户
|
||||
*/
|
||||
public void printOnlineUsers() {
|
||||
Set<String> users = getConnectedUsers();
|
||||
log.info("当前在线用户数: {}", users.size());
|
||||
users.forEach(userId -> log.info("在线用户: {}", userId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.timeline.user.ws;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.messaging.simp.user.SimpUser;
|
||||
import org.springframework.messaging.simp.user.SimpUserRegistry;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
public class WsNotifyService {
|
||||
|
||||
@Autowired
|
||||
private SimpMessagingTemplate messagingTemplate;
|
||||
|
||||
@Autowired
|
||||
private SimpUserRegistry userRegistry;
|
||||
|
||||
public void sendFriendNotify(String toUserId, Object payload) {
|
||||
log.info("发送好友通知给用户:{},内容:{}", toUserId, payload);
|
||||
checkUserOnline(toUserId);
|
||||
messagingTemplate.convertAndSendToUser(toUserId, "/queue/friend", payload);
|
||||
log.info("好友通知已发送到目标路径: /user/{}/queue/friend", toUserId);
|
||||
}
|
||||
|
||||
public void sendChatMessage(String toUserId, Object payload) {
|
||||
log.info("发送聊天消息给用户:{},内容:{}", toUserId, payload);
|
||||
checkUserOnline(toUserId);
|
||||
messagingTemplate.convertAndSendToUser(toUserId, "/queue/chat", payload);
|
||||
log.info("聊天消息已发送到目标路径: /user/{}/queue/chat", toUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户推送任意消息
|
||||
* @param toUserId 目标用户ID
|
||||
* @param destination 消息目的地
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void pushMessageToUser(String toUserId, String destination, Object payload) {
|
||||
log.info("推送消息给用户:{},目的地:{},内容:{}", toUserId, destination, payload);
|
||||
messagingTemplate.convertAndSendToUser(toUserId, destination, payload);
|
||||
log.debug("消息已推送");
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户推送通知消息
|
||||
* @param toUserId 目标用户ID
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void sendNotificationToUser(String toUserId, Object payload) {
|
||||
log.info("发送通知给用户:{},内容:{}", toUserId, payload);
|
||||
checkUserOnline(toUserId);
|
||||
messagingTemplate.convertAndSendToUser(toUserId, "/queue/notification", payload);
|
||||
log.info("通知已发送到目标路径: /user/{}/queue/notification", toUserId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 向指定用户推送好友相关通知到所有可能的频道
|
||||
* @param toUserId 目标用户ID
|
||||
* @param payload 消息内容
|
||||
*/
|
||||
public void sendFriendNotifyToAllChannels(String toUserId, Object payload) {
|
||||
log.info("向用户 {} 的所有频道发送好友通知,内容:{}", toUserId, payload);
|
||||
|
||||
// 发送到好友通知频道
|
||||
messagingTemplate.convertAndSendToUser(toUserId, "/queue/friend", payload);
|
||||
|
||||
// 发送到通知频道
|
||||
messagingTemplate.convertAndSendToUser(toUserId, "/queue/notification", payload);
|
||||
|
||||
log.info("好友通知已发送到所有频道");
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查用户是否在线并打印调试信息
|
||||
*/
|
||||
private void checkUserOnline(String userId) {
|
||||
SimpUser user = userRegistry.getUser(userId);
|
||||
if (user != null) {
|
||||
log.info("用户 {} 在线,会话数: {}", userId, user.getSessions().size());
|
||||
user.getSessions().forEach(session -> {
|
||||
log.debug("会话ID: {}, 订阅数: {}", session.getId(), session.getSubscriptions().size());
|
||||
});
|
||||
} else {
|
||||
log.warn("用户 {} 不在线,消息可能无法送达", userId);
|
||||
log.info("当前在线用户: {}", userRegistry.getUsers().stream()
|
||||
.map(SimpUser::getName)
|
||||
.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user