基于UniApp的微信小程序考务系统开发实战
去年帮某高校信息中心做了一款考务管理小程序从需求对接、方案选型到上线维护前前后后折腾了三个多月。教务科一开始提的需求很简单——“能查个考试安排就行”结果等角色和流程梳理完才发现排考、监考核对、成绩审核、缓考申请、通知推送每一块都要跟不同角色协作。学生要看安排、老师要收成绩单、学院教务员要核对考场、校级管理员要排考和发布最后前端这边选了UniApp编译到微信小程序端后端配了一个轻量级接口服务总算把考务这件事从一堆Excel表搬进了手机。这篇内容适合已经在做或准备做校园类小程序的开发者也适合接了类似外包的学生团队看看。我会把方案选型、数据建模、核心功能实现以及我在真机调试和审核过程中踩过的坑都拆开讲一遍尽量做到能照着复现。1. 项目定位与整体方案选型1.1 为什么要把考务系统做成微信小程序高校考务场景有个很明显的矛盾信息量不小但使用频率非常集中。学生一年也就查几次考试安排老师一年也就录几次成绩、核对几次监考任务如果专门给所有人装一个App激活成本和维护成本都收不住。微信小程序的好处是干掉安装这一步学生点开就用消息提醒还能通过订阅消息触达教务科甚至可以把小程序码直接贴在考场门口考前扫一下就能看到该考场的考试信息。还有一个现实原因高校里学生和老师的手机品牌非常杂微信几乎是所有人的交集。虽然也有学校在做自己的App但大多数时候学生并不愿意为一个“查考场”的功能多装一个应用。小程序在这个场景下是最低门槛的客户端形态这是选它的核心理由。当然小程序也不是没有短板。离线能力差、包体积受限、审核流程要提前安排这些在开发前就要留出余量。后面我会专门写一节审核和合规的注意点。1.2 UniApp相比原生小程序的优势与代价开发前我们在原生微信小程序和UniApp之间认真权衡过。原生小程序的性能上限确实更高对微信特性的支持也最及时但有一个致命问题写一套代码就只能跑在微信里后面如果学校提一句“能不能出个App版”或者“安卓微信里打开有点卡想上H5”那就等于重新写一遍。UniApp的核心价值恰恰在这里Vue语法写一套可以编译到微信小程序、支付宝小程序、H5和原生App相当于给后面的扩展留了一条后路。代价也是真实的。UniApp对微信某些深层能力的封装不够彻底遇到需要调用原生插件或者特殊接口的场景还是得靠条件编译写一段原生小程序代码。另外UniApp的编译产物有时候会比手写原生大一些页面复杂了要注意分包。表格对比一下我当时的选择依据维度原生微信小程序UniApp传统H5移动端多端复用仅微信微信支付宝H5App浏览器即可开发效率中等高Vue组件化高但缺微信接口能力微信能力调用最直接大部分支持偶尔需条件编译受限包体积控制可精细控制需注意分包不涉及推广门槛扫码即用扫码即用需要域名、打开成本高当时我们团队的判断很简单这个系统未来大概率要扩展到H5比如让教务老师在电脑上也能对着大屏核对考场还要保留出App的可能性所以UniApp是综合成本最低的选择。事实证明这个决定是对的后来学校想要一个网页版看大屏统计我们在原有代码上直接编译了一个H5包出来几乎零成本交付。1.3 总体技术架构与关键选型系统整体分三层。客户端用UniAppVue 3 Pinia编译目标是微信小程序为主H5为辅。后端我们当时选的是Node.js Express原因不是它最好而是团队最熟换任何你熟悉的语言都一样核心是接口设计要稳定。数据库用的MySQL考务数据本身关系性强适合用关系型数据库管理。服务端不搞微服务那一套一个单体应用就够用。用户体系分学生、教师、学院教务员、校级管理员四种角色登录走微信的uni.login换取openid后端签发JWT客户端把token存起来后续带上。数据表围绕“考试”“考场”“成绩”“通知”四条主线设计后面单独讲。这套架构放到中小型团队里非常实用前端一个人、后端一个人基本就能撑起整个开发接口联调也没有跨团队扯皮的成本。如果学校那边不想维护服务器也可以把后端直接搬上微信云开发数据库和云函数都省了不过要提前确认学校对数据存储位置有没有要求。2. 考务核心需求拆解与数据模型设计2.1 角色权限与业务边界划分考务系统最怕的不是功能多而是权限边界模糊。权限设计错了会出现学生误点进成绩录入页面或者老师改了不该改的考试安排这种事故一出来口碑直接崩。我们最终把业务拆成了四条权限线学生查考试安排、查考场座位、提交缓考/免考申请、接收考试提醒、查看已发布成绩。任课/监考教师查我带的/监考的考试、录入成绩暂存或提交、确认监考签到、查看学生名单和考场信息。学院教务员管理本院课程的考试安排审核本院学生申请通知下发到本院。校级管理员统筹全校考试资源教室、教师、时间段发布成绩与通知管理基础数据。权限在前端只是隐藏菜单真正的校验必须落在后端接口。每个接口都校验角色和资源归属比如老师只能提交“考试安排中指定为该老师”的成绩记录不能通过改ID的方式操作其他课程的成绩。2.2 核心数据表结构设计数据建模是整个项目里最花时间的环节因为考务有一层天然的关系网课程对应考试考试对应考场安排考场安排又关联监考老师和学生成绩又挂在考试和学生的交叉点上。表结构不提前设计清楚后面每个功能都会写得很别扭。最终核心表大概是这样落的users用户表包含openid、角色、姓名、学号/工号、所属院系、头像、状态等字段。openid是微信登录的关联键角色字段用整数枚举方便权限判断。exams考试表保存考试基础信息比如课程id、考试类型正常/补考/缓考、考试日期、开始时间、结束时间、状态草稿/已发布/已结束、创建人。这表是整个考务的“头条”所有功能都围着它转。exam_rooms考场安排表一个考试可以对应多个考场。字段包括所属考试id、教室名称、容量、具体楼栋、楼层、起止时间、监考教师ID列表JSON存简单直接。exam_students考试学生表记录哪个学生在哪个考场的哪个座位号考试还有状态正常/缺考/缓考。这张表是整个系统数据量最大的表之一索引要提前建好。grades成绩表学生id、考试id、分数、状态暂存/提交/已发布、录入人、修改记录JSON。重点是把“提交”和“发布”分开提交之后老师自己也不能无痕修改。notifications通知公告表标题、正文、接收对象范围、发送方式、发布时间用目标角色和目标院系两个字段控制推送范围。设计阶段最容易被忽略的是“状态机”。我建议考务系统里所有核心表都带一个状态字段因为考务流程是强状态流转的考试从草稿到发布成绩从暂存到提交再到发布每一步都要有记录后面排查问题全靠它。2.3 排考冲突检测的基本实现思路排考是考务系统里最“算法”的一个点。教务科每年排考最痛苦的就是一个个核对“这个老师这个时间在不在监考另一场考试”“这个教室这个时间段有没有被占用”。系统里排考页面要提供一个自动检测能力核心逻辑其实不复杂就是区间重叠判断// 判断两个考试时间段是否冲突 // 时间段A: startA ~ endA时间段B: startB ~ endB // 只要两个区间有重叠就说明冲突 function hasConflict(startA, endA, startB, endB) { // 比如 A: 08:00~10:00B: 09:30~11:30 // startA(08:00) endB(11:30) 且 startB(09:30) endA(10:00)返回true代表冲突 return startA endB startB endA; }教室冲突检测也一样把两场考试安排到同一个教室且时间重叠就是不合法。监考教师冲突则要遍历exam_rooms表里的监考教师JSON数组逐个做时间区间对比。这些检测如果单纯靠后端写SQL也能做但会把SQL搞得很复杂实际做法是把相关记录加载到内存里跑一遍数据量几千条以内性能完全没压力。排考算法可以再给一个提示先按考试时间分区再在同一个日期里做资源检测能大大减少无效计算。不要在循环里套循环对比全部考场记录提前把“今天要考的考试”和“今天会被占用的教室/教师”筛出来量级小很多。3. 核心功能模块的实操开发3.1 微信登录与身份鉴权流程登录是整个系统的入口不能想当然。UniApp里通过uni.login拿到临时code然后传给后端。后端拿这个code去微信接口换openid和session_key拿到openid后先查users表用户不存在就走自动注册流程存在就直接签发JWT返回前端。这里有几个细节值得注意。第一uni.login拿到的code有效期只有5分钟而且只能用一次后端接口要做好异常处理。第二小程序的appid必须和后端配置的一致否则code换openid那一步会直接报错我在联调阶段被这个问题卡过将近半天。第三JWT要设置合理的过期时间用户长期不使用后要求重新登录避免token被盗用的风险面扩大。前端拿到token后我会存在uni.setStorageSync里并在请求工具封装一个统一的拦截器每次request自动带上Authorization头。还有一个细节微信新版本对用户隐私比较敏感如果涉及读取用户头像昵称这种能力要在小程序管理后台配置隐私协议否则调用时会直接失败这个放到合规那节详细说。3.2 考试安排列表与详情卡片考试列表是小程序端最重要的首页模块。学生登录进去后第一眼看到的就是“我最近的考试”。这个页面我用的是分组列表按日期分组日期下面展示考试卡片卡片上放课程名、时间段、考场、座位号、考试状态一目了然。接口设计上列表接口要支持分页和状态筛选。分页我用的是page和pageSize参数返回时带上总数前端做上拉加载。缓存策略也很关键考试安排变动频率不高但学生在考试周会反复打开小程序如果每次都走网络会拖慢体验。我在页面onLoad时会先读一次本地缓存展示旧数据然后后台请求新数据回来再刷新界面这个“缓存优先、网络更新”的策略实测体验提升非常明显。列表页还要考虑一个场景一个学生如果缓考申请通过考试卡片上要显示“缓考”标签。这个状态是从exam_students表的status字段里拿的前端不要自己去推断状态点必须以后端接口为准。3.3 监考签到与考场二维码监考这块老师最痛的点是核对到考率。传统做法是考前点名几个考场来回跑效率很低。我们的方案是给每个考场生成一个专属小程序码老师到考场后扫码直接进入监考签到页面确认考场信息后点签到签到记录落库。在UniApp里获取小程序码不能直接在前端调用后端需要请求微信接口getwxacode传入scene参数考场ID生成二维码图片返回给前端展示或存储。这里有一个坑scene参数最长32个可见字符且不能包含一些特殊符号直接用数字ID最稳妥。如果要在二维码图片上显示考场信息需要通过页面链接的page参数指定一个专用于签到的页面路径。前端扫码后UniApp可以用uni.scanCode拿到result里的字符串再把考场ID提取出来请求签到接口。这个流程看起来简单但涉及的联调点很多我在第五章会列一个扫码场景的常见问题清单。监考签到的位置锁定是可选的。我们当时做了一个“签到范围限制”的功能获取老师的经纬度判断是否在考场周围500米内超出范围就不允许签到。实施下来发现GPS在室内经常漂移尤其教学楼里定位误差能到一两百米后来把判定半径放宽到了800米并且给教务员留了一个“特殊签到”开关应急时候用。3.4 成绩录入、审核与发布成绩模块容易做成“急性子”。所谓的急性子就是录入成绩后立刻让学生看到结果录错了又要紧急联系后端改数据。正确做法是成绩有三态暂存、提交、发布。老师在录入页面可以先暂存保存的是草稿状态提交后成绩进入“提交”状态此时老师自己不能再改了需要联系学院教务员解锁或者走“改分申请”流程。校级管理员或者教务员发布后学生端才可见。这个看似麻烦的设计上线后帮教务科挡掉了好几次“误操作事故”。成绩录入页面的交互也要设计好。考试往往几百个学生如果让老师一个个输入分数再单独保存体验极差。我写了一个批量录入模式教师切换页面或者点击“全部暂存”之前所有分数在本地内存里暂存只有点了“提交”才会逐条写库。这样老师可以对着纸质成绩单一口气抄完再统一提交。字段设计上grades表里一定要有一个modified_log记录每次改分的操作人、时间和原因。我们当时虽然没有被审计到但学校的教务科非常在意这个因为成绩改动的合规性牵涉到考试公平性。3.5 订阅消息与考前提醒考试提醒用到的是微信订阅消息。这里有一个很多人第一次做都会踩的坑微信订阅消息是“一次性订阅”用户订阅一次你只能给他推一条。所以“订阅后以后都能收到通知”这种想法在小程序里根本实现不了。实际可行的策略是这样。在考试列表页、考试详情页放一个“订阅提醒”按钮用户点击后触发uni.requestSubscribeMessage把需要的模板ID都传进去。用户点了允许订阅后端就把这条订阅关系记录到一张表里等到提醒节点比如考前24小时、考前1小时后端通过订阅消息接口推送。如果要多次提醒就要分多次请求用户订阅或者让用户在多个页面分别订阅。这里再补一个经验订阅消息请求最好绑定在一个明确的用户动作上不要在刚进首页就弹出来微信对体验要求很高而且用户也更容易拒绝。放一个“考试提醒”按钮让用户主动点订阅的通过率会高很多。模板消息的推送时间也要注意不要让后端在夜里两三点给用户发消息。4. 开发过程中的关键难点与运行优化4.1 跨端兼容的几个实操坑UniApp号称一套代码多端跑但真用起来跨端兼容是个必须正面处理的课题。第一个坑是单位。小程序原生推荐用rpxH5和App端也支持但在某些自定义组件里rpx换算会出偏差尤其是涉及canvas或者position定位时。我的原则是常规布局用rpx遇到复杂或精细的定位计算改用px并配合系统信息里的屏幕宽度做一次换算。第二个坑是原生组件层级问题。textarea、canvas、map这些组件在微信小程序的旧架构里层级最高会盖住正常页面元素。解决方法有两个一是升级到微信新架构skyline或使用cover-view二是尽量避免在弹窗里嵌入textarea改为placeholder提示加一个全屏编辑页面。我们当时做成绩录入时就是被textarea弹窗盖住按钮坑过一次最后改成独立编辑页问题彻底消失。第三个坑是条件编译。UniApp提供了一套#ifdef语法可以按平台写差异化代码这个能力非常关键尤其当你在微信端用到原生能力时// #ifdef MP-WEIXIN console.log(这段代码只会在微信小程序端执行) // #endif // #ifndef MP-WEIXIN console.log(这段代码会在非微信小程序平台执行) // #endif平时写页面尽量用uni内置组件比如uni.showToast、uni.navigateTo这些API在多个端的行为是统一的。一旦用了某个端的原生API或者第三方原生插件就必须用条件编译包一层并写清楚在哪几个端生效否则后面编译到App时会在毫无预兆的情况下报错。4.2 微信小程序审核与合规注意事项小程序审核是很多校园项目团队准备不足的地方。审核不是代码跑通了就能过还要求你的类目、资质、隐私协议都对得上。考务类小程序归类到“教育-在线教育”还是“教育-教育信息服务”审核要求不一样。我当时选择了“教育信息服务”类目需要提供相关资质材料。这里建议做之前先去小程序管理后台看需要的资质问清楚学校信息中心能提供哪些材料不要等开发完了才发现类目不符。域名方面必须使用HTTPS且需要在后台配置request合法域名。开发阶段可以勾选“不校验合法域名”但上线前必须配置好否则正式版接口全部请求失败。证书不能用自签证书必须要正规CA签发的。如果学校服务器做不到建议走云服务省心很多。隐私合规也要重视。现在微信对用户隐私抓得很紧如果你在代码里读取用户头像、位置等敏感信息但没在小程序后台填写对应的隐私保护指引首次调用时会直接弹错误。我们的做法是最小化收集头像给默认头像需要真人头像才通过选择头像能力获取位置信息只在监考签到时获取并且使用一次性授权。4.3 包体积控制与首屏性能优化微信小程序主包限制是2MBUniApp编译出来很容易超。我们第一次打完包就超了打开小程序开发工具一看主包1.9MB再加点图片素材直接崩。分包是必须做的。我的分包策略是tabBar页面和公共页面放主包考试详情、签到、成绩录入、申请这些业务页面全部放分包。小程序分包加载不会影响首屏但要注意页面跳转时要写对分包路径。还有一个容易忽略的点图片资源不要直接放工程里都传OSS或云存储代码里引用URL。这样包体积瞬间能降下来一大截。首屏优化另外一个惯用手段是列表懒加载。考试列表、通知列表这种长列表用分页加载每次拉20条配合一个“加载中”状态和“已经到底”状态。数据不大不要一次性返回几百条移动端渲染长列表非常吃力。如果后端一次返回的数据确实很大也尽量做字段裁剪去掉不必要的大字段比如正文文本先返回摘要详情再请求全文。4.4 数据安全与越权控制实战考务系统里的成绩和考生名单属于敏感数据权限控制必须严。我的经验是所有涉及数据的接口都在后端做权限校验前端隐藏菜单只是用户体验优化不是安全边界。具体做法有这么几个。第一后端写一个统一的权限中间件解析JWT后注入当前用户角色和ID每个业务接口再用一个“可访问性”函数校验资源归属。比如成绩提交接口先校验当前用户是不是这门考试指定的教师不是就返回403。第二所有列表查询接口都强制带userId参数后端基于这个参数做数据过滤不接受客户端传别人的ID来查数据。第三对成绩和考试这类敏感数据的查询在日志里记录完整的操作路径有问题时可以回放。前端也要有配合措施。token过期后统一跳回登录页公共请求方法里面对401状态码做集中处理。用户切换身份比如老师兼任校级管理员时前端刷新权限菜单但真正能不能操作还是看后端接口返回。5. 常见问题与排查技巧实录5.1 开发期高频问题速查表开发过程中我们记录了不少问题这里整理了一个高频排查表格基本都是验证过的经验现象可能原因处理建议真机请求接口报错未配置合法域名或未开“不校验域名”在公众平台配置request域名确认开启校验开关登录拿不到openidappid与后端配置不一致核对分别在两端配置的appid是否相同code换session失效code一次性且5分钟过期重新调用uni.login获取新code扫码签到拿不到参数用bindtap事件内result解析错误先打印resultscene参数需要解码再解析订阅消息发送失败用户未订阅或模板ID已失效确认用户列表里有订阅记录重新申请模板ID包体积超限图片、组件未分包图片转OSS页面按业务模块分包页面白屏卡顿接口无缓存且数据量大缓存优先策略接口返回字段裁剪考试成绩显示为旧值缓存未清理发布成绩后清除该学生的考试列表缓存5.2 典型问题真机调试接口SSL证书与域名问题这是一个绕不开的大坑。本地开发用http://localhost调接口没问题但手机扫码预览时请求的是局域网IP地址微信对这类请求默认拦截。如果你不勾选“不校验合法域名”真机上request会直接fail报错信息往往只有“request:fail”几个字特别容易让人一头雾水。排查思路要按顺序来先在开发工具里确认“不校验合法域名”是否开启没开启就先勾上再拿真机测试如果线上还是报错就去小程序后台看合法域名配置检查域名解析和HTTPS证书。我遇到过最隐蔽的一种情况是域名证书链不完整PC浏览器访问没问题但微信客户端对证书链检查更严格直接拒绝。解决办法是给证书配全中间证书链不要只传一个leaf证书。5.3 典型问题订阅消息用户收不到提醒学生反馈“明明订阅了考试前没收到提醒”这种情况一般是三个原因。第一订阅是一次性的用户在A页面订阅过后端在使用时可能已经过期需要重新订阅。第二模板消息的模板ID和关键词字段不匹配检查模板里是否包含了“考试时间”“考试科目”这些字段发送的消息内容要和模板字段完全对应。第三后端的定时任务执行时用户小程序不在线也会导致发送失败率上升。我们的解决办法是做一个“订阅状态”页学生可以在里面看到自己订阅了哪些提醒过期了可以一键重新订阅。后端推送的时候记录每次发送的结果如果有失败在管理端给出失败原因。这个设计上线后学生的提醒到达率提高了非常多教务科也很满意。6. 项目实施过程中的几点经验总结这套系统让我最受益的不是某个具体功能怎么写而是对“考务这种流程型业务”的开发节奏有了更清晰的认知。流程型业务最怕需求不明确就开写比如“排考”两个字看起来简单实际里面藏着时间冲突、资源占用、教师安排、学生名单导入、考场分配等一系列规则。建议拿到这类需求后第一步先画一张角色-流程-状态图给业务方确认让他们在流程图上签字确认再进入开发能省掉后面无数返工。另一个经验是校园类项目虽然以功能正确为主但性能和体验的影响比想象中大。学生用的手机千奇百怪低端安卓机跑小程序尤其吃力代码里一个不经意的setData大对象就可能让页面罕见掉到个位数帧率。写列表页时我习惯性用“分页本地缓存字段裁剪”三件套这套组合拳放到任何低端机上都能保住基本体验。最后提醒一点这类系统上线前一定要留出至少两周的“试运行”时间让教务科真实排一场考试、让几个老师真实录一次成绩。很多问题只能在真实数据、真实压力下暴露出来。试运行期间我发现的最大Bug是考场二维码在弱网下加载失败后来加了本地缓存方案才解决。这种Bug在联调环境里根本不可能提前发现只能靠真实场景去逼出来。如果后面再让我做一版同类系统我会考虑把排考算法独立成一个服务提供给教务管理后台和教师端共同调用而不是把逻辑散落在各个接口里。也会在数据层给所有变更记录加一个统一的审计表任何角色对成绩、考试安排的增删改都自动落审计日志。这些设计在一开始看起来是“过度设计”但在一个要长期维护、被多个部门反复使用的系统里早做远比晚做省钱。