支付宝支付
一、沙箱环境
1、逻辑
# 1 前端点击立即购买---》向咱们后端发送请求---》咱们后端生成一个支付链接(微信,支付宝支付)---》跳到不同的支付链接地址---》输入支付宝账号密码付款(手机扫码付款)----》付款成功---》支付宝收到了我们的付款---》
跳转回我们自己的项目---》支付宝会调用咱们后端的某个接口通知我们付款成功---》我们收到通知,就把订单状态改为已经支付 # 2 不同的付款 -微信支付:工商注册(营业执照),真正备案过的网址,没有测试环境 -支付宝支付:只支持, 工商注册(企业),沙箱环境(没有商户号的前提下测试)、、 # 3 营业执照---》申请商家账号:2088102176466324---》使用商家账号,申请应用---》应用名称+应用id号 -应用id号咱们付款需要 -公司不需要你来做 -最终:APPID 2016092000554611 # 4 使用支付宝sdk(第三方:官方的api封装的),生成支付链接 pip install python-alipay-sdk --upgrade # 生成公钥 私钥----》支付宝工具帮咱们生成:https://opendocs.alipay.com/common/02kipl # 我们的公钥---》配置到支付宝平台---》生成一个支付宝公钥 # 我们用支付宝公钥+我们私钥(开放平台密钥工具) 做加密和认证 # https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667
2、代码
from alipay import AliPay, DCAliPay, ISVAliPay from alipay.utils import AliPayConfig # 支付宝网页下载的证书不能直接被使用,需要加上头尾 # 你可以在此处找到例子: tests/certs/ali/ali_private_key.pem app_private_key_string = open("./pri").read() alipay_public_key_string = open("./aipay_pub").read() print(app_private_key_string) alipay = AliPay( appid="9021000130615836", 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) # 可选,请求超时时间 ) order_string = alipay.api_alipay_trade_page_pay( out_trade_no="0x1212", total_amount=999, subject='性感内衣', return_url="https://example.com", notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url ) # order_string = alipay.api_alipay_trade_wap_pay( # out_trade_no="20161112", # total_amount=0.01, # subject='性感内衣', # return_url="https://example.com", # notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url # ) print('https://openapi-sandbox.dl.alipaydev.com/gateway.do?' + order_string)
3、链接
App 支付客户端 DEMO&SDK - 支付宝文档中心 (alipay.com)
支付宝支付 - 刘清政 - 博客园 (cnblogs.com)
alipay/alipay/__init__.py at master · fzlee/alipay (github.com)
二、封装进django项目
1、目录结构
alipay_pay pem alipay_public_key.pem app_private_key.pem __init__.py pay.py settings.py ##### __init__.py from .pay import alipay, gateway ###### pay.py from alipay import AliPay from . import settings # 支付对象 alipay = AliPay( appid=settings.APP_ID, app_notify_url=None, app_private_key_string=settings.APP_PRIVATE_KEY_STRING, alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING, sign_type=settings.SIGN, debug=settings.DEBUG ) # 支付网关 gateway = settings.GATEWAY ### settings.py 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 = '9021000129694319' # 加密方式 SIGN = 'RSA2' # 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False DEBUG = True # 支付网关 GATEWAY = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'
2、表关系
# 分析订单板块需要的表 -订单表:订单名称,订单总价格,订单状态。。。。 -订单详情表:一个订单有多个详情
models
from django.db import models # Create your models here. from user.models import User from courses.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__()
3、下单接口
# 1 用户点击前端,立即购买按钮----》(判断是否登录,登录成功后)---》向后端提交一个下单请求---携带的数据:{courses:[1,],total_amount:99,subject:xx课程,pay_type:1}--->post到后端---》 校验: # 1)订单总价校验 # 2)生成订单号:唯一的 # 3)支付用户:request.user # 4)支付链接生成:pay_type---》支付宝支付链接 # 5)入库(两个表)的信息准备 保存 返回给前端支付链接 # 新增接口(两个表) # 1 问题一:多条的话,要写many=True courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True) # 2 问题二:支付宝生成支付链接时,用float强转一下
序列化类
from rest_framework import serializers from .models import Order, OrderDetail from courses.models import Course from rest_framework.validators import ValidationError import uuid from libs.alipay_pay import gateway, alipay # 校验, 保存(重写create) class PaySerializer(serializers.ModelSerializer): # courses 不是表的字段,需要重写 # courses = serializers.ListField() # 不用这个方式,一会换成别的 [1,4] courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True) # [id为1的课程对象,id为4的课程对象] class Meta: model = Order fields = ['courses', 'total_amount', 'subject', 'pay_type'] def _check_amount(self, attrs): # 1 取出一个个课程,循环拿到总价格,跟传入的价格比较,如果一致,就ok,不一致就抛异常 course = attrs.get('courses') # PrimaryKeyRelatedField--->[课程对象,课程对象] total_amount = attrs.get('total_amount') total = 0 for course in course: total += course.price if total_amount != total: raise ValidationError('价格不一致') def _get_out_trade_no(self): return str(uuid.uuid4()) def _get_user(self): request = self.context.get('request') return request.user def _get_pay_url(self, out_trade_no, attrs): res = alipay.api_alipay_trade_page_pay( out_trade_no=out_trade_no, total_amount=float(attrs.get('total_amount')), subject=attrs.get('subject'), return_url="https://example.com", notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url ) return gateway + res def _before_create(self, pay_url, attrs, user, out_trade_no): # 1 把pay_url放到context中,供视图类中取 self.context['pay_url'] = pay_url # 2 往attrs中加数据,后续都要存到表中 attrs['user'] = user attrs['out_trade_no'] = out_trade_no def validate(self, attrs): # 1)订单总价校验 self._check_amount(attrs) # 2)生成订单号:唯一的 out_trade_no = self._get_out_trade_no() # 3)支付用户:request.user user = self._get_user() # 4)支付链接生成:pay_type---》支付宝支付链接 pay_url = self._get_pay_url(out_trade_no, attrs) # 5)入库(两个表)的信息准备 self._before_create(pay_url, attrs, user, out_trade_no) return attrs def create(self, validated_data): # {courses,total_amount,subject,pay_type,user,out_trade_no} # 存到库中 courses = validated_data.pop('courses') # 1 Order表 order = Order.objects.create(**validated_data) # 2 存订单详情表 for course in courses: OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price) return order
视图类
from rest_framework.viewsets import GenericViewSet from rest_framework.mixins import CreateModelMixin from .serializer import PaySerializer from .models import Order from utils.common_response import APIResponse from rest_framework_jwt.authentication import JSONWebTokenAuthentication from rest_framework.permissions import IsAuthenticated class PayView(GenericViewSet, CreateModelMixin): # 加认证 authentication_classes = [JSONWebTokenAuthentication] permission_classes = [IsAuthenticated] serializer_class = PaySerializer def create(self, request, *args, **kwargs): ser = self.get_serializer(data=request.data, context={'request': request}) # ser = PaySerializer(data=request.data,context={'request':request}) ser.is_valid(raise_exception=True) super().perform_create(ser) pay_url = ser.context.get('pay_url') return APIResponse(msg='下单成功', pay_url=pay_url)
路由
from rest_framework.routers import SimpleRouter from .views import PayView router = SimpleRouter() # 127.0.0.1:8000/api/v1/orders/pay/---->post router.register('pay', PayView, 'pay') urlpatterns = [ ] urlpatterns += router.urls
前端支付功能
# 课程详情页 go_pay() { let token = this.$cookies.get('token') console.log(token) if (token) { this.$axios({ url: this.$settings.BASE_URL + 'orders/pay/', method: 'post', data: { "courses": [this.course_id], "total_amount": this.course_info.price, "subject": this.course_info.name, "pay_type": 1 }, headers: {'Authorization': 'jwt ' + token} }).then(res => { if (res.data.code == 100) { let pay_url = res.data.pay_url // 当前页面中打开pay_url 地址 open(pay_url, '_self'); } }) } else { this.$message('您没有登录,请先登录') } },
# 前台路由 { path: '/pay/success', name: 'success', component: PaySuccessView }, # 支付宝回调地址 def _get_pay_url(self, out_trade_no, attrs): res = alipay.api_alipay_trade_page_pay( out_trade_no=out_trade_no, total_amount=float(attrs.get('total_amount')), subject=attrs.get('subject'), return_url=settings.RETURN_URL, # 回调前台 notify_url= settings.NOTIFY_URL # 回调后台 ) ## settings中 # 后台基URL BACKEND_URL = 'http://127.0.0.1:8000' # 前台基URL LUFFY_URL = 'http://127.0.0.1:8080' # 支付宝同步异步回调接口配置 # 后台异步回调接口 NOTIFY_URL = BACKEND_URL + "/order/success/" # 前台同步回调接口,没有 / 结尾 RETURN_URL = LUFFY_URL + "/pay/success"
购买成功页面
PaySuccessView
<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 + 'orders/success/' + location.search, method: 'get', }).then(response => { if (response.data.code == 100) { this.$message('恭喜您购买成功') } else { alert(response.data.msg) } }).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>
from utils.common_logger import logger from rest_framework.response import Response class PaySuccess(ViewSet): def list(self, request, *args, **kwargs): out_trade_no = request.query_params.get('out_trade_no') res = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).exists() if res: return APIResponse() else: return APIResponse(code=101, msg='暂未收到你的付款,请稍后再试') # 支付宝回调用,修改订单状态的 def create(self, request, *args, **kwargs): try: result_data = request.data.dict() out_trade_no = result_data.get('out_trade_no') signature = result_data.pop('sign') from libs.alipay_pay import alipay result = alipay.verify(result_data, signature) 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) # 完成日志记录 logger.warning('%s订单支付成功' % out_trade_no) return Response('success') else: logger.error('%s订单支付失败' % out_trade_no) except: pass return Response('failed') # 支付宝没有收到success,在48小时内,给你发送8次