AccountController.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  1. <?php
  2. namespace App\Http\Controllers\Coach;
  3. use Illuminate\Http\Request;
  4. use App\Traits\ResponseTrait;
  5. use App\Traits\LocationDataTrait;
  6. use Illuminate\Support\Facades\Log;
  7. use App\Http\Controllers\Controller;
  8. use Illuminate\Support\Facades\Auth;
  9. use App\Enums\TechnicianLocationType;
  10. use App\Services\Coach\AccountService;
  11. use App\Http\Requests\Coach\SetLocationRequest;
  12. use App\Http\Requests\Coach\SendVerifyCodeRequest;
  13. use App\Http\Requests\Coach\SubmitBaseInfoRequest;
  14. use App\Http\Requests\Coach\SubmitRealNameRequest;
  15. use App\Http\Requests\Coach\UpdateBasicInfoRequest;
  16. use App\Http\Requests\Coach\SubmitQualificationRequest;
  17. /**
  18. * @group 技师端
  19. *
  20. * 技师账户相关的API接口
  21. */
  22. class AccountController extends Controller
  23. {
  24. use ResponseTrait;
  25. use LocationDataTrait;
  26. protected AccountService $service;
  27. public function __construct(AccountService $service)
  28. {
  29. $this->service = $service;
  30. }
  31. /**
  32. * [账户]提交基本信息
  33. *
  34. * @description 提交技师的基本个人信息,包括头像、生活照片、个人资料等
  35. *
  36. * 业务流程:
  37. * 1. 验证提交的数据
  38. * 2. 调用服务层处理业务逻辑
  39. * 3. 返回处理结果
  40. *
  41. * 注意事项:
  42. * - 同一时间只能有一条待审核记录
  43. * - 审核不通过可以重新提交
  44. * - 头像和生活照片支持任意格式的图片数据
  45. * - 除性别和手机号外,其他字段均为可选
  46. * - 生活照片为可选,支持多张
  47. *
  48. * @authenticated 需要技师身份认证
  49. *
  50. * @bodyParam nickname string nullable 昵称(2-20个字符) Example: 张三
  51. * @bodyParam avatar string nullable 头像图片 Example: base64或其他格式的图片数据
  52. * @bodyParam life_photos array nullable 生活照片数组
  53. * @bodyParam life_photos.* string required 生活照片 Example: base64或其他格式的图片数据
  54. * @bodyParam gender string required 性别(1:男 2:女) Example: 1
  55. * @bodyParam mobile string required 手机号 Example: 13800138000
  56. * @bodyParam birthday date nullable 出生日期(年龄需满18岁) Example: 1990-01-01
  57. * @bodyParam work_years integer nullable 工作年限(0-99) Example: 5
  58. * @bodyParam intention_city string nullable 意向城市 Example: 北京
  59. * @bodyParam introduction string nullable 个人简介(10-255个字符) Example: 专业按摩师,从业5年
  60. *
  61. * @response 200 {
  62. * "status": true,
  63. * "message": "基本信息提交成功"
  64. * }
  65. * @response 404 {
  66. * "message": "技师信息不存在"
  67. * }
  68. * @response 422 {
  69. * "message": "已有待审核的基本信息记录"
  70. * }
  71. * @response 422 {
  72. * "message": "验证错误",
  73. * "errors": {
  74. * "nickname": ["昵称不能少于2个字符"],
  75. * "gender": ["性别不能为空"],
  76. * "mobile": ["手机号不能为空"],
  77. * "life_photos.*": ["生活照片格式不正确"]
  78. * }
  79. * }
  80. */
  81. public function submitBaseInfo(SubmitBaseInfoRequest $request)
  82. {
  83. // 获取验证后的数据
  84. $data = $request->validated();
  85. // 调用服务层处理业务逻辑
  86. // 传入当前认证用户和验证后的数据
  87. return $this->success(
  88. $this->service->submitBaseInfo(Auth::user(), $data)
  89. );
  90. }
  91. /**
  92. * [账户]提交资质信息
  93. *
  94. * @description 提交技师的资质认证信息,包括资质照片、营业执照和健康证
  95. *
  96. * @authenticated
  97. *
  98. * @bodyParam qual_type int required 资质类型(1:初级按摩师 2:中级按摩师 3:高级按摩师) Example: 1
  99. * @bodyParam qual_photo string required 资质证书照片 Example: base64或其他格式的图片数据
  100. * @bodyParam business_license string required 营业执照照片 Example: base64或其他格式的图片数据
  101. * @bodyParam health_cert string required 健康证照片 Example: base64或其他格式的图片数据
  102. *
  103. * @response {
  104. * "message": "资质信息提交成功"
  105. * }
  106. */
  107. public function submitQualification(SubmitQualificationRequest $request)
  108. {
  109. $data = $request->validated();
  110. return $this->success($this->service->submitQualification(Auth::user(), $data));
  111. }
  112. /**
  113. * [账户]提交实名认证
  114. *
  115. * @description 提交技师的实名认证信息
  116. *
  117. * @authenticated
  118. *
  119. * @bodyParam real_name string nullable 姓名(2-20个字符) Example: 张三
  120. * @bodyParam id_card string nullable 身份证号(18位) Example: 370602199001011234
  121. * @bodyParam id_card_front_photo string required 身份证正面照片 Example: base64或其他格式的图片数据
  122. * @bodyParam id_card_back_photo string required 身份证反面照片 Example: base64或其他格式的图片数据
  123. * @bodyParam id_card_hand_photo string required 手持身份证照片 Example: base64或其他格式的图片数据
  124. *
  125. * @response {
  126. * "message": "实名认证信息提交成功"
  127. * }
  128. */
  129. public function submitRealName(SubmitRealNameRequest $request)
  130. {
  131. $data = $request->validated();
  132. return $this->success($this->service->submitRealName(Auth::user(), $data));
  133. }
  134. /**
  135. * [账户]获取技师认证信息
  136. *
  137. * @description 获取技师的基本信息、资质信息和实名认证信息
  138. *
  139. * @authenticated
  140. *
  141. * @response {
  142. * "data": {
  143. * "base_info": {
  144. * "nickname": "张三",
  145. * "avatar": "base64或其他格式的图片数据",
  146. * "life_photos": [
  147. * "base64或其他格式的图片数据1",
  148. * "base64或其他格式的图片数据2"
  149. * ],
  150. * "gender": "1",
  151. * "mobile": "138****8000",
  152. * "birthday": "1990-01-01",
  153. * "work_years": 5,
  154. * "intention_city": "北京",
  155. * "introduction": "专业按摩师,从业5年",
  156. * "state": 1,
  157. * "state_text": "已通过",
  158. * "audit_remark": "审核通过"
  159. * },
  160. * "qualification": {
  161. * "qual_type": "高级按摩师",
  162. * "qual_photo": "base64或其他格式的图片数据",
  163. * "business_license": "base64或其他格式的图片数据",
  164. * "health_cert": "base64或其他格式的图片数据",
  165. * "state": 1,
  166. * "state_text": "已通过",
  167. * "audit_remark": "审核通过"
  168. * },
  169. * "real_name": {
  170. * "real_name": "张三",
  171. * "id_card": "370602****1234",
  172. * "id_card_front_photo": "base64或其他格式的图片数据",
  173. * "id_card_back_photo": "base64或其他格式的图片数据",
  174. * "id_card_hand_photo": "base64或其他格式的图片数据",
  175. * "state": 1,
  176. * "state_text": "已通过",
  177. * "audit_remark": "审核通过"
  178. * }
  179. * }
  180. * }
  181. * @response 404 {
  182. * "message": "用户不存在"
  183. * }
  184. * @response 404 {
  185. * "message": "技师信息不存在"
  186. * }
  187. */
  188. public function info()
  189. {
  190. return $this->success($this->service->getCoachInfo(Auth::user()));
  191. }
  192. /**
  193. * [账户]设置技师位置信息
  194. *
  195. * @description 设置技师的当前位置或常用位置
  196. *
  197. * @authenticated
  198. *
  199. * @bodyParam latitude float required 纬度 Example: 39.9042
  200. * @bodyParam longitude float required 经度 Example: 116.4074
  201. * @bodyParam type int nullable 位置类型(1:当前位置 2:常用位置) Example: 2
  202. * @bodyParam province string nullable 省份 Example: 北京市
  203. * @bodyParam city string nullable 城市 Example: 北京市
  204. * @bodyParam district string nullable 区县 Example: 朝阳区
  205. * @bodyParam address string nullable 详细地址 Example: 建国路93号万达广场
  206. * @bodyParam adcode string nullable 行政区划代码 Example: 110105
  207. *
  208. * @response {
  209. * "message": "位置信息设置成功"
  210. * }
  211. */
  212. public function setLocation(SetLocationRequest $request)
  213. {
  214. // 获取验证后的数据
  215. $validated = $request->validated();
  216. // 确保用户和技师存在
  217. $user = Auth::user();
  218. abort_if(!$user->coach, 404, '技师信息不存在');
  219. // 提取位置信息
  220. $locationInfo = $this->extractLocationInfo($validated);
  221. // 传递技师ID给服务层
  222. $this->service->setLocation(
  223. $user->coach->id,
  224. $validated['latitude'],
  225. $validated['longitude'],
  226. $validated['type'] ?? TechnicianLocationType::COMMON->value,
  227. $locationInfo
  228. );
  229. return $this->success(['message' => '位置信息设置成功']);
  230. }
  231. /**
  232. * [账户]获取技师位置信息
  233. *
  234. * @description 获取技师的位置信息
  235. *
  236. * @authenticated
  237. *
  238. * @queryParam type int 位置类型(1:当前位置 2:常用位置) Example: 2
  239. *
  240. * @response {
  241. * "data": {
  242. * "province": "浙江省",
  243. * "city": "杭州市",
  244. * "district": "西湖区",
  245. * "address": "文三路478号",
  246. * "adcode": "330106",
  247. * "longitude": 120.12345,
  248. * "latitude": 30.12345,
  249. * "updated_at": "2024-03-22 10:00:00"
  250. * }
  251. * }
  252. */
  253. public function getLocation(Request $request)
  254. {
  255. $validated = $request->validate([
  256. 'type' => 'required|integer|in:1,2'
  257. ]);
  258. return $this->success(
  259. $this->service->getLocation(Auth::user(), $validated['type'])
  260. );
  261. }
  262. /**
  263. * [账户]设置排班时间
  264. *
  265. * @description 设置技师每天通用的排班时间段
  266. *
  267. * @authenticated
  268. *
  269. * @bodyParam time_ranges array required 时间段数组
  270. * @bodyParam time_ranges[].start_time string required 开始时间(HH:mm格式) Example: "09:00"
  271. * @bodyParam time_ranges[].end_time string required 结束时间(HH:mm格式) Example: "12:00"
  272. *
  273. * @response {
  274. * "status": true,
  275. * "message": "排班设置成功",
  276. * "data": {
  277. * "coach_id": 1,
  278. * "time_ranges": [
  279. * {
  280. * "start_time": "09:00",
  281. * "end_time": "12:00"
  282. * },
  283. * {
  284. * "start_time": "14:00",
  285. * "end_time": "18:00"
  286. * }
  287. * ]
  288. * }
  289. * }
  290. * @response 400 {
  291. * "message": "时间段格式错误"
  292. * }
  293. * @response 400 {
  294. * "message": "时间格式错误,应为HH:mm格式"
  295. * }
  296. * @response 400 {
  297. * "message": "结束时间必须大于开始时间"
  298. * }
  299. * @response 400 {
  300. * "message": "时间段之间不能重叠"
  301. * }
  302. */
  303. public function setSchedule(Request $request)
  304. {
  305. $validated = $request->validate([
  306. 'time_ranges' => 'required|array|min:1',
  307. 'time_ranges.*.start_time' => [
  308. 'required',
  309. 'string',
  310. 'regex:/^([01][0-9]|2[0-3]):[0-5][0-9]$/',
  311. ],
  312. 'time_ranges.*.end_time' => [
  313. 'required',
  314. 'string',
  315. 'regex:/^([01][0-9]|2[0-3]):[0-5][0-9]$/',
  316. ],
  317. ], [
  318. 'time_ranges.required' => '必须设置时间段',
  319. 'time_ranges.array' => '时间段必须是数组格式',
  320. 'time_ranges.min' => '至少设置一个时间段',
  321. 'time_ranges.*.start_time.required' => '开始时间不能为空',
  322. 'time_ranges.*.start_time.regex' => '开始时间格式错误,应为HH:mm格式',
  323. 'time_ranges.*.end_time.required' => '结束时间不能为空',
  324. 'time_ranges.*.end_time.regex' => '结束时间格式错误,应为HH:mm格式',
  325. ]);
  326. return $this->success(
  327. $this->service->setSchedule(Auth::user()->id, $validated['time_ranges'])
  328. );
  329. }
  330. /**
  331. * [账户]更改技师工作状态
  332. *
  333. * @description 更改技师的工作状态(休息中/工作中),工作状态会自动判断为空闲或忙碌
  334. *
  335. * @authenticated
  336. *
  337. * @bodyParam status int required 状态(1:休息中 2:工作中) Example: 2
  338. *
  339. * @response {
  340. * "status": true,
  341. * "message": "状态更新成功",
  342. * "data": {
  343. * "work_status": 2,
  344. * "work_status_text": "空闲中",
  345. * "updated_at": "2024-03-20 10:00:00"
  346. * }
  347. * }
  348. * @response 400 {
  349. * "message": "无效的状态值"
  350. * }
  351. * @response 422 {
  352. * "message": "当前状态不能更改为休息状态"
  353. * }
  354. */
  355. public function updateWorkStatus(Request $request)
  356. {
  357. $validated = $request->validate([
  358. 'status' => 'required|integer|in:1,2',
  359. ], [
  360. 'status.required' => '状态不能为空',
  361. 'status.integer' => '状态必须是整数',
  362. 'status.in' => '无效的状态值',
  363. ]);
  364. return $this->success(
  365. $this->service->updateWorkStatus(Auth::user()->id, $validated['status'])
  366. );
  367. }
  368. /**
  369. * [账户]获取技师工作状态
  370. *
  371. * @description 获取技师当前工作状态
  372. *
  373. * @authenticated
  374. *
  375. * @response {
  376. * "data": {
  377. * "work_status": 2,
  378. * "work_status_text": "空闲中",
  379. * "updated_at": "2024-03-22 10:00:00"
  380. * }
  381. * }
  382. * @response 404 {
  383. * "message": "技师不存在"
  384. * }
  385. */
  386. public function getWorkStatus()
  387. {
  388. return $this->success(
  389. $this->service->getWorkStatus(Auth::user()->coach->id)
  390. );
  391. }
  392. /**
  393. * [账户]获取技师排班信息
  394. *
  395. * @return \Illuminate\Http\JsonResponse
  396. *
  397. * @description 技师获取自己的排班时间段信息
  398. *
  399. * @response {
  400. * "status": true,
  401. * "message": "获取成功",
  402. * "data": {
  403. * "time_ranges": [
  404. * {
  405. * "start_time": "09:00",
  406. * "end_time": "12:00"
  407. * },
  408. * {
  409. * "start_time": "14:00",
  410. * "end_time": "18:00"
  411. * }
  412. * ],
  413. * "updated_at": "2024-03-20 10:00:00"
  414. * }
  415. * }
  416. */
  417. public function getSchedule()
  418. {
  419. $schedule = $this->service->getSchedule(Auth::id());
  420. return $this->success($schedule);
  421. }
  422. /**
  423. * [账户]获取技师详细信息
  424. *
  425. * @description 获取当前登录技师的详细信息,包括基本信息、认证状态、位置信息、统计数据等
  426. *
  427. * @authenticated 需要技师身份认证
  428. *
  429. * @response {
  430. * "status": true,
  431. * "message": "获取成功",
  432. * "data": {
  433. * "coach_no": "00000001",
  434. * "mobile": "13800138000",
  435. * "nickname": "张三",
  436. * "avatar": "https://example.com/avatar.jpg",
  437. * "age": 25,
  438. * "gender": 1,
  439. * "work_years": 5,
  440. * "intention_city": "杭州",
  441. * "life_photos": [
  442. * "https://example.com/photo1.jpg",
  443. * "https://example.com/photo2.jpg"
  444. * ],
  445. * "introduction": "专业按摩师,从业5年",
  446. * "state": 1,
  447. * "state_text": "正常",
  448. * "invite_code": "C1",
  449. * "wallet": {
  450. * "balance": 1000,
  451. * "frozen": 200,
  452. * "total_income": 5000,
  453. * "today_income": 300,
  454. * "withdrawable": 800
  455. * },
  456. * "auth_status": {
  457. * "base_info": {
  458. * "state": 1,
  459. * "state_text": "已通过",
  460. * "audit_remark": "审核通过",
  461. * "updated_at": "2024-03-22 10:00:00"
  462. * },
  463. * "real_name": {
  464. * "state": 1,
  465. * "state_text": "已通过",
  466. * "audit_remark": "审核通过",
  467. * "updated_at": "2024-03-22 10:00:00"
  468. * },
  469. * "qualification": {
  470. * "state": 1,
  471. * "state_text": "已通过",
  472. * "audit_remark": "审核通过",
  473. * "updated_at": "2024-03-22 10:00:00"
  474. * }
  475. * },
  476. * "location": {
  477. * "province": "浙江省",
  478. * "city": "杭州市",
  479. * "district": "西湖区",
  480. * "address": "文三路478号",
  481. * "adcode": "330106",
  482. * "longitude": 120.12345,
  483. * "latitude": 30.12345
  484. * },
  485. * "shop": {
  486. * "id": 1,
  487. * "name": "示例店铺",
  488. * "address": "杭州市西湖区文三路478号",
  489. * "phone": "0571-88888888"
  490. * },
  491. * "statistics": {
  492. * "order_count": 100,
  493. * "completed_order_count": 80,
  494. * "level": 2,
  495. * "level_text": "中级技师",
  496. * "rating": 4.8
  497. * }
  498. * }
  499. * }
  500. * @response 404 {
  501. * "message": "技师信息不存在"
  502. * }
  503. */
  504. public function detail()
  505. {
  506. // 获取技师详细信息
  507. $data = $this->service->getCoachDetail();
  508. // 返回成功响应
  509. return $this->success($data, '获取成功');
  510. }
  511. /**
  512. * [账户]更新基础信息
  513. *
  514. * @description 更新技师的基础信息,包括昵称、性别和手机号。修改手机号时需要先获取验证码进行验证。
  515. *
  516. * 业务流程:
  517. * 1. 验证提交的数据
  518. * 2. 验证手机验证码(如果修改手机号)
  519. * 3. 调用服务层处理更新
  520. * 4. 返回更新结果
  521. *
  522. * 注意事项:
  523. * - 可以单独更新某个字段,不需要同时提交所有字段
  524. * - 修改手机号时必须提供验证码
  525. * - 同一时间只能有一条待审核记录
  526. *
  527. * @authenticated 需要技师身份认证
  528. *
  529. * @bodyParam nickname string nullable 昵称(2-20个字符) Example: 张三
  530. * @bodyParam gender integer nullable 性别(1:男 2:女) Example: 1
  531. * @bodyParam mobile string nullable 手机号 Example: 13800138000
  532. * @bodyParam code string required if:mobile 验证码(修改手机号时必填) Example: 123456
  533. *
  534. * @response {
  535. * "status": true,
  536. * "message": "基础信息修改申请已提交",
  537. * "data": {
  538. * "record_id": 1,
  539. * "updated_fields": ["nickname", "gender"]
  540. * }
  541. * }
  542. *
  543. * @response {
  544. * "status": true,
  545. * "message": "基础信息修改申请已更新",
  546. * "data": {
  547. * "record_id": 1,
  548. * "updated_fields": ["mobile"]
  549. * }
  550. * }
  551. *
  552. * @response 404 {
  553. * "message": "未找到有效的基础信息记录"
  554. * }
  555. * @response 422 {
  556. * "message": "没有需要更新的字段"
  557. * }
  558. * @response 422 {
  559. * "message": "验证码错误或已过期"
  560. * }
  561. * @response 422 {
  562. * "message": "验证错误",
  563. * "errors": {
  564. * "nickname": ["昵称不能少于2个字符"],
  565. * "gender": ["性别只能是1(男)或2(女)"],
  566. * "mobile": ["手机号格式不正确"],
  567. * "code": ["验证码不能为空"]
  568. * }
  569. * }
  570. */
  571. public function updateBasicInfo(UpdateBasicInfoRequest $request)
  572. {
  573. return $this->success(
  574. $this->service->updateBasicInfo(Auth::user()->coach, $request->validated())
  575. );
  576. }
  577. /**
  578. * [账户]发送验证码
  579. *
  580. * @description 发送手机验证码,用于修改手机号等操作
  581. *
  582. * @authenticated 需要技师身份认证
  583. *
  584. * @bodyParam mobile string required 手机号 Example: 13800138000
  585. * @bodyParam type string required 验证码类型(update:修改手机号) Example: "update"
  586. *
  587. * @response {
  588. * "status": true,
  589. * "message": "验证码发送成功",
  590. * "data": {
  591. * "mobile": "138****8000",
  592. * "expire_time": 300
  593. * }
  594. * }
  595. *
  596. * @response 422 {
  597. * "message": "该手机号已被使用"
  598. * }
  599. */
  600. public function sendVerifyCode(SendVerifyCodeRequest $request)
  601. {
  602. return $this->success(
  603. $this->service->sendVerifyCode($request->validated())
  604. );
  605. }
  606. /**
  607. * [账户]获取已开通项目
  608. *
  609. * @description 获取技师已开通的服务项目列表
  610. *
  611. * @authenticated 需要技师身份认证
  612. *
  613. * @response {
  614. * "status": true,
  615. * "message": "获取成功",
  616. * "data": {
  617. * "total": 2,
  618. * "list": [
  619. * {
  620. * "id": 1,
  621. * "project_id": 100,
  622. * "project_name": "全身按摩",
  623. * "category_id": 1,
  624. * "category_name": "按摩",
  625. * "duration": 60,
  626. * "price": 200.00,
  627. * "discount_amount": 20.00,
  628. * "final_price": 180.00,
  629. * "service_gender": "all",
  630. * "service_distance": 5000,
  631. * "traffic_fee_type": "round_trip",
  632. * "traffic_fee": 10.00,
  633. * "created_at": "2024-03-16 10:00:00",
  634. * "updated_at": "2024-03-16 10:00:00"
  635. * }
  636. * ]
  637. * }
  638. * }
  639. */
  640. public function enabledProjects()
  641. {
  642. return $this->success(
  643. $this->service->getEnabledProjects(Auth::user()->coach->id)
  644. );
  645. }
  646. }