《探花交友》
了解项目的背景
了解项目的技术架构、方案
了解项目的功能设计
掌握模拟器的使用
掌握工程的搭建
掌握发送短信验证码
完成用户登录功能
课程介绍
1563457093887-166177245506021功能介绍
项目介绍
工程搭建
短信验证码
实现用户登录功能
1、功能介绍 探花交友是一个陌生人的在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台还会通过大数据计算进行智能推荐,通过智能推荐可以找到更加匹配的好友,这样才能增进用户对产品的喜爱度。探花平台还提供了在线即时通讯功能,可以实时的与好友进行沟通,让沟通随时随地的进行。
1.1、功能列表
功能
说明
备注
注册、登录
用户无需单独注册,直接通过手机号登录即可
首次登录成功后需要完善个人信息
交友
主要功能有:测灵魂、桃花传音、搜附近、探花等
圈子
类似微信朋友圈,用户可以发动态、查看好友动态等
消息
通知类消息 + 即时通讯消息
小视频
类似抖音,用户可以发小视频,评论等
显示小视频列表需要进行推荐算法计算后进行展现。
我的
我的动态、关注数、粉丝数、通用设置等
1.2、注册登录 业务说明:
用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
1.3、交友 交友是探花项目的核心功能之一,用户可以查看好友,添加好友,搜索好友等操作。
1.3.1、首页 在首页中,主要功能有“今日佳人”、“推荐”、“最近访客”等
今日佳人
按照“缘分值”进行匹配,将“缘分值”最高的用户展现出来
推荐
按照“缘分值”进行推荐,由后台的推荐系统计算得出,展现出来
最近访客
1.3.2、探花
说明:左划喜欢,右划不喜欢,每天限量不超过100个,开通会员可增加限额。双方互相喜欢则配对成功。
实现:数据来源推荐系统计算后的结果。
1.3.3、搜附近
根据用户当前所在的位置进行查询,并且在10km的范围内进行查询,可以通过筛选按钮进行条件筛选。
1.3.4、桃花传音 功能类似QQ中的漂流瓶,用户可以发送和接收语音消息,陌生人就会接收到消息。
1.3.5、测灵魂
测试题用于对用户进行分类,每次提交答案后更新用户属性
测试题在后台进行维护
测试题测试完后产生结果页可以进行分享
测试题为顺序回答,回答完初级题解锁下一级问题
点击锁定问题 显示提示 请先回答上一级问题
1.4、圈子 1、推荐频道为根据问卷及喜好推荐相似用户动态
2、显示内容为用户头像、用户昵称、用户性别、用户年龄、用户标签和用户发布动态
3、图片最多不超过6张或发布一个小视频
4、动态下方显示发布时间距离当时时间,例如10分钟前、3小时前、2天前,显示时间进行取整
5、动态下方显示距离为发布动态地与本地距离
6、显示用户浏览量
7、显示点赞数、评论数 转发数
1.5、消息 消息包含通知类的消息和好友消息。
1.6、小视频 用户可以上传小视频,也可以查看小视频列表,并且可以进行点赞操作。
1.7、我的 显示关注数、喜欢数、粉丝数、我的动态等信息。
2、项目介绍(★★★★★) 2.1、项目背景 在线社交是互联网时代的产物,已成为互联网用户的基础需求之一。移动互联网自2003年起快速发展,促使在线社交逐渐从PC端转移至移动端。移动社交最初以熟人社交为主,以维系熟人关系、共享资源信息的形式存在。随着人们交友需求的延伸,移动社交开始向陌生人社交、兴趣社交等垂直方向发展,形式丰富多样。
2.2、市场分析 探花交友项目定位于 陌生人交友市场 。
根据《2018社交领域投融资报告》中指出:虽然相比2017年,投融资事件减少29.5%,但是融资的总额却大幅增长,达到68%。
这些迹象说明:社交领域的发展规模正在扩大,而很多没有特色的产品也会被淘汰。而随着那些尾部产品的倒下,对我们来说就是机会,及时抓住不同社交需求的机会。以社交为核心向不同的细分领域衍生正在逐渐走向成熟化。
而我们按照娱乐形式和内容为主两个维度,将社交行业公司分类为:即时通信、内容社群、陌生人社交、泛娱乐社交以及兴趣社交几个领域。
而在2018年社交的各个细分领域下,均有备受资本所关注的项目,根据烯牛数据2018年的报告中,也同样指出:内容社交及陌生人社交为资本重要关注领域,合计融资占比达73%。
根据市场现状以及融资事件来看:陌生人社交、内容社群、兴趣社交在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、技术架构
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
接口定义:
mock数据,YApi提供了mock功能,就是模拟服务端返回测试数据:
还可以运行http请求(需要在Chrome中安装支持跨域扩展 https://juejin.im/post/6844904057707085832):
2.9、基础环境 探花交友项目的开发统一使用提供的Centos7环境,该环境中部署安装了项目所需要的各种服务,如:MySQL、MongoDB、Redis、RocketMQ等。
虚拟机的root用户密码为:root123
虚拟机的mysql密码为:root
默认参数:CPU:2核,内存:4G,硬盘:60G
IP地址建议设置为192.168.31.81,否则有些服务将不可用,比如:Redis、RocketMQ等。
拷贝虚拟机需要修改这个网段
登录注册功能 3、注册登录 业务说明:
用户通过手机验证码进行登录,如果是第一次登录则需要完善个人信息,在上传图片时,需要对上传的图片做人像的校验,防止用户上传非人像的图片作为头像。流程完成后,则登录成功。
流程:
3.1、单点登录系统 为什么要使用单点登录系统?
以前实现的登录和注册是在同一个tomcat内部完成,我们现在的系统架构是每一个系统都是由一个团队进行维护,每个系统都是单独部署运行一个单独的tomcat,所以,不能将用户的登录信息保存到session中(多个tomcat的session是不能共享的),所以我们需要一个单独的系统来维护用户的登录信息。
SSO在整个系统架构中的应用
由上图可以看出:
客户端需要通过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 > <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 > <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 > <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 > <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 > <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 > <dependency > <groupId > com.alibaba.boot</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > <version > 0.2.0</version > </dependency > <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 > <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 > <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/
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_ mybatis-plus.global-config.db-config.id-type =auto 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.name-server =192.168.31.81:9876 rocketmq.producer.group =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插件:
如果不安装插件,程序可以正常执行,但是看不到生成的一些代码,如: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注解
是不是很神奇?!
测试二:使用@Slf4j注解
测试:
测试三:@AllArgsConstructor、@NoArgsConstructor注解的使用
测试四:@Builder
测试结果:
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) 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; }
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; 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; }
对自动填充字段的处理:
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") @SpringBootApplication public class MyApplication { public static void main (String[] args) { SpringApplication.run(MyApplication.class, args); } }
3.6、短信验证码 发送短信验证码的流程:
流程说明:
用户向SSO系统发送请求,在请求中传递手机号;
SSO系统接收到请求后,生成随机验证码以及短信内容,请求阿里云短信服务;
阿里云短信服务接收到请求后,会进行一系列的验证,比如账号余额、短信模板是否正确等,最后向运营商发起请求;
运营商接收到请求后,向该手机号下发短信,用户即可收到短信;
3.6.1、阿里云短信服务 3.6.1.1、申请签名与模板 https://dysms.console.aliyun.com/dysms.htm?spm=5176.12818093.0.ddysms.2a4316d0ql6PyD
说明:申请签名时,个人用户只能申请一个并且签名的名称必须为“ABC商城”,否则审核不通过。
申请模板:
审核时间需要1~2
小时,请耐心等待~
3.6.1.2、设置用户权限 在阿里云中,需要在RAM服务中创建用户以及权限,才能通过api进行访问接口。
创建用户:
创建完成后要保存AccessKey Secret和AccessKey ID,AccessKey Secret只显示这一次,后面将不再显示。
添加权限:
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;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" ); request.putQueryParameter("TemplateParam" , "{\"code\":\"123456\"}" ); try { CommonResponse response = client.getCommonResponse(request); 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
需要注意中文编码问题:
读取配置:
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 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; 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()); 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
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; @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; 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()); 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 ; } 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(); } 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" ); String jwt = Jwts.builder() .setHeader(header) .setClaims(claims) .signWith(SignatureAlgorithm.HS256, secret) .setExpiration(new Date (System.currentTimeMillis() + 3000 )) .compact(); System.out.println(jwt); } @Test public void testDecodeToken () { String token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJtb2JpbGUiOiIxMzMzMzMzMzMzIiwiaWQiOiIyIiwiZXhwIjoxNjA1NTEzMDA2fQ.1eG3LpudD4XBycUG39UQDaKVBQHgaup-E1OLWo_m8m8" ; try { Map<String, Object> body = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); System.out.println(body); } catch (ExpiredJwtException e) { System.out.println("token已经过期!" ); } catch (Exception e) { System.out.println("token不合法!" ); } } }
3.8、用户登录 用户接收到验证码后,进行输入验证码,点击登录,前端系统将手机号以及验证码提交到SSO进行校验。
3.7.1、mock接口 接口地址:https://mock-java.itheima.net/project/164/interface/api/12593
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; @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; 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 ; } Map<String, Object> claims = new HashMap <String, Object>(); claims.put("id" , user.getId()); String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS256, secret) .setExpiration(new DateTime ().plusHours(12 ).toDate()) .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、测试
完善个人信息 课程介绍
完善个人信息
阿里云OSS服务应用
人脸识别
MongoDB快速入门
SpringBoot整合MongoDB
1、完善个人信息 用户在首次登录时需要完善个人信息,包括性别、昵称、生日、城市、头像等。
其中,头像数据需要做图片上传,这里采用阿里云的OSS服务作为我们的图片服务器,并且对头像要做人脸识别,非人脸照片不得上传。
1.1、图片上传 1.1.1、图片存储解决方案 实现图片上传服务,需要有存储的支持,那么我们的解决方案将以下几种:
直接将图片保存到服务的硬盘
优点:开发便捷,成本低
缺点:扩容困难
使用分布式文件系统进行存储
优点:容易实现扩容
缺点:开发复杂度稍大(有成熟的产品可以使用,比如:FastDFS)
使用nfs做存储
优点:开发较为便捷
缺点:需要有一定的运维知识进行部署和维护
使用第三方的存储服务
优点:开发简单,拥有强大功能,免维护
缺点:付费
在本套课程中选用阿里云的OSS服务进行图片存储。
1.1.2、阿里云OSS存储 流程:
1.1.2.1、什么是OSS服务? 地址:https://www.aliyun.com/product/oss
1.1.2.2、购买服务 使用第三方服务最大的缺点就是需要付费,下面,我们看下如何购买开通服务。
购买下行流量包: (不购买也可以使用,按照流量付费)
说明:OSS的上行流量是免费的,但是下行流量是需要购买的。
1.1.2.3、创建Bucket 使用OSS,首先需要创建Bucket,Bucket翻译成中文是水桶的意思,把存储的图片资源看做是水,想要盛水必须得有桶,就是这个意思了。
进入控制台,https://oss.console.aliyun.com/overview
选择Bucket后,即可看到对应的信息,如:url、消耗流量等 :
文件管理:
查看文件:
1.1.2.4、创建用户 创建用户的方式与短信接口中的方式一样,需要设置oss权限。
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 { 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; private String status; 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、测试
1.2、人脸识别 人脸识别技术采用虹软开放平台实现(免费使用)。官网:https://www.arcsoft.com.cn/
1.2.1、使用说明 使用虹软平台需要先注册开发者账号:https://ai.arcsoft.com.cn/ucenter/user/userlogin
注册完成后进行登录,然后进行创建应用:
创建完成后,需要进行实名认证,否则相关的SDK是不能使用的。
实名认证后即可下载对应平台的SDk,我们需要下载windows以及linux平台。
添加SDK(Linux与Windows平台):
下载SDK,打开解压包,可以看到有提供相应的jar包以及示例代码:
需要特别说明的是:每个账号的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 > </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 (); 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; } 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); } }
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) { 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) { User user = this .userService.queryUserByToken(token); if (null == user) { return false ; } try { boolean b = this .faceEngineService.checkIsPortrait(file.getBytes()); if (!b) { return false ; } } catch (IOException e) { e.printStackTrace(); } 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; @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); } @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、测试
图片上传超过1MB出错的解决方案:
1 2 3 4 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 @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 { Map<String, Object> body = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); User user = new User (); user.setId(Long.valueOf(body.get("id" ).toString())); 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()); 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、测试
数据已经存储到redis中:
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字段设置为主键
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() 方法,语法格式如下:
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。
查看数据:
或使用Navicat Premium 15:
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 > <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.application.name = itcast-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 =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、首页 在用户登录成功后,就会进入首页,首页中有今日佳人、推荐好友、探花、搜附近等功能。
2、系统架构 在开发完SSO系统中的登录功能后,接下来就需要实现其他的功能,在整体架构中,完成与APP对接的服务工程叫my-tanhua-server,真正的核心业务逻辑使用dubbo完成,其工程名叫:my-tanhua-dubbo,它们的架构示意图如下:
说明:
客户端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 启动:
重启加载配置文件命令:nginx.exe -s reload
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、测试
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 > <dependency > <groupId > com.alibaba.boot</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > dubbo</artifactId > </dependency > <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_ mybatis-plus.global-config.db-config.id-type =auto 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 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") @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 > <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 > com.alibaba.boot</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > dubbo</artifactId > </dependency > <dependency > <groupId > org.apache.zookeeper</groupId > <artifactId > zookeeper</artifactId > </dependency > <dependency > <groupId > com.github.sgroschupf</groupId > <artifactId > zkclient</artifactId > </dependency > <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.application.name = itcast-tanhua-dubbo-service dubbo.scan.basePackages = com.tanhua.dubbo.server dubbo.application.name = dubbo-provider-tanhua dubbo.protocol.name = dubbo dubbo.protocol.port = 20880 dubbo.registry.address = zookeeper://192.168.31.81:2181 dubbo.registry.client = zkclient dubbo.registry.timeout = 60000 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、工程结构 最终搭建完成的效果如下:
各工程之间的关系如下:
3、今日佳人 今日佳人,会推荐缘分值最大的用户,进行展现出来。缘分值的计算是由用户的行为进行打分,如:点击、点赞、评论、学历、婚姻状态等信息组合而成的。
实现:我们先不考虑推荐的逻辑,假设现在已经有推荐的结果,我们只需要从结果中查询到缘分值最高的用户就可以了。至于推荐的逻辑以及实现,我们将后面的课程中讲解。
流程:
3.1、表结构 1 2 3 4 5 6 7 # 表结构,表名:recommend_user { "userId":1001, #推荐的用户id "toUserId":1002, #用户id "score":90, #推荐得分 "date":"2019/1/1" #日期 }
已经提供的测试数据(4855条数据):
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 { RecommendUser queryWithMaxScore (Long userId) ; 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; @Indexed private Long userId; private Long toUserId; @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") 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
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; }
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; 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; 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; @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) { User user = this .userService.queryUserByToken(token); if (null == user) { 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 (); 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 ); factory.setConnectTimeout(5000 ); 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;@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()); 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服务启动完成后进行测试。
3.3.5、解决MongoDB启动bug 在项目中,添加了mongo的依赖的话,springboot就会自动去连接本地的mongo,由于他连接不上会导致出错。
解决:
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") @SpringBootApplication(exclude = {MongoAutoConfiguration.class, MongoDataAutoConfiguration.class}) public class ServerApplication { public static void main (String[] args) { SpringApplication.run(ServerApplication.class, args); } }
4、推荐列表
4.1、mock接口 地址:https://mock-java.itheima.net/project/35/interface/api/623
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; 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 @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 public PageResult queryRecommendation (String token, RecommendUserQueryParam queryParam) { User user = this .userService.queryUserByToken(token); if (null == user) { 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; } Set<Long> userIds = new HashSet <>(); for (RecommendUser record : records) { userIds.add(record.getUserId()); } QueryWrapper<UserInfo> queryWrapper = new QueryWrapper <>(); 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()); 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 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 public List<UserInfo> queryUserInfoList (QueryWrapper queryWrapper) { return this .userInfoMapper.selectList(queryWrapper); }
4.6、测试
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.*;@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) @Documented public @interface Cache { 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 ; } if (!(handler instanceof HandlerMethod)) { return true ; } if (!((HandlerMethod) handler).hasMethodAnnotation(GetMapping.class)) { return true ; } 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 ; } response.setCharacterEncoding("UTF-8" ); response.setContentType("application/json; charset=utf-8" ); response.getWriter().write(cacheData); return false ; } 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 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) { 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、测试
可以看到数据已经缓存到Redis中,并且其缓存时间也是30秒,与预期一致。
6、整合测试 测试时需要注意,由于用户数据较少,所以测试时需要把条件注释掉,否则查询不到数据:
效果:
圈子功能实现 课程说明
抽取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工程:
1.5、抽取utils 将server工程的utils进行抽取公用,后续的工具类也放置到common工程中。
抽取完成后进行测试,确保可以正常启动以及功能都正常。
2、圈子功能 2.1、功能说明 探花交友项目中的圈子功能,类似微信的朋友圈,基本的功能为:发布动态、浏览好友动态、浏览推荐动态、点赞、评论、喜欢等功能。
发布:
2.2、实现方案分析 对于圈子功能的实现,我们需要对它的功能特点做分析:
数据量会随着用户数增大而增大
读多写少,一般而言,浏览朋友圈动态会多一些,发动态相对就会少一些
非好友看不到其动态内容
……
针对以上特点,我们来分析一下:
对于数据量大而言,显然不能够使用关系型数据库进行存储,我们需要通过MongoDB进行存储
对于读多写少的应用,尽可能的减少读取数据的成本
比如说,一条SQL语句,单张表查询一定比多张表查询要快
条件越多的查询速度将越慢,尽可能的减少条件以提升查询速度
所以对于存储而言,主要是核心的4张表:
发布表:记录了所有用户的发布的东西信息,如图片、视频等。
相册:相册是每个用户独立的,记录了该用户所发布的所有内容。
评论:针对某个具体发布的朋友评论和点赞操作。
时间线:所谓“刷朋友圈”,就是刷时间线,就是一个用户所有的朋友的发布内容。
流程:
流程说明:
用户发布动态,动态中一般包含了图片和文字,图片上传到阿里云,上传成功后拿到图片地址,将文字和图片地址进行持久化存储
首先,需要将动态数据写入到发布表中,其次,再写入到自己的相册表中,需要注意的是,相册表中只包含了发布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; private Long friendId; private Long date; }
在mock数据中,为每个用户构造了10个好友数据:
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; private Long pid; private Long userId; private String text; private List<String> medias; private Integer seeType; 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; private ObjectId publishId; 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; private ObjectId publishId; 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; private Integer commentType; private String content; private Long userId; private Long publishUserId; private Boolean isParent = false ; private ObjectId parentId; 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 { 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; } List<Object> ids = CollUtil.getFieldValues(timeLineList, "publishId" ); 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 <dependency > <groupId > cn.hutool</groupId > <artifactId > hutool-all</artifactId > <version > 5.5.2</version > </dependency > <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)); } }
测试结果:
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; private Long userId; private String avatar; private String nickname; private String gender; private Integer age; private String[] tags; private String textContent; private String[] imageContent; private String distance; private String createDate; private Integer likeCount; private Integer commentCount; private Integer loveCount; private Integer hasLiked; private Integer hasLoved; }
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; @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) { PageResult pageResult = new PageResult (); pageResult.setPage(page); pageResult.setPagesize(pageSize); User user = this .userService.queryUserByToken(token); if (user == null ) { return pageResult; } 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; } 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 ); quanZiVo.setDistance("1.2公里" ); quanZiVo.setHasLiked(0 ); quanZiVo.setLikeCount(0 ); quanZiVo.setHasLoved(0 ); quanZiVo.setLoveCount(0 ); } }
1 2 3 4 5 6 7 8 9 10 11 12 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; private Long userId; @Alias("logo") private String avatar; @Alias("nickName") private String nickname; private String gender; private Integer age; private String[] tags; private String textContent; private String[] imageContent; private String distance; private String createDate; private Integer likeCount; private Integer commentCount; private Integer loveCount; private Integer hasLiked; private Integer hasLoved; }
4.3.4、测试
5、统一校验token 在之前的开发中,我们会在每一个Service中对token做处理,相同的逻辑一定是要进行统一处理的,该如何处理呢?
由于程序是运行在web容器中,每一个HTTP请求都是一个独立线程,也就是可以理解成我们编写的应用程序运行在一个多线程的环境中,那么我们就可以使用ThreadLocal在HTTP请求的生命周期内进行存值、取值操作。
如下图:
说明:
用户的每一个请求,都是一个独立的线程
图中的TL就是ThreadLocal,一旦将数据绑定到ThreadLocal中,那么在整个请求的生命周期内都可以随时拿到ThreadLocal中当前线程的数据。
根据上面的分析,我们只需要在Controller请求之前进行对token做校验,如果token有效,则会拿到User对象,然后将该User对象保存到ThreadLocal中即可,最后放行请求,在后续的各个环节中都可以获取到该数据了。
如果token无效,给客户端响应401状态码,拦截请求,不再放行到Controller中。
由此可见,这个校验的逻辑是比较适合放在拦截器中完成的。
5.1、编写UserThreadLocal 在my-tanhua-common工程中,编写UserThreadLocal。
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 () { } public static void set (User user) { LOCAL.set(user); } public static User get () { return LOCAL.get(); } 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 { if (!(handler instanceof HandlerMethod)) { return true ; } if (((HandlerMethod) handler).hasMethodAnnotation(NoAuthorization.class)) { return true ; } String token = request.getHeader("Authorization" ); if (StrUtil.isNotEmpty(token)){ User user = this .userService.queryUserByToken(token); if (user != null ){ UserThreadLocal.set(user); return true ; } } response.setStatus(401 ); return false ; } @Override public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { 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 public PageResult queryPublishList (Integer page, Integer pageSize) { PageResult pageResult = new PageResult (); pageResult.setPage(page); pageResult.setPagesize(pageSize); User user = UserThreadLocal.get(); PageInfo<Publish> pageInfo = this .quanZiApi.queryPublishList(user.getId(), page, pageSize); return pageResult; }
需要注意的是,在APP中,如果请求响应401,会跳转到登录页面。
6、发布动态 用户可以在圈子中发布动态,动态内容中可以有文字和图片。如下图:
6.1、dubbo服务 6.1.1、定义接口 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 { PageInfo<Publish> queryPublishList (Long userId, Integer page, Integer pageSize) ; 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 public String savePublish (Publish publish) { if (!ObjectUtil.isAllNotEmpty(publish.getText(), publish.getUserId())) { return null ; } publish.setId(ObjectId.get()); try { publish.setPid(this .idService.createId(IdType.PUBLISH)); publish.setCreated(System.currentTimeMillis()); 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) { 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;@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); 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
从接口中可以看出,主要的参数有:文字、图片、位置等内容。
6.2.1、图片上传 图片上传功能原来是在sso中完成的,为了能公用该功能,所以需要将图片上传的Service以及配置移动至common工程中。
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点:
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 @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 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 @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; } List<String> pids = StrUtil.split(data, ',' ); 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; } 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 @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 public PageResult queryRecommendPublishList (Integer page, Integer pageSize) { PageResult pageResult = new PageResult (); pageResult.setPage(page); pageResult.setPagesize(pageSize); User user = UserThreadLocal.get(); 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 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 ); quanZiVo.setDistance("1.2公里" ); quanZiVo.setHasLiked(0 ); quanZiVo.setLikeCount(0 ); quanZiVo.setHasLoved(0 ); quanZiVo.setLoveCount(0 ); } 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、测试
圈子和小视频 课程说明
圈子实现点赞、喜欢功能
圈子实现评论
圈子实现评论的点赞
小视频功能介绍
FastDFS入门学习
实现发布小视频功能
实现查询小视频列表功能
1、圈子点赞实现分析 在圈子功能中,对于圈子的点赞、喜欢、评论等均可理解为用户对动态的评论(Comment),在quanzi_comment表中使用commentType进行区分。
在具体的实现中,需要将点赞数、某用户是否点赞等数据保存到Reds中,以减轻MongoDB的压力。
具体存储结构如下:
说明:在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;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 { Publish queryPublishById (String id) ; Boolean likeComment (Long userId, String publishId) ; Boolean disLikeComment (Long userId, String publishId) ; Long queryLikeCount (String publishId) ; 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 { 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 ; } Boolean result = this .saveComment(userId, publishId, CommentType.LIKE, null ); if (!result) { return false ; } 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 ; } 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) { 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); } Long count = this .queryCommentCount(publishId, CommentType.LIKE); this .redisTemplate.opsForHash().put(redisKey, hashKey, String.valueOf(count)); return count; } @Override public Boolean queryUserIsLike (Long userId, String publishId) { 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" ); } 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 ; } this .redisTemplate.opsForHash().put(redisKey, userHashKey, "1" ); return true ; } 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); 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 ; } 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 ; } 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
从接口文档来看,点赞完成后需要返回点赞数。
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 @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(); } @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 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 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 ); quanZiVo.setDistance("1.2公里" ); quanZiVo.setHasLiked(this .quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0 ); quanZiVo.setLikeCount(Convert.toInt(this .quanZiApi.queryLikeCount(quanZiVo.getId()))); quanZiVo.setHasLoved(0 ); quanZiVo.setLoveCount(0 ); }
2.3.4、测试
从测试结果中可以看出,在响应结果中返回了点赞数以及是否点赞的数据。
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 Boolean loveComment (Long userId, String publishId) ; Boolean disLoveComment (Long userId, String publishId) ; Long queryLoveCount (String publishId) ; 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 @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 ; } 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 ; } 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) { 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); } Long count = this .queryCommentCount(publishId, CommentType.LOVE); 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" ); } 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 @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(); } @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 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 ; } 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 ); quanZiVo.setDistance("1.2公里" ); quanZiVo.setHasLiked(this .quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0 ); quanZiVo.setLikeCount(Convert.toInt(this .quanZiApi.queryLikeCount(quanZiVo.getId()))); quanZiVo.setHasLoved(this .quanZiApi.queryUserIsLove(user.getId(), quanZiVo.getId()) ? 1 : 0 ); quanZiVo.setLoveCount(Convert.toInt(this .quanZiApi.queryLoveCount(quanZiVo.getId()))); }
1.2.3、测试
4、查询单条动态 用户点击评论时需要查询单条动态详情,需要有接口支持。
服务接口地址:https://mock-java.itheima.net/project/35/interface/api/695
响应的数据接口与查询好友动态一致,只是单条返回而不是集合。
要注意的是,dubbo服务接口在前面已经开发完成,现在只要想实现APP端的接口服务即可。
4.1、定义服务接口 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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 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、测试
可以看到,返回了单条数据。
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 @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 PageInfo<Comment> queryCommentList (String publishId, Integer page, Integer pageSize) ; 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 @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; } @Override public Boolean saveComment (Long userId, String publishId, String content) { return this .saveComment(userId, publishId, CommentType.COMMENT, content); }
5.2、APP接口服务 根据响应结果的数据结构定义对象:
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; private String avatar; private String nickname; private String content; private String createDate; private Integer likeCount; private Integer hasLiked; }
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; @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(); } @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(); } @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 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; } public Boolean saveComments (String publishId, String content) { User user = UserThreadLocal.get(); return this .quanZiApi.saveComment(user.getId(), publishId, content); }
5.2.4、测试
测试点赞时会发现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 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 { } } this .mongoTemplate.save(comment); return true ; } catch (Exception e) { log.error("保存Comment出错~ userId = " + userId + ", publishId = " + publishId + ", commentType = " + commentType, e); } return false ; } private Comment queryCommentById (String id) { return this .mongoTemplate.findById(new ObjectId (id), Comment.class); }
这样,点赞功能正常了。
6、小视频 6.1、功能说明 小视频功能类似于抖音、快手小视频的应用,用户可以上传小视频进行分享,也可以浏览查看别人分享的视频,并且可以对视频评论和点赞操作。
效果:
查看详情:
评论:
点赞:
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称为存储服务器。
每个 tracker 节点地位平等,收集 Storage 集群的状态。
Storage 分为多个组,每个组之间保存的文件是不同的。每个组内部可以有多个成员,组成员内部保存的内容是一样的,组成员的地位是一致的,没有主从的概念。
7.3、文件的上传
客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。
7.4、文件的下载
客户端下载请求到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.so-timeout = 1501 fdfs.connect-timeout = 601 fdfs.thumb-image.width = 150 fdfs.thumb-image.height = 150 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); System.out.println(fdfsWebServer.getWebServerUrl() + storePath.getFullPath()); } catch (IOException e) { e.printStackTrace(); } } }
通过浏览器访问图片:
8、发布小视频 发布小视频的流程如下:
说明:
用户发通过客户端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; private Long vid; private Long userId; private String text; private String picUrl; private String videoUrl; private Long created; private Integer seeType; 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 { 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; @Override public String saveVideo (Video video) { try { if (!ObjectUtil.isAllNotEmpty(video.getUserId(), video.getPicUrl(), video.getVideoUrl())){ return null ; } video.setId(ObjectId.get()); video.setVid(this .idService.createId(IdType.VIDEO)); video.setCreated(System.currentTimeMillis()); 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
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; @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; 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 ); 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、测试 如果上传视频,会导致异常,是因为请求太大的缘故:
解决:application.properties
1 2 spring.servlet.multipart.max-file-size =30MB spring.servlet.multipart.max-request-size =30MB
测试:
可以看到数据已经写入到了MongoDB中。
9、小视频列表 小视频的列表查询的实现需要注意的是,如果有推荐视频,优先返回推荐视频,如果不够或没有,按照时间倒序查询视频表。
推荐数据:
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 { Boolean saveVideo (Video video) ; 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 @Override public PageInfo<Video> queryVideoList (Long userId, Integer page, Integer pageSize) { PageInfo<Video> pageInfo = new PageInfo <>(); pageInfo.setPageNum(page); pageInfo.setPageSize(pageSize); 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, ',' ); 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; } 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 )); 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; private String signature; private Integer likeCount; private Integer hasLiked; private Integer hasFocus; 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 { @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 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("我就是我~" ); videoVo.setCommentCount(0 ); videoVo.setHasFocus(0 ); videoVo.setHasLiked(0 ); videoVo.setLikeCount(0 ); 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、测试
可以看到已经查询到数据。下面使用手机进行测试:
小视频和通讯 课程说明
实现视频点赞、评论、关注功能
了解什么是即时通信
了解探花交友的消息功能
了解即时通信的技术方案
了解环信的即时通讯
实现环信的用户体系集成
实现添加联系人、联系人列表功能
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 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 Video queryVideoById (String videoId) ;
编写实现:
1 2 3 4 5 6 @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 @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(); } @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 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 ; } 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 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("我就是我~" ); videoVo.setCommentCount(0 ); videoVo.setHasFocus(0 ); videoVo.setHasLiked(this .quanZiApi.queryUserIsLike(user.getId(), videoVo.getId()) ? 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 @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(); } @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(); } @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(); } @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 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 Long queryCommentCount (String publishId) ;
2.3.1.2、编写实现 1 2 3 4 5 6 @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 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("我就是我~" ); videoVo.setCommentCount(Convert.toInt(this .quanZiApi.queryCommentCount(videoVo.getId()))); videoVo.setHasFocus(0 ); videoVo.setHasLiked(this .quanZiApi.queryUserIsLike(user.getId(), videoVo.getId()) ? 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、测试
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; private Long userId; private Long followUserId; 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 Boolean followUser (Long userId, Long followUserId) ; Boolean disFollowUser (Long userId, Long followUserId) ; 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 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 { 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); 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 ) { 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 @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 public Boolean followUser (Long userId) { User user = UserThreadLocal.get(); return this .videoApi.followUser(user.getId(), userId); } 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 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("我就是我~" ); 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 ); 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、测试
可以看到,已经完成了关注用户。
4、即时通信 4.1、什么是即时通信?
4.2、功能说明 在探花交友项目中也提供了类似微信的聊天功能,用户可以和好友或陌生人聊天。
如果是陌生人,通过《聊一下》功能进行打招呼,如果对方同意后,就成为了好友,可以进行聊天了。
陌生人之间如果相互喜欢,那么就会成为好友,也就可以聊天了。
在消息界面中也可以查看:点赞、评论、喜欢、公告等消息信息。
5、技术方案 对于高并发的即时通讯实现,还是很有挑战的,所需要考虑的点非常多,除了要实现功能,还要考虑并发、流量、负载、服务器、容灾等等。虽然有难度也并不是高不可攀。
对于现实即时通讯往往有两种方案:
方案一:
自主实现,从设计到架构,再到实现。
技术方面可以采用:Netty + WebSocket + RocketMQ + MongoDB + Redis + ZooKeeper + MySQL
方案二:
对接第三方服务完成。
这种方式简单,只需要按照第三方的api进行对接就可以了。
如:环信、网易、容联云通讯等。
如何选择呢?
如果是中大型企业做项目可以选择自主研发,如果是中小型企业研发中小型的项目,选择第二种方案即可。方案一需要有大量的人力、物力的支持,开发周期长,成本高,但可控性强。方案二,成本低,开发周期短,能够快速的集成起来进行功能的开发,只是在可控性方面来说就差了一些。
探花交友项目选择方案二进行实现。
6、环信 官网:https://www.easemob.com/ 稳定健壮,消息必达,亿级并发的即时通讯云
环信平台为黑马学员开设的专用注册地址:https://datayi.cn/w/woVL50vR
6.1、开发简介 文档地址:http://docs-im.easemob.com/
平台架构:
集成:
环信和用户体系的集成主要发生在2个地方,服务器端集成和客户端集成。
探花集成:
探花前端使用AndroidSDK进行集成
后端集成用户体系
6.2、环信Console 需要使用环信平台,那么必须要进行注册,登录之后即可创建应用。环信100以内的用户免费使用,100以上就要注册企业版了。
企业版价格:
创建应用:
创建完成:
6.3、整体流程图
说明:
在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 > <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 > <dependency > <groupId > com.alibaba.boot</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > dubbo</artifactId > </dependency > <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.application.name = itcast-tanhua-dubbo-huanxin dubbo.scan.basePackages = com.tanhua.dubbo.server dubbo.application.name = dubbo-provider-huanxin dubbo.protocol.name = dubbo dubbo.protocol.port = 20881 dubbo.registry.address = zookeeper://192.168.31.81:2181 dubbo.registry.client = zkclient dubbo.registry.timeout = 60000 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_ 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}) public class HuanXinDubboApplication { public static void main (String[] args) { SpringApplication.run(HuanXinDubboApplication.class, args); } }
7.2、配置 相关的配置,在环信管理控制台中,可以找到相关的参数。
1 2 3 4 5 6 7 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; public String getToken () { String token = this .redisTemplate.opsForValue().get(REDIS_KEY); if (StrUtil.isNotEmpty(token)) { return token; } return this .refreshToken(); } 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)) { 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 { 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中了:
8、用户系统集成 使用环信平台,最重要的就是集成用户体系,基本的逻辑是这样的:新用户在注册后,同时需要注册环信用户。
流程如下:
流程说明:
用户在登录时在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 <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}) @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 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参数说明:
@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; @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) .body(body) .timeout(20000 ) .execute(); if (response.getStatus() == 401 ) { 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()); 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; private String username; private String password; private String nickname; private Long userId; 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 Boolean register (Long userId) ; 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 @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 () { System.out.println(this .huanXinApi.register(1L )); } @Test public void testQueryHuanXinUser () { 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 > <dependency > <groupId > com.alibaba.boot</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > dubbo</artifactId > </dependency > <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.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;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()); } } Map<String, Object> claims = new HashMap <String, Object>(); claims.put("id" , user.getId()); String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS256, secret) .setExpiration(new DateTime ().plusHours(12 ).toDate()) .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进行测试,使用新手机号进行登录测试。
新注册的用户:
所对应的环信用户:
环信平台:
可以看到已经注册到了环信。
8.3、查询环信用户信息 在app中,用户登录后需要根据用户名密码登录环信,由于用户名密码保存在后台,所以需要提供接口进行返回。
mock地址: https://mock-java.itheima.net/project/35/interface/api/563
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(); HuanXinUser huanXinUser = this .huanXinApi.queryHuanXinUser(user.getId()); if (ObjectUtil.isNotEmpty(huanXinUser)) { return new HuanXinUserVo (huanXinUser.getUsername(), huanXinUser.getPassword()); } return null ; } }
8.3.4、测试
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 HuanXinUser queryUserByUserName (String userName) ;
8.4.1.2、编写实现 1 2 3 4 5 6 7 8 @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; @Alias("logo") private String avatar; @Alias("nickName") private String nickname; private String birthday; private String age; private String gender; private String city; @Alias("edu") private String education; private String income; @Alias("industry") private String profession; private Integer marriage; }
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 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; @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 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、测试
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; @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)) { 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、测试
8.6、发送消息给客户端 目前已经完成了用户体系的对接,下面我们进行测试发送消息,场景是这样的:
点击“聊一下”,就会给对方发送一条陌生人信息,这个消息由系统发送完成。
我们暂时通过环信的控制台进行发送:
消息内容:
1 {"userId":1,"huanXinId":"HX_1","nickname":"黑马小妹","strangerQuestion":"你喜欢去看蔚蓝的大海还是去爬巍峨的高山?","reply":"我喜欢秋天的落叶,夏天的泉水,冬天的雪地,只要有你一切皆可~"}
可以看到已经接收到了消息。
8.7、将用户数据同步到环信 需要将1~99用户注册到环信,因为我们提供的数据都是这些用户的数据。
1 2 3 4 5 6 7 8 @Test public void testRegisterAllUser () { for (int i = 1 ; i < 100 ; i++) { this .huanXinApi.register(Long.valueOf(i)); } }
环信:
9、 添加联系人 点击“聊一下”,就会成为联系人(好友)。
实现:
将好友写入到MongoDB中
将好友关系注册到环信
具体的流程如下:
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 { String saveUsers (Long userId, Long friendId) ; 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 Boolean addUserFriend (Long userId, Long friendId) ; 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 @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 { 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 { 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 @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; 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、测试 接口测试:
Monodb数据:
环信平台好友数据:
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; @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 List<Users> queryAllUsersList (Long userId) ; 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 @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 @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 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、测试
消息个人主页 课程说明
点赞消息列表
喜欢消息列表
评论消息列表
公告列表
个人主页
聊一下功能
谁看过我的功能
1、消息点赞、喜欢、评论列表 在消息模块中的点赞、喜欢、评论列表,是别人对自己发布的内容的操作,其实现基本一致,所以在一起实现。
效果:
点赞列表接口地址: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 PageInfo<Comment> queryLikeCommentListByUser (Long userId, Integer page, Integer pageSize) ; PageInfo<Comment> queryLoveCommentListByUser (Long userId, Integer page, Integer pageSize) ; 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 @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接口服务 根据接口定义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; }
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 @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(); } @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(); } @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 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、测试
2、公告列表 公告是后台系统对所有用户发布的公告消息。
效果:
接口地址: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 @GetMapping("announcements") @NoAuthorization 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、测试
3、个人主页 点击首页的今日佳人或任意推荐人的图片就会进入个人主页页面,效果如下:
在个人主页的页面中,会显示出个人信息、缘分值、个人相册等内容。
3.1、个人信息 3.1.1、dubbo服务 需要在dubbo服务中提供查询缘分值的接口服务。
3.1.1.1、定义接口 1 2 3 4 5 6 7 8 9 10 Double queryScore (Long userId, Long toUserId) ;
3.1.1.2、实现接口 1 2 3 4 5 6 7 8 9 10 11 12 @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; @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 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、测试
3.2、个人相册 3.2.1、dubbo服务 3.2.1.1、定义接口 1 2 3 4 5 6 7 8 9 10 11 12 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 @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 @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 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、测试
3.3、整合测试
4、聊一下 在个人主页中,点击聊一下按钮,会弹出回答问题窗口,输入答案后,系统会向对方发送一条陌聊消息,如果对方在陌聊消息中点击聊一下,他们就会成为好友。
用户1在用户2的个人主页中点击“聊一下”,流程如下:
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 @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 public String queryQuestion (Long userId) { Question question = this .questionService.queryQuestion(userId); if (ObjectUtil.isNotEmpty(question)){ return question.getTxt(); } return "你的爱好是什么?" ; }
4.2.3、测试
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 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;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 @Override public Boolean sendMsgFromAdmin (String targetUserName, HuanXinMessageType huanXinMessageType, String msg) { String targetUrl = this .huanXinConfig.getUrl() + this .huanXinConfig.getOrgName() + "/" + this .huanXinConfig.getAppName() + "/messages" ; try { 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(); 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 @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 @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、测试
用户heima_37收到陌生人消息:
可以看到好友已经添加完成,可以在通讯录中选择好友进行聊天。
在陌聊消息中的“确认添加”功能,就是前面实现的添加联系人接口。
5、谁看过我 查询别人来访了我的主页的信息,其他用户在浏览我的主页时,需要记录访客数据。访客在一天内每个用户只记录一次。
查询数据时,如果用户查询过列表,就需要记录这次查询数据的时间,下次查询时查询大于等于该时间的数据。
如果,用户没有记录查询时间,就查询最近的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 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; private Long visitorUserId; 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 { String saveVisitor (Long userId, Long visitorUserId, String from) ; 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) { 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 @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 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 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、测试
搜附近和探花 课程说明
上报地位位置
实现搜附近功能
实现探花功能
用户基本信息维护
1、上报地理位置 当客户端检测用户的地理位置,当变化大于500米时或每隔5分钟,向服务端上报地理位置。
用户的地理位置存储到Elasticsearch中,需要使用环境提供的ES集群,如下:
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 > <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 > <dependency > <groupId > com.alibaba.boot</groupId > <artifactId > dubbo-spring-boot-starter</artifactId > </dependency > <dependency > <groupId > com.alibaba</groupId > <artifactId > dubbo</artifactId > </dependency > <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.application.name = itcast-tanhua-dubbo-es dubbo.scan.basePackages = com.tanhua.dubbo.es dubbo.application.name = dubbo-provider-es dubbo.protocol.name = dubbo dubbo.protocol.port = 20882 dubbo.registry.address = zookeeper://192.168.31.81:2181 dubbo.registry.client = zkclient dubbo.registry.timeout = 60000 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}) 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; @GeoPointField private GeoPoint location; @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; 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 { 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(); 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; @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、测试
2、搜附近 在首页中点击“搜附近”可以搜索附近的好友,效果如下:
实现思路:根据当前用户的位置,查询附近范围内的用户。范围是可以设置的。
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 UserLocationVo queryByUserId (Long userId) ; 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 @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 ; } @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" ; 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 @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 @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 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、测试
3、探花 探花功能是将推荐的好友随机的通过卡片的形式展现出来,用户可以选择左滑、右滑操作,左滑:“不喜欢”,右滑:“喜欢”。
喜欢:如果双方喜欢,那么就会成为好友。
如果已经喜欢或不喜欢的用户在列表中不再显示。
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; @Indexed private Long likeUserId; 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 { Boolean likeUser (Long userId, Long likeUserId) ; Boolean notLikeUser (Long userId, Long likeUserId) ; Boolean isMutualLike (Long userId, Long likeUserId) ; List<Long> queryLikeList (Long userId) ; 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_" ; @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()); this .mongoTemplate.save(userLike); 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 ; } private String getLikeRedisKey (Long userId) { return LIKE_REDIS_KEY_PREFIX + userId; } private String getNotLikeRedisKey (Long userId) { return NOT_LIKE_REDIS_KEY_PREFIX + userId; } private Boolean isLike (Long userId, Long likeUserId) { String redisKey = this .getLikeRedisKey(userId); String hashKey = String.valueOf(likeUserId); return this .redisTemplate.opsForHash().hasKey(redisKey, hashKey); } 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)){ Query query = Query.query(Criteria .where("userId" ).is(userId) .and("likeUserId" ).is(likeUserId) ); this .mongoTemplate.remove(query, UserLike.class); 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) { 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) { 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 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 @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 @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 @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 public List<TodayBest> queryCardsList () { User user = UserThreadLocal.get(); int count = 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); } } int showCount = Math.min(10 , recommendUserList.size()); List<RecommendUser> result = new ArrayList <>(); for (int i = 0 ; i < showCount; i++) { 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、测试
效果:
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 @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(); } @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 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、测试
user_like表,可以看到已经相互喜欢了:
tanhua_users表,可以看到相互是好友了:
环信平台:
4、用户资料 在我的中心模块中,可以对个人信息做修改。
4.1、基本信息 在前面实现的查询个人信息接口中,已经返回个人基本数据,所以可以直接展现出个人信息,下面只需要进行实现数据的保存即可。
4.4.1、接口信息 接口地址:https://mock-java.itheima.net/project/35/interface/api/887
请求参数:
4.4.2、MyCenterController 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 @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 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 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 public UserInfoVo queryUserInfoByUserId (Long userId) { if (ObjectUtil.isEmpty(userId)) { 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; @PostMapping("header") public ResponseEntity<Object> saveLogo (@RequestParam("headPhoto") MultipartFile file, @RequestHeader("Authorization") String token) { return this .userInfoController.saveUserLogo(file, token); } }
我的功能实现 课程说明
实现我的喜欢功能
实现用户通用设置
实现黑名单功能
实现修改手机号功能
1、我的喜欢统计数 在我的模块中,将详细展现“喜欢”相关的数据,如下:
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 Long queryMutualLikeCount (Long userId) ; Long queryLikeCount (Long userId) ; 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 @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) { 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 @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 2 3 4 5 6 7 8 9 10 11 12 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、测试
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 PageInfo<UserLike> queryMutualLikeList (Long userId, Integer page, Integer pageSize) ; PageInfo<UserLike> queryLikeList (Long userId, Integer page, Integer pageSize) ; 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 @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 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 @Override public List<Visitors> queryMyVisitor (Long userId) { 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); 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; private Integer matchRate; private Boolean alreadyLove; }
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 @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 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 ; 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()); 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、测试
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 @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 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 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是好友:
取消喜欢:
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 @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 @Autowired private TanHuaService tanHuaService; public void likeFan (Long userId) { this .tanHuaService.likeUser(userId); }
2.5.3、测试
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; 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 @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 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、测试
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 @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 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 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 @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 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、测试
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 @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 @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 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 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 @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 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) { 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 @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 public Boolean checkVerificationCode (String code, String token) { User user = this .userService.queryUserByToken(token); if (ObjectUtil.isEmpty(user)){ return false ; } 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 @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 public Boolean updatePhone (String token, String newPhone) { 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 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、电商是推荐系统的先行者
京东的推荐:
1.3、推荐系统业务流程
推荐系统广泛存在于各类网站中,作为一个应用为用户提供个性化的推荐。它需要一些用户的历史数据,一般由三个部分组成:基础数据、推荐算法系统、前台展示。
基础数据包括很多维度,包括用户的访问、浏览、下单、收藏,用户的历史订单信息,评价信息等很多信息;
推荐算法系统主要是根据不同的推荐诉求由多个算法组成的推荐模型;
前台展示主要是对客户端系统进行响应,返回相关的推荐信息以供展示。
1.4、协同过滤推荐算法 迄今为止,在个性化推荐系统中,协同过滤技术是应用最成功的技术。目前国内外有许多大型网站应用这项技术为用户更加智能(个性化、千人千面)的推荐内容。
核心思想:
协同过滤一般是在海量的用户中发掘出一小部分和你品位比较类似的,在协同过滤中,这些用户成为邻居,然后根据他们喜欢的其他东西组织成一个排序的目彔作为推荐给你。
1.4.1、基于用户的推荐 UserCF
对于用户A,根据用户的历史偏好,这里只计算得到一个邻居–用户C,然后将用户C 喜欢的物品D 推荐给用户A。
基于用户的协同过滤算法先计算的是用户与用户的相似度(兴趣相投,物以类聚人以群分),然后将相似度比较接近的用户A购买的物品推荐给用户B,专业的说法是该算法用最近邻居(nearest-neighbor)算法找出一个用户的邻居集合,该集合的用户和该用户有相似的喜好,算法根据邻居的偏好对该用户进行预测。
1.4.2、基于商品的推荐 ItemCF
基于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、流程
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、流程说明
流程说明:
用户对圈子的动态操作,如:发布、浏览、点赞、喜欢等,就会给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 <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.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; public Boolean publishMsg (String publishId) { return this .sendMsg(publishId, 1 ); } public Boolean queryPublishMsg (String publishId) { return this .sendMsg(publishId, 2 ); } public Boolean likePublishMsg (String publishId) { return this .sendMsg(publishId, 3 ); } public Boolean disLikePublishMsg (String publishId) { return this .sendMsg(publishId, 6 ); } public Boolean lovePublishMsg (String publishId) { return this .sendMsg(publishId, 4 ); } public Boolean disLovePublishMsg (String publishId) { return this .sendMsg(publishId, 7 ); } public Boolean commentPublishMsg (String publishId) { return this .sendMsg(publishId, 5 ); } 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) { PageResult pageResult = new PageResult (); pageResult.setPage(page); pageResult.setPagesize(pageSize); User user = UserThreadLocal.get(); 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; } 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 ); quanZiVo.setDistance("1.2公里" ); quanZiVo.setHasLiked(this .quanZiApi.queryUserIsLike(user.getId(), quanZiVo.getId()) ? 1 : 0 ); quanZiVo.setLikeCount(Convert.toInt(this .quanZiApi.queryLikeCount(quanZiVo.getId()))); quanZiVo.setHasLoved(this .quanZiApi.queryUserIsLove(user.getId(), quanZiVo.getId()) ? 1 : 0 ); quanZiVo.setLoveCount(Convert.toInt(this .quanZiApi.queryLoveCount(quanZiVo.getId()))); } 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; } 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) { PageResult pageResult = new PageResult (); pageResult.setPage(page); pageResult.setPagesize(pageSize); User user = UserThreadLocal.get(); 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; } 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 ; } 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 ); } 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; } 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 > <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 > <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.name-server =192.168.31.81:9876 rocketmq.producer.group =tanhua 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; private Long publishId; 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); 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()); 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 ; } } this .mongoTemplate.save(recommendQuanZi); } catch (Exception e) { log.error("处理消息出错!msg = " + msg, e); } } }
1.7、测试 测试方法:使用APP进行操作,可以看到在MongoDB中已经有数据写入。
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查看是否已经有数据:
5、小视频推荐 小视频的推荐和动态推荐的实现逻辑非常的类似。
5.1、动态计分规则
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; public Boolean videoMsg (String videoId) { return this .sendMsg(videoId, 1 ); } public Boolean likeVideoMsg (String videoId) { return this .sendMsg(videoId, 2 ); } public Boolean disLikeVideoMsg (String videoId) { return this .sendMsg(videoId, 3 ); } public Boolean commentVideoMsg (String videoId) { return this .sendMsg(videoId, 4 ); } 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; public Boolean saveVideo (MultipartFile picFile, MultipartFile videoFile) { User user = UserThreadLocal.get(); Video video = new Video (); video.setUserId(user.getId()); video.setSeeType(1 ); try { PicUploadResult uploadResult = this .picUploadService.upload(picFile); video.setPicUrl(uploadResult.getName()); 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("我就是我~" ); 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 ); 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; } 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 ; } 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); } public Boolean followUser (Long userId) { User user = UserThreadLocal.get(); return this .videoApi.followUser(user.getId(), userId); } 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; private Long videoId; 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" ); 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、测试
可以看到,用户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
测试: