• 【Vue+DRF 生鲜电商】订单支付(九)


    1. 支付宝沙箱环境配置

    蚂蚁金服平台:https://open.alipay.com/platform/home.htm(正式接入:创建应用)。

    因为个人不能接入支付宝进行支付,只有企业才可以,因此本项目采用支付宝沙箱环境进行模拟支付。

    沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info

    1.1 生成公钥私钥

    涉及到的三个概念:

    • 支付宝提供的公钥:使用沙箱应用公钥设置,由支付宝提供
    • 沙箱应用公钥:工具生成
    • 沙箱应用私钥:工具生成

    沙箱应用公钥私钥生成

    须借助支付宝提供的工具进行生成,工具下载地址:https://docs.open.alipay.com/291/105971/,选择 Windows 下载安装。

    生成方法

    打开密钥文件,并将两个密钥文件拷贝到项目(新建) apps/trade/keys/,分别改名为:private_2048.txt、pub_2048.txt,并对文件内容进行修改:

    -----BEGIN PRIVATE KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAnUSwOnN9Kuoxxxxxxxxxxxxxxxxxxxxx
    -----END PRIVATE KEY-----
    

    文档:https://opendocs.alipay.com/open/291/106097


    支付宝公钥生成

    推荐 RSA2,点击 沙箱应用 ---> 信息配置 ----> 必看部分 ----> RSA2(SHA256)密钥(推荐) 进行设置,将沙箱公钥设置到此处。

    设置成功后,支付宝生成一个公钥,复制公钥内容将其拷贝到 apps/trade/keys/,新建文件 alipay_key_2048.txt,开头结尾仍然使用上面的方式进行包裹。

    1.2 支付宝支付接口分析

    开放文档接口:https://openhome.alipay.com/developmentDocument.htm,选择电脑网站支付,点击 API 列表,点击 alipay.trade.page.pay, 进入统一收单下单并支付页面接口。

    本项目只模拟订单支付,不支持退款、退款查询等功能,因此使用 alipay.trade.page.pay 接口即可,具体请求参数请参考官方文档。

    请求地址: https://openapi.alipay.com/gateway.do

    1.3 请求签名实现

    如果未使用支付宝开发平台 SDK,需要自行实现签名,主要分为两种:

    • 应用私钥生成请求签名
    • 支付宝公钥生成请求签名

    下面介绍的是应用私钥生成请求签名:

    1、筛选并排序

    获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数,并按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。

    2、拼接

    将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串。

    例如下面的请求示例,参数值都是示例,开发者参考格式即可:

    REQUEST URL: https://openapi.alipay.com/gateway.do
    REQUEST METHOD: POST
    CONTENT:
    app_id=2014072300007148
    method=alipay.mobile.public.menu.add
    charset=GBK
    sign_type=RSA2
    timestamp=2014-07-24 03:07:50
    biz_content={"button":[{"actionParam":"ZFB_HFCZ","actionType":"out","name":"话费充值"},{"name":"查询","subButton":[{"actionParam":"ZFB_YECX","actionType":"out","name":"余额查询"},{"actionParam":"ZFB_LLCX","actionType":"out","name":"流量查询"},{"actionParam":"ZFB_HFCX","actionType":"out","name":"话费查询"}]},{"actionParam":"http://m.alipay.com","actionType":"link","name":"最新优惠"}]}
    sign=e9zEAe4TTQ4LPLQvETPoLGXTiURcxiAKfMVQ6Hrrsx2hmyIEGvSfAQzbLxHrhyZ48wOJXTsD4FPnt+YGdK57+fP1BCbf9rIVycfjhYCqlFhbTu9pFnZgT55W+xbAFb9y7vL0MyAxwXUXvZtQVqEwW7pURtKilbcBTEW7TAxzgro=
    version=1.0
    

    则待签名字符串为:

    app_id=2014072300007148&biz_content={"button":[{"actionParam":"ZFB_HFCZ","actionType":"out","name":"话费充值"},{"name":"查询","subButton":[{"actionParam":"ZFB_YECX","actionType":"out","name":"余额查询"},{"actionParam":"ZFB_LLCX","actionType":"out","name":"流量查询"},{"actionParam":"ZFB_HFCX","actionType":"out","name":"话费查询"}]},{"actionParam":"http://m.alipay.com","actionType":"link","name":"最新优惠"}]}&charset=GBK&method=alipay.mobile.public.menu.add&sign_type=RSA2&timestamp=2014-07-24 03:07:50&version=1.0
    

    3、调用签名函数

    使用各自语言对应的SHA256WithRSA(对应sign_type为RSA2)或SHA1WithRSA(对应sign_type为RSA)签名函数利用商户私钥对待签名字符串进行签名,并进行Base64编码。

    参考文档:https://opendocs.alipay.com/open/291/106118

    2. 支付宝接口

    2.1 支付接口实现

    下载接口文件并将其拷贝到 utils/alipay.py 中,下载地址:https://github.com/liyaopinner/mxshop_sources

    # -*- coding: utf-8 -*-
    
    # pip install pycryptodome
    __author__ = 'bobby'
    
    from datetime import datetime
    from Crypto.PublicKey import RSA
    from Crypto.Signature import PKCS1_v1_5
    from Crypto.Hash import SHA256
    from base64 import b64encode, b64decode
    from urllib.parse import quote_plus
    from urllib.parse import urlparse, parse_qs
    from urllib.request import urlopen
    from base64 import decodebytes, encodebytes
    import json
    
    
    class AliPay(object):
        """
        支付宝支付接口
        """
    
        def __init__(self, app_id, notify_url, app_private_key_path, alipay_public_key_path, return_url, debug=True):
            self.app_id = app_id  # 支付宝分配的应用ID
            self.notify_url = notify_url  # 支付宝服务器主动通知商户服务器里指定的页面http/https路径;用户一旦支付,会向该url发一个异步的请求给自己服务器,这个一定需要公网可访问
            self.app_private_key_path = app_private_key_path  # 个人私钥路径
            self.app_private_key = None  # 个人私钥内容
            self.return_url = return_url  # 网页上支付完成后跳转回自己服务器的url
            with open(self.app_private_key_path) as fp:
                # 读取个人私钥文件提取到私钥内容
                self.app_private_key = RSA.importKey(fp.read())
    
            self.alipay_public_key_path = alipay_public_key_path
            with open(self.alipay_public_key_path) as fp:
                # 读取支付宝公钥文件提取公钥内容,支付宝公钥在代码中验签使用
                self.alipay_public_key = RSA.import_key(fp.read())
    
            if debug is True:
                # 使用沙箱的网关
                self.__gateway = "https://openapi.alipaydev.com/gateway.do"
            else:
                self.__gateway = "https://openapi.alipay.com/gateway.do"
    
        def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs):
            biz_content = {  # 请求参数的集合
                "subject": subject,  # 订单标题
                "out_trade_no": out_trade_no,  # 商户订单号,
                "total_amount": total_amount,  # 订单总金额
                "product_code": "FAST_INSTANT_TRADE_PAY",  # 销售产品码,默认
                # "qr_pay_mode":4
            }
    
            biz_content.update(kwargs)  # 合并其他请求参数字典
            data = self.build_body("alipay.trade.page.pay", biz_content, return_url)  # 将请求参数合并到公共参数字典的键biz_content中
            return self.sign_data(data)
    
        def build_body(self, method, biz_content, return_url=None):
            """
            组合所有的请求参数到一个字典中
            :param method:
            :param biz_content:
            :param return_url:
            :return:
            """
            data = {
                "app_id": self.app_id,
                "method": method,
                "charset": "utf-8",
                "sign_type": "RSA2",
                "timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
                "version": "1.0",
                "biz_content": biz_content
            }
    
            if return_url is None:
                data["notify_url"] = self.notify_url
                data["return_url"] = self.return_url
    
            return data
    
        def ordered_data(self, data):
            """
            并按照第一个字符的键值ASCII码递增排序(字母升序排序),如果遇到相同字符则按照第二个字符的键值ASCII码递增排序,以此类推。
            :param data:
            :return: 返回的是数组列表,按照数据中的k进行排序的
            """
            complex_keys = []
            for key, value in data.items():
                if isinstance(value, dict):
                    complex_keys.append(key)
    
            # 将字典类型的数据dump出来
            for key in complex_keys:
                data[key] = json.dumps(data[key], separators=(',', ':'))
    
            return sorted([(k, v) for k, v in data.items()])
    
        def sign(self, unsigned_string):
            """
            使用各自语言对应的SHA256WithRSA(对应sign_type为RSA2)或SHA1WithRSA(对应sign_type为RSA)签名函数利用商户私钥对待签名字符串进行签名,并进行Base64编码。
            :param unsigned_string:
            :return:
            """
            # 开始计算签名
            key = self.app_private_key
            signer = PKCS1_v1_5.new(key)
            signature = signer.sign(SHA256.new(unsigned_string))
            # base64 编码,转换为unicode表示并移除回车
            sign = encodebytes(signature).decode("utf8").replace("
    ", "")
            return sign
    
        def sign_data(self, data):
            """
            获取所有请求参数,不包括字节类型参数,如文件、字节流,剔除sign字段,剔除值为空的参数。
            进行排序。
            将排序后的参数与其对应值,组合成“参数=参数值”的格式,并且把这些参数用&字符连接起来,此时生成的字符串为待签名字符串。
            然后对该字符串进行签名。
            把生成的签名赋值给sign参数,拼接到请求参数中。
            :param data:
            :return:
            """
            data.pop("sign", None)
            # 排序后的字符串
            ordered_items = self.ordered_data(data)  # 数组列表,进行遍历拼接
            unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in ordered_items)  # 使用参数=值得格式用&连接
    
            sign = self.sign(unsigned_string.encode("utf-8"))  # 得到签名后的字符串
            quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in ordered_items)  # quote_plus给url进行预处理,特殊字符串在url中会有问题
    
            # 获得最终的订单信息字符串
            signed_string = quoted_string + "&sign=" + quote_plus(sign)
            return signed_string
    
        def _verify(self, raw_content, signature):
            # 开始计算签名
            key = self.alipay_public_key
            signer = PKCS1_v1_5.new(key)
            digest = SHA256.new()
            digest.update(raw_content.encode("utf8"))
            if signer.verify(digest, decodebytes(signature.encode("utf8"))):
                return True
            return False
    
        def verify(self, data, signature):
            if "sign_type" in data:
                sign_type = data.pop("sign_type")
            # 排序后的字符串
            unsigned_items = self.ordered_data(data)
            message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)
            return self._verify(message, signature)
    
    if __name__ == "__main__":
        return_url = 'http://47.92.87.172:8000/?total_amount=0.01&timestamp=2017-08-15+17%3A15%3A13&sign=jnnA1dGO2iu2ltMpxrF4MBKE20Akyn%2FLdYrFDkQ6ckY3Qz24P3DTxIvt%2BBTnR6nRk%2BPAiLjdS4sa%2BC9JomsdNGlrc2Flg6v6qtNzTWI%2FEM5WL0Ver9OqIJSTwamxT6dW9uYF5sc2Ivk1fHYvPuMfysd90lOAP%2FdwnCA12VoiHnflsLBAsdhJazbvquFP%2Bs1QWts29C2%2BXEtIlHxNgIgt3gHXpnYgsidHqfUYwZkasiDGAJt0EgkJ17Dzcljhzccb1oYPSbt%2FS5lnf9IMi%2BN0ZYo9%2FDa2HfvR6HG3WW1K%2FlJfdbLMBk4owomyu0sMY1l%2Fj0iTJniW%2BH4ftIfMOtADHA%3D%3D&trade_no=2017081521001004340200204114&sign_type=RSA2&auth_app_id=2016080600180695&charset=utf-8&seller_id=2088102170208070&method=alipay.trade.page.pay.return&app_id=2016080600180695&out_trade_no=201702021222&version=1.0'
    
        alipay = AliPay(
            appid="2016080600180695",
            app_notify_url="http://projectsedus.com/",
            app_private_key_path=u"H:/VueShop/RSA/private_2048.txt",
            alipay_public_key_path="H:/VueShop/RSA/ali_pub.txt",  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            debug=True,  # 默认False,
            return_url="http://47.92.87.172:8000/"
        )
    
        o = urlparse(return_url)
        query = parse_qs(o.query)
        processed_query = {}
        ali_sign = query.pop("sign")[0]
        for key, value in query.items():
            processed_query[key] = value[0]
        print (alipay.verify(processed_query, ali_sign))
    
        url = alipay.direct_pay(
            subject="测试订单",
            out_trade_no="201702021222",
            total_amount=0.01
        )
        re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
        print(re_url)
    

    该文件可以实现支付宝请求签名,生成支付宝订单支付页面,依赖于 RSA 签名,需要安装:pip install pycryptodome

    测试用例中将 APPID 和沙箱公钥、私钥、支付宝公钥修改为自己本项目的:

    # 测试用例
    alipay = AliPay(
        # 沙箱里面的appid值
        appid="2016102900dd898",
        # notify_url是异步的url
        app_notify_url="http://192.168.131.131:8000/",
        # 我们自己商户的密钥
        app_private_key_path="../trade/keys/private_2048.txt",
        # 支付宝的公钥
        alipay_public_key_path="../trade/keys/alipay_key_2048.txt",  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
        # debug为true时使用沙箱的url。如果不是用正式环境的url
        debug=True,  # 默认False,
        return_url="http://192.168.131.131:8000/"
    )
    

    运行 alipay.py,生成一个订单支付页面,点击浏览器运行,然后输入 沙箱买家账户和支付宝密码进行支付(蚂蚁金服---沙箱账号 https://openhome.alipay.com/platform/appDaily.htm?tab=account)

    支付完成后若一直停留在支付成功页面,可配置 return_url

    2.2 return_url 和 notify_url 分析

    在支付接口两个比较重要参数是:

    • return_url:支付成功后跳转的页面
    • notify_url:关闭支付页面,获取支付状态

    支付成功后,支付宝会自动跳转到 return_url 配置的地址,我们可以获取 URL 中的参数,来验证是否翼支付,若已支付则修改订单状态。

    若用户提交了订单,未进行支付而是关闭了支付页面,这时应用就无法判断订单状态,就需要用到 notify_url。支付宝会通过异步方式向该 URL 发起一个请求 (post),并传递一些参数,通过获取这些参数来对订单进行修改。

    支付成功后跳转的 URLhttp://47.92.87.172:8000/?total_amount=0.01&timestamp=2017-08-15+17%3A15%3A13&sign=jnnA1dGO2iu2ltMpxrF4MBKE20Akyn%2FLdYrFDkQ6ckY3Qz24P3DTxIvt%2BBTnR6nRk%2BPAiLjdS4sa%2BC9JomsdNGlrc2Flg6v6qtNzTWI%2FEM5WL0Ver9OqIJSTwamxT6dW9uYF5sc2Ivk1fHYvPuMfysd90lOAP%2FdwnCA12VoiHnflsLBAsdhJazbvquFP%2Bs1QWts29C2%2BXEtIlHxNgIgt3gHXpnYgsidHqfUYwZkasiDGAJt0EgkJ17Dzcljhzccb1oYPSbt%2FS5lnf9IMi%2BN0ZYo9%2FDa2HfvR6HG3WW1K%2FlJfdbLMBk4owomyu0sMY1l%2Fj0iTJniW%2BH4ftIfMOtADHA%3D%3D&trade_no=2017081521001004340200204114&sign_type=RSA2&auth_app_id=2016080600180695&charset=utf-8&seller_id=2088102170208070&method=alipay.trade.page.pay.return&app_id=2016080600180695&out_trade_no=201702021222&version=1.0

    可以提取其中的参数对其进行验证,若返回 True 则表示支付成功,否则表示未支付:

    return_url = 'http://47.92.87.172:8000/?total_amount=0.01&timestamp=2017-08-15+17%3A15%3A13&sign=jnnA1dGO2iu2ltMpxrF4MBKE20Akyn%2FLdYrFDkQ6ckY3Qz24P3DTxIvt%2BBTnR6nRk%2BPAiLjdS4sa%2BC9JomsdNGlrc2Flg6v6qtNzTWI%2FEM5WL0Ver9OqIJSTwamxT6dW9uYF5sc2Ivk1fHYvPuMfysd90lOAP%2FdwnCA12VoiHnflsLBAsdhJazbvquFP%2Bs1QWts29C2%2BXEtIlHxNgIgt3gHXpnYgsidHqfUYwZkasiDGAJt0EgkJ17Dzcljhzccb1oYPSbt%2FS5lnf9IMi%2BN0ZYo9%2FDa2HfvR6HG3WW1K%2FlJfdbLMBk4owomyu0sMY1l%2Fj0iTJniW%2BH4ftIfMOtADHA%3D%3D&trade_no=2017081521001004340200204114&sign_type=RSA2&auth_app_id=2016080600180695&charset=utf-8&seller_id=2088102170208070&method=alipay.trade.page.pay.return&app_id=2016080600180695&out_trade_no=201702021222&version=1.0'
    
    alipay = AliPay(
        appid="2016102900776898",
        app_notify_url="http://projectsedus.com/",
        app_private_key_path="../trade/keys/private_2048.txt",
        alipay_public_key_path="../trade/keys/alipay_key_2048.txt",  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
        debug=True,  # 默认False,
        return_url="http://192.168.131.131:8000/"
    )
    
    # return_url 解析
    o = urlparse(return_url)
    query = parse_qs(o.query)
    processed_query = {}
    ali_sign = query.pop("sign")[0]
    for key, value in query.items():
        processed_query[key] = value[0]
    
    print(processed_query)
    print(ali_sign)
    print(alipay.verify(processed_query, ali_sign))
    
    # 创建订单
    url = alipay.direct_pay(
        subject="测试订单",
        out_trade_no="201702021222",
        total_amount=0.01,
        return_url="http://192.168.131.131:8000/"
    )
    re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
    print(re_url)
    

    运行结果:

    {'total_amount': '0.01', 'timestamp': '2017-08-15 17:15:13', 'trade_no': '2017081521001004340200204114', 'sign_type': 'RSA2', 'auth_app_id': '2016080600180695', 'charset': 'utf-8', 'seller_id': '2088102170208070', 'method': 'alipay.trade.page.pay.return', 'app_id': '2016080600180695', 'out_trade_no': '201702021222', 'version': '1.0'}
    
    jnnA1dGO2iu2ltMpxrF4MBKE20Akyn/LdYrFDkQ6ckY3Qz24P3DTxIvt+BTnR6nRk+PAiLjdS4sa+C9JomsdNGlrc2Flg6v6qtNzTWI/EM5WL0Ver9OqIJSTwamxT6dW9uYF5sc2Ivk1fHYvPuMfysd90lOAP/dwnCA12VoiHnflsLBAsdhJazbvquFP+s1QWts29C2+XEtIlHxNgIgt3gHXpnYgsidHqfUYwZkasiDGAJt0EgkJ17Dzcljhzccb1oYPSbt/S5lnf9IMi+N0ZYo9/Da2HfvR6HG3WW1K/lJfdbLMBk4owomyu0sMY1l/j0iTJniW+H4ftIfMOtADHA==
    
    False
    
    https://openapi.alipaydev.com/gateway.do?app_id=2016102900776898&biz_content=%7B%22subject%22%3A%22%5Cu6d4b%5Cu8bd5%5Cu8ba2%5Cu5355%22%2C%22out_trade_no%22%3A%22201702021222%22%2C%22total_amount%22%3A0.01%2C%22product_code%22%3A%22FAST_INSTANT_TRADE_PAY%22%7D&charset=utf-8&method=alipay.trade.page.pay&notify_url=http%3A%2F%2Fprojectsedus.com%2F&return_url=http%3A%2F%2F192.168.131.131%3A8000%2F&sign_type=RSA2&timestamp=2020-07-08+11%3A28%3A21&version=1.0&sign=aSXzDVyiKVVG3j7w0a4lmmmKgU5IluSPNK9Nq4e83xwJDsDOvXKrozmsLSnz6BjFXxQrsTeNPKPVOMJCvAtWA9z4acAYC%2FrN4rXcKGKRwqd18sXhiRAKMsJPJHoCyvTwxhQ%2Fn7h%2F0B0eC5iU4z0gkbpW%2FtynnReiY%2Fo1T1FKG8%2F%2Bd7mS3Fcc1isLS5hHamVGCMuQyJTbmTtywDghwCBgX%2BnujSD%2BI1vTyeneTAZXhqUZkuamoUURCmrRIHUqkpPQTJ95GtJ2SeAg7XDlDzJd9hHwWRSTlBvZTBjlTOvcZ51WXelnwwb3u3YZ69xBo0fyuEZAUJbvCt54P8nGZAOhdA%3D%3D
    

    3. Django 集成支付功能

    需求:

    • 用户提交订单,自动跳转到支付页面:即在订单接口中需要生成支付 URL,并且前端实现自动跳转
    • Django 后端处理 return_url、notify_url

    return_url 为同步 get 请求,notify_url 为异步 post 请求,且都与支付相关,因此可以放在同一视图中。

    1、trade/views.py

    from rest_framework.views import APIView
    
    
    class AliPayView(APIView):
        def get(self, request):
            """
            处理支付宝return_url返回
            :param request:
            :return:
            """
            pass
    
        def post(self, request):
            """
            处理支付宝notify_url异步通知
            :param request:
            :return:
            """
            pass
    

    2、MxShop/urls.py

    # 支付宝支付相关
    path('alipay/return/', AlipayView.as_view(), name='alipay'),
    

    3、将 return_url、notify_url 都修改为 http://192.168.131.131:8000/alipay/return/

    alipay = AliPay(
            appid="2016102900776898",
            app_notify_url="http://192.168.131.131:8000/alipay/return/",
            app_private_key_path="../trade/keys/private_2048.txt",
            alipay_public_key_path="../trade/keys/alipay_key_2048.txt",  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            debug=True,  # 默认False,
            return_url="http://192.168.131.131:8000/alipay/return/"
        )
    
        # 创建订单
        url = alipay.direct_pay(
            subject="测试订单",
            out_trade_no="201702021229",
            total_amount=0.01,
            return_url="http://192.168.131.131:8000/alipay/return/"
        )
    

    运行 alipay.py 生成支付连接,然后使用 Pycharm 进行调试(主要调试 post 请求),支付成功后,查看是否有请求进入 AlipayView()


    notify_url 逻辑

    notify_url 用于验证处理订单状态,修改 trade/views.py

    from rest_framework.views import APIView
    from rest_framework.response import Response
    from utils.alipay import AliPay, get_server_ip
    from DjangoOnlineFreshSupermarket.settings import app_id, alipay_debug,  alipay_public_key_path, app_private_key_path
    from django.utils import timezone
    
    
    class AliPayView(APIView):
        def get(self, request):
            """
            处理支付宝return_url返回
            :param request:
            :return:
            """
            pass
    
        def post(self, request):
            """
            处理支付宝notify_url异步通知
            :param request:
            :return:
            """
            processed_dict = {}  # 存放 post 中所有数据
            # 取出 post 中所有数据
            for key, value in request.POST.items():
                processed_dict[key] = value
            # 去掉 sign
            sign = processed_dict.pop('sign', None)
    
            # 生成 Alipay 对象
            alipay = AliPay(
                appid=settings.appid,
                app_notify_url=settings.app_notify_url,
                app_private_key_path=settings.private_key_path,
                alipay_public_key_path=settings.ali_pay_key_path,
                debug=True,
                return_url=settings.return_url
            )
    
            print('alipay', alipay)
            print(processed_dict)
    
            # 进行验证
            verify_re = alipay.verify(processed_dict, sign)
            print(verify_re)
    
            # 验证成功
            if verify_re is True:
                order_sn = processed_dict.get('out_trade_no', None)  # 商户网站唯一订单号
                trade_no = processed_dict.get('trade_no', None)  # 支付宝系统交易流水号
                trade_status = processed_dict.get('trade_status', None)
    
                # 查询数据库中订单记录
                existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
                print('existed_orders', existed_orders)
                for existed_order in existed_orders:
                    order_goods = existed_order.goods.all()  # 订单商品项
                    for order_good in order_goods:
                        goods = order_good.goods
                        goods.sold_num += order_good.goods_num
                        goods.save()
    
                    # 更新订单状态
                    existed_order.pay_status = trade_status
                    existed_order.trade_no = trade_no
                    existed_order.save()
    
                # 返回一个 success 给支付宝,若不返回支付宝会一直发送订单支付成功的消息
                return Response("success")
    

    验证成功后,给支付宝返回一个 success,否则支付宝会重复发送请求。


    return_url 逻辑

    class AlipayView(APIView):
        """支付相关"""
    
        def get(self, request):
            """
            处理支付宝的 return_url 返回
            支付成功后要跳转的页面
            :param request:
            :return:
            """
            processed_dict = {}  # 存放 post 中所有数据
            # 取出 post 中所有数据
            for key, value in request.GET.items():
                processed_dict[key] = value
            # 去掉 sign
            sign = processed_dict.pop('sign', None)
            print('-------------------', processed_dict)
    
            # 生成 Alipay 对象
            alipay = AliPay(
                appid=settings.appid,
                app_notify_url=settings.app_notify_url,
                app_private_key_path=settings.private_key_path,
                alipay_public_key_path=settings.ali_pay_key_path,
                debug=True,
                return_url=settings.return_url
            )
    
            # 进行验证
            verify_re = alipay.verify(processed_dict, sign)
    
            # 验证成功
            if verify_re is True:
                order_sn = processed_dict.get('out_trade_no', None)  # 商户网站唯一订单号
                trade_no = processed_dict.get('trade_no', None)  # 支付宝系统交易流水号
                trade_status = processed_dict.get('trade_status', None)
    
                # 查询数据库中订单记录
                existed_orders = OrderInfo.objects.filter(order_sn=order_sn)
                for existed_order in existed_orders:
                    # 更新订单状态
                    existed_order.pay_status = trade_status
                    existed_order.trade_no = trade_no
                    existed_order.save()
    
                response = redirect('index')
                response.set_cookie('nextPath', 'pay', max_age=2)
                return response
            else:
                response = redirect('index')
                return response
    
            #     response = redirect('/index/#/app/home/member/order')
            #     return response
            # else:
            #     response = redirect('index')
            #     return response
    
        def post(self, request):
            """
            处理支付宝的 notify_url
            支付宝服务器主动通知商户服务器里指定的页面http/https路径
            :param request:
            :return:
            """
    

    settings 配置

    return_url、notify_url、APPID 等放在 settings 中统一配置,方便后续修改:

    # 支付宝相关
    private_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/private_2048.txt')
    ali_pay_key_path = os.path.join(BASE_DIR, 'apps/trade/keys/alipay_key_2048.txt')
    app_notify_url = "http://192.168.131.131:8000/alipay/retutn/"
    return_url = "http://192.168.131.131:8000/alipay/retutn/"
    appid = "2016102900xx98"
    

    参考文档:支付结果异步通知

    4. Django 和 vue 联调

    4.1 订单测试

    修改前端代码,将请求地址由 127.0.0.1 改为服务器IP 192.168.131.131

    //  src/api/api.js
    
    //let local_host = 'http://127.0.0.1:8000';
    let local_host = 'http://192.168.131.131:8000';
    

    1、trade/serializes.py 中添加 alipay 字段:

    class OrderDetailSerializer(serializers.ModelSerializer):
        """
        订单中商品详细信息
        """
        # goods字段需要嵌套一个OrderGoodsSerializer
        goods = OrderGoodsSerializer(many=True)
        add_time = serializers.DateTimeField(read_only=True, format="%Y-%m-%d %H:%M")
    
        # 支付订单的url
        alipay_url = serializers.SerializerMethodField(read_only=True)
    
        def get_alipay_url(self, obj):
            alipay = AliPay(
                appid=settings.appid,
                app_notify_url=settings.app_notify_url,
                app_private_key_path=settings.private_key_path,
                alipay_public_key_path=settings.ali_pay_key_path,
                debug=True,
                return_url=settings.return_url
            )
    
            url = alipay.direct_pay(
                subject=obj.order_sn,
                out_trade_no=obj.order_sn,
                total_amount=obj.order_mount,
            )
            re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
    
            return re_url
    
        class Meta:
            model = OrderInfo
            fields = '__all__'
    
    
    class OrderSerializer(serializers.ModelSerializer):
        """订单"""
        user = serializers.HiddenField(
            default=serializers.CurrentUserDefault()
        )
    
        # 生成订单时,不需 post以下数据
        pay_status = serializers.CharField(read_only=True)
        trade_no = serializers.CharField(read_only=True)
        order_sn = serializers.CharField(read_only=True)
        pay_time = serializers.DateTimeField(read_only=True)
        nonce_str = serializers.CharField(read_only=True)
        pay_type = serializers.CharField(read_only=True)
    
        add_time = serializers.DateTimeField(read_only=True, format="%Y-%m-%d %H:%M")
    
        # 支付订单的url
        alipay_url = serializers.SerializerMethodField(read_only=True)
    
        def get_alipay_url(self, obj):
            alipay = AliPay(
                appid=settings.appid,
                app_notify_url=settings.app_notify_url,
                app_private_key_path=settings.private_key_path,
                alipay_public_key_path=settings.ali_pay_key_path,
                debug=True,
                return_url=settings.return_url
            )
    
            url = alipay.direct_pay(
                subject=obj.order_sn,
                out_trade_no=obj.order_sn,
                total_amount=obj.order_mount,
            )
            re_url = "https://openapi.alipaydev.com/gateway.do?{data}".format(data=url)
    
            return re_url
    
        def generate_order_sn(self):
            """
            生成订单号:当前时间+userid+随机数
            :return:
            """
            from random import Random
            random_str = Random()
            order_sn = "{time_str}{userid}{ranstr}".format(time_str=time.strftime("%Y%m%d%H%M%S"),
                                                           userid=self.context['request'].user.id,
                                                           ranstr=random_str.randint(10, 99))
            return order_sn
    
        def validate(self, attrs):
            """validate 中添加 order_sn,然后再 view 中就可以 save"""
            attrs['order_sn'] = self.generate_order_sn()
            return attrs
    
        class Meta:
            model = OrderInfo
            fields = '__all__'
    

    2、Vue 购物提交订单结算 src/views/cart/cart.vue

    balanceCount () { // 结算
        if(this.addrInfo.length==0){
            alert("请选择收货地址")
        }else{
        createOrder(
            {
            post_script:this.post_script,
            address:this.address,
            signer_name:this.signer_name,
            singer_mobile:this.signer_mobile,
            order_mount:this.totalPrice
            }
        ).then((response)=> {
            alert('订单创建成功')
            window.location.href=response.data.alipay_url;
        }).catch(function (error) {
            console.log(error);
        });
        }
    },
    

    3、前端页面选择一个商品加入购物车,留言提交订单,访问:http://192.168.131.131:8000/orders/ 查看已提交的订单:

    4.2 支付成功跳转

    用户支付成功后,并不能跳转到 Vue 页面,而是返回后端接口地址,如果想跳转到 Vue 页面,有两种方法:

    • Vue 中显示支付宝返回的二维码图片(蚂蚁金服文档有方法介绍如何生成图片),支付成功后,Vue 跳转到其他页面(需要额外新增一个页面,再将图片嵌入进去)
    • 将由 node.js 代理渲染的 Vue 页面,由 Django 代码渲染,(需要将前端文件打包)

    本项目采用第二种方式,切换到 vue 项目根目录,执行 npm run build 命令进行打包;生成 dist 目录,里面有:static/、index.entry.js、index.html

    1、MxShop 新建 static,将 static/、index.entry.js 拷贝到其中。

    2、再将 index.html 拷贝到 templates/ 中。

    3、配置路由 MxShop/urls.py

    from django.views.generic import TemplateView
    
     # 首页
    path('index/', TemplateView.as_view(template_name='index.html'), name='index'),
    

    并修改 index.html scrip 标签的路径:

    <!DOCTYPE html>
    {% load static %}
    <html>
      <head>
        <meta charset="utf-8">
        <title>首页</title>
      </head>
      <body>
        <div id="app"></div>
      <script src="{% static 'index.entry.js' %}"></script></body>
    </html>
    

    3、配置静态文件路径 settings

    STATICFILES_DIRS = (
        os.path.join(BASE_DIR, 'static'),
    )
    

    4、支付成功后,可以逻辑进入到 AliPayView() get 中,在这里可以控制要跳转到地方 trade/views.py

    from django.shortcuts import redirect, reverse
    
    
    class AliPayView(APIView):
        def get(self, request):
            """
            处理支付宝return_url返回
            :param request:
            :return:
            """
            ....
            verify_result = alipay.verify(processed_dict, sign)  # 验证签名,如果成功返回True
    
            if verify_result:
                # POST中已经修改数据库订单状态,无需再GET中修改,且,GET中也得不到支付状态值
    
                # 给支付宝返回一个消息,证明已收到异步通知
                # return Response('success')
                # 修改为跳转到Vue页面
                response = redirect(reverse('index'))
                response.set_cookie('nextPath', 'pay', max_age=2)  # max_age设置为2s,让其快速过期,用一次就好了。
                # 跳转回Vue中时,直接跳转到Vue的pay的页面,后台无法配置,只能让Vue实现跳转。
                return response
            else:
                # 验证不通过直接跳转回首页就行,不设置cookie
                return redirect(reverse('index'))
    

    5、Vue 前端从 cookie 中获取 nextPath 进行分析,来判断是否需要跳转 src/router/index.js

    //进行路由判断
    router.beforeEach((to, from, next) => {
      var nextPath = cookie.getCookie('nextPath')
      console.log(nextPath)
      if(nextPath=="pay"){
        next({
          path: '/app/home/member/order',
        });
      }else{
        if(to!=undefined){
          if(to.meta.need_log){
            console.log(to.meta.need_log)
            if(!store.state.userInfo.token){
              next({
                path: '/app/login',
              });
            }else {
              next();
            }
          }else {
            if (to.path === '/') {
              next({
                path: '/app/home/index',
              });
            }else {
              next();
            }
          }
        }else {
          if (to.path === '/') {
            next({
              path: '/app/home/index',
            });
          }else {
            next();
          }
        }
      }
    

    现在可以通过 http://192.168.131.131:8000/index/#/app/home/index 进行访问项目,而不需要 node 启动前端项目。

    参考文档:alipay.py

  • 相关阅读:
    Python数据分析与机器学习-Matplot_2
    Python数据分析与机器学习-Matplot_1
    1008. 数组元素循环右移问题 (20)
    Latex小技巧
    执行PowerShell脚本的时候出现"在此系 统上禁止运行脚本"错误
    Linux使用MentoHust联网线上校园网, 回到普通有线网络却连不上?
    Re:uxul
    Linux下nautilus的右键快捷菜单项设置
    从入门到入狱——搭讪技巧
    Latex命令
  • 原文地址:https://www.cnblogs.com/midworld/p/13629749.html
Copyright © 2020-2023  润新知