【探花交友DAY 02】项目搭建和用户登录功能的实现 | 您所在的位置:网站首页 › 交友软件搭建 › 【探花交友DAY 02】项目搭建和用户登录功能的实现 |
1. 项目介绍
探花交友是一个陌生人的在线交友平台,在该平台中可以搜索附近的人,查看好友动态,平台还会通过大数据计算进行智能推荐,通过智能推荐可以找到更加匹配的好友,这样才能增进用户对产品的喜爱度。探花平台还提供了在线即时通讯功能,可以实时的与好友进行沟通,让沟通随时随地的进行。 项目仓库:https://github.com/liyuxuan7762/tanhuaAPP 1.1 项目功能简介 功能说明备注注册、登录用户无需单独注册,直接通过手机号登录即可首次登录成功后需要完善个人信息交友主要功能有:测灵魂、桃花传音、搜附近、探花等圈子类似微信朋友圈,用户可以发动态、查看好友动态等消息通知类消息 + 即时通讯消息小视频类似抖音,用户可以发小视频,评论等显示小视频列表需要进行推荐算法计算后进行展现。我的我的动态、关注数、粉丝数、通用设置等 1.2、项目背景探花交友项目定位于 陌生人交友市场。 根据《2018社交领域投融资报告》中指出:虽然相比2017年,投融资事件减少29.5%,但是融资的总额却大幅增长,达到68%。这些迹象说明:社交领域的发展规模正在扩大,而很多没有特色的产品也会被淘汰。而随着那些尾部产品的倒下,对我们来说就是机会,及时抓住不同社交需求的机会。以社交为核心向不同的细分领域衍生正在逐渐走向成熟化。而我们按照娱乐形式和内容为主两个维度,将社交行业公司分类为:即时通信、内容社群、陌生人社交、泛娱乐社交以及兴趣社交几个领域。而在2018年社交的各个细分领域下,均有备受资本所关注的项目,根据烯牛数据2018年的报告中,也同样指出:内容社交及陌生人社交为资本重要关注领域,合计融资占比达73%。
前端: flutter + android + 环信SDK + redux + shared_preferences + connectivity + iconfont + webview + sqflite后端: Spring Boot + SpringMVC + Mybatis + MybatisPlus + DubboElasticsearch geo 实现地理位置查询MongoDB 实现海量数据的存储Redis 数据的缓存Spark + MLlib 实现智能推荐第三方服务 环信即时通讯第三方服务 阿里云 OSS 、 短信服务第三方服务 虹软开放平台 / 阿里云项目基于前后端分离的架构进行开发,前后端分离架构总体上包括前端和服务端,通常是多人协作开发 前后端分离开发基于HTTP+JSON交互 通过接口文档(API文档)定义规范 前后端按照文档定义请求及响应数据
探花交友项目的开发统一使用提供的Centos7环境,该环境中部署安装了项目所需要的各种服务,如:RabbitMQ,MongoDB、Redis等。我们同意使用Docker对项目所用的到组件进行管理。 3.2 Android模拟器本项目不同于传统的基于HTML页面的前端项目,采用了安卓APP的方式。因此在本项目中我们使用了网易的MUMU模拟器来运行我们的前端项目。 本项目在测试接口的时候使用POSTMAN进行测试。 一下是本项目所用到的所有的数据表 数据库表说明tb_user用户表tb_user_info用户详情表tb_settings用户设置表tb_question好友问题表tb_black_list黑名单tb_announcement公告表 4.2 基础服务组件之前提到本项目中所有的组件都是通过Docker进行管理。为了方便学习与减少基础服务占用的学习时间,全部使用docker-compose的方式集中式部署。这些文件在linux虚拟机中的/root/docker-file文件夹下 /root/docker-file文件夹下包含base, fasdfs, rmq目录,作用如下: base 其中包含redis,nacos,yapi,mongofastdfs 包含fastdfs操作需要的组件rmq 包含RabbitMQ需要所有组件每个文件夹中都包含一个docker-compose.yml配置文件,一键启动并部署应用。 #进入组件目录 cd /root/docker-file/base/ #执行docker-compose命令 docker-compose up -d 4.3 项目工程结构整体项目使用Maven架构搭建,采用聚合工程形式管理模块,为了便于调用,dubbo需要拆分为接口模块和服务模块整体项目使用Maven架构搭建,采用聚合工程形式管理模块,为了便于调用,dubbo需要拆分为接口模块和服务模块
模块依赖分析 用户使用手机号进行登录,如果是新用户则需要完善个人信息,并上传头像,在上传头像的时候需要对图像进行校验,判断用户上传的图像是否是人像,防止用户上传非人像图片。流程完成后,则登录成功。 对于已经注册的用户,在验证通过后直接进入到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); }相关的依赖如下: javax.mail mail 1.4.7 com.sun.mail javax.mail 1.5.3 5.3 实现发送信息组件的自动装配在实际的企业开发中,对于这种发送短信的通用功能,通常会抽取到一个模块中,其他模块如果想引用的话,可以通过自动装配的方式实现组件的引用。因此接下来我们要改造发送短信的代码,抽取到一个模块中。后续使用的话则通过自动装配的方式。 Springboot自动创配的流程如下: 扫描依赖模块中的META-INF/spring.factories执行装配类中的方法,在IOC容器中创建相关的对象在核心工程中通过自动装配的方法使用该对象在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"); } } 5.4 登录验证码实现登录的流程如下: 客户端发送请求服务端接收大请求,调用短信发送接口用户端获取相应结果,将结果保存到Redis中服务端想客户端发送响应信息![]() ![]() 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 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 6. JWT 6.1 简介和之前的项目不同,在分布式情况下,我们很难在使用session保存相关的登录信息,转而使用JWT进行登录验证。JSON Web token简称JWT, 是用于对应用程序上的用户进行身份验证的标记。也就是说, 使用 JWTS 的应用程序不再需要保存有关其用户的 cookie 或其他session数据。此特性便于可伸缩性, 同时保证应用程序的安全 6.2 格式JWT就是一个字符串,经过加密处理与校验处理的字符串,形式为:A.B.C A由JWT头部信息header加密得到 B由JWT用到的身份验证信息json数据加密得到 C由A和B加密得到,是校验部分 要使用JWT,我们需要先引用相关的依赖 io.jsonwebtoken jjwt 0.9.1编写测试类,实现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; } } 7. 用户登录在学习完JWT之后,我们来实现用户登录的功能。 7.1 流程分析
在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 qw = new QueryWrapper(); qw.eq("mobile",mobile); return userMapper.selectOne(qw); } @Override public Long save(User user) { userMapper.insert(user); return user.getId(); } 8. 代码优化 8.1 抽取BasePojo通过观察数据表发现,很多数据表都有created 和 update 字段,这就意味着对应的实体类中也会有这些字段,每次都需要重复写导致代码冗余。因此我们考虑抽取一个实体类的父类,所有的实体类都继承这个类即可 @Data public abstract class BasePojo implements Serializable { @TableField(fill = FieldFill.INSERT) //自动填充 private Date created; @TableField(fill = FieldFill.INSERT_UPDATE) private Date updated; } 8.2 自动填充对于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 9. Nacos的一个问题 9.1 问题描述事情是这样的,为一个项目配置了注册中心nacos,一开始配置的是本机的nacos服务,后面将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 |
CopyRight 2018-2019 实验室设备网 版权所有 |