news 2026/10/1 2:26:43

福州永泰麻将微信小程序后端实战:Spring Boot + Redis + WebSocket架构解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
福州永泰麻将微信小程序后端实战:Spring Boot + Redis + WebSocket架构解析

简介:这是一份面向微信小程序后端开发的Java工程资源,围绕福州永泰麻将业务场景提供可运行的服务器端代码与基础配置,适合毕业设计、课程大作业或想了解小程序后端分层结构的Java学习者参考。压缩包共25个文件,整体约83KB,以17个Java源文件为主体,搭配properties配置、xml配置、jar依赖及mvnw构建脚本,覆盖数据访问、业务处理到启动运行的完整链路。已有80人浏览学习,文件体量虽小但目录组织清晰,main与test分明,便于快速定位入口类与核心模块。通过阅读源码,可学习Maven工程的标准目录结构、包划分方式、配置加载思路,以及小程序接口对接时的参数处理与返回格式约定;mp3素材与.gitignore等辅助文件也能帮助理解多媒体资源在后台项目中的存放方式。整体适合作为入门级小程序后端项目的剖析范本,用于借鉴其模块拆分与配置管理思路。

1. 福州永泰麻将微信小程序后端.zip:一个可交付的地方棋牌后端该有的东西

拿到“福州永泰麻将微信小程序后端.zip”这个标题,先别急着解压。它指的是一套完整的微信小程序后端工程包:玩家通过微信登录、创建房间、邀请好友、打牌过程中同步对局状态、结束后落牌局记录,运营方在后台管理用户和牌局。这类项目真正难的不是小程序前端,而是后端在登录鉴权、房间并发、牌局状态流转、消息推送、部署上线这几条链路上能不能接得住。这篇笔记适合两类人:准备自己搭地方棋牌后端的技术负责人,以及接手这类 zip 工程要做二次开发的 Java 后端。下面按一套能直接落地、能扛住真实对局的方案往下拆。

2. 后端技术选型与工程落地:为什么自建 Spring Boot 而不是微信云开发

2.1 微信云开发撑不起牌局状态管理

很多初学者看到“微信小程序后端”,第一反应是“微信云开发不是能写后端吗”。对工具类小程序,云开发确实省事,但麻将场景不一样。一局牌从发牌、摸打、碰杠到胡牌,几十个动作要在秒级内同步给四个客户端,还有断线重连、超时托管、流局判定,这些状态如果都存在云开发的文档型数据库里,每次动作都去查库更新,很快会被高频读写拖垮。云开发更适合表单、内容、低并发工具类业务,不是为“长时间会话型”对局设计的。

自建后端的好处在于,长连接、在线状态、分布式锁、定时任务全部握在自己手里,出了问题能看日志、能抓现场。我见过不止一个项目用云开发做到了 20 人同时在线就开始出现房间状态不一致,最后不得不迁回自建。选型上,常见做法是 Spring Boot + MyBatis-Plus + Redis + MySQL + WebSocket。Spring Boot 管接口和定时任务,MyBatis-Plus 省掉单表 CRUD,Redis 承担在线状态和牌局缓存,MySQL 只落最终结果。这套组合对几十到几百人同时在线的棋牌小程序完全够用,也不会被云厂商绑定。

2.2 工程骨架与核心配置

一份标准的 zip 后端工程,目录结构通常长这样:

mj-server/ ├── src/main/java/com/xxx/mj/ │ ├── controller/ # HTTP 接口层 │ ├── service/ # 业务逻辑层 │ ├── mapper/ # MyBatis 数据访问层 │ ├── entity/ # 数据库实体 │ ├── config/ # 全局配置 │ ├── common/ # 通用返回、异常、工具类 │ ├── task/ # 定时任务(清理过期房间等) │ └── websocket/ # 对局长连接 ├── src/main/resources/ │ ├── application.yml │ └── mapper/ # XML 文件 └── sql/ └── init.sql # 初始化表结构

拿到之后先看application.yml,这里集中了几乎所有会让你跑不起来的配置项:

server: port: 8080 spring: datasource: url: jdbc:mysql://127.0.0.1:3306/mj_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: ${DB_PASSWORD} redis: host: 127.0.0.1 port: 6379 database: 0 timeout: 3000ms wx: miniapp: appid: ${WX_APP_ID} secret: ${WX_APP_SECRET} mj: room: expire-minutes: 120 token: expire-hours: 72

参数说明:数据库连接串里的serverTimezone=Asia/Shanghai一定要有,否则 JDBC 驱动和 MySQL 的时区不一致会导致时间字段差 8 小时,后面做牌局复盘时非常痛苦。DB_PASSWORD、WX_APP_ID、WX_APP_SECRET都走环境变量注入,不要写死在代码里,否则 zip 一旦传到公共仓库,密钥就泄露了。mj.room.expire-minutes控制房间的自动清理时间,麻将房间不是永久会话,两小时没动静就应该回收,靠的是定时任务扫描 Redis 里的房间 key。token 过期设 72 小时,保证玩家第二天打开小程序不用重新登录,又不至于让 token 长期有效增加被盗用风险。

2.3 从 zip 到能跑起来的三个步骤

第一步,本地把工程构建起来。如果 zip 里带 Maven 仓库缓存,直接mvn spring-boot:run就能启动;没有的话先执行打包,首次拉依赖会比较久:

mvn clean package -DskipTests java -jar target/mj-server.jar --spring.profiles.active=dev

注意 JDK 版本。Spring Boot 2.7 通常要求 JDK 8 或 11,Spring Boot 3.x 必须 JDK 17。如果 zip 里的 pom.xml 用的是老版本,别直接拿 JDK 17 跑,先看java -version和 pom 里java.version是否一致,不一致就切 JDK 版本,省得踩一堆莫名其妙的编译报错。

第二步,初始化数据库。一般在sql/目录下有一份建表脚本,核心表就四张:

CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE, nickname VARCHAR(64), avatar_url VARCHAR(512), created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_room ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_no VARCHAR(6) NOT NULL UNIQUE, owner_user_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT '0等待 1对局中 2已结束 3已取消', rule_template VARCHAR(32) COMMENT '规则模板编码', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_game_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, room_id BIGINT NOT NULL, round_no INT DEFAULT 1, winner_user_id BIGINT, score_json TEXT COMMENT '本局各玩家积分变化', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE t_pay_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, user_id BIGINT NOT NULL, amount INT NOT NULL COMMENT '金额,单位分', status TINYINT DEFAULT 0 COMMENT '0待支付 1已支付 2已退款', wx_transaction_id VARCHAR(64) );

说明:t_user.openid是玩家唯一标识,直接建唯一索引,防止微信登录回调重复插入。t_room.room_no是六位房间号,玩家输入房间号加入,唯一索引能挡住并发下重复开房的脏数据。t_game_record.score_json用 JSON 存积分变化,因为不同规则模板产生的得分项不一样,单靠字段存不下。t_pay_order是后面支付幂等的基础,订单号必须有唯一约束,这一点在第 4 章还会展开。

第三步,配置微信小程序后台。登录微信公众平台,在小程序后台的“开发管理 - 开发设置”里配服务器域名。HTTP 接口配到 request 合法域名,WebSocket 地址配到 socket 合法域名,都必须 HTTPS/WSS,且域名不能带端口。本地联调阶段后端 IP 没法配进去,常见做法是内网穿透或者在小程序开发者工具里勾选“不校验合法域名”,但真机预览必须走正式域名。这一步没做好,你会发现后端起来了、数据库也有数据,小程序却一直报request:fail url not in domain list,这就是域名白名单没配或者填错了。

3. 永泰麻将的核心后端实现:登录鉴权、房间与状态机

3.1 微信登录 code2Session 与自定义 token 的完整时序

小程序端wx.login()拿到的 code 是临时凭证,后端拿它去微信接口换 openid 和 session_key,这个交换只能在服务端做。给出一段可用的登录服务代码:

@Service public class WxAuthService { @Value("${wx.miniapp.appid}") private String appid; @Value("${wx.miniapp.secret}") private String secret; private final RestTemplate restTemplate = new RestTemplate(); public LoginResult wxLogin(String code) { String url = String.format( "https://api.weixin.qq.com/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code", appid, secret, code); Map<String, Object> resp = restTemplate.getForObject(url, Map.class); if (resp == null || resp.containsKey("errcode")) { throw new BizException("wx login failed: " + resp); } String openid = (String) resp.get("openid"); String sessionKey = (String) resp.get("session_key"); User user = userMapper.selectByOpenid(openid); if (user == null) { user = new User(); user.setOpenid(openid); userMapper.insert(user); } String token = UUID.randomUUID().toString().replace("-", ""); redisTemplate.opsForValue().set( "mj:token:" + token, openid, 72, TimeUnit.HOURS); return new LoginResult(token, user.getId()); } }

参数说明:code只能用一次,而且有效期五分钟,前端每次wx.login()都要重新获取,不能把旧 code 缓存起来重复请求。接口返回的session_key永远不要下发到前端,它用于解密手机号等敏感数据,一旦暴露,用户数据就等于脱了裤子。后端自己生成 UUID token 作为登录态,Redis key 设计成mj:token:{token},value 存 openid,检索时反查 openid 来识别用户身份。72 小时过期意味着玩家需要重新登录时,小程序端会收到 401 错误码,这时候前端再主动调用wx.login()换新 code,循环就闭环了。

3.2 房间与牌局状态机:从等待开局到流局

麻将房间是有生命周期的,后端必须用状态机约束流转,否则会出现“已结束的房间还能出牌”“流局了还能继续摸牌”这种低级事故。状态枚举定义如下:

public enum RoomStatus { WAITING(0, "等待玩家"), PLAYING(1, "对局中"), FINISHED(2, "已结束"), CANCELED(3, "已取消"); private final int code; private final String desc; RoomStatus(int code, String desc) { this.code = code; this.desc = desc; } }

状态流转规则:

当前状态触发动作下一状态校验条件
WAITING房主点击开始PLAYING房间内玩家数 >= 2
PLAYING有人胡牌/流局FINISHED本局得分已结算写入
WAITING房主解散或超时CANCELED无人在对局中
PLAYING全员同意解散CANCELED需要四人确认或三分之二确认

操作房间状态时,不能先查再改,那是典型的并发事故现场。正确做法是让 Redis 执行原子操作,用一个 Lua 脚本校验当前状态并推进:

local key = KEYS[1] local expected = ARGV[1] local target = ARGV[2] local current = redis.call('get', key) if current == expected then redis.call('set', key, target) return 1 end return 0

这段脚本的意思是:只有当前状态等于期望值时,才把状态改成目标值。比如只有 WAITING 状态才能变 PLAYING,如果房间已经有人在打,expected=0就会失败。用 Redis 是因为房间状态缓存在 Redis 里,读写快,Lua 脚本保证这一段是原子的,不会出现两个请求同时读到 WAITING、同时都改成 PLAYING 的竞态。状态落库是事后异步做的,MySQL 里只留最终结果,中间过程全部以 Redis 为准。

牌局内还有一层更细的状态:当前圈数、当前庄家、当前出牌人。这些不适合塞在 RoomStatus 里,所以单独设计一个mj:room:{roomId}:game的 Redis Hash,字段包括round、dealer、currentPlayer、remainingTiles。整局打完后一次性写t_game_record,Redis 里的临时数据直接删除,不做持久化。这样做的原因是麻将过程数据高频写入,MySQL 扛不住,丢了也不可惜,只要结果对就行。

3.3 地方规则配置化:金、番型、胡牌校验都不要写死

永泰麻将和福州其他区县在用不用“金”(百搭牌)、封顶番数、是否允许吃牌上并不完全一致。如果这些规则写死在代码里,每次调整都要重新发版,运营方会在微信群骂人。常见做法是做成规则模板表:

CREATE TABLE t_rule_template ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(32) COMMENT '规则模板名,如永泰经典、永泰带金', enable_gold TINYINT DEFAULT 1 COMMENT '是否启用金', gold_count INT DEFAULT 4 COMMENT '金的数量', max_score INT DEFAULT 200 COMMENT '封顶番数', allow_chi TINYINT DEFAULT 0 COMMENT '是否允许吃牌', allow_gang_score TINYINT DEFAULT 1 COMMENT '杠是否计分', created_at DATETIME DEFAULT CURRENT_TIMESTAMP );

后端在创建房间时把ruleTemplateId写进房间,对局过程中胡牌判定、得分计算都通过规则引擎读取这份配置。接口层面提供一张配置表给管理员维护,不需要动代码。真正的胡牌牌型判断(比如平胡、七对、十三烂)还是需要专门写算法模块,但“哪些玩法开不开启、加多少分、封顶多少、金是不是百搭”全部交配置。这样每个房间创建时选一套模板,后端只是执行器。

规则配置化的另一个好处是方便 A/B 测试。比如运营方想试点“金改成两张、封顶 100 番”的新玩法,只需要再造一个模板,房间号发给目标用户组,不需要为这批人单独部署一套后端。维护成本差别非常大。

4. 前后端分离下的关键对接:请求封装、跨域、WebSocket 与支付回调

4.1 小程序请求封装和后端拦截器对齐

前后端分离的第一步不是写接口,而是约定错误码。我一般统一规定:0成功,401登录过期,403无权限,409业务冲突,500服务端异常。小程序端所有请求都走同一个封装,不散落在业务页面里:

const request = (url, method, data) => { return new Promise((resolve, reject) => { wx.request({ url: 'https://api.example.com' + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') }, success: (res) => { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); return; } if (res.data.code !== 0) { wx.showToast({ title: res.data.msg, icon: 'none' }); reject(res.data); return; } resolve(res.data.data); }, fail: reject }); }); };

对应的后端拦截器负责统一校验 token,业务代码里不再重复写“取 token、查 openid”这几行:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); String openid = redisTemplate.opsForValue().get("mj:token:" + token); if (openid == null) { response.setStatus(401); return false; } request.setAttribute("openid", openid); return true; } }

注意一个细节:前端失败分支里做了wx.navigateTo跳登录页,后端 401 响应就能驱动前端整个会话重置。两个端必须对“401 代表什么”达成一致,否则前端会一直带着过期 token 请求,后端一直拒绝,用户看到的就是“页面白屏、接口全是红”。另外,token 放在Authorization头而不是 URL 参数里,避免出现在 Nginx access log 中,减少泄露面。

4.2 后端跨域与 Nginx 反代:一个走 CORS 一个走反向代理

小程序端没有浏览器同源策略问题,但管理后台是 Web 端,所以后端必须处理跨域。开发环境直接在 Spring Boot 里配置 CORS:

@Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("http://localhost:*", "https://admin.example.com") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

生产环境反而不建议靠后端 CORS,而是用 Nginx 统一反代,后端只监听内网端口。Nginx 核心配置如下:

server { listen 443 ssl; server_name api.example.com; location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /ws/ { proxy_pass http://127.0.0.1:8080/ws/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_read_timeout 3600s; } }

参数说明:location /api/后面的proxy_pass带不带结尾/语义完全不同。写成http://127.0.0.1:8080/,请求/api/user/info会被转发成/user/info;如果去掉/,会转发成/api/user/info。后端 Controller 的 RequestMapping 决定了用哪种,很多排错半天发现自己接口 404 就是这里出了问题。WebSocket 必须显式设置Upgrade和Connection "upgrade"头,否则 Nginx 默认当普通 HTTP 转发,长连接压根建立不起来。proxy_read_timeout 3600s是针对 WebSocket 的,默认 60 秒就会断开空闲连接,对局中玩家可能在思考,超过 60 秒没消息就被 Nginx 掐断,所以必须调大。

4.3 WebSocket 对局推送:掉线重连与心跳保活

麻将的四个玩家必须实时看到彼此的出牌,HTTP 轮询做不到秒级同步,WebSocket 是必需品。服务端用 Spring WebSocket 注册一个专门处理对局的 Handler:

@Component public class GameWebSocketHandler extends TextWebSocketHandler { private final Map<Long, WebSocketSession> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) { Long roomId = (Long) session.getAttributes().get("roomId"); sessions.put(roomId, session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) { // 解析消息,核对当前出牌人,执行动作 } public void sendToRoom(Long roomId, String payload) { WebSocketSession s = sessions.get(roomId); if (s != null && s.isOpen()) { s.sendMessage(new TextMessage(payload)); } } }

心跳保活是必须做的。客户端每 30 秒发一个ping,服务端返回pong,连续三次没有收到就判定掉线,把玩家标记为托管状态,牌子由后端代为随机出。前端在小程序里监听onSocketClose后要自动重连,不能用固定间隔,要指数退避:第一次 1 秒、第二次 2 秒、第三次 4 秒,最多等 60 秒。因为如果服务端正在重启,所有客户端同时重连会造成连接风暴,退避能错开重连时间。

4.4 支付回调与幂等处理:避免同一订单入账两次

棋牌场景常见的是购买房卡或道具,走微信支付。支付回调是异步通知,微信可能因为是同一个notify_url发送多条通知,如果后端不做幂等控制,就会出现用户买一张房卡到账两张的“资损事故”。回调处理的核心是先验签再查询订单状态:

public void handlePayNotify(String body, String signature) { boolean valid = verifyWxpaySignature(body, signature); if (!valid) { throw new BizException("invalid sign"); } PayNotifyData data = decryptNotifyData(body); // 微信支付V3报文是加密的 PayOrder order = payOrderMapper.selectByOrderNo(data.getOutTradeNo()); if (order == null) { throw new BizException("order not found"); } if (order.getStatus() == 1) { return; // 已支付,直接幂等返回 } if (order.getAmount() != data.getAmount()) { throw new BizException("amount mismatch"); } payOrderMapper.updateStatus(order.getId(), 1); userAssetMapper.addCards(order.getUserId(), 1); }

幂等的关键在“先查状态,已支付直接返回”这几行。但注意这个过程仍有并发漏洞:如果两条回调同时到达,都查到 status=0,就会执行两次到账。所以要给t_pay_order加一个“状态更新”的原子 SQL,UPDATE t_pay_order SET status = 1 WHERE id = ? AND status = 0,受影响行数为 0 说明别人已经处理过,本线程直接放弃。这是用数据库行锁兜底,比任何代码层面的 if 判断都可靠。另外,支付回调的报文在微信支付 V3 里是加密的,必须先解密再取字段,不能直接把out_trade_no拿出来当明文用。iOS 小程序虚拟支付受应用商店限制,客户端要做好“iOS 端隐藏购买入口、只走积分或实物兑换”的处理。

5. 上线前后最容易翻车的四类问题:审核、并发、越权与部署

5.1 微信小程序审核被拒:不是代码问题,是类目和资质问题

现象:代码全部写完,域名配好,管理后台也正常,提交审核后收到“涉及棋牌服务,需提供相应资质”的拒审通知。

原因:微信公众平台对棋牌类小程序有专门的类目要求,选择了“游戏”类目但没有版号,或者选了生活服务类目但实际功能明显是棋牌对局,都会被判定为类目与内容不符。

解决:提交审核前先确认类目选择,棋牌对局需要按平台要求选择对应类目并上传相应的资质材料,比如软件著作权证书。审核版本里要准备测试账号,账号里预置房卡和积分,方便审核人员在真机上完整体验“开房-进入-开局-结束”全流程。如果审核人员进不了房间,或者一进来就要付费,多半直接拒绝。另外把审核版本的后端独立部署一套,不能和线上正式环境混在一起,否则审核人员测试时产生的脏数据会污染正式玩家。

5.2 并发开房与入座:本地测不出来的竞态

现象:上线第一天 20 人同时开房,出现两个房间号一样、或者一个玩家同时加入两个房间的情况。本地测试只有几个人,永远复现不了。

原因:房间号是后端生成后查库判重,两个请求同时生成同一个号,同时查库发现不存在,再同时插入,后插入的那个被唯一索引挡住,但前端已经收到了成功响应。入座也是一样,先查房间人数再插入成员,两个请求同时查到 3 人,都判断可以入座,最后变成了 5 人局。

解决:房间号生成后不要查库,直接在 Redis 里用INCR加一个自增前缀,拼上随机位,Redis 的单线程模型天然保证不重复。入座操作改成一次原子 Lua 脚本:

local roomKey = KEYS[1] local memberCount = tonumber(redis.call('scard', roomKey)) local maxCount = tonumber(ARGV[1]) if memberCount < maxCount then redis.call('sadd', roomKey, ARGV[2]) return 1 end return 0

脚本在判断人数和写入成员之间没有间隙,两个请求不会同时通过。严格按照“校验和写入必须在同一个原子操作里”这个原则去审查其他接口,能规避掉大部分并发问题。

5.3 接口越权:只校验登录不校验归属

现象:玩家把请求里的房间号改成别人的房间号,居然能拉出对方的牌局记录;把用户 ID 改成任意数字,能查到别人的积分。

原因:后端的鉴权拦截器只确认了“你有 token、你是合法登录用户”,没确认“你有权操作这个房间”。对局接口拿到的 roomId 是前端传的参数,服务端没有校验调用者和房间的关系。

解决:凡是操作房间的接口,服务端必须从 token 取出 openid,再查这个 openid 是否在房间成员表里,不在就直接拒绝。这个校验放在 service 层,不能依赖前端传 userId。写接口时的检查清单:查列表接口要限定“只能查自己”;改资源接口要“先查归属再改”;删除接口要“房主才能删”。棋牌场景里最怕的是恶意玩家通过遍历房间号看别人牌局,这种漏洞在审核时不一定被查出,但上线后被同行盯上就麻烦了。

5.4 上线前一天的三件事:HTTPS、时区、日志

现象:真机预览时 Android 能连上,iOS 连不上;日志时间和实际时间差 8 小时;服务跑了一周,排查问题时发现日志文件只有几行。

原因:iOS 对 HTTP 明文限制比 Android 严格,小程序正式环境要求所有接口必须 HTTPS,如果证书没配好或者后端只开了 HTTP,iOS 直接拒绝请求。服务器时区默认 UTC,MySQL 连接串里没指定serverTimezone,日志时间全是 8 小时前的。日志级别配成 WARN,平时不输出 INFO,排查问题时啥也看不到。

解决:上线前用curl -I https://api.example.com确认证书链完整,小程序开发者工具里关掉“不校验合法域名”再跑一遍全部接口。服务器执行timedatectl set-timezone Asia/Shanghai,MySQL URL 里保留serverTimezone=Asia/Shanghai,日志级别改成:

logging: level: com.xxx.mj: INFO org.springframework.web: INFO

日志是线上排障的唯一线索,省什么都不能省日志。另外给日志落盘加个按天切割,单文件超过 200MB 后处理起来非常痛苦。

6. 上线后的验证与复盘技巧

6.1 用一条 SQL 串起整局牌的过程

线上出问题后,最快的定位方式是拿一个房间号,把从建房间到结算的所有记录串起来看:

SELECT r.room_no, r.status AS room_status, g.round_no, g.winner_user_id, g.score_json, g.created_at FROM t_room r LEFT JOIN t_game_record g ON g.room_id = r.id WHERE r.room_no = '123456' ORDER BY g.round_no;

这条 SQL 能看到这个房间开了几局、每局谁胡、积分怎么变化。如果发现某局 winner_user_id 为空但 score_json 有值,基本可以确定是流局分支的数据没写干净。再把时间拉出来和玩家反馈“这局有问题”的时间点对比,就能锁定是某个版本引入的缺陷还是脏数据。这种“以房间号为主键的复盘思维”贯穿整个运维周期,每当运营反馈“某个人好像分不对”,都是用这套查询把上下文捞回来。

6.2 升级前先灰度:按房间号把流量切到新节点

WebSocket 服务不能像普通 HTTP 服务一样直接重启,重启瞬间所有玩家都掉线。我的做法是有两个以上的后端节点时,Nginx 上游只把一部分房间号的连接分到新节点,验证新版本跑了一天没有问题,再把剩余节点切换。切换完不要马上停旧节点,等旧节点上所有对局自然结束再下线。在线长连接服务最忌讳“重启大法”,先放量、再观察、后全量,这个节奏能帮你避开绝大多数线上事故。

我自己的习惯是,每改完一个和房间状态相关的代码,都要先开两个微信号、自己和自己打一局,确认状态流转、积分结算、掉线重连都正常,再提交测试。做这类后端,不怕逻辑复杂,怕的是你连“哪一步状态不对”都发现不了。希望这些路径和坑位能帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/1 2:24:59

C# + YOLOv8 + TensorRT + ByteTrack:上位机实时目标检测追踪方案

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 2:24:57

从WSL2到物理机:Nextcloud私有云盘部署与性能调优

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/1 2:23:23

图的存储结构(哈喜老师版本)

1、邻接矩阵 1.1&#xff1a;邻接矩阵的概念1.2&#xff1a;用邻接矩阵存储图对应的代码#define Max_Vertex_Num 20 //定义最大顶点数量 typedef char VertexType; typedef struct{int vexnum,arcnum; //目前图中实际的顶点数和边数VertexType vexs[Max_Vertex_Nu…

作者头像 李华
网站建设 2026/10/1 2:22:19

BertTokenizer深度解析:从WordPiece原理到文本分类实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华