Files
lotteryLaravel/app/Services/Agent/AgentSiteProvisioningService.php
kang 6b2ea39ea1
Some checks failed
lotterLaravel CI / test (push) Has been cancelled
fix: 修复并发竞态与幂等性缺陷并优化子树查询
- 扩大玩家ID随机数位宽降低碰撞概率
- 授信额度变更加 lockForUpdate 防并发下调
- 结算冲正与入账利用唯一索引实现幂等闸门
- 失败登录计数加锁防丢失
- 代理子树查询改用子查询替代全表 pluck
- 修复返点默认值百分比单位转换
- 测试中全量 fake 广播事件防 CI 500
2026-06-18 09:38:24 +08:00

155 lines
6.2 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
namespace App\Services\Agent;
use App\Models\AdminSite;
use App\Models\AdminUser;
use App\Models\AgentNode;
use App\Support\AdminUserStatus;
use App\Support\AgentPlatformRole;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
final class AgentSiteProvisioningService
{
public function __construct(
private readonly AgentProfileService $agentProfileService,
) {}
/**
* Generate a unique agent code based on site code and counter.
* Format: {site_code}-agent-{counter}
*/
private function generateUniqueAgentCode(int $siteId): string
{
$site = AdminSite::query()->find($siteId);
if ($site === null) {
throw new \RuntimeException('Site not found');
}
$prefix = strtolower(trim($site->code));
$counter = 1;
while (true) {
$code = sprintf('%s-agent-%d', $prefix, $counter);
if (!AgentNode::query()->where('code', $code)->exists()) {
return $code;
}
$counter++;
}
}
/**
* 在已存在的接入站点上创建一级代理(根节点)及后台登录账号。
*
* @param array<string, mixed> $payload site_code, code?, name, username, password, email?, status?, profile fields
* @return array{site: AdminSite, agent_node: AgentNode}
*/
public function createRootAgent(AdminUser $actor, array $payload): array
{
$siteCode = strtolower(trim((string) ($payload['site_code'] ?? '')));
$code = strtolower(trim((string) ($payload['code'] ?? '')));
$name = trim((string) ($payload['name'] ?? ''));
$username = trim((string) ($payload['username'] ?? ''));
$password = (string) ($payload['password'] ?? '');
$email = isset($payload['email']) ? trim((string) $payload['email']) : null;
$status = (int) ($payload['status'] ?? 1);
if ($siteCode === '' || $name === '' || $username === '' || $password === '') {
throw ValidationException::withMessages([
'site_code' => $siteCode === '' ? ['required'] : [],
'name' => $name === '' ? ['required'] : [],
'username' => $username === '' ? ['required'] : [],
'password' => $password === '' ? ['required'] : [],
]);
}
$site = AdminSite::query()->where('code', $siteCode)->first();
if ($site === null) {
throw ValidationException::withMessages(['site_code' => ['exists']]);
}
// Auto-generate code if not provided
if ($code === '') {
$code = $this->generateUniqueAgentCode($site->id);
} else {
if (AgentNode::query()->where('code', $code)->exists()) {
throw ValidationException::withMessages(['code' => ['unique']]);
}
}
if (AdminUser::query()->where('username', $username)->exists()) {
throw ValidationException::withMessages(['username' => ['unique']]);
}
$existingRoot = AgentNode::query()
->where('admin_site_id', $site->id)
->where('depth', 0)
->first();
if ($existingRoot !== null) {
throw ValidationException::withMessages([
'site_code' => ['site_root_exists'],
]);
}
return DB::transaction(function () use ($actor, $site, $code, $name, $username, $password, $email, $status, $payload): array {
$node = AgentNode::query()->create([
'admin_site_id' => $site->id,
'parent_id' => null,
'path' => '/',
'depth' => 0,
'code' => $code,
'name' => $name,
'status' => $status === 0 ? 0 : 1,
'created_by' => $actor->id,
'extra_json' => null,
]);
$node->path = '/'.$node->id.'/';
$node->save();
AgentPlatformRole::resolve();
$user = AdminUser::query()->create([
'username' => $username,
'name' => $name,
'email' => $email !== '' ? $email : null,
'password' => $password,
'status' => AdminUserStatus::fromAgentNodeStatus($status === 0 ? 0 : 1),
]);
DB::table('admin_user_agents')->insert([
'admin_user_id' => $user->id,
'agent_node_id' => $node->id,
'is_primary' => true,
'granted_at' => now(),
]);
AgentPlatformRole::assignPrimaryOperator($user, $node);
$defaults = config('agent_line_defaults', []);
// rebate_limit / default_player_rebate 在 DB 存储为小数0-1但 controller 与 field rules
// 按百分比0-100入参upsertForNode 内部会做 /100故透传 config 默认值时需 *100 转回。
$defaultRebateLimitPct = isset($defaults['rebate_limit']) ? (float) $defaults['rebate_limit'] * 100 : 0.0;
$defaultPlayerRebatePct = isset($defaults['default_player_rebate']) ? (float) $defaults['default_player_rebate'] * 100 : 0.0;
$this->agentProfileService->upsertForNode($node, [
'total_share_rate' => (float) ($payload['total_share_rate'] ?? $defaults['total_share_rate'] ?? 100),
'credit_limit' => (int) ($payload['credit_limit'] ?? $defaults['credit_limit'] ?? 0),
'rebate_limit' => array_key_exists('rebate_limit', $payload)
? (float) $payload['rebate_limit']
: $defaultRebateLimitPct,
'default_player_rebate' => array_key_exists('default_player_rebate', $payload)
? (float) $payload['default_player_rebate']
: $defaultPlayerRebatePct,
'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? true),
'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? true),
'can_create_player' => (bool) ($payload['can_create_player'] ?? true),
]);
return [
'site' => $site->fresh(),
'agent_node' => $node->fresh(['adminSite']),
];
});
}
}