Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

投资满兑活动技术方案设计 #23

Open
TFdream opened this issue Oct 16, 2018 · 0 comments
Open

投资满兑活动技术方案设计 #23

TFdream opened this issue Oct 16, 2018 · 0 comments

Comments

@TFdream
Copy link
Owner

TFdream commented Oct 16, 2018

场景

活动期间,用户投资指定理财产品计划每满XX元(例如每满1000元),兑换积分加1,用户可以使用兑换积分去兑换礼品。

1.投资得兑换积分

注:需要把不足1000的投资记录下来,比如两次分别投资1500元,第一次得1个兑换积分,第二次得2个兑换积分,一共3个兑换积分。

2.兑换积分换礼品

实物、虚拟卡、红包 多种类型的礼品,全都平铺在页面上。实物和虚拟卡礼品有总数限制
已兑完的礼品,按钮为“抢光啦”,不可点击。

设计思路

计算用户兑换积分

用户兑换礼品

兑换礼品主逻辑如下:

/**
 * @author Ricky Fung
 */
@Service
public class GiftExchangeService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Resource(name = "giftService")
    private GiftService giftService;

    @Resource(name = "userService")
    private UserService userService;

    @Resource(name = "userPointService")
    private UserPointService userPointService;

    /**
     * 提交兑换礼品请求
     * @param activityId 活动id
     * @param userId 用户id
     * @param giftId 活动礼品id
     * @param quantity 兑换数量
     * @return
     */
    public ApiResult<Boolean> submit(Integer activityId, Long userId, Long giftId, int quantity) {
        logger.info("活动-兑换礼品, 用户userId:{}, activityId:{}, giftId:{}, quantity:{} 请求处理开始",
                userId, activityId, giftId, quantity);
        //1.校验参数
        if (giftId==null || giftId.longValue() <1) {
            return ApiResult.buildInvalidParameterResult("giftId必须大于0");
        }
        if (quantity < 1) {
            return ApiResult.buildInvalidParameterResult("quantity必须大于0");
        }

        //2.查询用户兑换的礼品信息
        ActivityGift activityGift = giftService.getGiftById(activityId, giftId);
        if (activityGift==null) {
            return ApiResult.buildFailureResult(1001, "礼品id不存在");
        }

        //3.用户信息
        User user = userService.getUserById(userId);
        if (user==null) {
            return ApiResult.buildFailureResult(1002, "用户不存在");
        }

        //4.1 校验库存数量
        int count = giftService.getGiftStock(giftId);
        if (count<1) {
            return ApiResult.buildFailureResult(1003, "礼品库存不足");
        }

        //加分布式锁
        String lockKey = String.format("%s:%s", GiftConstant.GIFT_EXCHANGE_USER_LOCK_KEY, userId);
        DistributedLock lock = distributedLockClient.getLock(lockKey);
        try {
            boolean success = lock.tryLock(0, 30, TimeUnit.SECONDS);
            if (!success) {
                logger.info("活动-兑换礼品, 用户userId:{} activityId:{}, giftId:{} 用户加锁失败",
                        userId, activityId, giftId);
                return ApiResult.buildFailureResult(1004, "请勿重复操作");
            }
            //加锁成功
            try {
                //4.2 可用积分校验
                UserPoint userPoint = userPointService.getUserPoint(userId, activityId);
                if (userPoint==null) {
                    return ApiResult.buildFailureResult(1005, "对不起,您的兑换卡数量不足");
                }
                int requiredPoint = activityGift.getPointValue() * quantity;
                if (userPoint.getAvailablePoint().intValue() < requiredPoint) {
                    logger.info("活动-兑换礼品, 用户userId:{} activityId:{} 当前可用积分:{} 不满足兑换要求:{}",
                            userId, activityId, userPoint.getAvailablePoint(), requiredPoint);
                    return ApiResult.buildFailureResult(1005, "对不起,您的可用积分不足");
                }

                //5.1 减库存
                boolean updateSuccess = giftService.decreaseGiftStock(giftId, quantity);
                if (!updateSuccess) {
                    logger.info("活动-兑换礼品, 用户userId:{} activityId:{}, giftId:{} 更新礼品库存失败",
                            userId, activityId, giftId);
                    return ApiResult.buildFailureResult(1003, "礼品库存不足");
                }
                //5.2 创建用户活动礼品记录
                ActivityUserGift record = giftService.createOrder(activityId, userId, activityGift, quantity);
                //5.3 减用户可用积分并保存兑换记录
                ApiResult<Long> result = giftService.saveUserGift(userPoint, requiredPoint, record);
                if (result.isSuccess()) {
                    Long id = result.getData();
                    logger.info("活动-兑换礼品, 用户userId:{} activityId:{}, giftId:{}, quantity:{} 兑换成功id:{}",
                            userId, activityId, giftId, quantity, id);

                    //发放活动奖品
                    sendActivityGift(userId, activityId, id);
                    return ApiResult.buildSuccessResult(Boolean.TRUE);
                }
            } finally {
                //释放锁
                lock.unlock();
            }
        } catch (InterruptedException e) {
            logger.error(String.format("活动-兑换礼品, 用户userId:%s, activityId:%s, giftId:%s, quantity:%s 加锁异常",
                    userId, activityId, giftId, quantity));
        } catch (UserPointShortageException e) {
            logger.error("活动-兑换礼品, 用户userId:{} activityId:{}, giftId:{}, quantity:{} 兑换失败-更新用户可用积分失败",
                    userId, activityId, giftId, quantity);
            return ApiResult.buildFailureResult(1005, "对不起,您的兑换卡数量不足");
        } catch (Exception e) {
            logger.error(String.format("活动-兑换礼品, 用户userId:%s activityId:%s, giftId:%s, quantity:%s 兑换异常",
                    userId, activityId, giftId, quantity), e);
        }
        return ApiResult.buildSystemErrorResult();
    }

    private void sendActivityGift(Long userId, Integer activityId, Long id) {
        //发送 发放活动礼品mq消息

    }
}

GiftService 库存相关代码如下:

/**
 * @author Ricky Fung
 */
@Service
public class GiftService {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private final DefaultRedisScript<Long> stockScript;

    public GiftService() {
        this.stockScript = new DefaultRedisScript<>();
        stockScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("scripts/stock_decr.lua")));
        stockScript.setResultType(Long.class);
    }

    /**
     * 获取活动礼品库存数量
     * @param giftId
     * @return
     */
    public int getGiftStock(Long giftId) {
        String key = getActivityAwardStockKey(giftId);
        String str = stringRedisTemplate.opsForValue().get(key);
        int count = 0;
        if (StringUtils.isNotEmpty(str)) {
            count = Integer.parseInt(str);
            if (count < 0) {
                count = 0;
            }
        }
        return count;
    }

    /**
     * 扣减活动礼品库存
     * @param giftId
     * @param quantity
     * @return
     */
    public boolean decreaseGiftStock(Long giftId, int quantity) {
        String key = getActivityAwardStockKey(giftId);
        //LUA脚本保证原子性
        Long update = stringRedisTemplate.execute(stockScript, Collections.singletonList(key), String.valueOf(quantity));
        logger.info("活动-兑换礼品, giftId:{} 减库存quantity:{} 后的结果:{}",
                giftId, quantity, update);
        return update >= 0;
    }

    private String getActivityAwardStockKey(Long giftId) {
        return String.format("%s:%s", "stock", giftId);
    }

}

GiftService 扣减用户可用积分代码如下:

    @Transactional(rollbackFor = Exception.class)
    public ApiResult<Long> saveUserGift(UserPoint userPoint, int requiredPoint, ActivityUserGift record) {
        Long id = userPoint.getId();
        //1.扣减用户可用积分
        int update = userPointMapper.updateUserAvailablePoint(id, requiredPoint);
        if (update<1) {
            throw new UserPointShortageException("可用积分不足");
        }
        //2.插入兑换礼品记录
        activityUserGiftMapper.insert(record);
        return ApiResult.buildSuccessResult(record.getId().longValue());
    }

    public ActivityGift getGiftById(Integer activityId, Long giftId) {
        return activityGiftMapper.findGiftById(giftId);
    }

    public ActivityUserGift createOrder(Integer activityId, Long userId, ActivityGift activityGift, int quantity) {
        ActivityUserGift userGift = new ActivityUserGift();
        //赋值
        return userGift;
    }

其中,扣减用户可用积分SQL如下:

  <update id="updateUserAvailablePoint">
    update activity_user_point
    set available_point = available_point - #{requiredPoint}
	where id=#{id} and available_point >=#{requiredPoint};
  </update>

activity_user_point表DDL如下:

CREATE TABLE `activity_user_point` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',
`activity_id` bigint(20) NOT NULL COMMENT '活动id',
`user_id` bigint(20) unsigned NOT NULL COMMENT '用户id',
`name` varchar(45) NOT NULL COMMENT '用户真实姓名',
`total_point` bigint(20) NOT NULL DEFAULT '0' COMMENT '用户获得总积分数',
`available_point` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '用户可用的积分数',
`version` int(10) NOT NULL DEFAULT '1' COMMENT '版本号',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_user_id` (`user_id`,`activity_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='动用户积分表';

其中,available_point为 bigint(20) unsigned 可以在数据库层面保证 available_point小于0情况发生。

附录 - Lua脚本

1、investment_point.lua

local investment_key = KEYS[1]
local point_key = KEYS[2]

local investmentAmount = tonumber(ARGV[1])
local pointUnit = tonumber(ARGV[2])

local totalInvestmentAmount = tonumber(redis.call("INCRBY", investment_key, investmentAmount))
local totalPoints = math.floor(totalInvestmentAmount / pointUnit);

redis.call("SET", point_key, totalPoints)

return {totalInvestmentAmount, totalPoints}

  1. stock_decr.lua
local stocks_key = KEYS[1]
local acquiredNum = tonumber(ARGV[1])

local current = tonumber(redis.call("GET", stocks_key))
if current == nil then
  current = 0
end
local success = -1;
if current >= acquiredNum then
    success = tonumber(redis.call("DECRBY", stocks_key, acquiredNum))
end

return success
@TFdream TFdream changed the title 投资满返需求技术方案设计 投资满兑活动技术方案设计 Oct 16, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant