• Springboot写电商系统(2)


    1.新增收货地址

    1.创建t_addresss数据库表

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

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

    2.创建Address实体类

    记得要继承BaseEntity类(里面有四个字段)。
    把除了BaseEntity里面的四个字段外的所有字段在这个实体类里添加,然后创建他们的get,set;equals和hashCode;toString。
    实体类的字段名要符合驼峰命名,所以会有和数据库字段名不一样的,记得要在持久层做字段名的映射。

    /**收货地址额实体类*/
    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
     */
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23

    3.数据库操作的持久层

    新增收获地址之前要判断一下这个uid用户的收货地址是否已经有20个了,如果是就不能再新增;如果是0,那么用户添加的第一个就要默认地址赋值1,如果是1-20,那默认地址就要赋值0。所以这里面设计到2个SQL语句:

    insert into t_address (aid以外的所有字段) values (字段值)
    select count(*) from t_address where uid=?
    
    • 1
    • 2

    1.接口写抽象方法

    在接口AddressMapper里面写上面两个SQL语句的抽象方法,第一个传递参数address,第二个传递uid。

    public interface AddressMapper {
        Integer insert (Address address);
        Integer countByUid(Integer uid);
    }
    
    • 1
    • 2
    • 3
    • 4

    2.xml写方法映射sql

    在AddressMapper.xml里面首先写xml对mybatis和上面接口的映射。

    <?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.example.store.mapper.AddressMapper">
    </mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    然后在resultMap标签里面写表字段和实体字段的映射(这里写名字不一样的字段和自增的主键字段aid),type写实体类的全路径。

       <resultMap id="AddressEntityMap" type="com.example.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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    最后写上面2个方法的sql映射,id对应接口里定义的方法名,有自增主键的要加上useGeneratedKeys和keyProperty,定义返回类型的加上resultType。

        <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>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3.测试

    首先给AddressMapperTests添加两个注解,一个是声明是测试类最终不会打包的@SpringBootTest,一个是可以运行的测试类@RunWith(SpringRunner.class);然后通过@Autowired自动装配Mapper,就可以使用mapper里面的方法了。

    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class AddressMapperTests {
        @Autowired
        private AddressMapper addressMapper;
        @Test
        public void insert(){
            Address address=new Address();
            address.setUid(1);
            address.setName("zoe");
            address.setAddress("Los Angeles");
            addressMapper.insert(address);
        }
        @Test
        public void countByUid(){
            Integer count=addressMapper.countByUid(1);
            System.err.println(count);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    4.前后数据交互的业务层

    拿到前端数据进行数据插入时会出现错误uid的用户不存在,这个异常UsernameNotFoundException已存在。
    当用户插入数据超过20条时不可以再添加,所以需要添加异常AddressCountLimitException。
    插入操作中出现的异常InsertException已存在。

    1.sql操作的异常抛出

    需要添加一个AddressCountLimitException异常并继承ServiceException ,然后重构5个构造方法。

    public class AddressCountLimitException extends ServiceException {
        /**重写ServiceException的所有构造方法*/
    }
    
    • 1
    • 2
    • 3

    2.交互方法的接口定义

    在IAddressService 接口里面定义新增地址的方法addNewAddress,这里面从前端拿到的表单信息是address(这里面有name是收货人),然后从session中拿到uid和username(这个将是修改人和创建人)。

    public interface IAddressService {
        void addNewAddress(Integer uid, String username, Address address);
    }
    
    • 1
    • 2
    • 3

    3.接口的方法实现

    首先要给这个接口实现类添加注解@Service,之后spring才能找到这个bean。
    在AddressServiceImpl里面实现接口方法。业务层里面将会用到持久层里面定义的sql方法,所以将用到方法的mapper(接口)导入并用@Autowired自动装载。
    这里用到了一个超参:用户收获地址的最大条数,在application.properties里面设置user.address.max-count=20;然后在类里面通过注解将值赋值到变量maxCount中。

    @Service//交给spring管理,所以需要在类上加@Service
    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("用户收货地址超出上限");
            }
            Integer isDefault=count==0?1:0;
            address.setIsDefault(isDefault);
            address.setUid(uid);
            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("插入用户收获地址时发生未知异常");
            }
        }
    }
    
    • 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

    4.测试

    还是先添加测试的两个注解@SpringBootTest@RunWith(SpringRunner.class),然后自动装载@Autowired业务层的接口,在测试方法里使用接口方法测试。

    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class AddressServiceTests {
        @Autowired
        private IAddressService addressService;
        @Test
        public void addNewAddress(){
            Address address=new Address();
            address.setName("老王");
            addressService.addNewAddress(2,"zoe",address);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12

    5.与前端交互的控制层

    1.添加请求成功的异常响应

    首先在业务层的时候有一个异常抛出,所以在控制层里面要把这个异常添加到响应的异常里面,在BaseController里添加:

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

    2.处理请求

    首先AddressController要继承BaseController。
    然后添加注解@RequestMapping("addresses")@RestController,用到了业务层的接口,所以用@Autowired自动装配,添加功能的响应地址@RequestMapping("add_new_address"),然后拿到前端数据进行处理。

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

    浏览器中输入http://localhost:8080/addresses/add_new_address?name=tom&phone=987进行测试。

    6.前端ajax请求

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

    然后就可以提交表单进行测试。

    7.后端获取省市区三级菜单

    现在的三级菜单功能是写在了前端的js里面,但实际业务中是要把这些数据写在数据库里面的,所以现在需要实现这个功能,首先把前端这个js代码删除掉:

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

    然后开始实现这个功能,这里前端用户选择省市区之后有一个联动效果,这个就是通过父code查到子name的操作。
    还有一个就是用户选择省市区之后传递给后端的其实是一个code,要补全数据库内容的话还需要有一个自己code查自己name的操作。所以这里的sql将是两个操作。

    1.创建地址表

    parent代表父区域的代码号;code代表自身的代码号;省的父代码号是+86,代表中国

    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;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    然后将这些规定好的数据插入到表中:

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

    2.创建实体类

    这里就不用再继承了,因为这个表里面没有那四个是字段,这个表只是用来查询父子地址的。
    因为没有继承BaseEntity所以需要实现接口Serializable序列化。
    添加表字段之后,添加get,set; equals和hashCode; toString方法

    public class District implements Serializable {
        private Integer id;
        private String parent;
        private String code;
        private String name;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    3.sql持久层

    为了之后这个功能的复用,将这个新建持久层DistrictMapper接口,然后写mapper接口和sql映射

    public interface DistrictMapper {
        List<District> findByParent(String parent);
        String findNameByCode(String code);
    }
    <?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.example.store.mapper.DistrictMapper">
        <select id="findByParent" resultType="com.example.store.entity.District">
            select * from t_dict_district where parent=#{parent} order by code ASC
        </select>
        <select id="findNameByCode" resultType="java.lang.String">
            select name from t_dict_district where code=#{code}
        </select>
    </mapper>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    测试持久层:

    @SpringBootTest
    @RunWith(SpringRunner.class)
    public class DistrictMapperTests {
        @Autowired
        private DistrictMapper districtMapper;
        @Test
        public void findByParent(){
            List<District> list=districtMapper.findByParent("86");
            for (District d:list){
                System.out.println(d);
            }
        }
        @Test
        public void findNameByCode(){
            String name= districtMapper.findNameByCode("610000");
            System.out.println(name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    4.前后端数据操作的业务层

    首先查询的时候不会有什么异常,所以直接定义抽象接口方法,然后实现方法。从前端拿到parent(父的code)然后查询并返回数据,这里返回数据的时候将每一条子数据的id和parent都置为null,利于数据的传输,所以每个子数据传到前端的就是这个子的code和name。

    public interface IDistrictService {
        List <District> findByParent(String parent);
        String findNameByCode(String code);
    }
    @Service
    public class DistrictServiceImpl implements IDistrictService {
        @Autowired
        private DistrictMapper districtMapper;
        @Override
        public List<District> findByParent(String parent) {
            List<District> list=districtMapper.findByParent(parent);
            for (District d :list){
                d.setId(null);
                d.setParent(null);
            }
            return list;
        }
        @Override
        public String findNameByCode(String code) {
            return districtMapper.findNameByCode(code);
        }
    }
    
    • 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 DistrictServiceTests {
        @Autowired
        private IDistrictService districtService;
        @Test
        public void findByParent(){
            List<District> list= districtService.findByParent("86");
            for (District d:list){
                System.out.println(d);
            }
        }
         @Test
        public void findNameByCode(){
            String name=districtService.findNameByCode("610000");
            System.out.println(name);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    这里要分开讨论,首先是联动选择,当前端选择一个父code到后端之后,后端需要拿到子code和那么,这个和前端交互,所以需要写一个controller,当前端有动作请求时后端就要判断是不是要用这个控制器来返回给前端子name数据。
    但是通过code找到自己的name,这个数据不需要传递给前端,只需要在后端把这个name数据添加到district对象上,然后保存到数据库。所以第二个code找name的方法不用到控制层,在Address添加地址的时候,把前端传来的code找到name,然后添加到数据库即可。所以在AddressServiceImpl新增收货地址的时候用这个服务实现功能:

    @Autowired
    private IDistrictService districtService;
    		String provinceName=districtService.findNameByCode(address.getProvinceCode());
            String cityName = districtService.findNameByCode(address.getCityCode());
            String areaName = districtService.findNameByCode(address.getAreaCode());
            address.setProvinceName(provinceName);
            address.setCityName(cityName);
            address.setAreaName(areaName);
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    5.响应前端的控制层

    首先是没有新的error,所以不用处理异常,直接继承BaseConrtroller即可,然后开始处理响应,

    @RequestMapping("districts")
    @RestController
    public class DistrictController extends BaseController {
        @Autowired
        private IDistrictService districtService;
        @RequestMapping({"/",""})
        public JsonResult<List<District>> findByParent(String parent){
            List<District> list=districtService.findByParent(parent);
            return new JsonResult<>(OK,list);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    输入http://localhost:8080/districts?parent=86网址进行测试。

    6.前端请求

    进入页面显示省的列表加载,然后省的组件有改变的话就向后端发送请求市,同理获取第三级数据。

    var defaultOption="";// option标签并不会把内容发送到后端,而是将value值发送给后端,所以用value表示当前这个区域的code值
    			$(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;//获取所有省对象的List集合
    							for (var i = 0; i < list.length; i++) {
    								var opt =
    										"+list[i].name+"";
    								$("#province-list").append(opt);
    							}
    						} else {
    							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) {//如果继续程序,后面的ajax接收的json数据中的data是空集合[],进不了for循环,没有任何意义,所以直接在这里终止程序
    					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 =
    										"+list[i].name+"";
    								$("#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 =
    										"+list[i].name+"";
    								$("#area-list").append(opt);
    							}
    						} else {
    							alert("县的信息加载失败")
    						}
    					}
    				});
    			});
    
    • 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

    2.删除收货地址

    设为默认地址和删除收货地址有很多相似之处,我就只写一下删除吧。首先是sql语句的设计。

    1.持久层

    删除的话就是通过aid删除数据,但如果删除的数据是默认地址(通过getIsDefault拿到真否),那么就要得到modeified_time最近的那一条数据作为默认(拿到之后设为默认,设默认这个功能我已经实现了,可以自己写一下),
    那么些接口和映射文件,limit 0,1表示查询到的第一条数据(limit (n-1),pageSize),这样查询后就只会获得第一条数据。
    select 整个数据时,一定要有resultMap="AddressEntityMap",对实体和表进行映射。

    Integer deleteByAid(Integer aid);
    Address findLastModified(Integer uid);
    <delete id="deleteByAid">
            delete from t_address where aid=#{aid}
    </delete>
    <select id="findLastModified" resultMap="AddressEntityMap">
            select * from t_address where uid=#{uid} order by modified_time DESC limit 0,1
    </select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    2.业务层

    这里sql执行会出现的异常包括:没有该条地址数据(已开发),地址数据归属错误(已开发)和删除的时候可能会产生未知的异常(DeleteException )。

    public class DeleteException extends ServiceException{
        /**重写ServiceException的所有构造方法*/
    }
    
    • 1
    • 2
    • 3

    然后实现delete这个功能:

    void delete(Integer aid,Integer uid,String username);
    @Override
        public void delete(Integer aid, Integer uid, String username) {
            Address result=addressMapper.findByAid(aid);
            if (result == null) {
                throw new AddressNotFoundException("收货地址数据不存在");
            }
            if (!result.getUid().equals(uid)) {
                throw new AccessDeniedException("非法数据访问");
            }
            Integer rows = addressMapper.deleteByAid(aid);
            if (rows != 1) {
                throw new DeleteException("删除数据时产生未知的异常");
            }
            if (result.getIsDefault() == 0) {
                return;
            }
            Integer count = addressMapper.countByUid(uid);
            if (count == 0) {
                return;
            }
            Address address = addressMapper.findLastModified(uid);
            rows = addressMapper.updateDefaultByAid(address.getAid(), username, new Date());
            if (rows != 1) {
                throw new UpdateException("更新数据时产生未知的异常");
            }
        }
    
    • 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

    3.控制层

    添加delete的异常到BaseController:

    else if (e instanceof DeleteException) {
        result.setState(5002);
        result.setMessage("删除数据时产生未知的异常");
    }
    
    • 1
    • 2
    • 3
    • 4

    然后是后端响应:/addresses/{aid}/set_default(以前的数据是通过表单直接提交的,还有一种提交方式就是RestFul风格,这种提交方式可以提交更多的数据,这里用这个提交方式)。
    RestFul编写时不管参数名和占位符是否一致都必须加@PathVariable(“aid”)

    @RequestMapping("{aid}/delete")
    public JsonResult<Void> delete(@PathVariable("aid") Integer aid,HttpSession session) {
        addressService.delete(
            aid,
            getUidFromSession(session),
            getUsernameFromSession(session));
        return new JsonResult<>(OK);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    4.前端请求和渲染

    给显示列表的"删除"按钮添加onclick属性并指向deleteByAid(aid)方法。

    <td><a onclick="delete_(#{aid})" class="btn btn-xs add-del btn-info"><span class="fa fa-trash-o"></span> 删除</a></td>
    
    • 1

    在显示列表的for循环中为占位符aid赋值。

    tr = tr.replace("#{aid}",list[i].aid);
    
    • 1

    然后写点击了删除按钮和触发的delete_函数,这里不能写delete,因为这是一个关键字。

    function delete_(aid) {
    			$.ajax({
    				url: "/addresses/"+aid+"/delete",
    				type: "POST",
    				dataType: "JSON",
    				success: function (json) {
    					if (json.state == 200) {
    						//重新加载收货地址列表页面
    						showAddressList();
    					} else {
    						alert("删除收货地址失败")
    					}
    				},
    				error: function (xhr) {
    					alert("删除收货地址时产生未知的异常!"+xhr.message);
    				}
    			});
    		}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    3.显示购物车列表

    这里涉及到一个多表查询,用到了cart和product两个表的信息,所以表不用建,但是这两个表组成的数据是什么类型的实体呢(即不是product也不是cart),所以在store包下创建一个vo包,在该包下面创建CartVO类,不需要继承BaseController类,那相应的就需要单独实现Serializable接口。
    VO全称Value Object,值对象。当进行select查询时,查询的结果属于多张表中的内容,此时发现结果集不能直接使用某个POJO实体类来接收,因为POJO实体类不能包含多表查询出来的信息,解决方式是:重新去构建一个新的对象,这个对象用于存储所查询出来的结果集对应的映射,所以把这个对象称之为值对象。

    1.VO实体类

    首先分析前端需要的数据都有哪些,哪些是cart表里的数据,哪些是product表里的数据,最后在生成他们的getset,equal hash和tostring

    public class CartVO implements Serializable {
        private Integer cid;//购物车页面的勾选功能,要有cid
        private Integer uid;//要拿到某个用户的购物车信息,要有uid
        private Integer pid;//购物车操作后要更新product表里面的库存等信息,所以要有pid
        private Long price;//这个价格是用户加入购物车时的商品价格
        private Integer num;//这个num是购物车里这个用户这个商品的数量,和product里的num不一样(库存量)
        private String title;//这个是product表里的信息,要展示在购物车列表的前端
        private Long realPrice;//这个是product表里的价格
        private String image;//这个是product表里的信息,要展示在购物车列表的前端
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    2.持久层

    首先在CartMapper接口中定义方法,这个方法是通过uid查询到上面vo实体数据,然后两表关联的时候不希望有null数据,所以以cart表作为主表(左连接)查询此用户的vo信息。

    List<CartVO> findVoByUid(Integer uid);
    <select id="findVoByUid" resultType="com.example.store.vo.CartVO">
            select cid,uid,pid,t_cart.price,t_cart.num,title,t_product.price as realPrice,image
            from t_cart left join t_product on t_cart.pid=t_product.id
            where uid=#{uid}
            order by t_cart.created_time desc
     </select>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

    3.业务层

    List<CartVO> getVOByUid(Integer uid);
    @Override
        public List<CartVO> getVOByUid(Integer uid) {
            return cartMapper.findVoByUid(uid);
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    4.控制层

    @RequestMapping({"", "/"})
    public JsonResult<List<CartVO>> getVOByUid(HttpSession session) {
        List<CartVO> data = cartService.getVOByUid(getUidFromSession(session));
        return new JsonResult<List<CartVO>>(OK, data);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

    5.前端请求与渲染

    <script type="text/javascript">
        $(document).ready(function() {
        showCartList();
    	});
    
        //展示购物车列表数据
        function showCartList() {
            $("#cart-list").empty();
            $.ajax({
                url: "/carts",
                type: "GET",
                dataType: "JSON",
                success: function(json) {
                    var list = json.data;
                    for (var i = 0; i < list.length; i++) {
                        var tr = '\n' +
                            '\n' +
                            '<input name="cids" value="#{cid}" type="checkbox" class="ckitem" />\n' +
                            '\n' +
                            '<td><img src="..#{image}collect.png" class="img-responsive" /></td>\n' +
                            '<td>#{title}#{msg}</td>\n' +
                            '<td>¥<span id="goodsPrice#{cid}">#{singlePrice}</span></td>\n' +
                            '\n' +
                            '<input type="button" value="-" class="num-btn" οnclick="reduceNum(1)" />\n' +
                            '<input id="goodsCount#{cid}" type="text" size="2" readonly="readonly" class="num-text" value="#{num}">\n' +
                            '<input class="num-btn" type="button" value="+" οnclick="addNum(#{cid})" />\n' +
                            '\n' +
                            '<td><span id="goodsCast#{cid}">#{totalPrice}</span></td>\n' +
                            '\n' +
                            '<input type="button" οnclick="delCartItem(this)" class="cart-del btn btn-default btn-xs" value="删除" />\n' +
                            '\n' +
                            '';
                        tr = tr.replaceAll(/#{cid}/g, list[i].cid);
                        tr = tr.replaceAll(/#{image}/g, list[i].image);
                        tr = tr.replaceAll(/#{title}/g, list[i].title);
                        tr = tr.replaceAll(/#{singlePrice}/g, list[i].realPrice);
                        tr = tr.replaceAll(/#{num}/g, list[i].num);
                        tr = tr.replaceAll(/#{totalPrice}/g, list[i].realPrice * list[i].num);
                        if (list[i].realPrice < list[i].price) {
                            tr = tr.replace(/#{msg}/g, "比加入时降价" + (list[i].price - list[i].realPrice) + "元");
                        } else {
                            tr = tr.replace(/#{msg}/g, "");
                        }
                        $("#cart-list").append(tr);
                    }
                },
                error: function (xhr) {
                    alert("加载购物车列表数据时产生未知的异常"+xhr.status);
                }
            });
        }
    </script>
    
    • 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

    4.补充知识点

    1.当从url中拿数据传递给后端时,在ajax请求里的data可以用下面的方式:

    data:location.search.substring(1),// URL 中的 ? 及之后的部分,1就是从下标为1的开始,相当于把?去掉了
    
    • 1

    5.利用AOP统计业务层执行时长

    如果我们想对业务某一些方法同时添加相同的功能需求,并且在不改变业务功能逻辑的基础之上进行完成,就可以使用AOP的切面编程进行开发。
    使用步骤:
    1.首先定义一个类,将这个类作为切面类。(访问权限是public,返回值类型可以是void或Object,如果该方法被@Around注解修饰,必须使用Object作为返回值类型,切面方法可以接收参数,参数是ProccedingJoinPoint接口类型的参数.但是@Around所修饰的方法必须要传递这个参数.其他注解修饰的方法要不要该参数都可以)
    2.在这个类中定义切面方法(5种:前置,后置,环绕,异常,最终)
    3.将这个切面方法中的业务逻辑对应的代码进行编写和设计
    4.通过连接点来连接目标方法,就是用粗粒度表达式和细粒度表达式来进行连接。

    1.导包

    因为AOP不是Spring内部封装的技术,所以需要进行导包操作:在pom.xml文件中添加两个关于AOP的依赖aspectjweaver和aspectjtools。

    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
    </dependency>
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjtools</artifactId>
    </dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    2.创建切面类和切面方法

    在com.cy.store.aop包下创建TimerAspect切面类,给类添加两个注解进行修饰:
    1.@Aspect(将当前类标记为切面类)
    2.@Component(将当前类的对象创建使用维护交由Spring容器维护)
    然后在类里面定义自己的切面方法,这里用了环绕通知方法(参数ProceedingJoinPoint接口表示连接点,也就是是目标方法的对象)

    @Aspect
    @Component
    public class TimerAspect {
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            //开始时间
            long start = System.currentTimeMillis();
            //调用目标方法,
            Object result = pjp.proceed();
            //结束时间
            long end = System.currentTimeMillis();
            System.out.println("耗时:"+(end-start));
            return result;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    3.映射切面

    将当前环绕通知映射到某个切面上,也就是指定连接的点.给自己定义的去切面方法around添加注解@Around

    @Around("execution(* com.example.store.service.impl.*.*(..))")
    
    • 1

    第一个表示方法返回值是任意的
    第二个
    表示imp包下的类是任意的
    第三个*表示类里面的方法是任意的
    (…)表示方法的参数是任意的
    最后项目执行到service层时,就是计时每个方法的耗时。

  • 相关阅读:
    在OpenCloudOS 上安装.NET 6
    【C++&Python&Java】字符处理详细解读_字符_ASCLL码_字母数字转换_算法竞赛_开发语言
    Maxwell 是什么?
    【数据结构】树与二叉树(七):二叉树的遍历
    c++——线程安全的单例
    一桩字符串长度引发的惨案
    数据大屏指南 | 一屏看世界,一点选专业
    java私钥加密——SHA256withRSA
    STM32第九节(中级篇):RCC(第一节)——时钟树讲解
    在云服务器上打开ftp服务-踩坑及心得
  • 原文地址:https://blog.csdn.net/weixin_38226321/article/details/133918447