需求:
用户通过报到、活动、下单等途径获取积分,积分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机制,会触发自动续期。
至此一个积分功用就大体完成了。除了上述说到的功用还有积分项的过期任务,积分获取规矩、过期规矩、抵扣规矩装备等需求完成。假如有其他计划,或者文中完成有问题,欢迎评论和指出。