OrderService.php 87 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757
  1. <?php
  2. namespace App\Services\Client;
  3. use App\Enums\OrderGrabRecordStatus;
  4. use App\Enums\OrderRecordStatus;
  5. use App\Enums\OrderStatus;
  6. use App\Enums\OrderType;
  7. use App\Enums\PaymentMethod;
  8. use App\Enums\ProjectStatus;
  9. use App\Enums\TechnicianAuthStatus;
  10. use App\Enums\TechnicianLocationType;
  11. use App\Enums\TechnicianStatus;
  12. use App\Enums\TransactionType;
  13. use App\Enums\UserStatus;
  14. use App\Models\AgentInfo;
  15. use App\Models\CoachUser;
  16. use App\Models\MemberAddress;
  17. use App\Models\MemberUser;
  18. use App\Models\Order;
  19. use App\Models\OrderGrabRecord;
  20. use App\Models\OrderRecord;
  21. use App\Models\Project;
  22. use App\Models\WalletPaymentRecord;
  23. use App\Services\Client\Traits\CalculatesOrderAmounts;
  24. use App\Services\Client\Traits\HandlesOrderRecords;
  25. use App\Services\Client\Traits\HandlesPayments;
  26. use App\Services\Client\Traits\ValidatesServiceTime;
  27. use Carbon\Carbon;
  28. use Exception;
  29. use Illuminate\Database\Eloquent\Collection;
  30. use Illuminate\Support\Facades\Auth;
  31. use Illuminate\Support\Facades\DB;
  32. use Illuminate\Support\Facades\Log;
  33. use Illuminate\Support\Facades\Redis;
  34. use SimpleSoftwareIO\QrCode\Facades\QrCode;
  35. use App\Models\CoachLocation;
  36. readonly class OrderService
  37. {
  38. use CalculatesOrderAmounts;
  39. use HandlesOrderRecords;
  40. use HandlesPayments;
  41. use ValidatesServiceTime;
  42. public function __construct(
  43. private AgentService $agentService,
  44. private ProjectService $projectService,
  45. private CoachService $coachService,
  46. private WalletService $walletService
  47. ) {}
  48. /**
  49. * 订单初始化
  50. *
  51. * @param int $userId 用户ID
  52. * @param array $data 订单数据
  53. * @return array 初始化的订单信息
  54. *
  55. * @throws \Exception
  56. *
  57. * 逻辑描述:
  58. * 1. 验证初始化参数(project_id、coach_id)
  59. * 2. 获取用户信息(包含钱包和地址)
  60. * 3. 获取区域编码(从地址或参数)
  61. * 4. 验证技师状态并获取可用时间
  62. * 5. 获取项目详情(含价格)
  63. * 6. 计算订单金额(含路费)
  64. * 7. 返回初始化数据(钱包、技师、项目、地址、金额、可用时间)
  65. */
  66. public function initialize(int $userId, array $data): array
  67. {
  68. // 参数验证
  69. $this->validateInitializeData($data);
  70. return DB::transaction(function () use ($userId, $data) {
  71. // 获取用户信息
  72. $user = $this->getUserWithWalletAndAddress($userId);
  73. // 获取区域编码
  74. $areaCode = $this->getAreaCode($user->address, $data);
  75. // 获取技师信息
  76. $coach = $this->validateCoach($data['coach_id']);
  77. // 计算距离和获取技师位置
  78. [$distance, $coachLocation] = $this->calculateDistanceAndLocation(
  79. userId: $userId,
  80. coachId: $coach->id,
  81. inputData: $data,
  82. userAddress: $user->address
  83. );
  84. // 设置技师的距离和位置信息
  85. $coach->distance = $distance;
  86. if ($coachLocation) {
  87. $coach->location = $coachLocation;
  88. }
  89. // 获取技师可用时间
  90. $availableTimeSlots = $this->coachService->getSchedule($coach->id);
  91. // 获取项目信息
  92. $project = $this->getProjectDetail($data['project_id'], $areaCode);
  93. // 计算订单金额
  94. $amounts = $this->calculateOrderAmount(
  95. userId: $userId,
  96. addressId: $user->address?->id ?? 0,
  97. coachId: $data['coach_id'],
  98. projectId: $data['project_id'],
  99. agentId: $project->agent_id,
  100. useBalance: false,
  101. distance: $distance,
  102. lat: $data['latitude'] ?? ($user->address?->latitude ?? 0),
  103. lng: $data['longitude'] ?? ($user->address?->longitude ?? 0)
  104. );
  105. return [
  106. 'wallet' => $user->wallet,
  107. 'coach' => $coach,
  108. 'project' => $project,
  109. 'address' => $user->address,
  110. 'amounts' => $amounts,
  111. 'availableTimeSlots' => $availableTimeSlots,
  112. ];
  113. });
  114. }
  115. /**
  116. * 计算距离并获取技师位置信息
  117. *
  118. * 计算逻辑优先级:
  119. * 1. 使用前端传入的距离
  120. * 2. 使用用户默认地址计算
  121. * 3. 使用前端传入的经纬度计算
  122. *
  123. * @param int $userId 用户ID
  124. * @param int $coachId 技师ID
  125. * @param array $inputData 输入数据
  126. * @param ?object $userAddress 用户默认地址
  127. * @return array{0: float, 1: ?array} [距离, 位置信息]
  128. */
  129. private function calculateDistanceAndLocation(
  130. int $userId,
  131. int $coachId,
  132. array $inputData,
  133. ?object $userAddress
  134. ): array {
  135. // 1. 如果前端直接传入距离,直接使用
  136. if (isset($inputData['distance'])) {
  137. return [$inputData['distance'], null];
  138. }
  139. // 2. 确定用户坐标来源
  140. $userCoordinates = $this->getUserCoordinates($inputData, $userAddress);
  141. if (!$userCoordinates) {
  142. return [0.0, null];
  143. }
  144. // 3. 计算距离和获取位置
  145. return $this->calculateDistanceFromCoordinates(
  146. userId: $userId,
  147. coachId: $coachId,
  148. longitude: $userCoordinates['longitude'],
  149. latitude: $userCoordinates['latitude']
  150. );
  151. }
  152. /**
  153. * 获取用户坐标
  154. *
  155. * @return array{longitude: float, latitude: float}|null
  156. */
  157. private function getUserCoordinates(array $inputData, ?object $userAddress): ?array
  158. {
  159. // 优先使用默认地址
  160. if ($userAddress && $userAddress->latitude && $userAddress->longitude) {
  161. return [
  162. 'longitude' => $userAddress->longitude,
  163. 'latitude' => $userAddress->latitude
  164. ];
  165. }
  166. // 其次使用前端传入的坐标
  167. if (isset($inputData['latitude']) && isset($inputData['longitude'])) {
  168. return [
  169. 'longitude' => $inputData['longitude'],
  170. 'latitude' => $inputData['latitude']
  171. ];
  172. }
  173. return null;
  174. }
  175. /**
  176. * 根据坐标计算距离和获取技师位置
  177. *
  178. * @param int $userId 用户ID
  179. * @param int $coachId 技师ID
  180. * @param float $longitude 经度
  181. * @param float $latitude 纬度
  182. * @return array{0: float, 1: ?array} [距离, 位置信息]
  183. */
  184. private function calculateDistanceFromCoordinates(
  185. int $userId,
  186. int $coachId,
  187. float $longitude,
  188. float $latitude
  189. ): array {
  190. // 临时存储用户位置用于计算
  191. $userLocationKey = "user_{$userId}_temp_location";
  192. Redis::geoadd(
  193. 'coach_locations',
  194. $longitude,
  195. $latitude,
  196. $userLocationKey
  197. );
  198. try {
  199. $distances = [];
  200. // 计算与技师两个位置点的距离
  201. foreach ([TechnicianLocationType::COMMON, TechnicianLocationType::CURRENT] as $locationType) {
  202. $locationKey = "{$coachId}_{$locationType->value}";
  203. $distance = Redis::geodist('coach_locations', $userLocationKey, $locationKey, 'km');
  204. if ($distance !== null) {
  205. $distances[] = [
  206. 'distance' => (float)$distance,
  207. 'key' => $locationKey,
  208. 'type' => $locationType->value
  209. ];
  210. }
  211. }
  212. // 如果没有有效距离,返回默认值
  213. if (empty($distances)) {
  214. return [0.0, null];
  215. }
  216. // 获取最小距离的记录
  217. $minDistance = array_reduce($distances, function ($carry, $item) {
  218. if (!$carry || $item['distance'] < $carry['distance']) {
  219. return $item;
  220. }
  221. return $carry;
  222. });
  223. // 从 Redis 获取原始位置信息
  224. $originalLocation = Redis::hget(
  225. "coach_location_original:{$coachId}",
  226. "type_{$minDistance['type']}"
  227. );
  228. if ($originalLocation) {
  229. $locationData = json_decode($originalLocation, true);
  230. return [
  231. $minDistance['distance'],
  232. [
  233. 'longitude' => (float)$locationData['longitude'],
  234. 'latitude' => (float)$locationData['latitude']
  235. ]
  236. ];
  237. }
  238. // 如果没有原始数据,则从数据库获取
  239. $location = CoachLocation::where('coach_id', $coachId)
  240. ->where('type', $minDistance['type'])
  241. ->latest()
  242. ->first();
  243. return [
  244. $minDistance['distance'],
  245. $location ? [
  246. 'longitude' => (float)$location->longitude,
  247. 'latitude' => (float)$location->latitude
  248. ] : null
  249. ];
  250. } finally {
  251. // 清理临时数据
  252. Redis::zrem('coach_locations', $userLocationKey);
  253. }
  254. }
  255. /**
  256. * 验证初始化数据
  257. *
  258. * 逻辑描述:
  259. * 1. 验证项目ID不能为空
  260. * 2. 验证技师ID不能为空
  261. */
  262. private function validateInitializeData(array $data): void
  263. {
  264. abort_if(empty($data['project_id']), 400, '项目ID不能为空');
  265. abort_if(empty($data['coach_id']), 400, '技师ID不能为空');
  266. }
  267. /**
  268. * 获取用户信息(包含钱包和地址)
  269. *
  270. * 逻辑描述:
  271. * 1. 查询用户信息(关联钱包和地址)
  272. * 2. 验证用户状态是否正常
  273. * 3. 返回用户信息
  274. */
  275. private function getUserWithWalletAndAddress(int $userId): MemberUser
  276. {
  277. $user = MemberUser::with(['wallet', 'address'])
  278. ->where('id', $userId)
  279. ->where('state', UserStatus::OPEN->value)
  280. ->first();
  281. abort_if(! $user, 400, '用户状态异常');
  282. return $user;
  283. }
  284. /**
  285. * 获取区域编码
  286. *
  287. * 逻辑描述:
  288. * 1. 优先使用地址中的区域编码
  289. * 2. 其次使用参数中的区域编码
  290. * 3. 验证区域编码不能为空
  291. */
  292. private function getAreaCode(?object $address, array $data): string
  293. {
  294. $areaCode = $address?->area_code ?? ($data['area_code'] ?? null);
  295. abort_if(empty($areaCode), 400, '区域编码不能为空');
  296. return $areaCode;
  297. }
  298. /**
  299. * 获取项目详情
  300. *
  301. * 逻辑描述:
  302. * 1. 调用项目服务获取详情
  303. * 2. 验证项目是否存在
  304. * 3. 返回项目信息
  305. */
  306. private function getProjectDetail(int $projectId, string $areaCode): Project
  307. {
  308. $project = $this->projectService->getProjectDetail($projectId, $areaCode);
  309. abort_if(! $project, 400, '项目不存在');
  310. return $project;
  311. }
  312. /**
  313. * 记录错误日志
  314. *
  315. * 逻辑描述:
  316. * 1. 记录错误信息
  317. * 2. 记录上下文数据
  318. * 3. 记录错误堆栈
  319. */
  320. private function logError(string $message, Exception $e, array $context = []): void
  321. {
  322. Log::error($message, [
  323. ...$context,
  324. 'error' => $e->getMessage(),
  325. 'trace' => $e->getTraceAsString(),
  326. ]);
  327. }
  328. /**
  329. * 创建订单
  330. *
  331. * @param array{
  332. * user_id: int,
  333. * project_id: int,
  334. * coach_id: ?int,
  335. * address_id: int,
  336. * service_time: string,
  337. * remark: ?string,
  338. * use_balance: bool,
  339. * agent_id: ?int,
  340. * distance: float,
  341. * payment_type: int,
  342. * type: int,
  343. * } $data 订单数据
  344. * @return array{
  345. * order_id: int,
  346. * order_no: string,
  347. * total_amount: float,
  348. * balance_amount: float,
  349. * pay_amount: float
  350. * }
  351. *
  352. * @throws \Exception
  353. *
  354. * 逻辑描述:
  355. * 1. 验证基础数据(用户状态、项目状态、服务时间、技师状态等)
  356. * 2. 计算订单金额(项目价格、路费、优惠等)
  357. * 3. 处理加钟订单特殊逻辑(使用原订单地址和技师)
  358. * 4. 获取服务地址信息
  359. * 5. 创建订单数据
  360. * 6. 创建订单相关记录
  361. * 7. 处理余额支付(如果是余额支付)
  362. * 8. 发送订单创建通知
  363. * 9. 返回订单信息
  364. */
  365. public function createOrder(int $userId, array $data): array
  366. {
  367. return DB::transaction(function () use ($userId, $data) {
  368. try {
  369. // 转换时间格式
  370. if (!empty($data['service_time'])) {
  371. $data['service_time'] = Carbon::parse($data['service_time'])->format('Y-m-d H:i:s');
  372. }
  373. // 1. 验证基础数据
  374. $this->validateOrderData($userId, $data);
  375. // 2. 计算订单金额
  376. $amounts = $this->calculateOrderAmount(
  377. userId: $userId,
  378. addressId: $data['address_id'] ?? null,
  379. coachId: $data['coach_id'] ?? null,
  380. projectId: $data['project_id'],
  381. agentId: $data['agent_id'] ?? null,
  382. useBalance: $data['use_balance'] ?? false,
  383. distance: $data['distance'] ?? 0
  384. );
  385. // 加钟订单使用原订单地址和技师ID
  386. if (! empty($data['order_id'])) {
  387. $order = Order::findOrFail($data['order_id']);
  388. // 验证订单状态
  389. abort_if(
  390. ! in_array($order->state, [
  391. OrderStatus::PAID->value,
  392. OrderStatus::ACCEPTED->value,
  393. OrderStatus::SERVICE_START->value,
  394. OrderStatus::SERVICING->value,
  395. ]),
  396. 422,
  397. '原订单状态不允许加钟'
  398. );
  399. $data['address_id'] = $order->address_id;
  400. $data['coach_id'] = $order->coach_id;
  401. $data['service_time'] = $order->service_end_time;
  402. }
  403. // 3. 获取地址
  404. $address = MemberUser::find($userId)->addresses()
  405. ->where('id', $data['address_id'])
  406. ->first();
  407. abort_if(! $address, 404, '收货地址不存在');
  408. // 3. 创建订单
  409. $order = $this->createOrderData(['user_id' => $userId, ...$data], $amounts, $address);
  410. // 4. 创建订单相关记录
  411. $this->createRelatedRecords($order);
  412. // 5. 检测支付类型,自动进行余额支付
  413. if ($order->payment_type == PaymentMethod::BALANCE->value) {
  414. // 处理余额支付
  415. if ($amounts['balance_amount'] > 0) {
  416. $this->processBalancePayment($order);
  417. }
  418. // 更改订单状态
  419. $order->state = OrderStatus::PAID->value;
  420. $order->service_start_time = $data['service_time'];
  421. $order->service_end_time = Carbon::parse($data['service_time'])
  422. ->addMinutes($order->project->duration);
  423. $order->save();
  424. // 创建订单相关记录
  425. $this->createRelatedRecords($order);
  426. }
  427. // 6. 发送通知
  428. $this->notifyOrderCreated($order);
  429. return [
  430. 'order_id' => $order->id,
  431. 'order_no' => $order->order_no,
  432. 'total_amount' => $amounts['total_amount'],
  433. 'balance_amount' => $amounts['balance_amount'],
  434. 'pay_amount' => $amounts['pay_amount'],
  435. ];
  436. } catch (Exception $e) {
  437. $this->logError('创建订单失败', $e, $data);
  438. throw $e;
  439. }
  440. });
  441. }
  442. /**
  443. * 验证订单创建数据
  444. */
  445. private function validateOrderData(int $userId, array $data): void
  446. {
  447. // 验证用户状态
  448. $user = MemberUser::where('id', $userId)
  449. ->where('state', UserStatus::OPEN->value)
  450. ->first();
  451. abort_if(! $user, 404, '用户不存在');
  452. abort_if($user->state != UserStatus::OPEN->value, 403, '用户状态异常');
  453. // 验证项目状态
  454. $project = Project::where('id', $data['project_id'])->first();
  455. abort_if(! $project, 404, '服务项目不存在');
  456. abort_if($project->state != ProjectStatus::OPEN->value, 403, '服务项目未开放');
  457. // 验证服务时间和技师状态
  458. if (! empty($data['service_time']) && ! empty($data['coach_id'])) {
  459. $this->validateServiceTimeParams($data['coach_id'], $data['service_time']);
  460. }
  461. }
  462. /**
  463. * 创建订单数据
  464. */
  465. private function createOrderData(array $data, array $amounts, object $address): Order
  466. {
  467. return Order::create([
  468. 'order_no' => $this->generateOrderNo(),
  469. 'user_id' => $data['user_id'],
  470. 'project_id' => $data['project_id'],
  471. 'coach_id' => $data['coach_id'] ?? null,
  472. 'agent_id' => $data['agent_id'] ?? null,
  473. 'address_id' => $data['address_id'],
  474. 'service_time' => $data['service_time'] ?? null,
  475. 'remark' => $data['remark'] ?? '',
  476. 'total_amount' => $amounts['total_amount'],
  477. 'balance_amount' => $amounts['balance_amount'],
  478. 'pay_amount' => $amounts['pay_amount'],
  479. 'traffic_amount' => $amounts['delivery_fee'],
  480. 'project_amount' => $amounts['project_amount'],
  481. 'discount_amount' => $amounts['coupon_amount'],
  482. 'state' => OrderStatus::CREATED->value,
  483. // 如果余额支付且未使用优惠券且未支付金额,则使用余额支付,否则使用传入的支付方式
  484. 'payment_type' => $amounts['balance_amount'] > 0 && $amounts['pay_amount'] == 0
  485. ? PaymentMethod::BALANCE->value
  486. : $data['payment_type'],
  487. 'latitude' => $address->latitude,
  488. 'longitude' => $address->longitude,
  489. 'location' => $address->location,
  490. 'address' => $address->detail,
  491. 'area_code' => $address->area_code,
  492. 'type' => $data['order_type'],
  493. 'distance' => $data['distance'] ?? 0, // 添加距离字段记录
  494. ]);
  495. }
  496. private function createOrderRecord(Order $order, int $objectId, string $objectType, OrderRecordStatus $status, string $remark): OrderRecord
  497. {
  498. return OrderRecord::create([
  499. 'order_id' => $order->id,
  500. 'object_id' => $objectId,
  501. 'object_type' => $objectType,
  502. 'state' => $status->value,
  503. 'remark' => $remark,
  504. ]);
  505. }
  506. /**
  507. * 生成订单号
  508. */
  509. private function generateOrderNo(): string
  510. {
  511. return 'O' . date('YmdHis') . str_pad(random_int(1, 9999), 4, '0', STR_PAD_LEFT);
  512. }
  513. /**
  514. * 处理余额支付
  515. */
  516. private function processBalancePayment(Order $order): void
  517. {
  518. $user = MemberUser::find($order->user_id);
  519. abort_if(! $user, 404, sprintf('用户[%d]不存在', $order->user_id));
  520. $wallet = $user->wallet;
  521. abort_if(
  522. $wallet->available_balance < $order->balance_amount,
  523. 400,
  524. sprintf(
  525. '用户[%d]可用余额[%.2f]不足,需要[%.2f]',
  526. $user->id,
  527. $wallet->available_balance,
  528. $order->balance_amount
  529. )
  530. );
  531. // 扣除余额
  532. if ($order->balance_amount > 0) {
  533. $this->walletService->deduct(
  534. userId: $user->id,
  535. amount: $order->balance_amount,
  536. type: TransactionType::PAYMENT->value,
  537. objectId: $order->id,
  538. remark: sprintf('订单[%s]余额支付', $order->order_no)
  539. );
  540. }
  541. }
  542. /**
  543. * 创建订单相关记录
  544. */
  545. private function createRelatedRecords(Order $order): void
  546. {
  547. // 创建订单状态记录
  548. OrderRecord::create([
  549. 'order_id' => $order->id,
  550. 'object_id' => $order->user_id,
  551. 'object_type' => MemberUser::class,
  552. 'state' => $order->state,
  553. 'remark' => $order->state === OrderStatus::PAID->value
  554. ? '余额支付成功'
  555. : '订单创建成功',
  556. ]);
  557. }
  558. /**
  559. * 发送订单创建通知
  560. */
  561. private function notifyOrderCreated(Order $order): void
  562. {
  563. // TODO: 实现订单创建通知
  564. // 1. 通知用户
  565. // event(new OrderCreatedEvent($order));
  566. }
  567. /**
  568. * 获取订单地址
  569. */
  570. private function getOrderAddress(MemberUser $user, array $data, OrderType $orderType): object
  571. {
  572. // 加钟订单使用原订单地址
  573. if ($orderType === OrderType::OVERTIME) {
  574. $originalOrder = $this->getOriginalOrder($user, $data['order_id']);
  575. $data['address_id'] = $originalOrder->address_id;
  576. }
  577. abort_if(empty($data['address_id']), 400, '地址ID不能为空');
  578. return $user->addresses()
  579. ->where('id', $data['address_id'])
  580. ->firstOrFail();
  581. }
  582. /**
  583. * 验证支付方式和余额
  584. */
  585. private function validatePayment(MemberUser $user, array $data, array $amounts): void
  586. {
  587. abort_if($amounts['total_amount'] <= 0, 400, '订单金额异常');
  588. if ($data['payment_type'] === PaymentMethod::BALANCE->value) {
  589. $wallet = $user->wallet;
  590. abort_if($wallet->available_balance < $amounts['balance_amount'], 400, '可用余额不足');
  591. }
  592. }
  593. /**
  594. * 判断是否需要处理余额支付
  595. */
  596. private function shouldHandleBalancePayment(Order $order, OrderType $orderType): bool
  597. {
  598. return $order->payment_type === PaymentMethod::BALANCE->value
  599. && $orderType !== OrderType::GRAB;
  600. }
  601. /**
  602. * 验证技师状态
  603. */
  604. private function validateCoach(int $coachId): CoachUser
  605. {
  606. $coach = CoachUser::query()
  607. ->with(['info'])
  608. ->where('id', $coachId)
  609. ->where('state', TechnicianStatus::ACTIVE->value)
  610. ->first();
  611. abort_if(! $coach, 400, '技师不存在或未激活');
  612. abort_if(
  613. ! $coach->info || $coach->info->state !== TechnicianAuthStatus::PASSED->value,
  614. 400,
  615. '技师未通过认证'
  616. );
  617. return $coach;
  618. }
  619. // 提取方法:获取原始订单
  620. private function getOriginalOrder($user, $orderId): Order
  621. {
  622. $originalOrder = $user->orders->where('id', $orderId)
  623. ->whereIn('state', [OrderStatus::SERVICING->value, OrderStatus::COMPLETED->value])
  624. ->firstOrFail();
  625. return $originalOrder;
  626. }
  627. // 提取方法:准备加钟订单数据
  628. private function prepareAddTimeData($originalOrder, $data): array
  629. {
  630. if ($originalOrder->state == OrderStatus::SERVICING->value) {
  631. $startTime = now();
  632. } else {
  633. $startTime = now();
  634. }
  635. return [
  636. ...$data,
  637. 'order_id' => $data['order_id'],
  638. 'address_id' => $originalOrder->address_id,
  639. 'service_time' => $startTime,
  640. 'coach_id' => $originalOrder->coach_id,
  641. ];
  642. }
  643. /**
  644. * 取消订单
  645. *
  646. * @param int $userId 用户ID
  647. * @param int $orderId 订单ID
  648. * @param string|null $reason 取消原因
  649. * @return array{message: string}
  650. *
  651. * @throws \Exception
  652. */
  653. public function cancelOrder(int $userId, int $orderId, ?string $reason = null): array
  654. {
  655. return DB::transaction(function () use ($userId, $orderId, $reason) {
  656. try {
  657. // 1. 验证订单状态
  658. $order = $this->validateOrderForCancel($userId, $orderId);
  659. // 2. 计算取消���用
  660. $cancelAmounts = $this->calculateCancelAmount($order);
  661. // 3. 处理退款
  662. $this->processCancelRefund($order, $cancelAmounts);
  663. // 4. 更新订单状态
  664. $this->updateOrderCancelStatus($order);
  665. // 5. 创建取消记录
  666. $this->createCancelRecords($order, $cancelAmounts, $reason);
  667. // 6. 发送取消通知
  668. $this->notifyCancel($order);
  669. return ['message' => '订单取消成功'];
  670. } catch (Exception $e) {
  671. $this->logError('取消订单失败', $e, [
  672. 'user_id' => $userId,
  673. 'order_id' => $orderId,
  674. 'reason' => $reason,
  675. ]);
  676. throw $e;
  677. }
  678. });
  679. }
  680. /**
  681. * 验证订单取消条件
  682. */
  683. private function validateOrderForCancel(int $userId, int $orderId): Order
  684. {
  685. // 获取并锁定订单
  686. $order = Order::where('id', $orderId)
  687. ->whereIn('state', [
  688. OrderStatus::CREATED->value,
  689. OrderStatus::PAID->value,
  690. OrderStatus::ACCEPTED->value,
  691. ])
  692. ->lockForUpdate()
  693. ->firstOrFail();
  694. // 验证订单所属用户
  695. abort_if($order->user_id !== $userId, 403, '无权操作此订单');
  696. // 验证订单是否已取消
  697. abort_if($order->state === OrderStatus::CANCELLED->value, 400, '订单已取消');
  698. return $order;
  699. }
  700. /**
  701. * 计算取消费用
  702. *
  703. * @return array{
  704. * total_refund: float,
  705. * balance_refund: float,
  706. * payment_refund: float,
  707. * penalty_amount: float
  708. * }
  709. */
  710. private function calculateCancelAmount(Order $order): array
  711. {
  712. $penaltyAmount = 0;
  713. // 计算违约金(根据订单状态和时间)
  714. if ($order->state === OrderStatus::ACCEPTED->value) {
  715. // 已接单取消,收取10%违约金
  716. $penaltyAmount = bcmul($order->total_amount, '0.1', 2);
  717. }
  718. // 计算实际退款金额
  719. $totalRefund = bcsub($order->total_amount, $penaltyAmount, 2);
  720. // 分配退款金额(优先退还支付金额)
  721. $balanceRefund = min($order->balance_amount, $totalRefund);
  722. $paymentRefund = bcsub($totalRefund, $balanceRefund, 2);
  723. return [
  724. 'total_refund' => $totalRefund,
  725. 'balance_refund' => $balanceRefund,
  726. 'payment_refund' => $paymentRefund,
  727. 'penalty_amount' => $penaltyAmount,
  728. ];
  729. }
  730. /**
  731. * 处理取消退款
  732. */
  733. private function processCancelRefund(Order $order, array $cancelAmounts): void
  734. {
  735. // 处理余额退款
  736. if ($cancelAmounts['balance_refund'] > 0) {
  737. $this->processBalanceCancelRefund($order, $cancelAmounts['balance_refund']);
  738. }
  739. // 处理支付退款
  740. if ($cancelAmounts['payment_refund'] > 0) {
  741. $this->processPaymentCancelRefund($order, $cancelAmounts['payment_refund']);
  742. }
  743. }
  744. /**
  745. * 处理余额退款
  746. */
  747. private function processBalanceCancelRefund(Order $order, float $amount): void
  748. {
  749. $wallet = $order->user->wallet;
  750. // 更新钱包余额
  751. $wallet->increment('total_balance', $amount);
  752. $wallet->increment('available_balance', $amount);
  753. // 创建钱包交易记录
  754. $wallet->transRecords()->create([
  755. 'amount' => $amount,
  756. 'trans_type' => 'cancel_refund',
  757. 'owner_type' => Order::class,
  758. 'owner_id' => $order->id,
  759. 'remark' => '订单取消退款',
  760. 'before_balance' => $wallet->total_balance - $amount,
  761. 'after_balance' => $wallet->total_balance,
  762. 'state' => 'success',
  763. ]);
  764. }
  765. /**
  766. * 处理支付退款
  767. */
  768. private function processPaymentCancelRefund(Order $order, float $amount): void
  769. {
  770. // TODO: 实现第三方支付退款逻辑
  771. // $paymentService = app(PaymentService::class);
  772. // $paymentService->refund($order->payment_no, $amount);
  773. }
  774. /**
  775. * 更新订单取消状态
  776. */
  777. private function updateOrderCancelStatus(Order $order): void
  778. {
  779. $order->update([
  780. 'state' => OrderStatus::CANCELLED->value,
  781. ]);
  782. }
  783. /**
  784. * 创建取消记录
  785. */
  786. private function createCancelRecords(Order $order, array $cancelAmounts, ?string $reason): void
  787. {
  788. // TODO: 创建取消记录
  789. // 创建取消记录
  790. // $order->cancelRecords()->create([
  791. // 'total_refund_amount' => $cancelAmounts['total_refund'],
  792. // 'balance_refund_amount' => $cancelAmounts['balance_refund'],
  793. // 'payment_refund_amount' => $cancelAmounts['payment_refund'],
  794. // 'penalty_amount' => $cancelAmounts['penalty_amount'],
  795. // 'remark' => $reason ?? '',
  796. // 'state' => 'success',
  797. // ]);
  798. // 创建订单状态记录
  799. OrderRecord::create([
  800. 'order_id' => $order->id,
  801. 'object_id' => Auth::id(),
  802. 'object_type' => MemberUser::class,
  803. 'state' => OrderRecordStatus::CANCELLED->value,
  804. 'remark' => "用户取消订单:{$reason}",
  805. ]);
  806. }
  807. /**
  808. * 发送取消通知
  809. */
  810. private function notifyCancel(Order $order): void
  811. {
  812. // TODO: 实现取消通知逻辑
  813. // 1. ��知用户
  814. // event(new OrderCancelledEvent($order));
  815. // 2. 通知技师
  816. if ($order->coach_id) {
  817. // event(new OrderCancelledToCoachEvent($order));
  818. }
  819. }
  820. /**
  821. * 结束订单
  822. */
  823. public function finishOrder(int $userId, int $orderId): array
  824. {
  825. return DB::transaction(function () use ($userId, $orderId) {
  826. try {
  827. // 1. 验证用户和订单
  828. $order = $this->validateOrderForFinish($userId, $orderId);
  829. // 2. 验证技师状���
  830. $coach = $this->validateCoach($order->coach_id);
  831. // 4. 完成订单
  832. $this->completeOrder($order, $userId);
  833. // 5. ���知技师
  834. // event(new OrderFinishedEvent($order));
  835. return ['message' => '订单已完成'];
  836. } catch (Exception $e) {
  837. $this->logFinishOrderError($e, $userId, $orderId);
  838. throw $e;
  839. }
  840. });
  841. }
  842. /**
  843. * 验证订单完成条件
  844. */
  845. private function validateOrderForFinish(int $userId, int $orderId): Order
  846. {
  847. // 验证用户状态
  848. $user = MemberUser::where('id', $userId)
  849. ->where('state', UserStatus::OPEN->value)
  850. ->firstOrFail();
  851. // 验证订单状态
  852. $order = Order::where('user_id', $userId)
  853. ->where('id', $orderId)
  854. ->where('state', OrderStatus::SERVICING->value)
  855. ->lockForUpdate()
  856. ->firstOrFail();
  857. return $order;
  858. }
  859. /**
  860. * 完成订单
  861. */
  862. private function completeOrder(Order $order, int $userId): void
  863. {
  864. // 1. 创建订单记录
  865. OrderRecord::create([
  866. 'order_id' => $order->id,
  867. 'object_id' => $userId,
  868. 'object_type' => MemberUser::class,
  869. 'state' => OrderRecordStatus::COMPLETED->value,
  870. 'remark' => '服务完成',
  871. ]);
  872. // 2. 更新订单状态
  873. $order->state = OrderStatus::COMPLETED->value;
  874. $order->save();
  875. }
  876. /**
  877. * 记录订单完成错误日志
  878. */
  879. private function logFinishOrderError(Exception $e, int $userId, int $orderId): void
  880. {
  881. Log::error('结束订单失败:', [
  882. 'message' => $e->getMessage(),
  883. 'user_id' => $userId,
  884. 'order_id' => $orderId,
  885. 'trace' => $e->getTraceAsString(),
  886. ]);
  887. }
  888. /**
  889. * 确认技师离开
  890. */
  891. public function confirmLeave(int $userId, int $orderId): array
  892. {
  893. return DB::transaction(function () use ($userId, $orderId) {
  894. try {
  895. // 1. 参数校验
  896. $order = Order::where('user_id', $userId)
  897. ->where('id', $orderId)
  898. ->where('state', OrderStatus::SERVICE_END->value) // 订单状态必须是服务结束
  899. ->firstOrFail();
  900. if (! $order) {
  901. throw new Exception('订单不能撤离');
  902. }
  903. // 2. 添加订单撤离记录
  904. OrderRecord::create([
  905. 'order_id' => $orderId,
  906. 'object_id' => $userId,
  907. 'object_type' => MemberUser::class,
  908. 'state' => OrderRecordStatus::LEFT->value,
  909. 'remark' => '技师已离开',
  910. ]);
  911. // 3. 修改订单状态为撤离
  912. $order->state = OrderStatus::LEFT->value;
  913. $order->save();
  914. return ['message' => '已确技师离开'];
  915. } catch (Exception $e) {
  916. Log::error('确认技师离开失败:', [
  917. 'message' => $e->getMessage(),
  918. 'user_id' => $userId,
  919. 'order_id' => $orderId,
  920. ]);
  921. throw $e;
  922. }
  923. });
  924. }
  925. /**
  926. * 获取订单列表
  927. *
  928. * @param int $userId 用户ID
  929. * @param array{
  930. * state: ?string,
  931. * start_date: ?string,
  932. * end_date: ?string,
  933. * project_id: ?int,
  934. * coach_id: ?int,
  935. * order_no: ?string,
  936. * per_page: ?int,
  937. * page: ?int
  938. * } $filters 过滤条件
  939. *
  940. * @throws \Exception
  941. */
  942. public function getOrderList(int $userId, array $filters = [])
  943. {
  944. try {
  945. // 1. 验证用户
  946. $user = $this->validateUserForQuery($userId);
  947. // 2. 构建基础查询
  948. $query = $this->buildOrderListQuery($user);
  949. // 3. 应用过滤条件
  950. $this->applyOrderFilters($query, $filters);
  951. // 4. 加载关联数据并分页
  952. return $this->paginateOrderList($query, $filters);
  953. } catch (Exception $e) {
  954. $this->logError('获取订单列表失败', $e, [
  955. 'user_id' => $userId,
  956. 'filters' => $filters,
  957. ]);
  958. throw $e;
  959. }
  960. }
  961. /**
  962. * 验证用户查询权限
  963. */
  964. private function validateUserForQuery(int $userId): MemberUser
  965. {
  966. return MemberUser::where('id', $userId)
  967. ->where('state', UserStatus::OPEN->value)
  968. ->firstOrFail();
  969. }
  970. /**
  971. * 构建订单列表基础查询
  972. */
  973. private function buildOrderListQuery(MemberUser $user)
  974. {
  975. return $user->orders()
  976. ->with([
  977. 'coach' => function ($query) {
  978. $query->with(['info' => function ($q) {
  979. $q->select(['id', 'coach_id', 'nickname', 'avatar', 'gender']);
  980. }]);
  981. },
  982. 'project:id,title,subtitle,duration,cover',
  983. 'records' => function ($query) {
  984. $query->orderBy('created_at', 'desc')
  985. ->select(['id', 'order_id', 'state', 'remark', 'created_at']);
  986. },
  987. // 'evaluation:id,order_id,score,content,created_at',
  988. ]);
  989. }
  990. /**
  991. * 分页获取订���列表
  992. */
  993. private function paginateOrderList($query, array $filters)
  994. {
  995. $paginatedOrders = $query->orderBy('created_at', 'desc')
  996. ->paginate(
  997. perPage: $filters['per_page'] ?? 10,
  998. page: $filters['page'] ?? 1
  999. );
  1000. $formattedOrders = $paginatedOrders->map(function ($order) {
  1001. return $this->formatOrderListItem($order);
  1002. });
  1003. return [
  1004. 'items' => $formattedOrders,
  1005. 'total' => $paginatedOrders->total(),
  1006. ];
  1007. }
  1008. /**
  1009. * 格式化订单列表项
  1010. */
  1011. private function formatOrderListItem(Order $order): array
  1012. {
  1013. return [
  1014. 'order_id' => $order->id,
  1015. 'order_no' => $order->order_no,
  1016. 'state' => $order->state,
  1017. 'state_description' => $this->getOrderStateDescription($order->state),
  1018. 'total_amount' => $order->total_amount,
  1019. 'service_time' => $order->service_time,
  1020. 'created_at' => $order->created_at->toDateTimeString(),
  1021. 'coach' => $order->coach ? [
  1022. 'id' => $order->coach->id,
  1023. 'nickname' => $order->coach->info->nickname,
  1024. 'avatar' => $order->coach->info->avatar,
  1025. ] : null,
  1026. 'project' => $order->project ? [
  1027. 'id' => $order->project->id,
  1028. 'title' => $order->project->title,
  1029. 'subtitle' => $order->project->subtitle,
  1030. 'duration' => $order->project->duration,
  1031. 'cover_image' => $order->project->cover_image,
  1032. ] : null,
  1033. 'address' => $order->address ? [
  1034. 'location' => $order->location,
  1035. 'address' => $order->address,
  1036. ] : null,
  1037. 'latest_record' => $order->records->first() ? [
  1038. 'state' => $order->records->first()->state,
  1039. 'remark' => $order->records->first()->remark,
  1040. 'created_at' => $order->records->first()->created_at->format('Y-m-d H:i:s'),
  1041. ] : null,
  1042. 'evaluation' => $order->evaluation ? [
  1043. 'score' => $order->evaluation->score,
  1044. 'content' => $order->evaluation->content,
  1045. ] : null,
  1046. ];
  1047. }
  1048. /**
  1049. * 获取订单详情
  1050. *
  1051. * @param int $userId 用户ID
  1052. * @param int $orderId 订单ID
  1053. * @return array 订单详情
  1054. *
  1055. * @throws \Exception
  1056. */
  1057. public function getOrderDetail(int $userId, int $orderId): array
  1058. {
  1059. try {
  1060. // 1. 验证订单权限
  1061. $order = $this->validateOrderAccess($userId, $orderId);
  1062. // 2. 加载订单关联数据
  1063. $order->load([
  1064. 'coach.info' => function ($query) {
  1065. $query->select(['id', 'coach_id', 'nickname', 'avatar', 'gender']);
  1066. },
  1067. 'project:id,title,subtitle,duration,cover',
  1068. 'records' => function ($query) {
  1069. $query->orderBy('created_at', 'desc')
  1070. ->select(['id', 'order_id', 'state', 'remark', 'created_at']);
  1071. },
  1072. // 'evaluation' => function ($query) {
  1073. // $query->select(['id', 'order_id', 'score', 'content', 'images', 'created_at']);
  1074. // },
  1075. ]);
  1076. // 3. 格式化返回数据
  1077. return $this->formatOrderDetail($order);
  1078. } catch (Exception $e) {
  1079. $this->logError('获取订单详情失败', $e, [
  1080. 'user_id' => $userId,
  1081. 'order_id' => $orderId,
  1082. ]);
  1083. throw $e;
  1084. }
  1085. }
  1086. /**
  1087. * 验证订单访问权限
  1088. */
  1089. private function validateOrderAccess(int $userId, int $orderId): Order
  1090. {
  1091. return Order::where('id', $orderId)
  1092. ->where('user_id', $userId)
  1093. ->firstOrFail();
  1094. }
  1095. /**
  1096. * 格式化订单详情数据
  1097. */
  1098. private function formatOrderDetail(Order $order): array
  1099. {
  1100. return [
  1101. 'order_id' => $order->id,
  1102. 'order_no' => $order->order_no,
  1103. 'state' => $order->state,
  1104. 'state_description' => $this->getOrderStateDescription($order->state),
  1105. 'total_amount' => $order->total_amount,
  1106. 'balance_amount' => $order->balance_amount,
  1107. 'pay_amount' => $order->pay_amount,
  1108. 'delivery_fee' => $order->delivery_fee,
  1109. 'project_amount' => $order->project_amount,
  1110. 'coupon_amount' => $order->coupon_amount,
  1111. 'service_time' => $order->service_time,
  1112. 'created_at' => $order->created_at->toDateTimeString(),
  1113. 'remark' => $order->remark,
  1114. 'coach' => $order->coach ? [
  1115. 'id' => $order->coach->id,
  1116. 'nickname' => $order->coach->info->nickname,
  1117. 'avatar' => $order->coach->info->avatar,
  1118. 'gender' => $order->coach->info->gender,
  1119. 'score' => $order->coach->info?->score,
  1120. ] : null,
  1121. 'project' => $order->project ? [
  1122. 'id' => $order->project->id,
  1123. 'title' => $order->project->title,
  1124. 'subtitle' => $order->project->subtitle,
  1125. 'duration' => $order->project->duration,
  1126. 'cover' => $order->project->cover,
  1127. ] : null,
  1128. 'address' => $order->address ? [
  1129. 'id' => $order->address_id,
  1130. 'location' => $order->location,
  1131. 'address' => $order->address,
  1132. ] : null,
  1133. 'records' => $order->records->map(function ($record) {
  1134. return [
  1135. 'id' => $record->id,
  1136. 'state' => $record->state,
  1137. 'remark' => $record->remark,
  1138. 'created_at' => $record->created_at->format('Y-m-d H:i:s'),
  1139. ];
  1140. })->toArray(),
  1141. // 'evaluation' => $order->evaluation ? [
  1142. // 'score' => $order->evaluation->score,
  1143. // 'content' => $order->evaluation->content,
  1144. // 'images' => $order->evaluation->images,
  1145. // 'created_at' => $order->evaluation->created_at->format('Y-m-d H:i:s'),
  1146. // ] : null,
  1147. ];
  1148. }
  1149. /**
  1150. * 应用订单过滤条件
  1151. */
  1152. private function applyOrderFilters($query, array $filters): void
  1153. {
  1154. // 按状态筛选
  1155. if (! empty($filters['state'])) {
  1156. $query->where('state', $filters['state']);
  1157. }
  1158. // 按时间范围筛选
  1159. if (! empty($filters['start_date'])) {
  1160. $query->where('created_at', '>=', $filters['start_date']);
  1161. }
  1162. if (! empty($filters['end_date'])) {
  1163. $query->where('created_at', '<=', $filters['end_date']);
  1164. }
  1165. // 按服务类型筛选
  1166. if (! empty($filters['project_id'])) {
  1167. $query->where('project_id', $filters['project_id']);
  1168. }
  1169. // 按技师筛选
  1170. if (! empty($filters['coach_id'])) {
  1171. $query->where('coach_id', $filters['coach_id']);
  1172. }
  1173. // 按订单号搜索
  1174. if (! empty($filters['order_no'])) {
  1175. $query->where('order_no', 'like', "%{$filters['order_no']}%");
  1176. }
  1177. }
  1178. /**
  1179. * 获取订单状态描述
  1180. */
  1181. private function getOrderStateDescription(string $state): string
  1182. {
  1183. return match ($state) {
  1184. OrderStatus::CREATED->value => '待支付',
  1185. OrderStatus::PAID->value => '待接单',
  1186. OrderStatus::ACCEPTED->value => '已接单',
  1187. OrderStatus::DEPARTED->value => '已出发',
  1188. OrderStatus::ARRIVED->value => '已到达',
  1189. OrderStatus::SERVICING->value => '服务中',
  1190. OrderStatus::SERVICE_END->value => '已完成',
  1191. OrderStatus::CANCELLED->value => '已取消',
  1192. OrderStatus::REFUNDED->value => '已退款',
  1193. default => '未知状态',
  1194. };
  1195. }
  1196. /**
  1197. * 订单退款
  1198. *
  1199. * @param int $orderId 订单ID
  1200. * @return array{message: string}
  1201. *
  1202. * @throws \Exception
  1203. */
  1204. public function refundOrder(int $orderId): array
  1205. {
  1206. return DB::transaction(function () use ($orderId) {
  1207. try {
  1208. // 1. 验证订单状态
  1209. $order = $this->validateOrderForRefund($orderId);
  1210. // 2. 计算退款金额
  1211. $refundAmounts = $this->calculateRefundAmount($order);
  1212. // 3. 处理退款
  1213. $this->processRefund($order, $refundAmounts);
  1214. // 4. 更新订单状态
  1215. $this->updateOrderRefundStatus($order);
  1216. // 5. 记录退款操作
  1217. $this->createRefundRecords($order, $refundAmounts);
  1218. // 6. 通知相关方
  1219. $this->notifyRefund($order);
  1220. return ['message' => '退款成功'];
  1221. } catch (Exception $e) {
  1222. $this->logError('订单退款失败', $e, [
  1223. 'order_id' => $orderId,
  1224. ]);
  1225. throw $e;
  1226. }
  1227. });
  1228. }
  1229. /**
  1230. * 验证订单退款条件
  1231. */
  1232. private function validateOrderForRefund(int $orderId): Order
  1233. {
  1234. // 获取并锁定订单,防止并发操作
  1235. $order = Order::where('id', $orderId)
  1236. ->whereIn('state', [
  1237. OrderStatus::PAID->value,
  1238. OrderStatus::ACCEPTED->value,
  1239. OrderStatus::DEPARTED->value,
  1240. ])
  1241. ->lockForUpdate()
  1242. ->firstOrFail();
  1243. // 验证当前用户是否为订单所有者
  1244. $user = Auth::user();
  1245. abort_if($order->user_id !== $user->id, 403, '无权操作此订单');
  1246. // 检查订单是否已经退款
  1247. abort_if($order->state === OrderStatus::REFUNDED->value, 400, '订单已退款');
  1248. return $order;
  1249. }
  1250. /**
  1251. * 计算退款金额
  1252. */
  1253. private function calculateRefundAmount(Order $order): array
  1254. {
  1255. // 初始化扣除金额
  1256. $deductAmount = 0;
  1257. // 根据订单状态计算违约金
  1258. switch ($order->state) {
  1259. case OrderStatus::ACCEPTED->value:
  1260. // 已接单状态扣除订单金额的20%作为违约金
  1261. $deductAmount = bcmul($order->total_amount, '0.2', 2);
  1262. break;
  1263. case OrderStatus::DEPARTED->value:
  1264. // 已出发状态扣除订单金额的50%作为违约金
  1265. $deductAmount = bcmul($order->total_amount, '0.5', 2);
  1266. break;
  1267. }
  1268. // 计算实际可退金额(总金额减去违约金)
  1269. $totalRefund = bcsub($order->total_amount, $deductAmount, 2);
  1270. // 优先退还用户使用的余额部分
  1271. $balanceRefund = min($order->balance_amount, $totalRefund);
  1272. // 剩余部分退还到支付账户
  1273. $paymentRefund = bcsub($totalRefund, $balanceRefund, 2);
  1274. return [
  1275. 'total_refund' => $totalRefund, // 总退款金额
  1276. 'balance_amount' => $balanceRefund, // 退还到余额的金额
  1277. 'payment_refund' => $paymentRefund, // 退还到支付账户的金额
  1278. 'deduct_amount' => $deductAmount, // 扣除的违约金
  1279. ];
  1280. }
  1281. /**
  1282. * 处理退款操作
  1283. */
  1284. private function processRefund(Order $order, array $refundAmounts): void
  1285. {
  1286. // 处理余额退款部分
  1287. if ($refundAmounts['balance_refund'] > 0) {
  1288. $this->processBalanceRefund($order, $refundAmounts['balance_refund']);
  1289. }
  1290. // 处理支付退款部分
  1291. if ($refundAmounts['payment_refund'] > 0) {
  1292. $this->processPaymentRefund($order, $refundAmounts['payment_refund']);
  1293. }
  1294. }
  1295. /**
  1296. * 处理余额退款
  1297. */
  1298. private function processBalanceRefund(Order $order, float $amount): void
  1299. {
  1300. $wallet = $order->user->wallet;
  1301. // 增加用户钱包总余额
  1302. $wallet->increment('total_balance', $amount);
  1303. // 增加可用余额
  1304. $wallet->increment('available_balance', $amount);
  1305. // 创建退款交易记录
  1306. $wallet->transRecords()->create([
  1307. 'amount' => $amount,
  1308. 'trans_type' => 'refund',
  1309. 'owner_type' => Order::class,
  1310. 'owner_id' => $order->id,
  1311. 'remark' => '订单退款',
  1312. 'before_balance' => $wallet->total_balance - $amount,
  1313. 'after_balance' => $wallet->total_balance,
  1314. 'state' => 'success',
  1315. ]);
  1316. }
  1317. /**
  1318. * 处理支付退款
  1319. */
  1320. private function processPaymentRefund(Order $order, float $amount): void
  1321. {
  1322. // TODO: 实现第三方支付退款逻辑
  1323. // $paymentService = app(PaymentService::class);
  1324. // $paymentService->refund($order->payment_no, $amount);
  1325. }
  1326. /**
  1327. * 更新订单退款状态
  1328. */
  1329. private function updateOrderRefundStatus(Order $order): void
  1330. {
  1331. $order->update([
  1332. 'state' => OrderStatus::REFUNDED->value,
  1333. 'refund_time' => now(),
  1334. ]);
  1335. }
  1336. /**
  1337. * 创建退款记录
  1338. */
  1339. private function createRefundRecords(Order $order, array $refundAmounts): void
  1340. {
  1341. // 创建退款记录
  1342. $refundRecord = $order->refundRecords()->create([
  1343. 'total_refund_amount' => $refundAmounts['total_refund'],
  1344. 'balance_refund_amount' => $refundAmounts['balance_refund'],
  1345. 'payment_refund_amount' => $refundAmounts['payment_refund'],
  1346. 'deduct_amount' => $refundAmounts['deduct_amount'],
  1347. 'state' => 'success',
  1348. 'remark' => '用户申请退款',
  1349. ]);
  1350. // 创建订单状态记录
  1351. OrderRecord::create([
  1352. 'order_id' => $order->id,
  1353. 'object_id' => Auth::id(),
  1354. 'object_type' => MemberUser::class,
  1355. 'state' => OrderRecordStatus::REFUNDING->value,
  1356. 'remark' => '订单退款中',
  1357. ]);
  1358. }
  1359. /**
  1360. * 发送退款通知
  1361. */
  1362. private function notifyRefund(Order $order): void
  1363. {
  1364. // TODO: 实现退款通知逻辑
  1365. // 1. 通知用户
  1366. // event(new OrderRefundedEvent($order));
  1367. // 2. 通知技师
  1368. if ($order->coach_id) {
  1369. // event(new OrderRefundedToCoachEvent($order));
  1370. }
  1371. }
  1372. /**
  1373. * 获取代理商配置
  1374. */
  1375. public function getAgentConfig(int $agentId): array
  1376. {
  1377. $agent = AgentInfo::where('id', $agentId)
  1378. ->where('state', 'enable')
  1379. ->firstOrFail();
  1380. // $config = AgentConfig::where('agent_id', $agentId)->firstOrFail();
  1381. return [
  1382. // 'min_distance' => $config->min_distance,
  1383. // 'min_fee' => $config->min_fee,
  1384. // 'per_km_fee' => $config->per_km_fee
  1385. ];
  1386. }
  1387. /**
  1388. * 获取技师配置
  1389. */
  1390. public function getCoachConfig(int $coachId): array
  1391. {
  1392. $coach = CoachUser::where('id', $coachId)
  1393. ->where('state', 'enable')
  1394. ->where('auth_state', 'passed')
  1395. ->firstOrFail();
  1396. // $config = CoachConfig::where('coach_id', $coachId)->firstOrFail();
  1397. return [
  1398. // 'delivery_fee_type' => $config->delivery_fee_type,
  1399. // 'charge_delivery_fee' => $config->charge_delivery_fee
  1400. ];
  1401. }
  1402. /**
  1403. * 计算路费
  1404. *
  1405. * @param int $coachId 技师ID
  1406. * @param int $projectId 项目ID
  1407. * @param int|null $agentId 代理商ID
  1408. * @param float $distance 距离(公里)
  1409. * @return float 路费金额
  1410. *
  1411. * @throws \Exception
  1412. */
  1413. public function calculateDeliveryFee(
  1414. int $coachId,
  1415. int $projectId,
  1416. ?int $agentId,
  1417. float $distance
  1418. ): float {
  1419. try {
  1420. // 1. 验证基础参数
  1421. $this->validateDeliveryFeeParams($coachId, $projectId, $distance);
  1422. // 2. 获取计费规则
  1423. $feeRules = $this->getDeliveryFeeRules($coachId, $agentId);
  1424. // 3. ��算路费
  1425. return $this->calculateFeeByRules($distance, $feeRules);
  1426. } catch (Exception $e) {
  1427. $this->logError('计算路费失败', $e, [
  1428. 'coach_id' => $coachId,
  1429. 'project_id' => $projectId,
  1430. 'agent_id' => $agentId,
  1431. 'distance' => $distance,
  1432. ]);
  1433. throw $e;
  1434. }
  1435. }
  1436. /**
  1437. * 验证路费计算参数
  1438. */
  1439. private function validateDeliveryFeeParams(int $coachId, int $projectId, float $distance): void
  1440. {
  1441. // 验证技师状态
  1442. $coach = $this->validateCoach($coachId);
  1443. // 验证项目状态
  1444. $project = Project::where('id', $projectId)
  1445. ->where('state', ProjectStatus::OPEN->value)
  1446. ->firstOrFail();
  1447. // 验证距离有效性
  1448. abort_if($distance < 0, 400, '距离计算错误');
  1449. }
  1450. /**
  1451. * 获取路费计算规则
  1452. *
  1453. * @return array{
  1454. * min_distance: float,
  1455. * min_fee: float,
  1456. * per_km_fee: float,
  1457. * max_distance: float,
  1458. * max_fee: float,
  1459. * free_distance: float
  1460. * }
  1461. */
  1462. private function getDeliveryFeeRules(int $coachId, ?int $agentId): array
  1463. {
  1464. // 1. 获取系统默认规则
  1465. $defaultRules = $this->getDefaultDeliveryFeeRules();
  1466. // 2. 获取代理商规则(如果有)
  1467. $agentRules = $agentId ? $this->getAgentDeliveryFeeRules($agentId) : [];
  1468. // 3. 获取技师个性化规则
  1469. $coachRules = $this->getCoachDeliveryFeeRules($coachId);
  1470. // 4. 合并规则,优先级:技师 > 代理商 > 系统默认
  1471. return array_merge(
  1472. $defaultRules,
  1473. $agentRules,
  1474. $coachRules
  1475. );
  1476. }
  1477. /**
  1478. * 获取系统默认路费规则
  1479. */
  1480. private function getDefaultDeliveryFeeRules(): array
  1481. {
  1482. // TODO: 从配置表中获取系统默认路费规则
  1483. return [
  1484. 'min_distance' => 3.0, // 最小计费距离(公里)
  1485. 'min_fee' => 10.0, // 最小路费(元)
  1486. 'per_km_fee' => 3.0, // 每公里费用(元)
  1487. 'max_distance' => 120.0, // 最大服务距离(公里)
  1488. 'max_fee' => 50.0, // 最大路费(元)
  1489. 'free_distance' => 1.0, // 免费服务距离(公里)
  1490. ];
  1491. }
  1492. /**
  1493. * 获取代理商路费规则
  1494. */
  1495. private function getAgentDeliveryFeeRules(int $agentId): array
  1496. {
  1497. // $agentConfig = AgentConfig::where('agent_id', $agentId)
  1498. // ->where('state', 'enable')
  1499. // ->first();
  1500. // if (! $agentConfig) {
  1501. // return [];
  1502. // }
  1503. // return [
  1504. // 'min_distance' => $agentConfig->min_distance,
  1505. // 'min_fee' => $agentConfig->min_fee,
  1506. // 'per_km_fee' => $agentConfig->per_km_fee,
  1507. // 'max_distance' => $agentConfig->max_distance,
  1508. // 'max_fee' => $agentConfig->max_fee,
  1509. // 'free_distance' => $agentConfig->free_distance,
  1510. // ];
  1511. return [];
  1512. }
  1513. /**
  1514. * 获取技师路费规则
  1515. */
  1516. private function getCoachDeliveryFeeRules(int $coachId): array
  1517. {
  1518. // TODO: 从配置表中获取技师个性化路费规则
  1519. // $coachConfig = CoachConfig::where('coach_id', $coachId)
  1520. // ->where('state', 'enable')
  1521. // ->first();
  1522. $coachConfig = null;
  1523. if (! $coachConfig) {
  1524. return [];
  1525. }
  1526. // 如果技师设置了不收取路费
  1527. if (! $coachConfig->charge_delivery_fee) {
  1528. return [
  1529. 'min_fee' => 0,
  1530. 'per_km_fee' => 0,
  1531. 'max_fee' => 0,
  1532. ];
  1533. }
  1534. return [
  1535. 'min_distance' => $coachConfig->min_distance,
  1536. 'min_fee' => $coachConfig->min_fee,
  1537. 'per_km_fee' => $coachConfig->per_km_fee,
  1538. 'max_distance' => $coachConfig->max_distance,
  1539. 'max_fee' => $coachConfig->max_fee,
  1540. 'free_distance' => $coachConfig->free_distance,
  1541. ];
  1542. }
  1543. /**
  1544. * 根据规则计算路费
  1545. *
  1546. * @param float $distance 距离(公里)
  1547. * @param array{
  1548. * min_distance: float, // 最小计费距离(公里)
  1549. * min_fee: float, // 小路费(元)
  1550. * per_km_fee: float, // 每公里费用(元)
  1551. * max_distance: float, // 最大服务距离(公里)
  1552. * max_fee: float, // 最大路费(元)
  1553. * free_distance: float // 免费服务距离(公里)
  1554. * } $rules 计费规则
  1555. * @return float 路费金额(元)
  1556. */
  1557. private function calculateFeeByRules(float $distance, array $rules): float
  1558. {
  1559. // 1. 检查是否超出最大服务距离
  1560. abort_if(
  1561. $distance > $rules['max_distance'],
  1562. 400,
  1563. sprintf('超出最大服务距离 %.1f 公里', $rules['max_distance'])
  1564. );
  1565. // 2. 检查是否在免费距离内
  1566. if ($distance <= $rules['free_distance']) {
  1567. return 0;
  1568. }
  1569. // 3. 计算实际计费距离
  1570. $chargeDistance = max(0, $distance - $rules['free_distance']);
  1571. // 4. 检查是否达到最小计费距离
  1572. if ($chargeDistance < $rules['min_distance']) {
  1573. return $rules['min_fee'];
  1574. }
  1575. // 5. 计算基础路费
  1576. $fee = bcmul($chargeDistance, $rules['per_km_fee'], 2);
  1577. // 6. 应用最小路费限制
  1578. $fee = max($fee, $rules['min_fee']);
  1579. // 7. 应用最大路费限制
  1580. $fee = min($fee, $rules['max_fee']);
  1581. return $fee;
  1582. }
  1583. /**
  1584. * 计算两点之间的距离
  1585. *
  1586. * @param float $lat1 起点纬度
  1587. * @param float $lng1 起点经度
  1588. * @param float $lat2 终点纬度
  1589. * @param float $lng2 终点经度
  1590. * @return float 距离(公里)
  1591. */
  1592. private function getDistance(float $lat1, float $lng1, float $lat2, float $lng2): float
  1593. {
  1594. // 将角度转为弧度
  1595. $radLat1 = deg2rad($lat1);
  1596. $radLat2 = deg2rad($lat2);
  1597. $radLng1 = deg2rad($lng1);
  1598. $radLng2 = deg2rad($lng2);
  1599. // 地球半径(公里)
  1600. $earthRadius = 6371;
  1601. // 计算距离
  1602. $distance = acos(
  1603. sin($radLat1) * sin($radLat2) +
  1604. cos($radLat1) * cos($radLat2) * cos($radLng1 - $radLng2)
  1605. ) * $earthRadius;
  1606. // 保留2位小数
  1607. return round($distance, 2);
  1608. }
  1609. /**
  1610. * 计算订单金额
  1611. *
  1612. * @param int $userId 用户ID
  1613. * @param int|null $addressId 地址ID
  1614. * @param int|null $coachId 技师ID
  1615. * @param int $projectId 项目ID
  1616. * @param int|null $agentId 代理商ID
  1617. * @param bool $useBalance 是否使用余额
  1618. * @param float $distance 距离
  1619. * @param float $lat 纬度
  1620. * @param float $lng 经度
  1621. * @return array{
  1622. * total_amount: float,
  1623. * balance_amount: float,
  1624. * pay_amount: float,
  1625. * coupon_amount: float,
  1626. * project_amount: float,
  1627. * delivery_fee: float
  1628. * }
  1629. *
  1630. * @throws \Exception
  1631. */
  1632. public function calculateOrderAmount(
  1633. int $userId,
  1634. ?int $addressId,
  1635. ?int $coachId,
  1636. int $projectId,
  1637. ?int $agentId = null,
  1638. bool $useBalance = false,
  1639. float $distance = 0,
  1640. float $lat = 0,
  1641. float $lng = 0
  1642. ): array {
  1643. try {
  1644. // 1. 验证用户状态和权限
  1645. $user = $this->validateUserForCalculation($userId);
  1646. // 2. 获取项目信息和价格(包含代理价格)
  1647. $project = $this->getProjectWithPrice($projectId, $agentId);
  1648. // 3. 计算路费(如果有技师)
  1649. $deliveryFee = $this->calculateDeliveryFeeForOrder(
  1650. coachId: $coachId,
  1651. projectId: $projectId,
  1652. agentId: $agentId,
  1653. distance: $distance,
  1654. addressId: $addressId,
  1655. lat: $lat,
  1656. lng: $lng
  1657. );
  1658. // 4. 计算优惠金额(优惠券、活动等)
  1659. $discountAmount = $this->calculateDiscountAmount($projectId, $userId);
  1660. // 5. 计算订单总金额(项目价格 + 路费 - 优惠金额)
  1661. $totalAmount = $this->calculateTotalAmount($project->price, $deliveryFee, $discountAmount);
  1662. // 6. 处理余额支付分配(优先使用余额)
  1663. $paymentAmounts = $this->calculatePaymentDistribution(
  1664. user: $user,
  1665. totalAmount: $totalAmount,
  1666. useBalance: $useBalance
  1667. );
  1668. // 7. 返回计算结果
  1669. return [
  1670. 'total_amount' => $totalAmount, // 订单总金额
  1671. 'balance_amount' => $paymentAmounts['balance_amount'], // 余额支付金额
  1672. 'pay_amount' => $paymentAmounts['pay_amount'], // 需要支付金额
  1673. 'coupon_amount' => $discountAmount, // 优惠金额
  1674. 'project_amount' => $project->price, // 项目原价
  1675. 'delivery_fee' => $deliveryFee, // 路费
  1676. ];
  1677. } catch (Exception $e) {
  1678. // 记录错误日志
  1679. $this->logError('计算订单金额失败', $e, [
  1680. 'user_id' => $userId,
  1681. 'project_id' => $projectId,
  1682. 'coach_id' => $coachId,
  1683. 'address_id' => $addressId,
  1684. ]);
  1685. throw $e;
  1686. }
  1687. }
  1688. /**
  1689. * 验证用户状态
  1690. */
  1691. private function validateUserForCalculation(int $userId): MemberUser
  1692. {
  1693. $user = MemberUser::with('wallet')
  1694. ->where('id', $userId)
  1695. ->where('state', UserStatus::OPEN->value)
  1696. ->first();
  1697. abort_if(! $user, 404, '用户不存在或状态异常');
  1698. return $user;
  1699. }
  1700. /**
  1701. * 获取项目信息和价格
  1702. */
  1703. private function getProjectWithPrice(int $projectId, ?int $agentId): Project
  1704. {
  1705. // 获取基础项目信息
  1706. $project = Project::where('id', $projectId)
  1707. ->where('state', ProjectStatus::OPEN->value)
  1708. ->first();
  1709. abort_if(! $project, 404, '项目不存在或状态异常');
  1710. // 如果有代理商,获取代理商价格
  1711. if ($agentId) {
  1712. $agentProject = $this->getAgentProjectPrice($agentId, $projectId);
  1713. if ($agentProject) {
  1714. $project->price = $agentProject->price;
  1715. }
  1716. }
  1717. return $project;
  1718. }
  1719. /**
  1720. * 获取代理商项目价格
  1721. */
  1722. private function getAgentProjectPrice(int $agentId, int $projectId): ?object
  1723. {
  1724. return DB::table('agent_project')
  1725. ->where('agent_id', $agentId)
  1726. ->where('project_id', $projectId)
  1727. ->where('state', 'enable')
  1728. ->first();
  1729. }
  1730. /**
  1731. * 计算订单路费
  1732. */
  1733. private function calculateDeliveryFeeForOrder(
  1734. ?int $coachId,
  1735. int $projectId,
  1736. ?int $agentId,
  1737. float $distance,
  1738. ?int $addressId,
  1739. ?float $lat,
  1740. ?float $lng
  1741. ): float {
  1742. // 如果没有技师,无需计算路费
  1743. if (! $coachId) {
  1744. return 0;
  1745. }
  1746. if (! $addressId && ! $lat && ! $lng) {
  1747. return 0;
  1748. }
  1749. // 如果距离为0,需要重新计算距离
  1750. if ($distance <= 0) {
  1751. $distance = $this->calculateDistance($coachId, $addressId, $lat, $lng);
  1752. }
  1753. return $this->calculateDeliveryFee($coachId, $projectId, $agentId, $distance);
  1754. }
  1755. /**
  1756. * 获取技师位置信息
  1757. */
  1758. private function getCoachLocation(int $coachId): array
  1759. {
  1760. // 从技师服务获取最新位置信息
  1761. $location = $this->coachService->getLocation($coachId);
  1762. // 如果没有位置信息,返回系统默认位置
  1763. if (empty($location)) {
  1764. return [
  1765. 'latitude' => config('business.default_latitude', 0),
  1766. 'longitude' => config('business.default_longitude', 0),
  1767. ];
  1768. }
  1769. // 格式化位置数据
  1770. return [
  1771. 'latitude' => (float) $location->latitude,
  1772. 'longitude' => (float) $location->longitude,
  1773. ];
  1774. }
  1775. /**
  1776. * 计算技师与服务地址的距离
  1777. */
  1778. private function calculateDistance(int $coachId, int $addressId, float $lat, float $lng): float
  1779. {
  1780. try {
  1781. // 如果提供了地址ID,优先使用地址表中的经纬度
  1782. if ($addressId > 0) {
  1783. $address = MemberAddress::find($addressId);
  1784. // 使用地址的经纬度,如果没有则使用传入的经纬度
  1785. $lat = $address?->latitude ?? $lat;
  1786. $lng = $address?->longitude ?? $lng;
  1787. }
  1788. // 从Redis获取技师的两个位置点信息
  1789. // 获取常用地址位置(如家庭地址)
  1790. $homeLocation = Redis::geopos(
  1791. 'coach_locations',
  1792. $coachId . '_' . TechnicianLocationType::COMMON->value
  1793. );
  1794. // 获取当前实时位置
  1795. $workLocation = Redis::geopos(
  1796. 'coach_locations',
  1797. $coachId . '_' . TechnicianLocationType::CURRENT->value
  1798. );
  1799. $distances = [];
  1800. // 计算与常用地址的距离
  1801. if ($homeLocation && $homeLocation[0]) {
  1802. $distances[] = $this->getDistance($homeLocation[0][1], $homeLocation[0][0], $lat, $lng);
  1803. }
  1804. // 计算与当前位置的距离
  1805. if ($workLocation && $workLocation[0]) {
  1806. $distances[] = $this->getDistance($workLocation[0][1], $workLocation[0][0], $lat, $lng);
  1807. }
  1808. // 如果有有效距离,返回最小值;否则返回0
  1809. return ! empty($distances) ? min($distances) : 0.0;
  1810. } catch (\Exception $e) {
  1811. Log::error('计算距离失败', [
  1812. 'error' => $e->getMessage(),
  1813. 'coach_id' => $coachId,
  1814. 'address_id' => $addressId,
  1815. 'lat' => $lat,
  1816. 'lng' => $lng,
  1817. ]);
  1818. return 0.0;
  1819. }
  1820. }
  1821. /**
  1822. * 计算优惠金额
  1823. */
  1824. private function calculateDiscountAmount(int $projectId, int $userId): float
  1825. {
  1826. $discountAmount = 0;
  1827. // TODO: 实现优惠券、活动等优惠计算逻辑
  1828. return $discountAmount;
  1829. }
  1830. /**
  1831. * 计算订单总金额
  1832. */
  1833. private function calculateTotalAmount(float $projectPrice, float $deliveryFee, float $discountAmount): float
  1834. {
  1835. $totalAmount = bcadd($projectPrice, $deliveryFee, 2);
  1836. $totalAmount = bcsub($totalAmount, $discountAmount, 2);
  1837. return max(0, $totalAmount);
  1838. }
  1839. /**
  1840. * 计算支付金额分配
  1841. *
  1842. * @return array{balance_amount: float, pay_amount: float}
  1843. */
  1844. private function calculatePaymentDistribution(MemberUser $user, float $totalAmount, bool $useBalance): array
  1845. {
  1846. if (! $useBalance || $totalAmount <= 0) {
  1847. return [
  1848. 'balance_amount' => 0,
  1849. 'pay_amount' => $totalAmount,
  1850. ];
  1851. }
  1852. $availableBalance = $user->wallet?->available_balance ?? 0;
  1853. if ($availableBalance >= $totalAmount) {
  1854. return [
  1855. 'balance_amount' => $totalAmount,
  1856. 'pay_amount' => 0,
  1857. ];
  1858. }
  1859. return [
  1860. 'balance_amount' => $availableBalance,
  1861. 'pay_amount' => bcsub($totalAmount, $availableBalance, 2),
  1862. ];
  1863. }
  1864. /**
  1865. * 获取订单抢单池列表
  1866. *
  1867. * @param int $orderId 订单ID
  1868. * @return array 抢单池列表
  1869. */
  1870. public function getOrderGrabList(int $orderId): array
  1871. {
  1872. try {
  1873. // 查询订单信息
  1874. $order = Order::where('id', $orderId)
  1875. ->whereIn('state', [OrderStatus::CREATED->value])
  1876. ->firstOrFail();
  1877. // 查询抢单池���表
  1878. $grabList = $order->grabRecords()->with(['coach.info'])->get();
  1879. // 格式化返回数据
  1880. $result = [];
  1881. foreach ($grabList as $grab) {
  1882. $coach = $grab->coach;
  1883. $result[] = [
  1884. 'id' => $grab->id,
  1885. 'coach_id' => $coach->id,
  1886. 'nickname' => $coach->info->nickname,
  1887. 'avatar' => $coach->info->avatar,
  1888. 'distance' => $grab->distance,
  1889. 'created_at' => $grab->created_at->format('Y-m-d H:i:s'),
  1890. ];
  1891. }
  1892. return $result;
  1893. } catch (\Exception $e) {
  1894. Log::error('获取订单抢单池列表失败', [
  1895. 'error' => $e->getMessage(),
  1896. 'order_id' => $orderId,
  1897. ]);
  1898. throw $e;
  1899. }
  1900. }
  1901. /**
  1902. * 指定技师
  1903. *
  1904. * @param int $userId 用户ID
  1905. * @param int $orderId 订单ID
  1906. * @param int $coachId 技师ID
  1907. *
  1908. * @throws \Exception
  1909. */
  1910. public function assignCoach(int $userId, int $orderId, int $coachId): bool
  1911. {
  1912. return DB::transaction(function () use ($userId, $orderId, $coachId) {
  1913. try {
  1914. // 1. 验证参数
  1915. $order = $this->validateAssignOrder($userId, $orderId);
  1916. // 2. 验证技师
  1917. $coach = $this->validateCoach($coachId);
  1918. // 3. 检查抢单池状态
  1919. $this->validateGrabPool($order);
  1920. // 4. 更新订单信息
  1921. $this->updateOrderForAssign($order, $coachId);
  1922. // 5. 创建订单记录(指派)
  1923. $this->createAssignRecord($order, $userId);
  1924. // 6. 处理支付
  1925. if ($order->payment_type === PaymentMethod::BALANCE->value) {
  1926. $this->handleBalancePaymentForAssign($order, $userId, $coachId);
  1927. }
  1928. // 7. 发送通知
  1929. $this->notifyAssignment($order, $coach);
  1930. return true;
  1931. } catch (Exception $e) {
  1932. $this->logError('指定技师失败', $e, [
  1933. 'user_id' => $userId,
  1934. 'order_id' => $orderId,
  1935. 'coach_id' => $coachId,
  1936. ]);
  1937. throw $e;
  1938. }
  1939. });
  1940. }
  1941. /**
  1942. * 验证指定技师的订单条件
  1943. */
  1944. private function validateAssignOrder(int $userId, int $orderId): Order
  1945. {
  1946. // 验证用户状态
  1947. $user = MemberUser::where('id', $userId)
  1948. ->where('state', UserStatus::OPEN->value)
  1949. ->firstOrFail();
  1950. // 验证订单状态
  1951. $order = $user->orders()
  1952. ->where('id', $orderId)
  1953. ->whereIn('state', [OrderStatus::CREATED->value, OrderStatus::PAID->value])
  1954. ->lockForUpdate()
  1955. ->firstOrFail();
  1956. return $order;
  1957. }
  1958. /**
  1959. * 验证抢单池状态
  1960. */
  1961. private function validateGrabPool(Order $order): void
  1962. {
  1963. // 检查抢单池是否已有抢单成功记录
  1964. $existsGrabSuccess = $order->grabRecords()
  1965. ->where('state', OrderGrabRecordStatus::SUCCEEDED->value)
  1966. ->exists();
  1967. abort_if($existsGrabSuccess, 400, '该订单已抢单完成');
  1968. }
  1969. /**
  1970. * 更新订单信息
  1971. */
  1972. private function updateOrderForAssign(Order $order, int $coachId): void
  1973. {
  1974. // 修改订单技师
  1975. $order->coach_id = $coachId;
  1976. // 待支付订单需要重新计算金额
  1977. if ($order->state === OrderStatus::CREATED->value) {
  1978. $amounts = $this->calculateOrderAmount(
  1979. userId: $order->user_id,
  1980. addressId: $order->address_id,
  1981. coachId: $coachId,
  1982. projectId: $order->project_id,
  1983. agentId: $order->agent_id,
  1984. useBalance: $order->payment_type === PaymentMethod::BALANCE->value
  1985. );
  1986. // 更新订单金额
  1987. $order->fill([
  1988. 'total_amount' => $amounts['total_amount'],
  1989. 'balance_amount' => $amounts['balance_amount'],
  1990. 'pay_amount' => $amounts['pay_amount'],
  1991. 'discount_amount' => $amounts['coupon_amount'],
  1992. 'project_amount' => $amounts['project_amount'],
  1993. 'traffic_amount' => $amounts['delivery_fee'],
  1994. ]);
  1995. }
  1996. $order->save();
  1997. }
  1998. /**
  1999. * 处理指派订单的余额支付
  2000. */
  2001. private function handleBalancePaymentForAssign(Order $order, int $userId, int $coachId): void
  2002. {
  2003. // 验证余额
  2004. $user = MemberUser::find($userId);
  2005. $wallet = $user->wallet;
  2006. abort_if($wallet->available_balance < $order->balance_amount, 400, '可用余额不足');
  2007. // 扣除余额
  2008. DB::transaction(function () use ($wallet, $order, $userId, $coachId) {
  2009. // 更新钱包余额
  2010. $wallet->decrement('total_balance', $order->balance_amount);
  2011. $wallet->decrement('available_balance', $order->balance_amount);
  2012. // 更新订单状态
  2013. $order->update(['state' => OrderStatus::PAID->value]);
  2014. // 创建钱包支付记录
  2015. $this->createWalletPaymentRecord($order, $wallet);
  2016. // 创建支付成功记录
  2017. $this->createPaymentSuccessRecord($order, $userId);
  2018. // 更新抢单记录
  2019. $this->updateGrabRecords($order->id, $coachId);
  2020. });
  2021. }
  2022. /**
  2023. * 创建钱包支付记录
  2024. */
  2025. private function createWalletPaymentRecord(Order $order, $wallet): void
  2026. {
  2027. WalletPaymentRecord::create([
  2028. 'order_id' => $order->id,
  2029. 'wallet_id' => $wallet->id,
  2030. 'payment_no' => 'B_' . $order->id,
  2031. 'payment_method' => 'balance',
  2032. 'total_amount' => $order->balance_amount,
  2033. 'actual_amount' => 0,
  2034. 'used_wallet_balance' => $order->balance_amount,
  2035. 'used_recharge_balance' => 0,
  2036. 'state' => 'success',
  2037. ]);
  2038. }
  2039. /**
  2040. * 创建支付成功记录
  2041. */
  2042. private function createPaymentSuccessRecord(Order $order, int $userId): void
  2043. {
  2044. OrderRecord::create([
  2045. 'order_id' => $order->id,
  2046. 'object_id' => $userId,
  2047. 'object_type' => MemberUser::class,
  2048. 'state' => OrderRecordStatus::PAID->value,
  2049. 'remark' => '余额支付成功',
  2050. ]);
  2051. }
  2052. /**
  2053. * 更新抢单记录
  2054. */
  2055. private function updateGrabRecords(int $orderId, int $coachId): void
  2056. {
  2057. OrderGrabRecord::where('order_id', $orderId)
  2058. ->update([
  2059. 'state' => OrderGrabRecordStatus::SUCCEEDED->value,
  2060. 'coach_id' => $coachId,
  2061. ]);
  2062. }
  2063. /**
  2064. * 发送指派通知
  2065. */
  2066. private function notifyAssignment(Order $order, CoachUser $coach): void
  2067. {
  2068. // TODO: 实现通知逻辑
  2069. // event(new OrderAssignedEvent($order, $coach));
  2070. }
  2071. /**
  2072. * 生成订单核销码
  2073. *
  2074. * @param int $userId 用户ID
  2075. * @param int $orderId 订单ID
  2076. * @return array{
  2077. * order_id: int,
  2078. * qr_code: string,
  2079. * qr_image: ?string,
  2080. * expired_at: string,
  2081. * state: string
  2082. * } 返回核销码信息
  2083. *
  2084. * @throws \Exception
  2085. */
  2086. public function generateVerificationCode(int $userId, int $orderId): array
  2087. {
  2088. try {
  2089. // 验证订单状态和归属
  2090. $order = $this->validateOrderForVerification($userId, $orderId);
  2091. // 生成核销码数据
  2092. $verificationData = $this->generateVerificationData($order->id);
  2093. // 生成二维码图片
  2094. $qrCodeImage = $this->generateQRCodeImage($verificationData['qr_code']);
  2095. return [
  2096. 'order_id' => $order->id,
  2097. 'qr_code' => $verificationData['qr_code'],
  2098. 'qr_image' => $qrCodeImage,
  2099. 'expired_at' => $verificationData['expired_at'],
  2100. 'state' => $order->state,
  2101. ];
  2102. } catch (Exception $e) {
  2103. $this->logError('生成订单核销码失败', $e, [
  2104. 'user_id' => $userId,
  2105. 'order_id' => $orderId,
  2106. ]);
  2107. throw $e;
  2108. }
  2109. }
  2110. /**
  2111. * 验证订单状态和归属
  2112. */
  2113. private function validateOrderForVerification(int $userId, int $orderId): Order
  2114. {
  2115. // 验证用户状态
  2116. $user = MemberUser::where('id', $userId)
  2117. ->where('state', UserStatus::OPEN->value)
  2118. ->firstOrFail();
  2119. // 验证订单状态和归属
  2120. $order = $user->orders()
  2121. ->where('id', $orderId)
  2122. ->whereIn('state', [
  2123. OrderStatus::PAID->value,
  2124. OrderStatus::ACCEPTED->value,
  2125. OrderStatus::DEPARTED->value,
  2126. OrderStatus::ARRIVED->value,
  2127. ])
  2128. ->firstOrFail();
  2129. return $order;
  2130. }
  2131. /**
  2132. * 生成核销码数据
  2133. *
  2134. * @param int $orderId 订单ID
  2135. * @return array{qr_code: string, expired_at: string}
  2136. */
  2137. private function generateVerificationData(int $orderId): array
  2138. {
  2139. // 生成时间戳
  2140. $timestamp = time();
  2141. // 生成签名
  2142. $sign = md5("order_{$orderId}_{$timestamp}_" . config('app.key'));
  2143. // 组装二维码内容
  2144. $qrCode = "order_{$orderId}_{$timestamp}_{$sign}";
  2145. return [
  2146. 'qr_code' => $qrCode,
  2147. 'expired_at' => date('Y-m-d H:i:s', $timestamp + 300), // 5分钟有效期
  2148. ];
  2149. }
  2150. /**
  2151. * 生成二维码图片
  2152. *
  2153. * @param string $content 二维码内容
  2154. * @return string|null Base64编码的图片数据
  2155. */
  2156. private function generateQRCodeImage(string $content): ?string
  2157. {
  2158. try {
  2159. // 生成PNG格式的二维码
  2160. $qrCodeImage = QrCode::format('png')
  2161. ->size(200) // 设置二维码大小为200px
  2162. ->margin(1) // 设置二维码边距为1
  2163. ->errorCorrection('H') // 设置纠错级别为最高级别H
  2164. ->generate($content);
  2165. // 将PNG转为base64
  2166. return 'data:image/png;base64,' . base64_encode($qrCodeImage);
  2167. } catch (Exception $e) {
  2168. Log::error('生成二维码图片失败:', [
  2169. 'content' => $content,
  2170. 'error' => $e->getMessage(),
  2171. ]);
  2172. return null;
  2173. }
  2174. }
  2175. /**
  2176. * 验证技师服务时间是否可用
  2177. */
  2178. public function validateServiceTime(int $coachId, string $serviceTime): bool
  2179. {
  2180. try {
  2181. // 1. 验证基础参数(技师状态、时间格式等)
  2182. $this->validateServiceTimeParams($coachId, $serviceTime);
  2183. // 2. 获取技师工作时间安排
  2184. $workSchedule = $this->getCoachWorkSchedule($coachId);
  2185. // 3. 验证是否在工作时间内
  2186. $this->validateWorkingHours($serviceTime, $workSchedule);
  2187. // 4. 检查是否与其他订单时间冲突
  2188. $this->checkTimeConflicts($coachId, $serviceTime);
  2189. return true;
  2190. } catch (Exception $e) {
  2191. // 记录错误日志
  2192. $this->logError('验证服务时间失败', $e, [
  2193. 'coach_id' => $coachId,
  2194. 'service_time' => $serviceTime,
  2195. ]);
  2196. throw $e;
  2197. }
  2198. }
  2199. /**
  2200. * 检查订单时间冲突
  2201. */
  2202. private function checkTimeConflicts(int $coachId, string $serviceTime): void
  2203. {
  2204. $serviceDateTime = Carbon::parse($serviceTime);
  2205. // 获取服务时长配置(默认2小时)
  2206. $serviceDuration = config('business.default_service_duration', 120);
  2207. // 计算本次服务的结束时间
  2208. $serviceEndTime = $serviceDateTime->copy()->addMinutes($serviceDuration);
  2209. // 检查是否与其他订单时间重叠
  2210. $conflictOrder = Order::where('coach_id', $coachId)
  2211. ->whereIn('state', [
  2212. OrderStatus::PAID->value, // 已支付
  2213. OrderStatus::ACCEPTED->value, // 已接单
  2214. OrderStatus::SERVICING->value, // 服务中
  2215. ])
  2216. ->where(function ($query) use ($serviceDateTime, $serviceEndTime) {
  2217. $query->where(function ($q) use ($serviceDateTime) {
  2218. // 检查服务开始时间是否冲突
  2219. $q->where('service_time', '<=', $serviceDateTime)
  2220. ->where('service_end_time', '>', $serviceDateTime);
  2221. })->orWhere(function ($q) use ($serviceEndTime) {
  2222. // 检查服务结束时间是否冲突
  2223. $q->where('service_time', '<', $serviceEndTime)
  2224. ->where('service_end_time', '>=', $serviceEndTime);
  2225. });
  2226. })
  2227. ->first();
  2228. // 如果存在时间冲突,抛出异常
  2229. abort_if(
  2230. $conflictOrder,
  2231. 400,
  2232. '该时间段技师已有其他订单'
  2233. );
  2234. }
  2235. /**
  2236. * 订单评价
  2237. *
  2238. * @param int $userId 用户ID
  2239. * @param int $orderId 订单ID
  2240. * @param array{
  2241. * score: int,
  2242. * content: string,
  2243. * images: ?array,
  2244. * tags: ?array
  2245. * } $data 评价数据
  2246. * @return array{message: string}
  2247. *
  2248. * @throws \Exception
  2249. */
  2250. public function evaluateOrder(int $userId, int $orderId, array $data): array
  2251. {
  2252. return DB::transaction(function () use ($userId, $orderId, $data) {
  2253. try {
  2254. // 1. 验证订单状态
  2255. $order = $this->validateOrderForEvaluation($userId, $orderId);
  2256. // 2. 创建评价记录
  2257. $evaluation = $this->createEvaluation($order, $data);
  2258. // 3. 更新技师评分
  2259. // TODO: 更新技师评分
  2260. // $this->updateCoachScore($order->coach_id);
  2261. // 4. 更新订单状态
  2262. $this->updateOrderEvaluationStatus($order);
  2263. // 5. 创建评价奖励
  2264. // TODO: 创建评价奖励
  2265. // $this->createEvaluationReward($order, $evaluation);
  2266. // 6. 发送评价通知
  2267. // $this->notifyEvaluation($order, $evaluation);
  2268. return ['message' => '评价成功'];
  2269. } catch (Exception $e) {
  2270. $this->logError('订单评价失败', $e, [
  2271. 'user_id' => $userId,
  2272. 'order_id' => $orderId,
  2273. 'data' => $data,
  2274. ]);
  2275. throw $e;
  2276. }
  2277. });
  2278. }
  2279. /**
  2280. * 验证订单评价条件
  2281. */
  2282. private function validateOrderForEvaluation(int $userId, int $orderId): Order
  2283. {
  2284. // 获取订单信息
  2285. $order = Order::where('id', $orderId)
  2286. ->where('user_id', $userId)
  2287. ->where('state', OrderStatus::LEFT->value)
  2288. ->firstOrFail();
  2289. // 检查是否已评价
  2290. abort_if(
  2291. $order->evaluation()->exists(),
  2292. 400,
  2293. '订单已评价'
  2294. );
  2295. // 检查评价时效(7天内可评价)
  2296. $evaluationExpireDays = config('business.evaluation_expire_days', 7);
  2297. abort_if(
  2298. $order->finish_time->addDays($evaluationExpireDays)->isPast(),
  2299. 400,
  2300. '评价已过期'
  2301. );
  2302. return $order;
  2303. }
  2304. /**
  2305. * 更新技师评分
  2306. */
  2307. // private function updateCoachScore(int $coachId): void
  2308. // {
  2309. // $coach = CoachUser::findOrFail($coachId);
  2310. // // 计算平均分
  2311. // $avgScore = OrderEvaluation::where('coach_id', $coachId)
  2312. // ->where('state', 'normal')
  2313. // ->avg('score');
  2314. // // 更新技师评分
  2315. // $coach->info()->update([
  2316. // 'score' => round($avgScore, 1),
  2317. // 'evaluation_count' => OrderEvaluation::where('coach_id', $coachId)
  2318. // ->where('state', 'normal')
  2319. // ->count(),
  2320. // ]);
  2321. // }
  2322. /**
  2323. * 更新订单评价状态
  2324. */
  2325. private function updateOrderEvaluationStatus(Order $order): void
  2326. {
  2327. // 创建订单记录
  2328. OrderRecord::create([
  2329. 'order_id' => $order->id,
  2330. 'object_id' => $order->user_id,
  2331. 'object_type' => MemberUser::class,
  2332. 'state' => OrderRecordStatus::EVALUATED->value,
  2333. 'remark' => '用户评价完成',
  2334. ]);
  2335. }
  2336. /**
  2337. * 创建评价奖励
  2338. */
  2339. // private function createEvaluationReward(Order $order, OrderEvaluation $evaluation): void
  2340. // {
  2341. // // 获取评价奖励配置
  2342. // $rewardAmount = config('business.evaluation_reward_amount', 0);
  2343. // if ($rewardAmount <= 0) {
  2344. // return;
  2345. // }
  2346. // // 更新用户钱包
  2347. // $wallet = $order->user->wallet;
  2348. // $wallet->increment('total_balance', $rewardAmount);
  2349. // $wallet->increment('available_balance', $rewardAmount);
  2350. // // 创建奖励记录
  2351. // $wallet->transRecords()->create([
  2352. // 'amount' => $rewardAmount,
  2353. // 'trans_type' => 'evaluation_reward',
  2354. // 'owner_type' => OrderEvaluation::class,
  2355. // 'owner_id' => $evaluation->id,
  2356. // 'remark' => '评价奖励',
  2357. // 'before_balance' => $wallet->total_balance - $rewardAmount,
  2358. // 'after_balance' => $wallet->total_balance,
  2359. // 'state' => 'success',
  2360. // ]);
  2361. // }
  2362. /**
  2363. * 发送评价通知
  2364. */
  2365. // private function notifyEvaluation(Order $order, OrderEvaluation $evaluation): void
  2366. // {
  2367. // // TODO: 实现评价通知逻辑
  2368. // // 1. 通知技师
  2369. // // event(new OrderEvaluatedEvent($order, $evaluation));
  2370. // // 2. 更新评价缓存
  2371. // // Cache::tags(['coach_evaluations'])->forget("coach:{$order->coach_id}");
  2372. // }
  2373. private function getNearbyCoaches(float $lat, float $lng, float $radius = 5.0): Collection
  2374. {
  2375. // 获取在线技师列表
  2376. return CoachUser::query()
  2377. ->with(['info'])
  2378. ->where('state', TechnicianStatus::ACTIVE->value)
  2379. ->whereHas('info', fn($q) => $q->where('state', TechnicianAuthStatus::PASSED->value))
  2380. ->get();
  2381. }
  2382. private function validateCoachForGrab(int $coachId): CoachUser
  2383. {
  2384. $coach = CoachUser::query()
  2385. ->with(['info'])
  2386. ->where('id', $coachId)
  2387. ->where('state', TechnicianStatus::ACTIVE->value)
  2388. ->first();
  2389. abort_if(! $coach, 400, '技师不存在或未激活');
  2390. // 验证技师认证状态
  2391. abort_if(
  2392. ! $coach->info || $coach->info->state !== TechnicianAuthStatus::PASSED->value,
  2393. 400,
  2394. '技师未通过认证'
  2395. );
  2396. return $coach;
  2397. }
  2398. public function getOrder(int $orderId): Order
  2399. {
  2400. $order = Order::find($orderId);
  2401. abort_if(! $order, 404, '订单不存在');
  2402. return $order;
  2403. }
  2404. private function findOrder(int $orderId): Order
  2405. {
  2406. $order = Order::find($orderId);
  2407. abort_if(! $order, 404, sprintf('订单[%d]不存在', $orderId));
  2408. return $order;
  2409. }
  2410. private function getOrderForPayment(int $orderId): Order
  2411. {
  2412. $order = Order::where('id', $orderId)
  2413. ->whereIn('state', [OrderStatus::CREATED->value])
  2414. ->first();
  2415. abort_if(! $order, 404, sprintf('订单[%d]不存在', $orderId));
  2416. abort_if(
  2417. $order->state !== OrderStatus::CREATED->value,
  2418. 422,
  2419. sprintf('订单[%d]状态为[%s],不允许支付', $orderId, $order->state)
  2420. );
  2421. return $order;
  2422. }
  2423. }