支付宝标准页面

DEMO示例

请打开支付宝扫描下方二维码观看示例:

image.png

下图红框内的logo和显示名称支持商家或者服务商自定义:

image.png

流程图

image.png

说明:

  1. 用户扫描开票二维码,首先进入ISV的中间页面,在该页面ISV需准备好开票所需参数,例如金额,订单号等,然后携带这些参数重定向到支付宝的标准开票;
  2. 在支付宝标准开票页上用户可以选择抬头然后提交开票申请;
  3. 支付宝服务端接到开票申请之后,会落地申请到数据库,用户可以在发票管家-开票申请中找到申请详情。然后支付宝服务端会推送开票所需信息给ISV的服务器;
  4. 如果开票成功,ISV将发票回传到支付宝;
  5. 如果开票失败,ISV调用支付宝开票结果接口返回失败原因。

接口说明

1.重定向至支付宝标准开票页所需参数说明

用户扫描开票二维码之后打开ISV的页面,ISV需要构造开票参数重定向到支付宝的标准开票页。

invoiceAmount String 开票金额,保留2位小数,单位为元
orderNo String 订单号,由数字、26个英文字母或下划线组成的字符串,长度不超过64
mShortName String 商户的品牌名称简称,与发票回传接口的商户名称保持一致
subShortName String 支付宝为商户分配的商户门店简称,与发票回传接口的商户名称保持一致
resultUrl String 用户提交申请之后,支付宝侧会提供入口让用户访问ISV提供的结果页面(可选,链接需进行urlencode)
sign String 签名,防篡改

以上参数为必填参数,其他透传参数可由ISV按需提供,支付宝侧负责透传至开票申请接口。

跳转标准开票页面链接格式

alipays://platformapi/startapp?appId=20000920&startMultApp=YES&appClearTop=false&url=encodeUrl(${url});

${url}格式说明
/www/route.htm?scene=STANDARD_INVOICE&invoiceParams=encodeUrl(${invoiceParams})

${invoiceParams}的格式见下面加签验签说明

alipays://platformapi/startapp?appId=20000920&startMultApp=YES&appClearTop=false&url=%2Fwww%2Froute.htm%3Fscene%3DSTANDARD_INVOICE%26invoiceParams%3DcustomParam2%253D%25E8%2587%25AA%25E5%25AE%259A%25E4%25B9%2589%25E5%258F%2582%25E6%2595%25B02%2526invoiceAmount%253D10.80%2526mShortName%253DSTANDARD_INVOICE%2526orderNo%253D123456%2526resultUrl%253Dhttp%25253A%25252F%25252Fdummy.com%2526sign%253Dc%25252Bm%25252BI0y5f6G3UTq570XWVfdPi8Wwkk1kjrupdnUhnZ%25252BYzYXcTHNQp3WbImiwJIZ%25252BbrV9YhI3S3ZRKtQ2bMF0roAr8vpO4ie0sdKhGNpuiho3ny3tdA9%25252B9Ag48AByaesaQu3EakdfL0ZdncvEuYi2BQ1wMItng6XtXIQlVS1cJ20%25253D%2526subShortName%253DSTANDARD_INVOICE

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

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 业务结果码
sign String 签名

结果码

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

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

3.ISV开票结果响应

开票成功

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

回传接口中:out_trade_no必须跟orderNo一致。参考《alipay.ebpp.invoice.info.send

开票失败

如果开票失败调用开票结果同步接口,返回开票失败原因。
参考《alipay.ebpp.invoice.apply.result.sync (税控商向支付宝同步发票申请结果)》

4.加签验签说明

加签

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

验签

a.获取参数中的sign:从参数的最后面截取sign
b.解密sign:用公钥解密sign得到摘要1
c.对比摘要:用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.security.KeyPair;
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 = ConvertUtil.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);
        if (null == fieldList) {
            return extensionMap;
        }
        for (String field : fieldList) {
            String[] entry = StringUtil.split(field, "=");
            if (entry != null && 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;
    }
}

5.商家批量入驻申请:alipay.ebpp.invoice.merchantlist.enter.apply

请求参数

AlipayClient alipayClient = new DefaultAlipayClient("
https://openapi.alipay.com/gateway.do
","app_id","your private_key","json","GBK","alipay_public_key","RSA2");
AlipayEbppInvoiceMerchantlistEnterApplyRequest request = new AlipayEbppInvoiceMerchantlistEnterApplyRequest();
request.setBizContent("{" +
"\"merchant_base\":{" +
"\"m_short_name\":\"MYJF\"," +
"\"m_name\":\"蚂蚁金融服务集团\"," +
"\"logo_info\":\"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAA...\"" +
"    }," +
"      \"sub_merchant_list\":[{" +
"        \"sub_m_short_name\":\"MYJF_HZ\"," +
"\"sub_m_name\":\"杭州蚂蚁金服信息技术有限公司\"," +
"\"register_no\":\"91500000747150346A\"" +
"        }]," +
"\"sub_merchant_common_info\":{" +
"\"product_code\":\"QRCODE_MERCHANT_OPEN\"," +
"\"s_short_name\":\"SAD\"" +
"    }" +
"  }");
AlipayEbppInvoiceMerchantlistEnterApplyResponse response = alipayClient.execute(request);
if(response.isSuccess()){
System.out.println("调用成功");
} else {
System.out.println("调用失败");
}

JSON响应示例

{
"alipay_ebpp_invoice_merchantlist_enter_apply_response":{
    "code":"10000",
    "msg":"Success"
  }
,"sign":"ERITJKEIJKJHKKKKKKKHJEREEEEEEEEEEE"
}

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

发票信息发送: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