微信电子发票--“自建平台模式”--小程序开票

注:小程序开发票在官方文档(https://mp.weixin.qq.com/wiki?t=resource/res_main&id=21518166863ccFdP)的第四节


项目说明 

1.我有一个小程序,是交通行业的。用户买完票之后,可以在电子票页面申请开电子发票;

2. 小程序开发票用的是公众号的access_token,发送到用户卡包也是通过公众号发的,小程序要做的是申请电子发票和完成授权。
3. 虽然买完票就能申请发票,但是为了减少后续的冲红操作,在用户乘车之后才会去申请第三方平台开发票;
4. 代码用的是PHP。


项目大概逻辑

1. 商户获取获取access_token。(方法:getAccessToken()
2. 提前获取开票平台标识s_pappid(方法:getInvoiceUrl()
3. 设置商户联系方式(方法:setContact()
4. 商户获取授权页(方法:getTicket());
5.  商户获取授权页url(方法:getAuthUrl());
6. 根据第5步的结果,在小程序开票按钮的点击事件上部署跳转到小程序授权页的逻辑;
7. 获取用户填写的抬头信息有两种办法:1)主动查询授权状态(方法:getAuthData())、2)根据授权回调获取授权状态(方法:getAuthDataWxAccount());
8. 创建发票卡券模板(方法:getInvoiceCardId());
9. 在自建发票平台开具电子发票(意思是,自己去开发票,比如通过第三方开票平台,然后把开票返回的发票号码、发票代码、发票pdf文件等保存下来;最后再用这些信息调用微信的PDF上传接口和下发到用户卡包的接口);
10. 上传发票PDF文件(方法:setPDF());
11. 将电子发票添加到用户微信卡包(方法:sendInvoice())。

注:具体的使用方法请看“调用示例”


代码与说明

说明:域名空间下面引用的类,基本上都是数据库操作,大家可以不用看,主要是所取到的要用的字段含义。

AppIdModel用的表是zc2_ticket_appid,请求微信接口用到的字段基本都在这张表了:

CREATE TABLE `zc2_ticket_appid` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
  `name` varchar(50) NOT NULL COMMENT '调用方名称',
  `desc` varchar(100) NOT NULL COMMENT '调用方描述',
  `appid` varchar(50) NOT NULL COMMENT '调用方APPID',
  `appsecret` varchar(50) DEFAULT NULL COMMENT '调用方秘钥',
  `wx_appid` varchar(50) NOT NULL DEFAULT '' COMMENT '微信APPID',
  `wx_appsecret` varchar(125) NOT NULL DEFAULT '' COMMENT '微信APPSECRET',
  `wx_mch_id` varchar(50) NOT NULL DEFAULT '' COMMENT '商户ID,多商户小程序在zc2_sale_address表配置',
  `pay_acid` int(11) NOT NULL DEFAULT '0' COMMENT '支付商户ID,多商户小程序在zc2_sale_address表配置',
  `access_token` varchar(1000) NOT NULL DEFAULT '' COMMENT '小程序access_token',
  `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '1:有效 2:无效',
  `createtime` int(10) DEFAULT '0',
  `createuser` int(10) DEFAULT '0',
  `updatetime` int(10) DEFAULT '0',
  `updateuser` int(10) DEFAULT '0',
  `bonus_insurance` tinyint(1) NOT NULL DEFAULT '2' COMMENT '赠送保险开关:1 开,2 关',
  `type` tinyint(1) NOT NULL DEFAULT '1' COMMENT '渠道类别:1前端;2管理后台;3设备接口',
  `auth_ticket` varchar(1000) NOT NULL DEFAULT '' COMMENT '授权页ticket凭证',
  `invoice_url` varchar(512) NOT NULL DEFAULT '' COMMENT '开票平台链接',
  `invoice_card_id` varchar(512) NOT NULL DEFAULT '' COMMENT '发票卡券模板',
  `phone` varchar(20) NOT NULL DEFAULT '' COMMENT '商户联系手机',
  `wx_account_appid` varchar(50) NOT NULL DEFAULT '' COMMENT '小程序关联的微信公众号APPID',
  `wx_account_appsecret` varchar(125) NOT NULL DEFAULT '' COMMENT '小程序关联的微信公众号APPSECRET',
  `wx_account_access_token` varchar(1000) NOT NULL DEFAULT '' COMMENT '小程序关联的微信公众号access_token',
  PRIMARY KEY (`id`),
  UNIQUE KEY `appid` (`appid`)
) ENGINE=MyISAM AUTO_INCREMENT=15 DEFAULT CHARSET=utf8 COMMENT='接口调用方appid表';

fetchByWxAppId($appId);
        if($row)
        {
            //存在数据库的公众号access_token
            $token = empty($row['wx_account_access_token']) ? null : json_decode($row['wx_account_access_token'], true);
            if($token && $token['expire'] > time())
            {
                return $token['access_token'];
            }else
            {
                $wxAccountAppid = $row['wx_account_appid'];//公众号appid
                $wxAccountAppSecret = $row['wx_account_appsecret'];//公众号appsecret
                $url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$wxAccountAppid}&secret={$wxAccountAppSecret}";
                $curl = new CUrl(30);
                $ret = $curl->get($url);
                $ret = json_decode($ret, true);
                if(isset($ret['errcode']))
                {
                    \PhalApi\DI()->logger->error('Function getAccessToken.$url : ' . $url);
                    throw new RemoteServiceException("获取AccessToken失败:".$ret['errmsg'],98);
                }else
                {
                    //保存到数据库
                    $expire = time() + $ret['expires_in'] - 200; //过期时间
                    $accessToken = $ret['access_token'];
                    $updateData = array('wx_account_access_token' => json_encode(array('access_token' => $accessToken, 'expire' => $expire)));
                    $params = array('wx_appid' => $appId);
                    $appIdModel->updateData($updateData, $params);
                    return $accessToken;
                }
            }
        }else
        {
            throw new LocalServiceException("账户信息不存在或配置有误",99);
        }
    }


    /**
     * @function 验证access_token是否有效
     * @param $access_token
     * @return bool
     */
    public function checkAccessToken($access_token)
    {
        \PhalApi\DI()->logger->info('Function checkAccessToken.$access_token : ' . $access_token);
        if(!empty($access_token))
        {
            $url = "https://api.weixin.qq.com/cgi-bin/getcallbackip?access_token=".$access_token;
            $curl = new CUrl(30);
            $ret = $curl->get($url);
            \PhalApi\DI()->logger->info('Function checkAccessToken.$ret : ' . $ret);

            $ret = json_decode($ret, true);
            if(!isset($ret['errcode']))
            {
                return true;
            }
        }

        return false;
    }


    /**
     * @function 获取自身的开票平台识别码
     * @param $appId
     * @return bool|mixed
     * @throws LocalServiceException
     * @throws RemoteServiceException
     * @desc 开票平台可以通过此接口获得本开票平台的预开票url,进而获取s_pappid。
     *       开票平台将该s_pappid并透传给商户,商户可以通过该s_pappid参数在微信
     *       电子发票方案中标识出为自身提供开票服务的开票平台。
     */
    public function getInvoiceUrl($appId)
    {
        $appIdModel = new AppIDModel();
        $row = $appIdModel->fetchByWxAppId($appId);
        if($row)
        {
            $invoiceUrl = empty($row['invoice_url']) ? null : $row['invoice_url'];
            if($invoiceUrl)
            {
                return $invoiceUrl;
            }else
            {
                $token = $this->getAccessToken($appId);
                $url = "https://api.weixin.qq.com/card/invoice/seturl?access_token={$token}";
                $curl = new CUrl(30);
                $curlRes = $curl->post($url,json_encode(array()));
                $ret = json_decode($curlRes, true);

                \PhalApi\DI()->logger->error('$appId : ' . $appId);
                \PhalApi\DI()->logger->error('$url : ' . $url);
                \PhalApi\DI()->logger->error('$res : ' . json_encode($curlRes));

                if($ret['errcode']==0 and $ret['errmsg']=='ok')
                {
                    $invoiceUrl = $ret['invoice_url'];
                    $updateData = array('invoice_url' => $invoiceUrl);
                    $params = array('wx_appid' => $appId);
                    $appIdModel->updateData($updateData, $params);
                    return $invoiceUrl;
                }else
                {
                    throw new RemoteServiceException("获取自身的开票平台识别码失败:".$ret['errcode'].$ret['errmsg'],98);
                }
            }
        }else
        {
            throw new LocalServiceException("账户信息不存在或配置有误",99);
        }
    }


    /**
     * @function 设置商户联系方式
     * @param $appId
     * @param $time_out
     * @return bool|mixed
     * @throws LocalServiceException
     * @throws RemoteServiceException
     * @desc 商户获取授权链接之前,需要先设置商户的联系方式
     */
    public function setContact($appId,$time_out)
    {
        $appIdModel = new AppIDModel();
        $row = $appIdModel->fetchByWxAppId($appId);
        if($row and $row['phone'])
        {
            //1.获取AccessToken
            $token = $this->getAccessToken($appId);

            //2.
            $phone = $row['phone'];
            $url = "https://api.weixin.qq.com/card/invoice/setbizattr?action=set_contact&access_token={$token}";
            $params = json_encode(array('contact'=>array('phone'=>$phone,'time_out'=>$time_out)));
            $curl = new CUrl(30);
            $curlRes = $curl->post($url,$params);
            $ret = json_decode($curlRes, true);

            \PhalApi\DI()->logger->error('$appId : ' . $appId);
            \PhalApi\DI()->logger->error('$url : ' . $url);
            \PhalApi\DI()->logger->error('$res : ' . json_encode($curlRes));

            if(!empty($ret['errcode'])) {
                throw new RemoteServiceException("设置商户联系方式失败:".$ret['errcode'].$ret['errmsg'],98);

            }else {
                return $ret;
            }

        }else
        {
            throw new LocalServiceException("账户信息不存在或配置有误",99);
        }
    }


    /**
     * @function 获取授权页ticket
     * @param $appId
     * @return bool|mixed
     * @throws LocalServiceException
     * @throws RemoteServiceException
     * @desc 商户在调用授权页前需要先获取一个7200s过期的授权页ticket,在获取授权页接口中,
     *       该ticket作为参数传入,加强安全性。
     */
    public function getTicket($appId)
    {
        $appIdModel = new AppIDModel();
        $row = $appIdModel->fetchByWxAppId($appId);
        if($row)
        {
            $authTicket = empty($row['auth_ticket']) ? null : json_decode($row['auth_ticket'], true);
            if($authTicket && $authTicket['expire'] > time())
            {
                return $authTicket['ticket'];
            }else
            {
                $token = $this->getAccessToken($appId);
                $url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token={$token}&type=wx_card";
                $curl = new CUrl(30);
                $ret = $curl->get($url);
                $ret = json_decode($ret, true);

                \PhalApi\DI()->logger->error('$appId : ' . $appId);
                \PhalApi\DI()->logger->error('$url : ' . $url);
                \PhalApi\DI()->logger->error('$ret : ' . json_encode($ret));

                if(!empty($ret['errcode']))
                {
                    throw new RemoteServiceException("获取网页授权ticket失败:".$ret['errcode'].$ret['errmsg'],98);
                }else
                {
                    $expire = time() + $ret['expires_in'] - 200; //过期时间
                    $ticket = $ret['ticket'];
                    $updateData = array('auth_ticket' => json_encode(array('ticket' => $ticket, 'expire' => $expire)));
                    $params = array('wx_appid' => $appId);
                    $appIdModel->updateData($updateData, $params);
                    return $ticket;
                }
            }
        }else
        {
            throw new LocalServiceException("账户信息不存在或配置有误",99);
        }
    }


    /**
     * @function 获取授权页链接
     * @param $appId
     * @param $s_pappid string 开票平台在微信的标识号,商户需要找开票平台提供
     * @param $order_id string 订单id,在商户内单笔开票请求的唯一识别号
     * @param $money string 订单金额,以分为单位
     * @param $timestamp int 时间戳
     * @param $source string 开票来源,app:app开票,web:微信h5开票,wxa:小程序开发票,wap:普通网页开票
     * @param $redirect_url string 授权成功后跳转页面。本字段只有在source为H5的时候需要填写,引导用户在微信中进行下一步流程。app开票因为从外部app拉起微信授权页,授权完成后自动回到原来的app,故无需填写。
     * @param $ticket string 授权页ticket
     * @param $type int 授权类型,0:开票授权,1:填写字段开票授权,2:领票授权
     *
     * @return bool|mixed
     * @throws LocalServiceException
     * @throws RemoteServiceException
     */
    public function getAuthUrl($appId,$s_pappid,$order_id,$money,$timestamp,$source,$redirect_url,$ticket,$type)
    {
        //1.获取AccessToken
        $token = $this->getAccessToken($appId);

        //2.
        $url = "https://api.weixin.qq.com/card/invoice/getauthurl?access_token={$token}";
        $params = array(
            's_pappid'=>$s_pappid,
            'order_id'=>$order_id,
            'money'=>$money,
            'timestamp'=>$timestamp,
            'source'=>$source,
            'redirect_url'=>$redirect_url,
            'ticket'=>$ticket,
            'type'=>$type,
        );
        $curl = new CUrl(30);
        $curlRes = $curl->post($url,json_encode($params));
        $ret = json_decode($curlRes, true);

        \PhalApi\DI()->logger->error('$appId : ' . $appId);
        \PhalApi\DI()->logger->error('$url : ' . $url);
        \PhalApi\DI()->logger->error('$res : ' . json_encode($curlRes));

        if(!empty($ret['errcode'])) {
            throw new RemoteServiceException("获取网页授权url失败:".$ret['errcode'].$ret['errmsg'],98);

        }else {
            return $ret;
        }
    }


    /**
     * @function 创建发票卡券模板
     * @param $appId
     * @param $params
    {
    "invoice_info": {
    "base_info": {
    "logo_url": "http://mmbiz.qpic.cn/mmbiz/1.jpg", //发票商家 LOGO
    "title": "xx公司",                              //收款方(显示在列表),上限为 9 个汉字,建议填入商户简称
    "custom_url_name": "xyz",                       //开票平台自定义入口名称,与 custom_url 字段共同使用,长度限制在 5 个汉字内
    "custom_url": "xyz",                            //开票平台自定义入口跳转外链的地址链接 , 发票外跳的链接会带有发票参数,用于标识是从哪张发票跳出的链接
    "custom_url_sub_title": "xyz",                  //显示在入口右侧的 tips ,长度限制在 6 个汉字内
    "promotion_url_name": "puname",                 //发营销场景的自定义入口
    "promotion_url": "purl",                        //入口跳转外链的地址链接,发票外跳的链接会带有发票参数,用于标识是从那张发票跳出的链接
    "promotion_url_sub_title": "ptitle",            //显示在入口右侧的 tips ,长度限制在 6 个汉字内
    },
    "type": " 广东省增值税普通发票 ",                    //发票类型
    "payee": " 测试 - 收款方 ",                         //收款方(开票方)全称,显示在发票详情内。故建议一个收款方对应一个发票卡券模板
    }
    }
     * @return bool|mixed
     * @throws LocalServiceException
     * @throws RemoteServiceException
     */
    public function getInvoiceCardId($appId,$params)
    {
        $appIdModel = new AppIDModel();
        $row = $appIdModel->fetchByWxAppId($appId);
        if($row)
        {
            $invoiceCardId = empty($row['invoice_card_id']) ? null : $row['invoice_card_id'];
            if($invoiceCardId)
            {
                return $invoiceCardId;
            }else
            {
                $token = $this->getAccessToken($appId);
                $url = "https://api.weixin.qq.com/card/invoice/platform/createcard?access_token={$token}";
                $curl = new CUrl(30);
                $curlRes = $curl->post($url,$params);
                $ret = json_decode($curlRes, true);

                \PhalApi\DI()->logger->error('$appId : ' . $appId);
                \PhalApi\DI()->logger->error('$url : ' . $url);
                \PhalApi\DI()->logger->error('$params : ' . $params);
                \PhalApi\DI()->logger->error('$res : ' . json_encode($curlRes));

                if(!empty($ret['errcode']))
                {
                    throw new RemoteServiceException("创建发票卡券模板失败:".$ret['errcode'].$ret['errmsg'],98);
                }else
                {
                    $invoiceCardId = $ret['card_id'];
                    $updateData = array('invoice_card_id' => $invoiceCardId);
                    $params = array('wx_appid' => $appId);
                    $appIdModel->updateData($updateData, $params);
                    return $invoiceCardId;
                }
            }

        }else
        {
            throw new LocalServiceException("账户信息不存在或配置有误",99);
        }

    }


    /**
     * @param $appId
     * @param $uid
     * @param $order_no
     * @param $ticket_no
     * @return bool|mixed
     * @throws LocalServiceException
     * @throws RemoteServiceException
     *
     * @desc 小程序开具电子发票的步骤如下:
            1 提前获取开票平台标识s_pappid,因为同一个开票平台的s_pappid都相同,所以获取s_pappid的操作只需要进行一次。不同接入模式获取s_pappid的方法略有不同:

            如果商户接入模式为“自建平台模式”:s_pappid通过调用调用获取自身开票平台识别码接口获得

            2 商户获取获取access_token。调用方法见获取access_token;
            3 设置商户联系方式。调用方法见设置商户联系方式。注意,本步骤不能忽略,否则将造成下一步获取授权页报错;
            4 商户获取授权页ticket。调用方法见获取授权页ticket;
            5 商户获取授权页url,上一步获取的授权页ticket将作为参数传入。另外,本环节里面作为参数传入的order_id要注意保留,传递给开票平台作为向用户提供电子发票的依据。调用方法见获取授权页链接;
            6 在小程序开票按钮的点击事件上部署跳转到小程序授权页的逻辑。上一步获得的auth_url和开票小程序appid要作为参数传入。调用方法见小程序打开授权页;
            7 商户在后台等待接收用户的授权完成事件,获取授权事件方法见收取授权完成事件推送;
            8 创建发票卡券模板。发票卡券模板应和背后的开票主体构成一一对应关系,便于后续若开票主体发生变化时,可以便捷修改。调用方法见创建发票卡券模板;
            9 在自建发票平台开具电子发票;
            10 上传发票PDF文件。此步骤获得的s_media_id起到关联PDF和发票卡券的作用,将作为参数在下一步的插卡接口中传入。调用方法见上传PDF;
            11 将电子发票添加到用户微信卡包。调用方法见将电子发票卡券插入用户卡包。
     */
    public function getWxAuthUrl($appId,$uid,$order_no,$ticket_no)
    {

        $orderTicketModel = new ModelOrderTickets();
        $ticketRow = $orderTicketModel->getTicket($order_no,$ticket_no);
        if(empty($ticketRow)){
            throw new LocalServiceException("操作异常。订单号:{$order_no}票号:{$ticket_no}的票不存在。");
        }
        $order_id = $order_no.'_'.$ticket_no;
        $money = $ticketRow['fee'];
        $timestamp = time();
        $source = 'wxa';
        $redirect_url = '';
        $type = '1';

        //1.解析InvoiceUrl中的s_pappid参数值
        $s_pappid = '';
        $invoiceUrl = $this->getInvoiceUrl($appId);
        $invoiceUrlArr = parse_url($invoiceUrl);
        parse_str($invoiceUrlArr['query']);
        $invoiceUrlSpappid = $s_pappid;

        //3
        $this->setContact($appId,30);

        //4
        $authTicket = $this->getTicket($appId);

        return $this->getAuthUrl($appId,$invoiceUrlSpappid,$order_id,$money,$timestamp,$source,$redirect_url,$authTicket,$type);
    }


    /**
     * @function 发送已经申请第三方成功的发票到用户微信卡包
     * @param $appId
     * @return array
     * @throws LocalServiceException
     * @throws RemoteServiceException
     */
    public function sendInvoice($appId)
    {
        $orderTicketsModel = new ModelOrderTickets();

        $appIdModel = new AppIDModel();
        $appIdRow = $appIdModel->fetchByWxAppId($appId);
        if(!$appIdRow) {
            throw new LocalServiceException('系统异常:配置信息错误。');
        }

        $cardParams = array(
            'invoice_info'=>array(
                'type'=>'广东省增值税普通发票',
                'payee'=>' 测试 - 收款方',
                'base_info'=>array(
                    'logo_url'=>'http://mmbiz.qpic.cn/mmbiz/iaL1LJM1mF9aRKPZJkmG8xXhiaHqkKSVMMWeN3hLut7X7hicFNjakmxibMLGWpXrEXB33367o7zHN0CwngnQY7zb7g/0',
                    'title'=>'空港快线',
                )
            )
        );
        $invoiceCardId = $this->getInvoiceCardId($appId,json_encode($cardParams,JSON_UNESCAPED_UNICODE));
        $wxAccountAppId = $appIdRow['wx_account_appid'];

        //获取AppConfig配置信息
        $appConfigModel = new ModelAppConfig();
        $appConfigRes = $appConfigModel->getAppConfig();
        if(empty($appConfigRes)){
            throw new LocalServiceException("系统异常:AppConfig配置信息有误");
        }
        $appParams = json_decode($appConfigRes['app_params'],true);


        $invoiceModel = new ModelInvoice();
        $preSendInvoices = $invoiceModel->getPreSendInvoice();
        $invoiceTotal = count($preSendInvoices);

        $successCount = 0;
        $failCount = 0;
        foreach ($preSendInvoices as $preSendInvoice)
        {
            $invoice_detail = json_decode($preSendInvoice['invoice_detail'],true);

            $info = array(
                array(
                    'name'=>$invoice_detail['XMMC'],
                    'num'=>$invoice_detail['XMSL'],
                    'unit'=>$invoice_detail['DW'],
                    'price'=>$invoice_detail['XMDJ']*100,
                )
            );

            $invoice_user_data = array(
                'fee'=>$preSendInvoice['invoice_price'],
                'title'=>$preSendInvoice['receiver_name'],
                'billing_time'=>$preSendInvoice['invoice_time'],
                'billing_no'=>$preSendInvoice['invoice_code'],
                'billing_code'=>$preSendInvoice['invoice_no'],
                'fee_without_tax'=>$preSendInvoice['no_invoice_price'],
                'tax'=>$preSendInvoice['total_tax'],
                's_pdf_media_id'=>$preSendInvoice['pdf_media_id'],
                'check_code'=>$preSendInvoice['verify_code'],
                'buyer_number'=>$preSendInvoice['tax_number'],
                'buyer_address_and_phone'=>$preSendInvoice['address'].$preSendInvoice['telephone'],
                'buyer_bank_account'=>$preSendInvoice['bank_name'].$preSendInvoice['bank_account'],
                'seller_number'=>$appParams['KP_NSRSBH'],
                'seller_address_and_phone'=>$appParams['XHF_DZ'].$appParams['XHF_DH'],
                'seller_bank_account'=>$appParams['XHF_YHZH'],
                'cashier'=>$appParams['SKR'],
                'maker'=>$appParams['KPR'],
                'info'=>$info,
            );

            $card_ext = array(
                'nonce_str'=> rand(1000000000,9999999999),
                'user_card'=>array('invoice_user_data'=>$invoice_user_data),
            );

            $params = array(
                'order_id'=>$preSendInvoice['order_no'].'_'.$preSendInvoice['ticket_no'],
                'card_id'=>$invoiceCardId,
                'appid'=>$wxAccountAppId,
                'card_ext'=>$card_ext,
            );

            $token = $this->getAccessToken($appId);
            $url = "https://api.weixin.qq.com/card/invoice/insert?access_token={$token}";
            $curl = new CUrl(30);
            $curlRes = $curl->post($url,json_encode($params,JSON_UNESCAPED_UNICODE));
            $ret = json_decode($curlRes, true);

            \PhalApi\DI()->logger->error('$appId : ' . $appId);
            \PhalApi\DI()->logger->error('$url : ' . $url);
            \PhalApi\DI()->logger->error('$params : ' . json_encode($params));
            \PhalApi\DI()->logger->error('$res : ' . json_encode($curlRes));

            if(!empty($ret['errcode']))
            {
                $failCount++;
            }else
            {
                $invUpdateData = array('state'=>7);
                $invUpdateParams = array('order_no'=>$preSendInvoice['order_no'],'ticket_no'=>$preSendInvoice['ticket_no']);
                $invoiceModel->updateInvoice($invUpdateParams,$invUpdateData);

                //改变电子票列表的申请发票状态
                $ticUpdateData = array('invoice_state'=>7);
                $ticUpdateParams =  array('order_no'=>$preSendInvoice['order_no'],'ticket_no'=>$preSendInvoice['ticket_no']);
                $orderTicketsModel->updateTicket($ticUpdateParams,$ticUpdateData);
                $successCount++;
            }

        }

        return array('invoiceTotal'=>$invoiceTotal,'successCount'=>$successCount,'failCount'=>$failCount);
    }


    /**
     * @function 查询授权完成状态
     *
     * @desc
     * 本接口的调用场景包括两个:
     * 一、若商户在某次向用户展示授权页后经过较长时间仍未收到授权完成状态推送,可以使用本接口主动查询用户是否实际上已完成授权,只是由于网络等原因未收到授权完成事件
     * 二、若商户向用户展示的授权页为type=1类型,商户在收到授权完成事件推送后需要进一步获取用户的开票信息,也可以调用本接口。
     *
     * @param $appId
     * @return mixed
     * @throws LocalServiceException
     * @throws RemoteServiceException
     */
    public function getAuthData($appId)
    {
        //1.获取AccessToken
        $token = $this->getAccessToken($appId);

        //2.解析InvoiceUrl中的s_pappid参数值
        $s_pappid = '';
        $invoiceUrl = $this->getInvoiceUrl($appId);
        $invoiceUrlArr = parse_url($invoiceUrl);
        parse_str($invoiceUrlArr['query']);
        $invoiceUrlSpappid = $s_pappid;

        //3.获取已经申请航信的发票,上传PDF
        $invoiceModel = new ModelInvoice();
        $preSendInvoices = $invoiceModel->getAuthInvoice();
        $invoiceTotal = count($preSendInvoices);

        $successCount = 0;
        $failCount = 0;
        foreach ($preSendInvoices as $preSendInvoice)
        {
            $order_id = $preSendInvoice['order_no'].'_'.$preSendInvoice['ticket_no'];
            $url = "https://api.weixin.qq.com/card/invoice/getauthdata?access_token={$token}";
            $params = json_encode(array('s_pappid' => $invoiceUrlSpappid, 'order_id' => $order_id));
            $curl = new CUrl(30);
            $curlRes = $curl->post($url, $params);
            $ret = json_decode($curlRes, true);

            \PhalApi\DI()->logger->info('$url : ' . $url);
            \PhalApi\DI()->logger->info('$params : ' . $params);
            \PhalApi\DI()->logger->info('$ret : ' . json_encode($ret));

            if(!empty($ret['errcode']) and $ret['invoice_status']!= 'auth success'){
                $failCount++;
                continue;
            }

            if(!empty($ret['user_auth_info']['user_field']))//个人发票
            {
                $invoice_type = '2';
                $receiver_name = !empty($ret['user_auth_info']['user_field']['title'])?$ret['user_auth_info']['user_field']['title']:'';
                $telephone = !empty($ret['user_auth_info']['user_field']['phone'])?$ret['user_auth_info']['user_field']['phone']:'';
                $email = !empty($ret['user_auth_info']['user_field']['email'])?$ret['user_auth_info']['user_field']['email']:'';
                $updateData = array(
                    'state'=>'2',
                    'invoice_type'=>$invoice_type,
                    'receiver_name'=>$receiver_name,
                    'telephone'=>$telephone,
                    'email'=>$email,
                );

            }elseif (!empty($ret['user_auth_info']['biz_field']))//企业发票
            {
                $invoice_type = '1';
                $receiver_name = !empty($ret['user_auth_info']['biz_field']['title'])?$ret['user_auth_info']['biz_field']['title']:'';
                $tax_number = !empty($ret['user_auth_info']['biz_field']['tax_no'])?$ret['user_auth_info']['biz_field']['tax_no']:'';
                $address = !empty($ret['user_auth_info']['biz_field']['addr'])?$ret['user_auth_info']['biz_field']['addr']:'';
                $telephone = !empty($ret['user_auth_info']['biz_field']['phone'])?$ret['user_auth_info']['biz_field']['phone']:'';
                $bank_name = !empty($ret['user_auth_info']['biz_field']['bank_type'])?$ret['user_auth_info']['biz_field']['bank_type']:'';
                $bank_account = !empty($ret['user_auth_info']['biz_field']['bank_no'])?$ret['user_auth_info']['biz_field']['bank_no']:'';

                $updateData = array(
                    'state'=>'2',
                    'invoice_type'=>$invoice_type,
                    'receiver_name'=>$receiver_name,
                    'tax_number'=>$tax_number,
                    'address'=>$address,
                    'telephone'=>$telephone,
                    'bank_name'=>$bank_name,
                    'bank_account'=>$bank_account,
                );

            }else{
                $updateData = array();
            }

            $params = array('id' => $preSendInvoice['id']);
            $invoiceModel->updateInvoice($params,$updateData);

            $orderTicketModel = new ModelOrderTickets();
            $ticParams = array('order_no'=>$preSendInvoice['order_no'],'ticket_no'=>$preSendInvoice['ticket_no']);
            $ticUpdateData = array('invoice_state'=>'2');
            $orderTicketModel->updateTicket($ticParams,$ticUpdateData);

            $successCount++;
        }

        return array('invoiceTotal'=>$invoiceTotal,'successCount'=>$successCount,'failCount'=>$failCount);
    }


    /**
     * @function 上传PDF
     * @desc
     * 商户或开票平台可以通过该接口上传PDF。PDF上传成功后将获得发票文件的标识,后续可以通过插卡接口将PDF关联到用户的发票卡券上,一并插入到收票用户的卡包中。
     * 注意:若上传成功的PDF在三天内没有被关联到发票卡券发送到用户卡包上,将会被清理。若商户或开票平台需要在三天后再关联发票卡券的话,需要重新上传。
     *
     * @param $appId
     * @return array
     * @throws LocalServiceException
     * @throws RemoteServiceException
     */
    public function setPDF($appId)
    {
        $appIdModel = new AppIDModel();
        $row = $appIdModel->fetchByWxAppId($appId);
        if($row)
        {
            //1.获取AccessToken
            $token = $this->getAccessToken($appId);

            //2.
            $url = "https://api.weixin.qq.com/card/invoice/platform/setpdf?access_token={$token}";

            //3.获取已经申请航信的发票,上传PDF
            $invoiceModel = new ModelInvoice();
            $preSendInvoices = $invoiceModel->getPreSendInvoice();
            $invoiceTotal = count($preSendInvoices);

            $successCount = 0;
            $failCount = 0;
            foreach ($preSendInvoices as $preSendInvoice)
            {
                $fileName = $preSendInvoice['file_name'].'.pdf';
                $fileUrl = $preSendInvoice['file_url'];
                $fields = array(
                    'type' => 'pdf',
                    'filename' => $fileName,
                    'filesize' => '',
                    'offset' => 0,
                    'filetype' => '.pdf',
                    'originName' => $fileName,
                    'upload'=>file_get_contents($fileUrl)
                );

                $ret = $this->postPDF($url,$fields);
                \PhalApi\DI()->logger->error('$res : ' . json_encode($ret));

                if($ret === false or !empty($ret['errcode'])){
                    $failCount++;
                    continue;
                }

                $PDFMediaId = $ret['s_media_id'];
                $updateData = array('pdf_media_id' => $PDFMediaId);
                $params = array('id' => $preSendInvoice['id']);
                $invoiceModel->updateInvoice($params,$updateData);
                $successCount++;

            }
            return array('invoiceTotal'=>$invoiceTotal,'successCount'=>$successCount,'failCount'=>$failCount);

        }else
        {
            throw new LocalServiceException("账户信息不存在或配置有误",99);
        }
    }


    /**
     * @function 发送pdf
     * @param $url string
     * @param $param array
     * @return mixed
     */
    private static function postPDF($url,$param)
    {
        $delimiter = uniqid();
        $data = '';
        $eol = "\r\n";
        $upload = $param['upload'];
        unset($param['upload']);

        foreach ($param as $name => $content)
        {
            $data .= "--" . $delimiter . "\r\n"
                . 'Content-Disposition: form-data; name="' . $name . "\"\r\n\r\n"
                . $content . "\r\n";
        }
        // 拼接文件流
        $data .= "--" . $delimiter . $eol
            . 'Content-Disposition: form-data; name="pdf"; filename="' . $param['filename'] . '"' . "\r\n"
            . 'Content-Type:application/octet-stream'."\r\n\r\n";

        $data .= $upload . "\r\n";
        $data .= "--" . $delimiter . "--\r\n";

        $post_data = $data;
        $curl = curl_init($url);
        curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($curl, CURLOPT_POST, true);
        curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // stop verifying certificate
        curl_setopt($curl, CURLOPT_POSTFIELDS, $post_data);
        curl_setopt($curl, CURLOPT_HTTPHEADER, [
            "Content-Type: multipart/form-data; boundary=" . $delimiter,
            "Content-Length: " . strlen($post_data)
        ]);
        $response = curl_exec($curl);
        curl_close($curl);
        $info = json_decode($response, true);
        return $info;
    }




}

调用示例

1.获取授权链接 

$invoiceClass = new Invoice();
$appId = \PhalApi\DI()->config->get('app.kgkx.wxAppId');//用于获取对应(zc2_ticket_appid)的配置信息
$uid = $this->uid;//车票所属的用户openid
$order_no = $this->orderNo;//车票所属的订单号
$ticket_no = $this->ticketNo;//车票号

//getWxAuthUrl方法已经包含了“大概逻辑”里的前5步,最后只要将授权链接在小程序打开即可
$invoiceClass ->getWxAuthUrl($appId,$uid,$order_no,$ticket_no); 

返回结果

{
    "ret": 200,
    "data": {
        "errcode": 0,
        "errmsg": "ok",
        "auth_url": "pages/auth/auth?s_pappid=d3g1Y2Q4NTI3NWI4NDdlM2I2X7xcO5NPKJ9cUhYRLlygl8Dc2ZUl9PjYwiOwmLytYQwA&appid=wx5cd85275b847e3b6&num=1&o1=201904191345503490_0028657333&m1=15&t1=1557816628&source=wxa&type=1&signature=75dcbd921494457089d6c59534f9769624802b27",
        "appid": "wx9db2c16d0633c2e7"
    },
    "msg": ""
}

 

2. 小程序打开授权链接代码

//发票授权
  invoiceAuth: function () {
    wx.navigateToMiniProgram({
      appId: 'wx9db2c16d0633c2e7',
      path: 'pages/auth/auth?s_pappid=d3g1Y2Q4NTI3NWI4NDdlM2I2X7xcO5NPKJ9cUhYRLlygl8Dc2ZUl9PjYwiOwmLytYQwA&appid=wx5cd85275b847e3b6&num=1&o1=201904191345503490_0028657333&m1=15&t1=1557816628&source=wxa&type=1&signature=75dcbd921494457089d6c59534f9769624802b27',
      success(res) {
        console.log('navigateToMiniProgram success:', res)
      },
      fail(error) {
        console.log('navigateToMiniProgram fail:', error)
      },
      complete(res) {
        console.log('navigateToMiniProgram complete:', res)
      }
    })
  }

值得一提的是,这里的appId需要再app.json文件先配置好

"navigateToMiniProgramAppIdList": [
    "wx9db2c16d0633c2e7"
  ]

 

3.主动查询授权状态

 说明:这里会用到定时任务的方式。查询到授权状态后,会将信息填到电子发票记录表,用来请求第三方开票平台

$invoiceClass = new Invoice();
$appId = \PhalApi\DI()->config->get('app.kgkx.wxAppId');//用于获取对应(zc2_ticket_appid)的配置信息

$invoiceClass ->getAuthData($appId); 

 

4. 上传PDF

说明: 这里会用到定时任务的方式。请求第三方开票平台后,会返回一个发票PDF文件的地址,我们先用file_get_contents下载PDF文件,再通过数据格式使用multipart/form-data上传到微信。上传成功后,会返回一个s_media_id, 这个在发送发票到用户卡包的时候用到,需要对应发票记录来存。这里面的代码参考了:https://blog.51cto.com/suiwnet/2125883

$invoiceClass = new Invoice();
$appId = \PhalApi\DI()->config->get('app.kgkx.wxAppId');//用于获取对应(zc2_ticket_appid)的配置信息

$invoiceClass ->setPDF($appId); 

 

5.发送发票到用户卡包

$invoiceClass = new Invoice();
$appId = \PhalApi\DI()->config->get('app.kgkx.wxAppId');//用于获取对应(zc2_ticket_appid)的配置信息

$invoiceClass ->sendInvoice($appId); 


总结

1. 开发票的流程到这里已经完成。

2. 需要完善的是,发票冲红的问题。

3. 存在一个问题,access_token过了一段时间(这段时间小于返回的有效时间-200秒)之后就过期了,这里也需要继续完善。(2019-08-20)问题已经确认,是因为在多个地方用到了这个公众号的access_token,每用一次,上一个就可能过期,解决办法是,加一个验证checkAccessToken($access_token)。

你可能感兴趣的:(PHP,微信小程序)