增加 订阅
This commit is contained in:
parent
4fecd91165
commit
d7a294aaf9
@ -7,6 +7,8 @@
|
|||||||
use App\Jobs\Host\ScanAllHostsJob;
|
use App\Jobs\Host\ScanAllHostsJob;
|
||||||
use App\Jobs\Module\DispatchFetchModuleJob;
|
use App\Jobs\Module\DispatchFetchModuleJob;
|
||||||
use App\Jobs\Module\SendModuleEarningsJob;
|
use App\Jobs\Module\SendModuleEarningsJob;
|
||||||
|
use App\Jobs\Subscription\DeleteDraftJob;
|
||||||
|
use App\Jobs\Subscription\UpdateSubscriptionStatusJob;
|
||||||
use App\Jobs\User\CheckAndChargeBalanceJob;
|
use App\Jobs\User\CheckAndChargeBalanceJob;
|
||||||
use App\Jobs\User\ClearTasksJob;
|
use App\Jobs\User\ClearTasksJob;
|
||||||
use App\Jobs\User\DeleteUnverifiedUserJob;
|
use App\Jobs\User\DeleteUnverifiedUserJob;
|
||||||
@ -61,6 +63,10 @@ protected function schedule(Schedule $schedule): void
|
|||||||
|
|
||||||
// 删除注册超过 3 天未验证邮箱的用户
|
// 删除注册超过 3 天未验证邮箱的用户
|
||||||
$schedule->job(new DeleteUnverifiedUserJob())->daily()->onOneServer()->name('删除注册超过 3 天未验证邮箱的用户');
|
$schedule->job(new DeleteUnverifiedUserJob())->daily()->onOneServer()->name('删除注册超过 3 天未验证邮箱的用户');
|
||||||
|
|
||||||
|
// 订阅
|
||||||
|
$schedule->job(new DeleteDraftJob())->daily()->onOneServer()->name('删除超过 1 天的草稿订阅');
|
||||||
|
$schedule->job(new UpdateSubscriptionStatusJob())->everyMinute()->onOneServer()->name('更新订阅状态');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
66
app/Http/Controllers/Api/SubscriptionController.php
Normal file
66
app/Http/Controllers/Api/SubscriptionController.php
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SubscriptionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(): JsonResponse
|
||||||
|
{
|
||||||
|
$subscriptions = Subscription::thisUser()->with('module')->orderBy('status')->paginate();
|
||||||
|
|
||||||
|
return $this->success($subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Subscription $subscription): JsonResponse
|
||||||
|
{
|
||||||
|
$subscription->load('module');
|
||||||
|
|
||||||
|
return $this->success($subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(Request $request, Subscription $subscription): JsonResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'cancel_at_period_end' => 'nullable|boolean',
|
||||||
|
'status' => 'nullable|in:active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->filled('cancel_at_period_end')) {
|
||||||
|
$subscription->update([
|
||||||
|
'cancel_at_period_end' => $request->cancel_at_period_end,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('status') && $request->input('status') === 'active') {
|
||||||
|
if (! $subscription->active()) {
|
||||||
|
return $this->badRequest('无法激活此订阅。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->success($subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Subscription $subscription): JsonResponse
|
||||||
|
{
|
||||||
|
$subscription->safeDelete();
|
||||||
|
|
||||||
|
return $this->deleted();
|
||||||
|
}
|
||||||
|
}
|
108
app/Http/Controllers/Module/SubscriptionController.php
Normal file
108
app/Http/Controllers/Module/SubscriptionController.php
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Module;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SubscriptionController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index(Request $request, User $user): JsonResponse
|
||||||
|
{
|
||||||
|
$subscriptions = $user->subscriptions()->where('module_id', $request->user('module')->id);
|
||||||
|
|
||||||
|
if ($request->filled('status')) {
|
||||||
|
$subscriptions->where('status', $request->input('status'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('plan_id')) {
|
||||||
|
$subscriptions->where('plan_id', $request->input('plan_id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscriptions = $subscriptions->paginate();
|
||||||
|
|
||||||
|
return $this->success($subscriptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 向用户发送订阅请求。
|
||||||
|
*/
|
||||||
|
public function store(Request $request, User $user): JsonResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'name' => 'required|string|max:255',
|
||||||
|
'plan_id' => 'required|string|max:255',
|
||||||
|
'configuration' => 'nullable|json',
|
||||||
|
'price' => 'required|numeric|min:0',
|
||||||
|
'trial_ends_at' => 'nullable|date|after:now',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscription = $user->subscriptions()->create([
|
||||||
|
'name' => $request->input('name'),
|
||||||
|
'plan_id' => $request->input('plan_id'),
|
||||||
|
'configuration' => $request->input('configuration'),
|
||||||
|
'price' => $request->input('price'),
|
||||||
|
'trial_ends_at' => $request->input('trial_ends_at'),
|
||||||
|
'module_id' => $request->user('module')->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscription->url = route('subscriptions.show', $subscription);
|
||||||
|
|
||||||
|
return $this->success($subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 展示订阅详情。
|
||||||
|
*/
|
||||||
|
public function show(Subscription $subscription): JsonResponse
|
||||||
|
{
|
||||||
|
return $this->success($subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新订阅。
|
||||||
|
*/
|
||||||
|
public function update(Request $request, User $user, Subscription $subscription): JsonResponse
|
||||||
|
{
|
||||||
|
unset($user);
|
||||||
|
|
||||||
|
if ($subscription->status === 'active') {
|
||||||
|
return $this->badRequest('此订阅已经成立,无法修改。');
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription->update($request->only([
|
||||||
|
'name',
|
||||||
|
'plan_id',
|
||||||
|
'configuration',
|
||||||
|
'price',
|
||||||
|
'trial_ends_at',
|
||||||
|
]));
|
||||||
|
|
||||||
|
return $this->success($subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(User $user, Subscription $subscription): JsonResponse
|
||||||
|
{
|
||||||
|
unset($user);
|
||||||
|
|
||||||
|
$subscription->safeDelete();
|
||||||
|
|
||||||
|
return $this->deleted();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function by_plan_id(User $user, Subscription $subscription): JsonResponse
|
||||||
|
{
|
||||||
|
unset($user);
|
||||||
|
|
||||||
|
return $this->success($subscription);
|
||||||
|
}
|
||||||
|
}
|
63
app/Http/Controllers/Web/SubscriptionController.php
Normal file
63
app/Http/Controllers/Web/SubscriptionController.php
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Web;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\RedirectResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
class SubscriptionController extends Controller
|
||||||
|
{
|
||||||
|
public function index(Subscription $subscription)
|
||||||
|
{
|
||||||
|
$subscriptions = $subscription->thisUser()->with('module')->orderBy('status')->get();
|
||||||
|
|
||||||
|
return view('subscription.index', compact('subscriptions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(Request $request, Subscription $subscription): JsonResponse|RedirectResponse
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'cancel_at_period_end' => 'nullable|boolean',
|
||||||
|
'status' => 'nullable|in:active',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($request->filled('cancel_at_period_end')) {
|
||||||
|
$subscription->update([
|
||||||
|
'cancel_at_period_end' => $request->cancel_at_period_end,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->filled('status') && $request->input('status') === 'active') {
|
||||||
|
if (! $subscription->active()) {
|
||||||
|
if ($request->ajax()) {
|
||||||
|
return $this->badRequest('无法激活此订阅。');
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->withErrors('无法激活此订阅。');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->ajax()) {
|
||||||
|
return $this->success($subscription);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function show(Subscription $subscription)
|
||||||
|
{
|
||||||
|
$subscription->load('module');
|
||||||
|
|
||||||
|
return view('subscription.show', compact('subscription'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(Subscription $subscription)
|
||||||
|
{
|
||||||
|
$subscription->safeDelete();
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
}
|
25
app/Jobs/Subscription/DeleteDraftJob.php
Normal file
25
app/Jobs/Subscription/DeleteDraftJob.php
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Subscription;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class DeleteDraftJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 删除超过 1 天的草稿订阅。
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
Subscription::where('status', 'draft')
|
||||||
|
->where('created_at', '<', now()->subDay())
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
}
|
80
app/Jobs/Subscription/UpdateSubscriptionStatusJob.php
Normal file
80
app/Jobs/Subscription/UpdateSubscriptionStatusJob.php
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Jobs\Subscription;
|
||||||
|
|
||||||
|
use App\Models\Subscription;
|
||||||
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
|
use Illuminate\Queue\InteractsWithQueue;
|
||||||
|
use Illuminate\Queue\SerializesModels;
|
||||||
|
|
||||||
|
class UpdateSubscriptionStatusJob implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||||
|
|
||||||
|
private ?Subscription $subscription;
|
||||||
|
|
||||||
|
public function __construct(?Subscription $subscription = null)
|
||||||
|
{
|
||||||
|
$this->subscription = $subscription;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute the job.
|
||||||
|
*/
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
if (! $this->subscription) {
|
||||||
|
// 遍历所有订阅
|
||||||
|
(new Subscription)->where('status', 'active')->chunk(100, function ($subscriptions) {
|
||||||
|
$subscriptions->each(function ($subscription) {
|
||||||
|
self::dispatch($subscription);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subscription = $this->subscription;
|
||||||
|
|
||||||
|
// 如果是试用期,在到期当天,自动续费
|
||||||
|
if ($subscription->trial_ends_at && $subscription->trial_ends_at->isToday()) {
|
||||||
|
if ($subscription->cancel_at_period_end) {
|
||||||
|
// 到期不续费了,直接过期
|
||||||
|
$subscription->update([
|
||||||
|
'status' => 'expired',
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// 去除试用标识
|
||||||
|
$subscription->update([
|
||||||
|
'trial_ends_at' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$subscription->renew();
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果已经过期,则设置为 expired
|
||||||
|
if ($subscription->expired_at && $subscription->expired_at->lt(now())) {
|
||||||
|
$subscription->update([
|
||||||
|
'status' => 'expired',
|
||||||
|
]);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果还有 7 天过期,则提醒用户续费
|
||||||
|
// if ($subscription->cancel_at_period_end && $subscription->expired_at && $subscription->expired_at->gt(now()->addDays(7))) {
|
||||||
|
//
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 剩余 3 天就要过期时,自动续费
|
||||||
|
if ($subscription->cancel_at_period_end && $subscription->expired_at && $subscription->expired_at->gt(now()->addDays(3))) {
|
||||||
|
// 发送邮件提醒用户续费
|
||||||
|
$subscription->renew();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
169
app/Models/Subscription.php
Normal file
169
app/Models/Subscription.php
Normal file
@ -0,0 +1,169 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\Exceptions\User\BalanceNotEnoughException;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Subscription extends Model
|
||||||
|
{
|
||||||
|
public $fillable = [
|
||||||
|
'name',
|
||||||
|
'status',
|
||||||
|
'plan_id',
|
||||||
|
'configuration',
|
||||||
|
'price',
|
||||||
|
'expired_at',
|
||||||
|
'trial_ends_at',
|
||||||
|
'module_id',
|
||||||
|
'user_id',
|
||||||
|
'cancel_at_period_end',
|
||||||
|
'renew_price',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $casts = [
|
||||||
|
'configuration' => 'array',
|
||||||
|
'price' => 'decimal:2',
|
||||||
|
'renew_price' => 'decimal:2',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected $dates = [
|
||||||
|
'expired_at',
|
||||||
|
'trial_ends_at',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function scopeThisUser($query, $module_id = null)
|
||||||
|
{
|
||||||
|
$query = $query->where('user_id', auth()->id());
|
||||||
|
|
||||||
|
if ($module_id) {
|
||||||
|
$query = $query->where('module_id', $module_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(User::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function module(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Module::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function active(): bool
|
||||||
|
{
|
||||||
|
return $this->canActivate() && $this->renew();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function canActivate(bool $ignore_activated = false): bool
|
||||||
|
{
|
||||||
|
if ($ignore_activated && $this->isActive()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检测 trial_ends_at 是否过期
|
||||||
|
if ($this->trial_ends_at && $this->trial_ends_at->isPast()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isActive(): bool
|
||||||
|
{
|
||||||
|
return $this->status === 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function renew(): bool
|
||||||
|
{
|
||||||
|
if ($this->isTrial()) {
|
||||||
|
$this->status = 'active';
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果过期时间距离今天超过了 7 天,那么就不能续费了
|
||||||
|
if ($this->expired_at && $this->expired_at->diffInDays(now()) > 7) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$price = $this->price;
|
||||||
|
|
||||||
|
try {
|
||||||
|
$this->user->reduce($price, '订阅: '.$this->name, true, [
|
||||||
|
'module_id' => $this->module_id,
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'subscription_id' => $this->id,
|
||||||
|
]);
|
||||||
|
$this->module->charge($price, 'module_balance', '订阅: '.$this->name.' 续费', [
|
||||||
|
'module_id' => $this->module_id,
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'subscription_id' => $this->id,
|
||||||
|
]);
|
||||||
|
} catch (BalanceNotEnoughException) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->renew_price = $price;
|
||||||
|
|
||||||
|
if (! $this->isTrial()) {
|
||||||
|
$this->expired_at = now()->addMonth();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->status = 'active';
|
||||||
|
$this->save();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isTrial(): bool
|
||||||
|
{
|
||||||
|
return $this->trial_ends_at !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function safeDelete()
|
||||||
|
{
|
||||||
|
// 如果是试用,那么就直接删除
|
||||||
|
if ($this->isTrial() || $this->isExpired() || $this->isDraft() || ! $this->renew_price) {
|
||||||
|
$this->delete();
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果是正式订阅,那么就按使用天数计算退款
|
||||||
|
$days = $this->expired_at ? $this->expired_at->diffInDays(now()) : 27;
|
||||||
|
// 获取 create_at 当时的月份的天数
|
||||||
|
$daysInMonth = $this->created_at->daysInMonth;
|
||||||
|
|
||||||
|
// 按照使用天数计算退款(bcdiv 保留两位小数)
|
||||||
|
$refund = bcdiv($this->renew_price, $daysInMonth, 2) * $days;
|
||||||
|
|
||||||
|
// 如果退款金额大于 0,那么就退款
|
||||||
|
if ($refund > 0) {
|
||||||
|
$this->user->charge($refund, 'balance', '订阅: '.$this->name.' 退款', [
|
||||||
|
'module_id' => $this->module_id,
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'subscription_id' => $this->id,
|
||||||
|
]);
|
||||||
|
$this->module->reduce($refund, '订阅: '.$this->name.' 退款', false, [
|
||||||
|
'module_id' => $this->module_id,
|
||||||
|
'user_id' => $this->user_id,
|
||||||
|
'subscription_id' => $this->id,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isExpired(): bool
|
||||||
|
{
|
||||||
|
return $this->expired_at && $this->expired_at->isPast();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDraft(): bool
|
||||||
|
{
|
||||||
|
return $this->status === 'draft';
|
||||||
|
}
|
||||||
|
}
|
54
app/Observers/SubscriptionObserve.php
Normal file
54
app/Observers/SubscriptionObserve.php
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Observers;
|
||||||
|
|
||||||
|
use App\Events\Users;
|
||||||
|
use App\Models\Subscription;
|
||||||
|
|
||||||
|
class SubscriptionObserve
|
||||||
|
{
|
||||||
|
public function creating(Subscription $subscription): void
|
||||||
|
{
|
||||||
|
// 如果没有设置 status,就设置为 draft
|
||||||
|
if (! $subscription->status) {
|
||||||
|
$subscription->status = 'draft';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Subscription "created" event.
|
||||||
|
*/
|
||||||
|
public function created(Subscription $subscription): void
|
||||||
|
{
|
||||||
|
broadcast(new Users($subscription->user, 'subscription.created', $subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function updating(Subscription $subscription): void
|
||||||
|
{
|
||||||
|
// 如果 status 是 expired, expired_at 为空,就设置为当前时间
|
||||||
|
if ($subscription->status === 'expired' && ! $subscription->expired_at) {
|
||||||
|
$subscription->expired_at = now();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果 expired_at 和 trial_ends_at 为空,就当作过期处理
|
||||||
|
if ($subscription->status !== 'draft' && ! $subscription->expired_at && ! $subscription->trial_ends_at) {
|
||||||
|
$subscription->status = 'expired';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Subscription "updated" event.
|
||||||
|
*/
|
||||||
|
public function updated(Subscription $subscription): void
|
||||||
|
{
|
||||||
|
broadcast(new Users($subscription->user, 'subscription.updated', $subscription));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle the Subscription "deleted" event.
|
||||||
|
*/
|
||||||
|
public function deleted(Subscription $subscription): void
|
||||||
|
{
|
||||||
|
broadcast(new Users($subscription->user, 'subscription.deleted', $subscription));
|
||||||
|
}
|
||||||
|
}
|
@ -6,12 +6,14 @@
|
|||||||
use App\Models\Host;
|
use App\Models\Host;
|
||||||
use App\Models\Module;
|
use App\Models\Module;
|
||||||
use App\Models\PersonalAccessToken;
|
use App\Models\PersonalAccessToken;
|
||||||
|
use App\Models\Subscription;
|
||||||
use App\Models\Task;
|
use App\Models\Task;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
use App\Models\WorkOrder\WorkOrder;
|
use App\Models\WorkOrder\WorkOrder;
|
||||||
use App\Observers\BalanceObserver;
|
use App\Observers\BalanceObserver;
|
||||||
use App\Observers\HostObserver;
|
use App\Observers\HostObserver;
|
||||||
use App\Observers\ModuleObserver;
|
use App\Observers\ModuleObserver;
|
||||||
|
use App\Observers\SubscriptionObserve;
|
||||||
use App\Observers\TaskObserver;
|
use App\Observers\TaskObserver;
|
||||||
use App\Observers\UserObserver;
|
use App\Observers\UserObserver;
|
||||||
use App\Observers\WorkOrderObserver;
|
use App\Observers\WorkOrderObserver;
|
||||||
@ -20,16 +22,11 @@
|
|||||||
use Illuminate\Support\ServiceProvider;
|
use Illuminate\Support\ServiceProvider;
|
||||||
use Laravel\Sanctum\Sanctum;
|
use Laravel\Sanctum\Sanctum;
|
||||||
|
|
||||||
|
// use App\Models\Invoice;
|
||||||
|
// use App\Observers\InvoiceObserver;
|
||||||
|
|
||||||
class AppServiceProvider extends ServiceProvider
|
class AppServiceProvider extends ServiceProvider
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* Register any application services.
|
|
||||||
*/
|
|
||||||
public function register(): void
|
|
||||||
{
|
|
||||||
//
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Bootstrap any application services.
|
* Bootstrap any application services.
|
||||||
*/
|
*/
|
||||||
@ -63,5 +60,7 @@ private function registerObservers(): void
|
|||||||
Module::observe(ModuleObserver::class);
|
Module::observe(ModuleObserver::class);
|
||||||
Balance::observe(BalanceObserver::class);
|
Balance::observe(BalanceObserver::class);
|
||||||
WorkOrder::observe(WorkOrderObserver::class);
|
WorkOrder::observe(WorkOrderObserver::class);
|
||||||
|
// Invoice::observe(InvoiceObserver::class);
|
||||||
|
Subscription::observe(SubscriptionObserve::class);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,68 @@
|
|||||||
|
<?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('subscriptions', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
|
||||||
|
// 类型
|
||||||
|
$table->enum('status', [
|
||||||
|
'draft',
|
||||||
|
'active',
|
||||||
|
'expired',
|
||||||
|
'canceled',
|
||||||
|
])->index();
|
||||||
|
|
||||||
|
// 名称
|
||||||
|
$table->string('name')->nullable();
|
||||||
|
|
||||||
|
// 计划 ID
|
||||||
|
$table->string('plan_id')->index();
|
||||||
|
|
||||||
|
// 配置项目
|
||||||
|
$table->json('configuration')->nullable();
|
||||||
|
|
||||||
|
// 价格
|
||||||
|
$table->decimal('price', 10)->default(0);
|
||||||
|
|
||||||
|
// 结束时间
|
||||||
|
$table->timestamp('expired_at')->nullable();
|
||||||
|
|
||||||
|
// 试用结束时间
|
||||||
|
$table->timestamp('trial_ends_at')->nullable();
|
||||||
|
|
||||||
|
// 下个月取消
|
||||||
|
$table->boolean('cancel_at_period_end')->default(false)->index();
|
||||||
|
|
||||||
|
// 续费时价格
|
||||||
|
$table->decimal('renew_price', 10)->default(0);
|
||||||
|
|
||||||
|
// 模块 ID
|
||||||
|
$table->string('module_id')->index()->nullable();
|
||||||
|
$table->foreign('module_id')->references('id')->on('modules')->nullOnDelete();
|
||||||
|
|
||||||
|
// 用户 ID
|
||||||
|
$table->unsignedBigInteger('user_id')->index()->nullable();
|
||||||
|
$table->foreign('user_id')->references('id')->on('users')->nullOnDelete();
|
||||||
|
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('subscriptions');
|
||||||
|
}
|
||||||
|
};
|
109
resources/views/subscription/index.blade.php
Normal file
109
resources/views/subscription/index.blade.php
Normal file
@ -0,0 +1,109 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', '订阅')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h3>订阅</h3>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<th>ID</th>
|
||||||
|
<th>名称</th>
|
||||||
|
<th>模块</th>
|
||||||
|
<th>计划 ID</th>
|
||||||
|
<th>续期价格</th>
|
||||||
|
<th>状态</th>
|
||||||
|
<th>到期时间</th>
|
||||||
|
<th>操作</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
@foreach ($subscriptions as $subscription)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ $subscription->id }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ $subscription->name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ $subscription->module->name }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ $subscription->plan_id }}
|
||||||
|
</td>
|
||||||
|
<td class="small">
|
||||||
|
{{ $subscription->price }} 元
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<x-host-status :status="$subscription->status"/>
|
||||||
|
@if ($subscription->cancel_at_period_end)
|
||||||
|
<br/>
|
||||||
|
<small>
|
||||||
|
<span class="text-danger">自动续订已取消</span>
|
||||||
|
</small>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="small">
|
||||||
|
@if ($subscription->isTrial())
|
||||||
|
{{ $subscription->trial_ends_at }}(试用)
|
||||||
|
@else
|
||||||
|
{{ $subscription->expired_at }}
|
||||||
|
@endif
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<div class="dropdown">
|
||||||
|
<button class="btn btn-sm btn-secondary dropdown-toggle" type="button"
|
||||||
|
data-bs-toggle="dropdown"
|
||||||
|
aria-expanded="false">
|
||||||
|
操作
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
@if ($subscription->isDraft())
|
||||||
|
<a class="dropdown-item active"
|
||||||
|
href="{{ route('subscriptions.show', $subscription) }}">
|
||||||
|
开始订阅
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if ($subscription->isActive())
|
||||||
|
<a class="dropdown-item" href="#"
|
||||||
|
onclick="document.getElementById('update-{{$subscription->id}}').submit()">
|
||||||
|
{{ $subscription->cancel_at_period_end ? '启用自动续订' : '取消自动续订'}}
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form action="{{ route('subscriptions.update', $subscription) }}"
|
||||||
|
id="update-{{$subscription->id}}"
|
||||||
|
method="post" class="d-none">
|
||||||
|
@csrf
|
||||||
|
@method('PATCH')
|
||||||
|
<input type="hidden" name="cancel_at_period_end"
|
||||||
|
value="{{ !$subscription->cancel_at_period_end ? '1' : '0'}}">
|
||||||
|
</form>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
<a class="dropdown-item" href="#"
|
||||||
|
onclick="return confirm('删除操作将不可恢复,确定吗?') ? document.getElementById('delete-{{$subscription->id}}').submit() : false;">
|
||||||
|
删除订阅
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<form action="{{ route('subscriptions.destroy', $subscription) }}"
|
||||||
|
id="delete-{{$subscription->id}}"
|
||||||
|
method="post" class="d-none">
|
||||||
|
@csrf
|
||||||
|
@method('DELETE')
|
||||||
|
</form>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@endsection
|
64
resources/views/subscription/show.blade.php
Normal file
64
resources/views/subscription/show.blade.php
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
@extends('layouts.app')
|
||||||
|
|
||||||
|
@section('title', $subscription->name)
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
@if (!$subscription->canActivate(true))
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 85vh">
|
||||||
|
<div class="text-center">
|
||||||
|
<h2>不能激活此订阅。</h2>
|
||||||
|
<br />
|
||||||
|
<p>
|
||||||
|
模块向您发送了一个不正确的订阅草稿。
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@elseif ($subscription->isDraft())
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 85vh">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-center">激活此订阅</h2>
|
||||||
|
<table class="table table-bordered table-striped">
|
||||||
|
<tr>
|
||||||
|
<td>模块</td>
|
||||||
|
<td>{{ $subscription->module->name }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>月付价格</td>
|
||||||
|
<td>{{ $subscription->price }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>计划 ID</td>
|
||||||
|
<td>{{ $subscription->plan_id }}</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>截止</td>
|
||||||
|
<td>
|
||||||
|
{{ ($subscription->expired_at ?? $subscription->trial_ends_at) ?? '订阅后开始计算' }}
|
||||||
|
@if ($subscription->isTrial())
|
||||||
|
<span class="badge badge-success">试用</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="text-center">
|
||||||
|
<form action="{{ route('subscriptions.update', $subscription) }}" method="post">
|
||||||
|
@csrf
|
||||||
|
@method('PATCH')
|
||||||
|
<input type="hidden" name="status" value="active"/>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-primary btn-sm btn-block">激活</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
|
||||||
|
@if($subscription->isActive())
|
||||||
|
<div class="d-flex justify-content-center align-items-center" style="height: 85vh">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-center">谢谢。</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
@endsection
|
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Controllers\Api\MaintenanceController;
|
use App\Http\Controllers\Api\MaintenanceController;
|
||||||
use App\Http\Controllers\Api\ModuleController;
|
use App\Http\Controllers\Api\ModuleController;
|
||||||
use App\Http\Controllers\Api\ReplyController;
|
use App\Http\Controllers\Api\ReplyController;
|
||||||
|
use App\Http\Controllers\Api\SubscriptionController;
|
||||||
use App\Http\Controllers\Api\TaskController;
|
use App\Http\Controllers\Api\TaskController;
|
||||||
use App\Http\Controllers\Api\UserController;
|
use App\Http\Controllers\Api\UserController;
|
||||||
use App\Http\Controllers\Api\WorkOrderController;
|
use App\Http\Controllers\Api\WorkOrderController;
|
||||||
@ -39,6 +40,7 @@
|
|||||||
Route::apiResource('hosts', HostController::class);
|
Route::apiResource('hosts', HostController::class);
|
||||||
|
|
||||||
Route::apiResource('work-orders', WorkOrderController::class)->only(['index', 'store']);
|
Route::apiResource('work-orders', WorkOrderController::class)->only(['index', 'store']);
|
||||||
|
Route::apiResource('subscriptions', SubscriptionController::class)->middleware('resource_owner:subscription');
|
||||||
|
|
||||||
Route::withoutMiddleware('auth:sanctum')->prefix('work-orders')->group(function () {
|
Route::withoutMiddleware('auth:sanctum')->prefix('work-orders')->group(function () {
|
||||||
Route::get('{workOrder:uuid}', [WorkOrderController::class, 'show']);
|
Route::get('{workOrder:uuid}', [WorkOrderController::class, 'show']);
|
||||||
|
@ -5,6 +5,7 @@
|
|||||||
use App\Http\Controllers\Module\HostController;
|
use App\Http\Controllers\Module\HostController;
|
||||||
use App\Http\Controllers\Module\ModuleController;
|
use App\Http\Controllers\Module\ModuleController;
|
||||||
use App\Http\Controllers\Module\ReplyController;
|
use App\Http\Controllers\Module\ReplyController;
|
||||||
|
use App\Http\Controllers\Module\SubscriptionController;
|
||||||
use App\Http\Controllers\Module\TaskController;
|
use App\Http\Controllers\Module\TaskController;
|
||||||
use App\Http\Controllers\Module\UserController;
|
use App\Http\Controllers\Module\UserController;
|
||||||
use App\Http\Controllers\Module\WorkOrderController;
|
use App\Http\Controllers\Module\WorkOrderController;
|
||||||
@ -24,7 +25,8 @@
|
|||||||
|
|
||||||
// 用户信息
|
// 用户信息
|
||||||
Route::post('users/attempt', [UserController::class, 'attempt']);
|
Route::post('users/attempt', [UserController::class, 'attempt']);
|
||||||
Route::resource('users', UserController::class)->only(['index', 'show', 'update', 'store']);
|
Route::apiResource('users', UserController::class)->only(['index', 'show', 'update', 'store']);
|
||||||
|
Route::apiResource('users.subscriptions', SubscriptionController::class);
|
||||||
|
|
||||||
Route::get('token/{token}', [UserController::class, 'auth']);
|
Route::get('token/{token}', [UserController::class, 'auth']);
|
||||||
Route::get('users/{user}/hosts', [UserController::class, 'hosts']);
|
Route::get('users/{user}/hosts', [UserController::class, 'hosts']);
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
use App\Http\Controllers\Web\HostController;
|
use App\Http\Controllers\Web\HostController;
|
||||||
use App\Http\Controllers\Web\MaintenanceController;
|
use App\Http\Controllers\Web\MaintenanceController;
|
||||||
use App\Http\Controllers\Web\RealNameController;
|
use App\Http\Controllers\Web\RealNameController;
|
||||||
|
use App\Http\Controllers\Web\SubscriptionController;
|
||||||
use App\Http\Controllers\Web\TransferController;
|
use App\Http\Controllers\Web\TransferController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
@ -86,6 +87,10 @@ function () {
|
|||||||
Route::middleware('guest')->withoutMiddleware(['verified', 'auth:web'])->get('affiliates/{affiliate:uuid}', [AffiliateController::class, 'show'])->name('affiliates.show');
|
Route::middleware('guest')->withoutMiddleware(['verified', 'auth:web'])->get('affiliates/{affiliate:uuid}', [AffiliateController::class, 'show'])->name('affiliates.show');
|
||||||
/* End 推介 */
|
/* End 推介 */
|
||||||
|
|
||||||
|
/* Start 订阅 */
|
||||||
|
Route::middleware('resource_owner:subscription')->resource('subscriptions', SubscriptionController::class)->except(['edit']);
|
||||||
|
/* End 订阅 */
|
||||||
|
|
||||||
/* Start 匿名登录 */
|
/* Start 匿名登录 */
|
||||||
Route::get('auth_request/{auth_request}', [AuthController::class, 'showAuthRequest'])->withoutMiddleware(['auth:web', 'verified'])->name('auth_request.show');
|
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');
|
Route::post('auth_request', [AuthController::class, 'storeAuthRequest'])->name('auth_request.store');
|
||||||
|
Loading…
Reference in New Issue
Block a user