项目目录
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
表示代号,例如110000
,parent
表示父级单位的代码,区的父级就是市,市的父级是省,省的父级代号统一使用的是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
}
所以,在开发时,必须做到:
- 所有的增、删、改操作,在业务层调用时,必须判断返回值,如果视为操作错误,必须抛出异常;
- 业务层抛出的异常必须是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
中处理新的异常:AddressNotFoundException
、AccessDeniedException
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文件(新上传的)
- 新建文本文档,将下载的脚本文件的内容复制并粘贴到新建的文本文档中
- 保存文本文档之前,选择编码为utf-8
- 保存文本文档
- 导入刚才保存的文本文档
作业
- 通过
http://localhost:8080/goods/list/分类id
可以获取指定分类下的所有商品的数据 - 通过
http://localhost:8080/category/list/父级分类id
可以获取指定父级分类下的所有子级分类的数据 - 通过
http://localhost:8080/goods/details/商品id
可以获取指定id的商品的详细数据