typora-copy-images-to: img
面面项目第三天
今日目标
- 能够完成基础题库列表展示
- 能够完成学科的初始化
- 能够完成行业方向的初始化
- 能够完成数据字典(城市)的初始化
- 能够完成企业的初始化
- 能够完成题目新增
第一章-题库管理分析
知识点-题库管理模块分析
1.目标
- 知道题库管理的需求
2.路径
- 需求分析
- 涉及到的表
3.讲解
3.1需求分析
基础题库管理是普通录入人员操作的功能,新的题目先经过基础题库录入,加入精选后方可进入精选题库列表,此时题目的状态默认是待审核状态,有操作精选题库权限的用户,可以审核题目。
基础题库与精选题库,从数据库上都来源于同一张主表t_question,只是题目的类型有所不同。区分两种题库主要是通过is_classic字段来区分,is_classic为0是基础题目,为1是精选题目。题目状态是通过status区分,审核状态靠review_status来区分。具体题目的类型、状态及审核状态,总结如下:
题目类型(is_classic):0 基础题目、1精选题目
题目状态(status): 0 待发布(待审核、已拒绝)、1 已发布(已审核)、 2 已下架(已审核)
审核状态(review_status): 0 待审核、1 审核通过、2 审核拒绝
3.2涉及到的表
题目业务涉及的表有t_user、t_question、t_question_item、t_company、t_course、t_catalog、t_tag七张表。
4.小结
第二章-基础题库
案例-基础题库列表展示
1.需求
2.分析
2.1业务分析
基础题目列表中,需要展示创建者、使用次数及学科名称,故需要三表连接,列表中还需要显示使用次数,需要使用嵌套子查询统计题目被用户使用题的次数。基础题目列表需要分页,所有需要使用相同的条件查询总记录数和数据集。根据页面分析,查询需要组合的条件很多,可以使用QueryPageBean的queryParams来动态组合查询。试题编号规则采用从1000+主键ID.
2.2数据模型
2.2.1请求参数格式
- 前端页面
{
"currentPage": 1,
"pageSize": 10,
"queryParams": {
"courseId": 24,
"difficulty": 2,
"type": 5,
"keyWord": "Java"
}
}
- 后台使用QueryPageBean接收
public class QueryPageBean implements Serializable{
private Integer currentPage; // 页码
private Integer pageSize; //每页记录数
private Map queryParams; //查询条件
}
2.2.2响应数据格式
- 前端页面
{
"flag": true,
"message": "获取题目列表成功",
"result": {
"rows": [{
"courseName": "java",
"createDate": "2019-08-08 00:00:00.0",
"creator": "admin",
"difficulty": 4,
"id": 11,
"number": "",
"status": 0,
"subject": "<p>反爬虫措施?</p>\r\n",
"type": 3,
"usedQty": "0"
},
{
"courseName": "java",
"createDate": "2019-08-08 00:00:00.0",
"creator": "admin",
"difficulty": 5,
"id": 12,
"number": "",
"status": 0,
"subject": "<p>加入Redis里面有1亿个key,其中10w个key是以某个固定的一直的前缀开头的,如何将他们全部找出来。</p>\r\n",
"type": 3,
"usedQty": "0"
}
],
"total": 20
}
}
- 后台使用Result
public class Result implements java.io.Serializable {
private boolean flag;//执行结果,true为执行成功 false为执行失败
private String message;//返回结果信息
private Object result;//返回数据
}
- Result里面的Object result有两个属性(total,rows), 所以我们创建PageResult对象 给Object result赋值
public class PageResult implements Serializable{
private Long total;//总记录数
private List rows;//当前页结果
}
2.3思路分析
2.3.1加载所有学科思路
- 在questionBasicList.html的initCourses()里面, 发送Ajax请求CourseController
- 在CourseController里面创建findAll()方法
//1.调用业务 获得学科的列表 List<Course> list
//2.封装, 响应
- 在CourseService里面创建findAll
public List<Course> findAll(){
//调用Dao
}
- 在CourseDao查询所有的学科
2.3.2基础题库列表思路
- 在questionBasicList.html的getList()里面, 发送Ajax请求QuestionController ,携带请求参数
{
"currentPage": 1,
"pageSize": 10,
"queryParams": {
"courseId": 1,
"difficulty": 1,
"keyWord": "String",
"type": 1
}
}
- 在QuestionController 里面创建findListByPage()方法
//1.获得请求参数 封装成QueryPageBean
//2.调用业务 获得分页的数据 PageResult
//3.把PageResult封装成Result 响应
- 在QuestionService创建
public PageResult findListByPage(QueryPageBean queryPageBean){
//调用dao,封装PageResult
}
- 在QuestionDao
查询题目的总数量(带条件)
查询题目一页展示的List
3.实现
3.1加载所有学科实现
3.1.1前端
- questionBasicList.html
//获取所有学科信息,初始化学科下拉列表
initCourses() {
let t = this;
// 必传参数
let params = {
status: 0
};
//1. 发送异步请求,获取所有学科列表
axios.get("/course/findAll.do?status="+status).then(response=>{
if (response.data.flag) {
//获取学科列表成功
this.$message({
type:"success",
message:response.data.message,
showClose:true
})
// 将响应数据设置给数据模型
this.courses = response.data.result
}else {
//获取学科列表失败
this.$message({
type:"error",
message:response.data.message,
showClose:true
})
}
})
}
3.1.2后台
- CourseController
@RequestMapping("/course/findAll")
public void findAll(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
//1. 获取请求参数status
String status = request.getParameter("status");
//调用业务层的方法,查询所有学科信息
List<Course> courseList = courseService.findAll(status);
//封装响应结果
JsonUtils.printResult(response,new Result(true,"获取所有学科成功",courseList));
} catch (Exception e) {
e.printStackTrace();
JsonUtils.printResult(response,new Result(false,"获取所有学科失败"));
}
}
- CourseService
public List<Course> findAll(String status) throws Exception {
SqlSession sqlSession = SqlSessionFactoryUtils.openSqlSession();
CourseDao courseDao = sqlSession.getMapper(CourseDao.class);
List<Course> courseList = courseDao.findAll(status);
SqlSessionFactoryUtils.commitAndClose(sqlSession);
return courseList;
}
- CourseDao
List<Course> findAll();
- CourseDao.xml
<!--进行一对多的分步查询映射-->
<resultMap id="courseCatalogMap" type="Course">
<id property="id" column="id"></id>
<!--一对多使用collection标签-->
<!--查询二级目录列表-->
<collection property="catalogList" ofType="Catalog" column="id"
select="com.itheima.mm.dao.CatalogDao.findCatalogListByCourseId"></collection>
</resultMap>
<select id="findAll" resultMap="courseCatalogMap" parameterType="string">
select id,name from t_course
<where>
<if test="status != null and status != '' and status != 'null'">
isShow=#{status}
</if>
</where>
</select>
- CatalogDao
public interface CatalogDao {
List<Catalog> findCatalogListByCourseId(int courseId);
}
- CatalogDao.xml
<select id="findCatalogListByCourseId" resultType="Catalog" parameterType="int">
select * from t_catalog where courseId=#{courseId}
</select>
3.2基础题库列表实现
3.2.1前端
- questionBasicList.html
//查询试题分页列表
getList() {
let t = this;
// 必传参数
let params = {
currentPage: t.pagination.pageNum,//当前页数
pageSize: t.pagination.pageSize//每页条数
};
// 选传参数
// 搜索条件
let queryParams = {};
let courseId = t.requestParameters.courseId;
//如果选择了学科,就将学科id放到搜索条件中
if (courseId !== '') {
queryParams.courseId = courseId;
}
let difficulty = t.requestParameters.difficulty;
//如果选择了难度,就将难度放到搜索条件中
if (difficulty !== '') {
queryParams.difficulty = difficulty;
}
let type = t.requestParameters.type;
//如果选择了提醒,就将提醒放到搜索条件中
if (type !== '') {
queryParams.type = type;
}
let keyWord = t.requestParameters.keyWord;
//如果输入了搜索关键字,就将关键字放到搜索条件中
if (keyWord !== '') {
queryParams.keyWord = keyWord;
}
//如果搜索条件中有内容,那么就将搜索条件添加到请求参数中
if (Object.keys(queryParams).length) {
params.queryParams = queryParams;
}
console.log("基础题库列表请求参数:");
console.log(params);
//发送异步请求
axios.post("/question/findBasicByPage.do",params).then(response=>{
if (response.data.flag) {
//获取基础题库列表成功
//赋值操作
this.pagination.total = response.data.result.total
this.items = response.data.result.rows
}else {
this.$message({
type:"error",
message:response.data.message,
showClose:true
})
}
})
}
3.2.2后台
- QuestionController
@RequestMapping("/question/findBasicByPage")
public void findBasicByPage(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
//1. 获取请求参数,封装到QueryPageBean对象中
QueryPageBean queryPageBean = JsonUtils.parseJSON2Object(request, QueryPageBean.class);
//2. 调用业务层的方法,进行分页查询
PageResult pageResult = questionService.findBasicByPage(queryPageBean);
JsonUtils.printResult(response,new Result(true,"获取基础题库成功",pageResult));
} catch (Exception e) {
e.printStackTrace();
JsonUtils.printResult(response,new Result(false,"获取基础题库失败"));
}
}
- QuestionService
public PageResult findBasicByPage(QueryPageBean queryPageBean) throws Exception {
SqlSession sqlSession = SqlSessionFactoryUtils.openSqlSession();
QuestionDao questionDao = sqlSession.getMapper(QuestionDao.class);
//获取总条数
Long total = questionDao.findTotalBasicCount(queryPageBean);
//获取当前页数据集合
List<Question> basicList = questionDao.findBasicPageList(queryPageBean);
SqlSessionFactoryUtils.commitAndClose(sqlSession);
return new PageResult(total,basicList);
}
- QuestionDao
Long findTotalBasicCount(QueryPageBean queryPageBean);
List<Question> findBasicPageList(QueryPageBean queryPageBean);
- 映射配置文件
<sql id="select_where">
<if test="queryParams != null">
<if test="queryParams.courseId != null">
and courseId=#{queryParams.courseId}
</if>
<if test="queryParams.difficulty != null">
and difficulty=#{queryParams.difficulty}
</if>
<if test="queryParams.keyWord != null">
and subject like concat("%",#{queryParams.keyWord},"%")
</if>
<if test="queryParams.type != null">
and type=#{queryParams.type}
</if>
</if>
</sql>
<select id="findTotalBasicCount" resultType="Long" parameterType="QueryPageBean">
select count(*) from t_question
where isClassic=0
<include refid="select_where"></include>
</select>
<select id="findBasicPageList" resultType="Question" parameterType="QueryPageBean">
select
10000+id number,type,subject,createDate,difficulty,
(select name from t_course where id=q.courseId) courseName,
(select username from t_user where id=q.userId) creator,
(select count(*) from tr_member_question where questionId=q.id) usedQty
from t_question q
where isClassic=0
<include refid="select_where"></include>
limit #{offset},#{pageSize}
</select>
4.小结
4.1数据模型
4.1.1请求参数格式
- 前端页面
{
"currentPage": 1,
"pageSize": 10,
"queryParams": {
"courseId": 24,
"difficulty": 2,
"type": 5,
"keyWord": "Java"
}
}
- 后台使用QueryPageBean接收
public class QueryPageBean implements Serializable{
private Integer currentPage; // 页码
private Integer pageSize; //每页记录数
private Map queryParams; //查询条件
}
4.1.2响应数据格式
- 前端页面
{
"flag": true,
"message": "获取题目列表成功",
"result": {
"rows": [{
"courseName": "java",
"createDate": "2019-08-08 00:00:00.0",
"creator": "admin",
"difficulty": 4,
"id": 11,
"number": "",
"status": 0,
"subject": "<p>反爬虫措施?</p>\r\n",
"type": 3,
"usedQty": "0"
},
{
"courseName": "java",
"createDate": "2019-08-08 00:00:00.0",
"creator": "admin",
"difficulty": 5,
"id": 12,
"number": "",
"status": 0,
"subject": "<p>加入Redis里面有1亿个key,其中10w个key是以某个固定的一直的前缀开头的,如何将他们全部找出来。</p>\r\n",
"type": 3,
"usedQty": "0"
}
],
"total": 20
}
}
- 后台使用Result
public class Result implements java.io.Serializable {
private boolean flag;//执行结果,true为执行成功 false为执行失败
private String message;//返回结果信息
private Object result;//返回数据
}
- Result里面的Object result有两个属性(total,rows), 所以我们创建PageResult对象 给Object result赋值
public class PageResult implements Serializable{
private Long total;//总记录数
private List rows;//当前页结果
}
4.2思路
4.2.1加载所有的学科
查询所有的学科
4.2.2基础题目列表
- 前端传给后台 请求参数===> QueryPageBean
- 后台响应 new Result(true, “成功”,pageResult)
案例-基础题库新增
1.需求
新增题目页面需要先初始化页面的数据(学科、学科目录及标签、公司、省市列表、行业方向),然后再提交数据进行保存,保存数据需要涉及大约九张表的更新操作。题目选项在单选与复选时可以上传图片,图片需要考虑上传但未保存数据库服务器垃圾图片的处理问题。
2.分析
2.1初始化页面数据分析
2.1.1初始化学科
页面选择学科时,学科目录及学科标签列表也随之发生改变。所以需要在初始化学科数据时,与之对应的学科目录及标签数据同时获取。 涉及到的表 t_course, t_catalog, t_tag
- 在questionEditor.html的initCourses(), 发送ajax请求CourseController
- 在CourseController创建findListAll()方法
//1.调用业务 获得List<Course> list
//2.封装Result 响应
- 在CourseService里面创建findListAll()方法, 调用Dao
- 在CourseDao, 使用ResultMap封装 使用collocation标签
注意: 根据学科,查询出学科对应的目录数据,以及学科对应的标签数据
2.2数据保存分析
页面初始化已完成,用户根据页面进行数据输入,输入元素包含单选、复选、简单,其中单选、复选选项可以上传图片,如果不上传图片,页面中输入元素使用的是富文本编辑器输入文本。所谓富文本编辑器是指可以编写带有复杂样式的文本,数据保存到数据库中内容是带有html标签样式的。
选择学科及学科目录,选择不同的学科,与之对应的标签列表也会不同;
选择公司后,与之对应的省市、行业方向是自动选择,同时可以再次编辑后与题目一起提交;
可以为当前题目设置多个标签,标签列表是根据当前选择的学科变化而变化的。
根据以上分析,保存题目数据的同时,需要更新题目数据、题目选项数据、标签数据、公司数据,故实现本小结功能,我们先把流程业务走通,把数据从前端页面传递控制器,从控制器传递到Service,Service先保证传递到后端的数据是正确的,然后再分步完成数据的保存。
- 提交的参数
{
"id": 0,
"is_classic": null,
"courseId": 1,
"catalogId": 1,
"companyId": 1,
"cityIds": [2, 10],
"industryIds": [1, 2, 18, 26, 27],
"type": 2,
"difficulty": 3,
"subject": "<p>测试题目</p>",
"questionItemList": [{
"id": 0,
"content": "a",
"isRight": true,
"imgUrl": ""
}, {
"id": 0,
"content": "b",
"isRight": false,
"imgUrl": ""
}, {
"id": 0,
"content": "c",
"isRight": false,
"imgUrl": ""
}, {
"id": 0,
"content": "d",
"isRight": false,
"imgUrl": ""
}, {
"id": 0,
"content": "e",
"isRight": false,
"imgUrl": ""
}, {
"id": 0,
"content": "f",
"isRight": false,
"imgUrl": ""
}],
"analysis": "<p>a答案正确</p>",
"analysisVideo": "http://www.baidu.com",
"remark": "测试题目",
"tagIds": [1, 3]
}
2.2.1保存/更新题目信息
题目的主体数据(题干,难度,题目类型) 保存到t_question, 如果选项Id为0,即为保存,非0为更新。
2.2.2保存/更新题干选项
题目选项数据保存到t_question_item表中,如果选项Id为0,即为保存,非0为更新。
2.2.3保存/更新标签信息
题目与学科标签是多对多的关系,需要使用关系表tr_question_tag来保存题目与学科标签的关系。当前业务需要考虑题目的新建与更新,对于的标签关系也是如此。如果标签关系发生改变,需要同时删除旧关系,新增新关系的方式来实现。
- questionEditor.html的createItem()函数里面 请求QuestionController
- 在QuestionController里面创建addOrUpdate()
//1.获得请求参数 封装成Question
//2.获得用户id
//3.调用业务 进行新增或者更新
//4.响应
- 在QuestionService里面创建addOrUpdate()
public void addOrUpdate(Question question){
//1.保存或者更新题目信息
//2.保存或者更新题目选项选项
//3.维护题目和标签的关系
}
- QuestionDao, QuestionItemDao, TagDao
3.实现
3.1初始化页面数据实现
3.1.1初始化学科
前端
- questionEditor.html
//获取所有学科,初始化学科下拉列表
initCourses() {
let t = this;
//发送异步请求,获取所有学科数据
axios.get("/course/findAll.do").then(response=>{
if (response.data.flag) {
//获取成功
this.courses = response.data.result
}else {
//获取失败
this.$message({
type:"error",
message:response.data.message,
showClose:true
})
}
})
}
后台
- CourseController(不需要修改)
@RequestMapping("/course/findAll")
public void findAll(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
//1. 获取请求参数status
String status = request.getParameter("status");
//调用业务层的方法,查询所有学科信息
List<Course> courseList = courseService.findAll(status);
//封装响应结果
JsonUtils.printResult(response,new Result(true,"获取所有学科成功",courseList));
} catch (Exception e) {
e.printStackTrace();
JsonUtils.printResult(response,new Result(false,"获取所有学科失败"));
}
}
- CourseService(不需要修改)
public List<Course> findAll(String status) throws Exception {
SqlSession sqlSession = SqlSessionFactoryUtils.openSqlSession();
CourseDao courseDao = sqlSession.getMapper(CourseDao.class);
List<Course> courseList = courseDao.findAll(status);
SqlSessionFactoryUtils.commitAndClose(sqlSession);
return courseList;
}
CatalogDao
TagDao
public interface TagDao {
List<Tag> findTagListByCourseId(int courseId);
}
<select id="findTagListByCourseId" resultType="Tag" parameterType="int">
select * from t_tag where courseId=#{courseId}
</select>
- CourseDao (添加一个外部查询)
<!--进行一对多的分步查询映射-->
<resultMap id="courseCatalogMap" type="Course">
<id property="id" column="id"></id>
<!--一对多使用collection标签-->
<!--查询二级目录列表-->
<collection property="catalogList" ofType="Catalog" column="id"
select="com.itheima.mm.dao.CatalogDao.findCatalogListByCourseId"></collection>
<!--查询标签列表-->
<collection property="tagList" ofType="Tag" column="id"
select="com.itheima.mm.dao.TagDao.findTagListByCourseId"></collection>
</resultMap>
<select id="findAll" resultMap="courseCatalogMap" parameterType="string">
select id,name from t_course
<where>
<if test="status != null and status != '' and status != 'null'">
isShow=#{status}
</if>
</where>
</select>
3.2数据保存实现
- questionEditor.html
// 提交表单
createItem() {
// 表单校验
let isValid = false;
this.$refs['formData'].validate((valid) => {
isValid = valid;
});
// 如果表单校验不通过,直接终止
if (!isValid) {
return;
}
// 封装请求参数
let formData = this.formData;
// 包装企业相关数据
let industrys = this.industrys;
let industryList = [];
formData.industryIds.forEach(industryId => {
let industry = industrys.find(industry => {
return industry.id === industryId;
});
// TODO 会有新增的方向
//industryList.push(industry);
if(industry){
industryList.push(industry);
}else{
industryList.push({
id:0,
name:industryId
});
}
});
let company = {
id: formData.companyId,
cityId: formData.cityIds[1],
industryList: industryList
};
formData.company = company;
// 包装选项相关数据
let questionItemList = this.formData.questionItemList;
questionItemList.forEach(val => {
val.isRight = val.isRight ? 1 : 0;
});
// 包装标签相关数据
let tags = this.tags;
let tagList = [];
formData.tagIds.forEach(tagId => {
let tag = tags.find(tag => {
return tag.id === tagId;
});
// TODO 会有新增的标签
//tagList.push(tag);
if(tag){
tagList.push(tag);
}else{
tagList.push({
id:0,
name:tagId
});
}
});
formData.tagList = tagList;
console.log("新增更新题库请求参数:");
console.log(this.formData);
//到这里为止,请求参数已经弄好了
//发送异步请求提交请求参数
axios.post("/question/add.do",formData).then(response=>{
if (response.data.flag) {
this.$message({
type:"success",
message:response.data.message,
showClose:true
})
// 返回到上一级
if (!this.formData.is_classic) {
setTimeout(function () {
window.location.href = "questionBasicList.html";
}
, 1000);
} else {
setTimeout(function () {
window.location.href = "questionClassicList.html";
}, 1000);
}
}else {
this.$message({
type:"error",
message:response.data.message,
showClose:true
})
}
})
}
- QuestionController
@RequestMapping("/question/add")
public void addQuestion(HttpServletRequest request, HttpServletResponse response) throws IOException {
try {
//1. 获取请求参数,并且封装到Question对象中
Question question = JsonUtils.parseJSON2Object(request, Question.class);
//手动设置缺失的数据: status,reviewStatus,createDate,userId
question.setStatus(Constants.QUESTION_PRE_PUBLISH);
question.setReviewStatus(Constants.QUESTION_PRE_REVIEW);
question.setCreateDate(DateUtils.parseDate2String(new Date()));
User user = (User) request.getSession().getAttribute(Constants.LOGIN_USER);
question.setUserId(user.getId());
//2. 调用业务层的方法,添加题目信息
questionService.add(question);
JsonUtils.printResult(response,new Result(true,"添加试题成功"));
} catch (Exception e) {
e.printStackTrace();
JsonUtils.printResult(response,new Result(false,"添加试题失败"));
}
}
- QuestionService
public void add(Question question){
SqlSession sqlSession = null;
try {
sqlSession = SqlSessionFactoryUtils.openSqlSession();
QuestionDao questionDao = sqlSession.getMapper(QuestionDao.class);
//1. 将t_question表相关的数据存储到t_question表
questionDao.add(question);
//2. 判断选项的集合是否为空,如果不为空将题目的选项信息,存储到t_question_item表
QuestionItemDao questionItemDao = sqlSession.getMapper(QuestionItemDao.class);
List<QuestionItem> questionItemList = question.getQuestionItemList();
if (questionItemList != null && questionItemList.size() > 0) {
for (QuestionItem questionItem : questionItemList) {
//手动设置该选项所属的question的id
questionItem.setQuestionId(question.getId());
//调用dao层的方法,存储每一个题目的选项信息
questionItemDao.add(questionItem);
}
}
//3. 将题目和标签的关联信息,存储到tr_question_tag表中
//获取该题目的所有标签
List<Tag> tagList = question.getTagList();
if (tagList != null && tagList.size() > 0) {
for (Tag tag : tagList) {
Map paramMap = new HashMap<>();
paramMap.put("questionId",question.getId());
paramMap.put("tagId",tag.getId());
//进行关联
questionDao.addQuestionTag(paramMap);
}
}
SqlSessionFactoryUtils.commitAndClose(sqlSession);
} catch (IOException e) {
e.printStackTrace();
SqlSessionFactoryUtils.rollbackAndClose(sqlSession);
}
}
- QuestionDao
<insert id="add" parameterType="Question">
insert into t_question(
subject,type,difficulty,analysis,analysisVideo,
remark,isClassic,status,reviewStatus,createDate,
userId,companyId,catalogId,courseId
)
values (
#{subject},#{type},#{difficulty},#{analysis},#{analysisVideo},
#{remark},#{isClassic},#{status},#{reviewStatus},#{createDate},
#{userId},#{companyId},#{catalogId},#{courseId}
)
<!--获取自增长id-->
<selectKey keyProperty="id" keyColumn="id" resultType="int" order="AFTER">
select last_insert_id()
</selectKey>
</insert>
<insert id="addQuestionTag" parameterType="map">
insert into tr_question_tag values(#{questionId},#{tagId})
</insert>
- QuestionItemDao
<insert id="add" parameterType="QuestionItem">
insert into t_question_item(content,imgUrl,isRight,questionId)
values (#{content},#{imgUrl},#{isRight},#{questionId})
</insert>
4.小结
全部完成后,测试题目数据保存的过程,可以使用如下查询,查询数据是否正确保存。
#查询题目
SELECT * from t_question order by id desc;
#查询题目选项
SELECT * from t_question_item order by question_id desc;
案例-图片上传
6.1 功能分析
题目的单选或复选,可以上传图片作为选项,页面通过后端提供上传图片组件来完成单个图片上传,图片暂时上传到服务端指定的目录保存。图片上传成功后,后端返回一个访问的相对路径(包含图片上传后的新名称),这个路径需要在表单最终提交时,与其他表单数据提交到服务端与题目选项数据一起保存到数据表中。
6.2 实现思路
后端需要提供一个单独的上传图片组件,然后返回给客户端一个可以保持到数据库的相对路径,另外对上传的图片要重新命名,保证图片名称不冲突。上传图片组件将放在一个名为公共控制器中,本图片上传不涉及其他操作,故不需要Service和Dao。
图片上传涉及到底层IO流的操作,本节将使用三方组件commons-fileupload组件来完成图片上传的功能。
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
6.3 知识点补充
6.3.1 文件上传表单分析
- 凡是表单带上传文件,表单提交类型必须是【 enctype=”multipart/form-data”】
- 必须从底层IO流获取原始数据,然后进行数据解析,获取上传的数据
- 通过三方组件commons-fileupload(依赖commons-io)来解析IO流数据
6.3.2 commons组件
commons-IO组件
- IOUtils类
- FileUtils类
commons-fileupload组件
- DiskFileItemFactory 类
- ServletFileUpload 类
- FileItem 类
6.4 实现上传组件
考虑到上传组件的复用性,在完成时考虑同时批量上传的情况。
6.4.1 初始化上传文件路径配置
在webapp下创建img/upload目录
如图所示:
6.4.2 新增CommonController
package com.itheima.mm.controller;
import com.itheima.framework.anno.Controller;
import com.itheima.framework.anno.RequestMapping;
import com.itheima.mm.entry.Result;
import com.itheima.mm.utils.JsonUtils;
import com.itheima.mm.utils.UploadUtils;
import org.apache.commons.fileupload.FileItem;
import org.apache.commons.fileupload.disk.DiskFileItemFactory;
import org.apache.commons.fileupload.servlet.ServletFileUpload;
import org.apache.commons.io.IOUtils;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.List;
/**
* 包名:com.itheima.mm.controller
*
* @author Leevi
* 日期2020-08-04 14:38
*/
@Controller
public class CommonController {
@RequestMapping("/common/uploadFile")
public void uploadFile(HttpServletRequest request, HttpServletResponse response) throws IOException {
InputStream is = null;
FileOutputStream os = null;
try {
//1. 获取客户端上传的图片
//创建磁盘工厂对象
DiskFileItemFactory itemFactory = new DiskFileItemFactory();
//创建Servlet的上传解析对象,构造方法中,传递磁盘工厂对象
ServletFileUpload fileUpload = new ServletFileUpload(itemFactory);
/*
* fileUpload调用方法 parseRequest,解析request对象
* 页面可能提交很多内容 文本框,文件,菜单,复选框 是为FileItem对象
* 返回集合,存储的文件项对象
*/
List<FileItem> list = fileUpload.parseRequest(request);
String imgUrl = null;
for (FileItem fileItem : list) {
if (!fileItem.isFormField()) {
//是上传的文件
//获取文件名
String fileName = fileItem.getName();
//获取上传的文件
is = fileItem.getInputStream();
//创建一个输出流,输出文件到指定的目录
//使用字节输出流输出到那个文件夹(img/upload)
String uploadPath = request.getServletContext().getRealPath("img/upload");
//创建一个File
File file = new File(uploadPath);
if (!file.exists()) {
//这个目录不存在
//将这个目录创建出来
file.mkdirs();
}
//文件名会不会重复呢????最好的办法是生成唯一的文件名
String uuidName = UploadUtils.getUUIDName(fileName);
os = new FileOutputStream(new File(file, uuidName));
//将输入流中的文件,通过输出流写到指定的目录中
IOUtils.copy(is, os);
//获取文件的存储路径imgUrl
imgUrl = "img/upload/" + uuidName;
}
}
JsonUtils.printResult(response, new Result(true, "图片上传成功", imgUrl));
} catch (Exception e) {
e.printStackTrace();
JsonUtils.printResult(response, new Result(true, "图片上传失败"));
} finally {
is.close();
os.close();
}
}
}
6.4.3 编写前端代码
在questionEditor.html页面中,找到handleHttpRequest方法,加入axios请求。
//文件上传的方法
handleHttpRequest(params, index) {
let file = params.file;
//请求参数
let formData = new FormData();
//键值对: key就是文件名、value就是文件
formData.append(file.name, file);
//文件上传一定是发送post请求,而且一定要设置请求参数的类型(表单数据类型)
axios.defaults.headers.post['Content-Type'] = 'multipart/form-data;charse=UTF-8';
axios.post("/common/uploadFile.do",formData).then(response=>{
if (response.data.flag) {
//文件上传成功
this.$message({
type:"success",
message:response.data.message,
showClose:true
})
//1. 将响应的数据(文件存储路径),设置给选项的imgUrl属性
this.formData.questionItemList[index].imgUrl = response.data.result
//2. 图片回显
}else {
//文件上传失败
this.$message({
type:"error",
message:response.data.message,
showClose:true
})
}
})
}
两处el-upload
<el-upload
class="avatar-uploader"
action=""
:show-file-list="false"
:http-request="function(params) {return handleHttpRequest(params, index)}">
<img v-if="item.imgUrl" :src="'/' + item.imgUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
单独测试成功后,输入表单数据,查看数据库表中的数据是否正确,正确如图所示: