最近把一个微信小程序竞赛报名系统做完了交付,前后折腾了小半个月,踩了不少坑。这个项目本身不算复杂,但涉及的业务流程比想象中多:比赛创建、报名填报、后台审核、人数统计、消息通知,一环扣一环。如果你也在做类似的项目,或者正准备接这类私活,这篇应该能帮你省不少事,尤其是那些平时文档里不会写清楚的边界问题和微信平台的隐性限制。
简单介绍下这个系统能干什么。它解决的核心痛点是:线下竞赛报名时填表混乱、统计费劲、通知靠吼。用微信小程序做报名入口,参赛者扫码即用,不用下载App,不用注册新账号,微信授权一下就能报名。管理员在后台创建比赛、设置报名起止时间和人数上限,报名数据自动进库,审核通过或驳回后,用户能在小程序里看到结果。整个流程从发布到组队、报名、审核、通知,全链路都串起来了。
适合谁参考?一是拿这类题目做毕业设计的学生,二是在公司或学校内部需要快速落地报名场景的开发者,三是想接外包单子的独立开发者。接下来我把这个系统的设计思路、数据库结构、小程序端实现、后台审核闭环,以及部署上线后碰到的典型问题,全部拆开讲。
1. 项目背景与需求拆解
1.1 为什么选微信小程序而不是网页或H5
这是开工前第一个要想清楚的问题。竞赛报名过去有两种常见做法:一种是Excel表发群里,大家自己填,最后统计时乱七八糟;另一种是做个网页报名系统,但使用门槛卡在推广上,用户填个报名信息还要先打开浏览器、输入网址、注册账号,很多人填到一半就放弃了。
微信小程序在这条赛道上优势非常明显。首先是分发路径短,用户扫一个码,或者群里点小程序卡片,直接就打开了,不需要经历“下载App、安装、注册”这种高成本动作。其次是身份识别成本低,微信小程序的wx.login可以直接换取用户的唯一标识 openid,不需要用户单独注册一个账号,对参赛者来说“零门槛”,对开发者来说也省掉了一整套账号体系的开发量。第三是通知能力,审核结果和赛前提醒可以通过订阅消息触达,虽然现在订阅消息被限制成“一次性订阅”,但在报名场景里恰好够用。
当然,选小程序也有代价。比如代码包有2MB大小限制,比如关键接口必须配HTTPS合法域名,再比如发布上线前要经过微信侧的审核。这些约束会在后文展开,选型时心里有数就好,别等到开发一半才来抱怨平台限制。
1.2 角色与核心业务流程梳理
竞赛报名系统里有三类角色,对应三类需求:
| 角色 | 核心诉求 | 关键操作 |
|---|---|---|
| 参赛者 | 快速找到比赛、顺利报上名、随时看审核状态 | 浏览赛事列表、填写报名信息、查看我的报名、取消报名 |
| 管理员 | 创建比赛、控制报名节奏、统计名单、通知结果 | 发布赛事、审核报名、导出名单、发送状态通知 |
| 开发者 | 保障系统稳定、数据准确、合规上线 | 维护数据表、排查并发与审核问题、跟进小程序审核 |
业务流的核心可以拆成一条主线:管理员创建比赛并设置时间与人数限制,发布后用户在小程序端看到比赛,点击报名并填写信息,数据进入待审核状态,管理员在后台逐条审核,审核结果回写数据库,用户在下一次打开小程序时看到最新状态,同时收到一条订阅消息提醒。
状态机的设计是这个项目里最容易忽略但最重要的事。比赛本身有状态:草稿、报名中、报名截止、已结束;报名记录也有状态:待审核、已通过、已驳回、已取消。两个状态机都要在数据库里存字段,前端页面根据状态决定按钮文案和可点击性。线下讨论需求时常常只谈“能报名就行”,但实际上很多隐藏问题都出在状态流转上:比如比赛截止后用户还能不能取消报名?审核通过后还能不能修改报名信息?这些都需要在设计之初就定清楚。我给这个项目定的规则是:报名截止后不能提交也不能取消,审核通过后信息锁定。
1.3 需要预先确认的业务细节
开工前一定要跟需求方确认清楚几个问题,否则后面返工非常痛苦。第一,报名是个人报名还是团队报名?如果是团队赛,表单里要加队伍名称和成员列表,逻辑复杂度直接上一个台阶。第二,是否需要限制人数上限?超过上限后是禁止报名还是进入候补队列?第三,是否需要收费?如果比赛要收报名费,就涉及微信支付商户号接入,个人主体小程序无法开通,需要企业资质,这个坑一旦踩进去格外烧时间。第四,是否需要管理员审核?有些比赛报名即通过,那审核模块就可以砍掉,简化整个流程。
我这次做的系统包含团队报名和后台审核,因为需求方明确说比赛有队伍人数限制,还要人工确认参赛资格。如果一开始没把这些问清楚,等到数据库建完、页面写完才发现要做团队字段,改动量相当大——涉及用户表、报名表、前端表单、后台审核列表、导出名单五个地方。
2. 数据库模型与接口设计
2.1 核心数据表设计
数据库我用的是云开发数据库,因为部署省心,不必单独买服务器,开箱即用。但表结构的思路是通用的,换MySQL、PostgreSQL也一样。
整个项目一共三张核心表:用户表、比赛表、报名表。
用户表的设计很简单,但有一个关键点要特别注意:不要把微信昵称头像当成用户表主键,用户真正的唯一标识是 openid,它由微信分配,与用户微信号绑定,业务系统里所有与用户相关的查询都应该用 openid 关联。昵称和头像只是展示字段,随时可能被用户更换或清空,不能承担业务关联的职责。
比赛表的核心字段包括:
| 字段 | 类型 | 说明 |
|---|---|---|
| competition_id | string | 比赛唯一ID,前端路由参数 |
| title | string | 比赛名称 |
| description | string | 比赛介绍,支持换行 |
| cover | string | 封面图地址 |
| team_limit | number | 每队人数上限,个人赛填1 |
| max_teams | number | 报名队伍总数上限 |
| signup_start | timestamp | 报名开始时间 |
| signup_end | timestamp | 报名截止时间 |
| status | string | draft / published / ended |
| category | string | 比赛分类,如编程、数学、体育 |
时间字段统一存时间戳,而不是字符串。这一点主要是为了排序和比较方便,前端拿到时间戳后自己格式化展示。如果存字符串“2024-05-01 09:00:00”,在数据库里做大于小于比较时格式一旦不统一很容易出错。
报名表是整个系统的核心,它的字段设计决定了后续审核和统计是否顺利:
| 字段 | 类型 | 说明 |
|---|---|---|
| registration_id | string | 报名记录唯一ID |
| competition_id | string | 关联比赛 |
| openid | string | 报名用户 |
| team_name | string | 队伍名称,个人赛填个人名 |
| leader_name | string | 队长姓名 |
| leader_phone | string | 队长手机号 |
| members | json | 团队成员列表,字段为姓名和微信号 |
| status | string | pending / approved / rejected / canceled |
| created_at | timestamp | 提交时间 |
| reviewed_at | timestamp | 审核时间 |
| remark | string | 管理员驳回原因 |
members 存成 JSON 数组,是个人赛和团队赛统一的关键选择。如果不存JSON,就得额外建一张team_member表,报名时做事务性写入,审核时再做联表查询,复杂度明显上升。对于报名系统这个体量,JSON字段完全够用,导出名单时直接解析即可。
2.2 接口清单与权限控制
接口设计遵循“小程序端只做展示和提交,核心判断放服务端”的原则。主要接口如下:
| 接口 | 方法 | 说明 | 权限 |
|---|---|---|---|
| /login | POST | wx.login 换 openid | 所有用户 |
| /competitions | GET | 比赛列表,支持状态筛选与分页 | 所有用户 |
| /competitions/:id | GET | 比赛详情 | 所有用户 |
| /registrations | POST | 提交报名 | 登录用户 |
| /registrations/mine | GET | 我的报名记录 | 登录用户 |
| /registrations/:id/cancel | POST | 取消报名 | 报名本人 |
| /admin/competitions | POST | 创建或更新比赛 | 管理员 |
| /admin/registrations | GET | 按比赛查报名列表 | 管理员 |
| /admin/registrations/:id/review | POST | 审核通过或驳回 | 管理员 |
登录接口是整个系统的基础。小程序端调用wx.login()拿到临时 code,把 code 发给服务端,服务端调用微信的 code2Session 接口换取 openid 和 session_key,再把 openid 返回给前端存起来。这里有一个安全细节:千万不要信任前端传上来的 openid,正确的姿势是前端只传 code,openid 由服务端解析后从接口响应里下发。如果直接把 openid 放前端请求体里传过来,等于任何人都能伪装成别人。
报名提交接口要重点防两件事:重复报名和超员。重复报名靠数据库的唯一约束实现,同一用户同一比赛只允许一条非取消状态记录,云开发里可以用where查询配合add的原子性来兜底。超员判断则必须在服务端做,用户点击报名时先查当前已通过加待审核的记录数,再和max_teams比较。不要小看这一步,线下同时有十几个人在报名时,如果不加控制,很容易出现“最后一个人也报名成功,但实际已超员”的竞态问题。
3. 小程序端开发实操
3.1 项目结构与应用配置
小程序端的目录结构我按业务模块划分,避免全部页面堆在一起:
miniprogram/ ├── app.js ├── app.json ├── app.wxss ├── pages/ │ ├── index/ // 赛事列表首页 │ ├── detail/ // 比赛详情页 │ ├── signup/ // 报名表单页 │ ├── mine/ // 我的报名 │ └── admin/ // 管理端入口(管理员可见) ├── components/ │ ├── empty-state/ // 空状态组件 │ └── status-tag/ // 状态标签组件 ├── utils/ │ └── request.js // 网络请求封装 └── api/ └── index.js // 接口定义app.json 里最值得强调的是 tabBar 配置。首页和“我的报名”作为两个 tab 比较顺手,用户进来先看赛事列表,报名后去第二个 tab 查状态。部分竞赛项目会把“我的报名”藏进个人中心,但我实测下来,两个 tab 对用户更友好,因为报名后马上要查状态是高频动作,入口越显眼越好。
窗口样式上,微信小程序顶部导航栏高度在不同机型上不一致,尤其刘海屏和普通屏差距明显。处理这个问题的通用做法是获取系统信息里的statusBarHeight和menuButtonRect,动态计算自定义导航栏的高度。但如果你不想自己写导航栏,直接用默认导航栏就行,别为了炫技引入自定义导航组件,平白加一堆兼容代码。
3.2 网络请求封装与统一错误处理
这是小程序端被很多人忽略但极其重要的一层。我写了一个基于wx.request的 Promise 封装:
// utils/request.js function request(url, method, data, header = {}) { return new Promise((resolve, reject) => { wx.request({ url: getApp().globalData.baseUrl + url, method: method, data: data, header: { 'content-type': 'application/json', 'Authorization': getApp().globalData.token || '', ...header }, success(res) { if (res.statusCode === 200 && res.data.code === 0) { resolve(res.data.data); } else if (res.statusCode === 401) { // 登录态失效,重新登录后重试 wx.showToast({ title: '登录已过期,请重新进入', icon: 'none' }); reject(res.data); } else { wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常,请稍后重试', icon: 'none' }); reject(err); } }); }); }这里统一了错误提示,业务代码里就不用每个页面都写一遍wx.showToast。401 的判断很关键,微信小程序的登录态会过期,用户在比赛列表页停留很久再点报名,很可能 token 已经失效,此时直接提示让用户重新进入页面,比报一个莫名其妙的失败更友好。
接口地址统一放在api/index.js里管理,页面代码只调用语义化的函数,比如getCompetitionList(page)、submitRegistration(data)。这样后期换接口地址或者加统一拦截逻辑,只改一处。
3.3 赛事列表页与分页加载
赛事列表页是所有流量入口,也是第一个要处理体验细节的地方。我使用的是onShow里刷新数据,因为用户报名成功后返回列表页,列表数据要反映最新状态,用onLoad只在页面首次加载时触发,数据会陈旧。
分页加载这块,微信小程序原生没有提供滚动加载的组件,靠的是页面触底事件onReachBottom。实现起来就是维护一个page变量,每次触底page++再请求,接口返回的数据判断是否还有下一页,没有就显示“没有更多了”。这里有个细节:onReachBottom的触发距离可以在页面 json 里配置onReachBottomDistance,默认值是50px,我实测列表页建议设成100px,用户手指滑到底部附近就预先加载,体验更顺滑。
列表页还有一个绕不开的问题:下拉刷新。小程序里启用页面下拉刷新很简单,在页面 json 里把enablePullDownRefresh设为 true,再监听onPullDownRefresh。下拉刷新时重置分页参数并重新请求,数据回来后wx.stopPullDownRefresh()关闭刷新动画。这个操作看似基础,忘了stopPullDownRefresh的话,刷新动画会一直转圈,测试时很尴尬。
赛事卡片的展示要根据状态做差异化。报名中的比赛显示截止时间和剩余名额,用高亮色突出“报名中”;未开始的显示“即将开始”;已截止的不显示报名按钮,只留下“查看详情”。状态判断在前端做一次,在后端再校验一次,用户在截止时间前一秒点报名,前端校验通过了,提交到后端仍可能被拒,这是正常的。
3.4 报名表单页与提交逻辑
报名表单页是用户和数据打交道最直接的地方,也是工作量最大的页面。字段不一定全,但每个字段都要有校验。我自己总结的校验原则是“前端校验保体验,后端校验保数据”。
姓名和队长的信息用输入框收集,手机号要做正则校验。这里有个实打实的坑:微信小程序的input组件在部分安卓机型上会出现输入光标错位的问题,尤其是页面内容较长需要滚动时。解决办法是给input设置adjust-position相关属性,或在bindfocus时手动滚动页面到输入框可见区域。不要觉得这是小概率事件,我在真机上碰到过好几次。
提交按钮的防重复处理很关键。用户弱网环境下点击报名,请求还在路上,又点一次,就会产生两条报名记录。前端用变量锁定:点击后立即把按钮状态设为 loading 并 disabled,请求结束才恢复。同时后端也要做唯一约束,双保险。代码大致长这样:
async handleSubmit() { if (this.data.submitting) return; this.setData({ submitting: true }); try { const data = await api.submitRegistration({ competitionId: this.data.competitionId, leaderName: this.data.form.leaderName, leaderPhone: this.data.form.leaderPhone, teamName: this.data.form.teamName, members: this.data.form.members }); wx.showToast({ title: '报名成功,等待审核', icon: 'success' }); wx.redirectTo({ url: '/pages/detail/detail?id=' + this.data.competitionId }); } finally { this.setData({ submitting: false }); } }wx.redirectTo和wx.navigateTo的差别是:redirectTo会关闭当前报名页,防止用户返回后再点一次“报名”造成重复提交。这个细节是我踩过坑后才补的,一开始用navigateTo,结果用户报名成功后能返回到表单页,重新提交一遍,后台出现两条记录。
3.5 我的报名与订阅消息
“我的报名”页承担的是用户查询状态的核心需求。页面默认展示当前用户所有报名记录,每条记录显示比赛名称、报名时间、当前状态。状态标签很重要:待审核用黄色,已通过用绿色,已驳回用红色,已取消用灰色,用户一眼就能看明白。
取消报名功能要加上限制:如果比赛已经到截止时间,取消按钮要隐藏;审核通过后也不能取消,因为管理员可能已经统计了名单。这些规则后端也验证,前端只是提前拦截。
订阅消息的接入是这个项目里做起来最繁琐的部分。微信要求先在小程序后台申请订阅消息模板,审核通过后得到一个模板ID,然后前端在合适的时机调用wx.requestSubscribeMessage让用户授权。我的做法是:用户在提交报名成功的回执弹窗里,顺带请求订阅消息授权,文案写清楚“审核结果将通过微信通知您”,这样转化率比事后突然弹授权高很多。审核通过后服务端调用微信的 subscribeMessage 接口推送结果。
订阅消息有“一次性订阅”的限制,用户授权一次只能接收一条消息。报名审核只有一次结果通知,恰好匹配。但如果还要做赛前提醒,就需要在合适时机再引导用户订阅一次,这个在产品层面就要规划好,别想着一条授权用到天荒地老。
4. 管理端与报名审核闭环
4.1 管理后台的功能规划
管理端我做了两个入口:一个是在小程序里给管理员角色看到的管理页面,另一个是独立的Web后台。小程序里的管理页面适合管理员在手机上快速审核,Web后台则适合批量操作和统计导出。两个入口共用同一套数据库和接口,不增加额外复杂度。
管理功能核心是三大块。第一块是赛事管理,管理员可以创建新比赛、编辑已有比赛、手动关闭报名通道。创建比赛时要能配置报名开始时间、截止时间、人数上限、队伍人数限制、比赛介绍和封面图。第二块是报名审核,按比赛维度筛选报名记录,每条记录显示报名人信息、队伍信息、提交时间,管理员操作通过或驳回,驳回时填理由。第三块是数据统计,在赛事实时展示报名总数、待审核数、通过数、驳回数,帮助管理员判断报名进度。
Web后台的名单导出功能用的是服务端生成Excel。前端点击导出按钮,服务端查库生成xlsx并返回下载链接。如果图省事直接在小程序里做导出,小程序端无法直接下载二进制文件到本地,体验很差,这个功能放在Web后台是最合理的。
4.2 审核并发与人数上限控制
审核操作有一个特别容易翻车的地方:管理员审核的速度和报名人数上限之间的关系。假设比赛限报20队,当前待审核记录有5条,管理员一口气把这5条全部点通过,但实际系统里已经有18条通过记录了,再点通过就会超员。
解决办法是在审核接口里做剩余名额校验,伪代码如下:
async reviewRegistration({ registrationId, action, remark }) { const reg = await db.collection('registrations').doc(registrationId).get(); if (action === 'approve') { const competition = await db.collection('competitions').doc(reg.competitionId).get(); const approvedCount = await db.collection('registrations') .where({ competitionId: reg.competitionId, status: 'approved' }) .count(); if (approvedCount.total >= competition.maxTeams) { // 返回“名额已满,无法通过” } } // 更新状态 }这个校验必须放在服务端,因为管理员可能同时打开多个浏览器标签页批量操作,前端拦截不靠谱。批量审核时也要注意,前端虽然会把多条记录一起提交,但后端每一条都要独立做名额校验,确保“先到先得、按审核时间排序”。
这里我实际踩过的一个坑是:明确了“待审核记录不占名额,审核通过时才占名额”的规则后,排队的报名用户可能等很久才知道结果。所以我在报名详情页里加了一行提示“报名后请耐心等待审核,人数达到上限后未审核的报名可能无法通过”,提前管理用户预期,后续客诉少了很多。
4.3 通知触达的几种方案对比
审核通过或驳回后,怎么触达用户,我对比过三种方案。
第一种是小程序订阅消息,这是首选。用户在小程序内完成过授权,且消息模板提前在小程序后台申请好了,服务端调订阅消息接口推送,用户能收到微信服务通知。优势是成本低、流程闭环,劣势是一次性订阅限制,用户如果不授权就发不出去。
第二种是公众号模板消息。如果团队本身有服务号,且用户关注了公众号,可以通过公众号模板消息推送审核结果。但这里有个问题:用户的 openid 在小程序和服务号体系下是不一致的,需要用户在报名时同时提供微信号或引导关注公众号,然后做openid关联,操作成本高。这个方案我没有采用,只做了记录,以后如果用户量大再考虑。
第三种是短信通知。这需要买短信服务,且用户要填手机号,成本高一些,覆盖面广,但对报名系统来说性价比低。我的建议是:订阅消息作为主通道,同时在小程序“我的报名”页提供一个负一屏的刷新入口,哪怕用户没授权订阅消息,只要打开小程序就能看到状态,不至于漏掉通知。
5. 部署、测试与常见问题排查
5.1 从代码到可体验的版本
代码写完后,距离真正能发给别人用还差几步配置。小程序上传到微信后台,生成体验版二维码,在“成员管理”里把测试人员的微信号加进体验成员,对方扫码就能进入体验版。这里有个常见误区:开发者工具里的模拟器能跑,不代表真机一定没问题,所以体验版一定要发给多个不同机型的用户试,收集几天反馈再迭代,比直接提审上线稳妥得多。
正式上线前,服务器域名必须配置HTTPS,并且要在微信公众平台的“开发管理-服务器域名”里把接口域名加进request合法域名。不加这个配置,模拟器里能跑,真机一请求就报request:fail url not in domain list。调试阶段可以在开发者工具的“详情-本地设置”里勾选“不校验合法域名”,但当心,这个设置是“本地设置”,每台开发机都要勾一次,而且换工具版本后可能要重新勾选,很容易在真机预览时忘记。
开发版、体验版、正式版三者的区别要清楚:开发版只能在开发者工具里打开,体验版可以由体验成员扫码打开,且无需经过微信审核,只有正式版才需要提交代码审核。如果项目着急验证,直接上传体验版给团队内部试用就行,不必先过审核关卡。
5.2 常见问题与排查速查表
整理一下我在这个项目里以及同类型项目中出现过的常见问题,按表格形式给出,方便你对照排查。
| 问题现象 | 原因 | 解决方案 |
|---|---|---|
真机请求报url not in domain list | 服务器域名未配置到小程序后台白名单 | 将接口域名加入 request 合法域名,开发工具取消域名校验仅限调试用 |
| 上传代码提示包体积超2MB | 主包文件过大,图片或组件冗余 | 启用分包加载,把报名详情页、证件照页面等放入分包;压缩图片 |
用wx.getUserProfile拿不到头像昵称 | 微信已经收回常驻授权能力 | 引导用户自行填写头像昵称,或使用“头像昵称填写能力”官方方案 |
| 订阅消息授权后仍然收不到通知 | 授权一次性用完,或模板ID配置错误 | 检查模板ID是否与申请到的模板匹配,检查用户是否重复授权 |
| 报名成功后提交两条记录 | 前端重复点击或未用 redirectTo 关闭页面 | 按钮加 loading 锁定状态,提交成功关闭报名页;后端加唯一约束 |
| 模拟器正常但真机白屏 | 基础库版本过低或使用了不兼容API | 在 app.json 中配置libraries,或升级基础库版本后重测 |
| 部分安卓机型输入框光标错位 | input 组件与页面滚动冲突 | 监听 focus 事件,手动pageScrollTo滚到输入框位置 |
| 列表页下拉刷新后动画一直转 | 未调用wx.stopPullDownRefresh() | 接口结束后必须关闭刷新动画 |
| 比赛报名截止后还能提交 | 仅前端隐藏了按钮,后端没校验 | 报名提交接口必须校验当前时间是否在signup_start和signup_end之间 |
| 云开发数据库提示权限不足 | 数据库默认权限是“仅创建者可写”,报名页读取比赛列表被拒绝 | 配置数据库权限为“所有用户可读,创建者可写”,敏感集合单独处理 |
新手最容易被绊住的就是域名校验和包体积超限。包体积这个坑比较可惜,因为多数时候不是代码多,而是图片多。封面图一定不要直接放本地静态资源,建议传到云存储或CDN,代码包只保留必要的几张图标。我这边的比赛封面图全部走云存储链接,主包控制在1.2MB左右,稳稳过关。
5.3 报错日志与线上问题定位
小程序上线后最痛苦的事情是线上报错你不知道用户那边发生了什么。开发者工具的控制台和真机调试只能覆盖你自己设备上的情况,真实用户的各种机型、各种操作路径会带来意想不到的问题。我给项目加了一个简单的上报逻辑:在wx.onError和app.js的onError里捕获异常,调用了一个日志上报接口,记录用户openid、页面路径、错误堆栈和微信版本号。
日志表结构也不复杂,就是报错信息加时间戳加用户身份。有了这些日志,用户在群里反馈“我这边打不开”,你先查后台日志看一眼有没有对应记录,再让用户配合提供微信版本,很多时候几分钟就能定位。这个操作不费什么力气,但对线上项目的维护体验提升巨大。有些私活项目根本不接入监控,上线后一有问题只能远程帮用户看,效率极低,这一点我强烈建议加上。
日志系统还能反哺功能优化。比如我发现日志里有大量“点击报名后接口504”的记录,顺着追查是报名接口查询人数上限时,数据库在高峰期响应变慢。后来给报名相关的查询字段加了索引,情况立刻好转。这种判断如果没有日志支撑,只能靠猜。
6. 测试要点与项目交付心得
6.1 真机测试清单
交付前我列了一份真机测试清单,每项都要在至少一台安卓、一台iPhone上验证。这种交叉验证很重要,因为wx.showToast的图标、订阅消息授权弹窗样式、input 聚焦交互在两端表现都有差异。
测试清单包括:赛事列表分页加载是否顺畅;下拉刷新后数据是否正确;报名表单字段校验是否生效;弱网环境下重复点击提交是否会产生重复记录;订阅消息授权后能否收到推送;“我的报名”页状态刷新是否正确;管理员审核通过后用户端状态是否同步;比赛截止后报名按钮是否禁用;取消报名后剩余名额是否回滚。
其中弱网测试最容易被忽略。微信开发者工具里可以模拟网络状态,把网络改成弱网或断网,看页面会不会报错、按钮会不会卡死。我在弱网测试时发现了一个问题:报名请求发出后网络中断,按钮已经锁定了,用户不知道发生了什么。后来在提交逻辑里加了一个超时提示,网络异常时弹“网络异常,请稍后重试”,同时解锁按钮让用户可以再试一次。这类细节很影响用户对系统的信任感。
6.2 数据安全与隐私合规
作为接私活或者做毕设项目,数据安全同样要过一遍脑子,尤其是报名表里的手机号、姓名这类个人信息。我的处理原则是:能少收集就少收集,非必填字段一律不收集。手机号是联系参赛者需要,必须收集,但其它无关字段如身份证号、住址、紧急联系人,一律不放进表单。
另外,数据库的开权限要小心。使用云开发时,默认权限如果设置成“所有用户可读”,那比赛信息暴露没问题,但报名记录如果开放了读权限,用户通过云开发控制台就能看到别人的报名信息,这是严重事故。正确做法是:比赛集合开放所有用户可读;报名集合只允许本人读,管理员通过管理端接口访问,不直接开放客户端读库权限。审核操作全部走后端接口,数据库的写权限能关就关。
管理员的口令或登录机制也别做得太简单,至少要用一个管理员专用登录页,密码不要明文存在任何地方。如果项目预算允许,接入一个简单的验证码登录也行。我这次用的是专属口令加验证码双重校验,口令可定期更换,避免长期有效泄露。
6.3 从竞赛报名系统到通用活动报名系统的扩展思路
项目收尾后回头看,这套系统的可扩展性其实很强,换一个壳就可以支撑多种场景。例如把比赛表改成活动表,字段从比赛名称替换成活动名称,报名表保留队伍信息和审核状态,就是一个通用的活动报名系统。校园招聘会、社团纳新、讲座预约、运动会项目填报,都是同样的业务模型。
进一步扩展的话,一个方向是接入微信支付,做付费报名。需要企业主体的小程序并开通微信支付商户号,开发量主要在支付回调处理和退款流程上。另一个方向是做证书或电子票的发放,审核通过后系统生成一个带二维码的参赛凭证,参赛者到场时扫码签到,这就能把报名系统和线下签到串联起来。还有一个方向是多轮筛选,第一轮报名通过后进入下一轮面试或测试,报名表增加阶段字段和备注字段即可。
从我个人的体会来说,竞赛报名这类业务“看起来简单,做起来细节多”,真正花时间的不是在页面上,而是在状态流转、并发控制、权限管理和线上故障排查里。每次交付完一个这样的项目,我都会把测试清单和踩坑记录整理成自己的文档库,下次再做类似系统时能省下大量时间。如果你正在做类似的项目,建议也多留一份这样的记录,哪怕只是Markdown文件,几个月后回头看都会觉得值。