SpringBoot-项目5-订单模块

81. 确认订单页-显示收货地址列表

此前已经完成“显示收货地址列表”功能,客户端(页面)向http://localhost:8080/addresses发请求,就可以得到收货地址列表的数据!所以,只需要在orderConfirm.html页面中,当加载页面时,就直接向以上路径发请求,获取数据,并显示在下拉列表中即可:

  <script type="text/javascript">
  $(document).ready(function() {
    showAddressList();
  });
  
  // orderConfirm.html: 第118行:增加id
  function showAddressList() {
    console.log("准备收货地址列表……");
    $("#address-list").empty();
    $.ajax({
      "url":"/addresses",
      "type":"get",
      "dataType":"json",
      "success":function(json) {
        var list = json.data;
        console.log("count=" + list.length);
        for (var i = 0; i < list.length; i++) {
          console.log(list[i].name);
          var html = '';
          
          html = html.replace(/#{aid}/g, list[i].aid);
          html = html.replace(/#{tag}/g, list[i].tag);  
          html = html.replace(/#{name}/g, list[i].name);  
          html = html.replace(/#{province}/g, list[i].provinceName);  
          html = html.replace(/#{city}/g, list[i].cityName);  
          html = html.replace(/#{area}/g, list[i].areaName);  
          html = html.replace(/#{address}/g, list[i].address);  
          html = html.replace(/#{phone}/g, list[i].phone);  
            
          $("#address-list").append(html);
        }
      }
    });
  }
  </script>

82. 确认订单页-显示所选择的购物车数据-持久层

(a) 规划需要的SQL语句

在购物车列表页,用户可以自由选择若干条数据,点击“结算”时,页面会将用户勾选的数据id传递到确认订单页,在确认订单页中,就需要根据这些id来显示对应的数据,所以,需要实现”根据若干个id查询购物车列表“的功能,需要执行的SQL语句与此前完成的“查询某用户的购物车列表”是高度相似的,只是查询条件要改为“根据若干个id查询”,则SQL语句大致是:

select 
  cid, uid, pid, t_cart.num, t_cart.price,
  title, t_product.price AS realPrice, image
from 
  t_cart 
left join 
  t_product 
on 
  t_cart.pid=t_product.id 
where 
  cid in (?,?,? ... ?) 
order by 
  t_cart.created_time desc

(b) 设计抽象方法

CartMapper中添加:

  /**
   * 查询若干个数据id匹配的购物车列表
   * @param cids 若干个数据id
   * @return 匹配的购物车列表
   */
  List<CartVO> findByCids(Integer[] cids); // List / Integer[] / Integer...

© 配置映射并测试

映射:

  
  
  <select id="findByCids" resultType="cn.tedu.store.vo.CartVO">
    SELECT 
      cid, uid, pid, t_cart.num, t_cart.price,
      title, t_product.price AS realPrice, image
    FROM 
      t_cart 
    LEFT JOIN
      t_product 
    ON 
      t_cart.pid=t_product.id 
    WHERE 
      cid IN 
      <foreach collection="array" item="cid" separator=","
        open="(" close=")">
        #{cid}
      foreach>
    ORDER BY
      t_cart.created_time DESC
  select>

测试:

  @Test
  public void findByCids() {
    Integer[] cids = { 10, 8, 14, 12, 6, 15, 16, 18, 20 };
    List<CartVO> list = mapper.findByCids(cids);
    System.err.println("count=" + list.size());
    for (CartVO item : list) {
      System.err.println(item);
    }
  }

83. 确认订单页-显示所选择的购物车数据-业务层

(a) 规划业务流程、业务逻辑,并创建可能出现的异常

(b) 设计抽象方法

CartService添加抽象方法:

  /**
   * 查询若干个数据id匹配的购物车列表
   * @param cids 若干个数据id
   * @param uid 用户的id
   * @return 匹配的购物车列表
   */
  List<CartVO> getByCids(Integer[] cids, Integer uid);

© 实现抽象方法并测试

CartServiceImpl中,先将持久层新添加的抽象方法复制过来,改成私有方法并实现:

  /**
   * 查询若干个数据id匹配的购物车列表
   * @param cids 若干个数据id
   * @return 匹配的购物车列表
   */
  private List<CartVO> findByCids(Integer[] cids) {
    return cartMapper.findByCids(cids);
  }

然后,规划重写接口中的抽象方法:

  @Override
  public List<CartVO> getByCids(Integer[] cids, Integer uid) {
    // 调用findByCids(cids)执行查询,得到列表数据
    List<CartVO> carts = findByCids(cids);
    
    // 从列表中移除非当前登录用户的数据:在遍历过程中移除集合中的元素,需要使用迭代器
    Iterator<CartVO> it = carts.iterator();
    while (it.hasNext()) {
      CartVO cart = it.next();
      if (!cart.getUid().equals(uid)) {
        it.remove();
      }
    }

    // 返回列表
    return carts;
  }

最后,测试:

  @Test
  public void findByCids() {
    Integer[] cids = { 10, 8, 14, 12, 6, 15, 16, 18, 20 };
    Integer uid = 13;
    List<CartVO> list = service.getByCids(cids, uid);
    System.err.println("count=" + list.size());
    for (CartVO item : list) {
      System.err.println(item);
    }
  }

84. 确认订单页-显示所选择的购物车数据-控制器层

(a) 处理新创建的异常类型

(b) 设计需要处理的请求

  • 请求路径:/carts/get_by_cids
  • 请求参数:Integer[] cids, HttpSession session
  • 请求方式:GET
  • 响应数据:JsonResult>

© 处理请求并测试

CartController中处理请求:

  // http://localhost:8080/carts/get_by_cids?cids=6&cids=8&cids=15&cids=16
  @GetMapping("get_by_cids")
  public JsonResult<List<CartVO>> getByCids(Integer[] cids, HttpSession session) {
    Integer uid = getUidFromSession(session);
    List<CartVO> data = cartService.getByCids(cids, uid);
    return new JsonResult<>(OK, data);
  }

通过http://localhost:8080/carts/get_by_cids?cids=6&cids=8&cids=15&cids=16

85. 确认订单页-显示所选择的购物车数据-前端页面

function showCartList(){
			$("#cart-list").empty();
			$.ajax({
				"url":"/carts/get_by_cids",
				"data":location.search.substr(1),
				"type":"get",
				"dateType":"json",
				"success":function(json){
					var arr = json.date;
					for (var i = 0; i < arr.length; i++) {
						var html = ''
							+ ''
							+ '#{title}'
							+ #{realPrice}'
							+ '#{num}'
							+ '#{totalPrice}'
							+ '';
				          console.log(arr[i].cid);
				          // 使用正则全文匹配
				          html = html.replace(/#{image}/g, arr[i].image); 
				          html = html.replace(/#{cid}/g, arr[i].cid); 
				          html = html.replace(/#{title}/g, arr[i].title); 
				          html = html.replace(/#{realPrice}/g, arr[i].realPrice); 
				          html = html.replace(/#{num}/g, arr[i].num); 
				          html = html.replace(/#{totalPrice}/g, arr[i].realPrice * arr[i].num);	
				          
						$("#cart-list").append(html);
					}
				}
			});
		}

86. 订单-创建数据表

创建订单表

CREATE TABLE t_order (
  oid INT AUTO_INCREMENT COMMENT '订单id',
  uid INT COMMENT '归属用户的id',
  recv_name VARCHAR(20) 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 '收货详细地址',
  order_time DATETIME COMMENT '下单时间',
  pay_time DATETIME COMMENT '支付时间',
  total_price BIGINT COMMENT '支付金额',
  status INT(1) COMMENT '订单状态:0-未支付,1-已支付,2-已取消,3-已关闭',
  created_user VARCHAR(20) COMMENT '创建人',
  created_time DATETIME COMMENT '创建时间',
  modified_user VARCHAR(20) COMMENT '最后修改人',
  modified_time DATETIME COMMENT '最后修改时间',
  PRIMARY KEY (oid)
) DEFAULT CHARSET=utf8mb4;

创建订单商品表

CREATE TABLE t_order_item (
  id INT AUTO_INCREMENT COMMENT 'id',
  oid INT COMMENT '归属订单',
  pid INT COMMENT '商品id',
  title VARCHAR(100) 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)
) DEFAULT CHARSET=utf8mb4;

87. 订单-创建实体类

订单的实体类:

/**
 * 订单数据的实体类
 */
public class Order extends BaseEntity {

  private static final long serialVersionUID = -3216224344757796927L;

  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 Date orderTime;
  private Date payTime;
  private Long totalPrice;
  private Integer status;
  
}

订单商品的实体类:

/**
 * 订单商品实体类
 */
public class OrderItem extends BaseEntity {

  private static final long serialVersionUID = -8879247924788259070L;
  
  private Integer id;
  private Integer oid;
  private Integer pid;
  private String title;
  private String image;
  private Long price;
  private Integer num;
  
}

88. 订单-创建订单-持久层

cn.demo.store.mapper中创建OrderMapper接口,添加抽象方法:

/**
 * 处理订单数据和订单商品数据的持久层接口
 */
public interface OrderMapper {

  /**
   * 插入订单数据
   * @param order 订单数据
   * @return 受影响的行数
   */
  Integer insertOrder(Order order);

  /**
   * 插入订单商品数据
   * @param orderItem 订单商品数据
   * @return 受影响的行数
   */
  Integer insertOrderItem(OrderItem orderItem);

}

src/main/resources/mappers下复制粘贴得到OrderMapper.xml文件,配置以上抽象方法的映射:

  


<mapper namespace="cn.demo.store.mapper.OrderMapper">

  
  
  <insert id="insertOrder" useGeneratedKeys="true" keyProperty="oid">
    INSERT INTO t_order (
      uid, recv_name, recv_phone, recv_province,
      recv_city, recv_area, recv_address, order_time,
      pay_time, total_price, status,
      created_user, created_time, modified_user, modified_time
    ) VALUES (
      #{uid}, #{recvName}, #{recvPhone}, #{recvProvince},
      #{recvCity}, #{recvArea}, #{recvAddress}, #{orderTime},
      #{payTime}, #{totalPrice}, #{status},
      #{createdUser}, #{createdTime}, #{modifiedUser}, #{modifiedTime}
    )
  insert>
  
  
  
  <insert id="insertOrderItem" useGeneratedKeys="true" keyProperty="id">
    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}
    )
  insert>

mapper>

最后,创建测试类,并测试以上方法:

@RunWith(SpringRunner.class)
@SpringBootTest 
public class OrderMapperTests {
  
  @Autowired
  private OrderMapper mapper;

  @Test
  public void insertOrder() {
    Order order = new Order();
    order.setUid(1);
    Integer rows = mapper.insertOrder(order);
    System.err.println("rows=" + rows);
    System.err.println(order);
  }
  
  @Test
  public void insertOrderItem() {
    OrderItem orderItem = new OrderItem();
    orderItem.setOid(1);
    Integer rows = mapper.insertOrderItem(orderItem);
    System.err.println("rows=" + rows);
    System.err.println(orderItem);
  }

}

89. 订单-创建订单-业务层

在本次处理过程中,需要根据收货地址id查询收货地址详情,由于是在Service层调用的,应该在AddressService中定义相关的方法,以便于调用!

所以,先在AddressService 中添加抽象方法:

  /**
   * 根据收货地址id查询收货地址详情
   * @param aid 收货地址id
   * @return 匹配的收货地址详情,如果没有匹配的数据,则返回null
   */
  Address getByAid(Integer aid);

然后,在AddressServiceImpl中实现以上方法:

  @Override
  public Address getByAid(Integer aid) {
    // 调用私有的findByAid()找出数据
    Address result = findByAid(aid);
    // 判断查询结果是否为null
    if (result == null) {
      // 是:AddressNotFoundException
      throw new AddressNotFoundException("获取收货地址数据失败!尝试访问的数据不存在!");
    }

    // 将不需要对外提供的属性值设置为null
    result.setProvinceCode(null);
    result.setCityCode(null);
    result.setAreaCode(null);
    result.setIsDefault(null);
    result.setCreatedUser(null);
    result.setCreatedTime(null);
    result.setModifiedUser(null);
    result.setModifiedTime(null);
    // 返回数据
    return result;
  }

创建OrderService接口,在接口中添加抽象方法:

Order createOrder(Integer aid, Integer[] cids, Integer uid, String username);

创建OrderServiceImpl类,实现以上接口,添加@Service注解,添加@Autowired private OrderMapper orderMapper;持久层对象、@Autowired private AddressService addressService;处理收货地址的业务对象、@Autowired private CartService cartService;处理购物车数据的业务对象:

@Service
public class OrderServiceImpl implements OrderService {
  
  @Autowired 
  private OrderMapper orderMapper;
  @Autowired 
  private AddressService addressService;
  @Autowired 
  private CartService cartService;
  
}

然后,将持久层的2个方法复制到当前类中,改为私有方法并实现:

  /**
   * 插入订单数据
   * @param order 订单数据
   */
  private void insertOrder(Order order) {
    Integer rows = orderMapper.insertOrder(order);
    if (rows != 1) {
      throw new InsertException("创建订单失败!插入订单数据时出现未知错误,请联系系统管理员!");
    }
  }

  /**
   * 插入订单商品数据
   * @param orderItem 订单商品数据
   */
  private void insertOrderItem(OrderItem orderItem) {
    Integer rows = orderMapper.insertOrderItem(orderItem);
    if (rows != 1) {
      throw new InsertException("创建订单失败!插入订单商品数据时出现未知错误,请联系系统管理员!");
    }
  }

然后,规划如何重写接口中的抽象方法:

  @Override
  @Transactional
  public Order createOrder(Integer aid, Integer[] cids, Integer uid, String username) {
    // 创建当前时间对象now
    Date now = new Date();

    // 根据参数aid调用addressService.getByAid()查询收货地址详情
    Address address = addressService.getByAid(aid);

    // 根据参数cids调用cartService.getByCids(),得到List
    List<CartVO> carts = cartService.getByCids(cids, uid);
    // 定义totalPrice变量
    Long totalPrice = 0L;
    // 遍历以上查询到的List,计算出totalPrice
    for (CartVO cart : carts) {
      totalPrice += cart.getRealPrice() * cart.getNum();
    }

    // 创建Order对象
    Order order = new Order();
    // 补全Order对象中的属性:uid > 参数uid
    order.setUid(uid);
    // 补全Order对象中的属性:recv* > 收货地址详情
    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对象中的属性:orderTime > now
    order.setOrderTime(now);
    // 补全Order对象中的属性:payTime > null
    // 补全Order对象中的属性:totalPrice > totalPrice
    order.setTotalPrice(totalPrice);
    // 补全Order对象中的属性:status > 0
    order.setStatus(0);
    // 补全Order对象中的属性:4个日志
    order.setCreatedUser(username);
    order.setCreatedTime(now);
    order.setModifiedUser(username);
    order.setModifiedTime(now);
    // 调用insertOrder(Order order)插入订单数据
    insertOrder(order);

    // 遍历查询到的List
    for (CartVO cart : carts) {
      // -- 创建OrderItem对象
      OrderItem orderItem = new OrderItem();
      // -- 补全OrderItem对象中的属性:oid > order.getOid();
      orderItem.setOid(order.getOid());
      // -- 补全OrderItem对象中的属性:pid, title, image, price, num > CartVO对象中的属性
      orderItem.setPid(cart.getPid());
      orderItem.setTitle(cart.getTitle());
      orderItem.setImage(cart.getImage());
      orderItem.setPrice(cart.getRealPrice());
      orderItem.setNum(cart.getNum());
      // -- 补全OrderItem对象中的属性:4个日志
      orderItem.setCreatedUser(username);
      orderItem.setCreatedTime(now);
      orderItem.setModifiedUser(username);
      orderItem.setModifiedTime(now);
      // -- 多次调用insertOrderItem(OrderItem orderItem)插入订单商品数据
      insertOrderItem(orderItem);
    }

    // 返回订单数据
    Order result = new Order();
    result.setOid(order.getOid());
    result.setTotalPrice(order.getTotalPrice());
    return result;
  }

完成后,创建测试类,并测试:

@RunWith(SpringRunner.class)
@SpringBootTest
public class OrderServiceTests {

  @Autowired
  private OrderService service;

  @Test
  public void createOrder() {
    try {
      Integer aid = 28;
      Integer[] cids = { 5, 10, 14, 15 };
      Integer uid = 12;
      String username = "HAHA";
      service.createOrder(aid, cids, uid, username);
      System.err.println("OK.");
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
      System.err.println(e.getMessage());
    }
  }
  
}

90. 订单-创建订单-控制器层

关于需要处理的请求:

  • 请求路径:/orders/create
  • 请求参数:Integer aid, Integer[] cids, HttpSession session
  • 请求方式:POST
  • 响应数据:JsonResult

创建控制器类,并处理请求:

@RestController
@RequestMapping("orders")
public class OrderController extends BaseController {
  
  @Autowired
  private OrderService orderService;
  
  // http://localhost:8080/orders/create?aid=28&cids=12&cids=8&cids=10
  @PostMapping("create")
  public JsonResult<Order> create(Integer aid, Integer[] cids, HttpSession session) {
    Integer uid = getUidFromSession(session);
    String username = getUsernameFromSession(session);
    Order data = orderService.createOrder(aid, cids, uid, username);
    return new JsonResult<>(OK, data);
  }

}

91. 订单-创建订单-前端页面

$("#btn-create-order").click(function(){
			// 前端对数据进行校验
			$.ajax({
				"url":"/orders/create",
				"data":$("#form-create-order").serialize(),
				"type":"post",
				"dateType":"json",
				"success":function(json){
					if(json.state==200){
						alert("创建订单成功!订单号是:");
						
					}else{
						alert(json.message);
					}
				},
				"error":function(){
					alert("您的登录信息已过期,请重新登录!");
				}
			});
		});

92. 订单-创建订单-修改业务-创建时删除购物车中对应的数据

在持久层,并没有删除购物车中的数据的功能,由于具体操作是购物车数据,所以,应该由CartMapper负责完成!所以,在CartMapper接口中添加:

Integer deleteByCids(@Param("cids") Integer[] cids, @Param("uid") Integer uid);

配置的映射:

  <!-- 删除某用户的若干个购物车数据 -->
  <!-- Integer deleteByCids(
      @Param("cids") Integer[] cids, 
      @Param("uid") Integer uid
  ); -->
  <delete id="deleteByCids">
    DELETE FROM
      t_cart
    WHERE
      uid=#{uid} AND cid IN
      <foreach collection="cids" item="cid" separator=","
        open="(" close=")">
        #{cid}
      </foreach>
  </delete>

关于测试:

  @Test
  public void deleteByCids() {
    Integer uid = 12;
    Integer[] cids = { 5, 7, 9 };
    Integer rows = mapper.deleteByCids(cids, uid);
    System.err.println("rows=" + rows);
  }

然后,还需要在处理购物车的业务层提供该功能,所以,在CartService中添加:

void delete(Integer[] cids, Integer uid);

CartServiceImpl中,先将持久层新增的方法复制过来,改为私有方法:

  /**
   * 删除某用户的若干个购物车数据
   * @param cids 被删除的购物车数据的id
   * @param uid 用户的id
   */
  private void deleteByCids(Integer[] cids, Integer uid) {
    Integer rows = cartMapper.deleteByCids(cids, uid);
    if (rows < 1) {
      throw new DeleteException("删除购物车数据失败!删除购物车数据时出现未知错误,请联系系统管理员!");
    }
  }

然后重写业务接口中的抽象方法:

public void delete(Integer[] cids, Integer uid) {
  deleteByCids(cids, uid);
}

完成后,测试:

  @Test
  public void delete() {
    Integer uid = 12;
    Integer[] cids = { 6, 8 };
    service.delete(cids, uid);
    System.err.println("OK.");
  }

OrderServiceImpl中添加,调用其删除购物车方法即可

// 创建订单后删除购物车中的记录
cartService.delete(cids, uid);
// 将库存做减法操作

93. 创建订单-销库存

销库存的操作本质是减少商品表中的库存值,是关于商品数据的操作,所以,应该在处理商品数据的持久层和业务层来完成对应的操作。

先在ProductMapper中添加“更新商品库存值”的抽象方法,其对应的SQL语句是:

update t_product set num=?, modified_user=?, modified_time=? where id=?

所以,可以将抽象方法设计为:

  /**
   * 更新商品的库存
   * @param id 商品的id
   * @param num 新的库存值
   * @param modifiedUser 最后修改人
   * @param modifiedTime 最后修改时间
   * @return 受影响的行数
   */
  Integer updateNumById(
    @Param("id") Integer id, 
    @Param("num") Integer num, 
    @Param("modifiedUser") String modifiedUser, 
    @Param("modifiedTime") Date modifiedTime
  );

然后,在ProductMapper.xml中配置映射:

<update id="updateNumById">
    UPDATE 
      t_product
      SET
        num=#{num},
        modified_user=#{modifiedUser},
        modified_time=#{modifiedTime}
      WHERE
        id=#{id}
update>

完成后,在ProductMapperTests中编写并执行单元测试:

  @Test
  public void updateNumById() {
    Integer id = 10000001;
    Integer num = 5;
    String modifiedUser = "系统管理员";
    Date modifiedTime = new Date();
    Integer rows = mapper.updateNumById(id, num, modifiedUser, modifiedTime);
    System.err.println("rows=" + rows);
  }

接下来,应该完成业务层的开发,首先,应该创建ProductOutOfStockException异常,用于表示“商品库存不足”。

然后,在业务层接口ProductService中添加抽象方法:

  /**
   * 减少商品的库存
   * @param id 商品的id
   * @param amount 减少的数量
   * @param username 用户名
   */
  void reduceNum(Integer id, Integer amount, String username);

在业务层实现类ProductServiceImpl中,先将持久层的抽象方法复制过来,改为私有方法,并实现:

  /**
   * 更新商品的库存
   * @param id 商品的id
   * @param num 新的库存值
   * @param modifiedUser 最后修改人
   * @param modifiedTime 最后修改时间
   */
  private void updateNumById(Integer id, Integer num, String modifiedUser, Date modifiedTime) {
    Integer rows = productMapper.updateNumById(id, num, modifiedUser, modifiedTime);
    if (rows != 1) {
      throw new UpdateException("更新商品库存失败!更新库存值时出现未知错误,请联系系统管理员!");
    }
  }

然后,设计重写接口中的抽象方法:

  @Override
  public void reduceNum(Integer id, Integer amount, String username) {
    // 基于参数id调用findById()查询商品数据
    Product result = findById(id);
    // 判断查询结果是否为null
    if (result == null) {
      // 是:ProductNotFoundException
      throw new ProductNotFoundException("减少商品库存失败!尝试访问的商品数据不存在!");
    }

    // 通过查询结果可以得到原库存值,结合参数amount,计算得到新的库存值
    Integer num = result.getNum() - amount;
    // 判断新的库存值是否<0
    if (num < 0) {
      // 是:抛出ProductOutOfStockException
      throw new ProductOutOfStockException("更新商品库存失败!商品库存不足!");
    }

    // 调用updateNumById()更新商品的库存
    updateNumById(id, num, username, new Date());
  }

完成后,在ProductServiceTests中测试:

  @Test
  public void reduceNum() {
    try {
      Integer id = 10000001;
      Integer amount = 5;
      String username = "ADMIN";
      service.reduceNum(id, amount, username);
      System.err.println("OK.");
    } catch (ServiceException e) {
      System.err.println(e.getClass().getName());
      System.err.println(e.getMessage());
    }
  }

最后,在GlobalHandleException类中添加处理ProductOutOfStockException异常。

至此,处理商品数据的持久层和业务层已经完成所需要负责的功能,只需要创建订单过程中添加“销库存”的操作即可。

94. 超时未支付时归还库存的解决方案

首先,如果只是解决“归还库存”的功能,其数据操作与以上“销库存”是极为相似的,代码的开发难度也不高。该操作比较纠结的是“如何归还”,也就是通过什么技术手段来实现“创建订单后,15分钟(或其它)后未支付则关闭订单并归还库存”。

关于功能的实现,可以通过线程来操作,例如,自定义一个线程,大概是:

public class XxxThread extends Thread {
  private Integer pid;
  private Integer amount;
  
  public XxxThread(Integer pid, Integer amount) {
    this.pid = pid;
    this.amount = amount;
  }
  
  @Override
  public void run() {
    try {
      // 休眠15分钟
      Thread.sleep(15 * 60 * 1000);
    } catch (Exception e) {
    }
    // 判断订单状态,如果状态仍为0,表示未支付,则归还库存
  }
  
}

然后,每次创建订单时,都启动一个这样的线程即可!

当然,如果不使用线程,使用其它相关类似的做法也是可以的,例如定时器等。

这种做法是非常直接且有效的做法!但是,由于每个线程的执行时间都是15分钟以上,在大型电商平台中,15分钟内产生的订单量可能非常大,而每个订单对应1个线程,就会导致服务器需要创建大量的线程对象,并在在内存中存在大量的线程对象!甚至,服务器根本不足以支撑实现!

以上做法不可行时,就需要考虑其它的解决方案,可行的解决方案还可以是:

public class XxxThread extends Thread {
  
  private boolean isRunning;
  
  public void setIsRunning(boolean isRunning) {
    this.isRunning = isRunning;
  }
  
  @Override
  public void run() {
    isRunning = true;
    while (isRunning) {
      try {
        // 休眠10秒钟
        Thread.sleep(10 * 1000);
      } catch (Exception e) {
      }
      // 查找订单表中所有status=0并且order_time达到15分钟或以上的数据
      // 将这些订单数据的status改为“已关闭”,并基于这些订单数据归还库存
    }
  }
  
}

使用这种做法的缺陷在于时效性可能不够精准,以上代码中休眠的时间越长,误差就会越大!但是,由于时效性不精准导致的误差,最大的问题就是“订单刚刚超时的那一段时间之前,用户仍可以支付该订单”,这个问题作为电商平台是可以接受的,所以,可以不当成实际需要解决的问题。

95. 关于参数检查

首先,客户端应该对所有需要提交到服务器的数据进行基本检查,例如字符串的长度、字符串的组成、字符串的格式、数值的大小等等,如果任何一个数据检查不通过,都不应该提交请求到服务器。

然后,在服务器端,在控制器中,收到请求参数的第一时间,也应该做同样的检查,因为客户端提交的数据都应该视为不可靠,在大型应用中,客户端的种类可能比较多,如果某些数据格式有所调整,并不一定是所有客户端都使用了新的规则,甚至有些客户端版本没有更新,则会导致一些没有通过新规则检查的数据被提交到服务器,甚至,客户端软件是安装在用户的设备的,存在被破解、篡改的可能,所以,客户端提交的所有数据都应该视为不可靠数据,都必须检查!

需要注意的是:即使在服务器的控制器中有完整的数据格式检查,客户端的检查依然是必须存在的,这样的阻止大量的错误格式数据的请求,从而减轻服务器的压力!

所以,客户端检查的目的是为了拦截绝大部分格式本身就有问题的数据,当数据本身有问题时,并不向服务器发送请求,以减轻服务器压力,而服务器端的控制器检查的目的才是真正确保数据格式无误的!

在一些大型应用中,甚至业务层(Service)也可能会对各参数进行检查!在普通的数据处理中,数据提交到服务器后,会由控制器接收,控制器对这些数据进行检查后,再调用业务层进行下一步的处理,此时,业务层得到的数据参数其实都是控制器已经检查过来,都是格式无误的数据,可以不必检查,但是,不排除某些业务方法并不是由控制器调用的,例如一些计划任务等,它们是服务器内部自发执行,可能某些参数来自数据库、配置信息、程序运行过程中产生的值,所以,这些值并不一定是安全有效的值,也应该在执行业务方法之前,对这些值进行基本检查。

所以,关于参数的检查主要体现在:

  • 客户端检查:保证该客户端不会提交格式错误的数据到服务器;
  • 服务器端控制器检查:保证接收到的数据格式无误;
  • 服务器端业务层检查:如果任何客户端都没有提交请求,却有某些业务需要被执行,这些业务的执行参数需要被检查。

你可能感兴趣的:(Java)