增加 推介系统

This commit is contained in:
iVampireSP.com 2023-02-23 02:00:53 +08:00
parent 6a18314e06
commit 16cec5c737
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
11 changed files with 441 additions and 0 deletions

View File

@ -0,0 +1,102 @@
<?php
namespace App\Http\Controllers\Web;
use App\Http\Controllers\Controller;
use App\Models\Affiliate\Affiliates;
use App\Models\Affiliate\AffiliateUser;
use App\Models\User;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class AffiliateController extends Controller
{
/**
* Display a listing of the resource.
*/
public function index(): View|RedirectResponse
{
$user = auth()->user();
$user->load('affiliate');
$affiliate = $user->affiliate;
// 检测用户是否激活了推介计划
if (! $affiliate) {
return redirect()->route('affiliates.create');
}
$affiliateUsers = auth()->user()->affiliateUsers()->paginate(10);
return view('affiliates.index', compact('affiliateUsers', 'affiliate'));
}
/**
* Show the form for creating a new resource.
*/
public function create(): View|RedirectResponse
{
$user = auth('web')->user();
$user->load('affiliate', 'affiliateUser.affiliate.user');
if ($user->affiliate) {
return redirect()->route('affiliates.index')->with('error', '您已经激活了推介计划。');
}
return view('affiliates.create', compact('user'));
}
/**
* Store a newly created resource in storage.
*/
public function store(Request $request): RedirectResponse
{
if (auth()->user()->affiliate) {
return redirect()->route('affiliates.index')->with('error', '您已经激活了推介计划。');
}
$request->user('web')->affiliate()->create();
return redirect()->route('affiliates.index')->with('success', '欢迎您,并感谢您。');
}
/**
* Display the specified resource.
*/
public function show(Affiliates $affiliate): RedirectResponse
{
if (auth('web')->guest()) {
// save the affiliate id in the session
session()->put('affiliate_id', $affiliate->id);
$cache_key = 'affiliate_ip:'.$affiliate->id.':'.request()->ip();
if (! Cache::has($cache_key)) {
$affiliate->increment('visits');
Cache::put($cache_key, true, now()->addHour());
}
}
return redirect()->route('index');
}
/**
* Remove the specified resource from storage.
*/
public function destroy(Affiliates $affiliate): RedirectResponse
{
// 检测是不是自己的推介计划
if ($affiliate->user_id !== auth()->id()) {
return redirect()->route('affiliates.index')->with('error', '您没有权限删除此推介计划。');
}
AffiliateUser::where('affiliate_id', $affiliate->id)->delete();
User::where('affiliate_id', $affiliate->id)->update(['affiliate_id' => null]);
$affiliate->delete();
return redirect()->route('affiliates.create')->with('success', '推介计划已经成功删除。');
}
}

View File

@ -0,0 +1,85 @@
<?php
namespace App\Models\Affiliate;
use App\Models\User;
use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Support\Facades\Cache;
class AffiliateUser extends Model
{
use Cachable;
public $fillable = [
'revenue',
'affiliate_id',
'user_id',
];
public $casts = [
'revenue' => 'decimal:2',
];
public $with = [
'user',
'originalAffiliateUser',
];
public function affiliate(): BelongsTo
{
return $this->belongsTo(Affiliates::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function originalAffiliateUser(): HasOneThrough
{
return $this->hasOneThrough(
User::class,
Affiliates::class,
'user_id',
'id',
'affiliate_id',
'user_id'
);
}
// 给用户添加佣金
public function addRevenue(string $revenue): void
{
$this->load('user');
if (! $this->user->isRealNamed()) {
return;
}
$this->load('affiliate.user');
// 给 affiliate_id 中的 user_id 添加佣金
Cache::lock('affiliate_user_'.$this->id, 10)->block(10, function () use ($revenue) {
// 计算应得
$commission_referral = config('settings.billing.commission_referral') * 100;
$revenue = bcdiv($revenue, $commission_referral, 2);
$this->update([
'revenue' => bcadd($this->revenue, $revenue, 2),
]);
// 给上级添加佣金
if ($this->affiliate->user_id) {
$this->affiliate->update([
'revenue' => bcadd($this->affiliate->revenue, $revenue, 2),
]);
$this->affiliate->user->charge($revenue, 'affiliate', '下属用户 '.$this->user->name.'#'.$this->user_id.' 充值所获得的佣金。');
}
});
}
}

View File

@ -0,0 +1,50 @@
<?php
namespace App\Models\Affiliate;
use App\Models\User;
use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
class Affiliates extends Model
{
use Cachable;
public $fillable = [
'uuid',
'visits',
'revenue',
'user_id',
];
public $casts = [
'visits' => 'integer',
'revenue' => 'decimal:2',
];
public static function booted()
{
static::creating(function (self $affiliate) {
$affiliate->uuid = Str::ulid();
});
}
public function scopeThisUser(Builder $query): Builder
{
return $query->where('user_id', auth('web')->id());
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function users(): HasMany
{
return $this->hasMany(AffiliateUser::class);
}
}

View File

@ -4,6 +4,8 @@
use App\Events\Users;
use App\Exceptions\User\BalanceNotEnoughException;
use App\Models\Affiliate\Affiliates;
use App\Models\Affiliate\AffiliateUser;
use GeneaLabs\LaravelModelCaching\CachedBuilder;
use GeneaLabs\LaravelModelCaching\Traits\Cachable;
use Illuminate\Contracts\Auth\MustVerifyEmail;
@ -14,6 +16,9 @@
use Illuminate\Database\Eloquent\Prunable;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Database\Eloquent\Relations\HasManyThrough;
use Illuminate\Database\Eloquent\Relations\HasOne;
use Illuminate\Database\Eloquent\Relations\HasOneThrough;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
@ -48,6 +53,7 @@ class User extends Authenticatable implements MustVerifyEmail
'email',
'password',
'receive_marketing_email',
'affiliate_id',
];
/**
@ -82,6 +88,27 @@ public function hosts(): HasMany
return $this->hasMany(Host::class);
}
public function affiliate(): HasOne
{
return $this->hasOne(Affiliates::class);
}
public function affiliateUsers(): HasManyThrough
{
return $this->hasManyThrough(AffiliateUser::class, Affiliates::class, 'user_id', 'affiliate_id');
}
public function affiliateUser(): BelongsTo
{
return $this->belongsTo(AffiliateUser::class, 'affiliate_id');
}
// 通过 affiliate_id 获取到 affiliates 中的 user_id
public function promoter(): HasOneThrough
{
return $this->hasOneThrough(User::class, Affiliates::class, 'id', 'id', 'affiliate_id', 'user_id');
}
public function getBirthdayFromIdCard(string|null $id_card = null): Carbon
{
if (empty($id_card)) {

View File

@ -24,10 +24,17 @@ public function updated(Balance $balance): void
{
if ($balance->isDirty('paid_at')) {
if ($balance->paid_at) {
$balance->load('user');
$balance->load('user.affiliateUser');
$balance->notify(new UserCharged());
broadcast(new Users($balance->user, 'balance.updated', $balance));
$balance->user->charge($balance->amount, $balance->payment, $balance->order_id);
if ($balance->user->affiliate_id) {
$balance->user->affiliateUser->addRevenue($balance->amount);
}
}
}
}

View File

@ -2,16 +2,39 @@
namespace App\Observers;
use App\Models\Affiliate\AffiliateUser;
use App\Models\User;
use Illuminate\Support\Facades\Crypt;
use Illuminate\Support\Str;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
class UserObserver
{
/**
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function creating(User $user): void
{
$user->email_md5 = md5($user->email);
$user->uuid = Str::uuid();
// if session has affiliate_id, then set it to user
if (session()->has('affiliate_id')) {
$user->affiliate_id = session()->get('affiliate_id');
}
}
public function created(User $user): void
{
// if user has affiliate_id, then create an affiliate_user record
if ($user->affiliate_id) {
AffiliateUser::create([
'affiliate_id' => $user->affiliate_id,
'user_id' => $user->id,
]);
}
}
public function updating(User $user): void

View File

@ -0,0 +1,61 @@
<?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::create('affiliates', function (Blueprint $table) {
$table->id();
$table->ulid()->index();
// 访问数量
$table->unsignedBigInteger('visits')->default(0);
// 累计收益
$table->decimal('revenue', 10)->default(0);
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamps();
});
Schema::create('affiliate_users', function (Blueprint $table) {
$table->id();
// 从中盈利
$table->decimal('revenue', 10)->default(0);
$table->foreignId('affiliate_id')->constrained('affiliates')->cascadeOnDelete();
$table->foreignId('user_id')->constrained('users')->cascadeOnDelete();
$table->timestamps();
});
Schema::table('users', function (Blueprint $table) {
$table->foreignId('affiliate_id')->nullable()->after('user_group_id')->constrained('affiliates')->nullOnDelete();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('users', function (Blueprint $table) {
$table->dropForeign(['affiliate_id']);
$table->dropColumn('affiliate_id');
});
Schema::dropIfExists('affiliate_users');
Schema::dropIfExists('affiliates');
}
};

View File

@ -0,0 +1,24 @@
@extends('layouts.app')
@section('title', '推介计划')
@section('content')
<h3>加入推介计划</h3>
<p>
让更多人用上我们的产品,您将从他们实名认证成功之后的每笔充值中获取 {{ config('settings.billing.commission_referral') * 100 . '%' }}
的佣金。</p>
@if ($user->affiliate_id)
<span>您被 {{ $user->affiliateUser->affiliate->user->name }}#{{ $user->affiliateUser->affiliate->user_id }} 引荐。</span>
@if ($user->affiliateUser->affiliate->revenue > 5)
<span>您的推介人已经获得了 {{ $user->affiliateUser->affiliate->revenue }} 元的佣金。</span>
@endif
@endif
<form class="mt-3" method="post" action="{{ route('affiliates.store') }}">
@csrf
<button type="submit" class="btn btn-primary">加入推介计划</button>
</form>
@endsection

View File

@ -0,0 +1,52 @@
@extends('layouts.app')
@section('title', '推介计划')
@section('content')
<h3>推介计划</h3>
<p>
访问量:{{ $affiliate->visits }}
</p>
<p>
盈利:{{ $affiliate->revenue }}
</p>
<p>推介 URL: {{ \Illuminate\Support\Facades\URL::route('affiliates.show', $affiliate->uuid) }}</p>
<h3>用户列表</h3>
@php($count = $affiliateUsers->count())
@if ($count)
<table class="table table-striped">
<thead>
<tr>
<th scope="col">用户名</th>
<th scope="col">盈利</th>
<th scope="col">注册时间</th>
</tr>
</thead>
<tbody>
@foreach($affiliateUsers as $user)
<tr>
<td>{{ $user->user->name }}</td>
<td>{{ $user->revenue }} </td>
<td>{{ $user->created_at }}</td>
</tr>
@endforeach
</tbody>
</table>
{{ $affiliateUsers->links() }}
@else
<p>您还没有推介用户。</p>
@endif
<h4>离开推介计划</h4>
<form method="post" action="{{ route('affiliates.destroy', $affiliate->id) }}"
onclick="return confirm('删除后将不会获得收益,推介数据也会被删除。')">
@csrf
@method('DELETE')
<button type="submit" class="btn btn-danger">删除推介计划</button>
</form>
@endsection

View File

@ -55,6 +55,9 @@
<li class="nav-item">
<a class="nav-link" href="{{ route('transactions') }}">记录</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ route('affiliates.index') }}">推介</a>
</li>
@endauth
<li class="nav-item">

View File

@ -1,5 +1,6 @@
<?php
use App\Http\Controllers\Web\AffiliateController;
use App\Http\Controllers\Web\Auth\ConfirmPasswordController;
use App\Http\Controllers\Web\Auth\ForgotPasswordController;
use App\Http\Controllers\Web\Auth\LoginController;
@ -83,6 +84,10 @@ function () {
Route::post('real_name', [RealNameController::class, 'store'])->name('real_name.store');
/* End 实名认证 */
/* Start 推介 */
Route::resource('affiliates', AffiliateController::class)->only(['index', 'create', 'store', 'destroy']);
/* End 推介 */
/* Start 匿名登录 */
Route::get('auth_request/{auth_request}', [AuthController::class, 'showAuthRequest'])->withoutMiddleware(['auth:web', 'verified'])->name('auth_request.show');
Route::post('auth_request', [AuthController::class, 'storeAuthRequest'])->name('auth_request.store');
@ -98,3 +103,5 @@ function () {
// 维护
Route::get('maintenance', MaintenanceController::class)->name('maintenances');
Route::middleware('guest')->get('affiliates/{affiliate:uuid}', [AffiliateController::class, 'show'])->name('affiliates.show');