第四天 圈子功能實現
- 抽取common工程
- 圈子功能說明
- 圈子技術實現
- 圈子技術方案
- 圈子實現發布動態
- 圈子實現好友動態
- 圈子實現推薦動態
1、抽取common工程
在項目中一般需要將公用的對象進行抽取放到common工程中,其他的工程依賴此工程即可。下面我們將sso以及server工程中的公用的對象進行抽取。
1.1、創建my-tanhua-common工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>my-tanhua</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-tanhua-common</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
</dependencies>
</project>
1.2、通用枚舉
將SexEnum枚舉移動至common工程,並且后續創建的枚舉也要放到次工程中,以達到公用的目的。
package com.tanhua.common.enums;
import com.baomidou.mybatisplus.core.enums.IEnum;
public enum SexEnum implements IEnum<Integer> {
MAN(1,"男"),
WOMAN(2,"女"),
UNKNOWN(3,"未知");
private int value;
private String desc;
SexEnum(int value, String desc) {
this.value = value;
this.desc = desc;
}
@Override
public Integer getValue() {
return this.value;
}
@Override
public String toString() {
return this.desc;
}
}
需要修改server與sso工程中的application.properties配置:
# 枚舉包掃描
mybatis-plus.type-enums-package=com.tanhua.common.enums
將server與sso工程中的SexEnum對象刪除以及將相關的類引用進行修改。
1.3、抽取mapper
需要將UserInfoMapper以及UserMapper放置到common工程的com.tanhua.common.mapper包下。
package com.tanhua.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.User;
public interface UserMapper extends BaseMapper<User> {
}
package com.tanhua.common.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.UserInfo;
public interface UserInfoMapper extends BaseMapper<UserInfo> {
}
說明:抽取完成后,需要將原工程的代碼刪除以及修改其他代碼中引入的依賴。
1.4、抽取pojo
將BasePojo、User、UserInfo移動至common工程:
1.5、抽取utils
將server工程的utils進行抽取公用,后續的工具類也放置到common工程中。
抽取完成后進行測試,確保可以正常啟動以及功能都正常。
2、圈子功能
2.1、功能說明
探花交友項目中的圈子功能,類似微信的朋友圈,基本的功能為:發布動態、瀏覽好友動態、瀏覽推薦動態、點贊、評論、喜歡等功能。
發布:
2.2、實現方案分析
對於圈子功能的實現,我們需要對它的功能特點做分析:
- 數據量會隨着用戶數增大而增大
- 讀多寫少,一般而言,瀏覽朋友圈動態會多一些,發動態相對就會少一些
- 非好友看不到其動態內容
- ……
針對以上特點,我們來分析一下:
- 對於數據量大而言,顯然不能夠使用關系型數據庫進行存儲,我們需要通過MongoDB進行存儲
- 對於讀多寫少的應用,盡可能的減少讀取數據的成本
- 比如說,一條SQL語句,單張表查詢一定比多張表查詢要快
- 條件越多的查詢速度將越慢,盡可能的減少條件以提升查詢速度
所以對於存儲而言,主要是核心的4張表:
- 發布表:記錄了所有用戶的發布的東西信息,如圖片、視頻等。
- 相冊:相冊是每個用戶獨立的,記錄了該用戶所發布的所有內容。
- 評論:針對某個具體發布的朋友評論和點贊操作。
- 時間線:所謂“刷朋友圈”,就是刷時間線,就是一個用戶所有的朋友的發布內容。
流程:
流程說明:
- 用戶發布動態,動態中一般包含了圖片和文字,圖片上傳到阿里雲,上傳成功后拿到圖片地址,將文字和圖片地址進行持久化存儲
- 首先,需要將動態數據寫入到發布表中,其次,再寫入到自己的相冊表中,需要注意的是,相冊表中只包含了發布id,不會冗余存儲發布數據
- 最后,需要將發布數據異步的寫入到好友的時間線表中,之所以考慮異步操作,是因為希望發布能夠盡快給用戶反饋,發布成功
- 好友刷朋友圈時,實際上只需要查詢自己的時間線表即可,這樣最大限度的提升了查詢速度,再配合redis的緩存,那速度將是飛快的
- 用戶在對動態內容進行點贊、喜歡、評論操作時,只需要寫入到評論表即可,該表中也是只會記錄發布id,並不會冗余存儲發布數據
2.3、表結構設計
發布表:
#表名:quanzi_publish
{
"_id":"5fae53d17e52992e78a3db61",#主鍵id
"pid":1001, #發布id(Long類型)
"userId":1, #用戶id
"text":"今天心情很好", #文本內容
"medias":"http://xxxx/x/y/z.jpg", #媒體數據,圖片或小視頻 url
"seeType":1, #誰可以看,1-公開,2-私密,3-部分可見,4-不給誰看
"seeList":[1,2,3], #部分可見的列表
"notSeeList":[4,5,6],#不給誰看的列表
"longitude":108.840974298098,#經度
"latitude":34.2789316522934,#緯度
"locationName":"上海市浦東區", #位置名稱
"created",1568012791171 #發布時間
}
相冊表:
#表名:quanzi_album_{userId}
{
"_id":"5fae539d7e52992e78a3b684",#主鍵id
"publishId":"5fae53d17e52992e78a3db61", #發布id
"created":1568012791171 #發布時間
}
時間線表:
#表名:quanzi_time_line_{userId}
{
"_id":"5fae539b7e52992e78a3b4ae",#主鍵id,
"userId":2, #好友id
"publishId":"5fae53d17e52992e78a3db61", #發布id
"date":1568012791171 #發布時間
}
評論表:
#表名:quanzi_comment
{
"_id":"5fae539d7e52992e78a3b648", #主鍵id
"publishId":"5fae53d17e52992e78a3db61", #發布id
"commentType":1, #評論類型,1-點贊,2-評論,3-喜歡
"content":"給力!", #評論內容
"userId":2, #評論人
"publishUserId":9, #發布動態的人的id
"isParent":false, #是否為父節點,默認是否
"parentId":1001, #父節點id
"created":1568012791171
}
3、好友關系數據
由於圈子中會涉及的好友關系數據,雖然現在主線是開發圈子功能,但是也需要對於好友關系有所了解,在我們提供的Mongodb數據庫中有一些mock數據。
好友關系結構:
package com.tanhua.dubbo.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.mapping.Document;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "tanhua_users")
public class Users implements java.io.Serializable{
private static final long serialVersionUID = 6003135946820874230L;
private ObjectId id;
private Long userId; //用戶id
private Long friendId; //好友id
private Long date; //時間
}
在mock數據中,為每個用戶構造了10個好友數據:
4、查詢好友動態
查詢好友動態與查詢推薦動態顯示的結構是一樣的,只是其查詢數據源不同:
4.1、基礎代碼
在my-tanhua-dubbo-interface中編寫:
package com.tanhua.dubbo.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
import java.util.List;
/**
* 發布表,動態內容
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "quanzi_publish")
public class Publish implements java.io.Serializable {
private static final long serialVersionUID = 8732308321082804771L;
@Id
private ObjectId id; //主鍵id
private Long pid; //發布id
private Long userId; //發布用戶id
private String text; //文字
private List<String> medias; //媒體數據,圖片或小視頻 url
private Integer seeType; // 誰可以看,1-公開,2-私密,3-部分可見,4-不給誰看
private List<Long> seeList; //部分可見的列表
private List<Long> notSeeList; //不給誰看的列表
private String longitude; //經度
private String latitude; //緯度
private String locationName; //位置名稱
private Long created; //發布時間
}
package com.tanhua.dubbo.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
/**
* 相冊表,用於存儲自己發布的數據,每一個用戶一張表進行存儲
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "quanzi_album_{userId}")
public class Album implements java.io.Serializable {
private static final long serialVersionUID = 432183095092216817L;
@Id
private ObjectId id; //主鍵id
private ObjectId publishId; //發布id
private Long created; //發布時間
}
package com.tanhua.dubbo.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
/**
* 時間線表,用於存儲發布的數據,每一個用戶一張表進行存儲
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "quanzi_time_line_{userId}")
public class TimeLine implements java.io.Serializable {
private static final long serialVersionUID = 9096178416317502524L;
@Id
private ObjectId id;
private Long userId; // 好友id
private ObjectId publishId; //發布id
private Long date; //發布的時間
}
package com.tanhua.dubbo.server.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
/**
* 評論表
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "quanzi_comment")
public class Comment implements java.io.Serializable{
private static final long serialVersionUID = -291788258125767614L;
@Id
private ObjectId id;
private ObjectId publishId; //發布id
private Integer commentType; //評論類型,1-點贊,2-評論,3-喜歡
private String content; //評論內容
private Long userId; //評論人
private Long publishUserId; //發布動態的用戶id
private Boolean isParent = false; //是否為父節點,默認是否
private ObjectId parentId; // 父節點id
private Long created; //發表時間
}
4.2、dubbo服務
圈子的具體業務邏輯的實現需要在dubbo中完成,所以需要開發dubbo服務。
4.2.1、定義接口
在my-tanhua-dubbo-interface工程中:
package com.tanhua.dubbo.server.api;
import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.dubbo.server.vo.PageInfo;
public interface QuanZiApi {
/**
* 查詢好友動態
*
* @param userId 用戶id
* @param page 當前頁數
* @param pageSize 每一頁查詢的數據條數
* @return
*/
PageInfo<Publish> queryPublishList(Long userId, Integer page, Integer pageSize);
}
4.2.2、實現接口
在my-tanhua-dubbo-service中完成:
package com.tanhua.dubbo.server.api;
import cn.hutool.core.collection.CollUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.dubbo.server.pojo.TimeLine;
import com.tanhua.dubbo.server.vo.PageInfo;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import java.util.ArrayList;
import java.util.List;
@Service(version = "1.0.0")
public class QuanZiApiImpl implements QuanZiApi {
@Autowired
private MongoTemplate mongoTemplate;
@Override
public PageInfo<Publish> queryPublishList(Long userId, Integer page, Integer pageSize) {
//分析:查詢好友的動態,實際上查詢時間線表
PageInfo<Publish> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);
Pageable pageable = PageRequest.of(page - 1, pageSize,
Sort.by(Sort.Order.desc("date")));
Query query = new Query().with(pageable);
List<TimeLine> timeLineList = this.mongoTemplate.find(query, TimeLine.class, "quanzi_time_line_" + userId);
if(CollUtil.isEmpty(timeLineList)){
//沒有查詢到數據
return pageInfo;
}
//獲取時間線列表中的發布id的列表
List<Object> ids = CollUtil.getFieldValues(timeLineList, "publishId");
//根據動態id查詢動態列表
Query queryPublish = Query.query(Criteria.where("id").in(ids))
.with(Sort.by(Sort.Order.desc("created")));
List<Publish> publishList = this.mongoTemplate.find(queryPublish, Publish.class);
pageInfo.setRecords(publishList);
return pageInfo;
}
}
引入Hutool工具包,官方文檔:https://www.hutool.cn/docs/#/
<!-- 在my-tanhua工程中定義依賴 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.2</version>
</dependency>
<!-- 在my-tanhua-dubbo-service中引入使用 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
4.2.3、測試用例
package com.tanhua.dubbo.server.api;
import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.dubbo.server.vo.PageInfo;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestQuanZiApi {
@Autowired
private QuanZiApi quanZiApi;
@Test
public void testQueryPublishList(){
this.quanZiApi.queryPublishList(1L, 1, 2)
.getRecords().forEach(publish -> System.out.println(publish));
System.out.println("------------");
this.quanZiApi.queryPublishList(1L, 2, 2)
.getRecords().forEach(publish -> System.out.println(publish));
System.out.println("------------");
this.quanZiApi.queryPublishList(1L, 3, 2)
.getRecords().forEach(publish -> System.out.println(publish));
}
}
測試結果:
4.3、APP接口服務
開發完成dubbo服務后,我們將開發APP端的接口服務,依然是需要按照mock接口的中的接口定義實現。
接口地址:https://mock-java.itheima.net/project/35/interface/api/683
4.3.1、QuanZiVo
根據接口中響應的數據結構進行定義vo對象:(在my-tanhua-server工程中)
package com.tanhua.server.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QuanZiVo {
private String id; //動態id
private Long userId; //用戶id
private String avatar; //頭像
private String nickname; //昵稱
private String gender; //性別 man woman
private Integer age; //年齡
private String[] tags; //標簽
private String textContent; //文字動態
private String[] imageContent; //圖片動態
private String distance; //距離
private String createDate; //發布時間 如: 10分鍾前
private Integer likeCount; //點贊數
private Integer commentCount; //評論數
private Integer loveCount; //喜歡數
private Integer hasLiked; //是否點贊(1是,0否)
private Integer hasLoved; //是否喜歡(1是,0否)
}
4.3.2、QuanZiController
根據服務接口編寫QuanZiController,其請求方法為GET請求,會傳遞page、pageSize、token等信息。
代碼實現如下:
package com.tanhua.server.controller;
import com.tanhua.server.service.QuanZiService;
import com.tanhua.server.vo.PageResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("movements")
public class QuanZiController {
@Autowired
private QuanZiService quanZiService;
/**
* 查詢好友動態
*
* @param page
* @param pageSize
* @return
*/
@GetMapping
public PageResult queryPublishList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize,
@RequestHeader("Authorization") String token) {
return this.quanZiService.queryPublishList(page, pageSize, token);
}
}
4.3.3、QuanZiService
在QuanZiService中將實現具體的業務邏輯,需要調用quanzi的dubbo服務完成數據的查詢,並且要完成用戶登錄是否有效的校驗,最后按照服務接口中定義的結構進行封裝數據。
package com.tanhua.server.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.User;
import com.tanhua.common.pojo.UserInfo;
import com.tanhua.common.utils.RelativeDateFormat;
import com.tanhua.dubbo.server.api.QuanZiApi;
import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.dubbo.server.vo.PageInfo;
import com.tanhua.server.vo.PageResult;
import com.tanhua.server.vo.QuanZiVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
@Service
public class QuanZiService {
@Reference(version = "1.0.0")
private QuanZiApi quanZiApi;
@Autowired
private UserService userService;
@Autowired
private UserInfoService userInfoService;
public PageResult queryPublishList(Integer page, Integer pageSize, String token) {
//分析:通過dubbo中的服務查詢用戶的好友動態
//通過mysql查詢用戶的信息,回寫到結果對象中(QuanZiVo)
PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);
//校驗token是否有效
User user = this.userService.queryUserByToken(token);
if (user == null) {
//token已經失效
return pageResult;
}
//通過dubbo查詢數據
PageInfo<Publish> pageInfo = this.quanZiApi.queryPublishList(user.getId(), page, pageSize);
List<Publish> records = pageInfo.getRecords();
if (CollUtil.isEmpty(records)) {
return pageResult;
}
List<QuanZiVo> quanZiVoList = new ArrayList<>();
records.forEach(publish -> {
QuanZiVo quanZiVo = new QuanZiVo();
quanZiVo.setId(publish.getId().toHexString());
quanZiVo.setTextContent(publish.getText());
quanZiVo.setImageContent(publish.getMedias().toArray(new String[]{}));
quanZiVo.setUserId(publish.getUserId());
quanZiVo.setCreateDate(RelativeDateFormat.format(new Date(publish.getCreated())));
quanZiVoList.add(quanZiVo);
});
//查詢用戶信息
List<Object> userIds = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);
for (QuanZiVo quanZiVo : quanZiVoList) {
//找到對應的用戶信息
for (UserInfo userInfo : userInfoList) {
if(quanZiVo.getUserId().longValue() == userInfo.getUserId().longValue()){
this.fillUserInfoToQuanZiVo(userInfo, quanZiVo);
break;
}
}
}
pageResult.setItems(quanZiVoList);
return pageResult;
}
/**
* 填充用戶信息
*
* @param userInfo
* @param quanZiVo
*/
private void fillUserInfoToQuanZiVo(UserInfo userInfo, QuanZiVo quanZiVo){
BeanUtil.copyProperties(userInfo, quanZiVo, "id");
quanZiVo.setGender(userInfo.getSex().name().toLowerCase());
quanZiVo.setTags(StringUtils.split(userInfo.getTags(), ','));
quanZiVo.setCommentCount(0); //TODO 評論數
quanZiVo.setDistance("1.2公里"); //TODO 距離
quanZiVo.setHasLiked(0); //TODO 是否點贊(1是,0否)
quanZiVo.setLikeCount(0); //TODO 點贊數
quanZiVo.setHasLoved(0); //TODO 是否喜歡(1是,0否)
quanZiVo.setLoveCount(0); //TODO 喜歡數
}
}
// com.tanhua.server.service.UserInfoService
/**
* 根據用戶id的集合查詢用戶列表
*
* @param userIds
* @return
*/
public List<UserInfo> queryUserInfoList(Collection<?> userIds) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.in("user_id", userIds);
return this.queryUserInfoList(queryWrapper);
}
在com.tanhua.server.vo.QuanZiVo中增加字段別名,方便直接拷貝屬性數據:
package com.tanhua.server.vo;
import cn.hutool.core.annotation.Alias;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class QuanZiVo {
private String id; //動態id
private Long userId; //用戶id
@Alias("logo") //別名
private String avatar; //頭像
@Alias("nickName") //別名
private String nickname; //昵稱
private String gender; //性別 man woman
private Integer age; //年齡
private String[] tags; //標簽
private String textContent; //文字動態
private String[] imageContent; //圖片動態
private String distance; //距離
private String createDate; //發布時間 如: 10分鍾前
private Integer likeCount; //點贊數
private Integer commentCount; //評論數
private Integer loveCount; //喜歡數
private Integer hasLiked; //是否點贊(1是,0否)
private Integer hasLoved; //是否喜歡(1是,0否)
}
4.3.4、測試
5、統一校驗token
在之前的開發中,我們會在每一個Service中對token做處理,相同的邏輯一定是要進行統一處理的,該如何處理呢?
由於程序是運行在web容器中,每一個HTTP請求都是一個獨立線程,也就是可以理解成我們編寫的應用程序運行在一個多線程的環境中,那么我們就可以使用ThreadLocal在HTTP請求的生命周期內進行存值、取值操作。
如下圖:
說明:
- 用戶的每一個請求,都是一個獨立的線程
- 圖中的TL就是ThreadLocal,一旦將數據綁定到ThreadLocal中,那么在整個請求的生命周期內都可以隨時拿到ThreadLocal中當前線程的數據。
根據上面的分析,我們只需要在Controller請求之前進行對token做校驗,如果token有效,則會拿到User對象,然后將該User對象保存到ThreadLocal中即可,最后放行請求,在后續的各個環節中都可以獲取到該數據了。
如果token無效,給客戶端響應401狀態碼,攔截請求,不再放行到Controller中。
由此可見,這個校驗的邏輯是比較適合放在攔截器中完成的。
5.1、編寫UserThreadLocal
在my-tanhua-common工程中,編寫UserThreadLocal。
package com.tanhua.common.utils;
import com.tanhua.common.pojo.User;
public class UserThreadLocal {
private static final ThreadLocal<User> LOCAL = new ThreadLocal<>();
private UserThreadLocal(){
}
/**
* 將對象放入到ThreadLocal
*
* @param user
*/
public static void set(User user){
LOCAL.set(user);
}
/**
* 返回當前線程中的User對象
*
* @return
*/
public static User get(){
return LOCAL.get();
}
/**
* 刪除當前線程中的User對象
*/
public static void remove(){
LOCAL.remove();
}
}
5.2、編寫TokenInterceptor
package com.tanhua.server.interceptor;
import cn.hutool.core.util.StrUtil;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.NoAuthorization;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.server.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class UserTokenInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//校驗handler是否是HandlerMethod
if (!(handler instanceof HandlerMethod)) {
return true;
}
//判斷是否包含@NoAuthorization注解,如果包含,直接放行
if (((HandlerMethod) handler).hasMethodAnnotation(NoAuthorization.class)) {
return true;
}
//從請求頭中獲取token
String token = request.getHeader("Authorization");
if(StrUtil.isNotEmpty(token)){
User user = this.userService.queryUserByToken(token);
if(user != null){
//token有效
//將User對象放入到ThreadLocal中
UserThreadLocal.set(user);
return true;
}
}
//token無效,響應狀態為401
response.setStatus(401); //無權限
return false;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
//從ThreadLocal中移除User對象
UserThreadLocal.remove();
}
}
5.3、編寫注解NoAuthorization
package com.tanhua.common.utils;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented //標記注解
public @interface NoAuthorization {
}
5.4、注冊攔截器
package com.tanhua.server.config;
import com.tanhua.server.interceptor.RedisCacheInterceptor;
import com.tanhua.server.interceptor.UserTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RedisCacheInterceptor redisCacheInterceptor;
@Autowired
private UserTokenInterceptor userTokenInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//考慮攔截器的順序
registry.addInterceptor(this.userTokenInterceptor).addPathPatterns("/**");
registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");
}
}
5.5、使用ThreadLocal
在所有的Service中,如果需要獲取User對象的,直接從UserThreadLocal獲取即可,同時在Controller中也無需進行獲取token操作。
例如:
//com.tanhua.server.service.QuanZiService
public PageResult queryPublishList(Integer page, Integer pageSize) {
PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);
//獲取User對象,無需對User對象校驗,其一定不為null
User user = UserThreadLocal.get();
PageInfo<Publish> pageInfo = this.quanZiApi.queryPublishList(user.getId(), page, pageSize);
//。。。。代碼略。。。。。
return pageResult;
}
需要注意的是,在APP中,如果請求響應401,會跳轉到登錄頁面。
6、發布動態
用戶可以在圈子中發布動態,動態內容中可以有文字和圖片。如下圖:

6.1、dubbo服務
6.1.1、定義接口
package com.tanhua.dubbo.server.api;
import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.dubbo.server.vo.PageInfo;
public interface QuanZiApi {
/**
* 查詢好友動態
*
* @param userId 用戶id
* @param page 當前頁數
* @param pageSize 每一頁查詢的數據條數
* @return
*/
PageInfo<Publish> queryPublishList(Long userId, Integer page, Integer pageSize);
/**
* 發布動態
*
* @param publish
* @return 發布成功返回動態id
*/
String savePublish(Publish publish);
}
6.1.2、實現接口
/**
* 發布動態
*
* @param publish
* @return 發布成功返回動態id
*/
public String savePublish(Publish publish) {
//對publish對象校驗
if (!ObjectUtil.isAllNotEmpty(publish.getText(), publish.getUserId())) {
//發布失敗
return null;
}
//設置主鍵id
publish.setId(ObjectId.get());
try {
//設置自增長的pid
publish.setPid(this.idService.createId(IdType.PUBLISH));
publish.setCreated(System.currentTimeMillis());
//寫入到publish表中
this.mongoTemplate.save(publish);
//寫入相冊表
Album album = new Album();
album.setId(ObjectId.get());
album.setCreated(System.currentTimeMillis());
album.setPublishId(publish.getId());
this.mongoTemplate.save(album, "quanzi_album_" + publish.getUserId());
//寫入好友的時間線表(異步寫入)
this.timeLineService.saveTimeLine(publish.getUserId(), publish.getId());
} catch (Exception e) {
//TODO 需要做事務的回滾,Mongodb的單節點服務,不支持事務,對於回滾我們暫時不實現了
log.error("發布動態失敗~ publish = " + publish, e);
}
return publish.getId().toHexString();
}
package com.tanhua.dubbo.server.service;
import com.tanhua.dubbo.server.enums.IdType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
//生成自增長的id,原理:使用redis的自增長值
@Service
public class IdService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public Long createId(IdType idType) {
String idKey = "TANHUA_ID_" + idType.toString();
return this.redisTemplate.opsForValue().increment(idKey);
}
}
package com.tanhua.dubbo.server.enums;
public enum IdType {
PUBLISH, VIDEO;
}
6.1.3、好友時間線數據
好友的時間線數據需要異步執行。這里使用Spring的@Async注解實現異步執行,其底層是通過啟動獨立線程來執行,從而可以異步執行。通過返回的CompletableFuture來判斷是否執行成功以及是否存在異常。同時需要在啟動類中添加@EnableAsync 開啟異步的支持。
package com.tanhua.dubbo.server.service;
import cn.hutool.core.collection.CollUtil;
import com.tanhua.dubbo.server.pojo.TimeLine;
import com.tanhua.dubbo.server.pojo.Users;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.concurrent.CompletableFuture;
@Service
@Slf4j
public class TimeLineService {
@Autowired
private MongoTemplate mongoTemplate;
@Async //異步執行,原理:底層開一個線程去執行該方法
public CompletableFuture<String> saveTimeLine(Long userId, ObjectId publishId) {
//寫入好友的時間線表
try {
//查詢好友列表
Query query = Query.query(Criteria.where("userId").is(userId));
List<Users> usersList = this.mongoTemplate.find(query, Users.class);
if (CollUtil.isEmpty(usersList)) {
//返回成功
return CompletableFuture.completedFuture("ok");
}
//依次寫入到好友的時間線表中
for (Users users : usersList) {
TimeLine timeLine = new TimeLine();
timeLine.setId(ObjectId.get());
timeLine.setDate(System.currentTimeMillis());
timeLine.setPublishId(publishId);
timeLine.setUserId(userId);
//寫入數據
this.mongoTemplate.save(timeLine, "quanzi_time_line_" + users.getFriendId());
}
} catch (Exception e) {
log.error("寫入好友時間線表失敗~ userId = " + userId + ", publishId = " + publishId, e);
//TODO 事務回滾問題
return CompletableFuture.completedFuture("error");
}
return CompletableFuture.completedFuture("ok");
}
}
開啟異步執行:
package com.tanhua.dubbo.server;
import cn.hutool.core.util.StrUtil;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;
@SpringBootApplication
@EnableAsync //開啟異步執行的支持
public class DubboApplication {
public static void main(String[] args) {
SpringApplication.run(DubboApplication.class, args);
}
}
6.1.4、測試好友時間線
package com.tanhua.dubbo.server.api;
import com.tanhua.dubbo.server.service.TimeLineService;
import org.bson.types.ObjectId;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestTimeLineService {
@Autowired
private TimeLineService timeLineService;
@Test
public void testSaveTimeLine() {
ObjectId objectId = ObjectId.get();
System.out.println("生成的id為:" + objectId.toHexString());
CompletableFuture<String> future = this.timeLineService.saveTimeLine(1L, objectId);
future.whenComplete((s, throwable) -> {
System.out.println("執行完成:" + s);
});
System.out.println("異步方法執行完成");
try {
future.get(); //阻塞當前的主線程,等待異步執行的結束
} catch (Exception e) {
e.printStackTrace();
}
}
}
6.1.5、測試發布動態
將dubbo服務啟動起來,在my-tanhua-server工程中進行功能的測試:
package com.tanhua.server;
import cn.hutool.core.collection.ListUtil;
import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.dubbo.server.api.QuanZiApi;
import com.tanhua.dubbo.server.pojo.Publish;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
@RunWith(SpringRunner.class)
@SpringBootTest
public class TestQuanZiApi {
@Reference(version = "1.0.0")
private QuanZiApi quanZiApi;
@Test
public void testSavePublish(){
Publish publish = new Publish();
publish.setText("人生不如意事十之八九,真正有格局的人,既能享受最好的,也能承受最壞的。");
publish.setMedias(ListUtil.toList("https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/6/1.jpg", "https://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/photo/6/CL-3.jpg"));
publish.setUserId(1L);
publish.setSeeType(1);
publish.setLongitude("116.350426");
publish.setLatitude("40.066355");
publish.setLocationName("中國北京市昌平區建材城西路16號");
this.quanZiApi.savePublish(publish);
}
}
6.2、APP接口服務
接口地址:https://mock-java.itheima.net/project/35/interface/api/701
從接口中可以看出,主要的參數有:文字、圖片、位置等內容。
6.2.1、圖片上傳
圖片上傳功能原來是在sso中完成的,為了能公用該功能,所以需要將圖片上傳的Service以及配置移動至common工程中。
pom.xml:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>my-tanhua</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>my-tanhua-common</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>
</project>
需要注意3點:
-
將sso系統中的相關代碼刪除
-
將aliyun.properties復制到my-tanhua-server中
-
啟動類中需要將包掃描范圍擴大到comm.tanhua,因為相關類被移動到com.tanhua.common下,默認掃描不能被掃描到。
-
sso與server系統都需要設置:
-
package com.tanhua.server; @MapperScan("com.tanhua.common.mapper") //設置mapper接口的掃描包 @SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) //排除mongo的自動配置 @ComponentScan(basePackages={"com.tanhua"}) //設置掃描包范圍 public class ServerApplication { public static void main(String[] args) { SpringApplication.run(ServerApplication.class, args); } }
-
6.2.2、接口服務
需要注意的是,文字是必須提交的,圖片是非必須的。
//com.tanhua.server.controller.QuanZiController
/**
* 發送動態
*
* @param textContent
* @param location
* @param multipartFile
* @return
*/
@PostMapping
public ResponseEntity<Void> savePublish(@RequestParam("textContent") String textContent,
@RequestParam(value = "location", required = false) String location,
@RequestParam(value = "latitude", required = false) String latitude,
@RequestParam(value = "longitude", required = false) String longitude,
@RequestParam(value = "imageContent", required = false) MultipartFile[] multipartFile) {
try {
String publishId = this.quanZiService.savePublish(textContent, location, latitude, longitude, multipartFile);
if (StrUtil.isNotEmpty(publishId)) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
6.2.3、QuanZiService實現
// com.tanhua.server.service.QuanZiService
@Autowired
private PicUploadService picUploadService;
public String savePublish(String textContent,
String location,
String latitude,
String longitude,
MultipartFile[] multipartFile) {
//查詢當前的登錄信息
User user = UserThreadLocal.get();
Publish publish = new Publish();
publish.setUserId(user.getId());
publish.setText(textContent);
publish.setLocationName(location);
publish.setLatitude(latitude);
publish.setLongitude(longitude);
publish.setSeeType(1);
List<String> picUrls = new ArrayList<>();
//圖片上傳
for (MultipartFile file : multipartFile) {
PicUploadResult picUploadResult = this.picUploadService.upload(file);
picUrls.add(picUploadResult.getName());
}
publish.setMedias(picUrls);
return this.quanZiApi.savePublish(publish);
}
7、查詢推薦動態
推薦動態是通過推薦系統計算出的結果,現在我們只需要實現查詢即可,推薦系統在后面的課程中完成。
推薦系統計算完成后,會將結果數據寫入到Redis中,數據如下:
192.168.31.81:6379> get QUANZI_PUBLISH_RECOMMEND_1
"2562,3639,2063,3448,2128,2597,2893,2333,3330,2642,2541,3002,3561,3649,2384,2504,3397,2843,2341,2249"
可以看到,在Redis中的數據是有多個發布id組成(pid)由逗號分隔。所以實現中需要自己對這些數據做分頁處理。
7.1、dubbo服務
7.1.1、定義接口
//com.tanhua.dubbo.server.api.QuanZiApi
/**
* 查詢推薦動態
*
* @param userId 用戶id
* @param page 當前頁數
* @param pageSize 每一頁查詢的數據條數
* @return
*/
PageInfo<Publish> queryRecommendPublishList(Long userId, Integer page, Integer pageSize);
7.1.2、編寫實現
//com.tanhua.dubbo.server.api.QuanZiApiImpl
@Autowired
private RedisTemplate<String, String> redisTemplate;
public PageInfo<Publish> queryRecommendPublishList(Long userId, Integer page, Integer pageSize) {
PageInfo<Publish> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);
// 查詢推薦結果數據
String key = "QUANZI_PUBLISH_RECOMMEND_" + userId;
String data = this.redisTemplate.opsForValue().get(key);
if (StrUtil.isEmpty(data)) {
return pageInfo;
}
//查詢到的pid進行分頁處理
List<String> pids = StrUtil.split(data, ',');
//計算分頁
//[0, 10]
int[] startEnd = PageUtil.transToStartEnd(page - 1, pageSize);
int startIndex = startEnd[0]; //開始
int endIndex = Math.min(startEnd[1], pids.size()); //結束
List<Long> pidLongList = new ArrayList<>();
for (int i = startIndex; i < endIndex; i++) {
pidLongList.add(Long.valueOf(pids.get(i)));
}
if (CollUtil.isEmpty(pidLongList)) {
//沒有查詢到數據
return pageInfo;
}
//根據pid查詢publish
Query query = Query.query(Criteria.where("pid").in(pidLongList))
.with(Sort.by(Sort.Order.desc("created")));
List<Publish> publishList = this.mongoTemplate.find(query, Publish.class);
if (CollUtil.isEmpty(publishList)) {
//沒有查詢到數據
return pageInfo;
}
pageInfo.setRecords(publishList);
return pageInfo;
}
7.2、APP服務
地址:https://mock-java.itheima.net/project/35/interface/api/677
通過接口的定義可以看出,其響應的數據結構與好友動態結構一樣,所以可以復用QuanZiVo對象。
7.2.1、QuanZiController
//com.tanhua.server.controller.QuanZiController
/**
* 查詢推薦動態
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("recommend")
public PageResult queryRecommendPublishList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
return this.quanZiService.queryRecommendPublishList(page, pageSize);
}
7.2.2、QuanZiService
在實現中,將查詢好友動態的方法中公共的內容,進行抽取,具體如下:
//com.tanhua.server.service.QuanZiService
public PageResult queryRecommendPublishList(Integer page, Integer pageSize) {
//分析:通過dubbo中的服務查詢系統推薦動態
//通過mysql查詢用戶的信息,回寫到結果對象中(QuanZiVo)
PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);
//直接從ThreadLocal中獲取對象
User user = UserThreadLocal.get();
//通過dubbo查詢數據
PageInfo<Publish> pageInfo = this.quanZiApi.queryRecommendPublishList(user.getId(), page, pageSize);
List<Publish> records = pageInfo.getRecords();
if (CollUtil.isEmpty(records)) {
return pageResult;
}
pageResult.setItems(this.fillQuanZiVo(records));
return pageResult;
}
/**
* 填充用戶信息
*
* @param userInfo
* @param quanZiVo
*/
private void fillUserInfoToQuanZiVo(UserInfo userInfo, QuanZiVo quanZiVo){
BeanUtil.copyProperties(userInfo, quanZiVo, "id");
quanZiVo.setGender(userInfo.getSex().name().toLowerCase());
quanZiVo.setTags(StringUtils.split(userInfo.getTags(), ','));
quanZiVo.setCommentCount(0); //TODO 評論數
quanZiVo.setDistance("1.2公里"); //TODO 距離
quanZiVo.setHasLiked(0); //TODO 是否點贊(1是,0否)
quanZiVo.setLikeCount(0); //TODO 點贊數
quanZiVo.setHasLoved(0); //TODO 是否喜歡(1是,0否)
quanZiVo.setLoveCount(0); //TODO 喜歡數
}
/**
* 根據查詢到的publish集合填充QuanZiVo對象
*
* @param records
* @return
*/
private List<QuanZiVo> fillQuanZiVo(List<Publish> records){
List<QuanZiVo> quanZiVoList = new ArrayList<>();
records.forEach(publish -> {
QuanZiVo quanZiVo = new QuanZiVo();
quanZiVo.setId(publish.getId().toHexString());
quanZiVo.setTextContent(publish.getText());
quanZiVo.setImageContent(publish.getMedias().toArray(new String[]{}));
quanZiVo.setUserId(publish.getUserId());
quanZiVo.setCreateDate(RelativeDateFormat.format(new Date(publish.getCreated())));
quanZiVoList.add(quanZiVo);
});
//查詢用戶信息
List<Object> userIds = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);
for (QuanZiVo quanZiVo : quanZiVoList) {
//找到對應的用戶信息
for (UserInfo userInfo : userInfoList) {
if(quanZiVo.getUserId().longValue() == userInfo.getUserId().longValue()){
this.fillUserInfoToQuanZiVo(userInfo, quanZiVo);
break;
}
}
}
return quanZiVoList;
}