简介:基于小程序与SpringBoot+Vue的急救常识学习系统,是一套面向毕业设计、课程设计或前后端分离初学者的完整工程。项目以SpringBoot提供后端接口,Vue与uniapp构建管理端及小程序端,MySQL存储急救知识数据,整体采用前后端分离架构,可通过SQL脚本完成数据库初始化并部署运行。压缩包共1334个文件,约11.95MB,主要涉及Java源码、Vue组件、小程序wxml/wxss页面、JS逻辑脚本、JSON配置、图片素材及数据库SQL文件,目录按前端、后端、小程序等模块划分,可对照查看接口调用与数据流。目前已有75人学习下载,适合需要快速搭建急救常识学习系统并做二次开发的读者。资源内提供可运行源码、SQL脚本和配套文档,可支撑从环境配置、接口调试到页面联调的完整实践,便于学习SpringBoot与uniapp的结合方式及项目改造思路,对理解前后端分离项目结构和跨端开发流程有直接参考价值。
1. 基于小程序做急救常识学习,SpringBoot + Vue + uniapp 这套组合到底怎么分工
急救常识这类内容天然适合做成学习系统:知识点短、形式固定(图文、视频、题库),又需要记录用户学了什么、练得怎么样。但很多初接触的开发者容易把它想成“做一个文章展示小程序”,于是把题库、记录、统计全塞进前端,后端只留一个用户登录。这样做 demo 可以,真到上线就会发现权限、数据回溯、多端复用得全部重来。这个标题里出现的 SpringBoot、Vue、uniapp 三件套,指向的是一个更常见的生产结构:SpringBoot 提供业务接口与数据持久化,Vue 负责后台管理页面,uniapp 负责面向用户的小程序端。三者的边界如果切对,后续加功能、换端口都只是增量工作。
这套技术选型适合谁?适合已经能独立写接口和页面的后端或全栈工程师,想用一套代码同时覆盖微信小程序、H5 甚至 App 端,又不想给学习类业务引入过重的微服务架构。文章会顺着“数据模型 → 后端接口 → 小程序端实现 → 部署验证”这条完整链路走一遍,把参数设计和排错点都放在可执行的位置上。你不需要依赖任何付费组件,纯开源工具就能把它跑起来。
2. SpringBoot 端先立数据模型:急救常识的内容结构不是一张表能搞定的
学习系统的核心不是“展示”,而是“学习闭环”:用户看内容、做练习、留下记录。所以后端设计的第一步是认清业务对象,而不是急着写 Controller。
2.1 急救常识学习系统的实体划分与表结构设计
常见的误区是建一张article表,字段放标题、正文、分类,然后就没有然后了。实际至少要拆成四类实体:
- 知识点(急救常识条目)
- 题库(选择题,绑定知识点)
- 用户学习记录(看过哪个知识点、最后一次学习时间)
- 练习记录(答对答错、得分)
知识点和题库都要有“分类”字段。急救常识按场景分:心肺复苏、止血包扎、烧伤烫伤、异物卡喉、中暑溺水等。分类不建议单独建表,用category_code字符串即可,理由是这个分类层级浅、数量少,建表反而多一次 JOIN。
一张核心表参考结构如下:
CREATE TABLE `firstaid_knowledge` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `title` varchar(200) NOT NULL COMMENT '标题', `category_code` varchar(50) NOT NULL COMMENT '分类编码,如 cpr、bleeding', `content` text NOT NULL COMMENT '正文,支持富文本或 markdown', `video_url` varchar(500) DEFAULT NULL COMMENT '视频链接,存相对路径或第三方播放地址', `cover_url` varchar(500) DEFAULT NULL COMMENT '封面图', `difficulty` tinyint(4) DEFAULT 1 COMMENT '难度:1入门 2进阶 3专业', `view_count` int(11) DEFAULT 0 COMMENT '浏览量', `status` tinyint(4) DEFAULT 1 COMMENT '1上架 0下架', `sort_order` int(11) DEFAULT 0 COMMENT '排序', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_code`), KEY `idx_status_sort` (`status`, `sort_order`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='急救常识知识点表';题库表的差异点是必须有knowledge_id外键和analysis字段。analysis是答案解析,很多系统把解析直接放前端,这会导致用户抓包后能看到所有题目的答案。后端返回题目时只返回题干和选项,不返回答案,提交后再判断并返回解析,这个细节决定了练习功能是否合规。
用户学习记录表建议用联合主键(user_id, knowledge_id),记录最后学习位置。如果之后要做“继续学习”功能,还需要冗余一个last_position(视频播放秒数或文章滚动比例)。这两个字段在初期容易漏,后续补字段虽然不难,但要处理存量数据,成本比一开始加上高得多。
2.2 用 SpringBoot 把领域服务搭起来的基建选择
项目骨架用 Spring Initializr 生成即可,依赖选spring-boot-starter-web、mybatis-plus、mysql-connector-java、lombok。这里核心选择是 MyBatis-Plus 而不是 JPA——原因有两条:
- 学习系统里单表 CRUD 占绝大多数(知识点增删改查、题库维护),MyBatis-Plus 的
BaseMapper几乎不用写 SQL。 - 项目交接和招聘时,国内团队对 MyBatis 系更熟悉,自定义 SQL 排查更直观。
实体类用@TableName注解映射表名,字段上加@TableId(type = IdType.AUTO)。注意content这种大字段在 MyBatis-Plus 查询时默认会查出来,列表页不需要正文,可以用LambdaQueryWrapper.select()排除大字段,减少网络传输和内存占用。这是一个容易忽略的点,但是列表接口响应变慢的常见原因。
项目分包按controller / service / mapper / entity / common展开。common里放统一响应体R<T>、异常处理、分页参数。统一响应体建议直接给固定格式:
public class R<T> { private Integer code; private String msg; private T data; public static <T> R<T> ok(T data) { ... } public static <T> R<T> fail(String msg) { ... } }这个小类算是个“傻瓜式”设计,但实用价值极高。后端任何接口都返回这个结构,前端 axios 拦截器统一判断code字段,省掉大量重复的异常分支。为什么不直接用 HTTP 状态码?因为小程序端的网络请求库对非 2xx 的处理会自动走到 fail 回调,但很多系统会把业务错误(参数不对、内容下架)也映射成 4xx,这会导致前端无法区分“请求失败”和“业务失败”,而用业务 code 能把两者彻底分开。
2.3 小程序端和后端接口的字段对齐:别用驼峰直接连数据库
SpringBoot 后端默认返回驼峰命名(如viewCount),uniapp 端 JS 也习惯用驼峰,这段天然一致。但有两处必须做转换:
- 数据库下划线字段 → 实体类驼峰属性,MyBatis-Plus 默认开启
map-underscore-to-camel-case,不用额外配。 - 实体类布尔字段不要用
is开头命名。比如isHot会被框架识别为hot,造成局部 bug 且肉眼极难发现。建议改成hotFlag或status = 1来标记。
写接口时另一个常见坑是时间格式。MySQLdatetime返回给 Jackson 序列化后默认是一长串时间戳,小程序端拿到后要自己格式化。推荐直接加配置:
spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8这样前端直接用字符串渲染即可。如果后续有“距离上次学习已过 N 天”这类计算,前端拿到字符串后用new Date()解析即可,无兼容问题。
3. 后端接口实现:知识点列表、详情、练习提交三条核心链路的写法
接口设计不追求多,追求链路完整。学习系统最核心的接口就三个:列表(含分类筛选与分页)、详情(计数 + 记录学习行为)、提交练习(判分 + 返回解析)。把这三个写稳,其余功能都是增补。
3.1 知识点列表接口:分类筛选、分页与浏览计数的实现
Controller 层用@RestController+@RequestMapping("/api/knowledge")。列表接口接收四个参数:pageNum、pageSize、categoryCode、keyword。keyword支持标题模糊搜索是刚需,用户可能记不清完整标题,只记得“心”“压”等关键词。
@GetMapping("/list") public R<Page<KnowledgeVO>> list( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) String categoryCode, @RequestParam(required = false) String keyword) { LambdaQueryWrapper<Knowledge> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(categoryCode), Knowledge::getCategoryCode, categoryCode) .like(StringUtils.isNotBlank(keyword), Knowledge::getTitle, keyword) .eq(Knowledge::getStatus, 1) .orderByAsc(Knowledge::getSortOrder) .orderByDesc(Knowledge::getCreateTime); wrapper.select(Knowledge.class, i -> !"content".equals(i.getColumn()) && !"videoUrl".equals(i.getColumn())); Page<Knowledge> page = knowledgeService.page(new Page<>(pageNum, pageSize), wrapper); return R.ok(page); }这段代码的要点在wrapper.select那行:列表页只需要标题、封面、分类、难度、浏览量,不需要正文和视频地址。排除大字段后,接口响应体从几 KB 降到了几百字节,列表滑动加载明显变快。eq和like方法的第一个参数是布尔条件,条件为true才拼 SQL,这是 MyBatis-Plus 条件构造器的核心用法。orderByAsc(Knowledge::getSortOrder)保证运营手动排序生效,orderByDesc(Knowledge::getCreateTime)作为次级排序。
请求参数那行@RequestParam(defaultValue = "1")是必需的,前端如果漏传参数,后端不至于报 500。在接口文档中要写明 pageNum 从 1 开始,很多前端同学会下意识以为是 0 开始,调通之前会浪费一次沟通成本。
3.2 详情与学习记录的写入时机
详情接口返回完整内容,同时需要做两件事:浏览量加一、写入/更新学习记录。有些后台系统会用 Redis 做计数器异步落库,这里不过度设计,直接同步完成即可,单机每秒几百次点击完全扛得住。
@GetMapping("/detail/{id}") public R<KnowledgeVO> detail(@PathVariable Long id, @RequestParam Long userId) { Knowledge knowledge = knowledgeService.getById(id); if (knowledge == null || knowledge.getStatus() != 1) { return R.fail("内容不存在或已下架"); } // 浏览量 +1,并发下使用 setSql 原子更新 LambdaUpdateWrapper<Knowledge> updateWrapper = new LambdaUpdateWrapper<>(); updateWrapper.eq(Knowledge::getId, id) .setSql("view_count = view_count + 1"); knowledgeService.update(updateWrapper); // 记录学习行为 LearningRecord record = new LearningRecord(); record.setUserId(userId); record.setKnowledgeId(id); record.setLastTime(new Date()); record.setUserId(userId); learningRecordService.saveOrUpdate(record, new LambdaQueryWrapper<LearningRecord>() .eq(LearningRecord::getUserId, userId) .eq(LearningRecord::getKnowledgeId, id)); return R.ok(convertToVO(knowledge)); }setSql("view_count = view_count + 1")是原子自增的关键。如果先查出来getViewCount() + 1再 update,两个并发请求会互相覆盖,浏览量会偏小。saveOrUpdate的第一个参数是实体对象,第二个参数是查询条件,满足条件就更新,否则插入,恰好匹配“学习记录存在则更新时间,不存在则新增”的场景。
注意这里userId直接从参数传进来的做法,在真实项目中应该从 JWT Token 获取。本文不展开权限设计,但至少要明白:小程序端传什么userId都信,是生产事故的温床。可以在HandlerInterceptor里统一从请求头token解析用户,Controller方法参数里只留@RequestParam的knowledgeId。
3.3 练习提交接口:前端不拿答案,后端批量判分
练习题目的核心矛盾是:提交时要一次性提交一组答案,而不是一题一题提交。一次十题,用户做完后点“交卷”,前端把[{questionId, answer}]数组 POST 给后端。后端循环判分,统计得分率,保存练习记录,返回结果列表(题干、用户答案、正确答案、解析)。
@PostMapping("/practice/submit") public R<PracticeResultVO> submit(@RequestBody PracticeSubmitDTO dto) { List<Question> questions = questionService.listByIds( dto.getAnswers().stream().map(PracticeAnswerDTO::getQuestionId).collect(Collectors.toList())); int correctCount = 0; List<QuestionResultVO> results = new ArrayList<>(); for (Question q : questions) { String userAnswer = dto.getAnswers().stream() .filter(a -> a.getQuestionId().equals(q.getId())) .findFirst() .map(PracticeAnswerDTO::getAnswer) .orElse(""); boolean correct = q.getCorrectAnswer().equals(userAnswer); if (correct) correctCount++; QuestionResultVO vo = new QuestionResultVO(); vo.setQuestionId(q.getId()); vo.setStem(q.getStem()); vo.setOptions(q.getOptions()); vo.setUserAnswer(userAnswer); vo.setCorrectAnswer(q.getCorrectAnswer()); vo.setAnalysis(q.getAnalysis()); vo.setCorrect(correct); results.add(vo); } // 保存练习记录:总分、正确率、耗时 PracticeRecord record = new PracticeRecord(); record.setUserId(dto.getUserId()); record.setTotalCount(questions.size()); record.setCorrectCount(correctCount); record.setScore(correctCount * 100 / questions.size()); record.setDurationSeconds(dto.getDurationSeconds()); practiceRecordService.save(record); PracticeResultVO result = new PracticeResultVO(); result.setScore(correctCount * 100 / questions.size()); result.setResults(results); return R.ok(result); }correctCount * 100 / questions.size()这里先乘后除,避免整数除法直接把小数抹掉。例如 3 题对 2 题:2 * 100 / 3 = 66,而100 / 3 * 2 = 0。这是后端开发最常见的低级但隐蔽的 bug,加一段注释防未来接手的人改错。
PracticeSubmitDTO里必须有@NotNull校验,@Validated注解加在接口参数上,内容为空时直接返回 400。练习记录只需要存储分数和题目数量,不要逐个存用户答案,否则数据量会爆炸。想看某次练习具体错了哪题,可以在返回结果里让前端单次展示,历史记录只存分数。
4. uniapp 小程序端:从页面结构到请求封装、学习进度条与视频兼容
小程序端是用户直接接触的部分,开发体验和微信平台限制是绕不开的话题。uniapp 的价值在于:一份代码编译到微信小程序、H5、App。但“一套代码跑三端”是有代价的,最典型的就是视频组件差异和下钻场景的条件编译。
4.1 项目初始化和请求拦截器的最小可运行配置
用 HBuilderX 创建 uni-app 项目,选择 Vue 3 版本。模板选默认空模板,不要选带 UI 库的模板,原因有二:后续可能用 uview-plus 或 nut-ui,预置模板反而有冗余;部分模板自带的地图、登录等示例会让新人误以为平台有内置完整逻辑。
请求封装用uni.request包一层 Promise。小程序端没有 axios,但可以封装出类似体验:
// utils/request.js const BASE_URL = 'https://api.example.com' export function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.data) } else { uni.showToast({ title: res.data.msg, icon: 'none' }) reject(res.data) } }, fail: (err) => { uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) reject(err) } }) }) }BASE_URL一定要用 HTTPS 且在微信公众平台配置合法域名,开发调试时可以临时勾选“不校验合法域名”,但上线前必须配好。把BASE_URL抽成单独常量文件而不是写死在每个页面,因为测试环境和生产环境要切换,改动一处即可生效。uni.showToast是全局统一错误提示,统一在拦截器里做,页面层不再重复写 loading 和错误弹窗,代码量会大幅度降低。
4.2 首页分类导航与知识点列表页的生命周期细节
小程序首页布局通常是:顶部搜索框、分类横向滚动、下方知识点瀑布流或列表。分类切换和数据加载要处理“重复请求”与“下拉刷新”两个场景。
分类数据结构不用频繁请求接口,可以在onLoad时一次性拉取并缓存到globalData,之后切 tab 直接本地过滤,避免每次切换都转圈。列表数据要分页:onReachBottom里判断pageNum * pageSize < total才能继续请求下一页,否则提示“没有更多了”。这个判断防不了高并发,但对单用户操作足够。
列表项的关键数据是view_count,它构成用户的选择偏好——“看的人多”会被理解为“内容靠谱”。列表项 UI 右下角显示阅读量,格式化函数是常见小工具:
export function formatCount(count) { if (count >= 10000) { return (count / 10000).toFixed(1) + '万' } return String(count) }4.3 学习记录与进度显示的落地做法
“学习进度”是这个系统区别于普通文章站的功能。学习进度有两种实现粒度:
- 粗粒度:用户点开详情页就记录“已学习”,适合纯图文内容。
- 细粒度:记录阅读位置或视频播放位置,适合长文和视频。
急救常识正文一般 800 字以内,用粗粒度即可。但用户需要在小程序首页看到一个“继续学习”入口,数据来源于学习记录表里last_time最新的记录。列表接口:
@GetMapping("/learning/recent") public R<List<LearningRecordVO>> recent(@RequestParam Long userId) { List<LearningRecord> records = learningRecordService.list( new LambdaQueryWrapper<LearningRecord>() .eq(LearningRecord::getUserId, userId) .orderByDesc(LearningRecord::getLastTime) .last("limit 5")); // 联表查知识点标题、封面 ... }这里last("limit 5")是 MyBatis-Plus 直接拼接 SQL 尾部的方法,只传常量不传用户输入,没有注入风险。联表建议手动写一个LearningRecordMapper.xml里的selectRecentWithKnowledge方法,涉及多表查询时不要用QueryWrapper拼 JOIN,可读性差而且不好维护。
“继续学习”这个入口是提高留存的小功能,成本低但用户感知强。它相当于把被动浏览变成了主动延续:用户不用翻记录去找上次看的条目。
4.4 小程序端视频播放的兼容问题
急救常识经常会配教学视频,比如心肺复苏的正确手势。视频格式建议用 MP4(H.264 编码),微信小程序原生<video>组件直接支持。如果视频源是 m3u8(HLS),微信小程序并不直接支持播放,需要额外处理,这是最常见的“开发时好好的、真机一测黑的”场景。
推荐的做法:后端转码后提供 MP4 地址,或使用支持 HLS 的第三方播放器插件。小型系统选第一条路更省事。但要注意:
- 视频文件不要放服务器本地磁盘,应该传到对象存储(阿里云 OSS、腾讯云 COS),否则带宽会成为瓶颈。
- 视频地址要加签名防盗链,否则会被外部站点直接引用。对象存储一般支持 URL 签名,有效期设置 30 分钟即可。
- 小程序端
<video>组件的src属性用:src动态绑定,页面onLoad时拿到的地址才是最终地址,不能写在模板字符串里硬编码。
<video v-if="detail.videoUrl" :src="videoSrc" controls @error="handleVideoError" style="width: 100%; height: 200px;" ></video>export default { data() { return { detail: {}, videoSrc: '' } }, onLoad(options) { this.loadDetail(options.id) }, methods: { async loadDetail(id) { const data = await request({ url: `/knowledge/detail/${id}`, data: { userId: this.userId } }) this.detail = data this.videoSrc = data.videoUrl }, handleVideoError() { uni.showToast({ title: '视频加载失败', icon: 'none' }) } } }v-if="detail.videoUrl"保证没有视频时组件不渲染,否则会出现一个黑框占位。有些开发会忘记清理videoSrc,导致切换知识点时旧视频一闪而过。在onLoad里赋值就没有这个问题,因为每次进入页面都是新实例。
5. Vue 管理后台与 SpringBoot 项目的目录协同
管理后台用 Vue 3 + Element Plus,通过 Vite 构建。它解决的是素材维护问题:运营人员录入急救常识内容、上传图片和视频、管理题库。这个后台不直接面对 C 端用户,所以界面可以尽量朴素,但功能一个不能少。
5.1 管理后台“内容录入”页面的关键交互
急救常识的后台管理页面,文章编辑器是最复杂的部分。运营录入的正文要支持图文混排,建议直接用v-html渲染富文本。富文本编辑器选 wangEditor 5 或 quill,两者都是纯前端开源方案,无授权问题。上传图片用el-upload组件的自定义上传方法,走后端/api/upload接口,返回图片 URL 后插入编辑区。
<el-upload :action="uploadUrl" :headers="{ token: getToken() }" :on-success="handleUploadSuccess" name="file" > <el-button size="small">上传图片</el-button> </el-upload>后端upload接口的核心逻辑是保存文件并返回访问路径。关键参数是文件大小上限、允许的扩展名集合。建议限制为 jpg/png/gif/webp,单张不超过 5MB,否则存储压力大且前端加载慢。
getToken()方法从localStorage里取管理员登录后的 token。前后端分离下,文件上传接口也需要鉴权,不能裸奔。handleUploadSuccess回调里拿到后端返回的 URL,插入富文本编辑器的当前位置。这一步如果做不好,运营每次都要先传图再手动拼链接,录入效率会降低一半。
5.2 管理端路由权限与 Vue 动态添加路由的实现
管理端有“管理员”和“运营编辑”两种角色,管理员能删数据、配置系统参数,运营只能维护内容。路由权限用 Vue Router 的beforeEach全局守卫 + 动态addRoute实现。
router.beforeEach((to, from, next) => { const token = localStorage.getItem('admin_token') if (!token && to.path !== '/login') { next('/login') } else { next() } })动态权限在登录后拉取用户角色,根据角色过滤路由表。新手容易跳过这块,直接写死全部路由,开发很爽但上线会出安全问题:运营直接改 URL 就能进入系统管理页。虽然按钮不显示,但路由注册了就能访问,这种漏洞属于“开发模式惯性”留下的隐患。最小可行做法是:写入路由表的meta.roles字段,守卫里比对,不匹配就next('/403')。
5.3 SpringBoot 后端与 Vue 前端在跨域与代理上的历史遗留问题
本地开发时 Vue 跑http://localhost:5173,后端跑http://localhost:8080,直接请求必然跨域。常见做法是 Vite 配置开发代理:
// vite.config.js export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })开发环境用代理后,前端请求/api/list会被转发到8080/api/list,浏览器的地址栏还是5173,不存在跨域问题。生产环境 Nginx 也要做同样代理,前后端部署在同一个域下。这样后端就不用配@CrossOrigin了,@CrossOrigin是临时的,配了它意味着线上会暴露允许跨域的来源,安全审计时会被扣分。
6. 从压缩包到上线的最后三件事:编译产物、环境配置与验证清单
项目的最终交付形态是.zip压缩包,里面应该是三个子项目:SpringBoot 后端、Vue 管理后台、uniapp 前端。拿到项目后不急着改代码,先按顺序做三件事:检查配置、初始化数据库、跑最小验证。
6.1 数据库初始化与 three 端配置核对
数据库脚本文件一般放在压缩包的sql/目录下。用 Navicat 或命令行导入后,先检查三张核心表是否有数据。很多时候压缩包带的是完整 SQL,导入后就有初始题库,但视频和图片 URL 是作者本机路径(如localhost:8080/files),必须全局排查替换成你自己的服务器地址。用编辑器全局搜索localhost把这个值全量替换,比在数据库里逐条改快得多,也不容易漏。
后端配置集中在application.yml,以下三个配置是上线的硬门槛:
spring: datasource: url: jdbc:mysql://localhost:3306/first_aid?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password servlet: multipart: max-file-size: 10MB max-request-size: 20MBserverTimezone=Asia/Shanghai不配,MySQL 8 驱动会报时区错误。max-file-size和max-request-size跟随你的上传限制:如果后台限制图片 5MB,那这里至少 10MB,因为 base64 上传会导致体积膨胀,虽然这里用的是 multipart 表单,但请求总大小仍要预留余量。
6.2 uniapp 小程序打包与发布检查
uniapp 怎么打包是这个项目被搜索最多的问题,具体操作路径是:HBuilderX 菜单栏“发行”→“小程序-微信”,输入微信小程序的 AppID,点击发行。产物目录在dist/build/mp-weixin,然后打开微信开发者工具,导入该目录即可预览。
打包前要检查三个配置点,任何一处遗漏都会导致真机异常:
manifest.json里的微信小程序 AppID 必须填真实值,测试号能看但无法上传。- 接口地址必须是 HTTPS 且已配置在微信公众平台的“服务器域名”里。
- 不校验合法域名只在开发者工具里勾选,真机预览需要真实可访问的 HTTPS 接口。
上线审核时,急救类内容不涉及医疗建议违规,但注意不要让系统出现“诊断”“治疗”“用药指导”等词,避免被平台分类为医疗健康类目。急救常识属于知识科普,文案上保持一致就好。
6.3 小程序端验证清单与常见失败排查
上线前跑一遍下方清单,能拦截大多数问题。用表格列出来方便逐项核对:
| 验证项 | 预期结果 | 常见失败原因 |
|---|---|---|
| 用户首次授权登录 | 弹出授权弹窗,登录后显示头像昵称 | manifest里隐私协议未配置 |
| 知识点列表加载 | 首页能看到分类和内容列表 | 后端接口 404,检查 Nginx 代理是否存在 |
| 详情页视频播放 | 视频能加载,进度条可拖拽 | 视频域名未在微信后台配置为业务域名 |
| 提交练习 | 显示得分和每道题的解析 | 请求体结构不匹配,检查JSON.stringify是否序列化了数组 |
| 后台添加内容 | 小程序端下拉能刷出新增内容 | 小程序端缓存了旧数据,手动静默缓存导致 |
“手动静默缓存”是一个容易忽略的实现问题:有些开发者会用uni.setStorageSync缓存列表数据,上线后发布新内容,用户看到的还是旧数据。要么加“下拉刷新时强制重新请求并覆盖缓存”的逻辑,要么干脆不缓存列表。学习类内容本身不重,每次请求实时拉取即可,省心且不会出同步问题。
另外建议在详情页埋点记录页面停留时长,上报到后端learning_record的duration_seconds字段。之后统计“哪个知识点完读率高、哪个分类被跳过最多”,这些数据可以指导内容调整。这个埋点用onHide和onUnload生命周期计算时间差即可,不需要引入统计 SDK,是编辑后续运营方向的低成本方案。
本文还有配套的精品资源,点击获取