支付宝支付
一、支付宝支付介绍
# 购买课程,付款--->支付宝支付
- 支付宝支付(即便没有账号,也可以测试)
- 微信支付(需要用营业执照申请商家帐号)
- 银联支付
# 支付宝支付介绍地址
https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667
集成支付宝流程
-我们自己的网站:点击购买按钮--->向我们后端发送请求--->携带购买商品信息--->生成订单,入库,订单是未支付状态----->生成支付宝支付链接---->返回给前端
-前端拿到支付链接--->get请求打开---->咱们的前端就来到了支付宝的页面-->用户掏出手机扫描支付--->付款完成---->支付宝收到了钱---->get回调(咱们配置回调地址)---->跳回我们自己的网页--->支付宝还会发送post请求给我们后端---->我们要验证签名,通过后,把订单状态改为已支付状态
支付宝沙箱环境
# 要使用支付宝:需要营业执照注册,咱们没有,咱们使用沙箱环境测试, 测试通过,后期只要换成公司的商户号和支付宝公钥私钥即可
# 沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info
# 需要生成公钥私钥:非对称加密--->公钥加密,私钥解密
https://opendocs.alipay.com/common/02kipl
# 生成公钥和私钥
# 把你的公钥,配置在支付宝账号里,生成支付宝公钥---->写支付,需要用支付宝公钥和私钥
把生成的公钥保存到沙箱商家中,并拿到支付宝公钥
二、GitHub开源框架
安装
# 使用第三方sdk
-基于官方的api封装的
-地址:https://github.com/fzlee/alipay
# 依赖
>: pip install python-alipay-sdk --upgrade
# 如果抛ssl相关错误,代表缺失该包
>: pip install pyopenssl
alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
拿应用公钥跟支付宝换来的支付宝公钥
-----END PUBLIC KEY-----
app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
通过支付宝公钥私钥签发软件签发的应用私钥
-----END RSA PRIVATE KEY-----
测试dom
from alipay import AliPay, DCAliPay, ISVAliPay
from alipay.utils import AliPayConfig
# 私钥和支付宝公钥 需要去支付宝官网生成配置
# 只能使用测试环境:沙箱环境:https://openhome.alipay.com/platform/appDaily.htm?tab=info
# 应用私钥
app_private_key_string = open("./app_private_key.pem").read()
# 支付宝公钥
alipay_public_key_string = open("./alipay_public_key.pem").read()
alipay = AliPay(
appid="9021000122697972", # 支付宝页面上复制,沙箱环境--->公司有人会给你
app_notify_url=None, # 默认回调 url
app_private_key_string=app_private_key_string,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # RSA 或者 RSA2
debug=False, # 默认 False
verbose=False, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)
subject = '兰博基尼' # 订单的字符串
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no="201102102102102103", # 订单号,唯一
total_amount=100,
subject=subject,
return_url="https://example.com",
notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
# print(order_string)
# 拼接支付宝支付网关地址
print('https://openapi-sandbox.dl.alipaydev.com/gateway.do?' + order_string) # 支付宝支付地址
三、支付宝二次封装
结构
libs
├── own_alipay # aliapy二次封装包
│ ├── __init__.py # 包文件
│ ├── pem # 公钥私钥文件夹
│ │ ├── alipay_public_key.pem # 支付宝公钥文件
│ │ ├── app_private_key.pem # 应用私钥文件
│ ├── pay.py # 支付文件
└── └── settings.py # 应用配置
settings
import os
# 应用私钥
APP_PRIVATE_KEY_STRING = open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
# 支付宝公钥
ALIPAY_PUBLIC_KEY_STRING = open(
os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
# 应用ID
APP_ID = '9021000122697972'
# 加密方式
SIGN = 'RSA2'
# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True
# 支付网关地址
# 三元表达式:条件成立之后的结果 if 条件 else 条件不成功之后的结果
GATEWAY = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'
pay.py
from alipay import AliPay
from alipay.utils import AliPayConfig
from . import settings
# 支付对象
alipay = AliPay(
appid=settings.APP_ID, # 支付宝页面上复制,沙箱环境--->公司有人会给你
app_notify_url=None, # 默认回调 url
# 应用私钥
app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
sign_type=settings.SIGN, # RSA 或者 RSA2
debug=settings.DEBUG, # 默认 False
verbose=False, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)
# 支付网关地址
gateway = settings.GATEWAY
init.py
# 包对外提供的变量
from .pay import alipay, gateway
order/urls.py
from django.urls import path
from .views import AlipayView
urlpatterns = [
path('test_alipay/', AlipayView.as_view()),
]
order/views.py
from rest_framework.views import APIView
from utils.common_response import APIResponse
# 导入支付宝的包
from libs.own_alipay import alipay, gateway
## 测试支付宝二次封装的包
class AlipayView(APIView):
def get(self, request, *args, **kwargs):
res = alipay.api_alipay_trade_page_pay(
out_trade_no="201103232102102102102", # 订单编号
total_amount=9000, # 转账钱数
subject='狗狗课程', # 主题
return_url="https://example.com",
notify_url="https://example.com/notify"
)
# 给前端支付宝转账链接
pay_url = gateway + res
return APIResponse(pay_url=pay_url)
四、下单应用
4.1 创建order应用
到apps路径下
python ../../manage.py startapp order
4.2 订单相关表设计
# Order 订单表
# OrderDetail 订单详情表
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 = (
(1, '支付宝'),
(2, '微信支付'),
)
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) # 唯一,不能重复,后期可以根据订单号修改订单状态
trade_no = models.CharField(max_length=64, null=True, verbose_name="交易流水号") # 支付宝支付,生成流水号,存在着
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
pay_time = models.DateTimeField(null=True, verbose_name="支付时间") # 支付宝返回的时间
# 外键
user = models.ForeignKey(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='创建时间')
class Meta:
db_table = "luffy_order"
verbose_name = "订单记录"
verbose_name_plural = "订单记录"
def __str__(self):
return "%s - ¥%s" % (self.subject, self.total_amount)
class OrderDetail(models.Model):
"""订单详情"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,
verbose_name="订单")
course = models.ForeignKey(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 = "订单详情"
def __str__(self):
try:
return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
except:
return super().__str__()
# 数据迁移命令
五、后台-下单接口
# 前端传入数据--->在订单表中插入记录,生成支付链接,返回给前端
-前端传入数据:{'subject':'某个课程', 'total_amount':100, 'pay_type':1, 'courses':[1,2,3]}
-后端做的事情--要保存,要校验--->序列化类来做
# 1)订单总价校验
# 2)生成订单号:唯一的
# 3)获取支付用户:request.user
# 4)支付链接生成:支付宝支付链接
# 5)入库(两个表)的信息准备
视图类
from utils.common_response import APIResponse
from rest_framework.viewsets import GenericViewSet
from .serializer import PayOrderSerializer
from rest_framework_jwt.authentication import JSONWebTokenAuthentication # 认证类
from rest_framework.permissions import IsAuthenticated # 频率类
# 下单支付接口
# http://127.0.0.1:8000/api/v1/order/pay/ ---->post请求,需要先登录,请求头中Authorization:jwt
class PayOrderView(GenericViewSet):
serializer_class = PayOrderSerializer
# 认证类和权限类
authentication_classes = [JSONWebTokenAuthentication]
permission_classes = [IsAuthenticated]
def create(self, request, *args, **kwargs):
# 新增数据,支付成功后,需要在订单表、订单详情表中都新增一条数据
ser = self.get_serializer(data=request.data, context={'request': request}) # 用上下文保存request对象,context是个字典
ser.is_valid(raise_exception=True)
ser.save()
pay_url = ser.context.get('pay_url')
return APIResponse(msg='下单成功', pay_url=pay_url)
序列化类
from rest_framework import serializers
from .models import Order, OrderDetail
from course.models import Course
from rest_framework.exceptions import APIException
from libs.own_alipay import alipay, gateway
import uuid
# 下单的序列化,需要校验数据,保存
class PayOrderSerializer(serializers.ModelSerializer):
# {'subject':'某个课程', 'total_amount':100, 'pay_type':1, 'courses':[1,2,3]}
# 高级用法 传入的courses=[1,2,3]---->courses=[id为1的课程对象,id为2的课程对象,id为3的课程对象]
courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True) # 多条数据加上many=True
class Meta:
model = Order
fields = ['subject', 'total_amount', 'pay_type', 'courses']
def _check_price(self, attrs):
# 取出前端传入的总价格
total_amount = attrs.get('total_amount')
# 根据传入的课程,计算出总价格,做比较
courses = attrs.get('courses') # 此时就已经是一个个对象了
# 根据列表for循环其中的对象计算总价格
real_total_amount = 0
for one_course in courses:
real_total_amount += one_course.price
if not total_amount == real_total_amount:
raise APIException('价格不一致')
def _get_order_no(self):
# 使用uuid生成订单号
return str(uuid.uuid4()) # uuids是对象,需要str转一下
def _get_user(self):
# 返回当前登录用户,只有登录后,执行认证类后保存用户信息
request = self.context.get('request')
return request.user
def _get_pay_url(self, out_trade_no, total_amount, subject):
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=out_trade_no, # 订单号,唯一
total_amount=float(total_amount),
subject=subject,
return_url="https://example.com",
notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
pay_url = gateway + order_string
return pay_url
def validate(self, attrs):
# 1)订单总价校验
self._check_price(attrs)
# 2)生成订单号:唯一的
out_trade_no = self._get_order_no()
# 3)获取支付用户:request.user
user = self._get_user()
# 4)支付链接生成:支付宝支付链接
pay_url = self._get_pay_url(out_trade_no, attrs.get('total_amount'), attrs.get('subject'))
# 5)入库(两个表)的信息准备
attrs['user'] = user
attrs['out_trade_no'] = out_trade_no
# attrs={subject, total_amount, pay_type, courses, user, out_trade_no}
self.context['pay_url'] = pay_url
return attrs
def create(self, validated_data):
# {subject, total_amount, pay_type, courses, user, out_trade_no}
courses = validated_data.pop('courses')
# 增加订单表数据
order = Order.objects.create(**validated_data)
# 增加订单详情表的数据,可能有多门课程,使用for循环
for course in courses:
OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
return order
路由
from .views import AlipayView, PayOrderView
from rest_framework.routers import SimpleRouter
router = SimpleRouter()
router.register('pay', PayOrderView, 'pay')
urlpatterns = [
]
urlpatterns += router.urls
六、下单接口前端
课程主页或是详情页或者搜索页
CourseDetail.vue
<template>
...
<!-- 所有的立即购买都需要加上点击事件 -->
<button class="buy-now" @click="handleGoPay">立即购买</button>
...
</template>
<script>
export default {
methods: {
// 给购买按钮增加点击事件
handleGoPay(){
// 1 判断是否登录,必须登陆后才能购买
let token= this.$cookies.get('token') // let,在这里使用let表示token是局部变量,只能在这方法中使用
if(token){
// 向后端发起订单请求
this.$axios.post(`${this.$settings.BASE_URL}order/pay/`,{
subject:this.course_info.name,// course_info课程的信息,是created中像后端发请求,拿取课程数据后赋值的变量
total_amount:this.course_info.price,
pay_type:1,
courses:[this.course_info.id], // 前端的数据中是字典,只有id这个key,没有pk
},{
headers:{ // 请求头的数据
Authorization:'jwt '+token
}
}).then(res=>{
if(res.data.code==100){
let pay_url=res.data.pay_url
// 打开这个地址,在当前页面中打开
open(pay_url,'_self')
}else {
this.$message('下单失败')
}
})
}else {
// 还可以弹出登录组件
this.$message('您没有登录,请先登录')
}
},
}
}
</script>
七、支付宝支付成功后的get回调和post回调
7.1 之前的代码修改
settings/dev.py
# 上线后必须换成公网地址
## 支付宝的设置
# 后台基URL
BACKEND_URL = 'http://127.0.0.1:8000'
# 前台基URL
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付宝同步异步回调接口配置
# 后台异步回调接口
NOTIFY_URL = BACKEND_URL + "/api/v1/order/success/" # post回调地址--->配合接口--->修改订单状态
# 前台同步回调接口,没有 / 结尾
RETURN_URL = LUFFY_URL + "/pay/success" # 给用户看的,Vue的组件
order/serializer.py
from django.conf import settings
def _get_pay_url(self, out_trade_no, total_amount, subject):
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=out_trade_no, # 订单号,唯一
total_amount=float(total_amount),
subject=subject,
return_url=settings.RETURN_URL, # get回调地址,支付宝前端回调的
notify_url=settings.NOTIFY_URL # post回调地址,支付宝会向这个接口发送post请求,告诉咱们支付成功了
)
pay_url = gateway + order_string
return pay_url
7.2 前端回调页面--支付成功页面
<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/Header"
export default {
name: "Success",
data() {
return {
result: {},
};
},
created() {
// 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('&') : []; // ['a=1', 'b=2']
//逐个将每一项添加到args对象中
for (let i = 0; i < items.length; i++) { // 第一次循环a=1,第二次b=2
let k_v = items[i].split('='); // ['a', '1']
//解码操作,因为查询字符串经过编码的
if (k_v.length >= 2) {
// url编码反解
let k = decodeURIComponent(k_v[0]);
this.result[k] = decodeURIComponent(k_v[1]);
// 没有url编码反解
// this.result[k_v[0]] = k_v[1];
}
}
// 解析后的结果
// console.log(this.result);
// 把地址栏上面的支付结果,再get请求转发给后端
this.$axios({
url: this.$settings.BASE_URL + 'order/success/' + location.search,
method: 'get',
}).then(response => {
if (response.data.code !== 100) {
// console.log(res.data);
alert('暂时未收到您的付款,请稍后刷新再试')
}
}).catch(() => {
console.log('支付结果同步失败');
})
},
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>
路由
import PaySuccess from "@/views/PaySuccess.vue";
const routes = [
// ...
{
path: '/pay/success', // 支付页面组件
name: 'pay_success',
component: PaySuccess
},
]
7.3 前端回调get请求
路由
urlpatterns = [
path('success/', SuccessView.as_view({'get': 'get', 'post': 'post'})),
]
order/views.py
# 后端支付宝回调接口
class SuccessView(ViewSet):
# 要不要加认证类,不要加认证类!!!,加上后post请求永远不会成功,支付宝没有登录的token
def get(self, request, *args, **kwargs):
# 前端支付成功页面的get请求,检查表中是不是真的更改数据
try:
out_trade_no = request.query_params.get('out_trade_no')
# 直接查询订单存在,并且后端已经修改了支付状态的数据对象(支付宝已经调用了post修改状态了)
Order.objects.get(out_trade_no=out_trade_no, order_status=1)
return APIResponse()
except Exception as e:
return APIResponse(code=101, msg='订单未支付')
7.4 后端支付宝回调接口
# 因为咱们 地址是 127.0.0.1 ,支付宝回调,只能回调公网地址,调不回我们这,我们收不到支付宝的回调--->
解决方案:
1 使用公网ip部署项目,买个服务器
2 使用内网穿透:花生壳、...(就是个软件)
内网穿透方案:https://zhuanlan.zhihu.com/p/370483324
# 后端支付宝回调接口
class SuccessView(ViewSet):
# 要不要加认证类,不要加认证类!!!,加上后post请求永远不会成功,支付宝没有登录的token
# ...
def post(self, request, *args, **kwargs):
try:
# 支付宝回调的post请求给我们--->数据携带再请求体中--->多种格式:urlencoded(QueryDict格式)、formdata(QueryDict格式跟文件二进制格式)、json(字典格式)
# 支付宝的post是urlencoded -->request.data是QueryDict格式,使用dict做成字典格式
result_data = request.data.dict()
out_trade_no = result_data.get('out_trade_no')
signature = result_data.pop('sign')
result = alipay.verify(result_data, signature) # 验签通过表示是支付宝给的,不是别人伪造的
print(result_data)
# trade_status 确认订单付款成功了
if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
# 完成订单修改:订单状态、流水号、支付时间
Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1)
# Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1, trade_no=result_data.get('trade_no'), pay_time=result_data.get('timestamp'))
# 完成日志记录
logger.warning('%s订单支付成功' % out_trade_no)
return Response('success') # 给支付宝的返回必须这样,不能有别的东西
else:
logger.error('%s订单支付失败' % out_trade_no)
except:
pass
return Response('failed')