doge 13 saat önce
ebeveyn
işleme
16a708892a

+ 11 - 2
app/Http/Controllers/api/Wallet.php

@@ -17,6 +17,7 @@ use App\Models\RechargeChannel;
 use App\Models\PaymentOrder;
 use App\Services\BalanceLogService;
 use App\Services\PaymentOrderService;
+use App\Services\Payment\JdPayService;
 use App\Services\QianBaoWithdrawService;
 use App\Services\WithdrawService;
 use Illuminate\Support\Facades\DB;
@@ -43,6 +44,11 @@ class Wallet extends BaseController
             $recharge_channel_group_id = request()->user->recharge_channel_group_id;
         }
         $list = RechargeChannel::getFormatChannel(1, $recharge_channel_group_id);
+        if (!JdPayService::canUserRecharge(request()->user->id)) {
+            $list = array_values(array_filter($list, function ($item) {
+                return !JdPayService::isChannel($item['value'] ?? '');
+            }));
+        }
         return $this->success([
             'list' => $list,
         ]);
@@ -64,6 +70,9 @@ class Wallet extends BaseController
             
             $member_id = request()->user->member_id;
             $user = User::where('member_id', $member_id)->first();
+            if (JdPayService::isChannel($params['payment_type']) && !JdPayService::canUserRecharge($user->id)) {
+                throw new Exception(lang("不支持此充值方式"));
+            }
             //校验是否支持此充值方式
             $check  = RechargeChannel::checkRechargeChannel($params['payment_type'], $user->recharge_channel_group_id);
             if ($check === false) {
@@ -334,14 +343,14 @@ class Wallet extends BaseController
     
 
     /**
-     * 提现(手动到账): DF001 支付宝转卡; DF002 支付宝转支付宝; DF005数字人民币; rgtx(人工提现,手动打款)
+     * 提现(手动到账): DF001 支付宝转卡; DF002 支付宝转支付宝; DF005数字人民币; JDpay JD钱包; rgtx(人工提现,手动打款)
      */
     public function payout() {
         $error_code = -1;
         try {
             $params = request()->validate([
                 'amount' => ['required', 'numeric', 'min:0.01'],
-                'channel' => ['required', 'string', 'in:DF001,DF002,DF005,rgtx'],
+                'channel' => ['required', 'string', 'in:DF001,DF002,DF005,JDpay,jdpay,rgtx'],
                 'bank_name' => ['required', 'string'],
                 'account' => ['required', 'string'],
                 'card_no' => ['required', 'string'],

+ 195 - 0
app/Services/Payment/JdPayService.php

@@ -0,0 +1,195 @@
+<?php
+
+namespace App\Services\Payment;
+
+use App\Services\BaseService;
+use GuzzleHttp\Client;
+
+class JdPayService extends BaseService
+{
+    public const CHANNEL = 'JDpay';
+
+    public const PAY_STATUS_SUCCESS = '3';
+    public const PAY_STATUS_FAIL = '4';
+
+    public const REMIT_STATUS_SUCCESS = '2';
+    public const REMIT_STATUS_FAIL = '3';
+
+    public static function getMerchantId()
+    {
+        return config('app.jd_pay_mch_id');
+    }
+
+    public static function getSecret()
+    {
+        return config('app.jd_pay_key');
+    }
+
+    public static function getNotifyUrl(): string
+    {
+        return rtrim(config('app.url'), '/') . '/api/pay/harvest';
+    }
+
+    public static function getRemitNotifyUrl(): string
+    {
+        return rtrim(config('app.url'), '/') . '/api/pay/notify';
+    }
+
+    public static function getClient(): Client
+    {
+        return new Client([
+            'timeout' => 10.0,
+        ]);
+    }
+
+    public static function isChannel(?string $channel): bool
+    {
+        return strtolower((string)$channel) === strtolower(self::CHANNEL)
+            || strtolower((string)$channel) === 'jdpay'
+            || (string)$channel === 'JD钱包';
+    }
+
+    public static function canUserRecharge($userId): bool
+    {
+        $allowedUserIds = array_values(array_filter(array_map(
+            'trim',
+            explode(',', (string)config('app.jd_pay_recharge_user_ids', ''))
+        )));
+
+        if (empty($allowedUserIds)) {
+            return true;
+        }
+
+        return in_array((string)$userId, $allowedUserIds, true);
+    }
+
+    public static function amount($amount): string
+    {
+        return number_format((float)$amount, 2, '.', '');
+    }
+
+    public static function signature(array $values): string
+    {
+        $values[] = self::getSecret();
+        return strtoupper(md5(implode('&', $values)));
+    }
+
+    public static function pay($amount, string $orderNo): array
+    {
+        $amount = self::amount($amount);
+        $data = [
+            'userCode' => self::getMerchantId(),
+            'orderCode' => $orderNo,
+            'amount' => $amount,
+            'callbackUrl' => self::getNotifyUrl(),
+        ];
+        $data['sign'] = self::signature([$orderNo, $amount, $data['userCode']]);
+
+        return self::post(config('app.jd_pay_gateway'), $data);
+    }
+
+    public static function queryPayOrder(string $orderNo = '', string $customerOrderNo = ''): array
+    {
+        $data = [
+            'userCode' => self::getMerchantId(),
+            'orderCode' => $orderNo,
+            'customerOrderCode' => $customerOrderNo,
+        ];
+        $data['sign'] = self::signature([$data['orderCode'], $data['customerOrderCode'], $data['userCode']]);
+
+        return self::post(config('app.jd_pay_query_gateway'), $data);
+    }
+
+    public static function remit($amount, string $orderNo, string $address): array
+    {
+        $amount = self::amount($amount);
+        $data = [
+            'userCode' => self::getMerchantId(),
+            'orderCode' => $orderNo,
+            'amount' => $amount,
+            'address' => $address,
+            'callbackUrl' => self::getRemitNotifyUrl(),
+        ];
+        $data['sign'] = self::signature([$orderNo, $amount, $address, $data['userCode']]);
+
+        return self::post(config('app.jd_remit_gateway'), $data);
+    }
+
+    public static function queryRemitOrder(string $orderNo = '', string $customerOrderNo = ''): array
+    {
+        $data = [
+            'userCode' => self::getMerchantId(),
+            'orderCode' => $orderNo,
+            'customerOrderCode' => $customerOrderNo,
+        ];
+        $data['sign'] = self::signature([$data['orderCode'], $data['customerOrderCode'], $data['userCode']]);
+
+        return self::post(config('app.jd_remit_query_gateway'), $data);
+    }
+
+    public static function balance(): array
+    {
+        $timestamp = (string)round(microtime(true) * 1000);
+        $userCode = self::getMerchantId();
+        $sign = self::signature([$userCode, $timestamp]);
+
+        $response = self::getClient()->get(rtrim(config('app.jd_balance_gateway'), '/') . '/' . $userCode, [
+            'query' => [
+                'timestamp' => $timestamp,
+                'sign' => $sign,
+            ],
+        ]);
+
+        return json_decode($response->getBody()->getContents(), true) ?: [];
+    }
+
+    public static function verifyPayNotify(array $params): bool
+    {
+        if (($params['userCode'] ?? '') !== self::getMerchantId()) {
+            return false;
+        }
+
+        $sign = self::signature([
+            $params['orderCode'] ?? '',
+            (string)($params['amount'] ?? ''),
+            $params['userCode'] ?? '',
+            (string)($params['status'] ?? ''),
+        ]);
+
+        return hash_equals($sign, strtoupper((string)($params['sign'] ?? '')));
+    }
+
+    public static function verifyRemitNotify(array $params): bool
+    {
+        if (($params['userCode'] ?? '') !== self::getMerchantId()) {
+            return false;
+        }
+
+        $sign = self::signature([
+            $params['orderCode'] ?? '',
+            $params['customerOrderCode'] ?? '',
+            (string)($params['amount'] ?? ''),
+            $params['userCode'] ?? '',
+            (string)($params['status'] ?? ''),
+        ]);
+
+        return hash_equals($sign, strtoupper((string)($params['sign'] ?? '')));
+    }
+
+    private static function post(string $url, array $data): array
+    {
+        $response = self::getClient()->post($url, [
+            'form_params' => $data,
+            'headers' => [
+                'Content-Type' => 'application/x-www-form-urlencoded',
+            ],
+        ]);
+
+        return json_decode($response->getBody()->getContents(), true) ?: [];
+    }
+
+    public static function getWhere(array $search = []): array
+    {
+        return [];
+    }
+}

+ 248 - 13
app/Services/PaymentOrderService.php

@@ -7,6 +7,7 @@ use App\Models\PaymentOrder;
 use Exception;
 use Illuminate\Support\Facades\DB;
 use Illuminate\Support\Facades\Log;
+use App\Services\Payment\JdPayService;
 use App\Services\Payment\QianBaoService;
 use App\Services\Payment\SanJinService;
 use App\Services\ConfigService;
@@ -199,10 +200,12 @@ class PaymentOrderService extends BaseService
         $max = 0;
         $min = 0;
         $rate = 0;
+        $selectedProduct = null;
 
         $geText = '';
         foreach ($product as $k => $v) {
             if ($v['type'] == $paymentType) {
+                $selectedProduct = $v;
                 if ($v['type'] == 'zfbge') {
                     if (in_array($amount, $v['fixed'])) {
                         $channel = $k;
@@ -234,7 +237,7 @@ class PaymentOrderService extends BaseService
         }
 
         // 没有找到支付通道
-        if (empty($channel)) {
+        if (empty($channel) && !JdPayService::isChannel($paymentType)) {
             // $text = "发起充值失败 \n";
             // $text .= "最低充值:" . $min . " \n";
             // $text .= "最高充值:" . $max . " \n";
@@ -248,6 +251,51 @@ class PaymentOrderService extends BaseService
             return $result;
         }
 
+        if (JdPayService::isChannel($paymentType) || JdPayService::isChannel($channel)) {
+            $channel = JdPayService::CHANNEL;
+            $bankName = $selectedProduct['name'] ?? 'JD钱包';
+            $data = [];
+            $data['type'] = self::TYPE_PAY;
+            $data['member_id'] = $memberId;
+            $data['amount'] = JdPayService::amount($amount);
+            $data['channel'] = $channel;
+            $data['fee'] = $amount * $rate;
+            $data['bank_name'] = $bankName;
+            $order_no = self::createOrderNo('jd' . $data['type'] . '_', $memberId);
+            $data['order_no'] = $order_no;
+            $data['callback_url'] = JdPayService::getNotifyUrl();
+            $data['remark'] = '充值费率:' . $rate;
+            $data['status'] = self::STATUS_STAY;
+
+            $ret = JdPayService::pay($amount, $order_no);
+            Log::channel('payment')->info('JD支付发起', $ret);
+            if (($ret['code'] ?? 0) == 200) {
+                $item = $ret['data'] ?? [];
+                $payUrl = $item['url'] ?? '';
+                $data['status'] = self::STATUS_PROCESS;
+                $data['pay_no'] = $item['orderNo'] ?? '';
+                $data['pay_url'] = $payUrl;
+                $data['pay_data'] = json_encode($ret, JSON_UNESCAPED_UNICODE);
+                static::$MODEL::create($data);
+
+                if ($payUrl) {
+                    $result['image'] = asset(self::createPaymentQrCode($payUrl));
+                }
+                $text = "{$bankName}充值确认 \n";
+                $text .= "请使用浏览器扫码或者复制支付地址到浏览器打开 \n";
+                $text .= "支付地址:{$payUrl}\n";
+                $text .= "支付金额:" . $amount . " RMB \n";
+                $text .= "请按实际支付金额进行付款,否则影响到账 \n";
+                $text .= "支付完成后请耐心等待,支付到账会第一时间通知您! \n";
+                $result['text'] = $text;
+                $result['url'] = $payUrl;
+            } else {
+                $result['text'] = $ret['message'] ?? 'JD支付发起失败';
+                $result['code'] = 20002;
+            }
+            return $result;
+        }
+
         $data = [];
         $data['type'] = self::TYPE_PAY;
         $data['member_id'] = $memberId;
@@ -296,8 +344,49 @@ class PaymentOrderService extends BaseService
      */
     public static function receivePay($params)
     {
+        if (($params['userCode'] ?? '') === JdPayService::getMerchantId()) {
+            $info = self::findOne(['order_no' => $params['orderCode'] ?? ''])
+                ?: static::$MODEL::where('pay_no', $params['orderCode'] ?? '')->first();
+            if (!$info || $info->type != self::TYPE_PAY) {
+                return false;
+            }
+            if (!JdPayService::verifyPayNotify($params)) {
+                return false;
+            }
+            if (bccomp(JdPayService::amount($info->amount), JdPayService::amount($params['amount'] ?? 0), 2) !== 0) {
+                return false;
+            }
+            if ($info->status != self::STATUS_PROCESS) {
+                return true;
+            }
+
+            $info->callback_data = json_encode($params, JSON_UNESCAPED_UNICODE);
+            $info->state = $params['status'];
+            if ((string)$params['status'] === JdPayService::PAY_STATUS_SUCCESS) {
+                $info->status = self::STATUS_SUCCESS;
+                $wallet = WalletService::findOne(['member_id' => $info->member_id]);
+                $balance = $wallet->available_balance;
+                $available_balance = bcadd($balance, JdPayService::amount($info->amount), 10);
+                $wallet->available_balance = $available_balance;
+                $wallet->save();
+
+                BalanceLogService::addLog($info->member_id, $info->amount, $balance, $available_balance, '三方充值', $info->id, '');
+                self::rechargesBibiReturn($info->member_id, $info->amount, $info->id);
+
+                $text = "✅ 支付成功 \n";
+                $text .= "充值金额:{$info->amount} RMB \n";
+                $text .= "订单号:{$info->order_no} \n";
+                $text .= "您充值的金额已到账,请注意查收!";
+                self::sendMessage($info->member_id, $text);
+            } elseif ((string)$params['status'] === JdPayService::PAY_STATUS_FAIL) {
+                $info->status = self::STATUS_FAIL;
+            }
+            $info->save();
+            return true;
+        }
+
         // 判断商户号
-        if ($params['mchId'] == SanJinService::getMerchantId()) {
+        if (($params['mchId'] ?? '') == SanJinService::getMerchantId()) {
             $must = ['mchId', 'productId', 'tradeNo', 'outTradeNo', 'amount', 'payAmount', 'state', 'createTime', 'payTime'];
 
 
@@ -433,9 +522,20 @@ class PaymentOrderService extends BaseService
             if (!$order) throw new Exception("订单不存在_{$orderId}", HttpStatus::CUSTOM_ERROR);
             $amount = $order->amount;
             $amount = number_format($amount, 2, '.', '');
-            $ret = QianBaoService::payout($amount, $order->order_no, $order->bank_name, $order->account, $order->card_no);
-            Log::error('第三方代付接口调用:' . json_encode($ret, JSON_UNESCAPED_UNICODE));
-            if ($ret['code'] != 200) throw new Exception($ret['msg'], HttpStatus::CUSTOM_ERROR);
+            if (JdPayService::isChannel($order->channel)) {
+                self::assertJdBalanceEnough($amount);
+                $ret = JdPayService::remit($amount, $order->order_no, $order->card_no);
+                Log::channel('payment')->info('JD下发接口调用', $ret);
+                if (($ret['code'] ?? 0) != 200) {
+                    throw new Exception($ret['message'] ?? 'JD下发失败', HttpStatus::CUSTOM_ERROR);
+                }
+                $order->pay_no = $ret['data']['orderNo'] ?? '';
+                $order->pay_data = json_encode($ret, JSON_UNESCAPED_UNICODE);
+            } else {
+                $ret = QianBaoService::payout($amount, $order->order_no, $order->bank_name, $order->account, $order->card_no);
+                Log::error('第三方代付接口调用:' . json_encode($ret, JSON_UNESCAPED_UNICODE));
+                if ($ret['code'] != 200) throw new Exception($ret['msg'], HttpStatus::CUSTOM_ERROR);
+            }
             $order->status = self::STATUS_PROCESS;
             $order->save();
         } catch (Exception $e) {
@@ -499,7 +599,7 @@ class PaymentOrderService extends BaseService
             $data['bank_name'] = $bank_name;
             $data['account'] = $account;
             $data['card_no'] = $card_no;
-            $data['callback_url'] = QianBaoService::getNotifyUrl();
+            $data['callback_url'] = JdPayService::isChannel($channel) ? JdPayService::getRemitNotifyUrl() : QianBaoService::getNotifyUrl();
             $data['status'] = self::STATUS_STAY;
             $data['remark'] = '提现费率:0.2%+2';
 
@@ -534,13 +634,34 @@ class PaymentOrderService extends BaseService
         }
 
         // 调用三方支付接口(在事务外)
-        $ret = QianBaoService::payout($amount, $order_no, $bank_name, $account, $card_no);
-        Log::error('第三方代付接口调用:' . json_encode($ret, JSON_UNESCAPED_UNICODE));
-        if ($ret['code'] == 200) {
+        if (JdPayService::isChannel($channel)) {
+            try {
+                self::assertJdBalanceEnough($amount);
+                $ret = JdPayService::remit($amount, $order_no, $card_no);
+                Log::channel('payment')->info('JD下发接口调用', $ret);
+                $success = (($ret['code'] ?? 0) == 200);
+                $failureMessage = $ret['message'] ?? 'JD下发失败';
+            } catch (Exception $e) {
+                $ret = ['message' => $e->getMessage()];
+                $success = false;
+                $failureMessage = $e->getMessage();
+            }
+        } else {
+            $ret = QianBaoService::payout($amount, $order_no, $bank_name, $account, $card_no);
+            Log::error('第三方代付接口调用:' . json_encode($ret, JSON_UNESCAPED_UNICODE));
+            $success = (($ret['code'] ?? 0) == 200);
+            $failureMessage = $ret['msg'] ?? '代付失败';
+        }
+
+        if ($success) {
             // 更新提现记录状态为处理中
             DB::beginTransaction();
             try {
                 $info->status = self::STATUS_PROCESS;
+                if (JdPayService::isChannel($channel)) {
+                    $info->pay_no = $ret['data']['orderNo'] ?? '';
+                    $info->pay_data = json_encode($ret, JSON_UNESCAPED_UNICODE);
+                }
                 $info->save();
                 DB::commit();
 
@@ -573,14 +694,14 @@ class PaymentOrderService extends BaseService
 
                 // 更新提现记录状态为失败
                 $info->status = self::STATUS_FAIL;
-                $info->remark = $ret['msg'];
+                $info->remark = $failureMessage;
                 $info->save();
 
                 // 记录退款日志
                 BalanceLogService::addLog($memberId, $default_amount, $available_balance, $balance, '三方提现', $id, '提现失败退款');
 
                 DB::commit();
-                $result['text'] = $ret['msg'];
+                $result['text'] = $failureMessage;
 
             } catch (Exception $e) {
                 DB::rollBack();
@@ -604,8 +725,24 @@ class PaymentOrderService extends BaseService
      */
     public static function receiveOrder($params)
     {
+        if (($params['userCode'] ?? '') === JdPayService::getMerchantId()) {
+            $info = self::findOne(['order_no' => $params['customerOrderCode'] ?? ''])
+                ?: static::$MODEL::where('pay_no', $params['orderCode'] ?? '')->first();
+            if (!$info || $info->type != self::TYPE_PAYOUT) {
+                return false;
+            }
+            if (!JdPayService::verifyRemitNotify($params)) {
+                return false;
+            }
+            if (bccomp(JdPayService::amount($info->amount), JdPayService::amount($params['amount'] ?? 0), 2) !== 0) {
+                return false;
+            }
+            self::onSubmitJdPayout($params, $info);
+            return true;
+        }
+
         // 判断商户号是否一致
-        if ($params['merchantNum'] == QianBaoService::getMerchantId()) {
+        if (($params['merchantNum'] ?? '') == QianBaoService::getMerchantId()) {
 
             $info = self::findOne(['order_no' => $params['orderNo']]);
             if ($info) {
@@ -638,6 +775,60 @@ class PaymentOrderService extends BaseService
      * @param {*} $params
      * @return {*}
      */
+    public static function onSubmitJdPayout($params, $info)
+    {
+        if ($info->status != self::STATUS_PROCESS) {
+            return;
+        }
+
+        $memberId = $info->member_id;
+        $amount = JdPayService::amount($params['amount'] ?? $info->amount);
+        $data = [];
+        $chat_id = $info->member_id;
+        $data['callback_data'] = json_encode($params, JSON_UNESCAPED_UNICODE);
+        DB::beginTransaction();
+        try {
+            if ((string)$params['status'] === JdPayService::REMIT_STATUS_SUCCESS) {
+                $data['status'] = self::STATUS_SUCCESS;
+                $res = static::$MODEL::where(['id' => $info->id])->update($data);
+                if ($res) {
+                    $text = "✅ 提现通知 \n";
+                    $text .= "提现平台:{$info->bank_name} \n";
+                    $text .= "收款人:{$info->account} \n";
+                    $text .= "收款地址:{$info->card_no} \n";
+                    $text .= "提现金额:{$info->amount} \n";
+                    $text .= "提现成功,金额已到账,请注意查收!";
+                    self::sendMessage($chat_id, $text);
+                }
+            } elseif ((string)$params['status'] === JdPayService::REMIT_STATUS_FAIL) {
+                $data['status'] = self::STATUS_FAIL;
+                $res = static::$MODEL::where(['id' => $info->id])->update($data);
+
+                $wallet = WalletService::findOne(['member_id' => $info->member_id]);
+                $balance = $wallet->available_balance;
+                $available_balance = bcadd($balance, $amount, 10);
+                $wallet->available_balance = $available_balance;
+                $wallet->save();
+
+                BalanceLogService::addLog($memberId, $amount, $balance, $available_balance, '三方提现', $info->id, '提现失败退款');
+                if ($res) {
+                    $text = "❌ 提现通知 \n";
+                    $text .= "提现平台:{$info->bank_name} \n";
+                    $text .= "收款人:{$info->account} \n";
+                    $text .= "收款地址:{$info->card_no} \n";
+                    $text .= "提现金额:{$info->amount} \n";
+                    $text .= "提现失败,金额已返回钱包,请注意查收!";
+                    self::sendMessage($chat_id, $text);
+                }
+            }
+
+            DB::commit();
+        } catch (Exception $e) {
+            DB::rollBack();
+            Log::error('JD提现回调处理异常: ' . $e->getMessage(), $params);
+        }
+    }
+
     public static function onSubmitPayout($params, $info)
     {
         $memberId = $info->member_id;
@@ -706,6 +897,37 @@ class PaymentOrderService extends BaseService
         $msg['code'] = self::NOT;
         $info = self::findOne(['id' => $id]);
         if ($info && $info->status == self::STATUS_PROCESS) {
+            if (JdPayService::isChannel($info->channel)) {
+                $ret = JdPayService::queryPayOrder($info->pay_no ?? '', $info->order_no);
+                Log::channel('payment')->info('JD支付查询订单', $ret);
+                if (($ret['code'] ?? 0) == 200) {
+                    $item = $ret['data'] ?? [];
+                    if ((string)($item['status'] ?? '') === JdPayService::PAY_STATUS_SUCCESS) {
+                        $info->status = self::STATUS_SUCCESS;
+                        $info->state = $item['status'];
+                        $info->callback_data = json_encode($ret, JSON_UNESCAPED_UNICODE);
+                        $info->save();
+
+                        $wallet = WalletService::findOne(['member_id' => $info->member_id]);
+                        $balance = $wallet->available_balance;
+                        $available_balance = bcadd($balance, $info->amount, 10);
+                        $wallet->available_balance = $available_balance;
+                        $wallet->save();
+
+                        BalanceLogService::addLog($info->member_id, $info->amount, $balance, $available_balance, '三方充值', $info->id, '');
+                        self::rechargesBibiReturn($info->member_id, $info->amount, $info->id);
+
+                        $msg['code'] = self::YES;
+                        $msg['msg'] = '支付成功';
+                    } else {
+                        $msg['msg'] = '支付中';
+                    }
+                } else {
+                    $msg['msg'] = '查询失败:' . ($ret['message'] ?? '');
+                }
+                return $msg;
+            }
+
             $ret = SanJinService::queryOrder($info->order_no);
             Log::error('三斤支付查询订单:', $ret);
             if ($ret['code'] == 0) {
@@ -741,6 +963,19 @@ class PaymentOrderService extends BaseService
 
     }
 
+    private static function assertJdBalanceEnough($amount): void
+    {
+        $ret = JdPayService::balance();
+        Log::channel('payment')->info('JD余额查询', $ret);
+        if (($ret['code'] ?? 0) != 200) {
+            throw new Exception($ret['message'] ?? 'JD余额查询失败', HttpStatus::CUSTOM_ERROR);
+        }
+        $balance = $ret['data']['balance'] ?? null;
+        if ($balance === null || bccomp((string)$balance, JdPayService::amount($amount), 2) < 0) {
+            throw new Exception('JD商户余额不足', HttpStatus::CUSTOM_ERROR);
+        }
+    }
+
     public static function syncPayOrder()
     {
         $list = static::$MODEL::where('state', 0)->where('type', self::TYPE_PAY)->take(100)->get();
@@ -762,4 +997,4 @@ class PaymentOrderService extends BaseService
         // }
     }
 
-}
+}

+ 3 - 2
app/Services/QianBaoWithdrawService.php

@@ -9,6 +9,7 @@ use App\Models\Bank;
 use App\Models\Config;
 use App\Models\PaymentOrder;
 use App\Models\Wallet;
+use App\Services\Payment\JdPayService;
 use App\Services\Payment\QianBaoService;
 use Exception;
 use Illuminate\Support\Facades\Cache;
@@ -468,7 +469,7 @@ class QianBaoWithdrawService
             $data['bank_name'] = $bank_name;
             $data['account'] = $account;
             $data['card_no'] = $card_no;
-            $data['callback_url'] = QianBaoService::getNotifyUrl();
+            $data['callback_url'] = JdPayService::isChannel($channel) ? JdPayService::getRemitNotifyUrl() : QianBaoService::getNotifyUrl();
             $data['status'] = PaymentOrderService::STATUS_STAY;
             $data['remark'] = '提现费率:0.2%+2';
             // 创建待处理状态的提现记录
@@ -702,4 +703,4 @@ class QianBaoWithdrawService
         Cache::delete(get_step_key($chatId));
         return static::banks($chatId, $messageId, $channel);
     }
-}
+}

+ 9 - 0
config/app.php

@@ -17,6 +17,15 @@ return [
     // 第三方收款配置
     'tree_pay_mch_id' => env('TREE_PAY_MCH_ID',''),
     'tree_pay_key' => env('TREE_PAY_MCH_KEY',''),
+    // JD钱包支付配置
+    'jd_pay_mch_id' => env('JD_PAY_MCH_ID', ''),
+    'jd_pay_key' => env('JD_PAY_KEY', ''),
+    'jd_pay_gateway' => env('JD_PAY_GATEWAY', 'https://openapi.jdpayapi.com/jdpayOpen/api/pay'),
+    'jd_pay_query_gateway' => env('JD_PAY_QUERY_GATEWAY', 'https://openapi.jdpayapi.com/jdpayOpen/api/query/pay-order'),
+    'jd_remit_gateway' => env('JD_REMIT_GATEWAY', 'https://openapi.jdpayapi.com/jdpayOpen/api/remit'),
+    'jd_remit_query_gateway' => env('JD_REMIT_QUERY_GATEWAY', 'https://openapi.jdpayapi.com/jdpayOpen/api/query/remit-order'),
+    'jd_balance_gateway' => env('JD_BALANCE_GATEWAY', 'https://openapi.jdpayapi.com/jdpayOpen/api/balance'),
+    'jd_pay_recharge_user_ids' => env('JD_PAY_RECHARGE_USER_IDS', ''),
     /*
     |--------------------------------------------------------------------------
     | Application Name

+ 90 - 0
database/migrations/2026_05_29_120000_add_jd_pay_channel.php

@@ -0,0 +1,90 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration {
+    public function up()
+    {
+        if (Schema::hasTable('recharge_channel')) {
+            $this->upsertRechargeChannel(1, 'JD钱包', 0, 100, 49999);
+            $this->upsertRechargeChannel(2, 'JD钱包', 0.002, 100, 49999);
+        }
+
+        if (Schema::hasTable('recharge_channel_group')) {
+            $this->appendGroupType('recharge_type');
+            $this->appendGroupType('withdraw_type');
+        }
+    }
+
+    public function down()
+    {
+        if (Schema::hasTable('recharge_channel')) {
+            DB::table('recharge_channel')->where('type', 'JDpay')->delete();
+        }
+
+        if (Schema::hasTable('recharge_channel_group')) {
+            $this->removeGroupType('recharge_type');
+            $this->removeGroupType('withdraw_type');
+        }
+    }
+
+    private function upsertRechargeChannel(int $dataType, string $name, float $rate, int $min, int $max): void
+    {
+        $row = [
+            'data_type' => $dataType,
+            'name' => $name,
+            'type' => 'JDpay',
+            'rate' => $rate,
+            'min' => $min,
+            'max' => $max,
+        ];
+
+        foreach (['key' => 'JDpay', 'fixed' => '', 'status' => 1, 'sort' => 99, 'from' => 1] as $column => $value) {
+            if (Schema::hasColumn('recharge_channel', $column)) {
+                $row[$column] = $value;
+            }
+        }
+
+        DB::table('recharge_channel')->updateOrInsert(
+            ['data_type' => $dataType, 'type' => 'JDpay'],
+            $row
+        );
+    }
+
+    private function appendGroupType(string $column): void
+    {
+        if (!Schema::hasColumn('recharge_channel_group', $column)) {
+            return;
+        }
+
+        DB::table('recharge_channel_group')->orderBy('id')->chunkById(100, function ($groups) use ($column) {
+            foreach ($groups as $group) {
+                $types = array_values(array_filter(explode(',', (string)$group->{$column})));
+                if (!in_array('JDpay', $types, true)) {
+                    $types[] = 'JDpay';
+                    DB::table('recharge_channel_group')->where('id', $group->id)->update([
+                        $column => implode(',', $types),
+                    ]);
+                }
+            }
+        });
+    }
+
+    private function removeGroupType(string $column): void
+    {
+        if (!Schema::hasColumn('recharge_channel_group', $column)) {
+            return;
+        }
+
+        DB::table('recharge_channel_group')->orderBy('id')->chunkById(100, function ($groups) use ($column) {
+            foreach ($groups as $group) {
+                $types = array_values(array_diff(array_filter(explode(',', (string)$group->{$column})), ['JDpay']));
+                DB::table('recharge_channel_group')->where('id', $group->id)->update([
+                    $column => implode(',', $types),
+                ]);
+            }
+        });
+    }
+};

+ 10 - 2
example.env

@@ -76,6 +76,16 @@ TREE_PAYMENT_SECRET=
 TREE_PAY_MCH_ID=
 TREE_PAY_MCH_KEY=
 
+# JD钱包支付配置
+JD_PAY_MCH_ID=
+JD_PAY_KEY=
+JD_PAY_GATEWAY=https://openapi.jdpayapi.com/jdpayOpen/api/pay
+JD_PAY_QUERY_GATEWAY=https://openapi.jdpayapi.com/jdpayOpen/api/query/pay-order
+JD_REMIT_GATEWAY=https://openapi.jdpayapi.com/jdpayOpen/api/remit
+JD_REMIT_QUERY_GATEWAY=https://openapi.jdpayapi.com/jdpayOpen/api/query/remit-order
+JD_BALANCE_GATEWAY=https://openapi.jdpayapi.com/jdpayOpen/api/balance
+JD_PAY_RECHARGE_USER_IDS=
+
 # 数据库
 DB_CONNECTION=mysql
 DB_HOST=127.0.0.1
@@ -146,5 +156,3 @@ VITE_PUSHER_APP_CLUSTER="${PUSHER_APP_CLUSTER}"
 
 
 
-
-