SpringBoot项目整合Vue做一个完整的用户注册功能

前言

用户注册功能是每一个系统的入口门面功能,很多人可能会以为很简单,不就是一个简单的CRUD吗?其实不然,要把前后端功能都做出来,页面跳转也没问题,还真不简单。这次笔者做这么一个看似简单的用户注册功能就花了足足两天多时间,中间调试和解决Bug也花了好长时间。这次我就把自己做出的完整功能的实现过程作了一个提炼分享到我的公众号上来。希望有需要了解如何实现用户注册完整过程的读者朋友能够仔细看一看。

说明:本文前后端代码的实现分别在本人之前二次开发的开源项目vue-element-adminvueblog两个项目的基础上进行

1 实现用户注册流程

1.1 用户注册完整流程

用户注册流程

1.2 用户注册信息及校验

用户注册信息

2 后台接口设计

2.1 上传头像接口

1)接口url

http://localhost:8081/blog/upload/user/avatar

  1. 请求类型
    POST

3)接口入参

参数名称 参数类型 是否必传 备注
file MultipartFile 多媒体图片文件

4)接口出参

参数名称 参数类型 示例值 备注
status Integer 200 状态码:200-成功; 500-失败
msg String “success” 响应信息:“success”-上传头像成功; “upload file failed”-上传头像失败
data String https://vueblog2022.oss-cn-shenzhen.aliyuncs.com/avatar/63be8be25fee4c0f8df679238435d8d2.png 上传头像成功后的下载地址

2.2 用户注册接口

1)接口url

http://localhost:8081/blog/user/reg

  1. 请求类型
    POST

3)接口入参

参数名称 参数类型 是否必填 备注
username String 用户账号
nickname String 用户昵称
password String 用户登录密码
userface String 用户头像链接地址
phoneNum Long 用户手机号码
email String 用户邮箱地址
  1. 接口出参
参数名称 参数类型 示例值 备注
status Integer 200 响应码: 200-成功;500-失败
msg String 注册成功 响应消息
data Integer 0 注册成功标识:0-注册成功;1-用户名重复; null-内部服务异常

3 后端代码实现

3.1 用户头像上传接口编码实现

文件上传,这里选用了阿里云的对象存储,需要先开通阿里云对象存储服务,关于如何开通阿里云短信服务并将阿里云对象存储服务集成到SpringBoot项目中,请参考我之前发布的文章SpringBoot项目集成阿里云对象存储服务实现文件上传

3.1.1 服务层编码

新建OssClientService类继承阿里云对象存储服务SDK完成图片上传功能


@Service
public class OssClientService {
    
    

    @Resource
    private OssProperties ossProperties;

    private static final Logger logger =  LoggerFactory.getLogger(OssClientService.class);

    public String uploadFile(MultipartFile file){
    
    
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(ossProperties.getEndPoint(), ossProperties.getAccessKey(),
                ossProperties.getSecretKey());
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        String objectName = "avatar/" + uuid + ".png";
        String imageUrl = null;
        try {
    
    
            InputStream inputStream =  file.getInputStream();  
            ossClient.putObject(ossProperties.getBucketName(), objectName, inputStream);
            imageUrl = "https://" + ossProperties.getBucketName() + "." + ossProperties.getEndPoint() + "/" + objectName;
        } catch (OSSException oe) {
    
    
            logger.error("Caught an OSSException, which means your request made it to OSS, but was rejected with an error response for some reason.");
            logger.error("Error Message:" + oe.getErrorMessage());
            logger.error("Error Code:" + oe.getErrorCode());
            logger.error("RequestId: " + oe.getRequestId());
            logger.error("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
    
    
            logger.error("Caught an ClientException, which means the client encountered a serious internal problem " +
                    "while trying to communicate with OSS,such as not being able to access the network");
            logger.error("Error Message:" + ce.getErrorMessage());
        } catch (FileNotFoundException fe) {
    
    
            logger.error("file not found exception");
            logger.error("Error Message:" + fe.getMessage(), fe);
        } catch (IOException exception){
    
    
            logger.error("file get input stream error, caused by " + exception.getMessage(), exception);
        }
        finally {
    
    
            if (ossClient!=null) {
    
    
                ossClient.shutdown();
            }
        }
        return imageUrl;
    }
}

注意:升级到3.9.1版本后的aliyun-sdk-oss需要在每次上传文件时新建一个OSS实例, 上传完文件之后再调用shutdown方法关闭这个实例

3.1.2 控制器层编码

新建UploadFileController类完成从前端接收附件参数,并调用OssClientService服务实现图片上传

@RestController
@RequestMapping("/upload")
public class UploadFileController {
    
    

    @Resource
    private OssClientService ossClientService;

    @PostMapping("/user/avatar")
    @ApiOperation(value = "userAvatar", notes = "用户上传头像接口",
    produces = "application/octet-stream", consumes = "application/json")
    public RespBean uploadUserAvatar(HttpServletRequest request){
    
    
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
        // 获取上传文件对象
        MultipartFile file = multipartRequest.getFile("file");
        RespBean respBean = new RespBean();
        String downloadUrl = ossClientService.uploadFile(file);
        if (!StringUtils.isEmpty(downloadUrl)) {
    
    
            respBean.setStatus(200);
            respBean.setMsg("success");
            respBean.setData(downloadUrl);
        } else {
    
    
            respBean.setStatus(500);
            respBean.setMsg("upload file failed");
        }
        return respBean;
    }
}

3.2 用户注册接口

1) 数据库访问层编码

UserMapper接口类中新增注册用户抽象方法

int registerUser(UserDTO user);

然后在UserMapper.xml文件中完成用户数据入库sql编写

<insert id="registerUser" useGeneratedKeys="true" keyProperty="id" parameterType="org.sang.pojo.dto.UserDTO">
        INSERT INTO user(username, nickname, password, phoneNum,email, userface, regTime,enabled)
        values(#{username,jdbcType=VARCHAR},#{nickname,jdbcType=VARCHAR},
        #{password,jdbcType=VARCHAR}, #{phoneNum,jdbcType=BIGINT}, #{email,jdbcType=VARCHAR},
        #{userface,jdbcType=VARCHAR},now(),1)
    </insert>

2 ) 服务层编码

CustomUserDetailsService接口类中添加注册用户抽象方法

int registerUser(UserDTO user);

然后在 CustomUserDetailsService接口类的实现类UserService类中完成用户注册逻辑

    @Override
    public int registerUser(UserDTO user) {
    
    
        // 判断用户是否重复注册
        UserDTO userDTO  = userMapper.loadUserByUsername(user.getUsername());
        if (userDTO != null) {
    
    
            return 1;
        }
        //插入用户, 插入之前先对密码进行加密
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setEnabled(1);//用户可用
        int result = userMapper.registerUser(user);
        //配置用户的角色,默认都是普通用户
        List<Integer> roleIds = Arrays.asList(2);
        int i = rolesMapper.setUserRoles(roleIds, user.getId());
        boolean b = i == roleIds.size() && result == 1;
        if (b) {
    
    
            // 注册成功
            return 0;
        } else {
    
    
            // 注册失败
            return 2;
        }
    }
  1. 控制器层编码
    LoginRegController类中完成用户登录接口从前端接收参数到调用UserService服务类完成用户注册业务
@RequestMapping(value = "/login_page", method = RequestMethod.GET)
    @ApiOperation(value = "loginPage", notes = "尚未登录跳转", produces = "application/json",
            consumes = "application/json", response = RespBean.class)
    public RespBean loginPage() {
    
    
        return new RespBean(ResponseStateConstant.UN_AUTHORIZED, "尚未登录,请登录!");
    }

    @PostMapping("/user/reg")
    @ApiOperation(value = "reg", notes = "用户注册", produces = "application/json",
            consumes = "application/json", response = RespBean.class)
    public RespBean reg(@RequestBody UserDTO user) {
    
    
        int result = userService.registerUser(user);
        if (result == 0) {
    
    
            //成功
            return new RespBean(ResponseStateConstant.SERVER_SUCCESS, "注册成功!");
        } else if (result == 1) {
    
    
            return new RespBean(ResponseStateConstant.DUPLICATE_ERROR, "用户名重复,注册失败!");
        } else {
    
    
            //失败
            return new RespBean(ResponseStateConstant.SERVER_ERROR, "注册失败!");
        }
    }

由于以上两个接口都是需要放开权限控制的,因此完成以上两个接口的编码后还需要在security配置类WebSecurityConfig类中支持匿名访问

只需要在configure(HttpSecurity http)方法中添加如下几行代码即可

http.authorizeRequests()
                .antMatchers("/user/reg").anonymous()
                .antMatchers("/upload/user/avatar").anonymous()

完成后端编码后可以启动Mysql服务和redis服务,然后运行BlogserverApplication类中的Main方法成功后就可以通过postman工具测试接口了

4 前端代码实现

4.1 完成用户注册界面vue组件编码

src/views目录下新建register文件夹,然后在register目录下新建index.vue文件

完成用户注册组件编码

这里的文件上传选择了element-ui组件库中的upload组件

<template>
    <div class="register-container">
        <el-form :model="registerModel" :rules="rules" ref="registerForm" label-width="100px" class="register-form">
            <el-form-item label="用户账号" prop="userAccount" required>
                <el-input 
                  v-model="registerModel.userAccount"
                  placeholder="请输入用户名"/>
            </el-form-item>
            <el-form-item label="用户昵称" prop="nickName" required>
                <el-input 
                  v-model="registerModel.nickName"
                  type="text"
                  placeholder="请输入用户昵称"/>
            </el-form-item>
            <el-form-item label="登录密码" prop="password" required>
                <el-input 
                  v-model="registerModel.password" 
                  type="password"
                  placeholder="请输入密码"
                  suffix-icon="el-icon-lock"/>
            </el-form-item>
            <el-form-item label="确认密码" prop="password2" required>
                <el-input 
                  v-model="registerModel.password2"
                  type="password"
                  :show-password="false"  
                  placeholder="请再次输入密码"
                  suffix-icon="el-icon-lock" />
            </el-form-item>
            <el-form-item label="头像">
                <el-upload class="avatar-uploader"
                    :show-file-list="false"
                    accept="image"
                    :action="uploadAvatarUrl"
                    :on-preview="previewAvatar" 
                    :before-upload="beforeAvartarUpload"
                    :on-success="handleSuccessAvatar"
                >   
                    <img v-if="avatarUrl" :src="avatarUrl" class="avatar" />
                    <div v-else class="upload-btn" >
                        <el-button>点击上传头像</el-button>
                        <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10M</div>
                    </div>
                </el-upload>
            </el-form-item>
            <el-form-item label="手机号" prop="phoneNum" required>
                <el-input type="tel" 
                v-model="registerModel.phoneNum"
                placeholder="请输入手机号" 
                />
            </el-form-item>
            <el-form-item label="邮箱" prop="email">
                <el-input type="email" 
                v-model="registerModel.email"
                placeholder="请输入你的邮箱" />
            </el-form-item>
            <el-form-item class="btn-area">
               <el-button class="submit-btn" type="primary" :loading="onLoading"  @click="handleRegister('registerForm')">提交</el-button>
               <el-button class="reset-btn" type="info" @click="resetForm('registerForm')">重置</el-button> 
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
import {
      
       Message } from 'element-ui'
import {
      
       isNumber, validatePhoneNum, validatePassword, validEmail } from '@/utils/validate'
export default {
      
      
    name: 'register',
    data(){
      
      
        // 密码校验器
        const passwordValidator = (rule,value, callback) =>{
      
      
            console.log(rule)
            if(!validatePassword(value)){
      
      
                callback('密码强度不满足要求,密码必须同时包含字母、数字和特殊字符,请重新输入')
            } else {
      
      
                callback()
            }
        }
        // 二次密码校验器
        const password2Validator = (rule, value, callback) => {
      
      
            console.log(rule)
            const password = this.registerModel.password
            if(password!=value){
      
      
                callback(new Error('两次输入的密码不一致'))
            } else {
      
      
                callback()
            }
        }
        // 手机号码校验器
       const  phoneNumValidator = (rule, value, callback)=> {
      
      
             console.log(rule)
            if(!(value.length==11 && isNumber(value))){
      
      
                callback(new Error('手机号码必须是11位数字'))
            } else if(!validatePhoneNum(parseInt(value))){
      
      
                callback(new Error('手机号码不合法'))
            } else {
      
      
                callback()
            }
       }
       // 邮件地址校验器
       const emailValidator = (rule, value, callback) => {
      
      
          console.log(rule)
          if(value!='' && !validEmail(value)){
      
      
             callback(new Error('邮箱地址不合法'))
          } else {
      
      
            callback()
          }
       }
        // 区分本地开发环境和生产环境
       let uploadAvatarUrl = ''
       if(window.location.host='localhost'){
      
      
           uploadAvatarUrl = 'http://localhost:8081/blog/upload/user/avatar'
       } else {
      
      
          uploadAvatarUrl = 'http://www.javahsf.club:8081/blog/upload/user/avatar'
       }
        return {
      
      
            uploadAvatarUrl: uploadAvatarUrl,
            registerModel: {
      
      
                userAccount: '',
                nickName: '',
                password: '',
                password2: '',
                avatarSize: 32,
                uploadUrl: uploadUrl,
                phoneNum: '',
                email: ''
            },
            onLoading: false,
            avatarUrl: '',
            password2Style: {
      
      
                dispaly: 'none',
                color: 'red'
            },
            // 表单校验规则
            rules: {
      
      
                userAccount: [
                    {
      
       required: true, message: '请输入用户账号', trigger: 'blur' },
                    {
      
       min: 2, max: 64, message: '2-64个字符', trigger: 'blur' }
                ],
                nickName: [
                    {
      
       required: true, message: '请输入昵称',  trigger: 'blur' },
                    {
      
       min: 2, max: 64, message: '长度控制在2-64个字符',trigger: 'blur' }
                ],
                password: [
                    {
      
       required: true, message: '请输入密码', trigger: 'blur' },
                    {
      
       min: 6, max: 18, message: '长度控制在6-18个字符', trigger: 'blur' },
                    {
      
       validator: passwordValidator, trigger: 'blur' }
                ],
                password2: [
                    {
      
       required: true, message: '请再次输入密码', trigger: 'blur' },
                    {
      
       min: 6, max: 18, message: '长度控制在6-18个字符', trigger: 'blur' },
                    {
      
       validator: password2Validator, trigger: 'blur' }
                ],
                phoneNum: [
                    {
      
       required: true, message: '请输入手机号',  trigger: 'blur'},
                    {
      
       validator: phoneNumValidator, trigger: 'blur' }
                ],
                email: [
                    {
      
       min: 0, max: 64, message: '长度控制在64个字符'},
                    {
      
       validator: emailValidator, trigger: 'blur' }
                ]

            },
            redirect: undefined
        }
    },
    watch: {
      
      
        $route: {
      
      
            handler: function(route) {
      
      
                const query = route.query
                if (query) {
      
      
                this.redirect = query.redirect
                this.otherQuery = this.getOtherQuery(query)
                }
            },
            immediate: true
        }
   },
    methods: {
      
         
        // 图片上传之前校验图片格式和附件大小
        beforeAvartarUpload(file) {
      
      
           console.log(file)
           if(!(file.type=='image/jpeg' ||file.type=='image/png')){
      
      
              Message.error('头像图片必须是jpg或png格式')  
           }else if(file.size/(1024*1024)>10){
      
      
              Message.error('图片大小不能超过10M')
           }
        },
        // 上传图片预览
        previewAvatar(file){
      
      
            console.log(file)
        },
        // 图片上传成功回调
        handleSuccessAvatar(response){
      
      
           console.log(response.data)
           this.avatarUrl = response.data
        },
        // 提交注册
        handleRegister(formName){
      
      
            this.$refs[formName].validate((valid=>{
      
      
                if(valid){
      
       // 表单校验通过
                    const params = {
      
      
                        username: this.registerModel.userAccount,
                        nickname: this.registerModel.nickName,
                        password: this.registerModel.password,
                        phoneNum: this.registerModel.phoneNum,
                        email: this.registerModel.email,
                        userface: this.avatarUrl
                   }
                    this.onLoading = true
                    this.$store.dispatch('user/register', params).then(res=>{
      
      
                        this.onLoading = true
                        if(res.status===200){
      
      
                            Message.success('恭喜注册成功,现在就可以登录系统了!')
                            // 跳转到登录界面
                            this.$router.push({
      
       path: '/login', query: this.otherQuery })
                        } else {
      
      
                            Message.error(res.msg)
                        }
                    })
                }else{
      
        // 表单校验不通过,拒绝提交注册
                    this.onLoading = true
                    Message.error('用户注册信息校验不通过,请重新填写注册信息')
                    return false
                }
            }))
        },
        // 表单重置
        resetForm(formName) {
      
      
          this.$refs[formName].resetFields()
        },
        getOtherQuery(query) {
      
      
            return Object.keys(query).reduce((acc, cur) => {
      
      
                if (cur !== 'redirect') {
      
      
                acc[cur] = query[cur]
                }
                return acc
            }, {
      
      })
        }
    }
}
</script>
<!--页面样式-->
<style lang="scss" scoped>
    .register-container{
      
      
        margin-top: 100px;
        margin-left: 10%;
        .el-input{
      
      
            width: 60%;
        }
        .avatar-uploader .avatar{
      
      
            width: 240px;
            height: 240px;
        }
        .el-button.submit-btn{
      
      
            width: 10%;
            height: 40px;
            margin-left: 150px;
            margin-right: 25px;
        }
        .el-button.reset-btn{
      
      
            width: 10%;
            height: 40px;
        }
    }
</style>

4.2 工具类中增加校验方法

src/utils/validate.js中增加校验密码和手机号码的方法

export function validatePhoneNum(phoneNum) {
    
    
  const reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
  return reg.test(phoneNum)
}

export function validatePassword(password) {
    
    
  // 强密码:字母+数字+特殊字符
  const reg = /^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&*]+$)(?![\d!@#$%^&*]+$)[a-zA-Z\d!@#$%^&*]+$/
  return reg.test(password)
}

以上校验均使用正则表达式校验

  1. src/api/user.js文件中新增用户注册接口方法
export function register(data) {
    
    
  return request({
    
    
    url: '/user/reg',
    method: 'post',
    data
  })
}
  1. src/store/modules/user.js 文件中的actions对象中增加用户注册行为方法
const actions = {
    
    
  // user register
  register({
     
      commit }, registerInfo) {
    
    
    return new Promise((resolve, reject) => {
    
    
      register(registerInfo).then(response => {
    
    
        if (response.status === 200 && response.data.status === 200) {
    
    
          const resInfo = {
    
     status: response.status, msg: '注册成功' }
          resolve(resInfo)
        } else {
    
    
          const resInfo = {
    
     status: response.status, msg: response.data.msg }
          resolve(resInfo)
        }
      }).catch(error => {
    
    
        console.error(error)
        reject(error)
      })
    })
  },
    // ......省略其他已有方法
}

因为用户注册完之后需要跳转到登录界面,直接在注册页面调用后台用户注册接口成功后调用this.$router.push方法发现无法实现页面的跳转效果, 因此改为在vuex的全局dispatch中调用注册接口

  1. src/router/index.js文件的固定路由列表中添加注册组件的路由
import Register from '@/views/register/index'

export const constantRoutes = [
  {
    
    
    id: '0',
    path: '/register',
    component: Register,
    hidden: true
  },
   //...... 省略其他路由
 ]
  1. 登录组件中添加用户注册的跳转链接

src/views/login/index.vue文件中的模板代码部分的登录按钮标签下面添加如下两行代码

<div>
   <router-link to="/resetPass" class="forget-password">忘记密码</router-link>
   <router-link class="register" to="/register">注册账号</router-link>
 </div>

同时对忘记密码注册账号两个链接添加样式(忘记密码功能尚待实现)

<style lang="scss" scoped>
    .register, .forget-password{
      
      
        width: 20%;
        height: 35px;
        color: blue;
        margin-right: 20px;
        cursor: pointer;	
  }
</style>
  1. 在路由跳转控制文件src/permission.js文件中将注册用户的路由添加到白名单中
const whiteList = ['/login', '/register', '/auth-redirect'] // no redirect whitelist

如果不在白名单中加上用户注册的路由,你会发现在用户登录界面压根无法跳转到用户注册界面的

5 效果体验

在启动后端服务后,在vue-element-admin项目下通过 鼠标右键->git bash进入命令控制台

然后输入npm run dev 项目启动前端服务

然后在谷歌浏览器中输入:http://localhost:3000/回车进入登录界面

用户登录界面
然后填写好用户注册信息并上传头像
用户注册信息填写页面
填写好用户注册信息后就可以点击下面的【提交】按钮提交注册了,注册成功后系统会弹框提示用户中注册成功,并重新跳转到【用户登录】界面

6 写在最后

本文演示了在spring-boot项目中继承阿里云对象存储sdk实现了图片上传和用户提交登录两个接口的详细实现,同时前端使用element-ui库中的upload组件调用后端图片上传接口实现了附件上传功能,实现了一个完整的用户登录信息的校验和提交注册及注册成功后的页面跳转等功能。相信对想要了解一个系统的用户模块是如何实现用户的注册以及注册成功后的页面跳转的完整功能的是如何实现的读者朋友一定会有所帮助的!

本文前后端项目代码仓库地址

blogserver项目代码gitee地址

vue-element-admin项目gitee地址

本文首发个人微信公众号【阿福谈Web编程】,欢迎感兴趣的读者朋友们价格公众号关注,感谢阅读!
微信公众号

猜你喜欢

转载自blog.csdn.net/heshengfu1211/article/details/125608866
今日推荐