first commit

This commit is contained in:
2026-09-21 19:21:35 +08:00
commit 6c944c4ffd
2733 changed files with 296956 additions and 0 deletions
@@ -0,0 +1,42 @@
package com.ruoyi.playingmethod.config;
import com.rabbitmq.client.Channel;
import com.ruoyi.admin.service.TtFightService;
import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig;
import com.ruoyi.common.rabbitmq.config.FightQueueConfig;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.playingmethod.service.ApiRollService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Date;
@Component
@Slf4j
public class FightRabbitConsumer {
// private final TtFightService ttFightService;
//
// public FightRabbitConsumer(TtFightService ttFightService) {
// this.ttFightService = ttFightService;
// }
//
// @RabbitListener(queues = FightQueueConfig.FIGHT_QUEUE)
// public void rollReceiveDelayed(String fightId,Message message, Channel channel) throws IOException {
// log.info("===============接收对战队列消息====================" + fightId);
//
// // 通知 MQ 消息已被接收,可以ACK(从队列中删除)了
// channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
// try {
// ttFightService.endFight(fightId);
// } catch (Exception e) {
// log.error("============消费失败,尝试消息补发再次消费!==============");
// log.error(e.getMessage());
// // 消息补发 true=消息退回到queue(有可能被其它的consumer(集群)接收到) false=只补发给当前的consumer
// channel.basicRecover(false);
// }
// }
}
@@ -0,0 +1,39 @@
package com.ruoyi.playingmethod.config;
import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper;
import com.ruoyi.admin.util.core.fight.LotteryMachine;
import com.ruoyi.admin.util.core.fight.PrizePool;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.redis.config.RedisLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
@Configuration
public class LotteryMachineConfig {
@Autowired
RedisLock redisLock;
@Autowired
RedisCache redisCache;
@Autowired
TtBoxOrnamentsMapper boxOrnamentsMapper;
@Bean
public LotteryMachine lotteryMachine(){
LotteryMachine machine = LotteryMachine.builder()
.boxOrnamentsMapper(boxOrnamentsMapper)
.redisLock(redisLock)
.redisCache(redisCache)
.prizePools(new HashMap<String, PrizePool>())
.build();
machine.preheat();
return machine;
}
}
@@ -0,0 +1,63 @@
package com.ruoyi.playingmethod.config;
import cn.hutool.core.util.ObjectUtil;
import com.rabbitmq.client.Channel;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.playingmethod.service.ApiRollService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Component
@Slf4j
public class RollRabbitConsumer {
private final ApiRollService apiRollService;
public RollRabbitConsumer(ApiRollService apiRollService) {
this.apiRollService = apiRollService;
}
@RabbitListener(queues = DelayedQueueConfig.DLK_QUEUE)
public void rollReceiveDelayed(Message message, Channel channel) throws IOException {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
Integer rollId = Integer.valueOf(new String(message.getBody()));
log.info("【死信队列消费】 roll房开奖,接受到的Roll房ID:{}", rollId);
try {
// Roll房分配结果逻辑
R r = apiRollService.endROLL(rollId);
if (!r.getCode().equals(200)) {
log.error("roll开奖 消费失败,尝试消息补发再次消费!" + r.getMsg());
channel.basicNack(deliveryTag, true, true);
return;
}
if (ObjectUtil.isNotEmpty(r.getMsg())) log.info("{}", r.getMsg());
// 通知 MQ 消息已被接收,可以ACK(从队列中删除)了
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
log.error("roll开奖 消费失败,尝试消息补发再次消费!");
log.error(e.getMessage());
channel.basicNack(deliveryTag, true, true);
}
}
//@RabbitListener(queues = DelayedQueueConfig.DLK_QUEUE)
public void t1(Message message, Channel channel) throws IOException {
// long deliveryTag = message.getMessageProperties().getDeliveryTag();
log.info("mq 测试");
System.out.println(new String(message.getBody()));
}
}
@@ -0,0 +1,9 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class BoxIdRequest {
/** 宝箱ID */
private Integer boxId;
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class ClaimWelfareRequest {
private Integer welfareId;
}
@@ -0,0 +1,42 @@
package com.ruoyi.playingmethod.contract.api.request;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.validator.constraints.Range;
import jakarta.validation.constraints.Min;
/**
* 经典开箱记录统一查询请求
*
* 使用规则:
* - 不传任何参数:查询全服记录(匿名访问)
* - boxId 为空:查询全部箱子的记录
* - boxId 不为空:仅查询当前选中箱子的记录
*/
@ApiModel("经典开箱记录查询参数")
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
public class ClassicBoxRecordPageRequest {
@ApiModelProperty("页码,默认 1")
@Min(value = 1, message = "页码最小 1")
private Integer page = 1;
@ApiModelProperty("每页条数,默认 20")
@Range(min = 1, max = 50, message = "每页条数 1-50")
private Integer size = 20;
@ApiModelProperty("选中箱子 ID:为空则查询全部箱子的记录,不为空则仅查询该箱子")
private Integer boxId;
@ApiModelProperty("排序方式:0-时间降序,1-时间升序,2-价格降序,默认 0")
@Builder.Default
private Integer orderByFie = 0;
}
@@ -0,0 +1,34 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
import java.math.BigDecimal;
/**
* 对战大厅列表查询请求
*/
@Data
public class FightHallListRequest {
private Integer pageNum;
private Integer pageSize;
/** 房间分类(含义随mode变化):mode=1时0-等待中 1-进行中 2-已结束 3-我的 4-全部;mode=2时0-超神(历史全部) 1-最大金额(今日) 2-最大增长(今日) 3-最大倍数(今日);mode=3时4-全服 3-我的 */
private String category;
/** 房间模式:0-欧皇 1-非酋 */
private String model;
/** 筛选条件:mode=1时0-全部 1-初级(≤200) 2-高级(200~800) 3-巅峰(800+);mode=3时0-全部 1-初级(≤200) 2-高级(200~800) 3-巅峰(800+) */
private String filter;
/** 房间号搜索,默认不传 */
private String keyword;
/** 房间名称搜索(创建人昵称),默认不传 */
private String roomName;
/** 模式:1-战场 2-百佳 3-历史 */
private String mode;
}
@@ -0,0 +1,9 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class FightIdRequest {
private Integer fightId;
}
@@ -0,0 +1,11 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
/**
* 对战房间信息查询请求
*/
@Data
public class FightRoomInfoRequest {
private Integer fightId;
}
@@ -0,0 +1,11 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class GetBoxListRequest {
private Integer boxTypeId;
private String homeFlag;
private String boxType;
}
@@ -0,0 +1,9 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class GetFightBoxListRequest {
/** 宝箱类型ID(可选) */
private Integer boxTypeId;
}
@@ -0,0 +1,13 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class GetFightListRequest {
/** 模式 */
private String model;
/** 状态 */
private String status;
/** 用户ID */
private Integer userId;
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class GetRedPackByKeyRequest {
private String redPackKey;
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class GetRedPackDetailRequest {
private Long recordId;
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class GetRedPackListRequest {
private Integer type;
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class GetRedPackRequest {
private Long redPackId;
}
@@ -0,0 +1,26 @@
package com.ruoyi.playingmethod.contract.api.request;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@AllArgsConstructor
@NoArgsConstructor
@Data
public class GetRollListParamRequest {
private String rollName;
// 按ID精确搜索
private Integer rollId;
// 0官方 1主播
@ApiModelProperty("0官方 1主播")
private Integer rollType;
// 0全部 1可参与 2我的 3已结束
@ApiModelProperty("0全部 1可参与 2我的 3已结束")
private Integer rollStatus;
private Integer userId;
}
@@ -0,0 +1,18 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
@Data
public class OpenBoxRequest {
@NotNull(message = "宝箱ID不能为空")
private Integer boxId;
@NotNull(message = "开箱数量不能为空")
@Min(value = 1, message = "开箱数量不能小于1")
@Max(value = 50, message = "单次开箱数量不能超过50")
private Integer num;
}
@@ -0,0 +1,16 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.Size;
import java.util.List;
@Data
public class OpenWelfareBoxRequest {
/** 要开启的福利箱子库存ID列表 */
@NotEmpty(message = "请选择要打开的箱子")
@Size(min = 1, max = 15, message = "每次最少开1个,最多开15个箱子")
private List<Long> boxInventoryIds;
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class RollIdRequest {
private Integer rollId;
}
@@ -0,0 +1,9 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class TenTopFightRequest {
//十佳对战, 1:最大金额,2:最大增长,3:最大倍数
private Integer maxType;
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.contract.api.request;
import lombok.Data;
@Data
public class TenTopUpgradeRequest {
private Boolean isVictory;
}
@@ -0,0 +1,9 @@
package com.ruoyi.playingmethod.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api/battleRoyale")
public class ApiBattleRoyaleController {
}
@@ -0,0 +1,383 @@
package com.ruoyi.playingmethod.controller;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.service.TtUserAmountRecordsService;
import com.ruoyi.admin.service.TtBoxService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.RateLimiter;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.enums.LimitType;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.TtboxRecordSource;
import com.ruoyi.domain.common.constant.TtboxRecordStatus;
import com.ruoyi.domain.dto.boxRecords.BoxRecordPageRequest;
import com.ruoyi.domain.dto.boxRecords.TenTopQuery;
import com.ruoyi.domain.dto.boxRecords.queryCondition;
import com.ruoyi.domain.entity.recorde.TtUserAmountRecords;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.*;
import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO;
import com.ruoyi.playingmethod.contract.api.request.BoxIdRequest;
import com.ruoyi.playingmethod.contract.api.request.GetBoxListRequest;
import com.ruoyi.playingmethod.contract.api.request.OpenBoxRequest;
import com.ruoyi.playingmethod.contract.api.request.ClassicBoxRecordPageRequest;
import com.ruoyi.playingmethod.service.ApiBindBoxService;
import com.ruoyi.playingmethod.service.ApiBoxRecordsService;
import com.ruoyi.system.service.ISysConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Api(tags = "玩转盲盒模式")
@Slf4j
@RestController
@RequestMapping("/api/bindbox")
public class ApiBindBoxController extends BaseController {
private final ISysConfigService sysConfigService;
private final ApiBindBoxService bindBoxService;
private final TtUserService userService;
private final TtBoxService boxService;
private final TtUserAmountRecordsService userAmountRecordsService;
public ApiBindBoxController(ISysConfigService sysConfigService,
ApiBindBoxService bindBoxService,
TtUserAmountRecordsService userAmountRecordsService,
TtUserService userService,
TtBoxService boxService) {
this.sysConfigService = sysConfigService;
this.bindBoxService = bindBoxService;
this.userService = userService;
this.userAmountRecordsService = userAmountRecordsService;
this.boxService = boxService;
}
@Autowired
private ApiBoxRecordsService apiBoxRecordsService;
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private RedisLock redisLock;
@ApiOperation("获取宝箱信息")
@Anonymous
@PostMapping("/boxData")
public R<TtBoxA> getBoxData(@RequestBody BoxIdRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance");
if ("1".equals(bindBoxMaintenance)) {
return R.fail("盲盒开箱功能正在维护中......");
}
TtBoxA boxData = bindBoxService.getBoxData(request.getBoxId());
return R.ok(boxData);
}
@ApiOperation("获取补偿箱信息")
@PostMapping("/getCompBoxData")
public AjaxResult getCompBoxData(@RequestBody BoxIdRequest request) {
List<TtOrnamentsA> compBoxData = bindBoxService.getCompBoxData(request.getBoxId());
return success(compBoxData);
}
@ApiOperation("获取宝箱列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "boxType", value = "宝箱类型(1普通箱 2对战箱 3补偿箱 4九宫格)", dataType = "String", dataTypeClass = String.class)
})
@Anonymous
@PostMapping("/getBoxList")
public R<List<TtBoxVO>> getBoxList(@RequestBody(required=false) GetBoxListRequest request) {
Integer boxTypeId = request != null ? request.getBoxTypeId() : null;
String homeFlag = request != null ? request.getHomeFlag() : null;
String boxType = request != null ? request.getBoxType() : null;
// 经典开箱接口只返回经典宝箱(is_fight = '1'),不混入对战箱
List<TtBoxVO> boxData = bindBoxService.getBoxList(boxTypeId, homeFlag, boxType, "1");
if (boxData.isEmpty()) {
return R.ok(null, "没有匹配的数据。");
}
return R.ok(boxData);
}
/**
* 盲盒开箱模式开箱
*/
@ApiOperation("盲盒开箱")
@UpdateUserCache
@RateLimiter(time = 60, count = 30, limitType = LimitType.IP)
@PostMapping("/openBox")
public R openBox(@RequestBody @Validated OpenBoxRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance");
if ("1".equals(bindBoxMaintenance)) {
return R.fail("盲盒开箱功能正在维护中......");
}
TtUser ttUser = userService.getById(getUserId());
TtBox ttBox = boxService.getById(request.getBoxId());
if (ttBox == null) return R.fail("宝箱不存在");
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired();
}
// 先做充值校验:新用户(从未充值)直接拦截,返回 703。
// 归档兼容:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL,
// 避免历史充值流水被归档后误判为“未充值”。
BigDecimal cacheRecharge = ttUser.getTotalRechargeAmount();
boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0;
if (!recharged) {
BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource(
Collections.singletonList(ttUser.getUserId()),
TtAccountRecordSource.RECHARGE.getCode(),
TtAccountRecordType.INPUT.getCode());
recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0;
}
if (!recharged) {
return R.fail(703, "玩家未充值不能游玩任何游戏");
}
// 再做余额校验:与本次开箱所需金额(单价 * 数量)比较,不足则拒绝
BigDecimal openBoxTotal = ttBox.getPrice().multiply(new BigDecimal(request.getNum()));
if (ttUser.getAccountAmount() == null || ttUser.getAccountAmount().compareTo(openBoxTotal) < 0) {
return R.fail(702, "账户余额不足");
}
// BigDecimal openBoxBeanTotal = ttBox.getPrice().multiply(new BigDecimal(num));
// if (ttUser.getAccountAmount().compareTo(openBoxBeanTotal) < 0) return R.fail("您的账户游戏币不足!");
// 控制层防重锁:按 userId 串行,锁包裹整个 @Transactional 的 blindBox,解锁晚于提交,
// 避免弱网重复提交导致重复开箱/重复扣款(锁粒度=单用户,不影响其他用户)
String lockKey = RedisConstants.OPEN_BOX_LOCK + ttUser.getUserId();
boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS);
if (!locked) {
log.warn("开箱加锁失败, userId={}", ttUser.getUserId());
return R.fail("开箱处理中,请勿重复操作");
}
try {
// P1#1: 锁内重新读取用户信息,避免 TOCTOU 竞态
ttUser = userService.getById(ttUser.getUserId());
return bindBoxService.blindBox(ttUser, ttBox, request.getNum());
} finally {
redisLock.unlock(lockKey);
}
}
@ApiOperation("获取历史开箱信息")
@PostMapping("/getBindBoxHistory")
public R getBindBoxHistory(@RequestBody @Validated queryCondition param) {
if (param.getStatus().equals(1) || param.getStatus().equals(10)) {
return R.fail("非法的状态参数。");
}
PageUtils.clearPage();
List<TtBoxRecordsVO> ttBoxRecordsVOS = apiBoxRecordsService.byCondition(param);
return R.ok(ttBoxRecordsVOS);
}
@ApiOperation("获取个人历史开箱记录")
@PostMapping("/myBoxHistory")
public R<List<TtBoxRecordsVO>> myBoxHistory(@RequestBody queryCondition param) {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) {
return R.fail("登录过期。");
}
param.setUserId(userId.intValue());
param.setOrderByFie(0);
param.setStatus(Arrays.asList(
TtboxRecordStatus.IN_PACKSACK_ON.getCode(),
TtboxRecordStatus.APPLY_DELIVERY.getCode(),
TtboxRecordStatus.DELIVERY_YET.getCode(),
TtboxRecordStatus.RESOLVE.getCode()
));
// 仅展示经典开箱(盲盒)来源的记录,避免混入极速糖果/疯狂转盘等其他游戏记录
param.setSource(Arrays.asList(
TtboxRecordSource.BLIND_BOX.getCode()
));
PageUtils.clearPage();
List<TtBoxRecordsVO> list = apiBoxRecordsService.byCondition(param);
return R.ok(list);
}
@ApiOperation("获取当前补偿值")
@PostMapping("/getCompValue")
public AjaxResult getCompValue(@RequestBody BoxIdRequest request) {
Long userId = getUserId();
TtUser ttUser = userService.getById(userId);
TtBox ttBox = boxService.getById(request.getBoxId());
if (Objects.isNull(ttBox))
return error("宝箱不存在");
if (!"3".equals(ttBox.getBoxType()))
return error("该宝箱不是补偿宝箱");
return success(bindBoxService.getCompValue(ttUser, ttBox));
}
@ApiOperation("盲盒top")
@PostMapping("/top")
public R<List<TtBoxRecordsVO>> topBlinkBox() {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) {
return R.fail("登录过期。");
}
queryCondition param = new queryCondition();
param.setUserId(userId.intValue());
param.setOrderByFie(2);
param.setSource(Collections.singletonList(TtboxRecordSource.BLIND_BOX.getCode()));
param.setStatus(Arrays.asList(TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode(), TtboxRecordStatus.DELIVERY_YET.getCode()));
param.setPage(1);
param.setSize(1);
PageUtils.clearPage();
List<TtBoxRecordsVO> ttBoxRecordsVOS = new ArrayList<>(apiBoxRecordsService.byCondition(param));
param.setSource(Collections.singletonList(TtboxRecordSource.FIGHT.getCode()));
ttBoxRecordsVOS.addAll(apiBoxRecordsService.byCondition(param));
param.setSource(Collections.singletonList(TtboxRecordSource.UPGRADE.getCode()));
ttBoxRecordsVOS.addAll(apiBoxRecordsService.byCondition(param));
return R.ok(ttBoxRecordsVOS);
}
@ApiOperation("十佳掉落")
@PostMapping("/tenTop")
public R<List<TtBoxRecordsVO>> tenTop() {
TenTopQuery param = new TenTopQuery();
param.setSource(Collections.singletonList(TtboxRecordSource.BLIND_BOX.getCode()));
param.setStatus(Arrays.asList(TtboxRecordStatus.IN_PACKSACK_ON.getCode(), TtboxRecordStatus.APPLY_DELIVERY.getCode(), TtboxRecordStatus.DELIVERY_YET.getCode()));
param.setPage(1);
param.setSize(10);
PageUtils.clearPage();
List<TtBoxRecordsVO> ttBoxRecordsVOS = apiBoxRecordsService.tenTopQuery(param);
return R.ok(ttBoxRecordsVOS);
}
@ApiOperation("今日百佳")
@PostMapping("/todayTopHundred")
public R<List<TtBoxRecordsVO>> todayTopHundred() {
List<Integer> status = Arrays.asList(
TtboxRecordStatus.IN_PACKSACK_ON.getCode(),
TtboxRecordStatus.APPLY_DELIVERY.getCode(),
TtboxRecordStatus.DELIVERY_YET.getCode(),
TtboxRecordStatus.RESOLVE.getCode()
);
List<Integer> source = Arrays.asList(
TtboxRecordSource.BLIND_BOX.getCode()
);
PageUtils.clearPage();
List<TtBoxRecordsVO> list = apiBoxRecordsService.topHundredQuery(status, source, true);
return R.ok(list);
}
@ApiOperation("历史百佳")
@PostMapping("/historyTopHundred")
public R<List<TtBoxRecordsVO>> historyTopHundred() {
List<Integer> status = Arrays.asList(
TtboxRecordStatus.IN_PACKSACK_ON.getCode(),
TtboxRecordStatus.APPLY_DELIVERY.getCode(),
TtboxRecordStatus.DELIVERY_YET.getCode(),
TtboxRecordStatus.RESOLVE.getCode()
);
List<Integer> source = Arrays.asList(
TtboxRecordSource.BLIND_BOX.getCode()
);
PageUtils.clearPage();
List<TtBoxRecordsVO> list = apiBoxRecordsService.topHundredQuery(status, source, false);
return R.ok(list);
}
@ApiOperation("实时 - 所有人历史开箱按时间排序")
@Anonymous
@PostMapping("/realtime")
public R<List<TtBoxRecordsVO>> realtime(@RequestBody @Validated BoxRecordPageRequest request) {
queryCondition param = new queryCondition();
param.setPage(request.getPage());
param.setSize(request.getSize());
// 仅展示当前选中箱子的开箱记录
param.setBoxId(request.getBoxId());
param.setOrderByFie(0);
param.setStatus(Arrays.asList(
TtboxRecordStatus.IN_PACKSACK_ON.getCode(),
TtboxRecordStatus.APPLY_DELIVERY.getCode(),
TtboxRecordStatus.DELIVERY_YET.getCode(),
TtboxRecordStatus.RESOLVE.getCode()
));
param.setSource(Arrays.asList(
TtboxRecordSource.BLIND_BOX.getCode()
));
PageUtils.clearPage();
List<TtBoxRecordsVO> list = apiBoxRecordsService.byCondition(param);
return R.ok(list);
}
@ApiOperation("欧皇 - 所有人开箱按最高价格排序")
@Anonymous
@PostMapping("/luckyKing")
public R<List<TtBoxRecordsVO>> luckyKing(@RequestBody @Validated BoxRecordPageRequest request) {
queryCondition param = new queryCondition();
param.setPage(request.getPage());
param.setSize(request.getSize());
// 仅展示当前选中箱子的开箱记录
param.setBoxId(request.getBoxId());
param.setOrderByFie(2);
param.setStatus(Arrays.asList(
TtboxRecordStatus.IN_PACKSACK_ON.getCode(),
TtboxRecordStatus.APPLY_DELIVERY.getCode(),
TtboxRecordStatus.DELIVERY_YET.getCode(),
TtboxRecordStatus.RESOLVE.getCode()
));
param.setSource(Arrays.asList(
TtboxRecordSource.BLIND_BOX.getCode()
));
// 仅展示金色品质(饰品级别 ID=1)的饰品
param.setOrnamentLevelIds(Arrays.asList(1));
PageUtils.clearPage();
List<TtBoxRecordsVO> list = apiBoxRecordsService.byCondition(param);
return R.ok(list);
}
/**
* 经典开箱记录统一接口(支持匿名查询)
*
* 使用场景:
* - 全服全部箱子:不传参数 / 不登录 → 查看所有用户的所有箱子的开箱记录
* - 全服当前箱子:不传 userId + 传 boxId / 不登录 → 查看指定箱子的全服记录
*/
@ApiOperation("经典开箱记录")
@Anonymous
@PostMapping("/classicRecords")
public R<List<TtBoxRecordsVO>> classicRecords(@RequestBody @Validated ClassicBoxRecordPageRequest request) {
// 构建查询条件
queryCondition condition = new queryCondition();
condition.setPage(request.getPage());
condition.setSize(request.getSize());
condition.setBoxId(request.getBoxId());
condition.setOrderByFie(request.getOrderByFie());
// 固定过滤:仅展示经典开箱来源(source=1),有效状态包括背包显示、申请提货、已提货、分解
condition.setStatus(Arrays.asList(
TtboxRecordStatus.IN_PACKSACK_ON.getCode(), // 0-在背包显示
TtboxRecordStatus.APPLY_DELIVERY.getCode(), // 2-申请提货
TtboxRecordStatus.DELIVERY_YET.getCode(), // 3-已经提货
TtboxRecordStatus.RESOLVE.getCode() // 5-分解
));
condition.setSource(Arrays.asList(TtboxRecordSource.BLIND_BOX.getCode()));
PageUtils.clearPage();
List<TtBoxRecordsVO> records = apiBoxRecordsService.byCondition(condition);
return R.ok(records);
}
}
@@ -0,0 +1,38 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.domain.dto.boxRecords.queryCondition;
import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO;
import com.ruoyi.playingmethod.service.ApiBoxRecordsService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Api(tags = "出货记录")
@RestController
@RequestMapping("/api/boxRecords")
public class ApiBoxRecordsController {
@Autowired
private ApiBoxRecordsService apiBoxRecordsService;
@ApiOperation("获取历史出货记录")
@PostMapping("/historyByCondition")
public R getBindBoxHistory(@RequestBody @Validated queryCondition param) {
if (param.getStatus().contains(1) || param.getStatus().contains(10)) {
return R.fail("非法的状态参数。");
}
PageUtils.clearPage();
List<TtBoxRecordsVO> ttBoxRecordsVOS = apiBoxRecordsService.byCondition(param);
return R.ok(ttBoxRecordsVOS);
}
}
@@ -0,0 +1,56 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.playingmethod.service.ApiCompoundService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@Api(tags = "合成")
@RestController
@RequestMapping("/api/compound")
public class ApiCompoundController extends BaseController {
private final ISysConfigService sysConfigService;
private final TtUserService userService;
private final ApiCompoundService compoundService;
public ApiCompoundController(ISysConfigService sysConfigService,
TtUserService userService,
ApiCompoundService compoundService) {
this.sysConfigService = sysConfigService;
this.userService = userService;
this.compoundService = compoundService;
}
@ApiOperation("合成")
@PostMapping("/compound")
public R<UserPackSackDataVO> compound(@RequestBody List<Long> packSackIds) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String compoundMaintenance = sysConfigService.selectConfigByKey("compoundMaintenance");
if ("1".equals(compoundMaintenance)) {
return R.fail("汰换合成功能正在维护中......");
}
TtUser ttUser = userService.getById(getUserId());
return compoundService.compound(packSackIds, ttUser);
}
@ApiOperation("获取用户合成记录")
@PostMapping("/getUserCompoundRecord")
public PageDataInfo<UserPackSackDataVO> getUserCompoundRecord() {
startPage();
List<UserPackSackDataVO> userCompoundRecord = compoundService.getUserCompoundRecord(getUserId().intValue());
return getPageData(userCompoundRecord);
}
}
@@ -0,0 +1,153 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.admin.service.TtBoxService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.common.utils.PageUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.dto.exponent.ExponentJoinParam;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.playingmethod.contract.api.request.BoxIdRequest;
import com.ruoyi.playingmethod.contract.api.request.OpenBoxRequest;
import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper;
import com.ruoyi.playingmethod.service.ApiExponentService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Api(tags = "指数盲盒")
@RestController
@RequestMapping("/api/exponent")
public class ApiExponentController extends BaseController {
@Autowired
private ApiExponentService apiExponentService;
@Autowired
private TtUserService ttUserService;
@Autowired
private ApiBindBoxMapper bindBoxMapper;
@Autowired
private TtBoxService boxService;
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private RedisLock redisLock;
@ApiOperation("加入指数盲盒")
@Anonymous
@UpdateUserCache
@PostMapping("/join")
public R join(@RequestBody @Validated ExponentJoinParam param) {
TtUser ttUser = ttUserService.getById(getUserId());
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired();
}
// 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL,
// 避免历史充值流水被归档后误判为“未充值”。
BigDecimal cacheRecharge = ttUser.getTotalRechargeAmount();
boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0;
if (!recharged) {
BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource(
Collections.singletonList(ttUser.getUserId()),
TtAccountRecordSource.RECHARGE.getCode(),
TtAccountRecordType.INPUT.getCode());
recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0;
}
if (!recharged) {
return R.fail(703, "玩家未充值不能游玩任何游戏");
}
// 控制层防重锁:按 userId 串行,避免弱网重复提交导致重复扣款/重复参与(锁粒度=单用户)
String lockKey = RedisConstants.EXPONENT_JOIN_LOCK + ttUser.getUserId();
boolean locked = redisLock.tryLock(lockKey, 0L, 30L, TimeUnit.SECONDS);
if (!locked) {
return R.fail("处理中,请勿重复操作");
}
try {
return apiExponentService.join(ttUser, param);
} finally {
redisLock.unlock(lockKey);
}
}
@ApiOperation("获取宝箱列表")
@ApiImplicitParams({
@ApiImplicitParam(name = "boxType", value = "宝箱类型(1普通箱 2对战箱 3补偿箱 4九宫格 5指数盲盒)", dataType = "String", dataTypeClass = String.class)
})
@Anonymous
@PostMapping("/box")
public R box(@RequestBody BoxIdRequest request) {
TtUser ttUser = ttUserService.getById(getUserId());
return apiExponentService.box(ttUser, request.getBoxId());
}
@ApiOperation("最近十期结果")
@Anonymous
@PostMapping("/recenttenperiod")
public R recenttenperiod() {
PageUtils.clearPage();
return apiExponentService.recenttenperiod();
}
@ApiOperation("结果信息")
@Anonymous
@PostMapping("/info")
public R info() {
PageUtils.clearPage();
TtUser ttUser = ttUserService.getById(getUserId());
return apiExponentService.info(ttUser);
}
@ApiOperation("指数记录")
@Anonymous
@PostMapping("/records")
public R records() {
startPage();
TtUser ttUser = ttUserService.getById(getUserId());
return apiExponentService.records(ttUser);
}
@ApiOperation("我的箱子")
@Anonymous
@PostMapping("/mybox")
public R mybox() {
TtUser ttUser = ttUserService.getById(getUserId());
return apiExponentService.mybox(ttUser);
}
@ApiOperation("盲盒开箱")
@UpdateUserCache
@PostMapping("/openBox")
public R openBox(@RequestBody OpenBoxRequest request) {
TtUser ttUser = ttUserService.getById(getUserId());
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired();
}
TtBox ttBox = boxService.getById(request.getBoxId());
return apiExponentService.openbox(ttUser, ttBox, request.getNum());
}
}
@@ -0,0 +1,667 @@
package com.ruoyi.playingmethod.controller;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.*;
import com.ruoyi.admin.util.core.fight.LotteryMachine;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.constant.ErrorCode;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.RandomUtil;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.dto.fight.FightDetailParam;
import com.ruoyi.domain.dto.fight.FightOnMyOwnParam;
import com.ruoyi.domain.dto.fight.QuickJoinFightParam;
import com.ruoyi.domain.dto.fight.BaiJiaFightParam;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateFightBody;
import com.ruoyi.domain.other.FightBoutData;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.domain.other.TtBoxVO;
import com.ruoyi.domain.vo.ApiFightListDataVO;
import com.ruoyi.domain.vo.FightResultDataVO;
import com.ruoyi.domain.vo.fight.FightBoxVO;
import com.ruoyi.domain.vo.fight.FightResultVO;
import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO;
import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO;
import com.ruoyi.playingmethod.service.ApiBindBoxService;
import com.ruoyi.playingmethod.service.ApiFightService;
import com.ruoyi.playingmethod.contract.api.request.BoxIdRequest;
import com.ruoyi.domain.other.TtBoxA;
import com.ruoyi.system.service.ISysConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Collections;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.stream.Collectors;
import java.util.concurrent.TimeUnit;
@Api(tags = "对战模式")
@Slf4j
@RestController
@RequestMapping("/api/fight")
public class ApiFightController extends BaseController {
private final TtFightResultService fightResultService;
private final ISysConfigService sysConfigService;
private final TtUserService userService;
private final ApiFightService apiFightService;
private final RedisCache redisCache;
@Autowired
private TtBoxOrnamentsService boxOrnamentsService;
@Autowired
private TtOrnamentService ttOrnamentService;
@Autowired
private LotteryMachine lotteryMachine;
@Autowired
private TtBoxService boxService;
@Autowired
private TtUserMapper ttUserMapper;
public ApiFightController(TtFightResultService fightResultService,
ISysConfigService sysConfigService,
TtUserService userService,
ApiFightService apiFightService,
RedisCache redisCache) {
this.fightResultService = fightResultService;
this.sysConfigService = sysConfigService;
this.userService = userService;
this.apiFightService = apiFightService;
this.redisCache = redisCache;
}
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private ApiBindBoxService bindBoxService;
public R checkLogin() {
Long userId;
try {
userId = getUserId();
if (ObjectUtil.isEmpty(userId)) return R.fail(ErrorCode.LOGIN_EXPIRED, "登录过期,请重新登录。");
return R.ok(userId);
} catch (Exception e) {
return R.fail("登录过期,请重新登录。");
}
}
/**
* Step1 创建对战房间
*/
@ApiOperation("创建对战房间")
@UpdateUserCache
@PostMapping("/createFight")
public R createFight(@Validated @RequestBody CreateFightBody createFightParam) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) {
return R.fail("开箱对战功能正在维护中......");
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) {
return checkLogin;
}
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser player = userService.getById(userId);
// 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL,
// 避免历史充值流水被归档后误判为"未充值"。
BigDecimal cacheRecharge = player.getTotalRechargeAmount();
boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0;
if (!recharged) {
BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource(
Collections.singletonList(userId),
TtAccountRecordSource.RECHARGE.getCode(),
TtAccountRecordType.INPUT.getCode());
recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0;
}
if (!recharged) {
// 未充值统一返回业务码 702,便于前端识别并引导充值
return R.fail(702, "您的账户未进行充过值!");
}
return apiFightService.createFight(createFightParam, player);
}
/**
* 复刻对战房间:复制目标房间的宝箱配置、人数、组队模式等,创建一个新房间,当前用户成为房主
*/
@ApiOperation("复刻对战房间")
@UpdateUserCache
@PostMapping("/copyFight")
public R copyFight(@RequestBody FightDetailParam param) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) {
return R.fail("开箱对战功能正在维护中......");
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) {
return checkLogin;
}
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser player = userService.getById(userId);
// 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL
BigDecimal cacheRecharge = player.getTotalRechargeAmount();
boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0;
if (!recharged) {
BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource(
Collections.singletonList(userId),
TtAccountRecordSource.RECHARGE.getCode(),
TtAccountRecordType.INPUT.getCode());
recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0;
}
if (!recharged) {
// 未充值统一返回业务码 702,便于前端识别并引导充值
return R.fail(702, "您的账户未进行充过值!");
}
return apiFightService.copyFight(param.getFightId(), player);
}
/**
* Step2 加入房间(占座),先行HTTP
*/
@ApiOperation("加入对战房间")
@PostMapping("/joinFightRoom")
public R joinFightRoom(@RequestBody FightDetailParam param) {
Integer fightId = param.getFightId();
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) {
return R.fail("开箱对战功能正在维护中......");
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser player = userService.getById(userId);
// 归档兼容充值校验:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL,
// 避免历史充值流水被归档后误判为"未充值"。
BigDecimal cacheRecharge = player.getTotalRechargeAmount();
boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0;
if (!recharged) {
BigDecimal sumRecharge = ttUserBlendErcashMapper.sumAmountByUserIdsAndSource(
Collections.singletonList(userId),
TtAccountRecordSource.RECHARGE.getCode(),
TtAccountRecordType.INPUT.getCode());
recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0;
}
if (!recharged) {
// 未充值统一返回业务码 702,便于前端识别并引导充值
return R.fail(702, "您的账户未进行充过值!");
}
return apiFightService.joinFight(fightId, player, param.getSeatCode());
}
/**
* 退出房间
*/
@ApiOperation("退出房间")
@PostMapping("/fightRoomExit")
public R fightRoomExit(@RequestBody FightDetailParam param) {
Integer fightId = param.getFightId();
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......");
String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) return R.fail("开箱对战功能正在维护中......");
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser player = userService.getById(userId);
return apiFightService.fightRoomExit(fightId, player);
}
/**
* Step3 玩家准备游戏
*/
@ApiOperation("玩家准备游戏")
@UpdateUserCache
@PostMapping("/seatrReady")
public R seatrReady(@RequestBody FightDetailParam param) {
Integer fightId = param.getFightId();
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) {
return R.fail("开箱对战功能正在维护中......");
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser player = userService.getById(userId);
return apiFightService.seatrReady(fightId, player);
}
/**
* Step4 开始游戏(前端满人自动开始)
*/
@ApiOperation("开始游戏")
@PostMapping("/fightBegin")
public R fightBegin(@RequestBody FightDetailParam param) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) {
return R.fail("开箱对战功能正在维护中......");
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser player = userService.getById(userId);
return apiFightService.fightBegin(param.getFightId(), player);
}
/**
* 游戏结束标记
*/
@ApiOperation("游戏结束标记")
@PostMapping("/fightEnd")
public R fightEnd(@RequestBody FightDetailParam param) {
Integer fightId = param.getFightId();
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......");
String fightMaintenance = sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) return R.fail("开箱对战功能正在维护中......");
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
return apiFightService.fightEnd(fightId, getUserId());
}
/**
* 抽奖机测试
*/
// @ApiOperation("抽奖机测试")
// @GetMapping("LotteryMachineTest")
public R LotteryMachineTest() {
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
Integer userId = ((Long) checkLogin.getData()).intValue();
TtUser player = userService.getById(userId);
TtBox box = boxService.getById(2L);
String ornamentId = lotteryMachine.singleLottery(player, box);
return R.ok(ornamentId);
}
@ApiOperation("我参与的历史对战记录")
@PostMapping("/fightOnMyOwn")
public R fightOnMyOwn(@RequestBody FightOnMyOwnParam param) {
Long userId = getUserId();
if (ObjectUtil.isNull(userId)) return R.fail(ErrorCode.LOGIN_EXPIRED, "登录过期。");
param.setPlayerId(userId.intValue());
List<ApiFightListDataVO> list = apiFightService.getFightList(param);
return R.ok(list);
}
/**
* 快速加入对战房间 - 在本阵营(model 0=欧皇 1=非酋)内返回等待中且有空座位、
* 总价不超过用户余额的最高价房间 id(不落座,需登录)
*/
@ApiOperation("快速加入对战房间")
@UpdateUserCache
@PostMapping("/quickJoinFightRoom")
public R<Long> quickJoinFightRoom(@RequestBody QuickJoinFightParam param) {
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return R.fail(ErrorCode.LOGIN_EXPIRED, checkLogin.getMsg());
Integer userId = ((Long) checkLogin.getData()).intValue();
return apiFightService.quickJoinFightRoom(userId, param != null ? param.getModel() : null);
}
@ApiOperation("全服历史对战记录")
@PostMapping("/fightOnAll")
public R fightOnAll(@RequestBody FightOnMyOwnParam param) {
List<ApiFightListDataVO> list = apiFightService.getFightList(param);
return R.ok(list);
}
@ApiOperation("历史对战详情")
@PostMapping("/fightDetail")
public R fightDetail(@RequestBody FightDetailParam param) {
return apiFightService.fightDetail(param);
}
@ApiOperation("获取指定记录更早的历史对战")
@PostMapping("/earlierHistory")
public R earlierHistory(@RequestBody FightDetailParam param) {
return apiFightService.earlierHistory(param);
}
@ApiOperation("获取对战宝箱详情")
@PostMapping("/simpleBoxDetail")
public PageDataInfo<SimpleOrnamentVO> simpleBoxDetail(@RequestBody Map<String, Integer> param) {
startPage();
Integer boxId = param.get("boxId");
List<SimpleOrnamentVO> list = boxOrnamentsService.simpleBoxDetail(boxId);
return getPageData(list);
}
@ApiOperation("获取对战宝箱")
@PostMapping("/getFightBoxList")
@Anonymous
public R<List<TtBoxVO>> getFightBoxList(@RequestBody(required = false) Map<String, Integer> param) {
Integer boxTypeId = (param != null) ? param.get("boxTypeId") : null;
List<TtBoxVO> boxData = apiFightService.getFightBoxList(boxTypeId);
if (ObjectUtil.isEmpty(boxData) || boxData.isEmpty()) return R.ok(null, "没有匹配的数据。");
return R.ok(boxData);
}
@ApiOperation("获取对战列表")
@Anonymous
@PostMapping("/getFightList")
public PageDataInfo<ApiFightListDataVO> getFightList(@RequestBody(required = false) Map<String, Object> param) {
startPage();
String model = param != null ? (String) param.get("model") : null;
String status = param != null ? (String) param.get("status") : null;
Integer userId = param != null && param.get("userId") != null ? Integer.valueOf(param.get("userId").toString()) : null;
List<ApiFightListDataVO> list = apiFightService.getFightList(model, status, userId, null);
return getPageData(list);
}
@ApiOperation("获取对战大厅房间列表(HTTP)")
@Anonymous
@PostMapping("/hall")
public R<Map<String, Object>> fightHall(@RequestBody(required = false) Map<String, Object> param) {
Integer pageNum = param != null && param.get("pageNum") != null ? Integer.valueOf(param.get("pageNum").toString()) : 1;
Integer pageSize = param != null && param.get("pageSize") != null ? Integer.valueOf(param.get("pageSize").toString()) : 10;
String status = param != null ? (String) param.get("status") : null;
String model = param != null ? (String) param.get("model") : null;
BigDecimal boxPriceTotalMin = param != null && param.get("boxPriceTotalMin") != null ? new BigDecimal(param.get("boxPriceTotalMin").toString()) : null;
BigDecimal boxPriceTotalMax = param != null && param.get("boxPriceTotalMax") != null ? new BigDecimal(param.get("boxPriceTotalMax").toString()) : null;
Page<TtFight> pageInfo = new Page<>(pageNum, pageSize);
pageInfo.setOptimizeCountSql(false);
LambdaQueryWrapper<TtFight> fightQuery = new LambdaQueryWrapper<>();
fightQuery
.eq(status != null, TtFight::getStatus, status)
.eq(model != null, TtFight::getModel, model)
.between(boxPriceTotalMin != null && boxPriceTotalMax != null,
TtFight::getBoxPriceTotal, boxPriceTotalMin, boxPriceTotalMax)
.ge(boxPriceTotalMin == null && boxPriceTotalMax != null,
TtFight::getBoxPriceTotal, boxPriceTotalMax)
.orderByDesc(TtFight::getCreateTime);
List<TtFight> fightList = apiFightService.page(pageInfo, fightQuery).getRecords();
Map<String, Object> result = new HashMap<>();
if (CollectionUtils.isEmpty(fightList)) {
result.put("rows", new ArrayList<>());
result.put("total", 0);
return R.ok(result, "所有对战房间");
}
List<Integer> boxIds = new ArrayList<>();
for (TtFight fight : fightList) {
if (fight == null || MapUtils.isEmpty(fight.getBoxData())) {
continue;
}
for (Map.Entry<String, FightBoxVO> entry : fight.getBoxData().entrySet()) {
Object value = entry.getValue();
if (value == null) {
continue;
}
Integer boxId;
if (value instanceof LinkedHashMap) {
Map<String, Object> map = (LinkedHashMap) value;
boxId = map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null;
} else {
boxId = ((FightBoxVO) value).getBoxId();
}
if (boxId != null) {
boxIds.add(boxId);
}
}
}
LambdaQueryWrapper<TtBox> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(TtBox::getBoxId, boxIds);
List<TtBox> boxes = boxService.list(queryWrapper);
Map<Integer, BigDecimal> boxMap = boxes.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice));
for (TtFight fight : fightList) {
if (fight == null || MapUtils.isEmpty(fight.getBoxData())) {
continue;
}
for (Map.Entry<String, FightBoxVO> entry : fight.getBoxData().entrySet()) {
Object value = entry.getValue();
if (value == null) {
continue;
}
FightBoxVO vo;
if (value instanceof LinkedHashMap) {
Map<String, Object> map = (LinkedHashMap) value;
vo = new FightBoxVO(
map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null,
map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null,
map.get("boxImg01") != null ? map.get("boxImg01").toString() : null,
map.get("boxImg02") != null ? map.get("boxImg02").toString() : null);
vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0)));
} else {
vo = (FightBoxVO) value;
vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0)));
}
fight.getBoxData().put(vo.getBoxId().toString(), vo);
}
}
apiFightService.enrichOwnerNickNames(fightList);
result.put("rows", fightList);
result.put("total", apiFightService.page(pageInfo, fightQuery).getTotal());
return R.ok(result, "所有对战房间");
}
@ApiOperation("获取对战数据")
@PostMapping("/getFightData")
public R<ApiFightListDataVO> getFightData(@RequestBody FightDetailParam param) {
ApiFightListDataVO apiFightListDataVO = apiFightService.getFightList(null, null, null, param.getFightId()).get(0);
return R.ok(apiFightListDataVO);
}
// @ApiOperation("获取对战结果")
// @GetMapping("/getFightResult/{fightId}")
// public R<FightResultDataVO> getFightResult(@PathVariable("fightId") Integer fightId) {
// FightResultDataVO fightResultDataVO = fightResultService.getFightResult(fightId);
// return R.ok(fightResultDataVO);
// }
@ApiOperation("获取对战结果")
@PostMapping("/getFightResult")
public R<FightResultVO> getFightResult(@RequestBody FightDetailParam param) {
FightResultVO fightResultVO = fightResultService.getFightResult(param.getFightId());
return R.ok(fightResultVO);
}
@ApiOperation("存储对战回合数据")
@PostMapping("/saveFightBoutData")
public R<Boolean> saveFightBoutData(@RequestBody FightBoutData fightBoutData) {
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
String key = "fight_bout_data:fight_" + fightBoutData.getFightId();
redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS);
return R.ok();
}
@ApiOperation("获取对战回合数")
@PostMapping("/getFightBoutNum")
public R<Integer> getFightBoutNum(@RequestBody FightDetailParam param) {
Integer fightId = param.getFightId();
String key = "fight_bout_data:fight_" + fightId;
FightBoutData fightBoutData = redisCache.getCacheObject(key);
if (StringUtils.isNotNull(fightBoutData)) {
return R.ok(fightBoutData.getBoutNum());
} else {
// return R.fail("对战回合不存在");
return R.ok();
}
}
/**
* 获取当前回合开箱结果
*/
// @GetMapping("/getFightRecord")
public R<FightResultDataVO> getFightRecord(@RequestParam(value = "fightId", required = false) Integer fightId,
@RequestParam(value = "round", required = false) Integer round,
@RequestParam(value = "rounds", required = false) Integer rounds) {
// System.out.println("=========================获取当前回合开箱结果=============================");
// System.out.println("fightId:" + fightId);
// System.out.println("round:" + round);
// System.out.println("rounds:" + rounds);
log.info("获取当前回合开箱结果 api /{} 参数fightId{} round{} rounds{}", "getFightRecord", fightId, round, rounds);
// 检查是否已经开始游戏
TtFight fight = apiFightService.getById(fightId);
if (ObjectUtil.isEmpty(fight)) return R.fail("不存在的对局");
if (fight.getStatus().equals(0)) return R.fail("对局尚未开始,请稍后重试。");
FightResultDataVO fightResultDataVO = apiFightService.getFightRecord(fightId, round, rounds);
if (ObjectUtil.isEmpty(fightResultDataVO)) {
return R.fail("对局fightId 没有产生结果");
}
return R.ok(fightResultDataVO);
}
@ApiOperation("获取对战排行榜")
@Anonymous
@PostMapping("/fightRanking")
public AjaxResult getFightRanking() {
// 创建日期格式化对象,指定日期格式
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
// 获取今天日期
Date today = new Date();
String formattedTodayDate = formatter.format(today);
List<ApiFightRankingVO> todayFightRanking = apiFightService.getFightRankingByDate(formattedTodayDate);
// 获取昨天日期
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -1);
Date yesterday = cal.getTime();
String formattedYesterdayDate = formatter.format(yesterday);
List<ApiFightRankingVO> yesterdayFightRanking = apiFightService.getFightRankingByDate(formattedYesterdayDate);
Map<String, List<ApiFightRankingVO>> map = new HashMap<>();
map.put("todayFightRanking", todayFightRanking);
map.put("yesterdayFightRanking", yesterdayFightRanking);
return AjaxResult.success(map);
}
@ApiOperation("十佳对战, 1:最大金额,2:最大增长,3:最大倍数")
@Anonymous
@PostMapping("/tenTopFight")
public AjaxResult tenTopFight(@RequestBody BaiJiaFightParam param) {
return AjaxResult.success(apiFightService.tenTopFight(param.getMaxType(), param.getModel(), param.getMinPrice(), param.getMaxPrice(), param.getKeyword()));
}
@ApiOperation("邀请机器人加入对战房间")
@PostMapping({"/joinFightRoomForRobot"})
public R joinFightRoomForRobot(@RequestBody FightDetailParam param) {
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) return checkLogin;
Integer fightId = param.getFightId();
String websiteMaintenance = this.sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
} else {
String fightMaintenance = this.sysConfigService.selectConfigByKey("fightMaintenance");
if ("1".equals(fightMaintenance)) {
return R.fail("开箱对战功能正在维护中......");
} else {
List<TtUser> robotIdList = new LambdaQueryChainWrapper<TtUser>(this.ttUserMapper)
.eq(TtUser::getUserType, UserType.ROBOT_USER.getCode()).list();
if (CollectionUtils.isEmpty(robotIdList)) {
return R.ok();
} else {
TtUser player = RandomUtil.choice(robotIdList);
R<Object> objectR = this.apiFightService.joinFight(fightId, player);
if (objectR.getCode() == 500) {
return objectR;
} else {
log.info("机器人{}加入对战{}", player.getUserId(), fightId);
R fight = this.apiFightService.seatrReady(fightId, player);
log.info("fight robot task fight is {}", fight == null ? "null" : fight.toString());
FightBoutData fightBoutData = new FightBoutData();
fightBoutData.setFightId(fightId);
fightBoutData.setBoutNum(1);
fightBoutData.setExpirationTime(16000);
String key = "fight_bout_data:fight_" + fightBoutData.getFightId();
this.redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS);
this.apiFightService.fightBegin(fightId, player);
return R.ok();
}
}
}
}
}
@ApiOperation("对战宝箱详情")
@Anonymous
@PostMapping("/boxData")
public R<TtBoxA> getFightBoxData(@RequestBody BoxIdRequest request) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
TtBoxA boxData = bindBoxService.getFightBoxData(request.getBoxId());
return R.ok(boxData);
}
}
@@ -0,0 +1,236 @@
package com.ruoyi.playingmethod.controller;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.admin.mapper.TtBoxMapper;
import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.other.FightBoutData;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.domain.vo.TtBoxOrnamentsDataVO;
import com.ruoyi.domain.vo.fight.FightBoxVO;
import com.ruoyi.domain.vo.fight.FightResultVO;
import com.ruoyi.playingmethod.contract.api.request.FightRoomInfoRequest;
import com.ruoyi.playingmethod.service.ApiFightService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 对战房间 HTTP 接口(原 WsFightRoom WebSocket 接口的 HTTP 替代)
*/
@Api(tags = "对战房间HTTP接口")
@Slf4j
@RestController
@RequestMapping("/api/fight/room")
public class ApiFightRoomController extends BaseController {
@Autowired
private ApiFightService apiFightService;
@Autowired
private TtBoxRecordsService boxRecordsService;
@Autowired
private TtBoxOrnamentsMapper boxOrnamentsMapper;
@Autowired
private TtBoxMapper ttBoxMapper;
@Autowired
private RedisCache redisCache;
@ApiOperation("获取对战房间信息/结果")
@PostMapping("/info")
public R<Object> info(@RequestBody FightRoomInfoRequest request) {
Long userId = getUserId();
Integer fightId = request.getFightId();
if (ObjectUtil.isEmpty(fightId)) {
return R.fail("对局ID不能为空");
}
TtFight fight = apiFightService.getById(fightId);
if (ObjectUtil.isEmpty(fight)) {
return R.fail("不存在的对局ID:" + fightId);
}
// 构建宝箱数据及饰品详情
Map<String, FightBoxVO> boxData = fight.getBoxData();
ArrayList<FightBoxVO> fightBoxVOList = new ArrayList<>();
if (boxData != null && !boxData.isEmpty()) {
boxData.keySet().stream()
.sorted(Comparator.comparingInt(Integer::parseInt))
.forEach(boxId -> {
List<TtBoxOrnamentsDataVO> boxOrnamentsVOS = boxOrnamentsMapper.selectTtBoxOrnamentsList(Integer.valueOf(boxId));
FightBoxVO fightBoxVO = JSONUtil.toBean(JSONUtil.toJsonStr(boxData.get(boxId)), FightBoxVO.class);
fightBoxVO.setOrnaments(boxOrnamentsVOS);
boxData.put(boxId, fightBoxVO);
fightBoxVOList.add(boxData.get(boxId));
});
LambdaQueryWrapper<TtBox> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(TtBox::getBoxId, boxData.keySet());
List<TtBox> boxes = ttBoxMapper.selectList(queryWrapper);
var boxMapObj = boxes.stream().filter(Objects::nonNull).collect(Collectors.groupingBy(TtBox::getBoxId));
Map<Integer, BigDecimal> boxMap = boxes.stream().filter(Objects::nonNull).collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice));
for (FightBoxVO vo : fightBoxVOList) {
vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), new BigDecimal(0)));
if (boxMapObj.get(vo.getBoxId()) != null) {
vo.setBoxName(boxMapObj.get(vo.getBoxId()).getFirst().getBoxName());
}
}
fight.getBoxData().keySet().forEach(boxId -> {
Object value = fight.getBoxData().get(boxId);
FightBoxVO a = null;
if (value instanceof LinkedHashMap) {
Map<String, Object> map = (LinkedHashMap) value;
a = new FightBoxVO(
map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null,
map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null,
map.get("boxImg01") != null ? map.get("boxImg01").toString() : null,
map.get("boxImg02") != null ? map.get("boxImg02").toString() : null);
a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO));
} else {
a = (FightBoxVO) value;
a.setPrice(boxMap.getOrDefault(a.getBoxId(), BigDecimal.ZERO));
}
fight.getBoxData().put(boxId, a);
});
}
// 如果游戏结束,发送结果集数据
if (fight.getStatus().equals(2) || fight.getStatus().equals(3)) {
LambdaQueryWrapper<TtBoxRecords> boxRecordsQuery = new LambdaQueryWrapper<>();
boxRecordsQuery.eq(TtBoxRecords::getFightId, fightId);
List<TtBoxRecords> allBoxRecords = boxRecordsService.list(boxRecordsQuery);
// 分离正常回合结果和赛后奖励
List<TtBoxRecords> roundResults = allBoxRecords.stream()
.filter(r -> r.getFightRoundNumber() != null && r.getFightRoundNumber() > 0)
.collect(Collectors.toList());
List<TtBoxRecords> endGameRecords = allBoxRecords.stream()
.filter(r -> r.getFightRoundNumber() == null || r.getFightRoundNumber() <= 0)
.collect(Collectors.toList());
// 胜利方获得的所有饰品
List<Integer> winnerIds = fight.getWinnerIds() != null ? fight.getWinnerIds() : new java.util.ArrayList<>();
List<TtBoxRecords> winnerPrize = allBoxRecords.stream()
.filter(r -> r.getHolderUserId() != null && winnerIds.contains(r.getHolderUserId()))
.filter(r -> r.getOrnamentId() != null)
.collect(Collectors.toList());
// 失败方安慰奖
List<TtBoxRecords> endGamePrize = endGameRecords.stream()
.filter(r -> r.getHolderUserId() != null && !winnerIds.contains(r.getHolderUserId()))
.collect(Collectors.toList());
// 计算饰品总价
BigDecimal totalOrnamentsPrice = allBoxRecords.stream()
.map(TtBoxRecords::getOrnamentsPrice)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
FightResultVO resultVO = FightResultVO.builder()
.currentRound(-1L)
.fight(fight)
.winnerIds(winnerIds)
.fightResult(roundResults)
.winnerPrize(winnerPrize)
.endGamePrize(endGamePrize)
.totalOrnamentsPrice(totalOrnamentsPrice)
.fightBoxVOList(fightBoxVOList)
.build();
log.info("用户{}查询房间{},对局已结束", userId, fightId);
return R.ok(resultVO, "对局已结束,对局最新信息");
} else if (fight.getStatus().equals(1)) {
LambdaQueryWrapper<TtBoxRecords> boxRecordsQuery = new LambdaQueryWrapper<>();
boxRecordsQuery.eq(TtBoxRecords::getFightId, fightId)
.eq(TtBoxRecords::getStatus, 0);
List<TtBoxRecords> allBoxRecords;
String key = "fight_result:fight_" + fightId;
allBoxRecords = redisCache.getCacheObject(key);
if (Objects.isNull(allBoxRecords)) {
allBoxRecords = boxRecordsService.list(boxRecordsQuery);
}
Integer currentRound = 0;
FightBoutData fightBoutData = redisCache.getCacheObject("fight_bout_data:fight_" + fightId);
if (!Objects.isNull(fightBoutData)) {
currentRound = fightBoutData.getBoutNum();
}
// 分离正常回合结果和赛后奖励
List<TtBoxRecords> roundResults = allBoxRecords.stream()
.filter(r -> r.getFightRoundNumber() != null && r.getFightRoundNumber() > 0)
.collect(Collectors.toList());
List<TtBoxRecords> endGameRecords = allBoxRecords.stream()
.filter(r -> r.getFightRoundNumber() == null || r.getFightRoundNumber() <= 0)
.collect(Collectors.toList());
// 胜利方获得的所有饰品
List<Integer> winnerIds = fight.getWinnerIds() != null ? fight.getWinnerIds() : new java.util.ArrayList<>();
List<TtBoxRecords> winnerPrize = allBoxRecords.stream()
.filter(r -> r.getHolderUserId() != null && winnerIds.contains(r.getHolderUserId()))
.filter(r -> r.getOrnamentId() != null)
.collect(Collectors.toList());
// 失败方安慰奖
List<TtBoxRecords> endGamePrize = endGameRecords.stream()
.filter(r -> r.getHolderUserId() != null && !winnerIds.contains(r.getHolderUserId()))
.collect(Collectors.toList());
// 计算饰品总价
BigDecimal totalOrnamentsPrice = allBoxRecords.stream()
.map(TtBoxRecords::getOrnamentsPrice)
.filter(Objects::nonNull)
.reduce(BigDecimal.ZERO, BigDecimal::add);
FightResultVO resultVO = FightResultVO.builder()
.currentRound(currentRound.longValue())
.fight(fight)
.winnerIds(winnerIds)
.fightResult(roundResults)
.winnerPrize(winnerPrize)
.endGamePrize(endGamePrize)
.totalOrnamentsPrice(totalOrnamentsPrice)
.fightBoxVOList(fightBoxVOList)
.build();
log.info("用户{}查询房间{},对局进行中", userId, fightId);
return R.ok(resultVO, "对局进行中,对局最新信息");
} else if (fight.getStatus().equals(0)) {
log.info("用户{}查询房间{},对局准备中", userId, fightId);
return R.ok(fight, "对局准备中,对战房间最新信息");
}
return R.fail("未知对局状态");
}
}
@@ -0,0 +1,178 @@
package com.ruoyi.playingmethod.controller;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.annotation.UpdateUserCache;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.upgrade.MyUpgradeRecordRequest;
import com.ruoyi.domain.dto.upgrade.UpgradeCondition;
import com.ruoyi.domain.dto.upgrade.UpgradeRecordPageRequest;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.other.ApiLuckyUpgradeBody;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.UpgradeBodyA;
import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO;
import com.ruoyi.domain.vo.ApiLuckyUpgradeRecentPullVO;
import com.ruoyi.playingmethod.contract.api.request.TenTopUpgradeRequest;
import com.ruoyi.playingmethod.service.ApiLuckyUpgradeService;
import com.ruoyi.playingmethod.service.ApiUpgradeRecordService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.service.ApiUserPackSackService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
@Api(tags = "幸运升级模式")
@RestController
@RequestMapping("/api/luckyUpgrade")
public class ApiLuckyUpgradeController extends BaseController {
@Autowired
private ApiUserPackSackService userPackSackService;
@Autowired
private ApiUpgradeRecordService upgradeRecordService;
private final TtUserService userService;
private final ApiLuckyUpgradeService apiLuckyUpgradeService;
private final ISysConfigService sysConfigService;
public ApiLuckyUpgradeController(TtUserService userService,
ApiLuckyUpgradeService apiLuckyUpgradeService,
ISysConfigService sysConfigService) {
this.userService = userService;
this.apiLuckyUpgradeService = apiLuckyUpgradeService;
this.sysConfigService = sysConfigService;
}
public R checkLogin(){
Long userId;
try {
userId = getUserId();
if (ObjectUtil.isEmpty(userId)) AjaxResult.error(401,"登录过期,请重新登录。");
return R.ok(userId);
}catch (Exception e){
return R.fail(401,"登录过期,请重新登录。");
}
}
@ApiOperation("获取可升级的饰品列表")
@Anonymous
@PostMapping("/getOrnamentsList")
public R<List<ApiLuckyOrnamentsDataVO>> getOrnamentsList(@RequestBody ApiLuckyUpgradeBody apiLuckyUpgradeBody){
// 幸运值按用户类型区分:已登录取真实类型,未登录默认普通用户
String userType = UserType.COMMON_USER.getCode();
R login = checkLogin();
if (login.getCode().equals(200) && login.getData() != null) {
TtUser user = userService.getById(((Long) login.getData()).intValue());
if (user != null) {
// 白号按 06 读取自己的幸运值(兼容历史数据只改了 is_blank_account、没改 user_type 的行)
if (Integer.valueOf(1).equals(user.getIsBlankAccount())) {
userType = UserType.BLANK_ACCOUNT.getCode();
} else if (ObjectUtil.isNotEmpty(user.getUserType())) {
userType = user.getUserType();
}
}
}
apiLuckyUpgradeBody.setUserType(userType);
List<ApiLuckyOrnamentsDataVO> list = apiLuckyUpgradeService.getOrnamentsList(apiLuckyUpgradeBody);
return R.ok(list);
}
@ApiOperation("幸运升级")
@UpdateUserCache
@PostMapping("/upgrade")
public R upgrade(@RequestBody @Validated UpgradeBodyA upgradeParam){
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......");
String luckyUpgradeMaintenance = sysConfigService.selectConfigByKey("luckyUpgradeMaintenance");
if ("1".equals(luckyUpgradeMaintenance)) return R.fail("幸运升级维护中......");
R check = checkLogin();
if (!check.getCode().equals(200)) return R.fail(401,"登录过期,请重新登录。");
Integer userId = ((Long)check.getData()).intValue();
TtUser ttUser = userService.getById(userId);
// 余额校验(与本次升级所需金额比较)与充值记录校验均由 service 内部 upgradeCheck 统一处理。
return apiLuckyUpgradeService.upgrade(ttUser, upgradeParam);
}
@ApiOperation("获取幸运升级历史记录")
@PostMapping("/getUpgradeRecord")
public R getUpgradeRecord(@RequestBody @Validated UpgradeCondition param){
return upgradeRecordService.historyDetail(param);
}
@ApiOperation("获取幸运升级十佳记录")
@PostMapping("/tenTopUpgradeRecord")
public R tenTopUpgradeRecord(@RequestBody(required = false) TenTopUpgradeRequest request){
Boolean isVictory = (request == null) ? null : request.getIsVictory();
R result = upgradeRecordService.tenTopDetail(isVictory);
// 确保结果格式一致
if (!result.getCode().equals(200)) {
return result;
}
Object data = result.getData();
if (data != null && CollectionUtils.isEmpty((List<?>)data)) {
result.setData(new ArrayList<>()); // 确保返回空列表而非 null
}
return result;
}
@ApiOperation("获取个人幸运升级历史记录列表")
@PostMapping("/myHistoryRecord")
public R myHistoryRecord(@RequestBody(required = false) @Validated MyUpgradeRecordRequest request){
R check = checkLogin();
if (!check.getCode().equals(200)) {
return R.fail(401, "登录过期,请重新登录。");
}
Integer userId = ((Long) check.getData()).intValue();
if (request == null) {
request = new MyUpgradeRecordRequest();
}
return upgradeRecordService.myHistoryRecord(userId, request);
}
@ApiOperation("获取当前饰品最近100条拉取记录(含用户头像、概率、饰品全名、磨损度、品质图、图片、价格)")
@PostMapping("/recentPulls")
public R<List<ApiLuckyUpgradeRecentPullVO>> recentPulls(@RequestBody @Validated UpgradeCondition param) {
if (param.getUpgradeRecordId() == null) {
return R.fail("upgradeRecordId(升级饰品ID)不能为空");
}
List<ApiLuckyUpgradeRecentPullVO> list = upgradeRecordService.getRecentPulls(param.getUpgradeRecordId().intValue());
return R.ok(list);
}
@ApiOperation("幸运升级记录分页(scope: 0全服/1我的;itemScope: 0全部饰品/1当前饰品;均需登录)")
@PostMapping("/recordPage")
public R recordPage(@RequestBody @Validated UpgradeRecordPageRequest req) {
// 记录页无论全服/我的均需登录
R check = checkLogin();
if (!check.getCode().equals(200) || check.getData() == null) {
return R.fail(401, "登录过期,请重新登录。");
}
Integer userId = ((Long) check.getData()).intValue();
// scope=1(我的) 才按 userId 过滤;scope=0(全服) 不过滤用户
Integer filterUserId = Integer.valueOf(1).equals(req.getScope()) ? userId : null;
// itemScope=1(当前饰品) 时 upgradeId 必填
if (Integer.valueOf(1).equals(req.getItemScope()) && req.getUpgradeId() == null) {
return R.fail("当前饰品模式下 upgradeId(可升级饰品ID) 不能为空");
}
return upgradeRecordService.recordPage(filterUserId, req);
}
}
@@ -0,0 +1,102 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.common.annotation.NoRepeatSubmit;
import com.ruoyi.common.annotation.UserPermission;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.domain.other.TtRedPack;
import com.ruoyi.domain.other.TtRedPacketRecord;
import com.ruoyi.playingmethod.contract.api.request.GetRedPackByKeyRequest;
import com.ruoyi.playingmethod.contract.api.request.GetRedPackDetailRequest;
import com.ruoyi.playingmethod.contract.api.request.GetRedPackListRequest;
import com.ruoyi.playingmethod.contract.api.request.GetRedPackRequest;
import com.ruoyi.playingmethod.service.ApiRedPackService;
import com.ruoyi.thirdparty.wechat.domain.PtLoginUser;
import com.ruoyi.thirdparty.wechat.service.ApiTokenService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
/**
* 不用这个api领红包了
*/
@Deprecated
@RestController
@RequestMapping("/api/redPackData")
@Api(tags = "红包")
public class ApiRedPackContoller extends BaseController {
@Autowired
private ApiTokenService tokenService;
@Autowired
private ApiRedPackService redPackService;
// 已停用(无调用方)
// @PostMapping("/getRedPackList")
@UserPermission
@NoRepeatSubmit
@ApiOperation(value = "获取红包列表")
public TableDataInfo getRedPackList(HttpServletRequest request, @RequestBody GetRedPackListRequest getRedPackListRequest) {
PtLoginUser user = tokenService.getLoginUser(request);
startPage();
List<TtRedPack> redPacks = redPackService.getRedPackList(user, getRedPackListRequest.getType());
return getDataTable(redPacks);
}
@Deprecated
// 已停用(无调用方)
// @PostMapping("/getRedPack")
@UserPermission
@NoRepeatSubmit
@ApiOperation(value = "领取红包")
public AjaxResult getRedPack(@RequestBody GetRedPackRequest request) {
Long userId = getUserId();
return redPackService.getRedPack(userId, request.getRedPackId());
}
// 已停用:口令兑换请走 POST /api/bonus/receiveRedPacket(唯一的正规入口)
// @PostMapping("/getRedPackByKey")
@UserPermission
@NoRepeatSubmit
@ApiOperation(value = "根据红包口令兑换红包")
public AjaxResult getRedPackByKey(@RequestBody GetRedPackByKeyRequest request) {
LoginUser loginUser = getLoginUser();
if (request.getRedPackKey() == null || "".equals(request.getRedPackKey())) {
return AjaxResult.error("没有填写红包口令");
}
return redPackService.getRedPackByKey(loginUser, request.getRedPackKey());
}
// 已停用(无调用方)
// @PostMapping("/getActivityDataHis")
@UserPermission
@NoRepeatSubmit
@ApiOperation(value = "查询参与的红包活动记录")
public TableDataInfo getBoxDataHis(HttpServletRequest request) {
PtLoginUser user = tokenService.getLoginUser(request);
startPage();
List<TtRedPacketRecord> boxRecords = redPackService.getActivityDataHis(user);
return getDataTable(boxRecords);
}
// 已停用(无调用方)
// @PostMapping("/getRedPackDataDetail")
@UserPermission
@NoRepeatSubmit
@ApiOperation(value = "查询历史记录")
public AjaxResult getRedPackDataDetail(HttpServletRequest request, @RequestBody GetRedPackDetailRequest getRedPackDetailRequest) {
PtLoginUser user = tokenService.getLoginUser(request);
return redPackService.getRedPackDataDetail(user, getRedPackDetailRequest.getRecordId());
}
}
@@ -0,0 +1,42 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.common.annotation.UserPermission;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.framework.web.service.TokenService;
import com.ruoyi.playingmethod.model.body.SynthesizeRequest;
import com.ruoyi.playingmethod.service.IApiReplacementRecordService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import jakarta.servlet.http.HttpServletRequest;
/**
* 汰换记录Controller
*
* @author junhai
* @date 2023-09-10
*/
@RestController
@RequestMapping("/api/replacementRecord")
@Api(tags = "汰换记录")
public class ApiReplacementRecordController extends BaseController {
@Autowired
private IApiReplacementRecordService ttReplacementRecordService;
@PostMapping("/synthesizeItems")
@UserPermission
@ApiOperation("汰换饰品")
public AjaxResult synthesizeItems(@RequestBody SynthesizeRequest synthesizeRequest) {
LoginUser loginUser = getLoginUser();
return ttReplacementRecordService.synthesizeItems(loginUser, synthesizeRequest.getBoIds());
}
}
@@ -0,0 +1,193 @@
package com.ruoyi.playingmethod.controller;
import cn.hutool.core.util.ObjectUtil;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig;
import com.ruoyi.domain.dto.roll.GetRollOpenPrizeParam;
import com.ruoyi.domain.dto.roll.GetRollPlayersParam;
import com.ruoyi.domain.dto.roll.GetRollPrizePool;
import com.ruoyi.domain.entity.roll.TtRoll;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.annotation.RepeatSubmit;
import com.ruoyi.common.annotation.Anonymous;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.PageDataInfo;
import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO;
import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO;
import com.ruoyi.domain.vo.roll.RollUserVO;
import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest;
import com.ruoyi.playingmethod.contract.api.request.RollIdRequest;
import com.ruoyi.playingmethod.service.ApiRollService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.domain.vo.RollDetailsDataVO;
import com.ruoyi.domain.vo.RollListDataVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiModelProperty;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import jakarta.validation.constraints.Max;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.List;
@Api(tags = "ROLL房模式")
@RestController
@RequestMapping("/api/roll")
public class ApiRollController extends BaseController {
private final ISysConfigService sysConfigService;
private final TtUserService userService;
private final ApiRollService apiRollService;
public ApiRollController(ISysConfigService sysConfigService,
TtUserService userService,
ApiRollService apiRollService) {
this.sysConfigService = sysConfigService;
this.userService = userService;
this.apiRollService = apiRollService;
}
@AllArgsConstructor
@NoArgsConstructor
@Data
public static class JoinRollParam {
@NotNull(message = "roll房id不能为空。")
private Integer rollId;
private String rollPassword;
}
@ApiOperation("加入 ROLL 房")
@RepeatSubmit(message = "操作过于频繁,请稍后重试!")
@PostMapping("/joinRoll")
public R joinRoll(@RequestBody @Validated JoinRollParam param) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......");
String rollMaintenance = sysConfigService.selectConfigByKey("rollMaintenance");
if ("1".equals(rollMaintenance)) return R.fail("Roll 房功能正在维护中......");
Long userId = getUserId();
if (ObjectUtil.isEmpty(userId)) return R.fail(401, "登录过期,请重新登录。");
TtUser player = userService.getById(userId);
if (ObjectUtil.isEmpty(player)) return R.fail("异常的用户状态。");
// 新增:检查用户是否被封禁或已删除
if ("1".equals(player.getStatus())) return R.fail("您的账号已被封禁,无法参与 Roll 房。");
if (!"0".equals(player.getDelFlag())) return R.fail("您的账号已被删除,无法参与 Roll 房。");
return apiRollService.joinRoll(param, player);
// return StringUtils.isEmpty(msg) ? R.ok("成功加入Roll房!") : R.fail(msg, "加入失败!");
}
@ApiOperation("获取ROLL房列表")
@Anonymous
@PostMapping("/getRollList")
public TableDataInfo getRollList(@RequestBody GetRollListParamRequest param) {
// 支持免登录访问,登录用户可获取个性化数据
try {
Long userId = getUserId();
if (userId != null && param.getRollStatus() != null && (param.getRollStatus() == 1 || param.getRollStatus() == 2)) {
param.setUserId(userId.intValue());
}
} catch (Exception ignore) {
// 未登录时忽略 userId 设置
}
startPage();
List<RollListDataVO> list = apiRollService.getRollList(param);
return getDataTable(list);
}
/**
* 我参与的ROLL房列表
*/
// @GetMapping("/getMyPartRollList")
public PageDataInfo<RollListDataVO> getMyPartRollList(@RequestBody GetRollListParamRequest param) {
startPage();
List<RollListDataVO> list = apiRollService.getRollList(param);
return getPageData(list);
}
@ApiOperation("获取ROLL房详情")
@PostMapping("/getRollDetails")
public R<RollDetailsDataVO> getRollDetails(@RequestBody RollIdRequest request) {
Long userId = getUserId();
return apiRollService.getRollDetails(request.getRollId(), userId);
}
@ApiOperation("获取ROLL房参与人员详情")
@PostMapping("/getRollPlayers")
public R<List<RollUserVO>> getRollPlayers(@RequestBody @Validated GetRollPlayersParam param) {
return apiRollService.getRollPlayers(param);
}
@ApiOperation("获取ROLL房奖池详情")
@PostMapping("/getRollPrizePool")
public R<List<RollJackpotOrnamentsVO>> getRollPrizePool(@RequestBody @Validated GetRollPrizePool param) {
return apiRollService.getRollPrizePool(param);
}
@ApiOperation("获取ROLL开奖详情")
@PostMapping("/getRollOpenPrize")
public R<List<TtBoxRecordsVO>> getRollOpenPrize(@RequestBody @Validated GetRollOpenPrizeParam param) {
return apiRollService.getRollOpenPrize(param);
}
@ApiOperation("开奖测试,正式环境删除")
@PostMapping("/endRoll")
public R endRoll(@RequestBody RollIdRequest request){
// TODO: 移除或添加管理员权限校验
return apiRollService.endROLL(request.getRollId());
}
// @Autowired
private RabbitTemplate rabbitTemplate;
// @GetMapping("/endRoll/{rollId}")
public String mqTest(@PathVariable("rollId") Integer rollId) {
// Calendar c = Calendar.getInstance();
// long timeInMillis = c.getTimeInMillis();
// c.add(Calendar.SECOND, 3);
// long timeInMillis1 = c.getTimeInMillis();
// long l = timeInMillis1 - timeInMillis;
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration("3000"); // 消息的过期属性,单位 ms
Message message = new Message("这条消息 4 秒后过期".getBytes(), messageProperties);
rabbitTemplate.convertAndSend(
DelayedQueueConfig.OPEN_ROLL_EXCHANGE,
DelayedQueueConfig.OPEN_ROLL_KEY,
message
);
return "1";
}
@ApiOperation("roll房间号搜索")
@PostMapping("/searchRollId")
public TableDataInfo searchRoll(@RequestBody RollIdRequest request){
GetRollListParamRequest param = new GetRollListParamRequest();
param.setRollId(request.getRollId());
List<RollListDataVO> list = apiRollService.getRollList(param);
TableDataInfo rspData = new TableDataInfo();
rspData.setCode(200);
rspData.setMsg("查询成功");
rspData.setRows(list);
rspData.setTotal(list.size());
return rspData;
}
}
@@ -0,0 +1,52 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.playingmethod.entity.TtAttendanceRecord;
import com.ruoyi.playingmethod.service.IApiAttendanceRecordService;
import io.swagger.annotations.Api;
import org.apache.poi.ss.formula.functions.T;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Api(tags = "签到记录")
@RestController
@RequestMapping("/api/attendanceRecord")
public class ApiTtAttendanceRecordController extends BaseController {
private final IApiAttendanceRecordService iApiAttendanceRecordService;
public ApiTtAttendanceRecordController(IApiAttendanceRecordService iApiAttendanceRecordService) {
this.iApiAttendanceRecordService = iApiAttendanceRecordService;
}
/**
* 签到
*/
// @GetMapping("/attendance")
public R<String> attendance() {
System.out.println("==================签到=================");
TtAttendanceRecord ttAttendanceRecord = iApiAttendanceRecordService.selectByUid(getUserId().intValue());
if (ttAttendanceRecord == null){
// 增加签到记录
iApiAttendanceRecordService.insert(getUserId().intValue());
return R.ok("签到成功");
}
// List<UserPackSackDataVO> userCompoundRecord = iApiAttendanceRecordService.getUserCompoundRecord(getUserId().intValue());
return R.fail("签到失败,一天只能签到一次");
}
/**
* 七天签到记录
*/
// @GetMapping("/sevenAttendance")
public R<List<TtAttendanceRecord>> sevenAttendance() {
System.out.println("==================七天签到=================");
// TtAttendanceRecord ttAttendanceRecord = iApiAttendanceRecordService.selectByUid(getUserId().intValue());
List<TtAttendanceRecord> ttAttendanceRecordList = iApiAttendanceRecordService.selectSevenAttendance(getUserId().intValue());
return R.ok(ttAttendanceRecordList);
}
}
@@ -0,0 +1,48 @@
package com.ruoyi.playingmethod.controller;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.admin.service.TtUserAvatarService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.other.TtUserAvatar;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.stream.Collectors;
@Api(tags = "用户头像")
@RestController
@RequestMapping("/api/userAvatar")
public class ApiUserAvatarController extends BaseController {
private final TtUserAvatarService userAvatarService;
public ApiUserAvatarController(TtUserAvatarService userAvatarService) {
this.userAvatarService = userAvatarService;
}
@ApiOperation("用户头像列表")
@PostMapping("/list")
public R<List<TtUserAvatar>> list() {
LambdaQueryWrapper<TtUserAvatar> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TtUserAvatar::getStatus, "0")
.orderByAsc(TtUserAvatar::getSort);
List<TtUserAvatar> list = userAvatarService.list(wrapper);
return R.ok(list);
}
@ApiOperation("获取头像图片资源路径列表")
@PostMapping("/images")
public R<List<String>> getAvatarImages() {
LambdaQueryWrapper<TtUserAvatar> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TtUserAvatar::getStatus, "0")
.orderByAsc(TtUserAvatar::getSort);
List<String> avatarUrls = userAvatarService.list(wrapper)
.stream()
.map(TtUserAvatar::getAvatar)
.collect(Collectors.toList());
return R.ok(avatarUrls);
}
}
@@ -0,0 +1,117 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.service.TtWelfareService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.domain.entity.TtWelfareBoxInventory;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtWelfare;
import com.ruoyi.domain.vo.OpenBoxVO;
import com.ruoyi.playingmethod.contract.api.request.ClaimWelfareRequest;
import com.ruoyi.playingmethod.contract.api.request.OpenWelfareBoxRequest;
import com.ruoyi.playingmethod.model.ApiWelfare;
import com.ruoyi.playingmethod.service.ApiWelfareService;
import com.ruoyi.system.service.ISysConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.*;
@Api(tags = "福利")
@RestController
@RequestMapping("/api/welfare")
public class ApiWelfareController extends BaseController {
@Autowired
private ApiWelfareService apiWelfareService;
@Autowired
private TtWelfareService ttWelfareService;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private TtUserService userService;
@ApiOperation("获取福利列表")
@ApiResponse(code = 200, message = "eligible【是否具备领取条件(0否 1是)】" +
"<br>claimStatus【领取状态(0未领取 1已领取)】")
@PostMapping("/getWelfareList")
public TableDataInfo getWelfareList() {
startPage();
Long userId = getUserId();
List<ApiWelfare> list = apiWelfareService.getWelfareList(userId);
return getDataTable(list);
}
@ApiOperation("领取福利")
@PostMapping("/claimWelfare")
public AjaxResult claimWelfare(@RequestBody ClaimWelfareRequest request) {
Long userId = getUserId();
// 检查福利是否存在
TtWelfare ttWelfare = ttWelfareService.selectTtWelfareByWelfareId(request.getWelfareId());
if (Objects.isNull(ttWelfare)) {
return error("福利不存在");
}
// 检查是否尚未具备领取条件
if (apiWelfareService.checkNotEligible(request.getWelfareId(), userId)) {
return AjaxResult.error("尚未达到领取条件");
}
// 检查是否已领取
if (apiWelfareService.checkClaimed(request.getWelfareId(), userId)) {
return AjaxResult.error("该福利已被领取");
}
// 进入开箱流程
String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance");
if ("1".equals(bindBoxMaintenance)) {
return AjaxResult.error("盲盒开箱功能正在维护中......");
}
TtUser ttUser = userService.getById(getUserId());
if ("0".equals(ttUser.getIsRealCheck())) {
return AjaxResult.realNameRequired();
}
OpenBoxVO openBoxVO = apiWelfareService.claimWelfare(request.getWelfareId(), userId);
return AjaxResult.success(openBoxVO);
}
@ApiOperation("每日领取福利箱子")
@PostMapping("/claimDailyBox")
public R<?> claimDailyBox() {
Long userId = getUserId();
try {
TtWelfareBoxInventory inventory = apiWelfareService.claimDailyBox(userId);
return R.ok(inventory);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
}
@ApiOperation("批量开启福利箱子(1-15个)")
@PostMapping("/openBoxes")
public R<?> openBoxes(@RequestBody @Validated OpenWelfareBoxRequest request) {
Long userId = getUserId();
String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance");
if ("1".equals(bindBoxMaintenance)) {
return R.fail("盲盒开箱功能正在维护中......");
}
TtUser ttUser = userService.getById(userId);
if ("0".equals(ttUser.getIsRealCheck())) {
return R.realNameRequired();
}
try {
List<OpenBoxVO> results = apiWelfareService.openWelfareBoxes(
request.getBoxInventoryIds(), userId);
return R.ok(results);
} catch (RuntimeException e) {
return R.fail(e.getMessage());
}
}
}
@@ -0,0 +1,83 @@
package com.ruoyi.playingmethod.controller;
import com.ruoyi.admin.service.ITtWelfareMonthlyRechargesService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.controller.BaseController;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.page.TableDataInfo;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtWelfareMonthlyRecharges;
import com.ruoyi.domain.vo.OpenBoxVO;
import com.ruoyi.playingmethod.contract.api.request.ClaimWelfareRequest;
import com.ruoyi.playingmethod.model.ApiWelfare;
import com.ruoyi.playingmethod.model.vo.ApiWelfareMonthlyRechargesVO;
import com.ruoyi.playingmethod.service.ApiWelfareMonthlyRechargesService;
import com.ruoyi.system.service.ISysConfigService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Objects;
@Api(tags = "每月充值福利")
@RestController
@RequestMapping("/api/welfare/monthlyRecharges")
public class ApiWelfareMonthlyRechargesController extends BaseController {
@Autowired
private ApiWelfareMonthlyRechargesService apiWelfareMonthlyRechargesService;
@Autowired
private ITtWelfareMonthlyRechargesService ttWelfareMonthlyRechargesService;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private TtUserService userService;
@ApiOperation("获取每月充值福利列表")
@ApiResponse(code = 200, message = "eligible【是否具备领取条件(0否 1是)】" +
"<br>claimStatus【领取状态(0未领取 1已领取)】")
@PostMapping("/list")
public TableDataInfo getWelfareList() {
startPage();
Long userId = SecurityUtils.getUserId();
List<ApiWelfareMonthlyRechargesVO> list = apiWelfareMonthlyRechargesService.getWelfareList(userId);
return getDataTable(list);
}
@ApiOperation("领取福利")
@PostMapping("/claimWelfare")
public AjaxResult claimWelfare(@RequestBody ClaimWelfareRequest request) {
Long userId = SecurityUtils.getUserId();
// 检查福利是否存在
TtWelfareMonthlyRecharges ttWelfareMonthlyRecharges = ttWelfareMonthlyRechargesService.selectTtWelfareMonthlyRechargesById(request.getWelfareId());
if (Objects.isNull(ttWelfareMonthlyRecharges)) {
return error("福利不存在");
}
// 检查是否尚未具备领取条件
if (!apiWelfareMonthlyRechargesService.checkEligible(request.getWelfareId(), userId)) {
return AjaxResult.error("尚未达到领取条件");
}
// 检查是否已领取
if (apiWelfareMonthlyRechargesService.checkClaimed(request.getWelfareId(), userId)) {
return AjaxResult.error("该福利已被领取");
}
// 进入开箱流程
String bindBoxMaintenance = sysConfigService.selectConfigByKey("bindBoxMaintenance");
if ("1".equals(bindBoxMaintenance)) {
return AjaxResult.error("盲盒开箱功能正在维护中......");
}
TtUser ttUser = userService.getById(getUserId());
if ("0".equals(ttUser.getIsRealCheck())) {
return AjaxResult.realNameRequired();
}
OpenBoxVO openBoxVO = apiWelfareMonthlyRechargesService.claimWelfare(request.getWelfareId(), userId);
return AjaxResult.success(openBoxVO);
}
}
@@ -0,0 +1,7 @@
package com.ruoyi.playingmethod.customException;
public class CreateFightException extends RuntimeException{
public CreateFightException(String msg){
super(msg);
}
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.customException;
// websocket用户重复连接异常
public class WsRepetitiontException extends RuntimeException{
public WsRepetitiontException(String msg){
super(msg);
}
}
@@ -0,0 +1,20 @@
package com.ruoyi.playingmethod.domain.vo;
import com.ruoyi.domain.other.TtRobotFightGroupBox;
import lombok.Data;
import java.util.List;
@Data
public class ApiRobotFightGroupVO {
private Integer groupId;
private String groupName;
private Integer seatNum;
private String model;
private List<TtRobotFightGroupBox> ttRobotFightGroupBoxList;
}
@@ -0,0 +1,13 @@
package com.ruoyi.playingmethod.domain.vo;
import lombok.Data;
@Data
public class CompValueVO {
/** 当前补偿值 */
private Integer currentCompValue;
/** 设定补偿值 */
private Integer desiredCompValue;
}
@@ -0,0 +1,47 @@
package com.ruoyi.playingmethod.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.ruoyi.common.annotation.Excel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.util.Date;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Builder
public class TtAttendanceRecord {
private static final long serialVersionUID = 1L;
/** */
private Integer id;
/** 用户ID */
@Excel(name = "用户ID")
private Integer userId;
/** 创建时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date createTime;
/** 更新时间 */
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date updateTime;
/** 获取金币数 */
@Excel(name = "获取金币数")
private Integer coin;
@Excel(name = "是否删除")
private String delFlag;
private String isStatus = "0";
}
@@ -0,0 +1,19 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.playingmethod.entity.TtAttendanceRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
@Mapper
public interface ApiAttendanceRecordMapper {
TtAttendanceRecord selectByUid(@Param("uid") Integer uid);
int insertAttendanceRecord(TtAttendanceRecord ttAttendanceRecord);
TtAttendanceRecord selectSevenAttendance(@Param("uid") Integer uid,@Param("createTime") String createTime);
}
@@ -0,0 +1,7 @@
package com.ruoyi.playingmethod.mapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApiBattleRoyaleMapper {
}
@@ -0,0 +1,38 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.domain.other.TtBoxUser;
import com.ruoyi.domain.other.TtBoxA;
import com.ruoyi.domain.other.TtOrnamentsA;
import com.ruoyi.domain.other.TtBoxLevelA;
import com.ruoyi.playingmethod.controller.ApiBindBoxController;
import com.ruoyi.playingmethod.domain.vo.CompValueVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiBindBoxMapper {
TtBoxA getBoxData(Integer boxId);
List<TtOrnamentsA> getBoxOrnamentsList(Integer boxId);
/** 对战宝箱饰品列表,概率使用 robot_odds */
List<TtOrnamentsA> getFightBoxOrnamentsList(Integer boxId);
List<TtBoxA> getBoxList(
@Param("boxTypeId") Integer boxTypeId,
@Param("homeFlag") String homeFlag,
@Param("boxType") String boxType,
@Param("isFight") String isFight
);
TtOrnamentsA getOrnamentsData(@Param("boxId") Integer boxId, @Param("ornamentsId") Integer ornamentsId, @Param("id") Long id);
// List<TtBoxUser> getBindBoxHistory(ApiBindBoxController.boxHistoryParam param);
List<TtBoxLevelA> getProbabilityDistribution(Integer boxId);
TtOrnamentsA ornamentsInfo(@Param("boxId") Integer boxId,@Param("ornamentId") String ornamentId);
}
@@ -0,0 +1,10 @@
package com.ruoyi.playingmethod.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.other.TtBoxOrnaments;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApiBoxOrnamentsMapper extends BaseMapper<TtBoxOrnaments> {
}
@@ -0,0 +1,13 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiCompoundMapper {
UserPackSackDataVO selectCompoundDataById(Long packSackId);
List<UserPackSackDataVO> selectCompoundRecordByUserId(Integer userId);
}
@@ -0,0 +1,49 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.domain.other.TtBoxA;
import com.ruoyi.domain.vo.ApiFightListDataVO;
import com.ruoyi.domain.vo.JoinFightUserDataVO;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO;
import com.ruoyi.playingmethod.model.vo.ApiFightTenTopVo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.Date;
import java.util.List;
@Mapper
public interface ApiFightMapper {
JoinFightUserDataVO selectJoinFightUserData(Integer fightUserId);
List<UserPackSackDataVO> selectOrnamentsDataListByUserIdAndFightId(@Param("userId") Integer userId, @Param("fightId") Integer fightId);
List<ApiFightListDataVO> getFightList(@Param("model") String model, @Param("status") String status, @Param("userId") Integer userId, @Param("fightId") Integer fightId);
List<TtBoxA> getFightBoxList(Integer boxTypeId);
/**
* 获取指定日期的对战排行榜
*/
List<ApiFightRankingVO> getFightRankingByDate(String date);
/**
* 盲盒对战十佳对战(keyword 仅支持房间号 fightId 精确查询)
*/
List<ApiFightTenTopVo> tenTopFight(@Param("startDate") String startDate, @Param("endDate") String endDate, @Param("maxType") Integer maxType, @Param("model") String model, @Param("minPrice") Integer minPrice, @Param("maxPrice") Integer maxPrice, @Param("fightId") Integer fightId);
/**
* 查看对战开箱出来的总金额
*/
List<ApiFightTenTopVo> fightOrnamentsPrice(@Param("fightIds") List<Integer> fightIds);
/**
* 查询房主在对战中获得饰品总价值(按 fightId 批量)
*/
List<ApiFightTenTopVo> ownerEarnPrice(@Param("fightIds") List<Integer> fightIds);
/**
* 查询每个持有者在对战中获得的饰品总价值(按 fightId + holder_user_id 批量)
*/
List<ApiFightTenTopVo> holderOrnamentsPrice(@Param("fightIds") List<Integer> fightIds);
}
@@ -0,0 +1,17 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.domain.dto.upgrade.UpgradeCondition;
import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO;
import com.ruoyi.domain.other.ApiLuckyUpgradeBody;
import com.ruoyi.domain.vo.ApiLuckyUpgradeRecordDataVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiLuckyUpgradeMapper {
List<ApiLuckyOrnamentsDataVO> getOrnamentsList(ApiLuckyUpgradeBody apiLuckyUpgradeBody);
List<ApiLuckyUpgradeRecordDataVO> getUpgradeRecord(UpgradeCondition param);
}
@@ -0,0 +1,80 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.other.TtReplacementRecord;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
/**
* 汰换记录Mapper接口
*
* @author junhai
* @date 2023-09-10
*/
public interface ApiReplacementRecordMapper {
/**
* 查询汰换记录
*
* @param id 汰换记录主键
* @return 汰换记录
*/
public TtReplacementRecord selectTtReplacementRecordById(Long id);
/**
* 查询汰换记录列表
*
* @param ttReplacementRecord 汰换记录
* @return 汰换记录集合
*/
public List<TtReplacementRecord> selectTtReplacementRecordList(TtReplacementRecord ttReplacementRecord);
/**
* 新增汰换记录
*
* @param ttReplacementRecord 汰换记录
* @return 结果
*/
public int insertTtReplacementRecord(TtReplacementRecord ttReplacementRecord);
/**
* 修改汰换记录
*
* @param ttReplacementRecord 汰换记录
* @return 结果
*/
public int updateTtReplacementRecord(TtReplacementRecord ttReplacementRecord);
/**
* 删除汰换记录
*
* @param id 汰换记录主键
* @return 结果
*/
public int deleteTtReplacementRecordById(Long id);
/**
* 批量删除汰换记录
*
* @param ids 需要删除的数据主键集合
* @return 结果
*/
public int deleteTtReplacementRecordByIds(Long[] ids);
/**
* 根据以下参数进行饰品的查询
*
* @param map
* @return
*/
List<TtOrnament> findByPriceRange(Map<String, BigDecimal> map);
/**
* 查询背包饰品
*/
List<UserPackSackDataVO> selectUserPackSack(@Param("userId") Integer userId,
@Param("itemIds") List<Long> itemIds);
}
@@ -0,0 +1,12 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.playingmethod.domain.vo.ApiRobotFightGroupVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiRobotFightGroupMapper {
public List<ApiRobotFightGroupVO> selectApiRobotFightGroupBoxVOList();
}
@@ -0,0 +1,19 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.domain.other.JackpotData;
import com.ruoyi.domain.vo.RollDetailsDataVO;
import com.ruoyi.domain.vo.RollListDataVO;
import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface ApiRollMapper {
List<RollListDataVO> getRollList(GetRollListParamRequest param);
RollDetailsDataVO getRollDetails(Integer rollId);
List<JackpotData> getJackpotData(Integer jackpotId);
}
@@ -0,0 +1,40 @@
package com.ruoyi.playingmethod.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.other.TtUpgradeRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface ApiTtUserBlendErcashMapper extends BaseMapper<TtUserBlendErcash> {
/**
* 查询单用户指定 source/type 的累计金额(主表 + 归档表 UNION ALL)
*/
@Select("SELECT IFNULL(SUM(ABS(tube.amount)), 0) FROM (" +
"SELECT amount FROM tt_user_blend_ercash WHERE user_id = #{userId} AND source = #{source} AND type = #{type} " +
"UNION ALL " +
"SELECT amount FROM tt_user_blend_ercash_archive WHERE user_id = #{userId} AND source = #{source} AND type = #{type}" +
") tube")
BigDecimal sumRechargeWithArchive(@Param("userId") Integer userId,
@Param("source") Integer source,
@Param("type") Integer type);
/**
* 批量查询指定用户列表的累计金额(兼容 sumAmountByUserIdsAndSource 接口)
*/
default BigDecimal sumAmountByUserIdsAndSource(List<Integer> userIds, Integer source, Integer type) {
if (userIds == null || userIds.isEmpty()) return BigDecimal.ZERO;
BigDecimal total = BigDecimal.ZERO;
for (Integer uid : userIds) {
BigDecimal amt = sumRechargeWithArchive(uid, source, type);
if (amt != null) total = total.add(amt);
}
return total;
}
}
@@ -0,0 +1,29 @@
package com.ruoyi.playingmethod.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.domain.other.TtUpgradeRecord;
import com.ruoyi.domain.vo.ApiLuckyUpgradeRecentPullVO;
import com.ruoyi.domain.vo.ApiUpgradeRecordItemVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiUpgradeRecordMapper extends BaseMapper<TtUpgradeRecord> {
/** 最近100条拉取记录(按 upgradeId 查,所有用户) */
List<ApiLuckyUpgradeRecentPullVO> getRecentPulls(@Param("upgradeId") Integer upgradeId);
/**
* 幸运升级记录分页(全服/我的 × 当前饰品/全部饰品)
*
* @param page 分页参数
* @param userId 非空=仅查该用户(我的);null=全服
* @param upgradeId 非空=仅查该可升级饰品(当前饰品);null=全部饰品
*/
IPage<ApiUpgradeRecordItemVO> selectRecordPage(IPage<ApiUpgradeRecordItemVO> page,
@Param("userId") Integer userId,
@Param("upgradeId") Long upgradeId);
}
@@ -0,0 +1,37 @@
package com.ruoyi.playingmethod.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.entity.TtWelfareBoxInventory;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
@Mapper
public interface ApiWelfareBoxInventoryMapper extends BaseMapper<TtWelfareBoxInventory> {
/**
* 检查用户今日是否已领取
*/
boolean checkTodayClaimed(@Param("userId") Long userId);
/**
* 查询今日已领取的用户ID列表(定时任务批量发放用)
*/
List<Long> selectTodayClaimedUserIds();
/**
* 批量插入福利箱子库存
*/
int batchInsertInventory(@Param("list") List<TtWelfareBoxInventory> list);
/**
* 查询用户未开启的福利箱子
*/
List<TtWelfareBoxInventory> getUnopenedBoxes(@Param("userId") Long userId);
/**
* 批量更新状态
*/
int batchUpdateStatus(@Param("ids") List<Long> ids, @Param("status") Integer status);
}
@@ -0,0 +1,33 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.playingmethod.model.ApiWelfare;
import com.ruoyi.playingmethod.model.ApiWelfareRecord;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface ApiWelfareMapper {
List<ApiWelfare> getWelfareList(Long userId);
/**
* 检查是否已领取
*/
boolean checkClaimed(
@Param("welfareId") Integer welfareId,
@Param("userId") Long userId,
@Param("type") String type);
Integer getBoxIdByWelfareId(Integer welfareId);
int saveClaimWelfareRecord(ApiWelfareRecord apiWelfareRecord);
/**
* 批量保存领取记录(定时任务批量发放用)
*/
int batchSaveClaimRecord(@Param("list") List<ApiWelfareRecord> list);
}
@@ -0,0 +1,33 @@
package com.ruoyi.playingmethod.mapper;
import com.ruoyi.playingmethod.model.ApiWelfare;
import com.ruoyi.playingmethod.model.ApiWelfareRecord;
import com.ruoyi.playingmethod.model.vo.ApiWelfareMonthlyRechargesVO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.math.BigDecimal;
import java.util.List;
@Mapper
public interface ApiWelfareMonthlyRechargesMapper {
List<ApiWelfareMonthlyRechargesVO> getWelfareList(Long userId);
/**
* 查询当月充值量
*/
BigDecimal selectRechargeRecordsOfMonth(Long userId);
/**
* 检查是否已领取
*/
boolean checkClaimed(
@Param("welfareId") Integer welfareId,
@Param("userId") Long userId,
@Param("type") String type);
Integer getBoxIdByWelfareId(Integer welfareId);
int saveClaimWelfareRecord(ApiWelfareRecord apiWelfareRecord);
}
@@ -0,0 +1,51 @@
package com.ruoyi.playingmethod.model;
import com.ruoyi.domain.other.TtBox;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ApiWelfare {
/**
* 福利ID
*/
private Integer welfareId;
/**
* 福利名称
*/
private String welfareName;
/**
* 福利类型
*/
private String type;
/**
* VIP等级
*/
private Integer vipLevel;
/**
* 是否具备领取条件(0否 1是)
*/
private String eligible;
/**
* 领取状态(0未领取 1已领取)
*/
private String claimStatus;
/**
* 对应等级达标金额
*/
private BigDecimal rechargeThreshold;
/**
* 宝箱信息
*/
private TtBox ttBox;
}
@@ -0,0 +1,17 @@
package com.ruoyi.playingmethod.model;
import lombok.Data;
import java.util.Date;
@Data
public class ApiWelfareRecord {
private Integer welfareId;
private Long userId;
private String type;
private Date createTime;
}
@@ -0,0 +1,12 @@
package com.ruoyi.playingmethod.model.body;
import lombok.Data;
import java.util.List;
@Data
public class SynthesizeRequest {
private List<Long> boIds;
// private List<Long> yoIds;
}
@@ -0,0 +1,17 @@
package com.ruoyi.playingmethod.model.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ApiFightRankingVO {
private Long userId;
private String nickName;
private String avatar;
private BigDecimal totalBoxPrice;
}
@@ -0,0 +1,29 @@
package com.ruoyi.playingmethod.model.vo;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ApiFightTenTopVo {
/**
* 对战ID
*/
private Integer id;
/**
* 持有者/玩家ID
*/
private Integer userId;
/**
* 宝箱开出来饰品总价值
*/
private BigDecimal totalOrnamentsPrice;
/**
* 宝箱开出来饰品减去宝箱总金额 * 玩家个数
*/
private BigDecimal increaseOrnamentsPrice;
/**
* 倍数:总饰品金额 / (宝箱单价 * 玩家数)
*/
private BigDecimal multiple;
}
@@ -0,0 +1,28 @@
package com.ruoyi.playingmethod.model.vo;
import com.ruoyi.domain.other.TtBox;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class ApiWelfareMonthlyRechargesVO {
/** 福利ID */
private Integer welfareId;
/** 福利名称 */
private String welfareName;
/** 充值金额 */
private BigDecimal amount;
/** 是否具备领取条件(0否 1是) */
private String eligible;
/** 领取状态(0未领取 1已领取) */
private String claimStatus;
/** 宝箱信息 */
private TtBox ttBox;
}
@@ -0,0 +1,26 @@
package com.ruoyi.playingmethod.model.vo;
import com.ruoyi.domain.other.TtBox;
import lombok.Data;
@Data
public class ApiWelfareVO {
/** 福利ID */
private Integer welfareId;
/** 福利名称 */
private String welfareName;
/** 福利类型 */
private String type;
/** 是否具备领取条件(0否 1是) */
private String eligible;
/** 领取状态(0未领取 1已领取) */
private String claimStatus;
/** 宝箱信息 */
private TtBox ttBox;
}
@@ -0,0 +1,79 @@
package com.ruoyi.playingmethod.scheduled;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.service.TtFightRankingRewardService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtFightRankingReward;
import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO;
import com.ruoyi.playingmethod.service.ApiFightService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
@Slf4j
@Component("FightRankingRewardTask")
public class FightRankingRewardTask {
@Autowired
private ApiFightService apiFightService;
@Autowired
private TtFightRankingRewardService ttFightRankingRewardService;
@Autowired
private TtUserService userService;
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
/** 发放对战排行榜奖励 */
public void distributeFightRankingReward() {
log.info("开始发放对战排行榜奖励");
// 查询昨日排行榜
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
Calendar cal = Calendar.getInstance();
cal.add(Calendar.DATE, -1);
Date yesterday = cal.getTime();
String formattedYesterdayDate = formatter.format(yesterday);
List<ApiFightRankingVO> yesterdayFightRanking = apiFightService.getFightRankingByDate(formattedYesterdayDate);
for (int i = 0; i < yesterdayFightRanking.size(); i++) {
// 查询排行榜对应奖励
TtFightRankingReward ttFightRankingReward = ttFightRankingRewardService.selectTtFightRankingRewardById(i + 1);
Long userId = yesterdayFightRanking.get(i).getUserId();
BigDecimal reward = ttFightRankingReward.getReward();
// 加钱
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, userId)
.setSql("account_amount = account_amount + " + reward);
userService.update(userUpdate);
// 综合消费日志
TtUser ttUser = userService.getById(userId);
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(userId.intValue())
.amount(reward.compareTo(BigDecimal.ZERO) > 0 ? reward : null)
// ttUser 已在加钱 SQL 之后重新查询,账户余额已包含本次 reward,直接作为快照,切勿再 add(reward)
.finalAmount(reward.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountAmount() : null)
.total(reward) // 收支合计
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.FIGHT_RANKING_REWARD.getCode())
.remark(TtAccountRecordSource.FIGHT_RANKING_REWARD.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
ttUserBlendErcashMapper.insert(blendErcash);
}
}
}
@@ -0,0 +1,460 @@
package com.ruoyi.playingmethod.scheduled;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtBoxMapper;
import com.ruoyi.admin.mapper.TtFightMapper;
import com.ruoyi.admin.mapper.TtRobotFightJoinConfigMapper;
import com.ruoyi.admin.mapper.TtRobotFightPriceConfigMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.DateTimeUtils;
import com.ruoyi.common.utils.RandomUtil;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.fight.FightSeat;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.CreateFightBody;
import com.ruoyi.domain.other.FightBoutData;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.domain.other.TtRobotFightGroupBox;
import com.ruoyi.domain.other.TtRobotFightJoinConfig;
import com.ruoyi.domain.other.TtRobotFightPriceConfig;
import com.ruoyi.domain.vo.ApiFightListDataVO;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.playingmethod.domain.vo.ApiRobotFightGroupVO;
import com.ruoyi.playingmethod.mapper.ApiFightMapper;
import com.ruoyi.playingmethod.mapper.ApiRobotFightGroupMapper;
import com.ruoyi.playingmethod.service.ApiFightService;
import com.ruoyi.system.service.ISysConfigService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Component("FightRobotTask")
public class FightRobotTask {
@Value("${mkcsgo.fight.roundTime}")
private final Integer fightRoundTimeMs = 4000;
@Autowired
private RedisCache redisCache;
@Autowired
private TtFightMapper ttFightMapper;
@Autowired
private TtUserMapper ttUserMapper;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private ApiFightService apiFightService;
@Autowired
private ApiRobotFightGroupMapper robotFightGroupMapper;
@Autowired
private ApiFightMapper apiFightMapper;
@Autowired
private TtBoxMapper boxMapper;
@Autowired
private TtRobotFightJoinConfigMapper joinConfigMapper;
@Autowired
private TtRobotFightPriceConfigMapper priceConfigMapper;
/**
* 机器人加入对战
*/
public void robotJoinFight() {
log.info("机器人加入对战");
// 查询所有处于等待中的对战房间ID(覆盖欧皇model=0与非酋model=1)
QueryWrapper<TtFight> queryWrapper = new QueryWrapper<>();
queryWrapper.eq("status", 0);
queryWrapper.in("model", "0", "1");
List<TtFight> fightList = ttFightMapper.selectList(queryWrapper);
if (CollectionUtils.isEmpty(fightList)) {
log.info("没有等待中的对战房间");
return;
}
// 查询所有机器人ID
List<Integer> robotIdList = ttUserMapper.selectObjs(
new QueryWrapper<TtUser>()
.select("user_id")
.eq("user_type", UserType.ROBOT_USER.getCode())
);
if (robotIdList.isEmpty()) {
log.info("没有机器人");
return;
}
// 查询启用的机器人进房时间档位配置(按最小金额升序)
TtRobotFightJoinConfig queryConfig = new TtRobotFightJoinConfig();
queryConfig.setStatus("0");
List<TtRobotFightJoinConfig> joinConfigs = joinConfigMapper.selectTtRobotFightJoinConfigList(queryConfig);
if (CollectionUtils.isEmpty(joinConfigs)) {
log.info("未配置机器人进房时间档位,跳过");
return;
}
// 打乱对战ID列表顺序,取前面指定数量的对战ID
Collections.shuffle(fightList);
List<Integer> userIdList = fightList.stream().map(TtFight::getUserId).filter(Objects::nonNull)
.collect(Collectors.toList());
if (userIdList.isEmpty()) {
return;
}
Map<Integer, TtUser> userMap = ttUserMapper.selectByIds(userIdList).stream()
.collect(Collectors.toMap(TtUser::getUserId, v -> v));
for (TtFight ttFight : fightList) {
TtUser user = userMap.get(ttFight.getUserId());
if (user == null) {
continue;
}
// 补偿:满员且全员已准备但仍未开局的僵尸房间(开局请求丢失/异常导致),重新触发开局
List<FightSeat> seats = ttFight.getSeats();
boolean hasEmptySeat = seats.stream().anyMatch(s -> Objects.isNull(s.getPlayerId()));
if (!hasEmptySeat) {
boolean allReady = seats.stream().allMatch(s -> s.getStatus() != null && s.getStatus().equals(2));
if (allReady) {
FightBoutData fightBoutData = new FightBoutData();
fightBoutData.setFightId(ttFight.getId());
fightBoutData.setBoutNum(1);
fightBoutData.setExpirationTime(fightRoundTimeMs);
String key = "fight_bout_data:fight_" + fightBoutData.getFightId();
redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS);
R begin = apiFightService.fightBegin(ttFight.getId(), user);
log.warn("补偿触发满员未开局房间{}开局,结果:{}", ttFight.getId(), begin == null ? "" : begin.getMsg());
}
continue;
}
BigDecimal boxPriceTotal = ttFight.getBoxPriceTotal();
if (boxPriceTotal == null) {
continue;
}
// 命中金额档位:minAmount < 金额 <= maxAmount(金额超过最大档位则不加机器人)
Optional<TtRobotFightJoinConfig> configOpt = joinConfigs.stream()
.filter(c -> c.getMinAmount() != null && c.getMaxAmount() != null
&& boxPriceTotal.compareTo(c.getMinAmount()) > 0
&& boxPriceTotal.compareTo(c.getMaxAmount()) <= 0)
.findFirst();
if (!configOpt.isPresent()) {
continue;
}
TtRobotFightJoinConfig config = configOpt.get();
int randomSeconds = config.getRandomSeconds() == null ? 10 : config.getRandomSeconds();
long time = config.getJoinSeconds() + RandomUtil.rand(1, randomSeconds + 1); // 在基础秒数上加1~randomSeconds秒随机偏移,错峰加入
if (ttFight.getCreateTime().getTime() + time * 1000 >= DateTimeUtils.now() * 1000) {
continue;
}
List<FightSeat> fightSeats = ttFight.getSeats();
var inRoomPlayerIds = fightSeats.stream()
.map(FightSeat::getPlayerId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
// 收集所有空位
List<FightSeat> emptySeats = fightSeats.stream()
.filter(s -> Objects.isNull(s.getPlayerId()))
.collect(Collectors.toList());
if (emptySeats.isEmpty()) {
continue;
}
// 如果房主是机器人且未准备(status=1),先让房主准备
FightSeat ownerSeat = fightSeats.stream()
.filter(s -> Objects.nonNull(s.getPlayerId()) && s.getPlayerId().equals(ttFight.getUserId()) && s.getStatus().equals(1))
.findFirst().orElse(null);
if (ownerSeat != null) {
TtUser owner = userMap.get(ttFight.getUserId());
if (owner != null && UserType.ROBOT_USER.getCode().equals(owner.getUserType())) {
apiFightService.seatrReady(ttFight.getId(), owner);
log.info("机器人房主{}准备对战{}", owner.getUserId(), ttFight.getId());
}
}
List<Integer> availableRobotIds = new ArrayList<>(robotIdList);
availableRobotIds.removeAll(inRoomPlayerIds);
if (availableRobotIds.size() < emptySeats.size()) {
log.warn("可用机器人不足以填满对战{}的空位, 需要{}个, 可用{}个", ttFight.getId(), emptySeats.size(), availableRobotIds.size());
continue;
}
// 依次填充空位,每次之间有时间差
for (int i = 0; i < emptySeats.size(); i++) {
if (i > 0) {
// 第二个及之后的机器人加入延迟1~randomSeconds秒,保证间隔且不会同时涌入
try {
Thread.sleep(RandomUtil.rand(1, randomSeconds + 1) * 1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
Integer robotId = availableRobotIds.get(RandomUtil.rand(availableRobotIds.size()));
availableRobotIds.remove(robotId);
TtUser player = ttUserMapper.selectById(robotId);
R<Object> objectR = apiFightService.joinFight(ttFight.getId(), player);
if (!objectR.getCode().equals(200)) {
log.warn("机器人{}加入对战{}失败:{}", robotId, ttFight.getId(), objectR.getMsg());
break;
}
log.info("机器人{}加入对战{}", robotId, ttFight.getId());
// 座位就绪后立即触发准备状态
apiFightService.seatrReady(ttFight.getId(), player);
log.info("机器人{}准备对战{}", robotId, ttFight.getId());
// 最后一个空位填满后直接开始对战
if (i == emptySeats.size() - 1) {
FightBoutData fightBoutData = new FightBoutData();
fightBoutData.setFightId(ttFight.getId());
fightBoutData.setBoutNum(1);
fightBoutData.setExpirationTime(fightRoundTimeMs);
String key = "fight_bout_data:fight_" + fightBoutData.getFightId();
redisCache.setCacheObject(key, fightBoutData, fightBoutData.getExpirationTime(), TimeUnit.MILLISECONDS);
apiFightService.fightBegin(ttFight.getId(), player);
log.info("对战{}满员,机器人触发开始", ttFight.getId());
}
}
}
}
/**
* 机器人创建对战
* 分别为欧皇(model=0)和非酋(model=1)独立维持房间数量
*/
public void robotCreateFight() {
log.info("机器人创建对战");
// 查询所有机器人ID
List<Integer> robotIdList = ttUserMapper.selectObjs(
new QueryWrapper<TtUser>()
.select("user_id")
.eq("user_type", UserType.ROBOT_USER.getCode())
);
if (robotIdList.isEmpty()) {
log.warn("无可用机器人,停止创建对战");
return;
}
// 查询机器人宝箱分组(全量,后续按model过滤)
List<ApiRobotFightGroupVO> apiRobotFightGroupVOList = robotFightGroupMapper.selectApiRobotFightGroupBoxVOList();
if (CollectionUtils.isEmpty(apiRobotFightGroupVOList)) {
return;
}
if (apiRobotFightGroupVOList.size() > 10000) {
return;
}
// 预加载所有分组关联的宝箱信息
Set<Integer> boxIds = apiRobotFightGroupVOList.stream()
.flatMap(v -> v.getTtRobotFightGroupBoxList().stream())
.map(TtRobotFightGroupBox::getBoxId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
List<TtBox> allBoxes = Collections.emptyList();
if (!boxIds.isEmpty()) {
allBoxes = new LambdaQueryChainWrapper<>(boxMapper)
.in(TtBox::getBoxId, boxIds)
.eq(TtBox::getIsFight, "0")
.eq(TtBox::getStatus, "0")
.list();
}
Map<Integer, TtBox> boxMap = allBoxes.stream()
.collect(Collectors.toMap(TtBox::getBoxId, v -> v, (v1, v2) -> v1));
// 计算每个分组的总价
Map<Integer, BigDecimal> groupPriceMap = new HashMap<>();
for (ApiRobotFightGroupVO groupVO : apiRobotFightGroupVOList) {
BigDecimal boxTotalPrice = BigDecimal.ZERO;
if (CollectionUtils.isNotEmpty(groupVO.getTtRobotFightGroupBoxList())) {
for (TtRobotFightGroupBox groupBox : groupVO.getTtRobotFightGroupBoxList()) {
TtBox box = boxMap.get(groupBox.getBoxId());
if (box != null) {
BigDecimal multiply = BigDecimal.valueOf(groupBox.getBoxNum()).multiply(box.getPrice());
boxTotalPrice = boxTotalPrice.add(multiply);
}
}
}
groupPriceMap.put(groupVO.getGroupId(), boxTotalPrice);
}
// 分别为欧皇(model=0)和非酋(model=1)独立创建房间
String[] models = {"0", "1"};
for (String model : models) {
createFightByModel(model, robotIdList, apiRobotFightGroupVOList, groupPriceMap);
}
}
/**
* 按模式独立创建对战房间
* @param model 模式:0=欧皇,1=非酋
* @param robotIdList 机器人ID列表
* @param allGroups 所有分组配置
* @param groupPriceMap 分组ID -> 总价映射
*/
private void createFightByModel(String model, List<Integer> robotIdList,
List<ApiRobotFightGroupVO> allGroups,
Map<Integer, BigDecimal> groupPriceMap) {
String modelName = "0".equals(model) ? "欧皇" : "非酋";
log.info("开始为{}模式创建对战房间", modelName);
// 查询启用的价格档位配置
TtRobotFightPriceConfig priceQuery = new TtRobotFightPriceConfig();
priceQuery.setStatus("0");
List<TtRobotFightPriceConfig> priceConfigs = priceConfigMapper.selectTtRobotFightPriceConfigList(priceQuery);
if (CollectionUtils.isEmpty(priceConfigs)) {
log.warn("{}模式未配置价格档位,跳过创建对战房间", modelName);
return;
}
// 查询该模式下等待中的房间存量
List<ApiFightListDataVO> vos = apiFightMapper.getFightList(model, "0", null, null);
if (CollectionUtils.isEmpty(vos)) {
vos = new ArrayList<>();
}
vos.forEach(v -> {
if (v.getBoxPriceTotal() == null) {
v.setBoxPriceTotal(BigDecimal.ZERO);
}
});
// 统计当前各价位对战的存量
int configSize = priceConfigs.size();
int[] currentCounts = new int[configSize];
for (ApiFightListDataVO vo : vos) {
int index = matchPriceConfigIndex(vo.getBoxPriceTotal(), priceConfigs);
if (index >= 0) {
currentCounts[index]++;
}
}
// 计算需要创建的数量
int[] needCreateCounts = new int[configSize];
StringBuilder currentLog = new StringBuilder();
StringBuilder needLog = new StringBuilder();
for (int i = 0; i < configSize; i++) {
needCreateCounts[i] = Math.max(priceConfigs.get(i).getTargetCount() - currentCounts[i], 0);
if (i > 0) {
currentLog.append(", ");
needLog.append(", ");
}
currentLog.append(priceConfigs.get(i).getPriceLimit().stripTrailingZeros().toPlainString()).append("档=").append(currentCounts[i]).append("个");
needLog.append(priceConfigs.get(i).getPriceLimit().stripTrailingZeros().toPlainString()).append("档=").append(needCreateCounts[i]).append("个");
}
log.info("{}模式存量: {}; 需创建: {}", modelName, currentLog, needLog);
// 筛选该模式的分组
List<ApiRobotFightGroupVO> modelGroups = allGroups.stream()
.filter(g -> model.equals(g.getModel()))
.collect(Collectors.toList());
if (CollectionUtils.isEmpty(modelGroups)) {
log.info("{}模式无可用分组配置,跳过", modelName);
return;
}
var groupMap = modelGroups.stream()
.collect(Collectors.toMap(ApiRobotFightGroupVO::getGroupId, v -> v));
// 循环处理不同价位的创建任务
for (int i = 0; i < configSize; i++) {
int needCount = needCreateCounts[i];
if (needCount <= 0) {
continue;
}
BigDecimal limitPrice = priceConfigs.get(i).getPriceLimit();
BigDecimal lowPrice = i > 0 ? priceConfigs.get(i - 1).getPriceLimit() : BigDecimal.ZERO;
// 筛选出符合当前价位的分组
List<ApiRobotFightGroupVO> matchedGroups = groupPriceMap.entrySet().stream()
.filter(entry -> groupMap.containsKey(entry.getKey()))
.filter(entry -> entry.getValue().compareTo(lowPrice) > 0 && entry.getValue().compareTo(limitPrice) <= 0)
.map(entry -> groupMap.get(entry.getKey()))
.collect(Collectors.toList());
if (CollectionUtils.isNotEmpty(matchedGroups)) {
createFight(matchedGroups, needCount, robotIdList);
}
}
}
/**
* 根据房间金额匹配价格档位索引
* @param boxPriceTotal 房间宝箱总金额
* @param priceConfigs 按 sort_order 升序排列的价格档位配置
* @return 命中档位索引,未命中返回 -1
*/
private int matchPriceConfigIndex(BigDecimal boxPriceTotal, List<TtRobotFightPriceConfig> priceConfigs) {
if (boxPriceTotal == null || CollectionUtils.isEmpty(priceConfigs)) {
return -1;
}
for (int i = 0; i < priceConfigs.size(); i++) {
TtRobotFightPriceConfig config = priceConfigs.get(i);
if (config.getPriceLimit() != null && boxPriceTotal.compareTo(config.getPriceLimit()) <= 0) {
return i;
}
}
return -1;
}
private void createFight(List<ApiRobotFightGroupVO> groupList, int canCreateCount, List<Integer> robotIdList) {
if (CollectionUtils.isEmpty(groupList) || CollectionUtils.isEmpty(robotIdList)) {
return;
}
// 创建指定数量的房间
for (int i = 0; i < canCreateCount; i++) {
// 每个房间创建间隔1-10秒随机延迟,错峰创建
if (i > 0) {
try {
Thread.sleep(RandomUtil.rand(1, 11) * 1000L);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
// 随机取一个机器人
Integer robotId = robotIdList.get(RandomUtil.rand(robotIdList.size()));
TtUser player = ttUserMapper.selectById(robotId);
// 随机取一个宝箱分组
ApiRobotFightGroupVO apiRobotFightGroupVO = groupList.get(RandomUtil.rand(groupList.size()));
log.info("机器人{}使用宝箱分组{}", robotId, apiRobotFightGroupVO.getGroupId());
// 创建对战
CreateFightBody createFightBody = new CreateFightBody();
createFightBody.setModel(apiRobotFightGroupVO.getModel());
createFightBody.setPlayerNumber(apiRobotFightGroupVO.getSeatNum());
Map<Integer, Integer> boxIdAndNumber = new HashMap<>();
for (TtRobotFightGroupBox ttRobotFightGroupBox : apiRobotFightGroupVO.getTtRobotFightGroupBoxList()) {
boxIdAndNumber.put(ttRobotFightGroupBox.getBoxId(), ttRobotFightGroupBox.getBoxNum());
}
createFightBody.setBoxIdAndNumber(boxIdAndNumber);
apiFightService.createFight(createFightBody, player);
}
}
}
@@ -0,0 +1,130 @@
package com.ruoyi.playingmethod.scheduled;
import cn.hutool.core.date.DateUnit;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.TtBoxRecordsMapper;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.admin.service.TtRollService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.rabbitmq.config.DelayedQueueConfig;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.entity.roll.TtRoll;
import com.ruoyi.playingmethod.service.ApiFightService;
import com.ruoyi.playingmethod.service.ApiRollService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON;
@Slf4j
@Configuration // 1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class RollTask {
@Value("${mkcsgo.fight.roundTime}")
private final Integer fightRoundTime = null;
@Autowired
private ApiFightService fightService;
@Autowired
private TtBoxRecordsMapper boxRecordsMapper;
@Autowired
private TtBoxRecordsService boxRecordsService;
@Autowired
private TtRollService ttRollService;
@Autowired
private Executor customThreadPoolExecutor;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ApiRollService apiRollService;
// 定时检查即将开奖的roll房(临界区:1分钟)
@Scheduled(cron = "0 */1 * * * ?")
private void refreshDayTask() {
// System.out.println("11111111111111111");
//System.out.println("roll 开奖临界区检查");
// 查询两分钟以内将要开奖的roll房
Calendar c = Calendar.getInstance();
c.add(Calendar.MINUTE, 2);
Timestamp criticalTime = new Timestamp(c.getTimeInMillis());
LambdaQueryWrapper<TtRoll> wrapper = new LambdaQueryWrapper<>();
wrapper
.eq(TtRoll::getRollStatus, 0)
.eq(TtRoll::getDelFlag, 0)
.lt(TtRoll::getEndTime, criticalTime);
List<TtRoll> criticalRoll = ttRollService.list(wrapper);
if (criticalRoll.isEmpty()) return;
log.info("roll开奖临界区检查,{}个roll房进入临界区", criticalRoll.size());
// 加入延时队列
for (TtRoll roll : criticalRoll) {
if (DateUtils.getNowDate().compareTo(roll.getEndTime()) > 0) {
log.info("roll房{}已经超时,直接开奖。", roll.getId());
R r = apiRollService.endROLL(roll.getId());
if (!r.getCode().equals(200)) {
log.error("roll开奖 消费失败,尝试消息补发再次消费!" + r.getMsg());
}
if (ObjectUtil.isNotEmpty(r.getMsg())) log.info("{}", r.getMsg());
// 已直接开奖,跳过后续延时队列入队,避免同一房间重复触发开奖
continue;
}
// 相差时间
long betweenDay = DateUtil.between(DateUtils.getNowDate(), roll.getEndTime(), DateUnit.MS);
System.out.println("设置消息超时时间");
log.info("设置消息超时时间:{}",betweenDay);
MessageProperties messageProperties = new MessageProperties();
messageProperties.setExpiration(String.valueOf(betweenDay)); // 消息的过期属性,单位 ms
Message message = new Message(String.valueOf(roll.getId()).getBytes(), messageProperties);
// 延时队列实现开奖
rabbitTemplate.convertAndSend(
DelayedQueueConfig.OPEN_ROLL_EXCHANGE,
DelayedQueueConfig.OPEN_ROLL_KEY,
message);
log.info("Roll房{}-{}加入临界区,{}秒后开奖", roll.getId(), roll.getRollName(), betweenDay / 1000);
}
}
}
@@ -0,0 +1,60 @@
package com.ruoyi.playingmethod.scheduled;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.playingmethod.service.ApiWelfareService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* 每日福利箱子发放定时任务。
* <p>
* 注册到 RuoYi 系统监控-定时任务:
* - 任务名称:每日福利箱子发放
* - 任务组名:WELFARE
* - 调用目标:welfareBoxTask.autoIssueDailyWelfareBox()
* - cron:0 0 0 * * ? (每天凌晨 0 点)
* <p>
* 原 @Scheduled(cron = "0 0 0 * * ?") 迁移至此,由 sys_job 统一调度,请勿再加 @Scheduled。
*/
@Slf4j
@Component("welfareBoxTask")
public class WelfareBoxTask {
@Autowired
private TtUserService ttUserService;
@Autowired
private ApiWelfareService apiWelfareService;
/**
* 每日0点自动为所有用户发放对应VIP等级的福利箱子
*/
public void autoIssueDailyWelfareBox() {
log.info("[福利箱子] 开始自动发放每日福利箱子...");
long start = System.currentTimeMillis();
// 1. 一次查出所有正常用户(排除机器人、已禁用)
LambdaQueryWrapper<TtUser> query = new LambdaQueryWrapper<>();
query.ne(TtUser::getUserType, UserType.ROBOT_USER.getCode())
.eq(TtUser::getStatus, "0");
List<TtUser> users = ttUserService.list(query);
if (users == null || users.isEmpty()) {
log.info("[福利箱子] 未查询到可发放用户,任务结束。");
return;
}
// 2. 批量发放(内部只有3次DB操作:查已领取、查福利配置、批量写入)
int[] result = apiWelfareService.batchAutoIssueDailyBoxes(users);
long elapsed = System.currentTimeMillis() - start;
log.info("[福利箱子] 自动发放完成。共处理用户: {}, 成功: {}, 跳过: {}, 耗时: {}ms",
users.size(), result[0], result[1], elapsed);
}
}
@@ -0,0 +1,231 @@
package com.ruoyi.playingmethod.scheduled;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.TtBoxMapper;
import com.ruoyi.admin.mapper.TtBoxRecordsMapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.domain.vo.fight.FightBoxVO;
import com.ruoyi.playingmethod.service.ApiFightService;
import com.ruoyi.framework.websocket.connector.PushService;
import com.ruoyi.framework.websocket.connector.SubscriptionManager;
import com.ruoyi.framework.websocket.connector.WsResult;
import com.ruoyi.playingmethod.websocket.constant.SMsgKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON;
@Slf4j
@Configuration //1.主要用于标记配置类,兼备Component的效果。
@EnableScheduling // 2.开启定时任务
public class fightTask {
@Value("${mkcsgo.fight.roundTime:6000}")
private int fightRoundTime;
@Value("${mkcsgo.fight.startingDuration:3000}")
private int startingDuration;
@Value("${mkcsgo.fight.revealDuration:4000}")
private int revealDuration;
@Autowired
private ApiFightService fightService;
@Autowired
private TtBoxRecordsMapper boxRecordsMapper;
@Autowired
private TtBoxRecordsService boxRecordsService;
@Autowired
private PushService pushService;
@Autowired
private TtBoxMapper ttBoxMapper;
@Autowired
private TtUserMapper ttUserMapper;
@Autowired
private Executor customThreadPoolExecutor;
@Autowired
private com.ruoyi.common.core.redis.RedisCache redisCache;
/**
* 定时清理超时未结束的对局(每分钟一次)
* 正常流程由 FightStateManager 管理对战生命周期;
* 此任务作为兜底,清理服务重启/推进线程丢失产生的僵尸对战(status=1 但无人推进),
* 统一标记为 status=3 并广播房间更新、清理 Redis 状态。
*/
@Scheduled(cron = "0 * * * * ?")
public void refreshDayTask() {
log.info("定时更新超时未结束的对局");
// Timestamp now = new Timestamp(System.currentTimeMillis());
LambdaQueryWrapper<TtFight> fightQuery = new LambdaQueryWrapper<>();
fightQuery
.eq(TtFight::getStatus,1)
.and(wrapper->{
wrapper.isNull(TtFight::getEndTime);
})
.and(wrapper->{
wrapper.isNotNull(TtFight::getBeginTime);
})
// TODO: 2024/4/13 优化sql
// 对局开始时间早于当前时间减去一定时间间隔(再减去6秒),就会被认为是超时的对局
// begin_time < DATE_SUB(NOW(), INTERVAL 100 SECOND)
.last("AND begin_time < DATE_SUB(NOW(), INTERVAL " + fightRoundTime / 1000 + "* round_number + " + startingDuration / 1000 + " + " + revealDuration / 1000 + " + 15 second)");
List<TtFight> list = fightService.list(fightQuery);
if (list.isEmpty()) return;
List<Integer> fightIds = new ArrayList<>();
// 更新fight
list.stream().forEach(item->{
item.setEndTime(new Timestamp(System.currentTimeMillis()));
item.setStatus(3);
item.setRemark("超时强制更新结束状态");
fightIds.add(item.getId());
// 断开房间所有连接
pushService.closeChannel(SubscriptionManager.fightChannel(item.getId().longValue()));
});
fightService.updateBatchById(list);
// 异步更新对局数据
CompletableFuture.runAsync(() -> {
pushService.broadcastToChannel(SubscriptionManager.hallChannel(), WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, buildRoomUpdateData(list), "对战房间最新信息"));
}, customThreadPoolExecutor);
// 更新openBoxRecords(机器人持有的饰品不入背包,仅供前端展示)
new LambdaUpdateChainWrapper<>(boxRecordsMapper)
// .eq(TtBoxRecords::getStatus,IN_PACKSACK_ON.getCode())
.in(TtBoxRecords::getFightId,fightIds)
.and(w -> w.isNull(TtBoxRecords::getHolderUserId)
.or().notInSql(TtBoxRecords::getHolderUserId,
"SELECT user_id FROM tt_user WHERE user_type = '" + UserType.ROBOT_USER.getCode() + "'"))
.set(TtBoxRecords::getStatus,IN_PACKSACK_ON.getCode())
.update();
// 清理 Redis 状态
fightIds.forEach(id -> redisCache.deleteObject("fight_state:" + id));
log.info("更新 {} 条超时未结束的对局记录",list.size());
}
private void enrichBoxPrices(List<TtFight> fights) {
List<Integer> boxIds = new ArrayList<>();
for (TtFight fight : fights) {
if (fight == null || fight.getBoxData() == null || fight.getBoxData().isEmpty()) continue;
for (String boxId : fight.getBoxData().keySet()) {
Object value = fight.getBoxData().get(boxId);
Integer id = extractBoxId(value);
if (id != null) boxIds.add(id);
}
}
if (boxIds.isEmpty()) return;
LambdaQueryWrapper<TtBox> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.in(TtBox::getBoxId, boxIds);
List<TtBox> boxes = ttBoxMapper.selectList(queryWrapper);
Map<Integer, BigDecimal> boxMap = boxes.stream().filter(Objects::nonNull)
.collect(Collectors.toMap(TtBox::getBoxId, TtBox::getPrice));
for (TtFight fight : fights) {
if (fight == null || fight.getBoxData() == null) continue;
for (String boxId : fight.getBoxData().keySet()) {
Object value = fight.getBoxData().get(boxId);
FightBoxVO vo = resolveFightBoxVO(value);
if (vo != null) {
vo.setPrice(boxMap.getOrDefault(vo.getBoxId(), BigDecimal.ZERO));
fight.getBoxData().put(boxId, vo);
}
}
}
}
private Map<String, Object> buildRoomUpdateData(List<TtFight> fights) {
enrichBoxPrices(fights);
enrichOwnerNickNames(fights);
Map<String, Object> data = new HashMap<>();
data.put("rows", fights);
return data;
}
private void enrichOwnerNickNames(List<TtFight> fights) {
if (fights == null || fights.isEmpty()) return;
List<Integer> ownerIds = fights.stream()
.map(TtFight::getUserId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (ownerIds.isEmpty()) return;
List<TtUser> users = ttUserMapper.selectBatchIds(ownerIds);
Map<Integer, String> nickMap = users.stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(TtUser::getUserId,
u -> u.getNickName() != null ? u.getNickName() : "", (a, b) -> a));
for (TtFight fight : fights) {
if (fight.getUserId() != null) {
// 用户存在时用实时昵称覆盖;用户被物理删除时保留入库的 ownerNickName
String liveNick = nickMap.get(fight.getUserId());
if (liveNick != null) {
fight.setOwnerNickName(liveNick);
}
}
}
}
private Integer extractBoxId(Object value) {
if (value == null) return null;
if (value instanceof FightBoxVO) return ((FightBoxVO) value).getBoxId();
if (value instanceof LinkedHashMap) {
Object idField = ((LinkedHashMap<?, ?>) value).get("boxId");
return idField != null ? Integer.valueOf(idField.toString()) : null;
}
return null;
}
private FightBoxVO resolveFightBoxVO(Object value) {
if (value == null) return null;
if (value instanceof FightBoxVO) return (FightBoxVO) value;
if (value instanceof LinkedHashMap) {
Map<String, Object> map = (LinkedHashMap<String, Object>) value;
return new FightBoxVO(
map.get("boxId") != null ? Integer.valueOf(map.get("boxId").toString()) : null,
map.get("number") != null ? Integer.valueOf(map.get("number").toString()) : null,
map.get("boxImg01") != null ? map.get("boxImg01").toString() : null,
map.get("boxImg02") != null ? map.get("boxImg02").toString() : null);
}
return null;
}
}
@@ -0,0 +1,4 @@
package com.ruoyi.playingmethod.service;
public interface ApiBattleRoyaleService {
}
@@ -0,0 +1,38 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtBoxA;
import com.ruoyi.domain.other.TtOrnamentsA;
import com.ruoyi.domain.other.TtBoxVO;
import com.ruoyi.playingmethod.domain.vo.CompValueVO;
import java.util.List;
public interface ApiBindBoxService {
TtBoxA getBoxData(Integer boxId);
/** 对战宝箱详情,概率使用 robot_odds */
TtBoxA getFightBoxData(Integer boxId);
List<TtOrnamentsA> getCompBoxData(Integer boxId);
List<TtBoxVO> getBoxList(Integer boxTypeId, String homeFlag, String boxType, String isFight);
List<TtBoxVO> groupByBoxType(List<TtBoxA> boxData);
List<TtOrnamentsA> openBox(TtBox ttBox, Integer num, TtUser ttUser);
List<TtBoxRecords> addBoxRecord(TtUser ttUser, TtBox ttBox, Integer num);
List<TtBoxRecords> openBoxArithmetic(Integer fightId,TtUser ttUser, TtBox ttBox, Integer num);
// List<TtBoxUser> getBindBoxHistory(ApiBindBoxController.boxHistoryParam param);
R blindBox(TtUser ttUser, TtBox ttBox, Integer num);
CompValueVO getCompValue(TtUser ttUser, TtBox ttBox);
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.domain.other.TtBoxOrnaments;
public interface ApiBoxOrnamentsService extends IService<TtBoxOrnaments> {
}
@@ -0,0 +1,18 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.domain.dto.boxRecords.TenTopQuery;
import com.ruoyi.domain.dto.boxRecords.queryCondition;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO;
import java.util.List;
public interface ApiBoxRecordsService extends IService<TtBoxRecords> {
List<TtBoxRecordsVO> byCondition(queryCondition condition);
List<TtBoxRecordsVO> tenTopQuery(TenTopQuery query);
List<TtBoxRecordsVO> topHundredQuery(List<Integer> status, List<Integer> source, boolean todayOnly);
}
@@ -0,0 +1,14 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import java.util.List;
public interface ApiCompoundService {
R<UserPackSackDataVO> compound(List<Long> packSackIds, TtUser ttUser);
List<UserPackSackDataVO> getUserCompoundRecord(Integer userId);
}
@@ -0,0 +1,38 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.exponent.ExponentJoinParam;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtBox;
public interface ApiExponentService {
/**
* 加入指数
* @param param
* @return
*/
R join(TtUser ttUser, ExponentJoinParam param);
/**
* 宝箱信息
*/
R box(TtUser ttUser, Integer id);
/**
* 最近十期指数
*/
R recenttenperiod();
/**
* 返回信息
* @param ttUser
* @return
*/
R info(TtUser ttUser);
R records(TtUser ttUser);
R mybox(TtUser ttUser);
R openbox(TtUser ttUser, TtBox ttBox, Integer num);
}
@@ -0,0 +1,80 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.fight.FightDetailParam;
import com.ruoyi.domain.dto.fight.FightOnMyOwnParam;
import com.ruoyi.domain.other.CreateFightBody;
import com.ruoyi.domain.other.TtBoxVO;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.vo.ApiFightListDataVO;
import com.ruoyi.domain.vo.FightResultDataVO;
import com.ruoyi.domain.vo.fight.FightResultVO;
import com.ruoyi.domain.vo.fight.TtFightVO;
import com.ruoyi.playingmethod.model.vo.ApiFightRankingVO;
import java.util.Date;
import java.util.List;
import java.util.Map;
public interface ApiFightService extends IService<TtFight> {
R<Object> createFight(CreateFightBody createFightParamVO, TtUser ttUser);
/**
* 复刻房间:根据已有房间配置创建一个新房间,当前用户成为房主,其余座位为空
*/
R<Object> copyFight(Integer sourceFightId, TtUser player);
R<Object> joinFight(Integer fightId, TtUser player);
/** 加入房间并指定座位(seatCode=null 时自动坐第一个空位) */
R<Object> joinFight(Integer fightId, TtUser player, Integer seatCode);
List<TtBoxVO> getFightBoxList(Integer boxTypeId);
List<ApiFightListDataVO> getFightList(String model, String status, Integer userId, Integer fightId);
FightResultDataVO getFightRecord(Integer fightId, Integer round, Integer rounds);
R fightBegin(Integer fightId, TtUser player);
R fightEnd(Integer fightId, Long userId);
R seatrReady(Integer fightId, TtUser player);
R fightRoomExit(Integer fightId, TtUser player);
List<ApiFightListDataVO> getFightList(FightOnMyOwnParam param);
R fightDetail(FightDetailParam param);
R earlierHistory(FightDetailParam param);
// AjaxResult joinFightRoom(Integer userId,Integer fightId);
List<ApiFightRankingVO> getFightRankingByDate(String date);
Map<String, List<FightResultVO>> tenTopFight(Integer maxType, String model, Integer minPrice, Integer maxPrice, String keyword);
/**
* 批量填充房主昵称(供 controller 中直接操作 TtFight 列表的场景使用)
*/
void enrichOwnerNickNames(List<TtFight> fights);
/**
* 快速加入对战房间
* 根据用户金额在本阵营等待中房间内,找到总价小于等于用户余额的最高价房间(不落座)
* 规则说明:
* 1. 只查询等待中房间(status = '0')且 model 与当前大厅阵营一致(不跨阵营匹配)
* 2. 找到所有房间价格 <= 用户余额的房间
* 3. 在这些房间中选择价格最高的一个返回
* 示例:用户有 10 金币,大厅有 1 元/2 元/9 元三个房间 → 选择 9 元的房间
*
* @param userId 用户 ID
* @param model 房间模式(大厅阵营):0-欧皇 1-非酋
* @return 房间 id(Long),本阵营无匹配房间时 data 为 null
*/
R<Long> quickJoinFightRoom(Integer userId, String model);
}
@@ -0,0 +1,7 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.domain.other.TtFightUser;
public interface ApiFightUserService extends IService<TtFightUser> {
}
@@ -0,0 +1,18 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.other.ApiLuckyUpgradeBody;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.UpgradeBodyA;
import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO;
import java.util.List;
public interface ApiLuckyUpgradeService {
List<ApiLuckyOrnamentsDataVO> getOrnamentsList(ApiLuckyUpgradeBody apiLuckyUpgradeBody);
R upgrade(TtUser ttUser, UpgradeBodyA upgradeBodyA);
// List<ApiLuckyUpgradeRecordDataVO> getUpgradeRecord(getUpgradeRecord param);
}
@@ -0,0 +1,22 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.domain.other.TtRedPack;
import com.ruoyi.domain.other.TtRedPacketRecord;
import com.ruoyi.thirdparty.wechat.domain.PtLoginUser;
import java.util.List;
public interface ApiRedPackService {
List<TtRedPack> getRedPackList(PtLoginUser user, Integer type);
AjaxResult getRedPack(Long userId, Long redPackId);
List<TtRedPacketRecord> getActivityDataHis(PtLoginUser user);
AjaxResult getRedPackDataDetail(PtLoginUser user, Long recordId);
AjaxResult getRedPackByKey(LoginUser user, String redPackKey);
}
@@ -0,0 +1,37 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.roll.GetRollOpenPrizeParam;
import com.ruoyi.domain.dto.roll.GetRollPlayersParam;
import com.ruoyi.domain.dto.roll.GetRollPrizePool;
import com.ruoyi.domain.entity.roll.TtRoll;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.vo.RollDetailsDataVO;
import com.ruoyi.domain.vo.RollListDataVO;
import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO;
import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO;
import com.ruoyi.domain.vo.roll.RollUserVO;
import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest;
import com.ruoyi.playingmethod.controller.ApiRollController.JoinRollParam;
import java.util.List;
public interface ApiRollService extends IService<TtRoll> {
R joinRoll(JoinRollParam param, TtUser player);
R endROLL(Integer rollId);
// List<RollListDataVO> getRollList(GetRollListParam param);
List<RollListDataVO> getRollList(GetRollListParamRequest param);
R<RollDetailsDataVO> getRollDetails(Integer rollId, Long userId);
R<List<RollUserVO>> getRollPlayers(GetRollPlayersParam param);
R<List<RollJackpotOrnamentsVO>> getRollPrizePool(GetRollPrizePool param);
R<List<TtBoxRecordsVO>> getRollOpenPrize(GetRollOpenPrizeParam param);
}
@@ -0,0 +1,40 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.domain.dto.upgrade.MyUpgradeRecordRequest;
import com.ruoyi.domain.dto.upgrade.UpgradeCondition;
import com.ruoyi.domain.dto.upgrade.UpgradeRecordPageRequest;
import com.ruoyi.domain.other.*;
import com.ruoyi.domain.vo.ApiLuckyUpgradeRecentPullVO;
import java.util.List;
public interface ApiUpgradeRecordService extends IService<TtUpgradeRecord> {
R historyDetail(UpgradeCondition param);
R tenTopDetail(Boolean isVictory);
/**
* 当前登录用户的幸运升级历史记录(高性能版)
* <p>仅按 user_id 单字段精准过滤 + open_time 倒序,依赖 (user_id, open_time) 复合索引,
* 同一用户记录避免按行回查用户表,最多查询一次用户信息。</p>
*
* @param userId 当前登录用户ID(从Token获取,不信任前端入参)
* @param request 分页与可选过滤条件
*/
R myHistoryRecord(Integer userId, MyUpgradeRecordRequest request);
/** 最近100条拉取记录(按 upgradeId 查,所有用户) */
List<ApiLuckyUpgradeRecentPullVO> getRecentPulls(Integer upgradeId);
/**
* 幸运升级记录分页(全服/我的 × 当前饰品/全部饰品)
*
* @param userId 我的记录=当前登录用户ID;全服记录传 null
* @param req 查询与分页条件
*/
R recordPage(Integer userId, UpgradeRecordPageRequest req);
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.domain.entity.TtUserBlendErcash;
public interface ApiUserBlendErcashService extends IService<TtUserBlendErcash> {
}
@@ -0,0 +1,30 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.domain.vo.OpenBoxVO;
import com.ruoyi.playingmethod.model.ApiWelfare;
import com.ruoyi.playingmethod.model.vo.ApiWelfareMonthlyRechargesVO;
import java.util.List;
public interface ApiWelfareMonthlyRechargesService {
/**
* 获取福利列表
*/
List<ApiWelfareMonthlyRechargesVO> getWelfareList(Long userId);
/**
* 领取福利
*/
OpenBoxVO claimWelfare(Integer welfareId, Long userId);
/**
* 检查是否符合领取条件
*/
boolean checkEligible(Integer welfareId, Long userId);
/**
* 检查是否已领取
*/
boolean checkClaimed(Integer welfareId, Long userId);
}
@@ -0,0 +1,55 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.domain.entity.TtWelfareBoxInventory;
import com.ruoyi.domain.vo.OpenBoxVO;
import com.ruoyi.playingmethod.model.ApiWelfare;
import java.util.List;
public interface ApiWelfareService {
/**
* 获取福利列表
*/
List<ApiWelfare> getWelfareList(Long userId);
/**
* 领取福利
*/
OpenBoxVO claimWelfare(Integer welfareId, Long userId);
/**
* 检查是否不符合领取条件
*/
boolean checkNotEligible(Integer welfareId, Long userId);
/**
* 检查是否已领取
*/
boolean checkClaimed(Integer welfareId, Long userId);
/**
* 每日领取福利箱子(不开箱,放入库存)
*/
TtWelfareBoxInventory claimDailyBox(Long userId);
/**
* 定时任务批量发放每日福利箱子(无循环DB操作)
*
* @param users 待发放的用户列表
* @return [成功数, 跳过数]
*/
int[] batchAutoIssueDailyBoxes(List<com.ruoyi.domain.entity.sys.TtUser> users);
/**
* 查询用户未开启的福利箱子列表
*/
List<TtWelfareBoxInventory> getMyWelfareBoxes(Long userId);
/**
* 批量开箱(开箱结果默认放入库存)
* @param boxInventoryIds 福利箱子库存ID列表
* @return 开箱结果列表
*/
List<OpenBoxVO> openWelfareBoxes(List<Long> boxInventoryIds, Long userId);
}
@@ -0,0 +1,379 @@
package com.ruoyi.playingmethod.service;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.TtFightMapper;
import com.ruoyi.admin.mapper.TtOrnamentMapper;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.vo.fight.FightBoxVO;
import com.ruoyi.domain.vo.fight.FightBrief;
import com.ruoyi.domain.vo.fight.FightState;
import com.ruoyi.framework.websocket.connector.PushService;
import com.ruoyi.framework.websocket.connector.SubscriptionManager;
import com.ruoyi.framework.websocket.connector.WsResult;
import com.ruoyi.playingmethod.websocket.constant.SMsgKey;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON;
import com.ruoyi.domain.common.constant.UserType;
@Slf4j
@Component
public class FightStateManager {
@Autowired
private RedisCache redisCache;
@Autowired
private PushService pushService;
@Autowired
private TtFightMapper fightMapper;
@Autowired
private TtOrnamentMapper ttOrnamentMapper;
@Autowired
private TtBoxRecordsService boxRecordsService;
@Value("${mkcsgo.fight.roundTime:6000}")
private int fightRoundTime;
@Value("${mkcsgo.fight.startingDuration:3000}")
private int startingDuration;
@Value("${mkcsgo.fight.revealDuration:4000}")
private int revealDuration;
@Value("${mkcsgo.fight.settledDuration:1000}")
private int settledDuration;
private static final String STATE_KEY_PREFIX = "fight_state:";
private static final int STATE_TTL_MINUTES = 10;
public static final String PHASE_WAITING = "WAITING";
public static final String PHASE_STARTING = "STARTING";
public static final String PHASE_ROUND_OPENING = "ROUND_OPENING";
public static final String PHASE_REVEAL = "REVEAL";
public static final String PHASE_SETTLED = "SETTLED";
private final Executor stateExecutor = Executors.newVirtualThreadPerTaskExecutor();
public void startFightProgress(TtFight fight, List<TtBoxRecords> allRecords,
List<Integer> winnerIds, List<FightBoxVO> fightBoxVOList) {
// 磨损度不持久化到 tt_box_records,统一从 tt_ornament 实时查询填充
fillExteriorNames(allRecords);
Map<Integer, List<TtBoxRecords>> roundMap = allRecords.stream()
.collect(Collectors.groupingBy(r ->
r.getFightRoundNumber() != null ? r.getFightRoundNumber() : 0));
FightState fullState = FightState.builder()
.phase(PHASE_STARTING)
.currentRound(0)
.totalRounds(fight.getRoundNumber())
.fight(FightBrief.from(fight))
.revealedRounds(new ArrayList<>())
.winnerIds(winnerIds)
.fightBoxVOList(fightBoxVOList)
.build();
saveState(fight.getId(), fullState);
FightState slimStart = FightState.builder()
.phase(PHASE_STARTING)
.totalRounds(fight.getRoundNumber())
.build();
broadcastState(fight.getId(), slimStart, "对战开始");
stateExecutor.execute(() -> runFightProgression(fight, roundMap, winnerIds, allRecords));
}
/**
* 从 tt_ornament 查询磨损度并回填到对战开箱记录(tt_box_records 不持久化该字段)。
*/
private void fillExteriorNames(List<TtBoxRecords> records) {
if (records == null || records.isEmpty()) {
return;
}
List<Long> ornamentIds = records.stream()
.map(TtBoxRecords::getOrnamentId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (ornamentIds.isEmpty()) {
return;
}
Map<Long, String> exteriorMap = new LambdaQueryChainWrapper<>(ttOrnamentMapper)
.in(TtOrnament::getId, ornamentIds)
.list()
.stream()
// 印花胶囊/武器箱/安慰奖等无磨损度饰品 exterior_name 为 null,必须过滤:
// 否则 Collectors.toMap 底层 Map.merge 对 null 值抛 NPE,导致 startFightProgress
// 在广播 STARTING 前中断,对局 status 已置 1 但前端永远卡在准备中 (WAITING)
// 非标准磨损度(如"普通")统一替换为"崭新出厂"
.collect(Collectors.toMap(
TtOrnament::getId,
o -> {
String exterior = o.getExteriorName();
// 标准 5 种磨损度才保留原值,其他统一为崭新出厂
if (exterior == null) return "崭新出厂";
boolean isStandard = "崭新出厂".equals(exterior)
|| "略有磨损".equals(exterior)
|| "久经沙场".equals(exterior)
|| "破损不堪".equals(exterior)
|| "战痕累累".equals(exterior);
return isStandard ? exterior : "崭新出厂";
},
(a, b) -> a
));
for (TtBoxRecords r : records) {
if (r.getOrnamentId() != null) {
r.setExteriorName(exteriorMap.get(r.getOrnamentId()));
}
}
}
private void runFightProgression(TtFight fight,
Map<Integer, List<TtBoxRecords>> roundMap,
List<Integer> winnerIds,
List<TtBoxRecords> allRecords) {
Integer fightId = fight.getId();
String channel = SubscriptionManager.fightChannel(fightId.longValue());
int totalRounds = fight.getRoundNumber();
try {
Thread.sleep(startingDuration);
playRounds(fightId, roundMap, totalRounds);
playReveal(fightId, allRecords, winnerIds);
playSettled(fightId, channel);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.warn("对战{}进度推进被中断", fightId);
} catch (Exception e) {
log.error("对战{}进度推进异常", fightId, e);
// 兜底:异常时强制结算,避免产生僵尸对战(settleFight 内部 CAS eq(status,1),幂等安全)
try {
settleFight(fightId);
} catch (Exception ex) {
log.error("对战{}异常兜底结算失败", fightId, ex);
}
try {
pushService.closeChannel(channel);
} catch (Exception ignored) {}
} finally {
stateExecutor.execute(() -> {
try { Thread.sleep(30000); } catch (InterruptedException ignored) {}
redisCache.deleteObject(STATE_KEY_PREFIX + fightId);
});
}
}
private void playRounds(Integer fightId, Map<Integer, List<TtBoxRecords>> roundMap, int totalRounds) throws InterruptedException {
for (int round = 1; round <= totalRounds; round++) {
final int currentRound = round;
List<TtBoxRecords> records = roundMap.get(round);
if (records == null) records = Collections.emptyList();
final List<TtBoxRecords> currentRecords = records;
updateState(fightId, state -> {
state.setPhase(PHASE_ROUND_OPENING);
state.setCurrentRound(currentRound);
state.getRevealedRounds().add(
FightState.RoundResult.builder()
.round(currentRound).records(currentRecords).build());
});
List<FightState.RoundRecord> roundResults = currentRecords.stream()
.map(r -> FightState.RoundRecord.builder()
.recordId(r.getId())
.userId(r.getUserId())
.ornamentId(r.getOrnamentId())
.boxId(r.getBoxId())
.ornamentName(r.getOrnamentName())
.ornamentsPrice(r.getOrnamentsPrice())
.imageUrl(r.getImageUrl())
.ornamentsLevelId(r.getOrnamentsLevelId())
.ornamentLevelImg(r.getOrnamentLevelImg())
.exteriorName(r.getExteriorName())
.build())
.collect(Collectors.toList());
FightState slimRound = FightState.builder()
.phase(PHASE_ROUND_OPENING)
.currentRound(currentRound)
.totalRounds(totalRounds)
.roundResults(roundResults)
.build();
broadcastState(fightId, slimRound, "第" + currentRound + "回合");
// 最后一回合无需等待,直接进入 REVEAL(前端自行控制最后一回合动画节奏)
if (round < totalRounds) {
Thread.sleep(fightRoundTime);
}
}
List<TtBoxRecords> consolationRecords = roundMap.get(0);
if (consolationRecords != null && !consolationRecords.isEmpty()) {
updateState(fightId, state -> {
state.getRevealedRounds().add(
FightState.RoundResult.builder()
.round(0).records(consolationRecords).build());
});
}
}
private void playReveal(Integer fightId, List<TtBoxRecords> allRecords, List<Integer> winnerIds) throws InterruptedException {
FightState revealState = buildRevealState(allRecords, winnerIds);
updateState(fightId, state -> {
state.setPhase(PHASE_REVEAL);
state.setCurrentRound(null);
state.setWinnerIds(winnerIds);
state.setSettlement(revealState.getSettlement());
});
// DB 结算提前到最后一回合推送后立即执行:HTTP 拉结果即刻可用(观战/参与者均可提前拉取数据)
try {
settleFight(fightId);
} catch (Exception e) {
log.error("对战{}提前结算失败", fightId, e);
}
// 等待前端播放最后一回合开箱动画,再广播 REVEAL(避免动画未播完就触发前端弹窗)
Thread.sleep(fightRoundTime);
broadcastState(fightId, revealState, "对战结果揭示");
}
private void playSettled(Integer fightId, String channel) throws InterruptedException {
FightState slimSettled = FightState.builder().phase(PHASE_SETTLED).build();
broadcastState(fightId, slimSettled, "对战已结算");
Thread.sleep(settledDuration);
pushService.closeChannel(channel);
log.info("对战{}状态同步完成,频道已关闭", fightId);
}
private FightState buildRevealState(List<TtBoxRecords> allRecords, List<Integer> winnerIds) {
List<FightState.RoundAssignment> roundAssignments = new ArrayList<>();
List<FightState.PrizeRecord> prizeRecords = new ArrayList<>();
for (TtBoxRecords r : allRecords) {
if (r.getFightRoundNumber() != null) {
roundAssignments.add(FightState.RoundAssignment.builder()
.recordId(r.getId())
.userId(r.getUserId())
.ornamentId(r.getOrnamentId())
.boxId(r.getBoxId())
.ornamentsLevelId(r.getOrnamentsLevelId())
.ornamentLevelImg(r.getOrnamentLevelImg())
.exteriorName(r.getExteriorName())
.holderUserId(r.getHolderUserId())
.build());
} else {
prizeRecords.add(FightState.PrizeRecord.builder()
.recordId(r.getId())
.userId(r.getUserId())
.ornamentId(r.getOrnamentId())
.holderUserId(r.getHolderUserId())
.ornamentName(r.getOrnamentName())
.ornamentsPrice(r.getOrnamentsPrice())
.imageUrl(r.getImageUrl())
.ornamentsLevelId(r.getOrnamentsLevelId())
.ornamentLevelImg(r.getOrnamentLevelImg())
.exteriorName(r.getExteriorName())
.build());
}
}
FightState.Settlement settlement = FightState.Settlement.builder()
.roundAssignments(roundAssignments)
.prizeRecords(prizeRecords)
.build();
return FightState.builder()
.phase(PHASE_REVEAL)
.winnerIds(winnerIds)
.settlement(settlement)
.build();
}
private void settleFight(Integer fightId) {
new LambdaUpdateChainWrapper<>(fightMapper)
.eq(TtFight::getId, fightId)
.eq(TtFight::getStatus, 1)
.set(TtFight::getStatus, 2)
.set(TtFight::getEndTime, new Timestamp(System.currentTimeMillis()))
.update();
LambdaUpdateWrapper<TtBoxRecords> update = new LambdaUpdateWrapper<>();
update.eq(TtBoxRecords::getFightId, fightId)
// 机器人持有的饰品不入背包(仅供前端展示),保持"背包不显示"状态
.and(w -> w.isNull(TtBoxRecords::getHolderUserId)
.or().notInSql(TtBoxRecords::getHolderUserId,
"SELECT user_id FROM tt_user WHERE user_type = '" + UserType.ROBOT_USER.getCode() + "'"))
.set(TtBoxRecords::getStatus, IN_PACKSACK_ON.getCode());
boxRecordsService.update(update);
TtFight fight = fightMapper.selectById(fightId);
Map<String, Object> data = new HashMap<>();
data.put("rows", Arrays.asList(fight));
log.info("对局{}结算, 推送HALL频道: key={}, data.rows.size={}, fight.status={}",
fightId, SMsgKey.FIGHT_ROOM_UPDATE, 1, fight.getStatus());
pushService.broadcastToChannel(
SubscriptionManager.hallChannel(),
WsResult.ok(SMsgKey.FIGHT_ROOM_UPDATE, data, "对战结束"));
}
public FightState getStateSnapshot(Integer fightId) {
return redisCache.getCacheObject(STATE_KEY_PREFIX + fightId);
}
public FightState buildWaitingState(TtFight fight, List<FightBoxVO> fightBoxVOList) {
return FightState.builder()
.phase(PHASE_WAITING)
.totalRounds(fight.getRoundNumber())
.fight(FightBrief.from(fight))
.fightBoxVOList(fightBoxVOList)
.build();
}
private void saveState(Integer fightId, FightState state) {
redisCache.setCacheObject(STATE_KEY_PREFIX + fightId, state, STATE_TTL_MINUTES, TimeUnit.MINUTES);
}
private void updateState(Integer fightId, Consumer<FightState> updater) {
FightState state = redisCache.getCacheObject(STATE_KEY_PREFIX + fightId);
if (state == null) {
log.warn("对战{}状态已过期", fightId);
return;
}
updater.accept(state);
saveState(fightId, state);
}
private void broadcastState(Integer fightId, FightState state, String msg) {
pushService.broadcastToChannel(
SubscriptionManager.fightChannel(fightId.longValue()),
WsResult.ok(SMsgKey.FIGHT_STATE, state, msg));
}
}
@@ -0,0 +1,14 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.playingmethod.entity.TtAttendanceRecord;
import java.util.List;
public interface IApiAttendanceRecordService {
TtAttendanceRecord selectByUid(int uid);
int insert(int uid);
List<TtAttendanceRecord> selectSevenAttendance(int uid);
}
@@ -0,0 +1,11 @@
package com.ruoyi.playingmethod.service;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import java.util.List;
public interface IApiReplacementRecordService {
AjaxResult synthesizeItems(LoginUser user, List<Long> itemIds);
}
@@ -0,0 +1,109 @@
package com.ruoyi.playingmethod.service.impl;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.ruoyi.playingmethod.entity.TtAttendanceRecord;
import com.ruoyi.playingmethod.mapper.ApiAttendanceRecordMapper;
import com.ruoyi.playingmethod.service.IApiAttendanceRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.text.SimpleDateFormat;
import java.util.*;
@Service
@Slf4j
public class ApiAttendanceRecordServiceImpl implements IApiAttendanceRecordService{
private final ApiAttendanceRecordMapper apiTtAttendanceRecordMapper;
public ApiAttendanceRecordServiceImpl(ApiAttendanceRecordMapper apiTtAttendanceRecordMapper) {
this.apiTtAttendanceRecordMapper = apiTtAttendanceRecordMapper;
}
@Override
public TtAttendanceRecord selectByUid(int uid) {
return apiTtAttendanceRecordMapper.selectByUid(uid);
}
@Override
public int insert(int uid) {
TtAttendanceRecord ttAttendanceRecord = new TtAttendanceRecord();
ttAttendanceRecord.setUserId(uid);
ttAttendanceRecord.setCreateTime(new Date());
ttAttendanceRecord.setCoin(1);
return apiTtAttendanceRecordMapper.insertAttendanceRecord(ttAttendanceRecord);
}
@Override
public List<TtAttendanceRecord> selectSevenAttendance(int uid) {
List<TtAttendanceRecord> ttAttendanceRecords = new ArrayList<>();
//周一
DateTime dateTime = DateUtil.beginOfWeek(new Date());
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(dateTime));
TtAttendanceRecord zhouYittAttendanceRecor = new TtAttendanceRecord();
if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(dateTime)) != null )zhouYittAttendanceRecor.setIsStatus("1");
zhouYittAttendanceRecor.setCreateTime(dateTime);
ttAttendanceRecords.add(zhouYittAttendanceRecor);
//周二
Calendar calendar = new GregorianCalendar();
calendar.setTime(dateTime);
calendar.add(Calendar.DATE, 1);
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime()));
TtAttendanceRecord zhouErttAttendanceRecord = new TtAttendanceRecord();
if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null)zhouErttAttendanceRecord.setIsStatus("1");
zhouErttAttendanceRecord.setCreateTime(calendar.getTime());
ttAttendanceRecords.add(zhouErttAttendanceRecord);
//周三
calendar.add(Calendar.DATE, 1);
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime()));
TtAttendanceRecord zhouSanttAttendanceRecord = new TtAttendanceRecord();
if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouSanttAttendanceRecord.setIsStatus("1");
zhouSanttAttendanceRecord.setCreateTime(calendar.getTime());
ttAttendanceRecords.add(zhouSanttAttendanceRecord);
//周四
calendar.add(Calendar.DATE, 1);
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime()));
TtAttendanceRecord zhouSittAttendanceRecord = new TtAttendanceRecord();
if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouSittAttendanceRecord.setIsStatus("1");
zhouSittAttendanceRecord.setCreateTime(calendar.getTime());
ttAttendanceRecords.add(zhouSittAttendanceRecord);
//周五
calendar.add(Calendar.DATE, 1);
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime()));
TtAttendanceRecord zhouWuttAttendanceRecord = new TtAttendanceRecord();
if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouWuttAttendanceRecord.setIsStatus("1");
zhouWuttAttendanceRecord.setCreateTime(calendar.getTime());
ttAttendanceRecords.add(zhouWuttAttendanceRecord);
//周六
calendar.add(Calendar.DATE, 1);
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime()));
TtAttendanceRecord zhouLiuttAttendanceRecord = new TtAttendanceRecord();
if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouLiuttAttendanceRecord.setIsStatus("1");
zhouLiuttAttendanceRecord.setCreateTime(calendar.getTime());
ttAttendanceRecords.add(zhouLiuttAttendanceRecord);
//周天
calendar.add(Calendar.DATE, 1);
System.out.println(new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime()));
TtAttendanceRecord zhouRittAttendanceRecord = new TtAttendanceRecord();
if (apiTtAttendanceRecordMapper.selectSevenAttendance(uid,new SimpleDateFormat("yyyy-MM-dd").format(calendar.getTime())) != null )zhouRittAttendanceRecord.setIsStatus("1");
zhouRittAttendanceRecord.setCreateTime(calendar.getTime());
ttAttendanceRecords.add(zhouRittAttendanceRecord);
return ttAttendanceRecords;
}
}
@@ -0,0 +1,8 @@
package com.ruoyi.playingmethod.service.impl;
import com.ruoyi.playingmethod.service.ApiBattleRoyaleService;
import org.springframework.stereotype.Service;
@Service
public class ApiBattleRoyaleServiceImpl implements ApiBattleRoyaleService {
}
@@ -0,0 +1,508 @@
package com.ruoyi.playingmethod.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtBoxOrnamentsMapper;
import com.ruoyi.admin.mapper.TtBoxTypeMapper;
import com.ruoyi.admin.mapper.TtCompRecordMapper;
import com.ruoyi.admin.model.UpdateUserAccountBo;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.admin.service.TtBoxService;
import com.ruoyi.admin.service.TtOrnamentsLevelService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.util.core.fight.LotteryMachine;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.MessageCategoryEnum;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.TtboxRecordSource;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.*;
import com.ruoyi.domain.vo.OpenBoxVO;
import com.ruoyi.playingmethod.domain.vo.CompValueVO;
import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper;
import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper;
import com.ruoyi.playingmethod.service.ApiBindBoxService;
import com.ruoyi.playingmethod.utils.customException.OrnamentNullException;
import com.ruoyi.framework.websocket.connector.PushService;
import com.ruoyi.framework.websocket.connector.SubscriptionManager;
import com.ruoyi.framework.websocket.connector.WsResult;
import com.ruoyi.playingmethod.websocket.constant.SMsgKey;
import com.ruoyi.message.service.MessageService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON;
import static java.math.BigDecimal.ROUND_HALF_UP;
@Service
@Slf4j
public class ApiBindBoxServiceImpl implements ApiBindBoxService {
private final TtUserService userService;
private final TtBoxService boxService;
private final TtBoxTypeMapper boxTypeMapper;
private final ApiBindBoxMapper bindBoxMapper;
private final TtBoxOrnamentsMapper boxOrnamentsMapper;
private final TtBoxRecordsService boxRecordsService;
private final RedisCache redisCache;
private final Executor customThreadPoolExecutor;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private LotteryMachine lotteryMachine;
@Autowired
private TtCompRecordMapper ttCompRecordMapper;
@Autowired
private ApiTtUserBlendErcashMapper apiTtUserBlendErcashMapper;
@Autowired
private TtOrnamentsLevelService ornamentsLevelService;
@Autowired
private PushService pushService;
@Autowired
private MessageService messageService;
public ApiBindBoxServiceImpl(TtUserService userService,
TtBoxService boxService,
TtBoxTypeMapper boxTypeMapper,
ApiBindBoxMapper bindBoxMapper,
TtBoxOrnamentsMapper boxOrnamentsMapper,
TtBoxRecordsService boxRecordsService,
RedisCache redisCache,
Executor customThreadPoolExecutor) {
this.userService = userService;
this.boxService = boxService;
this.boxTypeMapper = boxTypeMapper;
this.bindBoxMapper = bindBoxMapper;
this.boxOrnamentsMapper = boxOrnamentsMapper;
this.boxRecordsService = boxRecordsService;
this.redisCache = redisCache;
this.customThreadPoolExecutor = customThreadPoolExecutor;
}
//@PostConstruct
public void postConstruct() {
List<Integer> boxIds = boxOrnamentsMapper.selectBoxIdList();
if (ObjectUtils.isEmpty(boxIds)) return;
for (Integer boxId : boxIds) {
boxService.isReplenishment(boxId);
}
}
@Override
public TtBoxA getBoxData(Integer boxId) {
TtBoxA boxData = bindBoxMapper.getBoxData(boxId);
if (ObjectUtils.isEmpty(boxData)) return null;
List<TtOrnamentsA> boxOrnamentsList = bindBoxMapper.getBoxOrnamentsList(boxId);
if (ObjectUtils.isEmpty(boxOrnamentsList)) return null;
// 计算概率(仅展示爆率,不计算真实开奖概率)
BigDecimal sum = new BigDecimal(boxOrnamentsList.stream().mapToInt(TtOrnamentsA::getOdds).sum());
BigDecimal oneHundred = new BigDecimal("100").setScale(2);
BigDecimal max = BigDecimal.ZERO;
Long maxId = null;
BigDecimal add = BigDecimal.ZERO;
for (TtOrnamentsA vo : boxOrnamentsList) {
Integer odds = vo.getOdds();
BigDecimal oddsResult = new BigDecimal(odds).divide(sum, 4, ROUND_HALF_UP).multiply(oneHundred).setScale(2);
add = oddsResult.add(add);
if (max.compareTo(oddsResult) <= 0) {
max = oddsResult;
maxId = vo.getOrnamentId();
}
vo.setOddsResult(oddsResult);
}
// 平差
BigDecimal erroValue = oneHundred.subtract(add);
for (TtOrnamentsA vo : boxOrnamentsList) {
if (vo.getOrnamentId().equals(maxId)) {
vo.setOddsResult(vo.getOddsResult().add(erroValue));
break;
}
}
if (ObjectUtils.isEmpty(boxOrnamentsList)) return null;
boxOrnamentsList = boxOrnamentsList.stream().peek(boxOrnamentsDataVO -> boxOrnamentsDataVO.setOdds(null)).collect(Collectors.toList());
List<TtBoxLevelA> probabilityDistribution = bindBoxMapper.getProbabilityDistribution(boxId);
boxOrnamentsList.sort((o1, o2) -> o2.getUsePrice().compareTo(o1.getUsePrice()));
boxData.setBoxOrnamentsList(boxOrnamentsList);
boxData.setProbabilityDistribution(probabilityDistribution);
return boxData;
}
@Override
public List<TtOrnamentsA> getCompBoxData(Integer boxId) {
List<TtOrnamentsA> boxOrnamentsList = bindBoxMapper.getBoxOrnamentsList(boxId);
if (ObjectUtils.isEmpty(boxOrnamentsList))
return null;
// 剪除非补偿物品
boxOrnamentsList.removeIf(ttOrnamentsA -> ttOrnamentsA.getCompOdds() == 0);
// 计算概率
BigDecimal sum = new BigDecimal(boxOrnamentsList.stream().mapToInt(TtOrnamentsA::getCompOdds).sum());
BigDecimal oneHundred = new BigDecimal("100").setScale(2);
BigDecimal max = BigDecimal.ZERO;
Long maxId = null;
BigDecimal add = BigDecimal.ZERO;
for (TtOrnamentsA ttOrnamentsA : boxOrnamentsList) {
Integer compOdds = ttOrnamentsA.getCompOdds();
BigDecimal compOddsResult = new BigDecimal(compOdds).divide(sum, 4, ROUND_HALF_UP).multiply(oneHundred).setScale(2);
add = compOddsResult.add(add);
if (max.compareTo(compOddsResult) <= 0) {
max = compOddsResult;
maxId = ttOrnamentsA.getOrnamentId();
}
ttOrnamentsA.setCompOddsResult(compOddsResult);
}
// 平差
BigDecimal errorValue = oneHundred.subtract(add);
for (TtOrnamentsA vo : boxOrnamentsList) {
if (vo.getOrnamentId().equals(maxId)) {
vo.setOddsResult(vo.getCompOddsResult().add(errorValue));
break;
}
}
return boxOrnamentsList;
}
@Override
public TtBoxA getFightBoxData(Integer boxId) {
TtBoxA boxData = bindBoxMapper.getBoxData(boxId);
if (ObjectUtils.isEmpty(boxData)) return null;
// 对战宝箱用 robot_odds 作为展示概率
List<TtOrnamentsA> boxOrnamentsList = bindBoxMapper.getFightBoxOrnamentsList(boxId);
if (ObjectUtils.isEmpty(boxOrnamentsList)) return null;
// 过滤掉 robot_odds = 0 的饰品(对战奖池中实际不存在的饰品)
boxOrnamentsList.removeIf(v -> v.getOdds() == null || v.getOdds() == 0);
if (ObjectUtils.isEmpty(boxOrnamentsList)) return null;
BigDecimal sum = new BigDecimal(boxOrnamentsList.stream().mapToInt(TtOrnamentsA::getOdds).sum());
BigDecimal oneHundred = new BigDecimal("100").setScale(2);
BigDecimal max = BigDecimal.ZERO;
Long maxId = null;
BigDecimal add = BigDecimal.ZERO;
for (TtOrnamentsA vo : boxOrnamentsList) {
Integer odds = vo.getOdds();
BigDecimal oddsResult = new BigDecimal(odds).divide(sum, 4, ROUND_HALF_UP).multiply(oneHundred).setScale(2);
add = oddsResult.add(add);
if (max.compareTo(oddsResult) <= 0) {
max = oddsResult;
maxId = vo.getOrnamentId();
}
vo.setOddsResult(oddsResult);
}
// 平差
BigDecimal erroValue = oneHundred.subtract(add);
for (TtOrnamentsA vo : boxOrnamentsList) {
if (vo.getOrnamentId().equals(maxId)) {
vo.setOddsResult(vo.getOddsResult().add(erroValue));
break;
}
}
boxOrnamentsList = boxOrnamentsList.stream().peek(v -> v.setOdds(null)).collect(Collectors.toList());
List<TtBoxLevelA> probabilityDistribution = bindBoxMapper.getProbabilityDistribution(boxId);
boxOrnamentsList.sort((o1, o2) -> o2.getUsePrice().compareTo(o1.getUsePrice()));
boxData.setBoxOrnamentsList(boxOrnamentsList);
boxData.setProbabilityDistribution(probabilityDistribution);
return boxData;
}
@Override
public List<TtBoxVO> getBoxList(Integer boxTypeId, String homeFlag, String boxType, String isFight) {
List<TtBoxA> boxList = bindBoxMapper.getBoxList(boxTypeId, homeFlag, boxType, isFight);
if (CollectionUtils.isEmpty(boxList)) {
return new ArrayList<>();
}
// 性能优化:一次 SQL 批量加载所有 boxId 的 box_ornaments,避免 N+1
List<Integer> boxIds = boxList.stream()
.map(TtBoxA::getBoxId)
.filter(Objects::nonNull)
.collect(Collectors.toList());
Map<Integer, List<TtBoxOrnaments>> boxOrnamentsMap = new HashMap<>();
if (!boxIds.isEmpty()) {
List<TtBoxOrnaments> all = new LambdaQueryChainWrapper<>(boxOrnamentsMapper)
.in(TtBoxOrnaments::getBoxId, boxIds)
.list();
boxOrnamentsMap = all.stream()
.filter(o -> o != null && o.getBoxId() != null)
.collect(Collectors.groupingBy(TtBoxOrnaments::getBoxId));
}
// 内存过滤空箱子(保持原语义:存在且至少一个 realOdds > 0)
Map<Integer, List<TtBoxOrnaments>> finalBoxOrnamentsMap = boxOrnamentsMap;
List<TtBoxA> collect = boxList.stream().filter(item -> {
List<TtBoxOrnaments> list = finalBoxOrnamentsMap.get(item.getBoxId());
if (list == null || list.isEmpty()) return false;
for (TtBoxOrnaments ornaments : list) {
if (ornaments.getRealOdds() != null && ornaments.getRealOdds() > 0) {
return true;
}
}
return false;
}).collect(Collectors.toList());
return groupByBoxType(collect);
}
@Override
public List<TtBoxVO> groupByBoxType(List<TtBoxA> boxData) {
Map<Integer, List<TtBoxA>> collect = boxData.stream().collect(Collectors.groupingBy(TtBoxA::getBoxTypeId));
if (collect.isEmpty()) {
return new ArrayList<>();
}
// 性能优化:一次 SQL 批量加载所有 boxType,避免 N+1
List<Integer> typeIds = new ArrayList<>(collect.keySet());
Map<Integer, TtBoxType> typeMap = boxTypeMapper.selectBatchIds(typeIds).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(TtBoxType::getId, t -> t, (a, b) -> a));
List<TtBoxVO> resultList = new ArrayList<>();
for (Map.Entry<Integer, List<TtBoxA>> entry : collect.entrySet()) {
TtBoxType ttBoxType = typeMap.get(entry.getKey());
if (StringUtils.isNull(ttBoxType)) continue;
// 过滤掉前端隐藏的分类
if ("1".equals(ttBoxType.getIsShow())) continue;
TtBoxVO boxTypeA = TtBoxVO.builder().build();
boxTypeA.setBoxTypeId(ttBoxType.getId());
boxTypeA.setBoxTypeName(ttBoxType.getName());
boxTypeA.setIcon(ttBoxType.getIcon());
boxTypeA.setSort(ttBoxType.getSort());
boxTypeA.setBoxList(entry.getValue());
resultList.add(boxTypeA);
}
resultList.sort(Comparator.comparingInt(a -> a.getSort() == null ? Integer.MAX_VALUE : a.getSort()));
return resultList;
}
@Override
public List<TtOrnamentsA> openBox(TtBox ttBox, Integer num, TtUser ttUser) {
return null;
}
@Override
public List<TtBoxRecords> addBoxRecord(TtUser ttUser, TtBox ttBox, Integer num) {
return null;
}
@Override
public List<TtBoxRecords> openBoxArithmetic(Integer fightId, TtUser ttUser, TtBox ttBox, Integer num) {
return null;
}
@Override
@Transactional(rollbackFor = Exception.class)
public R blindBox(TtUser user, TtBox box, Integer num) {
// 同步扣款
BigDecimal totalmoney = box.getPrice().multiply(new BigDecimal(num));
R r = upgradeAccounting(totalmoney, user);
if (R.isError(r)) {
// 透传原始 code(如余额不足 702),避免被默认覆盖为 500
return R.fail(r.getCode().intValue(), r.getMsg());
}
// 抽奖
List<TtBoxRecords> boxRecords = new ArrayList<>();
Map<Long, TtOrnamentsA> boxToOrnament = new HashMap<>();
for (int i = 0; i < num; i++) {
String ornamentId = lotteryMachine.singleLottery(user, box);
if (StringUtils.isBlank(ornamentId)) {
throw new OrnamentNullException("单次抽奖无结果#"+box.getBoxId()+"#"+ornamentId);
//return R.fail("系统繁忙,请稍后重试。");
}
// 宝箱详细信息
TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(box.getBoxId(), ornamentId);
if (ObjectUtil.isEmpty(ornamentsData)){
throw new OrnamentNullException("没有符合条件的宝箱信息#"+box.getBoxId()+"#"+ornamentId);
}
// 比对补偿箱类型开箱价格和奖品价值,记录补偿值。如果饰品大于等于门票,清空补偿值
if ("3".equals(box.getBoxType()) && ornamentsData.getUsePrice().compareTo(box.getPrice()) < 0) {
compRecord(user.getUserId(), box.getBoxId());
} else {
ttCompRecordMapper.deleteTtCompRecord(user.getUserId(), box.getBoxId());
}
// 抽奖成功,构建开箱记录数据
TtBoxRecords boxRecord = TtBoxRecords.builder()
.userId(user.getUserId())
.boxId(box.getBoxId())
.boxName(box.getBoxName())
.boxPrice(box.getPrice())
.ornamentId(Long.valueOf(ornamentId))
// .marketHashName(ornamentsData.)
.ornamentName(ObjectUtil.isNotEmpty(ornamentsData.getName()) ? ornamentsData.getName() : ObjectUtil.isNotEmpty(ornamentsData.getShortName()) ? ornamentsData.getShortName() : "无名称")
.imageUrl(ornamentsData.getImageUrl())
.ornamentsPrice(ornamentsData.getUsePrice())
.ornamentsLevelId(ornamentsData.getOrnamentsLevelId())
.ornamentLevelImg(ornamentsData.getLevelImg())
.holderUserId(user.getUserId())
.source(TtboxRecordSource.BLIND_BOX.getCode())
.status(IN_PACKSACK_ON.getCode())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
boxRecords.add(boxRecord);
boxToOrnament.put(Long.valueOf(ornamentId), ornamentsData);
}
// 同步保存游戏记录
boxRecordsService.saveBatch(boxRecords);
// 发送邮件通知
for (TtBoxRecords record : boxRecords) {
String priceStr = record.getOrnamentsPrice() == null ? "0"
: record.getOrnamentsPrice().stripTrailingZeros().toPlainString();
String content = "恭喜您在常规补给中获得饰品【" + record.getOrnamentName() + "】(价值 ¥" + priceStr + ")。";
messageService.sendNotice(
Long.valueOf(user.getUserId()),
MessageCategoryEnum.GAME_BOX.getTitle(),
content,
MessageCategoryEnum.GAME_BOX.getCode());
}
// 返回值
List<OpenBoxVO> openBoxVOs = boxRecords.stream().map(item -> {
OpenBoxVO openBoxVO = new OpenBoxVO();
BeanUtil.copyProperties(item, openBoxVO);
TtOrnamentsA ornamentsData = boxToOrnament.get(item.getOrnamentId());
if (ornamentsData != null) {
openBoxVO.setShortName(ornamentsData.getShortName());
openBoxVO.setExteriorName(ornamentsData.getExteriorName());
}
openBoxVO.setUsePrice(openBoxVO.getOrnamentsPrice());
openBoxVO.setLevelImg(item.getOrnamentLevelImg());
return openBoxVO;
}).collect(Collectors.toList());
// 异步任务 广播开箱消息
broadcastToBoxRoom(boxRecords);
return R.ok(openBoxVOs);
}
@Override
public CompValueVO getCompValue(TtUser ttUser, TtBox ttBox) {
CompValueVO compValueVO = new CompValueVO();
// 查询宝箱设定的补偿值
compValueVO.setDesiredCompValue(ttBox.getCompAmount());
// 查询用户当前补偿值
TtCompRecord ttCompRecord = ttCompRecordMapper.selectTtCompRecord(ttUser.getUserId(), ttBox.getBoxId());
if (Objects.isNull(ttCompRecord))
compValueVO.setCurrentCompValue(0);
else
compValueVO.setCurrentCompValue(ttCompRecord.getCompAmount());
return compValueVO;
}
// 扣款(经典开箱:差额规则——金币优先扣到与积分相等,超出部分金币与积分同步扣减)
public R upgradeAccounting(BigDecimal consumption, TtUser player) {
// 再次检查余额:与本次开箱所需金额比较
player = userService.getById(player.getUserId());
if (player.getAccountAmount() == null || player.getAccountAmount().compareTo(consumption) < 0) {
return R.fail(702, "账户余额不足");
}
return userService.updateUserAccount(player.getUserId(), consumption.negate(), TtAccountRecordSource.GAME_TYPE_01);
}
// 异步任务 广播开箱消息
public void broadcastToBoxRoom(List<TtBoxRecords> boxRecords) {
CompletableFuture.runAsync(() -> {
// 数据按boxId排序
Collections.sort(boxRecords, new Comparator<TtBoxRecords>() {
@Override
public int compare(TtBoxRecords o1, TtBoxRecords o2) {
return o1.getBoxId().compareTo(o2.getBoxId());
}
});
// 缓冲区
List<TtBoxRecords> cache = new ArrayList<>();
Integer flagBoxId = -1;
for (TtBoxRecords record : boxRecords) {
if (cache.isEmpty()) {
// 缓冲区为空,直接加入
cache.add(record);
flagBoxId = record.getBoxId();
continue;
} else {
// 不为空
if (record.getBoxId().equals(flagBoxId)) {
cache.add(record);
} else {
Integer bid = cache.get(0).getBoxId();
// 广播最新数据
pushService.broadcastToChannel(SubscriptionManager.bindboxChannel(bid.longValue()), WsResult.ok(SMsgKey.Blind_Box_Current_Data, cache, "开盒子更新。"));
cache.clear();
}
}
}
Integer bid = cache.get(0).getBoxId();
// 广播最新数据
pushService.broadcastToChannel(SubscriptionManager.bindboxChannel(bid.longValue()), WsResult.ok(SMsgKey.Blind_Box_Current_Data, cache, "开盒子更新。"));
}, customThreadPoolExecutor);
}
// 记录补偿值
public void compRecord(Integer userId, Integer boxId) {
// 查询当前补偿值
TtCompRecord ttCompRecord = ttCompRecordMapper.selectTtCompRecord(userId, boxId);
if (!Objects.isNull(ttCompRecord)) {
// 补偿值+1
ttCompRecord.setCompAmount(ttCompRecord.getCompAmount() + 1);
ttCompRecordMapper.updateTtCompRecord(ttCompRecord);
}
}
}
@@ -0,0 +1,11 @@
package com.ruoyi.playingmethod.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.domain.other.TtBoxOrnaments;
import com.ruoyi.playingmethod.mapper.ApiBoxOrnamentsMapper;
import com.ruoyi.playingmethod.service.ApiBoxOrnamentsService;
import org.springframework.stereotype.Service;
@Service
public class ApiBoxOrnamentsServiceImpl extends ServiceImpl<ApiBoxOrnamentsMapper, TtBoxOrnaments> implements ApiBoxOrnamentsService {
}
@@ -0,0 +1,65 @@
package com.ruoyi.playingmethod.service.impl;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.admin.mapper.*;
import com.ruoyi.domain.dto.boxRecords.TenTopQuery;
import com.ruoyi.domain.dto.boxRecords.queryCondition;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO;
import com.ruoyi.playingmethod.service.ApiBoxRecordsService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.*;
@Service
@Slf4j
public class ApiBoxRecordsServiceImpl extends ServiceImpl<TtBoxRecordsMapper, TtBoxRecords> implements ApiBoxRecordsService {
@Override
public List<TtBoxRecordsVO> byCondition(queryCondition condition) {
condition.setLimit((condition.getPage() - 1) * condition.getSize());
if (ObjectUtil.isNull(condition.getOrderByFie())) condition.setOrderByFie(1);
List<TtBoxRecordsVO> list = this.baseMapper.byCondition(
condition.getUserId(),
condition.getLimit(),
condition.getSize(),
condition.getOrderByFie(),
condition.getBoxRecordId(),
condition.getBoxId(),
condition.getUserType(),
condition.getSource(),
condition.getStatus(),
condition.getOrnamentPriceMin(),
condition.getOrnamentPriceMax(),
condition.getOrnamentLevelIds()
);
return list;
}
@Override
public List<TtBoxRecordsVO> tenTopQuery(TenTopQuery query) {
query.setLimit((query.getPage() - 1) * query.getSize());
return this.baseMapper.tenTopQuery(
query.getLimit(),
query.getSize(),
query.getSource(),
query.getStatus()
);
}
/**
* 「今日百佳 / 历史百佳」不缓存,直接查库。
* 索引已建好、查询为索引 top100,直接查库也很快、彻底无延迟,仅并发略有上升。
*/
@Override
public List<TtBoxRecordsVO> topHundredQuery(List<Integer> status, List<Integer> source, boolean todayOnly) {
return this.baseMapper.topHundredQuery(100, status, source, todayOnly);
}
}
@@ -0,0 +1,121 @@
package com.ruoyi.playingmethod.service.impl;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.mapper.TtOrnamentMapper;
import com.ruoyi.domain.common.constant.TtboxRecordStatus;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.other.TtOrnamentsLevel;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.admin.service.TtOrnamentsLevelService;
import com.ruoyi.admin.service.WebsitePropertyService;
import com.ruoyi.admin.util.RandomUtils;
import com.ruoyi.domain.vo.WebsitePropertyDataVO;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.playingmethod.mapper.ApiCompoundMapper;
import com.ruoyi.playingmethod.service.ApiCompoundService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.service.ApiUserPackSackService;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Comparator;
import java.util.Date;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtboxRecordSource.MALL_EXCHANGE;
@Service
@Slf4j
public class ApiCompoundServiceImpl implements ApiCompoundService {
private final ISysConfigService configService;
private final ApiUserPackSackService userPackSackService;
private final WebsitePropertyService websitePropertyService;
private final TtBoxRecordsService boxRecordsService;
private final TtOrnamentMapper ornamentsMapper;
private final TtOrnamentsLevelService ornamentsLevelService;
private final ApiCompoundMapper apiCompoundMapper;
public ApiCompoundServiceImpl(ISysConfigService configService,
ApiUserPackSackService userPackSackService,
WebsitePropertyService websitePropertyService,
TtBoxRecordsService boxRecordsService,
TtOrnamentMapper ornamentsMapper,
TtOrnamentsLevelService ornamentsLevelService,
ApiCompoundMapper apiCompoundMapper) {
this.configService = configService;
this.userPackSackService = userPackSackService;
this.websitePropertyService = websitePropertyService;
this.boxRecordsService = boxRecordsService;
this.ornamentsMapper = ornamentsMapper;
this.ornamentsLevelService = ornamentsLevelService;
this.apiCompoundMapper = apiCompoundMapper;
}
@Override
public R<UserPackSackDataVO> compound(List<Long> packSackIds, TtUser ttUser) {
List<TtBoxRecords> boxRecordsList = userPackSackService.packSackHandle(packSackIds, ttUser, 3);
if (ObjectUtils.isEmpty(boxRecordsList)) return R.fail("请选择饰品后再进行合成!");
List<Long> ornamentsIds = boxRecordsList.stream().map(TtBoxRecords::getOrnamentId).collect(Collectors.toList());
List<Long> websitePropertyIds = websitePropertyService.list().stream().map(WebsitePropertyDataVO::getId).collect(Collectors.toList());
websitePropertyIds.retainAll(ornamentsIds);
if (!websitePropertyIds.isEmpty()) return R.fail("选择的饰品中存在网站专属道具,请重新选择!");
String maxCompoundNumStr = configService.selectConfigByKey("maxCompoundNum");
int maxCompoundNum = Integer.parseInt(maxCompoundNumStr);
if (boxRecordsList.size() > maxCompoundNum) return R.fail("最多选择" + maxCompoundNum + "个饰品!");
BigDecimal compoundBeanTotal = boxRecordsList.stream().map(TtBoxRecords::getOrnamentsPrice).reduce(BigDecimal.ZERO, BigDecimal::add);
String compoundMinPriceStr = configService.selectConfigByKey("compoundMinPrice"); // 获取配置中的汰换最低价
BigDecimal compoundMinPrice = new BigDecimal(compoundMinPriceStr);
if (compoundBeanTotal.compareTo(compoundMinPrice) < 0)
return R.fail("选择的饰品总价必须大于" + compoundMinPrice + "游戏币才可以汰换哦!");
Optional<BigDecimal> maxPriceOptional = boxRecordsList.stream().map(TtBoxRecords::getOrnamentsPrice).max(Comparator.naturalOrder());
if (!maxPriceOptional.isPresent()) return null;
try {
String compoundMinPremiumRateStr = configService.selectConfigByKey("compoundMinPremiumRate");
BigDecimal minPremiumRatePrice = new BigDecimal(compoundMinPremiumRateStr).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP)
.multiply(maxPriceOptional.get());
String compoundMaxPremiumRateStr = configService.selectConfigByKey("compoundMaxPremiumRate");
BigDecimal maxPremiumRatePrice = new BigDecimal(compoundMaxPremiumRateStr).divide(new BigDecimal(100), 2, RoundingMode.HALF_UP)
.multiply(compoundBeanTotal);
BigDecimal minPrice = maxPriceOptional.get().subtract(minPremiumRatePrice);
BigDecimal maxPrice = compoundBeanTotal.subtract(maxPremiumRatePrice);
List<TtOrnament> ornamentsList = new LambdaQueryChainWrapper<>(ornamentsMapper).in(TtOrnament::getType, 2, 3, 4, 5)
.between(TtOrnament::getUsePrice, minPrice, maxPrice).list();
if (StringUtils.isNull(ornamentsList) || ornamentsList.isEmpty())
return R.fail("汰换失败,饰品池中未筛选出符合您价格区间的饰品!");
int randomIndex = RandomUtils.getRandomIndex(ornamentsList.size());
TtOrnament ttOrnament = ornamentsList.get(randomIndex);
List<Integer> levelIds = ornamentsLevelService.list().stream().map(TtOrnamentsLevel::getId).collect(Collectors.toList());
Integer ornamentsLevelId = levelIds.get(RandomUtils.getRandomIndex(levelIds.size()));
boxRecordsService.updateBatchById(boxRecordsList, 1);
TtBoxRecords boxRecords = TtBoxRecords.builder().build();
boxRecords.setUserId(ttUser.getUserId());
boxRecords.setOrnamentId(ttOrnament.getId());
boxRecords.setOrnamentsPrice(ttOrnament.getUsePrice());
boxRecords.setOrnamentsLevelId(ornamentsLevelId);
boxRecords.setStatus(TtboxRecordStatus.IN_PACKSACK_ON.getCode());
boxRecords.setCreateTime(new Date());
boxRecords.setSource(MALL_EXCHANGE.getCode()); //// TODO: 2024/4/1 类型不对
boxRecords.setHolderUserId(ttUser.getUserId());
boxRecordsService.save(boxRecords);
UserPackSackDataVO compoundData = apiCompoundMapper.selectCompoundDataById(boxRecords.getId());
return R.ok(compoundData);
} catch (Exception e) {
return R.fail("数据异常!");
}
}
@Override
public List<UserPackSackDataVO> getUserCompoundRecord(Integer userId) {
return apiCompoundMapper.selectCompoundRecordByUserId(userId);
}
}
@@ -0,0 +1,555 @@
package com.ruoyi.playingmethod.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.admin.mapper.TtBoxMapper;
import com.ruoyi.admin.mapper.TtExponentMapper;
import com.ruoyi.admin.mapper.TtExponentUserBoxMapper;
import com.ruoyi.admin.mapper.TtExponentUserMapper;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.admin.service.TtBoxService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.util.core.fight.LotteryMachine;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.TtboxRecordSource;
import com.ruoyi.domain.dto.exponent.*;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.exponent.TtExponent;
import com.ruoyi.domain.entity.exponent.TtExponentUser;
import com.ruoyi.domain.entity.exponent.TtExponentUserBox;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.domain.other.TtBoxA;
import com.ruoyi.domain.other.TtOrnamentsA;
import com.ruoyi.domain.vo.OpenBoxVO;
import com.ruoyi.playingmethod.mapper.ApiBindBoxMapper;
import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper;
import com.ruoyi.playingmethod.service.ApiExponentService;
import com.ruoyi.playingmethod.utils.customException.OrnamentNullException;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.function.Function;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON;
@Service
@Slf4j
public class ApiExponentServiceImpl implements ApiExponentService {
@Autowired
private TtExponentMapper ttExponentMapper;
@Autowired
private TtExponentUserMapper ttExponentUserMapper;
@Autowired
private TtBoxService boxService;
@Autowired
private TtUserService userService;
@Autowired
private LotteryMachine lotteryMachine;
@Autowired
private ApiBindBoxMapper bindBoxMapper;
@Autowired
private TtBoxRecordsService boxRecordsService;
@Autowired
private ApiTtUserBlendErcashMapper apiTtUserBlendErcashMapper;
@Autowired
private TtBoxMapper ttBoxMapper;
@Autowired
private TtExponentUserBoxMapper ttExponentUserBoxMapper;
@Override
public R join(TtUser ttUser, ExponentJoinParam param) {
if (param == null || param.getId() == null || param.getBoxId() == null || param.getForward() == null) {
return R.fail("参数不合法");
}
TtBox ttBox = boxService.getById(param.getBoxId());
if (ttBox == null) {
return R.fail("宝箱参数不合法");
}
// 同步扣款
R<Map<String, BigDecimal>> mapR = upgradeAccounting(ttBox.getPrice(), ttUser);
if (!mapR.getCode().equals(200)) {
return mapR;
}
TtExponentUser exponentUser = new TtExponentUser();
exponentUser.setExponentId(param.getId());
exponentUser.setBoxId(param.getBoxId());
exponentUser.setUserId(ttUser.getUserId());
exponentUser.setPrice(ttBox.getPrice());
exponentUser.setForward(param.getForward());
exponentUser.setStatus(0);
exponentUser.setBoxRecordId(0L);
exponentUser.setCreateTime(new Date());
exponentUser.setUpdateTime(new Date());
ttExponentUserMapper.insert(exponentUser);
return R.ok();
}
// 扣款
public R<Map<String, BigDecimal>> upgradeAccounting(BigDecimal consumption, TtUser player) {
// 再次检查余额
player = userService.getById(player.getUserId());
if (player.getAccountAmount() == null || player.getAccountAmount().compareTo(consumption) < 0) {
return R.fail("余额不足");
}
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate.eq(TtUser::getUserId, player.getUserId());
Map<String, BigDecimal> map;
if (player.getAccountAmount().compareTo(consumption) >= 0) {
userUpdate.set(TtUser::getAccountAmount, player.getAccountAmount().subtract(consumption));
map = MapUtil.builder("Amount", consumption).map();
} else {
BigDecimal subtract = consumption.subtract(player.getAccountAmount());
userUpdate
.set(TtUser::getAccountAmount, 0)
.set(TtUser::getAccountCredits, player.getAccountCredits().subtract(subtract));
map = MapUtil.builder("Amount", player.getAccountAmount()).map();
map.put("Credits", subtract);
}
userService.update(userUpdate);
// 综合消费日志
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(player.getUserId())
.amount(null)
.finalAmount(null)
.credits(null)
.finalCredits(null)
.total(consumption.negate()) // 收支合计
.type(TtAccountRecordType.OUTPUT.getCode())
.source(TtAccountRecordSource.EXPONENT_GAME_TYPE_01.getCode())
.remark(TtAccountRecordSource.EXPONENT_GAME_TYPE_01.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
apiTtUserBlendErcashMapper.insert(blendErcash);
return R.ok(map);
}
@Override
public R box(TtUser ttUser, Integer id) {
List<TtBoxA> boxList = bindBoxMapper.getBoxList(null, null, "5", null);
if (boxList.isEmpty()) {
return R.ok(new ArrayList<>(), "没有匹配的数据。");
}
List<ExponentBoxVo> boxVos = new ArrayList<>();
List<Integer> boxIds = new ArrayList<>();
for (TtBoxA ttBoxA : boxList) {
ExponentBoxVo boxVo = new ExponentBoxVo();
BeanUtils.copyProperties(ttBoxA, boxVo);
boxVo.setBoxId(ttBoxA.getBoxId());
boxIds.add(ttBoxA.getBoxId());
boxVos.add(boxVo);
}
// 查看当前登录者是否购买本期指数
LambdaQueryWrapper<TtExponentUser> wrapper1 = Wrappers.lambdaQuery();
wrapper1.in(TtExponentUser::getBoxId, boxIds);
wrapper1.eq(TtExponentUser::getExponentId, id);
wrapper1.eq(TtExponentUser::getUserId, ttUser.getUserId());
List<TtExponentUser> exponentUsers = ttExponentUserMapper.selectList(wrapper1);
Map<Integer, Integer> boxIdToUp = new HashMap<>();
Map<Integer, Integer> boxIdToDown = new HashMap<>();
if (CollectionUtils.isNotEmpty(exponentUsers)) {
for (TtExponentUser user : exponentUsers) {
if (user == null) {
continue;
}
if (Objects.equals(user.getForward(), 1)) {
int num = boxIdToUp.getOrDefault(user.getBoxId(), 0);
boxIdToUp.put(user.getBoxId(), num + 1);
}
if (Objects.equals(user.getForward(), 2)) {
int num = boxIdToDown.getOrDefault(user.getBoxId(), 0);
boxIdToDown.put(user.getBoxId(), num + 1);
}
}
}
for (ExponentBoxVo vo : boxVos) {
vo.setBuy_forward_up(boxIdToUp.getOrDefault(vo.getBoxId(), 0));
vo.setBuy_forward_down(boxIdToDown.getOrDefault(vo.getBoxId(), 0));
}
return R.ok(boxVos);
}
@Override
public R recenttenperiod() {
LambdaQueryWrapper<TtExponent> wrapper = Wrappers.lambdaQuery();
wrapper.orderByDesc(TtExponent::getId);
wrapper.last("limit 10");
List<TtExponent> db = ttExponentMapper.selectList(wrapper);
return R.ok(db);
}
/**
*
*/
@Override
public R info(TtUser ttUser) {
LambdaQueryWrapper<TtExponent> wrapper = Wrappers.lambdaQuery();
wrapper.orderByDesc(TtExponent::getId);
wrapper.last("limit 1");
TtExponent db = ttExponentMapper.selectOne(wrapper);
if (db == null) {
return R.fail("指数信息不存在");
}
ExponentInfoVo vo = new ExponentInfoVo();
Integer id = db.getId() + 1;
vo.setId(id);
vo.setExponent(0);
// 创建日期格式化对象,指定日期格式
SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd");
// 获取Calendar实例
Calendar calendar = Calendar.getInstance();
// 设置时间为今天凌晨
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// 获取今天凌晨时间
Date todayMidnight = calendar.getTime();
String period = "";
if (db.getCreateTime().after(todayMidnight)) {
period = formatter.format(new Date()) + String.format("%04d", (db.getNumber() + 1));
} else {
period = formatter.format(new Date()) + "0001";
}
vo.setPeriod(period);
int createTime = (int) (db.getCreateTime().getTime() / 1000);
vo.setEnd_time(createTime + 60);
vo.setPoints("");
vo.setState(0);
vo.setNumber(db.getNumber() + 1);
vo.setForward(0);
vo.setCreatetime(createTime + 30);
vo.setUpdatetime(createTime + 30);
int nowTime = (int) (System.currentTimeMillis() / 1000);
vo.setTimer(vo.getCreatetime() - nowTime);
// 查看当前登录者是否购买本期指数
LambdaQueryWrapper<TtExponentUser> wrapper2 = Wrappers.lambdaQuery();
wrapper2.eq(TtExponentUser::getExponentId, id);
wrapper2.eq(TtExponentUser::getUserId, ttUser.getUserId());
List<TtExponentUser> exponentUsers2 = ttExponentUserMapper.selectList(wrapper2);
// 若购买
if (CollectionUtils.isNotEmpty(exponentUsers2)) {
vo.setBuy(exponentUsers2.size());
BigDecimal up = BigDecimal.ZERO;
BigDecimal down = BigDecimal.ZERO;
for (TtExponentUser exponentUser : exponentUsers2) {
if (exponentUser == null) {
continue;
}
if (Objects.equals(exponentUser.getForward(), 1)) {
up = up.add(exponentUser.getPrice());
}
if (Objects.equals(exponentUser.getForward(), 2)) {
down = down.add(exponentUser.getPrice());
}
}
vo.setBuy_forward_up(up);
vo.setBuy_forward_down(down);
}
ExponentInfoVo last = new ExponentInfoVo();
last.setId(db.getId());
last.setExponent(db.getExponent());
period = formatter.format(db.getCreateTime()) + String.format("%04d", db.getNumber());
last.setPeriod(period);
int lastCreateTime = (int) (db.getCreateTime().getTime() / 1000);
last.setEnd_time(lastCreateTime + 30);
last.setPoints(db.getPoints());
last.setState(2);
last.setNumber(db.getNumber());
last.setForward(db.getForward());
last.setCreatetime(lastCreateTime);
last.setUpdatetime(lastCreateTime);
vo.setLast(last);
// 查看当前登录者是否购买本期指数
LambdaQueryWrapper<TtExponentUser> wrapper1 = Wrappers.lambdaQuery();
wrapper1.eq(TtExponentUser::getExponentId, db.getId());
wrapper1.eq(TtExponentUser::getUserId, ttUser.getUserId());
List<TtExponentUser> exponentUsers = ttExponentUserMapper.selectList(wrapper1);
ExponentJieguoVo jieguo = new ExponentJieguoVo();
jieguo.setPeriod(period);
jieguo.setExponent(db.getExponent());
jieguo.setForward(db.getForward());
// 若购买
if (CollectionUtils.isNotEmpty(exponentUsers)) {
jieguo.setSkinarr(openExponent(db.getForward(), ttUser, exponentUsers));
jieguo.setBuy(exponentUsers.size());
BigDecimal up = BigDecimal.ZERO;
BigDecimal down = BigDecimal.ZERO;
BigDecimal price = BigDecimal.ZERO;
for (TtExponentUser exponentUser : exponentUsers) {
if (exponentUser == null) {
continue;
}
price = price.add(exponentUser.getPrice());
if (Objects.equals(exponentUser.getForward(), 1)) {
up = up.add(exponentUser.getPrice());
}
if (Objects.equals(exponentUser.getForward(), 2)) {
down = down.add(exponentUser.getPrice());
}
}
jieguo.setBuy_money(price);
jieguo.setBuy_forward_up(up);
jieguo.setBuy_forward_down(down);
}
vo.setJieguo(jieguo);
return R.ok(vo);
}
/**
* 指数开箱
*/
private List<ExponentSkinarrVo> openExponent(int forward, TtUser ttUser, List<TtExponentUser> exponentUsers) {
Map<Integer, Integer> boxIdToNum = new HashMap<>();
// 判断箱子是否已判定结果
Map<Integer, Boolean> boxIdToOpen = new HashMap<>();
for (TtExponentUser user : exponentUsers) {
if (user == null) {
continue;
}
// 等于1,则代表指数结果已判定结果
if (Objects.equals(user.getStatus(), 1)) {
boxIdToOpen.put(user.getBoxId(), true);
int num = boxIdToNum.getOrDefault(user.getBoxId(), 0);
boxIdToNum.put(user.getBoxId(), num + 1);
continue;
}
// 等于2,则代表指数结果已判定结果
if (Objects.equals(user.getStatus(), 2)) {
boxIdToOpen.put(user.getBoxId(), true);
boxIdToNum.put(user.getBoxId(), 0);
continue;
}
if (Objects.equals(user.getForward(), forward)) {
int num = boxIdToNum.getOrDefault(user.getBoxId(), 0);
boxIdToNum.put(user.getBoxId(), num + 1);
user.setStatus(1);
} else {
user.setStatus(2);
}
user.setUpdateTime(new Date());
ttExponentUserMapper.updateById(user);
}
if (MapUtils.isEmpty(boxIdToNum)) {
return new ArrayList<>();
}
// 查询箱子信息
LambdaQueryWrapper<TtBox> wrapper = Wrappers.lambdaQuery();
wrapper.in(TtBox::getBoxId, boxIdToNum.keySet());
List<TtBox> ttBoxes = ttBoxMapper.selectList(wrapper);
Map<Integer, TtBox> ttBoxMap = ttBoxes.stream().collect(Collectors.toMap(TtBox::getBoxId, Function.identity()));
// 查询该用户已拥有的箱子信息
LambdaQueryWrapper<TtExponentUserBox> wrapper1 = Wrappers.lambdaQuery();
wrapper1.in(TtExponentUserBox::getBoxId, boxIdToNum.keySet());
wrapper1.eq(TtExponentUserBox::getUserId, ttUser.getUserId());
List<TtExponentUserBox> userBoxes = ttExponentUserBoxMapper.selectList(wrapper1);
Map<Integer, TtExponentUserBox> userBoxeMap = userBoxes.stream().collect(Collectors.toMap(TtExponentUserBox::getBoxId, Function.identity()));
List<ExponentSkinarrVo> vos = new ArrayList<>();
for (Map.Entry<Integer, Integer> entry : boxIdToNum.entrySet()) {
ExponentSkinarrVo vo = new ExponentSkinarrVo();
vo.setId(entry.getKey());
vo.setCount(entry.getValue());
TtBox ttBox = ttBoxMap.get(entry.getKey());
if (ttBox != null) {
vo.setImage(ttBox.getBoxImg01());
vo.setImage2(ttBox.getBoxImg02());
}
vos.add(vo);
// 若指数没有判定过,则进行放入箱子数量
if (!boxIdToOpen.getOrDefault(entry.getKey(), false)) {
TtExponentUserBox userBox = userBoxeMap.get(entry.getKey());
if (userBox == null) {
userBox = new TtExponentUserBox();
userBox.setBoxId(entry.getKey());
userBox.setUserId(ttUser.getUserId());
userBox.setNumber(entry.getValue());
userBox.setCreateTime(new Date());
userBox.setUpdateTime(new Date());
ttExponentUserBoxMapper.insert(userBox);
} else {
userBox.setNumber(userBox.getNumber() + entry.getValue());
userBox.setUpdateTime(new Date());
ttExponentUserBoxMapper.updateById(userBox);
}
}
}
return vos;
}
@Override
public R records(TtUser ttUser) {
List<ExponentOpenBoxVo> vos = ttExponentMapper.exponentOpenBox(ttUser.getUserId());
if (CollectionUtils.isEmpty(vos)) {
return R.ok(new ArrayList<>());
}
List<Integer> boxIds = new ArrayList<>();
for (ExponentOpenBoxVo vo : vos) {
boxIds.add(vo.getBoxId());
if (vo.getFailed() != null && vo.getFailed() > 0) {
vo.setState(2);
}
if (vo.getSuccess() != null && vo.getSuccess() > 0) {
vo.setState(1);
}
}
// 查询箱子信息
LambdaQueryWrapper<TtBox> wrapper = Wrappers.lambdaQuery();
wrapper.in(TtBox::getBoxId, boxIds);
List<TtBox> ttBoxes = ttBoxMapper.selectList(wrapper);
Map<Integer, TtBox> ttBoxMap = ttBoxes.stream().collect(Collectors.toMap(TtBox::getBoxId, Function.identity()));
for (ExponentOpenBoxVo vo : vos) {
TtBox ttBox = ttBoxMap.get(vo.getBoxId());
if (ttBox != null) {
vo.setImage(ttBox.getBoxImg01());
vo.setImage2(ttBox.getBoxImg02());
}
}
return R.ok(vos);
}
@Override
public R mybox(TtUser ttUser) {
// 查询该用户已拥有的箱子信息
LambdaQueryWrapper<TtExponentUserBox> wrapper1 = Wrappers.lambdaQuery();
wrapper1.eq(TtExponentUserBox::getUserId, ttUser.getUserId());
List<TtExponentUserBox> userBoxes = ttExponentUserBoxMapper.selectList(wrapper1);
if (CollectionUtils.isEmpty(userBoxes)) {
return R.ok(new ArrayList<>());
}
List<ExponentSkinarrVo> vos = new ArrayList<>();
List<Integer> boxIds = new ArrayList<>();
for (TtExponentUserBox userBox : userBoxes) {
ExponentSkinarrVo vo = new ExponentSkinarrVo();
vo.setId(userBox.getBoxId());
boxIds.add(userBox.getBoxId());
vo.setCount(userBox.getNumber());
vos.add(vo);
}
// 查询箱子信息
LambdaQueryWrapper<TtBox> wrapper = Wrappers.lambdaQuery();
wrapper.in(TtBox::getBoxId, boxIds);
List<TtBox> ttBoxes = ttBoxMapper.selectList(wrapper);
Map<Integer, TtBox> ttBoxMap = ttBoxes.stream().collect(Collectors.toMap(TtBox::getBoxId, Function.identity()));
for (ExponentSkinarrVo vo : vos) {
TtBox ttBox = ttBoxMap.get(vo.getId());
if (ttBox != null) {
vo.setImage(ttBox.getBoxImg01());
vo.setImage2(ttBox.getBoxImg02());
}
}
return R.ok(vos);
}
@Override
public R openbox(TtUser user, TtBox box, Integer num) {
// 查询该用户已拥有的箱子信息
LambdaQueryWrapper<TtExponentUserBox> wrapper1 = Wrappers.lambdaQuery();
wrapper1.eq(TtExponentUserBox::getUserId, user.getUserId());
wrapper1.eq(TtExponentUserBox::getBoxId, box.getBoxId());
List<TtExponentUserBox> userBoxes = ttExponentUserBoxMapper.selectList(wrapper1);
if (CollectionUtils.isEmpty(userBoxes) || userBoxes.get(0) == null || userBoxes.get(0).getNumber() < num) {
return R.ok(new ArrayList<>());
}
TtExponentUserBox userBox = userBoxes.get(0);
// 抽奖
List<TtBoxRecords> boxRecords = new ArrayList<>();
Map<Long, TtOrnamentsA> boxToOrnament = new HashMap<>();
for (int i = 0; i < num; i++) {
String ornamentId = lotteryMachine.singleLottery(user, box);
if (StringUtils.isBlank(ornamentId)) {
throw new OrnamentNullException("单次抽奖无结果#"+box.getBoxId()+"#"+ornamentId);
//return R.fail("系统繁忙,请稍后重试。");
}
// 宝箱详细信息
TtOrnamentsA ornamentsData = bindBoxMapper.ornamentsInfo(box.getBoxId(), ornamentId);
if (ObjectUtil.isEmpty(ornamentsData)){
throw new OrnamentNullException("没有符合条件的宝箱信息#"+box.getBoxId()+"#"+ornamentId);
}
// 抽奖成功,构建开箱记录数据
TtBoxRecords boxRecord = TtBoxRecords.builder()
.userId(user.getUserId())
.boxId(box.getBoxId())
.boxName(box.getBoxName())
.boxPrice(box.getPrice())
.ornamentId(Long.valueOf(ornamentId))
// .marketHashName(ornamentsData.)
.ornamentName(ObjectUtil.isNotEmpty(ornamentsData.getName()) ? ornamentsData.getName() : ObjectUtil.isNotEmpty(ornamentsData.getShortName()) ? ornamentsData.getShortName() : "无名称")
.imageUrl(ornamentsData.getImageUrl())
.ornamentsPrice(ornamentsData.getUsePrice())
.ornamentsLevelId(ornamentsData.getOrnamentsLevelId())
.ornamentLevelImg(ornamentsData.getLevelImg())
.holderUserId(user.getUserId())
.source(TtboxRecordSource.EXPONENT_BOX.getCode())
.status(IN_PACKSACK_ON.getCode())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
boxRecords.add(boxRecord);
boxToOrnament.put(Long.valueOf(ornamentId), ornamentsData);
}
// 同步保存游戏记录
boxRecordsService.saveBatch(boxRecords);
// 返回值
List<OpenBoxVO> openBoxVOs = boxRecords.stream().map(item -> {
OpenBoxVO openBoxVO = new OpenBoxVO();
BeanUtil.copyProperties(item, openBoxVO);
TtOrnamentsA ornamentsData = boxToOrnament.get(item.getOrnamentId());
if (ornamentsData != null) {
openBoxVO.setShortName(ornamentsData.getShortName());
openBoxVO.setExteriorName(ornamentsData.getExteriorName());
}
openBoxVO.setUsePrice(openBoxVO.getOrnamentsPrice());
openBoxVO.setLevelImg(item.getOrnamentLevelImg());
return openBoxVO;
}).collect(Collectors.toList());
userBox.setNumber(userBox.getNumber() - num);
userBox.setUpdateTime(new Date());
ttExponentUserBoxMapper.updateById(userBox);
return R.ok(openBoxVOs);
}
}
@@ -0,0 +1,11 @@
package com.ruoyi.playingmethod.service.impl;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.admin.mapper.TtFightUserMapper;
import com.ruoyi.domain.other.TtFightUser;
import com.ruoyi.playingmethod.service.ApiFightUserService;
import org.springframework.stereotype.Service;
@Service
public class ApiFightUserServiceImpl extends ServiceImpl<TtFightUserMapper, TtFightUser> implements ApiFightUserService {
}
@@ -0,0 +1,825 @@
package com.ruoyi.playingmethod.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.admin.mapper.TtBoxRecordsMapper;
import com.ruoyi.admin.mapper.TtOrnamentMapper;
import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper;
import com.ruoyi.admin.mapper.TtUpgradeFailOrnamentsMapper;
import com.ruoyi.admin.mapper.TtUpgradeOrnamentsMapper;
import com.ruoyi.admin.mapper.TtUpgradeRecordMapper;
import com.ruoyi.admin.model.UpdateUserAccountBo;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.admin.service.TtUpgradeOrnamentsService;
import com.ruoyi.admin.service.TtUpgradeRecordService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.domain.common.constant.LockKey.UpgradeLock;
import com.ruoyi.domain.common.constant.MessageCategoryEnum;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.common.constant.TtboxRecordSource;
import com.ruoyi.domain.common.constant.TtboxRecordStatus;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.ApiLuckyUpgradeBody;
import com.ruoyi.domain.other.TtUpgradeOrnaments;
import com.ruoyi.domain.other.TtUpgradeRecord;
import com.ruoyi.domain.other.UpgradeBodyA;
import com.ruoyi.domain.vo.ApiLuckyOrnamentsDataVO;
import com.ruoyi.domain.vo.upgrade.SimpleOrnamentVO;
import com.ruoyi.playingmethod.mapper.ApiLuckyUpgradeMapper;
import com.ruoyi.playingmethod.mapper.ApiTtUserBlendErcashMapper;
import com.ruoyi.playingmethod.service.ApiLuckyUpgradeService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.message.service.MessageService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Slf4j
@Service
public class ApiLuckyUpgradeServiceImpl implements ApiLuckyUpgradeService {
@Value("${mkcsgo.upgrade.defaultRequired}")
private Integer defaultRequired;
@Value("${mkcsgo.upgrade.anchorDefaultRequired}")
private Integer anchorDefaultRequired;
@Autowired
private Executor customThreadPoolExecutor;
@Autowired
private ApiTtUserBlendErcashMapper apiTtUserBlendErcashMapper;
private final TtUserService userService;
private final ISysConfigService configService;
private final ApiLuckyUpgradeMapper apiLuckyUpgradeMapper;
private final TtUpgradeOrnamentsMapper upgradeOrnamentsMapper;
private final TtUpgradeFailOrnamentsMapper upgradeFailOrnamentsMapper;
private final TtUpgradeRecordMapper upgradeRecordMapper;
private final TtOrnamentMapper ornamentsMapper;
private final TtBoxRecordsMapper boxRecordsMapper;
private final TtOrnamentsLevelMapper ornamentsLevelMapper;
@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private RedisLock redisLock;
@Autowired
private RedisCache redisCache;
public ApiLuckyUpgradeServiceImpl(TtUserService userService,
ISysConfigService configService,
ApiLuckyUpgradeMapper apiLuckyUpgradeMapper,
TtUpgradeOrnamentsMapper upgradeOrnamentsMapper,
TtUpgradeFailOrnamentsMapper upgradeFailOrnamentsMapper,
TtUpgradeRecordMapper upgradeRecordMapper,
TtOrnamentMapper ornamentsMapper,
TtBoxRecordsMapper boxRecordsMapper,
TtOrnamentsLevelMapper ornamentsLevelMapper) {
this.userService = userService;
this.configService = configService;
this.apiLuckyUpgradeMapper = apiLuckyUpgradeMapper;
this.upgradeOrnamentsMapper = upgradeOrnamentsMapper;
this.upgradeFailOrnamentsMapper = upgradeFailOrnamentsMapper;
this.upgradeRecordMapper = upgradeRecordMapper;
this.ornamentsMapper = ornamentsMapper;
this.boxRecordsMapper = boxRecordsMapper;
this.ornamentsLevelMapper = ornamentsLevelMapper;
}
@Autowired
private TtUpgradeOrnamentsService ttUpgradeOrnamentsService;
@Autowired
private TtUpgradeOrnamentsMapper ttUpgradeOrnamentsMapper;
@Autowired
private TtBoxRecordsService ttBoxRecordsService;
@Autowired
private TtUpgradeRecordService ttUpgradeRecordService;
@Autowired
private MessageService messageService;
@Override
public List<ApiLuckyOrnamentsDataVO> getOrnamentsList(ApiLuckyUpgradeBody apiLuckyUpgradeBody) {
// type 对应 tt_ornament.type 数字字典码(1匕首 2手枪 3步枪 4微型冲锋枪 5重型武器 6手套 7印花 8其它)。
// 在这里只把空字符串归一为 null,避免 SQL 里 'type = '' 造成隐式转换全表扫描;传具体值时要透传到 mapper 。
if (apiLuckyUpgradeBody.getType() != null && apiLuckyUpgradeBody.getType().trim().isEmpty()) {
apiLuckyUpgradeBody.setType(null);
}
return apiLuckyUpgradeMapper.getOrnamentsList(apiLuckyUpgradeBody);
}
public R upgradeCheck(TtUser player, BigDecimal price) {
if (player == null) {
return R.fail("用户不存在");
}
// 白号允许游玩:playing() 走纯概率分支,不写共享幸运值/本轮进度,
// 与真实玩家(01/02)完全隔离,不会影响真实池出货节奏。
if (!isBlankAccount(player)
&& !UserType.ANCHOR.getCode().equals(player.getUserType())
&& !UserType.COMMON_USER.getCode().equals(player.getUserType())) {
return R.fail("非法的用户类型");
}
// 封禁 / 删除用户不允许参与游戏(status='1' 封禁,del_flag!='0' 已删除)
if ("1".equals(player.getStatus())) {
return R.fail("账号已被封禁,无法参与游戏");
}
if (!"0".equals(player.getDelFlag())) {
return R.fail("账号状态异常,无法参与游戏");
}
// 充值记录校验:未充过值的账号不允许参与幸运升级,防止黑产账户刷单。先于余额校验,保证新用户返回 703。
// 归档兼容:优先读 tt_user 累计充值缓存(O(1)),缓存为空时兜底查主表+归档表 UNION ALL,
// 避免历史充值流水被归档后误判为“未充值”。
BigDecimal cacheRecharge = player.getTotalRechargeAmount();
boolean recharged = cacheRecharge != null && cacheRecharge.compareTo(BigDecimal.ZERO) > 0;
if (!recharged) {
BigDecimal sumRecharge = apiTtUserBlendErcashMapper.sumAmountByUserIdsAndSource(
Collections.singletonList(player.getUserId()),
TtAccountRecordSource.RECHARGE.getCode(),
TtAccountRecordType.INPUT.getCode());
recharged = sumRecharge != null && sumRecharge.compareTo(BigDecimal.ZERO) > 0;
}
if (!recharged) {
return R.fail(703, "玩家未充值不能游玩任何游戏");
}
// 余额校验:与本次升级所需金额比较,不足则返回 702
if (player.getAccountAmount() == null || player.getAccountAmount().compareTo(price) < 0) {
return R.fail(702, "账户余额不足");
}
return R.ok();
}
/**
* 白号判定:全局口径 is_blank_account=1,兼容历史数据只打了 user_type='06' 标签的行。
*/
private boolean isBlankAccount(TtUser user) {
return Integer.valueOf(1).equals(user.getIsBlankAccount())
|| UserType.BLANK_ACCOUNT.getCode().equals(user.getUserType());
}
/**
* 奖池类型:普通(02)/主播(01)/白号(06) 各一套进度与幸运值,互不污染。
* 白号优先按 is_blank_account 判定,兼容历史数据只改了标记没改 user_type 的行。
*/
private String poolType(TtUser user) {
return isBlankAccount(user) ? UserType.BLANK_ACCOUNT.getCode() : user.getUserType();
}
/**
* 玩家使用的幸运区间:主播=anchor_luck_section,白号=blank_luck_section(未配置则回退普通),其余=普通 luck_section。
*/
private String resolveLuckSection(TtUpgradeOrnaments ornament, TtUser user) {
if (isBlankAccount(user)) {
return StringUtils.isNotEmpty(ornament.getBlankLuckSection())
? ornament.getBlankLuckSection()
: ornament.getLuckSection();
}
return UserType.ANCHOR.getCode().equals(user.getUserType())
? ornament.getAnchorLuckSection()
: ornament.getLuckSection();
}
/**
* 根据目标进度计算本轮目标金额:price * targetProgress / 100,保留两位小数。
*/
private BigDecimal calcTargetAmount(BigDecimal ornamentPrice, Integer targetProgress) {
if (ornamentPrice == null || targetProgress == null || targetProgress <= 0) {
return null;
}
return ornamentPrice
.multiply(BigDecimal.valueOf(targetProgress))
.divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP);
}
/**
* 根据升级饰品价格与前端传入的概率计算本次消费金额。
* 公式:price = ornamentPrice * probability / 100,保留两位小数。
* 价格必须大于 0,以防止 100% 成功率、价格为 0 的异常场景。
*/
private BigDecimal calcUpgradePrice(TtUpgradeOrnaments upgradeOrnament, Integer probability) {
BigDecimal ornamentPrice = upgradeOrnament.getOrnamentPrice();
if (ornamentPrice == null || ornamentPrice.compareTo(BigDecimal.ZERO) <= 0) {
return BigDecimal.ZERO;
}
return ornamentPrice
.multiply(BigDecimal.valueOf(probability))
.divide(BigDecimal.valueOf(100), 2, java.math.RoundingMode.HALF_UP);
}
/**
* 单次升级概率封顶值:超过此值一律按此值计算。
* 设计意图:避免高概率(如 90% / 100%)一拉就出货,保障多升级几次的体验。
* 同时扣款按封顶后概率计算,对用户可能更友好。
*/
private static final int PROBABILITY_CAP = 75;
@Override
@Transactional
public R upgrade(TtUser ttUser, UpgradeBodyA upgradeParam) {
// 双口径概率设计:
// 1)前端传入的原始概率(rawProbability,如 90)用于“价格计算 + 扣款 + 数据库落库记录”,
// 保证账单与历史记录与用户看到的一致。
// 2)后端抑制后的有效概率(effectiveProbability,超过 PROBABILITY_CAP 一律按 75)仅用于中奖判定,
// 避免高概率(如 90%)一拉就出货。
Integer rawProbability = upgradeParam.getProbability();
if (rawProbability == null) {
return R.fail("概率不能为空。");
}
int effectiveProbability = Math.min(rawProbability, PROBABILITY_CAP);
TtUpgradeOrnaments upgradeOrnament = new LambdaQueryChainWrapper<>(upgradeOrnamentsMapper)
.eq(TtUpgradeOrnaments::getId, upgradeParam.getUpgradeId())
.eq(TtUpgradeOrnaments::getStatus, "0")
.one();
if (ObjectUtil.isNull(upgradeOrnament)) {
return R.fail("该饰品未在幸运升级开放。");
}
// 价格口径与前端展示对齐:以 tt_ornament.use_price(实时价)为准。
// tt_upgrade_ornaments.ornament_price 是“批量填货”当时的快照,
// 三方价格任务刷新 tt_ornament.use_price 后并不会同步该快照,
// 若仍按快照计算会导致前端显示金额(实时价×概率)与记录里 amount_consumed 不一致。
TtOrnament currentOrnament = new LambdaQueryChainWrapper<>(ornamentsMapper)
.eq(TtOrnament::getId, upgradeOrnament.getOrnamentsId())
.one();
if (ObjectUtil.isNotNull(currentOrnament) && ObjectUtil.isNotEmpty(currentOrnament.getUsePrice())
&& currentOrnament.getUsePrice().compareTo(BigDecimal.ZERO) > 0) {
upgradeOrnament.setOrnamentPrice(currentOrnament.getUsePrice());
}
// 后端根据升级饰品价格和原始概率计算消费金额,不再用前端传入的 price,
// 以防止前端传入 0 价格返回 100% 成功率这种问题。
// 注意:价格使用 rawProbability(如 90),而非 effectiveProbability(封顶 75)。
BigDecimal price = calcUpgradePrice(upgradeOrnament, rawProbability);
if (price.compareTo(BigDecimal.ZERO) <= 0) {
return R.fail("该饰品价格异常,无法参与幸运升级。");
}
// 真实计算金额:用于后台进度统计,超过 75% 按 75% 计算
BigDecimal effectiveAmount = calcTargetAmount(upgradeOrnament.getOrnamentPrice(), effectiveProbability);
// 检查
R check = upgradeCheck(ttUser, price);
if (!check.getCode().equals(200)) return check;
// lock_key:锁粒度 = 饰品位 + 奖池类型(普通/主播/白号各自串行,互不阻塞)
Integer upgradeOrnamentId = upgradeParam.getUpgradeId();
String poolType = poolType(ttUser);
String upgradeLock = UpgradeLock.UPGRADE_LOCK.getLock() + upgradeOrnamentId + ":" + poolType;
// 2 尝试获取锁
Boolean lock = false;
for (int t = 0; t < 2; t++) {
lock = redisLock.tryLock(upgradeLock, 2L, 7L, TimeUnit.SECONDS);
if (!lock) {
} else {
break;
}
}
if (!lock) return R.fail("系统繁忙,请稍后重试。");
// 3 扣款
R<Map<String, BigDecimal>> mapR = upgradeAccounting(price, ttUser);
if (!mapR.getCode().equals(200)) {
redisLock.unlock(upgradeLock);
return mapR;
}
// 4 计算游戏(中奖判定用封顶后的 effectiveProbability)
UpgradePlayResult playResult;
try {
// 升级核心
playResult = playing(ttUser, upgradeOrnament, upgradeParam, effectiveProbability);
} finally {
// 锁释放必须发生在 finally 中,避免异常捕获后 unlock 本身抛异常导致锁泄漏
try {
redisLock.unlock(upgradeLock);
} catch (Exception e) {
log.error("释放幸运升级锁失败。userId={}, upgradeId={}",
ttUser.getUserId(), upgradeParam.getUpgradeId(), e);
}
}
boolean isVictory = playResult.isWin();
// 5 同步保存本次结果。
// 快照本轮目标金额与已投金额(用于升级记录历史展示,需在累加/重置前读取)
// 本轮目标金额使用实际随机出来的目标进度(fullV),而非幸运区间固定最大值
boolean anchorPool = UserType.ANCHOR.getCode().equals(poolType);
boolean blankPool = UserType.BLANK_ACCOUNT.getCode().equals(poolType);
BigDecimal snapshotRequired = calcTargetAmount(upgradeOrnament.getOrnamentPrice(), playResult.getTargetProgress());
if (snapshotRequired == null) {
snapshotRequired = anchorPool
? upgradeOrnament.getAnchorAmountRequired()
: upgradeOrnament.getAmountRequired();
}
// 本轮已投金额快照:白号读自己池的一整套(与普通/主播同口径,只是各记各的列)
BigDecimal snapshotInvested = blankPool
? upgradeOrnament.getBlankAmountInvested()
: (anchorPool ? upgradeOrnament.getAnchorAmountInvested() : upgradeOrnament.getAmountInvested());
BigDecimal snapshotEffectiveInvested = blankPool
? upgradeOrnament.getBlankEffectiveAmountInvested()
: (anchorPool ? upgradeOrnament.getAnchorEffectiveAmountInvested() : upgradeOrnament.getEffectiveAmountInvested());
// 幸运值快照(累计原始概率,累加/重置前读取,按奖池类型区分)
Integer snapshotProbabilityInvested = blankPool
? upgradeOrnament.getBlankProbabilityInvested()
: (anchorPool ? upgradeOrnament.getAnchorProbabilityInvested() : upgradeOrnament.getProbabilityInvested());
int prevProbabilityInvested = snapshotProbabilityInvested == null ? 0 : snapshotProbabilityInvested;
// 本饰品当前幸运值:出货成功清 0,失败累加前端原始概率(白号在自己的池里同样累计)
int luckyValue = isVictory ? 0 : prevProbabilityInvested + rawProbability;
// 更新本轮已投入金额(含幸运值)
// 白号维护自己的一整套字段(本轮已投金额 + 幸运值),与普通/主播同样的算法,只是不碰它们的列
LambdaUpdateWrapper<TtUpgradeOrnaments> investedUpdate = new LambdaUpdateWrapper<>();
investedUpdate.eq(TtUpgradeOrnaments::getId, upgradeOrnament.getId());
if (blankPool) {
if (isVictory) {
// 成功:白号本轮进度清 0 重置,溢出不带入下一轮
investedUpdate.set(TtUpgradeOrnaments::getBlankAmountInvested, BigDecimal.ZERO);
investedUpdate.set(TtUpgradeOrnaments::getBlankEffectiveAmountInvested, BigDecimal.ZERO);
investedUpdate.set(TtUpgradeOrnaments::getBlankProbabilityInvested, 0);
} else {
// 失败:实际扣款累计 + price;真实计算口径累计 + effectiveAmount;幸运值累加原始概率
investedUpdate.setSql("blank_amount_invested = blank_amount_invested + " + price);
investedUpdate.setSql("blank_effective_amount_invested = blank_effective_amount_invested + " + effectiveAmount);
investedUpdate.setSql("blank_probability_invested = COALESCE(blank_probability_invested, 0) + " + rawProbability);
}
} else if (anchorPool) {
if (isVictory) {
// 成功:本轮进度清 0 重置,溢出不带入下一轮
investedUpdate.set(TtUpgradeOrnaments::getAnchorAmountInvested, BigDecimal.ZERO);
investedUpdate.set(TtUpgradeOrnaments::getAnchorEffectiveAmountInvested, BigDecimal.ZERO);
// 幸运值(累计原始概率)同步清 0
investedUpdate.set(TtUpgradeOrnaments::getAnchorProbabilityInvested, 0);
} else {
// 失败:实际扣款累计 + price;真实计算口径累计 + effectiveAmount
investedUpdate.setSql("anchor_amount_invested = anchor_amount_invested + " + price);
investedUpdate.setSql("anchor_effective_amount_invested = anchor_effective_amount_invested + " + effectiveAmount);
// 幸运值累加前端原始概率(如 90)
investedUpdate.setSql("anchor_probability_invested = COALESCE(anchor_probability_invested, 0) + " + rawProbability);
}
} else {
if (isVictory) {
// 成功:本轮进度清 0 重置,溢出不带入下一轮
investedUpdate.set(TtUpgradeOrnaments::getAmountInvested, BigDecimal.ZERO);
investedUpdate.set(TtUpgradeOrnaments::getEffectiveAmountInvested, BigDecimal.ZERO);
// 幸运值(累计原始概率)同步清 0
investedUpdate.set(TtUpgradeOrnaments::getProbabilityInvested, 0);
} else {
// 失败:实际扣款累计 + price;真实计算口径累计 + effectiveAmount
investedUpdate.setSql("amount_invested = amount_invested + " + price);
investedUpdate.setSql("effective_amount_invested = effective_amount_invested + " + effectiveAmount);
// 幸运值累加前端原始概率(如 90)
investedUpdate.setSql("probability_invested = COALESCE(probability_invested, 0) + " + rawProbability);
}
}
upgradeOrnamentsMapper.update(null, investedUpdate);
TtUpgradeRecord upgradeRecord; // 升级记录
List<TtBoxRecords> boxRecords; // 获得物品记录
List<SimpleOrnamentVO> gainList; // 与 boxRecords 顺序一一对应,落库后会回填 id 用于分解接口
if (isVictory) {
Long ornamentId = upgradeOrnament.getOrnamentsId();
// 胜
List<SimpleOrnamentVO> prizeList = ornamentsMapper.simpleOrnamentInfo(Arrays.asList(ornamentId));
SimpleOrnamentVO ornament = prizeList.get(0);
// 奖励集合 并统计奖励总价值
// List<SimpleOrnamentVO> prizeList = Arrays.asList(ornament);
BigDecimal prizeTotal = BigDecimal.ZERO;
for (SimpleOrnamentVO vo : prizeList) {
// 兜底:价格为空时跳过累加,避免 NPE(use_price 未回填)
if (vo.getOrnamentPrice() != null) {
prizeTotal = prizeTotal.add(vo.getOrnamentPrice());
}
}
gainList = prizeList;
// 构造升级记录(gainOrnamentList 在 boxRecords 落库拿到 id 后再填充)
upgradeRecord = TtUpgradeRecord.builder()
.userId(ttUser.getUserId())
.userType(ttUser.getUserType())
.nickName(ObjectUtil.isNotEmpty(ttUser.getNickName()) ? ttUser.getNickName() : ttUser.getUserName())
.userAvatar(ttUser.getAvatar())
.isVictory(isVictory)
.amountConsumed(price)
.probability(upgradeParam.getProbability())
.luckyValue(luckyValue)
.amountRequired(snapshotRequired)
.amountInvested(snapshotInvested)
.effectiveAmountInvested(snapshotEffectiveInvested)
.effectiveAmount(effectiveAmount)
.targetUpgradeId(upgradeOrnament.getId())
.targetOrnamentId(ornamentId)
.targetOrnamentPrice(ornament.getOrnamentPrice())
.gainOrnamentsPrice(prizeTotal)
.openTime(new Date())
.build();
// 构造获得物品记录
boxRecords = Arrays.asList(TtBoxRecords.builder()
.source(TtboxRecordSource.UPGRADE.getCode())
.ornamentId(ornamentId)
.ornamentName(ornament.getOrnamentName())
.userId(ttUser.getUserId())
.holderUserId(ttUser.getUserId())
.imageUrl(ornament.getOrnamentImgUrl())
.ornamentsPrice(ornament.getOrnamentPrice())
.ornamentsLevelId(ornament.getOrnamentLevelId())
.ornamentLevelImg(ornament.getOrnamentLevelImg())
.status(TtboxRecordStatus.IN_PACKSACK_ON.getCode())
.marketHashName(ornament.getOrnamentHashName())
.createTime(new Date())
.updateTime(new Date())
.build());
} else {
// 负
// 奖励集合 并统计奖励总价值
List<SimpleOrnamentVO> failPrize = upgradeFailOrnamentsMapper.ornamentInfoByUpgradeId(upgradeOrnament.getId());
BigDecimal prizeTotal = BigDecimal.ZERO;
if (!failPrize.isEmpty()) {
for (SimpleOrnamentVO item : failPrize) {
// 兜底:安慰奖价格/数量为空时按 0 计,避免 NPE(ornament_price 未配置或 use_price 未回填)
BigDecimal itemPrice = item.getOrnamentPrice() == null ? BigDecimal.ZERO : item.getOrnamentPrice();
int itemNumber = item.getOrnamentNumber() == null ? 0 : item.getOrnamentNumber();
prizeTotal = prizeTotal.add(itemPrice.multiply(BigDecimal.valueOf(itemNumber)));
}
}
gainList = failPrize;
// 构造升级记录(gainOrnamentList 在 boxRecords 落库拿到 id 后再填充)
upgradeRecord = TtUpgradeRecord.builder()
.userId(ttUser.getUserId())
.userType(ttUser.getUserType())
.nickName(ObjectUtil.isNotEmpty(ttUser.getNickName()) ? ttUser.getNickName() : ttUser.getUserName())
.userAvatar(ttUser.getAvatar())
.amountConsumed(price)
.probability(upgradeParam.getProbability())
.luckyValue(luckyValue)
.amountRequired(snapshotRequired)
.amountInvested(snapshotInvested)
.effectiveAmountInvested(snapshotEffectiveInvested)
.effectiveAmount(effectiveAmount)
.isVictory(isVictory)
.targetUpgradeId(upgradeOrnament.getId())
.targetOrnamentId(upgradeOrnament.getOrnamentsId())
.targetOrnamentPrice(upgradeOrnament.getOrnamentPrice())
.gainOrnamentsPrice(prizeTotal)
.openTime(new Date())
.build();
// 构造获得物品记录
boxRecords = failPrize.stream().map(item -> {
return TtBoxRecords.builder()
.source(TtboxRecordSource.UPGRADE.getCode())
.ornamentId(item.getOrnamentId())
.ornamentName(item.getOrnamentName())
.userId(ttUser.getUserId())
.holderUserId(ttUser.getUserId())
.imageUrl(item.getOrnamentImgUrl())
.ornamentsPrice(item.getOrnamentPrice())
.ornamentsLevelId(item.getOrnamentLevelId())
.ornamentLevelImg(item.getOrnamentLevelImg())
.status(TtboxRecordStatus.IN_PACKSACK_ON.getCode())
.marketHashName(item.getOrnamentHashName())
.createTime(new Date())
.updateTime(new Date())
.build();
}).collect(Collectors.toList());
}
// 游戏记录
// 1) 先落库 boxRecords 拿到 id(tt_box_records.id,即背包记录ID),
// 然后回填到 gainOrnamentList 中的每个饰品,供前端在《幸运升级》中
// 调用分解接口 /api/userPackSack/decompose(入参 packSackIds)使用。
ttBoxRecordsService.saveBatch(boxRecords);
if (gainList != null && !gainList.isEmpty()) {
int size = Math.min(gainList.size(), boxRecords.size());
for (int i = 0; i < size; i++) {
gainList.get(i).setId(boxRecords.get(i).getId());
}
upgradeRecord.setGainOrnamentList(JSONUtil.toJsonStr(gainList));
} else {
upgradeRecord.setGainOrnamentList("[]");
}
ttUpgradeRecordService.save(upgradeRecord);
// 发送邮件通知(取第一件获得的饰品)
if (!boxRecords.isEmpty()) {
TtBoxRecords record = boxRecords.get(0);
String priceStr = record.getOrnamentsPrice() == null ? "0"
: record.getOrnamentsPrice().stripTrailingZeros().toPlainString();
String content = isVictory
? "恭喜您在进阶风暴中成功升级,获得【" + record.getOrnamentName() + "】(价值 ¥" + priceStr + "),已放入背包。"
: "您本次进阶风暴升级未成功,获得【" + record.getOrnamentName() + "】(价值 ¥" + priceStr + "),已放入背包。";
messageService.sendNotice(
Long.valueOf(ttUser.getUserId()),
MessageCategoryEnum.GAME_LUCKY_UPGRADE.getTitle(),
content,
MessageCategoryEnum.GAME_LUCKY_UPGRADE.getCode());
}
// 回填本饰品当前幸运值(瞬态字段,不入库),返回给前端 cocos 展示
upgradeRecord.setLuckyValue(luckyValue);
return R.ok(upgradeRecord);
}
public UpgradePlayResult playing(TtUser ttUser, TtUpgradeOrnaments upgradeOrnament, UpgradeBodyA upgradeParam, int effectiveProbability) {
Integer upgradeOrnamentId = upgradeOrnament.getId();
// 奖池类型:白号(06)走自己的进度池与幸运值,Redis key 天然按类型隔离,白号之间共享 06 池
String poolType = poolType(ttUser);
String rangeFixedKey = RedisConstants.UPGRADE_RANGE_FIXED + upgradeOrnamentId + ":" + poolType;
String rangeFloatKey = RedisConstants.UPGRADE_RANGE + upgradeOrnamentId + ":" + poolType;
// 读redis的固定概率 [x,y] x进度,y空间大小(缓存缺失或非法时返回 null)
Integer[] rangeFixed = parseRange(redisCache.getCacheObject(rangeFixedKey));
// 读redis的饰品概率区间 [begin,end]
Integer[] rangeFloat = parseRange(redisCache.getCacheObject(rangeFloatKey));
if (isValidRange(rangeFixed)) {
// 抽奖
return doUpgrade(
ttUser,
upgradeOrnament,
rangeFixed,
upgradeParam,
rangeFixedKey,
rangeFloatKey,
rangeFloat,
effectiveProbability);
}
// 没有,使用redis的概率区间新建,
if (isValidRange(rangeFloat)) {
// 使用饰品概率区间创建新的固定概率(进度从 0 开始)
Integer[] newRangeFixed = createRangeFixed(rangeFloat, 0);
// 抽奖
return doUpgrade(
ttUser,
upgradeOrnament,
newRangeFixed,
upgradeParam,
rangeFixedKey,
rangeFloatKey,
rangeFloat,
effectiveProbability);
}
// 也没有概率区间,用mysql的数据(按奖池类型取:主播/白号/普通)创建固定概率,抽奖
String luckSection = resolveLuckSection(upgradeOrnament, ttUser);
if (StringUtils.isNotEmpty(luckSection)) {
rangeFloat = JSONUtil.toList(luckSection, Integer.class).toArray(new Integer[2]);
}
rangeFixed = createRangeFixed(rangeFloat, 0);
// 缓存固定概率
// redisCache.setCacheList(rangeFixedKey, Arrays.asList(rangeFixed));
// redisCache.setCacheObject(rangeFixedKey, Arrays.asList(rangeFixed));
// 缓存概率区间
redisCache.setCacheObject(rangeFloatKey, rangeFloat, 1, TimeUnit.DAYS);
// 抽奖
return doUpgrade(
ttUser,
upgradeOrnament,
rangeFixed,
upgradeParam,
rangeFixedKey,
rangeFloatKey,
rangeFloat,
effectiveProbability);
}
/**
* @param rangeFixed 本次的固定概率
* @param param http请求参数
* @param rangeFixedKey redis固定概率key
* @param rangeFloat 升级饰品的概率区间
* @param effectiveProbability 封顶后的有效概率(仅用于中奖判定)
* @return
*/
public UpgradePlayResult doUpgrade(TtUser ttUser,
TtUpgradeOrnaments upgradeOrnament,
Integer[] rangeFixed,
UpgradeBodyA param,
String rangeFixedKey,
String rangeFloatKey,
Integer[] rangeFloat,
int effectiveProbability) {
// 计算进度
Integer currentV = rangeFixed[0];
Integer fullV = rangeFixed[1];
// 中奖判定使用封顶后的 effectiveProbability(如 75),而非前端原始传入的 param.getProbability()(如 90)
int sub = fullV - (effectiveProbability + currentV);
if (sub > 0) {
// 失败,记录进度
int i = effectiveProbability + currentV;
rangeFixed[0] = i;
// redisCache.setCacheList(rangeFixedKey, Arrays.asList(rangeFixed));
redisCache.setCacheObject(rangeFixedKey, rangeFixed, 1, TimeUnit.DAYS);
return new UpgradePlayResult(false, fullV);
}
// 成功:先保存本轮目标进度(创建新的固定概率前)
Integer currentRoundTarget = fullV;
// 如果没有概率区间,新建
if (isValidRange(rangeFloat)) {
// 按奖池类型重新读一遍幸运区间(主播/白号/普通),后台改配置后本轮中奖即生效
String luckSection = resolveLuckSection(upgradeOrnament, ttUser);
if (StringUtils.isNotEmpty(luckSection)) {
rangeFloat = JSONUtil.toList(luckSection, Integer.class).toArray(new Integer[2]);
}
// 缓存概率区间
redisCache.setCacheObject(rangeFloatKey, rangeFloat, 1, TimeUnit.DAYS);
// rangeFixed = createRangeFixed(rangeFloat,0);
}
// 成功,随机创建新的固定概率(进度清 0 重置,溢出不带入下一轮)
Integer[] newRangeFixed = createRangeFixed(rangeFloat, 0);
// 保存新的固定概率
// redisCache.setCacheList(rangeFixedKey, Arrays.asList(newRangeFixed));
redisCache.setCacheObject(rangeFixedKey, newRangeFixed, 1, TimeUnit.DAYS);
return new UpgradePlayResult(true, currentRoundTarget);
}
public Integer[] createRangeFixed(Integer[] rangeFloat, Integer initVal) {
// 防护:区间为空或元素缺失时使用默认区间,避免 NPE
if (rangeFloat == null || rangeFloat.length < 2 || rangeFloat[0] == null || rangeFloat[1] == null) {
log.warn("幸运区间为空或非法,使用默认区间 [100, 160]");
rangeFloat = new Integer[]{100, 160};
}
// 防护:如果区间配置异常(min >= max),强制修正
if (rangeFloat[0] >= rangeFloat[1]) {
log.warn("幸运区间配置异常: [{}, {}],强制交换", rangeFloat[0], rangeFloat[1]);
int temp = rangeFloat[0];
rangeFloat[0] = rangeFloat[1];
rangeFloat[1] = temp;
// 如果交换后仍然相等,设置默认值
if (rangeFloat[0] >= rangeFloat[1]) {
rangeFloat[0] = 0;
rangeFloat[1] = 160;
}
}
// 创建固定概率:进度初值 initVal,空间大小在 [min, max) 内随机
Integer[] newRangeFixed = new Integer[2];
newRangeFixed[0] = initVal;
newRangeFixed[1] = ThreadLocalRandom.current().nextInt(rangeFloat[1] - rangeFloat[0]) + rangeFloat[0];
return newRangeFixed;
}
/**
* 升级游戏结果包装:携带胜负与本轮实际目标进度(fullV),用于计算本轮目标金额快照。
*/
private static class UpgradePlayResult {
private final boolean win;
private final Integer targetProgress;
UpgradePlayResult(boolean win, Integer targetProgress) {
this.win = win;
this.targetProgress = targetProgress;
}
boolean isWin() {
return win;
}
Integer getTargetProgress() {
return targetProgress;
}
}
/**
* 安全解析 Redis 中缓存的概率区间对象为 Integer[2]。
* 缓存不存在、解析失败或元素缺失时返回 null,避免 [null, null] 被误判为有效区间。
*/
private Integer[] parseRange(Object cacheObj) {
if (cacheObj == null) {
return null;
}
try {
List<Integer> list = JSONUtil.toList(JSONUtil.parseArray(cacheObj), Integer.class);
if (list.size() < 2 || list.get(0) == null || list.get(1) == null) {
return null;
}
return new Integer[]{list.get(0), list.get(1)};
} catch (Exception e) {
log.warn("解析幸运升级概率区间失败: {}", cacheObj, e);
return null;
}
}
/**
* 判断概率区间是否有效:非空且两个元素都不为 null。
*/
private boolean isValidRange(Integer[] range) {
return range != null && range.length >= 2 && range[0] != null && range[1] != null;
}
// 扣款
public R<Map<String, BigDecimal>> upgradeAccounting(BigDecimal consumption, TtUser player) {
R<UpdateUserAccountBo> updateResult = userService.updateUserAccount(player.getUserId(), consumption.negate(),
TtAccountRecordSource.GOLD_DEVICE);
if (R.isError(updateResult)) {
return R.fail(updateResult.getMsg());
}
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate.eq(TtUser::getUserId, player.getUserId());
Map<String, BigDecimal> map = new HashMap<>();
map.put("Amount", updateResult.getData().getAccountAmount());
map.put("Credits", updateResult.getData().getAccountCredits());
map.put("total", consumption);
return R.ok(map);
}
}
@@ -0,0 +1,288 @@
package com.ruoyi.playingmethod.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.ruoyi.admin.mapper.TtRedPacketMapper;
import com.ruoyi.admin.mapper.TtRedPacketRecordMapper;
import com.ruoyi.admin.mapper.TtUserBlendErcashMapper;
import com.ruoyi.admin.service.TtRedPackService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.util.RandomUtils;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtAccountRecordType;
import com.ruoyi.domain.entity.TtUserBlendErcash;
import com.ruoyi.domain.entity.fight.TtFight;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtRedPack;
import com.ruoyi.domain.other.TtRedPacket;
import com.ruoyi.domain.other.TtRedPacketRecord;
import com.ruoyi.domain.param.RedPackRecordParam;
import com.ruoyi.playingmethod.service.ApiRedPackService;
import com.ruoyi.thirdparty.wechat.domain.PtLoginUser;
import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeParam;
import com.ruoyi.thirdparty.wechat.entity.TtCoinRechargeRecord;
import com.ruoyi.thirdparty.wechat.service.ITtCoinRechargeRecordService;
import com.ruoyi.thirdparty.wechat.service.ITtCoinRecordService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
@Service
@Slf4j
public class ApiRedPackServiceImpl implements ApiRedPackService {
@Autowired
private TtRedPackService ttRedPackService;
@Autowired
private ITtCoinRechargeRecordService coinRechargeRecordService;
@Autowired
private ITtCoinRecordService recordService;
@Autowired
private TtUserService ttUserService;
@Autowired
private TtUserBlendErcashMapper ttUserBlendErcashMapper;
@Autowired
private TtRedPacketRecordMapper ttRedPacketRecordMapper;
@Autowired
private TtRedPacketMapper ttRedPacketMapper;
@Autowired
private TtUserService userService;
@Autowired
private RedisLock redisLock;
@Override
public List<TtRedPack> getRedPackList(PtLoginUser user, Integer type) {
TtRedPack redPackParam = new TtRedPack();
// redPackParam.setStatus(1L);
// redPackParam.setPackType(type + "");
return ttRedPackService.selectTtRedPackList(redPackParam);
}
@Override
@Transactional
public AjaxResult getRedPack(Long userId, Long redPackId) {
TtUser user = ttUserService.getById(userId);
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
TtRedPack redPack = ttRedPackService.selectTtRedPackById(redPackId);
TtCoinRechargeParam rechargeParam = new TtCoinRechargeParam();
rechargeParam.setUid(user.getUserId().longValue());
rechargeParam.setBeginTime(format.format(redPack.getBeginTime()));
rechargeParam.setEndTime(format.format(redPack.getEndTime()));
rechargeParam.setCoin(redPack.getRechargeLimit());
// 查询用户在指定时间内的大于最小充值金额的充值记录
LambdaQueryWrapper<TtUserBlendErcash> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(TtUserBlendErcash::getUserId, rechargeParam.getUid())
.eq(TtUserBlendErcash::getType, 1)
.eq(TtUserBlendErcash::getSource, 0)
.gt(TtUserBlendErcash::getTotal, rechargeParam.getCoin())
.gt(TtUserBlendErcash::getCreateTime, rechargeParam.getBeginTime())
.lt(TtUserBlendErcash::getCreateTime, rechargeParam.getEndTime());
List<TtUserBlendErcash> rechargeRecords = ttUserBlendErcashMapper.selectList(queryWrapper);
// 根据红包ID和用户ID查询红包记录,检查用户是否已经领取过某个特定的红包
LambdaQueryWrapper<TtRedPacketRecord> ttRedPacketRecordWrapper = new LambdaQueryWrapper<>();
ttRedPacketRecordWrapper
.eq(TtRedPacketRecord::getRedPacketId, redPack.getId())
.eq(TtRedPacketRecord::getUserId, user.getUserId());
List<TtRedPacketRecord> redPackRecords = ttRedPacketRecordMapper.selectList(ttRedPacketRecordWrapper);
if (redPackRecords.size() >= rechargeRecords.size()) {
return AjaxResult.error("可领取次数不足");
}
// TODO 减红包剩余数
String lockName = "RED_PACK_LOCK:" + redPackId;
boolean lock = false;
try {
// 尝试获取锁,最多尝试10次,每次等待3秒,锁定时间7秒
for (int i = 0; i < 10; i++) {
lock = redisLock.tryLock(lockName, 3, 7, TimeUnit.SECONDS);
if (lock) {
break;
}
}
if (!lock) {
return AjaxResult.error("服务器繁忙,稍后重试。");
}
// 执行业务逻辑
TtRedPack ttRedPack = ttRedPackService.selectTtRedPackById(redPackId);
if (ttRedPack.getValidCount() <= 0) {
return AjaxResult.error("红包已被领完");
} else {
ttRedPack.setValidCount(ttRedPack.getValidCount() - 1);
ttRedPackService.updateTtRedPack(ttRedPack);
}
} finally {
if (lock) {
redisLock.unlock(lockName);
}
}
TtRedPacketRecord ttRedPackRecord = new TtRedPacketRecord();
BigDecimal randomValue = RandomUtils.getRandomaBigdecimal(redPack.getValueMin(), redPack.getValueMax());
ttRedPackRecord.setRedPacketId(redPackId.intValue());
ttRedPackRecord.setUserId(user.getUserId());
ttRedPackRecord.setReceiveAmount(randomValue);
ttRedPackRecord.setReceiveTime(new Date());
ttRedPacketRecordMapper.insert(ttRedPackRecord);
// 加钱
LambdaUpdateWrapper<TtUser> userUpdate = new LambdaUpdateWrapper<>();
userUpdate
.eq(TtUser::getUserId, user.getUserId())
.setSql("account_credits = account_credits + " + randomValue);
userService.update(userUpdate);
// 综合消费日志
TtUser ttUser = userService.getById(user.getUserId());
TtUserBlendErcash blendErcash = TtUserBlendErcash.builder()
.userId(user.getUserId())
.credits(randomValue.compareTo(BigDecimal.ZERO) > 0 ? randomValue : null)
.finalCredits(randomValue.compareTo(BigDecimal.ZERO) > 0 ? ttUser.getAccountCredits().add(randomValue) : null)
.total(randomValue.add(randomValue)) // 收支合计
.type(TtAccountRecordType.INPUT.getCode())
.source(TtAccountRecordSource.CHARGE_RED_PACKET.getCode())
.remark(TtAccountRecordSource.CHARGE_RED_PACKET.getMsg())
.createTime(new Timestamp(System.currentTimeMillis()))
.updateTime(new Timestamp(System.currentTimeMillis()))
.build();
ttUserBlendErcashMapper.insert(blendErcash);
return AjaxResult.success(ttRedPackRecord);
}
@Override
@Transactional
public AjaxResult getRedPackByKey(LoginUser user, String redPackKey) {
SimpleDateFormat format = new SimpleDateFormat("yyyy-MM-dd");
// 根据KEY查询红包
LambdaUpdateWrapper<TtRedPacket> selectRedPackByKeyWrapper = new LambdaUpdateWrapper<>();
selectRedPackByKeyWrapper
.eq(TtRedPacket::getPassword, redPackKey);
TtRedPacket redPack = ttRedPacketMapper.selectOne(selectRedPackByKeyWrapper);
if (redPack == null){
return AjaxResult.error("未找到红包,请检查口令是否正确");
}
// 指定领取人的红包(亏损返利CDK)只走 /api/bonus/receiveRedPacket 领取。
// 这个旧入口(ApiRedPackContoller 已 @Deprecated)不校验收款人、个数、状态、有效期,
// 放行等于绕过收款人绑定,直接拒绝。
if (redPack.getReceiverUserId() != null && redPack.getReceiverUserId() > 0) {
return AjaxResult.error("请前往「CDK兑换口令页」领取");
}
TtCoinRechargeParam rechargeParam = new TtCoinRechargeParam();
rechargeParam.setUid(user.getUserId());
rechargeParam.setBeginTime(format.format(new Date()) + " 00:00:00");
rechargeParam.setEndTime(format.format(new Date()) + " 23:59:59");
LambdaQueryWrapper<TtRedPacketRecord> ttRedPacketRecordWrapper = new LambdaQueryWrapper<>();
ttRedPacketRecordWrapper
.eq(TtRedPacketRecord::getRedPacketId, redPack.getId())
.eq(TtRedPacketRecord::getUserId, user.getUserId().intValue());
List<TtRedPacketRecord> redPackRecords = ttRedPacketRecordMapper.selectList(ttRedPacketRecordWrapper);
if (redPackRecords.size() > 0){
return AjaxResult.error("您已经领取过。");
}
TtRedPacketRecord ttRedPackRecord = new TtRedPacketRecord();
// 此处解析最小值和最大值[1, 5]
String amount = redPack.getAmount();
// 去除方括号和空格,得到 "1,5"
String cleanString = amount.replaceAll("\\[|\\]|\\s", "");
// 分割字符串,得到字符串数组 ["1", "5"]
String[] parts = cleanString.split(",");
// 转换为 BigDecimal
BigDecimal minValue = new BigDecimal(parts[0]);
BigDecimal maxValue = new BigDecimal(parts[1]);
BigDecimal randomValue = RandomUtils.getRandomaBigdecimal(minValue, maxValue);
ttRedPackRecord.setRedPacketId(redPack.getId());
ttRedPackRecord.setUserId(user.getUserId().intValue());
ttRedPackRecord.setReceivePassword(redPackKey);
ttRedPackRecord.setReceiveAmount(randomValue);
ttRedPackRecord.setReceiveTime(new Date());
ttRedPacketRecordMapper.insert(ttRedPackRecord);
// 加钱
userService.updateOnlyUserAccount(user.getUserId().intValue(), randomValue, TtAccountRecordSource.RECEIVE_RED_PACKET);
return AjaxResult.success(ttRedPackRecord);
}
@Override
public List<TtRedPacketRecord> getActivityDataHis(PtLoginUser user) {
RedPackRecordParam redPackRecordParam = new RedPackRecordParam();
redPackRecordParam.setuId(user.getUserId().intValue());
LambdaQueryWrapper<TtRedPacketRecord> ttRedPacketRecordWrapper = new LambdaQueryWrapper<>();
ttRedPacketRecordWrapper
.eq(TtRedPacketRecord::getUserId, user.getUserId().intValue());
List<TtRedPacketRecord> redPackRecords = ttRedPacketRecordMapper.selectList(ttRedPacketRecordWrapper);
return redPackRecords;
}
@Override
public AjaxResult getRedPackDataDetail(PtLoginUser user, Long recordId) {
TtRedPacketRecord ttRedPackRecord = ttRedPacketRecordMapper.selectById(recordId);
if (ttRedPackRecord.getReceivePassword() != null) {
TtRedPacket redPackKey = ttRedPacketMapper.selectById(ttRedPackRecord.getRedPacketId());
// TODO 缺少属性
// ttRedPackRecord.setRedPackKey(redPackKey);
} else {
TtRedPack redPack = ttRedPackService.selectTtRedPackById(ttRedPackRecord.getRedPacketId().longValue());
// TODO 缺少属性
// ttRedPackRecord.setRedPack(redPack);
}
return AjaxResult.success(ttRedPackRecord);
}
/**
* 解析红包最小值和最大值
*/
public static List<BigDecimal> parseBigDecimalList(String input) {
// 去掉中括号和空格
String cleanInput = input.replace("[", "").replace("]", "").replaceAll("\\s+", "");
// 按逗号分割成字符串数组
String[] parts = cleanInput.split(",");
// 转换为 BigDecimal 对象列表
List<BigDecimal> bigDecimals = Arrays.stream(parts)
.map(BigDecimal::new) // 使用 BigDecimal 的构造方法转换为 BigDecimal 对象
.collect(Collectors.toList());
return bigDecimals;
}
}
@@ -0,0 +1,198 @@
package com.ruoyi.playingmethod.service.impl;
import com.ruoyi.admin.mapper.TtBoxMapper;
import com.ruoyi.admin.mapper.TtBoxRecordsMapper;
import com.ruoyi.admin.mapper.TtOrnamentMapper;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.admin.service.TtOrnamentService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.model.LoginUser;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.domain.common.constant.TtboxRecordSource;
import com.ruoyi.domain.dto.packSack.PackSackCondition;
import com.ruoyi.domain.entity.TtBoxRecords;
import com.ruoyi.domain.entity.TtOrnament;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.TtBox;
import com.ruoyi.domain.other.TtReplacementRecord;
import com.ruoyi.domain.vo.UserPackSackDataVO;
import com.ruoyi.playingmethod.mapper.ApiReplacementRecordMapper;
import com.ruoyi.playingmethod.service.IApiReplacementRecordService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.thirdparty.wechat.domain.PtLoginUser;
import com.ruoyi.user.mapper.ApiUserPackSackMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.*;
/**
* 业务端汰换逻辑实现
*
* @author junhai
*/
@Service
public class ApiReplacementRecordServiceImpl implements IApiReplacementRecordService {
@Autowired
private ApiReplacementRecordMapper apiReplacementRecordMapper;
@Autowired
private TtBoxRecordsService ttBoxRecordsService;
@Autowired
private TtBoxRecordsMapper ttBoxRecordsMapper;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private TtOrnamentMapper ttOrnamentMapper;
// 概率配置数组
private static final double[][] percent = {
{0.5, 0.6},
{0.6, 0.7},
{0.7, 0.8},
{0.8, 0.9},
{0.9, 1},
{1, 2}
};
@Override
@Transactional
public AjaxResult synthesizeItems(LoginUser user, List<Long> itemIds) {
int size = 0;
int size1 = 0;
if (itemIds != null) {
size = itemIds.size();
}
int allCount = size + size1;
if (allCount < 3 || allCount > 9) {
return AjaxResult.error("汰换数量在3~9个");
}
BigDecimal totalPrice = new BigDecimal(0);
//查询饰品列表
// 1.先查询宝箱记录里面的饰品
if(!itemIds.isEmpty()){
// 从我的背包中查询饰品总价值
List<UserPackSackDataVO> boxs = apiReplacementRecordMapper
.selectUserPackSack(user.getUserId().intValue(), itemIds);
for (int i = 0; i < boxs.size(); i++) {
BigDecimal bigDecimal = boxs.get(i).getOrnamentsPrice();
totalPrice = totalPrice.add(bigDecimal);
}
}
//累加装备总价值
String status = "8";
//更新记录的状态 updateStatusByIds
if(!itemIds.isEmpty()){
ttBoxRecordsMapper.updateStatusByIds(itemIds, status);
}
//获取最小汰换金额
BigDecimal minPrice = new BigDecimal(sysConfigService.selectConfigByKey("minExchangeAmount"));
// 汰换饰品合计总价不得低于
if (totalPrice.compareTo(minPrice) < 0) {
return AjaxResult.error("汰换饰品合计总价不得低于" + minPrice);
}
//获取饰品的id
Long id = getOId(totalPrice);
//获取饰品详情
TtOrnament ttOrnament = ttOrnamentMapper.selectOrnamentById(id);
//进行汰换的日志记录
TtReplacementRecord replacementRecord = new TtReplacementRecord();
replacementRecord.setUid(user.getUserId());
replacementRecord.setUname(user.getUserData().getNickName());
replacementRecord.setAwardOid(ttOrnament.getId());
replacementRecord.setAwardOname(ttOrnament.getName());
replacementRecord.setAwardOimg(ttOrnament.getImageUrl());
replacementRecord.setAwardOprice(ttOrnament.getUsePrice());
replacementRecord.setTime(DateUtils.getNowDate());
replacementRecord.setCreateTime(DateUtils.getNowDate());
apiReplacementRecordMapper.insertTtReplacementRecord(replacementRecord);
//将汰换的奖品加入背包
TtBoxRecords ttBoxRecords = new TtBoxRecords();
ttBoxRecords.setSource(TtboxRecordSource.REPLACEMENT.getCode());
ttBoxRecords.setUserId(user.getUserId().intValue());
ttBoxRecords.setHolderUserId(user.getUserId().intValue());
ttBoxRecords.setBoxName("汰换奖品");
ttBoxRecords.setOrnamentName(ttOrnament.getName());
ttBoxRecords.setImageUrl(ttOrnament.getImageUrl());
ttBoxRecords.setOrnamentId(ttOrnament.getId());
ttBoxRecords.setOrnamentsPrice(ttOrnament.getUsePrice());
ttBoxRecords.setStatus(0);
ttBoxRecords.setCreateTime(DateUtils.getNowDate());
ttBoxRecordsService.save(ttBoxRecords);
return AjaxResult.success(replacementRecord);
}
/**
* 随机产生饰品id
*
* @param bean
* @return
*/
public Long getOId(BigDecimal bean) {
List<TtOrnament> ornament = new ArrayList<>();
initArray(bean, ornament);
Random random = new Random();
int randomIndex = random.nextInt(ornament.size());
return ornament.get(randomIndex).getId();
}
/**
* 初始化 饰品列表
*
* @param bean
* @param ornaments
*/
private void initArray(BigDecimal bean, List<TtOrnament> ornaments) {
for (double[] range : percent) {
BigDecimal start = bean.multiply(BigDecimal.valueOf(range[0]));
BigDecimal end = bean.multiply(BigDecimal.valueOf(range[1]));
Map<String, BigDecimal> map = new HashMap<>();
map.put("start", start);
map.put("end", end);
int configQuantity = 1;
if (configQuantity > 0) {
List<TtOrnament> eligibleOrnaments = apiReplacementRecordMapper.findByPriceRange(map);
shuffleList(eligibleOrnaments);
int quantityToAdd = Math.min(configQuantity, eligibleOrnaments.size());
for (int i = 0; i < quantityToAdd; i++) {
ornaments.add(eligibleOrnaments.get(i));
}
}
}
}
/**
* 采用了 Fisher-Yates 随机洗牌算法,
*
* @param list
*/
private void shuffleList(List<TtOrnament> list) {
Random random = new Random();
int n = list.size();
for (int i = n - 1; i > 0; i--) {
int j = random.nextInt(i + 1);
TtOrnament temp = list.get(i);
list.set(i, list.get(j));
list.set(j, temp);
}
}
}
@@ -0,0 +1,879 @@
package com.ruoyi.playingmethod.service.impl;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.admin.config.RedisConstants;
import com.ruoyi.admin.mapper.*;
import com.ruoyi.admin.service.TtBoxRecordsService;
import com.ruoyi.admin.service.TtOrnamentsLevelService;
import com.ruoyi.admin.service.TtRollUserService;
import com.ruoyi.admin.service.TtUserService;
import com.ruoyi.admin.util.RandomUtils;
import com.ruoyi.common.core.domain.R;
import com.ruoyi.common.redis.config.RedisLock;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.domain.common.constant.MessageCategoryEnum;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.common.constant.TtboxRecordSource;
import com.ruoyi.domain.common.constant.UserType;
import com.ruoyi.domain.common.constant.roll.RollGetPrizeWay;
import com.ruoyi.domain.common.constant.roll.RollStatus;
import com.ruoyi.domain.common.constant.roll.RollType;
import com.ruoyi.domain.dto.roll.GetRollOpenPrizeParam;
import com.ruoyi.domain.dto.roll.GetRollPlayersParam;
import com.ruoyi.domain.dto.roll.GetRollPrizePool;
import com.ruoyi.domain.entity.*;
import com.ruoyi.domain.entity.roll.*;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.domain.other.*;
import com.ruoyi.domain.vo.boxRecords.TtBoxRecordsVO;
import com.ruoyi.domain.vo.roll.RollJackpotOrnamentsVO;
import com.ruoyi.domain.vo.roll.RollUserPrizeVO;
import com.ruoyi.domain.vo.roll.RollUserVO;
import com.ruoyi.domain.vo.roll.SimpleRollOrnamentVO;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.playingmethod.contract.api.request.GetRollListParamRequest;
import com.ruoyi.playingmethod.controller.ApiRollController.JoinRollParam;
import com.ruoyi.playingmethod.mapper.ApiRollMapper;
import com.ruoyi.playingmethod.service.ApiRollService;
import com.ruoyi.domain.vo.RollDetailsDataVO;
import com.ruoyi.domain.vo.RollListDataVO;
import com.ruoyi.message.service.MessageService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.ruoyi.domain.common.constant.TtboxRecordStatus.IN_PACKSACK_ON;
@Service
@Slf4j
public class ApiRollServiceImpl extends ServiceImpl<TtRollMapper, TtRoll> implements ApiRollService {
private final ApiRollMapper apiRollMapper;
private final TtUserMapper userMapper;
private final TtOrnamentMapper ornamentsMapper;
private final TtBoxRecordsService boxRecordsService;
private final TtRollMapper rollMapper;
private final TtRollUserService rollUserService;
private final TtRollJackpotOrnamentsMapper rollJackpotOrnamentsMapper;
private final TtRechargeRecordMapper rechargeRecordMapper;
private final RedisLock redisLock;
public ApiRollServiceImpl(ApiRollMapper apiRollMapper,
TtUserMapper userMapper,
TtOrnamentMapper ornamentsMapper,
TtBoxRecordsService boxRecordsService,
TtRollMapper rollMapper,
TtRollUserService rollUserService,
TtRollJackpotOrnamentsMapper rollJackpotOrnamentsMapper,
TtRechargeRecordMapper rechargeRecordMapper,
RedisLock redisLock) {
this.apiRollMapper = apiRollMapper;
this.userMapper = userMapper;
this.ornamentsMapper = ornamentsMapper;
this.boxRecordsService = boxRecordsService;
this.rollMapper = rollMapper;
this.rollUserService = rollUserService;
this.rollJackpotOrnamentsMapper = rollJackpotOrnamentsMapper;
this.rechargeRecordMapper = rechargeRecordMapper;
this.redisLock = redisLock;
}
@Autowired
private TtRollUserMapper ttRollUserMapper;
@Autowired
private MessageService messageService;
@Autowired
private TtRollUserPrizeMapper ttRollUserPrizeMapper;
@Autowired
private TtRollJackpotMapper ttRollJackpotMapper;
@Autowired
private TtRollJackpotOrnamentsMapper ttRollJackpotOrnamentsMapper;
@Autowired
private TtRollMapper ttRollMapper;
@Autowired
private TtOrnamentMapper ttOrnamentMapper;
@Autowired
private TtBoxRecordsService ttBoxRecordsService;
@Autowired
private TtOrnamentsLevelMapper ttOrnamentsLevelMapper;
@Autowired
private TtOrnamentsLevelService ttOrnamentsLevelService;
@Autowired
private TtBoxRecordsMapper ttBoxRecordsMapper;
@Autowired
private TtUserService ttUserService;
public R<TtRoll> joinRollCheck(TtRoll ttRoll, TtRollUser rollUser, TtUser player, JoinRollParam param) {
if (StringUtils.isNull(ttRoll)) return R.fail("当前Roll房已结束");
if (StringUtils.isNotNull(rollUser)) return R.fail("您当前已在该房间内,请勿重复加入房间!");
String password = ttRoll.getRollPassword();
if (StringUtils.isNotEmpty(password)) {
if (StringUtils.isEmpty(param.getRollPassword())) return R.fail("请输入密码!");
if (!password.equals(param.getRollPassword())) return R.fail("密码错误!");
}
// 检查加入条件:充值下限
BigDecimal minRecharge = ttRoll.getMinRecharge();
if (minRecharge.compareTo(BigDecimal.ZERO) > 0) {
BigDecimal rechargeTotalAmount = getRechargeTotalAmount(player.getUserId(), ttRoll.getRechargeStartTime());
if (minRecharge.compareTo(rechargeTotalAmount) > 0) return R.fail("未满足该房间的充值条件!");
}
// 检查加入条件:VIP等级
Integer vipLevel = ttRoll.getVipLevel();
if (vipLevel != null && vipLevel > 0) {
Integer userVipLevel = player.getVipLevel();
if (userVipLevel == null || userVipLevel < vipLevel) {
return R.fail("未满足该房间的VIP等级条件!需要VIP" + vipLevel + "以上。");
}
}
return R.ok();
}
@Override
public R joinRoll(JoinRollParam param, TtUser player) {
// roll房
TtRoll ttRoll = new LambdaQueryChainWrapper<>(rollMapper)
.eq(TtRoll::getId, param.getRollId())
.eq(TtRoll::getRollStatus, "0")
.gt(TtRoll::getEndTime, DateUtils.getNowDate())
.one();
// roll房成员
TtRollUser rollUser = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper())
.eq(TtRollUser::getUserId, player.getUserId())
.eq(TtRollUser::getRollId, param.getRollId())
.one();
// 检查
R<TtRoll> check = joinRollCheck(ttRoll, rollUser, player, param);
if (!check.getCode().equals(200)) return check;
// 如果是主播房
if (ttRoll.getRollType().equals(RollType.ANCHOR.getCode())) {
TtUser anchor = new LambdaQueryChainWrapper<>(userMapper)
.eq(TtUser::getUserId, ttRoll.getUserId())
.eq(TtUser::getUserType, UserType.ANCHOR.getCode())
.eq(TtUser::getDelFlag, "0")
.one();
// 检查是否粉丝
Integer parentId = player.getParentId();
if (StringUtils.isNull(parentId) || !anchor.getUserId().equals(parentId)) {
return R.fail("您不是该主播的粉丝,请先加入主播粉丝团才可加入房间!");
}
}
// 加入房间
Boolean lock = false;
for (int i = 0; i < 2; i++) {
lock = redisLock.tryLock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId(), 2L, 7L, TimeUnit.SECONDS);
if (!lock) {
try {
TimeUnit.SECONDS.sleep(1L);
} catch (Exception e) {
log.warn("try lock sleep warn");
} finally {
continue;
}
} else {
break;
}
}
if (lock) {
try {
List<TtRollUser> rollUserList = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper())
.eq(TtRollUser::getRollId, param.getRollId())
.list();
if (ttRoll.getPeopleNum() <= rollUserList.size()) {
return R.fail("roll房人满。");
}
// 锁内二次校验:防止并发重复加入
boolean alreadyJoined = rollUserList.stream()
.anyMatch(ru -> ru.getUserId().equals(player.getUserId()));
if (alreadyJoined) {
return R.fail("您当前已在该房间内,请勿重复加入房间!");
}
TtRollUser ttRollUser = TtRollUser.builder()
.rollId(param.getRollId())
.userId(player.getUserId())
.userName(player.getUserName())
.nickName(player.getNickName())
.avatar(player.getAvatar())
.joinTime(DateUtils.getNowDate())
.build();
rollUserService.getBaseMapper().insert(ttRollUser);
return R.ok();
} catch (Exception e) {
e.printStackTrace();
return R.fail("加入失败,请稍后重试。");
} finally {
redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId());
}
}
redisLock.unlock(RedisConstants.JOIN_ROLL_LOCK + param.getRollId());
return R.fail("服务器繁忙。");
}
// roll房开奖
@Override
@Transactional(rollbackFor = Exception.class)
public R endROLL(Integer rollId) {
log.info("server roll房开奖 rollId={}", rollId);
// 1 原子性抢占:将状态从0改为1,数据库行锁防止并发重复开奖
boolean updated = new LambdaUpdateChainWrapper<>(rollMapper)
.eq(TtRoll::getId, rollId)
.eq(TtRoll::getRollStatus, "0")
.eq(TtRoll::getDelFlag, "0")
.set(TtRoll::getRollStatus, RollStatus.END.getCode())
.set(TtRoll::getUpdateTime, new Date())
.update();
if (!updated) {
log.info("roll房{}已开奖或不存在,跳过", rollId);
return R.ok("已开奖,拒绝重复开奖");
}
TtRoll ttRoll = rollMapper.selectById(rollId);
List<TtRollUser> list = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper())
.eq(TtRollUser::getRollId, rollId)
.list();
if (ObjectUtil.isEmpty(list) || list.isEmpty()) {
ttRoll.setRollStatus(RollStatus.END.getCode());
rollMapper.updateById(ttRoll);
// 无人参与时,主播房退还全部奖池价值的金币和对应积分
if (RollType.ANCHOR.getCode().equals(ttRoll.getRollType())
&& ttRoll.getUserId() != null && ttRoll.getJackpotId() != null) {
TtRollJackpot jackpot = ttRollJackpotMapper.selectById(ttRoll.getJackpotId());
if (jackpot != null && jackpot.getTotalPrice() != null
&& jackpot.getTotalPrice().compareTo(BigDecimal.ZERO) > 0) {
BigDecimal refundAmount = jackpot.getTotalPrice();
// 全额退回金币
R<?> refund = ttUserService.updateOnlyUserAccount(
ttRoll.getUserId(), refundAmount, TtAccountRecordSource.ROLL_UNISSUED_REFUND);
if (R.isError(refund)) {
log.error("Roll房无人参与退款失败 rollId={}, userId={}, amount={}, msg={}",
rollId, ttRoll.getUserId(), refundAmount, refund.getMsg());
} else {
// 全额退回积分(创建时实际扣除额)
BigDecimal refundCredits = ttRoll.getDeductCredits() != null
? ttRoll.getDeductCredits() : BigDecimal.ZERO;
if (refundCredits.compareTo(BigDecimal.ZERO) > 0) {
ttUserService.updateUserCredits(
ttRoll.getUserId(), refundCredits, TtAccountRecordSource.ROLL_UNISSUED_REFUND);
}
String amountStr = refundAmount.stripTrailingZeros().toPlainString();
String creditsStr = refundCredits.stripTrailingZeros().toPlainString();
String content = "您创建的【" + ttRoll.getRollName()
+ "】无人参与,已退回 " + amountStr + " 金币、"
+ creditsStr + " 积分至您的账户。";
messageService.sendNotice(
Long.valueOf(ttRoll.getUserId()), "Roll房", content, resolveRollCategory(ttRoll));
log.info("Roll房无人参与退款成功 rollId={}, userId={}, amount={}, credits={}",
rollId, ttRoll.getUserId(), refundAmount, refundCredits);
}
}
}
return R.ok("roll房无人参与。");
}
// 2 分配系统指定,并返回剩余奖品
List<TtRollUser> rollUsers = new LambdaQueryChainWrapper<>(ttRollUserMapper)
.eq(TtRollUser::getRollId, ttRoll.getId())
.eq(TtRollUser::getGetPrizeWay, RollGetPrizeWay.SYS.getCode())
.list();
log.info("所有参与人员" + rollUsers);
Map<Long, TtRollJackpotOrnaments> surplusPrize = rollSurplusOrnaments(rollUsers, ttRoll);
// 3 过滤出未分配奖品的用户
LambdaQueryWrapper<TtRollUser> rollUserQuery = new LambdaQueryWrapper<>();
rollUserQuery
.eq(TtRollUser::getRollId, rollId)
.eq(TtRollUser::getStatus, "0");
if (!rollUsers.isEmpty()) {
List<Integer> rollUserIds = rollUsers.stream()
.map(TtRollUser::getId)
.collect(Collectors.toList());
rollUserQuery.notIn(TtRollUser::getId, rollUserIds);
}
List<TtRollUser> rollUserList = rollUserService.list(rollUserQuery);
// 4 打乱用户顺序
Collections.shuffle(rollUserList);
log.info("过滤系统指定以后" + rollUserList);
// 5 分配奖品、
List<TtBoxRecords> res = new ArrayList<>();
// 预加载所有等级信息,避免循环内逐条查询
Set<Integer> levelIds = surplusPrize.values().stream()
.map(TtRollJackpotOrnaments::getOrnamentLevelId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
Map<Integer, TtOrnamentsLevel> levelMap = new HashMap<>();
if (!levelIds.isEmpty()) {
List<TtOrnamentsLevel> levels = ttOrnamentsLevelService.listByIds(levelIds);
for (TtOrnamentsLevel lv : levels) {
levelMap.put(lv.getId(), lv);
}
}
// 一人拿一件 - 创建副本避免并发修改原始对象
boolean iGetIt = false;
for (TtRollUser rollUser : rollUserList) {
// if (iGetIt) continue;
for (Map.Entry<Long, TtRollJackpotOrnaments> entry : surplusPrize.entrySet()) {
Integer numb = entry.getValue().getOrnamentsNum();
if (numb <= 0) continue;
// 等级信息(从预加载 Map 获取)
TtOrnamentsLevel level = levelMap.get(entry.getValue().getOrnamentLevelId());
TtBoxRecords boxRecords = TtBoxRecords.builder()
.rollId(rollId)
.createTime(new Timestamp(System.currentTimeMillis()))
.holderUserId(rollUser.getUserId())
.ornamentId(entry.getKey())
.ornamentsPrice(entry.getValue().getPrice())
.imageUrl(entry.getValue().getImgUrl())
.ornamentName(entry.getValue().getOrnamentName())
.ornamentLevelImg(level != null ? level.getLevelImg() : null)
.source(TtboxRecordSource.ROLL.getCode())
.userId(rollUser.getUserId())
.updateTime(new Timestamp(System.currentTimeMillis()))
.status(IN_PACKSACK_ON.getCode())
.build();
res.add(boxRecords);
log.info("{}获得 {}", rollUser.getNickName(), entry.getValue().getOrnamentName());
// 扣减数量 - 使用独立变量避免修改共享对象
int updatedNum = numb - 1;
if (updatedNum <= 0) {
surplusPrize.remove(entry.getKey());
} else {
TtRollJackpotOrnaments updatedEntry = new TtRollJackpotOrnaments();
updatedEntry.setOrnamentLevelId(entry.getValue().getOrnamentLevelId());
updatedEntry.setPrice(entry.getValue().getPrice());
updatedEntry.setImgUrl(entry.getValue().getImgUrl());
updatedEntry.setOrnamentName(entry.getValue().getOrnamentName());
updatedEntry.setOrnamentsId(entry.getKey());
updatedEntry.setOrnamentsNum(updatedNum);
updatedEntry.setJackpotId(entry.getValue().getJackpotId());
surplusPrize.put(entry.getKey(), updatedEntry);
}
break;
}
}
log.info("保存开奖结果:" + res);
boxRecordsService.saveBatch(res);
// ===== 新增:主播房开奖后,未发放饰品价值退回创建者(主播),并发送退款通知 =====
refundUnissuedOrnaments(ttRoll, surplusPrize);
// ===== 新增退款结束 =====
// ===== 新增:Roll房开奖通知(过滤机器人用户 userType=03,覆盖中奖与未中奖用户)=====
// 事务化后,开奖通知需在事务提交后再异步发送,否则通知线程读取不到本事务尚未提交的中奖记录
final TtRoll noticeRoll = ttRoll;
if (TransactionSynchronizationManager.isSynchronizationActive()) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
AsyncManager.me().run(() -> sendRollResultNotice(noticeRoll));
}
});
} else {
AsyncManager.me().run(() -> sendRollResultNotice(noticeRoll));
}
// ===== 新增roll房开奖通知结束 =====
// 6 状态已在方法开头原子更新,无需重复更新
log.info("roll房开奖成功 rollId={} rollName={}", ttRoll.getId(), ttRoll.getRollName());
return R.ok();
}
/**
* 主播房开奖后,将未发放的饰品价值退回给创建者(主播),并发送退款通知。
* 官方房(rollType=0)创建时不扣费,不退款。
*
* @param ttRoll Roll房
* @param surplusPrize 开奖分配后剩余的奖池饰品,value.ornamentsNum 为剩余未发放数量
*/
private void refundUnissuedOrnaments(TtRoll ttRoll, Map<Long, TtRollJackpotOrnaments> surplusPrize) {
try {
// 仅主播房退款,官方房不扣费也不退款
if (ttRoll == null || !RollType.ANCHOR.getCode().equals(ttRoll.getRollType()) || ttRoll.getUserId() == null) {
return;
}
if (ObjectUtil.isEmpty(surplusPrize)) {
return;
}
// 汇总未发放饰品的数量与价值
int unissuedCount = 0;
BigDecimal refundAmount = BigDecimal.ZERO;
for (TtRollJackpotOrnaments ornament : surplusPrize.values()) {
Integer num = ornament.getOrnamentsNum();
if (num == null || num <= 0) {
continue;
}
BigDecimal price = ornament.getPrice() == null ? BigDecimal.ZERO : ornament.getPrice();
unissuedCount += num;
refundAmount = refundAmount.add(price.multiply(BigDecimal.valueOf(num)));
}
// 全部发放完毕,无需退款
if (refundAmount.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
// 退款分母:奖池总价值(对应创建时扣减的金币额)
BigDecimal jackpotTotal = BigDecimal.ZERO;
if (ttRoll.getJackpotId() != null) {
TtRollJackpot jackpot = ttRollJackpotMapper.selectById(ttRoll.getJackpotId());
if (jackpot != null && jackpot.getTotalPrice() != null) {
jackpotTotal = jackpot.getTotalPrice();
}
}
// 积分按未发放价值占奖池总价值的比例,退还创建时实际扣除的积分(避免积分虚增)
BigDecimal deductCredits = ttRoll.getDeductCredits() != null ? ttRoll.getDeductCredits() : BigDecimal.ZERO;
BigDecimal refundCredits = BigDecimal.ZERO;
if (deductCredits.compareTo(BigDecimal.ZERO) > 0 && jackpotTotal.compareTo(BigDecimal.ZERO) > 0) {
refundCredits = deductCredits.multiply(refundAmount)
.divide(jackpotTotal, 2, java.math.RoundingMode.HALF_UP);
}
// 退回主播金币(未发放价值,不动积分),来源标记为 Roll房未发放退回主播
R<?> refund = ttUserService.updateOnlyUserAccount(ttRoll.getUserId(), refundAmount, TtAccountRecordSource.ROLL_UNISSUED_REFUND);
if (R.isError(refund)) {
log.error("Roll房未发放饰品退款失败 rollId={}, userId={}, amount={}, msg={}",
ttRoll.getId(), ttRoll.getUserId(), refundAmount, refund.getMsg());
return;
}
// 按比例退回积分
if (refundCredits.compareTo(BigDecimal.ZERO) > 0) {
ttUserService.updateUserCredits(ttRoll.getUserId(), refundCredits, TtAccountRecordSource.ROLL_UNISSUED_REFUND);
}
// 发送退款通知给创建者(主播)
String amountStr = refundAmount.stripTrailingZeros().toPlainString();
String creditsStr = refundCredits.stripTrailingZeros().toPlainString();
String content = "您创建的【" + ttRoll.getRollName() + "】已开奖,本次有 " + unissuedCount
+ " 件饰品未发放,已退回价值 " + amountStr + " 金币、" + creditsStr + " 积分至您的账户。";
messageService.sendNotice(Long.valueOf(ttRoll.getUserId()), "Roll房", content, resolveRollCategory(ttRoll));
log.info("Roll房未发放饰品退款成功 rollId={}, userId={}, count={}, amount={}",
ttRoll.getId(), ttRoll.getUserId(), unissuedCount, refundAmount);
} catch (Exception e) {
log.error("Roll房未发放饰品退款异常 rollId={}, error={}",
ttRoll == null ? null : ttRoll.getId(), e.getMessage(), e);
}
}
//{ornamentId:TtRollJackpotOrnaments} TtRollJackpotOrnaments中有数量
public Map<Long, TtRollJackpotOrnaments> rollSurplusOrnaments(List<TtRollUser> rollUsers, TtRoll ttRoll) {
// 奖池所有物品
List<TtRollJackpotOrnaments> jackpotOrnaments = new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper)
.eq(TtRollJackpotOrnaments::getJackpotId, ttRoll.getJackpotId())
.list();
HashMap<Long, TtRollJackpotOrnaments> AllPrizePool = new HashMap<>();
for (TtRollJackpotOrnaments i : jackpotOrnaments) {
AllPrizePool.put(i.getOrnamentsId(), i);
}
// 没有系统指定,直接返回奖池所有物品
if (rollUsers.isEmpty()) return AllPrizePool;
List<Integer> rollUserIds = rollUsers.stream()
.map(TtRollUser::getId)
.collect(Collectors.toList());
// 系统指定信息
List<RollUserPrizeVO> RollUserPrizeVOs = ttRollUserPrizeMapper.byRollUserIds(rollUserIds);
// 没有系统指定,直接返回奖池所有物品
if (RollUserPrizeVOs.isEmpty()) return AllPrizePool;
// 分配系统指定
List<TtBoxRecords> res = new ArrayList<>();
for (RollUserPrizeVO userPrize : RollUserPrizeVOs) {
// 分配奖品
for (int i = 0; i < userPrize.getNumber(); i++) {
TtBoxRecords boxRecords = TtBoxRecords.builder()
.rollId(ttRoll.getId())
.createTime(new Timestamp(System.currentTimeMillis()))
.holderUserId(userPrize.getUserId())
.ornamentsPrice(userPrize.getPrice())
.imageUrl(userPrize.getImgUrl())
.ornamentId(userPrize.getOrnamentId())
.ornamentName(userPrize.getOrnamentName())
.source(TtboxRecordSource.ROLL.getCode())
.userId(userPrize.getUserId())
.updateTime(new Timestamp(System.currentTimeMillis()))
.status(IN_PACKSACK_ON.getCode())
.build();
res.add(boxRecords);
}
// 在所有奖品中减去当前用户获得的奖品
for (Long ornamentId : AllPrizePool.keySet()) {
if (ornamentId.equals(userPrize.getOrnamentId())) {
// 减去
TtRollJackpotOrnaments ornaments = AllPrizePool.get(ornamentId);
int count = ornaments.getOrnamentsNum() - userPrize.getNumber();
ornaments.setOrnamentsNum(Math.max(count, 0));
AllPrizePool.put(ornamentId, ornaments);
break;
}
}
}
// 批量保存已分配的奖品
log.info("保存系统指定奖品:{}", res.size());
boxRecordsService.saveBatch(res);
return AllPrizePool;
}
@Override
public List<RollListDataVO> getRollList(GetRollListParamRequest param) {
List<RollListDataVO> rollList = apiRollMapper.getRollList(param);
if (CollectionUtils.isEmpty(rollList)) {
return new ArrayList<>();
}
// 对数据进行处理
rollList.forEach(item -> {
// VIP等级门槛直接返回数字,空值默认0
if (item.getVipLevel() == null) {
item.setVipLevel(0);
}
// 补充奖池信息
TtRoll roll = this.getById(item.getId());
List<SimpleRollOrnamentVO> prizeList = ttRollJackpotOrnamentsMapper.rollShow(roll.getJackpotId());
// 直接设置对象列表,便于前端直接使用,无需额外 JSON.parse
item.setJackpotOrnamentsDataList(prizeList);
Integer ornamentsNumberOfRoll = null;
if (CollectionUtils.isNotEmpty(prizeList)) {
ornamentsNumberOfRoll = ttRollJackpotOrnamentsMapper.ornamentsNumberOfRoll(prizeList.get(0).getJackpotId());
}
// 空奖池或统计为空时默认0,避免前端 ornamentsNum.toString() 空指针崩溃
item.setOrnamentsNum(ornamentsNumberOfRoll == null ? 0 : ornamentsNumberOfRoll);
if (!StringUtils.isBlank(item.getRollPassword())) {
item.setHasPW(true);
item.setRollPassword(null);
} else {
item.setHasPW(false);
}
});
return rollList;
}
@Override
public R<RollDetailsDataVO> getRollDetails(Integer rollId, Long userId) {
TtRoll ttRoll = getById(rollId);
if (ObjectUtil.isEmpty(ttRoll)) return R.fail("不存在的roll房。");
RollDetailsDataVO data = apiRollMapper.getRollDetails(rollId);
data.setHasPW(!StringUtils.isBlank(ttRoll.getRollPassword()));
// 判断当前登录用户是否已参与
boolean joined = false;
if (userId != null) {
LambdaQueryWrapper<TtRollUser> wrapper = Wrappers.lambdaQuery();
wrapper.eq(TtRollUser::getRollId, rollId);
wrapper.eq(TtRollUser::getUserId, userId.intValue());
joined = ttRollUserMapper.selectCount(wrapper) > 0;
}
data.setJoined(joined);
// 奖池
List<SimpleRollOrnamentVO> prizeList = ttRollJackpotOrnamentsMapper.rollShow(ttRoll.getJackpotId());
data.setJackpotOrnamentsDataList(prizeList);
// 获奖名单
// List<TtBoxRecordsVO> boxRecords = ttBoxRecordsMapper.rollOpenPrize(rollId);
// data.setOpenPrizeList(boxRecords);
return R.ok(data);
}
@Override
public R<List<RollUserVO>> getRollPlayers(GetRollPlayersParam param) {
param.setLimit((param.getPage() - 1) * param.getSize());
List<RollUserVO> collect = ttRollUserMapper.pageByRollId(param);
return R.ok(collect);
}
@Override
public R<List<RollJackpotOrnamentsVO>> getRollPrizePool(GetRollPrizePool param) {
param.setLimit((param.getPage() - 1) * param.getSize());
TtRoll one = new LambdaQueryChainWrapper<>(ttRollMapper)
.eq(TtRoll::getId, param.getRollId())
.eq(TtRoll::getDelFlag, 0)
.one();
if (ObjectUtil.isNull(one)) return R.fail("不存在的roll房。");
param.setJackpotId(one.getJackpotId());
List<RollJackpotOrnamentsVO> list = ttRollJackpotOrnamentsMapper.listByRollId(param);
return R.ok(list);
}
@Override
public R<List<TtBoxRecordsVO>> getRollOpenPrize(GetRollOpenPrizeParam param) {
param.setLimit((param.getPage() - 1) * param.getSize());
// 获奖名单
List<TtBoxRecordsVO> boxRecords = ttBoxRecordsMapper.rollOpenPrize(param);
return R.ok(boxRecords);
}
private TtBoxRecords addBoxRecordsData(Integer rollId, Long ornamentsId, Integer userId, TtRollJackpotOrnaments rollJackpotOrnaments) {
TtOrnament ttOrnament = new LambdaQueryChainWrapper<>(ornamentsMapper).eq(TtOrnament::getId, ornamentsId).one();
TtBoxRecords boxRecords = TtBoxRecords.builder().build();
boxRecords.setUserId(userId);
boxRecords.setOrnamentId(ornamentsId);
boxRecords.setOrnamentsPrice(ttOrnament.getUsePrice());
boxRecords.setOrnamentsLevelId(rollJackpotOrnaments.getOrnamentLevelId());
boxRecords.setStatus(IN_PACKSACK_ON.getCode());
boxRecords.setCreateTime(DateUtils.getNowDate());
boxRecords.setSource(TtboxRecordSource.ROLL.getCode());
boxRecords.setRollId(rollId);
boxRecords.setHolderUserId(userId);
return boxRecords;
}
private TtRollJackpotOrnaments getRollJackpotOrnaments(Long ornamentsId, Map<Long, TtRollJackpotOrnaments> map, Integer jackpotId) {
TtRollJackpotOrnaments ttRollJackpotOrnaments = map.get(ornamentsId);
if (ttRollJackpotOrnaments == null) {
ttRollJackpotOrnaments = new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper).eq(TtRollJackpotOrnaments::getJackpotId, jackpotId)
.eq(TtRollJackpotOrnaments::getOrnamentsId, ornamentsId).one();
map.put(ornamentsId, ttRollJackpotOrnaments);
}
return ttRollJackpotOrnaments;
}
private List<Long> getSurplusOrnamentsList(Integer rollId, Integer jackpotId) {
// roll房奖池
List<TtRollJackpotOrnaments> ornamentsList = new LambdaQueryChainWrapper<>(rollJackpotOrnamentsMapper)
.eq(TtRollJackpotOrnaments::getJackpotId, jackpotId)
.list();
Map<Long, Integer> data = new HashMap<>();
for (TtRollJackpotOrnaments ornaments : ornamentsList) {
data.put(ornaments.getOrnamentsId(), ornaments.getOrnamentsNum());
}
// 打乱奖池
List<Long> surplusOrnamentsList = RandomUtils.toList(data);
// 系统指定的获奖者
List<TtRollUser> allocatedRollUserList = new LambdaQueryChainWrapper<>(rollUserService.getBaseMapper())
.eq(TtRollUser::getRollId, rollId)
.eq(TtRollUser::getStatus, "2")
.list();
List<Long> allocated = allocatedRollUserList.stream().map(TtRollUser::getOrnamentsId).collect(Collectors.toList());
for (Long i : allocated) {
Iterator<Long> iterator = surplusOrnamentsList.iterator();
while (iterator.hasNext()) {
Long next = iterator.next();
if (Objects.equals(next, i)) {
iterator.remove();
break;
}
}
}
return surplusOrnamentsList;
}
private BigDecimal getRechargeTotalAmount(Integer userId, Date rechargeStartTime) {
LambdaQueryWrapper<TtRechargeRecord> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(TtRechargeRecord::getUserId, userId)
.eq(TtRechargeRecord::getStatus, "0")
.eq(TtRechargeRecord::getDelFlag, "0")
.and(w -> w.isNull(TtRechargeRecord::getIsBlankCard).or().ne(TtRechargeRecord::getIsBlankCard, 1));
if (StringUtils.isNotNull(rechargeStartTime)) wrapper.gt(TtRechargeRecord::getCreateTime, rechargeStartTime);
return rechargeRecordMapper.selectList(wrapper).stream()
.map(e -> e.getAmountActuallyPaid() == null ? BigDecimal.ZERO : e.getAmountActuallyPaid())
.reduce(BigDecimal.ZERO, BigDecimal::add);
}
/**
* Roll房开奖结果通知(过滤机器人用户)
* 中奖用户:您参与的【官方Roll房/主播Roll房】已开奖,获得“龙狙(价值 ¥12,000)”,已放入背包。
* 未中奖用户:您参与的【官方Roll房/主播Roll房】已开奖,本次未中奖,感谢您的参与。
*/
private void sendRollResultNotice(TtRoll ttRoll) {
try {
Integer rollId = ttRoll.getId();
// 1. 查询该 roll 房所有真实参与用户(过滤机器人)
List<TtRollUser> rollUserList = new LambdaQueryChainWrapper<>(ttRollUserMapper)
.eq(TtRollUser::getRollId, rollId)
.list();
if (ObjectUtil.isEmpty(rollUserList)) {
log.warn("Roll 房无参与用户,跳过的开奖通知 rollId={}", rollId);
return;
}
List<Integer> allParticipantIds = rollUserList.stream()
.map(TtRollUser::getUserId)
.distinct()
.collect(Collectors.toList());
List<TtUser> realUsers = new LambdaQueryChainWrapper<>(userMapper)
.in(TtUser::getUserId, allParticipantIds)
.ne(TtUser::getUserType, UserType.ROBOT_USER.getCode())
.list();
if (ObjectUtil.isEmpty(realUsers)) {
return;
}
// 2. 汇总全部中奖记录(包含系统指定 + 随机分配,从数据库查询保证完整)
List<TtBoxRecords> allWinRecords = new LambdaQueryChainWrapper<>(ttBoxRecordsMapper)
.eq(TtBoxRecords::getRollId, rollId)
.eq(TtBoxRecords::getSource, TtboxRecordSource.ROLL.getCode())
.list();
fillMissingOrnamentNames(allWinRecords);
Map<Integer, List<TtBoxRecords>> userItemsMap = allWinRecords.stream()
.filter(r -> r.getHolderUserId() != null)
.collect(Collectors.groupingBy(TtBoxRecords::getHolderUserId));
// 3. 给每个真实用户发送通知
String rollTypeName = RollType.OFFICIAL.getCode().equals(ttRoll.getRollType()) ? "官方Roll房" : "主播Roll房";
String category = resolveRollCategory(ttRoll);
for (TtUser user : realUsers) {
List<TtBoxRecords> items = userItemsMap.get(user.getUserId());
String contentStr;
if (items == null || items.isEmpty()) {
contentStr = "您参与的【" + rollTypeName + "】已开奖,本次未中奖,感谢您的参与。";
} else {
// 一个账号理论上只获得一件饰品,取第一件
TtBoxRecords item = items.get(0);
String priceStr = item.getOrnamentsPrice() == null ? "0"
: item.getOrnamentsPrice().stripTrailingZeros().toPlainString();
contentStr = "您参与的【" + rollTypeName + "】已开奖,获得“"
+ item.getOrnamentName() + "(价值 ¥" + priceStr + ")”,已放入背包。";
}
messageService.sendNotice(Long.valueOf(user.getUserId()), "Roll房", contentStr, category);
}
} catch (Exception e) {
log.error("Roll 房开奖通知推送失败,rollId={}, error={}",
ttRoll == null ? null : ttRoll.getId(), e.getMessage(), e);
}
}
/**
* 根据 Roll房类型解析消息分类
*/
private String resolveRollCategory(TtRoll roll) {
if (roll == null) {
return MessageCategoryEnum.SYS_ROLL_STREAMER.getCode();
}
return RollType.OFFICIAL.getCode().equals(roll.getRollType())
? MessageCategoryEnum.SYS_ROLL_OFFICIAL.getCode()
: MessageCategoryEnum.SYS_ROLL_STREAMER.getCode();
}
/**
* 防御性补充中奖记录中的空饰品名称
*/
private void fillMissingOrnamentNames(List<TtBoxRecords> records) {
if (ObjectUtil.isEmpty(records)) {
return;
}
Set<Long> missingIds = records.stream()
.filter(r -> StringUtils.isEmpty(r.getOrnamentName()) && r.getOrnamentId() != null)
.map(TtBoxRecords::getOrnamentId)
.collect(Collectors.toSet());
if (missingIds.isEmpty()) {
return;
}
List<TtOrnament> ornaments = new LambdaQueryChainWrapper<>(ornamentsMapper)
.in(TtOrnament::getId, missingIds)
.list();
Map<Long, String> nameMap = new HashMap<>();
for (TtOrnament ornament : ornaments) {
String name = StringUtils.isNotEmpty(ornament.getShortName()) ? ornament.getShortName() : ornament.getName();
nameMap.put(ornament.getId(), StringUtils.isNotEmpty(name) ? name : "未知饰品");
}
for (TtBoxRecords record : records) {
if (StringUtils.isEmpty(record.getOrnamentName()) && record.getOrnamentId() != null) {
record.setOrnamentName(nameMap.getOrDefault(record.getOrnamentId(), "未知饰品"));
}
}
}
}

Some files were not shown because too many files have changed in this diff Show More