赞
踩
目录
项目简介:为超市实现一个信息管理平台,使超市的工作人员可以进行商品的管理和订单的管理
主要模块:◇ 用户管理 :用户注册、用户登录
◇ 商品管理 :商品上架,浏览商品,更新商品
◇ 订单管理:购买商品、浏览订单
项目环境:Windows,IDEA,Maven,Java8,MySQL
项目技术:Spring,SpringMVC,Mybatis,密码加密存储,事务管理
用户表(主键user_id、用户名(唯一username)、密码password) 商品表(主键product_id、关系字段user_id、名称name、介绍introduce、库存stock、单位unit、价格price、折扣discount) 订单表(主键order_id、关系字段user_id、订单编号(uuid)、下单时间created_at、完成时间finished_at、应付payable、实付actual、状态status) 订单项表(主键id、两个关系字段order_id和product_id、为了防止商品下架后信息丢失,将商品信息备份一份(product_name、product_introduce、product_number、product_unit、product_price、product_discount)):订单和商品的关系表,记录本次订单的每一份商品信息
<img src="https://gitee.com/xial1007/img-up-load/raw/master/img/202209090622538.png" alt="image-20220909062217684" style="zoom:25%;" />
用户管理 : 用户注册,用户登录
商品管理 :商品上架,浏览商品,更新商品
订单管理 : 购买商品(生成订单、确认订单、取消订单),浏览订单
在用户注册、用户登录中使用一个加密库(实际上是在做hash,不是真正的加密),不再保存明文密码
使用BCrypt算法:专门做密码hash的。例:BCrypt算法用于加密,及验证密码是否匹配
- package org.mindrot.jbcrypt;
-
- public class BCryptDemo {
- public static void main(String[] args) {
- String password = "123";
-
- String salt = BCrypt.gensalt(); // 每次随机出来的盐值是不同的
- System.out.println("salt = " + salt);
-
- String hashpw = BCrypt.hashpw(password, salt);
- System.out.println("hashpw = " + hashpw);
- System.out.println("长度 = " + hashpw.length());
-
- // 用做密码加密的 hash 算法都是精心设计过的,碰撞概率很低。
- // 使用同样的 hash 算法去 hash "123",如果 hash 值一样,大概率用户的密码是正确的
- boolean checkpw = BCrypt.checkpw("123", hashpw);
- System.out.println(checkpw);
- }
- }

不能明文、不能直接MD5、使用加盐后的Hash算法->B Crypt成熟的算法
引入第三方类:orgmindrotjbcrypt.BCrypt
基本用法: 1.存储过程:(注册) 1)生成一个随机的盐值:String salt=BCrypt.gensalt(); 2)将明文密码+盐值一同做hash,得到要储存的密码:Stringp=BCrypt. hashpw(password,salt); 2.验证过程: (登录) 1)从存储中取出密文密码:String hashpw=从表中获取 2)BCryptcheckpw(password,hashpw):对比hash值(理论上,确实 存在不同密码,但验证成功的情况,但概率很小
资源名 | 作用 | 类型 |
---|---|---|
/reqister.html | 提供一个form表单 | 静态资源 GET |
/reqister.do | 验证注册逻辑 | 动态资源 POST |
注册逻辑
1)Controller(读取form表单提交上来的用户信息,做参数合法性校验)
2)Service(负责密码加密的工作)
3)Mapper(数据库的插入)一mybatis
DoController下 /register.do
1. 进行参数的合法性校验
1)用户名的要求:不是 null && 不是 "" && 长度不能超过 50 && 不能重复,一旦出错,重定向到register.html
2)密码的要求:不是 null && 不是 ""
3)优化:
a. 将用户名合法性验证、密码合法性验证部分代码封装成两个类,减少代码长度,增加代码可读性
b. 为防止过抛出异常、重定向语句出现错误,不好修改,将其封装成类完成错误捕获(ErrorRedirectException和CashControllerAdvice)
2. 进行注册工作UserService的register方法(密码加密,并保存至数据库)
1)用户名重复:抛出异常,重定向到register.html
2)用户名不重复:执行4.完成注册步骤
3. 完成注册:
1)合法的注册数据处理:进行密码加密处理,并将合法的注册信息添加到数据库:UserService的register方法
2)直接完成登录操作:把刚注册的用户信息放入 session 中,视为登录了
3)最终注册成功之后,跳转到首页(/)
- // 这个方法中没有 try-catch,catch 在 CashControllerAdvice 中
- @PostMapping("/register.do")
- // 由于参数名称和 form 表单里的 input 的 name 是对应上的,所以省略了 @RequestParam 注解了
- public String register(String username, String password, HttpServletRequest request) {
- String module = "用户注册"; // 用户注册里抛出的异常
- String redirectUrl = "/register.html"; // 抛出异常后要重定向的页面
- // 规则,无论哪个动态资源,先打印功能 + 参数
- log.debug("用户注册: username = {}, password = {}", username, password);
-
- // 1. 进行参数的合法性校验 validate
- // 1.1)用户名合法性校验
- username = usernameValidator.validate(module, redirectUrl, username); // 如果有错,直接异常穿透 register 方法就出去了
- // 1.2)密码合法性校验
- password = passwordValidator.validate(module, redirectUrl, password); // 同理
-
- // 2. 完成注册的工作
- try {
- // 2.1)用户名不重复 ——> 执行 3.注册成功
- // 3.1) 合法的注册数据处理:进行密码加密处理,并将合法的注册信息添加到数据库
- User user = userService.register(username, password);
-
- // 3.2). 直接完成登录操作(把刚注册的用户信息放入 session 中,视为登录了)
- HttpSession session = request.getSession();
- session.setAttribute("currentUser", user);
-
- // 3.3). 最终注册成功之后,跳转到首页(/)
- log.debug("用户注册: 注册成功, user = {}", user);
- return "redirect:/";
-
- } catch (DuplicateKeyException exc) { // 重复的key异常
- // 2.2)用户名重复——注册失败
- throw new ErrorRedirectException("username 重复", module, redirectUrl, exc);
- }
- }

父类AbsValidator
由于多个属性都需要进行判空操作,因此我们将它写成一个抽象类,其他属性进行合法性检验时,不需要重复判空,只需要继承该类即可
- // 这个类不准备实例化对象,所以定义成抽象类
- public abstract class AbsValidator {
- public String validate(String module, String redirectUrl, String value) {
- if (value == null) {
- throw new ErrorRedirectException("value 是 null", module, redirectUrl);
- }
-
- value = value.trim();
- if (value.isEmpty()) {
- throw new ErrorRedirectException("value 是 \"\"", module, redirectUrl);
- }
-
- return value;
- }
- }
子类 1)用户名合法性检验
不用再重复判空,直接继承父类方法
- @Slf4j
- @Component // 交给 Spring 管理
- public class UsernameValidator {
- public String validate(String module, String redirectUrl, String username) {
- // 用户名的要求:不是 null && 不是 "" && 长度不能超过 50 && 不能重复
- if (username == null) {
- throw new ErrorRedirectException("username 是 null", module, redirectUrl);
- }
-
- username = username.trim(); // 去掉两边的空格
- if (username.isEmpty()) {
- throw new ErrorRedirectException("username 是 \"\"", module, redirectUrl);
- }
-
- if (username.length() > 50) {
- throw new ErrorRedirectException("username 的长度超过 50", module, redirectUrl);
- }
-
- return username;
- }
- }

子类 2)密码合法性检验
- @Slf4j
- @Component // 交给 Spring 管理
- public class UsernameValidator {
- public String validate(String module, String redirectUrl, String username) {
- // 用户名的要求:不是 null && 不是 "" && 长度不能超过 50 && 不能重复
- if (username == null) {
- throw new ErrorRedirectException("username 是 null", module, redirectUrl);
- }
-
- username = username.trim(); // 去掉两边的空格
- if (username.isEmpty()) {
- throw new ErrorRedirectException("username 是 \"\"", module, redirectUrl);
- }
-
- if (username.length() > 50) {
- throw new ErrorRedirectException("username 的长度超过 50", module, redirectUrl);
- }
-
- return username;
- }
- }

1)抛出异常ErrorRedirectException
捕获错误然后需要重定向的异常,希望是一个非受查异常(继承在 RuntimeException)
由于项目中有多种错误,因此对不同的错误,应该有不同的处理方式,跳转到不同的页面,故我们在此定义三个属性以区分不同错误和重定向结果
- /* 错误然后重定向的异常,希望是一个非受查异常(继承在 RuntimeException)*/
- @Getter
- public class ErrorRedirectException extends RuntimeException {
- private final String error; // 错误类型
- private final String module; // 哪个功能抛出的异常
- private final String redirectUrl; // 重定向到哪
-
- // 构造错误信息的对象
- public ErrorRedirectException(String error, String module, String redirectUrl) {
- // 调用父类的构造方法
- super(); // 这句可以省略
-
- this.error = error;
- this.module = module;
- this.redirectUrl = redirectUrl;
- }
- // 构造错误信息和捕获到的异常对象
- public ErrorRedirectException(String error, String module, String redirectUrl, Throwable cause) {
- // 调用父类的构造方法
- super(cause); // 这句就不能省略了
-
- this.error = error;
- this.module = module;
- this.redirectUrl = redirectUrl;
- }
- }

2)重定向CashControllerAdvice
添加@ControllerAdvice,作为异常统一处理的类 AOP思想的体现,在某个事务完全执行完毕后调用(AfterResourceHandler),AOP里Advice是通知的意思
关于捕获到的错误对象处理的方法:根据不同的逻辑,打印不同,跳转的页面也不同
- @ExceptionHandler(ErrorRedirectException.class)
- public String logAndRedirect(ErrorRedirectException exc) {
- log.debug("{}: {}", exc.getModule(), exc.getError());
- return "redirect:" + exc.getRedirectUrl();
- }
UserService下register方法
对密码进行加密处理
保存至数据库:插入User数据库(通过UserMapper处理)
- @Slf4j
- @Service
- public class UserService {
- // 注入 UserMapper
- private final UserMapper userMapper;
-
- @Autowired
- public UserService(UserMapper userMapper) {
- this.userMapper = userMapper;
- }
-
- public User register(String username, String password) {
- // 对密码进行加密处理
- String salt = BCrypt.gensalt();
- String hashPassword = BCrypt.hashpw(password, salt);
-
- // 进行插入操作
- User user = new User(username, hashPassword);
- userMapper.insert(user);
-
- return user;
- }
- }

UserMapper接口下的insert方法
用Mybatis里XML形式直接将User对象映射到sql语句里插入数据表User
- @Repository
- @Mapper
- public interface UserMapper {
- void insert(User user);
- }
UserMapper.xml文件
- <mapper namespace="com.xlinzhang.cash.mapper.UserMapper">
- <insert id="insert" useGeneratedKeys="true" keyProperty="userId" keyColumn="user_id">
- insert into users (username, password) values (#{username}, #{password})
- </insert>
- </mapper>
资源名 | 作用 | 类型 |
---|---|---|
/login.html | 提供一个form表单 | 静态资源 |
/login.do | 验证登录逻辑 | 动态资源 |
DoController下 /login.do
进行参数的合法性校验:用户名、密码合法性校验
进行登录工作UserService的login方法(查询用户名并验证密码)
1)查询到的用户对象User为空:登陆不成功
2)查询到的用户对象User不为空:完成登录
完成登录:
1)登录信息记录:把刚注册的用户信息放入 session 中,视为登录了
2)登录成功之后,跳转到首页
- @PostMapping("/login.do")
- public String login(String username, String password, HttpServletRequest request) {
- String module = "用户登录"; // 用户登录里抛出的异常
- String redirectUrl = "/login.html"; // 抛出异常后要重定向的页面
- log.debug("{}: username = {}, password = {}", module, username, password);
-
- // 进行参数的合法性校验:用户名、密码合法性校验
- username = usernameValidator.validate(module, redirectUrl, username);
- password = passwordValidator.validate(module, redirectUrl, password);
-
- // 进行登录工作UserService的login方法
- User user = userService.login(username, password);
-
- if (user == null) {
- throw new ErrorRedirectException("username or password 错误", module, redirectUrl);
- }
-
- HttpSession session = request.getSession();
- session.setAttribute("currentUser", user);
-
- log.debug("{}: 登录成功, user = {}", module, user);
- return "redirect:/";
- }

UserService下login方法
在数据库中查询用户名和密码(User对象)
用户名为空:返回空值
用户名不空:进行密码验证
密码验证:密码错误,返回null;密码正确,返回获取到的User对象
- public User login(String username, String password) {
- User user = userMapper.selectByUsername(username);
-
- log.debug("通过 mybatis 查询得到的用户 = {}", user);
-
- if (user == null) {
- return null;
- }
-
- // 进行密码验证
- if (!BCrypt.checkpw(password, user.getPassword())) {
- return null;
- }
-
- return user;
- }

UserMapper接口下的insert方法
- // 由于只有一个参数,所以省略了 @Param 注解
- User selectByUsername(String username);
UserMapper.xml文件
- <select id="selectByUsername" resultType="com.xlinzhang.cash.model.user.User">
- <!-- User对象中保存的是 userId,而表的列名是 user_id,所以需要做个映射 -->
- <!-- 因为仅仅只是转一下 user_id 到 userId 就些 resultMapper 有点麻烦,mybatis 已经提供了一个选项,开启即可 -->
- select user_id, username, password from users where username = #{username}
- </select>
资源名 | 作用 | 类型 |
---|---|---|
/product/create.html | 提供一个form表单 | 静态资源 |
/product/create.do | 保存的逻辑,对应的表是products | 动态资源 |
DoController下/product/create.do
由于product参数列表比较长,所以不采用一个个列出来的方式,而是使用一个ProductParam对象的方式,通过对象接收请求参数的方式
判断用户是否登录:通过session中的“currentUser”
1)否:无权限进行上架操作,回到登录页面
2)是:获取当前登录用户信息,即User对象,继续进行上架操作
进行参数的合法性校验
name:不空,长度不超过100
introduce:不空,长度不超过200
unit:不空,长度不超过10(范围是<=10)
stock:不空,确认下 stock 是不是数字(int类型),并且得是 > 0 的数字
price:不空,确认下 price 是不是数字(double类型),并且得是 > 0 的数字(如果传入的价格参数有小数点后二位之后,则直接丢弃)
discount:不空,得确认下 discount 是不是数字,并且得是 [1,100] 的数字
进行上架工作:操作上架的是哪个用户,在插入商品记录时就用哪个用户的id(即当前登录的用户)
ProductService的create方法(将商品记录插入数据库)
完成上架:跳转到商品列表页
- @PostMapping("/product/create.do")
- // 由于参数列表比较长,所以我们不采用一个个列出来的方式,而是使用一个对象的方式
- // 通过对象接收请求参数的方式
- public String productCreate(ProductParam productParam, HttpServletRequest request) {
- String module = "商品上架";
- String redirectUrl = "/product/create.html";
- log.debug("{}: 请求参数 = {}", module, productParam);
-
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- // 用户已登录
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明用户未登录
- log.debug("{}: 用户未登录,无权进行该操作", module);
- return "redirect:/login.html"; // 重定向到登录页,让用户登录
- }
-
- // 验证合法性【整体业务写完再回来写这个,但要求自己输入的时候,一定要输入合法的参数,比如价格如果不是数字,就会 500
- // productParam.validate(module, redirectUrl);
- // 使用 service -> mapper 保存
- Product product = productService.create(currentUser, productParam);
-
- log.debug("{}: 成功,上架商品为: {}", module, product);
- return "redirect:/product/list.html"; // 商品上架成功后跳转到商品列表页
- }

ProductService的create方法
构造Product对象:根据填入的数据(ProductParam)和 当前用户(User)构造
调用 mapper 的 insert,完成数据库插入
- @Slf4j
- @Service
- public class ProductService {
- private final ProductMapper productMapper;
-
- @Autowired
- public ProductService(ProductMapper productMapper) {
- this.productMapper = productMapper;
- }
-
- public Product create(User user, ProductParam param) {
- // 1. 构造一个 Product 对象(从 ProductParam 对象)
- Product product = new Product(user, param);
- // 2. 调用 mapper 的 insert,完成数据库插入
- productMapper.insert(product);
- // 3. 返回构造好的对象
- return product;
- }
- }

ProductMapper接口下的insert方法
- @Repository
- @Mapper
- public interface ProductMapper {
- void insert(Product product);
- }
ProductMapper.xml文件
- <insert id="insert" useGeneratedKeys="true" keyProperty="productId" keyColumn="product_id">
- insert into products
- (user_id, name, introduce, stock, unit, price, discount)
- values
- (#{userId}, #{name}, #{introduce}, #{stock}, #{unit}, #{price}, #{discount})
- </insert>
资源名 | 作用 | 类型 |
---|---|---|
/product/list.html | 提供HTML框架 | 静态资源 GET |
/product/list.js | 发起Ajax请求,读取JSON格式的商品列表,修改DOM树(动态的添加数据记录) | 静态资源 GET |
/product/list.json | 返回所有的商品,以JSON形式返回 | 动态资源 GET |
JsonController下/product/list.json
判断用户是否登录:通过session中的“currentUser”
1)否:无权限进行浏览操作,回到登录页面
2)是:获取当前登录用户信息,即User对象,继续进行浏览操作
以列表形式返回Products表中数据:List<Product>
将后端列表数据 转换成 前端用的ProductListView对象:由于对外展示的对象格式和内部使用的对象略有区别,因此我们定义一个ProductView类。从数据表中,我们获取到的是一组view对象,因此再定义一个ProductListView类。这两个类只会在当前Controller下使用,所以就定义成当前Controller下的静态内部类。如,对于price属性:不关心后端如何存储,前端看到的价格都是10.07这种形式
- private int productId;
- private String name;
- private String introduce;
- private int stock;
- private String unit;
- private double price;
- private int discount;
-
- public ProductView(Product product) {
- // 将后端Product对象 转换成 前端显示用的view对象
- log.debug("ProductView(product = {})", product);
- this.productId = product.getProductId();
- this.name = product.getName();
- this.introduce = product.getIntroduce();
- this.stock = product.getStock();
- this.unit = product.getUnit();
- // price在前端要保留两位小数保存:
- // product.getPrice()返回int类型,100也是int类型,若用 int/int 结果也是 int,向下取整,丢失小数信息,故必须是 100.0 而不能是 100
- this.price = product.getPrice() / 100.0;
- this.discount = product.getDiscount();
- }
- }
-
- // 存放从数据表Products中获取的一组数据记录转换成的一组ProductView对象
- @Slf4j
- @Data
- private static class ProductListView {
- // JsonInclude 这个注解是属于 jackson 的(负责 JSON 序列化/反序列化 的一个第三方库,是 Spring 的默认库)
- // JsonInclude.Include.NON_NULL 只有在 不是 null 的时候,才会写到响应中,如:若未登录,无权查看商品表,故data为null,不显示
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private String redirectUrl;
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private List<ProductView> data;
-
- public static ProductListView success(List<Product> productList) {
- ProductListView view = new ProductListView();
- // Product 的线性表 -> ProductView 的线性表
- // 普通写法
- view.data = new ArrayList<>();
- for (Product product : productList) {
- ProductView productView = new ProductView(product);
- view.data.add(productView);
- }
-
- return view;
- }
-
- public static ProductListView failure(String redirectUrl) {
- // 注意,这里不是 HTTP 协议层面的重定向,我们一会儿会让前端进行跳转
- ProductListView view = new ProductListView();
- view.redirectUrl = redirectUrl;
- return view;
- }
- }
-
-
- @GetMapping("/product/list.json")
- public ProductListView getProductList(HttpServletRequest request) {
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明没有登录
- return ProductListView.failure("/login.html");
- }
-
- List<Product> productList = productService.getList();
-
- return ProductListView.success(productList);
- }

ProductService的getList方法
从Products表中获取所有商品数据记录
- public List<Product> getList() {
- return productMapper.selectAll();
- }
ProductMapper接口下的selectAll方法
List<Product> selectAll();
ProductMapper.xml文件
- <select id="selectAll" resultType="com.xlinzhang.cash.model.product.Product">
- select product_id, user_id, name, introduce, stock, unit, price, discount
- from products order by product_id desc
- </select>
list.js文件
将后端数据库数据通过“/product/list.json”转换成前端ProductListView对象
通过get请求,获取'/product/list.json'里的资源(Json形式)
将资源转换成js对象后,判断是否登陆了
1)有 redirectUrl:用户未登录,跳转到 redirectUrl地址中
2)无 redirectUrl:用户登录了,将获取到的ProductListView对象里的列表data里的记录逐一添加到list.html页面中
- var xhr = new XMLHttpRequest()
- xhr.open('get', '/product/list.json')
- xhr.onload = function () {
- console.log(this.status)
- console.log(this.responseText)
-
- // 进行 JSON 字符串 -> JS 对象
- var ret = JSON.parse(this.responseText)
-
- // 根据是否有 redirectUrl 来判断是正确还是错误情况
- if (ret.redirectUrl) {
- // 返回里有 redirectUrl,说明错了,由 JS 进行页面的跳转
- // JS 提供了一个方法,让页面跳转到新的地方
- location.assign(ret.redirectUrl) // 到时候的跳转是我们通过 JS 自己完成的,不是 HTTP 层面上的重定向(浏览器自动完成)
- return; // 这个 return 其实可以不加
- }
-
- var productList = ret.data;
- var oTbody = document.querySelector('tbody')
- // 通过遍历 productList,得到每一个 商品,根据商品中的值,修改 DOM 结构(为 <tbody> 添加一个 <tr> 一行
- for (var product of productList) {
- var html = "<tr>" +
- `<td>${product.name}</td>` + // 反引号,模板字符串
- `<td>${product.introduce}</td>` + // 反引号,模板字符串
- `<td>${product.stock}</td>` + // 反引号,模板字符串
- `<td>${product.unit}</td>` + // 反引号,模板字符串
- `<td>${product.price}</td>` + // 反引号,模板字符串
- `<td>${product.discount}</td>` + // 反引号,模板字符串
- "</tr>";
-
- // 添加到 tbody 的内部
- oTbody.innerHTML += html
- }
- }
- // 真正发起请求
- xhr.send()

资源名 | 作用 | 类型 |
---|---|---|
/product/update.html | 提供一个form表单 | 静态资源 |
/product/update.do | 保存的逻辑,对应的表是products | 动态资源 |
DoController下 /product/update.do
判断用户是否登录:通过session中的“currentUser”
1)否:无权限进行浏览操作,回到登录页面
2)是:获取当前登录用户信息,即User对象,继续进行更新操作
根据表单中的数据更新Products表
- @PostMapping("/product/update.do")
- public String productUpdate(ProductParam productParam, HttpServletRequest request) {
- log.debug("商品更新: 请求参数 = {}", productParam);
-
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明用户未登录
- log.debug("商品更新: 用户未登录,无权进行该操作");
- return "redirect:/login.html"; // 重定向到登录页,让用户登录
- }
-
- Product product = productService.update(currentUser, productParam);
-
- log.debug("商品更新: 成功,product = {}", product);
-
- return "redirect:/product/list.html";
- }

ProductService下update方法
构造Product对象:根据填入的数据(ProductParam)和 当前用户(User)构造
调用 mapper 的 updateByProductId,完成数据库插入
- public Product update(User user, ProductParam productParam) {
- Product product = new Product(user, productParam);
-
- productMapper.updateByProductId(product);
-
- return product;
- }
ProductMapper接口下的updateByProductId方法
void updateByProductId(Product product);
ProductMapper.xml文件
- <update id="updateByProductId">
- update products set
- user_id = #{userId},
- name = #{name},
- introduce = #{introduce},
- stock = #{stock},
- unit = #{unit},
- price = #{price},
- discount = #{discount}
- where product_id = #{productId}
- </update>
1. 订单状态:开始 -> 未支付 -> 已支付
2. 购买商品(开始 -> 未支付)
对应很多条SQL:整体上应该当作一个事务来对待
1)查询商品库存是否够
2)每个商品进行减库存操作 —— 更新
3)创建订单信息(状态是末支付)—— 插入
4)创建订单项信息
3. 确认(未支付->己支付)
更新订单状态:未支付更新 为已支付
update orders set status ='已支付’ where order_id = ... ;
4. 取消(未支付->开始)
进行数据的回滚操作:当时【购买商品】时所做的数据变更全部反向再来一遍
1)订单项删除【不考虑期间商品被下架的情况】
2)订单删除
3)商品的库存增加
事务: Order create(Map<商品id,购买数量>productMap):创建订单项 Order confirm(int orderld):确认订单 Order cancel(int orderld):取消订单 有了订单 id,我们就可以查询出该订单关联的所有商品id,从订单项表中可以查出
OrderService下create
1. 确认商品库存是否足够
1)执行
select … from products where product id in (?, ?, .., ?);
2)List<product>转成 Map<商品id, Product>
3)遍历,检查每个商品的库存是否够
2. 商品减库存:遍历每个商品(entry<商品 id,购买数量>),依次执行
update products set stock = stock- 购买数量 where product id = 商品 id
3. 创建订单记录
1)准备订单里的各种数据(uuid、创建订单时的时间(现在)、应付总额、实付总额,订单状态(未支付))
a. uuid 来自 UUID 算法计算出来后,把其中的"-"全部去掉
b. 当前时间:Timestamp.from(nstant.now0)
c. 应付金额:求和(商品的价格 商品的购买数量1针对每个商品)
d. 实付金额:求和(商品的价格* 商品的购买数量*商品折扣1 针对每个商品)
2)执行:插入订单
insert into orders (user id, uuid, created at, payable, actual, status) values (...)
4. 创建订单项记录
执行:插入订单
insert into order_items ( order_id,product_id,product_name,product_introduce,product_number,product_unit,product_price,product_discount ) values (...)
代码:
- @Transactional // 整体上应该作为事务存储
- // toBoughtProductMap<商品 id, 要购买的数量>
- public Order create(User user, Map<Integer, Integer> toBoughtProductMap) {
- log.debug("toBoughtProductMap = {}", toBoughtProductMap);
- // 1. 确认商品的库存足够
- Map<Integer, Product> productMap = 确认商品库存是否足够(toBoughtProductMap);
- log.debug("productMap = {}", productMap);
-
- // 2. 商品减库存
- 商品减库存(toBoughtProductMap);
- // // 3. 创建订单记录
- Order order = 创建订单记录(user, productMap, toBoughtProductMap);
- // 4. 创建订单项记录
- 插入订单项(order, productMap, toBoughtProductMap);
-
-
- return order;
- }
-
- private Order 创建订单记录(User user, Map<Integer, Product> productMap, Map<Integer, Integer> toBoughtProductMap) {
- String uuid = generateUUID();
- Timestamp createdAt = Timestamp.from(Instant.now());
-
- // 计算应付总额
- int payable = 计算应付总额(productMap, toBoughtProductMap);
- // 计算实付总额
- int actual = 计算实付总额(productMap, toBoughtProductMap);
- // 初识状态
- OrderStatus status = OrderStatus.未支付;
-
- Order order = new Order(user.getUserId(), uuid, createdAt, payable, actual, status);
-
- // OrderMapper 完成订单插入
- orderMapper.insert(order);
- log.debug("order = {}", order);
-
- return order;
- }
-
- private void 插入订单项(Order order, Map<Integer, Product> productMap, Map<Integer, Integer> toBoughtProductMap) {
- // 先遍历每个商品,构造 OrderItem 对象
- Set<Integer> productIdSet = productMap.keySet();
- Set<OrderItem> orderItemSet = new HashSet<>();
- for (Integer productId : productIdSet) {
- Product product = productMap.get(productId);
- int number = toBoughtProductMap.get(productId);
-
- OrderItem orderItem = new OrderItem(order, product, number);
- orderItemSet.add(orderItem);
- }
-
- orderItemMapper.insertBatch(orderItemSet);
- }
-
- // 实付总额就是每个商品的价格 * 商品购买数量 * 折扣,然后求和
- private int 计算实付总额(Map<Integer, Product> productMap, Map<Integer, Integer> toBoughtProductMap) {
- Set<Integer> productIdSet = productMap.keySet();
-
- int sum = 0;
- for (Integer productId : productIdSet) {
- int number = toBoughtProductMap.get(productId);
- Product product = productMap.get(productId);
- int price = product.getPrice();
- double discount = product.getDiscount() / 100.0;
-
- int productActual = (int)(price * number * discount);
- sum += productActual;
- }
-
- return sum;
- }
-
- // 实付总额就是每个商品的价格 * 商品购买数量,然后求和
- private int 计算应付总额(Map<Integer, Product> productMap, Map<Integer, Integer> toBoughtProductMap) {
- Set<Integer> productIdSet = productMap.keySet();
-
- int sum = 0;
- for (Integer productId : productIdSet) {
- int number = toBoughtProductMap.get(productId);
- Product product = productMap.get(productId);
- int price = product.getPrice();
-
- int productPayable = price * number;
- sum += productPayable;
- }
-
- return sum;
- }
-
- private String generateUUID() {
- String s = UUID.randomUUID().toString();
- return s.replace("-", "");
- }
-
- private void 商品减库存(Map<Integer, Integer> toBoughtProductMap) {
- for (Map.Entry<Integer, Integer> entry : toBoughtProductMap.entrySet()) {
- int productId = entry.getKey();
- int number = entry.getValue();
-
- productMapper.decrementStockByProductId(productId, number);
- }
- }
-
- private Map<Integer, Product> 确认商品库存是否足够(Map<Integer, Integer> toBoughtProductMap) {
- // 把所有的商品 id 收集起来
- Set<Integer> productIdSet = toBoughtProductMap.keySet();
-
- List<Product> productList = productMapper.selectProductListByProductIdSet(productIdSet);
-
- // 把 List<Product> -> Map<商品id, Product>
- Map<Integer, Product> productMap = new HashMap<>();
- for (Product product : productList) {
- productMap.put(product.getProductId(), product);
- }
-
- // TODO: 这里暂时不考虑传入的商品 id在表中不存在的情况了
- // 比较库存是否够
- for (Integer productId : productIdSet) {
- int number = toBoughtProductMap.get(productId);
- Product product = productMap.get(productId);
-
- if (number > product.getStock()) {
- throw new RuntimeException(product.getProductId().toString()); // 说明我们这个商品的库存是不够的
- }
- }
-
- // 说明所有商品的库存都是够的
- return productMap;
- }

查询库存和修改库存
ProductMapper下selectProductListByProductIdSet,decrementStockByProductId
- List<Product> selectProductListByProductIdSet(@Param("list") Set<Integer> productIdSet);
-
- void decrementStockByProductId(@Param("productId") int productId, @Param("number") int number);
ProductMapper.xml
- <select id="selectProductListByProductIdSet" resultType="com.xlinzhang.cash.model.product.Product">
- select product_id, user_id, name, introduce, stock, unit, price, discount
- from products where product_id in (
- <foreach collection="list" item="product_id" separator=", ">
- #{product_id}
- </foreach>
- )
- </select>
-
- <update id="decrementStockByProductId">
- update products set stock = stock - #{number} where product_id = #{productId}
- </update>
插入订单记录
OrderMapper下insert
void insert(Order order);
OrderMapper.xml
- <insert id="insert" useGeneratedKeys="true" keyProperty="orderId" keyColumn="order_id">
- insert into orders
- (user_id, uuid, created_at, payable, actual, status)
- values
- (#{userId}, #{uuid}, #{createdAt}, #{payable}, #{actual}, #{status.value})
- </insert>
插入订单项
OrderItemMapper下insertBatch
void insertBatch(@Param("set") Set<OrderItem> orderItemSet);
OrderItemMapper.xml
- <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
- insert into order_items
- (
- order_id,
- product_id,
- product_name,
- product_introduce,
- product_number,
- product_unit,
- product_price,
- product_discount
- )
- values
- <foreach collection="set" item="item" separator=", ">
- (
- #{item.orderId},
- #{item.productId},
- #{item.productName},
- #{item.productIntroduce},
- #{item.productNumber},
- #{item.productUnit},
- #{item.productPrice},
- #{item.productDiscount}
- )
- </foreach>
- </insert>

OrderService下confirm:更新当前订单状态为已支付
- @Transactional // 虽然只有一条 SQL,但加上也没错,本来一条 SQL 就是隐式事务
- public void confirm(int orderId) {
- // 更新订单记录(状态变成已支付,设置订单完成时间是当前时间)
- Timestamp finishedAt = Timestamp.from(Instant.now());
- OrderStatus status = OrderStatus.已支付;
-
- orderMapper.updateConfirm(orderId, finishedAt, status);
- }
修改订单记录状态
OrderMapper下updateConfirm
- void updateConfirm(
- @Param("orderId") int orderId,
- @Param("finishedAt") Timestamp finishedAt,
- @Param("status") OrderStatus status);
OrderMapper.xml文件
- <update id="updateConfirm">
- update orders set finished_at = #{finishedAt}, status = #{status.value} where order_id = #{orderId}
- </update>
OrderService下cancel
根据当前订单id(orderId) 查询出这个订单涉及的所有商品 id 和 它对应的购买数量
删除所有该订单(orderId) 关联的商品项记录(删除订单项)
删除这个订单
遍历每个订单中涉及到的商品,增加商品库存
- @Transactional
- public void cancel(int orderId) {
- // 1. 根据 orderId 查询出这个订单涉及的所有商品 id 和 它对应的购买数量
- List<OrderItem> orderItemList = orderItemMapper.selectByOrderId(orderId);
- Map<Integer, Integer> toBoughtProductMap = new HashMap<>();
- for (OrderItem orderItem : orderItemList) {
- int productId = orderItem.getProductId();
- int number = orderItem.getProductNumber();
- toBoughtProductMap.put(productId, number);
- }
-
- // 2. 删除所有该 orderId 关联的 order_items 记录(删除订单项)
- orderItemMapper.deleteByOrderId(orderId);
-
- // 3. 删除该 orderId 对应的订单记录
- orderMapper.deleteByOrderId(orderId);
-
- // 4. 遍历每个商品,为每个商品增加库存
- for (Map.Entry<Integer, Integer> entry : toBoughtProductMap.entrySet()) {
- int productId = entry.getKey();
- int number = entry.getValue();
-
- productMapper.incrementStockByProductId(productId, number);
- }
- }

查询订单中商品 和 删除订单项
OrerItemMapper下selectByOrderId,deleteByOrderId
- List<OrderItem> selectByOrderId(int orderId);
-
- void deleteByOrderId(int orderId);
OrerItemMapper.xml
- <select id="selectByOrderId" resultType="com.xlinzhang.cash.model.order.OrderItem">
- select product_id, product_number from order_items where order_id = #{orderId}
- </select>
-
- <delete id="deleteByOrderId">
- delete from order_items where order_id = #{orderId}
- </delete>
删除订单记录
OrderMapper下deleteByOrderId
void deleteByOrderId(int orderId);
OrderMapper.xml
- <delete id="deleteByOrderId">
- delete from orders where order_id = #{orderId}
- </delete>
恢复商品库存
ProductMapper下incrementStockByProductId
void incrementStockByProductId(@Param("productId") int productId, @Param("number") int number);
ProductMapper.xml文件
- <update id="incrementStockByProductId">
- update products set stock = stock + #{number} where product_id = #{productId}
- </update>
资源名 | 作用 | 类型 |
---|---|---|
/order/create.html | 提供一个form表单 | 静态资源 |
/order/create.do | 保存订单 | 动态资源 |
DoController下/order/create.do
1. 将订单参数createParam转成Map<商品id,购买数量>:
2. 判断用户是否登录
1)未登录:login.html
2)登录:session记录,继续执行
3. 生成订单:orderService下create事务
4. 跳转至用户正在处理的订单(刚刚生成的订单)详情页面
- @PostMapping("/order/create.do")
- public String orderCreate(@RequestParam("create-param") String createParam, HttpServletRequest request) {
- log.debug("购买商品: createParam = {}", createParam);
- // 1. 将订单createParam转成Map<商品id,购买数量>
- Map<Integer, Integer> toBoughtProductMap = new HashMap<>();
- // TODO: 暂时不考虑参数合法性的问题
- String[] split = createParam.split(",");
- for (String s : split) {
- String[] group = s.split("-");
- String productIdStr = group[0]; // 参数不对,就会抛异常
- String numberStr = group[1]; // 参数不对,就会抛异常
-
- int productId = Integer.parseInt(productIdStr); // 参数不对,就会抛异常
- int number = Integer.parseInt(numberStr); // 参数不对,就会抛异常
-
- toBoughtProductMap.put(productId, number);
- }
-
- log.debug("toBoughtProductMap = {}", toBoughtProductMap);
- // 2. 判断用户是否登录
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明用户未登录
- log.debug("商品更新: 用户未登录,无权进行该操作");
- return "redirect:/login.html"; // 重定向到登录页,让用户登录
- }
-
- log.debug("当前用户: user = {}", currentUser);
- Order order = orderService.create(currentUser, toBoughtProductMap);
- log.debug("创建订单: {}", order);
-
- return "redirect:/order/detail/" + order.getUuid();
- }

DoController下/order/detail/{uuid}
1. 判断用户是否登录
1)未登录:login.html
2)登录:session记录,继续执行
2. 查询当前正在处理的订单内容并展示
- @GetMapping("/order/detail/{uuid}")
- public String orderDetail(@PathVariable("uuid") String uuid, HttpServletRequest request, Model model) {
- log.debug("订单详情: uuid = {}", uuid);
-
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明用户未登录
- log.debug("商品更新: 用户未登录,无权进行该操作");
- return "redirect:/login.html"; // 重定向到登录页,让用户登录
- }
- log.debug("当前用户: user = {}", currentUser);
-
- OrderDetail orderDetail = orderService.query(uuid);
-
- log.debug("order = {}", orderDetail);
-
- model.addAttribute("order", orderDetail);
-
- return "order-detail";
- }

展示用户正在处理的订单详情
OrderService下query
- public OrderDetail query(String uuid) {
- OrderDetail order = orderMapper.selectByUUID(uuid);
-
- order.setItemList(orderItemMapper.selectAllByOrderId(order.getOrderId()));
-
- return order;
- }
查询用户处理的订单
OrderMapper下selectByUUID
OrderDetail selectByUUID(String uuid);
OrderMapper.xml
- <select id="selectByUUID" resultType="com.xlinzhang.cash.model.order.OrderDetail">
- select
- order_id, orders.user_id, username, uuid, created_at, status, payable, actual
- from orders
- join users on users.user_id = orders.user_id
- where uuid = #{uuid}
- </select>
查询当前订单的具体订单项
OrderItemMapper下selectAllByOrderId
List<OrderItemDetail> selectAllByOrderId(int orderId);
OrderItemMapper.xml
- <select id="selectAllByOrderId" resultType="com.xlinzhang.cash.model.order.OrderItemDetail">
- select product_id, product_name, product_price, product_number, product_unit, product_discount
- from order_items where order_id = #{orderId}
- order by id
- </select>
DoController下/order/confirm/{orderId}
判断用户是否登录,未登录:login.html;登录:session记录,继续执行
完成确认订单事务:OrderService下confirm事务
转到浏览订单页
- @GetMapping("/order/confirm/{orderId}")
- public String orderConfirm(@PathVariable int orderId, HttpServletRequest request) {
- log.debug("确认订单: orderId = {}", orderId);
-
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明用户未登录
- log.debug("商品更新: 用户未登录,无权进行该操作");
- return "redirect:/login.html"; // 重定向到登录页,让用户登录
- }
- log.debug("当前用户: user = {}", currentUser);
-
- orderService.confirm(orderId);
-
- return "redirect:/order/list.html";
- }

DoController下/order/cancel/{orderId}
判断用户是否登录,未登录:login.html;登录:session记录,继续执行
完成取消订单事务:OrderService下cancel事务
转到浏览订单页
- @GetMapping("/order/cancel/{orderId}")
- public String orderCancel(@PathVariable int orderId, HttpServletRequest request) {
- log.debug("确认订单: orderId = {}", orderId);
-
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明用户未登录
- log.debug("商品更新: 用户未登录,无权进行该操作");
- return "redirect:/login.html"; // 重定向到登录页,让用户登录
- }
- log.debug("当前用户: user = {}", currentUser);
-
- orderService.cancel(orderId);
-
- return "redirect:/order/list.html";
- }

资源名 | 作用 | 类型 |
---|---|---|
/order/list.html | 提供HTML框架 | 静态资源 |
/order/list.js | 发起Ajax请求,读取JSON格式的order列表,修改DOM树(动态的添加数据记录) | 静态资源 |
/order/list.json | 返回所有的order,以JSON形式返回 | 动态资源 |
JsonController下/order/list.json
判断用户是否登录:通过session中的“currentUser”
1)否:无权限进行浏览操作,回到登录页面
2)是:获取当前登录用户信息,即User对象,继续进行浏览操作
以列表形式返回Order表中数据:List<Order>
将后端列表数据 转换成 前端用的OrderListView对象:由于对外展示的对象格式和内部使用的对象略有区别,因此我们定义一个OrderView类。从数据表中,我们获取到的是一组view对象,因此再定义一个OrderListView类。这两个类只会在当前Controller下使用,所以就定义成当前Controller下的静态内部类
- @Data
- private static class OrderView {
- private String uuid;
- private String status;
- private Timestamp createdAt;
- private Timestamp finishedAt;
-
- OrderView(Order order) {
- this.uuid = order.getUuid();
- this.status = order.getStatus().toString();
- this.createdAt = order.getCreatedAt();
- this.finishedAt = order.getFinishedAt();
- }
- }
-
- @Data
- private static class OrderListView {
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private String redirectUrl;
- @JsonInclude(JsonInclude.Include.NON_NULL)
- private List<OrderView> data;
-
- public static OrderListView success(List<Order> orderList) {
- OrderListView view = new OrderListView();
- view.data = orderList.stream()
- .map(OrderView::new)
- .collect(Collectors.toList());
- return view;
- }
-
- public static OrderListView failure(String redirectUrl) {
- OrderListView view = new OrderListView();
- view.redirectUrl = redirectUrl;
- return view;
- }
- }
-
- @GetMapping("/order/list.json")
- public OrderListView orderList(HttpServletRequest request) {
- User currentUser = null;
- HttpSession session = request.getSession(false);
- if (session != null) {
- currentUser = (User) session.getAttribute("currentUser");
- }
-
- if (currentUser == null) {
- // 说明没有登录
- return OrderListView.failure("/login.html");
- }
-
- List<Order> orderList = orderService.getList();
-
- return OrderListView.success(orderList);
- }

OrserSerive下getList
- public List<Order> getList() {
- return orderMapper.selectAll();
- }
OrderMapper下selectAll
List<Order> selectAll();
OrderMapper.xml
- <select id="selectAll" resultType="com.xlinzhang.cash.model.order.Order">
- select
- uuid, status, created_at, finished_at
- from orders
- order by order_id desc
- </select>
项目实现了超市收银系统的基本功能,为用户提供了登陆、注册、上架商品、购买商品等基础功能。未来项目功能还可以进行改进:
1功能方面:增加统计功能(为超市老板开发).增加权限控制 (不同角色的功能不同)
2易用性方面:参数合法性校验、更新商品不需要再全部输入、购买商品可以使用对接扭描墙
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。