Browse Source

fixed:用户端-优化申请技师

刘学玺 3 months ago
parent
commit
dbf09962aa

+ 28 - 0
app/DTOs/CoachApplicationDTO.php

@@ -0,0 +1,28 @@
+<?php
+namespace App\DTOs;
+
+class CoachApplicationDTO
+{
+    public function __construct(
+        public readonly int $age,
+        public readonly string $mobile,
+        public readonly int $gender,
+        public readonly string $workYears,
+        public readonly string $intentionCity,
+        public readonly array $lifePhotos,
+        public readonly ?string $introduction = null,
+    ) {}
+
+    public static function fromRequest(array $data): self
+    {
+        return new self(
+            age: $data['age'],
+            mobile: $data['mobile'],
+            gender: $data['gender'],
+            workYears: $data['work_years'],
+            intentionCity: $data['intention_city'],
+            lifePhotos: $data['life_photos'],
+            introduction: $data['introduction'] ?? null,
+        );
+    }
+}

+ 37 - 38
app/Http/Controllers/Client/UserController.php

@@ -2,14 +2,15 @@
 
 namespace App\Http\Controllers\Client;
 
+use App\DTOs\CoachApplicationDTO;
 use App\Http\Controllers\Controller;
-use App\Http\Requests\Client\User\ApplyCoachRequest;
-use App\Http\Requests\Client\User\FeedbackRequest;
-use App\Http\Requests\Client\User\RegisterRequest;
-use App\Http\Requests\Client\User\UpdateRequest;
-use App\Http\Resources\Client\UserResource;
 use App\Services\Client\UserService;
 use Illuminate\Support\Facades\Auth;
+use App\Http\Resources\Client\UserResource;
+use App\Http\Requests\Client\User\UpdateRequest;
+use App\Http\Requests\Client\User\FeedbackRequest;
+use App\Http\Requests\Client\User\RegisterRequest;
+use App\Http\Requests\Client\User\ApplyCoachRequest;
 
 /**
  * @group 用户端
@@ -83,7 +84,7 @@ class UserController extends Controller
      *   "code": 422,
      *   "message": "验证失败",
      *   "errors": {
-     *     "mobile": ["手机号���格式不正确"],
+     *     "mobile": ["手机号格式不正确"],
      *     "code": ["验证码错误"]
      *   }
      * }
@@ -188,18 +189,31 @@ class UserController extends Controller
      *
      * @description 普通用户申请成为平台技师,提交技师申请信息
      *
-     * @authenticated
+     * 业务流程:
+     * 1. 检查用户申请资格
+     * 2. 创建或更新技师基础信息
+     * 3. 创建申请记录
+     *
+     * 注意事项:
+     * - 同一时间只能有一条待审核申请
+     * - 已是技师的用户不能重复申请
+     * - 生活照片支持任意格式的图片数据
+     * - 年龄必须在18-60岁之间
+     * - 工作年限不能超过实际年龄
+     *
+     * @authenticated 需要用户身份认证
      *
      * @bodyParam age integer required 年龄(18-60岁) Example: 25
      * @bodyParam mobile string required 联系电话 Example: 13800138000
      * @bodyParam gender integer required 性别(1:男/2:女) Example: 1
      * @bodyParam work_years integer required 工作年限(0-50年) Example: 5
      * @bodyParam intention_city string required 意向城市 Example: 杭州
-     * @bodyParam portrait_images array required 形象照片(最多6张) Example: ["https://example.com/portrait1.jpg"]
+     * @bodyParam life_photos array required 生活照片(最多6张)
+     * @bodyParam life_photos.* string required 生活照片 Example: base64或其他格式的图片数据
      * @bodyParam introduction string optional 个人简介(最多1000字) Example: 专业按摩师,有多年经验
      *
      * @response 200 {
-     *   "code": 200,
+     *   "status": true,
      *   "message": "申请提交成功",
      *   "data": {
      *     "id": 1,
@@ -209,54 +223,39 @@ class UserController extends Controller
      *     "gender": 1,
      *     "work_years": 5,
      *     "intention_city": "杭州",
-     *     "portrait_images": ["https://example.com/portrait1.jpg"],
+     *     "life_photos": [
+     *         "base64或其他格式的图片数据1",
+     *         "base64或其他格式的图片数据2"
+     *     ],
      *     "introduction": "专业按摩师,有多年经验",
      *     "state": "auditing",
      *     "created_at": "2024-03-20 10:00:00",
      *     "updated_at": "2024-03-20 10:00:00"
      *   }
      * }
-     * @response 401 {
-     *   "code": 401,
-     *   "message": "请先登录",
-     *   "data": null
+     * @response 422 {
+     *   "message": "您已是技师,无需重复申请"
      * }
      * @response 422 {
-     *   "code": 422,
-     *   "message": "验证失败",
+     *   "message": "您有正在审核的申请,请耐心等待"
+     * }
+     * @response 422 {
+     *   "message": "验证错误",
      *   "errors": {
      *     "mobile": ["手机号码格式不正确"],
      *     "gender": ["性别只能是1(男)或2(女)"],
      *     "work_years": ["工作年限必须是0-50之间的整数"],
      *     "intention_city": ["意向城市不能为空"],
-     *     "portrait_images": ["形象照片不能为空"],
-     *     "portrait_images.*": ["图片必须是有效的URL地址"]
+     *     "life_photos": ["生活照片不能为空"],
+     *     "life_photos.*": ["图片格式不正确"]
      *   }
      * }
-     * @response 422 {
-     *   "code": 422,
-     *   "message": "您已是技师,无需重复申请",
-     *   "data": null
-     * }
-     * @response 422 {
-     *   "code": 422,
-     *   "message": "您有正在审核的申请,请耐心等待",
-     *   "data": null
-     * }
      */
     public function applyCoach(ApplyCoachRequest $request)
     {
-        $validated = $request->validated();
+        $dto = CoachApplicationDTO::fromRequest($request->validated());
 
-        $result = $this->service->applyCoach(
-            $validated['age'],
-            $validated['mobile'],
-            $validated['gender'],
-            $validated['work_years'],
-            $validated['intention_city'],
-            $validated['portrait_images'],
-            $validated['introduction'] ?? null
-        );
+        $result = $this->service->applyCoach($dto);
 
         return $this->success($result, '申请提交成功');
     }

+ 18 - 10
app/Http/Requests/Client/User/ApplyCoachRequest.php

@@ -17,12 +17,12 @@ class ApplyCoachRequest extends FormRequest
             'age' => [
                 'required',
                 'integer',
-                'min:18',
-                'max:60',
+                'between:18,60',
             ],
             'mobile' => [
                 'required',
                 'string',
+                'size:11',
                 'regex:/^1[3-9]\d{9}$/',
             ],
             'gender' => [
@@ -41,12 +41,17 @@ class ApplyCoachRequest extends FormRequest
                 'string',
                 'max:50',
             ],
-            'portrait_images' => [
+            'life_photos' => [
                 'required',
                 'array',
                 'min:1',
                 'max:6',
             ],
+            'life_photos.*' => [
+                'required',
+                'string',
+                'max:2048',  // 限制单张图片大小
+            ],
             'introduction' => [
                 'nullable',
                 'string',
@@ -60,9 +65,9 @@ class ApplyCoachRequest extends FormRequest
         return [
             'age.required' => '年龄不能为空',
             'age.integer' => '年龄必须是整数',
-            'age.min' => '年龄不能小于18岁',
-            'age.max' => '年龄不能大于60岁',
+            'age.between' => '年龄必须在18-60岁之间',
             'mobile.required' => '手机号不能为空',
+            'mobile.size' => '手机号必须是11位',
             'mobile.regex' => '手机号格式不正确',
             'gender.required' => '性别不能为空',
             'gender.in' => '性别只能是1(男)或2(女)',
@@ -72,11 +77,14 @@ class ApplyCoachRequest extends FormRequest
             'work_years.max' => '工作年限不能超过50年',
             'intention_city.required' => '意向城市不能为空',
             'intention_city.max' => '意向城市不能超过50个字符',
-            'portrait_images.required' => '形象照片不能为空',
-            'portrait_images.array' => '形象照片必须是数组',
-            'portrait_images.min' => '至少上传1张形象照片',
-            'portrait_images.max' => '最多上传6张形象照片',
-            'introduction.max' => '个人简介不能超过1000个字符',
+            'life_photos.required' => '生活照片不能为空',
+            'life_photos.array' => '生活照片必须是数组',
+            'life_photos.min' => '至少上传1张生活照片',
+            'life_photos.max' => '最多上传6张生活照片',
+            'life_photos.*.required' => '生活照片不能为空',
+            'life_photos.*.string' => '生活照片格式不正确',
+            'life_photos.*.max' => '单张照片大小不能超过2MB',
+            'introduction.max' => '个人简介不能超过1000字',
         ];
     }
 }

+ 117 - 60
app/Services/Client/UserService.php

@@ -2,14 +2,15 @@
 
 namespace App\Services\Client;
 
-use App\Enums\TechnicianAuthStatus;
-use App\Enums\TechnicianStatus;
-use App\Models\CoachInfoRecord;
 use App\Models\CoachUser;
 use App\Models\MemberUser;
-use Illuminate\Support\Facades\Auth;
+use App\Enums\TechnicianStatus;
+use App\Models\CoachInfoRecord;
+use App\DTOs\CoachApplicationDTO;
 use Illuminate\Support\Facades\DB;
+use App\Enums\TechnicianAuthStatus;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Auth;
 use Illuminate\Support\Facades\Redis;
 use SimpleSoftwareIO\QrCode\Facades\QrCode;
 
@@ -42,7 +43,7 @@ class UserService
      *
      * @return MemberUser 返回用户信息
      *
-     * @throws \Exception 获取用户信息失败时抛出异���
+     * @throws \Exception 获取用户信息失败时抛出异
      */
     public function getUserInfo(): MemberUser
     {
@@ -84,7 +85,7 @@ class UserService
         try {
             DB::beginTransaction();
 
-            // 检查手机号是否已��
+            // 检查手机号是否已
             abort_if(MemberUser::where('mobile', $mobile)->exists(), 422, '该手机号已注册');
 
             // 验证短信验证码
@@ -257,78 +258,76 @@ class UserService
      * 申请成为技师
      *
      * 业务逻辑:
-     * 1. 检查户申请资格
+     * 1. 检查户申请资格
      * 2. 创建或更新技师基础信息
      * 3. 创建申请记录
+     * 4. 更新关联关系
      *
-     * @param  int  $age  年龄
-     * @param  string  $mobile  联系电话
-     * @param  int  $gender  性别(1:男/2:女)
-     * @param  string  $work_years  工作年限
-     * @param  string  $intention_city  意向市
-     * @param  array  $portrait_images  形象照片数组
-     * @param  string|null  $introduction  个人简介
-     * @return \App\Models\CoachInfoRecord 返回申请记录
-     *
+     * @param CoachApplicationDTO $data 申请数据传输对象
+     * @return CoachInfoRecord 返回申请记录
      * @throws \Exception 申请失败时抛出异常
      */
-    public function applyCoach(
-        int $age,
-        string $mobile,
-        int $gender,
-        string $work_years,
-        string $intention_city,
-        array $portrait_images,
-        ?string $introduction = null
-    ): CoachInfoRecord {
-        try {
-            DB::beginTransaction();
-
+    public function applyCoach(CoachApplicationDTO $data): CoachInfoRecord
+    {
+        return DB::transaction(function () use ($data) {
             // 检查申请资格
             $this->checkCoachApplicationEligibility();
 
+            // 验证业务规则
+            $this->validateBusinessRules($data);
+
+            // 获取当前用户
             /** @var MemberUser $user */
             $user = Auth::user();
 
-            // 获取用户技师身份
-            $coach = $user->coach;
-            // 如果用户不存在技师身份
-            if(!$coach){
-                // 创建技师用户记录
-                $coach = CoachUser::create([
-                    'user_id' => $user->id,
-                    'state' => TechnicianStatus::PENDING->value,
-                ]);
-            }
+            // 创建或获取技师用户记录
+            $coach = $this->getOrCreateCoachUser($user);
 
             // 创建技师信息记录
-            $infoRecord = CoachInfoRecord::create([
-                'coach_id' => $coach->id,
-                'age' => $age,
-                'mobile' => $mobile,
-                'gender' => $gender,
-                'work_years' => $work_years,
-                'intention_city' => $intention_city,
-                'portrait_images' => $portrait_images,
-                'introduction' => $introduction,
-                'state' => TechnicianAuthStatus::AUDITING->value,
-            ]);
+            $infoRecord = $this->createCoachInfoRecord($coach, $data);
 
             // 更新技师用户记录的信息记录ID
             $coach->update(['info_record_id' => $infoRecord->id]);
 
-            DB::commit();
-
             return $infoRecord;
-        } catch (\Exception $e) {
-            DB::rollBack();
-            Log::error('申请成为技师失败', [
-                'error' => $e->getMessage(),
-                'user_id' => Auth::id(),
-                'mobile' => $mobile,
-            ]);
-            throw $e;
-        }
+        });
+    }
+
+    /**
+     * 验证技师申请业务规则
+     *
+     * 验证规则:
+     * 1. 工作年限不能超过实际工作可能年限(年龄-18)
+     * 2. 生活照片数量验证(1-6张)
+     * 3. 手机号格式统一验证
+     *
+     * @param CoachApplicationDTO $data 申请数据
+     * @throws \Exception 验证失败时抛出异常
+     */
+    private function validateBusinessRules(CoachApplicationDTO $data): void
+    {
+        // 验证工作年限合理性
+        $maxPossibleWorkYears = $data->age - 18; // 实际可能的最大工作年限
+        abort_if(
+            $data->workYears > $maxPossibleWorkYears,
+            422,
+            sprintf('工作年限不能超过%d年(当前年龄%d岁)', $maxPossibleWorkYears, $data->age)
+        );
+
+        // 验证生活照片数量
+        $photoCount = count($data->lifePhotos);
+        abort_if(
+            $photoCount < 1 || $photoCount > 6,
+            422,
+            '生活照片数量必须在1-6张之间'
+        );
+
+        // 验证手机号格式
+        abort_if(
+            !preg_match('/^1[3-9]\d{9}$/', $data->mobile),
+            422,
+            '手机号格式不正确'
+        );
     }
 
     /**
@@ -521,4 +520,62 @@ class UserService
             throw $e;
         }
     }
+
+    /**
+     * 创建或获取技师用户记录
+     *
+     * @param MemberUser $user 用户对象
+     * @return CoachUser 技师用户记录
+     */
+    private function getOrCreateCoachUser(MemberUser $user): CoachUser
+    {
+        // 获取用户技师身份
+        $coach = $user->coach;
+
+        // 如果用户不存在技师身份,创建新记录
+        if (!$coach) {
+            $coach = CoachUser::create([
+                'user_id' => $user->id,
+                'state' => TechnicianStatus::PENDING->value,
+            ]);
+        }
+
+        return $coach;
+    }
+
+    /**
+     * 创建技师信息记录
+     *
+     * @param CoachUser $coach 技师用户对象
+     * @param CoachApplicationDTO $data 技师信息数据
+     * @return CoachInfoRecord 技师信息记录
+     */
+    private function createCoachInfoRecord(CoachUser $coach, CoachApplicationDTO $data): CoachInfoRecord
+    {
+        // 定义允许的字段和默认值
+        $allowedFields = [
+            'age' => $data->age,
+            'mobile' => $data->mobile,
+            'gender' => $data->gender,
+            'work_years' => $data->workYears,
+            'intention_city' => $data->intentionCity,
+            'life_photos' => json_encode($data->lifePhotos),  // 将数组转换为JSON字符串
+            'introduction' => $data->introduction,
+            'coach_id' => $coach->id,
+            'state' => TechnicianAuthStatus::AUDITING->value,
+        ];
+
+        // 过滤空值,但保留必填字段
+        $requiredFields = ['age', 'mobile', 'gender', 'work_years', 'intention_city', 'life_photos', 'coach_id', 'state'];
+
+        $recordData = array_filter(
+            $allowedFields,
+            function ($value, $key) use ($requiredFields) {
+                return !is_null($value) || in_array($key, $requiredFields);
+            },
+            ARRAY_FILTER_USE_BOTH
+        );
+
+        return CoachInfoRecord::create($recordData);
+    }
 }