上传头像 错误方法:把文件存到数据库中,需要图片时访问数据库,数据库将文件解析为字节流返回,最后写到本地的某一个文件.这种方法太耗费资源和时间了
正确方法:将对应的文件保存在操作系统上,然后再把这个文件路径记录下来,因为在记录路径的时候是非常便捷和方便的,将来如果要打开这个文件可以依据这个路径找到这个文件,所以说在数据库中保存该文件的路径即可.
稍微大一点的公司都会将所有的静态资源(图片,文件,其他资源文件)放到某台电脑上,再把这台电脑作为一台单独的服务器使用
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 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 void changeAvatar (Integer uid, String avatar, String username) ;
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) { if (file.isEmpty()) { throw new FileEmptyException ("文件为空" ); } if (file.getSize()>AVATAR_MAX_SIZE) { throw new FileSizeException ("文件超出限制" ); } String contentType = file.getContentType(); if (!AVATAR_TYPE.contains(contentType)) { throw new FileTypeException ("文件类型不支持" ); } String parent = session.getServletContext().getRealPath("/upload" ); System.out.println(parent); File dir = new File (parent); if (!dir.exists()) { dir.mkdirs(); } String originalFilename = file.getOriginalFilename(); System.out.println("OriginalFilename=" +originalFilename); int index = originalFilename.lastIndexOf("." ); String suffix = originalFilename.substring(index); String filename = UUID.randomUUID().toString().toUpperCase()+suffix; File dest = new File (dir, filename); try { file.transferTo(dest); } catch (FileStateException e) { throw new FileStateException ("文件状态异常" ); } catch (IOException e) { 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 = 10 1024*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 () { MultipartConfigFactory factory = new MultipartConfigFactory (); factory.setMaxFileSize(DataSize.of(10 , DataUnit.MEGABYTES)); factory.setMaxRequestSize(DataSize.of(15 ,DataUnit.MEGABYTES)); 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-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);
在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.选中数据表
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; }
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 { Integer insert (Address address) ; 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 > < ! < 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 public class AddressCountLimitException extends 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 { 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; @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 ("用户收货地址超出上限" ); } address.setUid(uid); Integer isDefault = count == 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; }
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 { 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 { 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); 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 () { 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({"/",""}) 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 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 var defaultOption="<option value='0'>-----请选择-----</option>" ; $(document ).ready (function ( ) { showProvinceList (); $("#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 ; 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 ("省/直辖区的信息加载失败" ) } } }); } $("#province-list" ).change (function ( ) { var parent = $("#province-list" ).val (); $("#city-list" ).empty (); $("#area-list" ).empty (); $("#city-list" ).append (defaultOption); $("#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>" ; $("#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 ("县的信息加载失败" ) } } }); });