Browse Source

fixed:技师端-优化签到位置

刘学玺 3 months ago
parent
commit
b296bb27cb

+ 25 - 10
app/Http/Controllers/Coach/AccountController.php

@@ -4,10 +4,13 @@ namespace App\Http\Controllers\Coach;
 
 use Illuminate\Http\Request;
 use App\Traits\ResponseTrait;
+use App\Traits\LocationDataTrait;
+use Illuminate\Support\Facades\Log;
 use App\Http\Controllers\Controller;
 use Illuminate\Support\Facades\Auth;
 use App\Enums\TechnicianLocationType;
 use App\Services\Coach\AccountService;
+use App\Http\Requests\Coach\SetLocationRequest;
 use App\Http\Requests\Coach\SubmitBaseInfoRequest;
 use App\Http\Requests\Coach\SubmitRealNameRequest;
 use App\Http\Requests\Coach\SubmitQualificationRequest;
@@ -20,6 +23,7 @@ use App\Http\Requests\Coach\SubmitQualificationRequest;
 class AccountController extends Controller
 {
     use ResponseTrait;
+    use LocationDataTrait;
 
     protected AccountService $service;
 
@@ -166,25 +170,36 @@ class AccountController extends Controller
      *
      * @bodyParam latitude float required 纬度 Example: 39.9042
      * @bodyParam longitude float required 经度 Example: 116.4074
-     * @bodyParam type int 位置类型(1:当前位置 2:常用位置) Example: 2
+     * @bodyParam type int nullable 位置类型(1:当前位置 2:常用位置) Example: 2
+     * @bodyParam province string nullable 省份 Example: 北京市
+     * @bodyParam city string nullable 城市 Example: 北京市
+     * @bodyParam district string nullable 区县 Example: 朝阳区
+     * @bodyParam address string nullable 详细地址 Example: 建国路93号万达广场
+     * @bodyParam adcode string nullable 行政区划代码 Example: 110105
      *
      * @response {
      *   "message": "位置信息设置成功"
      * }
      */
-    public function setLocation(Request $request)
+    public function setLocation(SetLocationRequest $request)
     {
-        $validated = $request->validate([
-            'latitude' => 'required|numeric|between:-90,90',
-            'longitude' => 'required|numeric|between:-180,180',
-            'type' => 'sometimes|integer|in:1,2',
-        ]);
+        // 获取验证后的数据
+        $validated = $request->validated();
+
+        // 确保用户和技师存在
+        $user = Auth::user();
+        abort_if(!$user->coach, 404, '技师信息不存在');
+
+        // 提取位置信息
+        $locationInfo = $this->extractLocationInfo($validated);
 
-        $result = $this->service->setLocation(
-            Auth::user()->coach->id,
+        // 传递技师ID给服务层
+        $this->service->setLocation(
+            $user->coach->id,
             $validated['latitude'],
             $validated['longitude'],
-            $validated['type'] ?? TechnicianLocationType::COMMON->value
+            $validated['type'] ?? TechnicianLocationType::COMMON->value,
+            $locationInfo
         );
 
         return $this->success(['message' => '位置信息设置成功']);

+ 41 - 0
app/Http/Requests/Coach/SetLocationRequest.php

@@ -0,0 +1,41 @@
+<?php
+
+namespace App\Http\Requests\Coach;
+
+use Illuminate\Foundation\Http\FormRequest;
+
+class SetLocationRequest extends FormRequest
+{
+    public function rules()
+    {
+        return [
+            'latitude' => 'required|numeric|between:-90,90',
+            'longitude' => 'required|numeric|between:-180,180',
+            'type' => 'nullable|integer|in:1,2',
+            'province' => 'nullable|string|max:50',
+            'city' => 'nullable|string|max:50',
+            'district' => 'nullable|string|max:50',
+            'address' => 'nullable|string|max:255',
+            'adcode' => 'nullable|string|size:6',
+        ];
+    }
+
+    public function messages()
+    {
+        return [
+            'latitude.required' => '纬度不能为空',
+            'latitude.numeric' => '纬度必须是数字',
+            'latitude.between' => '纬度必须在-90到90之间',
+            'longitude.required' => '经度不能为空',
+            'longitude.numeric' => '经度必须是数字',
+            'longitude.between' => '经度必须在-180到180之间',
+            'type.integer' => '位置类型必须是整数',
+            'type.in' => '位置类型只能是1(当前位置)或2(常用位置)',
+            'province.max' => '省份不能超过50个字符',
+            'city.max' => '城市不能超过50个字符',
+            'district.max' => '区县不能超过50个字符',
+            'address.max' => '详细地址不能超过255个字符',
+            'adcode.size' => '行政区划代码必须是6位',
+        ];
+    }
+}

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

@@ -5,6 +5,7 @@ namespace App\Services\Coach;
 use App\Models\CoachUser;
 use App\Enums\OrderStatus;
 use App\Models\MemberUser;
+use App\Models\CoachLocation;
 use App\Models\CoachSchedule;
 use Illuminate\Support\Facades\DB;
 use App\Enums\TechnicianAuthStatus;
@@ -13,9 +14,12 @@ use Illuminate\Support\Facades\Log;
 use App\Enums\TechnicianLocationType;
 use Illuminate\Support\Facades\Cache;
 use Illuminate\Support\Facades\Redis;
+use App\Traits\LocationValidationTrait;
 
 class AccountService
 {
+    use LocationValidationTrait;
+
     private const CACHE_KEY_PREFIX = 'coach_info_';
 
     private const CACHE_TTL = 300; // 5分钟
@@ -381,68 +385,56 @@ class AccountService
 
     /**
      * 设置定位信息
+     * 支持设置当前位置和常用位置,包含地理位置和行政区划信息
      *
-     * @param  int  $coachId  技师ID
-     * @param  float  $latitude  纬度
-     * @param  float  $longitude  经度
-     * @param  int  $type  位置类型 (current:1|common:2)
-     * @return bool
+     * 业务流程:
+     * 1. 验证经纬度参数
+     * 2. 验证位置类型
+     * 3. 保存到Redis的地理位置数据结构
+     * 4. 同步保存到数据库
      *
-     * @throws \Exception
+     * @param int $coachId 技师ID
+     * @param float $latitude 纬度
+     * @param float $longitude 经度
+     * @param int $type 位置类型 (current:1|common:2)
+     * @param array $locationInfo 位置信息,包含:
+     *        - province: string|null 省份
+     *        - city: string|null 城市
+     *        - district: string|null 区县
+     *        - address: string|null 详细地址
+     *        - adcode: string|null 行政区划代码
+     * @return bool 返回缓存更新结果
+     * @throws \Exception 当验证失败或保存失败时抛出异常
      */
-    public function setLocation($coachId, $latitude, $longitude, $type = TechnicianLocationType::COMMON->value)
+    public function setLocation($coachId, $latitude, $longitude, $type = TechnicianLocationType::COMMON->value, array $locationInfo = [])
     {
-        DB::beginTransaction();
-        try {
-            // 验证经纬度参数
-            if (! is_numeric($latitude) || ! is_numeric($longitude)) {
-                throw new \Exception('无效的经纬度坐标');
-            }
+        // 使用事务确保数据一致性
+        return DB::transaction(function () use ($coachId, $latitude, $longitude, $type, $locationInfo) {
+            // 验证经纬度的有效性(-90≤纬度≤90,-180≤经度≤180)
+            $this->validateCoordinates($latitude, $longitude);
 
-            // 验证位置类型
-            if (! in_array($type, [TechnicianLocationType::CURRENT->value, TechnicianLocationType::COMMON->value])) {
-                throw new \Exception('无效的位置类型');
-            }
+            // 验证位置类型是否为有效值(1:当前位置 2:常用位置)
+            $this->validateLocationType($type);
 
-            // 生成Redis键
-            $key = $coachId . '_' . $type;
+            // 格式化并验证位置信息(省市区、地址、行政区划代码)
+            $formattedLocation = $this->formatLocationInfo($locationInfo);
 
-            // 将置信息写入Redis
-            $result = Redis::geoadd('coach_locations', $longitude, $latitude, $key);
+            // 更新Redis地理位置缓存,用于实时位置查询
+            $result = $this->updateLocationCache($coachId, $longitude, $latitude, $type);
 
-            // 同时写入数据库保存历史记录
-            DB::table('coach_locations')->updateOrInsert(
+            // 同步更新数据库,保存历史位置记录
+            CoachLocation::updateOrCreate(
+                // 查询条件:根据技师ID和位置类型确定唯一记录
                 ['coach_id' => $coachId, 'type' => $type],
-                [
+                // 更新数据:合并基础位置信息和格式化后的地址信息
+                array_merge([
                     'latitude' => $latitude,
                     'longitude' => $longitude,
-                    'updated_at' => now(),
-                ]
+                ], $formattedLocation)
             );
 
-            DB::commit();
-
-            Log::info('技师位置信息设置成功', [
-                'coach_id' => $coachId,
-                'type' => $type,
-                'latitude' => $latitude,
-                'longitude' => $longitude,
-            ]);
-
             return $result;
-        } catch (\Exception $e) {
-            DB::rollBack();
-            Log::error('技师位置信息设置异常', [
-                'coach_id' => $coachId,
-                'latitude' => $latitude,
-                'longitude' => $longitude,
-                'type' => $type,
-                'error' => $e->getMessage(),
-                'file' => $e->getFile(),
-                'line' => $e->getLine(),
-            ]);
-            throw $e;
-        }
+        });
     }
 
     /**
@@ -986,7 +978,7 @@ class AccountService
     /**
      * 检查是否存在审核记录
      *
-     * @param CoachUser $coach 技师象
+     * @param CoachUser $coach 技师
      * @param string $type 记录类型(info|qual|real)
      * @return bool
      */
@@ -1003,4 +995,62 @@ class AccountService
             ->where('state', TechnicianAuthStatus::AUDITING->value)
             ->exists();
     }
+
+    /**
+     * 格式化位置信息
+     * 过滤和验证位置相关字段
+     *
+     * @param array $locationInfo 原始位置信息
+     * @return array 格式化后的位置信息
+     * @throws \Exception 当行政区划代码格式无效时抛出异常
+     */
+    private function formatLocationInfo(array $locationInfo): array
+    {
+        // 定义允许的字段列表,确保数据安全性
+        $allowedFields = [
+            'province',  // 省份
+            'city',      // 城市
+            'district',  // 区县
+            'address',   // 详细地址
+            'adcode'     // 行政区划代码
+        ];
+
+        // 过滤并验证字段:
+        // 1. 只保留允许的字段
+        // 2. 移除空值
+        $formatted = array_filter(
+            array_intersect_key($locationInfo, array_flip($allowedFields)),
+            function ($value) {
+                return !is_null($value) && $value !== '';
+            }
+        );
+
+        // 验证行政区划代码格式(6位数字)
+        if (isset($formatted['adcode'])) {
+            abort_if(!preg_match('/^\d{6}$/', $formatted['adcode']), 422, '无效的行政区划代码');
+        }
+
+        return $formatted;
+    }
+
+    /**
+     * 更新位置缓存
+     * 处理Redis地理位置数据结构的更新
+     * 使用Redis的GEOADD命令存储地理位置信息
+     *
+     * @param int $coachId 技师ID
+     * @param float $longitude 经度
+     * @param float $latitude 纬度
+     * @param int $type 位置类型
+     * @return bool 操作是否成功
+     */
+    private function updateLocationCache(int $coachId, float $longitude, float $latitude, int $type): bool
+    {
+        // 生成缓存键:技师ID_位置类型
+        $key = $coachId . '_' . $type;
+
+        // 使用Redis的GEOADD命令添加地理位置信息
+        // 参数顺序:key longitude latitude member
+        return Redis::geoadd('coach_locations', $longitude, $latitude, $key);
+    }
 }

+ 24 - 0
app/Traits/LocationDataTrait.php

@@ -0,0 +1,24 @@
+<?php
+
+namespace App\Traits;
+
+trait LocationDataTrait
+{
+    /**
+     * 提取位置信息
+     * 从请求数据中提取并过滤位置相关字段
+     *
+     * @param array $data 原始数据
+     * @return array 过滤后的位置信息
+     */
+    protected function extractLocationInfo(array $data): array
+    {
+        return array_filter([
+            'province' => $data['province'] ?? null,
+            'city' => $data['city'] ?? null,
+            'district' => $data['district'] ?? null,
+            'address' => $data['address'] ?? null,
+            'adcode' => $data['adcode'] ?? null,
+        ]);
+    }
+}

+ 83 - 0
app/Traits/LocationValidationTrait.php

@@ -0,0 +1,83 @@
+<?php
+
+namespace App\Traits;
+
+use App\Enums\TechnicianLocationType;
+
+trait LocationValidationTrait
+{
+    /**
+     * 验证位置类型
+     * 检查位置类型是否为有效值
+     *
+     * @param int|null $type 位置类型
+     * @param int $code HTTP状态码
+     * @param string $message 错误消息
+     * @return void
+     */
+    protected function validateLocationType(?int $type, int $code = 422, string $message = '无效的位置类型'): void
+    {
+        abort_if(
+            !in_array($type, [TechnicianLocationType::CURRENT->value, TechnicianLocationType::COMMON->value]),
+            $code,
+            $message
+        );
+    }
+
+    /**
+     * 验证经纬度
+     * 检查经纬度的有效性
+     *
+     * @param float $latitude 纬度
+     * @param float $longitude 经度
+     * @param int $code HTTP状态码
+     * @param string $latMessage 纬度错误消息
+     * @param string $lngMessage 经度错误消息
+     * @throws \Exception 当经纬度无效时抛出异常
+     */
+    protected function validateCoordinates(
+        $latitude,
+        $longitude,
+        int $code = 422,
+        string $latMessage = '无效的纬度坐标',
+        string $lngMessage = '无效的经度坐标'
+    ): void {
+        // 验证纬度 (-90° to 90°)
+        abort_if(
+            !is_numeric($latitude) || !($latitude >= -90 && $latitude <= 90),
+            $code,
+            $latMessage
+        );
+
+        // 验证经度 (-180° to 180°)
+        abort_if(
+            !is_numeric($longitude) || !($longitude >= -180 && $longitude <= 180),
+            $code,
+            $lngMessage
+        );
+    }
+
+    /**
+     * 获取有效的位置类型列表
+     *
+     * @return array 位置类型列表
+     */
+    protected function getValidLocationTypes(): array
+    {
+        return [
+            TechnicianLocationType::CURRENT->value,
+            TechnicianLocationType::COMMON->value
+        ];
+    }
+
+    /**
+     * 检查是否为有效的位置类型
+     *
+     * @param int|null $type 位置类型
+     * @return bool
+     */
+    protected function isValidLocationType(?int $type): bool
+    {
+        return in_array($type, $this->getValidLocationTypes());
+    }
+}