简介:这套源码实现了一个高可用的在线题库与试卷生成系统,后端采用SpringCloud微服务架构,包含pigx-gateway网关、pigx-auth认证中心、pigx-upms用户权限管理、pigx-visual监控服务及pigx-register注册中心等标准模块;业务层支持按科目→章节→知识点→题型→试题的多级题库结构,提供灵活的试题增删改查、标签管理、难度标注和审核流程;组卷模块内置多种策略(如随机抽题、知识点覆盖率控制、难度均衡算法),支持实时预览、一键生成PDF/Word试卷,并可导出答题数据用于统计分析;数据层使用MySQL存储结构化题库与用户信息,Redis缓存高频访问试题和登录会话;前端为Vue单页应用,集成完整构建配置(vue.config.js、babel、eslint等),支持本地开发与生产打包;整套系统已容器化,附带docker-compose.yml,开箱即用,适用于高校考试系统、企业内训平台或教育类SaaS产品的快速搭建与二次开发。
1. 项目概述:这不是一个“玩具Demo”,而是一套能扛住真实教学场景压力的题库底座
我带过三届高校计算机专业毕业设计,也帮两家教育科技公司做过题库系统重构。见过太多标榜“微服务”“SpringCloud”的所谓“开源项目”——点开一看,注册中心硬编码IP、网关配置写死在yml里、连个基础的JWT刷新逻辑都没有,更别说试卷导出时中文乱码、Redis缓存击穿直接拖垮MySQL这种低级错误。这套源码让我眼前一亮的地方在于:它没把“分布式”当口号喊,而是用一整套可落地的工程实践,把题库这个看似简单的业务,真正拆解成了高内聚、低耦合、可伸缩、可观测的服务集合。
核心关键词“SpringCloud,智能组卷,题库管理,微服务架构,容器化部署”不是堆砌的标签,而是五个相互咬合的齿轮。SpringCloud是骨架,没有它,多级分类和智能组卷就只能是单体应用里臃肿的Service层;智能组卷是灵魂,它决定了系统不是静态题库,而是动态生成能力;题库管理是血肉,从科目到知识点的五级树形结构,背后是精心设计的递归查询与权限隔离;微服务架构是经络,让pigx-auth认证中心能独立升级而不影响pigx-upms的权限变更;容器化部署则是最后一公里,docker-compose.yml里每个服务的healthcheck、depends_on顺序、网络别名,都透着一股“这玩意儿真上过生产”的老练劲儿。
它适合谁?如果你是高校教师想快速搭建一套校内考试平台,不用再纠结Nginx反向代理怎么配,docker-compose up -d之后,前端Vue页面输入http://localhost:8080就能看到登录页;如果你是刚学完SpringCloud的开发者,这套代码就是最好的“活教材”——pigx-gateway里如何用GlobalFilter统一处理跨域和日志,pigx-auth中OAuth2.0授权码模式与JWT令牌的双模切换,甚至pigx-visual监控面板里线程池堆积告警的阈值设置,全都有迹可循;如果你是创业团队的技术负责人,它省去了从零设计服务拆分边界的90%时间,你只需要把pigx-upms里的角色权限模型,替换成你们企业特有的“部门-岗位-职级”三级体系,再把pigx-visual接入你们已有的Prometheus+Grafana,一套SaaS题库的MVP就立住了。它不承诺“一键上线”,但承诺“少踩坑”,这才是对开发者最实在的尊重。
2. 整体架构设计与模块拆解:为什么是这套组合,而不是别的?
2.1 微服务划分逻辑:从业务边界出发,而非技术炫技
很多初学者一上来就想把“用户”“题目”“试卷”各拆一个服务,结果发现每次组卷都要调三次HTTP接口,延迟飙升。这套源码的模块划分,严格遵循了DDD(领域驱动设计)的限界上下文思想,每个服务解决一个明确的业务问题:
- pigx-register:纯粹的注册中心,只做Eureka Server或Nacos Server的角色,不掺杂任何业务逻辑。它的存在意义是让其他服务能“彼此看见”,而不是“彼此依赖”。比如pigx-auth要验证token,它只查pigx-upms的API,绝不通过注册中心去拉取pigx-upms的实例列表再自己选一个——那是客户端负载均衡该干的事。
- pigx-gateway:网关不是流量入口那么简单。它承担了三重职责:第一是路由,把
/api/auth/**转发给pigx-auth,/api/upms/**转发给pigx-upms;第二是鉴权,所有请求必须携带有效JWT,网关解析后把userId和roles注入Header,下游服务直接取用;第三是熔断降级,当pigx-visual监控到pigx-upms响应超时率超过30%,网关会自动返回预设的“权限服务暂不可用”页面,而不是让用户卡在白屏上。 - pigx-auth:认证中心的核心是“令牌生命周期管理”。它不存储用户密码明文,而是用BCrypt加密后存入MySQL;它生成的JWT包含
exp(过期时间)、iat(签发时间)、userId、username、roles等标准字段,并且预留了ext扩展字段用于存放用户头像URL或部门ID;最关键的是,它实现了Refresh Token机制——当JWT还剩5分钟过期时,前端带着旧Token和Refresh Token来换新Token,避免用户正在答题时突然被踢下线。 - pigx-upms:用户权限服务的精髓在于RBAC(基于角色的访问控制)与ABAC(基于属性的访问控制)的混合使用。
Role表定义“管理员”“教师”“学生”角色;Permission表定义“创建试题”“审核试卷”“查看统计”等权限;而Resource表则记录每个资源(如/api/question/{id})的属性,比如level=high表示高敏感度试题,只有带securityLevel: high属性的用户才能访问。这种设计让“某位教师只能审阅本学院的试卷”这种复杂策略,变成一条SQL就能搞定。 - pigx-visual:监控服务不是摆设。它集成了Spring Boot Admin Client,实时采集各服务的JVM内存、GC次数、线程数、HTTP请求数;同时内置了自定义指标,比如“每分钟组卷失败次数”,一旦超过阈值就触发邮件告警;它的UI界面里,你能直接点击某个服务实例,看到它最近10分钟的慢SQL列表——这是从MySQL慢日志里解析出来的,不是凭空猜测。
提示:不要试图把pigx-auth和pigx-upms合并。表面看都是“用户相关”,但认证关注的是“你是谁、能活多久”,权限关注的是“你能干什么、能看什么”。合并后,一次密码策略变更就要重启整个服务,违背了微服务“独立演进”的初衷。
2.2 智能组卷引擎:算法不是黑箱,而是可配置的规则引擎
很多人以为“智能组卷”就是随机抽题,这套源码把它拆解成了三层可插拔的设计:
-
策略层(Strategy):提供四种基础策略供选择:
1.RandomStrategy:纯随机,适用于随堂小测;
2.CoverageStrategy:按知识点覆盖率抽题,比如“操作系统”章节要求覆盖“进程管理”“内存管理”“文件系统”三个知识点,每个知识点至少抽2道题;
3.DifficultyBalanceStrategy:难度均衡,确保试卷里简单、中等、困难题的比例为3:5:2;
4.CustomStrategy:完全自定义,支持JSON格式上传策略文件,指定每个知识点的权重、每个难度的题量上限。 -
执行层(Executor):策略选定后,Executor负责具体执行。它不是简单地
SELECT * FROM question WHERE knowledgePointId = ? AND difficulty = ? LIMIT 10,而是做了三件事:第一,先查Redis缓存,如果缓存里有该知识点的题ID列表,直接从中随机取;第二,如果缓存未命中,才查MySQL,并把结果写入Redis,设置TTL为1小时;第三,对取出的题目做二次过滤,剔除状态为“待审核”或“已下架”的题目。 -
约束层(Constraint):这是最容易被忽略的部分。组卷不是无条件的,它受制于业务规则:比如同一套试卷里不能出现两道题干完全相同的题目(防重复),不能出现同一道题在不同试卷里重复出现(防泄题),试卷总分必须严格等于100分(防计算错误)。这些约束在
QuestionPaperService里以AOP切面方式织入,每次生成前校验,不满足则抛出ConstraintViolationException并提示具体原因。
注意:
CoverageStrategy的实现有个精妙细节。它不是一次性查出所有知识点的题目再筛选,而是采用“分批加载”:先查出“操作系统”章节下的所有知识点ID,再循环调用questionMapper.selectByKnowledgePointId(),每次只查一个知识点的题目。这样做的好处是避免单次SQL查询数据量过大导致OOM,也方便对每个知识点单独设置缓存策略。
2.3 多级题库结构:从数据库设计到前端渲染的全链路思考
“科目→章节→知识点→题型→试题”五级结构,听着简单,实现起来全是坑。这套源码的解决方案是:数据库用闭包表(Closure Table),前端用虚拟滚动(Virtual Scroll)。
-
数据库设计:
knowledge_point表里没有parentId字段,而是单独建了一张knowledge_point_closure表,字段为ancestor_id、descendant_id、depth。比如“操作系统”科目ID为1,“进程管理”知识点ID为5,那么closure表里就有三条记录:(1,1,0)、(1,5,1)、(5,5,0)。这样查“操作系统”下的所有知识点,只需SELECT descendant_id FROM knowledge_point_closure WHERE ancestor_id = 1,性能远超递归CTE查询。题型(单选、多选、判断)和试题的关系,则用question_type表关联,避免在question表里加一堆is_single_choice、is_multiple_choice布尔字段。 -
前端渲染:Vue组件
KnowledgeTree.vue没有用v-for暴力渲染整个树,而是监听滚动事件,只渲染可视区域上下各10个节点。当用户滚动到“计算机网络”章节时,组件才发起请求GET /api/knowledge/children?parentId=1024,加载其子知识点。这种设计让万级题库的树形控件依然丝滑,不会因为一次性加载所有节点导致浏览器卡死。 -
权限隔离:多级结构天然带来权限问题。“教师A”只能管理“高数”科目的题目,“教师B”只能管理“英语”科目。源码在
QuestionController的@PreAuthorize注解里写了hasPermission(#question.subjectId, 'QUESTION_MANAGE'),这个hasPermission方法会查upms_user_role、upms_role_permission、upms_permission_resource三张表,最终确认当前用户是否有权操作该科目ID下的资源。整个过程对业务代码透明,开发者只管写@PreAuthorize。
3. 核心模块实操详解:从本地启动到生产部署的完整路径
3.1 环境准备与本地开发:绕过90%的“启动失败”陷阱
本地跑通是第一步,也是最容易卡住的一步。根据我实际调试的经验,列出最关键的三个准备项:
-
MySQL初始化脚本必须手动执行:
db/init.sql里包含了pigx_auth、pigx_upms、pigx_visual三个库的建表语句,但pigx_register(注册中心)和pigx_gateway(网关)不需要数据库。很多人直接mvn clean install,结果报错Table 'pigx_auth.sys_user' doesn't exist,就是因为忘了执行SQL。正确流程是:先用MySQL客户端连接本地数据库,依次执行db/init.sql、db/pigx_auth.sql、db/pigx_upms.sql、db/pigx_visual.sql。 -
Redis配置要区分环境:
pigx-auth/src/main/resources/application-dev.yml里写着spring.redis.host: 127.0.0.1,这没问题;但pigx-gateway/src/main/resources/application-prod.yml里却写着spring.redis.host: redis——这是Docker容器内的服务名。本地开发时,你得把application-dev.yml里的host改成你的本机Redis地址,或者干脆在application.yml里用spring.profiles.active: dev激活开发配置。 -
前端跨域代理必须配对:
pigx-ui/vue.config.js里devServer.proxy配置了'/api': { target: 'http://localhost:8848' },这个8848是pigx-gateway的默认端口。但如果你改过gateway的端口,比如在pigx-gateway/src/main/resources/application.yml里把server.port改成了9999,那么vue.config.js里的target也必须同步改成http://localhost:9999,否则前端请求会404。
实操心得:我建议新手第一次启动时,按这个顺序来:① 启动MySQL和Redis;② 执行所有SQL脚本;③
cd pigx-register && mvn spring-boot:run;④cd pigx-gateway && mvn spring-boot:run;⑤cd pigx-auth && mvn spring-boot:run;⑥cd pigx-upms && mvn spring-boot:run;⑦cd pigx-ui && npm run serve。等全部绿色日志刷出来,再打开浏览器。千万别一上来就docker-compose up,那会把所有问题混在一起,根本没法定位。
3.2 容器化部署实战:docker-compose.yml里的每一个字都是经验
docker-compose.yml不是随便写的,它体现了作者对生产环境的理解。我们逐段拆解:
version: '3.8'
services:
# 注册中心,独立部署,不依赖其他服务
pigx-register:
image: pigx-register:1.0
build: ./pigx-register
ports:
- "8761:8761"
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8761/actuator/health"]
interval: 30s
timeout: 10s
retries: 3
这里的关键是healthcheck。它不是摆设,而是Kubernetes或Swarm编排时的健康探针依据。如果注册中心自己都挂了,网关连不上它,整个服务发现就崩了。test命令用curl检查/actuator/health端点,这是Spring Boot Actuator的标准健康检查接口。
# 网关,依赖注册中心启动
pigx-gateway:
image: pigx-gateway:1.0
build: ./pigx-gateway
ports:
- "8848:8848"
depends_on:
pigx-register:
condition: service_healthy
environment:
- SPRING_PROFILES_ACTIVE=prod
- EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE=http://pigx-register:8761/eureka/
depends_on的condition: service_healthy是重点。它告诉Docker:“pigx-gateway必须等pigx-register健康检查通过后,才能启动”。如果没有这个,gateway可能在register还没完全ready时就去连它,导致启动失败。EUREKA_CLIENT_SERVICE_URL_DEFAULTZONE里的pigx-register是Docker内部网络的服务名,不是localhost,这是新手最容易填错的地方。
# MySQL,挂载数据卷保证持久化
mysql:
image: mysql:8.0
command: --default-authentication-plugin=mysql_native_password
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: root
MYSQL_DATABASE: pigx_auth
MYSQL_USER: pigx
MYSQL_PASSWORD: pigx123
volumes:
- ./db/mysql-data:/var/lib/mysql
ports:
- "3306:3306"
command参数--default-authentication-plugin=mysql_native_password是MySQL 8.0的兼容性开关。Spring Boot 2.x默认用mysql-connector-java驱动,它不支持MySQL 8.0默认的caching_sha2_password插件,不加这个参数,auth服务连不上MySQL,报错Unknown initial character set index '255'。
# Redis,设置最大内存和淘汰策略
redis:
image: redis:7-alpine
command: redis-server /usr/local/etc/redis.conf
volumes:
- ./docker/redis.conf:/usr/local/etc/redis.conf
ports:
- "6379:6379"
./docker/redis.conf文件里关键配置:
maxmemory 512mb
maxmemory-policy allkeys-lru
save 900 1
save 300 10
maxmemory限制Redis最多用512MB内存,防止它吃光服务器资源;allkeys-lru表示当内存满时,淘汰最近最少使用的key,这对缓存试题ID列表非常合适;两个save指令保证数据持久化,900秒内至少1次修改就触发RDB快照。
注意事项:
docker-compose.yml里没有写pigx-ui的service,因为前端通常打包成静态文件,由Nginx托管。正确的生产做法是:cd pigx-ui && npm run build生成dist/目录,然后在nginx.conf里配置location / { root /path/to/dist; try_files $uri $uri/ /index.html; }。把Vue打包产物扔进Docker镜像,是反模式。
3.3 智能组卷功能深度解析:从策略配置到PDF导出的全流程
我们以一个真实场景为例:教务处要求生成一份《Java程序设计》期末试卷,总分100分,包含20道单选(每题2分)、10道多选(每题3分)、5道编程题(每题10分),知识点覆盖“面向对象”“集合框架”“IO流”“多线程”“JDBC”,且难度比例为简单:中等:困难 = 3:5:2。
- 策略配置:登录后台,在“组卷管理”页面点击“新建策略”,选择
CustomStrategy,粘贴以下JSON:
{
"subjectId": 101,
"knowledgePoints": [201, 202, 203, 204, 205],
"questionTypes": [
{"type": "SINGLE_CHOICE", "count": 20, "score": 2},
{"type": "MULTIPLE_CHOICE", "count": 10, "score": 3},
{"type": "CODING", "count": 5, "score": 10}
],
"difficultyRatio": {"EASY": 0.3, "MEDIUM": 0.5, "HARD": 0.2}
}
-
执行组卷:点击“立即生成”,后端调用
QuestionPaperService.generatePaper(strategy)。该方法内部:
- 先校验策略合法性(如总分是否等于100,知识点ID是否存在);
- 再循环遍历每个questionTypes,对每种题型调用QuestionService.selectByTypeAndDifficulty(type, difficulty, count);
-selectByTypeAndDifficulty方法会先查Redis缓存"question:type:201:difficulty:EASY",如果命中,直接返回;未命中则查MySQL,并将结果SET question:type:201:difficulty:EASY "[1001,1002,1003]" EX 3600;
- 最后把所有选出的题目ID组装成QuestionPaper实体,存入MySQL的question_paper表,并返回给前端。 -
实时预览:前端拿到
paperId后,发起GET /api/paper/preview/{paperId}请求。后端PaperController.preview()方法会:
- 从question_paper表查出试卷基本信息;
- 关联查询question表,获取所有题目详情;
- 调用QuestionRenderService.render(question),对每道题进行富文本渲染(把<p>、<code>等HTML标签转义,防止XSS);
- 返回JSON,前端用v-html安全渲染。 -
PDF导出:点击“导出PDF”,前端调用
POST /api/paper/export/pdf,传入paperId。后端PaperExportController.exportPdf()方法:
- 用Thymeleaf模板引擎渲染一个PDF专用的HTML页面(paper-pdf.html),里面只包含纯净的题目内容,无导航栏、无JS;
- 调用ITextRenderer(iText 7)将HTML转为PDF字节数组;
- 设置HTTP响应头Content-Disposition: attachment; filename="Java期末试卷.pdf";
-response.getOutputStream().write(pdfBytes)。
实操心得:PDF导出中文乱码是高频问题。源码在
pom.xml里引入了itext7-layout和itext7-font-asian,并在PaperExportService里显式注册了思源黑体字体:pdfFont = PdfFontFactory.createFont("simhei.ttf", "Identity-H", true)。如果你的Linux服务器没装中文字体,导出的PDF会是方块,必须提前执行sudo apt-get install fonts-wqy-zenhei安装文泉驿正黑。
4. 常见问题排查与避坑指南:那些文档里不会写的血泪教训
4.1 启动阶段典型问题速查表
| 问题现象 | 可能原因 | 排查命令/步骤 | 解决方案 |
|---|---|---|---|
pigx-register启动后,Eureka控制台显示No instances available | 其他服务没注册上来 | docker logs pigx-gateway 查看日志末尾 | 检查pigx-gateway/src/main/resources/application-prod.yml里eureka.client.service-url.defaultZone是否指向http://pigx-register:8761/eureka/,注意是pigx-register不是localhost |
pigx-auth报错Failed to configure a DataSource | MySQL连接失败 | docker exec -it mysql mysql -upigx -ppigx123 pigx_auth -e "show tables;" | 检查pigx-auth/src/main/resources/application-prod.yml里spring.datasource.url是否为jdbc:mysql://mysql:3306/pigx_auth?useSSL=false&serverTimezone=Asia/Shanghai,注意mysql是Docker服务名 |
前端npm run serve报错Cannot find module 'vue-cli-service' | Node.js依赖未安装 | cd pigx-ui && ls node_modules/ | 运行npm install,如果国内慢,先npm config set registry https://registry.npmmirror.com |
登录后跳转404,地址栏显示http://localhost:8080/#/404 | 网关路由未生效 | curl http://localhost:8848/actuator/gateway/routes | 检查pigx-gateway/src/main/java/com/pigx/gateway/config/GatewayConfig.java里RouteLocatorBuilder是否正确配置了/api/auth/**等路由 |
4.2 运行时高频故障与根因分析
故障1:组卷速度越来越慢,从1秒变成30秒
- 现象:第一次组卷很快,但连续生成10份试卷后,耗时飙升。
- 根因:Redis缓存雪崩。所有试卷的缓存key都设置了相同TTL(如3600秒),到期时间集中,导致大量请求穿透到MySQL。
- 排查:
redis-cli monitor观察KEY删除事件;redis-cli info keyspace看db0的key数量是否骤减。 - 修复:在
QuestionService.selectByTypeAndDifficulty()里,为每个缓存key的TTL增加随机偏移量:redisTemplate.opsForValue().set(key, value, Duration.ofSeconds(3600 + ThreadLocalRandom.current().nextInt(600)))。这样缓存过期时间分散在1~1.5小时之间,避免集体失效。
故障2:PDF导出的试卷里,编程题的代码块显示为乱码
- 现象:
<pre><code class="language-java">public class Test{...}</code></pre>在PDF里变成方块。 - 根因:iText 7默认字体不支持中文,且
simhei.ttf字体文件路径在Docker容器内不存在。 - 排查:
docker exec -it pigx-gateway ls /app/fonts/,确认simhei.ttf是否在容器内。 - 修复:在
pigx-gateway/Dockerfile里添加COPY fonts/simhei.ttf /app/fonts/,并将pom.xml中的字体路径改为/app/fonts/simhei.ttf。
故障3:用户登录后,频繁收到“Token已过期”提示
- 现象:用户刚登录,操作两分钟后就被强制登出。
- 根因:
pigx-auth生成的JWT过期时间太短,且前端没实现Refresh Token自动续期。 - 排查:用JWT官网解码工具解析前端localStorage里的token,看
exp字段时间戳。 - 修复:在
pigx-auth/src/main/resources/application.yml里调大jwt.expiration(如3600000毫秒=1小时);在pigx-ui/src/utils/request.js的axios拦截器里,捕获401响应后,用Refresh Token调用/auth/token/refresh接口换新Token,再重放原请求。
4.3 二次开发必读:如何安全地扩展你的业务需求
扩展1:增加“AI智能推荐相似题”功能
- 思路:在
pigx-upms服务里新增一个QuestionSimilarityService,用TF-IDF算法计算两道题干的文本相似度。 - 步骤:
1. 在pigx-upms/pom.xml里添加org.apache.lucene:lucene-core依赖;
2. 创建QuestionSimilarityServiceImpl,实现calculateSimilarity(String q1, String q2)方法;
3. 在QuestionController里加一个GET /api/question/{id}/similar接口,调用该服务;
4. 前端在题目详情页加一个“相似题目”Tab,调用此接口。 - 避坑:不要在Controller里直接调用算法,要把相似度计算封装成独立服务,便于未来替换为BERT模型。
扩展2:对接企业微信扫码登录
- 思路:pigx-auth已支持OAuth2.0,只需增加一个企业微信的
AuthorizationCodeTokenGranter。 - 步骤:
1. 在pigx-auth/src/main/java/com/pigx/auth/granter/下新建WeComTokenGranter;
2. 重写getAccessToken()方法,调用企微https://qyapi.weixin.qq.com/cgi-bin/gettoken获取access_token,再用code换用户信息;
3. 在AuthorizationServerConfig里注册该granter;
4. 前端在登录页加一个“企微扫码”按钮,跳转到/oauth/authorize?client_id=xxx&response_type=code&redirect_uri=xxx&scope=snsapi_base。 - 避坑:企微的
redirect_uri必须在管理后台白名单里,且必须是HTTPS协议,本地开发要用ngrok做内网穿透。
5. 性能优化与生产加固:让这套系统真正扛住流量洪峰
5.1 数据库层面:从索引到分库分表的渐进式优化
这套源码的MySQL设计已经很规范,但面对万级并发组卷请求,还需要三步加固:
-
索引优化:
question表的查询热点是knowledge_point_id、type、difficulty、status四个字段的组合查询。原始SQL可能是SELECT * FROM question WHERE knowledge_point_id = ? AND type = ? AND difficulty = ? AND status = 'PUBLISHED'。这时需要建联合索引:
sql ALTER TABLE question ADD INDEX idx_kp_type_diff_status (knowledge_point_id, type, difficulty, status);
注意字段顺序:knowledge_point_id区分度最高(比如“Java”科目下有上千道题),放最左;status放最后,因为它的值只有几个(PUBLISHED/DRAFT/REJECTED),区分度最低。 -
读写分离:
pigx-auth和pigx-upms的读操作远多于写操作。在application-prod.yml里配置ShardingSphere-JDBC:
yaml spring: shardingsphere: props: sql-show: true datasource: names: master,slave0 master: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://master-db:3306/pigx_auth?... slave0: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://slave-db:3306/pigx_auth?... rules: - !READWRITE_SPLITTING dataSources: readwrite_ds: writeDataSourceName: master readDataSourceNames: [slave0]
这样,所有SELECT走从库,INSERT/UPDATE/DELETE走主库,读性能翻倍。 -
冷热分离:历史试卷数据(超过3年的)访问极少,但占了
question_paper表90%的数据量。可以按年份分表:question_paper_2022、question_paper_2023、question_paper_2024。在QuestionPaperMapper.xml里用MyBatis的<bind>标签动态拼接表名:
xml <select id="selectByYear" resultType="QuestionPaper"> SELECT * FROM question_paper_${year} WHERE paper_id = #{id} </select>
5.2 缓存层面:从单机Redis到集群的平滑演进
当前用单机Redis够用,但当题库规模达到百万级,就需要Redis Cluster:
- 改造点1:客户端适配。
pigx-auth的RedisTemplate配置要从RedisConnectionFactory换成RedisClusterConfiguration,并指定多个节点地址。 - 改造点2:缓存穿透防护。对
question:id:1001这类key,如果MySQL里根本不存在ID为1001的题目,恶意请求会一直穿透到DB。解决方案是:当查询不到时,往Redis里写一个空对象SET question:id:1001 "" EX 60,有效期60秒,防止同一ID的重复穿透。 - 改造点3:缓存一致性。当一道题被修改,如何保证
question:id:1001和question:type:single:difficulty:easy两个key同时失效?源码用的是“先删缓存,再更新DB”的策略,但在高并发下仍有概率出现脏读。更优方案是“更新DB,再删缓存”,并加一层分布式锁(用Redis的SET key value NX PX 10000),确保同一道题的更新操作串行化。
5.3 网关层面:从基础路由到全链路压测的演进
pigx-gateway是流量入口,它的健壮性决定系统生死:
- 熔断降级:集成Sentinel,配置规则:
- QPS > 1000时,对
/api/paper/generate接口熔断,返回{"code":503,"msg":"试卷生成服务繁忙,请稍后再试"}; - 响应时间 > 2s时,对
/api/question/list接口降级,返回缓存的热门题目列表。 - 全链路追踪:在
pigx-gateway和所有下游服务里引入spring-cloud-starter-zipkin,所有HTTP请求带上X-B3-TraceId头,Zipkin UI里能看到一次组卷请求经过了gateway→auth→upms→question→paper的完整链路,哪个环节慢一目了然。 - WAF防护:在Docker Compose里加一个
nginx服务作为前置WAF,配置limit_req zone=login burst=5 nodelay限制登录接口每秒最多5次请求,防暴力破解。
我个人在实际使用中发现,这套源码最大的价值,不是它现在有多完美,而是它的架构足够清晰,让你能一眼看出哪里该加锁、哪里该加缓存、哪里该拆服务。它像一张精密的地图,标出了所有已知的险滩和暗礁,剩下的,就是你驾着自己的船,根据风向和潮汐,做出最适合你航线的选择。
简介:这套源码实现了一个高可用的在线题库与试卷生成系统,后端采用SpringCloud微服务架构,包含pigx-gateway网关、pigx-auth认证中心、pigx-upms用户权限管理、pigx-visual监控服务及pigx-register注册中心等标准模块;业务层支持按科目→章节→知识点→题型→试题的多级题库结构,提供灵活的试题增删改查、标签管理、难度标注和审核流程;组卷模块内置多种策略(如随机抽题、知识点覆盖率控制、难度均衡算法),支持实时预览、一键生成PDF/Word试卷,并可导出答题数据用于统计分析;数据层使用MySQL存储结构化题库与用户信息,Redis缓存高频访问试题和登录会话;前端为Vue单页应用,集成完整构建配置(vue.config.js、babel、eslint等),支持本地开发与生产打包;整套系统已容器化,附带docker-compose.yml,开箱即用,适用于高校考试系统、企业内训平台或教育类SaaS产品的快速搭建与二次开发。

480

被折叠的 条评论
为什么被折叠?



