《探花交友》

  • 了解项目的背景
  • 了解项目的技术架构、方案
  • 了解项目的功能设计
  • 掌握模拟器的使用
  • 掌握工程的搭建
  • 掌握发送短信验证码
  • 完成用户登录功能

课程介绍

  • 1563457093887-166177245506021功能介绍
  • 项目介绍
  • 工程搭建
  • 短信验证码
  • 实现用户登录功能

1、功能介绍

探花交友是一个陌生人的在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台还会通过大数据计算进行智能推荐,通过智能推荐可以找到更加匹配的好友,这样才能增进用户对产品的喜爱度。探花平台还提供了在线即时通讯功能,可以实时的与好友进行沟通,让沟通随时随地的进行。

1.1、功能列表

功能 说明 备注
注册、登录 用户无需单独注册,直接通过手机号登录即可 首次登录成功后需要完善个人信息
交友 主要功能有:测灵魂、桃花传音、搜附近、探花等
圈子 类似微信朋友圈,用户可以发动态、查看好友动态等
消息 通知类消息 + 即时通讯消息
小视频 类似抖音,用户可以发小视频,评论等 显示小视频列表需要进行推荐算法计算后进行展现。
我的 我的动态、关注数、粉丝数、通用设置等

1.2、注册登录

业务说明:

用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。

image-20231004075313529

image-20231004075309195

image-20231004075301908

image-20231004075257030

image-20231004075252655

image-20231004075248793

1.3、交友

交友是探花项目的核心功能之一,用户可以查看好友,添加好友,搜索好友等操作。

image-20231004075244315

1.3.1、首页

在首页中,主要功能有“今日佳人”、“推荐”、“最近访客”等

  • 今日佳人
    • 按照“缘分值”进行匹配,将“缘分值”最高的用户展现出来
  • 推荐
    • 按照“缘分值”进行推荐,由后台的推荐系统计算得出,展现出来
  • 最近访客
    • 显示最近来看“我”的用户

1.3.2、探花

image-20231004075240213

说明:左划喜欢,右划不喜欢,每天限量不超过100个,开通会员可增加限额。双方互相喜欢则配对成功。

实现:数据来源推荐系统计算后的结果。

1.3.3、搜附近

image-20231004075235576

根据用户当前所在的位置进行查询,并且在10km的范围内进行查询,可以通过筛选按钮进行条件筛选。

1.3.4、桃花传音

功能类似QQ中的漂流瓶,用户可以发送和接收语音消息,陌生人就会接收到消息。

image-20231004075230521

1.3.5、测灵魂

  1. 测试题用于对用户进行分类,每次提交答案后更新用户属性

  2. 测试题在后台进行维护

  3. 测试题测试完后产生结果页可以进行分享

  4. 测试题为顺序回答,回答完初级题解锁下一级问题

  5. 点击锁定问题 显示提示 请先回答上一级问题

image-20231004075226007

image-20231004075221816

image-20231004075216425

1.4、圈子

1、推荐频道为根据问卷及喜好推荐相似用户动态

2、显示内容为用户头像、用户昵称、用户性别、用户年龄、用户标签和用户发布动态

3、图片最多不超过6张或发布一个小视频

4、动态下方显示发布时间距离当时时间,例如10分钟前、3小时前、2天前,显示时间进行取整

5、动态下方显示距离为发布动态地与本地距离

6、显示用户浏览量

7、显示点赞数、评论数 转发数

image-20231004075211997

image-20231004075207699

1.5、消息

消息包含通知类的消息和好友消息。

image-20231004075202945

1.6、小视频

用户可以上传小视频,也可以查看小视频列表,并且可以进行点赞操作。

image-20231004075159256

1.7、我的

显示关注数、喜欢数、粉丝数、我的动态等信息。

image-20231004075154116

image-20231004075149859

2、项目介绍(★★★★★)

2.1、项目背景

在线社交是互联网时代的产物,已成为互联网用户的基础需求之一。移动互联网自2003年起快速发展,促使在线社交逐渐从PC端转移至移动端。移动社交最初以熟人社交为主,以维系熟人关系、共享资源信息的形式存在。随着人们交友需求的延伸,移动社交开始向陌生人社交、兴趣社交等垂直方向发展,形式丰富多样。

2.2、市场分析

探花交友项目定位于 陌生人交友市场

  • 根据《2018社交领域投融资报告》中指出:虽然相比2017年,投融资事件减少29.5%,但是融资的总额却大幅增长,达到68%。
  • 这些迹象说明:社交领域的发展规模正在扩大,而很多没有特色的产品也会被淘汰。而随着那些尾部产品的倒下,对我们来说就是机会,及时抓住不同社交需求的机会。以社交为核心向不同的细分领域衍生正在逐渐走向成熟化。
  • 而我们按照娱乐形式和内容为主两个维度,将社交行业公司分类为:即时通信、内容社群、陌生人社交、泛娱乐社交以及兴趣社交几个领域。
  • 而在2018年社交的各个细分领域下,均有备受资本所关注的项目,根据烯牛数据2018年的报告中,也同样指出:内容社交及陌生人社交为资本重要关注领域,合计融资占比达73%。

image-20231004075144087

根据市场现状以及融资事件来看:陌生人社交、内容社群、兴趣社交在2019年仍然保持强劲的动力,占到近70%的比例,它们仍然是资本市场主要关注领域。从增长率来看陌生人社交的增长速度远远大于其他几类,因此我们要从这个方向入手。

2.3、目标用户群体

从整体年龄段来看:目前目标用户群体主要以30岁以下为主,其中以18-25岁年龄群体为主要受众人群。

  • 上班群体:热衷于通过分享内容或表达“个人情绪”在陌生人面前建立特殊的人设,并借此提升自我价值扩大自己的交际圈;
  • 学生群体:追求个性选择,更倾向找到有共同话题的陌生人对象并建立长期的关系,乐于展现自我;
  • 文艺群体:拥有自己独特的爱好且拥有特别的个人追求,追求文艺圈子内的交流,希望通过分享结交更多好友;
  • 沟通弱势群体:对现有长期保持线上对社交模式表现无力且无效,渴望有更加有效且安全的社交方式出现,解决目前单调乏味的沟通方式;

2.4、技术方案

前端:

  • flutter + android + 环信SDK + redux + shared_preferences + connectivity + iconfont + webview + sqflite

后端:

  • Spring Boot + SpringMVC + Mybatis + MybatisPlus + Dubbo
  • Elasticsearch geo 实现地理位置查询
  • MongoDB 实现海量数据的存储
  • Redis 数据的缓存
  • Spark + MLlib 实现智能推荐
  • 第三方服务 环信即时通讯
  • 第三方服务 阿里云 OSS 、 短信服务
  • 第三方服务 虹软开放平台

2.5、技术架构

image-20231004075135525

2.6、技术解决方案

  • 使用Elasticsearch geo实现附近的人的解决方案
  • 使用Spark + Mllib实现智能推荐的解决方案
  • 使用MongoDB进行海量数据的存储的解决方案
  • 使用采用分布式文件系统存储小视频数据的解决方案
  • 使用虹软开放平台进行人脸识别的解决方案
  • 使用阿里云进行短信验证码发送的解决方案

2.7、技术亮点

  • 采用Elasticsearch geo实现地理位置查询
  • 采用RocketMQ作为消息服务中间件
  • 采用MongoDB进行海量数据的存储
  • 采用Spark + Mllib实现智能推荐
  • 采用环信服务实现即时通讯
  • 采用分布式文件系统存储小视频数据
  • 采用Apache Dobbo作为微服务架构技术
  • 采用SpringBoot + Mybatis实现系统主架构
  • 采用Redis集群实现缓存的高可用

2.8、开发方式

探花交友项目采用前后端分离的方式开发,就是前端由前端团队负责开发,后端负责接口的开发,这种开发方式有2点好处:

  • 扬长避短,每个团队做自己擅长的事情
  • 前后端并行开发,需要事先约定好接口地址以及各种参数、响应数据结构等

什么是接口?接口就是一个http的请求地址,在定义接口的时候主要就是去定义:请求路径,请求方式,请求参数,响应结果数据等内容。

对于接口的定义我们采用YApi进行管理,YApi是一个开源的接口定义、管理、提供mock数据的管理平台。

地址:https://mock-java.itheima.net/

用户名:tanhua@itcast.cn

密码:123456

image-20201017100705286

接口定义:

image-20201017100848313

mock数据,YApi提供了mock功能,就是模拟服务端返回测试数据:

image-20201017101228582

image-20201017101252482

还可以运行http请求(需要在Chrome中安装支持跨域扩展 https://juejin.im/post/6844904057707085832):

image-20201017103028622

image-20201017103045864

2.9、基础环境

探花交友项目的开发统一使用提供的Centos7环境,该环境中部署安装了项目所需要的各种服务,如:MySQL、MongoDB、Redis、RocketMQ等。

  • 虚拟机的root用户密码为:root123
  • 虚拟机的mysql密码为:root
  • 默认参数:CPU:2核,内存:4G,硬盘:60G
  • IP地址建议设置为192.168.31.81,否则有些服务将不可用,比如:Redis、RocketMQ等。

拷贝虚拟机需要修改这个网段

登录注册功能

3、注册登录

业务说明:

用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。

流程:

image-20220827193004852

image-20220827193000149

3.1、单点登录系统

为什么要使用单点登录系统?

以前实现的登录和注册是在同一个tomcat内部完成,我们现在的系统架构是每一个系统都是由一个团队进行维护,每个系统都是单独部署运行一个单独的tomcat,所以,不能将用户的登录信息保存到session中(多个tomcat的session是不能共享的),所以我们需要一个单独的系统来维护用户的登录信息。

image-20201016221250343

image-20220827193829828

SSO在整个系统架构中的应用

image-20201117235203408

由上图可以看出:

  • 客户端需要通过SSO系统才能获取到token;
  • 客户端在请求服务系统时,服务系统需要通过SSO系统进行对token进行校验;
  • SSO系统在整个系统架构中处于核心位置;

3.2、搭建工程

3.2.1、my-tanhua

itcast-tanhua是父工程,集中定义了依赖的版本以及所需要的依赖信息。

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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
<?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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>

<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua</artifactId>
<version>1.0-SNAPSHOT</version>

<!-- 集中定义依赖版本号 -->
<properties>
<mysql.version>5.1.47</mysql.version>
<jackson.version>2.9.9</jackson.version>
<druid.version>1.0.9</druid.version>
<servlet-api.version>2.5</servlet-api.version>
<jsp-api.version>2.0</jsp-api.version>
<joda-time.version>2.9.9</joda-time.version>
<commons-lang3.version>3.7</commons-lang3.version>
<commons-io.version>1.3.2</commons-io.version>
<mybatis.version>3.2.8</mybatis.version>
<mybatis.mybatis-plus>3.1.1</mybatis.mybatis-plus>
<lombok.version>1.18.4</lombok.version>
</properties>

<!--通用依赖-->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<!-- mybatis-plus插件依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis.mybatis-plus}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis.mybatis-plus}</version>
</dependency>
<!-- MySql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>

<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
<version>3.9.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>${lombok.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<!--RocketMQ相关-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
<version>2.0.3</version>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
<version>4.6.0</version>
</dependency>
<!-- Jackson Json处理工具包 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.11</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${joda-time.version}</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.32.Final</version>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
<version>3.4.13</version>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
<version>0.1</version>
</dependency>
<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>0.2.0</version>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.4</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.8.3</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.3</version>
</dependency>
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

</project>

3.2.2、my-tanhua-sso

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
<?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-sso</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>

<!--RocketMQ相关-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</dependency>
<!--简化代码的工具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
</dependencies>


</project>

3.2.3、网易模拟器

探花交友项目的前端采用Android APP的形式,所以我们需要使用模拟器或真机进行测试。

对于模拟器这里推荐使用网易模拟器,其兼容性好、功能完善而且还简洁,缺点是它不支持虚拟机中安装。或者雷电模拟器

下载:https://mumu.163.com/

image-20201016214917500

3.3、数据库表

数据库使用的mysql:

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
CREATE TABLE `tb_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`mobile` varchar(11) DEFAULT NULL COMMENT '手机号',
`password` varchar(32) DEFAULT NULL COMMENT '密码,需要加密',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `mobile` (`mobile`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户表';

CREATE TABLE `tb_user_info` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`nick_name` varchar(50) DEFAULT NULL COMMENT '昵称',
`logo` varchar(100) DEFAULT NULL COMMENT '用户头像',
`tags` varchar(50) DEFAULT NULL COMMENT '用户标签:多个用逗号分隔',
`sex` int(1) DEFAULT '3' COMMENT '性别,1-男,2-女,3-未知',
`age` int(11) DEFAULT NULL COMMENT '用户年龄',
`edu` varchar(20) DEFAULT NULL COMMENT '学历',
`city` varchar(20) DEFAULT NULL COMMENT '居住城市',
`birthday` varchar(20) DEFAULT NULL COMMENT '生日',
`cover_pic` varchar(50) DEFAULT NULL COMMENT '封面图片',
`industry` varchar(20) DEFAULT NULL COMMENT '行业',
`income` varchar(20) DEFAULT NULL COMMENT '收入',
`marriage` varchar(20) DEFAULT NULL COMMENT '婚姻状态',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用户信息表';

3.4、编写配置

application.properties:

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
spring.application.name = itcast-tanhua-sso

server.port = 18080

spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.81:3306/mytanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root


# 枚举包扫描
mybatis-plus.type-enums-package=com.tanhua.sso.enums
# 表名前缀
mybatis-plus.global-config.db-config.table-prefix=tb_
# id策略为自增长
mybatis-plus.global-config.db-config.id-type=auto


# Redis相关配置
spring.redis.jedis.pool.max-wait = 5000ms
spring.redis.jedis.pool.max-Idle = 100
spring.redis.jedis.pool.min-Idle = 10
spring.redis.timeout = 10s
spring.redis.cluster.nodes = 192.168.31.81:6379,192.168.31.81:6380,192.168.31.81:6381
spring.redis.cluster.max-redirects=5

# RocketMQ相关配置
rocketmq.name-server=192.168.31.81:9876
rocketmq.producer.group=tanhua

#itcast_tanhua
#盐 值
jwt.secret=76bd425b6f29f7fcc2e0bfc286043df1

#虹软相关配置
arcsoft.appid=*****
arcsoft.sdkKey=****
arcsoft.libPath=F:\\code\\WIN64

3.5、编写基础代码

3.5.1、Lombok

lombok 提供了简单的注解的形式来帮助我们简化消除一些必须有但显得很臃肿的 java 代码,尤其是针对pojo。

官网:https://projectlombok.org/

3.5.1.1、配置安装

导入依赖:

1
2
3
4
5
6
<!--简化代码的工具包-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

安装IDEA插件: image-20231004075049367

如果不安装插件,程序可以正常执行,但是看不到生成的一些代码,如:get、set方法。

3.5.1.2、常用注解
  • @Data:注解在类上;提供类所有属性的 getting 和 setting 方法,此外还提供了equals、canEqual、hashCode、toString 方法
  • @Setter:注解在属性上;为属性提供 setting 方法
  • @Getter:注解在属性上;为属性提供 getting 方法
  • @Slf4j:注解在类上;为类提供一个 属性名为log 的 slf4j日志对象
  • @NoArgsConstructor:注解在类上;为类提供一个无参的构造方法
  • @AllArgsConstructor:注解在类上;为类提供一个全参的构造方法
  • @Builder:使用Builder模式构建对象

测试一:使用@Data注解

image-20201116113610810

是不是很神奇?!

测试二:使用@Slf4j注解

image-20201116113645107

测试:

image-20201116113812602

测试三:@AllArgsConstructor、@NoArgsConstructor注解的使用

image-20201116114013519

测试四:@Builder

image-20201116114233416

测试结果:

image-20201116114254202

3.5.2、SexEnum

用户的性别用枚举进行表示。

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
package com.tanhua.sso.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;
}
}

3.5.3、User、UserInfo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.sso.pojo;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;
import lombok.Data;

import java.util.Date;

@Data
public abstract class BasePojo {

@TableField(fill = FieldFill.INSERT) //MP自动填充
private Date created;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updated;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tanhua.sso.pojo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User extends BasePojo {

private Long id;
private String mobile; //手机号

@JsonIgnore
private String password; //密码,json序列化时忽略

}

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
package com.tanhua.sso.pojo;

import com.tanhua.sso.enums.SexEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo extends BasePojo {

private Long id;
private Long userId; //用户id
private String nickName; //昵称
private String logo; //用户头像
private String tags; //用户标签:多个用逗号分隔
private SexEnum sex; //性别
private Integer age; //年龄
private String edu; //学历
private String city; //城市
private String birthday; //生日
private String coverPic; // 封面图片
private String industry; //行业
private String income; //收入
private String marriage; //婚姻状态

}

3.5.4、MyMetaObjectHandler

对自动填充字段的处理:

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
package com.tanhua.sso.handler;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class MyMetaObjectHandler implements MetaObjectHandler {

@Override
public void insertFill(MetaObject metaObject) {
Object created = getFieldValByName("created", metaObject);
if (null == created) {
//字段为空,可以进行填充
setFieldValByName("created", new Date(), metaObject);
}

Object updated = getFieldValByName("updated", metaObject);
if (null == updated) {
//字段为空,可以进行填充
setFieldValByName("updated", new Date(), metaObject);
}
}

@Override
public void updateFill(MetaObject metaObject) {
//更新数据时,直接更新字段
setFieldValByName("updated", new Date(), metaObject);
}
}

3.5.5、UserMapper

1
2
3
4
5
6
7
8
package com.tanhua.sso.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.sso.pojo.User;

public interface UserMapper extends BaseMapper<User> {

}
1
2
3
4
5
6
7
8
package com.tanhua.sso.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.sso.pojo.UserInfo;

public interface UserInfoMapper extends BaseMapper<UserInfo> {

}

3.5.6、MyApplication

SpringBoot的启动类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.tanhua.sso;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.tanhua.sso.mapper") //设置mapper接口的扫描包
@SpringBootApplication
public class MyApplication {

public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}
}

3.6、短信验证码

发送短信验证码的流程:

image-20201116090449776

流程说明:

  • 用户向SSO系统发送请求,在请求中传递手机号;
  • SSO系统接收到请求后,生成随机验证码以及短信内容,请求阿里云短信服务;
  • 阿里云短信服务接收到请求后,会进行一系列的验证,比如账号余额、短信模板是否正确等,最后向运营商发起请求;
  • 运营商接收到请求后,向该手机号下发短信,用户即可收到短信;

3.6.1、阿里云短信服务

3.6.1.1、申请签名与模板

https://dysms.console.aliyun.com/dysms.htm?spm=5176.12818093.0.ddysms.2a4316d0ql6PyD

image-20201016193819097

说明:申请签名时,个人用户只能申请一个并且签名的名称必须为“ABC商城”,否则审核不通过。

申请模板:

image-20201016193927924

审核时间需要1~2小时,请耐心等待~

3.6.1.2、设置用户权限

在阿里云中,需要在RAM服务中创建用户以及权限,才能通过api进行访问接口。

image-20201016194331790

创建用户:

image-20201016194409024

image-20201016195011698

创建完成后要保存AccessKey Secret和AccessKey ID,AccessKey Secret只显示这一次,后面将不再显示。

添加权限:

image-20201016194636739

3.6.1.3、示例代码

文档:https://help.aliyun.com/document_detail/101414.html?spm=a2c4g.11186623.6.625.18705ffa8u4lwj:

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
package com.tanhua.sso.service;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
/*
pom.xml
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>4.5.3</version>
</dependency>
*/
public class SendSms {
public static void main(String[] args) {
DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou",
"LTAI4G7d2Q9CHc741gighjTF", "uKOOGdIKvmoGhHlej8cJY8H3nlU6Fj");
IAcsClient client = new DefaultAcsClient(profile);

CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain("dysmsapi.aliyuncs.com");
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", "cn-hangzhou");
request.putQueryParameter("PhoneNumbers", "158****7944"); //目标手机号
request.putQueryParameter("SignName", "ABC商城"); //签名名称
request.putQueryParameter("TemplateCode", "SMS_204756062"); //短信模板code
request.putQueryParameter("TemplateParam", "{\"code\":\"123456\"}");//模板中变量替换
try {
CommonResponse response = client.getCommonResponse(request);

//{"Message":"OK","RequestId":"EC2D4C9A-0EAC-4213-BE45-CE6176E1DF23","BizId":"110903802851113360^0","Code":"OK"}
System.out.println(response.getData());
} catch (ServerException e) {
e.printStackTrace();
} catch (ClientException e) {
e.printStackTrace();
}
}
}

3.6.1.4、实现发送短信方法

配置文件:aliyun.properties

1
2
3
4
5
6
aliyun.sms.regionId = cn-hangzhou
aliyun.sms.accessKeyId = LTAI4G7d2Q9CHc741gighjTF
aliyun.sms.accessKeySecret = uKOOGdIKvmoGhHlej8cJY8H3nlU6Fj
aliyun.sms.domain= dysmsapi.aliyuncs.com
aliyun.sms.signName= ABC商城
aliyun.sms.templateCode= SMS_204756062

需要注意中文编码问题:

image-20201016203847636

读取配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tanhua.sso.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun.sms")
@Data
public class AliyunSMSConfig {

private String regionId;
private String accessKeyId;
private String accessKeySecret;
private String domain;
private String signName;
private String templateCode;

}

代码实现:

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
//SmsService.java
package com.nbchen.tanhua.sso.service;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.nbchen.tanhua.sso.config.AliyunSMSConfig;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
@Slf4j
public class SmsService {
@Autowired
private AliyunSMSConfig aliyunSMSConfig;
/**
* 发送短信验证码
*
* @param mobile
* @return
*/
public String sendSms(String mobile) {
DefaultProfile profile = DefaultProfile.getProfile(this.aliyunSMSConfig.getRegionId(),
this.aliyunSMSConfig.getAccessKeyId(), this.aliyunSMSConfig.getAccessKeySecret());
IAcsClient client = new DefaultAcsClient(profile);

String code = RandomUtils.nextInt(100000, 999999) + "";

CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain(this.aliyunSMSConfig.getDomain());
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", this.aliyunSMSConfig.getRegionId());
request.putQueryParameter("PhoneNumbers", mobile); //目标手机号
request.putQueryParameter("SignName", this.aliyunSMSConfig.getSignName()); //签名名称
request.putQueryParameter("TemplateCode", this.aliyunSMSConfig.getTemplateCode()); //短信模板code
request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");//模板中变量替换
try {
CommonResponse response = client.getCommonResponse(request);
String data = response.getData();
if (StringUtils.contains(data, "\"Message\":\"OK\"")) {
return code;
}
log.info("发送短信验证码失败~ data = " + data);
} catch (Exception e) {
log.error("发送短信验证码失败~ mobile = " + mobile, e);
}
return null;
}
}

3.6.2、SSO短信接口服务

3.6.2.1、mock接口

地址:https://mock-java.itheima.net/project/35/interface/api/581

image-20231004075609747

3.6.2.2、编写接口服务

编写ErrorResult,ErrorResult对象是与前端约定好的结构,如果发生错误需要返回该对象,如果未发生错误响应200即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.tanhua.sso.vo;

import lombok.Builder;
import lombok.Data;

@Data
@Builder
public class ErrorResult {

private String errCode;
private String errMessage;
}

SmsController:

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
package com.tanhua.sso.controller;

import com.tanhua.sso.service.SmsService;
import com.tanhua.sso.vo.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("user")
@Slf4j
public class SmsController {

@Autowired
private SmsService smsService;

/**
* 发送短信验证码接口
*
* @param param
* @return
*/
@PostMapping("login")
public ResponseEntity<ErrorResult> sendCheckCode(@RequestBody Map<String, String> param) {
ErrorResult errorResult = null;
String phone = param.get("phone");
try {
errorResult = this.smsService.sendCheckCode(phone);
if (null == errorResult) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
log.error("发送短信验证码失败~ phone = " + phone, e);
errorResult = ErrorResult.builder().errCode("000002").errMessage("短信验证码发送失败!").build();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}

}

SmsService:

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
package com.tanhua.sso.service;

import com.aliyuncs.CommonRequest;
import com.aliyuncs.CommonResponse;
import com.aliyuncs.DefaultAcsClient;
import com.aliyuncs.IAcsClient;
import com.aliyuncs.exceptions.ClientException;
import com.aliyuncs.exceptions.ServerException;
import com.aliyuncs.http.MethodType;
import com.aliyuncs.profile.DefaultProfile;
import com.tanhua.sso.config.AliyunSMSConfig;
import com.tanhua.sso.vo.ErrorResult;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.time.Duration;

@Service
@Slf4j
public class SmsService {

@Autowired
private AliyunSMSConfig aliyunSMSConfig;

@Autowired
private RedisTemplate<String,String> redisTemplate;

/**
* 发送短信验证码
*
* @param mobile
* @return
*/
public String sendSms(String mobile) {
DefaultProfile profile = DefaultProfile.getProfile(this.aliyunSMSConfig.getRegionId(),
this.aliyunSMSConfig.getAccessKeyId(), this.aliyunSMSConfig.getAccessKeySecret());
IAcsClient client = new DefaultAcsClient(profile);

String code = RandomUtils.nextInt(100000, 999999) + "";

CommonRequest request = new CommonRequest();
request.setSysMethod(MethodType.POST);
request.setSysDomain(this.aliyunSMSConfig.getDomain());
request.setSysVersion("2017-05-25");
request.setSysAction("SendSms");
request.putQueryParameter("RegionId", this.aliyunSMSConfig.getRegionId());
request.putQueryParameter("PhoneNumbers", mobile); //目标手机号
request.putQueryParameter("SignName", this.aliyunSMSConfig.getSignName()); //签名名称
request.putQueryParameter("TemplateCode", this.aliyunSMSConfig.getTemplateCode()); //短信模板code
request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");//模板中变量替换
try {
CommonResponse response = client.getCommonResponse(request);
String data = response.getData();
if (StringUtils.contains(data, "\"Message\":\"OK\"")) {
return code;
}
log.info("发送短信验证码失败~ data = " + data);
} catch (Exception e) {
log.error("发送短信验证码失败~ mobile = " + mobile, e);
}
return null;
}

/**
* 发送短信验证码
* 实现:发送完成短信验证码后,需要将验证码保存到redis中
* @param phone
* @return
*/
public ErrorResult sendCheckCode(String phone) {

String redisKey = "CHECK_CODE_" + phone;

//先判断该手机号发送的验证码是否还未失效
if(this.redisTemplate.hasKey(redisKey)){
String msg = "上一次发送的验证码还未失效!";
return ErrorResult.builder().errCode("000001").errMessage(msg).build();
}

String code = this.sendSms(phone);
if(StringUtils.isEmpty(code)){
String msg = "发送短信验证码失败!";
return ErrorResult.builder().errCode("000000").errMessage(msg).build();
}

//短信发送成功,将验证码保存到redis中,有效期为5分钟
this.redisTemplate.opsForValue().set(redisKey, code, Duration.ofMinutes(5));

return null;
}
}

3.7、JWT

3.7.1、简介

JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全。

在身份验证过程中, 当用户使用其凭据成功登录时, 将返回 JSON Web token, 并且必须在本地保存 (通常在本地存储中)。

每当用户要访问受保护的路由或资源 (端点) 时, 用户代理(user agent)必须连同请求一起发送 JWT, 通常在授权标头中使用Bearer schema。后端服务器接收到带有 JWT 的请求时, 首先要做的是验证token。

3.7.2、格式

  • JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C

  • A由JWT头部信息header经过base64加密得到

    • #默认的头信息
      {
        "alg": "HS256",
        "typ": "JWT"
      }
      
      #官网测试:https://jwt.io/
      #base64加密后的字符串为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
      
      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

      - B是payload,存放有效信息的地方,这些信息包含三个部分:

      - 标准中注册的声明 (建议但不强制使用)

      - iss: jwt签发者
      - sub: jwt所面向的用户
      - aud: 接收jwt的一方
      - exp: jwt的过期时间,这个过期时间必须要大于签发时间
      - nbf: 定义在什么时间之前,该jwt都是不可用的.
      - iat: jwt的签发时间
      - jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

      - 公共的声明

      - 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.

      - 私有的声明

      - 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。

      - ~~~json
      #存放的数据:
      {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
      }

      #base64后的字符串为:
      eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ
  • C由A和B通过加密算法得到,用作对token进行校验,看是否有效

    • 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。

    • #secret为:itcast
      #得到的加密字符串为:DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc
      
      #整体的token为:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.DwMTjJktoFFdClHqjJMRgYzICo6FJOUc3Jmev9EScBc
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16


      #### 3.7.3、流程

      ![image-20231004075346300](./06_交友探花项目/image-20231004075346300.png)

      #### 3.7.4、示例

      导入依赖:

      ~~~xml
      <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt</artifactId>
      <version>0.9.1</version>
      </dependency>

编写测试用例:

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
package com.tanhua.sso.service;

import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwsHeader;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.junit.Test;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class TestJWT {

String secret = "itcast";

@Test
public void testCreateToken(){

Map<String, Object> header = new HashMap<String, Object>();
header.put(JwsHeader.TYPE, JwsHeader.JWT_TYPE);
header.put(JwsHeader.ALGORITHM, "HS256");

Map<String, Object> claims = new HashMap<String, Object>();
claims.put("mobile", "1333333333");
claims.put("id", "2");

// 生成token
String jwt = Jwts.builder()
.setHeader(header) //header,可省略
.setClaims(claims) //payload,存放数据的位置,不能放置敏感数据,如:密码等
.signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐
.setExpiration(new Date(System.currentTimeMillis() + 3000)) //设置过期时间,3秒后过期
.compact();

System.out.println(jwt);

}

@Test
public void testDecodeToken(){
String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtb2JpbGUiOiIxMzMzMzMzMzMzIiwiaWQiOiIyIiwiZXhwIjoxNjA1NTEzMDA2fQ.1eG3LpudD4XBycUG39UQDaKVBQHgaup-E1OLWo_m8m8";
try {
// 通过token解析数据
Map<String, Object> body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();
System.out.println(body); //{mobile=1333333333, id=2, exp=1605513392}
} catch (ExpiredJwtException e) {
System.out.println("token已经过期!");
} catch (Exception e) {
System.out.println("token不合法!");
}
}

}

3.8、用户登录

用户接收到验证码后,进行输入验证码,点击登录,前端系统将手机号以及验证码提交到SSO进行校验。

image-20201118231431191

3.7.1、mock接口

接口地址:https://mock-java.itheima.net/project/164/interface/api/12593

image-20231004075548382

3.7.2、UserController

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
package com.tanhua.sso.controller;

import com.tanhua.sso.service.UserService;
import com.tanhua.sso.vo.ErrorResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("user")
public class UserController {

@Autowired
private UserService userService;

/**
* 用户登录
*
* @param param
* @return
*/
@PostMapping("loginVerification")
public ResponseEntity<Object> login(@RequestBody Map<String,String> param){
try {
String phone = param.get("phone");
String code = param.get("verificationCode");

String data = this.userService.login(phone, code);
if(StringUtils.isNotEmpty(data)){
//登录成功
Map<String, Object> result = new HashMap<>(2);
String[] ss = StringUtils.split(data, '|');

result.put("token", ss[0]);
result.put("isNew", Boolean.valueOf(ss[1]));
return ResponseEntity.ok(result);
}
} catch (Exception e) {
e.printStackTrace();
}
ErrorResult errorResult = ErrorResult.builder().errCode("000002").errMessage("登录失败!").build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}

}

3.7.3、UserService

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
package com.tanhua.sso.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.tanhua.sso.mapper.UserMapper;
import com.tanhua.sso.pojo.User;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.messaging.MessagingException;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class UserService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Autowired
private UserMapper userMapper;

@Value("${jwt.secret}")
private String secret;

@Autowired
private RocketMQTemplate rocketMQTemplate;


/**
* 用户登录
*
* @param phone 手机号
* @param code 验证码
* @return
*/
public String login(String phone, String code) {
String redisKey = "CHECK_CODE_" + phone;
boolean isNew = false;

//校验验证码
String redisData = this.redisTemplate.opsForValue().get(redisKey);
if (!StringUtils.equals(code, redisData)) {
return null; //验证码错误
}

//验证码在校验完成后,需要废弃
this.redisTemplate.delete(redisKey);

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("mobile", phone);

User user = this.userMapper.selectOne(queryWrapper);

if (null == user) {
//需要注册该用户
user = new User();
user.setMobile(phone);
user.setPassword(DigestUtils.md5Hex("123456"));

//注册新用户
this.userMapper.insert(user);
isNew = true;
}

//生成token
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("id", user.getId());

// 生成token
String token = Jwts.builder()
.setClaims(claims) //payload,存放数据的位置,不能放置敏感数据,如:密码等
.signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐
.setExpiration(new DateTime().plusHours(12).toDate()) //设置过期时间,12小时后过期
.compact();

try {
//发送用户登录成功的消息
Map<String,Object> msg = new HashMap<>();
msg.put("id", user.getId());
msg.put("date", System.currentTimeMillis());

this.rocketMQTemplate.convertAndSend("tanhua-sso-login", msg);
} catch (MessagingException e) {
log.error("发送消息失败!", e);
}

return token + "|" + isNew;
}
}

3.7.4、测试

image-20231004075555672

完善个人信息

课程介绍

  • 完善个人信息
  • 阿里云OSS服务应用
  • 人脸识别
  • MongoDB快速入门
  • SpringBoot整合MongoDB

1、完善个人信息

用户在首次登录时需要完善个人信息,包括性别、昵称、生日、城市、头像等。

其中,头像数据需要做图片上传,这里采用阿里云的OSS服务作为我们的图片服务器,并且对头像要做人脸识别,非人脸照片不得上传。

1.1、图片上传

1.1.1、图片存储解决方案

实现图片上传服务,需要有存储的支持,那么我们的解决方案将以下几种:

  1. 直接将图片保存到服务的硬盘
    1. 优点:开发便捷,成本低
    2. 缺点:扩容困难
  2. 使用分布式文件系统进行存储
    1. 优点:容易实现扩容
    2. 缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS)
  3. 使用nfs做存储
    1. 优点:开发较为便捷
    2. 缺点:需要有一定的运维知识进行部署和维护
  4. 使用第三方的存储服务
    1. 优点:开发简单,拥有强大功能,免维护
    2. 缺点:付费

在本套课程中选用阿里云的OSS服务进行图片存储。

1.1.2、阿里云OSS存储

流程:

image-20201122161002420

1.1.2.1、什么是OSS服务?

地址:https://www.aliyun.com/product/oss

image-20201017104006281

1.1.2.2、购买服务

使用第三方服务最大的缺点就是需要付费,下面,我们看下如何购买开通服务。

image-20201017112543995

image-20201017112639162

购买下行流量包: (不购买也可以使用,按照流量付费)

image-20201017112802078

说明:OSS的上行流量是免费的,但是下行流量是需要购买的。

1.1.2.3、创建Bucket

使用OSS,首先需要创建Bucket,Bucket翻译成中文是水桶的意思,把存储的图片资源看做是水,想要盛水必须得有桶,就是这个意思了。

进入控制台,https://oss.console.aliyun.com/overview

image-20201017113211471

选择Bucket后,即可看到对应的信息,如:url、消耗流量等 :

image-20201017113430118

文件管理:

image-20201017113536835

查看文件:

image-20201017113557135

1.1.2.4、创建用户

创建用户的方式与短信接口中的方式一样,需要设置oss权限。

image-20201017114136997

1.1.3、导入依赖

1
2
3
4
5
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>2.8.3</version>
</dependency>

1.1.4、OSS配置

aliyun.properties:

1
2
3
4
5
aliyun.endpoint = http://oss-cn-zhangjiakou.aliyuncs.com
aliyun.accessKeyId = ***********
aliyun.accessKeySecret = ***************
aliyun.bucketName= tanhua-dev
aliyun.urlPrefix=http://tanhua-dev.oss-cn-zhangjiakou.aliyuncs.com/

AliyunConfig:

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
package com.tanhua.sso.config;

import com.aliyun.oss.OSSClient;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:aliyun.properties")
@ConfigurationProperties(prefix = "aliyun")
@Data
public class AliyunConfig {

private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
private String urlPrefix;

@Bean
public OSSClient oSSClient() {
return new OSSClient(endpoint, accessKeyId, accessKeySecret);
}

}

1.1.5、PicUploadService

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
package com.tanhua.sso.service;

import com.aliyun.oss.OSSClient;
import com.tanhua.sso.config.AliyunConfig;
import com.tanhua.sso.vo.PicUploadResult;
import org.apache.commons.lang3.RandomUtils;
import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.ByteArrayInputStream;

@Service
public class PicUploadService {

// 允许上传的格式
private static final String[] IMAGE_TYPE = new String[]{".bmp", ".jpg",
".jpeg", ".gif", ".png"};

@Autowired
private OSSClient ossClient;

@Autowired
private AliyunConfig aliyunConfig;

public PicUploadResult upload(MultipartFile uploadFile) {

PicUploadResult fileUploadResult = new PicUploadResult();

//图片做校验,对后缀名
boolean isLegal = false;

for (String type : IMAGE_TYPE) {
if (StringUtils.endsWithIgnoreCase(uploadFile.getOriginalFilename(),
type)) {
isLegal = true;
break;
}
}

if (!isLegal) {
fileUploadResult.setStatus("error");
return fileUploadResult;
}

// 文件新路径
String fileName = uploadFile.getOriginalFilename();
String filePath = getFilePath(fileName);

// 上传到阿里云
try {
// 目录结构:images/2018/12/29/xxxx.jpg
ossClient.putObject(aliyunConfig.getBucketName(), filePath, new
ByteArrayInputStream(uploadFile.getBytes()));
} catch (Exception e) {
e.printStackTrace();
//上传失败
fileUploadResult.setStatus("error");
return fileUploadResult;
}

// 上传成功
fileUploadResult.setStatus("done");
fileUploadResult.setName(this.aliyunConfig.getUrlPrefix() + filePath);
fileUploadResult.setUid(String.valueOf(System.currentTimeMillis()));

return fileUploadResult;
}

private String getFilePath(String sourceFileName) {
DateTime dateTime = new DateTime();
return "images/" + dateTime.toString("yyyy")
+ "/" + dateTime.toString("MM") + "/"
+ dateTime.toString("dd") + "/" + System.currentTimeMillis() +
RandomUtils.nextInt(100, 9999) + "." +
StringUtils.substringAfterLast(sourceFileName, ".");
}

}

所需其他的代码:

PicUploadResult:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tanhua.sso.vo;

import lombok.Data;

@Data
public class PicUploadResult {

// 文件唯一标识
private String uid;
// 文件名
private String name;
// 状态有:uploading done error removed
private String status;
// 服务端响应内容,如:'{"status": "success"}'
private String response;

}

1.1.6、PicUploadController

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
package com.tanhua.sso.controller;

import com.tanhua.sso.service.PicUploadService;
import com.tanhua.sso.vo.PicUploadResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.multipart.MultipartFile;

@RequestMapping("pic/upload")
@Controller
public class PicUploadController {

@Autowired
private PicUploadService picUploadService;

@PostMapping
@ResponseBody
public PicUploadResult upload(@RequestParam("file") MultipartFile multipartFile) {
return this.picUploadService.upload(multipartFile);
}
}

1.1.7、测试

image-20231004075407874

image-20231004075411161

image-20231004075414711

1.2、人脸识别

人脸识别技术采用虹软开放平台实现(免费使用)。官网:https://www.arcsoft.com.cn/ image-20231004075423119

1.2.1、使用说明

使用虹软平台需要先注册开发者账号:https://ai.arcsoft.com.cn/ucenter/user/userlogin

image-20231004075428172

注册完成后进行登录,然后进行创建应用:

image-20201123103558410

创建完成后,需要进行实名认证,否则相关的SDK是不能使用的。

image-20231004075437042

实名认证后即可下载对应平台的SDk,我们需要下载windows以及linux平台。

添加SDK(Linux与Windows平台):

image-20201123103650588

image-20201123103734310

image-20201123103924768

下载SDK,打开解压包,可以看到有提供相应的jar包以及示例代码:

image-20231004075448512

需要特别说明的是:每个账号的SDK包不通用,所以自己要下载自己的SDK包。

1.2.2、安装jar到本地仓库

进入到libs目录,需要将arcsoft-sdk-face-3.0.0.0.jar安装到本地仓库:

1
mvn install:install-file -DgroupId=com.arcsoft.face -DartifactId=arcsoft-sdk-face -Dversion=3.0.0.0 -Dpackaging=jar -Dfile=arcsoft-sdk-face-3.0.0.0.jar

安装成功后,即可通过maven坐标引用了:

1
2
3
4
5
6
7
8
<dependency>
<groupId>com.arcsoft.face</groupId>
<artifactId>arcsoft-sdk-face</artifactId>
<version>3.0.0.0</version>
<!--<scope>system</scope>-->
<!--如果没有安装到本地仓库,可以将jar包拷贝到工程的lib下面下,直接引用-->
<!--<systemPath>${project.basedir}/lib/arcsoft-sdk-face-3.0.0.0.jar</systemPath>-->
</dependency>

1.2.3、开始使用

说明:虹软的SDK是免费使用的,但是首次使用时需要联网激活,激活后可离线使用。使用周期为1年,1年后需要联网再次激活。

个人免费激活SDK总数量为100。

配置:application.properties

1
2
3
4
#虹软相关配置(在虹软应用中找到对应的参数)
arcsoft.appid=******************
arcsoft.sdkKey=*****************
arcsoft.libPath=F:\\code\\WIN64

FaceEngineService:

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
package com.tanhua.sso.service;

import com.arcsoft.face.EngineConfiguration;
import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.FunctionConfiguration;
import com.arcsoft.face.enums.DetectMode;
import com.arcsoft.face.enums.DetectOrient;
import com.arcsoft.face.enums.ErrorInfo;
import com.arcsoft.face.enums.ImageFormat;
import com.arcsoft.face.toolkit.ImageFactory;
import com.arcsoft.face.toolkit.ImageInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import java.io.File;
import java.util.ArrayList;
import java.util.List;

@Service
public class FaceEngineService {

private static final Logger LOGGER = LoggerFactory.getLogger(FaceEngineService.class);

@Value("${arcsoft.appid}")
private String appid;

@Value("${arcsoft.sdkKey}")
private String sdkKey;

@Value("${arcsoft.libPath}")
private String libPath;

private FaceEngine faceEngine;

@PostConstruct
public void init() {
// 激活并且初始化引擎
FaceEngine faceEngine = new FaceEngine(libPath);
int activeCode = faceEngine.activeOnline(appid, sdkKey);
if (activeCode != ErrorInfo.MOK.getValue() && activeCode != ErrorInfo.MERR_ASF_ALREADY_ACTIVATED.getValue()) {
LOGGER.error("引擎激活失败");
throw new RuntimeException("引擎激活失败");
}

//引擎配置
EngineConfiguration engineConfiguration = new EngineConfiguration();
//IMAGE检测模式,用于处理单张的图像数据
engineConfiguration.setDetectMode(DetectMode.ASF_DETECT_MODE_IMAGE);
//人脸检测角度,全角度
engineConfiguration.setDetectFaceOrientPriority(DetectOrient.ASF_OP_ALL_OUT);

//功能配置
FunctionConfiguration functionConfiguration = new FunctionConfiguration();
functionConfiguration.setSupportAge(true);
functionConfiguration.setSupportFace3dAngle(true);
functionConfiguration.setSupportFaceDetect(true);
functionConfiguration.setSupportFaceRecognition(true);
functionConfiguration.setSupportGender(true);
functionConfiguration.setSupportLiveness(true);
functionConfiguration.setSupportIRLiveness(true);
engineConfiguration.setFunctionConfiguration(functionConfiguration);

//初始化引擎
int initCode = faceEngine.init(engineConfiguration);

if (initCode != ErrorInfo.MOK.getValue()) {
LOGGER.error("初始化引擎出错!");
throw new RuntimeException("初始化引擎出错!");
}

this.faceEngine = faceEngine;
}

/**
* 检测图片是否为人像
*
* @param imageInfo 图像对象
* @return true:人像,false:非人像
*/
public boolean checkIsPortrait(ImageInfo imageInfo) {
// 定义人脸列表
List<FaceInfo> faceInfoList = new ArrayList<FaceInfo>();
faceEngine.detectFaces(imageInfo.getImageData(), imageInfo.getWidth(), imageInfo.getHeight(), ImageFormat.CP_PAF_BGR24, faceInfoList);
return !faceInfoList.isEmpty();
}

public boolean checkIsPortrait(byte[] imageData) {
return this.checkIsPortrait(ImageFactory.getRGBData(imageData));
}

public boolean checkIsPortrait(File file) {
return this.checkIsPortrait(ImageFactory.getRGBData(file));
}

}

1
2
3
4
5
#问题:
Caused by: java.lang.UnsatisfiedLinkError: D:\gongju\renlian\haha\libs\WIN64\libarcsoft_face.dll: Can't find dependent libraries

解决:
安装资料中的:vcredist_x64.exe,即可解决。

1.2.4、测试

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
package com.tanhua.sso.service;

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.SpringJUnit4ClassRunner;

import java.io.File;

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class TestFaceEngineService {

@Autowired
private FaceEngineService faceEngineService;

@Test
public void testCheckIsPortrait(){
File file = new File("F:\\1.jpg");
boolean checkIsPortrait = this.faceEngineService.checkIsPortrait(file);
System.out.println(checkIsPortrait); // true|false
}
}

1.3、实现完善个人信息

完善个人信息的功能实现,分为2个接口完成,分别是:完善个人资料信息、头像上传。

mock接口:

1.3.1、UserInfoMapper

1
2
3
4
5
6
7
8
package com.tanhua.sso.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.sso.pojo.UserInfo;

public interface UserInfoMapper extends BaseMapper<UserInfo> {
}

1.3.2、UserInfoService

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
package com.tanhua.sso.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tanhua.sso.enums.SexEnum;
import com.tanhua.sso.mapper.UserInfoMapper;
import com.tanhua.sso.pojo.User;
import com.tanhua.sso.pojo.UserInfo;
import com.tanhua.sso.vo.PicUploadResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.Map;

@Service
public class UserInfoService {

@Autowired
private UserService userService;

@Autowired
private UserInfoMapper userInfoMapper;

@Autowired
private FaceEngineService faceEngineService;

@Autowired
private PicUploadService picUploadService;

public Boolean saveUserInfo(Map<String, String> param, String token) {
//校验token
User user = this.userService.queryUserByToken(token);
if (null == user) {
return false;
}

UserInfo userInfo = new UserInfo();
userInfo.setUserId(user.getId());
userInfo.setSex(StringUtils.equalsIgnoreCase(param.get("gender"), "man") ? SexEnum.MAN : SexEnum.WOMAN);
userInfo.setNickName(param.get("nickname"));
userInfo.setBirthday(param.get("birthday"));
userInfo.setCity(param.get("city"));
return this.userInfoMapper.insert(userInfo) == 1;
}

public Boolean saveUserLogo(MultipartFile file, String token) {
//校验token
User user = this.userService.queryUserByToken(token);
if (null == user) {
return false;
}

try {
//校验图片是否是人像,如果不是人像就返回false
boolean b = this.faceEngineService.checkIsPortrait(file.getBytes());
if (!b) {
return false;
}
} catch (IOException e) {
e.printStackTrace();
}

//图片上传到阿里云OSS
PicUploadResult result = this.picUploadService.upload(file);
if (StringUtils.isEmpty(result.getName())) {
//上传失败
return false;
}

//把头像保存到用户信息表中
UserInfo userInfo = new UserInfo();
userInfo.setLogo(result.getName());

QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", user.getId());

return this.userInfoMapper.update(userInfo, queryWrapper) == 1;
}
}

1.3.3、UserInfoController

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
package com.tanhua.sso.controller;

import com.tanhua.sso.service.UserInfoService;
import com.tanhua.sso.service.UserService;
import com.tanhua.sso.vo.ErrorResult;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("user")
public class UserInfoController {

@Autowired
private UserInfoService userInfoService;

/**
* 完善个人信息-基本信息
*
* @param param
* @return
*/
@PostMapping("loginReginfo")
public ResponseEntity<Object> saveUserInfo(@RequestBody Map<String, String> param,
@RequestHeader("Authorization") String token) {
try {
Boolean bool = this.userInfoService.saveUserInfo(param, token);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
ErrorResult errorResult = ErrorResult.builder().errCode("000001").errMessage("保存用户信息失败!").build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}

/**
* 完善个人信息-用户头像
*
* @return
*/
@PostMapping("loginReginfo/head")
public ResponseEntity<Object> saveUserLogo(@RequestParam("headPhoto") MultipartFile file,
@RequestHeader("Authorization") String token) {
try {
Boolean bool = this.userInfoService.saveUserLogo(file, token);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
ErrorResult errorResult = ErrorResult.builder().errCode("000001").errMessage("保存用户logo失败!").build();
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResult);
}

}

1.4.4、测试

image-20231004075508664

image-20231004075513133

image-20231004075517401

图片上传超过1MB出错的解决方案:

1
2
3
4
#在application.properties文件中,填入下面的配置
#设置最大的文件上传大小
spring.servlet.multipart.max-request-size=30MB
spring.servlet.multipart.max-file-size=30MB

2、校验token

在整个系统架构中,只有SSO保存了JWT中的秘钥,所以只能通过SSO系统提供的接口服务进行对token的校验,所以在SSO系统中,需要对外开放接口,通过token进行查询用户信息,如果返回null说明用户状态已过期或者是非法的token,否则返回User对象数据。

2.1、UserController

1
2
3
4
5
6
7
8
9
10
/**
* 校验token,根据token查询用户数据
*
* @param token
* @return
*/
@GetMapping("{token}")
public User queryUserByToken(@PathVariable("token") String token) {
return this.userService.queryUserByToken(token);
}

2.2、UserService

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
public User queryUserByToken(String token) {
try {
// 通过token解析数据
Map<String, Object> body = Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(token)
.getBody();

User user = new User();
user.setId(Long.valueOf(body.get("id").toString()));

//需要返回user对象中的mobile,需要查询数据库获取到mobile数据
//如果每次都查询数据库,必然会导致性能问题,需要对用户的手机号进行缓存操作
//数据缓存时,需要设置过期时间,过期时间要与token的时间一致
//如果用户修改了手机号,需要同步修改redis中的数据

String redisKey = "TANHUA_USER_MOBILE_" + user.getId();
if(this.redisTemplate.hasKey(redisKey)){
String mobile = this.redisTemplate.opsForValue().get(redisKey);
user.setMobile(mobile);
}else {
//查询数据库
User u = this.userMapper.selectById(user.getId());
user.setMobile(u.getMobile());

//将手机号写入到redis中
//在jwt中的过期时间的单位为:秒
long timeout = Long.valueOf(body.get("exp").toString()) * 1000 - System.currentTimeMillis();
this.redisTemplate.opsForValue().set(redisKey, u.getMobile(), timeout, TimeUnit.MILLISECONDS);
}

return user;
} catch (ExpiredJwtException e) {
log.info("token已经过期! token = " + token);
} catch (Exception e) {
log.error("token不合法! token = "+ token, e);
}
return null;
}

2.3、测试

image-20201123155434121

数据已经存储到redis中:

image-20201123162229283

3、MongoDB快速入门

3.1、MongoDB简介

MongoDB是一个基于分布式文件存储的数据库。由C++语言编写。旨在为WEB应用提供可扩展的高性能数据存储解决方案。

MongoDB是一个介于关系数据库和非关系数据库之间的产品,是非关系数据库当中功能最丰富,最像关系数据库的,它支持的数据结构非常松散,是类似json的bson格式,因此可以存储比较复杂的数据类型。

MongoDB最大的特点是它支持的查询语言非常强大,其语法有点类似于面向对象的查询语言,几乎可以实现类似关系数据库单表查询的绝大部分功能,而且还支持对数据建立索引。

官网:https://www.mongodb.com

3.2、通过docker安装MongoDB

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
#拉取镜像
docker pull mongo:4.0.3

#创建容器
docker create --name mongodb-server -p 27018:27017 -v mongodb-data:/data/db mongo:4.0.3 --auth

#启动容器
docker start mongodb-server

#进入容器
docker exec -it mongodb-server /bin/bash


#进入admin数据库
mongo
use admin

#添加管理员,其拥有管理用户和角色的权限
db.createUser({ user: 'root', pwd: 'root', roles: [ { role: "root", db: "admin" } ] })

#测试,发现是没有权限操作的
> show dbs
2020-10-20T09:09:15.543+0000 E QUERY [js] Error: listDatabases failed:{
"ok" : 0,
"errmsg" : "command listDatabases requires authentication",
"code" : 13,
"codeName" : "Unauthorized"
} :

#进行认证
mongo -u "root" -p "root" --authenticationDatabase "admin"

#通过admin添加普通用户
use admin
db.createUser({ user: 'tanhua', pwd: 'l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV', roles: [ { role: "readWrite", db: "tanhua" } ] });

#通过tanhua用户登录进行测试
mongo -u "tanhua" -p "l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV" --authenticationDatabase "admin"

#测试
root@5d848955ff7e:/# mongo -u "tanhua" -p "tanhua123" --authenticationDatabase "admin"
MongoDB shell version v4.0.3
connecting to: mongodb://127.0.0.1:27017
Implicit session: session { "id" : UUID("6c368269-30f0-4b29-a224-05a38b5847e2") }
MongoDB server version: 4.0.3
> use tanhua
switched to db tanhua
> db.user.insert({id:1,username:'zhangsan',age:20})
WriteResult({ "nInserted" : 1 })
> db.user.find()
{ "_id" : ObjectId("5f8eb2726e0de0aa9517afd3"), "id" : 1, "username" : "zhangsan", "age" : 20 }

3.3、MongoDB基本操作

3.3.1、基本概念

为了更好的理解,下面与SQL中的概念进行对比:

SQL术语/概念 MongoDB术语/概念 解释/说明
database database 数据库
table collection 数据库表/集合
row document 数据记录行/文档
column field 数据字段/域
index index 索引
table joins 表连接,MongoDB不支持
primary key primary key 主键,MongoDB自动将_id字段设置为主键

img

3.3.2、数据库以及表的操作

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
#查看所有的数据库
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB

#通过use关键字切换数据库
> use admin
switched to db admin

#创建数据库
#说明:在MongoDB中,数据库是自动创建的,通过use切换到新数据库中,进行插入数据即可自动创建数据库
> use testdb
switched to db testdb
> show dbs #并没有创建数据库
admin 0.000GB
config 0.000GB
local 0.000GB
> db.user.insert({id:1,name:'zhangsan'}) #插入数据
WriteResult({ "nInserted" : 1 })
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB
testdb 0.000GB #数据库自动创建

#查看表
> show tables
user
> show collections
user
>

#删除集合(表)
> db.user.drop()
true #如果成功删除选定集合,则 drop() 方法返回 true,否则返回 false。

#删除数据库
> use testdb #先切换到要删除的数据库中
switched to db testdb
> db.dropDatabase() #删除数据库
{ "dropped" : "testdb", "ok" : 1 }
> show dbs
admin 0.000GB
config 0.000GB
local 0.000GB

3.3.3、新增数据

在MongoDB中,存储的文档结构是一种类似于json的结构,称之为bson(全称为:Binary JSON)。

1
2
3
4
5
6
7
8
9
10
11
#插入数据

#语法:db.COLLECTION_NAME.insert(document)
> db.user.insert({id:1,username:'zhangsan',age:20})
WriteResult({ "nInserted" : 1 })
> db.user.save({id:2,username:'lisi',age:25})
WriteResult({ "nInserted" : 1 })
> db.user.find() #查询数据
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "id" : 1, "username" : "zhangsan", "age" : 20 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }

3.3.4、更新数据

update() 方法用于更新已存在的文档。语法格式如下:

1
2
3
4
5
6
7
8
9
db.collection.update(
<query>,
<update>,
[
upsert: <boolean>,
multi: <boolean>,
writeConcern: <document>
]
)

参数说明:

  • query : update的查询条件,类似sql update查询内where后面的。
  • update : update的对象和一些更新的操作符(如$,$inc…)等,也可以理解为sql update查询内set后面的
  • upsert : 可选,这个参数的意思是,如果不存在update的记录,是否插入objNew,true为插入,默认是false,不插入。
  • multi : 可选,mongodb 默认是false,只更新找到的第一条记录,如果这个参数为true,就把按条件查出来多条记录全部更新。
  • writeConcern :可选,抛出异常的级别。
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
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "id" : 1, "username" : "zhangsan", "age" : 20 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }

> db.user.update({id:1},{$set:{age:22}}) #更新数据

WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })

> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "id" : 1, "username" : "zhangsan", "age" : 22 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }

#注意:如果这样写,会删除掉其他的字段
> db.user.update({id:1},{age:25})
WriteResult({ "nMatched" : 1, "nUpserted" : 0, "nModified" : 1 })
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25 }

#更新不存在的字段,会新增字段
> db.user.update({id:2},{$set:{sex:1}}) #更新数据
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25, "sex" : 1 }

#更新不存在的数据,默认不会新增数据
> db.user.update({id:3},{$set:{sex:1}})
WriteResult({ "nMatched" : 0, "nUpserted" : 0, "nModified" : 0 })
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25, "sex" : 1 }

#如果设置第一个参数为true,就是新增数据
> db.user.update({id:3},{$set:{sex:1}},true)
WriteResult({
"nMatched" : 0,
"nUpserted" : 1,
"nModified" : 0,
"_id" : ObjectId("5c08cb281418d073246bc642")
})
> db.user.find()
{ "_id" : ObjectId("5c08c0024b318926e0c1f6dc"), "age" : 25 }
{ "_id" : ObjectId("5c08c0134b318926e0c1f6dd"), "id" : 2, "username" : "lisi", "age" : 25, "sex" : 1 }
{ "_id" : ObjectId("5c08cb281418d073246bc642"), "id" : 3, "sex" : 1 }

3.3.5、删除数据

通过remove()方法进行删除数据,语法如下:

1
2
3
4
5
6
7
db.collection.remove(
<query>,
{
justOne: <boolean>,
writeConcern: <document>
}
)

参数说明:

  • query :(可选)删除的文档的条件。
  • justOne : (可选)如果设为 true 或 1,则只删除一个文档,如果不设置该参数,或使用默认值 false,则删除所有匹配条件的文档。
  • writeConcern :(可选)抛出异常的级别。

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
> db.user.remove({age:25})
WriteResult({ "nRemoved" : 2 }) #删除了2条数据

#插入4条测试数据
db.user.insert({id:1,username:'zhangsan',age:20})
db.user.insert({id:2,username:'lisi',age:21})
db.user.insert({id:3,username:'wangwu',age:22})
db.user.insert({id:4,username:'zhaoliu',age:22})

> db.user.remove({age:22},true)
WriteResult({ "nRemoved" : 1 }) #删除了1条数据

#删除所有数据
> db.user.remove({})

#说明:为了简化操作,官方推荐使用deleteOne()与deleteMany()进行删除数据操作。
db.user.deleteOne({id:1})
db.user.deleteMany({}) #删除所有数据

3.3.6、查询数据

MongoDB 查询数据的语法格式如下:

1
db.user.find([query],[fields])
  • query :可选,使用查询操作符指定查询条件
  • fields :可选,使用投影操作符指定返回的键。查询时返回文档中所有键值, 只需省略该参数即可(默认省略)。

如果你需要以易读的方式来读取数据,可以使用 pretty() 方法,语法格式如下:

1
>db.col.find().pretty()

pretty() 方法以格式化的方式来显示所有文档。

条件查询:

操作 格式 范例 RDBMS中的类似语句
等于 {<key>:<value>} db.col.find({"by":"黑马程序员"}).pretty() where by = '黑马程序员'
小于 {<key>:{$lt:<value>}} db.col.find({"likes":{$lt:50}}).pretty() where likes < 50
小于或等于 {<key>:{$lte:<value>}} db.col.find({"likes":{$lte:50}}).pretty() where likes <= 50
大于 {<key>:{$gt:<value>}} db.col.find({"likes":{$gt:50}}).pretty() where likes > 50
大于或等于 {<key>:{$gte:<value>}} db.col.find({"likes":{$gte:50}}).pretty() where likes >= 50
不等于 {<key>:{$ne:<value>}} db.col.find({"likes":{$ne:50}}).pretty() where likes != 50

实例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#插入测试数据
db.user.insert({id:1,username:'zhangsan',age:20})
db.user.insert({id:2,username:'lisi',age:21})
db.user.insert({id:3,username:'wangwu',age:22})
db.user.insert({id:4,username:'zhaoliu',age:22})

db.user.find() #查询全部数据
db.user.find({},{id:1,username:1}) #只查询id与username字段
db.user.find().count() #查询数据条数
db.user.find({id:1}) #查询id为1的数据
db.user.find({age:{$lte:21}}) #查询小于等于21的数据
db.user.find({age:{$lte:21}, id:{$gte:2}}) #and查询,age小于等于21并且id大于等于2
db.user.find({$or:[{id:1},{id:2}]}) #查询id=1 or id=2

#分页查询:Skip()跳过几条,limit()查询条数
db.user.find().limit(2).skip(1) #跳过1条数据,查询2条数据

db.user.find().sort({id:-1}) #按照age倒序排序,-1为倒序,1为正序

3.4、索引

索引通常能够极大的提高查询的效率,如果没有索引,MongoDB在读取数据时必须扫描集合中的每个文件并选取那些符合查询条件的记录。

这种扫描全集合的查询效率是非常低的,特别在处理大量的数据时,查询可以要花费几十秒甚至几分钟,这对网站的性能是非常致命的。

索引是特殊的数据结构,索引存储在一个易于遍历读取的数据集合中,索引是对数据库表中一列或多列的值进行排序的一种结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#查看索引
> db.user.getIndexes()
[
{
"v" : 2,
"key" : {
"_id" : 1
},
"name" : "_id_",
"ns" : "testdb.user"
}
]

#说明:1表示升序创建索引,-1表示降序创建索引。
1
2
3
4
5
6
7
8
#创建索引
> db.user.createIndex({'age':1})
{
"createdCollectionAutomatically" : false,
"numIndexesBefore" : 1,
"numIndexesAfter" : 2,
"ok" : 1
}
1
2
3
4
#删除索引
db.user.dropIndex("age_1")
#或者,删除除了_id之外的索引
db.user.dropIndexes()
1
2
#创建联合索引
db.user.createIndex({'age':1, 'id':-1})
1
2
#查看索引大小,单位:字节
db.user.totalIndexSize()

3.5、执行计划

MongoDB 查询分析可以确保我们建议的索引是否有效,是查询语句性能分析的重要工具。

1
2
#插入1000条数据
for(var i=1;i<1000;i++)db.user.insert({id:100+i,username:'name_'+i,age:10+i})
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
#查看执行计划
> db.user.find({age:{$gt:100},id:{$lt:200}}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "testdb.user",
"indexFilterSet" : false,
"parsedQuery" : {
"$and" : [
{
"id" : {
"$lt" : 200
}
},
{
"age" : {
"$gt" : 100
}
}
]
},
"winningPlan" : { #最佳执行计划
"stage" : "FETCH", #查询方式,常见的有COLLSCAN/全表扫描、IXSCAN/索引扫描、FETCH/根据索引去检索文档、SHARD_MERGE/合并分片结果、IDHACK/针对_id进行查询
"inputStage" : {
"stage" : "IXSCAN",
"keyPattern" : {
"age" : 1,
"id" : -1
},
"indexName" : "age_1_id_-1",
"isMultiKey" : false,
"multiKeyPaths" : {
"age" : [ ],
"id" : [ ]
},
"isUnique" : false,
"isSparse" : false,
"isPartial" : false,
"indexVersion" : 2,
"direction" : "forward",
"indexBounds" : {
"age" : [
"(100.0, inf.0]"
],
"id" : [
"(200.0, -inf.0]"
]
}
}
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "c493d5ff750a",
"port" : 27017,
"version" : "4.0.3",
"gitVersion" : "7ea530946fa7880364d88c8d8b6026bbc9ffa48c"
},
"ok" : 1
}

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
#测试没有使用索引
> db.user.find({username:'zhangsan'}).explain()
{
"queryPlanner" : {
"plannerVersion" : 1,
"namespace" : "testdb.user",
"indexFilterSet" : false,
"parsedQuery" : {
"username" : {
"$eq" : "zhangsan"
}
},
"winningPlan" : {
"stage" : "COLLSCAN", #全表扫描
"filter" : {
"username" : {
"$eq" : "zhangsan"
}
},
"direction" : "forward"
},
"rejectedPlans" : [ ]
},
"serverInfo" : {
"host" : "c493d5ff750a",
"port" : 27017,
"version" : "4.0.3",
"gitVersion" : "7ea530946fa7880364d88c8d8b6026bbc9ffa48c"
},
"ok" : 1
}

3.6、UI客户端工具

Robo 3T是MongoDB的客户端工具,我们可以使用它来操作MongoDB。

image-20231004075108508

查看数据: image-20231004075119549

或使用Navicat Premium 15:

image-20231004075123723

4、SpringBoot整合MongoDB

spring-data对MongoDB做了支持,使用spring-data-mongodb可以简化MongoDB的操作。

地址:https://spring.io/projects/spring-data-mongodb

4.1、导入依赖

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
<?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">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
</parent>

<groupId>cn.itcast.mongodb</groupId>
<artifactId>itcast-mongodb</artifactId>
<version>1.0-SNAPSHOT</version>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
<version>1.18.4</version>
</dependency>
</dependencies>

<build>
<plugins>
<!-- java编译插件 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.2</version>
<configuration>
<source>1.8</source>
<target>1.8</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
</plugins>
</build>

</project>

4.2、编写application.properties配置文件

1
2
3
4
5
6
7
8
9
10
11
12
13
# Spring boot application
spring.application.name = itcast-mongodb

#无认证信息的配置
#spring.data.mongodb.uri=mongodb://192.168.31.81:27017/tanhua

#springboot 配置
spring.data.mongodb.username=tanhua
spring.data.mongodb.password=l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.database=tanhua
spring.data.mongodb.port=27018
spring.data.mongodb.host=192.168.31.81

4.3、编写实体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package cn.itcast.mongodb.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.bson.types.ObjectId;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Person {

private ObjectId id;
private String name;
private int age;
private Address address;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
package cn.itcast.mongodb.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Address {
private String street;
private String city;
private String zip;
}

4.4、编写dao

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
package cn.itcast.mongodb.dao;

import cn.itcast.mongodb.pojo.Person;
import com.mongodb.client.result.DeleteResult;
import com.mongodb.client.result.UpdateResult;
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.data.mongodb.core.query.Update;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
public class PersonDao {

@Autowired
private MongoTemplate mongoTemplate;

public void savePerson(Person person) {
this.mongoTemplate.save(person);
}

public List<Person> queryPersonListByName(String name) {
Query query = Query.query(Criteria.where("name").is(name));
return this.mongoTemplate.find(query, Person.class);
}

public List<Person> queryPersonPageList(Integer page, Integer rows) {
Query query = new Query().limit(rows).skip((page - 1) * rows);
return this.mongoTemplate.find(query, Person.class);
}

public UpdateResult update(Person person) {
Query query = Query.query(Criteria.where("id").is(person.getId()));
Update update = Update.update("age", person.getAge());
return this.mongoTemplate.updateFirst(query, update, Person.class);
}

public DeleteResult deleteById(String id) {
Query query = Query.query(Criteria.where("id").is(id));
return this.mongoTemplate.remove(query, Person.class);
}
}

4.5、编写启动类

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast.mongodb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MongoApplication {

public static void main(String[] args) {
SpringApplication.run(MongoApplication.class, args);
}
}

4.6、编写单元测试

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
package cn.itcast.mongodb;

import cn.itcast.mongodb.dao.PersonDao;
import cn.itcast.mongodb.pojo.Address;
import cn.itcast.mongodb.pojo.Person;
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.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestPersonDao {

@Autowired
private PersonDao personDao;

@Test
public void testSave() {
Person person = new Person(ObjectId.get(), "张三", 20,
new Address("人民路", "上海市", "666666"));
this.personDao.savePerson(person);
}

@Test
public void testQuery() {
List<Person> personList = this.personDao.queryPersonListByName("张三");
for (Person person : personList) {
System.out.println(person);
}
}

@Test
public void testQuery2() {
List<Person> personList = this.personDao.queryPersonPageList(2, 2);
for (Person person : personList) {
System.out.println(person);
}
}

@Test
public void testUpdate() {
Person person = new Person();
person.setId(new ObjectId("5c0956ce235e192520086736"));
person.setAge(30);
this.personDao.update(person);
}

@Test
public void testDelete() {
this.personDao.deleteById("5c09ca05235e192d8887a389");
}

}

今日佳人功能

课程说明

  • 首页功能说明
  • 系统架构说明
  • 实现今日佳人功能
  • 实现推荐用户的列表
  • 接口增加缓存功能
  • 整合前端联调测试

1、首页

在用户登录成功后,就会进入首页,首页中有今日佳人、推荐好友、探花、搜附近等功能。

image-20231004075534878

2、系统架构

在开发完SSO系统中的登录功能后,接下来就需要实现其他的功能,在整体架构中,完成与APP对接的服务工程叫my-tanhua-server,真正的核心业务逻辑使用dubbo完成,其工程名叫:my-tanhua-dubbo,它们的架构示意图如下:

image-20201130102009217

说明:

  • 客户端APP发起请求到Nginx,在Nginx中对请求做出判断,将请求转发至sso系统或server系统。
  • sso系统中,将对接第三方平台以及完成数据的缓存、消息发送、用户的注册登录功能。
  • server系统为APP提供了接口服务的支撑
    • 用户请求中携带的token需要到sso系统中进行校验
    • 通过rpc调用dubbo中提供的服务,在dubbo服务中与MongoDB对接,完成数据的CRUD操作
    • 将一些数据缓存到Redis,从而提升数据查询性能
    • 用户数据的查询将基于MySQL数据库进行查询

2.1、nginx服务

2.1.1、部署安装

安装包在资料中:nginx-1.17.3.zip

安装在任意目录,通过命令:start nginx.exe 启动:

image-20231004075642858

重启加载配置文件命令:nginx.exe -s reload

image-20231004075646861

2.1.2、配置

修改conf目录下的nginx.conf文件:

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
#user  nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

location /user/ { #请求路径中凡是以/user/开头的请求,转发到sso系统
client_max_body_size 300m; #设置最大的请求体大小,解决大文件上传不了的问题
proxy_connect_timeout 300s; #代理连接超时时间
proxy_send_timeout 300s; #代理发送数据的超时时间
proxy_read_timeout 300s; #代理读取数据的超时时间
proxy_pass http://127.0.0.1:18080; #转发请求
}

location / { #上面未匹配到的在这里处理
client_max_body_size 300m;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_pass http://127.0.0.1:18081; #转发请求到server系统
}
}

}

2.1.3、测试

image-20201130111356817

2.2、搭建server工程

2.2.1、导入依赖

pom.xml文件:

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
<?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-server</artifactId>

<dependencies>
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-collections4</artifactId>
<version>4.4</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
</dependency>
</dependencies>
</project>

2.2.2、application.properties

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
spring.application.name = itcast-tanhua-server
server.port = 18081

#数据库连接信息
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.81:3306/mytanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

# 枚举包扫描
mybatis-plus.type-enums-package=com.tanhua.server.enums
# 表名前缀
mybatis-plus.global-config.db-config.table-prefix=tb_
# id策略为自增长
mybatis-plus.global-config.db-config.id-type=auto

#dubbo注册中心配置
dubbo.application.name = itcast-tanhua-server
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000
dubbo.consumer.timeout = 60000

#sso系统服务地址
tanhua.sso.url=http://127.0.0.1
#默认今日佳人推荐用户
tanhua.sso.default.user=2

2.2.3、ServerApplication

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.tanhua.server;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@MapperScan("com.tanhua.server.mapper") //设置mapper接口的扫描包
@SpringBootApplication
public class ServerApplication {

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}

2.3、搭建dubbo工程

my-tanhua-dubbo是dubbo工程的父工程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?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-dubbo</artifactId>
<packaging>pom</packaging>
<modules>
<module>my-tanhua-dubbo-interface</module>
<module>my-tanhua-dubbo-service</module>
</modules>


</project>

2.3.1、创建my-tanhua-dubbo-interface工程

该工程中定义了dubbo服务中的interface与实体对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<?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-dubbo</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>my-tanhua-dubbo-interface</artifactId>
<dependencies>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
</dependencies>
</project>

2.3.2、创建my-tanhua-dubbo-service工程

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
<?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-dubbo</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>my-tanhua-dubbo-service</artifactId>

<dependencies>
<!--引入interface依赖-->
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>

<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>

<!--MongoDB相关依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<dependency>
<groupId>org.mongodb</groupId>
<artifactId>mongodb-driver-sync</artifactId>
</dependency>

<!--其他工具包依赖-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>

<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>

</dependencies>


</project>

application.properties:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Spring boot application
spring.application.name = itcast-tanhua-dubbo-service

# dubbo 扫描包配置
dubbo.scan.basePackages = com.tanhua.dubbo.server
dubbo.application.name = dubbo-provider-tanhua

#dubbo 对外暴露的端口信息
dubbo.protocol.name = dubbo
dubbo.protocol.port = 20880

#dubbo注册中心的配置
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000

#springboot MongoDB配置
spring.data.mongodb.username=tanhua
spring.data.mongodb.password=l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.database=tanhua
spring.data.mongodb.port=27017
spring.data.mongodb.host=192.168.31.81

编写启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
package com.tanhua.dubbo.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class DubboApplication {

public static void main(String[] args) {
SpringApplication.run(DubboApplication.class, args);
}
}

2.4、工程结构

最终搭建完成的效果如下:

image-20201130120238951

各工程之间的关系如下:

image-20210202165713450

3、今日佳人

今日佳人,会推荐缘分值最大的用户,进行展现出来。缘分值的计算是由用户的行为进行打分,如:点击、点赞、评论、学历、婚姻状态等信息组合而成的。

实现:我们先不考虑推荐的逻辑,假设现在已经有推荐的结果,我们只需要从结果中查询到缘分值最高的用户就可以了。至于推荐的逻辑以及实现,我们将后面的课程中讲解。

流程:

image-20201130150720621

3.1、表结构

1
2
3
4
5
6
7
#表结构,表名:recommend_user
{
"userId":1001, #推荐的用户id
"toUserId":1002, #用户id
"score":90, #推荐得分
"date":"2019/1/1" #日期
}

已经提供的测试数据(4855条数据):

image-20201130151126971

3.2、编写dubbo服务

3.2.1、编写接口

在my-tanhua-dubbo-interface工程中定义接口:

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
package com.tanhua.dubbo.server.api;

import com.tanhua.dubbo.server.vo.PageInfo;
import com.tanhua.dubbo.server.pojo.RecommendUser;

public interface RecommendUserApi {

/**
* 查询一位得分最高的推荐用户
*
* @param userId
* @return
*/
RecommendUser queryWithMaxScore(Long userId);

/**
* 按照得分倒序
*
* @param userId
* @param pageNum
* @param pageSize
* @return
*/
PageInfo<RecommendUser> queryPageInfo(Long userId, Integer pageNum, Integer pageSize);
}

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
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.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "recommend_user")
public class RecommendUser implements java.io.Serializable{

private static final long serialVersionUID = -4296017160071130962L;

@Id
private ObjectId id; //主键id
@Indexed
private Long userId; //推荐的用户id
private Long toUserId; //用户id
@Indexed
private Double score; //推荐得分
private String date; //日期
}

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
package com.tanhua.dubbo.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Collections;
import java.util.List;

@Data
@AllArgsConstructor
public class PageInfo<T> implements java.io.Serializable {

private static final long serialVersionUID = -2105385689859184204L;

/**
* 总条数
*/
private Integer total = 0;

/**
* 当前页
*/
private Integer pageNum = 0;

/**
* 一页显示的大小
*/
private Integer pageSize = 0;

/**
* 数据列表
*/
private List<T> records = Collections.emptyList();

}

3.2.2、编写实现

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
package com.tanhua.dubbo.server.api;

import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.RecommendUser;
import com.tanhua.dubbo.server.vo.PageInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
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.List;

@Service(version = "1.0.0") //申明这是一个dubbo服务
public class RecommendUserApiImpl implements RecommendUserApi {

@Autowired
private MongoTemplate mongoTemplate;

@Override
public RecommendUser queryWithMaxScore(Long userId) {
//查询得分最高的用户,按照得分倒序排序
Query query = Query.query(Criteria.where("toUserId").is(userId))
.with(Sort.by(Sort.Order.desc("score"))).limit(1);
return this.mongoTemplate.findOne(query, RecommendUser.class);
}

@Override
public PageInfo<RecommendUser> queryPageInfo(Long userId, Integer pageNum, Integer pageSize) {

//分页并且排序参数
PageRequest pageRequest = PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Order.desc("score")));

//查询参数
Query query = Query.query(Criteria.where("toUserId").is(userId)).with(pageRequest);

List<RecommendUser> recommendUserList = this.mongoTemplate.find(query, RecommendUser.class);

//暂时不提供数据总数
return new PageInfo<>(0, pageNum, pageSize, recommendUserList);
}
}

3.2.3、测试

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
package com.tanhua.dubbo.server.api;

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 TestRecommendUserApi {

@Autowired
private RecommendUserApi recommendUserApi;

@Test
public void testQueryWithMaxScore(){
System.out.println(this.recommendUserApi.queryWithMaxScore(1L));
System.out.println(this.recommendUserApi.queryWithMaxScore(8L));
System.out.println(this.recommendUserApi.queryWithMaxScore(26L));
}

@Test
public void testQueryPageInfo(){
System.out.println(this.recommendUserApi.queryPageInfo(1L,1,5));
System.out.println(this.recommendUserApi.queryPageInfo(1L,2,5));
System.out.println(this.recommendUserApi.queryPageInfo(1L,3,5));
}

}

3.3、实现今日佳人服务

3.3.1、mock服务

地址:https://mock-java.itheima.net/project/35/interface/api/617

image-20201130152808705

image-20231004075625695

3.3.2、基础代码

3.3.2.1、SexEnum
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
package com.tanhua.server.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;
}
}

3.3.2.2、BasePojo
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tanhua.server.pojo;

import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.TableField;

import java.util.Date;


public abstract class BasePojo {

@TableField(fill = FieldFill.INSERT)
private Date created;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updated;
}

3.3.2.3、User
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.tanhua.server.pojo;

import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class User extends BasePojo {

private Long id;
private String mobile; //手机号

@JsonIgnore
private String password; //密码,json序列化时忽略

}

3.3.2.4、UserInfo
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
package com.tanhua.server.pojo;

import com.tanhua.server.enums.SexEnum;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserInfo extends BasePojo {

private Long id;
private Long userId; //用户id
private String nickName; //昵称
private String logo; //用户头像
private String tags; //用户标签:多个用逗号分隔
private SexEnum sex; //性别
private Integer age; //年龄
private String edu; //学历
private String city; //城市
private String birthday; //生日
private String coverPic; // 封面图片
private String industry; //行业
private String income; //收入
private String marriage; //婚姻状态

}

3.3.3、实现功能

实现描述:

  • 需要根据前端定义的结构定义java对象
  • 根据sso系统提供的接口查询当前登录用户的信息
  • 根据dubbo系统提供的服务进行查询今日佳人数据
3.3.3.1、TodayBest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* 今日佳人
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class TodayBest {

private Long id;
private String avatar;
private String nickname;
private String gender; //性别 man woman
private Integer age;
private String[] tags;
private Long fateValue; //缘分值

}

3.3.3.2、TodayBestController
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
package com.tanhua.server.controller;

import com.tanhua.server.service.TodayBestService;
import com.tanhua.server.vo.TodayBest;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("tanhua")
@Slf4j
public class TodayBestController {

@Autowired
private TodayBestService todayBestService;

/**
* 查询今日佳人
*
* @param token
* @return
*/
@GetMapping("todayBest")
public ResponseEntity<TodayBest> queryTodayBest(@RequestHeader("Authorization") String token) {
try {
TodayBest todayBest = this.todayBestService.queryTodayBest(token);
if (null != todayBest) {
return ResponseEntity.ok(todayBest);
}
} catch (Exception e) {
log.error("查询今日佳人出错~ token = " + token, e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);
}
}

3.3.3.3、TodayBestService
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
package com.tanhua.server.service;

import com.tanhua.server.pojo.User;
import com.tanhua.server.pojo.UserInfo;
import com.tanhua.server.vo.TodayBest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

@Service
public class TodayBestService {

@Autowired
private UserService userService;

@Autowired
private RecommendUserService recommendUserService;

@Autowired
private UserInfoService userInfoService;

@Value("${tanhua.sso.default.user}")
private Long defaultUser;

public TodayBest queryTodayBest(String token) {
//校验token是否有效,通过SSO的接口进行校验
User user = this.userService.queryUserByToken(token);
if (null == user) {
//token非法或已经过期
return null;
}

//查询推荐用户(今日佳人)
TodayBest todayBest = this.recommendUserService.queryTodayBest(user.getId());
if(null == todayBest){
//给出默认的推荐用户
todayBest = new TodayBest();
todayBest.setId(defaultUser);
todayBest.setFateValue(80L); //固定值
}

//补全个人信息
UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(todayBest.getId());
if(null == userInfo){
return null;
}
todayBest.setAvatar(userInfo.getLogo());
todayBest.setNickname(userInfo.getNickName());
todayBest.setTags(StringUtils.split(userInfo.getTags(), ','));
todayBest.setGender(userInfo.getSex().getValue() == 1 ? "man" : "woman");
todayBest.setAge(userInfo.getAge());

return todayBest;
}
}

3.3.3.4、UserService
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
package com.tanhua.server.service;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tanhua.server.pojo.User;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
@Slf4j
public class UserService {

@Autowired
private RestTemplate restTemplate;

@Value("${tanhua.sso.url}")
private String ssoUrl;

private static final ObjectMapper MAPPER = new ObjectMapper();

/**
* 通过sso的rest接口查询
*
* @param token
* @return
*/
public User queryUserByToken(String token) {
String url = ssoUrl + "/user/" + token;
try {
String data = this.restTemplate.getForObject(url, String.class);
if (StringUtils.isEmpty(data)) {
return null;
}
return MAPPER.readValue(data, User.class);
} catch (Exception e) {
log.error("校验token出错,token = " + token, e);
}
return null;
}
}

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
package com.tanhua.server.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.client.RestTemplate;

import java.nio.charset.Charset;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate(ClientHttpRequestFactory factory) {
RestTemplate restTemplate = new RestTemplate(factory);
// 支持中文编码
restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(Charset.forName("UTF-8")));
return restTemplate;
}

@Bean
public ClientHttpRequestFactory simpleClientHttpRequestFactory() {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
factory.setReadTimeout(5000);//单位为ms
factory.setConnectTimeout(5000);//单位为ms
return factory;
}
}
3.3.3.5、RecommendUserService
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
package com.tanhua.server.service;

import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.dubbo.server.api.RecommendUserApi;
import com.tanhua.dubbo.server.pojo.RecommendUser;
import com.tanhua.server.vo.TodayBest;
import org.springframework.stereotype.Service;

/**
* 负责与dubbo服务进行交互
*/
@Service
public class RecommendUserService {

@Reference(version = "1.0.0")
private RecommendUserApi recommendUserApi;

public TodayBest queryTodayBest(Long userId) {
RecommendUser recommendUser = this.recommendUserApi.queryWithMaxScore(userId);
if(null == recommendUser){
return null;
}

TodayBest todayBest = new TodayBest();
todayBest.setId(recommendUser.getUserId());

//缘分值
double score = Math.floor(recommendUser.getScore());//取整,98.2 -> 98
todayBest.setFateValue(Double.valueOf(score).longValue());

return todayBest;
}
}

3.3.3.6、UserInfoService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tanhua.server.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tanhua.server.mapper.UserInfoMapper;
import com.tanhua.server.pojo.UserInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class UserInfoService {

@Autowired
private UserInfoMapper userInfoMapper;

public UserInfo queryUserInfoByUserId(Long userId) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
return this.userInfoMapper.selectOne(queryWrapper);
}
}

3.3.3.7、UserInfoMapper
1
2
3
4
5
6
7
8
9
package com.tanhua.server.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.UserInfo;

public interface UserInfoMapper extends BaseMapper<UserInfo> {

}

3.3.4、测试

单元测试,测试dubbo服务:

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
package com.tanhua.server;

import com.tanhua.server.service.RecommendUserService;
import com.tanhua.server.vo.TodayBest;
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.SpringJUnit4ClassRunner;

@SpringBootTest
@RunWith(SpringJUnit4ClassRunner.class)
public class TestRecommendUserApi {

@Autowired
private RecommendUserService recommendUserService;

@Test
public void testQueryTodayBest(){
TodayBest todayBest = this.recommendUserService.queryTodayBest(1L);
System.out.println(todayBest);
}

}

整合功能测试,需要将sso、dubbo服务启动完成后进行测试。

image-20201130170209235

3.3.5、解决MongoDB启动bug

在项目中,添加了mongo的依赖的话,springboot就会自动去连接本地的mongo,由于他连接不上会导致出错。

image-20231004075724231

解决:

springboot中添加排除自动配置的注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.server;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;

@MapperScan("com.tanhua.server.mapper") //设置mapper接口的扫描包
@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) //排除mongo的自动配置
public class ServerApplication {

public static void main(String[] args) {
SpringApplication.run(ServerApplication.class, args);
}
}

4、推荐列表

image-20231004075730987

4.1、mock接口

地址:https://mock-java.itheima.net/project/35/interface/api/623

image-20201130181444611

image-20231004075709338

4.2、查询参数对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class RecommendUserQueryParam {

private Integer page = 1; //当前页数
private Integer pagesize = 10; //页尺寸
private String gender; //性别 man woman
private String lastLogin; //近期登陆时间
private Integer age; //年龄
private String city; //居住地
private String education; //学历
}

4.3、结果对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult {

private Integer counts = 0;//总记录数
private Integer pagesize = 0;//页大小
private Integer pages = 0;//总页数
private Integer page = 0;//当前页码
private List<?> items = Collections.emptyList(); //列表

}

4.4、Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* 查询推荐用户列表
*
* @param token
* @param queryParam
* @return
*/
@GetMapping("recommendation")
public ResponseEntity<PageResult> queryRecommendation(@RequestHeader("Authorization") String token,
RecommendUserQueryParam queryParam){
try {
PageResult pageResult = this.todayBestService.queryRecommendation(token, queryParam);
if (null != pageResult) {
return ResponseEntity.ok(pageResult);
}
} catch (Exception e) {
log.error("查询推荐用户列表出错~ token = " + token, e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(null);

}

4.5、Service

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
/**
* 查询推荐用户列表
*
* @param queryParam
* @param token
* @return
*/
public PageResult queryRecommendation(String token, RecommendUserQueryParam queryParam) {
//校验token是否有效,通过SSO的接口进行校验
User user = this.userService.queryUserByToken(token);
if (null == user) {
//token非法或已经过期
return null;
}

PageResult pageResult = new PageResult();
pageResult.setPage(queryParam.getPage());
pageResult.setPagesize(queryParam.getPagesize());

PageInfo<RecommendUser> pageInfo = this.recommendUserService.queryRecommendUserList(user.getId(), queryParam.getPage(), queryParam.getPagesize());
List<RecommendUser> records = pageInfo.getRecords();
if (CollectionUtils.isEmpty(records)) {
//没有查询到推荐的用户列表
return pageResult;
}

//填充个人信息

//收集推荐用户的id
Set<Long> userIds = new HashSet<>();
for (RecommendUser record : records) {
userIds.add(record.getUserId());
}

QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();

//用户id参数
queryWrapper.in("user_id", userIds);

if (StringUtils.isNotEmpty(queryParam.getGender())) {
//需要性别参数查询
queryWrapper.eq("sex", StringUtils.equals(queryParam.getGender(), "man") ? 1 : 2);
}

if (StringUtils.isNotEmpty(queryParam.getCity())) {
//需要城市参数查询
queryWrapper.like("city", queryParam.getCity());
}

if (queryParam.getAge() != null) {
//设置年龄参数,条件:小于等于
queryWrapper.le("age", queryParam.getAge());
}

List<UserInfo> userInfoList = this.userInfoService.queryUserInfoList(queryWrapper);
if(CollectionUtils.isEmpty(userInfoList)){
//没有查询到用户的基本信息
return pageResult;
}

List<TodayBest> todayBests = new ArrayList<>();
for (UserInfo userInfo : userInfoList) {
TodayBest todayBest = new TodayBest();

todayBest.setId(userInfo.getUserId());
todayBest.setAvatar(userInfo.getLogo());
todayBest.setNickname(userInfo.getNickName());
todayBest.setTags(StringUtils.split(userInfo.getTags(), ','));
todayBest.setGender(userInfo.getSex().getValue() == 1 ? "man" : "woman");
todayBest.setAge(userInfo.getAge());

//缘分值
for (RecommendUser record : records) {
if(record.getUserId().longValue() == userInfo.getUserId().longValue()){
double score = Math.floor(record.getScore());//取整,98.2 -> 98
todayBest.setFateValue(Double.valueOf(score).longValue());
break;
}
}

todayBests.add(todayBest);
}

//按照缘分值进行倒序排序
Collections.sort(todayBests, (o1, o2) -> new Long(o2.getFateValue() - o1.getFateValue()).intValue());

pageResult.setItems(todayBests);

return pageResult;
}
1
2
3
4
//RecommendUserService
public PageInfo<RecommendUser> queryRecommendUserList(Long id, Integer page, Integer pagesize) {
return this.recommendUserApi.queryPageInfo(id, page, pagesize);
}
1
2
3
4
5
6
7
8
9
10
//UserInfoService
/**
* 查询用户信息列表
*
* @param queryWrapper
* @return
*/
public List<UserInfo> queryUserInfoList(QueryWrapper queryWrapper) {
return this.userInfoMapper.selectList(queryWrapper);
}

4.6、测试

image-20201130190537686

5、缓存

在接口服务中,有必要对于接口进行缓存处理,尤其是GET请求,如果每个接口单独添加的话会存在很多的重复的逻辑,所以可以编写一套通用的解决方案。

实现思路:

  • 通过拦截器实现对请求的拦截,在拦截器中实现缓存的命中。
  • 通过ResponseBodyAdvice进行对响应的拦截,可以将数据缓存到Redis中。
  • 考虑到,不能对于所有的请求都一刀切,所以需要创建@Cache注解进行标记,只有标记的Controller才进行缓存处理。
  • 缓存的处理中,仅针对GET请求处理,其他的请求均不做处理。

5.1、自定义注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tanhua.server.utils;

import java.lang.annotation.*;

/**
* 被标记为Cache的Controller进行缓存,其他情况不进行缓存
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented //标记注解
public @interface Cache {

/**
* 缓存时间,默认为60秒
* @return
*/
String time() default "60";
}

5.2、采用拦截器进行缓存命中

编写拦截器:RedisCacheInterceptor。

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
package com.tanhua.server.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tanhua.server.utils.Cache;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
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 RedisCacheInterceptor implements HandlerInterceptor {

@Value("${tanhua.cache.enable}")
private Boolean enable;

@Autowired
private RedisTemplate<String, String> redisTemplate;

private static final ObjectMapper MAPPER = new ObjectMapper();

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
//缓存的全局开关的校验
if (!enable) {
return true;
}

//校验handler是否是HandlerMethod
if (!(handler instanceof HandlerMethod)) {
return true;
}

//判断是否为get请求
if (!((HandlerMethod) handler).hasMethodAnnotation(GetMapping.class)) {
return true;
}

//判断是否添加了@Cache注解
if (!((HandlerMethod) handler).hasMethodAnnotation(Cache.class)) {
return true;
}

//缓存命中
String redisKey = createRedisKey(request);
String cacheData = this.redisTemplate.opsForValue().get(redisKey);
if(StringUtils.isEmpty(cacheData)){
//缓存未命中
return true;
}

// 将data数据进行响应
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(cacheData);

return false;
}

/**
* 生成redis中的key,规则:SERVER_CACHE_DATA_MD5(url + param + token)
*
* @param request
* @return
*/
public static String createRedisKey(HttpServletRequest request) throws Exception {
String url = request.getRequestURI();
String param = MAPPER.writeValueAsString(request.getParameterMap());
String token = request.getHeader("Authorization");

String data = url + "_" + param + "_" + token;
return "SERVER_CACHE_DATA_" + DigestUtils.md5Hex(data);
}
}

application.properties:

1
2
3
4
5
6
7
8
9
10
#是否开启数据缓存
tanhua.cache.enable=false

# Redis相关配置
spring.redis.jedis.pool.max-wait = 5000ms
spring.redis.jedis.pool.max-Idle = 100
spring.redis.jedis.pool.min-Idle = 10
spring.redis.timeout = 10s
spring.redis.cluster.nodes = 192.168.31.81:6379,192.168.31.81:6380,192.168.31.81:6381
spring.redis.cluster.max-redirects=5

注册拦截器到Spring容器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tanhua.server.config;

import com.tanhua.server.interceptor.RedisCacheInterceptor;
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;

@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(this.redisCacheInterceptor).addPathPatterns("/**");
}
}

4.3、响应结果写入到缓存

使用ResponseBodyAdvice进行对响应结果处理,将结果写入到Redis中:

具体实现:

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
package com.tanhua.server.interceptor;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.tanhua.server.utils.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.MethodParameter;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

import java.util.concurrent.TimeUnit;

@ControllerAdvice
public class MyResponseBodyAdvice implements ResponseBodyAdvice {

@Value("${tanhua.cache.enable}")
private Boolean enable;

@Autowired
private RedisTemplate<String, String> redisTemplate;

private static final ObjectMapper MAPPER = new ObjectMapper();

@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 开关处于开启状态 是get请求 包含了@Cache注解
return enable && returnType.hasMethodAnnotation(GetMapping.class)
&& returnType.hasMethodAnnotation(Cache.class);
}

@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response) {
if (null == body) {
return null;
}

try {

String redisValue = null;
if (body instanceof String) {
redisValue = (String) body;
} else {
redisValue = MAPPER.writeValueAsString(body);
}

String redisKey = RedisCacheInterceptor.createRedisKey(((ServletServerHttpRequest) request).getServletRequest());

Cache cache = returnType.getMethodAnnotation(Cache.class);

//缓存的时间单位是秒
this.redisTemplate.opsForValue().set(redisKey, redisValue, Long.valueOf(cache.time()), TimeUnit.SECONDS);

} catch (Exception e) {
e.printStackTrace();
}

return body;
}
}

4.4、测试

image-20201130202845964

可以看到数据已经缓存到Redis中,并且其缓存时间也是30秒,与预期一致。

image-20201130203238548

6、整合测试

测试时需要注意,由于用户数据较少,所以测试时需要把条件注释掉,否则查询不到数据:

image-20201130202631311

效果:

image-20201130200736231

image-20201130202349684

圈子功能实现

课程说明

  • 抽取common工程
  • 圈子功能说明
  • 圈子技术实现
  • 圈子技术方案
  • 圈子实现发布动态
  • 圈子实现好友动态
  • 圈子实现推荐动态

1、抽取common工程

在项目中一般需要将公用的对象进行抽取放到common工程中,其他的工程依赖此工程即可。下面我们将sso以及server工程中的公用的对象进行抽取。

1.1、创建my-tanhua-common工程

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
<?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工程,并且后续创建的枚举也要放到次工程中,以达到公用的目的。

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
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配置:

1
2
# 枚举包扫描
mybatis-plus.type-enums-package=com.tanhua.common.enums

将server与sso工程中的SexEnum对象删除以及将相关的类引用进行修改。

1.3、抽取mapper

需要将UserInfoMapper以及UserMapper放置到common工程的com.tanhua.common.mapper包下。

1
2
3
4
5
6
7
8
package com.tanhua.common.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.User;

public interface UserMapper extends BaseMapper<User> {

}
1
2
3
4
5
6
7
8
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工程:

image-20201208143929602

1.5、抽取utils

将server工程的utils进行抽取公用,后续的工具类也放置到common工程中。

image-20201211113721758

抽取完成后进行测试,确保可以正常启动以及功能都正常。

2、圈子功能

2.1、功能说明

探花交友项目中的圈子功能,类似微信的朋友圈,基本的功能为:发布动态、浏览好友动态、浏览推荐动态、点赞、评论、喜欢等功能。

image-20231004075804244

发布:

image-20231004075808356

2.2、实现方案分析

对于圈子功能的实现,我们需要对它的功能特点做分析:

  • 数据量会随着用户数增大而增大
  • 读多写少,一般而言,浏览朋友圈动态会多一些,发动态相对就会少一些
  • 非好友看不到其动态内容
  • ……

针对以上特点,我们来分析一下:

  • 对于数据量大而言,显然不能够使用关系型数据库进行存储,我们需要通过MongoDB进行存储
  • 对于读多写少的应用,尽可能的减少读取数据的成本
    • 比如说,一条SQL语句,单张表查询一定比多张表查询要快
    • 条件越多的查询速度将越慢,尽可能的减少条件以提升查询速度

所以对于存储而言,主要是核心的4张表:

  • 发布表:记录了所有用户的发布的东西信息,如图片、视频等。
  • 相册:相册是每个用户独立的,记录了该用户所发布的所有内容。
  • 评论:针对某个具体发布的朋友评论和点赞操作。
  • 时间线:所谓“刷朋友圈”,就是刷时间线,就是一个用户所有的朋友的发布内容。

流程:

image-20201208180725556

流程说明:

  • 用户发布动态,动态中一般包含了图片和文字,图片上传到阿里云,上传成功后拿到图片地址,将文字和图片地址进行持久化存储
  • 首先,需要将动态数据写入到发布表中,其次,再写入到自己的相册表中,需要注意的是,相册表中只包含了发布id,不会冗余存储发布数据
  • 最后,需要将发布数据异步的写入到好友的时间线表中,之所以考虑异步操作,是因为希望发布能够尽快给用户反馈,发布成功
  • 好友刷朋友圈时,实际上只需要查询自己的时间线表即可,这样最大限度的提升了查询速度,再配合redis的缓存,那速度将是飞快的
  • 用户在对动态内容进行点赞、喜欢、评论操作时,只需要写入到评论表即可,该表中也是只会记录发布id,并不会冗余存储发布数据

2.3、表结构设计

发布表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#表名: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 #发布时间
}

相册表:

1
2
3
4
5
6
#表名:quanzi_album_{userId}
{
"_id":"5fae539d7e52992e78a3b684",#主键id
"publishId":"5fae53d17e52992e78a3db61", #发布id
"created":1568012791171 #发布时间
}

时间线表:

1
2
3
4
5
6
7
#表名:quanzi_time_line_{userId}
{
"_id":"5fae539b7e52992e78a3b4ae",#主键id,
"userId":2, #好友id
"publishId":"5fae53d17e52992e78a3db61", #发布id
"date":1568012791171 #发布时间
}

评论表:

1
2
3
4
5
6
7
8
9
10
11
12
#表名: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数据。

好友关系结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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个好友数据:

image-20201208212007691

4、查询好友动态

查询好友动态与查询推荐动态显示的结构是一样的,只是其查询数据源不同:

4.1、基础代码

在my-tanhua-dubbo-interface中编写:

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
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; //发布时间

}
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
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; //发布时间

}
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
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; //发布的时间

}

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
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工程中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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中完成:

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
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/#/

1
2
3
4
5
6
7
8
9
10
11
12
<!-- 在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、测试用例

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
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));

}

}

测试结果: image-20201214105907912

4.3、APP接口服务

开发完成dubbo服务后,我们将开发APP端的接口服务,依然是需要按照mock接口的中的接口定义实现。

接口地址:https://mock-java.itheima.net/project/35/interface/api/683

4.3.1、QuanZiVo

根据接口中响应的数据结构进行定义vo对象:(在my-tanhua-server工程中)

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
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等信息。

代码实现如下:

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
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服务完成数据的查询,并且要完成用户登录是否有效的校验,最后按照服务接口中定义的结构进行封装数据。

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
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 喜欢数
}
}

1
2
3
4
5
6
7
8
9
10
11
12
// 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中增加字段别名,方便直接拷贝属性数据:

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
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、测试

image-20201209162304889

5、统一校验token

在之前的开发中,我们会在每一个Service中对token做处理,相同的逻辑一定是要进行统一处理的,该如何处理呢?

由于程序是运行在web容器中,每一个HTTP请求都是一个独立线程,也就是可以理解成我们编写的应用程序运行在一个多线程的环境中,那么我们就可以使用ThreadLocal在HTTP请求的生命周期内进行存值、取值操作。

如下图:

image-20201209195645804

说明:

  • 用户的每一个请求,都是一个独立的线程
  • 图中的TL就是ThreadLocal,一旦将数据绑定到ThreadLocal中,那么在整个请求的生命周期内都可以随时拿到ThreadLocal中当前线程的数据。

根据上面的分析,我们只需要在Controller请求之前进行对token做校验,如果token有效,则会拿到User对象,然后将该User对象保存到ThreadLocal中即可,最后放行请求,在后续的各个环节中都可以获取到该数据了。

如果token无效,给客户端响应401状态码,拦截请求,不再放行到Controller中。

由此可见,这个校验的逻辑是比较适合放在拦截器中完成的。

5.1、编写UserThreadLocal

在my-tanhua-common工程中,编写UserThreadLocal。

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
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

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
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

1
2
3
4
5
6
7
8
9
10
package com.tanhua.common.utils;

import java.lang.annotation.*;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented //标记注解
public @interface NoAuthorization {

}

5.4、注册拦截器

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
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操作。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//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、发布动态

用户可以在圈子中发布动态,动态内容中可以有文字和图片。如下图:

image-20231004074907728

6.1、dubbo服务

6.1.1、定义接口

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
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、实现接口

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
/**
* 发布动态
*
* @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();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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);
}

}
1
2
3
4
5
6
7
package com.tanhua.dubbo.server.enums;

public enum IdType {

PUBLISH, VIDEO;

}

6.1.3、好友时间线数据

好友的时间线数据需要异步执行。这里使用Spring的@Async注解实现异步执行,其底层是通过启动独立线程来执行,从而可以异步执行。通过返回的CompletableFuture来判断是否执行成功以及是否存在异常。同时需要在启动类中添加@EnableAsync 开启异步的支持。

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
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");
}

}

开启异步执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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、测试好友时间线

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
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工程中进行功能的测试:

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
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

image-20201210173432543

从接口中可以看出,主要的参数有:文字、图片、位置等内容。

6.2.1、图片上传

图片上传功能原来是在sso中完成的,为了能公用该功能,所以需要将图片上传的Service以及配置移动至common工程中。

image-20201210213548508

pom.xml:

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
<?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);
          }
      }
      
      
      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

      #### 6.2.2、接口服务

      需要注意的是,文字是必须提交的,图片是非必须的。

      ~~~java
      //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实现

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
// 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中,数据如下:

1
2
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、定义接口

1
2
3
4
5
6
7
8
9
10
11
12
//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、编写实现

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
//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

1
2
3
4
5
6
7
8
9
10
11
12
13
//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

在实现中,将查询好友动态的方法中公共的内容,进行抽取,具体如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//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;
}
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
/**
* 填充用户信息
*
* @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;
}

7.3、测试

image-20220829215153508

圈子和小视频

课程说明

  • 圈子实现点赞、喜欢功能
  • 圈子实现评论
  • 圈子实现评论的点赞
  • 小视频功能介绍
  • FastDFS入门学习
  • 实现发布小视频功能
  • 实现查询小视频列表功能

1、圈子点赞实现分析

在圈子功能中,对于圈子的点赞、喜欢、评论等均可理解为用户对动态的评论(Comment),在quanzi_comment表中使用commentType进行区分。

在具体的实现中,需要将点赞数、某用户是否点赞等数据保存到Reds中,以减轻MongoDB的压力。

具体存储结构如下:

image-20201218153502042

说明:在Redis的存储结构中,采用的是Hash存储,这样的好处就在于一条动态的点赞、喜欢等数据都会集中的存储到一起,从而减少了Redis中数据条数。

2、点赞

2.1、定义枚举

为了规范使用CommentType,所以将其定义为枚举类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tanhua.dubbo.server.enums;

/**
* 评论类型:1-点赞,2-评论,3-喜欢
*/
public enum CommentType {

LIKE(1), COMMENT(2), LOVE(3);

int type;

CommentType(int type) {
this.type = type;
}

public int getType() {
return type;
}
}

2.2、dubbo服务

2.2.1、定义接口

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
package com.tanhua.dubbo.server.api;

import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.dubbo.server.vo.PageInfo;

public interface QuanZiApi {

//........此处忽略其他代码..........

/**
* 根据id查询动态
*
* @param id 动态id
* @return
*/
Publish queryPublishById(String id);

/**
* 点赞
*
* @param userId
* @param publishId
* @return
*/
Boolean likeComment(Long userId, String publishId);

/**
* 取消点赞
*
* @param userId
* @param publishId
* @return
*/
Boolean disLikeComment(Long userId, String publishId);

/**
* 查询点赞数
*
* @param publishId
* @return
*/
Long queryLikeCount(String publishId);

/**
* 查询用户是否点赞该动态
*
* @param userId
* @param publishId
* @return
*/
Boolean queryUserIsLike(Long userId, String publishId);

}

2.2.2、编写实现

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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
package com.tanhua.dubbo.server.api;

@Service(version = "1.0.0")
@Slf4j
public class QuanZiApiImpl implements QuanZiApi {

//评论数据存储在Redis中key的前缀
private static final String COMMENT_REDIS_KEY_PREFIX = "QUANZI_COMMENT_";

//用户是否点赞的前缀
private static final String COMMENT_USER_LIEK_REDIS_KEY_PREFIX = "USER_LIKE_";

//用户是否喜欢的前缀
private static final String COMMENT_USER_LOVE_REDIS_KEY_PREFIX = "USER_LOVE_";

@Autowired
private MongoTemplate mongoTemplate;

@Autowired
private RedisTemplate<String, String> redisTemplate;

//........此处忽略其他代码..........

@Override
public Publish queryPublishById(String id) {
return this.mongoTemplate.findById(new ObjectId(id), Publish.class);
}

@Override
public Boolean likeComment(Long userId, String publishId) {
//判断该用户是否已经点赞,如果已经点赞就直接返回
if (this.queryUserIsLike(userId, publishId)) {
return false;
}

//保存Comment数据
Boolean result = this.saveComment(userId, publishId, CommentType.LIKE, null);
if (!result) {
return false;
}

//修改redis中的点赞数以及是否点赞

//修改点赞数
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String hashKey = CommentType.LIKE.toString();
this.redisTemplate.opsForHash().increment(redisKey, hashKey, 1);

//用户是否点赞
String userHashKey = this.getCommentUserLikeRedisKeyPrefix(userId);
this.redisTemplate.opsForHash().put(redisKey, userHashKey, "1");

return true;
}

private String getCommentRedisKeyPrefix(String publishId) {
return COMMENT_REDIS_KEY_PREFIX + publishId;
}

private String getCommentUserLikeRedisKeyPrefix(Long userId) {
return COMMENT_USER_LIKE_REDIS_KEY_PREFIX + userId;
}

@Override
public Boolean disLikeComment(Long userId, String publishId) {
//判断用户是否已经点赞,如果没有点赞就返回
if (!this.queryUserIsLike(userId, publishId)) {
return false;
}

//删除评论数据
Boolean result = this.removeComment(userId, publishId, CommentType.LIKE);
if (!result) {
return false;
}

//修改Redis中的数据

//修改点赞数
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String hashKey = CommentType.LIKE.toString();
this.redisTemplate.opsForHash().increment(redisKey, hashKey, -1);

//用户是否点赞
String userHashKey = this.getCommentUserLikeRedisKeyPrefix(userId);
this.redisTemplate.opsForHash().delete(redisKey, userHashKey);

return true;
}

@Override
public Long queryLikeCount(String publishId) {
//从Redis中命中查询,如果命中直接返回即可
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String hashKey = CommentType.LIKE.toString();
Object data = this.redisTemplate.opsForHash().get(redisKey, hashKey);
if (ObjectUtil.isNotEmpty(data)) {
return Convert.toLong(data);
}

//查询Mongodb
Long count = this.queryCommentCount(publishId, CommentType.LIKE);
//写入Redis中
this.redisTemplate.opsForHash().put(redisKey, hashKey, String.valueOf(count));

return count;
}

@Override
public Boolean queryUserIsLike(Long userId, String publishId) {
//从redis中查询数据
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String userHashKey = this.getCommentUserLikeRedisKeyPrefix(userId);
Object data = this.redisTemplate.opsForHash().get(redisKey, userHashKey);
if (ObjectUtil.isNotEmpty(data)) {
return StrUtil.equals(Convert.toStr(data), "1");
}

//查询Mongodb,确定是否已经点赞
Query query = Query.query(Criteria.where("publishId").is(new ObjectId(publishId))
.and("userId").is(userId)
.and("commentType").is(CommentType.LIKE)
);
long count = this.mongoTemplate.count(query, Comment.class);
if(count == 0){
return false;
}

//写入到redis中
this.redisTemplate.opsForHash().put(redisKey, userHashKey, "1");

return true;
}

/**
* 保存Comment
*
* @return
*/
private Boolean saveComment(Long userId, String publishId,
CommentType commentType, String content) {
try {
Comment comment = new Comment();
comment.setId(ObjectId.get());
comment.setUserId(userId);
comment.setPublishId(new ObjectId(publishId));
// 评论类型
comment.setCommentType(commentType.getType());
// 内容
comment.setContent(content);
comment.setCreated(System.currentTimeMillis());

Publish publish = this.queryPublishById(publishId);
//TODO 其他评论对象,暂不处理
comment.setPublishUserId(publish.getUserId());

this.mongoTemplate.save(comment);

return true;
} catch (Exception e) {
log.error("保存Comment出错~ userId = " + userId + ", publishId = " + publishId + ", commentType = " + commentType, e);
}

return false;
}

/**
* 删除评论数据
*
* @param userId
* @param publishId
* @param commentType
* @return
*/
private Boolean removeComment(Long userId, String publishId, CommentType commentType) {
Query query = Query.query(Criteria.where("userId").is(userId)
.and("publishId").is(new ObjectId(publishId))
.and("commentType").is(commentType.getType())
);
return this.mongoTemplate.remove(query, Comment.class).getDeletedCount() > 0;
}

/**
* 查询数量
*
* @param publishId
* @param commentType
* @return
*/
private Long queryCommentCount(String publishId, CommentType commentType) {
Query query = Query.query(Criteria.where("publishId").is(new ObjectId(publishId))
.and("commentType").is(commentType.getType())
);
return this.mongoTemplate.count(query, Comment.class);
}
}

2.2.3、编写测试用例

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
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 testLike(){
Long userId = 1L;
String publishId = "5fae53947e52992e78a3afb1";
Boolean data = this.quanZiApi.queryUserIsLike(userId, publishId);
System.out.println(data);

System.out.println(this.quanZiApi.likeComment(userId, publishId));

System.out.println(this.quanZiApi.queryLikeCount(publishId));

System.out.println(this.quanZiApi.disLikeComment(userId, publishId));

System.out.println(this.quanZiApi.queryLikeCount(publishId));
}

}

2.3、APP接口服务

点赞接口地址:https://mock-java.itheima.net/project/35/interface/api/707

image-20201218161505609

image-20201218161521081

从接口文档来看,点赞完成后需要返回点赞数。

2.3.1、编写接口服务

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
//com.tanhua.server.controller.QuanZiController
/**
* 点赞
*
* @param publishId
* @return
*/
@GetMapping("/{id}/like")
public ResponseEntity<Long> likeComment(@PathVariable("id") String publishId) {
try {
Long likeCount = this.quanZiService.likeComment(publishId);
if (likeCount != null) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 取消点赞
*
* @param publishId
* @return
*/
@GetMapping("/{id}/dislike")
public ResponseEntity<Long> disLikeComment(@PathVariable("id") String publishId) {
try {
Long likeCount = this.quanZiService.disLikeComment(publishId);
if (null != likeCount) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.3.2、编写服务实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//com.tanhua.server.service.QuanZiService

public Long likeComment(String publishId) {
User user = UserThreadLocal.get();
//点赞
Boolean result = this.quanZiApi.likeComment(user.getId(), publishId);
if(result){
//查询点赞数
return this.quanZiApi.queryLikeCount(publishId);
}
return null;
}

public Long disLikeComment(String publishId) {
User user = UserThreadLocal.get();
//取消点赞
Boolean result = this.quanZiApi.disLikeComment(user.getId(), publishId);
if(result){
//查询点赞数
return this.quanZiApi.queryLikeCount(publishId);
}
return null;
}

2.3.3、修改查询动态点赞数

查询点赞数、是否点赞,需要通过dubbo服务进行查询。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//com.tanhua.server.service.QuanZiService
/**
* 填充用户信息
*
* @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(), ','));

//当前用户
User user = UserThreadLocal.get();

quanZiVo.setCommentCount(0); //TODO 评论数
quanZiVo.setDistance("1.2公里"); //TODO 距离
quanZiVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
quanZiVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(quanZiVo.getId()))); //点赞数
quanZiVo.setHasLoved(0); //TODO 是否喜欢(1是,0否)
quanZiVo.setLoveCount(0); //TODO 喜欢数
}

2.3.4、测试

image-20201221105505538

从测试结果中可以看出,在响应结果中返回了点赞数以及是否点赞的数据。

3、喜欢

喜欢的实现与点赞类似,只是其类型不同。需要注意的是,在推荐动态中才有喜欢功能,好友动态中是没有此功能的。

3.1、dubbo服务

3.1.1、定义接口

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
//com.tanhua.dubbo.server.api.QuanZiApi

/**
* 喜欢
*
* @param userId
* @param publishId
* @return
*/
Boolean loveComment(Long userId, String publishId);

/**
* 取消喜欢
*
* @param userId
* @param publishId
* @return
*/
Boolean disLoveComment(Long userId, String publishId);

/**
* 查询喜欢数
*
* @param publishId
* @return
*/
Long queryLoveCount(String publishId);

/**
* 查询用户是否喜欢该动态
*
* @param userId
* @param publishId
* @return
*/
Boolean queryUserIsLove(Long userId, String publishId);

3.1.2、编写实现

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
//com.tanhua.dubbo.server.api.QuanZiApiImpl

@Override
public Boolean loveComment(Long userId, String publishId) {
//查询该用户是否已经喜欢
if (this.queryUserIsLove(userId, publishId)) {
return false;
}

//喜欢
boolean result = this.saveComment(userId, publishId, CommentType.LOVE, null);
if (!result) {
return false;
}

//喜欢成功后,修改Redis中的总的喜欢数
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String hashKey = CommentType.LOVE.toString();
this.redisTemplate.opsForHash().increment(redisKey, hashKey, 1);

//标记用户已经喜欢
hashKey = this.getCommentUserLoveRedisKey(userId);
this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");

return true;
}

private String getCommentUserLoveRedisKey(Long userId) {
return COMMENT_USER_LOVE_REDIS_KEY_PREFIX + userId;
}

@Override
public Boolean disLoveComment(Long userId, String publishId) {
if (!this.queryUserIsLove(userId, publishId)) {
//如果用户没有喜欢,就直接返回
return false;
}

boolean result = this.removeComment(userId, publishId, CommentType.LOVE);
if (!result) {
//删除失败
return false;
}

//删除redis中的记录
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String hashKey = this.getCommentUserLoveRedisKey(userId);
this.redisTemplate.opsForHash().delete(redisKey, hashKey);
this.redisTemplate.opsForHash().increment(redisKey, CommentType.LOVE.toString(), -1);

return true;
}

@Override
public Long queryLoveCount(String publishId) {
//首先从redis中命中,如果命中的话就返回,没有命中就查询Mongodb
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String hashKey = CommentType.LOVE.toString();
Object value = this.redisTemplate.opsForHash().get(redisKey, hashKey);
if (ObjectUtil.isNotEmpty(value)) {
return Convert.toLong(value);
}

//查询count
Long count = this.queryCommentCount(publishId, CommentType.LOVE);

//存储到redis中
this.redisTemplate.opsForHash().put(redisKey, hashKey, String.valueOf(count));

return count;
}

@Override
public Boolean queryUserIsLove(Long userId, String publishId) {
String redisKey = this.getCommentRedisKeyPrefix(publishId);
String hashKey = this.getCommentUserLoveRedisKey(userId);
Object value = this.redisTemplate.opsForHash().get(redisKey, hashKey);
if (ObjectUtil.isNotEmpty(value)) {
return StrUtil.equals(Convert.toStr(value), "1");
}

//查询mongodb
Query query = Query.query(Criteria.where("publishId")
.is(new ObjectId(publishId))
.and("userId").is(userId)
.and("commentType").is(CommentType.LOVE.getType()));
long count = this.mongoTemplate.count(query, Comment.class);
if (count == 0) {
return false;
}

//标记用户已经喜欢
this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");

return true;
}

3.2、APP接口服务

3.2.1、编写接口服务

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
//com.tanhua.server.controller.QuanZiController

/**
* 喜欢
*
* @param publishId
* @return
*/
@GetMapping("/{id}/love")
public ResponseEntity<Long> loveComment(@PathVariable("id") String publishId) {
try {
Long loveCount = this.quanZiService.loveComment(publishId);
if (null != loveCount) {
return ResponseEntity.ok(loveCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 取消喜欢
*
* @param publishId
* @return
*/
@GetMapping("/{id}/unlove")
public ResponseEntity<Long> disLoveComment(@PathVariable("id") String publishId) {
try {
Long loveCount = this.quanZiService.disLoveComment(publishId);
if (null != loveCount) {
return ResponseEntity.ok(loveCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.2.2、编写服务实现

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
//com.tanhua.server.service.QuanZiService

public Long loveComment(String publishId) {
User user = UserThreadLocal.get();
//喜欢
Boolean result = this.quanZiApi.loveComment(user.getId(), publishId);
if(result){
//查询喜欢数
return this.quanZiApi.queryLoveCount(publishId);
}
return null;
}

public Long disLoveComment(String publishId) {
User user = UserThreadLocal.get();
//取消喜欢
Boolean result = this.quanZiApi.disLoveComment(user.getId(), publishId);
if(result){
//查询喜欢数
return this.quanZiApi.queryLoveCount(publishId);
}
return null;
}

/**
* 填充用户信息
*
* @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(), ','));

//当前用户
User user = UserThreadLocal.get();

quanZiVo.setCommentCount(0); //TODO 评论数
quanZiVo.setDistance("1.2公里"); //TODO 距离
quanZiVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
quanZiVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(quanZiVo.getId()))); //点赞数
quanZiVo.setHasLoved(this.quanZiApi.queryUserIsLove(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否喜欢(1是,0否)
quanZiVo.setLoveCount(Convert.toInt(this.quanZiApi.queryLoveCount(quanZiVo.getId()))); //喜欢数
}

1.2.3、测试

image-20201221114335332

4、查询单条动态

用户点击评论时需要查询单条动态详情,需要有接口支持。

服务接口地址:https://mock-java.itheima.net/project/35/interface/api/695

image-20201221114927609

响应的数据接口与查询好友动态一致,只是单条返回而不是集合。

要注意的是,dubbo服务接口在前面已经开发完成,现在只要想实现APP端的接口服务即可。

4.1、定义服务接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.server.controller.QuanZiController

/**
* 查询单条动态信息
*
* @param publishId
* @return
*/
@GetMapping("/{id}")
public ResponseEntity<QuanZiVo> queryById(@PathVariable("id") String publishId) {
try {
QuanZiVo movements = this.quanZiService.queryById(publishId);
if(null != movements){
return ResponseEntity.ok(movements);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

4.2、服务实现

1
2
3
4
5
6
7
8
9
//com.tanhua.server.service.QuanZiService

public QuanZiVo queryById(String publishId) {
Publish publish = this.quanZiApi.queryPublishById(publishId);
if (publish == null) {
return null;
}
return this.fillQuanZiVo(Arrays.asList(publish)).get(0);
}

4.3、测试

image-20201221214045637

可以看到,返回了单条数据。

4.4、异常的解决

在完成查询单条动态接口后,会发现,刷新首页时会出现如下异常:

1
2
3
4
5
6
7
8
9
java.lang.IllegalArgumentException: invalid hexadecimal representation of an ObjectId: [visitors]
at org.bson.types.ObjectId.parseHexString(ObjectId.java:550)
at org.bson.types.ObjectId.<init>(ObjectId.java:239)
at com.tanhua.dubbo.server.api.QuanZiApiImpl.queryPublishById(QuanZiApiImpl.java:411)
at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java)
at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:47)
at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:76)
at com.alibaba.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:52)
at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56)

原因是:谁看过我的接口还没实现,导致了映射到了查询单条动态的接口,导致的异常,接口地址:https://mock-java.itheima.net/project/35/interface/api/743

解决方法:编写一个空的方法《谁看过我》的接口实现。

1
2
3
4
5
6
7
8
9
10
//com.tanhua.server.controller.QuanZiController
/**
* TODO:谁看过我
*
* @return
*/
@GetMapping("visitors")
public ResponseEntity<Object> queryVisitors() {
return ResponseEntity.ok(Collections.EMPTY_LIST);
}

5、评论

在单条动态打开后,可以看到有评论列表,功能包括:查询评论列表,评论点赞、取消点赞。

需要注意的是,评论的点赞操作与圈子动态的点赞使用同一套逻辑。

5.1、dubbo服务

5.1.1、定义服务接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.dubbo.server.api.QuanZiApi

/**
* 查询评论
*
* @return
*/
PageInfo<Comment> queryCommentList(String publishId, Integer page, Integer pageSize);

/**
* 发表评论
*
* @param userId
* @param publishId
* @param content
* @return
*/
Boolean saveComment(Long userId, String publishId, String content);

5.1.2、编写实现

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
/**
* 查询评论列表
*
* @param publishId
* @param page
* @param pageSize
* @return
*/
@Override
public PageInfo<Comment> queryCommentList(String publishId, Integer page, Integer pageSize) {
PageRequest pageRequest = PageRequest.of(page - 1, pageSize, Sort.by(Sort.Order.asc("created")));

Query query = new Query(Criteria
.where("publishId").is(new ObjectId(publishId))
.and("commentType").is(CommentType.COMMENT.getType())).with(pageRequest);

//查询评论列表
List<Comment> commentList = this.mongoTemplate.find(query, Comment.class);

PageInfo<Comment> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);
pageInfo.setRecords(commentList);
return pageInfo;
}

/**
* 发表评论
*
* @param userId
* @param publishId
* @param content
* @return
*/
@Override
public Boolean saveComment(Long userId, String publishId, String content) {
return this.saveComment(userId, publishId, CommentType.COMMENT, content);
}

5.2、APP接口服务

5.2.1、CommentVo

根据响应结果的数据结构定义对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
* 评论
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommentVo {

private String id; //评论id
private String avatar; //头像
private String nickname; //昵称
private String content; //评论
private String createDate; //评论时间: 08:27
private Integer likeCount; //点赞数
private Integer hasLiked; //是否点赞(1是,0否)

}

5.2.2、编写Controller

在APP接口服务中,需要开发4个接口,分别是查询评论列表、发表评论、点赞、取消点赞。

由于其接口的url地址与QuanZiConroller地址不同,所以需要创建不同的Controller类。

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
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.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

/**
* 圈子功能中的评论
*/
@RestController
@RequestMapping("comments")
public class QuanZiCommentController {

@Autowired
private QuanZiService quanZiService;

/**
* 查询评论列表
*
* @return
*/
@GetMapping
public ResponseEntity<PageResult> queryCommentsList(@RequestParam("movementId") String publishId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PageResult pageResult = this.quanZiService.queryCommentList(publishId, page, pageSize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

}

/**
* 保存评论
*/
@PostMapping
public ResponseEntity<Void> saveComments(@RequestBody Map<String, String> param) {
try {
String publishId = param.get("movementId");
String content = param.get("comment");
Boolean result = this.quanZiService.saveComments(publishId, content);
if (result) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 点赞
*
* @param publishId
* @return
*/
@GetMapping("{id}/like")
public ResponseEntity<Long> likeComment(@PathVariable("id") String publishId) {
try {
Long likeCount = this.quanZiService.likeComment(publishId);
if (likeCount != null) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 取消点赞
*
* @param publishId
* @return
*/
@GetMapping("{id}/dislike")
public ResponseEntity<Long> disLikeComment(@PathVariable("id") String publishId) {
try {
Long likeCount = this.quanZiService.disLikeComment(publishId);
if (null != likeCount) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

}

5.2.3、编写Service实现

Service的具体实现依然是放到QuanZiSerivce中完成。

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
//com.tanhua.server.service.QuanZiService

/**
* 查询评论列表
*
* @param publishId
* @param page
* @param pageSize
* @return
*/
public PageResult queryCommentList(String publishId, Integer page, Integer pageSize) {

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

User user = UserThreadLocal.get();

//查询评论列表数据
PageInfo<Comment> pageInfo = this.quanZiApi.queryCommentList(publishId, page, pageSize);
List<Comment> records = pageInfo.getRecords();
if(CollUtil.isEmpty(records)){
return pageResult;
}

//查询用户信息
List<Object> userIdList = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIdList);

List<CommentVo> result = new ArrayList<>();
for (Comment record : records) {
CommentVo commentVo = new CommentVo();
commentVo.setContent(record.getContent());
commentVo.setId(record.getId().toHexString());
commentVo.setCreateDate(DateUtil.format(new Date(record.getCreated()), "HH:mm"));
//是否点赞
commentVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), commentVo.getId()) ? 1 : 0);
//点赞数
commentVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(commentVo.getId())));


for (UserInfo userInfo : userInfoList) {
if(ObjectUtil.equals(record.getUserId(), userInfo.getUserId())){

commentVo.setAvatar(userInfo.getLogo());
commentVo.setNickname(userInfo.getNickName());

break;
}
}

result.add(commentVo);
}

pageResult.setItems(result);

return pageResult;
}

/**
* 发表评论
* @param publishId
* @param content
* @return
*/
public Boolean saveComments(String publishId, String content) {
User user = UserThreadLocal.get();
return this.quanZiApi.saveComment(user.getId(), publishId, content);
}

5.2.4、测试

image-20201222115831760

测试点赞时会发现dubbo服务中会出现null指针异常,如下:

1
2
3
4
5
6
7
8
java.lang.NullPointerException
at com.tanhua.dubbo.server.api.QuanZiApiImpl.saveComment(QuanZiApiImpl.java:386)
at com.tanhua.dubbo.server.api.QuanZiApiImpl.likeComment(QuanZiApiImpl.java:180)
at com.alibaba.dubbo.common.bytecode.Wrapper1.invokeMethod(Wrapper1.java)
at com.alibaba.dubbo.rpc.proxy.javassist.JavassistProxyFactory$1.doInvoke(JavassistProxyFactory.java:47)
at com.alibaba.dubbo.rpc.proxy.AbstractProxyInvoker.invoke(AbstractProxyInvoker.java:76)
at com.alibaba.dubbo.config.invoker.DelegateProviderMetaDataInvoker.invoke(DelegateProviderMetaDataInvoker.java:52)
at com.alibaba.dubbo.rpc.protocol.InvokerWrapper.invoke(InvokerWrapper.java:56)

原因是:原有的点赞实现中,需要查询Publish对象,但是现在实现的是针对评论的点赞,是查询不到Publish对象的,所以抛出了空指针异常。

解决如下:

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
//com.tanhua.dubbo.server.api.QuanZiApiImpl

/**
* 保存Comment
*
* @return
*/
private Boolean saveComment(Long userId, String publishId,
CommentType commentType, String content) {
try {
Comment comment = new Comment();
comment.setId(ObjectId.get());
comment.setUserId(userId);
comment.setPublishId(new ObjectId(publishId));
// 评论类型
comment.setCommentType(commentType.getType());
// 内容
comment.setContent(content);
comment.setCreated(System.currentTimeMillis());

Publish publish = this.queryPublishById(publishId);
if (ObjectUtil.isNotEmpty(publish)) {
comment.setPublishUserId(publish.getUserId());
} else {
//查询评论
Comment myComment = this.queryCommentById(publishId);
if(ObjectUtil.isNotEmpty(myComment)){
comment.setPublishUserId(myComment.getUserId());
}else{
//TODO 其他情况,比如小视频等
}
}

this.mongoTemplate.save(comment);

return true;
} catch (Exception e) {
log.error("保存Comment出错~ userId = " + userId + ", publishId = " + publishId + ", commentType = " + commentType, e);
}

return false;
}

/**
* 根据id查询Comment对象
*
* @param id
* @return
*/
private Comment queryCommentById(String id) {
return this.mongoTemplate.findById(new ObjectId(id), Comment.class);
}

image-20201222120513728

这样,点赞功能正常了。

6、小视频

6.1、功能说明

小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。

效果:

image-20231004075845481

查看详情:

image-20231004075849740

评论:

image-20231004075854600

点赞:

image-20231004075859897

6.2、技术方案

对于小视频的功能的开发,核心点就是:存储 + 推荐 + 加载速度 。

  • 对于存储而言,小视频的存储量以及容量都是非常巨大的。
    • 所以我们选择自己搭建分布式存储系统 FastDFS进行存储。
  • 对于推荐算法,我们将采用多种权重的计算方式进行计算。
  • 对于加载速度,除了提升服务器带宽外可以通过CDN的方式进行加速,当然了这需要额外购买CDN服务。

7、FastDFS

7.1、FastDFS是什么?

FastDFS是分布式文件系统。使用 FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

7.2、工作原理

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过 Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到 Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。

Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storage server 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。

image-20231004074923690

每个 tracker 节点地位平等,收集 Storage 集群的状态。

Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。

7.3、文件的上传

image-20231004074927941

客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。

7.4、文件的下载

image-20231004074933273

客户端下载请求到Tracker服务,Tracker返回给客户端storage的信息,客户端根据这些信息进行请求storage获取到文件。

7.5、开始使用

在我们提供的虚拟机中已经通过docker搭建了FastDFS环境,下面我们来学习下如何通过Java程序来使用FastDFS。

7.5.1、引入依赖

关于使用FastDFS上传小视频的逻辑我们在server工程中完成,所以需要在server工程中引入依赖。

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
<exclusions>
<exclusion>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</exclusion>
</exclusions>
</dependency>

7.5.2、编写配置文件

在application.properties配置文件中加入如下内容:

1
2
3
4
5
6
7
8
9
10
11
12
# ===================================================================
# 分布式文件系统FDFS配置
# ===================================================================
fdfs.so-timeout = 1501
fdfs.connect-timeout = 601
#缩略图生成参数
fdfs.thumb-image.width= 150
fdfs.thumb-image.height= 150
#TrackerList参数,支持多个
fdfs.tracker-list=192.168.31.81:22122
#访问路径
fdfs.web-server-url=http://192.168.31.81:8888/

7.5.3、测试代码

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
package com.tanhua.server;

import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.apache.commons.io.FileUtils;
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.io.File;
import java.io.IOException;

@RunWith(SpringRunner.class)
@SpringBootTest
public class TestFastDFS {

@Autowired
protected FastFileStorageClient storageClient;

@Autowired
private FdfsWebServer fdfsWebServer;

@Test
public void testUpload(){
String path = "F:\\1.jpg";
File file = new File(path);

try {
StorePath storePath = this.storageClient.uploadFile(FileUtils.openInputStream(file), file.length(), "jpg", null);

System.out.println(storePath); //StorePath [group=group1, path=M00/00/00/wKgfUV2GJSuAOUd_AAHnjh7KpOc1.1.jpg]
System.out.println(fdfsWebServer.getWebServerUrl() + storePath.getFullPath());//group1/M00/00/00/wKgfUV2GJSuAOUd_AAHnjh7KpOc1.1.jpg
} catch (IOException e) {
e.printStackTrace();
}
}
}

通过浏览器访问图片:

image-20201222171613466

8、发布小视频

发布小视频的流程如下:

image-20201224231912916

说明:

  • 用户发通过客户端APP上传视频到server服务
  • server服务上传视频到FastDFS文件系统,上传成功后返回视频的url地址
  • server通过rpc的调用dubbo服务进行保存小视频数据

8.1、dubbo服务

8.1.1、编写pojo

在dubbo接口工程中编写pojo:

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
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;

import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "video")
public class Video implements java.io.Serializable {

private static final long serialVersionUID = -3136732836884933873L;

private ObjectId id; //主键id
private Long vid; //自增长id
private Long userId;
private String text; //文字
private String picUrl; //视频封面文件
private String videoUrl; //视频文件
private Long created; //创建时间
private Integer seeType; // 谁可以看,1-公开,2-私密,3-部分可见,4-不给谁看
private List<Long> seeList; //部分可见的列表
private List<Long> notSeeList; //不给谁看的列表
private String longitude; //经度
private String latitude; //纬度
private String locationName; //位置名称
}

8.1.2、定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tanhua.dubbo.server.api;

import com.tanhua.dubbo.server.pojo.Video;

public interface VideoApi {

/**
* 保存小视频
*
* @param video
* @return 保存成功后,返回视频id
*/
String saveVideo(Video video);

}

8.1.3、编写实现

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
package com.tanhua.dubbo.server.api;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.mongodb.Mongo;
import com.tanhua.dubbo.server.enums.IdType;
import com.tanhua.dubbo.server.pojo.Video;
import com.tanhua.dubbo.server.service.IdService;
import lombok.extern.slf4j.Slf4j;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;

@Service(version = "1.0.0")
@Slf4j
public class VideoApiImpl implements VideoApi {

@Autowired
private IdService idService;

@Autowired
private MongoTemplate mongoTemplate;

/**
* 发布小视频
*
* @param video
* @return
*/
@Override
public String saveVideo(Video video) {
try {
//校验
if(!ObjectUtil.isAllNotEmpty(video.getUserId(), video.getPicUrl(), video.getVideoUrl())){
return null;
}

//设置id
video.setId(ObjectId.get());
video.setVid(this.idService.createId(IdType.VIDEO));

//发布时间
video.setCreated(System.currentTimeMillis());

//保存到Mongodb中
this.mongoTemplate.save(video);

return video.getId().toHexString();
} catch (Exception e) {
log.error("小视频发布失败~ video = " + video, e);
}
return null;
}
}

8.2、APP接口服务

接口地址:https://mock-java.itheima.net/project/35/interface/api/821

image-20201222212229360

8.2.1、VideoController

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
package com.tanhua.server.controller;

import com.tanhua.server.service.VideoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("smallVideos")
public class VideoController {

@Autowired
private VideoService videoService;

/**
* 发布小视频
*
* @param picFile
* @param videoFile
* @return
*/
@PostMapping
public ResponseEntity<Void> saveVideo(@RequestParam("videoThumbnail") MultipartFile picFile,
@RequestParam("videoFile") MultipartFile videoFile) {
try {
Boolean bool = this.videoService.saveVideo(picFile, videoFile);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

8.2.2、VideoService

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
package com.tanhua.server.service;

import cn.hutool.core.util.StrUtil;
import com.alibaba.dubbo.config.annotation.Reference;
import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.tanhua.common.pojo.User;
import com.tanhua.common.service.PicUploadService;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.common.vo.PicUploadResult;
import com.tanhua.dubbo.server.api.VideoApi;
import com.tanhua.dubbo.server.pojo.Video;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@Slf4j
public class VideoService {

@Autowired
private PicUploadService picUploadService;

@Autowired
protected FastFileStorageClient storageClient;

@Autowired
private FdfsWebServer fdfsWebServer;

@Reference(version = "1.0.0")
private VideoApi videoApi;

/**
* 发布小视频
*
* @param picFile
* @param videoFile
* @return
*/
public Boolean saveVideo(MultipartFile picFile, MultipartFile videoFile) {
User user = UserThreadLocal.get();

Video video = new Video();
video.setUserId(user.getId());
video.setSeeType(1); //默认公开

try {
//上传封面图片
PicUploadResult picUploadResult = this.picUploadService.upload(picFile);
video.setPicUrl(picUploadResult.getName()); //图片路径

//上传视频
StorePath storePath = storageClient.uploadFile(videoFile.getInputStream(),
videoFile.getSize(),
StrUtil.subAfter(videoFile.getOriginalFilename(), '.', true),
null);

//设置视频url
video.setVideoUrl(fdfsWebServer.getWebServerUrl() + storePath.getFullPath());

String videoId = this.videoApi.saveVideo(video);
return StrUtil.isNotEmpty(videoId);
} catch (Exception e) {
log.error("发布小视频失败!file = " + picFile.getOriginalFilename() , e);
}

return false;
}
}

5.4.3、测试

如果上传视频,会导致异常,是因为请求太大的缘故:

image-20231004075926291

解决:application.properties

1
2
spring.servlet.multipart.max-file-size=30MB
spring.servlet.multipart.max-request-size=30MB

测试:

image-20201222214127912

image-20201222214313987

可以看到数据已经写入到了MongoDB中。

9、小视频列表

小视频的列表查询的实现需要注意的是,如果有推荐视频,优先返回推荐视频,如果不够或没有,按照时间倒序查询视频表。

推荐数据:

image-20201222224514603

9.1、dubbo服务

9.1.1、定义dubbo服务

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
package com.tanhua.dubbo.server.api;

import com.tanhua.dubbo.server.pojo.Video;
import com.tanhua.dubbo.server.vo.PageInfo;

public interface VideoApi {

/**
* 保存小视频
*
* @param video
* @return
*/
Boolean saveVideo(Video video);

/**
* 分页查询小视频列表,按照时间倒序排序
*
* @param userId
* @param page
* @param pageSize
* @return
*/
PageInfo<Video> queryVideoList(Long userId, Integer page, Integer pageSize);

}

9.1.2、实现dubbo服务

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
//com.tanhua.dubbo.server.api.VideoApiImpl

/**
* 查询小视频列表,优先展现推荐的视频,如果没有推荐的视频或已经查询完成,就需要查询系统视频数据
*
* @param userId
* @param page
* @param pageSize
* @return
*/
@Override
public PageInfo<Video> queryVideoList(Long userId, Integer page, Integer pageSize) {
PageInfo<Video> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);

//从redis中获取推荐视频的数据
String redisKey = "QUANZI_VIDEO_RECOMMEND_" + userId;
String redisData = this.redisTemplate.opsForValue().get(redisKey);
List<Long> vids = new ArrayList<>();
int recommendCount = 0;
if (StrUtil.isNotEmpty(redisData)) {
//手动分页查询数据
List<String> vidList = StrUtil.split(redisData, ',');
//计算分页
//[0, 10]
int[] startEnd = PageUtil.transToStartEnd(page - 1, pageSize);
int startIndex = startEnd[0]; //开始
int endIndex = Math.min(startEnd[1], vidList.size()); //结束

for (int i = startIndex; i < endIndex; i++) {
vids.add(Convert.toLong(vidList.get(i)));
}
recommendCount = vidList.size();
}

if (CollUtil.isEmpty(vids)) {
//没有推荐或前面推荐已经查询完毕,查询系统的视频数据

//计算前面的推荐视频页数
int totalPage = PageUtil.totalPage(recommendCount, pageSize);

PageRequest pageRequest = PageRequest.of(page - totalPage - 1, pageSize, Sort.by(Sort.Order.desc("created")));
Query query = new Query().with(pageRequest);
List<Video> videoList = this.mongoTemplate.find(query, Video.class);
pageInfo.setRecords(videoList);
return pageInfo;
}

//根据vid查询对应的视频数据了
Query query = Query.query(Criteria.where("vid").in(vids));
List<Video> videoList = this.mongoTemplate.find(query, Video.class);
pageInfo.setRecords(videoList);

return pageInfo;
}

9.1.3、测试用例

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
package com.tanhua.dubbo.server.api;

import com.tanhua.dubbo.server.pojo.Video;
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 TestVideoApi {

@Autowired
private VideoApi videoApi;

@Test
public void testQueryVideoList() {
//返回的推荐结果数据
System.out.println(this.videoApi.queryVideoList(1L, 1, 8));
//返回少于pageSize数据,因为推荐数据不够了
System.out.println(this.videoApi.queryVideoList(1L, 3, 8));

//返回系统数据
System.out.println(this.videoApi.queryVideoList(1L, 4, 8));

}

}

9.2、APP接口服务

服务地址:https://mock-java.itheima.net/project/35/interface/api/815

9.2.1、定义VideoVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class VideoVo {

private String id;
private Long userId;
private String avatar; //头像
private String nickname; //昵称
private String cover; //封面
private String videoUrl; //视频URL
private String signature; //签名
private Integer likeCount; //点赞数量
private Integer hasLiked; //是否已赞(1是,0否)
private Integer hasFocus; //是是否关注 (1是,0否)
private Integer commentCount; //评论数量
}

9.2.2、VideoController

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
@RestController
@RequestMapping("smallVideos")
public class VideoController {
/**
* 查询小视频列表
*
* @param page
* @param pageSize
* @return
*/
@GetMapping
public ResponseEntity<PageResult> queryVideoList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
if (page <= 0) {
page = 1;
}
PageResult pageResult = this.videoService.queryVideoList(page, pageSize);
if (null != pageResult) {
return ResponseEntity.ok(pageResult);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

9.2.3、VideoService

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
//com.tanhua.server.service.VideoService

public PageResult queryVideoList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

PageInfo<Video> pageInfo = this.videoApi.queryVideoList(user.getId(), page, pageSize);
List<Video> records = pageInfo.getRecords();

if(CollUtil.isEmpty(records)){
return pageResult;
}

//查询用户信息
List<Object> userIds = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<VideoVo> videoVoList = new ArrayList<>();
for (Video record : records) {
VideoVo videoVo = new VideoVo();

videoVo.setUserId(record.getUserId());
videoVo.setCover(record.getPicUrl());
videoVo.setVideoUrl(record.getVideoUrl());
videoVo.setId(record.getId().toHexString());
videoVo.setSignature("我就是我~"); //TODO 签名

videoVo.setCommentCount(0); //TODO 评论数
videoVo.setHasFocus(0); //TODO 是否关注
videoVo.setHasLiked(0); //TODO 是否点赞(1是,0否)
videoVo.setLikeCount(0);//TODO 点赞数

//填充用户信息
for (UserInfo userInfo : userInfoList) {
if (ObjectUtil.equals(videoVo.getUserId(), userInfo.getUserId())) {
videoVo.setNickname(userInfo.getNickName());
videoVo.setAvatar(userInfo.getLogo());
break;
}
}

videoVoList.add(videoVo);
}

pageResult.setItems(videoVoList);
return pageResult;
}

9.2.5、测试

image-20201222234918988

image-20201222234936528

可以看到已经查询到数据。下面使用手机进行测试:

image-20220829215404376

小视频和通讯

课程说明

  • 实现视频点赞、评论、关注功能
  • 了解什么是即时通信
  • 了解探花交友的消息功能
  • 了解即时通信的技术方案
  • 了解环信的即时通讯
  • 实现环信的用户体系集成
  • 实现添加联系人、联系人列表功能

1、视频点赞

点赞逻辑与圈子点赞逻辑一致,所以可以复用圈子点赞的逻辑,需要注意的是点赞对象是Video,设置publishUserId的逻辑也需要完善下。

1.1、dubbo服务

修改保存Comment逻辑,在原有逻辑中增加对小视频的支持:

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
//com.tanhua.dubbo.server.api.QuanZiApiImpl

/**
* 保存Comment
*
* @return
*/
private Boolean saveComment(Long userId, String publishId,
CommentType commentType, String content) {
try {
Comment comment = new Comment();
comment.setId(ObjectId.get());
comment.setUserId(userId);
comment.setPublishId(new ObjectId(publishId));
// 评论类型
comment.setCommentType(commentType.getType());
// 内容
comment.setContent(content);
comment.setCreated(System.currentTimeMillis());

Publish publish = this.queryPublishById(publishId);
if (ObjectUtil.isNotEmpty(publish)) {
comment.setPublishUserId(publish.getUserId());
} else {
//查询评论
Comment myComment = this.queryCommentById(publishId);
if(ObjectUtil.isNotEmpty(myComment)){
comment.setPublishUserId(myComment.getUserId());
}else{
//查询小视频
Video video = this.videoApi.queryVideoById(publishId);
if(ObjectUtil.isNotEmpty(video)){
comment.setPublishUserId(video.getUserId());
}else{
// 其他情况,直接返回
return false;
}
}
}

this.mongoTemplate.save(comment);

return true;
} catch (Exception e) {
log.error("保存Comment出错~ userId = " + userId + ", publishId = " + publishId + ", commentType = " + commentType, e);
}

return false;
}

在VideoApi中定义根据id查询Video的方法:

1
2
3
4
5
6
7
8
9
// com.tanhua.dubbo.server.api.VideoApi

/**
* 根据id查询视频对象
*
* @param videoId 小视频id
* @return
*/
Video queryVideoById(String videoId);

编写实现:

1
2
3
4
5
6
// com.tanhua.dubbo.server.api.VideoApiImpl

@Override
public Video queryVideoById(String videoId) {
return this.mongoTemplate.findById(new ObjectId(videoId), Video.class);
}

1.2、APP接口服务

接口地址:

1.2.1、VideoController

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
/**
* 视频点赞
*
* @param videoId 视频id
* @return
*/
@PostMapping("/{id}/like")
public ResponseEntity<Long> likeComment(@PathVariable("id") String videoId) {
try {
Long likeCount = this.videoService.likeComment(videoId);
if (likeCount != null) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 取消点赞
*
* @param videoId
* @return
*/
@PostMapping("/{id}/dislike")
public ResponseEntity<Long> disLikeComment(@PathVariable("id") String videoId) {
try {
Long likeCount = this.videoService.disLikeComment(videoId);
if (null != likeCount) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

1.2.2、VideoService

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
//com.tanhua.server.service.VideoService

/**
* 点赞
*
* @param videoId
* @return
*/
public Long likeComment(String videoId) {
User user = UserThreadLocal.get();
Boolean result = this.quanZiApi.likeComment(user.getId(), videoId);
if (result) {
return this.quanZiApi.queryLikeCount(videoId);
}
return null;
}

/**
* 取消点赞
*
* @param videoId
* @return
*/
public Long disLikeComment(String videoId) {
User user = UserThreadLocal.get();
Boolean result = this.quanZiApi.disLikeComment(user.getId(), videoId);
if (result) {
return this.quanZiApi.queryLikeCount(videoId);
}
return null;
}

1.2.3、修改点赞数查询

在查询小视频列表中,需要完善之前TODO的部分。

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
//com.tanhua.server.service.VideoService

public PageResult queryVideoList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

PageInfo<Video> pageInfo = this.videoApi.queryVideoList(user.getId(), page, pageSize);
List<Video> records = pageInfo.getRecords();

if (CollUtil.isEmpty(records)) {
return pageResult;
}

//查询用户信息
List<Object> userIds = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<VideoVo> videoVoList = new ArrayList<>();
for (Video record : records) {
VideoVo videoVo = new VideoVo();

videoVo.setUserId(record.getUserId());
videoVo.setCover(record.getPicUrl());
videoVo.setVideoUrl(record.getVideoUrl());
videoVo.setId(record.getId().toHexString());
videoVo.setSignature("我就是我~"); //TODO 签名

videoVo.setCommentCount(0); //TODO 评论数
videoVo.setHasFocus(0); //TODO 是否关注
videoVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), videoVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
videoVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(videoVo.getId())));//点赞数

//填充用户信息
for (UserInfo userInfo : userInfoList) {
if (ObjectUtil.equals(videoVo.getUserId(), userInfo.getUserId())) {
videoVo.setNickname(userInfo.getNickName());
videoVo.setAvatar(userInfo.getLogo());
break;
}
}

videoVoList.add(videoVo);
}

pageResult.setItems(videoVoList);
return pageResult;
}

2、视频评论

小视频的评论与圈子的评论逻辑类似,所以也可以使用同一套逻辑,所以只需要开发APP接口功能即可。

评论相关的接口定义:

2.1、VideoController

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
//com.tanhua.server.controller.VideoController

/**
* 评论列表
*/
@GetMapping("/{id}/comments")
public ResponseEntity<PageResult> queryCommentsList(@PathVariable("id") String videoId,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PageResult pageResult = this.videoService.queryCommentList(videoId, page, pageSize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 提交评论
*
* @param param
* @param videoId
* @return
*/
@PostMapping("/{id}/comments")
public ResponseEntity<Void> saveComments(@RequestBody Map<String, String> param,
@PathVariable("id") String videoId) {
try {
String content = param.get("comment");
Boolean result = this.videoService.saveComment(videoId, content);
if (result) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 评论点赞
*
* @param videoCommentId 视频中的评论id
* @return
*/
@PostMapping("/comments/{id}/like")
public ResponseEntity<Long> commentsLikeComment(@PathVariable("id") String videoCommentId) {
try {
Long likeCount = this.videoService.likeComment(videoCommentId);
if (likeCount != null) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 评论取消点赞
*
* @param videoCommentId 视频中的评论id
* @return
*/
@PostMapping("/comments/{id}/dislike")
public ResponseEntity<Long> disCommentsLikeComment(@PathVariable("id") String videoCommentId) {
try {
Long likeCount = this.videoService.disLikeComment(videoCommentId);
if (null != likeCount) {
return ResponseEntity.ok(likeCount);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.2、VideoService

1
2
3
4
5
6
7
8
9
// com.tanhua.server.service.VideoService

public PageResult queryCommentList(String videoId, Integer page, Integer pageSize) {
return this.quanZiService.queryCommentList(videoId, page, pageSize);
}

public Boolean saveComment(String videoId, String content) {
return this.quanZiService.saveComments(videoId, content);
}

2.3、查询评论数

在小视频列表查询结果中,需要返回该视频的评论数据,由于之前在dubbo服务中没有提供查询方法,所以需要先实现查询方法。

2.3.1、dubbo服务

2.3.1.1、定义接口
1
2
3
4
5
6
7
8
9
//com.tanhua.dubbo.server.api.QuanZiApi

/**
* 查询评论数
*
* @param publishId
* @return
*/
Long queryCommentCount(String publishId);
2.3.1.2、编写实现
1
2
3
4
5
6
// com.tanhua.dubbo.server.api.QuanZiApiImpl

@Override
public Long queryCommentCount(String publishId) {
return this.queryCommentCount(publishId, CommentType.COMMENT);
}

2.3.2、查询评论数

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
//com.tanhua.server.service.VideoService

public PageResult queryVideoList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

PageInfo<Video> pageInfo = this.videoApi.queryVideoList(user.getId(), page, pageSize);
List<Video> records = pageInfo.getRecords();

if (CollUtil.isEmpty(records)) {
return pageResult;
}

//查询用户信息
List<Object> userIds = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<VideoVo> videoVoList = new ArrayList<>();
for (Video record : records) {
VideoVo videoVo = new VideoVo();

videoVo.setUserId(record.getUserId());
videoVo.setCover(record.getPicUrl());
videoVo.setVideoUrl(record.getVideoUrl());
videoVo.setId(record.getId().toHexString());
videoVo.setSignature("我就是我~"); //TODO 签名

videoVo.setCommentCount(Convert.toInt(this.quanZiApi.queryCommentCount(videoVo.getId()))); //评论数
videoVo.setHasFocus(0); //TODO 是否关注
videoVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), videoVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
videoVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(videoVo.getId())));//点赞数

//填充用户信息
for (UserInfo userInfo : userInfoList) {
if (ObjectUtil.equals(videoVo.getUserId(), userInfo.getUserId())) {
videoVo.setNickname(userInfo.getNickName());
videoVo.setAvatar(userInfo.getLogo());
break;
}
}

videoVoList.add(videoVo);
}

pageResult.setItems(videoVoList);
return pageResult;
}

2.4、测试

image-20201228152357376

3、关注用户

关注用户是关注小视频发布的作者,需要在dubbo服务中提供给相关的服务。

3.1、dubbo服务

3.1.1、FollowUser

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
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 = "follow_user")
public class FollowUser implements java.io.Serializable{

private static final long serialVersionUID = 3148619072405056052L;

private ObjectId id; //主键id
private Long userId; //用户id
private Long followUserId; //关注的用户id
private Long created; //关注时间
}

3.1.2、定义接口

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
//com.tanhua.dubbo.server.api.VideoApi

/**
* 关注用户
*
* @param userId 当前用户
* @param followUserId 关注的目标用户
* @return
*/
Boolean followUser(Long userId, Long followUserId);

/**
* 取消关注用户
*
* @param userId 当前用户
* @param followUserId 关注的目标用户
* @return
*/
Boolean disFollowUser(Long userId, Long followUserId);

/**
* 查询用户是否关注某个用户
*
* @param userId 当前用户
* @param followUserId 关注的目标用户
* @return
*/
Boolean isFollowUser(Long userId, Long followUserId);

3.1.3、编写实现

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
//com.tanhua.dubbo.server.api.VideoApiImpl

private static final String VIDEO_FOLLOW_USER_KEY_PREFIX = "VIDEO_FOLLOW_USER_";

@Override
public Boolean followUser(Long userId, Long followUserId) {
if (!ObjectUtil.isAllNotEmpty(userId, followUserId)) {
return false;
}

try {
//需要将用户的关注列表,保存到redis中,方便后续的查询
//使用redis的hash结构
if (this.isFollowUser(userId, followUserId)) {
return false;
}

FollowUser followUser = new FollowUser();
followUser.setId(ObjectId.get());
followUser.setUserId(userId);
followUser.setFollowUserId(followUserId);
followUser.setCreated(System.currentTimeMillis());

this.mongoTemplate.save(followUser);

//保存数据到redis
String redisKey = this.getVideoFollowUserKey(userId);
String hashKey = String.valueOf(followUserId);
this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");

return true;
} catch (Exception e) {
e.printStackTrace();
}

return false;
}

@Override
public Boolean disFollowUser(Long userId, Long followUserId) {
if (!ObjectUtil.isAllNotEmpty(userId, followUserId)) {
return false;
}

if (!this.isFollowUser(userId, followUserId)) {
return false;
}

//取消关注,删除关注数据即可
Query query = Query.query(Criteria.where("userId").is(userId)
.and("followUserId").is(followUserId)
);
DeleteResult result = this.mongoTemplate.remove(query, FollowUser.class);
if (result.getDeletedCount() > 0) {
//同时删除redis中的数据
String redisKey = this.getVideoFollowUserKey(userId);
String hashKey = String.valueOf(followUserId);
this.redisTemplate.opsForHash().delete(redisKey, hashKey);

return true;
}

return false;
}

@Override
public Boolean isFollowUser(Long userId, Long followUserId) {
String redisKey = this.getVideoFollowUserKey(userId);
String hashKey = String.valueOf(followUserId);
return this.redisTemplate.opsForHash().hasKey(redisKey, hashKey);
}

private String getVideoFollowUserKey(Long userId) {
return VIDEO_FOLLOW_USER_KEY_PREFIX + userId;
}

3.2、APP服务

关注用户:https://mock-java.itheima.net/project/35/interface/api/839

取消关注:https://mock-java.itheima.net/project/35/interface/api/845

3.2.1、VideoController

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
//com.tanhua.server.controller.VideoController

/**
* 视频用户关注
*/
@PostMapping("/{id}/userFocus")
public ResponseEntity<Void> saveUserFocusComments(@PathVariable("id") Long userId) {
try {
Boolean bool = this.videoService.followUser(userId);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 取消视频用户关注
*/
@PostMapping("/{id}/userUnFocus")
public ResponseEntity<Void> saveUserUnFocusComments(@PathVariable("id") Long userId) {
try {
Boolean bool = this.videoService.disFollowUser(userId);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.2.2、VideoService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//com.tanhua.server.service.VideoService

/**
* 关注用户
*
* @param userId
* @return
*/
public Boolean followUser(Long userId) {
User user = UserThreadLocal.get();
return this.videoApi.followUser(user.getId(), userId);
}

/**
* 取消关注
*
* @param userId
* @return
*/
public Boolean disFollowUser(Long userId) {
User user = UserThreadLocal.get();
return this.videoApi.disFollowUser(user.getId(), userId);
}

3.2.3、查询是否关注

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
//com.tanhua.server.service.VideoService

public PageResult queryVideoList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

PageInfo<Video> pageInfo = this.videoApi.queryVideoList(user.getId(), page, pageSize);
List<Video> records = pageInfo.getRecords();

if (CollUtil.isEmpty(records)) {
return pageResult;
}

//查询用户信息
List<Object> userIds = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<VideoVo> videoVoList = new ArrayList<>();
for (Video record : records) {
VideoVo videoVo = new VideoVo();

videoVo.setUserId(record.getUserId());
videoVo.setCover(record.getPicUrl());
videoVo.setVideoUrl(record.getVideoUrl());
videoVo.setId(record.getId().toHexString());
videoVo.setSignature("我就是我~"); //TODO 签名

videoVo.setCommentCount(Convert.toInt(this.quanZiApi.queryCommentCount(videoVo.getId()))); //评论数
videoVo.setHasFocus(this.videoApi.isFollowUser(user.getId(), videoVo.getUserId()) ? 1 : 0); //是否关注
videoVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), videoVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
videoVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(videoVo.getId())));//点赞数

//填充用户信息
for (UserInfo userInfo : userInfoList) {
if (ObjectUtil.equals(videoVo.getUserId(), userInfo.getUserId())) {
videoVo.setNickname(userInfo.getNickName());
videoVo.setAvatar(userInfo.getLogo());
break;
}
}

videoVoList.add(videoVo);
}

pageResult.setItems(videoVoList);
return pageResult;
}

3.3、测试

image-20201228162135491

可以看到,已经完成了关注用户。

4、即时通信

4.1、什么是即时通信?

image-20231004075959279

4.2、功能说明

在探花交友项目中也提供了类似微信的聊天功能,用户可以和好友或陌生人聊天。

如果是陌生人,通过《聊一下》功能进行打招呼,如果对方同意后,就成为了好友,可以进行聊天了。

陌生人之间如果相互喜欢,那么就会成为好友,也就可以聊天了。

在消息界面中也可以查看:点赞、评论、喜欢、公告等消息信息。

image-20231004080004002

image-20231004080008672

image-20231004080013003

5、技术方案

对于高并发的即时通讯实现,还是很有挑战的,所需要考虑的点非常多,除了要实现功能,还要考虑并发、流量、负载、服务器、容灾等等。虽然有难度也并不是高不可攀。

对于现实即时通讯往往有两种方案:

  • 方案一:
    • 自主实现,从设计到架构,再到实现。
    • 技术方面可以采用:Netty + WebSocket + RocketMQ + MongoDB + Redis + ZooKeeper + MySQL
    • image-20231004080018285
  • 方案二:
    • 对接第三方服务完成。
    • 这种方式简单,只需要按照第三方的api进行对接就可以了。
    • 如:环信、网易、容联云通讯等。

如何选择呢?

如果是中大型企业做项目可以选择自主研发,如果是中小型企业研发中小型的项目,选择第二种方案即可。方案一需要有大量的人力、物力的支持,开发周期长,成本高,但可控性强。方案二,成本低,开发周期短,能够快速的集成起来进行功能的开发,只是在可控性方面来说就差了一些。

探花交友项目选择方案二进行实现。

6、环信

官网:https://www.easemob.com/ 稳定健壮,消息必达,亿级并发的即时通讯云

环信平台为黑马学员开设的专用注册地址:https://datayi.cn/w/woVL50vR

image-20231004080023039

image-20231004080027325

6.1、开发简介

文档地址:http://docs-im.easemob.com/

平台架构:

image-20231004080031562

集成:

环信和用户体系的集成主要发生在2个地方,服务器端集成和客户端集成。

探花集成:image-20231004080038404

6.2、环信Console

需要使用环信平台,那么必须要进行注册,登录之后即可创建应用。环信100以内的用户免费使用,100以上就要注册企业版了。

企业版价格:

image-20231004080043556

创建应用:

image-20201228163305986

创建完成:

image-20231004080050650

6.3、整体流程图

image-20201228165200223

说明:

  • 在APP端与后端系统,都需要完成与环信的集成。
  • 在APP端,使用Android的SDK与环信进行通信,通信时需要通过后台系统的接口查询当前用户的环信用户名和密码,进行登录环信。
  • 后台系统,在用户注册后,同步注册环信用户到环信平台,在后台系统中保存环信的用户名和密码。
  • APP拿到用户名和密码后,进行登录环信,登录成功后即可向环信发送消息给好友。
  • 后台系统也可以通过管理员的身份给用户发送系统信息。

7、获取管理员权限

环信提供的 REST API 需要权限才能访问,权限通过发送 HTTP 请求时携带 token 来体现。

官方文档:获取管理员权限

与环信的集成,我们将相关的代码逻辑写入到新的dubbo工程中,名字叫:my-tanhua-dubbo-huanxin。

7.1、创建dubbo工程

pom.xml:

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
<?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-dubbo</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>my-tanhua-dubbo-huanxin</artifactId>
<dependencies>
<!--引入interface依赖-->
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>

<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>

</project>

application.properties:

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
# Spring boot application
spring.application.name = itcast-tanhua-dubbo-huanxin

# dubbo 扫描包配置
dubbo.scan.basePackages = com.tanhua.dubbo.server
dubbo.application.name = dubbo-provider-huanxin

#dubbo 对外暴露的端口信息
dubbo.protocol.name = dubbo
dubbo.protocol.port = 20881

#dubbo注册中心的配置
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000

# Redis相关配置
spring.redis.jedis.pool.max-wait = 5000ms
spring.redis.jedis.pool.max-Idle = 100
spring.redis.jedis.pool.min-Idle = 10
spring.redis.timeout = 10s
spring.redis.cluster.nodes = 192.168.31.81:6379,192.168.31.81:6380,192.168.31.81:6381
spring.redis.cluster.max-redirects=5

#数据库连接信息
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://192.168.31.81:3306/mytanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false
spring.datasource.username=root
spring.datasource.password=root

# 表名前缀
mybatis-plus.global-config.db-config.table-prefix=tb_
# id策略为自增长
mybatis-plus.global-config.db-config.id-type=auto

入口启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.tanhua.dubbo.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;

@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) //排除mongo的自动配置
public class HuanXinDubboApplication {

public static void main(String[] args) {
SpringApplication.run(HuanXinDubboApplication.class, args);
}
}

7.2、配置

相关的配置,在环信管理控制台中,可以找到相关的参数。

1
2
3
4
5
6
7
#huanxin.properties

tanhua.huanxin.url=http://a1.easemob.com/
tanhua.huanxin.orgName=1105190515097562
tanhua.huanxin.appName=tanhua
tanhua.huanxin.clientId=YXA67ZofwHblEems-_Fh-17T2g
tanhua.huanxin.clientSecret=YXA60r45rNy2Ux5wQ7YYoEPwynHmUZk

编写配置类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tanhua.dubbo.server.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;

@Configuration
@PropertySource("classpath:huanxin.properties")
@ConfigurationProperties(prefix = "tanhua.huanxin")
@Data
public class HuanXinConfig {

private String url;
private String orgName;
private String appName;
private String clientId;
private String clientSecret;

}

7.3、编写实现

具体的获取token的业务逻辑在TokenService中完成。实现要点:

  • 分析官方文档中的请求url、参数、响应数据等内容
  • 请求到token需要缓存到redis中,不能频繁的获取token操作,可能会被封号
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
package com.tanhua.dubbo.server.service;

import cn.hutool.core.util.StrUtil;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.tanhua.dubbo.server.config.HuanXinConfig;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
@Slf4j
public class TokenService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

private static final String REDIS_KEY = "HX_TOKEN";

@Autowired
private HuanXinConfig huanXinConfig;

/**
* 获取token,先从redis中获取,如果没有,再去环信接口获取
*
* @return
*/
public String getToken() {
String token = this.redisTemplate.opsForValue().get(REDIS_KEY);
if (StrUtil.isNotEmpty(token)) {
return token;
}

//访问环信接口获取token
return this.refreshToken();
}

/**
* 刷新token,请求环信接口,将token存储到redis中
*
* @return
*/
public String refreshToken() {
String targetUrl = this.huanXinConfig.getUrl() +
this.huanXinConfig.getOrgName() + "/" +
this.huanXinConfig.getAppName() + "/token";

Map<String, Object> param = new HashMap<>();
param.put("grant_type", "client_credentials");
param.put("client_id", this.huanXinConfig.getClientId());
param.put("client_secret", this.huanXinConfig.getClientSecret());

HttpResponse response = HttpRequest.post(targetUrl)
.body(JSONUtil.toJsonStr(param))
.timeout(20000) //请求超时时间
.execute();

if (!response.isOk()) {
log.error("刷新token失败~~~ ");
return null;
}

String jsonBody = response.body();
JSONObject jsonObject = JSONUtil.parseObj(jsonBody);
String token = jsonObject.getStr("access_token");
if (StrUtil.isNotEmpty(token)) {
//将token数据缓存到redis中,缓存时间由expires_in决定
//提前1小时失效
long timeout = jsonObject.getLong("expires_in") - 3600;
this.redisTemplate.opsForValue().set(REDIS_KEY, token, timeout, TimeUnit.SECONDS);

return token;
}

return null;
}
}

7.4、定义接口

接口定义在my-tanhua-dubbo-interface工程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.dubbo.server.api;

/**
* 与环信平台集成的相关操作
*/
public interface HuanXinApi {

/**
* 获取环信token(获取管理员权限)
* 参见:http://docs-im.easemob.com/im/server/ready/user#%E8%8E%B7%E5%8F%96%E7%AE%A1%E7%90%86%E5%91%98%E6%9D%83%E9%99%90
*
* @return
*/
String getToken();


}

7.5、实现接口

在my-tanhua-dubbo-huanxin中完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.tanhua.dubbo.server.api;

@Service(version = "1.0.0")
@Slf4j
public class HuanXinApiImpl implements HuanXinApi {

@Autowired
private TokenService tokenService;

@Override
public String getToken() {
return this.tokenService.getToken();
}
}

7.6、测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.tanhua.dubbo.server;

import com.tanhua.dubbo.server.api.HuanXinApi;
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;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestHuanXinApi {

@Autowired
private HuanXinApi huanXinApi;

@Test
public void testGetToken(){
String token = this.huanXinApi.getToken();
System.out.println(token);
}
}

测试结果,已经保存到redis中了:

image-20201229095744752

8、用户系统集成

使用环信平台,最重要的就是集成用户体系,基本的逻辑是这样的:新用户在注册后,同时需要注册环信用户。

流程如下:

image-20201229102452178

流程说明:

  • 用户在登录时在sso系统中进行判断,如果是新用户,在注册完成后,需要调用dubbo中的环信服务进行注册环信用户。
  • dubbo-huanxin服务在注册环信用户时,需要随机生成密码,携带token请求环信的REST API进行用户注册。
  • 注册成功后,需要将环信的用户信息保存到MySQL中。
  • 用户在APP端使用即时通讯功能时,需要通过环信用户信息登录到环信平台,由于数据存储到服务端,所以需要通过dubbo-huanxin进行查询。
  • 在拿到环信账号信息后,登录环信,登录成功后即可与环信平台进行交互。
  • 需要注意的是,APP端与环信平台交互,是不走后端系统的,是直连操作。

官方文档:《用户管理》

8.1、通用请求逻辑

在与环信接口通信时,使用的是环信的REST接口,所以我们需要封装一个通用的请求服务,在与所有环信接口对接时使用。

另外,请求接口时都需要携带token,前面我们已经将token存储到redis中,但是,可能存在这样一种情况,token在我们redis中有效,但是在环信平台已经失效,这样环信平台会给我们响应401状态码。

对于这种情况,我们就需要检测状态码是否为401,如果是401的话,就需要重新刷新token,再重新执行此次请求。

也就是要支持请求的重试。

8.1.1、Spring-Retry

Spring提供了重试的功能,使用非常的简单、优雅。

第一步,导入依赖:

1
2
3
4
5
6
7
8
9
<!--Spring重试模块-->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
</dependency>

第二步,在启动类中添加@EnableRetry注解来激活重试功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.dubbo.server;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;
import org.springframework.retry.annotation.EnableRetry;

@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) //排除mongo的自动配置
@EnableRetry
public class HuanXinDubboApplication {

public static void main(String[] args) {
SpringApplication.run(HuanXinDubboApplication.class, args);
}
}

第三步,在需要支持重试操作的Service方法中添加@Retryable注解,demo如下:

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
//将此类放到test包下测试即可

package com.tanhua.dubbo.server;

import cn.hutool.core.util.RandomUtil;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

@Service
public class RetryService {

@Retryable(value = RuntimeException.class, maxAttempts = 3, backoff = @Backoff(delay = 2000L, multiplier = 2))
public int execute(int max) {
int data = RandomUtil.randomInt(1, 99);
System.out.println("生成:" + data);
if (data < max) {
throw new RuntimeException();
}
return data;
}

@Recover //全部重试失败后执行
public int recover(Exception e) {
System.out.println("全部重试完成。。。。。");
return 88; //返回默认
}

}

@Retryable参数说明:

  • value:抛出指定异常才会重试

  • maxAttempts:最大重试次数,默认3次

  • backoff:重试等待策略,默认使用@Backoff

    • @Backoff 的value默认为1000L,我们设置为2000L;
    • multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为2,则第一次重试为2秒,第二次为4秒,第三次为6秒。

@Recover标注的方法,是在所有的重试都失败的情况下,最后执行该方法,该方法有2个要求:

  • 方法的第一个参数必须是 Throwable 类型,最好与 @Retryable 中的 value一致。
  • 方法的返回值必须与@Retryable的方法返回值一致,否则该方法不能被执行。

测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tanhua.dubbo.server;

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;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestRetryService {

@Autowired
private RetryService retryService;

@Test
public void testRetry() {
System.out.println(this.retryService.execute(90));
}
}

测试结果,会有3次重试机会进行生成随机数,如果3次随机数都小于90,最后返回88。

8.1.2、RequestService

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
package com.tanhua.dubbo.server.service;

import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import com.tanhua.dubbo.server.exception.UnauthorizedException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;

/**
* 环信接口通用请求服务
*/
@Service
@Slf4j
public class RequestService {

@Autowired
private TokenService tokenService;

/**
* 通用的发送请求方法
*
* @param url 请求地址
* @param body 请求参数
* @param method 请求方法
* @return
*/
@Retryable(value = UnauthorizedException.class, maxAttempts = 5, backoff = @Backoff(delay = 2000L, multiplier = 2))
public HttpResponse execute(String url, String body, Method method) {
String token = this.tokenService.getToken();

HttpRequest httpRequest;

switch (method) {
case POST: {
httpRequest = HttpRequest.post(url);
break;
}
case DELETE: {
httpRequest = HttpRequest.delete(url);
break;
}
case PUT: {
httpRequest = HttpRequest.put(url);
break;
}
case GET: {
httpRequest = HttpRequest.get(url);
break;
}
default: {
return null;
}
}

HttpResponse response = httpRequest
.header("Content-Type", "application/json") //设置请求内容类型
.header("Authorization", "Bearer " + token) //设置token
.body(body) // 设置请求数据
.timeout(20000) // 超时时间
.execute(); // 执行请求

if (response.getStatus() == 401) {
//token失效,重新刷新token
this.tokenService.refreshToken();

//抛出异常,需要进行重试
throw new UnauthorizedException(url, body, method);
}

return response;
}

@Recover //全部重试失败后执行
public HttpResponse recover(UnauthorizedException e) {
log.error("获取token失败!url = " + e.getUrl() + ", body = " + e.getBody() + ", method = " + e.getMethod().toString());
//如果重试5次后,依然不能获取到token,说明网络或账号出现了问题,只能返回null了,后续的请求将无法再执行
return null;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tanhua.dubbo.server.exception;

import cn.hutool.http.Method;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@AllArgsConstructor
@NoArgsConstructor
@Data
public class UnauthorizedException extends RuntimeException {

private String url;
private String body;
private Method method;

}

测试用例:

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
package com.tanhua.dubbo.server;

import cn.hutool.http.HttpResponse;
import cn.hutool.http.Method;
import com.tanhua.dubbo.server.config.HuanXinConfig;
import com.tanhua.dubbo.server.service.RequestService;
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;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestRequestService {

@Autowired
private RequestService requestService;

@Autowired
private HuanXinConfig huanXinConfig;

@Test
public void testQueryHuanXinUser() {
String targetUrl = this.huanXinConfig.getUrl()
+ this.huanXinConfig.getOrgName() + "/"
+ this.huanXinConfig.getAppName() + "/users/1";
HttpResponse response = this.requestService.execute(targetUrl, null, Method.GET);
System.out.println(response);

}
}

8.2、注册环信用户

注册环信用户分为2种,开放注册、授权注册,区别在于开发注册不需要token,授权注册需要token。我们使用的授权注册。

官方文档:《注册单个用户(授权)》

说明:环信用户数据需要保存到数据中。

8.2.1、HuanXinUser

在my-tanhua-dubbo-interface工程中创建该类:

需要在此工程中添加MybatisPlus依赖:

1
2
3
4
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
</dependency>
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
package com.tanhua.dubbo.server.pojo;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Date;

/**
* 环信用户对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@TableName("tb_huanxin_user")
public class HuanXinUser implements java.io.Serializable{

private static final long serialVersionUID = -6400630011196593976L;

private Long id; //主键Id

/**
* 环信 ID ;也就是 IM 用户名的唯一登录账号,长度不可超过64个字符长度
*/
private String username;
/**
* 登录密码,长度不可超过64个字符长度
*/
private String password;
/**
* 昵称(可选),在 iOS Apns 推送时会使用的昵称(仅在推送通知栏内显示的昵称),
* 并不是用户个人信息的昵称,环信是不保存用户昵称,头像等个人信息的,
* 需要自己服务器保存并与给自己用户注册的IM用户名绑定,长度不可超过100个字符
*/
private String nickname;

private Long userId; //用户id

private Date created; //创建时间

private Date updated; //更新时间

}

数据库表结构:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `tb_huanxin_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`username` varchar(32) NOT NULL COMMENT '环信用户名',
`password` varchar(32) NOT NULL COMMENT '环信密码',
`nickname` varchar(100) DEFAULT NULL COMMENT '昵称',
`created` datetime DEFAULT NULL COMMENT '创建时间',
`updated` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`),
KEY `username` (`username`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

8.2.2、定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.dubbo.server.api.HuanXinApi

/**
* 注册环信用户
* 参见:http://docs-im.easemob.com/im/server/ready/user#%E6%B3%A8%E5%86%8C%E5%8D%95%E4%B8%AA%E7%94%A8%E6%88%B7_%E5%BC%80%E6%94%BE
*
* @param userId 用户id
* @return
*/
Boolean register(Long userId);

/**
* 根据用户Id询环信账户信息
*
* @param userId
* @return
*/
HuanXinUser queryHuanXinUser(Long userId);

8.2.3、实现接口

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
//com.tanhua.dubbo.server.api.HuanXinApiImpl

@Override
public Boolean register(Long userId) {
String targetUrl = this.huanXinConfig.getUrl()
+ this.huanXinConfig.getOrgName() + "/" +
this.huanXinConfig.getAppName() + "/users";

HuanXinUser huanXinUser = new HuanXinUser();
huanXinUser.setUsername("HX_" + userId); // 用户名
huanXinUser.setPassword(IdUtil.simpleUUID()); //随机生成的密码

HttpResponse response = this.requestService.execute(targetUrl, JSONUtil.toJsonStr(Arrays.asList(huanXinUser)), Method.POST);
if (response.isOk()) {
//将环信的账号信息保存到数据库
huanXinUser.setUserId(userId);
huanXinUser.setCreated(new Date());
huanXinUser.setUpdated(huanXinUser.getCreated());

this.huanXinUserMapper.insert(huanXinUser);

return true;
}

return false;
}

@Override
public HuanXinUser queryHuanXinUser(Long userId) {
QueryWrapper<HuanXinUser> wrapper = new QueryWrapper<>();
wrapper.eq("user_id", userId);
return this.huanXinUserMapper.selectOne(wrapper);
}
1
2
3
4
5
6
7
8
9
10
package com.tanhua.dubbo.server.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.dubbo.server.pojo.HuanXinUser;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface HuanXinUserMapper extends BaseMapper<HuanXinUser> {
}

8.2.4、测试用例

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
package com.tanhua.dubbo.server;

import com.tanhua.dubbo.server.api.HuanXinApi;
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;

@SpringBootTest
@RunWith(SpringRunner.class)
public class TestHuanXinApi {

@Autowired
private HuanXinApi huanXinApi;

@Test
public void testRegister(){
//注册用户id为1的用户到环信
System.out.println(this.huanXinApi.register(1L));
}

@Test
public void testQueryHuanXinUser(){
//根据用户id查询环信用户信息
System.out.println(this.huanXinApi.queryHuanXinUser(1L));
}

}

8.2.5、在sso中注册环信用户

需要在sso系统中使用dubbo服务进行注册环信用户。

第一步,导入依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>

第二步,增加dubbo注册中心配置

application.properties:

1
2
3
4
5
6
#dubbo注册中心配置
dubbo.application.name = itcast-tanhua-server
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000
dubbo.consumer.timeout = 60000

第三步,在UserService中增加相应的逻辑:

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
@Reference(version = "1.0.0")
private HuanXinApi huanXinApi;

/**
* 用户登录
*
* @param phone 手机号
* @param code 验证码
* @return
*/
public String login(String phone, String code) {
String redisKey = "CHECK_CODE_" + phone;
boolean isNew = false;

//校验验证码
String redisData = this.redisTemplate.opsForValue().get(redisKey);
if (!StringUtils.equals(code, redisData)) {
return null; //验证码错误
}

//验证码在校验完成后,需要废弃
this.redisTemplate.delete(redisKey);

QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("mobile", phone);

User user = this.userMapper.selectOne(queryWrapper);

if (null == user) {
//需要注册该用户
user = new User();
user.setMobile(phone);
user.setPassword(DigestUtils.md5Hex("123456"));

//注册新用户
this.userMapper.insert(user);
isNew = true;

//注册环信用户
Boolean result = this.huanXinApi.register(user.getId());
if (!result) {
//注册环信失败,记录日志
log.error("注册环信用户失败~ userId = " + user.getId());
}
}

//生成token
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("id", user.getId());

// 生成token
String token = Jwts.builder()
.setClaims(claims) //payload,存放数据的位置,不能放置敏感数据,如:密码等
.signWith(SignatureAlgorithm.HS256, secret) //设置加密方法和加密盐
.setExpiration(new DateTime().plusHours(12).toDate()) //设置过期时间,12小时后过期
.compact();

try {
//发送用户登录成功的消息
Map<String, Object> msg = new HashMap<>();
msg.put("id", user.getId());
msg.put("date", System.currentTimeMillis());

this.rocketMQTemplate.convertAndSend("tanhua-sso-login", msg);
} catch (MessagingException e) {
log.error("发送消息失败!", e);
}

return token + "|" + isNew;
}

8.2.6、测试

将服务全部跑起来,使用APP进行测试,使用新手机号进行登录测试。

新注册的用户:

image-20201229170351678

所对应的环信用户:

image-20201229170423587

环信平台:

image-20201229170507112

可以看到已经注册到了环信。

8.3、查询环信用户信息

在app中,用户登录后需要根据用户名密码登录环信,由于用户名密码保存在后台,所以需要提供接口进行返回。

mock地址: https://mock-java.itheima.net/project/35/interface/api/563

image-20201229170858474

8.3.1、HuanXinUserVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class HuanXinUserVo {

private String username;
private String password;

}

8.3.2、HuanXinController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.tanhua.server.controller;

import com.tanhua.server.service.HuanXinService;
import com.tanhua.server.vo.HuanXinUserVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("huanxin")
public class HuanXinController {

@Autowired
private HuanXinService huanXinService;

@GetMapping("user")
public HuanXinUserVo queryHuanXinUser(){
return this.huanXinService.queryHuanXinUser();
}
}

8.3.3、HuanXinService

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
package com.tanhua.server.service;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.dubbo.server.api.HuanXinApi;
import com.tanhua.dubbo.server.pojo.HuanXinUser;
import com.tanhua.server.vo.HuanXinUserVo;
import org.springframework.stereotype.Service;

@Service
public class HuanXinService {

@Reference(version = "1.0.0")
private HuanXinApi huanXinApi;

public HuanXinUserVo queryHuanXinUser() {
User user = UserThreadLocal.get();
//通过dubbo服务查询环信用户
HuanXinUser huanXinUser = this.huanXinApi.queryHuanXinUser(user.getId());
if (ObjectUtil.isNotEmpty(huanXinUser)) {
return new HuanXinUserVo(huanXinUser.getUsername(), huanXinUser.getPassword());
}
return null;
}
}

8.3.4、测试

image-20201229202131623

8.4、查询个人信息

在消息模块中,需要实现根据环信用户名查询个人的用户信息。

接口文档:https://mock-java.itheima.net/project/35/interface/api/2921

8.4.1、dubbo服务

8.4.1.1、定义接口
1
2
3
4
5
6
7
8
9
//com.tanhua.dubbo.server.api.HuanXinApi

/**
* 根据环信用户名查询用户信息
*
* @param userName
* @return
*/
HuanXinUser queryUserByUserName(String userName);
8.4.1.2、编写实现
1
2
3
4
5
6
7
8
//com.tanhua.dubbo.server.api.HuanXinApiImpl

@Override
public HuanXinUser queryUserByUserName(String userName) {
QueryWrapper<HuanXinUser> wrapper = new QueryWrapper<>();
wrapper.eq("username", userName);
return this.huanXinUserMapper.selectOne(wrapper);
}

8.4.2、APP接口服务

8.4.2.1、UserInfoVo
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
package com.tanhua.server.vo;

import cn.hutool.core.annotation.Alias;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfoVo {

@Alias("userId")
private Long id; //用户id
@Alias("logo")
private String avatar; //头像
@Alias("nickName")
private String nickname; //昵称
private String birthday; //生日 2019-09-11
private String age; //年龄
private String gender; //性别 man woman
private String city; //城市
@Alias("edu")
private String education; //学历
private String income; //月收入
@Alias("industry")
private String profession; //行业
private Integer marriage; //婚姻状态(0未婚,1已婚)

}

8.4.2.2、IMController
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
//com.tanhua.server.controller.IMController

package com.tanhua.server.controller;

import cn.hutool.core.util.ObjectUtil;
import com.tanhua.server.service.IMService;
import com.tanhua.server.vo.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("messages")
@RestController
@Slf4j
public class IMController {

@Autowired
private IMService imService;

/**
* 根据环信用户名查询用户信息
*
* @param userName 环信用户
* @return
*/
@GetMapping("userinfo")
public ResponseEntity<UserInfoVo> queryUserInfoByUserName(@RequestParam("huanxinId") String userName) {
try {
UserInfoVo userInfoVo = this.imService.queryUserInfoByUserName(userName);
if (ObjectUtil.isNotEmpty(userInfoVo)) {
return ResponseEntity.ok(userInfoVo);
}
} catch (Exception e) {
log.error("根据环信id查询用户信息! userName = " + userName, e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

}

8.4.2.3、IMService
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
//com.tanhua.server.service.IMService

package com.tanhua.server.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.UserInfo;
import com.tanhua.dubbo.server.api.HuanXinApi;
import com.tanhua.dubbo.server.pojo.HuanXinUser;
import com.tanhua.server.vo.UserInfoVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class IMService {

@Reference(version = "1.0.0")
private HuanXinApi huanXinApi;

@Autowired
private UserInfoService userInfoService;

public UserInfoVo queryUserInfoByUserName(String userName) {
//查询环信账户
HuanXinUser huanXinUser = this.huanXinApi.queryUserByUserName(userName);
if (ObjectUtil.isEmpty(huanXinUser)) {
return null;
}

//查询用户信息
UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(huanXinUser.getUserId());
if (ObjectUtil.isEmpty(userInfo)) {
return null;
}

UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class, "marriage");
userInfoVo.setGender(userInfo.getSex().toString().toLowerCase());
userInfoVo.setMarriage(StrUtil.equals("已婚", userInfo.getMarriage()) ? 1 : 0);

return userInfoVo;
}
}

8.4.2.4、测试

image-20201230225943923

8.5、根据用户id查询个人信息

在消息模块与我的模块中,需要根据用户id查询个人信息,其响应的数据结构与上面一致,均为:UserInfoVo对象。

接口地址:https://mock-java.itheima.net/project/35/interface/api/875

8.5.1、MyCenterController

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
package com.tanhua.server.controller;

import cn.hutool.core.util.ObjectUtil;
import com.tanhua.server.service.MyCenterService;
import com.tanhua.server.vo.UserInfoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("users")
@RestController
@Slf4j
public class MyCenterController {

@Autowired
private MyCenterService myCenterService;

/**
* 根据用户id查询用户信息
*
* @param userId 用户id,如果为空,表示查询当前登录人的信息
* @return
*/
@GetMapping
public ResponseEntity<UserInfoVo> queryUserInfoByUserId(@RequestParam(value = "userID", required = false) Long userId) {
try {
UserInfoVo userInfoVo = this.myCenterService.queryUserInfoByUserId(userId);
if (ObjectUtil.isNotEmpty(userInfoVo)) {
return ResponseEntity.ok(userInfoVo);
}
} catch (Exception e) {
log.error("根据用户id查询用户信息出错~ userId = " + userId, e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

8.5.2、MyCenterService

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
package com.tanhua.server.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.tanhua.common.pojo.UserInfo;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.server.vo.UserInfoVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyCenterService {

@Autowired
private UserInfoService userInfoService;

public UserInfoVo queryUserInfoByUserId(Long userId) {
if (ObjectUtil.isEmpty(userId)) {
//如果查询id为null,就表示查询当前用户信息
userId = UserThreadLocal.get().getId();
}
//查询用户信息
UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(userId);
if (ObjectUtil.isEmpty(userInfo)) {
return null;
}

UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class, "marriage");
userInfoVo.setGender(userInfo.getSex().toString().toLowerCase());
userInfoVo.setMarriage(StrUtil.equals("已婚", userInfo.getMarriage()) ? 1 : 0);
return userInfoVo;
}
}

8.5.3、测试

image-20201230225943923

8.6、发送消息给客户端

目前已经完成了用户体系的对接,下面我们进行测试发送消息,场景是这样的:

image-20231004080123067

点击“聊一下”,就会给对方发送一条陌生人信息,这个消息由系统发送完成。

我们暂时通过环信的控制台进行发送: image-20231004080129762

消息内容:

1
{"userId":1,"huanXinId":"HX_1","nickname":"黑马小妹","strangerQuestion":"你喜欢去看蔚蓝的大海还是去爬巍峨的高山?","reply":"我喜欢秋天的落叶,夏天的泉水,冬天的雪地,只要有你一切皆可~"}

image-20231004080135696

image-20231004080141652

image-20231004080147024

可以看到已经接收到了消息。

8.7、将用户数据同步到环信

需要将1~99用户注册到环信,因为我们提供的数据都是这些用户的数据。

1
2
3
4
5
6
7
8
//com.tanhua.dubbo.server.TestHuanXinApi

@Test
public void testRegisterAllUser(){
for (int i = 1; i < 100; i++) {
this.huanXinApi.register(Long.valueOf(i));
}
}

image-20210105151927666

环信:

image-20210105151812294

9、 添加联系人

点击“聊一下”,就会成为联系人(好友)。

实现:

  • 将好友写入到MongoDB中
  • 将好友关系注册到环信

具体的流程如下:

image-20201229222115658

9.1、好友dubbo服务

9.1.1、定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tanhua.dubbo.server.api;

public interface UsersApi {

/**
* 保存好友关系
*
* @param userId 用户id
* @param friendId 好友id
* @return
*/
String saveUsers(Long userId, Long friendId);


/**
* 删除好友数据
*
* @param userId 用户id
* @param friendId 好友id
* @return
*/
Boolean removeUsers(Long userId, Long friendId);
}

9.1.2、编写实现

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
package com.tanhua.dubbo.server.api;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.Users;
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;

@Service(version = "1.0.0")
public class UsersApiImpl implements UsersApi {

@Autowired
private MongoTemplate mongoTemplate;


@Override
public String saveUsers(Long userId, Long friendId) {

if (!ObjectUtil.isAllNotEmpty(userId, friendId)) {
return null;
}

// 检测是否该好友关系是否存在
Query query = Query.query(Criteria
.where("userId").is(userId)
.and("friendId").is(friendId));
long count = this.mongoTemplate.count(query, Users.class);
if (count > 0) {
return null;
}

Users users = new Users();
users.setId(ObjectId.get());
users.setDate(System.currentTimeMillis());
users.setUserId(userId);
users.setFriendId(friendId);

//注册我与好友的关系
this.mongoTemplate.save(users);

//注册好友与我的关系
users.setId(ObjectId.get());
users.setUserId(friendId);
users.setFriendId(userId);
this.mongoTemplate.save(users);

return users.getId().toHexString();
}

@Override
public Boolean removeUsers(Long userId, Long friendId) {

Query query1 = Query.query(Criteria.where("userId").is(userId)
.and("friendId").is(friendId));

//删除我与好友的关系数据
long count1 = this.mongoTemplate.remove(query1, Users.class).getDeletedCount();

Query query2 = Query.query(Criteria.where("userId").is(friendId)
.and("friendId").is(userId));
//删除好友与我的关系数据
long count2 = this.mongoTemplate.remove(query2, Users.class).getDeletedCount();

return count1 > 0 && count2 > 0;
}
}

9.2、环信dubbo服务

9.2.1、定义接口

在my-tanhua-dubbo-interface中定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.dubbo.server.api.HuanXinApi

/**
* 添加好友(双向好友关系)
* 参见:http://docs-im.easemob.com/im/server/ready/user#%E6%B7%BB%E5%8A%A0%E5%A5%BD%E5%8F%8B
*
* @param userId 自己的id
* @param friendId 好友的id
* @return
*/
Boolean addUserFriend(Long userId, Long friendId);

/**
* 删除好友关系(双向删除)
* 参见:http://docs-im.easemob.com/im/server/ready/user#%E7%A7%BB%E9%99%A4%E5%A5%BD%E5%8F%8B
*
* @param userId 自己的id
* @param friendId 好友的id
* @return
*/
Boolean removeUserFriend(Long userId, Long friendId);

9.2.2、编写实现

在my-tanhua-dubbo-huanxin中实现。

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
//com.tanhua.dubbo.server.api.HuanXinApiImpl

@Override
public Boolean addUserFriend(Long userId, Long friendId) {
String targetUrl = this.huanXinConfig.getUrl()
+ this.huanXinConfig.getOrgName() + "/"
+ this.huanXinConfig.getAppName() + "/users/HX_" +
userId + "/contacts/users/HX_" + friendId;
try {
// 404 -> 对方未在环信注册
return this.requestService.execute(targetUrl, null, Method.POST).isOk();
} catch (Exception e) {
e.printStackTrace();
}

// 添加失败
return false;
}

@Override
public Boolean removeUserFriend(Long userId, Long friendId) {
String targetUrl = this.huanXinConfig.getUrl()
+ this.huanXinConfig.getOrgName() + "/"
+ this.huanXinConfig.getAppName() + "/users/HX_" +
userId + "/contacts/users/HX_" + friendId;
try {
// 404 -> 对方未在环信注册
return this.requestService.execute(targetUrl, null, Method.DELETE).isOk();
} catch (Exception e) {
e.printStackTrace();
}

// 添加失败
return false;
}

9.3、APP接口服务

接口地址:https://mock-java.itheima.net/project/35/interface/api/809

在my-tanhua-server中完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.server.controller.IMController

/**
* 添加好友
*
* @param param
* @return
*/
@PostMapping("contacts")
public ResponseEntity<Void> contactUser(@RequestBody Map<String, Object> param) {
try {
Long friendId = Long.valueOf(param.get("userId").toString());
boolean result = this.imService.contactUser(friendId);
if (result) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
log.error("添加联系人失败! param = " + param, e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
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
package com.tanhua.server.service;

import cn.hutool.core.util.StrUtil;
import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.dubbo.server.api.HuanXinApi;
import com.tanhua.dubbo.server.api.UsersApi;
import org.springframework.stereotype.Service;

@Service
public class IMService {

@Reference(version = "1.0.0")
private UsersApi usersApi;

@Reference(version = "1.0.0")
private HuanXinApi huanXinApi;

/**
* 添加好友
*
* @param friendId 好友id
*/
public boolean contactUser(Long friendId) {
User user = UserThreadLocal.get();

String id = this.usersApi.saveUsers(user.getId(), friendId);

if (StrUtil.isNotEmpty(id)) {
//注册好友关系到环信
return this.huanXinApi.addUserFriend(user.getId(), friendId);
}

return false;
}
}

9.4、测试

接口测试:

image-20210105162343933

Monodb数据:

image-20210105162412000

环信平台好友数据:

image-20210105162436620

9.5、重新生成好友关系数据

由于之前的数据并没有完整的双向数据,所以需要重新生成,如下:

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
package com.tanhua.server;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.RandomUtil;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.server.service.IMService;
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 TestIMService {

@Autowired
private IMService imService;

/**
* 构造好友数据,为1~99用户构造10个好友
*/
@Test
public void testUsers() {
for (int i = 1; i <= 99; i++) {
for (int j = 0; j < 10; j++) {
User user = new User();
user.setId(Convert.toLong(i));
UserThreadLocal.set(user);
this.imService.contactUser(this.getFriendId(user.getId()));
}
}
}

private Long getFriendId(Long userId) {
Long friendId = RandomUtil.randomLong(1, 100);
if (friendId.intValue() == userId.intValue()) {
getFriendId(userId);
}
return friendId;
}
}

10、联系人列表

用户在消息模块中,可以查看联系人列表(好友列表)。

接口文档地址:https://mock-java.itheima.net/project/35/interface/api/803

10.1、dubbo服务

10.1.1、定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.dubbo.server.api.UsersApi

/**
* 根据用户id查询全部Users列表
*
* @param userId
* @return
*/
List<Users> queryAllUsersList(Long userId);

/**
* 根据用户id查询Users列表(分页查询)
*
* @param userId
* @return
*/
PageInfo<Users> queryUsersList(Long userId, Integer page, Integer pageSize);

10.1.2、接口实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.dubbo.server.api.UsersApiImpl
@Override
public List<Users> queryAllUsersList(Long userId) {
Query query = Query.query(Criteria.where("userId").is(userId));
return this.mongoTemplate.find(query, Users.class);
}

@Override
public PageInfo<Users> queryUsersList(Long userId, Integer page, Integer pageSize) {
PageRequest pageRequest = PageRequest.of(page - 1, pageSize, Sort.by(Sort.Order.desc("created")));
Query query = Query.query(Criteria.where("userId").is(userId)).with(pageRequest);

List<Users> usersList = this.mongoTemplate.find(query, Users.class);

PageInfo<Users> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);
pageInfo.setRecords(usersList);
return pageInfo;
}

10.2、APP接口服务

10.2.1、UsersVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UsersVo {

private Long id;
private String userId;
private String avatar;
private String nickname;
private String gender;
private Integer age;
private String city;

}

10.2.2、IMController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//com.tanhua.server.controller.IMController

/**
* 查询联系人列表
*
* @param page
* @param pageSize
* @param keyword
* @return
*/
@GetMapping("contacts")
public ResponseEntity<PageResult> queryContactsList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "keyword", required = false) String keyword) {
PageResult pageResult = this.imService.queryContactsList(page, pageSize, keyword);
return ResponseEntity.ok(pageResult);
}

10.2.3、IMService

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
//com.tanhua.server.service.IMService

public PageResult queryContactsList(Integer page, Integer pageSize, String keyword) {
PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

User user = UserThreadLocal.get();

List<Users> usersList;
if (StringUtils.isNotEmpty(keyword)) {
//关键不为空,查询所有的好友,在后面进行关键字过滤
usersList = this.usersApi.queryAllUsersList(user.getId());
} else {
//关键字为空,进行分页查询
PageInfo<Users> usersPageInfo = this.usersApi.queryUsersList(user.getId(), page, pageSize);
usersList = usersPageInfo.getRecords();
}

if (CollUtil.isEmpty(usersList)) {
return pageResult;
}


List<Object> userIds = CollUtil.getFieldValues(usersList, "friendId");

QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.in("user_id", userIds);
if (StringUtils.isNotEmpty(keyword)) {
queryWrapper.like("nick_name", keyword);
}

List<UserInfo> userInfoList = this.userInfoService.queryUserInfoList(queryWrapper);

List<UsersVo> contactsList = new ArrayList<>();
//填充用户信息
for (UserInfo userInfo : userInfoList) {
UsersVo usersVo = new UsersVo();
usersVo.setId(userInfo.getUserId());
usersVo.setAge(userInfo.getAge());
usersVo.setAvatar(userInfo.getLogo());
usersVo.setGender(userInfo.getSex().name().toLowerCase());
usersVo.setNickname(userInfo.getNickName());
//环信用户账号
usersVo.setUserId("HX_" + String.valueOf(userInfo.getUserId()));
usersVo.setCity(StringUtils.substringBefore(userInfo.getCity(), "-"));
contactsList.add(usersVo);
}

pageResult.setItems(contactsList);
return pageResult;
}

10.3、测试

image-20201229234205482

image-20220829215458186

消息个人主页

课程说明

  • 点赞消息列表
  • 喜欢消息列表
  • 评论消息列表
  • 公告列表
  • 个人主页
  • 聊一下功能
  • 谁看过我的功能

1、消息点赞、喜欢、评论列表

在消息模块中的点赞、喜欢、评论列表,是别人对自己发布的内容的操作,其实现基本一致,所以在一起实现。

效果:

image-20201231162248125

点赞列表接口地址:https://mock-java.itheima.net/project/35/interface/api/779

评论列表接口地址:https://mock-java.itheima.net/project/35/interface/api/785

喜欢列表接口地址:https://mock-java.itheima.net/project/35/interface/api/791

1.1、dubbo服务

1.1.1、定义接口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.dubbo.server.api.QuanZiApi
/**
* 查询对我的点赞消息列表
*
* @return
*/
PageInfo<Comment> queryLikeCommentListByUser(Long userId, Integer page, Integer pageSize);

/**
* 查询对我的喜欢消息列表
*
* @return
*/
PageInfo<Comment> queryLoveCommentListByUser(Long userId, Integer page, Integer pageSize);

/**
* 查询对我的评论消息列表
*
* @return
*/
PageInfo<Comment> queryCommentListByUser(Long userId, Integer page, Integer pageSize);

1.1.2、编写实现

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
//com.tanhua.dubbo.server.api.QuanziApiImpl

@Override
public PageInfo<Comment> queryLikeCommentListByUser(Long userId, Integer page, Integer pageSize) {
return this.queryCommentListByUser(userId, CommentType.LIKE, page, pageSize);
}

@Override
public PageInfo<Comment> queryLoveCommentListByUser(Long userId, Integer page, Integer pageSize) {
return this.queryCommentListByUser(userId, CommentType.LOVE, page, pageSize);
}

@Override
public PageInfo<Comment> queryCommentListByUser(Long userId, Integer page, Integer pageSize) {
return this.queryCommentListByUser(userId, CommentType.COMMENT, page, pageSize);
}

private PageInfo<Comment> queryCommentListByUser(Long userId, CommentType commentType, Integer page, Integer pageSize) {
PageRequest pageRequest = PageRequest.of(page - 1, pageSize,
Sort.by(Sort.Order.desc("created")));

Query query = new Query(Criteria
.where("publishUserId").is(userId)
.and("commentType").is(commentType.getType())).with(pageRequest);

List<Comment> commentList = this.mongoTemplate.find(query, Comment.class);

PageInfo<Comment> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);
pageInfo.setRecords(commentList);
return pageInfo;
}

1.2、APP接口服务

1.2.1、MessageCommentVo

根据接口定义vo对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class MessageCommentVo {

private String id;
private String avatar;
private String nickname;
private String createDate; //格式:2019-09-08 10:07

}

1.2.3、IMController

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
//com.tanhua.server.controller.IMController

/**
* 查询消息点赞列表
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("likes")
public ResponseEntity<PageResult> queryLikeCommentList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PageResult pageResult = this.imService.queryLikeCommentList(page, pageSize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
log.error("查询点赞列表失败~ ", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 查询消息评论列表
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("comments")
public ResponseEntity<PageResult> queryUserCommentList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PageResult pageResult = this.imService.queryUserCommentList(page, pageSize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
log.error("查询评论列表失败~ ", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();

}

/**
* 查询消息喜欢列表
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("loves")
public ResponseEntity<PageResult> queryLoveCommentList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PageResult pageResult = this.imService.queryLoveCommentList(page, pageSize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
log.error("查询喜欢列表失败~ ", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

1.2.4、IMService

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
//com.tanhua.server.service.IMService

public PageResult queryLikeCommentList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();
PageInfo<Comment> pageInfo = this.quanZiApi.queryLikeCommentListByUser(user.getId(), page, pageSize);
return this.fillUserCommentList(pageInfo);
}

public PageResult queryLoveCommentList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();
PageInfo<Comment> pageInfo = this.quanZiApi.queryLoveCommentListByUser(user.getId(), page, pageSize);
return this.fillUserCommentList(pageInfo);
}

public PageResult queryUserCommentList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();
PageInfo<Comment> pageInfo = this.quanZiApi.queryCommentListByUser(user.getId(), page, pageSize);
return this.fillUserCommentList(pageInfo);
}

private PageResult fillUserCommentList(PageInfo<Comment> pageInfo){
PageResult pageResult = new PageResult();
pageResult.setPage(pageInfo.getPageNum());
pageResult.setPagesize(pageInfo.getPageSize());

List<Comment> records = pageInfo.getRecords();
if(CollUtil.isEmpty(records)){
//没有查询到数据
return pageResult;
}

List<Object> userIdList = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIdList);

List<MessageCommentVo> messageCommentVoList = new ArrayList<>();
for (Comment comment : records) {
for (UserInfo userInfo : userInfoList) {
if(ObjectUtil.equals(comment.getUserId(), userInfo.getUserId())){

MessageCommentVo messageCommentVo = new MessageCommentVo();
messageCommentVo.setId(comment.getId().toHexString());
messageCommentVo.setAvatar(userInfo.getLogo());
messageCommentVo.setNickname(userInfo.getNickName());
messageCommentVo.setCreateDate(DateUtil.format(new Date(comment.getCreated()), "yyyy-MM-dd HH:mm"));

messageCommentVoList.add(messageCommentVo);
break;
}
}
}

pageResult.setItems(messageCommentVoList);

return pageResult;
}

1.3、测试

image-20201231164025217

2、公告列表

公告是后台系统对所有用户发布的公告消息。

效果:

image-20201231171005416

接口地址:https://mock-java.itheima.net/project/35/interface/api/797

2.1、表结构

1
2
3
4
5
6
7
8
9
CREATE TABLE `tb_announcement` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`title` varchar(200) DEFAULT NULL COMMENT '标题',
`description` text COMMENT '描述',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `created` (`created`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 COMMENT='公告表';
1
2
3
4
5
--插入数据
INSERT INTO `tb_announcement` (`id`, `title`, `description`, `created`, `updated`) VALUES ('1', '探花新版本上线发布啦~,盛夏high趴开始了,赶紧来报名吧!', '探花App2019年7月23日起在苹果商店…,浓情夏日,清爽一聚,探花将吧大家聚…', '2019-10-14 11:06:34', '2019-10-14 11:06:37');
INSERT INTO `tb_announcement` (`id`, `title`, `description`, `created`, `updated`) VALUES ('2', '探花交友的圈子功能正式上线啦~~', '探花交友的圈子功能正式上线啦,欢迎使用~', '2019-10-14 11:09:31', '2019-10-14 11:09:33');
INSERT INTO `tb_announcement` (`id`, `title`, `description`, `created`, `updated`) VALUES ('3', '国庆放假期间,探花交友正常使用~', '国庆放假期间,探花交友正常使用~', '2019-10-14 11:10:01', '2019-10-14 11:10:04');

2.2、pojo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.common.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Announcement extends BasePojo {

private Long id;
private String title;
private String description;

}

2.3、AnnouncementMapper

1
2
3
4
5
6
7
8
package com.tanhua.common.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.Announcement;

public interface AnnouncementMapper extends BaseMapper<Announcement> {
}

2.4、AnnouncementService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tanhua.server.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tanhua.common.mapper.AnnouncementMapper;
import com.tanhua.common.pojo.Announcement;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class AnnouncementService {

@Autowired
private AnnouncementMapper announcementMapper;


public IPage<Announcement> queryList(Integer page, Integer pageSize) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.orderByDesc("created");
return this.announcementMapper.selectPage(new Page<Announcement>(page, pageSize), queryWrapper);
}
}

2.5、定义vo对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class AnnouncementVo {

private String id;
private String title;
private String description;
private String createDate;

}

2.6、IMController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* 查询公告列表
*
* @param page
* @param pageSize
* @return
*/
@GetMapping("announcements")
@NoAuthorization //优化,无需进行token校验
public ResponseEntity<PageResult> queryMessageAnnouncementList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize) {
try {
PageResult pageResult = this.imService.queryMessageAnnouncementList(page, pageSize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
log.error("查询公告列表失败~ ", e);
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.7、IMService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public PageResult queryMessageAnnouncementList(Integer page, Integer pageSize) {
IPage<Announcement> announcementPage = this.announcementService.queryList(page, pageSize);

List<AnnouncementVo> announcementVoList = new ArrayList<>();

for (Announcement record : announcementPage.getRecords()) {
AnnouncementVo announcementVo = new AnnouncementVo();
announcementVo.setId(record.getId().toString());
announcementVo.setTitle(record.getTitle());
announcementVo.setDescription(record.getDescription());
announcementVo.setCreateDate(DateUtil.format(record.getCreated(), "yyyy-MM-dd HH:mm"));

announcementVoList.add(announcementVo);
}

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);
pageResult.setItems(announcementVoList);

return pageResult;
}

2.8、测试

1571025206121

3、个人主页

点击首页的今日佳人或任意推荐人的图片就会进入个人主页页面,效果如下:

image-20210107103455768

在个人主页的页面中,会显示出个人信息、缘分值、个人相册等内容。

3.1、个人信息

3.1.1、dubbo服务

需要在dubbo服务中提供查询缘分值的接口服务。

3.1.1.1、定义接口
1
2
3
4
5
6
7
8
9
10
//com.tanhua.dubbo.server.api.RecommendUserApi

/**
* 查询推荐好友的缘分值
*
* @param userId 好友的id
* @param toUserId 我的id
* @return
*/
Double queryScore(Long userId, Long toUserId);
3.1.1.2、实现接口
1
2
3
4
5
6
7
8
9
10
11
12
//com.tanhua.dubbo.server.api.RecommendUserApiImpl

@Override
public Double queryScore(Long userId, Long toUserId) {
Query query = Query.query(Criteria.where("toUserId").is(toUserId)
.and("userId").is(userId));
RecommendUser recommendUser = this.mongoTemplate.findOne(query, RecommendUser.class);
if (null != recommendUser) {
return recommendUser.getScore();
}
return null;
}

3.1.2、APP接口服务

接口地址:https://mock-java.itheima.net/project/35/interface/api/629

说明:该接口的返回值接口与今日佳人的结构一致,所以可以通用今日佳人的对象。

3.1.2.1、TanHuaController
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
package com.tanhua.server.controller;

import com.tanhua.server.service.TanHuaService;
import com.tanhua.server.vo.TodayBest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("tanhua")
@RestController
public class TanHuaController {

@Autowired
private TanHuaService tanHuaService;

/**
* 查询个人主页的个人信息
*
* @param userId
* @return
*/
@GetMapping("{id}/personalInfo")
public ResponseEntity<TodayBest> queryUserInfo(@PathVariable("id") Long userId) {
try {
TodayBest todayBest = this.tanHuaService.queryUserInfo(userId);
return ResponseEntity.ok(todayBest);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

}

3.1.2.2、TanHuaService
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
package com.tanhua.server.service;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.tanhua.common.pojo.User;
import com.tanhua.common.pojo.UserInfo;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.server.vo.TodayBest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class TanHuaService {

@Autowired
private UserInfoService userInfoService;

@Autowired
private RecommendUserService recommendUserService;

public TodayBest queryUserInfo(Long userId) {

UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(userId);
if(ObjectUtil.isEmpty(userInfo)){
return null;
}

TodayBest todayBest = new TodayBest();
todayBest.setId(userId);
todayBest.setAge(userInfo.getAge());
todayBest.setGender(userInfo.getSex().name().toLowerCase());
todayBest.setNickname(userInfo.getNickName());
todayBest.setTags(Convert.toStrArray(StrUtil.split(userInfo.getTags(),',')));
todayBest.setAvatar(userInfo.getLogo());

//缘分值
User user = UserThreadLocal.get();
todayBest.setFateValue(this.recommendUserService.queryScore(userId, user.getId()).longValue());

return todayBest;
}
}

3.1.2.3、RecommendUserService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//com.tanhua.server.service.RecommendUserService

/**
* 查询推荐好友的缘分值
*
* @param userId
* @param toUserId
* @return
*/
public Double queryScore(Long userId, Long toUserId){
Double score = this.recommendUserApi.queryScore(userId, toUserId);
if(ObjectUtil.isNotEmpty(score)){
return score;
}
//默认值
return 98d;
}

3.1.3、测试

image-20210107114351388

image-20210107114407301

3.2、个人相册

3.2.1、dubbo服务

3.2.1.1、定义接口
1
2
3
4
5
6
7
8
9
10
11
12
//com.tanhua.dubbo.server.api.QuanZiApi

/**
* 查询相册表
*
* @param userId
* @param page
* @param pageSize
* @return
*/
PageInfo<Publish> queryAlbumList(Long userId, Integer page, Integer pageSize);

3.2.1.2、实现接口
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
//com.tanhua.dubbo.server.api.QuanZiApiImpl

@Override
public PageInfo<Publish> queryAlbumList(Long userId, Integer page, Integer pageSize) {

PageInfo<Publish> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);

PageRequest pageRequest = PageRequest.of(page - 1 , pageSize,
Sort.by(Sort.Order.desc("created")));
Query query = new Query().with(pageRequest);

//查询自己的相册表
List<Album> albumList = this.mongoTemplate.find(query, Album.class, "quanzi_album_" + userId);

if(CollUtil.isEmpty(albumList)){
return pageInfo;
}

List<Object> publishIdList = CollUtil.getFieldValues(albumList, "publishId");

Query queryPublish = Query.query(Criteria.where("id").in(publishIdList))
.with(Sort.by(Sort.Order.desc("created")));

List<Publish> publishList = this.mongoTemplate.find(queryPublish, Publish.class);

pageInfo.setRecords(publishList);

return pageInfo;
}

3.2.2、APP接口服务

接口文档地址:https://mock-java.itheima.net/project/35/interface/api/689

3.2.2.1、QuanZiController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//com.tanhua.server.controller.QuanZiController

/**
* 自己的所有动态
*
* @return
*/
@GetMapping("all")
public ResponseEntity<PageResult> queryAlbumList(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "userId") Long userId) {
try {
PageResult pageResult = this.quanZiService.queryAlbumList(userId, page, pageSize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
3.2.2.2、QuanZiService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.server.service.QuanZiService

public PageResult queryAlbumList(Long userId, Integer page, Integer pageSize) {
PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

//查询数据
PageInfo<Publish> pageInfo = this.quanZiApi.queryAlbumList(userId, page, pageSize);
if(CollUtil.isEmpty(pageInfo.getRecords())){
return pageResult;
}

//填充数据
pageResult.setItems(this.fillQuanZiVo(pageInfo.getRecords()));

return pageResult;
}

3.2.3、测试

image-20210107115204410

image-20210107115218775

3.3、整合测试

image-20210107115347195

4、聊一下

在个人主页中,点击聊一下按钮,会弹出回答问题窗口,输入答案后,系统会向对方发送一条陌聊消息,如果对方在陌聊消息中点击聊一下,他们就会成为好友。

用户1在用户2的个人主页中点击“聊一下”,流程如下:

image-20201229222115658

4.1、陌聊问题

点击聊一下时,需要显示出问题,所以需要在mysql中存储用户的问题。

4.1.1、表结构

1
2
3
4
5
6
7
8
9
CREATE TABLE `tb_question` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
`txt` varchar(200) DEFAULT NULL COMMENT '问题内容',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

4.1.2、Question实体对象

在my-tanhua-common工程中完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.common.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Question extends BasePojo {

private Long id;
private Long userId;
//问题内容
private String txt;

}

4.1.3、QuestionMapper

在my-tanhua-common工程中完成。

1
2
3
4
5
6
7
8
package com.tanhua.common.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.Question;

public interface QuestionMapper extends BaseMapper<Question> {

}

4.1.4、QuestionService

在my-tanhua-server工程中完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.tanhua.server.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tanhua.common.mapper.QuestionMapper;
import com.tanhua.common.pojo.Question;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class QuestionService {

@Autowired
private QuestionMapper questionMapper;


public Question queryQuestion(Long userId) {
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("user_id", userId);
return this.questionMapper.selectOne(queryWrapper);
}
}

4.2、APP接口服务

接口地址:https://mock-java.itheima.net/project/35/interface/api/635

4.2.1、TanHuaController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.server.controller.TanHuaController

/**
* 查询陌生人问题
*
* @param userId
* @return
*/
@GetMapping("strangerQuestions")
public ResponseEntity<String> queryQuestion(@RequestParam("userId") Long userId) {
try {
String question = this.tanHuaService.queryQuestion(userId);
return ResponseEntity.ok(question);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

4.2.2、TanHuaService

1
2
3
4
5
6
7
8
9
10
//com.tanhua.server.service.TanHuaService

public String queryQuestion(Long userId) {
Question question = this.questionService.queryQuestion(userId);
if(ObjectUtil.isNotEmpty(question)){
return question.getTxt();
}
//默认的问题
return "你的爱好是什么?";
}

4.2.3、测试

image-20210107163832702

4.3、回复陌生人问题

点击问题窗口中的“聊一下”,需要通过admin权限发送系统消息。

4.3.1、dubbo服务

4.3.1.1、定义接口
1
2
3
4
5
6
7
8
9
10
11
12
13
14
//com.tanhua.dubbo.server.api.HuanXinApi

/**
* 以管理员身份发送消息
* 文档地址:http://docs-im.easemob.com/im/server/basics/messages#%E5%8F%91%E9%80%81%E6%B6%88%E6%81%AF
*
* @param targetUserName 发送目标的用户名
* @param huanXinMessageType 消息类型
* @param msg
* @return
*/
Boolean sendMsgFromAdmin(String targetUserName, HuanXinMessageType huanXinMessageType, String msg);


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tanhua.dubbo.server.enums;

/**
* 消息类型;txt:文本消息,img:图片消息,loc:位置消息,audio:语音消息,video:视频消息,file:文件消息
*/
public enum HuanXinMessageType {

TXT("txt"), IMG("img"), LOC("loc"), AUDIO("audio"), VIDEO("video"), FILE("file");

String type;

HuanXinMessageType(String type) {
this.type = type;
}

public String getType() {
return type;
}
}
4.3.1.2、实现接口
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
//com.tanhua.dubbo.server.api.HuanXinApiImpl

@Override
public Boolean sendMsgFromAdmin(String targetUserName, HuanXinMessageType huanXinMessageType, String msg) {
String targetUrl = this.huanXinConfig.getUrl()
+ this.huanXinConfig.getOrgName() + "/"
+ this.huanXinConfig.getAppName() + "/messages";

try {
//{"target_type": "users","target": ["user2","user3"],"msg": {"type": "txt","msg": "testmessage"},"from": "user1"}
String body = JSONUtil.createObj()
.set("target_type", "users")
.set("target", JSONUtil.createArray().set(targetUserName))
.set("msg", JSONUtil.createObj()
.set("type", huanXinMessageType.getType())
.set("msg", msg)).toString();
//表示消息发送者;无此字段Server会默认设置为“from”:“admin”,有from字段但值为空串(“”)时请求失败
// .set("from", "")

return this.requestService.execute(targetUrl, body, Method.POST).isOk();
} catch (Exception e) {
log.error("发送消息失败~ targetUserName = " + targetUserName+", type = " + huanXinMessageType.getType()+", msg = " + msg, e);
}
return false;
}

4.3.2、APP接口服务

接口文档:https://mock-java.itheima.net/project/35/interface/api/641

4.3.2.1、TanHuaController
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.server.controller.TanHuaController

/**
* 回复陌生人问题
*
* @return
*/
@PostMapping("strangerQuestions")
public ResponseEntity<Void> replyQuestion(@RequestBody Map<String, Object> param) {
try {
Long userId = Long.valueOf(param.get("userId").toString());
String reply = param.get("reply").toString();
Boolean result = this.tanHuaService.replyQuestion(userId, reply);
if (result) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
4.3.2.2、TanHuaService
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.server.service.TanHuaService

@Reference(version = "1.0.0")
private HuanXinApi huanXinApi;

public Boolean replyQuestion(Long userId, String reply) {
User user = UserThreadLocal.get();
UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(user.getId());

//构建消息内容
Map<String, Object> msg = new HashMap<>();
msg.put("userId", user.getId());
msg.put("huanXinId", "HX_" + user.getId());
msg.put("nickname", userInfo.getNickName());
msg.put("strangerQuestion", this.queryQuestion(userId));
msg.put("reply", reply);

//发送环信消息
return this.huanXinApi.sendMsgFromAdmin("HX_" + userId,
HuanXinMessageType.TXT, JSONUtil.toJsonStr(msg));
}

4.3.3、测试

image-20210113100053745

image-20210113100124529

用户heima_37收到陌生人消息:

image-20210113100252153

image-20210113100331415

可以看到好友已经添加完成,可以在通讯录中选择好友进行聊天。

在陌聊消息中的“确认添加”功能,就是前面实现的添加联系人接口。

5、谁看过我

查询别人来访了我的主页的信息,其他用户在浏览我的主页时,需要记录访客数据。访客在一天内每个用户只记录一次。

查询数据时,如果用户查询过列表,就需要记录这次查询数据的时间,下次查询时查询大于等于该时间的数据。

如果,用户没有记录查询时间,就查询最近的5个来访用户。

页面效果如下:

image-20210108121812200

5.1、dubbo服务

5.1.1、实体对象

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
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 = "visitors")
public class Visitors implements java.io.Serializable{

private static final long serialVersionUID = 2811682148052386573L;

private ObjectId id;
private Long userId; //我的id
private Long visitorUserId; //来访用户id
private String from; //来源,如首页、圈子等
private Long date; //来访时间

private Double score; //得分

}

5.1.2、定义接口

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
package com.tanhua.dubbo.server.api;

import com.tanhua.dubbo.server.pojo.Visitors;

import java.util.List;

public interface VisitorsApi {

/**
* 保存访客数据
*
* @param userId 我的id
* @param visitorUserId 访客id
* @param from 来源
* @return
*/
String saveVisitor(Long userId, Long visitorUserId, String from);

/**
* 查询我的访客数据,存在2种情况:
* 1. 我没有看过我的访客数据,返回前5个访客信息
* 2. 之前看过我的访客,从上一次查看的时间点往后查询5个访客数据
*
* @param userId
* @return
*/
List<Visitors> queryMyVisitor(Long userId);

}

5.1.3、编写实现

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
package com.tanhua.dubbo.server.api;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.RecommendUser;
import com.tanhua.dubbo.server.pojo.Visitors;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.PageRequest;
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 org.springframework.data.redis.core.RedisTemplate;

import java.util.List;

@Service(version = "1.0.0")
public class VisitorsApiImpl implements VisitorsApi {

@Autowired
private MongoTemplate mongoTemplate;

private static final String VISITOR_REDIS_KEY = "VISITOR_USER";

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Override
public String saveVisitor(Long userId, Long visitorUserId, String from) {
//校验
if (!ObjectUtil.isAllNotEmpty(userId, visitorUserId, from)) {
return null;
}

//查询访客用户在今天是否已经记录过,如果已经记录过,不再记录
String today = DateUtil.today();
Long minDate = DateUtil.parseDateTime(today + " 00:00:00").getTime();
Long maxDate = DateUtil.parseDateTime(today + " 23:59:59").getTime();

Query query = Query.query(Criteria.where("userId").is(userId)
.and("visitorUserId").is(visitorUserId)
.andOperator(Criteria.where("date").gte(minDate),
Criteria.where("date").lte(maxDate)
)
);
long count = this.mongoTemplate.count(query, Visitors.class);
if (count > 0) {
//今天已经记录过的
return null;
}

Visitors visitors = new Visitors();
visitors.setFrom(from);
visitors.setVisitorUserId(visitorUserId);
visitors.setUserId(userId);
visitors.setDate(System.currentTimeMillis());
visitors.setId(ObjectId.get());

//存储数据
this.mongoTemplate.save(visitors);

return visitors.getId().toHexString();
}


@Override
public List<Visitors> queryMyVisitor(Long userId) {
// 查询前5个访客数据,按照访问时间倒序排序
// 如果用户已经查询过列表,记录查询时间,后续查询需要按照这个时间往后查询

// 上一次查询列表的时间
Long date = Convert.toLong(this.redisTemplate.opsForHash().get(VISITOR_REDIS_KEY, String.valueOf(userId)));

PageRequest pageRequest = PageRequest.of(0, 5, Sort.by(Sort.Order.desc("date")));
Query query = Query.query(Criteria.where("userId").is(userId))
.with(pageRequest);
if (ObjectUtil.isNotEmpty(date)) {
query.addCriteria(Criteria.where("date").gte(date));
}

List<Visitors> visitorsList = this.mongoTemplate.find(query, Visitors.class);
//查询每个来访用户的得分
for (Visitors visitors : visitorsList) {

Query queryScore = Query.query(Criteria.where("toUserId")
.is(userId).and("userId").is(visitors.getVisitorUserId())
);
RecommendUser recommendUser = this.mongoTemplate.findOne(queryScore, RecommendUser.class);
if(ObjectUtil.isNotEmpty(recommendUser)){
visitors.setScore(recommendUser.getScore());
}else {
//默认得分
visitors.setScore(90d);
}
}

return visitorsList;
}
}

5.1.4、单元测试

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
package com.tanhua.dubbo.server.api;

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 TestVisitorsApi {

@Autowired
private VisitorsApi visitorsApi;

@Test
public void testSaveVisitor(){
this.visitorsApi.saveVisitor(1L, 2L, "个人主页");
this.visitorsApi.saveVisitor(1L, 3L, "个人主页");
this.visitorsApi.saveVisitor(1L, 2L, "个人主页");
}

@Test
public void testQueryMyVisitor(){
this.visitorsApi.queryMyVisitor(1L)
.forEach(visitors -> System.out.println(visitors));
}

}

5.2、APP接口服务

文档地址:https://mock-java.itheima.net/project/35/interface/api/743

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class VisitorsVo {

private Long id;
private String avatar;
private String nickname;
private String gender;
private Integer age;
private String[] tags;
private Integer fateValue;

}

5.2.2、QuanZiController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//com.tanhua.server.controller.QuanZiController

/**
* 谁看过我
*
* @return
*/
@GetMapping("visitors")
public ResponseEntity<List<VisitorsVo>> queryVisitorsList(){
try {
List<VisitorsVo> list = this.quanZiService.queryVisitorsList();
return ResponseEntity.ok(list);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

5.2.3、QuanZiService

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
//com.tanhua.server.service.QuanZiService

public List<VisitorsVo> queryVisitorsList() {
User user = UserThreadLocal.get();
List<Visitors> visitorsList = this.visitorsApi.queryMyVisitor(user.getId());
if (CollUtil.isEmpty(visitorsList)) {
return Collections.emptyList();
}

List<Object> userIds = CollUtil.getFieldValues(visitorsList, "visitorUserId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<VisitorsVo> visitorsVoList = new ArrayList<>();

for (Visitors visitor : visitorsList) {
for (UserInfo userInfo : userInfoList) {
if (ObjectUtil.equals(visitor.getVisitorUserId(), userInfo.getUserId())) {

VisitorsVo visitorsVo = new VisitorsVo();
visitorsVo.setAge(userInfo.getAge());
visitorsVo.setAvatar(userInfo.getLogo());
visitorsVo.setGender(userInfo.getSex().name().toLowerCase());
visitorsVo.setId(userInfo.getUserId());
visitorsVo.setNickname(userInfo.getNickName());
visitorsVo.setTags(StringUtils.split(userInfo.getTags(), ','));
visitorsVo.setFateValue(visitor.getScore().intValue());

visitorsVoList.add(visitorsVo);
break;
}
}
}

return visitorsVoList;
}

5.3、记录访客数据

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
//com.tanhua.server.service.TanHuaService

public TodayBest queryUserInfo(Long userId) {

UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(userId);
if(ObjectUtil.isEmpty(userInfo)){
return null;
}

TodayBest todayBest = new TodayBest();
todayBest.setId(userId);
todayBest.setAge(userInfo.getAge());
todayBest.setGender(userInfo.getSex().name().toLowerCase());
todayBest.setNickname(userInfo.getNickName());
todayBest.setTags(Convert.toStrArray(StrUtil.split(userInfo.getTags(),',')));
todayBest.setAvatar(userInfo.getLogo());

//缘分值
User user = UserThreadLocal.get();
todayBest.setFateValue(this.recommendUserService.queryScore(userId, user.getId()).longValue());

//记录来访用户
this.visitorsApi.saveVisitor(userId, user.getId(), "个人主页");

return todayBest;
}

5.4、测试

image-20220829215553135

搜附近和探花

课程说明

  • 上报地位位置
  • 实现搜附近功能
  • 实现探花功能
  • 用户基本信息维护

1、上报地理位置

当客户端检测用户的地理位置,当变化大于500米时或每隔5分钟,向服务端上报地理位置。

用户的地理位置存储到Elasticsearch中,需要使用环境提供的ES集群,如下:

image-20210108104814073

1.1、dubbo服务

用户地理位置的服务独立一个新的工程来实现,名字为:my-tanhua-dubbo-es。

1.1.1、创建工程

pom.ml文件如下:

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
<?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-dubbo</artifactId>
<groupId>cn.itcast.tanhua</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>my-tanhua-dubbo-es</artifactId>

<dependencies>
<!--引入interface依赖-->
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<!--dubbo的springboot支持-->
<dependency>
<groupId>com.alibaba.boot</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
</dependency>
<!--dubbo框架-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
</dependency>
<!--zk依赖-->
<dependency>
<groupId>org.apache.zookeeper</groupId>
<artifactId>zookeeper</artifactId>
</dependency>
<dependency>
<groupId>com.github.sgroschupf</groupId>
<artifactId>zkclient</artifactId>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>

application.properties文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Spring boot application
spring.application.name = itcast-tanhua-dubbo-es

# dubbo 扫描包配置
dubbo.scan.basePackages = com.tanhua.dubbo.es
dubbo.application.name = dubbo-provider-es

#dubbo 对外暴露的端口信息
dubbo.protocol.name = dubbo
dubbo.protocol.port = 20882

#dubbo注册中心的配置
dubbo.registry.address = zookeeper://192.168.31.81:2181
dubbo.registry.client = zkclient
dubbo.registry.timeout = 60000

#ES集群配置
spring.data.elasticsearch.cluster-name=es-tanhua-cluster
spring.data.elasticsearch.cluster-nodes=192.168.31.81:9300,192.168.31.81:9301,192.168.31.81:9302

启动类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
package com.tanhua.dubbo.es;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration;
import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration;

@SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) //排除mongo的自动配置
public class ESApplication {

public static void main(String[] args) {
SpringApplication.run(ESApplication.class, args);
}
}

1.1.2、定义pojo

在my-tanhua-dubbo-interface中创建:

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
package com.tanhua.dubbo.server.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.GeoPointField;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(indexName = "tanhua", type = "user_location", shards = 6, replicas = 2)
public class UserLocation {

@Id
private Long userId; //用户id
@GeoPointField
private GeoPoint location; //lon:经度 lat:纬度

@Field(type = FieldType.Keyword)
private String address; //位置描述

@Field(type = FieldType.Long)
private Long created; //创建时间

@Field(type = FieldType.Long)
private Long updated; //更新时间

@Field(type = FieldType.Long)
private Long lastUpdated; //上次更新时间
}

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
package com.tanhua.dubbo.server.vo;

import cn.hutool.core.bean.BeanUtil;
import com.tanhua.dubbo.server.pojo.UserLocation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserLocationVo implements java.io.Serializable {

private static final long serialVersionUID = 4133419501260037769L;

private Long userId; //用户id
private Double longitude; //经度
private Double latitude; //维度
private String address; //位置描述
private Long created; //创建时间
private Long updated; //更新时间
private Long lastUpdated; //上次更新时间

public static final UserLocationVo format(UserLocation userLocation) {
UserLocationVo userLocationVo = BeanUtil.toBean(userLocation, UserLocationVo.class);
userLocationVo.setLongitude(userLocation.getLocation().getLon());
userLocationVo.setLatitude(userLocation.getLocation().getLat());
return userLocationVo;
}

public static final List<UserLocationVo> formatToList(List<UserLocation> userLocations) {
List<UserLocationVo> list = new ArrayList<>();
for (UserLocation userLocation : userLocations) {
list.add(format(userLocation));
}
return list;
}
}

由于UserLocation不能序列化,所以要再定义UserLocationVo进行返回数据。

在my-tanhua-dubbo-interface中添加依赖:

1
2
3
4
5
6
7
8
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>

1.1.3、定义dubbo接口

在my-tanhua-dubbo-interface工程中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.dubbo.server.api;

public interface UserLocationApi {

/**
* 更新用户地理位置
*
* @param userId 用户id
* @param longitude 经度
* @param latitude 纬度
* @param address 地址名称
* @return
*/
Boolean updateUserLocation(Long userId, Double longitude, Double latitude, String address);

}

1.1.4、编写实现

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
package com.tanhua.dubbo.es.api;

import cn.hutool.core.util.ObjectUtil;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.api.UserLocationApi;
import com.tanhua.dubbo.server.pojo.UserLocation;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.common.geo.GeoPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.*;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

@Service(version = "1.0.0")
@Slf4j
public class UserLocationApiImpl implements UserLocationApi {

@Autowired
private ElasticsearchTemplate elasticsearchTemplate;

/**
* 初始化索引库
*
*/
@PostConstruct
public void initIndex(){
//判断索引库是否存在,如果不存在,需要创建
if(!this.elasticsearchTemplate.indexExists("tanhua")){
this.elasticsearchTemplate.createIndex(UserLocation.class);
}

//判断表是否存在,如果不存在,需要创建
if(!this.elasticsearchTemplate.typeExists("tanhua", "user_location")){
this.elasticsearchTemplate.putMapping(UserLocation.class);
}
}

@Override
public Boolean updateUserLocation(Long userId, Double longitude, Double latitude, String address) {
//查询个人的地理位置数据,如果不存在,需要新增,如果是存在数据,更新数据

try {
GetQuery getQuery = new GetQuery();
getQuery.setId(String.valueOf(userId));
UserLocation userLocation = this.elasticsearchTemplate.queryForObject(getQuery, UserLocation.class);
if(ObjectUtil.isEmpty(userLocation)){
//新增数据
userLocation = new UserLocation();
userLocation.setUserId(userId);
userLocation.setAddress(address);
userLocation.setCreated(System.currentTimeMillis());
userLocation.setUpdated(userLocation.getCreated());
userLocation.setLastUpdated(userLocation.getCreated());
userLocation.setLocation(new GeoPoint(latitude, longitude));

IndexQuery indexQuery = new IndexQueryBuilder().withObject(userLocation).build();

//保存数据到ES中
this.elasticsearchTemplate.index(indexQuery);
}else {
//更新数据

//更新的字段
Map<String,Object> map = new HashMap<>();
map.put("location", new GeoPoint(latitude, longitude));
map.put("updated", System.currentTimeMillis());
map.put("lastUpdated", userLocation.getUpdated());
map.put("address", address);

UpdateRequest updateRequest = new UpdateRequest();
updateRequest.doc(map);

UpdateQuery updateQuery = new UpdateQueryBuilder()
.withId(String.valueOf(userId))
.withClass(UserLocation.class)
.withUpdateRequest(updateRequest).build();

//更新数据
this.elasticsearchTemplate.update(updateQuery);
}

return true;
} catch (Exception e) {
log.error("更新地理位置失败~ userId = " + userId + ", longitude = " + longitude + ", latitude = " + latitude + ", address = " + address, e);
}

return false;
}
}

1.1.5、单元测试

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
package com.tanhua.dubbo.es;

import com.tanhua.dubbo.server.api.UserLocationApi;
import com.tanhua.dubbo.server.vo.PageInfo;
import com.tanhua.dubbo.server.vo.UserLocationVo;
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 TestUserLocationApi {

@Autowired
private UserLocationApi userLocationApi;

@Test
public void testUpdateUserLocation() {
this.userLocationApi.updateUserLocation(1L, 121.512253, 31.24094, "金茂大厦");
this.userLocationApi.updateUserLocation(2L, 121.506377, 31.245105, "东方明珠广播电视塔");
this.userLocationApi.updateUserLocation(10L, 121.508815, 31.243844, "陆家嘴地铁站");
this.userLocationApi.updateUserLocation(12L, 121.511999, 31.239185, "上海中心大厦");
this.userLocationApi.updateUserLocation(25L, 121.493444, 31.240513, "上海市公安局");
this.userLocationApi.updateUserLocation(27L, 121.494108, 31.247011, "上海外滩美术馆");
this.userLocationApi.updateUserLocation(30L, 121.462452, 31.253463, "上海火车站");
this.userLocationApi.updateUserLocation(32L, 121.81509, 31.157478, "上海浦东国际机场");
this.userLocationApi.updateUserLocation(34L, 121.327908, 31.20033, "虹桥火车站");
this.userLocationApi.updateUserLocation(38L, 121.490155, 31.277476, "鲁迅公园");
this.userLocationApi.updateUserLocation(40L, 121.425511, 31.227831, "中山公园");
this.userLocationApi.updateUserLocation(43L, 121.594194, 31.207786, "张江高科");
}

}

1.2、APP接口

接口文档:https://mock-java.itheima.net/project/35/interface/api/557

1.2.1、BaiduController

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
package com.tanhua.server.controller;

import com.tanhua.server.service.BaiduService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("baidu")
public class BaiduController {

@Autowired
private BaiduService baiduService;

/**
* 更新位置
*
* @param param
* @return
*/
@PostMapping("location")
public ResponseEntity<Void> updateLocation(@RequestBody Map<String, Object> param) {
try {
Double longitude = Double.valueOf(param.get("longitude").toString());
Double latitude = Double.valueOf(param.get("latitude").toString());
String address = param.get("addrStr").toString();

Boolean bool = this.baiduService.updateLocation(longitude, latitude, address);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}

return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}

1.2.2、BaiduService

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
package com.tanhua.server.service;

import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.dubbo.server.api.UserLocationApi;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@Slf4j
public class BaiduService {

@Reference(version = "1.0.0")
private UserLocationApi userLocationApi;

public Boolean updateLocation(Double longitude, Double latitude, String address) {
User user = UserThreadLocal.get();
try {
return this.userLocationApi.updateUserLocation(user.getId(), longitude, latitude, address);
} catch (Exception e) {
log.error("更新地理位置失败~ userId = " + user.getId() + ", longitude = " + longitude + ", latitude = " + latitude + ", address = " + address, e);
}
return false;
}

}

1.3、测试

image-20210107230457781

2、搜附近

在首页中点击“搜附近”可以搜索附近的好友,效果如下:

1571966080530

实现思路:根据当前用户的位置,查询附近范围内的用户。范围是可以设置的。

2.1、dubbo服务

2.1.1、定义接口方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.dubbo.server.api.UserLocationApi

/**
* 查询用户地理位置
*
* @param userId
* @return
*/
UserLocationVo queryByUserId(Long userId);

/**
* 根据位置搜索
*
* @param longitude 经度
* @param latitude 纬度
* @param distance 距离(米)
* @param page 页数
* @param pageSize 页面大小
*/
PageInfo<UserLocationVo> queryUserFromLocation(Double longitude, Double latitude, Double distance, Integer page, Integer pageSize);

2.1.2、编写实现

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
//com.tanhua.dubbo.es.api.UserLocationApiImpl

/**
* 查询用户的位置信息
*
* @param userId
* @return
*/
@Override
public UserLocationVo queryByUserId(Long userId) {

GetQuery getQuery = new GetQuery();
getQuery.setId(String.valueOf(userId));
UserLocation userLocation = this.elasticsearchTemplate.queryForObject(getQuery, UserLocation.class);
if(ObjectUtil.isNotEmpty(userLocation)){
return UserLocationVo.format(userLocation);
}

return null;
}

/**
* 根据位置搜索
*
* @param longitude 经度
* @param latitude 纬度
* @param distance 距离(米)
* @param page 页数
* @param pageSize 页面大小
*/
@Override
public PageInfo<UserLocationVo> queryUserFromLocation(Double longitude, Double latitude, Double distance, Integer page, Integer pageSize) {
PageInfo<UserLocationVo> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);

String fieldName = "location";

//实现了SearchQuery接口,构造分页、排序
NativeSearchQueryBuilder searchQueryBuilder = new NativeSearchQueryBuilder();

//分页
PageRequest pageRequest = PageRequest.of(page - 1, pageSize);
searchQueryBuilder.withPageable(pageRequest);

BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

//以一个点为中心,指定范围查询
GeoDistanceQueryBuilder geoDistanceQueryBuilder = new GeoDistanceQueryBuilder(fieldName);
//中心点
geoDistanceQueryBuilder.point(new GeoPoint(latitude, longitude));
//距离(画圆的半径)单位:公里
geoDistanceQueryBuilder.distance(distance / 1000, DistanceUnit.KILOMETERS);

boolQueryBuilder.must(geoDistanceQueryBuilder);
searchQueryBuilder.withQuery(boolQueryBuilder);

//排序,由近到远排序
GeoDistanceSortBuilder geoDistanceSortBuilder = new GeoDistanceSortBuilder(fieldName, latitude, longitude);
geoDistanceSortBuilder.order(SortOrder.ASC); //正序排序
geoDistanceSortBuilder.unit(DistanceUnit.KILOMETERS); //设置单位
searchQueryBuilder.withSort(geoDistanceSortBuilder);

AggregatedPage<UserLocation> aggregatedPage = this.elasticsearchTemplate.queryForPage(searchQueryBuilder.build(), UserLocation.class);
if(CollUtil.isEmpty(aggregatedPage.getContent())){
return pageInfo;
}

pageInfo.setRecords(UserLocationVo.formatToList(aggregatedPage.getContent()));

return pageInfo;
}

2.1.3、单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//com.tanhua.dubbo.es.TestUserLocationApi

@Test
public void testQueryByUserId(){
UserLocationVo userLocationVo = this.userLocationApi.queryByUserId(1L);
System.out.println(userLocationVo);
}

@Test
public void testQueryUserFromLocation(){
UserLocationVo userLocationVo = this.userLocationApi.queryByUserId(1L);
PageInfo<UserLocationVo> pageInfo = this.userLocationApi
.queryUserFromLocation(userLocationVo.getLongitude(),
userLocationVo.getLatitude(), 5000d, 1, 10);
pageInfo.getRecords().forEach(vo -> System.out.println(vo));
}

2.2、APP接口服务

文档地址:https://mock-java.itheima.net/project/35/interface/api/611

2.2.1、NearUserVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class NearUserVo {

private Long userId;
private String avatar;
private String nickname;

}

2.2.2、TanHuaController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.server.controller.TanHuaController

/**
* 搜附近
*
* @param gender
* @param distance
* @return
*/
@GetMapping("search")
public ResponseEntity<List<NearUserVo>> queryNearUser(@RequestParam(value = "gender", required = false) String gender,
@RequestParam(value = "distance", defaultValue = "2000") String distance) {
try {
List<NearUserVo> list = this.tanHuaService.queryNearUser(gender, distance);
return ResponseEntity.ok(list);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.2.3、TanHuaService

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
//com.tanhua.server.service.TanHuaService

public List<NearUserVo> queryNearUser(String gender, String distance) {
//查询当前用户的位置
User user = UserThreadLocal.get();
UserLocationVo userLocationVo = this.userLocationApi.queryByUserId(user.getId());
if(ObjectUtil.isEmpty(userLocationVo)){
return ListUtil.empty();
}

PageInfo<UserLocationVo> pageInfo = this.userLocationApi.queryUserFromLocation(userLocationVo.getLongitude(),
userLocationVo.getLatitude(),
Convert.toDouble(distance),
1,
50
);

List<UserLocationVo> records = pageInfo.getRecords();
if(CollUtil.isEmpty(records)){
return ListUtil.empty();
}

//构造筛选条件
List<Object> userIdList = CollUtil.getFieldValues(records, "userId");
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.in("user_id", userIdList);
if(StrUtil.equalsIgnoreCase(gender, "man")){
queryWrapper.eq("sex", SexEnum.MAN);
}else if(StrUtil.equalsIgnoreCase(gender, "woman")){
queryWrapper.eq("sex", SexEnum.WOMAN);
}

List<UserInfo> userInfoList = this.userInfoService.queryUserInfoList(queryWrapper);

List<NearUserVo> result = new ArrayList<>();
for (UserLocationVo locationVo : records) {
//排除自己
if(ObjectUtil.equals(locationVo.getUserId(), user.getId())){
continue;
}

for (UserInfo userInfo : userInfoList) {
if(ObjectUtil.equals(locationVo.getUserId(), userInfo.getUserId())){

NearUserVo nearUserVo = new NearUserVo();
nearUserVo.setUserId(userInfo.getUserId());
nearUserVo.setAvatar(userInfo.getLogo());
nearUserVo.setNickname(userInfo.getNickName());
result.add(nearUserVo);
break;
}
}
}

return result;
}

2.2.4、测试

image-20210114102844696

3、探花

探花功能是将推荐的好友随机的通过卡片的形式展现出来,用户可以选择左滑、右滑操作,左滑:“不喜欢”,右滑:“喜欢”。

喜欢:如果双方喜欢,那么就会成为好友。

1572105631737

如果已经喜欢或不喜欢的用户在列表中不再显示。

3.1、喜欢的dubbo服务

用户的喜欢与不喜欢列表需要保存在redis中,为了防止redis中的数据丢失,同时需要将数据保存到mongodb进行持久化保存。

3.1.1、实体对象

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
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.index.Indexed;
import org.springframework.data.mongodb.core.mapping.Document;

@Data
@NoArgsConstructor
@AllArgsConstructor
@Document(collection = "user_like")
public class UserLike implements java.io.Serializable {

private static final long serialVersionUID = 6739966698394686523L;

private ObjectId id;
@Indexed
private Long userId; //用户id,自己
@Indexed
private Long likeUserId; //喜欢的用户id,对方
private Long created; //创建时间

}

3.1.2、定义接口

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
package com.tanhua.dubbo.server.api;

import java.util.List;

public interface UserLikeApi {

/**
* 喜欢
*
* @param userId
* @param likeUserId
* @return
*/
Boolean likeUser(Long userId, Long likeUserId);

/**
* 不喜欢
*
* @param userId
* @param likeUserId
* @return
*/
Boolean notLikeUser(Long userId, Long likeUserId);


/**
* 是否相互喜欢
*
* @param userId
* @param likeUserId
* @return
*/
Boolean isMutualLike(Long userId, Long likeUserId);


/**
* 查询喜欢列表
*
* @param userId
* @return
*/
List<Long> queryLikeList(Long userId);

/**
* 查询不喜欢列表
*
* @param userId
* @return
*/
List<Long> queryNotLikeList(Long userId);

}

3.1.3、编写实现

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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
package com.tanhua.dubbo.server.api;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.convert.Convert;
import com.alibaba.dubbo.config.annotation.Service;
import com.tanhua.dubbo.server.pojo.UserLike;
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.data.redis.core.RedisTemplate;

import java.util.ArrayList;
import java.util.List;
import java.util.Set;

@Service(version = "1.0.0")
public class UserLikeApiImpl implements UserLikeApi {

@Autowired
private MongoTemplate mongoTemplate;

@Autowired
private RedisTemplate<String,String> redisTemplate;

public static final String LIKE_REDIS_KEY_PREFIX = "USER_LIKE_";

public static final String NOT_LIKE_REDIS_KEY_PREFIX = "USER_NOT_LIKE_";


/**
* 喜欢
*
* @param userId
* @param likeUserId
* @return
*/
@Override
public Boolean likeUser(Long userId, Long likeUserId) {
//判断该用户是否已经喜欢,如果已经喜欢就返回
if(this.isLike(userId, likeUserId)){
return false;
}

UserLike userLike = new UserLike();
userLike.setId(ObjectId.get());
userLike.setUserId(userId);
userLike.setLikeUserId(likeUserId);
userLike.setCreated(System.currentTimeMillis());

//将数据存储到MongoDB
this.mongoTemplate.save(userLike);

//用户的喜欢数据保存到redis
//用户1:key -> USER_LIKE_1 , value -> 2, "1"
//用户1:key -> USER_LIKE_1 , value -> 3, "1"
//用户2:key -> USER_LIKE_2 , value -> 4, "1"
String redisKey = this.getLikeRedisKey(userId);
String hashKey = String.valueOf(likeUserId);
this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");

//判断,喜欢的用户是否在不喜欢的列表中,如果在,就需要删除数据
if(this.isNotLike(userId, likeUserId)){
redisKey = this.getNotLikeRedisKey(userId);
this.redisTemplate.opsForHash().delete(redisKey, hashKey);
}

return true;
}

/**
* 获取喜欢数据的redis key
*
* @param userId
* @return
*/
private String getLikeRedisKey(Long userId){
return LIKE_REDIS_KEY_PREFIX + userId;
}

/**
* 获取不喜欢数据的redis key
*
* @param userId
* @return
*/
private String getNotLikeRedisKey(Long userId){
return NOT_LIKE_REDIS_KEY_PREFIX + userId;
}

/**
* 是否喜欢
*
* @param userId
* @param likeUserId
* @return
*/
private Boolean isLike(Long userId, Long likeUserId){
String redisKey = this.getLikeRedisKey(userId);
String hashKey = String.valueOf(likeUserId);
return this.redisTemplate.opsForHash().hasKey(redisKey, hashKey);
}

/**
* 是否不喜欢
*
* @param userId
* @param likeUserId
* @return
*/
private Boolean isNotLike(Long userId, Long likeUserId){
String redisKey = this.getNotLikeRedisKey(userId);
String hashKey = String.valueOf(likeUserId);
return this.redisTemplate.opsForHash().hasKey(redisKey, hashKey);
}

@Override
public Boolean notLikeUser(Long userId, Long likeUserId) {
//判断用户是否已经不喜欢,如果已经不喜欢,就返回
if(this.isNotLike(userId, likeUserId)){
return false;
}

//将用户保存到不喜欢列表中
String redisKey = this.getNotLikeRedisKey(userId);
String hashKey = String.valueOf(likeUserId);
this.redisTemplate.opsForHash().put(redisKey, hashKey, "1");

//判断用户是否在喜欢列表中,如果存在的话,需要删除数据
if(this.isLike(userId, likeUserId)){
//删除MongoDB数据
Query query = Query.query(Criteria
.where("userId").is(userId)
.and("likeUserId").is(likeUserId)
);
this.mongoTemplate.remove(query, UserLike.class);

//删除redis中的数据
redisKey = this.getLikeRedisKey(userId);
this.redisTemplate.opsForHash().delete(redisKey, hashKey);
}

return true;
}

@Override
public Boolean isMutualLike(Long userId, Long likeUserId) {
return this.isLike(userId, likeUserId)
&& this.isLike(likeUserId, userId);
}

@Override
public List<Long> queryLikeList(Long userId) {
// 查询redis
String redisKey = this.getLikeRedisKey(userId);
Set<Object> keys = this.redisTemplate.opsForHash().keys(redisKey);
if(CollUtil.isEmpty(keys)){
return ListUtil.empty();
}

List<Long> result = new ArrayList<>(keys.size());
keys.forEach(o -> result.add(Convert.toLong(o)));
return result;
}

@Override
public List<Long> queryNotLikeList(Long userId) {
// 查询redis
String redisKey = this.getNotLikeRedisKey(userId);
Set<Object> keys = this.redisTemplate.opsForHash().keys(redisKey);
if(CollUtil.isEmpty(keys)){
return ListUtil.empty();
}

List<Long> result = new ArrayList<>(keys.size());
keys.forEach(o -> result.add(Convert.toLong(o)));
return result;
}
}

3.1.4、单元测试

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
package com.tanhua.dubbo.server.api;

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 TestUserLikeApi {

@Autowired
private UserLikeApi userLikeApi;

@Test
public void testUserLike() {
System.out.println(this.userLikeApi.likeUser(1L, 2L));
System.out.println(this.userLikeApi.likeUser(1L, 3L));
System.out.println(this.userLikeApi.likeUser(1L, 4L));

System.out.println(this.userLikeApi.notLikeUser(1L, 5L));
System.out.println(this.userLikeApi.notLikeUser(1L, 6L));

System.out.println(this.userLikeApi.likeUser(1L, 5L));
System.out.println(this.userLikeApi.notLikeUser(1L, 2L));
}

@Test
public void testQueryList(){
this.userLikeApi.queryLikeList(1L).forEach(a -> System.out.println(a));
System.out.println("-------");
this.userLikeApi.queryNotLikeList(1L).forEach(a -> System.out.println(a));
}
}

3.2、查询推荐列表dubbo服务

3.2.1、定义接口

1
2
3
4
5
6
7
8
9
10
//com.tanhua.dubbo.server.api.RecommendUserApi

/**
* 查询探花列表,查询时需要排除不喜欢列表用户
*
* @param userId
* @param count
* @return
*/
List<RecommendUser> queryCardList(Long userId, Integer count);

3.2.2、编写实现

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
//com.tanhua.dubbo.server.api.RecommendUserApiImpl

@Override
public List<RecommendUser> queryCardList(Long userId, Integer count) {
//设置分页以及排序,按照得分倒序排序
PageRequest pageRequest = PageRequest.of(0, count, Sort.by(Sort.Order.desc("score")));

//排除已喜欢或不喜欢的用户
List<Long> userIds = new ArrayList<>();
//查询喜欢列表
userIds.addAll(this.userLikeApi.queryLikeList(userId));

//查询不喜欢列表
userIds.addAll(this.userLikeApi.queryNotLikeList(userId));

//构造查询条件
Criteria criteria = Criteria.where("toUserId").is(userId);
if(CollUtil.isNotEmpty(userIds)){
//加入到查询条件中,排除这些用户
criteria.andOperator(Criteria.where("userId").nin(userIds));
}

Query query = Query.query(criteria).with(pageRequest);
List<RecommendUser> recommendUserList = this.mongoTemplate.find(query, RecommendUser.class);
return recommendUserList;
}

3.2.3、单元测试

1
2
3
4
5
6
7
//com.tanhua.dubbo.server.api.TestRecommendUserApi

@Test
public void testQueryCardList(){
this.recommendUserApi.queryCardList(2L, 20)
.forEach(recommendUser -> System.out.println(recommendUser));
}

3.3、查询推荐列表APP接口实现

接口文档:https://mock-java.itheima.net/project/35/interface/api/593

3.3.1、TanHuaController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 探花
*
* @return
*/
@GetMapping("cards")
public ResponseEntity<List<TodayBest>> queryCardsList() {
try {
List<TodayBest> list = this.tanHuaService.queryCardsList();
return ResponseEntity.ok(list);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.3.2、TanHuaService

1
2
#默认推荐列表
tanhua.default.recommend.users=2,3,8,10,18,20,24,29,27,32,36,37,56,64,75,88
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
/**
* 查询推荐卡片列表,从推荐列表中随机选取10个用户
*
* @return
*/
public List<TodayBest> queryCardsList() {

User user = UserThreadLocal.get();
int count = 50;

//查询到的50条数据,并不是用来直接展现,需要从这50条数据中随机返回一些数据
List<RecommendUser> recommendUserList = this.recommendUserService.queryCardList(user.getId(), count);
if (CollUtil.isEmpty(recommendUserList)) {
recommendUserList = new ArrayList<>();
//默认推荐列表
List<String> list = StrUtil.split(defaultRecommendUsers, ',');
for (String userId : list) {
RecommendUser recommendUser = new RecommendUser();

recommendUser.setToUserId(user.getId());
recommendUser.setUserId(Convert.toLong(userId));
recommendUserList.add(recommendUser);
}
}

//计算展现的数量,默认展现10个
int showCount = Math.min(10, recommendUserList.size());
List<RecommendUser> result = new ArrayList<>();
for (int i = 0; i < showCount; i++) {
//TODO 可能重复
int index = RandomUtil.randomInt(0, recommendUserList.size());
RecommendUser recommendUser = recommendUserList.get(index);
result.add(recommendUser);
}

List<Object> userIdList = CollUtil.getFieldValues(result, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIdList);
List<TodayBest> todayBests = new ArrayList<>();
for (UserInfo userInfo : userInfoList) {
TodayBest todayBest = new TodayBest();
todayBest.setId(userInfo.getUserId());
todayBest.setAge(userInfo.getAge());
todayBest.setAvatar(userInfo.getLogo());
todayBest.setGender(userInfo.getSex().name().toLowerCase());
todayBest.setNickname(userInfo.getNickName());
todayBest.setTags(Convert.toStrArray(StrUtil.split(userInfo.getTags(), ',')));
todayBest.setFateValue(0L);

todayBests.add(todayBest);
}

return todayBests;
}

3.3.3、测试

image-20210114162852839

image-20210114162907596

效果:

image-20210114163241129

3.4、左滑右滑

左滑:“不喜欢”,右滑:“喜欢”,如果双方喜欢,那么就会成为好友。

喜欢的接口文档:https://mock-java.itheima.net/project/35/interface/api/599

不喜欢的接口文档:https://mock-java.itheima.net/project/35/interface/api/605

3.4.1、TanHuaController

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
/**
* 喜欢
*
* @param likeUserId
* @return
*/
@GetMapping("{id}/love")
public ResponseEntity<Void> likeUser(@PathVariable("id") Long likeUserId) {
try {
this.tanHuaService.likeUser(likeUserId);
return ResponseEntity.ok(null);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

/**
* 不喜欢
*
* @param likeUserId
* @return
*/
@GetMapping("{id}/unlove")
public ResponseEntity<Void> notLikeUser(@PathVariable("id") Long likeUserId) {
try {
this.tanHuaService.notLikeUser(likeUserId);
return ResponseEntity.ok(null);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.4.2、TanHuaService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.server.service.TanHuaService

public Boolean likeUser(Long likeUserId) {
User user = UserThreadLocal.get();
Boolean result = this.userLikeApi.likeUser(user.getId(), likeUserId);
if (!result) {
return false;
}

if (this.userLikeApi.isMutualLike(user.getId(), likeUserId)) {
//相互喜欢成为好友
this.imService.contactUser(likeUserId);
}
return true;
}

public Boolean notLikeUser(Long likeUserId) {
User user = UserThreadLocal.get();
return this.userLikeApi.notLikeUser(user.getId(), likeUserId);
}

3.4.3、测试

image-20210114165501156

image-20210114165651535

user_like表,可以看到已经相互喜欢了:

image-20210114165721204

tanhua_users表,可以看到相互是好友了:

image-20210114170129360

环信平台:

image-20210114170153850

4、用户资料

在我的中心模块中,可以对个人信息做修改。

image-20210114203734594

4.1、基本信息

在前面实现的查询个人信息接口中,已经返回个人基本数据,所以可以直接展现出个人信息,下面只需要进行实现数据的保存即可。

4.4.1、接口信息

接口地址:https://mock-java.itheima.net/project/35/interface/api/887

image-20210114204120298

请求参数:

image-20210114204151930

4.4.2、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.server.controller.MyCenterController

/**
* 更新用户信息
*
* @param userInfoVo
* @return
*/
@PutMapping
public ResponseEntity<Void> updateUserInfo(@RequestBody UserInfoVo userInfoVo){
try {
Boolean bool = this.myCenterService.updateUserInfo(userInfoVo);
if(bool){
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

4.4.3、MyCenterService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//com.tanhua.server.service.MyCenterService

public Boolean updateUserInfo(UserInfoVo userInfoVo) {
User user = UserThreadLocal.get();
UserInfo userInfo = new UserInfo();
userInfo.setUserId(user.getId());
userInfo.setAge(Integer.valueOf(userInfoVo.getAge()));
userInfo.setSex(StringUtils.equalsIgnoreCase(userInfoVo.getGender(), "man") ? SexEnum.MAN : SexEnum.WOMAN);
userInfo.setBirthday(userInfoVo.getBirthday());
userInfo.setCity(userInfoVo.getCity());
userInfo.setEdu(userInfoVo.getEducation());
userInfo.setIncome(StringUtils.replaceAll(userInfoVo.getIncome(), "K", ""));
userInfo.setIndustry(userInfoVo.getProfession());
userInfo.setMarriage(userInfoVo.getMarriage() == 1 ? "已婚" : "未婚");
return this.userInfoService.updateUserInfoByUserId(userInfo);
}

4.4.4、UserInfoService

1
2
3
4
5
6
7
//com.tanhua.server.service.UserInfoService

public boolean updateUserInfoByUserId(UserInfo userInfo) {
QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userInfo.getUserId());
return this.userInfoMapper.update(userInfo, queryWrapper) > 0;
}

4.4.5、bug修复

在之前的查询个人信息中接口中,返回数据中的性别数据有误,需要返回man或woman。

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.server.service.MyCenterService

public UserInfoVo queryUserInfoByUserId(Long userId) {
if (ObjectUtil.isEmpty(userId)) {
//如果查询id为null,就表示查询当前用户信息
userId = UserThreadLocal.get().getId();
}
//查询用户信息
UserInfo userInfo = this.userInfoService.queryUserInfoByUserId(userId);
if (ObjectUtil.isEmpty(userInfo)) {
return null;
}

UserInfoVo userInfoVo = BeanUtil.copyProperties(userInfo, UserInfoVo.class, "marriage");
userInfoVo.setGender(userInfo.getSex().getValue() == 1 ? "man" : "women");
userInfoVo.setMarriage(StrUtil.equals("已婚", userInfo.getMarriage()) ? 1 : 0);
return userInfoVo;
}

4.2、更新头像

上传头像使用sso中的上传逻辑即可,只是路径不同,所以我们只需要修改nginx配置和在sso中定义Controller即可。

接口文档:https://mock-java.itheima.net/project/35/interface/api/881

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
#user  nobody;
worker_processes 1;

#error_log logs/error.log;
#error_log logs/error.log notice;
#error_log logs/error.log info;

#pid logs/nginx.pid;


events {
worker_connections 1024;
}


http {
include mime.types;
default_type application/octet-stream;

#log_format main '$remote_addr - $remote_user [$time_local] "$request" '
# '$status $body_bytes_sent "$http_referer" '
# '"$http_user_agent" "$http_x_forwarded_for"';

#access_log logs/access.log main;

sendfile on;
#tcp_nopush on;

#keepalive_timeout 0;
keepalive_timeout 65;

#gzip on;

server {
listen 80;
server_name localhost;

#charset koi8-r;

#access_log logs/host.access.log main;

#error_page 404 /404.html;

# redirect server error pages to the static page /50x.html
#
error_page 500 502 503 504 /50x.html;
location = /50x.html {
root html;
}

location /user/ { #请求路径中凡是以/user/开头的请求,转发到sso系统
client_max_body_size 300m; #设置最大的请求体大小,解决大文件上传不了的问题
proxy_connect_timeout 300s; #代理连接超时时间
proxy_send_timeout 300s; #代理发送数据的超时时间
proxy_read_timeout 300s; #代理读取数据的超时时间
proxy_pass http://127.0.0.1:18080; #转发请求
}

location /users/header { #请求路径中凡是以/user/header开头的请求,转发到sso系统
client_max_body_size 300m; #设置最大的请求体大小,解决大文件上传不了的问题
proxy_connect_timeout 300s; #代理连接超时时间
proxy_send_timeout 300s; #代理发送数据的超时时间
proxy_read_timeout 300s; #代理读取数据的超时时间
proxy_pass http://127.0.0.1:18080; #转发请求
}

location / { #上面未匹配到的在这里处理
client_max_body_size 300m;
proxy_connect_timeout 300s;
proxy_send_timeout 300s;
proxy_read_timeout 300s;
proxy_pass http://127.0.0.1:18081; #转发请求到server系统
}
}

}

4.2.2、MyCenterController

在sso工程中定义MyCenterController。

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
package com.tanhua.sso.controller;

import com.tanhua.sso.vo.ErrorResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@RestController
@RequestMapping("users")
public class MyCenterController {

@Autowired
private UserInfoController userInfoController;

/**
* 上传头像
*
* @param file
* @param token
* @return
*/
@PostMapping("header")
public ResponseEntity<Object> saveLogo(@RequestParam("headPhoto") MultipartFile file, @RequestHeader("Authorization") String token) {
return this.userInfoController.saveUserLogo(file, token);
}
}

我的功能实现

课程说明

  • 实现我的喜欢功能
  • 实现用户通用设置
  • 实现黑名单功能
  • 实现修改手机号功能

1、我的喜欢统计数

在我的模块中,将详细展现“喜欢”相关的数据,如下:

1572445671626

1572445655283

1.1、概念说明

  • 喜欢
    • 我喜欢别人,如:张三喜欢李四,就是喜欢的数据,并不代表李四也喜欢张三。
  • 粉丝
    • 对于李四而言,张三就是他的粉丝。
  • 相互关注(喜欢)
    • 如果李四也喜欢张三,那么,张三和李四就是相互喜欢。

1.2、dubbo服务

1.2.1、UserLikeApi

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//com.tanhua.dubbo.server.api.UserLikeApi

/**
* 相互喜欢的数量
*
* @return
*/
Long queryMutualLikeCount(Long userId);

/**
* 喜欢数
*
* @return
*/
Long queryLikeCount(Long userId);

/**
* 粉丝数
*
* @return
*/
Long queryFanCount(Long userId);

1.2.2、UserLikeApiImpl

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
//com.tanhua.dubbo.server.api.UserLikeApiImpl

/**
* 查询相互喜欢数
* 实现2种方式:第一种:查询redis,第二种:查询MongoDB
* 建议:优先使用redis查询,其次考虑使用Mongodb
*
* @param userId
* @return
*/
@Override
public Long queryMutualLikeCount(Long userId) {
//查询我的喜欢列表
List<Long> likeUserIdList = this.queryLikeList(userId);

Long count = 0L;
for (Long likeUserId : likeUserIdList) {
String redisKey = this.getLikeRedisKey(likeUserId);
String hashKey = String.valueOf(userId);
//“别人” 的喜欢列表中是否有 “我”
if (this.redisTemplate.opsForHash().hasKey(redisKey, hashKey)) {
count++;
}
}
return count;
}

@Override
public Long queryLikeCount(Long userId) {
String redisKey = this.getLikeRedisKey(userId);
return this.redisTemplate.opsForHash().size(redisKey);
}

@Override
public Long queryFanCount(Long userId) {
//无法通过redis查询完成,必须从Mongodb中查询
Query query = Query.query(Criteria.where("likeUserId").is(userId));
return this.mongoTemplate.count(query, UserLike.class);
}

1.2.3、单元测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tanhua.dubbo.server.api;

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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest
public class TestUserLikeApiImpl {

@Autowired
private UserLikeApi userLikeApi;

@Test
public void testQueryCounts(){
System.out.println(this.userLikeApi.queryEachLikeCount(1L));
System.out.println(this.userLikeApi.queryFanCount(1L));
System.out.println(this.userLikeApi.queryLikeCount(1L));
}

}

1.3、APP接口服务

文档地址:https://mock-java.itheima.net/project/35/interface/api/899

1.3.1、CountsVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CountsVo {

private Long eachLoveCount; //互相喜欢
private Long loveCount; //喜欢
private Long fanCount; //粉丝

}

1.3.2、UsersController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//com.tanhua.server.controller.MyCenterController

/**
* 互相喜欢,喜欢,粉丝 - 统计
*
* @return
*/
@GetMapping("counts")
public ResponseEntity<CountsVo> queryCounts(){
try {
CountsVo countsVo = this.myCenterService.queryCounts();
if(null != countsVo){
return ResponseEntity.ok(countsVo);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

1.3.3、UsersService

1
2
3
4
5
6
7
8
9
10
11
12
//com.tanhua.server.service.MyCenterService

public CountsVo queryCounts() {
User user = UserThreadLocal.get();
CountsVo countsVo = new CountsVo();

countsVo.setEachLoveCount(this.userLikeApi.queryMutualLikeCount(user.getId()));
countsVo.setFanCount(this.userLikeApi.queryFanCount(user.getId()));
countsVo.setLoveCount(this.userLikeApi.queryLikeCount(user.getId()));

return countsVo;
}

1.3.4、测试

image-20210120113145052

2、喜欢列表

接口服务:https://mock-java.itheima.net/project/35/interface/api/905

该接口集成了4个接口,用type做了区分: 1 互相关注 2 我关注 3 粉丝 4 谁看过我

2.1、喜欢dubbo接口服务

2.1.1、定义接口

在dubbo接口中定义方法:

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
//com.tanhua.dubbo.server.api.UserLikeApi

/**
* 查询相互喜欢列表
*
* @param userId
* @param page
* @param pageSize
* @return
*/
PageInfo<UserLike> queryMutualLikeList(Long userId, Integer page, Integer pageSize);

/**
* 查询我喜欢的列表
*
* @param userId
* @param page
* @param pageSize
* @return
*/
PageInfo<UserLike> queryLikeList(Long userId, Integer page, Integer pageSize);

/**
* 查询粉丝列表
*
* @param userId
* @param page
* @param pageSize
* @return
*/
PageInfo<UserLike> queryFanList(Long userId, Integer page, Integer pageSize);

2.1.2、实现接口

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
//com.tanhua.dubbo.server.api.UserLikeApiImpl

@Override
public PageInfo<UserLike> queryMutualLikeList(Long userId, Integer page, Integer pageSize) {
//查询我的喜欢列表
List<Long> userLikeIdList = this.queryLikeList(userId);

//查询喜欢我的人
Query query = Query.query(Criteria.where("userId").in(userLikeIdList)
.and("likeUserId").is(userId)
);

return this.queryList(query, page, pageSize);
}

@Override
public PageInfo<UserLike> queryLikeList(Long userId, Integer page, Integer pageSize) {
Query query = Query.query(Criteria.where("userId").is(userId));
return this.queryList(query, page, pageSize);
}

@Override
public PageInfo<UserLike> queryFanList(Long userId, Integer page, Integer pageSize) {
Query query = Query.query(Criteria.where("likeUserId").is(userId));
return this.queryList(query, page, pageSize);
}

private PageInfo<UserLike> queryList(Query query, Integer page, Integer pageSize) {
//设置分页
PageRequest pageRequest = PageRequest.of(page - 1, pageSize,
Sort.by(Sort.Order.desc("created")));
query.with(pageRequest);

List<UserLike> userLikeList = this.mongoTemplate.find(query, UserLike.class);

PageInfo<UserLike> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);
pageInfo.setRecords(userLikeList);

return pageInfo;
}

2.2、最近访客dubbo服务

2.2.1、定义接口

1
2
3
4
5
6
7
8
9
10
11
//com.tanhua.dubbo.server.api.VisitorsApi

/**
* 按照时间倒序排序,查询最近的访客信息
*
* @param userId
* @param page
* @param pageSize
* @return
*/
PageInfo<Visitors> topVisitor(Long userId, Integer page, Integer pageSize);

2.2.2、编写实现

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
//com.tanhua.dubbo.server.api.VisitorsApiImpl

@Override
public List<Visitors> queryMyVisitor(Long userId) {
// 查询前5个访客数据,按照访问时间倒序排序
// 如果用户已经查询过列表,记录查询时间,后续查询需要按照这个时间往后查询

// 上一次查询列表的时间
Long date = Convert.toLong(this.redisTemplate.opsForHash().get(VISITOR_REDIS_KEY, String.valueOf(userId)));

PageRequest pageRequest = PageRequest.of(0, 5, Sort.by(Sort.Order.desc("date")));
Query query = Query.query(Criteria.where("userId").is(userId))
.with(pageRequest);
if (ObjectUtil.isNotEmpty(date)) {
query.addCriteria(Criteria.where("date").gte(date));
}
return this.queryList(query, userId);
}

private List<Visitors> queryList(Query query, Long userId){
List<Visitors> visitorsList = this.mongoTemplate.find(query, Visitors.class);
//查询每个来访用户的得分
for (Visitors visitors : visitorsList) {

Query queryScore = Query.query(Criteria.where("toUserId")
.is(userId).and("userId").is(visitors.getVisitorUserId())
);
RecommendUser recommendUser = this.mongoTemplate.findOne(queryScore, RecommendUser.class);
if(ObjectUtil.isNotEmpty(recommendUser)){
visitors.setScore(recommendUser.getScore());
}else {
//默认得分
visitors.setScore(90d);
}
}

return visitorsList;
}

@Override
public PageInfo<Visitors> topVisitor(Long userId, Integer page, Integer pageSize) {
PageRequest pageRequest = PageRequest.of(page - 1, pageSize,
Sort.by(Sort.Order.desc("date")));
Query query = Query.query(Criteria.where("userId").is(userId)).with(pageRequest);
List<Visitors> visitorsList = this.queryList(query, userId);

PageInfo<Visitors> pageInfo = new PageInfo<>();
pageInfo.setPageNum(page);
pageInfo.setPageSize(pageSize);
pageInfo.setRecords(visitorsList);

//记录当前的时间到redis中,在首页查询时,就可以在这个时间之后查询了
String redisKey = VISITOR_REDIS_KEY;
String hashKey = String.valueOf(userId);
String value = String.valueOf(System.currentTimeMillis());
this.redisTemplate.opsForHash().put(redisKey, hashKey, value);

return pageInfo;
}

2.3、APP接口服务

接口文档:https://mock-java.itheima.net/project/35/interface/api/905

2.3.1、UserLikeListVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserLikeListVo {

private Long id;
private String avatar;
private String nickname;
private String gender;
private Integer age;
private String city;
private String education;
private Integer marriage; //婚姻状态(0未婚,1已婚)
private Integer matchRate; //匹配度
private Boolean alreadyLove; //是否喜欢ta

}

2.3.2、MyCenterController

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
//com.tanhua.server.controller.MyCenterController

/**
* 互相关注、我关注、粉丝、谁看过我 - 翻页列表
*
* @param type 1 互相关注 2 我关注 3 粉丝 4 谁看过我
* @param page
* @param pageSize
* @param nickname
* @return
*/
@GetMapping("friends/{type}")
public ResponseEntity<PageResult> queryLikeList(@PathVariable("type") String type,
@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pageSize,
@RequestParam(value = "nickname", required = false) String nickname) {
try {
page = Math.max(1, page);
PageResult pageResult = this.myCenterService.queryLikeList(Integer.valueOf(type), page, pageSize, nickname);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.3.3、MyCenterService

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
//com.tanhua.server.service.MyCenterService

public PageResult queryLikeList(Integer type, Integer page, Integer pageSize, String nickname) {
PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

Long userId = UserThreadLocal.get().getId();

List<Object> userIdList = null;

//1 互相关注 2 我关注 3 粉丝 4 谁看过我
switch(type){
case 1:{
PageInfo<UserLike> pageInfo = this.userLikeApi.queryMutualLikeList(userId, page, pageSize);
userIdList = CollUtil.getFieldValues(pageInfo.getRecords(), "userId");
break;
}
case 2:{
PageInfo<UserLike> pageInfo = this.userLikeApi.queryLikeList(userId, page, pageSize);
userIdList = CollUtil.getFieldValues(pageInfo.getRecords(), "likeUserId");
break;
}
case 3:{
PageInfo<UserLike> pageInfo = this.userLikeApi.queryFanList(userId, page, pageSize);
userIdList = CollUtil.getFieldValues(pageInfo.getRecords(), "userId");
break;
}
case 4:{
PageInfo<Visitors> pageInfo = this.visitorsApi.topVisitor(userId, page, pageSize);
userIdList = CollUtil.getFieldValues(pageInfo.getRecords(), "visitorUserId");
break;
}
default:
return pageResult;
}

if(CollUtil.isEmpty(userIdList)){
return pageResult;
}

QueryWrapper<UserInfo> queryWrapper = new QueryWrapper<>();
queryWrapper.in("user_id", userIdList);
if(StrUtil.isNotEmpty(nickname)){
queryWrapper.like("nick_name", nickname);
}

List<UserInfo> userInfoList = this.userInfoService.queryUserInfoList(queryWrapper);
List<UserLikeListVo> userLikeListVos = new ArrayList<>();
for (UserInfo userInfo : userInfoList) {
UserLikeListVo userLikeListVo = new UserLikeListVo();
userLikeListVo.setAge(userInfo.getAge());
userLikeListVo.setAvatar(userInfo.getLogo());
userLikeListVo.setCity(userInfo.getCity());
userLikeListVo.setEducation(userInfo.getEdu());
userLikeListVo.setGender(userInfo.getSex().name().toLowerCase());
userLikeListVo.setId(userInfo.getUserId());
userLikeListVo.setMarriage(StringUtils.equals(userInfo.getMarriage(), "已婚") ? 1 : 0);
userLikeListVo.setNickname(userInfo.getNickName());
//是否喜欢 userLikeApi中的isLike开放出来
userLikeListVo.setAlreadyLove(this.userLikeApi.isLike(userId, userInfo.getUserId()));


Double score = this.recommendUserService.queryScore(userId, userInfo.getUserId());
userLikeListVo.setMatchRate(Convert.toInt(score));

userLikeListVos.add(userLikeListVo);
}

pageResult.setItems(userLikeListVos);

return pageResult;
}

2.3.4、测试

1572535858665

1572535875980

1572535893293

image-20210120221811034

1572535940981

2.4、取消喜欢

在列表中可以进行“取消喜欢”操作。

接口文档:https://mock-java.itheima.net/project/35/interface/api/923

2.4.1、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.server.controller.MyCenterController

/**
* 取消喜欢
*
* @param userId
* @return
*/
@DeleteMapping("like/{uid}")
public ResponseEntity<Void> disLike(@PathVariable("uid") Long userId) {
try {
this.myCenterService.disLike(userId);
return ResponseEntity.ok(null);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.4.2、MyCenterService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.server.service.MyCenterService

/**
* 取消喜欢
*
* @param userId
*/
public void disLike(Long userId) {
//判断当前用户与此用户是否相互喜欢
User user = UserThreadLocal.get();
Boolean mutualLike = this.userLikeApi.isMutualLike(user.getId(), userId);

//取消喜欢
this.userLikeApi.notLikeUser(user.getId(), userId);

if(mutualLike){
//取消好友关系,解除在环信平台的好友关系
this.imService.removeUser(userId);
}
}

2.4.3、IMService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//com.tanhua.server.service.IMService 

/**
* 删除好友
*
* @param userId 好友id
*/
public void removeUser(Long userId) {
//删除好友关系
User user = UserThreadLocal.get();
Boolean result = this.usersApi.removeUsers(user.getId(), userId);
if(result){
//将环信平台的好友关系解除
this.huanXinApi.removeUserFriend(user.getId(), userId);
}
}

2.4.4、测试

测试时,需要将MongoDB中的user_like表数据清空以及将Redis中喜欢和不喜欢数据删除。

使用用户2进行测试,现在和用户1是好友:

image-20210120231421237

取消喜欢:

image-20210120231523590

2.5、 喜欢粉丝

在查看粉丝列表中,可以对粉丝进行“喜欢操作”。

文档地址:https://mock-java.itheima.net/project/35/interface/api/917

2.5.1、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.server.controller.MyCenterController

/**
* 关注粉丝
*
* @param userId
* @return
*/
@PostMapping("fans/{uid}")
public ResponseEntity<Void> likeFan(@PathVariable("uid") Long userId){
try {
this.myCenterService.likeFan(userId);
return ResponseEntity.ok(null);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.5.2、MyCenterService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//com.tanhua.server.service.MyCenterService

@Autowired
private TanHuaService tanHuaService;

/**
* 喜欢
*
* @param userId
*/
public void likeFan(Long userId) {
//喜欢用户,如果用户是相互喜欢的话就会成为好友
this.tanHuaService.likeUser(userId);
}

2.5.3、测试

image-20210121094407113

3、用户通用设置

3.1、表结构

1
2
3
4
5
6
7
8
9
10
CREATE TABLE `tb_settings` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL,
`like_notification` tinyint(4) DEFAULT '1' COMMENT '推送喜欢通知',
`pinglun_notification` tinyint(4) DEFAULT '1' COMMENT '推送评论通知',
`gonggao_notification` tinyint(4) DEFAULT '1' COMMENT '推送公告通知',
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='设置表';

3.2、pojo

my-tanhua-common工程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.tanhua.common.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Settings extends BasePojo {

private Long id;
private Long userId;
private Boolean likeNotification = true;
private Boolean pinglunNotification = true;
private Boolean gonggaoNotification = true;

}

3.3、SettingsMapper

1
2
3
4
5
6
7
8
9
package com.tanhua.common.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.Settings;

public interface SettingsMapper extends BaseMapper<Settings> {

}

3.4、SettingsService

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
package com.tanhua.server.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.tanhua.common.mapper.SettingsMapper;
import com.tanhua.common.pojo.Settings;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class SettingsService {

@Autowired
private SettingsMapper settingsMapper;

/**
* 根据用户id查询配置
*
* @param userId
* @return
*/
public Settings querySettings(Long userId) {
QueryWrapper<Settings> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);
return this.settingsMapper.selectOne(queryWrapper);
}
}

3.5、查询配置

文档地址:https://mock-java.itheima.net/project/35/interface/api/893

3.5.1、SettingsVo

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
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class SettingsVo {

private Long id;
//陌生人问题
private String strangerQuestion = "";
//手机号
private String phone;
//推送喜欢通知
private Boolean likeNotification = true;
//推送评论通知
private Boolean pinglunNotification = true;
//推送公告通知
private Boolean gonggaoNotification = true;

}

3.5.2、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//com.tanhua.server.controller.MyCenterController

/**
* 查询配置
*
* @return
*/
@GetMapping("settings")
public ResponseEntity<SettingsVo> querySettings() {
try {
SettingsVo settingsVo = this.myCenterService.querySettings();
if (null != settingsVo) {
return ResponseEntity.ok(settingsVo);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.5.3、MyCenterService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//com.tanhua.server.service.MyCenterService

public SettingsVo querySettings() {
SettingsVo settingsVo = new SettingsVo();
User user = UserThreadLocal.get();

//设置用户的基本信息
settingsVo.setId(user.getId());
settingsVo.setPhone(user.getMobile());

//查询用户的配置数据
Settings settings = this.settingsService.querySettings(user.getId());
if(ObjectUtil.isNotEmpty(settings)){
settingsVo.setGonggaoNotification(settings.getGonggaoNotification());
settingsVo.setLikeNotification(settings.getLikeNotification());
settingsVo.setPinglunNotification(settings.getPinglunNotification());
}

//查询陌生人问题
settingsVo.setStrangerQuestion(this.tanHuaService.queryQuestion(user.getId()));

return settingsVo;
}

2.5.4、测试

image-20210121105800044

image-20210121105822095

3.6、保存陌生人问题

文档地址:https://mock-java.itheima.net/project/35/interface/api/929

3.6.1、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.server.controller.MyCenterController

/**
* 设置陌生人问题
*
* @return
*/
@PostMapping("questions")
public ResponseEntity<Void> saveQuestions(@RequestBody Map<String, String> param) {
try {
String content = param.get("content");
this.myCenterService.saveQuestions(content);
return ResponseEntity.ok(null);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.6.2、MyCenterService

1
2
3
4
5
6
//com.tanhua.server.service.MyCenterService

public void saveQuestions(String content) {
User user = UserThreadLocal.get();
this.questionService.save(user.getId(), content);
}

3.6.3、QuestionService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//com.tanhua.server.service.QuestionService

public void save(Long userId, String content) {
Question question = this.queryQuestion(userId);
if(null != question){
question.setTxt(content);
this.questionMapper.updateById(question);
}else {
question = new Question();
question.setUserId(userId);
question.setTxt(content);
question.setCreated(new Date());
question.setUpdated(question.getCreated());
this.questionMapper.insert(question);
}
}

3.7、黑名单列表

黑名单功能可以用在陌生人打招呼时,进行判断,如果是黑名单的则不能打招呼。

接口文档:https://mock-java.itheima.net/project/35/interface/api/935

3.7.1、表结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
CREATE TABLE `tb_black_list` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) DEFAULT NULL,
`black_user_id` bigint(20) DEFAULT NULL,
`created` datetime DEFAULT NULL,
`updated` datetime DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='黑名单';


--测试数据
INSERT INTO `tb_black_list` (`id`, `user_id`, `black_user_id`, `created`, `updated`) VALUES ('1', '1', '22', '2019-11-01 15:47:22', '2019-11-01 15:47:24');
INSERT INTO `tb_black_list` (`id`, `user_id`, `black_user_id`, `created`, `updated`) VALUES ('2', '1', '23', '2019-11-01 15:47:39', '2019-11-01 15:47:42');
INSERT INTO `tb_black_list` (`id`, `user_id`, `black_user_id`, `created`, `updated`) VALUES ('3', '1', '24', '2019-11-01 15:47:51', '2019-11-01 15:47:56');

3.7.2、pojo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tanhua.common.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class BlackList extends BasePojo {

private Long id;
private Long userId;
private Long blackUserId;
}

3.7.3、BlackListMapper

1
2
3
4
5
6
7
8
package com.tanhua.common.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.tanhua.common.pojo.BlackList;

public interface BlackListMapper extends BaseMapper<BlackList> {
}

3.7.4、BlackListService

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
package com.tanhua.server.service;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.tanhua.common.mapper.BlackListMapper;
import com.tanhua.common.pojo.BlackList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;


@Service
public class BlackListService {

@Autowired
private BlackListMapper blackListMapper;

public IPage<BlackList> queryBlacklist(Long userId, Integer page, Integer pageSize) {
QueryWrapper<BlackList> wrapper = new QueryWrapper<BlackList>();
wrapper.eq("user_id", userId);
wrapper.orderByDesc("created");
Page<BlackList> pager = new Page<>(page, pageSize);
return this.blackListMapper.selectPage(pager, wrapper);
}
}

配置分页插件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.tanhua.server.config;

import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MybatisPlusConfig {
/**
* 分页插件
*/
@Bean
public PaginationInterceptor paginationInterceptor() {
return new PaginationInterceptor();
}
}

3.7.5、BlackListVo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.tanhua.server.vo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class BlackListVo {

private Long id;
private String avatar;
private String nickname;
private String gender;
private Integer age;

}

3.7.6、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.server.controller.MyCenterController

/**
* 查询黑名单
*
* @param page
* @param pagesize
* @return
*/
@GetMapping("blacklist")
public ResponseEntity<PageResult> queryBlacklist(@RequestParam(value = "page", defaultValue = "1") Integer page,
@RequestParam(value = "pagesize", defaultValue = "10") Integer pagesize) {
try {
PageResult pageResult = this.myCenterService.queryBlacklist(page, pagesize);
return ResponseEntity.ok(pageResult);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.7.7、MyCenterService

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
//com.tanhua.server.service.MyCenterService

public PageResult queryBlacklist(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();

IPage<BlackList> blackListIPage = this.blackListService.queryBlacklist(user.getId(), page, pageSize);

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);
pageResult.setCounts(Convert.toInt(blackListIPage.getTotal()));
pageResult.setPages(Convert.toInt(blackListIPage.getPages()));

List<BlackList> records = blackListIPage.getRecords();
if(CollUtil.isEmpty(records)){
return pageResult;
}

List<Object> userIds = CollUtil.getFieldValues(records, "blackUserId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<BlackListVo> blackListVos = new ArrayList<>();
for (UserInfo userInfo : userInfoList) {
BlackListVo blackListVo = new BlackListVo();
blackListVo.setAge(userInfo.getAge());
blackListVo.setAvatar(userInfo.getLogo());
blackListVo.setGender(userInfo.getSex().name().toLowerCase());
blackListVo.setId(userInfo.getUserId());
blackListVo.setNickname(userInfo.getNickName());

blackListVos.add(blackListVo);
}

pageResult.setItems(blackListVos);

return pageResult;
}

3.7.8、测试

1572595630492

3.8、移除黑名单

接口文档:https://mock-java.itheima.net/project/35/interface/api/941

3.8.1、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* 移除黑名单
*
* @return
*/
@DeleteMapping("blacklist/{uid}")
public ResponseEntity<Void> delBlacklist(@PathVariable("uid") Long userId) {
try {
this.myCenterService.delBlacklist(userId);
return ResponseEntity.ok(null);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

3.8.2、MyCenterService

1
2
3
4
public void delBlacklist(Long userId) {
User user = UserThreadLocal.get();
this.blackListService.delBlacklist(user.getId(), userId);
}

3.8.3、BlackListService

1
2
3
4
5
public Boolean delBlacklist(Long userId, Long blackUserId) {
QueryWrapper<BlackList> wrapper = new QueryWrapper<BlackList>();
wrapper.eq("user_id", userId).eq("black_user_id", blackUserId);
return this.blackListMapper.delete(wrapper) > 0;
}

3.9、更新通知

接口文档:https://mock-java.itheima.net/project/35/interface/api/965

3.9.1、MyCenterController

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//com.tanhua.server.controller.MyCenterController

/**
* 更新通知设置
*
* @param param
* @return
*/
@PostMapping("notifications/setting")
public ResponseEntity<Void> updateNotification(@RequestBody Map<String, Boolean> param) {
try {
Boolean likeNotification = param.get("likeNotification");
Boolean pinglunNotification = param.get("pinglunNotification");
Boolean gonggaoNotification = param.get("gonggaoNotification");

this.usersService.updateNotification(likeNotification, pinglunNotification, gonggaoNotification);
return ResponseEntity.ok(null);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

2.9.2、MyCenterService

1
2
3
4
5
6
//com.tanhua.server.service.MyCenterService

public void updateNotification(Boolean likeNotification, Boolean pinglunNotification, Boolean gonggaoNotification) {
User user = UserThreadLocal.get();
this.settingsService.updateNotification(user.getId(), likeNotification, pinglunNotification, gonggaoNotification);
}

2.9.3、SettingsService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//com.tanhua.server.service.SettingsService

public void updateNotification(Long userId, Boolean likeNotification, Boolean pinglunNotification, Boolean gonggaoNotification) {
QueryWrapper<Settings> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("user_id", userId);

Settings settings = this.settingsMapper.selectOne(queryWrapper);
if(null == settings){
//如果没有数据的话,插入一条数据
settings = new Settings();
settings.setUserId(userId);
this.settingsMapper.insert(settings);
}else{
//更新
settings.setLikeNotification(likeNotification);
settings.setPinglunNotification(pinglunNotification);
settings.setGonggaoNotification(gonggaoNotification);
this.settingsMapper.update(settings, queryWrapper);
}
}

3.10、更新手机号

更新手机号的逻辑在sso系统中完成,其流程是:旧手机号获取验证码,验证码校验通过后,设置新手机号,最后保存新的手机号。

步骤1,发送短信验证码:https://mock-java.itheima.net/project/35/interface/api/947

步骤2,校验验证码:https://mock-java.itheima.net/project/35/interface/api/953

步骤3,保存新手机号:https://mock-java.itheima.net/project/35/interface/api/959

配置nginx:

1
2
3
4
5
6
7
location /users/phone {  #请求路径中凡是以/user/phone开头的请求,转发到sso系统
client_max_body_size 300m; #设置最大的请求体大小,解决大文件上传不了的问题
proxy_connect_timeout 300s; #代理连接超时时间
proxy_send_timeout 300s; #代理发送数据的超时时间
proxy_read_timeout 300s; #代理读取数据的超时时间
proxy_pass http://127.0.0.1:18080; #转发请求
}

3.10.1、 发送短信验证码

MyCenterController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//com.tanhua.sso.controller.MyCenterController

/**
* 发送短信验证码
*
* @return
*/
@PostMapping("phone/sendVerificationCode")
public ResponseEntity<Void> sendVerificationCode(@RequestHeader("Authorization") String token) {
try {
boolean bool = this.myCenterService.sendVerificationCode(token);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

MyCenterService:

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
//com.tanhua.sso.service.MyCenterService

package com.tanhua.sso.service;

import com.tanhua.common.pojo.User;
import com.tanhua.sso.vo.ErrorResult;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MyCenterService {

@Autowired
private UserService userService;

@Autowired
private SmsService smsService;

public Boolean sendVerificationCode(String token) {
//校验token
User user = this.userService.queryUserByToken(token);
if(ObjectUtil.isEmpty(user)){
return false;
}

ErrorResult errorResult = this.smsService.sendCheckCode(user.getMobile());
return errorResult == null;
}
}

3.10.2、 校验验证码

MyCenterController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//com.tanhua.sso.controller.MyCenterController

/**
* 校验验证码
*
* @param param
* @param token
* @return
*/
@PostMapping("phone/checkVerificationCode")
public ResponseEntity<Map<String, Object>> checkVerificationCode(@RequestBody Map<String, String> param,
@RequestHeader("Authorization") String token) {
try {
String code = param.get("verificationCode");
Boolean bool = this.myCenterService.checkVerificationCode(code, token);
Map<String, Object> result = new HashMap<>();
result.put("verification", bool);
return ResponseEntity.ok(result);
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

MyCenterService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.sso.service.MyCenterService

public Boolean checkVerificationCode(String code, String token) {
//校验token
User user = this.userService.queryUserByToken(token);
if(ObjectUtil.isEmpty(user)){
return false;
}

//校验验证码,先查询redis中的验证码
String redisKey = "CHECK_CODE_" + user.getMobile();
String value = this.redisTemplate.opsForValue().get(redisKey);

if(StrUtil.equals(code, value)){
//将验证码删除
this.redisTemplate.delete(redisKey);
return true;
}

return false;
}

3.10.3、保存新手机号

MyCenterController:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//com.tanhua.sso.controller.MyCenterController

/**
* 保存新手机号
*
* @return
*/
@PostMapping("phone")
public ResponseEntity<Void> updatePhone(@RequestBody Map<String, String> param,
@RequestHeader("Authorization") String token) {
try {
String newPhone = param.get("phone");
boolean bool = this.myCenterService.updatePhone(token, newPhone);
if (bool) {
return ResponseEntity.ok(null);
}
} catch (Exception e) {
e.printStackTrace();
}
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}

MyCenterService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//com.tanhua.sso.service.MyCenterService

public Boolean updatePhone(String token, String newPhone) {
//校验token
User user = this.userService.queryUserByToken(token);
if(ObjectUtil.isEmpty(user)){
return false;
}
Boolean result = this.userService.updatePhone(user.getId(), newPhone);
if(result){
String redisKey = "TANHUA_USER_MOBILE_" + user.getId();
this.redisTemplate.delete(redisKey);
}

return result;
}

UserService:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//com.tanhua.sso.service.UserService

public Boolean updatePhone(Long userId, String newPhone) {
//先查询新手机号是否已经注册,如果已经注册,就不能修改
QueryWrapper<User> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("mobile", newPhone);
User user = this.userMapper.selectOne(queryWrapper);
if(ObjectUtil.isNotEmpty(user)){
//新手机号已经被注册
return false;
}

user = new User();
user.setId(userId);
user.setMobile(newPhone);

return this.userMapper.updateById(user) > 0;
}

推荐功能实现

课程说明

  • 了解推荐系统
  • 实现好友的推荐
  • 圈子推荐功能说明
  • 圈子推荐功能流程
  • 圈子推荐功能的实现
  • 小视频推荐功能的实现

1、了解推荐系统

1.1、什么是推荐系统?

为了解决信息过载和用户无明确需求的问题,找到用户感兴趣的物品,才有了个性化推荐系统。

其实,解决信息过载的问题,代表性的解决方案是分类目录和搜索引擎,如hao123,电商首页的分类目录以及百度,360搜索等。

不过分类目录和搜索引擎只能解决用户主动查找信息的需求,即用户知道自己想要什么,并不能解决用户没用明确需求很随便的问题。

经典语录是:你想吃什么,随便!面对这种很随便又得罪不起的用户(女友和上帝),只能通过分析用户的历史行为给用户的兴趣建模,从而主动给用户推荐能够满足他们兴趣和需求的信息。比如问问女友的闺蜜,她一般什么时候喜欢吃什么。

1.2、电商是推荐系统的先行者

  • 电子商务网站是个性化推荐系统重要地应用的领域之一,亚马逊就是个性化推荐系统的积极应用者和推广者,亚马逊的推荐系统深入到网站的各类商品,为亚马逊带来了至少30%的销售额。
  • 不光是电商类,推荐系统无处不在。QQ,微信的好友推荐;新浪微博的你可能感兴趣的人;优酷,土豆的电影推荐;豆瓣的图书推荐;大从点评的餐饮推荐;脉脉的同事推荐等。
  • 推荐引擎的鼻祖思想源泉:http://portal.acm.org/citation.cfm?id=1070751
  • 亚马逊最早提出基亍物品的协同过滤推荐算法:http://portal.acm.org/citation.cfm?id=372071

京东的推荐: image-20231004074948594

1.3、推荐系统业务流程

image-20231004074953433

推荐系统广泛存在于各类网站中,作为一个应用为用户提供个性化的推荐。它需要一些用户的历史数据,一般由三个部分组成:基础数据、推荐算法系统、前台展示。

  • 基础数据包括很多维度,包括用户的访问、浏览、下单、收藏,用户的历史订单信息,评价信息等很多信息;
  • 推荐算法系统主要是根据不同的推荐诉求由多个算法组成的推荐模型;
  • 前台展示主要是对客户端系统进行响应,返回相关的推荐信息以供展示。

1.4、协同过滤推荐算法

迄今为止,在个性化推荐系统中,协同过滤技术是应用最成功的技术。目前国内外有许多大型网站应用这项技术为用户更加智能(个性化、千人千面)的推荐内容。

核心思想:

协同过滤一般是在海量的用户中发掘出一小部分和你品位比较类似的,在协同过滤中,这些用户成为邻居,然后根据他们喜欢的其他东西组织成一个排序的目彔作为推荐给你。

1.4.1、基于用户的推荐 UserCF

image-20231004074959171

image-20231004075005162

对于用户A,根据用户的历史偏好,这里只计算得到一个邻居–用户C,然后将用户C 喜欢的物品D 推荐给用户A。

基于用户的协同过滤算法先计算的是用户与用户的相似度(兴趣相投,物以类聚人以群分),然后将相似度比较接近的用户A购买的物品推荐给用户B,专业的说法是该算法用最近邻居(nearest-neighbor)算法找出一个用户的邻居集合,该集合的用户和该用户有相似的喜好,算法根据邻居的偏好对该用户进行预测。

1.4.2、基于商品的推荐 ItemCF

image-20231004075009009

  • 基于ItemCF的原理和基于UserCF类似,只是在计算邻居时采用物品本身,而不是从用户的角度,即基于用户对物品的偏好找到相似的物品,然后根据用户的历史偏好,推荐相似的物品给他。
  • 从计算的角度看,就是将所有用户对某个物品的偏好作为一个向量来计算物品之间的相似度,得到物品的相似物品后,根据用户历史的偏好预测当前用户还没有表示偏好的物品,计算得到一个排序的物品列表作为推荐。
  • 解释:对于物品A,根据所有用户的历史偏好,喜欢物品A 的用户都喜欢物品C,得出物品A 和物品C 比较相似,而用户C 喜欢物品A,那么可以推断出用户C 可能也喜欢物品C。

1.5、ALS算法

ALS 是交替最小二乘 (alternating least squares)的简称。在机器学习的上下文中,ALS 特指使用交替最小二乘求解的一个协同推荐算法。它通过观察到的所有用户给产品的打分,来推断每个用户的喜好并向用户推荐适合的产品。从协同过滤的分类来说,ALS算法属于User-Item CF,也叫做混合CF。它同时考虑了User和Item两个方面。

用户和商品的关系,可以抽象为如下的三元组:<User,Item,Rating>。其中,Rating是用户对商品的评分,表征用户对该商品的喜好程度。如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
196	242	3	881250949
186 302 3 891717742
22 377 1 878887116
244 51 2 880606923
166 346 1 886397596
298 474 4 884182806
115 265 2 881171488
253 465 5 891628467
305 451 3 886324817
6 86 3 883603013
62 257 2 879372434
286 1014 5 879781125
200 222 5 876042340
210 40 3 891035994
................

2、好友推荐

对于好友的推荐,需要找出每个用户之间的相似性,具体规则如下:

字段 权重分
年龄差 0-2岁 30分 3-5 20分 5-10岁 10分 10岁以上 0分
性别 异性 30分 同性 0分
位置 同城 20分 不同 0分
学历 相同 20分 不同 0分

2.1、流程

image-20210127101912885

2.2、部署好友推荐服务

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
#拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/itcast/tanhua-spark-recommend-user:1.0.1

#创建容器
docker create --name tanhua-spark-recommend-user \
--env MONGODB_HOST=192.168.31.81 \
--env MONGODB_PORT=27017 \
--env MONGODB_USERNAME=tanhua \
--env MONGODB_PASSWORD=l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV \
--env MONGODB_DATABASE=tanhua \
--env MONGODB_COLLECTION=recommend_user \
--env JDBC_URL="jdbc:mysql://192.168.31.81:3306/mytanhua?useUnicode=true&characterEncoding=utf8&autoReconnect=true&allowMultiQueries=true&useSSL=false" \
--env JDBC_DRIVER=com.mysql.jdbc.Driver \
--env JDBC_USER=root \
--env JDBC_PASSWORD=root \
--env JDBC_TABLE=tb_user_info \
--env SCHEDULE_PERIOD=30 \
registry.cn-hangzhou.aliyuncs.com/itcast/tanhua-spark-recommend-user:1.0.1

#参数说明
#MONGODB_HOST mongodb服务的地址
#MONGODB_PORT mongodb服务的端口
#MONGODB_USERNAME mongodb服务的认证用户名
#MONGODB_PASSWORD mongodb服务的认证密码
#MONGODB_DATABASE mongodb连接的数据库
#MONGODB_COLLECTION 操作表
#JDBC_URL mysql数据库连接地址
#JDBC_DRIVER jdbc驱动
#JDBC_USER 数据库连接用户名
#JDBC_PASSWORD 数据库连接密码
#JDBC_TABLE 数据库表名
#SCHEDULE_PERIOD 下次执行时间间隔,但是为分,默认为10分钟

#启动服务
docker start tanhua-spark-recommend-user
#查看日志
docker logs -f tanhua-spark-recommend-user

执行完成后,可以看到MongoDB中的recommend_user表中数据已经重新生成了。

3、圈子推荐

3.1、功能说明

在圈子功能中,针对于用户发布的动态信息,系统可以根据用户的发布、浏览、点赞等操作,对动态信息做计算,然后对每个用户进行不同的推荐。

3.2、流程说明

image-20210126143347337

流程说明:

  • 用户对圈子的动态操作,如:发布、浏览、点赞、喜欢等,就会给RocketMQ进行发送消息;
  • 推荐系统接收消息,并且处理消息数据,处理之后将结果数据写入到MongoDB中;
  • Spark系统拉取数据,然后进行推荐计算;
  • 计算之后的结果数据写入到Redis中,为每个用户都进行个性化推荐;

3.3、动态计分规则

  • 浏览 +1
  • 点赞 +5
  • 喜欢 +8
  • 评论 + 10
  • 发布动态
    • 文字长度:50以内1分,50~100之间2分,100以上3分
    • 图片个数:每个图片一分

核心推荐逻辑:

  • 推荐模型:用户 | 动态 | 评分
  • 其中,评分是用户对动态操作的得分合计
  • 为什么自己发布动态还要计分? 是因为,自己发布就相当于自己对此动态也感兴趣,这样就可以在相似的人之间进行推荐了。

3.4、发送消息

3.4.1、QuanziMQService

my-tanhua-server增加依赖:

1
2
3
4
5
6
7
8
9
<!--RocketMQ相关-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</dependency>

配置文件:

1
2
3
# RocketMQ相关配置
rocketmq.name-server=192.168.31.81:9876
rocketmq.producer.group=tanhua
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
package com.tanhua.server.service;

import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.dubbo.server.api.QuanZiApi;
import com.tanhua.dubbo.server.pojo.Publish;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class QuanziMQService {

@Autowired
private RocketMQTemplate rocketMQTemplate;

@Reference(version = "1.0.0")
private QuanZiApi quanZiApi;

/**
* 发布动态消息
*
* @param publishId
* @return
*/
public Boolean publishMsg(String publishId) {
return this.sendMsg(publishId, 1);
}

/**
* 浏览动态消息
*
* @param publishId
* @return
*/
public Boolean queryPublishMsg(String publishId) {
return this.sendMsg(publishId, 2);
}

/**
* 点赞动态消息
*
* @param publishId
* @return
*/
public Boolean likePublishMsg(String publishId) {
return this.sendMsg(publishId, 3);
}

/**
* 取消点赞动态消息
*
* @param publishId
* @return
*/
public Boolean disLikePublishMsg(String publishId) {
return this.sendMsg(publishId, 6);
}

/**
* 喜欢动态消息
*
* @param publishId
* @return
*/
public Boolean lovePublishMsg(String publishId) {
return this.sendMsg(publishId, 4);
}

/**
* 取消喜欢动态消息
*
* @param publishId
* @return
*/
public Boolean disLovePublishMsg(String publishId) {
return this.sendMsg(publishId, 7);
}

/**
* 评论动态消息
*
* @param publishId
* @return
*/
public Boolean commentPublishMsg(String publishId) {
return this.sendMsg(publishId, 5);
}

/**
* 发送圈子操作相关的消息
*
* @param publishId
* @param type 1-发动态,2-浏览动态, 3-点赞, 4-喜欢, 5-评论,6-取消点赞,7-取消喜欢
* @return
*/
private Boolean sendMsg(String publishId, Integer type) {
try {
User user = UserThreadLocal.get();

Publish publish = this.quanZiApi.queryPublishById(publishId);

//构建消息
Map<String, Object> msg = new HashMap<>();
msg.put("userId", user.getId());
msg.put("date", System.currentTimeMillis());
msg.put("publishId", publishId);
msg.put("pid", publish.getPid());
msg.put("type", type);

this.rocketMQTemplate.convertAndSend("tanhua-quanzi", msg);
} catch (Exception e) {
log.error("发送消息失败! publishId = " + publishId + ", type = " + type, e);
return false;
}

return true;
}
}

3.4.2、修改QuanZiService

在QuanZiService完成发送消息方法调用。

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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
package com.tanhua.server.service;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
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.service.PicUploadService;
import com.tanhua.common.utils.RelativeDateFormat;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.common.vo.PicUploadResult;
import com.tanhua.dubbo.server.api.QuanZiApi;
import com.tanhua.dubbo.server.api.VisitorsApi;
import com.tanhua.dubbo.server.pojo.Comment;
import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.dubbo.server.pojo.Visitors;
import com.tanhua.dubbo.server.vo.PageInfo;
import com.tanhua.server.vo.CommentVo;
import com.tanhua.server.vo.PageResult;
import com.tanhua.server.vo.QuanZiVo;
import com.tanhua.server.vo.VisitorsVo;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.util.*;

@Service
public class QuanZiService {

@Reference(version = "1.0.0")
private QuanZiApi quanZiApi;

@Reference(version = "1.0.0")
private VisitorsApi visitorsApi;

@Autowired
private UserService userService;

@Autowired
private UserInfoService userInfoService;

@Autowired
private PicUploadService picUploadService;

@Autowired
private QuanziMQService quanziMQService;

public PageResult queryPublishList(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.queryPublishList(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(), ','));

//当前用户
User user = UserThreadLocal.get();

quanZiVo.setCommentCount(0); //TODO 评论数
quanZiVo.setDistance("1.2公里"); //TODO 距离
quanZiVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
quanZiVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(quanZiVo.getId()))); //点赞数
quanZiVo.setHasLoved(this.quanZiApi.queryUserIsLove(user.getId(), quanZiVo.getId()) ? 1 : 0); //是否喜欢(1是,0否)
quanZiVo.setLoveCount(Convert.toInt(this.quanZiApi.queryLoveCount(quanZiVo.getId()))); //喜欢数
}

/**
* 根据查询到的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;
}

/**
* 发布动态
*
* @param textContent
* @param location
* @param latitude
* @param longitude
* @param multipartFile
* @return
*/
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);
String publishId = this.quanZiApi.savePublish(publish);

if(StrUtil.isNotEmpty(publishId)){
//发送消息
this.quanziMQService.publishMsg(publishId);
}

return publishId;
}

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 publishId
* @return
*/
public Long likeComment(String publishId) {
User user = UserThreadLocal.get();

Boolean result = this.quanZiApi.likeComment(user.getId(), publishId);
if (result) {

//发消息
this.quanziMQService.likePublishMsg(publishId);

//查询点赞数
return this.quanZiApi.queryLikeCount(publishId);
}
return null;
}

/**
* 动态的取消点赞
*
* @param publishId
* @return
*/
public Long disLikeComment(String publishId) {
User user = UserThreadLocal.get();

Boolean result = this.quanZiApi.disLikeComment(user.getId(), publishId);
if (result) {
//发消息
this.quanziMQService.disLikePublishMsg(publishId);
//查询点赞数
return this.quanZiApi.queryLikeCount(publishId);
}
return null;
}

public Long loveComment(String publishId) {
User user = UserThreadLocal.get();
//喜欢
Boolean result = this.quanZiApi.loveComment(user.getId(), publishId);
if(result){
//发消息
this.quanziMQService.lovePublishMsg(publishId);
//查询喜欢数
return this.quanZiApi.queryLoveCount(publishId);
}
return null;
}

public Long disLoveComment(String publishId) {
User user = UserThreadLocal.get();
//取消喜欢
Boolean result = this.quanZiApi.disLoveComment(user.getId(), publishId);
if(result){
//发消息
this.quanziMQService.disLovePublishMsg(publishId);
//查询喜欢数
return this.quanZiApi.queryLoveCount(publishId);
}
return null;
}

public QuanZiVo queryById(String publishId) {
Publish publish = this.quanZiApi.queryPublishById(publishId);
if (publish == null) {
return null;
}

//发消息
this.quanziMQService.queryPublishMsg(publishId);

return this.fillQuanZiVo(Arrays.asList(publish)).get(0);
}

/**
* 查询评论列表
*
* @param publishId
* @param page
* @param pageSize
* @return
*/
public PageResult queryCommentList(String publishId, Integer page, Integer pageSize) {

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

User user = UserThreadLocal.get();

//查询评论列表数据
PageInfo<Comment> pageInfo = this.quanZiApi.queryCommentList(publishId, page, pageSize);
List<Comment> records = pageInfo.getRecords();
if(CollUtil.isEmpty(records)){
return pageResult;
}

//查询用户信息
List<Object> userIdList = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIdList);

List<CommentVo> result = new ArrayList<>();
for (Comment record : records) {
CommentVo commentVo = new CommentVo();
commentVo.setContent(record.getContent());
commentVo.setId(record.getId().toHexString());
commentVo.setCreateDate(DateUtil.format(new Date(record.getCreated()), "HH:mm"));
//是否点赞
commentVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), commentVo.getId()) ? 1 : 0);
//点赞数
commentVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(commentVo.getId())));


for (UserInfo userInfo : userInfoList) {
if(ObjectUtil.equals(record.getUserId(), userInfo.getUserId())){

commentVo.setAvatar(userInfo.getLogo());
commentVo.setNickname(userInfo.getNickName());

break;
}
}

result.add(commentVo);
}

pageResult.setItems(result);

return pageResult;
}

/**
* 发表评论
* @param publishId
* @param content
* @return
*/
public Boolean saveComments(String publishId, String content) {
User user = UserThreadLocal.get();
Boolean result = this.quanZiApi.saveComment(user.getId(), publishId, content);

if(result){
//发消息
this.quanziMQService.commentPublishMsg(publishId);
}

return result;
}

public PageResult queryAlbumList(Long userId, Integer page, Integer pageSize) {
PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

//查询数据
PageInfo<Publish> pageInfo = this.quanZiApi.queryAlbumList(userId, page, pageSize);
if(CollUtil.isEmpty(pageInfo.getRecords())){
return pageResult;
}

//填充数据
pageResult.setItems(this.fillQuanZiVo(pageInfo.getRecords()));

return pageResult;
}

public List<VisitorsVo> queryVisitorsList() {
User user = UserThreadLocal.get();
List<Visitors> visitorsList = this.visitorsApi.queryMyVisitor(user.getId());
if (CollUtil.isEmpty(visitorsList)) {
return Collections.emptyList();
}

List<Object> userIds = CollUtil.getFieldValues(visitorsList, "visitorUserId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<VisitorsVo> visitorsVoList = new ArrayList<>();

for (Visitors visitor : visitorsList) {
for (UserInfo userInfo : userInfoList) {
if (ObjectUtil.equals(visitor.getVisitorUserId(), userInfo.getUserId())) {

VisitorsVo visitorsVo = new VisitorsVo();
visitorsVo.setAge(userInfo.getAge());
visitorsVo.setAvatar(userInfo.getLogo());
visitorsVo.setGender(userInfo.getSex().name().toLowerCase());
visitorsVo.setId(userInfo.getUserId());
visitorsVo.setNickname(userInfo.getNickName());
visitorsVo.setTags(StringUtils.split(userInfo.getTags(), ','));
visitorsVo.setFateValue(visitor.getScore().intValue());

visitorsVoList.add(visitorsVo);
break;
}
}
}

return visitorsVoList;
}
}

3.5、接收消息

接收消息的工作需要新创建my-tanhua-recommend工程,在此工程中完成相关的操作。

3.5.1、创建my-tanhua-recommend工程

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
<?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-recommend</artifactId>
<dependencies>
<!--引入interface依赖-->
<dependency>
<groupId>cn.itcast.tanhua</groupId>
<artifactId>my-tanhua-dubbo-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
<!--RocketMQ相关-->
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.apache.rocketmq</groupId>
<artifactId>rocketmq-client</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>

</project>

3.5.2、配置文件

application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring.application.name = itcast-rocketmq
server.port = 18082

# RocketMQ相关配置
rocketmq.name-server=192.168.31.81:9876
rocketmq.producer.group=tanhua

# mongodb相关配置
spring.data.mongodb.username=tanhua
spring.data.mongodb.password=l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV
spring.data.mongodb.authentication-database=admin
spring.data.mongodb.database=tanhua
spring.data.mongodb.port=27017
spring.data.mongodb.host=192.168.31.81

3.5.3、启动类

1
2
3
4
5
6
7
8
9
10
11
12
package com.tanhua.recommend;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class RecommendApplication {

public static void main(String[] args) {
SpringApplication.run(RecommendApplication.class, args);
}
}

3.5.4、RecommendQuanZi

存储到MongoDB的中的实体结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tanhua.recommend.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 = "recommend_quanzi")
public class RecommendQuanZi {

private ObjectId id;
private Long userId;// 用户id
private Long publishId; //动态id,需要转化为Long类型
private Double score; //得分
private Long date; //时间戳
}

3.5.5、QuanZiMsgConsumer

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
package com.tanhua.recommend.msg;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.tanhua.dubbo.server.pojo.Publish;
import com.tanhua.recommend.pojo.RecommendQuanZi;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "tanhua-quanzi",
consumerGroup = "tanhua-quanzi-consumer")
@Slf4j
public class QuanZiMsgConsumer implements RocketMQListener<String> {

@Autowired
private MongoTemplate mongoTemplate;

@Override
public void onMessage(String msg) {
try {
JSONObject jsonObject = JSONUtil.parseObj(msg);

Long userId = jsonObject.getLong("userId");
Long date = jsonObject.getLong("date");
String publishId = jsonObject.getStr("publishId");
Long pid = jsonObject.getLong("pid");
Integer type = jsonObject.getInt("type");

RecommendQuanZi recommendQuanZi = new RecommendQuanZi();
recommendQuanZi.setUserId(userId);
recommendQuanZi.setId(ObjectId.get());
recommendQuanZi.setDate(date);
recommendQuanZi.setPublishId(pid);

//1-发动态,2-浏览动态, 3-点赞, 4-喜欢, 5-评论,6-取消点赞,7-取消喜欢

switch (type) {
case 1: {

Publish publish = this.mongoTemplate.findById(new ObjectId(publishId), Publish.class);
if (ObjectUtil.isNotEmpty(publish)) {
double score = 0d;

//获取图片数
score += CollUtil.size(publish.getMedias());

//获取文本的长度
//文字长度:50以内1分,50~100之间2分,100以上3分
int length = StrUtil.length(publish.getText());
if (length >= 0 && length < 50) {
score += 1;
} else if (length < 100) {
score += 2;
} else {
score += 3;
}

recommendQuanZi.setScore(score);
}

break;
}
case 2: {
recommendQuanZi.setScore(1d);
break;
}
case 3: {
recommendQuanZi.setScore(5d);
break;
}
case 4: {
recommendQuanZi.setScore(8d);
break;
}
case 5: {
recommendQuanZi.setScore(10d);
break;
}
case 6: {
recommendQuanZi.setScore(-5d);
break;
}
case 7: {
recommendQuanZi.setScore(-8d);
break;
}
default: {
recommendQuanZi.setScore(0d);
break;
}

}

//数据保存到MongoDB中
this.mongoTemplate.save(recommendQuanZi);
} catch (Exception e) {
log.error("处理消息出错!msg = " + msg, e);
}
}
}

1.7、测试

测试方法:使用APP进行操作,可以看到在MongoDB中已经有数据写入。

image-20210126160159662

4、部署推荐系统

在推荐系统中,我们将基于前面写入到推荐表中的数据通过Spark进行计算,在Spark计算完成后将结果写入到Redis中,以供在业务系统中进行查询。

推荐服务我们将基于docker的形式进行部署:

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
#拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/itcast/tanhua-spark-quanzi:1.0

#创建容器
docker create --name tanhua-spark-quanzi \
--env MONGODB_HOST=192.168.31.81 \
--env MONGODB_PORT=27017 \
--env MONGODB_USERNAME=tanhua \
--env MONGODB_PASSWORD=l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV \
--env MONGODB_DATABASE=tanhua \
--env MONGODB_COLLECTION=recommend_quanzi \
--env SCHEDULE_PERIOD=10 \
--env REDIS_NODES="192.168.31.81:6379,192.168.31.81:6380,192.168.31.81:6381" \
registry.cn-hangzhou.aliyuncs.com/itcast/tanhua-spark-quanzi:1.0

#参数说明
#MONGODB_HOST mongodb服务的地址
#MONGODB_PORT mongodb服务的端口
#MONGODB_USERNAME mongodb服务的认证用户名
#MONGODB_PASSWORD mongodb服务的认证密码
#MONGODB_DATABASE mongodb连接的数据库
#MONGODB_COLLECTION 操作表
#SCHEDULE_PERIOD 下次执行时间间隔,但是为分,默认为10分钟
#REDIS_NODES redis集群地址,也可以使用单节点

#mongodb开启认证服务
#docker create --name mongodb --restart=always -p 27017:27017 -v mongodb:/data/db mongo:4.0.3 --auth

#启动服务,启动之后就会进行执行,在SCHEDULE_PERIOD时间后再次执行
docker start tanhua-spark-quanzi

#查看日志
docker logs -f tanhua-spark-quanzi

#执行完成后会将数据写入到redis中

进入redis查看是否已经有数据: image-20200821104559656

5、小视频推荐

小视频的推荐和动态推荐的实现逻辑非常的类似。

5.1、动态计分规则

  • 发布+2
  • 点赞 +5
  • 评论 + 10

5.2、发送消息

5.2.1、VideoMQService

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
package com.tanhua.server.service;

import com.alibaba.dubbo.config.annotation.Reference;
import com.tanhua.common.pojo.User;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.dubbo.server.api.VideoApi;
import com.tanhua.dubbo.server.pojo.Video;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.core.RocketMQTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class VideoMQService {

@Autowired
private RocketMQTemplate rocketMQTemplate;

@Reference(version = "1.0.0")
private VideoApi videoApi;

/**
* 发布小视频消息
*
* @return
*/
public Boolean videoMsg(String videoId) {
return this.sendMsg(videoId, 1);
}

/**
* 点赞小视频
*
* @return
*/
public Boolean likeVideoMsg(String videoId) {
return this.sendMsg(videoId, 2);
}

/**
* 取消点赞小视频
*
* @return
*/
public Boolean disLikeVideoMsg(String videoId) {
return this.sendMsg(videoId, 3);
}

/**
* 评论小视频
*
* @return
*/
public Boolean commentVideoMsg(String videoId) {
return this.sendMsg(videoId, 4);
}

/**
* 发送小视频操作相关的消息
*
* @param videoId
* @param type 1-发动态,2-点赞, 3-取消点赞,4-评论
* @return
*/
private Boolean sendMsg(String videoId, Integer type) {
try {
User user = UserThreadLocal.get();

Video video = this.videoApi.queryVideoById(videoId);

//构建消息
Map<String, Object> msg = new HashMap<>();
msg.put("userId", user.getId());
msg.put("date", System.currentTimeMillis());
msg.put("videoId", videoId);
msg.put("vid", video.getVid());
msg.put("type", type);

this.rocketMQTemplate.convertAndSend("tanhua-video", msg);
} catch (Exception e) {
log.error("发送消息失败! videoId = " + videoId + ", type = " + type, e);
return false;
}

return true;
}
}

4.3.2、VideoService

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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
package com.tanhua.server.service;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.dubbo.config.annotation.Reference;
import com.github.tobato.fastdfs.domain.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.fdfs.StorePath;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import com.tanhua.common.pojo.User;
import com.tanhua.common.pojo.UserInfo;
import com.tanhua.common.service.PicUploadService;
import com.tanhua.common.utils.UserThreadLocal;
import com.tanhua.common.vo.PicUploadResult;
import com.tanhua.dubbo.server.api.QuanZiApi;
import com.tanhua.dubbo.server.api.VideoApi;
import com.tanhua.dubbo.server.pojo.Video;
import com.tanhua.dubbo.server.vo.PageInfo;
import com.tanhua.server.vo.PageResult;
import com.tanhua.server.vo.VideoVo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@Service
@Slf4j
public class VideoService {

@Reference(version = "1.0.0")
private VideoApi videoApi;

@Autowired
private PicUploadService picUploadService;

@Autowired
protected FastFileStorageClient storageClient;

@Autowired
private FdfsWebServer fdfsWebServer;

@Autowired
private UserInfoService userInfoService;

@Reference(version = "1.0.0")
private QuanZiApi quanZiApi;

@Autowired
private QuanZiService quanZiService;

@Autowired
private VideoMQService videoMQService;

/**
* 小视频上传
*
* @param picFile 封面图片
* @param videoFile 视频文件
* @return
*/
public Boolean saveVideo(MultipartFile picFile, MultipartFile videoFile) {

User user = UserThreadLocal.get();

Video video = new Video();
video.setUserId(user.getId());
video.setSeeType(1);

try {
//上传图片到阿里云oss
PicUploadResult uploadResult = this.picUploadService.upload(picFile);
video.setPicUrl(uploadResult.getName());

//上传视频到FastDFS中
StorePath storePath = this.storageClient.uploadFile(videoFile.getInputStream(),
videoFile.getSize(),
StrUtil.subAfter(videoFile.getOriginalFilename(), '.', true),
null);
video.setVideoUrl(fdfsWebServer.getWebServerUrl() + storePath.getFullPath());


String videoId = this.videoApi.saveVideo(video);
if(StrUtil.isNotEmpty(videoId)){
//发送消息
this.videoMQService.videoMsg(videoId);
}

return StrUtil.isNotEmpty(videoId);
} catch (IOException e) {
log.error("上传小视频出错~ userId = " + user.getId() + ", file = " + videoFile.getOriginalFilename(), e);
}
return null;
}

public PageResult queryVideoList(Integer page, Integer pageSize) {
User user = UserThreadLocal.get();

PageResult pageResult = new PageResult();
pageResult.setPage(page);
pageResult.setPagesize(pageSize);

PageInfo<Video> pageInfo = this.videoApi.queryVideoList(user.getId(), page, pageSize);
List<Video> records = pageInfo.getRecords();

if (CollUtil.isEmpty(records)) {
return pageResult;
}

//查询用户信息
List<Object> userIds = CollUtil.getFieldValues(records, "userId");
List<UserInfo> userInfoList = this.userInfoService.queryUserInfoByUserIdList(userIds);

List<VideoVo> videoVoList = new ArrayList<>();
for (Video record : records) {
VideoVo videoVo = new VideoVo();

videoVo.setUserId(record.getUserId());
videoVo.setCover(record.getPicUrl());
videoVo.setVideoUrl(record.getVideoUrl());
videoVo.setId(record.getId().toHexString());
videoVo.setSignature("我就是我~"); //TODO 签名

videoVo.setCommentCount(Convert.toInt(this.quanZiApi.queryCommentCount(videoVo.getId()))); //评论数
videoVo.setHasFocus(this.videoApi.isFollowUser(user.getId(), videoVo.getUserId()) ? 1 : 0); //是否关注
videoVo.setHasLiked(this.quanZiApi.queryUserIsLike(user.getId(), videoVo.getId()) ? 1 : 0); //是否点赞(1是,0否)
videoVo.setLikeCount(Convert.toInt(this.quanZiApi.queryLikeCount(videoVo.getId())));//点赞数

//填充用户信息
for (UserInfo userInfo : userInfoList) {
if (ObjectUtil.equals(videoVo.getUserId(), userInfo.getUserId())) {
videoVo.setNickname(userInfo.getNickName());
videoVo.setAvatar(userInfo.getLogo());
break;
}
}

videoVoList.add(videoVo);
}

pageResult.setItems(videoVoList);
return pageResult;
}

/**
* 点赞
*
* @param videoId
* @return
*/
public Long likeComment(String videoId) {
User user = UserThreadLocal.get();
Boolean result = this.quanZiApi.likeComment(user.getId(), videoId);
if (result) {

//发送消息
this.videoMQService.likeVideoMsg(videoId);

return this.quanZiApi.queryLikeCount(videoId);
}
return null;
}

/**
* 取消点赞
*
* @param videoId
* @return
*/
public Long disLikeComment(String videoId) {
User user = UserThreadLocal.get();
Boolean result = this.quanZiApi.disLikeComment(user.getId(), videoId);
if (result) {

//发送消息
this.videoMQService.disLikeVideoMsg(videoId);

return this.quanZiApi.queryLikeCount(videoId);
}
return null;
}

public Boolean saveComment(String videoId, String content) {
Boolean result = this.quanZiService.saveComments(videoId, content);

if(result){
//发送消息
this.videoMQService.commentVideoMsg(videoId);
}

return result;
}

public PageResult queryCommentList(String videoId, Integer page, Integer pageSize) {
return this.quanZiService.queryCommentList(videoId, page, pageSize);
}

/**
* 关注用户
*
* @param userId
* @return
*/
public Boolean followUser(Long userId) {
User user = UserThreadLocal.get();
return this.videoApi.followUser(user.getId(), userId);
}

/**
* 取消关注
*
* @param userId
* @return
*/
public Boolean disFollowUser(Long userId) {
User user = UserThreadLocal.get();
return this.videoApi.disFollowUser(user.getId(), userId);
}
}

5.3、接收消息

5.3.1、RecommendVideo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.tanhua.recommend.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 = "recommend_video")
public class RecommendVideo {

private ObjectId id;
private Long userId;// 用户id
private Long videoId; //视频id,需要转化为Long类型
private Double score; //得分
private Long date; //时间戳
}

5.3.2、VideoMsgConsumer

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
package com.tanhua.recommend.msg;

import cn.hutool.json.JSONObject;
import cn.hutool.json.JSONUtil;
import com.tanhua.recommend.pojo.RecommendVideo;
import lombok.extern.slf4j.Slf4j;
import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
import org.apache.rocketmq.spring.core.RocketMQListener;
import org.bson.types.ObjectId;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.stereotype.Component;

@Component
@RocketMQMessageListener(topic = "tanhua-video",
consumerGroup = "tanhua-video-consumer")
@Slf4j
public class VideoMsgConsumer implements RocketMQListener<String> {

@Autowired
private MongoTemplate mongoTemplate;

@Override
public void onMessage(String msg) {
try {
JSONObject jsonObject = JSONUtil.parseObj(msg);
Long userId = jsonObject.getLong("userId");
Long vid = jsonObject.getLong("vid");
Integer type = jsonObject.getInt("type");

//1-发动态,2-点赞, 3-取消点赞,4-评论
RecommendVideo recommendVideo = new RecommendVideo();
recommendVideo.setUserId(userId);
recommendVideo.setId(ObjectId.get());
recommendVideo.setDate(System.currentTimeMillis());
recommendVideo.setVideoId(vid);

switch (type) {
case 1: {
recommendVideo.setScore(2d);
break;
}
case 2: {
recommendVideo.setScore(5d);
break;
}
case 3: {
recommendVideo.setScore(-5d);
break;
}
case 4: {
recommendVideo.setScore(10d);
break;
}
default: {
recommendVideo.setScore(0d);
break;
}
}

this.mongoTemplate.save(recommendVideo);

} catch (Exception e) {
log.error("处理小视频消息失败~" + msg, e);
}
}
}

5.3.3、测试

image-20210126172911927

可以看到,用户1对于视频有点赞、取消点赞、评论等操作。

5.4、部署推荐服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#拉取镜像
docker pull registry.cn-hangzhou.aliyuncs.com/itcast/tanhua-spark-video:1.0

#创建容器
docker create --name tanhua-spark-video \
--env MONGODB_HOST=192.168.31.81 \
--env MONGODB_PORT=27017 \
--env MONGODB_USERNAME=tanhua \
--env MONGODB_PASSWORD=l3SCjl0HvmSkTtiSbN0Swv40spYnHhDV \
--env MONGODB_DATABASE=tanhua \
--env MONGODB_COLLECTION=recommend_video \
--env SCHEDULE_PERIOD=10 \
--env REDIS_NODES="192.168.31.81:6379,192.168.31.81:6380,192.168.31.81:6381" \
registry.cn-hangzhou.aliyuncs.com/itcast/tanhua-spark-video:1.0

#启动服务
docker start tanhua-spark-video

#查看日志
docker logs -f tanhua-spark-video

测试:

1570452212069