ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序canvas游戏与Java后端联调:飞翔的小鸟学习版demo解析

微信小程序canvas游戏与Java后端联调:飞翔的小鸟学习版demo解析 简介这是一份微信小程序完整demo实现“飞翔的小鸟”休闲游戏前端采用canvas绘制并控制小鸟运动后端使用java提供数据接口属于学习版资源。面向正在学习小程序开发或准备课程设计的学生群体可对照该项目理解游戏类小程序从界面渲染到接口联调的全链路。资源共35个文件压缩包约284KB文件类型覆盖png/jpg图片素材、js逻辑脚本、wxml/wxss页面与样式、json和xml配置、java服务端代码以及README说明文档目录结构区分页面、工具、图片等模块便于按需查看。项目演示了canvas动画渲染、碰撞检测、计分处理以及小程序请求java后端的通信方式同时可作为二次开发或毕业设计的基础模板。目前已有541人学习下载对想快速具备小程序游戏开发与前后端联调能力的开发者具有较高参考价值。1. 微信小程序源码里的“飞翔的小鸟”canvas游戏加java后端学习版demo值不值得看最近在给学生找练手项目时又翻到这类微信小程序源码一个用canvas实现的“飞翔的小鸟”完整demo前端是小程序原生canvas绘图后端配了一个java工程标注“适用1221(学习版)”。这类demo在小程序初学者里流传范围很广因为它把前端最“游戏化”的canvas绘图和后端最基础的HTTP接口都串了起来比单纯抄一个页面模板能学到的东西多得多。整套东西要解决的实际问题很具体游戏循环怎么驱动、小鸟的抛物线运动怎么算、管道怎么生成、分数怎么存到后端、排行榜怎么拉取。对于刚学完小程序基础、想知道一个小游戏从零到能玩需要哪些步骤的人它是一个很好的参照物对于带学生的老师它也是一个能直接讲清楚前后端分工的教学载体。在往下看之前先明确一件事这个demo叫“学习版”意味着它的目的是把链路讲通而不是给你一个能直接上线的成品。抱着“导入就能玩、改改就上线”的心态去看后面很多设计会觉得别扭把它当成一套可运行的代码教材才能看出哪些地方值得抄、哪些地方必须自己重写。2. 拆开这份小程序完整democanvas游戏循环与前后端的分工边界2.1 先看工程目录游戏页面、工具函数、后端接口各自负责什么拿到压缩包先别急着导入开发者工具把目录结构过一遍。我见过太多人一上来就点编译报错了才回头找文件结果连报错在哪个工程里都分不清。这份demo按“前端小程序 后端java”拆成两个目录前端在小程序开发者工具里打开后端用IDE单独启动。下面是我整理出来的典型结构。feidian-bird/ ├── miniprogram/ # 小程序前端 │ ├── pages/ │ │ └── game/ │ │ ├── game.wxml # 页面结构只有一个canvas节点 │ │ ├── game.wxss # 页面样式canvas铺满屏幕 │ │ ├── game.js # 游戏主逻辑循环、绘制、碰撞 │ │ └── game.json # 页面配置可设navigationStyle │ ├── utils/ │ │ ├── collision.js # 矩形碰撞检测工具 │ │ └── request.js # wx.request封装供分数上报使用 │ └── app.js / app.json # 小程序入口与全局配置 └── server/ # java后端 ├── src/main/java/ │ └── com/demo/bird/ │ ├── controller/ # 接收HTTP请求的分数组、排行榜 │ ├── service/ # 业务逻辑比如查榜、写入分数 │ └── model/ # 分数、玩家的数据模型 └── pom.xml 或 build.gradle # 依赖管理这个结构里值得注意的分工是前端只负责画和玩后端只负责存和取。游戏循环、小鸟运动、管道生成这类高频逻辑全部放在前端 game.js 里后端接口不会管你游戏怎么跑它只接收“谁得了多少分”这个结果。这样设计的好处是学习版里前后端可以独立调试后端没启动时游戏照玩只是分数上报失败前端没写完时后端也能用接口测试工具验证。2.2 canvas游戏循环用type2d拿节点requestAnimationFrame驱动canvas小游戏和普通页面最大的区别在于普通页面是“状态变了重绘一次”游戏是“每帧都在重绘”。飞翔的小鸟这类游戏画布里的背景在滚动、管道在移动、小鸟在受重力下落任何一个时刻停下来画面都应该是一张完整的静止图所以需要一个稳定频率的循环来反复执行“清屏→更新坐标→重绘”这三步。新版微信小程序里拿canvas推荐用type2d老式的 wx.createCanvasContext 已经不太适合做游戏因为它在真机上拿不到高频重绘所需的底层画布对象。常见做法是用 wx.createSelectorQuery() 找到canvas节点然后取 node 上的 context再结合 requestAnimationFrame 驱动循环。下面是game.js里最小可用的初始化写法。// game.js 中初始化2d画布 initCanvas() { const query wx.createSelectorQuery() query.select(#gameCanvas) .fields({ node: true, size: true }) .exec((res) { if (!res || !res[0]) { console.error(未找到画布节点请检查game.wxml中的canvas id) return } const canvas res[0].node // 拿到canvas节点本身 const ctx canvas.getContext(2d) const dpr wx.getSystemInfoSync().pixelRatio // 设备像素比 canvas.width res[0].width * dpr canvas.height res[0].height * dpr ctx.scale(dpr, dpr) this.canvas canvas this.ctx ctx this.canvasWidth res[0].width // 逻辑像素宽 this.canvasHeight res[0].height // 逻辑像素高 this.startLoop() }) }初始化里最重要的两个参数是画布宽高和像素比。画布宽高如果不乘 dpr在iPhone这类高分屏上会发虚文字和鸟看起来有锯齿乘完 dpr 之后必须再执行一次 ctx.scale(dpr, dpr)不然坐标系又对不上触摸点位和绘制位置会错位。这个坑在真机调试里几乎每个人都会撞一次。循环部分我喜欢用 requestAnimationFrame它会跟随屏幕刷新率切到后台时自动暂停省电也省CPU。// 游戏循环主入口 startLoop() { const step () { this.update() // 更新小鸟位置、管道坐标、碰撞状态 this.draw() // 清屏并重绘所有元素 this.frameId this.canvas.requestAnimationFrame(step) } this.frameId this.canvas.requestAnimationFrame(step) } stopLoop() { if (this.frameId) { this.canvas.cancelAnimationFrame(this.frameId) this.frameId null } }注意这里用的是 canvas.requestAnimationFrame而不是全局的 requestAnimationFrame。在小程序里必须通过 canvas 节点上的方法注册回调循环才有正确的帧率行为用全局方法在部分安卓机上会出现计时漂移游戏越跑越快。update 和 draw 分离也是刻意为之update里只改数据draw里只读数据画图这样后期加暂停、加速、回放都只要控制 startLoop 和 stopLoop 就行。2.3 绘制顺序与碰撞管道生成、小鸟旋转的常见做法绘制顺序直接决定画面正确性飞翔的小鸟一般是“背景 → 管道 → 小鸟 → 分数”。背景在最底层分数在最上层管道夹在中间。每次重绘开始前要先清屏不清的话上一帧的残影就是一条拖尾在canvas游戏里属于经典翻车现场。// draw 方法里的清屏与背景 draw() { const ctx this.ctx ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight) // 背景先画天空再画匀速向左滚动的地面 ctx.fillStyle #4ec0ca ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight - 80) ctx.fillStyle #ded895 ctx.fillRect(0, this.canvasHeight - 80, this.canvasWidth, 80) // 地面滚动偏移量每帧左移移出屏幕后回到起点 this.groundOffset - this.groundSpeed if (this.groundOffset -this.canvasWidth) { this.groundOffset this.canvasWidth } this.drawPipes(ctx) // 管道 this.drawBird(ctx) // 小鸟 this.drawScore(ctx) // 分数 }地面滚动的逻辑是典型的“无限滚动”写法让地面图案每帧向左移动groundSpeed像素一旦偏移量超过画布宽度就加回一个画布宽度。这样玩家看到的地面永远是连续移动的实际上只是同一张图在循环位移。碰撞检测在这个demo里用矩形近似就够小鸟用一个略小于身体外框的矩形管道用上下两根矩形管子。精度不需要达到像素级给玩家留一点容错反而手感更好。// utils/collision.js 矩形碰撞检测 function checkCollision(rectA, rectB) { return rectA.x rectB.x rectB.w rectA.x rectA.w rectB.x rectA.y rectB.y rectB.h rectA.y rectA.h rectB.y } // game.js 里判断小鸟是否撞到管道 hitPipe() { const birdRect this.getBirdRect() // 管道数组里只检查当前屏幕区域内的两根 for (const pipe of this.pipes) { if (pipe.x this.canvasWidth 20) continue const topRect { x: pipe.x, y: 0, w: pipe.w, h: pipe.topH } const bottomRect { x: pipe.x, y: pipe.y, w: pipe.w, h: pipe.bottomH } if (checkCollision(birdRect, topRect) || checkCollision(birdRect, bottomRect)) { this.gameOver() return true } } return false }管道在循环里的更新逻辑是每帧把每根管道的x向左移动pipeSpeed当管道完全移出左侧屏幕后从数组里移除同时在右侧生成一根新的。管道的上下留一个gap变量这个gap就是小鸟能飞过去的口子gap越小难度越高。老手改难度基本都是动pipeSpeed和gap这两个值后面参数调整会细说。3. 在微信开发者工具里跑通前端导入、画布适配与重力参数调整3.1 导入源码后必改的三处配置appid、域名校验、基础库版本把前端miniprogram目录导入微信开发者工具时第一次编译大概率不会一次通过因为有三处环境相关的配置和原作者机器上的不一致。我一般按顺序处理这三处能省掉一半的报错排查时间。第一处是appid。用测试号还是自己的appid都行但要在 project.config.json 里确认。拿到的源码里如果写了原作者的appid直接用自己的替换不然工具会提示无权使用该appid。第二处是“不校验合法域名”开关它在工具右上角的“详情 → 本地设置”里这个开关决定wx.request能不能打到本地java服务联调阶段不打开基本没法玩。第三处是基础库版本在“详情 → 本地设置 → 调试基础库”里检查一下demo标注适用1221我习惯把基础库选到与1221标记匹配的版本附近至少不要低于2.9.0因为type2d的canvas节点取法依赖较新的基础库。这三处改完编译通过只是第一步页面能进、canvas能画出来才是真的跑通。如果编译后页面白屏先看console里有没有“未找到画布节点”这个报错我在后面避坑章节专门展开。3.2 画布尺寸与顶部导航栏高度让游戏铺满可视区域的参数设置canvas小游戏最容易被新手忽略的是画布尺寸适配。飞翔的小鸟是老式小游戏风格理想状态是铺满整个屏幕但小程序页面有导航栏底部可能还有tabBar直接把canvas宽高写成屏幕分辨率游戏会有一部分被遮挡或者触摸区域整体偏下。在做适配时我一般会先拿 wx.getWindowInfo() 拿到窗口尺寸再根据当前页面是否自定义导航栏决定画布可用高度。这里有个长期存在的差异点微信小程序顶部导航栏高度在不同机型上不是固定值iPhone的刘海屏、安卓的挖孔屏状态栏高度差很多直接把状态栏高度写死成20px的代码在真机上一大半机型会顶出黑条或漏白。正确做法是动态读取而不是写死。// game.js 里计算画布区域 initLayout() { const windowInfo wx.getWindowInfo() // 基础库2.20.1之后推荐用这个 const menuRect wx.getMenuButtonBoundingClientRect() // 没有自定义导航栏时顶部留给系统导航栏 let topBarHeight 0 if (!this.customNavigationBar) { topBarHeight menuRect.top menuRect.height / 2 } // 底部有tabBar时也要让出高度纯游戏页通常隐藏tabBar const bottomBarHeight 0 this.layout { x: 0, y: topBarHeight, width: windowInfo.windowWidth, height: windowInfo.windowHeight - topBarHeight - bottomBarHeight, } }menuRect 拿到的是胶囊按钮的位置用它的top加上高度的一半可以估算出状态栏到导航栏底部的距离比直接读 statusBarHeight 更贴近实际视觉。下面用 layout 的尺寸去初始化canvas的宽高画布和触摸区域就对上了。设置之后记得在 game.json 里配导航栏样式页面用自定义导航栏时 windowInfo 的取值会有变化这两处要配套着改。3.3 图片转canvas不依赖本地图片资源用纯绘图函数画小鸟拿到demo后如果发现小鸟是图片素材资源加载失败时会画不出去。小程序包体积限制2M放了大量图片素材的源码很容易超限而且 canvas 里 drawImage 一张大图在高分屏上还有缩放开销。我在学习版demo里更推荐直接用canvas绘图API画小鸟也就是把“图片转canvas”这一步反过来——不是导图片而是用代码生成图形。// 用canvas基础API画一只简单的小鸟 drawBird() { const ctx this.ctx const x this.bird.x const y this.bird.y const r this.bird.radius ctx.save() ctx.translate(x, y) // 旋转中心放到小鸟重心 ctx.rotate(this.bird.rotation) // 小鸟上仰/下俯的角度 // 身体 ctx.fillStyle #f5b642 ctx.beginPath() ctx.ellipse(0, 0, r * 1.2, r, 0, 0, Math.PI * 2) ctx.fill() ctx.strokeStyle #8a5a2a ctx.stroke() // 眼睛 ctx.fillStyle #ffffff ctx.beginPath() ctx.arc(r * 0.6, -r * 0.3, r * 0.32, 0, Math.PI * 2) ctx.fill() ctx.fillStyle #333333 ctx.beginPath() ctx.arc(r * 0.72, -r * 0.32, r * 0.15, 0, Math.PI * 2) ctx.fill() // 嘴 ctx.fillStyle #e25822 ctx.beginPath() ctx.moveTo(r * 0.9, 0) ctx.lineTo(r * 1.4, r * 0.1) ctx.lineTo(r * 0.9, r * 0.25) ctx.closePath() ctx.fill() ctx.restore() }这套画法看着代码量大但好处很明显不占包体积、不依赖外部资源加载、真机渲染稳定。用ellipse画身体用arc画眼睛用三角形画嘴就得到一只风格化的鸟。rotation这个值控制小鸟姿态上升时仰头、下落时俯身手感会比永远水平飞行真实很多。参数上小鸟的半径是10px重力是320px/秒平方点击一次给一个向上的瞬时速度。这几个值是学习版里最常见的初始配置具体改多少后面讲。参数调整思路记住一句话重力决定下落快慢瞬时速度决定点击灵敏度两个参数要一起调单改一个会出现“按了没反应”或“一按就撞顶”的手感失衡。4. 把java后端接起来分数上报接口、排行榜查询与联调参数4.1 后端接口设计POST /api/score 与 GET /api/rank 的数据结构这个demo的后端职责很小就两张表的事玩家分数和排行榜。正因为小接口设计反而是全项目里最值得研究的部分。后端要能接受前端在游戏结束时上报的分数也能返回一个排行列表给前端渲染。下面是我从学习版demo上总结出的一份接口约定前端和后端各自按这个对联调时基本不会吵架。接口方法入参出参上报分数POST /api/score{ playerId: stu_001, score: 66 }{ code: 0, message: ok }查询榜单GET /api/rank?limit10limit 控制返回条数{ code: 0, data: [ { playerId: stu_001, score: 66, rank: 1 } ] }查询个人最好成绩GET /api/score/player/{playerId}路径参数 playerId{ code: 0, data: { bestScore: 66 } }我习惯在接口里统一放一个 code 字段0表示成功非0表示业务错误。这样前端 wx.request 的 success 回调里先判 code再处理数据不会出现HTTP 200但业务上其实失败的情况。排行榜的分页用limit一个参数就够学习版不需要翻页取前10名或者前50名都是它。4.2 学习版后端选型Spring Boot还是原生Servletjava后端这一段很多人纠结该用Spring Boot还是原生Servlet。这是学习版demo里最常被问到的选型问题。原生Servlet胜在零依赖、启动快、逻辑透明一个 war 包丢进Tomcat就能跑还特别适合讲HTTP原理。但写起来啰嗦每个接口要手动处理请求解析和响应序列化中途加个拦截器还要翻web.xml。Spring Boot 则把“写一个能跑的HTTP接口”的成本降到了极低RestController 内嵌Tomcat命令行 java -jar 就能起来前后端联调时少很多环境问题。学习版的目的既然是快速看懂链路我给学生的提示是如果目标是理解HTTP和Servlet机制选手写的servlet如果目标是快速跑通demo并在此基础上加功能用Spring Boot把时间花在接口业务上更划算。下面是这个demo后端典型的Spring Boot接口写法对应的就是一个Controller加一个内存存储。这种写法没有连数据库数据存在JVM内存里重启就丢符合学习版的定位后续要接MySQL在Service层加个DAO即可。// ScoreController.java RestController RequestMapping(/api) public class ScoreController { private final ScoreService scoreService; public ScoreController(ScoreService scoreService) { this.scoreService scoreService; } // 上报分数 PostMapping(/score) public ResultVO reportScore(RequestBody ScoreVO scoreVO) { // ScoreVO 里只有 playerId 和 score 两个字段 scoreService.saveOrUpdate(scoreVO.getPlayerId(), scoreVO.getScore()); return ResultVO.success(); } // 查询排行榜 GetMapping(/rank) public ResultVO rank(RequestParam(defaultValue 10) int limit) { ListRankItem list scoreService.top(limit); return ResultVO.success(list); } // 查询个人最好成绩 GetMapping(/score/player/{playerId}) public ResultVO bestScore(PathVariable String playerId) { return ResultVO.success(scoreService.bestScore(playerId)); } }代码里的三个接口和前面的表格一一对应。RequestBody 让Spring自动把请求体JSON反序列化成ScoreVORequestParam 和 PathVariable 分别处理查询参数和路径参数。ResultVO 是一个简单的包装类统一code/message/data三层结构这样前端不管接哪个接口解析逻辑都一致。ScoreService 在示例里可以先用 ConcurrentHashMap 存分数考虑并发时用 ConcurrentHashMap.compute 做原子更新避免两个请求同时写同一个人分数时出现覆盖。4.3 小程序端request联调超时时间、请求头、合法域名白名单前端和后端接口约定好了小程序端要做的就是把 wx.request 封装成一个可复用的函数。下面是我常用的request封装几个参数第一次联调时就要确认。// utils/request.js 封装wx.request function request({ url, method GET, data {}, timeout 5000 }) { return new Promise((resolve, reject) { wx.request({ url: http://127.0.0.1:8080 url, // 联调时先用本机地址 method, data, timeout, // 单位毫秒默认5秒 header: { Content-Type: application/json }, success: (res) { if (res.statusCode 200 res.statusCode 300) { resolve(res.data) } else { reject(new Error(HTTP res.statusCode)) } }, fail: (err) reject(err), }) }) } module.exports { request }timeout 参数很容易被忽略。默认值在小程序里是60秒但实际游戏场景中玩家提交分数的请求如果5秒内不回界面早就切走了。我给游戏内请求设置的默认超时是5000毫秒这背后有两条经验一是本机联调时Java服务启动慢第一次请求容易超时可以先手动访问一次http://127.0.0.1:8080/api/rank让服务预编译二是真机预览时手机访问不到电脑的127.0.0.1要用局域网IP替换localhost同时确保电脑防火墙放行Java进程的端口。这里需要重点提的是合法域名。开发者工具里勾了“不校验合法域名”本机联调没问题但真机预览和上线时必须把接口域名配置到小程序后台的request合法域名里而且要HTTPS。学习版demo阶段建议先用局域网IP凑合联调真正部署时再上云服务器和域名不要一上来就折腾证书那不属于游戏demo本身的范畴。5. 避坑canvas小游戏和java后端联调的5个踩坑记录5.1 白屏canvas节点取不到console报“未找到画布节点”现象页面能进console里报“未找到画布节点请检查game.wxml中的canvas id”全屏白什么都画不出来。 原因最常见的是canvas标签写错了type或者game.js在onReady里还没完成节点渲染就去查询。我见过最典型的翻车是把type2d漏掉了老式canvas节点用fields({ node: true })是取不到node的。 解决先确认wxml里是 再确认game.js里查询逻辑放在onReady里而不是onLoad里最后确认wxss里canvas节点有明确的宽高display:none或宽高为0都会让查询失败。5.2 分数提交失败statusCode 404Java服务根本没收到请求现象游戏能玩一旦点提交分数请求秒失败开发者工具network面板里状态码404。 原因我在联调时最常遇到的是路径拼错或服务没启动。Spring Boot默认context-path为空接口是POST /api/score但前端request封装里如果多写了一个/或者漏了/api前缀404几乎必现。 解决先用工具或浏览器直接访问接口地址确认服务本身通再把前端url和Controller的RequestMapping比对一遍别少写或多写一个斜杠。另外确认Java服务确实监听了8080端口cmd里执行netstat -ano | findstr 8080看一眼没有输出就是服务没起来。5.3 中文乱码排行榜里玩家昵称变成问号现象分数能存能查但后端返回的玩家昵称或排行榜里的中文全部变成???。 原因Spring Boot接口如果没做编码配置响应JSON里中文序列化默认是UTF-8但小程序端解析时可能出现不一致更常见的是本地联调时用POST json但请求头里Content-Type没带charset导致后端读取请求体时按老编码解析。 解决Spring Boot里在application.properties加 server.servlet.encoding.forcetrue 和 server.servlet.encoding.charsetUTF-8前端request封装里把header写成 Content-Type: application/json; charsetutf-8。两边一起改基本不会再出现中文问号。5.4 触摸偏移点击位置和鸟的位置错位尤其真机明显现象在开发者工具里点击正常到真机上手指按的位置和小鸟跳起的时间明显对不上好像整个触摸区域偏了。 原因画布用了dpr缩放后触摸事件的坐标有时需要自己做转换尤其是页面有导航栏或安全区时。开发者工具模拟器窗口比例固定掩盖了这个问题真机上的顶部导航栏高度和安全区差异会放大偏移量。 解决确保canvas宽高和布局计算里让出的顶部高度一致。前端做触摸检测时优先用canvas节点上的坐标而不是全局pageX/pageYgetBoundingClientRect之后做一个差值换算把“手指在屏幕上的位置”转成“手指在canvas里的位置”不要拿原始坐标直接参与游戏逻辑。5.5 掉帧复杂场景下帧率上不去安卓低端机明显现象同一份代码iPhone上流畅安卓中低端机上小鸟飞行一卡一卡管道移动不连续。 原因canvas重绘时每帧都做了大量fillStyle赋值、beginPath和closePath绘制指令太多或者开启了跟游戏无关的定时器在页面不可见时还在重绘CPU和GPU被白白占用。 解决先把绘制指令优化把不变的颜色值在循环外定义好避免每帧重复赋值再把离屏缓存做起来背景中不变的部分画到一张离屏canvas上每帧只drawImage这张缓存而不是重新画几百个图元。具体做法在后面的进阶章节展开先记住一句话游戏循环里每帧都在执行的语句越少掉帧概率越低。6. 进阶从学习版到可上线版本帧率监控、离屏渲染与后端签名校验学习版demo跑通后真正决定它能不能变成作品的是三项基础工程能力帧率监控、离屏渲染、以及防止接口被人刷的签名校验。这三件事恰好对应了性能、体验和安全性是做canvas小游戏绕不开的三道坎。帧率监控是优化的前提。我给canvas小游戏加帧率统计的方式很简单在游戏循环里记录相邻两帧的时间戳维护一个每秒帧数数组打印最近60帧的平均帧率。低端机上如果平均帧率低于30说明绘制指令确实超标需要做离屏优化调到50以上手感才接近流畅。这个统计工具本身只花几十行代码非常划算。离屏渲染的做法更直接。飞翔的小鸟里天空和地面是静态背景管道重复出现这些元素每帧重新画完全没必要。我把背景先画到一张离屏canvas上游戏循环里每帧只做一次ctx.drawImage(offscreenCanvas, 0, 0)绘制指令从上百条降到个位数。要注意离屏canvas的尺寸也要乘dpr否则贴图模糊离屏canvas创建要放在初始化阶段不要在循环里反复创建。后端签名校验则是学习版升生产版最明显的一个分水岭。demo提交分数时如果什么都不校验任何人都能抓包后伪造一个POST请求把分数改成999999。我见过学员自建的排行榜被脚本刷爆整个榜单失去意义。常见做法是前端算出一个校验串比如把playerId、score和一个约定密钥拼接后用MD5生成sign后端对同一规则再算一遍不一致就拒绝写入。虽然后端可以查MD5这种散列算法但在学习项目里够用了真做上线产品就换HMAC和密钥动态下发。我最早给学员准备的榜单服务就没有校验结果一个周末排行榜前100全是脚本刷出来的999999连带着前端排名显示都出现了负数反转那次的教训让我把“接口防刷”写进了每次联调的检查清单。做项目别急着加炫酷功能把监控、性能、防刷这三样地基打好demo才会真的长大。希望帮到你也期待看到你改出能上线的那一版。本文还有配套的精品资源点击获取
返回列表