<?php

namespace Server\payment;

use App\Services\AccountService;
use App\Services\HttpService;
use App\Services\MicroService;
use Alipay\EasySDK\Kernel\Factory;
use Alipay\EasySDK\Kernel\Config;
use App\Utils\WeModule;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Symfony\Component\Process\Process;
use WeChatPay\Builder;
use WeChatPay\Crypto\Rsa;
use WeChatPay\Formatter;
use WeChatPay\Util\PemUtil;

const SERVICE_PAYMENT = MICRO_SERVER . "payment/";

class PaymentService extends MicroService
{

    public $configs = array(

    );
    public $uniacid = 0;
    public $tablePayLog = "core_paylog";
    public $states = array('待支付','已支付','已取消','已退款','已失效');
    public $payments = array(
        'alipay'=>'支付宝',
        'wechat'=>"微信支付",
        'credit'=>"余额支付",
        'paypal'=>"PayPal"
    );

    function __construct($uniacid=0){
        parent::__construct('payment');
        global $_W;
        if (empty($uniacid)){
            $uniacid = $_W['uniacid'];
        }
        $this->uniacid = intval($uniacid);
        $this->Unique = true;
        $setting = $this->SettingLoad('payment', $uniacid);
        if (!empty($setting['payment'])){
            $this->configs = $setting['payment'];
        }
    }

    /**
     * HTTP方式访问
     * @param string|null $platform 路由通道，可选web、app、api及自定义通道
     * @param string|null $route 路由名称
     * @return array|\error 返回接口数据或报错信息
     * @throws \Exception
     */
    public function HttpRequest($platform="web", $route=""){
        global $_GPC, $_W;
        if (empty($route)){
            $route = empty($_GPC['ctrl']) ? 'index' : trim($_GPC['ctrl']);
        }
        $route = str_replace(".","/",$route);
        list($controller, $method) = explode("/", $route);
        if (empty($method)) $method = 'main';

        $class = "Server\\{$this->identity}\\{$platform}\\" . ucfirst($controller) . "Controller";
        if (!class_exists($class)){
            $class = "Server\\{$this->identity}\\{$platform}\IndexController";
            $method = $controller;
            $controller = 'index';
        }

        if($platform=='app'){
            if (!function_exists('message')){
                require_once app_path("Helpers/app.php");
            }
        }

        $_W['controller'] = $controller;
        $_W['action'] = $method;

        if (!class_exists($class)) return error(-1,__('controllerNotFound', ['ctrl'=>$class]));
        $instance = new $class();
        if (!method_exists($instance,$method)) return error(-1,"Method $class::$method() dose not exist!");
        return $instance->$method();
    }

    public function getone($id, $column='plid'){
        if (is_array($id)) return $id;
        return pdo_get($this->tablePayLog, array(
            $column=>$column=='plid'?intval($id):trim($id)
        ));
    }

    /**
     * 统一收银台（H5）
     * @param array $params 支付参数（金额amount、订单号tid、商品描述subject）
     * @param string|null $module 来源模块/微服务
     * @return array|bool|mixed
     */
    public function cashier($params, $module=''){
        $payLog = $this->orderBuild($params, $module);
        if (is_error($payLog)){
            if ($payLog['errno']==-1){
                //支付成功
                $notify = $this->payResult($params['tid'], 'return');
                if (is_error($notify) || is_array($notify) || is_bool($notify)){
                    return $this->message("支付成功！", "", "success");
                }else{
                    return $notify;
                }
            }
            return $this->message($payLog['message']);
        }
        global $_W;
        $params['amount'] = $payLog['fee'];
        $params['subject'] = $payLog['tag'];
        $params['openid'] = $payLog['openid'];
        $setting = $this->configs;
        $_W['inWeiXin'] = strpos($_SERVER['HTTP_USER_AGENT'], 'MicroMessenger')!==false;
        $payments = array(
            'wechat'=>['enabled'=>(bool)$setting['wechat']['pay_switch'], 'url'=>$this->payUrl('wechat',['tid'=>$params['tid']])],
            'alipay'=>['enabled'=>(bool)$setting['alipay']['pay_switch'], 'url'=>$this->payUrl('alipay',['tid'=>$params['tid']])],
            'credit'=>['enabled'=>(bool)$setting['credit']['pay_switch'], 'url'=>$this->payUrl('credit',['tid'=>$params['tid']])]
        );
        if (!empty($params['isRecharge'])){
            //充值模式
            $payments['wechat']['enabled'] = (bool)$setting['wechat']['recharge_switch'];
            $payments['wechat']['alipay'] = (bool)$setting['alipay']['recharge_switch'];
            $payments['credit']['enabled'] = false;
        }
        if(!$_W['inWeiXin'] && !$setting['wechat']['h5_switch']){
            //非微信且未开启微信H5支付
            $payments['wechat']['enabled'] = false;
        }
        if (!$_W['member']['uid']){
            //未登录
            $payments['credit']['enabled'] = false;
        }
        return $this->View([
            'payment'=>$payments,
            'paylog'=>$payLog,
            'params'=>$params,
            'member'=>$_W['member'],
            'title'=>'收银台'
        ], 'cashier');
    }

    public function payUrl($route='', $params=array())
    {
        $params['state'] = 'we7sid-' . session()->getId();
        return $this->api($route, $params, 'app');
    }

    /**
     * 统一下单
     * @param array $params 订单信息
     * @param string|null $module 订单来源
     * @return array 返回系统订单信息
    */
    public function orderBuild($params, $module='core'){
        //查询已有订单
        $payLog = $this->getone($params['tid'], 'tid');
        if (empty($payLog)){
            if (empty($params['tid'])) return $this->error("订单号(tid)不能为空", -11);
            if (empty($params['amount'])) return $this->error("订单金额(amount)不能为空", -12);
            if (empty($params['subject'])) return $this->error("商品描述subject不能为空", -13);
            //插入订单
            $log = array(
                "tid"=>trim($params['tid']),
                "fee"=>floatval($params['amount']),
                "tag"=>trim($params['subject']),
                "module"=>$module,
                'is_usecard'=>0,
                'card_type'=>0,
                'card_id'=>0,
                'card_fee'=>0,
                "uniontid"=>trim($params['uniontid']),
                "encrypt_code"=>random(6,true),
                "is_wish"=>0,
                "uniacid"=>$this->uniacid
            );
            if (isset($params['openid'])){
                $log['openid'] = trim($params['openid']);
            }
            if (empty($log['uniontid'])){
                $mid = 0;
                if (strpos($module, 'server:')!==false){
                    $mid = (int)pdo_getcolumn($this->tableName, array('identity'=>str_replace('server:', '', $module)), 'id');
                }elseif ($module!='core'){
                    $mid = (int)DB::table('modules')->where(array('name' => $module))->value('mid');
                }
                $log['uniontid'] = date('YmdHis').(sprintf("%04d", $mid)).random(6,true);
            }
            $id = DB::table("core_paylog")->insertGetId($log);
            if (!$id){
                return $this->error("订单保存失败，请重试", -14);
            }
            return $this->getone($id);
        }else{
            if (!empty($module) && $payLog['module']!=$module){
                return $this->error("重复的订单号(tid)", -15);
            }
            if ($payLog['status']!=0){
                return $this->error("该订单已完成或已取消", 0 - $payLog['status']);
            }
            $update = array();
            if (!empty($params['amount'])){
                $update['fee'] = floatval($params['amount']);
            }
            if (!empty($params['subject'])){
                $update['tag'] = trim($params['subject']);
            }
            if (isset($params['openid'])){
                $update['openid'] = trim($params['openid']);
            }
            if (!empty($update)){
                DB::table("core_paylog")->where("plid", $payLog['plid'])->update($update);
                $payLog = array_merge($payLog, $update);
            }
        }
        return $payLog;
    }

    /**
     * 根据订单信息创建支付页面
     * @param string $type 支付方式，alipay、wechat
     * @param array $orderInfo 订单信息（uniontid、title、fee、openid?）
     * @return string|array 返回可直接唤醒支付的脚本或报错信息
     */
    public function create($type, $orderInfo=array()){
        $result = false;
        $config = $this->configs;
        switch($type){
            case 'alipay':
                if(empty($config['alipay'])){
                    return error(-1,'未配置支付宝支付');
                }
                if($config['alipay']['pay_switch'] != 1){
                    return error(-1,'未开启支付宝支付');
                }
                $result = $this->AliPayCreate($orderInfo);
                break;
            case 'wechat':
                if(empty($config['wechat'])){
                    return error(-1, "未配置微信支付！");
                }
                if($config['wechat']['pay_switch'] != 1){
                    return error(-1, "暂未开放微信支付！");
                }
                $params = array(
                    "out_trade_no"=>$orderInfo['uniontid'],
                    "subject"=>$orderInfo['title'],
                    "total_amount"=>$orderInfo['fee']
                );
                if (!empty($orderInfo['openid'])){
                    $params['openid'] = $orderInfo['openid'];
                }
                $result = $this->WechatCreate($params);
                break;
            default:
                break;
        }
        pdo_update($this->tablePayLog, array('type'=>$type), array('uniontid'=>$orderInfo));
        return $result;
    }

    public function queryBuild($params=array()){
        $tid = request()->input("tid", $params['tid']);
        if (empty($params)){
            $_params = request()->input('params', "");
            $params = $_params=="" ? [] : @json_decode(base64_decode($_params), true);
            if (!empty($params['tid'])){
                $tid = $params['tid'];
            }
        }
        return $this->getone($tid, "tid");
    }

    /**
     * 支付宝网页下单
     * $config 支付配置
     * $orderInfo 订单信息
     */
    public function AliPayCreate($orderInfo, $config=array(), $cert =false){
        global $_W;
        if ($config['sign_type']=='MD5'){
            $orderInfo['title'] = $orderInfo['tag'];
            $ret = $this->AlipayBuild($orderInfo, $config);
            return '<title>支付宝支付</title><script type="text/javascript" src="/static/payment/alipay/ap.js"></script><script type="text/javascript">_AP.pay("'.$ret['url'].'")</script>';
        }
        $this->Composer();
        Factory::setOptions($this->AlipayOptions($config, $cert));
        try {
            $result = Factory::payment()->Wap()->pay($orderInfo['tag'], $orderInfo['uniontid'], trim($orderInfo['fee']), $_W['siteurl'], $this->api("return/alipay", [], "app"));
            if (!empty($result->body)){
                return $result->body;
            }
            return $this->error("支付失败");
        }catch (\Exception $exception){
            return $this->error($exception->getMessage());
        }
    }

    public function AlipayBuild($params, $alipay = array()){
        $tid = $params['uniontid'];
        $set = array();
        $set['service'] = 'alipay.wap.create.direct.pay.by.user';
        $set['partner'] = $alipay['partner'];
        $set['_input_charset'] = 'utf-8';
        $set['sign_type'] = 'MD5';
        $set['notify_url'] = $this->api("notify/alipay");
        $set['return_url'] = $this->api("return/alipay", [], "app");
        $set['out_trade_no'] = $tid;
        $set['subject'] = $params['title'];
        $set['total_fee'] = $params['fee'];
        $set['seller_id'] = $alipay['account'];
        $set['payment_type'] = 1;
        $set['body'] = $this->uniacid;
        if ($params['service'] == 'create_direct_pay_by_user') {
            $set['service'] = 'create_direct_pay_by_user';
            $set['seller_id'] = $alipay['partner'];
        } else {
            $set['app_pay'] = 'Y';
        }
        $prepares = array();
        foreach($set as $key => $value) {
            if($key != 'sign' && $key != 'sign_type') {
                $prepares[] = "{$key}={$value}";
            }
        }
        sort($prepares);
        $string = implode('&', $prepares);
        $string .= $alipay['secret'];
        $set['sign'] = md5($string);
        //dd($string, $params, $set);

        $response = HttpService::ihttp_request('https://mapi.alipay.com/gateway.do?' . http_build_query($set, '', '&'), array(), array('CURLOPT_FOLLOWLOCATION' => 0));
        if (empty($response['headers']['Location'])) {
            session_exit(iconv('gbk', 'utf-8', $response['content']));
        }
        return array('url' => $response['headers']['Location']);
    }

    public function AlipayQuery($params, $config=array()){
        $this->Composer();
        Factory::setOptions($this->AlipayOptions($config));
        try {
            $result = Factory::payment()->common()->query($params['out_trade_no']);
            if (!empty($result->code) && $result->code == 10000) {
                return array(
                    "msg"=>$result->msg,
                    "code"=>$result->code,
                    "tradeStatus"=>$result->tradeStatus,
                    "totalAmount"=>$result->totalAmount,
                    "tradeNo"=>$result->tradeNo,
                    "buyerUserId"=>$result->buyerUserId,
                    "outTradeNo"=>$result->outTradeNo
                );
            }else{
                return $this->error($result->msg."(".$result->subMsg.")");
            }
        }catch (\Exception $exception){
            return $this->error($exception->getMessage());
        }
    }

    public function AlipayOptions($config, $cert=false){
        if (empty($config)) $config = $this->configs['alipay'];
        $this->Composer();
        $options = new Config();
        $options->protocol = 'https';
        $options->gatewayHost = 'openapi.alipay.com';
        $options->signType = 'RSA2';
        $options->appId = $config['appid'];
        $options->merchantPrivateKey = $config['privatekey'];
        if ($cert){
            //证书模式：待完善
            $options->alipayCertPath = '';
            $options->alipayRootCertPath = '';
            $options->merchantCertPath = '';
        }else{
            $options->alipayPublicKey = $config['publickey'];
        }
        $options->notifyUrl = $this->api("notify/alipay");
        return $options;
    }

    public function AlipayVerify($params){
        $payResult = array('trade_status'=>'TRADE_SUCCESS','total_amount'=>0,'isxml'=>false);
        $payResult['out_trade_no'] = $params['out_trade_no'];
        if($params['sign_type']=='RSA2'){
            $result = $params['payResult'] ?: $this->AlipayQuery($params);
            if (is_error($result)) return $result;
            if ($result['tradeStatus']!="TRADE_SUCCESS"){
                return error((0-$result['code']), $result['msg']);
            }
            $payResult['total_amount'] = $params['total_amount'] = floatval($result['totalAmount']);
        }else{
            $sign = $this->AlipayGenSign($params);
            if ($sign!=$params['sign']){
                return error(-1, '签名验证失败：'.$sign);
            }
            if (isset($params['total_fee'])){
                $payResult['total_amount'] = $params['total_fee'];
            }
        }
        return $payResult;
    }

    public function AlipayGenSign($data){
        unset($data['sign'], $data['sign_type'], $data['i']);
        $prepares = [];
        foreach($data as $key => $value) {
            if($key != 'sign' && $key != 'sign_type') {
                $prepares[] = "$key=$value";
            }
        }
        sort($prepares);
        $string = implode('&', $prepares);
        $string .= $this->configs['alipay']['secret'];
        return md5($string);
    }

    public function AlipayRefund($params, $config=array()){
        $this->Composer();
        Factory::setOptions($this->AlipayOptions($config));

        try {
            $result = Factory::payment()->common()->refund($params['out_trade_no'], trim($params['fee']));
            if (!empty($result->code) && $result->code == 10000) {
                return array(
                    "msg"=>$result->msg,
                    "code"=>$result->code,
                    "tradeNo"=>$result->tradeNo,
                    "buyerUserId"=>$result->buyerUserId,
                    "outTradeNo"=>$result->outTradeNo
                );
            }else{
                return $this->error($result->msg."(".$result->subMsg.")");
            }
        }catch (\Exception $exception){
            return $this->error($exception->getMessage());
        }
    }

    /**
     * 支付宝APP下单
     * @param array $params 支付参数（金额total_amount、外部订单号out_trade_no、商品描述subject）
     * @param array $config 支付配置
     * @return array|string 返回WxPAY-SDK支付所需要的支付参数或报错信息
    */
    public function AlipayApp($params, $config=array()){
        $this->Composer();
        Factory::setOptions($this->AlipayOptions($config));
        try {
            $result = Factory::payment()->app()->pay($params['subject'], $params['out_trade_no'], $params['total_amount']);
            return $result->body ?? error(-1, "支付接口异常");
        }catch (\Exception $exception){
            return $this->error($exception->getMessage());
        }
    }

    public function WechatQuery($out_trade_no, $config=[]){
        if (empty($config)) $config = $this->configs['wechat'];
        $this->Composer();
        try {
            $Builder = $this->WechatBuilder($config);
            if (is_error($Builder)) return $Builder;
            $chain = "v3/pay/transactions/out-trade-no/$out_trade_no?mchid={$config['mchid']}";
            $instance = $Builder[0];
            $resp = $instance->chain($chain)->get();
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return json_decode($resp->getBody(), true);
        }catch (\Exception $exception){
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return $this->WechatResponse($exception->getMessage());
        }
    }

    /**
     * 微信APP支付
     * @param array $params 支付参数（金额total_amount、外部订单号out_trade_no、商品描述subject）
     * @param array $config 支付配置
     * @return array 返回WxPAY-SDK支付所需要的支付参数
    */
    public function WechatApp($params, $config=[]){
        if (empty($config)){
            $config = $this->configs['wechat'];
            if (empty($config['app_switch'])){
                return error(-1, "暂不支持微信APP支付");
            }
        }
        $this->Composer();
        try {
            $Builder = $this->WechatBuilder($config);
            if (is_error($Builder)) return $Builder;
            $total = intval($params['total_amount']*100);
            $postData = [
                'appid'=>$config['app_appid'],
                'mchid'=>$config['mchid'],
                'description'=>$params['subject'],
                'out_trade_no'=>$params['out_trade_no'],
                'notify_url'=>$GLOBALS['_W']['siteroot']."api/server/run/payment/notify".$this->uniacid,
                'amount'=>[
                    'total'=>$total
                ]
            ];
            $payApi = "v3/pay/transactions/app";
            if ($config['partner']){
                $payApi = "v3/pay/partner/transactions/app";
                $postData['sp_appid'] = $config['sp_appid'];
                $postData['sp_mchid'] = $config['sp_mchid'];
                $postData['sub_mchid'] = $config['mchid'];
                $postData['sub_appid'] = $config['app_appid'];
                unset($postData['appid'], $postData['mchid']);
            }
            if (!empty($params['time_expire'])){
                $postData['time_expire'] = $params['time_expire'];
            }
            list($instance, $merchantPrivateKeyInstance) = $Builder;
            $resp = $instance->chain($payApi)->post(['json'=>$postData]);
            $package = json_decode($resp->getBody(), true);
            $response = [
                'appid'     => $config['app_appid'],
                'timestamp' => (string)Formatter::timestamp(),
                'noncestr'  => Formatter::nonce(),
                'prepayid'  => $package['prepay_id']
            ];
            $response += ['sign'=>Rsa::sign(
                Formatter::joinedByLineFeed(...array_values($response)),
                $merchantPrivateKeyInstance
            ), 'partnerid'=>$config['partner']?$config['sp_mchid']:$config['mchid'], 'package'=>"Sign=WXPay"];
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return $response;
        }catch (\Exception $exception){
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return $this->WechatResponse($exception->getMessage());
        }
    }

    /**
     * 构造微信实例
     * @param array $config 支付配置
     * @return array 微信支付实例
    */
    public function WechatBuilder($config){
        // 检查是否使用公钥证书方式
        if (!empty($config['use_public_cert']) && $config['use_public_cert'] == 1) {
            return $this->WechatBuilderWithPublicCert($config);
        }

        // 原有的平台证书方式
        $merchantKeyFilePaths = $this->WechatCert(['key', 'platform'], true);
        $merchantPrivateKeyInstance = Rsa::from("file://".$merchantKeyFilePaths['key'], Rsa::KEY_TYPE_PRIVATE);
        $platformCertificateFilePath = "file://".$merchantKeyFilePaths['platform'];
        $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
        $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
        $payload = [
            'mchid'=>$config['partner'] ? $config['sp_appid'] : $config['mchid'],
            'serial'=>$config['serialno'],
            'privateKey'=>$merchantPrivateKeyInstance,
            'certs'=>[
                $platformCertificateSerial => $platformPublicKeyInstance,
            ]
        ];
        $instance = Builder::factory($payload);
        return [$instance, $merchantPrivateKeyInstance];
    }

    /**
     * 使用公钥证书方式构建微信支付实例
     * @param array $config 支付配置
     * @return array|error 返回支付实例和私钥实例或错误信息
     */
    public function WechatBuilderWithPublicCert($config){
        try {
            // 获取商户私钥、平台公钥证书
            $merchantKeyFilePaths = $this->WechatCert(['key', 'cert', 'public_cert'], true);
            if (empty($merchantKeyFilePaths['key'])) {
                return $this->error("未上传商户私钥文件");
            }
            if (empty($merchantKeyFilePaths['public_cert'])) {
                return $this->error("未上传微信支付平台公钥证书");
            }

            $merchantPrivateKeyInstance = Rsa::from("file://".$merchantKeyFilePaths['key'], Rsa::KEY_TYPE_PRIVATE);


            $platformPublicKeyFileFilePath = 'file://' . $merchantKeyFilePaths['public_cert'];
            $platformPublicKeyInstance = Rsa::from($platformPublicKeyFileFilePath, Rsa::KEY_TYPE_PUBLIC);
            $platformCertificateSerialOrPublicKeyId = $config['public_serial_id'];

            $payload = [
                'mchid'=>$config['partner'] ? $config['sp_appid'] : $config['mchid'],
                'serial'=>$config['serialno'],
                'privateKey'=>$merchantPrivateKeyInstance,
                'certs'=>[
                    $platformCertificateSerialOrPublicKeyId => $platformPublicKeyInstance,
                ]
            ];
            $instance = Builder::factory($payload);
            return [$instance, $merchantPrivateKeyInstance];
        } catch (\Exception $exception) {
            return $this->error("公钥证书方式初始化失败：" . $exception->getMessage());
        }
    }

    /**
     * 微信H5支付
     * @param array $params 支付参数（金额total_amount、外部订单号out_trade_no、商品描述subject）
     * @param array $config 支付配置
     * @return array 返回H5跳转链接或错误信息（error）
    */
    public function WechatH5($params, $config=[]){
        if (empty($config)){
            $config = $this->configs['wechat'];
        }
        $this->Composer();
        global $_W;
        try {
            $total = intval($params['total_amount']*100);
            $postData = array(
                'mchid'=>$config['mchid'],
                'out_trade_no'=>$params['out_trade_no'],
                'appid'=>$config['appid'],
                'description'=>$params['subject'],
                'notify_url'=>$_W['siteroot']."api/server/run/payment/notify".$this->uniacid,
                'amount'=>array(
                    'total'=>$total
                ),
                'scene_info'=>array(
                    'payer_client_ip'=>$_W['clientip'],
                    'h5_info'=>array(
                        'type'=>'Wap',
                        'app_name'=>$_W['account']['name']
                    )
                )
            );
            if (!empty($params['time_expire'])){
                $postData['time_expire'] = $params['time_expire'];
            }
            $payApi = "v3/pay/transactions/h5";
            if ($config['partner']){
                $postData['sp_appid'] = $config['sp_appid'];
                $postData['sp_mchid'] = $config['sp_mchid'];
                $postData['sub_mchid'] = $config['mchid'];
                $payApi = "v3/pay/partner/transactions/h5";
                unset($postData['mchid']);
            }
            $Builder = $this->WechatBuilder($config);
            if (is_error($Builder)) return $Builder;
            $instance = $Builder[0];
            $resp = $instance->chain($payApi)->post(['json'=>$postData]);
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return json_decode($resp->getBody(), true);
        }catch (\Exception $exception){
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return $this->WechatResponse($exception->getMessage());
        }
    }

    /**
     * 微信JS-API支付
     * @param array $params 支付参数（金额total_amount、外部订单号out_trade_no、商品描述subject）
     * @param array $config 支付配置
     * @return array 返回JSAPI支付所需要的支付参数
     * @doc https://pay.weixin.qq.com/wiki/doc/apiv3_partner/apis/chapter4_1_4.shtml
    */
    public function WechatCreate(array $params, $config=[]){
        if (empty($config)){
            $config = $this->configs['wechat'];
        }
        $this->Composer();
        $total = intval($params['total_amount']*100);
        $postData = [
            'description'=>$params['subject'],
            'out_trade_no'=>$params['out_trade_no'],
            'notify_url'=>$GLOBALS['_W']['siteroot']."api/server/run/payment/notify".$this->uniacid,
            'amount'=>[
                'total'=>$total
            ]
        ];
        $payApi = "v3/pay/transactions/jsapi";
        if($config['partner']==1){
            $payApi = "v3/pay/partner/transactions/jsapi";
            $postData['sp_appid'] = $config['sp_appid'];
            $postData['sp_mchid'] = $config['sp_mchid'];
            $postData['sub_mchid'] = $config['mchid'];
            $postData['sub_appid'] = $config['appid'];
            $postData['payer'] = ["sub_openid"=>$params['openid']];
        }else{
            $postData['appid'] = $config['appid'];
            $postData['mchid'] = $config['mchid'];
            $postData['payer'] = ['openid'=>$params['openid']];
        }
        if (!empty($params['time_expire'])){
            $postData['time_expire'] = $params['time_expire'];
        }
        try {
            $Builder = $this->WechatBuilder($config);
            if (is_error($Builder)) return $Builder;
            list($instance, $merchantPrivateKeyInstance) = $Builder;
            $resp = $instance->chain($payApi)->post(['json'=>$postData]);
            $package = json_decode($resp->getBody(), true);
            $params = [
                'appId'     => $config['appid'],
                'timeStamp' => (string)Formatter::timestamp(),
                'nonceStr'  => Formatter::nonce(),
                'package'   => 'prepay_id='.$package['prepay_id'],
            ];
            $params += ['paySign' => Rsa::sign(
                Formatter::joinedByLineFeed(...array_values($params)),
                $merchantPrivateKeyInstance
            ), 'signType' => 'RSA'];
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return $params;
        }catch (\Exception $exception){
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return $this->WechatResponse($exception->getMessage());
        }
    }

    /**
     * 微信退款
     * @param array $params 订单信息（外部订单号out_trade_no，外部退款单号out_refund_no，退款金额refund_amount）
     * @param array $config 支付配置
     * @return array 返回接口退款请求结果（退款结果会异步通知）
     * @doc https://pay.weixin.qq.com/wiki/doc/apiv3/apis/chapter3_1_9.shtml
    */
    public function WechatRefund($params, $config=[]){
        if (empty($config)){
            $config = $this->configs['wechat'];
        }
        $this->Composer();
        try {
            // 检查是否使用公钥证书方式
            if (!empty($config['use_public_cert']) && $config['use_public_cert'] == 1) {
                $Builder = $this->WechatBuilderWithPublicCert($config);
            } else {
                $merchantKeyFilePaths = $this->WechatCert(['key', 'platform'], true);
                $merchantPrivateKeyInstance = Rsa::from("file://".$merchantKeyFilePaths['key'], Rsa::KEY_TYPE_PRIVATE);
                $platformCertificateFilePath = "file://".$merchantKeyFilePaths['platform'];
                $platformPublicKeyInstance = Rsa::from($platformCertificateFilePath, Rsa::KEY_TYPE_PUBLIC);
                $platformCertificateSerial = PemUtil::parseCertificateSerialNo($platformCertificateFilePath);
                $payload = [
                    'mchid'=>$config['partner'] ? $config['sp_appid'] : $config['mchid'],
                    'serial'=>$config['serialno'],
                    'privateKey'=>$merchantPrivateKeyInstance,
                    'certs'=>[
                        $platformCertificateSerial => $platformPublicKeyInstance,
                    ]
                ];
                $Builder = [Builder::factory($payload), $merchantPrivateKeyInstance];
            }

            if (is_error($Builder)) return $Builder;
            list($instance, $merchantPrivateKeyInstance) = $Builder;

            $total = $refund = intval($params['total_amount']*100);
            if (!empty($params['refund_amount'])){
                $refund = intval($params['refund_amount']*100);
            }
            $postData = [
                'out_trade_no'=>$params['out_trade_no'],
                'out_refund_no'=>$params['out_refund_no'],
                'notify_url'=>$GLOBALS['_W']['siteroot']."api/server/run/payment/refund".$this->uniacid,
                'amount'=>[
                    'total'=>$total,
                    'refund'=>$refund,
                    'currency'=>'CNY'
                ]
            ];
            if ($config['partner']){
                $postData['sub_mchid'] = $config['mchid'];
            }
            if (!empty($params['reason'])){
                $postData['reason'] = $params['reason'];
            }
            $resp = $instance->chain("v3/refund/domestic/refunds")->post(['json'=>$postData]);
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return json_decode($resp->getBody(), true);
        }catch (\Exception $exception){
            $this->WechatClearCert(['key', 'platform', 'public_cert']);
            return $this->WechatResponse($exception->getMessage());
        }
    }

    public function WechatResponse($message){
        if (!strexists($message, "response:")) return error(-1, $message);
        $JSON = preg_replace('/^.*(\{.*\}).*$/s', '$1', $message);
        $resp = json_decode($JSON, true);
        if (empty($JSON) || empty($resp)){
            return error(-1, $message);
        }
        return error(-1, $resp['message']."({$resp['code']})");
    }

    public function CreditRefund($result)
    {
        $detail = $this->getone($result['out_trade_no'], 'uniontid');
        //退还用户余额
        $uid = $this->openid2uid($detail['openid']);
        if (empty($uid)){
            return true;
        }
        $res = serv('ucenter')->UpdateCredit($uid, 'credit2', $detail['fee'], ['remark'=>"[全额退款:{$detail['tid']}]".$detail['tag']]);
        return is_error($res) ? $res : true;
    }

    public function openid2uid($openid)
    {
        if (is_numeric($openid)) return intval($openid);
        $wechat = serv('wechat');
        if ($wechat->enabled || Schema::hasTable('mc_mapping_fans')){
            $uid = (int)DB::table('mc_mapping_fans')->where(['openid'=>$openid, 'uniacid'=>$this->uniacid])->value('uid');
        }
        return $uid ?? 0;
    }

    public function Refund($out_trade_no, $column='uniontid'){
        $detail = $this->getone($out_trade_no, $column);
        if ($detail['status']!=1) return $this->error("该记录未支付或已失效");
        if (!isset($this->payments[$detail['type']])) return $this->error("无效的支付方式");
        if ($detail['type']=='alipay'){
            $result = $this->AlipayRefund(array(
                'out_trade_no'=>$detail['uniontid'],
                'fee'=>$detail['fee']
            ));
            if (is_error($result)) return $result;
            $result['status'] = "SUCCESS";
        }else{
            $method = ucfirst($detail['type'])."Refund";
            try {
                $result = $this->$method([
                    'out_trade_no'=>$detail['uniontid'],
                    'out_refund_no'=>$detail['tid'],
                    'total_amount'=>$detail['fee']
                ]);
                if (is_error($result)) return $result;
            }catch (\Exception $exception){
                return error(-1, $exception->getMessage());
            }
        }
        $this->refundResult(['out_trade_no'=>$detail['uniontid'], 'tid'=>$detail['tid'], 'total_amount'=>$detail['fee'], 'refund_amount'=>$detail['fee']], $detail);
        return $result;
    }

    public function refundResult($params, $payLog=array()){
        pdo_update($this->tablePayLog, array('status'=>3), array('uniontid'=>$params['out_trade_no']));
        if (empty($payLog)) $payLog = $this->getone($params['out_trade_no'], 'uniontid');
        if (!empty($payLog['module']) && $payLog['module']!='core'){
            global $_W;
            if ($_W['uniacid']!=$payLog['uniacid']){
                $_W['uniacid'] = $payLog['uniacid'];
                $_W['account'] = AccountService::FetchUni($_W['uniacid']);
            }
            $params['weid'] = $payLog['weid'];
            $params['uniacid'] = $payLog['uniacid'];
            $params['result'] = 'success';
            $params['from'] = 'notify';
            if (strpos($payLog['module'], 'server:')!==false){
                $server = str_replace('server:', '', $payLog['module']);
                return serv($server)->refundResult($params);
            }
            $WeModule = new WeModule();
            try {
                $site = $WeModule->create($payLog['module']);
                if (is_error($site)){
                    return $site;
                }
                return $site->refundResult($params);
            }catch (\Exception $exception){
                return error(-1,$exception->getMessage());
            }
        }
        return true;
    }

    public function payQuery($out_trade_no, $payment="", $config=[]){
        $paylog = $this->getone($out_trade_no, 'uniontid');
        if (empty($paylog)) return error(-1, "找不到该订单");
        if (empty($payment)){
            $payment = $paylog['type'];
        }
        $payResult = array(
            'trade_state'=>'NOTPAY',
            'in_trade_no'=>$paylog['tid'],
            'out_trade_no'=>$out_trade_no,
            'total_amount'=>floatval($paylog['fee']),
            'trade_msg'=>'订单未支付',
            'payment'=>strtoupper($payment),
            'openid'=>$paylog['openid'],
            'payer'=>''
        );
        if (empty($payment)){
            $payResult['payment'] = 'NONE';
            return $payResult;
        }
        if ($payment=='credit'){
            if ($paylog['status']==1){
                $payResult['trade_state'] = 'SUCCESS';
                $payResult['trade_msg'] = '支付成功！';
            }elseif ($paylog['status']>0){
                $payResult['trade_state'] = 'FAIL';
                $payResult['trade_msg'] = "订单".$this->states[$paylog['status']];
            }
            return $payResult;
        }
        if (empty($config)) $config = $this->configs[$payment];
        if ($payment=='alipay'){
            $res = $this->AlipayQuery(['out_trade_no'=>$out_trade_no]);
            if (!is_error($res)){
                $res += [
                    "trade_state"=>$res['tradeStatus']=='TRADE_SUCCESS'?'SUCCESS':$res['tradeStatus'],
                    "trade_state_desc"=>$res['msg']
                ];
                $res['out_trade_no'] = $res['outTradeNo'];
                $res['payer'] = ['openid'=>$res['buyerUserId']];
            }
        }else{
            $method = ucfirst($payment)."Query";
            $res = $this->$method($out_trade_no, $config);
        }
        if (is_error($res)){
            $payResult['trade_state'] = "ERROR";
            $payResult['trade_msg'] = $res['message'];
            return $payResult;
        }
        $payResult['trade_state'] = $res['trade_state'];
        $payResult['trade_msg'] = $res['trade_state_desc'];
        $payResult['out_trade_no'] = $res['out_trade_no'];
        $payResult['payer'] = $res['payer']['openid'];
        return $payResult;
    }

    public function Notify($payment, $params, $from='notify'){
        $unionTid = trim($params['out_trade_no']);
        if (empty($unionTid)){
            return error(-1,'交易单号异常');
        }
        $payLog = pdo_get($this->tablePayLog, array('uniontid'=>$unionTid));
        if (empty($payLog)){
            return error(-1,'交易不存在');
        }
        $payRes = false;
        if ($payment=='alipay'){
            $payRes = $this->AlipayVerify($params);
        }
        if (is_error($payRes)){
            return $payRes;
        }
        if($from=='notify'){
            $data = array('type'=>$payment, 'status'=>1);
            pdo_update($this->tablePayLog, $data, array('plid'=>$payLog['plid']));
            $payLog = array_merge($payLog, $data);
        }
        if (!empty($from)){
            if(empty($payLog['module'])) return $payRes;
            return $this->payResult($payLog, $from);
        }
        return $this->success("支付成功！");
    }

    public function payResult($tidOrPayLog, $from='notify'){
        $payLog = is_array($tidOrPayLog) ? $tidOrPayLog : $this->getone($tidOrPayLog, 'tid');
        global $_W;
        $_W['uniacid'] = $this->uniacid;
        if ($_W['uniacid']!=$payLog['uniacid']){
            $_W['uniacid'] = $payLog['uniacid'];
            $_W['account'] = AccountService::FetchUni($_W['uniacid']);
        }
        $ret = array();
        $ret['weid'] = $payLog['weid'];
        $ret['uniacid'] = $payLog['uniacid'];
        $ret['result'] = 'success';
        $ret['type'] = $payLog['type'];
        $ret['from'] = $from;
        $ret['tid'] = $payLog['tid'];
        $ret['uniontid'] = $payLog['uniontid'];
        $ret['transaction_id'] = $payLog['transaction_id'];
        $ret['user'] = $payLog['openid'];
        $ret['fee'] = $payLog['fee'];
        $ret['is_usecard'] = $payLog['is_usecard'];
        $ret['card_type'] = $payLog['card_type'];
        $ret['card_fee'] = $payLog['card_fee'];
        $ret['card_id'] = $payLog['card_id'];
        if ($from=='notify'){
            define('IN_API', true);
            $_W['isapi'] = true;
        }
        $payLog['module'] = str_replace('server:payment', 'core', $payLog['module']);
        if($payLog['module']=='core'){
            //系统订单
            return true;
        }elseif (strpos($payLog['module'], 'server:')!==false){
            //微服务订单
            $server = str_replace('server:', '', $payLog['module']);
            return serv($server)->payResult($ret);
        }
        //内置应用订单
        $WeModule = new WeModule();
        try {
            $site = $WeModule->create($payLog['module']);
            if (is_error($site)){
                return $site;
            }
            return $site->payResult($ret);
        }catch (\Exception $exception){
            return error(-1,$exception->getMessage());
        }
    }

    public function WechatGenCert(){
        // 检查是否使用公钥证书方式
        if (!empty($this->configs['wechat']['use_public_cert']) && $this->configs['wechat']['use_public_cert'] == 1) {
            return $this->error("当前使用公钥证书方式，请手动上传微信支付平台公钥证书文件，无需自动获取");
        }

        $certs = $this->WechatCert(["key"], true);
        if (empty($certs)) return error(-1, "未上传apiclient_key.pem");
        $apiV3key = $this->configs['wechat']['apikey'];
        if (empty($apiV3key)) return error(-1, "未配置微信支付密钥");
        $mchId = $this->configs['wechat']['mchid'];
        if (empty($mchId)) return error(-1, "未配置微信支付商户号");
        $mchSerialNo = $this->configs['wechat']['serialno'];
        if (empty($mchSerialNo)) return error(-1, "未配置微信支付证书序列号");
        $outputFilePath = SERVICE_PAYMENT . "cert/";

        $vendorPath = DEVELOPMENT ? SERVICE_PAYMENT."vendor/wechatpay/wechatpay/bin/CertificateDownloader.php" : base_path("vendor/wechatpay/wechatpay/bin/CertificateDownloader.php");
        $commands = explode(" ", "php $vendorPath -k $apiV3key -m $mchId -f {$certs['key']} -s $mchSerialNo -o $outputFilePath");
        try {
            $this->Composer();
            $process = new Process($commands);
            $process->run();
            if ($process->isSuccessful()) {
                $Output = $process->getOutput();
                if (strexists($Output, "-----BEGIN CERTIFICATE-----")){
                    preg_match("/(.*?)wechatpay\_([A-Z,\d].+)\.pem(.*?)/i", $Output, $matchs);
                    $filepath = SERVICE_PAYMENT . "cert/wechatpay_{$matchs[2]}.pem";
                    $CERTIFICATE = file_get_contents($filepath);
                    $PlatForm = $this->WechatEecrypt($CERTIFICATE."\n", 'platform');
                    if (is_error($PlatForm)) return $PlatForm;
                    @unlink($filepath);
                    return $PlatForm;
                }else{
                    file_get_contents(MICRO_SERVER."payment/logs/GenCertFail{$mchId}".TIMESTAMP.".txt", $Output);
                    $res = error(-1, "证书解析失败");
                }
            }else{
                $res = error(-1, $process->getOutput());
            }
        }catch (\Exception $exception){
            //Todo something
            $res = error(-1, $process->getOutput());
        }
        $this->WechatClearCert();
        return $res;
    }

    public function WechatEecrypt($certInfo, $name='cert'){
        $begin = $name=='key' ? '-----BEGIN PRIVATE KEY-----' : '-----BEGIN CERTIFICATE-----';
        $end = $name=='key' ? '---END PRIVATE KEY-----' : '---END CERTIFICATE-----';
        if($name=='public_cert'){
            $begin = '-----BEGIN PUBLIC KEY-----';
            $end = '-----END PUBLIC KEY-----';
        }
        if (strexists($certInfo, '<?php') || !strexists($certInfo, $begin) || !strexists($certInfo, $end)){
            return $this->error("证书内容不合法，请重新上传");
        }
        $encode = $this->authcode($certInfo, 'ENCODE');
        $filename = md5($this->uniacid.$name.$GLOBALS['_W']['config']['setting']['authkey']);
        if (!file_put_contents(SERVICE_PAYMENT."cert/$filename.crt", $encode)){
            return $this->error("保存失败，请重试");
        }
        return str_replace("\\", "/", SERVICE_PAYMENT."cert/$filename.crt");
    }

    public function WechatClearCert($names="key"){
        if (empty($names)) return false;
        if (!is_array($names)) $names = [$names];
        $uniacid = $this->uniacid;
        foreach ($names as $value){
            $path = SERVICE_PAYMENT."cert/".md5($value.$uniacid).".pem";
            if (file_exists($path)){
                @unlink($path);
            }
        }
        return true;
    }

    public function WechatCert($names="key", $make=false){
        if (!is_array($names)) $names = [$names];
        $certs = [];
        foreach ($names as $value){
            $filename = md5($this->uniacid.$value.$GLOBALS['_W']['config']['setting']['authkey']);
            if (!file_exists(SERVICE_PAYMENT."cert/$filename.crt")) continue;
            $code = file_get_contents(SERVICE_PAYMENT."cert/$filename.crt");
            $certs[$value] = $this->authcode($code);
        }
        if ($make && !empty($certs)){
            $paths = [];
            $uniacid = $this->uniacid;
            foreach ($certs as $key=>$value){
                $path = SERVICE_PAYMENT."cert/".md5($key.$uniacid).".pem";
                $path = str_replace("\\", "/", $path);
                if (file_put_contents($path, $value)){
                    $paths[$key] = $path;
                }
            }
            return $paths;
        }
        return $certs;
    }

    public function authcode($string, $operation = 'DECODE', $key = '', $expiry = 0) {
        $ckey_length = 4;
        $key = md5('' != $key ? $key : $GLOBALS['_W']['config']['setting']['authkey']);
        $keya = md5(substr($key, 0, 16));
        $keyb = md5(substr($key, 16, 16));
        $keyc = $ckey_length ? ('DECODE' == $operation ? substr($string, 0, $ckey_length) : substr(md5(microtime()), -$ckey_length)) : '';

        $cryptkey = $keya . md5($keya . $keyc);
        $key_length = strlen($cryptkey);

        $string = 'DECODE' == $operation ? base64_decode(substr($string, $ckey_length)) : sprintf('%010d', $expiry ? $expiry + time() : 0) . substr(md5($string . $keyb), 0, 16) . $string;
        $string_length = strlen($string);

        $result = '';
        $box = range(0, 255);

        $rndkey = array();
        for ($i = 0; $i <= 255; ++$i) {
            $rndkey[$i] = ord($cryptkey[$i % $key_length]);
        }

        for ($j = $i = 0; $i < 256; ++$i) {
            $j = ($j + $box[$i] + $rndkey[$i]) % 256;
            $tmp = $box[$i];
            $box[$i] = $box[$j];
            $box[$j] = $tmp;
        }

        for ($a = $j = $i = 0; $i < $string_length; ++$i) {
            $a = ($a + 1) % 256;
            $j = ($j + $box[$a]) % 256;
            $tmp = $box[$a];
            $box[$a] = $box[$j];
            $box[$j] = $tmp;
            $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
        }

        if ('DECODE' == $operation) {
            if ((0 == substr($result, 0, 10) || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26) . $keyb), 0, 16)) {
                return substr($result, 26);
            } else {
                return '';
            }
        } else {
            return $keyc . str_replace('=', '', base64_encode($result));
        }
    }

}
