微信小程序交通规则系统,核心目标是让驾校学员或准备科目一、科目四的用户,在小程序里完成交通法规学习、模拟考试、错题回顾和成绩统计。很多开发者第一次做这类系统时,容易把页面设计、动画效果和扩展功能想得太多,反而忽略了最关键的刷题链路。我建议把精力放在三件事上:题库怎么组织、考试流程怎么闭环、用户数据怎么记录。如果你正在做基于微信小程序的驾校模拟考试、交通规则学习平台,或者刚拿到一个毕设、外包项目,这篇文章的路线会比较实用:从小程序端、Spring Boot 后端、MySQL 数据表,到订阅消息、微信支付、上线审核和常见问题排查,都会按落地顺序拆开讲。最值得关注的不是某个单独功能,而是整套系统在普通配置下能不能稳定跑起来。
1. 做这类交规小程序,先想清楚核心模块和用户主流程
交规系统最忌讳一上来就堆功能。用户打开小程序,不是为了看花哨首页,而是为了刷题、模考、看错题。先把主链路理清,后面所有页面和接口都好设计。
1.1 核心用户是谁,他们最重视什么
核心用户是驾校学员和准备科目一、科目四的考生。这类用户有两个典型使用场景:一是碎片时间刷题,比如地铁上、排队时;二是考试前做整套模拟,检验自己能不能在限定时间内及格。所以他们最关心的不是页面好看不好看,而是题目加载快不快、交卷稳不稳、错题能不能方便回顾。驾校管理员或教练也需要一个管理后台,用来维护题库、查看学员通过率。这里要注意:管理后台不一定非要放在小程序里,用 Web 管理端会更合适。小程序端页面栈和适配成本有限,不要把复杂表单、图表统计全部塞进去。
1.2 角色和权限怎么划分
第一版可以只做两级角色:普通用户和管理员。普通用户对应小程序里的学员,可以练习、模拟考试、查看错题和成绩。管理员在后台维护题库、导入题目、查看考试统计。用户表里加一个 role 字段,后端在管理接口上做简单拦截就可以。细粒度的权限、菜单权限、角色权限,等业务真正需要时再加。不要一上来把 Spring Security 的复杂权限模型设计进去,交规系统早期最重的数据是题目,不是用户权限。
1.3 功能优先级:先把考试闭环跑通
第一版建议只做这几个模块:
- 题库列表:按分类或随机显示题目。
- 练习模式:逐题作答,实时判断对错。
- 模拟考试:有限时间、固定题量、交卷评分。
- 错题本:自动收集答错的题目,支持继续练习。
- 成绩记录:查看最近考试分数、正确率、用时。
第二版再考虑收藏、每日一题、学习进度、排行榜、订阅消息、微信支付、蓝牙打印。为什么这个顺序很重要?因为交规系统最核心的价值是帮助用户通过考试,考试链路不稳定,其他功能做得再多也是空转。我见过有的项目先做了一堆动画和抽奖页面,结果交卷时请求参数对不上,成绩一直存不进去,这就是功能优先级没理清。
1.4 页面主流程和页面跳转关系
页面不用设计得太深,建议保持这样的关系:
pages/index/index 首页 -> pages/exam/exam 练习或考试页 -> pages/result/result 成绩页 -> pages/wrong/wrong 错题页首页可以通过模式参数区分练习和考试:进入 exam 页面时传入 mode=practice 或 mode=exam。考试交卷后跳转 result 页,result 页显示成绩和错题入口。错题页可以单独从首页进入,考试结果页也可以带 examId 进入。小程序页面栈层级太深会出现返回逻辑混乱,所以页面层级尽量控制在三层以内。
2. 技术选型:小程序 + Spring Boot + MySQL 为什么比较稳
选型不用追新,关键是团队能维护、部署简单、跑得稳。微信小程序作为前端展示和交互层,Spring Boot 负责接口,MySQL 存题库和用户数据,是目前这类系统比较常见的组合。
2.1 为什么不能只用本地缓存
有些同学觉得几千道题可以直接放到小程序本地缓存里,不需要后端。这个想法在很小规模时可行,但交通规则题目通常包含文字、图片、解析,还会定期更新。如果只放本地,题库更新需要发版,用户考试记录、错题同步、多设备登录都无法处理。小程序本地缓存更适合存用户最近一次答题状态、登录 token 这类轻量数据,不适合做题库主存储。所以服务端接口是必须的。
2.2 前端选择:原生小程序还是 uni-app
做微信小程序交通规则系统,优先推荐原生微信小程序。原因很简单:官方文档和工具链最直接,真机调试、上传发布、组件兼容都少一层中间转换。如果你团队熟悉 Vue,并且确定以后要同时发布支付宝小程序、抖音小程序,那用 uni-app 也可以。但要注意,uni-app 编译后部分原生组件表现不同,比如 video、canvas、地图组件,都需要额外测试。如果只是做一个微信小程序,没必要为了跨端而跨端。
2.3 Spring Boot 后端结构
后端不需要复杂微服务,一个单体 Spring Boot 应用足够。建议按职责分包:
com.example.traffic ├── controller // 接收请求,返回统一结果 ├── service // 业务逻辑 ├── mapper // 数据库访问 ├── entity // 数据库实体 ├── common // 统一返回、异常处理、工具类 └── config // 配置类,如微信配置、拦截器不要把所有代码都写在 controller 里,也不要过度分层。简单项目 controller 调用 service,service 调用 mapper,基本上够用。如果后面题库量变大,再把缓存、消息队列加进来。
2.4 环境准备清单
不用一次性准备太多,先满足最小开发环境即可。下面是我常用的起步配置:
| 项目 | 建议 |
|---|---|
| 微信开发者工具 | 下载稳定版,申请小程序 AppID |
| 基础库版本 | 以微信开发者工具当前稳定版为准,设置一个最低兼容版本 |
| JDK | JDK 8 或 17,取决于 Spring Boot 版本 |
| Maven | 3.6 以上 |
| 后端框架 | Spring Boot 2.7 或 3.x,根据 JDK 选择 |
| 数据库 | MySQL 5.7 或 8.0 |
| 服务器 | 学习阶段本机即可;上线建议 2 核 4G 起步 |
| 域名 | 需要 HTTPS 域名,且域名备案,用于小程序 request 合法域名 |
这里的版本没有绝对标准,以你当前下载到的稳定版本为准。如果后端是 Spring Boot 3.x,JDK 必须是 17 以上;如果还用 JDK 8,就选 Spring Boot 2.7。这个问题在项目初始化时最容易踩坑。
2.5 最小化依赖
后端一开始不需要引入太多依赖。Spring Boot Web、MyBatis-Plus 或 Spring Data JPA、MySQL Connector、Lombok 就够了。Redis、RabbitMQ、定时任务这些都先不放,等真的需要再引入。依赖越多,环境兼容问题和部署问题越多。
3. 最小可运行 Demo:先让一条题目从后端跑到小程序里
有没有一个最小闭环?有:后端提供一条交通规则题目的接口,小程序拉取后展示,用户点击某个选项后给出对错反馈。只要这个闭环跑通,后面所有功能都是在这个基础上扩展。
3.1 创建后端项目并配置端口
用 Spring Initializr 创建一个项目,引入 web、mysql 相关依赖。在 application.yml 里做基础配置:
server: port: 8080 servlet: context-path: /api spring: datasource: url: jdbc:mysql://localhost:3306/traffic_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Drivercontext-path 用 /api 可以让接口路径更清晰,后续小程序请求统一带 /api 前缀。端口不要和小程序开发者工具其他服务冲突,8080 比较常见。
3.2 建一张最简单的题目表
第一版不用把选项拆到多张表,先用单表把题目、选项、答案、解析放在一起:
CREATE TABLE traffic_question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, type TINYINT NOT NULL COMMENT '1-判断 2-单选 3-多选', title VARCHAR(500) NOT NULL, option_a VARCHAR(200), option_b VARCHAR(200), option_c VARCHAR(200), option_d VARCHAR(200), answer VARCHAR(20) NOT NULL, analysis VARCHAR(1000), category_id BIGINT, status TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;插入两条样例数据,一条判断题,一条单选题。排序字段不需要一开始就做,先按 id 排序。如果后面要支持章节、分类、按难度出题,再加字段和索引。
3.3 编写查询接口
用 MyBatis-Plus 写一个最简单的随机题目接口:
@RestController @RequestMapping("/question") public class QuestionController { @Autowired private QuestionService questionService; @GetMapping("/random") public Result<List<Question>> random(@RequestParam(defaultValue = "10") Integer limit) { return Result.success(questionService.getRandomQuestions(limit)); } }这里要注意一个问题:练习模式虽然需要即时判题,但考试模式不能把正确答案下发到小程序端,否则用户通过接口直接看到 answer 字段,考试就没意义了。建议返回给前端时把 answer、analysis 字段过滤掉,或使用单独 VO 对象。判题放在服务端完成。
3.4 小程序端请求题目
在页面 onLoad 里发起 wx.request:
wx.request({ url: 'https://your-domain.com/api/question/random', data: { limit: 10 }, success: (res) => { if (res.data.code === 0) { this.setData({ questions: res.data.data }); } else { console.log('接口返回错误', res.data); } }, fail: (err) => { console.log('请求失败', err); } });开发阶段如果还没有正式域名,可以在微信开发者工具右上角“详情 -> 本地设置”里勾选“不校验合法域名”。但这只能用于本地开发,真机预览和正式上线必须配置合法域名。
3.5 怎么判断 Demo 跑通
判断标准很简单:
- 小程序冷启动后能看到题目列表。
- 点击选项,页面能根据服务端返回或本地逻辑判断对错。
- 后端控制台能看到请求日志。
- 修改数据库题目内容后,小程序重新进入能拿到新数据。
如果题目加载不出来,不要急着加倒计时、答题卡、交卷逻辑。先看网络面板里的请求状态,再看后端日志有没有堆栈。很多问题不是代码逻辑问题,而是数据库没连上、接口路径写错、返回字段名对不上。
4. 交通规则系统的数据模型:题目、考试记录和错题表怎么设计
数据模型决定了后续功能好不好扩展。交通规则系统的核心数据不是用户,而是题目和用户答题行为。把这部分设计清楚,后续功能会顺利很多。
4.1 题库表:单表冗余字段还是选项子表
题库表有两种方案。第一种是把选项 A/B/C/D 直接放在 traffic_question 表里,适合判断、单选、固定四项多选题。优点是简单,查询快,写代码直观。缺点是如果某道题有五个选项,或者选项带图片,扩展起来麻烦。
第二种是把选项拆成 traffic_option 子表,每个选项一行,关联 question_id。适合选项数量不固定、选项有图片、需要单独维护选项顺序的场景。缺点是查询时需要多表关联或多次查询,开发量稍大。
我建议第一版先用单表冗余字段。等遇到选项需要加图片、数量不固定时,再考虑拆表。很多毕设和内部项目用单表就够。不要为了“标准化”过早增加复杂度。
4.2 用户学习行为表
除了题目表,至少还需要几张行为表:
| 表名 | 用途 | 关键字段 |
|---|---|---|
| traffic_user | 用户信息 | openid, unionid, nickname, avatar, role |
| traffic_answer_record | 每次答题明细 | user_id, question_id, user_answer, is_correct, source_type |
| traffic_exam_record | 模拟考试主记录 | user_id, score, total_count, correct_count, duration, status |
| traffic_wrong_book | 错题本 | user_id, question_id, wrong_count, last_wrong_time, is_mastered |
| traffic_favorite | 收藏表 | user_id, question_id, created_time |
其中 traffic_answer_record 可以记录用户每次答题结果,source_type 用来区分是练习还是考试。traffic_exam_record 保存每次模拟考试的整体情况。traffic_wrong_book 不是把所有错误记录都存一次,而是保存一个“错题状态”,同一个用户同一道题可以累计错题次数,这样错题本里不会出现重复题目。
4.3 为什么 openid 要做唯一索引
用户登录后,后端通过 wx.login 的 code 调用微信接口换取 openid。openid 是用户在当前小程序里的唯一身份标识,设计时一定要加唯一索引。否则同一个用户重复登录会产生多条记录,错题和成绩无法关联。
昵称和头像的处理要注意。微信已经调整了用户信息接口,不能像以前那样直接弹出授权获取昵称头像。更稳妥的做法是:先用 openid 建立用户记录,默认头像和昵称为空,等用户主动填写或通过头像昵称填写能力补充。不要在登录流程里强制要求头像昵称,否则会卡住一批用户。
4.4 判断题、单选题、多选题的答案约定
答案字段的格式要统一,否则判题逻辑会很乱。我建议这样约定:
- 判断题:answer 存 0 或 1,0 表示错误,1 表示正确。
- 单选题:answer 存 A、B、C、D。
- 多选题:answer 存 A,B,C 或 A,C,D,统一用逗号分隔。
多选题判题时最怕用户选择顺序不一样。用户选 A,D,C,正确答案是 C,A,D,如果直接用字符串比较会判定错误。所以服务端判题前要先把选项排序再比较:
public boolean checkAnswer(String correctAnswer, String userAnswer) { String correct = normalize(correctAnswer); String user = normalize(userAnswer); return correct.equals(user); } private String normalize(String answer) { return Arrays.stream(answer.split(",")) .map(String::trim) .map(String::toUpperCase) .sorted() .collect(Collectors.joining(",")); }这样多选题不管用户点选顺序如何,判断结果都正确。
4.5 考试模式下答案安全
考试模式必须注意:不要在小程序端下发正确答案。很多新手把题库接口写成一个万能列表接口,练习和考试都用同一个接口,返回内容又把 answer 字段带出去了。用户只要在网络面板里看一次请求,就能拿到所有题目答案。
正确做法是:练习接口可以返回解析,考试接口只返回题目、选项、类型,不返回答案。交卷时服务端根据 answer 字段判分,再把成绩和解析统一返回。这样既保证考试公平,也减少不必要的业务风险。
5. 小程序端核心页面和交互:答题页是重头戏
小程序端虽然有多个页面,但真正复杂的是答题页。考试倒计时、答题卡、多选题切换、交卷确认,这些交互都要在答题页里处理。
5.1 页面目录和跳转关系
推荐页面目录如下:
pages/index/index 首页 pages/exam/exam 答题页,练习和考试共用 pages/result/result 成绩页 pages/wrong/wrong 错题页 pages/favorite/favorite 收藏页,可选 pages/profile/profile 个人中心页index 页面放两个入口:练习模式和模拟考试。点击对应入口后,跳转到 exam 页面,并带上 mode 参数。exam 页面根据 mode 决定是否显示倒计时、交卷按钮和即时解析。result 页面展示考试成绩,可以从 exam 页面带 examId 跳转,也可以从历史记录进入。
5.2 答题页的数据结构
答题页不要直接修改 questionList 里的数据,维护一份独立的答案映射表会更方便。推荐在 data 中这样设计:
data: { mode: 'practice', // practice 或 exam questionList: [], currentIndex: 0, selected: [], // 当前题已选答案 answerMap: {}, // 题号 -> 已选答案 remainSeconds: 600, // 考试剩余秒数 examId: null }questionList 只负责渲染。当前题选中状态单独用 selected。用户切换题目时,从 answerMap 中恢复 selected,这样不会出现切出去再切回来选项丢失的问题。
5.3 单选/判断题的点击逻辑
单选题和判断题,点击选项后是直接替换 selected 里的值:
handleTapOption(e) { const { value } = e.currentTarget.dataset; this.setData({ selected: [value] }); }多选题要支持选中和取消,使用 toggle 逻辑:
handleTapOption(e) { const { value } = e.currentTarget.dataset; let selected = [...this.data.selected]; const index = selected.indexOf(value); if (index > -1) { selected.splice(index, 1); } else { selected.push(value); } this.setData({ selected }); }每次点击选项后,同步更新 answerMap:
const { answerMap, currentIndex, selected } = this.data; answerMap[currentIndex] = selected; this.setData({ answerMap, selected });这样交卷时只需要遍历 answerMap,就能拼出完整的答案列表。
5.4 上一题下一题和答题卡
答题卡组件可以显示题目序号和答题状态。已答的题号用实心颜色,未答的用灰色。点击题号直接跳转到对应题目。切换题目时,要做两件事:先保存当前题答案到 answerMap,再从 answerMap 里恢复新题目的 selected。
顶部可以显示整体进度,比如“第 5/100 题”。如果要做自定义导航栏,还需要获取状态栏高度和胶囊按钮位置,使用 wx.getMenuButtonBoundingClientRect 拿到胶囊信息,否则自定义标题栏在全面屏手机上会顶到状态栏。这个细节看起来小,但真机适配时经常出问题。
5.5 倒计时和交卷
模拟考试需要倒计时。最简单的方式是 setInterval 每秒减少 remainSeconds。但用户可能锁屏、切后台,setInterval 不一定准。更稳妥的方案是记录考试开始时间 timeStamp,页面每次切到前台时,用当前时间减去开始时间,再重新计算剩余秒数:
const now = Date.now(); const elapsed = Math.floor((now - this.data.startTime) / 1000); const remain = Math.max(0, this.data.totalSeconds - elapsed); this.setData({ remainSeconds: remain });倒计时归零后自动交卷。交卷前要弹窗提示未答题目数,避免用户误点。页面 onUnload 时要清理定时器,否则会内存泄漏。
5.6 交卷和结果页
交卷时需要把 answerMap 转换成一个数组,按题目顺序提交:
const submitData = []; const { questionList, answerMap, examId } = this.data; questionList.forEach((item, index) => { const userAnswer = answerMap[index] || []; submitData.push({ questionId: item.id, userAnswer: userAnswer.join(',') }); }); wx.request({ url: 'https://your-domain.com/api/exam/submit', method: 'POST', data: { examId: examId, answers: submitData }, success: (res) => { const data = res.data.data; wx.redirectTo({ url: `/pages/result/result?examId=${data.examId}` }); } });成绩页显示分数、正确率、用时,还可以显示本次考试所有错题的解析。练习模式交卷后可以即时显示解析,考试模式要等交卷时统一返回,避免提前泄露答案。
6. 后端接口设计:返回格式、分页、随机出题和幂等交卷
后端接口设计要和前端页面对应好。接口路径、参数、返回结构都要提前约定,否则前后端联调时会反复改。
6.1 统一返回格式
建议所有接口都返回同一结构:
{ "code": 0, "message": "success", "data": {} }code 是业务状态码,0 表示成功。登录过期、参数错误、业务异常使用非 0。前端可以封装一个 request 方法,统一判断 code。不要出现一个接口返回对象,一个接口返回数组,另一个接口直接返回字符串,否则前端很难处理。
6.2 核心接口列表
第一版的核心接口可以按这张表来:
| 接口 | 方法 | 说明 |
|---|---|---|
| /api/user/login | POST | 接收 wx.login 的 code,换 openid,返回 token 和用户信息 |
| /api/question/random | GET | 随机返回练习题,支持按分类和数量过滤 |
| /api/exam/start | POST | 创建一次模拟考试,返回 examId 和题目列表,不含答案 |
| /api/exam/submit | POST | 提交考试答案,返回成绩、正确率、错题列表 |
| /api/wrong/list | GET | 分页查询错题列表 |
| /api/wrong/remove | POST | 移出错题或标记为已掌握 |
| /api/study/progress | GET | 返回用户学习进度、答题总数、正确率 |
考试开始和提交是独立接口。start 接口生成一条 exam_record,状态为进行中;submit 接口通过 examId 定位记录,计算成绩。这样即使前端网络抖动,用户重新进入考试也能找到未完成的记录。
6.3 随机出题的实现方式
练习模式要随机抽题。最直接的 SQL 是:
SELECT * FROM traffic_question ORDER BY RAND() LIMIT 10;数据量小的时候没问题,但题目上万之后,ORDER BY RAND() 会对全表排序,性能下降明显。如果只是毕设或几十人的内部系统,可以继续用。如果是准备上线的真实项目,建议先把题目 id 列表缓存起来,比如放在 Redis 或内存里,每次随机从 id 列表里抽取,再按 id 查询题目。这样可以避免大表的随机排序。
考试模式还要考虑每次考试的题目不重复、同一用户短时间内不能抽到完全一样的试卷。可以在 start 接口生成试卷时保存快照,或者只保存题目 id 列表。简单做法是生成一个随机种子,然后把题目按固定算法排序,保证考试题目稳定。
6.4 提交答案的幂等性
考试提交接口必须考虑重复提交。用户可能网络超时后点了两次交卷,或者页面重复跳转导致请求重发。如果后端不做幂等,会产生两次考试记录,成绩也会变乱。
简单方案是:start 接口在 exam_record 里生成一个唯一 examId,初始 status=0(进行中)。submit 接口先查这个 examId:
if (examRecord.getStatus() == 2) { return Result.success(examRecord); } examRecord.setStatus(2); examRecord.setScore(score); examRecord.setCorrectCount(correctCount); examRecordService.updateById(examRecord);已经交过卷的记录,重复提交时直接返回第一次的成绩。这样能避免重复计分,也能避免前端重试后表现不一致。
6.5 分页和列表过滤
错题列表、收藏列表、成绩历史这些列表接口都要做分页。分页参数统一用 page 和 size:
GET /api/wrong/list?page=1&size=20返回结构里除了 records,还要有 total。前端可以通过 total 判断是否还有下一页,配合 onReachBottom 做上拉加载。分类查询可以通过 type 参数过滤判断题、单选题、多选题,也可以通过 categoryId 按章节筛选。字段命名建议后端统一使用驼峰返回,比如 correctCount 而不是 correct_count,小程序端取值时可以少一层转换。
7. 订阅消息、微信支付和蓝牙打印:扩展功能要单独设计
核心刷题功能稳定后,很多需求方会加扩展功能。扩展功能不是不能用,但一定要和核心功能解耦,否则一个扩展功能出问题,会影响整个小程序。
7.1 订阅消息推送
订阅消息适合做学习提醒、考试结果通知、新题库上线提醒。小程序端需要先调用 wx.requestSubscribeMessage 让用户授权,拿到一次性订阅的授权后才能给用户发一次消息。同一个模板最多弹出一次授权框,不能每次都弹。
服务端发送订阅消息前,需要先获取 access_token,调用 subscribeMessage.send 接口,带上用户的 openid、模板 ID、页面路径和消息数据。access_token 有有效期,建议缓存起来,避免每次都重新获取。开发时要注意:用户不授权,服务端就不能发送;用户授权次数用完,再发送会报错。不要把消息推送设计成关键通知,比如交卷成绩不能只靠订阅消息展示,否则用户不授权就看不到成绩。
7.2 微信支付 V3:先确认资质再动手
如果要做付费题库、VIP 会员,就需要接入微信支付。微信支付 V3 涉及商户号、API 证书、API v3 密钥、平台证书、回调地址、订单号等很多要素。开发时要特别注意:
- 小程序类目必须合规,支付资质要提前申请。
- 不要拿正式 API 密钥反复测试,先用测试环境或模拟数据验证流程。
- 支付成功后依赖回调通知更新订单状态,不能只靠前端返回结果。
- 一旦小程序违规,支付功能会被限制,所以接入前一定要确认业务资质和平台规则。
我的建议是,支付功能等核心刷题和考试体系稳定后再接。不要让题库接口依赖支付模块的配置,否则支付配置缺一个参数,整个小程序都可能起不来。
7.3 蓝牙打印:线下驾校场景可以后置
有些驾校希望学员考完试直接通过蓝牙打印成绩单。这个需求在线下场景真实存在,但蓝牙打印的坑很多。小程序需要处理蓝牙适配器初始化、设备搜索、连接、服务发现、写入指令,不同打印机的指令格式还不同。这个功能适合放到二期,并单独抽出一个工具模块来写,不要和考试提交、成绩展示混合在一起。如果没有明确的线下打印需求,不建议放到第一版。
7.4 通过配置开关控制扩展功能
为了降低扩展功能带来的风险,可以在后端配置里增加开关:
feature: pay-enabled: false push-enabled: false print-enabled: false代码中通过 @ConfigurationProperties 或 @Value 读取配置。接口根据开关返回不同结果。这样做的好处是:某个扩展功能没配好,核心刷题接口仍然可以正常使用。灰度发布时也可以先开一部分用户,观察稳定性后再全量放开。
8. 从开发到上线:域名、HTTPS、版本审核和常见坑
小程序开发完成不等于上线完成。域名、HTTPS、版本审核、类目资质这些环节反而最容易卡人。建议按流程走,别跳步。
8.1 上线发布流程
正式发布流程是:在微信开发者工具中上传代码,填写版本号和备注,然后在微信公众平台提交审核。审核通过后,再点击发布。如果是自己学习或测试,可以先使用体验版,不需要提交审核。但体验版只能添加体验成员,不能面向所有用户。
正式发布前要确认小程序类目。交通规则学习类可以选择教育或工具相关类目,具体以微信公众平台当前要求为准。个人主体很多权限受限,如果要做支付、订阅消息、用户信息扩展,建议使用企业主体。
8.2 request 合法域名和 HTTPS
小程序 wx.request 请求的接口域名,必须在小程序管理后台配置为 request 合法域名。域名必须支持 HTTPS,且需要备案。开发时可以在本地设置里勾选“不校验合法域名”,但真机预览、体验版、正式版都不行。
常见问题是:服务器 80 端口能访问,443 端口没放行;SSL 证书只配置了主域名,没配置 www;接口路径里带了下划线;请求 URL 里出现 http 而不是 https。遇到接口请求失败,先看微信开发者工具 Network 面板里的具体报错,再用浏览器或 curl 直接请求接口,确认后端本身是否正常。如果后端正常但小程序请求失败,大概率是域名和证书的配置问题。
8.3 常见错误排查顺序
遇到问题先不要乱改代码,按这个顺序排查:
| 现象 | 优先排查点 | 常见处理 |
|---|---|---|
| 小程序请求报 url not in domain list | 合法域名配置 | 在公众平台添加 request 合法域名并下载校验文件 |
| 接口超时或 504 | 后端服务、数据库连接、接口耗时 | 看后端日志,确认数据库连接池是否正常 |
| 页面空白 | 接口返回格式、setData key 名 | 在 Network 面板看返回 JSON,确认字段名一致 |
| 考试无法交卷 | 是否缺少 examId、answerMap 是否为空 | 检查请求参数和后端校验逻辑 |
| 自定义导航栏错位 | 状态栏高度、胶囊位置 | 用 wx.getMenuButtonBoundingClientRect 获取实际高度 |
| 视频播放失败 | 视频域名、视频格式、组件层级 | 确认视频文件在合法域名下,或使用官方视频组件兼容方案 |
有个典型场景:如果用 uni-app 开发,从 HBuilderX 运行到微信开发者工具时提示“不是开发者”。这个问题通常不是代码问题,而是微信开发者工具没有开启服务端口。需要在微信开发者工具“设置 -> 安全设置”里打开服务端口,并确认 HBuilderX 里配置的微信开发者工具路径正确。
8.4 低配服务器上的性能和并发边界
交规系统的特点是读多写少,大多数请求是拉取题目和提交答案。如果只有 2 核 4G 的轻量服务器,日常几十人、上百人在线问题不大。但模拟考试会集中在某个时间段,可能产生并发高峰。
上线前期不要盲目调高并发参数。Tomcat 默认线程数可以参考,但不建议一上来就调到 1000,低配服务器很可能会 OOM。更稳妥的做法是:先用脚本模拟 50 个用户同时提交答案,观察接口平均响应时间和错误率。如果响应时间没有明显劣化,再逐步增加并发。同时给 MySQL 连接池设置合理上限,比如 20 到 50,避免连接被占满导致后端假死。
8.5 上线后的数据监控和备份
上线后至少要保留后端日志和慢 SQL 日志。每次发版前先备份数据库,尤其是题库表、用户表、考试记录表。题目更新不要直接改生产库,建议通过管理后台导入 Excel,导入时校验题号、答案、重复内容。上线后重点看几个指标:日活、模拟考试完成率、平均成绩、错题集中分布。错题排行能反哺题库内容,比如某些题目错误率过高,可能是题目表述有问题,也可能是讲解不够清楚,这时可以针对性地补充解析。
其实这类小程序真正落地后,最影响体验的往往不是某个炫技功能,而是题库结构是否清晰、考试提交是否可靠、域名和证书是否配置正确。先跑通最小闭环,再补扩展功能,会省掉很多返工。我个人的建议是:第一版只做练习、模拟考试、错题和成绩四个模块,等这些稳定了,再慢慢加订阅消息、支付和蓝牙打印。