项目之热点问题和问答列表(9)
36. 热点问题-持久层
先创建封装数据的VO类:
@Data
public class QuestionListItemVO { private Integer id; private String title; private Integer status; private Integer hits;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
在持久层接口QuestionMapper
中添加抽象方法:
@Repository
public interface QuestionMapper extends BaseMapper<Question> { /** * 查询点击量最多的问题的列表 * * @return 点击量最多的问题的列表 */ List<QuestionListItemVO> findMostHits();
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
并在QuestionMapper.xml
中配置映射:
<select id="findMostHits" resultType="cn.tedu.straw.portal.vo.QuestionListItemVO"> SELECT id, title, status, hits FROM question WHERE is_public=1 AND is_delete=0 ORDER BY hits DESC, id DESC LIMIT 0, 10
</select>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
单元测试:
@Slf4j
@SpringBootTest
public class QuestionMapperTests { @Autowired QuestionMapper mapper; @Test void findMostHits() { List<QuestionListItemVO> questions = mapper.findMostHits(); log.debug("question count={}", questions.size()); for (QuestionListItemVO question : questions) { log.debug(">>> {}", question); } }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
37. 热点问题-业务层
在业务层接口IQuestionService
中添加抽象方法:
/**
* 查询点击数量最多的问题的列表,将从缓存中获取列表,如果缓存中没有数据,会从数据库中查询数据并更新缓存
*
* @return 点击数量最多的问题的列表
*/
List<QuestionListItemVO> getMostHits();
/**
* 查询点击数量最多的问题的缓存列表,当缓存被清空后,可能获取到空的列表
*
* @return 点击数量最多的问题的缓存列表
*/
List<QuestionListItemVO> getCachedMostHits();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
在QuestionServiceImpl
中实现:
private List<QuestionListItemVO> questions = new CopyOnWriteArrayList<>();
@Override
public List<QuestionListItemVO> getMostHits() { if (questions.isEmpty()) { synchronized (CacheSchedule.LOCK_CACHE_QUESTION) { if (questions.isEmpty()) { questions.addAll(questionMapper.findMostHits()); } } } return questions;
}
@Override
public List<QuestionListItemVO> getCachedMostHits() { return questions;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
并在计划任务中添加新的清除缓存任务:
@Autowired
private IQuestionService questionService;
public static final Object LOCK_CACHE_QUESTION = new Object();
@Scheduled(initialDelay = 1 * 60 * 1000, fixedRate = 1 * 60 * 1000)
public void clearQuestionCache() { synchronized (LOCK_CACHE_QUESTION) { questionService.getCachedMostHits().clear(); log.debug("clear question cache ..."); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
为了便于学习时修改数据后缓存能更快清空,暂时将计划任务的周期调整为1分钟。
单元测试:
@Test
void getMostHits() { List<QuestionListItemVO> questions = service.getMostHits(); log.debug("question count={}", questions.size()); for (QuestionListItemVO question : questions) { log.debug(">>> {}", question); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
38. 热点问题-控制器层
// http://localhost:8080/api/v1/questions/hits
@GetMapping("hits")
public R<List<QuestionListItemVO>> mostHits() { return R.ok(questionService.getMostHits());
}
- 1
- 2
- 3
- 4
- 5
39. 前端页面
注意:此前开发“我要提问”时,创建的Vue对象时,设置的id覆盖范围太大,应该将此前设置的id调整到仅覆盖“提问”的表单,否则,此次将创建Vue对象的范围将在此前范围的子级,将无法正常使用。
在question/create.html中,先找到显示“热点问题”的列表,在其父级添加id="mostHitQuestions"
,在被遍历的标签及子级添加Vue的绑定:
<div id="mostHitQuestionsApp" class="container-fluid bg-light mt-5"> <h4 class="m-2 p-2 font-weight-light"><i class="fa fa-list" aria-hidden="true"></i> 热点问题</h4> <div v-for="question in questions" class="list-group list-group-flush"> <a href="../question/detail.html" class="list-group-item list-group-item-action"> <div class="d-flex w-100 justify-content-between"> <h6 class="mb-1 text-dark" v-text="question.title">equals和==的区别是啥?</h6> </div> <div class="row"> <div class="col-6"> <small class="mr-2">1条回答</small> <small v-text="question.statusText" v-bind:class="[question.statusClass]">已解决</small> </div> <div class="col-6 text-right"> <small><span v-text="question.hits">10</span>浏览</small> </div> </div> </a> </div>
</div>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
然后,创建**/js/commons/most_hits.js**文件,编写测试数据绑定:
let mostHitQuestionsApp = new Vue({ el: '#mostHitQuestionsApp', data: { questions: [ { id: 1, title: '第1个问题', status: 0, hits: 20, statusText: '未回复', statusClass: "text-warning" }, { id: 3, title: '第2个问题', status: 2, hits: 42, statusText: '已解决', statusClass: "text-success" }, { id: 7, title: '第3个问题', status: 0, hits: 67, statusText: '未回复', statusClass: "text-warning" }, { id: 10, title: '第4个问题', status: 1, hits: 35, statusText: '未解决', statusClass: "text-info" }, { id: 17, title: '第5个问题', status: 1, hits: 16, statusText: '未解决', statusClass: "text-info" }, ] }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
然后,在create.html中引用以上js文件,即可看到测试效果。
然后,在most_hits.js中补全数据访问:
let mostHitQuestionsApp = new Vue({ el: '#mostHitQuestionsApp', data: { questions: [ { id: 1, title: '第1个问题', status: 0, hits: 20, statusText: '未回复', statusClass: "text-warning" }, { id: 3, title: '第2个问题', status: 2, hits: 42, statusText: '已解决', statusClass: "text-success" }, { id: 7, title: '第3个问题', status: 0, hits: 67, statusText: '未回复', statusClass: "text-warning" }, { id: 10, title: '第4个问题', status: 1, hits: 35, statusText: '未解决', statusClass: "text-info" }, { id: 17, title: '第5个问题', status: 1, hits: 16, statusText: '未解决', statusClass: "text-info" }, ] }, methods: { loadMostHitQuestions: function () { $.ajax({ url: '/api/v1/questions/hits', success: function (json) { let questions = []; let statusTexts = ['未回复', '未解决', '已解决']; let statusClasses = ['text-warning', 'text-info', 'text-success']; for (let i = 0; i < json.data.length; i++) { questions[i] = json.data[i]; questions[i].statusText = statusTexts[questions[i].status]; questions[i].statusClass = statusClasses[questions[i].status]; } mostHitQuestionsApp.questions = questions; } }); } }, created: function () { this.loadMostHitQuestions(); }
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
40. 显示主页
将static下的index.html移动到templates下。
在SystemController
中添加:
@GetMapping("/index.html")
public String index() { return "index";
}
- 1
- 2
- 3
- 4
在SecurityConfig
中,将/index.html
从白名单中移除,要求必须登录才可以访问主页!
可以发现,在“主页”和“我要提问”页面,都存在相同的区域:顶部的标签导航,右侧的热点问题列表。如果在2个页面都单独处理,就会出现重复的代码!
Thymeleaf框架可以将页面中的某个部分设置为“碎片(fragment)”,在其它页面中可以直接引用该碎片,就不必编写重复的代码了!设置碎片的代码是在标签是添加th:fragment="自定义名称"
,在其它页面,通过th:replace="碎片所在页面的视图名称::碎片名称"
即可引用碎片!
以“显示顶部标签导航”为例,在index.html中,为原有标签添加th:fragment="nav_tags"
:
<div th:fragment="nav_tags" class="container-fluid" id="navTagsApp"> <div class="nav font-weight-light"> <a href="../tag/tag_question.html" class="nav-item nav-link text-info"><small>全部</small></a> <a v-for="tag in tags" href="../tag/tag_question.html" class="nav-item nav-link text-info"><small v-text="tag.name">Java基础</small></a> </div>
</div>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
在create.html中使用th:replace="index::nav_tags"
即可:
<div th:replace="index::nav_tags"></div>
- 1
41. 我的问答列表-持久层
(a) 分析需要执行的SQL语句
如果需要显示当前登录的用户的问答列表,需要执行的SQL语句大致是:
select * from question where user_id=? order by created_time desc
- 1
最终在页面中显示列表时,还需要显示每个问题的标签,关于标签,在question_tag
中已经存储了“问题”与“标签”的对应关系,所以,需要显示标签名称时,可以通过关联查询得到各标签的名称,例如:
select *
from question
left join question_tag on question.id=question_tag.question_id
left join tag on question_tag.tag_id=tag.id
where user_id=?
order by created_time desc
- 1
- 2
- 3
- 4
- 5
- 6
另外,在question
表中,在每次发表提问时,还使用tag_ids
记录了每个问题的标签的id列表,这是一种冗余的记录,其优点是“只需要查1张表就可以知道该问题有哪些标签”,缺点在于:
- 存储了冗余的数据,额外占用了存储空间;
- 数据更新更加麻烦,如果修改标签,则2张数据表都需要调整;
- 如果只查1张表,只能查出标签的id,无法显示标签的名称!
关于以上问题的分析:
- 额外占用的空间不大,在查询时却能提升查询效率(对于关联3张表的查询,只需要查询1张表肯定更加高效);
- 需要同时修改2张表效率确实更低,但是,从用户的使用角度来看,修改标签的概率更低,但是显示列表的概率更高,所以,相比之下应该优先考虑显示时的效率,修改的概率是次要的;
- 由于标签是相对固定的数据,此前的设计中就已经使用了缓存,相比关联查询3张表而言,只查1张表并结合内存中的缓存数据来得到完整数据,后者的效率更高一些。
综合来看,更加合理的解决方案应该是:只查question
这1张表即可,当查出数据后,根据结果中的tagIds
再从内存缓存的标签列表中取出各标签数据即可!
(b) 在接口中定义抽象方法
最终,向客户端响应的数据中必须包括若干个Tag
对象,所以需要创建对应的VO类:
@Data
public class QuestionVO { private Integer id; private String title; private String content; private Integer userId; private String userNickName; private Integer status; private Integer hits; private Integer isPublic; private Integer isDelete; private LocalDateTime createdTime; private LocalDateTime modifiedTime; private String tagIds; private List<TagVO> tags;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
在持久层接口QuestionMapper
中添加抽象方法:
/**
* 查询某用户的问题列表
*
* @param userId 用户的id
* @return 该用户的问题列表
*/
List<QuestionVO> findListByUserId(Integer userId);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
© 配置抽象方法的映射
在QuestionMapper.xml
中配置映射:
<resultMap id="QuestionVOMap" type="cn.tedu.straw.portal.vo.QuestionVO"> <id column="id" property="id" /> <result column="title" property="title" /> <result column="content" property="content" /> <result column="user_nick_name" property="userNickName" /> <result column="user_id" property="userId" /> <result column="created_time" property="createdTime" /> <result column="status" property="status" /> <result column="hits" property="hits" /> <result column="is_public" property="isPublic" /> <result column="modified_time" property="modifiedTime" /> <result column="is_delete" property="isDelete" /> <result column="tag_ids" property="tagIds" />
</resultMap>
<select id="findListByUserId" resultMap="QuestionVOMap"> SELECT * FROM question WHERE user_id=#{userId} ORDER BY created_time DESC
</select>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
(d) 测试
在QuestionMapperTestes
中测试:
@Test
void findListByUserId() { Integer userId = 9; List<QuestionVO> questions = mapper.findListByUserId(userId); log.debug("question count={}", questions.size()); for (QuestionVO question : questions) { log.debug(">>> {}", question); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
测试输出结果例如:
question count=3
>>> QuestionVO(id=3, title=什么是线程安全问题, content=当创建多个线程后,对电脑运行的安全会有影响吗?会不会让电脑烧坏了?<br>, userId=9, userNickName=野原新之助, status=0, hits=101, isPublic=1, isDelete=0, createdTime=2020-07-23T20:42:34, modifiedTime=2020-07-23T20:42:34, tagIds=3, 15, tags=null)
>>> QuestionVO(id=2, title=什么是继承, content=<p>参考网上的说法,答案如下,请老师评估是否正确:<br></p><p>继承是一种利用已有类,快速创建新的类的机制。</p><p>被继承的类称之为父类,或超类,或基类,继承自其它类的类称之为子类,或派生类。</p><p>Java语言只能单继承,也就是说:每个类只能有1个直接父类。</p><p>如果某个类没有显式的继承另一个类,则默认继承自Object类。</p><p>当子类继承了父类后,将得到父类中所有成员,但是,需要注意:</p><ol><li>从数据存在的角度来看,私有成员也是可以得到的,但是,从实际使用来看,除非使用反射,否则,父类中的私有成员对于子类是不可见的;</li><li>构造方法不存在继承的说法,并且,如果父类中不存在无参数构造方法,子类需要显式的声明构造方法;</li><li>父类中的静态成员也不存在继承的说法,但是,通过子类的类名或子类的对象可以调用。<br></li></ol>, userId=9, userNickName=野原新之助, status=0, hits=123, isPublic=1, isDelete=0, createdTime=2020-07-23T20:41:21, modifiedTime=2020-07-23T20:41:21, tagIds=2, 1, 15, tags=null)
>>> QuestionVO(id=1, title=写Java HelloWorld时需要注意什么, content=<p>需要注意的问题有:</p><ol><li>安装好JDK;</li><li>配置好环境变量;</li><li>不要出现明显的语法错误,例如关键字的拼写、符号的使用;</li><li>使用System.out.println()输出字符串时,特殊的符号需要转义。<br></li></ol>, userId=9, userNickName=野原新之助, status=0, hits=161, isPublic=1, isDelete=0, createdTime=2020-07-23T20:36:24, modifiedTime=2020-07-23T20:36:24, tagIds=1, 15, tags=null)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
42. 我的问答列表-业务层
(a) xx
(b) 在接口中定义抽象方法
在IQuestionService
中添加抽象方法(暂不考虑Tags的问题):
List<QuestionVO> getQuestionsByUserId(Integer userId);
- 1
© 实现业务
在处理标签数据时,使用Map
再做一个缓存对象,使用标签的id
作为Key,标签对象TagVO
作为Value,后续,就可以根据id
从Map
对象中获取对应的TagVO
了!
所以,在处理标签数据的业务接口ITagService
中添加抽象方法:
/**
* 根据标签的id从缓存中获取标签对象
*
* @param tagId 标签的id
* @return 标签对象
*/
TagVO getTagVOById(Integer tagId);
/**
* 获取缓存的标签的Map集合
*
* @return 缓存的标签的Map集合
*/
Map<Integer, TagVO> getCachedTagsMap();
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
在处理标签数据的业务实现类TagServiceImpl
中声明缓存对象:
/**
* 缓存的标签Map集合
*/
private Map<Integer, TagVO> tagsMap = new ConcurrentHashMap<>();
- 1
- 2
- 3
- 4
线程安全问题的前提:
- 存在多个线程;
- 多个线程同时处于运行状态;
- 多个线程会访问到同一个数据;
- 多个线程对这同一个数据都有“写”操作。
当以上4个条件全部满足时,就需要考虑如何解决线程安全问题了!
尽量不要将数据声明为全局的属性,可能导致线程安全问题,例如:在某Service实现类中声明了全局属性,由于Spring是使用单例模式管理对象的,所以,在整个项目运行期间,该Service类的对象只会存在1个,则类中的全局属性也只有1个,若干个线程访问时,用到的都是同一个全局属性,就可能存在线程安全问题!所以,能不声明为全局变量就不要声明为全局变量,如果一定需要使用,需要评估该全局变量是否可能存在修改,例如在Service中装配的持久层对象就不会被修改,只是用于调用方法的,就不存在线程安全问题,如果是List集合,或某些表现数值的数据,就可能存在写入的操作,就存在线程安全问题,在写入时,必须使用互斥锁!
HashMap
是多线程不安全的,HashTable
是安全的,但是,HashTable
的处理效率低下,建议使用ConcurrentHashMap
。
然后,原有的缓存标签数据的过程中,将原本获取到的标签数据逐一添加到以上Map
中:
@Override
public List<TagVO> getTags() { // 判断有没有必要锁住代码 if (tags.isEmpty()) { // 锁住代码 synchronized (CacheSchedule.LOCK_CACHE) { // 判断有没有必要重新加载数据 if (tags.isEmpty()) { tags.addAll(tagMapper.findAll()); log.debug("create tags cache ..."); log.debug(">>> tags : {}", tags); for (TagVO tag : tags) { tagsMap.put(tag.getId(), tag); } log.debug("create tags map cache ..."); log.debug(">>> tags map : {}", tagsMap); } } } return tags;
}
@Override
public Map<Integer, TagVO> getCachedTagsMap() { return tagsMap;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
再重写接口中的抽象方法,实现“根据标签id
获取TagVO
对象”:
@Override
public TagVO getTagVOById(Integer tagId) { // 如果缓存数据不存在,调用以上方法从数据库中读取数据并缓存下来 if (tagsMap.isEmpty()) { getTags(); } // 从缓存的Map中取出数据 TagVO tag = tagsMap.get(tagId); // 返回 return tag;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
在CacheSchedule
计划任务中补充清除原标签缓存时一并清除Map
中的缓存:
@Scheduled(initialDelay = 10 * 60 * 1000, fixedRate = 10 * 60 * 1000)
public void clearCache() { synchronized (LOCK_CACHE) { tagService.getCachedTags().clear(); tagService.getCachedTagsMap().clear(); log.debug("clear tags cache ..."); userService.findCachedTeachers().clear(); log.debug("clear teacher cache ..."); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
在QuestionServiceImpl
中实现以上抽象方法:
@Autowired
private ITagService tagService;
@Override
public List<QuestionVO> getQuestionsByUserId(Integer userId) { // 调用持久层方法查询问题列表,该列表中的数据只有标签的id,并不包括标签数据 List<QuestionVO> questions = questionMapper.findListByUserId(userId); // 遍历以上列表,取出每个问题中记录的标签的ids,并根据这些id从缓存中取出TagVO封装到QuestionVO对象中 for (QuestionVO question : questions) { // 取出标签的id String tagIdsStr = question.getTagIds(); // 1, 2, 3 // 拆分 String[] tagIds = tagIdsStr.split(", "); // 创建用于存放若干个标签的集合 question.setTags(new ArrayList<>()); // 遍历数组,从缓存中找出对应的TagVO for (String tagId : tagIds) { // 从缓存中取出对应的TagVO Integer id = Integer.valueOf(tagId); TagVO tag = tagService.getTagVOById(id); // 将取出的TagVO添加到QuestionVO对象中 question.getTags().add(tag); } } // 返回 return questions;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
(d) 测试
在QuestionServiceTests
中测试:
@Test
void getQuestionsByUserId() { Integer userId = 11; List<QuestionVO> questions = service.getQuestionsByUserId(userId); log.debug("question count={}", questions.size()); for (QuestionVO question : questions) { log.debug(">>> {}", question); }
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
43. 我的问答列表-业务层-分页重构
PageHelper
框架提供了便捷的分页处理!只需要在调用MyBatis持久层的查询方法之前,配置分页参数,即可实现注入Limit子句实现分页查询,对原有的持久层代码没有任何入侵,并且,在返回结果中,会自动添加分页相关的各项参数。
首先,应该添加PageHelper
框架所需的依赖:
<dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.13</version>
</dependency>
- 1
- 2
- 3
- 4
- 5
关于PageHelper
的使用:
- 应该在业务层使用;
- 业务方法必须使用
PageInfo<?>
类型作为返回值,其中泛型就是需要查询的数据的实体类或VO类(也可以理解为这里的泛型是List
集合中的元素类型); - 调用
PageHelper
时需要指定“当前页面”和“查询多少条数据”,这2个参数可以声明为抽象方法的参数,“查询多少条数据”也可以理解为“每页显示多少条数据”,是相对固定的值,可以直接写死,或写成配置值等。
基本以上规则,将业务接口中原有的抽象方法改为:
/**
* 获取某用户某页的问题列表
*
* @param userId 用户的id
* @param page 页码
* @return 匹配的问题列表
*/
PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer page);
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
本次将把“每页显示多少条数据”设置为配置,所以,在抽象方法中并不将其声明为参数。
然后,将业务层实现类的业务方法的声明改为与接口一致,在实现时,在调用持久层方法之前配置分页参数:
// 设置分页参数
PageHelper.startPage(page, 2);
// 调用持久层方法查询问题列表,该列表中的数据只有标签的id,并不包括标签数据
List<QuestionVO> questions = questionMapper.findListByUserId(userId);
- 1
- 2
- 3
- 4
最后,返回匹配类型的结果:
// 返回
return new PageInfo<>(questions);
- 1
- 2
完整代码如下:
@Override
public PageInfo<QuestionVO> getQuestionsByUserId(Integer userId, Integer page) { // 设置分页参数 PageHelper.startPage(page, 2); // 调用持久层方法查询问题列表,该列表中的数据只有标签的id,并不包括标签数据 List<QuestionVO> questions = questionMapper.findListByUserId(userId); // 遍历以上列表,取出每个问题中记录的标签的ids,并根据这些id从缓存中取出TagVO封装到QuestionVO对象中 for (QuestionVO question : questions) { // 取出标签的id String tagIdsStr = question.getTagIds(); // 1, 2, 3 // 拆分 String[] tagIds = tagIdsStr.split(", "); // 创建用于存放若干个标签的集合 question.setTags(new ArrayList<>()); // 遍历数组,从缓存中找出对应的TagVO for (String tagId : tagIds) { // 从缓存中取出对应的TagVO Integer id = Integer.valueOf(tagId); TagVO tag = tagService.getTagVOById(id); // 将取出的TagVO添加到QuestionVO对象中 question.getTags().add(tag); } } // 返回 return new PageInfo<>(questions);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
完成后,即可执行单元测试:
@Test
void getQuestionsByUserId() { Integer userId = 11; Integer page = 0; PageInfo<QuestionVO> pageInfo = service.getQuestionsByUserId(userId, page); log.debug("page info >>> {}", pageInfo);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
测试无误后,在application.properties中添加关于“每页显示多少条数据”的配置:
project.question-list.page-size=2
- 1
在QuestionServiceImpl
中添加:
@Value("${project.question-list.page-size}")
private Integer pageSize;
- 1
- 2
最后,将以上pageSize
应用于PageHelper.start()
方法中作为参数即可。
44. 我的问答列表-控制器层
(a) 处理异常
如果在业务层抛出新的(从未处理过的)异常,需要进行处理。
(b) 设计请求
请求路径:http://localhost:8080/api/v1/questions/my?page=1
请求方式:GET
请求参数:Integer page
,用户的id
响应结果:PageInfo<QuestionVO>
© 处理请求
在QuestionController
中添加处理请求的方法:
// http://localhost:8080/api/v1/questions/my
@GetMapping("/my")
public R<PageInfo<QuestionVO>> getMyQuestions(Integer page, @AuthenticationPrincipal UserInfo userInfo) { if (page == null || page < 1) { page = 1; } PageInfo<QuestionVO> questions = questionService.getQuestionsByUserId(userInfo.getId(), page); return R.ok(questions);
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
(d) 测试
打开浏览器,输入URL后测试。
45. 我的问答列表-前端页面
参考此前显示列表的方式来显示“我的问答列表”,关于Vue的使用:
v-for
:用于遍历当前标签及其所有子级标签,配置的参数意义可参考Java中的增强for循环;v-text
:用于绑定某标签中显示的文本信息;v-html
:用于绑定某标签中填充的HTML源代码;
另外,在“我的问答列表”中,每一个问题都有对应的图片,取出**/img/tag/**文件夹中与当前问题第1个Tag Id匹配的图片即可,也就是说,第1个Tag Id就是图片的文件名。
关于主页的“我的问答列表”下方的分页按钮,尽量完成。
文章来源: haiyong.blog.csdn.net,作者:海拥✘,版权归原作者所有,如需转载,请联系作者。
原文链接:haiyong.blog.csdn.net/article/details/107733061
- 点赞
- 收藏
- 关注作者
评论(0)