AccountService.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412
  1. <?php
  2. namespace App\Services\Coach;
  3. use App\Enums\TechnicianAuthStatus;
  4. use App\Enums\TechnicianLocationType;
  5. use App\Models\MemberUser;
  6. use Illuminate\Support\Facades\Cache;
  7. use Illuminate\Support\Facades\DB;
  8. use Illuminate\Support\Facades\Log;
  9. use Illuminate\Support\Facades\Redis;
  10. class AccountService
  11. {
  12. private const CACHE_KEY_PREFIX = 'coach_info_';
  13. private const CACHE_TTL = 300; // 5分钟
  14. /**
  15. * 提交技师基本信息
  16. */
  17. public function submitBaseInfo($user, array $data)
  18. {
  19. DB::beginTransaction();
  20. try {
  21. $this->setTransactionConfig();
  22. abort_if(! $user->coach, 404, '技师信息不存在');
  23. // 检查是否有待审核的记录
  24. $pendingRecord = $user->coach->infoRecords()
  25. ->where('state', TechnicianAuthStatus::AUDITING->value)
  26. ->exists();
  27. abort_if($pendingRecord, 422, '已有待审核的基本信息记录');
  28. // 创建技师信息
  29. $record = $user->coach->infoRecords()->create(array_merge($data, [
  30. 'state' => TechnicianAuthStatus::AUDITING->value,
  31. ]));
  32. // 清除技师信息缓存
  33. $this->clearCoachCache($user->coach->id);
  34. DB::commit();
  35. $this->logInfo('技师提交基本信息成功', $user, $data);
  36. return ['message' => '基本信息提交成功'];
  37. } catch (\Exception $e) {
  38. DB::rollBack();
  39. $this->logError('提交技师基本信息失败', $user, $data, $e);
  40. throw $e;
  41. }
  42. }
  43. /**
  44. * 提交技师资质信息
  45. */
  46. public function submitQualification($user, array $data)
  47. {
  48. DB::beginTransaction();
  49. try {
  50. $this->setTransactionConfig();
  51. abort_if(! $user->coach, 404, '技师信息不存在');
  52. // 检查是否有待审核的记录
  53. $pendingRecord = $user->coach->qualRecords()
  54. ->where('state', TechnicianAuthStatus::AUDITING->value)
  55. ->exists();
  56. abort_if($pendingRecord, 422, '已有待审核的资质信息记录');
  57. // 创建资质信息
  58. $record = $user->coach->qualRecords()->create(array_merge($data, [
  59. 'state' => TechnicianAuthStatus::AUDITING->value,
  60. ]));
  61. // 清除技师信息缓存
  62. $this->clearCoachCache($user->coach->id);
  63. DB::commit();
  64. $this->logInfo('技师提交资质信息成功', $user, $data);
  65. return ['message' => '资质信息提交成功'];
  66. } catch (\Exception $e) {
  67. DB::rollBack();
  68. $this->logError('提交技师资质信息失败', $user, $data, $e);
  69. throw $e;
  70. }
  71. }
  72. /**
  73. * 提交实名认证信息
  74. */
  75. public function submitRealName($user, array $data)
  76. {
  77. DB::beginTransaction();
  78. try {
  79. $this->setTransactionConfig();
  80. abort_if(! $user->coach, 404, '技师信息不存在');
  81. // 检查是否有待审核的记录
  82. $pendingRecord = $user->coach->realRecords()
  83. ->where('state', TechnicianAuthStatus::AUDITING->value)
  84. ->exists();
  85. abort_if($pendingRecord, 422, '已有待审核的实名认证信息');
  86. // 创建实名认证信息
  87. $record = $user->coach->realRecords()->create(array_merge($data, [
  88. 'state' => TechnicianAuthStatus::AUDITING->value,
  89. ]));
  90. // 清除技师信息缓存
  91. $this->clearCoachCache($user->coach->id);
  92. DB::commit();
  93. $this->logInfo('技师提交实名认证信息成功', $user, $this->maskSensitiveData($data));
  94. return ['message' => '实名认证信息提交成功'];
  95. } catch (\Exception $e) {
  96. DB::rollBack();
  97. $this->logError('提交实名认证信息失败', $user, $this->maskSensitiveData($data), $e);
  98. throw $e;
  99. }
  100. }
  101. /**
  102. * 获取技师信息
  103. */
  104. public function getCoachInfo($user)
  105. {
  106. try {
  107. abort_if(! $user, 404, '用户不存在');
  108. abort_if(! $user->coach, 404, '技师信息不存在');
  109. return Cache::remember(
  110. self::CACHE_KEY_PREFIX.$user->coach->id,
  111. self::CACHE_TTL,
  112. function () use ($user) {
  113. return $this->fetchCoachInfo($user->coach);
  114. }
  115. );
  116. } catch (\Exception $e) {
  117. $this->logError('获取技师信息失败', $user, [], $e);
  118. throw $e;
  119. }
  120. }
  121. /**
  122. * 设置事务配置
  123. */
  124. private function setTransactionConfig()
  125. {
  126. DB::statement('SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED');
  127. DB::statement('SET SESSION innodb_lock_wait_timeout=10');
  128. }
  129. /**
  130. * 记录信息日志
  131. */
  132. private function logInfo(string $message, $user, array $data)
  133. {
  134. Log::info($message, [
  135. 'user_id' => $user->id,
  136. 'coach_id' => $user->coach->id,
  137. 'data' => $data,
  138. 'ip' => request()->ip(),
  139. 'timestamp' => now()->toDateTimeString(),
  140. ]);
  141. }
  142. /**
  143. * 记录错误日志
  144. */
  145. private function logError(string $message, $user, array $data, \Exception $e)
  146. {
  147. Log::error($message, [
  148. 'user_id' => $user->id,
  149. 'coach_id' => $user->coach->id ?? null,
  150. 'data' => $data,
  151. 'error' => $e->getMessage(),
  152. 'file' => $e->getFile(),
  153. 'line' => $e->getLine(),
  154. 'ip' => request()->ip(),
  155. 'timestamp' => now()->toDateTimeString(),
  156. ]);
  157. }
  158. /**
  159. * 获取技师详细信息
  160. */
  161. private function fetchCoachInfo($coach)
  162. {
  163. $baseInfo = $coach->infoRecords()->latest()->first();
  164. $qualification = $coach->qualRecords()->latest()->first();
  165. $realName = $coach->realRecords()->latest()->first();
  166. return [
  167. 'base_info' => $baseInfo ? $this->formatBaseInfo($baseInfo) : null,
  168. 'qualification' => $qualification ? $this->formatQualification($qualification) : null,
  169. 'real_name' => $realName ? $this->formatRealName($realName) : null,
  170. ];
  171. }
  172. /**
  173. * 格式化基本信息
  174. */
  175. private function formatBaseInfo($info)
  176. {
  177. return [
  178. 'nickname' => $info->nickname,
  179. 'avatar' => $info->avatar,
  180. 'gender' => $info->gender,
  181. 'mobile' => $this->maskMobile($info->mobile),
  182. 'birthday' => $info->birthday,
  183. 'work_years' => $info->work_years,
  184. 'intention_city' => $info->intention_city,
  185. 'introduction' => $info->introduction,
  186. 'state' => $info->state,
  187. 'state_text' => TechnicianAuthStatus::fromValue($info->state)->label(),
  188. 'audit_remark' => $info->audit_remark,
  189. ];
  190. }
  191. /**
  192. * 格式化资质信息
  193. */
  194. private function formatQualification($qual)
  195. {
  196. return [
  197. 'qual_type' => $qual->qual_type,
  198. 'qual_no' => $qual->qual_no,
  199. 'qual_photo' => $qual->qual_photo,
  200. 'valid_start' => $qual->valid_start,
  201. 'valid_end' => $qual->valid_end,
  202. 'state' => $qual->state,
  203. 'state_text' => TechnicianAuthStatus::fromValue($qual->state)->label(),
  204. 'audit_remark' => $qual->audit_remark,
  205. ];
  206. }
  207. /**
  208. * 格式化实名信息
  209. */
  210. private function formatRealName($real)
  211. {
  212. return [
  213. 'real_name' => $real->real_name,
  214. 'id_card' => $this->maskIdCard($real->id_card),
  215. 'id_card_front_photo' => $real->id_card_front_photo,
  216. 'id_card_back_photo' => $real->id_card_back_photo,
  217. 'id_card_hand_photo' => $real->id_card_hand_photo,
  218. 'state' => $real->state,
  219. 'state_text' => TechnicianAuthStatus::fromValue($real->state)->label(),
  220. 'audit_remark' => $real->audit_remark,
  221. ];
  222. }
  223. /**
  224. * 手机号脱敏
  225. */
  226. private function maskMobile($mobile)
  227. {
  228. return substr_replace($mobile, '****', 3, 4);
  229. }
  230. /**
  231. * 身份证号脱敏
  232. */
  233. private function maskIdCard($idCard)
  234. {
  235. return substr_replace($idCard, '****', 6, 8);
  236. }
  237. /**
  238. * 敏感数据脱敏
  239. */
  240. private function maskSensitiveData(array $data)
  241. {
  242. if (isset($data['id_card'])) {
  243. $data['id_card'] = $this->maskIdCard($data['id_card']);
  244. }
  245. if (isset($data['mobile'])) {
  246. $data['mobile'] = $this->maskMobile($data['mobile']);
  247. }
  248. return $data;
  249. }
  250. /**
  251. * 清除技师信息缓存
  252. */
  253. private function clearCoachCache($coachId)
  254. {
  255. Cache::forget(self::CACHE_KEY_PREFIX.$coachId);
  256. }
  257. /**
  258. * 设置定位信息
  259. *
  260. * @param int $coachId 技师ID
  261. * @param float $latitude 纬度
  262. * @param float $longitude 经度
  263. * @param int $type 位置类型 (current:1|common:2)
  264. * @return bool
  265. *
  266. * @throws \Exception
  267. */
  268. public function setLocation($coachId, $latitude, $longitude, $type = TechnicianLocationType::COMMON->value)
  269. {
  270. DB::beginTransaction();
  271. try {
  272. // 验证经纬度参数
  273. if (! is_numeric($latitude) || ! is_numeric($longitude)) {
  274. throw new \Exception('无效的经纬度坐标');
  275. }
  276. // 验证位置类型
  277. if (! in_array($type, [TechnicianLocationType::CURRENT->value, TechnicianLocationType::COMMON->value])) {
  278. throw new \Exception('无效的位置类型');
  279. }
  280. // 生成Redis键
  281. $key = $coachId.'_'.$type;
  282. // 将位置信息写入Redis
  283. $result = Redis::geoadd('coach_locations', $longitude, $latitude, $key);
  284. // 同时写入数据库保存历史记录
  285. DB::table('coach_locations')->updateOrInsert(
  286. ['coach_id' => $coachId, 'type' => $type],
  287. [
  288. 'latitude' => $latitude,
  289. 'longitude' => $longitude,
  290. 'updated_at' => now(),
  291. ]
  292. );
  293. DB::commit();
  294. Log::info('技师位置信息设置成功', [
  295. 'coach_id' => $coachId,
  296. 'type' => $type,
  297. 'latitude' => $latitude,
  298. 'longitude' => $longitude,
  299. ]);
  300. return $result;
  301. } catch (\Exception $e) {
  302. DB::rollBack();
  303. Log::error('技师位置信息设置异常', [
  304. 'coach_id' => $coachId,
  305. 'latitude' => $latitude,
  306. 'longitude' => $longitude,
  307. 'type' => $type,
  308. 'error' => $e->getMessage(),
  309. 'file' => $e->getFile(),
  310. 'line' => $e->getLine(),
  311. ]);
  312. throw $e;
  313. }
  314. }
  315. /**
  316. * 获取技师位置信息
  317. *
  318. * @param int $userId 用户ID
  319. * @return array 位置信息
  320. */
  321. public function getLocation($userId)
  322. {
  323. try {
  324. // 改进:直接使用 coach 模型
  325. $user = MemberUser::find($userId);
  326. abort_if(! $user, 404, '用户不存在');
  327. abort_if(! $user->coach, 404, '技师信息不存在');
  328. // 获取常用位置信息
  329. $location = $user->coach->locations()
  330. ->where('type', TechnicianLocationType::COMMON->value)
  331. ->first();
  332. $result = [
  333. 'address' => $location ? $location->location : null,
  334. ];
  335. // 记录日志
  336. Log::info('获取技师常用位置信息成功', [
  337. 'coach_id' => $user->coach->id,
  338. 'location' => $result,
  339. ]);
  340. return $result;
  341. } catch (\Exception $e) {
  342. Log::error('获取技师常用位置信息异常', [
  343. 'coach_id' => $user->coach->id ?? null,
  344. 'error' => $e->getMessage(),
  345. 'file' => $e->getFile(),
  346. 'line' => $e->getLine(),
  347. ]);
  348. throw $e;
  349. }
  350. }
  351. }