feat: 完成独立的邀请与达人榜系统架构
This commit is contained in:
@@ -105,6 +105,16 @@ class AuthController extends Controller
|
||||
return response()->json(['status' => 'error', 'message' => '该用户名已被系统禁止注册,请更换其他名称。'], 422);
|
||||
}
|
||||
|
||||
// --- 提取邀请人 Cookie ---
|
||||
$inviterIdCookie = $request->cookie('inviter_id');
|
||||
$inviterId = null;
|
||||
if ($inviterIdCookie && is_numeric($inviterIdCookie)) {
|
||||
// 简单校验邀请人是否存在,防止脏数据
|
||||
if (User::where('id', $inviterIdCookie)->exists()) {
|
||||
$inviterId = (int) $inviterIdCookie;
|
||||
}
|
||||
}
|
||||
|
||||
$newUser = User::create([
|
||||
'username' => $username,
|
||||
'password' => Hash::make($password),
|
||||
@@ -113,10 +123,16 @@ class AuthController extends Controller
|
||||
'user_level' => 1, // 默认普通用户等级
|
||||
'sex' => $sex,
|
||||
'usersf' => '1.gif', // 默认头像
|
||||
'inviter_id' => $inviterId, // 记录邀请人
|
||||
]);
|
||||
|
||||
$this->performLogin($newUser, $ip);
|
||||
|
||||
// 如果是通过邀请注册的,响应成功后建议清除 Cookie,防止污染后续注册
|
||||
if ($inviterId) {
|
||||
\Illuminate\Support\Facades\Cookie::queue(\Illuminate\Support\Facades\Cookie::forget('inviter_id'));
|
||||
}
|
||||
|
||||
return response()->json(['status' => 'success', 'message' => '注册并登录成功!']);
|
||||
}
|
||||
|
||||
|
||||
46
app/Http/Controllers/InviteController.php
Normal file
46
app/Http/Controllers/InviteController.php
Normal file
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Cookie;
|
||||
|
||||
class InviteController extends Controller
|
||||
{
|
||||
/**
|
||||
* 处理邀请链接跳转
|
||||
*
|
||||
* @param int $inviter_id 邀请人ID
|
||||
*/
|
||||
public function handle(Request $request, int $inviter_id)
|
||||
{
|
||||
// 查找邀请人是否存在
|
||||
$inviter = User::find($inviter_id);
|
||||
|
||||
if ($inviter) {
|
||||
// 将邀请人ID记录到 Cookie 中,有效期7天(7 * 24 * 60 = 10080 分钟)
|
||||
// 确保Cookie仅通过 HTTP 访问且作用于全站
|
||||
Cookie::queue('inviter_id', $inviter->id, 10080);
|
||||
}
|
||||
|
||||
// 重定向回聊天室首页进行注册/登录
|
||||
return redirect()->route('home');
|
||||
}
|
||||
|
||||
/**
|
||||
* 独立展示邀请全站排行榜页面
|
||||
*/
|
||||
public function leaderboard()
|
||||
{
|
||||
// 邀请达人榜 (Top 50)
|
||||
$topInviters = User::withCount('invitees')
|
||||
->with(['activePosition.position.department'])
|
||||
->having('invitees_count', '>', 0)
|
||||
->orderByDesc('invitees_count')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
return view('invite.leaderboard', compact('topInviters'));
|
||||
}
|
||||
}
|
||||
@@ -66,6 +66,12 @@ class UserController extends Controller
|
||||
$data['meili'] = $targetUser->meili ?? 0;
|
||||
}
|
||||
|
||||
// 仅当自己看自己时,附加邀请相关信息,用于展示专属邀请链接
|
||||
if ($operator && $operator->id === $targetUser->id) {
|
||||
$data['id'] = $targetUser->id;
|
||||
$data['invitees_count'] = $targetUser->invitees()->count();
|
||||
}
|
||||
|
||||
// 职务履历(所有任职记录,按任命时间倒序;positions() 关系已含 with)
|
||||
$data['position_history'] = $targetUser->positions
|
||||
->map(fn ($up) => [
|
||||
|
||||
@@ -36,6 +36,7 @@ class User extends Authenticatable
|
||||
'sex',
|
||||
'sign',
|
||||
'user_level',
|
||||
'inviter_id',
|
||||
'room_id',
|
||||
'first_ip',
|
||||
'previous_ip',
|
||||
@@ -183,4 +184,20 @@ class User extends Authenticatable
|
||||
{
|
||||
return $this->activePosition()->exists();
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联:邀请当前用户的人
|
||||
*/
|
||||
public function inviter(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(self::class, 'inviter_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 关联:当前用户邀请的所有人
|
||||
*/
|
||||
public function invitees(): HasMany
|
||||
{
|
||||
return $this->hasMany(self::class, 'inviter_id');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->unsignedBigInteger('inviter_id')->nullable()->after('id')->comment('邀请人ID');
|
||||
$table->foreign('inviter_id')->references('id')->on('users')->nullOnDelete();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('users', function (Blueprint $table) {
|
||||
$table->dropForeign(['inviter_id']);
|
||||
$table->dropColumn('inviter_id');
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -648,6 +648,30 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 邀请贡献卡片 (仅查看自己资料时显示) --}}
|
||||
<div x-show="userInfo.username === window.chatContext.username && userInfo.id !== undefined"
|
||||
style="margin-top: 12px; border-radius: 8px; overflow: hidden; border: 1px dashed #c7d2fe;">
|
||||
<div
|
||||
style="background: #e0e7ff; padding: 6px 10px; font-size: 11px; font-weight: bold; color: #4338ca; display: flex; align-items: center; justify-content: space-between;">
|
||||
<span>🤝 我的邀请推广</span>
|
||||
<span style="font-size: 10px;">累计邀请:<span x-text="userInfo.invitees_count"
|
||||
style="color: #ef4444; font-size: 12px;"></span> 人</span>
|
||||
</div>
|
||||
<div
|
||||
style="padding: 8px 10px; background: #f8fafc; font-size: 11px; display: flex; flex-direction: column; gap: 6px;">
|
||||
<span style="color: #64748b;">复制下方专属链接,邀请好友注册聊天室:</span>
|
||||
<div style="display: flex; gap: 6px; align-items: center;">
|
||||
<input type="text" readonly :value="window.location.origin + '/' + userInfo.id"
|
||||
style="flex: 1; border: 1px solid #cbd5e1; border-radius: 4px; padding: 4px 6px; font-size: 11px; background: #fff; color: #334155;">
|
||||
<button
|
||||
x-on:click="navigator.clipboard.writeText(window.location.origin + '/' + userInfo.id); $alert('邀请链接已复制到剪贴板', '复制成功', '#22c55e');"
|
||||
style="background: #4f46e5; color: white; border: none; border-radius: 4px; padding: 4px 8px; font-size: 11px; cursor: pointer; font-weight: bold;">
|
||||
复制
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 管理员可见区域 (IP 与 归属地) --}}
|
||||
<div x-show="userInfo.last_ip !== undefined"
|
||||
style="margin-top: 8px; border-radius: 8px; overflow: hidden;">
|
||||
|
||||
139
resources/views/invite/leaderboard.blade.php
Normal file
139
resources/views/invite/leaderboard.blade.php
Normal file
@@ -0,0 +1,139 @@
|
||||
{{--
|
||||
文件功能:邀请排行达人榜
|
||||
展示全站前 50 名邀请人数最多的用户
|
||||
--}}
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('title', '新星邀请达人榜 - 飘落流星')
|
||||
|
||||
@section('nav-icon', '🏆')
|
||||
@section('nav-title', '邀请风云排行榜')
|
||||
|
||||
@section('content')
|
||||
<div class="max-w-7xl mx-auto py-10 px-4 sm:px-6 lg:px-8">
|
||||
|
||||
{{-- 顶部说明区域 --}}
|
||||
<div
|
||||
class="mb-8 bg-gradient-to-r from-rose-50 to-indigo-50 rounded-2xl p-6 md:p-10 shadow-sm border border-rose-100 flex flex-col md:flex-row items-center justify-between gap-6">
|
||||
<div>
|
||||
<h2
|
||||
class="text-3xl font-extrabold text-transparent bg-clip-text bg-gradient-to-r from-rose-600 to-indigo-600 mb-2">
|
||||
新星崛起·邀请达人榜
|
||||
</h2>
|
||||
<p class="text-rose-900/80 text-sm md:text-base max-w-2xl">
|
||||
欢迎来到漂落流星星光榜单!在房间里打开您的个人名片,复制专属推广链接去邀请新朋友吧!
|
||||
每成功邀请一位并完成注册,即可积累一次战绩,赶快向着全站 Top 50 进发!
|
||||
</p>
|
||||
</div>
|
||||
<div class="hidden md:flex shrink-0">
|
||||
<span class="text-7xl">🌟</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 榜单主体区域 --}}
|
||||
<div class="bg-white rounded-2xl shadow-sm border border-gray-100 overflow-hidden relative min-h-[400px]">
|
||||
@if ($topInviters->isNotEmpty())
|
||||
{{-- 表头 --}}
|
||||
<div
|
||||
class="grid grid-cols-12 gap-4 bg-gray-50/80 px-6 py-4 border-b border-gray-100 text-xs font-bold text-gray-500 uppercase tracking-wider sticky top-0 z-10">
|
||||
<div class="col-span-2 text-center">排行</div>
|
||||
<div class="col-span-1 text-center">头像</div>
|
||||
<div class="col-span-5 md:col-span-4 pl-4">用户昵称</div>
|
||||
<div class="col-span-4 md:col-span-3 text-left">在职职务</div>
|
||||
<div class="col-span-12 md:col-span-2 text-right font-black text-rose-500 mt-2 md:mt-0 max-md:hidden">
|
||||
邀请记录</div>
|
||||
</div>
|
||||
|
||||
{{-- 列表项 --}}
|
||||
<div class="divide-y divide-gray-50">
|
||||
@foreach ($topInviters as $index => $inviter)
|
||||
<div
|
||||
class="grid grid-cols-12 gap-4 items-center px-6 py-4 hover:bg-rose-50/30 transition duration-150 relative group">
|
||||
|
||||
{{-- 移动端显示的收徒数 (紧凑模式) --}}
|
||||
<div class="absolute right-4 top-1/2 -translate-y-1/2 md:hidden">
|
||||
<span class="font-bold text-rose-500 text-lg">{{ $inviter->invitees_count }}</span>
|
||||
<span class="text-[10px] text-rose-300">人</span>
|
||||
</div>
|
||||
|
||||
{{-- 1. 排名徽标 --}}
|
||||
<div class="col-span-2 flex justify-center">
|
||||
@if ($index === 0)
|
||||
<div
|
||||
class="w-10 h-10 flex items-center justify-center rounded-full bg-gradient-to-br from-yellow-300 to-yellow-500 text-white font-black text-lg shadow-md shadow-yellow-200 ring-2 ring-yellow-100">
|
||||
1</div>
|
||||
@elseif ($index === 1)
|
||||
<div
|
||||
class="w-9 h-9 flex items-center justify-center rounded-full bg-gradient-to-br from-gray-300 to-gray-400 text-white font-black text-base shadow-sm ring-2 ring-gray-100">
|
||||
2</div>
|
||||
@elseif ($index === 2)
|
||||
<div
|
||||
class="w-8 h-8 flex items-center justify-center rounded-full bg-gradient-to-br from-amber-600 to-amber-700 text-white font-bold text-sm shadow-sm ring-2 ring-amber-50">
|
||||
3</div>
|
||||
@elseif ($index < 10)
|
||||
<div
|
||||
class="w-7 h-7 flex items-center justify-center rounded-full bg-indigo-50 text-indigo-600 font-bold text-sm">
|
||||
{{ $index + 1 }}</div>
|
||||
@else
|
||||
<div class="w-7 h-7 flex items-center justify-center text-gray-400 font-medium text-sm">
|
||||
{{ $index + 1 }}</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 2. 头像 --}}
|
||||
<div class="col-span-1 flex justify-center">
|
||||
<div
|
||||
class="w-10 h-10 rounded-md overflow-hidden bg-white border border-gray-200 shadow-sm shrink-0">
|
||||
<img src="/images/headface/{{ strtolower($inviter->headface ?: '1.gif') }}"
|
||||
onerror="this.style.display='none'" class="w-full h-full object-cover">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{-- 3. 用户名 --}}
|
||||
<div class="col-span-5 md:col-span-4 pl-4 truncate">
|
||||
<span class="font-bold text-gray-800 {{ $index < 3 ? 'text-lg' : 'text-base' }}"
|
||||
title="{{ $inviter->username }}">
|
||||
{{ $inviter->username }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{{-- 4. 职务徽章 --}}
|
||||
<div class="col-span-4 md:col-span-3 flex items-center text-left">
|
||||
@if ($inviter->activePosition)
|
||||
<div class="inline-flex items-center gap-1.5 px-3 py-1 bg-gradient-to-r from-purple-50 to-indigo-50 rounded-full text-xs text-purple-700 border border-purple-100 shadow-sm max-w-full truncate"
|
||||
title="{{ $inviter->activePosition->position->name }}">
|
||||
<span>{{ $inviter->activePosition->position->icon }}</span>
|
||||
<span
|
||||
class="font-medium truncate">{{ $inviter->activePosition->position->name }}</span>
|
||||
</div>
|
||||
@else
|
||||
<span class="text-xs text-gray-300">—</span>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
{{-- 5. 收徒数 (PC 端) --}}
|
||||
<div class="col-span-12 md:col-span-2 text-right hidden md:block">
|
||||
<div class="inline-flex items-baseline gap-1">
|
||||
<span
|
||||
class="text-2xl font-black text-rose-500 tracking-tight">{{ $inviter->invitees_count }}</span>
|
||||
<span class="text-xs font-semibold text-rose-300">人</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@else
|
||||
{{-- 空白提示区 --}}
|
||||
<div class="absolute inset-0 flex flex-col items-center justify-center">
|
||||
<div class="text-6xl mb-4 grayscale opacity-50">🚀</div>
|
||||
<h3 class="text-xl font-bold text-gray-800 mb-2">全站排行榜暂无数据</h3>
|
||||
<p class="text-gray-500 text-sm max-w-md text-center bg-gray-50 p-4 rounded-lg">
|
||||
目前还没有达人拉取到足够的新用户入驻。成为第一个霸榜的人吧!
|
||||
<br><br>
|
||||
<span class="font-semibold text-rose-500">提示:</span> 在聊天室左上角或右侧的在线列表中点击您自己的名字,即可获取并复制专属推广链接。
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
@endsection
|
||||
@@ -73,6 +73,10 @@
|
||||
class="text-indigo-200 hover:text-white font-bold flex items-center transition hidden sm:flex">
|
||||
大厅
|
||||
</a>
|
||||
<a href="{{ route('invite.leaderboard') }}"
|
||||
class="text-rose-400 hover:text-rose-300 font-bold flex items-center transition hidden sm:flex {{ request()->routeIs('invite.leaderboard') ? 'text-rose-200 underline underline-offset-4' : '' }}">
|
||||
邀请排行
|
||||
</a>
|
||||
<a href="{{ route('leaderboard.index') }}"
|
||||
class="text-yellow-400 hover:text-yellow-300 font-bold flex items-center transition hidden sm:flex">
|
||||
风云榜
|
||||
|
||||
@@ -40,12 +40,18 @@ Route::middleware(['chat.auth'])->group(function () {
|
||||
Route::delete('/rooms/{id}', [RoomController::class, 'destroy'])->name('rooms.destroy');
|
||||
Route::post('/rooms/{id}/transfer', [RoomController::class, 'transfer'])->name('rooms.transfer');
|
||||
|
||||
// ---- 第九阶段:外围矩阵 - 风云排行榜 ----
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 排行榜系统 (风云榜、土豪榜等)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
Route::get('/leaderboard', [\App\Http\Controllers\LeaderboardController::class, 'index'])->name('leaderboard.index');
|
||||
// 今日风云榜(独立页,经验/金币/魅力今日排行)
|
||||
Route::get('/leaderboard/today', [\App\Http\Controllers\LeaderboardController::class, 'todayIndex'])->name('leaderboard.today');
|
||||
Route::get('/leaderboard/today', [\App\Http\Controllers\LeaderboardController::class, 'today'])->name('leaderboard.today');
|
||||
|
||||
// 用户个人积分流水日志(查询自己的经验/金币/魅力历史)
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 邀请排行达人榜
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
Route::get('/invites/leaderboard', [\App\Http\Controllers\InviteController::class, 'leaderboard'])->name('invite.leaderboard');
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════用户个人积分流水日志(查询自己的经验/金币/魅力历史)
|
||||
Route::get('/my/currency-logs', [\App\Http\Controllers\LeaderboardController::class, 'myLogs'])->name('currency.my-logs');
|
||||
|
||||
// ---- 勤务台(展示四榜)----
|
||||
@@ -529,3 +535,11 @@ Route::middleware(['chat.auth', 'chat.has_position'])->prefix('admin')->name('ad
|
||||
Route::delete('/forbidden-usernames/{id}', [\App\Http\Controllers\Admin\ForbiddenUsernameController::class, 'destroy'])->name('forbidden-usernames.destroy');
|
||||
});
|
||||
});
|
||||
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
// 邀请链接路由 (严格纯数字)
|
||||
// 必须放在最后以避免与其他如 /admin 路由冲突
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
Route::get('/{inviter_id}', [\App\Http\Controllers\InviteController::class, 'handle'])
|
||||
->where('inviter_id', '[0-9]+')
|
||||
->name('invite.link');
|
||||
|
||||
Reference in New Issue
Block a user