目录
一、项目介绍
多线程,并发是Java的高级特性,也是项目开发中经常需要用到的安全和优化手段。
良好的并发程序是线程安全和高效的,即首先保证运行结果正确,在这个基础上充分利用现代计算机多核处理器的优势来提高程序性能和响应速度。
该项目模拟驾校批量生成模拟试卷功能,目的是学习Java并发编程的知识以及在实战中的应用。
当然这只是Java并发编程的冰山一角,没有涉及到更多的并发编程技术,以后有时间我还会上传更多的实战项目,下面有兴趣的小伙伴们就跟着我一起来学习吧。
二、整体构架
功能描述:
生成某驾校K1模拟考试卷。
每次生成N份考试卷,每份考试卷M道题。(N和M可设定)
每张考试卷从题库随机抽取M道题作为考试卷试题。(M可设定比如100)
保存试卷和试卷所属考试题。
功能要求:
1 通过并发、线程池(优化时间)等技术实现批量生成模拟考试卷。
2 支持自定义设置生成试卷套数和每套试卷题数。
3 支持执行任务实时进度查看。
4 支持子任务(生成试卷)成功或失败原因实时查看。
5 支持重置,清除当前任务生成数据,重新生成任务。
用到的多线程技术:
1 ThreadPoolExecutor线程池,用来并发执行任务。
2 ConcurrentHashMap并发容器,用来安全的存放任务。
3 AtomicInteger,原子类用来安全的计数。
三、数据库表设计
数据库表
c_question_bank 题库
用于生成考试题目。
id:varchar 唯一标识uuid
title:varchar 题目,类似机动车驾驶人应当每年进行一次身体检查。
candidate_answer 候选答案,json,类似 [A:正确,B:错误]
degree_of_difficulty:int 题目难度,1简单,2中等难度,3高等难度
k:varchar 所属科目, k1,k3
answer:varchar 正确答案,类似A
enabled:int 启用0,禁止1
create_user 创建人
create_time 创建时间
c_exam_paper 考试卷
试卷
id:varchar 考试卷唯一编号uuid
name:varchar 考试卷名字
k:varchar 所属科目, k1,k3
enabled:int 启用0,禁止1
create_user 创建人
create_time 创建时间
c_exam_question 考试题目
试卷所属题目
id:varchar 唯一标识uuid
bank_id:varchar 所属题库Id,外键
paper_id:varchar 所属考试卷Id,外键
title:varchar 题目,类似机动车驾驶人应当每年进行一次身体检查。
candidate_answer 候选答案,json,类似 [A:正确,B:错误]
degree_of_difficulty:int 题目难度,1简单,2中等难度,3高等难度
k:varchar 所属科目,比如 k1,k3
answer:varchar 正确答案,类似A
enabled:int 启用0,禁止1
create_user 创建人
create_time 创建时间
四、思路和代码实现
1 基础实体类,dao,service代码
/**
* 题库
*/
@Data
@TableName("c_question_bank")
public class QuestionBank implements Serializable {
@TableId("id")
private String id;
private String title;
private String candidateAnswer;
private int degreeOfDifficulty;
private String k;
private String answer;
private int enabled;
private String createUser;
@JsonFormat(locale="zh", timezone="GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
/**
* 题库mapper
*/
public interface QuestionBankMapper extends BaseMapper<QuestionBank> {
// 批量生成
Integer insertQuestionBanks(@Param("questionBankList") List<QuestionBank> questionBankList);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 题库mapper.xml -->
<mapper namespace="cc.myspring.c1.mapper.QuestionBankMapper">
<insert id="insertQuestionBanks">
INSERT INTO c_question_bank (id, title,candidate_answer,degree_of_difficulty,k,
answer,enabled,create_user,create_time)
VALUES
<foreach collection ="questionBankList" item="q" separator =",">
(#{q.id}, #{q.title},#{q.candidateAnswer},
#{q.degreeOfDifficulty},#{q.k}, #{q.answer},#{q.enabled}, #{q.createUser}, #{q.createTime})
</foreach>
</insert>
</mapper>
/**
* 题库服务接口
*/
public interface QuestionBankService extends IService<QuestionBank> {
// 批量生成
Integer insertQuestionBanks(List<QuestionBank> questionBankList);
}
/**
* 题库服务
*/
@Service
public class QuestionBankServiceImpl extends ServiceImpl<QuestionBankMapper, QuestionBank> implements QuestionBankService {
@Autowired
QuestionBankMapper questionBankMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public Integer insertQuestionBanks(List<QuestionBank> questionBankList) {
return questionBankMapper.insertQuestionBanks(questionBankList);
}
}
---
/**
* 考试题目
*/
@Data
@TableName("c_exam_question")
public class ExamQuestion implements Serializable {
@TableId("id")
private String id;
/** 所属题库Id */
private String bankId;
/** 所属考试卷Id */
private String paperId;
private String title;
private String candidateAnswer;
private int degreeOfDifficulty;
private String k;
private String answer;
private int enabled;
private String createUser;
@JsonFormat(locale="zh", timezone="GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
/**
* 试题mapper
*/
public interface ExamQuestionMapper extends BaseMapper<ExamQuestion> {
// 批量生成
Integer insertExamQuestions(@Param("examQuestionList") List<ExamQuestion> examQuestionList);
}
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!-- 试题mapper.xml -->
<mapper namespace="cc.myspring.c1.mapper.ExamQuestionMapper">
<insert id="insertExamQuestions">
INSERT INTO c_exam_question (id, bank_id,paper_id,title,candidate_answer,degree_of_difficulty,k,
answer,enabled,create_user,create_time)
VALUES
<foreach collection ="examQuestionList" item="q" separator =",">
(#{q.id}, #{q.bankId},#{q.paperId}, #{q.title},#{q.candidateAnswer},
#{q.degreeOfDifficulty},#{q.k}, #{q.answer},#{q.enabled}, #{q.createUser}, #{q.createTime})
</foreach>
</insert>
</mapper>
/**
* 试题服务接口
*/
public interface ExamQuestionService extends IService<ExamQuestion> {
}
/**
* 试题服务
*/
@Service
public class ExamQuestionServiceImpl extends ServiceImpl<ExamQuestionMapper, ExamQuestion> implements ExamQuestionService {
}
---
/**
* 考试卷
*/
@Data
@TableName("c_exam_paper")
public class ExamPaper implements Serializable {
private String id;
private String name;
private String k;
private int enabled;
private String createUser;
@JsonFormat(locale="zh", timezone="GMT+8",pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
}
/**
* 试卷mapper
*/
public interface ExamPaperMapper extends BaseMapper<ExamPaper> {
}
/**
* 试卷服务接口
*/
public interface ExamPaperService extends IService<ExamPaper> {
// 生成一张试卷
public PaperResult generateOneExamPaper(GenerateExamPaperRequire require);
// 还原测试环境
public void reset();
}
---
2 生成一套模拟试卷的业务代码,供线程池调用,无侵入性,可单独执行。
/**
* 模拟试卷服务
*/
@Service
public class ExamPaperServiceImpl extends ServiceImpl<ExamPaperMapper, ExamPaper> implements ExamPaperService {
// 题库dao
@Autowired
QuestionBankMapper questionBankMapper;
// 模拟试卷dao
@Autowired
ExamPaperMapper examPaperMapper;
// 模拟试题dao
@Autowired
ExamQuestionMapper examQuestionMapper;
/**
*
* @param require 生成考试题库条件
* @return PaperResult 试卷生成结果
*/
@Override
@Transactional(rollbackFor = Exception.class)
public PaperResult generateOneExamPaper(GenerateExamPaperRequire require) {
// 生成前清除已有数据
// 生成试卷uuid
String paperId = IdUtils.fastSimpleUUID();
// 检查运行时间
System.out.println("startTime:"+
new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss:SSS") .format(new Date()));
// 获取题库里激活的和所有k1的考试题
QueryWrapper<QuestionBank> bankQueryWrapper = new QueryWrapper<>();
bankQueryWrapper.lambda().eq(QuestionBank::getEnabled,0)
.eq(QuestionBank::getK,require.getK());
List<QuestionBank> questionBankList = questionBankMapper.selectList(bankQueryWrapper);
// 题库题目大小
int questionBankListSize = questionBankList.size();
// 随机组成一张试卷,100道题目
int questionNumbers = require.getQuestionNumbers();
if(questionNumbers<=0){
return new PaperResult(
GeneralResultType.Failure,require.getName(),paperId,"考试卷题目数量小于0");
}
// 已经生成的考试卷题目数量 随机抽取100道题
// 实现1,打乱原list,获取list某一区间,当list较大时耗时久
// 实现2,根据大小生成随机数组装,当list规模较小时生成快
int alreadyQuestionNumber = 0;
// 保存生成的题库uuid
List<String> questionUUIDs = new ArrayList<>();
// 生成的题目
List<ExamQuestion> examQuestionList = new ArrayList<>();
Set<Integer> alreadyQuestionSet = new HashSet<>();
while(alreadyQuestionNumber!=require.getQuestionNumbers()){
// 最坏情况随机生成题目超时,返回Failure
// if(time>10s) return new PaperResult(GeneralResultType.Failure,require.getName(),paperId,"生成考试题目超时");
int number = (int)(Math.random()*questionBankListSize);
if(alreadyQuestionSet.contains(number)){
continue;
}else{
QuestionBank randomQuestionBank = questionBankList.get(number);
questionUUIDs.add(randomQuestionBank.getId());
// QuestionBank转换为ExamQuestion 相同属性填充
ExamQuestion e = Convert2ExamQuestion.convert2ExamQuestion(randomQuestionBank);
String questionId = IdUtils.fastSimpleUUID();
e.setId(questionId);
e.setPaperId(paperId);
e.setBankId(randomQuestionBank.getId());
e.setCreateTime(new Date());
e.setCreateUser("lee");
examQuestionList.add(e);
alreadyQuestionSet.add(number);
alreadyQuestionNumber++;
}
}
System.out.println(questionUUIDs);
// 保存试卷和考题
// 自定义方法,批量生成试题。定义在 ExamQuestionMapper.xml 中。
examQuestionMapper.insertExamQuestions(examQuestionList);
ExamPaper examPaper = new ExamPaper();
examPaper.setId(paperId);
examPaper.setName(require.getName());
examPaper.setEnabled(0);
examPaper.setK(require.getK());
examPaper.setCreateTime(new Date());
examPaper.setCreateUser("lee");
examPaperMapper.insert(examPaper);
// 注册每张试卷总题目生成进度
// 模拟耗时操作
try {
Thread.sleep((long)(Math.random()*6)*1000);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
// 模拟任务失败抛出异常
int ramdomInt = (int)(Math.random()*7);
int exceptionZeroDivision = 1/ramdomInt;
System.out.println("endTime:"+
new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss:SSS") .format(new Date()));
return new PaperResult(GeneralResultType.Success,require.getName(),paperId,"");
}
@Override
// 重置任务,清除数据库表
public void reset() {
// 效率删除是在xml调用truncate语句
QueryWrapper<ExamQuestion> questionWrapper = new QueryWrapper<>();
examQuestionMapper.delete(questionWrapper);
QueryWrapper<ExamPaper> paperWrapper = new QueryWrapper<>();
examPaperMapper.delete(paperWrapper);
}
}
---
public class Convert2ExamQuestion {
/**
* 考试卷题目 convert
* 从题库类转换为考试题目
* 相同属性填充
*
* @param questionBank
* @return
*/
public static ExamQuestion convert2ExamQuestion(QuestionBank questionBank) {
ExamQuestion examQuestion = new ExamQuestion();
// 拷贝属性
BeanUtils.copyProperties(questionBank, examQuestion);
return examQuestion;
}
}
---
/**
* 生成考试题库条件
*/
@Data
public class GenerateExamPaperRequire {
// 考试科目 k1,k4
private String k;
// 考卷名
private String name;
// 一次批量生成考试卷数量
private int examPaperNumbers;
// 每张考试卷题目数量
private int questionNumbers;
/** 默认 */
public GenerateExamPaperRequire(){
this.k = "k1";
this.name = "c1";
this.examPaperNumbers = 10;
this.questionNumbers = 10;
}
/** 自定义 */
public GenerateExamPaperRequire(String k, String name, int examPaperNumbers, int questionNumbers) {
this.k = k;
this.name = name;
this.examPaperNumbers = examPaperNumbers;
this.questionNumbers = questionNumbers;
}
}
---
/**
* 试卷生成结果
*/
@Data
public class PaperResult {
private final GeneralResultType resultType;//子任务是否成功完成
private final String paperName;//试卷名字
private final String paperId;//试卷Id
private final String reason;//如果子任务失败,失败原因
public PaperResult(GeneralResultType resultType, String paperName, String paperId, String reason) {
this.resultType = resultType;
this.paperName = paperName;
this.paperId = paperId;
this.reason = reason;
}
}
---
/**
* 考试卷生成结果枚举类
* 成功/失败/异常
*/
public enum GeneralResultType {
Success,Failure,Exception
}
业务核心逻辑
从题库随机抽取M道题目(要求不重复),生成一份试卷(M可设定比如100)。
a) 根据传入参数(是否启用,科目)获取所有题库试题保存在questionBankList中,一次性
加载。
b) 生成试卷唯一UUID。
c) 从questionBankList随机抽取M道题,复制属性并填充外键属性(所属题库,所属试卷)
保存在examQuestionList中,要求不重复。
d) 批量插入examQuestionList(生成试题)。
e) 根据传入参数(试卷名,科目)保存试卷。
---
3 任务明细类 PapersInfo
/**
* 批量生成多套考试题信息
*
*/
@Data
public class PapersInfo {
// 考试科目 k1,k4
private String k;
// 考卷名字
private String name;
// 一次批量生成考试卷数量
private int examPaperNumbers;
// 每张考试卷题目数量
private int questionNumbers;
// 存放每一张试卷生成结果
private LinkedBlockingDeque<PaperResult> paperQueues;
private AtomicInteger successCount;//任务的成功次数
private AtomicInteger taskProcessCount;//工作中任务目前已经处理的次数
public PapersInfo(String k, String name, int examPaperNumbers, int questionNumbers) {
this.k = k;
this.name = name;
this.examPaperNumbers = examPaperNumbers;
this.questionNumbers = questionNumbers;
this.successCount = new AtomicInteger(0);
this.taskProcessCount = new AtomicInteger(0);
this.paperQueues = new LinkedBlockingDeque<PaperResult>(examPaperNumbers);
}
/**
* 子任务完成后加入到任务结果容器中并计数
* @param result
*/
public void addPaperTaskResult(PaperResult result) {
if(GeneralResultType.Success.equals(result.getResultType())) {
successCount.incrementAndGet();
}
taskProcessCount.incrementAndGet();
paperQueues.addLast(result);
}
/**
* 提供工作的整体进度信息
* @return
*/
public Map<String,String> getTotalProcess() {
final Map<String,String> totalInfoMap = new HashMap<>();
// 批量生成试卷任务完成状态
String finishStatus = taskProcessCount.get() == examPaperNumbers?"1":"0";
// 批量生成试卷任务成功状态,如果为1表示成功
String batchSuccessStatus = successCount.get() == examPaperNumbers?"1":"0";
// 进度信息
String totalProcessString = "Success["+successCount.get()+"] /Current["+taskProcessCount.get()
+"] Total["+examPaperNumbers+"]";
totalInfoMap.put("finishStatus",finishStatus);
totalInfoMap.put("batchSuccessStatus",batchSuccessStatus);
totalInfoMap.put("totalProcessString",totalProcessString);
return totalInfoMap;
}
/**
* 返回每张试卷的结果
* @return
*/
public Map<String,Object> getPapersInfo(){
final Map<String,Object> papersInfoMap = new HashMap<>();
// 结果信息
List<PaperResult> paperResultList = new LinkedList<>();
Iterator<PaperResult> paperResultListIterator = paperQueues.iterator();
while(paperResultListIterator.hasNext()){
PaperResult p = paperResultListIterator.next();
paperResultList.add(p);
}
papersInfoMap.put("paperResultListString",paperResultList);
// 批量生成试卷任务完成状态
String finishStatus = taskProcessCount.get() == examPaperNumbers?"1":"0";
papersInfoMap.put("finishStatus",finishStatus);
return papersInfoMap;
}
}
4 线程池服务类。
/**
* 生成考试卷Pool
*/
@Service
public class GenerateExamPaperPool {
// 试卷生成服务
@Autowired
ExamPaperService examPaperService;
//框架运行时的线程数,与机器的CPU数相同
private static final int THREAD_COUNTS
= Runtime.getRuntime().availableProcessors();
// 队列,线程池使用,用以存放待处理的任务
private static BlockingQueue<Runnable> taskQueue
= new ArrayBlockingQueue<Runnable>(500);
// 线程池,固定大小,有界队列
private static ExecutorService taskExecutor
= new ThreadPoolExecutor(THREAD_COUNTS, THREAD_COUNTS,
60, TimeUnit.SECONDS, taskQueue);
// 工作信息的存放容器
// 任务名:任务明细
private static ConcurrentHashMap<String, PapersInfo> papersInfoMap
= new ConcurrentHashMap<String, PapersInfo>();
// 获取任务容器
public static Map<String, PapersInfo> getMap(){
return papersInfoMap;
}
// 创建PaperTask
// 线程池执行的具体任务,实现了Runnable,可以交给线程池执行。
public PaperTask createPaperTask(PapersInfo papersInfo){
return new PaperTask(papersInfo);
}
// 包装,提交给线程池使用,并处理任务的结果
private class PaperTask implements Runnable{
private PapersInfo papersInfo;
public PaperTask(PapersInfo papersInfo) {
this.papersInfo = papersInfo;
}
@Override
public void run() {
PaperResult paperResult = null;
try {
// 反向注册GenerateExamPaperRequire,获取任务的具体参数
GenerateExamPaperRequire generateExamPaperRequire = new GenerateExamPaperRequire();
generateExamPaperRequire.setName(papersInfo.getName());
generateExamPaperRequire.setK(papersInfo.getK());
generateExamPaperRequire.setExamPaperNumbers(papersInfo.getExamPaperNumbers());
generateExamPaperRequire.setQuestionNumbers(papersInfo.getQuestionNumbers());
paperResult = examPaperService.generateOneExamPaper(generateExamPaperRequire);
if(paperResult == null){
paperResult = new PaperResult(GeneralResultType.Exception,
"", "","paperResult is NULL");
}
if(paperResult.getResultType()==null){
if(paperResult.getReason()==null){
paperResult = new PaperResult(GeneralResultType.Exception,
"", "","paperResult is NULL");
}else{
paperResult = new PaperResult(GeneralResultType.Exception,
"", "",
"paperResult is NULL,reason:"+paperResult.getReason());
}
}
}catch(Exception e) {
e.printStackTrace();
paperResult = new PaperResult(
GeneralResultType.Exception, "","",e.getMessage());
}finally {
//将结果加入到PaperInfo
papersInfo.addPaperTaskResult(paperResult);
}
}
}
/**
* 注册生成模拟考试任务
* @param generateExamPaperRequire
*/
public void registerPapersTask(GenerateExamPaperRequire generateExamPaperRequire){
PapersInfo papersInfo = new PapersInfo(
generateExamPaperRequire.getK(),
generateExamPaperRequire.getName(),
generateExamPaperRequire.getExamPaperNumbers(),
generateExamPaperRequire.getQuestionNumbers());
if(papersInfoMap.putIfAbsent(generateExamPaperRequire.getName(), papersInfo)!=null) {
throw new RuntimeException(generateExamPaperRequire.getName()+"生成考试题任务已经注册!");
}
}
/**
* 执行生成模拟考试任务
*/
public void generatePapersTask(String taskName){
// 获取缓存中的任务
PapersInfo papersInfo = getTask(taskName);
// 生成任务
PaperTask paperTask = createPaperTask(papersInfo);
// 使用线程池执行
taskExecutor.execute(paperTask);
}
/**
* 根据任务名字获取任务
* @param taskName
* @return
*/
@SuppressWarnings("unchecked")
private PapersInfo getTask(String taskName){
PapersInfo papersInfo = (PapersInfo) papersInfoMap.get(taskName);
if (null==papersInfo){
throw new RuntimeException(taskName+"是非法任务!");
}
return papersInfo;
}
/**
* 获得工作的整体处理进度
* @param taskName
* @return
*/
public Map<String,String> getTaskProgess(String taskName) {
PapersInfo papersInfo = getTask(taskName);
return papersInfo.getTotalProcess();
}
/**
* 获得每张试卷的结果
* @param taskName
* @return
*/
public Map<String,Object> getPapersInfo(String taskName) {
PapersInfo papersInfo = getTask(taskName);
return papersInfo.getPapersInfo();
}
/**
* 清空任务容器
*/
public void reset(){
papersInfoMap = new ConcurrentHashMap<String, PapersInfo>();
}
}
PaperTask实现时需要注意的地方
这里要考虑到业务执行的所有可能性,因为有可能业务实现者和并发工具开发者
不是同一个人,你不能完全信任并保证业务的调用不会有问题,比如返回结果为空,
或者返回结果中的某一个属性没有正确填充,最后用try,catch将整个业务代码包裹住,
如果业务执行失败,通过框架生成失败信息。
---
5 服务调用。
/**
* 线程池工具controller
*/
@RestController
@RequestMapping("/generateExamPaper")
public class GenerateExamPaperController extends BaseController {
// 试卷生成服务
@Autowired
ExamPaperService examPaperService;
// 线程池服务类
@Autowired
GenerateExamPaperPool generateExamPaperPool;
/**
* 批量生成考试内容
* @param generateExamPaperRequire
* @return
*/
@GetMapping("/generate")
public AjaxResult generatePapers(GenerateExamPaperRequire generateExamPaperRequire){
// 如果没有传参数,使用默认参数
if(Objects.isNull(generateExamPaperRequire)){
generateExamPaperRequire = new GenerateExamPaperRequire();
}
// 注册任务
generateExamPaperPool.registerPapersTask(generateExamPaperRequire);
// 执行任务
for(int i=0;i<generateExamPaperRequire.getExamPaperNumbers();i++) {
generateExamPaperPool.generatePapersTask(generateExamPaperRequire.getName());
}
return AjaxResult.success("任务已启动,等待完成中...");
}
/**
* 获取任务进度
* @param name
* @return
*/
@GetMapping(value="/queryProgess")
public AjaxResult queryProgess(String name){
return AjaxResult.success(generateExamPaperPool.getTaskProgess(name));
}
/**
* 获取子任务执行结果
* @param name
* @return
*/
@GetMapping(value="/queryPapersInfo")
public AjaxResult queryPapersInfo(String name){
return AjaxResult.success(generateExamPaperPool.getPapersInfo(name));
}
/**
* 还原测试环境
* 再次测试前,清除所有数据,
* @return
*/
@GetMapping("/reset")
public AjaxResult resetAll(){
// 清除papersInfoMap
generateExamPaperPool.reset();
// 清除数据库表数据
examPaperService.reset();
return AjaxResult.success("数据已清除!");
}
}
有了线程池服务和任务明细类,执行任务就很方便了。
首先在线程池服务类注册任务信息,然后交给线程池执行,前端通过任务明细类就可以
实时获取任务进度以及子任务处理结果了。
---
6 前端代码
<html>
<head>
<title>首页</title>
<script type="text/javascript" src="/static/js/jquery/jquery-3.5.1.min.js"></script>
<style type="text/css">
#main{
margin:10px;
}
#title{
margin:10px;
font-size: 20px;
font-weight:bold;
}
#progressInfoDiv{
margin-top: 20px;
}
#progressInfo{
margin-top:10px;
}
#PapersInfoDiv{
margin-top: 20px;
margin-bottom: 20px;
}
.greenColor{
color:green;
}
.redColor{
color:red;
}
</style>
</head>
<body>
<div id="container">
<div id = "title">驾校批量生成k1考试模拟卷</div>
<div id="main">
选择科目:
<select id="k">
<option value="k1">k1</option>
</select>
生成试卷名字:
<input id="name" value="c1" />
生成试卷套数:
<input id="examPaperNumbers" value="20" />
每张试卷题数:
<input id="questionNumbers" value="100" />
<button id="generateBtn">执行任务</button>
<button id="resetBtn">重置任务</button>
<div id = "progressInfoDiv">
任务进度:
<div id="progressInfo">
</div>
</div>
<div id = PapersInfoDiv>
任务结果:
<div id="papersInfos">
</div>
</div>
</div>
</div>
<script>
$(document).ready(function(){
// 执行任务
$("#generateBtn").click(function(){
generate();
});
// 重置
$("#resetBtn").click(function(){
reset();
});
});
var otimer = null; // 任务进度定时器
var ptimer = null; // 任务结果定时器
// 执行任务
function generate(){
$.ajax({
type:"GET",
url:"/generateExamPaper/generate",
dataType: 'json',
data: {k: $("#k").val(),name:$("#name").val(),
examPaperNumbers:$("#examPaperNumbers").val(),
questionNumbers:$("#questionNumbers").val()},
success:function(result){
console.log(result);
// 执行定时查询任务进度
timeGetTaskProgress();
// 执行定时查询任务结果
timeGetPapersInfo();
}
});
}
// 任务进度查询
function getTaskProgress(){
$.ajax({
type:"GET",
url:"/generateExamPaper/queryProgess?name="+$("#name").val(),
dataType: 'json',
success:function(result){
console.log(result);
// 获取任务进度字符串描述
var taskProgess = result.data.totalProcessString;
$("#progressInfo").text(taskProgess);
// 任务执行完成停止定时刷新进度
if(result.data.finishStatus == "1"){
clearInterval(otimer);
}
// 任务结束并且批处理全部成功
if(result.data.finishStatus == "1" && result.data.batchSuccessStatus=="1"){
$("#progressInfo").toggleClass("greenColor")
// this.greenColor = 1;
}
// 任务结束并且批处理结果有异常
if(result.data.finishStatus == "1" && result.data.batchSuccessStatus=="0"){
$("#progressInfo").toggleClass("redColor")
// this.greenColor = 1;
}
}
});
}
// 定时任务进度查询
function timeGetTaskProgress(){
getTaskProgress();
otimer = setInterval(getTaskProgress, 200);
}
// 任务结果查询
function getPapersInfo(){
$.ajax({
type:"GET",
url:"/generateExamPaper/queryPapersInfo?name="+$("#name").val(),
dataType: 'json',
success:function(result){
console.log(result);
// 获取任务结果字符串描述
var papersInfoPre = result.data.paperResultListString;
// 增加颜色的进度字符串描述
var papersInfoHTML = "";
// 根据任务结果成功与否展示不同的颜色
papersInfoPre.forEach((item, index, arr) => {
// 用绿色和红色标记是否成功
if(item.resultType == "Success"){
item.color = "green"
papersInfoHTML+='<span style="color:green">'
+"处理结果:成功,"+"试卷名字:"+item.paperName+",试卷id:"+item.paperId+",失败原因["+item.reason+"]"
+'</span><br>'
}else{
item.color = "red"
papersInfoHTML+='<span style="color:red">'
+"处理结果:失败,"+"试卷名字:"+item.paperName+",试卷id:"+item.paperId+",失败原因["+item.reason+"]"
+'</span><br>'
}
})
$("#papersInfos").html(papersInfoHTML);
// 任务执行完成停止定时刷新结果
if(result.data.finishStatus == "1"){
clearInterval(ptimer);
}
}
});
}
// 定时调用任务结果查询
function timeGetPapersInfo(){
getPapersInfo();
ptimer = setInterval(getPapersInfo, 200);
}
// 重置任务
function reset(){
$("#progressInfo").removeClass();
// 停止定时查询任务进度
clearInterval(otimer);
// 停止定时查询任务结果明细
clearInterval(ptimer);
$("#papersInfos").html("");
$.ajax({
type:"GET",
url:"/generateExamPaper/reset",
dataType: 'json',
success:function(result){
console.log(result);
}
});
}
</script>
</body>
</html>
---
7 项目运行截图
---
五、源代码下载和补充
<点击这里下载源码>(单应用)。
<点击这里下载源码>(分布式应用和Vue)。
部署前请先查看readme.txt说明文档。
sql文件在/resources/sql目录下,需要先导入到数据库中。
项目使用了mybatis-plus来实现底层的数据库操作。
项目除了并发应用的其他功能实现比较简单,或还不完善,比如重置任务只是简单的
清除表所有数据,多套试卷名字应该可以指定,批量任务失败时(有一个或多个子任务失
败),应该支持单个失败子任务重新生成。有兴趣的小伙伴可以自己补全和扩展。
如果有问题可以随时私信我,看到即回。