# Store **Repository Path**: kriwen/store ## Basic Information - **Project Name**: Store - **Description**: Java仓库 - **Primary Language**: Unknown - **License**: MulanPSL-2.0 - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-07-23 - **Last Updated**: 2025-07-23 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 用户注册 ## 1.创建数据表 ### 1.选中数据表 ```sql use store; ``` ### 2.创建t_user表 ```sql CREATE table t_user ( uid INT auto_increment COMMENT '用户id', username VARCHAR(20) NOT NULL UNIQUE COMMENT '用户名', password CHAR(32) NOT NULL COMMENT '密码', salt CHAR(36) COMMENT '盐值', phone VARCHAR(20) COMMENT '电话号码', email VARCHAR(30) COMMENT '电子邮件', gender INT COMMENT '性别:0-女,1-男', avatar VARCHAR(50) COMMENT '头像', is_delete INT COMMENT '是否删除:0-未删除,1-已删除', created_user VARCHAR(20) COMMENT '日志-创建人', created_time DATETIME COMMENT '日志-创建时间', modified_user VARCHAR(20) COMMENT '日志-最后修改创建人', modified_time DATETIME COMMENT '日志-最后修改创建时间', PRIMARY KEY(uid) ) ENGINE=INNODB DEFAULT CHARSET=utf8; ``` ## 2.创建用户实体类 1.通过表的结构提取出表的公共字段,放在一个实体类的基类BaseEntity当中 ```java package com.cy.store.entity; import java.io.Serializable; import java.util.Date; import java.util.Objects; // 作为实体类的基类 public class BaseEntity implements Serializable { private String createdUser; private Date createdTime; private String modifiedUser; private Date modifiedTime; public String getCreatedUser() { return createdUser; } public void setCreatedUser(String createdUser) { this.createdUser = createdUser; } public Date getCreatedTime() { return createdTime; } public void setCreatedTime(Date createdTime) { this.createdTime = createdTime; } public String getModifiedUser() { return modifiedUser; } public void setModifiedUser(String modifiedUser) { this.modifiedUser = modifiedUser; } public Date getModifiedTime() { return modifiedTime; } public void setModifiedTime(Date modifiedTime) { this.modifiedTime = modifiedTime; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; BaseEntity that = (BaseEntity) o; return Objects.equals(getCreatedUser(), that.getCreatedUser()) && Objects.equals(getCreatedTime(), that.getCreatedTime()) && Objects.equals(getModifiedUser(), that.getModifiedUser()) && Objects.equals(getModifiedTime(), that.getModifiedTime()); } @Override public int hashCode() { return Objects.hash(getCreatedUser(), getCreatedTime(), getModifiedUser(), getModifiedTime()); } @Override public String toString() { return "BaseEntity{" + "createdUser='" + createdUser + '\'' + ", createdTime=" + createdTime + ", modifiedUser='" + modifiedUser + '\'' + ", modifiedTime=" + modifiedTime + '}'; } } ``` 2.创建用户的实体类,需要用到BaseEntity基类 ```java package com.cy.store.entity; import java.io.Serializable; import java.util.Objects; // 用户的实体类: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 isDelete; public Integer getUid() { return uid; } public void setUid(Integer uid) { this.uid = uid; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getSalt() { return salt; } public void setSalt(String salt) { this.salt = salt; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public Integer getGender() { return gender; } public void setGender(Integer gender) { this.gender = gender; } public String getAvatar() { return avatar; } public void setAvatar(String avatar) { this.avatar = avatar; } public Integer getIsDelete() { return isDelete; } public void setIsDelete(Integer isDelete) { this.isDelete = isDelete; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; user user = (user) o; return Objects.equals(getUid(), user.getUid()) && Objects.equals(getUsername(), user.getUsername()) && Objects.equals(getPassword(), user.getPassword()) && Objects.equals(getSalt(), user.getSalt()) && Objects.equals(getPhone(), user.getPhone()) && Objects.equals(getEmail(), user.getEmail()) && Objects.equals(getGender(), user.getGender()) && Objects.equals(getAvatar(), user.getAvatar()) && Objects.equals(getIsDelete(), user.getIsDelete()); } @Override public int hashCode() { return Objects.hash(super.hashCode(), getUid(), getUsername(), getPassword(), getSalt(), getPhone(), getEmail(), getGender(), getAvatar(), getIsDelete()); } @Override public String toString() { return "user{" + "uid=" + uid + ", username='" + username + '\'' + ", password='" + password + '\'' + ", salt='" + salt + '\'' + ", phone='" + phone + '\'' + ", email='" + email + '\'' + ", gender=" + gender + ", avatar='" + avatar + '\'' + ", isDelete=" + isDelete + '}'; } } ``` ## 3.用户注册-持久层 通过MyBatis来操作数据库 #### 3.1 规划需要的SQL语句 1.用户注册功能,相当于数据插入操作 ```sql insert int t_user (username, password) values(值列表) ``` 2.用户注册时首先查询当前用户名是否存在,如果存在则不能注册,相当于查询操作 ```sql select * from t_user where username=? ``` #### 3.2 设计接口和方法 1.定义mapper接口。在项目目录结构下首先创建一个mapper包,在这个包下根据不同的功能模块创建mapper接口。创建一个UserMapper接口 ```java package com.cy.store.mapper; import com.cy.store.entity.User; /** 用户模块的持久层接口 */ //@Mapper public interface UserMapper { /** * 插入用户数据 * @param user 用户数据 * @return 受影响的行数 */ Integer insert(User user); /** * 根据用户名查询用户数据 * @param username 用户名 * @return 匹配的用户数据,如果没有匹配的数据,则返回null */ User findByUsername(String username); } ``` 2.在启动类配置mapper接口文件位置 ```Java // mapperScan注解指定当前项目中mapper接口所在的位置,项目启动时会加载注解的mapper接口 @MapperScan("com.cy.store.mapper") ``` #### 3.3 编写映射 定义xml映射文件,与对应的接口进行关联。所以的映射文件需要放置在resources目录下,在这个目录下创建一个mapper文件夹,然后在这个文件夹下存放Mapper的映射文件 2.创建接口对应映射文件,遵循和接口名称保存一致。创建一个UserMapper.xml文件 ``` ``` 3.配置接口中的方法对应上SQL语句。需要借助标签完成,insert/updata/delete/select,对于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}, #{createUser}, #{createTime}, #{modifiedUser}, #{modifiedTime}) ``` 3.单元测试:每个独立层编写完毕后测试当前功能。在test包结构下创建一个mapper包,在这个包下测试持久层 ``` ``` ## 4.用户注册-业务层 #### 4.1 规划异常 1.RunTimeException异常,作为这个异常的子类,然后再去定义具体的异常类型来继承这个异常。业务层异常的基类,serviceException异常,这个异常继承RuntimeException异常。异常机制的建立 ```java package com.cy.store.service.ex; /** 业务层异常的基类 */ 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 package com.cy.store.service.ex; /** * 用户名被占用的异常 */ public class UsernameDuplicatedException extends ServiceException { public UsernameDuplicatedException() { super(); } public UsernameDuplicatedException(String message) { super(message); } public UsernameDuplicatedException(String message, Throwable cause) { super(message, cause); } public UsernameDuplicatedException(Throwable cause) { super(cause); } protected UsernameDuplicatedException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ``` 3.正在执行数据插入的时候,服务器、数据库宕机。处于正在执行插入过程产生的异常:InsertException ```java package com.cy.store.service.ex; /** * 插入数据时产生的异常 */ public class InsertException extends RuntimeException { public InsertException() { super(); } public InsertException(String message) { super(message); } public InsertException(String message, Throwable cause) { super(message, cause); } public InsertException(Throwable cause) { super(cause); } protected InsertException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ``` #### 4.2 设计接口和抽象方法 在service包下创建一个IUserService接口 ```java package com.cy.store.service; import com.cy.store.entity.User; /** * 处理用户数据的业务层接口 */ public interface IUserService { /** * 用户注册 * @param user 用户数据对象 */ void reg(User user); } ``` 2.创建一个实现类UserServiceImpl类,放在impl包下 ```java package com.cy.store.service.impl; import com.cy.store.entity.User; import com.cy.store.mapper.UserMapper; import com.cy.store.service.IUserService; import com.cy.store.service.ex.InsertException; import com.cy.store.service.ex.UsernameDuplicatedException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; // 导入 @Service 注解 import org.springframework.util.DigestUtils; import java.util.Date; import java.util.UUID; // 使用 @Service 注解将该类标记为 Spring Bean @Service // 讲当前类的对象交给Spring来管理,自动创建对象,自动注入属性 public class UserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public void reg(User user) { String usernane = user.getUsername(); User result = userMapper.findByUsername(usernane); if (result != null) { throw new UsernameDuplicatedException("用户名被占用!"); } // 密码加密 String oldPassword = user.getPassword(); // 随机生成盐值 String salt = UUID.randomUUID().toString().toUpperCase(); // 加密 String md5Password = getMd5Password(oldPassword, salt); user.setPassword(md5Password); // 将盐值写入到数据库 user.setSalt(salt); user.setIsDelete(0); user.setCreatedUser(usernane); user.setModifiedUser(usernane); Date date = new Date(); user.setCreatedTime(date); user.setModifiedTime(date); Integer rows = userMapper.insert(user); if (rows != 1) { throw new InsertException("用户注册过程中产生未知异常!"); } } /** 定义一个md5加密算法 */ private String getMd5Password(String password, String salt) { for (int i = 0; i < 3; i++) { password = DigestUtils.md5DigestAsHex((salt + password + salt).getBytes()).toUpperCase(); } return password; } } ``` 3.单元测试包中创建一个UserServiceTest ```java package com.cy.store.service; import com.cy.store.entity.User; import com.cy.store.mapper.UserMapper; 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; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class UserServiceTests { /** * 1.必须被@Test修饰 * 2.返回值类型void * 3.方法的参数列表,不指定任何类型,不指定任何参数 * 5.修饰符:public * 4.方法体: * 一般情况下,我们需要测试的方法的操作,都应该是: * 1)调用方法 * 2)断言(判断方法的返回值是否符合预期) */ @Autowired private IUserService userService; @Test public void reg() { try { User user = new User(); user.setUsername("test"); user.setPassword("123"); userService.reg(user); System.out.println("ok!"); } catch (ServiceException e) { // System.out.println(e.getClass().getSimpleName()); System.out.println(e.getMessage()); } } } ``` ## 5.用户注册-控制层 #### 5.1 创建响应 状态码、状态描述信息、数据。这部分封装在一个类中,将这类作为方法返回值,返回给前端浏览器。 ```java /** * Json格式的数据进行响应 */ public class JsonResult implements Serializable { /** 状态码值 */ private Integer state; /** 状态描述信息 */ private String message; /** 响应数据 */ // E 表示响应数据的类型,在具体使用时,需要指定具体的类型 private E data; ``` #### 5.2 设计请求 依据当前的业务功能模块进行请求的设计 ``` 请求路径:/users/reg 请求参数:User user 请求类型:PUST 响应结果:JsonResult ``` #### 5.3 处理请求 1.创建一个控制层对应的UserController类,依赖于业务层接口。 ```java package com.cy.store.controller; import com.cy.store.entity.User; import com.cy.store.service.IUserService; import com.cy.store.service.ex.InsertException; import com.cy.store.service.ex.UsernameDuplicatedException; import com.cy.store.util.JsonResult; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; //@Controller @RestController // @Controller + @ResponseBody @RequestMapping("users") public class UserController { @Autowired private IUserService userService; @RequestMapping("reg") public JsonResult reg(User user) { JsonResult result = new JsonResult<>(); try { userService.reg(user); result.setState(200); result.setMessage("用户注册成功!"); } catch (UsernameDuplicatedException e) { result.setState(40000); result.setMessage("用户名被占用!!"); } catch (InsertException e) { result.setState(50000); result.setMessage("注册时产生未知异常!!!!"); } return result; } } ``` 1.创建一个控制层对应的UserController类, #### 5.4 控制层优化设计 在控制层抽离出一个父类,在这个父类中统一去处理异常相关操作。编写一个BaseController类 ```java package com.cy.store.controller; import com.cy.store.service.ex.InsertException; import com.cy.store.service.ex.ServiceException; import com.cy.store.service.ex.UsernameDuplicatedException; import com.cy.store.util.JsonResult; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.ExceptionHandler; /** * 控制层基类 */ public class BaseController { public static final int OK = 200; /** * 异常处理方法,用于处理项目中抛出的异常,方法返回值就是需要传递给前端的数据 * 自动将异常对象传递给此方法的参数列表上 * 当前项目中抛出的异常统一交给此方法进行处理,此方法的返回值直接交给前端 */ @ExceptionHandler(ServiceException.class) // 异常处理方法 public JsonResult handleException(Throwable e) { JsonResult result = new JsonResult<>(e); if (e instanceof UsernameDuplicatedException) { result.setState(40000); result.setMessage("用户名已经被占用!"); } else if (e instanceof InsertException) { result.setState(50000); result.setMessage("注册时产生了未知的异常!!!"); } return result; } } ``` ## 6.用户注册-前端页面 1.在register页面中编写发送请求的方法,点击事件来完成。选中对应按钮($(“选择器”)),再去添加点击事件,$ajax()函数发送异步请求 2.JQUery封装了一个函数,称为$.ajax()函数,通过对象调用ajax()函数,可以异步加载相关的请求。依靠的是Javascript提供的一个对象XHR(XmlHttpResponse),封装了这个对象。 3.ajax()使用方式。需要传递一个方法体作为方法的参数来使用,一对大括号称之为方法体。ajax接受多个参数,参数与参数之间要求使用“,”进行分割,每组参数之间使用“:”分割,参数的组成部分一个是参数名称(不能随意定义),是参数的值,参数的值要求使用字符串来表识。参数的声明顺序没有要求。语法结构: ```javascript $.ajax({ url: "", type: "", data: "", dataType: "", success: function() { }, error: function() { } }); ``` 4.ajax()函数参数的含义: | 参数 | 功能描述 | | -------- | ------------------------------------------------------------ | | url | 标识请求的地址(url地址)。例如:url:"localhost:/8080/users/reg" | | type | 请求的类型(GET和POST)。例如:type:“POST” | | data | 想指定的请求url地址提交的数据。例如:data: "username=tom&pwd=123" | | dataType | 提交的数据的类型。数据类型一般指定位json类型。dataType:"json" | | success | 当服务器正常响应客户端时,会自动调用success参数的方法,并且将服务器返回的数据以参数的形式传递到这个方法的参数上 | | error | 当服务器异常响应客户端时,会自动调用error参数的方法,并且将服务器返回的数据以参数的形式传递到这个方法的参数上 | 5.js代码可以独立存在在一个js文件里或者声明在一个script标签中。 ```javascript ``` # 用户登录 当用户输入用户名和密码讲数据提交给后台数据库进行查询,如果存在对应的用户名和密码则表示成功,登录成功后跳转到系统的主页就是index.html页面,跳转在前端使用jqery来完成 ## 1.登录-持久层 #### 1.1 规划需要执行的SQL语句 依据用户提交的用户名做select查询,密码的校验在业务层完成 ```sql select * from t_user where username=? ``` > 说明:如果分析过程发现某个功能模块已经被开发完成,可以省略当前开发步骤,分析过分不能省略 #### 1.2 接口设计和方法 > 不用重复开发。单元测试也不许单独执行 ## 2.登录-业务层 #### 2.1 规划异常 1.用户名对应的密码错误,密码匹配异常:PasswordNotMatchException异常,运行时异常,业务异常 2.用户名没有找到,抛出异常:UsernameNotFoundException,运行时异常,业务层异常 3.异常的编写: - 业务层异常需要继承ServiceException异常类 - 再具体的异常类中定义构造方法(可以快捷快生成,有五个构造方法) #### 2.2 设计业务接口和抽象方法 #### 2.3 抽象方法的实现 1.直接在IUserService接口中编写抽象方法,login(String username, String password).将当前登录成功的用户数据以当前用户对象形式返回。状态管理:可以将数据保存在cookie或者session中,可以避免重复的很高的数据多次频繁操作数据进行获取(用户名、用户Id-存放在session中,用户头像-cookie中) ```java package com.cy.store.service; import com.cy.store.entity.User; /** * 处理用户数据的业务层接口 */ public interface IUserService { /** * 用户注册 * @param user 用户数据对象 */ void reg(User user); /** * 用户登录 * @param username 用户名 * @param password 密码 * @return 匹配的用户数据,如果没有匹配的数据,则返回null */ User login(String username, String password); } ``` 2.需要在实现类中实现父类接口的抽象方法 ```java @Override public User login(String username, String password) { User user = userMapper.findByUsername(username); if (user == null) { throw new UserNotFoundException("用户数据不存在!"); } // 检查密码是否匹配 String oldPassword = user.getPassword(); String salt = user.getSalt(); String newMd5Password = getMd5Password(password, salt); // 判断密码是否匹配 if (!oldPassword.equals(newMd5Password)) { throw new PasswordNotMatchException("用户密码错误!"); } // 判断isDelete是否为1 if (user.getIsDelete() == 1) { throw new UserNotFoundException("用户数据不存在!"); } User result = new User(); result.setUid(user.getUid()); result.setUsername(user.getUsername()); result.setAvatar(user.getAvatar()); // 将查询到的用户数据返回 return result; } ``` 3.测试类中测试业务层登录的方法是否可以执行通过 ```java @Test public void login() { User user = userService.login("test001", "123"); System.out.println(user); } ``` ## 3.登录-控制层 #### 3.1 处理异常 业务层抛出的异常是什么,需要再统一的异常处理类中进行捕获和处理,如果也曾抛出异常类型已经再统一异常处理类中曾经处理,则不需要重复添加 ```java else if (e instanceof UserNotFoundException) { result.setState(50001); result.setMessage("用户数据不存在的异常!"); } else if (e instanceof PasswordNotMatchException) { result.setState(50002); result.setMessage("用户名的密码错误的的异常!"); } ``` #### 3.2 设计请求 ``` 请求路径:/users/login 请求方式:POST 请求数据:String username, String password, HttpSession session 响应结果:JsonResult ``` #### 3.3 处理请求 在UserController类中编写处理请求的方法 ```java @RequestMapping("login") public JsonResult login(String username, String password) { User data = userService.login(username, password); return new JsonResult<>(OK,data); } ``` ## 前端请求页面 1.在login.html页面中依据前面所设计的请求来发送ajax请求。 ```java $("#btn-login").click(function () { $.ajax({ url: "/users/login", type: "post", data: $("#form-login").serialize(), dataType: "json", success: function (json) { if (json.state == 200) { alert("登录成功!") // 跳转到系统主页 location.href = "index.html" } else { alert("登录失败!") } }, error: function (xhr) { alert("登录时产生未知的异常" + xhr.message) } }) }) ``` 2.访问页面进行用户登录操作 ## 用户回话session session对象主要存在服务器端,可以用于保持服务器的临时数据的对象,所保存的数据可以在整个项目中都可以通过访问获取,把session的数据看作一个共享的数据。首次登录时所获取的用户数据,转移到session对象中就可以了。session.getAttrbute("key")可以将获取session中的数据这种行为进行封装,封装在BaseController中 1.封装session对象中数据的获取(封装在父类中),数据的设置(用户登录成功后进行数据的设置,设置到全局的session对象中) 2.在父类中封装两个数据:获取uid和获取username对应的两个方法。用户头像暂时不考虑,将来封装到cookie中 ```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(); } ``` 3.在登录的方法中将数据封装在session对象中。服务器本身自动创建有session对象,已经是一个全局的session对象。SpringBoot直接使用session对象,直接将HttpSession类型的对象作为请求处理方法的参数,会自动将全局的session对象注入到请求处理方法的session形参上 ```java @RequestMapping("login") public JsonResult login(String username, String password, HttpSession session) { User data = userService.login(username, password); // 将uid和username保存到session对象中 session.setAttribute("uid",data.getUid()); session.setAttribute("username",data.getUsername()); // 获取session对象的数据 System.out.println(getUidFromSession(session)); System.out.println(getUsernameFromSession(session)); return new JsonResult<>(OK,data); } ``` ## 拦截器 拦截器:首先将所有的请求拦截到拦截器中,可以在拦截器中定义过滤的规则,如果不满足系统的设置规则,统一的处理是重新去打开login.html页面(重定向和转发),推荐使用重定向 在SpringBoot项目中拦截器的定义和使用。SpringBoot是依靠SpringMVC来完成的。SpringMVC提供了一个HanderInterceptor接口,用于表示一个拦截器。首先自定义一个类,在这个类中是想这个接口 1.首先自定义一个类,实现HanderInterceptor接口 ```java package com.cy.store.interceptor; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.web.servlet.HandlerInterceptor; /** * 登录拦截器 */ public class LoginInterceptor implements HandlerInterceptor { /** * 检测全局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 obj = request.getSession().getAttribute("uid"); if (obj == null) { // 没有登录 // 重定向到登录页面 response.sendRedirect("/web/login.html"); // 不放行 return false; } // 放行 return true; } } ``` HanderInterceptor接口源码解析: ```java public interface HandlerInterceptor { // 在调用所有处理请求的方法之前被自动调用执行的方法 default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { return true; } // 在ModelAndView对象返回之后被调用 default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception { } // 在整个请求所用关联的资源被执行完毕最后执行的方法 default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception { } } ``` 2.注册过滤器:添加白名单(哪些资源可以在不登录的情况下访问:login.html/request.html/login/reg/index.html/product.html)、添加黑名单(在用户登录的情况下才能访问的页面资源)。 3.注册过滤器的技术:借助WebMvcConfigure接口,可以将用户定义的拦截器进行注册,才能保证拦截器能够生效和使用。定义一个类,然后这个类实现WebMvcConfigure接口。配置信息,建议存放在项目包结构的configure下。 ```java package com.cy.store.config; import com.cy.store.interceptor.LoginInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.List; /** * 登录拦截器配置类 */ @Configuration // 该类是一个配置类,用于配置拦截器对象 public class LoginInterceptorConfigurer implements WebMvcConfigurer { // 配置拦截器对象 @Override public void addInterceptors(InterceptorRegistry registry) { // 创建自定义拦截器对象 HandlerInterceptor interceptor = new LoginInterceptor(); // 配置白名单,存放在数组中 List patterns = new ArrayList(); patterns.add("/bootstrap/**"); patterns.add("/css/**"); patterns.add("/images/**"); patterns.add("/fonts/**"); patterns.add("/web/register.html"); patterns.add("/web/login.html"); patterns.add("/web/index.html"); patterns.add("/web/product.html"); patterns.add("/users/reg"); patterns.add("/users/login"); // 完成拦截器的注册 registry.addInterceptor(interceptor) .addPathPatterns("/**") .excludePathPatterns(patterns); // 配置拦截的路径 } } ``` 源代码方法: ```Java // 将自定义的拦截器进行注册 default void addInterceptors(InterceptorRegistry registry) { } ``` 4.提示重定向次数过多,login.html页面无法打开。将浏览器cookie情况,将浏览器初始化一下。 ## 修改密码 需要用户提交原始密码和新密码,再根据当前登录的用户进行信息的修改操作。 ### 1.修改密码-持久层 #### 1.1.规划需要执行的SQL语句 根据用户的uid修改password: ```mysql updata t_user set password=?,modified_user=?,modified_time=? where uid=? ``` 根据uid查询用户的数据。再修改密码之前,首先要保证这用户数据存在,检验是被标记为已经删除、检测输入的原始密码是否正确 ```mysql select * from t_user where uid=? ``` #### 1.2 设计接口和抽象方法 userMapper接口,将以上两个方法的抽象定义出来。将来映射到SQL语句上 ```java /** * 根据uid查询用户数据 * @param uid 用户的id * @return 匹配的用户数据,如果没有匹配的数据,则返回null */ User findByUid(Integer uid); /** * 更新用户的密码 * @param uid 用户的id * password 新密码 * modifiedUser 修改执行人 * modifiedTime 修改时间 * @return 受影响的行数 */ Integer updatePasswordByUid(Integer uid, String password, String modifiedUser, String modifiedTime); ``` #### 1.3 SQL映射 配置到Usermapper.xml文件中 ```mysql UPDATE t_user SET password = #{password}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} where uid = #{uid} ``` 单元测试 ```java @Test public void findById() { User user = userMapper.findByUid(6); System.out.println(user); } @Test public void update() { // 获取当前日期时间 java.time.LocalDateTime now = java.time.LocalDateTime.now(); // 定义日期时间格式 java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 格式化日期时间 String formattedDateTime = now.format(formatter); userMapper.updatePasswordByUid(6, "321", "管理员", formattedDateTime); } ``` ### 2.修改密码-业务层 #### 2.1 规划异常 1.用户的原密码错误,is_delete==1,uid找不到,找不到用户的异常 2.update在更新时可能产生未知异常:UpdateException ```java package com.cy.store.service.ex; /** * 数据更新异常 */ public class UpdateException extends ServiceException{ public UpdateException() { super(); } public UpdateException(String message) { super(message); } public UpdateException(String message, Throwable cause) { super(message, cause); } public UpdateException(Throwable cause) { super(cause); } protected UpdateException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ``` #### 2.2设计接口和抽象方法 执行用户修改密码的方法 ```java void changePassword(Integer uid, String username, String oldPassword, String newPassword ); ``` 在实现类中实现抽象方法 ```java @Override 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 salt = result.getSalt(); String md5Password = getMd5Password(oldPassword, salt); if (!result.getPassword().equals(md5Password)) { throw new PasswordNotMatchException("密码错误!"); } // 新密码加密 String newMd5Password = getMd5Password(newPassword, salt); java.time.LocalDateTime now = java.time.LocalDateTime.now(); // 定义日期时间格式 java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 格式化日期时间 String formattedDateTime = now.format(formatter); Integer row = userMapper.updatePasswordByUid(uid, newPassword, username, formattedDateTime); if (row != 1) { throw new UpdateException("更新数据时产生未知错误!"); } } ``` 单元测试 ```java @Test public void changePassword() { userService.changePassword(4,"管理员","123456","321"); } ``` ### 3.修改密码-控制层 #### 3.1 处理异常 UpdateException需要配置在统一异常处理类中 ```java else if (e instanceof UpdateException) { result.setState(50003); result.setMessage("更新数据时产生未知的异常!"); } ``` #### 3.2 设计请求 ``` /users/change_password post String oldPassword, String newPassword //需要和表单的name属性保持一致 Json ``` #### 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.修改密码-前端页面 ```javascript ``` ## 修改个人资料 ### 1. 个人资料-持久层 #### 1.1 规划执行的SQL语句 1.更新用户信息的SQL语句 ```sql update t_user set phone=?,email=?,gender=?,modified_user=?,modified=_time=? where uid=? ``` 2.查询用户数据 ```mysql select * form t_user where uid=? ``` > 查询用户不需要重复开发 #### 1.2 接口与抽象方法 更新用户的信息方法的定义 ```java /** * 更新用户的基本信息 * @param user 用户的基本信息 * @return 受影响的行数 */ Integer updateInfoByUid(User user); ``` #### 1.3 抽象方法的映射 UserMapper.xml编写 ```xml UPDATE t_user SET phone = #{phone}, email = #{email}, gender = #{gender}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} where uid = #{uid} ``` 单元测试 ```java @Test public void updateInfoByUid() { User user = new User(); user.setUid(4); user.setPhone("123456"); user.setEmail("lll@qq.com"); user.setGender(0); userMapper.updateInfoByUid(user); } ``` ### 2. 个人资料-业务层 #### 2.1 异常规划 1.设计两个功能 - 打开页面时获取用户信息并填充到文本框 - 检测用户是否点击了修改按钮,如果检测到执行用户修改信息操作 2.打开页面找不到数据,点击删除按钮之前需要再次检测用户数据是否存在 #### 2.2 设计接口和抽象方法 主要有两个功能模块,对应两个抽象方法 ```java /** * 根据uid查询用户数据 * @param uid 用户的id * @return 匹配的用户数据,如果没有匹配的数据,则返回null */ User getByUid(Integer uid); /** * 更新用户的基本信息 * @param uid * @param username * @param user * @return 受影响的行数 */ void changeInfo(Integer uid,String username,User user); ``` #### 2.3 实现抽象方法 在UserServiceimply类中添加两个抽象方法的具体实现 ```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.setPhone(result.getPhone()); user.setEmail(result.getEmail()); user.setGender(result.getGender()); return user; } @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 row = userMapper.updateInfoByUid(user); if (row != 1) { throw new UpdateException("更新数据时产生未知异常!"); } } ``` 单元测试 ```java @Test public void changeInfo() { User user = new User(); user.setPhone("123456"); user.setEmail("tom001@qq.com"); user.setGender(1); userService.changeInfo(5, "tom001", user); } ``` ### 3. 个人资料控制层 #### 3.1 异常处理 > 暂无 #### 3.2 设计请求 1.设置一打开页面就发送当前用户数据的查询 ``` /users/get_by_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) { // user对象有四个数据:phone,email,gender Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); userService.changeInfo(uid, username, user); return new JsonResult<>(OK); } ``` ### 个人资料-前端页面 1.再打开userdata.html页面自动发送Ajax请求(get_by_uid),查询到的数据填充到页面上 ```javascript ``` 2.在检测到用户点击了修改按钮之后也需要发送一个Ajax请求(change_info) ```javascript $("#btn-change-info").click(function() { $.ajax({ url: "/users/change_info", type: "POST", data: $("#form-change-info").serialize(), dataType: "json", success: function(json) { if (json.state === 200) { alert("用户信息修改成功!"); // 刷新页面 location.href = "userdata.html"; } else { alert("用户信息修改失败!"); } }, error: function (xhr) { alert("用户信息修改时时产生未知的错误或异常!" + xhr.status); } }) }) ``` # 上传头像 ### 1. 上传头像-持久层 #### 1.1 规划SQL语句 将对象文件保存在操作系统上,然后把文件的路径记录下来,因为记录路径是非常便捷的,将来如果要打开这个文件可以根据路径去寻找。在**数据库中保存这个文件的路径**即可。将所有的静态资源(图片、文件、其他资源文件)放到某台电脑上,再把这台电脑作为一台单独的服务器使用 对应的是一个更新用户avator字段的SQL语句 ```mysql update t_user set avator=?,modified_time=?,modified_user=? where uid=? ``` #### 2.2 设计接口和抽象方法 UserMapper接口定义个抽象方法用于修改用户头像 ```java /** * @Param("SQL映射文件的#{}占位符的变量名"):解决的问题是:当SQL语句的占位符的变量名与方法的参数名不一致时, * 可以使用该注解来指定SQL语句的占位符的变量名 * 更新用户头像 * @param uid 用户的id * @param avator 头像的路径 * @param modifiedUser 修改执行人 * @param modifiedTime 修改时间 * @return 受影响的行数 */ Integer updateAvatarByUid(@Param("uid") Integer uid, @Param("avator") String avator, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") String modifiedTime); ``` #### 1.3 接口映射 UserMapper.xml文件中 ```xml UPDATE t_user SET avatar = #{avatar}, modified_user = #{modifiedUser}, modified_time = #{modifiedTime} WHERE uid = #{uid} ``` #### 单元测试 ```java @Test public void updateAvatarByUid() { java.time.LocalDateTime now = java.time.LocalDateTime.now(); // 定义日期时间格式 java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 格式化日期时间 String formattedDateTime = now.format(formatter); userMapper.updateAvatarByUid(4, "/update/avatar.png", "管理员", formattedDateTime); } ``` ### 2. 上传头像-业务层 #### 2.1 规划异常 1.用户数据不存在,找不到对应的用户数据 2.更新时异常 > 无需重复 #### 2.2 设计接口和抽象方法 ```java /** * 更新用户的头像 * @param uid 用户的id * @param avatar 头像的路径 * @param username 更新人的用户名 * @return 无返回值 */ 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("用户数据不存在!"); } java.time.LocalDateTime now = java.time.LocalDateTime.now(); // 定义日期时间格式 java.time.format.DateTimeFormatter formatter = java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 格式化日期时间 String formattedDateTime = now.format(formatter); Integer row = userMapper.updateAvatarByUid(uid, avatar, username, formattedDateTime); if (row != 1) { throw new UpdateException("更新数据时产生未知异常!"); } } ``` 单元测试 ```java @Test public void changeAvatar() { userService.changeAvatar(4, "/upload/lll.png", "管理员"); } ``` ### 3. 上传头像-控制层 #### 3.1 规划异常 ``` fileUploadException 泛指文件上传异常(父类)继承RunTimeException 父类是:fileUploadException fileEmptyExcepiton 空文件异常 fileSizeException 文件大小超出限制异常 fileTypeException 文件类型异常 fileUploadIOException 文件读写异常 fileStateExcepiton 文件状态异常 ``` > 五个构造方法显示生命出来,再去继承相关的父类 #### 3.2 处理异常 在基类BaseController类中进行编写和统一处理 ```java else if (e instanceof FileEmptyException) { result.setState(6000); } else if (e instanceof FileSizeException) { result.setState(6001); } else if (e instanceof FileTypeException) { result.setState(6002); } else if (e instanceof FileStateException) { result.setState(6003); } else if (e instanceof FileUploadIOException) { result.setState(6004); } ``` 在异常统一处理方法的参数列表中加入新的异常处理作为他的参数 ```java @ExceptionHandler({ServiceException.class, FileUploadException.class}) ``` #### 设计请求 ``` /users/change_avatar post(get请求数据2kb左右) HttpSession session, MultipartFile file JsonResult ``` #### 处理请求 ```java /** * MultipartFile是SpringMVC提供的一个接口,用于封装上传的文件对象,SprintBoot整合了Spring MVC, * 只需要在请求处理的参数列表上设置MultipartFile类型的参数,然后SpringBoot会自动将上传的文件封装到MultipartFile类型的参数中 *

* @RequestParam 表示请求中的参数,将请求的参数注入到方法的参数列表中, * @RequestParam("name")表示从请求参数里获取名为 name 的值,并将其绑定到方法参数 name 上 *

* 处理头像上传的请求 * @param session session对象 * @param file 上传的文件对象,SpringBoot会将上传的文件封装到MultipartFile类型的参数中 * @return JsonResult对象,包含头像的路径 */ @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("文件超出" + AVATAR_MAX_SIZE + "限制!" ); } // 判断文件类型是否匹配 String contentType = file.getContentType(); if (!AVATAR_TYPE.contains(contentType)) { throw new FileTypeException("文件类型不支持" + contentType + ""); } // 上传的文件../upload/文件.png String parent = session.getServletContext(). getRealPath("upload"); // file对象指向这个路径,file是否存在 File dir = new File(parent); if (!dir.exists()) { dir.mkdirs(); // 创建目录 } // 获取文件的原始文件名 String originalFilename = file.getOriginalFilename(); // 获取文件的后缀名 int index = originalFilename.lastIndexOf("."); String suffix = originalFilename.substring(index); String newFileName = System.currentTimeMillis() + suffix; // 创建一个新的文件对象,用于保存上传的文件 File dest = new File(dir, newFileName); // 空文件 // 参数file中的数据写入这个空文件中 try { file.transferTo(dest); // 将file文件中的数据写入到dest文件中 } catch (FileStateException e) { throw new FileStateException("文件状态异常!"); } catch (IOException e) { throw new FileUploadIOException("文件读写异常!"); } Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); // 返回头像的路径/upload/文件.png String avatar = "/upload" + newFileName; userService.changeAvatar(uid, avatar, username); // 返回头像的路径, 将来可以通过头像的路径访问头像 return new JsonResult<>(OK, avatar); } ``` ### 4. 上传头像-前端页面 在upload页面编写上传头像的代码 > 说明:如果使用表单进行文件上传,需要给表单显示添加一个属性enctype="multipart/from-data"声明出来,不会将目标文件的数据结构做修改再上传,不同于字符串 ### 5. 解决bug #### 5.1更改默认的大小限制 SpringMVC默认1MB文件可以上传,手动修改SpringMVC默认上传文件的大小 方式1:直接在配置文件中修改 ```properties spring.servlet.multipart.max-file-size=10MB spring.servlet.multipart.max-request-size=15MB ``` 方式2:需要采用Java代码的形式来设置文件的上传限制。主类当中配置,可以定义一个方法,必须使用@bean修饰。在类的前面添加@configuration进行修饰。方法返回值是MultipartConfigElement类型 ```java @Bean // public MultipartConfigElement getMultipartConfigElement() { // // 创建一个配置的工厂类对象 // MultipartConfigFactory factory = new MultipartConfigFactory(); // // // 设置文件的最大大小 // factory.setMaxFileSize(DataSize.of(10, DataUnit.MEGABYTES)); // factory.setMaxRequestSize(DataSize.of(15, DataUnit.MEGABYTES)); // // //通过工厂类来创建一个配置对象 // return factory.createMultipartConfig(); // } ``` #### 5.2 显示头像 在页面中提交Ajax请求来提交文件,提交完成后返回json串,解析出data中的数据,设置到imag头像标签的src属性中 - serialize():可以将表单中的数据自动拼接为key=value的结果进行提交给服务器,一般是提交的普通的控件类型中的数据(text\password\radio\chekbx等) - FormData类:将表单中的数据保持原有的结构进行数据的提交。 ```js new FormData($("#form")[0]); //文件类型的数据可以使用FormData对象进行存储 ``` - Ajax默认处理数据是按照字符串的形式进行处理,以及默认采用字符串的形式提交数据。关闭这两个默认 ```js processData: false, // 告诉jQuery不要去处理发送的数据 contentType: false, // 告诉jQuery不要去设置Content-Type请求头 ``` #### 5.3 登录后显示头像 可以在更新头像成功后,将服务器返回的头像路径保存在客户端的cookie对象中,每次检测到用户打开上传头像页面,这个页面中通过read()方法自动去读取cookie中头像路径并设置到src属性中 1.设置cookie中的头像路径 导入cookie.js文件 ``` ``` 调用cookie的方法 ``` $.cookie(key, value, time); // 单位:天 ``` 2.在upload.html中先引入cookie.js文件 ``` ``` 3.登录成功后将avatar设置到cookie中 ```js if (json.data.avatar) { // 定义 cookie 配置 const cookieOptions = { expires: 7 }; // 单位:天 try { // 将服务器返回的头像路径保存到 cookie 中 $.cookie("avatar", json.data.avatar, cookieOptions); } catch (error) { console.error("保存头像路径到 cookie 时出错:", error); } } else { console.log("服务器未返回有效的头像路径"); } console.log(json.data.username) ``` 4.在upload.html中通过read()函数自动读取cookie的数据。 ```js $(document).ready(function () { let avatar = $.cookie("avatar"); console.log(avatar) if (avatar) { $("#img-avatar").attr("src", avatar); } }) ``` #### 5.4 显示最新头像 更新完头像后,将最新的头像地址,再次保存到cookie中,同名保存回家覆盖原有cookie中的值 ```js $.cookie("avatar", json.data, {expirse: 7}) ``` ## 商品热销排名 ### 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. 商品-创建实体类 在com.cy.store包下的entity包下创建Product类 ```java 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; // generate: Getter and Setter, generate hashCode() and equals(),toStirng() } ``` ### 3. 商品热销排行-持久层 #### 3.1 规划SQL语句 ```mysql select * from t_product where status=1 order by priority DESC limit 0,4 ``` #### 3.2 接口和抽象方法 在com.cy.store.mapper文件下创建ProductMapper接口文件 ```java package com.cy.store.mapper; import com.cy.store.entity.Product; import java.util.List; /** * 处理商品数据的持久层接口 */ public interface ProductMapper { /** * 查询热销商品列表 * @return 热销商品列表 */ List findHotList(); } ``` #### 3.3 编写SQL映射 在Resource包下的mapper包下创建ProductMapper.xml ```xml ``` #### 3.4 单元测试 ```java package com.cy.store.mapper; import com.cy.store.entity.Address; 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; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class ProductMapperTests { @Autowired private ProductMapper productMapper; @Test public void findHotList() { List list = productMapper.findHotList(); for (Product product : list) { System.err.println(product); } } } ``` ### 4. 商品热销排行-业务层 #### 4.1 异常规划 无(查询功能一般不需要额外的异常) #### 4.2 接口和抽象方法 在com.cy.store.service包下创建接口IProductService ```java package com.cy.store.service; import com.cy.store.entity.Product; import java.util.List; public interface IProductService { /** * 查询热销商品列表 * @return 热销商品列表 */ List findHostList(); } ``` #### 4.3 实现方法 在com.cy.store.service.impl包下创建ProductServiceImpl类 ```java package com.cy.store.service.impl; import com.cy.store.entity.Product; import com.cy.store.mapper.ProductMapper; import com.cy.store.service.IProductService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class ProductServiceImpl implements IProductService { @Autowired private ProductMapper productMapper; @Override public List findHostList() { List list = productMapper.findHotList(); for (Product product : list) { product.setPriority(null); product.setCreatedTime(null); product.setCreatedUser(null); product.setModifiedTime(null); product.setModifiedUser(null); } return list; } } ``` #### 4.4 单元测试 ```java package com.cy.store.service; import com.cy.store.entity.District; 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.List; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class ProductServiceTests { @Autowired private IProductService productService; @Test public void findHotList() { List list = productService.findHotList(); for (Product product : list) { System.err.println(product); } } } ``` ### 5. 控制层 #### 5.1 处理异常 无(查询操作一般不做处理) #### 5.2 设计请求 ``` url: /product/hot_list type: get data: 无 return: JsonResult> 是否拦截:否,需要将index.html和product/** 添加到白名单 ``` 在LoginInterceptorConfigurer类中将index.html和product/** 添加到白名单 ```java patterns.add("/web/index.html) patterns.add("products/**") ``` #### 5.3 处理请求 ```java package com.cy.store.controller; import com.cy.store.entity.Product; import com.cy.store.service.IProductService; 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 java.util.List; @RestController @RequestMapping("products") public class ProductController extends BaseController{ @Autowired private IProductService productService; @RequestMapping("hot_list") public JsonResult> getHotList() { List data = productService.findHotList(); return new JsonResult<>(OK, data); } } ``` 单元测试 访问http://localhost:8080/products/hot_list ### 6. 前端页面 1.Index.html页面183行代码给热销排行的div标签设置id属性 ```html

``` 加载页面时发送请求展示热销商品 ``` ``` ## 展示商品详情 ### 1. 持久层 #### 1.1 规划SQL语句 ```mysql select * from t_pruduct where id=? ``` #### 1.2 接口和抽象方法 ```java /** * 根据id查询商品数据 * @param id 商品id * @return 匹配的商品数据,如果没有匹配的数据,则返回null */ Product findById(Integer id); ``` #### 1.3 编写SQL语句 ```mysql ``` 单元测试 ```java @Test public void findById() { Product product = productMapper.findById(10000001); System.err.println(product); } ``` ### 2. 业务层 #### 2.1 规划异常 商品数据不存在,抛出ProductNotFoundException ```java package com.cy.store.service.ex; public class ProductNotFoundException extends RuntimeException { public ProductNotFoundException() { } public ProductNotFoundException(String message) { super(message); } public ProductNotFoundException(String message, Throwable cause) { super(message, cause); } public ProductNotFoundException(Throwable cause) { super(cause); } public ProductNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ``` #### 2.2 接口和抽象方法 ```java /** * 根据id查询商品数据 * @param id 商品id * @return 匹配的商品数据,如果没有匹配的数据,则返回null */ Product findById(Integer id); ``` #### 2.3 实现方法 ```java @Override public Product findById(Integer id) { Product product = productMapper.findById(id); if (product == null) { throw new ProductNotFoundException("商品数据不存在!"); } product.setPriority(null); product.setCreatedTime(null); product.setCreatedUser(null); product.setModifiedTime(null); product.setModifiedUser(null); return product; } ``` 单元测试 ### 3. 控制层 #### 3.1 处理异常 BaseController中t添加 ``` else if (e instanceof ProductNotFoundException) { result.setState(5006); result.setMessage("插入数据产生了未知的异常!!!"); } ``` #### 3.2 设计请求 ``` url:/products/details/{id} data: @PathVariable("id") Integer id type:GET return: JsonResult ``` #### 3.3 实现请求 ```java else if (e instanceof ProductNotFoundException) { result.setState(5006); result.setMessage("插入数据产生了未知的异常!!!"); } ``` 单元测试 访问http://localhost:8080/products/details/10000042 ### 4. 前端页面 拦截器释放,非登录加载../js/jquery-getUrlParam.js,以便使用$.getUrlParam("id")函数获取网页的id属性发送给后端请求 ``` patterns.add("/js/**"); ``` 页面加载时发送Ajax请求 ```js ``` # 新增收货地址 ### 1. 设计数据表 ```mysql 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; ``` ### 2. 新增收货地址-创建实体类 ```java package com.cy.store.entity; import java.util.Objects; public class Address extends BaseEntity{ private Integer aid; private Integer uid; private String name; private String provinceNme; 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; public Integer getAid() { return aid; } public void setAid(Integer aid) { this.aid = aid; } public Integer getUid() { return uid; } public void setUid(Integer uid) { this.uid = uid; } public String getName() { return name; } public void setName(String name) { this.name = name; } public String getProvinceNme() { return provinceNme; } public void setProvinceNme(String provinceNme) { this.provinceNme = provinceNme; } public String getProvinceCode() { return provinceCode; } public void setProvinceCode(String provinceCode) { this.provinceCode = provinceCode; } public String getCityName() { return cityName; } public void setCityName(String cityName) { this.cityName = cityName; } public String getCityCode() { return cityCode; } public void setCityCode(String cityCode) { this.cityCode = cityCode; } public String getAreaName() { return areaName; } public void setAreaName(String areaName) { this.areaName = areaName; } public String getAreaCode() { return areaCode; } public void setAreaCode(String areaCode) { this.areaCode = areaCode; } public String getZip() { return zip; } public void setZip(String zip) { this.zip = zip; } public String getAddress() { return address; } public void setAddress(String address) { this.address = address; } public String getPhone() { return phone; } public void setPhone(String phone) { this.phone = phone; } public String getTel() { return tel; } public void setTel(String tel) { this.tel = tel; } public String getTag() { return tag; } public void setTag(String tag) { this.tag = tag; } public Integer getIsDefault() { return isDefault; } public void setIsDefault(Integer isDefault) { this.isDefault = isDefault; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; if (!super.equals(o)) return false; Address address1 = (Address) o; return Objects.equals(getAid(), address1.getAid()) && Objects.equals(getUid(), address1.getUid()) && Objects.equals(getName(), address1.getName()) && Objects.equals(getProvinceNme(), address1.getProvinceNme()) && Objects.equals(getProvinceCode(), address1.getProvinceCode()) && Objects.equals(getCityName(), address1.getCityName()) && Objects.equals(getCityCode(), address1.getCityCode()) && Objects.equals(getAreaName(), address1.getAreaName()) && Objects.equals(getAreaCode(), address1.getAreaCode()) && Objects.equals(getZip(), address1.getZip()) && Objects.equals(getAddress(), address1.getAddress()) && Objects.equals(getPhone(), address1.getPhone()) && Objects.equals(getTel(), address1.getTel()) && Objects.equals(getTag(), address1.getTag()) && Objects.equals(getIsDefault(), address1.getIsDefault()); } @Override public int hashCode() { return Objects.hash(super.hashCode(), getAid(), getUid(), getName(), getProvinceNme(), getProvinceCode(), getCityName(), getCityCode(), getAreaName(), getAreaCode(), getZip(), getAddress(), getPhone(), getTel(), getTag(), getIsDefault()); } @Override public String toString() { return "Address{" + "aid=" + aid + ", uid=" + uid + ", name='" + name + '\'' + ", provinceNme='" + provinceNme + '\'' + ", provinceCode='" + provinceCode + '\'' + ", cityName='" + cityName + '\'' + ", cityCode='" + cityCode + '\'' + ", areaName='" + areaName + '\'' + ", areaCode='" + areaCode + '\'' + ", zip='" + zip + '\'' + ", address='" + address + '\'' + ", phone='" + phone + '\'' + ", tel='" + tel + '\'' + ", tag='" + tag + '\'' + ", isDefault=" + isDefault + '}'; } } ``` ### 3. 新增收货地址-持久层 #### 3.1 各功能的开发顺序 当前收货地址模块:列表的展示、修改、删除、设置默认、新增收货地址。开发顺序:新增收货地址-列表的展示-设置默认收货地址-删除收货地址-修改收货地址 #### 3.2 规划需要执行的SQL语句 1.新增收货地址-插入语句 ```mysql insert into t_address (除aid外字段列表) values (字段值列表); ``` 2.一个用户的收货地址规定最多只能有20条数据对应.插入用户数据之前先做查询操作.收货地址逻辑控制的异常 ```mysql select count(*) from t_address where uid=? ``` #### 3.3 接口和抽象方法 创建一个接口AddressMapper,在这个接口定义两个SQL语句的抽象方法 ```java package com.cy.store.mapper; import com.cy.store.entity.Address; /* 表示收货地址的持久层接口 */ public interface AddressMapper { /** * 插入用户的收货地址数据 * @param address 收货地址数据 * @return 受影响的行数 */ Integer insert(Address address); /** * 根据uid统计用户的收货地址数据条数 * @param uid 用户的id * @return 匹配的收货地址数据总数,如果没有匹配的数据,则返回null */ Integer countByUid(Integer uid); } ``` #### 3.4 配置SQL映射 创建一个AddressMapper.xml映射文件,在这个方法中添加抽象方法映射 ```xml INSERT INTO t_address ( uid, name, province_name, province_code, city_name, city_code, area_name, area_code, zip, address, phone, is_default, created_user, created_time, modified_user, modified_time ) VALUES ( #{aid}, #{name}, #{provinceNme}, #{provinceCode}, #{cityName}, #{cityCode}, #{areaName}, #{areaCode}, #{zip}, #{address}, #{phone}, #{tel}, #{tag}, #{isDefault}, d#{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime} ) ``` #### 3.5 单元测试 ```java package com.cy.store.mapper; import com.cy.store.entity.Address; import com.cy.store.entity.User; 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; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class AddressMapperTests { @Autowired private AddressMapper addressMapper; @Test public void insert() { Address address = new Address(); address.setUid(7); address.setName("江熠"); address.setPhone("1234567"); addressMapper.insert(address); } @Test public void countByUid() { Integer count = addressMapper.countByUid(7); System.out.println(count); } } ``` ### 4. 新增收货地址-业务层 ##### 4.1 规划异常 如果用户是第一次插入用户的收货地址,规则:当用户插入的数据是第一条,需要将当前的地址作为默认的收货地址,如果查询到的统计总数为0则将当前地址的is_default设置为1.查询统计的结果为0不代表异常 查询到的结果大于20,这时候需要抛出业务控制的异常AddressCountLimitException异常。 ```java package com.cy.store.controller.ex; import com.cy.store.service.ex.ServiceException; /* 表示收货地址数量超出限制的异常(20条) */ public class AddressCountLimitException extends ServiceException { public AddressCountLimitException() { super(); } public AddressCountLimitException(String message) { super(message); } public AddressCountLimitException(String message, Throwable cause) { super(message, cause); } public AddressCountLimitException(Throwable cause) { super(cause); } protected AddressCountLimitException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ``` 插入数据时异常,需要重复创建 #### 4.2 接口与抽象方法 1.创建一个IAddressService接口,在其中定义业务的抽象方法。 ```java package com.cy.store.service; import com.cy.store.entity.Address; /* 处理收货地址数据的业务层接口 */ public interface IAddressService { /** * 新增收货地址 * @param uid 用户的id * @param username 用户名 * @param address 收货地址数据 */ void addNewAddress(Integer uid, String username, Address address); } ``` #### 4.3 实现抽象方法 创建一个AddressServiceImpy实现类,去实现接口的抽象方法 配置项设置最多地址条数 ``` # spring读取配置文件的数据: @value("${user.address.max-count}"} user.address.max-count=20 ``` ```java package com.cy.store.service.impl; import com.cy.store.controller.ex.AddressCountLimitException; import com.cy.store.entity.Address; import com.cy.store.mapper.AddressMapper; import com.cy.store.service.IAddressService; import com.cy.store.service.ex.InsertException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import java.util.Date; @Service 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("用户收货地址超出上限"); } // uid isDefault address.setUid(uid); Integer isDefault = count == 0 ? 1 : 0; // 0: 不是默认地址 1: 是默认地址 address.setIsDefault(isDefault); // 补全四项日志 address.setCreatedUser(username); address.setModifiedUser(username); address.setCreatedTime(new Date()); address.setModifiedTime(new Date()); // 插入数据 Integer row = addressMapper.insert(address); if (row != 1) { throw new InsertException("插入用户的收货地址数据时产生未知的异常!"); } } } ``` #### 4.4 单元测试 新建一个AddressService测试类 ```java package com.cy.store.service; import com.cy.store.entity.Address; import com.cy.store.entity.User; 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; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class AddressServiceTests { @Autowired private IAddressService addressService; @Test public void addNewAddress() { Address address = new Address(); address.setPhone("123456789"); address.setName("江熠"); addressService.addNewAddress(6, "管理员", address); } } ``` ### 5. 新增收货地址-控制层 #### 5.1 处理异常 业务层抛出了收货地址总数超标的异常,在BaseController中进行捕获 ```java else if (e instanceof AddressCountLimitException) { result.setState(4003); result.setMessage("用户的收货地址超出上限的异常!"); } ``` #### 5.2 设计请求 ``` /address/add_new_address post Address address, HttpSession session JsonResult ``` #### 5.3 处理请求 在控制层创建AddressController来处理用户收货地址的请求和响应 ```java package com.cy.store.controller; import com.cy.store.entity.Address; import com.cy.store.service.IAddressService; import com.cy.store.util.JsonResult; import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RequestMapping("address") @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); } } ``` #### 5.4 单元测试 登录后,访问http://localhost:8080/address/add_new_address?name=Tom&phone=17284345进行测试 ### 6. 新增收货地址-前端页面 ```js ``` # 获取省市区列表 ### 1. 获取省市区列表-数据库 ```mysql 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; ``` parent:父区域代码号,code:自身代码号,name:自身名字,省的父代码号+86. ### 2. 获取省市区列表-实体类 创建一个District实体类 ```java /* 省、市、区的实体类 */ public class District { private Integer id; private String parent; private String code; private String name; ``` ### 2. 获取省市区列表-持久层类 #### 2.1 规划SQL语句 查询语句,根据父代号进行查询 ```mysql select * from t_dict_district where parent=? order by code ASC ``` #### 2.2. 接口和抽象方法 创建DistrictMapper接口 ```java package com.cy.store.mapper; import com.cy.store.entity.District; import java.util.List; public interface DistrictMapper{ /** * 根据父级代号查询子级数据列表 * @param parent 父级代号 * @return 子级数据列表 */ List findByParent(String parent); } ``` #### 2.3 编写SQL映射 ```xml ``` #### 2.4 单元测试 ```java package com.cy.store.mapper; import com.cy.store.entity.Address; import com.cy.store.entity.District; 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; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class DistrictMapperTests { @Autowired private DistrictMapper districtMapper; @Test public void findByParentId() { List list = districtMapper.findByParent("210100"); for(District d : list) { System.out.println(d); } } } ``` ### 3. 获取省市区列表-业务层 #### 3.1 创建接口IDistrictService,定义抽象方法 ```java package com.cy.store.service; import com.cy.store.entity.Address; import com.cy.store.entity.District; import java.util.List; /* 处理收货地址数据的业务层接口 */ public interface IDistrictService { /** * 根据父级代号查询子级数据列表(省市区) * @param parent 父级代号 * @return 子级数据列表 */ List getByParent(String parent); } ``` #### 3.2 实现接口 创建DistrictServiceImpl实现类,实现抽象方法 ```java package com.cy.store.service.impl; import com.cy.store.entity.District; import com.cy.store.mapper.DistrictMapper; import com.cy.store.service.IDistrictService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class DistrictServiceImpl implements IDistrictService { @Autowired private DistrictMapper districtMapper; /** * 根据父级代号查询子级数据列表(省市区) * @param parent 父级代号 * @return 子级数据列表 */ @Override public List getByParent(String parent) { List list = districtMapper.findByParent(parent); /** * 网络数据传输时为了尽量避免无效数据的传输,需要将无效数据设置为null * 可以节省流量,另一方面提高程序的性能 * id和parent属性设置为null,只传输name属性和code属性 */ for (District d : list) { d.setId(null); d.setParent(null); } return list; } } ``` #### 3.3 单元测试 ```java package com.cy.store.service.impl; import com.cy.store.entity.District; import com.cy.store.mapper.DistrictMapper; import com.cy.store.service.IDistrictService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class DistrictServiceImpl implements IDistrictService { @Autowired private DistrictMapper districtMapper; /** * 根据父级代号查询子级数据列表(省市区) * @param parent 父级代号 * @return 子级数据列表 */ @Override public List getByParent(String parent) { List list = districtMapper.findByParent(parent); /** * 网络数据传输时为了尽量避免无效数据的传输,需要将无效数据设置为null * 可以节省流量,另一方面提高程序的性能 * id和parent属性设置为null,只传输name属性和code属性 */ for (District d : list) { d.setId(null); d.setParent(null); } return list; } } ``` ### 4. 获取省市区列表-控制层 #### 4.1 规划异常 无 #### 4.2 设计请求 ``` /districts/ get String parent JsonResult> ``` #### 4.3 实现请求 创建DistrictController类,来编写处理请求的方法 ```java package com.cy.store.controller; import com.cy.store.entity.District; import com.cy.store.service.IDistrictService; 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 java.util.List; @RestController @RequestMapping("districts") public class DistrictController extends BaseController { @Autowired private IDistrictService districtService; // districts开头的请求,都会被拦截,然后执行下面的方法 @RequestMapping({"/", ""}) public JsonResult> getByParent(String parent) { List data = districtService.getByParent(parent); return new JsonResult<>(OK, data); } } ``` districtsd请求添加白名单 ```java patterns.add("districts/**"); ``` #### 单元测试 访问http://localhost:8080/districts?parent=86或http://localhost:8080/districts**/**?parent=610000进行测试 ### 5. 获取省市区列表-前端页面 1.注释掉通过js完成省市区列表加载的js代码 ```js ``` 2.检查前端页面再提交省市区数据时是否有相关name和id属性 3.运行前端看是否可以正常保存数据(除省市区外) ## 获取省市区的名称 ### 1. 获取省市区的名称-持久层 #### 1.1 规划SQL语句 根据当前code来获取当前省市区的名称 ```mysql select name from t_dict_district where code=? ``` #### 1.2 抽象接口和方法 DistrictMapper中定义抽象方法 ```java String findNameByCode(String code); ``` #### 1.3 编写映射 DistrictMapper.xml中 ```xml ``` #### 1.4 单元测试 ```java @Test public void findByCode() { String name = districtMapper.findNameByCode("610000"); System.out.println(name); } ``` ### 2. 获取省市区的名称-业务层 #### 2.1 规划异常 无 #### 2.2 接口和抽象方法 ```java /** * 根据代号查询名称 * @param code 代号 * @return 名称 */ String getNameByCode(String code); ``` #### 2.3 方法实现 ```java @Override public String getNameByCode(String code) { return districtMapper.findNameByCode(code); } ``` ### 3. 获取省市区的名称-控制层 #### 3.1 添加依赖 AdderssServiceimpl添加地址层依赖于IDistrictService层 ```java // 添加用户的收货地址数据时业务层依赖于IDistrictService接口,所以需要将IDistrictService接口的实现类注入到业务层 @Autowired private IDistrictService districtService; ``` #### 3.2 增添依赖数据 在addNewAddress方法中将districtService接口中获取的省市区数据转移到address对象,这个对象就包含收货地址的所有数据了. code值通过前端传输包含在Address对象里 ```java // 补全数据:省市区的名称 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.1 规划SQL语句 数据库查询操作 ```mysql select * from t_address where uid=? order by is_default DESC,created_time DESC; ``` #### 1.2 接口和抽象方法 ```java /** * 根据uid查询用户的收货地址数据列表 * @param uid 用户的id * @return 匹配的收货地址数据列表,如果没有匹配的数据,则返回null */ List
findByUid(Integer uid); ``` #### 1.3 SQL映射 ```xml ``` #### 1.4 单元测试 ```java @Test public void findByUid() { List
list = addressMapper.findByUid(7); for (Address a : list) { System.out.println(a); } } ``` ### 2. 收货地址列表展示-业务层 #### 2.1 规划异常 无 #### 2.2 接口和抽象方法 ```java /** * 根据用户的id查询收货地址列表 * @param uid 用户的id * @return 该用户的收货地址列表 */ List
getAddressByUid(Integer uid); ``` #### 2.3 实现方法 ```java @Override public List
getAddressByUid(Integer uid) { List
list = addressMapper.findByUid(uid); for (Address address : list) { // address.setAid(null); // address.setUid(null); address.setProvinceCode(null); address.setCityCode(null); address.setAreaCode(null); address.setZip(null); address.setTel(null); address.setCreatedTime(null); address.setModifiedTime(null); address.setCreatedUser(null); address.setModifiedUser(null); address.setIsDefault(null); } return list; } ``` #### 2.4 单元测试 ### 3. 收货地址列表展示-控制层 #### 3.1 设计请求 ``` /addresses HttpSession session GET JsonResult> ``` #### 3.2 实现请求 ```java @RequestMapping({"/", ""}) public JsonResult> getAddressByUid(HttpSession session) { Integer uid = getUidFromSession(session); List
data = addressService.getAddressByUid(uid); return new JsonResult<>(OK, data); } ``` #### 3.3 单元测试 先登录后访问http://localhost:8080/address得到返回的数据列表 ### 4. 收货地址列表展示-前端页面 在address.html页面编写查询用户收货地址的展示列表 ```js ``` ## 设置默认收货地址 ### 1. 持久层 #### 1.1 规划SQL语句 1.检测当前用户设置为默认收货地址的数据是否存在 ```mysql select * from t_address where aid=? ``` 2.修改用户的某人地址之前,现将所有的收货地址设置为非默认 ```mysql update t_address set is_default=0 where uid=? ``` 3.将用户当前选中的这条记录设为默认 ```mysql update t_address set is_default=1, modified_user=?, modified_time=? where aid=? ``` #### 1.2 设计抽象方法 AddressMapper接口中 ```java /** * 根据aid查询收货地址数据 * @param aid 收货地址数据的id * @return 匹配的收货地址数据,如果没有匹配的数据,则返回null */ Address findByAid(Integer aid); /** * 根据uid查询用户的最后修改的收货地址数据 * @param uid 用户的id * @return 返回受影响的行数,如果没有匹配的数据,则返回null */ Integer updateNonDefault(Integer uid); /** * 根据aid更新收货地址数据为默认 * @param aid 收货地址数据的id * @param modifiedUser 修改执行人 * @param modifiedTime 修改时间 * @return 返回受影响的行数,如果没有匹配的数据,则返回null */ Integer updateDefaultByAid(@Param("aid") Integer aid, @Param("modifiedUser") String modifiedUser, @Param("modifiedTime") Date modifiedTime); ``` #### 1.3 配置SQL映射 ```xml UPDATE t_address SET is_default = 0 WHERE uid = #{uid} UPDATE t_address SET is_default = 1, modified_time = #{modifiedTime}, modified_user = #{modifiedUser} WHERE aid = #{aid} ``` #### 1.4 单元测试 ```java @Test public void findByAid() { Address address = addressMapper.findByAid(9); System.err.println(address); } @Test public void update() { addressMapper.updateNonDefault(7); } @Test public void delete() { addressMapper.updateDefaultByAid(9, "管理员", new Date()); } ``` ### 2. 业务层 #### 2.1 规划异常 1.更新时产生未知的UpdateException异常。 2.访问的数据不是当前登录用户的收货数据,非法访问:AccessDeniedException 3.收货地址可能不存在异常:AddressNotFoundException #### 2.2 接口和抽象方法 在接口IAddressService中编写抽象方法 ```java /** * 修改某个用户的某收货地址为默认收货地址 * @param aid 收货地址id * @param uid 用户的id * @param username 修改执行人的用户名 */ void setDefault(Integer aid, Integer uid, String username); ``` #### 2.3 方法实现 AddressServiceImpl开发 ```java @Override public void setDefault(Integer aid, Integer uid, String username) { Address address = addressMapper.findByAid(aid); if (address == null) { throw new AddressNotFoundException("收货地址不存在!"); } // 检测当前获取的收货地址数据的归属是否为当前登录的用户 if (!address.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("更新用户的收货地址数据时产生未知的异常!"); } } ``` #### 2.4 单元测试 ```java @Test public void setDefaultAddress() { addressService.setDefault(8, 7, "管理员"); } ``` ### 3. 控制层 #### 3.1 处理异常 BaseController中统一处理 ```java else if (e instanceof AddressNotFoundException) { result.setState(4004); result.setMessage("用户的收货地址不存在的异常!"); } else if (e instanceof AccessDeniedException) { result.setState(4005); result.setMessage("非法访问的异常!"); } ``` #### 3.2 设计请求 ``` /address/{aid}/set_default // Restful风格 // @PathVariable("aid") Integer aid 把参数aid注入到@PathVariable("aid") @PathVariable("aid) Integer aid, HttpSession session POST JsonResult ``` #### 3.3 实现请求 ```java @RequestMapping("{aid}/set_default") public JsonResult setDefault(@PathVariable("aid") Integer aid, HttpSession session) { Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); addressService.setDefault(aid, uid, username); return new JsonResult<>(OK); } ``` #### 3.4 单元测试 登录后访问http://localhost:8080/address/9/set_default ### 前端页面 1.给设置默认收货地址按钮添加一个onclick属性,指向一个方法的调用,在这个方法中完成Ajax请求 ```js let tr = '\n' + '#{tag}\n' + '#{name}\n' + '#{address}\n' + '#{phone}\n' + ' 修改\n' + ' 删除\n' + // 更改 '设为默认\n' + ''; tr = tr.replace(/#{tag}/g, list[i].tag); tr = tr.replace(/#{name}/g, list[i].name); tr = tr.replace(/#{address}/g, list[i].provinceName + list[i].cityName + list[i].areaName + list[i].address); tr = tr.replace(/#{phone}/g, list[i].phone); // 更改 tr = tr.replace(/#{aid}/g, list[i].aid); ``` address.html页面点击“设置默认”按钮,发送Ajax请求.完成setDefault()方法声明和定义 ```js function setDefault(aid) { $.ajax({ url: "/address/" + aid + "/set_default", type: "POST", dataType: "json", success: function(json) { if (json.state === 200) { // 重新加载收货列表 showAddressList() } else { alert("设置默认收货地址失败!"); } }, error: function (xhr) { alert("设置默认收货地址时产生未知的错误或异常!" + xhr.status); } }) } ``` 单元测试 ## 删除收货地址 ### 1. 持久层 #### 1.1 规划SQL语句 1.删除之前判断是否存在,判断该条地址的归属是否是当前用户。不用重复开发 2.执行删除收货地址信息 ``` delete * from t_address where aid=? ``` 3.如果用户删除的是默认的收货地址,将剩下的地址中的某一条设置为默认的收货地址。规则可以定义为最新修改的收货地址设为默认(根据modified_time字段) ``` limit n(n-1), pageSize n:页数(展示哪一页) pageSize:每页条数 select * from t_address where uid=? order by modified_time DESC limit 0,1 ``` 4.如果用户本身只有一条收货地址数据,删除后,其他操作就不进行 #### 1.2 设计抽象方法 AddressMapper下 ```java /** * 根据aid删除收货地址数据 * @param aid 收货地址数据的id * @return 返回受影响的行数,如果没有匹配的数据,则返回null */ Integer deleteByAid(Integer aid); /** * 根据uid查询用户的最后修改的收货地址数据 * @param uid 用户的id * @return 匹配的收货地址数据,如果没有匹配的数据,则返回null */ Address findLastModifiedTime(Integer uid); ``` #### 1.3 编写SQL映射 ```xml DELETE from t_address WHERE aid = #{aid} ``` #### 1.4 单元测试 ```java @Test public void deleteByAid() { addressMapper.deleteByAid(1); } @Test public void findLastModifiedTime() { System.out.println(addressMapper.findLastModifiedTime(7)); } ``` ### 2. 业务层 #### 2.1 规划异常 在执行删除操作时可能产生未知的异常导致数据删除失败,抛出DeleteException,需要创建 ```java package com.cy.store.service.ex; /** * 数据删除异常 */ public class DeleteException extends ServiceException{ public DeleteException() { super(); } public DeleteException(String message) { super(message); } public DeleteException(String message, Throwable cause) { super(message, cause); } public DeleteException(Throwable cause) { super(cause); } protected DeleteException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ``` #### 2.2 设计抽象方法 IAddressService接口中 ``` /** * 删除用户选中的收货地址数据 * @param aid 收货地址id * @param uid 用户id * @param username 用户名 */ void delete(Integer aid, Integer uid, String username); ``` #### 2.3 实现抽象方法 ``` @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("删除数据产生未知的异常!"); } Integer count = addressMapper.countByUid(uid); if (count == 0) { return; } // 判断是否是默认地址 // 需要将前面方法中的返回 isDefault=null注释掉 if (result.getIsDefault() == 0) { return; } Address address = addressMapper.findLastModifiedTime(uid); rows = addressMapper.updateDefaultByAid( address.getAid(), username, new Date()); if (rows != 1) { throw new UpdateException("更新用户的默认收货地址数据时产生未知的异常!"); } } ``` #### 2.4 单元测试 ```java @Test public void delete() { // addressService.delete(2, 7, "管理员"); addressService.delete(8, 7, "管理员"); } ``` ### 3. 控制层 #### 3.1 处理异常 1.需要处理DeleteException异常 BaseController中 ```java else if (e instanceof DeleteException) { result.setState(5002); result.setMessage("删除数据时产生未知的异常!"); } ``` #### 3.2 设计请求 ``` /address/delete/{aid} POST Integer aid, HttpSession session JsonResult ``` #### 3.3 实现请求 ```java @RequestMapping("delete/{aid}") public JsonResult delete(@PathVariable("aid") Integer aid, HttpSession session) { Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); addressService.delete(aid, uid, username); return new JsonResult<>(OK); } ``` ### 4. 前端页面 1.给设置删除按钮添加一个onclick属性,指向一个方法的调用,在这个方法中完成Ajax请求 ```js let tr = '\n' + '#{tag}\n' + '#{name}\n' + '#{address}\n' + '#{phone}\n' + ' 修改\n' + // 修改 ' 删除\n' + '设为默认\n' + ''; tr = tr.replace(/#{tag}/g, list[i].tag); tr = tr.replace(/#{name}/g, list[i].name); tr = tr.replace(/#{address}/g, list[i].provinceName + list[i].cityName + list[i].areaName + list[i].address); tr = tr.replace(/#{phone}/g, list[i].phone); // 更改 tr = tr.replace(/#{aid}/g, list[i].aid); ``` 在address.html页面添加 ```js function deleteByAid(aid) { $.ajax({ url: "/address/delete" + aid, type: "POST", dataType: "json", success: function(json) { if (json.state === 200) { // 重新加载收货列表 showAddressList() } else { alert("删除收货地址失败!"); } }, error: function (xhr) { alert("删除收货地址时产生未知的错误或异常!" + xhr.status); } }) } ``` 单元测试 # 购物车 功能实现分两步: 1. 选择商品数量 2. 点击加入购物车后将用户所有购物车数据列表展示 3. 结算 ## 加入购物车 ### 数据表创建 ``` 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; ``` ### 创建实体类 entity类创建实体类 ```java package com.cy.store.entity; import java.util.Objects; public class Cart extends BaseEntity{ private Integer cid; private Integer uid; private Integer pid; private Long price; private Integer num; //... } ``` ### 1. 持久层 #### 1.1 规划SQL语句 1.向购物车表中插入数据 ```mysql insert into t_cart (cid除外) values (值列表) ``` 2.当前商品已经在购物车中存在,则直接更新num即可 ```mysql update t_cart set num=? where cid=? ``` 3.在插入或者更新具体执行哪个语句取决于数据库是否已有当前数据,先做查询操作 ```mysql select * from t_cart where pid=? and uid=? ``` #### 1.2 接口和抽象方法 CartMapper接口 ```java package com.cy.store.mapper; import com.cy.store.entity.Cart; import com.cy.store.entity.User; import org.apache.ibatis.annotations.Param; import java.util.Date; /** 购物车的持久层接口 */ //@Mapper 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 购物车数据 */ Cart findByUidAndPid(Integer uid, Integer pid); } ``` #### 1.3 编写SQL映射文件 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} ``` #### 1.4 单元测试 ```java package com.cy.store.mapper; import com.cy.store.entity.Cart; import com.cy.store.entity.User; 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; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class CartMapperTests { @Autowired private CartMapper cartMapper; @Test public void insert() { Cart cart = new Cart(); cart.setUid(7); cart.setPid(10000011); cart.setNum(2); cart.setPrice(1000L); cartMapper.insert(cart); } @Test public void updateNumByCid() { cartMapper.updateNumByCid(1, 4, "管理员", new Date()); } @Test public void findByUidAndPid() { Cart cart = cartMapper.findByUidAndPid(7, 10000011); System.err.println(cart); } } ``` ### 2. 业务层 #### 2.1 异常处理 1. 插入时异常:InsertException 2. 更新数据(num)异常:UpdateException #### 2.2 接口和抽象方法 创建一个IcartService接口文件 ``` package com.cy.store.service; public interface ICartService { /** * 将商品添加到购物车中 * @param uid 用户id * @param pid 商品id * @param num 新增之后的数量 * @param username 用户用 */ void addToCart(Integer uid, Integer pid, Integer num, String username); } ``` #### 2.3 实现接口 创建CartServiceImpl实现类 ```java package com.cy.store.service.impl; import com.cy.store.entity.Cart; import com.cy.store.mapper.CartMapper; import com.cy.store.mapper.ProductMapper; import com.cy.store.service.ICartService; import com.cy.store.service.ex.InsertException; import com.cy.store.service.ex.UpdateException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Date; @Service public class CartServiceImpl implements ICartService { // 购物车的业务层依赖于购物车的持久层和商品的持久层 @Autowired private CartMapper cartMapper; // 依赖于商品表 @Autowired private ProductMapper productMapper; @Override public void addToCart(Integer uid, Integer pid, Integer num, String username) { // 查询当前添加的购物车是否已经存在 Cart result = cartMapper.findByUidAndPid(uid, pid); Date date = new Date(); if (result == null) { // 表示这个商品没有被添加到购物车 // 填充数据 Cart cart = new Cart(); cart.setUid(uid); cart.setPid(pid); cart.setNum(num); // 加完之后的数量 cart.setPrice(productMapper.findById(pid).getPrice()); // 根据商品查询价格 cart.setCreatedUser(username); cart.setCreatedTime(date); cart.setModifiedUser(username); cart.setModifiedTime(date); Integer rows = cartMapper.insert(cart); if (rows != 1) { throw new InsertException("插入购物车数据时异常"); } } else { Integer amount = result.getNum() + num; Integer rows = cartMapper.updateNumByCid(result.getCid(), amount, username, new Date()); if (rows != 1) { throw new UpdateException("更新购物车商品数量时异常!"); } } } } ``` #### 2.4 单元测试 ```java package com.cy.store.service; import com.cy.store.entity.User; 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; // 测试类 不会随同项目一起打包 @SpringBootTest @RunWith(SpringRunner.class) // @RunWith()表示启动这个单元测试类,必须要加的注解,括号中内容是一个类,是SpringRunner的子类 public class CartServiceTests { @Autowired private ICartService cartService; @Test public void addToCart() { // cartService.addToCart(7, 10000005, 1, "管理员"); cartService.addToCart(7, 10000005, 1, "管理员"); } } // 执行两次测试数量更改是否正确 ``` ### 3. 控制层 #### 3.1 异常处理 > 无 #### 3.2 设计请求 ``` url:carts/add_to_cart type:get data:Interger pid, Integer num, HttpSession session return:Json ``` #### 3.3 实现请求 创建CartController类 ```java package com.cy.store.controller; import com.cy.store.service.ICartService; import com.cy.store.util.JsonResult; import jakarta.servlet.http.HttpSession; 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; @RequestMapping("add_to_cart") public JsonResult addToCart(Integer pid, Integer num, HttpSession session) { cartService.addToCart(getUidFromSession(session), pid, num, getUsernameFromSession(session)); return new JsonResult<>(OK); } } ``` 单元测试:先登录后访问:http://localhost:8080/carts/add_to_cart?pid=10000015&num=5 ### 4. 前端页面 在produce.html页面给【加入购物车】按钮加入点击时间,并发送Ajax请求 ```js $("#btn-add-to-cart").click(function () { $.ajax({ url: "/carts/add_to_cart", type: "get", data: { "pid": id, "num": $("#num").val() }, dataType: "json", success: function (json) { if (json.state === 200) { alert("加入购物车成功!") }else { alert("加入购物车失败!") } }, error: function (xhr) { alert("加入购物车时产生未知的异常" + xhr.status) } }) }) ``` Ajax函数中data参数的设置方式: - data: $("#form表单选择").serialize() 参数过多并且在一个表单中,字符串的提交 - data: new FormData($("#form表单选择")[0]). 适用于提交文件 - data: "username=tom". 适合参数值固定,并且参数值列表有限,可以手动拼接 - ``` let user = "tom" data: "username=" + user ``` - 使用json格式提交 ``` data: { "username": "tom", "age": 18, "sex": 1 } ``` ## 显示购物车列表 ### 1. 持久层 #### 1.1 规划SQL语句 查询语句 ```mysql SELECT cid, uid, pid, t_cart.price, cart.num, image, title, t_product.price as realPrice FROM t_cart LEFT JOIN t_product ON t_cart.pid = t_product.id WHERE uid=? ORDER BY t_cart.created_time DESC; ``` VO: value object 值对象。当进行select查询时,查询的结果属于多张表中的内容,此时发现结果集不能使用pojo实体类来接收(pojo实体类不能包含**多表查询**出来的结果)。解决方法:重新构建一个新的对象用来存储查询出来的结果集对应的映射,所以把这个结果集的对象称为值对象 在store包下创建vo包,创建CartVO ```java package com.cy.store.vo; import java.io.Serializable; import java.util.Objects; /* 购物车数据的VO类 */ public class CartVo implements Serializable { private Integer cid; private Integer uid; private Integer pid; private Long price; private Integer num; private String image; private String title; private Long realPrice; public Integer getCid() { return cid; } public void setCid(Integer cid) { this.cid = cid; } public Integer getUid() { return uid; } public void setUid(Integer uid) { this.uid = uid; } public Integer getPid() { return pid; } public void setPid(Integer pid) { this.pid = pid; } public Long getPrice() { return price; } public void setPrice(Long price) { this.price = price; } public Integer getNum() { return num; } public void setNum(Integer num) { this.num = num; } public String getImage() { return image; } public void setImage(String image) { this.image = image; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public Long getRealPrice() { return realPrice; } public void setRealPrice(Long realPrice) { this.realPrice = realPrice; } @Override public boolean equals(Object o) { if (o == null || getClass() != o.getClass()) return false; CartVo cartVo = (CartVo) o; return Objects.equals(cid, cartVo.cid) && Objects.equals(uid, cartVo.uid) && Objects.equals(pid, cartVo.pid) && Objects.equals(price, cartVo.price) && Objects.equals(num, cartVo.num) && Objects.equals(image, cartVo.image) && Objects.equals(title, cartVo.title) && Objects.equals(realPrice, cartVo.realPrice); } @Override public int hashCode() { return Objects.hash(cid, uid, pid, price, num, image, title, realPrice); } @Override public String toString() { return "CartVo{" + "cid=" + cid + ", uid=" + uid + ", pid=" + pid + ", price=" + price + ", num=" + num + ", image='" + image + '\'' + ", title='" + title + '\'' + ", realPrice=" + realPrice + '}'; } } ``` #### 1.2 接口和抽象方法 ```java /** * 通过uid查询购物车列表所需的数据 * @param uid 用户id * @return CartVo数据 */ List findByUid(Integer uid); ``` #### 1.3 编写SQL映射 ```xml ``` 单元测试 ```java @Test public void findByUid() { List list = cartMapper.findByUid(7); for (CartVo cartVo : list) { System.err.println(cartVo); } } ``` ### 2. 业务层 #### 2.1 规划异常 无 #### 2.2 接口和抽象方法 ```java /** * 查询购物车列表 * @param uid 用户id * @return CartVo列表 */ List getVOByUid(Integer uid); ``` #### 2.3 接口实现 ```java @Override public List getVOByUid(Integer uid) { return cartMapper.findByUid(uid); } ``` 单元测试 ### 3. 控制层 #### 3.1 处理异常 无 #### 3.2 设计请求 ``` url:/carts/ type: get data: HttpSession session return: JsonResult> ``` #### 3.3 实现请求 ```java @RequestMapping({"/", ""}) public JsonResult> getVOByUid(HttpSession session) { Integer uid = getUidFromSession(session); List data = cartService.getVOByUid(uid); return new JsonResult<>(OK, data); } ``` 单元测试 登录后访问:http://localhost:8080/carts/ ### 前端页面 1.注释掉cart.js文件 ```js ``` ```js $(document).ready(function () { showCartList(); }); // 展示购物车数据 function showCartList() { // 清空tbody标签数据 $("#cart-list").empty() $.ajax({ url: "/carts", type: "GET", dataType: "json", success: function (json) { let list = json.data; for (let i = 0; i < list.length; i++) { let tr = '\n' + '\n' + '\t\n' + '\n' + '\n' + '#{title}#{msg}\n' + '¥#{singlePrice}\n' + '\n' + '\t\n' + '\t\n' + '\t\n' + '\n' + '#{totalPrice}\n' + '\n' + '\t\n' + '\n' + ''; tr = tr.replace(/#{cid}/g, list[i].cid); tr = tr.replace(/#{image}/g, list[i].image); tr = tr.replace(/#{title}/g, list[i].title); tr = tr.replace(/#{msg}/g, list[i].realPrice); tr = tr.replace(/#{num}/g, list[i].num); tr = tr.replace(/#{singlePrice}/g, list[i].price); tr = tr.replace(/#{totalPrice}/g, list[i].price * list[i].num); $("#cart-list").append(tr) } }, error: function (xhr) { alert("购物车列表数据加载产生未知的异常" + xhr.status) } }); } ``` ## 增加购物车商品数量 ### 1. 持久层 #### 1.1 规划SQL语句 1.执行更新t_cart记录的num值,无需重复开发 2.根据cid的值查询当前购物车数据是否存在 ```mysql select * from t_cart where cid=? ``` #### 1.2 接口和抽象方法 ```java /** * 根据cid查询购物车数据是否存在 * @param cid 购物车id * @return 购物车数据 */ CartVo findByCid(Integer cid); ``` #### 1.3 编写SQL映射 ```xml ``` 单元测试 ```java @Test public void findByCid() { System.err.println(cartMapper.findByCid(1)); } ``` ### 2. 业务层 #### 2.1 规划异常 更新时异常:updateException 查询的数据是否有访问权限:AccessException 查询数据不存在,抛出:CartNotFoundException ``` package com.cy.store.controller.ex; public class CartNotFoundException extends RuntimeException{ public CartNotFoundException() { super(); } public CartNotFoundException(String message) { super(message); } public CartNotFoundException(String message, Throwable cause) { super(message, cause); } public CartNotFoundException(Throwable cause) { super(cause); } protected CartNotFoundException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) { super(message, cause, enableSuppression, writableStackTrace); } } ``` #### 2.2 接口和抽象方法 ```java /** * 更新用户的购物车数据的数量 * @param cid * @param uid * @param username * @return 增加成功后新的数量 */ Integer addNum(Integer cid, Integer uid, String username); ``` #### 2.3 实现方法 ```java @Override public Integer addNum(Integer cid, Integer uid, String username) { Cart result = cartMapper.findByCid(cid); if (result == null) { throw new CartNotFoundException("购物车数据不存在!"); } if (!result.getUid().equals(uid)) { throw new AccessDeniedException("数据非法访问!"); } Integer num = result.getNum() + 1; Integer rows = cartMapper.updateNumByCid(cid, num, username, new Date()); if (rows != 1) { throw new UpdateException("更新数据失败!"); } return num; } ``` 单元测试 ### 3. 控制层 #### 3.1 处理异常 ``` else if (e instanceof CartNotFoundException) { result.setState(4007); result.setMessage("购物车数据不存在的异常!"); } ``` #### 3.2 设计请求 ``` /carts/{cid}/num/add Post Interger cid, HttpSession session JsonResult ``` #### 3.3 实现请求 ```java @RequestMapping("{cid}/add/num") public JsonResult addNum(Integer cid, HttpSession session) { Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); Integer data = cartService.addNum(cid, uid, username); return new JsonResult<>(OK, data); } ``` 单元测试 登录后访问:http://localhost:8080/carts/1/add/num ### 前端页面 ``` '\n' + '\n' + ``` ``` function addNum(cid) { $.ajax({ url: "/carts/" + cid + "/add/num", type: "POST", dataType: "json", success: function (json) { if (json.state === 200) { $("#goodsCount" + cid).val(json.data); let price = $("#goodsPrice" + cid).html(); let totalPrice = price * json.data; $("#goodsCast" + cid).html(totalPrice); } else { alert("增加购物车数据失败," + json.message) } }, error: function (xhr) { alert("增加购物车商品数量产生未知的异常" + xhr.status) } }); } function reduceNum(cid) { $.ajax({ url: "/carts/" + cid + "/reduce/num", type: "POST", dataType: "json", success: function (json) { if (json.state === 200 && json.data >= 1) { $("#goodsCount" + cid).val(json.data); let price = $("#goodsPrice" + cid).html(); let totalPrice = price * json.data; $("#goodsCast" + cid).html(totalPrice); } else { alert("减少购物车数据失败," + json.message) } }, error: function (xhr) { alert("增加购物车商品数量产生未知的异常" + xhr.status) } }); } ``` 单元测试 ## 显示勾选的购物车数据 ### 1. 持久层 #### 1.1 规划SQL语句 用户在购物车列表随机勾选相关的商品,点击【结算】按钮后,跳转到结算页面,在这个页面展示用户在上一个页面勾选的数据。列表的展示,内容还是购物车表。两个页面需要将多个cid传递 ```mysql SELECT cid, uid, pid, t_cart.price, cart.num, image, title, t_product.price as realPrice 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 接口和抽象方法 ``` List findVOByCid(Integer[] cids); ``` #### 1.3 编写SQL映射 ```mysql ``` 单元测试 ``` @Test public void findVOBycid() { Integer[] cids = {1,2,3,4,5,6,7}; System.out.println(cartMapper.findVOByCid(cids)); } ``` ### 2. 业务层 #### 2.1 规划异常 无 #### 2.2 接口和抽象方法 ```java List getVOByCid(Integer uid, Integer[] cids); ``` #### 2.3 实现方法 ```java @Override public List getVOByCid(Integer uid, Integer[] cids) { List result = cartMapper.findVOByCid(cids); Iterator it = result.iterator(); while (it.hasNext()) { CartVo cartVo = it.next(); if (!cartVo.getUid().equals(uid)) { // 表示当前用户不属于当前用户 result.remove(cartVo); // 从集合中移除这个元素 } } return result; } ``` 单元测试 ### 3. 控制层 #### 3.1 处理异常 无 #### 3.2 设计请求 ``` /carts/list Integer[] cids, HttpSession session POST JsonResult> ``` #### 3.3 实现请求 ``` @RequestMapping("list") public JsonResult> getVOByCid(Integer[] cids, HttpSession session) { Integer uid = getUidFromSession(session); List data = cartService.getVOByCid(uid, cids); return new JsonResult<>(OK, data); } ``` 单元测试 登录后访问:http://localhost:8080/carts/list?cids=1&cids=2&cids=3 ### 4. 前端页面 #### 4.1 显示勾选的购物车数据-前端页面 cart.html页面中结算按钮type属性更改为submit ``` ``` 在orderConfirm.html页面中添加自动加载从上个页面传递过来的cids数据,再发送Ajax请求 ```js ``` #### 4.2 购物车页面显示收货地址列表-前端页面 1.收货地址存放在select下拉列表中,将查询到的当前用户的收货地址动态加载到这个下来列表中。从数据库角度看,是一个select查询语句。已经编写了根据uid查询收货地址 2.OrderConfirm页面中,收货地址数据需要自动进行加载,将方法逻辑在ready()函数中 ```js $(document).ready(function () { showCartList(); showAddressList(); }); ``` 3.定义showAddressList方法 ```js function showAddressList() { $("#address-list").empty() $.ajax({ url: "/address", type: "POST", dataType: "json", success: function (json) { if (json.state === 200) { let list = json.data; for (let i = 0; i < list.length; i++) { let opt = "" opt = opt.replace(/#{aid}/g, list[i].aid); opt = opt.replace(/#{tag}/g, list[i].tag); opt = opt.replace(/#{receiveName}/g, list[i].name); opt = opt.replace(/#{provinceName}/g, list[i].provinceName); opt = opt.replace(/#{cityName}/g, list[i].cityName); opt = opt.replace(/#{areaName}/g, list[i].areaName); opt = opt.replace(/#{address}/g, list[i].address); opt = opt.replace(/#{phone}/g, list[i].phone); $("#address-list").append(opt) } } }, error: function (xhr) { alert("购物车对应收货地址列表加载产生未知的异常" + xhr.status) } }); } ``` ## 订单 ### 数据表创建 ```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; ``` ### 创建实体类 1.订单的实体类 ```java /** 订单数据的实体类 */ 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; ``` 2.订单项的实体类 ```java /** 订单中的商品数据 */ public class OrderItem extends BaseEntity { private Integer id; private Integer oid; private Integer pid; private String title; private String image; private Long price; private Integer num; ``` ### 1. 持久层 #### 1.1 规划SQL语句 1.插入操作 ```mysql insert into t_order (oid除外所有字段) values (???...) ``` 2.讲数据插入到订单项的表中 ```java insert into t_order (id除外所有字段) values (???...) ``` #### 1.2 接口和抽象方法 创建OrderMapper接口 ```java package com.cy.store.mapper; import com.cy.store.entity.Cart; import com.cy.store.entity.Order; import com.cy.store.entity.OrderItem; import com.cy.store.vo.CartVo; import org.apache.ibatis.annotations.Param; import java.util.Date; import java.util.List; /** 订单的持久层接口 */ //@Mapper public interface OrderMapper { /** * 插入订单表数据 * @param order order对象 * @return 受影响的行数 */ Integer insertOrder(Order order); /** * 插入订单项表数据 * @param orderItem orderItem对象 * @return 受影响的行数 */ Integer insertOrderItem(OrderItem orderItem); } ``` #### 1.3 编写SQL映射 创建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} ) ``` 单元测试 ``` @Test public void insertOrder(){ Order order = new Order(); order.setUid(7); order.setRecvName("管理员"); order.setRecvPhone("232423"); orderMapper.insertOrder(order); } @Test public void insertOrderItme(){ OrderItem orderItem = new OrderItem(); orderItem.setOid(1); orderItem.setPid(10000005); orderItem.setTitle("圆珠笔"); orderMapper.insertOrderItem(orderItem); } ``` ### 2. 业务层 在IAddressService定义根据aid获取address地址数据 ``` Address getByAid(Integer aid, Integer uid); ``` 实现该方法 ``` @Override public Address getByAid(Integer aid, Integer uid) { 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.setCreatedTime(null); address.setModifiedTime(null); address.setCreatedUser(null); address.setModifiedUser(null); return address; } ``` #### 2.1 规划异常 插入时异常,已编写 #### 2.2 接口和抽象方法 创建IOrderService接口 ``` Order create(Integer aid, Integer uid, String username, Integer[] cids); ``` #### 2.3 方法实现 ```java package com.cy.store.service.impl; import com.cy.store.controller.ex.AddressCountLimitException; import com.cy.store.entity.Address; import com.cy.store.entity.Order; import com.cy.store.mapper.AddressMapper; import com.cy.store.mapper.OrderMapper; import com.cy.store.service.IAddressService; import com.cy.store.service.ICartService; import com.cy.store.service.IDistrictService; import com.cy.store.service.IOrderService; import com.cy.store.service.ex.*; import com.cy.store.vo.CartVo; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; 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; @Override public Order create(Integer aid, Integer uid, String username, Integer[] cids) { List list = cartService.getVOByCid(uid, cids); // 计算商品总价 Long totalPrice = 0L; for (CartVo c : list) { totalPrice += c.getRealPrice() + c.getNum(); } Address address = addressService.getByAid(aid, uid); Order order = new Order(); order.setUid(uid); // 收货地址数据 order.setRecvName(address.getName()); order.setRecvPhone(address.getPhone()); order.setRecvProvince(address.getProvinceName()); order.setRecvCity(address.getCityName()); order.setRecvArea(address.getAreaName()); order.setRecvAddress(address.getAddress()); // 支付 order.setTotalPrice(totalPrice); order.setStatus(0); order.setOrderTime(new Date()); // 日志 order.setCreatedUser(username); order.setCreatedTime(new Date()); order.setModifiedUser(username); order.setModifiedTime(new Date()); Integer rows = orderMapper.insertOrder(order); if (rows != 1) { throw new InsertException("插入时异常!"); } return order; } } ``` 单元测试 ```java @Test public void creat() { Integer[] cids = {1, 2}; Order order = orderService.create(10, 7, "test002", cids); System.out.println(order); } ``` ### 3. 控制层 #### 3.1 处理异常 无 #### 3.2 设计请求 ``` /orders/create aid, session, cids post JsonResult ``` #### 3.3 实现请求 ``` package com.cy.store.controller; import com.cy.store.entity.Order; import com.cy.store.service.IOrderService; import com.cy.store.util.JsonResult; import jakarta.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("orders") public class OrderController extends BaseController{ @Autowired private IOrderService orderService; @RequestMapping("create") public JsonResult create(Integer aid, Integer[] cids, HttpSession session) { Integer uid = getUidFromSession(session); String username = getUsernameFromSession(session); Order data = orderService.create(aid, uid, username, cids); return new JsonResult<>(OK, data); } } ``` 单元测试 #### 4. 前端页面 订单确定页面OrderConfirm添加发送请求的处理方法 ```js $("#btn-create-order").click(function () { let aid = $("#address-list").val() let cids = location.search.substr(1) $.ajax({ url: "/orders/create", type: "GET", data: "aid=" + aid + "&" + cids, dataType: "json", success: function (json) { if (json.state === 200) { location.href = "payment.html"; alert("订单创建成功!") } }, error: function (xhr) { alert("订单数据加载产生未知的异常" + xhr.status) } }); }) ```