探花交友是一个陌生人的在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台还会通过大数据计算进行智能推荐,通过智能推荐可以找到更加匹配的好友,这样才能增进用户对产品的喜爱度。探花平台还提供了在线即时通讯功能,可以实时的与好友进行沟通,让沟通随时随地的进行。 项目仓库:https://github.com/liyuxuan7762/tanhuaAPP
功能 | 说明 | 备注 |
---|---|---|
注册、登录 | 用户无需单独注册,直接通过手机号登录即可 | 首次登录成功后需要完善个人信息 |
交友 | 主要功能有:测灵魂、桃花传音、搜附近、探花等 | |
圈子 | 类似微信朋友圈,用户可以发动态、查看好友动态等 | |
消息 | 通知类消息 + 即时通讯消息 | |
小视频 | 类似抖音,用户可以发小视频,评论等 | 显示小视频列表需要进行推荐算法计算后进行展现。 |
我的 | 我的动态、关注数、粉丝数、通用设置等 |
探花交友项目定位于 陌生人交友市场。
根据市场现状以及融资事件来看:陌生人社交、内容社群、兴趣社交在2019年仍然保持强劲的动力,占到近70%的比例,它们仍然是资本市场主要关注领域。从增长率来看陌生人社交的增长速度远远大于其他几类,因此我们要从这个方向入手。
前端:
后端:
项目基于前后端分离的架构进行开发,前后端分离架构总体上包括前端和服务端,通常是多人协作开发
前后端分离开发基于HTTP+JSON交互
通过接口文档(API文档)定义规范
前后端按照文档定义请求及响应数据
在本项目中采用YApi对接口规范进行管理。对于接口的定义我们采用YApi进行管理,YApi是一个开源的接口定义、管理、提供mock数据的管理平台。
探花交友项目的开发统一使用提供的Centos7环境,该环境中部署安装了项目所需要的各种服务,如:RabbitMQ,MongoDB、Redis等。我们同意使用Docker对项目所用的到组件进行管理。
本项目不同于传统的基于HTML页面的前端项目,采用了安卓APP的方式。因此在本项目中我们使用了网易的MUMU模拟器来运行我们的前端项目。
一下是本项目所用到的所有的数据表
数据库表 | 说明 |
---|---|
tb_user | 用户表 |
tb_user_info | 用户详情表 |
tb_settings | 用户设置表 |
tb_question | 好友问题表 |
tb_black_list | 黑名单 |
tb_announcement | 公告表 |
之前提到本项目中所有的组件都是通过Docker进行管理。为了方便学习与减少基础服务占用的学习时间,全部使用docker-compose的方式集中式部署。这些文件在linux虚拟机中的/root/docker-file
文件夹下
/root/docker-file
文件夹下包含base, fasdfs, rmq目录,作用如下:
base
fastdfs
rmq
每个文件夹中都包含一个docker-compose.yml配置文件,一键启动并部署应用。
#进入组件目录
cd /root/docker-file/base/
#执行docker-compose命令
docker-compose up -d
整体项目使用Maven架构搭建,采用聚合工程形式管理模块,为了便于调用,dubbo需要拆分为接口模块和服务模块整体项目使用Maven架构搭建,采用聚合工程形式管理模块,为了便于调用,dubbo需要拆分为接口模块和服务模块
父工程 | 工程名称 | 说明 |
---|---|---|
tanhua | tanhua-autoconfig | 自动装配的工具类 |
tanhua | tanhua-domain | 实体类模块 |
tanhua | tanhua-dubbo | Dubbo子模块(可以理解为文件夹,管理dubbo模块) |
tanhua | tanhua-app | 与手机端交互的入口模块 |
tanhua-dubbo | tanhua-dubbo-interface | Dubbo接口模块 |
tanhua-dubbo | tanhua-dubbo-db | Dubbo服务模块(数据库部分) |
tanhua-dubbo | tanhua-dubbo-mongo | Dubbo服务模块(MongoDB部分) |
用户使用手机号进行登录,如果是新用户则需要完善个人信息,并上传头像,在上传头像的时候需要对图像进行校验,判断用户上传的图像是否是人像,防止用户上传非人像图片。流程完成后,则登录成功。
对于已经注册的用户,在验证通过后直接进入到APP主页,对于未注册的用户,在登录成功后需要跳转到完善用户信息界面。
由于发送短信需要资质和费用,因此这里使用邮箱发送验证码来替代。之前我们在瑞吉外卖项目中已经实现了通过邮箱发送验证码的代码,这里就不在解释,相关的代码如下:
private boolean sendMailByQQMail(String to, String text, String title) {
try {
final Properties props = new Properties();
props.put("mail.smtp.auth", "true");
// 注意发送邮件的方法中,发送给谁的,发送给对应的app,※
// 要改成对应的app。扣扣的改成qq的,网易的要改成网易的。※
// props.put("mail.smtp.host", "smtp.qq.com");
props.put("mail.smtp.host", "smtp.qq.com");
// 发件人的账号
props.put("mail.user", emailProperties.getUser());
//发件人的密码
props.put("mail.password", emailProperties.getPassword());
// 构建授权信息,用于进行SMTP进行身份验证
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
// 用户名、密码
String userName = props.getProperty("mail.user");
String password = props.getProperty("mail.password");
return new PasswordAuthentication(userName, password);
}
};
// 使用环境属性和授权信息,创建邮件会话
Session mailSession = Session.getInstance(props, authenticator);
// 创建邮件消息
MimeMessage message = new MimeMessage(mailSession);
// 设置发件人
String username = props.getProperty("mail.user");
InternetAddress form = new InternetAddress(username);
message.setFrom(form);
// 设置收件人
InternetAddress toAddress = new InternetAddress(to);
message.setRecipient(Message.RecipientType.TO, toAddress);
// 设置邮件标题
message.setSubject(title);
// 设置邮件的内容体
message.setContent(text, "text/html;charset=UTF-8");
// 发送邮件
Transport.send(message);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void sendCode(String recevier, String code) {
String text = "欢迎使用探花交友软件,您本次的验证码为" + code + "。(本验证码在1分钟之内有效,请勿将验证码泄露给他人)";
String title = "探花交友登录验证码";
sendMailByQQMail(recevier, text, title);
}
相关的依赖如下:
<dependency>
<groupId>javax.mailgroupId>
<artifactId>mailartifactId>
<version>1.4.7version>
dependency>
<dependency>
<groupId>com.sun.mailgroupId>
<artifactId>javax.mailartifactId>
<version>1.5.3version>
dependency>
在实际的企业开发中,对于这种发送短信的通用功能,通常会抽取到一个模块中,其他模块如果想引用的话,可以通过自动装配的方式实现组件的引用。因此接下来我们要改造发送短信的代码,抽取到一个模块中。后续使用的话则通过自动装配的方式。
Springboot自动创配的流程如下:
META-INF/spring.factories
在tanhua-autoconfig创建发送短信的工具类
public class EmailTemplate {
// private static final String USER = "[email protected]"; // 发件人称号,同邮箱地址※
// private static final String PASSWORD = "ysdimzjzxfqmbdeg"; // 授权码,开启SMTP时显示※
private EmailProperties emailProperties;
public EmailTemplate(EmailProperties emailProperties) {
this.emailProperties = emailProperties;
}
private boolean sendMailByQQMail(String to, String text, String title) {
try {
final Properties props = new Properties();
props.put("mail.smtp.auth", "true");
// 注意发送邮件的方法中,发送给谁的,发送给对应的app,※
// 要改成对应的app。扣扣的改成qq的,网易的要改成网易的。※
// props.put("mail.smtp.host", "smtp.qq.com");
props.put("mail.smtp.host", "smtp.qq.com");
// 发件人的账号
props.put("mail.user", emailProperties.getUser());
//发件人的密码
props.put("mail.password", emailProperties.getPassword());
// 构建授权信息,用于进行SMTP进行身份验证
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
// 用户名、密码
String userName = props.getProperty("mail.user");
String password = props.getProperty("mail.password");
return new PasswordAuthentication(userName, password);
}
};
// 使用环境属性和授权信息,创建邮件会话
Session mailSession = Session.getInstance(props, authenticator);
// 创建邮件消息
MimeMessage message = new MimeMessage(mailSession);
// 设置发件人
String username = props.getProperty("mail.user");
InternetAddress form = new InternetAddress(username);
message.setFrom(form);
// 设置收件人
InternetAddress toAddress = new InternetAddress(to);
message.setRecipient(Message.RecipientType.TO, toAddress);
// 设置邮件标题
message.setSubject(title);
// 设置邮件的内容体
message.setContent(text, "text/html;charset=UTF-8");
// 发送邮件
Transport.send(message);
return true;
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
public void sendCode(String recevier, String code) {
String text = "欢迎使用探花交友软件,您本次的验证码为" + code + "。(本验证码在1分钟之内有效,请勿将验证码泄露给他人)";
String title = "探花交友登录验证码";
sendMailByQQMail(recevier, text, title);
}
}
针对发送邮件是需要提供的秘钥等信息,可以写到application.yml中,然后通过@ConfigurationProperties
注解读取到一个信息配置类中。然后在EmailTemplate
中通过这个类来设置相关的信息。
首先创建一个EmailProperties
类来保存秘钥等信息
@Data
@ConfigurationProperties(prefix = "tanhua.email")
public class EmailProperties {
private String user;
private String password;
}
@ConfigurationProperties(prefix = "tanhua.email")
会从配置文件中读取到相关的属性,然后在IOC容器中创建对象
然后将秘钥等信息写到tanhua-app-server
模块下的application.yml
中
email:
user: [email protected]
password: ysdimzjzxfqmbdeg
修改EmailTemplate
代码,添加EmailProperties
成员变量和构造方法
public class EmailTemplate {
private EmailProperties emailProperties;
public EmailTemplate(EmailProperties emailProperties) {
this.emailProperties = emailProperties;
}
......
Authenticator authenticator = new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
// 用户名、密码
String userName = props.getProperty("mail.user");
String password = props.getProperty("mail.password");
return new PasswordAuthentication(userName, password);
}
};
接下来要编写自动装配的配置类,将EmailTemplate
对象存入IOC容器
@EnableConfigurationProperties({
SmsProperties.class,
EmailProperties.class
})
public class TanhuaAutoConfiguration {
@Bean
public EmailTemplate emailTemplate(EmailProperties properties) {
return new EmailTemplate(properties);
}
}
最后还要在resource下创建META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.tanhua.autoconfig.TanhuaAutoConfiguration
至此,关于发送短信模块的抽取就结束了,下面可以编写测试类进行测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AppServerApplication.class)
public class SmsTemplateTest {
@Autowired
private EmailTemplate emailTemplate;
//测试
@Test
public void testSendSms() {
emailTemplate.sendCode("[email protected]", "1234");
}
}
登录的流程如下:
LoginController.java
@PostMapping("/login")
public ResponseEntity login(@RequestBody Map map) {
String phone = map.get("phone").toString();
this.userService.sendMsg(phone);
return ResponseEntity.ok(null);
}
UseService.java
@Service
public class UserService {
@Resource
private EmailTemplate emailTemplate;
@Resource
private RedisTemplate<String, String> redisTemplate;
@DubboReference
private UserApi userApi;
public void sendMsg(String phone) {
// 1. 生成验证码
String code = RandomStringUtils.randomNumeric(6);
// 2.调用发送验证码的方法
emailTemplate.sendCode(phone, code);
// 3.将验证码存入redis
redisTemplate.opsForValue().set(VERIFICATION_CODE_PREFIX + phone, code, Duration.ofMinutes(5));
}
}
这里为了方便开发测试,以后将验证码都写死为123456
和之前的项目不同,在分布式情况下,我们很难在使用session保存相关的登录信息,转而使用JWT进行登录验证。JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全
要使用JWT,我们需要先引用相关的依赖
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwtartifactId>
<version>0.9.1version>
dependency>
编写测试类,实现JWT对数据的封装和根据JWT token解析数据
public class JwtTest {
@Test
public void testCreateToken() {
// 1.准备数据
Map map = new HashMap();
map.put("id",1);
map.put("mobile","13800138000");
// 2.使用JWT工具生成JWT字符串
String token = Jwts.builder()
.signWith(SignatureAlgorithm.HS512, "itcast") // 指定加密算法和秘钥
.setClaims(map) // 设置数据
.setExpiration(new Date(System.currentTimeMillis() + 1000000)) // 设置有效期5秒
.compact();
System.out.println(token);
}
//解析token
/**
* SignatureException : token不合法
* ExpiredJwtException:token已过期
*/
@Test
public void testParseToken() {
try {
String token = "eyJhbGciOiJIUzUxMiJ9.eyJtb2JpbGUiOiIxMzgwMDEzODAwMCIsImlkIjoxLCJleHAiOjE2NzE1MzE0Njd9.eGljWldVwurcmwkWzc-Jfm6XIsokCVx_TwMazLqiFk0rdabY9ALnbpUEavrqF_maN3FiWl9oOtjZKuJF1rfhUw";
Claims claims = Jwts.parser()
.setSigningKey("itcast") // 设置秘钥
.parseClaimsJws(token) // 设置token
.getBody();
Object id = claims.get("id");
Object code = claims.get("mobile");
System.out.println(id + "---" + code);
} catch (SignatureException e){
System.out.println("token不合法");
} catch (ExpiredJwtException e) {
System.out.println("token已过期");
}
}
}
这里如果我们设置的token时间已经过了,那么会抛出
ExpiredJwtException
异常;
如果是token被篡改了,那么会抛出SignatureException
异常
为了方便使用,我们可以将JWT的相关操作封装成一个工具类
public class JwtUtils {
// TOKEN的有效期1小时(S)
private static final int TOKEN_TIME_OUT = 1 * 3600;
// 加密KEY
private static final String TOKEN_SECRET = "itcast";
// 生成Token
public static String getToken(Map params){
long currentTime = System.currentTimeMillis();
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, TOKEN_SECRET) //加密方式
.setExpiration(new Date(currentTime + TOKEN_TIME_OUT * 1000)) //过期时间戳
.addClaims(params)
.compact();
}
/**
* 获取Token中的claims信息
*/
public static Claims getClaims(String token) {
return Jwts.parser()
.setSigningKey(TOKEN_SECRET)
.parseClaimsJws(token).getBody();
}
/**
* 是否有效 true-有效,false-失效
*/
public static boolean verifyToken(String token) {
if(StringUtils.isEmpty(token)) {
return false;
}
try {
Claims claims = Jwts.parser()
.setSigningKey("itcast")
.parseClaimsJws(token)
.getBody();
}catch (Exception e) {
return false;
}
return true;
}
}
在学习完JWT之后,我们来实现用户登录的功能。
@PostMapping("/loginVerification")
public ResponseEntity loginVerification(@RequestBody Map map) {
// 1.解析参数
String phone = map.get("phone").toString();
String code = map.get("verificationCode").toString();
// 2.调用Service方法
Map retMap = this.userService.loginVerification(phone, code);
// 3.返回结果
return ResponseEntity.ok(retMap);
}
在UserService中编写登录流程
public Map loginVerification(String phone, String code) {
// 1.从Redis中获取到验证码
String redisCode = this.redisTemplate.opsForValue().get(VERIFICATION_CODE_PREFIX + phone);
// 2.比较验证码
if (StringUtils.isEmpty(redisCode) || !redisCode.equals(code)) {
// 验证码无效或者验证码错误
throw new RuntimeException("验证码失效");
}
// 3.判断用户是否已经存在
User user = userApi.findByMobile(phone);
// 4.如果不存在则新建用户
boolean isNew = false;
if (user == null) {
// 用户不存在
user = new User();
user.setMobile(phone);
user.setPassword(DigestUtils.md5Hex("123456"));
Long id = this.userApi.save(user);
user.setId(id);
isNew = true;
}
// 5.生成Token 保存id和phone
Map tokenMap = new HashMap();
tokenMap.put("id", user.getId());
tokenMap.put("mobile", phone);
String token = JwtUtils.getToken(tokenMap);
// 6.封装结果
Map retMap = new HashMap();
retMap.put("token", token);
retMap.put("isNew", isNew);
return retMap;
}
由于查询和新增用户需要访问数据库,因此需要调用tanhua-dubbo-db
和tanhua-dubbo-interface
相关代码
首先在tanhua-dubbo-interface
中创建API接口
public interface UserApi {
//根据手机号码查询用户
User findByMobile(String mobile);
//保存用户,返回用户id
Long save(User user);
}
由于我们要访问的是MySQL,所以需要在tanhua-dubbo-db
中创建UserApi
的实现类
@Override
public User findByMobile(String mobile) {
QueryWrapper<User> qw = new QueryWrapper<>();
qw.eq("mobile",mobile);
return userMapper.selectOne(qw);
}
@Override
public Long save(User user) {
userMapper.insert(user);
return user.getId();
}
通过观察数据表发现,很多数据表都有created
和 update
字段,这就意味着对应的实体类中也会有这些字段,每次都需要重复写导致代码冗余。因此我们考虑抽取一个实体类的父类,所有的实体类都继承这个类即可
@Data
public abstract class BasePojo implements Serializable {
@TableField(fill = FieldFill.INSERT) //自动填充
private Date created;
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updated;
}
对于created和updated字段,每次操作都需要手动设置。为了解决这个问题,mybatis-plus支持自定义处理器的形式实现保存更新的自动填充。
首先需要在实体类的字段上通过@TableField
注解指定当前的这个字段在什么情况下自动填充。
然后就需要在tanhua-dubbo-db
编写相应的handler实现自动填充
@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);
}
}
注意不要忘了加
@Component
事情是这样的,为一个项目配置了注册中心nacos,一开始配置的是本机的nacos服务,后面将nacos地址改为虚拟机后,项目虽然启动成功,但是报nacos异常,如下:
通过观察错误信息发现一直访问的注册中心地址是localhost,而我们实际上已经在配置文件中配置了nacos的地址。打开nacos可以发现服务实际上已经被注册上去了
这是nacos读取本身自动配置的优先级高于application文件中的配置时引起的,而nacos本身的自动配置是127.0.0.1:8848端口的nacos服务,所以发生了以上异常,故而需要将配置文件的优先级提升
创建一个bootstrap.properties
或bootstrap.yml
文件配置nacos地址就可以了。这个配置是系统级的,优先级最高,先从这个文件读取nacos地址就不会报错了
bootstrap.properties
spring.cloud.nacos.server-addr=192.168.136.160:8848