公会-土耳其-公会水晶结算

This commit is contained in:
2025-08-20 15:13:34 +08:00
parent b81d267f14
commit c161422b32
16 changed files with 322 additions and 7 deletions

View File

@@ -1328,6 +1328,7 @@ public enum RedisKey {
guild_member_room_mic_record_vo,
guild_week_rank,
guild_month_rank,
guild_crystal_settlement,
//幸运24
lucky_24_stock,

View File

@@ -274,6 +274,7 @@ public enum BillObjTypeEnum {
LUCKY_GIFT_INCOME_ALLOT( 182, "幸运礼物价值分成", BillTypeEnum.IN, CurrencyEnum.GUILD_CRYSTAL, BillDomainTypeEnum.GUILD_POLICY2),
NORMAL_GIFT_INCOME_ALLOT( 183, "普通礼物价值分成", BillTypeEnum.IN, CurrencyEnum.GUILD_CRYSTAL, BillDomainTypeEnum.GUILD_POLICY2),
GUILD_POLICY2_CRYSTAL_SETTLEMENT( 184, "公会紫晶结算", BillTypeEnum.OUT, CurrencyEnum.GUILD_CRYSTAL, BillDomainTypeEnum.GUILD_POLICY2),
;
BillObjTypeEnum(int value, String desc, BillTypeEnum type, CurrencyEnum currency, BillDomainTypeEnum domain) {

View File

@@ -0,0 +1,24 @@
package com.accompany.business.model.guildpolicy2;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import java.math.BigDecimal;
import java.util.Date;
@Data
public class GuildMemberCrystalSettlementRecord {
@TableId(type = IdType.AUTO)
private Long id;
private String cycleDate;
private Integer partitionId;
private Long guildMemberId;
private Integer guildId;
private Byte roleType;
private Long uid;
private Double crystalNum;
private Date createTime;
}

View File

@@ -0,0 +1,7 @@
package com.accompany.business.mapper;
import com.accompany.business.model.guildpolicy2.GuildMemberCrystalSettlementRecord;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
public interface GuildMemberCrystalSettlementRecordMapper extends BaseMapper<GuildMemberCrystalSettlementRecord> {
}

View File

@@ -44,6 +44,10 @@ public interface UserPurseMapper extends BaseMapper<UserPurse> {
int excGoldToGuildUsd(@Param("uid") Long uid, @Param("goldNum") Double goldNum, @Param("guildUsdNum") Double guildUsdNum);
List<UserPurse> selectGuildCrystalByUids(@Param("uids") List<Long> uids);
int updateSettlementGuildCrystal(@Param("uid") Long uid, @Param("guildCrystal") Double guildCrystal);
UserPurse queryByUid(@Param("uid") Long uid);
}

View File

@@ -1,11 +1,10 @@
package com.accompany.business.service.clan;
package com.accompany.business.service.family;
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;

View File

@@ -77,7 +77,6 @@ public class GuildMemberService extends ServiceImpl<GuildMemberMapper, GuildMemb
.update();
}
public List<Long> listVaildGuildMember(Integer familyId) {
List<GuildMember> guildMembers = this.lambdaQuery()
.eq(null != familyId, GuildMember::getGuildId, familyId)
@@ -96,4 +95,8 @@ public class GuildMemberService extends ServiceImpl<GuildMemberMapper, GuildMemb
public List<GuildMember> listByPartitionId(int partitionId) {
return this.lambdaQuery().eq(GuildMember::getPartitionId, partitionId).list();
}
public List<GuildMember> listValidGuildMemberByPartitionId(int partitionId) {
return this.lambdaQuery().eq(GuildMember::getEnable, Boolean.TRUE).eq(GuildMember::getPartitionId, partitionId).list();
}
}

View File

@@ -0,0 +1,139 @@
package com.accompany.business.service.guildpolicy2;
import com.accompany.business.model.UserPurse;
import com.accompany.business.model.guild.GuildMember;
import com.accompany.business.model.guildpolicy2.GuildMemberCrystalSettlementRecord;
import com.accompany.business.mybatismapper.UserPurseMapper;
import com.accompany.business.service.guild.GuildMemberService;
import com.accompany.business.service.purse.GuildCrystalSettlementPurseService;
import com.accompany.business.service.record.BillRecordService;
import com.accompany.common.utils.DateTimeUtil;
import com.accompany.core.enumeration.BillObjTypeEnum;
import com.alibaba.fastjson.JSON;
import com.google.common.collect.Lists;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLocalCachedMap;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
@Slf4j
@Service
public class GuildCrystalSettlementService {
@Autowired
private UserPurseMapper userPurseMapper;
@Autowired
private GuildMemberService guildMemberService;
@Autowired
private GuildCrystalSettlementPurseService guildCrystalSettlementPurseService;
@Autowired
private BillRecordService billRecordService;
@Autowired
private GuildMemberCrystalSettlementRecordService guildMemberCrystalSettlementRecordService;
@SneakyThrows
public void settlement(Integer partitionId, Date cycleTime, Integer waitSecond) {
//当天零点再前推1秒相当于昨天23:59:59 给客户端周查询
Date now = null == cycleTime? DateTimeUtil.addSeconds(DateTimeUtil.getBeginTimeOfDay(new Date()),-1): DateTimeUtil.getEndTimeOfDay(cycleTime);
String cycleDate = DateTimeUtil.convertDate(now, DateTimeUtil.DEFAULT_DATE_PATTERN);
List<GuildMember> guildMemberList = guildMemberService.listValidGuildMemberByPartitionId(partitionId);
if (CollectionUtils.isEmpty(guildMemberList)){
return;
}
RLocalCachedMap<Long, Double> settlementCrystalMap = guildCrystalSettlementPurseService.getGuildCrystalSettlementMap();
// uid: golds
List<Long> uidList = guildMemberList.stream().map(GuildMember::getUid).distinct().collect(Collectors.toList());
Map<Long, Double> memberCrystalMap = userPurseMapper.selectGuildCrystalByUids(uidList)
.stream().collect(Collectors.toMap(UserPurse::getUid, UserPurse::getGuildCrystal));
//先把他们设置为结算状态
settlementCrystalMap.putAll(memberCrystalMap);
log.info("[guild crystal 结算] 设置结算状态 uids: {}", uidList.stream().map(Object::toString).collect(Collectors.joining(", ")));
log.info("[guild crystal 结算] 获得当前公会成员除会长的guild crystal快照 {}", JSON.toJSONString(memberCrystalMap));
//冷却3秒等已经抢到lock准备subGuildCrystal都差不多执行完再结算
Thread.sleep(3 * 1000);
//因为没有获取每个成员的钱包锁间接用double check方法但不能在极端情况下保持最终一致性例如先减少后加
//再查一次,获取冷却后的最新值,取两者最小值,确保在不扣结算等待期间的所加的金币
Map<Long, Double> memberCrystalMapSecond = userPurseMapper.selectGuildCrystalByUids(uidList)
.stream().collect(Collectors.toMap(UserPurse::getUid, UserPurse::getGuildCrystal));
log.info("[guild crystal 结算] 获得当前公会成员冷却后的紫晶快照 {}", JSON.toJSONString(memberCrystalMap));
for (Map.Entry<Long, Double> entry: memberCrystalMap.entrySet()){
Double newCrystalNum = memberCrystalMapSecond.get(entry.getKey());
if (null != newCrystalNum && Double.compare(entry.getValue(), newCrystalNum) > 0){
entry.setValue(newCrystalNum);
}
}
log.info("[guild crystal 结算] 获得当前公会成员的紫晶快照(冷却前后取最小值) {}", JSON.toJSONString(memberCrystalMap));
//留给测试去结算状态下测试
if (null == waitSecond || waitSecond < 3){
waitSecond = 0;
}
if (waitSecond > 0){
Thread.sleep(waitSecond * 1000);
}
int batchSize = 30;
List<List<GuildMember>> batchList = Lists.partition(guildMemberList, batchSize);
for (List<GuildMember> batch: batchList){
List<GuildMemberCrystalSettlementRecord> recordList = batch.parallelStream()
.map(guildMember -> {
Long uid = guildMember.getUid();
Double guildCrystal = memberCrystalMap.get(uid);
if (Double.compare(guildCrystal, 0d) > 0){
//扣金币
userPurseMapper.updateSettlementGolds(uid, guildCrystal);
}
//结算记录
GuildMemberCrystalSettlementRecord settlementRecord = new GuildMemberCrystalSettlementRecord();
settlementRecord.setPartitionId(partitionId);
settlementRecord.setCycleDate(cycleDate);
settlementRecord.setGuildMemberId(guildMember.getId());
settlementRecord.setGuildId(guildMember.getGuildId());
settlementRecord.setRoleType(guildMember.getRoleType());
settlementRecord.setUid(uid);
settlementRecord.setCrystalNum(guildCrystal);
settlementRecord.setCreateTime(now);
return settlementRecord;
}).toList();
guildMemberCrystalSettlementRecordService.saveBatch(recordList);
saveBillRecord(recordList);
}
//清空结算状态
settlementCrystalMap.clear();
log.info("[guild crystal 结算] 清除结算状态");
}
private void saveBillRecord(List<GuildMemberCrystalSettlementRecord> recordList) {
recordList.parallelStream().filter(record->record.getCrystalNum() > 0d)
.forEach(record -> {
UserPurse up = new UserPurse();
up.setUid(record.getUid());
up.setGuildCrystal(0D);
billRecordService.insertGeneralBillRecord(record.getUid(), record.getId().toString(),
BillObjTypeEnum.GUILD_POLICY2_CRYSTAL_SETTLEMENT, record.getCrystalNum(), up);
});
}
}

View File

@@ -0,0 +1,11 @@
package com.accompany.business.service.guildpolicy2;
import com.accompany.business.mapper.GuildMemberCrystalSettlementRecordMapper;
import com.accompany.business.model.guildpolicy2.GuildMemberCrystalSettlementRecord;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import org.springframework.stereotype.Service;
@Service
public class GuildMemberCrystalSettlementRecordService extends ServiceImpl<GuildMemberCrystalSettlementRecordMapper, GuildMemberCrystalSettlementRecord> {
}

View File

@@ -14,6 +14,7 @@ import com.accompany.business.util.FullMonthCycleTimeUtil;
import com.accompany.business.vo.guild.CycleDateVo;
import com.accompany.business.vo.guildpolicy.GuildPolicy2PersonalVo;
import com.accompany.business.vo.guildpolicy.GuildPolicy2Vo;
import com.accompany.common.constant.Constant;
import com.accompany.common.result.BusiResult;
import com.accompany.common.status.BusiStatus;
import com.accompany.common.utils.DateTimeUtil;
@@ -59,6 +60,8 @@ public class GuildPolicy2Service {
private UserPurseService userPurseService;
@Resource(name = "bizExecutor")
private ThreadPoolExecutor bizExecutor;
@Autowired
private GuildCrystalSettlementService guildCrystalSettlementService;
public BusiResult<GuildPolicy2Vo> getGuildPolicy2(Long uid, String cycleBeginDate) {
GuildPolicy2Vo guildPolicy2Vo = new GuildPolicy2Vo();
@@ -236,4 +239,11 @@ public class GuildPolicy2Service {
});
}
}
public void clearGuildCrystal(PartitionEnum partitionEnum, Date lastMonthDate) {
if (!Constant.ClanMode.GUILD_POLICY2.equals(partitionEnum.getClanMode())) {
return;
}
guildCrystalSettlementService.settlement(partitionEnum.getId(), lastMonthDate, null);
}
}

View File

@@ -0,0 +1,62 @@
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.util.concurrent.TimeUnit;
@Slf4j
@Service
public class GuildCrystalSettlementPurseService implements InitializingBean {
@Autowired
private RedissonClient redissonClient;
@Getter
private RLocalCachedMap<Long, Double> guildCrystalSettlementMap;
public boolean inSettlement(Long uid){
return guildCrystalSettlementMap.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);
guildCrystalSettlementMap = redissonClient.getLocalCachedMap(RedisKey.guild_crystal_settlement.getKey(), options);
}
}

View File

@@ -76,6 +76,8 @@ public class UserPurseService extends ServiceImpl<UserPurseMapper,UserPurse> {
private FamilyDiamondSettlementPurseService familyDiamondSettlementPurseService;
@Autowired
private BillRecordService billRecordService;
@Autowired
private GuildCrystalSettlementPurseService guildCrystalSettlementPurseService;
private final Gson gson = new Gson();
@@ -524,7 +526,7 @@ public class UserPurseService extends ServiceImpl<UserPurseMapper,UserPurse> {
}
//todo
if (familyDiamondSettlementPurseService.inSettlement(uid)){
if (guildCrystalSettlementPurseService.inSettlement(uid)){
throw new ServiceException(BusiStatus.CLAN_GOLD_SETTLEMENT);
}

View File

@@ -0,0 +1,23 @@
<?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.mapper.GuildMemberCrystalSettlementRecordMapper">
<!-- 通用查询结果列 -->
<resultMap id="BaseResultMap" type="com.accompany.business.model.guildpolicy2.GuildMemberCrystalSettlementRecord">
<id column="id" property="id"/>
<result column="cycle_date" property="cycleDate"/>
<result column="partition_id" property="partitionId"/>
<result column="guild_member_id" property="guildMemberId"/>
<result column="guild_id" property="guildId"/>
<result column="role_type" property="roleType"/>
<result column="uid" property="uid"/>
<result column="crystal_num" property="crystalNum"/>
<result column="create_time" property="createTime"/>
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, cycle_date, partition_id, guild_member_id, guild_id, role_type, uid, crystal_num, create_time
</sql>
</mapper>

View File

@@ -59,6 +59,7 @@
</update>
<select id="selectGoldsByUids" resultType="com.accompany.business.model.UserPurse">
/* SHARDINGSPHERE_HINT: WRITE_ROUTE_ONLY=true */
select uid, golds from user_purse
where uid in <foreach item="id" index="index" collection="uids" open="(" separator="," close=")">#{id}</foreach>
</select>
@@ -92,4 +93,15 @@
group by u.partition_id
</select>
<select id="selectGuildCrystalByUids" resultType="com.accompany.business.model.UserPurse">
/* SHARDINGSPHERE_HINT: WRITE_ROUTE_ONLY=true */
select uid, guild_crystal from user_purse
where uid in <foreach item="id" index="index" collection="uids" open="(" separator="," close=")">#{id}</foreach>
</select>
<update id="updateSettlementGuildCrystal">
update user_purse set guild_crystal = (case when guild_crystal - #{guildCrystal} > 0 then guild_crystal - #{guildCrystal} else 0 end), update_time=now()
where uid=#{uid}
</update>
</mapper>

View File

@@ -1,6 +1,6 @@
package com.accompany.scheduler.task;
import com.accompany.business.service.clan.FamilyDiamondSettlementService;
import com.accompany.business.service.family.FamilyDiamondSettlementService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;

View File

@@ -8,16 +8,19 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ThreadPoolExecutor;
@Component
@Slf4j
public class GuildPolicy2ClearDiamondTask {
public class GuildPolicy2SettlementTask {
@Autowired
private GuildPolicy2Service guildPolicy2Service;
@Resource(name = "bizExecutor")
private ThreadPoolExecutor bizExecutor;
/**
* 公户月结算用户明细
@@ -38,4 +41,18 @@ public class GuildPolicy2ClearDiamondTask {
}
}
}
/**
* 公户月结算用户明细
* 每月1号凌晨0点0分15秒执行
*/
@Scheduled(cron = "0 0 0 1 * ?", zone = "Etc/GMT-3")
public void guildPolicy2Settlement() {
List<PartitionEnum> partitionEnumList = List.of(PartitionEnum.TURKEY);
for (PartitionEnum partitionEnum : partitionEnumList) {
bizExecutor.execute(()->{
guildPolicy2Service.clearGuildCrystal(partitionEnum, DateUtil.offsetMonth(new Date(), -1));
});
}
}
}