785 lines
33 KiB
PHP
785 lines
33 KiB
PHP
<?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_index(DiceRewardConfig.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;
|
||
$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;
|
||
$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';
|
||
}
|
||
}
|
||
|
||
$previewRows[] = array_merge($row, [
|
||
'weight' => $weight,
|
||
'old_start_index' => $oldStart,
|
||
'old_end_index' => $oldEnd,
|
||
'old_tier' => $oldTier,
|
||
'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
|
||
* - 显示ui:ui_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(?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];
|
||
}
|
||
|
||
/**
|
||
* 对照表落点行:档位按结算金额推断,备注与奖励配置页规则一致
|
||
*
|
||
* @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'] ?? '')));
|
||
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;
|
||
}
|
||
}
|