Compare commits
10 Commits
4e9dfd6018
...
4f48e61a2c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4f48e61a2c | ||
|
|
9d125527cb | ||
|
|
e074bef702 | ||
|
|
7cfe061469 | ||
|
|
c9ef5768e0 | ||
|
|
60ba57142b | ||
|
|
8ecb762d77 | ||
|
|
6141430a88 | ||
|
|
34bb1fdbcf | ||
|
|
8f66a9af94 |
@ -126,6 +126,9 @@ class OrderController extends Controller
|
||||
if (!$order) {
|
||||
return $this->fail([400, __('Order does not exist or has been paid')]);
|
||||
}
|
||||
if ($order->total_amount < 0) {
|
||||
return $this->fail([400, __('Order amount is invalid')]);
|
||||
}
|
||||
// free process
|
||||
if ($order->total_amount <= 0) {
|
||||
$orderService = new OrderService($order);
|
||||
|
||||
@ -11,6 +11,7 @@ use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\AuthService;
|
||||
use App\Services\NodeSyncService;
|
||||
use App\Services\Plugin\HookManager;
|
||||
use App\Services\UserService;
|
||||
use App\Traits\QueryOperators;
|
||||
use App\Utils\Helper;
|
||||
@ -33,7 +34,16 @@ class UserController extends Controller
|
||||
return $this->fail([400202, '用户不存在']);
|
||||
$user->token = Helper::guid();
|
||||
$user->uuid = Helper::guid(true);
|
||||
return $this->success($user->save());
|
||||
$result = $user->save();
|
||||
|
||||
if ($result) {
|
||||
HookManager::call('admin.user.secret.reset', [
|
||||
'user' => $user,
|
||||
'request' => $request,
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->success($result);
|
||||
}
|
||||
|
||||
// Apply filters and sorts to the query builder.
|
||||
@ -178,6 +188,8 @@ class UserController extends Controller
|
||||
->select((new User())->getTable() . '.*')
|
||||
->selectRaw('(u + d) as total_used');
|
||||
|
||||
$userModel = HookManager::filter('admin.user.fetch.query', $userModel, $request);
|
||||
|
||||
$this->applyFiltersAndSorts($request, $userModel);
|
||||
|
||||
$users = $userModel->orderBy('id', 'desc')
|
||||
@ -193,11 +205,12 @@ class UserController extends Controller
|
||||
// Transform user fields for API response.
|
||||
public static function transformUserData(User $user): array
|
||||
{
|
||||
$model = $user;
|
||||
$user = $user->toArray();
|
||||
$user['balance'] = $user['balance'] / 100;
|
||||
$user['commission_balance'] = $user['commission_balance'] / 100;
|
||||
$user['subscribe_url'] = Helper::getSubscribeUrl($user['token']);
|
||||
return $user;
|
||||
return HookManager::filter('admin.user.transform', $user, $model);
|
||||
}
|
||||
|
||||
public function getUserInfoById(Request $request)
|
||||
@ -208,6 +221,7 @@ class UserController extends Controller
|
||||
'id.required' => '用户ID不能为空'
|
||||
]);
|
||||
$user = User::find($request->input('id'))->load('invite_user');
|
||||
$user = HookManager::filter('admin.user.detail', $user, $request);
|
||||
return $this->success($user);
|
||||
}
|
||||
|
||||
@ -257,12 +271,27 @@ class UserController extends Controller
|
||||
$params['commission_balance'] = $params['commission_balance'] * 100;
|
||||
}
|
||||
|
||||
$params = HookManager::filter('admin.user.update.params', $params, $request, $user);
|
||||
|
||||
HookManager::call('admin.user.update.before', [
|
||||
'user' => $user,
|
||||
'params' => $params,
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
try {
|
||||
$user->update($params);
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return $this->fail([500, '保存失败']);
|
||||
}
|
||||
|
||||
HookManager::call('admin.user.update.after', [
|
||||
'user' => $user->refresh(),
|
||||
'params' => $params,
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
return $this->success(true);
|
||||
}
|
||||
|
||||
@ -664,6 +693,11 @@ class UserController extends Controller
|
||||
'id.exists' => '用户不存在'
|
||||
]);
|
||||
$user = User::find($request->input('id'));
|
||||
HookManager::call('admin.user.destroy.before', [
|
||||
'user' => $user,
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$user->orders()->delete();
|
||||
@ -672,6 +706,12 @@ class UserController extends Controller
|
||||
$user->tickets()->delete();
|
||||
$user->delete();
|
||||
DB::commit();
|
||||
|
||||
HookManager::call('admin.user.destroy.after', [
|
||||
'user' => $user,
|
||||
'request' => $request,
|
||||
]);
|
||||
|
||||
return $this->success(true);
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Services\Plugin\HookManager;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
class UserUpdate extends FormRequest
|
||||
@ -13,7 +14,7 @@ class UserUpdate extends FormRequest
|
||||
*/
|
||||
public function rules()
|
||||
{
|
||||
return [
|
||||
$rules = [
|
||||
'id' => 'required|integer',
|
||||
'email' => 'email:strict',
|
||||
'password' => 'nullable|min:8',
|
||||
@ -34,11 +35,13 @@ class UserUpdate extends FormRequest
|
||||
'speed_limit' => 'nullable|integer',
|
||||
'device_limit' => 'nullable|integer'
|
||||
];
|
||||
|
||||
return HookManager::filter('admin.user.update.rules', $rules, $this);
|
||||
}
|
||||
|
||||
public function messages()
|
||||
{
|
||||
return [
|
||||
$messages = [
|
||||
'email.required' => '邮箱不能为空',
|
||||
'email.email' => '邮箱格式不正确',
|
||||
'transfer_enable.numeric' => '流量格式不正确',
|
||||
@ -65,5 +68,7 @@ class UserUpdate extends FormRequest
|
||||
'speed_limit.integer' => '限速格式不正确',
|
||||
'device_limit.integer' => '设备数量格式不正确'
|
||||
];
|
||||
|
||||
return HookManager::filter('admin.user.update.messages', $messages, $this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,9 +36,7 @@ class OrderHandleJob implements ShouldQueue
|
||||
*/
|
||||
public function handle()
|
||||
{
|
||||
$order = Order::where('trade_no', $this->tradeNo)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
$order = Order::where('trade_no', $this->tradeNo)->first();
|
||||
if (!$order) return;
|
||||
$orderService = new OrderService($order);
|
||||
switch ($order->status) {
|
||||
|
||||
@ -18,7 +18,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||
* @property int $total_amount
|
||||
* @property int|null $handling_amount
|
||||
* @property int|null $balance_amount
|
||||
* @property int|null $refund_amount
|
||||
* @property int|null $surplus_credit
|
||||
* @property int|null $surplus_amount
|
||||
* @property int $type
|
||||
* @property int $status
|
||||
|
||||
@ -239,7 +239,7 @@ class General extends AbstractProtocol
|
||||
|
||||
$user = $uuid . '@' . Helper::wrapIPv6($host) . ':' . $port;
|
||||
$query = http_build_query($config);
|
||||
$fragment = urlencode($name);
|
||||
$fragment = rawurlencode($name);
|
||||
$link = sprintf("vless://%s?%s#%s\r\n", $user, $query, $fragment);
|
||||
return $link;
|
||||
}
|
||||
|
||||
@ -381,6 +381,12 @@ class Shadowrocket extends AbstractProtocol
|
||||
"obfs" => 'none',
|
||||
"fastopen" => 1
|
||||
];
|
||||
if (($upMbps = data_get($protocol_settings, 'bandwidth.up')) > 0) {
|
||||
$params['upmbps'] = $upMbps;
|
||||
}
|
||||
if (($downMbps = data_get($protocol_settings, 'bandwidth.down')) > 0) {
|
||||
$params['downmbps'] = $downMbps;
|
||||
}
|
||||
if ($serverName = data_get($protocol_settings, 'tls.server_name')) {
|
||||
$params['peer'] = $serverName;
|
||||
}
|
||||
@ -411,7 +417,8 @@ class Shadowrocket extends AbstractProtocol
|
||||
$params = [
|
||||
'alpn' => data_get($protocol_settings, 'alpn'),
|
||||
'sni' => data_get($protocol_settings, 'tls.server_name'),
|
||||
'insecure' => data_get($protocol_settings, 'tls.allow_insecure')
|
||||
'insecure' => data_get($protocol_settings, 'tls.allow_insecure'),
|
||||
'congestion_control' => data_get($protocol_settings, 'congestion_control', 'cubic')
|
||||
];
|
||||
if (data_get($protocol_settings, 'version') === 4) {
|
||||
$params['token'] = $password;
|
||||
|
||||
@ -162,7 +162,7 @@ class DeviceStateService
|
||||
}
|
||||
}
|
||||
if (!empty($ips)) {
|
||||
$result[$userId] = array_unique($ips);
|
||||
$result[$userId] = array_values(array_unique($ips));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -15,8 +15,8 @@ use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GiftCardService
|
||||
{
|
||||
protected readonly GiftCardCode $code;
|
||||
protected readonly GiftCardTemplate $template;
|
||||
protected GiftCardCode $code;
|
||||
protected GiftCardTemplate $template;
|
||||
protected ?User $user = null;
|
||||
|
||||
public function __construct(string $code)
|
||||
@ -106,6 +106,23 @@ class GiftCardService
|
||||
}
|
||||
|
||||
return DB::transaction(function () use ($options) {
|
||||
$this->code = GiftCardCode::whereKey($this->code->id)
|
||||
->lockForUpdate()
|
||||
->first()
|
||||
?? throw new ApiException('兑换码不存在');
|
||||
|
||||
$this->template = GiftCardTemplate::whereKey($this->code->template_id)
|
||||
->lockForUpdate()
|
||||
->first()
|
||||
?? throw new ApiException('该礼品卡类型不存在');
|
||||
|
||||
$this->user = User::whereKey($this->user->id)
|
||||
->lockForUpdate()
|
||||
->first()
|
||||
?? throw new ApiException('用户信息未提供');
|
||||
|
||||
$this->validate();
|
||||
|
||||
$actualRewards = $this->template->calculateActualRewards($this->user);
|
||||
|
||||
if ($this->template->type === GiftCardTemplate::TYPE_MYSTERY) {
|
||||
@ -198,7 +215,7 @@ class GiftCardService
|
||||
return null;
|
||||
}
|
||||
|
||||
$inviteUser = User::find($this->user->invite_user_id);
|
||||
$inviteUser = User::lockForUpdate()->find($this->user->invite_user_id);
|
||||
if (!$inviteUser) {
|
||||
return null;
|
||||
}
|
||||
@ -212,7 +229,9 @@ class GiftCardService
|
||||
if (isset($rewards['balance']) && $rewards['balance'] > 0) {
|
||||
$inviteBalance = intval($rewards['balance'] * $rate);
|
||||
if ($inviteBalance > 0) {
|
||||
$userService->addBalance($inviteUser->id, $inviteBalance);
|
||||
if (!$userService->addBalance($inviteUser->id, $inviteBalance)) {
|
||||
throw new ApiException('邀请人余额发放失败');
|
||||
}
|
||||
$inviteRewards['balance'] = $inviteBalance;
|
||||
}
|
||||
}
|
||||
|
||||
@ -33,6 +33,21 @@ class OrderService
|
||||
$this->order = $order;
|
||||
}
|
||||
|
||||
private function lockCurrentOrder(): ?Order
|
||||
{
|
||||
return Order::whereKey($this->order->id)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
}
|
||||
|
||||
private function lockCurrentOrderWhenStatus(int $status): ?Order
|
||||
{
|
||||
return Order::whereKey($this->order->id)
|
||||
->where('status', $status)
|
||||
->lockForUpdate()
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an order from a request.
|
||||
*
|
||||
@ -56,6 +71,15 @@ class OrderService
|
||||
HookManager::call('order.create.before', [$user, $plan, $period, $couponCode]);
|
||||
|
||||
return DB::transaction(function () use ($user, $plan, $period, $couponCode, $userService) {
|
||||
$user = User::lockForUpdate()->find($user->id);
|
||||
if (!$user) {
|
||||
throw new ApiException(__('The user does not exist'));
|
||||
}
|
||||
|
||||
if ($userService->isNotCompleteOrderByUserId($user->id)) {
|
||||
throw new ApiException(__('You have an unpaid or pending order, please try again later or cancel it'));
|
||||
}
|
||||
|
||||
$newPeriod = PlanService::getPeriodKey($period);
|
||||
|
||||
$order = new Order([
|
||||
@ -74,12 +98,13 @@ class OrderService
|
||||
|
||||
$orderService->setVipDiscount($user);
|
||||
$orderService->setOrderType($user);
|
||||
$orderService->setInvite(user: $user);
|
||||
|
||||
if ($user->balance && $order->total_amount > 0) {
|
||||
$orderService->handleUserBalance($user, $userService);
|
||||
}
|
||||
|
||||
$orderService->setInvite(user: $user);
|
||||
|
||||
if (!$order->save()) {
|
||||
throw new ApiException(__('Failed to create order'));
|
||||
}
|
||||
@ -94,17 +119,23 @@ class OrderService
|
||||
|
||||
public function open(): void
|
||||
{
|
||||
$order = $this->order;
|
||||
$plan = Plan::find($order->plan_id);
|
||||
$openedOrder = DB::transaction(function () {
|
||||
$order = $this->lockCurrentOrderWhenStatus(Order::STATUS_PROCESSING);
|
||||
if (!$order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HookManager::call('order.open.before', $order);
|
||||
$plan = Plan::find($order->plan_id);
|
||||
if (!$plan) {
|
||||
throw new \RuntimeException('订阅不存在');
|
||||
}
|
||||
|
||||
HookManager::call('order.open.before', $order);
|
||||
|
||||
DB::transaction(function () use ($order, $plan) {
|
||||
$this->user = User::lockForUpdate()->find($order->user_id);
|
||||
|
||||
if ($order->refund_amount) {
|
||||
$this->user->balance += $order->refund_amount;
|
||||
if ($order->surplus_credit) {
|
||||
$this->user->balance += $order->surplus_credit;
|
||||
}
|
||||
|
||||
if ($order->surplus_order_ids) {
|
||||
@ -129,10 +160,19 @@ class OrderService
|
||||
if (!$order->save()) {
|
||||
throw new \RuntimeException('订单信息保存失败');
|
||||
}
|
||||
|
||||
return $order;
|
||||
});
|
||||
|
||||
if (!$openedOrder) {
|
||||
return;
|
||||
}
|
||||
|
||||
$order = $openedOrder;
|
||||
$this->order = $order;
|
||||
|
||||
$eventId = match ((int) $order->type) {
|
||||
Order::STATUS_PROCESSING => admin_setting('new_order_event_id', 0),
|
||||
Order::TYPE_NEW_PURCHASE => admin_setting('new_order_event_id', 0),
|
||||
Order::TYPE_RENEWAL => admin_setting('renew_order_event_id', 0),
|
||||
Order::TYPE_UPGRADE => admin_setting('change_order_event_id', 0),
|
||||
default => 0,
|
||||
@ -158,7 +198,7 @@ class OrderService
|
||||
if ((int) admin_setting('surplus_enable', 1))
|
||||
$this->getSurplusValue($user, $order);
|
||||
if ($order->surplus_amount >= $order->total_amount) {
|
||||
$order->refund_amount = (int) ($order->surplus_amount - $order->total_amount);
|
||||
$order->surplus_credit = (int) ($order->surplus_amount - $order->total_amount);
|
||||
$order->total_amount = 0;
|
||||
} else {
|
||||
$order->total_amount = (int) ($order->total_amount - $order->surplus_amount);
|
||||
@ -256,7 +296,7 @@ class OrderService
|
||||
return;
|
||||
}
|
||||
|
||||
$orderAmountSum = $orders->sum(fn($item) => $item->total_amount + $item->balance_amount + $item->surplus_amount - $item->refund_amount);
|
||||
$orderAmountSum = $orders->sum(fn($item) => $item->total_amount + $item->balance_amount + $item->surplus_amount - $item->surplus_credit);
|
||||
$orderMonthSum = $orders->sum(fn($item) => self::STR_TO_TIME[PlanService::getPeriodKey($item->period)] ?? 0);
|
||||
$firstOrderAt = $orders->min('created_at');
|
||||
$expiredAt = Carbon::createFromTimestamp($firstOrderAt)->addMonths($orderMonthSum);
|
||||
@ -285,16 +325,31 @@ class OrderService
|
||||
|
||||
public function paid(string $callbackNo)
|
||||
{
|
||||
$order = $this->order;
|
||||
if ($order->status !== Order::STATUS_PENDING)
|
||||
return true;
|
||||
$order->status = Order::STATUS_PROCESSING;
|
||||
$order->paid_at = time();
|
||||
$order->callback_no = $callbackNo;
|
||||
if (!$order->save())
|
||||
return false;
|
||||
try {
|
||||
OrderHandleJob::dispatchSync($order->trade_no);
|
||||
[$order, $shouldDispatch] = DB::transaction(function () use ($callbackNo) {
|
||||
$order = $this->lockCurrentOrder();
|
||||
if (!$order) {
|
||||
throw new \RuntimeException('Order not found.');
|
||||
}
|
||||
if ((int) $order->status !== Order::STATUS_PENDING) {
|
||||
return [$order, false];
|
||||
}
|
||||
|
||||
$order->status = Order::STATUS_PROCESSING;
|
||||
$order->paid_at = time();
|
||||
$order->callback_no = $callbackNo;
|
||||
if (!$order->save()) {
|
||||
throw new \RuntimeException('Failed to save order status.');
|
||||
}
|
||||
|
||||
return [$order, true];
|
||||
});
|
||||
|
||||
$this->order = $order;
|
||||
|
||||
if ($shouldDispatch) {
|
||||
OrderHandleJob::dispatchSync($order->trade_no);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
Log::error($e);
|
||||
return false;
|
||||
@ -304,25 +359,37 @@ class OrderService
|
||||
|
||||
public function cancel(): bool
|
||||
{
|
||||
$order = $this->order;
|
||||
HookManager::call('order.cancel.before', $order);
|
||||
try {
|
||||
DB::beginTransaction();
|
||||
$order->status = Order::STATUS_CANCELLED;
|
||||
if (!$order->save()) {
|
||||
throw new \Exception('Failed to save order status.');
|
||||
}
|
||||
if ($order->balance_amount) {
|
||||
$userService = new UserService();
|
||||
if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
|
||||
throw new \Exception('Failed to add balance.');
|
||||
$cancelledOrder = DB::transaction(function () {
|
||||
$order = $this->lockCurrentOrderWhenStatus(Order::STATUS_PENDING);
|
||||
if (!$order) {
|
||||
return null;
|
||||
}
|
||||
|
||||
HookManager::call('order.cancel.before', $order);
|
||||
|
||||
$order->status = Order::STATUS_CANCELLED;
|
||||
if (!$order->save()) {
|
||||
throw new \RuntimeException('Failed to save order status.');
|
||||
}
|
||||
if ($order->balance_amount) {
|
||||
$userService = new UserService();
|
||||
if (!$userService->addBalance($order->user_id, $order->balance_amount)) {
|
||||
throw new \RuntimeException('Failed to add balance.');
|
||||
}
|
||||
}
|
||||
|
||||
return $order;
|
||||
});
|
||||
|
||||
if (!$cancelledOrder) {
|
||||
return false;
|
||||
}
|
||||
DB::commit();
|
||||
HookManager::call('order.cancel.after', $order);
|
||||
|
||||
$this->order = $cancelledOrder;
|
||||
HookManager::call('order.cancel.after', $cancelledOrder);
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
DB::rollBack();
|
||||
Log::error($e);
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('v2_order', function (Blueprint $table) {
|
||||
$table->renameColumn('refund_amount', 'surplus_credit');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('v2_order', function (Blueprint $table) {
|
||||
$table->renameColumn('surplus_credit', 'refund_amount');
|
||||
});
|
||||
}
|
||||
};
|
||||
@ -102,7 +102,7 @@ class AlipayF2F
|
||||
'method' => $this->method,
|
||||
'charset' => 'UTF-8',
|
||||
'sign_type' => $this->signType,
|
||||
'timestamp' => date('Y-m-d H:m:s'),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
'biz_content' => $this->bizContent,
|
||||
'version' => '1.0',
|
||||
'_input_charset' => 'UTF-8'
|
||||
|
||||
@ -1 +1 @@
|
||||
Subproject commit fe6dc2952241c6bed807d1ca965e7d5907303265
|
||||
Subproject commit ef5f43da335092cbff8fdf0ad7ff9b4d92d7d0d7
|
||||
@ -26,6 +26,7 @@
|
||||
"No active subscription. Unable to use our provided Apple ID": "No active subscription. Unable to use our provided Apple ID",
|
||||
"Oops, there's a problem... Please refresh the page and try again later": "Oops, there's a problem... Please refresh the page and try again later",
|
||||
"Order does not exist": "Order does not exist",
|
||||
"Order amount is invalid": "Order amount is invalid",
|
||||
"Order does not exist or has been paid": "Order does not exist or has been paid",
|
||||
"Payment failed. Please check your credit card information": "Payment failed. Please check your credit card information",
|
||||
"Payment gateway request failed": "Payment gateway request failed",
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
"No active subscription. Unable to use our provided Apple ID": "Нет активной подписки. Невозможно использовать предоставленный Apple ID",
|
||||
"Oops, there's a problem... Please refresh the page and try again later": "Ой, возникла проблема... Обновите страницу и попробуйте позже",
|
||||
"Order does not exist": "Заказ не существует",
|
||||
"Order amount is invalid": "Некорректная сумма заказа",
|
||||
"Order does not exist or has been paid": "Заказ не существует или уже оплачен",
|
||||
"Payment failed. Please check your credit card information": "Ошибка оплаты. Проверьте данные карты",
|
||||
"Payment gateway request failed": "Ошибка запроса к платёжному шлюзу",
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"Withdrawal account": "提现账号",
|
||||
"Unsupported withdrawal": "不支持提现",
|
||||
"Order does not exist": "订单不存在",
|
||||
"Order amount is invalid": "订单金额异常",
|
||||
"You have an unpaid or pending order, please try again later or cancel it": "您有未付款或开通中的订单,请稍后再试或将其取消",
|
||||
"This subscription has been sold out, please choose another subscription": "该订阅已售罄,请更换其它订阅",
|
||||
"This subscription cannot be renewed, please change to another subscription": "该订阅无法续费,请更换其它订阅",
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"Withdrawal account": "提現賬號",
|
||||
"Unsupported withdrawal": "不支持提現",
|
||||
"Order does not exist": "訂單不存在",
|
||||
"Order amount is invalid": "訂單金額異常",
|
||||
"You have an unpaid or pending order, please try again later or cancel it": "您有未付款或開通中的訂單,請稍後再試或將其取消",
|
||||
"This subscription has been sold out, please choose another subscription": "該訂閱已售罄,請更換其它訂閱",
|
||||
"This subscription cannot be renewed, please change to another subscription": "該訂閱無法續費,請更換其它訂閱",
|
||||
|
||||
@ -6,7 +6,6 @@ use App\Models\Server;
|
||||
use App\Models\ServerMachine;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Tests\TestCase;
|
||||
|
||||
class ServerHandshakeTest extends TestCase
|
||||
@ -18,7 +17,7 @@ class ServerHandshakeTest extends TestCase
|
||||
parent::setUp();
|
||||
|
||||
config()->set('app.key', 'base64:' . base64_encode(str_repeat('a', 32)));
|
||||
Cache::forever('admin_settings', [
|
||||
admin_setting([
|
||||
'server_token' => 'server-token',
|
||||
'server_ws_enable' => 0,
|
||||
]);
|
||||
|
||||
9
tests/TestCase.php
Normal file
9
tests/TestCase.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Tests;
|
||||
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
|
||||
abstract class TestCase extends BaseTestCase
|
||||
{
|
||||
}
|
||||
193
tests/Unit/Services/OrderServiceConcurrencyTest.php
Normal file
193
tests/Unit/Services/OrderServiceConcurrencyTest.php
Normal file
@ -0,0 +1,193 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use App\Exceptions\ApiException;
|
||||
use App\Models\GiftCardCode;
|
||||
use App\Models\GiftCardTemplate;
|
||||
use App\Models\Order;
|
||||
use App\Models\Plan;
|
||||
use App\Models\User;
|
||||
use App\Services\GiftCardService;
|
||||
use App\Services\OrderService;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Laravel\Sanctum\Sanctum;
|
||||
use Tests\TestCase;
|
||||
|
||||
class OrderServiceConcurrencyTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
public function test_cancel_only_refunds_once_when_called_with_stale_order_models(): void
|
||||
{
|
||||
$user = $this->makeUser(['balance' => 0]);
|
||||
$plan = $this->makePlan();
|
||||
$order = $this->makeOrder($user, $plan, [
|
||||
'status' => Order::STATUS_PENDING,
|
||||
'balance_amount' => 100,
|
||||
'total_amount' => 1000,
|
||||
]);
|
||||
|
||||
$firstStaleOrder = Order::findOrFail($order->id);
|
||||
$secondStaleOrder = Order::findOrFail($order->id);
|
||||
|
||||
$this->assertTrue((new OrderService($firstStaleOrder))->cancel());
|
||||
$this->assertFalse((new OrderService($secondStaleOrder))->cancel());
|
||||
$this->assertSame(100, User::findOrFail($user->id)->balance);
|
||||
}
|
||||
|
||||
public function test_open_only_applies_subscription_once_when_called_with_stale_order_models(): void
|
||||
{
|
||||
$user = $this->makeUser([
|
||||
'balance' => 0,
|
||||
'expired_at' => 0,
|
||||
'transfer_enable' => 0,
|
||||
'u' => 1073741824,
|
||||
'd' => 0,
|
||||
]);
|
||||
$plan = $this->makePlan();
|
||||
$order = $this->makeOrder($user, $plan, [
|
||||
'status' => Order::STATUS_PROCESSING,
|
||||
'balance_amount' => 1100,
|
||||
'total_amount' => 0,
|
||||
]);
|
||||
|
||||
$before = time();
|
||||
$firstStaleOrder = Order::findOrFail($order->id);
|
||||
$secondStaleOrder = Order::findOrFail($order->id);
|
||||
|
||||
(new OrderService($firstStaleOrder))->open();
|
||||
(new OrderService($secondStaleOrder))->open();
|
||||
|
||||
$user->refresh();
|
||||
$this->assertSame(1, $user->reset_count);
|
||||
$this->assertSame(Order::STATUS_COMPLETED, Order::findOrFail($order->id)->status);
|
||||
$this->assertLessThan($before + (45 * 86400), $user->expired_at);
|
||||
}
|
||||
|
||||
public function test_gift_card_redeem_only_grants_rewards_once_for_stale_code_models(): void
|
||||
{
|
||||
$user = $this->makeUser(['balance' => 0]);
|
||||
$template = GiftCardTemplate::create([
|
||||
'name' => 'race-test',
|
||||
'description' => 'race-test',
|
||||
'type' => GiftCardTemplate::TYPE_GENERAL,
|
||||
'status' => 1,
|
||||
'rewards' => ['balance' => 100],
|
||||
'admin_id' => 1,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
GiftCardCode::create([
|
||||
'template_id' => $template->id,
|
||||
'code' => 'RACEUNITTEST',
|
||||
'status' => GiftCardCode::STATUS_UNUSED,
|
||||
'usage_count' => 0,
|
||||
'max_usage' => 1,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
]);
|
||||
|
||||
$firstService = (new GiftCardService('RACEUNITTEST'))->setUser(User::findOrFail($user->id));
|
||||
$secondService = (new GiftCardService('RACEUNITTEST'))->setUser(User::findOrFail($user->id));
|
||||
|
||||
$firstService->validate();
|
||||
$secondService->validate();
|
||||
$firstService->redeem();
|
||||
|
||||
try {
|
||||
$secondService->redeem();
|
||||
$this->fail('The second stale gift card redeem should fail.');
|
||||
} catch (ApiException $exception) {
|
||||
$this->assertNotSame('', $exception->getMessage());
|
||||
}
|
||||
|
||||
$user->refresh();
|
||||
$code = GiftCardCode::where('code', 'RACEUNITTEST')->firstOrFail();
|
||||
$this->assertSame(100, $user->balance);
|
||||
$this->assertSame(1, $code->usage_count);
|
||||
$this->assertSame(1, $code->usages()->count());
|
||||
}
|
||||
|
||||
public function test_checkout_rejects_negative_order_amount(): void
|
||||
{
|
||||
$user = $this->makeUser(['balance' => 0]);
|
||||
Sanctum::actingAs($user);
|
||||
|
||||
$order = $this->makeOrder($user, $this->makePlan(), [
|
||||
'status' => Order::STATUS_PENDING,
|
||||
'total_amount' => -1,
|
||||
]);
|
||||
|
||||
$response = $this->postJson('/api/v1/user/order/checkout', [
|
||||
'trade_no' => $order->trade_no,
|
||||
]);
|
||||
|
||||
$response->assertStatus(400);
|
||||
$this->assertSame(Order::STATUS_PENDING, Order::findOrFail($order->id)->status);
|
||||
$this->assertNull(User::findOrFail($user->id)->plan_id);
|
||||
}
|
||||
|
||||
private function makeUser(array $overrides = []): User
|
||||
{
|
||||
return User::create(array_merge([
|
||||
'email' => 'race-test@example.com',
|
||||
'password' => 'password',
|
||||
'uuid' => '00000000-0000-0000-0000-000000000001',
|
||||
'token' => '0123456789abcdef0123456789abcdef',
|
||||
'balance' => 0,
|
||||
'commission_balance' => 0,
|
||||
'transfer_enable' => 0,
|
||||
'u' => 0,
|
||||
'd' => 0,
|
||||
'banned' => 0,
|
||||
'is_admin' => 0,
|
||||
'is_staff' => 0,
|
||||
'expired_at' => 0,
|
||||
'remind_expire' => 1,
|
||||
'remind_traffic' => 1,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
private function makePlan(array $overrides = []): Plan
|
||||
{
|
||||
return Plan::create(array_merge([
|
||||
'group_id' => null,
|
||||
'transfer_enable' => 1111,
|
||||
'name' => 'Race Test Plan',
|
||||
'speed_limit' => null,
|
||||
'show' => 1,
|
||||
'sort' => 0,
|
||||
'renew' => 1,
|
||||
'prices' => [
|
||||
Plan::PERIOD_MONTHLY => 11,
|
||||
],
|
||||
'reset_traffic_method' => Plan::RESET_TRAFFIC_MONTHLY,
|
||||
'capacity_limit' => null,
|
||||
'sell' => 1,
|
||||
'device_limit' => null,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
], $overrides));
|
||||
}
|
||||
|
||||
private function makeOrder(User $user, Plan $plan, array $overrides = []): Order
|
||||
{
|
||||
return Order::create(array_merge([
|
||||
'user_id' => $user->id,
|
||||
'plan_id' => $plan->id,
|
||||
'type' => Order::TYPE_NEW_PURCHASE,
|
||||
'period' => Plan::PERIOD_MONTHLY,
|
||||
'trade_no' => uniqid('race_', true),
|
||||
'total_amount' => 0,
|
||||
'balance_amount' => 0,
|
||||
'status' => Order::STATUS_PENDING,
|
||||
'commission_status' => 0,
|
||||
'commission_balance' => 0,
|
||||
'created_at' => time(),
|
||||
'updated_at' => time(),
|
||||
], $overrides));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user