电脑商城项目03头像上传和新增收货地址

上传头像

错误方法:把文件存到数据库中,需要图片时访问数据库,数据库将文件解析为字节流返回,最后写到本地的某一个文件.这种方法太耗费资源和时间了

正确方法:将对应的文件保存在操作系统上,然后再把这个文件路径记录下来,因为在记录路径的时候是非常便捷和方便的,将来如果要打开这个文件可以依据这个路径找到这个文件,所以说在数据库中保存该文件的路径即可.

稍微大一点的公司都会将所有的静态资源(图片,文件,其他资源文件)放到某台电脑上,再把这台电脑作为一台单独的服务器使用

1.上传头像-持久层

1.1SQL语句的规划

更新用户avatar字段的sql语句

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

1.2设计接口和抽象方法

在UserMapper接口中定义一个抽象方法用于修改用户的头像

1
2
3
4
5
6
7
8
9
10
11
/** 根据用户uid修改用户的头像*/
/**
* 注解@Param("SQL映射文件中#{}占位符的变量名"),解决的问题:
* 当SQL语句的占位符和映射的接口方法参数名不一致时,需要将某个参数强行注入到某个
* 占位符变量上时,可以使用@Param这个注解来标注映射的关系
* */
Integer updateAvatarByUid(@Param("uid") Integer iddddd,//@Param("参数名")注解中的参数名需要和sql语句中
//的#{参数名}的参数名保持一致.该处表示iddddd中的变量值要注入到sql语句的uid中
String avatar,
String modifiedUser,
Date modifiedTime);

1.3编写映射

UserMapper.xml文件中编写映射的SQL语句

1
2
3
4
5
6
7
8
9
<update id="updateAvatarByUid">
update t_user
set
avatar = #{avatar},
modified_user = #{modifiedUser},
modified_time = #{modifiedTime}
where
uid = #{uid}
</update>

1.4单元测试

1
2
3
4
5
6
7
8
@Test
public void updateAvatarByUid() {
userMapper.updateAvatarByUid(
11,
"abc",
"mxy",
new Date());
}

2.上传头像-业务层

2.1规划异常

  • 用户数据不存在,找不到对应的用户数据
  • 更新的时候,出现未知异常

无需重复开发

2.2设计接口和抽象方法及实现

1.先分析一下业务层接口需要哪些参数:那就需要看持久层接口要的有什么参数:

uid,avatar,modifiedUser,modifiedTime,其中modifiedTime是在方法中创建的,uid和modifiedUser从session中获取,但是session对象是在控制层的并不会出现在业务层,所以业务层要保留这两个参数,以便控制层可以传递过来

1
2
3
4
5
6
7
8
9
10
/**
* 修改用户的头像
* @param uid 用户uid
* @param avatar 用户头像的路径
* @param username 用户名称
*/
void changeAvatar(Integer uid,
String avatar,
String username);//业务层一般叫username而不叫modifiedUser,因
// 为业务层并没有直接和数据库关联

2.编写业务层的更新用户头像的方法

1
2
3
4
5
6
7
8
9
10
11
12
@Override
public void changeAvatar(Integer uid, String avatar, String username) {
//查询当前的用户数据是否存在
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("更新用户头像时产生未知异常");
}
}

2.3单元测试

1
2
3
4
@Test
public void changeAvatar() {
userService.changeAvatar(11,"222","mmm");
}

3.上传头像-控制层

文件上传过程中产生的异常太多了,再比如文件类型不匹配或文件被损坏

3.1规划异常

客户端传递文件给服务器,服务器的控制端controller接收文件,接收时可能抛出异常,因为用户传过来的文件有可能超出了我们的大小限制

该异常能放在业务层抛出吗?没必要的,因为此时数据是从控制层往下传的,所以控制层产生的异常直接在这一层(控制层)抛就可以了

上传文件时的异常都是文件异常,所以可以先创建一个文件异常类的基类FileUploadException并使其继承RuntimeException

文件异常基类的子类有:

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

在controller包下创子包ex,在ex包里面创建文件异常类的基类和上述五个文件异常类,创建的六个类都重写其父类的五个构造方法

3.2处理异常

在基类BaseController中进行编写和统一处理

1
2
3
4
5
6
7
8
9
10
11
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异常类不会被拦截到该方法中,点进@ExceptionHandler注解可以发现传参可以传数组类型,所以可以将异常统一处理方法上的注解改为:

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

3.3设计请求

  • /users/change_avatar
  • POST(GET请求提交数据只有2KB左右)
  • HttpSession session(获取uid和username),MultipartFile file
  • JsonResult(不能是JsonResult:如果上传头像后浏览别的页面,然后再回到上传头像的页面就展示不出来了,所以图片一旦上传成功,就要保存该图片在服务器的哪个位置,这样的话一旦检测到进入上传头像的页面就可以通过保存的路径拿到图片,最后展示在页面上)

3.4处理请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
@RequestMapping("change_avatar")
public JsonResult<String> changeAvatar(HttpSession session,
MultipartFile file) {
/**
* 1.参数名为什么必须用file:在upload.html页面的147行<input type=
* "file" name="file">中的name="file",所以必须有一个方法的参数名
* 为file用于接收前端传递的该文件.如果想要参数名和前端的name不一
* 样:@RequestParam("file")MultipartFile ffff:把表单中name=
* "file"的控件值传递到变量ffff上
* 2.参数类型为什么必须是MultipartFile:这是springmvc中封装的一个
* 包装接口,如果类型是MultipartFile并且参数名和前端上传文件的name
* 相同,则会自动把整体的数据包传递给file
*/
//判断文件是否为null
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("文件类型不支持");
}

//上传的文件路径:.../upload/文件名.png
/**
* session.getServletContext()获取当前Web应用程序的上下文
* 对象(每次启动tomcat都会创建一个新的上下文对象)
* getRealPath("/upload")的/代表当前web应用程序的根目录,通过该相
* 对路径获取绝对路径,返回一个路径字符串,如果不能进行映射返回null,单
* 斜杠可要可不要
*/
String parent =
session.getServletContext().getRealPath("/upload");
System.out.println(parent);//调试用

//File对象指向这个路径,通过判断File是否存在得到该路径是否存在
File dir = new File(parent);
if (!dir.exists()) {//检测目录是否存在
dir.mkdirs();//创建当前目录
}

//获取这个文件名称(文件名+后缀,如avatar01.png,不包含父目录结构)用UUID
// 工具生成一个新的字符串作为文件名(好处:避免了因文件名重复发生的覆盖)
String originalFilename = file.getOriginalFilename();
System.out.println("OriginalFilename="+originalFilename);
int index = originalFilename.lastIndexOf(".");
String suffix = originalFilename.substring(index);
//filename形如SAFS1-56JHIOHI-HIUGHUI-5565TYRF.png
String filename =
UUID.randomUUID().toString().toUpperCase()+suffix;

//在dir目录下创建filename文件(此时是空文件)
File dest = new File(dir, filename);

//java可以把一个文件的数据直接写到同类型的文件中,这里将参数file中的数据写入到空文件dest中
try {
file.transferTo(dest);//transferTo是一个封装的方法,用来将file文件中的数据写入到dest文件

/**
* 先捕获FileStateException再捕获IOException是
* 因为后者包含前者,如果先捕获IOException那么
* FileStateException就永远不可能会被捕获
*/
} 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,avatar,username);
//返回用户头像的路径给前端页面,将来用于头像展示使用
return new JsonResult<>(OK,avatar);
}

4.上传头像-前端页面

1.在upload.html的上传头像的表单加上三个属性:

  • action=“/users/change_avatar”
  • method=“post”(get请求提交数据只有2KB左右)
  • enctype=“multipart/form-data”(如果直接使用表单进行文件的上传,需要给表单加该属性,这样不会将目标文件的数据结构做修改后再上传,这不同于字符串,字符串随意切割修改也能拼在一起,但文件不行)

2.确认的type和name以及<input type=“submit” class=“btn btn-primary” value=“上传” />中的type

5.前端页面优化——修复bug

5.1更改默认的大小限制

springmvc默认为1MB文件可以进行上传,如果刚好是10241024=1048576 bytes则会报代码错误,自己在控制层设置的public static final int AVATAR_MAX_SIZE = 101024*1024;需要在不超过原有大小的情况下才会起作用,所以要手动修改springmvc默认上传文件的大小

方式1:直接在配置文件application.properties中进行配置:

  • spring.servlet.multipart.max-file-size=10MB(表示上传的文件最大是多大)
  • spring.servlet.multipart.max-request-size=15MB(整个文件是放在了request中发送给服务器的,请求当中还会有消息头等其他携带的信息,这里设置请求最大为15MB)

方式2:采用java代码的形式来设置文件的上传大小的限制:

1.该代码必须在主类中进行配置,因为主类是最早加载的,而配置文件必须是最早加载的

2.在主类中定义一个方法,方法名无所谓,但方法需要用@bean修饰,表示该方法返回值是一个bean对象,并且该bean对象被bean修饰,也就是这个方法返回了一个对象,然后把该对象交给bean管理,类似spring中的bean标签,含义是一样的,只是这里改为了注解

3.用@Configuration修饰主类使@bean注解生效,但其实@SpringBootApplication是@SpringBootConfiguration,@EnableAutoConfiguration,@ComponentScan三个注解的合并,所以可以不需要@Configuration

4.方法返回值是MultipartConfigElement类型,表示所要配置的目标的元素

1
2
3
4
5
6
7
8
9
10
11
12
@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();
}

5.2上传后显示头像

上传头像成功后不能显示头像.

在页面中通过ajax请求来提交文件,提交完成后返回了json串,解析出json串中的data数据设置到img标签的src属性上

1.删掉在upload.html的上传头像的表单中加的三个属性:action=“/users/change_avatar”,method=“post”,enctype=“multipart/form-data”.加上id属性:id=“form-change-avatar”

2.把153行的input标签里面的type=”submit”改为type=“button”(因为submit按钮不能添加事件,所以要改为普通的按钮)并加上属性id=“btn-change-avatar”

1.serialize():可以将表单数据自动拼接成key=value的结构提交给服务器,一般提交的是普通的控件类型中的数据(type=text/password/radio/checkbox等等)

2.FormData类:将表单中数据保持原有的结构进行数据提交.文件类型的数据可以使用FormData对象进行存储

使用方法:new FormData($(“form”)[0]);

这行代码的含义是将id=”form”的表单的第一个元素的整体值作为创建FormData对象的数据

3.虽然我们把文件的数据保护下来了,但是ajax默认处理数据时按照字符串的形式进行处理,以及默认会采用字符串的形式进行数据提交.手动关闭这两个功能:

  • processData: false,//处理数据的形式,关闭处理数据
  • contentType: false,//提交数据的形式,关闭默认提交数据的形式

下面给提交表单加上事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<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>

5.3登录后显示头像

将头像上传后会显示头像,但是关闭浏览器后再进入个人头像页面就不会显示头像了,因为只有点击”上传”才能发送ajax请求并显示头像.

可以在每次用户登录成功后将avatar保存在cookie中,登录的业务层返回给控制层user对象,该对象包含uid,username,avatar.所以要在登录页面login.html中将服务器返回的头像路径设置到cookie中,然后每次检测到用户打开上传头像页面,在这个页面中通过ready()方法来自动读取cookie中头像路径并设到src属性上

1.需要在login.html页面头部导入cookie.js文件

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

2.调用cookie方法保存路径

1
$.cookie(key,value,time);//time单位:天

在ajax请求原有的代码上加$.cookie(“avatar”,json.data.avatar,{expires: 7});

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

3.需要在upload.html获取cookie中的值,所以要在页面头部导入cookie.js文件

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

4.在upload.html的script标签中加ready()自动读取cookie数据

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

5.4显示最新头像

上传头像后不重新登录而是浏览其他页面,然后再进入个人头像页面时展示的头像是上次上传的,因为此时cookie中的值是上次上传的头像的路径,所以需要上传头像后使用同名覆盖更改cookie中路径

在ajax函数的success属性值的if语句加:

1
$.cookie("avatar",json.data,{expires: 7});

完善后重启服务测试,结果若和预测的不一样,则参考项目环境搭建->项目测试->测试静态资源能否正常加载里面的四种解决方法

新增收货地址

1.创建数据表

1.选中数据表

1
use store

2.在store数据库中创建t_address表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
CREATE TABLE t_address (
aid INT AUTO_INCREMENT COMMENT '收货地址id',
uid INT COMMENT '归属的用户id',
`name` VARCHAR(20) COMMENT '收货人姓名',
province_name VARCHAR(15) COMMENT '省-名称',
province_code CHAR(6) COMMENT '省-行政代号',
city_name VARCHAR(15) COMMENT '市-名称',
city_code CHAR(6) COMMENT '市-行政代号',
area_name VARCHAR(15) COMMENT '区-名称',
area_code CHAR(6) COMMENT '区-行政代号',
zip CHAR(6) COMMENT '邮政编码',
address VARCHAR(50) COMMENT '详细地址',
phone VARCHAR(20) COMMENT '手机',
tel VARCHAR(20) COMMENT '固话',
tag VARCHAR(6) 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 (aid)
) ENGINE=INNODB DEFAULT CHARSET=utf8;

注意name是关键字,所以需要用``

2.创建收货地址的实体类

在entity包下创建实体类Address继承BaseEntity类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**收货地址额实体类*/
public class Address extends BaseEntity {
private Integer aid;
private Integer uid;
private String name;
private String provinceName;
private String provinceCode;
private String cityName;
private String cityCode;
private String areaName;
private String areaCode;
private String zip;
private String address;
private String phone;
private String tel;
private String tag;
private Integer isDefault;
/**
* get,set
* equals和hashCode
* toString
*/
}

3.新增收货地址-持久层

3.1各功能的开发顺序

当前收货地址功能模块:

  • 第一个页面:列表的展示,修改,删除,设置默认
  • 第二个页面:新增收货地址

开发顺序:新增收货地址->列表的展示->设置默认收货地址->删除收货地址->修改收货地址

3.2规划需要执行的SQL语句

1.新增收货地址对应的是插入语句:

1
insert into t_address (aid以外的所有字段) values (字段值)

2.大部分平台都会规定一个用户的收货地址数量,这里规定最多20个.那么在插入用户新的地址之前就要先做查询操作.如果查询到的是刚好20,这并不是一个java语法的异常,可以认为是业务控制的异常,这个异常随后在service抛,在controller捕获

1
select count(*) from t_address where uid=?

3.3设计接口和抽象方法

创建接口AddressMapper,在这个接口中定义上面两个SQL语句抽象方法定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**收货地址持久层的接口*/
public interface AddressMapper {
/**
* 插入用户的收货地址数据
* @param address 收货地址数据
* @return 受影响的行数
*/
Integer insert (Address address);

/**
* 根据用户的uid统计收货地址数量
* @param uid 用户的uid
* @return 当前用户的收货地址总数
*/
Integer countByUid(Integer uid);
}

3.4编写映射

1.快速创建一个AddressMapper.xml映射文件:

  • 鼠标放在UserMapper.xml文件上并ctrl+c,再把鼠标放到mapper文件夹上ctrl+v,在弹出的窗口中把UserMapper改为AddressMapper.
  • 进入AddressMapper.xml文件将mapper标签下的代码全部删除并修改mapper标签属性值为namespace=“com.cy.store.mapper.AddressMapper”

结果如下所示:

1
2
3
4
5
6
<?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">
<mapper namespace="com.cy.store.mapper.AddressMapper">
</mapper>

2.在mapper标签中配置Address类属性与数据库中表的字段映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<resultMap id="AddressEntityMap" type="com.cy.store.entity.Address">
<id column="aid" property="aid"/>
<result column="province_name" property="provinceName"/>
<result column="province_code" property="provinceCode"/>
<result column="city_name" property="cityName"/>
<result column="city_code" property="cityCode"/>
<result column="area_name" property="areaName"/>
<result column="area_code" property="areaCode"/>
<result column="is_default" property="isDefault"/>
<result column="created_user" property="createdUser"/>
<result column="created_time" property="createdTime"/>
<result column="modified_user" property="modifiedUser"/>
<result column="modified_time" property="modifiedTime"/>
</resultMap>

判断该映射是否配置成功:按着ctrl并点击type=”com.cy.store.entity.Address”中的Address,如果能跳转到Address类说明映射成功

3.在AddressMapper.xml中配置以上两个抽象方法的映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<insert id="insert" useGeneratedKeys="true" keyProperty="aid">
INSERT INTO t_address (
uid, `name`, province_name, province_code, city_name, city_code, area_name, area_code, zip,
address, phone, tel,tag, is_default, created_user, created_time, modified_user, modified_time
) VALUES (
#{uid}, #{name}, #{provinceName}, #{provinceCode}, #{cityName}, #{cityCode}, #{areaName},
#{areaCode}, #{zip}, #{address}, #{phone}, #{tel}, #{tag}, #{isDefault}, #{createdUser},
#{createdTime}, #{modifiedUser}, #{modifiedTime}
)
</insert>

<!--resultType="java.lang.Integer"不写会报错,因为Integer不是基本数据类型-->
<select id="countByUid" resultType="java.lang.Integer">
select count(*) from t_address where uid=#{uid}
</select>

3.5单元测试

在test下的mapper文件夹下创建AddressMapperTests测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@SpringBootTest
@RunWith(SpringRunner.class)
public class AddressMapperTests {

@Autowired
private AddressMapper addressMapper;

@Test
public void insert() {
Address address = new Address();
address.setUid(11);
address.setPhone("133336");
address.setName("女朋友");
addressMapper.insert(address);
}

@Test
public void countByUid() {
Integer count = addressMapper.countByUid(11);
System.out.println(count);
}
}

4.新增收货地址-业务层

4.1规划异常

  • 插入数据时用户不存在(被管理员误删等等),抛UsernameNotFoundException异常(已经有了,不需要重复创建)

  • 当用户插入的地址是第一条时,需要将当前地址作为默认收货地址

    实现办法:如果查询到统计总数为0则将当前地址的is_default值设置为1

  • 如果查询的结果>=20,这时需要抛出业务控制的异常AddressCountLimitException

    1
    2
    3
    4
    /**收货地址总数超出限制的异常(20条)*/
    public class AddressCountLimitException extends ServiceException {
    /**重写ServiceException的所有构造方法*/
    }
  • 插入数据时产生未知的异常InsertException(已经有了,不需要重复创建)

4.2设计接口和抽象方法及实现

1.创建一个IAddressService接口,在接口中定义业务的抽象方法

因为mapper层接口该功能模块定义了两个抽象方法,所以就要在service层接口该功能模块也定义两个抽象方法?不是这样的,要看mapper层的这两个方法是依赖关系还是独立关系,如果某一个抽象方法依赖于另一个抽象方法,那就需要在业务层将这两个方法整合到一个方法中.一句话来说就是:一个功能模块可能需要多条sql语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
/**收货地址的业务层接口*/
@Service
public interface IAddressService {
/**
*这三个参数的由来:
* 1.首先肯定要有address
* 2.业务层需要根据uid查询该用户收货地址总数及新建地址时给字段uid赋值
* 但新建收货地址的表单中并没有哪个控件让输入用户uid,所以需要控制层将uid传给业务层
* 3.业务层在创建/修改收货地址时需要同时修改数据库中创建人/修改人的字段
* 但新建收货地址的表单中并没有哪个控件让输入用户username,所以需要控制层将username传给业务层
* 注意:> 可以用HttpSession session代替Integer uid, String username,但
* 这样写的话就需要把BaseController类下获取uid,username的方法重新封装到一个
* 类中并让IAddressServiceImp实现类继承该类,这样就需要微调一下代码逻辑,太麻
* 烦,并且,最好每一层只处理该层需要做的事情,session对象是控制层传递的,所以就
* 把session对象定义封装在控制层中,不需要在业务层中额外处理以降低耦合
*/
void addNewAddress(Integer uid, String username, Address address);
}

方法addNewAddress中三个参数的由来:

  • 首先肯定要有address

  • 业务层需要根据uid查询该用户收货地址总数及新建地址时给字段uid赋值

    但新建收货地址的表单中并没有哪个控件让输入用户uid,所以需要控制层将uid传给业务层并在业务层封装到address对象中

  • 业务层在创建/修改收货地址时需要同时修改数据库中创建人/修改人的字段

    但新建收货地址的表单中并没有哪个控件让输入用户username,所以需要控制层将username传给业务层并在业务层封装到address对象中

可以用HttpSession session代替Integer uid, String username,但这样写的话就需要把BaseController类下获取uid,username的方法重新封装到一个类中并让AddressServiceImpl实现类继承该类,这样就需要微调一下代码逻辑,太麻烦,并且,最好每一层只处理该层需要做的事情,session对象是控制层传递的,所以就把session对象定义封装在控制层中,不需要在业务层中额外处理,这样可以降低耦合

2.创建一个AddressServiceImpl类实现接口中抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
/**新增收货地址的实现类*/
public class AddressServiceImpl implements IAddressService {
@Autowired
private AddressMapper addressMapper;
@Autowired
private UserMapper userMapper;

/**
* 为了方便日后修改最大收货地址数量,可以在配置文件
* application.properties中定义user.address.max-count=20
*/
//spring读取配置文件中数据:@Value("${user.address.max-count}")
@Value("${user.address.max-count}")
private Integer maxCount;

@Override
public void addNewAddress(Integer uid, String username, Address address) {
User result = userMapper.findByUid(uid);
if (result ==null || result.getIsDelete() == 1) {
throw new UsernameNotFoundException("用户数据不存在");
}

//调用统计收货地址数量的方法
Integer count = addressMapper.countByUid(uid);
if (count >= maxCount) {
throw new AddressCountLimitException("用户收货地址超出上限");
}

//uid,isDefault
address.setUid(uid);
Integer isDefault = count == 0 ? 1 : 0;//1表示默认收货地址,0反之
address.setIsDefault(isDefault);

//补全四项日志
address.setCreatedUser(username);
address.setModifiedUser(username);
address.setCreatedTime(new Date());
address.setModifiedTime(new Date());

//调用插入收货地址的方法
Integer rows = addressMapper.insert(address);
if (rows != 1) {
throw new InsertException("插入用户的收货地址时产生未知异常");
}
}
}

别忘了在配置文件application.properties中定义user.address.max-count=20

4.3单元测试

在test下的service文件夹下创建AddressServiceTests测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootTest
@RunWith(SpringRunner.class)
public class AddressServiceTests {
@Autowired
private IAddressService addressService;

@Test
public void addNewAddress() {
Address address = new Address();
address.setPhone("175726");
address.setName("男朋友");
addressService.addNewAddress(11,"mxy",address);
}
}

5.新增收货地址-控制层

5.1处理异常

义务层抛出了收货地址总数超出上限的异常,在BaseController中进行捕获处理

1
2
3
4
else if (e instanceof AddressCountLimitException) {
result.setState(4003);
result.setMessage("用户的收货地址超出上限的异常");
}

5.2设计请求

  • /addresses/add_new_address
  • post
  • Address address,HttpSession session
  • JsonResult

5.3处理请求

在controller包下创建AddressController并继承BaseController,该类用来处理用户收货地址的请求和响应

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@RequestMapping("addresses")
@RestController
public class AddressController extends BaseController{
@Autowired
private IAddressService addressService;

@RequestMapping("add_new_address")
public JsonResult<Void> addNewAddress(Address address, HttpSession session) {
Integer uid = getUidFromSession(session);
String username = getUsernameFromSession(session);
addressService.addNewAddress(uid,username,address);
return new JsonResult<>(OK);
}
}

启动服务器,登录账号后在地址栏输入http://localhost:8080/addresses/add_new_address?name=tom&phone=98745612进行测试

6.新增收货地址-前端页面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script>
$("#btn-add-new-address").click(function () {
$.ajax({
url: "/addresses/add_new_address",
type: "POST",
data: $("#form-add-new-address").serialize(),
dataType: "JSON",
success: function (json) {
if (json.state == 200) {
alert("新增收货地址成功")
} else {
alert("新增收货地址失败")
}
},
error: function (xhr) {
alert("新增收货地址时产生未知的异常!"+xhr.message);
}
});
});
</script>

获取省市区列表

新增收货地址页面的三个下拉列表的内容展示没有和数据库进行交互,而是通过前端实现的(将代码逻辑放在了distpicker.data.js文件中),实现方法是在加载新增收货地址页面时加载该js文件,这种做法不可取(我不知道为啥)

正确做法是:把这些数据保存到数据库中,用户点击下拉列表时相应的数据会被详细的展示出来,然后监听用户选择了哪一项以便后面的下拉列表进行二级关联

1.创建数据表

1.创建t_dict_district表

1
2
3
4
5
6
7
CREATE TABLE t_dict_district (
id INT(11) NOT NULL AUTO_INCREMENT,
parent VARCHAR(6) DEFAULT NULL,
`code` VARCHAR(6) DEFAULT NULL,
`name` VARCHAR(16) DEFAULT NULL,
PRIMARY KEY (id)
) ENGINE=INNODB DEFAULT CHARSET=utf8;
  • code和name需要加``
  • parent代表父区域的代码号
  • code代表自身的代码号
  • 省的父代码号是+86,代表中国

2.向该表中插入省市区数据

1
2
3
LOCK TABLES t_dict_district WRITE;
INSERT INTO t_dict_district VALUES (1,'110100','110101','东城区'),(2,'110100','110102','西城区')等等等等;
UNLOCK TABLES;

LOCK和UNLOVK干嘛用的?

2.创建省市区的实体类

在包entity下创建实体类District(不需要继承BaseEntity,但因为没有继承BaseEntity所以需要实现接口Serializable序列化)

1
2
3
4
5
6
7
8
9
10
11
12
/**省市区的数据实体类*/
public class District implements Serializable {
private Integer id;
private String parent;
private String code;
private String name;
/**
* get,set
* equals和hashCode
* toString
*/
}

3.获取省市区列表-持久层

3.1规划需执行的SQL语句

1
select * from t_dict_district where parent=? order by ASC

3.2设计接口和抽象方法

日后可能开发新的模块仍要用到省市区列表,那么为了降低耦合性,就要创建新的接口

在mapper层下创建接口DistrictMapper

1
2
3
4
5
6
7
8
9
public interface DistrictMapper {

/**
* 根据父代码号查询区域信息
* @param parent 父代码号
* @return 某个父区域下所有的区域列表
*/
List<District> findByParent(String parent);//查询的结果可能是多个,所以放在集合中
}

3.3编写映射

创建一个DistrictMapper.xml映射文件并配置上述抽象方法的映射

1
2
3
4
5
6
7
8
9
10
<?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">
<mapper namespace="com.cy.store.mapper.DistrictMapper">
<select id="findByParent" resultType="com.cy.store.entity.District">
select * from t_dict_district where parent=#{parent}
order by code ASC
</select>
</mapper>

3.4单元测试

创建DistrictMapperTests测试类编写代码进行测试

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
@RunWith(SpringRunner.class)
public class DistrictMapperTests {

@Autowired
private DistrictMapper districtMapper;

@Test
public void findByParent() {
List<District> list = districtMapper.findByParent("210100");
for (District district : list) {
System.out.println(district);
}
}
}

4.获取省市区列表-业务层

4.1规划异常

没有异常需要处理

4.2设计接口和抽象方法及实现

1.创建一个接口IDistrictService,并定义抽象方法

1
2
3
4
5
6
7
8
9
public interface IDistrictService {

/**
* 根据父代码号来查询区域信息(省或市或区)
* @param parent 父代码号
* @return 多个区域的信息
*/
List<District> getByParent(String parent);
}

2.创建DistrictServiceImpl实现类来实现抽象方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Service
public class DistrictServiceImpl implements IDistrictService {

@Autowired
private DistrictMapper districtMapper;

@Override
public List<District> getByParent(String parent) {
List<District> list = districtMapper.findByParent(parent);
/**
* 在进行网络数据传输时,为了尽量避免无效数据的传递,可以将无效数据
* 设置为null,这样既节省流量,又提升了效率
*/
for (District district : list) {
district.setId(null);
district.setParent(null);
}
return list;
}
}

4.3单元测试

在test下的service文件夹下创建DistrictServiceTests测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@SpringBootTest
@RunWith(SpringRunner.class)
public class DistrictServiceTests {
@Autowired
private IDistrictService districtService;

@Test
public void getByParent() {
//86代表中国,所有的省父代码号都是86
List<District> list = districtService.getByParent("86");
for (District district : list) {
System.err.println(district);
}
}
}

5.获取省市区列表-控制层

5.1设计请求

  • /districts/
  • GET
  • String parent
  • JsonResult<List>

5.2处理请求

1.创建一个DistrictController类,在类中编写处理请求的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RequestMapping("districts")
@RestController
public class DistrictController extends BaseController{
@Autowired
private IDistrictService districtService;

/**
* 请求路径和父路径相同时用@RequestMapping({"/",""}),表
* 示districts后面跟/或者什么也不跟都会进入这个方法
* 点进RequestMapping发现参数类型是String[],且传入一
* 个路径时默认有{},传入一个以上路径时需要手动添加{}
*/
@RequestMapping({"/",""})
public JsonResult<List<District>> getByParent(String parent) {
List<District> data = districtService.getByParent(parent);
return new JsonResult<>(OK,data);
}
}

2.为了能不登录也可以访问该数据,需要将districts请求添加到白名单中:

在LoginInterceptorConfigure类的addInterceptors方法中添加代码:patterns.add(“/districts/**”);

3.启动服务器,不登录账号,直接在地址栏输入http://localhost:8080/districts?parent=86测试能否正常获取数据

6.获取省市区列表-前端页面

1.原始的下拉列表展示是将数据放在js,再动态获取js中的数据,而目前为止我们已经将数据放在了数据库,所以不能让它再使用这种办法了,所以需要注释掉addAddress.html页面的这两行js代码:

1
2
<script type="text/javascript" src="../js/distpicker.data.js"></script>
<script type="text/javascript" src="../js/distpicker.js"></script>

关于这两行js代码:前者是为了获取数据,后者是为了将获取到的数据展示到下拉列表中

2.检查前端页面在提交省市区数据时是否有相关name属性和id属性(name用于提交数据,id用于监听用户的点击)

3.启动服务器,在前端验证一下是否还可以正常保存数据(除了省市区)

获取省市区名称

上一个模块获取省市区列表是通过父代码号获取子代码号完成联动,该模块获取省市区名称是通过自身的code获取自身的name

1.获取省市区名称-持久层

3.1规划需要执行的SQL语句

根据当前code来获取当前省市区的名称,对应就是一条查询语句

1
select * from t_dict_district where code=?

3.2设计接口和抽象方法

在DistrictMapper接口定义findNameByCode方法

1
String findNameByCode(String code);

3.3编写映射

在DistrictMapper.xml文件中添加findNameByCode方法的映射

1
2
3
<select id="findNameByCode" resultType="java.lang.String">
select name from t_dict_district where code=#{code}
</select>

3.4单元测试

在DistrictMapperTests编写测试代码

1
2
3
4
5
@Test
public void findNameByCode() {
String name = districtMapper.findNameByCode("610000");
System.out.println(name);
}

2.获取省市区名称-业务层

2.1规划异常

没有异常需要处理

2.2设计接口和抽象方法及实现

1.在IDistrictService接口定义对应的业务层接口中的抽象方法

1
String getNameByCode(String code);

2.在DistrictServiceImpl实现此方法

1
2
3
4
@Override
public String getNameByCode(String code) {
return districtMapper.findNameByCode(code);
}

2.3单元测试

业务层只是调用持久层对应的方法然后返回,没有什么额外的实现,可以不用测试(一般超过8行的代码都要进行测试)

3.获取省市区名称-控制层

实际开发中在获取省市区名称时并不需要前端传控制层,然后传业务层,再传持久层,而是在新增收货地址的业务层需要获取省市区名称,也就是说获取省市区名称的模块不需要控制层,只是需要被新增收货地址的业务层所依赖

4.获取省市区名称-业务层优化

1.在新增收货地址的业务层需要对address进行封装,使其存有所有数据,然后将address传给持久层(记住,持久层只会根据传过来的参数调用某个方法与数据库交互,永远不会有额外的实现),而此时新增收货地址的业务层并没有省市区的数据,所以需要依赖于获取省市区列表的业务层对应的接口中的getNameByCode方法

所以需要在业务层实现类AddressServiceImpl中加

1
2
@Autowired
private IDistrictService districtService;

2.在AddressServiceImpl的方法中将DistrictService接口中获取到的省市区数据封装到address对象,此时address就包含了所有用户收货地址的数据

1
2
3
4
5
6
7
8
9
10
/**
* 对address对象中的数据进行补全:省市区的名字看前端代码发现前端传递过来的省市区的name分别为:
* provinceCode,cityCode,areaCode,所以这里可以用address对象的get方法获取这三个的数据
*/
String provinceName = districtService.getNameByCode(address.getProvinceCode());
String cityName = districtService.getNameByCode(address.getCityCode());
String areaName = districtService.getNameByCode(address.getAreaCode());
address.setProvinceName(provinceName);
address.setCityName(cityName);
address.setAreaName(areaName);

5.获取省市区名称-前端页面

在addAddress.html页面中来编写对应的省市区展示及根据用户的不同选择来限制对应的标签中的内容

分析:

  • 在加载该页面时三个下拉列表的内容都显示为”—–请选择—–”
  • 没有选择市时如果点击区的下拉列表则列表中只有一个”—–请选择—–”
  • 加载该页面时需要自动发送一个请求把parent=86发送出去,然后将返回的省/直辖市填充到select标签中
  • 点击四川省后发送请求获取其下的市,并且将获取到的市罗列在市区域下拉列表中
  • 省点击”—–请选择—–“则需要把市,县内容填充为”—–请选择—–”终止请求而不是程序继续跑下去
  • 切换省份时,市,县内容更换为”—–请选择—–”

在addAddress.html中编写js代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
/**因为清空后下拉列表的select标签没有option标签,所以需要设置一个默认的option标
* 签并给市,县加上该标签.option标签并不会把内容发送到后端,而是将value值发
* 送给后端,所以用value表示当前这个区域的code值
* */
var defaultOption="<option value='0'>-----请选择-----</option>";
$(document).ready(function () {
//加载省的数据罗列时代码量较多,建议定义在外部方法中,然后在这里调用定义的方法
showProvinceList();

//将省,市,县的下拉列表内容设为"-----请选择-----"
/**
* select标签默认获取第一个option的内容填充到下拉列表中,所以即使加载
* 页面时省区域的下拉列表中已经有了所有省但仍然会显示-----请选择-----
* */
$("#province-list").append(defaultOption);

$("#city-list").append(defaultOption);
$("#area-list").append(defaultOption);
});

//省的下拉列表数据展示
function showProvinceList() {
$.ajax({
url: "/districts",//发送请求用于获取所有省对象
type: "POST",
data: "parent=86",
dataType: "JSON",
success: function (json) {
if (json.state == 200) {
var list = json.data;//获取所有省对象的List集合
for (var i = 0; i < list.length; i++) {
var opt =
"<option value='"+list[i].code+"'>"+list[i].name+"</option>";
$("#province-list").append(opt);
}
} else {
<!--这个其实永远不会执行,因为没有编写
异常,控制层返回的状态码永远是OK-->
alert("省/直辖区的信息加载失败")
}
}
//这里没有写属性error,不知道为啥不用写,感觉写了更好
});
}

/**
* change()函数用于监听某个控件是否发生改变,一旦发生改变就
* 会触发参数形式的函数,所以参数需要是function(){}
* */
$("#province-list").change(function () {
//先获取到省区域父代码号
var parent = $("#province-list").val();

/**
* 如果我选择了河南省洛阳市涧西区,然后又选择了河北省,此时需要
* 将市,县下拉列表的所有option清除并显示内容-----请选择-----
* empty()表示某标签的所有子标签(针对此页面来说select的子标
* 签只有option)
* */
$("#city-list").empty();
$("#area-list").empty();
//填充默认值:-----请选择-----
$("#city-list").append(defaultOption);
$("#area-list").append(defaultOption);

if (parent == 0) {//如果继续程序,后面的ajax接收的json数据中的data是
return;//空集合[],进不了for循环,没有任何意义,所以直接在这里终止程序
}
$.ajax({
url: "/districts",
type: "POST",
data: "parent="+parent,
dataType: "JSON",
success: function (json) {
if (json.state == 200) {
var list = json.data;
for (var i = 0; i < list.length; i++) {
var opt =
"<option value='"+list[i].code+"'>"+list[i].name+"</option>";
$("#city-list").append(opt);
}
} else {
alert("市的信息加载失败")
}
}
});
});

$("#city-list").change(function () {
var parent = $("#city-list").val();
$("#area-list").empty();
$("#area-list").append(defaultOption);

if (parent == 0) {
return;
}
$.ajax({
url: "/districts",
type: "POST",
data: "parent="+parent,
dataType: "JSON",
success: function (json) {
if (json.state == 200) {
var list = json.data;
for (var i = 0; i < list.length; i++) {
var opt =
"<option value='"+list[i].code+"'>"+list[i].name+"</option>";
$("#area-list").append(opt);
}
} else {
alert("县的信息加载失败")
}
}
});
});

电脑商城项目03头像上传和新增收货地址
https://yztldxdz.top/2022/07/24/电脑商城项目03头像上传和新增收货地址/
发布于
2022年7月24日
许可协议