diff --git a/skins-service/game-olympus-service/pom.xml b/skins-service/game-olympus-service/pom.xml deleted file mode 100644 index 7d33b8b..0000000 --- a/skins-service/game-olympus-service/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - 4.0.0 - - com.ruoyi - skins-service - 4.8.2 - - - game-olympus-service - - - 8 - 8 - UTF-8 - - - - - com.ruoyi - ruoyi-framework - - - com.ruoyi - service-admin - 4.8.2 - - - com.ruoyi - service-user - 4.8.2 - - - com.ruoyi - ruoyi-common - 4.8.2 - - - diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/RecordPageRequest.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/RecordPageRequest.java deleted file mode 100644 index 339a081..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/RecordPageRequest.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.ruoyi.game.olympus.contract.request; - -import lombok.Data; - -/** 玫瑰女郎游戏记录分页请求(scope: 0全服/1我的,默认1我的;均分页) */ -@Data -public class RecordPageRequest { - - /** 记录范围:0=全服,1=我的(默认) */ - private Integer scope; - - /** 页码,默认 1 */ - private Integer pageNum; - - /** 每页条数,默认 10 */ - private Integer pageSize; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/SpinRequest.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/SpinRequest.java deleted file mode 100644 index 33a4ad7..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/request/SpinRequest.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ruoyi.game.olympus.contract.request; - -import lombok.Data; - -@Data -public class SpinRequest { - /** 非空=开始新游戏;null=继续当前游戏(下一步) */ - private Long boxId; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/BoxResponse.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/BoxResponse.java deleted file mode 100644 index 540fb7f..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/BoxResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.ruoyi.game.olympus.contract.response; - -import lombok.Data; - -import java.math.BigDecimal; - -@Data -public class BoxResponse { - private Long boxId; - private String boxName; - private BigDecimal basicBet; - private BigDecimal price; - private String boxType; - private String modeName; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/RecordItemResponse.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/RecordItemResponse.java deleted file mode 100644 index da70aa8..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/RecordItemResponse.java +++ /dev/null @@ -1,34 +0,0 @@ -package com.ruoyi.game.olympus.contract.response; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; - -/** 玫瑰女郎战绩列表项(我的记录/全服记录共用,只暴露客户端展示需要的字段) */ -@Data -public class RecordItemResponse { - - /** 玩家昵称 */ - private String nickName; - - /** 玩家头像 */ - private String avatar; - - /** 本局消费(下注金额) */ - private BigDecimal cost; - - /** 本局获得饰品的价格(= 结算金额) */ - private BigDecimal settledWin; - - /** 饰品图片 */ - private String prizeImageUrl; - - /** 饰品全名:名称(磨损品质),无品质时仅名称 */ - private String prizeName; - - /** 开局时间 */ - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") - private Date createTime; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/SpinResponse.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/SpinResponse.java deleted file mode 100644 index 8c020e0..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/contract/response/SpinResponse.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.ruoyi.game.olympus.contract.response; - -import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.Data; - -import java.math.BigDecimal; - -@Data -@JsonInclude(JsonInclude.Include.NON_NULL) -public class SpinResponse { - - private String state; // "PLAYING" | "FINISH" - - private Integer spinNum; // 免费旋转序号(0=基础旋转) - private Integer tumbleNum; // 本旋内翻滚序号 - private Integer globalStep; - private Integer globalTotalSteps; - private Integer spinsRemaining; - - private Object gridState; - private Object multiplierDetail; - private Object symbolLinks; // 本旋累积连线(每波追加,tumble 标明旋内第几波;面板由它汇总) - private Object dropData; - private Object fillData; - private Object scatterTrigger; - - private BigDecimal winAmount; // 本旋累计(该旋末帧 = 这一旋的总额) - private BigDecimal spinWin; // 本局累计(末帧 = totalWin) - private BigDecimal accumulator; // 本步后炸弹累计倍数(未 ×bet;FS 跨旋传递,重连恢复累计用) - - private Object retriggerInfo; - private String bigWinLevel; // 小/中/大:small | middle | big(仅免费旋转的旋末帧可能有值,其余 null) - - // ---- 仅 FINISH ---- - private BigDecimal totalWin; - private BigDecimal totalMul; - private Boolean maxWinReached; // 1000x 封顶标识 - private Object prizeInfo; - private Long boxRecordId; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusAdminController.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusAdminController.java deleted file mode 100644 index 42f067e..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusAdminController.java +++ /dev/null @@ -1,729 +0,0 @@ -package com.ruoyi.game.olympus.controller; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ruoyi.common.enums.BusinessType; -import com.ruoyi.common.annotation.Log; -import com.ruoyi.common.core.controller.BaseController; -import com.ruoyi.common.core.domain.R; -import com.ruoyi.game.olympus.domain.OlympusBox; -import com.ruoyi.game.olympus.domain.OlympusBoxOrnament; -import com.ruoyi.game.olympus.enums.OlympusAudience; -import com.ruoyi.game.olympus.enums.OlympusModeType; -import com.ruoyi.game.olympus.mapper.OlympusBoxMapper; -import com.ruoyi.game.olympus.mapper.OlympusBoxOrnamentMapper; -import com.ruoyi.game.olympus.mapper.OlympusRecordMapper; -import com.ruoyi.game.olympus.repository.OlympusRewardPoolCacheRepository; -import com.ruoyi.game.olympus.domain.OlympusOrnament; -import com.ruoyi.game.olympus.domain.OlympusPrizeOrnament; -import com.ruoyi.game.olympus.domain.OlympusPrizeRange; -import com.ruoyi.game.olympus.domain.OlympusRound; -import com.ruoyi.game.olympus.mapper.OlympusOrnamentMapper; -import com.ruoyi.game.olympus.mapper.OlympusPrizeOrnamentMapper; -import com.ruoyi.game.olympus.mapper.OlympusPrizeRangeMapper; -import com.ruoyi.game.olympus.mapper.OlympusRoundMapper; -import com.ruoyi.game.olympus.domain.OlympusBoxTemplate; -import com.ruoyi.game.olympus.mapper.OlympusBoxTemplateMapper; -import com.ruoyi.game.olympus.repository.OlympusCycleRepository; -import com.ruoyi.game.olympus.service.OlympusDistributionService; -import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; -import com.ruoyi.admin.service.TtUserService; -import com.ruoyi.domain.entity.sys.TtUser; -import com.ruoyi.domain.other.TtOrnamentsLevel; -import com.ruoyi.game.olympus.domain.OlympusRewardLog; -import com.ruoyi.game.olympus.mapper.OlympusRewardLogMapper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.bind.annotation.*; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.*; -import java.util.stream.Collectors; - -/** 奥林匹斯管理端:箱子配置 / 桶分布 / 水位(按箱注水抽水)。 */ -@RestController -@RequestMapping("/admin/olympus") -public class OlympusAdminController extends BaseController { - - @Autowired private OlympusBoxMapper boxMapper; - @Autowired private OlympusBoxOrnamentMapper boxOrnamentMapper; - @Autowired private OlympusRecordMapper recordMapper; - @Autowired private OlympusRewardPoolCacheRepository rewardPoolRepository; - @Autowired private OlympusOrnamentMapper ornamentMapper; - @Autowired private OlympusRoundMapper roundMapper; - @Autowired private OlympusPrizeOrnamentMapper prizeOrnamentMapper; - @Autowired private OlympusPrizeRangeMapper prizeRangeMapper; - @Autowired private OlympusBoxTemplateMapper boxTemplateMapper; - @Autowired private OlympusCycleRepository cycleRepository; - @Autowired private OlympusDistributionService distributionService; - @Autowired(required = false) private TtOrnamentsLevelMapper ornamentsLevelMapper; - @Autowired private TtUserService userService; - @Autowired private OlympusRewardLogMapper rewardLogMapper; - - @GetMapping("/box/list") - public R boxList(@RequestParam(defaultValue = "1") int pageNum, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(required = false) String boxType) { - LambdaQueryWrapper qw = new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS"); - if (boxType != null && !boxType.isEmpty()) qw.eq(OlympusBox::getBoxType, boxType); - qw.orderByAsc(OlympusBox::getBoxId); - Page page = boxMapper.selectPage(new Page<>(pageNum, pageSize), qw); - - // 各身份分布一次全量查出(30箱×4身份量小),内存分组,避免逐身份 N+1 - // 桶期望按"这个箱子的池子实际会赔多少"取(每个箱子查一次,缓存复用) - Map> expectByBox = new HashMap<>(); - Map> distByKey = new HashMap<>(); - for (OlympusBoxOrnament bo : boxOrnamentMapper.selectList(null)) { - if (bo.getBoxId() == null || bo.getAudience() == null) continue; - distByKey.computeIfAbsent(bo.getBoxId() + ":" + bo.getAudience(), k -> new ArrayList<>()).add(bo); - } - - for (OlympusBox box : page.getRecords()) { - try { - OlympusModeType m = OlympusModeType.fromBoxType(box.getBoxType()); - double cm = m.getCostMultiplier(); - Map ornMul = expectByBox.computeIfAbsent(box.getBoxId(), - k -> distributionService.loadPoolExpect(k)); - box.setModeName(m.getName()); - box.setCostMultiplier(BigDecimal.valueOf(cm)); - Map targetMap = new HashMap<>(); - Map actualMap = new HashMap<>(); - Map totalMap = new HashMap<>(); - for (OlympusAudience aud : OlympusAudience.values()) { - // 目标RTP:从该身份分布反推 = Σ(count×expect_mul) / Σ(count) / CM × 100 - List list = distByKey.get(box.getBoxId() + ":" + aud.getCode()); - if (list != null && !list.isEmpty()) { - double sumMul = 0; int totalCount = 0; - for (OlympusBoxOrnament bo : list) { - int c = bo.getCount() != null ? bo.getCount() : 0; - if (c <= 0) continue; - sumMul += c * ornMul.getOrDefault(bo.getOrnamentId(), 0.0); - totalCount += c; - } - if (totalCount > 0) { - targetMap.put(aud.getCode(), BigDecimal.valueOf(sumMul / totalCount / cm * 100) - .setScale(2, RoundingMode.HALF_UP)); - // 该身份一轮总回合 = 固定池 count 和反推(身份级独立) - double cr = box.getRtpConvergeRatio() != null ? box.getRtpConvergeRatio().doubleValue() : 0.15; - totalMap.put(aud.getCode(), BigDecimal.valueOf( - OlympusDistributionService.deriveTotalRounds(totalCount, cr))); - } - } - // 实时RTP:按 (boxId, audience) 循环账本各查一次 - BigDecimal r = cycleRepository.realtimeRtp(box.getBoxId(), aud.getCode(), cm); - if (r != null) actualMap.put(aud.getCode(), r); - } - box.setTargetRtpByAudience(targetMap); - box.setActualRtpByAudience(actualMap); - box.setTotalRoundsByAudience(totalMap); - box.setActualRtp(actualMap.get("common")); // 兼容保留 common 口径 - } catch (Exception ignore) {} - } - return R.ok(Map.of("rows", page.getRecords(), "total", page.getTotal())); - } - - @Log(title = "箱子配置", action = "编辑", businessType = BusinessType.UPDATE) - @PostMapping("/box/save") - @Transactional(rollbackFor = Exception.class) - public R boxSave(@RequestBody OlympusBox box) { - if (box.getBet() == null || box.getBoxType() == null) return R.fail("下注与模式不能为空"); - try { - OlympusModeType m = OlympusModeType.fromBoxType(box.getBoxType()); - box.setGameType("OLYMPUS"); - box.setPrice(box.getBet().multiply(BigDecimal.valueOf(m.getCostMultiplier()))); - } catch (IllegalArgumentException e) { - return R.fail("无效模式: " + box.getBoxType()); - } - if (box.getBoxId() != null && boxMapper.selectById(box.getBoxId()) != null) boxMapper.updateById(box); - else boxMapper.insert(box); - return R.ok(Map.of("boxId", box.getBoxId())); - } - - /** 某箱某身份的桶分布:含 convergeCount(收敛账本) + fixedRounds/convergeRounds(按该身份分布反推) */ - @GetMapping("/box/ornaments/{boxId}") - public R boxOrnaments(@PathVariable Long boxId, - @RequestParam(required = false, defaultValue = "common") String audience) { - List list = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); - for (OlympusBoxOrnament bo : list) { - bo.setConvergeCount(cycleRepository.convergeCount(boxId, audience, bo.getOrnamentId())); - } - OlympusBox box = boxMapper.selectById(boxId); - // 固定/收敛回合按该身份分布 count 之和反推(身份级独立),不使用箱子级 total_ornaments - int fixedSum = list.stream().mapToInt(bo -> bo.getCount() != null ? bo.getCount() : 0).sum(); - double cr = (box != null && box.getRtpConvergeRatio() != null) ? box.getRtpConvergeRatio().doubleValue() : 0.15; - int fixedRounds = fixedSum; - int convergeRounds = fixedSum > 0 ? Math.max(0, OlympusDistributionService.deriveTotalRounds(fixedSum, cr) - fixedSum) : 0; - // poolExpect:该箱每个桶"池子里实际会赔多少"的均值(= 列表页「目标RTP」用的同一口径)。 - // 前端「计算RTP」必须拿它算,否则会拿桶定义的名义 expect_mul(区间中点)算出跟目标RTP不一致的数。 - return R.ok(Map.of("list", list, "audience", audience, "fixedRounds", fixedRounds, - "convergeRounds", convergeRounds, "poolExpect", distributionService.loadPoolExpect(boxId))); - } - - @Log(title = "箱子配置", action = "保存", businessType = BusinessType.UPDATE) - /** 保存某箱某身份的饰品回合分布(items 为固定池回合,反推回写总回合) */ - @PostMapping("/box/ornaments/save") - @Transactional(rollbackFor = Exception.class) - public R boxOrnamentsSave(@RequestBody Map params) { - Long boxId = asLong(params.get("boxId")); - if (boxId == null) return R.fail("boxId 不能为空"); - String audience = params.get("audience") != null ? params.get("audience").toString() : "common"; - @SuppressWarnings("unchecked") - List> items = (List>) params.get("items"); - distributionService.writeDistribution(boxId, audience, items); - cycleRepository.resetCycle(boxId, audience); - return R.ok(); - } - - /** 按目标 RTP 重算某箱某身份分布(只写 box_ornament) */ - @PostMapping("/box/ornaments/calc") - public R calcDistribution(@RequestBody Map params) { - Long boxId = asLong(params.get("boxId")); - String audience = params.get("audience") != null ? params.get("audience").toString() : "common"; - Integer totalRounds = asInt(params.get("totalRounds")); - Double rtp = params.get("rtp") != null ? Double.parseDouble(params.get("rtp").toString()) : 0.8; - Double convergeRatio = params.get("convergeRatio") != null ? Double.parseDouble(params.get("convergeRatio").toString()) : null; - OlympusBox box = boxMapper.selectById(boxId); - if (box == null) return R.fail("箱子不存在"); - OlympusModeType mode = OlympusModeType.fromBoxType(box.getBoxType()); - double cr = convergeRatio != null ? convergeRatio - : (box.getRtpConvergeRatio() != null ? box.getRtpConvergeRatio().doubleValue() : 0.15); - int fixedRoundsN = OlympusDistributionService.deriveFixedRounds(totalRounds != null ? totalRounds : 200, cr); - int canonicalN = OlympusDistributionService.deriveTotalRounds(fixedRoundsN, cr); - box.setTotalOrnaments(canonicalN); - box.setRtp(BigDecimal.valueOf(rtp)); - if (convergeRatio != null) box.setRtpConvergeRatio(BigDecimal.valueOf(convergeRatio)); - boxMapper.updateById(box); - distributionService.reset(boxId, audience); - cycleRepository.resetCycle(boxId, audience); - List saved = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); - Map ornMul = distributionService.loadPoolExpect(boxId); - List> result = new ArrayList<>(); - int total = 0; double contrib = 0; - for (OlympusBoxOrnament bo : saved) { - int c = bo.getCount() != null ? bo.getCount() : 0; - result.add(Map.of("ornamentId", bo.getOrnamentId(), "count", c)); - total += c; - contrib += c * ornMul.getOrDefault(bo.getOrnamentId(), 0.0); - } - double cm = mode.getCostMultiplier(); - double calcRtpValue = total > 0 ? contrib / total / cm * 100 : 0; - return R.ok(Map.of("items", result, "totalRounds", total, "boxTotal", canonicalN, - "convergeRounds", Math.max(0, canonicalN - total), - "calcRtp", String.format("%.1f%%", calcRtpValue))); - } - - /** 模拟 payoutCap 约束下的预估实际 RTP */ - @PostMapping("/box/ornaments/simulate-rtp") - public R simulateRtp(@RequestBody Map params) { - @SuppressWarnings("unchecked") - List> items = (List>) params.get("items"); - Long boxId = asLong(params.get("boxId")); - double rtp = params.get("rtp") != null ? Double.parseDouble(params.get("rtp").toString()) : 0.8; - if (items == null || items.isEmpty()) return R.ok(Map.of("estimatedRtp", "0.0%")); - Map dist = new LinkedHashMap<>(); - for (Map item : items) { - Integer ornamentId = asInt(item.get("ornamentId")); - Integer count = asInt(item.get("count")); - if (ornamentId != null && count != null && count > 0) dist.put(ornamentId, count); - } - double est = distributionService.simulateActualRtp(dist, boxId, rtp, 10); - return R.ok(Map.of("estimatedRtp", String.format("%.1f%%", est))); - } - - // ==================== 分布模板(按身份) ==================== - - @GetMapping("/box/templates/{boxId}") - public R templateList(@PathVariable Long boxId, - @RequestParam(required = false, defaultValue = "common") String audience) { - List list = boxTemplateMapper.selectList( - new LambdaQueryWrapper() - .eq(OlympusBoxTemplate::getBoxId, boxId) - .eq(OlympusBoxTemplate::getAudience, audience) - .orderByDesc(OlympusBoxTemplate::getId)); - for (OlympusBoxTemplate t : list) { - t.setItems(distributionService.parseItems(t.getItemsJson())); - t.setItemsJson(null); - } - return R.ok(Map.of("list", list)); - } - - @Log(title = "箱子配置", action = "保存为模板", businessType = BusinessType.INSERT) - @PostMapping("/box/template/save") - @Transactional(rollbackFor = Exception.class) - public R templateSave(@RequestBody Map params) { - Long boxId = asLong(params.get("boxId")); - if (boxId == null) return R.fail("boxId 不能为空"); - String audience = params.get("audience") != null ? params.get("audience").toString() : "common"; - String name = params.get("name") != null ? params.get("name").toString().trim() : ""; - if (name.isEmpty()) return R.fail("模板名称不能为空"); - @SuppressWarnings("unchecked") - List> items = (List>) params.get("items"); - if (items == null || items.isEmpty()) return R.fail("模板饰品分布不能为空"); - Long dup = boxTemplateMapper.selectCount(new LambdaQueryWrapper() - .eq(OlympusBoxTemplate::getBoxId, boxId).eq(OlympusBoxTemplate::getAudience, audience) - .eq(OlympusBoxTemplate::getName, name)); - if (dup != null && dup > 0) return R.fail("模板名称已存在,请使用不同的名称"); - int total = 0; - for (Map item : items) { Integer c = asInt(item.get("count")); if (c != null && c > 0) total += c; } - OlympusBoxTemplate tpl = new OlympusBoxTemplate(); - tpl.setBoxId(boxId); - tpl.setAudience(audience); - tpl.setName(name); - tpl.setTotalRounds(total); - tpl.setItemsJson(distributionService.toItemsJson(items)); - tpl.setIsActive(0); - boxTemplateMapper.insert(tpl); - return R.ok(Map.of("id", tpl.getId())); - } - - @Log(title = "箱子配置", action = "删除", businessType = BusinessType.DELETE) - @DeleteMapping("/box/template/{id}") - public R templateDelete(@PathVariable Long id) { - boxTemplateMapper.deleteById(id); - return R.ok(); - } - - @Log(title = "箱子配置", action = "应用选中模板", businessType = BusinessType.UPDATE) - @PostMapping("/box/template/apply") - @Transactional(rollbackFor = Exception.class) - public R templateApply(@RequestBody Map params) { - Long templateId = asLong(params.get("templateId")); - if (templateId == null) return R.fail("templateId 不能为空"); - OlympusBoxTemplate tpl = boxTemplateMapper.selectById(templateId); - if (tpl == null) return R.fail("模板不存在"); - distributionService.writeDistribution(tpl.getBoxId(), tpl.getAudience(), distributionService.parseItems(tpl.getItemsJson())); - cycleRepository.resetCycle(tpl.getBoxId(), tpl.getAudience()); - List siblings = boxTemplateMapper.selectList(new LambdaQueryWrapper() - .eq(OlympusBoxTemplate::getBoxId, tpl.getBoxId()).eq(OlympusBoxTemplate::getAudience, tpl.getAudience())); - for (OlympusBoxTemplate s : siblings) { - int active = s.getId().equals(templateId) ? 1 : 0; - if (!Integer.valueOf(active).equals(s.getIsActive())) { s.setIsActive(active); boxTemplateMapper.updateById(s); } - } - return R.ok(); - } - - /** 一键重算所有箱子 × 所有身份分布 */ - @PostMapping("/box/recalc_all") - public R recalcAll() { - List boxes = boxMapper.selectList( - new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); - int done = 0; - for (OlympusBox box : boxes) { - for (OlympusAudience aud : OlympusAudience.values()) { - try { - distributionService.reset(box.getBoxId(), aud.getCode()); - cycleRepository.resetCycle(box.getBoxId(), aud.getCode()); - done++; - } catch (Exception ignore) {} - } - } - return R.ok(Map.of("recalculated", done)); - } - - @Log(title = "箱子配置", action = "一键重算分布", businessType = BusinessType.CLEAN) - /** - * 重算分布(弹窗):按统一参数批量重算。 - * 入参:boxType 可选(空=全部模式);audience 可选(空=全部身份);rtp/totalRounds/convergeRatio 非空则统一设置到箱,再按最新配置重算分布。 - */ - @PostMapping("/box/recalcBatch") - @Transactional(rollbackFor = Exception.class) - public R recalcBatch(@RequestBody Map params) { - String boxType = params.get("boxType") != null && !params.get("boxType").toString().isEmpty() - ? params.get("boxType").toString() : null; - String audience = params.get("audience") != null && !params.get("audience").toString().isEmpty() - ? params.get("audience").toString() : null; - Double rtp = params.get("rtp") != null ? Double.parseDouble(params.get("rtp").toString()) : null; - Integer totalRounds = asInt(params.get("totalRounds")); - Double convergeRatio = params.get("convergeRatio") != null - ? Double.parseDouble(params.get("convergeRatio").toString()) : null; - - LambdaQueryWrapper qw = new LambdaQueryWrapper() - .eq(OlympusBox::getGameType, "OLYMPUS"); - if (boxType != null) qw.eq(OlympusBox::getBoxType, boxType); - List boxes = boxMapper.selectList(qw); - if (boxes.isEmpty()) return R.fail("没有匹配的箱子"); - - List audiences = audience != null ? Collections.singletonList(audience) - : Arrays.stream(OlympusAudience.values()).map(OlympusAudience::getCode).collect(Collectors.toList()); - - int done = 0; - for (OlympusBox box : boxes) { - boolean changed = false; - if (rtp != null) { box.setRtp(BigDecimal.valueOf(rtp)); changed = true; } - if (totalRounds != null && totalRounds > 0) { box.setTotalOrnaments(totalRounds); changed = true; } - if (convergeRatio != null) { box.setRtpConvergeRatio(BigDecimal.valueOf(convergeRatio)); changed = true; } - if (changed) boxMapper.updateById(box); - for (String aud : audiences) { - try { - distributionService.reset(box.getBoxId(), aud); - cycleRepository.resetCycle(box.getBoxId(), aud); - done++; - } catch (Exception ignore) {} - } - } - return R.ok(Map.of("recalculated", done, "boxes", boxes.size())); - } - - /** 玩家记录列表:可按玩家ID + 身份筛选;行内回填 audience(默认全部身份,含白号/机器人) */ - @GetMapping("/record/list") - public R recordList(@RequestParam(defaultValue = "1") int pageNum, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(required = false) Long userId, - @RequestParam(required = false) String audience) { - LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); - // 空 = 全部身份(与奖池页"未选身份默认普通"不同:这里空是不过滤) - if (audience != null && !audience.trim().isEmpty() && !POOL_AUDIENCES.contains(audience.trim())) { - return R.fail("身份无效"); - } - qw.inSql(com.ruoyi.game.olympus.domain.OlympusRecord::getUserId, audienceUserSubQuery(audience)); - if (userId != null) qw.eq(com.ruoyi.game.olympus.domain.OlympusRecord::getUserId, userId); - qw.orderByDesc(com.ruoyi.game.olympus.domain.OlympusRecord::getCreateTime); - Page page = recordMapper.selectPage(new Page<>(pageNum, pageSize), qw); - fillAudience(page.getRecords()); - return R.ok(Map.of("rows", page.getRecords(), "total", page.getTotal())); - } - - /** - * 身份 → tt_user 子查询。奖池 4 池口径(common/anchor/robot/white),另附封禁/逻辑删除兜底: - * 封禁(status=1)和已删除用户任何身份都不展示;身份为空=全部身份(不再无条件排除白号/机器人)。 - */ - private static String audienceUserSubQuery(String audience) { - String base = "SELECT user_id FROM tt_user WHERE status != '1' AND del_flag = '0' AND "; - if ("white".equals(audience)) return base + "is_blank_account = 1"; - if ("anchor".equals(audience)) return base + "COALESCE(is_blank_account,0) != 1 AND user_type = '01'"; - if ("robot".equals(audience)) return base + "COALESCE(is_blank_account,0) != 1 AND user_type = '03'"; - if ("common".equals(audience)) return base + "COALESCE(is_blank_account,0) != 1 AND (user_type IS NULL OR user_type NOT IN ('01','03'))"; - return base + "1 = 1"; - } - - /** 批量回填每行身份:复用 OlympusAudience.resolve,勿另写一套判定 */ - private void fillAudience(List rows) { - if (rows == null || rows.isEmpty()) return; - Set ids = rows.stream().map(com.ruoyi.game.olympus.domain.OlympusRecord::getUserId) - .filter(Objects::nonNull).collect(Collectors.toSet()); - if (ids.isEmpty()) return; - Map audMap = new HashMap<>(); - for (TtUser u : userService.listByIds(ids)) { - if (u.getUserId() != null) audMap.put(u.getUserId().longValue(), OlympusAudience.resolve(u).getCode()); - } - for (com.ruoyi.game.olympus.domain.OlympusRecord r : rows) { - r.setAudience(audMap.getOrDefault(r.getUserId(), "common")); - } - } - - @GetMapping("/win/summary") - public R winSummary(@RequestParam(required = false) String dateStart, - @RequestParam(required = false) String dateEnd) { - Map row = recordMapper.winSummary(dateStart, dateEnd); - return R.ok(row); - } - - // ==================== 饰品桶定义(饰品列表) ==================== - - @GetMapping("/ornament/list") - public R ornamentList() { - List list = ornamentMapper.selectList( - new LambdaQueryWrapper().orderByAsc(OlympusOrnament::getId)); - return R.ok(Map.of("list", list)); - } - - // ==================== 预生成回合(回合管理) ==================== - - @GetMapping("/round/list") - public R roundList(@RequestParam(defaultValue = "1") int pageNum, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(required = false) Long roundId, - @RequestParam(required = false) String boxType) { - LambdaQueryWrapper qw = new LambdaQueryWrapper<>(); - if (roundId != null) qw.eq(OlympusRound::getRoundId, roundId); - // 收集箱子 id→模式,供筛选与回填 boxType - List boxes = boxMapper.selectList( - new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); - Map boxTypeMap = new HashMap<>(); - List filterBoxIds = new ArrayList<>(); - for (OlympusBox b : boxes) { - boxTypeMap.put(b.getBoxId(), b.getBoxType()); - if (boxType != null && !boxType.isEmpty() && boxType.equals(b.getBoxType())) filterBoxIds.add(b.getBoxId()); - } - if (boxType != null && !boxType.isEmpty()) { - if (filterBoxIds.isEmpty()) return R.ok(Map.of("rows", Collections.emptyList(), "total", 0)); - qw.in(OlympusRound::getBoxId, filterBoxIds); - } - qw.orderByAsc(OlympusRound::getRoundId); - Page page = roundMapper.selectPage(new Page<>(pageNum, pageSize), qw); - for (OlympusRound r : page.getRecords()) r.setBoxType(boxTypeMap.get(r.getBoxId())); - return R.ok(Map.of("rows", page.getRecords(), "total", page.getTotal())); - } - - // ==================== 每日统计 ==================== - - /** 每日统计列表:按天聚合 game_olympus_record(与 winSummary 同源同口径,支持日期筛选) */ - @GetMapping("/win/list") - public R winList(@RequestParam(defaultValue = "1") int pageNum, - @RequestParam(defaultValue = "10") int pageSize, - @RequestParam(required = false) String dateStart, - @RequestParam(required = false) String dateEnd) { - List> all = recordMapper.dailyList(dateStart, dateEnd); - int from = Math.max(0, (pageNum - 1) * pageSize); - if (from >= all.size()) return R.ok(Map.of("rows", Collections.emptyList(), "total", all.size())); - return R.ok(Map.of("rows", all.subList(from, Math.min(all.size(), from + pageSize)), "total", all.size())); - } - - // ==================== 返奖饰品(奖品管理) ==================== - - @GetMapping("/prize/list") - public R prizeList() { - List list = prizeOrnamentMapper.selectList( - new LambdaQueryWrapper().orderByAsc(OlympusPrizeOrnament::getSort)); - List ranges = prizeRangeMapper.selectList(new LambdaQueryWrapper<>()); - Map rangeMap = new HashMap<>(); - for (OlympusPrizeRange r : ranges) rangeMap.put(r.getPrizeId(), r); - for (OlympusPrizeOrnament o : list) { - OlympusPrizeRange r = rangeMap.get(o.getId()); - if (r != null) { o.setWinLow(r.getWinLow()); o.setWinHigh(r.getWinHigh()); } - if (o.getGrade() != null && ornamentsLevelMapper != null) { - TtOrnamentsLevel lv = ornamentsLevelMapper.selectById(o.getGrade()); - if (lv != null) o.setLevelImg(lv.getLevelImg()); - } - } - return R.ok(Map.of("list", list)); - } - - @Log(title = "奖品管理", action = "编辑", businessType = BusinessType.UPDATE) - @PostMapping("/prize/save") - @Transactional(rollbackFor = Exception.class) - public R prizeSave(@RequestBody OlympusPrizeOrnament prize) { - if (prize.getName() == null || prize.getName().trim().isEmpty()) return R.fail("饰品名称不能为空"); - prize.setGameType("OLYMPUS"); - if (prize.getStatus() == null) prize.setStatus("1"); - if (prize.getId() != null && prizeOrnamentMapper.selectById(prize.getId()) != null) prizeOrnamentMapper.updateById(prize); - else prizeOrnamentMapper.insert(prize); - // 同步区间(按 prizeId 唯一) - if (prize.getWinLow() != null && prize.getWinHigh() != null) { - OlympusPrizeRange r = prizeRangeMapper.selectOne( - new LambdaQueryWrapper().eq(OlympusPrizeRange::getPrizeId, prize.getId()).last("LIMIT 1")); - if (r == null) { r = new OlympusPrizeRange(); r.setPrizeId(prize.getId()); } - r.setWinLow(prize.getWinLow()); - r.setWinHigh(prize.getWinHigh()); - if (r.getId() != null) prizeRangeMapper.updateById(r); else prizeRangeMapper.insert(r); - } - return R.ok(Map.of("id", prize.getId())); - } - - // ==================== 水位(按【箱 × 身份】注水/抽水) ==================== - - /** 奖池身份维度,与 OlympusAudience 对齐 */ - private static final List POOL_AUDIENCES = Arrays.asList("common", "anchor", "robot", "white"); - - /** 身份入参归一化:空=common;非法返回 null(调用方报错,不静默兜底) */ - private static String audienceOf(Object v) { - if (v == null || v.toString().trim().isEmpty()) return "common"; - String s = v.toString().trim(); - return POOL_AUDIENCES.contains(s) ? s : null; - } - - /** 奖池配置快照:某身份各箱水位/RTP/本轮进度/收敛占比 + 一轮花费基数 */ - @GetMapping("/rewardPool/config") - public R rewardPoolConfig(@RequestParam(required = false) String audience) { - String aud = audienceOf(audience); - if (aud == null) return R.fail("身份无效"); - return R.ok(poolConfigMap(aud)); - } - - /** 组装奖池配置(total + boxes),供 config 与 adjust/batch/reset 复用 */ - private Map poolConfigMap(String audience) { - List boxes = boxMapper.selectList( - new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS").orderByAsc(OlympusBox::getBoxId)); - List> boxList = new ArrayList<>(); - BigDecimal totalBet = BigDecimal.ZERO, totalWin = BigDecimal.ZERO, totalBalance = BigDecimal.ZERO; - for (OlympusBox box : boxes) { - Long boxId = box.getBoxId(); - com.ruoyi.game.olympus.model.cache.OlympusRewardPoolCache c = rewardPoolRepository.getBoxCache(boxId, audience); - BigDecimal balance = rewardPoolRepository.getBoxBalance(boxId, audience); - double cm = 1.0; - try { cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); } catch (Exception ignore) {} - int rounds = box.getTotalOrnaments() != null ? box.getTotalOrnaments() : 0; - BigDecimal costPerRound = coalesce(box.getBet()).multiply(BigDecimal.valueOf(cm)); - BigDecimal cycleCost = costPerRound.multiply(BigDecimal.valueOf(rounds)); - long drawn = cycleRepository.drawn(boxId, audience); - Double cycleRtp = null; - if (drawn > 0 && cm > 0) cycleRtp = cycleRepository.mulSum(boxId, audience) / drawn / cm; - Map m = new LinkedHashMap<>(); - m.put("boxId", boxId); - m.put("name", box.getName()); - m.put("audience", audience); - m.put("boxType", box.getBoxType()); - m.put("bet", box.getBet()); - m.put("totalBet", c.getTotalBet()); - m.put("totalWin", c.getTotalWin()); - m.put("adjust", c.getAdjust()); - m.put("balance", balance); - m.put("costPerRound", costPerRound.setScale(2, RoundingMode.HALF_UP)); - m.put("cycleRounds", rounds); - m.put("cycleCost", cycleCost.setScale(2, RoundingMode.HALF_UP)); - m.put("totalRounds", rounds); - m.put("convergeRatio", box.getRtpConvergeRatio()); - m.put("cycleDrawn", drawn); - m.put("cycleRtp", cycleRtp); - m.put("rtp", c.getTotalBet().signum() > 0 - ? c.getTotalWin().divide(c.getTotalBet(), 4, RoundingMode.HALF_UP) : null); - boxList.add(m); - totalBet = totalBet.add(c.getTotalBet()); - totalWin = totalWin.add(c.getTotalWin()); - totalBalance = totalBalance.add(balance); - } - Map total = new LinkedHashMap<>(); - total.put("audience", audience); - total.put("totalBet", totalBet); - total.put("totalWin", totalWin); - total.put("adjust", rewardPoolRepository.getGlobalAdjust(audience)); - total.put("balance", totalBalance); - total.put("rtp", totalBet.signum() > 0 ? totalWin.divide(totalBet, 4, RoundingMode.HALF_UP) : null); - Map result = new LinkedHashMap<>(); - result.put("audience", audience); - result.put("total", total); - result.put("boxes", boxList); - return result; - } - - @Log(title = "奖池水位", action = "注水/抽水", businessType = BusinessType.GRANT) - /** 逐箱注水/抽水:type=1注水/2抽水,记审计日志 */ - @PostMapping("/rewardPool/adjust") - public R rewardPoolAdjust(@RequestBody Map body) { - String audience = audienceOf(body.get("audience")); - if (audience == null) return R.fail("身份无效"); - Long boxId = asLong(body.get("boxId")); - Integer type = asInt(body.get("type")); - BigDecimal amount = asBd(body.get("amount")); - String remark = body.get("remark") != null ? body.get("remark").toString() : null; - OlympusBox box = boxId != null ? boxMapper.selectById(boxId) : null; - if (box == null) return R.fail("箱子不存在"); - if (type == null || (type != 1 && type != 2)) return R.fail("调整类型无效"); - if (amount.signum() <= 0) return R.fail("金额必须大于 0"); - BigDecimal before = rewardPoolRepository.getBoxBalance(boxId, audience); - rewardPoolRepository.adjustBox(boxId, audience, type == 2 ? amount.negate() : amount); - BigDecimal after = rewardPoolRepository.getBoxBalance(boxId, audience); - writeLog(boxId, box.getName(), audience, type, amount, before, after, remark); - return R.ok(poolConfigMap(audience)); - } - - @Log(title = "奖池水位", action = "批量注水/抽水", businessType = BusinessType.GRANT) - /** 批量按一轮花费比例注水/抽水:ratio 百分比(0~1000],记一条汇总日志 */ - @PostMapping("/rewardPool/adjustBatch") - public R rewardPoolAdjustBatch(@RequestBody Map body) { - String audience = audienceOf(body.get("audience")); - if (audience == null) return R.fail("身份无效"); - Integer type = asInt(body.get("type")); - BigDecimal ratio = asBd(body.get("ratio")); - String remark = body.get("remark") != null ? body.get("remark").toString() : null; - if (type == null || (type != 1 && type != 2)) return R.fail("调整类型无效"); - if (ratio.signum() <= 0 || ratio.compareTo(new BigDecimal("1000")) > 0) return R.fail("比例须在 0~1000"); - List boxes = boxMapper.selectList( - new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); - int adjusted = 0; - BigDecimal totalAmount = BigDecimal.ZERO; - for (OlympusBox box : boxes) { - double cm = 1.0; - try { cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); } catch (Exception ignore) {} - BigDecimal base = coalesce(box.getBet()).multiply(BigDecimal.valueOf(cm)) - .multiply(BigDecimal.valueOf(box.getTotalOrnaments() != null ? box.getTotalOrnaments() : 0)); - if (base.signum() <= 0) continue; - BigDecimal amount = base.multiply(ratio).divide(new BigDecimal("100"), 2, RoundingMode.HALF_UP); - if (amount.signum() <= 0) continue; - rewardPoolRepository.adjustBox(box.getBoxId(), audience, type == 2 ? amount.negate() : amount); - totalAmount = totalAmount.add(amount); - adjusted++; - } - writeLog(null, "批量(" + adjusted + "箱 " + ratio.stripTrailingZeros().toPlainString() + "%)", - audience, type, totalAmount, null, null, remark); - Map r = poolConfigMap(audience); - r.put("adjusted", adjusted); - r.put("totalAmount", totalAmount); - return R.ok(r); - } - - @Log(title = "奖池水位", action = "重置调整", businessType = BusinessType.CLEAN) - /** 重置人工调整:该身份所有箱子 adjust 归零,仅保留真实水位;记一条汇总日志 */ - @PostMapping("/rewardPool/adjustReset") - public R rewardPoolAdjustReset(@RequestBody(required = false) Map body) { - String audience = audienceOf(body != null ? body.get("audience") : null); - if (audience == null) return R.fail("身份无效"); - String remark = body != null && body.get("remark") != null ? body.get("remark").toString() : null; - List boxes = boxMapper.selectList( - new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); - int cleared = 0; - BigDecimal totalCleared = BigDecimal.ZERO; - for (OlympusBox box : boxes) { - BigDecimal adjust = rewardPoolRepository.getBoxAdjust(box.getBoxId(), audience); - if (adjust.signum() == 0) continue; - rewardPoolRepository.adjustBox(box.getBoxId(), audience, adjust.negate()); - totalCleared = totalCleared.add(adjust.abs()); - cleared++; - } - BigDecimal residual = rewardPoolRepository.getGlobalAdjust(audience); - if (residual.signum() != 0) rewardPoolRepository.adjustGlobal(audience, residual.negate()); - writeLog(null, "全部(" + cleared + "箱)", audience, 3, totalCleared, null, null, remark); - Map r = poolConfigMap(audience); - r.put("cleared", cleared); - r.put("totalCleared", totalCleared); - return R.ok(r); - } - - /** 奖池调整历史(分页,可按身份筛选) */ - @GetMapping("/rewardPool/adjustLog") - public R rewardPoolAdjustLog(@RequestParam(defaultValue = "1") int page, - @RequestParam(defaultValue = "10") int size, - @RequestParam(required = false) String audience) { - String aud = audienceOf(audience); - if (aud == null) return R.fail("身份无效"); - Page p = rewardLogMapper.selectPage(new Page<>(page, size), - new LambdaQueryWrapper() - .eq(OlympusRewardLog::getAudience, aud) - .orderByDesc(OlympusRewardLog::getId)); - return R.ok(Map.of("rows", p.getRecords(), "total", p.getTotal())); - } - - private void writeLog(Long boxId, String boxName, String audience, Integer type, BigDecimal amount, - BigDecimal before, BigDecimal after, String remark) { - try { - OlympusRewardLog rlog = new OlympusRewardLog(); - rlog.setBoxId(boxId); - rlog.setBoxName(boxName); - rlog.setAudience(audience); - rlog.setType(type); - rlog.setAmount(amount); - rlog.setBalanceBefore(before); - rlog.setBalanceAfter(after); - rlog.setPoolBalance(totalPoolBalance(audience)); - rlog.setOperator(getUsername()); - rlog.setRemark(remark); - rlog.setCreateTime(new java.util.Date()); - rewardLogMapper.insert(rlog); - } catch (Exception ignore) {} - } - - /** 该身份全站总池余额 = 各箱水位之和(写日志时取操作后快照) */ - private BigDecimal totalPoolBalance(String audience) { - List list = boxMapper.selectList( - new LambdaQueryWrapper().eq(OlympusBox::getGameType, "OLYMPUS")); - BigDecimal sum = BigDecimal.ZERO; - for (OlympusBox b : list) sum = sum.add(rewardPoolRepository.getBoxBalance(b.getBoxId(), audience)); - return sum; - } - - private static BigDecimal coalesce(BigDecimal v) { return v != null ? v : BigDecimal.ZERO; } - - private static Long asLong(Object v) { - if (v == null) return null; - try { return Long.valueOf(v.toString().trim()); } catch (Exception e) { return null; } - } - private static Integer asInt(Object v) { - if (v == null) return null; - try { return Integer.valueOf(v.toString().trim()); } catch (Exception e) { return null; } - } - private static BigDecimal asBd(Object v) { - if (v == null) return BigDecimal.ZERO; - try { return new BigDecimal(v.toString().trim()); } catch (Exception e) { return BigDecimal.ZERO; } - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusClientController.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusClientController.java deleted file mode 100644 index f7220bf..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/controller/OlympusClientController.java +++ /dev/null @@ -1,175 +0,0 @@ -package com.ruoyi.game.olympus.controller; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.ruoyi.common.core.controller.BaseController; -import com.ruoyi.common.core.domain.R; -import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.game.olympus.contract.request.SpinRequest; -import com.ruoyi.game.olympus.contract.request.RecordPageRequest; -import com.ruoyi.game.olympus.contract.response.BoxResponse; -import com.ruoyi.game.olympus.contract.response.RecordItemResponse; -import com.ruoyi.game.olympus.contract.response.SpinResponse; -import com.ruoyi.game.olympus.domain.OlympusBox; -import com.ruoyi.game.olympus.domain.OlympusPrizeOrnament; -import com.ruoyi.game.olympus.domain.OlympusRecord; -import com.ruoyi.game.olympus.enums.OlympusModeType; -import com.ruoyi.game.olympus.mapper.OlympusBoxMapper; -import com.ruoyi.game.olympus.mapper.OlympusPrizeOrnamentMapper; -import com.ruoyi.game.olympus.mapper.OlympusRecordMapper; -import com.ruoyi.game.olympus.service.OlympusGameService; -import com.ruoyi.system.service.ISysConfigService; -import io.swagger.annotations.Api; -import io.swagger.annotations.ApiOperation; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.web.bind.annotation.*; - -import java.util.*; -import java.util.stream.Collectors; - -@Slf4j -@Api(tags = "玫瑰女郎游戏") -@RestController -@RequestMapping("/api/olympus") -public class OlympusClientController extends BaseController { - - @Autowired private OlympusGameService gameService; - @Autowired private OlympusBoxMapper boxMapper; - @Autowired private OlympusRecordMapper recordMapper; - @Autowired private OlympusPrizeOrnamentMapper prizeOrnamentMapper; - @Autowired private ISysConfigService sysConfigService; - - /** 进行中的局不展示:结算时 settle_source=start,播完末帧才改 finish;超过单局缓存 TTL(30分钟) 视为已结束兜底 */ - private static final long IN_PROGRESS_MS = 30 * 60 * 1000L; - - @ApiOperation("获取箱子列表") - @PostMapping("/boxes") - public R boxes() { - List list = boxMapper.selectList(new LambdaQueryWrapper() - .eq(OlympusBox::getGameType, "OLYMPUS").eq(OlympusBox::getStatus, "1") - .orderByAsc(OlympusBox::getBoxId)); - List boxes = list.stream().map(b -> { - BoxResponse r = new BoxResponse(); - r.setBoxId(b.getBoxId()); - r.setBoxName(b.getName()); - r.setBasicBet(b.getBet()); - r.setPrice(b.getPrice()); - r.setBoxType(b.getBoxType()); - try { r.setModeName(OlympusModeType.fromBoxType(b.getBoxType()).getName()); } - catch (Exception e) { r.setModeName(b.getBoxType()); } - return r; - }).collect(Collectors.toList()); - return R.ok(Map.of("list", boxes)); - } - - @ApiOperation("执行旋转(新游戏或继续游戏)") - @PostMapping("/dospin") - public R dospin(@RequestBody SpinRequest req) { - Long userId = getUserId(); - String websiteMaintenance = sysConfigService.selectConfigByKey("websiteMaintenance"); - String olympusMaintenance = sysConfigService.selectConfigByKey("olympusMaintenance"); - boolean isMaintenance = "1".equals(websiteMaintenance) || "1".equals(olympusMaintenance); - - // 仅“新开一局”(boxId 非空)受维护限制;继续当前对局(boxId 为空)不受影响,可正常播完 - boolean isNewGame = req != null && req.getBoxId() != null; - if (isMaintenance && isNewGame) { - if ("1".equals(websiteMaintenance)) return R.fail("网站维护中......"); - if ("1".equals(olympusMaintenance)) return R.fail("玫瑰女郎维护中......"); - } - try { - SpinResponse resp = gameService.doSpin(userId, req); - return R.ok(resp); - } catch (ServiceException se) { - return se.getCode() != null ? R.fail(se.getCode().intValue(), se.getMessage()) : R.fail(se.getMessage()); - } catch (Exception e) { - return R.fail(e.getMessage()); - } - } - - @ApiOperation("查询游戏记录(分页;scope: 0全服/1我的,默认1我的;只含已播完的局)") - @PostMapping("/records") - public R records(@RequestBody(required = false) RecordPageRequest req) { - Long userId = getUserId(); - int scope = (req != null && req.getScope() != null) ? req.getScope() : 1; - int pageNum = (req != null && req.getPageNum() != null) ? req.getPageNum() : 1; - int pageSize = (req != null && req.getPageSize() != null) ? req.getPageSize() : 10; - - // 用 Java 时间算 cutoff(勿改成 SQL NOW():DB 会话时区是 UTC,create_time 存的是本地时间) - Date cutoff = new Date(System.currentTimeMillis() - IN_PROGRESS_MS); - LambdaQueryWrapper qw = new LambdaQueryWrapper() - .eq(OlympusRecord::getSettled, 1) - // 只展示已结束的局:还在播动画的那局 settle_source 仍为 start,等播完(或超时兜底)才进列表 - .and(w -> w.ne(OlympusRecord::getSettleSource, "start") - .or().lt(OlympusRecord::getCreateTime, cutoff)); - if (scope == 1) { - qw.eq(OlympusRecord::getUserId, userId); - } else { - // 全服:排除机器人/封禁/已删除用户(保留白号,白号可上榜) - qw.inSql(OlympusRecord::getUserId, - "SELECT user_id FROM tt_user WHERE user_type != '03' AND status != '1' AND del_flag = '0'"); - } - qw.orderByDesc(OlympusRecord::getCreateTime); - - Page page = recordMapper.selectPage(new Page<>(pageNum, pageSize), qw); - List records = page.getRecords(); - - // 昵称/头像(两种 scope 都返回) - Map> userMap = new HashMap<>(); - Set userIds = new HashSet<>(); - for (OlympusRecord r : records) userIds.add(r.getUserId()); - if (!userIds.isEmpty()) { - for (Map m : recordMapper.getUserInfos(userIds)) { - Object uid = m.get("user_id") != null ? m.get("user_id") : m.get("userId"); - if (uid != null) userMap.put(Long.valueOf(uid.toString()), m); - } - } - - // 饰品全名 = 名称(磨损品质):新记录按 prize_id 关联,老记录(prize_id 为空)按名称兜底 - Map exteriorById = new HashMap<>(); - Map exteriorByName = new HashMap<>(); - for (OlympusPrizeOrnament o : prizeOrnamentMapper.selectList(null)) { - if (o.getExteriorName() == null || o.getExteriorName().isEmpty()) continue; - exteriorById.putIfAbsent(o.getId(), o.getExteriorName()); - if (o.getName() != null) exteriorByName.putIfAbsent(o.getName(), o.getExteriorName()); - } - - List rows = new ArrayList<>(); - for (OlympusRecord r : records) { - RecordItemResponse vo = new RecordItemResponse(); - Map u = userMap.get(r.getUserId()); - if (u != null) { - vo.setNickName(str(u.get("nick_name"))); - vo.setAvatar(str(u.get("avatar"))); - } - vo.setCost(r.getCost()); - vo.setSettledWin(r.getSettledWin()); - vo.setPrizeImageUrl(r.getPrizeImageUrl()); - vo.setPrizeName(fullPrizeName(r.getPrizeName(), r.getPrizeId(), exteriorById, exteriorByName)); - vo.setCreateTime(r.getCreateTime()); - rows.add(vo); - } - return R.ok(Map.of("rows", rows, "total", page.getTotal())); - } - - private static String str(Object v) { - return v != null ? v.toString() : null; - } - - /** 饰品全名:名称已是全名,有磨损品质时追加 " (品质)" */ - private static String fullPrizeName(String name, Long prizeId, - Map exteriorById, Map exteriorByName) { - if (name == null) return null; - String exterior = prizeId != null ? exteriorById.get(prizeId) : null; - if (exterior == null) exterior = exteriorByName.get(name); - return exterior == null || exterior.isEmpty() ? name : name + " (" + exterior + ")"; - } - - @ApiOperation("查询是否有进行中的游戏(断线重连)") - @PostMapping("/hasActive") - public R hasActive() { - Long userId = getUserId(); - // 维护期间不强制结算:进行中的对局可正常重连并播完(“当前不受影响”,仅拦新开一局) - return R.ok(gameService.hasActive(userId)); - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBox.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBox.java deleted file mode 100644 index 0a96cbc..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBox.java +++ /dev/null @@ -1,86 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; -import java.util.Map; - -/** 箱子配置表 game_olympus_box —— 每种模式×下注档位一个箱子 */ -@Data -@TableName("game_olympus_box") -public class OlympusBox { - - @TableId(value = "box_id", type = IdType.INPUT) - private Long boxId; - - @TableField("game_type") - private String gameType; - - /** 模式:normal | ante | buy_fs */ - @TableField("box_type") - private String boxType; - - private BigDecimal bet; - - private String name; - - /** 实际扣费 = bet × CM (normal=1/ante=2/buy_fs=20) */ - private BigDecimal price; - - @TableField("total_ornaments") - private Integer totalOrnaments; - - private BigDecimal rtp; - - @TableField("rtp_converge_ratio") - private BigDecimal rtpConvergeRatio; - - private Integer sort; - - private String status; - - @TableField("del_flag") - private String delFlag; - - private String remark; - - @TableField("create_time") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") - private Date createTime; - - @TableField("update_time") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") - private Date updateTime; - - // ======== transient ======== - - /** 模式中文名,运行时由 OlympusModeType 填充 */ - @TableField(exist = false) - private String modeName; - - /** 费用倍率 CM,运行时填充 */ - @TableField(exist = false) - private BigDecimal costMultiplier; - - /** 实时RTP,从 game_olympus_record 统计 */ - @TableField(exist = false) - private BigDecimal actualRtp; - - /** 各身份分布反推目标RTP(%),运行时填充 {audience: percent} */ - @TableField(exist = false) - private Map targetRtpByAudience; - - /** 各身份实时RTP(%),运行时填充 {audience: percent} */ - @TableField(exist = false) - private Map actualRtpByAudience; - - /** 各身份一轮总回合(按各自固定池分布反推),运行时填充 {audience: n} */ - @TableField(exist = false) - private Map totalRoundsByAudience; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxOrnament.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxOrnament.java deleted file mode 100644 index a2ae923..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxOrnament.java +++ /dev/null @@ -1,51 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -/** - * 箱子-饰品桶分布表 game_olympus_box_ornament(多身份) - * 唯一键 (box_id, ornament_id, audience):每种身份一套独立的 count/used 分布。 - * 白号(audience=white)独立库存,抽取只递减自己的 used,永不消耗其他身份。 - */ -@Data -@TableName("game_olympus_box_ornament") -public class OlympusBoxOrnament { - - @TableId(type = IdType.AUTO) - private Long id; - - @TableField("box_id") - private Long boxId; - - @TableField("ornament_id") - private Integer ornamentId; - - /** 身份维度 common/anchor/robot/white */ - private String audience; - - /** 分配回合数(加权随机权重) */ - private Integer count; - - /** 已消耗回合数:剩余 = count - used */ - private Integer used; - - // ======== transient(SQL JOIN 填充)======== - - @TableField(exist = false) - private String ornamentName; - - @TableField(exist = false) - private java.math.BigDecimal expectMul; - - /** 剩余 = count - used,SQL 计算填充 */ - @TableField(exist = false) - private Integer remaining; - - /** 本轮收敛阶段该桶已动态选取回合数(轮次账本) */ - @TableField(exist = false) - private Long convergeCount; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxTemplate.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxTemplate.java deleted file mode 100644 index 19fd212..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusBoxTemplate.java +++ /dev/null @@ -1,45 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Data; - -import java.util.Date; -import java.util.List; -import java.util.Map; - -/** 箱子饰品分布模板 game_olympus_box_template(按身份 audience,抽干补桶依据) */ -@Data -@TableName("game_olympus_box_template") -public class OlympusBoxTemplate { - - @TableId(type = IdType.AUTO) - private Long id; - - @TableField("box_id") - private Long boxId; - - private String audience; - - private String name; - - @TableField("total_rounds") - private Integer totalRounds; - - @TableField("items_json") - private String itemsJson; - - @TableField("is_active") - private Integer isActive; - - @TableField("create_time") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") - private Date createTime; - - /** 反序列化后的分布,回传前端用 */ - @TableField(exist = false) - private List> items; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusOrnament.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusOrnament.java deleted file mode 100644 index a7f2e61..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusOrnament.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; - -/** 饰品桶定义表 game_olympus_ornament(17 桶:0x~1000x,展示与落桶区间参考) */ -@Data -@TableName("game_olympus_ornament") -public class OlympusOrnament { - - @TableId(type = IdType.INPUT) - private Long id; - - private String gameType; - - /** 期望倍率(区间均值,最高 1000) */ - private BigDecimal expectMul; - - private String name; - - /** 落桶区间下限(含) */ - private BigDecimal rangeLow; - - /** 落桶区间上限(含) */ - private BigDecimal rangeHigh; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeOrnament.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeOrnament.java deleted file mode 100644 index 8a20e5a..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeOrnament.java +++ /dev/null @@ -1,36 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -/** 返奖饰品定义表 game_olympus_prize_ornament(展示用,按赢额区间匹配) */ -@Data -@TableName("game_olympus_prize_ornament") -public class OlympusPrizeOrnament { - - @TableId(type = IdType.AUTO) - private Long id; - - private String gameType; - private String name; - private String imageUrl; - private Integer grade; - private String exteriorName; - private Integer sort; - private String status; - private Integer isFallback; - - // ======== transient ======== - - @TableField(exist = false) - private java.math.BigDecimal winLow; - - @TableField(exist = false) - private java.math.BigDecimal winHigh; - - @TableField(exist = false) - private String levelImg; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeRange.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeRange.java deleted file mode 100644 index d707787..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusPrizeRange.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; - -/** 返奖饰品区间表 game_olympus_prize_range */ -@Data -@TableName("game_olympus_prize_range") -public class OlympusPrizeRange { - - @TableId(type = IdType.AUTO) - private Long id; - - private Long prizeId; - private BigDecimal winLow; - private BigDecimal winHigh; - - // 兼容 sweet 的字段命名(部分查询用 gameType/ornamentId/sort,olympus 表无这些列时忽略) - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private String gameType; - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private Long ornamentId; - @com.baomidou.mybatisplus.annotation.TableField(exist = false) - private Integer sort; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRecord.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRecord.java deleted file mode 100644 index dd262c6..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRecord.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; - -/** 玩家游玩记录表 game_olympus_record —— 每次开局一条 */ -@Data -@TableName("game_olympus_record") -public class OlympusRecord { - - @TableId(type = IdType.AUTO) - private Long id; - - private Long userId; - private Long boxId; - private Long roundId; - private BigDecimal cost; - private BigDecimal totalWin; - - private Integer settled; // 0未结算 1已结算(幂等守卫) - private String modeType; // normal/ante/buy_fs - private String rewardType; // gold / ornament - private BigDecimal settledWin; - private Long prizeId; - private String prizeName; - private String prizeImageUrl; - private String prizeLevelImg; - private String settleSource; // start=开局已结算(动画还在播) / finish=末帧播完(已结束,进战绩列表);reconnect/scheduled/force_settle 为历史值 - - /** 身份 common/anchor/robot/white:不入库,列表查询时按 tt_user 运行期回填(与奖池 4 池同一口径) */ - @TableField(exist = false) - private String audience; - - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Shanghai") - private Date createTime; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRewardLog.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRewardLog.java deleted file mode 100644 index dc65b3f..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRewardLog.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; - -/** 奖池水位调整日志 game_olympus_reward_log(注水/抽水/重置审计) */ -@Data -@TableName("game_olympus_reward_log") -public class OlympusRewardLog { - - @TableId(type = IdType.AUTO) - private Long id; - - @TableField("box_id") - private Long boxId; - - @TableField("box_name") - private String boxName; - - /** 身份:common/anchor/robot/white(奖池按 箱子×身份 拆池) */ - private String audience; - - /** 1注水 2抽水 3重置 */ - private Integer type; - - private BigDecimal amount; - - @TableField("balance_before") - private BigDecimal balanceBefore; - - @TableField("balance_after") - private BigDecimal balanceAfter; - - /** 操作后全站总池余额快照 */ - @TableField("pool_balance") - private BigDecimal poolBalance; - - private String operator; - - private String remark; - - @TableField("create_time") - @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8") - private Date createTime; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRound.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRound.java deleted file mode 100644 index a82d7cb..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusRound.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableField; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; - -/** 预生成回合表 game_olympus_round */ -@Data -@TableName("game_olympus_round") -public class OlympusRound { - - @TableId(value = "round_id", type = IdType.INPUT) - private Long roundId; - - private Long boxId; - - /** 落入的饰品桶ID (1-17) */ - private Integer ornamentId; - - /** 实际总倍率 = 所有步骤赢额之和 / bet(≤1000,已封顶) */ - private BigDecimal actualMul; - - /** 免费旋转总次数(0=未进FS) */ - private Integer freeSpinCount; - - /** 总 cascade 步数 */ - private Integer totalCascades; - - private Date createTime; - - // ======== transient ======== - - @TableField(exist = false) - private String boxType; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusStep.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusStep.java deleted file mode 100644 index cb22c97..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusStep.java +++ /dev/null @@ -1,79 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; - -/** 回合步骤表 game_olympus_step —— 前端逐步播放的每一帧 */ -@Data -@TableName("game_olympus_step") -public class OlympusStep { - - @TableId(type = IdType.INPUT) - private Long id; - - private Long roundId; - - /** 免费旋转序号 (0=基础旋转, >0=第N次免费旋转) */ - private Integer freeSpinId; - - /** 本旋转内 cascade 序号 */ - private Integer cascadeSeq; - - /** 网格状态 JSON 6×5 */ - private String gridState; - - /** 炸弹符号 JSON [{"pos","val","quality"}] */ - private String multiplierDetail; - - /** 本步中奖明细 JSON */ - private String symbolLinks; - - /** 本旋累计消除符号 JSON {"CROWN":9,...} */ - private String accumulatedSymbols; - - /** 获胜者面板 JSON:本旋逐波追加 [{"symbol","count","win"}] */ - private String spinWinners; - - /** 消除位置 JSON */ - private String removedPositions; - - /** 下落数据 JSON */ - private String dropData; - - /** 填充数据 JSON */ - private String fillData; - - /** 基础盘触发免费旋转 JSON */ - private String scatterTrigger; - - /** 免费旋转重触发 JSON */ - private String retriggerInfo; - - /** 本步基础分(未乘炸弹) */ - private BigDecimal baseScore; - - /** 本步赢额(回合末帧含炸弹结算) */ - private BigDecimal winAmount; - - /** 本步前累计炸弹倍数 */ - private BigDecimal accumulatorBefore; - - /** 本步后累计炸弹倍数(跨旋传递) */ - private BigDecimal accumulatorAfter; - - /** 整局累计赢额 */ - private BigDecimal spinWin; - - /** 本步赢额被 1000x 封顶截断 0否1是 */ - private Integer winCapped; - - /** 爆奖档位(小/中/大:small/middle/big;只在免费旋转里判档,按单旋赢额) */ - private String bigwinLevel; - - /** 本旋末帧 */ - private Integer isFinal; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusUser.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusUser.java deleted file mode 100644 index ee84a8f..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/domain/OlympusUser.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.ruoyi.game.olympus.domain; - -import com.baomidou.mybatisplus.annotation.IdType; -import com.baomidou.mybatisplus.annotation.TableId; -import com.baomidou.mybatisplus.annotation.TableName; -import lombok.Data; - -import java.math.BigDecimal; -import java.util.Date; - -/** 玩家统计表 game_olympus_user —— 每次结算累加 */ -@Data -@TableName("game_olympus_user") -public class OlympusUser { - - @TableId(type = IdType.AUTO) - private Long id; - - private Long userId; - private Long totalSpins; - private BigDecimal totalCost; - private BigDecimal totalWin; - private Integer totalFreeSpins; - private BigDecimal maxWinMul; - private Date updateTime; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusAudience.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusAudience.java deleted file mode 100644 index ae84a74..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusAudience.java +++ /dev/null @@ -1,43 +0,0 @@ -package com.ruoyi.game.olympus.enums; - -import com.ruoyi.domain.entity.sys.TtUser; - -/** - * 身份维度(audience):每种身份一套独立的饰品桶数量分布,实现差异化 RTP。 - * 与 olympus-py config.AUDIENCES 对齐。 - * 解析优先级(白号最高,压过 user_type): - * is_blank_account==1 → white;user_type=01 → anchor;03 → robot;其余 → common。 - */ -public enum OlympusAudience { - COMMON("common"), // 普通用户(user_type=02 及招商/卡商/超管等其他) - ANCHOR("anchor"), // 主播(user_type=01) - ROBOT("robot"), // 机器人(user_type=03) - WHITE("white"); // 白号(is_blank_account=1,压过 user_type) - - private final String code; - - OlympusAudience(String code) { - this.code = code; - } - - public String getCode() { - return code; - } - - /** 白号:纯概率隔离池,不计水位、不消耗其他身份库存 */ - public boolean isWhite() { - return this == WHITE; - } - - /** - * 按玩家解析 audience:白号最高优先,压过 user_type。 - */ - public static OlympusAudience resolve(TtUser player) { - if (player == null) return COMMON; - if (Integer.valueOf(1).equals(player.getIsBlankAccount())) return WHITE; - String userType = player.getUserType(); - if ("01".equals(userType)) return ANCHOR; - if ("03".equals(userType)) return ROBOT; - return COMMON; - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusModeType.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusModeType.java deleted file mode 100644 index 0d37e29..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/enums/OlympusModeType.java +++ /dev/null @@ -1,58 +0,0 @@ -package com.ruoyi.game.olympus.enums; - -/** - * Olympus 三种互斥模式(对齐 olympus-py config.MODES): - * normal(1x) / ante(2x, 底注) / buy_fs(50x, 购买免费旋转)。 - */ -public enum OlympusModeType { - NORMAL("normal", "普通旋转", 1.0), - ANTE("ante", "底注", 2.0), - // 售价 50 倍(原 20 倍):局均赔付 = 50×0.8 = 40 倍底注 → 配比落在 25x/40x/60x 桶 - // (FS 可用预算 22~57),每局才演得出 6~10 次消除 + 4~6 次炸弹累计; - // 20 倍时局均只有 16 倍,配比被迫压在 10x/16x 桶(预算 7~13),动画"钱不够"。 - BUY_FS("buy_fs", "购买免费旋转", 50.0); - - private final String boxType; - private final String name; - private final double costMultiplier; - - OlympusModeType(String boxType, String name, double costMultiplier) { - this.boxType = boxType; - this.name = name; - this.costMultiplier = costMultiplier; - } - - public String getBoxType() { return boxType; } - public String getName() { return name; } - public double getCostMultiplier() { return costMultiplier; } - - /** 购买模式:保证直接进入免费旋转 */ - public boolean isBuyMode() { - return this == BUY_FS; - } - - /** 底注效果:SCATTER 概率提升(仅 ante) */ - public boolean anteBoost() { - return this == ANTE; - } - - /** 该模式可用饰品桶 ID 范围 [minId, maxId],对齐 py 端 MODE_ORNAMENT_IDS */ - public int[] getOrnamentRange() { - switch (this) { - case NORMAL: return new int[]{1, 13}; // 0x-150x - case ANTE: return new int[]{1, 14}; // 0x-200x - // buy_fs 从 10x 起(原 5 = 4x 起):4x/6.5x 桶的整局预算只有 1~3.5 倍底注, - // 演不出"15 旋里 10 旋都在消 + 炸弹累计",玩家看到的一直是空转动画。 - // 10x 起后每局预算 7~13 倍底注,能装 5~8 次消除 + 1~2 次炸弹累计。 - case BUY_FS: return new int[]{7, 17}; // 10x-1000x - default: return new int[]{1, 17}; - } - } - - public static OlympusModeType fromBoxType(String boxType) { - for (OlympusModeType t : values()) { - if (t.boxType.equals(boxType)) return t; - } - throw new IllegalArgumentException("Unknown boxType: " + boxType); - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxMapper.java deleted file mode 100644 index eee1ae2..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusBox; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface OlympusBoxMapper extends BaseMapper { -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxOrnamentMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxOrnamentMapper.java deleted file mode 100644 index 5d7c608..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxOrnamentMapper.java +++ /dev/null @@ -1,32 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusBoxOrnament; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.util.List; - -/** - * 箱子-饰品桶分布 Mapper(多身份 audience)。 - * 所有查询/消耗均按 (boxId, audience) 维度隔离,白号不碰其他身份库存。 - */ -@Mapper -public interface OlympusBoxOrnamentMapper extends BaseMapper { - - /** 查某箱某身份的桶分布(含 expectMul/remaining 连表填充) */ - List selectByBoxIdAndAudience(@Param("boxId") Long boxId, - @Param("audience") String audience); - - int batchInsert(@Param("list") List list); - - /** 删除某箱某身份的分布(重算/应用模板前清空该身份) */ - int deleteByBoxIdAndAudience(@Param("boxId") Long boxId, @Param("audience") String audience); - - /** 该身份某桶 used+1(仅固定阶段消耗) */ - int incrUsed(@Param("boxId") Long boxId, @Param("ornamentId") Integer ornamentId, - @Param("audience") String audience); - - /** 该身份 used 归 0(补桶) */ - int resetUsed(@Param("boxId") Long boxId, @Param("audience") String audience); -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxTemplateMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxTemplateMapper.java deleted file mode 100644 index e9ed706..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusBoxTemplateMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusBoxTemplate; -import org.apache.ibatis.annotations.Mapper; - -/** 箱子饰品分布模板 game_olympus_box_template(按身份 audience)。 */ -@Mapper -public interface OlympusBoxTemplateMapper extends BaseMapper { -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusOrnamentMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusOrnamentMapper.java deleted file mode 100644 index 2f0e847..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusOrnamentMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusOrnament; -import org.apache.ibatis.annotations.Mapper; - -/** 饰品桶定义 game_olympus_ornament(17 桶)。 */ -@Mapper -public interface OlympusOrnamentMapper extends BaseMapper { -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeOrnamentMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeOrnamentMapper.java deleted file mode 100644 index 76f944c..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeOrnamentMapper.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusPrizeOrnament; -import org.apache.ibatis.annotations.Mapper; - -@Mapper -public interface OlympusPrizeOrnamentMapper extends BaseMapper { -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeRangeMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeRangeMapper.java deleted file mode 100644 index cb95bed..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusPrizeRangeMapper.java +++ /dev/null @@ -1,50 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusPrizeRange; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - -import java.math.BigDecimal; -import java.util.Map; - -/** - * 返奖饰品区间:按赢额匹配展示饰品。联表 game_olympus_prize_ornament 取 name/imageUrl/grade, - * 再联 tt_ornaments_level 取 level_img AS levelImageUrl(品质/等级图片,前端 prizeInfo.levelImageUrl 用这个)。 - */ -@Mapper -public interface OlympusPrizeRangeMapper extends BaseMapper { - - /** 精确区间匹配 win_low < win <= win_high */ - @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " - + "l.level_img AS levelImageUrl " - + "FROM game_olympus_prize_range r JOIN game_olympus_prize_ornament o ON o.id = r.prize_id " - + "LEFT JOIN tt_ornaments_level l ON l.id = o.grade " - + "WHERE #{win} > r.win_low AND #{win} <= r.win_high AND o.status = '1' " - + "ORDER BY r.win_low DESC LIMIT 1") - Map findPrizeByWin(@Param("win") BigDecimal win); - - /** 兜底饰品(is_fallback=1) */ - @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " - + "l.level_img AS levelImageUrl " - + "FROM game_olympus_prize_ornament o LEFT JOIN tt_ornaments_level l ON l.id = o.grade " - + "WHERE o.is_fallback = 1 AND o.status = '1' ORDER BY o.sort ASC LIMIT 1") - Map findFallbackPrize(); - - /** 最低区间饰品 */ - @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " - + "l.level_img AS levelImageUrl " - + "FROM game_olympus_prize_range r JOIN game_olympus_prize_ornament o ON o.id = r.prize_id " - + "LEFT JOIN tt_ornaments_level l ON l.id = o.grade " - + "WHERE o.status = '1' ORDER BY r.win_low ASC LIMIT 1") - Map findLowestPrize(); - - /** 最高区间饰品 */ - @Select("SELECT o.id, o.name, o.image_url AS imageUrl, o.grade, o.exterior_name AS exteriorName, " - + "l.level_img AS levelImageUrl " - + "FROM game_olympus_prize_range r JOIN game_olympus_prize_ornament o ON o.id = r.prize_id " - + "LEFT JOIN tt_ornaments_level l ON l.id = o.grade " - + "WHERE o.status = '1' ORDER BY r.win_high DESC LIMIT 1") - Map findHighestPrize(); -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRecordMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRecordMapper.java deleted file mode 100644 index 0ad1b7f..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRecordMapper.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusRecord; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - -import java.util.List; -import java.util.Map; -import java.util.Set; - -@Mapper -public interface OlympusRecordMapper extends BaseMapper { - - /** 批量查各箱实时 RTP(排除白号/机器人/封禁/删除,与首页口径一致) */ - @Select("") - List> selectBoxRtpList(@Param("boxIds") List boxIds); - - @Select("") - List> getUserInfos(@Param("userIds") Set userIds); - - /** 每日统计汇总(排除白号/机器人/封禁/删除) */ - @Select("") - Map winSummary(@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd); - - /** 每日统计列表:按天聚合(口径与 winSummary 一致,排除白号/机器人/封禁/删除) */ - @Select("") - List> dailyList(@Param("dateStart") String dateStart, @Param("dateEnd") String dateEnd); - - /** - * 某箱 + 某身份奖池真实累计(按已结算记录派生)。身份口径同 {@link com.ruoyi.game.olympus.enums.OlympusAudience#resolve}, - * 四个身份互斥且并集 = 全部已结算记录(拆分口径,总量与旧"非白号一个池"一致)。 - * totalBet=Σcost,totalWin=Σsettled_win(实际发放价值)。 - */ - @Select("") - Map sumPool(@Param("boxId") Long boxId, @Param("audience") String audience); -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRewardLogMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRewardLogMapper.java deleted file mode 100644 index 483b4cb..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRewardLogMapper.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusRewardLog; -import org.apache.ibatis.annotations.Mapper; - -/** 奖池水位调整日志 game_olympus_reward_log。 */ -@Mapper -public interface OlympusRewardLogMapper extends BaseMapper { -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRoundMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRoundMapper.java deleted file mode 100644 index c5dfba6..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusRoundMapper.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusRound; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; - -import java.math.BigDecimal; -import java.util.List; -import java.util.Map; - -@Mapper -public interface OlympusRoundMapper extends BaseMapper { - - OlympusRound pickByOrnament(@Param("boxId") Long boxId, @Param("ornamentId") Integer ornamentId, - @Param("needFreeSpins") Boolean needFreeSpins); - - OlympusRound pickAnyByBox(@Param("boxId") Long boxId, @Param("needFreeSpins") Boolean needFreeSpins); - - /** 收敛阶段:桶内取 actual_mul 最接近 desiredMul 的回合,优先 actual_mul ≤ capMul */ - OlympusRound pickClosestMul(@Param("boxId") Long boxId, @Param("ornamentId") Integer ornamentId, - @Param("needFreeSpins") Boolean needFreeSpins, - @Param("desiredMul") BigDecimal desiredMul, - @Param("capMul") BigDecimal capMul); - - /** 统计某箱池内各桶实际均值:[{ornamentId, sampleCount, avgMul}](配比/RTP 按它算) */ - List> avgMulByOrnament(@Param("boxId") Long boxId); -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusStepMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusStepMapper.java deleted file mode 100644 index 0240957..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusStepMapper.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusStep; -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 OlympusStepMapper extends BaseMapper { - - @Select("SELECT * FROM game_olympus_step WHERE round_id = #{roundId} ORDER BY id ASC") - List selectByRoundId(@Param("roundId") Long roundId); -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusUserMapper.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusUserMapper.java deleted file mode 100644 index d367b16..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/mapper/OlympusUserMapper.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.ruoyi.game.olympus.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.ruoyi.game.olympus.domain.OlympusUser; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - -@Mapper -public interface OlympusUserMapper extends BaseMapper { - - @Select("SELECT * FROM game_olympus_user WHERE user_id = #{userId} LIMIT 1") - OlympusUser selectByUserId(@Param("userId") Long userId); -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/PickedRound.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/PickedRound.java deleted file mode 100644 index 3e954e0..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/PickedRound.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.ruoyi.game.olympus.model; - -import com.ruoyi.game.olympus.domain.OlympusRound; -import com.ruoyi.game.olympus.domain.OlympusStep; -import lombok.AllArgsConstructor; -import lombok.Data; - -import java.util.List; - -/** 选中回合 + 其步骤 + 阶段标记(固定/收敛,决定是否 incrUsed 与 conv 记账) */ -@Data -@AllArgsConstructor -public class PickedRound { - private OlympusRound round; - private List steps; - private boolean converge; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusGameCache.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusGameCache.java deleted file mode 100644 index 3e20200..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusGameCache.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.ruoyi.game.olympus.model.cache; - -import lombok.Data; - -import java.io.Serializable; -import java.math.BigDecimal; -import java.util.List; - -/** Olympus 单局进行中缓存(Redis,TTL 30 分钟) */ -@Data -public class OlympusGameCache implements Serializable { - - private Long boxId; - private String boxType; // normal|ante|buy_fs - private BigDecimal bet; - private BigDecimal cost; - - private Long roundId; - private BigDecimal totalMul; // round.actualMul - private Integer freeSpinCount; - - private int stepIndex; // 当前已返回步数(1-based) - private List steps; - - private BigDecimal totalWin; - - private Long recordId; - private Long boxRecordId; - - /** 身份维度 common/anchor/robot/white,白号隔离依据 */ - private String audience; - /** 是否白号:白号不注水/不扣水/不受水位约束 */ - private boolean blankAccount; - - @Data - public static class CachedStep implements Serializable { - private int freeSpinId; - private int cascadeSeq; - private String gridState; - private String multiplierDetail; - private String symbolLinks; // 本旋累积连线(含 tumble) - private BigDecimal winAmount; // 本旋累计(该旋末帧 = 这一旋的总额) - private BigDecimal spinWin; // 本局累计(末帧 = totalWin) - /** 本步后炸弹累计倍数(FS 跨旋传递);断线重连时前端靠它恢复累计倍数 */ - private BigDecimal accumulatorAfter; - private int winCapped; - private String bigwinLevel; - private int isFinal; - private String retriggerInfo; - private String dropData; - private String fillData; - private String scatterTrigger; - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java deleted file mode 100644 index ed791b9..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/model/cache/OlympusRewardPoolCache.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.ruoyi.game.olympus.model.cache; - -import lombok.Data; - -import java.math.BigDecimal; - -/** - * Olympus 按箱奖池(水位)缓存。 - * 水位 = totalBet - totalWin + adjust(人工注水/抽水),仅真实玩家(白号隔离)。 - */ -@Data -public class OlympusRewardPoolCache { - private BigDecimal totalBet; - private BigDecimal totalWin; - private BigDecimal adjust; -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java deleted file mode 100644 index c8b151c..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCacheRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.ruoyi.game.olympus.repository; - -import com.ruoyi.common.core.redis.RedisCache; -import com.ruoyi.game.olympus.model.cache.OlympusGameCache; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Component; - -import java.util.concurrent.TimeUnit; - -import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_CACHE; - -/** 单局进行中缓存(Redis,TTL 30 分钟) */ -@Component -public class OlympusCacheRepository { - - @Autowired - private RedisCache redisCache; - - public OlympusGameCache getCache(Long userId) { - return redisCache.getCacheObject(OLYMPUS_GAME_CACHE + userId); - } - - public void setCache(Long userId, OlympusGameCache cache) { - redisCache.setCacheObject(OLYMPUS_GAME_CACHE + userId, cache, 30, TimeUnit.MINUTES); - } - - public void clearCache(Long userId) { - redisCache.deleteObject(OLYMPUS_GAME_CACHE + userId); - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java deleted file mode 100644 index bf96b9a..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusCycleRepository.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.ruoyi.game.olympus.repository; - -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.concurrent.TimeUnit; - -import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_CYCLE; - -/** - * 一轮内 RTP 动态收敛账本,按 (boxId, audience) 隔离。 - * Hash key = ruoyi:olympus:cycle:{boxId}:{audience},字段 drawn/mulSum/conv:{ornamentId}。 - * 白号(audience=white)也有独立账本,与其他身份完全隔离。 - */ -@Component -public class OlympusCycleRepository { - - @Autowired - public RedisTemplate redisTemplate; - - private static final String F_DRAWN = "drawn"; - private static final String F_MUL_SUM = "mulSum"; - private static final String F_CONV_PREFIX = "conv:"; - private static final long TTL_DAYS = 30; - - private String key(Long boxId, String audience) { - return OLYMPUS_GAME_CYCLE + boxId + ":" + audience; - } - - public long drawn(Long boxId, String audience) { - Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_DRAWN); - return v == null ? 0L : toLong(v); - } - - public double mulSum(Long boxId, String audience) { - Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_MUL_SUM); - return v == null ? 0d : toDouble(v); - } - - /** 记一抽:drawn+1、mulSum+actualMul;收敛阶段额外 conv:{ornamentId}+1。 */ - public void recordDraw(Long boxId, String audience, Integer ornamentId, BigDecimal actualMul, boolean converge) { - String k = key(boxId, audience); - redisTemplate.opsForHash().increment(k, F_DRAWN, 1L); - double mul = actualMul != null ? actualMul.doubleValue() : 0d; - if (mul != 0d) redisTemplate.opsForHash().increment(k, F_MUL_SUM, mul); - if (converge && ornamentId != null) { - redisTemplate.opsForHash().increment(k, F_CONV_PREFIX + ornamentId, 1L); - } - redisTemplate.expire(k, TTL_DAYS, TimeUnit.DAYS); - } - - public void resetCycle(Long boxId, String audience) { - redisTemplate.delete(key(boxId, audience)); - } - - public long convergeCount(Long boxId, String audience, Integer ornamentId) { - Object v = redisTemplate.opsForHash().get(key(boxId, audience), F_CONV_PREFIX + ornamentId); - return v == null ? 0L : toLong(v); - } - - /** 本轮实时 RTP(%) = mulSum / drawn / cm × 100(common 身份口径,供后台展示)。 */ - public BigDecimal realtimeRtp(Long boxId, String audience, double cm) { - long d = drawn(boxId, audience); - if (d <= 0 || cm <= 0) return null; - double rtp = mulSum(boxId, audience) / d / cm * 100; - return BigDecimal.valueOf(rtp).setScale(2, RoundingMode.HALF_UP); - } - - private static long toLong(Object v) { - if (v instanceof Number) return ((Number) v).longValue(); - try { return Long.parseLong(v.toString().trim()); } catch (Exception e) { return 0L; } - } - - private static double toDouble(Object v) { - if (v instanceof Number) return ((Number) v).doubleValue(); - try { return Double.parseDouble(v.toString().trim()); } catch (Exception e) { return 0d; } - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java deleted file mode 100644 index 6aa960c..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/repository/OlympusRewardPoolCacheRepository.java +++ /dev/null @@ -1,115 +0,0 @@ -package com.ruoyi.game.olympus.repository; - -import com.ruoyi.game.olympus.model.cache.OlympusRewardPoolCache; -import com.ruoyi.game.olympus.mapper.OlympusRecordMapper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Component; - -import java.math.BigDecimal; -import java.util.Map; -import java.util.concurrent.TimeUnit; - -import static com.ruoyi.admin.config.RedisConstants.OLYMPUS_GAME_REWARD_POOL_CACHE; - -/** - * Olympus 按【箱子 × 身份】奖池(水位)仓储 —— 对齐荒野争锋 bullet 的按箱水位模型,再按身份(audience)拆池。 - * 每箱每身份独立键::box:{boxId}:{audience}:dbbet / :dbwin / :adjust;全局 adjust 也按身份一份。 - * 水位 = box:bet − box:win + box:adjust(可能为负)。 - *

- * 记账口径(方案B''):bet/win **由 DB 已结算记录按身份派生**(game_olympus_record + tt_user), - * Redis 仅做短缓存供 payoutCap/展示快速读取;adjust 为管理员人工注水/抽水(Redis)。 - * 因 record 与发奖同一事务提交,池值恒等于已提交的真实账,无"提交后记账崩溃窗口"。 - */ -@Component -public class OlympusRewardPoolCacheRepository { - - @Autowired - public RedisTemplate redisTemplate; - - @Autowired - private OlympusRecordMapper recordMapper; - - private static final String KEY_BOX_PREFIX = OLYMPUS_GAME_REWARD_POOL_CACHE + ":box:"; - /** 全局人工调整累计(各箱 adjust 之和,仅展示),按身份一份 */ - private static final String KEY_ADJUST = OLYMPUS_GAME_REWARD_POOL_CACHE + ":adjust:"; - - private static final String KEY_DB_BET = ":dbbet"; - private static final String KEY_DB_WIN = ":dbwin"; - /** ponytail: DB 派生结果短缓存;DB 为真相,TTL 内只是短暂滞后、自愈,无永久漂移 */ - private static final long DB_CACHE_TTL_SECONDS = 5; - - private static String boxKey(Long boxId, String audience, String suffix) { - return KEY_BOX_PREFIX + boxId + ":" + audience + suffix; - } - - private static String adjustKey(String audience) { - return KEY_ADJUST + audience; - } - - /** 管理员逐箱注水/抽水:delta 为正注水、为负抽水;同时累加该身份的全局 adjust 键 */ - public void adjustBox(Long boxId, String audience, BigDecimal delta) { - if (boxId == null || delta == null || delta.signum() == 0) return; - redisTemplate.opsForValue().increment(boxKey(boxId, audience, ":adjust"), delta.doubleValue()); - redisTemplate.opsForValue().increment(adjustKey(audience), delta.doubleValue()); - } - - /** 仅调整全局 adjust 键(重置已删除箱子遗留残余),不动箱子键 */ - public void adjustGlobal(String audience, BigDecimal delta) { - if (delta == null || delta.signum() == 0) return; - redisTemplate.opsForValue().increment(adjustKey(audience), delta.doubleValue()); - } - - public BigDecimal getGlobalAdjust(String audience) { - return readValue(adjustKey(audience)); - } - - public OlympusRewardPoolCache getBoxCache(Long boxId, String audience) { - OlympusRewardPoolCache cache = new OlympusRewardPoolCache(); - cache.setTotalBet(dbPoolTotal(boxId, audience, KEY_DB_BET)); - cache.setTotalWin(dbPoolTotal(boxId, audience, KEY_DB_WIN)); - cache.setAdjust(readValue(boxKey(boxId, audience, ":adjust"))); - return cache; - } - - public BigDecimal getBoxAdjust(Long boxId, String audience) { - return readValue(boxKey(boxId, audience, ":adjust")); - } - - /** 箱子×身份水位 = 该身份箱内累计投注 − 累计返奖 + 人工调整(可能为负) */ - public BigDecimal getBoxBalance(Long boxId, String audience) { - OlympusRewardPoolCache c = getBoxCache(boxId, audience); - return c.getTotalBet().subtract(c.getTotalWin()).add(c.getAdjust()) - .setScale(2, java.math.RoundingMode.HALF_UP); - } - - /** 读取某箱某身份 bet/win 的 DB 派生值(短缓存;缓存缺失时按 record 聚合重算) */ - private BigDecimal dbPoolTotal(Long boxId, String audience, String suffix) { - String key = boxKey(boxId, audience, suffix); - Object v = redisTemplate.opsForValue().get(key); - if (v != null) return toBd(v); - refreshDbPool(boxId, audience); - return toBd(redisTemplate.opsForValue().get(key)); - } - - private void refreshDbPool(Long boxId, String audience) { - Map row = boxId != null ? recordMapper.sumPool(boxId, audience) : null; - BigDecimal bet = toBd(row != null ? row.get("totalBet") : null); - BigDecimal win = toBd(row != null ? row.get("totalWin") : null); - redisTemplate.opsForValue().set(boxKey(boxId, audience, KEY_DB_BET), bet.doubleValue(), DB_CACHE_TTL_SECONDS, TimeUnit.SECONDS); - redisTemplate.opsForValue().set(boxKey(boxId, audience, KEY_DB_WIN), win.doubleValue(), DB_CACHE_TTL_SECONDS, TimeUnit.SECONDS); - } - - private BigDecimal readValue(String key) { - return toBd(redisTemplate.opsForValue().get(key)); - } - - private static BigDecimal toBd(Object v) { - if (v == null) return BigDecimal.ZERO; - try { - return new BigDecimal(v.toString()); - } catch (Exception e) { - return BigDecimal.ZERO; - } - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java deleted file mode 100644 index 464ac0b..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusDistributionService.java +++ /dev/null @@ -1,354 +0,0 @@ -package com.ruoyi.game.olympus.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.fasterxml.jackson.core.type.TypeReference; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.ruoyi.game.olympus.domain.OlympusBox; -import com.ruoyi.game.olympus.domain.OlympusBoxOrnament; -import com.ruoyi.game.olympus.domain.OlympusBoxTemplate; -import com.ruoyi.game.olympus.domain.OlympusOrnament; -import com.ruoyi.game.olympus.enums.OlympusModeType; -import com.ruoyi.game.olympus.mapper.OlympusBoxMapper; -import com.ruoyi.game.olympus.mapper.OlympusBoxOrnamentMapper; -import com.ruoyi.game.olympus.mapper.OlympusBoxTemplateMapper; -import com.ruoyi.game.olympus.mapper.OlympusOrnamentMapper; -import com.ruoyi.game.olympus.mapper.OlympusRoundMapper; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.util.*; - -/** - * Olympus 饰品桶分布计算 / 重置服务(按身份 audience)。 - * 算法移植自 sweet:目标 RTP → 幂律权重二分 → 单调约束 → 微调逼近; - * 差异:expect_mul 从 game_olympus_ornament 读,基础权重按模式桶区间通用生成(30×0.6^k)。 - */ -@Service -public class OlympusDistributionService { - - private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - - @Autowired private OlympusBoxMapper boxMapper; - @Autowired private OlympusBoxOrnamentMapper boxOrnamentMapper; - @Autowired private OlympusBoxTemplateMapper boxTemplateMapper; - @Autowired private OlympusOrnamentMapper ornamentMapper; - @Autowired private OlympusRoundMapper roundMapper; - - /** - * 桶"实际发出期望"覆盖:桶 17 展示 1000x(区间 630~1370),但单局封顶 1000, - * 真正发出去的是 min(原始分, 1000)。理论值(原始分均匀分布)E[min(x,1000)] = 907.5; - * 实测(buy_fs 10 箱 × 桶17 全部回合 = 200 个)均值 834.6、中位 821.3、27.5% 触顶, - * 因为生成端种的大奖目标集中在 1000 附近而不是均匀分布。 - * 配比计算(calcRounds)与后台「目标RTP」都按"实际会发出去多少钱"算,所以用实测值: - * 用名义 1000 会把 RTP 高估约 0.5~1.6 个百分点。 - * 注:抽回合时的 payoutCap 过滤(OlympusRoundPicker)仍读 DB 的名义 expect_mul,偏保守、保留安全余量。 - */ - private static final Map PAYOUT_EXPECT_OVERRIDE = Map.of(17, 835.0); - - /** 饰品桶 ID → 实际发出期望(受封顶影响的桶用覆盖值,其余用 DB 的 expect_mul)。 */ - public Map loadOrnamentMul() { - Map m = new HashMap<>(); - for (OlympusOrnament o : ornamentMapper.selectList(null)) { - if (o.getId() == null) continue; - int id = o.getId().intValue(); - Double override = PAYOUT_EXPECT_OVERRIDE.get(id); - if (override != null) { m.put(id, override); continue; } - m.put(id, o.getExpectMul() != null ? o.getExpectMul().doubleValue() : 0.0); - } - return m; - } - - /** 该桶池子里一条回合都没有时才回退名义期望:只要池子有回合,它的均值就是这个箱子的真实期望 */ - private static final int MIN_POOL_SAMPLE = 1; - - /** - * 桶 → **这个箱子的池子实际会赔出去多少钱**的期望(AVG(actual_mul),按 boxId 统计)。 - * - * 为什么不用名义 expect_mul:桶内实际均值与区间中点有偏差(实测 -18% ~ +11%,桶 17 还受 1000x 封顶 - * 影响只有 ~835),用名义值算配比 → 「设 80% 实际赔 81.8%」这种系统性偏差;而且每个箱子的池子是 - * 独立生成的一批回合,只有按它自己的池内均值算,算出来的配比才真的等于该箱的期望赔付。 - * 样本不足的桶回退名义值(池子还没生成时也能算)。 - */ - public Map loadPoolExpect(Long boxId) { - Map expect = new HashMap<>(loadOrnamentMul()); - if (boxId == null) return expect; - try { - for (Map row : roundMapper.avgMulByOrnament(boxId)) { - Object idObj = row.get("ornamentId"); - Object nObj = row.get("sampleCount"); - Object avgObj = row.get("avgMul"); - if (idObj == null || avgObj == null) continue; - int id = ((Number) idObj).intValue(); - long n = nObj instanceof Number ? ((Number) nObj).longValue() : 0L; - double avg = ((Number) avgObj).doubleValue(); - if (!expect.containsKey(id)) continue; - if (n < MIN_POOL_SAMPLE || avg <= 0) continue; - expect.put(id, avg); - } - } catch (Exception ignore) { - // 池子还没生成/查询异常 → 用名义值兜底 - } - return expect; - } - - /** - * 按目标 RTP 动态计算各饰品桶回合数(幂律 power 二分搜索 + 单调 + 微调)。 - * 期望值用 {@link #loadPoolExpect}(**该箱**池内实际均值),所以算出来的配比实际赔付就等于 targetRtp。 - * @param mode 模式 @param boxId 箱子(决定用哪个池子的实际均值) @param totalRounds 固定池回合数 @param targetRtp 目标 RTP(如 0.8) - */ - public Map calcRounds(OlympusModeType mode, Long boxId, int totalRounds, double targetRtp) { - double cm = mode.getCostMultiplier(); - double targetAvg = targetRtp * cm; - Map ornMul = loadPoolExpect(boxId); - - // 基础权重:模式桶区间 [lo,hi] 内 30×0.6^k 等比递减 - int[] range = mode.getOrnamentRange(); - int lo = range[0], hi = range[1]; - int n = Math.max(1, hi - lo + 1); - double[][] baseWeights = new double[n][2]; - for (int i = 0; i < n; i++) { - baseWeights[i][0] = lo + i; - baseWeights[i][1] = 30.0 * Math.pow(0.6, i); - } - - int nOrns = baseWeights.length; - // 回合数不足:每桶至少 1,均摊 - if (totalRounds <= nOrns) { - Map flat = new LinkedHashMap<>(); - for (double[] w : baseWeights) flat.put((int) w[0], 1); - return flat; - } - int remainingRounds = totalRounds - nOrns; - - double guaranteedMulSum = 0; - for (double[] w : baseWeights) guaranteedMulSum += ornMul.getOrDefault((int) w[0], 0.0); - double targetTotalMul = targetAvg * totalRounds; - double remainingTargetMul = targetTotalMul - guaranteedMulSum; - double remainingTargetAvg = remainingTargetMul / remainingRounds; - - // 二分 power - double power = 1.0, plo = 0.2, phi = 10.0; - for (int iter = 0; iter < 30; iter++) { - double sumW = 0, sumWMul = 0; - for (double[] w : baseWeights) { - double weight = Math.pow(w[1], power); - sumW += weight; - sumWMul += weight * ornMul.getOrDefault((int) w[0], 0.0); - } - double avg = sumW > 0 ? sumWMul / sumW : 0; - if (Math.abs(avg - remainingTargetAvg) < Math.max(remainingTargetAvg * 0.01, 0.001)) break; - if (avg < remainingTargetAvg) phi = power; else plo = power; - power = (plo + phi) / 2; - } - - Map result = new LinkedHashMap<>(); - for (double[] w : baseWeights) result.put((int) w[0], 1); - double totalW = 0; - Map adjWeights = new LinkedHashMap<>(); - for (double[] w : baseWeights) { - double weight = Math.pow(w[1], power); - adjWeights.put((int) w[0], weight); - totalW += weight; - } - int rem = remainingRounds; - for (double[] w : baseWeights) { - int ornId = (int) w[0]; - int extra = totalW > 0 ? (int) Math.round(adjWeights.get(ornId) / totalW * remainingRounds) : 0; - int put = Math.min(extra, rem); - if (put > 0) { result.put(ornId, result.get(ornId) + put); rem -= put; } - } - if (rem > 0) { int first = (int) baseWeights[0][0]; result.put(first, result.get(first) + rem); } - - // 单调:count[i] >= count[i+1] - List sortedIds = new ArrayList<>(result.keySet()); - sortedIds.sort(Integer::compareTo); - for (int i = sortedIds.size() - 2; i >= 0; i--) { - int cur = sortedIds.get(i), next = sortedIds.get(i + 1); - if (result.get(cur) < result.get(next)) result.put(cur, result.get(next)); - } - int curTotal = result.values().stream().mapToInt(Integer::intValue).sum(); - int diff = totalRounds - curTotal; - if (diff != 0) { int first = sortedIds.get(0); result.put(first, result.get(first) + diff); } - - // 微调:任取两个桶搬 1 个(保持单调),只要更接近目标 - // (原来只在"最大的 3 个桶"之间搬,池内均值口径下根本走不到目标,会差 0.5~1 个百分点) - for (int iter = 0; iter < 400; iter++) { - double currentMul = 0; - for (Map.Entry e : result.entrySet()) - currentMul += e.getValue() * ornMul.getOrDefault(e.getKey(), 0.0); - double deltaMul = targetAvg * totalRounds - currentMul; - if (Math.abs(deltaMul) < 0.01) break; - int bestLo = -1, bestHi = -1; - double bestDiff = Math.abs(deltaMul); - for (int loId : sortedIds) { - if (result.get(loId) <= 1) continue; - for (int hiId : sortedIds) { - if (loId == hiId) continue; - double change = ornMul.getOrDefault(hiId, 0.0) - ornMul.getOrDefault(loId, 0.0); - if (Math.abs(change) < 0.001) continue; - double nd = Math.abs(deltaMul - change); - if (nd < bestDiff) { - boolean ok = true; - int ca = result.get(loId) - 1, idxLo = sortedIds.indexOf(loId); - if (idxLo < sortedIds.size() - 1 && ca < result.get(sortedIds.get(idxLo + 1))) ok = false; - int ca2 = result.get(hiId) + 1, idxHi = sortedIds.indexOf(hiId); - if (idxHi > 0 && ca2 > result.get(sortedIds.get(idxHi - 1))) ok = false; - if (sortedIds.indexOf(loId) < sortedIds.indexOf(hiId) && ca < ca2) ok = false; - if (ok) { bestDiff = nd; bestLo = loId; bestHi = hiId; } - } - } - } - if (bestLo < 0) break; - result.put(bestLo, result.get(bestLo) - 1); - result.put(bestHi, result.get(bestHi) + 1); - } - return result; - } - - /** 模拟 payoutCap 约束下的实际 RTP(%):桶的赔付期望按该箱池内实际均值算。 */ - public double simulateActualRtp(Map distribution, Long boxId, double rtp, int simRounds) { - if (distribution == null || distribution.isEmpty() || simRounds <= 0) return 0; - Map ornMul = loadPoolExpect(boxId); - double cm = 1.0; - if (boxId != null) { - OlympusBox box = boxMapper.selectById(boxId); - if (box != null) cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); - } - if (cm <= 0) return 0; - List mulList = new ArrayList<>(); - for (Map.Entry e : distribution.entrySet()) { - if (e.getValue() == null || e.getValue() <= 0) continue; - double mul = ornMul.getOrDefault(e.getKey(), 0.0); - for (int i = 0; i < e.getValue(); i++) mulList.add(mul); - } - if (mulList.isEmpty()) return 0; - Random rnd = new Random(); - double totalRtpSum = 0; - for (int round = 0; round < simRounds; round++) { - Collections.shuffle(mulList, rnd); - double balance = 0, totalPayout = 0, totalCost = 0; - for (double mul : mulList) { - double payoutCap = Math.max(0, balance + cm) * rtp; - double actualMul = (mul <= payoutCap) ? mul : 0; - balance += cm - actualMul; - totalPayout += actualMul; - totalCost += cm; - } - totalRtpSum += totalCost > 0 ? totalPayout / totalCost : 0; - } - return (totalRtpSum / simRounds) * 100; - } - - /** 由固定池回合数反推总回合 N = round(fixed/(1-cr))。 */ - public static int deriveTotalRounds(int fixedRounds, double convergeRatio) { - double cr = convergeRatio < 0 ? 0 : (convergeRatio > 0.9 ? 0.9 : convergeRatio); - return (int) Math.round(fixedRounds / (1.0 - cr)); - } - - /** 由总回合 N 算固定池回合数 fixed = round(N*(1-cr))。 */ - public static int deriveFixedRounds(int totalRounds, double convergeRatio) { - double cr = convergeRatio < 0 ? 0 : (convergeRatio > 0.9 ? 0.9 : convergeRatio); - return (int) Math.round(totalRounds * (1.0 - cr)); - } - - private static double convergeRatioOf(OlympusBox box) { - double cr = (box != null && box.getRtpConvergeRatio() != null) ? box.getRtpConvergeRatio().doubleValue() : 0.15; - if (cr < 0) cr = 0; - if (cr > 0.9) cr = 0.9; - return cr; - } - - /** 重算某箱某身份分布:按目标 RTP 分配固定池 count,used 归 0。 */ - @Transactional(rollbackFor = Exception.class) - public void reset(Long boxId, String audience) { - OlympusBox box = boxMapper.selectById(boxId); - if (box == null) return; - OlympusModeType mode = OlympusModeType.fromBoxType(box.getBoxType()); - int totalRounds = box.getTotalOrnaments() != null ? box.getTotalOrnaments() : 200; - double targetRtp = box.getRtp() != null ? box.getRtp().doubleValue() : 0.8; - int fixedRounds = deriveFixedRounds(totalRounds, convergeRatioOf(box)); - if (fixedRounds < 1) fixedRounds = totalRounds; - Map dist = calcRounds(mode, boxId, fixedRounds, targetRtp); - List list = new ArrayList<>(); - for (Map.Entry e : dist.entrySet()) { - if (e.getValue() == null || e.getValue() <= 0) continue; - OlympusBoxOrnament bo = new OlympusBoxOrnament(); - bo.setBoxId(boxId); - bo.setOrnamentId(e.getKey()); - bo.setAudience(audience); - bo.setCount(e.getValue()); - bo.setUsed(0); - list.add(bo); - } - boxOrnamentMapper.deleteByBoxIdAndAudience(boxId, audience); - if (!list.isEmpty()) boxOrnamentMapper.batchInsert(list); - } - - /** 应用模板分布到某箱某身份(count 取模板、used 归 0),并按固定池反推回写总回合。 */ - @Transactional(rollbackFor = Exception.class) - public void writeDistribution(Long boxId, String audience, List> items) { - boxOrnamentMapper.deleteByBoxIdAndAudience(boxId, audience); - int total = 0; - List list = new ArrayList<>(); - if (items != null) { - for (Map item : items) { - Integer ornamentId = toInt(item.get("ornamentId")); - Integer count = toInt(item.get("count")); - if (ornamentId == null || count == null || count <= 0) continue; - OlympusBoxOrnament bo = new OlympusBoxOrnament(); - bo.setBoxId(boxId); - bo.setOrnamentId(ornamentId); - bo.setAudience(audience); - bo.setCount(count); - bo.setUsed(0); - list.add(bo); - total += count; - } - } - if (!list.isEmpty()) boxOrnamentMapper.batchInsert(list); - OlympusBox box = boxMapper.selectById(boxId); - if (box != null) { - box.setTotalOrnaments(deriveTotalRounds(total, convergeRatioOf(box))); - boxMapper.updateById(box); - } - } - - /** 某箱某身份抽干补桶:优先按该身份生效模板重写;否则仅 used 归 0。 */ - @Transactional(rollbackFor = Exception.class) - public void refillFromConfig(Long boxId, String audience) { - OlympusBoxTemplate active = boxTemplateMapper.selectOne( - new LambdaQueryWrapper() - .eq(OlympusBoxTemplate::getBoxId, boxId) - .eq(OlympusBoxTemplate::getAudience, audience) - .eq(OlympusBoxTemplate::getIsActive, 1) - .orderByDesc(OlympusBoxTemplate::getId).last("LIMIT 1")); - if (active != null && active.getItemsJson() != null) { - List> items = parseItems(active.getItemsJson()); - if (!items.isEmpty()) { writeDistribution(boxId, audience, items); return; } - } - boxOrnamentMapper.resetUsed(boxId, audience); - } - - public List distribution(Long boxId, String audience) { - return boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); - } - - public List> parseItems(String itemsJson) { - try { - return OBJECT_MAPPER.readValue(itemsJson, new TypeReference>>() {}); - } catch (Exception e) { - return new ArrayList<>(); - } - } - - public String toItemsJson(Object items) { - try { return OBJECT_MAPPER.writeValueAsString(items); } catch (Exception e) { return "[]"; } - } - - private static Integer toInt(Object v) { - if (v == null) return null; - if (v instanceof Number) return ((Number) v).intValue(); - try { return Integer.valueOf(v.toString().trim()); } catch (Exception e) { return null; } - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusGameService.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusGameService.java deleted file mode 100644 index 3714425..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusGameService.java +++ /dev/null @@ -1,526 +0,0 @@ -package com.ruoyi.game.olympus.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; -import com.ruoyi.admin.mapper.TtBoxRecordsMapper; -import com.ruoyi.admin.mapper.TtOrnamentsLevelMapper; -import com.ruoyi.admin.service.TtUserService; -import com.ruoyi.common.core.domain.R; -import com.ruoyi.message.service.MessageService; -import com.ruoyi.common.exception.ServiceException; -import com.ruoyi.common.redis.config.RedisLock; -import com.ruoyi.domain.common.constant.MessageCategoryEnum; -import com.ruoyi.domain.common.constant.TtAccountRecordSource; -import com.ruoyi.domain.common.constant.TtboxRecordSource; -import com.ruoyi.domain.entity.TtBoxRecords; -import com.ruoyi.domain.entity.sys.TtUser; -import com.ruoyi.domain.other.TtOrnamentsLevel; -import com.ruoyi.game.olympus.contract.request.SpinRequest; -import com.ruoyi.game.olympus.contract.response.SpinResponse; -import com.ruoyi.game.olympus.domain.*; -import com.ruoyi.game.olympus.enums.OlympusAudience; -import com.ruoyi.game.olympus.enums.OlympusModeType; -import com.ruoyi.game.olympus.mapper.*; -import com.ruoyi.game.olympus.model.PickedRound; -import com.ruoyi.game.olympus.model.cache.OlympusGameCache; -import com.ruoyi.game.olympus.repository.OlympusCacheRepository; -import com.ruoyi.game.olympus.repository.OlympusCycleRepository; -import com.ruoyi.game.olympus.repository.OlympusRewardPoolCacheRepository; -import com.ruoyi.user.service.ApiUserService; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; -import org.springframework.transaction.support.TransactionTemplate; - -import java.math.BigDecimal; -import java.util.*; -import java.util.concurrent.TimeUnit; - -import static com.ruoyi.admin.config.RedisConstants.COMMON_LOCK; - -/** - * Olympus 核心游戏服务:dospin 开局/播放/结算 + 多身份选池 + 按箱水位 + 白号隔离。 - */ -@Slf4j -@Service -public class OlympusGameService { - - @Autowired private OlympusBoxMapper boxMapper; - @Autowired private OlympusRoundPicker roundPicker; - @Autowired private OlympusBoxOrnamentMapper boxOrnamentMapper; - @Autowired private OlympusRecordMapper recordMapper; - @Autowired private OlympusUserMapper userMapper; - @Autowired private OlympusRoundMapper roundMapper; - @Autowired private OlympusPrizeRangeMapper prizeRangeMapper; - @Autowired private OlympusDistributionService distributionService; - @Autowired private OlympusCacheRepository cacheRepository; - @Autowired private OlympusCycleRepository cycleRepository; - @Autowired private OlympusRewardPoolCacheRepository rewardPoolRepository; - - @Autowired private TtUserService userService; - @Autowired private ApiUserService apiUserService; - @Autowired private RedisLock redisLock; - @Autowired private TransactionTemplate transactionTemplate; - - @Autowired(required = false) private TtBoxRecordsMapper boxRecordsMapper; - @Autowired(required = false) private TtOrnamentsLevelMapper ornamentsLevelMapper; - @Autowired private MessageService messageService; - - private static final com.fasterxml.jackson.databind.ObjectMapper MAPPER = - new com.fasterxml.jackson.databind.ObjectMapper(); - - /** 单局封顶:总倍率≥此值视为已 1000x 封顶 */ - private static final BigDecimal MAX_WIN_CAP_MUL = new BigDecimal("1000"); - - /** 免费旋转总旋数上限:15 起,每次重触发 +5,封顶 30 */ - private static final int MAX_FREE_SPINS = 30; - - // ============================================================ - // dospin 主入口 - // ============================================================ - - public SpinResponse doSpin(Long userId, SpinRequest req) throws Exception { - Boolean lock = redisLock.tryLock(COMMON_LOCK + userId, 3L, 7L, TimeUnit.SECONDS); - if (!Boolean.TRUE.equals(lock)) throw new RuntimeException("请求频繁,请稍后再试"); - try { - OlympusGameCache cache = cacheRepository.getCache(userId); - return cache == null ? startNewGame(userId, req != null ? req.getBoxId() : null) : continueGame(userId, cache); - } finally { - redisLock.unlock(COMMON_LOCK + userId); - } - } - - // ============================================================ - // 新游戏:扣费 + 多身份选池 + 缓存步骤 + 返回第一步 - // ============================================================ - - private SpinResponse startNewGame(Long userId, Long boxId) { - if (boxId == null) throw new IllegalArgumentException("boxId 不能为空"); - OlympusBox box = boxMapper.selectById(boxId); - if (box == null || !"1".equals(box.getStatus())) throw new IllegalArgumentException("箱子不可用"); - - OlympusModeType mode = OlympusModeType.fromBoxType(box.getBoxType()); - BigDecimal bet = coalesce(box.getBet()); - BigDecimal cost = bet.multiply(BigDecimal.valueOf(mode.getCostMultiplier())); - - R rechargeCheck = apiUserService.checkRecharge(userId.intValue()); - if (R.isError(rechargeCheck)) throw new ServiceException("玩家未充值不能游玩任何游戏", 703); - - TtUser player = userService.getById(userId); - if (player == null || player.getAccountAmount() == null || player.getAccountAmount().compareTo(cost) < 0) { - throw new ServiceException("账户余额不足", 702); - } - - // 身份解析:白号最高优先,压过 user_type - OlympusAudience audience = OlympusAudience.resolve(player); - boolean isBlank = audience.isWhite(); - - // 购买模式必须选有免费旋转的回合 - Boolean needFreeSpins = mode.isBuyMode() ? Boolean.TRUE : null; - - // 水位赔付上限:真实玩家按 (本箱水位 + 本局成本) × rtp 约束;白号 payoutCap=null(纯概率、不受水位) - BigDecimal payoutCap = null; - if (!isBlank) { - double ratio = box.getRtp() != null ? box.getRtp().doubleValue() : 0.8; - BigDecimal balance = rewardPoolRepository.getBoxBalance(boxId, audience.getCode()).add(cost); - if (balance.signum() < 0) balance = BigDecimal.ZERO; - payoutCap = scale2(balance.multiply(BigDecimal.valueOf(ratio))); - } - - PickedRound picked = roundPicker.pick(boxId, audience.getCode(), needFreeSpins, bet, payoutCap); - if (picked == null || picked.getRound() == null) throw new RuntimeException("该箱子暂无可用回合"); - - BigDecimal totalMul = coalesce(picked.getRound().getActualMul()); - BigDecimal totalWin = scale2(totalMul.multiply(bet)); - - List cachedSteps = new ArrayList<>(); - for (OlympusStep s : picked.getSteps()) { - OlympusGameCache.CachedStep cs = new OlympusGameCache.CachedStep(); - cs.setFreeSpinId(coalesce0(s.getFreeSpinId())); - cs.setCascadeSeq(coalesce0(s.getCascadeSeq())); - cs.setGridState(s.getGridState()); - cs.setMultiplierDetail(s.getMultiplierDetail()); - cs.setSymbolLinks(s.getSymbolLinks()); - cs.setWinAmount(s.getWinAmount()); - cs.setSpinWin(coalesce(s.getSpinWin())); - cs.setAccumulatorAfter(coalesce(s.getAccumulatorAfter())); - cs.setWinCapped(coalesce0(s.getWinCapped())); - cs.setBigwinLevel(s.getBigwinLevel()); - cs.setIsFinal(coalesce0(s.getIsFinal())); - cs.setRetriggerInfo(s.getRetriggerInfo()); - cs.setDropData(s.getDropData()); - cs.setFillData(s.getFillData()); - cs.setScatterTrigger(s.getScatterTrigger()); - cachedSteps.add(cs); - } - - OlympusGameCache cache = new OlympusGameCache(); - cache.setBoxId(boxId); - cache.setBoxType(box.getBoxType()); - cache.setBet(bet); - cache.setCost(cost); - cache.setRoundId(picked.getRound().getRoundId()); - cache.setTotalMul(totalMul); - cache.setFreeSpinCount(coalesce0(picked.getRound().getFreeSpinCount())); - cache.setStepIndex(1); - cache.setSteps(cachedSteps); - cache.setTotalWin(totalWin); - cache.setAudience(audience.getCode()); - cache.setBlankAccount(isBlank); - - boolean firstIsFinal = cachedSteps.size() == 1; - - transactionTemplate.executeWithoutResult(status -> { - R deductR = userService.updateUserAccount(userId.intValue(), cost.negate(), TtAccountRecordSource.OLYMPUS_SPIN); - if (R.isError(deductR)) throw new RuntimeException(deductR.getMsg()); - - OlympusRecord record = new OlympusRecord(); - record.setUserId(userId); - record.setBoxId(boxId); - record.setRoundId(picked.getRound().getRoundId()); - record.setCost(cost); - record.setTotalWin(BigDecimal.ZERO); - record.setModeType(box.getBoxType()); - record.setSettled(0); - record.setCreateTime(new Date()); - recordMapper.insert(record); - cache.setRecordId(record.getId()); - - // 仅固定阶段消耗对应 audience 一个回合(白号消耗自己的 white 库存,不碰其他身份) - if (!picked.isConverge()) { - boxOrnamentMapper.incrUsed(boxId, picked.getRound().getOrnamentId(), audience.getCode()); - } - // 开局即结算:回合结果已确定,立即将饰品写入背包(与扣费同事务,中途退出/刷新无损) - settleAtStart(userId, cache); - }); - - // 事务已提交(钱已扣、奖已落库)。以下全是 Redis 缓存/节奏账本/补桶,非资金操作: - // 一旦失败必须吞掉——否则接口返回 500,玩家以为没开局而重试,会再开一局、再扣一次钱。 - try { - cycleRepository.recordDraw(boxId, audience.getCode(), picked.getRound().getOrnamentId(), - coalesce(picked.getRound().getActualMul()), picked.isConverge()); - // 一轮 N 按该身份固定池分布反推(与 RoundPicker 一致,身份级独立) - int fixedSum = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience.getCode()).stream() - .mapToInt(bo -> bo.getCount() != null ? bo.getCount() : 0).sum(); - double cr = box.getRtpConvergeRatio() != null ? box.getRtpConvergeRatio().doubleValue() : 0.15; - int cycleN = fixedSum > 0 ? OlympusDistributionService.deriveTotalRounds(fixedSum, cr) : coalesce0(box.getTotalOrnaments()); - if (cycleN > 0 && cycleRepository.drawn(boxId, audience.getCode()) >= cycleN) { - cycleRepository.resetCycle(boxId, audience.getCode()); - distributionService.refillFromConfig(boxId, audience.getCode()); - } - - if (firstIsFinal) { - if (cache.getTotalWin().compareTo(BigDecimal.ZERO) <= 0) cache.setTotalWin(new BigDecimal("0.01")); - cacheRepository.clearCache(userId); - } else { - cacheRepository.setCache(userId, cache); - } - } catch (Exception e) { - log.error("olympus 开局后置处理失败(扣费与发奖已提交,不影响本局结果)userId={} boxId={}", userId, boxId, e); - } - // 单步局开局即播完,通知此刻发;多步局等末帧(continueGame) - if (firstIsFinal) sendWinNotice(userId, cache); - return buildStepResponse(cache, cachedSteps.get(0), 1, firstIsFinal); - } - - // ============================================================ - // 继续游戏:取下一步 - // ============================================================ - - private SpinResponse continueGame(Long userId, OlympusGameCache cache) { - int nextIdx = cache.getStepIndex() + 1; - if (nextIdx > cache.getSteps().size()) { - cacheRepository.clearCache(userId); - throw new RuntimeException("游戏已结束"); - } - OlympusGameCache.CachedStep step = cache.getSteps().get(nextIdx - 1); - cache.setStepIndex(nextIdx); - - if (nextIdx == cache.getSteps().size()) { - if (cache.getTotalWin().compareTo(BigDecimal.ZERO) <= 0) cache.setTotalWin(new BigDecimal("0.01")); - // 末帧已播完:这一局才算结束,战绩列表(我的/全服)才会展示 - recordMapper.update(null, new LambdaUpdateWrapper() - .eq(OlympusRecord::getId, cache.getRecordId()) - .set(OlympusRecord::getSettleSource, "finish")); - // 末帧到了才发结算通知(开局发会在动画播完前剧透),失败只记日志不影响本局 - sendWinNotice(userId, cache); - // 已在开局结算入背包,末帧仅揭晓;回填 boxRecordId 供前端选择保留/立即分解 - SpinResponse resp = buildStepResponse(cache, step, nextIdx, true); - cacheRepository.clearCache(userId); - return resp; - } - cacheRepository.setCache(userId, cache); - return buildStepResponse(cache, step, nextIdx, false); - } - - /** 结算通知(站内信):只在"这一局播完"的时刻发——单步局=开局,多步局=末帧。 - * 开局就发会在动画播完前把结果剧透给玩家(饰品开局已入背包,通知是唯一会提前泄露的通道)。 - * 多步局中途退出/断线未播完的局不发通知,饰品仍在背包,战绩按 30 分钟兜底照常展示。 */ - private void sendWinNotice(Long userId, OlympusGameCache cache) { - if (cache.getRecordId() == null) return; - try { - OlympusRecord rec = recordMapper.selectById(cache.getRecordId()); - if (rec == null || rec.getPrizeName() == null) return; - String priceStr = rec.getSettledWin() == null ? "0" : rec.getSettledWin().stripTrailingZeros().toPlainString(); - messageService.sendNotice( - userId, - MessageCategoryEnum.GAME_OLYMPUS.getTitle(), - "玫瑰女郎挑战结算:您本局获得饰品 " + rec.getPrizeName() + "(价值 ¥" + priceStr + ")", - MessageCategoryEnum.GAME_OLYMPUS.getCode()); - } catch (Exception e) { - log.error("olympus 结算通知发送失败 userId={} recordId={}", userId, cache.getRecordId(), e); - } - } - - // ============================================================ - // 开局即结算(事务中,饰品直接写入背包,中途退出/刷新无损) - // ============================================================ - - private void settleAtStart(Long userId, OlympusGameCache cache) { - BigDecimal totalWin = cache.getTotalWin(); - if (totalWin == null || totalWin.compareTo(BigDecimal.ZERO) <= 0) totalWin = new BigDecimal("0.01"); - BigDecimal win = scale2(totalWin); - - Map prize = findPrizeWithFallback(win); - Long prizeId = asLong(prize != null ? prize.get("id") : null); - String prizeName = prize != null ? (String) prize.get("name") : null; - Integer prizeGrade = prize != null && prize.get("grade") instanceof Integer ? (Integer) prize.get("grade") : 1; - String prizeImageUrl = prize != null ? (String) prize.get("imageUrl") : null; - String prizeExteriorName = prize != null ? (String) prize.get("exteriorName") : null; - String prizeLevelImg = prize != null ? (String) prize.get("levelImageUrl") : null; - - // 幂等守卫。settle_source:开局写 start,客户端播完末帧才改 finish(战绩列表只展示已结束的局); - // 单步局开局即播完,直接写 finish。 - boolean playedOut = cache.getSteps() == null || cache.getSteps().size() <= 1; - int guard = recordMapper.update(null, new LambdaUpdateWrapper() - .eq(OlympusRecord::getId, cache.getRecordId()) - .eq(OlympusRecord::getSettled, 0) - .set(OlympusRecord::getSettled, 1) - .set(OlympusRecord::getRewardType, "ornament") - .set(OlympusRecord::getTotalWin, totalWin) - .set(OlympusRecord::getSettledWin, win) - .set(OlympusRecord::getPrizeId, prizeId) - .set(OlympusRecord::getSettleSource, playedOut ? "finish" : "start") - .set(OlympusRecord::getPrizeName, prizeName) - .set(OlympusRecord::getPrizeImageUrl, prizeImageUrl)); - if (guard == 0) { cacheRepository.clearCache(userId); return; } - - // 写入背包 - Long boxRecordId = null; - if (boxRecordsMapper != null && prizeId != null) { - if (prizeLevelImg == null && ornamentsLevelMapper != null) { - TtOrnamentsLevel lv = ornamentsLevelMapper.selectById(prizeGrade); - if (lv != null) prizeLevelImg = lv.getLevelImg(); - } - TtBoxRecords bi = TtBoxRecords.builder() - .userId(userId.intValue()).holderUserId(userId.intValue()) - .boxId(cache.getBoxId() != null ? cache.getBoxId().intValue() : 0) - .boxName("OLYMPUS").boxPrice(cache.getCost()) - .ornamentId(prizeId).ornamentsPrice(win) - .ornamentName(prizeName).imageUrl(prizeImageUrl) - .marketHashName(prizeExteriorName != null ? prizeName + " (" + prizeExteriorName + ")" : prizeName) - .ornamentsLevelId(prizeGrade != null ? prizeGrade : 1) - .ornamentLevelImg(prizeLevelImg) - .source(TtboxRecordSource.OLYMPUS_BOX.getCode()) - .status(0).isShow(1).createTime(new Date()) - .build(); - boxRecordsMapper.insert(bi); - boxRecordId = bi.getId(); - cache.setBoxRecordId(bi.getId()); - // 注:结算通知不在这里发——开局就发会在动画播完前把结果剧透给玩家,改由 startNewGame/continueGame 在末帧发 - } - - // 注:饰品一律以 ornament 写入背包(开局即结算,断网也不丢)。自动分解由前端开关控制: - // 末帧前端调 /api/userPackSack/decompose 即按玫瑰女郎奖励(61)入账;调不动也只是留在背包,无资金风险。 - - // 更新用户统计 - OlympusUser user = userMapper.selectByUserId(userId); - if (user == null) { - user = new OlympusUser(); - user.setUserId(userId); - user.setTotalSpins(1L); - user.setTotalCost(cache.getCost()); - user.setTotalWin(totalWin); - user.setTotalFreeSpins(cache.getFreeSpinCount() != null && cache.getFreeSpinCount() > 0 ? 1 : 0); - user.setMaxWinMul(cache.getTotalMul()); - userMapper.insert(user); - } else { - user.setTotalSpins(coalesceL(user.getTotalSpins()) + 1L); - user.setTotalCost(coalesce(user.getTotalCost()).add(cache.getCost())); - user.setTotalWin(coalesce(user.getTotalWin()).add(totalWin)); - if (cache.getFreeSpinCount() != null && cache.getFreeSpinCount() > 0) - user.setTotalFreeSpins(coalesce0(user.getTotalFreeSpins()) + 1); - if (cache.getTotalMul() != null && cache.getTotalMul().compareTo(coalesce(user.getMaxWinMul())) > 0) - user.setMaxWinMul(cache.getTotalMul()); - userMapper.updateById(user); - } - - // 注:奖池注水与返奖记入由 startNewGame 事务提交后统一执行(开局即结算,白号隔离) - } - - // ============================================================ - // 断线重连 - // ============================================================ - - public Map hasActive(Long userId) { - OlympusGameCache cache = cacheRepository.getCache(userId); - // 开局即结算模型:无缓存即无进行中动画可恢复(饰品已在背包),直接返回 - if (cache == null) return Map.of("hasActive", false); - List steps = cache.getSteps(); - int played = Math.min(Math.max(cache.getStepIndex(), 0), steps.size()); - BigDecimal bet = coalesce(cache.getBet()); - // 已播累计直接取最后一步的 spin_win(本局累计),不能逐帧加 win_amount: - // win_amount 是"本旋内累计",同一旋的每一波都是同一个累计量的快照,相加会把中间波重复计入。 - BigDecimal playedWin = played > 0 - ? scale2(coalesce(steps.get(played - 1).getSpinWin()).multiply(bet)) - : BigDecimal.ZERO; - Map result = new HashMap<>(); - result.put("hasActive", true); - result.put("boxId", cache.getBoxId()); - result.put("boxType", cache.getBoxType()); - result.put("bet", cache.getBet()); - result.put("cost", cache.getCost()); - result.put("stepIndex", cache.getStepIndex()); - result.put("totalSteps", steps.size()); - result.put("freeSpinCount", cache.getFreeSpinCount()); - result.put("spinsRemaining", spinsRemainingAfter(steps, played)); - result.put("totalWin", scale2(playedWin)); - result.put("roundId", cache.getRoundId()); - if (played > 0) result.put("lastStep", buildStepResponse(cache, steps.get(played - 1), played, false)); - return result; - } - - // ============================================================ - // 构建单步响应 - // ============================================================ - - private SpinResponse buildStepResponse(OlympusGameCache cache, OlympusGameCache.CachedStep cs, int idx, boolean isFinal) { - BigDecimal bet = coalesce(cache.getBet()); - SpinResponse resp = new SpinResponse(); - resp.setState(isFinal ? "FINISH" : "PLAYING"); - resp.setTumbleNum(cs.getCascadeSeq()); - resp.setSpinNum(cs.getFreeSpinId()); - resp.setGridState(toJson(cs.getGridState())); - resp.setMultiplierDetail(toJson(cs.getMultiplierDetail())); - resp.setSymbolLinks(scaleMoneyList(cs.getSymbolLinks(), bet, new String[]{"base_score", "score", "win"})); - resp.setDropData(toJson(cs.getDropData())); - resp.setFillData(toJson(cs.getFillData())); - resp.setScatterTrigger(scaleMoneyMap(cs.getScatterTrigger(), bet, new String[]{"scatterWin", "startAmount"})); - resp.setWinAmount(scale2(coalesce(cs.getWinAmount()).multiply(bet))); - resp.setSpinWin(scale2(coalesce(cs.getSpinWin()).multiply(bet))); - // 炸弹累计倍数(未 ×bet):跨旋传递的累计器,断线重连只回最后一步,前端靠这一步的值接回累计 - resp.setAccumulator(scale2(coalesce(cs.getAccumulatorAfter()))); - if (cs.getRetriggerInfo() != null && !cs.getRetriggerInfo().isEmpty()) - resp.setRetriggerInfo(scaleMoneyMap(cs.getRetriggerInfo(), bet, new String[]{"scatterWin"})); - // 爆奖动画:py 已按单旋赢额判档写入 bigwin_level,直接透传 - if (cs.getBigwinLevel() != null && !cs.getBigwinLevel().isEmpty()) resp.setBigWinLevel(cs.getBigwinLevel()); - - resp.setGlobalStep(idx); - resp.setGlobalTotalSteps(cache.getSteps().size()); - // 剩余免费旋转:每帧都给(前端直接显示倒计,不用自己加减);1000x 封顶时下面覆盖成 0 - resp.setSpinsRemaining(spinsRemainingAfter(cache.getSteps(), idx)); - - if (isFinal) { - resp.setTotalWin(scale2(cache.getTotalWin())); - resp.setTotalMul(scale2(cache.getTotalMul())); - if (coalesce(cache.getTotalMul()).compareTo(MAX_WIN_CAP_MUL) >= 0) { - resp.setMaxWinReached(true); - resp.setSpinsRemaining(0); - } - // 开局已结算入背包:末帧揭晓饰品 + 回填 boxRecordId 供前端选择保留/立即分解 - try { - BigDecimal win = scale2(cache.getTotalWin()); - if (win.compareTo(BigDecimal.ZERO) <= 0) win = new BigDecimal("0.01"); - Map prize = findPrizeWithFallback(win); - if (prize != null && !prize.isEmpty()) resp.setPrizeInfo(prize); - } catch (Exception ignore) {} - resp.setBoxRecordId(cache.getBoxRecordId()); - } - return resp; - } - - // ============================================================ - // 工具 - // ============================================================ - - private Map findPrizeWithFallback(BigDecimal totalWin) { - Map prize = prizeRangeMapper.findPrizeByWin(totalWin); - if (prize != null && !prize.isEmpty()) { prize.put("isFallback", false); return prize; } - prize = prizeRangeMapper.findFallbackPrize(); - if (prize == null || prize.isEmpty()) prize = prizeRangeMapper.findLowestPrize(); - if (prize == null || prize.isEmpty()) prize = prizeRangeMapper.findHighestPrize(); - if (prize != null) prize.put("isFallback", true); - return prize; - } - - private static BigDecimal scale2(BigDecimal v) { return v == null ? BigDecimal.ZERO : v.setScale(2, java.math.RoundingMode.HALF_UP); } - private static BigDecimal coalesce(BigDecimal v) { return v != null ? v : BigDecimal.ZERO; } - private static int coalesce0(Integer v) { return v != null ? v : 0; } - private static long coalesceL(Long v) { return v != null ? v : 0L; } - - private static Long asLong(Object v) { - if (v == null) return null; - if (v instanceof Long) return (Long) v; - if (v instanceof Number) return ((Number) v).longValue(); - try { return Long.valueOf(v.toString()); } catch (Exception e) { return null; } - } - - private static Object toJson(String s) { - if (s == null || s.isEmpty()) return null; - try { return MAPPER.readTree(s); } catch (Exception e) { return s; } - } - - /** 剩余免费旋转 = 已发放 − 已播完的 FS 旋数(played = 算到第几步,含这一步;中间波帧不减,旋末帧才 −1)。 - * 已发放只算玩家**已经看到**的:触发帧的 freeSpins(=15) + 已播步里的 retriggerInfo.extraSpins(=5),封顶 30 - * (封顶后 3+ 个 S 的旋仍会写 retrigger_info=5 但不再加旋,所以这里必须夹 30)。 - * 例:第 2 旋末帧出 3 个 S → 15+5−2 = 18(那一帧 −1 与 +5 同时结算,净 +4)。*/ - private static int spinsRemainingAfter(List steps, int played) { - int awarded = 0, completed = 0; - for (int i = 0; i < played && i < steps.size(); i++) { - OlympusGameCache.CachedStep s = steps.get(i); - awarded += jsonInt(s.getScatterTrigger(), "freeSpins") + jsonInt(s.getRetriggerInfo(), "extraSpins"); - if (s.getIsFinal() == 1 && s.getFreeSpinId() > 0) completed++; - } - return Math.max(0, Math.min(awarded, MAX_FREE_SPINS) - completed); - } - - /** 取 JSON 对象里的整数字段(缺失 / null / 解析失败一律按 0) */ - private static int jsonInt(String json, String key) { - if (json == null || json.isEmpty()) return 0; - try { - com.fasterxml.jackson.databind.JsonNode node = MAPPER.readTree(json).get(key); - return node == null ? 0 : node.asInt(); - } catch (Exception e) { return 0; } - } - - /** 解析 JSON 数组,把指定金额字段 ×bet */ - @SuppressWarnings("unchecked") - private static Object scaleMoneyList(String json, BigDecimal bet, String[] keys) { - if (json == null || json.isEmpty()) return null; - try { - List> list = MAPPER.readValue(json, List.class); - for (Map m : list) scaleKeys(m, bet, keys); - return list; - } catch (Exception e) { return toJson(json); } - } - - @SuppressWarnings("unchecked") - private static Object scaleMoneyMap(String json, BigDecimal bet, String[] keys) { - if (json == null || json.isEmpty()) return null; - try { - Map m = MAPPER.readValue(json, Map.class); - scaleKeys(m, bet, keys); - return m; - } catch (Exception e) { return toJson(json); } - } - - private static void scaleKeys(Map m, BigDecimal bet, String[] keys) { - for (String key : keys) { - Object v = m.get(key); - if (v instanceof Number) { - m.put(key, BigDecimal.valueOf(((Number) v).doubleValue()) - .multiply(coalesce(bet)).setScale(2, java.math.RoundingMode.HALF_UP)); - } - } - } -} diff --git a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusRoundPicker.java b/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusRoundPicker.java deleted file mode 100644 index 82679f2..0000000 --- a/skins-service/game-olympus-service/src/main/java/com/ruoyi/game/olympus/service/OlympusRoundPicker.java +++ /dev/null @@ -1,279 +0,0 @@ -package com.ruoyi.game.olympus.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.ruoyi.game.olympus.domain.*; -import com.ruoyi.game.olympus.enums.OlympusModeType; -import com.ruoyi.game.olympus.mapper.*; -import com.ruoyi.game.olympus.model.PickedRound; -import com.ruoyi.game.olympus.repository.OlympusCycleRepository; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.stereotype.Service; - -import java.math.BigDecimal; -import java.math.RoundingMode; -import java.util.*; -import java.util.concurrent.ThreadLocalRandom; - -/** - * Olympus 回合选择器(多身份 + 两阶段 + 白号隔离)。 - * - * 每个身份(audience)读自己的桶分布与轮次账本: - * - 白号(payoutCap==null):纯概率原逻辑,不进两阶段、不受水位约束,但消耗自己的 white 库存(isConverge=false); - * - 真实身份(payoutCap!=null):固定阶段(payoutCap 过滤+剩余加权) → 收敛阶段(按 desiredNextMul 在水位内选桶)。 - * - * 消耗(used+1,仅固定阶段)与账本 recordDraw 由 OlympusGameService 在扣费事务内/提交后执行。 - */ -@Service -public class OlympusRoundPicker { - - @Autowired - private OlympusBoxOrnamentMapper boxOrnamentMapper; - @Autowired - private OlympusRoundMapper roundMapper; - @Autowired - private OlympusStepMapper stepMapper; - @Autowired - private OlympusDistributionService distributionService; - @Autowired - private OlympusBoxMapper boxMapper; - @Autowired - private OlympusCycleRepository cycleRepository; - - private static final double DEFAULT_CONVERGE_RATIO = 0.15; - - public PickedRound pick(Long boxId, String audience, Boolean needFreeSpins, BigDecimal bet, BigDecimal payoutCap) { - List dist = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); - if (dist.isEmpty()) throw new RuntimeException("Box " + boxId + " audience=" + audience + " 暂无饰品分布数据"); - - // 白号 / 无水位约束:纯概率原逻辑(消耗自己的库存,不受水位) - if (payoutCap == null) { - int totalRemaining = dist.stream().mapToInt(bo -> safe(bo.getRemaining())).sum(); - if (totalRemaining <= 0) { - distributionService.refillFromConfig(boxId, audience); - dist = boxOrnamentMapper.selectByBoxIdAndAudience(boxId, audience); - } - Integer ornId = weightedPickByRemaining(dist); - if (ornId == null) ornId = dist.get(0).getOrnamentId(); - OlympusRound r = resolveRound(boxId, ornId, needFreeSpins, bet, null); - return new PickedRound(r, stepMapper.selectByRoundId(r.getRoundId()), false); - } - - // 真实玩家:两阶段 - OlympusBox box = boxMapper.selectById(boxId); - double cm = 1.0, rtp = 0.8, convergeRatio = DEFAULT_CONVERGE_RATIO; - if (box != null) { - try { cm = OlympusModeType.fromBoxType(box.getBoxType()).getCostMultiplier(); } catch (Exception ignore) {} - if (box.getRtp() != null) rtp = box.getRtp().doubleValue(); - if (box.getRtpConvergeRatio() != null) convergeRatio = box.getRtpConvergeRatio().doubleValue(); - } - if (convergeRatio < 0) convergeRatio = 0; - if (convergeRatio > 0.9) convergeRatio = 0.9; - // 一轮总回合 N 按该身份固定池分布反推(身份级独立,各身份可不同),不使用箱子级 total_ornaments; - // 无分布时回退箱子级值兜底 - int fixedSum = dist.stream().mapToInt(bo -> safe(bo.getCount())).sum(); - int N; - if (fixedSum > 0) { - N = (int) Math.round(fixedSum / (1.0 - convergeRatio)); - } else { - N = (box != null && box.getTotalOrnaments() != null && box.getTotalOrnaments() > 0) - ? box.getTotalOrnaments() : fixedSum; - } - int fixedRounds = (int) Math.round(N * (1 - convergeRatio)); - long drawn = cycleRepository.drawn(boxId, audience); - int totalRemaining = dist.stream().mapToInt(bo -> safe(bo.getRemaining())).sum(); - - // 固定阶段 - if (drawn < fixedRounds && totalRemaining > 0) { - Integer ornId = pickFixedOrnId(dist, bet, payoutCap); - if (ornId == null) ornId = dist.get(0).getOrnamentId(); - OlympusRound r = resolveRound(boxId, ornId, needFreeSpins, bet, payoutCap); - return new PickedRound(r, stepMapper.selectByRoundId(r.getRoundId()), false); - } - - // 收敛阶段 - double targetMul = rtp * cm; - long remainingDraws = Math.max(1, N - drawn); - double remainingBudget = N * targetMul - cycleRepository.mulSum(boxId, audience); - double desiredNextMul = Math.max(0, remainingBudget / remainingDraws); - OlympusRound r = pickConverge(boxId, dist, needFreeSpins, bet, payoutCap, desiredNextMul); - return new PickedRound(r, stepMapper.selectByRoundId(r.getRoundId()), true); - } - - private Integer pickFixedOrnId(List dist, BigDecimal bet, BigDecimal payoutCap) { - List eligible = new ArrayList<>(); - for (OlympusBoxOrnament bo : dist) { - if (Math.max(0, safe(bo.getRemaining())) <= 0) continue; - if (expectPayout(bo, bet).compareTo(payoutCap) <= 0) eligible.add(bo); - } - return !eligible.isEmpty() ? weightedPickByRemaining(eligible) : minPayoutOrnId(dist, bet); - } - - private OlympusRound resolveRound(Long boxId, Integer ornId, Boolean needFreeSpins, BigDecimal bet, BigDecimal payoutCap) { - OlympusRound round = pickRoundWithCap(boxId, ornId, needFreeSpins, bet, payoutCap); - if (round == null) round = copyFromSameBucket(boxId, ornId, needFreeSpins); - if (round == null && needFreeSpins != null) { - round = roundMapper.pickByOrnament(boxId, ornId, null); - if (round == null) round = copyFromSameBucket(boxId, ornId, null); - } - if (round == null) throw new RuntimeException("暂无可用回合数据"); - return round; - } - - /** 桶内随机取一条,真实玩家还要 actual_mul×bet ≤ payoutCap(最多重抽 5 次,抽不到就退回原条) */ - private OlympusRound pickRoundWithCap(Long boxId, Integer ornId, Boolean needFreeSpins, BigDecimal bet, BigDecimal payoutCap) { - OlympusRound round = roundMapper.pickByOrnament(boxId, ornId, needFreeSpins); - if (round == null || payoutCap == null) return round; - BigDecimal b = bet != null ? bet : BigDecimal.ZERO; - if (withinCap(round, b, payoutCap)) return round; - for (int i = 0; i < 5; i++) { - OlympusRound r = roundMapper.pickByOrnament(boxId, ornId, needFreeSpins); - if (r != null && withinCap(r, b, payoutCap)) return r; - } - return round; - } - - private OlympusRound pickConverge(Long boxId, List dist, Boolean needFreeSpins, - BigDecimal bet, BigDecimal payoutCap, double desiredNextMul) { - BigDecimal b = (bet != null && bet.signum() > 0) ? bet : BigDecimal.ONE; - List allowed = new ArrayList<>(); - for (OlympusBoxOrnament bo : dist) { - if (expectPayout(bo, b).compareTo(payoutCap) <= 0) allowed.add(bo); - } - OlympusBoxOrnament chosen = allowed.isEmpty() ? minExpectMul(dist) : closestExpectMul(allowed, desiredNextMul); - Integer ornId = chosen.getOrnamentId(); - - double capMul = payoutCap.doubleValue() / b.doubleValue(); - double effectiveDesired = Math.min(desiredNextMul, capMul); - BigDecimal desiredBd = BigDecimal.valueOf(effectiveDesired).setScale(4, RoundingMode.HALF_UP); - BigDecimal capBd = BigDecimal.valueOf(capMul).setScale(4, RoundingMode.HALF_UP); - - OlympusRound round = roundMapper.pickClosestMul(boxId, ornId, needFreeSpins, desiredBd, capBd); - if (round == null && needFreeSpins != null) round = roundMapper.pickClosestMul(boxId, ornId, null, desiredBd, capBd); - if (round == null) round = copyFromSameBucket(boxId, ornId, needFreeSpins); - if (round == null) round = copyFromSameBucket(boxId, ornId, null); - if (round == null) throw new RuntimeException("收敛阶段暂无可用回合数据"); - return round; - } - - private OlympusBoxOrnament closestExpectMul(List list, double desired) { - List sorted = new ArrayList<>(list); - sorted.sort(Comparator.comparingDouble(bo -> { - double mul = bo.getExpectMul() != null ? bo.getExpectMul().doubleValue() : 0; - return Math.abs(mul - desired); - })); - int pickCount = Math.min(3, sorted.size()); - return sorted.get(ThreadLocalRandom.current().nextInt(pickCount)); - } - - private OlympusBoxOrnament minExpectMul(List list) { - OlympusBoxOrnament best = list.get(0); - double bestMul = Double.MAX_VALUE; - for (OlympusBoxOrnament bo : list) { - double mul = bo.getExpectMul() != null ? bo.getExpectMul().doubleValue() : 0; - if (mul < bestMul) { bestMul = mul; best = bo; } - } - return best; - } - - /** 同桶复制已有回合(新ID,动画数据原样拷贝),保证一定有回合可用。 */ - private OlympusRound copyFromSameBucket(Long boxId, Integer ornamentId, Boolean needFreeSpins) { - OlympusRound template = null; - for (int i = 0; i < 10; i++) { - template = roundMapper.pickByOrnament(boxId, ornamentId, needFreeSpins); - if (template != null && nz(template.getActualMul()).signum() > 0) break; - } - if (template == null) { - for (int i = 0; i < 10; i++) { - template = roundMapper.pickAnyByBox(boxId, needFreeSpins); - if (template != null && nz(template.getActualMul()).signum() > 0) break; - } - if (template == null) return null; - } - List templateSteps = stepMapper.selectByRoundId(template.getRoundId()); - Long newRoundId = nextRoundId(); - long newStepId = nextStepId(); - for (OlympusStep ts : templateSteps) { - OlympusStep ns = new OlympusStep(); - ns.setId(newStepId++); - ns.setRoundId(newRoundId); - ns.setFreeSpinId(ts.getFreeSpinId()); - ns.setCascadeSeq(ts.getCascadeSeq()); - ns.setGridState(ts.getGridState()); - ns.setMultiplierDetail(ts.getMultiplierDetail()); - ns.setSymbolLinks(ts.getSymbolLinks()); - ns.setAccumulatedSymbols(ts.getAccumulatedSymbols()); - ns.setSpinWinners(ts.getSpinWinners()); - ns.setRemovedPositions(ts.getRemovedPositions()); - ns.setDropData(ts.getDropData()); - ns.setFillData(ts.getFillData()); - ns.setScatterTrigger(ts.getScatterTrigger()); - ns.setRetriggerInfo(ts.getRetriggerInfo()); - ns.setBaseScore(ts.getBaseScore()); - ns.setWinAmount(ts.getWinAmount()); - ns.setAccumulatorBefore(ts.getAccumulatorBefore()); - ns.setAccumulatorAfter(ts.getAccumulatorAfter()); - ns.setSpinWin(ts.getSpinWin()); - ns.setWinCapped(ts.getWinCapped()); - ns.setBigwinLevel(ts.getBigwinLevel()); - ns.setIsFinal(ts.getIsFinal()); - stepMapper.insert(ns); - } - OlympusRound copy = new OlympusRound(); - copy.setRoundId(newRoundId); - copy.setBoxId(boxId); - copy.setOrnamentId(ornamentId); - copy.setActualMul(template.getActualMul()); - copy.setFreeSpinCount(template.getFreeSpinCount()); - copy.setTotalCascades(template.getTotalCascades()); - copy.setCreateTime(new Date()); - roundMapper.insert(copy); - return copy; - } - - private Long nextRoundId() { - List max = roundMapper.selectList( - new LambdaQueryWrapper().orderByDesc(OlympusRound::getRoundId).last("limit 1")); - return max.isEmpty() ? 1L : max.get(0).getRoundId() + 1; - } - - private Long nextStepId() { - List max = stepMapper.selectList( - new LambdaQueryWrapper().orderByDesc(OlympusStep::getId).last("limit 1")); - return max.isEmpty() ? 1L : max.get(0).getId() + 1; - } - - private BigDecimal expectPayout(OlympusBoxOrnament bo, BigDecimal bet) { - BigDecimal mul = bo.getExpectMul() != null ? bo.getExpectMul() : BigDecimal.ZERO; - return mul.multiply(bet != null ? bet : BigDecimal.ZERO); - } - - private Integer weightedPickByRemaining(List list) { - if (list.isEmpty()) return null; - int totalWeight = list.stream().mapToInt(bo -> Math.max(0, safe(bo.getRemaining()))).sum(); - if (totalWeight <= 0) return list.get(0).getOrnamentId(); - int r = ThreadLocalRandom.current().nextInt(totalWeight); - int acc = 0; - for (OlympusBoxOrnament bo : list) { - acc += Math.max(0, safe(bo.getRemaining())); - if (r < acc) return bo.getOrnamentId(); - } - return list.get(0).getOrnamentId(); - } - - private Integer minPayoutOrnId(List dist, BigDecimal bet) { - OlympusBoxOrnament best = null; - for (OlympusBoxOrnament bo : dist) { - if (Math.max(0, safe(bo.getRemaining())) <= 0) continue; - if (best == null || expectPayout(bo, bet).compareTo(expectPayout(best, bet)) < 0) best = bo; - } - return best != null ? best.getOrnamentId() : dist.get(0).getOrnamentId(); - } - - private boolean withinCap(OlympusRound round, BigDecimal bet, BigDecimal payoutCap) { - BigDecimal mul = nz(round.getActualMul()); - return mul.signum() > 0 && mul.multiply(bet).compareTo(payoutCap) <= 0; - } - - private static BigDecimal nz(BigDecimal v) { return v != null ? v : BigDecimal.ZERO; } - private static int safe(Integer v) { return v != null ? v : 0; } -} diff --git a/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusBoxOrnamentMapper.xml b/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusBoxOrnamentMapper.xml deleted file mode 100644 index 83dee2a..0000000 --- a/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusBoxOrnamentMapper.xml +++ /dev/null @@ -1,36 +0,0 @@ - - - - - - - - - INSERT INTO game_olympus_box_ornament (box_id, ornament_id, audience, count, used) VALUES - - (#{it.boxId}, #{it.ornamentId}, #{it.audience}, #{it.count}, #{it.used}) - - - - - DELETE FROM game_olympus_box_ornament WHERE box_id = #{boxId} AND audience = #{audience} - - - - UPDATE game_olympus_box_ornament SET used = used + 1 - WHERE box_id = #{boxId} AND ornament_id = #{ornamentId} AND audience = #{audience} - - - - UPDATE game_olympus_box_ornament SET used = 0 - WHERE box_id = #{boxId} AND audience = #{audience} - - - diff --git a/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusRoundMapper.xml b/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusRoundMapper.xml deleted file mode 100644 index 2300ef5..0000000 --- a/skins-service/game-olympus-service/src/main/resources/mapper/olympus/OlympusRoundMapper.xml +++ /dev/null @@ -1,42 +0,0 @@ - - - - - - - - - - - - - -