where('idempotency_key', $idempotencyKey . ':out')->first(); if ($existing) { $this->assertSameTransfer($existing, $fromAgentId, $toAgentId, $amount); return [ 'from_balance' => (string) Agent::query()->findOrFail($fromAgentId)->balance, 'to_balance' => (string) Agent::query()->findOrFail($toAgentId)->balance, ]; } $locked = Agent::query()->whereIn('id', [$fromAgentId, $toAgentId])->orderBy('id')->lockForUpdate()->get()->keyBy('id'); $existing = AgentBalanceLog::query()->where('idempotency_key', $idempotencyKey . ':out')->first(); if ($existing) { $this->assertSameTransfer($existing, $fromAgentId, $toAgentId, $amount); return [ 'from_balance' => (string) Agent::query()->findOrFail($fromAgentId)->balance, 'to_balance' => (string) Agent::query()->findOrFail($toAgentId)->balance, ]; } $from = $locked->get($fromAgentId); $to = $locked->get($toAgentId); if (!$from || !$to) { throw new \RuntimeException('代理不存在'); } if (bccomp((string) $from->balance, $amount, 4) < 0) { throw new \RuntimeException('代理余额不足'); } $fromBefore = bcadd((string) $from->balance, '0', 4); $toBefore = bcadd((string) $to->balance, '0', 4); $from->balance = bcsub($fromBefore, $amount, 4); $to->balance = bcadd($toBefore, $amount, 4); $from->save(); $to->save(); AgentBalanceLog::query()->create([ 'agent_id' => $fromAgentId, 'type' => 'transfer_out', 'amount' => bcsub('0', $amount, 4), 'before_balance' => $fromBefore, 'after_balance' => $from->balance, 'reference_type' => 'agent_transfer', 'reference_id' => $toAgentId, 'operator_type' => 'agent', 'operator_id' => $operatorId, 'idempotency_key' => $idempotencyKey . ':out', 'remark' => $remark, ]); AgentBalanceLog::query()->create([ 'agent_id' => $toAgentId, 'type' => 'transfer_in', 'amount' => $amount, 'before_balance' => $toBefore, 'after_balance' => $to->balance, 'reference_type' => 'agent_transfer', 'reference_id' => $fromAgentId, 'operator_type' => 'agent', 'operator_id' => $operatorId, 'idempotency_key' => $idempotencyKey . ':in', 'remark' => $remark, ]); return ['from_balance' => (string) $from->balance, 'to_balance' => (string) $to->balance]; }); } public function adjust( int $agentId, string $amount, string $type, string $remark, string $idempotencyKey, string $operatorType, ?int $operatorId, string $referenceType = '', ?int $referenceId = null ): AgentBalanceLog { if (!in_array($type, ['credit', 'debit', 'commission', 'withdraw_refund'], true)) { throw new \InvalidArgumentException('资金变动类型错误'); } if (bccomp($amount, '0', 4) <= 0) { throw new \InvalidArgumentException('金额必须大于0'); } return DB::transaction(function () use ($agentId, $amount, $type, $remark, $idempotencyKey, $operatorType, $operatorId, $referenceType, $referenceId) { $existing = AgentBalanceLog::query()->where('idempotency_key', $idempotencyKey)->first(); if ($existing) { $this->assertSameAdjustment($existing, $agentId, $type, $amount); return $existing; } $agent = Agent::query()->lockForUpdate()->findOrFail($agentId); $existing = AgentBalanceLog::query()->where('idempotency_key', $idempotencyKey)->first(); if ($existing) { $this->assertSameAdjustment($existing, $agentId, $type, $amount); return $existing; } $before = bcadd((string) $agent->balance, '0', 4); $signed = $type === 'debit' ? bcsub('0', $amount, 4) : bcadd($amount, '0', 4); $after = bcadd($before, $signed, 4); if (bccomp($after, '0', 4) < 0) { throw new \RuntimeException('代理余额不足'); } $agent->balance = $after; $agent->save(); return AgentBalanceLog::query()->create([ 'agent_id' => $agentId, 'type' => $type, 'amount' => $signed, 'before_balance' => $before, 'after_balance' => $after, 'operator_type' => $operatorType, 'operator_id' => $operatorId, 'idempotency_key' => $idempotencyKey, 'remark' => $remark, 'reference_type' => $referenceType, 'reference_id' => $referenceId, ]); }); } private function assertSameTransfer(AgentBalanceLog $log, int $fromAgentId, int $toAgentId, string $amount): void { $expected = bcsub('0', $amount, 4); if ((int) $log->agent_id !== $fromAgentId || (int) $log->reference_id !== $toAgentId || bccomp((string) $log->amount, $expected, 4) !== 0) { throw new \RuntimeException('request_id 已用于其他资金操作'); } } private function assertSameAdjustment(AgentBalanceLog $log, int $agentId, string $type, string $amount): void { $expected = $type === 'debit' ? bcsub('0', $amount, 4) : bcadd($amount, '0', 4); if ((int) $log->agent_id !== $agentId || $log->type !== $type || bccomp((string) $log->amount, $expected, 4) !== 0) { throw new \RuntimeException('request_id 已用于其他资金操作'); } } }