first(); $current = $this->policy(false); $pending = $this->policy(true); return [ 'id' => $setting ? (int)$setting->id : null, 'enabled' => (int)($setting->enabled ?? 0), 'share_domain' => (string)($setting->share_domain ?? ''), 'valid_member_min_deposit' => $this->money($setting->valid_member_min_deposit ?? '20'), 'valid_member_min_effective_bet' => isset($setting->valid_member_min_effective_bet) ? $this->money($setting->valid_member_min_effective_bet) : null, 'initialized' => DB::table('share_system_state')->where('key', 'initialized_at')->exists(), 'effective_date' => $pending['effective_date'] ?? null, 'current_policy' => $current, 'pending_policy' => $pending, ]; } public function saveSettings(array $input): array { // Retired fields have no effect, including when submitted by an older client. unset($input['benefit_enabled'], $input['activation_date']); foreach (self::RETIRED as $key) { if (array_key_exists($key, $input)) $this->invalid($key, '旧分享参数已移除,请使用有效会员条件及分类返佣规则'); } foreach (['effective_date', 'current_policy', 'pending_policy'] as $key) { if (array_key_exists($key, $input)) $this->invalid($key, '生效日期及版本由系统生成,不可指定'); } $data = Validator::make($input, [ 'enabled' => ['required', Rule::in([0, 1])], 'share_domain' => ['required', 'string', 'max:500'], 'valid_member_min_deposit' => ['required', 'regex:/^\d{1,14}(?:\.\d{1,2})?$/'], 'valid_member_min_effective_bet' => array_merge(['required'], self::MONEY), ])->validate(); $data['share_domain'] = $this->origin($data['share_domain']); return DB::transaction(function () use ($data) { $this->lockConfiguration(); $setting = ShareSetting::query()->lockForUpdate()->first() ?: new ShareSetting(); $before = $setting->exists ? $setting->toArray() : []; if (!DB::table('share_system_state')->where('key', 'initialized_at')->exists()) { $this->invalid('initialized', '会员分享关系初始化完成后才可保存佣金配置'); } $setting->fill($data + OperationAuditService::actor()); $setting->save(); $policy = $this->writablePolicy(); foreach (['enabled', 'valid_member_min_deposit', 'valid_member_min_effective_bet'] as $field) { $policy[$field] = $setting->getRawOriginal($field); } $this->savePolicy($policy); OperationAuditService::record('share_setting', (int)$setting->id, 'update', $before, $setting); return $this->settings(); }); } public function rateRules(?string $category = null): array { if ($category !== null && !array_key_exists($category, ShareDefaults::categories())) $this->invalid('category', '无效的游戏分类'); $current = $this->policy(false); $pending = $this->policy(true); $categories = []; foreach (ShareDefaults::categories() as $key => $label) { if ($category !== null && $category !== $key) continue; $categories[] = ['category' => $key, 'name' => $label, 'current_rules' => $current['rules'][$key] ?? [], 'pending_rules' => $pending['rules'][$key] ?? null]; } return ['current_policy_id' => $current['id'] ?? null, 'current_effective_date' => $current['effective_date'] ?? null, 'pending_policy_id' => $pending['id'] ?? null, 'pending_effective_date' => $pending['effective_date'] ?? null, 'categories' => $categories]; } public function saveRateRules(string $category, array $rules): array { $data = Validator::make(['category' => $category, 'rules' => $rules], [ 'category' => ['required', Rule::in(array_keys(ShareDefaults::categories()))], 'rules' => ['required', 'array', 'min:1', 'max:100'], 'rules.*.min_valid_members' => ['required', 'integer', 'min:0', 'max:2147483647'], 'rules.*.min_performance' => array_merge(['required'], self::MONEY), 'rules.*.rate' => array_merge(['required'], self::RATE), ])->validate(); $normalized = []; foreach ($data['rules'] as $row) { $normalized[] = ['min_valid_members' => (int)$row['min_valid_members'], 'min_performance' => bcadd((string)$row['min_performance'], '0', 10), 'rate' => bcadd((string)$row['rate'], '0', 4)]; } usort($normalized, fn ($a, $b) => bccomp($a['min_performance'], $b['min_performance'], 10) ?: ($a['min_valid_members'] <=> $b['min_valid_members'])); $previous = null; foreach ($normalized as $row) { if ($previous !== null) { if (bccomp($row['min_performance'], $previous['min_performance'], 10) === 0 && $row['min_valid_members'] === $previous['min_valid_members']) { $this->invalid('rules', '同一业绩门槛和人数门槛的档位不可重复'); } if ($row['min_valid_members'] < $previous['min_valid_members']) { $this->invalid('rules', '随业绩档位提高,有效直属人数门槛不可降低'); } if (bccomp($row['rate'], $previous['rate'], 4) < 0) { $this->invalid('rules', '随业绩和人数档位提高,返佣比例不可降低'); } } $previous = $row; } return DB::transaction(function () use ($category, $normalized) { $this->lockConfiguration(); $policy = $this->writablePolicy(); $before = $policy; $policy['rules'][$category] = $normalized; $id = $this->savePolicy($policy); OperationAuditService::record('share_policy', $id, 'rate_rules', $before, $policy); return $this->rateRules($category); }); } public function levels(): array { $total = '0'; $rows = []; foreach (DB::table('share_levels')->orderBy('level')->get() as $row) { $total = bcadd($total, (string)$row->incremental_commission, 10); $rows[] = ['id' => (int)$row->id, 'level' => (int)$row->level, 'name' => $row->name, 'image' => $row->image, 'incremental_commission' => $this->money($row->incremental_commission), 'cumulative_commission' => $this->money($total)]; } return ['data' => $rows]; } public function saveLevel(array $input): array { $data = Validator::make($input, [ 'id' => ['nullable', 'integer', 'min:1'], 'level' => ['required', 'integer', 'min:1', 'max:2147483647'], 'name' => ['required', 'string', 'max:255'], 'image' => ['present', 'nullable', 'string', 'max:1000'], 'incremental_commission' => array_merge(['required'], self::MONEY), ])->validate(); $data['image'] = (string)($data['image'] ?? ''); return DB::transaction(function () use ($data) { $this->lockConfiguration(); $id = (int)($data['id'] ?? 0); $old = $id ? DB::table('share_levels')->where('id', $id)->lockForUpdate()->first() : null; if ($id && !$old) $this->invalid('id', '分享等级不存在'); if ($old && (int)$old->level !== (int)$data['level']) $this->invalid('level', '已有等级不可更改等级编号'); if ((int)$data['level'] === 1 && bccomp((string)$data['incremental_commission'], '0', 10) !== 0) $this->invalid('incremental_commission', '等级1的增量佣金必须为0'); if (DB::table('share_levels')->where('level', $data['level'])->when($id, fn ($q) => $q->where('id', '!=', $id))->exists()) $this->invalid('level', '分享等级编号已存在'); unset($data['id']); $now = $this->now(); if ($id) DB::table('share_levels')->where('id', $id)->update($data + ['updated_at' => $now]); else $id = DB::table('share_levels')->insertGetId($data + ['created_at' => $now, 'updated_at' => $now]); $this->promoteMembers(); OperationAuditService::record('share_level', $id, $old ? 'update' : 'create', (array)$old, $data); return $this->levels(); }); } public function deleteLevel(int $id): void { DB::transaction(function () use ($id) { $this->lockConfiguration(); $row = DB::table('share_levels')->where('id', $id)->lockForUpdate()->first(); if (!$row) $this->invalid('id', '分享等级不存在'); if ((int)$row->level === 1) $this->invalid('id', '初始等级不可删除'); if (DB::table('share_members')->where('level', $row->level)->exists()) $this->invalid('id', '该等级已有会员,不可删除'); DB::table('share_levels')->where('id', $id)->delete(); $this->promoteMembers(); OperationAuditService::record('share_level', $id, 'delete', (array)$row, []); }); } private function promoteMembers(): void { $threshold = '0'; foreach (DB::table('share_levels')->orderBy('level')->get() as $row) { $threshold = bcadd($threshold, (string)$row->incremental_commission, 10); DB::table('share_members')->where('level', '<', $row->level) ->whereIn('user_id', DB::table('share_accounts')->select('user_id')->where('earned_amount', '>=', $threshold)) ->update(['level' => $row->level, 'updated_at' => $this->now()]); } } private function writablePolicy(): array { $row = DB::table('share_policies')->where('effective_date', $this->tomorrow())->lockForUpdate()->first() ?: DB::table('share_policies')->where('effective_date', '<=', $this->today())->orderByDesc('effective_date')->first(); $policy = $row ? (array)$row : ['enabled' => 0, 'valid_member_min_deposit' => '20', 'valid_member_min_effective_bet' => null, 'indirect_bonus_rate' => null, 'rules' => ShareDefaults::rules()]; unset($policy['id'], $policy['created_at'], $policy['updated_at'], $policy['benefit_enabled']); // Keep archived coefficients on past versions only; new versions use the receiver's bracket rate by depth. $policy['indirect_bonus_rate'] = null; $policy['rules'] = is_string($policy['rules']) ? json_decode($policy['rules'], true, 512, JSON_THROW_ON_ERROR) : $policy['rules']; $policy['effective_date'] = $this->tomorrow(); return $policy; } private function savePolicy(array $policy): int { $policy['rules'] = json_encode($policy['rules'], JSON_THROW_ON_ERROR); $old = DB::table('share_policies')->where('effective_date', $this->tomorrow())->first(); $policy['updated_at'] = $this->now(); if ($old) { DB::table('share_policies')->where('id', $old->id)->where('effective_date', '>', $this->today())->update($policy); return (int)$old->id; } return (int)DB::table('share_policies')->insertGetId($policy + ['created_at' => $this->now()]); } private function policy(bool $pending): ?array { $query = DB::table('share_policies'); $row = $pending ? $query->where('effective_date', $this->tomorrow())->first() : $query->where('effective_date', '<=', $this->today())->orderByDesc('effective_date')->first(); if (!$row) return null; $data = (array)$row; foreach (['id', 'enabled'] as $field) $data[$field] = (int)$data[$field]; foreach (['valid_member_min_deposit', 'valid_member_min_effective_bet'] as $field) $data[$field] = $data[$field] === null ? null : $this->money($data[$field]); unset($data['indirect_bonus_rate'], $data['benefit_enabled']); $data['rules'] = json_decode($data['rules'], true, 512, JSON_THROW_ON_ERROR); foreach ($data['rules'] as &$rows) foreach ($rows as &$rule) { $rule['min_valid_members'] = (int)$rule['min_valid_members']; $rule['min_performance'] = $this->money($rule['min_performance']); $rule['rate'] = bcadd((string)$rule['rate'], '0', 4); } unset($rule, $rows); return $data; } private function lockConfiguration(): void { if (!DB::table('share_system_state')->where('key', 'relations_lock')->lockForUpdate()->first()) { throw new \RuntimeException('分享系统尚未初始化配置表'); } } private function origin(string $value): string { $value = trim($value); if (!preg_match('~^https?://~i', $value)) $value = 'https://' . $value; $parts = parse_url($value); if (!filter_var($value, FILTER_VALIDATE_URL) || !$parts || !in_array(strtolower($parts['scheme'] ?? ''), ['http', 'https'], true) || isset($parts['user'], $parts['pass']) || isset($parts['user']) || isset($parts['query']) || isset($parts['fragment']) || !in_array($parts['path'] ?? '', ['', '/'], true)) { $this->invalid('share_domain', '请填写PC网站域名源地址,不可包含路径、凭证或查询参数'); } return strtolower($parts['scheme']) . '://' . strtolower($parts['host']) . (isset($parts['port']) ? ':' . $parts['port'] : ''); } private function money($value): string { return bcadd((string)$value, '0', 2); } private function today(): string { return Carbon::now(ShareDefaults::TIMEZONE)->toDateString(); } private function tomorrow(): string { return Carbon::now(ShareDefaults::TIMEZONE)->addDay()->toDateString(); } private function now(): string { return Carbon::now(ShareDefaults::TIMEZONE)->format('Y-m-d H:i:s'); } private function invalid(string $key, string $message): void { throw ValidationException::withMessages([$key => $message]); } }