MonikaChen
MonikaChen
  • 发布:2020-12-21 21:32
  • 更新:2020-12-22 00:01
  • 阅读:2799

uni-app uniCloud node.js支付宝网页支付开发心得

分类:uniCloud

支付宝网页支付的流程:前端请求支付宝支付表单参数->后端生成支付表单参数给前端->前端根据支付参数构建form表单对支付宝发起POST请求->支付宝支付成功POST异步通知开发者的服务器。

支付宝用到的是RSA加密,搞懂RSA加密的原理有助于理解支付宝支付流程。推荐李永乐老师讲非对称加密的视频。

这是已经写好的测试例子:https://static-b1ebbd3c-ca49-405b-957b-effe60782276.bspapp.com/static/alipay-demo.html

代码很简单,直接看代码就理解了,注意点写在注释里。

请求支付参数的服务端代码:

'use strict'  
const NodeRSA = require('node-rsa') // 需要执行npm install node-rsa才能调用  
const querystring = require('querystring') // node自带,无需安装,直接调用  
exports.main = async (event, context) => {  
    const appId = '支付宝的appId'  
    let merchantPrivateKey = '支付宝商家公钥'  
      
    let ua = ''  
    try {  
        ua = event.headers['user-agent']  
    }catch(e){}  
    let productCode = 'FAST_INSTANT_TRADE_PAY'  
    let method = 'alipay.trade.page.pay'  
    if (ua.indexOf('Mobile') > -1) {  
        productCode = 'QUICK_WAP_WAY'  
        method = 'alipay.trade.wap.pay'  
    }  
    let bodyObj = querystring.parse(event.body) // url请求参数字符串转object。uni-app云函数实例化后,POST的请求参数在body里  
    if (Object.keys(bodyObj).length) {  
        const passbackParams = JSON.stringify({mobile: '18888888888', sku: 'year'}) // 开发者想要传递的参数,字符串,支付宝异步通知会带上这个  
        let bizContent = JSON.stringify({  
            subject: '支付宝测试-'+bodyObj.price+'元',  
            out_trade_no: (new Date()).getTime(),  
            total_amount: bodyObj.price,  
            product_code: productCode,  
            quit_url: 'https://imgbed.cn/static/alipay-return.html', // 手机网页支付放弃支付时返回的网址  
            passback_params: passbackParams  
        })  
        let queryObject = ksort({  
            app_id: appId,  
            biz_content: bizContent,  
            charset: 'UTF-8',  
            method: method,  
            notify_url: '支付宝异步通知地址',  
            return_url: 'https://imgbed.cn/static/alipay-return.html',  
            sign_type: 'RSA2',  
            timestamp: time2date((new Date()).getTime()), // Y-m-d H:i:s格式的字符串  
            version: '1.0'  
        })  
        const query = querystring.unescape(querystring.stringify(queryObject)) // 要再套一层querystring.unescape,否则query被转义,会导致签名的字符串跟支付宝不一致  
        const key = new NodeRSA(merchantPrivateKey, 'pkcs8-private')  
        const sign = key.sign(Buffer.from(query)).toString('base64')  
        queryObject.sign = sign  
        return {code:0, alipayParams: queryObject}  
    } else {  
        return {code: 1, msg: 'event.body is empty'}  
    }  
      
    /****************************************************************************************************/  
    // 毫秒时间戳转Y-m-d H:i:s或者Y-m-d  
    function add0(m){return m<10?'0'+m:m }  
    function time2date(shijianchuo, onlyDate) {  
        var time = new Date(shijianchuo);  
        var y = time.getFullYear();  
        var m = time.getMonth()+1;  
        var d = time.getDate();  
        var h = time.getHours();  
        var mm = time.getMinutes();  
        var s = time.getSeconds();  
        let returnStr = y+'-'+add0(m)+'-'+add0(d)  
        if (!onlyDate) {  
            returnStr += ' '+add0(h)+':'+add0(mm)+':'+add0(s)  
        }  
        return returnStr  
    }  
      
    // 对object的key进行排序  
    function ksort(params) {  
        let keys = Object.keys(params).sort();  
        let newParams = {};  
        keys.forEach((key) => {  
            newParams[key] = params[key];  
        });  
        return newParams;  
    }  
}

前端支付代码,为了方便,我用了vue+vant:

<!DOCTYPE html>  
<html>  
    <head>  
        <meta charset="utf-8">  
        <meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0, viewport-fit=cover">  
        <title>支付宝支付演示</title>  
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/vant@2.11/lib/index.css"/>  
    </head>  
    <body>  
        <div id='app' style="display: none; padding: 100px 15px 15px 15px;">  
            <div style="max-width: 512px; margin: 0 auto;">  
                <p><img height="44px" src="https://vkceyugu.cdn.bspapp.com/VKCEYUGU-imgbed/dca221bb-2a37-4527-bea7-6fcb92945c18.png"></p>  
                <p v-for="price in prices"><van-button @click="alipay(price)" type="info" block>{{price}}元</van-button></p>  
            </div>  
            <div id="form-pay"></div>  
              
            <van-overlay :show="showLoading">  
                <div style="display: flex; align-items: center; justify-content: center; height: 100%">  
                    <van-loading size="24px" vertical>加载中...</van-loading>  
                </div>  
            </van-overlay>  
        </div>  
        <script src="https://cdn.bootcdn.net/ajax/libs/zepto/1.2.0/zepto.min.js"></script>  
        <script src="https://cdn.bootcdn.net/ajax/libs/vue/2.6.12/vue.min.js"></script>  
        <script src="https://cdn.jsdelivr.net/npm/vant@2.11/lib/vant.min.js"></script>  
        <script src="https://cdn.bootcdn.net/ajax/libs/axios/0.21.0/axios.min.js"></script>  
        <script src="https://cdn.bootcdn.net/ajax/libs/qs/6.9.4/qs.min.js"></script>  
        <script>  
            const vm = new Vue({  
                el: '#app',  
                data() {  
                    return {  
                        prices: [0.01, 1, 10, 100],  
                        showLoading: false  
                    }  
                },  
                methods: {  
                    alipay(price) {  
                        if (navigator.userAgent.indexOf('MicroMessenger')>-1) {  
                            vant.Dialog({ message: '点击微信右上角···,选择“用浏览器打开”' })  
                        } else {  
                            this.showLoading = true  
                            axios({  
                                url: 'https://b1ebbd3c-ca49-405b-957b-effe60782276.bspapp.com/http/alipay-params-demo', // uni-app云函数URL实例化的api  
                                data: Qs.stringify({price: price}),  
                                method: 'POST'  
                            }).then(res=>{  
                                this.showLoading = false  
                                if (0===res.data.code) {  
                                    const obj = res.data.alipayParams  
                                    const keys = Object.keys(obj)  
                                    let formHtml = ''  
                                    formHtml += '<meta charset="utf-8">'  
                                    formHtml += '<form id="alipaysubmit" method="POST" name="alipaysubmit" action="https://openapi.alipay.com/gateway.do?charset=UTF-8">'  
                                    for (i=0; i<keys.length; i++) {  
                                        formHtml += '<input type="hidden" name="'+keys[i]+'" value=\''+obj[keys[i]]+'\'>'  
                                    }  
                                    // formHtml += '<input type="submit">' // 手动提交表单  
                                    formHtml += '</form>'  
                                    $('#form-pay').html(formHtml)  
                                    document.forms["alipaysubmit"].submit() // 自动提交表单  
                                } else {  
                                    console.error(res.data.msg)  
                                }  
                            }).catch(err=>{  
                                this.showLoading = false  
                                console.error(err)  
                            })  
                        }  
                    }  
                }  
            })  
            $(document).ready(function(){  
                $('#app').show()  
            })  
        </script>  
    </body>  
</html>

支付成功后,支付宝会异步通知,开发者接收异步通知,验签,代码如下:

'use strict'  
const querystring = require('querystring')  
const NodeRSA = require('node-rsa')  
exports.main = async (event, context) => {  
    const alipayPublicKey = '支付宝公钥'  
      
    let bodyObj = ksort(querystring.parse(event.body))  
    if ('TRADE_SUCCESS'===bodyObj.trade_status) { // 支付失败也有可能会收到异步通知,所以这里要判断TRADE_SUCCESS  
        const outTradeNo = bodyObj.out_trade_no  
        if (outTradeNo是否已经存在于数据库) { // 异步通知可能会收到多次,判断out_trade_no是否存过数据库来判断重复通知  
            const sign = bodyObj.sign  
            delete bodyObj.sign  
            delete bodyObj.sign_type  
            const body = querystring.unescape(querystring.stringify(bodyObj, '&', '='))  
            const key = new NodeRSA(alipayPublicKey, 'pkcs8-public')  
            if (key.verify(Buffer.from(body), sign, 'utf8', 'base64')) { // 验签通过,继续执行业务代码  
                // 用户处理订单的代码  
            } else {  
                console.error('验签失败')  
            }  
        } else {  
            console.error('订单号已存在')  
        }  
    } else {  
        console.error('trade_status', bodyObj.trade_status)  
    }  
      
    return 'success' // 一定要返回'success'给支付宝,否则会重复多次通知  
}  
  
// 对object的key进行排序  
function ksort(params) {  
    let keys = Object.keys(params).sort();  
    let newParams = {};  
    keys.forEach((key) => {  
        newParams[key] = params[key];  
    });  
    return newParams;  
}

转载自:https://coding3.com/archives/unicloud-alipay.html

1 关注 分享
1***@qq.com

要回复文章请先登录注册

MonikaChen

MonikaChen (作者)

回复 DCloud_heavensoft :
uniPay必须支持
2020-12-22 00:01
DCloud_heavensoft

DCloud_heavensoft

欢迎给uniPay贡献源码:https://gitee.com/dcloud/uniPay
2020-12-21 23:58