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 $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 键 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 */ 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 $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} * @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 * - 显示ui:ui_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> */ 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> $list * @return array{rows: array>, 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 $landingConfig * @return array */ 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> */ 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> $computedRows * @param array> $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> $rows * @return 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; } }