fix: 修复并发竞态与幂等性缺陷并优化子树查询
Some checks failed
lotterLaravel CI / test (push) Has been cancelled

- 扩大玩家ID随机数位宽降低碰撞概率
- 授信额度变更加 lockForUpdate 防并发下调
- 结算冲正与入账利用唯一索引实现幂等闸门
- 失败登录计数加锁防丢失
- 代理子树查询改用子查询替代全表 pluck
- 修复返点默认值百分比单位转换
- 测试中全量 fake 广播事件防 CI 500
This commit is contained in:
2026-06-18 09:38:24 +08:00
parent 2e0b257160
commit 6b2ea39ea1
13 changed files with 400 additions and 135 deletions

View File

@@ -15,31 +15,34 @@ return new class extends Migration
$defaults = config('agent_line_defaults', []);
$now = now();
$roots = DB::table('agent_nodes')->where('depth', 0)->get(['id']);
// 整批包在事务中:避免中途某条失败时部分根代理已补 profile、部分缺失的中间态。
DB::transaction(function () use ($defaults, $now): void {
$roots = DB::table('agent_nodes')->where('depth', 0)->get(['id']);
foreach ($roots as $root) {
$nodeId = (int) $root->id;
if (DB::table('agent_profiles')->where('agent_node_id', $nodeId)->exists()) {
continue;
foreach ($roots as $root) {
$nodeId = (int) $root->id;
if (DB::table('agent_profiles')->where('agent_node_id', $nodeId)->exists()) {
continue;
}
DB::table('agent_profiles')->insert([
'agent_node_id' => $nodeId,
'total_share_rate' => (float) ($defaults['total_share_rate'] ?? 100),
'credit_limit' => (int) ($defaults['credit_limit'] ?? 0) > 0
? (int) $defaults['credit_limit']
: 1_000_000,
'allocated_credit' => 0,
'used_credit' => 0,
'rebate_limit' => (float) ($defaults['rebate_limit'] ?? 0.005),
'default_player_rebate' => (float) ($defaults['default_player_rebate'] ?? 0.005),
'can_grant_extra_rebate' => true,
'can_create_child_agent' => true,
'can_create_player' => true,
'created_at' => $now,
'updated_at' => $now,
]);
}
DB::table('agent_profiles')->insert([
'agent_node_id' => $nodeId,
'total_share_rate' => (float) ($defaults['total_share_rate'] ?? 100),
'credit_limit' => (int) ($defaults['credit_limit'] ?? 0) > 0
? (int) $defaults['credit_limit']
: 1_000_000,
'allocated_credit' => 0,
'used_credit' => 0,
'rebate_limit' => (float) ($defaults['rebate_limit'] ?? 0.005),
'default_player_rebate' => (float) ($defaults['default_player_rebate'] ?? 0.005),
'can_grant_extra_rebate' => true,
'can_create_child_agent' => true,
'can_create_player' => true,
'created_at' => $now,
'updated_at' => $now,
]);
}
});
}
public function down(): void

View File

@@ -0,0 +1,73 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
// 业务幂等保护:
// - credit_ledger: 同一 (ref_type, ref_id, reason) 三元组只允许一条;同 ticket_item 可存在
// 多 reason 流水bet_hold_release + game_settlement_loss但同一笔业务动作同一 reason
// 重复插入会被拒。注ref_id IS NULL 的行(如 bet 占位)不参与唯一约束。
// - share_ledger.reversal_of_id: 同一原账只允许一条反转(业务语义「补差冲正」每张原票仅一次)。
$driver = Schema::getConnection()->getDriverName();
if (Schema::hasTable('credit_ledger') && ! $this->hasIndex('credit_ledger', 'uk_credit_ledger_ref_reason')) {
if ($driver === 'pgsql') {
DB::statement('CREATE UNIQUE INDEX uk_credit_ledger_ref_reason ON credit_ledger (ref_type, ref_id, reason) WHERE ref_id IS NOT NULL');
} elseif ($driver === 'sqlite') {
DB::statement('CREATE UNIQUE INDEX uk_credit_ledger_ref_reason ON credit_ledger (ref_type, ref_id, reason) WHERE ref_id IS NOT NULL');
} else {
Schema::table('credit_ledger', function (Blueprint $table): void {
$table->unique(['ref_type', 'ref_id', 'reason'], 'uk_credit_ledger_ref_reason');
});
}
}
if (Schema::hasTable('share_ledger') && ! $this->hasIndex('share_ledger', 'uk_share_ledger_reversal_of')) {
if ($driver === 'pgsql') {
DB::statement('CREATE UNIQUE INDEX uk_share_ledger_reversal_of ON share_ledger (reversal_of_id) WHERE reversal_of_id IS NOT NULL');
} elseif ($driver === 'sqlite') {
DB::statement('CREATE UNIQUE INDEX uk_share_ledger_reversal_of ON share_ledger (reversal_of_id) WHERE reversal_of_id IS NOT NULL');
} else {
Schema::table('share_ledger', function (Blueprint $table): void {
$table->unique('reversal_of_id', 'uk_share_ledger_reversal_of');
});
}
}
}
public function down(): void
{
$driver = Schema::getConnection()->getDriverName();
if (Schema::hasTable('share_ledger') && $this->hasIndex('share_ledger', 'uk_share_ledger_reversal_of')) {
if (in_array($driver, ['pgsql', 'sqlite'], true)) {
DB::statement('DROP INDEX IF EXISTS uk_share_ledger_reversal_of');
} else {
Schema::table('share_ledger', function (Blueprint $table): void {
$table->dropUnique('uk_share_ledger_reversal_of');
});
}
}
if (Schema::hasTable('credit_ledger') && $this->hasIndex('credit_ledger', 'uk_credit_ledger_ref_reason')) {
if (in_array($driver, ['pgsql', 'sqlite'], true)) {
DB::statement('DROP INDEX IF EXISTS uk_credit_ledger_ref_reason');
} else {
Schema::table('credit_ledger', function (Blueprint $table): void {
$table->dropUnique('uk_credit_ledger_ref_reason');
});
}
}
}
private function hasIndex(string $table, string $indexName): bool
{
return Schema::hasIndex($table, $indexName);
}
};