From 3cc2c3c6985e2c0adf90f442d41d9fbcb480681f Mon Sep 17 00:00:00 2001 From: flykhan Date: Sun, 5 Mar 2023 19:27:35 +0800 Subject: [PATCH] =?UTF-8?q?=E5=88=9B=E5=BB=BAcloud=E7=88=B6=E7=BB=84?= =?UTF-8?q?=E4=BB=B6,=E5=B9=B6=E5=B0=86=E9=A1=B9=E7=9B=AE=E6=94=B9?= =?UTF-8?q?=E4=B8=BA=E4=B8=BB=E6=9C=8D=E5=8A=A1+=E5=BE=AE=E6=9C=8D?= =?UTF-8?q?=E5=8A=A1MatchingSystem?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backendcloud/.gitignore | 33 ++ backendcloud/.mvn/wrapper/maven-wrapper.jar | Bin 0 -> 59925 bytes .../.mvn/wrapper/maven-wrapper.properties | 18 + backendcloud/backend/pom.xml | 130 ++++++ .../com/kob/backend/BackendApplication.java | 13 + .../com/kob/backend/config/CorsConfig.java | 44 +++ .../backend/config/RestTemplateConfig.java | 18 + .../kob/backend/config/SecurityConfig.java | 64 +++ .../kob/backend/config/WebSocketConfig.java | 15 + .../filter/JwtAuthenticationTokenFilter.java | 73 ++++ .../kob/backend/consumer/WebSocketServer.java | 231 +++++++++++ .../com/kob/backend/consumer/utils/Cell.java | 12 + .../com/kob/backend/consumer/utils/Game.java | 370 ++++++++++++++++++ .../consumer/utils/JwtAuthenticationUtil.java | 20 + .../kob/backend/consumer/utils/Player.java | 61 +++ .../controller/pk/StartGameController.java | 25 ++ .../controller/user/UserController.java | 72 ++++ .../user/account/InfoController.java | 20 + .../user/account/LoginController.java | 27 ++ .../user/account/RegisterController.java | 24 ++ .../controller/user/bot/AddController.java | 18 + .../user/bot/GetListController.java | 22 ++ .../controller/user/bot/RemoveController.java | 20 + .../controller/user/bot/UpdateController.java | 20 + .../com/kob/backend/mapper/BotMapper.java | 9 + .../com/kob/backend/mapper/RecordMapper.java | 9 + .../com/kob/backend/mapper/UserMapper.java | 11 + .../main/java/com/kob/backend/pojo/Bot.java | 35 ++ .../java/com/kob/backend/pojo/Record.java | 34 ++ .../main/java/com/kob/backend/pojo/User.java | 30 ++ .../service/impl/UserDetailsServiceImpl.java | 35 ++ .../service/impl/pk/StartGameServiceImpl.java | 15 + .../impl/user/account/InfoServiceImpl.java | 34 ++ .../impl/user/account/LoginServiceImpl.java | 50 +++ .../user/account/RegisterServiceImpl.java | 85 ++++ .../service/impl/user/bot/AddServiceImpl.java | 78 ++++ .../impl/user/bot/GetListServiceImpl.java | 33 ++ .../impl/user/bot/RemoveServiceImpl.java | 53 +++ .../impl/user/bot/UpdateServiceImpl.java | 90 +++++ .../service/impl/utils/UserDetailsImpl.java | 61 +++ .../backend/service/pk/StartGameService.java | 7 + .../service/user/account/InfoService.java | 8 + .../service/user/account/LoginService.java | 8 + .../service/user/account/RegisterService.java | 9 + .../backend/service/user/bot/AddService.java | 7 + .../service/user/bot/GetListService.java | 9 + .../service/user/bot/RemoveService.java | 6 + .../service/user/bot/UpdateService.java | 7 + .../java/com/kob/backend/utils/JwtUtil.java | 74 ++++ .../src/main/resources/application.properties | 8 + .../src/main/resources/static/image/img.png | Bin 0 -> 43486 bytes .../main/resources/templates/pk/index.html | 12 + .../kob/backend/BackendApplicationTests.java | 18 + backendcloud/matchingsystem/pom.xml | 51 +++ .../MatchingSystemApplication.java | 15 + .../config/RestTemplateConfig.java | 13 + .../matchingsystem/config/SecurityConfig.java | 34 ++ .../controller/MatchingController.java | 33 ++ .../service/MatchingService.java | 10 + .../service/impl/MatchingServiceImpl.java | 28 ++ .../service/impl/utils/MatchingPool.java | 140 +++++++ .../service/impl/utils/Player.java | 15 + .../src/main/resources/application.properties | 1 + backendcloud/mvnw | 316 +++++++++++++++ backendcloud/mvnw.cmd | 188 +++++++++ backendcloud/pom.xml | 57 +++ 66 files changed, 3086 insertions(+) create mode 100644 backendcloud/.gitignore create mode 100644 backendcloud/.mvn/wrapper/maven-wrapper.jar create mode 100644 backendcloud/.mvn/wrapper/maven-wrapper.properties create mode 100644 backendcloud/backend/pom.xml create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/BackendApplication.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/config/CorsConfig.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/config/RestTemplateConfig.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/config/SecurityConfig.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/config/WebSocketConfig.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Game.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthenticationUtil.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Player.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/pk/StartGameController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/UserController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/InfoController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/LoginController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/RegisterController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/AddController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/GetListController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/RemoveController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/UpdateController.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/mapper/BotMapper.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/mapper/RecordMapper.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/mapper/UserMapper.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/pojo/Bot.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/pojo/Record.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/pojo/User.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/UserDetailsServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/pk/StartGameServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/InfoServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/LoginServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/AddServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/GetListServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/RemoveServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/UpdateServiceImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/impl/utils/UserDetailsImpl.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/pk/StartGameService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/user/account/InfoService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/user/account/LoginService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/user/account/RegisterService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/AddService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/GetListService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/RemoveService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/UpdateService.java create mode 100644 backendcloud/backend/src/main/java/com/kob/backend/utils/JwtUtil.java create mode 100644 backendcloud/backend/src/main/resources/application.properties create mode 100644 backendcloud/backend/src/main/resources/static/image/img.png create mode 100644 backendcloud/backend/src/main/resources/templates/pk/index.html create mode 100644 backendcloud/backend/src/test/java/com/kob/backend/BackendApplicationTests.java create mode 100644 backendcloud/matchingsystem/pom.xml create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/MatchingSystemApplication.java create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/RestTemplateConfig.java create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/SecurityConfig.java create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/controller/MatchingController.java create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/MatchingService.java create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/MatchingServiceImpl.java create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/MatchingPool.java create mode 100644 backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/Player.java create mode 100644 backendcloud/matchingsystem/src/main/resources/application.properties create mode 100644 backendcloud/mvnw create mode 100644 backendcloud/mvnw.cmd create mode 100644 backendcloud/pom.xml diff --git a/backendcloud/.gitignore b/backendcloud/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/backendcloud/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/backendcloud/.mvn/wrapper/maven-wrapper.jar b/backendcloud/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..bf82ff01c6cdae4a1bb754a6e062954d77ac5c11 GIT binary patch literal 59925 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

dy!05UMhSKsK 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 VCNKd3JGtOvEm(5t0lFyE{{i--k}m)N literal 0 HcmV?d00001 diff --git a/backendcloud/.mvn/wrapper/maven-wrapper.properties b/backendcloud/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..ca5ab4b --- /dev/null +++ b/backendcloud/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,18 @@ +# 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. +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.7/apache-maven-3.8.7-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.1/maven-wrapper-3.1.1.jar diff --git a/backendcloud/backend/pom.xml b/backendcloud/backend/pom.xml new file mode 100644 index 0000000..9738650 --- /dev/null +++ b/backendcloud/backend/pom.xml @@ -0,0 +1,130 @@ + + + 4.0.0 + + com.kob + backendcloud + 0.0.1-SNAPSHOT + + + com.kob.backend + backend + + + 8 + 8 + UTF-8 + + + + + + + + org.springframework.boot + spring-boot-starter-jdbc + 3.0.2 + + + + + + org.projectlombok + lombok + 1.18.26 + provided + + + + + + com.mysql + mysql-connector-j + 8.0.32 + + + + + + com.baomidou + mybatis-plus-boot-starter + 3.5.3.1 + + + + + + com.baomidou + mybatis-plus-generator + 3.5.3.1 + + + + + + org.springframework.boot + spring-boot-starter-security + 3.0.2 + + + + + + io.jsonwebtoken + jjwt-api + 0.11.5 + + + + + io.jsonwebtoken + jjwt-impl + 0.11.5 + runtime + + + + + io.jsonwebtoken + jjwt-jackson + 0.11.5 + runtime + + + + + + org.springframework.boot + spring-boot-starter-websocket + 3.0.2 + + + + + + com.alibaba.fastjson2 + fastjson2 + 2.0.24 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + org.jetbrains + annotations + 13.0 + compile + + + + \ No newline at end of file diff --git a/backendcloud/backend/src/main/java/com/kob/backend/BackendApplication.java b/backendcloud/backend/src/main/java/com/kob/backend/BackendApplication.java new file mode 100644 index 0000000..4a7c2bf --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/BackendApplication.java @@ -0,0 +1,13 @@ +package com.kob.backend; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class BackendApplication { + + public static void main(String[] args) { + SpringApplication.run(BackendApplication.class, args); + } + +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/config/CorsConfig.java b/backendcloud/backend/src/main/java/com/kob/backend/config/CorsConfig.java new file mode 100644 index 0000000..c06a711 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/config/CorsConfig.java @@ -0,0 +1,44 @@ +package com.kob.backend.config; + +import org.springframework.context.annotation.Configuration; + +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +// 这个类用于解决CORS跨域问题 +@Configuration +public class CorsConfig implements Filter { + @Override + public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { + HttpServletResponse response = (HttpServletResponse) res; + HttpServletRequest request = (HttpServletRequest) req; + + String origin = request.getHeader("Origin"); + if (origin != null) { + response.setHeader("Access-Control-Allow-Origin", origin); + } + + String headers = request.getHeader("Access-Control-Request-Headers"); + if (headers != null) { + response.setHeader("Access-Control-Allow-Headers", headers); + response.setHeader("Access-Control-Expose-Headers", headers); + } + + response.setHeader("Access-Control-Allow-Methods", "*"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + + chain.doFilter(request, response); + } + + @Override + public void init(FilterConfig filterConfig) { + + } + + @Override + public void destroy() { + } +} \ No newline at end of file diff --git a/backendcloud/backend/src/main/java/com/kob/backend/config/RestTemplateConfig.java b/backendcloud/backend/src/main/java/com/kob/backend/config/RestTemplateConfig.java new file mode 100644 index 0000000..6d82461 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/config/RestTemplateConfig.java @@ -0,0 +1,18 @@ +// 需要用到哪些东西的时候,就定义一个它的 @Configuration 再加一个 @Bean 注解,返回它的实例即可 +// 在需要使用的类里面加上一个 @Autowired 注入即可 +// 向后端(匹配系统)发请求的工具,想取得什么,就加一个 Bean 注解 +package com.kob.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +// 加注解 Configuration +// RestTemplate 用于处理 Spring 服务之间的 http 请求 +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate getRestTemplate(){ + return new RestTemplate(); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/config/SecurityConfig.java b/backendcloud/backend/src/main/java/com/kob/backend/config/SecurityConfig.java new file mode 100644 index 0000000..6ca863f --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/config/SecurityConfig.java @@ -0,0 +1,64 @@ +package com.kob.backend.config; + +/* +主要作用:放行登录、注册等接口 +*/ + +import com.kob.backend.config.filter.JwtAuthenticationTokenFilter; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +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; + +// TODO: 2023/2/19 WebSecurityConfigurerAdapter 已被弃用,注意替换 +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + @Autowired + private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; + + // 配置密码加密方式 + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + /* + .antMatchers("/user/account/token/", "/user/account/register/").permitAll() + 用于配置公开链接 + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/user/account/token/", "/user/account/register/").permitAll() + .antMatchers("/pk/start/").hasIpAddress("127.0.0.1") + .antMatchers(HttpMethod.OPTIONS).permitAll() + .anyRequest().authenticated(); + + http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); + } + + // 用于配置 websocket:放行所有的 websocket/ 链接 + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers("/websocket/**"); + } +} \ No newline at end of file diff --git a/backendcloud/backend/src/main/java/com/kob/backend/config/WebSocketConfig.java b/backendcloud/backend/src/main/java/com/kob/backend/config/WebSocketConfig.java new file mode 100644 index 0000000..f4a09f9 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/config/WebSocketConfig.java @@ -0,0 +1,15 @@ +package com.kob.backend.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import org.springframework.web.socket.server.standard.ServerEndpointExporter; + + +@Configuration +public class WebSocketConfig { + @Bean + public ServerEndpointExporter serverEndpointExporter() { + return new ServerEndpointExporter(); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java b/backendcloud/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..670d63d --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/config/filter/JwtAuthenticationTokenFilter.java @@ -0,0 +1,73 @@ +package com.kob.backend.config.filter; + +//filter 类用于验证 +/* +主要作用: + 1. 用来验证jwt token,如果验证成功,则将User信息注入上下文中 +*/ + +import com.kob.backend.mapper.UserMapper; +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +import com.kob.backend.utils.JwtUtil; +import com.sun.xml.internal.bind.v2.TODO; +import io.jsonwebtoken.Claims; +import org.jetbrains.annotations.NotNull; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; +import org.springframework.web.filter.OncePerRequestFilter; + +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 JwtAuthenticationTokenFilter extends OncePerRequestFilter { + @Autowired + private UserMapper userMapper; + + @Override + protected void doFilterInternal(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException { + String token = request.getHeader("Authorization"); + +// TODO 这里可以更改 bearar 为其他字符作为验证前缀 + if (!StringUtils.hasText(token) || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + token = token.substring(7); + + + +// 核心验证逻辑 + String userid; + try { +// 将 token 解析,如果能成功解析出 userid 表示合法,否则表示不合法 + Claims claims = JwtUtil.parseJWT(token); + userid = claims.getSubject(); + } catch (Exception e) { + throw new RuntimeException(e); + } + + User user = userMapper.selectById(Integer.parseInt(userid)); + +// User user = userMapper.selectById(new JwtAuthenticationUtil().getUserId(token)); + if (user == null) { + throw new RuntimeException("用户名未登录"); + } + + UserDetailsImpl loginUser = new UserDetailsImpl(user); + UsernamePasswordAuthenticationToken authenticationToken = + new UsernamePasswordAuthenticationToken(loginUser, null, null); + + SecurityContextHolder.getContext().setAuthentication(authenticationToken); + + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/backendcloud/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java b/backendcloud/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java new file mode 100644 index 0000000..69043ca --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/consumer/WebSocketServer.java @@ -0,0 +1,231 @@ +package com.kob.backend.consumer; + +// WebSocket用于前后端通信 + +import com.alibaba.fastjson2.JSONObject; +import com.kob.backend.config.RestTemplateConfig; +import com.kob.backend.consumer.utils.Game; +import com.kob.backend.consumer.utils.JwtAuthenticationUtil; +import com.kob.backend.mapper.RecordMapper; +import com.kob.backend.mapper.UserMapper; +import com.kob.backend.pojo.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.web.WebProperties; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import javax.websocket.*; +import javax.websocket.server.PathParam; +import javax.websocket.server.ServerEndpoint; +import javax.xml.stream.events.StartDocument; +import java.io.IOException; +import java.util.Iterator; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArraySet; + +@Component +@ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾 +public class WebSocketServer { + /* + 存储所有链接:对所有 websocket 可见的全局变量,存储为 static 静态变量 + 因为每个 websocket 实例都在一个独立的线程里,所以该公共变量应该是线程安全的 + 使用线程安全的 Hash 表 ConcurrentHashMap<>(), + 将 userId 映射到 webSocketServer + */ + public static final ConcurrentHashMap users = new ConcurrentHashMap<>(); + // 用于和 MatchingSystem 进行通信 + private static RestTemplate restTemplate; + // 在 WebSocketServer 中注入数据库的方法演示-> 使用 static 定义为独一份的变量 + private static UserMapper userMapper; + // 注入 RecordMapper 用于调用实现游戏数据到数据库的存储 + public static RecordMapper recordMapper; + // 后端向前端发送信息,首先需要创建一个 session + private Session session = null; + // 用户信息:定义一个成员变量 + private User user; + private Game game = null; + // addPlayer 添加用户到匹配池的 URL; removePlayer 从匹配池移除用户的 URL + private final static String addPlayerUrl = "http://127.0.0.1:3001/player/add/"; + private final static String removePlayerUrl = "http://127.0.0.1:3001/player/remove/"; + + @Autowired + public void setRestTemplate(RestTemplate restTemplate) { + // 注入 RestTemplate:作用是微服务之间发送请求 + WebSocketServer.restTemplate = restTemplate; + } + + // 注入方法 + @Autowired + public void setUserMapper(UserMapper userMapper) { + // 静态变量 userMapper 访问需要使用类名 WebSocketServer 访问 + WebSocketServer.userMapper = userMapper; + } + + @Autowired + public void setRecordMapper(RecordMapper recordMapper) { + WebSocketServer.recordMapper = recordMapper; + } + + + // @OnOpen 创建链接时自动触发这个函数 + @OnOpen + public void onOpen(Session session, @PathParam("token") String token) throws IOException { + // 建立链接时需要将 session 存下来 + this.session = session; + // 成功建立连接时,输出 connected! + System.out.println("backend connected!"); + // 将 userId 取出来,通过 userId 将 user 找出来 + // int userId = Integer.parseInt(token); + int userId = JwtAuthenticationUtil.getUserId(token); + this.user = userMapper.selectById(userId); + + // 如果 user 存在, 表示用户登录成功, 用户信息是存在的 + if (this.user != null) { + // 将 user 存到 users HashMap里 + users.put(userId, this); + + // (测试)后台输出看用户信息 + // System.out.println(user); + } + // 否则,断开连接(这里需要抛出异常) + else { + this.session.close(); + } + + + } + + @OnClose + public void onClose() { + // 关闭链接 + System.out.println("backend disconnected!"); + // 断开连接时,需要将 user 从 users 里面删掉 + if (this.user != null) { + users.remove(this.user.getId()); + } + } + + // 匹配逻辑函数 + public static void startGame(Integer aId, Integer bId) { + User a = userMapper.selectById(aId); + User b = userMapper.selectById(bId); + + // 匹配成功时,创建联机地图 + Game game = new Game(13, 14, 20, a.getId(), b.getId()); + game.createMap(); // 初始化地图 + // 当用户不为空时,才能执行下面的操作(防止有人退出游戏后用户已经成为空指针) + if (users.get(a.getId()) != null) + users.get(a.getId()).game = game; // 将 game 赋给 a 玩家 + + if (users.get(b.getId()) != null) + users.get(b.getId()).game = game; + + game.start(); // 开启新线程,执行函数 + + JSONObject respGameData = new JSONObject(); + respGameData.put("game_map", game.getG()); + respGameData.put("rows", game.getRows()); + respGameData.put("cols", game.getCols()); + respGameData.put("inner_walls_count", game.getInnerWallsCount()); + respGameData.put("a_id", game.getPlayerA().getId()); + respGameData.put("a_sx", game.getPlayerA().getSx()); + respGameData.put("a_sy", game.getPlayerA().getSy()); + respGameData.put("b_id", game.getPlayerB().getId()); + respGameData.put("b_sx", game.getPlayerB().getSx()); + respGameData.put("b_sy", game.getPlayerB().getSy()); + + // 将 a 配对成功的消息传回客户端 + JSONObject respA = new JSONObject(); + respA.put("event", "start-matching"); + respA.put("opponent_username", b.getUsername()); + respA.put("opponent_photo", b.getPhoto()); + respA.put("game_data", respGameData); + // 获取 a 的链接,并通过 sendMessage 将消息传给前端 + if (users.get(a.getId()) != null) + users.get(a.getId()).sendMessage(respA.toJSONString()); + + // 同理,将 b 的匹配成功信息传回给前端 + JSONObject respB = new JSONObject(); + respB.put("event", "start-matching"); + respB.put("opponent_username", a.getUsername()); + respB.put("opponent_photo", a.getPhoto()); + respB.put("game_data", respGameData); + if (users.get(b.getId()) != null) + users.get(b.getId()).sendMessage(respB.toJSONString()); + } + + // 开始匹配的逻辑部分:开始匹配时向 MatchingSystem 服务端发一个请求 + private void startMatching() { + System.out.println("start matching"); + // 这里的参数列表需要与 MatchingSystem(微服务)->MatchingController->addPlayer 的参数对应 + MultiValueMap data = new LinkedMultiValueMap<>(); + data.add("user_id", this.user.getId().toString()); + data.add("rating", this.user.getRating().toString()); + // postForObject(请求的URL,传出的数据,返回值的类型.class) -> Java 反射机制 + String res = restTemplate.postForObject(addPlayerUrl, data, String.class); + System.out.println(this.user.getId().toString() + " " + this.user.getUsername() + " " + res); + } + + // 取消匹配的逻辑部分:向 MatchingSystem 发送请求:从匹配池移除一个用户 + private void stopMatching() { + System.out.println("stop matching"); + MultiValueMap data = new LinkedMultiValueMap<>(); + data.add("user_id", this.user.getId().toString()); + String res = restTemplate.postForObject(removePlayerUrl, data, String.class); + System.out.println(this.user.getId().toString() + " " + this.user.getUsername() + " " + res); + } + + // direction 传入 move(移动) 方向参数 + private void move(int direction) { + // 判断角色:如果是 A 角色,则将获取到的方向设置为 A 的 nextStep 方向 + // user.getId() 是获取当前链接的用户 id + if (game.getPlayerA().getId().equals(user.getId())) { + game.setNextStepA(direction); + } else if (game.getPlayerB().getId().equals(user.getId())) { + game.setNextStepB(direction); + } + } + + // @OnMessage 用于从前端接收请求: 一般 onMessage 当成路由使用,做为消息判断处理的中间部分 + @OnMessage + public void onMessage(String message, Session session) { + // 从 Client 接收消息 + System.out.println("receive message"); + // 解析从前端接收到的请求 + JSONObject data = JSONObject.parseObject(message); + String event = data.getString("event"); + // System.out.println(event); + + // 匹配状态函数调用 + if ("start-matching".equals(event)) { + startMatching(); + } else if ("stop-matching".equals(event)) { + stopMatching(); + } else if ("move".equals(event)) { + move(data.getInteger("direction")); + } + } + + @OnError + public void onError(Session session, Throwable error) { + error.printStackTrace(); + } + + // 从后端向前端发送信息 + public void sendMessage(String message) { + // 异步通信过程,先加一个锁 + synchronized (this.session) { + try { + // 将 message 发送到前端 + this.session.getBasicRemote().sendText(message); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + +} + + diff --git a/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java new file mode 100644 index 0000000..f74e2a3 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Cell.java @@ -0,0 +1,12 @@ +package com.kob.backend.consumer.utils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Cell { + int x, y; +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Game.java b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Game.java new file mode 100644 index 0000000..86a83a9 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Game.java @@ -0,0 +1,370 @@ +package com.kob.backend.consumer.utils; + +import com.alibaba.fastjson2.JSONObject; +import com.kob.backend.consumer.WebSocketServer; +import com.kob.backend.pojo.Record; + +import javax.swing.event.InternalFrameEvent; +import java.sql.Time; +import java.util.*; +import java.util.concurrent.locks.ReentrantLock; + +// 用来管理整个游戏流程:这里需要多线程 +public class Game extends Thread { + // 游戏地图:行数,列数,内部障碍物数量 + private final Integer rows; + private final Integer cols; + private final Integer inner_walls_count; + // 地图数组 + private final int[][] g; + // 定义"上右下左"四个方向的 dx, dy偏移量 + private final int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1}; + private final Player playerA, playerB; + // 玩家下一步操作状态定义 + private Integer nextStepA = null; + private Integer nextStepB = null; + // 加锁:解决读写冲突用 + private ReentrantLock lock = new ReentrantLock(); + // 整个游戏的当前状态: playing(正在进行) --> finished(游戏结束) + private String status = "playing"; + // 定义失败者: all(平局), A(A输), B(B输) + private String loser = ""; + + + // 初始化(有参)构造函数 + public Game(Integer rows, Integer cols, Integer inner_walls_count, Integer idA, Integer idB) { + this.rows = rows; + this.cols = cols; + this.inner_walls_count = inner_walls_count; + this.g = new int[rows][cols]; + playerA = new Player(idA, rows - 2, 1, new ArrayList<>()); + playerB = new Player(idB, 1, cols - 2, new ArrayList<>()); + } + + public Player getPlayerA() { + return playerA; + } + + public Player getPlayerB() { + return playerB; + } + + public int getRows() { + return rows; + } + + public int getCols() { + return cols; + } + + public int getInnerWallsCount() { + return inner_walls_count; + } + + private String getGameMapString() { + StringBuilder res = new StringBuilder(); + // 将地图数据展开成一维 +/* for (int i = 0; i < rows; i++) { + for (int j = 0; j < cols; j++) { + res.append(g[i][j]); + } + }*/ + for (int[] row : g) { + for (int col : row) { + res.append(col); + } + } + + return res.toString(); + } + + public void setNextStepA(Integer nextStepA) { + // 为了防止两方读写冲突,需要加线程锁之后操作 nextStep 值 + lock.lock(); + try { + this.nextStepA = nextStepA; + } finally { + // 操作完成后解锁 + lock.unlock(); + } + } + + public void setNextStepB(Integer nextStepB) { + lock.lock(); + try { + this.nextStepB = nextStepB; + } finally { + lock.unlock(); + } + } + + // 返回生成的地图 + public int[][] getG() { + return g; + } + + // 联通检测方法---true(联通)---false(不通),参数: 起点坐标 sx,sy ,终点坐标 tx,ty + private boolean check_connectivity(int sx, int sy, int tx, int ty) { + // 起点就是终点时,结果联通,直接返回 true + if (sx == tx && sy == ty) return true; + g[sx][sy] = 1; + + //枚举"上右下左"四个方向,求当前点下一个相邻点的坐标 + for (int i = 0; i < 4; i++) { + int x = sx + dx[i]; + int y = sy + dy[i]; + + // 判断是否撞到障碍物( g[x][y] == 0 表示没有碰到障碍物 ),如果没有赚到障碍物,且可以找到重点的话,返回 true(联通) + if (x >= 0 && x < this.rows && y >= 0 && y < this.cols && g[x][y] == 0) { + if (check_connectivity(x, y, tx, ty)) { + // 还原状态 + g[sx][sy] = 0; + return true; + } + } + } + + // 还原状态 + g[sx][sy] = 0; + return false; + } + + // 画地图 + public boolean draw() { + // 一开始现将所有障碍物初始化为 false + for (int r = 0; r < this.rows; r++) { + for (int c = 0; c < this.cols; c++) { + // 0 表示空地, 1 表示障碍物(墙) + g[r][c] = 0; + } + } + + // 地图左右边缘障碍物 + for (int r = 0; r < this.rows; r++) { + g[r][0] = g[r][this.cols - 1] = 1; + } + // 地图上下边缘障碍物 + for (int c = 0; c < this.cols; c++) { + g[0][c] = g[this.rows - 1][c] = 1; + } + + Random random = new Random(); + // 创建内部随机障碍物,每次计算时会生成两个障碍物,因此这里的循环次数 this.inner_walls_count 需要处以 2 + for (int i = 0; i < this.inner_walls_count / 2; i++) { + // 避免位置重复:重复遍历 1000 次,只要找到了已经存在障碍物的位置就禁止随机 + for (int j = 0; j < 1000; j++) { + // 找出本次随机到的行-r 列-c 坐标 + int r = random.nextInt(this.rows); // random.nextInt(7) 返回 0-7之间的随机整数 + int c = random.nextInt(this.cols); + + // 中心对称:当本坐标或者它的中心对称坐标已经存在障碍物了,则重新计算下一个坐标 + if (g[r][c] == 1 || g[this.rows - 1 - r][this.cols - 1 - c] == 1) + continue; + + // 避免内部障碍物覆盖掉左下角和右上角的 A-B 角色出发点,如果是这两个位置,则重新计算下一个坐标 + if (r == this.rows - 2 && c == 1 || r == 1 && c == this.cols - 2) + continue; + + // 将计算求得的随机障碍物合法的位置赋值为 1 ,以对该位置进行绘制(包括本坐标及其中心对称坐标) + g[r][c] = g[this.rows - 1 - r][this.cols - 1 - c] = 1; + + // 1000 次遍历中,规定数量的内部障碍物已经够数之后就 break 掉 + break; + } + } + + // 确保 A-B 角色的运动区域是联通的:如果不连通,则直接在创建地图对象之前取消绘制( return false ) + return check_connectivity(this.rows - 2, 1, 1, this.cols - 2); + } + + public void createMap() { + // 循环绘制:如果发现哪次循环中画地图成功了,则跳出循环,绘制结束 + for (int i = 0; i < 1000; i++) { + if (draw()) + break; + } + } + + // 获取两名玩家的下一步操作 + private boolean nextStep() { + try { + // 返回下一步操作之前,先睡眠 200 毫秒,用于等待前端渲染完成,避免渲染速度跟不上后端运算处理速度 + // 前端定义了每 1 秒钟 5 个格子,则每 200 毫秒走一格(每一步至少需要 200 毫秒的时间) + Thread.sleep(200); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + + // 循环 50 秒钟,每次 100 毫秒,判断用户输入(50 * 100 = 5000 ms后如果没有接收到输入,则判断输赢) + for (int i = 0; i < 50; i++) { + try { + // 两次接收输入的等待间隔 + Thread.sleep(100); + lock.lock(); + try { + // 判断两名玩家是否都已经读取到输入:如果读入都非空,则表示读取结束,返回 true + if (nextStepA != null && nextStepB != null) { + // 将下一步操作存下来 + playerA.getSteps().add(nextStepA); + playerB.getSteps().add(nextStepB); + + // 成功获取到两名玩家的下一步操作,返回 true + return true; + } + } finally { + lock.unlock(); + } + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + + return false; + } + + // 碰撞合法性检测辅助函数:需要判断 snakeA 和 snakeB 是否重合 + private boolean checkValid(List cellsA, List cellsB) { + int n = cellsA.size(); // cellsA 的当前长度 + Cell cell = cellsA.get(n - 1); // 取出 cellsA 的蛇头位置(数组最后一位)(第0个元素是蛇尾巴) + if (g[cell.x][cell.y] == 1) return false; // 判断 A 的最后一位(蛇头)的坐标是否是障碍物(值为1),如果是障碍物则返回false(判断已碰撞) + + // 判断 A蛇头位置是否和A除了蛇头以外的其他身体部分坐标有重合 + for (int i = 0; i < n - 1; i++) { + if (cellsA.get(i).x == cell.x && cellsA.get(i).y == cell.y) + return false; + } + for (Cell cellB : cellsB) { // 判断 A蛇头位置是否和B蛇身体各部分坐标有重合:有重合返回不合法(表示已经碰撞) + if (cellB.x == cell.x && cellB.y == cell.y) + return false; + } + + // 以上判断条件如果都合法,则返回 true合法(表示没有碰撞) + return true; + } + + // 判断两名玩家下一步操作是否合法 + private void judge() { + // 取出 A,B 两蛇 + List cellsA = playerA.getCells(); + List cellsB = playerB.getCells(); + + boolean validA = checkValid(cellsA, cellsB); + boolean validB = checkValid(cellsB, cellsA); + + // 如果 A 和 B 有人这一步不合法(已经产生碰撞),将游戏状态改为 "finished",表示游戏结束 + if (!validA || !validB) { + status = "finished"; + + // 游戏结束时,判断输赢 + if (!validA && !validB) { + loser = "all"; + } else if (!validA && validB) { + loser = "A"; + } else if (validA && !validB) { + loser = "B"; + } + } + } + + // 向两个 Client 广播信息 + private void sendAllMessage(String message) { + if (WebSocketServer.users.get(playerA.getId()) != null) + WebSocketServer.users.get(playerA.getId()).sendMessage(message); + if (WebSocketServer.users.get(playerB.getId()) != null) + WebSocketServer.users.get(playerB.getId()).sendMessage(message); + } + + // 向两个 Client 传递移动信息 + private void sendMove() { + //由于这里需要读入 nextStepA, nextStepB, 这里需要加一下线程锁 + lock.lock(); + try { + JSONObject resp = new JSONObject(); + resp.put("event", "move"); // event(事件类型): move(移动信息) + resp.put("a_direction", nextStepA); // a_direction( playerA 移动的方向) + resp.put("b_direction", nextStepB); // b_direction( playerB 移动的方向) + sendAllMessage(resp.toJSONString()); + // 本次获取完下一步操作的同时需要进行再下一步操作输入,在这之前,需要将当前的 nextStep 清空 + nextStepA = nextStepB = null; + } finally { + lock.unlock(); + } + } + + // 将游戏结果存到数据库中 + private void saveToDataBase() { + Record record = new Record( + null, + playerA.getId(), + playerA.getSx(), + playerA.getSy(), + playerB.getId(), + playerB.getSx(), + playerB.getSy(), + playerA.getStepsString(), + playerB.getStepsString(), + getGameMapString(), + loser, + new Date() + ); + + WebSocketServer.recordMapper.insert(record); + } + + + // 向两个 Client 公布结果 + private void sendResult() { + lock.lock(); + try { + JSONObject resp = new JSONObject(); + resp.put("event", "result"); // event(事件类型): result(结果) + resp.put("loser", loser); // 失败者 + // 将最后碰撞时的蛇的眼睛指向传给前端 + resp.put("a_eyes_finally_direction", nextStepA); + resp.put("b_eyes_finally_direction", nextStepB); + // 发送结果之前,先将结果存到数据库中 + saveToDataBase(); + sendAllMessage(resp.toJSONString()); + } finally { + lock.unlock(); + } + } + + // 重写线程函数 + @Override + public void run() { + // 循环 1000 步: 13格x14格x(最大)3步/格 < 1000,保证 1000 次一定可以走完 + for (int i = 0; i < 1000; i++) { + if (nextStep()) { // 是否获取到两条蛇的下一步操作 + // 判断下一步操作是否合法 + judge(); + + // 如果游戏状态还是 playing,则向两个玩家广播移动信息 + if ("playing".equals(status)) { + sendMove(); + } else if ("finished".equals(status)) { + // 如果游戏状态已经更改为了 finished,则将结果返回给 Client 并 break 中断执行后续操作 + sendResult(); + break; + } + + } else { + // 未获取到某蛇的下一步操作,则将游戏状态改为 finished + status = "finished"; + // 判断失败者:这里需要加锁,因为涉及到对 nextStep 的读操作 + lock.lock(); + try { + if (nextStepA == null && nextStepB == null) { + loser = "all"; + } else if (nextStepA == null) { + loser = "A"; + } else { + loser = "B"; + } + } finally { + lock.unlock(); + } + } + } + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthenticationUtil.java b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthenticationUtil.java new file mode 100644 index 0000000..2606ae7 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/JwtAuthenticationUtil.java @@ -0,0 +1,20 @@ +package com.kob.backend.consumer.utils; + +import com.kob.backend.utils.JwtUtil; +import io.jsonwebtoken.Claims; + +//Jwt 验证配置工具类 +public class JwtAuthenticationUtil { + public static Integer getUserId(String token) { + // 默认 userid 赋值为 -1 ,表示不存在 + int userId = -1; + try { + // 将 token 解析,如果能成功解析出 userid 表示合法,否则表示不合法 + Claims claims = JwtUtil.parseJWT(token); + userId = Integer.parseInt(claims.getSubject()); + } catch (Exception e) { + throw new RuntimeException(e); + } + return userId; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Player.java b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Player.java new file mode 100644 index 0000000..66be0ff --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/consumer/utils/Player.java @@ -0,0 +1,61 @@ +package com.kob.backend.consumer.utils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.ArrayList; +import java.util.List; + +/// 玩家信息 +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Player { + private Integer id; + private Integer sx; + private Integer sy; + // 玩家走过的路径中每一步的方向 + private List steps; + + // 检验当前回合蛇的长度(蛇尾)是否需要增加 + public boolean checkTailIncreasing(int step) { + // 前 10 回合每次都增加一节蛇尾,后面每 3 回合增加一节蛇尾 + if (step <= 10) return true; + if (step % 3 == 1) return true; + + return false; + } + + // 蛇身 Cell 列表:记录蛇身各部分坐标 + public List getCells() { + List res = new ArrayList<>(); + + // 四方向偏移量 + int[] dx = {-1, 0, 1, 0}, dy = {0, 1, 0, -1}; + int x = sx, y = sy; // 起始坐标 + int step = 0; // 定义当前回合数: 开始时为 0 回合 + res.add(new Cell(x, y)); // 现将蛇头添加进 List + for (int d : steps) { // 使用下一步方向求出下一个结点的坐标 + x += dx[d]; + y += dy[d]; + res.add(new Cell(x, y)); // 添加新的身体结点 + step++; // 回合数 +1 + // 判断蛇尾要不要增加:如果本回合蛇尾不增加,则将本回合新生成的蛇尾(列表的第0个元素)删掉 + if (!checkTailIncreasing(step)) { + res.remove(0); + } + } + return res; + } + + // steps 转换为 string 的辅助函数 + public String getStepsString() { + StringBuilder res = new StringBuilder(); + for (int d : steps) { + res.append(d); + } + + return res.toString(); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/pk/StartGameController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/pk/StartGameController.java new file mode 100644 index 0000000..c49ad2d --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/pk/StartGameController.java @@ -0,0 +1,25 @@ +package com.kob.backend.controller.pk; + + +import com.kob.backend.service.pk.StartGameService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Objects; + +@RestController +public class StartGameController { + @Autowired + private StartGameService startGameService; + + + @PostMapping("/pk/start/") + public String startGame(@RequestParam MultiValueMap data) { + Integer aId = Integer.parseInt(Objects.requireNonNull(data.getFirst("a_id"))); + Integer bId = Integer.parseInt(Objects.requireNonNull(data.getFirst("b_id"))); + return startGameService.startGame(aId, bId); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/UserController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/UserController.java new file mode 100644 index 0000000..c3b562e --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/UserController.java @@ -0,0 +1,72 @@ +package com.kob.backend.controller.user; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.kob.backend.mapper.UserMapper; +import com.kob.backend.pojo.User; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +//实现对用户类 User 的增删查操作->这个类仅用于调试,故 Deparecated 掉 +@Deprecated +@RestController +public class UserController { + +// 用到数据库里的 Mapper 时,需要加 @Autowired 注解 + @Autowired + UserMapper userMapper; + +// 返回所有用户 + @GetMapping("/user/all") + public List getAll() { +// null 表示查询所有的 + return userMapper.selectList(null); + } + +/* + @GetMapping("/user/{userId}/") + public User getUser(@PathVariable int userId) { +// 查询某个用户 +// return userMapper.selectById(userId); + +// 封装查询语句:查询某个 id 的用户 + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("id", userId); + return userMapper.selectOne(queryWrapper); + } +*/ + + @GetMapping("/user/{userId}") + public List getUser(@PathVariable int userId) { +// 封装查询语句:查询某个范围 id 的用户: ge 是大于等于, gt 是大于, le 是小于等于, lt 是小于 + QueryWrapper queryWrapper1 = new QueryWrapper<>(); + queryWrapper1.ge("id", 2).le("id", 4); + return userMapper.selectList(queryWrapper1); + } + + @GetMapping("/user/add/{userId}/{username}/{password}") + public String addUser(@PathVariable int userId, + @PathVariable String username, + @PathVariable String password) { + if(password.length() < 6){ + return "密码少于6位,请重设密码"; + } +// 加密明文密码并存入密文到数据库 + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + String encodedPassword = passwordEncoder.encode(password); + User user = new User(userId, username, encodedPassword,"",null); + userMapper.insert(user); + return "Add User Successfully"; + } + + @GetMapping("/user/delete/{userId}") + public String deleteUser(@PathVariable int userId){ + userMapper.deleteById(userId); + return "Delete User Successfully"; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/InfoController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/InfoController.java new file mode 100644 index 0000000..e41cc7b --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/InfoController.java @@ -0,0 +1,20 @@ +package com.kob.backend.controller.user.account; + +import com.kob.backend.service.user.account.InfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +public class InfoController { + @Autowired + private InfoService infoService; + +// 获取信息 + @GetMapping("/user/account/info/") + public Map getInfo(){ + return infoService.getInfo(); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/LoginController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/LoginController.java new file mode 100644 index 0000000..60acf37 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/LoginController.java @@ -0,0 +1,27 @@ +package com.kob.backend.controller.user.account; + +import com.kob.backend.service.user.account.LoginService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +//@RestController 配合 @PostMapping("/.../.../") 使用:映射 Url 地址 +@RestController +public class LoginController { +// @Autowired 注入接口 + @Autowired + private LoginService loginService; + +// 登录使用 POST 请求,密文传输,更安全 + @PostMapping("/user/account/token/") + public Map getToken(@RequestParam Map map){ + String username = map.get("username"); + String password = map.get("password"); + + return loginService.getToken(username,password); + } + +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/RegisterController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/RegisterController.java new file mode 100644 index 0000000..1ecf8dc --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/account/RegisterController.java @@ -0,0 +1,24 @@ +package com.kob.backend.controller.user.account; + +import com.kob.backend.service.user.account.RegisterService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +public class RegisterController { + @Autowired + private RegisterService registerService; + + @PostMapping("/user/account/register/") + public Map register(@RequestParam Map map) { + String username = map.get("username"); + String password = map.get("password"); + String confirmedPassword = map.get("confirmedPassword"); + return registerService.register(username, password, confirmedPassword); + } + +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/AddController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/AddController.java new file mode 100644 index 0000000..2a958aa --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/AddController.java @@ -0,0 +1,18 @@ +package com.kob.backend.controller.user.bot; + +import com.kob.backend.service.user.bot.AddService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +import java.util.Map; + +@RestController +public class AddController { + @Autowired + private AddService addService; + + @PostMapping("/user/bot/add/") + public Map add(@RequestParam Map data){ + return addService.add(data); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/GetListController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/GetListController.java new file mode 100644 index 0000000..69c4848 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/GetListController.java @@ -0,0 +1,22 @@ +package com.kob.backend.controller.user.bot; + +import com.kob.backend.pojo.Bot; +import com.kob.backend.service.user.bot.GetListService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; +import java.util.Map; + +@RestController +public class GetListController { + @Autowired + private GetListService getListService; + + @GetMapping("/user/bot/getlist/") + public List getList(){ + return getListService.getList(); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/RemoveController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/RemoveController.java new file mode 100644 index 0000000..630f709 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/RemoveController.java @@ -0,0 +1,20 @@ +package com.kob.backend.controller.user.bot; + +import com.kob.backend.service.user.bot.RemoveService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +public class RemoveController { + @Autowired + private RemoveService removeService; + + @PostMapping("/user/bot/remove/") + public Map remove(@RequestParam Map data){ + return removeService.remove(data); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/UpdateController.java b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/UpdateController.java new file mode 100644 index 0000000..8ee59e8 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/controller/user/bot/UpdateController.java @@ -0,0 +1,20 @@ +package com.kob.backend.controller.user.bot; + +import com.kob.backend.service.user.bot.UpdateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +public class UpdateController { + @Autowired + private UpdateService updateService; + + @PostMapping("/user/bot/update/") + public Map update(@RequestParam Map data){ + return updateService.update(data); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/mapper/BotMapper.java b/backendcloud/backend/src/main/java/com/kob/backend/mapper/BotMapper.java new file mode 100644 index 0000000..51c3372 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/mapper/BotMapper.java @@ -0,0 +1,9 @@ +package com.kob.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.kob.backend.pojo.Bot; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface BotMapper extends BaseMapper { +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/mapper/RecordMapper.java b/backendcloud/backend/src/main/java/com/kob/backend/mapper/RecordMapper.java new file mode 100644 index 0000000..4541ca4 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/mapper/RecordMapper.java @@ -0,0 +1,9 @@ +package com.kob.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.kob.backend.pojo.Record; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface RecordMapper extends BaseMapper { +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/mapper/UserMapper.java b/backendcloud/backend/src/main/java/com/kob/backend/mapper/UserMapper.java new file mode 100644 index 0000000..71646a8 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/mapper/UserMapper.java @@ -0,0 +1,11 @@ +//用于将 class 的操作转化成 sql 语句 +package com.kob.backend.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.kob.backend.pojo.User; +import org.apache.ibatis.annotations.Mapper; + +@Mapper +public interface UserMapper extends BaseMapper { + +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/pojo/Bot.java b/backendcloud/backend/src/main/java/com/kob/backend/pojo/Bot.java new file mode 100644 index 0000000..eade9f0 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/pojo/Bot.java @@ -0,0 +1,35 @@ +// 用于将 Mysql 表 Bot 转换为 class +// 数据库中 user_id 这种在 pojo 里要命名为 userId +package com.kob.backend.pojo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Bot { + // 主键自增 + @TableId(type = IdType.AUTO) + private Integer id; + + // 注意:在pojo中需要定义成userId,在queryWrapper中的名称仍然为user_id + private Integer userId; + private String title; + private String description; + private String content; + // pojo中定义日期格式的注解:@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + // timezone 用于修改时区 + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createtime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date modifytime; + +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/pojo/Record.java b/backendcloud/backend/src/main/java/com/kob/backend/pojo/Record.java new file mode 100644 index 0000000..dd00e62 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/pojo/Record.java @@ -0,0 +1,34 @@ +package com.kob.backend.pojo; + +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.util.Date; +import java.util.Timer; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Record { + @TableId(type = IdType.AUTO) + private Integer id; + + private Integer aId; + private Integer aSx; + private Integer aSy; + private Integer bId; + private Integer bSx; + private Integer bSy; + + private String aSteps; + private String bSteps; + private String map; + private String loser; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") + private Date createtime; +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/pojo/User.java b/backendcloud/backend/src/main/java/com/kob/backend/pojo/User.java new file mode 100644 index 0000000..6251019 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/pojo/User.java @@ -0,0 +1,30 @@ +//用于将 mysql 表 User 转换为 class +package com.kob.backend.pojo; + +/* +Data 用于编译时自动生成 getter setter 方法; +NoArgsConstructor 用于生成无参构造函数; +AllArgsConstructor 用于生成所有参数构造函数. +*/ +import com.baomidou.mybatisplus.annotation.IdType; +import com.baomidou.mybatisplus.annotation.TableId; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class User { + +// 使用对象类型定义而不是 Int 防止 Mybatis 报错 +// @TableId(type = IdType.AUTO) 用于 id 的自增 + @TableId(type = IdType.AUTO) + private Integer id; + private String username; + private String password; + private String photo; + + private Integer rating; + +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/UserDetailsServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/UserDetailsServiceImpl.java new file mode 100644 index 0000000..223512a --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/UserDetailsServiceImpl.java @@ -0,0 +1,35 @@ +package com.kob.backend.service.impl; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.kob.backend.mapper.UserMapper; +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +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; + +//自定义 Spring-Starter-Security 登录信息; @Autowired 需要在外层加入 @Service 注解 +@Service +public class UserDetailsServiceImpl implements UserDetailsService { + + @Autowired + private UserMapper userMapper; + @Override + public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { +/* + 从数据库里读取用户信息,比对用户名密码与数据库存在的用户名密码,信息一致,给用户发一个 sessionID , + 存到用户本地浏览器,服务器同时会自己存一份。未来用户再登录时服务器会将自己的 sessionID 与用户发过来 + 请求包含的 sessionID 进行比照,如果一致,则授权成功。 +*/ + + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("username",username); + User user = userMapper.selectOne(queryWrapper); + if(user == null){ + throw new RuntimeException("用户不存在"); + } + return new UserDetailsImpl(user); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/pk/StartGameServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/pk/StartGameServiceImpl.java new file mode 100644 index 0000000..387e8bf --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/pk/StartGameServiceImpl.java @@ -0,0 +1,15 @@ +package com.kob.backend.service.impl.pk; + +import com.kob.backend.consumer.WebSocketServer; +import com.kob.backend.service.pk.StartGameService; +import org.springframework.stereotype.Service; + +@Service +public class StartGameServiceImpl implements StartGameService { + @Override + public String startGame(Integer aId, Integer bId) { + System.out.println("start game " + aId + " " + bId); + WebSocketServer.startGame(aId, bId); // 调用 WebSocketServer->startGame 函数 + return "start game success"; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/InfoServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/InfoServiceImpl.java new file mode 100644 index 0000000..119a3d4 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/InfoServiceImpl.java @@ -0,0 +1,34 @@ +package com.kob.backend.service.impl.user.account; + +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +import com.kob.backend.service.user.account.InfoService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + + +@Service +public class InfoServiceImpl implements InfoService { + @Override + public Map getInfo() { + UsernamePasswordAuthenticationToken authenticationToken = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + + UserDetailsImpl loginUser = (UserDetailsImpl) authenticationToken.getPrincipal(); + User user = loginUser.getUser(); + + Map map = new HashMap<>(); + map.put("error_message","success"); + map.put("id",user.getId().toString()); + map.put("username",user.getUsername()); + map.put("photo",user.getPhoto()); + + + return map; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/LoginServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/LoginServiceImpl.java new file mode 100644 index 0000000..19071fe --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/LoginServiceImpl.java @@ -0,0 +1,50 @@ +package com.kob.backend.service.impl.user.account; + +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +import com.kob.backend.service.user.account.LoginService; +import com.kob.backend.utils.JwtUtil; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +//@Service 注解用于注入接口实现到 Spring 里面 +@Service +public class LoginServiceImpl implements LoginService { + +// @Autowired 用于注入 + @Autowired + private AuthenticationManager authenticationManager; + + @Override + public Map getToken(String username, String password) { + +// 封装用户名和密码成 UsernamePasswordAuthenticationToken 类:此类不存储明文,而是存储加密之后的字符串 + UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username,password); + +// 验证是否正常登录,登录验证失败时会自动处理(报异常) + Authentication authenticate = authenticationManager.authenticate(authenticationToken); +// 登录成功,取出用户 + UserDetailsImpl loginUser = (UserDetailsImpl) authenticate.getPrincipal(); + User user = loginUser.getUser(); + +// 封装 jwt 信息:将 userID 转换为 String + String jwt = JwtUtil.createJWT(user.getId().toString()); + +/* + 成功之后返回结果: + 1. "error_message" 为 success;失败之后会报异常,自动处理掉 + 2. "token" 返回 jwt-token 信息 +*/ + Map map = new HashMap<>(); + map.put("error_message","success"); + map.put("token",jwt); + + return map; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java new file mode 100644 index 0000000..57de7c9 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/account/RegisterServiceImpl.java @@ -0,0 +1,85 @@ +package com.kob.backend.service.impl.user.account; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.kob.backend.pojo.User; +import com.kob.backend.mapper.UserMapper; +import com.kob.backend.service.user.account.RegisterService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; +import java.util.List; + +@Service +public class RegisterServiceImpl implements RegisterService { + // 这里使用 @Autowired 注入是为了调用数据库,然后进行数据库查询,比较是否有用户名重复 + @Autowired + private UserMapper userMapper; + + @Override + public Map register(String username, String password, String confirmedPassword) { + Map map = new HashMap<>(); +// 如果用户名为空,则提示用户返回 + if (username == null) { + map.put("error_message", "用户名不能为空"); + return map; + } + if (password == null || confirmedPassword == null) { + map.put("error_message", "密码不能为空"); + return map; + } +// 用户名需要将输入的首尾空格删掉 + username = username.trim(); + if (username.length() == 0) { + map.put("error_message", "用户名不能为空"); + return map; + } + if (password.length() == 0) { + map.put("error_message", "密码不能为空"); + return map; + } + if (username.length() > 100) { + map.put("error_message", "用户名过长"); + return map; + } + + + +/* + 查询数据库里是否有用户名 this.username 已存在的用户,并将结果存入 users 中, + 如果发现 users 不是空的,则告诉注册用户当前用户名已存在 +*/ + QueryWrapper queryWrapper = new QueryWrapper(); + queryWrapper.eq("username", username); + List users = userMapper.selectList(queryWrapper); + if (!users.isEmpty()) { + map.put("error_message", "用户名已存在"); + return map; + } + // 密码验证是 String 类型比较,应该用 equals() 方法 + else if (!confirmedPassword.equals(password)) { + map.put("error_message", "两次密码输入不一致"); + return map; + } else if (password.length() < 2) { + map.put("error_message", "密码不能少于2位"); + return map; + } else if (password.length() > 100) { + map.put("error_message", "密码过长"); + return map; + } + +// 异常情况判断结束,开始将合法用户注册信息注入数据库 +// 对密码进行加密 + String encodedPassword = new BCryptPasswordEncoder().encode(password); +// 默认头像 + String photo = "https://typoraflykhan.oss-cn-beijing.aliyuncs.com/202302251824054.png"; +// id 是数据库自增,这里生成新用户只需要将 id 参数写为 null 即可 + User user = new User(null, username, encodedPassword, photo, 1500); + userMapper.insert(user); + + map.put("error_message", "successRegister"); + return map; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/AddServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/AddServiceImpl.java new file mode 100644 index 0000000..df1afa5 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/AddServiceImpl.java @@ -0,0 +1,78 @@ +package com.kob.backend.service.impl.user.bot; + +import com.kob.backend.mapper.BotMapper; +import com.kob.backend.pojo.Bot; +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +import com.kob.backend.service.user.bot.AddService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class AddServiceImpl implements AddService { + // 使用 @Autowired 注入接口,调用 BotMapper 来修改数据库 + @Autowired + private BotMapper botMapper; + + @Override + public Map add(Map data) { +// 取出 User + UsernamePasswordAuthenticationToken authenticationToken = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + UserDetailsImpl loginUser = (UserDetailsImpl) authenticationToken.getPrincipal(); + User user = loginUser.getUser(); + + String title = data.get("title"); + String description = data.get("description"); + String content = data.get("content"); + + Map map = new HashMap<>(); + + if (title == null || title.length() == 0) { + map.put("error_message", "标题不能为空"); + return map; + } + + if (title.length() > 100) { + map.put("error_message", "标题长度不能大于100"); + return map; + } + + if (description == null || description.length() == 0) { + // 提供默认描述信息 + description = "这个用户很懒,什么也没留下~"; + } + + if (description != null && description.length() > 300) { + map.put("error_message", "描述信息长度不能大于300"); + return map; + } + + if (content == null || content.length() == 0) { + map.put("error_message", "代码不能为空"); + return map; + } + + if (content.length() > 10000) { + map.put("error_message", "代码长度不能超过一万"); + return map; + } + +// 定义当前时间 + Date now = new Date(); +// 定义一个新 Bot ;创建时间和修改时间开始应该一样 + Bot bot = new Bot(null, user.getId(), title, description, content, now, now); + +// 将新建的 Bot 注入数据库中 + botMapper.insert(bot); + map.put("error_message", "success"); + + return map; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/GetListServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/GetListServiceImpl.java new file mode 100644 index 0000000..37e6bc9 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/GetListServiceImpl.java @@ -0,0 +1,33 @@ +package com.kob.backend.service.impl.user.bot; + +import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; +import com.kob.backend.mapper.BotMapper; +import com.kob.backend.pojo.Bot; +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +import com.kob.backend.service.user.bot.GetListService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class GetListServiceImpl implements GetListService { + @Autowired + private BotMapper botMapper; + + @Override + public List getList() { + UsernamePasswordAuthenticationToken authenticationToken = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + UserDetailsImpl loginUser = (UserDetailsImpl) authenticationToken.getPrincipal(); + User user = loginUser.getUser(); + + QueryWrapper queryWrapper = new QueryWrapper<>(); + queryWrapper.eq("user_id",user.getId()); + + return botMapper.selectList(queryWrapper); + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/RemoveServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/RemoveServiceImpl.java new file mode 100644 index 0000000..60f763f --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/RemoveServiceImpl.java @@ -0,0 +1,53 @@ +package com.kob.backend.service.impl.user.bot; + +import com.kob.backend.mapper.BotMapper; +import com.kob.backend.pojo.Bot; +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +import com.kob.backend.service.user.bot.RemoveService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.HashMap; +import java.util.Map; + +@Service +public class RemoveServiceImpl implements RemoveService { + @Autowired + private BotMapper botMapper; + + @Override + public Map remove(Map data) { +// 先取出当前用户信息 + UsernamePasswordAuthenticationToken authenticationToken = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + UserDetailsImpl loginUser = (UserDetailsImpl) authenticationToken.getPrincipal(); + User user = loginUser.getUser(); + +// 从用户输入参数判断要删除的 bot id + int bot_id = Integer.parseInt(data.get("bot_id")); + Bot bot = botMapper.selectById(bot_id); + +// 定义返回值 + Map map = new HashMap<>(); + +// 如果 bot 不存在 + if (bot == null) { + map.put("error_message", "Bot 不存在或已被删除"); + return map; + } + +// 如果 bot 的 user_id 不等于 User 的 id + if (!bot.getUserId().equals(user.getId())) { + map.put("error_message", "你无权删除别人的 bot"); + return map; + } + +// 判断成功后进行删除 + botMapper.deleteById(bot_id); + map.put("error_message", "success"); + return map; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/UpdateServiceImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/UpdateServiceImpl.java new file mode 100644 index 0000000..7894f0a --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/user/bot/UpdateServiceImpl.java @@ -0,0 +1,90 @@ +package com.kob.backend.service.impl.user.bot; + +import com.kob.backend.mapper.BotMapper; +import com.kob.backend.pojo.Bot; +import com.kob.backend.pojo.User; +import com.kob.backend.service.impl.utils.UserDetailsImpl; +import com.kob.backend.service.user.bot.UpdateService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.stereotype.Service; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +@Service +public class UpdateServiceImpl implements UpdateService { + + @Autowired + private BotMapper botMapper; + + @Override + public Map update(Map data) { +// 先获取当前用户,用于判断更新对象是否有权限 + UsernamePasswordAuthenticationToken authenticationToken = + (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); + UserDetailsImpl loginUser = (UserDetailsImpl) authenticationToken.getPrincipal(); + User user = loginUser.getUser(); + + int bot_id = Integer.parseInt(data.get("bot_id")); + + String title = data.get("title"); + String description = data.get("description"); + String content = data.get("content"); + + Map map = new HashMap<>(); + + if (title == null || title.length() == 0) { + map.put("error_message", "标题不能为空"); + return map; + } + if (title.length() > 100) { + map.put("error_message", "标题长度不能大于100"); + return map; + } + if (description == null || description.length() == 0) { + // 提供默认描述信息 + description = "这个用户很懒,什么也没留下~"; + } + if (description != null && description.length() > 300) { + map.put("error_message", "描述信息长度不能大于300"); + return map; + } + if (content == null || content.length() == 0) { + map.put("error_message", "代码不能为空"); + return map; + } + if (content.length() > 10000) { + map.put("error_message", "代码长度不能超过一万"); + return map; + } + + Bot bot = botMapper.selectById(bot_id); + + if (bot == null) { + map.put("error_message", "所查 bot 不存在或已被删除"); + return map; + } + + if (!bot.getUserId().equals(user.getId())) { + map.put("error_message", "你无权更改别人的 bot"); + return map; + } + + if (bot.getTitle().equals(title) && bot.getDescription().equals(description) && bot.getContent().equals(content)) { + map.put("error_message", "未作出修改"); + return map; + } + + Date now = new Date(); + Date createTime = bot.getCreatetime(); + + Bot newBot = new Bot(bot.getId(), user.getId(), title, description, content, createTime, now); + + botMapper.updateById(newBot); + map.put("error_message", "success"); + return map; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/impl/utils/UserDetailsImpl.java b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/utils/UserDetailsImpl.java new file mode 100644 index 0000000..aa04c18 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/impl/utils/UserDetailsImpl.java @@ -0,0 +1,61 @@ +package com.kob.backend.service.impl.utils; + +import com.kob.backend.pojo.User; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Collection; + +//实现 UserDetails 接口:需要加上 lombok 的注解,来自动生成构造函数 +@Data +@AllArgsConstructor +@NoArgsConstructor +public class UserDetailsImpl implements UserDetails { + +// 定义一个 pojo 的 User 类对象 + private User user; + + @Override + public Collection getAuthorities() { + return null; + } + +// 获取 pojo -> User 类对象 user 的用户密码 + @Override + public String getPassword() { + return user.getPassword(); + } + +// 获取 pojo -> User 类对象 user 的用户名 + @Override + public String getUsername() { + return user.getUsername(); + } + +// 用户账号是否没有过期 + @Override + public boolean isAccountNonExpired() { + return true; + } + +// 用户是否没有被锁定 + @Override + public boolean isAccountNonLocked() { + return true; + } + +// 用户授权是否没有过期 + @Override + public boolean isCredentialsNonExpired() { + return true; + } + +// 用户是否被启用 + @Override + public boolean isEnabled() { + return true; + } +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/pk/StartGameService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/pk/StartGameService.java new file mode 100644 index 0000000..3b06180 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/pk/StartGameService.java @@ -0,0 +1,7 @@ +// 匹配接口:配合 MatchingSystem 微服务进行使用 +package com.kob.backend.service.pk; + +public interface StartGameService { + // 参数是匹配的两位玩家的 id + String startGame(Integer aId, Integer bId); +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/InfoService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/InfoService.java new file mode 100644 index 0000000..9826259 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/InfoService.java @@ -0,0 +1,8 @@ +package com.kob.backend.service.user.account; + +import java.util.Map; + +//根据令牌返回用户信息 +public interface InfoService { + public Map getInfo(); +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/LoginService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/LoginService.java new file mode 100644 index 0000000..16b813c --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/LoginService.java @@ -0,0 +1,8 @@ +package com.kob.backend.service.user.account; + +import java.util.Map; + +//验证用户名密码,验证成功后返回jwt token(令牌) +public interface LoginService { + public Map getToken(String username, String password); +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/RegisterService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/RegisterService.java new file mode 100644 index 0000000..5e59592 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/user/account/RegisterService.java @@ -0,0 +1,9 @@ +package com.kob.backend.service.user.account; + +import java.util.Map; + +//注册账号 +public interface RegisterService { +// confirmedPassword 密码确认 + public Map register(String username, String password, String confirmedPassword); +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/AddService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/AddService.java new file mode 100644 index 0000000..5bc0d1c --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/AddService.java @@ -0,0 +1,7 @@ +package com.kob.backend.service.user.bot; + +import java.util.Map; + +public interface AddService { + public Map add(Map data); +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/GetListService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/GetListService.java new file mode 100644 index 0000000..6997651 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/GetListService.java @@ -0,0 +1,9 @@ +package com.kob.backend.service.user.bot; + +import com.kob.backend.pojo.Bot; + +import java.util.List; + +public interface GetListService { + List getList(); // getList() 不需要传参数 +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/RemoveService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/RemoveService.java new file mode 100644 index 0000000..3f256b9 --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/RemoveService.java @@ -0,0 +1,6 @@ +package com.kob.backend.service.user.bot; + +import java.util.Map; +public interface RemoveService { + public Map remove(Map data); +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/UpdateService.java b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/UpdateService.java new file mode 100644 index 0000000..d05e02d --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/service/user/bot/UpdateService.java @@ -0,0 +1,7 @@ +package com.kob.backend.service.user.bot; + +import java.util.Map; + +public interface UpdateService { + Map update(Map data); +} diff --git a/backendcloud/backend/src/main/java/com/kob/backend/utils/JwtUtil.java b/backendcloud/backend/src/main/java/com/kob/backend/utils/JwtUtil.java new file mode 100644 index 0000000..b23d22b --- /dev/null +++ b/backendcloud/backend/src/main/java/com/kob/backend/utils/JwtUtil.java @@ -0,0 +1,74 @@ +package com.kob.backend.utils; + +//jwt工具类,用来创建、解析 jwt token +/* +主要作用: + 1. 将字符串加上密钥,加上有效期,转换为加密后的字符串; + 2. 给一个令牌,将其 userID 解析出来. + +依赖:添加到 pom.xml 中,然后使用 Maven 重新加载依赖项 + jjwt-apt; + jjwt-impl; + jjwt-jackson. +*/ + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtBuilder; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.springframework.stereotype.Component; + +import javax.crypto.SecretKey; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; +import java.util.Date; +import java.util.UUID; + +@Component +public class JwtUtil { + public static final long JWT_TTL = 60 * 60 * 1000L * 24 * 14; // 有效期14天:单位-毫秒 ms + public static final String JWT_KEY = "SDFGjhdsfalshdfHFdsjkdsfds121232113afasdfad"; // 密钥:随机字符串-大小写英文字母+数字 + + public static String getUUID() { + return UUID.randomUUID().toString().replaceAll("-", ""); + } + + public static String createJWT(String subject) { + JwtBuilder builder = getJwtBuilder(subject, null, getUUID()); + return builder.compact(); + } + + private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) { + SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; + SecretKey secretKey = generalKey(); + long nowMillis = System.currentTimeMillis(); + Date now = new Date(nowMillis); + if (ttlMillis == null) { + ttlMillis = JwtUtil.JWT_TTL; + } + + long expMillis = nowMillis + ttlMillis; + Date expDate = new Date(expMillis); + return Jwts.builder() + .setId(uuid) + .setSubject(subject) + .setIssuer("sg") + .setIssuedAt(now) + .signWith(signatureAlgorithm, secretKey) + .setExpiration(expDate); + } + + public static SecretKey generalKey() { + byte[] encodeKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY); + return new SecretKeySpec(encodeKey, 0, encodeKey.length, "HmacSHA256"); + } + + public static Claims parseJWT(String jwt) throws Exception { + SecretKey secretKey = generalKey(); + return Jwts.parserBuilder() + .setSigningKey(secretKey) + .build() + .parseClaimsJws(jwt) + .getBody(); + } +} \ No newline at end of file diff --git a/backendcloud/backend/src/main/resources/application.properties b/backendcloud/backend/src/main/resources/application.properties new file mode 100644 index 0000000..2a217c6 --- /dev/null +++ b/backendcloud/backend/src/main/resources/application.properties @@ -0,0 +1,8 @@ +#default server port +server.port=3000 + +#mysql database connect profile +spring.datasource.username=root +spring.datasource.password=mysqlPassword +spring.datasource.url=jdbc:mysql://localhost:3306/kob?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8 +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver diff --git a/backendcloud/backend/src/main/resources/static/image/img.png b/backendcloud/backend/src/main/resources/static/image/img.png new file mode 100644 index 0000000000000000000000000000000000000000..857520aa085d777d2372884a3ed3aedb5b5e58d4 GIT binary patch literal 43486 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 icOE#`m&E>T + + + + Title + + +

+ + \ No newline at end of file diff --git a/backendcloud/backend/src/test/java/com/kob/backend/BackendApplicationTests.java b/backendcloud/backend/src/test/java/com/kob/backend/BackendApplicationTests.java new file mode 100644 index 0000000..b7a66fd --- /dev/null +++ b/backendcloud/backend/src/test/java/com/kob/backend/BackendApplicationTests.java @@ -0,0 +1,18 @@ +package com.kob.backend; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; + +@SpringBootTest +class BackendApplicationTests { + +// 测试明文加密 + @Test + void contextLoads() { + PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); + System.out.println(passwordEncoder.encode("123")); + } + +} diff --git a/backendcloud/matchingsystem/pom.xml b/backendcloud/matchingsystem/pom.xml new file mode 100644 index 0000000..4dc1bfc --- /dev/null +++ b/backendcloud/matchingsystem/pom.xml @@ -0,0 +1,51 @@ + + + 4.0.0 + + com.kob + backendcloud + 0.0.1-SNAPSHOT + + + com.kob.matchingsystem + matchingsystem + + + 8 + 8 + UTF-8 + + + + + + + org.projectlombok + lombok + 1.18.26 + provided + + + + + + org.springframework.boot + spring-boot-starter-security + 3.0.2 + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + \ No newline at end of file diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/MatchingSystemApplication.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/MatchingSystemApplication.java new file mode 100644 index 0000000..d4754c8 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/MatchingSystemApplication.java @@ -0,0 +1,15 @@ +package com.kob.matchingsystem; + +import com.kob.matchingsystem.service.impl.MatchingServiceImpl; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class MatchingSystemApplication { + public static void main(String[] args) { + // 启动 MatchingSystem 服务之前启动 MatchingPool 线程 + MatchingServiceImpl.matchingPool.start(); // 启动匹配线程 + + SpringApplication.run(MatchingSystemApplication.class, args); + } +} \ No newline at end of file diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/RestTemplateConfig.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/RestTemplateConfig.java new file mode 100644 index 0000000..3e9ab47 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/RestTemplateConfig.java @@ -0,0 +1,13 @@ +package com.kob.matchingsystem.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class RestTemplateConfig { + @Bean + public RestTemplate getRestTemplate() { + return new RestTemplate(); + } +} diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/SecurityConfig.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/SecurityConfig.java new file mode 100644 index 0000000..df09e44 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/config/SecurityConfig.java @@ -0,0 +1,34 @@ +// 链接访问权限控制 +package com.kob.matchingsystem.config; + +/* +主要作用:放行登录、注册等接口 +*/ + +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpMethod; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; + +// TODO: 2023/2/19 WebSecurityConfigurerAdapter 已被弃用,注意替换 +@Configuration +@EnableWebSecurity +public class SecurityConfig extends WebSecurityConfigurerAdapter { + /* + .antMatchers("/user/account/token/", "/user/account/register/").permitAll() + 用于配置公开链接 + .hasIpAddress("127.0.0.1") 用于限制访问的IP地址, 127.0.0.1 意为只允许本地访问 + */ + @Override + protected void configure(HttpSecurity http) throws Exception { + http.csrf().disable() + .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) + .and() + .authorizeRequests() + .antMatchers("/player/add/", "/player/remove/").hasIpAddress("127.0.0.1") + .antMatchers(HttpMethod.OPTIONS).permitAll() + .anyRequest().authenticated(); + } +} \ No newline at end of file diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/controller/MatchingController.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/controller/MatchingController.java new file mode 100644 index 0000000..d022657 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/controller/MatchingController.java @@ -0,0 +1,33 @@ +package com.kob.matchingsystem.controller; + +import com.kob.matchingsystem.service.MatchingService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.util.MultiValueMap; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Objects; + +@RestController +public class MatchingController { + // 注入接口 + @Autowired + private MatchingService matchingService; + + // 涉及到对数据的修改,使用 POST 请求 + // MultiValueMap 每个关键字对应一个列表的值, Map 每个关键字只能对应一个单值 + @PostMapping("/player/add/") + public String addPlayer(@RequestParam MultiValueMap data) { + Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id"))); + Integer rating = Integer.parseInt(Objects.requireNonNull(data.getFirst("rating"))); + return matchingService.addPlayer(userId, rating); + } + + @PostMapping("/player/remove/") + public String removePlayer(@RequestParam MultiValueMap data) { + Integer userId = Integer.parseInt(Objects.requireNonNull(data.getFirst("user_id"))); + return matchingService.removePlayer(userId); + } + +} diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/MatchingService.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/MatchingService.java new file mode 100644 index 0000000..39df470 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/MatchingService.java @@ -0,0 +1,10 @@ +// 定义接口 +package com.kob.matchingsystem.service; + +public interface MatchingService { + // 给匹配池添加一名玩家 + String addPlayer(Integer userId, Integer rating); + + // 从匹配池删除一名玩家 + String removePlayer(Integer userId); +} diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/MatchingServiceImpl.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/MatchingServiceImpl.java new file mode 100644 index 0000000..c4e4018 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/MatchingServiceImpl.java @@ -0,0 +1,28 @@ +// 实现接口 +package com.kob.matchingsystem.service.impl; + +import com.kob.matchingsystem.service.MatchingService; +import com.kob.matchingsystem.service.impl.utils.MatchingPool; +import org.springframework.stereotype.Service; + +@Service +public class MatchingServiceImpl implements MatchingService { + // MatchingPool 全局只有一个线程,所以这里定义成静态 + public final static MatchingPool matchingPool = new MatchingPool(); + + @Override + public String addPlayer(Integer userId, Integer rating) { + System.out.println("add player: " + userId + " " + rating); + // 向匹配池添加一名玩家 + matchingPool.addPlayer(userId, rating); + return "add player success"; + } + + @Override + public String removePlayer(Integer userId) { + System.out.println("remove player: " + userId); + // 从匹配池移除一名玩家 + matchingPool.removePlayer(userId); + return "remove player success"; + } +} diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/MatchingPool.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/MatchingPool.java new file mode 100644 index 0000000..9e00040 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/MatchingPool.java @@ -0,0 +1,140 @@ +// 用来维护线程 +package com.kob.matchingsystem.service.impl.utils; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.locks.ReentrantLock; + +@Component +public class MatchingPool extends Thread { + // 把所有用户存下来 + private static List playerList = new ArrayList<>(); + // 定义一个锁, Reentrant Lock 是可重入锁 + private final ReentrantLock lock = new ReentrantLock(); + // 定义微服务传值 URL + private static final String startGameUrl = "http://127.0.0.1:3000/pk/start/"; + // 定义 RestTemplate 用来微服务数据通信 + private static RestTemplate restTemplate; + + @Autowired + private void setRestTemplate(RestTemplate restTemplate) { + MatchingPool.restTemplate = restTemplate; + } + + public void addPlayer(Integer userId, Integer rating) { + lock.lock(); + try { + // 一开始匹配等待时间是 0 + playerList.add(new Player(userId, rating, 0)); + } finally { + lock.unlock(); + } + } + + public void removePlayer(Integer userId) { + lock.lock(); + try { + // 先创建一个新列表,将没有删的用户先存下来 + List newPlayerList = new ArrayList<>(); + // 枚举所有的 player 并存入 newPlayerList + for (Player player : playerList) { + // 如果当前 player 的 id 不等于要删除的用户 id,则表示此用户保留,将要保留的用户信息存入新列表 + // 否则不存入新列表 + if (!player.getUserId().equals(userId)) + newPlayerList.add(player); + } + // 将删除用户后的新 newPlayerList 赋值给 playerList + playerList = newPlayerList; + } finally { + lock.unlock(); + } + } + + // 将所有玩家的等待时间 +1 + private void increaseWaitingTime() { + for (Player player : playerList) { + player.setWaitingTime(player.getWaitingTime() + 1); + } + } + + // 判断两名玩家是否匹配 + private boolean checkMatched(Player a, Player b) { + int ratingDelta = Math.abs(a.getRating() - b.getRating()); // 两名玩家的天梯积分之差 + // a b 两名玩家的等待时间最小值 + // min 是两方任意一方接受等待时间就可以匹配, max 是两方都接受的匹配 + int minWaitingTime = Math.min(a.getWaitingTime(), b.getWaitingTime()); + // 匹配结果可被 a 接受: a b 的分差小于等于 a和b 最小等待时间乘以 10 + // 匹配结果可被 b 接受: a b 的分差小于等于 a和b 最小等待时间乘以 10 + return ratingDelta <= minWaitingTime * 10; + } + + // 返回 a 和 b 的匹配结果 + private void sendResult(Player a, Player b) { + MultiValueMap data = new LinkedMultiValueMap<>(); + data.add("a_id", a.getUserId().toString()); + data.add("b_id", b.getUserId().toString()); + restTemplate.postForObject(startGameUrl, data, String.class); + } + + // 尝试匹配所有玩家 + private void matchPlayers() { + //TODO 后端调试 + System.out.println("match players: " + playerList.toString()); + // havaMatched 表示玩家已经匹配过了 + boolean[] haveMatched = new boolean[playerList.size()]; + for (int i = 0; i < playerList.size(); i++) { + if (haveMatched[i]) continue; // 如果当前枚举到的玩家已经匹配过了,则跳过改玩家 + for (int j = i + 1; j < playerList.size(); j++) { // j 从 i+1 开始枚举 + if (haveMatched[j]) continue; + // 如果 i 和 j 都没有匹配,则将 a 和 b 玩家取出来 + Player a = playerList.get(i); + Player b = playerList.get(j); + // 判断 a 和 b 能否匹配:如果匹配,则将结果返回,并将 a 和 b 的位置置为 true + if (checkMatched(a, b)) { + haveMatched[i] = haveMatched[j] = true; // 置为已匹配 + sendResult(a, b); // 返回匹配玩家结果 + break; + } + } + } + + // 匹配成功后,需要将匹配过的玩家从匹配池移除 + List newPlayerList = new ArrayList<>(); + for (int i = 0; i < playerList.size(); i++) { + // 如果当前枚举到的玩家没有匹配,则将该玩家存入新的 list + if (!haveMatched[i]) + newPlayerList.add(playerList.get(i)); + } + // 将新的 newPlayerList 重新赋值 playerList + playerList = newPlayerList; + } + + // 实现线程 + @Override + public void run() { + while (true) { + try { + Thread.sleep(1000); + lock.lock(); + try { + // 每隔一秒钟调用“等待时间增加”函数 + increaseWaitingTime(); + // 尝试匹配玩家 + matchPlayers(); + } finally { + lock.unlock(); + } + } catch (InterruptedException e) { + // 如果捕获到异常,则输出异常并终止运行 + e.printStackTrace(); + break; + } + } + } +} diff --git a/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/Player.java b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/Player.java new file mode 100644 index 0000000..a6576af --- /dev/null +++ b/backendcloud/matchingsystem/src/main/java/com/kob/matchingsystem/service/impl/utils/Player.java @@ -0,0 +1,15 @@ +// 存储玩家 +package com.kob.matchingsystem.service.impl.utils; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class Player { + private Integer userId; + private Integer rating; + private Integer waitingTime; // 等待时间 +} diff --git a/backendcloud/matchingsystem/src/main/resources/application.properties b/backendcloud/matchingsystem/src/main/resources/application.properties new file mode 100644 index 0000000..032aec6 --- /dev/null +++ b/backendcloud/matchingsystem/src/main/resources/application.properties @@ -0,0 +1 @@ +server.port=3001 \ No newline at end of file diff --git a/backendcloud/mvnw b/backendcloud/mvnw new file mode 100644 index 0000000..8a8fb22 --- /dev/null +++ b/backendcloud/mvnw @@ -0,0 +1,316 @@ +#!/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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# 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 + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -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 "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; 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; \\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 + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# 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/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# 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. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +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 "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -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 + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/backendcloud/mvnw.cmd b/backendcloud/mvnw.cmd new file mode 100644 index 0000000..1d8ab01 --- /dev/null +++ b/backendcloud/mvnw.cmd @@ -0,0 +1,188 @@ +@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 Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@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 DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_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 DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_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('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@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% diff --git a/backendcloud/pom.xml b/backendcloud/pom.xml new file mode 100644 index 0000000..d4b524d --- /dev/null +++ b/backendcloud/pom.xml @@ -0,0 +1,57 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.7.7 + + + com.kob + backendcloud + 0.0.1-SNAPSHOT + backendcloud + backendcloud + + matchingsystem + backend + + pom + + + 1.8 + + + + + + org.springframework.cloud + spring-cloud-dependencies + 2022.0.1 + pom + import + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + +