# 电脑商城项目实战 **Repository Path**: darkski/computer-mall-project-practice ## Basic Information - **Project Name**: 电脑商城项目实战 - **Description**: SpringBoot+MyBatis+MySQL - **Primary Language**: Java - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 1 - **Forks**: 1 - **Created**: 2022-08-08 - **Last Updated**: 2024-05-05 ## Categories & Tags **Categories**: Uncategorized **Tags**: Java ## README 电脑商城项目实战 #### 介绍 比较简单的练习项目 SpringBoot+MyBatis+MySQL #### 项目分析 (1)项目功能:登录、注册、热销商品、用户管理(密码、个人i西南西、头像、收货地址)、购物车(展示、增加、删除)、订单模块 (2)开发顺序:注册、登录、用户管理、购物车、商品、订单模块 (3)模块的开发 * 持久层开发:依据前端页面的设置规划相关的SQL语句,以及进行配置 * 业务层开发:核心功能的控制、业务的操作以及异常处理 * 控制层开发:接收请求、处理响应 * 前端开发:JS、JQuery、AJAX这些技术连接后台 #### 项目环境 (1)JDK:1.8及以上版本 (2)Maven:配置到idea,3.6.1版本 (3)数据库:Maria DB,MySQL5.1及以上版本 (4)开发平台:idea # 一、搭建项目 (1)项目名称:store (2)项目结构:com.cy.store ```` java web mybatis mysql driver ```` (3)资源文件:resources文件夹下 * static:静态资源 * templates:模板 (4)单元测试:test.com.cy.store (5)配置数据库的连接元信息 ````properties # 应用服务 WEB 访问端口 server.port=8080 #下面这些内容是为了让MyBatis映射 #指定Mybatis的Mapper文件 mybatis.mapper-locations=classpath:mapper/*xml #指定Mybatis的实体目录 mybatis.type-aliases-package=com.cy.store.mybatis.entity # 数据库驱动: spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver # 数据源名称 spring.datasource.name=defaultDataSource # 数据库连接地址 spring.datasource.url=jdbc:mysql://localhost:3306/store?serverTimezone=UTC # 数据库用户名&密码: spring.datasource.username=root spring.datasource.password=123 ```` (6)创建数据库 见tools\sql_src文件夹 (7)测试连接 * SpringBoot主类启动成功 * 单元测试类中测试数据库的来凝结是否可以正常加载 (8)访问项目的静态资源是否可以正常的架子啊。所有的静态资源放在static目录下 > 注意:idea对js代码的兼容性较差,如果不能正常的加载 > > 1、idea缓存清理 > > 2、clear-instal > > 3、rebuild重新构建 > > 4、重启idea和操作系统 # 二、用户注册 ### 1、创建数据表 ### 2、创建用户实体类 #### 2.1 BaseEntity基类 ````java /** * 项目中许多实体类都会有日志相关的四个属性,所以在创建实体类之前,应先创建这些实体类的基类,将4个日志属性声明在基类中 */ @Data @AllArgsConstructor @NoArgsConstructor @ToString @EqualsAndHashCode public class BaseEntity implements Serializable { private String createdUser; private Date createdTime; private String modifiedUser; private Date modifiedTime; } ```` #### 2.2 User类 ````java /** * 用户实体类:get和set方法,equals和HashCode()方法、ToString方法 */ @Data @ToString @EqualsAndHashCode //@Component SpringBoot约定大于配置 public class User extends BaseEntity implements Serializable { private Integer uid; private String username; private String password; private String salt; private String phone; private String email; private Integer gender; private String avatar; private Integer is_delete; } ```` ### 3、注册——持久层 通过MyBatis来操作数据库。即在做Mybatis的开发流程 #### 3.1 规划需要执行的SQL语句 (1)用户的注册功能,相当于在做数据的插入操作 ````sql insert into t_user (username, password) values () ```` (2)在注册时,要首先进行查询当前用户名是否存在,存在才能进行注册——查询语句 ````sql select * from t_user where username = ? ```` #### 3.2 设计接口和抽象方法 定义Mapper接口。在项目的目录结构下构建一个mapper包,在这个包下再根据不同模块来创建mapper接口。 (1)创建一个UserMapper的接口 ````java //@Mapper 不推荐在接口中是哟个Mapper注解,因为一个项目中会又多个Mapper,推荐在MainApplication中采用MapperScan注解 public interface UserMapper { /** * 插入用户的数据 * @param user * @return 受到影响的行数 */ Integer insert(User user); /** * 根据用户名查找用户数据 * @param username * @return 如果找到,返回用户数据;如果没有找到,返回null */ User findByUserName(String username); } ```` (2)在启动类配置上mapper接口文件的位置 ````java // MapperScan注解指定当前项目中的Mapper接口路径的指定位置,在项目启动的时候自动加载接口文件 @MapperScan("com.cy.mapper") ```` #### 3.3 编写映射 定义xml映射文件,于对应的接口进行关联。所有映射文件需要放置在resources目录下,在这个目录下擦黄健一个mapper文件夹,在这个文件中存放Mapper的映射文件 (1)创建映射文件,和接口名称保持一致。 ````xml ```` (2)配置接口中的方法对应的SQL语句,借助标签来完成 ````xml insert into t_user ( username,password,salt,phone,email,gender,avatar,is_delete,created_user,created_time,modified_user,modified_time ) values ( #{username},#{password},#{salt},#{phone},#{email},#{gender},#{avatar},#{isDelete},#{createdUser},#{createdTime},#{modifiedUser},#{modifiedTime} ) ```` (3)单元测试 > 每个独立的层编写完毕后,需要编写单元测试,来测试当前的功能。 > 在test包结构下创建一个mapper包,在这个包下在创建持久层测试 ### 4、注册——业务层 #### 4.1 规划异常 (1)RuntimeException异常,作为异常的子类,然后再去定义具体异常的类型来继承这个异常 > 即业务层异常的基类,ServiceException异常,这个异常继承RuntimeException异常,异常机制建立 ![image-20220809003751574](README.assets/image-20220809003751574.png) ````java /** * 业务层异常的基类 */ public class ServiceException extends RuntimeException { public ServiceException() { super(); } public ServiceException(String message) { super(message); } public ServiceException(String message, Throwable cause) { super(message, cause); } public ServiceException(Throwable cause) { super(cause); } protected ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ```` > 根据业务层不同的功能来详细定义具体的异常类型,统一的去继承ServiceException异常类 (2)用户在进行注册的时候可能会产生用户名被占用的错误,抛出一个异常:UsernameDuplicatedException异常 ````java // 用户名被占用异常 public class UsernameDuplicatedException extends ServiceException {...同上...} ```` (3)正在执行数据插入操作的时候,服务器、数据库宕机,处于正在执行插入操作的过程中产生的异常InsertException异常 ````java /** * 数据插入过程中产生的异常 */ public class InsertException extends ServiceException {...同上...} ```` #### 4.2 设计接口和抽象方法 (1)在service包下创建一个IUserService接口 ````java /** * 用户模块业务层接口 */ public interface IUserService { // 注册用户 void reg(User user); } ```` (2)创建一个实现类 ````java /** * 用户模块业务层的实现类 */ public class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public void reg(User user) { String username = user.getUsername(); // 先判断 User result = userMapper.findByUserName(username); if (result != null) { // 抛出异常 throw new UsernameDuplicatedException("用户名被占用!"); } // 数据的补全操作 user.setIsDelete(0); user.setCreatedUser(user.getUsername()); Date date = new Date(); user.setCreatedTime(date); user.setModifiedTime(date); // 再插入 Integer rows = userMapper.insert(user); if (rows != 1) { throw new InsertException("用户注册过程中产生了未知异常!"); } } } ```` (3)单元测试:UserServiceTests类 ### 5、注册——控制层 #### 5.1 创建响应 状态码、状态描述信息、数据。 这部分功能封装再一个类中,将这类作为方法返回值,返回给前端浏览器 ````java /** * Json格式的数据进行响应 */ public class JsonResult implements Serializable { // 状态码 private Integer state; // 描述信息 private String message; // 泛型表示任何类型的数据 private E data; public JsonResult() { } public JsonResult(Integer state) { this.state = state; } // 异常的捕获 public JsonResult(Throwable e) { this.message = e.getMessage(); } public JsonResult(Integer state, E data) { this.state = state; this.data = data; } } ```` #### 5.2 设计请求 依据当前的业务功能模块来进行设计 ```` 请求路径:/user/reg 请求参数: User user 请求类型: POST 响应结果: JsonResult ```` #### 5.3 处理请求 (1)创建控制层对应的类UserController。依赖于业务层的接口 ````java /** * 控制层 * @author DarkSky * @version 1.0 */ //@Controller @RestController //@Controller + ResponseBody @RequestMapping("users") public class UserController { @Autowired private IUserService userService; @RequestMapping("reg") //@ResponseBody // 表示将此方法的响应结果以Json格式进行数据的响应给到前端 public JsonResult reg(User user) { // 创建响应结果对象 JsonResult result = new JsonResult<>(); try { userService.reg(user); result.setState(200); result.setMessage("用户注册成功!"); } catch (UsernameDuplicatedException e) { result.setState(4000); result.setMessage("用户名被占用!"); } catch (InsertException e) { result.setState(5000); result.setMessage("注册时产生未知异常!"); } return result; } } ```` #### 5.4 控制层优化设计 在控制层剥离一个父类,在此父类中统一的去处理关于异常的相关操作,编写一个BaseController类,统一处理异常 ````java /** * 控制层基类 */ @RestController public class BaseController { // 操作成功的状态码 public static final int OK = 200; // 请求处理方法,这个方法的返回值需要传递给前端的数据,则返回值应该为JsonResult // 自动将异常对象传递给此方法的参数列表 @ExceptionHandler(ServiceException.class) // 用于统一处理抛出的异常:凡是括号内的异常都拦截到这里处理 public JsonResult handleException(Throwable e) { JsonResult result = new JsonResult<>(); if (e instanceof UsernameDuplicatedException) { result.setState(4000); result.setMessage("用户名被占用!"); } else if (e instanceof InsertException) { result.setState(5000); result.setMessage("注册时产生未知异常!"); } return result; } } @RestController @RequestMapping("users") public class UserController extends BaseController { @Autowired private IUserService userService; @RequestMapping("reg") public JsonResult reg(User user) { userService.reg(user); return new JsonResult<>(OK); } } ```` ### 6、注册——前端页面 在register页面中编写发送请求的方法,点击时间来完成。选中对应的按钮($("选择器")),再去添加点击事件,$.ajax()函数发送异步请求。 > JQuery封装了一个函数,称为@.ajac()函数,通过对象调用ajax函数,可以异步加载相关的请求,依靠的是JavaScript提供的一个对象XHR(XmlHttpResponse) > > 异步:即页面不动,数据动 #### 6.1 ajax() (1)ajax的使用 > * ajax接收多个参数 > > * 参数与参数之间 用","进行分割 > > * 参数的声明顺序没有要求 > > * 每一组参数的组成:参数名称: "参数的值" > * 参数名称:不能随意定义,必须使用ajax规定好的 > * 参数的值:使用字符串来表示 ````javascript 需要传递一个方法体作为方法的参数来使用 $.ajax({ // 方法体 url: "", type: "", data: "", dataType: "", success: function() { }, error: function() { } }); ```` (2)ajax函数参数的含义 | 参数 | 功能描述 | 示例 | | -------- | ------------------------------------------------------------ | ----------------------------------- | | url | 表示请求的地址(URL地址),不能包含参数列表的内容 | url: "localhost:8080/users/reg" | | type | 请求类型(GET,POST) | type: "POST" | | data | 向指定的请求url地址提交的数据 | data: "username=laohu&password=123" | | dataType | 提交的数据的类型,数据类型一般指定为Json类型 | dataType: "json" | | success | 当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递到这个方法的参数上 | | | error | 当服务器未正常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递到这个方法的参数上 | | > js代码可以独立声明在一个js的文件里或者声明在一个script标签中 (3)示例 ````javascript ```` (4)js代码无法正常被服务器解析执行 解决方案 1. 在项目的maven下clean清理项目,后install重新部署 2. 在项目的file选项下-cash清理缓存 3. 重新狗啊金项目:build选项下-rebuild选项 4. 重启idea 5. 重启电脑 # 三、用户登录 > 当用户输入用户名和密码将数据提交给后台数据库进行查询,如果存在对应的用户名和密码则表示登录成功,登录成功之后跳转到系统的主页,即index.html页面,跳转在前端使用Jquery来实现 ### 1、登录——持久层 #### 1.1 规划需要执行的SQL语句 依据用户提交的用户名和密码做select查询,数据库只参与数据相关的操作,业务逻辑都在业务层(如查询在持久层,比较在业务层) ````mysql select * from t_user where username = ? ```` > 说明:如果在分析过程中,发现某个功能模块被开发完成,可以省略该步骤,但是分析过程不能省略 #### 1.2 接口设计和抽象方法 ### 2、登录——业务层 #### 2.1 规划异常 (1)用户名对应的密码错误,密码匹配失败的异常:PasswordNotMatchException异常,属于运行时异常,业务层异常。 (2)用户名未找到,抛出异常:UsernameNotFoundException异常,属于业务层异常 (3)异常的编写 * 业务层异常需要继承ServiceException异常类 * 在具体的异常类中重写构造方法 #### 2.2 设计业务层接口和方法 (1)直接在IUserSercvice中编写抽象方法,login(String username, String password),将当前登录成功的用户已当前用户对象的形式返回给客户端,这样在切换页面时不用重复的发送请求,状态管理:可以将数据保存在cookie或者session中,可以避免重复读很高的数据多次频繁操作数据库进行获取 > 可以这样规定: > > 用户名、用户id存放在session中 > > 用户头像存放在cookie中 (2)在实现类中实现接口的方法 ````java // 用户登录 User login(String username, String password); ```` (3)单元测试 #### 2.3 抽象方法的实现 ````java public User login(String username, String password) { // 根据用户名称查询用户数据是否存在 User result = userMapper.findByUserName(username); if (result == null || result.getIsDelete()==1) { throw new UsernameNotFoundException("用户数据不存在!"); } // 比较密码是否匹配 // (1)获取数据库中对应的密码 String oldPassword = result.getPassword(); // 和用户传递的密码进行比较 // (2)获取盐值 String salt = result.getSalt(); // (3)传递的密码加盐 String newMd5Password = getMD5Password(password, salt); // (4)比较 if (!newMd5Password.equals(oldPassword)) { throw new PasswordNotMatchException("密码错误!"); } // 数据中转:数据的压缩,提升系统性能 User user = new User(); user.setUid(result.getUid()); user.setUsername(result.getUsername()); user.setAvatar(result.getAvatar()); return user; } ```` ### 3、登录——控制层 业务层抛出的异常是什么,需要再统一异常处理类中进行统一的捕获和处理,如果业务层抛出的异常已经被处理过,则不需要重复添加 #### 3.1 处理异常 ````java else if (e instanceof InsertException) { result.setState(5000); result.setMessage("注册时产生未知异常!"); } else if (e instanceof UsernameNotFoundException) { result.setState(5001); result.setMessage("用户数据不存在异常!"); } ```` #### 3.2 设计请求 ```` 请求路径:/users/login 请求方式:POST 请求数据:String username, String password 响应结果:压缩后的User ```` #### 3.3 处理请求 ````java @RequestMapping("/login") public JsonResult login(String username, String password) { User data = userService.login(username, password); return new JsonResult(OK, data); } ```` ### 4、登录——前端页面 在login.html页面中发送ajax请求 ````js ```` # 四、用户登录会话session session对象主要存储在服务器端,可以用于保存服务器的临时数据的对象,所保存的数据可以在整个项目中都可以通过访问来获取,把session的数据看作一个共享的数据。首次登录时所获取到的数据,转移到session对象中即可。session.getAttrbute("key")可以将获取session中数据这种行为进行封装,封装在父类BaseController类中(因为在此项目结构中,只有在控制层才需要使用到session对象) 对session对象中数据的获取、数据的设置进行封装 * 数据的获取——封装在父类中 * 数据的设置——当用户登录成功后进行数据的设置,设置到全局的session对象 ````java /** * 获取session对象的uid * @param session 传入的session参数 * @return uid */ protected final Integer getUidFromSession(HttpSession session) { return Integer.valueOf(session.getAttribute("uid").toString()); } /** * 获取session对象的username * @param session session参数对象 * @return username */ protected final String getUsernameFromSession(HttpSession session) { return session.getAttribute("username").toString(); } ```` > 在登录的方法中,将数据封装在session对象中 > > 服务器本身自动创建一个全局的session对象。在SpringBoot中,直接使用session对象,直接将HttpSession类型的对象作为请求处理方法的参数,会自动将全局的session对象注入到请求处理方法的session型参上——修改控制层 # 五、拦截器 拦截器:首先将所有的请求统一拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统设置的过滤规则,统一的处理是重新打开login.html(重定向和转发,推荐使用重定向) 在SpringBoot项目中拦截器的定义和使用:SpringBoot是依靠SpringMVC来完成的,SpringMVC提供了一个HandlerInterceptor接口,用于表示定义一个拦截器。首先自定义一个类,实现该接口 ### 1、实现拦截器 ````java /** * 拦截所有请求,检测全局session对象中是否有uid数据,有则放行,没有则重定向至登录页面 * @param request 请求对象 * @param response 响应对象 * @param handler 处理器——url和Controller的映射 * @return 如果返回值为true,表示放行请求,如果返回值为false,表示拦截请求 * @throws Exception */ @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // HttpServletRequest对象获取session对象 Object uid = request.getSession().getAttribute("uid"); if (uid == null) { // 重定向 response.sendRedirect("/web/login.html"); // 结束后续调用 return false; } return true; } ```` ### 2、注册过滤器 #### 2.1 添加白名单、添加黑名单 * 白名单:login.html、register.html、login/reg/、index.html、product.html * 黑名单(用户登录状态下才能访问) #### 2.2 注册过滤器 借助WebMvcConfigure接口,可以将用户定义的拦截器进行注册,才能保证拦截器能够生效和使用。定义一个实现该接口的类。配置信息建议存放在项目的config包结构下 ````java /** * 处理器拦截器的注册 */ @Configuration // 加载当前的拦截器,并注册 public class LoginInterceptorConfigurer implements WebMvcConfigurer { /** * 将自定义的拦截器进行注册 * @param registry */ @Override public void addInterceptors(InterceptorRegistry registry) { // 创建拦截器对象 HandlerInterceptor interceptor = new LoginInterceptor(); // 配置白名单 List patterns = new ArrayList<>(); patterns.add("/bootstrap3/**"); patterns.add("/css/**"); patterns.add("/images/**"); patterns.add("/js/**"); patterns.add("/web/register.html"); patterns.add("/web/login.html"); patterns.add("/web/product.html"); patterns.add("/users/reg"); patterns.add("/users/login"); // 完成拦截器的注册 registry.addInterceptor(interceptor).addPathPatterns("/**").excludePathPatterns(patterns); } } ```` # 六、修改密码 ### 1、修改密码——持久层 #### 1.1 规划需要执行的SQL语句 根据用户的uid修改用户的password值 ````sql update t_user set password = ? , modifiled_user=? , modified_time=? where uid = ? ```` 根据uid查找用户:保证用户存在,未被删除且原始密码正确 ````sql select * from t_user where uid = ? ```` #### 1.2 设计接口和抽象方法 UserMapper接口,将以上两个方法抽象出来 ````java /** * 修改密码 * @param uid id * @param password 新密码 * @param modifiedUser 修改的执行者 * @param modifiedTime 修改的时间 * @return 受影响的行数 */ Integer updatePasswordByUid(Integer uid, String password, String modifiedUser, Date modifiedTime); /** * 根据用户id查询用户数据 * @param uid id * @return 返回查到的对象 */ User findByUid(Integer uid); ```` #### 1.3 映射 ````xml update t_user set password=#{password}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime}, where uid = ${uid} ```` #### 1.4 单元测试 ### 2、修改密码——业务层 #### 2.1 规划异常 (1)用户原密码错误,已经删除,uid用户找不到 (2)update在更新的时候,可能产生未知的异常,UpdateException #### 2.2 设计接口和抽象方法 执行修改密码 ````java void changePassword(Integer uid, String username, String oldPassword, String newPassword); ```` 业务层实现 ````java public void changePassword(Integer uid, String username, String oldPassword, String newPassword) { User result = userMapper.findByUid(uid); if (result == null || result.getIsDelete() == 1) { throw new UserNotFoundException("用户不存在!"); } // 比较 String oldMd5Password = getMD5Password(oldPassword, result.getSalt()); if (!result.getPassword().equals(oldMd5Password)) { throw new PasswordNotMatchException("原密码错误!"); } // 设置新密码 String newMd5Password = getMD5Password(newPassword, result.getSalt()); Integer rows = userMapper.updatePasswordByUid(uid, newMd5Password, username, new Date()); if (rows != 1) { throw new UpdateException("更新数据时产生未知异常!"); } } ```` 单元测试 ### 3、修改密码——控制层 #### 3.1 处理异常 业务层有异常就处理,新的异常需要配置在统一的异常处理方法中 ````java else if (e instanceof UpdateException) { result.setState(5003); result.setMessage("更新数据时产生未知异常!"); } ```` #### 3.2 设计请求 ```` 请求路径:/users/change_password 请求方式:POST 请求数据:String oldPassword, String newPassword, HttpSession session 响应结果:JsonResult ```` #### 3.3 处理请求 ````java @RequestMapping("/change_password") public JsonResult changePassword(String oldPassword, String newPassword, HttpSession session) { Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); userService.changePassword(uid, username, oldPassword, newPassword); return new JsonResult<>(OK); } ```` ### 4、修改密码——前端页面 在password.html中添加ajax请求的处理 # 七、个人资料 ### 1、个人资料——持久层 #### 1.1 规划SQL语句 (1)更新用户信息 ````mysql update t_user set phone=?, email=?, gender=?, modified_user=?, modified_time=? where uid = ? ```` (2)根据用户名查询用户的数据 ````mysql select * from t_user where uid = ? ```` > 查询用户不用重复开发,但是思路不能少 #### 1.2 设计接口和抽象方法 ````java Integer updateInfoByUid(User user); ```` #### 1.3 映射 ````java UPDATE t_user SET phone = #{phone}, email = #{email}, gender = #{gender}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} WHERE uid = ${uid} ```` #### 1.4 功能测试 ### 2、个人资料——业务层 #### 2.1 规划异常 (1)涉及两个功能 * 当打开页面时,获取用户的信息,并且填充到文本框中 * 在页面上检测用户是否点击了修改按钮,如果检测到则执行修改用户信息的操作 (2)异常 打开页面的时候可能找不到用户的数据,点击删除按钮之前需要再次去检测用户的数据是否存在 #### 2.2 接口和抽象方法 ``` // 根据用户id查询用户数据 User getByUid(Integer uid); // 修改用户信息 void changeInfo(Integer uid, String username, User user); ``` #### 2.3 抽象方法的实现 ````java @Override public User getByUid(Integer uid) { User result = userMapper.findByUid(uid); if (result == null || result.getIsDelete() == 1) { throw new UserNotFoundException("用户未找到!"); } // 压缩数据 User user = new User(); user.setUsername(result.getUsername()); user.setPhone(result.getPhone()); user.setEmail(result.getEmail()); user.setGender(result.getGender()); return null; } @Override public void changeInfo(Integer uid, String username, User user) { User result = userMapper.findByUid(uid); if (result == null || result.getIsDelete() == 1) { throw new UserNotFoundException("用户未找到!"); } user.setUid(uid); //user.setUsername(username); user.setModifiedUser(username); user.setModifiedTime(new Date()); Integer rows = userMapper.updateInfoByUid(user); if (rows != 1) { throw new UpdateException("更新数据时发生未知异常!"); } } ```` #### 2.4 单元测试 ### 3、个人资料——控制层 #### 3.1 处理异常 > 暂无 #### 3.2 设计请求 (1)设置只要打开页面就发送当前用户数据的查询 ```` 请求路径:/users/get_bu_uid 请求方式:GET 请求数据:HttpSession session 响应结果:JsonResult ```` (2)点击修改按钮发送修改用户数据的修改操作请求设计 ```` 请求路径:/users/change_info 请求方式:POST 请求数据:User user, HttpSession session 响应结果:JsonResult ```` #### 3.3 处理请求 ````java @RequestMapping("/get_by_uid") public JsonResult getByUid(HttpSession session) { User data = userService.getByUid(getUidFromSession(session)); return new JsonResult<>(OK, data); } @RequestMapping("/change_info") public JsonResult changeInfo(User user, HttpSession session) { userService.changeInfo(getUidFromSession(session), getUsernameFromSession(session), user); return new JsonResult<>(OK); } ```` ### 4、个人资料——前端页面 (1)在打开userdata.html页面自动发送ajax请求(get_by_uid),查询到的数据填充到这个页面 (2)在检测到用户点击了修改按钮之后发送一个ajax请求(change_info) ````js ```` # 八、上传头像 ### 1、上传头像——持久层 #### 1.1 SQL语句的规划 将对象文件保存在操作系统上,然后把这个文件路径给记录下来,因为在记录路径的时候是非常便捷和方便的,将来如果要打开该问价可以依据路径去找到该文件。在数据库中保存文件路径即可,实际开发环境中一般将静态资源(图片、文件、其他资源文件)放在某台电脑上,把这个电脑作为一个单独的服务器使用。 上传头像,即对应更新用户avatar字段的SQL语句 ````mysql update t_user set avatar=?, modified_user=?, modified_time=? where uid= ? ```` #### 1.2 设计接口和抽象方法 UserMapper接口中定义抽象方法用于修改用户头像 > @Param("SQL映射文件中#{}占位符对应的变量名") > > 解决的问题:当SQL语句的占位符和映射接口的方法的参数名不一致时,需要将某个参数强行注入到某个占位符变量上时,或参数过多找不到对应参数时,用@Param(绑定)标注映射关系 ````java /** * 根据用户uid修改用户头像 * @param uid 用户id * @param avatar 头像资源地址——反序列化 * @param modifiedUser 修改人 * @param modifiedTime 修改时间 * @return 受影响行数 */ Integer updateAvatarByUid(@Param("uid") Integer uid, @Param("avatar") String avatar, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime); ```` #### 1.3 接口的映射 ````xml UPDATE t_user SET avatar = #{avatar}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} WHERE uid = #{uid} ```` #### 1.4 单元测试 ### 2、上传头像——业务层 #### 2.1 规划异常 (1)用户数据不存在,找不到对应的用户数据 (2)更新的时候,产生未知异常 > 无需开发 #### 2.2 设计接口和抽象方法 ````java void changeAvatar(Integer uid, String avatar, String username); ```` #### 2.3 抽象方法的实现 ````java @Override public void changeAvatar(Integer uid, String avatar, String username) { User result = userMapper.findByUid(uid); if (result == null || result.getIsDelete() == 1) { throw new UserNotFoundException("用户未找到!") } Integer rows = userMapper.updateAvatarByUid(uid, avatar, username, new Date()); if (rows != 1) { throw new UpdateException("更新头像时产生未知异常!"); } } ```` #### 2.4 单元测试 ### 3、上传头像——控制层 #### 3.1 规划异常 ```` 文件异常的父类: FileUploadException extends RunTimeException 泛指文件上传的异常 具体文件异常: FileEmptyException 文件为空的异常 FileSizeException 文件大小超出限制 FileTypeException 文件类型异常 FileUploadIOException 文件读写的异常 ```` #### 3.2 处理异常 ````java @ExceptionHandler({ServiceException.class, // 用于统一处理抛出的异常:凡是括号内的异常都拦截到这里处理 FileUploadException.class}) // 增加参数 public JsonResult handleException(Throwable e) { ...; else if (e instanceof FileEmptyException) { result.setState(6000); result.setMessage("上传的文件为空的异常!"); } else if (e instanceof FileSizeException) { result.setState(6001); result.setMessage("上传的文件的大小超出了限制值!"); } else if (e instanceof FileTypeException) { result.setState(6002); result.setMessage("上传的文件类型超出了限制!"); } else if (e instanceof FileStateException) { result.setState(6003); result.setMessage("上传的文件状态异常!"); } else if (e instanceof FileUploadIOException) { result.setState(6004); result.setMessage("上传文件时读写异常!"); } } ```` #### 3.3 设计请求 ```` 请求路径:/users/change_avatar 请求方式:POST(GET提交数据2KB左右) 请求数据:HttpSession session, MutipartFile file 响应结果:JsonResult ```` #### 3.3 处理请求 > (头像图片的地址): C:\Users\CHArbe\AppData\Local\Temp\tomcat-docbase.8080.406424320104424952\upload ````java // MultipartFile接口是由SpringMVC提供的一个接口,包装了获取文件类型的数据(任何类型的file都可以接收) @RequestMapping("/change_avatar") public JsonResult changeAvatar(HttpSession session, @RequestParam("file") 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("文件类型不支持!"); } // 上传文件地址:../upload/文件.png String parent = session.getServletContext().getRealPath("upload"); // 创建一个file指向路径,判断文件是否存在 File dir = new File(parent); if (!dir.exists()) { dir.mkdirs(); // 创建当前目录 } //System.out.println(dir); C:\Users\CHArbe\AppData\Local\Temp\tomcat-docbase.8080.406424320104424952\upload // 获取文件名称,UUID工具类生成一个新的字符串作为文件名 String originalFilename = file.getOriginalFilename(); System.out.println("filename:" + originalFilename); int index = originalFilename.lastIndexOf("."); // 文件后缀 String suffix = originalFilename.substring(index); String filename = UUID.randomUUID().toString().toUpperCase() + suffix; // 创建空文件 File dest = new File(dir, filename); // 将file中数据写入到空文件中 try { file.transferTo(dest); } catch (FileStateException e) { throw new FileStateException("文件状态异常!"); } catch (IOException e) { throw new FileUploadIOException("文件读写异常!"); } // 返回的头像路径 String avatar = "/upload" + filename; userService.changeAvatar(getUidFromSession(session), getUsernameFromSession(session), avatar); return new JsonResult<>(OK, avatar); } ```` ### 4、上传头像——前端页面 在uplaod.html中,使用表单进行文件的商户采纳,需要给表单添加属性 ````html
```` ### 5、上传头像——前端页面优化 #### 5.1 更改默认的大小限制 SpringMVC默认1MB文件可以进行上传 ```` org.apache.tomcat.util.http.fileupload.impl.FileSizeLimitExceededException: The field file exceeds its maximum permitted size of 1048576 bytes. ```` 方式一:在配置文件中进行配置 ````properties spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=15MB ```` 方式二:采用Java代码的形式来设置文件的上传大小的设置。 * 主类中进行配置,可以定义一个方法,必须使用@Bean注解进行注入 * 在此类的前面添加@Configration注解进行注入Spring * 该方法返回值必须是MultipartConfigElement类型 ````java @Configuration // 表示配置类 @SpringBootApplication // MapperScan注解指定当前项目中的Mapper接口路径的指定位置,在项目启动的时候自动加载接口文件 @MapperScan("com.cy.store.mapper") public class StoreApplication { public static void main(String[] args) { SpringApplication.run(StoreApplication.class, args); } @Bean public MultipartConfigElement getMultipartConfigElement() { // 创建一个配置的工厂类对象 MultipartConfigFactory factory = new MultipartConfigFactory(); // 设置需要创建的对象的相关信息 factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES)); factory.setMaxRequestSize(DataSize.of(15, DataUnit.MEGABYTES)); // 通过工厂类来创建MultipartConfigElement对象 return factory.createMultipartConfig(); } } ```` #### 5.2 显示头像 在页面中通过ajax的请求来提交文件,提交完成后返回json串,解析出data中的数据,设置到img头像标签的src属性 * serialize():可以将表达那数据自动拼接成key=value的结构进行提交给服务器,一般提交的是普通的控件类型中的数据(text\password\radio\checkbox)等等 * FormData类:将表单中的数据保持原有的结构进行数据的提交 * new FromData($("#form")[0]):将form表单中的某一个元素的整体值作为form的数据放入其对象之中 * ajax默认处理数据是按照字符串的形式进行处理,以及默认采用字符串的形式进行提交数据,需要关闭这两个功能 #### 5.3 登陆后显示头像 更新头像成功后,将服务器返回的头像返回在客户端的cookie都西昂中,每次检测到用户打开上传头像页面,在这个页面中通过ready()方法来自动检测去读取cookie中的头像并设置到src属性上 (1)设置cookie中的值 导入cookie.js的文件 ````html ```` 调用cookie的方法 ````js // 将服务器端返回的头像设置到cookie中 $.cookie("avatar", json.data.avatar, {expires: 7}); ```` (2)自动读取 导入cookie.js的文件 ````js ```` 通过ready()自动读取cookie中的值 ````js $(document).ready(function () { let avatar = $.cookie("avatar"); $("#img-avatar").attr("src", avatar); }); ```` #### 5.4 显示最新的头像 在修改完头像后,将最新的头像地址再次保存到cookie中 ````js // cookie中图片地址更新 $.cookie("avatar", json.data, {expires: 7}); ```` # 九、新增收货地址 实体类 ````java /** * 收货地址 */ @Data @ToString @EqualsAndHashCode @AllArgsConstructor @NoArgsConstructor public class Address { 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; } ```` ### 1、新增收货地址——持久层 #### 1.1 各功能的开发顺序 当前收货地址功能模块:列表的展示、修改、删除、设置默认、新增收货地址。 开发顺序:新增收货地址、列表的展示、设置默认收货地址、删除收货地址修改收货地 #### 1.2 规划需要执行的SQL语句 (1)插入语句: ````sql insert into t_address (处理aid外字段列表) values (字段值列表) ```` (2)一个用户的收获地址规定最多只能有20条数据,在插入用户数据之前要做查询操作 ````sql select count(*) from t_address where uid = ? ```` #### 1.3 接口和抽象方法 (1)创建一个新的接口Address,在这个接口中定义上面两个SQL语句的抽象方法的定义 ````java /** * 收货地址的接口和抽象方法 */ public interface AddressMapper { /** * 插入用户的收获地址数据 * @param address 收货地址数据 * @return 受影响的行数 */ Integer insert(Address address); /** * 根据uid统计收获地址的数量 * @param uid 用户uid * @return 当前用户地址的总数量 */ Integer countByUid(Integer uid); } ```` #### 1.4 配置SQL映射 ````xml 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} ) ```` ### 2、新增收货地址——业务层 #### 2.1 规划异常 > 如果用户是第一次插入用户的收货地址,规则:当用户插入的地址是第一条时,需要将当前地址作为默认的收货地址,如果查询的统计总数为0,则将当前地址的is_default值设置为1。 > > 另,查询统计的结果为0,不代表异常 > > 查询到的结果大于20,这时需要抛出业务控制的异常AddressCountLimitException异常(自建) ````java /** * 收货地址超出限制的异常:20条 */ public class AddressCountLimitException extends ServiceException { ...; } ```` #### 2.2 接口和抽象方法 (1)创建IAddressService接口,定义业务中的抽象方法 ````java /** * 收货地址业务层接口 */ public interface IAddressService { void addNewAddress(Integer uid, String username, Address address); } ```` (2)创建一个AddressServiceImpl类,实现IAddressService接口中的抽象方法 application.properrties ````properties # Spring读取配置文件中的数据:@Value(${user.address.max-count}) user.address.max-count=20 ```` ````java /** * 新增收货地址的实现类 */ public class AddressServiceImpl implements IAddressService { @Autowired private AddressMapper addressMapper; @Value("${user.address.max-count}") private Integer maxCount; @Override public void addNewAddress(Integer uid, String username, Address address) { Integer count = addressMapper.countByUid(uid); if (count >= maxCount) { throw new AddressCountLimitException("用户收货地址超出上限!"); } } } ```` #### 2.3 抽象方法的实现 ````java @Override public void addNewAddress(Integer uid, String username, Address address) { Integer count = addressMapper.countByUid(uid); if (count >= maxCount) { throw new AddressCountLimitException("用户收货地址超出上限!"); } // uid, isDelete address.setUid(uid); Integer isDefault = count == 0 ? 1 : 0; address.setIsDefault(isDefault); // 不全4项日志 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("插入用户收货地址发生未知异常!"); } } ```` #### 2.4 单元测试 ### 3、新增收货地址——控制层 #### 3.1 处理异常 业务层抛出收货地址总数超标的异常,在BaseController中进行处理 ```` else if (e instanceof AddressCountLimitException) { result.setState(4003); result.setMessage("用户的收货地址超出上限!"); } ```` #### 3.2 设计请求 ```` url: /addresses/add_new_address 请求方式: POST 请求数据: Address adress, HttpSession session 响应结果: JsonResult ```` #### 3.3 处理请求 在控制层创建AddressController来处理用户收货地址的请求和响应 ````java @RequestMapping("/addresses") @RestController // 以字符串的形式响应 public class AddressController extends BaseController { @Autowired private IAddressService addressService; @RequestMapping("/add_new_address") public JsonResult addNewAddress(Address address, HttpSession session) { Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); addressService.addNewAddress(uid, username, address); return new JsonResult<>(OK); } } ```` #### 3.4 单元测试 ### 4、新增收货地址——前端页面 ````js // (1)监听注册按钮是否被点击 $("#btn-add-new-address").click(function () { // (2)发送ajax的异步请求 $.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.status) } }) }); ```` # 十、获取省市区列表 ### 1、建立数据库t_dict_district > 见tools ### 2、获取省市区列表的实体类 ````java /** * 省市区列表实体类 */ @Data @ToString @EqualsAndHashCode public class District extends BaseEntity { private Integer id; private String parent; private String code; private String name; } ```` ### 3、获取省市区列表——持久层 #### 3.1 规划SQL语句 查询语句,根据父代号进行查询 ````sql // order by code ASC :升序 DESC:降序 select * from t_dict_district where parent=? ORDER BY code ASC ```` #### 3.2 接口和抽象方法 ````java public interface DistrictMapper { /** * 根据父代号查询区域信息,返回该代号内所有区域列表 * @param parent * @return */ List findByParent(Integer parent); } ```` #### 3.3 配置SQL映射 ````xml getByParent(String parent); } ```` #### 4.2 抽象方法的实现 ````java @Service public class DistrictServiceImpl implements IDistrictService { @Autowired private DistrictMapper districtMapper; @Override public List getByParent(String parent) { List list = districtMapper.findByParent(parent); // 在进行网络数据传输时,为了避免无效数据的传递,可以将无效数据设置为null // 可以节省流量,提高效率 for (District d : list) { d.setId(null); d.setParent(null); } return list; } } ```` #### 4.3 单元测试 ### 5、获取省市区列表——控制层 #### 5.1 设计请求 ```` url: /districts/ 请求方式: GET 请求数据: String parent 响应结果: JsonResult> ```` #### 5.2 处理请求 ````java @RequestMapping("/districts") @RestController public class DistrictController extends BaseController { @Autowired private IDistrictService districtService; @RequestMapping("/") public JsonResult> getByParent(String parent) { List data = districtService.getByParent(parent); return new JsonResult<>(OK, data); } } ```` > 加入白名单 ```java patterns.add("/district/"); ``` #### 5.3 单元测试 ### 6、获取省市区列表——前端页面 (1)注释掉原有的通过js来完成省市区列表加载的js代码 ````html ```` (2)检查前端页面在提交省市区数据时是否有相关name属性和id属性 # 十一、获取省市区的名称 ### 1、持久层 (1)规划SQL语句 ````sql seelct * from t_dist_district where code = ? ```` (2)接口和抽象方法 ````java String findNameByCode(String code); ```` (3)映射 ````xml ```` (4)单元测试 ### 2、业务层 (1)接口和抽象方法 ````java String getNameByCode(String code); ```` (2)抽象方法的实现 ````java @Override public String getNameByCode(String code) { return districtMapper.findNameByCode(code); } ```` (3)单元测试 > 超过八行代码的都需要进行独立的单元测试 ### 3、业务层优化 (1)新增收货地址层还依赖于DistrictService层 ````java @Service public class AddressServiceImpl implements IAddressService { @Autowired private AddressMapper addressMapper; @Autowired private IDistrictService districtService; ...; } ```` (2)在addNewAddress方法中将districtService接口中获取到的省市区数据转移到address都西昂,这个对象就包含了所有的用户收货地址的数据 ````java // address对象中的数据进行不全:省市区 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); ```` ### 4、前端页面 (1)addAddress.html页面中编写对应的省市区展示及根据用户的不同选择来显示对应的标签中方的内容 (2)前端代码 ````js ```` # 十二、收货地址列表展示 ### 1、持久层 (1)规划SQL语句 ````sql select * from t_address where uid = ? order by is_default DESC, created_time DESC ```` (2)接口和抽象方法 ````java /** * 根据用户id查询用户的收货地址数据 * @param uid * @return */ List
findByUid(Integer uid); ```` (3)配置映射 ````xml ```` (4)单元测试 ### 2、业务层 (1)接口和抽象方法 ````java List
getByUid(Integer uid); ```` (2)抽象方法的实现 ````java @Override public List
getByUid(Integer uid) { List
list = addressMapper.findByUid(uid); // 数据压缩 for (Address address : list) { address.setProvinceCode(null); address.setCityCode(null); address.setAreaCode(null); address.setZip(null); address.setTel(null); address.setIsDefault(null); address.setCreatedTime(null); address.setCreatedUser(null); address.setModifiedTime(null); address.setModifiedUser(null); } return null; } ```` ### 3、控制层 (1)设计请求 ```` url: /addresses 请求方式: GET 请求数据:Httpsession session 响应结果:JsonResult> ```` (2)处理请求 ````java @RequestMapping({"", "/"}) public JsonResult> getByUid(HttpSession session) { Integer uid = getUidFromSession(session); List
data = addressService.getByUid(uid); return new JsonResult<>(OK, data); } ```` ### 4、前端页面 ````html ```` # 十三、设置默认收货地址 ### 1、持久层 (1)规划SQL语句 检测当前用户想设置的默认收货地址这条数据是否存在 ````sql select * from t_address aid = ? ```` 在修改用户的收货默认地址之前,先将所有的收货地址设置为非默认 ````sql update t_address set -s_default = 0 where uid = ? ```` 将用户选中的目录设置为默认收货地址 ````sql update t_address set is_default = 1, modified_user = ?, modified_time = ? where aid = ? ```` (2)设计接口和抽象方法 ````java /** * 根据adi查询收货地址数据 * @param aid * @return */ Address findByAid(Integer aid); /** * 根据用户uid修改用户的所有收货地址为非默认 * @param uid * @return */ Integer updateNonDefault(Integer uid); /** * 根据aid修改默认收货地址 * @param aid * @return */ Integer updateDefaultByAid(@Param("aid") Integer aid,@Param("modifiedUser") String modifiedUser,@Param("modifiedTime") Date modifiedTime); ```` (3)映射 ````xml UPDATE t_address SET is_default = 0 Where uid = #{uid} UPDATE t_address SET is_default = 1, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} WHERE aid = #{aid} ```` (4)单元测试 ### 2、业务层 (1)异常规划 * 在执行更新时产生未知的UpdateException异常,已经创建无需重复创建 * 访问的数据不是当前登录用户的收货地址数据,非法访问:AccessDeniedException * 收货地址不存在的异常:AddressNotFoundException (2)接口和抽象方法 ````java void setDefault(Integer aid, Integer uid, String username); ```` (3)抽象方法的实现 ````java @Override public void setDefault(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.updateNonDefault(uid); if (rows < 1) { throw new UpdateException("更新数据产生未知异常!"); } // 将用户选中的地址设置未默认收货地址 rows = addressMapper.updateDefaultByAid(aid, username, new Date()); if (rows != 1) { throw new UpdateException("更新数据产生未知异常!"); } } ```` (4)单元测试 ### 3、控制层 (1)处理异常 ````java else if (e instanceof AddressNotFoundException) { result.setState(4004); result.setMessage("用户的收货地址数据不存在!"); } else if (e instanceof AccessDeniedException) { result.setState(4005); result.setMessage("收货地址数据非法访问!"); } ```` (2)设计请求 ```` /addresses/{aid}/set_default GET @PathVariable("aid") Integer aid, HttpSession session JsonResult ```` (3)处理请求 ````java @RequestMapping("/{aid}/set_default") public JsonResult setDefault(@PathVariable("aid") Integer aid, HttpSession session) { addressService.setDefault(aid, getUidFromSession(session), getUsernameFromSession(session)); return new JsonResult<>(OK); } ```` (4)单元测试 > 单元测试:打开浏览器登录,访问请求路径localhost:8080/addresses/12/set_default ### 4、前端页面 给设置默认收货地址按钮添加一个onclick属性,指向一个方法的调用,在这个方法中完成ajax请求 ````html ```` # 十四、删除收货地址 ### 1、持久层 (1)规划SQL语句 * 判断该数据是否存在——findByAid * 判断该条数据的归属是否属于当前用户 * 执行删除收货地址的信息 ````sql DELETE * FROM t_address where aid = ? ```` * 如果用户删除默认收货地址,将剩下的地址中的某一条设置未默认,规则自定义:最新的地址 ````sql # limit (n-1)*n, pageSize select * from t_address where uid = ? order by modified_time DESC limit 0, 1 ```` * 在执行以上操作之前,还需检查该用户的收货地址数据的数量,如果删除的收货地址是最后一条收货地址,则删除成功后无需再执行其他操作。统计收货地址数量的功能此前已经完成,无需再次开发。 (2)接口和抽象方法 ````sql /** * 删除地址 */ Integer deleteByAid(Integer aid); /** * 查询最新的地址数据 */ Address findLastModified(Integer uid); ```` (3)配置映射 ````xml DELETE * FROM t_address WHERE aid = #{aid}; ```` (4)单元测试 ### 2、业务层 (1)规划异常 在执行删除操作时,可能会删除数据失败,此时抛出DeleteException异常。在创建com.cy.store.service.ex.DeleteException异常类,并继承自ServiceException类。 ````java package com.cy.store.service.ex; /** 删除数据失败的异常 */ public class DeleteException extends ServiceException { // Override Methods... } ```` (2)接口和抽象方法 ````java void deleteAddress(Integer aid, Integer uid, String username); ```` (3)方法的实现 ````java @Override public void deleteAddress(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("删除数据产生未知的异常!"); } Integer count = addressMapper.countByUid(uid); if (count == 0) { return; } // 如果是默认地址,更新默认地址 if (result.getIsDefault() == 1) { // 将最新数据is_default设置为1 Address address = addressMapper.findLastModified(uid); rows = addressMapper.updateDefaultByAid(address.getAid(), username, new Date()); if (rows != 1) { throw new UpdateException("更新数据时产生未知的异常!"); } } } ```` (4)单元测试 ### 3、控制层 (1)处理异常 ````java else if (e instanceof DeleteException) { result.setState(5002); } ```` (2)设计请求 ```` /addresses/{aid}/delete POST @PathVariable("aid") Integer aid, HttpSession session JsonResult ```` (3)处理请求 ````java @RequestMapping("/{aid}/delete") public JsonResult deleteAddress(@PathVariable("aid") Integer aid, HttpSession session) { addressService.deleteAddress(aid, getUidFromSession(session), getUsernameFromSession(session)); return new JsonResult<>(OK); } ```` ### 4、前端页面 (1)给showAddressList()方法中的“设为默认”超链接按钮添加设置默认收货地址的点击事件。 ```javascript 删除 ``` (2) ````javascript function deleteByAid(aid) { $.ajax({ url: "/addresses/" + aid + "/delete", type: "POST", dataType: "JSON", success: function(json) { if (json.state == 200) { showAddressList(); } else { alert("删除收货地址失败!" + json.message); } }, error: function(json) { alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + json.status); location.href = "login.html"; } }); } ```` # 十五、商品热销排行 ### 1、前置准备 (1)创建数据表 ````mysql CREATE TABLE t_product ( id int(20) NOT NULL COMMENT '商品id', category_id int(20) DEFAULT NULL COMMENT '分类id', item_type varchar(100) DEFAULT NULL COMMENT '商品系列', title varchar(100) DEFAULT NULL COMMENT '商品标题', sell_point varchar(150) DEFAULT NULL COMMENT '商品卖点', price bigint(20) DEFAULT NULL COMMENT '商品单价', num int(10) DEFAULT NULL COMMENT '库存数量', image varchar(500) DEFAULT NULL COMMENT '图片路径', status int(1) DEFAULT '1' COMMENT '商品状态 1:上架 2:下架 3:删除', priority int(10) DEFAULT NULL COMMENT '显示优先级', created_time datetime DEFAULT NULL COMMENT '创建时间', modified_time datetime DEFAULT NULL COMMENT '最后修改时间', created_user varchar(50) DEFAULT NULL COMMENT '创建人', modified_user varchar(50) DEFAULT NULL COMMENT '最后修改人', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ```` (2)创建实体类 ````java /** * 商品实体类 */ @Data @ToString @EqualsAndHashCode public class Product extends BaseEntity implements Serializable { private Integer id; private Integer categoryId; private String itemType; private String title; private String sellPoint; private Long price; private Integer num; private String image; private Integer status; private Integer priority; } ```` ### 2、持久层 (1)规划SQL语句 查询热销商品列表的SQL语句 ````sql select * from t_product where status=1 order by priority DESC limit 0,4 ```` (2)接口和抽象方法 ````java /** 处理商品数据的持久层接口 */ public interface ProductMapper { /** * 查询热销商品的前四名 * @return 热销商品前四名的集合 */ List findHotList(); } ```` (3)映射 ````xml ```` ### 3、业务层 (1)规划异常——无异常 (2)接口和抽象方法 ````java /** 处理商品数据的业务层接口 */ public interface IProductService { /** * 查询热销商品的前四名 * @return 热销商品前四名的集合 */ List findHotList(); } ```` (3)方法实现 ````java /** 处理商品数据的业务层实现类 */ @Service public class ProductServiceImpl implements IProductService { @Autowired private ProductMapper productMapper; @Override public List findHotList() { List list = productMapper.findHotList(); for (Product product : list) { product.setPriority(null); product.setCreatedUser(null); product.setCreatedTime(null); product.setModifiedUser(null); product.setModifiedTime(null); } return list; } } ```` (4)单元测试 ### 4、控制层 (1)处理异常——无 (2)设计请求 ```` 请求路径:/products/hot_list 请求参数:无 请求类型:GET 响应结果:JsonResult> 是否拦截:否,需要将index.html和products/**添加到白名单 ```` (3)处理请求 ````java @RestController @RequestMapping("products") public class ProductController extends BaseController { @Autowired private IProductService productService; } ```` ### 5、前端页面 (1)在index.html页面给“热销排行”列表的div标签设置id属性值 ````html
```` (2)在index.html页面中body标签内部的最后,添加展示热销排行商品的代码 ````javascript ```` # 十六、显示商品详情 ### 1、持久层 (1)规划SQL语句 ````sql SELECT * FROM t_product WHERE id=? ```` (2)接口与抽象方法 ````java Product findById(Integer id); ```` (3)配置SQL映射 ````xml ```` ### 2、业务层 (1)规划异常 如果商品数据不存在,应该抛出ProductNotFoundException,需要创建com.cy.store.service.ex.ProductNotFoundException异常。 ```java package com.cy.store.service.ex; /** 商品数据不存在的异常 */ public class ProductNotFoundException extends ServiceException { // Override Methods... } ``` (2)接口与抽象方法 ````java Product findById(Integer id); ```` (3)实现抽象方法 ````java @Override public Product findById(Integer id) { // 根据参数id调用私有方法执行查询,获取商品数据 Product product = productMapper.findById(id); // 判断查询结果是否为null if (product == null) { // 是:抛出ProductNotFoundException throw new ProductNotFoundException("尝试访问的商品数据不存在"); } // 将查询结果中的部分属性设置为null product.setPriority(null); product.setCreatedUser(null); product.setCreatedTime(null); product.setModifiedUser(null); product.setModifiedTime(null); // 返回查询结果 return product; } ```` ### 3、控制层 (1)处理异常 在BaseController类中的handleException()方法中添加处理ProductNotFoundException的异常。 ```java // ... else if (e instanceof ProductNotFoundException) { result.setState(4006); } // ...设计请求 ``` (2)设计用户提交的请求,并设计响应的方式。 请求路径:/products/{id}/details 请求参数:@PathVariable("id") Integer id 请求类型:GET 响应结果:JsonResult (3)处理请求 在ProductController类中添加处理请求的getById()方法。 ```java @GetMapping("{id}/details") public JsonResult getById(@PathVariable("id") Integer id) { // 调用业务对象执行获取数据 Product data = productService.findById(id); // 返回成功和数据 return new JsonResult(OK, data); } ``` (4)前端页面 1.检查在product.html页面body标签内部的最后是否引入jquery-getUrlParam.js文件,如果引入无需重复引入。 ```java ``` 2.在product.html页面中body标签内部的最后添加获取当前商品详情的代码。 ```javascript ``` 3.完成后启动项目,先访问http://localhost:8080/web/index.html页面,然后点击“热销排行”中的某个子项,将跳转到product.html商品详情页,观察页面是否加载的是当前的商品信息。 # 十七、加入购物车 ### 1 购物车-创建数据表 在store数据库中创建t_cart用户数据表。 ```mysql CREATE TABLE t_cart ( cid INT AUTO_INCREMENT COMMENT '购物车数据id', uid INT NOT NULL COMMENT '用户id', pid INT NOT NULL COMMENT '商品id', price BIGINT COMMENT '加入时商品单价', num INT COMMENT '商品数量', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', PRIMARY KEY (cid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ### 2 购物车-创建实体类 在com.cy.store.entity包下创建购物车的Cart实体类。 ```java package com.cy.store.entity; import java.io.Serializable; /** 购物车数据的实体类 */ public class Cart extends BaseEntity implements Serializable { private Integer cid; private Integer uid; private Integer pid; private Long price; private Integer num; // Generate: Getter and Setter、Generate hashCode() and equals()、toString() } ``` ### 3 购物车-添加购物车-持久层 #### 3.1 规划需要执行的SQL语句 向购物车表中插入商品数据的SQL语句大致是: ```mysql insert into t_cart (除了cid以外的字段列表) values (匹配的值列表); ``` 如果用户曾经将某个商品加入到购物车过,则点击“加入购物车”按钮只会对购物车中相同商品数量做递增操作。 ```mysql update t_cart set num=? where cid=? ``` 关于判断“到底应该插入数据,还是修改数量”,可以通过“查询某用户是否已经添加某商品到购物车”来完成。如果查询到某结果,就表示该用户已经将该商品加入到购物车了,如果查询结果为null,则表示该用户没有添加过该商品。 ```mysql select * from t_cart where uid=? and pid=? ``` #### 3.2 接口与抽象方法 在com.cy.store.mapper包下创建CartMapper接口,并添加抽象相关的方法。 ```java package com.cy.store.mapper; import com.cy.store.entity.Cart; import org.apache.ibatis.annotations.Param; import java.util.Date; /** 处理购物车数据的持久层接口 */ public interface CartMapper { /** * 插入购物车数据 * @param cart 购物车数据 * @return 受影响的行数 */ Integer insert(Cart cart); /** * 修改购物车数据中商品的数量 * @param cid 购物车数据的id * @param num 新的数量 * @param modifiedUser 修改执行人 * @param modifiedTime 修改时间 * @return 受影响的行数 */ Integer updateNumByCid( @Param("cid") Integer cid, @Param("num") Integer num, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime); /** * 根据用户id和商品id查询购物车中的数据 * @param uid 用户id * @param pid 商品id * @return 匹配的购物车数据,如果该用户的购物车中并没有该商品,则返回null */ Cart findByUidAndPid( @Param("uid") Integer uid, @Param("pid") Integer pid); } ``` #### 3.3 配置SQL映射 1.在resources.mapper文件夹下创建CartMapper.xml文件,并在文件中配置以上三个方法的映射。 ```xml INSERT INTO t_cart (uid, pid, price, num, created_user, created_time, modified_user, modified_time) VALUES (#{uid}, #{pid}, #{price}, #{num}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime}) UPDATE t_cart SET num=#{num}, modified_user=#{modifiedUser}, modified_time=#{modifiedTime} WHERE cid=#{cid} ``` 2.在com.cy.store.mapper包下创建CartMapperTests测试类,并添加测试方法。 ```java package com.cy.store.mapper; import com.cy.store.entity.Cart; import com.cy.store.entity.Product; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.Date; import java.util.List; @RunWith(SpringRunner.class) @SpringBootTest public class CartMapperTests { @Autowired private CartMapper cartMapper; @Test public void insert() { Cart cart = new Cart(); cart.setUid(1); cart.setPid(2); cart.setNum(3); cart.setPrice(4L); Integer rows = cartMapper.insert(cart); System.out.println("rows=" + rows); } @Test public void updateNumByCid() { Integer cid = 1; Integer num = 10; String modifiedUser = "购物车管理员"; Date modifiedTime = new Date(); Integer rows = cartMapper.updateNumByCid(cid, num, modifiedUser, modifiedTime); System.out.println("rows=" + rows); } @Test public void findByUidAndPid() { Integer uid = 1; Integer pid = 2; Cart result = cartMapper.findByUidAndPid(uid, pid); System.out.println(result); } } ``` ### 4 购物车-添加购物车-业务层 #### 4.1 规划异常 在插入数据时,可能抛出InsertException异常;在修改数据时,可能抛出UpdateException异常。如果不限制购物车中的记录的数量,则没有其它异常。 #### 4.2 接口与抽象方法 在com.cy.store.service包下创建ICartService接口,并添加抽象方法。 ```java package com.cy.store.service; /** 处理商品数据的业务层接口 */ public interface ICartService { /** * 将商品添加到购物车 * @param uid 当前登录用户的id * @param pid 商品的id * @param amount 增加的数量 * @param username 当前登录的用户名 */ void addToCart(Integer uid, Integer pid, Integer amount, String username); } ``` #### 4.3 实现抽象方法 1.创建com.cy.store.service.impl.CartServiceImpl类,并实现ICartService接口,并在类的定义前添加@Service注解。在类中声明CartMapper持久层对象和IProductService处理商品数据的业务对象,并都添加@Autowired注修饰。 ```java package com.cy.store.service.impl; import com.cy.store.service.ICartService; import com.cy.store.service.IProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** 处理购物车数据的业务层实现类 */ @Service public class CartServiceImpl implements ICartService { @Autowired private CartMapper cartMapper; @Autowired private IProductService productService; } ``` 2.在CartServiceImpl类中实现业务层ICartService接口中定义的抽象方法。 ```java @Override public void addToCart(Integer uid, Integer pid, Integer amount, String username) { // 根据参数pid和uid查询购物车中的数据 // 判断查询结果是否为null // 是:表示该用户并未将该商品添加到购物车 // -- 创建Cart对象 // -- 封装数据:uid,pid,amount // -- 调用productService.findById(pid)查询商品数据,得到商品价格 // -- 封装数据:price // -- 封装数据:4个日志 // -- 调用insert(cart)执行将数据插入到数据表中 // 否:表示该用户的购物车中已有该商品 // -- 从查询结果中获取购物车数据的id // -- 从查询结果中取出原数量,与参数amount相加,得到新的数量 // -- 执行更新数量 } ``` 3.addToCart(Integer uid, Integer pid, Integer amount, String username)方法的代码具体实现。 ```java @Override public void addToCart(Integer uid, Integer pid, Integer amount, String username) { // 根据参数pid和uid查询购物车中的数据 Cart result = cartMapper.findByUidAndPid(uid, pid); Integer cid = result.getCid(); Date now = new Date(); // 判断查询结果是否为null if (result == null) { // 是:表示该用户并未将该商品添加到购物车 // 创建Cart对象 Cart cart = new Cart(); // 封装数据:uid,pid,amount cart.setUid(uid); cart.setPid(pid); cart.setNum(amount); // 调用productService.findById(pid)查询商品数据,得到商品价格 Product product = productService.findById(pid); // 封装数据:price cart.setPrice(product.getPrice()); // 封装数据:4个日志 cart.setCreatedUser(username); cart.setCreatedTime(now); cart.setModifiedUser(username); cart.setModifiedTime(now); // 调用insert(cart)执行将数据插入到数据表中 Integer rows = cartMapper.insert(cart); if (rows != 1) { throw new InsertException("插入商品数据时出现未知错误,请联系系统管理员"); } } else { // 否:表示该用户的购物车中已有该商品 // 从查询结果中获取购物车数据的id Integer cid = result.getCid(); // 从查询结果中取出原数量,与参数amount相加,得到新的数量 Integer num = result.getNum() + amount; // 执行更新数量 Integer rows = cartMapper.updateNumByCid(cid, num, username, now); if (rows != 1) { throw new InsertException("修改商品数量时出现未知错误,请联系系统管理员"); } } } ``` 4.在com.cy.store.service包下创建测试类CartServiceTests类,并编写测试方法。 ```java package com.cy.store.service; import com.cy.store.entity.Product; import com.cy.store.service.ex.ServiceException; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.util.List; @RunWith(SpringRunner.class) @SpringBootTest public class CartServiceTests { @Autowired private ICartService cartService; @Test public void addToCart() { try { Integer uid = 2; Integer pid = 10000007; Integer amount = 1; String username = "Tom"; cartService.addToCart(uid, pid, amount, username); System.out.println("OK."); } catch (ServiceException e) { System.out.println(e.getClass().getSimpleName()); System.out.println(e.getMessage()); } } } ``` ### 5 购物车-添加购物车-控制层 #### 5.1 处理异常 > **说明**:无异常。 #### 5.2 设计请求 设计用户提交的请求,并设计响应的方式。 请求路径:/carts/add_to_cart 请求参数:Integer pid, Integer amount, HttpSession session 请求类型:POST 响应结果:JsonResult #### 5.3 处理请求 1.在com.cy.store.controller包下创建CartController类并继承自BaseController类,添加@RequestMapping("carts")和@RestController注解;在类中声明ICartService业务对象,并使用@Autowired注解修饰。 ```java package com.cy.store.controller; import com.cy.store.service.ICartService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("carts") public class CartController extends BaseController { @Autowired private ICartService cartService; } ``` 2.在CartController类中添加处理请求的addToCart()方法。 ```java @RequestMapping("add_to_cart") public JsonResult addToCart(Integer pid, Integer amount, HttpSession session) { // 从Session中获取uid和username Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); // 调用业务对象执行添加到购物车 cartService.addToCart(uid, pid, amount, username); // 返回成功 return new JsonResult(OK); } ``` 3.完成后启动项目,先登录再访问http://localhost:8080/carts/add_to_cart?pid=10000017&amount=3进行测试。 ### 6 购物车-添加购物车-前端页面 1.在product.html页面中的body标签内的script标签里为“加入购物车”按钮添加点击事件。 ```javascript $("#btn-add-to-cart").click(function() { $.ajax({ url: "/carts/add_to_cart", type: "POST", data: { "pid": id, "amount": $("#num").val() }, dataType: "JSON", success: function(json) { if (json.state == 200) { alert("增加成功!"); } else { alert("增加失败!" + json.message); } }, error: function(xhr) { alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status); location.href = "login.html"; } }); }); ``` > $.ajax函数中参数data提交请参数的方式: > > ``` > // 1.适用于参数较多,且都在同一个表单中 > data: $("#form表单id属性值").serialize() > // 2.仅适用于上传文件 > data: new FormData($("##form表单id属性值")[0]) > // 3.参数拼接形式提交 > data: "pid=10000005&amount=3" > 类似于 > let num = 3 > data: "pid=10000005&amount=" + num > // 4.使用JSON格式提交参数 > data: { > "pid": 10000005, > "amount": 3 > } > ``` 2.完成后启动项目,先登录再访问http://localhost:8080/web/index.html页面进行测试。 ![](README.assets/1.png) # 十八、显示购物车列表 ### 1 购物车-显示列表-持久层 #### 1.1 规划需要执行的SQL语句 显示某用户的购物车列表数据的SQL语句大致是。 ```mysql SELECT cid, uid, pid, t_cart.price, t_cart.num, t_product.title, t_product.price AS realPrice, t_product.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 ``` #### 1.2 接口与抽象方法 1.由于涉及多表关联查询,必然没有哪个实体类可以封装此次的查询结果,因此需要创建VO类。创建com.cy.store.vo.CartVO类。 > VO: Value Object,值对象。当进行select查询时,查询的结果为多张表中的内容,此时不能直接用某个POJO类接收,POJO实体类不能包含多表查询的结果,解决方式:重新构建一个新的对象,该对象用于存储所查询出来的结果对应的映射,所以将该对象称为**值对象** ```java package com.cy.store.vo; import java.io.Serializable; /** 购物车数据的Value Object类 */ public class CartVO implements Serializable { private Integer cid; private Integer uid; private Integer pid; private Long price; private Integer num; private String title; private Long realPrice; private String image; // Generate: Getter and Setter、Generate hashCode() and equals()、toString() } ``` 2.在CartMapper接口中添加抽象方法。 ```java /** * 查询某用户的购物车数据 * @param uid 用户id * @return 该用户的购物车数据的列表 */ List findVOByUid(Integer uid); ``` #### 1.3 配置SQL映射 1.在CartMapper.xml文件中添加findVOByUid()方法的映射。 ```xml ``` 2.在CartMapperTests测试类中添加findVOByUid()方法的测试。 ```java @Test public void findVOByUid() { List list = cartMapper.findVOByUid(31); System.out.println(list); } ``` ### 2 购物车-显示列表-业务层 #### 2.1 规划异常 > **说明**:无异常。 #### 2.2 接口与抽象方法 在ICartService接口中添加findVOByUid()抽象方法。 ```java /** * 查询某用户的购物车数据 * @param uid 用户id * @return 该用户的购物车数据的列表 */ List getVOByUid(Integer uid); ``` #### 2.3 实现抽象方法 1.在CartServiceImpl类中重写业务接口中的抽象方法。 ```java @Override public List getVOByUid(Integer uid) { return cartMapper.findVOByUid(uid); } ``` 2.在CartServiceTests测试类中添加getVOByUid()测试方法。 ```java @Test public void getVOByUid() { List list = cartService.getVOByUid(31); System.out.println("count=" + list.size()); for (CartVO item : list) { System.out.println(item); } } ``` ### 3 购物车-显示列表-控制器 #### 3.1 处理异常 > **说明**:无异常。 #### 3.2 设计请求 设计用户提交的请求,并设计响应的方式。 请求路径:/carts/ 请求参数:HttpSession session 请求类型:GET 响应结果:JsonResult> #### 3.3 处理请求 1.在CartController类中编写处理请求的代码。 ```java @GetMapping({"", "/"}) public JsonResult> getVOByUid(HttpSession session) { // 从Session中获取uid Integer uid = getUidFromSession(session); // 调用业务对象执行查询数据 List data = cartService.getVOByUid(uid); // 返回成功与数据 return new JsonResult>(OK, data); } ``` 2.完成后启动项目,先登录再访问http://localhost:8080/carts请求进行测试。 ### 4 购物车-显示列表-前端页面 1.将cart.html页面的head头标签内引入的cart.js文件注释掉。 ```javascript ``` 2.给form标签添加action="orderConfirm.html"属性、tbody标签添加id="cart-list"属性、结算按钮的类型改为type="button"值。如果以上属性值已经添加过无需重复添加。 3.在cart.html页面body标签内的script标签中编写展示购物车列表的代码。 ```js ``` 4.完成后启动项目,先登录再访问http://localhost:8080/web/cart.html页面进行测试。 ![](README.assets/2.png) # 十九、增加商品数量 ### 1 购物车-增加商品数量-持久层 #### 1.1 规划需要执行的SQL语句 1.首先进行查询需要操作的购物车数据信息。 ```mysql SELECT * FROM t_cart WHERE cid=? ``` 2.然后计算出新的商品数量值,如果满足更新条件则执行更新操作。此SQL语句无需重复开发。 ```mysql UPDATE t_cart SET num=?, modified_user=?, modified_time=? WHERE cid=? ``` #### 1.2 接口与抽象方法 在CartMapper接口中添加抽象方法。 ```java /** * 根据购物车数据id查询购物车数据详情 * @param cid 购物车数据id * @return 匹配的购物车数据详情,如果没有匹配的数据则返回null */ Cart findByCid(Integer cid); ``` #### 1.3 配置SQL映射 1.在CartMapper文件中添加findByCid(Integer cid)方法的映射。 ```xml ``` 2.在CartMapperTests测试类中添加findByCid()测试方法。 ```java @Test public void findByCid() { Integer cid = 6; Cart result = cartMapper.findByCid(cid); System.out.println(result); } ``` ### 2 购物车-增加商品数量-业务层 #### 2.1 规划异常 1.如果尝试访问的购物车数据不存在,则抛出CartNotFoundException异常。创建com.cy.store.service.ex.CartNotFoundException类。 ```java /** 购物车数据不存在的异常 */ public class CartNotFoundException extends ServiceException { // Override Methods... } ``` 2.如果尝试访问的数据并不是当前登录用户的数据,则抛出AccessDeniedException异常。此异常类无需再次创建。 3.最终执行更新操作时,可能会抛出UpdateException异常。此异常类无需再次创建。 #### 2.2 接口与抽象方法 在业务层ICartService接口中添加addNum()抽象方法。 ```java /** * 将购物车中某商品的数量加1 * @param cid 购物车数量的id * @param uid 当前登录的用户的id * @param username 当前登录的用户名 * @return 增加成功后新的数量 */ Integer addNum(Integer cid, Integer uid, String username); ``` #### 2.3 实现抽象方法 1.在CartServiceImpl类中,实现接口中的抽象方法并规划业务逻辑。 ```java public Integer addNum(Integer cid, Integer uid, String username) { // 调用findByCid(cid)根据参数cid查询购物车数据 // 判断查询结果是否为null // 是:抛出CartNotFoundException // 判断查询结果中的uid与参数uid是否不一致 // 是:抛出AccessDeniedException // 可选:检查商品的数量是否大于多少(适用于增加数量)或小于多少(适用于减少数量) // 根据查询结果中的原数量增加1得到新的数量num // 创建当前时间对象,作为modifiedTime // 调用updateNumByCid(cid, num, modifiedUser, modifiedTime)执行修改数量 } ``` 2.实现addNum()方法中的业务逻辑代码。 ```java @Override public Integer addNum(Integer cid, Integer uid, String username) { // 调用findByCid(cid)根据参数cid查询购物车数据 Cart result = cartMapper.findByCid(cid); // 判断查询结果是否为null if (result == null) { // 是:抛出CartNotFoundException throw new CartNotFoundException("尝试访问的购物车数据不存在"); } // 判断查询结果中的uid与参数uid是否不一致 if (!result.getUid().equals(uid)) { // 是:抛出AccessDeniedException throw new AccessDeniedException("非法访问"); } // 可选:检查商品的数量是否大于多少(适用于增加数量)或小于多少(适用于减少数量) // 根据查询结果中的原数量增加1得到新的数量num Integer num = result.getNum() + 1; // 创建当前时间对象,作为modifiedTime Date now = new Date(); // 调用updateNumByCid(cid, num, modifiedUser, modifiedTime)执行修改数量 Integer rows = cartMapper.updateNumByCid(cid, num, username, now); if (rows != 1) { throw new InsertException("修改商品数量时出现未知错误,请联系系统管理员"); } // 返回新的数量 return num; } ``` 3.在CartServiceTests测试类中添加addNum()测试方法。 ```java @Test public void addNum() { try { Integer cid = 6; Integer uid = 31; String username = "管理员"; Integer num = cartService.addNum(cid, uid, username); System.out.println("OK. New num=" + num); } catch (ServiceException e) { System.out.println(e.getClass().getSimpleName()); System.out.println(e.getMessage()); } } ``` ### 3 购物车-增加商品数量-控制器 #### 3.1 处理异常 在BaseController类中添加CartNotFoundException异常类的统一管理。 ```java // ... else if (e instanceof CartNotFoundException) { result.setState(4007); } // ... ``` #### 3.2 设计请求 设计用户提交的请求,并设计响应的方式。 请求路径:/carts/{cid}/num/add 请求参数:@PathVariable("cid") Integer cid, HttpSession session 请求类型:POST 响应结果:JsonResult #### 3.3 处理请求 1.在CartController类中添加处理请求的addNum()方法。 ```java @RequestMapping("{cid}/num/add") public JsonResult addNum(@PathVariable("cid") Integer cid, HttpSession session) { // 从Session中获取uid和username Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); // 调用业务对象执行增加数量 Integer data = cartService.addNum(cid, uid, username); // 返回成功 return new JsonResult(OK, data); } ``` 2.完成后启动项目,先登录再访问http://localhost:8080/carts/6/num/add页面进行测试。 ![](README.assets/3-1660408953719.png) ### 4 购物车-增加商品数量-前端页面 1.首先确定在showCartList()函数中动态拼接的增加购物车按钮是绑定了addNum()事件,如果已经添加无需重复添加。 ```javascript ``` 2.在script标签中定义addNum()函数并编写增加购物车数量的逻辑代码。 ```javascript ``` 3.完成后启动项目,先登录再访问http://localhost:8080/web/cart.html页面点击“+”按钮进行测试。 # 二十、显示勾选的购物车数据 ### 1 显示确认订单页-显示勾选的购物车数据-持久层 #### 1.1 规划需要执行的SQL语句 在“确认订单页”显示的商品信息,应来自前端页面(购物车列表)中勾选的数据,所以显示的信息其实是购物车中的数据。到底需要显示哪些取决于用户的勾选操作,当用户勾选了若干条购物车数据后,这些数据的id应传递到当前“确认订单页”中,该页面根据这些id获取需要显示的数据列表。 所以在持久层需要完成“根据若干个不确定的id值,查询购物车数据表,显示购物车中的数据信息”。则需要执行的SQL语句大致是。 ```mysql SELECT cid, uid, pid, t_cart.price, t_cart.num, t_product.title, t_product.price AS realPrice, t_product.image FROM t_cart LEFT JOIN t_product ON t_cart.pid = t_product.id WHERE cid IN (?, ?, ?) ORDER BY t_cart.created_time DESC ``` #### 1.2 接口与抽象方法 在CartMapper接口中添加findVOByCids(Integer[] cids)方法。 ```java /** * 根据若干个购物车数据id查询详情的列表 * @param cids 若干个购物车数据id * @return 匹配的购物车数据详情的列表 */ List findVOByCids(Integer[] cids); ``` #### 1.3 配置SQL映射 1.在CartMapper.xml文件中添加SQL语句的映射配置。 ```xml ``` 2.在CartMapperTests测试类中添加findVOByCids()测试方法。 ```java @Test public void findVOByCids() { Integer[] cids = {1, 2, 6, 7, 8, 9, 10}; List list = cartMapper.findVOByCids(cids); System.out.println("count=" + list.size()); for (CartVO item : list) { System.out.println(item); } } ``` ### 2 显示确认订单页-显示勾选的购物车数据-业务层 #### 2.1 规划异常 > **说明**:无异常。 #### 2.2 接口与抽象方法 在ICartService接口中添加getVOByCids()抽象方法。 ```java /** * 根据若干个购物车数据id查询详情的列表 * @param uid 当前登录的用户的id * @param cids 若干个购物车数据id * @return 匹配的购物车数据详情的列表 */ List getVOByCids(Integer uid, Integer[] cids); ``` #### 2.3 实现抽象方法 1.在CartServiceImpl类中重写业务接口中的抽象方法。 ```java @Override public List getVOByCids(Integer uid, Integer[] cids) { List list = cartMapper.findVOByCids(cids); /** for (CartVO cart : list) { if (!cart.getUid().equals(uid)) { list.remove(cart); } } */ Iterator it = list.iterator(); while (it.hasNext()) { CartVO cart = it.next(); if (!cart.getUid().equals(uid)) { it.remove(); } } return list; } ``` 2.在CartServiceTests测试类中添加getVOByCids()测试方法。 ```java @Test public void getVOByCids() { Integer[] cids = {1, 2, 6, 7, 8, 9, 10}; Integer uid = 31; List list = cartService.getVOByCids(uid, cids); System.out.println("count=" + list.size()); for (CartVO item : list) { System.out.println(item); } } ``` ### 3 显示确认订单页-显示勾选的购物车数据-控制器 #### 3.1 处理异常 **说明**:无异常。 #### 3.2 设计请求 设计用户提交的请求,并设计响应的方式。 请求路径:/carts/list 请求参数:Integer[] cids, HttpSession session 请求类型:GET 响应结果:JsonResult> #### 3.3 处理请求 1.在CartController类中添加处理请求的getVOByCids()方法。 ```java @GetMapping("list") public JsonResult> getVOByCids(Integer[] cids, HttpSession session) { // 从Session中获取uid Integer uid = getUidFromSession(session); // 调用业务对象执行查询数据 List data = cartService.getVOByCids(uid, cids); // 返回成功与数据 return new JsonResult<>(OK, data); } ``` 2.完成后启动项目,先登录再访问http://localhost:8080/carts/list?cids=7&cids=8&cids=13&cids=14&cids=17地址进行测试。 ### 4 显示确认订单页-前端页面 #### 4.1 显示勾选的购物车数据-前端页面 1.将cart.html页面将“结算”按钮属性更改未type="submit" 2.在orderConfirm.html页面的head标签里注释掉引入外部的orderConfirm.js文件。 ```javascript ``` 3.在orderConfirm.html页面中检查必要控件的属性是否添加,如果已添加无需重复添加。 4.在orderConfirm.html页面中的body标签内的最后添加srcipt标签并在标签内部添加处理购物车“订单商品信息”列表展示的代码。 ```javascript ``` 4.完成后启动项目,先登录再访问http://localhost:8080/web/cart.html页面,勾选商品再点击“结算”按钮进行测试。 #### 4.2 显示选择收货地址-前端页面 > 收货地址存放在一个select下拉列表中,将查询到的当前登录用户的收货地址动态加载到这个下拉列表中,从数据库的角度来说,是一个select查询语句,已经编写了根据用户uid查询当前用户的收货地址数据 1.在orderConfirm.html页面中的body标签内的srcipt标签中添加获取收货地址列表方法的定义。 ```javascript function showAddressList() { $("#address-list").empty(); $.ajax({ url: "/addresses", type: "GET", dataType: "JSON", success: function(json) { let list = json.data; console.log("count=" + list.length); for (let i = 0; i < list.length; i++) { console.log(list[i].name); let opt = ''; opt = opt.replace(/#{aid}/g, list[i].aid); opt = opt.replace(/#{tag}/g, list[i].tag); opt = opt.replace("#{name}", list[i].name); opt = opt.replace("#{province}", list[i].provinceName); opt = opt.replace("#{city}", list[i].cityName); opt = opt.replace("#{area}", list[i].areaName); opt = opt.replace("#{address}", list[i].address); opt = opt.replace("#{phone}", list[i].phone); $("#address-list").append(opt); } } }); } ``` 2.在orderConfirm.html页面中的body标签内的srcipt标签中添加展示收货地址列表方法的调用。 ```javascript ``` 3.完成后启动项目,先登录再访问http://localhost:8080/web/orderConfirm.html页面进行测试。 ## 二十一、创建订单 ### 1 订单-创建数据表 1.使用use命令先选中store数据库。 ```mysql USE store; ``` 2.在store数据库中创建t_order和t_order_item数据表。 ```mysql CREATE TABLE t_order ( oid INT AUTO_INCREMENT COMMENT '订单id', uid INT NOT NULL COMMENT '用户id', recv_name VARCHAR(20) NOT NULL COMMENT '收货人姓名', recv_phone VARCHAR(20) COMMENT '收货人电话', recv_province VARCHAR(15) COMMENT '收货人所在省', recv_city VARCHAR(15) COMMENT '收货人所在市', recv_area VARCHAR(15) COMMENT '收货人所在区', recv_address VARCHAR(50) COMMENT '收货详细地址', total_price BIGINT COMMENT '总价', status INT COMMENT '状态:0-未支付,1-已支付,2-已取消,3-已关闭,4-已完成', order_time DATETIME COMMENT '下单时间', pay_time DATETIME COMMENT '支付时间', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', PRIMARY KEY (oid) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE t_order_item ( id INT AUTO_INCREMENT COMMENT '订单中的商品记录的id', oid INT NOT NULL COMMENT '所归属的订单的id', pid INT NOT NULL COMMENT '商品的id', title VARCHAR(100) NOT NULL COMMENT '商品标题', image VARCHAR(500) COMMENT '商品图片', price BIGINT COMMENT '商品价格', num INT COMMENT '购买数量', created_user VARCHAR(20) COMMENT '创建人', created_time DATETIME COMMENT '创建时间', modified_user VARCHAR(20) COMMENT '修改人', modified_time DATETIME COMMENT '修改时间', PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` ### 2 订单-创建实体类 1.在com.cy.store.entity包下创建Order实体类。 ```java package com.cy.store.entity; import java.io.Serializable; import java.util.Date; /** 订单数据的实体类 */ public class Order extends BaseEntity implements Serializable { private Integer oid; private Integer uid; private String recvName; private String recvPhone; private String recvProvince; private String recvCity; private String recvArea; private String recvAddress; private Long totalPrice; private Integer status; private Date orderTime; private Date payTime; // Generate: Getter and Setter、Generate hashCode() and equals()、toString() } ``` 2.在com.cy.store.entity包下创建OrderItem实体类。 ```java package com.cy.store.entity; import java.io.Serializable; /** 订单中的商品数据 */ public class OrderItem extends BaseEntity implements Serializable { private Integer id; private Integer oid; private Integer pid; private String title; private String image; private Long price; private Integer num; // Generate: Getter and Setter、Generate hashCode() and equals()、toString() } ``` ### 3 订单-持久层 #### 3.1 规划需要执行的SQL语句 1.插入订单数据的SQL语句大致是。 ```mysql INSERT INTO t_order ( uid, recv_name, recv_phone, recv_province, recv_city, recv_area, recv_address, total_price, status, order_time, pay_time, created_user, created_time, modified_user, modified_time ) VALUES ( #对应字段的值列表 ) ``` 2.插入订单商品数据的SQL语句大致是。 ```mysql INSERT INTO t_order_item ( oid, pid, title, image, price, num, created_user, created_time, modified_user, modified_time ) VALUES ( #对应字段的值列表 ) ``` #### 3.2 接口与抽象方法 在com.cy.store.mapper包下创建OrderMapper接口并在接口中添加抽象方法。 ```java package com.cy.store.mapper; import com.cy.store.entity.Order; import com.cy.store.entity.OrderItem; /** 处理订单及订单商品数据的持久层接口 */ public interface OrderMapper { /** * 插入订单数据 * @param order 订单数据 * @return 受影响的行数 */ Integer insertOrder(Order order); /** * 插入订单商品数据 * @param orderItem 订单商品数据 * @return 受影响的行数 */ Integer insertOrderItem(OrderItem orderItem); } ``` #### 3.3 配置SQL映射 1.在main\resources\mapper文件夹下创建OrderMapper.xml文件,并添加抽象方法的映射。 ```xml INSERT INTO t_order ( uid, recv_name, recv_phone, recv_province, recv_city, recv_area, recv_address, total_price,status, order_time, pay_time, created_user, created_time, modified_user, modified_time ) VALUES ( #{uid}, #{recvName}, #{recvPhone}, #{recvProvince}, #{recvCity}, #{recvArea}, #{recvAddress}, #{totalPrice}, #{status}, #{orderTime}, #{payTime}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime} ) INSERT INTO t_order_item ( oid, pid, title, image, price, num, created_user, created_time, modified_user, modified_time ) VALUES ( #{oid}, #{pid}, #{title}, #{image}, #{price}, #{num}, #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime} ) ``` 2.在com.cy.store.mapper包下创建OrderMapperTests测试类,并添加测试方法。 ```java package com.cy.store.mapper; import com.cy.store.entity.Order; import com.cy.store.entity.OrderItem; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class OrderMapperTests { @Autowired private OrderMapper orderMapper; @Test public void insertOrder() { Order order = new Order(); order.setUid(31); order.setRecvName("小王"); Integer rows = orderMapper.insertOrder(order); System.out.println("rows=" + rows); } @Test public void insertOrderItem() { OrderItem orderItem = new OrderItem(); orderItem.setOid(1); orderItem.setPid(2); orderItem.setTitle("高档铅笔"); Integer rows = orderMapper.insertOrderItem(orderItem); System.out.println("rows=" + rows); } } ``` ### 4 订单-业务层 #### 4.1 规划异常 > **说明**:无异常。 #### 4.2 接口与抽象方法 1.由于处理过程中还需要涉及收货地址数据的处理,所以需要先在IAddressService接口中添加getByAid()方法。 ```java /** * 根据收货地址数据的id,查询收货地址详情 * @param aid 收货地址id * @param uid 归属的用户id * @return 匹配的收货地址详情 */ Address getByAid(Integer aid, Integer uid); ``` 2.在AddressServiceImpl类中实现接口中的getByAid()抽象方法。 ```java @Override public Address getByAid(Integer aid, Integer uid) { // 根据收货地址数据id,查询收货地址详情 Address address = addressMapper.findByAid(aid); if (address == null) { throw new AddressNotFoundException("尝试访问的收货地址数据不存在"); } if (!address.getUid().equals(uid)) { throw new AccessDeniedException("非法访问"); } address.setProvinceCode(null); address.setCityCode(null); address.setAreaCode(null); address.setCreatedUser(null); address.setCreatedTime(null); address.setModifiedUser(null); address.setModifiedTime(null); return address; } ``` 3.在com.cy.store.service包下创建IOrderService业务层接口并添加抽象方法。 ```java package com.cy.store.service; import com.cy.store.entity.Order; /** 处理订单和订单数据的业务层接口 */ public interface IOrderService { /** * 创建订单 * @param aid 收货地址的id * @param cids 即将购买的商品数据在购物车表中的id * @param uid 当前登录的用户的id * @param username 当前登录的用户名 * @return 成功创建的订单数据 */ Order create(Integer aid, Integer[] cids, Integer uid, String username); } ``` #### 4.3 实现抽象方法 1.在com.cy.store.service.impl包下创建OrderServiceImpl业务层实现类并实现IOrderService接口;在类定义之前添加@Service注解,在类中添加OrderMapper订单持久层对象、IAddressService处理收货地址对象、ICartService购物车数据对象,并都添加@Autowired注解进行修饰。 ```java package com.cy.store.service.impl; import com.cy.store.entity.Address; import com.cy.store.entity.Order; import com.cy.store.entity.OrderItem; import com.cy.store.mapper.OrderMapper; import com.cy.store.service.IAddressService; import com.cy.store.service.ICartService; import com.cy.store.service.IOrderService; import com.cy.store.service.ex.InsertException; import com.cy.store.vo.CartVO; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.Date; import java.util.List; /** 处理订单和订单数据的业务层实现类 */ @Service public class OrderServiceImpl implements IOrderService { @Autowired private OrderMapper orderMapper; @Autowired private IAddressService addressService; @Autowired private ICartService cartService; // ... } ``` 2.在OrderServiceImpl类中重写父接口中的create()抽象方法。 ```java @Transactional @Override public Order create(Integer aid, Integer[] cids, Integer uid, String username) { // 创建当前时间对象 // 根据cids查询所勾选的购物车列表中的数据 // 计算这些商品的总价 // 创建订单数据对象 // 补全数据:uid // 查询收货地址数据 // 补全数据:收货地址相关的6项 // 补全数据:totalPrice // 补全数据:status // 补全数据:下单时间 // 补全数据:日志 // 插入订单数据 // 遍历carts,循环插入订单商品数据 // 创建订单商品数据 // 补全数据:oid(order.getOid()) // 补全数据:pid, title, image, price, num // 补全数据:4项日志 // 插入订单商品数据 // 返回 } ``` 3.OrderServiceImpl类中的create()方法具体逻辑代码实现见下。 ```java @Transactional @Override public Order create(Integer aid, Integer[] cids, Integer uid, String username) { // 创建当前时间对象 Date now = new Date(); // 根据cids查询所勾选的购物车列表中的数据 List carts = cartService.getVOByCids(uid, cids); // 计算这些商品的总价 long totalPrice = 0; for (CartVO cart : carts) { totalPrice += cart.getRealPrice() * cart.getNum(); } // 创建订单数据对象 Order order = new Order(); // 补全数据:uid order.setUid(uid); // 查询收货地址数据 Address address = addressService.getByAid(aid, uid); // 补全数据:收货地址相关的6项 order.setRecvName(address.getName()); order.setRecvPhone(address.getPhone()); order.setRecvProvince(address.getProvinceName()); order.setRecvCity(address.getCityName()); order.setRecvArea(address.getAreaName()); order.setRecvAddress(address.getAddress()); // 补全数据:totalPrice order.setTotalPrice(totalPrice); // 补全数据:status order.setStatus(0); // 补全数据:下单时间 order.setOrderTime(now); // 补全数据:日志 order.setCreatedUser(username); order.setCreatedTime(now); order.setModifiedUser(username); order.setModifiedTime(now); // 插入订单数据 Integer rows1 = orderMapper.insertOrder(order); if (rows1 != 1) { throw new InsertException("插入订单数据时出现未知错误,请联系系统管理员"); } // 遍历carts,循环插入订单商品数据 for (CartVO cart : carts) { // 创建订单商品数据 OrderItem item = new OrderItem(); // 补全数据:setOid(order.getOid()) item.setOid(order.getOid()); // 补全数据:pid, title, image, price, num item.setPid(cart.getPid()); item.setTitle(cart.getTitle()); item.setImage(cart.getImage()); item.setPrice(cart.getRealPrice()); item.setNum(cart.getNum()); // 补全数据:4项日志 item.setCreatedUser(username); item.setCreatedTime(now); item.setModifiedUser(username); item.setModifiedTime(now); // 插入订单商品数据 Integer rows2 = orderMapper.insertOrderItem(item); if (rows2 != 1) { throw new InsertException("插入订单商品数据时出现未知错误,请联系系统管理员"); } } // 返回 return order; } ``` 4.在com.cy.store.service测试包下创建OrderServiceTests测试类,并添加create()方法进行功能测试。 ```java package com.cy.store.service; import com.cy.store.entity.Order; import com.cy.store.service.ex.ServiceException; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; @RunWith(SpringRunner.class) @SpringBootTest public class OrderServiceTests { @Autowired private IOrderService orderService; @Test public void create() { try { Integer aid = 21; Integer[] cids = {4, 5, 6,7}; Integer uid = 31; String username = "订单管理员"; Order order = orderService.create(aid, cids, uid, username); System.out.println(order); } catch (ServiceException e) { System.out.println(e.getClass().getSimpleName()); System.out.println(e.getMessage()); } } } ``` ### 5 订单-控制器层 #### 5.1 处理异常 > **说明**:无异常。 #### 5.2 设计请求 设计用户提交的请求,并设计响应的方式。 请求路径:/orders/create 请求参数:Integer aid, Integer[] cids, HttpSession session 请求类型:POST 响应结果:JsonResult #### 5.3 处理请求 1.在com.cy.store.controller包下创建OrderController类,并继承自BaseController类;并在类前添加@RequestMapping("orders")注解和@RestController注解;在类中声明IOrderService业务对象,然后添加@Autowired注解修饰;最后在类中添加处理请求的方法。 ```java package com.cy.store.controller; import com.cy.store.entity.Order; import com.cy.store.service.IOrderService; import com.cy.store.util.JsonResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; @RestController @RequestMapping("orders") public class OrderController extends BaseController { @Autowired private IOrderService orderService; @RequestMapping("create") public JsonResult create(Integer aid, Integer[] cids, HttpSession session) { // 从Session中取出uid和username Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); // 调用业务对象执行业务 Order data = orderService.create(aid, cids, uid, username); // 返回成功与数据 return new JsonResult(OK, data); } } ``` 2.完成后启动项目,先登录再访问http://localhost:8080/orders/create?aid=21&cids=4&cids=5&cids=6&cids=7进行测试。 ![](README.assets/1-1660498206270.png) ### 6 订单-前端页面 1.在orderConfirm.xml页面中的body标签内的script标签内添加“在线支付”按钮的点击时间。 ```javascript $("#btn-create-order").click(function() { $.ajax({ url: "/orders/create", data: $("#form-create-order").serialize(), type: "POST", dataType: "JSON", success: function(json) { if (json.state == 200) { alert("创建订单成功!"); console.log(json.data); } else { alert("创建订单失败!" + json.message); } }, error: function(xhr) { alert("您的登录信息已经过期,请重新登录!HTTP响应码:" + xhr.status); location.href = "login.html"; } }); }); ``` 2.完成后启动项目,先登录再访问http://localhost:8080/web/cart.html页面,勾选购车中的商品,再点击“结算”按钮,最后在订单确认页中点击“在线支付”按钮进行功能的测试。 ## 二十二、AOP ### 1 Spring AOP AOP:面向切面(Aspect)编程。AOP并不是Spring框架的特性,只是Spring很好的支持了AOP。 如果需要在处理每个业务时,都执行特定的代码,则可以假设在整个数据处理流程中存在某个切面,切面中可以定义某些方法,当处理流程执行到切面时,就会自动执行切面中的方法。最终实现的效果就是:只需要定义好切面方法,配置好切面的位置(连接点),在不需要修改原有数据处理流程的代码的基础之上,就可以使得若干个流程都执行相同的代码。 ### 2 切面方法 1.切面方法的访问权限是public。 2.切面方法的返回值类型可以是void或Object,如果使用的注解是@Around时,必须使用Object作为返回值类型,并返回连接点方法的返回值;如果使用的注解是@Before或@After等其他注解时,则自行决定。 3.切面方法的名称可以自定义。 4.切面方法的参数列表中可以添加ProceedingJoinPoint接口类型的对象,该对象表示连接点,也可以理解调用切面所在位置对应的方法的对象,如果使用的注解是@Around时,必须添加该参数,反之则不是必须添加。 ### 3 统计业务方法执行时长 1.在使用Spring AOP编程时,需要先在pom.xml文件中添加两个关于AOP的依赖aspectjweaver和aspectjtools。 ```xml org.aspectj aspectjweaver org.aspectj aspectjtools ``` 2.在com.cy.store.aop包下创建TimerAspect切面类,在类之前添加@Aspect和@Component注解修饰。 ```java package com.cy.store.aop; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; @Aspect @Component public class TimerAspect { } ``` 3.在类中添加切面方法around(ProceedingJoinPoint pjp)。 ```java public Object around(ProceedingJoinPoint pjp) throws Throwable { // 记录起始时间 long start = System.currentTimeMillis(); // 执行连接点方法,即切面所在位置对应的方法。本项目中表示执行注册或执行登录等 Object result = pjp.proceed(); // 记录结束时间 long end = System.currentTimeMillis(); // 计算耗时 System.err.println("耗时:" + (end - start) + "ms."); // 返回连接点方法的返回值 return result; } ``` 4.最后需要在方法之前添加@Around注解,以配置连接点,即哪些方法需要应用该切面。 ```java @Around("execution(* com.cy.store.service.impl.*.*(..))") ``` 5.启动项目,在前端浏览器访问任意一个功能模块进行功能的测试。 ![](README.assets/1-1660501021858.png)