微服务的编程测评系统7-题库接口
提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- 前言
- 1. 题库管理
- 1.1 数据库设计
- 1.2 后端开发01
- 1.3 后端开发02
- 1.4 MyBatis 分⻚插件 PageHelper
- 1.5 查询数据总量
- 1.6 优化
- 1.7 前端设计
- 1.8 快捷键美化样式
- 1.9 前端开发
- 1.10 交互逻辑书写
- 1.11 分页动态展示数据
- 2. 添加题目
- 2.1 后端
- 3. 编辑题目功能
- 3.1 后端
- 4. 删除题目
- 4.1 后端
- 总结
前言
1. 题库管理
1.1 数据库设计
create table tb_question(
question_id bigint unsigned not null comment '题目id',
title varchar(50) not null comment '题目标题',
difficulty tinyint not null comment '题目难度1:简单 2:中等 3:困难',
time_limit int not null comment '时间限制',
space_limit int not null comment '空间限制',
content varchar(1000) not null comment '题目内容',
question_case varchar(1000) comment '题目用例',
default_code varchar(500) not null comment '默认代码块',
main_fuc varchar(500) not null comment 'main函数',
create_by bigint unsigned not null comment '创建人',
create_time datetime not null comment '创建时间',
update_by bigint unsigned comment '更新人',
update_time datetime comment '更新时间',
primary key(`question_id`)
);
1.2 后端开发01
@TableName("tb_question")
@Getter
@Setter
public class Question extends BaseEntity {@TableId(type = IdType.ASSIGN_ID)private Long questionId;private String title;private Integer difficulty;private Long timeLimit;private Long spaceLimit;private String content;private String questionCase;private String defaultCode;private String mainFuc;
}
@Data
public class QuestionQueryDTO {private String title;private Integer difficulty;private Integer pageNum = 1;private Integer pageSize = 10;
}
对于请求返回的表格数据,我们也对其返回数据像R一样处理,在core中
其中pageNum 和pageSize 是必传的
/*** 表格分页数据对象*/
@Getter
@Setter
public class TableDataInfo {/*** 总记录数*/private long total;/*** 列表数据*/private List<?> rows;/*** 消息状态码*/private int code;/*** 消息内容*/private String msg;/*** 表格数据对象*/public TableDataInfo() {}//未查出任何数据时调用public static TableDataInfo empty() {TableDataInfo rspData = new TableDataInfo();rspData.setCode(ResultCode.SUCCESS.getCode());rspData.setRows(new ArrayList<>());rspData.setMsg(ResultCode.SUCCESS.getMsg());rspData.setTotal(0);return rspData;}//查出数据时调用public static TableDataInfo success(List<?> list,long total) {TableDataInfo rspData = new TableDataInfo();rspData.setCode(ResultCode.SUCCESS.getCode());rspData.setRows(list);rspData.setMsg(ResultCode.SUCCESS.getMsg());rspData.setTotal(total);return rspData;}
}
@GetMapping("/list")public TableDataInfo list(QuestionQueryDTO questionQueryDTO) {log.info("查询题库列表,questionQueryDTO:{}", questionQueryDTO);return null;}
这样就很好了
1.3 后端开发02
这个查询操作比较复杂,我们用xml来写sql语句
@Data
public class QuestionVO {private Long questionId;private String title;private Integer difficulty;private String createName;private LocalDateTime createTime;
}
然后再resource下创建QuestionMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ck.system.mapper.question.QuestionMapper"><select id="selectQuestionList" resultType="com.ck.system.domain.question.vo.QuestionVO">SELECTtq.question_id,tq.title,tq.difficulty,ts.nick_name as create_name,tq.create_timeFROMtb_question tqleft jointb_sys_user tsontq.create_by = ts.user_id<where><if test="difficulty !=null ">AND difficulty = #{difficulty}</if><if test="title !=null and title !='' ">AND title LIKE CONCAT('%',#{title},'%')</if></where>ORDER BYcreate_time DESC</select></mapper>
这个没有增加分页查询的
@Mapper
public interface QuestionMapper extends BaseMapper<Question> {List<QuestionVO> selectQuestionList(QuestionQueryDTO questionQueryDTO);}
这样就OK了
@Overridepublic TableDataInfo list(QuestionQueryDTO questionQueryDTO) {List<QuestionVO> questionVOS = questionMapper.selectQuestionList(questionQueryDTO);if(CollectionUtil.isEmpty(questionVOS)){return TableDataInfo.empty();}return TableDataInfo.success(questionVOS, questionVOS.size());}
注意这里success返回的第二个参数应该是全部的数据量,是符合查询条件的size,但是不是满足了分页后的数据量,因为后面还要添加分页的sql语句
这样查询出来就是那一页的数据总量
但是我们要符合条件的所有的数据总量,所有页的数据总量
1.4 MyBatis 分⻚插件 PageHelper
官网
优势:
⾃动分⻚:PageHelper 会⾃动解析你执⾏的 SQL 语句,并根据你提供的⻚码和每⻚显⽰的记录数,⾃动添加分⻚相关的 SQL ⽚段,从⽽返回正确的分⻚结果。⽆需在 SQL语句中⼿动编写复杂的分⻚逻辑。
配置简单:在 Spring Boot 项⽬中通过添加依赖和简单配置就可以启⽤它。
易于集成:PageHelper 可以与 MyBatis 很好地集成,⽆论你是使⽤ XML 映射⽂件还是注解⽅式,都可以⽅便地使⽤ PageHelper。
性能优化:PageHelper 采⽤了物理分⻚的⽅式,相⽐于内存分⻚,它可以减少内存消耗并提⾼查询性能。
<dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>${pagehelper.boot.version}</version>
</dependency><pagehelper.boot.version>2.0.0</pagehelper.boot.version>
直接加载system下使用
@Overridepublic TableDataInfo list(QuestionQueryDTO questionQueryDTO) {PageHelper.startPage(questionQueryDTO.getPageNum(), questionQueryDTO.getPageSize());List<QuestionVO> questionVOS = questionMapper.selectQuestionList(questionQueryDTO);if(CollectionUtil.isEmpty(questionVOS)){return TableDataInfo.empty();}return TableDataInfo.success(questionVOS, questionVOS.size());}
直接这样写就可以了,就可以自动添加分页的sql语句
1.5 查询数据总量
在PageHelper.startPage中,这个方法其实已经把总量给查出来了
DEFAULT_COUNT传的值是true
就是PageHelper在执行分页之前,会先执行条件的sql语句,并把数量给查出来,然后再加上分页的sql语句,查出对应的数据
现在的问题就是怎么获取它查出来的数量呢
long total = new PageInfo<>(questionVOList).getTotal();
这个就是获取满足条件的数据总量,而不是那一页的数据总量
1.6 优化
在core里面增加一个类,PageQueryDTO
@Data
public class PageQueryDTO {private Integer pageNum = 1;private Integer pageSize = 10;
}
@Data
public class QuestionQueryDTO extends PageQueryDTO {private String title;private Integer difficulty;
}
因为每个分页的请求参数都会具有这两个参数
然后第二个优化就是service返回list,controller用baseController来返回
baseController中增加方法
然后把PageHelper的依赖增加到core中
在baseController中
public TableDataInfo getTableDataInfo(List<?> list){if(CollectionUtil.isEmpty(list)){return TableDataInfo.empty();}long total = new PageInfo<>(list).getTotal();return TableDataInfo.success(list, total);}
service
@Overridepublic List<QuestionVO> list(QuestionQueryDTO questionQueryDTO) {PageHelper.startPage(questionQueryDTO.getPageNum(), questionQueryDTO.getPageSize());return questionMapper.selectQuestionList(questionQueryDTO);}
controller
@GetMapping("/list")public TableDataInfo list(QuestionQueryDTO questionQueryDTO) {log.info("查询题库列表,questionQueryDTO:{}", questionQueryDTO);return getTableDataInfo(questionService.list(questionQueryDTO));}
开始测试
数据库中随便插入一些数据测试
用ai生成数据
注意这里没有勾选pageNum的话,才会触发默认值1,如果勾选的话,但是没有输入值,就表示传的值是null,而不会触发默认值
1.7 前端设计
question.vue
<template><el-form inline="true"><el-form-item><selector placeholder="请选择题⽬难度" style="width: 200px;"></selector></el-form-item><el-form-item><el-input placeholder="请您输⼊要搜索的题⽬标题" /></el-form-item><el-form-item><el-button plain>搜索</el-button><el-button plain type="info">重置</el-button><el-button plain type="primary" :icon="Plus">添加题⽬</el-button></el-form-item></el-form><el-table height="526px" :data="questionList"><el-table-column prop="questionId" width="180px" label="题⽬id" /><el-table-column prop="title" label="题⽬标题" /><el-table-column prop="difficulty" label="题⽬难度" width="90px"><template #default="{ row }"><div v-if="row.difficulty === 1" style="color:#3EC8FF;">简单</div><div v-if="row.difficulty === 2" style="color:#FE7909;">中等</div><div v-if="row.difficulty === 3" style="color:#FD4C40;">困难</div></template></el-table-column><el-table-column prop="createName" label="创建⼈" width="140px" /><el-table-column prop="createTime" label="创建时间" width="180px" /><el-table-column label="操作" width="100px" fixed="right"><template #default="{ row }"><el-button type="text">编辑</el-button><el-button type="text" class="red">删除</el-button></template></el-table-column></el-table>
</template><script setup>
import { Plus } from "@element-plus/icons-vue";
import Selector from "@/components/QuestionSelector.vue";
import { reactive } from "vue";
const questionList = reactive([{questionId: "1",title: "题⽬1",difficulty: 1,createName: "超级管理员",createTime: "2024-05-30 17:00:00",},{questionId: "2",title: "题⽬2",difficulty: 2,createName: "超级管理员",createTime: "2024-05-30 17:00:00",},{questionId: "3",title: "题⽬1",difficulty: 3,createName: "超级管理员",createTime: "2024-05-30 17:00:00",},
]);
</script>
QuestionSeletor.vue
<template><el-select><el-option v-for="item in difficultyList" :label="item.difficultyName"/></el-select>
</template>
<script setup>
import { reactive } from 'vue'
const difficultyList = reactive([{"difficulty": 1,"difficultyName": "简单",},{"difficulty": 2,"difficultyName": "中等",},{"difficulty": 3,"difficultyName": "困难",}
])
</script>
这个QuestionSeletor.vue其实就是那个题目难度选择框,因为可能会在其他地方也用到,所以我们提取出来
页面级的组件就用在views下,小组件就放在components下
还有css代码没有拷贝过来
我们把css样式也给提取出来了
为什么提取出来呢
因为题目管理,用户管理的表格的样式都是一样的
我们把公共的样式放在assets目录下,创建一个文件main.scss
.el-table {th {background-color: #32C5FF !important;color: #ffff;}
}.el-button--primary {color: #32c5ff !important;
}.el-button--text {color: #32c5ff !important;&.red span{color: #FD4C40;}
}.el-pagination {margin-top: 20px;justify-content: flex-end;.el-pager li.is-active {background-color: #32c5ff !important;}
}
1.8 快捷键美化样式
代码格式化:Shift+Alt + F
1.9 前端开发
Plus是图标添加
Selector是自定义的组件
但是我们还没有加载
import "@/assets/main.scss"
我们在main.js里面输入这个代码就可以了
然后开始分析代码
el-from是表单组件
el-table是表格组件
上面是表单
下面是表格
<el-form-item><selector placeholder="请选择题⽬难度" style="width: 200px;"></selector></el-form-item>
这个就是我们自定义的组件QuestionSelector.vue
el-select是选择器
<el-select><el-option v-for="item in difficultyList" :label="item.difficultyName" /></el-select>
v-for是遍历数组的指令
:label就是选择器显示的内容
<el-button plain>搜索</el-button>
plain属性的作用就是光标移到那里显示更明显好看
plain 属性的主要目的是在需要弱化按钮视觉强度的场景下使用,比如在表单中作为辅助操作按钮,或者在已经有主要操作按钮的情况下,突出主次关系
<el-form inline="true">
inline="true"表示为行内表单模式,如果没有这个,表单就是竖着展示的
我们去掉某个属性就知道这个属性的作用了
<el-table-column prop="difficulty" label="题⽬难度" width="90px"><template #default="{ row }"><div v-if="row.difficulty === 1" style="color:#3EC8FF;">简单</div><div v-if="row.difficulty === 2" style="color:#FE7909;">中等</div><div v-if="row.difficulty === 3" style="color:#FD4C40;">困难</div></template></el-table-column>
#default="{ row }"中的row表示这一行的数据
1.10 交互逻辑书写
在apis里面创建question.js
import service from "@/utils/request";export function getQuestionListService(params) {return service({url: "/question/list",method: "get",params,});
}
const questionList = ref([]);const paranms = reactive({pageNum: 1,pageSize: 10,title: '',difficulty : null
});async function getQuestionList() {const res = await getQuestionListService(paranms);console.log(res);questionList.value = res.rows;
}getQuestionList();
这样就成功了
<el-table height="526px" :data="questionList">
前端这里是会自动调用data属性的
1.11 分页动态展示数据
先高搞出一个分页器
<el-pagination background layout="prev, pager, next" :total="1000" />
因为多个管理都有分页器,所以把分页器的样式加在公共的main.scss中
.el-pagination {margin-top: 20px;justify-content: flex-end;.el-pager li.is-active {background-color: #32c5ff !important;}
}
查看官网,加上size属性就可以把分页器变小了
<el-pagination background size="small" layout="prev, pager, next" :total="1000" />
注意使用这个size的前提是elementplus的版本要大于2.7.6
npm uninstall element-plus
npm install element-plus
删除在重新安装一下就可以更新版本了
看这个示例
<el-pagination background size="small" layout="total, sizes, prev, pager, next, jumper" :total="1000" />
怎么中文变英文呢
这里就可以改变了
import ElementPlus from 'element-plus'
import zhCn from 'element-plus/es/locale/lang/zh-cn'app.use(ElementPlus, {locale: zhCn,
})
粘贴到main.js里面就可以了
这样就可以变中文了
总共条数怎么显示呢
就是配置total属性
const total = ref(0);async function getQuestionList() {const res = await getQuestionListService(paranms);console.log("获取题目管理表单数据:",res);questionList.value = res.rows;total.value = res.total;
}
<el-pagination background size="small" layout="total, sizes, prev, pager, next, jumper" :total="total" />
这样就可以了
然后是分页选项,我们要5,10,15,20,30
<el-pagination background size="small" :page-sizes="[5, 10, 15, 20 , 30]" layout="total, sizes, prev, pager, next, jumper" :total="total" />
但是我们切换pageNUm和pageSize就没有反应
页数会根据总数和每页数而自动变化,没有问题,问题就是不切换
调整之后就要向后端发送数据
就是@click,监听点击事件
用这两个就可以了
<el-pagination background size="small" :page-sizes="[5, 10, 15, 20 , 30]" layout="total, sizes, prev, pager, next, jumper" :total="total"@size-change="handleSizeChange"@current-change="handleCurrentChange"/>
触发这两个函数的时候,会自动传一个参数,就是修改后的页数或者每页数
function handleSizeChange(newSize) {paranms.pageSize = newSize;getQuestionList();
}
function handleCurrentChange(newPage) {paranms.pageNum = newPage;getQuestionList();
}
现在还有一个问题就是切换pageSize的时候,如果上一次是第二页,那么切换之后还是第二页开始看的
切换pageSize之后我们应该默认从第一页看,开始看
让pageNum变为1就可以了
然后请求就可以了,对应也要切换到第一页显示
用v-model来绑定就可以了
然后拷贝示例里面的代码就可以了
<el-pagination background size="small" :page-sizes="[5, 10, 15, 20 , 30]" layout="total, sizes, prev, pager, next, jumper" :total="total"@size-change="handleSizeChange"@current-change="handleCurrentChange"v-model:current-page="paranms.pageNum"/>
function handleSizeChange(newSize) {paranms.pageSize = newSize;paranms.pageNum = 1;getQuestionList();
}
这样就行了
所以
function handleCurrentChange(newPage) {// paranms.pageNum = newPage;getQuestionList();
}
这里就可以省略了,因为是双向绑定的,可以随着变化的
所以都可以双向绑定了pageSize
<el-pagination background size="small" :page-sizes="[5, 10, 15, 20 , 30]" layout="total, sizes, prev, pager, next, jumper" :total="total"@size-change="handleSizeChange"@current-change="handleCurrentChange"v-model:current-page="paranms.pageNum"v-model:page-size="paranms.pageSize"/>
function handleSizeChange(newSize) {// paranms.pageSize = newSize;paranms.pageNum = 1;getQuestionList();
}
function handleCurrentChange(newPage) {// paranms.pageNum = newPage;getQuestionList();
}
这样就OK了
<el-form-item><selector placeholder="请选择题⽬难度" v-model="paranms.difficulty" style="width: 200px;"></selector></el-form-item><el-form-item><el-input placeholder="请您输⼊要搜索的题⽬标题" v-model="paranms.title"/></el-form-item><el-form-item><el-button @click="onSearch" plain>搜索</el-button><el-button @click="onReset" plain type="info">重置</el-button><el-button plain type="primary" :icon="Plus">添加题⽬</el-button></el-form-item>
<el-select><el-option v-for="item in difficultyList" :label="item.difficultyName" :value="item.difficulty" /></el-select>
label是选择显示的内容,value则是每个选择的值
然后是上面的两个按钮
function onSearch(){paranms.pageNum = 1;getQuestionList();
}function onReset(){paranms.pageNum = 1;paranms.title = '';paranms.pageSize = 10;paranms.difficulty = '';getQuestionList();
}
这样就可以了
2. 添加题目
2.1 后端
@Data
public class QuestionAddDTO {private String title;private Integer difficulty;private Long timeLimit;private Long spaceLimit;private String content;private String questionCase;private String defaultCode;private String mainFuc;
}
@PostMapping("/add")public R<Void> add(@RequestBody QuestionAddDTO questionAddDTO) {log.info("添加题目,questionAddDTO:{}", questionAddDTO);return toR(questionService.add(questionAddDTO));}
@Overridepublic int add(QuestionAddDTO questionAddDTO) {List<Question> questions = questionMapper.selectList(new LambdaQueryWrapper<Question>().eq(Question::getTitle, questionAddDTO.getTitle()));if(CollectionUtil.isNotEmpty(questions)){throw new ServiceException(ResultCode.FAILED_ALREADY_EXISTS);}Question question = new Question();BeanUtil.copyProperties(questionAddDTO, question);return questionMapper.insert(question);}
其中 BeanUtil.copyProperties(questionAddDTO, question);是hutool的工具类,就是把questionAddDTO里面的属性赋值给question
3. 编辑题目功能
3.1 后端
这个需要先获取题目详细信息,然后是编辑
@Data
public class QuestionDetailVO {private Long questionId;private String title;private Integer difficulty;private Long timeLimit;private Long spaceLimit;private String content;private String questionCase;private String defaultCode;private String mainFuc;
}
因为编辑的时候要传入id,所以获取详细信息的时候就返回id
@GetMapping("/detail")public R<QuestionDetailVO> detail(Long questionId) {log.info("查询题目详情,questionId:{}", questionId);return questionService.detail(questionId);}
@Overridepublic R<QuestionDetailVO> detail(Long questionId) {Question question = questionMapper.selectById(questionId);if(question == null){throw new ServiceException(ResultCode.FAILED_NOT_EXISTS);}QuestionDetailVO questionDetailVO = new QuestionDetailVO();BeanUtil.copyProperties(question, questionDetailVO);return R.ok(questionDetailVO);}
这样就成功了
然后是编辑的接口
@Data
public class QuestionEditDTO extends QuestionAddDTO{private Long questionId;
}
@Overridepublic int edit(QuestionEditDTO questionEditDTO) {Question question = questionMapper.selectById(questionEditDTO.getQuestionId());if(question == null){throw new ServiceException(ResultCode.FAILED_NOT_EXISTS);}question.setTitle(questionEditDTO.getTitle());question.setDifficulty(questionEditDTO.getDifficulty());question.setTimeLimit(questionEditDTO.getTimeLimit());question.setSpaceLimit(questionEditDTO.getSpaceLimit());question.setContent(questionEditDTO.getContent());question.setQuestionCase(questionEditDTO.getQuestionCase());question.setDefaultCode(questionEditDTO.getDefaultCode());question.setMainFuc(questionEditDTO.getMainFuc());return questionMapper.updateById(question);}
edit 方法:直接使用 copyProperties 可能会导致以下问题:
参数注入风险:攻击者可能会通过 DTO 传递一些不允许修改的字段,比如 createTime、status 等。
空值覆盖问题:DTO 中没有设置的字段可能会被置为 null,而这并非是我们期望的结果。
一般来说,使用BeanUtil.copyProperties,都是大的转小的
在 “小对象转大对象” 的场景中,若目标对象原本已有属性值,而源对象中没有对应的属性,那么这些属性值会被重置为默认值。
User user = new User();
user.setId(1L);
user.setName("Alice");
user.setAge(20);UserForm form = new UserForm();
form.setName("Bob");BeanUtil.copyProperties(form, user); // user.age 变为 null!
所以一般是大的转小的,不要小的转大的,不然好多字段会变为null
@Overridepublic void updateFill(MetaObject metaObject) {log.info("开始更新填充...");this.strictUpdateFill(metaObject, "updateBy", Long.class, 1L);this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());}
然后再mybatis中增加这个,也是自动填充字段
这样就成功了
4. 删除题目
4.1 后端
@DeleteMapping("/delete")public R<Void> delete(Long questionId) {log.info("删除题目,questionId:{}", questionId);return toR(questionService.delete(questionId));}
@Overridepublic int delete(Long questionId) {Question question = questionMapper.selectById(questionId);if(question == null){throw new ServiceException(ResultCode.FAILED_NOT_EXISTS);}return questionMapper.deleteById(question);}