微信小程序彩票系统开发:选号状态管理、后端接口与资金冻结实践
简介一套完整的微信小程序彩票销售与管理系统源码面向需要开发彩票类小程序或学习小程序前后端整合的开发者。资源共1407个文件以977个PHP后端文件为主体另含110个JSON配置、61个JavaScript脚本、45个WXML与46个WXSS小程序页面样式文件以及图片、Markdown文档等整个压缩包约2.84MB。从代码片段与项目结构可看出系统覆盖双色球、大乐透、七星彩的选球操作、追加投注、合买、开奖信息查询、用户账户资金管理及中奖信息展示等模块后端基于CodeIgniter框架实现登录、用户、Tunnel等交互接口前端通过数据绑定管理选球与用户状态。目前已有534人学习/下载。资源目录结构清晰既包含小程序端wxml/wxss/js页面也包含后端PHP控制器与模型适合作为彩票类小程序开发的完整参考项目也可用于毕业设计或课程实战训练。1. 为什么一个彩票小程序要把选号、合买和开奖拉流拆成三层彩票类微信小程序的真实代码难点从来不在页面渲染而在状态同步。比如你点一个“双色球”的红球前端要立刻知道当前已选个数、是否与蓝球重复、是否超出单式投注上限提交合买时后端要校验期号是否封期、账户可用余额是否足够、追加投注的倍率怎么折算。拿到这份《微信小程序彩票销售与管理系统》源码最值得读的不是某个页面动画而是小程序页面作为纯前端如何用一个全局 data 对象把双色球选球、大乐透选球、合买详情、开奖信息、账户资金全部串起来。你如果想把这套代码跑通或改成自己的课程设计建议按“前端选球状态机——后端 PHP 接口——开奖与资金对账”三层来看。适合想同时练微信小程序和 CodeIgniter 后端的人不适合只想要成品页面的伸手党。2. 前端选球状态机ballsList 的数据结构与双色球/大乐透/七星彩的选号联动彩票类小程序前端最大的坑是“彩票种类多导致页面逻辑互相污染”。双色球要红球蓝球大乐透要前区后区加追加七星彩要按位取值。这个源码里把每种彩种的当前选球分别放在一个列表页面切换彩种时只需要切换渲染目标不需要清空其他列表这样用户来回切换也不会丢已选号码。2.1 三种彩种的选球数据结构小程序页面的 data 里有一组基础字段data: { ballsList: [], // 双色球选球 daletouBallsList: [], // 大乐透选球 qixingcaiBallsList: [], // 七星彩选球 zhuijia: false, // 大乐透是否追加 login: , // 保存用户登录后的 token code: , // 手机短信验证码 account: { totalmoney: 0, dongjiemoney: 0, usemoney: 0, } }ballsList并不是简单的一维数组而是一个“注”的集合。双色球一注包含 6 个红球和 1 个蓝球所以常见结构是[{ reds: [1,2,3,4,5,6], blue: 8 }]。源码中直接给空数组初始化是为了后续setData时能整体替换。我一般会在onLoad时根据彩种类型给每个列表一个内部 schema比如双色球默认值{ reds: [], blue: null, count: 1 }避免在wx:for中访问item.reds.length时出现 undefined 告警。不同彩种的选号规则差异也决定了数据结构不能统一。双色球红球从 1-33 中选 6 个蓝球从 1-16 中选 1 个大乐透前区从 1-35 中选 5 个后区从 1-12 中选 2 个七星彩则是从 0000000-9999999 中按位选 7 个 0-9 的数字。如果强行把三种彩种放进同一个ballsList后面的金额计算和开奖比对都会混乱。源码把daletouBallsList和qixingcaiBallsList单独拆出正是为了在提交订单时能直接取到对应彩种的原始结构不需要再根据彩种类型做二次转换。2.2 追加投注与合买详情状态标记zhuijia这个布尔值只影响大乐透。前端点击“追加投注”开关时通常是这样处理的toggleZhuijia(e) { const checked e.detail.value; this.setData({ zhuijia: checked }); this.calcTotalMoney(); }calcTotalMoney需要先判断当前选中的是哪种彩种再拼接注数和金额基数。大乐透追加后单注从 2 元变 3 元但这里容易和合买逻辑混淆合买详情里的每一份金额不是简单的“总金额 / 份数”因为如果合买发起人选择了追加那么每一份也必须按追加后的单价计算。源码中shuangHemai、daletouHemai、qixingcaiHemai三个数组保存三种彩种的合买订单订单里如果没有保留is_add标记等开奖时奖金计算就会出错。我在项目中会额外增加一个unit_price字段后端下单时存入前端只做展示。合买列表还有一个常见问题开奖后合买状态需要刷新。如果本地只存订单数据不校验期号用户看到的还是上一期进度。所以我每次进入合买页面都会请求issue与本地dangQi比较不一致就清空列表并重新拉取避免前端缓存污染。这里最直接的检验方式是抓包看 Network 面板如果进入合买页只发了一次请求说明本地根本没有做期号失效判断。2.3 登录态保存与 openid 的边界代码中login: 这个字段很容易被误认为直接存 openid。看后端Login.php控制器它是先用wx.login的临时 code 去微信接口换取 openid然后返回一个服务端 token前端把 token 存到这里。这里有一个安全边界openid 不能暴露给小程序页面否则任何人拿到 openid 都可以冒充用户。正确做法是后端返回 token前端把这个 token 放入wx.request的 header 里。wx.request({ url: https://api.example.com/user/prize, header: { X-Token: app.globalData.login } });如果后端没有维护 token 与 openid 的映射仅仅是把 openid 返回给前端那么这个登录体系在正式环境是不合格的。至少要做一层签名比如token md5(openid secret timestamp)并设置过期时间。短信验证码code字段也是一样只能作为前置校验真正的登录凭据必须由后端签发。实际开发中wx.login拿到的 code 有效期只有 5 分钟前端必须保证在用户进入首页后尽早调用登录接口否则用户选完号码再登录code已经过期后端会返回401或40029。这类错误在开发者工具里不常见但在真机上因为网络慢而频繁出现所以登录态刷新逻辑不能只写在onLoad里还要在每个关键请求的 401 回调里重新走一遍wx.login - 后端换 token。2.4 选球点击事件的幂等处理选球交互最容易出现重复点击。以双色球红球选择为例点击同一个号码两次应该取消选择而不是再添加一次。常见代码是tapRed(e) { const num e.currentTarget.dataset.num; const { reds } this.data.ballsList[0]; const idx reds.indexOf(num); if (idx -1) { reds.splice(idx, 1); } else { if (reds.length 6) { wx.showToast({ title: 最多选6个红球, icon: none }); return; } reds.push(num); } this.setData({ ballsList[0].reds: reds }); }这里的dataset.num必须是字符串或数字保持一致。如果号码是01这种补零格式而数组内是数字1indexOf会一直返回 -1导致号码无法取消。我在源码里看到的shuangseqiuRedballs是开奖号码数组开奖号码一般带前导零所以选号状态存储建议统一用字符串避免“01”和“1”重复。同时setData的路径写法ballsList[0].reds比ballsList: newList更精准不会触发其他列表的 diff 计算在低端机上交互会流畅一点。三种彩种的选球规则可以汇总成一张表方便后端接口设计时直接复用彩种选球列表单注金额追加规则双色球ballsList2 元不支持大乐透daletouBallsList2 元追加后 3 元七星彩qixingcaiBallsList2 元不支持这些规则不仅前端要用后端下单接口也要保留一份。前后端不一致的结果是前端显示 6 个红球后端却只收到 5 个数据库订单生成失败用户支付了钱却拿不到票。所以我会在下单接口增加一个balls_count校验字段后端根据彩种类型检查提交的号码数量不合法直接返回错误码。3. 后端 PHP 接口与期号管理CodeIgniter 控制器拆层与资金冻结这份源码的后端选用了 CodeIgniter 3 风格application/controllers下有Welcome.php、Login.php、User.php、Tunnel.php。其中Tunnel通常用于长连接或消息推送但彩票小程序核心接口仍以 HTTP JSON 为主。这样拆的优势是登录、用户、业务通道各自独立不会因为一个控制器过大而互相影响。Welcome.php基本是框架默认控制器生产环境应该改成直接跳转或返回错误 JSON而不是渲染welcome_message.php。如果哪个接口返回了 HTML 而不是 JSON第一反应就是查默认路由是否被覆盖。3.1 控制器与模型的边界以 Login 为例Login.php负责把小程序端的临时 code 换成 openid再生成用户会话。控制器里一般不直接写 SQL而是调用User_model。下面是一个简化版实现public function wxLogin() { $code $this-input-post(code); $url https://api.weixin.qq.com/sns/jscode2session . ?appid . APPID . secret . APPSECRET . js_code . $code . grant_typeauthorization_code; $res file_get_contents($url); $data json_decode($res, true); if (isset($data[openid])) { $token md5($data[openid] . time()); echo json_encode([code 0, token $token]); } else { log_message(error, wx login err: . json_encode($data)); echo json_encode([code 1, msg login failed]); } }参数说明js_code是前端wx.login接口返回的临时凭证5 分钟有效grant_type固定为authorization_code。代码里的APPID和APPSECRET在正式项目里应放在config.php或环境变量中不能出现在控制器顶部硬编码。file_get_contents在请求微信接口时有两个隐患一是没有超时控制二是小程序服务端要求 IP 白名单如果网络抖动会导致接口假死。我一般用 cURL 并设置CURLOPT_TIMEOUT为 3 秒同时把请求日志写到logs目录。控制器和模型的边界在于控制器只做参数接收和响应输出模型负责数据库操作和外部接口调用。源码里Login.php直接执行了file_get_contents虽然能跑但后续如果要在本地单元测试这个外部依赖非常难受。更好的做法是把微信接口调用放到application/libraries/WxApi.php控制器里调$this-wxapi-code2Session($code)这样返回的 openid 和错误码都能统一处理。3.2 期号dangQi与封期校验前端保存了dangQi: 2017137这是下单时携带的期号。后端在生成订单前必须校验期号是否仍是“当前销售期”。CodeIgniter 的模型里可以这样写public function getSellingIssue($lotteryType) { $sql SELECT issue, end_time FROM lottery_issue WHERE lottery_type ? AND status 1 LIMIT 1; $query $this-db-query($sql, [$lotteryType]); return $query-row_array(); }status 1表示该期正在销售中end_time是封期时间。如果当前时间已经超过end_time即使状态还是 1也不允许下单。判断时间的 SQL 用WHERE status 1 AND CURRENT_TIMESTAMP end_time这样做的好处是把封期判断下沉到数据库避免 PHP 应用服务器时钟不一致带来的偏差。下单接口收到一个dangQi参数后先用当前时间查getSellingIssue比对issue是否相等不相等直接返回“期号已过期”。注意不能用前端传的dangQi当主键去查询因为旧期号仍然存在查询结果会让校验失效。真实场景里双色球每周二、四、日开奖每期截止时间通常是开奖当晚 20:00。源码里dangQi是写死的2017137说明它只是示例数据。如果要变成可维护的版本建议在lottery_issue表增加close_time字段并用定时任务或数据库事件把过期期的status改成 0。否则到了新一期status仍然为 1用户会拿着上一期的彩票来兑奖后端无法判断该期是否已开奖。3.3 用户资金账户与冻结事务account对象里有totalmoney、dongjiemoney、usemoney三个金额。这里有一个易误解点用户充值时只会增加totalmoney和usemoney发起投注时usemoney减少、dongjiemoney增加等额资金开奖未中奖时dongjiemoney减少中奖则增加usemoney并返回奖金。数据库表至少要这样设计字段类型说明user_idint关联用户表totalmoneydecimal(10,2)历史累计充值不随消费减少usemoneydecimal(10,2)当前可用余额dongjiemoneydecimal(10,2)冻结中的资金versionint乐观锁版本号扣款和冻结必须在一个事务里完成避免用户并发下单导致负数余额。CodeIgniter 的事务代码$this-db-trans_start(); $this-db-query( UPDATE member_account SET usemoney usemoney - ?, dongjiemoney dongjiemoney ?, version version 1 WHERE user_id ? AND usemoney ? , [$amount, $amount, $uid, $amount]); $affected $this-db-affected_rows(); if ($affected 0) { $this-db-trans_rollback(); return [code 1, msg 余额不足]; } $this-db-insert(bet_order, $orderData); $this-db-trans_complete();代码逻辑说明先执行原子更新用WHERE usemoney ?保证扣款不会为负如果影响行数为 0说明余额不足直接回滚。这里version字段是乐观锁辅助在后续对账时如果发现扣款成功但订单插入失败可根据version反转。注意 CodeIgniter 3 的trans_start是启动事务trans_complete提交如果要回滚必须用trans_rollback不能只return。这里还有一个容易忽略的问题dongjiemoney的释放时机。如果用户购买的是下一期彩票开奖前这笔钱一直是冻结状态。用户想退款或者修改投注只能等封期前取消订单封期后就不能退款。所以订单表需要增加cancel_deadline字段后端在取消订单接口先判断当前时间是否早于这个截止时间否则返回“已封期不可取消”。源码里没有体现这个字段但实际运营时必须补上否则用户投注后立刻发起退款后端把冻结释放了但票已经按原号码打出去了形成对账差错。4. 开奖信息同步与账户清算双色球/大乐透/七星彩的数据结构设计前端 data 里有一段双色球开奖数据shuangseqiuTime、shuangseqiuRedballs、shuangseqiuBlueballs、shuangseqiuQihao等。相比直接存一个 JSON 字符串拆开存储可以让 WXML 直接wx:for渲染号码球但劣势是更新时容易漏字段。实际开发中我倾向于用一个对象保存全量数据同时用拆分字段做展示。4.1 开奖结果在页面的组装与更新拉取开奖历史的接口通常会返回多期记录前端需要把最新一期展示在首页历史记录放到列表页。下面是双色球最新开奖的组装逻辑getLatestSsq() { wx.request({ url: https://api.example.com/lottery/ssq/latest, success: (res) { const info res.data.data; this.setData({ shuangseqiuTime: info.openTime, shuangseqiuRedballs: info.redBalls.split(,), shuangseqiuBlueballs: [info.blueBall], shuangseqiuQihao: info.issue, shuangseqiuInformation: info }); } }); }这里info.redBalls.split(,)把01,05,12转成[01, 05, 12]。注意保留字符串前导零不要用parseInt转换否则号码球显示时会丢样式。shuangseqiuInformation同时存放原始对象方便跳转到详情页时直接传递省去二次请求。但小程序页面栈传参有大小限制如果info包含多期历史数据建议只传info.issue或使用全局 store。开奖时间的格式也要统一。后端如果返回2017-11-19 21:15:00前端直接显示没问题如果返回时间戳则在 setData 前需要格式化成YY-MM-DD HH:mm。这个格式化建议放在接口层的 JS 工具函数里不要在 WXML 里写复杂的日期截取。源码里shuangseqiuTime是数组我猜是因为同时存了开奖日期和具体时间但数组会让 WXML 的wx:for渲染多一层循环。如果只是一个开奖时刻字符串更合适。4.2 中奖状态与追加投注的奖金区分zhongjiang数组保存用户的中奖记录。中奖计算必须由后端完成前端只是把后端返回结果渲染出来。后端在计算大乐透中奖时要区分is_add字段if ($ticket[is_add] 1) { $prize $basePrize * 1.8; } else { $prize $basePrize; }很多初学者会在bet_order表里只存一个bet_type然后把追加与否当成前端展示字段。这样大乐透开奖时后端拿不到追加标记奖金计算就会少算。正确做法是在订单表增加独立字段is_add并在用户提交下单时从请求参数读取。中奖状态流转也容易出错未开奖的订单是status0开奖后根据中奖号码更新为1未中奖或2已中奖待派发。派发奖金时再更新为3已派发。前端zhongjiang只能显示status 2的记录否则用户会看到大量未派发的历史单误以为系统漏发。我见过一个项目把status1和status2都当成中奖导致用户领奖领了两次后端对账时才发现差额。所以状态机的定义一定要在数据库注释里写清楚前后端共用一份枚举文档。4.3 账户恒等式与合买订单对账账户三字段必须满足totalmoney - dongjiemoney usemoney。我们自己写的对账脚本可以直接查不相等的数据SELECT user_id, (totalmoney - dongjiemoney - usemoney) AS diff FROM member_account HAVING diff 0;HAVING在聚合后过滤AS diff可以在外层条件里引用。查出差异后需要逆向看最近 50 笔资金流水找出是哪个事务没有把dongjiemoney转回usemoney。常见原因有两个一是中奖奖金入账时只加了totalmoney和usemoney没有先释放冻结二是合买订单失败退款时只更新了usemoney没减dongjiemoney。合买对账则更复杂。shuangHemai里的合买详情如果没有唯一订单号退款时容易重复冻结。我习惯在后端加一个frozen_status字段0 表示待冻结1 表示已冻结2 表示已释放。只有frozen_status1的订单才允许退款这样配合数据库唯一索引order_id user_id可以避免并发发起取消合买时重复释放冻结资金。每次对账时直接查frozen_status1但关联订单状态为“已取消”的记录这些就是需要回滚的历史脏数据。5. 抓包调试与上线前检查彩票小程序项目的边界处理彩票类小程序由于不涉及真实交易项目更多是教学演示但调试流程和商业项目一样。这里分享两个最常用的手段抓包和上线检查。5.1 用微信开发者工具 Charles 抓包微信小程序抓包时先打开微信开发者工具在“详情”里勾选“不校验合法域名”然后配置 Charles 的 SSL 代理。手机端扫码预览时需要把手机局域网代理指向电脑 IP 和 8888 端口同时安装 Charles 的 CA 证书。这样能看到小程序发出的所有wx.request请求包括 header 里的 token、请求体里的dangQi和ballsList。抓包后重点核对Login接口返回的 token 是否生效以及下单接口是否把前端传来的旧期号打回。修改刚进入的加载页面也很常见app.json里的entryPagePath或首页onLoad逻辑需要和抓包配合如果首页在onLoad里发起了 3 个并发请求而Login接口还没返回 token这 3 个请求大概率会 401。调试时先看 Network 面板的时间线而不是怀疑后端报错。还有一种情况是开发者工具里请求正常真机上却失败这通常是因为工具勾选了“不校验合法域名”而真机没有需要在小程序后台把域名加入 request 合法域名列表。彩票类项目如果是演示用可以用测试号但正式上线前一定要把所有http://改成https://并且不要用 IP 直连。5.2 上线前检查清单检查项说明期号校验下单接口是否比较服务端当前期号资金恒等式跑一次totalmoney - dongjiemoney - usemoney差值 SQL合买状态合买退款时是否先判断frozen_status1重复提交下单接口是否用订单号做幂等键token 过期wx.request统一处理 401 后跳转登录页清单里最关键的是重复提交。用户连续点击“提交订单”两次后端事务里如果只查了usemoney而没有唯一索引会产生两张相同订单。常见做法是在bet_order表加request_id字段前端每笔订单生成一个 UUID后端插入时用INSERT ... ON DUPLICATE KEY UPDATE实现幂等。对于大乐透追加还要在request_id之外加一个is_add字段的判断避免同一个请求被不同参数重复利用。这个字段在插入前先查一次库如果存在相同request_id就直接返回第一次的订单号不再执行资金扣减这样即使前端因为网络超时重发两次后端也不会重复下单。本文还有配套的精品资源点击获取