简单的注册登陆(第三方微信登陆)注册登陆

注册登陆

注册包括邮箱注册和手机号注册,两种注册方法业务都相同,只是激活方式不同

注册模块:

  1. 用户在页面输入邮箱/手机号,点击获取验证码,就会向后台发送post请求

  2. 后端接收到前端请求,发送验证码

  • 通过'工具类',生成一个随机四位数作为验证码
  • 将验证码保存到**'redis'**中,redis保存数据是通过键值对的方式保存
    • 所以我们可以通过电话+一个固定的常量作为key值
    • 随机数+当前毫秒值作为value值
    • 注意设置有效期,一般为5分钟
  • 注意当用户点击获取验证码时,我们要判断用户是否已经点击过获取验证码
    • 通过输入的手机号,按照我们拼接key值的方式拼接,然后从redis中获取
    • 如果获取到的value值不为空,表示这不是第一次点击获取,此时我们需要判断当前时间和第一次点击获取的时间,如果小于一分钟,抛出异常,告诉用户不要重复获取验证码,如果超过了一分钟,此时我们的验证码还是等于第一次获取的验证码(这样做是为了防止网络问题导致验证码累计问题)
    • 如果value值为空时,表明是第一次获取验证码,直接拼接验证码发送给用户
  • 然后调用**'发送验证码工具类'**发送验证码给用户
  1. 用户在页面输入收到的验证码,点击注册
  • 向后台发送请求,带有注册的信息

  • 后端响应请求获取请求携带的参数,此时我们可以使用一个临时对象接受参数(方便定义添加属性)

  • 首先第一件事就是进行校验

    • 校验数据是否为空,如果为空,抛出异常提示用户输入完整的信息

    • 校验手机号是否已被注册,通过前端传入的用户输入的手机号到数据库用户表进行查询,如果存在,抛出异常提示该手机已注册

    • 校验两次密码是否相同

    • 校验验证码,通过用户手机号和固定常量从redis中获取刚才发送的验证码

      • 判断验证码是否为空,如果为空表示已过期,抛出异常告诉用户验证码过期,请重新获取验证码
      • 如果不为空,比较用户输入的验证码和保存在redis缓存中的验证码是否相等,如果不等,抛出异常告诉用户验证码错误
  • 校验成功之后,将临时对象转化为登陆信息对象,保存登陆信息对象,就有了id

    注意我们在保存密码时为了安全性,需要对密码进行加密,我们是通过盐值进行加密

    • 通过随机数获取盐值,并保存到登录信息中

    • 加密方式MD5:输入任意长度的信息,经过处理,输出都是128位的信息值,无法看到明文

      ​ 不同的输入对应的输出一定不同,保证唯一性

      ​ 计算速度快,加密速度快

    • 调用**MD5工具类**的方法使用密码+盐值进行加密并设置保存 注意有顺序次数的问题

    • 需要注意的是我们的登录信息包含前台普通用户和后台用户,为了区分,我们需要设置账户类型(0代表后台用户,1代表前台普通用户)

  • 将登陆信息对象转化为user前端用户对象并保存,这样我们将关联了前端用户和登陆信息对象

    • (在转化对象时,相同普通属性进行拷贝,特殊属性手动设置)
  • 没有问题就返回AjaxResult-success给前端

  • 前端接收返回值,判断,如果注册成功,跳转到前端首页,如果不成功,就将错误异常展示到页面

    账户密码(手机号,邮箱)登录模块

    表的设计

    用户是一张表,管理员是一张表,然后我们还做了一个登录表,登录表里面是所有用户和管理员的登录账号和密码,因为管理员也可以登录我们的网站享受服务,所以有可能一个手机或邮箱既是用户又是管理员。为了区分这种登录账号到底是要登录到后台还是网站,所以我们登录表里面有一个type字段,区分该账号到底是用户还是管理员

    登录

  • 当用户输入信息点击登录时发送post登录请求,并携带了登录信息

  • 后台响应请求使用临时对象接收参数

  • 校验登录信息

    • 校验数据是否完整
    • 校验用户名是否存在,通过用户输入的用户名查询登录信息表中的登录对象,判断是否为空
    • 密码是否正确 注意此时校验密码我们需要将临时对象中的密码按照我们注册时的加密方式进行加密,然后与数据库保存的密码比对
    • 还需要判断此用户对象对应的登录信息状态是否禁用
    • 返回对应的登录信息对象

    判断用户是否登录

    后端

    • 校验登录信息正确之后,通过UUID产生随机的token值

    • 将token值和对应的的登录信息对象保存到redis内存中

    • 将token作为key,登录信息对象作为value,返回一个map给前端

      前端

    • 登录成功,后端会返回map格式的token值,将token值保存到**localStorage**中

    • 跳转到前端首页

    判断登录

  • axios前端前置拦截器:当前端每次发起请求时,都要将token值放入请求头中,每一个请求都需要token,所以我们可以配置一个前置拦截器,为所有请求头添加token

  • 后端拦截器:后端接收前端的访问请求,判断有没有token,通过token查询数据库中是否有对应的登录信息

    • 如果没有,则没有登录,返回一个未登录的状态给前端,如果有就放行(注意配置拦截器拦截和放行的页面,比如注册和登陆的页面必须放行)
    • 如果有,返回true,同时再设置一次token有效期
  • axios前端后置拦截器:判断后端拦截器返回的状态,如果为未登录状态,则跳转到登录页面,如果已登录状态,则跳转到请求访问的页面 **问题?**返回什么结果如何跳转到请求页面

微信第三方登陆

  • 用户点击微信登陆时后端响应请求,拉取二维码,重定向到二维码展示界面
  • 用户扫码确认登陆,执行回调函数并携带了授权码,我们设计了一个前端页面来响应这个回调函数,向后端发送请求获取登陆信息对象
  • 后端响应请求,通过授权码获取用户的token信息地址url,我们**通过httpClient插件向token信息地址获取token和微信用户唯一标识**
  • 通过token和唯一标识就能获取登录的微信用户的信息
    • 我们通过微信中的唯一标识去微信用户表查询,如果存在就微信用户,继续判断是否有登陆信息,如果登陆信息不为空,直接登陆
    • 如果没有登陆信息,就返回微信的用户的唯一标识,此时前端判断没有登陆信息只有唯一标识,跳转到绑定页面
    • 如果有登陆信息,我们就创建token,放入redis中,并创建map放入token和登陆信息对象返回前端,前端判断有登陆信息,放入localstorage中,然后跳转到后台首页

相关工具代码

随机数工具类

package cn.itsource.pethome.basic.util;

import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * @author yaohuaipeng
 * @date 2018/10/26-16:16
 */
public class StrUtils {
    /**
     * 把逗号分隔的字符串转换字符串数组
     *
     * @param str
     * @return
     */
    public static String[] splitStr2StrArr(String str,String split) {
        if (str != null && !str.equals("")) {
            return str.split(split);
        }
        return null;
    }


    /**
     * 把逗号分隔字符串转换List的Long
     *
     * @param str
     * @return
     */
    public static List<Long> splitStr2LongArr(String str) {
        String[] strings = splitStr2StrArr(str,",");
        if (strings == null) return null;

        List<Long> result = new ArrayList<>();
        for (String string : strings) {
            result.add(Long.parseLong(string));
        }

        return result;
    }
    /**
     * 把逗号分隔字符串转换List的Long
     *
     * @param str
     * @return
     */
    public static List<Long> splitStr2LongArr(String str,String split) {
        String[] strings = splitStr2StrArr(str,split);
        if (strings == null) return null;

        List<Long> result = new ArrayList<>();
        for (String string : strings) {
            result.add(Long.parseLong(string));
        }

        return result;
    }

    public static String getRandomString(int length) {
        String str = "01234567892342343243";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(10);
            sb.append(str.charAt(number));
        }
        return sb.toString();

    }

    public static String getComplexRandomString(int length) {
        String str = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
        Random random = new Random();
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < length; i++) {
            int number = random.nextInt(62);
            sb.append(str.charAt(number));
        }
        return sb.toString();
    }

    public static void main(String[] args) {
        String s = getComplexRandomString(4);
        System.out.println(s);
    }

    public static String convertPropertiesToHtml(String properties){
        //1:容量:6:32GB_4:样式:12:塑料壳
        StringBuilder sBuilder = new StringBuilder();
        String[] propArr = properties.split("_");
        for (String props : propArr) {
            String[] valueArr = props.split(":");
            sBuilder.append(valueArr[1]).append(":").append(valueArr[3]).append("<br>");
        }
        return sBuilder.toString();
    }

}

复制代码

redis

了解数据库

​ 数据库分类

  • NoSQL非关系型数据库:存储数据都是没有结构的(没有表的概念),并且存储数据都是以key-value的的方式存储,都是把数据存储到内存或者磁盘上

  • RDBSM关系型数据库:是有行和列组成的二维表,存储数据是有一定格式的,都是把数据存储到磁盘上

    区别:

    1. 关系型数据库不支持高并发访问,非关系型数据库支持高并发访问
    2. 关系型数据库存储数据都是有结构的,而非关系型数据库存储数据是没有结构的(,没有表概念)
    3. 关系型数据库存储数据只能放到磁盘上,非关系型数据库存储数据是放在内存和磁盘上
    4. 关系型数据库的结构和数据存储都是有限的(列最多200列,存储数据量也是有限的,最多不超过200万),而非关系型数据库只要硬件够好,数据量是没有限制的

Redis是什么

Redis是一个高性能的开源的,c语言写的NoSQL,数据保存在内存/磁盘中。

Redis是以key-value形式存储,不一定遵循传统数据库的一些基本要求,比如不遵循sql标准,事务,表结构等,redis严格说来不是一个数据库,应该是一种数据结构化存储方法的集合

存储数据都是以字符串的形式进行存储,把存储的字符串按一定的规则进行摆放(规则:String list set zset Hash)

特点

  1. 开源免费
  2. 支持高并发,读取速度非常快
  3. 存储数据放到内存或者磁盘
  4. 支持多种类型的客户端访问(c,php,java)
  5. 支持集群(集群的意思:一台不够用就再加一台)

使用场景

  1. 做中央缓存
  2. 点赞计数器
  3. 防攻击系统

使用redis保存验证码

绿色版的在cmd终端启动服务 redis-server.exe redis.window.config

然后我们在springboot项目中使用redis

  • 导包

    <!--对redis的支持-->
                <dependency>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-starter-data-redis</artifactId>
                </dependency>
    复制代码
  • 配置文件application.properties

    # redis 属性配置
    ## redis数据库索引(默认为0)
    spring.redis.database=0
    ## redis服务器地址
    spring.redis.host=localhost
    ## redis服务器连接端口
    spring.redis.port=6379
    ## redis服务器连接密码(默认为空)
    spring.redis.password=123456
    ## 连接池最大连接数(使用负值表示没有限制)
    spring.redis.jedis.pool.max-active=8
    ## 连接池中的最大空闲连接
    spring.redis.jedis.pool.max-idle=8
    ## 连接池最大阻塞等待时间(使用负值表示没有限制)
    spring.redis.jedis.pool.max-wait=-1ms
    ## 连接池中的最小空闲连接
    spring.redis.jedis.pool.min-idle=0
    复制代码
  • 保存验证码到redis(我们为验证码设置对应的全局常量作为标识,并设置了当前的毫秒值,方便验证码过期问题)

     /package cn.itsource.pethome.basic.service.impl;
    
    import cn.itsource.pethome.basic.constant.PetHomeConstant;
    import cn.itsource.pethome.basic.service.IVerificationService;
    import cn.itsource.pethome.basic.util.SendMsgUtil;
    import cn.itsource.pethome.basic.util.StrUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.data.redis.core.RedisTemplate;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.util.StringUtils;
    
    import java.util.concurrent.TimeUnit;
    
    @Service
    @Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
    public class VerificationCodeServiceImpl implements IVerificationService {
    
        @Autowired
        private RedisTemplate redisTemplate;
    
        /**
         * 验证码特点:
         *      1.随机产生4位
         *      2.有效期5分===》redis
         *      3.区分验证码类型
         *
         * 市场上常见的验证码:
         *      1.有效期是5分钟,如果在60s以内我连续发了2次,
         *          1种方案是提示前端用户,不能重复发送,
         *          如果超过了60s,但没有超过5分钟,我就不产生新的验证码,还是用第一次产生的验证码
         *          如果超过了5分钟, 就产生全新的验证码
         *
         * @param phone  手机号码
         */
        @Override
        public void sendRegisterMobileCode(String phone) {
            //手机号发送手机验证码
            sendMobileCode(phone,PetHomeConstant.REGISTER_VERIFICATION_CODE);
        }
    
        @Override
        public void sendBinderMobileCode(String phone) {
            //绑定用户发送手机验证码
            sendMobileCode(phone,PetHomeConstant.BINDER_VERIFICATION_CODE);
        }
    
        private void sendMobileCode(String phone,String type){
            //1.产生随机4位的验证码  JK82
            String code = StrUtils.getComplexRandomString(4);
            //通过手机号码在redis中获取对应的验证码    JK82:21312433542423
            String codeValue = (String) redisTemplate.opsForValue().get(type+":"+phone);
            if(!StringUtils.isEmpty(codeValue)){
                //获取第一次的毫秒时间
                Long beginTimer = Long.valueOf(codeValue.split(":")[1]);
                if(System.currentTimeMillis()-beginTimer<60*1000){
                    throw new RuntimeException("一分钟以内不能重复获取验证码!!!");
                }
                //如果没有走抛异常就证明验证码依然在5分钟以内,但是超过了1分钟
                code = codeValue.split(":")[0];
            }
            //2.把验证码存储到redis中,并且有效期是5分钟
            redisTemplate.opsForValue().set(type+":"+phone,
                    code+":"+System.currentTimeMillis(),5, TimeUnit.MINUTES);
            String content = "尊敬的用户,你的验证码为:"+code+",请在5分钟以内完成验证操作!!";
            System.out.println(content);
           // 发送验证码
           // SendMsgUtil.sendMsg(phone, content);
        }
    
    
    }
    
    复制代码

手机发送验证码的包

<!-- https://mvnrepository.com/artifact/commons-httpclient/commons-httpclient -->
            <dependency>
                <groupId>commons-httpclient</groupId>
                <artifactId>commons-httpclient</artifactId>
                <version>3.1</version>
            </dependency>
复制代码

发送验证码的工具类

package cn.itsource.pethome.basic.util;


import org.apache.commons.httpclient.Header;
import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.NameValuePair;
import org.apache.commons.httpclient.methods.PostMethod;

import java.io.IOException;

public class SendMsgUtil {
    //本站用户名
    public static final String UID = "huangxuyang";
    public static final String KEY = "d41d8cd98f00b204e980";

    /**
     * 发送邮件
     * @param phone  手机号码
     * @param content  短信内容
     */
    public static void sendMsg(String phone,String content){
        PostMethod post = null;
        try {
            //创建客户端
            HttpClient client = new HttpClient();
            //发送post请求
            post = new PostMethod("http://utf8.api.smschinese.cn");
            //添加请求头信息
            post.addRequestHeader("Content-Type","application/x-www-form-urlencoded;charset=utf8");//在头文件中设置转码
            //设置请求的基本信息
            NameValuePair[] data ={ new NameValuePair("Uid", UID),
                    new NameValuePair("Key", KEY),
                    new NameValuePair("smsMob",phone),
                    new NameValuePair("smsText",content)};
            //设置请求体
            post.setRequestBody(data);
            //开始调用
            client.executeMethod(post);

            //以下代码没什么用了,就是返回响应状态而已
            Header[] headers = post.getResponseHeaders();
            int statusCode = post.getStatusCode();
            System.out.println("statusCode:"+statusCode);
            for(Header h : headers)
            {
                System.out.println(h.toString());
            }
            String result = new String(post.getResponseBodyAsString().getBytes("gbk"));
            System.out.println(result); //打印返回消息状态

        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if(post!=null){
                post.releaseConnection();
            }
        }
    }
}

复制代码

发送邮件的配置

# 设置邮箱主机(服务商)
spring.mail.host=smtp.qq.com
# 设置用户名
spring.mail.username=798477672@qq.com

# 设置密码,该处的密码是QQ邮箱开启SMTP的授权码而非QQ密码
spring.mail.password=eglwfhofzaoubche

# 必须进行授权认证,它的目的就是阻止他人任意乱发邮件
spring.mail.properties.mail.smtp.auth=true

#SMTP加密方式:连接到一个TLS保护连接
spring.mail.properties.mail.smtp.starttls.enable=true
spring.mail.properties.mail.smtp.starttls.required=true
复制代码

发送邮件的实现

package cn.itsource.pethome;

import cn.itsource.pethome.App;
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.mail.SimpleMailMessage;
import org.springframework.mail.javamail.JavaMailSender;
import org.springframework.mail.javamail.MimeMessageHelper;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import javax.mail.MessagingException;
import javax.mail.internet.MimeMessage;
import java.io.File;

@SpringBootTest(classes = App.class)
@RunWith(SpringJUnit4ClassRunner.class)
public class SimpleEmailTest {

    @Autowired
    private JavaMailSender javaMailSender;

    @Test
    public void test(){
        //首先创建普通邮件
        SimpleMailMessage message = new SimpleMailMessage();
        //设置普通邮件的发件人
        message.setFrom("798477672@qq.com");
        //设置收件人
        message.setTo("798477672@qq.com");
        //设置邮件标题
        message.setSubject("入学通知");
        //设置文件内容
        message.setText("恭喜你,我校对于你的表现十分满意,特招你成为我们的学生——新东方技术学院");
        //发送邮件
        javaMailSender.send(message);
    }

    @Test
    public void test2() throws MessagingException {
        //创建复杂文件对象
        MimeMessage mimeMessage = javaMailSender.createMimeMessage();
        //获取复杂邮件的工具类
        MimeMessageHelper messageHelper = new MimeMessageHelper(mimeMessage, true, "utf-8");
        //设置发件人
        messageHelper.setFrom("798477672@qq.com");
        //设置文件标题
        messageHelper.setSubject("天上人间邀请函");
        //设置文件内容
        messageHelper.setText("<h1>天上人间</h1>"
        +"<img src='https://pic.feizl.com/upload2007/allimg/170628/1KK2IF-14.jpg' />" ,true);
        //设置附件
        messageHelper.addAttachment("1.jpg", new File("D:\\图片壁纸\\壁纸\\1.jpg"));
        //设置收件人
        messageHelper.setTo("798477672@qq.com");
        //发送邮件
        javaMailSender.send(mimeMessage);
    }
}


复制代码

MD5加密工具类

package cn.itsource.pethome.basic.util;


import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;

public class MD5Utils {

    /**
     * 加密
     * @param context
     */
    public static String encrypByMd5(String context) {
        try {  
            MessageDigest md = MessageDigest.getInstance("MD5");
            md.update(context.getBytes());//update处理  
            byte [] encryContext = md.digest();//调用该方法完成计算  
  
            int i;  
            StringBuffer buf = new StringBuffer("");  
            for (int offset = 0; offset < encryContext.length; offset++) {//做相应的转化(十六进制)  
                i = encryContext[offset];  
                if (i < 0) i += 256;  
                if (i < 16) buf.append("0");  
                buf.append(Integer.toHexString(i));  
           }  
            return buf.toString();
        } catch (NoSuchAlgorithmException e) {
            // TODO Auto-generated catch block  
            e.printStackTrace();
            return  null;
        }  
    }

    public static void main(String[] args) {
        //加密
        System.out.println(MD5Utils.encrypByMd5("1")+"37XFtqiwKz");
        //加密加盐 查询用户时,除了查到加密密码外,还能查到颜值。 把输入密码+盐值加密和数据库存放密码比对就OK
        System.out.println(MD5Utils.encrypByMd5("123456"+ StrUtils.getComplexRandomString(32)));
        System.out.println(MD5Utils.encrypByMd5("123456"+ StrUtils.getComplexRandomString(32)));
        System.out.println(MD5Utils.encrypByMd5("123456"+ StrUtils.getComplexRandomString(32)));
    }

}
复制代码

storage的使用

 <script type="text/javascript">
        new Vue({
            el: "#app",
            mounted(){
                //获取url?传递的值
               let param = getParam()
                //发送axios请求,通过授权码获取token
                this.$http.post("/wechat/getToken",param).then(res=>{
                    console.debug(res.data);
                    let {success, msg, resultobj} = res.data;
                    //如果成功,并且openid有值
                    if (success && resultobj.openid) {
                        location.href = "binder.html?openid=" + resultobj.openid;
                    } else if (success && resultobj.token && resultobj.loginUser) {
                        //将token'值保存到localStorage中
                        localStorage.setItem("token", resultobj.token);
                        localStorage.setItem("loginUser", JSON.stringify(resultobj.loginUser));
                        location.href = "/index.html";
                    }

                })
            }
        })
    </script>
复制代码

保存密码加密

package cn.itsource.pethome.user.service.impl;

import cn.itsource.pethome.basic.constant.PetHomeConstant;
import cn.itsource.pethome.org.domain.Employee;
import cn.itsource.pethome.user.domain.Logininfo;
import cn.itsource.pethome.user.domain.User;
import cn.itsource.pethome.user.domain.dto.UserDto;
import cn.itsource.pethome.user.mapper.LogininfoMapper;
import cn.itsource.pethome.user.mapper.UserMapper;
import cn.itsource.pethome.user.service.IUserService;
import cn.itsource.pethome.basic.util.MD5Utils;
import cn.itsource.pethome.basic.util.StrUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;

@Service
@Transactional(readOnly = true,propagation = Propagation.SUPPORTS)
public class UserServiceImpl implements IUserService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private LogininfoMapper logininfoMapper;
    
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 注册用户
     * @param userDto
     */
    @Override
    public void register(UserDto userDto) {
        //校验传入的临时数据对象进行
        checkUserDto(userDto);
        //将临时数据转为登陆信息
        Logininfo logininfo = userDto2Logininfo(userDto);
        //保存登陆信息
        logininfoMapper.save(logininfo);
        //将登陆信息logininfo对象转化为user对象
        User user = Logininfo2User(logininfo);
        //保存user对象
        userMapper.save(user);
    }

    /**
     * 将logininfo登陆信息对象转化user对象
     * @param logininfo
     * @return
     */
    private User Logininfo2User(Logininfo logininfo) {
        User user = new User();
        //使用方法拷贝对象,只拷贝字段相同的属性
        BeanUtils.copyProperties(logininfo, user);
        //设置激活状态
        user.setState(PetHomeConstant.STATEOK);
        //设置登录信息
        user.setLogininfo(logininfo);
        return user;
    }

    /**
     * 将临时对象数据转为为登陆信息
     * @param userDto
     * @return
     */
    private Logininfo userDto2Logininfo(UserDto userDto) {
        Logininfo logininfo = new Logininfo();
        //设置用户名
        logininfo.setUsername(userDto.getPhone());
        //设置手机号
        logininfo.setPhone(userDto.getPhone());
        //设置盐值
        logininfo.setSalt(StrUtils.getComplexRandomString(10));
        //设置密码(使用盐值进行加密)
        logininfo.setPassword(MD5Utils.encrypByMd5(userDto.getPassword()+logininfo.getSalt()));
        //登录的类型(是后端登录还是前端登录)  0代表管理员,1代表普通用户,true默认为前端用户
        logininfo.setType(true);
        //disable是否可用默认为可用
        return  logininfo;
    }

    /**
     * 校验前端传入的注册信息
     * @param userDto
     */
    public void checkUserDto(UserDto userDto) {
        //1.校验前端传入的注册信息是否为空
        if(StringUtils.isEmpty(userDto.getCode()) || StringUtils.isEmpty(userDto.getPhone())
                || StringUtils.isEmpty(userDto.getPassword()) || StringUtils.isEmpty(userDto.getPasswordRepeat())){
            throw new RuntimeException("请输入完整信息!");
        }
        //2.校验手机号是否已被注册
            //查询根据前端传入的手机号与数据库比对
        User user = userMapper.findonebyphone(userDto.getPhone());
        //如果能查到手机号对应的用户,说明已被注册
        if(user!=null){
            throw new RuntimeException("该手机号已被注册");
        }
        //3.校验用户输入的两次密码是否一致
        if (!userDto.getPassword().equals(userDto.getPasswordRepeat())) {
            throw new RuntimeException("两次输出的密码不一致!");
        }
        //4.校验验证码是否过期
            //从redis内存中获取用户对应的验证码的信息,此时获得的验证码格式为 code+时间戳
        String codeValue = (String) redisTemplate.opsForValue().get(PetHomeConstant.REGISTER_VERIFICATION_CODE + ":" + userDto.getPhone());
            //判断验证码是否过期
        if (StringUtils.isEmpty(codeValue)) {
            throw new RuntimeException("验证码已过期,请重新获取");
        }
            //获取redis中的验证码
        String code = codeValue.split(":")[0];
            //如果内存中保存的验证码和前端传入的验证码不同,说明验证码已过期
        System.out.println(code);
        if (!code.toLowerCase().equals(userDto.getCode().toLowerCase())) {
            throw new RuntimeException("输入的验证码错误!");
        }
    }
}

复制代码

后端控制器

package cn.itsource.pethome.user.interceptor;

import cn.itsource.pethome.user.domain.Logininfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.concurrent.TimeUnit;


/**
 * 配置后端拦截器,判断当前请求是否有token,没有则告诉前端,尚未登录,无权限访问
 */
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    private RedisTemplate redisTemplate;
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        System.out.println(request.getRequestURL());
        //获取token的值
        String token = request.getHeader("token");
        if(StringUtils.isEmpty(token)){
//            提示前端用户,登录超时,请重新登录
            wirteError(response);
            return false;
        }
        Logininfo loginInfo = (Logininfo) redisTemplate.opsForValue().get(token);
        if(loginInfo == null){
            // 提示前端用户,登录超时,请重新登录
            wirteError(response);
            return false;
        }
        redisTemplate.opsForValue().set(token, loginInfo, 30, TimeUnit.MINUTES);
        return true;
    }
    private void wirteError(HttpServletResponse response){
        try {
            response.setContentType("text/json;charset=utf-8");
            PrintWriter writer = response.getWriter();
            writer.write("{\"success\":false,\"msg\":\"noLogin\"}");
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
复制代码

axios前置后置拦截器

Vue.prototype.$http = axios;
/*给所有的axios请求,添加请求前缀*/
axios.defaults.baseURL="http://localhost";
Vue.prototype.dfsUrl="http://115.159.217.249:8888";


/*axios前置拦截器*/
axios.interceptors.request.use(config => {
    //携带token
    let token = localStorage.getItem("token");
    if (token) {
        //在头信息中添加token
        config.headers['token'] = token;
    }
    return config;
}, error => {
    Promise.reject(error);
});
//动态获取url地址?
//axios后置拦截器
axios.interceptors.response.use(result => {
    let {success, msg} = result.data;
    if(!success && msg === "noLogin"){
        //清空本地存储
        localStorage.removeItem("token");
        localStorage.removeItem("loginUser");
        router.push({ path: '/login' });
    }
    return result;
}, error => {
    Promise.reject(error);
});
复制代码

微信获取token的插件

package cn.itsource.pethome.basic.util;

import org.apache.commons.httpclient.HttpClient;
import org.apache.commons.httpclient.methods.GetMethod;
import org.apache.commons.httpclient.params.HttpMethodParams;

import java.io.IOException;

/**
 * 使用httpclient组件发送http请求
 *   get:现在只用到get
 *   post
 */
public class HttpClientUtils {
    /**
     * 发送get请求
     * @param url 请求地址
     * @return 返回内容 json
     */
    public static String httpGet(String url){

        // 1 创建发起请求客户端
        try {
            HttpClient client = new HttpClient();
            // 2 创建要发起请求-tet
            GetMethod getMethod = new GetMethod(url);
//            getMethod.addRequestHeader("Content-Type",
//                    "application/x-www-form-urlencoded;charset=UTF-8");
            getMethod.getParams().setParameter(HttpMethodParams.HTTP_CONTENT_CHARSET,"utf8");
            // 3 通过客户端传入请求就可以发起请求,获取响应对象
            client.executeMethod(getMethod);
            // 4 提取响应json字符串返回
            String result = new String(getMethod.getResponseBodyAsString().getBytes("utf8"));
            return result;
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }
}

复制代码

微信登陆的实现

package cn.itsource.pethome.user.service.impl;

import cn.itsource.pethome.basic.constant.PetHomeConstant;
import cn.itsource.pethome.basic.util.HttpClientUtils;
import cn.itsource.pethome.basic.util.MD5Utils;
import cn.itsource.pethome.basic.util.StrUtils;
import cn.itsource.pethome.user.domain.Logininfo;
import cn.itsource.pethome.user.domain.User;
import cn.itsource.pethome.user.domain.WechatUser;
import cn.itsource.pethome.user.domain.dto.UserDto;
import cn.itsource.pethome.user.mapper.LogininfoMapper;
import cn.itsource.pethome.user.mapper.UserMapper;
import cn.itsource.pethome.user.mapper.WechatMapper;
import cn.itsource.pethome.user.service.IWechatService;
import com.alibaba.fastjson.JSONObject;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

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

@Service
public class WechatServerImpl implements IWechatService {

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private LogininfoMapper logininfoMapper;
    @Autowired
    private WechatMapper wechatMapper;

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 判断用户是否绑定
     * @param code  授权码
     * @return
     */
    @Override
    public Map<String, Object> getToken(String code) {

        //首先根据code获取token的url地址
        String tokenUrl = PetHomeConstant.TOKENURL.replace("APPID", PetHomeConstant.APPID)
                .replace("SECRET", PetHomeConstant.SECRET)
                .replace("CODE", code);

        //然后向url地址发送get请求获取json字符串格式的
        String httpGet = HttpClientUtils.httpGet(tokenUrl);
        //将json字符串转化为json对象
        JSONObject jsonObject = JSONObject.parseObject(httpGet);
        System.out.println(jsonObject);
        //通过json对象获取openid唯一标识
        String openid = jsonObject.getString("openid");
        //获取token
        String token = jsonObject.getString("access_token");
        //获取unionid,当用户授权之后才有的标识
        String unionid = (String) jsonObject.get("unionid");
        //通过token获取用户资源url
        String userinfoUrl = PetHomeConstant.USERINFOURL.replace("ACCESS_TOKEN", token)
                .replace("OPENID", openid);
        //通过资源url获取用户信息
        String userinfo = HttpClientUtils.httpGet(userinfoUrl);
        //将用户信息json字符串转为json对象
        JSONObject userObject = JSONObject.parseObject(userinfo);
        System.out.println(userObject);
        //获取唯一标识
        String userOpenid = userObject.getString("openid");
        //通过用户的唯一标识openid去数据库查找对应的数据
        WechatUser wechatUser = wechatMapper.loadByopenid(userOpenid);
        //设置一个map,封装数据返回给前端
        Map<String, Object> map = new HashMap<>();
        //如果查询的用户为空,说明用户还没有绑定登陆信息
        if (wechatUser == null) {
            WechatUser user = new WechatUser();
            //设置唯一标识
            user.setOpenid(userOpenid);
            //设置用户名
            user.setNickname(userObject.getString("nickname"));
            //设置性别
            user.setSex(userObject.getBoolean("sex"));
            //设置地址
            user.setAddress(userObject.getString("province")+userObject.getString("city"));
            //设置头像
            user.setHeadimgurl(userObject.getString("headimgurl"));
            //设置授权后唯一标识
            user.setUnionid(userObject.getString("unionid"));
            //保存当前用户
            wechatMapper.save(user);
            //将用户的唯一标识响应前端,跳转到授权页面
            map.put("openid", openid);
            return map;
        }else{//如果user不为空,判断用户是否绑定登陆信息
            Logininfo logininfo = wechatUser.getLogininfo();
            System.out.println(logininfo+"--------------------------------------1");
            if (logininfo == null) {
                //响应给前端跳转到绑定页面
                map.put("openid", openid);
                return map;
            }else{//不为空,代表已经绑定了,直接登陆
                System.out.println(logininfo+"----------------------------------------2");
                //创建token
                String token1 = UUID.randomUUID().toString();
                //将token放入内存中
                redisTemplate.opsForValue().set(token1, logininfo);
                //将token和用户信息返回
                map.put("token", token1);
                map.put("loginUser", logininfo);
                return map;
            }

        }


    }

    /**
     * 进行绑定跳转
     * @param userDto
     * @return
     */
    @Override
    public Map<String, Object> binder(UserDto userDto) {
        //首先校验前端数据
        checkDto(userDto);
        //设置用户名,方便在登陆信息中查询该用户对象
        userDto.setUsername(userDto.getPhone());
        //创建map集合,封装登录信息和token信息
        Map<String, Object> map = new HashMap<>();
        //根据用户名查询登陆信息
        Logininfo logininfo = logininfoMapper.loadByUserDto(userDto);
        if (logininfo == null) {//登陆信息为空
             logininfo = new Logininfo();
            //将用户信息转为登陆信息
            logininfo =userDto2logininfo(userDto);
            //保存登陆信息
            logininfoMapper.save(logininfo);
            //将登陆信息转为user对象信息
            User user = logiinfo2User(logininfo);
            //保存user信息
            userMapper.save(user);
        }
        //根据查询出来的登陆信息,将微信扫描码的用户信息与登陆信息关联
        wechatMapper.binder(logininfo.getId(), userDto.getOpenid());
        //然后创建token,返回前端,直接登陆
        //创建token
        String token1 = UUID.randomUUID().toString();
        //将token放入内存中
        redisTemplate.opsForValue().set("token", token1);
        //将token和用户信息返回
        map.put("token", token1);
        map.put("loginUser", logininfo);
        return map;
    }

    /**
     * 将登陆信息转为user信息
     * @param logininfo
     */
    private User logiinfo2User(Logininfo logininfo) {
        User user = new User();
        BeanUtils.copyProperties(logininfo, user);
        user.setState(PetHomeConstant.STATEOK);
        user.setLogininfo(logininfo);
        return user;
    }

    /**
     * 将临时信息转为登陆信息
     * @param userDto
     * @return
     */
    private Logininfo userDto2logininfo(UserDto userDto) {
        Logininfo logininfo = new Logininfo();
        logininfo.setUsername(userDto.getPhone());
        logininfo.setPhone(userDto.getPhone());
        logininfo.setType(userDto.getType());
        logininfo.setSalt(StrUtils.getComplexRandomString(10));
        logininfo.setPassword(MD5Utils.encrypByMd5(logininfo.getPhone()+logininfo.getSalt()));
        return logininfo;
    }

    private void checkDto(UserDto userDto) {
        //校验输入信息完整
        if(StringUtils.isEmpty(userDto.getPhone())||
                StringUtils.isEmpty(userDto.getCode())||
                StringUtils.isEmpty(userDto.getType())){
            throw new RuntimeException("请输入完整信息");
        }
        //校验验证码
        //从内存中获取验证码
        String codeValue = (String) redisTemplate.opsForValue().get(PetHomeConstant.BINDER_VERIFICATION_CODE + ":" + userDto.getPhone());
        if (codeValue == null) {
            throw new RuntimeException("验证码已过期");
        }
        //比对用户输入的验证码和redis内存中的验证码是否相等
        if (!codeValue.split(":")[0].toLowerCase().equals(userDto.getCode().toLowerCase())) {
            throw new RuntimeException("验证码错误");
        }
    }

}

复制代码

随机生成订单号

/**
 * 存放经纬度
 */
@Data
public class Point {
    //经度
    private Double lng;
    //维度
    private Double lat;
}



package cn.itsource.pethome.basic.util;

import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;


public class CodeGenerateUtils {
	
	/**
	 * 获取商品编码
	 * 商品编码规则:nanoTime(后5位)*5位随机数(10000~99999)
	 * @return
	 */
	public static String generateProductCode(){
		long nanoPart = System.nanoTime() % 100000L;
		if(nanoPart<10000L){
			nanoPart+=10000L;
		}
		long randomPart = (long)(Math.random()*(90000)+10000);
		String code = "0"+String.valueOf((new BigDecimal(nanoPart).multiply(new BigDecimal(randomPart))));
		return code.substring(code.length()-10);
	}
	
	/**
	 * @param id: 用户id
	 * 生成订单编号
	 * 订单编号规则:(10位):(年末尾*月,取后2位)+(用户ID%3.33*日取整后2位)+(timestamp*10000以内随机数,取后6位)
	 * @return
	 */
	public static String generateOrderSn(long id){
		Calendar calendar = Calendar.getInstance();
		int year = calendar.get(Calendar.YEAR);
		year = year % 10;
		if(year == 0) year = 10;
		int month = calendar.get(Calendar.MONTH)+1;
		int yearMonth  =  year * month;
		String yearMonthPart = "0"+yearMonth;
		yearMonthPart = yearMonthPart.substring(yearMonthPart.length() - 2 );
		
		int day = calendar.get(Calendar.DAY_OF_MONTH);
		int dayNum = (int)((id % 3.33) * day);
		String dayPart = "0"+dayNum;
		dayPart = dayPart.substring(dayPart.length() - 2);
		
		String timestampPart = ""+(Math.random() * 10000) * (System.currentTimeMillis()/10000);
		timestampPart = timestampPart.replace(".", "").replace("E", "");
		timestampPart = timestampPart.substring(0,6);
		return yearMonthPart+dayPart+timestampPart;
	}
	
	/**
	 * 生成统一支付单号
	 * 规则:年(2)月(2)日(2)时(2)分(2)+timestamp*5位随机整数取后5位
	 * @return
	 */
	public static String generateUnionPaySn(){
		Calendar calendar = Calendar.getInstance();
		SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddhhmm");
		String dateTime = dateFormat.format(calendar.getTime());
		dateTime = dateTime.substring(2);
		String timestampPart = ""+(Math.random() * 10000) * (System.currentTimeMillis()/10000);
		timestampPart = timestampPart.replace(".", "").replace("E", "");
		timestampPart = timestampPart.substring(0,5);
		return dateTime+timestampPart;
	}
	
	public static void main(String[] args) {
		for(long i=0;i<100;i++)
		{
			//String timestampPart = ""+(Math.random() * 10000) * (System.currentTimeMillis()/10000);
			//System.out.println(timestampPart);
			//System.out.println(generateOrderSn(i));
			System.out.println(generateUnionPaySn());
		}
	}
	
}

复制代码

通过地址获取经纬度距离

package cn.itsource.pethome.basic.util;


import cn.itsource.pethome.org.domain.Shop;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;

/**
 * 位置相关工具类
 */
public class DistanceUtil {

    /**
     * 通过地址转为经纬度
     * @param address
     * @return
     */
    public static  Point getPoint(String address){
        String Application_ID="PQ9FAt6qg7taDWj6LLABYO7u6bSETXhD";//配置上自己的百度地图应用的AK
        try{
            String sCurrentLine; String sTotalString;sCurrentLine ="";
            sTotalString = "";
            InputStream l_urlStream;
            URL l_url = new URL("http://api.map.baidu.com/geocoding/v3/?address="+address+"&output=json&ak="+Application_ID+"&callback=showLocation");
            HttpURLConnection l_connection = (HttpURLConnection) l_url.openConnection();
            l_connection.connect();
            l_urlStream = l_connection.getInputStream();
            java.io.BufferedReader l_reader = new java.io.BufferedReader(new InputStreamReader(l_urlStream));
            String str=l_reader.readLine();
            System.out.println(str);
            //用经度分割返回的网页代码  
            String s=","+"\""+"lat"+"\""+":";
            String strs[]=str.split(s,2);
            String s1="\""+"lng"+"\""+":";
            String a[]=strs[0].split(s1, 2);
            s1="}"+","+"\"";
            String a1[]=strs[1].split(s1,2);

            Point point=new Point();
            point.setLng(Double.valueOf(a[1]));
            point.setLat(Double.valueOf(a1[0]));
            return point;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //地球半径,进行经纬度运算需要用到的数据之一
    private static final double EARTH_RADIUS = 6378137;
    //根据坐标点获取弧度
    private static double rad(double d)
    {
        return d * Math.PI / 180.0;
    }

    /**
     * 根据两点间经纬度坐标(double值),计算两点间距离,单位为米
     * @param point1 A点坐标
     * @param point2 B点坐标
     * @return
     */
    public static double getDistance(Point point1,Point point2)
    {
        double radLat1 = rad(point1.getLat());
        double radLat2 = rad(point2.getLat());
        double a = radLat1 - radLat2;
        double b = rad(point1.getLng()) - rad(point2.getLng());
        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2) +
                Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2)));
        s = s * EARTH_RADIUS;
        s = Math.round(s * 10000) / 10000;
        return s;
    }

    /**
     * 根据两点间经纬度坐标(double值),计算两点间距离,单位为米
     * @param point 用户指定的地址坐标
     * @param shops 商店
     * @return
     */
    public static Shop getNearestShop (Point point, List<Shop> shops) {

        //如果传过来的集合只有一家店铺,那么直接将这家店铺的信息返回就是最近的店铺了
        Shop shop=shops.get(0);
        //获取集合中第一家店铺到指定地点的距离
        double distance=getDistance(point,getPoint(shops.get(0).getAddress()));
        //如果有多家店铺,那么就和第一家店铺到指定地点的距离做比较
        if (shops.size()>1){
            for (int i=1;i<shops.size();i++){
                if (getDistance(point,getPoint(shops.get(i).getAddress()))<distance){
                    shop=shops.get(i);
                }
            }
        }
        return shop;
    }

    public static void main(String[] args) {
        System.out.println(getPoint("成都市武侯区天府新谷-10号楼"));
    }
}
复制代码

fastdfs上传下载文件

package cn.itsource.pethome.basic.util;


import cn.itsource.pethome.org.domain.Shop;

import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.List;

/**
 * 位置相关工具类
 */
public class DistanceUtil {

    /**
     * 通过地址转为经纬度
     * @param address
     * @return
     */
    public static  Point getPoint(String address){
        String Application_ID="PQ9FAt6qg7taDWj6LLABYO7u6bSETXhD";//配置上自己的百度地图应用的AK
        try{
            String sCurrentLine; String sTotalString;sCurrentLine ="";
            sTotalString = "";
            InputStream l_urlStream;
            URL l_url = new URL("http://api.map.baidu.com/geocoding/v3/?address="+address+"&output=json&ak="+Application_ID+"&callback=showLocation");
            HttpURLConnection l_connection = (HttpURLConnection) l_url.openConnection();
            l_connection.connect();
            l_urlStream = l_connection.getInputStream();
            java.io.BufferedReader l_reader = new java.io.BufferedReader(new InputStreamReader(l_urlStream));
            String str=l_reader.readLine();
            System.out.println(str);
            //用经度分割返回的网页代码  
            String s=","+"\""+"lat"+"\""+":";
            String strs[]=str.split(s,2);
            String s1="\""+"lng"+"\""+":";
            String a[]=strs[0].split(s1, 2);
            s1="}"+","+"\"";
            String a1[]=strs[1].split(s1,2);

            Point point=new Point();
            point.setLng(Double.valueOf(a[1]));
            point.setLat(Double.valueOf(a1[0]));
            return point;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    //地球半径,进行经纬度运算需要用到的数据之一
    private static final double EARTH_RADIUS = 6378137;
    //根据坐标点获取弧度
    private static double rad(double d)
    {
        return d * Math.PI / 180.0;
    }

    /**
     * 根据两点间经纬度坐标(double值),计算两点间距离,单位为米
     * @param point1 A点坐标
     * @param point2 B点坐标
     * @return
     */
    public static double getDistance(Point point1,Point point2)
    {
        double radLat1 = rad(point1.getLat());
        double radLat2 = rad(point2.getLat());
        double a = radLat1 - radLat2;
        double b = rad(point1.getLng()) - rad(point2.getLng());
        double s = 2 * Math.asin(Math.sqrt(Math.pow(Math.sin(a/2),2) +
                Math.cos(radLat1)*Math.cos(radLat2)*Math.pow(Math.sin(b/2),2)));
        s = s * EARTH_RADIUS;
        s = Math.round(s * 10000) / 10000;
        return s;
    }

    /**
     * 根据两点间经纬度坐标(double值),计算两点间距离,单位为米
     * @param point 用户指定的地址坐标
     * @param shops 商店
     * @return
     */
    public static Shop getNearestShop (Point point, List<Shop> shops) {

        //如果传过来的集合只有一家店铺,那么直接将这家店铺的信息返回就是最近的店铺了
        Shop shop=shops.get(0);
        //获取集合中第一家店铺到指定地点的距离
        double distance=getDistance(point,getPoint(shops.get(0).getAddress()));
        //如果有多家店铺,那么就和第一家店铺到指定地点的距离做比较
        if (shops.size()>1){
            for (int i=1;i<shops.size();i++){
                if (getDistance(point,getPoint(shops.get(i).getAddress()))<distance){
                    shop=shops.get(i);
                }
            }
        }
        return shop;
    }

    public static void main(String[] args) {
        System.out.println(getPoint("成都市武侯区天府新谷-10号楼"));
    }
}
复制代码

常用包

 <!--对redis的支持-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-data-redis</artifactId>
            </dependency>
            <!-- 模拟发送http请求 -->
            <dependency>
                <groupId>commons-httpclient</groupId>
                <artifactId>commons-httpclient</artifactId>
                <version>3.1</version>
            </dependency>
            <!--处理json-->
            <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson -->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.58</version>
            </dependency>
            <dependency>
                <groupId>org.apache.commons</groupId>
                <artifactId>commons-lang3</artifactId>
                <version>3.7</version>
            </dependency>
            <!--支付宝支付所需jar包-->
            <dependency>
                <groupId>com.alipay.sdk</groupId>
                <artifactId>alipay-sdk-java</artifactId>
                <version>4.9.13.ALL</version>
            </dependency>
复制代码