Springboot写电商系统(1)

Springboot写电商系统(1)

1.环境

1.创建项目

项目名称:store
spring initializr:maven(type),17.0.4(JDK)
web->spring web
sql->mybatis framework,mysql driver

2.数据库连接

在application.properties中配置数据库的连接源信息

spring.datasource.url=jdbc:mysql://localhost:3306/store?useUnicode=true&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=root

然后在test中测试连接是否抛出异常:

	@Autowired//自动装配数据库
    private DataSource dataSource;
     @Test
    void getConnection() throws SQLException{
        System.out.println(dataSource.getConnection());
    }

Hikari是一个连接池,用来管理数据库的连接对象,是springboot默认内部整合的连接池。

3.前端测试

将静态资源放到工程的static目录下,然后在右侧的maven里面点击工程名,点击lifecycle,然后先clean,最后在install,然后启动项目主程序,打开http://localhost:8080/web/login.html就可以看到登录页面。

4.开发环境热部署

因为之后在写代码的时候会经常测试,利用Spring Boot提供的spring-boot-devtools组件,使得无须手动重启Spring Boot应用即可重新编译、启动项目,大大缩短编译启动的时间。devtools会监听classpath下的文件变动,触发Restart类加载器重新加载该类,从而实现类文件和属性文件的热部署。
1.在pom.xml配置文件中添加dev-tools依赖,并更新maven库。

<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

2.在application.properties中配置devtools。

#热部署生效
spring.devtools.restart.enabled=true
#设置重启目录
spring.devtools.restart.additional-paths=src/main/java
#设置classpath目录下的WEB-INF文件夹内容修改不重启
spring.devtools.restart.exclude=static/**

使用ctrl+f9项目更新不用重启。

2.项目

1.用户注册

1.首先创建用户表

use store
CREATE TABLE t_user (
	uid INT AUTO_INCREMENT COMMENT '用户id',
	username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名',
	password CHAR(32) NOT NULL COMMENT '密码',
	salt CHAR(36) COMMENT '盐值',
	phone VARCHAR(20) COMMENT '电话号码',
	email VARCHAR(30) COMMENT '电子邮箱',
	gender INT COMMENT '性别:0-女,1-男',
	avatar VARCHAR(50) COMMENT '头像',
	is_delete INT COMMENT '是否删除:0-未删除,1-已删除',
	created_user VARCHAR(20) COMMENT '日志-创建人',
	created_time DATETIME COMMENT '日志-创建时间',
	modified_user VARCHAR(20) COMMENT '日志-最后修改执行人',
	modified_time DATETIME COMMENT '日志-最后修改时间',
	PRIMARY KEY (uid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

2.实体类class(entity/model)定义数据结构

1.把表的公共字段创建在一个实体类的基类中,之后所有的表创建按实体类的时候继承这个基类就可以有这些公共字段,在store/entity/BaseEntity中:

public class BaseEntity implements Serializable {
    private String createdUser;
    private Date createdTime;
    private String modifiedUser;
    private Date modifiedTime;
}
  • BaseEntity类实现了Serializable接口。Serializable是Java中的一个标记接口,它没有任何方法,仅用于指示该类的对象可以被序列化。序列化是将对象转换为字节流的过程,以便可以将对象保存到文件、数据库或通过网络传输,然后在需要时反序列化还原成对象。
    这里用的包装类来创建对象的属性,包装类(Wrapper Class)是Java中的一种特殊类,用于将基本数据类型(如整数、字符、布尔值等)包装成对象。 Integer - 包装int类型,Double - 包装double类型 Boolean - 包装boolean类型 Character - 包装char类型, Byte - 包装byte类型 Short - 包装short类型 Long - 包装long类型 Float -包装float类型

1.1然后Alt+Insert:getter and setter 生成私有属性的get和set方法
1.2Alt+Insert:equals() and hashcode()
1.3Alt+Insert:toString()
2.在entity/User类里面创建用户实体类继承baseEntity

public class User extends BaseEntity{
    private Integer uid;
    private String username;
    private String password;
    private String salt;
    private String phone;
    private String email;
    private Integer gender;
    private String avatar;
    private Integer isDelete;
}

2.1然后Alt+Insert:getter and setter 生成私有属性的get和set方法
2.2Alt+Insert:equals() and hashcode()
2.3Alt+Insert:toString()

3.持久层interface(mapper/repository)Mybatis操作数据库

1.在接口里写抽象方法

1.1在store/mapper/UserMapper接口里面写SQL语句的抽象方法,注册页面用到了插入和查询两个方法。

public interface UserMapper {
    /**
     * 插入用户数据
     * @param user 用户数据
     * @return 受影响的行数
     */
    Integer insert(User user);

    /**
     * 根据用户名查询用户数据
     * @param username 用户名
     * @return 如果找到就返回用户数据,否则返回null
     */
    User findByUsername(String username);

}
  • Javadoc注释的快捷键:在函数的上一行输入/**,然后按enter。

1.2在启动类里面添加项目的mapper路径:

@MapperScan("com.example.store.mapper")
2.在xml里写抽象方法的映射内容

2.1在resources/mapper/UserMapper.xml里面写抽象方法的映射文件。因为所有的映射文件都属于资源文件,所以需要放在resources目录下。创建接口的映射文件,需要和接口的名称保持一致.如UserMapper.xml。

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--1.namespace用于指定当前的xml和哪个interface进行mapper,需要指定接口的文件路径,路径需要是包的完整路径结构-->
<mapper namespace="com.example.store.mapper.UserMapper">
<!--3.2在sql语句的最上面借助ResultMap标签来自定义映射规则
        id属性:表示给这个映射规则分配一个唯一的id值,对应的就是resultMap="id属性值"
        type属性:取值是一个类,表示数据库中的查询结果与java中哪个实体类进行结果集的映射
     -->
    <resultMap id="UserEntityMap" type="com.example.store.entity.User">
        <!--将表的字段和类的属性名不一致的进行匹配指定,名称一致的也可以指定,但没必要,但是,在定义映射规则时无论主键名称是否一致都不能省
            column属性:表示表中的字段名称
            property属性:表示类中的属性名称-->
        <id column="uid" property="uid"></id>
        <result column="is_delete" property="isDelete"></result>
        <result column="created_user" property="createdUser"></result>
        <result column="created_time" property="createdTime"></result>
        <result column="modified_user" property="modifiedUser"></result>
        <result column="modified_time" property="modifiedTime"></result>
    </resultMap>
<!--2.insert方法
        id属性:表示映射的接口中方法的名称,
        useGeneratedKeys="true"表示开启某个字段的值递增(大部分都是主键递增)
        keyProperty="uid"表示将表中哪个字段进行递增 -->
    <insert id="insert" useGeneratedKeys="true" keyProperty="uid">
        insert into t_user(
            username,password,salt,phone,email,gender,avatar,is_delete,
            created_user,created_time,modified_user,modified_time
        ) values (
                     #{username},#{password},#{salt},#{phone},#{email},#{gender},#{avatar},#{isDelete},#{createdUser},#{createdTime},#{modifiedUser},#{modifiedTime}
                 )
    </insert>
<!--3.1select方法
        select语句在执行的时候查询的结果无非两种:一个对象或多个对象
        resultType:表示查询的结果集类型,用来指定对应映射类的类型,且包含完整的包结构,但此处不能是resultType="com.cy.store.entity.User",因为这种写法要求表的字段的名字和类的属性名一模一样
        resultMap:表示当表的字段和类的对象属性名不一致时,来自定义查询结果集的映射规则-->
    <select id="findByUsername" resultMap="UserEntityMap">
        select * from t_user where username=#{username}
    </select>

</mapper>

2.2将mapper文件的位置注册到properties对应的配置文件中

mybatis.mapper-locations=classpath:mapper/*.xml
3.测试

在test\java\com\example\store\mapper\UserMapperTests.java里面单元测试。单元测试方法:不用启动整个项目可以做单元测试,需满足四点: 1.必须被@Test注解注释;2.返回值类型是void; 3.方法的参数类型不指定任何类型; 4.方法的访问修饰符必须是public

@SpringBootTest//标注当前类是测试类,打包时会自动过滤
@RunWith(SpringRunner.class)//@RunWith表示启动这个单元测试类,否则这个单元测试类是不能运行的,需要传递一个参数,该参数必须是SpringRunner的实例类型
public class UserMapperTests {
    @Autowired
    private UserMapper userMapper;
    @Test
    public void insert(){
        User user = new User();
        user.setUsername("张三");
        user.setPassword("123456");
        Integer rows = userMapper.insert(user);
        System.out.println(rows);
    }
    @Test
    public void findByUsername() {
        User user = userMapper.findByUsername("张三");
        System.out.println(user);
    }
}

4.业务层service业务逻辑

这里包括ex包用来写业务层的异常类,impl包用来写接口的实现类,以及各种接口。

1.异常类

1.异常,可能是在业务层产生异常,可能是在控制层产生异常,所以可以创建一个业务层异常的基类ServiceException,并使其继承RuntimeException异常, 因为整个业务的异常只有运行时才会产生,所以要求业务层的异常都要继承运行时异常RuntimeException并且重写父类的所有构造方法以便后期能抛出自已定义的异常

package com.example.store.service.ex;

public class ServiceException extends RuntimeException{//Alt+Insert选择override Methods里面的5个构造方法,如下
    public ServiceException() {//什么也不返回
        super();
    }
    public ServiceException(String message) {//返回异常信息(常用)
        super(message);
    }
    public ServiceException(String message, Throwable cause) {//返回异常信息和异常对象(常用)
        super(message, cause);
    }
    public ServiceException(Throwable cause) {
        super(cause);
    }
    protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
        super(message, cause, enableSuppression, writableStackTrace);
    }
}

2.然后再根据业务定义具体异常,这里包含用户名被占用的UsernameDuplicatedException异常和插入操作时服务器或数据库宕机的InsertException异常,这些异常继承ServiceException异常基类,然后快捷键生成抛出异常的5种构造方法。

public class UsernameDuplicatedException extends ServiceException{
}
public class InsertException extends ServiceException{
}
2.接口interface

在service包下创建IUserService接口(接口命名的默认规则:I+业务名字+层的名字),并定义接口的抽象方法reg。

package com.example.store.service;

import com.example.store.entity.User;

public interface IUserService {
    void reg(User user);
}
3.接口的实现类implements

在imp包里面创建UserServiceImpl类,实现IUserService接口,并且实现抽象的方法。这里调用Mapper层的数据库交互方法并且捕获业务层的异常。

@Service//交给spring管理,所以需要在类上加@Service
public class UserServiceImpl implements IUserService {
    @Autowired
    private UserMapper userMapper; //reg方法核心就是调用mapper层的方法,所以要声明UserMapper对象并加@Autowired注解
    @Override
    public void reg(User user) {
        String username=user.getUsername();
        User result=userMapper.findByUsername(username);
        if (result!=null){
            throw new UsernameDuplicatedException("用户名被占用");
        }
        String oldpassword=user.getPassword();
        String salt= UUID.randomUUID().toString().toUpperCase();
        String md5Password=getMD5Password(oldpassword,salt);
        user.setPassword(md5Password);
        user.setSalt(salt);
        user.setIsDelete(0);
        user.setCreatedUser(user.getUsername());
        user.setModifiedUser(user.getUsername());
        Date date = new Date();//java.util.Date
        user.setCreatedTime(date);
        user.setModifiedTime(date);
        Integer rows = userMapper.insert(user);//执行注册业务功能的实现
        if (rows != 1) {
            throw new InsertException("在用户注册过程中产生了未知的异常");
        }
    }
    private String getMD5Password(String password,String salt){
        for (int i=0;i<3;i++){
            password= DigestUtils.md5DigestAsHex((salt+password+salt).getBytes()).toUpperCase();
        }
        return password;
    }
}
4.测试
@SpringBootTest//标注当前类是测试类,打包时会自动过滤
@RunWith(SpringRunner.class)//@RunWith表示启动这个单元测试类,否则这个单元测试类是不能运行的,需要传递一个参数,该参数必须是SpringRunner的实例类型
public class UserServiceTests {
    @Autowired
    private IUserService userService;
    @Test
    public void reg(){
        try{
            User user = new User();
            user.setUsername("张4");
            user.setPassword("123456");
            userService.reg(user);
            System.out.println("OK");
        }catch(ServiceException e){
            System.out.println(e.getClass().getSimpleName());
            System.out.println(e.getMessage());
        }
    }
}

5.控制层controller(http请求和响应)

1.创建响应

响应都是状态码,状态描述信息,数据,所以把这部分功能封装到utils包下面的JsonResult类中,将这个类作为方法的返回值返回给前端浏览器。

public class JsonResult<E> implements Serializable {//因为所有的响应的结果都采用Json格式的数据进行响应,所以需要实现Serializable接口
    private Integer state;
    private String message;
    private E data;

    public JsonResult() {//Alt+Insert:constructor,生成无参构造方法
    }

    public JsonResult(Integer state) {//Alt+Insert:constructor,选择状态码,给状态码生成一个state参数构造方法
        this.state = state;
    }

    public JsonResult(Integer state, E data) {//Alt+Insert:constructor,选择状态码,给状态码生成一个state参数和数据构造方法
        this.state = state;
        this.data = data;
    }

    public JsonResult(Throwable e) {//Alt+Insert:constructor,生成异常构造方法
        this.message = e.getMessage();
    }
    //所有参数的 get set方法
}

2.处理异常响应

依据当前的业务功能模块进行请求的设计:

请求路径:/users/reg
请求参数:User user
请求类型:POST
响应结果:JsonResult

在controller包下面创建BaseController类作为请求处理错误拦截的基础响应:

public class BaseController {
    public static final int OK = 200;//操作成功的状态码
    /**
     * 1.@ExceptionHandler表示该方法用于处理捕获抛出的异常
     * 2.ServiceException.class,只要是抛出ServiceException异常就会被拦截到handleException方法,此时handleException方法就是请求处理方法,返回值就是需要传递给前端的数据
     * 3.被ExceptionHandler修饰后如果项目发生异常,那么异常对象就会被自动传递给此方法的参数列表上,所以形参就需要写Throwable e用来接收异常对象
     */
    @ExceptionHandler(ServiceException.class)
    public JsonResult<Void> handleException(Throwable e) {
        JsonResult<Void> result = new JsonResult<>(e);
        if (e instanceof UsernameDuplicatedException) {
            result.setState(4000);
            result.setMessage("用户名已经被占用");
        } else if (e instanceof InsertException) {
            result.setState(5000);
            result.setMessage("插入数据时产生未知的异常");
        }
        return result;
    }
}

3.处理成功响应

创建控制层对应的UserController类继承基类,实现用户注册功能和成功响应。

@RestController //其作用等同于@Controller+@ResponseBody
@RequestMapping("users")
public class UserController extends BaseController {
    @Autowired
    private IUserService userService;

    @RequestMapping("reg")
    //@ResponseBody //表示此方法的响应结果以json格式进行数据的响应给到前端
    public JsonResult<Void> reg(User user) {
        JsonResult<Void> result = new JsonResult<>();//创建响应结果对象即JsonResult对象
        userService.reg(user);
        return new JsonResult<>(OK);
    }
}

然后运行项目启动文件,在本地浏览器输入http://127.0.0.1:8080/users/reg?username=6677&password=123进行测试。

6.前端请求响应

这里使用jQuery封装的ajax异步请求方法,依靠的是JavaScript提供的一个对象:XHR(全称XmlHttpResponse)。
ajax()函数的语法结构如下:

$.ajax({
    url: "",//请求url地址,能包含参数列表部分的内容
    type: "",//请求类型(GET和POST请求的类型)
    data: "",//向指定的请求url地址提交的数据.例如:data:“username=tom&pwd=123”
    dataType: "",//提交的数据的类型.数据的类型一般指定为json类型
    success: function() {
        //当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上
    },
    error: function() {
        //当服务器未正常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递给这个方法的参数上
    }
});

注册时的ajax请求如下:

<script>
	$("#btn-reg").click(function(){
		$.ajax({
			url:"/users/reg",
			type:"POST",
			data:$("#form-reg").serialize(), //serialize这个API会自动检测该表单有什么控件,每个控件检测后还会获取每个控件的值,拿到这个值后并自动拼接成形如username=Tom&password=123的结构
			dataType:"JSON",
			success:function(json){
				if(json.state==200){
					alert("注册成功")
				}else{
					alert("注册失败")
				}
			},
			error:function(xhr){
				alert("注册时产生未知错误!"+xhr.status);
			}
		});
	});
	</script>

js代码未加载到页面时:

1.在maven下clear然后install重新部署
2.在file里面选择invalide cashes清理缓存
3.build里面选择rebuild重新构建项目
4.重启idea
5.重启电脑。

2.用户登录

根据用户名查询用户信息在持久层实现,密码的比较在业务层,所以实体类和持久层的功能都已经实现,接下来从业务层开始。

1.业务层

业务层是通过持久层的用户查询到用户数据后开始比较密码和是否删除,然后把用户的id,用户名和头像赋值给新的user,方便之后传递给前端展示,即将当前登录成功的用户数据以当前用户对象的形式进行返回,然后进行状态管理。

1.异常类

在登录这个业务里会出现用户没有查询到和密码不匹配两个异常,所以在service的ex包里面创建UsernameNotFoundException和PasswordNotMatchException两个异常类,并都继承ServiceException,然后生成抛出异常的5种构造方法。

public class UsernameNotFoundException extends ServiceException{
}
public class PasswordNotMatchException extends ServiceException{
}
2.接口interface

会出现的异常已经捕获,现在开始业务需要的功能接口,在IUserService接口中编写抽象方法login。因为将当前登录成功的用户数据以当前用户对象的形式进行返回,然后进行状态管理,所以此方法要返回User对象。

public interface IUserService {
    //void reg(User user);
    User login(String username,String password);
}
3.接口的实现类implements

在接口里面定义了要实现方法,所以在抽象类UserServiceImpl中具体实现。

@Override
    public User login(String username, String password) {
        User result = userMapper.findByUsername(username);
        if (result == null) {
            throw new UsernameNotFoundException("用户数据不存在");
        }
        if (result.getIsDelete() == 1) {
            throw new UsernameNotFoundException("用户数据不存在");
        }
        String oldPassword = result.getPassword();
        String salt = result.getSalt();
        String newMd5Password = getMD5Password(password, salt);
        if (!newMd5Password.equals(oldPassword)) {
            throw new PasswordNotMatchException("用户密码错误");
        }
        User user = new User();
        user.setUid(result.getUid());
        user.setUsername(result.getUsername());
        user.setAvatar(result.getAvatar());
        return user;
    }
4.测试

业务层的异常,接口和实现类都写完就可以进行测试。在UserServiceTests中添加测试方法

@Test
    public void login(){
        User user= userService.login("111","111");
        System.out.println(user);
    }

2.控制层controller

首先是创建响应,响应和登录一样,都是code,state和message三个信息,所以直接继承。

1.处理异常响应

在登录业务里出现了两个之前没有的异常,即用户不存在和密码不匹配,那么在BaseController类里面添加这两个异常的响应:

else if (e instanceof UsernameNotFoundException) {
    result.setState(4001);
    result.setMessage("用户数据不存在的异常");
} else if (e instanceof PasswordNotMatchException) {
    result.setState(4002);
    result.setMessage("用户名密码错误的异常");
}
2.处理成功响应

异常响应处理完,那么开始处理成功的响应。在UserController类中调用调用service层的login方法。

    @RequestMapping("login")
    public JsonResult<User> login(String username,String password) {
        User data = userService.login(username, password);
        return new JsonResult<User>(OK,data);
    }

然后运行程序,进入http://127.0.0.1:8080/users/login?username=6678&password=123进行登录测试。

3.前端请求响应

$("#btn-login").click(function(){
			$.ajax({
				url:"/users/login",
				type:"POST",
				data:$("#form-login").serialize(),
				dataType:"JSON",
				success:function(json){
					if(json.state==200){
						alert("登录成功")
						location.href="./index.html";
					}else{
						alert("登录失败")
					}
				},
				error:function(xhr){
					alert("登录时产生未知异常"+xhr.message);
				}
			});
		});

4.将登录功能返回的user对象保存到session对象

如果直接将HttpSession类型的对象作为请求处理方法的参数,这时springboot会自动将全局的session对象注入到请求处理方法的session形参上,所以在发送登录请求时添加HttpSession类型的对象,并向session对象中完成数据的绑定(这个session是全局的,项目的任何位置都可以访问)。在控制层登录时实现这个功能,将uid和username保存到session

    @RequestMapping("login")
    public JsonResult<User> login(String username, String password, HttpSession session) {
        User data = userService.login(username, password);
        session.setAttribute("uid",data.getUid());
        session.setAttribute("username",data.getUsername());
        return new JsonResult<User>(OK,data);
    }

5.通过session对象拿到用户的uid和username

因为每个页面都要从session拿到用户的这些数据,所以将获取的方法放到BaseControler中

public final Integer getUidFromSession(HttpSession session) {
            //getAttribute返回的是Object对象,需要转换为字符串再转换为包装类
            return Integer.valueOf(session.getAttribute("uid").toString());
        }

        public final String getUsernameFromSession(HttpSession session) {
            return session.getAttribute("username").toString();
        }

可以在UserController的登录功能里面测试一下,加入:

//测试能否正常获取session中存储的数据
        System.out.println(getUidFromSession(session));
        System.out.println(getUsernameFromSession(session));

6.拦截器

拦截器的作用是将所有的请求统一拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统设置的过滤规则,该项目统一的处理是重新去打开login.html页面(重定向和转发都可以,推荐使用重定向)
拦截器在springboot中本质是依靠springMVC完成的.springMVC提供了一个HandlerInterceptor接口用于表示定义一个拦截器。

1.拦截逻辑

在store下建包interceptor,包下建类LoginInterceptor并编写拦截器

public class LoginInterceptor implements HandlerInterceptor {//快捷键重写接口方法,共有3个方法,这里我们只用到了第一个
    @Override//在DispatcherServlet调用所有处理请求的方法前被自动调用执行的方法
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //springboot会自动把请求对象给到request,响应对象给到response,适配器给到handler
        Object obj = request.getSession().getAttribute("uid");//通过HttpServletRequest对象来获取session对象
        if (obj == null) {
            response.sendRedirect("/web/login.html");//说明用户没有登录过系统,则重定向到login.html页面
            return false;//结束后续的调用
        }
        return true;//放行这个请求
    }
    @Override//在ModelAndView对象返回给DispatcherServlet之后被自动调用的方法
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
    }
    @Override//在整个请求所有关联的资源被执行完毕后所执行的方法
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
    }
}
2.注册拦截器

定义一个类使其实现WebMvcConfigure接口并在其内部添加黑名单(在用户登录的状态下才可以访问的页面资源)和白名单(哪些资源可以在不登录的情况下访问:①register.html②login.html③index.html④/users/reg⑤/users/login⑥静态资源)。在store包下建config包,再定义类LoginInterceptorConfigure。

@@Configuration //自动加载当前的类并进行拦截器的注册,如果没有@Configuration就相当于没有写类LoginInterceptorConfigure
public class LoginInterceptorConfigure implements WebMvcConfigurer {
    @Override
    //配置拦截器
    public void addInterceptors(InterceptorRegistry registry) {
        //1.创建刚刚定义的拦截逻辑的拦截器对象
        HandlerInterceptor interceptor =  new LoginInterceptor();
        //2.配置白名单并存放在一个List集合
        List<String> patterns = new ArrayList<>();
        patterns.add("/bootstrap3/**");
        patterns.add("/css/**");
        patterns.add("/images/**");
        patterns.add("/js/**");
        patterns.add("/web/register.html");
        patterns.add("/web/login.html");
        patterns.add("/web/index.html");
        patterns.add("/web/product.html");
        patterns.add("/users/reg");
        patterns.add("/users/login");
        //registry.addInterceptor(interceptor);完成拦截器的注册,后面的addPathPatterns表示拦截哪些url再后面的excludePathPatterns表示有哪些是白名单,且参数是列表
        registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(patterns);
    }
}

3.修改密码

修改用户密码,用的还是user表和实体类,但与数据库的交互里会在repository层进行数据更新操作。所以从持久层开始。

1.数据库update操作的持久层

interface写接口方法,xml写方法的映射。

1.在接口里写抽象方法

根据分析,这里需要用到根据uid查询用户信息根据uid更新用户password和modified信息两个sql语句。所以在UserMapper接口中定义这两个抽象方法。

Integer updatePasswordByUid(Integer uid, String password, String modifiedUser, Date modifiedTime);
User findByUid(Integer uid);
2.在xml里写抽象方法的映射内容
<update id="updatePasswordByUid">
    update t_user set password=#{password},modified_user=#{modifiedUser},modified_time=#{modifiedTime} where uid=#{uid}
</update>
<select id="findByUid" resultMap="UserEntityMap">
    select * from t_user where uid=#{uid}
</select>
3.测试
 @Test
    public void updatePasswordByUid(){
        userMapper.updatePasswordByUid(6,"1111","zoe",new Date());
    }
    @Test
    public void findByUid(){
        System.out.println(userMapper.findByUid(6));
    }

2.运行SQL抛出异常和执行正确的业务层

1.运行sql时会出现的异常类

在ex中新建UpdateException类

public class UpdateException extends ServiceException{}//快捷键生成5个构造方法
2.运行sql的抽象方法

在IUserService接口里定义修改密码的抽象方法

void changePassword(Integer uid,String username,String oldPassword,String newPassword);
3.运行sql的具体实现和异常抛出

在UserServiceImpl类里面实现接口定义的方法

  @Override
    public void changePassword(Integer uid, String username, String oldPassword, String newPassword) {
        User result=userMapper.findByUid(uid);
        if(result==null||result.getIsDelete()==1){
            throw new UsernameNotFoundException("用户数据不存在");
        }
        String oldMd5Password=getMD5Password(oldPassword,result.getSalt());//先确认用户输入的旧密码对不对
        if (!result.getPassword().equals(oldMd5Password)){
            throw new PasswordNotMatchException("密码错误");
        }
        String newMd5Password=getMD5Password(newPassword,result.getSalt());
        Integer rows=userMapper.updatePasswordByUid(uid,newMd5Password,username,new Date());
        if (rows!=1){
            throw new UpdateException("更新数据发生未知异常");
        }
    }
4.测试
@Test
    public void changePassword(){
        userService.changePassword(2,"ZOE","123456","111");
    }

3.http响应抛出异常和执行正确的控制层

1.异常响应里多了UpdateException异常

在BaseController里面添加响应时会出现的更新异常

else if (e instanceof UpdateException) {
            result.setState(5001);
            result.setMessage("更新数据产生异常");
        }
2.处理成功响应
@RequestMapping("change_password")
    public JsonResult<Void> changePassword(String oldPassword,String newPassword,HttpSession session){
        Integer uid=getUidFromSession(session);
        String username=getUsernameFromSession(session);
        userService.changePassword(uid,username,oldPassword,newPassword);
        return new JsonResult<>(OK);
    }

最后进入网站http://localhost:8080/users/change_password?oldPassword=111&newPassword=123进行测试。

4.前端ajax请求

        <script>
            $("#btn-change-password").click(function () {
                $.ajax({
                    url: "/users/change_password",
                    type: "POST",
                    data: $("#form-change-password").serialize(),
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("密码修改成功")
                        } else {
                            alert("密码修改失败")
                        }
                    },
                    error: function (xhr) {
                        //xhr.message可以获取未知异常的信息
                        alert("修改密码时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>

4.修改个人资料

个人资料也用的是user表和实体类,所以功能从持久层开始。

1.数据库操作的持久层

1.接口

打开页面时首先是拿到session里面的uid,然后通过uid查询user信息,显示到表单中,这个查询操作已经存在;然后就是通过uid修改phone,email和gender信息,这个update的数据库操作需要写,所以先在UserMapper接口里面写抽象方法:

Integer updateInfoByUid(User user);
2.接口映射

然后再在UserMapper的xml文件里写方法的映射:

    <update id="updateInfoByUid">
        update t_user set 
                          <if test="phone!=null">phone=#{phone},</if>
                          <if test="email!=null">email=#{email},</if>
                          <if test="gender!=null">gender=#{gender},</if>
                          modified_user=#{modifiedUser},
                          modified_time=#{modifiedTime} where uid=#{uid}
    </update>
3.测试
@Test
    public void updateInfoByUid(){
        User user=new User();
        user.setUid(2);
        user.setPhone("18811752638");
        user.setEmail("18811752638@163.com");
        user.setGender(0);
        userMapper.updateInfoByUid(user);
    }

2.业务逻辑层(前端数据与后端数据的交互)

1.业务sql执行中会出现的error

首先是进入页面是通过uid查询用户数据时会找不到用户,这个错误之前已经定义。
然后是修改用户数据时会出现更新数据时产生错误,这个错误也已经存在。

2.业务逻辑

在IUserService接口里面定义业务逻辑的方法,这里涉及到通过uid拿到用户信息,然后通过uid更改用户信息。

User getByUid(Integer uid);
void changeInfo(Integer uid,String username,User user);//前面两个是session中拿到的,后面的user是前端控制层拿到的
3.接口功能实现(前端数据与后端数据的交互)

在UserServiceImpl中实现上接口中要完成的功能。

    @Override
    public User getByUid(Integer uid) {//uid通过session拿到
        User result=userMapper.findByUid(uid);
        if (result==null||result.getIsDelete()==1){
            throw new UsernameNotFoundException("用户数据不存在");
        }
        User user =new User();//把前端需要的数据放到新的user中进行数据传递
        user.setUsername(result.getUsername());
        user.setPhone(result.getPhone());
        user.setEmail(result.getEmail());
        user.setGender(result.getGender());
        return user;
    }

    @Override
    public void changeInfo(Integer uid, String username, User user) {
        User result=userMapper.findByUid(uid);
        if (result==null||result.getIsDelete()==1){
            throw new UsernameNotFoundException("用户数据不存在");
        }
        user.setUid(uid);
        user.setModifiedUser(username);
        user.setModifiedTime(new Date());
        Integer rows=userMapper.updateInfoByUid(user);
        if (rows!=1){
            throw new UpdateException("更新数据时产生异常");
        }
    }
4.测试
@Test
    public void getByUid(){
        System.err.println(userService.getByUid(2).getUsername());
    }
    @Test
    public void changeInfo(){
        User user=new User();
        user.setEmail("11@163.com");
        user.setPhone("188111");
        user.setGender(1);
        userService.changeInfo(2,"dz",user);
    }

3.控制层(前端请求失败与成功并拿到前端数据和session数据)

1.error

业务层没有新的error,这一层也没有。

2.成功响应

这里首先是进入修改信息的页面,从session拿到uid,然后查询user。
修改表单的话,从session拿到uid,username,然后从前端拿到username,email,phone和gender,然后再调用业务层,实现修改。(这里username有重复,其实业务层可以删掉session拿到的username,只用表单里的username即可)。

    @RequestMapping("get_by_uid")//输入http://localhost:8080/users/get_by_uid测试
    public JsonResult<User> getByUid(HttpSession session){
        User data=userService.getByUid(getUidFromSession(session));
        return new JsonResult<User>(OK,data);
    }
    @RequestMapping("change_info")//输入http://localhost:8080/users/change_info?phone=175726&email=6695@qq.com&gender=1测试
    public JsonResult<Void> changeInfo(User user,HttpSession session){
        Integer uid=getUidFromSession(session);
        String username=getUsernameFromSession(session);
        userService.changeInfo(uid,username,user);
        return new JsonResult<>(OK);
    }

4.前端ajax请求

	<script>
		//点击"个人资料"四个字加载userdata.html页面时$(document).ready(function(){});就会起作用发送ajax请求
		$(document).ready(function() {
			$.ajax({
				url: "/users/get_by_uid",
				type: "GET",
				data: "",
				dataType: "JSON",
				success: function (json) {
					if (json.state == 200) {
						//将查询到的数据设置到控件中
						$("#username").val(json.data.username);
						$("#phone").val(json.data.phone);
						$("#email").val(json.data.email);
						var radio = json.data.gender == 0 ?
								$("#gender-female") : $("#gender-male");
						//prop()表示给某个元素添加属性及属性的值
						radio.prop("checked","checked");
					} else {
						alert("用户的数据不存在")
					}
				},
				error: function (xhr) {
					//xhr.message可以获取未知异常的信息
					alert("查询用户信息时产生未知的异常!"+xhr.message);
				}
			});
		});
		$("#btn-change-info").click(function () {
			$.ajax({
				url: "/users/change_info",
				type: "POST",
				data: $("#form-change-info").serialize(),
				dataType: "JSON",
				success: function (json) {
					if (json.state == 200) {
						alert("用户信息修改成功")
						//修改成功后重新加载当前的页面
						location.href = "userdata.html";
					} else {
						alert("用户信息修改失败")
					}
				},
				error: function (xhr) {
					//xhr.message可以获取未知异常的信息
					alert("用户信息修改时产生未知的异常!"+xhr.message);
				}
			});
		});
	</script>

5.上传图像

用user表和实体,但数据库操作里需要添加一个根据uid更新avatar的update,所以从持久层开始。

1.持久层(数据库操作)

1.接口里写操作方法
Integer updateAvatarByUid(Integer uid,String avatar,String modifiedUser,Date modifiedTime);//根据uid更新后面三个参数,所以这里4个参数
2.mapper里写数据库操作步骤

这里是数据库字段数据接口传来的变量的对应更新。

<update id="updateAvatarByUid">
        update t_user set avatar=#{avatar},modified_user=#{modifiedUser},modified_time=#{modifiedTime} where uid=#{uid}
</update>
3.测试
 @Test
    public void updateAvatarByUid(){
        userMapper.updateAvatarByUid(2,"/upload/avatar.jpg","老王",new Date());
    }

2.业务层

首先是SQL执行会抛出的异常,包括找不到用户和更新时出现异常,这两个已经有了,所以sql执行期间的异常不用再写。
开始写sql正确执行的业务逻辑,首先现在接口里写要实现的方法:
首先从控制层传来uid,username和avatar地址数据,

void changeAvatar(Integer uid,String username,String avatar);//前面两个是session中拿到的,后面的图像地址从控制层拿到

然后再具体把这三个数据和业务层生成的date数据传递给持久层去实现数据库更新,这里是异常和正确执行的过程。

@Override
    public void changeAvatar(Integer uid, String username, String avatar) {
        User result=userMapper.findByUid(uid);
        if (result==null||result.getIsDelete()==1){//异常
            throw new UsernameNotFoundException("用户数据不存在");
        }
        Integer rows=userMapper.updateAvatarByUid(uid,avatar,username,new Date());//正确执行
        if (rows!=1){
            throw new UpdateException("更新用户头像时产生未知异常");
        }
    }

测试

@Test
    public void changeAvatar(){
        userService.changeAvatar(2,"dz","/upload/avatar2.jpg");
    }

3.控制层

业务层会出现的error已经写在了BaseController里面,所以不用再考虑,但控制层在文件上传过程中会出现error。

1.请求响应异常

这里的异常首先是文件上传过程中的异常FileUploadException,作为基类,其子类包括:

FileEmptyException:文件为空的异常(没有选择上传的文件就提交了表单,或选择的文件是0字节的空文件)
FileSizeException:文件大小超出限制
FileTypeException:文件类型异常(上传的文件类型超出了限制)
FileUploadIOException:文件读写异常
FileStateException:文件状态异常(上穿文件时该文件正在打开状态)

在controller包下创子包ex,在ex包里面创建文件异常类的基类继承RuntimeException和上述五个文件异常类继承父类,创建的六个类都重写其父类的五个构造方法。
然后再把这5个子异常类添加到BaseController,是所有继承这个基类的控制器都可以按需调用异常类。

else if (e instanceof FileEmptyException) {
    result.setState(6000);
} else if (e instanceof FileSizeException) {
    result.setState(6001);
} else if (e instanceof FileTypeException) {
    result.setState(6002);
} else if (e instanceof FileStateException) {
    result.setState(6003);
} else if (e instanceof FileUploadIOException) {
    result.setState(6004);
}

添加之后还需要修改一下这个异常捕获的注解,添加文件上传的异常捕获:

@ExceptionHandler({ServiceException.class,FileUploadException.class})
2.请求响应过程

这里用到了springmvc提供的MultipartFile 来获取表单上传的文件。

public static final Integer AVATAR_MAX_SIZE=1024*1024*10;//定义上传文件大小不超过10M
    public static final List<String> AVATAR_TYPE=new ArrayList<>();//定义上传文件类型到一个列表里
    static {
        AVATAR_TYPE.add("image/jpeg");
        AVATAR_TYPE.add("image/png");
        AVATAR_TYPE.add("image/bmp");
        AVATAR_TYPE.add("image/gif");
    }
    @RequestMapping("change_avatar")
    public JsonResult<String> changeAvatar(HttpSession session, MultipartFile file){//file是因为前端name=file:<input type="file" name="file">中的name="file",所以必须有一个方法的参数名为file用于接收前端传递的该文件.如果想要参数名和前端的name不一样:@RequestParam("file")MultipartFile ffff:把表单中name="file"的控件值传递到变量ffff上
        if (file.isEmpty()) {
            throw new FileEmptyException("文件为空");
        }
        if (file.getSize()>AVATAR_MAX_SIZE) {
            throw new FileSizeException("文件超出限制");
        }
        //判断文件的类型是否是我们规定的后缀类型
        String contentType = file.getContentType();
        //如果集合包含某个元素则返回值为true
        if (!AVATAR_TYPE.contains(contentType)) {
            throw new FileTypeException("文件类型不支持");
        }
        String parent = session.getServletContext().getRealPath("/upload");
        File dir = new File(parent);//File对象指向这个路径,通过判断File是否存在得到该路径是否存在
        if (!dir.exists()) {//检测目录是否存在
            dir.mkdirs();//创建当前目录
        }
        String originalFilename = file.getOriginalFilename();
        System.out.println("OriginalFilename="+originalFilename);
        int index = originalFilename.lastIndexOf(".");
        String suffix = originalFilename.substring(index);
        String filename = UUID.randomUUID().toString().toUpperCase()+suffix;
        File dest = new File(dir, filename);  //在dir目录下创建filename文件(此时是空文件)
        try {
            file.transferTo(dest);//transferTo是一个封装的方法,用来将file文件中的数据写入到dest文件
        } catch (FileStateException e) {
            throw new FileStateException("文件状态异常");
        } catch (IOException e) {
            //这里不用打印e,而是用自己写的FileUploadIOException类并
            // 抛出文件读写异常
            throw new FileUploadIOException("文件读写异常");
        }
        Integer uid = getUidFromSession(session);
        String username = getUsernameFromSession(session);
        String avatar = "/upload/"+filename;
        userService.changeAvatar(uid,username,avatar);
        return new JsonResult<>(OK,avatar);//返回用户头像的路径给前端页面,将来用于头像展示使用
    }

4.前端请求

这里用表单提交文件的方式,所以只需要在上传头像的表单里添加属性action,method和enctype

<form class="form-horizontal" role="form" action="/users/change_avatar" method="post" enctype="multipart/form-data">

然后表单的提交type是submit:

<input type="submit" class="btn btn-primary" value="上传" />

即可实现文件上传功能。

5.优化

1.文件上传大小的限制

springmvc默认为1MB文件可以上传,在控制层设置的大小也需要手动修改springmvc设置才能有效。这里有两种修改的方式:

1.application.propertie设置上传文件大小
spring.servlet.multipart.max-file-size=10MB//表示上传的文件最大是多大
spring.servlet.multipart.max-request-size=15MB//整个文件是放在了request中发送给服务器的,请求当中还会有消息头等其他携带的信息,这里设置请求最大为15MB
2.工程主类中设置上传文件大小
@Bean
public MultipartConfigElement getMultipartConfigElement() {
    //1.创建一个配置的工厂类对象
    MultipartConfigFactory factory = new MultipartConfigFactory();

    //2.设置需要创建的对象的相关信息
    factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES));
    factory.setMaxRequestSize(DataSize.of(15,DataUnit.MEGABYTES));

    //3.通过工厂类创建MultipartConfigElement对象
    return factory.createMultipartConfig();
}
2.上传后显示头像

要点击上传后显示新的头像,那么就需要ajax请求,上传后从后端再拿到数据显示。所以就不能用form提交的形式上传文件,用button按钮监听点击事件。所以

1.删掉在upload.html的上传头像的表单中加的三个属性:action=“/users/change_avatar”,method=“post”,enctype=“multipart/form-data”
2.并加上id属性:id=“form-change-avatar”.
3.把input标签里面的type="submit"改为type=“button”(因为submit按钮不能添加事件,所以要改为普通的按钮)
4.并加上属性id=“btn-change-avatar”.
5.serialize():可以将表单数据自动拼接成key=value的结构提交给服务器,一般提交的是普通的控件类型中的数据.
6.FormData类:将表单中数据保持原有的结构进行数据提交.文件类型的数据可以使用FormData对象进行存储.
7.ajax默认处理数据时按照字符串的形式进行处理,以及默认会采用字符串的形式进行数据提交.手动关闭这两个功能:
processData: false,//处理数据的形式,关闭处理数据 contentType: false,//提交数据的形式,关闭默认提交数据的形式

        <script>
            $("#btn-change-avatar").click(function () {
                $.ajax({
                    url: "/users/change_avatar",
                    type: "POST",
                    data: new FormData($("#form-change-avatar")[0]),
                    processData: false,//处理数据的形式,关闭处理数据
                    contentType: false,//提交数据的形式,关闭默认提交数据的形式
                    dataType: "JSON",
                    success: function (json) {
                        if (json.state == 200) {
                            alert("头像修改成功")
                            //将服务器端返回的头像地址设置到img标签的src属性上
                            //attr(属性,属性值)用来给某个属性设值
                            $("#img-avatar").attr("src",json.data);
                        } else {
                            alert("头像修改失败")
                        }
                    },
                    error: function (xhr) {
                        alert("修改头像时产生未知的异常!"+xhr.message);
                    }
                });
            });
        </script>
3.头像地址保存到cookie中使用

首先要用cookie的话,那就要(在login和upload)添加:

<script src="../bootstrap3/js/jquery.cookie.js" type="text/javascript" charset="utf-8"></script>

然后用户登录了之后就开始把后端传过来的avatar(登录成功后返回了user对象里面有avatar信息)存到前端的cookie:

cookie的使用方法:$.cookie(key,value,time);//time单位:天

success: function (json) {
    if (json.state == 200) {
        location.href = "index.html";
        $.cookie("avatar",json.data.avatar,{expires: 7});
    } else {
        alert("登录失败")
    }
},

然后上传(upload)界面里面需要添加ajax的网页响应,这样进来就会显示图像:

$(document).ready(function(){
    var avatar = $.cookie("avatar");
    console.log(avatar);//调试用
    $("#img-avatar").attr("src",avatar);
})

而且在upload请求成功后再拿一次avatar,这就是新的数据:

$.cookie("avatar",json.data,{expires: 7});
  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

我是小z呀

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

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

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

打赏作者

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

抵扣说明:

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

余额充值