AgentEquityService.php 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentEquityOrder;
  5. use App\Models\Agent\AgentSetting;
  6. use Illuminate\Support\Facades\DB;
  7. use Illuminate\Support\Str;
  8. /**
  9. * 入股:代理在独立后台选择购买比例,金额 = 比例 × 每 1% 价格。
  10. * 余额足够立即扣款开通;不够则生成待付款订单,USDT 充值到账后自动扣款开通,无需审核。
  11. * 分红每月由总后台根据报表人工发放。
  12. */
  13. class AgentEquityService
  14. {
  15. public function __construct(private AgentWalletService $wallets)
  16. {
  17. }
  18. public function overview(Agent $agent): array
  19. {
  20. $setting = AgentSetting::current();
  21. $pending = AgentEquityOrder::query()->where('agent_id', $agent->id)->where('status', AgentEquityOrder::STATUS_PENDING)->first();
  22. $held = bcadd((string) $agent->equity_percent, '0', 4);
  23. $remaining = bcsub((string) $setting->equity_max_percent, $held, 4);
  24. if ($agent->isEquity() && !$agent->is_test) {
  25. $available = AgentEquityAllocation::available($setting->company_equity_percent);
  26. if (bccomp($available, $remaining, 4) < 0) {
  27. $remaining = $available;
  28. }
  29. }
  30. return [
  31. 'enabled' => (int) $setting->equity_enabled,
  32. 'price_per_percent' => (string) $setting->equity_price_per_percent,
  33. 'min_percent' => (string) $setting->equity_min_percent,
  34. 'max_percent' => (string) $setting->equity_max_percent,
  35. 'equity_percent' => $held,
  36. 'remaining_percent' => bccomp($remaining, '0', 4) > 0 ? $remaining : '0.0000',
  37. 'balance' => $this->wallets->purchaseBalance($agent),
  38. 'pending_order' => $pending,
  39. ];
  40. }
  41. public function order(Agent $agent, string $percent): AgentEquityOrder
  42. {
  43. $order = DB::transaction(function () use ($agent, $percent) {
  44. $agent = Agent::query()->lockForUpdate()->findOrFail($agent->id);
  45. if (!$agent->isEquity()) throw new \RuntimeException('只有入股代理可以购买股份');
  46. $setting = AgentSetting::lockCurrent();
  47. if (!(int) $setting->equity_enabled || bccomp((string) $setting->equity_price_per_percent, '0', 4) <= 0) {
  48. throw new \RuntimeException('入股暂未开放');
  49. }
  50. if (bccomp($percent, (string) $setting->equity_min_percent, 4) < 0 || bccomp($percent, (string) $setting->equity_max_percent, 4) > 0) {
  51. throw new \InvalidArgumentException("购买比例须在 {$setting->equity_min_percent}% 至 {$setting->equity_max_percent}% 之间");
  52. }
  53. if (AgentEquityOrder::query()->where('agent_id', $agent->id)->where('status', AgentEquityOrder::STATUS_PENDING)->exists()) {
  54. throw new \RuntimeException('已有待付款的入股订单,请先充值或取消');
  55. }
  56. if (bccomp(bcadd((string) $agent->equity_percent, $percent, 4), (string) $setting->equity_max_percent, 4) > 0) {
  57. throw new \RuntimeException("累计持股不能超过 {$setting->equity_max_percent}%");
  58. }
  59. if (!$agent->is_test && bccomp($percent, AgentEquityAllocation::available($setting->company_equity_percent), 4) > 0) {
  60. throw new \RuntimeException('可购买股份不足');
  61. }
  62. return AgentEquityOrder::query()->create([
  63. 'order_no' => 'AE' . now()->format('YmdHis') . strtoupper(Str::random(6)),
  64. 'agent_id' => $agent->id, 'percent' => bcadd($percent, '0', 4),
  65. 'price_per_percent' => (string) $setting->equity_price_per_percent,
  66. 'amount' => bcmul($percent, (string) $setting->equity_price_per_percent, 4),
  67. 'status' => AgentEquityOrder::STATUS_PENDING,
  68. ]);
  69. });
  70. $this->fulfillPending((int) $agent->id);
  71. return $order->fresh();
  72. }
  73. public function cancel(Agent $agent, int $orderId): AgentEquityOrder
  74. {
  75. return $this->cancelOrder(fn ($query) => $query->where('agent_id', $agent->id), $orderId, null, '');
  76. }
  77. /** 总后台取消待付款订单(如代理长期不充值),需填写原因。 */
  78. public function adminCancel(int $orderId, int $adminId, string $remark): AgentEquityOrder
  79. {
  80. $remark = trim($remark);
  81. if ($remark === '') throw new \InvalidArgumentException('请填写取消原因');
  82. return $this->cancelOrder(fn ($query) => $query, $orderId, $adminId, $remark);
  83. }
  84. private function cancelOrder(callable $scope, int $orderId, ?int $adminId, string $remark): AgentEquityOrder
  85. {
  86. return DB::transaction(function () use ($scope, $orderId, $adminId, $remark) {
  87. $order = $scope(AgentEquityOrder::query())->lockForUpdate()->findOrFail($orderId);
  88. if ($order->status !== AgentEquityOrder::STATUS_PENDING) throw new \RuntimeException('只有待付款订单可以取消');
  89. $order->status = AgentEquityOrder::STATUS_CANCELLED;
  90. $order->cancelled_by = $adminId;
  91. $order->cancel_remark = $remark;
  92. $order->save();
  93. return $order;
  94. });
  95. }
  96. /**
  97. * 总后台:全部代理的入股购买记录。summary 按当前筛选条件汇总:
  98. * 已开通的比例和金额、待付款笔数和金额。
  99. */
  100. public function paginate(array $params): array
  101. {
  102. $page = max(1, (int) ($params['page'] ?? 1));
  103. $limit = min(100, max(1, (int) ($params['limit'] ?? 20)));
  104. $query = AgentEquityOrder::query();
  105. if (!empty($params['agent_id'])) $query->where('agent_id', (int) $params['agent_id']);
  106. if (!empty($params['username'])) {
  107. $query->whereIn('agent_id', Agent::query()->where('username', 'like', '%' . $params['username'] . '%')->select('id'));
  108. }
  109. if (!empty($params['order_no'])) $query->where('order_no', $params['order_no']);
  110. if (!empty($params['status'])) $query->where('status', $params['status']);
  111. $dateColumn = ($params['date_type'] ?? '') === 'completed_at' ? 'completed_at' : 'created_at';
  112. if (!empty($params['start_date'])) $query->where($dateColumn, '>=', $params['start_date'] . ' 00:00:00');
  113. if (!empty($params['end_date'])) $query->where($dateColumn, '<=', $params['end_date'] . ' 23:59:59');
  114. // 汇总不含测试代理(列表照常显示)
  115. $sums = \App\Support\StatsScope::excludeTestAgents(clone $query)->selectRaw(
  116. 'COALESCE(SUM(CASE WHEN status = ? THEN percent ELSE 0 END), 0) AS completed_percent, '
  117. . 'COALESCE(SUM(CASE WHEN status = ? THEN amount ELSE 0 END), 0) AS completed_amount, '
  118. . 'COALESCE(SUM(CASE WHEN status = ? THEN 1 ELSE 0 END), 0) AS pending_count, '
  119. . 'COALESCE(SUM(CASE WHEN status = ? THEN amount ELSE 0 END), 0) AS pending_amount',
  120. [AgentEquityOrder::STATUS_COMPLETED, AgentEquityOrder::STATUS_COMPLETED, AgentEquityOrder::STATUS_PENDING, AgentEquityOrder::STATUS_PENDING]
  121. )->first();
  122. $total = (clone $query)->count();
  123. $list = $query->with('agent:id,username,real_name,type,equity_percent,balance,is_test')->orderByDesc('id')->forPage($page, $limit)->get();
  124. $summary = [
  125. 'completed_percent' => bcadd((string) $sums->completed_percent, '0', 4),
  126. 'completed_amount' => bcadd((string) $sums->completed_amount, '0', 4),
  127. 'pending_count' => (int) $sums->pending_count,
  128. 'pending_amount' => bcadd((string) $sums->pending_amount, '0', 4),
  129. // 全平台入股代理当前持股合计(不受筛选影响)
  130. 'total_equity_percent' => bcadd((string) \App\Support\StatsScope::excludeTestAgents(Agent::query()->where('type', Agent::TYPE_EQUITY), 'id')->sum('equity_percent'), '0', 4),
  131. ];
  132. return compact('total', 'page', 'limit', 'list', 'summary');
  133. }
  134. /** 余额足够时完成待付款订单(下单后、充值到账后调用)。 */
  135. public function fulfillPending(int $agentId): int
  136. {
  137. return DB::transaction(function () use ($agentId) {
  138. $agent = Agent::query()->lockForUpdate()->findOrFail($agentId);
  139. if (!$agent->isEquity()) return 0;
  140. $done = 0;
  141. $orders = AgentEquityOrder::query()->where('agent_id', $agentId)->where('status', AgentEquityOrder::STATUS_PENDING)
  142. ->orderBy('id')->lockForUpdate()->get();
  143. if ($orders->isEmpty()) return 0;
  144. $available = $agent->is_test ? null : AgentEquityAllocation::available(AgentSetting::lockCurrent()->company_equity_percent, false);
  145. foreach ($orders as $order) {
  146. if (bccomp($this->wallets->purchaseBalance($agent), $order->amount, 4) < 0) break;
  147. if ($available !== null && bccomp($order->percent, $available, 4) > 0) break;
  148. $this->wallets->adjust($agentId, (string) $order->amount, 'equity_purchase', '购买股份 ' . $order->percent . '%',
  149. 'equity-order:' . $order->id, 'agent', $agentId, 'agent_equity_order', (int) $order->id);
  150. $agent = Agent::query()->lockForUpdate()->findOrFail($agentId);
  151. $agent->equity_percent = bcadd((string) $agent->equity_percent, (string) $order->percent, 4);
  152. $agent->save();
  153. $order->status = AgentEquityOrder::STATUS_COMPLETED;
  154. $order->completed_at = now();
  155. $order->save();
  156. if ($available !== null) $available = bcsub($available, $order->percent, 4);
  157. ++$done;
  158. }
  159. return $done;
  160. }, 3);
  161. }
  162. }