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(
appid="2021000120612450",
app_notify_url=None,
app_private_key_string=app_private_key_string,
alipay_public_key_string=alipay_public_key_string,
sign_type="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='<<肾宝>>男人要肾好,就要喝肾宝。喝了之后,比刘翔快,比姚明高。一瓶提神醒脑,两瓶永不疲劳,三瓶长生不老。噢耶...',
)
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
BASE_DIR = os.path.dirname(__file__)
print(BASE_DIR)
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')
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"
DEBUG = True
dev = 'https://openapi.alipaydev.com/gateway.do?'
master = 'https://openapi.alipay.com/gateway.do?'
ALIPAY_URL = dev if DEBUG else master
* 5. 在pay.py生成alipay支付对象
from .settings import *
from alipay import AliPay
alipay = AliPay(
appid=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,
debug=False,
)
* 6. __init__.py 中导入alipay对象 与 网关
在导入iPay时会自动触发__init__.py,则导入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, include
from . import views
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
import xadmin
from . import models
xadmin.site.register(models.Order)
xadmin.site.register(models.OrderDetail)
default_app_config = "order.apps.OrderConfig"
from django.apps import AppConfig
class 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 ValidationError
class 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']
extra_kwargs = {
'total_amount': {'required': True},
'pay_type': {'required': True},
}
def validate(self, attrs):
total_amount = self._check_price(attrs)
out_trade_no = self._gen_out_trade_no()
self._gen_pay_url(attrs, out_trade_no, total_amount)
user_obj = self._get_user()
self._before_create(attrs, user_obj, out_trade_no)
return attrs
@staticmethod
def _check_price(attrs):
total_amount = attrs.get('total_amount')
course_list = attrs.get('course')
total_price = 0
for course_obj in course_list:
total_price += course_obj.price
if total_amount != total_price:
raise ValidationError('价格不合法')
return total_amount
@staticmethod
def _gen_out_trade_no():
from uuid import uuid4
return 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
print(total_amount, type(total_amount))
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,
return_url=settings.NOTIFY_URL,
notify_url=settings.RETURN_URL
)
pay_url = alipay_url + order_string
self.context['pay_url'] = pay_url
def _get_user(self):
request = self.context.get('request')
return request.user
@staticmethod
def _before_create(attrs, user_obj, out_trade_no):
attrs['user'] = user_obj
attrs['out_trade_no'] = out_trade_no
def create(self, validated_data):
course_list = validated_data.pop('course')
order_obj = models.Order.objects.create(**validated_data)
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
BASE_URL = 'http://127.0.0.1:8000'
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
def create(self, request, *args, **kwargs):
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) {
let 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,
"course": [
course.id
]
}
}).then(response => {
if (response.data.data) {
this.$message({
message: response.data.data
})
return false
}
let pay_url = response.data
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() {
let params = location.search.substring(1);
let items = params.length ? params.split('&') : [];
for (let i = 0; i < items.length; i++) {
let k_v = items[i].split('=');
if (k_v.length >= 2) {
let k = decodeURIComponent(k_v[0]);
this.result[k] = decodeURIComponent(k_v[1]);
}
}
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 路由匹配不成功
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):
@staticmethod
def 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:
return Response(False)
return Response(True)
@staticmethod
def post(request):
from lib.iPay import alipay
from utils.logger import log
out_trade_no = request.data.get('out_trade_no')
pay_time = request.data.get('gmt_payment')
data = request.data.dict()
sign = data.pop('sign')
success = alipay.verify(data, sign)
if 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')
log.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