SpringBoot日记本系统全程直播03:把登录后台接起来撒~~

上一节:SpringBoot日记本系统全程直播02:登录页面搞起来撒~~

大家好,我是今天晚上的主讲老师,我是兔哥。

上一讲,我们学习了登录和注册页面,以及Controller访问页面的方法,还有如何处理静态资源。

今天我们继续来学习SpringBoot日记本系统,任务是进行后台的对接。就是说,用户注册和登录的功能,需要完善起来啦。还会涉及到很多企业级开发技术哦。

总之,这一节的内容非常之多,也比较丰富。视频我后期会补上,如果跟着做做不出来,一定要下载源码慢慢比对哈。

ok,那么我们现在开整!

目录

1.数据库建表

1.1 为什么不用UUID?

1.2 假如以后分库分表,ID重复怎么办?

2. 登录/注册页面访问后台

3.后端准备工作

3.1 系统核心包

3.1.1 BizException 业务异常类

3.1.2 GlobalExceptionHandler 全局异常处理

3.1.3 ExceptionCodeEnum 通用异常枚举

3.1.4 CommonResponseDataAdvice 统一封账响应结果

3.1.5 IgnoreCosmoResult 忽略统一结果返回封装

3.2 用户类和mybatisplus引入

3.3 redis引入(为了ID自增)

4. sa-token 鉴权框架

5. 用户接口和服务类

6. 如何下载源码?


1.数据库建表

navicat大家应该都有吧,或者类似的软件也可以,创建一个数据库,名字叫diary。

然后创建用户表:

CREATE TABLE `user_base` (
`uid`  bigint(20) NOT NULL COMMENT '用户ID' ,
`user_role`  tinyint(2) UNSIGNED NOT NULL DEFAULT 1 ,
`register_source`  tinyint(4) UNSIGNED NOT NULL DEFAULT 0 COMMENT '注册来源:1手机号 2邮箱 3用户名 4qq 5微信 6腾讯微博 7新浪微博' ,
`user_name`  varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户账号,必须唯一' ,
`password`  varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '密码' ,
`nick_name`  varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户昵称' ,
`gender`  tinyint(1) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户性别 0-female 1-male' ,
`birthday`  bigint(20) UNSIGNED NOT NULL DEFAULT 0 COMMENT '用户生日' ,
`signature`  varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '用户个人签名' ,
`mobile`  varchar(16) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '手机号码(唯一)' ,
`mobile_bind_time`  varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '手机号码绑定时间' ,
`email`  varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '邮箱(唯一)' ,
`email_bind_time`  varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '0' COMMENT '邮箱绑定时间' ,
`face`  varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '头像' ,
`face200`  varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '头像 200x200x80' ,
`srcface`  varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT '' COMMENT '原图头像' ,
`create_time`  varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '创建时间' ,
`update_time`  varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '修改时间' ,
PRIMARY KEY (`uid`)
)
ENGINE=InnoDB
DEFAULT CHARACTER SET=utf8mb4 COLLATE=utf8mb4_general_ci
COMMENT='用户基础信息表'
ROW_FORMAT=COMPACT
;

把上面的sql拖到navicat中执行,建表成功。

 

关于UID,我们用bigint类型。

1.1 为什么不用UUID?

因为UUID是看不出顺序的,我们用int类型可以看出用户创建的先后顺序。

1.2 假如以后分库分表,ID重复怎么办?

我们知道mysql没有sequence,字段不是自增的。如果用自增长ID,分库分表的话,很难保证ID不重复和连续。因此,我们可以采用redis的ID自增长策略,这样不管你有几个数据库,只要redis是一个,就没问题啦。(本节后面会给出具体的方案)

2. 登录/注册页面访问后台

回顾下注册页面

这一步,我们需要对接后台的登录注册接口。

引入vue,axios

<script src="${basePath}/js/vue.js"></script>
<script src="${basePath}/js/axios.min.js"></script>

 更改HTML

<div class="container" id="app">
    <div class="register-box">
        <h2 class="register-title">
            <span>没有账号,去</span>注册日记本
        </h2>
        <div class="input-box">
            <input type="text" v-model="userName1" placeholder="用户名" autocomplete="off">
            <input type="password" v-model="password1" placeholder="密码" autocomplete="off">
            <input type="password" v-model="password11" placeholder="确认密码" autocomplete="off">
        </div>
        <button @click="reg">注册</button>
    </div>
    <div class="login-box slide-up">
        <div class="center">
            <h2 class="login-title">
                <span>已有账号,去</span>登录日记本
            </h2>
            <div class="input-box">
                <input type="text" v-model="userName2" placeholder="用户名" autocomplete="off">
                <input type="password" v-model="password2" placeholder="密码" autocomplete="off">
            </div>
            <button @click="login">登录</button>
        </div>
    </div>
</div>

其实用jquery是完全可以的,不过现在jq基本没人用了,于是本项目我们也赶一下潮流吧。

vue代码:

let vue = new Vue({
    el:'#app',
    data: {
        //注册
        userName1:'',
        password1:'',
        password11:'',

        //登录
        userName2:'',
        password2:''
    },

    methods:{
        reg(){
            if(this.password1 != this.password11){
                layer.msg('两次输入密码不一致',{icon:2});
                return;
            }
            let data = {
                userName: this.userName1,
                password: this.password1
            }

            axios.post('${basePath}/user/register',data).then(r =>{

                if(r.data.code != '0000'){
                    layer.msg(r.data.message,{icon:2});
                    return;
                }
                layer.msg('注册成功!',{icon:2});
            });

        },

        login(){
            let data = {
                userName: this.userName2,
                password: this.password2
            }
            axios.post('${basePath}/user/login',data).then(r =>{

                if(r.data.code != '0000'){
                    layer.msg(r.data.message,{icon:2});
                    return;
                }
                layer.msg('登录成功!',{icon:1});
                //登录成功后,就缓存账号密码
                localStorage.setItem('userName2',this.userName2);
                localStorage.setItem('password2',this.password2);
                setTimeout(()=>{
                    location.href="/";
                },1000)
            });
        },

        //填充缓存的账号和密码
        fillAccount(){
            let userName2 = localStorage.getItem('userName2');
            let password2 = localStorage.getItem('password2');
            if(userName2){
                this.userName2 = userName2;
            }
            if(password2){
                this.password2 = password2;
            }
            if(userName2 && password2){
                setTimeout(()=>{
                    document.querySelector('.login-title').click();
                },1000)

            }
        }
    },

    created(){
        this.fillAccount();
    }
});

登录流程:获取用户名和密码,发送 /user/login 

返回报文格式是这样的:

 这个格式后台是怎么返回的,下面会说到。登录成功后,会把用户名密码缓存到本地,缓存技术用的是localStorage。代码如下:

login(){
    let data = {
        userName: this.userName2,
        password: this.password2
    }
    axios.post('${basePath}/user/login',data).then(r =>{

        if(r.data.code != '0000'){
            layer.msg(r.data.message,{icon:2});
            return;
        }
        layer.msg('登录成功!',{icon:1});
        //登录成功后,就缓存账号密码
        localStorage.setItem('userName2',this.userName2);
        localStorage.setItem('password2',this.password2);
        setTimeout(()=>{
            location.href="/";
        },1000)
    });
},

 注册流程:校验两次密码输入是否一致,然后发送用户名和密码到 /user/register 

代码如下:

reg(){
    if(this.password1 != this.password11){
        layer.msg('两次输入密码不一致',{icon:2});
        return;
    }
    let data = {
        userName: this.userName1,
        password: this.password1
    }

    axios.post('${basePath}/user/register',data).then(r =>{

        if(r.data.code != '0000'){
            layer.msg(r.data.message,{icon:2});
            return;
        }
        layer.msg('注册成功!',{icon:2});
    });

},

如果已经发现缓存中存在用户名和密码,就自动填充,并且显示登陆页面:

fillAccount(){
    let userName2 = localStorage.getItem('userName2');
    let password2 = localStorage.getItem('password2');
    if(userName2){
        this.userName2 = userName2;
    }
    if(password2){
        this.password2 = password2;
    }
    if(userName2 && password2){
        setTimeout(()=>{
            document.querySelector('.login-title').click();
        },1000)

    }
}

如果你已经登录过,那就会看到一个向上滑动的效果,然后系统会自动填充用户名和密码。

OK,以上就是前端的逻辑了。

3.后端准备工作

接下来,我们需要做一点准备工作。代码有点多,我们一个个来看。

3.1 系统核心包

创建上图所示核心包,位置:

com.rabbit.diary.bean.core

3.1.1 BizException 业务异常类

 这是我们系统自定义的业务异常类,所有业务上的报错,比如【用户名密码错误】之类的,我们就需要抛出这个异常。

package com.rabbit.diary.bean.core;

/**
 * 业务异常
 * biz是business的缩写
 *
 * @author sunting
 * @see ExceptionCodeEnum
 */
public class BizException extends RuntimeException {

    private ExceptionCodeEnum error;

    /**
     * 构造器,有时我们需要将第三方异常转为自定义异常抛出,但又不想丢失原来的异常信息,此时可以传入cause
     *
     * @param error
     * @param cause
     */
    public BizException(ExceptionCodeEnum error, Throwable cause) {
        super(cause);
        this.error = error;
    }

    /**
     * 构造器,只传入错误枚举
     *
     * @param error
     */
    public BizException(ExceptionCodeEnum error) {
        this.error = error;
    }

	public ExceptionCodeEnum getError() {
		return error;
	}

	public void setError(ExceptionCodeEnum error) {
		this.error = error;
	}
    
    
}

3.1.2 GlobalExceptionHandler 全局异常处理

作用是捕获异常,当发生异常的时候,就会进入对应的handler。

package com.rabbit.diary.bean.core;

import cn.dev33.satoken.exception.NotLoginException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理
 * 一般来说,全局异常处理只是一种兜底的异常处理策略,也就是说提倡自己处理异常。
 * 但现在其实很多人都喜欢直接在代码中抛异常,全部交给@RestControllerAdvice处理
 */
@RestControllerAdvice
public class GlobalExceptionHandler {
	
	/**
     * 权限异常 (注意,参数一定要是需要捕获的异常,否则进不来)
     * @param
     * @return
     */
    @ExceptionHandler(NotLoginException.class)
    public com.rabbit.diary.bean.core.Result<com.rabbit.diary.bean.core.ExceptionCodeEnum> handleNotLoginException(NotLoginException bizException) {
        return com.rabbit.diary.bean.core.Result.error(com.rabbit.diary.bean.core.ExceptionCodeEnum.NEED_LOGIN,bizException.getMessage());
    }

    /**
     * 业务异常(需要主动抛出)
     *
     * @param
     * @return
     */
    @ExceptionHandler(com.rabbit.diary.bean.core.BizException.class)
    public com.rabbit.diary.bean.core.Result<com.rabbit.diary.bean.core.ExceptionCodeEnum> handleBizException(com.rabbit.diary.bean.core.BizException bizException) {
        return com.rabbit.diary.bean.core.Result.error(bizException.getError());
    }

    /**
     * 运行时异常
     *
     * @param e
     * @return
     */
    @ExceptionHandler(RuntimeException.class)
    public com.rabbit.diary.bean.core.Result<com.rabbit.diary.bean.core.ExceptionCodeEnum> handleRunTimeException(RuntimeException e) {
        return com.rabbit.diary.bean.core.Result.error(com.rabbit.diary.bean.core.ExceptionCodeEnum.ERROR);
    }

}

3.1.3 ExceptionCodeEnum 通用异常枚举

package com.rabbit.diary.bean.core;

/**
 * 通用异常枚举
 * @author Administrator
 *
 */
public enum ExceptionCodeEnum {
	SUCCESS("0000","返回成功!"),
	ERROR("1111","与服务方通讯失败,请联系管理员!"),
	NEED_LOGIN("9999", "用户未登录!"),
	ERROR_PARAM("1000", "参数送错了!"),
	EMPTY_PARAM("2000", "参数为空!"),
	;
	
	private String code;
	private String desc;
	
	
	
	private ExceptionCodeEnum(String code, String desc) {
		this.code = code;
		this.desc = desc;
	}

	

	public String getCode() {
		return code;
	}
	public void setCode(String code) {
		this.code = code;
	}
	public String getDesc() {
		return desc;
	}
	
	//为了封装自定义信息,做特殊处理
	public ExceptionCodeEnum setDesc(String desc) {
		this.desc = desc;
		return this;
	}
	
	
}

3.1.4 CommonResponseDataAdvice 统一封账响应结果

作用是设置通用的返回,以后在RestController里面写方法,就不需要专门设置返回结果了。

/**
 * 统一封账响应结果
 * @author Administrator
 *
 */
@RestControllerAdvice
public class CommonResponseDataAdvice implements ResponseBodyAdvice<Object> {


    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        // 标注了@RestController,且类及方法上都没有标注@IgnoreCosmoResult的方法才进行包装
    	return methodParameter.getDeclaringClass().isAnnotationPresent(RestController.class)
                && !methodParameter.getDeclaringClass().isAnnotationPresent(com.rabbit.diary.bean.core.IgnoreCosmoResult.class)
                && !methodParameter.getMethod().isAnnotationPresent(com.rabbit.diary.bean.core.IgnoreCosmoResult.class);
    }

    @Override
    public Object beforeBodyWrite(Object o,
                                  MethodParameter methodParameter,
                                  MediaType mediaType,
                                  Class<? extends HttpMessageConverter<?>> aClass,
                                  ServerHttpRequest serverHttpRequest,
                                  ServerHttpResponse serverHttpResponse) {
    	// 已经包装过的,不再重复包装
        if (o instanceof com.rabbit.diary.bean.core.Result) {
            return o;
        }
        // 改一行代码即可:把Object返回值用Result封装
        return com.rabbit.diary.bean.core.Result.success(o);
    }
}

3.1.5 IgnoreCosmoResult 忽略统一结果返回封装

和上一个类配套使用,如果有的方法或者类不希望被统一封装,就加上这个注解。

/**
 * 如果有的方法不希望被统一封装结果,就用这个注解
 * @author Administrator
 *
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface IgnoreCosmoResult {
	
}

3.1.6 Result 通用返回结果

public class Result<T> implements Serializable{
	 private String code;
	    private String message;
	    private T data;

	    private Result(String code, String message, T data) {
	        this.code = code;
	        this.message = message;
	        this.data = data;
	    }

	    private Result(String code, String message) {
	        this.code = code;
	        this.message = message;
	        this.data = null;
	    }

	    /**
	     * 带数据成功返回
	     *
	     * @param data
	     * @param <T>
	     * @return
	     */
	    public static <T> Result<T> success(T data) {
	        return new Result<>(ExceptionCodeEnum.SUCCESS.getCode(), ExceptionCodeEnum.SUCCESS.getDesc(), data);
	    }

	    /**
	     * 不带数据成功返回
	     *
	     * @return
	     */
	    public static <T> Result<T> success() {
	        return success(null);
	    }

	    /**
	     * 通用错误返回
	     *
	     * @param exceptionCodeEnum
	     * @return
	     */
	    public static <T> Result<T> error(ExceptionCodeEnum exceptionCodeEnum) {
	        return new Result<>(exceptionCodeEnum.getCode(), exceptionCodeEnum.getDesc());
	    }

	    /**
	     * 通用错误返回
	     *
	     * @param exceptionCodeEnum
	     * @param msg
	     * @return
	     */
	    public static <T> Result<T> error(ExceptionCodeEnum exceptionCodeEnum, String msg) {
	        return new Result<>(exceptionCodeEnum.getCode(), msg);
	    }

	    /**
	     * 通用错误返回
	     *
	     * @param exceptionCodeEnum
	     * @param data
	     * @param <T>
	     * @return
	     */
	    public static <T> Result<T> error(ExceptionCodeEnum exceptionCodeEnum, T data) {
	        return new Result<>(exceptionCodeEnum.getCode(), exceptionCodeEnum.getDesc(), data);
	    }

		public String getCode() {
			return code;
		}

		public void setCode(String code) {
			this.code = code;
		}

		public String getMessage() {
			return message;
		}

		public void setMessage(String message) {
			this.message = message;
		}

		public T getData() {
			return data;
		}

		public void setData(T data) {
			this.data = data;
		}

	    
}

以上6个类的作用很大,帮我们解决了编写Controller方法的数据返回问题,和异常抛出的问题。有了他们,可以极大地方便我们写后面的代码。

3.2 用户类和mybatisplus引入

在第一小节,我们已经建好了表,接下来我们需要做一些Java后端的工作。首先是配置mybatis-plus:

pom.xml增加依赖

<!-- mybatis plus 支持 -->
<dependency>
	<groupId>com.baomidou</groupId>
	<artifactId>mybatis-plus-boot-starter</artifactId>
	<version>3.3.1.tmp</version>
</dependency>

application.yml配置myabtis

mybatis-plus:
  mapper-locations: classpath:mybatis/*.xml
  type-aliases-package: com.rabbit.diary.bean

注意就是配置xml文件的存放位置,和JavaBean的路径,JavaBean就是和数据库表对应的类。

User.java编写

@TableName("user_base")
@Data
@Service
public class User {
    @TableId(type = IdType.ASSIGN_UUID)
    private Long uid;
    private Integer userRole;
    private Integer registerSource;
    private String userName;
    private String password;
    private String nickName;
    private Integer gender;
    private String birthday;
    private String signature;
    private String mobile;
    private String mobileBindTime;
    private String email;
    private String emailBindTime;
    private String face;
    private String face200;
    private String srcface;
    private String createTime;
    private String updateTime;


}

我用了lombok,这个需要安装对应的idea插件,和maven依赖,当然你也可以选择手动生成get,set方法。

lombok依赖:

<dependency>
	<groupId>org.projectlombok</groupId>
	<artifactId>lombok</artifactId>
	<version>1.18.16</version>
</dependency>

因为使用了mybatis-plus,我们大部分情况, 其实是不需要再手动编写xml了。

创建包:com.rabbit.diary.dao

编写UserMapper.java

package com.rabbit.diary.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.rabbit.diary.bean.User;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface UserMapper extends BaseMapper<User> {
}

 这样就有基本的增删改查方法了。

3.3 redis引入(为了ID自增)

上面已经说过,为了应对分库分表的情况(虽然这个项目用不到哈),我们将采用redis的方式。redis大家可以自行去百度下载windows版的,安装很方便。

双击这个exe就可以打开了。

增加maven依赖:

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

springboot有对应的starter,引入了就行。

然后是application.yml配置

spring:

  redis:
    host: 127.0.0.1
    port: 6379
    password:
    jedis:
      pool:
        max-active: 8
        max-wait: -1
        max-idle: 500
        min-idle: 0
    lettuce:
      shutdown-timeout: 0
    timeout: 2s

注意是放在spring节点下,和mvc,datasource节点是同级的。如果还是不对,请下载源码自行比较。一般而言,我们刚下载的redis是没有密码的。

redis辅助工具类:

IRedisService

public interface IRedisService {

    // 加入元素
    void setValue(String key, Map<String, Object> value);
    // 加入元素
    void setValue(String key, String value);
    // 加入元素
    void setValue(String key, Object value);
    // 获取元素
    Object getMapValue(String key);
    // 获取元素
    Object getValue(String key);
}
RedisServiceImpl
package com.rabbit.diary.redis;

import com.rabbit.diary.util.SpringUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.data.redis.support.atomic.RedisAtomicLong;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.TimeUnit;

@Service
public class RedisServiceImpl implements IRedisService {
    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * @Description: 获取自增长值 (每调用1次,ID自增1)
     * @param key key
     * @return
     */
    public  Long getIncr(String key) {
        RedisAtomicLong entityIdCounter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        Long increment = entityIdCounter.getAndIncrement();
        //entityIdCounter.expire(0, TimeUnit.SECONDS);
        return increment;
    }

    /**
     * @Description: 初始化自增长值
     * @param key key
     * @param value 当前值
     */
    public void setIncr(String key, int value) {
        RedisAtomicLong counter = new RedisAtomicLong(key, redisTemplate.getConnectionFactory());
        counter.set(value);
        //counter.expire(0, TimeUnit.SECONDS);
    }

    @Override
    public void setValue(String key, Map<String, Object> value) {
        ValueOperations<String, Object> vo = redisTemplate.opsForValue();
        vo.set(key, value);
        redisTemplate.expire(key, 1, TimeUnit.HOURS); // 这里指的是1小时后失效
    }

    @Override
    public Object getValue(String key) {
        ValueOperations<String, String> vo = redisTemplate.opsForValue();
        return vo.get(key);
    }


    @Override
    public void setValue(String key, String value) {
        ValueOperations<String, Object> vo = redisTemplate.opsForValue();
        vo.set(key, value);
        redisTemplate.expire(key, 1, TimeUnit.HOURS); // 这里指的是1小时后失效
    }

    @Override
    public void setValue(String key, Object value) {
        ValueOperations<String, Object> vo = redisTemplate.opsForValue();
        vo.set(key, value);
        redisTemplate.expire(key, 1, TimeUnit.HOURS); // 这里指的是1小时后失效
    }

    @Override
    public Object getMapValue(String key) {
        ValueOperations<String, String> vo = redisTemplate.opsForValue();
        return vo.get(key);
    }


}

其中包含了

getIncr
setIncr

这两个方法可以用来设置、获取自增长值。

4. sa-token 鉴权框架

用户权限问题是一个系统不可缺少的部分,本项目采用sa-token框架。倒不是我给作者打广告哈,而是我觉得确实很好用,shiro太重了,我更倾向于选择sa-token,用我们国人自己的框架。

官网:Sa-Token

引入依赖:

<!-- Sa-Token 权限认证-->
<dependency>
	<groupId>cn.dev33</groupId>
	<artifactId>sa-token-spring-boot-starter</artifactId>
	<version>1.28.0</version>
</dependency>

创建包

com.rabbit.diary.config

编写

SaTokenConfigure
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
    // 注册Sa-Token的注解拦截器,打开注解式鉴权功能
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册注解拦截器,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
        registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
    }
}

写这个config是为了开启sa-token注解。

5. 用户接口和服务类

终于到用户接口编写啦,我们已经做了很多的准备工作,这个项目已经初步具备企业级工程化的标准了。很多企业小项目的架构杂乱无章,甚至都没有这么完备的体系。

用户服务类,我们采用接口+实现类的方式。

UserService
package com.rabbit.diary.service;

import com.rabbit.diary.bean.User;
import org.springframework.stereotype.Service;

public interface UserService {

    User getByUserName(String userName);

    void save(User user);
}
UserServiceImpl
package com.rabbit.diary.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.rabbit.diary.bean.User;
import com.rabbit.diary.dao.UserMapper;
import com.rabbit.diary.redis.RedisServiceImpl;
import com.rabbit.diary.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.lang.annotation.Annotation;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Autowired
    UserMapper userMapper;

    @Autowired
    RedisServiceImpl redisServiceImpl;

    /**
     * 根据用户名获取用户
     * @param userName
     * @return
     */
    @Override
    public User getByUserName(String userName) {
        List<User> users = userMapper.selectList(new QueryWrapper<User>().eq("user_name", userName));
        if(users.size() > 0){
            return users.get(0);
        }
        return null;
    }

    /**
     * 保存用户
     * @param user
     */
    @Override
    public void save(User user) {
        userMapper.insert(user);
    }


}

很简单吧,就两个方法。

UserController:

 

package com.rabbit.diary.web;

import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import com.rabbit.diary.bean.User;
import com.rabbit.diary.bean.core.BizException;
import com.rabbit.diary.bean.core.ExceptionCodeEnum;
import com.rabbit.diary.bean.core.Result;
import com.rabbit.diary.redis.RedisServiceImpl;
import com.rabbit.diary.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    UserService userService;

    @Autowired
    RedisServiceImpl redisServiceImpl;

    String salt = "diary188";

    @RequestMapping("register")
    public Result register(@RequestBody User user){
        if(StrUtil.isEmpty(user.getUserName())){
            throw new BizException(ExceptionCodeEnum.ERROR_PARAM.setDesc("用户名不允许为空!"));
        }
        if(StrUtil.isEmpty(user.getPassword())){
            throw new BizException(ExceptionCodeEnum.ERROR_PARAM.setDesc("密码不允许为空!"));
        }
        //检查用户名是否重复
        if(userService.getByUserName(user.getUserName()) != null){
            throw new BizException(ExceptionCodeEnum.ERROR_PARAM.setDesc("用户名"+user.getUserName()+"重复!"));
        }
        //拼装userBean
        user.setUid(redisServiceImpl.getIncr("userId")); //redis自增ID
        user.setPassword(SecureUtil.md5(user.getPassword() + salt));
        user.setCreateTime(DateUtil.now());
        user.setUpdateTime(DateUtil.now());
        userService.save(user);
        return Result.success();
    }

    @RequestMapping("login")
    public Result login(@RequestBody User user){
        User user1 = userService.getByUserName(user.getUserName());
        if(user1 == null){
            throw new BizException(ExceptionCodeEnum.ERROR_PARAM.setDesc("不存在的用户名:" + user.getUserName()));
        }
        String password = SecureUtil.md5(user.getPassword() + salt);
        if(!user1.getPassword().equals(password)){
            throw new BizException(ExceptionCodeEnum.ERROR_PARAM.setDesc("账号和密码不匹配:" + user.getUserName()));
        }
        /**登录维持ID* */
        StpUtil.login(user1.getUid(),"PC");
        return Result.success();
    }

}

下面我们挑几个需要注意的点

01. 如何实现redis自增长ID?

 user.setUid(redisServiceImpl.getIncr("userId")); //redis自增ID

02.如何保持登录ID?

StpUtil.login(user1.getUid(),"PC");
PageController
@Controller
public class PageController {

    @RequestMapping("login")
    public String login(){
        return "login";
    }

    @RequestMapping("/")
    @SaCheckLogin
    public String index(){
        return "index";
    }
}

注意,我们给index加上了saCheckLogin注解,这表示进入这个方法必须得是登录状态。

index.jsp是项目首页

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<c:set var="basePath" value="${pageContext.request.contextPath}"></c:set>
<html>
<head>
    <title>Title</title>
    <script src="${basePath}/layui/layui.all.js" charset="utf-8"></script>
    <link rel="stylesheet" href="${basePath}/layui/css/layui.css" media="all">
</head>
<body>
    首页
</body>
</html>

启动项目,让我们访问:http://localhost/

看到:

{"code":"9999","message":"Token无效:3fd99f78-10aa-4690-bd83-5fae198d1b87","data":null}

 这表示sa-token的配置已经生效了。

现在,我们去注册几个账号:

这就顺利进入了index.jsp。

6. 如何下载源码?

关注下方公众号,回复“日记本”即可。我把每一节的源码都单独打包给你准备好了。

  • 10
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 21
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 21
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

剽悍一小兔

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值