AgentWalletService.php 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentBalanceLog;
  5. use Illuminate\Support\Facades\DB;
  6. class AgentWalletService
  7. {
  8. public function transfer(int $fromAgentId, int $toAgentId, string $amount, string $remark, string $idempotencyKey, int $operatorId): array
  9. {
  10. if ($fromAgentId === $toAgentId) {
  11. throw new \InvalidArgumentException('转出与转入代理不能相同');
  12. }
  13. if (bccomp($amount, '0', 4) <= 0) {
  14. throw new \InvalidArgumentException('金额必须大于0');
  15. }
  16. return DB::transaction(function () use ($fromAgentId, $toAgentId, $amount, $remark, $idempotencyKey, $operatorId) {
  17. $locked = Agent::query()->whereIn('id', [$fromAgentId, $toAgentId])->orderBy('id')->lockForUpdate()->get()->keyBy('id');
  18. $from = $locked->get($fromAgentId);
  19. $to = $locked->get($toAgentId);
  20. if (!$from || !$to) {
  21. throw new \RuntimeException('代理不存在');
  22. }
  23. $operator = $locked->get($operatorId);
  24. $child = $operatorId === $fromAgentId ? $to : $from;
  25. if (!$operator || (int) $operator->status !== Agent::STATUS_ENABLED
  26. || (int) $operator->level !== Agent::LEVEL_ONE || (int) $child->parent_id !== $operatorId) {
  27. throw new \RuntimeException('只能操作当前直属下级的额度');
  28. }
  29. if ($operatorId === $fromAgentId && (int) $to->status !== Agent::STATUS_ENABLED) {
  30. throw new \RuntimeException('不能给已禁用代理下发额度');
  31. }
  32. // Current read also works when an outer transaction already established a snapshot.
  33. $existing = AgentBalanceLog::query()->where('idempotency_key', $idempotencyKey . ':out')->lockForUpdate()->first();
  34. if ($existing) {
  35. $this->assertSameTransfer($existing, $fromAgentId, $toAgentId, $amount);
  36. return ['from_balance' => (string) $from->balance, 'to_balance' => (string) $to->balance];
  37. }
  38. if (bccomp((string) $from->balance, $amount, 4) < 0) {
  39. throw new \RuntimeException('代理余额不足');
  40. }
  41. $fromBefore = bcadd((string) $from->balance, '0', 4);
  42. $toBefore = bcadd((string) $to->balance, '0', 4);
  43. $from->balance = bcsub($fromBefore, $amount, 4);
  44. $to->balance = bcadd($toBefore, $amount, 4);
  45. $from->save();
  46. $to->save();
  47. AgentBalanceLog::query()->create([
  48. 'agent_id' => $fromAgentId, 'type' => 'transfer_out', 'amount' => bcsub('0', $amount, 4),
  49. 'before_balance' => $fromBefore, 'after_balance' => $from->balance,
  50. 'reference_type' => 'agent_transfer', 'reference_id' => $toAgentId,
  51. 'operator_type' => 'agent', 'operator_id' => $operatorId,
  52. 'idempotency_key' => $idempotencyKey . ':out', 'remark' => $remark,
  53. ]);
  54. AgentBalanceLog::query()->create([
  55. 'agent_id' => $toAgentId, 'type' => 'transfer_in', 'amount' => $amount,
  56. 'before_balance' => $toBefore, 'after_balance' => $to->balance,
  57. 'reference_type' => 'agent_transfer', 'reference_id' => $fromAgentId,
  58. 'operator_type' => 'agent', 'operator_id' => $operatorId,
  59. 'idempotency_key' => $idempotencyKey . ':in', 'remark' => $remark,
  60. ]);
  61. return ['from_balance' => (string) $from->balance, 'to_balance' => (string) $to->balance];
  62. });
  63. }
  64. public function adjust(
  65. int $agentId,
  66. string $amount,
  67. string $type,
  68. string $remark,
  69. string $idempotencyKey,
  70. string $operatorType,
  71. ?int $operatorId,
  72. string $referenceType = '',
  73. ?int $referenceId = null
  74. ): AgentBalanceLog {
  75. if (!in_array($type, ['credit', 'debit', 'commission', 'withdraw_refund'], true)) {
  76. throw new \InvalidArgumentException('资金变动类型错误');
  77. }
  78. if (bccomp($amount, '0', 4) <= 0) {
  79. throw new \InvalidArgumentException('金额必须大于0');
  80. }
  81. return DB::transaction(function () use ($agentId, $amount, $type, $remark, $idempotencyKey, $operatorType, $operatorId, $referenceType, $referenceId) {
  82. $agent = Agent::query()->lockForUpdate()->findOrFail($agentId);
  83. $existing = AgentBalanceLog::query()->where('idempotency_key', $idempotencyKey)->lockForUpdate()->first();
  84. if ($existing) {
  85. $this->assertSameAdjustment($existing, $agentId, $type, $amount);
  86. return $existing;
  87. }
  88. $before = bcadd((string) $agent->balance, '0', 4);
  89. $signed = $type === 'debit' ? bcsub('0', $amount, 4) : bcadd($amount, '0', 4);
  90. $after = bcadd($before, $signed, 4);
  91. if (bccomp($after, '0', 4) < 0) {
  92. throw new \RuntimeException('代理余额不足');
  93. }
  94. $agent->balance = $after;
  95. $agent->save();
  96. return AgentBalanceLog::query()->create([
  97. 'agent_id' => $agentId, 'type' => $type, 'amount' => $signed,
  98. 'before_balance' => $before, 'after_balance' => $after,
  99. 'operator_type' => $operatorType, 'operator_id' => $operatorId,
  100. 'idempotency_key' => $idempotencyKey, 'remark' => $remark,
  101. 'reference_type' => $referenceType, 'reference_id' => $referenceId,
  102. ]);
  103. });
  104. }
  105. private function assertSameTransfer(AgentBalanceLog $log, int $fromAgentId, int $toAgentId, string $amount): void
  106. {
  107. $expected = bcsub('0', $amount, 4);
  108. if ((int) $log->agent_id !== $fromAgentId || (int) $log->reference_id !== $toAgentId || bccomp((string) $log->amount, $expected, 4) !== 0) {
  109. throw new \RuntimeException('request_id 已用于其他资金操作');
  110. }
  111. }
  112. private function assertSameAdjustment(AgentBalanceLog $log, int $agentId, string $type, string $amount): void
  113. {
  114. $expected = $type === 'debit' ? bcsub('0', $amount, 4) : bcadd($amount, '0', 4);
  115. if ((int) $log->agent_id !== $agentId || $log->type !== $type || bccomp((string) $log->amount, $expected, 4) !== 0) {
  116. throw new \RuntimeException('request_id 已用于其他资金操作');
  117. }
  118. }
  119. }