Browse Source

fixed:技师端-优化资质认证

刘学玺 4 months ago
parent
commit
13af0c3616

+ 13 - 14
app/Http/Controllers/Coach/AccountController.php

@@ -2,15 +2,15 @@
 
 namespace App\Http\Controllers\Coach;
 
-use App\Enums\TechnicianLocationType;
+use Illuminate\Http\Request;
+use App\Traits\ResponseTrait;
 use App\Http\Controllers\Controller;
+use Illuminate\Support\Facades\Auth;
+use App\Enums\TechnicianLocationType;
+use App\Services\Coach\AccountService;
 use App\Http\Requests\Coach\SubmitBaseInfoRequest;
-use App\Http\Requests\Coach\SubmitQualificationRequest;
 use App\Http\Requests\Coach\SubmitRealNameRequest;
-use App\Services\Coach\AccountService;
-use App\Traits\ResponseTrait;
-use Illuminate\Http\Request;
-use Illuminate\Support\Facades\Auth;
+use App\Http\Requests\Coach\SubmitQualificationRequest;
 
 /**
  * @group 技师端
@@ -58,15 +58,14 @@ class AccountController extends Controller
     /**
      * [账户]提交资质信息
      *
-     * @description 提交技师的资质认证信息
+     * @description 提交技师的资质认证信息,包括资质照片、营业执照和健康证
      *
      * @authenticated
      *
      * @bodyParam qual_type string required 资质类型(按摩师/理疗师等) Example: 高级按摩师
-     * @bodyParam qual_no string required 资质证书编号(5-50个字符) Example: XZ2024001
-     * @bodyParam qual_photo string required 资质证书照片 Example: http://example.com/cert.jpg
-     * @bodyParam valid_start date required 有效期开始日期 Example: 2024-01-01
-     * @bodyParam valid_end date required 有效期结束日期(必须大于开始日期) Example: 2029-01-01
+     * @bodyParam qual_photo string required 资质证书照片 Example: base64或其他格式的图片数据
+     * @bodyParam business_license string required 营业执照照片 Example: base64或其他格式的图片数据
+     * @bodyParam health_cert string required 健康证照片 Example: base64或其他格式的图片数据
      *
      * @response {
      *  "message": "资质信息提交成功"
@@ -106,7 +105,7 @@ class AccountController extends Controller
     /**
      * [账户]获取技师信息
      *
-     * @description 取技师的基本信息、资质信息和实名信息
+     * @description ��取技师的基本信息、资质信息和实名信息
      *
      * @authenticated
      *
@@ -186,7 +185,7 @@ class AccountController extends Controller
     /**
      * [账户]获取技师位置信息
      *
-     * @description 取技师的当前位置和常用位置信息
+     * @description ��取技师的当前位置和常用位置信息
      *
      * @authenticated
      *
@@ -269,7 +268,7 @@ class AccountController extends Controller
             'time_ranges.min' => '至少设置一个时间段',
             'time_ranges.*.start_time.required' => '开始时间不能为空',
             'time_ranges.*.start_time.regex' => '开始时间格式错误,应为HH:mm格式',
-            'time_ranges.*.end_time.required' => '结时间不能为空',
+            'time_ranges.*.end_time.required' => '结���时间不能为空',
             'time_ranges.*.end_time.regex' => '结束时间格式错误,应为HH:mm格式',
         ]);
 

+ 12 - 13
app/Http/Requests/Coach/SubmitQualificationRequest.php

@@ -15,10 +15,9 @@ class SubmitQualificationRequest extends FormRequest
     {
         return [
             'qual_type' => 'required|string|max:50',
-            'qual_no' => 'required|string|min:5|max:50',
-            'qual_photo' => 'required|url|max:255',
-            'valid_start' => 'required|date|date_format:Y-m-d',
-            'valid_end' => 'required|date|date_format:Y-m-d|after:valid_start',
+            'qual_photo' => 'required|string|max:2048',
+            'business_license' => 'required|string|max:2048',
+            'health_cert' => 'required|string|max:2048',
         ];
     }
 
@@ -26,17 +25,17 @@ class SubmitQualificationRequest extends FormRequest
     {
         return [
             'qual_type.required' => '资质类型不能为空',
+            'qual_type.string' => '资质类型必须是字符串',
             'qual_type.max' => '资质类型不能超过50个字符',
-            'qual_no.required' => '资质证书编号不能为空',
-            'qual_no.min' => '资质证书编号不能少于5个字符',
-            'qual_no.max' => '资质证书编号不能超过50个字符',
             'qual_photo.required' => '资质证书照片不能为空',
-            'qual_photo.url' => '资质证书照片必须是有效的URL地址',
-            'valid_start.required' => '有效期开始日期不能为空',
-            'valid_start.date' => '有效期开始日期格式不正确',
-            'valid_end.required' => '有效期结束日期不能为空',
-            'valid_end.date' => '有效期结束日期格式不正确',
-            'valid_end.after' => '有效期结束日期必须大于开始日期',
+            'qual_photo.string' => '资质证书照片格式不正确',
+            'qual_photo.max' => '资质证书照片数据过大',
+            'business_license.required' => '营业执照照片不能为空',
+            'business_license.string' => '营业执照照片格式不正确',
+            'business_license.max' => '营业执照照片数据过大',
+            'health_cert.required' => '健康证照片不能为空',
+            'health_cert.string' => '健康证照片格式不正确',
+            'health_cert.max' => '健康证照片数据过大',
         ];
     }
 }

+ 144 - 74
app/Services/Coach/AccountService.php

@@ -2,16 +2,16 @@
 
 namespace App\Services\Coach;
 
-use App\Enums\OrderStatus;
-use App\Enums\TechnicianAuthStatus;
-use App\Enums\TechnicianLocationType;
-use App\Enums\TechnicianWorkStatus;
-use App\Models\CoachSchedule;
 use App\Models\CoachUser;
+use App\Enums\OrderStatus;
 use App\Models\MemberUser;
-use Illuminate\Support\Facades\Cache;
+use App\Models\CoachSchedule;
 use Illuminate\Support\Facades\DB;
+use App\Enums\TechnicianAuthStatus;
+use App\Enums\TechnicianWorkStatus;
 use Illuminate\Support\Facades\Log;
+use App\Enums\TechnicianLocationType;
+use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Redis;
 
 class AccountService
@@ -27,14 +27,11 @@ class AccountService
     {
         DB::beginTransaction();
         try {
-            $this->setTransactionConfig();
 
             abort_if(! $user->coach, 404, '技师信息不存在');
 
             // 检查是否有待审核的记录
-            $pendingRecord = $user->coach->infoRecords()
-                ->where('state', TechnicianAuthStatus::AUDITING->value)
-                ->exists();
+            $pendingRecord = $this->hasPendingRecord($user->coach, 'info');
             abort_if($pendingRecord, 422, '已有待审核的基本信息记录');
 
             // 创建技师信息
@@ -50,7 +47,6 @@ class AccountService
             $this->logInfo('技师提交基本信息成功', $user, $data);
 
             return ['message' => '基本信息提交成功'];
-
         } catch (\Exception $e) {
             DB::rollBack();
             $this->logError('提交技师基本信息失败', $user, $data, $e);
@@ -60,38 +56,72 @@ class AccountService
 
     /**
      * 提交技师资质信息
+     * 包括资质证书照片、营业执照和健康证照片的提交和审核
+     *
+     * 业务流程:
+     * 1. 验证技师信息存在性
+     * 2. 检查是否有待审核的记录
+     * 3. 创建新的资质审核记录
+     * 4. 清除相关缓存
+     *
+     * 注意事项:
+     * - 同一时间只能有一条待审核记录
+     * - 审核不通过可以重新提交
+     * - 所有图片数据不限制格式
+     *
+     * @param User $user 当前认证用户
+     * @param array $data 资质信息数据,包含:
+     *        - qual_type: string 资质类型(如:高级按摩师)
+     *        - qual_photo: string 资质证书照片
+     *        - business_license: string 营业执照照片
+     *        - health_cert: string 健康证照片
+     * @return array 返回结果,包含:
+     *        - message: string 提示信息
+     *        - data: array 详细数据
+     *            - record_id: int 记录ID
+     *            - state: int 状态值
+     *            - state_text: string 状态文本
+     * @throws \Exception 当验证失败或保存失败时抛出异常
      */
     public function submitQualification($user, array $data)
     {
+        // 开启数据库事务
         DB::beginTransaction();
         try {
-            $this->setTransactionConfig();
-
-            abort_if(! $user->coach, 404, '技师信息不存在');
-
-            // 检查是否有待审核的记录
-            $pendingRecord = $user->coach->qualRecords()
-                ->where('state', TechnicianAuthStatus::AUDITING->value)
-                ->exists();
-            abort_if($pendingRecord, 422, '已有待审核的资质信息记录');
-
-            // 创建资质信息
-            $record = $user->coach->qualRecords()->create(array_merge($data, [
-                'state' => TechnicianAuthStatus::AUDITING->value,
-            ]));
+            // 验证技师信息是否存在
+            abort_if(!$user->coach, 404, '技师信息不存在');
+
+            // 检查是否有待审核的记录,避免重复提交
+            $pendingRecord = $this->hasPendingRecord($user->coach, 'qual');
+            // abort_if($pendingRecord, 422, '已有待审核的资质信息记录');
+
+            // 创建新的资质审核记录,设置为待审核状态
+            $record = $user->coach->qualRecords()->create([
+                'qual_type' => $data['qual_type'],
+                'qual_photo' => $data['qual_photo'],
+                'business_license' => $data['business_license'],
+                'health_cert' => $data['health_cert'],
+                'state' => TechnicianAuthStatus::AUDITING->value
+            ]);
 
-            // 清除技师信息缓存
+            // 清除技师信息缓存,确保数据一致性
             $this->clearCoachCache($user->coach->id);
 
+            // 提交事务
             DB::commit();
 
-            $this->logInfo('技师提交资质信息成功', $user, $data);
-
-            return ['message' => '资质信息提交成功'];
-
+            // 返回成功结果
+            return [
+                'message' => '资质信息提交成功',
+                'data' => [
+                    'record_id' => $record->id,
+                    'state' => TechnicianAuthStatus::AUDITING->value,
+                    'state_text' => TechnicianAuthStatus::fromValue(TechnicianAuthStatus::AUDITING->value)->label(),
+                ]
+            ];
         } catch (\Exception $e) {
+            // 发生异常时回滚事务
             DB::rollBack();
-            $this->logError('提交技师资质信息失败', $user, $data, $e);
             throw $e;
         }
     }
@@ -103,14 +133,11 @@ class AccountService
     {
         DB::beginTransaction();
         try {
-            $this->setTransactionConfig();
 
             abort_if(! $user->coach, 404, '技师信息不存在');
 
             // 检查是否有待审核的记录
-            $pendingRecord = $user->coach->realRecords()
-                ->where('state', TechnicianAuthStatus::AUDITING->value)
-                ->exists();
+            $pendingRecord = $this->hasPendingRecord($user->coach, 'real');
             abort_if($pendingRecord, 422, '已有待审核的实名认证信息');
 
             // 创建实名认证信息
@@ -126,7 +153,6 @@ class AccountService
             $this->logInfo('技师提交实名认证信息成功', $user, $this->maskSensitiveData($data));
 
             return ['message' => '实名认证信息提交成功'];
-
         } catch (\Exception $e) {
             DB::rollBack();
             $this->logError('提交实名认证信息失败', $user, $this->maskSensitiveData($data), $e);
@@ -144,7 +170,7 @@ class AccountService
             abort_if(! $user->coach, 404, '技师信息不存在');
 
             return Cache::remember(
-                self::CACHE_KEY_PREFIX.$user->coach->id,
+                self::CACHE_KEY_PREFIX . $user->coach->id,
                 self::CACHE_TTL,
                 function () use ($user) {
                     return $this->fetchCoachInfo($user->coach);
@@ -156,15 +182,6 @@ class AccountService
         }
     }
 
-    /**
-     * 设置事务配置
-     */
-    private function setTransactionConfig()
-    {
-        DB::statement('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED');
-        DB::statement('SET SESSION innodb_lock_wait_timeout=10');
-    }
-
     /**
      * 记录信息日志
      */
@@ -275,7 +292,7 @@ class AccountService
     }
 
     /**
-     * 身份证脱敏
+     * 身份证��脱敏
      */
     private function maskIdCard($idCard)
     {
@@ -302,7 +319,7 @@ class AccountService
      */
     private function clearCoachCache($coachId)
     {
-        Cache::forget(self::CACHE_KEY_PREFIX.$coachId);
+        Cache::forget(self::CACHE_KEY_PREFIX . $coachId);
     }
 
     /**
@@ -331,7 +348,7 @@ class AccountService
             }
 
             // 生成Redis键
-            $key = $coachId.'_'.$type;
+            $key = $coachId . '_' . $type;
 
             // 将置信息写入Redis
             $result = Redis::geoadd('coach_locations', $longitude, $latitude, $key);
@@ -356,7 +373,6 @@ class AccountService
             ]);
 
             return $result;
-
         } catch (\Exception $e) {
             DB::rollBack();
             Log::error('技师位置信息设置异常', [
@@ -402,7 +418,6 @@ class AccountService
             ]);
 
             return $result;
-
         } catch (\Exception $e) {
             Log::error('获取技师常用位置信息异常', [
                 'coach_id' => $user->coach->id ?? null,
@@ -466,7 +481,6 @@ class AccountService
                         'updated_at' => $schedule->updated_at->toDateTimeString(),
                     ],
                 ];
-
             } catch (\Exception $e) {
                 Log::error('技师排班设置败', [
                     'user_id' => $userId,
@@ -489,13 +503,19 @@ class AccountService
 
         // 验证每个时间段格式并转换为分钟数进行比较
         $ranges = collect($timeRanges)->map(function ($range) {
-            abort_if(! isset($range['start_time'], $range['end_time']),
-                400, '时间段格式错误');
+            abort_if(
+                ! isset($range['start_time'], $range['end_time']),
+                400,
+                '时间段格式错误'
+            );
 
             // 验证时间格式
             foreach (['start_time', 'end_time'] as $field) {
-                abort_if(! preg_match('/^([01][0-9]|2[0-3]):[0-5][0-9]$/', $range[$field]),
-                    400, '时间格式错,应为HH:mm格式');
+                abort_if(
+                    ! preg_match('/^([01][0-9]|2[0-3]):[0-5][0-9]$/', $range[$field]),
+                    400,
+                    '时间格式错,应为HH:mm格式'
+                );
             }
 
             // 转换为分钟数便于比较
@@ -503,8 +523,11 @@ class AccountService
             $endMinutes = $this->timeToMinutes($range['end_time']);
 
             // 验证时间先后
-            abort_if($startMinutes >= $endMinutes,
-                400, "时间段 {$range['start_time']}-{$range['end_time']} 结束时间必须大于开始时间");
+            abort_if(
+                $startMinutes >= $endMinutes,
+                400,
+                "时间段 {$range['start_time']}-{$range['end_time']} 结束时间必须大于开始时间"
+            );
 
             return [
                 'start_time' => $range['start_time'],
@@ -520,13 +543,16 @@ class AccountService
         $ranges->each(function ($range, $index) use ($ranges) {
             if ($index > 0) {
                 $prevRange = $ranges[$index - 1];
-                abort_if($range['start_minutes'] <= $prevRange['end_minutes'],
-                    400, "时间段 {$prevRange['start_time']}-{$prevRange['end_time']} 和 ".
-                         "{$range['start_time']}-{$range['end_time']} 之间存在重叠");
+                abort_if(
+                    $range['start_minutes'] <= $prevRange['end_minutes'],
+                    400,
+                    "时间段 {$prevRange['start_time']}-{$prevRange['end_time']} 和 " .
+                        "{$range['start_time']}-{$range['end_time']} 之间存在重叠"
+                );
             }
         });
 
-        // 返回排序后的时间,只保留需要的字段
+        // 返回排序后的时间,只保留需要的字段
         return $ranges->map(function ($range) {
             return [
                 'start_time' => $range['start_time'],
@@ -561,7 +587,6 @@ class AccountService
 
             // 清除相关的可预约时间段缓存
             $this->clearTimeSlotCache($coachId);
-
         } catch (\Exception $e) {
             Log::error('更新排班缓存失败', [
                 'coach_id' => $coachId,
@@ -645,7 +670,7 @@ class AccountService
                     TechnicianWorkStatus::FREE->value;
             }
 
-            // 如果状态没有变化,则不需要更新
+            // 如���状态没有变,则不需要更新
             if ($currentStatus === $newStatus) {
                 DB::rollBack();
 
@@ -687,7 +712,6 @@ class AccountService
                     'updated_at' => now()->toDateTimeString(),
                 ],
             ];
-
         } catch (\Exception $e) {
             DB::rollBack();
             Log::error('技师工作状态更新失败', [
@@ -707,18 +731,27 @@ class AccountService
     {
         // 验证基本信息认证
         $baseInfo = $coach->info;
-        abort_if(! $baseInfo || $baseInfo->state !== TechnicianAuthStatus::PASSED->value,
-            422, '基本信息未认证通过');
+        abort_if(
+            ! $baseInfo || $baseInfo->state !== TechnicianAuthStatus::PASSED->value,
+            422,
+            '基本信息未认证通过'
+        );
 
         // 验证资质认证
         $qualification = $coach->qual;
-        abort_if(! $qualification || $qualification->state !== TechnicianAuthStatus::PASSED->value,
-            422, '资质信息未认证通过');
+        abort_if(
+            ! $qualification || $qualification->state !== TechnicianAuthStatus::PASSED->value,
+            422,
+            '资质信息未认证通过'
+        );
 
         // 验证实名认证
         $realName = $coach->real;
-        abort_if(! $realName || $realName->state !== TechnicianAuthStatus::PASSED->value,
-            422, '实名信息未认证通过');
+        abort_if(
+            ! $realName || $realName->state !== TechnicianAuthStatus::PASSED->value,
+            422,
+            '实名信息未认证通过'
+        );
     }
 
     /**
@@ -784,7 +817,6 @@ class AccountService
             ];
 
             Redis::setex($cacheKey, 86400, json_encode($cacheData));
-
         } catch (\Exception $e) {
             Log::error('更新工作状态缓存失败', [
                 'coach_id' => $coachId,
@@ -815,7 +847,6 @@ class AccountService
                 'work_status_text' => TechnicianWorkStatus::fromValue($workStatus)->label(),
                 'updated_at' => now()->toDateTimeString(),
             ];
-
         } catch (\Exception $e) {
             Log::error('获取技师工作状态失败', [
                 'coach_id' => $coachId,
@@ -866,7 +897,6 @@ class AccountService
             ]);
 
             return $result;
-
         } catch (\Exception $e) {
             Log::error('获取技师排班信息失败', [
                 'user_id' => $userId,
@@ -876,4 +906,44 @@ class AccountService
             throw $e;
         }
     }
+
+    /**
+     * 验证技师基础信息
+     *
+     * @param User $user 用户对象
+     * @param bool $throwError 是否抛出异常
+     * @return bool
+     */
+    private function validateBasicCoach($user, bool $throwError = true): bool
+    {
+        if (!$user || !$user->coach) {
+            if ($throwError) {
+                abort_if(!$user, 404, '用户不存在');
+                abort_if(!$user->coach, 404, '技师信息不存在');
+            }
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 检查是否存在待审核记录
+     *
+     * @param CoachUser $coach 技师对象
+     * @param string $type 记录类型(info|qual|real)
+     * @return bool
+     */
+    private function hasPendingRecord($coach, string $type): bool
+    {
+        $method = match ($type) {
+            'info' => 'infoRecords',
+            'qual' => 'qualRecords',
+            'real' => 'realRecords',
+            default => throw new \InvalidArgumentException('Invalid record type')
+        };
+
+        return $coach->{$method}()
+            ->where('state', TechnicianAuthStatus::AUDITING->value)
+            ->exists();
+    }
 }