load(); } public function goods() { global $config; $oil = $this->combine_goods($config['oil_providers']); $phone = $this->combine_goods($config['phone_providers']); return array_merge($oil, $phone); } public function providers() { return ['oil' => $this->mOilProvider,'phone' => $this->mPhoneProvider]; } private function combine_goods($configs) { $result = []; foreach ($configs as $item) { $cfg = $item['cfg']; $card_types = $cfg['card_type'] ?? []; $amounts = $cfg['amount'] ?? []; foreach ($card_types as $type) { if (array_key_exists($type, $result)) { $item = $result[$type]; } else { $item = []; } foreach ($amounts as $amount => $val) { $item[] = $amount; } $item = array_unique($item); $result[$type] = $item; } } return $result; } private function load() { global $config; $oil_configs = $config['oil_providers']; $names = []; foreach ($oil_configs as $item) { $name = $item['name']; $cfg = $item['cfg']; $opened = $item['opened'] ?? true; $sort = $item['sort'] ?? 65536; $names[] = $name; try { $class = "refill\\{$name}\\RefillOil"; if (class_exists($class, false)) { $provider = new $class($cfg); $provider->setOpened($opened); $provider->setSort($sort); $this->mOilProvider[] = $provider; } else { $error = "Base Error: class {$class} isn't exists!"; throw new Exception($error); } } catch (Exception $ex) { Log::record($ex->getMessage(), Log::ERR); } } $pho_configs = $config['phone_providers']; foreach ($pho_configs as $item) { $name = $item['name']; $cfg = $item['cfg']; $opened = $item['opened'] ?? true; $sort = $item['sort'] ?? 65536; $names[] = $name; try { $class = "refill\\{$name}\\RefillPhone"; if (class_exists($class, false)) { $provider = new $class($cfg); $provider->setOpened($opened); $provider->setSort($sort); $this->mPhoneProvider[] = $provider; } else { $error = "Base Error: class {$class} isn't exists!"; throw new Exception($error); } } catch (Exception $ex) { Log::record($ex->getMessage(), Log::ERR); } } $this->mProviderNames = array_unique($names); } public function find_providers(int $amount, int $card_type): array { if ($card_type == mtopcard\SinopecCard || $card_type == mtopcard\PetroChinaCard) { return $this->find_oil($amount, $card_type); } elseif ($card_type == mtopcard\ChinaMobileCard || $card_type == mtopcard\ChinaUnicomCard || $card_type == mtopcard\ChinaTelecomCard) { return $this->find_phone($amount, $card_type); } else { return []; } } private function find_oil(int $amount, int $card_type): array { $providers = []; foreach ($this->mOilProvider as $provider) { $name = $provider->name(); [$success, $err] = $provider->check($amount, $card_type); if ($success) { $providers[] = $provider; } else { Log::record("{$name} provider cannot match check,err:{$err}", Log::DEBUG); } } return $providers; } public function provider(string $chname, int $card_type) { if ($card_type == mtopcard\SinopecCard || $card_type == mtopcard\PetroChinaCard) { $providers = $this->mOilProvider; } elseif ($card_type == mtopcard\ChinaMobileCard || $card_type == mtopcard\ChinaUnicomCard || $card_type == mtopcard\ChinaTelecomCard) { $providers = $this->mPhoneProvider; } else { return null; } foreach ($providers as $provider) { if ($provider->name() == $chname) { return $provider; } } return null; } private function find_phone(int $amount, int $card_type): array { $providers = []; foreach ($this->mPhoneProvider as $provider) { $name = $provider->name(); [$success, $err] = $provider->check($amount, $card_type); if ($success) { $providers[] = $provider; } else { Log::record("{$name} provider cannot match check,err:{$err}", Log::DEBUG); } } return $providers; } public function notify($chname, $input) { try { $class_name = "refill\\{$chname}\\RefillCallBack"; if (class_exists($class_name, false)) { $caller = new $class_name(); } else { $error = "Base Error: class {$class_name} isn't exists!"; throw new Exception($error); } } catch (Exception $ex) { Log::record($ex->getMessage(), Log::ERR); return false; } if ($caller->verify($input)) { [$order_id, $success, $can_try, $need_handle] = $caller->notify($input); if (!$need_handle) { return true; } if ($order_id !== false) { $mod_order = Model('vr_order'); $order_info = $mod_order->getOrderInfo(['order_id' => $order_id]); $order_state = intval($order_info['order_state']); if ($order_state != ORDER_STATE_SEND) { return false; } $logic_vr_order = Logic("vr_order"); if ($success) { $logic_vr_order->changeOrderStateSuccess($order_id); } elseif ($can_try) { try { $mod_refill = Model('refill_order'); $trans = new trans_wapper($mod_refill, __METHOD__); $refill_info = $mod_refill->getOrderInfo(['order_id' => $order_id,'inner_status' => 0]); if(!empty($refill_info)) { $logic_vr_order->changeOrderStateCancel($order_info, '', "{$chname}接口回调通知失败,正在重试"); if ($this->retry($refill_info, $order_info)) { $mod_refill->edit($order_id, ['inner_status' => 1, 'notify_time' => time(), 'notify_state' => 1]); $trans->commit(); return true; } } $trans->commit(); } catch (Exception $ex) { $trans->rollback(); Log::record("Error:" . $ex->getMessage(),Log::ERR); } } else { $logic_vr_order->changeOrderStateCancel($order_info, '', "{$chname}接口回调通知失败,不可重试."); } $mod_refill = Model('refill_order'); $mod_refill->edit($order_id, ['notify_time' => time(), 'notify_state' => 1]); QueueClient::push("NotifyMerchantComplete", ['order_id' => $order_id]); } else { Log::record("系统无此订单ID:{$order_id}", Log::ERR); } } else { Log::record("{$chname} 签名失败."); } return true; } private function retry(array $refill_info, array $order_info) { $checker = function ($refill, $order) { $state = intval($order['order_state']); if ($state !== ORDER_STATE_SEND) { return false; } // $times = intval($refill['commit_times']); // if ($times > 5) { // return false; // } $period = time() - $refill['order_time']; if ($period >= 10 * 60) { return false; } return true; }; $can_try = $checker($refill_info, $order_info); if (!$can_try) { return false; } $mchid = $refill_info['mchid']; $buyer_id = $order_info['buyer_id']; $amount = $refill_info['refill_amount']; $card_no = $refill_info['card_no']; $mch_order = $refill_info['mch_order']; $notify_url = $refill_info['notify_url']; $commit_times = $refill_info['commit_times'] + 1; $order_time = $refill_info['order_time']; $idcard = $refill_info['idcard'] ?? ''; $card_name = $refill_info['card_name'] ?? ''; [$success, $err] = $this->add($mchid, $buyer_id, $amount, $card_no, $mch_order, $idcard, $card_name, $notify_url, $order_time, $commit_times); return ($success === true); } public function add($mchid, $buyer_id, $amount, $card_no, $mch_order, $idcard, $card_name, $notify_url, $order_time = 0, $commit_times = 0) { $card_type = mtopcard\card_type($card_no); $providers = $this->find_providers($amount, $card_type); if (empty($providers)) { return [202, "找不到合适的充值通道"]; } if (empty($notify_url)) { $notify_url = ""; } $minfo = new member_info($buyer_id); $calc = new CalcMerchantPrice($mchid, $amount, $card_type); $mch_amount = $calc->calc_vgoods_price([]); $available = $minfo->available_predeposit(); if ($mch_amount > $available) { Log::record("下单时机构余额不足,可用余额为:{$available}", Log::DEBUG); return [203, "余额不足"]; } if ($order_time === 0) { $order_time = time(); } $ascending = function ($l, $r) use ($amount) { [$lid, $lprice] = $l->goods($amount); [$rid, $rprice] = $r->goods($amount); $lsort = $l->sort(); $rsort = $r->sort(); if($lprice == $rprice) { return $lsort < $rsort ? -1 : 1; } else { return $lprice < $rprice ? -1 : 1; } }; usort($providers, $ascending); $refill_state = false; foreach ($providers as $provider) { if(!$provider->opened()) continue; $channel_name = $provider->name(); [$goods_id, $price] = $provider->goods($amount); if ($price > $mch_amount) continue; $input['goods_id'] = $goods_id; $input['quantity'] = 1; $input['price'] = $price; $input['buyer_phone'] = $minfo->mobile(); $input['buyer_name'] = $minfo->truename(); $input['buyer_msg'] = $_POST['buyer_msg'] ?? ''; $input['order_from'] = 1; $input['pd_pay'] = true; $logic_buy_virtual = Logic('buy_virtual'); $result = $logic_buy_virtual->buyStep3($input, $buyer_id, [$calc, 'calc_vorder_amount'], true); $mod_refill = Model('refill_order'); if ($result['state'] === true) { $order_sn = $result['data']['order_sn']; $order_id = $result['data']['order_id']; if (empty($mch_order)) { $mch_order = $order_sn; } //虚拟订单表信息扩展 $orderext = ['order_id' => $order_id, 'order_sn' => $order_sn, 'mchid' => $mchid, 'refill_amount' => $amount, 'mch_order' => $mch_order, 'idcard' => $idcard, 'card_name' => $card_name, 'notify_url' => $notify_url, 'channel_name' => $channel_name, 'mch_amount' => $mch_amount, 'channel_amount' => $price, 'order_time' => $order_time, 'commit_times' => $commit_times, 'card_type' => $card_type, 'card_no' => $card_no]; $mod_refill->add_refill($orderext); } else { continue; } $params = ['order_sn' => $order_sn, 'idcard' => $idcard, 'card_name' => $card_name]; [$state, $err] = $provider->add($card_no, $card_type, $amount, $params); if ($state) { $trade_no = $err; if ($provider->refill_type() == 'api') { $logic_vr_order = Logic("vr_order"); $logic_vr_order->changeOrderStateSend($order_id); } $data = ['commit_time' => time(), 'ch_trade_no' => $trade_no]; $mod_refill->edit($order_id, $data); $refill_state = true; //如果对方没有回调能力,则启动主动查询. if($provider->callback() === false) { QueueClient::async_push("QueryRefillState",['order_id' => $order_id],180); } break; } else { Log::record("channel:{$channel_name} err:{$err}"); $logic_vr_order = Logic("vr_order"); $order_info = Model('vr_order')->getOrderInfo(['order_id' => $order_id]); $logic_vr_order->changeOrderStateCancel($order_info, '', "调用{$channel_name}接口失败"); $mod_refill->edit($order_id, ['commit_time' => time(), 'inner_status' => 1]); } } if ($refill_state) { return [true, $order_sn]; } else { return [204, "充值失败."]; } } private function is_url($url) { $checker = function ($haystack, $needle) { $length = strlen($needle); return (substr($haystack, 0, $length) === $needle); }; return $checker($url, "http://") || $checker($url, "https://"); } public function notify_merchant($order_id) { if ($order_id <= 0) { return [false, "订单ID小于0"]; } $vr_order = Model('vr_order'); $refill_order = Model('refill_order'); $order_info = $vr_order->getOrderInfo(['order_id' => $order_id]); $refill_info = $refill_order->getOrderInfo(['order_id' => $order_id]); if (empty($order_info) || empty($refill_info)) { return [false, "无此订单"]; } if ($refill_info['mch_notify_state'] != 0) { return [false, "已经通知客户方"]; } $notify_url = $refill_info['notify_url']; if (empty($notify_url)) { $refill_order->edit($order_id, ['mch_notify_state' => 1, 'mch_notify_times' => 0]); return [false, "回调地址为空"]; } $order_state = $order_info['order_state']; if ($order_state == ORDER_STATE_CANCEL) { $state = "CANCEL"; } elseif ($order_state == ORDER_STATE_SUCCESS) { $state = "SUCCESS"; } else { return [false, "错误的订单状态,不能通知."]; } $mchid = $refill_info['mchid']; $mch_info = Model('merchant')->getMerchantInfo(['mchid' => $mchid]); [$params, $sign] = $this->body($state, $refill_info, $mch_info); $params['sign'] = $sign; //如果http请求内部,又发出回调自己的请求,在处理进程非动态扩容的情况下,容易造成阻塞. if ($this->is_url($notify_url)) { $resp = http_request($notify_url, $params, 'POST'); } else { $resp = RBridgeFactory::instance()->notify($notify_url, $params); } if ($resp == "SUCCESS") { $refill_order->edit($order_id, ['mch_notify_state' => 1, 'mch_notify_times' => ['exp', 'mch_notify_times+1']]); return [true, ""]; } else { $refill_order->edit($order_id, ['mch_notify_times' => ['exp', 'mch_notify_times+1']]); $times = $refill_info['mch_notify_times'] + 1; if ($times > 80) { $refill_order->edit($order_id, ['mch_notify_state' => 2]); } else { $N = intval($times / 5); $period = intval(pow(2, $N)); QueueClient::async_push("NotifyMerchantComplete", ['order_id' => $order_id], $period); } return [false, "通知{$times}次,失败."]; } } public function query($order_id) { $mod_refill = Model('refill_order'); $refill_info = $mod_refill->getOrderInfo(['order_id' => $order_id,'inner_status' => 0]); $chname = $refill_info['channel_name']; $card_type = intval($refill_info['card_type']); $pthis = $this; $finder = function ($chname, $card_type) use ($pthis) { if ($card_type == mtopcard\SinopecCard || $card_type == mtopcard\PetroChinaCard) { $providers = $pthis->mOilProvider; } else { $providers = $pthis->mPhoneProvider; } foreach ($providers as $provider) { if ($chname == $provider->name()) { return $provider; } } return NULL; }; $provider = $finder($chname, $card_type); [$state, $order_state] = $provider->query($refill_info); if ($state === true) { $notify_state = $refill_info['notify_state']; if($notify_state == 0) { $modify_able = true; if(!$provider->callback()) { $logic_vr_order = Logic("vr_order"); if ($order_state == ORDER_STATE_SUCCESS) { $logic_vr_order->changeOrderStateSuccess($order_id); } elseif ($order_state == ORDER_STATE_CANCEL) { $mod_order = Model('vr_order'); $order_info = $mod_order->getOrderInfo(['order_id' => $order_id]); $logic_vr_order->changeOrderStateCancel($order_info, '', "{$chname}接口查询失败,不再重试."); } else { $modify_able = false; QueueClient::async_push("QueryRefillState",['order_id' => $order_id],180); } } if($modify_able) { $mod_refill->edit($order_id, ['notify_time' => time(), 'notify_state' => 1]); QueueClient::push("NotifyMerchantComplete", ['order_id' => $order_id]); } } return true; } else { return false; } } private function body($state, $refill_info, $mch_info) { $params = [ "mchid" => $refill_info['mchid'], "order_sn" => $refill_info['mch_order'], "amount" => $refill_info['refill_amount'], "cardno" => $refill_info['card_no'], "trade_no" => $refill_info['order_sn'], "idcard" => $refill_info['idcard'] ?? "", "card_name" => $refill_info['card_name'] ?? "", 'official_sn' => $refill_info['official_sn'] ?? "", "state" => $state]; $secure_key = $mch_info['secure_key']; $sign = $this->sign($params, $secure_key); return [$params, $sign]; } private function sign($params, $key) { ksort($params); $body = ""; $i = 0; foreach ($params as $k => $v) { if (false === $this->check_empty($v) && "@" != substr($v, 0, 1)) { if ($i == 0) { $body .= "{$k}" . "=" . urlencode($v); } else { $body .= "&" . "{$k}" . "=" . urlencode($v); } $i++; } } $body .= "&key={$key}"; return md5($body); } private function check_empty($value) { if (!isset($value)) return true; if ($value === null) return true; if (trim($value) === "") return true; return false; } }