首页 > 其他分享 >美食社交--feed服务

美食社交--feed服务

时间:2024-06-12 14:56:18浏览次数:12  
标签:feed Feed -- com itkaka import feeds 社交 id

前言: 本篇博客 Feed服务 是美食社交项目内第六篇文章, 是写在好友服务之后的一篇博客,大家可以先尝试阅读并编写第五篇–美食社交好友服务的文章之后再阅读此文,效果更佳;因为两者是有联系的!

1. 概念
移动互联网时代,Feed流产品非常常见,比如 朋友圈,微博,非常典型的Feed流产品,还有图片分享网站Pinterest,花瓣网等又是另一种形式的Feed流产品。除此之外,很多App的都会有一个模块,要么叫动态,要么叫消息广场,这些也是Feed流产品,可以说,Feed流产品是遍布天下所有的App中。


Feed:Feed流中的每一条状态或者消息都是Feed,比如朋友圈中的一个状态就是一个Feed,微博中的一条微博就是一个Feed。
Feed流:持续更新并呈现给用户内容的信息流。每个人的朋友圈,微博关注页等等都是一个Feed流。
Timeline:Timeline其实是一种Feed流的类型,微博,朋友圈都是Timeline类型的Feed流,但是由于Timeline类型出现最早,使用最广泛,最为人熟知,有时候也用Timeline来表示Feed流。
关注页Timeline:展示其他人Feed消息的页面,比如朋友圈,微博的首页等。
个人页Timeline:展示自己发送过的Feed消息的页面,比如微信中的相册,微博的个人页等。
2. 特征
多账号内容流:Feed流系统中肯定会存在成千上万的账号,账号之间可以关注,取关,加好友和拉黑等操作。只要满足这一条,那么就可以当做Feed流系统来设计。
非稳定的账号关系:由于存在关注,取关等操作,所以系统中的用户之间的关系就会一直在变化,是一种非稳定的状态。
读写比例100:1:读写严重不平衡,读多写少,一般读写比例在10:1,甚至100:1以上。
消息必达性要求高:比如发送了一条朋友圈后,结果部分朋友看到了,部分朋友没看到,如果偏偏女朋友没看到,那么可能会产生很严重的感情矛盾,后果很严重。
3. 分类
Timeline:按发布的时间顺序排序,先发布的先看到,后发布的排列在最顶端,类似于微信朋友圈,微博等。这也是一种最常见的形式。产品如果选择Timeline类型,那么就是认为 Feed流中的Feed不多,但是每个Feed都很重要,都需要用户看到 。
Rank:按某个非时间的因子排序,一般是按照用户的喜好度排序,用户最喜欢的排在最前面,次喜欢的排在后面。这种一般假定用户可能看到的Feed非常多,而用户花费在这里的时间有限,那么就为用户选择出用户最想看的Top N结果,场景的应用场景有图片分享、新闻推荐类、商品推荐等。
4. 实现
解决Feed流最核心的两个问题:一个是存储,另一个是推送
4.1 存储
因为该项目中Feed比较简单,就类比于空间说说,因此可以使用MySQL关系型数据库存储,如果对于数据结构比较复杂的Feed流就要使用NoSQL数据库,这样存储更方便与高效,比如MongoDB或者HBase
4.2 实现
在推送方案里面的,有三种方案,分别是:

拉方案:也称为 读扩散 ,用户主动去拉取关注人的Feed内容
推方案:也成为 写扩散 ,当用户添加Feed时,会自动将Feed通知给关注的人(优选)
使用Redis Sorted Sets(方便按时间排序Timeline)维护粉丝的Feed集合,当博主添加Feed时,主动将内容推送到粉丝的Feed集合中,这样用户可以很方便快速从集合中读取

推拉结合:比如微博,大部分用户的账号关系都是几百个,但是有个别用户是1000万以上才使用。
5. 表结构设计
CREATE TABLE `t_feeds` (
`id` int(11) NOT NULL AUTO_INCREMENT ,
`content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL
DEFAULT NULL COMMENT '内容' ,
`fk_diner_id` int(11) NULL DEFAULT NULL ,
`praise_amount` int(11) NULL DEFAULT NULL COMMENT '点赞数量' ,
`comment_amount` int(11) NULL DEFAULT NULL COMMENT '评论数量' ,
`fk_restaurant_id` int(11) NULL DEFAULT NULL ,
`create_date` datetime NULL DEFAULT NULL ,
`update_date` datetime NULL DEFAULT NULL ,
`is_valid` tinyint(1) NULL DEFAULT NULL ,
PRIMARY KEY (`id`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
AUTO_INCREMENT=14
ROW_FORMAT=COMPACT
;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
6. 构建模块
创建 fs_feeds 子模块
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>food_social</artifactId>
<groupId>com.itkaka</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>fs_feeds</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

<dependencies>
<!-- eureka client -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<!-- spring web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- sprin data redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- mybatis -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
</dependency>
<!-- commons 公共项目 -->
<dependency>
<groupId>com.itkaka</groupId>
<artifactId>fs_commons</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>

<!-- 集中定义项目所需插件 -->
<build>
<plugins>
<!-- spring boot maven 项目打包插件 -->
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
配置文件

server:
port: 8095 # 端口

spring:
application:
name: fs_feeds # 应用名
# 数据库
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
url: jdbc:mysql://127.0.0.1:3306/db_lezijie_food_social?serverTimezone=Asia/Shanghai&characterEncoding=utf8&useUnicode=true&useSSL=false
# Redis
redis:
port: 6379
host: 192.168.10.101
timeout: 3000
password: 123456
database: 2
# Swagger
swagger:
base-package: com.imooc.feeds
title: 美食社交Feed流API接口文档

# 配置 Eureka Server 注册中心
eureka:
instance:
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:8090/eureka/

service:
name:
fs-oauth-server: http://fs_oauth/
fs-diners-server: http://fs_diners/
fs-follow-server: http://fs_follow/

mybatis:
configuration:
map-underscore-to-camel-case: true # 开启驼峰映射

# 配置日志
logging:
pattern:
console: '%d{2100-01-01 13:14:00.666} [%thread] %-5level %logger{50} - %msg%n'

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
7. 编写实体类
package com.itkaka.feeds.model.pojo;

import com.itkaka.commons.model.base.BaseModel;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

@ApiModel(description = "Feed信息类")
@Getter
@Setter
public class Feeds extends BaseModel {

@ApiModelProperty("内容")
private String content;
@ApiModelProperty("食客")
private Integer fkDinerId;
@ApiModelProperty("点赞")
private int praiseAmount;
@ApiModelProperty("评论")
private int commentAmount;
@ApiModelProperty("关联的餐厅")
private Integer fkRestaurantId;

}

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
7.1 配置类以及全局异常处理
Redis配置类和Rest配置类,还有全局异常处理。 同之前模块一样,拷贝即可
7.2 此时项目结构如下图:

8. 添加Feed功能
8.1 FeedsMapper添加操作
package com.itkaka.feeds.mapper;

import com.itkaka.feeds.model.pojo.Feeds;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Options;

/**
* Feed服务 Mapper
*/
public interface FeedsMapper {

// Feed 添加
@Insert("insert into t_feeds (content, fk_diner_id, praise_amount, comment_amount,fk_restaurant_id, create_date, update_date, is_valid)" +
" values (#{content}, #{fkDinerId}, #{praiseAmount}, #{commentAmount},#{fkRestaurantId}, now(), now(), 1)")
@Options(useGeneratedKeys = true,keyProperty = "id")
int save(Feeds feeds);


}


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
8.2 RedisKeyConstant
package com.itkaka.commons.constant;

import lombok.Getter;

@Getter
public enum RedisKeyConstant {

verify_code("verify_code:", "验证码"),
seckill_vouchers("seckill_vouchers:", "秒杀券的key"),
lock_key("lockby:", "分布式锁的key"),
following("following:", "关注集合Key"),
followers("followers:", "粉丝集合Key"),
// 新增这部分
following_feeds("following_feeds:", "我关注的好友的FeedsKey"),
;

private String key;
private String desc;

RedisKeyConstant(String key, String desc) {
this.key = key;
this.desc = desc;
}

}

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
8.3 FeedsService中添加业务逻辑
package com.itkaka.feeds.service;

import cn.hutool.core.bean.BeanUtil;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.constant.RedisKeyConstant;
import com.itkaka.commons.exception.ParameterException;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.vo.SignInDinerInfo;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.feeds.mapper.FeedsMapper;
import com.itkaka.feeds.model.pojo.Feeds;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Feed服务 Service
*/
@Service
public class FeedsService {

@Value("${service.name.fs_oauth-server}")
private String oauthServerName;
@Value("${service.name.fs_diners-server}")
private String dinersServerName;
@Value("${service.name.fs_follow-server}")
private String followServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;
@Resource
private FeedsMapper feedsMapper;

/**
* 添加 Feed
*
* @param feeds
* @param accessToken
*/
@Transactional(rollbackFor = Exception.class)
public void create(Feeds feeds,String accessToken){
// 非空校验
AssertUtil.isNotEmpty(feeds.getContent(),"其内容不为空,请输入内容!");
AssertUtil.isTrue(feeds.getContent().length() > 255,"输入内容太多,请重新输入");

//获取登录用户信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// Feed 关联用户信息
feeds.setFkDinerId(dinerInfo.getId());
// 添加 Feed
int count = feedsMapper.save(feeds);
AssertUtil.isTrue(count == 0,"添加失败!");
//推送到粉丝列表 首先拿我的粉丝列表
List<Integer> followers = findFollowers(dinerInfo.getId());
// 推送 Feeds, 以时间作为分数存储到 Redis ZSet
long now = System.currentTimeMillis();
followers.forEach(follower -> {
String key = RedisKeyConstant.following_feeds.getKey() + follower;
redisTemplate.opsForZSet().add(key,feeds.getId(),now);
});

}

//获取粉丝列表篇
private List<Integer> findFollowers(Integer dinerId) {
String url = followServerName + "followers/" +dinerId;
ResultInfo resultInfo = restTemplate.getForObject(url,ResultInfo.class);

if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE){
throw new ParameterException(resultInfo.getCode(),resultInfo.getMessage());
}

return (List<Integer>) resultInfo.getData();
}

//获取登录用户信息
private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
// 令牌是否存在
AssertUtil.mustLogin(accessToken);
// 拼接 URL
String url = oauthServerName + "user/me?access_token={accessToken}";
// 发送请求
ResultInfo resultInfo = restTemplate.getForObject(url,ResultInfo.class,accessToken);

if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE){
throw new ParameterException(resultInfo.getCode(),resultInfo.getMessage());
}

SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),new SignInDinerInfo(),false);
return dinerInfo;
}


}


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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
8.4 FeedsController方法
package com.itkaka.feeds.controller;

import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.feeds.model.pojo.Feeds;
import com.itkaka.feeds.service.FeedsService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
* Feed服务 Controller
*/
@RestController
public class FeedsController {

@Resource
private FeedsService feedsService;
@Resource
private HttpServletRequest request;

/**
* 添加 Feed
*
* @param feeds
* @param access_token
* @return
*/
@PostMapping
public ResultInfo<String> create(@RequestBody Feeds feeds, String access_token) {
feedsService.create(feeds, access_token);
return ResultInfoUtil.buildSuccess(request.getServletPath(), "添加成功");
}

}


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
34
35
36
37
38
39
8.5 网关配置
:::info
注意配置文件的缩进
:::

- id: fs_feeds
uri: lb://fs-feeds
predicates:
- Path=/feeds/**
filters:
- StripPrefix=1
1
2
3
4
5
6
server:
port: 80 # 端口

spring:
application:
name: fs_gateway # 应用名
cloud:
gateway:
discovery:
locator:
enabled: true # 开启配置注册中心进行路由功能
lower-case-service-id: true # 将服务名称转小写
routes:
- id: fs_diners
uri: lb://fs_diners
predicates:
- Path=/diners/**
filters:
- StripPrefix=1

- id: fs_oauth
uri: lb://fs_oauth
predicates:
- Path=/auth/**
filters:
- StripPrefix=1

- id: fs_seckill
uri: lb://fs_seckill
predicates:
- Path=/seckill/**
filters:
- StripPrefix=1

- id: fs_follow
uri: lb://fs_follow
predicates:
- Path=/follow/**
filters:
- StripPrefix=1

- id: fs_feeds
uri: lb://fs_feeds
predicates:
- Path=/feeds/**
filters:
- StripPrefix=1


secure:
ignore:
urls: # 配置白名单路径
- /actuator/**
- /auth/oauth/**
- /diners/signin
- /diners/send
- /diners/checkPhone
- /diners/register
- /seckill/add
- /restaurants/detail/**

# 配置 Eureka Server 注册中心
eureka:
instance:
# 开启 ip 注册
prefer-ip-address: true
instance-id: ${spring.cloud.client.ip-address}:${server.port}
client:
service-url:
defaultZone: http://localhost:8090/eureka/

# 配置日志
logging:
pattern:
console: '%d{2100-01-01 13:14:00.666} [%thread] %-5level %logger{50} - %msg%n'


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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
获取粉丝列表
通过关注微服务获取用户的粉丝列表ID
fs-follow微服务中添加获取粉丝列表的方法
fs-follow的FollowService中添加获取粉丝方法

/**
* 获取粉丝列表
*
* @param dinerId
* @return
*/
public Set<Integer> findFollowers(Integer dinerId) {
AssertUtil.isNotNull(dinerId, "请选择查看的用户");
Set<Integer> followers = redisTemplate.opsForSet()
.members(RedisKeyConstant.followers.getKey() + dinerId);
return followers;
}
1
2
3
4
5
6
7
8
9
10
11
12
fs-follow的FollowController中添加

/**
* 获取粉丝列表
*
* @param dinerId
* @return
*/
@GetMapping("followers/{dinerId}")
public ResultInfo findFollowers(@PathVariable Integer dinerId) {
Set<Integer> followers = followService.findFollowers(dinerId);
return ResultInfoUtil.buildSuccess(request.getServletPath(), followers);
}
1
2
3
4
5
6
7
8
9
10
11
测试
访问:http://localhost/feeds?access_token=a37b83ee-81e7-4bf1-8255-d3ea83bc327f

 

9. 删除 Feed
9.1 FeedsMapper操作
// 逻辑删除 Feed
@Update("update t_feeds set is_valid = 0, update_date = now() where id = #{id} and is_valid = 1")
int delete(@Param("id") Integer id);
1
2
3
9.2 FeedsService编写删除逻辑
package com.itkaka.feeds.service;

import cn.hutool.core.bean.BeanUtil;
import com.itkaka.commons.constant.ApiConstant;
import com.itkaka.commons.constant.RedisKeyConstant;
import com.itkaka.commons.exception.ParameterException;
import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.model.vo.SignInDinerInfo;
import com.itkaka.commons.utils.AssertUtil;
import com.itkaka.feeds.mapper.FeedsMapper;
import com.itkaka.feeds.model.pojo.Feeds;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;

import javax.annotation.Resource;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Feed服务 Service
*/
@Service
public class FeedsService {

@Value("${service.name.fs_oauth-server}")
private String oauthServerName;
@Value("${service.name.fs_diners-server}")
private String dinersServerName;
@Value("${service.name.fs_follow-server}")
private String followServerName;
@Resource
private RestTemplate restTemplate;
@Resource
private RedisTemplate redisTemplate;
@Resource
private FeedsMapper feedsMapper;

/**
* 添加 Feed
*
* @param feeds
* @param accessToken
*/
@Transactional(rollbackFor = Exception.class)
public void create(Feeds feeds,String accessToken){
// 非空校验
AssertUtil.isNotEmpty(feeds.getContent(),"其内容不为空,请输入内容!");
AssertUtil.isTrue(feeds.getContent().length() > 255,"输入内容太多,请重新输入");

//获取登录用户信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// Feed 关联用户信息
feeds.setFkDinerId(dinerInfo.getId());
// 添加 Feed
int count = feedsMapper.save(feeds);
AssertUtil.isTrue(count == 0,"添加失败!");
//推送到粉丝列表 首先拿我的粉丝列表
List<Integer> followers = findFollowers(dinerInfo.getId());
// 推送 Feeds, 以时间作为分数存储到 Redis ZSet
long now = System.currentTimeMillis();
followers.forEach(follower -> {
String key = RedisKeyConstant.following_feeds.getKey() + follower;
redisTemplate.opsForZSet().add(key,feeds.getId(),now);
});

}

//获取粉丝列表篇
private List<Integer> findFollowers(Integer dinerId) {
String url = followServerName + "followers/" +dinerId;
ResultInfo resultInfo = restTemplate.getForObject(url,ResultInfo.class);

if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE){
throw new ParameterException(resultInfo.getCode(),resultInfo.getMessage());
}

return (List<Integer>) resultInfo.getData();
}

//获取登录用户信息
private SignInDinerInfo loadSignInDinerInfo(String accessToken) {
// 令牌是否存在
AssertUtil.mustLogin(accessToken);
// 拼接 URL
String url = oauthServerName + "user/me?access_token={accessToken}";
// 发送请求
ResultInfo resultInfo = restTemplate.getForObject(url,ResultInfo.class,accessToken);

if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE){
throw new ParameterException(resultInfo.getCode(),resultInfo.getMessage());
}

SignInDinerInfo dinerInfo = BeanUtil.fillBeanWithMap((LinkedHashMap) resultInfo.getData(),new SignInDinerInfo(),false);
return dinerInfo;
}

/**
* 删除 Feed
*
* @param id
* @param accessToken
*/
@Transactional(rollbackFor = Exception.class)
public void delete(Integer id,String accessToken){
// 参数校验
AssertUtil.isTrue(id == null || id <1,"请选择要删除的Feed");
// 获取登录用户
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// 获取 Feed
Feeds feeds = feedsMapper.findById(id);
// 判断 Feed 是否已被删除 且 只能删除自己的 Feed
AssertUtil.isTrue(feeds == null,"该 Feed 已被删除!");
AssertUtil.isTrue(!feeds.getFkDinerId().equals(dinerInfo.getId()),"只能删除自己的Feed");
// 执行删除操作
int count = feedsMapper.delete(id);
if (count == 0){
return;
}
// 将内容从粉丝关注的 Feed 集合中删除 先拿我的粉丝列表
List<Integer> followers = findFollowers(dinerInfo.getId());
followers.forEach(follower ->{
String key = RedisKeyConstant.following_feeds.getKey() + follower;
redisTemplate.opsForZSet().remove(key,feeds.getId());
});
}

}


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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
由于调用查询接口,因此需要在持久层新建查询语句,完善之后的mapper如下

package com.itkaka.feeds.mapper;

import com.itkaka.feeds.model.pojo.Feeds;
import org.apache.ibatis.annotations.*;

/**
* Feed服务 Mapper
*/
public interface FeedsMapper {

// Feed 添加
@Insert("insert into t_feeds (content, fk_diner_id, praise_amount, comment_amount,fk_restaurant_id, create_date, update_date, is_valid)" +
" values (#{content}, #{fkDinerId}, #{praiseAmount}, #{commentAmount},#{fkRestaurantId}, now(), now(), 1)")
@Options(useGeneratedKeys = true,keyProperty = "id")
int save(Feeds feeds);

// 逻辑删除 Feed
@Update("update t_feeds set is_valid = 0, update_date = now() where id = #{id} and is_valid = 1")
int delete(@Param("id") Integer id);

// 查询 Feed
@Select("select id,content, fk_diner_id, praise_amount, comment_amount,fk_restaurant_id, create_date, update_date, is_valid" +
" from t_feeds where id = #{id} and is_valid = 1")
Feeds findById(@Param("id") Integer id);
}


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
9.3 FeedsController编写API方法
package com.itkaka.feeds.controller;

import com.itkaka.commons.model.domain.ResultInfo;
import com.itkaka.commons.utils.ResultInfoUtil;
import com.itkaka.feeds.model.pojo.Feeds;
import com.itkaka.feeds.service.FeedsService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;

/**
* Feed服务 Controller
*/
@RestController
public class FeedsController {

@Resource
private FeedsService feedsService;
@Resource
private HttpServletRequest request;

/**
* 添加 Feed
*
* @param feeds
* @param access_token
* @return
*/
@PostMapping
public ResultInfo<String> create(@RequestBody Feeds feeds, String access_token) {
feedsService.create(feeds, access_token);
return ResultInfoUtil.buildSuccess(request.getServletPath(), "添加成功");
}

/**
* 删除 Feed
*
* @param id
* @param access_token
* @return
*/
@DeleteMapping("{id}")
public ResultInfo<String> delete(@PathVariable Integer id, String access_token) {
feedsService.delete(id, access_token);
return ResultInfoUtil.buildSuccess(request.getServletPath(), "删除成功");
}

}


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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
9.4 Postman测试
访问:http://localhost/feeds/15?access_token=a37b83ee-81e7-4bf1-8255-d3ea83bc327f

 

10. [修改Feed] 关注/取关时处理用户
当A用户关注B用户时,那么要实时的将B的所有Feed推送到A用户的Feed集合中,同样如果A用户取关B用户,那么要将B用户所有的Feed从A用户的Feed集合中移除。
10.1 FeedsMapper查询方法
/**
* 根据食客 ID 查询 Feed
*/
@Select("SELECT id, content, update_date FROM t_feeds " +
"WHERE fk_diner_id = #{dinerId} AND is_valid = 1")
List<Feeds> findByDinerId(@Param("dinerId") Integer dinerId);
1
2
3
4
5
6
10.2 Service层处理添加和移除逻辑
/**
* 变更 Feed
*
* @param followingDinerId 关注的好友的 ID
* @param accessToken 登录用户的 token
* @param type 1 关注 0 取关
*/
@Transactional(rollbackFor = Exception.class)
public void addFollowingFeeds(Integer followingDinerId, String accessToken, int type) {
// 参数校验
AssertUtil.isTrue(followingDinerId == null || followingDinerId < 1,
"请选择关注或取关的好友");

// 获取登录用户
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);

// 获取关注/取关食客所有 Feed
List<Feeds> followingFeeds = feedsMapper.findByDinerId(followingDinerId);
if (followingFeeds == null || followingFeeds.isEmpty()) {
return;
}

// 我关注的好友的 FeedsKey
String key = RedisKeyConstant.following_feeds.getKey() + dinerInfo.getId();

// 取关
if (type == 0) {
List<Integer> feedIds = followingFeeds.stream()
.map(feed -> feed.getId())
.collect(Collectors.toList());
redisTemplate.opsForZSet().remove(key, feedIds.toArray(new Integer[]{}));
} else {
// 关注
Set<ZSetOperations.TypedTuple> typedTuples = followingFeeds.stream()
.map(feed -> new DefaultTypedTuple<>(feed.getId(), (double) feed.getUpdateDate().getTime()))
.collect(Collectors.toSet());
redisTemplate.opsForZSet().add(key, typedTuples);
}
}

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
34
35
36
37
38
39
10.3 在Controller层变API方法
/**
* 变更 Feed
*
* @param followingDinerId
* @param access_token
* @param type
* @return
*/
@PostMapping("updateFollowingFeeds/{followingDinerId}")
public ResultInfo<String> addFollowingFeeds(@PathVariable Integer followingDinerId,
String access_token, int type) {
feedsService.addFollowingFeeds(followingDinerId, access_token, type);
return ResultInfoUtil.buildSuccess(request.getServletPath(), "操作成功");
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
10.4 follow微服务中的关注和取关接口中添加Feed操作的方法
service:
name:
fs-oauth-server: http://fs_oauth/
fs-diners-server: http://fs_diners/
fs-feeds-server: http://fs_feeds/
1
2
3
4
5
在以下地方进行修改:

@Value("${service.name.fs-feeds-server}")
private String feedsServerName;
/**
* 发送请求添加或者移除关注人的Feed列表
*
* @param followDinerId 关注好友的ID
* @param accessToken 当前登录用户token
* @param type 0=取关 1=关注
*/
private void sendSaveOrRemoveFeed(Integer followDinerId, String accessToken, int type) {
String feedsUpdateUrl = feedsServerName + "updateFollowingFeeds/" + followDinerId + "?access_token=" + accessToken;

// 构建请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

// 构建请求体(请求参数)
MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
body.add("type", type);
HttpEntity<MultiValueMap<String, Object>> entity = new HttpEntity<>(body,headers);
restTemplate.postForEntity(feedsUpdateUrl, entity, ResultInfo.class);
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
以下地方也需修改完善:

// 如果没有关注信息,且要进行关注操作
if (follow == null && isFollowed == 1) {
// 添加关注信息
int count = followMapper.save(dinerInfo.getId(), followDinerId);
// 添加关注列表到 Redis
if (count == 1) {
addToRedisSet(dinerInfo.getId(), followDinerId);
// 保存 Feed
sendSaveOrRemoveFeed(followDinerId, accessToken, 1);
}
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE,"关注成功", path, "关注成功");
}


// 如果有关注信息,且目前处于取关状态,且要进行关注操作
if (follow != null && follow.getIsValid() == 0 && isFollowed == 1) {
// 重新关注
int count = followMapper.update(follow.getId(), isFollowed);
// 添加关注列表
if (count == 1) {
addToRedisSet(dinerInfo.getId(), followDinerId);
// 保存 Feed
sendSaveOrRemoveFeed(followDinerId, accessToken, 1);
}
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE, "关注成功", path, "关注成功");
}


// 如果有关注信息,且目前处于关注中状态,且要进行取关操作
if (follow != null && follow.getIsValid() == 1 && isFollowed == 0) {
// 取关
int count = followMapper.update(follow.getId(), isFollowed);
if (count == 1) {
// 移除 Redis 关注列表
removeFromRedisSet(dinerInfo.getId(), followDinerId);
// 移除 Feed
sendSaveOrRemoveFeed(followDinerId, accessToken, 0);
}
return ResultInfoUtil.build(ApiConstant.SUCCESS_CODE, "成功取关", path, "成功取关");
}

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
34
35
36
37
38
39
40
Postman测试
访问:http://localhost/follow/2?isFollowed=1&access_token=116af711-645c-4ce9-bc32-cd304bdda020
先让 ID 5 用户关注 ID 2 用户。

访问:http://localhost/follow/2?isFollowed=0&access_token=116af711-645c-4ce9-bc32-cd304bdda020
然后让 ID 5 用户取关 ID 2 用户。


11. 查询 Feed
11.1 构建返回的FeedsVO
package com.itkaka.feeds.model.vo;

import com.fasterxml.jackson.annotation.JsonFormat;
import com.itkaka.commons.model.base.BaseModel;
import com.itkaka.commons.model.vo.ShortDinerInfo;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Getter;
import lombok.Setter;

import java.util.Date;
@ApiModel(description = "Feed显示信息")
@Getter
@Setter
public class FeedsVo extends BaseModel {

@ApiModelProperty("主键")
private Integer id;
@ApiModelProperty("内容")
private String content;
@ApiModelProperty("点赞数")
private int praiseAmount;
@ApiModelProperty("评论数")
private int commentAmount;
@ApiModelProperty("餐厅")
private Integer fkRestaurantId;
@ApiModelProperty("用户ID")
private Integer fkDinerId;
@ApiModelProperty("用户信息")
private ShortDinerInfo dinerInfo;
@ApiModelProperty("显示时间")
@JsonFormat(pattern = "yyyy-MM-dd HH:mm")
public Date createDate;


}


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
34
35
36
37
⭐11.2 FeedsMapper查询方法
/**
* 根据多主键查询 Feed
*/
@Select("<script> " +
" select id, content, fk_diner_id, praise_amount, comment_amount, " +
" fk_restaurant_id, create_date, update_date, is_valid " +
" from t_feeds where is_valid = 1 and id in " +
" <foreach item='id' collection='feedIds' open='(' separator=',' close=')'> " +
" #{id} " +
" </foreach> order by id desc" +
" </script>")
List<Feeds> findFeedsByIds(@Param("feedIds") Set<Integer> feedIds);
1
2
3
4
5
6
7
8
9
10
11
12
11.3 ApiConstant
// Feed 默认每页条数
public static final int PAGE_SIZE = 20;
1
2
⭐11.4 编写service查询Feed的方法
获取登录用户信息
构建分页查询的参数start,end
从Redis的sorted sets中按照score的降序进行读取Feed的id
从数据库中获取Feed的信息
构建Feed关联的用户信息(不是循环逐条读取,而是批量获取)
/**
* 根据时间由近到远,每次查询 20 条 Feed
*
* @param page 页码
* @param accessToken 登录用户信息
* @return
*/
public List<FeedsVo> selectForPage(Integer page, String accessToken) {
if (page == null) {
page = 1;
}
// 获取登录用户信息
SignInDinerInfo dinerInfo = loadSignInDinerInfo(accessToken);
// 我关注的好友的 FeedsKey
String key = RedisKeyConstant.following_feeds.getKey() + dinerInfo.getId();
// SortedSet 的 ZREVRANGE 命令是闭区间(左闭右闭)
long start = (page - 1) * ApiConstant.PAGE_SIZE;
long end = page * ApiConstant.PAGE_SIZE - 1;
// 获取 20 条 Feed ID
Set<Integer> feedIds = redisTemplate.opsForZSet().reverseRange(key, start, end); // 闭区间,左闭右闭
if (feedIds == null || feedIds.isEmpty()) {
return Lists.newArrayList();
}
// 根据多主键查询 Feed
List<Feeds> feeds = feedsMapper.findFeedsByIds(feedIds);
// 初始化关注好友 ID 集合
List<Integer> followindDinerIds = new ArrayList<>();
// 添加用户 ID 至集合,顺带将 Feeds 转为 VO 对象
List<FeedsVo> feedsVos = feeds.stream()
.map(feed -> {
FeedsVo feedsVo = new FeedsVo();
BeanUtil.copyProperties(feed, feedsVo);
// 添加用户 ID 至集合
followindDinerIds.add(feed.getFkDinerId());
return feedsVo;
}).collect(Collectors.toList());
// 获取 Feed 中用户信息
ResultInfo resultInfo = restTemplate.getForObject(dinersServerName +
"findByIds?access_token={accessToken}&ids={ids}", ResultInfo.class,
accessToken, StrUtil.join(",", followindDinerIds));
if (resultInfo.getCode() != ApiConstant.SUCCESS_CODE) {
throw new ParameterException(resultInfo.getCode(), resultInfo.getMessage());
}
List<LinkedHashMap> dinerInfoMaps = (List<LinkedHashMap>) resultInfo.getData();
// 构建一个 Key 为用户 ID,Value 为 ShortDinerInfo 的 Map
Map<Integer, ShortDinerInfo> dinerInfos = dinerInfoMaps.stream()
.collect(Collectors.toMap(
// key
diner -> (Integer) diner.get("id"),
// value
diner -> BeanUtil.fillBeanWithMap(diner, new ShortDinerInfo(), false)
));
// 循环 vo 集合,根据用户 ID 从 Map 中获取用户信息并设置至 vo 对象
feedsVos.forEach(feedsVo -> feedsVo.setDinerInfo(dinerInfos.get(feedsVo.getFkDinerId())));
return feedsVos;
}

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
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
11.5 添加controller查询方法
传入页码进行查询,默认是20条

/**
* 分页获取关注的 Feed 数据
*
* @param page
* @param access_token
* @return
*/
@GetMapping("{page}")
public ResultInfo selectForPage(@PathVariable Integer page, String access_token) {
List<FeedsVo> feedsVos = feedsService.selectForPage(page, access_token);
return ResultInfoUtil.buildSuccess(request.getServletPath(), feedsVos);
}
1
2
3
4
5
6
7
8
9
10
11
12
Postman测试
访问:http://localhost/feeds/1?access_token=116af711-645c-4ce9-bc32-cd304bdda020


写在最后
Feed功能
这个功能中我们实现了添加 Feed、删除 Feed、关注取关时变更 Feed、查询 Feed 功能。
这个功能中 Redis 主要用于存储每个用户关注好友添加的 Feed 流集合,使用了 Sorted Set 数据类型。
————————————————

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。

原文链接:https://blog.csdn.net/Kaka_csdn14/article/details/130694205

标签:feed,Feed,--,com,itkaka,import,feeds,社交,id
From: https://www.cnblogs.com/gaoyanbing/p/18243938

相关文章

  • xpath使用contains文本定位不到元素的原因及解决方法
    某些情况下,前端开发可能出现如下的代码<uni-viewdata-v-fc36b70f=""class="letter_city_item">"波特兰;"<spandata-v-fc36b70f=""class="gray">PDX</span></uni-view><uni-viewdata-v-fc36b70f=&quo......
  • BIOS 编辑和修改的需求;编辑和修改 BIOS 的工具。以下是几款常见的同类工具
    AwardBIOSEditor是一种用于编辑和修改基于AwardBIOS(BasicInput/OutputSystem)的工具。BIOS是计算机主板上的一个固件,它在计算机启动过程中扮演关键角色,负责硬件初始化以及操作系统启动前的准备工作。特点与功能:BIOS修改: AwardBIOSEditor允许用户查看和修改BIOS......
  • 360 数字加固。某60 xposed hook 入口点
      XposedHelpers.findAndHookMethod("com.stub.StubApp",loadPackageParam.classLoader,"attachBaseContext",Context.class,newXC_MethodHook(){@Overr......
  • Netty--聊天业务
    1.聊天模块细分微服务:用户服务:处理用户身份验证、授权和管理。包括用户注册、登录、个人信息管理等功能。聊天服务:处理实时聊天功能,包括消息发送、接收和存储。可使用WebSocket等技术来实现实时通信。好友服务:管理用户的好友关系,包括好友请求、好友列表和好友关系的维护。群组服......
  • 高一高考集训欢乐赛
    大石碎胸口——万能青年旅店久违的头图渔王还想继续做渔王而海港已经不知去向此刻他醉倒在洗浴中心没有潮汐的梦胸口已暮色苍茫肥胖的城市递给他一个传统的方法来克制恐慌卖掉武器风暴喉咙换取饮食背叛能让你获得自由停电之后暂时摆脱了坚硬的时刻倒转......
  • codelity
    source:https://app.codility.com/programmers/trainings/5/three_letters/classSolution{publicstringsolution(intA,intB){//ImplementyoursolutionhereStringBuilderstr=newStringBuilder();if(A>B){......
  • 2024.06.02《构建之法》阅读笔记
    在以往自己书写代码的时候,每次完成一个小功能都要自己来测试一下,但是这样做的救过并不理想。因为自己写的软件,一些自己可以想到的方面都进行了解决,而一些不能解决的问题是测试也不能够解决的。但是这样写好的程序还有没有BUG呢?而且只写BUG是否容易出现?是否能够解决呢?所以,这时就应......
  • 2024.06.03
    importosimportopenpyxlimportdatetimefromtkinterimport*fromtkinterimportmessagebox#Createthemainwindowjzr=Tk()jzr.title('个人账本记录')jzr.geometry('400x600')jzr.config(bg='lightyellow')#Createatextboxtodisplay......
  • CSP历年复赛题-P5662 [CSP-J2019] 纪念品
    原题链接:https://www.luogu.com.cn/problem/P5662题意解读:n件物品,t天每天有不同的价格,类似股票,初始有m金币,每天都可以无限次买卖,问最后最多可以达到多少金币。解题思路:考试中一定要学会面向数据编程!1、对于 10%10% 的数据,......
  • 2024.06.04《个人总结》
      (大二下)课程总结——软件工程 1)回顾你的课程计划(第一周的计划),你完成的程度如何?请列出具体数据和实际例子。  1.你在这门课的计划是什么?参考一些学校的教学,你对这个课程有什么期待?你打算怎样度过这个课程?    计划就是尽力跟上建民老师的节奏同时,还能主动学习......