夜雨聆风学习资料网

ARTICLE · 1358184

JAVA智能题库AI题库系统源码支持小程序+公众号+H5

JAVA智能题库AI题库系统源码支持小程序+公众号+H5

源码交付 · 小程序+公众号+H5

SPRINGBOOT · UNIAPP

刷题 · 错题本 · AI答疑 · 会员

JAVA智能题库系统

做题记录沉淀成错题,AI 接住看不懂的题。

后台 SpringBoot + MyBatis-Plus + MySQL用户端 uni-app · 后台 Vue + ElementUI

题库类产品真正难的地方,从来不在做题页。做完一道题之后,这道题去哪了、下次还推不推、推错了怎么纠、答错了几次能不能归到同一个薄弱点上,这些串不起来,做题就是一次性消耗。

JAVA智能题库AI题库系统把刷题、错题、AI 答疑、会员权益放进同一条数据链路,用户端用 uni-app 一套代码跑小程序、公众号和 H5,管理后台用 Vue + ElementUI 管题、管人、管权益。

下面按用户从进来到续费的实际路径拆开讲,附三张实拍图与可以直接拿去二开的表结构、事务和后台表格代码。

  功能总览:每个模块解决哪个具体卡点

刷题中心

按学科年级筛出可做的题,替代无目的地翻题库

AI 学习中心

把薄弱点转成可执行的练习计划,而不是只给分数

错题本

错题自动归档,按知识点聚合,复习不用手抄

答题页

单题作答与即时判分,交卷即出结果

AI 答疑

卡在解析上时给出步骤级讲解,减少流失

勋章墙

用成就替代倒计时催促,做日常留存的抓手

会员中心

权益与次数写清楚,付费转化的落点

三端一套后台

小程序、公众号、H5 共用接口与权限模型

  功能拆解:从选题到续费的六个环节

把七个模块按用户真实动作排一遍,比按功能列表排更容易看出边界。

① 刷题中心:输入是筛选条件,输出是"今天该做哪几道"

用户进来先选学科(数学/语文/英语/物理/化学)、年级(初一到高三)和题型,系统按已做记录过滤掉最近三天做过的题,返回一组带解析的题目。状态变化只有一种:未做 → 已做。工程上的结论是筛选条件必须在服务端拼,前端只负责把筛选值传上去,否则分页一深就会漏题或重题。

【JAVA智能题库AI题库系统】实拍 01|刷题中心:学科筛选、题量统计与今日推荐;截图数据为演示数据

② 答题页:输入是作答,输出是判分结果与错题标记

单题作答,选完立刻判对错,交卷时统一提交一次。这里最容易出问题的是重复提交:网络抖一下用户连点两次,同一道题写两条记录,错题本里就出现重复。解法是给每次提交生成一个 submit_no,用唯一索引兜住,重复的那条直接忽略。

AI 学习中心放在这条链路的下一跳:把最近十次作答的正确率按知识点聚合,算出薄弱章节,生成当天的练习清单。它不是推荐算法展示,而是一个可执行的任务列表。

③ 错题本:输入是错题记录,输出是按知识点聚合的复习集

每道错题只留一条记录,同一题反复错就累加次数,而不是每次新建一条。用户看到的是"初二数学·人教版·一次函数"这样的分组,每组下面若干道题,点进去可以直接重做。工程结论:错题本本质是一张带唯一键的关联表,聚合逻辑放在查询里做,不要在写入时算。

【JAVA智能题库AI题库系统】实拍 02|答题页与错题本:判分结果、错题分组与重做入口;截图数据为演示数据

④ AI 答疑:输入是题目与用户追问,输出是分步骤讲解

用户卡在解析上时,可以把题干发给 AI 继续问。这里要守住两条:一是题目与解析先从本地题库取,AI 只做讲解不做事实来源;二是同一个用户对同一道题的连续追问要限次,否则成本随会话时长无限增长。实现上用一个会话表记轮次,超过阈值改为提示看解析原文。

⑤ 勋章墙:输入是行为事件,输出是可见的成就

连续打卡、首次满分、错题清零这类事件各发一枚勋章。勋章的价值不在图片,在于它给了用户一个不靠倒计时催促的回来理由。工程上把发勋章做成事件订阅,别写进业务主流程,否则加一枚勋章就要改一次答题事务。

⑥ 会员中心:输入是权益配置,输出是可用次数

免费与 VIP 的差别写在权益表里:每日可做题量、AI 答疑轮次、解析查看权限。用户付款后次数按订单发放,用的时候按次扣减。这块是唯一不能放在前端判断的地方:次数扣减必须在服务端做条件更新,前端展示的剩余次数只是缓存。

【JAVA智能题库AI题库系统】实拍 03|勋章墙与会员中心:成就墙、权益对比与开通入口;截图数据为演示数据

  技术架构:三端各做什么,哪些必须在服务端

用户端(uni-app,Vue 语法)负责交互与展示:题目渲染、选项收集、本地缓存未提交的作答。它不该做的事是判断对错、计算剩余次数、决定能不能看解析,这些一旦放前端,改个抓包就能绕过。

管理后台(Vue + ElementUI)负责题目录入、审核、上下架、用户与权益管理。它不该做的事是直接改用户余额,所有涉及资金的调整都应走一条带审批记录的接口。

服务端(SpringBoot + MyBatis-Plus + MySQL)负责判分、错题归集、次数扣减、支付回调、权限隔离与幂等。判定标准只有一条:凡是改一个数字会影响平台收钱或多退钱的,一律放服务端。

小程序、公众号、H5 三个入口共用同一套接口,差异只在登录方式:小程序走 wx.login 换 openid,公众号走网页授权,H5 走手机号验证码。用户主体在服务端统一成一个 user_id,三端的数据才能互通。

  数据库与核心代码

下面七段代码覆盖表结构、交卷事务、次数扣减、支付幂等、权限隔离、多端请求与后台表格。字段前后一致,事务、乐观扣减、幂等、权限四段之间不互相矛盾。

表结构:题目与答题记录

题目表把选项存成 JSON,答题记录表用 (user_id, paper_id, question_id, submit_no) 建唯一索引,这是后面所有幂等逻辑的地基。

SQL

-- 题库主表:题目与选项分离,选项存 JSONCREATE TABLE t_question (  id BIGINT PRIMARY KEY AUTO_INCREMENT,  category_id BIGINT NOT NULL,  subject VARCHAR(32) NOT NULL,  grade VARCHAR(16) NOT NULL,  stem MEDIUMTEXT NOT NULL,  options JSON NOT NULL,  answer VARCHAR(64) NOT NULL,  analysis MEDIUMTEXT,  difficulty TINYINT DEFAULT 2,  status TINYINT DEFAULT 1,  KEY idx_cat (category_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;-- 答题记录:一次作答一行,唯一索引挡重复提交CREATE TABLE t_answer_record (  id BIGINT PRIMARY KEY AUTO_INCREMENT,  user_id BIGINT NOT NULL,  question_id BIGINT NOT NULL,  paper_id BIGINT NOT NULL,  user_answer VARCHAR(64),  is_right TINYINT DEFAULT 0,  submit_no CHAR(32) NOT NULL,  created_at DATETIME DEFAULT CURRENT_TIMESTAMP,  UNIQUE KEY uk_submit (user_id,paper_id,    question_id, submit_no),  KEY idx_user (user_id,created_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

交卷事务:判分、归集错题、扣次数一次完成

一次交卷要么全成要么全不成。唯一索引冲突不抛给前端,因为那通常只是重复点击,用户看到的应该是同一份结果。

JAVA

// 交卷:判分 + 写错题本 + 扣会员次数,同一事务@Transactional(rollbackFor = Exception.class)public SubmitVO submit(SubmitDTO dto, Long uid) {    String no = dto.getSubmitNo();    // 幂等:同一 submitNo 重复提交直接返回旧结果    AnswerRecord exist = recordMapper.selectOne(        Wrappers.<AnswerRecord>lambdaQuery()            .eq(AnswerRecord::getSubmitNo, no)            .eq(AnswerRecord::getUserId, uid).last("limit 1"));    if (exist != null) return SubmitVO.replay(exist);    List<Long> qids = dto.getQuestionIds();    List<Question> qs = questionMapper.selectBatchIds(qids);    int right = 0;    for (Question q : qs) {        String ua = dto.getAnswers().get(q.getId());        boolean ok = checkAnswer(ua, q.getAnswer());        if (ok) right++;        AnswerRecord r = new AnswerRecord();        r.setUserId(uid); r.setPaperId(dto.getPaperId());        r.setQuestionId(q.getId()); r.setUserAnswer(ua);        r.setSubmitNo(no); r.setIsRight(ok ? 1 : 0);        try { recordMapper.insert(r); }        catch (DuplicateKeyException e) {            // 唯一索引兜住并发重复,不抛给前端            right -= ok ? 1 : 0;        }        if (!ok) wrongBookMapper.upsert(uid, q.getId());    }    memberService.consume(uid, qs.size());    return SubmitVO.of(right, qs.size(), no);}

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

次数扣减:条件更新,不先查后改

先 select 余额再 update,两个请求同时进来就会超扣。把判断写进 where 条件,影响行数为 0 才知道没扣到。

JAVA

// 会员剩余次数扣减:条件 UPDATE + 重试,不先查后改public boolean consume(Long uid, int need) {    for (int i = 0; i < 3; i++) {        int ok = memberMapper.update(null,            Wrappers.<Member>lambdaUpdate()                .setSql("remain = remain - " + need)                .eq(Member::getUserId, uid)                .eq(Member::getStatus, 1)                .ge(Member::getRemain, need));        if (ok > 0) return true;        // 影响行数为 0:余额不足或已被其它请求扣走        Member m = memberMapper.selectById(uid);        if (m == null || m.getRemain() < need) {            throw new BizException("QUOTA_NOT_ENOUGH");        }    }    throw new BizException("CONCURRENT_RETRY_EXCEEDED");}

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

支付回调:幂等键与状态 CAS

支付平台的通知会重复发。用订单号加交易号做唯一记录,重复通知直接返回;再用状态机做一次 CAS,保证权益只加一次。

JAVA

// 会员支付回调:唯一索引 + 状态 CAS,防止重复到账public void onPayNotify(PayNotify n) {    if (!signService.verify(n)) return;    // 幂等键:订单号 + 交易号,重复通知直接跳过    int inserted = payLogMapper.insertIgnore(        n.getOrderNo(), n.getTradeNo(), n.getAmount());    if (inserted == 0) {        log.info("replay {} {}", n.getOrderNo(), n.getTradeNo());        return;    }    int cas = orderMapper.update(null,        Wrappers.<PayOrder>lambdaUpdate()            .set(PayOrder::getStatus, PAID)            .eq(PayOrder::getOrderNo, n.getOrderNo())            .eq(PayOrder::getStatus, UNPAID));    if (cas == 0) return; // 已处理过,不重复加时长    memberService.grantQuota(n.getUserId(),        n.getQuota(), n.getOrderNo());}

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

权限隔离:注解 + AOP + 条件构造器

学校、年级、学科这些维度不该在每个查询里手写。用注解标在接口上,AOP 把范围塞进 ThreadLocal,Mapper 侧统一拼进条件。

JAVA

// 多租户/角色隔离:注解 + AOP,把条件塞进构造器@Target(ElementType.METHOD) @Retention(RUNTIME)public @interface RequireScope {    String value() default ""; // subject:grade}@Around("execution(* com.edu..*Controller.*(..))")public Object scope(ProceedingJoinPoint pjp) throws Throwable {    RequireScope a = resolve(pjp);    if (a == null) return pjp.proceed();    ScopeHolder.set(SessionUtil.currentScope());    try { return pjp.proceed(); }    finally { ScopeHolder.clear(); }}// Mapper 侧统一包一层,业务代码不用自己记得拼public static <T> LambdaQueryWrapper<T> scoped(        LambdaQueryWrapper<T> w) {    Scope s = ScopeHolder.get();    if (s == null || s.isAll()) return w;    return w.eq(T::getGrade, s.getGrade())            .eq(T::getSubject, s.getSubject());}

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

多端请求:一份封装带幂等键

三端共用一份请求封装,提交类接口自动带上本次操作号,服务端据此去重。401 统一跳登录,业务错误码统一抛,页面里不再各写一套判断。

JAVASCRIPT

// uniapp 请求封装:统一带幂等键,三端共用一份export function request({ url, method = 'GET',  data = {}, idem = true }) {  const headers = {    'Content-Type': 'application/json',    'X-Token': uni.getStorageSync('token') || '',  };  if (idem && method !== 'GET') {    // 提交类接口带上本次操作号,服务端据此去重    headers['X-Idem-Key'] = uuid();  }  return new Promise((resolve, reject) => {    uni.request({      url: BASE + url, method, data, header: headers,      success: (res) => {        if (res.statusCode === 401) return toLogin();        if (res.data.code !== 0) return reject(res.data);        resolve(res.data.data);      },      fail: (err) => reject(err),    });  });}

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

后台表格:分页、状态标签、审核操作

ElementUI 的表格写法,状态用 tag 表达,操作列 fixed 右侧。分页交给 el-pagination,不要自己在表格里算偏移。

VUE

<!-- 后台错题审核列表:分页 + 状态标签 + 操作 --><el-table :data="rows" v-loading="loading" stripe>  <el-table-column prop="stem" label="题干"    min-width="220" show-overflow-tooltip />  <el-table-column prop="subject" label="学科"    width="90" />  <el-table-column prop="grade" label="年级"    width="90" />  <el-table-column label="状态" width="110">    <template #default="{ row }">      <el-tag :type="tagOf(row.status)" size="small">        {{ statusText(row.status) }}      </el-tag>    </template>  </el-table-column>  <el-table-column label="操作" width="150" fixed="right">    <template #default="{ row }">      <el-button type="primary" link        @click="openAudit(row)">审核</el-button>      <el-button type="danger" link        @click="offline(row)">下架</el-button>    </template>  </el-table-column></el-table><el-pagination background layout="total, sizes, prev,  pager, next" :total="total" :page-size="size"  @size-change="onSize" @current-change="onPage" />

示例实现,生产环境需补充参数校验、权限、日志、监控、限流、支付验签等。

  合规与上线检查

题库产品天然会接触到未成年人,这一条要在上线前过一遍:注册协议里写清监护人同意路径,未成年人模式下限制付费入口与连续使用时长,AI 答疑的输出不许出现与学习无关的内容。

个人信息方面,收集手机号、学习记录、错题内容都要有对应目的说明,不收集与教学无关的设备信息,导出与删除入口要真的可用。

资金与权益方面,会员购买页要把有效期、可退规则、次数限制写在页面上,支付回调必须验签,对账记录至少留六个月。

内容方面,题目与解析属于平台自建内容,AI 生成部分要留审核与申诉入口;若接入第三方题库,注意授权范围,不要把别人的题当自有资产宣传。

  适合谁

一是想基于成熟后台做二次开发的团队:表结构、事务边界、权限模型都是现成的,换成自己的题库和视觉即可。

二是教培机构与学校的信息化负责人:要的不是一个大而全的平台,而是能把线下练习迁到线上、并且能看到错题分布的工具。

三是做知识付费的个体:需要一套能收会员费、能限量、能看数据的轻量系统。

  常见问题

题库要自己录吗?

后台支持单题录入与批量导入,导入模板按学科年级分类。系统本身不带题库内容,题目版权归录入方。

AI 答疑必须接大模型吗?

答疑模块是独立服务,接哪家由你定,也可以先关掉只用本地解析。关掉之后其余功能不受影响。

三端要维护三套代码吗?

用户端是 uni-app 一套代码,编译产物分别投小程序、公众号 H5 和普通 H5。差异只在登录那一层。

会员次数能不能改成不限量?

可以在权益表里把每日上限设为 0 表示不限,扣减逻辑会自动跳过。

  结语

如果你正在做在线教育或题库类产品,或想基于 SpringBoot + MyBatis-Plus + uni-app 做一套可二开的题库系统,欢迎找我领取演示资料和部署说明。

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料