2019_07_03store 文件上传 第五天

21.6. 确定上传文件的名称

为了避免文件被覆盖,每个文件的路径或文件名应该不发生冲突,例如可以使用唯一的文件,要保证文件名唯一,可以考虑使用时间、随机数等数据作为文件名的一部分,也可以使用用户的唯一数据,例如用户的id、用户名等作为文件名的一部分,具体规则可自行决定。

关于文件的扩展名部分,可以通过MultipartFileString getOriginalFilename()方法获取原始文件名,即用户上传的文件在客户端时使用的文件名,然后对文件名进行分析处理,得到原始的扩展名,例如:

String originalFilename = file.getOriginalFilename();
String suffix = "";
int beginIndex = originalFilename.lastIndexOf(".");
if (beginIndex != -1) {
	suffix = originalFilename.substring(beginIndex);
}
21.7. 确定上传文件的文件夹

上传的文件必须在Tomcat部署项目的目录中,否则,上传的文件将无法通过http协议进行访问,可以HttpServletRequest对象获取在webapp下指定名称的文件夹的实际路径,然后创建出所需的文件夹,例如,先在处理请求的方法中添加HttpServletRequest作为参数,然后:

String parentDir = request.getServletContext().getRealPath("upload");
File parent = new File(parentDir);
if (!parent.exists()) {
	parent.mkdirs();
}
21.8. 关于MultipartFile中的常用方法
  • String getOriginalFilename():获取原始文件名,即用户上传的文件在客户端时使用的文件名;

  • boolean isEmpty():判断上传的文件是否为空,如果用户没有选择文件就提交了上传请求,或选择的文件是0字节的,则视为空,将返回true,否则返回false

  • long getSize():获取文件的大小,以字节为单位;

  • String getContentType():获取文件的MIME类型,例如返回image/jpeg,关于扩展名与MIME的对应关系,可以上网查阅资料,也可以在Tomcat的conf目录下的web.xml中查找,文件的扩展名不同,得到的MIME类型可能是不同的;

  • InputStream getInputStream():获取文件的输入字节流,用于需要自定义处理用户上传的数据的应用场景,例如上传的文件较大,在存储时需要自定义缓冲区等,该方法不可与transferTo()方法同时使用;

  • void transferTo(File dest):保存客户端上传的文件,该方法不可以与getInputStream()方法同时使用。

21.9. 关于CommonsMultipartResolver的配置

spring.xml中可以对CommonsMultipartResolver的属性注入值:

  • maxUploadSize:最大上传的数据量,以字节为单位,假设设置为10M,则单次请求的最大数据就是10M,可能这次上传过程中有2个文件,则2个文件的总和不允许超过10M;

  • maxUploadSizePerFile:上传的每个文件的最大大小,假设设置为10M,且这次上传过程中有2个文件,则每个文件都不可以超过10M,但是这次请求的数据总大小可能接近20M;

  • defaultEncoding:默认字符集,用于设置上传时同一个表单中可能提交的其它字符的编码。

以上设置请求数据大小的配置,检查的时间节点比较靠前,在控制器还没有处理请求时就会检查;这些设置并不能取代在控制器通过getSize()获取文件大小并进行的相关检查,在配置文件中的这些配置是项目的全局化配置,即同一个项目中,无论是上传头像,还是上传附件,还是上传商品图片等,所有的上传功能都必须符合这些配置,而控制器中的检查是单项功能的检查,毕竟每个控制器只处理某1种请求,所以,上传头像的方法里自定义头像的大小,而上传商品图片的方法里自定义商品图片的大小,是根据具体功能作出的限制。

21.10. 如何一次性上传多个文件

如果需要上传的文件的数量是固定的,且文件的定位是不同的,例如上传身份证的正面和反面照片,则在客户端可以使用多个<input type="file" />控件,且在服务器处理请求的方法中添加多个MultipartFile类型的参数即可。

另外,如果上传的文件的定位是相同的,且数量不固定,则在客户端使用控件,添加multiple="multiple属性,即:

<input type="file" name="file" multiple="multiple" />

这样的文件浏览控件在操作时,可以按住Ctrl键,是可以一次性选中多个文件的。

然后,在服务器端的控制器中,将参数声明为MultipartFile[]即可。

21.11. 存储方式

所有的上传,都应该将文件存储到服务器的硬盘中,另外,在数据库中记录下文件的存储路径,当需要使用文件时,可以查询数据库获取指定文件的路径,再对文件进行访问。

22. 用户-上传头像-持久层

(a) 规划SQL语句

执行上传头像的数据库操作部分,只是修改用户数据的avatar字段的值,需要执行的SQL语句大致是:

update t_user set avatar=?,modified_user=?,modified_time=? where uid=?

在执行修改之前,还应该检查用户数据是否存在、是否标记为已删除,对应的功能已经存在,无需再次开发。

(b) 接口与抽象方法

UserMapper.java接口中添加抽象方法:

Integer updateAvatar(
	@Param("uid") Integer uid, 
	@Param("avatar") String avatar, 
	@Param("modifiedUser") String modifiedUser, 
	@Param("modifiedTime") Date modifiedTime);

© 配置映射

配置映射:

<!-- 更新头像 -->
<!-- Integer updateAvatar(
	@Param("uid") Integer uid, 
	@Param("avatar") String avatar, 
	@Param("modifiedUser") String modifiedUser, 
	@Param("modifiedTime") Date modifiedTime) -->
<update id="updateAvatar">
	UPDATE
		t_user
	SET
		avatar=#{avatar},
		modified_user=#{modifiedUser},
		modified_time=#{modifiedTime}
	WHERE
		uid=#{uid}
</update>

单元测试:

@Test
public void updateAvatar() {
	Integer uid = 7;
	String avatar ="1234";
	String modifiedUser = "超级管理员";
	Date modifiedTime = new Date();
	Integer rows = mapper.updateAvatar(uid, avatar, modifiedUser, modifiedTime);
	System.err.println("rows=" + rows);
}

################### 以上大纲步骤 1 ##########################

1.UserMapper.java中添加抽象方法
2.UserMapper.xml配置文件中添加sql配置代码
3.用户映射测试
a.更新测试效果为:控制台显示:
row=1

23. 用户-上传头像-业务层

(a) 规划异常

本次执行的主要是更新数据的操作,则可能出现UpdateException

在执行更新之前,还需要根据uid查询用户数据,检查数据是否存在,检查数据的is_delete,都可能出现UserNotFoundException

此次操作无需创建新的异常类。

(b) 接口与抽象方法

IUserService中添加“修改头像”的抽象方法:

void changeAvatar(Integer uid, String avatar, String modifiedUser) 
throws UserNotFoundException, UpdateException;

© 实现抽象方法

UserServiceImpl实现类中添加新的抽象方法并实现:

public void changeAvatar(Integer uid, String avatar, String modifiedUser) 
throws UserNotFoundException, UpdateException {
	// 根据参数uid查询用户数据
	// 判断查询结果是否为null:UserNotFoundException

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

	// 执行更新,获取返回值(受影响的行数)
	// 判断受影响的行数是否不为1:UpdateException
}

具体实现为:

@Override
public void changeAvatar(Integer uid, String avatar, String modifiedUser)
		throws UserNotFoundException, UpdateException {
	// 根据参数uid查询用户数据
	User result = userMapper.findByUid(uid);
	// 判断查询结果是否为null:UserNotFoundException
	if (result == null) {
		// 是:抛出UserNotFoundException
		throw new UserNotFoundException(
			"修改头像失败!用户数据不存在!");
	}

	// 判断查询结果中的isDelete是否为1:UserNotFoundException
	if (result.getIsDelete() == 1) {
		// 是:抛出UserNotFoundException
		throw new UserNotFoundException(
			"修改头像失败!用户数据不存在!");
	}
	
	// 执行更新,获取返回值(受影响的行数)
	Integer rows = userMapper.updateAvatar(uid, avatar, modifiedUser, new Date());
	// 判断受影响的行数是否不为1:UpdateException
	if (rows != 1) {
		throw new UpdateException(
			"修改头像失败!更新数据时出现未知错误!");
	}
}

单元测试:

@Test
public void changeAvatar() {
	try {
		Integer uid = 7;
		String avatar = "88888888";
		String modifiedUser = "系统管理员";
		service.changeAvatar(uid, avatar, modifiedUser);
		System.err.println("OK");
	} catch (ServiceException e) {
		System.err.println(e.getClass().getName());
		System.err.println(e.getMessage());
	}
}

################### 以上大纲步骤 2 ##########################

1.IuserService.java中添加抽象方法
2.UserServiceImpl.java中实现重写抽象方法
3.测试
a.更新测试效果为:控制台显示:
ok
b.用持久层的查询用户查询,用户头像是否插入成功。

24. 用户-上传头像-控制器层

(a) 统一处理异常

在处理上传文件之前,应该对用户选择并提交的文件数据进行检查,例如文件是否为空、文件大小是否超出限制、文件类型是否超出限制,如果不符合标准,应该抛出对应的异常:

cn.tedu.store.controller.ex.FileUploadException
cn.tedu.store.controller.ex.FileEmptyException
cn.tedu.store.controller.ex.FileSizeException
cn.tedu.store.controller.ex.FileTypeException
cn.tedu.store.controller.ex.FileUploadIOException
cn.tedu.store.controller.ex.FileStateException

这些都继承自cn.tedu.store.controller.ex.FileUploadException,而该FileUploadException继承自RuntimeException

另外,在执行保存文件时,调用的transferTo()方法也是会抛出异常的:

try {
	file.transferTo(dest);
} catch (IllegalStateException e) {
	e.printStackTrace();
} catch (IOException e) {
	e.printStackTrace();
}

当捕获到以上这些异常时,可以在捕获后抛出自定义异常FileStateExceptionFileUploadIOException,也是和以上异常在同一个包中,也都继承自FileUploadException,然后,在BaseController中对自定义异常进行统一处理!

(b) 设计请求

设计“上传头像”的请求方式:

请求路径:/users/change_avatar
请求参数:MultipartFile file, HttpServletRequest request
请求方式:POST
响应数据:JsonResult<String>

© 处理请求

1.在UserController中添加处理请求的方法:

@GetMapping("get_by_uid")
public JsonResult<User> getByUid(HttpSession session) {
	// 执行获取数据
	Integer uid = getUidFromSession(session);
	User data = userService.getByUid(uid);
	// 返回成功与数据
	return new JsonResult<>(SUCCESS, data);
}

/**
 * 允许上传的头像文件的最大大小
 */
public static final long AVATAR_MAX_SIZE = 2 * 1024 * 1024;
/**
 * 允许上传的头像文件的类型列表
 */
public static final List<String> AVATAR_CONTENT_TYPES = new ArrayList<String>();

static {
	AVATAR_CONTENT_TYPES.add("image/jpeg");
	AVATAR_CONTENT_TYPES.add("image/png");
}

@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(
	@RequestParam("file") MultipartFile file, 
	HttpServletRequest request) {
	// 检查文件是否为空
	if (file.isEmpty()) {
		throw new FileEmptyException(
			"上传头像失败!请选择有效的图片文件!");
	}
	
	// 检查文件大小是否超出限制
	if (file.getSize() > AVATAR_MAX_SIZE) {
		throw new FileSizeException(
			"上传头像失败!不允许上传超过" + (AVATAR_MAX_SIZE / 1024) + "KB的图片文件!");
	}
	
	// 检查文件类型是否超出限制
	if (!AVATAR_CONTENT_TYPES.contains(file.getContentType())) {
		throw new FileSizeException(
			"上传头像失败!选择的文件类型超出了限制!\r\r允许使用的文件类型有:"
			 + AVATAR_CONTENT_TYPES);
	}
	
	// 确定文件夹
	String parentPath = request.getServletContext().getRealPath("upload");
	File parent = new File(parentPath);
	if (!parent.exists()) {
		parent.mkdirs();
	}
	
	// 确定文件名
	String filename = UUID.randomUUID().toString();
	String originalFilename = file.getOriginalFilename();
	int beginIndex = originalFilename.lastIndexOf(".");
	String suffix = originalFilename.substring(beginIndex);
	String child = filename + suffix;
	
	// 保存用户上传的文件
	File dest = new File(parent, child);
	try {
		file.transferTo(dest);
	} catch (IllegalStateException e) {
		throw new FileStateException(
			"上传文件失败!文件状态有误,请重新尝试!");
	} catch (IOException e) {
		throw new FileUploadIOException(
			"上传文件失败!发生读写错误,请重新尝试!");
	}
	
	// 将文件的路径记录到数据库
	String avatarPath = "/upload/" + child;
	Integer uid = getUidFromSession(request.getSession());
	String username = getUsernameFromSession(request.getSession());
	userService.changeAvatar(uid, avatarPath, username);
	
	// 响应结果
	return new JsonResult<>(SUCCESS, avatarPath);
}

2.启动类中加上以下配置

@SpringBootApplication
@Configuration
@MapperScan(“cn.tedu.store.mapper”)
public class StoreApplication {

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

@Bean
public MultipartConfigElement getMultipartConfigElement() {
	MultipartConfigFactory factory
		= new MultipartConfigFactory();
	
	DataSize maxFileSize = DataSize.ofMegabytes(500);
	factory.setMaxFileSize(maxFileSize);
	
	DataSize maxRequestSize = DataSize.ofMegabytes(500);
	factory.setMaxRequestSize(maxRequestSize);
	
	return factory.createMultipartConfig();
}

}

3.login页面中加入以下代码(alert(登录成功)下) :
if (obj.state == 2000) {
alert(“登录成功!”);
// 将头像存到Cookie
$.cookie(“avatar”, obj.data.avatar, {“expires”:7});
console.log(“avatar=” + $.cookie(“avatar”));
} else {
alert(obj.message);
}

后端代码 调整部分

1.添加一系列的检查
a.UserController.java类中:
/**
* 允许上传的头像文件的最大大小
/
public static final long AVATAR_MAX_SIZE = 2 * 1024 * 1024;
/
*
* 允许上传的头像文件的类型列表
*/
public static final List AVATAR_CONTENT_TYPES = new ArrayList();

static {
	AVATAR_CONTENT_TYPES.add("image/jpeg");
	AVATAR_CONTENT_TYPES.add("image/png");
}

@PostMapping("change_avatar")
public JsonResult<String> changeAvatar(
	@RequestParam("file") MultipartFile file, 
	HttpServletRequest request) {
	// 检查文件是否为空
	if (file.isEmpty()) {
		throw new FileEmptyException(
			"上传头像失败!请选择有效的图片文件!");
	}
	
	// 检查文件大小是否超出限制
	if (file.getSize() > AVATAR_MAX_SIZE) {
		throw new FileSizeException(
			"上传头像失败!不允许上传超过" + (AVATAR_MAX_SIZE / 1024) + "KB的图片文件!");
	}
	
	// 检查文件类型是否超出限制
	if (!AVATAR_CONTENT_TYPES.contains(file.getContentType())) {
		throw new FileSizeException(
			"上传头像失败!选择的文件类型超出了限制!\r\r允许使用的文件类型有:
			" + AVATAR_CONTENT_TYPES);
	}
	
	// 确定文件夹
	String parentPath = request.getServletContext().getRealPath("upload");
	File parent = new File(parentPath);
	if (!parent.exists()) {
		parent.mkdirs();
	}
	
	// 确定文件名
	String filename = UUID.randomUUID().toString();
	String originalFilename = file.getOriginalFilename();
	int beginIndex = originalFilename.lastIndexOf(".");
	String suffix = originalFilename.substring(beginIndex);
	String child = filename + suffix;
	
	// 保存用户上传的文件
	File dest = new File(parent, child);
	try {
		file.transferTo(dest);
	} catch (IllegalStateException e) {
		throw new FileStateException(
			"上传文件失败!文件状态有误,请重新尝试!");
	} catch (IOException e) {
		throw new FileUploadIOException(
			"上传文件失败!发生读写错误,请重新尝试!");
	}
	
	// 将文件的路径记录到数据库
	String avatarPath = "/upload/" + child;
	Integer uid = getUidFromSession(request.getSession());
	String username = getUsernameFromSession(request.getSession());
	userService.changeAvatar(uid, avatarPath, username);
	
	// 响应结果
	return new JsonResult<>(SUCCESS, avatarPath);
}

b.BaseController.java类中:

@ExceptionHandler({ServiceException.class,FileUploadException.class})
@ResponseBody
public JsonResult handleException(Throwable e) {
JsonResult jr = new JsonResult<>(e);
jr.setMessage(e.getMessage());

		if (e instanceof UsernameDuplicateException) {
			jr.setState(4000);
		} else if (e instanceof UserNotFoundException) {
			jr.setState(4001);
		} else if (e instanceof PasswordNotMatchException) {
			jr.setState(4002);
		} else if (e instanceof InsertException) {
			jr.setState(5000);
		} else if (e instanceof UpdateException) {
			jr.setState(5001);
		} else if (e instanceof FileEmptyException) {
			jr.setState(6000);
		} else if (e instanceof FileSizeException) {
			jr.setState(6001);
		} else if (e instanceof FileTypeException) {
			jr.setState(6002);
		} else if (e instanceof FileStateException) {
			jr.setState(6003);
		} else if (e instanceof FileUploadIOException) {
			jr.setState(6004);
		}
		return jr;
	}

25. 用户-上传头像-前端界面

  1. 当登录成功时,将用户的头像路径响应给客户端,且客户端将头像路径保存到Cookie中。首先,应该检查在UserMapper.xmlfindByUsername()查询的字段列表中是否包含avatar字段;然后,在登录的前端界面中,当响应结果表示登录成功时,通过$.cookie("avatar", obj.data.avatar, {"expires":7});将服务器端响应的头像路径保存到客户端的Cookie中。

  2. 打开上传头像页面时,通过Cookie中保存的头像路径,显示当前登录的用户的头像:

26. 用户-上传头像-自定义上传大小的限制

在SpringBoot项目中,所有的上传默认都不允许超过1M,否则就会报错!

先在启动类StoreApplication之前添加@Configuration注解,然后,在类中添加:

@Bean
public MultipartConfigElement getMultipartConfigElement() {
	MultipartConfigFactory factory
		= new MultipartConfigFactory();
	
	DataSize maxFileSize = DataSize.ofMegabytes(500);
	factory.setMaxFileSize(maxFileSize);
	
	DataSize maxRequestSize = DataSize.ofMegabytes(500);
	factory.setMaxRequestSize(maxRequestSize);
	
	return factory.createMultipartConfig();
}

27. 收货地址-分析

关于“收货地址”数据的管理,涉及的功能有:增加,修改,删除,设为默认,显示列表。

以上功能的开发顺序应该是:增加 > 显示列表 > 设为默认 > 删除 > 修改。

28. 收货地址-创建数据表

创建收货地址数据表:

CREATE TABLE t_address (
	aid INT AUTO_INCREMENT COMMENT '收货地址id',
	uid INT COMMENT '用户id',
	name VARCHAR(50) COMMENT '收货人姓名',
	province_code CHAR(6) COMMENT '省代号',
	province_name VARCHAR(50) COMMENT '省名称',
	city_code CHAR(6) COMMENT '市代号',
	city_name VARCHAR(50) COMMENT '市名称',
	area_code CHAR(6) COMMENT '区代号',
	area_name VARCHAR(50) COMMENT '区名称',
	zip CHAR(6) COMMENT '邮编',
	address VARCHAR(100) COMMENT '详细地址',
	phone VARCHAR(20) COMMENT '手机',
	tel VARCHAR(20) COMMENT '固话',
	tag VARCHAR(30) COMMENT '地址类型',
	is_default INT COMMENT '是否默认,0-非默认,1-默认',
	created_user VARCHAR(50) COMMENT '创建人',
	created_time DATETIME COMMENT '创建时间',
	modified_user VARCHAR(50) COMMENT '最后修改人',
	modified_time DATETIME COMMENT '最后修改时间',
	PRIMARY KEY (aid)
) DEFAULT CHARSET=UTF8;

29. 收货地址-创建实体类

30. 收货地址-增加-持久层

(a) 规划SQL语句

(b) 接口与抽象方法

© 配置映射

31. 收货地址-增加-业务层

(a) 规划异常

(b) 接口与抽象方法

© 实现抽象方法

32. 收货地址-增加-控制器层

(a) 统一处理异常

(b) 设计请求

设计“xxx”的请求方式:

请求路径:/users/reg
请求参数:User user
请求方式:POST
响应数据:JsonResult<Void>

© 处理请求

33. 收货地址-增加-前端界面

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

程序员西柚柚

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

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

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

打赏作者

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

抵扣说明:

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

余额充值