diff --git a/application/config.php b/application/config.php index 2438492..1f0adab 100644 --- a/application/config.php +++ b/application/config.php @@ -373,5 +373,15 @@ return [ // 'wxpay' => '微信支付', ] ], + 'ccpay' => [//CCmerchant越南银行代收 + 'online_order_fac' => 'ccpay_online_order', + 'notify_url' => 'https://api.g7g7.top/order/ccpay_recharge_callback', + 'callback_url' => '', + 'api_url' => 'https://c.gmobvfxllc.com/gateway/api/trade', + 'mch_no' => 'M8822026041910000352', + 'pay_secret' => '820281b0d0b24a6bb701d8fc1c31a197', + 'timeout' => 600, + 'type' => ['pay' => 'CCpay越南银行'], + ], ] ]; diff --git a/application/onlinechip/controller/Order.php b/application/onlinechip/controller/Order.php index 2b35bab..185d145 100644 --- a/application/onlinechip/controller/Order.php +++ b/application/onlinechip/controller/Order.php @@ -15,6 +15,7 @@ class Order Extends Controller{ use traits\hengfuPay; use traits\alinPay; use traits\epayPay; + use traits\ccpayPay; public function __construct() { diff --git a/application/onlinechip/controller/Pcapi.php b/application/onlinechip/controller/Pcapi.php index 37b9beb..d88c117 100755 --- a/application/onlinechip/controller/Pcapi.php +++ b/application/onlinechip/controller/Pcapi.php @@ -18,6 +18,7 @@ use Waybill\WaybillRoulette; class Pcapi extends Controller{ use traits\alinPay; use traits\epayPay; + use traits\ccpayPay; public function __construct(){ parent::__construct(); // CORS headers diff --git a/application/onlinechip/controller/traits/ccpayPay.php b/application/onlinechip/controller/traits/ccpayPay.php new file mode 100644 index 0000000..9a59939 --- /dev/null +++ b/application/onlinechip/controller/traits/ccpayPay.php @@ -0,0 +1,411 @@ +post(); + if (isset($post['encryptData'])) { + $post = decrypt_data($post); + if (!$post) { + $data = encrypt_data(['status' => 0, 'message' => 'Dữ liệu không hợp lệ']); + return json(['Success' => 1, 'Data' => $data]); + } + } + + $user_id = intval($post['user_id'] ?? 0); + $money = intval($post['price'] ?? 0); + $client = intval($post['client'] ?? 0); + $username = trim($post['username'] ?? ''); + + $user = Db::name('user')->where([ + 'id' => $user_id, + 'status' => 1, + 'is_delete' => 0, + 'agent' => 0, + ])->find(); + + if (!$user || $user_id <= 0 || $money <= 0 || $client <= 0) { + $data = encrypt_data(['status' => 0, 'message' => 'Tham số không hợp lệ, vui lòng thử lại']); + return json(['Success' => 1, 'Data' => $data]); + } + + $zdlIdArr = explode(',', $user['agent_parent_id_path']); + $area_list = config('area_list'); + + if (!isset($area_list[$user['area_id']])) { + $data = encrypt_data(['status' => 0, 'message' => 'Khu vực hiện chưa hỗ trợ nạp tiền']); + return json(['Success' => 1, 'Data' => $data]); + } + + $isAllow = false; + foreach ($area_list[$user['area_id']]['limit_agent'] as $allowAgentId) { + if (in_array($allowAgentId, $zdlIdArr)) { + $isAllow = true; + } + } + if (!$isAllow) { + $data = encrypt_data(['status' => 0, 'message' => 'Tài khoản này chưa được phép nạp online']); + return json(['Success' => 1, 'Data' => $data]); + } + + $zdlId = $zdlIdArr[0]; + $zdl = Db::name('user')->where([ + 'id' => $zdlId, + 'status' => 1, + 'is_delete' => 0, + 'agent' => 1, + ])->find(); + + if (!$zdl) { + $data = encrypt_data(['status' => 0, 'message' => 'Tài khoản đại lý có vấn đề, không thể nạp']); + return json(['Success' => 1, 'Data' => $data]); + } + if ($zdl['money'] < $money) { + $maxAllowed = intval($zdl['money']); + $data = encrypt_data([ + 'status' => 0, + 'code' => 'AGENT_LIMIT', + 'message' => 'Hạn mức nạp mỗi lần hiện tại là ' . number_format($maxAllowed, 0, '.', ',') . ' VND. Vui lòng nhập số tiền nhỏ hơn hoặc bằng mức trên.', + 'max_amount' => $maxAllowed, + ]); + return json(['Success' => 1, 'Data' => $data]); + } + + $pay_list = config('pay_list'); + $pay_orderid = 'C' . date('YmdHis') . rand(100000, 999999); + $notify_url = $pay_list['ccpay']['notify_url'] ?? ''; + $timeout = intval($pay_list['ccpay']['timeout'] ?? 600); + + try { + $ccpay = new \pay\Ccpay(); + $result = $ccpay->createOrder( + $pay_orderid, + sprintf('%.0f', $money), + $notify_url, + 'uid_' . $user_id + ); + + if (!is_array($result)) { + addLogToFile('ccpay 下单: API 无响应', 'ccpay_error', 'ccpay'); + $data = encrypt_data(['status' => 0, 'message' => 'Kênh thanh toán không phản hồi, vui lòng thử lại']); + return json(['Success' => 1, 'Data' => $data]); + } + + $status = intval($result['status'] ?? -1); + $resultCode = intval($result['resultCode'] ?? -1); + + if ($status !== 0 || $resultCode !== 0) { + $errMsg = $result['errMsg'] ?? 'Lỗi không xác định'; + addLogToFile('ccpay 下单失败: order=' . $pay_orderid . ' err=' . $errMsg . ' raw=' . json_encode($result, JSON_UNESCAPED_UNICODE), 'ccpay_error', 'ccpay'); + $data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $errMsg]); + return json(['Success' => 1, 'Data' => $data]); + } + + $order_id = Db::name('order_record')->insertGetId([ + 'pay_channel_id' => 9, + 'pay_channel_name' => 'ccpay', + 'user_id' => $user_id, + 'order_sn' => $pay_orderid, + 'appid' => $ccpay->getConfigInfo()['mch_no'] ?? '', + 'api_key' => '', + 'money' => $money, + 'sign' => '', + 'create_time' => time(), + 'back_order_sn' => $result['cpOrderNo'] ?? '', + 'client' => $client, + ]); + + Db::name('order_log')->insert([ + 'pay_channel_id' => 9, + 'pay_channel_name' => 'ccpay', + 'money' => $money, + 'create_time' => time(), + 'remake' => 'user_id:' . $user_id . ',用户名:' . $username . ',CCpay发起充值:' . $money . ',时间:' . date('Y-m-d H:i:s'), + 'order_id' => $order_id, + 'client' => $client, + ]); + + $codeUrl = $result['codeUrl'] ?? ''; + $qrcode = null; + + $msgJson = $result['message'] ?? ''; + if ($msgJson) { + $bankInfo = json_decode($msgJson, true); + if (is_array($bankInfo) && !empty($bankInfo['AccNo'])) { + $expiresAt = time() + $timeout; + $qrcode = [ + 'order_sn' => $pay_orderid, + 'bank_account' => $bankInfo['AccNo'] ?? '', + 'bank_name' => $bankInfo['AccName'] ?? '', + 'bank_branch' => $bankInfo['Branch'] ?? '', + 'bank_short' => $bankInfo['BankShortName'] ?? '', + 'qr_data' => $bankInfo['qrcode'] ?? '', + 'remark' => $bankInfo['remark'] ?? '', + 'amount' => $money, + 'expires_at' => $expiresAt, + 'timeout_sec' => $timeout, + ]; + } + } + + addLogToFile('ccpay 下单成功: order=' . $pay_orderid . ' cp=' . ($result['cpOrderNo'] ?? ''), 'ccpay_request', 'ccpay'); + + $data = encrypt_data([ + 'status' => 1, + 'message' => 'Khởi tạo thanh toán thành công', + 'url' => $codeUrl, + 'qrcode' => $qrcode, + ]); + return json(['Success' => 1, 'Data' => $data]); + } catch (\Exception $e) { + addLogToFile('ccpay 下单异常: ' . $e->getMessage() . ' | file:' . $e->getFile() . ':' . $e->getLine(), 'ccpay_error', 'ccpay'); + $data = encrypt_data(['status' => 0, 'message' => 'Khởi tạo thanh toán thất bại: ' . $e->getMessage()]); + return json(['Success' => 1, 'Data' => $data]); + } + } + + public function ccpay_order_status() + { + $post = Request::instance()->post(); + if (isset($post['encryptData'])) { + $post = decrypt_data($post); + if (!$post) { + return json(['status' => 0, 'message' => '数据解密失败']); + } + } + + $order_sn = trim($post['order_sn'] ?? ''); + $user_id = intval($post['user_id'] ?? 0); + + if ($order_sn === '' || $user_id <= 0) { + $data = encrypt_data(['status' => 0, 'message' => '参数错误']); + return json(['Success' => 1, 'Data' => $data]); + } + + $order = Db::name('order_record') + ->where([ + 'order_sn' => $order_sn, + 'user_id' => $user_id, + 'pay_channel_id' => 9, + ]) + ->find(); + + if (!$order) { + $data = encrypt_data(['status' => 0, 'message' => '订单不存在']); + return json(['Success' => 1, 'Data' => $data]); + } + + if (intval($order['status']) == 3) { + $data = encrypt_data([ + 'status' => 1, + 'message' => 'Nạp tiền thành công', + 'money' => $order['money'], + 'paid_time' => $order['back_time'], + ]); + return json(['Success' => 1, 'Data' => $data]); + } + + $pay_list = config('pay_list'); + $timeout = intval($pay_list['ccpay']['timeout'] ?? 600); + if (time() - intval($order['create_time']) > $timeout) { + $data = encrypt_data([ + 'status' => 2, + 'message' => 'Đơn hàng đã hết hạn', + 'money' => $order['money'], + ]); + return json(['Success' => 1, 'Data' => $data]); + } + + $data = encrypt_data([ + 'status' => 0, + 'message' => 'Đang chờ thanh toán', + 'money' => $order['money'], + ]); + return json(['Success' => 1, 'Data' => $data]); + } + + public function ccpay_recharge_callback() + { + $raw = file_get_contents('php://input'); + $post = json_decode($raw, true); + if (!$post) { + $post = Request::instance()->post(); + } + + $json = json_encode($post, JSON_UNESCAPED_UNICODE); + addLogToFile('ccpay 回调原始数据: ' . $json, 'ccpay_callback', 'ccpay'); + + if (!is_array($post) || empty($post['mchOrderNo']) || empty($post['sign'])) { + Log::record($post, 'ccpay_error_callback'); + exit('fail'); + } + + $logId = Db::name('pay_callback_log')->insertGetId([ + 'pay_channel' => 'CCPAY_RECHARGE', + 'type' => 'recharge', + 'json' => $json, + 'create_time' => date('Y-m-d H:i:s'), + ]); + + try { + $ccpay = new \pay\Ccpay(); + + $verifyData = []; + $verifyData['status'] = strval($post['status'] ?? ''); + $verifyData['mchNo'] = $post['mchNo'] ?? ''; + $verifyData['resultCode'] = strval($post['resultCode'] ?? ''); + $verifyData['cpOrderNo'] = $post['cpOrderNo'] ?? ''; + $verifyData['mchOrderNo'] = $post['mchOrderNo'] ?? ''; + $verifyData['amount'] = strval($post['amount'] ?? ''); + $verifyData['payResult'] = $post['payResult'] ?? ''; + $verifyData['traceTime'] = $post['traceTime'] ?? ''; + $verifyData['message'] = $post['message'] ?? ''; + + $signData = array_filter($verifyData, function ($v) { + return $v !== '' && $v !== null; + }); + $signData['sign'] = $post['sign']; + + if (!$ccpay->verifySign($signData)) { + addLogToFile('ccpay 回调验签失败: ' . $json, 'ccpay_callback_error', 'ccpay'); + exit('fail'); + } + + if ($logId && !empty($post['mchOrderNo'])) { + Db::name('pay_callback_log')->where('id', $logId)->update(['unique_no' => $post['mchOrderNo']]); + } + + $payResult = strval($post['payResult'] ?? ''); + + if ($payResult === '0') { + $this->ccpay_recharge_settle($post); + addLogToFile('ccpay 回调成功: order=' . $post['mchOrderNo'], 'ccpay_callback', 'ccpay'); + exit('success'); + } else { + addLogToFile('ccpay 回调非成功状态: payResult=' . $payResult . ' order=' . $post['mchOrderNo'], 'ccpay_callback', 'ccpay'); + exit('success'); + } + } catch (\Exception $e) { + addLogToFile('ccpay 回调异常: ' . $e->getMessage(), 'ccpay_callback_error', 'ccpay'); + exit('fail'); + } + } + + private function ccpay_recharge_settle($data) + { + $orderNo = $data['mchOrderNo']; + + Db::startTrans(); + try { + $order = Db::name('order_record')->where('order_sn', $orderNo)->lock(true)->find(); + if (!$order || $order['status'] == 3) { + Db::rollback(); + return; + } + + $user = Db::name('user')->where([ + 'id' => $order['user_id'], + 'status' => 1, + 'is_delete' => 0, + 'agent' => 0, + ])->lock(true)->find(); + if (!$user) { + Db::rollback(); + return; + } + + $zdlIdArr = explode(',', $user['agent_parent_id_path']); + $zdlId = $zdlIdArr[0]; + + $zdl = Db::name('user')->where([ + 'id' => $zdlId, + 'status' => 1, + 'is_delete' => 0, + 'agent' => 1, + ])->lock(true)->find(); + + if (!$zdl || $zdl['money'] < $order['money']) { + Db::rollback(); + addLogToFile('ccpay 结算失败: 代理余额不足 order=' . $orderNo, 'ccpay_callback_error', 'ccpay'); + return; + } + + $actualMoney = isset($data['amount']) ? intval($data['amount']) : $order['money']; + + Db::name('order_record')->where('id', $order['id'])->update([ + 'back_money' => $actualMoney, + 'back_time' => time(), + 'status' => 3, + 'agent_parent_id' => $user['agent_parent_id'], + 'agent_parent_username' => $user['agent_parent_username'], + 'zdl_id' => $zdl['id'], + 'zdl_username' => $zdl['username'], + 'zdl_money_before' => $zdl['money'], + 'zdl_money_after' => $zdl['money'] - $order['money'], + 'money_before' => $user['money'], + 'money_after' => $user['money'] + $order['money'], + ]); + + Db::name('order_log')->insert([ + 'pay_channel_id' => 9, + 'pay_channel_name' => 'ccpay', + 'money' => $order['money'], + 'create_time' => time(), + 'remake' => 'CCpay回调成功 cp=' . ($data['cpOrderNo'] ?? '') . ' payer=' . ($data['message'] ?? ''), + 'order_id' => $order['id'], + ]); + + Db::name('recharge')->insert([ + 'type' => 4, + 'amount' => $order['money'], + 'mode' => 1, + 'agent_or_admin' => 3, + 'controller_id' => $zdl['id'], + 'controller_username' => $zdl['username'], + 'controller_nickname' => $zdl['nickname'], + 'controller_type' => 'CCpay越南银行第三方充值,扣取总代理余分', + 'controller_old_money' => $zdl['money'], + 'controller_new_money' => $zdl['money'] - $order['money'], + 'user_id' => $order['user_id'], + 'user_type' => $user['agent'], + 'user_agent_level' => 0, + 'username_for' => $user['username'], + 'nickname_for' => $user['nickname'], + 'user_parent_id' => $user['agent_parent_id'], + 'create_time' => time(), + 'old_money' => $user['money'], + 'new_money' => $user['money'] + $order['money'], + 'controller_system' => 3, + 'remake' => 'CCpay越南银行第三方充值成功', + ]); + + Db::name('user')->where('id', $zdl['id'])->update([ + 'money' => $zdl['money'] - $order['money'], + 'last_recharge_out' => $order['money'], + 'last_recharge_out_time' => time(), + 'recharge_out_count' => $zdl['recharge_out_count'] + 1, + 'recharge_out_total_amount' => $zdl['recharge_out_total_amount'] + $order['money'], + ]); + + Db::name('user')->where('id', $order['user_id'])->update([ + 'money' => $user['money'] + $order['money'], + 'last_recharge' => $order['money'], + 'last_recharge_time' => time(), + 'recharge_count' => $user['recharge_count'] + 1, + 'recharge_total_amount' => $user['recharge_total_amount'] + $order['money'], + ]); + + Db::commit(); + addLogToFile('ccpay 结算成功: order=' . $orderNo . ' money=' . $order['money'], 'ccpay_callback', 'ccpay'); + } catch (\Exception $e) { + Db::rollback(); + addLogToFile('ccpay 结算异常: ' . $e->getMessage(), 'ccpay_callback_error', 'ccpay'); + } + } +} diff --git a/extend/pay/Ccpay.php b/extend/pay/Ccpay.php new file mode 100644 index 0000000..447f3c3 --- /dev/null +++ b/extend/pay/Ccpay.php @@ -0,0 +1,132 @@ +config = config('pay_list.ccpay') ?: []; + $this->apiUrl = $this->config['api_url'] ?? ''; + $this->mchNo = $this->config['mch_no'] ?? ''; + $this->paySecret = $this->config['pay_secret'] ?? ''; + } + + public function getConfigInfo() + { + return $this->config; + } + + public function generateSign(array $params) + { + unset($params['sign']); + ksort($params); + $str = ''; + foreach ($params as $k => $v) { + if (is_string($v) && strlen($v) > 0) { + $str .= "{$k}={$v}&"; + } + } + $str = substr($str, 0, -1) . '&paySecret=' . $this->paySecret; + return strtoupper(md5($str)); + } + + public function verifySign(array $params) + { + $received = $params['sign'] ?? ''; + if (!$received) return false; + $calculated = $this->generateSign($params); + return hash_equals($calculated, $received); + } + + public function createOrder($orderNo, $amount, $notifyUrl, $memberId = 'og_user', $callbackUrl = '') + { + $extra = [ + 'bankType' => 'OFFLINE', + 'cardType' => '0', + 'orderPeriod' => '30', + 'memberId' => $memberId, + 'merchantName' => 'OG', + 'notifyUrl' => $notifyUrl, + 'callbackUrl' => $callbackUrl, + ]; + + $data = [ + 'tradeType' => 'cs.pay.submit', + 'version' => '2.0', + 'channel' => 'union_pay', + 'mchNo' => $this->mchNo, + 'body' => 'online_pay', + 'mchOrderNo' => $orderNo, + 'amount' => strval(intval($amount)), + 'currency' => 'VND', + 'timePaid' => date('YmdHis'), + 'remark' => 'online-pay', + ]; + + $signParams = array_merge($data, $extra); + $data['sign'] = $this->generateSign($signParams); + $data['extra'] = json_encode($extra); + + addLogToFile('ccpay 下单请求: order=' . $orderNo . ' amount=' . $amount . ' params=' . json_encode($data, JSON_UNESCAPED_UNICODE), 'ccpay_request', 'ccpay'); + + $result = $this->postForm($this->apiUrl, $data); + + addLogToFile('ccpay 下单返回: ' . json_encode($result, JSON_UNESCAPED_UNICODE), 'ccpay_response', 'ccpay'); + + return $result; + } + + public function queryOrder($orderNo) + { + $data = [ + 'tradeType' => 'cs.order.query', + 'version' => '2.0', + 'mchNo' => $this->mchNo, + 'mchOrderNo' => $orderNo, + ]; + $data['sign'] = $this->generateSign($data); + + return $this->postForm($this->apiUrl, $data); + } + + private function postForm($url, $params) + { + $ch = curl_init($url); + $body = ''; + foreach ($params as $k => $v) { + if (!is_string($v)) continue; + $body .= $k . '=' . urlencode($v) . '&'; + } + $body = rtrim($body, '&'); + + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $body, + CURLOPT_HTTPHEADER => ['Content-Type: application/x-www-form-urlencoded; charset=UTF-8'], + CURLOPT_CONNECTTIMEOUT => 10, + CURLOPT_TIMEOUT => 30, + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => false, + ]); + + $resp = curl_exec($ch); + $err = curl_error($ch); + $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + + if ($resp === false) { + addLogToFile("ccpay curl 失败: code={$code} err={$err}", 'ccpay_error', 'ccpay'); + return null; + } + + return json_decode($resp, true); + } +}