基于SpringBoot+MyBatis 五子棋双人对战

Posted 粉色的志明

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了基于SpringBoot+MyBatis 五子棋双人对战相关的知识,希望对你有一定的参考价值。

1. 核心功能

技术:
前端: html + CSS + javascript + AJAX
后端: SpringBoot + MyBatis + WebSocket + mysql 5.7

2. 演示效果


3. 创建项目




4. 数据库设计

create database if not exists java_gobang;

use java_gobang;

drop table if exists user;
create table user (
    userId int primary key auto_increment,
    username varchar(50) unique,
    password varchar(255),
    score int,        -- 天梯积分
    totalCount int,   -- 比赛总场数
    winCount int      -- 获胜场数
);

insert into user values(null,"cm","$2a$10$Bs4wNEkledVlGZa6wSfX7eCSD7wRMO0eUwkJH0WyhXzKQJrnk85li",1000,0,0);

5. 配置文件

application.yml

debug: true
logging:
    level:
        com:
            example: DEBUG
            example.onlinemusic.mapper: debug
        druid:
            sql:
                Statement: DEBUG
        root: INFO
spring:
    datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        password: root
        url: jdbc:mysql://localhost:3306/java_gobang?characterEncoding=utf8&serverTimezone=UTC
        username: root

mybatis:
    mapper-locations: classpath:mybatis/**Mapper.xml
server:
    port: 8081

mybatis.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.mapper.UserMapper">

   
</mapper>

6. 用户模块

6.1 登录实现

6.1.1 前后端交互接口

每一次我们设计都需要先设计前后端交互接口

请求
POST /login HTTP/1.1
username: "",password: ""

响应
HTTP/1.1 200 OK
Content-Type: application/json


    userId: 1,
    username: 'cm',
    score: 1000,
    totalCount: 0,
    winCount: 0
    

6.1.2 model 层

创建 User 类

@Data
public class User 
    private int userId;
    private String username;
    private String password;
    private int score;
    private int totalCount;
    private int winCount;


6.1.3 mapper 层

@Mapper 注解不要忘了

@Mapper
public interface UserMapper 

    //往数据里插入一个用户,用于注册功能
    int insert(User user);

    //根据用户名,来查询用户的详细信息,用于登录功能
    User selectByName(String username);

    // 总比赛场数 + 1, 获胜场数 + 1, 天梯分数 + 30
    int userWin(int userId);

    // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30
    int userLose(int userId);



6.1.4 xml 层

resources 底下创建 mybatis 包,在创建UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.java_gobang.mapper.UserMapper">

    <!-- 新增用户 -->
    <insert id="insert">
        insert into user values (null,#username,#password,1000,0,0);
    </insert>


    <!-- 根据用户名查找用户用户 -->
    <select id="selectByName" resultType="com.example.java_gobang.model.User">
        select * from user where username=#username;
    </select>

    <update id="userWin">
        update user set totalCount = totalCount + 1, winCount = winCount + 1, score = score + 30
        where userId = #userId
    </update>

    <update id="userLose">
        update user set totalCount = totalCount + 1, score = score - 30
        where userId = #userId
    </update>

</mapper>

6.1.5 service 层

调用 mapper 层的方法

@Service
public class UserService 

    @Resource
    private UserMapper userMapper;

    //往数据里插入一个用户,用于注册功能
    public int insert(User user)
        return userMapper.insert(user);
    

    //根据用户名,来查询用户的详细信息,用于登录功能
    public User selectByName(String username)
        return userMapper.selectByName(username);
    

    // 总比赛场数 + 1, 获胜场数 + 1, 天梯分数 + 30
    public int userWin(int userId)
        return userMapper.userWin(userId);
    

    // 总比赛场数 + 1, 获胜场数 不变, 天梯分数 - 30
    public int userLose(int userId)
        return userMapper.userLose(userId);
    

6.1.6 controller 层


用来存储 session 字符串

@RestController
public class UserController 

    @Autowired
    private UserService userService;

    @Resource
    private BCryptPasswordEncoder bCryptPasswordEncoder;


    @RequestMapping("/login")
    @ResponseBody
    public Object login(String username, String password, HttpServletRequest request)

        // 查询用户是否在数据库中存在
        User user = userService.selectByName(username);

        // 没有查到
        if(user == null) 
            System.out.println("登录失败!");
            return new User();
        else 

            //查到了,但密码不一样
            if(!bCryptPasswordEncoder.matches(password,user.getPassword())) 
                return new User();
            
            // 匹配成功,创建 session
            request.getSession().setAttribute(Constant.USER_SESSION_KEY,user);
            return user;
        
    

6.1.7 使用 BCrypt 进行密码加密

<!-- security依赖包 (加密)-->
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.security</groupId>
			<artifactId>spring-security-config</artifactId>
		</dependency>

启动类添加注解

@SpringBootApplication(exclude = org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration.class)

6.1.8 添加拦截器

创建 config 包

LoginInterceptor 类

public class LoginInterceptor implements HandlerInterceptor 
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        HttpSession session = request.getSession(false);
        if (session != null && session.getAttribute(Constant.USER_SESSION_KEY) != null)
            return true;
        
        response.sendRedirect("/login.html");
        return false;
    

AppConfig 类

@Override
    public void addInterceptors(InterceptorRegistry registry) 
        LoginInterceptor loginInterceptor = new LoginInterceptor();
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns("/**/login.html")
                .excludePathPatterns("/**/register.html")
                .excludePathPatterns("/**/css/**.css")
                .excludePathPatterns("/**/images/**")
                .excludePathPatterns("/**/js/**.js")
                .excludePathPatterns("/**/login")
                .excludePathPatterns("/**/register")
                .excludePathPatterns("/**/logout");
    

6.1.9 测试

6.2 注册实现

6.2.1 前后端交互接口

请求
POST /register HTTP/1.1
Content-Type: application/x-www-form-urlencoded

username=cm&password=123456

响应
HTTP/1.1 200 OK
Content-Type: application/json


    userId: 1,
    username: 'dingding',
    score: 1000,
    totalCount: 0,
    winCount: 0
    

注册失败(比如说用户名已经存在了),就返回 username 为 null 的对象

6.2.2 controller 层

@RequestMapping("/register")
    @ResponseBody
    public Object register(String username,String password)

        User user1 = userService.selectByName(username);
        if(user1 != null)
            System.out.println("当前用户已存在");
            return new User();
        else
            User user2 = new User();
            user2.setUsername(username);
            String password1 = bCryptPasswordEncoder.encode(password);
            user2.setPassword(password1);
            userService.insert(user2);
            return user2;
        
    

6.2.3 测试

再注册相同的就不行了

6.3. 获取用户信息

6.3.1 前后端交互接口

请求
GET /userinfo HTTP/1.1

响应
HTTP/1.1 200 OK
Content-Type: application/json


    userId: 1,
    username: 'cm',
    score: 1000,
    totalCount: 0,
    winCount: 0
    

6.3.2 controller 层

@RequestMapping("/userinfo")
    @ResponseBody
    public Object getUserInfo(HttpServletRequest request)
            try
                HttpSession session = request.getSession(false);
                User user = (User)session.getAttribute("user");
                User newUser = userService.selectByName(user.getUsername());
                return newUser;
            catch (NullPointerException e)
                System.out.println("没有该用户");
                return new User();
            

    

6.3.3 测试

因为我们之前用这个账户登录的已经保存了这个 JSESSIONID

6.4 退出登录

6.4.1 前后端交互接口

请求
GET /logout HTTP/1.1

响应
HTTP/1.1 200

6.4.2 controller 层

@RequestMapping("/logout")
public void logout(HttpServletRequest request, HttpServletResponse response) throws IOException 
    HttpSession session = request.getSession(false);
    // 拦截器的拦截, 所以不可能出现session为空的情况
    session.removeAttribute(Constant.USER_SESSION_KEY);
    response.sendRedirect("login.html");



6.4.3 测试

重定向了我的登录页面

7. 匹配模块

客户端主动向服务器发起请求,返回一个响应,如果客户端不主动发起请求,服务器不能主动联系客户端,在这里,我们需要服务器主动给客户端发消息,就要用到"消息推送"

这里就要约定前后端交互接口了,也都是基于 websocket 来展开的,他可以传输文本数据,也能传输二进制数据,这里就设计成传输 json 格式的文本数据

7.1 前后端交互接口

连接(URL)

ws://127.0.0.1:8080/findMatch

匹配请求


    message: 'startMatch' / 'stopMatch', // 开始/结束匹配

这里匹配是登录之后,已经拿到了用户信息,保存到了 HttpSession 中了

匹配响应1(这一个响应是发送请求后,服务器立即返回的匹配响应)


    ok: true,                // 是否成功. 比如用户 id 不存在, 则返回 false
    reason: '',                // 错误原因
    message: 'startMatch' / 'stopMatch'

匹配响应2(匹配到了对手,服务器主动推送回消息,匹配到的对手不需要在响应中体现,仍然都放到了服务器这边)


    ok: true,                // 是否成功. 比如用户 id 不存在, 则返回 false
    reason: '',                // 错误原因
    message: 'matchSuccess',    

7.2 匹配功能前端开发

// 此处进行初始化 websocket, 并且实现前端的匹配逻辑.
    // 此处的路径必须写作 /findMatch, 千万不要写作 /findMatch/
    let websocketUrl = 'ws://' + location.host + '/findMatch';
    let websocket = new WebSocket(websocketUrl);
    websocket.onopen = function() 
        console.log("onopen");
    
    websocket.onclose = function() 
        console.log("onclose");
    
    websocket.onerror = function() 
        console.log("onerror");
    
    // 监听页面关闭事件. 在页面关闭之前, 手动调用这里的 websocket 的 close 方法.
    window.onbeforeunload = function() 
        websocket.close();
    

    // 一会重点来实现, 要处理服务器返回的响应
    websocket.onmessage = function(e) 
        // 处理服务器返回的响应数据. 这个响应就是针对 "开始匹配" / "结束匹配" 来对应的
        // 解析得到的响应对象. 返回的数据是一个 JSON 字符串, 解析成 js 对象
        let resp = JSON.parse(e.data);
        let matchButton = document.querySelector('#match-button');
        if (!resp.ok) 
            console.log("游戏大厅中接收到了失败响应! " + resp.reason);
            return;
        
        if (resp.message == 'startMatch') 
            // 开始匹配请求发送成功
            console.log("进入匹配队列成功!");
            matchButton.innerHTML = '匹配中...(点击停止)'
         else if (resp.message == 'stopMatch') 
            // 结束匹配请求发送成功
            console.log("离开匹配队列成功!");
            matchButton.innerHTML = '开始匹配';
         else if (resp.message == 'matchSuccess') 
            // 已经匹配到对手了.
            console.log("匹配到对手! 进入游戏房间!");
            // location.assign("/game_room.html");
            location.replace("/game_room.html");
         else if (resp.message == 'repeatConnection') 
            alert("当前检测到多开! 请使用其他账号登录!");
            location.replace("/login.html");
         else 以上是关于基于SpringBoot+MyBatis 五子棋双人对战的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot Mybatis双数据源的配置

Springboot+mybatis双数据源(Druid和jdbc)

Springboot+mybatis双数据源(Druid和jdbc)

基于SpringBoot的完成mybatis整合

SpringBoot入门之基于注解的Mybatis

手把手教你C语言五子棋的实现(双玩家对战)