AgentApplicationService.php 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  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'],
  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. $total = (clone $query)->count();
  25. $rows = $query->select('a.id', 'a.user_id', 'u.member_id', 'u.account as member_account', 'a.username',
  26. 'a.status', 'a.agent_id', 'a.reject_reason', 'a.reviewed_by', 'a.reviewed_at', 'a.created_at')
  27. ->orderByDesc('a.id')->forPage((int) ($data['page'] ?? 1), (int) ($data['limit'] ?? 20))->get();
  28. return ['total' => $total, 'data' => $rows];
  29. }
  30. public function audit(array $input, AgentService $agents): array
  31. {
  32. $data = Validator::make($input, [
  33. 'id' => ['required', 'integer', 'min:1'], 'status' => ['required', 'in:approved,rejected'],
  34. 'remark' => ['required_if:status,rejected', 'nullable', 'string', 'max:500'],
  35. 'type' => ['nullable', 'integer', Rule::in(array_keys(Agent::TYPE_NAMES))],
  36. ])->validate();
  37. return DB::transaction(function () use ($data, $agents) {
  38. $found = AgentApplication::findOrFail($data['id']);
  39. $member = User::where('id', $found->user_id)->lockForUpdate()->firstOrFail();
  40. $application = AgentApplication::where('id', $found->id)->lockForUpdate()->firstOrFail();
  41. if ($application->status !== 'pending') {
  42. if ($application->status === $data['status']) return $application->toArray();
  43. throw new \RuntimeException('申请已审核,不能修改审核结果');
  44. }
  45. $this->assertMember($member, $data['status'] === 'rejected');
  46. $actor = OperationAuditService::actor();
  47. if (!$actor['operator_id']) throw new \RuntimeException('请使用后台管理员身份');
  48. $before = $application->toArray();
  49. if ($data['status'] === 'approved') {
  50. if (Agent::where('owner_user_id', $member->id)->exists()) throw new \RuntimeException('该会员已是代理');
  51. if (Agent::whereRaw('LOWER(username) = ?', [strtolower($application->username)])->exists()) throw new \RuntimeException('代理账号已被使用,请驳回后重新申请');
  52. $agent = $agents->createFromApplication($application, [
  53. 'type' => (int) ($data['type'] ?? Agent::TYPE_JOINT),
  54. 'real_name' => $member->first_name ?: '', 'language' => in_array($member->language, ['zh', 'en', 'vi'], true) ? $member->language : 'zh',
  55. ], (int) $actor['operator_id']);
  56. $this->bindMember($agent, (int) $member->id);
  57. $application->agent_id = $agent->id;
  58. }
  59. $application->status = $data['status'];
  60. $application->reject_reason = $data['status'] === 'rejected' ? trim((string) $data['remark']) : '';
  61. if ($data['status'] === 'rejected' && $application->reject_reason === '') throw new \RuntimeException('请填写驳回原因');
  62. $application->reviewed_by = $actor['operator_id'];
  63. $application->reviewed_at = now('Asia/Shanghai')->format('Y-m-d H:i:s');
  64. $application->password_hash = $application->withdraw_password_hash = null;
  65. $application->pending_user_id = $application->pending_username = null;
  66. $application->save();
  67. $this->notify($application, $member, $actor);
  68. OperationAuditService::record('agent_application', $application->id, $application->status, $before, $application);
  69. return $application->toArray();
  70. }, 3);
  71. }
  72. public function bindMember(Agent $agent, int $userId): Agent
  73. {
  74. return DB::transaction(function () use ($agent, $userId) {
  75. $member = User::where('id', $userId)->lockForUpdate()->firstOrFail();
  76. $this->assertMember($member);
  77. $agent = Agent::where('id', $agent->id)->lockForUpdate()->firstOrFail();
  78. if ($agent->owner_user_id !== null && (int) $agent->owner_user_id !== $userId) throw new \RuntimeException('代理已绑定其他会员');
  79. if (Agent::where('owner_user_id', $userId)->where('id', '<>', $agent->id)->exists()) throw new \RuntimeException('该会员已绑定其他代理账号');
  80. $agent->forceFill(['owner_user_id' => $userId])->save();
  81. return $agent->load(['profitCommissionProfile']);
  82. }, 3);
  83. }
  84. private function assertMember(User $member, bool $allowBanned = false): void
  85. {
  86. if ((int) $member->from !== 1 || !empty($member->delete_time) || (!$allowBanned && (int) $member->is_banned === 1)) {
  87. throw new \RuntimeException('会员状态不允许申请或绑定代理');
  88. }
  89. }
  90. private function notify(AgentApplication $application, User $member, array $actor): void
  91. {
  92. $approved = $application->status === 'approved';
  93. $account = htmlspecialchars($application->username, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  94. $reason = htmlspecialchars($application->reject_reason, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  95. $texts = [
  96. 'zh' => ['title' => $approved ? '代理申请已通过' : '代理申请未通过', 'content' => $approved ? "您的代理申请已通过,代理账号:{$account}。请使用申请时设置的密码登录代理后台。" : "您的代理申请未通过,原因:{$reason}。您可以重新申请。"],
  97. '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."],
  98. '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."],
  99. ];
  100. $now = now('Asia/Shanghai')->format('Y-m-d H:i:s');
  101. $language = in_array($member->language, ['zh', 'en', 'vi'], true) ? $member->language : 'zh';
  102. $messageId = DB::table('site_messages')->insertGetId([
  103. 'category' => 'internal', 'recipient_type' => 'specified_users', 'recipient_ids' => json_encode([$member->id]),
  104. 'status' => 'sent', 'send_at' => $now, 'sent_at' => $now, 'recipient_count' => 1, 'skipped_count' => 0,
  105. 'sender_id' => $actor['operator_id'], 'sender_name' => $actor['operator_name'],
  106. 'request_id' => 'agent-application:' . $application->id, 'request_hash' => hash('sha256', json_encode($texts)),
  107. 'created_at' => $now, 'updated_at' => $now,
  108. ]);
  109. foreach ($texts as $lang => $text) DB::table('site_message_translations')->insert($text + ['subtitle' => '', 'language' => $lang, 'message_id' => $messageId]);
  110. DB::table('site_message_recipients')->insert(['message_id' => $messageId, 'recipient_kind' => 'user', 'recipient_id' => $member->id, 'language' => $language, 'created_at' => $now]);
  111. OperationAuditService::record('site_message', $messageId, 'send', [], ['recipient_type' => 'specified_users', 'recipient_ids' => [$member->id], 'category' => 'internal', 'status' => 'sent']);
  112. SiteRealtimeService::changed('message', $messageId, 'send');
  113. }
  114. }