AgentApplicationService.php 8.7 KB

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