文章目录
- 1. 项目设计
- 2. 效果图展示
- 3. 创建项目以及配置文件
- 3.1 创建项目
- 3.2 配置文件
- 3.2.1 在 application.properties 中添加配置文件
- 3.2.2 在 resources 目录下创建mapper
- 4. 数据库设计与实现
- 5. 登录注册模块
- 5.1 设计登录注册交互接口
- 5.2 设置登录注册功能返回的响应类
- 5.3 使用 BCrypt 对密码进行加密
- 5.4 完成 MyBatis 操作
- 5.5 后端的实现
- 5.5.1 登录功能后端实现
- 5.5.2 注册功能后端实现
- 5.5.3 注销功能
- 5.6 前端的实现
- 5.6.1 登录前端实现
- 5.6.2 注册前端实现
- 5.7 添加拦截器
- 6. 大厅界面
- 6.1 交互接口设计
- 6.2 用户加载前后交互接口
- 6.3 前端和后端实现用户信息加载
- 6.3.1 后端的实现
- 6.3.2 前端的实现
- 6.4 实现匹配功能的前端代码
- 6.5 实现匹配功能的后端代码
- 6.5.1 创建在线状态
- 6.5.2 创建房间对象
- 6.5.3 创建房间管理器
- 6.5.4 创建匹配队列
- 6.5.5 写完 MatchController
- 6.6 大厅界面总结
- 7. 房间界面
- 7.1 交互接口设计
- 7.2 实现房间界面前端代码
- 7.2.1 设置棋盘界面, 以及显示框.
- 7.2.2 对应的js文件
- 7.2.3 初始化 websocket
- 7.2.4 落子时,发送落子请求
- 7.2.5 落子时, 发送落子响应
- 7.3 实现房间界面后端代码
- 7.3.1 注册GameController
- 7.3.2 创建GameController
- 7.3.3 创建对应的响应类和请求类
- 7.3.4 完成用户房间在线状态管理
- 7.3.5 添加 MyBatis 用来更新玩家积分
- 7.3.6 完成处理连接方法
- 7.3.7 完成处理连接断开的方法和连接异常的方法
- 7.3.8 在房间管理器中添加代码
- 7.3.9 Room类添加棋盘代码
- 7.3.10 实现handleTextMessage方法
- 7.3.11 实现putChess方法
- 7.3.12 完成用户胜负判断
1. 项目设计
前端 : HTML + CSS + JavaScript + Jquery + AJAX
后端 : Spring MVC + Spring Boot + MyBatis
2. 效果图展示
3. 创建项目以及配置文件
3.1 创建项目
3.2 配置文件
3.2.1 在 application.properties 中添加配置文件
spring.datasource.url=jdbc:mysql://localhost:3306/onlineGobang?characterEncoding=utf8&useSSL=true
spring.datasource.username=root
spring.datasource.password=0000
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis.mapper-locations=classpath:mapper/**Mapper.xml
3.2.2 在 resources 目录下创建mapper
mapper下添加 目录 **.xml 并添加代码
</mapper>
4. 数据库设计与实现
这里使用数据库存储每一个用户的信息, 初始的时候, 天梯分和场次都是默认的.
create database if not exists onlineGobang;
use onlineGobang;
drop table if exists user;
create table user(
userId int primary key auto_increment,
username varchar(20) unique,
password varchar(255) not null,
score int,
totalCount int,
winCount int
);
5. 登录注册模块
5.1 设计登录注册交互接口
登录功能
请求
POST /user/login HTTP/1.1
{username: "",password: ""}
响应
{
status: 1/-1,
message: "",
data: ""
}
注销功能
请求
GET /user/logout HTTP/1.1
响应
HTTP/1.1 200
注册功能
请求
POST /user/register HTTP/1.1
{username: "",password: ""}
响应
{
status: 1/-1,
message: "",
data: ""
}
5.2 设置登录注册功能返回的响应类
通过这个类, 方便前端接收内容
@Data
public class ResponseBodyMessage<T> {
private int status;
private String message;
private T data;
public ResponseBodyMessage(int status, String message, T data) {
this.status = status;
this.message = message;
this.data = data;
}
}
5.3 使用 BCrypt 对密码进行加密
在 pom.xml中添加依赖
<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})
在 cofig 包下, 创建一个类 AppConfig.
@Configuration
public class AppConfig implements WebMvcConfigurer {
@Bean
public BCryptPasswordEncoder getBCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
5.4 完成 MyBatis 操作
在 model 包中, 创建 User 实体类
@Data
public class User {
private int userId;
private String username;
private String password;
private int score;
private int totalCount;
private int winCount;
}
在 mapper 包中, 创建 UserMapper 接口
这个接口中 主要是完成
- 注册, 插入一个用户
- 登录的时候, 通过名字查询当前用户是否存在.
@Mapper
public interface UserMapper {
// 注册一个用户, 初始的天梯积分默认为1000, 场次默认为0
int insert(User user);
// 通过username查询当前用户是否存在
User selectByName(String username);
}
在 resources 目录下, 创建一个目录 mapper, 在目录下创建 UserMapper.xml
在 UserMapper.xml 中写对应UserMapper接口中对应的操作
<mapper namespace="com.example.gobang.mapper.UserMapper">
<insert id="insert">
insert into user values(null,#{username},#{password},1000,0,0)
</insert>
<select id="selectByName" resultType="com.example.gobang.model.User">
select * from user where username = #{username}
</select>
</mapper>
创建 service 包, 在包下创建 UserService 类, 这个类调用 Mapper接口中的方法
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
public int insert(User user){
return userMapper.insert(user);
}
public User selectByName(String username){
return userMapper.selectByName(username);
}
}
5.5 后端的实现
创建 controller包, 在包下创建一个 UserController 类
这个类是实现登录模块的功能的
- 这里需要注入 UserService, 调用数据库中的方法
- 还需要注入 BCryptPasswordEncoder, 对密码进行加密和比较
5.5.1 登录功能后端实现
注意这里的登录.
- 首先去数据库根据用户名查询是否存在当前用户.
- 如果不存在, 登录失败.
- 如果存在, 用输入的密码, 和数据库中的密码进行比较, 看是否相等. (注: 数据中的密码是加密的)
- 如果不相等, 登录失败.
- 如果相等, 创建 session, 并登录成功.
@RequestMapping("/login")
public ResponseBodyMessage<User> login(@RequestBody User user, HttpServletRequest request) {
User truUser = userService.selectByName(user.getUsername());
if (truUser == null) {
System.out.println("登录失败!");
return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);
}else {
boolean flg = bCryptPasswordEncoder.matches(user.getPassword(),truUser.getPassword());
if (!flg) {
return new ResponseBodyMessage<>(-1,"用户名密码错误!",user);
}
System.out.println("登录成功!");
HttpSession session = request.getSession(true);
session.setAttribute(Constant.USER_SESSION_KEY,truUser);
return new ResponseBodyMessage<>(1,"登录成功!",truUser);
}
}
5.5.2 注册功能后端实现
- 首先查看是否该用户是否存在
- 存在, 就注册失败
- 不存在, 就进行注册, 首先对当前密码进行加密.
- 加密之后对这个用户添加到数据库中.
@RequestMapping("/register")
public ResponseBodyMessage<User> register(@RequestBody User user) {
User truUser = userService.selectByName(user.getUsername());
if (truUser != null) {
return new ResponseBodyMessage<>(-1,"当前用户名已经存在!",user);
} else{
String password = bCryptPasswordEncoder.encode(user.getPassword());
user.setPassword(password);
userService.insert(user);
return new ResponseBodyMessage<>(1,"注册成功!",user);
}
}
5.5.3 注销功能
直接删除对应session 为
Constant.USER_SESSION_KEY
, 然后跳转到login.html
@RequestMapping("/logout")
public void userLogout(HttpServletRequest request, HttpServletResponse response) throws IOException, IOException {
HttpSession session = request.getSession(false);
// 拦截器的拦截, 所以不可能出现session为空的情况
session.removeAttribute(Constant.USER_SESSION_KEY);
response.sendRedirect("login.html");
}
注意: 这里的Constant.USER_SESSION_KEY
是存储的 session 字符串, 由于该 字符串是不变的, 所以存入 Constant 类中.
5.6 前端的实现
5.6.1 登录前端实现
let loginButton = document.querySelector('#loginButton');
loginButton.onclick = function() {
let username = document.querySelector('#loginUsername');
let password = document.querySelector('#loginPassword');
if (username.value.trim() == ""){
alert('请先输入用户名!');
username.focus();
return;
}
if (password.value.trim() == ""){
alert('请先输入密码!');
password.focus();
return;
}
$.ajax({
url: "user/login",
method: "POST",
data: JSON.stringify({username: username.value.trim(), password: password.value.trim()}),
contentType: "application/json;charset=utf-8",
success: function(data, status) {
if(data.status == 1) {
location.assign("index.html");
}else{
alert(data.message);
username.value="";
password.value="";
username.focus();
}
}
})
}
5.6.2 注册前端实现
let Reg = document.querySelector('#Reg');
Reg.onclick = function() {
let username = document.querySelector('#RegUsername');
let password1 = document.querySelector('#RegPassword1');
let password2 = document.querySelector('#RegPassword2');
if(!$('#checkbox').is(':checked')) {
alert("请勾选条款");
return;
}
if(username.value.trim() == ""){
alert("请先输入用户名!");
username.focus();
return;
}
if(password1.value.trim() == ""){
alert('请先输入密码!');
password1.focus();
return;
}
if(password2.value.trim() == ""){
alert('请再次输入密码!');
password2.focus();
return;
}
if(username.value.trim().length > 20) {
alert("用户名长度过长");
username.value="";
username.focus();
return;
}
if(password1.value.trim() != password2.value.trim()) {
alert('两次输入的密码不同!');
passwrod1.value="";
password2.value="";
return;
}
if(password1.value.trim().length > 255) {
alert("当前密码长度过长!");
password1.value="";
password2.value="";
password1.focus();
return;
}
$.ajax({
url: "user/register",
method: "POST",
data: JSON.stringify({username: username.value.trim(), password: password1.value.trim()}),
contentType: "application/json;charset=utf-8",
success: function(data,status){
if(data.status == 1) {
alert(data.message);
location.assign("login.html");
}else{
alert(data.message);
username.value="";
password1.value="";
password2.value="";
username.focus();
}
}
})
}
5.7 添加拦截器
LoginIntercepter
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
@Configuration
@EnableWebSocket
public class AppConfig implements WebSocketConfigurer,WebMvcConfigurer{
@Override
public void addInterceptors(InterceptorRegistry registry) {
LoginInterceptor loginInterceptor = new LoginInterceptor();
registry.addInterceptor(loginInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/**/login.html")
.excludePathPatterns("/**/css/**.css")
.excludePathPatterns("/**/images/**")
.excludePathPatterns("/**/fonts/**")
.excludePathPatterns("/**/js/**.js")
.excludePathPatterns("/**/scss/**")
.excludePathPatterns("/**/user/login")
.excludePathPatterns("/**/user/register")
.excludePathPatterns("/**/user/logout");
}
}
6. 大厅界面
6.1 交互接口设计
这里客户端1, 点击匹配发送消息给服务器, 客户端2, 也点击匹配发送消息给服务器, 当服务器收到两个人的请求之后, 就需要服务器主动向客户端发送消息, 这里就需要用到 websocket
URL: ws://127.0.0.1:8080/findMatch
匹配请求
{
message: ' startMatch ' / ' stopMatch'
}
匹配响应1 这个响应是点击匹配之后, 立刻返回的响应
{
status: '1' / '-1'
message: ' startMatch ' / ' stopMatch '
}
匹配响应2 这个响应是匹配成功之后的响应
{
status: '1' / '-1'
message: 'matchSuccess'
}
6.2 用户加载前后交互接口
请求
GET /user/userInfo HTTP/1.1
响应
{
status: 1/-1 (1 为成功, -1 为失败),
message: "对应信息",
data: "内容", (用户信息)
}
6.3 前端和后端实现用户信息加载
6.3.1 后端的实现
根据当前存储的session对象, 来查找对应的用户
@RequestMapping("/userInfo")
public ResponseBodyMessage<User> getUserInfo(HttpServletRequest request) {
HttpSession session = request.getSession(false);
User user = (User) session.getAttribute(Constant.USER_SESSION_KEY);
if (user == null) {
return new ResponseBodyMessage<>(-1,"当前用户不存在",null);
}else{
return new ResponseBodyMessage<>(1,"查找成功!", newUser);
}
}
6.3.2 前端的实现
load()