TransferLogic.php 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598
  1. <?php
  2. namespace App\Module\Transfer\Logics;
  3. use App\Module\Transfer\Enums\TransferStatus;
  4. use App\Module\Transfer\Enums\TransferType;
  5. use App\Module\Transfer\Models\TransferApp;
  6. use App\Module\Transfer\Models\TransferOrder;
  7. use App\Module\Transfer\Validations\TransferInValidation;
  8. use App\Module\Transfer\Validations\TransferOutValidation;
  9. use App\Module\Fund\Services\FundService;
  10. use App\Module\Transfer\Logics\OrderLogic;
  11. use App\Module\Transfer\Logics\CallbackLogic;
  12. use Illuminate\Support\Str;
  13. /**
  14. * Transfer模块核心业务逻辑
  15. */
  16. class TransferLogic
  17. {
  18. /**
  19. * 创建转出订单(对外接口)
  20. *
  21. * @param int $transferAppId 划转应用ID
  22. * @param int $userId 用户ID
  23. * @param string $amount 转出金额
  24. * @param string $password 安全密码
  25. * @param string|null $googleCode 谷歌验证码
  26. * @param string|null $outUserId 外部用户ID
  27. * @param string|null $remark 备注
  28. * @param array $callbackData 回调数据
  29. * @return TransferOrder
  30. * @throws \Exception
  31. */
  32. public static function createTransferOut(
  33. int $transferAppId,
  34. int $userId,
  35. string $amount,
  36. string $password,
  37. ?string $googleCode = null,
  38. ?string $outUserId = null,
  39. ?string $remark = null,
  40. array $callbackData = []
  41. ): TransferOrder {
  42. $data = [
  43. 'transfer_app_id' => $transferAppId,
  44. 'user_id' => $userId,
  45. 'amount' => $amount,
  46. 'password' => $password,
  47. 'google_code' => $googleCode,
  48. 'out_user_id' => $outUserId,
  49. 'remark' => $remark,
  50. 'callback_data' => $callbackData,
  51. ];
  52. return self::createTransferOutFromArray($data);
  53. }
  54. /**
  55. * 创建第三方应用转出订单(跳过密码验证)
  56. *
  57. * @param int $transferAppId 划转应用ID
  58. * @param int $userId 用户ID
  59. * @param string $amount 转出金额
  60. * @param string|null $outUserId 外部用户ID
  61. * @param string|null $remark 备注
  62. * @param array $callbackData 回调数据
  63. * @return TransferOrder
  64. * @throws \Exception
  65. */
  66. public static function createTransferOutForThirdParty(
  67. int $transferAppId,
  68. int $userId,
  69. string $amount,
  70. ?string $outUserId = null,
  71. ?string $remark = null,
  72. array $callbackData = []
  73. ): TransferOrder {
  74. $data = [
  75. 'transfer_app_id' => $transferAppId,
  76. 'user_id' => $userId,
  77. 'amount' => $amount,
  78. 'out_user_id' => $outUserId,
  79. 'remark' => $remark,
  80. 'callback_data' => $callbackData,
  81. ];
  82. return self::createTransferOutFromArrayForThirdParty($data);
  83. }
  84. /**
  85. * 创建第三方应用转出订单(内部实现,跳过密码验证)
  86. *
  87. * @param array $data 订单数据
  88. * @return TransferOrder
  89. * @throws \Exception
  90. */
  91. public static function createTransferOutFromArrayForThirdParty(array $data): TransferOrder
  92. {
  93. // 使用第三方应用专用验证类(跳过密码验证)
  94. $validation = new \App\Module\Transfer\Validations\TransferOutThirdPartyValidation($data);
  95. $validation->validated();
  96. // 获取应用配置
  97. $app = TransferApp::findOrFail($data['transfer_app_id']);
  98. if (!$app->is_enabled) {
  99. throw new \Exception('应用已禁用');
  100. }
  101. // 计算金额(转出:内部金额转换为外部金额)
  102. $amount = (string) $data['amount']; // 内部金额
  103. $outAmount = bcdiv($amount, (string) $app->exchange_rate, 10); // 外部金额 = 内部金额 ÷ 汇率
  104. // 计算手续费
  105. $feeInfo = $app->calculateOutFee($amount);
  106. // 生成外部订单ID
  107. $outOrderId = self::generateOutOrderId('OUT');
  108. // 创建订单
  109. $order = TransferOrder::create([
  110. 'transfer_app_id' => $app->id,
  111. 'out_id' => $app->out_id2 ?? 0,
  112. 'out_order_id' => $outOrderId,
  113. 'out_user_id' => $data['out_user_id'] ?? null,
  114. 'user_id' => $data['user_id'],
  115. 'currency_id' => $app->currency_id,
  116. 'fund_id' => $app->fund_id,
  117. 'type' => TransferType::OUT,
  118. 'status' => TransferStatus::CREATED,
  119. 'out_amount' => $outAmount,
  120. 'amount' => $amount,
  121. 'exchange_rate' => $app->exchange_rate,
  122. 'fee_rate' => $feeInfo['fee_rate'],
  123. 'fee_amount' => $feeInfo['fee_amount'],
  124. 'actual_amount' => $feeInfo['actual_amount'],
  125. 'callback_data' => $data['callback_data'] ?? [],
  126. 'remark' => $data['remark'] ?? null,
  127. ]);
  128. // 验证fund_to_uid必须存在
  129. if (empty($app->fund_to_uid)) {
  130. $order->updateStatus(TransferStatus::FAILED, '应用配置错误:未配置转入目标账户');
  131. throw new \Exception('应用配置错误:未配置转入目标账户(fund_to_uid)');
  132. }
  133. // 验证目标账户是否存在
  134. $targetFundService = new FundService($app->fund_to_uid, $app->fund_id);
  135. if (!$targetFundService->getAccount()) {
  136. $order->updateStatus(TransferStatus::FAILED, '目标账户不存在');
  137. throw new \Exception('目标账户不存在');
  138. }
  139. // 使用trade方法从用户转账到目标账户(实际到账金额)
  140. $userFundService = new FundService($data['user_id'], $app->fund_id);
  141. $tradeResult = $userFundService->trade(
  142. $app->fund_to_uid,
  143. $order->actual_amount, // 转出实际到账金额(扣除手续费后)
  144. 'transfer_out',
  145. $order->id,
  146. "转出到{$app->title}"
  147. );
  148. // 如果有手续费,处理手续费收取
  149. if ($order->hasFee()) {
  150. // 获取手续费收取账户UID(默认为1)
  151. $feeAccountUid = $app->getFeeAccountUid();
  152. $feeTradeResult = $userFundService->trade(
  153. $feeAccountUid,
  154. $order->fee_amount,
  155. 'transfer_out_fee',
  156. $order->id,
  157. "转出手续费-{$app->title}"
  158. );
  159. if (is_string($feeTradeResult)) {
  160. \Illuminate\Support\Facades\Log::warning('Transfer out fee collection failed', [
  161. 'order_id' => $order->id,
  162. 'fee_amount' => $order->fee_amount,
  163. 'fee_account_uid' => $feeAccountUid,
  164. 'error' => $feeTradeResult
  165. ]);
  166. } else {
  167. \Illuminate\Support\Facades\Log::info('Transfer out fee collected successfully', [
  168. 'order_id' => $order->id,
  169. 'fee_amount' => $order->fee_amount,
  170. 'fee_account_uid' => $feeAccountUid,
  171. 'configured_account' => $app->fee_account_uid
  172. ]);
  173. }
  174. }
  175. if (is_string($tradeResult)) {
  176. $order->updateStatus(TransferStatus::FAILED, '资金转移失败: ' . $tradeResult);
  177. throw new \Exception('余额不足或资金操作失败: ' . $tradeResult);
  178. }
  179. // 检查是否需要调用外部API
  180. if (!empty($app->order_out_create_url)) {
  181. // 配置了外部转出API,调用外部API处理
  182. OrderLogic::processTransferOut($order);
  183. } else {
  184. // 没有配置外部API,直接完成订单
  185. $order->updateStatus(TransferStatus::COMPLETED);
  186. }
  187. return $order;
  188. }
  189. /**
  190. * 创建转出订单(内部实现)
  191. *
  192. * 处理流程:
  193. * 1. 验证请求数据
  194. * 2. 获取应用配置
  195. * 3. 创建转出订单
  196. * 4. 验证fund_to_uid必须存在,进行用户间资金转移
  197. * 5. 根据是否配置外部API决定后续处理(调用外部API或直接完成)
  198. *
  199. * @param array $data 订单数据
  200. * @return TransferOrder
  201. * @throws \Exception
  202. */
  203. public static function createTransferOutFromArray(array $data): TransferOrder
  204. {
  205. // 验证请求数据
  206. $validation = new TransferOutValidation($data);
  207. $validation->validated();
  208. // 获取应用配置
  209. $app = TransferApp::findOrFail($data['transfer_app_id']);
  210. if (!$app->is_enabled) {
  211. throw new \Exception('应用已禁用');
  212. }
  213. // 计算金额(转出:内部金额转换为外部金额)
  214. $amount = (string) $data['amount']; // 内部金额
  215. $outAmount = bcdiv($amount, (string) $app->exchange_rate, 10); // 外部金额 = 内部金额 ÷ 汇率
  216. // 计算手续费
  217. $feeInfo = $app->calculateOutFee($amount);
  218. // 生成外部订单ID
  219. $outOrderId = self::generateOutOrderId('OUT');
  220. // 创建订单
  221. $order = TransferOrder::create([
  222. 'transfer_app_id' => $app->id,
  223. 'out_id' => $app->out_id2 ?? 0,
  224. 'out_order_id' => $outOrderId,
  225. 'out_user_id' => $data['out_user_id'] ?? null,
  226. 'user_id' => $data['user_id'],
  227. 'currency_id' => $app->currency_id,
  228. 'fund_id' => $app->fund_id,
  229. 'type' => TransferType::OUT,
  230. 'status' => TransferStatus::CREATED,
  231. 'out_amount' => $outAmount,
  232. 'amount' => $amount,
  233. 'exchange_rate' => $app->exchange_rate,
  234. 'fee_rate' => $feeInfo['fee_rate'],
  235. 'fee_amount' => $feeInfo['fee_amount'],
  236. 'actual_amount' => $feeInfo['actual_amount'],
  237. 'callback_data' => $data['callback_data'] ?? [],
  238. 'remark' => $data['remark'] ?? null,
  239. ]);
  240. // 验证fund_to_uid必须存在
  241. if (empty($app->fund_to_uid)) {
  242. $order->updateStatus(TransferStatus::FAILED, '应用配置错误:未配置转入目标账户');
  243. throw new \Exception('应用配置错误:未配置转入目标账户(fund_to_uid)');
  244. }
  245. // 验证目标账户是否存在
  246. $targetFundService = new FundService($app->fund_to_uid, $app->fund_id);
  247. if (!$targetFundService->getAccount()) {
  248. $order->updateStatus(TransferStatus::FAILED, '目标账户不存在');
  249. throw new \Exception('目标账户不存在');
  250. }
  251. // 使用trade方法从用户转账到目标账户(实际到账金额)
  252. $userFundService = new FundService($data['user_id'], $app->fund_id);
  253. $tradeResult = $userFundService->trade(
  254. $app->fund_to_uid,
  255. $order->actual_amount, // 转出实际到账金额(扣除手续费后)
  256. 'transfer_out',
  257. $order->id,
  258. "转出到{$app->title}"
  259. );
  260. // 如果有手续费,处理手续费收取
  261. if ($order->hasFee()) {
  262. // 获取手续费收取账户UID(默认为1)
  263. $feeAccountUid = $app->getFeeAccountUid();
  264. $feeTradeResult = $userFundService->trade(
  265. $feeAccountUid,
  266. $order->fee_amount,
  267. 'transfer_out_fee',
  268. $order->id,
  269. "转出手续费-{$app->title}"
  270. );
  271. if (is_string($feeTradeResult)) {
  272. \Log::warning('Transfer out fee collection failed', [
  273. 'order_id' => $order->id,
  274. 'fee_amount' => $order->fee_amount,
  275. 'fee_account_uid' => $feeAccountUid,
  276. 'error' => $feeTradeResult
  277. ]);
  278. } else {
  279. \Log::info('Transfer out fee collected successfully', [
  280. 'order_id' => $order->id,
  281. 'fee_amount' => $order->fee_amount,
  282. 'fee_account_uid' => $feeAccountUid,
  283. 'configured_account' => $app->fee_account_uid
  284. ]);
  285. }
  286. }
  287. if (is_string($tradeResult)) {
  288. $order->updateStatus(TransferStatus::FAILED, '资金转移失败: ' . $tradeResult);
  289. throw new \Exception('余额不足或资金操作失败: ' . $tradeResult);
  290. }
  291. // 检查是否需要调用外部API
  292. if (!empty($app->order_out_create_url)) {
  293. // 配置了外部转出API,调用外部API处理
  294. OrderLogic::processTransferOut($order);
  295. } else {
  296. // 没有配置外部API,直接完成订单
  297. $order->updateStatus(TransferStatus::COMPLETED);
  298. }
  299. return $order;
  300. }
  301. /**
  302. * 创建转入订单(对外接口)
  303. *
  304. * @param int $transferAppId 划转应用ID
  305. * @param int $userId 用户ID
  306. * @param string $businessId 业务订单ID
  307. * @param string $amount 转入金额
  308. * @param string|null $outUserId 外部用户ID
  309. * @param string|null $remark 备注
  310. * @param array $callbackData 回调数据
  311. * @return TransferOrder
  312. * @throws \Exception
  313. */
  314. public static function createTransferIn(
  315. int $transferAppId,
  316. int $userId,
  317. string $businessId,
  318. string $amount,
  319. ?string $outUserId = null,
  320. ?string $remark = null,
  321. array $callbackData = []
  322. ): TransferOrder {
  323. $data = [
  324. 'transfer_app_id' => $transferAppId,
  325. 'user_id' => $userId,
  326. 'business_id' => $businessId,
  327. 'amount' => $amount,
  328. 'out_user_id' => $outUserId,
  329. 'remark' => $remark,
  330. 'callback_data' => $callbackData,
  331. ];
  332. return self::createTransferInFromArray($data);
  333. }
  334. /**
  335. * 创建转入订单(内部实现)
  336. *
  337. * 处理流程:
  338. * 1. 验证请求数据
  339. * 2. 获取应用配置
  340. * 3. 创建转入订单
  341. * 4. 验证fund_in_uid必须存在,从指定账户转账到用户
  342. * 5. 根据是否配置回调URL决定后续处理(发送回调或直接完成)
  343. *
  344. * @param array $data 订单数据
  345. * @return TransferOrder
  346. * @throws \Exception
  347. */
  348. public static function createTransferInFromArray(array $data): TransferOrder
  349. {
  350. // 验证请求数据
  351. $validation = new TransferInValidation($data);
  352. $validation->validated();
  353. // 获取应用配置
  354. $app = TransferApp::findOrFail($data['transfer_app_id']);
  355. if (!$app->is_enabled) {
  356. throw new \Exception('应用已禁用');
  357. }
  358. // 检查外部订单ID是否已存在
  359. $existingOrder = TransferOrder::where('out_order_id', $data['business_id'])
  360. ->where('out_id', $app->out_id2 ?? 0)
  361. ->first();
  362. if ($existingOrder) {
  363. throw new \Exception('订单已存在');
  364. }
  365. // 计算金额(转入:外部金额转换为内部金额)
  366. $outAmount = (string) $data['amount']; // 外部金额
  367. $amount = bcmul($outAmount, (string) $app->exchange_rate, 10); // 内部金额 = 外部金额 × 汇率
  368. // 计算手续费
  369. $feeInfo = $app->calculateInFee($amount);
  370. // 创建订单
  371. $order = TransferOrder::create([
  372. 'transfer_app_id' => $app->id,
  373. 'out_id' => $app->out_id2 ?? 0,
  374. 'out_order_id' => $data['business_id'],
  375. 'out_user_id' => $data['out_user_id'] ?? null,
  376. 'user_id' => $data['user_id'],
  377. 'currency_id' => $app->currency_id,
  378. 'fund_id' => $app->fund_id,
  379. 'type' => TransferType::IN,
  380. 'status' => TransferStatus::CREATED,
  381. 'out_amount' => $outAmount,
  382. 'amount' => $amount,
  383. 'exchange_rate' => $app->exchange_rate,
  384. 'fee_rate' => $feeInfo['fee_rate'],
  385. 'fee_amount' => $feeInfo['fee_amount'],
  386. 'actual_amount' => $feeInfo['actual_amount'],
  387. 'callback_data' => $data['callback_data'] ?? [],
  388. 'remark' => $data['remark'] ?? null,
  389. ]);
  390. // 验证fund_in_uid必须存在
  391. if (empty($app->fund_in_uid)) {
  392. $order->updateStatus(TransferStatus::FAILED, '应用配置错误:未配置转入来源账户');
  393. throw new \Exception('应用配置错误:未配置转入来源账户(fund_in_uid)');
  394. }
  395. // 验证来源账户是否存在
  396. $sourceFundService = new FundService($app->fund_in_uid, $app->fund_id);
  397. if (!$sourceFundService->getAccount()) {
  398. $order->updateStatus(TransferStatus::FAILED, '来源账户不存在');
  399. throw new \Exception('来源账户不存在');
  400. }
  401. // 验证来源账户余额是否充足
  402. if ($sourceFundService->balance() < $amount) {
  403. $order->updateStatus(TransferStatus::FAILED, '来源账户余额不足');
  404. throw new \Exception('来源账户余额不足');
  405. }
  406. // 使用trade方法从来源账户转账到用户账户(扣除手续费后的实际金额)
  407. $tradeResult = $sourceFundService->trade(
  408. $data['user_id'],
  409. $order->actual_amount, // 转入实际到账金额(扣除手续费后)
  410. 'transfer_in',
  411. $order->id,
  412. "从{$app->title}转入"
  413. );
  414. // 如果有手续费,处理手续费收取
  415. if ($order->hasFee()) {
  416. // 获取手续费收取账户UID(默认为1)
  417. $feeAccountUid = $app->getFeeAccountUid();
  418. $feeTradeResult = $sourceFundService->trade(
  419. $feeAccountUid,
  420. $order->fee_amount,
  421. 'transfer_in_fee',
  422. $order->id,
  423. "转入手续费-{$app->title}"
  424. );
  425. if (is_string($feeTradeResult)) {
  426. \Log::warning('Transfer in fee collection failed', [
  427. 'order_id' => $order->id,
  428. 'fee_amount' => $order->fee_amount,
  429. 'fee_account_uid' => $feeAccountUid,
  430. 'error' => $feeTradeResult
  431. ]);
  432. } else {
  433. \Log::info('Transfer in fee collected successfully', [
  434. 'order_id' => $order->id,
  435. 'fee_amount' => $order->fee_amount,
  436. 'fee_account_uid' => $feeAccountUid,
  437. 'configured_account' => $app->fee_account_uid
  438. ]);
  439. }
  440. }
  441. if (is_string($tradeResult)) {
  442. $order->updateStatus(TransferStatus::FAILED, '资金转移失败: ' . $tradeResult);
  443. throw new \Exception('来源账户资金不足或操作失败: ' . $tradeResult);
  444. }
  445. // 检查是否需要发送回调通知
  446. if ($app->supportsCallback()) {
  447. // 配置了回调URL,更新状态为处理中并发送回调
  448. $order->updateStatus(TransferStatus::PROCESSING);
  449. CallbackLogic::sendCallback($order);
  450. } else {
  451. // 没有配置回调URL,直接完成订单
  452. $order->updateStatus(TransferStatus::COMPLETED);
  453. }
  454. return $order;
  455. }
  456. /**
  457. * 处理回调结果
  458. *
  459. * @param array $callbackData 回调数据
  460. * @return bool
  461. */
  462. public static function processCallback(array $callbackData): bool
  463. {
  464. // 查找订单
  465. $order = TransferOrder::where('out_order_id', $callbackData['business_id'])
  466. ->where('out_id', $callbackData['out_id'])
  467. ->first();
  468. if (!$order) {
  469. \Log::warning('Transfer callback order not found', $callbackData);
  470. return false;
  471. }
  472. // 更新订单状态
  473. $status = $callbackData['success'] ? TransferStatus::COMPLETED : TransferStatus::FAILED;
  474. $message = $callbackData['message'] ?? null;
  475. return $order->updateStatus($status, $message);
  476. }
  477. /**
  478. * 生成外部订单ID
  479. *
  480. * @param string $prefix 前缀
  481. * @return string
  482. */
  483. private static function generateOutOrderId(string $prefix = 'TR'): string
  484. {
  485. return $prefix . date('YmdHis') . Str::random(6);
  486. }
  487. /**
  488. * 重试订单处理
  489. *
  490. * @param int $orderId 订单ID
  491. * @return bool
  492. */
  493. public static function retryOrder(int $orderId): bool
  494. {
  495. $order = TransferOrder::findOrFail($orderId);
  496. if (!$order->canRetry()) {
  497. throw new \Exception('订单状态不允许重试');
  498. }
  499. // 重置状态为已创建
  500. $order->updateStatus(TransferStatus::CREATED);
  501. // 根据订单类型重新处理
  502. if ($order->isTransferOut()) {
  503. OrderLogic::processTransferOut($order);
  504. } else {
  505. // 转入订单重试主要是重新发送回调
  506. if ($order->transferApp->supportsCallback()) {
  507. CallbackLogic::sendCallback($order);
  508. }
  509. }
  510. return true;
  511. }
  512. /**
  513. * 手动完成订单
  514. *
  515. * @param int $orderId 订单ID
  516. * @param string $remark 备注
  517. * @return bool
  518. */
  519. public static function manualComplete(int $orderId, string $remark = ''): bool
  520. {
  521. $order = TransferOrder::findOrFail($orderId);
  522. if ($order->isFinalStatus()) {
  523. throw new \Exception('订单已处于最终状态');
  524. }
  525. // 更新状态为已完成
  526. $order->updateStatus(TransferStatus::COMPLETED, $remark);
  527. return true;
  528. }
  529. }