Browse Source

Merge branch 'master' of ssh://gogs.yinbin.ink:30004/didong/owl-admin

景好勇win11 4 months ago
parent
commit
dcb022b36b

+ 60 - 32
app/Http/Controllers/Coach/ProjectController.php

@@ -2,11 +2,12 @@
 
 namespace App\Http\Controllers\Coach;
 
-use App\Http\Controllers\Controller;
-use App\Services\Coach\ProjectService;
-use App\Traits\ResponseTrait;
 use Illuminate\Http\Request;
+use App\Traits\ResponseTrait;
+use App\Http\Controllers\Controller;
 use Illuminate\Support\Facades\Auth;
+use App\Services\Coach\ProjectService;
+use App\Http\Requests\Coach\OpenProjectRequest;
 
 /**
  * @group 技师端
@@ -27,54 +28,81 @@ class ProjectController extends Controller
     /**
      * [项目]获取可开通项目列表
      *
-     * @description 获取技师可以开通的项目列表
+     * @description 获取技师可以开通的项目列表,包含项目的基本信息、价格信息和服务说明,以及是否已开通状态
      *
-     * @authenticated
+     * @authenticated 需要技师身份认证
      *
      * @response {
-     *   "data": {
-     *     "items": [
-     *       {
-     *         "id": 1,
-     *         "name": "精油推拿",
-     *         "cover": "http://example.com/cover.jpg",
-     *         "price": "188.00",
-     *         "duration": 60,
-     *         "description": "项目描述",
-     *         "qualification_requirements": "资质要求"
-     *       }
-     *     ],
-     *     "total": 10
-     *   }
+     *   "status": true,
+     *   "message": "获取成功",
+     *   "data": [
+     *     {
+     *       "id": 1,                    // 项目ID
+     *       "title": "精油推拿",         // 项目标题
+     *       "subtitle": "专业精油推拿",  // 项目副标题
+     *       "cover": "http://example.com/cover.jpg", // 项目封面图
+     *       "price": "188.00",          // 项目价格
+     *       "original_price": "288.00", // 原价
+     *       "sales": 100,               // 销量
+     *       "duration": 60,             // 服务时长(分钟)
+     *       "project_desc": "项目描述",  // 项目描述
+     *       "service_desc": "服务说明",  // 服务说明
+     *       "type": 1,                  // 项目类型
+     *       "is_opened": false          // 是否已开通
+     *     }
+     *   ]
+     * }
+     *
+     * @response 404 {
+     *   "message": "技师信息不存在"
      * }
      */
     public function getAvailableProjects()
     {
-        return $this->success($this->service->getAvailableProjects(Auth::user()->id));
+        return $this->success(
+            $this->service->getAvailableProjects(Auth::user()->coach)
+        );
     }
 
     /**
-     * [项目]开通项目
+     * [项目]开通/关闭项目
      *
-     * @description 技师开通新的服务项目
+     * @description 技师开通或关闭服务项目,包含项目状态和资格检查
      *
-     * @authenticated
+     * 业务流程:
+     * 1. 验证项目ID和操作类型
+     * 2. 调用服务层处理开关逻辑
+     * 3. 返回操作结果
+     *
+     * @authenticated 需要技师身份认证
      *
      * @bodyParam project_id integer required 项目ID Example: 1
+     * @bodyParam action string required 操作类型(open:开通 close:关闭) Example: "open"
      *
      * @response {
-     *   "message": "项目开通成功",
-     *   "project_id": 1,
-     *   "project_name": "精油推拿"
+     *   "status": true,
+     *   "message": "获取成功",
+     *   "data": {
+     *     "project_id": 1,         // 项目ID
+     *     "project_name": "精油推拿", // 项目名称
+     *     "state": 1,              // 项目状态
+     *     "state_text": "已开通"    // 状态描述
+     *   }
+     * }
+     *
+     * @response 404 {
+     *   "message": "项目不存在或已下架"
+     * }
+     * @response 422 {
+     *   "message": "技师状态异常,无法操作项目"
      * }
      */
-    public function openProject(Request $request)
+    public function openProject(OpenProjectRequest $request)
     {
-        $data = $request->validate([
-            'project_id' => 'required|integer|exists:project,id',
-        ]);
-
-        return $this->success($this->service->openProject(Auth::user()->id, $data));
+        // 调用服务层处理开通/关闭逻辑并返回结果
+        return $this->success(
+            $this->service->openProject(Auth::user()->coach, $request->validated())
+        );
     }
 
     /**

+ 49 - 0
app/Http/Requests/Coach/OpenProjectRequest.php

@@ -0,0 +1,49 @@
+<?php
+
+namespace App\Http\Requests\Coach;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+/**
+ * 技师开通项目请求验证
+ */
+class OpenProjectRequest extends FormRequest
+{
+    /**
+     * 判断用户是否有权限进行此请求
+     */
+    public function authorize(): bool
+    {
+        return true;
+    }
+
+    /**
+     * 获取验证规则
+     *
+     * @return array<string, mixed> 验证规则数组
+     */
+    public function rules(): array
+    {
+        return [
+            'project_id' => 'required|integer|exists:project,id',
+            'action' => 'required|string|in:open,close',
+        ];
+    }
+
+    /**
+     * 获取验证错误消息
+     *
+     * @return array<string, string> 错误消息数组
+     */
+    public function messages(): array
+    {
+        return [
+            'project_id.required' => '项目ID不能为空',
+            'project_id.integer' => '项目ID必须是整数',
+            'project_id.exists' => '项目不存在',
+            'action.required' => '操作类型不能为空',
+            'action.string' => '操作类型必须是字符串',
+            'action.in' => '操作类型只能是开通或关闭',
+        ];
+    }
+}

+ 51 - 1
app/Models/CoachUser.php

@@ -2,8 +2,21 @@
 
 namespace App\Models;
 
-use App\Enums\TechnicianStatus;
+use App\Models\Order;
+use App\Models\Wallet;
+use App\Models\ShopInfo;
+use App\Models\CoachScore;
+use App\Enums\ProjectStatus;
+use App\Models\CoachProject;
+use App\Models\OrderComment;
 use App\Traits\HasStateText;
+use App\Models\CoachLocation;
+use App\Models\CoachStatistic;
+use App\Enums\TechnicianStatus;
+use App\Models\CoachQualRecord;
+use App\Models\CoachRealRecord;
+use App\Models\OrderGrabRecord;
+use App\Models\ShopCoachService;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Slowlyo\OwlAdmin\Models\BaseModel as Model;
 
@@ -198,4 +211,41 @@ class CoachUser extends Model
     {
         return $this->hasMany(CoachRealRecord::class, 'coach_id', 'id');
     }
+
+    /**
+     * 验证技师状态是否正常
+     *
+     * @param string|null $message 自定义错误消息
+     * @throws \Illuminate\Http\Exceptions\HttpResponseException 当技师状态异常时抛出异常
+     */
+    public function validateActiveStatus(?string $message = null): void
+    {
+        abort_if(
+            $this->state !== TechnicianStatus::ACTIVE->value,
+            422,
+            $message ?? '技师状态异常'
+        );
+    }
+
+    /**
+     * 创建或更新技师项目关联
+     *
+     * @param int $projectId 项目ID
+     * @param int $state 项目状态
+     * @return CoachProject 返回创建或更新的技师项目关联实例
+     */
+    public function updateOrCreateProjectRelation(int $projectId, int $state): CoachProject
+    {
+        return $this->projects()->updateOrCreate(
+            ['project_id' => $projectId],
+            [
+                'state' => $state,
+                'discount_amount' => 0.00,    // 默认折扣金额
+                'service_gender' => 0,        // 默认服务性别(不限)
+                'service_distance' => 0,      // 默认服务距离
+                'traffic_fee_type' => 2,      // 默认交通费类型(双程)
+            ]
+        );
+    }
+
 }

+ 44 - 0
app/Models/Project.php

@@ -2,6 +2,10 @@
 
 namespace App\Models;
 
+use App\Models\ProjectCate;
+use App\Models\ShopService;
+use App\Enums\ProjectStatus;
+use App\Models\CoachProject;
 use Illuminate\Database\Eloquent\SoftDeletes;
 use Slowlyo\OwlAdmin\Models\BaseModel as Model;
 
@@ -33,4 +37,44 @@ class Project extends Model
     {
         return $this->hasMany(ShopService::class, 'service_id', 'id');
     }
+
+    /**
+     * 验证项目是否存在且状态正常
+     *
+     * @param int $projectId 项目ID
+     * @param string|null $message 自定义错误消息
+     * @return self 返回项目模型实例
+     * @throws \Illuminate\Http\Exceptions\HttpResponseException 当项目不存在或状态异常时抛出异常
+     */
+    public static function validateOpenProject(int $projectId, ?string $message = null): self
+    {
+        $project = self::where('id', $projectId)
+            ->where('state', ProjectStatus::OPEN->value)
+            ->first();
+
+        abort_if(!$project, 404, $message ?? '项目不存在或已下架');
+
+        return $project;
+    }
+
+    /**
+     * 格式化项目操作结果
+     *
+     * @param int $state 项目状态
+     * @param string|null $stateText 状态描述文本
+     * @return array 格式化后的操作结果,包含:
+     *        - project_id: int 项目ID
+     *        - project_name: string 项目名称
+     *        - state: int 项目状态
+     *        - state_text: string 状态描述
+     */
+    public function formatActionResult(int $state, ?string $stateText = null): array
+    {
+        return [
+            'project_id' => $this->id,
+            'project_name' => $this->title,
+            'state' => $state,
+            'state_text' => $stateText ?? ($state === ProjectStatus::OPEN->value ? '已开通' : '已关闭'),
+        ];
+    }
 }

+ 115 - 111
app/Services/Coach/ProjectService.php

@@ -2,11 +2,12 @@
 
 namespace App\Services\Coach;
 
-use App\Enums\ProjectStatus;
-use App\Enums\TechnicianStatus;
-use App\Models\MemberUser;
 use App\Models\Project;
+use App\Models\CoachUser;
+use App\Models\MemberUser;
+use App\Enums\ProjectStatus;
 use App\Models\SettingGroup;
+use App\Enums\TechnicianStatus;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
 
@@ -15,123 +16,127 @@ class ProjectService
     /**
      * 获取所有可以开通的项目列表
      *
-     * @param  int  $userId  技师用户ID
+     * 业务流程:
+     * 1. 获取所有状态为开放的项目
+     * 2. 通过关联查询检查项目开通状态
+     * 3. 返回项目列表
+     *
+     * 注意事项:
+     * - 只返回状态为开放的项目
+     * - 已开通的项目必须是状态正常的
+     * - 使用 leftJoin 优化查询性能
+     * - 在数据库层面计算开通状态文本
+     *
+     * @param CoachUser $coach 技师对象
+     * @return array 项目列表,每个项目包含:
+     *        - id: int 项目ID
+     *        - title: string 项目标题
+     *        - subtitle: string|null 项目副标题
+     *        - cover: string 项目封面图
+     *        - price: decimal 项目价格
+     *        - original_price: decimal 原价
+     *        - sales: int 销量
+     *        - duration: int 服务时长(分钟)
+     *        - project_desc: string 项目描述
+     *        - service_desc: string 服务说明
+     *        - type: int 项目类型
+     *        - is_opened: bool 是否已开通
+     *        - opened_text: string 开通状态文本(已开通|未开通)
      */
-    public function getAvailableProjects(int $userId): array
+    public function getAvailableProjects(CoachUser $coach): array
     {
-        try {
-            // 加载用户和技师信息
-            $user = MemberUser::with(['coach', 'coach.projects'])->findOrFail($userId);
-            abort_if(! $user->coach, 404, '技师信息不存在');
-
-            // 获取所有可开通的项目
-            $projects = Project::where('state', ProjectStatus::OPEN->value)
-                ->select([
-                    'id',
-                    'title',
-                    'subtitle',
-                    'cover',
-                    'price',
-                    'original_price',
-                    'sales',
-                    'duration',
-                    'project_desc',
-                    'service_desc',
-                    'type',
-                ])
-                ->get();
-
-            // 记录日志
-            Log::info('获取可开通项目列表成功', [
-                'user_id' => $userId,
-                'coach_id' => $user->coach->id,
-                'project_count' => $projects->count(),
-            ]);
-
-            return [
-                'items' => $projects,
-                'total' => $projects->count(),
-            ];
-
-        } catch (\Exception $e) {
-            Log::error('获取可开通项目列表失败', [
-                'user_id' => $userId,
-                'error' => $e->getMessage(),
-                'file' => $e->getFile(),
-                'line' => $e->getLine(),
-            ]);
-            throw $e;
-        }
+        // 使用 leftJoin 优化查询,避免 N+1 问题
+        return Project::where('project.state', ProjectStatus::OPEN->value)
+            ->leftJoin('coach_project', function ($join) use ($coach) {
+                $join->on('project.id', '=', 'coach_project.project_id')
+                    ->where('coach_project.coach_id', '=', $coach->id)
+                    ->where('coach_project.state', '=', ProjectStatus::OPEN->value);
+            })
+            ->select([
+                'project.id',
+                'project.title',
+                'project.subtitle',
+                'project.cover',
+                'project.price',
+                'project.original_price',
+                'project.sales',
+                'project.duration',
+                'project.project_desc',
+                'project.service_desc',
+                'project.type',
+                DB::raw('CASE WHEN coach_project.id IS NOT NULL THEN true ELSE false END as is_opened'),
+                DB::raw('CASE WHEN coach_project.id IS NOT NULL THEN "已开通" ELSE "未开通" END as opened_text'),
+            ])
+            ->get()
+            ->toArray();
     }
 
     /**
-     * 技师开通项目
+     * 技师开通/关闭项目
      *
-     * @param  int  $userId  技师用户ID
-     * @param  array  $data  开通项目数据
+     * 业务流程:
+     * 1. 验证技师状态
+     * 2. 验证项目状态
+     * 3. 根据操作类型处理项目
+     * 4. 返回操作结果
+     *
+     * @param CoachUser $coach 技师对象
+     * @param array $data 项目操作数据,包含:
+     *        - project_id: int 项目ID
+     *        - action: string 操作类型(open:开通 close:关闭)
+     * @return array 操作结果,包含:
+     *        - project_id: int 项目ID
+     *        - project_name: string 项目名称
+     *        - state: int 项目状态
+     *        - state_text: string 状态描述
+     * @throws \Illuminate\Http\Exceptions\HttpResponseException 当验证失败时抛出异常
      */
-    public function openProject(int $userId, array $data): array
+    public function openProject(CoachUser $coach, array $data): array
     {
-        return DB::transaction(function () use ($userId, $data) {
-            try {
-                // 加载用户和技师信息
-                $user = MemberUser::with(['coach', 'coach.projects'])->findOrFail($userId);
-                abort_if(! $user->coach, 404, '技师信息不存在');
+        return DB::transaction(function () use ($coach, $data) {
+            // 检查技师状态是否正常
+            $coach->validateActiveStatus('技师状态异常,无法操作项目');
 
-                // 验证项目是否存在且状态正常
-                $project = Project::where('id', $data['project_id'])
-                    ->where('state', ProjectStatus::OPEN->value)
-                    ->first();
-                abort_if(! $project, 404, '项目不存在或已下架');
-
-                // 检查是否已开通该项目
-                $existingProject = $user->coach->projects()
-                    ->where('project_id', $data['project_id'])
-                    ->first();
-                abort_if($existingProject, 422, '已开通该项目');
+            // 验证项目是否存在且状态为开放
+            $project = Project::validateOpenProject($data['project_id']);
 
-                // 检查技师状态
-                abort_if($user->coach->state !== TechnicianStatus::ACTIVE->value,
-                    422, '技师状态异常,无法开通项目');
+            // 根据操作类型处理项目并获取状态
+            $state = match ($data['action']) {
+                'open' => $this->handleProjectOpen($coach, $data['project_id']),
+                'close' => $this->handleProjectClose($coach, $data['project_id']),
+            };
 
-                // 创建技师项目关联
-                $coachProject = $user->coach->projects()->create([
-                    'project_id' => $data['project_id'],
-                    'state' => ProjectStatus::OPEN->value,
-                    'discount_amount' => 0.00,
-                    'service_gender' => 0,
-                    'service_distance' => 0,
-                    'traffic_fee_type' => 2,
-                    'traffic_fee' => 0,
-                    'created_at' => now(),
-                    'updated_at' => now(),
-                ]);
-
-                // 记录日志
-                Log::info('技师开通项目成功', [
-                    'user_id' => $userId,
-                    'coach_id' => $user->coach->id,
-                    'project_id' => $data['project_id'],
-                    'project_name' => $project->name,
-                ]);
+            // 返回格式化的操作结果
+            return $project->formatActionResult($state);
+        });
+    }
 
-                return [
-                    'message' => '项目开通成功',
-                    'project_id' => $project->id,
-                    'project_name' => $project->name,
-                ];
+    /**
+     * 处理项目开通
+     *
+     * @param CoachUser $coach 技师对象
+     * @param int $projectId 项目ID
+     * @return int 返回开通后的状态值
+     */
+    private function handleProjectOpen(CoachUser $coach, int $projectId): int
+    {
+        // 创建或更新项目关联为开通状态
+        $coach->updateOrCreateProjectRelation($projectId, ProjectStatus::OPEN->value);
+        return ProjectStatus::OPEN->value;
+    }
 
-            } catch (\Exception $e) {
-                Log::error('技师开通项目失败', [
-                    'user_id' => $userId,
-                    'data' => $data,
-                    'error' => $e->getMessage(),
-                    'file' => $e->getFile(),
-                    'line' => $e->getLine(),
-                ]);
-                throw $e;
-            }
-        });
+    /**
+     * 处理项目关闭
+     *
+     * @param CoachUser $coach 技师对象
+     * @param int $projectId 项目ID
+     * @return int 返回关闭后的状态值
+     */
+    private function handleProjectClose(CoachUser $coach, int $projectId): int
+    {
+        // 创建或更新项目关联为关闭状态
+        $coach->updateOrCreateProjectRelation($projectId, ProjectStatus::CLOSE->value);
+        return ProjectStatus::CLOSE->value;
     }
 
     /**
@@ -170,7 +175,7 @@ class ProjectService
                     abort_if(! $voucherSettingItem, 404, '代金卷设置不存在');
 
                     // 从项目设置中验证代金卷金额的合理范围
-                    abort_if($data['voucher'] > $voucherSettingItem->max_value, 422, '代金卷金额不能超过'.$voucherSettingItem->max_value.'元');
+                    abort_if($data['voucher'] > $voucherSettingItem->max_value, 422, '代金卷金额不能超过' . $voucherSettingItem->max_value . '元');
 
                     // 更新技师折扣金额配置
                     $user->coach->settingValues()->updateOrCreate(
@@ -187,7 +192,7 @@ class ProjectService
                     abort_if(! $trafficFeeSettingItem, 404, '路费设置不存在');
 
                     // 从项目设置中验证交通费类型的合理范围
-                    abort_if($data['traffic_fee'] > $trafficFeeSettingItem->max_value, 422, '交通费类型不能超过'.$trafficFeeSettingItem->max_value.'元');
+                    abort_if($data['traffic_fee'] > $trafficFeeSettingItem->max_value, 422, '交通费类型不能超过' . $trafficFeeSettingItem->max_value . '元');
 
                     // 更新技师路费配置
                     $user->coach->settingValues()->updateOrCreate(
@@ -204,7 +209,7 @@ class ProjectService
                     abort_if(! $customerGenderSettingItem, 404, '顾客性别设置不存在');
 
                     // 从项目设置中验证顾客性别设置的合理范围
-                    abort_if($data['gender'] > $customerGenderSettingItem->max_value, 422, '顾客性别设置不能超过'.$customerGenderSettingItem->max_value.'元');
+                    abort_if($data['gender'] > $customerGenderSettingItem->max_value, 422, '顾客性别设置不能超过' . $customerGenderSettingItem->max_value . '元');
 
                     // 更新技师顾客性别设置
                     $user->coach->settingValues()->updateOrCreate(
@@ -228,7 +233,6 @@ class ProjectService
                     'coach_id' => $user->coach->id,
                     'settings' => $data,
                 ];
-
             } catch (\Exception $e) {
                 Log::error('技师项目设置更新失败', [
                     'user_id' => $userId,