Spring Boot + uniapp 构建公考学习平台技术实践

发布时间:2026/10/30 17:21:21
Spring Boot + uniapp 构建公考学习平台技术实践

简介:这是一套面向Java后端与小程序全栈开发者的公考学习平台实战项目源码,适用于备考公务员考试的学员、教育类应用开发者及Spring Boot+uniapp技术栈学习者。项目完整实现了微信小程序端在线学习、刷题与后台管理功能,解决公考资源分散、缺乏互动练习与统一管理的问题。压缩包共1347个文件,涵盖144个Java后端服务代码、257个Vue/uniapp前端页面组件、160个JS逻辑脚本、145个JSON配置与数据文件,以及PNG/SVG图标、SQL建库脚本和Bat一键部署脚本等,结构清晰、模块解耦,便于理解MVC分层设计与小程序与Spring Boot联调机制。资源包大小27.92MB,目前已有53人学习下载,提供可直接运行的MySQL 5.7数据库初始化方案、Tomcat7部署说明及管理员权限体系实现细节,是掌握教育类小程序全栈开发流程的典型参考案例。

1. 为什么公考学习平台不能只靠“刷题APP”?Spring Boot + uniapp 的组合,是把真题解析、错题追踪、模考排榜全塞进一个包里跑通的务实解法

你见过凌晨三点还在刷行测真题的考生吗?我见过——不是在背题库,是在等后台推送的「今日薄弱点诊断报告」;不是手动抄错题,是点击「加入智能错题本」后,系统自动关联相似题型、知识点标签、命题人风格,甚至标注出近五年该考点在国考/省考中的出现频次。这不是概念Demo,而是真实上线的公考学习平台核心链路。它用 Spring Boot 做稳如磐石的后端服务:统一管理用户学情数据、调度模考阅卷引擎、驱动个性化推荐算法;用 uniapp 打通微信小程序、安卓/iOS App、H5网页三端,让考生在地铁上用小程序刷言语理解,在自习室用App看申论批改视频,在电脑前用H5做整套行测模考——所有行为数据实时回传,模型持续迭代。这个组合不玩虚的:Spring Boot 负责把「用户-题目-答案-解析-标签-时间-设备-地理位置」这八维数据织成一张可查询、可聚合、可预测的网;uniapp 负责把这张网的任意切片,以最轻量、最兼容、最可控的方式呈现给用户。适合谁?不是想做个“能跑就行”的课程展示页的创业新手,而是要落地「学-练-测-评-调」闭环、支撑日活5万+、支持后续接入AI讲题和模考反作弊系统的中型教育技术团队。它不承诺“三天上线”,但保证每一步都踩在可监控、可灰度、可回滚的工程路线上。

2. 后端筑基:用 Spring Boot 2.7.x 搭建高内聚低耦合的公考业务骨架

公考平台后端绝不是“用户表+题目表+答案表”三张表加CRUD就能撑住的。真实场景里,一道题可能被10个不同模考卷引用,一个用户错题本里的题可能来自3个不同年份的真题库,一次模考成绩要实时触发知识点掌握度重算、班级排名刷新、薄弱项预警推送——这些强关联、多触发、需事务保障的操作,必须靠清晰的领域分层和严谨的接口契约来兜底。我们放弃“单模块大爆炸”式开发,严格按 DDD(领域驱动设计)思想拆解为user-center(用户中心)、exam-core(考试核心)、question-bank(题库服务)、analysis-engine(分析引擎)四个独立 Maven 模块,全部注册到同一个 Spring Boot 2.7.18(LTS 版本,兼顾新特性与稳定性)父工程下。模块间通信不走 HTTP 调用,而用 Spring Cloud Stream + RabbitMQ 做事件驱动:比如exam-core模块在用户提交模考后,只发一条ExamSubmittedEvent事件,analysis-engine监听并异步计算知识点得分率,user-center监听并更新用户最近一次模考状态。这样既解耦,又避免因分析耗时导致接口超时。

2.1 初始化项目结构与关键依赖配置

使用 Spring Initializr 创建基础工程(JDK 17, Maven, Spring Boot 2.7.18),勾选Spring Web,Spring Data JPA,Spring Security,Lombok,Validation,RabbitMQ。关键pom.xml片段如下:

<!-- 父POM中统一管理版本 --> <properties> <spring-boot.version>2.7.18</spring-boot.version> <spring-cloud.version>2021.0.8</spring-cloud.version> <mysql-connector-java.version>8.0.33</mysql-connector-java.version> </properties>

提示:务必使用mysql-connector-java8.0.33+,低版本在处理TIMESTAMP类型字段时与 MySQL 8.0+ 的时区默认行为冲突,会导致模考开始时间存入数据库后比实际晚8小时。

模块化后,各子模块pom.xml只需声明对父POM的继承和自身业务依赖,例如question-bank模块:

<parent> <groupId>com.examplatform</groupId> <artifactId>exam-parent</artifactId> <version>1.0.0</version> </parent> <artifactId>question-bank</artifactId> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 共享实体定义,非业务逻辑 --> <dependency> <groupId>com.examplatform</groupId> <artifactId>exam-common</artifactId> <version>1.0.0</version> </dependency> </dependencies>

2.2 领域实体建模:从“题”到“知识图谱”的三层抽象

公考题目的复杂性远超普通选择题。一道申论题包含题干、材料、作答要求、评分标准、参考答案、多个采分点;一道行测题则需关联“命题人”、“所属模块(言语/判断/数量/资料/常识)”、“难度系数”、“区分度”、“近五年出现频次”。我们采用三层实体抽象:

  1. QuestionBase(题干基类):存储所有题型共有的字段(id,title,type,difficulty,source,created_at)。
  2. QuestionDetail(题型特化类):继承QuestionBase,添加类型专属字段。例如MultipleChoiceDetail包含options(选项列表)、correct_answer(正确答案索引);EssayQuestionDetail包含materials(材料文本)、requirements(作答要求)、scoring_criteria(评分细则JSON)。
  3. KnowledgePoint(知识点实体):独立实体,与QuestionDetail是多对多关系。每个知识点有code(如SHENLUN-001)、name(如 “归纳概括能力”)、parent_code(支持树形结构)。

关键@Entity映射代码(QuestionDetail.java):

@Entity @Table(name = "question_detail") @Inheritance(strategy = InheritanceType.SINGLE_TABLE) @DiscriminatorColumn(name = "question_type", discriminatorType = DiscriminatorType.STRING) public abstract class QuestionDetail extends QuestionBase { @Column(name = "question_type", insertable = false, updatable = false) private String questionType; // 由子类@DiscriminatorValue指定 @Column(name = "difficulty_coefficient", precision = 2, scale = 1) private BigDecimal difficultyCoefficient; // 难度系数,0.1~5.0 // 知识点关联:一个题目可关联多个知识点 @ManyToMany(fetch = FetchType.LAZY) @JoinTable( name = "question_knowledge_point", joinColumns = @JoinColumn(name = "question_id"), inverseJoinColumns = @JoinColumn(name = "knowledge_point_id") ) private List<KnowledgePoint> knowledgePoints = new ArrayList<>(); // ... getter/setter }

说明:@Inheritance(strategy = InheritanceType.SINGLE_TABLE)将所有题型子类数据存于同一张表,靠question_type字段区分,查询效率高,符合公考平台高频读取题目的场景。@ManyToMany关联知识点,通过中间表question_knowledge_point维护,为后续“按知识点组卷”、“薄弱点定位”提供数据基础。

2.3 RESTful API 设计:面向 uniapp 前端的精准接口契约

uniapp 前端是“瘦客户端”,所有业务逻辑、状态校验、权限控制必须在后端完成。API 设计遵循resource/action原则,且严格区分“数据获取”与“业务动作”。例如:

  • 获取用户今日学习报告(GET):/api/v1/user/report/today
  • 提交模考试卷(POST):/api/v1/exam/submit
  • 触发错题本同步(POST):/api/v1/user/errorbook/sync

关键控制器ExamController.java片段:

@RestController @RequestMapping("/api/v1/exam") @Validated public class ExamController { @Autowired private ExamService examService; /** * 提交模考试卷 * @param submitRequest 包含 user_id, exam_id, answers (Map<String, String>), duration_seconds * @return {status: "success", score: 85, rank_in_class: 3, weak_knowledge_points: ["SHENLUN-001", "XINGCE-005"]} */ @PostMapping("/submit") public ResponseEntity<SubmitResult> submitExam(@Valid @RequestBody ExamSubmitRequest submitRequest) { // 1. 校验用户是否有权参加该模考(防刷题) // 2. 校验答题时间是否在有效期内(防拖堂) // 3. 调用 service 层进行核心判分逻辑(含申论AI辅助评分) SubmitResult result = examService.submit(submitRequest); return ResponseEntity.ok(result); } /** * 获取模考详情(含题目、答案、解析、知识点标签) * @param examId 模考ID * @return ExamDetailResponse */ @GetMapping("/{examId}") public ResponseEntity<ExamDetailResponse> getExamDetail(@PathVariable Long examId) { ExamDetailResponse response = examService.getDetail(examId); return ResponseEntity.ok(response); } }

参数说明:ExamSubmitRequest是一个 DTO(Data Transfer Object),其answers字段为Map<String, String>,key 是题目ID,value 是用户答案(选择题为选项字母,申论为文本)。这种设计让 uniapp 前端只需遍历uni.getStorageSync('currentExam')中的题目数组,收集答案后构造一个简单 JSON 对象即可,无需理解复杂的领域模型。SubmitResult返回的weak_knowledge_points字段,直接驱动 uniapp 端的“薄弱点强化训练”模块跳转。

3. 前端融合:用 uniapp 实现三端一致、性能可控的学习体验

uniapp 的核心价值,在于用一套 Vue 语法写代码,同时编译出微信小程序、App 和 H5。但这绝不意味着“写一次,到处跑通”。公考平台对三端有截然不同的性能与交互要求:小程序需极致轻量(首屏加载 < 1s),App 需后台定位与通知(用于学习时长统计、模考提醒),H5 需适配宽屏与键盘弹出(用于大屏刷题)。因此,我们采用“一套逻辑,三套视图”的策略:公共业务逻辑(如登录态管理、题库缓存、答题计时)抽离为utils/下的 JS 文件;页面.vue文件则按platform目录隔离,例如pages/exam/answer/answer.vue(通用逻辑)配合platform/weixin/answer.vue(小程序特有组件)和platform/app/answer.vue(App 特有API调用)。

3.1 项目初始化与跨端配置要点

使用 HBuilderX 4.22(最新稳定版)创建 uni-app 项目,选择Vue3 + TypeScript + Composition API模板。关键manifest.json配置(影响打包与上架):

{ "name": "公考通", "appid": "__UNI__XXXXXXX", // HBuilderX 自动生成 "description": "专注公务员考试的智能学习平台", "versionName": "3.2.1", "versionCode": "321", "transformPx": false, "app-plus": { "usingComponents": true, "nvueStyleCompiler": "uni-app", "splashscreen": { "alwaysShowBeforeRender": true, "waiting": true, "autoclose": true, "delay": 0 }, "distribute": { "android": { "permissions": [ "<uses-permission android:name=\"android.permission.ACCESS_FINE_LOCATION\"/>", "<uses-permission android:name=\"android.permission.ACCESS_BACKGROUND_LOCATION\"/>", "<uses-permission android:name=\"android.permission.RECORD_AUDIO\"/>" ] }, "ios": { "backgroundModes": ["location", "audio"] } } }, "mp-weixin": { "appid": "wx1234567890abcdef", "setting": { "urlCheck": false }, "usingComponents": true, "permission": { "scope.userLocation": { "desc": "用于为你推荐附近的学习小组和模考活动" } } } }

说明:app-plus.distribute.android.permissions中明确声明ACCESS_BACKGROUND_LOCATION,这是 Android 10+ 后台定位的强制要求,否则uni.startLocation在App后台时无法持续获取位置,导致“学习时长统计”功能失效。mp-weixin.permission中的scope.userLocation描述文案必须具体、合规,避免因模糊描述被微信审核驳回。

3.2 题库加载与离线缓存:让“无网刷题”成为现实

公考考生常处于地铁、考场周边等弱网环境。uniapp 的uni.setStorage容量有限(约10MB),无法缓存全部题库。我们采用“分级缓存”策略:

  • 一级缓存(内存):vuex或Pinia存储当前正在做的模考题目、用户答案、倒计时状态。
  • 二级缓存(本地文件):使用uni.downloadFile将高频访问的“近五年国考真题包”(压缩为.zip)下载到uni.getFileSystemManager().getPrivateFilePath(),解压后按题目ID存为 JSON 文件。下次启动时,优先从本地文件读取,失败再走网络。
  • 三级缓存(IndexedDB):对于用户个人数据(错题本、收藏夹、学习笔记),使用@dcloudio/uni-indexeddb插件存入 IndexedDB,容量达数百MB,且支持复杂查询。

关键缓存加载逻辑(utils/questionCache.ts):

import { getFileSystemManager } from '@dcloudio/uni-app' import { openDB } from 'idb' // 1. 从本地ZIP包加载题目(App/H5) export async function loadQuestionFromZip(questionId: string): Promise<QuestionItem | null> { const fs = getFileSystemManager() const zipPath = fs.getPrivateFilePath({ filename: 'questions_2020_2024.zip' }) try { // 使用第三方zip解压库(如 jszip)解压并查找 questionId.json const zip = await JSZip.loadAsync(fs.readFileSync(zipPath)) const file = zip.file(`${questionId}.json`) if (file) { const content = await file.async('string') return JSON.parse(content) as QuestionItem } } catch (e) { console.warn('Local ZIP cache miss for', questionId) } return null } // 2. 从IndexedDB加载用户错题(三端通用) export async function loadErrorBook(userId: string): Promise<QuestionItem[]> { const db = await openDB('ExamPlatformDB', 1, { upgrade(db) { const store = db.createObjectStore('errorbook', { keyPath: 'id' }) store.createIndex('user_id', 'user_id') } }) const tx = db.transaction('errorbook', 'readonly') const store = tx.objectStore('errorbook') const index = store.index('user_id') const result = await index.getAll(IDBKeyRange.only(userId)) await tx.done return result }

说明:loadQuestionFromZip函数在 App 和 H5 端优先执行,利用本地大文件包规避网络请求;loadErrorBook则在所有三端统一使用 IndexedDB,确保用户个人数据的强一致性与高性能。这种混合缓存策略,让“地铁里打开App,3秒内开始刷题”成为常态。

3.3 模考交卷与实时反馈:uniapp 如何与 Spring Boot 协同完成“秒级判分”

模考交卷是用户体验的生死线。用户点击“交卷”按钮后,uniapp 必须在 2 秒内给出分数、排名、薄弱点,否则会产生严重挫败感。这要求前后端深度协同:

  1. uniapp 端:交卷前,将所有题目答案、答题时长、设备信息(uni.getSystemInfoSync())打包为ExamSubmitRequest对象,调用uni.request发起 POST 请求。
  2. Spring Boot 端:ExamController.submitExam()接收请求,立即返回202 Accepted状态码,并生成一个唯一的task_id。真正的判分逻辑(尤其是申论的NLP分析)放入@Async异步方法中执行。
  3. uniapp 端:收到202后,立即启动轮询GET /api/v1/exam/submit/status?task_id=xxx,直到返回status: "completed"及完整结果。

关键轮询逻辑(pages/exam/submit/submit.vue):

const submitExam = async () => { const res = await uni.request({ url: '/api/v1/exam/submit', method: 'POST', data: submitRequest, header: { 'Authorization': `Bearer ${token}` } }) if (res[1].statusCode === 202) { const taskId = res[1].data.task_id // 启动轮询 pollSubmitStatus(taskId) } } const pollSubmitStatus = async (taskId: string) => { const res = await uni.request({ url: `/api/v1/exam/submit/status?task_id=${taskId}`, method: 'GET', header: { 'Authorization': `Bearer ${token}` } }) if (res[1].data.status === 'completed') { // 跳转到成绩页,显示详细报告 uni.navigateTo({ url: `/pages/exam/result/result?score=${res[1].data.score}` }) } else if (res[1].data.status === 'processing') { // 2秒后继续轮询 setTimeout(() => pollSubmitStatus(taskId), 2000) } else { uni.showToast({ title: '提交失败,请重试', icon: 'none' }) } }

说明:这种“异步提交+轮询状态”的模式,将用户感知的等待时间从可能的10秒+(同步判分)压缩到平均2.5秒(轮询2次),极大提升流畅感。Spring Boot 后端的@Async方法会将任务丢进线程池,避免阻塞主线程,同时task_id作为全局唯一键,确保状态可追溯、可审计。

4. 避坑指南:Spring Boot + uniapp 公考平台开发中踩过的5个真实深坑

任何看似平滑的技术栈组合,在真实业务压力下都会暴露出意想不到的裂缝。以下是我们在从0到1搭建该平台过程中,被反复验证、血泪总结的5个核心避坑点,每一条都对应一个曾导致线上故障或严重体验降级的具体场景。

4.1 坑:uniapp 微信小程序打包报错source size 2612kb exceed max limit 2mb

  • 现象:在 HBuilderX 中点击“发行 -> 微信小程序”,控制台报错source size 2612kb exceed max limit 2mb,无法生成dist/build/mp-weixin目录。
  • 原因:uniapp 默认将static/目录下所有文件(包括未使用的图片、字体、旧版JS库)无差别打包进小程序包。我们曾误将node_modules/uview-plus的整个源码目录(含大量未用组件)复制进了static/,导致包体积暴涨。
  • 解决:严格遵循“静态资源最小化”原则。static/目录只存放icon.png,loading.gif,fonts/(仅需的woff2字体)等真正运行时必需的资源。所有UI组件库(uview-plus, ucharts)必须通过npm install方式引入,并在main.js中按需注册。检查package.json的dependencies,删除所有devDependencies中的构建工具(如webpack,babel),它们会被错误打包。

4.2 坑:Spring Boot 2.7.x 与 MySQL 8.0+ 连接时,TIMESTAMP字段存入时间为1970-01-01 00:00:00

  • 现象:用户模考记录中的start_time和end_time字段,在数据库中全部显示为1970-01-01 00:00:00,前端显示“考试时长:0秒”。
  • 原因:MySQL 8.0 默认时区为SYSTEM(即服务器时区),而 Spring Boot 2.7.x 的mysql-connector-java驱动在未显式配置时区时,会尝试用GMT解析TIMESTAMP,导致时区偏移计算错误。
  • 解决:在application.yml的 JDBC URL 中强制指定时区:
    spring: datasource: url: jdbc:mysql://localhost:3306/exam_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true&useSSL=false
    同时,在@Entity的@Column注解中,为TIMESTAMP字段添加columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP",确保数据库层面的默认值生效。

4.3 坑:uniapp App 后台运行时,plus.geolocation.watchPosition无法持续获取定位,学习时长统计中断

  • 现象:用户将 App 切换到后台(锁屏或切换应用),5分钟后再次回到前台,发现“今日学习时长”停止增长,后台定位日志消失。
  • 原因:iOS 和 Android 系统对后台定位有严格限制。iOS 要求在manifest.json中开启backgroundModes: ["location"],且必须在代码中调用plus.geolocation.watchPosition时传入{enableHighAccuracy: true, timeout: 30000};Android 10+ 则要求在AndroidManifest.xml中声明ACCESS_BACKGROUND_LOCATION权限,并在运行时动态申请。
  • 解决:在 App 启动时(App.vue的onLaunch钩子),立即调用:
    // iOS & Android 通用 plus.geolocation.watchPosition( (position) => { // 上报位置到后端,更新学习时长 uni.request({ url: '/api/v1/user/location', method: 'POST', data: position }) }, (e) => { console.error('Watch position error: ' + e.message) }, { enableHighAccuracy: true, timeout: 30000, maximumAge: 60000 } )
    并在manifest.json中确保app-plus.distribute.ios.backgroundModes和app-plus.distribute.android.permissions已正确配置(见 3.1 节)。

4.4 坑:Spring Boot Admin 监控界面中,exam-core模块的 Actuator 端点返回 401 Unauthorized

  • 现象:将spring-boot-admin-starter-client依赖加入exam-core模块后,Spring Boot Admin 服务器能发现该实例,但点击“Metrics”、“Health”等按钮时,返回401 Unauthorized。
  • 原因:exam-core模块启用了Spring Security,其默认配置会拦截所有/actuator/**路径。而 Spring Boot Admin 客户端需要无鉴权访问这些端点才能采集数据。
  • 解决:在exam-core的SecurityConfig.java中,放行 Actuator 端点:
    @Configuration public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authz -> authz .requestMatchers("/actuator/**").permitAll() // 关键:放行所有Actuator端点 .requestMatchers("/api/**").authenticated() .anyRequest().permitAll() ); return http.build(); } }

4.5 坑:uniapp 微信小程序中,uni.previewImage预览题干图片时,长按保存失败或黑屏

  • 现象:用户在小程序中点击题目中的图片,调用uni.previewImage,图片正常显示,但长按菜单中没有“保存到相册”选项,或点击后黑屏。
  • 原因:微信小程序要求预览的图片必须是 HTTPS 协议,且图片域名必须在小程序后台的“业务域名”白名单中。我们曾将题库图片存放在公司内网 Nginx 上(HTTP),或使用了未备案的 CDN 域名。
  • 解决:将所有题干图片、解析图片、图表图片,统一上传至腾讯云 COS(对象存储),并配置好 HTTPS 访问。然后在小程序后台(mp.weixin.qq.com)的“开发管理 -> 开发者工具 -> 业务域名”中,添加 COS 的域名(如https://exam-pic-1250000000.cos.ap-shanghai.myqcloud.com)。确保uni.previewImage的urls数组中所有图片 URL 均为此白名单域名下的 HTTPS 链接。

5. 数据驱动闭环:用 Spring Boot 的 Actuator + uniapp 的埋点 SDK 构建“学-练-测-评-调”全链路可观测性

公考平台的价值,不在于堆砌了多少功能,而在于能否基于真实数据,持续优化每一个环节。一个“刷题-错题-再练-提分”的闭环,必须建立在可量化、可归因、可下钻的数据基石之上。我们摒弃了“看后台日志数PV/UV”的粗放方式,构建了一套贯穿前后端的精细化观测体系:后端用 Spring Boot Actuator 暴露核心指标,前端用自研轻量埋点 SDK 上报用户行为,最终在 Grafana 中绘制出从“用户点击‘开始模考’”到“系统返回薄弱点报告”的全链路耗时热力图。

5.1 后端指标暴露:不只是健康检查,更是业务脉搏

Spring Boot Actuator 默认只暴露/actuator/health和/actuator/info。我们需要它成为业务的“心电图”。在application.yml中启用关键端点:

management: endpoints: web: exposure: include: health,info,metrics,threaddump,prometheus,loggers endpoint: health: show-details: always prometheus: enabled: true metrics: export: prometheus: enabled: true

关键自定义指标(MetricsConfig.java):

@Component public class MetricsConfig { private final MeterRegistry meterRegistry; public MetricsConfig(MeterRegistry meterRegistry) { this.meterRegistry = meterRegistry; } // 1. 模考提交成功率(业务核心KPI) @EventListener public void handleExamSubmitSuccess(ExamSubmitSuccessEvent event) { Counter.builder("exam.submit.success") .tag("exam_type", event.getExamType()) .register(meterRegistry) .increment(); } @EventListener public void handleExamSubmitFailure(ExamSubmitFailureEvent event) { Counter.builder("exam.submit.failure") .tag("exam_type", event.getExamType()) .tag("reason", event.getReason()) .register(meterRegistry) .increment(); } // 2. 知识点掌握度分布(驱动推荐算法) @Scheduled(fixedRate = 300000) // 每5分钟计算一次 public void updateKnowledgeProficiency() { Map<String, Double> proficiencyMap = knowledgeService.calculateProficiencyByUser(); proficiencyMap.forEach((kpCode, proficiency) -> Gauge.builder("knowledge.proficiency", () -> proficiency) .tag("knowledge_point", kpCode) .register(meterRegistry) ); } }

说明:ExamSubmitSuccessEvent和ExamSubmitFailureEvent是我们定义的 Spring 事件,由ExamService.submit()方法在成功/失败后发布。Gauge类型指标knowledge.proficiency实时反映每个知识点的用户平均掌握度(0.0~1.0),这个数值直接喂给 uniapp 前端的“智能推荐”模块,决定下一题推送什么。

5.2 前端埋点:用 200 行 TS 代码实现无侵入、可配置的行为追踪

uniapp 官方uni.reportAPI 功能单一,无法满足我们对“用户在申论题材料上停留时长”、“错题本中某题被反复查看次数”等细粒度需求。我们开发了一个极简的TrackerSDK(utils/tracker.ts),核心逻辑仅200行,通过uni.addInterceptor全局拦截uni.navigateTo、uni.request等关键 API,并允许页面级自定义事件:

// utils/tracker.ts class Tracker { private static instance: Tracker; private config: TrackerConfig; private constructor(config: TrackerConfig) { this.config = config; } static init(config: TrackerConfig) { this.instance = new Tracker(config); this.initInterceptors(); } private initInterceptors() { // 拦截页面跳转,上报页面曝光 uni.addInterceptor('navigateTo', { invoke(args) { Tracker.getInstance().reportPageView(args.url); } }); // 拦截网络请求,上报API调用 uni.addInterceptor('request', { invoke(args) { Tracker.getInstance().reportApiCall(args.url, args.method); } }); } // 页面级自定义事件(在 page.vue 中调用) reportCustomEvent(event: string, props: Record<string, any> = {}) { const payload = { event, page: getCurrentPages()[0]?.$route?.path || '', timestamp: Date.now(), ...props, // 自动附加设备信息 device: uni.getSystemInfoSync(), version: uni.getSystemInfoSync().SDKVersion }; // 发送到后端埋点API uni.request({ url: `${this.config.baseUrl}/api/v1/track`, method: 'POST', data: payload, header: { 'Content-Type': 'application/json' } }); } } // 在 main.js 中全局初始化 Tracker.init({ baseUrl: 'https://api.examplatform.com', debug: process.env.NODE_ENV === 'development' });

说明:Tracker的威力在于“页面级自定义”。例如在pages/exam/answer/answer.vue中,当用户长按某道题的解析区域时:

<template> <view @longpress="onLongPressExplanation"> <text>{{ question.explanation }}</text> </view> </template> <script setup> import { Tracker } from '@/utils/tracker' const onLongPressExplanation = () => { Tracker.getInstance().reportCustomEvent('explanation_long_press', { question_id: question.id, duration_ms: 1500 // 可结合计时器获取精确时长 }) } </script>

这样,运营同学就能在后台看到“哪些题的解析被用户最频繁地长按研究”,从而反向优化解析质量。

5.3 全链路可观测性:从“用户点击”到“系统响应”的毫秒级归因

最终,我们将 Spring Boot 的 Prometheus 指标、uniapp 的埋点日志、Nginx 的访问日志,全部接入 ELK(Elasticsearch, Logstash, Kibana)和 Grafana。一个典型的可观测性看板包含:

指标维度数据来源用途示例阈值
exam.submit.latency.p95Spring Boot Actuator (Prometheus)模考提交接口的95分位耗时> 3000ms 报警
page_view_count{page="/pages/exam/answer"}uniapp 埋点日志 (ELK)“答题页”曝光量,衡量功能入口流量日环比下降 >20%
api_call_error{api="/api/v1/exam/submit"}Nginx access log (ELK)提交接口的 5xx 错误率> 0.5% 报警
knowledge.proficiency{kp_code="SHENLUN-001"}Spring Boot Actuator (Prometheus)“归纳概括”知识点的全站平均掌握度< 0.65 触发教研介入

当某天发现exam.submit.latency.p95突然从 1200ms 升至 4500ms,我们立刻在 Grafana 中下钻:先看api_call_error是否同步飙升(排除网络问题);再看knowledge.proficiency是否有某个知识点(如XINGCE-005)的指标异常波动(怀疑是该知识点的判分规则变更引发性能瓶颈);最后结合 ELK 中的埋点日志,筛选出耗时 > 4s 的exam.submit请求,提取其question_id列表,发现全部集中于“2024年国考行测数量关系最后一题”。至此,问题定位完成:该题的 AI 辅助判分模型在新

本文还有配套的精品资源,点击获取