Files
dafuweng-saiadmin6.x/server/app/dice/logic/reward/DiceRewardLogic.php

794 lines
34 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
// +----------------------------------------------------------------------
// | saiadmin [ saiadmin快速开发框架 ]
// +----------------------------------------------------------------------
namespace app\dice\logic\reward;
use app\dice\helper\AdminScopeHelper;
use app\dice\helper\ConfigScopeEditHelper;
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 $adminInfo = null,
$requestDeptId = null
): 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';
$keepIds = $this->resolveDedupedRewardIdsByGrid($direction, $tier, $adminInfo, $requestDeptId);
if ($keepIds === []) {
return [
'total' => 0,
'per_page' => $limit,
'current_page' => $page,
'data' => [],
];
}
$query = DiceReward::alias('r')
->whereIn('r.id', $keepIds)
->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.grid_number', 'asc');
$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,
];
}
/**
* 列表去重:每个方向、每个色子点数(5-30)仅保留一条(取 id 最大),避免历史重复数据导致 104 条
* @return int[]
*/
private function resolveDedupedRewardIdsByGrid(
int $direction,
string $tier,
?array $adminInfo,
$requestDeptId
): array {
$dedupeQuery = DiceReward::alias('rd')
->field('MAX(rd.id) AS keep_id')
->where('rd.direction', $direction)
->whereBetween('rd.grid_number', [5, 30]);
if ($adminInfo !== null) {
AdminScopeHelper::applyConfigScope($dedupeQuery, $adminInfo, $requestDeptId, 'rd.dept_id');
}
if ($tier !== '') {
$dedupeQuery->where('rd.tier', $tier);
}
$rows = $dedupeQuery->group('rd.grid_number')->select()->toArray();
$ids = [];
foreach ($rows as $row) {
$id = isset($row['keep_id']) ? (int) $row['keep_id'] : 0;
if ($id > 0) {
$ids[] = $id;
}
}
return $ids;
}
/**
* 按单方向批量更新权重(仅更新当前方向的 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, ?int $deptId = null): 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('Invalid config ID exists');
}
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
$configQuery = DiceRewardConfig::where('id', $id);
if ($deptId !== null) {
ConfigScopeEditHelper::applyDeptIdWhere($configQuery, $deptId);
}
$tier = $configQuery->value('tier');
if ($tier === null || $tier === '') {
throw new ApiException(\app\api\util\ApiLang::translateParams('配置ID %s 不存在或档位为空', [$id]));
}
$tier = (string) $tier;
$rewardQuery = DiceReward::where('tier', $tier)
->where('direction', $direction)
->where('end_index', $id);
if ($deptId !== null) {
ConfigScopeEditHelper::applyDeptIdWhere($rewardQuery, $deptId);
}
$affected = $rewardQuery->update(['weight' => $weight]);
if ($affected === 0) {
$m = new DiceReward();
$m->tier = $tier;
$m->direction = $direction;
$m->end_index = $id;
$m->weight = $weight;
if ($deptId !== null && $deptId > 0) {
$m->dept_id = $deptId;
}
$m->save();
}
}
DiceReward::refreshCache($deptId);
}
/**
* 按档位+单方向返回列表(用于权重编辑弹窗:当前方向下按档位分组的配置+权重)
* @param int $direction 0=顺时针 1=逆时针
* @return array<string, array> 键 T1|T2|...|BIGWIN值为该档位下带 weight 的行数组
*/
public function getListGroupedByTierForDirection(int $direction, ?int $deptId = null): array
{
$configInstance = DiceRewardConfig::getCachedInstance($deptId);
$byTier = $configInstance['by_tier'] ?? [];
$rewardInstance = DiceReward::getCachedInstance($deptId);
$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(?int $deptId = null): array
{
$rewardInstance = DiceReward::getCachedInstance($deptId);
$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, ?int $deptId = null): void
{
if (empty($items)) {
return;
}
foreach ($items as $item) {
if (!is_array($item)) {
continue;
}
$id = isset($item['id']) ? (int) $item['id'] : 0;
if ($id <= 0) {
$id = isset($item['reward_id']) ? (int) $item['reward_id'] : 0;
}
if ($id <= 0) {
throw new ApiException('Invalid DiceReward id exists');
}
$weight = isset($item['weight']) ? (int) $item['weight'] : self::WEIGHT_MIN;
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $weight));
$query = DiceReward::where('id', $id);
if ($deptId !== null) {
if (AdminScopeHelper::isTemplateDeptId($deptId)) {
$query->where(function ($q) {
$q->where('dept_id', AdminScopeHelper::DEFAULT_TEMPLATE_DEPT)
->whereOr('dept_id', null);
});
} else {
$query->where('dept_id', $deptId);
}
}
$model = $query->find();
if ($model !== null) {
$model->weight = $weight;
$model->save();
}
}
DiceReward::refreshCache($deptId);
}
/** 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 $deptId = null): int
{
$inst = DiceReward::getCachedInstance($deptId);
$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% 中奖
* 表 dice_reward 唯一键为 (direction, grid_number),同一点数同一方向仅一条记录,故先按该键查找再更新,避免重复插入
*/
public function updateBigwinWeight(int $gridNumber, int $weight, ?int $deptId = null): void
{
$weight = min(self::BIGWIN_WEIGHT_MAX, max(0, $weight));
if ($deptId === null) {
$deptId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
}
$configQuery = DiceRewardConfig::where('tier', 'BIGWIN')->where('grid_number', $gridNumber);
ConfigScopeEditHelper::applyDeptIdWhere($configQuery, $deptId);
$config = $configQuery->find();
if (! $config) {
return;
}
$configArr = $config->toArray();
foreach ([DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE] as $direction) {
// 按唯一键 (direction, grid_number) 查找,存在则更新,不存在则插入
$rowQuery = DiceReward::where('direction', $direction)->where('grid_number', $gridNumber);
ConfigScopeEditHelper::applyDeptIdWhere($rowQuery, $deptId);
$row = $rowQuery->find();
if ($row) {
$row->tier = 'BIGWIN';
$row->weight = $weight > 0 ? $weight : self::WEIGHT_MIN;
$row->start_index = (int) ($configArr['id'] ?? $row->start_index);
$row->end_index = (int) ($configArr['id'] ?? $row->end_index);
$row->ui_text = (string) ($configArr['ui_text'] ?? $row->ui_text);
$row->real_ev = (float) ($configArr['real_ev'] ?? $row->real_ev);
$row->remark = (string) ($configArr['remark'] ?? $row->remark);
$row->type = $configArr['type'] ?? $row->type;
$row->save();
} else {
$m = new DiceReward();
$m->tier = 'BIGWIN';
$m->direction = $direction;
$m->grid_number = (int) $gridNumber;
$m->start_index = (int) ($configArr['id'] ?? 0);
$m->end_index = (int) ($configArr['id'] ?? 0);
$m->ui_text = (string) ($configArr['ui_text'] ?? '');
$m->real_ev = (float) ($configArr['real_ev'] ?? 0);
$m->remark = (string) ($configArr['remark'] ?? '');
$m->type = $configArr['type'] ?? null;
$m->weight = $weight > 0 ? $weight : self::WEIGHT_MIN;
if (!AdminScopeHelper::isTemplateDeptId($deptId)) {
$m->dept_id = $deptId;
}
$m->save();
}
}
DiceReward::refreshCache($deptId);
}
/** 盘面格数(用于顺时针/逆时针计算 end_index */
private const BOARD_SIZE = 26;
/** 点数摇取范围5-30顺时针与逆时针均需创建 */
private const GRID_NUMBER_MIN = 5;
private const GRID_NUMBER_MAX = 30;
/**
* 预览:按当前 dice_reward_config 计算将要生成的 dice_reward不写库
* 若当前 dice_reward 与计算结果完全一致,则标记 unchanged=true并返回现有权重导入时将复用旧权重
*
* @return array{unchanged: bool, skipped: int, preview: array<string, array{0: array, 1: array}>}
* @throws ApiException
*/
public function createRewardReferencePreviewFromConfig(?int $deptId = null): array
{
$normalizedDeptId = $deptId;
$list = $this->loadConfigListForReference($normalizedDeptId);
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
$compare = $this->compareReferenceRows($computed['rows'], $existing);
$unchanged = $compare['unchanged'];
$specialGrids = [5, 10, 15, 20, 25, 30];
$previewRows = [];
foreach ($computed['rows'] as $row) {
$key = $row['direction'] . ':' . $row['grid_number'];
$gridNumber = isset($row['grid_number']) ? (int) $row['grid_number'] : 0;
$weight = $unchanged
? self::WEIGHT_MIN
: (in_array($gridNumber, $specialGrids, true) ? self::WEIGHT_MIN : 100);
$oldStart = null;
$oldEnd = null;
$oldTier = null;
$oldRemark = null;
$oldWeight = null;
if (isset($existing[$key])) {
$oldStart = isset($existing[$key]['start_index']) ? (int) $existing[$key]['start_index'] : null;
$oldEnd = isset($existing[$key]['end_index']) ? (int) $existing[$key]['end_index'] : null;
$oldTier = isset($existing[$key]['tier']) ? (string) $existing[$key]['tier'] : null;
$oldRemark = isset($existing[$key]['remark']) ? (string) $existing[$key]['remark'] : null;
$oldWeight = isset($existing[$key]['weight']) ? (int) $existing[$key]['weight'] : null;
}
// 映射未变化时:通常复用旧权重;但若旧权重为 1 且非特殊点数,则按新默认建议展示为 100方便管理员快速落配置
if ($unchanged && $oldWeight !== null) {
$oldWeight = (int) $oldWeight;
$weight = max(self::WEIGHT_MIN, min(self::WEIGHT_MAX, $oldWeight));
if ($weight === self::WEIGHT_MIN && !in_array($gridNumber, $specialGrids, true)) {
$weight = 100;
}
}
$diffChanged = false;
$diffFields = [];
if ($oldStart === null || $oldEnd === null || $oldTier === null) {
$diffChanged = true;
$diffFields[] = 'new';
} else {
if ((int) $oldStart !== (int) ($row['start_index'] ?? 0)) {
$diffChanged = true;
$diffFields[] = 'start_index';
}
if ((int) $oldEnd !== (int) ($row['end_index'] ?? 0)) {
$diffChanged = true;
$diffFields[] = 'end_index';
}
if (trim((string) $oldTier) !== trim((string) ($row['tier'] ?? ''))) {
$diffChanged = true;
$diffFields[] = 'tier';
}
if (trim((string) $oldRemark) !== trim((string) ($row['remark'] ?? ''))) {
$diffChanged = true;
$diffFields[] = 'remark';
}
}
$previewRows[] = array_merge($row, [
'weight' => $weight,
'old_start_index' => $oldStart,
'old_end_index' => $oldEnd,
'old_tier' => $oldTier,
'old_remark' => $oldRemark,
'old_weight' => $oldWeight,
'diff_changed' => $diffChanged,
'diff_fields' => $diffFields,
]);
}
return [
'unchanged' => $unchanged,
'skipped' => $computed['skipped'],
'preview' => $this->groupReferenceRowsByTierWithDirection($previewRows),
];
}
/**
* 创建奖励对照:先清空 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 = 按本条对照档位(推断后 T1-T5的默认备注T1 大奖、T2 小赚…),非落点格盘面 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(?int $deptId = null): array
{
$normalizedDeptId = $deptId;
$list = $this->loadConfigListForReference($normalizedDeptId);
$computed = $this->computeReferenceRowsFromConfigList($list, $normalizedDeptId);
$existing = $this->loadExistingRewardRowsForReference($normalizedDeptId);
$compare = $this->compareReferenceRows($computed['rows'], $existing);
if ($compare['unchanged']) {
return [
'created_clockwise' => 0,
'created_counterclockwise' => 0,
'updated_clockwise' => 0,
'updated_counterclockwise' => 0,
'skipped' => $computed['skipped'],
'unchanged' => true,
];
}
$table = (new DiceReward())->getTable();
if ($normalizedDeptId === null) {
Db::table($table)->whereNull('dept_id')->delete();
} else {
Db::table($table)->where('dept_id', $normalizedDeptId)->delete();
}
$createdCw = 0;
$createdCcw = 0;
foreach ($computed['rows'] as $row) {
$m = new DiceReward();
$m->tier = $row['tier'];
$m->direction = (int) $row['direction'];
$m->end_index = (int) $row['end_index'];
$m->weight = self::WEIGHT_MIN;
$m->grid_number = (int) $row['grid_number'];
$m->start_index = (int) $row['start_index'];
$m->ui_text = (string) ($row['ui_text'] ?? '');
$m->real_ev = $row['real_ev'] ?? null;
$m->remark = (string) ($row['remark'] ?? '');
$m->type = isset($row['type']) ? (int) $row['type'] : 0;
if ($normalizedDeptId !== null) {
$m->dept_id = $normalizedDeptId;
}
$m->save();
if ((int) $row['direction'] === DiceReward::DIRECTION_CLOCKWISE) {
$createdCw++;
} else {
$createdCcw++;
}
}
DiceReward::refreshCache($normalizedDeptId ?? AdminScopeHelper::DEFAULT_TEMPLATE_DEPT);
return [
'created_clockwise' => $createdCw,
'created_counterclockwise' => $createdCcw,
'updated_clockwise' => 0,
'updated_counterclockwise' => 0,
'skipped' => $computed['skipped'],
'unchanged' => false,
];
}
/**
* 读取奖励配置(按 id asc并把模板 dept 转为 null
* @return array<int, array<string, mixed>>
*/
private function loadConfigListForReference(?int &$deptId): array
{
$configQuery = DiceRewardConfig::order('id', 'asc');
if ($deptId === null || $deptId === AdminScopeHelper::DEFAULT_TEMPLATE_DEPT) {
$templateId = AdminScopeHelper::DEFAULT_TEMPLATE_DEPT;
$configQuery->where(function ($q) use ($templateId) {
$q->where('dept_id', $templateId)->whereOr('dept_id', 'null');
});
$deptId = null;
} else {
$configQuery->where('dept_id', $deptId);
}
$list = $configQuery->select()->toArray();
if (empty($list)) {
throw new ApiException('Reward config is empty, please maintain dice_reward_config first');
}
if (count($list) < self::BOARD_SIZE) {
throw new ApiException(
\app\api\util\ApiLang::translateParams(
'奖励配置需覆盖 26 个格位id 0-25 或 1-26当前仅 %s 条,无法完整生成 5-30 共26个点数、顺时针与逆时针的奖励对照',
[count($list)]
)
);
}
return $list;
}
/**
* 计算 5-30 两个方向的对照行(不含权重)
* @param array<int, array<string, mixed>> $list
* @return array{rows: array<int, array<string, mixed>>, skipped: int}
*/
private function computeReferenceRowsFromConfigList(array $list, ?int $deptId): array
{
// 按 id 排序后,盘面位置 0..25 对应 $list[$pos]
$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;
}
}
$rows = [];
$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;
if ($configCw !== null) {
$tier = isset($configCw['tier']) ? trim((string) $configCw['tier']) : '';
if ($tier !== '') {
$rows[] = $this->buildReferenceRowFromLandingConfig(
$tier,
$configCw,
DiceReward::DIRECTION_CLOCKWISE,
$gridNumber,
$startId
);
}
}
if ($configCcw !== null) {
$tier = isset($configCcw['tier']) ? trim((string) $configCcw['tier']) : '';
if ($tier !== '') {
$rows[] = $this->buildReferenceRowFromLandingConfig(
$tier,
$configCcw,
DiceReward::DIRECTION_COUNTERCLOCKWISE,
$gridNumber,
$startId
);
}
}
}
return ['rows' => $rows, 'skipped' => $skipped];
}
/**
* 对照表落点行:档位按落点格 real_ev 推断;备注随**本条对照档位**T1 大奖 / T2 小赚 …),
* 与奖励配置页「按结算金额匹配档位备注」一致,不拷贝落点格盘面备注(盘面可保留「大奖格」等细分文案)。
*
* @param array<string, mixed> $landingConfig
* @return array<string, mixed>
*/
private function buildReferenceRowFromLandingConfig(
string $tier,
array $landingConfig,
int $direction,
int $gridNumber,
int $startId
): array {
$realEv = isset($landingConfig['real_ev']) ? (float) $landingConfig['real_ev'] : 0.0;
if ($tier !== 'BIGWIN') {
$inferred = $this->inferTierFromRealEv($realEv);
if ($inferred !== '') {
$tier = $inferred;
}
}
return [
'tier' => $tier,
'direction' => $direction,
'weight' => self::WEIGHT_MIN,
'grid_number' => $gridNumber,
'start_index' => $startId,
'end_index' => isset($landingConfig['id']) ? (int) $landingConfig['id'] : 0,
'ui_text' => $landingConfig['ui_text'] ?? '',
'real_ev' => $landingConfig['real_ev'] ?? null,
'remark' => $this->defaultRemarkForTier($tier),
'type' => isset($landingConfig['type']) ? (int) $landingConfig['type'] : 0,
];
}
/**
* 按结算金额推断档位(与前端 generateIndexByRules 一致)
*/
private function inferTierFromRealEv(float $realEv): string
{
if ($realEv > 2) {
return 'T1';
}
if ($realEv > 1) {
return 'T2';
}
if ($realEv > 0) {
return 'T3';
}
if ($realEv < 0) {
return 'T4';
}
return 'T5';
}
/**
* 档位默认备注
*/
private function defaultRemarkForTier(string $tier): string
{
return match ($tier) {
'T1', 'BIGWIN' => '大奖',
'T2' => '小赚',
'T3' => '抽水',
'T4' => '惩罚',
'T5' => '再来一次',
default => '',
};
}
/**
* 读出当前 dice_reward用于对比/复用权重。key = "direction:grid_number"
* @return array<string, array<string, mixed>>
*/
private function loadExistingRewardRowsForReference(?int $deptId): array
{
$query = DiceReward::whereIn('grid_number', range(self::GRID_NUMBER_MIN, self::GRID_NUMBER_MAX))
->whereIn('direction', [DiceReward::DIRECTION_CLOCKWISE, DiceReward::DIRECTION_COUNTERCLOCKWISE]);
if ($deptId === null) {
$query->whereNull('dept_id');
} else {
$query->where('dept_id', $deptId);
}
$rows = $query->select()->toArray();
$map = [];
foreach ($rows as $r) {
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
$gn = isset($r['grid_number']) ? (int) $r['grid_number'] : 0;
$map[$dir . ':' . $gn] = $r;
}
return $map;
}
/**
* 对比 computed 与 existing 是否完全一致(忽略权重)
* @param array<int, array<string, mixed>> $computedRows
* @param array<string, array<string, mixed>> $existingMap
* @return array{unchanged: bool}
*/
private function compareReferenceRows(array $computedRows, array $existingMap): array
{
if (empty($computedRows)) {
return ['unchanged' => false];
}
foreach ($computedRows as $row) {
$key = $row['direction'] . ':' . $row['grid_number'];
if (!isset($existingMap[$key])) {
return ['unchanged' => false];
}
$ex = $existingMap[$key];
$same =
((int) ($ex['start_index'] ?? 0) === (int) ($row['start_index'] ?? 0)) &&
((int) ($ex['end_index'] ?? 0) === (int) ($row['end_index'] ?? 0)) &&
(trim((string) ($ex['tier'] ?? '')) === trim((string) ($row['tier'] ?? ''))) &&
(trim((string) ($ex['remark'] ?? '')) === trim((string) ($row['remark'] ?? '')));
if (!$same) {
return ['unchanged' => false];
}
}
return ['unchanged' => true];
}
/**
* 将行按 tier -> {0:[],1:[]} 组织,便于前端展示(与 weightRatioList 输出结构一致)
* @param array<int, array<string, mixed>> $rows
* @return array<string, array{0: array, 1: array}>
*/
private function groupReferenceRowsByTierWithDirection(array $rows): array
{
$result = [];
foreach (self::TIER_KEYS as $tier) {
$result[$tier] = [0 => [], 1 => []];
}
foreach ($rows as $r) {
$tier = isset($r['tier']) ? trim((string) $r['tier']) : '';
if ($tier === '' || !isset($result[$tier])) {
continue;
}
$dir = isset($r['direction']) ? (int) $r['direction'] : 0;
$dir = $dir === 1 ? 1 : 0;
$result[$tier][$dir][] = [
'reward_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,
'start_index' => isset($r['start_index']) ? (int) $r['start_index'] : 0,
'tier' => (string) ($r['tier'] ?? ''),
'old_start_index' => $r['old_start_index'] ?? null,
'old_end_index' => $r['old_end_index'] ?? null,
'old_tier' => $r['old_tier'] ?? null,
'old_weight' => $r['old_weight'] ?? null,
'diff_changed' => (bool) ($r['diff_changed'] ?? false),
'diff_fields' => $r['diff_fields'] ?? [],
];
}
return $result;
}
}