
去年年底接手了一个编号 13348 的竞赛报名小程序项目。需求看起来不复杂给高校的创新创业大赛做一套微信小程序报名入口支持赛事浏览、在线填报、资料上传、后台审核审核结果再通过订阅消息通知选手。但真正把需求落地时才发现“报名”这两个字背后全是细节防重复、防错填、状态同步、消息触达、数据导出哪一环没做好都容易被用户和组织者一起吐槽。这篇文章就把整个项目从需求拆解、技术选型、核心模块、踩坑记录到上线运营的完整过程捋一遍给正在做同类微信小程序项目的朋友一份可以直接参考的实战笔记。1. 项目定位与需求拆解1.1 竞赛报名系统到底要解决什么这个系统的本质是把传统竞赛中“发布通知 - 填写报名表 - 汇总表格 - 人工审核 - 通知结果”的整个链路搬到微信里。先不说选手端光看组织者的日常工作就能明白痛点通知发到各个班级群学生用 Excel 填表再回传负责老师一个人收表、催表、打开一个个文件手动对齐格式经常出现漏报、错报、重复报。遇到需要提交作品压缩包的比赛文件命名不规范更是会让人崩溃。小程序刚好能补上这些缺口选手扫码进入微信一键登录按引导填完表单提交后能看到实时状态主办方在后台看到的数据是结构化的还能按组别、学院、项目类型筛选和导出。所以这个项目真正要解决的不是“做一个表单页”而是“把报名入口做成一个前后端闭环的业务系统”让选手觉得好用让管理员觉得省心。1.2 为什么选微信小程序而不是网页或 App选型的时候其实也纠结过做一个响应式 H5 网页不香吗不用审核改完就能上线。但这个项目的传播场景几乎全在微信群和朋友圈微信小程序扫码即用登录、支付、订阅消息、图片上传这些能力天然闭环用户不用重新注册账号体验接近原生应用。App 就更不用说了下载安装这一步就能劝退一大半参赛者。还有一点需要提前说清楚小程序不是网页交付的源码工程不能在浏览器里直接打开。之前有同事把一个小程序 zip 包当网页项目部署到服务器发现打不开其实就是没搞清楚运行环境。小程序必须用微信开发者工具导入工程调试后上传为体验版审核通过才能线上使用。理解和接受这一点后续开发才不会走弯路。1.3 用户角色与核心业务规则这个系统里主要有三类角色参赛选手、赛事审核人员、超级管理员。选手关心的是入口好找、表单好填、提交后有反馈审核人员关心的是信息完整、重复可控、操作顺手超级管理员则要看得见数据、导得出名单、配得了赛事。业务闭环可以这样梳理管理员创建赛事并配置表单字段 - 小程序首页展示竞赛列表 - 选手登录后浏览详情 - 选择组别填写报名信息 - 上传附件 - 提交报名 - 后台审核通过或拒绝 - 通过订阅消息通知选手 - 选手在“我的报名”里查看状态或参赛凭证。报名状态我设计成这几个待提交、待审核、已通过、已拒绝、已截止、已退赛。这里有一个很关键的规则提交之后到审核完成之前允许用户有一次修改机会一旦审核通过报名数据锁死。为了防止同一个用户对同一赛事反复提交后端必须用user_id event_id做唯一索引这个是在数据层面兜底不能只靠前端按钮禁用。2. 技术选型与整体架构2.1 小程序端原生还是 uni-app技术选型被问得最多。先说结论这个项目我用的原生微信小程序没有上 uni-app。原因不外乎三点项目逻辑不复杂不需要多端复用原生调试流程短不会遇到框架层转译带来的奇怪问题包体积和页面性能更容易控制。热词里有一个很典型的报错uniapp 微信小程序打包 source size 2612kb exceed max limit 2mb很多用 uni-app 的朋友都栽在这上面。核心原因是依赖和组件库被重复打包页面又没有做分包主包直接超限。原生开发虽然写起来“原始”但每一行代码都是自己的包体积心里有数。组件库部分我引入了 Vant Weapp 的按需加载比如按钮、输入框、弹出层、Toast 这些常用组件。但报名表单的核心部分没有用现成的表单组件而是基于配置动态渲染自己封装原因后面会说。状态管理方面项目规模不大只用页面的data加全局App.globalData就够了没必要引入 Redux/MobX 增加概念负担。2.2 后端服务与数据存储后端这里要先分两种场景。如果团队没有自己的服务器、又不想搞域名备案最省事的是直接用微信云开发云函数 云数据库 云存储一套搞定登录、文件、数据库都不用自己运维个人项目和企业原型都能很快速地跑起来。但云开发也有硬伤冷启动延迟不稳定、复杂聚合查询能力弱、以后想迁出去非常痛苦。我这个项目选择的是 Node.js Express MySQL 自建后端因为主办方要求报名数据能按学校、学院、组别做多维统计还要导出 Excel 给教务处存档后续甚至有接进老管理系统的可能。服务器用了 2核4G域名备案后配好 HTTPS接口统一走 RESTful 风格返回结构固定为{ code, data, message }前后端联调的时候体验很舒服。数据表设计上没有搞得很复杂核心就这几张用户表、赛事表、报名表、审核记录表、系统配置表。报名表是最重要的除了用户 ID、赛事 ID、表单数据 JSON还留了状态字段和审核备注字段。表单数据存成 JSON 是刻意的因为不同赛事的字段不一样关系型建模反而会把简单问题复杂化。2.3 微信生态能力与关键 API做微信小程序绕不开微信开放能力这里面的规则一定要提前搞清楚否则开发到一半会被卡得很惨。登录选wx.login拿到临时 code然后把 code 发给后端由后端用appid AppSecret去微信接口换openid和session_key。这里切记AppSecret 绝对不要出现在小程序代码里任何人反编译小程序包都可能把它翻出来。业务后端拿到 openid 后查库没有就创建用户再签一个自己的登录 token 返回给前端后续请求都带这个 token 就行。手机号快速验证组件很香但它要求企业主体个人开发者基本用不了。订阅消息这里也要注意一次性订阅模板对用户每次授权只能发一条所以不能一进页面就弹授权框。我的做法是在报名提交成功页放一个“订阅审核结果通知”按钮用户主动点了再调wx.requestSubscribeMessage这样授权率高也符合微信的审核规范。微信支付就更直接必须要有商户号个人主体不支持。如果比赛免费别碰支付如果需要收费得提前把资质准备齐全。3. 核心模块设计与实现3.1 登录鉴权与用户体系登录的实现其实很短但流程细节不能少。核心代码大致是这样// 小程序端 wx.login({ success: async (res) { const loginRes await request.post(/auth/login, { code: res.code }); wx.setStorageSync(token, loginRes.data.token); wx.setStorageSync(userInfo, loginRes.data.userInfo); } });后端拿到 code 之后用 AppSecret 去微信接口换 openid查数据库有则更新最近登录时间无则新建用户。返回的 token 我用的 JWT过期时间设成 7 天小程序冷启动时如果 token 还在就直接当作已登录状态。头像昵称这一块别再死磕wx.getUserProfile了现在微信推荐用原生能力button open-typechooseAvatar选择头像input typenickname填写昵称这样既合规又不容易被审核打回。token 过期处理也值得说。我在请求拦截器里统一判断 401一旦遇到就重新执行wx.login换取新 token并把失败那次的请求重新放行。这里面要加一个isRefreshing标志和请求队列否则多个接口同时 401就会触发三五次并发登录容易把后端登录接口打崩。3.2 竞赛列表与详情页列表页第一版做得很简单结果就是用户快速滑动时onReachBottom连续触发接口被疯狂请求。后来统一封装了一个loadList方法参数是page、pageSize、filters请求前判断if (this.data.loading || !this.data.hasMore) return请求回来后重置 loading更新列表和hasMore。下拉刷新则把page重置为 1再拉一次全量数据。详情页是比较容易被小看的页面。竞赛开始时间、报名截止时间、可报人数这些字段一定要以服务端返回的数据为准不要用前端new Date()去判断有的用户把手机系统时间一改报名状态就全乱了。我在后端下发赛事详情时直接带上status字段not_started、in_progress、ended、full前端只用展示对应状态。赛事介绍里的富文本用rich-text组件渲染图片地址如果来自第三方平台很容易因为防盗链失效最好在导入赛事时把图片转存到自己的对象存储里。3.3 报名流程与动态表单竞赛报名系统最大的特点是不同比赛的表单长得完全不一样。有的赛项要选组别有的要填指导老师有的要上传作品压缩包还有的要填身份证号。如果每个赛事都写死一套表单那后台得维护成百上千个页面。我的方案是管理员在后台配置表单 Schema描述字段名、类型、是否必填、正则校验规则、最大长度、可选项列表等小程序端拿到这份 JSON 后动态渲染表单。动态表单的组件类型主要包含input、textarea、radio-group、checkbox-group、picker、date-picker、upload。这里有两个细节要注意一是上传附件先通过wx.uploadFile把文件传到自己服务器或云存储表单提交时只传 URL千万别传 base64 字符串不然一条报名记录就能把数据库撑爆二是单选框的 value 类型原生radio-group里拿到的都是字符串提交给后端前要按字段定义转换成数字或布尔不然服务端一校验就报错。提交按钮那里我加了 loading 状态和 disabled 控制防止用户连点产生多条记录。同时后端还做了两层防护第一层业务校验看报名时间窗口是否还开第二层依靠数据库唯一索引user_id event_id兜底。用户提交完成后跳转到“我的报名”页状态是待审核并且在那个页面放订阅消息授权按钮。3.4 后台管理端与审核管理后台我做成 Web 端没有塞进小程序里。因为报名数据的筛选、批量操作、导出在手机上操作效率太低谁用谁知道。Web 端功能包含赛事管理、表单 Schema 配置、报名列表、审核队列、名单导出、简单统计报表。审核队列我给了“待审核”和“已处理”两个视图。审核员点击进入详情左边是选手填写的完整报名信息右边是“通过”和“拒绝”两个按钮。拒绝时必须要填原因这个原因会拼进后续的订阅消息里发给选手时能减少大量“为什么被拒”的咨询。名单导出用后端生成 Excel前端一个下载链接搞定。如果报名人数到了几千导出必须改成异步任务先把文件生成到存储里再通知用户下载否则请求很容易超时。统计报表也不用上重型 BI直接 SQL 按学校、组别、报名时间分组统计前端用柱状图展示就行。4. 项目实测踩坑与性能优化4.1 包体积超限与分包策略微信小程序主包有 2MB 上限这是所有小程序开发者都要面对的第一道坎。我的项目刚开始做的时候本地图片、第三方组件、图表库全都塞在代码包里一度接近 1.8M虽然还没超限但加载速度已经明显不对劲了。优化主要做了四件事第一本地图片全部压缩能转 WebP 就转 WebP单张图片控制在 50KB 以内第二Vant Weapp 改成按需引入不再整库引入组件代码体积直接减半第三图表不用第三方库用 Canvas 手写简单柱状图和折线图够用就好第四如果页面数量多一定要配置分包主包只保留 tabBar 页面和公共模块报名流程、管理相关页面放进subpackage。如果你用 uni-app看到source size exceed max limit 2mb的报错基本就是主包超了还是得靠分包和 CDN 化解决。4.2 列表加载与分页的坑分页加载更多是一个非常经典的需求但坑也不少。第一个坑是onReachBottom在快速滑动时可能连续触发多次解决办法是加isLoading锁。第二个坑是筛选条件变化后忘记重置分页导致新条件的数据从第二页开始取页面出现空白。我的统一规则是任何条件变更都必须先page 1并清空列表再调用加载方法。async loadList(reset false) { if (this.data.loading) return; if (!reset !this.data.hasMore) return; const page reset ? 1 : this.data.page 1; this.setData({ loading: true }); try { const res await request.get(/api/events, { page, pageSize: 10, keyword: this.data.keyword }); this.setData({ list: reset ? res.data.list : this.data.list.concat(res.data.list), page, hasMore: this.data.list.length res.data.total }); } finally { this.setData({ loading: false }); } }另外后端接口要强制加limit不要因为管理端需要全量导出就把所有列表接口都做成返回全量数据。小程序渲染几千条数据页面会直接卡死列表加载更多就老老实实分页。4.3 顶部导航栏与安全区适配微信小程序顶部有胶囊按钮和网页完全不一样自定义导航栏时高度绝对不能写死。取高度的标准姿势是这样const systemInfo wx.getSystemInfoSync(); const menuButton wx.getMenuButtonBoundingClientRect(); const statusBarHeight systemInfo.statusBarHeight; const navBarHeight (menuButton.top - statusBarHeight) * 2 menuButton.height;导航栏总高度就是statusBarHeight navBarHeight内容区域再单独计算。如果不这样做会出现安卓和 iOS 上标题一个高一个低的情况。底部安全区也一样要给提交按钮加padding-bottom: env(safe-area-inset-bottom)不然 iPhone 底部的小横条会盖住按钮文字。热词里提到的“移动搜索框聚焦后会偏移”多半是自定义导航栏中input用了position: fixed键盘弹起后计算位置出错。解决办法是让搜索框位于页面滚动区域内或者监听onKeyboardHeightChange动态调整。4.4 网络请求与错误处理网络请求层如果不做统一封装项目联调阶段会非常痛苦。我封装的request函数做了这些事统一拼接baseURL设置 15 秒超时请求头自动带上 token返回非 0 状态码时自动 Toast 后端的message401时自动重新登录并重放请求。loading 使用全局计数器多个请求并行时只开启一次避免按钮频繁闪烁。错误码也提前和后端约定好这里是我们用的一套错误码含义前端处理0成功正常返回400参数校验失败显示后端 message401未登录或 token 过期重新登录后重放请求403无权限访问跳转提示页404资源不存在提示数据不存在500系统异常提示稍后重试10002报名已截止返回列表页刷新状态调试阶段可以用 Charles 抓包查看小程序请求PC 端和手机端都能抓主要用来排查登录、报名提交、支付回调问题。有一点要特别注意正式环境 HTTPS 证书必须有效如果证书链不完整iOS 端请求会直接失败而安卓有时会放行这个问题非常隐蔽。5. 测试、发布与运营5.1 开发者工具与真机调试拿到小程序源码工程后第一件事不是看代码而是启动项目。用微信开发者工具“导入项目”填上你的 AppID没有 AppID 可以先选测试号但测试号很多能力都受限支付、订阅消息、手机号这些基本都调不了。启动后先在模拟器里把主流程点一遍但有些功能模拟器测不了比如录音 API 生成的文件格式、定位、蓝牙、加速度计这些必须真机验证。要把小程序发给其他人试用最常规的做法是上传“体验版”。在开发者工具右上角点“上传”填好版本号和备注然后到微信公众平台后台的“成员管理”里添加体验成员。体验成员扫码后就能在“体验版”入口看到这个小程序。建议收集几天的试用反馈重点观察用户会不会卡在某个表单字段审核结果通知能不能正常收到有没有人反馈页面加载慢这些反馈比你自己点一百遍都值钱。5.2 审核与发布注意事项上线前要过微信审核这里有几个高频坑。小程序注册本身免费但如果要用微信支付、手机号组件、订阅消息长期模板就得完成企业认证认证费用按周期收大概 300 元一个周期个人主体玩不了这些能力。提审之前务必在小程序后台填写《用户隐私保护指引》把要收集的信息如手机号、姓名、头像、地理位置等列清楚审核被拒很多时候就是因为没有隐私弹窗和隐私政策。类目也要提前选对竞赛报名一般对“教育 - 教育信息服务”或“工具 - 信息查询”类目和实际功能不符会被打回。提审时间尽量避开周五晚上小程序审核通常 1-2 天遇到节假日排队会更久。发布的时候可以选“全量发布”如果团队对自己代码没底也可以先“分阶段发布”部分用户试运行。发布后不是终点后端接口要做好日志和告警一旦报名高峰流量把服务打挂那比赛报名就得延期这个责任谁都担不起。5.3 运营与数据反馈功能上线后竞赛报名系统的运营重点就变成了转化率。我当时在小程序后台和业务后端两个维度都埋了点访问用户数、报名页跳出率、各渠道来源、每日报名人数、组别分布、审核平均耗时。发现报名页跳出率高就赶紧看表单是不是太长、有没有字段含义不清楚。后来把一个“备注”字段改成选填并加了一行说明文字完成率立刻涨了一截。订阅消息的授权率也要专门盯着。我一开始把订阅授权弹窗放在首页自动弹出授权率只有 20% 多。改成报名提交成功后的主动按钮授权率升到 60% 左右。这个改动不大但直接影响后续通知到达率选手能不能及时收到审核结果都看它。后续扩展的方向其实很多增加参赛证 PDF 下载、成绩查询、证书发放、比赛日程提醒甚至可以在一个平台里承载多个赛事做成一个校园赛事服务的小生态。但无论怎么扩展核心原则不变报名流程要短、状态反馈要实时、数据要准确可追查。这次做完 13348 竞赛报名项目我个人最大的体会是竞赛报名小程序的技术门槛真的不高真正决定成败的是状态管理、消息触达和防重复这些“看不见的细节”。开发结束不是项目结束让真实用户拿体验版跑几天比自己在模拟器里点一百遍都管用。最后再分享一个小技巧在报名表单提交按钮上强制加一个二次确认弹窗把用户选择的组别、姓名和参赛项目展示出来能拦下不少选错组的报名。这种小设计文档里学不到但用户真的会在意。