羽毛球匹配项目实施清单

1. 数据库设计

1.1 数据库选型

  • 数据库:MySQL
  • 理由:关系型数据库,支持复杂查询和事务处理,适合存储用户、匹配、聊天记录等结构化数据。

1.2 表结构设计

1.2.1 用户表(users)

字段名

类型

描述

id

BIGINT

主键,自增

username

VARCHAR(50)

用户名,唯一

password_hash

VARCHAR(255)

密码哈希

email

VARCHAR(100)

邮箱,唯一

phone

VARCHAR(20)

电话号码

skill_level

ENUM

打球水平(初学者、进阶玩家、随便挥拍)

interests

JSON

兴趣标签

created_at

TIMESTAMP

创建时间

updated_at

TIMESTAMP

更新时间

1.2.2 匹配表(matches)

字段名

类型

描述

id

BIGINT

主键,自增

user_id

BIGINT

发起匹配的用户ID

matched_user_id

BIGINT

被匹配的用户ID

status

ENUM

匹配状态(待接受、已接受、已拒绝)

created_at

TIMESTAMP

创建时间

updated_at

TIMESTAMP

更新时间

1.2.3 聊天记录表(messages)

字段名

类型

描述

id

BIGINT

主键,自增

match_id

BIGINT

所属匹配记录ID

sender_id

BIGINT

发送者用户ID

receiver_id

BIGINT

接收者用户ID

message

TEXT

消息内容

created_at

TIMESTAMP

发送时间

1.2.4 评分表(ratings)

字段名

类型

描述

id

BIGINT

主键,自增

match_id

BIGINT

所属匹配记录ID

rater_id

BIGINT

评分者用户ID

ratee_id

BIGINT

被评分者用户ID

rating

INT

评分(1-5)

comment

TEXT

评论内容

created_at

TIMESTAMP

评分时间

1.2.5 优惠券表(coupons)

字段名

类型

描述

id

BIGINT

主键,自增

user_id

BIGINT

所属用户ID

code

VARCHAR(50)

优惠券代码,唯一

type

ENUM

优惠券类型(折扣、免费场次等)

discount_amount

DECIMAL(10,2)

折扣金额

is_used

BOOLEAN

是否已使用

expires_at

TIMESTAMP

过期时间

created_at

TIMESTAMP

创建时间

updated_at

TIMESTAMP

更新时间

1.2.6 积分表(points)

字段名

类型

描述

id

BIGINT

主键,自增

user_id

BIGINT

所属用户ID

total_points

INT

当前积分总数

history

JSON

积分变动历史

created_at

TIMESTAMP

创建时间

updated_at

TIMESTAMP

更新时间

1.3 数据库关系图

users (1) <----> (N) matches <----> (N) messages
users (1) <----> (N) ratings
users (1) <----> (N) coupons
users (1) <----> (1) points

2. 后端开发(Spring Boot + MyBatis)

2.1 技术栈

  • 框架:Spring Boot
  • ORM 框架:MyBatis
  • 安全:Spring Security, JWT
  • 数据库:MySQL
  • 实时通信:Spring WebSocket (STOMP协议)
  • 构建工具:Maven

2.2 项目初始化

使用Spring Initializr创建项目,添加以下依赖:

  • Spring Web
  • MyBatis Framework
  • Spring Security
  • MySQL Driver
  • Spring WebSocket
  • Lombok (可选)
  • Spring Boot DevTools (可选)
示例:pom.xml(Maven)
<project xmlns="http://maven.apache.org/POM/4.0.0" ...><modelVersion>4.0.0</modelVersion><groupId>com.example</groupId><artifactId>badminton-social-app</artifactId><version>0.0.1-SNAPSHOT</version><name>Badminton Social App</name><description>羽毛球社交匹配小程序/H5</description><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.7.5</version><relativePath/> <!-- lookup parent from repository --></parent><dependencies><!-- Spring Boot Starter Dependencies --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- MyBatis Starter --><dependency><groupId>org.mybatis.spring.boot</groupId><artifactId>mybatis-spring-boot-starter</artifactId><version>2.2.0</version></dependency><!-- Spring Security --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><!-- JWT --><dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt</artifactId><version>0.9.1</version></dependency><!-- MySQL Driver --><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><scope>runtime</scope></dependency><!-- WebSocket --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-websocket</artifactId></dependency><!-- Lombok (Optional) --><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><optional>true</optional></dependency><!-- DevTools (Optional) --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><scope>runtime</scope></dependency></dependencies><build><plugins><!-- Spring Boot Maven Plugin --><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>

2.3 配置文件

application.properties
# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/badminton_social?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=your_password# MyBatis 配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.badminton_social_app.model# JWT 配置
jwt.secret=YourJWTSecretKey
jwt.expiration=86400000 # 1天的毫秒数# WebSocket 配置
spring.websocket.path=/ws

2.4 实体类设计

使用MyBatis不需要复杂的实体类注解,但为了方便,可以使用Lombok简化代码。

2.4.1 用户实体(User)
package com.example.badminton_social_app.model;import lombok.Data;import java.sql.Timestamp;@Data
public class User {private Long id;private String username;private String passwordHash;private String email;private String phone;private String skillLevel; // 使用字符串表示ENUMprivate String interests;  // JSON字符串private Timestamp createdAt;private Timestamp updatedAt;
}
2.4.2 匹配实体(Match)
package com.example.badminton_social_app.model;import lombok.Data;import java.sql.Timestamp;@Data
public class Match {private Long id;private Long userId;private Long matchedUserId;private String status; // 使用字符串表示ENUMprivate Timestamp createdAt;private Timestamp updatedAt;
}
2.4.3 聊天记录实体(Message)
package com.example.badminton_social_app.model;import lombok.Data;import java.sql.Timestamp;@Data
public class Message {private Long id;private Long matchId;private Long senderId;private Long receiverId;private String message;private Timestamp createdAt;
}
2.4.4 评分实体(Rating)
package com.example.badminton_social_app.model;import lombok.Data;import java.sql.Timestamp;@Data
public class Rating {private Long id;private Long matchId;private Long raterId;private Long rateeId;private Integer rating;private String comment;private Timestamp createdAt;
}
2.4.5 优惠券实体(Coupon)
package com.example.badminton_social_app.model;import lombok.Data;import java.math.BigDecimal;
import java.sql.Timestamp;@Data
public class Coupon {private Long id;private Long userId;private String code;private String type; // 使用字符串表示ENUMprivate BigDecimal discountAmount;private Boolean isUsed;private Timestamp expiresAt;private Timestamp createdAt;private Timestamp updatedAt;
}
2.4.6 积分实体(Point)
package com.example.badminton_social_app.model;import lombok.Data;import java.sql.Timestamp;@Data
public class Point {private Long id;private Long userId;private Integer totalPoints;private String history; // JSON字符串private Timestamp createdAt;private Timestamp updatedAt;
}

2.5 Mapper 接口与 XML 映射

2.5.1 UserMapper

接口:

package com.example.badminton_social_app.mapper;import com.example.badminton_social_app.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.Optional;@Mapper
public interface UserMapper {User findByUsername(@Param("username") String username);User findByEmail(@Param("email") String email);void insertUser(User user);User findById(@Param("id") Long id);void updateUser(User user);
}

XML 映射文件: src/main/resources/mapper/UserMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton_social_app.mapper.UserMapper"><resultMap id="UserResultMap" type="com.example.badminton_social_app.model.User"><id property="id" column="id" /><result property="username" column="username" /><result property="passwordHash" column="password_hash" /><result property="email" column="email" /><result property="phone" column="phone" /><result property="skillLevel" column="skill_level" /><result property="interests" column="interests" /><result property="createdAt" column="created_at" /><result property="updatedAt" column="updated_at" /></resultMap><select id="findByUsername" parameterType="String" resultMap="UserResultMap">SELECT * FROM users WHERE username = #{username}</select><select id="findByEmail" parameterType="String" resultMap="UserResultMap">SELECT * FROM users WHERE email = #{email}</select><select id="findById" parameterType="Long" resultMap="UserResultMap">SELECT * FROM users WHERE id = #{id}</select><insert id="insertUser" parameterType="com.example.badminton_social_app.model.User">INSERT INTO users (username, password_hash, email, phone, skill_level, interests, created_at, updated_at)VALUES (#{username}, #{passwordHash}, #{email}, #{phone}, #{skillLevel}, #{interests}, NOW(), NOW())</insert><update id="updateUser" parameterType="com.example.badminton_social_app.model.User">UPDATE usersSETusername = #{username},password_hash = #{passwordHash},email = #{email},phone = #{phone},skill_level = #{skillLevel},interests = #{interests},updated_at = NOW()WHERE id = #{id}</update></mapper>
2.5.2 MatchMapper

接口:

package com.example.badminton_social_app.mapper;import com.example.badminton_social_app.model.Match;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface MatchMapper {void insertMatch(Match match);List<Match> findByUserId(@Param("userId") Long userId);List<Match> findByMatchedUserId(@Param("matchedUserId") Long matchedUserId);Match findById(@Param("id") Long id);void updateMatchStatus(@Param("id") Long id, @Param("status") String status);
}

XML 映射文件: src/main/resources/mapper/MatchMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton_social_app.mapper.MatchMapper"><resultMap id="MatchResultMap" type="com.example.badminton_social_app.model.Match"><id property="id" column="id" /><result property="userId" column="user_id" /><result property="matchedUserId" column="matched_user_id" /><result property="status" column="status" /><result property="createdAt" column="created_at" /><result property="updatedAt" column="updated_at" /></resultMap><insert id="insertMatch" parameterType="com.example.badminton_social_app.model.Match">INSERT INTO matches (user_id, matched_user_id, status, created_at, updated_at)VALUES (#{userId}, #{matchedUserId}, #{status}, NOW(), NOW())</insert><select id="findByUserId" parameterType="Long" resultMap="MatchResultMap">SELECT * FROM matches WHERE user_id = #{userId}</select><select id="findByMatchedUserId" parameterType="Long" resultMap="MatchResultMap">SELECT * FROM matches WHERE matched_user_id = #{matchedUserId}</select><select id="findById" parameterType="Long" resultMap="MatchResultMap">SELECT * FROM matches WHERE id = #{id}</select><update id="updateMatchStatus">UPDATE matchesSET status = #{status}, updated_at = NOW()WHERE id = #{id}</update></mapper>
2.5.3 MessageMapper

接口:

package com.example.badminton_social_app.mapper;import com.example.badminton_social_app.model.Message;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface MessageMapper {void insertMessage(Message message);List<Message> findByMatchId(@Param("matchId") Long matchId);
}

XML 映射文件: src/main/resources/mapper/MessageMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton_social_app.mapper.MessageMapper"><resultMap id="MessageResultMap" type="com.example.badminton_social_app.model.Message"><id property="id" column="id" /><result property="matchId" column="match_id" /><result property="senderId" column="sender_id" /><result property="receiverId" column="receiver_id" /><result property="message" column="message" /><result property="createdAt" column="created_at" /></resultMap><insert id="insertMessage" parameterType="com.example.badminton_social_app.model.Message">INSERT INTO messages (match_id, sender_id, receiver_id, message, created_at)VALUES (#{matchId}, #{senderId}, #{receiverId}, #{message}, NOW())</insert><select id="findByMatchId" parameterType="Long" resultMap="MessageResultMap">SELECT * FROM messages WHERE match_id = #{matchId} ORDER BY created_at ASC</select></mapper>
2.5.4 RatingMapper

接口:

package com.example.badminton_social_app.mapper;import com.example.badminton_social_app.model.Rating;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface RatingMapper {void insertRating(Rating rating);List<Rating> findByRateeId(@Param("rateeId") Long rateeId);
}

XML 映射文件: src/main/resources/mapper/RatingMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton_social_app.mapper.RatingMapper"><resultMap id="RatingResultMap" type="com.example.badminton_social_app.model.Rating"><id property="id" column="id" /><result property="matchId" column="match_id" /><result property="raterId" column="rater_id" /><result property="rateeId" column="ratee_id" /><result property="rating" column="rating" /><result property="comment" column="comment" /><result property="createdAt" column="created_at" /></resultMap><insert id="insertRating" parameterType="com.example.badminton_social_app.model.Rating">INSERT INTO ratings (match_id, rater_id, ratee_id, rating, comment, created_at)VALUES (#{matchId}, #{raterId}, #{rateeId}, #{rating}, #{comment}, NOW())</insert><select id="findByRateeId" parameterType="Long" resultMap="RatingResultMap">SELECT * FROM ratings WHERE ratee_id = #{rateeId} ORDER BY created_at DESC</select></mapper>
2.5.5 CouponMapper

接口:

package com.example.badminton_social_app.mapper;import com.example.badminton_social_app.model.Coupon;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface CouponMapper {void insertCoupon(Coupon coupon);List<Coupon> findByUserId(@Param("userId") Long userId);Coupon findByCode(@Param("code") String code);void updateCoupon(Coupon coupon);
}

XML 映射文件: src/main/resources/mapper/CouponMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton_social_app.mapper.CouponMapper"><resultMap id="CouponResultMap" type="com.example.badminton_social_app.model.Coupon"><id property="id" column="id" /><result property="userId" column="user_id" /><result property="code" column="code" /><result property="type" column="type" /><result property="discountAmount" column="discount_amount" /><result property="isUsed" column="is_used" /><result property="expiresAt" column="expires_at" /><result property="createdAt" column="created_at" /><result property="updatedAt" column="updated_at" /></resultMap><insert id="insertCoupon" parameterType="com.example.badminton_social_app.model.Coupon">INSERT INTO coupons (user_id, code, type, discount_amount, is_used, expires_at, created_at, updated_at)VALUES (#{userId}, #{code}, #{type}, #{discountAmount}, #{isUsed}, #{expiresAt}, NOW(), NOW())</insert><select id="findByUserId" parameterType="Long" resultMap="CouponResultMap">SELECT * FROM coupons WHERE user_id = #{userId} AND expires_at > NOW()</select><select id="findByCode" parameterType="String" resultMap="CouponResultMap">SELECT * FROM coupons WHERE code = #{code}</select><update id="updateCoupon" parameterType="com.example.badminton_social_app.model.Coupon">UPDATE couponsSETis_used = #{isUsed},updated_at = NOW()WHERE id = #{id}</update></mapper>
2.5.6 PointMapper

接口:

package com.example.badminton_social_app.mapper;import com.example.badminton_social_app.model.Point;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.Optional;@Mapper
public interface PointMapper {void insertPoint(Point point);Point findByUserId(@Param("userId") Long userId);void updatePoint(Point point);
}

XML 映射文件: src/main/resources/mapper/PointMapper.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton_social_app.mapper.PointMapper"><resultMap id="PointResultMap" type="com.example.badminton_social_app.model.Point"><id property="id" column="id" /><result property="userId" column="user_id" /><result property="totalPoints" column="total_points" /><result property="history" column="history" /><result property="createdAt" column="created_at" /><result property="updatedAt" column="updated_at" /></resultMap><insert id="insertPoint" parameterType="com.example.badminton_social_app.model.Point">INSERT INTO points (user_id, total_points, history, created_at, updated_at)VALUES (#{userId}, #{totalPoints}, #{history}, NOW(), NOW())</insert><select id="findByUserId" parameterType="Long" resultMap="PointResultMap">SELECT * FROM points WHERE user_id = #{userId}</select><update id="updatePoint" parameterType="com.example.badminton_social_app.model.Point">UPDATE pointsSETtotal_points = #{totalPoints},history = #{history},updated_at = NOW()WHERE id = #{id}</update></mapper>

2.6 安全配置

2.6.1 JWT 工具类

创建一个用于生成和验证JWT的工具类。

package com.example.badminton_social_app.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;public String generateToken(String username) {Map<String, Object> claims = new HashMap<>();return Jwts.builder().setClaims(claims).setSubject(username).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(SignatureAlgorithm.HS512, secret).compact();}public String extractUsername(String token) {return getClaims(token).getSubject();}public boolean validateToken(String token, String username) {final String extractedUsername = extractUsername(token);return (extractedUsername.equals(username) && !isTokenExpired(token));}private Claims getClaims(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}private boolean isTokenExpired(String token) {return getClaims(token).getExpiration().before(new Date());}
}
2.6.2 UserDetailsService 实现

实现Spring Security的UserDetailsService接口,用于加载用户详情。

package com.example.badminton_social_app.service;import com.example.badminton_social_app.mapper.UserMapper;
import com.example.badminton_social_app.model.User;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.*;
import org.springframework.stereotype.Service;import java.util.ArrayList;@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.findByUsername(username);if (user == null) {throw new UsernameNotFoundException("User not found with username: " + username);}return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPasswordHash(),new ArrayList<>());}
}
2.6.3 安全配置类

package com.example.badminton.config;import com.example.badminton.security.JwtRequestFilter;
import com.example.badminton.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
public class SecurityConfig extends org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter {@Autowiredprivate CustomUserDetailsService userDetailsService;@Autowiredprivate JwtRequestFilter jwtRequestFilter;@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {// 配置用户详情服务和密码编码器auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());}@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}// 暴露AuthenticationManager为Bean,以便在Controller中使用@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}@Overrideprotected void configure(HttpSecurity http) throws Exception {http.csrf().disable().authorizeRequests().antMatchers("/api/v1/auth/**").permitAll() // 认证相关接口无需认证.anyRequest().authenticated() // 其他接口需要认证.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 不使用Session// 添加JWT过滤器http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);}
}
2.6.4 JWT 请求过滤器(继续)

继续完善 JwtRequestFilter.java 类,用于在每个请求中提取和验证JWT。

package com.example.badminton.security;import com.example.badminton.service.CustomUserDetailsService;
import com.example.badminton.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import io.jsonwebtoken.ExpiredJwtException;import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class JwtRequestFilter extends OncePerRequestFilter {@Autowiredprivate CustomUserDetailsService userDetailsService;@Autowiredprivate JwtUtil jwtUtil;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {final String authorizationHeader = request.getHeader("Authorization");String username = null;String jwt = null;// 提取JWT Tokenif (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {jwt = authorizationHeader.substring(7);try {username = jwtUtil.extractUsername(jwt);} catch (ExpiredJwtException e) {logger.warn("JWT Token已过期");} catch (Exception e) {logger.error("无法解析JWT Token");}}// 如果用户名不为空且当前安全上下文没有认证if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);// 验证Tokenif (jwtUtil.validateToken(jwt, userDetails)) {UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());usernamePasswordAuthenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));// 设置认证信息到安全上下文SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);}}chain.doFilter(request, response);}
}
2.6.5 JWT 工具类(JwtUtil.java)

用于生成和验证JWT的工具类。

package com.example.badminton.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.security.core.userdetails.UserDetails;import java.util.Date;
import java.util.HashMap;
import java.util.Map;@Component
public class JwtUtil {@Value("${jwt.secret}")private String secret;@Value("${jwt.expiration}")private Long expiration;// 生成Tokenpublic String generateToken(UserDetails userDetails) {Map<String, Object> claims = new HashMap<>();// 可以在这里添加额外的claims,如角色等return Jwts.builder().setClaims(claims).setSubject(userDetails.getUsername()).setIssuedAt(new Date(System.currentTimeMillis())).setExpiration(new Date(System.currentTimeMillis() + expiration)).signWith(SignatureAlgorithm.HS512, secret).compact();}// 从Token中提取用户名public String extractUsername(String token) {return getClaims(token).getSubject();}// 验证Tokenpublic boolean validateToken(String token, UserDetails userDetails) {final String username = extractUsername(token);return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));}// 获取所有Claimsprivate Claims getClaims(String token) {return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();}// 检查Token是否过期private boolean isTokenExpired(String token) {return getClaims(token).getExpiration().before(new Date());}
}

2.7 控制器实现

2.7.1 认证控制器(AuthController.java)

处理用户注册和登录请求。

package com.example.badminton.controller;import com.example.badminton.model.User;
import com.example.badminton.model.Point;
import com.example.badminton.payload.LoginRequest;
import com.example.badminton.payload.RegisterRequest;
import com.example.badminton.payload.JwtResponse;
import com.example.badminton.payload.ResponseMessage;
import com.example.badminton.repository.UserMapper;
import com.example.badminton.repository.PointMapper;
import com.example.badminton.util.JwtUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;import java.util.Optional;@RestController
@RequestMapping("/api/v1/auth")
public class AuthController {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate UserMapper userMapper;@Autowiredprivate PointMapper pointMapper;@Autowiredprivate PasswordEncoder passwordEncoder;@Autowiredprivate JwtUtil jwtUtil;@PostMapping("/register")public ResponseEntity<?> registerUser(@RequestBody RegisterRequest registerRequest) {// 检查用户名是否存在Optional<User> existingUser = userMapper.findByUsername(registerRequest.getUsername());if (existingUser.isPresent()) {return ResponseEntity.badRequest().body(new ResponseMessage("用户名已存在,请选择其他用户名。"));}// 检查邮箱是否存在existingUser = userMapper.findByEmail(registerRequest.getEmail());if (existingUser.isPresent()) {return ResponseEntity.badRequest().body(new ResponseMessage("邮箱已注册,请使用其他邮箱。"));}// 创建新用户User user = new User();user.setUsername(registerRequest.getUsername());user.setPasswordHash(passwordEncoder.encode(registerRequest.getPassword()));user.setEmail(registerRequest.getEmail());user.setPhone(registerRequest.getPhone());user.setSkillLevel(registerRequest.getSkillLevel());user.setInterests(String.join(",", registerRequest.getInterests())); // 将List转换为逗号分隔的字符串userMapper.insertUser(user);// 初始化积分Point point = new Point();point.setUserId(user.getId());point.setTotalPoints(0);point.setHistory(""); // 可根据需求初始化pointMapper.insertPoint(point);return ResponseEntity.ok(new ResponseMessage("注册成功。"));}@PostMapping("/login")public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest loginRequest) throws Exception {try {authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));} catch (BadCredentialsException e) {return ResponseEntity.status(401).body(new ResponseMessage("无效的用户名或密码。"));}final UserDetails userDetails = userMapper.findByUsername(loginRequest.getUsername()).map(user -> org.springframework.security.core.userdetails.User.builder().username(user.getUsername()).password(user.getPasswordHash()).roles("USER") // 根据实际情况设置角色.build()).orElseThrow(() -> new Exception("用户未找到"));final String jwt = jwtUtil.generateToken(userDetails);return ResponseEntity.ok(new JwtResponse(jwt));}
}
2.7.2 请求与响应模型

定义请求和响应的Payload类。

package com.example.badminton.payload;import lombok.Data;import java.util.List;// 用户注册请求
@Data
public class RegisterRequest {private String username;private String password;private String email;private String phone;private SkillLevel skillLevel;private List<String> interests;
}// 用户登录请求
@Data
public class LoginRequest {private String username;private String password;
}// JWT响应
@Data
public class JwtResponse {private String jwt;public JwtResponse(String jwt) {this.jwt = jwt;}
}// 通用响应消息
@Data
public class ResponseMessage {private String message;public ResponseMessage(String message) {this.message = message;}
}// 打球水平枚举
public enum SkillLevel {初学者,进阶玩家,随便挥拍
}
2.7.3 Mapper接口与XML配置

使用MyBatis的Mapper接口和XML配置进行数据库操作。

2.7.3.1 用户Mapper(UserMapper.java)
package com.example.badminton.repository;import com.example.badminton.model.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.Optional;@Mapper
public interface UserMapper {void insertUser(User user);Optional<User> findByUsername(@Param("username") String username);Optional<User> findByEmail(@Param("email") String email);// 其他用户相关的数据库操作
}
2.7.3.2 用户Mapper XML(UserMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton.repository.UserMapper"><insert id="insertUser" parameterType="com.example.badminton.model.User" useGeneratedKeys="true" keyProperty="id">INSERT INTO users (username, password_hash, email, phone, skill_level, interests, created_at, updated_at)VALUES (#{username}, #{passwordHash}, #{email}, #{phone}, #{skillLevel}, #{interests}, NOW(), NOW())</insert><select id="findByUsername" parameterType="string" resultType="com.example.badminton.model.User">SELECT * FROM users WHERE username = #{username}</select><select id="findByEmail" parameterType="string" resultType="com.example.badminton.model.User">SELECT * FROM users WHERE email = #{email}</select><!-- 其他SQL语句 --></mapper>
2.7.3.3 积分Mapper(PointMapper.java)
package com.example.badminton.repository;import com.example.badminton.model.Point;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.Optional;@Mapper
public interface PointMapper {void insertPoint(Point point);Optional<Point> findByUserId(@Param("userId") Long userId);// 其他积分相关的数据库操作
}
2.7.3.4 积分Mapper XML(PointMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton.repository.PointMapper"><insert id="insertPoint" parameterType="com.example.badminton.model.Point" useGeneratedKeys="true" keyProperty="id">INSERT INTO points (user_id, total_points, history, created_at, updated_at)VALUES (#{userId}, #{totalPoints}, #{history}, NOW(), NOW())</insert><select id="findByUserId" parameterType="long" resultType="com.example.badminton.model.Point">SELECT * FROM points WHERE user_id = #{userId}</select><!-- 其他SQL语句 --></mapper>
2.7.3.5 匹配Mapper(MatchMapper.java)
package com.example.badminton.repository;import com.example.badminton.model.Match;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;
import java.util.Optional;@Mapper
public interface MatchMapper {void insertMatch(Match match);Optional<Match> findById(@Param("id") Long id);List<Match> findByUserId(@Param("userId") Long userId);List<Match> findByMatchedUserId(@Param("matchedUserId") Long matchedUserId);// 其他匹配相关的数据库操作
}
2.7.3.6 匹配Mapper XML(MatchMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton.repository.MatchMapper"><insert id="insertMatch" parameterType="com.example.badminton.model.Match" useGeneratedKeys="true" keyProperty="id">INSERT INTO matches (user_id, matched_user_id, status, created_at, updated_at)VALUES (#{userId}, #{matchedUserId}, #{status}, NOW(), NOW())</insert><select id="findById" parameterType="long" resultType="com.example.badminton.model.Match">SELECT * FROM matches WHERE id = #{id}</select><select id="findByUserId" parameterType="long" resultType="com.example.badminton.model.Match">SELECT * FROM matches WHERE user_id = #{userId}</select><select id="findByMatchedUserId" parameterType="long" resultType="com.example.badminton.model.Match">SELECT * FROM matches WHERE matched_user_id = #{matchedUserId}</select><!-- 其他SQL语句 --></mapper>
2.7.3.7 消息Mapper(MessageMapper.java)
package com.example.badminton.repository;import com.example.badminton.model.Message;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface MessageMapper {void insertMessage(Message message);List<Message> findByMatchId(@Param("matchId") Long matchId);// 其他消息相关的数据库操作
}
2.7.3.8 消息Mapper XML(MessageMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton.repository.MessageMapper"><insert id="insertMessage" parameterType="com.example.badminton.model.Message" useGeneratedKeys="true" keyProperty="id">INSERT INTO messages (match_id, sender_id, receiver_id, message, created_at)VALUES (#{matchId}, #{senderId}, #{receiverId}, #{message}, NOW())</insert><select id="findByMatchId" parameterType="long" resultType="com.example.badminton.model.Message">SELECT * FROM messages WHERE match_id = #{matchId} ORDER BY created_at ASC</select><!-- 其他SQL语句 --></mapper>
2.7.3.9 评分Mapper(RatingMapper.java)
package com.example.badminton.repository;import com.example.badminton.model.Rating;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;import java.util.List;@Mapper
public interface RatingMapper {void insertRating(Rating rating);List<Rating> findByRateeId(@Param("rateeId") Long rateeId);// 其他评分相关的数据库操作
}
2.7.3.10 评分Mapper XML(RatingMapper.xml)
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.example.badminton.repository.RatingMapper"><insert id="insertRating" parameterType="com.example.badminton.model.Rating" useGeneratedKeys="true" keyProperty="id">INSERT INTO ratings (match_id, rater_id, ratee_id, rating, comment, created_at)VALUES (#{matchId}, #{raterId}, #{rateeId}, #{rating}, #{comment}, NOW())</insert><select id="findByRateeId" parameterType="long" resultType="com.example.badminton.model.Rating">SELECT * FROM ratings WHERE ratee_id = #{rateeId} ORDER BY created_at DESC</select><!-- 其他SQL语句 --></mapper>

2.8 服务层实现

2.8.1 用户服务(UserService.java)
package com.example.badminton.service;import com.example.badminton.model.User;
import com.example.badminton.repository.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;import java.util.Optional;@Service
public class UserService {@Autowiredprivate UserMapper userMapper;public Optional<User> findByUsername(String username) {return userMapper.findByUsername(username);}public Optional<User> findByEmail(String email) {return userMapper.findByEmail(email);}public void saveUser(User user) {userMapper.insertUser(user);}// 其他用户相关的服务方法
}
2.8.2 用户详情服务(CustomUserDetailsService.java)

实现 UserDetailsService 接口,用于加载用户详情。

package com.example.badminton.service;import com.example.badminton.model.User;
import com.example.badminton.repository.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
import org.springframework.security.core.userdetails.User.UserBuilder;
import org.springframework.security.core.userdetails.User;@Service
public class CustomUserDetailsService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {Optional<com.example.badminton.model.User> userOpt = userMapper.findByUsername(username);if (!userOpt.isPresent()) {throw new UsernameNotFoundException("User not found with username: " + username);}com.example.badminton.model.User user = userOpt.get();// 构建Spring Security的User对象UserBuilder builder = User.withUsername(username);builder.password(user.getPasswordHash());builder.roles("USER"); // 根据实际情况设置角色return builder.build();}
}

2.9 登录流程

2.9.1 前端登录流程概述
  1. 用户在登录页面填写用户名和密码并提交。
  2. 前端发送 POST /api/v1/auth/login 请求,携带登录信息。
  3. 后端接收请求,验证用户凭证。
  4. 后端生成JWT Token并返回给前端。
  5. 前端存储Token(如在本地存储中),并在后续请求中携带Token。
  6. 用户登录成功后,前端跳转到首页或用户主页。
2.9.2 前端实现

以下示例基于 UniAppVuex 进行实现。

2.9.2.1 Vuex 认证模块(store/modules/auth.js)
// store/modules/auth.jsconst state = {token: uni.getStorageSync('token') || '',user: {}
}const mutations = {SET_TOKEN(state, token) {state.token = tokenuni.setStorageSync('token', token)},SET_USER(state, user) {state.user = user},CLEAR_AUTH(state) {state.token = ''state.user = {}uni.removeStorageSync('token')}
}const actions = {login({ commit }, payload) {return new Promise((resolve, reject) => {uni.request({url: 'https://yourapi.com/api/v1/auth/login',method: 'POST',data: payload,success: (res) => {if (res.statusCode === 200) {commit('SET_TOKEN', res.data.jwt)resolve(res)} else {reject(res.data.message)}},fail: (err) => {reject(err)}})})},register({ commit }, payload) {return new Promise((resolve, reject) => {uni.request({url: 'https://yourapi.com/api/v1/auth/register',method: 'POST',data: payload,success: (res) => {if (res.statusCode === 200) {resolve(res)} else {reject(res.data.message)}},fail: (err) => {reject(err)}})})},logout({ commit }) {commit('CLEAR_AUTH')}
}export default {namespaced: true,state,mutations,actions
}
2.9.2.2 登录页面(pages/login/login.vue)
<template><view class="container"><van-field v-model="username" label="用户名" placeholder="请输入用户名" /><van-field v-model="password" type="password" label="密码" placeholder="请输入密码" /><van-button type="primary" @click="login">登录</van-button><van-button type="default" @click="navigateToRegister">注册</van-button></view></template><script>
import { mapActions } from 'vuex'
export default {data() {return {username: '',password: ''}},methods: {...mapActions('auth', ['login']),async login() {if (this.username.trim() === '' || this.password.trim() === '') {uni.showToast({ title: '请输入用户名和密码', icon: 'none' })return}try {const response = await this.login({ username: this.username, password: this.password })uni.showToast({ title: '登录成功', icon: 'success' })// 获取用户信息后跳转到首页this.$router.push('/pages/home/home')} catch (error) {uni.showToast({ title: error, icon: 'none' })}},navigateToRegister() {this.$router.push('/pages/register/register')}}
}
</script><style scoped>
.container {padding: 20px;display: flex;flex-direction: column;gap: 20px;
}
</style>

2.10 Mapper接口与XML配置

2.10.1 Mapper接口

以下是MatchMapperMessageMapperRatingMapper的示例。已在前面的 2.7.3.52.7.3.72.7.3.9 部分定义。

2.10.2 XML配置

对应的XML配置也已在 2.7.3.52.7.3.72.7.3.9 部分定义。


2.11 前端接收消息

2.11.1 WebSocket 客户端配置

在前端,使用 SockJSStomp.js 实现WebSocket连接,并通过 Vuex 更新聊天消息。

2.11.1.1 安装依赖

确保已安装 SockJSStomp.js

npm install sockjs-client stompjs
2.11.1.2 WebSocket 配置(main.js)

main.js 中配置WebSocket连接,并将Stomp客户端添加到Vue实例中,以便全局访问。

// main.js
import Vue from 'vue'
import App from './App'
import store from './store'
import router from './router'
import Vant from 'vant';
import 'vant/lib/index.css';
import SockJS from 'sockjs-client'
import Stomp from 'stompjs'Vue.use(Vant);Vue.config.productionTip = falseApp.mpType = 'app'const app = new Vue({store,router,...App
})
app.$mount()// WebSocket 连接
const socket = new SockJS('https://yourapi.com/ws') // 确保使用HTTPS
const stompClient = Stomp.over(socket)
stompClient.debug = null // 关闭调试信息stompClient.connect({}, frame => {console.log('Connected: ' + frame)// 订阅个人消息队列stompClient.subscribe(`/user/queue/messages`, message => {const receivedMessage = JSON.parse(message.body)// 触发Vuex中的聊天消息更新store.commit('chat/ADD_MESSAGE', receivedMessage)})
})// 将stompClient添加到Vue原型中,便于在组件中访问
Vue.prototype.$stompClient = stompClient
2.11.2 Vuex 聊天模块(store/modules/chat.js)
// store/modules/chat.jsconst state = {messages: []
}const mutations = {ADD_MESSAGE(state, message) {state.messages.push(message)},CLEAR_MESSAGES(state) {state.messages = []}
}const actions = {// 根据需要添加异步操作
}export default {namespaced: true,state,mutations,actions
}
2.11.3 聊天页面实现(pages/chat/chat.vue)
<template><view class="container"><van-nav-bar title="聊天" left-text="返回" @click-left="goBack" /><scroll-view class="chat-container" scroll-y :scroll-top="scrollTop"><view v-for="message in messages" :key="message.id" :class="{'sent': message.senderId === user.id, 'received': message.senderId !== user.id}"><text>{{ message.message }}</text><text class="timestamp">{{ formatTimestamp(message.createdAt) }}</text></view></scroll-view><view class="input-container"><van-field v-model="newMessage" placeholder="输入消息" /><van-button type="primary" @click="sendMessage">发送</van-button></view></view></template><script>
import { mapState, mapMutations } from 'vuex'export default {data() {return {matchId: null,newMessage: '',scrollTop: 0}},computed: {...mapState('chat', ['messages']),...mapState('auth', ['user'])},methods: {...mapMutations('chat', ['ADD_MESSAGE']),goBack() {uni.navigateBack()},formatTimestamp(timestamp) {const date = new Date(timestamp)const hours = date.getHours().toString().padStart(2, '0')const minutes = date.getMinutes().toString().padStart(2, '0')return `${hours}:${minutes}`},sendMessage() {if (this.newMessage.trim() === '') return// 发送消息到后端uni.request({url: 'https://yourapi.com/api/v1/messages',method: 'POST',header: {'Authorization': `Bearer ${this.$store.state.auth.token}`},data: {matchId: this.matchId,message: this.newMessage},success: res => {if (res.statusCode === 200) {this.newMessage = ''// 本地添加消息(可选)// this.ADD_MESSAGE({//     id: res.data.id,//     senderId: this.user.id,//     receiverId: res.data.receiverId,//     message: res.data.message,//     createdAt: res.data.createdAt// })// 更新滚动位置this.scrollTop = this.messages.length * 100} else {uni.showToast({ title: res.data.message, icon: 'none' })}},fail: err => {uni.showToast({ title: '发送失败', icon: 'none' })}})}},onLoad(options) {this.matchId = options.matchId// 加载聊天记录uni.request({url: `https://yourapi.com/api/v1/messages/${this.matchId}`,method: 'GET',header: {'Authorization': `Bearer ${this.$store.state.auth.token}`},success: res => {if (res.statusCode === 200) {res.data.forEach(msg => {this.ADD_MESSAGE(msg)})// 更新滚动位置this.scrollTop = this.messages.length * 100} else {uni.showToast({ title: '加载聊天记录失败', icon: 'none' })}},fail: err => {uni.showToast({ title: '加载聊天记录失败', icon: 'none' })}})}
}
</script><style scoped>
.container {display: flex;flex-direction: column;height: 100%;
}.chat-container {flex: 1;padding: 10px;overflow-y: scroll;
}.sent {align-self: flex-end;background-color: #DCF8C6;padding: 10px;border-radius: 10px;margin-bottom: 5px;max-width: 70%;
}.received {align-self: flex-start;background-color: #FFFFFF;padding: 10px;border-radius: 10px;margin-bottom: 5px;max-width: 70%;
}.timestamp {font-size: 10px;color: #999;margin-top: 5px;display: block;
}.input-container {display: flex;padding: 10px;border-top: 1px solid #eee;align-items: center;gap: 10px;
}
</style>

2.12 WebSocket 实现

2.12.1 后端WebSocket配置

配置Spring Boot的WebSocket,以支持实时聊天功能。

2.12.1.1 WebSocketConfig.java
package com.example.badminton.config;import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.*;@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {@Overridepublic void configureMessageBroker(MessageBrokerRegistry config) {// 配置消息代理,支持点对点消息config.enableSimpleBroker("/queue"); // 订阅前缀config.setApplicationDestinationPrefixes("/app"); // 应用前缀}@Overridepublic void registerStompEndpoints(StompEndpointRegistry registry) {// 注册WebSocket端点registry.addEndpoint("/ws").setAllowedOrigins("*") // 根据需求设置允许的域.withSockJS(); // 启用SockJS}
}
2.12.2 后端消息处理器

处理通过WebSocket发送的聊天消息。

2.12.2.1 ChatWebSocketController.java
package com.example.badminton.controller;import com.example.badminton.model.Message;
import com.example.badminton.model.Match;
import com.example.badminton.model.User;
import com.example.badminton.payload.ChatMessage;
import com.example.badminton.repository.MessageMapper;
import com.example.badminton.repository.MatchMapper;
import com.example.badminton.repository.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.SendToUser;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;import java.security.Principal;@Controller
public class ChatWebSocketController {@Autowiredprivate SimpMessagingTemplate messagingTemplate;@Autowiredprivate MessageMapper messageMapper;@Autowiredprivate MatchMapper matchMapper;@Autowiredprivate UserMapper userMapper;@MessageMapping("/chat.sendMessage") // 接收/app/chat.sendMessage的消息public void sendMessage(@Payload ChatMessage chatMessage, Principal principal) {// 获取发送者信息User sender = userMapper.findByUsername(principal.getName()).orElseThrow(() -> new RuntimeException("用户未找到"));// 获取匹配记录Match match = matchMapper.findById(chatMessage.getMatchId()).orElseThrow(() -> new RuntimeException("匹配记录未找到"));// 确定接收者User receiver = match.getMatchedUserId().equals(sender.getId()) ? userMapper.findById(match.getUserId()).orElseThrow(() -> new RuntimeException("接收者未找到")) :userMapper.findById(match.getMatchedUserId()).orElseThrow(() -> new RuntimeException("接收者未找到"));// 创建消息记录Message message = new Message();message.setMatchId(match.getId());message.setSenderId(sender.getId());message.setReceiverId(receiver.getId());message.setMessage(chatMessage.getContent());message.setCreatedAt(new java.sql.Timestamp(System.currentTimeMillis()));messageMapper.insertMessage(message);// 推送消息给接收者messagingTemplate.convertAndSendToUser(receiver.getUsername(), "/queue/messages", message);}
}
2.12.3 聊天消息模型(ChatMessage.java)
package com.example.badminton.payload;import lombok.Data;@Data
public class ChatMessage {private Long matchId;private String content;
}
2.12.4 前端WebSocket集成
2.12.4.1 前端WebSocket连接(main.js)

在前面的 2.11.1.2 WebSocket 配置 中,已通过 SockJSStomp.js 建立了WebSocket连接,并订阅了用户的消息队列。确保将 /user/queue/messages 与后端配置一致。

2.12.4.2 发送消息

聊天页面 中,通过 Uni.request 发送HTTP请求来发送消息,同时通过WebSocket接收消息。

<!-- 已在2.11.3 聊天页面实现中展示 -->
2.12.4.3 接收消息并更新Vuex

main.js 中,已通过 stompClient.subscribe 订阅了消息队列,并通过 store.commit('chat/ADD_MESSAGE', receivedMessage) 更新Vuex中的聊天消息。


2.13 完整的登录流程

2.13.1 前端登录流程
  1. 用户在登录页面填写用户名和密码并提交。
  2. 前端发送 POST /api/v1/auth/login 请求,携带登录信息。
  3. 后端验证用户凭证,生成JWT Token并返回给前端。
  4. 前端接收JWT Token,存储在本地(如Vuex和本地存储)。
  5. 用户登录成功后,前端跳转到首页或用户主页。
2.13.2 后端登录流程
  1. 接收 POST /api/v1/auth/login 请求,包含用户名和密码。
  2. 通过Spring Security的 AuthenticationManager 认证用户凭证。
  3. 如果认证成功,使用 JwtUtil 生成JWT Token。
  4. 将JWT Token作为响应返回给前端。
示例:AuthController.java 中的登录方法(继续)
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest loginRequest) {try {authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));} catch (BadCredentialsException e) {return ResponseEntity.status(401).body(new ResponseMessage("无效的用户名或密码。"));}final User user = userMapper.findByUsername(loginRequest.getUsername()).orElseThrow(() -> new RuntimeException("用户未找到"));final UserDetails userDetails = org.springframework.security.core.userdetails.User.builder().username(user.getUsername()).password(user.getPasswordHash()).roles("USER") // 根据实际情况设置角色.build();final String jwt = jwtUtil.generateToken(userDetails);return ResponseEntity.ok(new JwtResponse(jwt));
}

3. 前端开发

3.1 技术栈

  • 框架:Vue.js
  • 跨平台:UniApp
  • 状态管理:Vuex
  • UI框架:Vant Weapp 或 uView UI
  • 实时通信:SockJS, Stomp.js
  • 构建工具:HBuilderX

3.2 项目初始化

使用HBuilderX创建一个UniApp项目,并安装所需的依赖。

2.9.2.1 Vuex 认证模块(store/modules/auth.js)

已在 2.9.2.1 部分定义。

3.3 页面与组件实现

3.3.1 注册页面(pages/register/register.vue)
<template><view class="container"><van-field v-model="username" label="用户名" placeholder="请输入用户名" /><van-field v-model="password" type="password" label="密码" placeholder="请输入密码" /><van-field v-model="confirmPassword" type="password" label="确认密码" placeholder="请确认密码" /><van-field v-model="email" label="邮箱" placeholder="请输入邮箱" /><van-field v-model="phone" label="电话" placeholder="请输入电话" /><van-picker v-model="skillLevel" :columns="skillLevels" title="选择打球水平" /><van-checkbox-group v-model="interests"><van-checkbox name="喜欢双打">喜欢双打</van-checkbox><van-checkbox name="热爱竞技">热爱竞技</van-checkbox><!-- 添加更多兴趣标签 --></van-checkbox-group><van-button type="primary" @click="register">注册</van-button><van-button type="default" @click="navigateToLogin">已有账户?登录</van-button></view></template><script>
import { mapActions } from 'vuex'
export default {data() {return {username: '',password: '',confirmPassword: '',email: '',phone: '',skillLevel: '',skillLevels: ['初学者', '进阶玩家', '随便挥拍'],interests: []}},methods: {...mapActions('auth', ['register']),async register() {if (this.password !== this.confirmPassword) {uni.showToast({ title: '密码不一致', icon: 'none' })return}if (this.username.trim() === '' || this.password.trim() === '' || this.email.trim() === '') {uni.showToast({ title: '请填写必填项', icon: 'none' })return}try {await this.register({username: this.username,password: this.password,email: this.email,phone: this.phone,skillLevel: this.skillLevel,interests: this.interests})uni.showToast({ title: '注册成功', icon: 'success' })this.$router.push('/pages/login/login')} catch (error) {uni.showToast({ title: error, icon: 'none' })}},navigateToLogin() {this.$router.push('/pages/login/login')}}
}
</script><style scoped>
.container {padding: 20px;display: flex;flex-direction: column;gap: 20px;
}
</style>
3.3.2 首页(pages/home/home.vue)
<template><view class="container"><van-nav-bar title="首页" right-text="注销" @click-right="logout" /><van-button type="primary" @click="navigateToMatch">开始匹配</van-button><van-list><van-cell v-for="match in matchHistory" :key="match.id" :title="match.matchedUser.username" :label="match.status" @click="viewMatchDetails(match.id)" /></van-list></view></template><script>
import { mapState, mapActions } from 'vuex'export default {data() {return {matchHistory: []}},computed: {...mapState('auth', ['user'])},methods: {...mapActions('matches', ['fetchMatchHistory']),...mapActions('auth', ['logout']),async fetchHistory() {try {const history = await this.fetchMatchHistory(this.user.id)this.matchHistory = history} catch (error) {uni.showToast({ title: '获取匹配历史失败', icon: 'none' })}},navigateToMatch() {this.$router.push('/pages/match/match')},viewMatchDetails(matchId) {this.$router.push(`/pages/matchDetails/matchDetails?matchId=${matchId}`)},async logout() {await this.logout()uni.showToast({ title: '已注销', icon: 'success' })this.$router.push('/pages/login/login')}},onLoad() {this.fetchHistory()}
}
</script><style scoped>
.container {padding: 20px;display: flex;flex-direction: column;gap: 20px;height: 100%;
}
</style>
3.3.3 匹配页面(pages/match/match.vue)
<template><view class="container"><van-nav-bar title="匹配" left-text="返回" @click-left="goBack" /><van-form @submit="submitMatch"><van-picker v-model="region" :columns="regions" title="选择区域" /><van-picker v-model="skillLevel" :columns="skillLevels" title="选择打球水平" /><van-datetime-picker v-model="selectedTime" type="datetime" title="选择时间" /><van-button type="primary" form-type="submit">查找匹配</van-button></van-form><van-list><van-cell v-for="user in matchedUsers" :key="user.id" :title="user.username" :label="user.skillLevel" @click="acceptMatch(user.id)" /></van-list></view></template><script>
import { mapActions, mapState } from 'vuex'export default {data() {return {region: '',regions: ['区域A', '区域B', '区域C'],skillLevel: '',skillLevels: ['初学者', '进阶玩家', '随便挥拍'],selectedTime: '',matchedUsers: []}},computed: {...mapState('auth', ['user'])},methods: {...mapActions('matches', ['findMatches', 'createMatch']),goBack() {uni.navigateBack()},async submitMatch() {if (this.region === '' || this.skillLevel === '') {uni.showToast({ title: '请填写匹配条件', icon: 'none' })return}try {const response = await this.findMatches({region: this.region,skillLevel: this.skillLevel,interests: this.user.interests})this.matchedUsers = response} catch (error) {uni.showToast({ title: '匹配失败', icon: 'none' })}},async acceptMatch(userId) {try {await this.createMatch({ matchedUserId: userId })uni.showToast({ title: '匹配请求已发送', icon: 'success' })} catch (error) {uni.showToast({ title: '发送匹配请求失败', icon: 'none' })}}}
}
</script><style scoped>
.container {padding: 20px;display: flex;flex-direction: column;gap: 20px;height: 100%;
}
</style>
3.3.4 匹配详情页面(pages/matchDetails/matchDetails.vue)
<template><view class="container"><van-nav-bar title="匹配详情" left-text="返回" @click-left="goBack" /><view class="details"><van-cell title="对方用户名" :value="matchedUser.username" /><van-cell title="打球水平" :value="matchedUser.skillLevel" /><van-cell title="兴趣标签" :value="matchedUser.interests" /></view><van-button type="primary" @click="navigateToChat">开始聊天</van-button><van-button type="danger" @click="rejectMatch">拒绝匹配</van-button></view></template><script>
import { mapActions, mapState } from 'vuex'export default {data() {return {matchId: null,matchedUser: {}}},computed: {...mapState('auth', ['user'])},methods: {...mapActions('matches', ['fetchMatchDetails', 'updateMatchStatus']),goBack() {uni.navigateBack()},async fetchDetails() {try {const details = await this.fetchMatchDetails(this.matchId)this.matchedUser = details.matchedUser} catch (error) {uni.showToast({ title: '获取匹配详情失败', icon: 'none' })}},navigateToChat() {this.$router.push(`/pages/chat/chat?matchId=${this.matchId}`)},async rejectMatch() {try {await this.updateMatchStatus({ matchId: this.matchId, status: '已拒绝' })uni.showToast({ title: '匹配已拒绝', icon: 'success' })this.goBack()} catch (error) {uni.showToast({ title: '拒绝匹配失败', icon: 'none' })}}},onLoad(options) {this.matchId = options.matchIdthis.fetchDetails()}
}
</script><style scoped>
.container {padding: 20px;display: flex;flex-direction: column;gap: 20px;height: 100%;
}
.details {display: flex;flex-direction: column;gap: 10px;
}
</style>
3.3.5 Vuex 匹配模块(store/modules/matches.js)
// store/modules/matches.jsconst state = {matchHistory: [],matchDetails: {}
}const mutations = {SET_MATCH_HISTORY(state, history) {state.matchHistory = history},SET_MATCH_DETAILS(state, details) {state.matchDetails = details},UPDATE_MATCH_STATUS(state, { matchId, status }) {const match = state.matchHistory.find(m => m.id === matchId)if (match) {match.status = status}}
}const actions = {fetchMatchHistory({ commit }, userId) {return new Promise((resolve, reject) => {uni.request({url: `https://yourapi.com/api/v1/matches/user/${userId}`,method: 'GET',header: {'Authorization': `Bearer ${uni.getStorageSync('token')}`},success: (res) => {if (res.statusCode === 200) {commit('SET_MATCH_HISTORY', res.data)resolve(res.data)} else {reject(res.data.message)}},fail: (err) => {reject(err)}})})},findMatches({ commit }, payload) {return new Promise((resolve, reject) => {uni.request({url: `https://yourapi.com/api/v1/matches`,method: 'GET',data: payload,header: {'Authorization': `Bearer ${uni.getStorageSync('token')}`},success: (res) => {if (res.statusCode === 200) {resolve(res.data)} else {reject(res.data.message)}},fail: (err) => {reject(err)}})})},createMatch({ commit }, payload) {return new Promise((resolve, reject) => {uni.request({url: `https://yourapi.com/api/v1/matches`,method: 'POST',data: payload,header: {'Authorization': `Bearer ${uni.getStorageSync('token')}`},success: (res) => {if (res.statusCode === 200) {resolve(res.data)} else {reject(res.data.message)}},fail: (err) => {reject(err)}})})},fetchMatchDetails({ commit }, matchId) {return new Promise((resolve, reject) => {uni.request({url: `https://yourapi.com/api/v1/matches/${matchId}`,method: 'GET',header: {'Authorization': `Bearer ${uni.getStorageSync('token')}`},success: (res) => {if (res.statusCode === 200) {commit('SET_MATCH_DETAILS', res.data)resolve(res.data)} else {reject(res.data.message)}},fail: (err) => {reject(err)}})})},updateMatchStatus({ commit }, payload) {return new Promise((resolve, reject) => {uni.request({url: `https://yourapi.com/api/v1/matches/${payload.matchId}/status`,method: 'PUT',data: { status: payload.status },header: {'Authorization': `Bearer ${uni.getStorageSync('token')}`},success: (res) => {if (res.statusCode === 200) {commit('UPDATE_MATCH_STATUS', payload)resolve(res.data)} else {reject(res.data.message)}},fail: (err) => {reject(err)}})})}
}export default {namespaced: true,state,mutations,actions
}

4. 部署与上线

4.1 服务器配置

4.1.1 选择服务器

选择云服务提供商,如阿里云、腾讯云或AWS,配置适当的服务器规格(如CPU、内存、存储)。

4.1.2 安装必要的软件
  • Java Runtime Environment (JRE): 运行Spring Boot应用
  • Nginx: 作为反向代理和静态资源服务器
  • MySQL: 数据库管理
  • Node.js: 编译前端项目(如需要)
4.1.3 配置MyBatis SQL Session Factory

application.properties 中添加MyBatis配置:

# MyBatis配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.badminton.model

确保所有Mapper XML文件放在 src/main/resources/mapper 目录下。

4.1.4 部署Spring Boot应用

打包Spring Boot应用为可执行的JAR文件,并配置为后台服务运行。

mvn clean package

上传生成的 badminton-social-app.jar 到服务器,并通过以下命令启动:

nohup java -jar badminton-social-app.jar > app.log 2>&1 &
4.1.5 配置Nginx

配置Nginx作为前端静态资源服务器和后端API的反向代理。

nginx.conf 示例
server {listen 80;server_name yourdomain.com;# 静态资源location / {root /var/www/html; # 前端H5文件路径try_files $uri $uri/ /index.html;}# 后端APIlocation /api/ {proxy_pass http://localhost:8080/api/;proxy_set_header Host $host;proxy_set_header X-Real-IP $remote_addr;proxy_set_header Authorization $http_authorization;}# WebSocketlocation /ws/ {proxy_pass http://localhost:8080/ws/;proxy_http_version 1.1;proxy_set_header Upgrade $http_upgrade;proxy_set_header Connection "Upgrade";proxy_set_header Host $host;}
}

重启Nginx以应用配置:

sudo systemctl restart nginx
4.1.6 配置数据库

在服务器上安装并配置MySQL,创建数据库并导入表结构。

CREATE DATABASE badminton_social;USE badminton_social;-- 创建用户表
CREATE TABLE users (id BIGINT AUTO_INCREMENT PRIMARY KEY,username VARCHAR(50) NOT NULL UNIQUE,password_hash VARCHAR(255) NOT NULL,email VARCHAR(100) NOT NULL UNIQUE,phone VARCHAR(20),skill_level ENUM('初学者', '进阶玩家', '随便挥拍'),interests VARCHAR(255),created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);-- 创建匹配表
CREATE TABLE matches (id BIGINT AUTO_INCREMENT PRIMARY KEY,user_id BIGINT NOT NULL,matched_user_id BIGINT NOT NULL,status ENUM('待接受', '已接受', '已拒绝') DEFAULT '待接受',created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,FOREIGN KEY (user_id) REFERENCES users(id),FOREIGN KEY (matched_user_id) REFERENCES users(id)
);-- 创建消息表
CREATE TABLE messages (id BIGINT AUTO_INCREMENT PRIMARY KEY,match_id BIGINT NOT NULL,sender_id BIGINT NOT NULL,receiver_id BIGINT NOT NULL,message TEXT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (match_id) REFERENCES matches(id),FOREIGN KEY (sender_id) REFERENCES users(id),FOREIGN KEY (receiver_id) REFERENCES users(id)
);-- 创建评分表
CREATE TABLE ratings (id BIGINT AUTO_INCREMENT PRIMARY KEY,match_id BIGINT NOT NULL,rater_id BIGINT NOT NULL,ratee_id BIGINT NOT NULL,rating INT NOT NULL,comment TEXT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,FOREIGN KEY (match_id) REFERENCES matches(id),FOREIGN KEY (rater_id) REFERENCES users(id),FOREIGN KEY (ratee_id) REFERENCES users(id)
);-- 创建优惠券表
CREATE TABLE coupons (id BIGINT AUTO_INCREMENT PRIMARY KEY,user_id BIGINT NOT NULL,code VARCHAR(50) NOT NULL UNIQUE,type ENUM('折扣', '免费场次') NOT NULL,discount_amount DECIMAL(10,2) NOT NULL,is_used BOOLEAN DEFAULT FALSE,expires_at TIMESTAMP,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,FOREIGN KEY (user_id) REFERENCES users(id)
);-- 创建积分表
CREATE TABLE points (id BIGINT AUTO_INCREMENT PRIMARY KEY,user_id BIGINT NOT NULL UNIQUE,total_points INT DEFAULT 0,history TEXT,created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,FOREIGN KEY (user_id) REFERENCES users(id)
);

4.2 前端部署

4.2.1 编译前端项目

使用HBuilderX或CLI工具编译UniApp项目为H5。

# 使用HBuilderX GUI进行编译,或使用CLI命令
# 示例(假设使用HBuilderX命令行)
hbuilderx build:h5
4.2.2 上传前端文件到服务器

将编译后的H5文件上传到Nginx配置的静态资源路径(如 /var/www/html)。

scp -r dist/build/h5/* user@yourserver:/var/www/html/

4.3 后端部署

4.3.1 部署Spring Boot应用

确保JAR文件已上传到服务器,并通过后台服务运行。

nohup java -jar badminton-social-app.jar > app.log 2>&1 &
4.3.2 配置数据库

在服务器上安装并配置MySQL,创建数据库并导入表结构。

CREATE DATABASE badminton_social;USE badminton_social;-- 导入表结构
-- 使用前面定义的SQL语句
4.3.3 配置环境变量

确保Spring Boot应用的 application.properties 文件中包含正确的数据库和JWT配置。

# 数据库配置
spring.datasource.url=jdbc:mysql://localhost:3306/badminton_social?useSSL=false&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=your_password# MyBatis配置
mybatis.mapper-locations=classpath:mapper/*.xml
mybatis.type-aliases-package=com.example.badminton.model# JWT 配置
jwt.secret=YourJWTSecretKey
jwt.expiration=86400000 # 1天的毫秒数# WebSocket 配置
spring.websocket.path=/ws

4.4 测试与上线

4.4.1 系统整体测试

在生产环境中进行全面的功能测试,确保所有API和前端功能正常工作。

4.4.2 稳定性测试

使用压力测试工具(如JMeter)测试系统在高并发下的表现,确保稳定性。

4.4.3 安全性测试

检查系统是否存在常见的安全漏洞,如SQL注入、XSS、CSRF等,确保数据传输和存储的安全性。

4.4.4 客户培训与交付
  • 提供详细的用户手册和技术文档。
  • 组织培训会议,向客户演示系统功能和使用方法。
  • 完成项目验收,确保客户满意。

5. 示例代码与技术细节

5.1 前端 API 调用示例

使用Vue的生命周期钩子和方法进行API调用。

获取匹配列表
methods: {async fetchMatches() {try {const response = await this.$store.dispatch('matches/findMatches', {region: this.region,skillLevel: this.skillLevel,interests: this.interests})this.matchedUsers = response} catch (error) {uni.showToast({ title: '获取匹配失败', icon: 'none' })}}
}

5.2 后端 API 实现示例

用户注册 API
@PostMapping("/register")
public ResponseEntity<?> registerUser(@RequestBody RegisterRequest registerRequest) {// 检查用户名和邮箱是否存在if (userMapper.findByUsername(registerRequest.getUsername()).isPresent()) {return ResponseEntity.badRequest().body(new ResponseMessage("用户名已存在,请选择其他用户名。"));}if (userMapper.findByEmail(registerRequest.getEmail()).isPresent()) {return ResponseEntity.badRequest().body(new ResponseMessage("邮箱已注册,请使用其他邮箱。"));}// 创建新用户User user = new User();user.setUsername(registerRequest.getUsername());user.setPasswordHash(passwordEncoder.encode(registerRequest.getPassword()));user.setEmail(registerRequest.getEmail());user.setPhone(registerRequest.getPhone());user.setSkillLevel(registerRequest.getSkillLevel());user.setInterests(String.join(",", registerRequest.getInterests()));userMapper.insertUser(user);// 初始化积分Point point = new Point();point.setUserId(user.getId());point.setTotalPoints(0);point.setHistory(""); // 根据需求初始化pointMapper.insertPoint(point);return ResponseEntity.ok(new ResponseMessage("注册成功。"));
}
用户登录 API
@PostMapping("/login")
public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginRequest loginRequest) {try {authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));} catch (BadCredentialsException e) {return ResponseEntity.status(401).body(new ResponseMessage("无效的用户名或密码。"));}final User user = userMapper.findByUsername(loginRequest.getUsername()).orElseThrow(() -> new RuntimeException("用户未找到"));final UserDetails userDetails = org.springframework.security.core.userdetails.User.builder().username(user.getUsername()).password(user.getPasswordHash()).roles("USER") // 根据实际情况设置角色.build();final String jwt = jwtUtil.generateToken(userDetails);return ResponseEntity.ok(new JwtResponse(jwt));
}

5.3 WebSocket 实现示例

后端消息发送
@Autowired
private SimpMessagingTemplate messagingTemplate;@MessageMapping("/chat.sendMessage")
public void sendMessage(@Payload ChatMessage chatMessage, Principal principal) {// 保存消息到数据库User sender = userMapper.findByUsername(principal.getName()).orElseThrow(() -> new UsernameNotFoundException("用户未找到"));Match match = matchMapper.findById(chatMessage.getMatchId()).orElseThrow(() -> new ResourceNotFoundException("匹配记录未找到"));User receiver = match.getMatchedUserId().equals(sender.getId()) ? userMapper.findById(match.getUserId()).orElseThrow(() -> new RuntimeException("接收者未找到")) :userMapper.findById(match.getMatchedUserId()).orElseThrow(() -> new RuntimeException("接收者未找到"));Message message = new Message();message.setMatchId(match.getId());message.setSenderId(sender.getId());message.setReceiverId(receiver.getId());message.setMessage(chatMessage.getContent());message.setCreatedAt(new java.sql.Timestamp(System.currentTimeMillis()));messageMapper.insertMessage(message);// 通过WebSocket推送消息给接收者messagingTemplate.convertAndSendToUser(receiver.getUsername(), "/queue/messages", message);
}
前端接收消息并更新Vuex

main.js 中,通过WebSocket订阅接收消息,并通过Vuex更新聊天消息。

// main.jsimport Vue from 'vue'
import App from './App'
import store from './store'
import router from './router'
import Vant from 'vant';
import 'vant/lib/index.css';
import SockJS from 'sockjs-client'
import Stomp from 'stompjs'Vue.use(Vant);Vue.config.productionTip = falseApp.mpType = 'app'const app = new Vue({store,router,...App
})
app.$mount()// WebSocket 连接
const socket = new SockJS('https://yourapi.com/ws') // 确保使用HTTPS
const stompClient = Stomp.over(socket)
stompClient.debug = null // 关闭调试信息stompClient.connect({}, frame => {console.log('Connected: ' + frame)// 订阅个人消息队列stompClient.subscribe(`/user/queue/messages`, message => {const receivedMessage = JSON.parse(message.body)// 触发Vuex中的聊天消息更新store.commit('chat/ADD_MESSAGE', receivedMessage)})
})// 将stompClient添加到Vue原型中,便于在组件中访问
Vue.prototype.$stompClient = stompClient

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/889824.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

初始Python篇(6)—— 字符串

找往期文章包括但不限于本期文章中不懂的知识点&#xff1a; 个人主页&#xff1a;我要学编程(ಥ_ಥ)-CSDN博客 所属专栏&#xff1a; Python 目录 字符串的常见操作 格式化字符串 占位符 f-string 字符串的 format 方法 字符串的编码与解码 与数据验证相关的方法 …

从 CephFS 到 JuiceFS:同程旅游亿级文件存储平台构建之路

随着公司业务的快速发展&#xff0c;同程旅行的非结构化的数据突破 10 亿&#xff0c;在 2022 年&#xff0c;同程首先完成了对象存储服务的建设。当时&#xff0c;分布式文件系统方面&#xff0c;同程使用的是 CephFS&#xff0c;随着数据量的持续增长&#xff0c;CephFS 的高…

Jenkins参数化构建详解(This project is parameterized)

本文详细介绍了Jenkins中不同类型的参数化构建方法&#xff0c;包括字符串、选项、多行文本、布尔值和git分支参数的配置&#xff0c;以及如何使用ActiveChoiceParameter实现动态获取参数选项。通过示例展示了传统方法和声明式pipeline的语法 文章目录 1. Jenkins的参数化构建1…

【图像处理】利用numpy实现直方图均衡、自适应直方图均衡、对比度受限自适应直方图均衡

直方图均衡化是一种在图像处理技术&#xff0c;通过调整图像的直方图来增强图像的对比度。 本博客不利用opencv库&#xff0c;仅利用numpy、matplotlib来实现直方图均衡、自适应直方图均衡、对比度受限自适应直方图均衡 直方图均衡 包括四个流程 计算图像RGB三通道的归一化直…

组织空转数据(人类+小鼠)

空间转录组&#xff08;Spatial Transcriptomics&#xff09;是一种新兴的高通量基因组学技术&#xff0c;它允许我们在组织切片中同时获取基因表达信息和细胞的空间位置信息。其可以帮助我们更好地理解细胞在组织中的空间分布和相互作用&#xff0c;揭示组织发育、器官功能和疾…

[数据结构#1] 并查集 | FindRoot | Union | 优化 | 应用

目录 1. 并查集原理 问题背景 名称与编号映射 数据结构设计 2. 并查集基本操作 (1) 初始化 (2) 查询根节点 (FindRoot) (3) 合并集合 (Union) (4) 集合操作总结 并查集优化 (1) 路径压缩 (2) 按秩合并 3. 并查集的应用 (1) 统计省份数量 (2) 判断等式方程是否成…

JPA 基本查询(一)

JPA 查询简介示例 JPA教程 - JPA查询简介示例 最简单的JPQL查询选择单个实体类型的所有实例。 考虑下面的查询: SELECT e FROM Employee eJPQL尽可能使用SQL语法。 SQL查询从表中选择。JPQL从应用程序域模型的实体中选择。 语法 选择查询的整体形式如下: SELECT <sel…

【操作系统1】一篇文章便可入门操作系统

操作系统 (Operating System,OS)是一种系统软件&#xff0c;它负责管理计算机的硬件和软件资源。它的主要任务是组织和调度计算机的工作&#xff0c;并分配资源给用户和其他软件。操作系统为用户和软件提供了方便的接口和环境。它是计算机系统中最基本的软件之一。 一、操作系…

μC/OS-Ⅱ源码学习(6)---事件标志组

快速回顾 μC/OS-Ⅱ中的多任务 μC/OS-Ⅱ源码学习(1)---多任务系统的实现 μC/OS-Ⅱ源码学习(2)---多任务系统的实现(下) μC/OS-Ⅱ源码学习(3)---事件模型 μC/OS-Ⅱ源码学习(4)---信号量 μC/OS-Ⅱ源码学习(5)---消息队列 本文进一步解析事件模型中&#xff0c;事件标志…

【经验分享】OpenHarmony5.0.0-release编译RK3568不过问题(已解决)

问题描述 根据操作手册正常拉取代码&#xff0c;然后编译OpenHarmony5.0.0版本rk3568项目 编译命令 ./build.sh --product-name rk3568 --ccache出现如下报错 然后真正开始出错的位置是下面这句log FAILED: ../kernel/src_tmp/linux-5.10/boot_linux ../kernel/checkpoint/c…

C++重点和练习-----多态

rpg.cpp: #include <iostream>using namespace std;/*模拟一个游戏场景有一个英雄&#xff1a;初始所有属性为1atk,def,apd,hp游戏当中有以下3种武器长剑Sword&#xff1a; 装备该武器获得 1atx&#xff0c;1def短剑Blade&#xff1a; 装备该武器获得 1atk&#xff0c;1…

Qt之点击鼠标右键创建菜单栏使用(六)

Qt开发 系列文章 - menu&#xff08;六&#xff09; 目录 前言 一、示例演示 二、菜单栏 1.MenuBar 2.Menu 总结 前言 QMainWindow是一个为用户提供主窗口程序的类&#xff0c;包含一个菜单栏&#xff08;menubar&#xff09;、多个工具栏(toolbars)、一个状态栏(status…

天猫魔盒M17/M17S_超级UI 线刷固件包-可救砖(刷机取消双勾)

在智能电视盒子的领域中&#xff0c;天猫魔盒 M17 以其独特魅力占据一席之地&#xff0c;然而&#xff0c;原厂设置有时难以满足进阶用户的多元需求。此刻&#xff0c;刷机成为开启全新体验的关键钥匙&#xff0c;为您的盒子注入鲜活能量。 一、卓越固件特性概览 此款精心打造的…

Elasticsearch 7.x入门学习-Spring Data Elasticsearch框架

1 Spring Data框架 Spring Data 是一个用于简化数据库、非关系型数据库、索引库访问&#xff0c;并支持云服务的开源框架。其主要目标是使得对数据的访问变得方便快捷&#xff0c;并支持 map-reduce 框架和云计算数据服务。 Spring Data 可以极大的简化 JPA的写法&#xff0c;…

【落羽的落羽 C语言篇】一些常见的字符函数、字符串函数、内存函数

文章目录 一、字符函数1. 字符分类函数2. 字符转换函数 二、字符串函数1. strlen的使用和模拟实现使用模拟实现 2. strcpy的使用和模拟实现使用模拟实现 3. strcat的使用和模拟实现使用模拟实现 4. strcmp的使用和模拟实现使用模拟实现 5. strncpy的使用6. strncat的使用7. str…

JAVA:访问者模式(Visitor Pattern)的技术指南

1、简述 访问者模式(Visitor Pattern)是一种行为型设计模式,允许你将操作分离到不同的对象中,而无需修改对象本身的结构。这种模式特别适合复杂对象结构中对其元素进行操作的场景。 本文将介绍访问者模式的核心概念、优缺点,并通过详细代码示例展示如何在实际应用中实现…

小米自研系统Vela全面开源:开启物联网新时代的技术革新之旅

目录 Vela系统的技术特点 1. 高性能与低功耗的完美平衡 2. 高度可扩展性与模块化设计 3. 强大的安全机制 4. 跨平台兼容性 Vela系统的应用场景 1. 智能家居领域 2. 工业物联网领域 3. 医疗健康领域 4. 智慧城市领域 Vela系统的深远影响 1. 推动物联…

Linux/CentOS编译TensorFlow

很多时候为了方便图省事&#xff0c;是通过pip安装TensorFlow的&#xff0c;然而很不幸运行的服务器不支持AVX指令&#xff0c;引入模块的时候会报错&#xff1a; The TensorFlow library was compiled to use AVX instructions, but these aren’t available on your machine.…

2021陇剑杯——流量分析

JWT简介 JWT&#xff08;JSON Web Token&#xff09;是一种开放标准&#xff08;RFC 7519&#xff09;&#xff0c;用于在网络应用环境中以一种紧凑的、URL安全的方式传递声明&#xff08;Claims&#xff09;。JWT通常用于身份验证、信息交换以及验证消息的完整性。JWT通过在不…

visual studio 2022 c++使用教程

介绍 c开发windows一般都是visual studio&#xff0c;linux一般是vscode&#xff0c;但vscode调试c不方便&#xff0c;所以很多情况都是2套代码&#xff0c;在windows上用vs开发方便&#xff0c;在转到linux。 安装 1、官网下载vs2022企业版–选择桌面开发–安装位置–安装–…