Initial commit

This commit is contained in:
2026-04-23 16:58:11 +08:00
commit 267eba1eca
2582 changed files with 273338 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
api: Spring项目入口模块如果是web项目则包含controller定义
common: 项目通用类模块
contract: 协议、数据模型定义比如dubbo、grpc的interface等发布到maven仓库给其他业务方使用。
repository与数据库交互接口包含dao层定义和mapper定义
service服务接口定义和实现
domain: 领域模型模块,包含实体类、值对象等
enums
model

View File

@@ -0,0 +1 @@
todo

View File

@@ -0,0 +1,134 @@
CREATE TABLE `game_sugar_spin` (
`gid` bigint NOT NULL AUTO_INCREMENT COMMENT '一次spin的唯一标识符。连续的free_spin算一次spin',
`score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '总得分',
`count` int NOT NULL DEFAULT '0' COMMENT 'step总数',
`feature` varchar(20) NOT NULL DEFAULT 'normal' COMMENT 'normal:普通spin free:购买的免费旋转 super_free:购买的超级免费旋转',
`extra_free` int NOT NULL DEFAULT '0' COMMENT '本次spin中额外触发的免费spin数量',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`gid`),
KEY `idx_feature` (`feature`),
KEY `idx_score` (`score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏spin表';
CREATE TABLE `game_sugar_step_info` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一自增id',
`gid` bigint NOT NULL COMMENT '所属的spin id',
`free_spin_id` int NOT NULL COMMENT '所属的free spin id',
`aes` int NOT NULL COMMENT 'step在本次spin中的step序号',
`multipler` varchar(512) DEFAULT '' COMMENT '开始前的倍数状态,偶数个,一对的前面是一维坐标,后面是倍数',
`grid` varchar(128) DEFAULT '' COMMENT '开始前的网格状态',
`symbol_links` varchar(1024) DEFAULT '' COMMENT '符号消除的集群信息JSON格式',
`score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '本次step的总倍数。基础倍数 * 级联翻倍',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gid_aes` (`gid`, `aes`) COMMENT 'gid+aes: 唯一标识一次step',
KEY `idx_gid` (`gid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏旋转步骤详情表';
CREATE TABLE `game_sugar_spin_free` (
`gid` bigint NOT NULL AUTO_INCREMENT COMMENT '一次spin的唯一标识符。连续的free_spin算一次spin',
`score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '总得分',
`count` int NOT NULL DEFAULT '0' COMMENT 'step总数',
`feature` varchar(20) NOT NULL DEFAULT 'normal' COMMENT 'normal:普通spin free:购买的免费旋转 super_free:购买的超级免费旋转',
`extra_free` int NOT NULL DEFAULT '0' COMMENT '本次spin中额外触发的免费spin数量',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`gid`),
KEY `idx_feature` (`feature`),
KEY `idx_score` (`score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏spin表';
CREATE TABLE `game_sugar_step_info_free` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一自增id',
`gid` bigint NOT NULL COMMENT '所属的spin id',
`free_spin_id` int NOT NULL COMMENT '所属的free spin id',
`aes` int NOT NULL COMMENT 'step在本次spin中的step序号',
`multipler` varchar(512) DEFAULT '' COMMENT '开始前的倍数状态,偶数个,一对的前面是一维坐标,后面是倍数',
`grid` varchar(128) DEFAULT '' COMMENT '开始前的网格状态',
`symbol_links` varchar(1024) DEFAULT '' COMMENT '符号消除的集群信息JSON格式',
`score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '本次step的总倍数。基础倍数 * 级联翻倍',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gid_aes` (`gid`, `aes`) COMMENT 'gid+aes: 唯一标识一次step',
KEY `idx_gid` (`gid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏旋转步骤详情表';
CREATE TABLE `game_sugar_spin_super` (
`gid` bigint NOT NULL AUTO_INCREMENT COMMENT '一次spin的唯一标识符。连续的free_spin算一次spin',
`score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '总得分',
`count` int NOT NULL DEFAULT '0' COMMENT 'step总数',
`feature` varchar(20) NOT NULL DEFAULT 'normal' COMMENT 'normal:普通spin free:购买的免费旋转 super_free:购买的超级免费旋转',
`extra_free` int NOT NULL DEFAULT '0' COMMENT '本次spin中额外触发的免费spin数量',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`gid`),
KEY `idx_feature` (`feature`),
KEY `idx_score` (`score`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏spin表';
CREATE TABLE `game_sugar_step_info_super` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '唯一自增id',
`gid` bigint NOT NULL COMMENT '所属的spin id',
`free_spin_id` int NOT NULL COMMENT '所属的free spin id',
`aes` int NOT NULL COMMENT 'step在本次spin中的step序号',
`multipler` varchar(512) DEFAULT '' COMMENT '开始前的倍数状态,偶数个,一对的前面是一维坐标,后面是倍数',
`grid` varchar(128) DEFAULT '' COMMENT '开始前的网格状态',
`symbol_links` varchar(1024) DEFAULT '' COMMENT '符号消除的集群信息JSON格式',
`score` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '本次step的总倍数。基础倍数 * 级联翻倍',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gid_aes` (`gid`, `aes`) COMMENT 'gid+aes: 唯一标识一次step',
KEY `idx_gid` (`gid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏旋转步骤详情表';
CREATE TABLE `game_sugar_user` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint NOT NULL COMMENT 'user表的id',
`total_bet` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户总下注额',
`total_win` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户总赢得金额',
`count` int NOT NULL DEFAULT '0' COMMENT '总spin次数',
`free_count` int NOT NULL DEFAULT '0' COMMENT '总购买free spin次数',
`super_free_count` int NOT NULL DEFAULT '0' COMMENT '总购买super_free spin次数',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏用户表';
CREATE TABLE `game_sugar_win` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`date` datetime NOT NULL COMMENT '日期',
`init_bet` decimal(10,2) NOT NULL COMMENT '系统初始下注额',
`total_bet` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户总下注额',
`total_win` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户总赢得金额',
`count` int NOT NULL DEFAULT '0' COMMENT '总spin次数',
`free_count` int NOT NULL DEFAULT '0' COMMENT '总购买free spin次数',
`super_free_count` int NOT NULL DEFAULT '0' COMMENT '总购买super_free spin次数',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_date` (`date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏每日统计表';
CREATE TABLE `game_sugar_record` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',
`user_id` bigint NOT NULL COMMENT 'user表的id',
`bet` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户下注额',
`win` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '用户赢得金额',
`multiplier` decimal(10,2) NOT NULL DEFAULT '0.00' COMMENT '倍率',
`kind` varchar(12) NOT NULL DEFAULT '' COMMENT '游戏模式',
`status` int NOT NULL DEFAULT 0 COMMENT '状态.0:进行中,1:完成',
`extra_free` int NOT NULL DEFAULT '0' COMMENT '本次spin中额外触发的免费spin数量',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '记录创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '记录更新时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='糖果游戏用户记录表';

View File

@@ -0,0 +1,40 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.ruoyi</groupId>
<artifactId>skins-service</artifactId>
<version>4.8.2</version>
</parent>
<artifactId>game-sugar-service</artifactId>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>ruoyi-framework</artifactId>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>service-admin</artifactId>
<version>4.8.2</version>
</dependency>
<dependency>
<groupId>com.ruoyi</groupId>
<artifactId>service-user</artifactId>
<version>4.8.2</version>
</dependency>
</dependencies>
</project>

View File

@@ -0,0 +1,10 @@
package com.ruoyi.game.sugar.contract.admin.request;
import lombok.Data;
@Data
public class AdminSugarUserListRequest {
private Integer pageNum;
private Integer pageSize;
private Long userId;
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.game.sugar.contract.admin.request;
import lombok.Data;
@Data
public class QueryUserRecordListRequest {
private Integer uid;
private Integer pageNum;
private Integer pageSize;
private String startTime;
private String endTime;
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.game.sugar.contract.admin.request;
import lombok.Data;
import java.util.List;
@Data
public class UpdateMockResultListRequest {
private Integer uid;
private List<Integer> normalScores;
private List<Integer> freeScores;
private List<Integer> superScores;
}

View File

@@ -0,0 +1,13 @@
package com.ruoyi.game.sugar.contract.admin.response;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import lombok.Data;
@Data
public class QueryGameConfig {
private GameSugarConfig config;
// 全局奖池数据
private double totalWin;
private double totalBet;
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.game.sugar.contract.api.request;
import lombok.Data;
@Data
public class QueryRankRequest {
private Integer pageNum;
private Integer pageSize;
// "real": 实时记录 "multipler": 倍数全服记录 "win": 赢奖全服记录排行
private String rankType;
private String kind;
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.game.sugar.contract.api.request;
import lombok.Data;
@Data
public class QueryRecordRequest {
private Integer pageNum;
private Integer pageSize;
private String kind;
private String rankType;
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.game.sugar.contract.api.response;
import lombok.Data;
@Data
public class ApiSugarBuySpinResponse {
private String credits;
private String balance;
private String cost;
private Boolean isSuperFree;
}

View File

@@ -0,0 +1,8 @@
package com.ruoyi.game.sugar.contract.api.response;
import lombok.Data;
@Data
public class ApiSugarHasFreeSpinResponse {
private Boolean hasFreeSpin;
}

View File

@@ -0,0 +1,18 @@
package com.ruoyi.game.sugar.contract.api.response;
import lombok.Data;
import java.util.List;
import java.util.Map;
@Data
public class ApiSugarSpinResultResponse {
private String balance;
private String credits;
private Integer index;
private Integer remainingFreeSpins;
private Integer extraFreeSpinCount;
// normal:普通旋转模式 standard:购买的免费旋转状态 super:购买的超级免费旋转状态
private String kind;
private List<Map<String, Object>> data;
}

View File

@@ -0,0 +1,23 @@
package com.ruoyi.game.sugar.contract.api.response;
import lombok.Data;
import java.util.List;
@Data
public class QueryRankResponse {
private Integer total;
private List<Record> rows;
@Data
public static class Record {
private String avatar;
private String nickName;
private String cost;
private String kind;
private String status;
private String reward;
private String multiplier;
private String createTime;
}
}

View File

@@ -0,0 +1,22 @@
package com.ruoyi.game.sugar.contract.api.response;
import lombok.Data;
import java.util.List;
@Data
public class QueryRecordResponse {
private Integer total;
private List<Record> rows;
@Data
public static class Record {
private String avatar;
private String nickName;
private String cost;
private String kind;
private String status;
private String reward;
private String createTime;
}
}

View File

@@ -0,0 +1,129 @@
package com.ruoyi.game.sugar.controller;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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.utils.DateTimeUtils;
import com.ruoyi.game.sugar.contract.admin.request.AdminSugarUserListRequest;
import com.ruoyi.game.sugar.contract.admin.request.QueryUserRecordListRequest;
import com.ruoyi.game.sugar.contract.admin.request.UpdateMockResultListRequest;
import com.ruoyi.game.sugar.contract.admin.response.QueryGameConfig;
import com.ruoyi.game.sugar.domain.GameSugarRecord;
import com.ruoyi.game.sugar.mapper.ApiGameSugarRecordMapper;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import com.ruoyi.game.sugar.repository.SugarRewardPoolCacheRepository;
import com.ruoyi.game.sugar.service.AdminSugarService;
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.math3.util.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@Api(tags = "极速永恒/Sugar Rush 1000")
@Slf4j
@RestController
@RequestMapping("/admin/sugar")
public class AdminSugarController extends BaseController {
@Autowired
private AdminSugarService adminSugarService;
@Autowired
private ApiGameSugarRecordMapper gameSugarRecordMapper;
@Autowired
private SugarRewardPoolCacheRepository sugarRewardPoolCacheRepository;
// 方法参数是一个具体的类型,则不用加@RequestParam
// 方法参数是一个Map则需要加@RequestParam
@ApiOperation("糖果用户信息列表")
@GetMapping("/user/list")
public PageDataInfo queryUserList(AdminSugarUserListRequest params) {
// pageNum 从1开始
Pair<Integer, List<Map<String, Object>>> list = adminSugarService.queryUserList(params);
PageDataInfo<Map<String, Object>> pageDataInfo = getPageData(list.getValue());
pageDataInfo.setTotal(list.getKey());
return pageDataInfo;
}
@ApiOperation("获取糖果游戏配置")
@GetMapping("/gameconfig")
public R<QueryGameConfig> getGameConfig() {
GameSugarConfig config = adminSugarService.getGameConfig();
QueryGameConfig result = new QueryGameConfig();
result.setConfig(config);
var cache = sugarRewardPoolCacheRepository.getCache();
result.setTotalWin(cache.getTotalWin().doubleValue());
result.setTotalBet(cache.getTotalBet().doubleValue());
return R.ok(result);
}
@ApiOperation("更新糖果游戏配置")
@PostMapping("/gameconfig")
public AjaxResult updateGameConfig(@RequestBody GameSugarConfig params) {
String msg = adminSugarService.updateGameConfig(params);
if (msg != null && !msg.isEmpty())
return AjaxResult.error(msg);
return AjaxResult.success();
}
@ApiOperation("糖果每日奖池信息列表")
@GetMapping("/dailywin/list")
public PageDataInfo queryDailyWinList(@RequestParam Map<String, Object> params) {
Pair<Integer, List<Map<String, Object>>> list = adminSugarService.queryDailyWinList(params);
PageDataInfo<Map<String, Object>> pageDataInfo = getPageData(list.getValue());
pageDataInfo.setTotal(list.getKey());
return pageDataInfo;
}
@ApiOperation("糖果主播临时结果列表")
@GetMapping("/mockresult/list")
public R<Map<String, List<Integer>>> getMockResultList(@RequestParam("uid") Integer uid) {
return R.ok(adminSugarService.getMockResultList(uid));
}
@ApiOperation("糖果主播临时结果列表")
@PostMapping("/mockresult/list")
public R<Map<String, List<Integer>>> updateMockResultList(@RequestBody UpdateMockResultListRequest request) {
var r = adminSugarService.updateMockResultList(request);
return R.ok(r);
}
@ApiOperation("游戏记录")
@GetMapping("/user/record/list")
public PageDataInfo queryUserRecordList(QueryUserRecordListRequest params) {
if (params.getPageNum() == null) {
params.setPageNum(1);
}
if (params.getPageSize() == null) {
params.setPageSize(10);
}
Page<GameSugarRecord> page = new Page<>(params.getPageNum(), params.getPageSize());
var result = new LambdaQueryChainWrapper<>(gameSugarRecordMapper)
.eq(params.getUid() != null, GameSugarRecord::getUserId, params.getUid())
.ge(params.getStartTime() != null, GameSugarRecord::getCreateTime, params.getStartTime())
.le(params.getEndTime() != null, GameSugarRecord::getCreateTime, params.getEndTime())
.orderByDesc(GameSugarRecord::getUpdateTime)
.page(page);
if (CollectionUtils.isNotEmpty(result.getRecords())) {
result.getRecords().forEach(item -> {
item.setCreateTime(DateTimeUtils.utcToZone8(item.getCreateTime()));
item.setUpdateTime(DateTimeUtils.utcToZone8(item.getUpdateTime()));
});
}
PageDataInfo<GameSugarRecord> pageDataInfo = new PageDataInfo<>();
pageDataInfo.setTotal(result.getTotal());
pageDataInfo.setRows(result.getRecords());
return pageDataInfo;
}
}

View File

@@ -0,0 +1,288 @@
package com.ruoyi.game.sugar.controller;
import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.common.annotation.UpdateUserCache;
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.utils.DateTimeUtils;
import com.ruoyi.common.utils.MoneyUtil;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.game.sugar.contract.api.request.QueryRankRequest;
import com.ruoyi.game.sugar.contract.api.request.QueryRecordRequest;
import com.ruoyi.game.sugar.contract.api.response.*;
import com.ruoyi.game.sugar.domain.GameSugarRecord;
import com.ruoyi.game.sugar.enums.GameSugarRecordStatus;
import com.ruoyi.game.sugar.mapper.ApiGameSugarRecordMapper;
import com.ruoyi.game.sugar.repository.SugarRewardPoolCacheRepository;
import com.ruoyi.game.sugar.service.ApiSugarService;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.user.service.ApiUserService;
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.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.math.BigDecimal;
import java.util.Collections;
import java.util.Objects;
import java.util.function.Function;
import java.util.stream.Collectors;
@Api(tags = "极速永恒/Sugar Rush 1000")
@Slf4j
@RestController
@RequestMapping("/api/sugar")
public class ApiSugarController extends BaseController {
@Autowired
private ApiSugarService apiSugarService;
@Autowired
private ApiUserService userService;
@Autowired
private ISysConfigService sysConfigService;
@Autowired
private ApiGameSugarRecordMapper recordMapper;
@Autowired
private TtUserMapper ttUserMapper;
@Autowired
private SugarRewardPoolCacheRepository sugarRewardPoolCacheRepository;
public R checkLogin() {
Long userId;
try {
userId = getUserId();
if (ObjectUtil.isEmpty(userId)) return R.fail(401, "登录过期,请重新登录。");
return R.ok(userId);
} catch (Exception e) {
return R.fail("登录过期,请重新登录。");
}
}
@ApiOperation("进行一次旋转")
@UpdateUserCache
@GetMapping("/dospin")
public R<ApiSugarSpinResultResponse> dospin(@RequestParam("bet") Long bet) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) {
return checkLogin;
}
Long userId = getUserId();
var r = userService.checkRecharge(userId.intValue());
if (R.isError(r)) {
return R.fail(r.getMsg());
}
return apiSugarService.doSpin(userId, BigDecimal.valueOf(bet));
}
@ApiOperation("购买免费旋转")
@UpdateUserCache
@GetMapping("/buy_free_spins")
public R<ApiSugarBuySpinResponse> buyFreeSpins(@RequestParam("kind") String kind, @RequestParam("bet") Long bet) {
String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance");
if ("1".equals(websiteMaintenance)) {
return R.fail("网站维护中......");
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) {
return checkLogin;
}
Long userId = getUserId();
var r = userService.checkRecharge(userId.intValue());
if (R.isError(r)) {
return R.fail(r.getMsg());
}
return apiSugarService.buyFreeSpins(userId, kind, BigDecimal.valueOf(bet));
}
@ApiOperation("检查是否有免费旋转")
@GetMapping("/has_free_spin")
public R<ApiSugarHasFreeSpinResponse> hasFreeSpin() {
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) {
return checkLogin;
}
Long userId = getUserId();
return apiSugarService.hasFreeSpin(userId);
}
@ApiOperation("查询记录")
@GetMapping("/records")
public R<QueryRecordResponse> queryRecords(QueryRecordRequest request) {
if (request.getPageNum() == null || request.getPageNum() <= 0) {
request.setPageNum(1);
}
if (request.getPageSize() == null || request.getPageSize() <= 0) {
request.setPageSize(10);
}
R checkLogin = checkLogin();
if (!checkLogin.getCode().equals(200)) {
return checkLogin;
}
Long userId = getUserId();
Page<GameSugarRecord> page = new Page<>(request.getPageNum(), request.getPageSize());
var query = new LambdaQueryChainWrapper<>(recordMapper).eq(GameSugarRecord::getUserId, userId);
if (StringUtils.isNotBlank(request.getKind())) {
String t = switch (request.getKind()) {
case "normal" -> "";
case "free" -> "standard";
case "super" -> "super";
default -> null;
};
if (t != null) {
query.eq(GameSugarRecord::getKind, t);
}
}
var data = query.orderByDesc(GameSugarRecord::getWin).page(page);
// 个人记录只查当前用户,直接单条查询用户信息
TtUser currentUser = ttUserMapper.selectTtUserById((long) userId.intValue());
var rows = data.getRecords().stream()
.map(v -> {
QueryRecordResponse.Record item = new QueryRecordResponse.Record();
if (currentUser != null) {
item.setAvatar(currentUser.getAvatar());
item.setNickName(currentUser.getNickName());
}
item.setCost(MoneyUtil.toStr(v.getBet()));
item.setReward(MoneyUtil.toStr(v.getWin()));
item.setKind(switch (v.getKind()) {
case "standard" -> "free";
case "super" -> "super";
default -> "normal";
});
item.setCreateTime(DateTimeUtils.dateTime(v.getCreateTime(), "MM-dd HH:mm:ss"));
var status = GameSugarRecordStatus.parseCode(v.getStatus());
if (status == null) {
item.setStatus("未知");
} else {
item.setStatus(status.getDesc());
}
return item;
}).collect(Collectors.toList());
QueryRecordResponse resp = new QueryRecordResponse();
resp.setTotal((int) data.getTotal());
resp.setRows(rows);
return R.ok(resp);
}
@ApiOperation("查询全服排行记录")
@GetMapping("/rank_records")
public R<QueryRankResponse> queryRankRecords(QueryRankRequest request) {
if (request.getPageNum() == null || request.getPageNum() <= 0) {
request.setPageNum(1);
}
if (request.getPageSize() == null || request.getPageSize() <= 0) {
request.setPageSize(10);
}
if (request.getPageNum() * request.getPageSize() > 100) {
QueryRankResponse resp = new QueryRankResponse();
resp.setRows(Collections.emptyList());
resp.setTotal(0);
return R.ok(resp);
}
Page<GameSugarRecord> page = new Page<>(request.getPageNum(), request.getPageSize());
var query = new LambdaQueryChainWrapper<>(recordMapper);
if (StringUtils.isNotBlank(request.getKind())) {
String t = switch (request.getKind()) {
case "normal" -> "";
case "free" -> "standard";
case "super" -> "super";
default -> null;
};
if (t != null) {
query.eq(GameSugarRecord::getKind, t);
}
}
if (Objects.equals(request.getRankType(), "multipler")) {
query.orderByDesc(GameSugarRecord::getMultiplier);
} else if (Objects.equals(request.getRankType(), "real")) {
query.orderByDesc(GameSugarRecord::getCreateTime);
} else if (Objects.equals(request.getRankType(), "win")) {
query.orderByDesc(GameSugarRecord::getWin);
}
var data = query.page(page);
if (CollectionUtils.isEmpty(data.getRecords())) {
QueryRankResponse resp = new QueryRankResponse();
resp.setTotal((int) data.getTotal());
resp.setRows(Collections.emptyList());
return R.ok(resp);
}
//var userIds = data.getRecords().stream().map(GameSugarRecord::getUserId).collect(Collectors.toList());
var userIds = data.getRecords().stream().map(v -> v.getUserId().intValue()).distinct().collect(Collectors.toList());
var userMap = ttUserMapper.selectByIds(userIds).stream()
.collect(Collectors.toMap(v -> v.getUserId().longValue(), Function.identity()));
var rows = data.getRecords().stream()
.filter(v -> {
// 过滤机器人03不允许上榜
var user = userMap.get(v.getUserId());
return user != null && !"03".equals(user.getUserType());
})
.map(v -> {
QueryRankResponse.Record item = new QueryRankResponse.Record();
if (userMap.containsKey(v.getUserId())) {
item.setAvatar(userMap.get(v.getUserId()).getAvatar());
item.setNickName(userMap.get(v.getUserId()).getNickName());
}
item.setCost(MoneyUtil.toStr(v.getBet()));
item.setCost(MoneyUtil.toStr(v.getBet()));
item.setReward(MoneyUtil.toStr(v.getWin()));
item.setKind(switch (v.getKind()) {
case "standard" -> "free";
case "super" -> "super";
default -> "normal";
});
item.setMultiplier(MoneyUtil.toStr(v.getMultiplier()));
item.setCreateTime(DateTimeUtils.dateTime(v.getCreateTime(), "MM-dd HH:mm:ss"));
var status = GameSugarRecordStatus.parseCode(v.getStatus());
if (status == null) {
item.setStatus("未知");
} else {
item.setStatus(status.getDesc());
}
return item;
}).collect(Collectors.toList());
QueryRankResponse resp = new QueryRankResponse();
resp.setTotal((int) data.getTotal());
resp.setRows(rows);
return R.ok(resp);
}
@ApiOperation("清空极速永恒奖池缓存(总下注/总返奖)归零")
@PostMapping("/rewardpool/reset")
public R resetRewardPool() {
sugarRewardPoolCacheRepository.resetCache();
return R.ok(null, "奖池缓存已清空");
}
}

View File

@@ -0,0 +1,39 @@
package com.ruoyi.game.sugar.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.experimental.Accessors;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
* 糖果游戏用户记录表
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@Builder
@TableName(value = "game_sugar_record")
public class GameSugarRecord {
@TableId(type = IdType.AUTO)
private Long id;
private Long userId;
private BigDecimal bet;
private BigDecimal win;
private BigDecimal multiplier;
private String kind;
private Integer extraFree;
/**
* @see com.ruoyi.game.sugar.enums.GameSugarRecordStatus
*/
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
}

View File

@@ -0,0 +1,26 @@
package com.ruoyi.game.sugar.enums;
import lombok.Getter;
@Getter
public enum GameSugarRecordStatus {
RUNING(0, "进行中"),
COMPLETE(1, "完成");
private final Integer code;
private final String desc;
GameSugarRecordStatus(Integer code, String desc) {
this.code = code;
this.desc = desc;
}
public static GameSugarRecordStatus parseCode(Integer code) {
for (GameSugarRecordStatus status : values()) {
if (status.code.equals(code)) {
return status;
}
}
return null;
}
}

View File

@@ -0,0 +1,12 @@
package com.ruoyi.game.sugar.mapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface AdminGameSugarSpinMapper {
@Select("SELECT max(score) FROM game_sugar_spin${type}")
Integer select(@Param("type") String type);
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.game.sugar.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.entity.GameSugarUser;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AdminGameSugarUserMapper extends BaseMapper<GameSugarUser> {
}

View File

@@ -0,0 +1,9 @@
package com.ruoyi.game.sugar.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.domain.entity.GameSugarWin;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface AdminGameSugarWinMapper extends BaseMapper<GameSugarWin> {
}

View File

@@ -0,0 +1,10 @@
package com.ruoyi.game.sugar.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.game.sugar.domain.GameSugarRecord;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ApiGameSugarRecordMapper extends BaseMapper<GameSugarRecord> {
}

View File

@@ -0,0 +1,29 @@
package com.ruoyi.game.sugar.mapper;
import com.ruoyi.domain.entity.GameSugarSpin;
import com.ruoyi.domain.entity.GameSugarStepInfo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
@Mapper
public interface ApiGameSugarSpinMapper {
@Select("SELECT * FROM game_sugar_spin${type} where ${where} ORDER BY score LIMIT 1")
GameSugarSpin selectMinOne(@Param("type") String type, @Param("where")String whereSql);
@Select("SELECT * FROM game_sugar_spin${type} where ${where} ORDER BY score LIMIT 5")
List<GameSugarSpin> selectMinFive(@Param("type") String type, @Param("where")String whereSql);
@Select("SELECT * FROM game_sugar_spin${type} where ${where} ORDER BY RAND() LIMIT 1")
GameSugarSpin selectRandomOne(@Param("type") String type, @Param("where")String whereSql);
@Select("SELECT * FROM game_sugar_spin${type} WHERE gid = #{gid} LIMIT 1")
GameSugarSpin selectByGid(@Param("gid") Long gid, @Param("type") String type);
@Select("SELECT * FROM game_sugar_step_info${type} WHERE gid = #{gid}")
List<GameSugarStepInfo> selectStepsByGid(@Param("gid") Long gid, @Param("type") String type);
}

View File

@@ -0,0 +1,31 @@
package com.ruoyi.game.sugar.mapper;
import com.ruoyi.domain.entity.GameSugarUser;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface ApiGameSugarUserMapper {
@Select("SELECT * FROM game_sugar_user where user_id = #{uid}")
GameSugarUser select(@Param("uid") Integer uid);
/**
* 插入用户数据,如果 user_id 已存在则更新
*/
@Insert("INSERT INTO game_sugar_user " +
"(user_id, total_bet, total_win, count, free_count, super_free_count) " +
"VALUES " +
"(#{user.userId}, #{user.totalBet}, #{user.totalWin}, #{user.count}, #{user.freeCount}, #{user.superFreeCount}) " +
"ON DUPLICATE KEY UPDATE " +
"total_bet = VALUES(total_bet), " +
"total_win = VALUES(total_win), " +
"count = VALUES(count), " +
"free_count = VALUES(free_count), " +
"super_free_count = VALUES(super_free_count), " +
"update_time = NOW()")
long insertOrUpdate(@Param("user") GameSugarUser user);
}

View File

@@ -0,0 +1,47 @@
package com.ruoyi.game.sugar.mapper;
import com.ruoyi.domain.entity.GameSugarWin;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Mapper
public interface ApiGameSugarWinMapper {
@Select("SELECT * FROM game_sugar_win where date = #{date} LIMIT 1")
GameSugarWin select(@Param("date") LocalDateTime dateTime);
@Insert("INSERT INTO game_sugar_win " +
"(date, init_bet, total_bet, total_win, count, free_count, super_free_count) " +
"VALUES " +
"(#{record.date}, #{record.initBet}, #{record.totalBet}, #{record.totalWin}, #{record.count}, #{record.freeCount}, #{record.superFreeCount})")
long insert(@Param("record") GameSugarWin record);
/**
* 通用更新方法:根据传入的字段动态累加
* 如果某个字段为 null则不更新该字段
*/
@Update("UPDATE game_sugar_win " +
"SET count = count + #{count}, " +
"free_count = #{freeCount} + free_count, " +
"super_free_count = #{superFreeCount} + super_free_count " +
"WHERE date = #{date}")
void updateCount(@Param("date") LocalDateTime date,
@Param("count") Integer count,
@Param("freeCount") Integer freeCount,
@Param("superFreeCount") Integer superFreeCount);
@Update("UPDATE game_sugar_win " +
"SET total_bet = total_bet + #{bet}, " +
"total_win = total_win + #{win} " +
"WHERE date = #{date}")
void updateAmount(@Param("date") LocalDateTime date,
@Param("bet") BigDecimal bet,
@Param("win") BigDecimal win);
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.game.sugar.model;
import lombok.Data;
@Data
public class GameSugarConfig {
private double defaultAwardPool;
private int[] jpPercent;
private double rtp;
private double minProfit;
}

View File

@@ -0,0 +1,33 @@
package com.ruoyi.game.sugar.model.cache;
import com.ruoyi.domain.entity.GameSugarStepInfo;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
@Data
public class SugarGameCache implements Serializable {
private BigDecimal bet;
private BigDecimal accountAmount;
private BigDecimal accountCredits;
private String kind;
private double globalRtp; // 本局开始时的奖池RTP用于判断是否触发免费旋转
private Integer totalFreeSpins;
private Integer freeSpinIndex;
private BigDecimal totalWin;
private GameSpin gameData;
private Long recordId;
@Data
public static class GameSpin implements Serializable {
private Long gid;
private BigDecimal score;
private List<GameSugarStepInfo> steps;
}
}

View File

@@ -0,0 +1,155 @@
package com.ruoyi.game.sugar.model.cache;
import lombok.Data;
import java.util.ArrayList;
import java.util.List;
@Data
public class SugarGameConfigCache {
private double globalRtp;
private double normalWinBoundary; // 1
private double freeWinBoundary; // 50
private double superWinBoundary; // 200
// RTP切换阈值(0.6)
private double rtpSwitchThreshold;
// 奖池 globalRtp >= 0.6
private List<AwardItem> normalAwardItems;
private List<AwardItem> freeAwardItems;
private List<AwardItem> superAwardItems;
// globalRtp < 0.6
private List<AwardItem> normalAwardItems2;
private List<AwardItem> freeAwardItems2;
private List<AwardItem> superAwardItems2;
@Data
public static class AwardItem {
private double lowRewardRound;
private double highRewardRound;
private double weight;
}
public static SugarGameConfigCache init() {
SugarGameConfigCache config = new SugarGameConfigCache();
// 初始化全局rtp
config.setGlobalRtp(0);
// 初始化边界值
config.setNormalWinBoundary(1);
config.setFreeWinBoundary(50);
config.setSuperWinBoundary(200);
// RTP 切换阈值
config.setRtpSwitchThreshold(0.6);
// 配置globalRtp >= 0.6
config.setNormalAwardItems(initNormalAwardItems());
config.setFreeAwardItems(initFreeAwardItems());
config.setSuperAwardItems(initSuperAwardItems());
// 配置globalRtp < 0.6
config.setNormalAwardItems2(initNormalAwardItems2());
config.setFreeAwardItems2(initFreeAwardItems2());
config.setSuperAwardItems2(initSuperAwardItems2());
return config;
}
// 普通旋转globalRtp >= 0.6
private static List<AwardItem> initNormalAwardItems() {
List<AwardItem> awardItems = new ArrayList<>();
awardItems.add(makeItem(0, 0, 60)); // 0 倍 60%
awardItems.add(makeItem(0, 0.9, 20)); // 0-0.9 倍 20%
awardItems.add(makeItem(1, 3, 10)); // 1-3 倍 10%
awardItems.add(makeItem(3.1, 5, 4)); // 3.1-5 倍 4%
awardItems.add(makeItem(5.1, 8, 3)); // 5.1-8 倍 3%
awardItems.add(makeItem(8.1, 12, 2)); // 8.1-12 倍 2%
awardItems.add(makeItem(12.1, 16, 1)); // 12.1-16 倍 1%
return awardItems;
}
// 免费旋转globalRtp >= 0.6
private static List<AwardItem> initFreeAwardItems() {
List<AwardItem> awardItems = new ArrayList<>();
awardItems.add(makeItem(0, 10, 50)); // 0-10 倍 50%
awardItems.add(makeItem(25, 30, 30)); // 25-30 倍 30%
awardItems.add(makeItem(40, 79, 9)); // 40-79 倍 9%
awardItems.add(makeItem(80, 150, 6)); // 80-150 倍 6%
awardItems.add(makeItem(200, 500, 4)); // 200-500 倍 4%
awardItems.add(makeItem(600, 1500, 1)); // 600-1500 倍 1%
return awardItems;
}
// 超级旋转globalRtp >= 0.6
private static List<AwardItem> initSuperAwardItems() {
List<AwardItem> awardItems = new ArrayList<>();
awardItems.add(makeItem(10, 40, 25)); // 10-40 倍 25%
awardItems.add(makeItem(61, 90, 25)); // 61-90 倍 25%
awardItems.add(makeItem(110, 180, 10)); // 110-180 倍 10%
awardItems.add(makeItem(210, 260, 15)); // 210-260 倍 15%
awardItems.add(makeItem(285, 310, 10)); // 285-310 倍 10%
awardItems.add(makeItem(380, 420, 10)); // 380-420 倍 10%
awardItems.add(makeItem(460, 560, 3)); // 460-560 倍 3%
awardItems.add(makeItem(700, 1000, 1)); // 700-1000 倍 1%
awardItems.add(makeItem(1500, 3000, 1)); // 1500-3000 倍 1%
return awardItems;
}
// 普通旋转globalRtp < 0.6
private static List<AwardItem> initNormalAwardItems2() {
List<AwardItem> awardItems = new ArrayList<>();
awardItems.add(makeItem(0, 0, 50)); // 0 倍 50%
awardItems.add(makeItem(0, 0.9, 20)); // 0-0.9 倍 20%
awardItems.add(makeItem(1, 3, 15)); // 1-3 倍 15%
awardItems.add(makeItem(4, 5, 6)); // 4-5 倍 6%
awardItems.add(makeItem(6, 8, 4)); // 6-8 倍 4%
awardItems.add(makeItem(10, 12, 3)); // 10-12 倍 3%
awardItems.add(makeItem(13, 15, 1.5));// 13-15 倍 1.5%
awardItems.add(makeItem(30, 40, 0.3));// 30-40 倍 0.3%
awardItems.add(makeItem(50, 60, 0.1));// 50-60 倍 0.1%
awardItems.add(makeItem(60, 80, 0.1));// 60-80 倍 0.1%
return awardItems;
}
// 免费旋转globalRtp < 0.6
private static List<AwardItem> initFreeAwardItems2() {
List<AwardItem> awardItems = new ArrayList<>();
awardItems.add(makeItem(0, 10, 42)); // 0-10 倍 42%
awardItems.add(makeItem(31, 39, 30)); // 31-39 倍 30%
awardItems.add(makeItem(50, 81, 11)); // 50-81 倍 11%
awardItems.add(makeItem(101, 151, 8)); // 101-151 倍 8%
awardItems.add(makeItem(201, 460, 6)); // 201-460 倍 6%
awardItems.add(makeItem(500, 600, 2)); // 500-600 倍 2%
awardItems.add(makeItem(600, 1100, 1)); // 600-1100 倍 1%
return awardItems;
}
// 超级旋转globalRtp < 0.6
private static List<AwardItem> initSuperAwardItems2() {
List<AwardItem> awardItems = new ArrayList<>();
awardItems.add(makeItem(10, 40, 20)); // 10-40 倍 20% 5
awardItems.add(makeItem(61, 90, 20)); // 61-90 倍 20% 15.1
awardItems.add(makeItem(110, 180, 10)); // 110-180 倍 10% 14
awardItems.add(makeItem(210, 260, 15)); // 210-260 倍 15% 35
awardItems.add(makeItem(315, 320, 10)); // 315-320 倍 10% 31
awardItems.add(makeItem(360, 400, 10)); // 360-400 倍 10% 38
awardItems.add(makeItem(460, 560, 10)); // 460-560 倍 10% 51
awardItems.add(makeItem(600, 700, 5)); // 600-700 倍 5% 32
awardItems.add(makeItem(800, 1200, 3)); // 800-1200 倍 3% 30
awardItems.add(makeItem(1300, 2000, 2)); // 1300-2000 倍 2%
return awardItems;
}
private static AwardItem makeItem(double low, double high, double weight) {
AwardItem item = new AwardItem();
item.setLowRewardRound(low);
item.setHighRewardRound(high);
item.setWeight(weight);
return item;
}
}

View File

@@ -0,0 +1,19 @@
package com.ruoyi.game.sugar.model.cache;
import lombok.Data;
import java.io.Serializable;
import java.util.List;
import java.util.Map;
@Data
public class SugarGameMockCache implements Serializable {
private List<MockItem> normalScores;
private List<MockItem> freeScores;
private List<MockItem> superScores;
@Data
public static class MockItem {
private int score;
}
}

View File

@@ -0,0 +1,11 @@
package com.ruoyi.game.sugar.model.cache;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class SugarRewardPoolCache {
private BigDecimal totalBet;
private BigDecimal totalWin;
}

View File

@@ -0,0 +1,21 @@
package com.ruoyi.game.sugar.model.cache;
import com.ruoyi.domain.entity.GameSugarSpin;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
@Data
public class SugarUserGameRecordCache {
List<Record> spinRecords;
@Data
public static class Record implements Serializable {
private GameSugarSpin spin;
private BigDecimal reward;
private BigDecimal cost;
}
}

View File

@@ -0,0 +1,24 @@
package com.ruoyi.game.sugar.repository;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.game.sugar.model.cache.SugarGameConfigCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import static com.ruoyi.admin.config.RedisConstants.SUGAR_GAME_CONFIG_CACHE;
@Component
public class GameConfigCacheRepository {
@Autowired
private RedisCache redisCache;
public void setCache(SugarGameConfigCache value) {
redisCache.setCacheObject(SUGAR_GAME_CONFIG_CACHE, value, 7, TimeUnit.DAYS);
}
public SugarGameConfigCache getCache() {
return redisCache.getCacheObject(SUGAR_GAME_CONFIG_CACHE);
}
}

View File

@@ -0,0 +1,38 @@
package com.ruoyi.game.sugar.repository;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.game.sugar.model.cache.SugarGameCache;
import com.ruoyi.game.sugar.model.cache.SugarGameMockCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import static com.ruoyi.admin.config.RedisConstants.SUGAR_GAME_CACHE;
import static com.ruoyi.admin.config.RedisConstants.SUGAR_GAME_MOCK_QUEUE;
@Component
public class SugarCacheRepository {
@Autowired
private RedisCache redisCache;
public void setCache(Long uid, SugarGameCache value) {
String cacheKey = SUGAR_GAME_CACHE + uid;
redisCache.setCacheObject(cacheKey, value, 7, TimeUnit.DAYS);
}
public SugarGameCache getCache(Long uid) {
String cacheKey = SUGAR_GAME_CACHE + uid;
return redisCache.getCacheObject(cacheKey);
}
public void setMockResultCache(Integer uid, SugarGameMockCache value) {
String cacheKey = SUGAR_GAME_MOCK_QUEUE + uid;
redisCache.setCacheObject(cacheKey, value, 7, TimeUnit.DAYS);
}
public SugarGameMockCache getMockResultCache(Integer uid) {
String cacheKey = SUGAR_GAME_MOCK_QUEUE + uid;
return redisCache.getCacheObject(cacheKey);
}
}

View File

@@ -0,0 +1,39 @@
package com.ruoyi.game.sugar.repository;
import com.ruoyi.game.sugar.model.cache.SugarRewardPoolCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import static com.ruoyi.admin.config.RedisConstants.SUGAR_GAME_REWARD_POOL_CACHE;
@Component
public class SugarRewardPoolCacheRepository {
@Autowired
public RedisTemplate redisTemplate;
public void incrementalBet(BigDecimal totalBet) {
redisTemplate.opsForValue().increment(SUGAR_GAME_REWARD_POOL_CACHE + ":bet", totalBet.doubleValue());
}
public void incrementalWin(BigDecimal totalWin) {
redisTemplate.opsForValue().increment(SUGAR_GAME_REWARD_POOL_CACHE + ":win", totalWin.doubleValue());
}
public SugarRewardPoolCache getCache() {
var totalBetS = redisTemplate.opsForValue().get(SUGAR_GAME_REWARD_POOL_CACHE + ":bet");
var totalWinS = redisTemplate.opsForValue().get(SUGAR_GAME_REWARD_POOL_CACHE + ":win");
SugarRewardPoolCache cache = new SugarRewardPoolCache();
cache.setTotalBet(totalBetS == null ? BigDecimal.ZERO : new BigDecimal(totalBetS.toString()));
cache.setTotalWin(totalWinS == null ? BigDecimal.ZERO : new BigDecimal(totalWinS.toString()));
return cache;
}
// 清空奖池缓存(总下注、总返奖归零)
public void resetCache() {
redisTemplate.delete(SUGAR_GAME_REWARD_POOL_CACHE + ":bet");
redisTemplate.delete(SUGAR_GAME_REWARD_POOL_CACHE + ":win");
}
}

View File

@@ -0,0 +1,26 @@
package com.ruoyi.game.sugar.repository;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.game.sugar.model.cache.SugarUserGameRecordCache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.concurrent.TimeUnit;
import static com.ruoyi.admin.config.RedisConstants.SUGAR_GAME_USER_RECORD_CACHE;
@Component
public class UserRecordCacheRepository {
@Autowired
private RedisCache redisCache;
public void setCache(Long uid, SugarUserGameRecordCache value) {
String cacheKey = SUGAR_GAME_USER_RECORD_CACHE + uid;
redisCache.setCacheObject(cacheKey, value, 7, TimeUnit.DAYS);
}
public SugarUserGameRecordCache getCache(Long uid) {
String cacheKey = SUGAR_GAME_USER_RECORD_CACHE + uid;
return redisCache.getCacheObject(cacheKey);
}
}

View File

@@ -0,0 +1,259 @@
package com.ruoyi.game.sugar.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.common.utils.json.SnakeCaseJsonUtils;
import com.ruoyi.domain.entity.GameSugarUser;
import com.ruoyi.domain.entity.GameSugarWin;
import com.ruoyi.domain.entity.sys.TtUser;
import com.ruoyi.game.sugar.contract.admin.request.AdminSugarUserListRequest;
import com.ruoyi.game.sugar.contract.admin.request.UpdateMockResultListRequest;
import com.ruoyi.game.sugar.mapper.AdminGameSugarSpinMapper;
import com.ruoyi.game.sugar.mapper.AdminGameSugarUserMapper;
import com.ruoyi.game.sugar.mapper.AdminGameSugarWinMapper;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import com.ruoyi.game.sugar.model.cache.SugarGameMockCache;
import com.ruoyi.game.sugar.repository.SugarCacheRepository;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.math.NumberUtils;
import org.apache.commons.math3.util.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;
@Service
@Slf4j
public class AdminSugarService {
private static final String EVENT_TAG = "糖果冲刺1000";
@Autowired
private AdminGameSugarUserMapper adminGameSugarUserMapper;
@Autowired
private AdminGameSugarWinMapper adminGameSugarWinMapper;
@Autowired
private TtUserMapper ttUserMapper;
@Autowired
private SysConfigMapper sysConfigMapper;
@Autowired
private SugarCacheRepository cacheRepository;
@Autowired
private AdminGameSugarSpinMapper adminGameSugarSpinMapper;
public Pair<Integer, List<Map<String, Object>>> queryUserList(AdminSugarUserListRequest params) {
Integer pageNum = params.getPageNum();
Integer pageSize = params.getPageSize();
if (pageNum == null || pageNum <= 0) pageNum = 1;
if (pageSize == null || pageSize <= 0) pageSize = 10;
Page<GameSugarUser> page = new Page<>(pageNum, pageSize);
page.setOptimizeCountSql(false);
LambdaQueryWrapper<GameSugarUser> wrapper = Wrappers.lambdaQuery();
if (params.getUserId() != null) {
wrapper.eq(GameSugarUser::getUserId, params.getUserId());
}
adminGameSugarUserMapper.selectList(wrapper);
IPage<GameSugarUser> userListPage = adminGameSugarUserMapper.selectPage(page, wrapper);
List<GameSugarUser> userList = userListPage.getRecords();
List<Map<String, Object>> r = userList.stream().map(v -> {
Map<String, Object> user = SnakeCaseJsonUtils.toSnakeCaseMap(v);
user.put("rtp", (v.getTotalBet().compareTo(BigDecimal.ZERO) > 0) ? v.getTotalWin().divide(v.getTotalBet(), 2, RoundingMode.HALF_UP) : 0);
user.put("profit", v.getTotalWin().subtract(v.getTotalBet()));
Long userId = v.getUserId().longValue();
TtUser ttUser = ttUserMapper.selectById(userId);
if (ttUser != null) {
user.put("nickname", ttUser.getNickName());
user.put("avatar", ttUser.getAvatar());
user.put("account_amount", ttUser.getAccountAmount());
user.put("account_credits", ttUser.getAccountCredits());
}
return user;
}).collect(Collectors.toList());
Integer total = (int) userListPage.getTotal();
return new Pair<>(total, r);
}
public GameSugarConfig getGameConfig() {
SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("game.sugar.setting");
if (sysConfig == null) {
GameSugarConfig config = new GameSugarConfig();
config.setRtp(0.85);
config.setJpPercent(new int[]{10, 100});
config.setDefaultAwardPool(0);
config.setMinProfit(0.3);
return config;
}
return SnakeCaseJsonUtils.fromSnakeCaseJson(sysConfig.getConfigValue(), GameSugarConfig.class);
}
public String updateGameConfig(GameSugarConfig params) {
if (params.getJpPercent().length != 2) {
return "jpPercent长度必须为2";
}
if (params.getRtp() < 0 || params.getRtp() > 1) {
return "rtp必须在[0,1]之间";
}
if (params.getMinProfit() < 0 || params.getMinProfit() > 1) {
return "minProfit必须在[0,1]之间";
}
SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("game.sugar.setting");
if (sysConfig == null) {
sysConfig = new SysConfig() {{
setConfigName("甜蜜糖果1000游戏配置");
setConfigKey("game.sugar.setting");
setConfigValue(SnakeCaseJsonUtils.toSnakeCaseJson(params));
}};
sysConfigMapper.insertConfig(sysConfig);
} else {
sysConfig.setConfigValue(SnakeCaseJsonUtils.toSnakeCaseJson(params));
sysConfigMapper.updateConfig(sysConfig);
}
return "";
}
public Pair<Integer, List<Map<String, Object>>> queryDailyWinList(Map<String, Object> params) {
int pageNum = NumberUtils.toInt((String) params.get("pageNum"), 1);
int pageSize = NumberUtils.toInt((String) params.get("pageSize"), 10);
Page<GameSugarWin> page = new Page<>(pageNum, pageSize);
page.setOptimizeCountSql(false);
LambdaQueryWrapper<GameSugarWin> wrapper = Wrappers.lambdaQuery();
if (params.get("dateRange[0]") != null) {
wrapper.ge(GameSugarWin::getDate, LocalDateTime.parse(params.get("dateRange[0]").toString()));
}
if (params.get("dateRange[1]") != null) {
wrapper.le(GameSugarWin::getDate, LocalDateTime.parse(params.get("dateRange[1]").toString()));
}
wrapper.orderByDesc(GameSugarWin::getUpdateTime);
IPage<GameSugarWin> userListPage = adminGameSugarWinMapper.selectPage(page, wrapper);
List<GameSugarWin> userList = userListPage.getRecords();
List<Map<String, Object>> r = userList.stream().map(v -> {
Map<String, Object> user = SnakeCaseJsonUtils.toSnakeCaseMap(v);
user.put("date", v.getDate().format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
user.put("rtp", (v.getTotalBet().compareTo(BigDecimal.ZERO) > 0) ? v.getTotalWin().divide(v.getTotalBet(), 2, RoundingMode.HALF_UP) : 0);
user.put("profit", v.getTotalBet().subtract(v.getTotalWin()).subtract(v.getInitBet()));
return user;
}).collect(Collectors.toList());
Integer total = (int) userListPage.getTotal();
return new Pair<>(total, r);
}
public Map<String, List<Integer>> getMockResultList(Integer uid) {
var cache = cacheRepository.getMockResultCache(uid);
if (cache == null) {
return new HashMap<>();
}
var result = new HashMap<String, List<Integer>>();
if (CollectionUtils.isNotEmpty(cache.getNormalScores()))
result.put("", cache.getNormalScores().stream().map(SugarGameMockCache.MockItem::getScore).collect(Collectors.toList()));
if (CollectionUtils.isNotEmpty(cache.getFreeScores()))
result.put("_free", cache.getFreeScores().stream().map(SugarGameMockCache.MockItem::getScore).collect(Collectors.toList()));
if (CollectionUtils.isNotEmpty(cache.getSuperScores()))
result.put("_super", cache.getSuperScores().stream().map(SugarGameMockCache.MockItem::getScore).collect(Collectors.toList()));
return result;
}
public Map<String, List<Integer>> updateMockResultList(UpdateMockResultListRequest request) {
List<String> types = Arrays.asList("", "_free", "_super");
List<Integer> maxScores = types.stream().map(type -> {
return adminGameSugarSpinMapper.select(type);
}).toList();
var cache = cacheRepository.getMockResultCache(request.getUid());
if (cache == null) {
cache = new SugarGameMockCache();
}
if (CollectionUtils.isNotEmpty(request.getNormalScores())) {
List<Integer> newScores = new ArrayList<>();
Integer maxScore = maxScores.get(0);
for (Integer score : request.getNormalScores()) {
if (score > maxScore) {
score = maxScore;
}
newScores.add(score);
}
var r = newScores.stream().map(v -> {
SugarGameMockCache.MockItem item = new SugarGameMockCache.MockItem();
item.setScore(v);
return item;
}).collect(Collectors.toList());
cache.setNormalScores(r);
} else {
cache.setNormalScores(new ArrayList<>());
}
if (CollectionUtils.isNotEmpty(request.getFreeScores())) {
List<Integer> newScores = new ArrayList<>();
Integer maxScore = maxScores.get(1);
for (Integer score : request.getFreeScores()) {
if (score > maxScore) {
score = maxScore;
}
newScores.add(score);
}
var r = newScores.stream().map(v -> {
SugarGameMockCache.MockItem item = new SugarGameMockCache.MockItem();
item.setScore(v);
return item;
}).collect(Collectors.toList());
cache.setFreeScores(r);
} else {
cache.setFreeScores(new ArrayList<>());
}
if (CollectionUtils.isNotEmpty(request.getSuperScores())) {
List<Integer> newScores = new ArrayList<>();
Integer maxScore = maxScores.get(2);
for (Integer score : request.getSuperScores()) {
if (score > maxScore) {
score = maxScore;
}
newScores.add(score);
}
var r = newScores.stream().map(v -> {
SugarGameMockCache.MockItem item = new SugarGameMockCache.MockItem();
item.setScore(v);
return item;
}).collect(Collectors.toList());
cache.setSuperScores(r);
} else {
cache.setSuperScores(new ArrayList<>());
}
cacheRepository.setMockResultCache(request.getUid(), cache);
var result = new HashMap<String, List<Integer>>();
if (CollectionUtils.isNotEmpty(cache.getNormalScores()))
result.put("", cache.getNormalScores().stream().map(SugarGameMockCache.MockItem::getScore).collect(Collectors.toList()));
if (CollectionUtils.isNotEmpty(cache.getFreeScores()))
result.put("_free", cache.getFreeScores().stream().map(SugarGameMockCache.MockItem::getScore).collect(Collectors.toList()));
if (CollectionUtils.isNotEmpty(cache.getSuperScores()))
result.put("_super", cache.getSuperScores().stream().map(SugarGameMockCache.MockItem::getScore).collect(Collectors.toList()));
return result;
}
}

View File

@@ -0,0 +1,670 @@
package com.ruoyi.game.sugar.service;
import com.baomidou.mybatisplus.extension.conditions.update.LambdaUpdateChainWrapper;
import com.ruoyi.admin.mapper.TtUserMapper;
import com.ruoyi.admin.model.UpdateUserAccountBo;
import com.ruoyi.admin.service.TtUserService;
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.MoneyUtil;
import com.ruoyi.common.utils.json.SnakeCaseJsonUtils;
import com.ruoyi.domain.common.constant.TtAccountRecordSource;
import com.ruoyi.domain.entity.GameSugarSpin;
import com.ruoyi.domain.entity.GameSugarStepInfo;
import com.ruoyi.domain.entity.GameSugarUser;
import com.ruoyi.domain.entity.GameSugarWin;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.game.sugar.contract.api.response.ApiSugarBuySpinResponse;
import com.ruoyi.game.sugar.contract.api.response.ApiSugarHasFreeSpinResponse;
import com.ruoyi.game.sugar.contract.api.response.ApiSugarSpinResultResponse;
import com.ruoyi.game.sugar.domain.GameSugarRecord;
import com.ruoyi.game.sugar.enums.GameSugarRecordStatus;
import com.ruoyi.game.sugar.mapper.ApiGameSugarRecordMapper;
import com.ruoyi.game.sugar.mapper.ApiGameSugarSpinMapper;
import com.ruoyi.game.sugar.mapper.ApiGameSugarUserMapper;
import com.ruoyi.game.sugar.mapper.ApiGameSugarWinMapper;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import com.ruoyi.game.sugar.model.cache.SugarGameCache;
import com.ruoyi.game.sugar.model.cache.SugarGameConfigCache;
import com.ruoyi.game.sugar.model.cache.SugarGameMockCache;
import com.ruoyi.game.sugar.repository.GameConfigCacheRepository;
import com.ruoyi.game.sugar.repository.SugarCacheRepository;
import com.ruoyi.game.sugar.repository.SugarRewardPoolCacheRepository;
import com.ruoyi.game.sugar.repository.UserRecordCacheRepository;
import com.ruoyi.game.sugar.service.sugar.*;
import com.ruoyi.system.domain.SysConfig;
import com.ruoyi.system.mapper.SysConfigMapper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.math3.util.Pair;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import static com.ruoyi.admin.config.RedisConstants.COMMON_LOCK;
@Service
@Slf4j
public class ApiSugarService {
private static final String EVENT_TAG = "糖果冲刺1000";
@Autowired
private RedisLock redisLock;
@Autowired
private SugarCacheRepository sugarCacheRepository;
@Autowired
private UserRecordCacheRepository userRecordCacheRepository;
@Autowired
private GameConfigCacheRepository gameConfigCacheRepository;
@Autowired
private TtUserMapper userMapper;
@Autowired
private ApiGameSugarSpinMapper gameSugarSpinMapper;
@Autowired
private SelectNormalGameLogic selectNormalGameLogic;
@Autowired
private SelectFreeGameLogic selectFreeGameLogic;
@Autowired
private SelectSuperFreeGameLogic selectSuperFreeGameLogic;
@Autowired
private PostGameLogicManager postGameLogicManager;
@Autowired
private ApiGameSugarUserMapper apiGameSugarUserMapper;
@Autowired
private ApiGameSugarWinMapper apiGameSugarWinMapper;
@Autowired
private SysConfigMapper sysConfigMapper;
@Autowired
private TtUserService userService;
@Autowired
private ApiGameSugarRecordMapper apiGameSugarRecordMapper;
@Autowired
private SugarRewardPoolCacheRepository rewardPoolCacheRepository;
// 符号映射
private static final Map<String, Integer> SYMBOL_MAPPING = new HashMap<String, Integer>() {{
put("S", 1);
put("A", 3);
put("B", 4);
put("C", 5);
put("D", 6);
put("E", 7);
put("F", 8);
put("G", 9);
}};
// 符号映射
private static final Map<Integer, Integer> FREE_SPIN_MAPPING = new HashMap<Integer, Integer>() {{
put(3, 10);
put(4, 12);
put(5, 15);
put(6, 20);
put(7, 30);
}};
@Transactional(rollbackFor = Throwable.class)
public R<ApiSugarSpinResultResponse> doSpin(Long userId, BigDecimal bet) {
if (bet == null || bet.compareTo(BigDecimal.ZERO) <= 0) {
return R.fail("下注金额不正确");
}
Boolean lock = redisLock.tryLock(COMMON_LOCK + userId, 3L, 7L, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(lock)) {
return R.fail("请求频繁,加锁失败");
}
try {
// 获取用户缓存
SugarGameCache cache = sugarCacheRepository.getCache(userId);
UpdateUserAccountBo updateUserAccountBo;
if (cache == null) {
// 扣减余额
R<UpdateUserAccountBo> updateResult = userService.updateUserAccount(userId.intValue(), bet.negate(), TtAccountRecordSource.SUGAR_SPIN_NORMAL);
if (R.isError(updateResult)) {
return R.fail(updateResult.getMsg());
}
updateUserAccountBo = updateResult.getData();
// 更新统计数据
GameSugarUser sugarUser = apiGameSugarUserMapper.select(userId.intValue());
if (sugarUser == null) {
sugarUser = initUser(userId);
}
sugarUser.setTotalBet(sugarUser.getTotalBet().add(bet));
apiGameSugarUserMapper.insertOrUpdate(sugarUser);
if (apiGameSugarWinMapper.select(DateUtils.getTodayAtZero()) == null) {
initWin();
}
apiGameSugarWinMapper.updateAmount(DateUtils.getTodayAtZero(), bet, BigDecimal.ZERO);
rewardPoolCacheRepository.incrementalBet(bet);
// 初始化缓存
cache = new SugarGameCache();
cache.setKind("normal");
cache.setAccountAmount(updateUserAccountBo.getAccountAmount());
cache.setAccountCredits(updateUserAccountBo.getAccountCredits());
cache.setBet(bet);
cache.setTotalFreeSpins(0);
cache.setFreeSpinIndex(0);
cache.setTotalWin(new BigDecimal("0"));
// 选择游戏
SugarGameCache.GameSpin gameData = selectGame("", userId, bet, bet);
cache.setGameData(gameData);
cache.setRecordId(initRecord(userId, "", bet));
log.info("开始新spin: uid={}, gid={}, score={}", userId, gameData.getGid(), gameData.getScore());
}
bet = cache.getBet();
// 处理免费旋转
Integer freeSpinIndex = cache.getFreeSpinIndex() + 1;
cache.setFreeSpinIndex(freeSpinIndex);
// 获取当前免费旋转的步骤
List<GameSugarStepInfo> currentSteps = cache.getGameData().getSteps().stream()
.filter(step -> step.getFreeSpinId().equals(freeSpinIndex))
.collect(Collectors.toList());
int extraFreeCount = 0;
if (CollectionUtils.isNotEmpty(currentSteps)) {
int count = (int) currentSteps.get(currentSteps.size() - 1).getGrid().chars()
.filter(c -> c == 'S').count();
Integer freeSpins = FREE_SPIN_MAPPING.get(count);
if (freeSpins != null) {
extraFreeCount = freeSpins;
cache.setTotalFreeSpins(cache.getTotalFreeSpins() + freeSpins);
}
}
// 转换并计算总赢
Pair<BigDecimal, List<Map<String, Object>>> pair = convert(currentSteps, cache.getTotalWin(), bet);
BigDecimal totalWin = pair.getFirst();
List<Map<String, Object>> steps = pair.getSecond();
cache.setTotalWin(totalWin);
// 保存缓存更新
sugarCacheRepository.setCache(userId, cache);
// 检查是否结束, 结束则进行结算
if (CollectionUtils.isEmpty(steps)) {
BigDecimal score = cache.getGameData().getScore();
BigDecimal win = score.multiply(bet);
// 增加余额
if (win.compareTo(BigDecimal.ZERO) <= 0) {
win = new BigDecimal("0.01");
}
R<UpdateUserAccountBo> updateResult = userService.updateUserAccount(userId.intValue(), win, TtAccountRecordSource.SUGAR_SPIN_REWARD);
if (R.isError(updateResult)) {
return R.fail(updateResult.getMsg());
}
updateUserAccountBo = updateResult.getData();
cache.setAccountAmount(updateUserAccountBo.getAccountAmount());
cache.setAccountCredits(updateUserAccountBo.getAccountCredits());
if ("normal".equals(cache.getKind()) && score.compareTo(new BigDecimal("30")) >= 0) {
String triggeredKind = score.compareTo(new BigDecimal("200")) >= 0 ? "super" : "standard";
String triggeredType = "super".equals(triggeredKind) ? "_super" : "_free";
log.info("普通旋转触发免费旋转: uid={}, score={}, triggeredKind={}", userId, score, triggeredKind);
sugarCacheRepository.setCache(userId, null);
BigDecimal finalWin = win;
Long recordId = cache.getRecordId();
AsyncManager.me().run(() -> {
GameSugarUser sugarUser = apiGameSugarUserMapper.select(userId.intValue());
sugarUser.setTotalWin(sugarUser.getTotalWin().add(finalWin));
apiGameSugarUserMapper.insertOrUpdate(sugarUser);
new LambdaUpdateChainWrapper<>(apiGameSugarRecordMapper)
.eq(GameSugarRecord::getId, recordId)
.set(GameSugarRecord::getStatus, GameSugarRecordStatus.COMPLETE.getCode())
.set(GameSugarRecord::getWin, finalWin)
.setSql("multiplier = win / bet")
.update();
apiGameSugarWinMapper.updateAmount(DateUtils.getTodayAtZero(), BigDecimal.ZERO, finalWin);
rewardPoolCacheRepository.incrementalWin(finalWin);
});
// 初始化免费旋转缓存
SugarGameCache freeCache = new SugarGameCache();
freeCache.setKind(triggeredKind);
freeCache.setBet(bet);
freeCache.setAccountAmount(updateUserAccountBo.getAccountAmount());
freeCache.setAccountCredits(updateUserAccountBo.getAccountCredits());
freeCache.setTotalFreeSpins(0);
freeCache.setFreeSpinIndex(0);
freeCache.setTotalWin(BigDecimal.ZERO);
SugarGameCache.GameSpin freeGameData = selectGame(triggeredType, userId, bet, BigDecimal.ZERO);
freeCache.setGameData(freeGameData);
freeCache.setRecordId(initRecord(userId, triggeredKind, BigDecimal.ZERO));
sugarCacheRepository.setCache(userId, freeCache);
ApiSugarSpinResultResponse triggeredResult = new ApiSugarSpinResultResponse();
triggeredResult.setKind(triggeredKind);
triggeredResult.setBalance(MoneyUtil.toStr(updateUserAccountBo.getAccountAmount()));
triggeredResult.setCredits(MoneyUtil.toStr(updateUserAccountBo.getAccountCredits()));
triggeredResult.setIndex(freeSpinIndex - 1);
triggeredResult.setRemainingFreeSpins(0);
triggeredResult.setExtraFreeSpinCount(0);
triggeredResult.setData(steps);
return R.ok(triggeredResult);
}
// 清除缓存
sugarCacheRepository.setCache(userId, null);
// 更新统计数据
BigDecimal finalWin = win;
Long recordId = cache.getRecordId();
AsyncManager.me().run(() -> {
GameSugarUser sugarUser = apiGameSugarUserMapper.select(userId.intValue());
sugarUser.setTotalWin(sugarUser.getTotalWin().add(finalWin));
apiGameSugarUserMapper.insertOrUpdate(sugarUser);
new LambdaUpdateChainWrapper<>(apiGameSugarRecordMapper)
.eq(GameSugarRecord::getId, recordId)
.set(GameSugarRecord::getStatus, GameSugarRecordStatus.COMPLETE.getCode())
.set(GameSugarRecord::getWin, finalWin)
.setSql("multiplier = win / bet")
.update();
apiGameSugarWinMapper.updateAmount(DateUtils.getTodayAtZero(), BigDecimal.ZERO, finalWin);
rewardPoolCacheRepository.incrementalWin(finalWin);
});
}
// 构建返回结果
ApiSugarSpinResultResponse result = new ApiSugarSpinResultResponse();
result.setKind(cache.getKind());
result.setBalance(MoneyUtil.toStr(cache.getAccountAmount()));
result.setCredits(MoneyUtil.toStr(cache.getAccountCredits()));
result.setIndex(freeSpinIndex - 1);
result.setRemainingFreeSpins(Math.max(0, cache.getTotalFreeSpins() + 1 - freeSpinIndex));
result.setExtraFreeSpinCount(extraFreeCount);
result.setData(steps);
return R.ok(result);
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
log.error("错误", e);
return R.ok();
} finally {
redisLock.unlock(COMMON_LOCK + userId);
}
}
private GameSugarUser initUser(Long userId) {
GameSugarUser sugarUser = new GameSugarUser();
sugarUser.setUserId(userId.intValue());
sugarUser.setTotalWin(BigDecimal.ZERO);
sugarUser.setTotalBet(BigDecimal.ZERO);
sugarUser.setCount(0);
sugarUser.setFreeCount(0);
sugarUser.setSuperFreeCount(0);
apiGameSugarUserMapper.insertOrUpdate(sugarUser);
return sugarUser;
}
public GameSugarConfig getGameConfig() {
SysConfig sysConfig = sysConfigMapper.checkConfigKeyUnique("game.sugar.setting");
if (sysConfig == null) {
GameSugarConfig config = new GameSugarConfig();
config.setRtp(0.8);
config.setJpPercent(new int[]{10, 100});
config.setDefaultAwardPool(0);
config.setMinProfit(0.3);
return config;
}
return SnakeCaseJsonUtils.fromSnakeCaseJson(sysConfig.getConfigValue(), GameSugarConfig.class);
}
private GameSugarWin initWin() {
GameSugarConfig config = getGameConfig();
GameSugarWin sugarWin = new GameSugarWin();
sugarWin.setDate(DateUtils.getTodayAtZero());
sugarWin.setTotalWin(BigDecimal.ZERO);
sugarWin.setInitBet(BigDecimal.valueOf(config.getDefaultAwardPool()));
sugarWin.setTotalBet(BigDecimal.valueOf(config.getDefaultAwardPool()));
sugarWin.setCount(0);
sugarWin.setFreeCount(0);
sugarWin.setSuperFreeCount(0);
apiGameSugarWinMapper.insert(sugarWin);
return sugarWin;
}
private SugarGameCache.GameSpin selectGame(String type, Long userId, BigDecimal bet, BigDecimal cost) {
StrategyContext context = new StrategyContext();
GameSugarUser sugarUser = apiGameSugarUserMapper.select(userId.intValue());
if (sugarUser == null) {
sugarUser = initUser(userId);
}
SugarGameConfigCache cache = gameConfigCacheRepository.getCache();
if (cache == null) {
cache = SugarGameConfigCache.init();
}
context.setGameConfigCache(cache);
GameSugarConfig gameConfig = getGameConfig();
var rewardPool = rewardPoolCacheRepository.getCache();
context.setType(type);
context.setBet(bet);
context.setCost(cost);
context.setSugarUser(sugarUser);
context.setSysConfig(gameConfig);
context.setRewardPool(rewardPool);
String whereSql = "";
if (Objects.equals(type, "")) {
sugarUser.setCount(sugarUser.getCount() + 1);
apiGameSugarWinMapper.updateCount(DateUtils.getTodayAtZero(), 1, 0, 0);
whereSql = selectNormalGameLogic.where(context);
} else if (Objects.equals(type, "_free")) {
sugarUser.setCount(sugarUser.getCount() + 1);
sugarUser.setFreeCount(sugarUser.getFreeCount() + 1);
apiGameSugarWinMapper.updateCount(DateUtils.getTodayAtZero(), 1, 1, 0);
whereSql = selectFreeGameLogic.where(context);
} else if (Objects.equals(type, "_super")) {
sugarUser.setCount(sugarUser.getCount() + 1);
sugarUser.setSuperFreeCount(sugarUser.getSuperFreeCount() + 1);
apiGameSugarWinMapper.updateCount(DateUtils.getTodayAtZero(), 1, 0, 1);
whereSql = selectSuperFreeGameLogic.where(context);
}
apiGameSugarUserMapper.insertOrUpdate(sugarUser);
if (StringUtils.isBlank(whereSql)) {
whereSql = " 1=1 ";
}
log.info(EVENT_TAG + " uid={} whereSql={}", userId, whereSql);
GameSugarSpin spin = null;
var mockResultCache = sugarCacheRepository.getMockResultCache(Math.toIntExact(userId));
if (mockResultCache != null) {
List<SugarGameMockCache.MockItem> r = null;
if (type.equals("_free")) {
r = mockResultCache.getFreeScores();
}
if (type.isEmpty()) {
r = mockResultCache.getNormalScores();
}
if (type.equals("_super")) {
r = mockResultCache.getSuperScores();
}
if (r != null && !r.isEmpty()) {
whereSql = " score >= " + r.removeFirst().getScore();
spin = gameSugarSpinMapper.selectMinOne(type, whereSql);
log.info(EVENT_TAG + " uid={} use mock result.sql={}", userId, whereSql);
sugarCacheRepository.setMockResultCache(Math.toIntExact(userId), mockResultCache);
}
}
if (spin == null) {
spin = gameSugarSpinMapper.selectRandomOne(type, whereSql);
if (spin == null) {
// 随机选择一个游戏
spin = gameSugarSpinMapper.selectRandomOne(type, "score <= 10");
}
}
// 获取游戏步骤
List<GameSugarStepInfo> steps = gameSugarSpinMapper.selectStepsByGid(spin.getGid(), type);
steps = steps.stream().peek(v -> {
v.setMultipler2(SnakeCaseJsonUtils.parseSnakeCaseList(v.getMultipler(), Integer.class));
v.setSymbolLinks2(SnakeCaseJsonUtils.parseSnakeCaseList(v.getSymbolLinks(),
GameSugarStepInfo.SymbolLink.class));
}).collect(Collectors.toList());
SugarGameCache.GameSpin result = new SugarGameCache.GameSpin();
result.setGid(spin.getGid());
result.setScore(spin.getScore());
result.setSteps(steps);
return result;
}
@Transactional(rollbackFor = Throwable.class)
public R<ApiSugarBuySpinResponse> buyFreeSpins(Long userId, String kind, BigDecimal bet) {
if (bet == null || bet.compareTo(BigDecimal.ZERO) <= 0) {
return R.fail("下注金额不正确");
}
if (kind == null || !Arrays.asList("standard", "super").contains(kind)) {
return R.fail("类型不正确");
}
Boolean lock = redisLock.tryLock(COMMON_LOCK + userId, 3L, 7L, TimeUnit.SECONDS);
if (!Boolean.TRUE.equals(lock)) {
return R.fail("请求频繁,加锁失败");
}
try {
// 检查用户是否正在进行免费旋转
SugarGameCache cache = sugarCacheRepository.getCache(userId);
if (cache != null) {
return R.fail("免费旋转进行中,请先完成当前免费旋转");
}
// 根据类型处理
BigDecimal requiredAmount = null;
String type = "";
TtAccountRecordSource t = null;
if ("standard".equals(kind)) {
requiredAmount = bet.multiply(new BigDecimal(50));
type = "_free";
t = TtAccountRecordSource.SUGAR_BUY_FREE_SPIN;
} else {
requiredAmount = bet.multiply(new BigDecimal(200));
type = "_super";
t = TtAccountRecordSource.SUGAR_BUY_SUPER_SPIN;
}
// 检查余额和扣减余额
R<UpdateUserAccountBo> updateResult = userService.updateUserAccount(userId.intValue(), requiredAmount.negate(), t);
if (R.isError(updateResult)) {
return R.fail(updateResult.getMsg());
}
GameSugarUser sugarUser = apiGameSugarUserMapper.select(userId.intValue());
if (sugarUser == null) {
sugarUser = initUser(userId);
}
sugarUser.setTotalBet(sugarUser.getTotalBet().add(requiredAmount));
apiGameSugarUserMapper.insertOrUpdate(sugarUser);
if (apiGameSugarWinMapper.select(DateUtils.getTodayAtZero()) == null) {
initWin();
}
apiGameSugarWinMapper.updateAmount(DateUtils.getTodayAtZero(), requiredAmount, BigDecimal.ZERO);
rewardPoolCacheRepository.incrementalBet(requiredAmount);
// 初始化缓存
cache = new SugarGameCache();
cache.setAccountAmount(updateResult.getData().getAccountAmount());
cache.setAccountCredits(updateResult.getData().getAccountCredits());
cache.setKind(kind);
cache.setBet(bet);
cache.setTotalFreeSpins(0);
cache.setFreeSpinIndex(0);
cache.setTotalWin(BigDecimal.ZERO);
// 选择标准免费游戏
SugarGameCache.GameSpin gameData = selectGame(type, userId, bet, requiredAmount);
cache.setGameData(gameData);
var recordId = initRecord(userId, kind, requiredAmount);
cache.setRecordId(recordId);
// 保存到缓存
sugarCacheRepository.setCache(userId, cache);
log.info("用户{}购买了{}免费旋转,下注:{},扣除金额:{},gid={},score={}", userId, kind, bet,
requiredAmount, gameData.getGid(), gameData.getScore());
ApiSugarBuySpinResponse response = new ApiSugarBuySpinResponse();
response.setCost(MoneyUtil.toStr(requiredAmount));
response.setBalance(MoneyUtil.toStr(updateResult.getData().getAccountAmount()));
response.setCredits(MoneyUtil.toStr(updateResult.getData().getAccountCredits()));
response.setIsSuperFree("super".equals(kind));
return R.ok(response);
} catch (Exception e) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return R.fail("内部错误");
} finally {
redisLock.unlock(COMMON_LOCK + userId);
}
}
private Long initRecord(Long userId, String kind, BigDecimal bet) {
GameSugarRecord record = new GameSugarRecord();
record.setUserId(userId);
record.setKind(kind);
record.setBet(bet);
record.setWin(BigDecimal.ZERO);
record.setExtraFree(0);
record.setStatus(GameSugarRecordStatus.RUNING.getCode());
apiGameSugarRecordMapper.insert(record);
return record.getId();
}
/**
* 转换步骤数据
*
* @param steps 步骤列表
* @param totalWin 当前总赢取金额
* @param bet 下注金额
* @return 转换结果
*/
public Pair<BigDecimal, List<Map<String, Object>>> convert(List<GameSugarStepInfo> steps, BigDecimal totalWin, BigDecimal bet) {
List<Map<String, Object>> result = new ArrayList<>();
if (CollectionUtils.isEmpty(steps)) {
return new Pair<>(totalWin, result);
}
for (GameSugarStepInfo step : steps) {
// 转换grid为符号映射值
List<Integer> s = step.getGrid().chars().mapToObj(c -> (char) c)
.map(symbol -> SYMBOL_MAPPING.getOrDefault(symbol + "", 0))
.collect(Collectors.toList());
// 转换multipler为slm格式
List<List<Integer>> slm = new ArrayList<>();
List<Integer> multipler = step.getMultipler2();
for (int i = 0; i < multipler.size(); i += 2) {
if (i + 1 < multipler.size()) {
slm.add(Arrays.asList(multipler.get(i), multipler.get(i + 1)));
}
}
// 处理symbol_links
List<List<List<Integer>>> symbolMultipler = new ArrayList<>();
List<List<Object>> symbolWin = new ArrayList<>();
List<List<Object>> symbolMark = new ArrayList<>();
for (GameSugarStepInfo.SymbolLink link : step.getSymbolLinks2()) {
// 处理s_mark
List<Object> markItem = new ArrayList<>();
markItem.add(SYMBOL_MAPPING.getOrDefault(link.getSymbol(), 0));
// 添加坐标列表
List<Integer> coordinates = new ArrayList<>(link.getLoc());
markItem.add(coordinates);
// 添加奖励金额
markItem.add(bet.multiply(BigDecimal.valueOf(link.getScore()))
.setScale(2, RoundingMode.HALF_UP).toPlainString());
symbolMark.add(markItem);
// 处理sm和swin
if (link.getTotalMulti() > 1) {
// 处理symbol_multipler (sm)
List<List<Integer>> multiItem = new ArrayList<>();
List<Integer> linkMultipler = link.getMultipler();
for (int i = 0; i < linkMultipler.size(); i += 2) {
if (i + 1 < linkMultipler.size()) {
multiItem.add(Arrays.asList(
linkMultipler.get(i),
linkMultipler.get(i + 1)
));
}
}
symbolMultipler.add(multiItem);
// 处理symbol_win (swin)
symbolWin.add(Arrays.asList(
bet.multiply(BigDecimal.valueOf(link.getBaseScore())),
link.getTotalMulti()
));
}
}
// 计算总赢取金额
totalWin = totalWin.add(bet.multiply(step.getScore()));
// 构建步骤结果
Map<String, Object> stepResult = new LinkedHashMap<>();
stepResult.put("tw", totalWin.setScale(2, RoundingMode.HALF_UP).toPlainString());
stepResult.put("w", step.getScore().multiply(bet)
.setScale(2, RoundingMode.HALF_UP).toPlainString());
stepResult.put("slm", slm);
stepResult.put("sm", symbolMultipler);
stepResult.put("swin", symbolWin);
stepResult.put("s_mark", symbolMark);
stepResult.put("s", s);
result.add(stepResult);
}
return new Pair<>(totalWin, result);
}
public R<ApiSugarHasFreeSpinResponse> hasFreeSpin(Long userId) {
// 检查用户是否正在进行免费旋转
SugarGameCache cache = sugarCacheRepository.getCache(userId);
if (cache != null) {
ApiSugarHasFreeSpinResponse resp = new ApiSugarHasFreeSpinResponse();
resp.setHasFreeSpin(true);
return R.ok(resp);
}
ApiSugarHasFreeSpinResponse resp = new ApiSugarHasFreeSpinResponse();
resp.setHasFreeSpin(false);
return R.ok(resp);
}
}

View File

@@ -0,0 +1,32 @@
package com.ruoyi.game.sugar.service.sugar;
import com.ruoyi.domain.entity.GameSugarSpin;
import com.ruoyi.game.sugar.mapper.ApiGameSugarSpinMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.RoundingMode;
@Component
public class PostGameLogicManager {
@Autowired
private ApiGameSugarSpinMapper gameSugarSpinMapper;
public GameSugarSpin postGameLogic(StrategyContext context, GameSugarSpin spin) {
if (spin.getScore().doubleValue() >= 20) {
return spin;
}
if (context.getCost().doubleValue() < 100) {
return spin;
} else if (context.getCost().doubleValue() <= 150) {
String whereSql = " score between 20 and 70 ";
spin = gameSugarSpinMapper.selectRandomOne(context.getType(), whereSql);
} else {
double maxScore = context.getCost().divide(context.getBet(), RoundingMode.HALF_UP).doubleValue() / 2;
double minScore = maxScore / 1.5;
String whereSql = String.format(" score between %.2f and %.2f ", minScore, maxScore);
spin = gameSugarSpinMapper.selectRandomOne(context.getType(), whereSql);
}
return spin;
}
}

View File

@@ -0,0 +1,59 @@
package com.ruoyi.game.sugar.service.sugar;
import com.ruoyi.common.utils.RandomUtil;
import com.ruoyi.game.sugar.model.cache.SugarGameConfigCache;
import com.ruoyi.game.sugar.service.sugar.strategy.MaxScoreStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
@Component
@Slf4j
public class SelectFreeGameLogic {
@Autowired
private SelectGameCommonLogic selectGameCommonLogic;
private static final String EVENT_TAG = "糖果冲刺1000免费旋转选择策略";
public String where(StrategyContext context) {
// todo
var userTag = selectGameCommonLogic.getUserTag(context);
var rewardPool = context.getRewardPool();
BigDecimal totalBet = rewardPool.getTotalBet();
BigDecimal totalWin = rewardPool.getTotalWin();
double globalRtp = 0.9;
if (totalBet.compareTo(BigDecimal.ZERO) > 0) {
globalRtp = totalWin.doubleValue() / totalBet.doubleValue();
}
context.getGameConfigCache().setGlobalRtp(globalRtp);
var table = selectGameCommonLogic.calcCurrentProbabilityTable(context);
table = table.stream().sorted(Comparator.comparing(SugarGameConfigCache.AwardItem::getLowRewardRound)).toList();
var indexes = new ArrayList<Integer>();
for (int i = 0; i < table.size(); i++) {
indexes.add(i);
}
var weights = table.stream().map(SugarGameConfigCache.AwardItem::getWeight).toList();
log.info("使用的奖励表: {}", table);
log.info("weights: {}", weights);
var rewardIndex = RandomUtil.choices(indexes, weights, 1).getFirst();
log.info("rewardIndex: {}", rewardIndex);
var rewardItem = table.get(rewardIndex);
var minScore = rewardItem.getLowRewardRound();
var maxScore = rewardItem.getHighRewardRound();
if (minScore == maxScore && rewardIndex < table.size() - 1) {
maxScore = table.get(rewardIndex + 1).getLowRewardRound();
}
context.getControlParam().setMinScore(minScore);
context.getControlParam().setMaxScore(maxScore);
new MaxScoreStrategy().calcScore(context);
return SelectGameCommonLogic.applyParam(log, EVENT_TAG, context);
}
}

View File

@@ -0,0 +1,95 @@
package com.ruoyi.game.sugar.service.sugar;
import cn.hutool.core.lang.Pair;
import com.ruoyi.game.sugar.model.cache.SugarGameConfigCache;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Component
@Slf4j
public class SelectGameCommonLogic {
public UserTag getUserTag(StrategyContext context) {
UserTag result = UserTag.NEWBIE;
// 获取用户信息
BigDecimal totalBet = context.getSugarUser().getTotalBet();
int betCount = context.getSugarUser().getCount();
// 判定新人奖池条件投注额小于5000或投注次数小于100次
if (totalBet.compareTo(new BigDecimal("5000")) < 0 || betCount < 100) {
return UserTag.NEWBIE;
}
// 判定老人奖池条件投注额大于等于1w或投注次数大于300次
if (totalBet.compareTo(new BigDecimal("10000")) >= 0 || betCount > 300) {
return UserTag.OLDIE;
}
// 判定中等奖池条件投注额大于4999小于1w或投注次数小于300次
if ((totalBet.compareTo(new BigDecimal("4999")) > 0 && totalBet.compareTo(new BigDecimal("10000")) < 0) || betCount < 300) {
return UserTag.MIDDLE;
}
return result;
}
public List<SugarGameConfigCache.AwardItem> calcCurrentProbabilityTable(StrategyContext context) {
SugarGameConfigCache cache = context.getGameConfigCache();
double globalRtp = cache.getGlobalRtp();
boolean useTable2 = globalRtp < cache.getRtpSwitchThreshold();
log.info("globalRtp={}, threshold={}, useTable2={}", globalRtp, cache.getRtpSwitchThreshold(), useTable2);
List<SugarGameConfigCache.AwardItem> awardItems = getAwardItems(context, cache, useTable2);
return new ArrayList<>(awardItems);
}
private static List<SugarGameConfigCache.AwardItem> getAwardItems(
StrategyContext context, SugarGameConfigCache cache, boolean useTable2) {
return switch (context.getType()) {
case "" -> useTable2 ? cache.getNormalAwardItems2() : cache.getNormalAwardItems();
case "_free" -> useTable2 ? cache.getFreeAwardItems2() : cache.getFreeAwardItems();
case "_super" -> useTable2 ? cache.getSuperAwardItems2() : cache.getSuperAwardItems();
default -> cache.getNormalAwardItems();
};
}
public static String applyParam(Logger log, String EVENT_TAG, StrategyContext context) {
// 校验
StrategyContext.ControlParam param = context.getControlParam();
if (param.getMinScore() > param.getMaxScore()) {
log.error("{} minScore is greater than maxScore.minScore={},maxScore={}",
EVENT_TAG, param.getMinScore(), param.getMaxScore());
param.setMinScore(-1);
}
String whereSql = "";
if (param.getMinScore() >= 0) {
whereSql += " and score >= " + param.getMinScore();
}
if (param.getMaxScore() >= 0) {
whereSql += " and score < " + param.getMaxScore();
}
if (param.isJP()) {
whereSql += " and extra_free > 0";
} else if (param.isNotJP()) {
whereSql += " and extra_free = 0";
}
if (whereSql.startsWith(" and")) {
whereSql = whereSql.substring(4);
}
return whereSql;
}
}

View File

@@ -0,0 +1,66 @@
package com.ruoyi.game.sugar.service.sugar;
import com.ruoyi.common.utils.RandomUtil;
import com.ruoyi.common.utils.json.SnakeCaseJsonUtils;
import com.ruoyi.game.sugar.model.cache.SugarGameConfigCache;
import com.ruoyi.game.sugar.service.sugar.strategy.MaxScoreStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
@Component
@Slf4j
public class SelectNormalGameLogic {
@Autowired
private SelectGameCommonLogic selectGameCommonLogic;
private static final String EVENT_TAG = "糖果冲刺1000普通选择策略";
public String where(StrategyContext context) {
// todo
var userTag = selectGameCommonLogic.getUserTag(context);
var rewardPool = context.getRewardPool();
BigDecimal totalBet = rewardPool.getTotalBet();
BigDecimal totalWin = rewardPool.getTotalWin();
double globalRtp = 0.9;
if (totalBet.compareTo(BigDecimal.ZERO) > 0) {
globalRtp = totalWin.doubleValue() / totalBet.doubleValue();
}
context.getGameConfigCache().setGlobalRtp(globalRtp);
var table = selectGameCommonLogic.calcCurrentProbabilityTable(context);
table = table.stream().sorted(Comparator.comparing(SugarGameConfigCache.AwardItem::getLowRewardRound)).toList();
var indexes = new ArrayList<Integer>();
for (int i = 0; i < table.size(); i++) {
indexes.add(i);
}
var weights = table.stream().map(SugarGameConfigCache.AwardItem::getWeight).toList();
log.info("使用的奖励表: {}", SnakeCaseJsonUtils.toSnakeCaseJson(table));
log.info("weights: {}", SnakeCaseJsonUtils.toSnakeCaseJson(weights));
var rewardIndex = RandomUtil.choices(indexes, weights, 1).getFirst();
log.info("rewardIndex: {}", rewardIndex);
var rewardItem = table.get(rewardIndex);
var minScore = rewardItem.getLowRewardRound();
var maxScore = rewardItem.getHighRewardRound();
if (minScore == maxScore && rewardIndex < table.size() - 1) {
maxScore = table.get(rewardIndex + 1).getLowRewardRound();
}
context.getControlParam().setMinScore(minScore);
context.getControlParam().setMaxScore(maxScore);
new MaxScoreStrategy().calcScore(context);
if (minScore >= 30) {
context.getControlParam().setJP(true);
} else {
context.getControlParam().setNotJP(true);
}
return SelectGameCommonLogic.applyParam(log, EVENT_TAG, context);
}
}

View File

@@ -0,0 +1,61 @@
package com.ruoyi.game.sugar.service.sugar;
import com.ruoyi.common.utils.RandomUtil;
import com.ruoyi.game.sugar.model.cache.SugarGameConfigCache;
import com.ruoyi.game.sugar.service.sugar.strategy.MaxScoreStrategy;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
@Component
@Slf4j
public class SelectSuperFreeGameLogic {
@Autowired
private SelectGameCommonLogic selectGameCommonLogic;
private static final String EVENT_TAG = "糖果冲刺1000超级免费旋转选择策略";
public static List<Strategy> strategies = new ArrayList<Strategy>() {{
add(new MaxScoreStrategy());
}};
public String where(StrategyContext context) {
var userTag = selectGameCommonLogic.getUserTag(context);
var rewardPool = context.getRewardPool();
BigDecimal totalBet = rewardPool.getTotalBet();
BigDecimal totalWin = rewardPool.getTotalWin();
double globalRtp = 0.9;
if (totalBet.compareTo(BigDecimal.ZERO) > 0) {
globalRtp = totalWin.doubleValue() / totalBet.doubleValue();
}
context.getGameConfigCache().setGlobalRtp(globalRtp);
var table = selectGameCommonLogic.calcCurrentProbabilityTable(context);
table = table.stream().sorted(Comparator.comparing(SugarGameConfigCache.AwardItem::getLowRewardRound)).toList();
var indexes = new ArrayList<Integer>();
for (int i = 0; i < table.size(); i++) {
indexes.add(i);
}
var weights = table.stream().map(SugarGameConfigCache.AwardItem::getWeight).toList();
log.info("使用的奖励表: {}", table);
log.info("weights: {}", weights);
var rewardIndex = RandomUtil.choices(indexes, weights, 1).getFirst();
log.info("rewardIndex: {}", rewardIndex);
var rewardItem = table.get(rewardIndex);
var minScore = rewardItem.getLowRewardRound();
var maxScore = rewardItem.getHighRewardRound();
if (minScore == maxScore && rewardIndex < table.size() - 1) {
maxScore = table.get(rewardIndex + 1).getLowRewardRound();
}
context.getControlParam().setMinScore(minScore);
context.getControlParam().setMaxScore(maxScore);
new MaxScoreStrategy().calcScore(context);
return SelectGameCommonLogic.applyParam(log, EVENT_TAG, context);
}
}

View File

@@ -0,0 +1,5 @@
package com.ruoyi.game.sugar.service.sugar;
public interface Strategy {
public void calcScore(StrategyContext context);
}

View File

@@ -0,0 +1,43 @@
package com.ruoyi.game.sugar.service.sugar;
import com.ruoyi.domain.entity.GameSugarUser;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import com.ruoyi.game.sugar.model.cache.SugarGameConfigCache;
import com.ruoyi.game.sugar.model.cache.SugarRewardPoolCache;
import com.ruoyi.game.sugar.model.cache.SugarUserGameRecordCache;
import lombok.Data;
import java.math.BigDecimal;
@Data
public class StrategyContext {
private String type;
private BigDecimal bet;
private BigDecimal cost;
private GameSugarUser sugarUser;
private SugarRewardPoolCache rewardPool;
private GameSugarConfig sysConfig;
private SugarUserGameRecordCache recordCache;
private SugarGameConfigCache gameConfigCache;
private ControlParam controlParam;
public StrategyContext() {
ControlParam param = new ControlParam();
param.setMinScore(-1);
param.setMaxScore(-1);
param.setJP(false);
param.setNotJP(false);
param.setRandom(false);
this.controlParam = param;
}
@Data
public static class ControlParam {
private double minScore;
private double maxScore;
private boolean isRandom;
private boolean isJP;
private boolean notJP;
}
}

View File

@@ -0,0 +1,14 @@
package com.ruoyi.game.sugar.service.sugar;
import lombok.AllArgsConstructor;
@AllArgsConstructor
public enum UserTag {
NEWBIE("new"), // 新人奖池
MIDDLE("middle"), // 中等奖池
OLDIE("old"), // 老人奖池
;
private String tag;
}

View File

@@ -0,0 +1,102 @@
package com.ruoyi.game.sugar.service.sugar.strategy;
import com.ruoyi.common.utils.RandomUtil;
import com.ruoyi.domain.entity.GameSugarUser;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import com.ruoyi.game.sugar.service.sugar.Strategy;
import com.ruoyi.game.sugar.service.sugar.StrategyContext;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.Arrays;
@Slf4j
public class DynamicRtpStrategy implements Strategy {
@Data
private static class RewardConfig {
private String name;
private double weight;
private double value;
public RewardConfig(String name, double weight, double value) {
this.name = name;
this.weight = weight;
this.value = value;
}
}
// 为了简单,每种奖的价值就是它的概率
private static final RewardConfig[] REWARD_CONFIGS = {
new RewardConfig("空奖", 56.76, 25), // score=[0,0]
new RewardConfig("小奖", 15.46, 25), // score=(0,0.5]
new RewardConfig("中奖", 27.11, 25), // score=(0.5,1.5.0]
new RewardConfig("大奖", 0.68, 25), // score=(1.5,+]
};
@Override
public void calcScore(StrategyContext context) {
GameSugarUser user = context.getSugarUser();
BigDecimal totalBet = user.getTotalBet();
BigDecimal totalWin = user.getTotalWin();
GameSugarConfig sugarConfig = context.getSysConfig();
// 还未下过注
if (totalBet.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
double rtp = totalWin.divide(totalBet, 4, RoundingMode.HALF_UP).doubleValue();
double targetRtp = sugarConfig.getRtp();
if (rtp > 0.5) {
return;
}
// 计算当前rtp与目标rtp的差值
// double error = rtp - targetRtp;
// log.info("DynamicRtpStrategy rtp={},targetRtp={},error={}", rtp, targetRtp, error);
//
// // 误差小于30%,不做处理
// if (Math.abs(error) < 0.3) {
// return;
// }
// if (error > 1) {
// error = 1;
// }
//
// double safe_error = Math.max(Math.min(error, 1.0), -1.0);
// // 调整的灵敏度系数
// double learning_factor = 0.01;
// // 如果error是正数则说明当前rtp高于目标rtp,要减少一些高价值符号的权重
// // 如果error是负数则说明当前rtp小于目标rtp,要增加一些高价值符号的权重
// double minWeight = 0.1;
RewardConfig[] rewardConfigs = Arrays.copyOf(REWARD_CONFIGS, REWARD_CONFIGS.length);
// for (RewardConfig config : rewardConfigs) {
// double adjustment_factor = -1 * safe_error * config.getWeight() * config.value * learning_factor;
// double newWeight = config.getWeight() + adjustment_factor;
// if (newWeight < minWeight) {
// newWeight = minWeight;
// }
// config.setWeight(newWeight);
// }
// RewardConfig config = RandomUtil.choices(rewardConfigs, Arrays.stream(rewardConfigs).mapToDouble(RewardConfig::getWeight).toArray(), 1).get(0);
// switch (config.getName()) {
// case "空奖":
// context.setMaxScore(0);
// break;
// case "小奖":
// context.setMaxScore(0.5);
// break;
// case "中奖":
// context.setMinScore(0.5);
// context.setMaxScore(2);
// break;
// case "大奖":
// context.setMinScore(1.5);
// break;
// }
}
}

View File

@@ -0,0 +1,43 @@
package com.ruoyi.game.sugar.service.sugar.strategy;
import com.ruoyi.common.utils.RandomUtil;
import com.ruoyi.domain.entity.GameSugarUser;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import com.ruoyi.game.sugar.service.sugar.Strategy;
import com.ruoyi.game.sugar.service.sugar.StrategyContext;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class JpStrategy implements Strategy {
@Override
public void calcScore(StrategyContext context) {
GameSugarUser user = context.getSugarUser();
BigDecimal totalBet = user.getTotalBet();
BigDecimal totalWin = user.getTotalWin();
GameSugarConfig sugarConfig = context.getSysConfig();
// 还未下过注
if (totalBet.compareTo(BigDecimal.ZERO) <= 0) {
return;
}
int r = RandomUtil.randInt(1, sugarConfig.getJpPercent()[1]);
if (r > sugarConfig.getJpPercent()[0]) {
return;
}
// 命中额外JP则限制MaxScore
// context.setJP(true);
// double rtp = totalWin.divide(totalBet, 4, RoundingMode.HALF_UP).doubleValue();
// if (rtp > context.getSysConfig().getRtp()) {
// if (context.getMaxScore() > 5 || context.getMaxScore() < 0) {
// context.setMaxScore(5);
// }
// } else {
// if (context.getMaxScore() > 10 || context.getMaxScore() < 0) {
// context.setMaxScore(10);
// }
// }
}
}

View File

@@ -0,0 +1,34 @@
package com.ruoyi.game.sugar.service.sugar.strategy;
import com.ruoyi.game.sugar.model.GameSugarConfig;
import com.ruoyi.game.sugar.service.sugar.Strategy;
import com.ruoyi.game.sugar.service.sugar.StrategyContext;
import java.math.BigDecimal;
import java.math.RoundingMode;
public class MaxScoreStrategy implements Strategy {
@Override
public void calcScore(StrategyContext context) {
var win = context.getRewardPool();
BigDecimal totalBet = win.getTotalBet();
BigDecimal totalWin = win.getTotalWin();
GameSugarConfig sugarConfig = context.getSysConfig();
double minProfit = sugarConfig.getMinProfit();
// 假设系统要保留30%的奖励minProfit=0.3, 那么可供抽奖的奖池就是total*0.7
// 返回的倍数*下注数一定小于可用奖池,防止奖池抽空
BigDecimal profitMargin = (totalBet.subtract(totalWin)).multiply(BigDecimal.valueOf(1 - minProfit));
BigDecimal maxScore = profitMargin.divide(context.getBet(), 2, RoundingMode.HALF_UP);
if (maxScore.compareTo(BigDecimal.ZERO) < 0) maxScore = BigDecimal.ZERO;
if (context.getControlParam().getMaxScore() > maxScore.doubleValue()
|| context.getControlParam().getMaxScore() < 0) {
context.getControlParam().setMaxScore(maxScore.doubleValue());
}
}
}