【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("\n", "")
        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

posted @ 2020-09-07 22:51  Hubery_Jun  阅读(590)  评论(0编辑  收藏  举报