['nullable', 'integer', 'min:1'], 'limit' => ['nullable', 'integer', 'between:1,100'], 'status' => ['nullable', 'in:pending,approved,rejected'], 'username' => ['nullable', 'string', 'max:64'], 'member_id' => ['nullable', 'string', 'max:64'], ])->validate(); $query = DB::table('agent_applications as a')->join('users as u', 'u.id', '=', 'a.user_id'); if (!empty($data['status'])) $query->where('a.status', $data['status']); if (!empty($data['username'])) $query->where('a.username', 'like', '%' . $data['username'] . '%'); if (!empty($data['member_id'])) $query->where('u.member_id', $data['member_id']); $total = (clone $query)->count(); $rows = $query->select('a.id', 'a.user_id', 'u.member_id', 'u.account as member_account', 'a.username', 'a.status', 'a.agent_id', 'a.reject_reason', 'a.reviewed_by', 'a.reviewed_at', 'a.created_at') ->orderByDesc('a.id')->forPage((int) ($data['page'] ?? 1), (int) ($data['limit'] ?? 20))->get(); return ['total' => $total, 'data' => $rows]; } public function audit(array $input, AgentService $agents): array { $data = Validator::make($input, [ 'id' => ['required', 'integer', 'min:1'], 'status' => ['required', 'in:approved,rejected'], 'remark' => ['required_if:status,rejected', 'nullable', 'string', 'max:500'], 'flow_commission_profile_id' => ['nullable', 'integer', Rule::exists('agent_flow_commission_profiles', 'id')->whereNull('owner_agent_id')], 'profit_commission_profile_id' => ['nullable', 'integer', Rule::exists('agent_profit_commission_profiles', 'id')->whereNull('owner_agent_id')], ])->validate(); return DB::transaction(function () use ($data, $agents) { $found = AgentApplication::findOrFail($data['id']); $member = User::where('id', $found->user_id)->lockForUpdate()->firstOrFail(); $application = AgentApplication::where('id', $found->id)->lockForUpdate()->firstOrFail(); if ($application->status !== 'pending') { if ($application->status === $data['status']) return $application->toArray(); throw new \RuntimeException('申请已审核,不能修改审核结果'); } $this->assertMember($member, $data['status'] === 'rejected'); $actor = OperationAuditService::actor(); if (!$actor['operator_id']) throw new \RuntimeException('请使用后台管理员身份'); $before = $application->toArray(); if ($data['status'] === 'approved') { if (Agent::where('owner_user_id', $member->id)->exists()) throw new \RuntimeException('该会员已是代理'); if (Agent::whereRaw('LOWER(username) = ?', [strtolower($application->username)])->exists()) throw new \RuntimeException('代理账号已被使用,请驳回后重新申请'); $options = array_filter(array_intersect_key($data, array_flip(['flow_commission_profile_id', 'profit_commission_profile_id'])), fn ($value) => $value !== null); $agent = $agents->createFromApplication($application, $options + [ 'real_name' => $member->first_name ?: '', 'language' => in_array($member->language, ['zh', 'en', 'vi'], true) ? $member->language : 'zh', ], (int) $actor['operator_id']); $this->bindMember($agent, (int) $member->id); $application->agent_id = $agent->id; } $application->status = $data['status']; $application->reject_reason = $data['status'] === 'rejected' ? trim((string) $data['remark']) : ''; if ($data['status'] === 'rejected' && $application->reject_reason === '') throw new \RuntimeException('请填写驳回原因'); $application->reviewed_by = $actor['operator_id']; $application->reviewed_at = now('Asia/Shanghai')->format('Y-m-d H:i:s'); $application->password_hash = $application->withdraw_password_hash = null; $application->pending_user_id = $application->pending_username = null; $application->save(); $this->notify($application, $member, $actor); OperationAuditService::record('agent_application', $application->id, $application->status, $before, $application); return $application->toArray(); }, 3); } public function bindMember(Agent $agent, int $userId): Agent { return DB::transaction(function () use ($agent, $userId) { $member = User::where('id', $userId)->lockForUpdate()->firstOrFail(); $this->assertMember($member); $agent = Agent::where('id', $agent->id)->lockForUpdate()->firstOrFail(); if ($agent->owner_user_id !== null && (int) $agent->owner_user_id !== $userId) throw new \RuntimeException('代理已绑定其他会员'); if (Agent::where('owner_user_id', $userId)->where('id', '<>', $agent->id)->exists()) throw new \RuntimeException('该会员已绑定其他代理账号'); $agent->forceFill(['owner_user_id' => $userId])->save(); return $agent->load(['flowCommissionProfile', 'profitCommissionProfile']); }, 3); } private function assertMember(User $member, bool $allowBanned = false): void { if ((int) $member->from !== 1 || !empty($member->delete_time) || (!$allowBanned && (int) $member->is_banned === 1)) { throw new \RuntimeException('会员状态不允许申请或绑定代理'); } } private function notify(AgentApplication $application, User $member, array $actor): void { $approved = $application->status === 'approved'; $account = htmlspecialchars($application->username, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $reason = htmlspecialchars($application->reject_reason, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); $texts = [ 'zh' => ['title' => $approved ? '代理申请已通过' : '代理申请未通过', 'content' => $approved ? "您的代理申请已通过,代理账号:{$account}。请使用申请时设置的密码登录代理后台。" : "您的代理申请未通过,原因:{$reason}。您可以重新申请。"], '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."], '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."], ]; $now = now('Asia/Shanghai')->format('Y-m-d H:i:s'); $language = in_array($member->language, ['zh', 'en', 'vi'], true) ? $member->language : 'zh'; $messageId = DB::table('site_messages')->insertGetId([ 'category' => 'internal', 'recipient_type' => 'specified_users', 'recipient_ids' => json_encode([$member->id]), 'status' => 'sent', 'send_at' => $now, 'sent_at' => $now, 'recipient_count' => 1, 'skipped_count' => 0, 'sender_id' => $actor['operator_id'], 'sender_name' => $actor['operator_name'], 'request_id' => 'agent-application:' . $application->id, 'request_hash' => hash('sha256', json_encode($texts)), 'created_at' => $now, 'updated_at' => $now, ]); foreach ($texts as $lang => $text) DB::table('site_message_translations')->insert($text + ['subtitle' => '', 'language' => $lang, 'message_id' => $messageId]); DB::table('site_message_recipients')->insert(['message_id' => $messageId, 'recipient_kind' => 'user', 'recipient_id' => $member->id, 'language' => $language, 'created_at' => $now]); OperationAuditService::record('site_message', $messageId, 'send', [], ['recipient_type' => 'specified_users', 'recipient_ids' => [$member->id], 'category' => 'internal', 'status' => 'sent']); SiteRealtimeService::changed('message', $messageId, 'send'); } }