优化中奖权重计算方式

This commit is contained in:
2026-03-12 17:17:00 +08:00
parent 064ce06393
commit 7e4ba86afa
25 changed files with 2344 additions and 403 deletions

View File

@@ -0,0 +1,427 @@
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
namespace app\dice\logic\reward;
use app\dice\model\reward\DiceReward;
use app\dice\model\reward_config\DiceRewardConfig;
use plugin\saiadmin\exception\ApiException;
use support\think\Db;
/**
* 奖励对照逻辑层DiceReward
* 权重 1-10000区分顺时针/逆时针,修改后刷新 DiceReward 缓存
*/
class DiceRewardLogic
{
private const WEIGHT_MIN = 1;
private const WEIGHT_MAX = 10000;
/** 档位键 */
private const TIER_KEYS = ['T1', 'T2', 'T3', 'T4', 'T5', 'BIGWIN'];
/**
* 分页列表(按方向筛选,关联 dice_reward_config 展示 grid_number、ui_text、real_ev、remark
* @param int $direction 0=顺时针 1=逆时针
* @param array{tier?: string, orderField?: string, orderType?: string} $where tier 档位筛选
* @param int $page
* @param int $limit
* @return array{total: int, per_page: int, current_page: int, data: array}
*/
public function getListWithConfig(int $direction, array $where, int $page = 1, int $limit = 10): array
{
$tier = isset($where['tier']) ? trim((string) $where['tier']) : '';
$orderField = isset($where['orderField']) && $where['orderField'] !== '' ? (string) $where['orderField'] : 'r.tier';
$orderType = isset($where['orderType']) && strtoupper((string) $where['orderType']) === 'DESC' ? 'desc' : 'asc';
$query = DiceReward::alias('r')
->where('r.direction', $direction)
->field('r.id,r.tier,r.direction,r.end_index,r.weight,r.grid_number,r.start_index,r.ui_text,r.real_ev,r.remark,r.type,r.create_time,r.update_time')
->order($orderField, $orderType)
->order('r.end_index', 'asc');
if ($tier !== '') {
$query->where('r.tier', $tier);
}
$paginator = $query->paginate($limit, false, ['page' => $page]);
$arr = $paginator->toArray();
$data = isset($arr['data']) ? $arr['data'] : $arr['records'] ?? [];
$total = (int) ($arr['total'] ?? 0);
$perPage = (int) ($arr['per_page'] ?? $limit);
$currentPage = (int) ($arr['current_page'] ?? $page);
foreach ($data as $i => $row) {
if (isset($row['id']) && $row['id'] !== '' && $row['id'] !== null) {
$data[$i]['id'] = (int) $row['id'];
} else {
$data[$i]['id'] = isset($row['end_index']) ? (int) $row['end_index'] : 0;
}
$data[$i]['start_index'] = isset($row['start_index']) && $row['start_index'] !== '' && $row['start_index'] !== null
? (int) $row['start_index']
: 0;
}
return [
'total' => $total,
'per_page' => $perPage,
'current_page' => $currentPage,
'data' => $data,
];
}
/**
* 按单方向批量更新权重(仅更新当前方向的 weight并刷新缓存
* @param int $direction 0=顺时针 1=逆时针
* @param array<int, array{id: int, weight: int}> $items id 为 end_indexDiceRewardConfig.id
*/
public function batchUpdateWeightsByDirection(int $direction, array $items): void
{
if (empty($items)) {
return;
}
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$id = isset($item['id']) ? (int) $item['id'] : 0;
$weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
if ($id <= 0) {
throw new ApiException('存在无效的配置ID');
}
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
$tier = DiceRewardConfig::where('id', $id)->value('tier');
if ($tier === null || $tier === '') {
throw new ApiException('配置ID ' . $id . ' 不存在或档位为空');
}
$tier = (string) $tier;
$affected = DiceReward::where('tier', $tier)->where('direction', $direction)->where('end_index', $id)->update(['weight' => $weight]);
if ($affected === 0) {
$m = new DiceReward();
$m->tier = $tier;
$m->direction = $direction;
$m->end_index = $id;
$m->weight = $weight;
$m->save();
}
}
DiceReward::refreshCache();
}
/**
* 按档位+单方向返回列表(用于权重编辑弹窗:当前方向下按档位分组的配置+权重)
* @param int $direction 0=顺时针 1=逆时针
* @return array<string, array> 键 T1|T2|...|BIGWIN值为该档位下带 weight 的行数组
*/
public function getListGroupedByTierForDirection(int $direction): array
{
$configInstance = DiceRewardConfig::getCachedInstance();
$byTier = $configInstance['by_tier'] ?? [];
$rewardInstance = DiceReward::getCachedInstance();
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
$result = [];
foreach (self::TIER_KEYS as $tier) {
$result[$tier] = [];
$rows = $byTier[$tier] ?? [];
$dirRows = $byTierDirection[$tier][$direction] ?? [];
$weightMap = [];
foreach ($dirRows as $r) {
$eid = isset($r['end_index']) ? (int) $r['end_index'] : 0;
$weightMap[$eid] = isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : 1;
}
foreach ($rows as $row) {
$id = isset($row['id']) ? (int) $row['id'] : 0;
$result[$tier][] = [
'id' => $id,
'grid_number' => $row['grid_number'] ?? 0,
'ui_text' => $row['ui_text'] ?? '',
'real_ev' => $row['real_ev'] ?? 0,
'remark' => $row['remark'] ?? '',
'tier' => $tier,
'weight' => $weightMap[$id] ?? 1,
];
}
}
return $result;
}
/**
* 按档位+方向返回 DiceReward 列表(用于权重配比弹窗),直接读 dice_reward 表,不依赖 config
* 每行含 reward_id(DiceReward 主键,用于按 id 更新权重)、id(end_index 展示用)、grid_number、ui_text、real_ev、remark、weight
*
* @return array<string, array{0: array, 1: array}>
*/
public function getListGroupedByTierWithDirection(): array
{
$rewardInstance = DiceReward::getCachedInstance();
$byTierDirection = $rewardInstance['by_tier_direction'] ?? [];
$result = [];
foreach (self::TIER_KEYS as $tier) {
$result[$tier] = [0 => [], 1 => []];
foreach ([0, 1] as $direction) {
$rows = $byTierDirection[$tier][$direction] ?? [];
foreach ($rows as $r) {
$result[$tier][$direction][] = [
'reward_id' => isset($r['id']) ? (int) $r['id'] : 0,
'id' => isset($r['end_index']) ? (int) $r['end_index'] : 0,
'grid_number' => isset($r['grid_number']) ? (int) $r['grid_number'] : 0,
'ui_text' => (string) ($r['ui_text'] ?? ''),
'real_ev' => $r['real_ev'] ?? 0,
'remark' => (string) ($r['remark'] ?? ''),
'weight' => isset($r['weight']) ? max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, (int) $r['weight'])) : self::WEIGHT_MIN,
];
}
}
}
return $result;
}
/**
* 批量更新权重:直接按 DiceReward 主键 id 更新 weight不依赖 direction/grid_number
*
* @param array<int, array{id: int, weight: int}> $items 每项 id 为 dice_reward 表主键weight 为 1-10000
* @throws ApiException
*/
public function batchUpdateWeights(array $items): void
{
if (empty($items)) {
return;
}
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$id = isset($item['id']) ? (int) $item['id'] : 0;
$weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
if ($id <= 0) {
throw new ApiException('存在无效的 DiceReward id');
}
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
DiceReward::where('id', $id)->update(['weight' => $weight]);
}
DiceReward::refreshCache();
}
/** BIGWIN 权重范围0=0% 中奖10000=100% 中奖grid_number=5/30 固定 100% 不可改 */
private const BIGWIN_WEIGHT_MAX = 10000;
/**
* 按 grid_number 获取 BIGWIN 档位权重(取顺时针方向,用于编辑展示)
* 若 DiceReward 无该点数则 5/30 返回 10000其余返回 0
*/
public function getBigwinWeightByGridNumber(int $gridNumber): int
{
$inst = DiceReward::getCachedInstance();
$rows = $inst['by_tier_direction']['BIGWIN'][DiceReward::DIRECTION_CLOCKWISE] ?? [];
foreach ($rows as $row) {
if ((int) ($row['grid_number'] ?? 0) === $gridNumber) {
return min(self::BIGWIN_WEIGHT_MAX, (int) ($row['weight'] ?? self::BIGWIN_WEIGHT_MAX));
}
}
return in_array($gridNumber, [5, 30], true) ? self::BIGWIN_WEIGHT_MAX : 0;
}
/**
* 更新 BIGWIN 档位某点数的权重(顺/逆时针同时更新0=0% 中奖10000=100% 中奖
*/
public function updateBigwinWeight(int $gridNumber, int $weight): void
{
$weight = min(self::BIGWIN_WEIGHT_MAX, max(0, $weight));
foreach ([DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE] as $direction) {
// 优先更新已存在记录
$affected = DiceReward::where('tier', 'BIGWIN')
->where('direction', $direction)
->where('grid_number', $gridNumber)
->update(['weight' => $weight]);
// 若不存在 BIGWIN 记录,则按当前 BIGWIN 配置懒加载创建一条 dice_reward 记录
if ($affected === 0) {
$config = DiceRewardConfig::where('tier', 'BIGWIN')
->where('grid_number', $gridNumber)
->find();
if ($config) {
$m = new DiceReward();
$m->tier = 'BIGWIN';
$m->direction = $direction;
$m->grid_number = (int) $gridNumber;
// 对于 BIGWIN仅需保证 real_ev、weight、grid_numberstart_index/end_index 取当前配置 id 即可
$m->start_index = (int) $config->id;
$m->end_index = (int) $config->id;
$m->ui_text = (string) ($config->ui_text ?? '');
$m->real_ev = (float) ($config->real_ev ?? 0);
$m->remark = (string) ($config->remark ?? '');
$m->type = $config->type ?? null;
$m->weight = $weight > 0 ? $weight : self::WEIGHT_MIN;
$m->save();
}
}
}
DiceReward::refreshCache();
}
/** 盘面格数(用于顺时针/逆时针计算 end_index */
private const BOARD_SIZE = 26;
/** 点数摇取范围5-30顺时针与逆时针均需创建 */
private const GRID_NUMBER_MIN = 5;
private const GRID_NUMBER_MAX = 30;
/**
* 创建奖励对照:先清空 dice_reward 表,再按两种方向为点数 5-30 生成记录。
*
* DiceReward 记录数据规则(与 config 通过 end_index 关联):
* - 方向direction = 0顺时针/ 1逆时针
* - 摇取点数grid_number
* - 起始索引start_index = DiceRewardConfig::where('grid_number', $grid_number)->first()->id
* - 结束索引顺时针end_index = ($start_index + $grid_number) % 26对 26 取余)
* - 结束索引逆时针end_index = ($start_index - $grid_number >= 0) ? ($start_index - $grid_number) : (26 + $start_index - $grid_number)
* - 奖励档位tier = DiceRewardConfig::where('id', $end_index)->first()->tier
* - 显示uiui_text = DiceRewardConfig::where('id', $end_index)->first()->ui_text
* - 实际中奖real_ev = DiceRewardConfig::where('id', $end_index)->first()->real_ev
* - 备注remark = DiceRewardConfig::where('id', $end_index)->first()->remark
* - 类型type = DiceRewardConfig::where('id', $end_index)->first()->type-2=唯一惩罚,-1=抽水,0=回本,1=再来一次,2=小赚,3=大奖格)
* - weight 默认 1后续在权重编辑弹窗设置
*
* 例如顺时针摇取点数为 5 时start_index = 配置中 grid_number=5 对应格位的 id
* 结束位置 = (起始位置 + grid_number) % 26再取该位置的 config 的 id 作为 end_index。
* 使用「按 id 排序后的盘面位置 0-25」做环形计算避免 config.id 非连续时取模结果找不到;
* 唯一键为 (direction, grid_number),保证每个点数、每个方向各一条记录,不因 end_index 相同而覆盖。
*
* @return array{created_clockwise: int, created_counterclockwise: int, updated_clockwise: int, updated_counterclockwise: int, skipped: int}
* @throws ApiException
*/
public function createRewardReferenceFromConfig(): array
{
$list = DiceRewardConfig::order('id', 'asc')->select()->toArray();
if (empty($list)) {
throw new ApiException('奖励配置为空,请先维护 dice_reward_config');
}
$configCount = count($list);
if ($configCount < self::BOARD_SIZE) {
throw new ApiException(
'奖励配置需覆盖 26 个格位id 0-25 或 1-26当前仅 ' . $configCount . ' 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照'
);
}
$table = (new DiceReward())->getTable();
Db::execute('DELETE FROM `' . $table . '`');
DiceReward::refreshCache();
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos],避免 config.id 非 0-25/1-26 时取模结果找不到
$gridToPosition = [];
foreach ($list as $pos => $row) {
$gn = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
if ($gn >= self::GRID_NUMBER_MIN && $gn <= self::GRID_NUMBER_MAX && !isset($gridToPosition[$gn])) {
$gridToPosition[$gn] = $pos;
}
}
$createdCw = 0;
$createdCcw = 0;
$updatedCw = 0;
$updatedCcw = 0;
$skipped = 0;
for ($gridNumber = self::GRID_NUMBER_MIN; $gridNumber <= self::GRID_NUMBER_MAX; $gridNumber++) {
if (!isset($gridToPosition[$gridNumber])) {
$skipped++;
continue;
}
$startPos = $gridToPosition[$gridNumber];
$startRow = $list[$startPos];
$startId = isset($startRow['id']) ? (int) $startRow['id'] : 0;
$endPosCw = ($startPos + $gridNumber) % self::BOARD_SIZE;
$endPosCcw = $startPos - $gridNumber >= 0 ? $startPos - $gridNumber : self::BOARD_SIZE + $startPos - $gridNumber;
$configCw = $list[$endPosCw] ?? null;
$configCcw = $list[$endPosCcw] ?? null;
$endIdCw = $configCw !== null && isset($configCw['id']) ? (int) $configCw['id'] : 0;
$endIdCcw = $configCcw !== null && isset($configCcw['id']) ? (int) $configCcw['id'] : 0;
if ($configCw !== null) {
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
if ($tier !== '') {
$payloadCw = [
'tier' => $tier,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => $endIdCw,
'ui_text' => $configCw['ui_text'] ?? '',
'real_ev' => $configCw['real_ev'] ?? null,
'remark' => $configCw['remark'] ?? '',
'type' => isset($configCw['type']) ? (int) $configCw['type'] : 0,
];
$existing = DiceReward::where('direction', DiceReward::DIRECTION_CLOCKWISE)->where('grid_number', $gridNumber)->find();
if ($existing) {
DiceReward::where('id', $existing->id)->update($payloadCw);
$updatedCw++;
} else {
$m = new DiceReward();
$m->tier = $tier;
$m->direction = DiceReward::DIRECTION_CLOCKWISE;
$m->end_index = $endIdCw;
$m->weight = self::WEIGHT_MIN;
$m->grid_number = $gridNumber;
$m->start_index = $startId;
$m->ui_text = $configCw['ui_text'] ?? '';
$m->real_ev = $configCw['real_ev'] ?? null;
$m->remark = $configCw['remark'] ?? '';
$m->type = isset($configCw['type']) ? (int) $configCw['type'] : 0;
$m->save();
$createdCw++;
}
}
}
if ($configCcw !== null) {
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
if ($tier !== '') {
$payloadCcw = [
'tier' => $tier,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => $endIdCcw,
'ui_text' => $configCcw['ui_text'] ?? '',
'real_ev' => $configCcw['real_ev'] ?? null,
'remark' => $configCcw['remark'] ?? '',
'type' => isset($configCcw['type']) ? (int) $configCcw['type'] : 0,
];
$existing = DiceReward::where('direction', DiceReward::DIRECTION_COUNTERCLOCKWISE)->where('grid_number', $gridNumber)->find();
if ($existing) {
DiceReward::where('id', $existing->id)->update($payloadCcw);
$updatedCcw++;
} else {
$m = new DiceReward();
$m->tier = $tier;
$m->direction = DiceReward::DIRECTION_COUNTERCLOCKWISE;
$m->end_index = $endIdCcw;
$m->weight = self::WEIGHT_MIN;
$m->grid_number = $gridNumber;
$m->start_index = $startId;
$m->ui_text = $configCcw['ui_text'] ?? '';
$m->real_ev = $configCcw['real_ev'] ?? null;
$m->remark = $configCcw['remark'] ?? '';
$m->type = isset($configCcw['type']) ? (int) $configCcw['type'] : 0;
$m->save();
$createdCcw++;
}
}
}
}
DiceReward::refreshCache();
return [
'created_clockwise' => $createdCw,
'created_counterclockwise' => $createdCcw,
'updated_clockwise' => $updatedCw,
'updated_counterclockwise' => $updatedCcw,
'skipped' => $skipped,
];
}
}