家族-定时任务-钻石结算

This commit is contained in:
khalil
2024-10-11 20:57:03 +08:00
parent f4b3443e2e
commit c953edaf9b
12 changed files with 306 additions and 1 deletions

View File

@@ -1356,6 +1356,8 @@ public enum RedisKey {
lucky_24_user_history,
lucky_24_user_lock,
lucky_24_robot_push_msg,
family_diamond_settlement,
;
public String getKey() {

View File

@@ -189,6 +189,8 @@ public enum BillObjTypeEnum {
SS_GUILD_MONTH_MEMBER_DIAMOND((byte) 125, "SS公会活动奖励", BillTypeEnum.IN, CurrencyEnum.GOLD),
CANCEL_CP((byte) 126, "解除cp", BillTypeEnum.OUT, CurrencyEnum.DIAMOND),
FAMILY_DIAMOND_SETTLEMENT((byte) 127, "家族钻石结算", BillTypeEnum.OUT, CurrencyEnum.GOLD),
;
BillObjTypeEnum(byte value, String desc, BillTypeEnum type, CurrencyEnum currency) {

View File

@@ -0,0 +1,21 @@
package com.accompany.business.model.family;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.util.Date;
@Data
public class FamilyMemberDiamondSettlementRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long familyMemberId;
private Integer familyId;
private Byte roleType;
private Long uid;
private Double diamondNum;
private Date createTime;
}

View File

@@ -0,0 +1,7 @@
package com.accompany.business.mybatismapper.family;
import com.accompany.business.model.family.FamilyMemberDiamondSettlementRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface FamilyMemberDiamondSettlementRecordMapper extends BaseMapper<FamilyMemberDiamondSettlementRecord> {
}

View File

@@ -19,4 +19,5 @@ public interface FamilyMemberMapper extends BaseMapper<FamilyMember> {
FamilyMember selectFamilyMemberByTime(@Param("familyId") Integer familyId, @Param("uid") Long uid, @Param("startTime") Date startTime, @Param("endTime") Date endTime);
List<FamilyMember> listValidFamilyMemberByPartitionId(@Param("familyId") Integer partitionId);
}

View File

@@ -0,0 +1,143 @@
package com.accompany.business.service.clan;
import com.accompany.business.model.UserPurse;
import com.accompany.business.model.family.FamilyMember;
import com.accompany.business.model.family.FamilyMemberDiamondSettlementRecord;
import com.accompany.business.mybatismapper.UserPurseMapper;
import com.accompany.business.mybatismapper.family.FamilyMemberDiamondSettlementRecordMapper;
import com.accompany.business.service.family.FamilyMemberService;
import com.accompany.business.service.purse.FamilyDiamondSettlementPurseService;
import com.accompany.business.service.purse.UserPurseService;
import com.accompany.business.service.record.BillRecordService;
import com.accompany.common.utils.DateTimeUtil;
import com.accompany.core.enumeration.BillObjTypeEnum;
import com.accompany.core.enumeration.PartitionEnum;
import lombok.extern.slf4j.Slf4j;
import org.apache.ibatis.session.ExecutorType;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.redisson.api.RLocalCachedMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
@Slf4j
@Service
public class FamilyDiamondSettlementService {
@Autowired
private UserPurseService userPurseService;
@Autowired
private SqlSessionFactory sqlSessionFactory;
@Autowired
private FamilyMemberService familyMemberService;
@Autowired
private FamilyDiamondSettlementPurseService familyDiamondSettlementPurseService;
@Autowired
private BillRecordService billRecordService;
@Resource(name = "bizExecutor")
private ThreadPoolExecutor bizExecutor;
public void settlement(Integer waitSecond) throws InterruptedException {
//当天零点再前推1秒相当于昨天23:59:59 给客户端周查询
Date now = DateTimeUtil.addSeconds(DateTimeUtil.getBeginTimeOfDay(new Date()),-1);
List<FamilyMember> familyMemberList = familyMemberService.listValidFamilyMemberByPartitionId(PartitionEnum.ENGLISH.getId());
if (CollectionUtils.isEmpty(familyMemberList)){
return;
}
RLocalCachedMap<Long, Double> settlementDiamondMap = familyDiamondSettlementPurseService.getFamilyDiamondSettlementMap();
// uid: golds
List<Long> uidList = familyMemberList.stream().map(FamilyMember::getUid).distinct().collect(Collectors.toList());
Map<Long, Double> memberGoldsMap = userPurseService.getBaseMapper().selectGoldsByUids(uidList)
.stream().collect(Collectors.toMap(UserPurse::getUid, UserPurse::getGolds));
//先把他们设置为结算状态
settlementDiamondMap.putAll(memberGoldsMap);
log.info("[家族钻石结算] 设置结算状态 uids: {}", uidList.stream().map(Object::toString).collect(Collectors.joining(", ")));
log.info("[家族钻石结算] 获得当前公会成员(除会长)的金币快照 {}", memberGoldsMap);
//冷却3秒等已经抢到lock准备subGolds都差不多执行完再结算
Thread.sleep(3 * 1000);
//因为没有获取每个成员的金币锁间接用double check方法但不能在极端情况下保持最终一致性例如先减少后加
//再查一次,获取冷却后的最新值,取两者最小值,确保在不扣结算等待期间的所加的金币
Map<Long, Double> memberGoldsMapSecond = userPurseService.getBaseMapper().selectGoldsByUids(uidList)
.stream().collect(Collectors.toMap(UserPurse::getUid, UserPurse::getGolds));
log.info("[家族金币结算] 获得当前公会成员冷却后的钻石快照 {}", memberGoldsMap);
for (Map.Entry<Long, Double> entry: memberGoldsMap.entrySet()){
Double newGolds = memberGoldsMapSecond.get(entry.getKey());
if (null != newGolds && Double.compare(entry.getValue(), newGolds) > 0){
entry.setValue(newGolds);
}
}
log.info("[家族金币结算] 获得当前公会成员的钻石快照(冷却前后取最小值) {}", memberGoldsMap);
//留给测试去结算状态下测试
if (null == waitSecond || waitSecond < 3){
waitSecond = 0;
}
if (waitSecond > 0){
Thread.sleep(waitSecond * 1000);
}
int batchSize = 100;
SqlSession batchSqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH);
UserPurseMapper userPurseMapper = batchSqlSession.getMapper(UserPurseMapper.class);
FamilyMemberDiamondSettlementRecordMapper familyMemberDiamondSettlementRecordMapper = batchSqlSession.getMapper(FamilyMemberDiamondSettlementRecordMapper.class);
try {
int memberIndex = 0;
for (FamilyMember familyMember: familyMemberList){
Long uid = familyMember.getUid();
Double golds = memberGoldsMap.get(uid);
if (Double.compare(golds, 0d) > 0){
//扣金币
userPurseMapper.updateSettlementGolds(uid, golds);
}
//结算记录
FamilyMemberDiamondSettlementRecord settlementRecord = new FamilyMemberDiamondSettlementRecord();
settlementRecord.setFamilyMemberId(familyMember.getId());
settlementRecord.setFamilyId(familyMember.getFamilyId());
settlementRecord.setRoleType(familyMember.getRoleType());
settlementRecord.setUid(uid);
settlementRecord.setDiamondNum(golds);
settlementRecord.setCreateTime(now);
familyMemberDiamondSettlementRecordMapper.insert(settlementRecord);
bizExecutor.execute(() -> {
billRecordService.insertGeneralBillRecord(familyMember.getUid(), familyMember.getUid(), settlementRecord.getId().toString(),
BillObjTypeEnum.FAMILY_DIAMOND_SETTLEMENT, golds);
});
if (memberIndex > 0 && memberIndex % batchSize == 0){
batchSqlSession.commit();
}
memberIndex ++;
}
batchSqlSession.commit();
} catch (Exception e){
batchSqlSession.rollback();
log.error("[家族金币结算] 扣成员金币发生异常回滚", e);
} finally {
batchSqlSession.close();
}
//清空结算状态
settlementDiamondMap.clear();
log.info("[家族金币结算] 清除结算状态");
}
}

View File

@@ -69,6 +69,10 @@ public class FamilyMemberService extends ServiceImpl<FamilyMemberMapper, FamilyM
return this.baseMapper.listVaildFamilyMemberByTime(familyId, startTime, endTime);
}
public List<FamilyMember> listValidFamilyMemberByPartitionId(Integer partitionId){
return this.baseMapper.listValidFamilyMemberByPartitionId(partitionId);
}
public void dismissMemberByFamilyId(Integer familyId, Date now) {
this.lambdaUpdate()
.set(FamilyMember::getUpdateTime, now)

View File

@@ -0,0 +1,63 @@
package com.accompany.business.service.purse;
import com.accompany.common.redis.RedisKey;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.LocalCachedMapOptions;
import org.redisson.api.RLocalCachedMap;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.concurrent.TimeUnit;
@Slf4j
@Service
public class FamilyDiamondSettlementPurseService implements InitializingBean {
@Autowired
private RedissonClient redissonClient;
@Getter
private RLocalCachedMap<Long, Double> familyDiamondSettlementMap;
public boolean inSettlement(Long uid){
return familyDiamondSettlementMap.containsKey(uid);
}
@Override
public void afterPropertiesSet() throws Exception {
LocalCachedMapOptions options = LocalCachedMapOptions.defaults()
// 用于淘汰清除本地缓存内的元素
// 共有以下几种选择:
// LFU - 统计元素的使用频率,淘汰用得最少(最不常用)的。
// LRU - 按元素使用时间排序比较,淘汰最早(最久远)的。
// SOFT - 元素用Java的WeakReference来保存缓存元素通过GC过程清除。
// WEAK - 元素用Java的SoftReference来保存, 缓存元素通过GC过程清除。
// NONE - 永不淘汰清除缓存元素。
.evictionPolicy(LocalCachedMapOptions.EvictionPolicy.SOFT)
// 如果缓存容量值为0表示不限制本地缓存容量大小
.cacheSize(1000)
// 以下选项适用于断线原因造成了未收到本地缓存更新消息的情况。
// 断线重连的策略有以下几种:
// CLEAR - 如果断线一段时间以后则在重新建立连接以后清空本地缓存
// LOAD - 在服务端保存一份10分钟的作废日志
// 如果10分钟内重新建立连接则按照作废日志内的记录清空本地缓存的元素
// 如果断线时间超过了这个时间,则将清空本地缓存中所有的内容
// NONE - 默认值。断线重连时不做处理。
.reconnectionStrategy(LocalCachedMapOptions.ReconnectionStrategy.CLEAR)
// 以下选项适用于不同本地缓存之间相互保持同步的情况
// 缓存同步策略有以下几种:
// INVALIDATE - 默认值。当本地缓存映射的某条元素发生变动时,同时驱逐所有相同本地缓存映射内的该元素
// UPDATE - 当本地缓存映射的某条元素发生变动时,同时更新所有相同本地缓存映射内的该元素
// NONE - 不做任何同步处理
.syncStrategy(LocalCachedMapOptions.SyncStrategy.INVALIDATE)
// 每个Map本地缓存里元素的有效时间默认毫秒为单位
.timeToLive(2, TimeUnit.SECONDS)
// 每个Map本地缓存里元素的最长闲置时间默认毫秒为单位
.maxIdle(2, TimeUnit.SECONDS);
familyDiamondSettlementMap = redissonClient.getLocalCachedMap(RedisKey.family_diamond_settlement.getKey(), options);
}
}

View File

@@ -1,7 +1,6 @@
package com.accompany.business.service.purse;
import com.accompany.business.model.UserPurse;
import com.accompany.business.model.UserPurseExample;
import com.accompany.business.model.clan.Clan;
import com.accompany.business.mybatismapper.UserPurseMapper;
import com.accompany.business.param.neteasepush.NeteasePushParam;
@@ -72,6 +71,8 @@ public class UserPurseService extends ServiceImpl<UserPurseMapper,UserPurse> {
private WithdrawUserLimitService withdrawUserLimitService;
@Autowired
private PartitionInfoService partitionInfoService;
@Autowired
private FamilyDiamondSettlementPurseService familyDiamondSettlementPurseService;
private final Gson gson = new Gson();
@@ -218,6 +219,11 @@ public class UserPurseService extends ServiceImpl<UserPurseMapper,UserPurse> {
if (restGold < 0d){
throw new ServiceException(BusiStatus.PURSE_MONEY_NOT_ENOUGH);
}
if (familyDiamondSettlementPurseService.inSettlement(uid)){
throw new ServiceException(BusiStatus.CLAN_GOLD_SETTLEMENT);
}
log.info("subGold 操作前,buss:{},userPurse:{}", businessType, gson.toJson(userPurse));
int ret = baseMapper.updateMinusGolds(uid,goldNum);
boolean result = SqlHelper.retBool(ret);
@@ -278,6 +284,11 @@ public class UserPurseService extends ServiceImpl<UserPurseMapper,UserPurse> {
if (remainNum < 0d){
throw new ServiceException(BusiStatus.PURSE_MONEY_NOT_ENOUGH);
}
if (familyDiamondSettlementPurseService.inSettlement(uid)){
throw new ServiceException(BusiStatus.CLAN_GOLD_SETTLEMENT);
}
//保持操作原子性
int ret = baseMapper.excGoldToGuildUsd(uid, goldNum, guildUsdNum);
boolean result = SqlHelper.retBool(ret);
@@ -357,6 +368,9 @@ public class UserPurseService extends ServiceImpl<UserPurseMapper,UserPurse> {
log.info("excAllGoldToDiamond,uid:{},goldNum == 0d", uid);
return goldNum;
}
if (familyDiamondSettlementPurseService.inSettlement(uid)){
throw new ServiceException(BusiStatus.CLAN_GOLD_SETTLEMENT);
}
//保持操作原子性
int ret = baseMapper.excGoldToDiamond(uid, goldNum, goldNum);
boolean result = SqlHelper.retBool(ret);
@@ -403,6 +417,10 @@ public class UserPurseService extends ServiceImpl<UserPurseMapper,UserPurse> {
throw new ServiceException(BusiStatus.PURSE_MONEY_NOT_ENOUGH);
}
if (familyDiamondSettlementPurseService.inSettlement(uid)){
throw new ServiceException(BusiStatus.CLAN_GOLD_SETTLEMENT);
}
log.info("subDiamondAndGold 操作前,userPurse:{}", gson.toJson(up));
int ret = baseMapper.updateMinusDiamondsAndGold(uid, up.getDiamonds(), needGoldNum);
boolean result = SqlHelper.retBool(ret);

View File

@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.accompany.business.mybatismapper.family.FamilyMemberDiamondSettlementRecordMapper">
</mapper>

View File

@@ -39,4 +39,13 @@
group by uid
</select>
<select id="listValidFamilyMemberByPartitionId"
resultType="com.accompany.business.model.family.FamilyMember">
select family_id, uid, role_type, create_time, update_time, enable
from family_member fm
inner join users u on fm.uid = u.uid
and fm.enable = 1
and u.partition_id = #{partition_id}
</select>
</mapper>

View File

@@ -0,0 +1,30 @@
package com.accompany.scheduler.task;
import com.accompany.business.service.clan.FamilyDiamondSettlementService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Slf4j
@Component
public class FamilyDiamondSettlementTask {
@Autowired
private FamilyDiamondSettlementService service;
/**
* 公会钻石结算
* 周一凌晨0点
* */
@Scheduled(cron = "0 0 0 1,16 * *")
public void familyDiamondSettlement() throws InterruptedException {
long now = System.currentTimeMillis();
log.info("familyGoldSettlement() start..........");
service.settlement(null);
long finish = System.currentTimeMillis();
log.info("familyGoldSettlement() cost {}ms finish..........", finish-now);
}
}