初始提交,实现WebSocket聊天室基本功能

This commit is contained in:
2024-04-14 12:11:56 +08:00
parent 7a4faa4e2e
commit 532e23938a
34 changed files with 2427 additions and 24 deletions
+1
View File
@@ -1,3 +1,4 @@
.idea/
target/ target/
pom.xml.tag pom.xml.tag
pom.xml.releaseBackup pom.xml.releaseBackup
+7 -24
View File
@@ -4,34 +4,17 @@
你画我猜 SpringBoot Websocket 你画我猜 SpringBoot Websocket
#### 软件架构 #### 软件架构
软件架构说明 - 后端:Java21、Maven、SpringBoot
- 前端:HTML + jQuery
- 通讯:HTTP + Websocket
#### 安装教程 #### 安装教程
1. xxxx 1. 修改前端`index.html`中的后端地址
2. xxxx 2. java -jar xxx.jar
3. xxxx 3. 访问`http://IP:8080`进行游玩
#### 使用说明 #### 使用说明
1. xxxx 1. 题库在`resources/topic.json`中,可自行添加。
2. xxxx
3. xxxx
#### 参与贡献
1. Fork 本仓库
2. 新建 Feat_xxx 分支
3. 提交代码
4. 新建 Pull Request
#### 特技
1. 使用 Readme\_XXX.md 来支持不同的语言,例如 Readme\_en.md, Readme\_zh.md
2. Gitee 官方博客 [blog.gitee.com](https://blog.gitee.com)
3. 你可以 [https://gitee.com/explore](https://gitee.com/explore) 这个地址来了解 Gitee 上的优秀开源项目
4. [GVP](https://gitee.com/gvp) 全称是 Gitee 最有价值开源项目,是综合评定出的优秀开源项目
5. Gitee 官方提供的使用手册 [https://gitee.com/help](https://gitee.com/help)
6. Gitee 封面人物是一档用来展示 Gitee 会员风采的栏目 [https://gitee.com/gitee-stars/](https://gitee.com/gitee-stars/)
Vendored Executable
+308
View File
@@ -0,0 +1,308 @@
#!/bin/sh
# ----------------------------------------------------------------------------
# Licensed to the Apache Software Foundation (ASF) under one
# or more contributor license agreements. See the NOTICE file
# distributed with this work for additional information
# regarding copyright ownership. The ASF licenses this file
# to you under the Apache License, Version 2.0 (the
# "License"); you may not use this file except in compliance
# with the License. You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing,
# software distributed under the License is distributed on an
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
# KIND, either express or implied. See the License for the
# specific language governing permissions and limitations
# under the License.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.2.0
#
# Required ENV vars:
# ------------------
# JAVA_HOME - location of a JDK home dir
#
# Optional ENV vars
# -----------------
# MAVEN_OPTS - parameters passed to the Java VM when running Maven
# e.g. to debug Maven itself, use
# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
# MAVEN_SKIP_RC - flag to disable loading of mavenrc files
# ----------------------------------------------------------------------------
if [ -z "$MAVEN_SKIP_RC" ] ; then
if [ -f /usr/local/etc/mavenrc ] ; then
. /usr/local/etc/mavenrc
fi
if [ -f /etc/mavenrc ] ; then
. /etc/mavenrc
fi
if [ -f "$HOME/.mavenrc" ] ; then
. "$HOME/.mavenrc"
fi
fi
# OS specific support. $var _must_ be set to either true or false.
cygwin=false;
darwin=false;
mingw=false
case "$(uname)" in
CYGWIN*) cygwin=true ;;
MINGW*) mingw=true;;
Darwin*) darwin=true
# Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
# See https://developer.apple.com/library/mac/qa/qa1170/_index.html
if [ -z "$JAVA_HOME" ]; then
if [ -x "/usr/libexec/java_home" ]; then
JAVA_HOME="$(/usr/libexec/java_home)"; export JAVA_HOME
else
JAVA_HOME="/Library/Java/Home"; export JAVA_HOME
fi
fi
;;
esac
if [ -z "$JAVA_HOME" ] ; then
if [ -r /etc/gentoo-release ] ; then
JAVA_HOME=$(java-config --jre-home)
fi
fi
# For Cygwin, ensure paths are in UNIX format before anything is touched
if $cygwin ; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --unix "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --unix "$CLASSPATH")
fi
# For Mingw, ensure paths are in UNIX format before anything is touched
if $mingw ; then
[ -n "$JAVA_HOME" ] && [ -d "$JAVA_HOME" ] &&
JAVA_HOME="$(cd "$JAVA_HOME" || (echo "cannot cd into $JAVA_HOME."; exit 1); pwd)"
fi
if [ -z "$JAVA_HOME" ]; then
javaExecutable="$(which javac)"
if [ -n "$javaExecutable" ] && ! [ "$(expr "\"$javaExecutable\"" : '\([^ ]*\)')" = "no" ]; then
# readlink(1) is not available as standard on Solaris 10.
readLink=$(which readlink)
if [ ! "$(expr "$readLink" : '\([^ ]*\)')" = "no" ]; then
if $darwin ; then
javaHome="$(dirname "\"$javaExecutable\"")"
javaExecutable="$(cd "\"$javaHome\"" && pwd -P)/javac"
else
javaExecutable="$(readlink -f "\"$javaExecutable\"")"
fi
javaHome="$(dirname "\"$javaExecutable\"")"
javaHome=$(expr "$javaHome" : '\(.*\)/bin')
JAVA_HOME="$javaHome"
export JAVA_HOME
fi
fi
fi
if [ -z "$JAVACMD" ] ; then
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
else
JAVACMD="$(\unset -f command 2>/dev/null; \command -v java)"
fi
fi
if [ ! -x "$JAVACMD" ] ; then
echo "Error: JAVA_HOME is not defined correctly." >&2
echo " We cannot execute $JAVACMD" >&2
exit 1
fi
if [ -z "$JAVA_HOME" ] ; then
echo "Warning: JAVA_HOME environment variable is not set."
fi
# traverses directory structure from process work directory to filesystem root
# first directory with .mvn subdirectory is considered project base directory
find_maven_basedir() {
if [ -z "$1" ]
then
echo "Path not specified to find_maven_basedir"
return 1
fi
basedir="$1"
wdir="$1"
while [ "$wdir" != '/' ] ; do
if [ -d "$wdir"/.mvn ] ; then
basedir=$wdir
break
fi
# workaround for JBEAP-8937 (on Solaris 10/Sparc)
if [ -d "${wdir}" ]; then
wdir=$(cd "$wdir/.." || exit 1; pwd)
fi
# end of workaround
done
printf '%s' "$(cd "$basedir" || exit 1; pwd)"
}
# concatenates all lines of a file
concat_lines() {
if [ -f "$1" ]; then
# Remove \r in case we run on Windows within Git Bash
# and check out the repository with auto CRLF management
# enabled. Otherwise, we may read lines that are delimited with
# \r\n and produce $'-Xarg\r' rather than -Xarg due to word
# splitting rules.
tr -s '\r\n' ' ' < "$1"
fi
}
log() {
if [ "$MVNW_VERBOSE" = true ]; then
printf '%s\n' "$1"
fi
}
BASE_DIR=$(find_maven_basedir "$(dirname "$0")")
if [ -z "$BASE_DIR" ]; then
exit 1;
fi
MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}; export MAVEN_PROJECTBASEDIR
log "$MAVEN_PROJECTBASEDIR"
##########################################################################################
# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
# This allows using the maven wrapper in projects that prohibit checking in binary data.
##########################################################################################
wrapperJarPath="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar"
if [ -r "$wrapperJarPath" ]; then
log "Found $wrapperJarPath"
else
log "Couldn't find $wrapperJarPath, downloading it ..."
if [ -n "$MVNW_REPOURL" ]; then
wrapperUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
else
wrapperUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
fi
while IFS="=" read -r key value; do
# Remove '\r' from value to allow usage on windows as IFS does not consider '\r' as a separator ( considers space, tab, new line ('\n'), and custom '=' )
safeValue=$(echo "$value" | tr -d '\r')
case "$key" in (wrapperUrl) wrapperUrl="$safeValue"; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
log "Downloading from: $wrapperUrl"
if $cygwin; then
wrapperJarPath=$(cygpath --path --windows "$wrapperJarPath")
fi
if command -v wget > /dev/null; then
log "Found wget ... using wget"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--quiet"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
wget $QUIET "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
else
wget $QUIET --http-user="$MVNW_USERNAME" --http-password="$MVNW_PASSWORD" "$wrapperUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
fi
elif command -v curl > /dev/null; then
log "Found curl ... using curl"
[ "$MVNW_VERBOSE" = true ] && QUIET="" || QUIET="--silent"
if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
curl $QUIET -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
else
curl $QUIET --user "$MVNW_USERNAME:$MVNW_PASSWORD" -o "$wrapperJarPath" "$wrapperUrl" -f -L || rm -f "$wrapperJarPath"
fi
else
log "Falling back to using Java to download"
javaSource="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.java"
javaClass="$MAVEN_PROJECTBASEDIR/.mvn/wrapper/MavenWrapperDownloader.class"
# For Cygwin, switch paths to Windows format before running javac
if $cygwin; then
javaSource=$(cygpath --path --windows "$javaSource")
javaClass=$(cygpath --path --windows "$javaClass")
fi
if [ -e "$javaSource" ]; then
if [ ! -e "$javaClass" ]; then
log " - Compiling MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/javac" "$javaSource")
fi
if [ -e "$javaClass" ]; then
log " - Running MavenWrapperDownloader.java ..."
("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$wrapperUrl" "$wrapperJarPath") || rm -f "$wrapperJarPath"
fi
fi
fi
fi
##########################################################################################
# End of extension
##########################################################################################
# If specified, validate the SHA-256 sum of the Maven wrapper jar file
wrapperSha256Sum=""
while IFS="=" read -r key value; do
case "$key" in (wrapperSha256Sum) wrapperSha256Sum=$value; break ;;
esac
done < "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.properties"
if [ -n "$wrapperSha256Sum" ]; then
wrapperSha256Result=false
if command -v sha256sum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | sha256sum -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
elif command -v shasum > /dev/null; then
if echo "$wrapperSha256Sum $wrapperJarPath" | shasum -a 256 -c > /dev/null 2>&1; then
wrapperSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available."
echo "Please install either command, or disable validation by removing 'wrapperSha256Sum' from your maven-wrapper.properties."
exit 1
fi
if [ $wrapperSha256Result = false ]; then
echo "Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised." >&2
echo "Investigate or delete $wrapperJarPath to attempt a clean download." >&2
echo "If you updated your Maven version, you need to update the specified wrapperSha256Sum property." >&2
exit 1
fi
fi
MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
# For Cygwin, switch paths to Windows format before running java
if $cygwin; then
[ -n "$JAVA_HOME" ] &&
JAVA_HOME=$(cygpath --path --windows "$JAVA_HOME")
[ -n "$CLASSPATH" ] &&
CLASSPATH=$(cygpath --path --windows "$CLASSPATH")
[ -n "$MAVEN_PROJECTBASEDIR" ] &&
MAVEN_PROJECTBASEDIR=$(cygpath --path --windows "$MAVEN_PROJECTBASEDIR")
fi
# Provide a "standardized" way to retrieve the CLI args that will
# work with both Windows and non-Windows executions.
MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $*"
export MAVEN_CMD_LINE_ARGS
WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
# shellcheck disable=SC2086 # safe args
exec "$JAVACMD" \
$MAVEN_OPTS \
$MAVEN_DEBUG_OPTS \
-classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
"-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
Vendored
+205
View File
@@ -0,0 +1,205 @@
@REM ----------------------------------------------------------------------------
@REM Licensed to the Apache Software Foundation (ASF) under one
@REM or more contributor license agreements. See the NOTICE file
@REM distributed with this work for additional information
@REM regarding copyright ownership. The ASF licenses this file
@REM to you under the Apache License, Version 2.0 (the
@REM "License"); you may not use this file except in compliance
@REM with the License. You may obtain a copy of the License at
@REM
@REM https://www.apache.org/licenses/LICENSE-2.0
@REM
@REM Unless required by applicable law or agreed to in writing,
@REM software distributed under the License is distributed on an
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
@REM KIND, either express or implied. See the License for the
@REM specific language governing permissions and limitations
@REM under the License.
@REM ----------------------------------------------------------------------------
@REM ----------------------------------------------------------------------------
@REM Apache Maven Wrapper startup batch script, version 3.2.0
@REM
@REM Required ENV vars:
@REM JAVA_HOME - location of a JDK home dir
@REM
@REM Optional ENV vars
@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
@REM e.g. to debug Maven itself, use
@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
@REM ----------------------------------------------------------------------------
@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
@echo off
@REM set title of command window
title %0
@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
@REM set %HOME% to equivalent of $HOME
if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
@REM Execute a user defined script before this one
if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
@REM check for pre script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
:skipRcPre
@setlocal
set ERROR_CODE=0
@REM To isolate internal variables from possible post scripts, we use another setlocal
@setlocal
@REM ==== START VALIDATION ====
if not "%JAVA_HOME%" == "" goto OkJHome
echo.
echo Error: JAVA_HOME not found in your environment. >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
:OkJHome
if exist "%JAVA_HOME%\bin\java.exe" goto init
echo.
echo Error: JAVA_HOME is set to an invalid directory. >&2
echo JAVA_HOME = "%JAVA_HOME%" >&2
echo Please set the JAVA_HOME variable in your environment to match the >&2
echo location of your Java installation. >&2
echo.
goto error
@REM ==== END VALIDATION ====
:init
@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
@REM Fallback to current working directory if not found.
set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
set EXEC_DIR=%CD%
set WDIR=%EXEC_DIR%
:findBaseDir
IF EXIST "%WDIR%"\.mvn goto baseDirFound
cd ..
IF "%WDIR%"=="%CD%" goto baseDirNotFound
set WDIR=%CD%
goto findBaseDir
:baseDirFound
set MAVEN_PROJECTBASEDIR=%WDIR%
cd "%EXEC_DIR%"
goto endDetectBaseDir
:baseDirNotFound
set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
cd "%EXEC_DIR%"
:endDetectBaseDir
IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
@setlocal EnableExtensions EnableDelayedExpansion
for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
:endReadAdditionalConfig
SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
set WRAPPER_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperUrl" SET WRAPPER_URL=%%B
)
@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
if exist %WRAPPER_JAR% (
if "%MVNW_VERBOSE%" == "true" (
echo Found %WRAPPER_JAR%
)
) else (
if not "%MVNW_REPOURL%" == "" (
SET WRAPPER_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.2.0/maven-wrapper-3.2.0.jar"
)
if "%MVNW_VERBOSE%" == "true" (
echo Couldn't find %WRAPPER_JAR%, downloading it ...
echo Downloading from: %WRAPPER_URL%
)
powershell -Command "&{"^
"$webclient = new-object System.Net.WebClient;"^
"if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
"$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
"}"^
"[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%WRAPPER_URL%', '%WRAPPER_JAR%')"^
"}"
if "%MVNW_VERBOSE%" == "true" (
echo Finished downloading %WRAPPER_JAR%
)
)
@REM End of extension
@REM If specified, validate the SHA-256 sum of the Maven wrapper jar file
SET WRAPPER_SHA_256_SUM=""
FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
IF "%%A"=="wrapperSha256Sum" SET WRAPPER_SHA_256_SUM=%%B
)
IF NOT %WRAPPER_SHA_256_SUM%=="" (
powershell -Command "&{"^
"$hash = (Get-FileHash \"%WRAPPER_JAR%\" -Algorithm SHA256).Hash.ToLower();"^
"If('%WRAPPER_SHA_256_SUM%' -ne $hash){"^
" Write-Output 'Error: Failed to validate Maven wrapper SHA-256, your Maven wrapper might be compromised.';"^
" Write-Output 'Investigate or delete %WRAPPER_JAR% to attempt a clean download.';"^
" Write-Output 'If you updated your Maven version, you need to update the specified wrapperSha256Sum property.';"^
" exit 1;"^
"}"^
"}"
if ERRORLEVEL 1 goto error
)
@REM Provide a "standardized" way to retrieve the CLI args that will
@REM work with both Windows and non-Windows executions.
set MAVEN_CMD_LINE_ARGS=%*
%MAVEN_JAVA_EXE% ^
%JVM_CONFIG_MAVEN_PROPS% ^
%MAVEN_OPTS% ^
%MAVEN_DEBUG_OPTS% ^
-classpath %WRAPPER_JAR% ^
"-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
%WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
if ERRORLEVEL 1 goto error
goto end
:error
set ERROR_CODE=1
:end
@endlocal & set ERROR_CODE=%ERROR_CODE%
if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
@REM check for post script, once with legacy .bat ending and once with .cmd ending
if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
:skipRcPost
@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
if "%MAVEN_BATCH_PAUSE%"=="on" pause
if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
cmd /C exit /B %ERROR_CODE%
+80
View File
@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.airnan</groupId>
<artifactId>draw-and-guess</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>draw-and-guess</name>
<description>draw-and-guess</description>
<properties>
<java.version>21</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
<!-- <dependency>-->
<!-- <groupId>jakarta.websocket</groupId>-->
<!-- <artifactId>jakarta.websocket-api</artifactId>-->
<!-- <version>2.1.1</version>-->
<!-- <scope>provided</scope>-->
<!-- </dependency>-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.26</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.48</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
<plugin><!--编译跳过测试文件检查的生命周期-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<skip>true</skip>
</configuration>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,15 @@
package cn.airnan.dg;
import cn.airnan.dg.service.TopicService;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DrawAndGuessApplication {
public static void main(String[] args) {
SpringApplication.run(DrawAndGuessApplication.class, args);
TopicService.loadTopics();
}
}
@@ -0,0 +1,19 @@
package cn.airnan.dg.configs;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* 配置跨域
*/
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**").allowedOriginPatterns("*")
.allowedMethods("GET", "HEAD", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true).maxAge(3600);
}
}
@@ -0,0 +1,58 @@
package cn.airnan.dg.configs;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.entity.Topic;
import jakarta.websocket.Session;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArraySet;
/**
* 全局缓存变量
*/
public class Global {
/**
* 消息类型-房间聊天(回答)
*/
public static final String MSG_TYPE_ROOM_CHAT = "room-chat";
/**
* 消息类型-房间绘画
*/
public static final String MSG_TYPE_ROOM_DRAW = "room-draw";
/**
* 消息类型-房间通知消息
*/
public static final String MSG_TYPE_ROOM_MSG = "room-msg";
/**
* 题库
*/
public static List<Topic> TOPICS = new ArrayList<>();
/**
* 房间列表
*/
public static final Map<String, Room> ROOMS = new HashMap<>();
/**
* 玩家列表
*/
public static final Map<String, Player> PLAYERS = new HashMap<>();
/**
* 会话列表
*/
public static final CopyOnWriteArraySet<Session> SESSIONS = new CopyOnWriteArraySet<>();
/**
* 在线连接池
*/
public static final Map<String, Session> SESSION_POOL = new HashMap<>();
}
@@ -0,0 +1,14 @@
package cn.airnan.dg.configs;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfiguration {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}
@@ -0,0 +1,78 @@
package cn.airnan.dg.controller;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.service.PlayerService;
import cn.airnan.dg.service.RoomService;
import cn.airnan.dg.utils.CommonResponse;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
/**
* 玩家相关接口
*/
@RestController
@RequestMapping("/api/player")
public class PlayerController {
private static final Logger log = LoggerFactory.getLogger(PlayerController.class);
@Resource
private PlayerService service;
/**
* 获取玩家列表
*
* @return 玩家列表
*/
@GetMapping("/list")
public CommonResponse list() {
try {
return CommonResponse.success(service.getPlayers());
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 判断玩家是否还在
*
* @return true/false
*/
@PostMapping("/isExist")
public CommonResponse isExist(@RequestBody Player player) {
try {
return CommonResponse.success(Global.PLAYERS.containsKey(player.getId()));
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 创建玩家
*
* @return 玩家信息
*/
@PostMapping("/create")
public CommonResponse create(@RequestBody Player player) {
try {
if (service.canCreatePlayer(player.getId())) {
var result = service.createPlayer(player);
if (result != null) {
log.info("【创建玩家】名称:{}", player.getId());
return CommonResponse.success(result);
} else {
return CommonResponse.error("创建玩家失败,请重试!");
}
} else {
return CommonResponse.error("玩家名称已存在,换一个吧!");
}
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
}
@@ -0,0 +1,91 @@
package cn.airnan.dg.controller;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.service.RoomService;
import cn.airnan.dg.utils.CommonResponse;
import jakarta.annotation.Resource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
/**
* 房间相关接口
*/
@RestController
@RequestMapping("/api/room")
public class RoomController {
private static final Logger log = LoggerFactory.getLogger(RoomController.class);
@Resource
private RoomService service;
/**
* 获取房间列表
*
* @return 房间列表
*/
@GetMapping("/list")
public CommonResponse list() {
try {
return CommonResponse.success(service.getRooms());
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 创建房间
*
* @return 房间信息
*/
@PostMapping("/create")
public CommonResponse create(@RequestBody Room room) {
try {
if (service.canCreateRoom(room.getId())) {
var result = service.createRoom(room);
if (result != null) {
log.info("【创建房间】{}创建房间:{}", room.getMaster(), room.getId());
return CommonResponse.success(result);
} else {
return CommonResponse.error("创建房间失败,请重试!");
}
} else {
return CommonResponse.error("房间名已存在,换一个吧!");
}
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
/**
* 判断是否可以加入房间
*
* @return true/false
*/
@PostMapping("/canJoin")
public CommonResponse canJoin(@RequestBody Room room) {
try {
if (service.canJoinRoom(room.getId())) {
List<Player> players = Global.ROOMS.get(room.getId()).getPlayers();
for (Player player : players) {
if (Objects.equals(room.getPlayers().getFirst().getId(), player.getId())) {
return CommonResponse.error("已经在房间里了!");
}
}
return CommonResponse.success(true);
} else {
return CommonResponse.error("房间已满或已经开始游戏,再换一个吧!");
}
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
}
@@ -0,0 +1,37 @@
package cn.airnan.dg.controller;
import cn.airnan.dg.service.TopicService;
import cn.airnan.dg.utils.CommonResponse;
import jakarta.annotation.Resource;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 题目相关接口
*/
@RestController
@RequestMapping("/api/topic")
public class TopicController {
@Resource
private TopicService service;
/**
* 随机获取题目列表
*
* @return 题目列表
*/
@GetMapping("/random")
public CommonResponse random() {
try {
return CommonResponse.success(service.randomTopics(4));
} catch (Exception e) {
return CommonResponse.error(e.getMessage());
}
}
}
@@ -0,0 +1,18 @@
package cn.airnan.dg.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Web首页
*/
@Controller
public class indexController {
@RequestMapping("/")
public String index() {
return "index.html";
}
}
@@ -0,0 +1,41 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.ArrayList;
import java.util.List;
/**
* 画板实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Board {
/**
* 画板ID
*/
private int id;
/**
* 画笔点位列表
*/
private List<Brush> brushes = new ArrayList<>();
/**
* 添加画笔点位
*
* @param brush 画笔点位
*/
private void addDraw(Brush brush) {
this.brushes.add(brush);
}
/**
* 清空画板
*/
public void clear() {
brushes.clear();
}
}
@@ -0,0 +1,28 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 画笔实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Brush {
/**
* 画笔颜色
*/
private String color;
/**
* 画笔粗细
*/
private String width;
/**
* 画笔坐标点
*/
private Point point;
}
@@ -0,0 +1,73 @@
package cn.airnan.dg.entity;
import cn.airnan.dg.utils.SocketMsg;
import com.alibaba.fastjson2.JSON;
import jakarta.websocket.Session;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 玩家实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Player {
/**
* 玩家要绑定的会话
*/
private Session session;
/**
* 玩家ID
*/
private String id;
/**
* 玩家位置
*/
private Integer index;
/**
* 玩家分数
*/
private Integer score = 0;
/**
* 加分
*
* @param add 要增加的分数
*/
private void addScore(Integer add) {
this.score += add;
}
/**
* 重置分数
*/
private void clearScore() {
this.score = 0;
}
/**
* 发送消息给玩家
*
* @param message 要发送的消息
*/
public void sendMessage(String type, String message) {
Session session = getSession();
if (session != null && session.isOpen()) {
try {
SocketMsg msg = new SocketMsg();
msg.setType(type);
msg.setData(message);
session.getAsyncRemote().sendText(JSON.toJSONString(msg));
} catch (Exception e) {
// 处理发送消息失败的情况
e.printStackTrace();
}
}
}
}
@@ -0,0 +1,22 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 坐标点实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Point {
/**
* X轴坐标点
*/
private double x;
/**
* Y轴坐标点
*/
private double y;
}
@@ -0,0 +1,99 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.ArrayList;
import java.util.List;
/**
* 房间实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Room {
private static final Logger log = LoggerFactory.getLogger(Room.class);
/**
* 房间ID
*/
private String id;
/**
* 房主ID
*/
private String master;
/**
* 玩家列表(最多五人)
*/
private List<Player> players = new ArrayList<>(5);
/**
* 画板
*/
private Board board;
/**
* 题目
*/
private Topic topic;
/**
* 游戏开始状态
*/
private boolean isBegin = false;
/**
* 倒计时
*/
private Integer countDown = 0;
/**
* 添加玩家
*
* @param player 要添加的玩家
*/
public void addPlayer(Player player) {
if (players.size() < 5) {
players.add(player);
} else {
System.out.println("房间已满,无法添加更多玩家。");
}
}
/**
* 根据玩家id移除房间内的玩家
*
* @param playerId 要移除的玩家id
*/
public void removePlayerById(String playerId) {
Player player = new Player();
player.setId(playerId);
players.remove(player);
}
/**
* 广播消息给房间内所有玩家
*
* @param type 消息类型
* @param message 要广播的消息
* @param playerId 发送者的ID(用于排除发送者)
*/
public void sendMessage(String type, String playerId, String message) {
log.info("【房间消息】{}: {}", id, message);
for (Player player : players) {
// 向除自己之外的玩家发送消息
// if (!player.getId().equals(playerId)) {
// player.sendMessage(type, message);
// }
// 向房间内所有玩家发送
player.sendMessage(type, message);
}
}
}
@@ -0,0 +1,25 @@
package cn.airnan.dg.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* 题目实体类
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Topic {
/**
* 题目名称
*/
private String name;
/**
* 题目提示
*/
private String tip;
}
@@ -0,0 +1,13 @@
package cn.airnan.dg.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RoomListVO {
private String id;
private String master;
}
@@ -0,0 +1,53 @@
package cn.airnan.dg.service;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import org.springframework.stereotype.Service;
import java.util.Map;
/**
* 玩家相关业务逻辑
*/
@Service
public class PlayerService {
/**
* 获取玩家列表
*
* @return
*/
public Map<String, Player> getPlayers() {
return Global.PLAYERS;
}
/**
* 创建玩家
*
* @return
*/
public Player createPlayer(Player player) {
try {
Global.PLAYERS.put(player.getId(), player);
return player;
}catch (Exception e){
return null;
}
}
/**
* 判断是否可以创建玩家
*
* @return true/false
*/
public boolean canCreatePlayer(String playerId) {
boolean exist = Global.PLAYERS.containsKey(playerId);
if (exist) {
return false;
}
return true;
}
}
@@ -0,0 +1,73 @@
package cn.airnan.dg.service;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Room;
import cn.airnan.dg.pojo.RoomListVO;
import org.springframework.stereotype.Service;
import java.util.*;
/**
* 房间相关业务逻辑
*/
@Service
public class RoomService {
/**
* 获取房间列表
*
* @return
*/
public List<RoomListVO> getRooms() {
List<RoomListVO> list = new ArrayList<>();
Global.ROOMS.values().forEach(room -> {
RoomListVO roomListVO = new RoomListVO();
roomListVO.setId(room.getId());
roomListVO.setMaster(room.getMaster());
list.add(roomListVO);
});
return list;
}
/**
* 创建房间
*
* @return
*/
public Room createRoom(Room room) {
try {
Global.ROOMS.put(room.getId(), room);
return room;
} catch (Exception e) {
return null;
}
}
/**
* 判断是否可以创建房间
*
* @return true/false
*/
public boolean canCreateRoom(String roomId) {
boolean exist = Global.ROOMS.containsKey(roomId);
if (exist) {
return false;
}
return true;
}
/**
* 判断是否可以加入房间
*
* @return true/false
*/
public boolean canJoinRoom(String roomId) {
int result = 5 - Global.ROOMS.get(roomId).getPlayers().size();
// 有空位并且游戏没有开始
if (result > 0 && !Global.ROOMS.get(roomId).isBegin()) {
return true;
}
return false;
}
}
@@ -0,0 +1,70 @@
package cn.airnan.dg.service;
import cn.airnan.dg.DrawAndGuessApplication;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Topic;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 题目相关业务逻辑
*/
@Slf4j
@Service
public class TopicService {
/**
* 从文件加载题库
*/
public static void loadTopics() {
try (InputStream inputStream = DrawAndGuessApplication.class.getResourceAsStream("/topic.json")) {
if (inputStream != null) {
String content = new String(inputStream.readAllBytes());
JSONArray jsonArray = JSON.parseArray(content);
for (Object obj : jsonArray) {
JSONObject jsonObject = (JSONObject) obj;
for (String key : jsonObject.keySet()) {
String value = jsonObject.getString(key);
Global.TOPICS.add(new Topic(key, value));
}
}
log.info("共加载{}条题目", Global.TOPICS.size());
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 随机取出N道题目
*
* @param n 取出的题目数量
* @return 题目列表
*/
public List<Topic> randomTopics(int n) {
Map<Integer, String> map = new HashMap();
List<Topic> news = new ArrayList();
if (Global.TOPICS.size() <= n) {
return Global.TOPICS;
} else {
while (map.size() < n) {
int random = (int) (Math.random() * Global.TOPICS.size());
if (!map.containsKey(random)) {
map.put(random, "");
news.add(Global.TOPICS.get(random));
}
}
return news;
}
}
}
@@ -0,0 +1,24 @@
package cn.airnan.dg.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class CommonResponse<T> {
private int code;
private String msg;
private T data;
public static <T> CommonResponse<T> success(T data) {
return new CommonResponse<T>(0, "success", data);
}
public static <T> CommonResponse<T> success() {
return new CommonResponse<T>(0, "success", null);
}
public static <T> CommonResponse<T> error(String msg) {
return new CommonResponse<T>(1, msg, null);
}
}
@@ -0,0 +1,35 @@
package cn.airnan.dg.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* Socket响应体
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SocketMsg<T> {
/**
* 消息类型
*/
private String type;
/**
* 消息发送人
*/
private String playerId;
/**
* 消息发送房间
*/
private String roomId;
/**
* 消息内容
*/
private T data;
}
@@ -0,0 +1,174 @@
package cn.airnan.dg.utils;
import cn.airnan.dg.configs.Global;
import cn.airnan.dg.entity.Player;
import cn.airnan.dg.entity.Room;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.TypeReference;
import jakarta.websocket.OnClose;
import jakarta.websocket.OnMessage;
import jakarta.websocket.OnOpen;
import jakarta.websocket.Session;
import jakarta.websocket.server.PathParam;
import jakarta.websocket.server.ServerEndpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.Map;
@Component
@Slf4j
@ServerEndpoint("/websocket/{playerId}/{roomId}")
public class WebSocket {
/**
* 加入房间时触发WebSocket事件
*
* @param session 当前会话
* @param playerId 玩家id
* @param roomId 房间id
*/
@OnOpen
public void onOpen(Session session, @PathParam(value = "playerId") String playerId, @PathParam(value = "roomId") String roomId) {
try {
// 将会话添加到会话列表
Global.SESSIONS.add(session);
// 将会话添加到连接池
Global.SESSION_POOL.put(playerId, session);
// 将会话与玩家绑定
Player player = Global.PLAYERS.get(playerId);
if (player != null) {
player.setSession(session);
}
// 加入房间
Room room = Global.ROOMS.get(roomId);
if (room != null) {
room.addPlayer(player);
}
// 通知其他玩家有新玩家加入
if (room != null) {
String message = "玩家【" + playerId + "】加入了房间";
room.sendMessage(Global.MSG_TYPE_ROOM_MSG, playerId, message);
room.sendMessage(Global.MSG_TYPE_ROOM_CHAT, playerId, "系统:"+message);
}
log.info("【加入房间】玩家 {} 进入房间 {},房间人数为:{}", playerId, roomId, Global.SESSIONS.size());
} catch (Exception e) {
log.error(e.getMessage());
}
}
@OnClose
public void onClose(Session session) {
try {
// 从会话列表中移除关闭的会话
Global.SESSIONS.remove(session);
// 从连接池中移除关闭的会话
String playerIdToRemove = null;
for (Map.Entry<String, Session> entry : Global.SESSION_POOL.entrySet()) {
if (entry.getValue().equals(session)) {
playerIdToRemove = entry.getKey();
break;
}
}
if (playerIdToRemove != null) {
Global.SESSION_POOL.remove(playerIdToRemove);
}
String playerId = "";
// 清除玩家与会话的绑定
for (Player player : Global.PLAYERS.values()) {
if (player.getSession() != null && player.getSession().equals(session)) {
player.setSession(null);
playerId = player.getId();
break;
}
}
// 从房间中移除玩家
for (Room room : Global.ROOMS.values()) {
room.removePlayerById(playerId);
String message = "玩家【" + playerId + "】退出了游戏,房间人数为:" + room.getPlayers().size();
room.sendMessage(Global.MSG_TYPE_ROOM_MSG, playerId, message);
room.sendMessage(Global.MSG_TYPE_ROOM_CHAT, playerId, "系统:"+message);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
@OnMessage
public void onMessage(String message) {
// log.info("【WebSocket消息】收到客户端消息:{}", message);
try {
// 解析消息为 SocketMsg 结构体
SocketMsg<String> socketMsg = JSON.parseObject(message, new TypeReference<>() {
});
// 匹配房间并转发消息
Global.ROOMS.get(socketMsg.getRoomId()).sendMessage(socketMsg.getType(), socketMsg.getPlayerId(), socketMsg.getData());
} catch (Exception e) {
log.error("解析消息失败:{}", message);
}
}
/**
* 此为广播消息
*
* @param message 消息
*/
public static void sendAllMessage(String message) {
for (Session session : Global.SESSIONS) {
try {
if (session.isOpen()) {
session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
/**
* 此为单点消息
*
* @param playerId 用户编号
* @param message 消息
*/
public static void sendOneMessage(String playerId, String message) {
Session session = Global.SESSION_POOL.get(playerId);
if (session != null && session.isOpen()) {
try {
synchronized (session) {
session.getAsyncRemote().sendText(message);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
/**
* 此为单点消息(多人)
*
* @param playerIds 用户编号列表
* @param message 消息
*/
public static void sendMoreMessage(String[] playerIds, String message) {
for (String playerId : playerIds) {
Session session = Global.SESSION_POOL.get(playerId);
if (session != null && session.isOpen()) {
try {
session.getAsyncRemote().sendText(message);
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
}
public static Integer getOnLine() {
return Global.SESSION_POOL.size();
}
}
+5
View File
@@ -0,0 +1,5 @@
spring:
application:
name: draw-and-guess
server:
port: 8080
+89
View File
@@ -0,0 +1,89 @@
#game-top-box {
width: 100%;
display: flex;
justify-content: space-between;
}
#draw-box {
width: 350px;
margin: 0 auto;
min-height: 350px;
box-sizing: border-box;
border: #000000 solid 1px;
border-radius: 15px;
background-color: #daeefd;
}
#topic-bar {
width: 350px;
height: 30px;
line-height: 30px;
display: flex;
justify-content: space-between;
font-size: 0.8em;
}
#topic-bar > div {
flex: 1;
}
#board {
width: 300px;
height: 300px;
background-color: #ffffff;
box-shadow: #000000 0 0 1px;
border-radius: 10px;
}
.color-btn {
width: 20px;
height: 20px;
padding: 0;
background-color: #ffffff;
border-radius: 20px;
border: 2px solid white;
}
#player-box {
display: flex;
justify-content: center;
width: 100%;
}
.player-item {
box-sizing: border-box;
width: 30px;
height: 30px;
line-height: 30px;
border-radius: 4px;
font-size: 1.4em;
color: coral;
background-color: #ffffff;
box-shadow: #00000022 0 0 5px;
margin: 5px 10px;
}
#chat-box {
width: calc(100vw - 20px);
height: calc(100vh - 540px);
box-sizing: border-box;
padding: 10px;
margin: auto;
border-radius: 10px;
background-color: #00000022;
box-shadow: #000000 0 0 1px;
text-align: left;
font-size: 0.8em;
overflow-y: scroll;
}
#chat-input-box {
width: 100vw;
height: 50px;
position: fixed;
bottom: 0;
background-color: #bbe1ec;
}
#chat-input {
width: calc(100vw - 100px);
}
+73
View File
@@ -0,0 +1,73 @@
#home-title {
width: 100vw;
height: 150px;
overflow: hidden;
}
#home-title label {
line-height: 35px;
padding: 10px 10px;
}
#player-id {
width: 40vw;
text-align: center;
}
#room-list-box {
width: 90vw;
height: calc(100vh - 220px);
margin: 0 auto;
padding: 10px;
border-radius: 10px;
border: #444444 2px solid;
overflow: hidden;
}
#room-item-title{
display: inline-block;
height: 30px;
line-height: 30px;
font-weight: bold;
}
#room-list{
height: calc(100vh - 240px);
overflow-y: scroll;
}
.room-item {
width: 80vw;
height: 60px;
box-sizing: border-box;
display: flex;
justify-content: space-between;
padding: 0 20px;
margin: 10px auto;
border-radius: 10px;
border: 2px solid #444444;
}
.room-item-room, .room-item-player {
display: block;
height: 30px;
text-align: left;
}
.room-item-room {
line-height: 40px;
font-weight: bold;
}
.room-item-player {
height: 20px;
line-height: 20px;
font-size: 0.8em;
color: #666666;
}
.room-item .button {
height: 45px;
border-radius: 6px;
margin-right: 0;
}
+104
View File
@@ -0,0 +1,104 @@
* {
outline: 0;
user-select: none;
}
/* 修改滚动条为黄色,宽度为3px */
::-webkit-scrollbar {
width: 3px;
}
/* 修改滚动条轨道颜色为透明 */
::-webkit-scrollbar-track {
background-color: transparent;
}
/* 修改滚动条滑块颜色为黄色 */
::-webkit-scrollbar-thumb {
background-color: #666666;
}
html,
body {
padding: 0;
margin: 0;
width: 100vw;
height: 100vh;
max-width: 100vw;
max-height: 100vh;
min-width: 100vw;
min-height: 100vh;
overflow: hidden;
text-align: center;
background-color: #bbe1ec;
}
#page-box {
display: flex;
justify-content: space-between;
width: 200vw;
height: 100vh;
position: fixed;
left: 0;
top: 0;
transition: left 0.3s cubic-bezier(0.25, 0.1, 0.25, 1);
}
#page-home,
#page-game {
width: 100vw;
height: 100vh;
}
#page-home {
/* background-color: #faf5b0; */
background-color: #bbe1ec;
}
#page-game {
background-color: #bbe1ec;
}
.button {
padding: 8px 10px;
line-height: 1;
box-sizing: border-box;
background-color: #fdf10d;
border: #444444 solid 2px;
border-radius: 100px;
margin: 5px 10px;
}
.input {
box-sizing: border-box;
width: calc(100% - 20px);
padding: 8px 10px;
line-height: 1;
background-color: #ffffff;
border: #444444 solid 2px;
border-radius: 100px;
margin: 5px 10px;
}
.input[disabled]{
background-color: #e5db63;
}
.toast{
display: block;
position: fixed;
box-sizing: border-box;
width: max-content;
max-width: 80vw;
height: max-content;
padding: 10px 20px;
top: 100px;
left: 0;
right: 0;
margin: auto;
background-color: #fceb32;
border: #000000 solid 2px;
color: #000000;
border-radius: 6px;
pointer-events: none;
}
+415
View File
@@ -0,0 +1,415 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="renderer" content="webkit"/>
<meta name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no">
<title>你画我猜</title>
<link rel="stylesheet" href="./css/main.css">
<link rel="stylesheet" href="./css/home.css">
<link rel="stylesheet" href="./css/game.css">
<script src="./js/jquery-3.7.1.min.js"></script>
</head>
<body>
<div id="page-box">
<!-- 主页 -->
<div id="page-home">
<div id="home-title">
<h1>你画我猜</h1>
<label for="player-id">昵称</label><input type="text" id="player-id" class="input"
placeholder="请输入玩家昵称"/>
<button class="button" onclick="createPlayer()">创建</button>
</div>
<div id="room-list-box">
<span id="room-item-title">房间列表</span>
<div id="room-list">
</div>
</div>
<div style="width: 100%;display: flex;justify-content: center;">
<button class="button" onclick="getRooms()">刷新列表</button>
<button class="button" onclick="createRoom()">创建房间</button>
</div>
<button class="button" onclick="goGame()">切换到游戏页面</button>
</div>
<!-- 游戏页面 -->
<div id="page-game">
<div id="game-top-box">
<button id="back-btn" class="button">返回</button>
<button id="userIcon" class="button">玩家:admin</button>
</div>
<div id="draw-box">
<div id="topic-bar">
<div>题目:<span id="topic-name">坐井观天</span></div>
<div>提示:<span id="topic-tip">成语,四个字</span></div>
</div>
<canvas id="board" width="300" height="300"></canvas>
</div>
<div>
<button id="black-btn" class="color-btn" style="background-color: #000000;"></button>
<button id="red-btn" class="color-btn" style="background-color: #ff0000;"></button>
<button id="green-btn" class="color-btn" style="background-color: #00ff00;"></button>
<button id="blue-btn" class="color-btn" style="background-color: #0000ff;"></button>
<button id="yellow-btn" class="color-btn" style="background-color: #ffff00;"></button>
</div>
<div><label for="widthPicker">选择画笔宽度:</label>
<input type="range" id="widthPicker" value="1" min="1">
</div>
<div id="player-box">
<div class="player-item">张</div>
<div class="player-item">王</div>
<div class="player-item"></div>
<div class="player-item"></div>
<div class="player-item"></div>
</div>
<div id="chat-box">
</div>
<div id="chat-input-box">
<input id="chat-input" type="text" autocomplete="off" placeholder="输入文字以发送" class="input"/>
<button class="button" onclick="sendInput()">发送</button>
</div>
</div>
</div>
</body>
</html>
<script>
// WebSocket实例
let ws = null;
// 当前玩家id
let playerId = "";
// 当前房间id
let roomId = "";
let backendAddr = "192.168.31.222:8080"
const api = "http://" + backendAddr + "/api"
init();
function init() {
window.addEventListener("beforeunload", (event) => {
// Cancel the event as stated by the standard.
event.preventDefault();
// Chrome requires returnValue to be set.
event.returnValue = "";
});
isHasWS();
getLocalPlayer();
getRooms();
}
/**
* 判断WebSocket是否可用
*/
function isHasWS() {
if ("WebSocket" in window) {
console.log("WebSocket可用");
} else {
alert("浏览器不支持WebSocket,无法进行游戏,请尝试其他浏览器");
$("body").hide();
}
}
/**
* 获取本地玩家信息
*/
function getLocalPlayer() {
playerId = window.localStorage.getItem("playerId");
if (playerId === undefined || playerId === null || playerId === "") {
playerId = "";
} else {
// 本地存在时判断服务端是否还在
$.ajax({
url: api + "/player/isExist",
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: playerId}),
success: function (data) {
if (data.code === 0) {
if (data.data === true) {
$("#player-id").val(playerId);
} else {
window.localStorage.removeItem("playerId");
showMsg("服务端玩家已注销,请重新创建!");
}
} else {
showMsg(data.msg)
}
},
error: function (xhr, status, error) {
// 请求失败时的处理逻辑
console.error('请求失败:', status, error);
}
});
}
}
/**
* 创建玩家
*/
function createPlayer() {
let postData = {
id: $("#player-id").val().trim()
}
if (postData.id == null || postData.id == undefined || postData.id == "") {
return showMsg("玩家昵称不能为空!")
}
$.ajax({
url: api + "/player/create",
type: 'POST',
contentType: 'application/json',
data: JSON.stringify(postData),
success: function (data) {
if (data.code === 0) {
showMsg("创建成功!");
window.localStorage.setItem("playerId", postData.id);
playerId = postData.id;
} else {
showMsg(data.msg)
}
},
error: function (xhr, status, error) {
// 请求失败时的处理逻辑
console.error('请求失败:', status, error);
}
});
}
/**
* 获取房间列表
*/
function getRooms() {
$("#room-list").html("");
$.ajax({
url: api + "/room/list",
type: 'GET',
success: function (data) {
// 成功接收到响应后的处理逻辑
showMsg("房间列表已刷新");
if (data.code === 0) {
let roomsData = data.data;
if (!Array.isArray(roomsData) || roomsData.length === 0) {
$("#room-list").html("—— 暂无房间 ——")
}
$.each(roomsData, function (index, room) {
// 创建房间元素
var $roomItem = $('<div class="room-item"></div>');
var $roomInfo = $('<div></div>');
var $roomName = $('<span class="room-item-room">' + room.id + '</span>');
var $playerInfo = $('<span class="room-item-player">@' + room.master + '</span>');
var $joinButton = $('<button class="button">加入房间</button>');
// 将房间名称和玩家信息添加到房间信息元素中
$roomInfo.append($roomName);
$roomInfo.append($playerInfo);
// 将房间信息和加入按钮添加到房间元素中
$roomItem.append($roomInfo);
$roomItem.append($joinButton);
// 将房间元素添加到页面中
$("#room-list").append($roomItem);
// 绑定按钮点击事件
$joinButton.on('click', function () {
if (playerId === "") {
return showMsg("请先创建玩家!");
}
var result = confirm("确定要加入房间【" + room.id + "】吗?");
if (result) {
joinRoom(room.id);
} else {
showMsg("取消操作")
}
});
});
}
},
error: function (xhr, status, error) {
// 请求失败时的处理逻辑
console.error('请求失败:', status, error);
}
});
}
function createRoom() {
if (playerId === "") {
return showMsg("请先创建玩家!");
}
let createInput = window.prompt("请输入房间名称:", "");
if (createInput !== null) {
if (createInput.trim() === "") {
return showMsg("房间名不能为空!")
}
$.ajax({
url: api + "/room/create",
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: createInput, master: playerId}),
success: function (data) {
if (data.code === 0) {
showMsg("创建成功!");
var result = confirm("创建成功,是否现在进入房间【" + createInput + "】?");
if (result) {
joinRoom(createInput)
}
getRooms();
} else {
showMsg(data.msg)
}
},
error: function (xhr, status, error) {
// 请求失败时的处理逻辑
console.error('请求失败:', status, error);
}
});
}
}
function showMsg(message) {
var $toast = $('<div class="toast"></div>').text(message).hide();
$('body').append($toast);
$toast.fadeIn(100);
setTimeout(function () {
$toast.fadeOut(100);
setTimeout(function () {
$toast.remove();
}, 600);
}, 2000);
}
function joinRoom(rid) {
// 判断是否可以加入
$.ajax({
url: api + "/room/canJoin",
type: 'POST',
contentType: 'application/json',
data: JSON.stringify({id: rid, players: [{id: playerId}]}),
success: function (data) {
if (data.code === 0) {
let url = "ws://" + backendAddr + "/websocket/" + playerId + "/" + rid;
// 打开一个 web socket
ws = new WebSocket(url);
ws.onopen = function () {
roomId = rid;
// 发送握手消息
sendMessage("room-chat", playerId + ":大家好,我是" + playerId);
goGame();
};
ws.onmessage = function (evt) {
let result = JSON.parse(evt.data);
//接收到通知消息
if (result.type === "room-msg") {
showMsg(result.data);
}
if (result.type === "room-chat") {
$("#chat-box").append("<p>" + result.data + "</p>")
}
};
ws.onclose = function () {
// 关闭 websocket
ws = null;
roomId = "";
};
ws.onerror = function (err) {
showMsg("加入失败,请重试");
roomId = "";
}
} else {
showMsg(data.msg)
}
},
error: function (xhr, status, error) {
// 请求失败时的处理逻辑
console.error('请求失败:', status, error);
}
});
}
/**
* 停止WebSocket
*/
function StopWS() {
if (ws !== null) {
ws.close();
ws = null;
roomId = "";
}
document.getElementById("image").src = "";
}
/**
* 向服务端发送消息
*/
function sendMessage(type, data) {
if (getWsState() !== 1) {
return showMsg("WebSocket未连接,无法发送消息!");
}
let msg = {
type,
playerId,
roomId,
data,
};
ws.send(JSON.stringify(msg));
}
/**
* 获取WebSocket状态
* 0 (WebSocket.CONNECTING)正在链接中
* 1 (WebSocket.OPEN)已经链接并且可以通讯
* 2 (WebSocket.CLOSING)连接正在关闭
* 3 (WebSocket.CLOSED)连接已关闭或者没有链接成功
*/
function getWsState() {
if (ws === null) {
return 3;
} else {
return ws.readyState;
}
}
function sendInput() {
let txt = $("#chat-input").val().trim();
if (txt !== "") {
sendMessage("room-chat", playerId + ":" + txt);
$("#chat-input").val("");
}
}
// 监听键盘事件
document.getElementById('chat-input').addEventListener('keydown', function (event) {
// 判断是否按下回车键(键码为13)
if (event.keyCode === 13) {
// 处理回车键事件
sendInput();
}
});
// 监听输入框的键盘事件
document.getElementById('chat-input').addEventListener('input', function (event) {
// 判断是否按下回车键
if (event.inputType === 'insertLineBreak') {
// 处理回车键事件
sendInput();
}
});
function goGame() {
$("#page-box").css("left", "-100vw");
$("#userIcon").html("玩家:" + playerId);
}
function goHome() {
$("#page-box").css("left", "0px");
}
</script>
File diff suppressed because one or more lines are too long
+63
View File
@@ -0,0 +1,63 @@
[
{"落井下石":"成语、4个字"},
{"画饼充饥":"成语、4个字"},
{"虎头蛇尾":"成语、4个字"},
{"泪流满面":"成语、4个字"},
{"捧腹大笑":"成语、4个字"},
{"画蛇添足":"成语、4个字"},
{"一手遮天":"成语、4个字"},
{"羊入虎口":"成语、4个字"},
{"掩耳盗铃":"成语、4个字"},
{"布娃娃":"儿童玩具、3个字"},
{"亡羊补牢":"成语、4个字"},
{"颠三倒四":"成语、4个字"},
{"拔苗助长":"成语、4个字"},
{"顺手牵羊":"成语、4个字"},
{"三长二短":"成语、4个字"},
{"抱头鼠窜":"成语、4个字"},
{"鸡鸣狗盗":"成语、4个字"},
{"头破血流":"成语、4个字"},
{"坐井观天":"成语、4个字"},
{"眼高手低":"成语、4个字"},
{"目瞪口呆":"成语、4个字"},
{"牛头马面":"成语、4个字"},
{"哈哈大笑":"成语、4个字"},
{"丢三落四":"成语、4个字"},
{"三心二意":"成语、4个字"},
{"一刀两断":"成语、4个字"},
{"空调":"日常物品、2个字"},
{"面粉":"日常物品、2个字"},
{"口红":"日常物品、2个字"},
{"铅笔":"日常物品、2个字"},
{"足球":"日常物品、2个字"},
{"酱油":"日常物品、2个字"},
{"大米":"日常物品、2个字"},
{"香水":"日常物品、2个字"},
{"橡皮":"日常物品、2个字"},
{"篮球":"日常物品、2个字"},
{"牛奶":"日常物品、2个字"},
{"馒头":"日常物品、2个字"},
{"雨伞":"日常物品、2个字"},
{"书包":"日常物品、2个字"},
{"冰箱":"日常物品、2个字"},
{"眼镜":"日常物品、2个字"},
{"啤酒":"日常物品、2个字"},
{"面条":"日常物品、2个字"},
{"超市":"日常物品、2个字"},
{"象棋":"日常物品、2个字"},
{"蛋糕":"日常物品、2个字"},
{"电话":"日常物品、2个字"},
{"手机":"日常物品、2个字"},
{"剪刀":"日常物品、2个字"},
{"凳子":"日常物品、2个字"},
{"火锅":"日常物品、2个字"},
{"茶叶":"日常物品、2个字"},
{"勺子":"日常物品、2个字"},
{"筷子":"日常物品、2个字"},
{"杯子":"日常物品、2个字"},
{"雨鞋":"日常物品、2个字"},
{"袜子":"日常物品、2个字"},
{"毛巾":"日常物品、2个字"},
{"脸盆":"日常物品、2个字"},
{"电脑":"日常物品、2个字"}
]
@@ -0,0 +1,15 @@
package cn.airnan.dg;
import cn.airnan.dg.service.TopicService;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class DrawAndGuessApplicationTests {
@Test
void contextLoads() {
TopicService.loadTopics();
}
}