支付开票

简介

前置条件
1.商家已接通支付宝支付功能;(目前支付开票支持的支付场景包括:当面付、Wap支付、生活缴费、线上支付等)
2.商家已开通或者授权ISV开通了支付宝的支付开票功能。

应用场景
用户在商家处消费,使用支付宝支付;在支付完成后,可在该笔交易的对应的页面直接发起开票请求,用户之后进入开票页面选择抬头,申请开具发票。商家开票服务开具发票,将发票插入到支付宝发票管家,发票管家会将发票交付并通知用户。

支付宝提供的能力

1.开票入口
在支付宝端内和交易相关的页面提供开票入口,方便用户快速便捷的发起开票,具体的入口有:

a.支付结果页
示例图,如下:

b.账单详情页
示例图,如下:

c.支付助手页
示例图,如下:

说明:
      a.在哪些页面露出开发票由商家决定;
      b.支付结果页可单独选择是否露出;
      c.支付助手和账单详情页是一体控制的,只能同时选择或者同时不选择。

2.抬头输出能力
在用户授权的情况下将抬头输出给商家用于开发票:

3.发票交付能力
用户支付开票后,调用支付宝的接口可将发票插入支付宝发票管家,由发票管家交付并通知用户。

业务流程

1.用户在商户处消费,使用支付宝支付;
2.用户在支付结果页、支付助手页、账单详情页点击【开发票】;
3.用户选择发票抬头;
4.提交开票申请;
5.商家开票系统自动开票;
6.发票回传到支付宝发票管家,发票管家将发票交付并通知用户。

模式介绍

支付宝支付开票根据【是否露出开发票】的判断方式不同,分为两种支付开票模式:

1.支付后判断模式

此模式开发票的入口判断放在支付完成后,由支付宝根据不同的维度进行判断;
对于直连支付的交易支持PID、收款账号id维、门店id三种维度的判断;
对于间连支付的交易支持SMID维度的判断。

2.支付前判断模式

此模式开发票的入口判断放在调用支付宝支付接口前,由商户或者商户委托开发【支付】功能的服务商进行判断;
在此模式下,调支付宝支付接口时将开发票标志、品牌简称、商户简称同时传递给支付宝;
支付宝根据支付时传递的标志判断是否露出开发票按钮;

具体支付接口中传参字段如下:

"invoice_info": {
        "key_info": {
            "is_support_invoice": "true",    //是否支持开票
            "invoice_merchant_name": "BEST_WONDER|BEST_WONDER",   //品牌简称|商户简称  不能带空格,|必须要英文竖线   中文无法校验
            "tax_num": "44010068329136" //税号
        }

说明
a.品牌简称和商户简称为调支付宝商户入驻接口{alipay.ebpp.invoice.merchantlist.enter.apply}的品牌和商户简称;
b.优先推荐【支付后判断】模式,如果商家业务上类似有不同开票项目、不同税率的、部分商品才能开票的诉求请选择【支付前判断】模式;
c.【支付前判断】模式,请提前协调好支付服务商进行改造处理。

DEMO

说明:logo和显示名称支持配置,由商家决定具体的logo和名称(通过【配置申请表】提交)。

系统流程

接入说明

1. 支付宝推送开票所需信息

ISV需按照以下规范提供申请开票接口。当用户在支付宝标准开票页提交开票申请之后,支付宝服务端会推送开票所需信息给ISV服务器的接口,由ISV检验开票参数,处理用户的开票申请。

ISV需要自行验证参数的有效性,并且做幂等控制,接收到支付宝推送的开票信息之后,ISV需要实时返回受理申请的结果。
支付宝端会对调用失败的申请进行重试,如有不可重试的错误需与支付宝进行约定。
ISV接口仅受理开票申请,需马上返回结果是否申请成功,注意这里仅仅是提交开票申请的操作,而不是执行开票的操作,ISV需保证开票操作异步化,不要在该接口上执行同步开票操作。

说明:支付宝只调用开票申请接口,不调用开票接口,开票信息组装和接口调用由ISV自行设计。

入参说明:

applyId String 支付宝开票申请id,该id为支付宝内的开票申请唯一标识,与订单号绑定。
invoiceAmount String 开票金额
orderNo String 订单号
mShortName String 商户的品牌名称简称,与发票回传接口的商户名称保持一致
subShortName String 支付宝为商户分配的商户门店简称,与发票回传接口的商户名称保持一致
payerName String 抬头名称
payerRegisterNo String 纳税人识别号
payerAddress String 地址
payerTelPhone String 电话
payerBankName String 开户行
payerBankAccount String 账号
userEmail String 用户邮箱
userMobileNo String 用户手机号
sign String 签名,防篡改

以上为支付宝必传的参数,其他重定向带过来的参数会一并透传。

响应参数:

resultMsg String 业务结果说明
resultCode String 业务结果码
resultUrl String 用户提交申请之后,支付宝侧会提供入口让用户访问ISV提供的结果页面(可选,链接需进行urlencode)
sign String 签名

结果码:

如果支付宝推送的开票信息已经开票成功,ISV需明确返回开票成功,如果开票异常也需要返回明确的异常结果。下面是结果码示例,ISV可以定义自己的错误码,但需要自定义的错误码需要跟支付宝端确认映射关系,错误码至少需要包含以下几种业务结果含义。

resultCode resultMsg 说明
APPLY_SUCCESS 申请成功 开票申请的信息校验无误,已提交开票
INVOICE_PARAM_ILLEGAL 开票参数非法 加签验证不通过,金额不准确等开票参数异常

2. ISV开票结果响应

开票成功
由于调用ISV开票申请接口是异步执行开票操作,ISV在开票成功时,ISV在调用回传发票接口时需要将支付宝开票申请id(applyId)一并带回,用于将发票与用户的开票申请关联起来。有了applyId就无需填写userId,因为开票申请id可以关联到用户。
参考《alipay.ebpp.invoice.info.send

3. 加签验签说明

加签

1.    排序:将筛选的参数按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推;
2.    拼接:将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串;
3.    生成sign:用sha1算法对待签名字符串计算摘要,然后使用RSA私钥对摘要进行加密,得到sign;
4.    生成最终参数:sign的值放到参数map中一起生成queryString格式的字符串。

待加签字符串
customParam2=自定义参数2&invoiceAmount=10.75&mShortName=xiaokai&orderNo=123456&productCode=PAYMENT_OPEN&resultUrl=http%3A%2F%2Fdummy.com&subShortName=xiaokai

加签后最终参数
customParam2=自定义参数2&invoiceAmount=10.75&mShortName=xiaokai&orderNo=123456&productCode=PAYMENT_OPEN&resultUrl=http%3A%2F%2Fdummy.com&sign=NMd%2Fm%2B2TlaQoMPhj%2BmYHOrgWgOM02PNKqeX0xxKj3DmvrXbCdvgfXmyzzyOOAAxyftYTcFzLfV0CNhwsuZZcoJHAzSLAZAi%2BoyonRhKGL2pGpYJMsJy8qXdkkzj9tIXlBa2n6jJg%2BdWLdZjUGRjPCrYGbGzvwcHd51ikMn0wB10%3D&subShortName=xiaokai

验签

1.    获取参数中的sign:从参数的最后面截取sign;
2.    解密sign:用公钥解密sign得到摘要1;
3.    对比摘要:用sha1算法对参数(排除sign)计算摘要2,如果摘要1与摘要2相等,说明参数未被篡改,否则说明参数被篡改。

代码示例

/**
 * Alipay.com Inc.
 * Copyright (c) 2004-2017 All Rights Reserved.
 */
package com.alipay.antinvoice.common.util;

import org.apache.commons.codec.binary.Base64;

import java.security.KeyFactory;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;

/**
 * RSA加密算法工具类
 * @author xingqian.xq
 * @version $Id: RSAUtil.java, v 0.1 2017-01-13 下午3:16 xingqian.xq Exp $$
 */
public class RSAUtil {
    /**
     * 加签
     * @param data  待加签数据
     * @param privateKey  私钥
     * @return  签名
     * @throws Exception  异常
     */
    public static String sign(String data, PrivateKey privateKey) throws Exception {

        byte[] keyBytes = privateKey.getEncoded();
        PKCS8EncodedKeySpec pkcs8KeySpec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateK = keyFactory.generatePrivate(pkcs8KeySpec);
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initSign(privateK);
        signature.update(data.getBytes("GBK"));
        return new String(Base64.encodeBase64(signature.sign()));
    }

    /**
     * 验签
     * @param publicKey  公钥
     * @param srcData  原始字符串
     * @param sign  签名
     * @return  是否验签通过
     * @throws Exception  异常
     */
    public static boolean verify(String srcData, PublicKey publicKey, String sign) throws Exception {

        byte[] keyBytes = publicKey.getEncoded();
        X509EncodedKeySpec keySpec = new X509EncodedKeySpec(keyBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PublicKey publicK = keyFactory.generatePublic(keySpec);
        Signature signature = Signature.getInstance("SHA1withRSA");
        signature.initVerify(publicK);
        signature.update(srcData.getBytes("GBK"));
        return signature.verify(Base64.decodeBase64(sign.getBytes()));
    }

    /**
     * 获取密钥对
     * @return  密钥对
     * @throws Exception  异常
     */
    public static KeyPair getKeyPair() throws Exception {
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(1024);
        KeyPair pair = generator.generateKeyPair();
        return pair;
    }

    /**
     * 获取私钥
     *
     * @param privateKey  私钥字符串
     * @return 私钥对象
     */
    public static PrivateKey getPrivateKey(String privateKey) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        byte[] encodedKey = privateKey.getBytes();

        encodedKey = Base64.decodeBase64(encodedKey);

        EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(encodedKey);
        return keyFactory.generatePrivate(keySpec);
    }

    /**
     * 获取公钥
     * @param publicKey   公钥字符串
     * @return 公钥对象
     * @throws Exception 异常
     */
    public static PublicKey getPublickey(String publicKey) throws Exception {
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");

        X509EncodedKeySpec bobPubKeySpec = new X509EncodedKeySpec(Base64.decodeBase64(publicKey
            .getBytes()));
        return keyFactory.generatePublic(bobPubKeySpec);
    }
}

/**
 * Alipay.com Inc.
 * Copyright (c) 2004-2018 All Rights Reserved.
 */
package com.alipay.antinvoice.core.service.stripper;

import java.net.URLDecoder;
import java.net.URLEncoder;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.alipay.antinvoice.common.util.RSAUtil;

/**
 * @author hansong.xhs
 * @version $Id: SignTest.java, v 0.1 2018年05月03日 下午8:36 hansong.xhs Exp $
 */
public class SignTest {

    public static String privateKey = "MIICdQIBADANBgkqhkiG9w0BAQEFAASCAl8wggJbAgEAAoGBAIgp2pq98axr1IcLrsW5MPUqeggdeGPAyh8+2aHObn4ER6KmpTgo+pi2SScFoNgesWx72kY2PEEGsUwcTXKkSYaeZVs7z7Z/ujQvP0kbPGouG7Q6MN5+jud0sM2Gkmu60fGJXQ8IfeFi6oQCKZpMviXg00KV7ct6JmHlgUpP0jR5AgMBAAECgYBlxAo6/t1iBVFZATVFV4ysn2uHJyd0PoGR6rJTSWqxSleTy8LN/2qTuiFgRceZ3w6xyrsvIJfV7b+S59BGb1z3ZdO09gOeviP5W3pC23ClItBlsyNf4njXylQus9Nl4ZnKcV/UEDvjChGIma8ZZChkxHNpLls9WGWFkQkFk88TkQJBALuOBCgCVyI1Lm2r0mPlC7RbuPiKzmT5kOs0+1696WulbRMODPGXsz8ivMJSBQy+ehU6xV//nd5GOAfjt5PqRw0CQQC52rJZdqPGxHhESw8eI5ZBe9Mz7b6T2UKuPJch6xwpiMPlQB7p0hk5Nhnyic01fppG3gpP6MEtHwiTOTMwXsgdAkAqzpkoQJB+oEC+i07zud1YBu9K2vOMnGF1LZyJ3TKffRxOExDlO0iQCm+msm2woPDgU4+k/4SarNAxDMpjmj8pAkA6/1WGWMb8nfmflEQkSR+1gd01qs7ImDs2nD1Noxi5hpTI/WXSy8L+ClKKT3w48wt+W5Xib/yCmktakNnTDQNxAkBbXimHdGVY3GrZszuAN1n3cyafBSTqpFUdqhfRZ/QLj7wJcmJ+PrLpyB6KMZVsnjzOCS9/tWtIZi14ynhOvM2l";

    public static String publicKey  = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCIKdqavfGsa9SHC67FuTD1KnoIHXhjwMofPtmhzm5+BEeipqU4KPqYtkknBaDYHrFse9pGNjxBBrFMHE1ypEmGnmVbO8+2f7o0Lz9JGzxqLhu0OjDefo7ndLDNhpJrutHxiV0PCH3hYuqEAimaTL4l4NNCle3LeiZh5YFKT9I0eQIDAQAB";

    public static void main(String[] args) throws Exception {
        String params = sign(RSAUtil.getPrivateKey(privateKey));
        System.out.println(params);
        verify(params, RSAUtil.getPublickey(publicKey));
    }

    public static void verify(String params, PublicKey publicKey) throws Exception {
        //解析参数
        Map<String, String> map = restoreMap(params, "&");
        //urldecode 签名
        String sign = URLDecoder.decode(map.get("sign"), "utf-8");
        //去除签名参数
        map.remove("sign");
        //生成待验签字符串
        String signatureContent = getSignatureContent(map);
        System.out.println(signatureContent);
        System.out.println(RSAUtil.verify(signatureContent, publicKey, sign));
    }

    /**
     * 加签demo方法
     */
    public static String sign(PrivateKey privateKey) {
        try {
            //获取待验签原始数据
            Map<String, String> params = new HashMap<String, String>();
            params.put("invoiceAmount", "开票金额");
            params.put("orderNo", "订单号");
            params.put("mShortName", "商户的品牌名称简称");
            params.put("subShortName", "商户门店简称");
            params.put("customParam1", "自定义参数1");
            params.put("customParam2", "自定义参数2");

            //获取原始加签内容
            String signatureContent = getSignatureContent(params);

            //获取签名,得到签名之后进行urlencode
            String sign = URLEncoder.encode(RSAUtil.sign(signatureContent, privateKey), "utf-8");
             params.put("sign", sign);

            return getSignatureContent(params);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return "";
    }

    /**
     * 获得需要签名的数据,按照参数名字母升序的顺序将所有参数用&连接起来
     * @param params 待签名参数集
     * @return 排好序的待签名字符串
     */
    public static String getSignatureContent(Map<String, String> params) {
        if (params == null) {
            return null;
        }
        StringBuffer content = new StringBuffer();
        List<String> keys = new ArrayList<String>(params.keySet());
        Collections.sort(keys);

        for (int i = 0; i < keys.size(); i++) {
            String key = keys.get(i);
            String value = params.get(key);
            content.append((i == 0 ? "" : "&") + key + "=" + value);
        }

        return content.toString();
    }

    /**
     * 把数据库里边String型的扩展字段转换成map型
     * @param extension 需要转换的String
     * @param spiltorKey 指定的字符串
     * @return      转换后的扩展MAP
     */
    public static Map<String, String> restoreMap(String extension, String spiltorKey) {
        Map<String, String> extensionMap = new HashMap<String, String>();
        String[] fieldList = extension.split(spiltorKey);
        for (String field : fieldList) {
            String[] entry = field.split("=");
            if (entry.length >= 2) {
                if (entry.length > 2) {
                    //如果包含多个“=”取第一个后边的为value,如url后边的
                    for (int i = 2; i < entry.length; i++) {
                        entry[1] += ("=" + entry[i]);
                    }
                }
                extensionMap.put(entry[0], entry[1]);
            }

        }
        return extensionMap;
    }
}

4.发票信息发送接口调用

发票信息发送:alipay.ebpp.invoice.info.sendSDK调用示例(以JAVA为例,更多语言示例参见接口文档)

AlipayClient alipayClient = new DefaultAlipayClient("
https://openapi.alipay.com/gateway.do
","app_id","your private_key","json","GBK","alipay_public_key","RSA2");
AlipayEbppInvoiceInfoSendRequest request = new AlipayEbppInvoiceInfoSendRequest();
request.setBizContent("{" +
"\"m_short_name\":\"XSD\"," +
"\"sub_m_short_name\":\"XSD_HL\"," +
"      \"invoice_info_list\":[{" +
"        \"user_id\":\"2088399922382233\"," +
"\"invoice_code\":\"4112740003\"," +
"\"invoice_no\":\"41791003\"," +
"\"invoice_date\":\"2017-10-10\"," +
"\"sum_amount\":\"101.00\"," +
"\"ex_tax_amount\":\"100.00\"," +
"\"tax_amount\":\"1.00\"," +
"          \"invoice_content\":[{" +
"            \"item_name\":\"餐饮费\"," +
"\"item_no\":\"1010101990000000000\"," +
"\"item_spec\":\"G39\"," +
"\"item_unit\":\"台\"," +
"\"item_quantity\":1," +
"\"item_unit_price\":\"100.00\"," +
"\"item_ex_tax_amount\":\"100.00\"," +
"\"item_tax_rate\":\"0.01\"," +
"\"item_tax_amount\":\"1.00\"," +
"\"item_sum_amount\":\"101.00\"," +
"\"row_type\":\"0\"" +
"            }]," +
"\"out_trade_no\":\"20171023293456785924325\"," +
"\"invoice_type\":\"BLUE\"," +
"\"invoice_kind\":\"PLAIN\"," +
"\"invoice_title\":{" +
"\"title_name\":\"支付宝(中国)网络技术有限公司\"," +
"\"payer_register_no\":\"9133010060913454XP\"," +
"\"payer_address_tel\":\"杭州市西湖区天目山路黄龙时代广场0571-11111111\"," +
"\"payer_bank_name_account\":\"中国建设银行11111111\"" +
"        }," +
"\"payee_register_no\":\"310101000000090\"," +
"\"payee_register_name\":\"支付宝(杭州)信息技术有限公司\"," +
"\"payee_address_tel\":\"杭州市西湖区某某办公楼 0571-237405862\"," +
"\"payee_bank_name_account\":\"西湖区建行11111111111\"," +
"\"check_code\":\"15170246985745164986\"," +
"\"out_invoice_id\":\"201710283459661232435535\"," +
"\"ori_blue_inv_code\":\"4112740002\"," +
"\"ori_blue_inv_no\":\"41791002\"," +
"\"file_download_type\":\"PDF\"," +
"\"file_download_url\":\"
http://img.hadalo.com/aa/kq/ddhrtdefgxKVXXXXa6apXXXXXXXXXX.pdf
\"," +
"\"payee\":\"张三\"," +
"\"checker\":\"李四\"," +
"\"clerk\":\"赵吴\"," +
"\"invoice_memo\":\"订单号:2017120800001\"," +
"\"extend_fields\":\"m_invoice_detail_url=
http://196.021.871.011:8080/invoice/detail.action?fpdm=
 4112740003&fphm=41791003\"" +
"        }]" +
"  }");
AlipayEbppInvoiceInfoSendResponse response = alipayClient.execute(request);
if(response.isSuccess()){
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}

JSON响应示例

{
"alipay_ebpp_invoice_info_send_response":{
    "code":"10000",
    "msg":"Success",
"url":"
alipays://platformapi/startapp?appId=20000920&url=%2Fwww%2Flist.htm%3Ffrom%3Doffline
"
  }
,"sign":"ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
}
onlineServer