| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652 |
- <?php
- namespace App\Module\Transfer\Logics;
- use App\Module\Transfer\Enums\TransferStatus;
- use App\Module\Transfer\Enums\TransferType;
- use App\Module\Transfer\Models\TransferApp;
- use App\Module\Transfer\Models\TransferOrder;
- use App\Module\Transfer\Validations\TransferInValidation;
- use App\Module\Transfer\Validations\TransferOutValidation;
- use App\Module\Fund\Services\FundService;
- use App\Module\Transfer\Logics\OrderLogic;
- use App\Module\Transfer\Logics\CallbackLogic;
- use Illuminate\Support\Str;
- use Illuminate\Support\Facades\Log;
- /**
- * Transfer模块核心业务逻辑
- */
- class TransferLogic
- {
- /**
- * 创建转出订单(对外接口)
- *
- * @param int $transferAppId 划转应用ID
- * @param int $userId 用户ID
- * @param string $amount 转出金额
- * @param string $password 安全密码
- * @param string|null $googleCode 谷歌验证码
- * @param string|null $outUserId 外部用户ID
- * @param string|null $remark 备注
- * @param array $callbackData 回调数据
- * @return TransferOrder
- * @throws \Exception
- */
- public static function createTransferOut(
- int $transferAppId,
- int $userId,
- string $amount,
- string $password,
- ?string $googleCode = null,
- ?string $outUserId = null,
- ?string $remark = null,
- array $callbackData = []
- ): TransferOrder {
- $data = [
- 'transfer_app_id' => $transferAppId,
- 'user_id' => $userId,
- 'amount' => $amount,
- 'password' => $password,
- 'google_code' => $googleCode,
- 'out_user_id' => $outUserId,
- 'remark' => $remark,
- 'callback_data' => $callbackData,
- ];
- return self::createTransferOutFromArray($data);
- }
- /**
- * 创建第三方应用转出订单(跳过密码验证)
- *
- * @param int $transferAppId 划转应用ID
- * @param int $userId 用户ID
- * @param string $amount 转出金额(外部金额,第三方系统的金额)
- * @param string|null $outUserId 外部用户ID
- * @param string|null $remark 备注
- * @param array $callbackData 回调数据
- * @return TransferOrder
- * @throws \Exception
- */
- public static function createTransferOutForThirdParty(
- int $transferAppId,
- int $userId,
- string $amount,
- ?string $outUserId = null,
- ?string $remark = null,
- array $callbackData = []
- ): TransferOrder {
- $data = [
- 'transfer_app_id' => $transferAppId,
- 'user_id' => $userId,
- 'amount' => $amount,
- 'out_user_id' => $outUserId,
- 'remark' => $remark,
- 'callback_data' => $callbackData,
- ];
- return self::createTransferOutFromArrayForThirdParty($data);
- }
- /**
- * 创建第三方应用转出订单(内部实现,跳过密码验证)
- *
- * 注意:第三方应用传入的金额是外部金额,需要转换为内部金额进行处理
- *
- * @param array $data 订单数据
- * @return TransferOrder
- * @throws \Exception
- */
- public static function createTransferOutFromArrayForThirdParty(array $data): TransferOrder
- {
- // 获取应用配置(需要先获取以便进行金额转换)
- /**
- * @var TransferApp $app
- */
- $app = TransferApp::findOrFail($data['transfer_app_id']);
- if (!$app->is_enabled) {
- throw new \Exception('应用已禁用');
- }
- // 验证汇率是否合理
- if (bccomp((string) $app->exchange_rate, '0', 10) <= 0) {
- throw new \Exception('汇率配置错误:汇率必须大于0');
- }
- // 第三方应用金额处理:传入的是外部金额,需要转换为内部金额
- $outAmount = (string) $data['amount']; // 外部金额(第三方系统金额)
- $amount = bcmul($outAmount, (string) $app->exchange_rate, 10); // 内部金额 = 外部金额 × 汇率
- // 验证外部金额是否合理
- if (bccomp($outAmount, '0', 10) <= 0) {
- throw new \Exception('转出金额必须大于0');
- }
- // 验证内部金额计算结果是否合理
- if (bccomp($amount, '0', 10) <= 0) {
- throw new \Exception('汇率计算结果异常:内部金额必须大于0');
- }
- // 修改验证数据:将外部金额替换为内部金额,这样验证器就能正确验证余额
- $validationData = $data;
- $validationData['amount'] = $amount; // 传递内部金额给验证器
- // 使用第三方应用专用验证类(跳过密码验证)
- $validation = new \App\Module\Transfer\Validations\TransferOutThirdPartyValidation($validationData);
- $validation->validated();
- // 计算手续费(基于内部金额)
- $feeInfo = $app->calculateOutFee($amount, ['user_id' => $data['user_id']]);
- // 生成外部订单ID
- $outOrderId = self::generateOutOrderId('OUT');
- // 创建订单
- $order = TransferOrder::create([
- 'transfer_app_id' => $app->id,
- 'out_id' => $app->out_id2 ?? 0,
- 'out_order_id' => $outOrderId,
- 'out_user_id' => $data['out_user_id'] ?? null,
- 'user_id' => $data['user_id'],
- 'currency_id' => $app->currency_id,
- 'fund_id' => $app->fund_id,
- 'type' => TransferType::OUT,
- 'status' => TransferStatus::CREATED,
- 'out_amount' => $outAmount,
- 'amount' => $amount,
- 'exchange_rate' => $app->exchange_rate,
- 'fee_rate' => $feeInfo['fee_rate'],
- 'fee_amount' => $feeInfo['fee_amount'],
- 'actual_amount' => $feeInfo['actual_amount'],
- 'callback_data' => $data['callback_data'] ?? [],
- 'remark' => $data['remark'] ?? null,
- ]);
- // 验证fund_to_uid必须存在
- if (empty($app->fund_to_uid)) {
- $order->updateStatus(TransferStatus::FAILED, '应用配置错误:未配置转入目标账户');
- throw new \Exception('应用配置错误:未配置转入目标账户(fund_to_uid)');
- }
- // 验证目标账户是否存在
- $targetFundService = new FundService($app->fund_to_uid, $app->fund_id);
- if (!$targetFundService->getAccount()) {
- $order->updateStatus(TransferStatus::FAILED, '目标账户不存在');
- throw new \Exception('目标账户不存在');
- }
- // 预先验证用户余额是否充足
- $userFundService = new FundService($data['user_id'], $app->fund_id);
- if ($userFundService->balance() < $order->actual_amount) {
- $order->updateStatus(TransferStatus::FAILED, '用户余额不足');
- throw new \Exception('用户余额不足');
- }
- // 使用trade方法从用户转账到目标账户(完整转出金额,不扣除手续费)
- $tradeResult = $userFundService->trade(
- $app->fund_to_uid,
- $order->amount, // 转出完整金额(不扣除手续费)
- 'transfer_out',
- $order->id,
- "转出到{$app->title}"
- );
- // 如果有手续费,处理手续费收取
- if ($order->hasFee()) {
- // 获取手续费收取账户UID(默认为1)
- $feeAccountUid = $app->getFeeAccountUid();
- $feeTradeResult = $userFundService->trade(
- $feeAccountUid,
- $order->fee_amount,
- 'transfer_out_fee',
- $order->id,
- "转出手续费-{$app->title}"
- );
- if (is_string($feeTradeResult)) {
- Log::warning('Transfer out fee collection failed', [
- 'order_id' => $order->id,
- 'fee_amount' => $order->fee_amount,
- 'fee_account_uid' => $feeAccountUid,
- 'error' => $feeTradeResult
- ]);
- } else {
- Log::info('Transfer out fee collected successfully', [
- 'order_id' => $order->id,
- 'fee_amount' => $order->fee_amount,
- 'fee_account_uid' => $feeAccountUid,
- 'configured_account' => $app->fee_account_uid
- ]);
- }
- }
- if (is_string($tradeResult)) {
- $order->updateStatus(TransferStatus::FAILED, '资金转移失败: ' . $tradeResult);
- throw new \Exception('余额不足或资金操作失败: ' . $tradeResult);
- }
- // 检查是否需要调用外部API
- if (!empty($app->order_out_create_url)) {
- // 配置了外部转出API,调用外部API处理
- OrderLogic::processTransferOut($order);
- } else {
- // 没有配置外部API,直接完成订单
- $order->updateStatus(TransferStatus::COMPLETED);
- }
- return $order;
- }
- /**
- * 创建转出订单(内部实现)
- *
- * 处理流程:
- * 1. 验证请求数据
- * 2. 获取应用配置
- * 3. 创建转出订单
- * 4. 验证fund_to_uid必须存在,进行用户间资金转移
- * 5. 根据是否配置外部API决定后续处理(调用外部API或直接完成)
- *
- * @param array $data 订单数据
- * @return TransferOrder
- * @throws \Exception
- */
- public static function createTransferOutFromArray(array $data): TransferOrder
- {
- // 验证请求数据
- $validation = new TransferOutValidation($data);
- $validation->validated();
- // 获取应用配置
- $app = TransferApp::findOrFail($data['transfer_app_id']);
- if (!$app->is_enabled) {
- throw new \Exception('应用已禁用');
- }
- // 计算金额(转出:内部金额转换为外部金额)
- $amount = (string) $data['amount']; // 内部金额
- // 验证汇率是否合理
- if (bccomp((string) $app->exchange_rate, '0', 10) <= 0) {
- throw new \Exception('汇率配置错误:汇率必须大于0');
- }
- // 验证内部金额是否合理
- if (bccomp($amount, '0', 10) <= 0) {
- throw new \Exception('转出金额必须大于0');
- }
- $outAmount = bcdiv($amount, (string) $app->exchange_rate, 10); // 外部金额 = 内部金额 ÷ 汇率
- // 验证计算结果是否合理
- if (bccomp($outAmount, '0', 10) <= 0) {
- throw new \Exception('汇率计算结果异常:外部金额必须大于0');
- }
- // 计算手续费
- $feeInfo = $app->calculateOutFee($amount, ['user_id' => $data['user_id']]);
- // 生成外部订单ID
- $outOrderId = self::generateOutOrderId('OUT');
- // 创建订单
- $order = TransferOrder::create([
- 'transfer_app_id' => $app->id,
- 'out_id' => $app->out_id2 ?? 0,
- 'out_order_id' => $outOrderId,
- 'out_user_id' => $data['out_user_id'] ?? null,
- 'user_id' => $data['user_id'],
- 'currency_id' => $app->currency_id,
- 'fund_id' => $app->fund_id,
- 'type' => TransferType::OUT,
- 'status' => TransferStatus::CREATED,
- 'out_amount' => $outAmount,
- 'amount' => $amount,
- 'exchange_rate' => $app->exchange_rate,
- 'fee_rate' => $feeInfo['fee_rate'],
- 'fee_amount' => $feeInfo['fee_amount'],
- 'actual_amount' => $feeInfo['actual_amount'],
- 'callback_data' => $data['callback_data'] ?? [],
- 'remark' => $data['remark'] ?? null,
- ]);
- // 验证fund_to_uid必须存在
- if (empty($app->fund_to_uid)) {
- $order->updateStatus(TransferStatus::FAILED, '应用配置错误:未配置转入目标账户');
- throw new \Exception('应用配置错误:未配置转入目标账户(fund_to_uid)');
- }
- // 验证目标账户是否存在
- $targetFundService = new FundService($app->fund_to_uid, $app->fund_id);
- if (!$targetFundService->getAccount()) {
- $order->updateStatus(TransferStatus::FAILED, '目标账户不存在');
- throw new \Exception('目标账户不存在');
- }
- // 预先验证用户余额是否充足(转出金额 + 手续费)
- $userFundService = new FundService($data['user_id'], $app->fund_id);
- $totalRequired = bcadd($order->amount, $order->fee_amount, 10);
- if (bccomp((string)$userFundService->balance(), $totalRequired, 10) < 0) {
- $order->updateStatus(TransferStatus::FAILED, '用户余额不足');
- throw new \Exception('用户余额不足');
- }
- // 使用trade方法从用户转账到目标账户(完整转出金额,不扣除手续费)
- $tradeResult = $userFundService->trade(
- $app->fund_to_uid,
- $order->amount, // 转出完整金额(不扣除手续费)
- 'transfer_out',
- $order->id,
- "转出到{$app->title}"
- );
- // 如果有手续费,处理手续费收取
- if ($order->hasFee()) {
- // 获取手续费收取账户UID(默认为1)
- $feeAccountUid = $app->getFeeAccountUid();
- $feeTradeResult = $userFundService->trade(
- $feeAccountUid,
- $order->fee_amount,
- 'transfer_out_fee',
- $order->id,
- "转出手续费-{$app->title}"
- );
- if (is_string($feeTradeResult)) {
- Log::warning('Transfer out fee collection failed', [
- 'order_id' => $order->id,
- 'fee_amount' => $order->fee_amount,
- 'fee_account_uid' => $feeAccountUid,
- 'error' => $feeTradeResult
- ]);
- } else {
- Log::info('Transfer out fee collected successfully', [
- 'order_id' => $order->id,
- 'fee_amount' => $order->fee_amount,
- 'fee_account_uid' => $feeAccountUid,
- 'configured_account' => $app->fee_account_uid
- ]);
- }
- }
- if (is_string($tradeResult)) {
- $order->updateStatus(TransferStatus::FAILED, '资金转移失败: ' . $tradeResult);
- throw new \Exception('余额不足或资金操作失败: ' . $tradeResult);
- }
- // 检查是否需要调用外部API
- if (!empty($app->order_out_create_url)) {
- // 配置了外部转出API,调用外部API处理
- OrderLogic::processTransferOut($order);
- } else {
- // 没有配置外部API,直接完成订单
- $order->updateStatus(TransferStatus::COMPLETED);
- }
- return $order;
- }
- /**
- * 创建转入订单(对外接口)
- *
- * @param int $transferAppId 划转应用ID
- * @param int $userId 用户ID
- * @param string $businessId 业务订单ID
- * @param string $amount 转入金额
- * @param string|null $outUserId 外部用户ID
- * @param string|null $remark 备注
- * @param array $callbackData 回调数据
- * @return TransferOrder
- * @throws \Exception
- */
- public static function createTransferIn(
- int $transferAppId,
- int $userId,
- string $businessId,
- string $amount,
- ?string $outUserId = null,
- ?string $remark = null,
- array $callbackData = []
- ): TransferOrder {
- $data = [
- 'transfer_app_id' => $transferAppId,
- 'user_id' => $userId,
- 'business_id' => $businessId,
- 'amount' => $amount,
- 'out_user_id' => $outUserId,
- 'remark' => $remark,
- 'callback_data' => $callbackData,
- ];
- return self::createTransferInFromArray($data);
- }
- /**
- * 创建转入订单(内部实现)
- *
- * 处理流程:
- * 1. 验证请求数据
- * 2. 获取应用配置
- * 3. 创建转入订单
- * 4. 验证fund_in_uid必须存在,从指定账户转账到用户
- * 5. 根据是否配置回调URL决定后续处理(发送回调或直接完成)
- *
- * @param array $data 订单数据
- * @return TransferOrder
- * @throws \Exception
- */
- public static function createTransferInFromArray(array $data): TransferOrder
- {
- // 验证请求数据
- $validation = new TransferInValidation($data);
- $validation->validated();
- // 获取应用配置
- $app = TransferApp::findOrFail($data['transfer_app_id']);
- if (!$app->is_enabled) {
- throw new \Exception('应用已禁用');
- }
- // 检查外部订单ID是否已存在
- $existingOrder = TransferOrder::where('out_order_id', $data['business_id'])
- ->where('out_id', $app->out_id2 ?? 0)
- ->first();
- if ($existingOrder) {
- throw new \Exception('订单已存在');
- }
- // 计算金额(转入:外部金额转换为内部金额)
- $outAmount = (string) $data['amount']; // 外部金额
- $amount = bcmul($outAmount, (string) $app->exchange_rate, 10); // 内部金额 = 外部金额 × 汇率
- // 计算手续费
- $feeInfo = $app->calculateInFee($amount);
- // 创建订单
- $order = TransferOrder::create([
- 'transfer_app_id' => $app->id,
- 'out_id' => $app->out_id2 ?? 0,
- 'out_order_id' => $data['business_id'],
- 'out_user_id' => $data['out_user_id'] ?? null,
- 'user_id' => $data['user_id'],
- 'currency_id' => $app->currency_id,
- 'fund_id' => $app->fund_id,
- 'type' => TransferType::IN,
- 'status' => TransferStatus::CREATED,
- 'out_amount' => $outAmount,
- 'amount' => $amount,
- 'exchange_rate' => $app->exchange_rate,
- 'fee_rate' => $feeInfo['fee_rate'],
- 'fee_amount' => $feeInfo['fee_amount'],
- 'actual_amount' => $feeInfo['actual_amount'],
- 'callback_data' => $data['callback_data'] ?? [],
- 'remark' => $data['remark'] ?? null,
- ]);
- // 验证fund_in_uid必须存在
- if (empty($app->fund_in_uid)) {
- $order->updateStatus(TransferStatus::FAILED, '应用配置错误:未配置转入来源账户');
- throw new \Exception('应用配置错误:未配置转入来源账户(fund_in_uid)');
- }
- // 验证来源账户是否存在
- $sourceFundService = new FundService($app->fund_in_uid, $app->fund_id);
- if (!$sourceFundService->getAccount()) {
- $order->updateStatus(TransferStatus::FAILED, '来源账户不存在');
- throw new \Exception('来源账户不存在');
- }
- // 验证来源账户余额是否充足
- if ($sourceFundService->balance() < $amount) {
- $order->updateStatus(TransferStatus::FAILED, '来源账户余额不足');
- throw new \Exception('来源账户余额不足');
- }
- // 使用trade方法从来源账户转账到用户账户(扣除手续费后的实际金额)
- $tradeResult = $sourceFundService->trade(
- $data['user_id'],
- $order->actual_amount, // 转入实际到账金额(扣除手续费后)
- 'transfer_in',
- $order->id,
- "从{$app->title}转入"
- );
- // 如果有手续费,处理手续费收取
- if ($order->hasFee()) {
- // 获取手续费收取账户UID(默认为1)
- $feeAccountUid = $app->getFeeAccountUid();
- $feeTradeResult = $sourceFundService->trade(
- $feeAccountUid,
- $order->fee_amount,
- 'transfer_in_fee',
- $order->id,
- "转入手续费-{$app->title}"
- );
- if (is_string($feeTradeResult)) {
- Log::warning('Transfer in fee collection failed', [
- 'order_id' => $order->id,
- 'fee_amount' => $order->fee_amount,
- 'fee_account_uid' => $feeAccountUid,
- 'error' => $feeTradeResult
- ]);
- } else {
- Log::info('Transfer in fee collected successfully', [
- 'order_id' => $order->id,
- 'fee_amount' => $order->fee_amount,
- 'fee_account_uid' => $feeAccountUid,
- 'configured_account' => $app->fee_account_uid
- ]);
- }
- }
- if (is_string($tradeResult)) {
- $order->updateStatus(TransferStatus::FAILED, '资金转移失败: ' . $tradeResult);
- throw new \Exception('来源账户资金不足或操作失败: ' . $tradeResult);
- }
- // 检查是否需要发送回调通知
- if ($app->supportsCallback()) {
- // 配置了回调URL,更新状态为处理中并发送回调
- $order->updateStatus(TransferStatus::PROCESSING);
- CallbackLogic::sendCallback($order);
- } else {
- // 没有配置回调URL,直接完成订单
- $order->updateStatus(TransferStatus::COMPLETED);
- }
- return $order;
- }
- /**
- * 处理回调结果
- *
- * @param array $callbackData 回调数据
- * @return bool
- */
- public static function processCallback(array $callbackData): bool
- {
- // 查找订单
- $order = TransferOrder::where('out_order_id', $callbackData['business_id'])
- ->where('out_id', $callbackData['out_id'])
- ->first();
- if (!$order) {
- Log::warning('Transfer callback order not found', $callbackData);
- return false;
- }
- // 更新订单状态
- $status = $callbackData['success'] ? TransferStatus::COMPLETED : TransferStatus::FAILED;
- $message = $callbackData['message'] ?? null;
- return $order->updateStatus($status, $message);
- }
- /**
- * 生成外部订单ID
- *
- * @param string $prefix 前缀
- * @return string
- */
- private static function generateOutOrderId(string $prefix = 'TR'): string
- {
- return $prefix . date('YmdHis') . Str::random(6);
- }
- /**
- * 重试订单处理
- *
- * @param int $orderId 订单ID
- * @return bool
- */
- public static function retryOrder(int $orderId): bool
- {
- $order = TransferOrder::findOrFail($orderId);
- if (!$order->canRetry()) {
- throw new \Exception('订单状态不允许重试');
- }
- // 重置状态为已创建
- $order->updateStatus(TransferStatus::CREATED);
- // 根据订单类型重新处理
- if ($order->isTransferOut()) {
- OrderLogic::processTransferOut($order);
- } else {
- // 转入订单重试主要是重新发送回调
- if ($order->transferApp->supportsCallback()) {
- CallbackLogic::sendCallback($order);
- }
- }
- return true;
- }
- /**
- * 手动完成订单
- *
- * @param int $orderId 订单ID
- * @param string $remark 备注
- * @return bool
- */
- public static function manualComplete(int $orderId, string $remark = ''): bool
- {
- $order = TransferOrder::findOrFail($orderId);
- if ($order->isFinalStatus()) {
- throw new \Exception('订单已处于最终状态');
- }
- // 更新状态为已完成
- $order->updateStatus(TransferStatus::COMPLETED, $remark);
- return true;
- }
- }
|