5.学城项目 支付宝支付
1. 支付宝API文档
* 1. 官方文档地址: https://opendocs.alipay.com/common/02fwvj/* 2. 支付宝API:六大接口 地址: https://docs.open.alipay.com/270/105900/* 3. 支付宝工作流程 地址: https://docs.open.alipay.com/270/105898/* 4. 支付宝8次异步通知机制(支付宝对我们服务器发送POST请求, 索要 success 7个字符)地址: https://docs.open.alipay.com/270/105902/
2. 沙箱环境下测试支付
2.1 测试步骤
沙箱环境是协助开发者进行接口开发及主要功能联调的模拟环境.
* 1. 电脑网站支付API: https://docs.open.alipay.com/270/105900/* 2. 在沙箱环境下实名认证地址: https://openhome.alipay.com/platform/appDaily.htm?tab=info* 3. 完成RSA密钥生成:https://docs.open.alipay.com/291/105971* 4. 在开发中心的沙箱应用下设置应用公钥:填入生成的公钥文件中的内容* 5. Python支付宝开源第三方框架:https://github.com/fzlee/alipay* 6. pip install python-alipay-sdk --upgrade* 7. 代码的密钥配置格式alipay_public_key.pem = """-----BEGIN PUBLIC KEY-----支付宝公钥-----END PUBLIC KEY-----"""app_private_key.pem = """-----BEGIN RSA PRIVATE KEY-----用户私钥-----END RSA PRIVATE KEY-----"""* 8. 支付宝网关真实:https://openapi.alipay.com/gateway.do沙箱:https://openapi.alipaydev.com/gateway.do # 带dev
2.2 沙箱环境配置
* 1. 认证
https://developers.alipay.com/dev/workspace/register?from=http%3A%2F%2Fopenhome.alipay.com%2Fplatform%2FappDaily.html
* 2. 配置沙箱应用1. 选择网页&移动应用2. 接口加签方式选择自定义密钥3. 公钥模式->设置并查看
* 3. 下载&安装 支付宝密钥生成器下载地址: https://gw.alipayobjects.com/os/bmw-prod/02b946e1-9faf-4394-8004-d241443c874e.exe
* 4. 生成密钥
* 5. 复制公钥
* 6. 将应用公钥填写到到沙箱中
* 7. 生成支付宝公钥
* 8. 沙箱账户
* 9. 下载支付宝客户端沙箱版本
* 10. 手机端安装支付宝沙箱版买家账号 aijatb2766@sandbox.com登录密码 111111支付密码 111111
2.3 测试代码
* 1. 安装python-alipay-sdk模块pip install python-alipay-sdk如果抛ssl相关错误, 代表缺失pyopenssl模块pip install pyopenssl
* 2. 测试代码 (设置密钥的时候不要有多余的空格)
from alipay import AliPay# 应用程序私钥
app_private_key_string = """-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAhe3/9BI+qAaoRprc1f3liRA9v9WYL/n9Xp9Cc+PPcbkfrI/pN7q/GOaaYf521wbCEXmZYW4qQVX4mP4i2S05FwRu2B1/7Ux4oREyM+0vazHrBCNyE7erqGV2+8GsLDJRnyQXApbN79FR40+chrlc5Dt29mGCtv1DKz1l/mnNJKj0srP55joxXfLmDwfeLcNcTEblJsxpgi1TKlnEG0b/0RtkFhGGOsUrClNiLxBK5iZJBUt+QX2P97MDHj8BIqLqKITBe+aSbwHcDzl587S9cq1142y41NVhEjlC20+2iHsESA82HQo8iH7i1A6BzHvRlvk7HoUqeYb8p91ZLI0d7QIDAQABAoIBAAuppQ9RA2nIYqD7XV25JWLhKi9pXz5WS60Qu02yOd9SWqLBSXLT7U4yzqDX8utYqE+zQhsM59sWrHZOMySsXntVpH1nXDuC3EJSaAfDkMyJ5UhP+eAjr2wTod/chqy2mQr9ro9IKJjIppPf2+aTf7ZUQ1DDPwnGVjIOv7H+7qFRf/XBUVyyW1j4uwFlN/ZporDa7B6lwqWgH9+Yvo7w3/Pmjo78z2BKZzPQJa2sq3WxolcphDImflycHzno8jIn+QnB3XmlCu7yxsx4HYXMQMKY8i3gzPejdOdWsjbAuxI3mfyyELN2W8ZCpaZKWfKEhpxR4DA56byZtESWN+s1HmECgYEA7WFD1vhO2gwWyiFBzyu9yHOTpmcH6mBJ30Km7mErPHD3yHOiaCv59Ba4XTD629073m3Q2z6EXpmv7Fwz+uezac12BCBeg7AU9WfBJPiSHBRvwR4qXbZzbU91tElWDFYlwCmjd3Sp5oTGWSpWU/BW3hkxx6KYIwnYUIDIhTChCQUCgYEAkG9j4pp/XisOCUM3cQmTaUzDWZfOILDQUjrWCnLEHfPJPrX5qjtkSkZ8Z6Q4GjGNVg5m4i98VRGkt3rY0wsnsuCDFrws7TzlvDVP9fo0DgTalO4JCQ4gE9fGZypbjRYtbHobTEsGnGmeE3eRrlY5cfNfcwfrr1vKceo7slJzNckCgYAd+8kr4BVlqV0/js/XMTk5lo+x1xXC3wK1tp+LQK7LZaGGqkR7UAK0eCI1czhciSdEwy48YzspD9SO0F6odJfO52revo/xpk4faUmWN+eMsHAlPoAvchpGVmERsqmxyTffe+Lv9cZ4HZFINfbNh3ARgbEt/DWnR1kRYhLx7+CHWQKBgQCNWGTwkm9IsWu4Bs6P0WYwK04VNGklNsN3ZVqnuO5RvYxY0W71d8/KnDYMmvnIMGv3JnrqqLvM6EpAwHjF92mvNOU0b4yr0eelCqsoteURPxDFpDi1YtxjbssblKkpZeWn/csPG3DpyrZGqMGpUXpAGIJ1KPAtmO+CEU7AUM2seQKBgQDmPWXLUXx6FKRVUYLgALEY+PkIc3feRw1gdhSa7Q8/sQw8jugD92YnZ9gdBkZxIlPHqiMpvba+adbFTcTIPx6Myoy8X8mfWFTHU0su+tiwiUXGWkxAvYU/8ebSABs6HRgFGkbJEDU7TqPOvDCLFjAFdTQJ/LKGwbI+E7XfR0f4Sw==
-----END RSA PRIVATE KEY-----"""# 支付宝公钥
alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfLBZtfSKJ3mwThLuRnLO1eEh5jf2fXDlC06VLH/lmFFJrZBfkJqzE9T41k5JoGpluI6r03vo6Vx29UYOP0bf531UUr9Bpg2TLBwmUVQYoY15plsUjKkqTqDYq55m3cYJpn0TBROMyurbGs4i0TcNq35zjC2wbl0DWmxmOhWyg3NhUu9uqWUJGbfxTuowKQxaK/3XDoOLx+mQCytz0JvNevUhBhOu6eZLCh3hNOswu8gMf3a5kCReya24uXOd26SKd5HYlwFHk4ndSSjwIYulQs9hnlSbkJn+zykjc6qdDQLMWuqkSKlb5Snu7TCh5FNUO4KsFqk2bEXgysJy23lhwIDAQAB
-----END PUBLIC KEY-----"""
alipay = AliPay(# 应用idappid="2021000120612450",# 默认回调app_notify_url=None,# 应用私钥字符串app_private_key_string=app_private_key_string,# 支付宝公钥字符串alipay_public_key_string=alipay_public_key_string,# 加密方式sign_type="RSA2", # RSA or RSA2# 开启调试(上线关闭)debug=True
)# 网关地址(上线之后需要修改)
alipay_url = 'https://openapi.alipaydev.com/gateway.do?'
# 生成对象(交易页面)
order_string = alipay.api_alipay_trade_page_pay(# 订单号唯一标识out_trade_no="202206041111",# 价格total_amount=9.9,# 商品名称subject='<<肾宝>>男人要肾好,就要喝肾宝。喝了之后,比刘翔快,比姚明高。一瓶提神醒脑,两瓶永不疲劳,三瓶长生不老。噢耶...',# 支付成功 get同步回调, 前台展示的页面, # return_url="",# 支付成功 post异步回调, 向后端发送请求 修改订单状态(以支付)# notify_url=""
)# 生成支付链接 (网关 + 交易页面)
print(alipay_url + order_string)
* 3. 运行程序, 得到一个商品支付地址:
https://openapi.alipaydev.com/gateway.do?app_id=2021000120612450&biz_content=%7B%22subject%22%3A%22%5Cu7537%5Cu4eba%5Cu8981%5Cu80be%5Cu597d%5Cuff0c%5Cu5c31%5Cu8981%5Cu559d%5Cu80be%5Cu5b9d%5Cu3002%5Cu559d%5Cu4e86%5Cu4e4b%5Cu540e%5Cuff0c%5Cu6bd4%5Cu5218%5Cu7fd4%5Cu5feb%5Cuff0c%5Cu6bd4%5Cu59da%5Cu660e%5Cu9ad8%5Cu3002%5Cu4e00%5Cu74f6%5Cu63d0%5Cu795e%5Cu9192%5Cu8111%5Cuff0c%5Cu4e24%5Cu74f6%5Cu6c38%5Cu4e0d%5Cu75b2%5Cu52b3%5Cuff0c%5Cu4e09%5Cu74f6%5Cu957f%5Cu751f%5Cu4e0d%5Cu8001%5Cu3002%5Cu5662%5Cu8036...%22%2C%22out_trade_no%22%3A%22%5Cu80be%5Cu5b9d%22%2C%22total_amount%22%3A9.9%2C%22product_code%22%3A%22FAST_INSTANT_TRADE_PAY%22%7D&charset=utf-8&method=alipay.trade.page.pay¬ify_url=https%3A%2F%2Fwww.luffycity.com%2Ffree-course&return_url=https%3A%2F%2Fwww.luffycity.com%2Ffree-course&sign_type=RSA2×tamp=2022-06-03+23%3A26%3A25&version=1.0&sign=BttMCJX7M6PGFNq1ldnaxR0HM1zLcVRbCeC7xmM3NDceOvs2U8tnRHmZb1gKlbozH2T8arQfPHrtJQ4dkjpptQHJMGWD6AU0cQzSi%2F4dI3m5GKEK4Y96w9OC1UNTjp80TXwZ1OlPCTzpHB4SAkjHeRSbxPXGJpvsvt7TjKOhEIbZ02xNAfawLa%2F2ptv6FXtplSb1ckmo1aV2k%2Fa8rQxRUVA52eeM9XNoH7GpoUiDAi47NZalgm1bcsrvrHfDizR83AU39hddA8EkqxFFAlFAOpVCRH6ZWSFgxJfTap4b6KkViYkaee74eELMgvG9qcuV9b04DB3JdFfbctl1VXyqrQ%3D%3D
* 4. 使用浏览器打开地址(如果浏览器打开的网站太多, 可能会提示钓鱼网站之类的提示.)
* 5. 使用支付宝沙箱版本支付
* 6. 沙箱账户中查看余额
3. 二次封装支付宝
在项目lib目录下重新封装支付宝
配置可以写在iPay下的settings.py文件或项目的dev.py配置文件中.
lib├── iPay # aliapy二次封装包│ ├── __init__.py # 包文件│ ├── pem # 公钥私钥文件夹│ │ ├── alipay_public_key.pem # 支付宝公钥文件│ │ ├── app_private_key.pem # 应用私钥文件│ ├── pay.py # 支付文件└── └── settings.py | dev.py # 应用配置
* 1. 在项目lib目录下新建iPay目录, 再按照目录结构创建其他文件和目录.
* 2. 在app_private_key.pem中写入支付宝公钥
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAhe3/9BI+qAaoRprc1f3liRA9v9WYL/n9Xp9Cc+PPcbkfrI/pN7q/GOaaYf521wbCEXmZYW4qQVX4mP4i2S05FwRu2B1/7Ux4oREyM+0vazHrBCNyE7erqGV2+8GsLDJRnyQXApbN79FR40+chrlc5Dt29mGCtv1DKz1l/mnNJKj0srP55joxXfLmDwfeLcNcTEblJsxpgi1TKlnEG0b/0RtkFhGGOsUrClNiLxBK5iZJBUt+QX2P97MDHj8BIqLqKITBe+aSbwHcDzl587S9cq1142y41NVhEjlC20+2iHsESA82HQo8iH7i1A6BzHvRlvk7HoUqeYb8p91ZLI0d7QIDAQABAoIBAAuppQ9RA2nIYqD7XV25JWLhKi9pXz5WS60Qu02yOd9SWqLBSXLT7U4yzqDX8utYqE+zQhsM59sWrHZOMySsXntVpH1nXDuC3EJSaAfDkMyJ5UhP+eAjr2wTod/chqy2mQr9ro9IKJjIppPf2+aTf7ZUQ1DDPwnGVjIOv7H+7qFRf/XBUVyyW1j4uwFlN/ZporDa7B6lwqWgH9+Yvo7w3/Pmjo78z2BKZzPQJa2sq3WxolcphDImflycHzno8jIn+QnB3XmlCu7yxsx4HYXMQMKY8i3gzPejdOdWsjbAuxI3mfyyELN2W8ZCpaZKWfKEhpxR4DA56byZtESWN+s1HmECgYEA7WFD1vhO2gwWyiFBzyu9yHOTpmcH6mBJ30Km7mErPHD3yHOiaCv59Ba4XTD629073m3Q2z6EXpmv7Fwz+uezac12BCBeg7AU9WfBJPiSHBRvwR4qXbZzbU91tElWDFYlwCmjd3Sp5oTGWSpWU/BW3hkxx6KYIwnYUIDIhTChCQUCgYEAkG9j4pp/XisOCUM3cQmTaUzDWZfOILDQUjrWCnLEHfPJPrX5qjtkSkZ8Z6Q4GjGNVg5m4i98VRGkt3rY0wsnsuCDFrws7TzlvDVP9fo0DgTalO4JCQ4gE9fGZypbjRYtbHobTEsGnGmeE3eRrlY5cfNfcwfrr1vKceo7slJzNckCgYAd+8kr4BVlqV0/js/XMTk5lo+x1xXC3wK1tp+LQK7LZaGGqkR7UAK0eCI1czhciSdEwy48YzspD9SO0F6odJfO52revo/xpk4faUmWN+eMsHAlPoAvchpGVmERsqmxyTffe+Lv9cZ4HZFINfbNh3ARgbEt/DWnR1kRYhLx7+CHWQKBgQCNWGTwkm9IsWu4Bs6P0WYwK04VNGklNsN3ZVqnuO5RvYxY0W71d8/KnDYMmvnIMGv3JnrqqLvM6EpAwHjF92mvNOU0b4yr0eelCqsoteURPxDFpDi1YtxjbssblKkpZeWn/csPG3DpyrZGqMGpUXpAGIJ1KPAtmO+CEU7AUM2seQKBgQDmPWXLUXx6FKRVUYLgALEY+PkIc3feRw1gdhSa7Q8/sQw8jugD92YnZ9gdBkZxIlPHqiMpvba+adbFTcTIPx6Myoy8X8mfWFTHU0su+tiwiUXGWkxAvYU/8ebSABs6HRgFGkbJEDU7TqPOvDCLFjAFdTQJ/LKGwbI+E7XfR0f4Sw==
-----END RSA PRIVATE KEY-----
* 3. 在alipay_public_key.pem文件中写入应用程序私钥
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtfLBZtfSKJ3mwThLuRnLO1eEh5jf2fXDlC06VLH/lmFFJrZBfkJqzE9T41k5JoGpluI6r03vo6Vx29UYOP0bf531UUr9Bpg2TLBwmUVQYoY15plsUjKkqTqDYq55m3cYJpn0TBROMyurbGs4i0TcNq35zjC2wbl0DWmxmOhWyg3NhUu9uqWUJGbfxTuowKQxaK/3XDoOLx+mQCytz0JvNevUhBhOu6eZLCh3hNOswu8gMf3a5kCReya24uXOd26SKd5HYlwFHk4ndSSjwIYulQs9hnlSbkJn+zykjc6qdDQLMWuqkSKlb5Snu7TCh5FNUO4KsFqk2bEXgysJy23lhwIDAQAB
-----END PUBLIC KEY-----
* 4. settings.py 配置文件中填写参数
import os# 获取到iPay的路径
BASE_DIR = os.path.dirname(__file__)
print(BASE_DIR)
# 得到iPay下的pem目录路径
PEM_DIR = os.path.join(BASE_DIR, 'pem')
# 应用程序私钥地址
APP_PRIVATE_KEY_URL = os.path.join(PEM_DIR, 'app_private_key.pem')
# 支付宝公钥地址
ALIPAY_PUBLIC_KEY_URL = os.path.join(PEM_DIR, 'alipay_public_key.pem')# 应用id
APPID = '2021000120612450'# 默认回调
APP_NOTIFY_URL = None# 应用私钥字符串
with open(APP_PRIVATE_KEY_URL, mode='r', encoding='utf8') as rf1:APP_PRIVATE_KEY_STRING = rf1.read()# 支付宝公钥字符串
with open(ALIPAY_PUBLIC_KEY_URL, mode='r', encoding='utf8') as rf2:ALIPAY_PUBLIC_KEY_STRING = rf2.read()# 加密方式
SIGN_TYPE = "RSA2"# BUG模式
DEBUG = True# 沙箱网关 ( DEBUG = True )
dev = 'https://openapi.alipaydev.com/gateway.do?'# 真实网关 ( DEBUG = False )
master = 'https://openapi.alipay.com/gateway.do?'# 网关
ALIPAY_URL = dev if DEBUG else master
* 5. 在pay.py生成alipay支付对象
# 导入配置参数
from .settings import *# 导入AliPay模块
from alipay import AliPay# 生成alipay对象
alipay = AliPay(# 应用idappid=APPID,# 默认回调app_notify_url=APP_NOTIFY_URL,# 应用私钥字符串app_private_key_string=APP_PRIVATE_KEY_STRING,# 支付宝公钥字符串alipay_public_key_string=ALIPAY_PUBLIC_KEY_STRING,# 加密方式sign_type=SIGN_TYPE,# 调试开关(上线设置False)debug=False,)
* 6. __init__.py 中导入alipay对象 与 网关在导入iPay时会自动触发__init__.py,则导入alipay对象 与 网关
# 导入alipay对象 与 网关
from .pay import alipay
from .settings import ALIPAY_URL"""
在导入iPay时则导入alipay对象 与 网关
from luffy/lib import iPay
iPay.alipay ...
iPay.ALIPAY_URL ...
"""
4. 订单接口
4.1 创建app
* 1. 新建订单app
cd luffy/apps/
python ../../manage.py startapp order
* 2. 注册app
INSTALLED_APPS = [...'order'
* 3. 总路由配置
path('order/', include('order.urls')),
* 4. 子路由 先在order目录下新建urls.py
from django.urls import re_path, includefrom . import views
# 导入SimpleRouter
from rest_framework.routers import SimpleRouter# 生成对象
router = SimpleRouter()
# 注册路由
router.register('pay', views.PayView, 'pay')# 路由列表
urlpatterns = [re_path('', include(router.urls)),
]
4.2 表设置
1. 订单表
2. 订单表与课程表的第三张表
from django.db import models# 导入用户表
from user.models import User
# 导入课程表
from course.models import Course# 订单表
class Order(models.Model):"""用户表(一) 对 (多)订单表一个用户可以有多个订单, 一个订单只能有一个用户. 一对多关系, 关联字段写在多的一方."""# 支付状态status_choices = ((0, '未支付'),(1, '已支付'),(2, '以超时'),(3, '超时取消'))# 支付状态pay_choices = ((0, '支付宝'),(1, '微信'))# 订单标签subject = models.CharField(max_length=150, verbose_name='订单标题')# 订单总价total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单总价', default=0)# 订单号 唯一out_trade_no = models.CharField(max_length=64, verbose_name='订单号', unique=True)# 订单状态order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name='订单状态')# 支付方式pay_type = models.SmallIntegerField(choices=pay_choices, default=0, verbose_name='支付方式')# 支付时间pay_time = models.DateTimeField(null=True, verbose_name='支付时间')# 外键user = models.ForeignKey(to=User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,verbose_name='下单用户')# 创建时间created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')# 最后更新时间updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')# 定义元类class Meta:# 表名db_table = 'luffy_order'# 定义一个变量verbose_name = '订单记录'# 后台展示的表名verbose_name_plural = verbose_name# 打印对象时展示商品的名字与价格def __str__(self):return f'{self.subject} - ¥:{self.total_amount}'# 订单详情
class OrderDetail(models.Model):"""订单表(多) 对 (多)课程表一个订单可以有多个课程, 一个课程可以在对个订单中第三张表外键关联订单表, 关联课程表, 创建额外的字段"""# 外键 关联订单表order = models.ForeignKey(to=Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,verbose_name='订单外键')# 外键 关联关联课程表course = models.ForeignKey(to=Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False,verbose_name='课程外键')# 原价price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='课程原价')# 实价real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name='课程实价')# 定义元类class Meta:# 表名db_table = 'luffy_order_detail'# 定义一个变量verbose_name = '订单详情'# 定向详情verbose_name_plural = verbose_name# 打印对象时展示 课程名字:课程价格def __str__(self):try:return f'{self.course.name}的订单: {self.order.out_trade_no}'# 没有录数据的时候不会至于报错except:return super().__str__()
数据库迁移:
python manage.py makemigrations
python manage.py migrate
# 修改order目录下的admin.py 文件将表注册到后台
import xadminfrom . import models
# 将订单表与订单详情表上条件到后台管理
xadmin.site.register(models.Order)
xadmin.site.register(models.OrderDetail)
# 后台展示中文菜单
# 修改order下的__init__.py
default_app_config = "order.apps.OrderConfig"# 修改order下的apps.py
from django.apps import AppConfigclass OrderConfig(AppConfig):name = 'order'verbose_name = '订单'
4.3 接口
支付接口分析
1. 支付接口生成订单, 生成支付连接, 返回支付链接2. 支付成功时: 支付宝异步回调的post接口验证签名, 修改订单状态3. 支付成功时: 支付宝同步get回调 (支付成功后展示的页面)vue页面创建时 生命周期钩子函数发送请求去后端查询订单状态(可有可无)
1. 订单总价校验从数据库中获取对应的课程对象, 课程的价格取出来累加, 再与前端传递的值比较是否相等.
2. 生成订单号使用一个uuid生成一个随机字符串
3. 支付链接生成使用自己封装的支付宝模块生成支付连接, 将支付链接存放在序列化对象的context属性中.
4. request.user 中获取用户对象视图类中重写create方法, 为context参数设置值为request, 序列化中取出用户对象
5. 入库(两张表)的信息准备将用户对象保存到attrs字典中
6. 重写序列化的create方法将课程从validated_data字典中pop出来...先创建支付表的数据, 在遍历创建支付详情表的数据
* 1. 模型序列化器
# 导入模型序列化
from rest_framework import serializers# 导入模型层
from . import models# 导入异常模块
from rest_framework.exceptions import ValidationErrorclass OrderModelSerializer(serializers.ModelSerializer):course_obj = models.Course.objects.all()"""PrimaryKeyRelatedField 将 {'course': [课程id1, 课程id2, ...]} 转为 {'course': [课程1_obj, 课程2_obj, ...]}queryset参数对于转换表的queryset对象, 提交的值是一个列表, 设置many=True, 只写模式Course表与 Order可以没有关系,只要设置queryset的参数值是一张表的数据即可 通过id获取到表对应的数据对象 """course = serializers.PrimaryKeyRelatedField(queryset=course_obj, many=True, write_only=True)class Meta:model = models.Order# 转换的字段 订单标签 价格 支付方式 购买的课程fields = ['subject', 'total_amount', 'pay_type', 'course']# (设置了默认值, 不会提示价格 支付方式字段必填, 使用require提示字段必须填写)extra_kwargs = {'total_amount': {'required': True},'pay_type': {'required': True},}# 全局校验def validate(self, attrs):# 1. 校验价格, 检验不成功直接抛出异常, 校验成功返回价格total_amount = self._check_price(attrs)# 2. 生成订单号out_trade_no = self._gen_out_trade_no()# 3. 生成支付链接并保存到context参数中self._gen_pay_url(attrs, out_trade_no, total_amount)# 4. 获取用户对象 在视图类中重写create方法, 在生成模型序列化对象时将request通过context参数传递user_obj = self._get_user()# 5. 写入数据库前准备self._before_create(attrs, user_obj, out_trade_no)return attrs# 价格校验@staticmethoddef _check_price(attrs):# 获取提交的价格total_amount = attrs.get('total_amount')# 获取课程对象course_list = attrs.get('course')# 计算数据库中的价格total_price = 0for course_obj in course_list:total_price += course_obj.price# 判断值是都相等的if total_amount != total_price:# 价格不一致, 直接抛出异常raise ValidationError('价格不合法')return total_amount# 生成订单号@staticmethoddef _gen_out_trade_no():# 订单号是一个唯一字符串, 使用uuid即可from uuid import uuid4# uuid的类型值的格式 bce5eed9-8985-42ab-b37a-51bf8909c1c5 类型是 <class 'uuid.UUID'># 将类类型转为str类型, 再将-去除 bce5eed9898542abb37a51bf8909c1c5return str(uuid4()).replace('-', '')# 生成支付链接def _gen_pay_url(self, attrs, out_trade_no, total_amount):# 导入支付模块from luffy.lib import iPay# 获取商品名称(前端传入)subject = attrs.get('subject')# 导入回调地址from django.conf import settings# 网关地址alipay_url = iPay.ALIPAY_URL# total_amount是Decimal类型, 支付宝无法识别, 转为float类型即可print(total_amount, type(total_amount)) # 138.00 <class 'decimal.Decimal'>total_amount = float(total_amount)# 生成对象order_string = iPay.alipay.api_alipay_trade_page_pay(# 订单号唯一标识out_trade_no=out_trade_no,# 价格total_amount=total_amount,# 商品名称subject=subject,# 支付成功 get同步回调, 前台展示的页面,return_url=settings.NOTIFY_URL,# 支付成功 post异步回调, 向后端发送请求 修改订单状态(以支付)notify_url=settings.RETURN_URL)# 生成支付地址pay_url = alipay_url + order_string# 将支付链接保存到context中self.context['pay_url'] = pay_url# 获取用户def _get_user(self):# 从context中获取request对象request = self.context.get('request')# 放回user对象(登入成功之后request.user会有用户的数据对象)return request.user# 写入数据前准备@staticmethoddef _before_create(attrs, user_obj, out_trade_no):# 将用户对象写入到序列化字典中attrs['user'] = user_obj# 将订单号写入到序列化字典中attrs['out_trade_no'] = out_trade_nodef create(self, validated_data):# 将课程列表虚拟字段pop掉course_list = validated_data.pop('course')# 支付表数据写入(需要的数据, 'subject', 'total_amount', 'course', 'user')order_obj = models.Order.objects.create(**validated_data)# 写入支付详情表, 需要的数据(order, course, price, real_price)for course_obj in course_list:models.OrderDetail.objects.create(order=order_obj, course=course_obj, price=course_obj.price,real_price=course_obj.price)# 将支付对象返回, 返回了也没用到return order_obj
# 支付宝回调地地址
# 后台URL
BASE_URL = 'http://127.0.0.1:8000'
# 前台URL
LUFFY_URL = 'http://127.0.0.1:8080'# 后台异步回调地址
RETURN_URL = BASE_URL + '/order/success/'
# 前台同步回调地址(没有/结尾)
NOTIFY_URL = LUFFY_URL + '/pay/success'
* 2. 视图类单独使用rest_framework_jwt.authentication.JSONWebTokenAuthentication的话所有游客都可以访问, request.user 是AnonymousUser 匿名用户, 使用rest_framework内置权限类, 对登入用户进行校验, 匿名用户禁止访问!
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import CreateModelMixin
from . import models
from .serializer import OrderModelSerializer
from rest_framework.response import Response
from rest_framework_jwt.authentication import JSONWebTokenAuthentication
from rest_framework.permissions import IsAuthenticated# 生成支付订单
class PayView(GenericViewSet, CreateModelMixin):# 登入认证类 + 限制类 现在限制只有登入用户才能使用authentication_classes = [JSONWebTokenAuthentication] # 没有用户登入返回permission_classes = [IsAuthenticated]# 使用的表数据queryset = models.Order.objects.all()# 使用的模型序列化类serializer_class = OrderModelSerializer# 重写视图类的 create方法 (复制CreateModelMixin的create方法进行修改)def create(self, request, *args, **kwargs):# 生成序列化器时, 将request对象创建给模型序列化器 通过context参数serializer = self.get_serializer(data=request.data, context=request)# 校验数据serializer.is_valid(raise_exception=True)# 保存数据self.perform_create(serializer)headers = self.get_success_headers(serializer.data)return Response(serializer.context.get('pay_url'))
4.4 接口测试
* 1. 接口地址: http://127.0.0.1:8000/order/pay/
* 2. 需要先登入(post请求)登入地址: http://127.0.0.1:8000/user/login/?username=root&password=zxc123456
* 3. 携带token访问支付接口
key: Authorization
value: jwt eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoxLCJ1c2VybmFtZSI6InJvb3QiLCJleHAiOjE2NTUyOTg4NTYsImVtYWlsIjoiMTM2QHFxLmNvbSJ9.7vHsFCMCM-8_Uc5-bOvEFMvUf4d5fBtCGayQG53mb2c
* token前面携带 jwt空格
* 4. 价格不合法测试三门课程都现在, 价格是138, 现在随便填写一个错误的价格
* 5. 价格正常测试, 返回支付地址
4.5 前台点击购买事件
为点击购买绑定事件, 免费课程页面, 页面详情, 搜索页面, 三个位置.
只有等有用户才能购买:
登入成功之后 this.$cookies.set('token', response.data.token) 将token保存到了scookies中
先从cokkies中获取token, 如果有值则可以发送请求, 如果没有值则提示用户登入.拿到支付地址之后跳转到支付页面(新开设页面)
window.open(url) 简写 open(url)
在当前页面打开网址
open(url, '_self')
<span class="buy-now" @click="buy_now(course)">立即购买</span>
// 购买课程
buy_now(course) {// 从cookies中获取tokenlet token = this.$cookies.get('token')// 判断是否为登入状态if (!token) {// 提示用户登入this.$message({message: '请先登入!'})return false}// 用户已经登入向后台发送请求this.$axios({method: 'post', url: this.$settings.base_url + '/order/pay/',headers: {Authorization: 'jwt ' + token}, data: {// 课程的名字作为商品的标题"subject": course.name,// 课程的价格"total_amount": course.price,// 支付方式"pay_type": 1,// 课程的id"course": [course.id]}}).then(response => {// 访问成功获取到支付链接 直接从response.data中获取数据(后端直接返回链接, 不是返回request.data)// 视图序列化器中报错会 需要从response.data.dat中获取错误信息// console.log(response.data)// 跳转到支付地址if (response.data.data) {this.$message({message: response.data.data // 异常新 例如 { "total_amount": [ "该字段是必填项。" ] }})return false}// 返回的不是错误信息那么就是支付链接let pay_url = response.data// console.log(pay_url)// 跳转到支付链接open(pay_url, '_self')}).catch(error => {this.$message({message: '未知错误, 请联系管理员!'})})
}
4.6 同步回调页面
两部分:
1. 分析支付宝回调参数展示订单的支付情况
2. 向后端get请求, 查询后端是否修改订单状态
* 1. 新建支付成功展示的页面 PayCourse.vue
<template><div class="pay-success"><!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)--><Header/><div class="main"><div class="title"><div class="success-tips"><p class="tips">您已成功购买 1 门课程!</p></div></div><div class="order-info"><p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p><p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p><p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p></div><div class="study"><span>立即学习</span></div></div></div>
</template><script>
import Header from "@/components/Head"export default {name: "PayCourse",data() {return {result: {},};},created() {// location.search 获取 url后拼接的参数:?及后面的所有参数 => ?a=1&b=2// console.log(location.search);// 解析支付宝回调的url参数let params = location.search.substring(1); // 去除? 保留 a=1&b=2// 按&切分得到一个元组, 要有是一个空元组let items = params.length ? params.split('&') : [];// console.log(items)/*0: "charset=utf-8"1: "out_trade_no=1c6bd7ee950842249a7cb0b6956224d2"...*/// 将每一项逐个添加到args对象中for (let i = 0; i < items.length; i++) { // 第一次循环a=1,第二次b=2// 按=切分得到 k = vlet k_v = items[i].split('='); // ['a', '1']// 列表元素少与2直接忽略, 有键缺值的情况if (k_v.length >= 2) {// url编码反解 解码操作,因为查询字符串经过编码的// 解码klet k = decodeURIComponent(k_v[0]);// 解码value 并保存到result对象中this.result[k] = decodeURIComponent(k_v[1]);}}// 解析后的结果// console.log(this.result);/*app_id: "2021000120612450"auth_app_id: "2021000120612450"charset: "utf-8"*/// 把地址栏上面的支付结果,再get请求转发给后端this.$axios({method: 'get',// 携带同步回调的参数访问后端url: this.$settings.base_url + '/order/success/' + location.search,}).then(response => {console.log(response.data)if (response.data) {this.$message({message: '支付结果同步成功'})} else {this.$message({message: '支付结果同步失败'})}}).catch(() => {this.$message({message: '查询订单支付状态失败'})})},components: {Header,}
}
</script><style scoped>
.main {padding: 60px 0;margin: 0 auto;width: 1200px;background: #fff;
}.main .title {display: flex;-ms-flex-align: center;align-items: center;padding: 25px 40px;border-bottom: 1px solid #f2f2f2;
}.main .title .success-tips {box-sizing: border-box;
}.title img {vertical-align: middle;width: 60px;height: 60px;margin-right: 40px;
}.title .success-tips {box-sizing: border-box;
}.title .tips {font-size: 26px;color: #000;
}.info span {color: #ec6730;
}.order-info {padding: 25px 48px;padding-bottom: 15px;border-bottom: 1px solid #f2f2f2;
}.order-info p {display: -ms-flexbox;display: flex;margin-bottom: 10px;font-size: 16px;
}.order-info p b {font-weight: 400;color: #9d9d9d;white-space: nowrap;
}.study {padding: 25px 40px;
}.study span {display: block;width: 140px;height: 42px;text-align: center;line-height: 42px;cursor: pointer;background: #ffc210;border-radius: 6px;font-size: 16px;color: #fff;
}
</style>
* 2. 设置PayCourse的路由
// 导入支付成功提示页面
import PayCourse from "@/views/PayCourse"// 支付成功提示页面 (与后台中配置的一致)
{path: '/pay/success',name: 'PayCourse',component: PayCourse
},
# 前台同步回调地址(没有/结尾)
RETURN_URL = LUFFY_URL + '/pay/success'
* 3. vue中url地址中多余的/#/不删除的话回调地址路由匹配不成功http://127.0.0.1:8080/pay/success 路由匹配不成功
// 路由中设置 mode: "history", 删除url中多余的/#/
const router = new VueRouter({mode: "history",routes
})
* 4. 购买课程测试, 购买成功之后回调地址携带参数
回调地址+参数
http://127.0.0.1:8080/pay/success?
// 编码格式
charset=utf8
// 订单号
&out_trade_no=ffb82dd39bf043808c311b4ae983d9ec&method=alipay.trade.page.pay.return
// 价格
&total_amount=39.00
// 签名
&sign=KFPDpjwAGuUa73qsBctb0m9Nj0514PoQnxcMyH%2Fzw1rPmzY%2F1%2BTwH2v4BB1hSyc5%2BRV6TIc%2Bvz1SrchxtND20HU%2BOjLuT%2BxSlGK034OFUDpxtiy4ofkYWWqcvxz84sQ8BUifdGKWDFoH1cgagbJVGl3DO1Lce5HbsvNZY2GeYQTC%2BTIWCUMKCcWYP375%2FQFCFoMUWj6LNcIQk3rrKMNJm9B%2F%2BwryrcRMJT1cNZnuLhKiYTzcIFRWnDyVu4aVfKa3xJtIEbTKAqIVHK9LuV%2BkrkBqD1GAwOy1gwIlrkB4VJ%2BvwPuyRpmVzGK7zVq6JIVzZNVawjFBhduwuoG0eUH7oA%3D%3D
// 支付宝流水号
&trade_no=2022060922001441860503213387
// 授权商户的App id
&auth_app_id=2021000120612450
// 版本
&version=1.0
// 应用id
&app_id=2021000120612450
// 签名类型
&sign_type=RSA2
// 卖方id
&seller_id=2088621959339670
// 时间戳
×tamp=2022-06-09+11%3A50%3A44#/
location.search 获取 url后拼接的参数
先去除? 在按&切分得到元组, 元组的元素'k=v',
在遍历元素, 按=切分得到一个元组 ['k', 'v']
对k, v的字符进行解码保存到一个对象中
4.7 订单状态接口
1. 查询订单状态接口 get请求
2. 修改订饭状态接口 post请求
* 1. 路由order.py的urls.py
# 订单状态接口
re_path('success', views.SuccessView.as_view())
# 支付状态
class SuccessView(APIView):@staticmethoddef get(request):# 从参数中获取订单号去数据库中查询订单的状态out_trade_no = request.query_params.get('out_trade_no')order_obj = models.Order.objects.filter(out_trade_no=out_trade_no).first()# 订单状态order_status = order_obj.order_status# 判断订单状态if order_status != 1: # 1 为支付成功状态return Response(False)return Response(True)# 支付宝异步回调(只能上线之后访问, 支付宝无法访问到我电脑本地的地址)@staticmethoddef post(request):# 导入支付类from lib.iPay import alipayfrom utils.logger import log# 获取订单号out_trade_no = request.data.get('out_trade_no')# 获取支付时间pay_time = request.data.get('gmt_payment')# 验证签名 支付宝返回的urlencode格式被转为queryset_dictdata = request.data.dict() # 装为普通字段sign = data.pop('sign')success = alipay.verify(data, sign)# 验证成功, 并且 data中trade_status的值必须是TRADE_SUCCESS或TRADE_FINISHEDif success and data['trade_status'] in ('TRADE_SUCCESS', 'TRADE_FINISHED'):# 更新订单状态与支付时间models.Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1, pay_time=pay_time)# 记录日志log.info(f'{out_trade_no}支付成功')return Response('success') # 验证成功必须返回successlog.info(f'{out_trade_no}支付失败')return Response('error') # 验证不成功
5. 上传到远程仓库
在Terminai中输入git命令
5.1 前端
# 添加到暂存区
PS D:\vue\luffy_vue> git add .
# 提交到版本库
PS D:\vue\luffy_vue> git commit -m '完成支付页面'
# 下拉
PS F:\synchro\Project\luffy> git pull origin dev
# 上传到远端仓库
PS F:\synchro\Project\luffy> git push origin dev
5.2 后端
# 添加到暂存区
PS F:\synchro\Project\luffy> git add .
# 提交到版本库
PS F:\synchro\Project\luffy> git commit -m '完成支付功能'
# 下拉
PS F:\synchro\Project\luffy> git pull origin dev
# 上传到远端仓库
PS F:\synchro\Project\luffy> git push origin dev
5.学城项目 支付宝支付相关推荐
- 路飞学城项目介绍与分析
路飞学城项目介绍与分析 文章目录 路飞学城项目介绍与分析 一.企业的web项目类型 二.企业项目开发流程 三.立项申请阶段 四.需求分析 1. 首页 2.注册登录 3.课程列表 4.课程详情 5.购物 ...
- Python高级全栈开发实战 老男孩课程S16+路飞学城项目+女神串讲 Python全栈直通车课程
python高级全栈开发实战 老男孩课程S16+路飞学城项目+女神串讲 Python全栈直通车课程 Python高级全栈开发实战老男孩课程,是可以帮助同学们从零基础开始到项目开发实战的全栈课程,内容非 ...
- 路飞学城项目之课程搜索关键字接口及支付宝支付功能
文章目录 1.课程之搜索关键字接口 2.支付宝支付功能 2.1.简单介绍 2.2.aliapy二次封装包 2.2.1.GitHub开源框架 2.2.2.依赖 2.2.3.结构 2.2.4.alipay ...
- 0. 学城项目 后端环境配置
1. 企业的web项目类型 1. 商城 (某某团购)1.1 B2C直销商城 商家与会员直接交易(Busioness To Customer)1.2 B2B批发商城 商家与商家直接交易1.3 B2B2C ...
- 无人驾驶8: 粒子滤波定位(优达学城项目)
优达学城无人车定位的项目实现: 粒子滤波算法流程图 粒子滤波的伪代码: step1:初始化 理论上说,当粒子数量足够多时,能准确地呈现贝叶斯后验分布,如果粒子太少,可能漏掉准确位置,粒子数量太多,会拖 ...
- 路飞学城项目之集成支付宝支付功能
文章目录 1.订单表设计 2.支付接口类:order/views.py 3.支付接口序列化类:model/serializers 4.支付生成页面 5.支付成功的回调页面 6.支付成功的回调接口 1. ...
- 路飞学城项目之项目上线
文章目录 1.购买服务器 2.连接服务器 3.服务器命令 3.1.管理员权限 3.2.配置终端 3.3.更新系统软件包 3.4.安装软件管理包和可能使用的依赖 3.5.检测是否成功:会将git作为依赖 ...
- 4.学城项目 课程接口
1. Django缓存到redis中 django_redis模块.(继承django的cache, 使用方式进而cache的使用一致, 数据被加密存到redis中) * 1. pip install ...
- 路飞学城项目之首页轮播图定时更新、课程页面前端及课程表分析
文章目录 1.首页轮播图定时更新(使用celery) 2.课程页面前端 3.课程表分析 3.1.课程表相关模型(实战课为例) 3.2.课程表数据录入 3.3.课程分类接口 3.3.课程分类前端展示(对 ...
- 2.学城项目 头部底部组件首页轮播图
1. 下载框架/组件/库 * 1. 下载项目中需要使用的框架个组件. 1. axion 网络请求的第三方框架 (之前测试前后端通信已经安装过了) cnpm install axios -S 2. vu ...
最新文章
- 19.04.02笔记
- WPF ListView DoubleClick
- 针对校园某服务器的一次渗透测试
- 【Transformer】ACMix:On the Integration of Self-Attention and Convolution
- .NET Framework VS .NET Core
- 居中的文字在小屏幕下后面的换行
- 爱奇艺网络协程编写高并发应用实践
- .net反编译工具ILSpy
- h5背景图片尺寸怎么设置_html如何设置图片大小
- 树莓派摄像头——图像 视频采集
- java 佛祖保佑_佛祖保佑 永无bug 注释模板设置详解(仅供娱乐)
- java闹钟_JAVA 闹钟程序
- 无人值守安装操作系统
- 笔记本电脑桌面上计算机打不开怎么办,笔记本电脑开了机一直进不去桌面怎么办...
- 手机编程python可以实现吗?有哪些软件值得推荐?
- htc legend 升级到 android 2.2 froyo 后,输入法问题
- 巧借“中国制造2025”东风占领“智”高点
- 如何利用海关数据开发客户?
- eclispe override报错的解决办法
- target_link_libraries 和link_libraries区别
热门文章
- python operator用法,Python operator.eq()函数与示例
- OJ刷题二——设计输出实数格式
- 王家林Spark视频
- 201621123068 Week03-面向对象入门
- 【历史上的今天】7 月 23 日:Novell 挑战微软;计算机界的教育家出生;第一颗 Landsat 卫星发射
- 5G消息富媒体最新形态
- android 面包屑控件,Android —— 面包屑控件(BreadcrumbTreeView)
- 华为云迁移工具推荐最佳实践:物理服务器迁移到华为云
- 计算机模拟水循环的过程,袋装水模拟做科学小实验水循环(步骤图解)
- 系统封装 EasyBoot如何将WIN7安装版提取到光盘