Browse Source

feat:技师端-撤离(完成服务并分佣)

刘学玺 4 months ago
parent
commit
bdbbc74fdb

+ 11 - 0
app/Enums/OrderStatus.php

@@ -92,6 +92,11 @@ enum OrderStatus: int
      */
     case ALARM = 17;
 
+    /**
+     * 订单状态:服务完成
+     */
+    case COMPLETED = 18;
+
     /**
      * 获取状态的显示文本
      *
@@ -116,6 +121,8 @@ enum OrderStatus: int
             self::LEFT => '撤离',
             self::COMMENTED => '已评价',
             self::REJECTED => '已拒单',
+            self::ALARM => '报警',
+            self::COMPLETED => '服务完成',
         };
     }
 
@@ -164,6 +171,8 @@ enum OrderStatus: int
             self::LEFT->value => self::LEFT,
             self::COMMENTED->value => self::COMMENTED,
             self::REJECTED->value => self::REJECTED,
+            self::ALARM->value => self::ALARM,
+            self::COMPLETED->value => self::COMPLETED,
             default => null
         };
     }
@@ -201,6 +210,8 @@ enum OrderStatus: int
             self::LEFT->value => self::LEFT->label(),
             self::COMMENTED->value => self::COMMENTED->label(),
             self::REJECTED->value => self::REJECTED->label(),
+            self::ALARM->value => self::ALARM->label(),
+            self::COMPLETED->value => self::COMPLETED->label(),
         ];
     }
 }

+ 30 - 0
app/Http/Controllers/Coach/OrderController.php

@@ -275,4 +275,34 @@ class OrderController extends Controller
             $validated['qr_code']
         );
     }
+
+    /**
+     * [订单]撤离
+     *
+     * @description 技师确认已完成服务并撤离服务地点
+     *
+     * @authenticated
+     *
+     * @urlParam order_id integer required 订单ID Example: 1
+     *
+     * @response {
+     *   "status": true,
+     *   "message": "撤离成功",
+     *   "data": {
+     *     "order_id": 1,
+     *     "state": "completed",
+     *     "leave_time": "2024-03-21 12:30:00"
+     *   }
+     * }
+     * @response 400 {
+     *   "message": "订单状态异常,无法确认撤离"
+     * }
+     * @response 403 {
+     *   "message": "该订单未分配给您"
+     * }
+     */
+    public function leave(int $order_id)
+    {
+        return $this->service->leave(Auth::user()->id, $order_id);
+    }
 }

+ 5 - 3
app/Models/WalletSplitRecord.php

@@ -10,7 +10,9 @@ use Slowlyo\OwlAdmin\Models\BaseModel as Model;
  */
 class WalletSplitRecord extends Model
 {
-	use SoftDeletes;
+    use SoftDeletes;
 
-	protected $table = 'wallet_split_records';
-}
+    protected $table = 'wallet_split_records';
+
+    protected $guarded = [];
+}

+ 244 - 0
app/Services/Coach/CommissionService.php

@@ -0,0 +1,244 @@
+<?php
+
+namespace App\Services\Coach;
+
+use App\Enums\OrderType;
+use App\Models\AgentInfo;
+use App\Models\MemberUser;
+use App\Models\Order;
+use App\Models\Wallet;
+use App\Models\WalletSplitRecord;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Log;
+
+class CommissionService
+{
+    // 分佣比例配置
+    private const COMMISSION_RATES = [
+        'travel' => [
+            'coach' => 0.9,
+            'platform' => 0.1,
+        ],
+        'service' => [
+            'normal' => 0.5,
+            'overtime' => 0.7,
+        ],
+        'inviter' => [
+            'first' => 0.2,
+            'second' => 0.1,
+        ],
+        'profit' => [
+            'agent' => 0.4,
+            'platform' => 0.4,
+            'platform_no_agent' => 1.0,
+        ],
+    ];
+
+    /**
+     * 处理订单分佣
+     */
+    public function handleOrderCommission(Order $order): void
+    {
+        DB::transaction(function () use ($order) {
+            try {
+                // 处理路费分佣
+                $this->handleTravelCommission($order);
+
+                // 处理服务费分佣
+                $profitAmount = $this->handleServiceCommission($order);
+
+                // 处理邀请人分佣
+                $inviterAmount = $this->handleInviterCommission($order, $profitAmount);
+
+                // 处理代理商和平台分佣
+                $this->handleAgentAndPlatformCommission($order, $profitAmount, $inviterAmount);
+
+            } catch (\Exception $e) {
+                Log::error('订单分佣失败', [
+                    'order_id' => $order->id,
+                    'error' => $e->getMessage(),
+                    'trace' => $e->getTraceAsString(),
+                ]);
+                throw $e;
+            }
+        });
+    }
+
+    /**
+     * 处理路费分佣
+     */
+    private function handleTravelCommission(Order $order): void
+    {
+        $travelAmount = $order->travel_amount;
+        if ($travelAmount <= 0) {
+            return;
+        }
+
+        // 技师路费分佣
+        $coachRate = self::COMMISSION_RATES['travel']['coach'];
+        $coachAmount = bcmul($travelAmount, $coachRate, 2);
+        $this->updateWalletBalance($order->coach->wallet, $coachAmount, $order, 'coach', '技师路费分账');
+
+        // 平台路费分佣
+        $platformRate = self::COMMISSION_RATES['travel']['platform'];
+        $platformAmount = bcmul($travelAmount, $platformRate, 2);
+        $platformWallet = Wallet::where('owner_type', 'platform')->first();
+        $this->updateWalletBalance($platformWallet, $platformAmount, $order, 'platform', '平台路费分账');
+
+        Log::info('路费分佣完成', [
+            'order_id' => $order->id,
+            'travel_amount' => $travelAmount,
+            'coach_amount' => $coachAmount,
+            'platform_amount' => $platformAmount,
+        ]);
+    }
+
+    /**
+     * 处理服务费分佣
+     */
+    private function handleServiceCommission(Order $order): float
+    {
+        // 计算服务费(总金额减去路费)
+        $serviceAmount = bcsub($order->amount, $order->travel_amount ?? 0, 2);
+
+        // 获取分佣比例
+        $rate = $order->type === OrderType::OVERTIME->value
+            ? self::COMMISSION_RATES['service']['overtime']
+            : self::COMMISSION_RATES['service']['normal'];
+
+        // 计算技师分佣金额
+        $commissionAmount = bcmul($serviceAmount, $rate, 2);
+
+        // 更新技师钱包
+        $this->updateWalletBalance(
+            $order->coach->wallet,
+            $commissionAmount,
+            $order,
+            'coach',
+            '技师服务佣金分账'
+        );
+
+        // 计算平台利润
+        $profitAmount = bcsub($serviceAmount, $commissionAmount, 2);
+
+        Log::info('服务费分佣完成', [
+            'order_id' => $order->id,
+            'service_amount' => $serviceAmount,
+            'commission_rate' => $rate,
+            'commission_amount' => $commissionAmount,
+            'profit_amount' => $profitAmount,
+        ]);
+
+        return $profitAmount;
+    }
+
+    /**
+     * 处理邀请人分佣
+     */
+    private function handleInviterCommission(Order $order, float $profitAmount): float
+    {
+        $totalInviterAmount = 0;
+
+        $user = MemberUser::with('inviter')->find($order->user_id);
+        if (! $user || ! $user->inviter) {
+            return $totalInviterAmount;
+        }
+
+        // 一级邀请人分佣
+        $firstInviterAmount = $this->processInviter(
+            $user->inviter,
+            $profitAmount,
+            self::COMMISSION_RATES['inviter']['first'],
+            $order,
+            '邀请人分账'
+        );
+        $totalInviterAmount = bcadd($totalInviterAmount, $firstInviterAmount, 2);
+
+        // 二级邀请人分佣
+        $secondInviter = $this->getSecondInviter($user);
+        if ($secondInviter) {
+            $secondInviterAmount = $this->processInviter(
+                $secondInviter,
+                $profitAmount,
+                self::COMMISSION_RATES['inviter']['second'],
+                $order,
+                '二级邀请人分账'
+            );
+            $totalInviterAmount = bcadd($totalInviterAmount, $secondInviterAmount, 2);
+        }
+
+        return $totalInviterAmount;
+    }
+
+    /**
+     * 处理代理商和平台分佣
+     */
+    private function handleAgentAndPlatformCommission(Order $order, float $profitAmount, float $inviterAmount): void
+    {
+        // 计算分佣基数
+        $baseAmount = bcsub($profitAmount, $inviterAmount, 2);
+
+        if ($order->agent_id) {
+            // 代理商分佣
+            $agentRate = self::COMMISSION_RATES['profit']['agent'];
+            $agentAmount = bcmul($baseAmount, $agentRate, 2);
+
+            $agent = AgentInfo::find($order->agent_id);
+            $this->updateWalletBalance($agent->wallet, $agentAmount, $order, 'agent', '代理商分账');
+
+            // 平台分佣
+            $platformRate = self::COMMISSION_RATES['profit']['platform'];
+            $platformAmount = bcmul($baseAmount, $platformRate, 2);
+        } else {
+            // 无代理商时平台获取全部收益
+            $platformRate = self::COMMISSION_RATES['profit']['platform_no_agent'];
+            $platformAmount = bcmul($baseAmount, $platformRate, 2);
+        }
+
+        // 记录平台分账
+        $this->createSplitRecord($order->id, 1, 'platform_commission', $baseAmount, $platformRate, $platformAmount, '平台分账');
+    }
+
+    /**
+     * 更新钱包余额并记录
+     */
+    private function updateWalletBalance(Wallet $wallet, float $amount, Order $order, string $role, string $remark): void
+    {
+        $wallet->increment('total_balance', $amount);
+        $wallet->increment('available_balance', $amount);
+        $wallet->save();
+
+        // 记录交易
+        $wallet->transRecords()->create([
+            'wallet_id' => $wallet->id,
+            'owner_id' => $order->id,
+            'owner_type' => Order::class,
+            'role' => $role,
+            'trans_type' => 1,
+            'storage_type' => 'balance',
+            'amount' => $amount,
+            'before_balance' => $wallet->available_balance - $amount,
+            'after_balance' => $wallet->available_balance,
+            'before_recharge_balance' => $wallet->recharge_balance,
+            'after_recharge_balance' => $wallet->recharge_balance,
+        ]);
+    }
+
+    /**
+     * 创建分账记录
+     */
+    private function createSplitRecord(int $orderId, int $ruleId, string $splitType, float $amount, float $ratio, float $splitAmount, string $remark): void
+    {
+        WalletSplitRecord::create([
+            'order_id' => $orderId,
+            'rule_id' => $ruleId,
+            'split_type' => $splitType,
+            'amount' => $amount,
+            'split_ratio' => $ratio,
+            'split_amount' => $splitAmount,
+            'entry_time' => now(),
+            'remark' => $remark,
+            'state' => 1,
+        ]);
+    }
+}

+ 136 - 2
app/Services/Coach/OrderService.php

@@ -29,9 +29,12 @@ class OrderService
 
     private SettingItemService $settingService;
 
-    public function __construct(SettingItemService $settingService)
+    private CommissionService $commissionService;
+
+    public function __construct(SettingItemService $settingService, CommissionService $commissionService)
     {
         $this->settingService = $settingService;
+        $this->commissionService = $commissionService;
     }
 
     /**
@@ -156,7 +159,7 @@ class OrderService
         $coach = $user->coach;
         abort_if(! $coach, 404, '技师不存在');
         abort_if(! $coach->info, 404, '技师信息不存在');
-        abort_if($coach->info->state != TechnicianStatus::ACTIVE->value, 404, '技师状异常');
+        abort_if($coach->info->state != TechnicianStatus::ACTIVE->value, 404, '技师状异常');
         abort_if($coach->real->state != TechnicianAuthStatus::PASSED->value, 404, '技师实名认证未通过');
         abort_if($coach->qual->state != TechnicianAuthStatus::PASSED->value, 404, '技师资质认证未通过');
 
@@ -844,4 +847,135 @@ class OrderService
         $correctSign = md5("order_{$order->id}_{$timestamp}_".config('app.key'));
         abort_if($sign !== $correctSign, 400, '二维码签名错误');
     }
+
+    /**
+     * 技师撤离
+     *
+     * @param int $userId 技师用户ID
+     * @param int $orderId 订单ID
+     * @return array
+     * @throws \Exception
+     */
+    public function leave(int $userId, int $orderId): array
+    {
+        return DB::transaction(function () use ($userId, $orderId) {
+            try {
+                // 获取技师信息
+                $user = MemberUser::with(['coach'])->findOrFail($userId);
+                $coach = $user->coach;
+                abort_if(!$coach, 404, '技师信息不存在');
+
+                // 获取订单信息
+                $order = Order::query()
+                    ->with(['coach', 'coach.wallet'])
+                    ->where('id', $orderId)
+                    ->lockForUpdate()
+                    ->firstOrFail();
+
+                // 验证订单状态和权限
+                $this->validateLeaveOrder($order, $coach);
+
+                // 更新订单状态
+                $this->updateOrderStatus($order);
+
+                // 记录订单状态变更
+                $this->createLeaveRecord($order, $coach);
+
+                // 处理订单分佣
+                $this->commissionService->handleOrderCommission($order);
+
+                // 清理技师位置信息
+                $this->cleanCoachLocation($coach);
+
+                // 记录日志
+                Log::info('技师撤离成功', [
+                    'coach_id' => $coach->id,
+                    'order_id' => $orderId,
+                    'leave_time' => now()
+                ]);
+
+                // TODO: 发送通知给用户
+                // event(new ServiceCompletedEvent($order));
+
+                return [
+                    'status' => true,
+                    'message' => '撤离成功',
+                    'data' => [
+                        'order_id' => $orderId,
+                        'state' => $order->state,
+                        'leave_time' => now()
+                    ]
+                ];
+
+            } catch (\Exception $e) {
+                Log::error('技师撤离失败', [
+                    'user_id' => $userId,
+                    'order_id' => $orderId,
+                    'error' => $e->getMessage(),
+                    'trace' => $e->getTraceAsString()
+                ]);
+                throw $e;
+            }
+        });
+    }
+
+    /**
+     * 验证撤离订单
+     */
+    private function validateLeaveOrder(Order $order, CoachUser $coach): void
+    {
+        // 检查是否是该技师的订单
+        abort_if($order->coach_id !== $coach->id, 403, '无权操作此订单');
+
+        // 检查订单状态
+        abort_if(!in_array($order->state, [
+            OrderStatus::LEFT->value,
+        ]), 400, '订单状态不正确,无法撤离');
+
+        // 检查钱包状态
+        abort_if(!$coach->wallet, 400, '技师钱包信息不存在');
+    }
+
+    /**
+     * 更新订单状态
+     */
+    private function updateOrderStatus(Order $order): void
+    {
+        $order->state = OrderStatus::COMPLETED->value;
+        $order->completed_at = now();
+        $order->save();
+    }
+
+    /**
+     * 创建撤离记录
+     */
+    private function createLeaveRecord(Order $order, CoachUser $coach): void
+    {
+        OrderRecord::create([
+            'order_id' => $order->id,
+            'state' => OrderRecordStatus::COMPLETED->value,
+            'object_id' => $coach->id,
+            'object_type' => CoachUser::class,
+            'remark' => '技师已撤离,服务完成'
+        ]);
+    }
+
+    /**
+     * 清理技师位置信息
+     */
+    private function cleanCoachLocation(CoachUser $coach): void
+    {
+        try {
+            Redis::zrem(
+                'coach_locations',
+                $coach->id . '_' . TechnicianLocationType::CURRENT->value
+            );
+        } catch (\Exception $e) {
+            Log::error('删除技师位置失败', [
+                'coach_id' => $coach->id,
+                'error' => $e->getMessage()
+            ]);
+            // 不抛出异常,继续执行
+        }
+    }
 }

+ 5 - 1
routes/api.php

@@ -23,7 +23,7 @@ Route::get('/enums', [EnumController::class, 'getEnumData']);
 // 客户端路由组
 Route::prefix('client')->group(function () {
 
-    // 无需认证的��路由
+    // 无需认证的路由
     Route::prefix('account')->group(function () {
         // 发验证码
         Route::post('send-code', [AccountController::class, 'sendVerifyCode']);
@@ -167,6 +167,10 @@ Route::middleware(['auth:sanctum', 'verified'])->prefix('coach')->group(function
         Route::post('/arrive/{order_id}', [CoachOrderController::class, 'arrive']);
         // 技师开始服务
         Route::post('/start-service', [CoachOrderController::class, 'startService']);
+        // 技师撤离
+        Route::post('coach/orders/{order_id}/leave', [CoachOrderController::class, 'leave'])
+            ->name('coach.orders.leave')
+            ->where('order_id', '[0-9]+');
     });
 
     // 项目相关路由