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