feat: add tags to plans, refactor order service, adn fix ticket lock

This commit is contained in:
xboard
2025-07-01 11:14:18 +08:00
parent 349c1710fe
commit 05afe870e7
13 changed files with 208 additions and 97 deletions
@@ -43,7 +43,7 @@ class OrderController extends Controller
$request->validate([
'trade_no' => 'required|string',
]);
$order = Order::with(['payment','plan'])
$order = Order::with(['payment', 'plan'])
->where('user_id', $request->user()->id)
->where('trade_no', $request->input('trade_no'))
->first();
@@ -77,45 +77,16 @@ class OrderController extends Controller
$plan = Plan::findOrFail($request->input('plan_id'));
$planService = new PlanService($plan);
// Validate plan purchase
$planService->validatePurchase($user, $request->input('period'));
return DB::transaction(function () use ($request, $plan, $user, $userService) {
$period = $request->input('period');
$newPeriod = PlanService::getPeriodKey($period);
$order = OrderService::createFromRequest(
$user,
$plan,
$request->input('period'),
$request->input('coupon_code')
);
// Create order
$order = new Order([
'user_id' => $user->id,
'plan_id' => $plan->id,
'period' => $newPeriod,
'trade_no' => Helper::generateOrderNo(),
'total_amount' => optional($plan->prices)[$newPeriod] * 100
]);
// Apply coupon if provided
if ($request->input('coupon_code')) {
$this->applyCoupon($order, $request->input('coupon_code'));
}
// Set order attributes
$orderService = new OrderService($order);
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
// Handle user balance
if ($user->balance && $order->total_amount > 0) {
$this->handleUserBalance($order, $user, $userService);
}
if (!$order->save()) {
throw new ApiException(__('Failed to create order'));
}
HookManager::call('order.after_create', $order);
return $this->success($order->trade_no);
});
return $this->success($order->trade_no);
}
protected function applyCoupon(Order $order, string $couponCode): void
@@ -43,8 +43,9 @@ class TicketController extends Controller
{
try{
DB::beginTransaction();
if ((int)Ticket::where('status', 0)->where('user_id', $request->user()->id)->lockForUpdate()->count()) {
throw new \Exception(__('There are other unresolved tickets'));
if (Ticket::where('status', 0)->where('user_id', $request->user()->id)->lockForUpdate()->first()) {
DB::rollBack();
return $this->fail([400, '存在未关闭的工单']);
}
$ticket = Ticket::create(array_merge($request->only([
'subject',
+4 -2
View File
@@ -17,7 +17,8 @@ class KnowledgeSave extends FormRequest
'category' => 'required',
'language' => 'required',
'title' => 'required',
'body' => 'required'
'body' => 'required',
'show' => 'nullable|boolean'
];
}
@@ -27,7 +28,8 @@ class KnowledgeSave extends FormRequest
'title.required' => '标题不能为空',
'category.required' => '分类不能为空',
'body.required' => '内容不能为空',
'language.required' => '语言不能为空'
'language.required' => '语言不能为空',
'show.boolean' => '显示状态必须为布尔值'
];
}
}
+2
View File
@@ -34,6 +34,7 @@ class PlanSave extends FormRequest
'speed_limit' => 'integer|nullable|min:0',
'device_limit' => 'integer|nullable|min:0',
'capacity_limit' => 'integer|nullable|min:0',
'tags' => 'array|nullable',
];
}
@@ -136,6 +137,7 @@ class PlanSave extends FormRequest
'device_limit.min' => '设备限制不能为负数',
'capacity_limit.integer' => '容量限制必须是整数',
'capacity_limit.min' => '容量限制不能为负数',
'tags.array' => '标签格式必须是数组',
];
}
+1
View File
@@ -23,6 +23,7 @@ class PlanResource extends JsonResource
'id' => $this->resource['id'],
'group_id' => $this->resource['group_id'],
'name' => $this->resource['name'],
'tags' => $this->resource['tags'],
'content' => $this->formatContent(),
...$this->getPeriodPrices(),
'capacity_limit' => $this->getFormattedCapacityLimit(),
+2
View File
@@ -18,6 +18,8 @@ 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_amount
* @property int $type
* @property int $status
* @property array|null $surplus_order_ids
+4 -1
View File
@@ -21,6 +21,7 @@ use Illuminate\Database\Eloquent\Relations\HasOne;
* @property bool $renew 是否允许续费
* @property bool $sell 是否允许购买
* @property array|null $prices 价格配置
* @property array|null $tags 标签
* @property int $sort 排序
* @property string|null $content 套餐描述
* @property int|null $reset_traffic_method 流量重置方式
@@ -85,7 +86,8 @@ class Plan extends Model
'reset_traffic_method',
'capacity_limit',
'sell',
'device_limit'
'device_limit',
'tags'
];
protected $casts = [
@@ -95,6 +97,7 @@ class Plan extends Model
'updated_at' => 'timestamp',
'group_id' => 'integer',
'prices' => 'array',
'tags' => 'array',
'reset_traffic_method' => 'integer',
];
+91 -5
View File
@@ -7,6 +7,8 @@ use App\Jobs\OrderHandleJob;
use App\Models\Order;
use App\Models\Plan;
use App\Models\User;
use App\Services\Plugin\HookManager;
use App\Utils\Helper;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Log;
@@ -28,6 +30,62 @@ class OrderService
$this->order = $order;
}
/**
* Create an order from a request.
*
* @param User $user
* @param Plan $plan
* @param string $period
* @param string|null $couponCode
* @param array|null $telegramMessageIds
* @return Order
* @throws ApiException
*/
public static function createFromRequest(
User $user,
Plan $plan,
string $period,
?string $couponCode = null,
): Order {
$userService = app(UserService::class);
$planService = new PlanService($plan);
$planService->validatePurchase($user, $period);
return DB::transaction(function () use ($user, $plan, $period, $couponCode, $userService) {
$newPeriod = PlanService::getPeriodKey($period);
$order = new Order([
'user_id' => $user->id,
'plan_id' => $plan->id,
'period' => $newPeriod,
'trade_no' => Helper::generateOrderNo(),
'total_amount' => (int) (optional($plan->prices)[$newPeriod] * 100),
]);
$orderService = new self($order);
if ($couponCode) {
$orderService->applyCoupon($couponCode);
}
$orderService->setVipDiscount($user);
$orderService->setOrderType($user);
$orderService->setInvite($user);
if ($user->balance && $order->total_amount > 0) {
$orderService->handleUserBalance($user, $userService);
}
if (!$order->save()) {
throw new ApiException(__('Failed to create order'));
}
HookManager::call('order.after_create', $order);
return $order;
});
}
public function open()
{
$order = $this->order;
@@ -68,7 +126,7 @@ class OrderService
}
$this->setSpeedLimit($plan->speed_limit);
$this->setDeviceLimit($plan->device_limit);
$this->setDeviceLimit($plan->device_limit);
if (!$this->user->save()) {
throw new \Exception('用户信息保存失败');
@@ -98,10 +156,10 @@ class OrderService
if ((int) admin_setting('surplus_enable', 1))
$this->getSurplusValue($user, $order);
if ($order->surplus_amount >= $order->total_amount) {
$order->refund_amount = $order->surplus_amount - $order->total_amount;
$order->refund_amount = (int) ($order->surplus_amount - $order->total_amount);
$order->total_amount = 0;
} else {
$order->total_amount = $order->total_amount - $order->surplus_amount;
$order->total_amount = (int) ($order->total_amount - $order->surplus_amount);
}
} else if ($user->expired_at > time() && $order->plan_id == $user->plan_id) { // 用户订阅未过期且购买订阅与当前订阅相同 === 续费
$order->type = Order::TYPE_RENEWAL;
@@ -187,7 +245,7 @@ class OrderService
$notUsedTraffic = $nowUserTraffic - (($user->u + $user->d) / 1073741824);
$result = $trafficUnitPrice * $notUsedTraffic;
$orderModel = Order::where('user_id', $user->id)->where('period', '!=', Plan::PERIOD_RESET_TRAFFIC)->where('status', Order::STATUS_COMPLETED);
$order->surplus_amount = $result > 0 ? $result : 0;
$order->surplus_amount = (int) ($result > 0 ? $result : 0);
$order->surplus_order_ids = array_column($orderModel->get()->toArray(), 'id');
}
@@ -222,7 +280,7 @@ class OrderService
$orderSurplusAmount = $avgPrice * $orderSurplusSecond;
if (!$orderSurplusSecond || !$orderSurplusAmount)
return;
$order->surplus_amount = $orderSurplusAmount > 0 ? $orderSurplusAmount : 0;
$order->surplus_amount = (int) ($orderSurplusAmount > 0 ? $orderSurplusAmount : 0);
$order->surplus_order_ids = array_column($orders, 'id');
}
@@ -344,4 +402,32 @@ class OrderService
break;
}
}
protected function applyCoupon(string $couponCode): void
{
$couponService = new CouponService($couponCode);
if (!$couponService->use($this->order)) {
throw new ApiException(__('Coupon failed'));
}
$this->order->coupon_id = $couponService->getId();
}
protected function handleUserBalance(User $user, UserService $userService): void
{
$remainingBalance = $user->balance - $this->order->total_amount;
if ($remainingBalance >= 0) {
if (!$userService->addBalance($this->order->user_id, -$this->order->total_amount)) {
throw new ApiException(__('Insufficient balance'));
}
$this->order->balance_amount = $this->order->total_amount;
$this->order->total_amount = 0;
} else {
if (!$userService->addBalance($this->order->user_id, -$user->balance)) {
throw new ApiException(__('Insufficient balance'));
}
$this->order->balance_amount = $user->balance;
$this->order->total_amount = $this->order->total_amount - $user->balance;
}
}
}