model = new \app\common\model\GameConfig(); return null; } /** * 将「可访问管理员 ID」转为「其负责的渠道 ID」,供 queryBuilder 使用 channel_id IN (...) * * @return list */ protected function getDataLimitAdminIds(): array { if (!$this->dataLimit || !$this->auth || $this->auth->isSuperAdmin()) { return []; } $adminIds = parent::getDataLimitAdminIds(); if ($adminIds === []) { return []; } $channelIds = Db::name('game_channel')->where('admin_id', 'in', $adminIds)->column('id'); if ($channelIds === []) { return [-1]; } return array_values(array_unique($channelIds)); } /** * @throws Throwable */ protected function _add(): Response { if ($this->request && $this->request->method() === 'POST') { $data = $this->request->post(); if (!$data) { return $this->error(__('Parameter %s can not be empty', [''])); } $data = $this->applyInputFilter($data); $data = $this->excludeFields($data); $err = $this->validateGameWeightPayload($data, null); if ($err !== null) { return $this->error($err); } if (!$this->auth->isSuperAdmin()) { $allowedChannelIds = $this->getDataLimitAdminIds(); $cid = $data['channel_id'] ?? null; if ($cid === null || $cid === '') { return $this->error(__('Parameter %s can not be empty', ['channel_id'])); } if ($allowedChannelIds !== [] && !in_array($cid, $allowedChannelIds)) { return $this->error(__('You have no permission')); } } $result = false; $this->model->startTrans(); try { if ($this->modelValidate) { $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); if (class_exists($validate)) { $validate = new $validate(); if ($this->modelSceneValidate) { $validate->scene('add'); } $validate->check($data); } } $result = $this->model->save($data); $this->model->commit(); } catch (Throwable $e) { $this->model->rollback(); return $this->error($e->getMessage()); } if ($result !== false) { return $this->success(__('Added successfully')); } return $this->error(__('No rows were added')); } return $this->error(__('Parameter error')); } /** * @throws Throwable */ protected function _edit(): Response { $pk = $this->model->getPk(); $id = $this->request ? ($this->request->post($pk) ?? $this->request->get($pk)) : null; $row = $this->model->find($id); if (!$row) { return $this->error(__('Record not found')); } $dataLimitAdminIds = $this->getDataLimitAdminIds(); if ($dataLimitAdminIds && !in_array($row[$this->dataLimitField], $dataLimitAdminIds)) { return $this->error(__('You have no permission')); } if ($this->request && $this->request->method() === 'POST') { $data = $this->request->post(); if (!$data) { return $this->error(__('Parameter %s can not be empty', [''])); } $data = $this->applyInputFilter($data); $data = $this->excludeFields($data); if (!$this->auth->isSuperAdmin()) { $data['channel_id'] = $row['channel_id']; $data['group'] = $row['group']; $data['name'] = $row['name']; $data['title'] = $row['title']; } elseif (!isset($data['name']) || $data['name'] === '' || $data['name'] === null) { // JSON/表单未带 name 时回退库值,避免走「每项≤10000」 $data['name'] = $row['name'] ?? ''; } // 超管编辑时若未传 group,用库值参与 game_weight 校验(与 name 回退一致) if ($this->auth->isSuperAdmin() && trim((string) ($data['group'] ?? '')) === '') { $data['group'] = (string) ($row['group'] ?? ''); } $err = $this->validateGameWeightPayload($data, $row['value'] ?? null); if ($err !== null) { return $this->error($err); } $result = false; $this->model->startTrans(); try { if ($this->modelValidate) { $validate = str_replace("\\model\\", "\\validate\\", get_class($this->model)); if (class_exists($validate)) { $validate = new $validate(); if ($this->modelSceneValidate) { $validate->scene('edit'); } $data[$pk] = $row[$pk]; $validate->check($data); } } $result = $row->save($data); $this->model->commit(); } catch (Throwable $e) { $this->model->rollback(); return $this->error($e->getMessage()); } if ($result !== false) { return $this->success(__('Update successful')); } return $this->error(__('No rows updated')); } return $this->success('', [ 'row' => $row ]); } /** * 与前端 gameWeightFixed.normalizeGameWeightConfigName 一致:trim、去括号后说明、小写 */ private function normalizeGameWeightConfigName(string $raw): string { $s = trim($raw); if (preg_match('/^([^((]+)/u', $s, $m)) { $s = trim($m[1]); } return strtolower($s); } /** * 解析 game_weight JSON 里每一项的权重:支持 int/float、数字字符串(与前端 JSON.stringify 一致常为字符串) */ private function parseGameWeightScalarToFloat(mixed $v): ?float { if (is_int($v) || is_float($v)) { return (float) $v; } if (is_bool($v) || is_array($v) || $v === null) { return null; } $s = trim((string) $v); if ($s === '') { return null; } $f = filter_var($s, FILTER_VALIDATE_FLOAT); if ($f !== false) { return $f; } $i = filter_var($s, FILTER_VALIDATE_INT); if ($i !== false) { return (float) $i; } return null; } /** * game_weight:tier/kill 每项≤100 且和必须=100;bigwin 每项 0~10000;编辑时键不可改 * * @param array $data */ private function validateGameWeightPayload(array &$data, ?string $originalValue): ?string { $group = strtolower(trim((string) ($data['group'] ?? ''))); if ($group !== 'game_weight') { return null; } $rawName = (string) ($data['name'] ?? ''); $rawName = preg_replace('/[\x{200B}-\x{200D}\x{FEFF}]/u', '', $rawName); $name = $this->normalizeGameWeightConfigName($rawName); $rawValue = $data['value'] ?? null; $valueWasArray = false; if (is_array($rawValue)) { $decoded = $rawValue; $valueWasArray = true; } elseif (is_string($rawValue)) { $s = trim($rawValue); if (str_starts_with($s, "\xEF\xBB\xBF")) { $s = substr($s, 3); } if ($s === '') { return __('Parameter error'); } $decoded = json_decode($s, true); // 双重 JSON 字符串(外层已解析为字符串时) if (is_string($decoded)) { $decoded = json_decode(trim($decoded), true); } } else { return __('Parameter error'); } if (!is_array($decoded)) { return __('Parameter error'); } // 骰子键 5~30 结构一律按大奖 0~10000 校验(避免库中 name 与 JSON 不一致时误走 tier/kill 单项上限) $diceBigwin = $this->gameWeightDecodedIsBigwinDiceKeys($decoded); // 归一化后的 name + 原始串包含 default_bigwin_weight(兼容说明后缀、异常空格),避免误走单项上限 $rawLower = strtolower($rawName); $useBigwinRules = $diceBigwin || ($name === self::BIGWIN_WEIGHT_NAME) || str_contains($rawLower, 'default_bigwin_weight'); $keys = []; $numbers = []; foreach ($decoded as $item) { if (!is_array($item)) { return __('Parameter error'); } foreach ($item as $k => $v) { $keys[] = (string) $k; $num = $this->parseGameWeightScalarToFloat($v); if ($num === null) { return __('Game config weight value must be numeric'); } if ($useBigwinRules) { if ($num < 0 || $num > 10000) { return __('Game config bigwin weight each 0 10000'); } $ks = strval($k); if (($ks === '5' || $ks === '30') && abs($num - 10000.0) > 0.000001) { return __('Game config bigwin weight locked 5 30'); } } else { if ($num > 100) { return __('Game config weight each value must not exceed 100'); } } $numbers[] = $num; } } if (count($numbers) === 0) { return __('Parameter %s can not be empty', ['value']); } if ($originalValue !== null && $originalValue !== '') { $oldKeys = $this->extractGameWeightKeys($originalValue); if ($oldKeys !== $keys) { return __('Game config weight keys cannot be modified'); } } if (!$useBigwinRules && in_array($name, self::WEIGHT_SUM_100_NAMES, true)) { $sum = array_sum($numbers); if (abs($sum - 100.0) > 0.000001) { return __('Game config weight sum must equal 100'); } } if ($valueWasArray) { $data['value'] = json_encode($decoded, JSON_UNESCAPED_UNICODE); } return null; } /** * 是否为 default_bigwin_weight 的固定骰子键集合(与前端 BIGWIN_WEIGHT_KEYS 一致) * * @param array $decoded */ private function gameWeightDecodedIsBigwinDiceKeys(array $decoded): bool { $keys = []; foreach ($decoded as $item) { if (!is_array($item)) { return false; } foreach (array_keys($item) as $k) { $keys[] = trim((string) $k); } } if (count($keys) !== 6) { return false; } $ints = []; foreach ($keys as $k) { if ($k === '' || !ctype_digit($k)) { return false; } $ints[] = (int) $k; } sort($ints, SORT_NUMERIC); return $ints === [5, 10, 15, 20, 25, 30]; } /** * @return list */ private function extractGameWeightKeys(string $value): array { $decoded = json_decode($value, true); if (!is_array($decoded)) { return []; } $keys = []; foreach ($decoded as $item) { if (!is_array($item)) { continue; } foreach ($item as $k => $_) { $keys[] = (string) $k; } } return $keys; } /** * 查看 * @throws Throwable */ protected function _index(): Response { // 如果是 select 则转发到 select 方法,若未重写该方法,其实还是继续执行 index if ($this->request && $this->request->get('select')) { return $this->select($this->request); } /** * 1. withJoin 不可使用 alias 方法设置表别名,别名将自动使用关联模型名称(小写下划线命名规则) * 2. 以下的别名设置了主表别名,同时便于拼接查询参数等 * 3. paginate 数据集可使用链式操作 each(function($item, $key) {}) 遍历处理 */ list($where, $alias, $limit, $order) = $this->queryBuilder(); $res = $this->model ->withJoin($this->withJoinTable, $this->withJoinType) ->with($this->withJoinTable) ->visible(['channel' => ['name']]) ->alias($alias) ->where($where) ->order($order) ->paginate($limit); return $this->success('', [ 'list' => $res->items(), 'total' => $res->total(), 'remark' => get_route_remark(), ]); } /** * 若需重写查看、编辑、删除等方法,请复制 @see \app\admin\library\traits\Backend 中对应方法至此进行重写 */ }