项目之热点问题和问答列表(9)

举报
海拥 发表于 2021/08/04 23:58:29 2021/08/04
【摘要】 36. 热点问题-持久层 先创建封装数据的VO类: @Data public class QuestionListItemVO { private Integer id; private String title; private Integer status; private Integer hits; } 123456789 在持久层接口QuestionMa...

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张表就可以知道该问题有哪些标签”,缺点在于:

  1. 存储了冗余的数据,额外占用了存储空间;
  2. 数据更新更加麻烦,如果修改标签,则2张数据表都需要调整;
  3. 如果只查1张表,只能查出标签的id,无法显示标签的名称!

关于以上问题的分析:

  1. 额外占用的空间不大,在查询时却能提升查询效率(对于关联3张表的查询,只需要查询1张表肯定更加高效);
  2. 需要同时修改2张表效率确实更低,但是,从用户的使用角度来看,修改标签的概率更低,但是显示列表的概率更高,所以,相比之下应该优先考虑显示时的效率,修改的概率是次要的;
  3. 由于标签是相对固定的数据,此前的设计中就已经使用了缓存,相比关联查询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,后续,就可以根据idMap对象中获取对应的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

【版权声明】本文为华为云社区用户转载文章,如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。