AgentApplicationService.php 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130
  1. <?php
  2. namespace App\Services\Agent;
  3. use App\Models\Agent\Agent;
  4. use App\Models\Agent\AgentApplication;
  5. use App\Models\User;
  6. use App\Services\OperationAuditService;
  7. use App\Services\SiteRealtimeService;
  8. use Illuminate\Support\Facades\DB;
  9. use Illuminate\Support\Facades\Validator;
  10. use Illuminate\Validation\Rule;
  11. class AgentApplicationService
  12. {
  13. public function listing(array $input): array
  14. {
  15. $data = Validator::make($input, [
  16. 'page' => ['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'between:1,100'],
  17. 'status' => ['nullable', 'in:pending,approved,rejected'], 'username' => ['nullable', 'string', 'max:64'],
  18. 'member_id' => ['nullable', 'string', 'max:64'], 'type' => ['nullable', 'integer', Rule::in(array_keys(Agent::TYPE_NAMES))],
  19. ])->validate();
  20. $query = DB::table('agent_applications as a')->join('users as u', 'u.id', '=', 'a.user_id');
  21. if (!empty($data['status'])) $query->where('a.status', $data['status']);
  22. if (!empty($data['username'])) $query->where('a.username', 'like', '%' . $data['username'] . '%');
  23. if (!empty($data['member_id'])) $query->where('u.member_id', $data['member_id']);
  24. if (!empty($data['type'])) $query->where('a.type', (int) $data['type']);
  25. $total = (clone $query)->count();
  26. $rows = $query->select('a.id', 'a.user_id', 'u.member_id', 'u.account as member_account', 'a.username', 'a.type',
  27. 'a.status', 'a.agent_id', 'a.reject_reason', 'a.reviewed_by', 'a.reviewed_at', 'a.created_at')
  28. ->orderByDesc('a.id')->forPage((int) ($data['page'] ?? 1), (int) ($data['limit'] ?? 20))->get()
  29. ->map(function ($row) {
  30. $row->type = (int) ($row->type ?: Agent::TYPE_JOINT);
  31. $row->type_text = Agent::TYPE_NAMES[$row->type] ?? '';
  32. return $row;
  33. });
  34. return ['total' => $total, 'data' => $rows];
  35. }
  36. public function audit(array $input, AgentService $agents): array
  37. {
  38. $data = Validator::make($input, [
  39. 'id' => ['required', 'integer', 'min:1'], 'status' => ['required', 'in:approved,rejected'],
  40. 'remark' => ['required_if:status,rejected', 'nullable', 'string', 'max:500'],
  41. 'type' => ['nullable', 'integer', Rule::in(array_keys(Agent::TYPE_NAMES))],
  42. ])->validate();
  43. return DB::transaction(function () use ($data, $agents) {
  44. $found = AgentApplication::findOrFail($data['id']);
  45. $member = User::where('id', $found->user_id)->lockForUpdate()->firstOrFail();
  46. $application = AgentApplication::where('id', $found->id)->lockForUpdate()->firstOrFail();
  47. if ($application->status !== 'pending') {
  48. if ($application->status === $data['status']) return $application->toArray();
  49. throw new \RuntimeException('申请已审核,不能修改审核结果');
  50. }
  51. $this->assertMember($member, $data['status'] === 'rejected');
  52. $actor = OperationAuditService::actor();
  53. if (!$actor['operator_id']) throw new \RuntimeException('请使用后台管理员身份');
  54. $before = $application->toArray();
  55. if ($data['status'] === 'approved') {
  56. if (Agent::where('owner_user_id', $member->id)->exists()) throw new \RuntimeException('该会员已是代理');
  57. if (Agent::whereRaw('LOWER(username) = ?', [strtolower($application->username)])->exists()) throw new \RuntimeException('代理账号已被使用,请驳回后重新申请');
  58. // 默认按会员申请时选择的类型开通;审核时传 type 可改为另一类型。
  59. $type = (int) ($data['type'] ?? ($application->type ?: Agent::TYPE_JOINT));
  60. $agent = $agents->createFromApplication($application, [
  61. 'type' => $type,
  62. 'real_name' => $member->first_name ?: '', 'language' => in_array($member->language, ['zh', 'en', 'vi'], true) ? $member->language : 'zh',
  63. ], (int) $actor['operator_id']);
  64. $this->bindMember($agent, (int) $member->id);
  65. $application->agent_id = $agent->id;
  66. $application->type = $type;
  67. }
  68. $application->status = $data['status'];
  69. $application->reject_reason = $data['status'] === 'rejected' ? trim((string) $data['remark']) : '';
  70. if ($data['status'] === 'rejected' && $application->reject_reason === '') throw new \RuntimeException('请填写驳回原因');
  71. $application->reviewed_by = $actor['operator_id'];
  72. $application->reviewed_at = now('Asia/Shanghai')->format('Y-m-d H:i:s');
  73. $application->password_hash = $application->withdraw_password_hash = null;
  74. $application->pending_user_id = $application->pending_username = null;
  75. $application->save();
  76. $this->notify($application, $member, $actor);
  77. OperationAuditService::record('agent_application', $application->id, $application->status, $before, $application);
  78. return $application->toArray();
  79. }, 3);
  80. }
  81. public function bindMember(Agent $agent, int $userId): Agent
  82. {
  83. return DB::transaction(function () use ($agent, $userId) {
  84. $member = User::where('id', $userId)->lockForUpdate()->firstOrFail();
  85. $this->assertMember($member);
  86. $agent = Agent::where('id', $agent->id)->lockForUpdate()->firstOrFail();
  87. if ($agent->owner_user_id !== null && (int) $agent->owner_user_id !== $userId) throw new \RuntimeException('代理已绑定其他会员');
  88. if (Agent::where('owner_user_id', $userId)->where('id', '<>', $agent->id)->exists()) throw new \RuntimeException('该会员已绑定其他代理账号');
  89. $agent->forceFill(['owner_user_id' => $userId])->save();
  90. return $agent->load(['profitCommissionProfile']);
  91. }, 3);
  92. }
  93. private function assertMember(User $member, bool $allowBanned = false): void
  94. {
  95. if ((int) $member->from !== 1 || !empty($member->delete_time) || (!$allowBanned && (int) $member->is_banned === 1)) {
  96. throw new \RuntimeException('会员状态不允许申请或绑定代理');
  97. }
  98. }
  99. private function notify(AgentApplication $application, User $member, array $actor): void
  100. {
  101. $approved = $application->status === 'approved';
  102. $account = htmlspecialchars($application->username, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  103. $reason = htmlspecialchars($application->reject_reason, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  104. $texts = [
  105. 'zh' => ['title' => $approved ? '代理申请已通过' : '代理申请未通过', 'content' => $approved ? "您的代理申请已通过,代理账号:{$account}。请使用申请时设置的密码登录代理后台。" : "您的代理申请未通过,原因:{$reason}。您可以重新申请。"],
  106. 'en' => ['title' => $approved ? 'Agent application approved' : 'Agent application rejected', 'content' => $approved ? "Your agent account {$account} is ready. Sign in to the agent portal with the password you set when applying." : "Your agent application was rejected. Reason: {$reason}. You may apply again."],
  107. 'vi' => ['title' => $approved ? 'Đơn đăng ký đại lý được duyệt' : 'Đơn đăng ký đại lý bị từ chối', 'content' => $approved ? "Tài khoản đại lý {$account} đã sẵn sàng. Đăng nhập bằng mật khẩu đã đặt khi đăng ký." : "Đơn đăng ký đại lý bị từ chối. Lý do: {$reason}. Bạn có thể đăng ký lại."],
  108. ];
  109. $now = now('Asia/Shanghai')->format('Y-m-d H:i:s');
  110. $language = in_array($member->language, ['zh', 'en', 'vi'], true) ? $member->language : 'zh';
  111. $messageId = DB::table('site_messages')->insertGetId([
  112. 'category' => 'internal', 'recipient_type' => 'specified_users', 'recipient_ids' => json_encode([$member->id]),
  113. 'status' => 'sent', 'send_at' => $now, 'sent_at' => $now, 'recipient_count' => 1, 'skipped_count' => 0,
  114. 'sender_id' => $actor['operator_id'], 'sender_name' => $actor['operator_name'],
  115. 'request_id' => 'agent-application:' . $application->id, 'request_hash' => hash('sha256', json_encode($texts)),
  116. 'created_at' => $now, 'updated_at' => $now,
  117. ]);
  118. foreach ($texts as $lang => $text) DB::table('site_message_translations')->insert($text + ['subtitle' => '', 'language' => $lang, 'message_id' => $messageId]);
  119. DB::table('site_message_recipients')->insert(['message_id' => $messageId, 'recipient_kind' => 'user', 'recipient_id' => $member->id, 'language' => $language, 'created_at' => $now]);
  120. OperationAuditService::record('site_message', $messageId, 'send', [], ['recipient_type' => 'specified_users', 'recipient_ids' => [$member->id], 'category' => 'internal', 'status' => 'sent']);
  121. SiteRealtimeService::changed('message', $messageId, 'send');
  122. }
  123. }