AcWing:SpringBoot 框架课 - 创建对战列表与排行榜页面 遗留操作
将 userMapper 修改为 public,因为要在对局结束,修改玩家积分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 backend/consumer/WebSocketServer.java package com.kob.backend.consumer; ... @Component // url链接:ws://127.0.0.1:3000/websocket/** @ServerEndpoint("/websocket/{token}") // 注意不要以'/'结尾 public class WebSocketServer { public static UserMapper userMapper; ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 backend/consumer/utils/Game.java package com.kob.backend.consumer.utils; ... import com.kob.backend.pojo.User; public class Game extends Thread { ... private void updateUserRating(Player player, Integer rating) { User user = WebSocketServer.userMapper.selectById(player.getId()); user.setRating(rating); WebSocketServer.userMapper.updateById(user); } private void saveToDataBase() { Integer ratingA = WebSocketServer.userMapper.selectById(playerA.getId()).getRating(); Integer ratingB = WebSocketServer.userMapper.selectById(playerB.getId()).getRating(); if("A".equals(loser)) { ratingA -= 2; ratingB += 5; } else if("B".equals(loser)) { ratingA += 5; ratingB -= 2; } updateUserRating(playerA, ratingA); updateUserRating(playerB, ratingB); ... } ... }
主要内容
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 backend/config/MybatisConfig.java package com.kob.backend.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class MybatisConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }
1.对局列表和回放
1.1 后端
1 2 3 4 5 6 7 8 9 10 backend/service/record/GetRecordListService.java package com.kob.backend.service.record; import com.alibaba.fastjson.JSONObject; public interface GetRecordListService { JSONObject getList(Integer page); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 backend/service/impl/record/GetRecordListServiceImpl.java package com.kob.backend.service.impl.record; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.kob.backend.mapper.RecordMapper; import com.kob.backend.mapper.UserMapper; import com.kob.backend.pojo.Record; import com.kob.backend.pojo.User; import com.kob.backend.service.record.GetRecordListService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.LinkedList; import java.util.List; @Service public class GetRecordListServiceImpl implements GetRecordListService { @Autowired private RecordMapper recordMapper; @Autowired private UserMapper userMapper; @Override public JSONObject getList(Integer page) { IPage<Record> recordIPage = new Page<>(page, 10); QueryWrapper<Record> queryWrapper = new QueryWrapper<>(); queryWrapper.orderByDesc("id"); List<Record> records = recordMapper.selectPage(recordIPage, queryWrapper).getRecords(); JSONObject resp = new JSONObject(); List<JSONObject> items = new LinkedList<>(); for(Record record : records) { User userA = userMapper.selectById(record.getAId()); User userB = userMapper.selectById(record.getBId()); JSONObject item = new JSONObject(); item.put("a_photo", userA.getPhoto()); item.put("a_username", userA.getUsername()); item.put("b_photo", userB.getPhoto()); item.put("b_username", userB.getUsername()); item.put("record", record); String result = "平局"; if("A".equals(record.getLoser())) result = "B胜"; else if("B".equals(record.getLoser())) result = "A胜"; item.put("result", result); items.add(item); } resp.put("records", items); resp.put("records_count", recordMapper.selectCount(null)); return resp; } }
在类内定义属性时,使用 spring 自动注入的话,若是在 service 或 controller 定义,则不用定义 static。翻译过来,意思就是,若当前类本身是单例,则属性本身就只会有一份,无论加不加静态变量,效果都一样;若是第三方类,也就是自己写的类,一般都是会定义多个对象,那么就要思考属性到底是属于类的,还是属于对象的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 backend/controller/record/GetRecordListController.java package com.kob.backend.controller.record; import com.alibaba.fastjson.JSONObject; import com.kob.backend.service.record.GetRecordListService; 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.Map; @RestController public class GetRecordListController { @Autowired private GetRecordListService getRecordListService; @GetMapping("/record/getlist/") JSONObject getList(@RequestParam Map<String, String> data) { Integer page = Integer.parseInt(data.get("page")); return getRecordListService.getList(page); } }
1.2 前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 src/views/record/RecordIndexView .vue <template> <ContentField > <table class ="table table-striped table-hover" style ="text-align: center;" > <thead > <tr > <th > A</th > <th > B</th > <th > 对战结果</th > <th > 对战时间</th > <th > 操作</th > </tr > </thead > <tbody > <tr v-for ="record in records" :key ="record.record.id" > <td > <img :src ="record.a_photo" alt ="" class ="record-user-photo" > <span class ="record-user-username" > {{ record.a_username }}</span > </td > <td > <img :src ="record.b_photo" alt ="" class ="record-user-photo" > <span class ="record-user-username" > {{ record.b_username }}</span > </td > <td > {{ record.result }} </td > <td > {{ record.record.createtime }}</td > <td > <button type ="button" class ="btn btn-secondary" > 查看录像</button > </td > </tr > </tbody > </table > </ContentField > </template > <script > import ContentField from '../../components/ContentField.vue' import { useStore } from 'vuex' ;import { ref } from 'vue' ;import $ from 'jquery' ;export default { components : { ContentField }, setup ( ) { const store = useStore (); let records = ref ([]); let current_page = 1 ; let total_records = 0 ; const pull_page = page => { $.ajax ({ url : "http://127.0.0.1:3000/record/getlist/" , data : { page, }, type : "get" , headers : { 'Authorization' : "Bearer " + store.state .user .token , }, success (resp ) { records.value = resp.records ; total_records = resp.records_count ; }, error (resp ) { console .log (resp); } }) }; pull_page (current_page); return { records, } } } </script > <style scoped > img .record-user-photo { width : 4vh ; border-radius : 50% ; } </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 src / store / record.js ; export default { state : { is_record : false , a_step : "" , b_step : "" , record_loser : "" , }, getters : {}, mutations : { updateIsRecord (state, is_record ) { state.is_record = is_record; }, updateSteps (state, data ) { state.a_steps = data.a_steps ; state.b_steps = data.b_steps ; }, updateRecordLoser (state, loser ) { state.record_loser = loser; }, }, actions : {}, modules : {}, };
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 src / store / index.js ; import { createStore } from "vuex" ;import ModuleUser from "./user" ;import ModulePk from "./pk" ;import ModuleRecord from "./record" ;export default createStore ({ state : {}, getters : {}, mutations : {}, actions : {}, modules : { user : ModuleUser , pk : ModulePk , record : ModuleRecord , }, });
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 src/views/record/RecordIndexView .vue <template> <ContentField > <table class ="table table-striped table-hover" style ="text-align: center;" > ... <tbody > <tr v-for ="record in records" :key ="record.record.id" > ... <td > <button @click ="open_record_content(record.record.id)" type ="button" class ="btn btn-secondary" > 查看录像</button > </td > </tr > </tbody > </table > </ContentField > </template> <script > ... import router from '../../router/index' ;export default { ... setup ( ) { ... const stringTo2D = map => { let g = []; for (let i = 0 , k = 0 ; i < 13 ; i++) { let line = []; for (let j = 0 ; j < 14 ; j++, k++) { if (map[k] === '0' ) line.push (0 ); else line.push (1 ); } g.push (line); } return g; } const open_record_content = recordId => { for (const record of records.value ) { if (record.record .id === recordId) { store.commit ("updateIsRecord" , true ); store.commit ("updateGame" , { map : stringTo2D (record.record .map ), a_id : record.record .aid , a_sx : record.record .asx , a_sy : record.record .asy , b_id : record.record .bid , b_sx : record.record .bsx , b_sy : record.record .bsy , }); store.commit ("updateSteps" , { a_steps : record.record .asteps , b_steps : record.record .bsteps , }); store.commit ("updateRecordLoser" , record.record .loser ); router.push ({ name : 'record_content' , params : { recordId, } }); break ; } } }; return { records, open_record_content, } } } </script > ...
当在 pk 页面时,要标记不是录像
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 src/views/pk/PkIndexView.vue <template> ... </template> <script> ... export default { ... setup() { ... store.commit("updateLoser", 'none'); store.commit("updateIsRecord", false); ... } } </script> <style scoped> </style>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 src/views/record/RecordContentView .vue <template> <PlayGround /> </template> <script > import PlayGround from '../../components/PlayGround.vue' ;export default { components : { PlayGround , }, setup ( ) { } } </script > <style scoped > </style >
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 src/router/index.js ... import RecordIndexView from '../views/record/RecordIndexView' import RecordContentView from '../views/record/RecordContentView' ... const routes = [ ... { path : "/record/:recordId/" , name : "record_content" , component : RecordContentView , meta : { requestAuth : true , } }, ... ] ...
判断是回放还是游戏
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 src/assets/scripts/GameMap .js ... export class GameMap extends AcGameObject { ... add_listening_events ( ) { if (this .store .state .record .is_record ) { let k = 0 ; const a_steps = this .store .state .record .a_steps ; const b_steps = this .store .state .record .b_steps ; const loser = this .store .state .record .record_loser ; const [snake0, snake1] = this .snakes ; const interval_id = setInterval (() => { if (k >= a_steps.length - 1 ) { if (loser === "all" || loser === "A" ) { snake0.status = "die" ; } if (loser === "all" || loser === "B" ) { snake1.status = "die" ; } clearInterval (interval_id); } else { snake0.set_direction (parseInt (a_steps[k])); snake1.set_direction (parseInt (b_steps[k])); } k++; }, 300 ); } else { this .ctx .canvas .focus (); this .ctx .canvas .addEventListener ("keydown" , e => { let d = -1 ; if (e.key === 'w' ) d = 0 ; else if (e.key === 'd' ) d = 1 ; else if (e.key === 's' ) d = 2 ; else if (e.key === 'a' ) d = 3 ; if (d >= 0 ) { this .store .state .pk .socket .send (JSON .stringify ({ event : "move" , direction : d, })); } }); } } ... }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 src/views/record/RecordIndexView .vue <template> <ContentField > ... <nav aria-label ="..." > <ul class ="pagination" style ="float: right;" > <li class ="page-item" @click ="click_page(-2)" > <a class ="page-link" href ="#" > 前一页</a > </li > <li :class ="'page-item ' + page.is_active" v-for ="page in pages" :key ="page.number" @click ="click_page(page.number)" > <a class ="page-link" href ="#" > {{ page.number }}</a > </li > <li class ="page-item" @click ="click_page(-1)" > <a class ="page-link" href ="#" > 后一页</a > </li > </ul > </nav > </ContentField > </template> <script > ... export default { components : { ... }, setup ( ) { ... let current_page = 1 ; let total_records = 0 ; let pages = ref ([]); const click_page = page => { if (page === -2 ) page = current_page - 1 ; else if (page === -1 ) page = current_page + 1 ; let max_pages = parseInt (Math .ceil (total_records / 10 )); if (page >= 1 && page <= max_pages) { pull_page (page); } } const update_pages = ( ) => { let max_pages = parseInt (Math .ceil (total_records / 10 )); let new_pages = []; for (let i = current_page - 2 ; i <= current_page + 2 ; i++) { if (i >= 1 && i <= max_pages) { new_pages.push ({ number : i, is_active : i === current_page ? "active" : "" , }); } } pages.value = new_pages; }; const pull_page = page => { current_page = page; $.ajax ({ ... success (resp ) { ... update_pages (); }, ... }) }; ... return { ... pages, click_page, } } } </script > <style scoped > ... </style >
2.排行榜
2.1 后端
1 2 3 4 5 6 7 8 9 10 backend/service/ranklist/GetRanklistService.java package com.kob.backend.service.ranklist; import com.alibaba.fastjson.JSONObject; public interface GetRanklistService { JSONObject getList(Integer page); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 backend/service/impl/ranklist/GetRanklistServiceImpl.java package com.kob.backend.service.impl.ranklist; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.kob.backend.mapper.UserMapper; import com.kob.backend.pojo.User; import com.kob.backend.service.ranklist.GetRanklistService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class GetRanklistServiceImpl implements GetRanklistService { @Autowired private UserMapper userMapper; @Override public JSONObject getList(Integer page) { IPage<User> userIPage = new Page<>(page, 3); QueryWrapper<User> queryWrapper = new QueryWrapper<>(); queryWrapper.orderByDesc("rating"); List<User> users = userMapper.selectPage(userIPage, queryWrapper).getRecords(); JSONObject resp = new JSONObject(); for(User user : users) user.setPassword(""); resp.put("users", users); resp.put("users_count", userMapper.selectCount(null)); return resp; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 backend/controller/ranklist/GetRanklistController.java package com.kob.backend.controller.ranklist; import com.alibaba.fastjson.JSONObject; import com.kob.backend.service.ranklist.GetRanklistService; 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.Map; @RestController public class GetRanklistController { @Autowired private GetRanklistService getRanklistService; @GetMapping("/ranklist/getlist/") public JSONObject getList(@RequestParam Map<String, String> data) { Integer page = Integer.parseInt(data.get("page")); return getRanklistService.getList(page); } }
2.2 前端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 src/views/ranklist/RanklistIndexView .vue <template> <ContentField > <table class ="table table-striped table-hover" style ="text-align: center;" > <thead > <tr > <th > 玩家</th > <th > 天梯分</th > </tr > </thead > <tbody > <tr v-for ="user in users" :key ="user.id" > <td > <img :src ="user.photo" alt ="" class ="user-photo" > <span class ="user-username" > {{ user.username }}</span > </td > <td > {{ user.rating }} </td > </tr > </tbody > </table > <nav aria-label ="..." > <ul class ="pagination" style ="float: right;" > <li class ="page-item" @click ="click_page(-2)" > <a class ="page-link" href ="#" > 前一页</a > </li > <li :class ="'page-item ' + page.is_active" v-for ="page in pages" :key ="page.number" @click ="click_page(page.number)" > <a class ="page-link" href ="#" > {{ page.number }}</a > </li > <li class ="page-item" @click ="click_page(-1)" > <a class ="page-link" href ="#" > 后一页</a > </li > </ul > </nav > </ContentField > </template > <script > import ContentField from '../../components/ContentField.vue' import { useStore } from 'vuex' ;import { ref } from 'vue' ;import $ from 'jquery' ;export default { components : { ContentField }, setup ( ) { const store = useStore (); let users = ref ([]); let current_page = 1 ; let total_users = 0 ; let pages = ref ([]); const click_page = page => { if (page === -2 ) page = current_page - 1 ; else if (page === -1 ) page = current_page + 1 ; let max_pages = parseInt (Math .ceil (total_users / 3 )); if (page >= 1 && page <= max_pages) { pull_page (page); } } const update_pages = ( ) => { let max_pages = parseInt (Math .ceil (total_users / 3 )); let new_pages = []; for (let i = current_page - 2 ; i <= current_page + 2 ; i++) { if (i >= 1 && i <= max_pages) { new_pages.push ({ number : i, is_active : i === current_page ? "active" : "" , }); } } pages.value = new_pages; }; const pull_page = page => { current_page = page; $.ajax ({ url : "http://127.0.0.1:3000/ranklist/getlist/" , data : { page, }, type : "get" , headers : { 'Authorization' : "Bearer " + store.state .user .token , }, success (resp ) { users.value = resp.users ; total_users = resp.users_count ; update_pages (); }, error (resp ) { console .log (resp); } }) }; pull_page (current_page); return { users, pages, click_page, } } } </script > <style scoped > img .user-photo { width : 4vh ; border-radius : 50% ; } </style >
3.修改创建 bot 最多数量
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 backend/service/impl/user/bot/AddServiceIMpl package com.kob.backend.service.impl.user.bot; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; ... @Service public class AddServiceImpl implements AddService { @Autowired private BotMapper botMapper; @Override public Map<String, String> add(Map<String, String> data) { ... if(content.length() > 10000) { map.put("error_message", "代码长度不能超过10000"); return map; } QueryWrapper<Bot> queryWrapper = new QueryWrapper<>(); queryWrapper.eq("user_id", user.getId()); if(botMapper.selectCount(queryWrapper) >= 10) { map.put("error_message", "每个用户最多只能创建10个Bot!"); return map; } ... } }
补充
部分玩家在未输入完操作就结束了游戏。这是由于有些玩家在前端是 2000ms 再开始游戏,而后端的 nextstep 是 200ms,导致后端没接收到输入,就提前结束了游戏。以下是修改是为了给游戏初始化充足的时间,并尽量写清晰逻辑。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 src/views/pk/PkIndexView .vue <template> ... </template> <script > ... export default { ... setup ( ) { ... onMounted (() => { ... socket.onmessage = msg => { const data = JSON .parse (msg.data ); if (data.event === "start-matching" ) { ... setTimeout (() => { store.commit ("updateStatus" , "playing" ); }, 2000 ); ... } ... } ... }); ... } } </script > <style scoped > </style >
在第一次接收输入时特判,等待 2000ms,之后每次接收输入是在 200ms 后判断
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 backend/consumer/utils/Game.java package com.kob.backend.consumer.utils; ... public class Game extends Thread { ... private boolean isStart = true; // 接收玩家的下一步操作 private boolean nextStep() { // 每秒五步操作,因此第一步操作是在200ms后判断是否接收到输入。并给地图初始化时间 try { if(isStart) { Thread.sleep(2000); isStart = false; } else { Thread.sleep(200); } } catch (InterruptedException e) { throw new RuntimeException(e); } ... } ... }