商城项目(上)

1. 项目大致分析

需要处理的数据的种类:用户,商品,收货地址,购物车,订单,收藏,商品分类……

开发时,应该先完成:比较简单的、与其它数据关联不大、是其它数据的基础支撑的数据,例如用户数据。

所以,处理数据的顺序可以大致是:用户 > 收货地址 > 商品分类与商品 > 购物车与收藏 > 订单。

每种数据的处理,都涉及一些功能,以用户数据为例,可能有:注册、登录、修改资料、修改密码、上传头像,通常,遵循“增 > 查 > 删 > 改”且“由简到难”的步骤一一完成,所以,当处理用户数据时,应该:注册 > 登录 > 修改密码 > 修改资料 > 上传头像。

在处理每一个功能时,都应该遵循的开发顺序:持久层 > 业务层 > 控制器层 > 界面。

2. 用户-注册-持久层

创建数据库

在MySQL中创建名为tedu_store的数据库:

CREATE DATABASE tedu_store;

创建数据表

CREATE TABLE t_user (
	id INT AUTO_INCREMENT COMMENT 'id',
	username VARCHAR(20) UNIQUE NOT NULL  COMMENT '用户名', 
	password CHAR(32) NOT NULL COMMENT '密码',
	salt CHAR(36) NOT NULL COMMENT '盐值',
	gender INT COMMENT '性别',
	phone VARCHAR(20) COMMENT '手机号码',
	email VARCHAR(50) COMMENT '电子邮箱',
	avatar VARCHAR(100) COMMENT '头像',
	is_delete INT DEFAULT 0 COMMENT '是否删除,0-未删除,1-已删除',
	created_user VARCHAR(20) COMMENT '创建者',
	created_time DATETIME COMMENT '创建时间',
	modified_user VARCHAR(20) COMMENT '最后修改者',
	modified_time DATETIME COMMENT '最后修改时间',
	PRIMARY KEY(id)
) DEFAULT CHARSET=UTF8;

创建项目

打开Spring Boot的官方网站:https://start.spring.io/

输入各参数,并勾选MyBatisMySQL,生成项目。

将下载的压缩包解压到Workspace中,然后,在Eclipse中通过Import > Import > Existing Maven Projects导入该项目(如果是第1次使用Spring Boot,需要保证与Maven服务器的网络畅通)。

由于创建项目时勾选了数据库编程的依赖,所以,当Spring Boot启动时,会尝试创建数据源对象,但是,没有添加任何配置时,是无法创建数据源对象的,就会导致启动失败,所以,先在配置文件application.properties中添加数据源的配置:

# spring datasource
spring.datasource.url=jdbc:mysql://localhost:3306/tedu_store?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

框架会自动读取数据库驱动jar包中的META-INF/services/java.sql.Driver文件,文件中的值就是数据源的driverClassName的值,所以,配置数据源时,无须配置driverClassName

完成后,测试连接是否可用:

package cn.tedu.store.db;

import java.sql.SQLException;

import javax.sql.DataSource;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class DataSourceTestCase {

	@Autowired
	DataSource dataSource;
	
	@Test
	public void getConnection() throws SQLException {
		System.out.println(dataSource.getConnection());
	}
}

创建实体类

创建实体类的基类cn.tedu.store.entity.BaseEntity,添加日志相关的4个属性,并实现序列化接口:

package cn.tedu.store.entity;

import java.io.Serializable;
import java.util.Date;

public class BaseEntity implements Serializable {

	private static final long serialVersionUID = -6185124879935579311L;

	private String createdUser;
	private Date createdTime;
	private String modifiedUser;
	private Date modifiedTime;

	public String getCreatedUser() {
		return createdUser;
	}

	public void setCreatedUser(String createdUser) {
		this.createdUser = createdUser;
	}

	public Date getCreatedTime() {
		return createdTime;
	}

	public void setCreatedTime(Date createdTime) {
		this.createdTime = createdTime;
	}

	public String getModifiedUser() {
		return modifiedUser;
	}

	public void setModifiedUser(String modifiedUser) {
		this.modifiedUser = modifiedUser;
	}

	public Date getModifiedTime() {
		return modifiedTime;
	}

	public void setModifiedTime(Date modifiedTime) {
		this.modifiedTime = modifiedTime;
	}

}

创建cn.tedu.store.entity.User类,添加与数据表对应的属性,并继承自以上的BaseEntity类:

package cn.tedu.store.entity;

public class User extends BaseEntity {

	private static final long serialVersionUID = -3643571432521920213L;

	private Integer id;
	private String username;
	private String password;
	private String salt;
	private Integer gender;
	private String phone;
	private String email;
	private String avatar;
	private Integer isDelete;

	public Integer getId() {
		return id;
	}

	public void setId(Integer id) {
		this.id = id;
	}

	public String getUsername() {
		return username;
	}

	public void setUsername(String username) {
		this.username = username;
	}

	public String getPassword() {
		return password;
	}

	public void setPassword(String password) {
		this.password = password;
	}

	public String getSalt() {
		return salt;
	}

	public void setSalt(String salt) {
		this.salt = salt;
	}

	public Integer getGender() {
		return gender;
	}

	public void setGender(Integer gender) {
		this.gender = gender;
	}

	public String getPhone() {
		return phone;
	}

	public void setPhone(String phone) {
		this.phone = phone;
	}

	public String getEmail() {
		return email;
	}

	public void setEmail(String email) {
		this.email = email;
	}

	public String getAvatar() {
		return avatar;
	}

	public void setAvatar(String avatar) {
		this.avatar = avatar;
	}

	public Integer getIsDelete() {
		return isDelete;
	}

	public void setIsDelete(Integer isDelete) {
		this.isDelete = isDelete;
	}

	@Override
	public String toString() {
		return "User [id=" + id + ", username=" + username + ", password=" + password + ", salt=" + salt + ", gender="
				+ gender + ", phone=" + phone + ", email=" + email + ", avatar=" + avatar + ", isDelete=" + isDelete
				+ "]";
	}

}

持久层接口

创建cn.tedu.store.mapper.UserMapper接口,并在接口中添加抽象方法:

package cn.tedu.store.mapper;

import cn.tedu.store.entity.User;

/**
 * 处理用户数据的持久层
 */
public interface UserMapper {

	/**
	 * 插入用户数据
	 * @param user 用户数据
	 * @return 受影响的行数
	 */
	Integer addnew(User user);
	
	/**
	 * 根据用户名查询用户数据
	 * @param username 用户名
	 * @return 匹配的用户数据,如果没有匹配的数据,则返回null
	 */
	User findByUsername(String username);
	
}

由于注册时需要判断用户名是否被占用,所以,添加了根据用户名查询用户数据的方法。

为了使得MyBatis能找到接口,可以在启动类之前添加@MapperScan注解:

@SpringBootApplication
@MapperScan("cn.tedu.store.mapper")
public class TeduStoreApplication {

	public static void main(String[] args) {
		SpringApplication.run(TeduStoreApplication.class, args);
	}

}

当然,在接口文件之前添加@Mapper注解也可以,相比之下,使用@MapperScan是一次性配置,如果不使用它的话,每个接口文件之前都必须添加@Mapper注解,这2种方案选取其中的任何一种即可。

接下来,需要配置接口文件的映射,首先,在application.properties中添加XML文件位置的配置:

# mybatis
mybatis.mapper-locations=classpath:mappers/*.xml

则需要在resources下创建名为mappers的文件夹,然后,在该文件夹下添加映射的XML,配置以上抽象方法的映射:

<?xml version="1.0" encoding="UTF-8" ?>  
<!DOCTYPE mapper PUBLIC "-//ibatis.apache.org//DTD Mapper 3.0//EN"      
	"http://ibatis.apache.org/dtd/ibatis-3-mapper.dtd">

<mapper namespace="cn.tedu.store.mapper.UserMapper">

	<!-- 插入用户数据 -->
	<!-- Integer addnew(User user) -->
	<insert id="addnew"
		parameterType="cn.tedu.store.entity.User"
		useGeneratedKeys="true"
		keyProperty="id">
		INSERT INTO t_user (
			username, password,
			salt, gender,
			phone, email,
			avatar, is_delete,
			created_user, created_time,
			modified_user, modified_time
		) VALUES (
			#{username}, #{password},
			#{salt}, #{gender},
			#{phone}, #{email},
			#{avatar}, #{isDelete},
			#{createdUser}, #{createdTime},
			#{modifiedUser}, #{modifiedTime}
		)
	</insert>
	
	<!-- 根据用户名查询用户数据 -->
	<!-- 应用于:注册,登录 -->
	<!-- User findByUsername(String username) -->
	<select id="findByUsername"
		resultType="cn.tedu.store.entity.User">
		SELECT 
			id, username, password,
			salt, avatar, 
			is_delete isDelete
		FROM 
			t_user
		WHERE 
			username=#{username}
	</select>

</mapper>

当以上内容全部完成后,应该执行单元测试:

package cn.tedu.store.mapper;

import java.util.Date;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import cn.tedu.store.entity.User;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserMapperTestCase {

	@Autowired
	private UserMapper userMapper;
	
	@Test
	public void addnew() {
		Date now = new Date();
		User user = new User();
		user.setUsername("root");
		user.setPassword("1234");
		user.setGender(1);
		user.setPhone("13800138001");
		user.setEmail("root@tedu.cn");
		user.setSalt("Hello,MD5!");
		user.setIsDelete(0);
		user.setCreatedUser("Admin");
		user.setModifiedUser("Admin");
		user.setCreatedTime(now);
		user.setModifiedTime(now);
		Integer rows = userMapper.addnew(user);
		System.err.println("rows=" + rows);
	}
	
	@Test
	public void findByUsername() {
		String username = "root";
		User user = userMapper.findByUsername(username);
		System.err.println(user);
	}
	
}

3. 用户-注册-业务层

业务层应该有业务层接口与实现类,允许被外部(控制器或其它业务类)调用的方法应该在接口中声明,例如reg()login()方法等。

当前完成的是“注册”功能,则先创建业务层接口cn.tedu.store.service.IUserService

/**
 * 处理用户数据的业务层接口
 */
public interface IUserService {

}

已知注册过程中,可能出现“用户名被占用异常”、“插入数据异常”,这2种都是业务异常,则应该在cn.tedu.store.service.exception先创建对应的异常类:

ServiceException
	DuplicateKeyException
	InsertException

然后,在接口中声明“注册”的抽象方法:

/**
 * 用户注册
 * @param user 用户的注册信息
 * @return 成功注册的用户数据
 * @throws DuplicateKeyException
 * @throws InsertException
 */
User reg(User user) 
	throws DuplicateKeyException, 
		InsertException;

如果事先并不清楚可能抛出哪些异常,甚至还没有创建对应的异常类,可以直接编写业务方法的代码,后续再补充抛出异常的声明。

然后,创建cn.tedu.store.service.impl.UserServiceImpl类,并实现IUserService接口:

package cn.tedu.store.service.impl;

import cn.tedu.store.entity.User;
import cn.tedu.store.service.IUserService;
import cn.tedu.store.service.exception.DuplicateKeyException;
import cn.tedu.store.service.exception.InsertException;

public class UserServiceImpl 
	implements IUserService {

	@Override
	public User reg(User user) throws DuplicateKeyException, InsertException {
		// TODO Auto-generated method stub
		return null;
	}

}

关于业务层的实现类,创建后,固定的任务有:

  • 添加@Service注解;

  • 声明@Autowired private UserMapper userMapper;持久层对象;

然后,添加与持久层对应的方法(甚至方法的声明几乎相同,如果是增删改方法,在业务层中,返回值可以调整为void,并通过异常表示操作失败),并声明为私有,通过调用持久层对象来完成方法:

/**
 * 插入用户数据
 * @param user 用户数据
 * @return 受影响的行数
 * @throws InsertException
 */
private void addnew(User user) {
	Integer rows = userMapper.addnew(user);
	if (rows != 1) {
		throw new InsertException(
			"增加用户数据时出现未知错误!");
	}
}

/**
 * 根据用户名查询用户数据
 * @param username 用户名
 * @return 匹配的用户数据,如果没有匹配的数据,则返回null
 */
private User findByUsername(String username) {
	return userMapper.findByUsername(username);
}

然后,在公有的reg()方法中编写思路:

@Override
public User reg(User user) throws DuplicateKeyException, InsertException {
	// 根据尝试注册的用户名查询用户数据
	// 判断查询到的数据是否为null
	// 是:用户名不存在,允许注册,则执行注册
	// -- 返回注册的用户对象
	// 否:用户名已被占用,抛出DuplicateKeyException异常
	return null;
}

然后,完成代码的编写:

@Override
public User reg(User user) throws DuplicateKeyException, InsertException {
	// 根据尝试注册的用户名查询用户数据
	User data = findByUsername(
			user.getUsername());
	// 判断查询到的数据是否为null
	if (data == null) {
		// 是:用户名不存在,允许注册,则执行注册
		addnew(user);
		// 返回注册的用户对象
		return user;
	} else {
		// 否:用户名已被占用,抛出DuplicateKeyException异常
		throw new DuplicateKeyException(
			"注册失败!尝试注册的用户名(" + user.getUsername() + ")已经被占用!");
	}
}

虽然还有“加密”及其它细节没有处理,但是,可以先进行测试,所以,创建cn.tedu.store.service.UserServiceTestCase

package cn.tedu.store.service;

import java.util.Date;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import cn.tedu.store.entity.User;
import cn.tedu.store.service.exception.ServiceException;

@RunWith(SpringRunner.class)
@SpringBootTest
public class UserServiceTestCase {

	@Autowired
	private IUserService userService;

	@Test
	public void reg() {
		try {
			Date now = new Date();
			User user = new User();
			user.setUsername("admin");
			user.setPassword("1234");
			user.setGender(1);
			user.setPhone("13800138002");
			user.setEmail("admin@tedu.cn");
			user.setSalt("Hello,MD5!");
			user.setIsDelete(0);
			user.setCreatedUser("Admin");
			user.setModifiedUser("Admin");
			user.setCreatedTime(now);
			user.setModifiedTime(now);
			User result = userService.reg(user);
			System.err.println("result=" + result);
		} catch (ServiceException e) {
			System.err.println("错误类型:" + e.getClass().getName());
			System.err.println("错误描述:" + e.getMessage());
		}
	}

}

接下来,需要完成密码加密的功能,本次加密使用了随机的盐,可以使用UUID作为随机盐。

UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和许多可能的数字。

在Java中,通过java.util.UUID类即可轻松获取到随机的UUID值:

String uuid = UUID.randomUUID().toString();

UserServiceImpl中添加加密方法:

/**
 * 获取根据MD5加密的密码
 * @param srcPassword 原密码
 * @param salt 盐值
 * @return 加密后的密码
 */
private String getMd5Password(
		String srcPassword, String salt) {
	// 【注意】以下加密规则是自由设计的
	// ----------------------------
	// 盐值 拼接 原密码 拼接 盐值
	String str = salt + srcPassword + salt;
	// 循环执行10次摘要运算
	for (int i = 0; i < 10; i++) {
		str = DigestUtils
			.md5DigestAsHex(str.getBytes());
	}
	// 返回摘要结果
	return str;
}

然后,在reg()方法中,在执行注册之前,先获取随机盐值,获取原始密码,执行加密,然后把盐值、加密后的密码封装回User对象中,再执行注册:

// 是:用户名不存在,允许注册,则处理密码加密
// 加密-1:获取随机的UUID作为盐值
String salt = UUID.randomUUID().toString();
// 加密-2:获取用户提交的原始密码
String srcPassword = user.getPassword();
// 加密-3:基于原始密码和盐值执行加密,获取通过MD5加密的密码
String md5Password = getMd5Password(srcPassword, salt);
// 加密-4:将加密后的密码封装在user对象中
user.setPassword(md5Password);
// 加密-5:将盐值封装在user对象中
user.setSalt(salt);
// 执行注册
addnew(user);

然后,在执行注册之前,还应该补充非用户提交的数据:

// 是否已经删除:否
user.setIsDelete(0); 
// 4项日志
Date now = new Date();
user.setCreatedUser(user.getUsername());
user.setCreatedTime(now);
user.setModifiedUser(user.getUsername());
user.setModifiedTime(now);

至此,用户注册功能已经完成,由于前序产生的数据可能存在密码没有加密等问题,后续将无法正常使用,所以,应该清除t_user表中所有数据,后续需要数据时,重新注册即可。

4. 用户-注册-控制器层

控制器类向客户端响应JSON格式的数据,需要自定义数据类型cn.tedu.store.util.ResponseResult

public class ResponseResult<T> {

	private Integer state;
	private String message;
	private T data;

	// SET/GET,Serializable
	// 无参数构造方法
	// 构造方法(int state):表示操作成功时
	// 构造方法(int state, String message):表示操作失败时
	// 构造方法(int state, Exception e):表示操作失败时
	// 构造方法(int state, T data):表示用户请求数据成功时
}

在本案例中第1次处理控制器层,则应该先创建cn.tedu.store.controller.BaseController,它将是当前案例中所有控制器类的基类!主要用于:统一处理异常;提供某些公共的资源……

然后,在BaseController中添加对已知异常的处理:

@ExceptionHandler(ServiceException.class)
@ResponseBody
public ResponseResult<Void> handleException(
		Exception e) {
	if (e instanceof DuplicateKeyException) {
		// 400-违反了Unique约束的异常
		return new ResponseResult<>(400, e);
	} else if (e instanceof InsertException) {
		// 500-插入数据异常
		return new ResponseResult<>(500, e);
	}
	
	return null;
}

然后添加正确响应时的代号的常量,便于统一调用并增强代码的可读性:

public static final Integer SUCCESS = 200;

接下来,应该创建cn.tedu.store.controller.UserController控制器类,继承自BaseController,添加@RestController@RequestMapping("/user")注解:

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

}

所有的控制器类本身都不实现功能,而是通过Service组件来实现,所以,还应该声明:

@Autowired
private IUserService userService;

接下来,设计“处理注册数据”的请求:

请求路径:/user/reg.do
请求方式:POST
请求参数:User
响应数据(成功时):无

然后在UserController中添加处理请求的方法:

@GetMapping("/reg.do")
public ResponseResult<Void> 
		handleReg(User user) {
}

完成后,通过http://localhost:8080/user/reg.do?username=spring&password=1234在浏览器中测试,成功后,将以上注解修改为@PostMapping

5. 用户-注册-界面

将静态页面压缩包中的5个文件夹复制到项目的src/main/resources下的static文件夹中。

启动项目,通过http://localhost:8080/web/register.html可以打开注册页面。

首先,为<form>添加id="form-reg",并为表单按钮添加id="btn-reg",由于后续将通过$("#表单id").serialize()序列化将提交的参数,所以,还应该为用户名和密码的输入框分别配置name属性的值为usernamepassword

然后,编写AJAX相关代码:

<script type="text/javascript">
// 为登录按钮绑定单击事件
$("#btn-reg").click(function() {
	// 将请求提交到哪里
	// 当前位置:web/register.html
	// 目标位置:user/reg.do
	var url = "../user/reg.do";
	// 请求参数
	var data = $("#form-reg").serialize();
	console.log("注册参数:" + data);
	// 发出ajax请求,并处理结果
	$.ajax({
		"url": url,
		"data": data,
		"type": "POST",
		"dataType": "json",
		"success": function(json) {
			if (json.state == 200) {
				alert("注册成功!");
			} else {
				alert(json.message);
			}
		}
	});
});
</script>

6. 用户-登录-持久层

当“注册”功能完成后,应该开发“登录”功能,且开发顺序:持久层 > 业务层 > 控制器层 > 界面。

由于登录时,持久层所需的功能“根据用户名查询用户数据”已经开发完成,所以,无需再开发持久层功能。

7. 用户-登录-业务层

业务层的设计步骤:

  1. 思考是否可能抛出新的异常,如果是,则创建异常类;

  2. 在业务层接口声明新的抽象方法;

  3. 在业务层实现中,先完成私有的、与持久层对应的方法,然后完成业务层接口中定义的方法。

在“登录”功能中,将会抛出新的异常:

  • 用户名不存在:UserNotFoundException

  • 密码错误:PasswordNotMatchException

接下来,在业务层接口声明新的抽象方法,抽象方法的返回值是只考虑操作成功的,方法名称与参数应该通俗易懂、简单易用:

User login(String username, String password) 
	throws UserNotFoundException, 
		PasswordNotMatchException;

然后,在UserServiceImpl实现类中,首先,由于持久层没有开发新的功能,所以不必添加新的私有方法,而是直接重写接口中新声明的抽象方法:

public User login(
	String username, String password) 
		throws UserNotFoundException, 
			PasswordNotMatchException {
	// 根据参数username查询用户数据
	// 判断用户数据是否为null
	// 是:为null,用户名不存在,则抛出UserNotFoundException
	// 否:非null,根据用户名找到了数据,取出盐值
	// 	对参数password执行加密
	// 	判断密码是否匹配
	//	是:匹配,密码正确,则判断是否被删除
	//		是:已被删除,则抛出UserNotFoundException或自定义“用户被删除异常”
	//		否:没被删除,则登录成功,将第1步查询的用户数据中的盐值和密码设置为null
	//		返回用户数据
	//	否:不匹配,密码错误,则抛出PasswordNotMatchException
}

完成后,在src/test/java下执行单元测试:

@Test
public void login() {
	try {
		String username = "spring";
		String password= "1234";
		User result = userService.login(username, password);
		System.err.println("result=" + result);
	} catch (ServiceException e) {
		System.err.println("错误类型:" + e.getClass().getName());
		System.err.println("错误描述:" + e.getMessage());
	}
}

8. 用户-登录-控制器层

开发控制器的基本步骤:

  1. 检查是否抛出了新的异常,如果有,在BaseController中进行处理;

  2. 设计处理请求的方法,即4个问题

  3. 添加处理请求的方法,完成后,测试。

所以,首先在BaseController中添加对UserNotFoundException(401)PasswordNotMatchException(402)的处理!

然后,设计请求:

请求路径:/user/login.do
请求类型:POST
请求参数:username(*), password(*)
响应数据:无

然后,在UserController中添加处理请求的方法:

@GetMapping("/login.do")
public ResponseResult<Void> handleLogin(
	@RequestParam("username") String username,
	@RequestParam("password") String password) {
	
}

完成后,通过http://localhost:8080/user/login.do?username=springboot&password=123456在浏览器中进行测试。测试无误后,将注解修改为@PostMapping

通常,登录成功后,会将当前用户的某些关键数据和常用数据存储在Session中,便于后续功能的处理,例如:根据id查询当前用户的相关信息、当修改数据或产生其它数据时根据username记录相关日志。为了记录Session数据,需要在处理登录请求的方法中添加HttpSession参数,并登录时获取返回值,然后将相关信息存在Session中:

@PostMapping("/login.do")
public ResponseResult<Void> handleLogin(
	@RequestParam("username") String username,
	@RequestParam("password") String password,
	HttpSession session) {
	// 执行登录
	User user
		= userService.login(username, password);
	// 将相关信息存入到Session
	session.setAttribute("uid", user.getId());
	session.setAttribute("username", user.getUsername());
	// 返回
	return new ResponseResult<>(SUCCESS);
}

9. 用户-修改密码-持久层

关于持久层的设计步骤:

  1. 设计所需执行的SQL语句;

  2. 在接口中声明抽象方法;

  3. 配置XML中的映射。

首先,关于SQL语句:

UPDATE 
	t_user
SET 
	password=?, 
	modified_user=?, modified_time=?
WHERE 
	id=?

SELECT 
	password, salt 
FROM 
	t_user 
WHERE 
	id=?

然后,设计以上2个功能的抽象方法:

Integer updatePassword(
	@Param("uid") Integer uid,
	@Param("password") String password,
	@Param("modifiedUser") String modifiedUser,
	@Param("modifiedTime") Date modifiedTime
);

User findById(Integer id);

然后,配置以上2个抽象方法的映射。

全部完成后,执行单元测试。

10. 用户-修改密码-业务层

业务层的设计步骤:

  1. 思考是否可能抛出新的异常,如果是,则创建异常类;

  2. 在业务层接口声明新的抽象方法;

  3. 在业务层实现中,先完成私有的、与持久层对应的方法,然后完成业务层接口中定义的方法。

首先,本次修改密码可能抛出的异常有:UserNotFoundExceptionPasswordNotMatchExceptionUpdateException,其中,UpdateException是新异常,应该先创建该异常类。

修改前序的findById()对应的SQL语句,添加查询usernameis_delete字段的值。

然后,在业务层的实现类添加2个私有方法,对应持久层的新创建的方法:

private void updatePassword(
	Integer uid, String password, 
	String modifiedUser, Date modifiedTime) {
	Integer rows 
		= userMapper.updatePassword(...);
	if (rows != 1) {
		throw new UpdateException("...");
	}
}

private User findById(Integer id) {
	return userMapper.findById(id);
}

接下来,在业务层接口中声明抽象方法:

void changePassword(
	Integer uid, 
	String oldPassword, 
	String newPassword) throws UserNotFoundException, PasswordNotMatchException, UpdateException;

然后,实现以上方法:

public void changePassword(
	Integer uid, 
	String oldPassword, 
	String newPassword) throws UserNotFoundException, PasswordNotMatchException, UpdateException {
	// 根据uid查询用户数据
	// 判断查询结果是否为null
	// 是:抛出异常:UserNotFoundException

	// 判断查询结果中的isDelete是否为1
	// 是:抛出异常:UserNotFoundException

	// 取出查询结果中的盐值
	// 对参数oldPassword执行MD5加密
	// 将加密结果与查询结果中的password对比是否匹配
	// 是:原密码正确,对参数newPassword执行MD5加密
	// 		获取当前时间
	// 		更新密码
	// 否:原密码错误,抛出异常:PasswordNotMatchException
}

完成后,执行单元测试,注意:不要使用密码未加密的数据进行测试。

11. 用户-修改密码-控制器层

开发控制器的基本步骤:

  1. 检查是否抛出了新的异常,如果有,在BaseController中进行处理;

  2. 设计处理请求的方法,即4个问题

  3. 添加处理请求的方法,完成后,测试。

首先,应该在BaseController中处理UpdateException

然后,设计处理“修改密码”的请求:

请求路径:/user/password.do
请求类型:GET > POST
请求参数:old_password(*), new_password(*), HttpSession
响应数据:无

关于请求参数,可以参考Service中需要执行的方法的参数。

关于响应数据,也可以参考Service中需要执行的方法的返回值。

接下来,在UserController中添加处理请求的方法:

@GetMapping("/password.do")
public ResponseResult<Void> changePassword(
	@RequestParam("old_password") String oldPassword,
	@RequestParam("new_password") String newPassword,
	HttpSession session) {

}

完成后,打开浏览器,先登录,然后,通过http://localhost:8080/user/password.do?old_password=1111&new_password=2222进行测试,无误后,将映射注解改为@PostMapping

在以上完成的代码中,获取uid的方式是Integer uid = Integer.valueOf(session.getAttribute("uid").toString());,后续,需要获取uid的应用场景还很多,以上语法相对比较长,可以在BaseController中封装方法将其简化:

protected Integer getUidFromSession(HttpSession session) {
	return Integer.valueOf(
			session.getAttribute("uid").toString());
}

此次的“修改密码”功能必须是基于“已登录”的,后续类似的功能还有许多,可以通过拦截器统一处理,即拦截下未登录的请求。

关于拦截器的注册

@Configuration
public class WebAppConfigurer 
	implements WebMvcConfigurer {

	@Override
	public void addInterceptors(
			InterceptorRegistry registry) {
		// 黑名单
		List<String> pathPatterns
			= new ArrayList<>();
		pathPatterns.add("/user/**");
		// 白名单
		List<String> excludePathPatterns
			= new ArrayList<>();
		excludePathPatterns.add("/user/reg.do");
		excludePathPatterns.add("/user/login.do");
		// 注册
		registry
			.addInterceptor(new LoginInterceptor())
			.addPathPatterns(pathPatterns)
			.excludePathPatterns(excludePathPatterns);
	}

}

12. 用户-修改密码-前端界面

register.htmllogin.html中复制AJAX请求相关代码,修改关键的id及HTML代码部分的表单标签的name属性即可。

13. 用户-修改资料-持久层

1. 设计SQL

执行修改资料(通常id和username不允许修改,password和avatar是单独的功能):

UPDATE 
	t_user
SET
	gender=?,
	phone=?,
	email=?,
	modified_user=?,
	modified_time=?
WHERE 
	id=?

由于执行修改个人资料之前,界面中还应该显示了当前用户的原资料,所以,还需要查询当前用户资料的功能,通常是根据id来查询,则,可以调整原有的findById()方法的映射,使之查询更多的字段,以符合本次功能的需求:

<!-- 根据用户id查询用户数据 -->
<!-- User findById(Integer id) -->
<select id="findById"
	resultType="cn.tedu.store.entity.User">
	SELECT 
		username,
		password, salt,
		is_delete AS isDelete
	FROM 
		t_user
	WHERE 
		id=#{id}
</select>

调整为:

<!-- 根据用户id查询用户数据 -->
<!-- User findById(Integer id) -->
<select id="findById"
	resultType="cn.tedu.store.entity.User">
	SELECT 
		username,
		gender, phone, email,
		password, salt,
		is_delete AS isDelete
	FROM 
		t_user
	WHERE 
		id=#{id}
</select>

2. 抽象方法

由于只新增了1个SQL-Update功能,所以,只需要增加1个抽象方法:

Integer updateInfo(User user);

3. 配置XML映射

<update id="updateInfo">
	UPDATE 
		t_user
	SET
		<if test="gender != null">
		gender=#{gender},
		</if>

		<if test="phone != null">
		phone=#{phone},
		</if>

		<if test="email != null">
		email=#{email},
		</if>

		modified_user=#{modifiedUser},
		modified_time=#{modifiedTime}
	WHERE 
		id=#{id}
</update>

14. 用户-修改资料-业务层

1. 新异常

修改资料大致流程是先查询,再更新数据,可能涉及的异常是“用户数据不存在”或“更新时的未知错误”,这些异常已经定义,则无须创建新异常。

2. 私有方法

相比持久层的方法,需要将返回值类型修改为void

private void updateInfo(User user) {
	// 执行更新,获取返回值
	// 判断返回值,出错时抛出“更新时的未知错误”
}

3. 抽象方法及实现

IUserService中声明抽象方法:

void changeInfo(User user)
	throws UserNotFoundException, 
		UpdateException;

然后,在实现类中:

public void changeInfo(User user)
	throws UserNotFoundException, 
		UpdateException {
	// 根据user.getId()查询用户数据
	// 判断数据是否为null
	// 是:抛出:UserNotFoundException

	// 判断is_delete是否为1
	// 是:抛出:UserNotFoundException

	// 向参数对象中封装:
	// - modified_user > data.getUsername()
	// - modified_time > new Date()
	// 执行修改:gender,phone,email,modified_user,modified_time
}

完成后执行单元测试。

由于后续的功能需要显示当前登录的用户数据,所以,业务层必须提供“根据id获取用户数据”的功能!

虽然在业务层的实现类UserServiceImpl中已存在私有的方法:

private User findById(Integer id) ...

但是,该方法已经应用于“修改密码”和“修改资料”功能,后续还可能应用到更多的功能,不适合随便调整。而本次查询时,由于数据最终需要返回到客户端,所以,返回的数据中不适合包括密码、盐值、isDelete等。

所以,在业务层接口IUserService中添加新的方法,表示可以被调用的、响应用户数据的方法:

User getById(Integer id);

然后,在实现该方法,依然通过原有的findById()方法来获取对应的数据,但是,在返回之前,将返回对象中的关键数据(不希望响应给客户端的数据)字段设置为null即可:

@Override
public User getById(Integer id) {
	User data = findById(id);
	data.setPassword(null);
	data.setSalt(null);
	data.setIsDelete(null);
	return data;
}

15. 用户-修改资料-控制器层

1. 处理新异常

无。

2. 设计所处理的请求

关于“修改资料”,涉及2个请求!

第1个请求:查询当前登录的用户的数据,以显示到界面中:

请求路径:/user/info.do
请求类型:GET/POST
请求参数:无,HttpSession
响应数据:ResponseResult<User>
是否拦截:是,登录拦截,无需修改配置

所以,对应的方法应该是:

@RequestMapping("/info.do")
public ResponseResult<User> getInfo(
	HttpSession session) {

}

第2个请求:执行修改当前用户的资料

请求路径:/user/change_info.do
请求类型:POST
请求参数:User, HttpSession
响应数据:ResponseResult<Void>
是否拦截:是,登录拦截,无需修改配置

所以,对应的方法应该是:

@GetMapping("/change_info.do")
public ResponseResult<Void> changeInfo(
	User user, HttpSession session) {

}

打开浏览器,先登录,然后通过以下URL进行测试:

http://localhost:8080/user/info.do

http://localhost:8080/user/change_info.do?gender=1&phone=13900139666&email=fancq@tedu.cn

测试完成后,将change_info.do限制为POST类型的请求。

16. 用户-修改资料-前端界面

1. 界面默认显示当前已登录的用户的信息

核心:当界面刚刚加载时,即发出请求,获取当前登录的用户的数据,且,获取到数据后,将数据显示到各控件中。

界面刚刚加载时:$(document).ready()即表示界面完成初始化,即前端的HTML、CSS、JS已经解析完毕,此时,就可以发出请求

$(document).ready(function() {
	$.ajax();
})

将数据显示到各控件中:为了方便操作,可以为各控件配置 id,则可以通过jQuery的ID选择器迅速选择到相关控件,然后,调用val(值)函数为控件设计值,例如:

$("#username").val("Mike");

由于“性别”涉及到2个控件,不可以使用相同的id值,所以,需要分别为2个控件设计不同的id,例如gender-malegender-female,“性别”使用的是<input type="radio" />标签,这种控件的选中是依赖于checked="checked"属性,所以,当需要通过JavaScript代码控制选中时,就是为其中的某个控件设置该属性即可,即:当需要选中“男”时,通过jQuery选择器选中到“男”对应的单选按钮,并将其checked属性的值设置为"checked"即可。

$("#gender-male").attr("checked", "checked");

2. 执行修改

// 为修改按钮绑定单击事件
$("#btn-change").click(function() {
	// 将请求提交到哪里
	var url = "../user/change_info.do";
	// 请求参数
	var data = $("#form-change-info").serialize();
	console.log("修改个人资料参数:" + data);
	// 发出ajax请求,并处理结果
	$.ajax({
		"url": url,
		"data": data,
		"type": "POST",
		"dataType": "json",
		"success": function(json) {
			if (json.state == 200) {
				alert("修改成功!");
			} else {
				alert(json.message);
			}
		}
	});
});

3. 关于登录超时后的访问

假设用户已登录,但长时间未操作导致Session超时,然后,再点击界面的按钮执行“修改资料”或“修改密码”,界面的表现是没有任何变化!主要原因是:异步请求可以理解为子线程发出的请求,由于登录超时,服务器端的拦截器会要求重定向到登录页,而获取到重定向这个响应结果的是子线程,主线程完全不知晓这个过程,所以,界面上没有任何变化!

ajax()函数中,如果服务器端响应的不是正确的响应码,而是3xx、4xx、5xx,需要通过error进行配置,例如:

$.ajax({
	"url": ..,
	"data": ..,
	"type": ..,
	"success": function () {},
	"error": function() {
		处理错误
	}
});

由于4xx错误都是请求出错,例如请求的URL不存在、请求类型错误、请求参数不足等,而请求是开发者编写的,理论上来说,完整的项目中应该不会出现4xx错误。

而5xx是服务器的程序运行出错,是更不应该出现的!

所以,通常导致error的大多是重定向,则,处理方式:

"error": function(xhr, text, errorThrown) {
	alert("您的登录信息已经过期!请重新登录!");
	location.href = "login.html";
}

附:SpringMVC文件上传

1. 设计前端界面

文件上传的控件是:

<input type="file" />

并且,该控件必须存在于以POST方式提交的表单中,且必须配置enctype="multipart/form-data"

<form method="post"
	enctype="multipart/form-data">
	<input type="file" />
</form>
2. 添加依赖

SpringMVC的上传是基于commons-fileupload的,所以,需要添加依赖:

<dependency>
	<groupId>commons-fileupload</groupId>
	<artifactId>commons-fileupload</artifactId>
	<version>1.3.3</version>
</dependency>
3. 配置CommonsMultipartResolver

在Spring的配置文件中添加配置:

<!-- CommonsMultipartResolver -->
<bean id="multipartResolver"
	class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>

<!-- 注解驱动 -->
<mvc:annotation-driven />
4. 处理上传请求

创建cn.tedu.spring.controller.UploadController类,添加@Controller注解,然后,添加处理上传请求的方法:

@PostMapping("/upload.do")
@ResponseBody
public String handleUpload(
	@RequestParam("file") CommonsMultipartFile file) throws IllegalStateException, IOException {
	// 保存文件
	file.transferTo(dest);
	
	// 响应:响应方式与上传无关
	return "OK.";
}
5. CommonsMultipartFile

在处理请求时,参数CommonsMultipartFile对象就是上传的文件,常用方法有:

  • String getOriginalFileName():获取原文件名,即客户端上传时的原始文件名;

  • long size():获取文件大小,返回值是以字节为单位的;

  • String getContentType():获取文件类型,返回文件的MIME类型,例如image/jpeg,关于MIME类型的列表,可参考Tomcat下的conf\web.xml或上网查询;

  • boolean isEmpty():上传的文件是否为空,当没有选择上传的文件就提交,或选中的文件没有内容(0字节)时,该方法返回true,否则,返回false

除此以外,还有byte[] getBytes()可以获取所上传的文件的数据的数组,和InputStream getInputStream()可以获取输入流,这2个方法都是用于自定义输出过程(存储数据的过程)的,用于取代transferTo()方法。

InputStream in = file.getInputStream();
FileOutputStream fos = new ...

BufferedInputStream bis 
	= new BufferdInputStream(in);
BufferedOutputStream bos
	= new BufferedOutputStream(fos);

byte[] buffer = new byte[1024 * 8];
int len;
while(len = bis.read(buffer)) {
	bos.write(buffer,0,len);
}
6. CommonsMultipartResolver

在Spring的配置文件中,配置了CommonsMultipartResolver,也可以为它的某些属性注入值,例如:

  • long maxUploadSize:最大上传大小,以字节为单位,假设设置值为10M,如果一次性上传10个文件,则10个文件的总大小不得超过10M

  • long maxUploadSizePerFile:单个文件最大大小,以字节为单位,在表单控件中设置multiple="multiple"即可一次性上传多个文件,假设设置值为10M,如果一次性上传10个文件,则每个文件的大小不得超过10M,而所有文件的总大小可能超过10M

  • int maxInMemorySize:最大占用内存

  • String defaultEncoding:默认编码

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值