从零开始,搭建一个简单的购物平台(十八)前端商城部分

本文最后更新于:6 个月前

从零开始,搭建一个简单的购物平台(十七)前端商城部分:
https://blog.csdn.net/time_____/article/details/108893925
项目源码(持续更新):https://gitee.com/DieHunter/myCode/tree/master/shopping

上篇文章对购物车进行了简单的介绍,商城的所有基础功能已经全部实现,这篇文章开始将介绍一下用户信息和订单相关的功能实现,用户信息登录在后端管理中已经实现,我们需要实现一个注册功能,和邮箱验证功能,具体实现可参照之前的一篇博客或是这篇文章

实现效果如下,分别是用户名密码登录,邮箱验证登录,注册功能

登录部分

账号密码登录与后台管理系统的登录一样,若用户输入用户名密码登录成功,则将用户部分信息加密成token传至前端并保存至本地,每次通过将token值发送后端进行继续操作

邮箱验证

在服务端的config文件中新建EmailTransporter静态变量,用来配置发送邮箱使用到的nodemailer模块

EmailTransporter: {
    // service: "qq", // 运营商  qq邮箱 网易  若使用QQ邮箱,则只需配置service:qq
    host: "smtp.163.com",// 若使用网易邮箱,则只需配置host:smtp.163.com
    port: 465, //端口
    auth: {
      user: "132*****516@163.com", //发送方的邮箱
      pass: "WAQM******WQEFKB", // pop3 授权码
    },
  },

然后新建邮箱验证码配置EmailConfig

EmailConfig: {
  time: 5,//验证码有效期,单位分钟
  codeLength: 4,//验证码长度
  sendTime: 1 * 60 * 1000,//后端验证码允许再次发送时间间隔,单位毫秒
  targetTime: 5 * 60 * 1000,//验证码有效期,单位毫秒
  title: "零食商贩",//验证码标题
},

接着在Utils中写一个生成随机验证码函数

/* 生成随机
 * @method    codeLength   函数名
 * @for    Utils     附属于哪个类
 * @param {number/string} len  随机数长度
 * @return {string}  _count   生成len个随机数
 */
static codeLength(len) {
  let _count = "";
  for (let i = 0; i < len; i++) {
    _count += Math.floor(Math.random() * 10); //生成len个随机数
  }
  return _count;
}

在Utils生成时间戳函数,记录验证码及验证码发送时间和有效时间

/* 生成时间戳
  * @method    randomCode
  * @for    Utils
  * @param
  * @return {object}  _count   生成len个随机数
  */
 static randomCode() {
   return {
     code: this.codeLength(EmailConfig.codeLength), //生成的随机数
     sendTime: new Date().getTime() + EmailConfig.sendTime, //发送时间
     targetTime: new Date().getTime() + EmailConfig.targetTime, //截止时间
   };
 }

在Utils文件夹下新建SendMail.js并新建发送邮件模块,在utils中调用

const nodemailer = require("nodemailer");
const Config = require("../config/config");
module.exports = class SendMail {
  static transporter = nodemailer.createTransport(Config.EmailTransporter); //邮箱配置项
  static mailOptions = null; //邮箱配置
  /* 发送邮件模块
   * @method    sendEmail
   * @for       SendMail
   * @param   {String} mail  用户邮箱号
   * @param   {String} title  邮件标题
   * @param   {String} content  邮件内容
   * @return {Boolean}   是否发送成功
   */
  static async sendEmail(mail, title, content) {
    this.mailOptions = {
      from: '"邮箱验证" <' + Config.EmailTransporter.auth.user + ">",
      to: mail,
      subject: title,
      text: content,
    };
    try {
      let result = await this.transporter.sendMail(this.mailOptions);
      console.log("发送成功");
      return true;
    } catch (error) {
      console.log(error);
      console.log("发送失败");
      return false;
    }
  }
};

在utils中新建生成邮件内容的函数

/* 生成邮件内容
   * @method    sendEmailCode
   * @for    Utils
   * @param   {String} code  验证码内容
   * @param   {String} email   用户邮箱
   */
  static async sendEmailCode(code, email) {
    return await SendMail.sendEmail(
      email,
      EmailConfig.title,
      `您的验证码为:${code},${EmailConfig.time}分钟内有效`
    );
  }

最后在utils编写一个异步发送邮箱的函数

/* 异步发送邮箱验证
   * @method    createEmailCode
   * @for    Utils
   * @param   {Object} codeList  邮箱验证码列表
   * @param   {String} email   用户邮箱
   * @param   {Object} findRes  数据库搜寻到的用户信息
   * @return {Boolean}  isSuccess   是否发送成功
   */
  static async createEmailCode(codeList, email, findRes) {
    if (!codeList[email] || new Date().getTime() > codeList[email].sendTime) {
      //已过1分钟,防止多次请求邮箱
      codeList[email] = this.randomCode();
      codeList[email].info = findRes;
      return await this.sendEmailCode(codeList[email].code, email);
    } else {
      //未过1分钟
      return false;
    }
  }

一个发送邮件的完整模块就实现完成,下一步要做的是验证码的验证功能

/* 核对验证码
  * @method    checkEmailCode
  * @for    Utils
  * @param   {Object} codeList  用户验证码列表
  * @param   {String} key   用户邮箱
  * @param   {Object} _data   用户提交的表单信息
  * @return   {Object} res   请求响应返回值
  */
 static checkEmailCode(codeList, key, _data) {
   if (!codeList[key]) {
     //未发送验证码
     return {
       result: 0,
       msg: "验证码错误",
     };
   } else if (
     new Date().getTime() < codeList[key].targetTime &&
     _data.mailcode == codeList[key].code
   ) {
     //验证码校对成功
     let _obj = {
       result: 1,
       token: Utils.createToken(
         codeList[key].info.userType || "",
         codeList[key].info.username || "",
         _data.remember || ""
       ),
       msg: "操作成功",
     };
     codeList[key] = null;
     return _obj;
   } else if (new Date().getTime() > codeList[key].targetTime) {
     //验证码超时
     return {
       result: 0,
       msg: "验证码超时",
     };
   } else {
     return {
       result: 0,
       msg: "验证失败",
     };
   }
 }

到这一步,关于验证码的所有准备工作已全部实现,下一步将实现注册登录功能,其中登录有两个途径,注册时邮箱为必填值,所以可以使用邮箱验证的方式进行登录

服务端获取验证码接口,通过一个codeType区分用户登录获取验证码和注册获取验证码

router.get(Config.ServerApi.getMailCode, async (_req, res) => {
  //用户邮箱验证
  let _data = Util.getCrypto(Util.parseUrl(_req, res).crypto);//解密参数
  //查询用户是否存在,若未找到用户,则返回错误响应值,否则异步发送邮件验证码
  let findRes = await findData(Mod, {
    mailaddress: _data.username.split('@')[0],
    mailurl: '@' + _data.username.split('@')[1],
  });
  if ((!findRes.length || !findRes) && _data.codeType !== 'reg') {//过滤区分用户注册登录
    res.send({
      result: 0,
      msg: "用户未注册"
    });
    return;
  }
  await Util.createEmailCode(userCodeList, _data.username, findRes[0] || {}) ? res.send({
    result: 1,
    msg: "发送成功",
  }) : res.send({
    result: 0,
    msg: "发送失败"
  });
});

在实现注册部分之前,我们要写一个工具方法,用于验证码倒计时,在此期间用户无法再次点击发送请求

import Vue from "vue";
import Config from "../config/config";
const { GetCodeTime, CodeText } = Config;
class TimeTick {
  static timer = GetCodeTime / 1000;//倒计时时间
  static _timeTick = null;//定时器
  static timeTick(fn) {
    if (!TimeTick._timeTick) {
      TimeTick._timeTick = setInterval(() => {
        if (TimeTick.timer-- <= 1) {
          // 重置倒计时和发送邮箱验证开关
          TimeTick.clearTick();
          fn({ content: CodeText, res: 1 });//倒计时归零
        } else {
          fn({ content: TimeTick.timer + "S", res: 0 });//倒计时中,阻止用户重复点击
        }
      }, 1000);
    }
  }
  static clearTick() {
    //清除定时器
    clearInterval(TimeTick._timeTick);
    TimeTick._timeTick = null;
  }
}
Vue.prototype.$timeTick = TimeTick;

用户注册界面

<template>
  <div class="login">
    <div>
      <mt-field
        placeholder="请输入用户名"
        :state="userInfo.username.length ? 'success' : 'error'"
        v-model="userInfo.username"
      ></mt-field>
      <mt-field
        placeholder="请输入密码"
        :state="userInfo.password.length ? 'success' : 'error'"
        v-model="userInfo.password"
        type="password"
      ></mt-field>
      <mt-field
        placeholder="请重复输入密码"
        :state="
          userInfo.repassword.length && userInfo.password == userInfo.repassword
            ? 'success'
            : 'error'
        "
        v-model="userInfo.repassword"
        type="password"
      ></mt-field>
      <mt-field
        placeholder="请输入邮箱"
        v-model="userInfo.mailaddress"
        :state="userInfo.mailaddress.length ? 'success' : 'error'"
      >
        <mt-button class="btn" @click="selectMail">{{
          userInfo.mailurl
        }}</mt-button>
      </mt-field>
      <mt-field
        placeholder="请输入验证码"
        :state="userInfo.mailcode.length == 4 ? 'success' : 'error'"
        v-model="userInfo.mailcode"
        type="number"
      >
        <mt-button class="btn" :disabled="canGetCode" @click="getCode">{{
          codeTime
        }}</mt-button>
      </mt-field>
      <mt-button class="btn" type="primary" @click="submit">注册</mt-button>
      <div class="shopPicker"></div>
      <ShopPicker :ShopMaxCount="address" pickerTitle="邮箱类型"></ShopPicker>
    </div>
  </div>
</template>

<script>
import Config from "../../config/config";
import Mail from "../../config/mail";
import ShopPicker from "../shopPicker/shopPicker";
import { Field, Button, Picker, Popup } from "mint-ui";
import RegBussiness from "./bussiness";
const { GetCodeTime, EventName, CodeText } = Config;
const { address } = Mail;
export default {
  components: {
    ShopPicker,
  },
  data() {
    return {
      codeTime: CodeText, //获取验证码按钮显示值
      address, //邮箱默认地址
      canGetCode: false, //防止重复点击开关
      userInfo: {
        //注册表单默认数据
        username: "",
        password: "",
        repassword: "",
        mailurl: address[0],
        mailaddress: "",
        mailcode: "",
      },
    };
  },
  created() {
    this.regBussiness = new RegBussiness(this);
    this.$events.onEvent(EventName.ChangeCount, (_count) => {
      this.userInfo.mailurl = _count; //切换邮箱地址
    });
  },
  destroyed() {
    this.$events.offEvent(EventName.ChangeCount);
  },
  methods: {
    selectMail() {
      this.$events.emitEvent(EventName.ShowPicker);
    },
    getCode() {
      if (this.canGetCode) {
        //是否允许发送邮箱验证
        return;
      }
      this.regBussiness.sendCode().then((res) => {
        this.canGetCode = true;//关闭点击开关
        this.$timeTick.timeTick((state) => {
          this.codeTime = state.content;
          switch (state.res) {
            case 0:
              this.canGetCode = false;//允许用户点击
              break;
          }
        });
      });
    },
    submit() {
      this.regBussiness.submitData();
    },
  },
};
</script>

<style lang="less" scoped>
@import "../../style/init.less";

.login {
  .btn {
    .f_s(34);
    width: 100%;
    .h(100);
  }
}
</style>

注册业务逻辑部分,bussiness.js

import Vue from 'vue'
import {
  Toast
} from "mint-ui";
import config from "../../config/config"
const {
  ServerApi,
  StorageName,
  EventName
} = config
export default class LoginBussiness extends Vue {
  constructor(_vueComponent) {
    super()
    this.vueComponent = _vueComponent
  }
  sendCode() {
    return new Promise((resolve, reject) => {
      if (!this.vueComponent.userInfo.mailaddress.length) {//过滤邮箱长度为0
        Toast('请填写正确的邮箱');
        return
      }
      this.$axios
        .get(ServerApi.user.getMailCode, {
          params: {
            crypto: this.$crypto.setCrypto({
              codeType: "reg",//区分注册登录类型
              username: this.vueComponent.userInfo.mailaddress + this.vueComponent.userInfo.mailurl
            })
          },
        }).then(res => {
          switch (res.result) {
            case 1:
              Toast(res.msg);
              resolve(res)
              break;
            default:
              reject(res)
              break;
          }
        }).catch(err => {
          reject(err)
        })
    })

  }
  submitData() {
    for (const key in this.vueComponent.userInfo) {
      if (this.vueComponent.userInfo.hasOwnProperty(key) && !this.vueComponent.userInfo[key].length) {//过滤表单项长度为0
        Toast('请填写完整的信息');
        return
      }
    }
    this.$axios
      .post(ServerApi.user.userReg, {
        crypto: this.$crypto.setCrypto({
          ...this.vueComponent.userInfo
        })
      }).then(res => {
        //成功后重置用户信息
        this.vueComponent.userInfo.password = "";
        this.vueComponent.userInfo.repassword = "";
        this.vueComponent.userInfo.mailcode = "";
        switch (res.result) {
          case 1:
            Toast(res.msg);
            history.go(-1)//返回登录页面
            break;
          default:
            break;
        }
      })
  }
}

注册部分完成,登录与注册功能类似,这里只介绍一下服务端token的生成

在服务端user.js中修改接口,和管理系统登录一样,新增邮箱验证登录,区分管理员和用户登录

router.post(Config.ServerApi.userLogin, async (req, res) => {
  let _data = Util.getCrypto(Util.parseUrl(req, res).crypto); //解密前端入参
  switch (_data.loginType) {
    case "code"://验证码登录,验证邮箱验证码
      res.send(Util.checkEmailCode(userCodeList, _data.username, _data));
      break;
    case "psd"://密码登录
    default:
      let findRes = await findData(Mod, {
        $or: [
          {
            mailaddress: _data.username.split("@")[0],
            mailurl: "@" + _data.username.split("@")[1],
          },
          {
            username: _data.username,
          },
          {
            phoneNum: _data.username,
          },
        ],
      });
      if (findRes && findRes.length > 0) {
        Util.checkBcrypt(_data.password, findRes[0].password)
          ? res.send({
              result: 1,
              token: Util.createToken(//生成前端token
                findRes[0].userType,
                findRes[0].username,
                _data.remember
              ),
              msg: "登录成功",
            })
          : res.send({
              result: 0,
              msg: "密码错误",
            });
        return;
      }
      res.send({
        result: 0,
        msg: "用户不存在",
      });
      break;
  }
});

总结
本篇文章将用户的注册登录邮箱验证功能基本实现,主要功能参照之前的邮箱验证登录注册的博客,文章中的重点是邮箱验证功能模块,与注册登录配合使用,注册则新增用户,登录则更新token值。下一篇将介绍用户信息修改,及后续将实现订单的生成及查看功能