序言
作为一名篮球爱好者的程序员,在使用目前市面篮球计分器时,总觉得用的不顺手,市面基本都是单机模式,广告很多,也不便于核对和多人协同记录。因此,我决定自己设计并开发一款篮球共享计分器小程序,实现多人协同记录赛事,提供多种方便快捷的计分方式和统计数据效果。
上效果图:
由于上篇《开发一个题库系统App和小程序的心得》已经叙述了很多资源方案,代码方案,部署方案,以及很多基础性的功能开发设计,此系统基本相同,不再赘述,直接讲解篮球共享计分器特有功能。
1. 功能目标
篮球共享计分器功能目标:
- 实现多人实时共享记录比赛
- 提供个人和团队数据和表现的统计数据
- 提供单机版/共享版/私密版三种模式记录比赛
- 提供简易/精准版模式选择
- 保存历史比赛数据,可用于文字直播。
1.1 功能列表
篮球共享计分器核心功能
- 实现常规简易单机版计分器
- 实现协同协同版计分器,协同实时切换简易版和精准版计分方式
- 实现团队对比数据,可视化图表展示和对比,以及完整文字记录
- 实现个人完整数据,以及个人完整文字记录
- 协同版区分公开和私密赛事,公开赛事可免登录参与,私密赛事支持多种精准权限控制记录比赛
1.2 功能截图
以下是App端的部分截图
以下是微信小程序端的部分截图
2. 实现方案
2.1 设计方案
2.1.1 数据库设计
赛事表
队员表
授权表
日志表
2.1.2 移动端开发-赛事列表和录入
赛事有三种添加方式:
- 已有比赛,可以选择复制一份,会复制赛事的队员和用户权限
- 录入比赛,自行录入比赛名称等信息,保存即可
- (需登录)添加授权码比赛,别人创建的私密比赛,分享的授权码,可以通过授权码进行添加
赛事录入细节:
- 长按赛事明细,可操作复制/编辑/授权/重置/取消/删除(可见操作按钮跟权限有关)
- 编辑赛事,可增加/修改/删除赛事双方队员:添加队员直接录入即可;搜索队员可查找有权限赛事的所有人员,因此同名队伍只需录入一次,下次直接搜索一次性添加即可;长按队员可编辑;删除按钮可删除。(赛事录入页面也可同样操作)
2.1.3 移动端开发-赛事授权(需登录)
赛事列表中,对于公开赛事,所有人都可见也都是管理员角色;对于私密赛事,长按明细,可选择授权操作。
有两种授权模式:
- 生成授权码,点击观众/主队管理员/客队管理员/管理员,可生成对应赛事授权码,其他人在赛事列表页面通过添加授权码比赛的方式即可获权
- 添加用户,点击添加用户,输入用户信息查找授权即可(为保证用户隐私,此处不支持模糊搜索)
授权细节说明:
- 赛事创建人是超级管理员,可以直接添加/修改/删除任何已授权用户
- 只能添加/修改/删除比自己低的权限,不会出现操作的权限超越操作人的权限的情况
- 可自己删除自己的授权,删除后赛事不可见
2.1.4 移动端开发-记录比赛
赛事记录方法:
- 点击【精准版】/【简易版】可以互相切换记录模式
- 点击【用时】,可查看实时比赛数据结果
- 点击【分享比赛】,可生成赛事二维码,让其他人参与
- 长按任意队员,可切换队员
- 长按日志明细,可作废或修改日志
赛事记录说明:
- 公开赛事记录,所有人都是管理员权限,都可以通过分享的二维码进行赛事记录。
- 私密赛事记录,扫码进来的默认赋权观众,需要进一步授权才可以记录比赛,否则只能观看。
- 私密赛事记录,管理员可以记录双方数据,主队管理员只能记录主队数据,客队管理员只能记录客队数据,观众只能观看。
- 加时赛说明,默认四节比赛,如果在第四节结束,两队分数一致,自动进入加时,否则比赛结束。
2.1.5 移动端开发-查看结果
查看结果有三种方式:
- 列表已经结束的比赛,点击查看即可查看比赛结果
- 记录比赛中,点击用时可以查看比赛结果
- 通过其他人分享的比赛结果二维码可以查看比赛结果
查看结果操作说明:
- 进入结果页面,可点击上方【*** 数据】切换页签,也可以左右滑动切换页签
- 在【**队 数据】页签,柱状图的“其他”表示整队的数据,也就是比赛记录时,选择了队伍,未选择具体人员
- 小程序图表可能出现空白,点一下图表内部,即可正常显示,图表组件在小程序上的层级Bug问题,暂时未修复
2.1.6 移动端开发-单机版计分器
操作说明:
- 点击【单机版】可清空重置数据
- 长按【犯规】或【队伍暂停】可减少次数
- 打开页面后可离线操作,离开页面或应用,数据缓存在本地,并不会消失,只能记录一个赛事,清空重置后,数据清除不可找回
2.2 开发方案
2.2.1 后端开发-框架Springboot
基于若依plus,在其基础上增加ruoyi-race模块,单独存放赛事相关功能,其中四个controller分别是:
Race:赛事功能
Log:日志功能
Member:成员功能
User:授权用户功能
Message:SSE发送消息功能(已弃用)
SSE:SSE连续功能(已弃用)
WxToken:获取小程序Token和生成小程序带参数二维码功能
SSE在使用时,微信小程序和APP不能很好的支持EventSource前端组件,长连接也存在兼容问题,效果都不好,所以弃用,使用WebSocket
在整合的框架中,使用的是Tio引擎的Websocket,增加两种赛事的消息类型,初始化消息和常规消息:
初始化消息:读取并回执当前某一赛事最新完整赛事数据
常规消息:发送记录赛事时的各种指令,更新赛事,记录日志,并回执更新后的完整赛事数据
2.2.2 移动端开发-框架Uniapp
核心在记录赛事使用websocket上,这里我们使用具体页面直连的方式,离开页面则关闭websocket,相关代码如下
onUnload() {this.timeClear()this.socket.close(true)},onShow() {this.diyApiGet('/race/race/getMoreByRaceId/' + this.setData.id).then(res => {this.setData = res.data;if(!this.userInfo || !this.userInfo.userId){// 公开赛事,无用户,随机生成一个this.userId = this.getRandom(2, 16);}else{this.userId = this.userInfo.userId;}// websocket非通讯状态,则重连// #ifdef APP-PLUSif(!this.socket){this.getMemberFirstList();this.getMemberSecondList();this.connectWebsocket(this.userId, this.setData.id);}// #endif// #ifndef APP-PLUSif(!this.socket || this.socket.getReadyState() != 1){this.getMemberFirstList();this.getMemberSecondList();this.connectWebsocket(this.userId, this.setData.id);}// #endif })},
连接websocket方法,未登录传入随机用户ID
connectWebsocket(userId, raceId){let url = globalData.wssUrl;let heartbeatTimeout = 50000; // 心跳超时时间,单位:毫秒let reconnInterval = 5000; // 重连间隔时间,单位:毫秒let binaryType = 'blob'; // 'blob' or 'arraybuffer';//arraybuffer是字节let paramStr = "app=urace&userId=" + userId + "&sessionId=" + raceIdlet param = "";this.socket = new TioSocket(url, paramStr, param, heartbeatTimeout, reconnInterval, binaryType);let ws = this.socket.connect(false);ws.onOpen((e) => {// 读取赛事初始信息 uni.sendSocketMessage({data: JSON.stringify({code:6, message: {raceId: raceId, userId: userId}})})this.socket.reset();})ws.onMessage((e) => {let message = JSONbig.parse(e.data);if(message.code == 2){let data = JSONbig.parse(message.message.content)if(data.msg){this.msg(data.msg);return;}if(data.data){this.setData = data.data;this.usedTime = data.data.usedTime || 0;if(data.data.status == 1){this.timeInterval();}else{this.timeClear();}if(data.data.status < 0){this.logList = []}this.showTitle();}if(data.log){if(data.log.logType == 99){this.getMemberFirstList();this.getMemberSecondList();this.invalidMessageLog(data.log)}else if(data.log.logType == 61){this.moveMessageLog(data.log)}else{if(data.log.logType == 34 || data.log.logType == 41 || data.log.logType == 42 || data.log.logType == 43 || data.log.logType == 81 || data.log.logType == 82 || data.log.logType == 83){if(data.log.teamIndex == 1){this.getMemberFirstList();}if(data.log.teamIndex == 2){this.getMemberSecondList();}}this.addMessageLog(data.log, true)}}}this.socket.reset();})},
赛事记录命令转译函数
export function getLogText(log, firstTeamName, secondTeamName, status){let text = "";if(log.teamIndex == 1){text += " " + firstTeamName}if(log.teamIndex == 2){text += " " + secondTeamName}if(log.memberName){text += " " + log.memberName}switch(log.logType){case -10:text += "比赛尚未开始";break;case -1:text += "比赛进行中";break;case -2:text += "比赛暂停中";break;case -3:if(log.period == 2){text += "第一节已结束";}else if(log.period == 3){text += "第二节已结束";}else if(log.period == 4){text += "第三节已结束";}else if(log.period >= 5 && log.firstTeamScore != log.secondTeamScore){text += "比赛已结束";}else if(log.period == 5){text += "第四节已结束";}else if(log.period == 6){text += "加时赛1已结束";}else if(log.period == 7){text += "加时赛2已结束";}else if(log.period == 8){text += "加时赛3已结束";}else if(log.period == 9){text += "加时赛4已结束";}else if(log.period == 10){text += "加时赛5已结束";}else{text += "加时赛已结束";}break;case -9:text += "比赛已取消";break;case 1:text += "比赛开始";break;case 2:text += "裁判暂停";break;case 3:if(log.period == 1){text += "第一节比赛继续";}else if(log.period == 2){text += "第二节比赛继续";}else if(log.period == 3){text += "第三节比赛继续";}else if(log.period == 4){text += "第四节比赛继续";}else if(log.period == 5){text += "加时赛1继续";}else if(log.period == 6){text += "加时赛2继续";}else if(log.period == 7){text += "加时赛3继续";}else if(log.period == 8){text += "加时赛4继续";}else if(log.period == 9){text += "加时赛5继续";}else if(log.period == 10){text += "加时赛6继续";}else{text += "加时赛继续";}break;case 4:if(log.period == 2){text += "第一节结束";}else if(log.period == 3){text += "第二节结束";}else if(log.period == 4){text += "第三节结束";}else if(log.period >= 5 && log.firstTeamScore == log.secondTeamScore){text += "比赛结束";}else if(log.period == 5){text += "第四节结束";}else if(log.period == 6){text += "加时赛1结束";}else if(log.period == 7){text += "加时赛2结束";}else if(log.period == 8){text += "加时赛3结束";}else if(log.period == 9){text += "加时赛4结束";}else if(log.period == 10){text += "加时赛5结束";}else{text += "加时赛结束";}break;case 5:text += "比赛取消";break;case 6:text += " 请求暂停";break;case 11:text += " 罚篮命中,得1分"break;case 12:text += " 投篮命中,得2分"break;case 13:text += " 投篮命中,得2分,加罚1次"break;case 14:text += " 突破上篮,得2分"break;case 15:text += " 突破上篮,得2分,加罚1次"break;case 16:text += " 投篮命中,得3分"break;case 17:text += " 投篮命中,得3分,加罚1次"break;case 18:text += " 扣篮成功,得2分"break;case 19:text += " 扣篮成功,得2分,加罚1次"break;case 20:text += " 扣1分"break;case 21:text += " 罚篮不中"break;case 22:text += " 2分不中"break;case 23:text += " 2分不中,被犯规,罚球2次"break;case 24:text += " 3分不中"break;case 25:text += " 3分不中,被犯规,罚球3次"break;case 26:text += " 上篮失败"break;case 27:text += " 上篮失败,被犯规,罚球2次"break;case 28:text += " 扣篮失败"break;case 29:text += " 扣篮失败,被犯规,罚球2次"break;case 31:text += " 助攻一次"break;case 32:text += " 失误,丢失球权"break;case 33:text += " 抢断,获得球权"break;case 34:text += " 普通犯规"break;case 35:text += " 被普通犯规"break;case 36:text += " 投篮被犯规,开始罚球"break;case 37:text += " 盖帽"break;case 38:text += " 被盖帽"break;case 39:text += " 获得前场篮板"break;case 40:text += " 获得后场篮板"break;case 41:text += " 犯规,对手罚球"break;case 42:text += " 违体犯规"break;case 43:text += " 技术犯规"break;case 81:text += " 上场"break;case 82:text += " 下场," + log.nextMemberName + " 上场"break;case 83:text += " 下场休息"break;default:text += log.logType}return " " + text }
3. 总结一下
篮球共享计分器,主要实现的是一个协同处理能力,此次小程序开发,不仅实现了App、小程序、H5的三端兼容,也实现了赛事数据的实时同步,经过测试,任意时间进入赛事的用户,都能保证比赛用时显示的同步,互相操作都能及时收到消息。
不足之处,图表使用的uchart工具,兼容性还有待研究,篮球规则懂的不全面,以及更多形式的赛事记录支持等。
以上分享心得只能描述个大概,个人文档和开发水平都有限,文档或有错误和不妥之处,欢迎指定!