Ver Fonte

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

景好勇win11 há 4 meses atrás
pai
commit
6bd0fce95c

+ 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, '申请提交成功');
     }

+ 74 - 12
app/Http/Controllers/Coach/AccountController.php

@@ -142,7 +142,7 @@ class AccountController extends Controller
     }
 
     /**
-     * [账户]获取技师信息
+     * [账户]获取技师认证信息
      *
      * @description 获取技师的基本信息、资质信息和实名认证信息
      *
@@ -247,26 +247,34 @@ class AccountController extends Controller
     /**
      * [账户]获取技师位置信息
      *
-     * @description 取技师的当前位置和常用位置信息
+     * @description 取技师的位置信息
      *
      * @authenticated
      *
+     * @queryParam type int 位置类型(1:当前位置 2:常用位置) Example: 2
+     *
      * @response {
      *   "data": {
-     *     "current": {
-     *       "address": "北京市朝阳区建国路93号万达广场"
-     *     },
-     *     "common": {
-     *       "address": "北京市海淀区中关村大街1号"
-     *     }
+     *     "province": "浙江省",
+     *     "city": "杭州市",
+     *     "district": "西湖区",
+     *     "address": "文三路478号",
+     *     "adcode": "330106",
+     *     "longitude": 120.12345,
+     *     "latitude": 30.12345,
+     *     "updated_at": "2024-03-22 10:00:00"
      *   }
-     *idid
+     * }
      */
-    public function getLocation()
+    public function getLocation(Request $request)
     {
-        $result = $this->service->getLocation(Auth::user()->id);
+        $validated = $request->validate([
+            'type' => 'required|integer|in:1,2'
+        ]);
 
-        return $this->success($result);
+        return $this->success(
+            $this->service->getLocation(Auth::user(), $validated['type'])
+        );
     }
 
     /**
@@ -435,4 +443,58 @@ class AccountController extends Controller
 
         return $this->success($schedule);
     }
+
+    /**
+     * [账户]获取技师详细信息
+     *
+     * @description 获取当前登录技师的详细信息,包括基本信息、邀请码和钱包信息
+     *
+     * 业务流程:
+     * 1. 验证用户身份
+     * 2. 获取技师详细信息
+     * 3. 返回数据
+     *
+     * @authenticated 需要技师身份认证
+     *
+     * @response 200 {
+     *   "status": true,
+     *   "message": "获取成功",
+     *   "data": {
+     *     "coach_no": "00000001",
+     *     "mobile": "13800138000",
+     *     "nickname": "张三",
+     *     "avatar": "https://example.com/avatar.jpg",
+     *     "age": 25,
+     *     "gender": 1,
+     *     "work_years": 5,
+     *     "intention_city": "杭州",
+     *     "life_photos": [
+     *       "https://example.com/photo1.jpg",
+     *       "https://example.com/photo2.jpg"
+     *     ],
+     *     "introduction": "专业按摩师,从业5年",
+     *     "state": 1,
+     *     "state_text": "正常",
+     *     "invite_code": "C1",
+     *     "wallet": {
+     *       "balance": 1000,
+     *       "frozen": 200,
+     *       "total_income": 5000,
+     *       "today_income": 300,
+     *       "withdrawable": 800
+     *     }
+     *   }
+     * }
+     * @response 404 {
+     *   "message": "技师信息不存在"
+     * }
+     */
+    public function detail()
+    {
+        // 获取技师详细信息
+        $data = $this->service->getCoachDetail();
+
+        // 返回成功响应
+        return $this->success($data, '获取成功');
+    }
 }

+ 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字',
         ];
     }
 }

+ 1 - 0
app/Models/CoachInfoRecord.php

@@ -24,6 +24,7 @@ class CoachInfoRecord extends Model
      */
     protected $casts = [
         'portrait_images' => 'array',
+        'life_photos' => 'array'
     ];
 
     /**

+ 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);
+    }
 }

+ 191 - 49
app/Services/Coach/AccountService.php

@@ -7,10 +7,13 @@ use App\Enums\OrderStatus;
 use App\Models\MemberUser;
 use App\Models\CoachLocation;
 use App\Models\CoachSchedule;
+use App\Enums\TechnicianStatus;
+use App\Models\CoachInfoRecord;
 use Illuminate\Support\Facades\DB;
 use App\Enums\TechnicianAuthStatus;
 use App\Enums\TechnicianWorkStatus;
 use Illuminate\Support\Facades\Log;
+use Illuminate\Support\Facades\Auth;
 use App\Enums\TechnicianLocationType;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Redis;
@@ -59,9 +62,7 @@ class AccountService
      */
     public function submitBaseInfo($user, array $data)
     {
-        // 开启数据库事务,确保数据一致性
-        DB::beginTransaction();
-        try {
+        return DB::transaction(function () use ($user, $data) {
             // 验证技师信息是否存在,不存在则抛出404异常
             abort_if(!$user->coach, 404, '技师信息不存在');
 
@@ -87,18 +88,8 @@ class AccountService
             // 避免用户获取到旧的缓存数据
             $this->clearCoachCache($user->coach->id);
 
-            // 提交事务,确保所有操作成功
-            DB::commit();
-
-            // 返回成功结果
             return ['message' => '基本信息提交成功'];
-        } catch (\Exception $e) {
-            // 发生异常时回滚事务,确保数据一致性
-            DB::rollBack();
-
-            // 向上抛出异常,由调用方处理
-            throw $e;
-        }
+        });
     }
 
     /**
@@ -304,7 +295,7 @@ class AccountService
         return [
             'nickname' => $info->nickname,
             'avatar' => $info->avatar,  // 支持任意格式的图片数据
-            'life_photos' => json_decode($info->life_photos, true) ?? [],  // 生活照片数组
+            'life_photos' => $info->life_photos ?? [],  // 生活照片数组
             'gender' => $info->gender,
             'mobile' => $this->maskMobile($info->mobile),  // 手机号脱敏处理
             'birthday' => $info->birthday,
@@ -411,7 +402,7 @@ class AccountService
      * 1. 验证经纬度参数
      * 2. 验证位置类型
      * 3. 保存到Redis的地理位置数据结构
-     * 4. 同步保存到据库
+     * 4. 同步保存到据库
      *
      * @param int $coachId 技师ID
      * @param float $latitude 纬度
@@ -428,7 +419,7 @@ class AccountService
      */
     public function setLocation($coachId, $latitude, $longitude, $type = TechnicianLocationType::COMMON->value, array $locationInfo = [])
     {
-        // 使用事务确保据一致性
+        // 使用事务确保据一致性
         return DB::transaction(function () use ($coachId, $latitude, $longitude, $type, $locationInfo) {
             // 验证经纬度的有效性(-90≤纬度≤90,-180≤经度≤180)
             $this->validateCoordinates($latitude, $longitude);
@@ -460,42 +451,77 @@ class AccountService
     /**
      * 获取技师位置信息
      *
-     * @param  int  $userId  用户ID
-     * @return array 位置信息
+     * 业务流程:
+     * 1. 获取指定类型的位置记录
+     * 2. 返回格式化的位置数据
+     *
+     * @param User $user 认证用户
+     * @param int $type 位置类型 (1:当前位置 2:常用位置)
+     * @return array 位置信息,包含:
+     *        - province: string 省份
+     *        - city: string 城市
+     *        - district: string 区县
+     *        - address: string 详细地址
+     *        - adcode: string 行政区划代码
+     *        - longitude: float 经度
+     *        - latitude: float 纬度
+     *        - updated_at: string 更新时间
      */
-    public function getLocation($userId)
+    public function getLocation($user, int $type): array
     {
-        try {
-            // 改进:直接使用 coach 模型
-            $user = MemberUser::find($userId);
-            abort_if(! $user, 404, '用户不存在');
-            abort_if(! $user->coach, 404, '技师信息不存在');
-
-            // 获取常用位置信息
-            $location = $user->coach->locations()
-                ->where('type', TechnicianLocationType::COMMON->value)
-                ->first();
+        // 获取指定类型的位置记录
+        $location = $this->getLocationByType($user->coach, $type);
 
-            $result = [
-                'address' => $location ? $location->location : null,
-            ];
+        // 返回格式化的位置数据
+        return $this->formatLocationResponse($location);
+    }
 
-            // 记录日志
-            Log::info('获取技师常用位置信息成功', [
-                'coach_id' => $user->coach->id,
-                'location' => $result,
-            ]);
+    /**
+     * 根据类型获取技师位置记录
+     *
+     * @param CoachUser $coach 技师对象
+     * @param int $type 位置类型 (1:当前位置 2:常用位置)
+     * @return CoachLocation 位置信息
+     * @throws \Illuminate\Http\Exceptions\HttpResponseException 当位置信息不存在时抛出404异常
+     */
+    private function getLocationByType(CoachUser $coach, int $type): array
+    {
+        // 获取指定类型的位置记录
+        $location = $coach->locations()
+            ->where('type', $type)
+            ->first();
+        abort_if(!$location, 404, '位置信息不存在');
+
+        // 返回位置数据
+        return $location;
+    }
 
-            return $result;
-        } catch (\Exception $e) {
-            Log::error('获取技师常用位置信息异常', [
-                'coach_id' => $user->coach->id ?? null,
-                'error' => $e->getMessage(),
-                'file' => $e->getFile(),
-                'line' => $e->getLine(),
-            ]);
-            throw $e;
-        }
+    /**
+     * 格式化位置响应数据
+     *
+     * @param CoachLocation $location 位置记录
+     * @return array 格式化后的位置信息,包含:
+     *        - province: string 省份
+     *        - city: string 城市
+     *        - district: string 区县
+     *        - address: string 详细地址
+     *        - adcode: string 行政区划代码
+     *        - longitude: float 经度
+     *        - latitude: float 纬度
+     *        - updated_at: string 更新时间
+     */
+    private function formatLocationResponse(CoachLocation $location): array
+    {
+        return [
+            'province' => $location->province,    // 省份
+            'city' => $location->city,           // 城市
+            'district' => $location->district,    // 区县
+            'address' => $location->address,      // 详细地址
+            'adcode' => $location->adcode,        // 行政区划代码
+            'longitude' => $location->longitude,  // 经度
+            'latitude' => $location->latitude,    // 纬度
+            'updated_at' => $location->updated_at->toDateTimeString(), // 更新时间
+        ];
     }
 
     /**
@@ -939,7 +965,7 @@ class AccountService
             $coach = $user->coach;
             abort_if(! $coach, 404, '技师信息不存在');
 
-            // 先尝试缓存获取
+            // 先尝试缓存获取
             $cacheKey = "coach:schedule:{$coach->id}";
             $cached = Redis::get($cacheKey);
             if ($cached) {
@@ -1073,4 +1099,120 @@ class AccountService
         // 参数顺序:key longitude latitude member
         return Redis::geoadd('coach_locations', $longitude, $latitude, $key);
     }
+
+    /**
+     * 获取技师最新基本信息
+     *
+     * @param CoachUser $coach 技师对象
+     * @return CoachInfoRecord 最新的基本信息记录
+     * @throws \Illuminate\Http\Exceptions\HttpResponseException 当信息不存在时抛出404异常
+     */
+    private function getLatestBaseInfo(CoachUser $coach): CoachInfoRecord
+    {
+        // 获取最新的技师信息记录(排除审核拒绝的记录)
+        $latestInfo = $coach->infoRecords()
+            ->where('state', '<>', TechnicianAuthStatus::REJECTED->value)
+            ->latest()
+            ->first();
+
+        abort_if(!$latestInfo, 404, '技师基本信息不存在');
+
+        return $latestInfo;
+    }
+
+    /**
+     * 获取技师详细信息
+     *
+     * 业务逻辑:
+     * 1. 获取技师最新基本信息(不包括审核拒绝的记录)
+     * 2. 获取技师邀请码信息
+     * 3. 获取钱包信息
+     * 4. 组装返回数据
+     *
+     * @return array 技师详细信息
+     * @throws \Exception 获取信息失败时抛出异常
+     */
+    public function getCoachDetail(): array
+    {
+        // 获取当前登录用户
+        /** @var MemberUser $user */
+        $user = Auth::user();
+
+        // 获取用户的技师信息
+        $coach = $user->coach;
+        abort_if(!$coach, 404, '技师信息不存在');
+
+        // 获取最新的基本信息记录
+        $latestInfo = $this->getLatestBaseInfo($coach);
+
+        // 生成技师工号(例如:8位数字,不足前面补0)
+        $coachNo = str_pad($coach->id, 8, '0', STR_PAD_LEFT);
+
+        // 获取技师邀请码
+        $inviteCode = $this->generateInviteCode($coach->id);
+
+        // 获取钱包信息
+        $wallet = $this->getWalletInfo($coach->id);
+
+        // 获取基本信息
+        $baseInfo = $this->formatBaseInfo($latestInfo);
+
+        // 组装返回数据
+        return array_merge(
+            [
+                'coach_no' => $coachNo,
+                'invite_code' => $inviteCode,
+                'wallet' => $wallet,
+            ],
+            $baseInfo
+        );
+    }
+
+    /**
+     * 生成技师邀请码
+     *
+     * @param int $coachId 技师ID
+     * @return string 邀请码
+     */
+    private function generateInviteCode(int $coachId): string
+    {
+        return sprintf('C%d', $coachId);
+    }
+
+    /**
+     * 获取技师钱包信息
+     *
+     * 业务逻辑:
+     * 1. 获取技师钱包关联数据
+     * 2. 如果钱包不存在,返回默认值
+     * 3. 返回钱包概况数据
+     *
+     * @param int $coachId 技师ID
+     * @return array 钱包概况信息
+     */
+    private function getWalletInfo(int $coachId): array
+    {
+        // 获取技师对象及其钱包关联
+        $coach = CoachUser::with('wallet')->find($coachId);
+
+        // 如果钱包不存在,返回默认值
+        if (!$coach || !$coach->wallet) {
+            return [
+                'total_balance' => 0,         // 总余额
+                'available_balance' => 0,     // 可用余额
+                'frozen_amount' => 0,         // 冻结金额
+                'total_income' => 0,          // 累计收入
+                'total_expense' => 0,         // 累计支出
+            ];
+        }
+
+        // 返回钱包概况数据
+        return [
+            'total_balance' => $coach->wallet->total_balance ?? 0,           // 总余额
+            'available_balance' => $coach->wallet->available_balance ?? 0,   // 可用余额
+            'frozen_amount' => $coach->wallet->frozen_amount ?? 0,          // 冻结金额
+            'total_income' => $coach->wallet->total_income ?? 0,            // 累计收入
+            'total_expense' => $coach->wallet->total_expense ?? 0,          // 累计支出
+        ];
+    }
 }

+ 17 - 12
routes/api.php

@@ -1,23 +1,23 @@
 <?php
 
-use App\Http\Controllers\Client\AccountController;
+use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\EnumController;
+use App\Http\Controllers\ScribeController;
+use App\Http\Controllers\UploadController;
+use App\Http\Controllers\Client\UserController;
 use App\Http\Controllers\Client\CoachController;
-use App\Http\Controllers\Client\CoachLocationController;
-use App\Http\Controllers\Client\CommentController;
-use App\Http\Controllers\Client\MarketDistTeamController;
 use App\Http\Controllers\Client\OrderController;
-use App\Http\Controllers\Client\ProjectController;
-use App\Http\Controllers\Client\UserAddressController;
-use App\Http\Controllers\Client\UserController;
 use App\Http\Controllers\Client\WalletController;
 use App\Http\Controllers\Client\WechatController;
+use App\Http\Controllers\Client\AccountController;
+use App\Http\Controllers\Client\CommentController;
+use App\Http\Controllers\Client\ProjectController;
+use App\Http\Controllers\Client\UserAddressController;
+use App\Http\Controllers\Client\CoachLocationController;
+use App\Http\Controllers\Client\MarketDistTeamController;
 use App\Http\Controllers\Coach\OrderController as CoachOrderController;
-use App\Http\Controllers\Coach\ProjectController as CoachProjectController;
 use App\Http\Controllers\Coach\WalletController as CoachWalletController;
-use App\Http\Controllers\EnumController;
-use App\Http\Controllers\ScribeController;
-use App\Http\Controllers\UploadController;
-use Illuminate\Support\Facades\Route;
+use App\Http\Controllers\Coach\ProjectController as CoachProjectController;
 
 // API文档相关
 Route::get('scribe/update-token/{mobile}', [ScribeController::class, 'updateAuthToken']);
@@ -176,6 +176,8 @@ Route::middleware(['auth:sanctum', 'coach'])->prefix('coach')->group(function ()
         Route::post('qualification', [App\Http\Controllers\Coach\AccountController::class, 'submitQualification']);
         Route::post('real-name', [App\Http\Controllers\Coach\AccountController::class, 'submitRealName'])
             ->middleware('throttle:3,1');  // 实名认证限制更严格
+
+        // 获取技师认证详情
         Route::get('info', [App\Http\Controllers\Coach\AccountController::class, 'info']);
         // 设置位置信息
         Route::post('location', [App\Http\Controllers\Coach\AccountController::class, 'setLocation'])
@@ -190,6 +192,9 @@ Route::middleware(['auth:sanctum', 'coach'])->prefix('coach')->group(function ()
         Route::get('work-status', [App\Http\Controllers\Coach\AccountController::class, 'getWorkStatus']);
         // 获取技师排班信息
         Route::get('schedule', [App\Http\Controllers\Coach\AccountController::class, 'getSchedule']);
+
+        // 获取技师详情
+        Route::get('detail', [App\Http\Controllers\Coach\AccountController::class, 'detail'])->name('coach.detail');
     });
 
     // 订单相关路由