商城项目(中)

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

上传功能是有个2部分的:把客户端提交的文件保存到服务器端的某个位置,把存储的文件的路径存储到数据库中。

对于持久层开发而言,只需要关注后者,即把文件的路径存储到数据库中,本质上,是一种Update操作。

所以,关于上传头像,会涉及的SQL语句大致是:

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

则抽象方法:

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

完成后,执行单元测试,即使是很简单的开发也应该有相应的测试

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

新异常

(无)

接口中的抽象方法

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

实现抽象方法

首先,先创建与持久层对应的私有方法:

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

然后,实现接口中定义的抽象方法:

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

	// 判断isDelete==1
	// 是:UserNotFoundException

	// 执行更新头像
}

完成后,执行单元测试。

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

处理异常

设计处理请求

请求路径:/user/upload.do
请求类型:POST
请求参数:HttpSession, CommonsMultipartFile
响应数据:ResponseResult<String>
是否拦截:是,无须修改配置

处理请求

private static final String UPLOAD_DIR_NAME = "upload";

@PostMapping("/upload.do")
public ResponseResult<String> handleUpload(
	HttpSession session,
	@RequestParam("file") MultipartFile file) {
	// 检查是否存在上传文件 > file.isEmpty()

	// 检查文件大小 > file.getSize()

	// 检查文件类型 > file.getContentType()

	// 确定上传文件夹的路径 > session.getServletContext.getRealPath(UPLOAD_DIR_NAME) > exists() > mkdirs()

	// 确定文件名 > getOriginalFileName() 

	// 执行保存文件

	// 获取当前用户的id
	// 更新头像数据

	// 返回
}

在SpringBoot项目中,上传的文件的实现类不再是CommonsMultipartFile,所以,在控制器中声明参数时,必须是父级的接口类型MultipartFile。

完成后,因为本次功能需要上传头像,无法在URL中直接测试,所以,暂不测试。

以上判断出错时,可抛出对应的异常:

RuntimeException
	RequestException
		FileUploadException
			FileEmptyException
			FileSizeOutOfLimitException
			FileTypeNotSupportException

BaseController中,处理异常时,先调整处理的异常的范围:

@ExceptionHandler({ServiceException.class,
		RequestException.class})

然后,继续添加判断语句,以处理新的异常。

附:使用Cookie

在当前项目中,如果需要使用Cookie,首先,在对应的HTML页面中需要添加:

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

关于Cookie的访问,将数据存入到Cookie的语法例如:

$.cookie("username", vusername, { 
	expires: 7
});

而获取Cookie中的数据的语法例如:

var username = $.cookie("username");

示例:用户上传头像后,每次登录后进入上传头像页面,默认显示此前上传的头像!

第1步:当用户登录时,查询用户信息时必须查询头像字段!

第2步:当用户登录成功后,必须向客户端响应用户的头像数据!

@PostMapping("/login.do")
public ResponseResult<User> 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, user);
}

第3步:当用户登录成功后,应该在Cookie中写入头像的数据,则:

"success": function(json) {
	if (json.state == 200) {
		alert("登录成功!");
		// 将头像路径存到Cookie
		$.cookie("avatar", json.data.avatar, {
			expires: 7
		});
		console.log("登录成功,将头像路径存到Cookie:" 
			+ $.cookie("avatar"));
	} else {
		alert(json.message);
	}
}

第4步:在上传头像页面中,先引入外部的jquery.cookie.js文件,然后,当页面完成初始化时,如果Cookie中有头像,则显示:

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

$(document).ready(function() {
	if ($.cookie("avatar") != null) {
		$("#img-avatar").attr("src", $.cookie("avatar"));
	}
});

第5步:如果重新上传头像,需要更新Cookie中存入的图片路径:

"success": function(json) {
	if (json.state == 200) {
		alert("修改头像成功!");
		$("#img-avatar").attr("src", json.data);
				
		$.cookie("avatar", json.data, {
			expires: 7
		});
	} else {
		alert(json.message);
	}
}

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

关于收货地址数据的处理,大致步骤是:增加 > 列表 > 删除 > 设为默认 > 修改收货地址。

首先,创建收货地址的数据表:

CREATE TABLE t_address (
	id INT AUTO_INCREMENT COMMENT 'id',
	uid INT NOT NULL COMMENT '数据归属的用户的id',
	name VARCHAR(20) COMMENT '收货人',
	province CHAR(6) COMMENT '省,例如:110000',
	city CHAR(6) COMMENT '市',
	area CHAR(6) COMMENT '区',
	district VARCHAR(30) COMMENT '省市区的中文名称,例如:河北省石家庄市和平区',
	zip CHAR(6) COMMENT '邮编',
	address VARCHAR(50) COMMENT '详细地址',
	phone VARCHAR(20) COMMENT '手机',
	tel VARCHAR(20) COMMENT '固定电话',
	tag VARCHAR(10) COMMENT '标签',
	is_default INT 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;

在设计数据表时,相关的约束(例如Unique或NOT NULL)并不一定是必须的,都是可以在程序中进行检查的,但是,通常,仍然建议在数据表中添加必要的约束,作为最后的数据保障!

然后,应该创建对应的实体类cn.tedu.store.entity.Address,继承自BaseEntity

设计SQL

增加数据的SQL语句为:

INSERT INTO t_address (
	除了id以外,包括4项日志在内的所有字段
) VALUES (
	与以上字段列表对应
)

通常,在增加数据时,还需要考虑某些数据的业务逻辑是否需要处理一些字段的值,例如用户数据中的插入,就必须伴随着“查询用户名”的功能,此次,处理的是收货数据地址,其中,“是否默认”的字段就应该有业务逻辑,逻辑规则是自行设定的,例如“每个用户创建的第1条收货地址就是默认的,后续创建的都不是默认的,如果把收货地址全部清空,再创建的第1条仍是默认,后续的仍不是默认的”,当应用该业务逻辑时,在增加数据时,还需要判断当前的收货地址是否应该默认,对应的SQL语句应该是“判断当前用户是否已经创建收货地址”:

SELECT COUNT(id) FROM t_address WHERE uid=?

接口和抽象方法

创建cn.tedu.store.mapper.AddressMapper接口,然后添加抽象方法:

Integer addnew(Address address);

Integer getCountByUid(Integer uid);

配置映射

复制得到新的AddressMapper.xml,修改根节点的namespace对应的接口,然后配置映射。

完成后,创建新的单元测试类,执行单元测试。

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

设计异常

此次操作可能出现InsertException,已经存在,无须再创建。

接口

创建cn.tedu.store.service.IAddressService接口,并添加抽象方法:

Address create(String username, 
	Address address) 
		throws InsertException;

实现

创建cn.tedu.store.service.impl.AddressServiceImpl类,实现以上接口,添加未实现的方法,添加@Service注解,添加@Autowired private AddressMapper addressMapper;属性。

然后,添加与持久层对应的2个私有方法:

private void addnew(Address address) {
	Integer rows 
		= addressMapper.addnew(address);
	if (rows != 1) {
		throw new InsertException("xxxx");
	}
}

private Integer getCountByUid(Integer uid) {
	return addressMapper.getCountByUid(uid);
}

然后,重写接口中的方法:

public Address create(String username,
	Address address) 
		throws InsertException {
	// 通过address.getUid()得到用户id,并以此查询该用户的收货地址数量
	// 判断数量是否为0
	// 是:当前用户首次创建地址,则该地址默认:address.setIsDefault(1);
	// 否:当前用户非首次创建地址,则该地址非默认:address.setIsDefault(0);

	// TODO 处理district

	// 封装日志

	// 执行创建新地址
}

完成后,创建新的单元测试类,执行单元测试。

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

创建cn.tedu.store.controller.AddressController,继承自BaseController,添加@RestController@RequestMapping("/address")注解,声明@Autowired private IAddressService addressService;变量。

处理新异常

(无)

设计所处理的请求

请求路径:/address/create
请求类型:GET测试完后改为POST
请求参数:Address, HttpSession
响应数据:ResponseResult<Void>
是否拦截:是,登录拦截,添加新配置

处理请求

@GetMapping("/create")
public ResponseResult<Void> handleCreate(
	Address address, HttpSession session) {
	// 根据session获取username

	// 根据session获取uid
	// 将uid封装到address中

	// 调用业务层对象执行创建收货地址
}

通过http://localhost:8080/address/create?name=Jack&province=440000&city=440300&area=440305&phone=13800138666&tag=Company执行测试。

附:省市区的数据处理

首先,导入t_dict_district.sql到数据库中。

该表记录了全国所有的省、市、区的数据(由于是测试数据,可能数据不完整),其中,name表示名称,例如北京市code表示代号,例如110000parent表示父级单位的代码,区的父级就是市,市的父级是省,省的父级代号统一使用的是86

由于使用了新的数据表,所以,在项目中应该创建新的实体类:

public class District implements Serializable {
	private Integer id;
	private String parent;
	private String code;
	private String name;
}

并且,还需要提供相关的数据访问功能,对于这种字典性质的数据表,并不存在增、删、改类型的操作,只需要查询即可,通常,包括查询某个父级的所有子级的列表(例如查询全国所有省、某省的所有市、某市的所有区)及根据代号查询名称的功能。

所以,还需要开发这些功能对应的持久层功能和业务层功能。

所以,先创建持久层接口cn.tedu.store.mapper.DistrictMapper,并添加抽象方法:

List<District> findByParent(String parent);

District findByCode(String code);

然后,配置以上抽象方法对应的映射。

当持久层开发完毕后,创建测试类,执行单元测试。

接下来,应该完成业务层,首先创建业务层接口cn.tedu.store.service.IDistrictService,声明抽象方法:

List<District> getListByParent(String parent);

District getByCode(String code);

然后,创建业务层实现类cn.tedu.store.service.impl.DistrictServiceImpl,添加@Service注解,添加@Autowired private DistrictMapper districtMapper;

添加2个私有方法:

private List<District> findByParent(String parent) {
	return districtMapper.findByParent(parent);
}

private District findByCode(String code) {
	return districtMapper.findByCode(code);
}

然后,实现接口中定义的方法:

public List<District> getListByParent(String parent) {
	return findByParent(parent);
}

public District getByCode(String code) {
	return findByParent(code);
}

IUserService -> UserMapper

IAddressService -> AddressMapper

IDistrictService -> DistrictMapper

23.收货地址-列表-持久层

1. SQL

SELECT 
	id, name, 
	phone, district, 
	address, tag, 
	is_default AS isDefault
FROM 
	t_address
WHERE 
	uid=?
ORDER BY 
	is_default DESC,
	modified_time DESC

2. 抽象方法

List<Address> findByUid(Integer uid);

3. 映射

<!-- 获取某用户的收货地址列表 -->
<!-- List<Address> findByUid(Integer uid) -->
<select id="findByUid"
	resultType="cn.tedu.store.entity.Address">
	SELECT 
		id, name, 
		phone, district, 
		address, tag, 
		is_default AS isDefault
	FROM 
		t_address
	WHERE 
		uid=#{uid}
	ORDER BY 
		is_default DESC,
		modified_time DESC
</select>

24.收货地址-列表-业务层

1. 是否抛出异常

2. 接口中添加抽象方法

List<Address> getListByUid(Integer uid);

3. 实现类中添加私有方法和重写未实现的方法

private List<Address> findByUid(Integer uid) {
	return addressMapper.findByUid(uid);
}

public List<Address> getListByUid(Integer uid) {
	return findByUid(uid);
}

25.收货地址-列表-控制器层

1. 新异常

2. 设计请求

请求路径:/address/list
请求类型:GET / POST
请求参数:HttpSession
响应数据:ResponseResult<List<Address>>
是否拦截:登录拦截,无需修改配置 

3. 处理请求

@RequestMapping("/list")
public ResponseResult<List<Address>> 
	getListByUid(HttpSession session) {
	// 获取uid
	Integer uid = getUidFromSession(session);
	// 查询数据
	List<Address> list
		= addressService.getListByUid(uid);
	// 返回
	return new ResponseResult<List<Address>>(
			SUCCESS, list);
}

26.收货管理-设为默认-持久层

1. SQL

UPDATE t_address SET is_default=0 WHERE uid=?

UPDATE t_address SET is_default=1 WHERE id=?

2. 抽象方法

Integer updateNonDefault(Integer uid);

Integer updateDefault(Integer id);

3. 映射

<!-- 将某用户的收货地址全部设置为非默认 -->
<!-- Integer updateNonDefault(Integer uid) -->
<update id="updateNonDefault">
	UPDATE
		t_address
	SET
		is_default=0
	WHERE 
		uid=#{uid}
</update>

<!-- 将指定id的收货地址设置为默认 -->
<!-- Integer updateDefault(Integer id) -->
<update id="updateDefault">
	UPDATE
		t_address
	SET
		is_default=1
	WHERE 
		id=#{id}
</update>

27.收货地址-设为默认-业务层

1. 异常

可能抛出UpdateException

2. 抽象方法

void setDefault(Integer uid, Integer id);

3. 实现

private void updateNonDefault(Integer uid) {
	Integer rows = addressMapper.updateNonDefault(uid);
	if (rows < 1) {
		throw new UpdateException("");
	}
}

private void updateDefault(Integer id) {
	Integer rows = addressMapper.updateDefault(id);
	if (rows != 1) {
		throw new UpdateException("");
	}
}

public void setDefault(Integer uid, Integer id) {
	updateNonDefault(uid);
	updateDefault(id);
}

附:使用事务(Transaction)

当执行的某个业务涉及2次或更多次的增、删、改操作时,必须使用事务来保证数据安全,例如2次Update,或1次Delete与1次Update,都是需要使用事务的,而查询不涉及事务。

当需要通过事务保证数据安全时,应该在业务方法之前添加@Transactional注解。

该注解也可以添加在业务类上,表示该业务类中所有的方法都是有事务保障的,通常,还是推荐将注解添加到业务方法上。

该功能是Spring提供的(spring-jdbc),与MyBatis无关。

需要注意的是,Spring框架在处理时,类似于:

开启事务 begin
try {
	执行数据操作
	可能:提交 commit
} catch (RuntimeException e) {
	可能:回滚 rollback
}

所以,在开发时,必须做到:

  1. 所有的增、删、改操作,在业务层调用时,必须判断返回值,如果视为操作错误,必须抛出异常;
  2. 业务层抛出的异常必须是RuntimeException的子孙类!

如果不是通过SpringBoot创建的项目,而是自行配置SSM的项目,则需要添加配置:

<!-- 事务管理器 -->
<bean id="transactionManager"
	class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource"
		ref="dataSource" />
</bean>

<!-- 注解驱动 -->
<tx:annotation-driven
	transaction-manager="transactionManager"/>



### 收货地址-设为默认-业务层

如果设为默认时:

setDefault(1, 3);

其中,1是uid,3是数据id,如果id=3的数据不归属于id=1的用户,则视为非法操作,不允许执行“设为默认”!

解决方案:根据id=3查询收货地址的数据详情,查询结果中必须包含uid,将查询到的结果中的uid与uid=1进行对比,如果一致,则允许操作,否则,视为非法操作!

所以,解决这个问题,必须先设计出“根据id查询收货地址数据”的功能!则应该先在持久层开发功能,然后在业务层补充私有方法来调用持久的功能。

select xx from t_address where id=?

Address findById(Integer id);

然后在业务中补充业务的设计:

@Override
@Transactional
public void setDefault(Integer uid, Integer id) {
	// 根据id查询收货地址数据
	Address data = findById(id);
	
	// 判断数据是否为null
	if (data == null) {
		throw new AddressNotFoundException(
			"设置默认收货地址失败!尝试访问的收货地址数据不存在!");
	}
	
	// 判断查询到的数据中的uid与参数uid是否一致
	if (data.getUid() != uid) {
		throw new AccessDeniedException(
			"设置默认收货地址失败!访问数据权限验证不通过!");
	}
	
	// 将该用户的所有收货地址设置为非默认
	updateNonDefault(uid);
	// 将指定id的收货地址设置为默认
	updateDefault(id);
}

28.收货地址-设为默认-控制器层

1. 处理新异常

需要在BaseController中处理新的异常:AddressNotFoundExceptionAccessDeniedException

2. 设计请求

请求路径:/address/default/{id}
请求参数:HttpSession
请求类型:GET
响应数据:ResponseResult<Void>
是否拦截:无需修改

3. 处理请求

@GetMapping("/default/{id}")
public ResponseResult<Void> 
	setDefault(HttpSession session,
		@PathVariable("id") Integer id) {
	// 从session中获取uid
	// 调用业务层方法执行设置
	// 返回
}

完成后,在浏览器中先登录,然后通过例如http://localhost:8080/address/default/20进行测试。

29.收货地址-删除-持久层

1. 设计SQL

删除数据:

DELETE FROM t_address WHERE id=?

如果删除的数据是最后一条数据,则后续不需要做特殊处理,可以通过已有的getCountByUid(uid)来实现。

如果删除的是默认收货地址,还需要将“最后修改的地址”设置为默认,则需要确定哪条数据才是“最后修改的地址”,即:取出当前用户的、根据最后修改时间倒序排列的第1条数据:

SELECT 
	id 
FROM 
	t_address 
WHERE 
	uid=? 
ORDER BY 
	modified_time DESC 
LIMIT 
	0,1

最后,再通过已有的setDefault()完成设置新的默认收货地址。

2. 抽象方法

Integer deleteById(Integer id);

Address findLastModified(Integer uid);

3. 配置映射

完成,执行单元测试。

30.收货地址-删除-业务层

1. 新的异常

DeleteException

2. 抽象方法

void delete(Integer uid, Integer id);

3. 实现

先完成新的私有方法。

然后,实现接口中的公有方法:

public void delete(Integer uid, Integer id) {
	// ----- 分析 -----
	// 1. 根据id查询收货地址数据findById(id)
	//	知晓即将删除的数据是不是默认收货地址
	//	需要修改findById的查询字段,增加is_default
	// 2. 根据id删除数据
	// 3. 检查该用户还有没有收货地址:getCountByUid(uid)
	// 4. 设置新的收货地址:setDefault(uid, id)
	//
	// ----------------
	//
	// 根据id查询收货地址数据:findById(id)
	// 检查数据是否为null
	// 是:抛出AddressNotFoundException
	// 检查数据归属是否有误
	// 是:抛出AccessDeniedException
	// 执行删除
	// 
	// 检查还有没有收货地址数据:getCountByUid(uid)
	// 是:判断刚才判断的是否是默认收货地址
	// -- 是:获取最后修改的收货地址:findLastModified(uid)
	// -- 将最后修改的收货地址设置为默认收货地址
}

具体实现代码为:

@Override
@Transactional
public void delete(Integer uid, Integer id) throws DeleteException {
	// 根据id查询收货地址数据:findById(id)
	Address data = findById(id);
	// 检查数据是否为null
	if (data == null) {
		// 是:抛出AddressNotFoundException
		throw new AddressNotFoundException("删除收货地址失败!尝试删除的数据不存在!");
	}
	
	// 检查数据归属是否有误
	if (data.getUid() != uid) {
		// 是:抛出AccessDeniedException
		throw new AccessDeniedException("删除收货地址失败!访问数据权限验证不通过!");
	}
	
	// 执行删除
	deleteById(id);
	
	// 检查还有没有收货地址数据:getCountByUid(uid)
	if (getCountByUid(uid) > 0) {
		// 是:判断刚才判断的是否是默认收货地址
		if (data.getIsDefault() == 1) {
			// -- 是:获取最后修改的收货地址:findLastModified(uid)
			Integer lastModifiedId
				= findLastModified(uid).getId();
			// -- 将最后修改的收货地址设置为默认收货地址
			setDefault(uid, lastModifiedId);
		}
	}
}

31.收货地址-删除-控制器层

1. 处理新异常

此次业务层抛出了新的异常:DeleteException

2. 设计请求

请求路径:/address/delete/{id}
请求参数:HttpSession
请求类型:GET
响应数据:ResponseResult<Void>
是否拦截:无需修改

3. 处理请求


附:导入乱码解决方案

尝试使用t_goods-2.zip文件(新上传的)

  1. 新建文本文档,将下载的脚本文件的内容复制并粘贴到新建的文本文档中
  2. 保存文本文档之前,选择编码为utf-8
  3. 保存文本文档
  4. 导入刚才保存的文本文档

作业

  1. 通过http://localhost:8080/goods/list/分类id可以获取指定分类下的所有商品的数据
  2. 通过http://localhost:8080/category/list/父级分类id可以获取指定父级分类下的所有子级分类的数据
  3. 通过http://localhost:8080/goods/details/商品id可以获取指定id的商品的详细数据
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值