在线考试系统(微服务,前后端分离)
前臺代碼
?👉manager-protal
后臺代碼
?👉 ? manager-service
視頻演示
?👉b站視頻
項目基本功能
?本在線考試系統主要完成了
系統架構
?在線考試系統主要采用Vue+SpringBoot+SpringCloud+Mybatis框架開發。內部采用標準的MVC架構進行基本框架搭建。通過Ngnix進行反向代理,服務器采用Docker進行統一管理,使用FastDFS完成遠程的文件上傳。具體的使用技術請看👉技術選型
技術選型
前臺
| HTML,CSS,LESS | emm…沒什么好說的 |
| Vue.js2.6 | 項目的前臺是完全基于Vue進行搭建的 |
| Npm | 前端安裝包工具 |
| Webpack | 前端模塊打包工具 |
| Vue-cli | Vue的腳手架,用于構建基本項目架構 |
| Vue-router | Vue的路由工具 |
| Vuex | Vue的狀態管理模式,集中式存儲管理 |
| Element-ui | Vue的一些基本組件庫 |
| axios | ajax的框架,用于異步請求 |
| v-charts | 構建統計視圖 |
| vue-quill-editor | 基于Vue的富文本框架 |
| vue-particles | 粒子特效 |
后臺
| SpringBoot | 該項目每個微服務內部都是使用SpringBoot進行搭建的,emmm,直接牛逼 |
| SpringCloud | 該項目是由好幾個微服務組成的,微服務之間的注冊和調用等是通過SpringCloud來完成的。使用到了Eureka,Zuul,Ribbon,Feign |
| MybatisPlus | 該項目使用MybatisPlus來完成對mysql的持久層操作 |
| SpringData | 該項目雖然沒有使用JPA來完成對mysql的操作,但是其他數據庫(MongoDB,redis,ElasticSearch)都是使用SpringData來操作的 |
| JWT | 該項目用jwt實現單點登錄,對用戶的請求進行認證。采用的是無狀態登錄 |
| Rsa | 一個非對稱機密算法,將token的載荷和秘鑰進行加密放入簽名域 |
| FastDFS | 一個輕量級的分布式文件系統,用于項目上傳圖片等文件 |
| RabbitMQ | 該技術是基于AMQP協議的消息代理軟件,通過該技術實現了手機,驗證碼的發送以及數據庫之間數據的同步 |
| Mysql | 該項目用Mysql來存儲主要的數據(用戶信息,學科信息,發布的試卷信息,用戶訂閱的考試信息,用戶的考試親狂) |
| MD5 | 一個不可逆加密算法,該項目用md5來實現對用戶密碼的加密 |
| Druid | 該項目使用Druid來作為mysql的數據源 |
| MongoDB | 該項目用MongoDB來存儲關于試卷的數據(試題信息,試卷信息)以及日志信息 |
| ElasticSearch | 該項目用ElasticSearch來存儲用于搜索的用戶數據,并實現搜索和聚合等功能 |
| Redis | 該項目用Redis來做部分數據的緩存,并且用redis來存儲手機和郵箱的驗證碼信息 |
| Nginx | 該項目用Nginx來實現反向代理 |
| Quartz | 定時任務框架,該項目用Quartz來實現某些操作的定 |
| Swagger2 | 該項目用swagger2實現對RESTful風格的api進行統一描述和可視化調用 |
| Lombok | 該項目使用Lombok來簡化實體類和日志 |
| Logback | 該項目使用logback來實現日志的輸出和持久化 |
| Hibernate-validator | 該項目使用hibernate-validator來進行部分實體類的數據校驗 |
| Docker | 該項目使用的服務器是用docker進行統一管理的 |
| 阿里云短信服務服務 | 該項目使用的短信服務是由阿里云提供的 |
| Git | 該項目用git來進行版本管理 |
其實,我這個項目不應該使用JWT完成單點登錄的,最好是使用SpringSecurity和OAuth2來完成權限和登錄的控制,一開始對項目的整體預估不足,貪圖簡單,就直接使用了JWT+Rsa來寫了,寫到后來權限控制那塊很難控制了,無奈呀~。沒辦法,都成型了,也懶得重構了,這個項目就這樣吧。下次注意!
項目目錄框架
前臺
后臺
單微服務目錄框架
這里已考試微服務為例
用戶微服務介紹
數據庫
CREATE TABLE `tb_user` (`id` bigint(64) NOT NULL COMMENT '雪花算法生成id',`name` varchar(10) DEFAULT NULL COMMENT '用戶的名稱',`age` int(3) DEFAULT '0' COMMENT '用戶的年齡',`area_province` varchar(10) DEFAULT NULL COMMENT '用戶的地區-省',`area_city` varchar(10) DEFAULT NULL COMMENT '用戶的地區-市',`area_county` varchar(10) DEFAULT NULL COMMENT '用戶的地區-縣',`status` tinyint(1) NOT NULL COMMENT '是否為管理員,1是,0不是',`username` varchar(32) NOT NULL COMMENT '用戶名',`sno` varchar(32) DEFAULT NULL COMMENT '用戶的學號',`password` varchar(32) NOT NULL COMMENT '密碼,加密存儲',`phone` varchar(11) DEFAULT NULL COMMENT '用戶的手機號',`email` varchar(50) DEFAULT NULL COMMENT '用戶的郵箱',`image` varchar(100) DEFAULT NULL COMMENT '用戶的頭像地址',`created` datetime NOT NULL COMMENT '創建時間',`salt` varchar(32) NOT NULL COMMENT '密碼加密的salt值',`version` bigint(20) DEFAULT '0' COMMENT '版本,樂觀鎖',`deleted` tinyint(1) DEFAULT '0' COMMENT '邏輯刪除,1刪除,0沒刪除',PRIMARY KEY (`id`),UNIQUE KEY `username` (`username`) USING BTREE ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶表';API
手機,郵箱驗證碼獲取接口
?這里就兩個接口,一個是獲取手機驗證碼,一個是獲取郵箱驗證碼
-
手機驗證碼通過rabbitmq發送驗證碼,并且設置2分鐘的過期時期保存在redis中
//發送消息 this.amqpTemplate.convertAndSend(this.authCodeProperties.getExchangeName(), "authCode.phone", authInfo);//將驗證碼放入redis中 this.redisTemplate.opsForValue().set(this.authCodeProperties.getPhoneName()+phone,authcode,2, TimeUnit.MINUTES); -
郵箱驗證碼通過rabbitmq發送驗證碼,并且設置2分鐘的過期時期保存在redis中
//發送消息 this.amqpTemplate.convertAndSend(this.authCodeProperties.getExchangeName(), "authCode.email", authInfo);//將驗證碼放入redis中 this.redisTemplate.opsForValue().set(this.authCodeProperties.getEmailName()+email,authcode,2, TimeUnit.MINUTES);
?👉 點擊查看rabbitmq接受消息代碼
用戶基本的DML操作服務接口
?DML操作無非就是贈刪改操作,但是看我們的API接口卻并沒有DELETE的操作,這是為什么呢?
?仔細看我們的用戶數據庫表,我是使用的邏輯刪除!
剩下的接口就不說了
考試微服務介紹
數據庫
學科表
CREATE TABLE `tb_subject` (`id` bigint(64) NOT NULL COMMENT '雪花算法生成id',`name` varchar(10) DEFAULT NULL COMMENT '學科的名稱',`note` varchar(100) DEFAULT NULL COMMENT '學科的備注信息',`icon` varchar(50) NOT NULL COMMENT '學科的圖標',`index` varchar(100) NOT NULL COMMENT '學科的前臺路徑',`created` datetime NOT NULL COMMENT '創建時間',`version` bigint(20) DEFAULT '0' COMMENT '版本,樂觀鎖',`deleted` tinyint(1) DEFAULT '0' COMMENT '邏輯刪除,1刪除,0沒刪除',PRIMARY KEY (`id`),UNIQUE KEY `name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='學科表';試題表
試題的信息是存放在MongoDB中的
- select:是用于存儲選擇題的選項的,判斷題不存在
- answer:試題的正確答案索引
- type:試題的類型,0:選擇題,1:判斷題
- subject:學科的id
- note:試題的備注信息
試卷表
試卷的信息也是存放在MongoDB中的
- name: 試卷名字
- subject:試卷的學科id
- school:出題學校的名字
- creatoe:出題人的用戶名(用戶名不可變)
- astrict:試卷的答題限制時間
- select:選擇題的題目id
- judge:判斷題的題目id
- selectScore:每到選擇題的分數
- judgeScore:每到判斷題的分數
- note:備注信息
- publicsh:是否發布
發布試卷記錄表
CREATE TABLE `tb_public_test` (`id` bigint(64) NOT NULL COMMENT '雪花算法生成id',`test_id` varchar(100) NOT NULL COMMENT '試卷的id',`start_time` timestamp NOT NULL DEFAULT '2020-01-01 00:00:00' COMMENT '試卷的開始時間',`end_time` timestamp NOT NULL DEFAULT '2020-01-01 00:00:00' COMMENT '試卷的結束時間',`status` int(3) NOT NULL COMMENT '試卷的狀態,-2:已刪除,-1:初始化,0:未開始,1:開啟中,2:已結束',`created` datetime NOT NULL COMMENT '創建時間',`version` bigint(20) DEFAULT '0' COMMENT '版本,樂觀鎖',`deleted` tinyint(1) DEFAULT '0' COMMENT '邏輯刪除,1刪除,0沒刪除',PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='試卷發布狀態表';我給start_time和end_time設置了一個過去的初始化時間
用戶訂閱試卷表
CREATE TABLE `tb_subscribe_exam` (`id` bigint(64) NOT NULL COMMENT '雪花算法生成id',`user_id` bigint(64) NOT NULL COMMENT '訂閱的用戶id',`test_id` varchar(100) NOT NULL COMMENT '試卷的id',`status` int(3) DEFAULT '0' COMMENT '訂閱記錄的狀態0:未考試,1:正在考試,2:已考試,3:再次考試',`score` double(6,1) DEFAULT '0.0' COMMENT '試卷的分數',`begin_work_time` timestamp NOT NULL DEFAULT '2020-01-01 00:00:00' COMMENT '開始答題時間',`finish_work_time` timestamp NOT NULL DEFAULT '2020-01-01 00:00:00' COMMENT '結束答題時間',`frequency` int(3) DEFAULT '0' COMMENT '考試的次數',`created` datetime NOT NULL COMMENT '創建時間',`version` bigint(20) DEFAULT '0' COMMENT '版本,樂觀鎖',`deleted` tinyint(1) DEFAULT '0' COMMENT '邏輯刪除,1刪除(取消訂閱后的狀態),0沒刪除(點擊訂閱后的狀態)',PRIMARY KEY (`id`),KEY `user_id` (`user_id`),CONSTRAINT `tb_subscribe_exam_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `tb_user` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶訂閱試卷表';試題答題情況表
CREATE TABLE `tb_exam_answer_situation` (`id` bigint(64) NOT NULL COMMENT '雪花算法生成id',`subscribe_exam_id` bigint(64) NOT NULL COMMENT '訂閱的考試id',`topic_id` varchar(100) NOT NULL COMMENT '試題id',`user_answer` varchar(200) NOT NULL COMMENT '用戶的答案,-1為未答題',`answer_situation` int(3) NOT NULL COMMENT '用戶的答題情況。-1:未答題,0:答錯,1:答對',`score` double(6,1) DEFAULT '0.0' COMMENT '試題的得分',`created` datetime NOT NULL COMMENT '創建時間',`version` bigint(20) DEFAULT '0' COMMENT '版本,樂觀鎖',`deleted` tinyint(1) DEFAULT '0' COMMENT '邏輯刪除,1刪除,0沒刪除',PRIMARY KEY (`id`),KEY `subscribe_exam_id` (`subscribe_exam_id`),CONSTRAINT `tb_exam_answer_situation_ibfk_1` FOREIGN KEY (`subscribe_exam_id`) REFERENCES `tb_subscribe_exam` (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶答題具體情況';用戶每一個答題都會生成一條記錄,與訂閱試卷表示一對多的關系
日志記錄表
用于記錄用戶,管理員的一些試卷操作。(比如:答題,取消試卷訂閱,重置試卷等)
API
Api以及具體的實現代碼太多了,就不多說了
消息服務介紹
以手機驗證碼為例
@RabbitListener(bindings = @QueueBinding(value = @Queue(value="MANAGER_PHONE_SMS_QUEUE",durable = "true"),exchange = @Exchange(value="MANAGER_EXCHANGE_SMS",ignoreDeclarationExceptions = "true",type=ExchangeTypes.TOPIC),key = "authCode.phone" )) public void sendPhoneAuthCode(Map<String,String> msg) throws ClientException {if(CollectionUtils.isEmpty(msg)){return ;}String phone = msg.get("phone");String authcode = msg.get("authcode");//放棄處理if(StringUtils.isAllBlank(phone,authcode)){return ;}log.info("接收到 {} 的驗證碼 {},準備發送",phone,authcode);if(!StringUtils.isEmpty(phone)&&!StringUtils.isEmpty(authcode)) {JsonObject jsonObject = new JsonObject();jsonObject.addProperty("authcode", authcode);this.sendPhoneSmsUtils.sendSms(phone,jsonObject.toString(),this.smsProperties.getSignName(),this.smsProperties.getVerifyCodeTemplate());} }- 當接收到數據時,會對數據進行一個簡單的空判斷,復雜的判斷在前臺和傳遞數據的時候已經校驗過了
- 數據沒問題,就會調用阿里云的手機驗證碼服務,對對應的手機號發送驗證碼
上傳微服務
- 上傳圖像回顯url,并修改數據庫
- 上傳縮略圖
搜索微服務
API
加一個搜索查詢API
監聽
/*** @Description 接受新增和修改用戶信息的消息* @date 2020/7/17 23:30* @param id* @return void*/ @RabbitListener(bindings = @QueueBinding(value=@Queue(value="MANAGER.SEARCH.SAVE.QUEUE",durable = "true"),exchange = @Exchange(value="MANAGER.EXCANGE.USER.SEARCH",ignoreDeclarationExceptions = "true",type = ExchangeTypes.TOPIC),key = {"user.insert","user.update"} )) public void save(Long id){if(id==null){throw new NullPointerException("新增(更新)檢索用戶信息的id為空");}this.userSearchService.save(id); }/*** @Description 接受刪除用戶信息的消息* @date 2020/7/17 23:31* @param id* @return void*/ @RabbitListener(bindings = @QueueBinding(value=@Queue(value="MANAGER.SEARCH.DELETE.QUEUE",durable = "true"),exchange = @Exchange(value="MANAGER.EXCANGE.USER.SEARCH",ignoreDeclarationExceptions = "true",type = ExchangeTypes.TOPIC),key = {"item.delete"} )) public void delete(Long id){if(id==null){throw new NullPointerException("刪除檢索用戶信息的id為空");}this.userSearchService.delete(id); }當新增,修改,刪除用戶時,會通知ElasticSearch進行數據修改,是數據庫信息同步
服務
/*** @author codekiller* @date 2020/7/16 20:17* @Description 用戶的搜索服務接口*/ public interface IUserSearchService {/*** @Description 構建用戶數據* @date 2020/7/17 20:18* @param user* @return top.codekiller.manager.search.pojo.UserInfo*/UserInfo buildUserInfo(User user);/*** @Description 檢索* @date 2020/7/17 21:20* @param searchRequest* @return top.codekiller.manager.search.pojo.result.user.SearchResult*/SearchResult search(SearchRequest searchRequest);/*** @Description 存儲新的用戶信息和更新* @date 2020/7/17 22:36* @param id* @return void*/void save(Long id);/*** @Description 刪除用戶信息* @date 2020/7/17 22:36* @param id* @return void*/void delete(Long id); }一共四個服務,就是增刪改查!
ps:
?項目有很多的不足,有很多都是應該好好完善的和修改的,我也懶得再去修改和重構了。因為是第一次完全靠自己寫一個項目,很多規范一開始做的不是很好。
?當在寫上傳和用戶等微服務時,很多東西沒有注意到,比如就說權限管理那塊;還有狀態碼相關,一開始全部用的自帶狀態碼,用來用去發現就那幾個…😒,后來大部分用得都是自建狀態碼。還有異常處理,實在懶得去做太多處理了,一個字:就是懶!封裝做的也不是太好!
?索性在寫試卷微服務時,有了一定的改善,代碼規范稍微好了一點,也有了一定的套路。但還是有很大的不足。
?怎么說呢,因為有期末考試的緣故,也花了好長時間去復習,所以斷斷續續做了一個多月。習慣了寫后端,這個前臺也確實花了我不少時間,也是第一次完全用Vue去構建前臺,雖然也有些不規范,并且很多功能沒有去添加。但是做下來,也算有所收獲吧。那就不虧!
?不知道下次要多久再去構建這么一個完整項目了,今年要多學些技術,明年就要考研了,一切都要時間,都需要去慢慢磨。這是一次不算很好,但也絕對不糟糕的體驗,以后會繼續努力!
總結
以上是生活随笔為你收集整理的在线考试系统(微服务,前后端分离)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 大众点评CAT开源监控系统剖析
- 下一篇: 基于SSM+MYSQL写的javaWeb