高并发项目学习

用户业务

短信发送业务

注册之前输入手机号,请求后端getOtp接口。接口生成验证码后,发送
到用户手机,并且用Map将验证码和手机绑定起来。企业级开发将Map放
到分布式Redis里面,这里直接放到Session里面

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//用户获取otp短信接口
@RequestMapping(value="courseselect/getotp",method = RequestMethod.POST)
@ResponseBody
public CommonReturnType getOtp(@RequestParam(value="stuno",required =
false)String stuno){
//按照一定的规则生成OTP验证码,一共5位
Random random = new Random();
int randomInt = random.nextInt(90000);
randomInt += 10000;
String otpCode = String.valueOf(randomInt);
//将OTP验证码同用户手机号关联,使用httpsession的方式绑定手机号与OTPCODE
httpServletRequest.getSession().setAttribute(stuno, otpCode);
String infomation=(String) this.httpServletRequest.getSession()
.getAttribute(stuno);
return CommonReturnType.create(otpCode);
}

注册业务

注册请求后端StuController.register接口,先进行短信验证,然后
将注册信息封装到StuModel,调用StuServiceImpl.register(),先
对注册信息进行入参校验,再将StuModel转成StuDO、StuPasswordDO
存入到数据库。同时需要注意的是StuServiceImpl.register() 方法
,设计到了数据库写操作,需要加上@Transactional注解,以事务的
方式进行处理

1
2
controller.StuController.register()
service.impl.StuServiceImpl.register()

添加数据时使用insertselective而不用insert,insertselective
会首先判断字段是否为null,如果为null就跳过,也就是不insert这
个字段,完全依赖于数据库提供的默认值。null 字段对于前端的展示
没有意义

登录业务

登录请求后端UserController.login接口,前端传过来手机号和密码。
判空之后,调用UserServiceImpl.validateLogin方法,这个方法先
通过手机号查询user_info表,看是否存在该用户返回UserDO 对象,
再根据UserDO.id去user_password表中查询密码。如果密码匹配则返
回UserModel对象给login方法,最后login方法将UserModel对象存
放到Session里面,即完成了登录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public UserModel validateLogin(String telphone,String encrptPassword) 
throws BusinessException{
//通过用户手机获取用户信息
UserDO userDO = userDOMapper.selectByTelphone(telphone);
if (userDO == null) {
throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL);
}
userPasswordDO userPasswordDO = userPasswordDOMapper.selectByUserId
(userDO.getId());
UserModel userModel = convertFromDataObject(userDO, userPasswordDO);
UserSaltDO userSaltDO= userSaltDOMapper.selectByUserId(userDO.getId());
String password=DigestUtils.md5Hex(encrptPassword+userSaltDO.getSalt());
//比对用户信息内加密的密码是否和传输进来的密码相匹配
if (!StringUtils.equals(password, userModel.getEncrptPassword())) {
throw new BusinessException(EmBusinessError.USER_LOGIN_FAIL);
}
return userModel;
}

课程业务

课程添加业务

请求后端ItemController.create接口,传入商品创建的各种信息,封装
到ItemModel对象,调用ItemServiceImpl.createItem方法,进行入参
校验,然后将ItemModel转换成ItemDO和ItemStockDO对象,分别写入数
据库

获取课程业务

请求后端ItemController.get接口传入一个Id,通过ItemServiceImpl
.getItemById先查询出ItemDO对象,再根据这个对象查出ItemStockDO
对象,最后两个对象封装成一个ItemModel对象返回

查询所有课程

请求后端ItemController.list接口,跟上面类似查询所有课程

交易业务

下单业务

请求后端OrderController.createOrder接口,传入商品IdItemId和下
单数量amount。接着在Session中获取用户登录信息,如果用户没有登录
,直接抛异常

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
@GetMapping(value = "soquick/item/createorder")
@ResponseBody
public CommonReturnType createOrder(@RequestParam(value = "itemId")
Integer itemId,@RequestParam(value = "amount") Integer amount,
@RequestParam(value = "promoId",required = false) Integer promoId)
throws BusinessException {

//获取用户登录信息
Boolean isLogin = (Boolean) httpServletRequest.getSession().
getAttribute("IS_LOGIN");
if (isLogin == null || !isLogin.booleanValue()) {
throw new BusinessException(EmBusinessError.USER_NOT_LOGIN,
"用户还未登录,不能下单");
}
HttpSession session=httpServletRequest.getSession();
UserModel userModel = (UserModel) httpServletRequest.getSession().
getAttribute("LOGIN_USER");
OrderModel orderModel = orderService.createOrder(userModel.getId(),
itemId,promoId, amount);
return CommonReturnType.create(null);
}

在将订单存入库之前,先要调用OrderServiceImpl.createOrder方法
,对课程信息、学生信息、下单数量进行校验

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//1.校验下单状态,下单的商品是否存在,用户是否合法,购买数量是否正确
ItemModel itemModel = itemService.getItemById(itemId);
if (itemModel == null) {
throw new BusinessException(EmBusinessError.
PARAMETER_VALIDATION_ERROR, "商品信息不存在");
}
UserModel userModel = userService.getUserById(userId);
if (userModel == null) {
throw new BusinessException(EmBusinessError.
PARAMETER_VALIDATION_ERROR, "用户信息不存在");
}
if (amount <= 0 || amount > 99) {
throw new BusinessException(EmBusinessError.
PARAMETER_VALIDATION_ERROR, "数量信息不存在");
}

此外还需要校验库存是否足够,最后将订单入库,再让销量增加

1
2
3
4
5
6
//2.落单减库存
//这个结果返回受影响的行数
boolean result = itemService.decreaseStock(itemId, amount);
if (!result) {
throw new BusinessException(EmBusinessError.STOCK_NOT_ENOUGH);
}

订单ID的生成

订单ID不能是简单的自增长,而是要符合一定的规则,比如前8位是年月
日,中间6位为自增序列,最后2位为分库分表信息

  1. 前8位比较好实现,使用LocalDateTime,处理一下格式即可
  2. 中间6位自增序列,需要新建一个sequence_info表,里面包含name、
    current_value、step三个字段。这个表及其对应的DO专门用来产生自增
    序列
  3. generatorOrderNo方法需要将序列更新信息写入到sequence_info
    表,而且该方法封装在OrderServiceImpl.createOrder 方法中。如果
    createOrder执行失败会进行回滚,默认情况下,generatorOrderNo
    也会回滚。而我们希望生成ID的事务不受影响,就算订单创建失败,ID
    还是继续生成,所以generatorOrderNo方法使用了REQUIRES_NEW事
    务传播方式
1
2
3
4
5
6
//生成交易流水号
orderModel.setId(generateOrderNo());
OrderDO orderDO=convertFromOrderModel(orderModel);
orderDOMapper.insertSelective(orderDO);
//加上商品的销量
itemService.increaseSales(itemId, amount);

秒杀业务

秒杀DO/Model和VO

PromoDO包含活动名称、起始、结束时间、参与活动的商品id、参与活动的、
价格。而我们希望在前端显示活动的状态,是开始?还是结束?还是正在
进行中?所以PromoModel对象新加一个status字段,通过从数据库的
start_time和end_time字段,与当前系统时间做比较,设置状态

1
2
3
4
5
6
7
8
//1是还未开始,2是进行中,3是已结束
if(promoModel.getStartDate().isAfterNow()) {
promoModel.setStatus(1);
}else if(promoModel.getEndDate().isBeforeNow()){
promoModel.setStatus(3);
}else{
promoModel.setStatus(2);
}

对于ItemModel,需要将PromoModel属性添加进去,这样就完成了商品和
活动信息的关联,在ItemServiceImpl.getItemById中,除了要查询商
品信息ItemDO、库存信息ItemStockDO外,还需要查询出PromoModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public ItemModel getItemById(Integer id) {
ItemDO itemDO=itemDOMapper.selectByPrimaryKey(id);
if(itemDO==null) return null;
//操作获得库存数量
ItemStockDO itemStockDO=itemStockDOMapper.selectByItemId(itemDO.getId());
//将dataObj转换成Model
ItemModel itemModel=convertModelFromDataObject(itemDO,itemStockDO);
//获取商品的活动信息
PromoModel promoModel= promoService.getPromoByItemId(itemModel.getId());
if(promoModel!=null&&promoModel.getStatus()!=3){
itemModel.setPromoModel(promoModel);
}
return itemModel;
}

对于ItemVO,也是一样的,我们需要把活动的信息(活动进行信息、活
动价格等)显示给前端,所以需要在ItemVO 里面添加promoStatus、
promoPrice等属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private String imgUrl;
//商品是否在秒杀活动中,以及其状态
private Integer promoStatus;
private BigDecimal promoPrice;
private Integer promoId;
//开始时间,用来做倒计时展示
private String startDate;

//ItemController
private ItemVO convertVOFromModel(ItemModel itemModel){
if(itemModel==null) return null;
ItemVO itemVO=new ItemVO();
BeanUtils.copyProperties(itemModel,itemVO);
//有秒杀活动,就在ItemVO设置相应信息。
if(itemModel.getPromoModel()!=null){
itemVO.setPromoStatus(itemModel.getPromoModel().getStatus());
itemVO.setPromoId(itemModel.getPromoModel().getId());
itemVO.setStartDate(itemModel.getPromoModel().getStartDate().
toString(DateTimeFormat.forPattern("yyyy-MM-dd HH:mm:ss")));
itemVO.setPromoPrice(itemModel.getPromoModel().
getPromoItemPrice());
}else{
itemVO.setPromoStatus(0);
}
}

升级获取商品业务

之前获取的商品不包含秒杀活动信息,现在需要把活动信息添加进去。还
是先请求ItemController.list 接口,获取所有商品信息。然后通过点
击的商品Id,请求ItemController.get接口,查询商品详细信息。
首先根据Id调用ItemServiceImpl.getItemById查询出商品信息、库
存信息、秒杀活动信息,一并封装到ItemModel中。然后再调用上面的
convertVOFromModel,将这个ItemModel对象转换成ItemVO对象,
包含了秒杀活动的信息,最后返回给前端以供显示

活动商品下单业务

秒杀活动商品的下单需要单独处理,以“秒杀价格”入下单库。所以OrderDO
也需要添加promoId属性

1
2
private BigDecimal orderPrice;
private Integer promoId;

之前活动商品的下单附带itemId、amount请求OrderController.createOrder
接口,现在会附带一个promoId请求接口,这个参数会作为OrderServiceImpl
.createOrder的参数,进行参数校验

1
2
3
4
5
6
7
8
9
10
11
12
//校验活动信息
if(promoId!=null){
//1.校验对应活动是否适用于该商品
if(promoId.intValue()!=itemModel.getPromoModel().getId()){
throw new BizException(EmBizError.PARAMETER_VALIDATION_ERROR,
"活动信息不存在");
//2.校验活动是否在进行中
}else if (itemModel.getPromoModel().getStatus()!=2){
throw new BizException(EmBizError.PARAMETER_VALIDATION_ERROR,
"活动还未开始");
}
}

最后如果promoId不为空,那么订单的价格就以活动价格为准

1
2
3
4
5
6
7
8
if(promoId!=null){
//以活动价格入库
orderModel.setItemPrice(itemModel.getPromoModel().getPromoItemPrice());
}else{
//以非活动价格入库
orderModel.setItemPrice(itemModel.getPrice());
}
orderModel.setPromoId(promoId);
Author: 高明
Link: https://skysea-gaoming.github.io/2021/04/15/%E9%AB%98%E5%B9%B6%E5%8F%91%E9%A1%B9%E7%9B%AE%E5%AD%A6%E4%B9%A0/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.