家族-定时任务-钻石结算
This commit is contained in:
@@ -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() {
|
||||
|
@@ -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) {
|
||||
|
@@ -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;
|
||||
|
||||
}
|
@@ -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> {
|
||||
}
|
@@ -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);
|
||||
}
|
@@ -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("[家族金币结算] 清除结算状态");
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -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)
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
@@ -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>
|
@@ -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>
|
@@ -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);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user