需求:

用户通过报到、活动、下单等途径获取积分,积分30天后过期(可装备)。用户下单能够运用积分抵扣金额,优先运用快过期的积分,但当订单取消后,积分也要原路退回。

需求剖析:

积分过期:需求存储每一次用户获取积分的项目,比方报到获取10积分,过期时刻是2023-04-30 12:00:00。这样每个积分项都有不同的过期时刻记载,来做到对不一起刻获取积分的过期操作。

积分运用:先运用最快要过期的积分,那么就需求积分项按照过期时刻排序再消费。例如用户报到10天获取到100积分,归于10个积分项,用户下单运用40积分,那么需求消费最早过期的4个积分项中的40积分。

积分交还:用户下单运用100积分,是用户报到10天获取到的,归于10个积分项,拥有不同的过期时刻。交还时要把这100个积分拆成10个交还到不同的过期时刻的积分项中。需求存储每次运用积分的记载id和积分项id,做到积分的交还。

表设计:

create table member
(
    id           varchar(36)   not null comment 'id',
    user_id      varchar(36)   not null comment '用户id',
    total_point  int           not null comment '总积分',
    avail_point  int           not null comment '可用积分',
    expire_point int           not null comment '过期积分'
    constraint id
        unique (id),
    constraint unq_user_id
        unique (user_id)
)
    comment '会员表';
create table member_point_item
(
    id           varchar(36)   not null comment 'id'
        primary key,
    user_id      varchar(36)   not null comment '用户id',
    point        int           not null comment '积分值',
    expire_point int default 0 not null comment '过期积分',
    source_id    varchar(36)   not null comment '来源id 例如活动id',
    status       smallint      not null comment '状况 1:正常 2:用光 3:过期'
)
    comment '会员积分项';
create table member_point_record
(
    id          varchar(36)   not null comment 'id'
        primary key,
    user_id     varchar(36)   not null comment '用户id',
    source_id   varchar(36)   null comment '来源id(例如订单id)',
    point       int           not null comment '积分值',
    type        smallint      not null comment '类型 1:报到  2:订单抵扣 3:过期'
)
    comment '会员积分记载';
create table member_point_record_relation
(
    id        varchar(36) not null comment 'id'
        primary key,
    record_id varchar(36) not null comment '记载id',
    point_id  varchar(36) not null comment '用户积分项id',
    use_point int         not null comment '运用积分数',
    status        smallint    not null comment '状况 1:抵扣  2:返还'
)
    comment '会员积分项与记载关系表';

member:存储用户的总积分,可用积分,过期积分。

member_point_item:存储用户积分项,记载不同的过期时刻、

member_point_record:存储用户取得,运用,过期积分记载。

member_point_record_relation:存储记载运用到的积分项关系,用来完成用户积分原路退回。

问题:

因为用户获取积分时刻不同,用户积分项或许数据较多,且积分数较小。例如100条获取1积分的积分项记载,标识用户拥有100积分,这100积分拥有不同的过期时刻。运用时需求扣除这100条记载的积分,需求修正100条记载。

处理计划:

  • 产品层面:跟产品battle,让他改成按小时过期,按日过期、按月过期,增大积分项的过期粒度,一起操控积分过期时刻。减少运用积分时需求操作的积分项记载数。(或许battle不过)
  • 异步做:先扣减用户积分额度,异步做积分项的积分运用。

下面简略放一下取得积分、扣减积分、交还积分的代码

取得积分:

/**
* 用户获取积分
*
* @param req 积分获取类
*/
@Override
@Transactional(rollbackFor = Exception.class)
public void create(MemberPointCreateDTO req) {
    //获取到的积分
    int point = req.getPoint();
    //用户id
    String userId = UserUtil.handleUserId();
    //生成积分记载,简略的存储member_point_record表操作
    memberPointRecordManager.create(userId, point, req.getType(), req.getSourceId());
    //存储用户积分项,简略的存储member_point_item表操作
    create(userId, point);
    //更新member用户积分
    memberService.addUserPoint(userId, point);
}
//更新member用户积分
public void addUserPoint(String userId, int point) {
    LambdaUpdateWrapper<Member> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.eq(Member::getUserId, userId)
        .setSql("total_point = total_point + " + point)
        .setSql("avail_point = avail_point + " + point);
    this.update(updateWrapper);
}

扣减积分

 /**
 * 消费积分
 *
 * @param req 积分消费类
 */
@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(name = "member:point", key = "#req.userId")
public void consume(MemberPointConsumeDTO req) {
    //获取用户积分判别积分是否满足消费
    Member member = memberService.getByUserId(req.getUserId());
    //用户积分不足
    if (member.getAvailPoint() < req.getPoint()) {
        throw MemberException.buildServiceException(MemberExceptionResponseCode.POINT_NOT_ENOUGH);
    }
    //要花费的总积分
    Integer totalPoint = req.getPoint();
    //扣减用户积分
    memberService.consumeUserPoint(req.getUserId(), req.getPoint());
    //生成积分记载
    MemberPointRecord record = memberPointRecordManager.create(req.getUserId(), req.getPoint(), req.getType(), req.getSourceId());
    //扣减用户积分项
    handleUserPointItemDeduct(req.getPoint(), req.getUserId(), record.getId());
}
/**
 * 扣减用户积分
 * @param userId 用户id
 * @param point 积分
 */
@Override
public boolean consumeUserPoint(String userId, int point) {
    LambdaUpdateWrapper<Member> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.eq(Member::getUserId, userId)
        .setSql("avail_point = avail_point - " + point)
        .ge(Member::getAvailPoint, point);
    return this.update(updateWrapper);
}
/**
 * 扣减用户积分项
 *
 * @param totalPoint 要花费的总积分
 * @param userId     用户id
 * @param recordId   记载id
 */
private void handleUserPointItemDeduct(Integer totalPoint, String userId, String recordId) {
    //查询用户积分项按过期时刻排序
    List<MemberPointItem> memberPointItems = memberPointItemService.getByUserIdOrderAscByExpireTime(userId);
    //遍历用户积分项,扣减积分
    List<MemberPointRecordRelation> relations = new ArrayList<>();
    List<MemberPointItem> updatePoints = new ArrayList<>();
    for (MemberPointItem item : memberPointItems) {
        if (totalPoint <= 0) {
            break;
        }
        //当时积分项需求运用的积分
        int usePoint;
        //假如需求运用的积分大于当时积分项
        if (totalPoint >= item.getPoint()) {
            totalPoint -= item.getPoint();
            //运用到的积分为积分项的积分数
            usePoint = item.getPoint();
            item.setPoint(0);
            //设置积分项状况为用光
            item.setStatus(MemberPointItemStatusEnum.EXHAUSTED.getValue());
        } else {
            //剩下积分
            int remainPoint = item.getPoint() - totalPoint;
            //运用到的积分为剩下的花费积分数
            usePoint = totalPoint;
            //设置剩下积分
            item.setPoint(remainPoint);
            totalPoint = 0;
        }
        //创建相关记载实体类,为了完成积分交还功用,简略的member_point_record_relation表的入库操作
        MemberPointRecordRelation relation = handleCreatePointRecordRelationDO(item.getId(), recordId, usePoint);
        relations.add(relation);
        updatePoints.add(item);
    }
    //存储
    memberPointItemService.updateBatchById(updatePoints, 100);
    memberPointRecordRelationService.saveBatch(relations, 100);
}
/**
 * 查询用户积分项按过期时刻排序
 * @param userId 用户id
 */
public List<MemberPointItem> getByUserIdOrderAscByExpireTime(String userId) {
    LambdaQueryWrapper<MemberPointItem> queryWrapper = new LambdaQueryWrapper<>();
    queryWrapper.eq(MemberPointItem::getUserId, userId)
        //查询状况是能够运用的积分项
        .eq(MemberPointItem::getStatus, MemberPointItemStatusEnum.NORMAL.getValue());
    queryWrapper.orderByAsc(MemberPointItem::getExpireTime);
    return memberPointItemMapper.selectList(queryWrapper);
}

交还积分

/**
 * 依据订单号交还积分
 *
 * @param orderId 订单号
 */
@Override
@Transactional(rollbackFor = Exception.class)
@RedissonLock(name = "member:point", key = "#userId")
public void refund(String userId, String orderId) {
    //依据订单id查询记载
    MemberPointRecord pointRecord = memberPointRecordManager.getByOrderId(orderId);
    if (Objects.isNull(pointRecord)) {
        throw MemberException.buildServiceException(MemberExceptionResponseCode.ORDER_NOT_POINT_DEDUCT);
    }
    //生成交还记载
    memberPointRecordManager.create(pointRecord.getUserId(), pointRecord.getPoint(), MemberPointTypeEnums.REFUND.getValue(), orderId);
    //依据积分记载交还积分
    refundPoint(pointRecord);
}
/**
 * 依据积分记载交还积分
 *
 * @param pointRecord 积分记载
 */
public void refundPoint(MemberPointRecord pointRecord) {
    //依据记载id查询该记载都花费了哪些积分,并退回
    List<MemberPointRecordRelation> relations = memberPointRecordRelationService.getByRecordId(pointRecord.getId());
    Map<String, MemberPointRecordRelation> relationMap = relations.stream().collect(Collectors.toMap(MemberPointRecordRelation::getPointItemId, Function.identity()));
    Set<String> pointIds = relations.stream().map(MemberPointRecordRelation::getPointItemId).collect(Collectors.toSet());
    //查询运用过的积分项
    List<MemberPointItem> memberPointItems = memberPointItemService.listByIds(pointIds);
    for (MemberPointItem item : memberPointItems) {
        MemberPointRecordRelation relation = relationMap.get(item.getId());
        //更新关系表状况为返还
        relation.setStatus(MemberPointRecordRelationTypeEnum.REFUND.getValue());
        //积分项积分加回运用掉的积分
        item.setPoint(item.getPoint() + relation.getUsePoint());
        //假如状况是不是正常,那么更新状况为正常
        if (!Objects.equals(item.getStatus(), MemberPointItemStatusEnum.NORMAL.getValue())) {
            item.setStatus(MemberPointItemStatusEnum.NORMAL.getValue());
        }
        //过期积分处理,状况设置为已过期,生成积分过期记载
        ...
    }
    memberPointItemService.updateBatchById(memberPointItems, 100);
    memberPointRecordRelationService.updateBatchById(relations, 100);
    //加回用户积分
    memberService.refundUserPoint(pointRecord.getUserId(), pointRecord.getPoint());
}
@Override
public void refundUserPoint(String userId, int point) {
    LambdaUpdateWrapper<Member> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.eq(Member::getUserId, userId)
            .setSql("avail_point = avail_point + " + point);
    this.update(updateWrapper);
}

在功用完成时,会有并发问题,或许会形成少消费,多交还等问题,这时候就需求加锁来处理了。

@RedissonLock是自定义的一个分布式锁切面,网上的完成也比较多。在运用时需求留意与@Transactional之间的顺序问题。

假如分布式锁比业务先一步释放,那么另一线程得到分布式锁进入办法,因为上一线程的业务还未提交,得到的数据仍是未提交时的数据,读取的数据并不是最新的。分布式锁的运用也就出现了问题。

@Transactional的Order为Ordered.LOWEST_PRECEDENCE,即最低优先级,只要保证@Order的值比它小即可。

相同也要考虑一下锁的租期问题,假如在锁的租期内办法没有完成,第二个线程相同也会获取锁,进入办法。在上锁的时刻上也要进行考虑。运用Redisson的话也能够不传入过期时刻,Redisson有WatchDog机制,会触发自动续期。

至此一个积分功用就大体完成了。除了上述说到的功用还有积分项的过期任务,积分获取规矩、过期规矩、抵扣规矩装备等需求完成。假如有其他计划,或者文中完成有问题,欢迎评论和指出。