商城拍卖活动设计方案 瞬时并发高可用

后端源码传送门:https://download.csdn.net/download/sun5769675/87323033

最近参加了公司内部的一个24小时编程比赛,组了个四人的小团队,设计了一个拍卖的功能,功能需求如下:

1. 登陆

2. 支付保证金

3. 商品信息展示阶段

4. 拍卖阶段叫价(最后一秒有人出价会延时5分钟)

5. 尾款支付

针对功能需求我们计划创建三个项目,一个后台拍卖活动数据维护,一个小程序做C端,一个api项目对前面两个项目做接口层的支持,业务流程走向如下:

接口层部分做了如下8个接口,当然还有8个数据维护的接口这里就不列举了,就是针对表数据的一个增删改查,后面看DB设计即可知晓,流程没有画的特别详细但是主要流程都写了

下面看下DB设计一共5张表

下面看下我们后端维护数据的页面设计:

这边设计的后台数据维护功能是一个拍卖活动下,可以配置多个单位级的拍卖商品,是一对多的关系。

然后是看下我们的小程序页面其实很简单的,就登陆成功后到列表页开始倒计时,然后点击详情,倒计时结束,进入拍卖中的倒计时,需要交保证金,交完保证金,去竞价即可,活动时间结束,谁拍到了就去付尾款。

竞拍部分代码预览:
 

/**
	 * 
	 * 竞拍,单次加价竞拍或一口价竞拍<br>
	 * 1、校验单次加价,要大于默认加价<br>
	 * 2、currentPrice与redis中价格比较<br>
	 * 3、判断锁是否存在,存在返回,不存在加锁<br>
	 * 4、加锁失败返回<br>
	 * 5、currentPrice与redis中价格比较,小于解锁后返回<br>
	 * 6、执行拍卖<br>
	 * 7、有异常解锁<br>
	 * 
	 * @param auctionRequestParamCommand 
	 * @return 
	 */
@Override
	public ResultCommand singleOrOnceAuctionProcess(AuctionRequestParamCommand auctionRequestParamCommand) {
		String code = auctionRequestParamCommand.getCode();
		Integer memberId = auctionRequestParamCommand.getMemberId();
		Integer skuId =  auctionRequestParamCommand.getSkuId();
		BigDecimal addPrice = auctionRequestParamCommand.getAddPrice();
		BigDecimal currentPrice = auctionRequestParamCommand.getCurrentPrice();
		
        //查询定金记录
		AuctionRequestParamCommand depositQuery = new AuctionRequestParamCommand();
		depositQuery.setCode(code);
		depositQuery.setMemberId(memberId);
		depositQuery.setSkuId(skuId);
		ResultCommand depositLog = auctionActivityDepositManager.findMemberDeposit(depositQuery);

        if (null == depositLog || depositLog.getData() ==null) {
        	LOGGER.info("该用户没有交拍卖定金:memberId:{},活动code{},拍卖商品ID{}", memberId, code, skuId);
        	return new ResultCommand(ErrorCode.ERROR_CODE_FIND_DEPOSIT_ERROR);
        }

		Date now = new Date();
		
        // 当前拍卖商品command
		StoAuctionProductCommand auctionProductCommand = null;
        // 当前拍卖价格
		String redisPrice = null;
        // 参数校验
		if(StringUtils.isEmpty(code) || memberId == null || skuId == null || currentPrice == null) {
            LOGGER.info("竞拍参数校验为空:{}", auctionRequestParamCommand.toString());
			return new ResultCommand(ErrorCode.ERROR_CODE_PARAM_EMPTY);
		}

        // 获取活动信息
		ResultCommand resultCommand = auctionActivityDetail(code);
		if (resultCommand == null
				|| !ErrorCode.ERROR_CODE_OK.getCode().equals(resultCommand.getCode())
				|| null == resultCommand.getData()) {
            LOGGER.info("竞拍获取活动信息为空!参数:{}", auctionRequestParamCommand.toString());
			return new ResultCommand(ErrorCode.ERROR_CODE_DATA_EMPTY);
		}
		AuctionActivityCommand auctionActivityCommand = (AuctionActivityCommand) resultCommand.getData();
		
        // 判断有效期
		// 判断竞拍商品的有效时间
		List<StoAuctionProductCommand> auctionProducts = auctionActivityCommand.getAuctionProducts();
		for (StoAuctionProductCommand stoAuctionProductCommand : auctionProducts) {
			if(!skuId.equals(stoAuctionProductCommand.getId())) {
				continue;
			}
			
            // 商品的有效期是否在有效范围内
            if (stoAuctionProductCommand.getStartTime() != null && stoAuctionProductCommand.getEndTime() != null
                    && now.after(stoAuctionProductCommand.getStartTime()) && now.before(stoAuctionProductCommand.getEndTime())) {
                // 校验通过
                auctionProductCommand = stoAuctionProductCommand;
                break;
            }
            LOGGER.info("竞拍活动商品不在有效时间范围内!参数:{}", auctionRequestParamCommand.toString());
            return new ResultCommand(ErrorCode.ERROR_CODE_OVER_EXPIRE);
		}
		
        // 当前竞拍价格缓存key:plugin_auction_活动编码_sku编码_拍卖商品表id
		String redisPriceKey = CacheConstants.PLUGIN_AUCTION_ACCOUNT_PRICE_KEY
				.concat(code)
				.concat(JOIN_SYMBOL)
				.concat(auctionProductCommand.getSkuCode()
				.concat(JOIN_SYMBOL)
				.concat(String.valueOf(auctionProductCommand.getId())));
			
        // 单次叫价拍卖用户加的价不能比默认的小
			if(addPrice != null && addPrice.compareTo(auctionProductCommand.getSinglePrice()) == -1) {
				return new ResultCommand(ErrorCode.ERROR_CODE_ADDPIRCE_LESS);
			}
			if(addPrice == null){
                // 自定义加价为空,使用默认单次加价
				addPrice = auctionProductCommand.getSinglePrice();
			}
		
        // 这个try/catch块中的内容是 在加锁成功之前的一些校验,这个通过说明加锁成功了
		try {
			redisPrice = redisUtils.getString(redisPriceKey);

            /**
            * 当前竞拍价格的设置
            * @see CacheConstant.PLUGIN_AUCTION_ACCOUNT_PRICE_KEY
            */
			if(StringUtils.isEmpty(redisPrice)) {
				redisUtils.setString(redisPriceKey,String.valueOf(auctionProductCommand.getBasePrice()));
				redisPrice = String.valueOf(auctionProductCommand.getBasePrice());
			}
		
            // 比较用户看到的竞拍价与缓存中的价格
			if(currentPrice.compareTo(new BigDecimal(redisPrice)) == -1) {
                // 价格比缓存中的小
				return new ResultCommand(ErrorCode.ERROR_CODE_CURRENTPRICE_EXPIRED);
			}
		
            // 为当前竞拍设置锁if not exits,锁期间其他进入的竞拍会被拦截
			// 锁设置5秒
			Long setNXResult = redisUtils.setnx(
					CacheConstants.LOCK_FOR_CURRENT_PRICE_KEY,
					CacheConstants.LOCK_FOR_CURRENT_PRICE_VALUE,5,TimeUnit.SECONDS);

			if(!Status.REDIS_SETNX_SUCCESS.getCode().equals(Integer.valueOf(String.valueOf(setNXResult)))) {
                // 设置锁失败
                LOGGER.info("竞拍设置锁失败!参数:{}", auctionRequestParamCommand.toString());
				return new ResultCommand(ErrorCode.ERROR_CODE_CURRENTPRICE_EXPIRED);
			}
		} catch (Exception e) {
            LOGGER.error("在加锁成功之前的校验出错", e);
			return new ResultCommand(ErrorCode.ERROR_CODE_SYSTEM_ERROR);
		}
		
		StoAuctionRecord auctionRecord = new StoAuctionRecord();
		try {
            // 锁设置成功
			redisPrice = redisUtils.getString(redisPriceKey);
            // 再次比较用户看到的竞拍价与缓存中的价格
			if(currentPrice.compareTo(new BigDecimal(redisPrice)) == -1) {
                // 价格比缓存中的小,解锁
				redisUtils.deleteKey(CacheConstants.LOCK_FOR_CURRENT_PRICE_KEY);
				return new ResultCommand(ErrorCode.ERROR_CODE_CURRENTPRICE_EXPIRED);
			}
			
            // 这个是会员竞拍到的价格
			BigDecimal auctionPrice = null;
            // 下面开始允许会员竞拍 1、保存竞拍记录;2、刷缓存
                // 单次叫价的竞拍价为 当前竞拍价加上addPrice
				auctionPrice = new BigDecimal(redisPrice).add(addPrice);

			
			auctionRecord.setAuctionActivityId(auctionActivityCommand.getId());
			auctionRecord.setAuctionPrice(auctionPrice);
			auctionRecord.setAuctionProductId(skuId);
			auctionRecord.setCreateBy(String.valueOf(memberId));
			auctionRecord.setCreateTime(new Date());
			auctionRecord.setLifecycle(Status.ACTIVE.getCode());
			auctionRecord.setMemberId(memberId);
			auctionRecord.setModifyBy(String.valueOf(memberId));
			auctionRecord.setModifyTime(new Date());
			
            // 保存拍卖纪录
			auctionRecord.setId(baseMysqlCRUDManager.save(auctionRecord));
			
            // 刷新当前竞拍价格缓存
			redisUtils.setString(redisPriceKey, String.valueOf(auctionPrice));

			// 拍卖顺延
			auctionExtendDate(code,skuId);


            // 竞拍成功解锁
			redisUtils.deleteKey(CacheConstants.LOCK_FOR_CURRENT_PRICE_KEY);
			
			return new ResultCommand(ErrorCode.ERROR_CODE_OK, auctionRecord);
		} catch (Exception e) {
            // 这里抓取到的异常要手动回滚、执行解锁操作
			if(auctionRecord != null && auctionRecord.getId() != null) {
                LOGGER.error("锁期间竞拍异常!回滚拍卖纪录");
                if(auctionRecord.getId() != null){
					baseMysqlCRUDManager.delete(auctionRecord);
				}
			}
			String newRedisPrice = redisUtils.getString(redisPriceKey);
			if(!redisPrice.equals(newRedisPrice)) {
                LOGGER.error("锁期间竞拍异常!商品当前拍卖价格缓存回滚");
				redisUtils.setString(redisPriceKey, redisPrice);
			}
            LOGGER.error("锁期间竞拍异常!解锁");
			redisUtils.deleteKey(CacheConstants.LOCK_FOR_CURRENT_PRICE_KEY);
			return new ResultCommand(ErrorCode.ERROR_CODE_SYSTEM_ERROR);
		}
	}

  • 4
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浮生(FS)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值