移除 周期性计费

This commit is contained in:
iVampireSP.com 2023-02-20 00:10:18 +08:00
parent d8503a04a3
commit ac538adc3a
No known key found for this signature in database
GPG Key ID: 2F7B001CA27A8132
11 changed files with 118 additions and 314 deletions

View File

@ -5,7 +5,6 @@
use App\Jobs\Host\DeleteHostJob;
use App\Jobs\Host\DispatchHostCostQueueJob;
use App\Jobs\Host\ScanAllHostsJob;
use App\Jobs\Host\SuspendOverdueHosts;
use App\Jobs\Module\DispatchFetchModuleJob;
use App\Jobs\Module\SendModuleEarningsJob;
use App\Jobs\User\CheckAndChargeBalanceJob;
@ -61,9 +60,6 @@ protected function schedule(Schedule $schedule): void
// 设置生日用户组
$schedule->job(new SetBirthdayGroupJob())->dailyAt('00:00')->onOneServer()->name('设置生日用户组');
// 暂停到期的循环计费主机
$schedule->job(new SuspendOverdueHosts())->dailyAt('00:00')->onOneServer()->name('暂停到期的循环计费主机');
}
/**

View File

@ -66,12 +66,6 @@ public function store(Request $request): Response|JsonResponse
$host = (new Host)->create($data);
if (! $user->hasBalance($host->getRenewPrice())) {
$host->delete();
return $this->error('此用户余额不足,无法开计费项目。');
}
$host['host_id'] = $host->id;
return $this->created($host);
@ -127,27 +121,6 @@ public function destroy($host): JsonResponse
$host = (new Host)->findOrFail($host);
}
if ($host?->isCycle()) {
$days = $host->next_due_at->diffInDays(now());
// 算出 1 天的价格
$price = bcdiv($host->getPrice(), 31, 4);
// 算出退还的金额
$amount = bcmul($price, $days, 4);
$host->user->charge($amount, 'balance', '删除主机退款。', [
'module_id' => $this->module_id,
'host_id' => $this->id,
'user_id' => $this->user_id,
]);
$host->module->reduce($amount, '删除主机退款。', false, [
'module_id' => $this->module_id,
'host_id' => $this->id,
]);
}
$host?->delete();
return $this->deleted();

View File

@ -6,14 +6,14 @@
use App\Http\Controllers\Controller;
use App\Notifications\User\UserNotification;
use function back;
use function config;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Auth;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
use function back;
use function config;
use function redirect;
use function session;
use function view;
@ -33,13 +33,13 @@ public function index(Request $request): View|RedirectResponse
$dashboardHost = parse_url(config('settings.dashboard.base_url'), PHP_URL_HOST);
if ($callbackHost === $dashboardHost) {
if (!$request->user('web')->isRealNamed()) {
if (! $request->user('web')->isRealNamed()) {
return redirect()->route('real_name.create')->with('status', '重定向已被打断,需要先实人认证。');
}
$token = $request->user()->createToken('Dashboard')->plainTextToken;
return redirect($callback . '?token=' . $token);
return redirect($callback.'?token='.$token);
}
session(['referer.domain' => parse_url($request->header('referer'), PHP_URL_HOST)]);
@ -121,7 +121,7 @@ public function exitSudo(): RedirectResponse
public function showAuthRequest($token): View|RedirectResponse
{
$data = Cache::get('auth_request:' . $token);
$data = Cache::get('auth_request:'.$token);
if (empty($data)) {
return redirect()->route('index')->with('error', '登录请求的 Token 不存在或已过期。');
@ -145,7 +145,7 @@ public function storeAuthRequest(Request $request): RedirectResponse
'token' => 'required|string|max:128',
]);
$data = Cache::get('auth_request:' . $request->input('token'));
$data = Cache::get('auth_request:'.$request->input('token'));
if (empty($data)) {
return back()->with('error', '登录请求的 Token 不存在或已过期。');
@ -169,7 +169,7 @@ public function storeAuthRequest(Request $request): RedirectResponse
$data['token'] = $user->createToken($data['meta']['description'] ?? Carbon::now()->toDateString(), $abilities)->plainTextToken;
}
Cache::put('auth_request:' . $request->input('token'), $data, 60);
Cache::put('auth_request:'.$request->input('token'), $data, 60);
return redirect()->route('index')->with('success', '登录请求已确认。');
}

View File

@ -6,7 +6,6 @@
use App\Models\Host;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\View\View;
class HostController extends Controller
@ -19,9 +18,8 @@ class HostController extends Controller
public function index(): View
{
$hosts = (new Host)->thisUser()->with(['user', 'module'])->paginate(20);
$times = config('settings.billing.cycle_delete_times_every_month') - Cache::get('host_delete_times:'.auth('web')->id(), 0);
return view('hosts.index', compact('hosts', 'times'));
return view('hosts.index', compact('hosts'));
}
public function update(Request $request, Host $host): RedirectResponse
@ -35,25 +33,6 @@ public function update(Request $request, Host $host): RedirectResponse
return $status ? back()->with('success', '修改成功。') : back()->with('error', '修改失败。');
}
public function renew(Host $host): RedirectResponse
{
$price = $host->getRenewPrice();
if ($price > auth()->user()->balance) {
return back()->with('error', '余额不足,续费需要:'.$price.' 元,您还需要充值:'.($price - auth()->user()->balance).' 元');
}
if (! $host->isCycle()) {
return back()->with('error', '该主机不是周期性付费,无法续费。');
}
if ($host->renew()) {
return back()->with('success', '续费成功,新的到期时间为:'.$host->next_due_at.'。');
}
return back()->with('error', '续费失败,请检查是否有足够的余额。');
}
/**
* Remove the specified resource from storage.
*

View File

@ -43,7 +43,7 @@ public function handle(): void
$host = $host->where('minute_at', $this->minute);
}
$host->whereIn('status', ['running', 'stopped'])->whereNull('billing_cycle')->with('user')->chunk(500, function ($hosts) {
$host->whereIn('status', ['running', 'stopped'])->with('user')->chunk(500, function ($hosts) {
foreach ($hosts as $host) {
dispatch(new self($this->minute, $host));
}

View File

@ -1,47 +0,0 @@
<?php
namespace App\Jobs\Host;
use App\Models\Host;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class SuspendOverdueHosts implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected ?Host $host;
/**
* Create a new job instance.
*
* @return void
*/
public function __construct(?Host $host = null)
{
$this->host = $host;
}
/**
* Execute the job.
*
* @return void
*/
public function handle(): void
{
if (! $this->host) {
(new Host)->where('next_due_at', '<', now())
->where('status', '!=', 'suspended')
->chunk(100, function ($hosts) {
foreach ($hosts as $host) {
dispatch(new self($host));
}
});
}
$this->host?->suspend();
}
}

View File

@ -2,7 +2,6 @@
namespace App\Models;
use App\Exceptions\User\BalanceNotEnoughException;
use App\Jobs\Host\HostJob;
use App\Jobs\Host\UpdateOrDeleteHostJob;
use GeneaLabs\LaravelModelCaching\Traits\Cachable;
@ -47,26 +46,21 @@ public function getUserHosts($user_id = null): array|Collection
})->get();
}
public function user(): BelongsToAlias
{
return $this->belongsTo(User::class);
}
public function module(): BelongsToAlias
{
return $this->belongsTo(Module::class);
}
public function scopeActive($query)
{
return $query->whereIn('status', ['running', 'stopped']);
}
// public function workOrders(): HasManyAlias
// {
// return $this->hasMany(WorkOrder::class);
// }
public function scopeActive($query)
{
return $query->whereIn('status', ['running', 'stopped']);
}
public function scopeDraft($query)
{
return $query->where('status', 'draft');
@ -91,11 +85,6 @@ public function scopeThisUser($query, $module = null)
}
}
public function scopeExpiringDays($query, $days)
{
return $query->where('status', 'running')->where('next_due_at', '<=', now()->addDays($days));
}
public function isDraft(): bool
{
return $this->status === 'draft';
@ -116,175 +105,20 @@ public function isUnavailable(): bool
return $this->status === 'unavailable';
}
public function renew($first = false): bool
{
if (! $this->isCycle()) {
return false;
}
$price = $this->getRenewPrice();
$description = ($first ? '新购' : '续费').' '.$this->name.',价格:'.$price.' 元。';
try {
$this->user->reduce($price, $description, true, [
'module_id' => $this->module_id,
'host_id' => $this->id,
'user_id' => $this->user_id,
]);
$this->module->charge($price, 'balance', '用户'.$description, [
'module_id' => $this->module_id,
'host_id' => $this->id,
]);
} catch (BalanceNotEnoughException) {
return false;
}
$this->addLog($price);
$this->next_due_at = $this->getNewDueDate();
$this->last_paid = $price;
if ($this->isSuspended()) {
$this->run();
}
$this->save();
return true;
}
public function isCycle(): bool
{
return $this->billing_cycle !== null;
}
public function getRenewPrice(): string
{
return match ($this->billing_cycle) {
'monthly' => $this->getPrice(),
'quarterly' => bcmul($this->getPrice(), 3),
'semi-annually' => bcmul($this->getPrice(), 6),
'annually' => bcmul($this->getPrice(), 12),
'biennially' => bcmul($this->getPrice(), 24),
'triennially' => bcmul($this->getPrice(), 36),
default => '0',
};
}
public function getPrice(): float
{
return $this->managed_price ?? $this->price;
}
public function addLog(string $amount = '0'): bool
{
if ($amount === '0') {
return false;
}
/** 统计收益开始 */
$current_month = now()->month;
$current_year = now()->year;
$cache_key = 'module_earning_'.$this->module_id;
// 应支付的提成
$commission = config('settings.billing.commission');
$should_amount = bcmul($amount, $commission, 4);
// 应得的余额
$should_balance = bcsub($amount, $should_amount, 4);
// 如果太小,则重置为 0.0001
if ($should_balance < 0.0001) {
$should_balance = 0.0001;
}
$earnings = Cache::get($cache_key, []);
if (! isset($earnings[$current_year])) {
$earnings[$current_year] = [];
}
if (isset($earnings[$current_year][$current_month])) {
$earnings[$current_year][$current_month]['balance'] = bcadd($earnings[$current_year][$current_month]['balance'], $amount, 4);
$earnings[$current_year][$current_month]['should_balance'] = bcadd($earnings[$current_year][$current_month]['should_balance'], $should_balance, 4);
} else {
$earnings[$current_year][$current_month] = [
'balance' => $amount,
// 应得(交了手续费)
'should_balance' => $should_balance,
];
}
// 删除 前 3 年的数据
if (count($earnings) > 3) {
$earnings = array_slice($earnings, -3, 3, true);
}
$this->module->charge($amount, 'balance', null);
// 保存 1 年
Cache::forever($cache_key, $earnings);
/** 统计收益结束 */
return true;
}
public function getNewDueDate(): string
{
$this->next_due_at = $this->next_due_at ?? now();
return match ($this->billing_cycle) {
'monthly' => $this->next_due_at->addMonth(),
'quarterly' => $this->next_due_at->addMonths(3),
'semi-annually' => $this->next_due_at->addMonths(6),
'annually' => $this->next_due_at->addYear(),
'biennially' => $this->next_due_at->addYears(2),
'triennially' => $this->next_due_at->addYears(3),
default => null,
};
}
public function isSuspended(): bool
{
return $this->status === 'suspended';
}
public function run(): bool
{
$this->update([
'status' => 'running',
]);
return true;
}
public function safeDelete(): bool
{
$is_user = auth()->guard('sanctum')->check() || auth()->guard('web')->check();
if ($this->isCycle() && $is_user) {
// 周期性的,每个月只能删除固定次数
$times = Cache::remember('host_delete_times:'.$this->user_id, 60 * 24 * 30, function () {
return 0;
});
if ($times >= config('settings.billing.cycle_delete_times_every_month')) {
return false;
}
Cache::increment('host_delete_times:'.$this->user_id);
// 根据 next_due_at 来计算退还的金额
if ($this->next_due_at === null) {
$this->next_due_at = now();
}
}
// 如果创建时间大于 1 小时
if (! $this->isCycle() && $this->created_at->diffInHours(now()) > 1) {
if ($this->created_at->diffInHours(now()) > 1) {
// 如果当前时间比扣费时间小,则说明没有扣费。执行扣费。
if (now()->minute < $this->minute_at) {
$this->cost();
@ -396,6 +230,61 @@ public function cost(
return true;
}
public function addLog(string $amount = '0'): bool
{
if ($amount === '0') {
return false;
}
/** 统计收益开始 */
$current_month = now()->month;
$current_year = now()->year;
$cache_key = 'module_earning_'.$this->module_id;
// 应支付的提成
$commission = config('settings.billing.commission');
$should_amount = bcmul($amount, $commission, 4);
// 应得的余额
$should_balance = bcsub($amount, $should_amount, 4);
// 如果太小,则重置为 0.0001
if ($should_balance < 0.0001) {
$should_balance = 0.0001;
}
$earnings = Cache::get($cache_key, []);
if (! isset($earnings[$current_year])) {
$earnings[$current_year] = [];
}
if (isset($earnings[$current_year][$current_month])) {
$earnings[$current_year][$current_month]['balance'] = bcadd($earnings[$current_year][$current_month]['balance'], $amount, 4);
$earnings[$current_year][$current_month]['should_balance'] = bcadd($earnings[$current_year][$current_month]['should_balance'], $should_balance, 4);
} else {
$earnings[$current_year][$current_month] = [
'balance' => $amount,
// 应得(交了手续费)
'should_balance' => $should_balance,
];
}
// 删除 前 3 年的数据
if (count($earnings) > 3) {
$earnings = array_slice($earnings, -3, 3, true);
}
$this->module->charge($amount, 'balance', null);
// 保存 1 年
Cache::forever($cache_key, $earnings);
/** 统计收益结束 */
return true;
}
public function changeStatus(
string $status
): bool {
@ -406,14 +295,14 @@ public function changeStatus(
return false;
}
if (! $this->isCycle() && ! $user->hasBalance('0.5')) {
if (! $user->hasBalance('0.5')) {
return false;
}
}
if ($status === 'running') {
return $this->run();
} elseif (($status === 'suspended' || $status === 'suspend') && ! $this->isCycle()) {
} elseif (($status === 'suspended' || $status === 'suspend')) {
return $this->suspend();
} elseif ($status === 'stopped') {
return $this->stop();
@ -422,6 +311,11 @@ public function changeStatus(
return false;
}
public function user(): BelongsToAlias
{
return $this->belongsTo(User::class);
}
public function isPending(): bool
{
return $this->status === 'pending';
@ -432,6 +326,15 @@ public function isOverdue(): bool
return now()->gt($this->next_due_at);
}
public function run(): bool
{
$this->update([
'status' => 'running',
]);
return true;
}
public function suspend(): bool
{
$this->update([

View File

@ -38,10 +38,6 @@ public function created(Host $host): void
$host->user->notify(new WebNotification($host, 'hosts.created'));
if ($host->isCycle()) {
$host->renew(true);
}
$host->save();
}

View File

@ -3,7 +3,6 @@
return [
'billing' => [
'commission' => '0.1',
'cycle_delete_times_every_month' => 2,
],
'wecom' => [
'robot_hook' => [

View File

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up(): void
{
Schema::table('hosts', function (Blueprint $table) {
$table->dropColumn('next_due_at');
$table->dropColumn('billing_cycle');
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down(): void
{
echo 'This migration cannot be reversed.'.PHP_EOL;
}
};

View File

@ -38,14 +38,6 @@
{{ $host->price }}
@endif
<br/>
@if($host->billing_cycle)
<x-billing-cycle :cycle="$host->billing_cycle"/>
到期时间:{{ $host->next_due_at }}
<br />
续费价格: {{ $host->getRenewPrice() }}
<br />
续费后到期时间: {{ $host->getNewDueDate() }}
@endif
</td>
<td>
<x-host-status :status="$host->status"/>
@ -65,19 +57,6 @@
操作
</button>
<ul class="dropdown-menu">
@if($host->billing_cycle)
<a class="dropdown-item" href="#"
onclick="return confirm('确定续费此主机?') ? document.getElementById('renew-{{$host->id}}').submit() : false;">
续费此主机
</a>
<form action="{{ route('hosts.renew', $host) }}" id="renew-{{$host->id}}"
method="post" class="d-none">
@csrf
</form>
@endif
@if(!$host->isRunning())
<a class="dropdown-item" href="#"
onclick="return confirm('确定执行此操作?') ? document.getElementById('start-{{$host->id}}').submit() : false;">
@ -92,7 +71,7 @@
</form>
@endif
@if(!$host->isSuspended() && !$host->isCycle())
@if(!$host->isSuspended())
<a class="dropdown-item" href="#"
onclick="return confirm('确定执行此操作?') ? document.getElementById('start-{{$host->id}}').submit() : false;">
暂停此主机
@ -128,9 +107,4 @@
{{-- 分页 --}}
{{ $hosts->links() }}
<br/>
<p>还剩下周期性计费删除次数: {{ $times }}。当次数用完后,您的周期性计费主机只能在到期后删除。</p>
<p>当您的主机处于 "暂停" 状态时计费会被终止。但是请注意3 天后主机将会被删除,请合理使用。</p>
@endsection