在數字化社交時代,傳統社交平台的確定性匹配模式逐漸引發用户審美疲勞,交友盲盒模式應運而生。這種結合隨機性與趣味性的社交新形態,不僅滿足了年輕人對未知社交體驗的好奇,也創造了更具吸引力的社交裂變場景。本文詳細介紹了基於 Spring Boot 框架的交友盲盒系統的設計與實現,包括系統架構、核心功能模塊、數據庫設計方案以及關鍵代碼實現。
技術棧選擇
- 源碼及演示:m.ymzan.top
- 後端框架:Spring Boot 2.7.x
- 數據庫:MySQL 8.0 + Redis
- 消息中間件:WebSocket + RabbitMQ
- 安全框架:Spring Security + JWT
- 部署:Docker + Nginx
數據庫設計方案
1. 數據庫總體設計
系統採用MySQL作為主數據庫,Redis作為緩存和會話存儲。數據庫設計遵循第三範式,確保數據一致性。
2. 核心表結構設計
-- 用户表
CREATE TABLE `user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '加密密碼',
`nickname` varchar(50) DEFAULT NULL COMMENT '暱稱',
`avatar` varchar(255) DEFAULT NULL COMMENT '頭像URL',
`gender` tinyint DEFAULT '0' COMMENT '性別 0:未知 1:男 2:女',
`age` int DEFAULT NULL COMMENT '年齡',
`interests` json DEFAULT NULL COMMENT '興趣標籤',
`matching_count` int DEFAULT '0' COMMENT '匹配次數',
`status` tinyint DEFAULT '1' COMMENT '狀態 0:禁用 1:正常',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
`updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 盲盒匹配記錄表
CREATE TABLE `blind_box_match` (
`id` bigint NOT NULL AUTO_INCREMENT,
`match_code` varchar(32) NOT NULL COMMENT '匹配會話碼',
`user_id1` bigint NOT NULL COMMENT '用户1ID',
`user_id2` bigint NOT NULL COMMENT '用户2ID',
`match_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '匹配時間',
`expire_time` datetime DEFAULT NULL COMMENT '匹配過期時間',
`status` tinyint DEFAULT '1' COMMENT '1:匹配中 2:已完成 3:已過期',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_match_code` (`match_code`),
KEY `idx_user_id1` (`user_id1`),
KEY `idx_user_id2` (`user_id2`),
KEY `idx_status` (`status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='盲盒匹配記錄';
-- 聊天消息表
CREATE TABLE `chat_message` (
`id` bigint NOT NULL AUTO_INCREMENT,
`match_id` bigint NOT NULL COMMENT '匹配記錄ID',
`sender_id` bigint NOT NULL COMMENT '發送者ID',
`content` text COMMENT '消息內容',
`message_type` tinyint DEFAULT '1' COMMENT '1:文本 2:圖片 3:語音',
`is_read` tinyint DEFAULT '0' COMMENT '是否已讀',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_match_id` (`match_id`),
KEY `idx_sender_id` (`sender_id`),
KEY `idx_created` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='聊天消息表';
-- 用户反饋表
CREATE TABLE `user_feedback` (
`id` bigint NOT NULL AUTO_INCREMENT,
`match_id` bigint NOT NULL,
`user_id` bigint NOT NULL,
`target_user_id` bigint NOT NULL,
`rating` tinyint DEFAULT NULL COMMENT '評分1-5',
`comment` varchar(500) DEFAULT NULL COMMENT '評價內容',
`tags` json DEFAULT NULL COMMENT '標籤',
`created_at` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_match_user` (`match_id`,`user_id`),
KEY `idx_target_user` (`target_user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户評價表';
3. Redis緩存設計
- 用户會話:
session:{userId} - 匹配隊列:
matching:queue - 在線狀態:
online:{userId} - 消息緩存:
chat:{matchId}:last10
核心功能實現
1. 用户認證模塊
1.1 JWT工具類實現
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private int jwtExpiration;
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(userDetails.getUsername())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + jwtExpiration))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
public String getUsernameFromToken(String token) {
return Jwts.parser()
.setSigningKey(jwtSecret)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token);
return true;
} catch (Exception e) {
return false;
}
}
}
1.2 Spring Security配置
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final UserDetailsServiceImpl userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**", "/websocket/**").permitAll()
.antMatchers("/api/user/register", "/api/user/login").permitAll()
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
}
}
2. 盲盒匹配算法
2.1 匹配服務實現
@Service
@Slf4j
public class MatchService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private UserService userService;
@Autowired
private BlindBoxMatchRepository matchRepository;
private static final String MATCHING_QUEUE = "matching:queue";
private static final long MATCH_TIMEOUT = 30 * 60 * 1000; // 30分鐘
/**
* 加入匹配隊列
*/
public MatchResult joinMatchingQueue(Long userId) {
// 檢查用户是否已在隊列中
Boolean isInQueue = redisTemplate.opsForSet().isMember(MATCHING_QUEUE, userId.toString());
if (Boolean.TRUE.equals(isInQueue)) {
return MatchResult.alreadyInQueue();
}
// 加入隊列
redisTemplate.opsForSet().add(MATCHING_QUEUE, userId.toString());
// 嘗試匹配
return tryMatchUser(userId);
}
/**
* 嘗試匹配用户
*/
private MatchResult tryMatchUser(Long userId) {
// 獲取隊列中所有用户
Set<String> queueUsers = redisTemplate.opsForSet().members(MATCHING_QUEUE);
if (queueUsers == null || queueUsers.size() < 2) {
return MatchResult.waiting();
}
// 排除自己
List<Long> otherUsers = queueUsers.stream()
.map(Long::valueOf)
.filter(id -> !id.equals(userId))
.collect(Collectors.toList());
if (otherUsers.isEmpty()) {
return MatchResult.waiting();
}
// 隨機選擇匹配用户
Long matchedUserId = otherUsers.get(new Random().nextInt(otherUsers.size()));
// 創建匹配記錄
BlindBoxMatch match = createMatchRecord(userId, matchedUserId);
// 從隊列移除
redisTemplate.opsForSet().remove(MATCHING_QUEUE,
userId.toString(), matchedUserId.toString());
return MatchResult.success(match, matchedUserId);
}
/**
* 創建匹配記錄
*/
private BlindBoxMatch createMatchRecord(Long userId1, Long userId2) {
BlindBoxMatch match = new BlindBoxMatch();
match.setMatchCode(generateMatchCode());
match.setUserId1(userId1);
match.setUserId2(userId2);
match.setMatchTime(new Date());
match.setExpireTime(new Date(System.currentTimeMillis() + MATCH_TIMEOUT));
match.setStatus(MatchStatus.MATCHING.getCode());
return matchRepository.save(match);
}
private String generateMatchCode() {
return UUID.randomUUID().toString().replace("-", "");
}
}
2.2 匹配結果DTO
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MatchResult {
private boolean success;
private String message;
private String matchCode;
private Long matchedUserId;
private Date matchTime;
public static MatchResult success(BlindBoxMatch match, Long matchedUserId) {
return new MatchResult(true, "匹配成功",
match.getMatchCode(), matchedUserId, match.getMatchTime());
}
public static MatchResult waiting() {
return new MatchResult(false, "等待匹配中...",
null, null, null);
}
public static MatchResult alreadyInQueue() {
return new MatchResult(false, "您已在匹配隊列中",
null, null, null);
}
}
3. 即時通訊模塊
3.1 WebSocket配置
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Autowired
private ChatWebSocketHandler chatWebSocketHandler;
@Override
public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
registry.addHandler(chatWebSocketHandler, "/ws/chat")
.setAllowedOrigins("*")
.addInterceptors(new HttpSessionHandshakeInterceptor());
}
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
3.2 WebSocket處理器
@Component
@Slf4j
public class ChatWebSocketHandler extends TextWebSocketHandler {
@Autowired
private ChatService chatService;
@Autowired
private JwtTokenProvider jwtTokenProvider;
private static final ConcurrentHashMap<String, WebSocketSession> sessions =
new ConcurrentHashMap<>();
@Override
public void afterConnectionEstablished(WebSocketSession session) {
String token = extractToken(session);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
sessions.put(username, session);
log.info("用户 {} WebSocket連接建立", username);
} else {
try {
session.close(CloseStatus.NOT_ACCEPTABLE);
} catch (IOException e) {
log.error("關閉連接失敗", e);
}
}
}
@Override
protected void handleTextMessage(WebSocketSession session,
TextMessage message) throws Exception {
String payload = message.getPayload();
ChatMessageDTO chatMessage = JSON.parseObject(payload, ChatMessageDTO.class);
// 保存消息到數據庫
chatService.saveMessage(chatMessage);
// 轉發給接收方
WebSocketSession targetSession = sessions.get(chatMessage.getReceiverId());
if (targetSession != null && targetSession.isOpen()) {
targetSession.sendMessage(message);
}
}
private String extractToken(WebSocketSession session) {
List<String> tokenHeaders = session.getHandshakeHeaders().get("Authorization");
if (tokenHeaders != null && !tokenHeaders.isEmpty()) {
String header = tokenHeaders.get(0);
if (header.startsWith("Bearer ")) {
return header.substring(7);
}
}
return null;
}
}
3.3 消息傳輸對象
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatMessageDTO {
private Long matchId;
private Long senderId;
private Long receiverId;
private String content;
private Integer messageType;
private Date sendTime;
}
@Entity
@Table(name = "chat_message")
@Data
public class ChatMessage {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "match_id")
private Long matchId;
@Column(name = "sender_id")
private Long senderId;
@Column(name = "content")
private String content;
@Column(name = "message_type")
private Integer messageType;
@Column(name = "is_read")
private Boolean isRead = false;
@Column(name = "created_at")
private Date createdAt = new Date();
}
4. 用户反饋系統
@Service
public class FeedbackService {
@Autowired
private FeedbackRepository feedbackRepository;
@Autowired
private MatchRepository matchRepository;
/**
* 提交用户反饋
*/
public FeedbackResult submitFeedback(FeedbackRequest request) {
// 驗證匹配記錄
Optional<BlindBoxMatch> matchOpt = matchRepository.findById(request.getMatchId());
if (!matchOpt.isPresent()) {
return FeedbackResult.error("匹配記錄不存在");
}
BlindBoxMatch match = matchOpt.get();
if (!match.getUserId1().equals(request.getUserId()) &&
!match.getUserId2().equals(request.getUserId())) {
return FeedbackResult.error("無權評價此匹配");
}
// 檢查是否已評價
Optional<UserFeedback> existingFeedback = feedbackRepository
.findByMatchIdAndUserId(request.getMatchId(), request.getUserId());
if (existingFeedback.isPresent()) {
return FeedbackResult.error("您已評價過此次匹配");
}
// 保存評價
UserFeedback feedback = new UserFeedback();
feedback.setMatchId(request.getMatchId());
feedback.setUserId(request.getUserId());
feedback.setTargetUserId(getTargetUserId(match, request.getUserId()));
feedback.setRating(request.getRating());
feedback.setComment(request.getComment());
feedback.setTags(request.getTags());
feedbackRepository.save(feedback);
return FeedbackResult.success("評價提交成功");
}
private Long getTargetUserId(BlindBoxMatch match, Long userId) {
return match.getUserId1().equals(userId) ?
match.getUserId2() : match.getUserId1();
}
}
部署與監控
1. Docker部署配置
# Dockerfile
FROM openjdk:11-jre-slim
VOLUME /tmp
COPY target/blind-box-0.0.1-SNAPSHOT.jar app.jar
ENTRYPOINT ["java", "-jar", "/app.jar"]
2. 應用監控
- Spring Boot Actuator健康檢查
- Prometheus + Grafana監控
- ELK日誌收集
結論
基於Spring Boot的交友盲盒系統開發,不僅是一次技術創新實踐,更是對現代社交需求的深度迴應。通過本文闡述的分層架構設計、智能匹配算法和實時交互方案,大家可以構建了一個既保障系統性能又注重用户體驗的社交平台,通過合理的數據庫設計和緩存策略,能夠支撐較高的併發訪問。