day88:luffy:支付宝同步结果通知&接收异步支付结果&用户购买记录&我的订单
当用户输入用户名和密码确认支付的时候,支付宝会给vue前端回复一个url,这个url很长,后面包含了很多参数,参数如下所示:
http://www.luffycity.cn:8080/payments/result? charset=utf8& out_trade_no=20190929151453000001000020& method=alipay.trade.page.pay.return& total_amount=310.00& sign=kebIZBI%2FpCNXmCivfJPPw21gcobulPZoSh%2BXiHR8l6cgexQi2STG4AZgr%2FEUhvc5kEMacJLvCmBaw1Wqo4WK3sPzbUaPmzq3NshUNzYK2lWTsmOauidNxlk1bK0Q%2FANBfQUkmj6TQjyB5T9QqEnS80KFsDrGrasU%2B%2Fz9W%2FjOCLrSji6TnKhRkI9pqBMdw823ABU75b7zOtXzcXKduO%2B6vsXVvluMzedss9dHs1celxPAWQV9jcKjzq%2F1bPbZcmgAGNQQecoJ%2BFSc3uTmTk24uV39PM54LIlg8aeRlkPNjvhBkJh%2FG0%2BURNDdG2593IFIF%2BUqoU%2F7ixm19dX222GCWg%3D%3D& trade_no=2019092922001439881000120282& auth_app_id=2016091600523592& version=1.0& app_id=2016091600523592& sign_type=RSA2& seller_id=2088102175868026& timestamp=2019-09-29%2015%3A15%3A53
支付宝将参数传递给了vue前端,vue前端要这些参数发送给后端,让后端去对这些参数做一个校验。判断这个url是不是支付宝发过来的
from django.urls import path,re_path from . import views urlpatterns = [ path('result/',views.AlipayResultView.as_view(),) ]
payment/view.py
class AlipayResultView(APIView): permission_classes = [IsAuthenticated, ] def get(self,request): # 1.创建alipay对象 alipay = AliPay( appid=settings.ALIAPY_CONFIG['appid'], app_notify_url=None, # 默认回调url app_private_key_string=open(settings.ALIAPY_CONFIG['app_private_key_path']).read(), # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥, alipay_public_key_string=open(settings.ALIAPY_CONFIG['alipay_public_key_path']).read(), sign_type=settings.ALIAPY_CONFIG['sign_type'], # RSA 或者 RSA2 debug=settings.ALIAPY_CONFIG['debug'], # 默认False ) # 2.校验支付宝响应数据 data = request.query_params.dict() # 获取那一大堆的参数 out_trade_no = data.get('out_trade_no') # 获取商户订单号 sign = data.pop('sign') # 获取签名 success = alipay.verify(data,sign) # 通过参数和签名来验证那一堆参数是不是支付宝发过来的 if not success: logger.error('%s,支付宝响应数据校验失败' % out_trade_no) return Response('支付宝响应数据校验失败',status=status.HTTP_400_BAD_REQUEST) # 响应结果 return Response({'msg':'ok','data':res_data})
3.vue前端发送请求验证get参数
如果校验没有问题 就可以显示购买成功了。
Success.vue
created(){ // 把地址栏上面的支付结果,转发给后端 this.send_alipay_params(); methods:{ send_alipay_params(){ let token = localStorage.token || sessionStorage.token; this.$axios.get(`${this.$settings.Host}/payment/result/${location.search}`,{ headers:{ 'Authorization':'jwt ' + token } }).then((res)=>{ // 如果后端验证这些参数没有问题 购买成功页面需要的参数就可以传递过来了 this.pay_time = res.data.data.pay_time; // 支付时间 this.course_list = res.data.data.course_list; // 购买课程列表 this.total_real_price = res.data.data.total_real_price; // 课程总真实价格 }).catch((error)=>{ this.$message.error(error.response.data.msg); }) },
class UserCourse(BaseModel): """用户的课程购买记录""" pay_choices = ( (1, '用户购买'), (2, '免费活动'), (3, '活动赠品'), (4, '系统赠送'), ) user = models.ForeignKey(User, related_name='user_courses', on_delete=models.DO_NOTHING, verbose_name="用户") course = models.ForeignKey(Course, related_name='course_users', on_delete=models.DO_NOTHING, verbose_name="课程") trade_no = models.CharField(max_length=128, null=True, blank=True, verbose_name="支付平台的流水号", help_text="将来依靠流水号到支付平台查账单") buy_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="购买方式") pay_time = models.DateTimeField(null=True, blank=True, verbose_name="购买时间") out_time = models.DateTimeField(null=True, blank=True, verbose_name="过期时间") # null表示永不过期 class Meta: db_table = 'ly_user_course' verbose_name = '课程购买记录' verbose_name_plural = verbose_name
class AlipayResultView(APIView): permission_classes = [IsAuthenticated, ] def post(self,request): # 创建alipay对象 alipay = AliPay( appid=settings.ALIAPY_CONFIG['appid'], app_notify_url=None, # 默认回调url app_private_key_string=open(settings.ALIAPY_CONFIG['app_private_key_path']).read(), # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥, alipay_public_key_string=open(settings.ALIAPY_CONFIG['alipay_public_key_path']).read(), sign_type=settings.ALIAPY_CONFIG['sign_type'], # RSA 或者 RSA2 debug=settings.ALIAPY_CONFIG['debug'], # 默认False ) # 校验支付宝响应数据 data = request.data.dict() sign = data.pop('sign') success = alipay.verify(data,sign) if success and data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"): self.change_order_status(data) return Response('success')
1.修改订单状态
2.扣除优惠劵 积分
3.清空购物车数据和结算页面全部数据
4.将相关信息存到用户购买记录表中
payment/views.py
class AlipayResultView(APIView): def change_order_status(self,data): # 修改订单状态 with transaction.atomic(): out_trade_no = data.get('out_trade_no') trade_no = data.get('trade_no') # A.修改订单状态 # 1.获取当前订单号的订单对象 order_obj = Order.objects.get(order_number=out_trade_no) # 2.将当前订单的订单状态改为1:已支付 order_obj.order_status = 1 # 3.保存订单信息 order_obj.save() # B.修改优惠券的使用状态 if order_obj.coupon > 0: # 如果用户使用了优惠劵 # 1.获取用户使用的那张优惠劵对象 user_coupon_obj = UserCoupon.objects.get(is_use=False, id=order_obj.coupon) # 2.将用户使用的那张优惠劵的状态由未使用改为已使用 user_coupon_obj.is_use = True # 3.保存用户的优惠劵的信息 user_coupon_obj.save() # C.支付成功,用户积分应该对应的扣除 # 1.查询当前订单使用了多少积分 use_credit = order_obj.credit # 2.查询到用户的总积分数减去使用的积分数得到用户的剩余积分数 self.request.user.credit -= use_credit # 3.保存用户的积分信息 self.request.user.save() # D.保存支付宝的交易流水号(购买记录表) # 1.通过订单对象反向查询到所有的订单详情对象 order_detail_objs = order_obj.order_courses.all() # 2.获取当前时间 now = datetime.datetime.now() # 购买成功 从redis中将课程的选中状态删除掉 conn = get_redis_connection('cart') pipe = conn.pipeline() pipe.delete('selected_cart_%s' % self.request.user.id) # 需要给购买成功页面(Success.vue返回的数据) res_data = { 'pay_time': now, 'course_list': [], 'total_real_price': order_obj.real_price, } for order_detail in order_detail_objs: # 购买成功 课程学习的学生数+1 course = order_detail.course course.students += 1 course.save() # 购买成功的课程应该显示课程列表的每个课程 res_data['course_list'].append(course.name) # 从课程详情页获取当前课程的有效期数值 expire_id = order_detail.expire if expire_id > 0: # 如果不是永久有效 # 根据expire_id查询到课程有效期model对象 expire_obj = CourseExpire.objects.get(id=expire_id) # 查询到课程的有效期(天数) expire_time = expire_obj.expire_time # 计算课程的过期时间 out_time = now + datetime.timedelta(days=expire_time) else: # 如果是永久有效,就没有过期时间 out_time = None # 一切处理完毕,将相关信息存到用户购买记录表中 UserCourse.objects.create(**{ 'user':self.request.user, 'course':course, 'trade_no':trade_no, 'buy_type':1, 'pay_time':now, 'out_time':out_time, }) # 购物车redis数据删除 pipe.hdel('cart_%s' % self.request.user.id, course.id) pipe.execute() return res_data
order/serializers.py
def create: order_obj.coupon = coupon_id order_obj.credit = credit order_obj.save()
Myorder.vue
<template> <div class="user-order"> <Vheader/> <div class="main"> <div class="banner"></div> <div class="profile"> <div class="profile-info"> <div class="avatar"><img class="newImg" width="100%" alt="" src="../../static/img/logo@2x.png"></div> <span class="user-name">吴某某</span> <span class="user-job">北京市 | 程序员</span> </div> <ul class="my-item"> <li>我的账户</li> <li class="active">我的订单</li> <li>个人资料</li> <li>账号安全</li> </ul> </div> <div class="user-data"> <ul class="nav"> <li class="order-info">订单</li> <li class="course-expire">有效期</li> <li class="course-price">课程价格</li> <li class="real-price">实付金额</li> <li class="order-status">交易状态</li> <li class="order-do">交易操作</li> </ul> <div class="my-order-item" v-for="(order_obj,index) in order_list" :key="index"> <div class="user-data-header"> <span class="order-time">xxxx</span> <span class="order-num">订单号: <span class="my-older-number">xxxx</span> </span> </div> <ul class="nav user-data-list" v-for="(course_obj,course_index) in order_obj.order_detail_data"> <li class="order-info"> <img :src="course_obj.course_img" alt=""> <div class="order-info-title"> <p class="course-title">xxxx</p> <p class="price-service">xxxx</p> </div> </li> <li class="course-expire">xxxx</li> <li class="course-price">xxxx</li> <li class="real-price">xxxx</li> <li class="order-status">xxxx</li> <li class="order-do"> <span class="btn btn2" v-if="order_obj.get_order_status_display==='已支付'">去学习</span> <span class="btn btn2" v-else-if="order_obj.get_order_status_display==='未支付'" @click="go_pay(order_obj.order_number)">去付款</span> <span class="btn btn2" v-else-if="order_obj.get_order_status_display==='超时取消'">超时取消</span> <span class="btn btn2" v-else>已取消</span> </li> </ul> </div> </div> </div> <Footer/> </div> </template> <script> import Vheader from "./common/Vheader" import Footer from "./common/Footer" export default{ name:"Myorder", data(){ return { }; }, created(){ }, methods:{ }, components:{ Vheader, Footer, } } </script> <style scoped> .main .banner{ width: 100%; height: 324px; background: url(../../static/img/my_bkging.0648ebe.png) no-repeat; background-size: cover; z-index: 1; } .profile{ width: 1200px; margin: 0 auto; } .profile-info{ text-align: center; margin-top: -80px; } .avatar{ width: 120px; height: 120px; border-radius: 60px; overflow: hidden; margin: 0 auto; } .user-name{ display: block; font-size: 24px; color: #4a4a4a; margin-top: 14px; } .user-job{ display: block; font-size: 11px; color: #9b9b9b; } .my-item{ list-style: none; line-height: 1.42857143; color: #333; width: 474px; height: 31px; display: -ms-flexbox; display: flex; cursor: pointer; margin: 41px auto 0; -ms-flex-pack: justify; justify-content: space-between; } .my-item .active{ border-bottom: 1px solid #000; } .user-data{ width: 1200px; height: auto; margin: 0 auto; padding-top: 30px; border-top: 1px solid #e8e8e8; margin-bottom: 63px; } .nav{ width: 100%; height: 60px; background: #e9e9e9; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; } .nav li{ margin-left: 20px; margin-right: 28px; height: 60px; line-height: 60px; list-style: none; font-size: 13px; color: #333; border-bottom: 1px solid #e9e9e9; width: 160px; } .nav .order-info{ width: 325px; } .nav .course-expire{ width: 60px; } .nav .course-price{ width: 130px; } .user-data-header{ display: flex; height: 44px; color: #4a4a4a; font-size: 14px; background: #f3f3f3; -ms-flex-align: center; align-items: center; } .order-time{ font-size: 12px; display: inline-block; margin-left: 20px; } .order-num{ font-size: 12px; display: inline-block; margin-left: 29px; } .user-data-list{ height: 100%; display: flex; } .user-data-list{ background: none; } .user-data-list li{ height: 60px; line-height: 60px; } .user-data-list .order-info{ display: flex; align-items: center; margin-right: 28px; } .user-data-list .order-info img{ max-width: 100px; max-height: 75px; margin-right: 22px; } .course-title{ width: 203px; font-size: 13px; color: #333; line-height: 5px; margin-top: -10px; } .order-info-title .price-service{ line-height: 18px; } .price-service{ font-size: 12px; color: #fa6240; padding: 0 5px; border: 1px solid #fa6240; border-radius: 4px; margin-top: 4px; position: absolute; } .order-info-title{ margin-top: -10px; } .user-data-list .course-expire{ font-size: 12px; color: #ff5502; width: 60px; text-align: center; } .btn { width: 100px; height: 32px; font-size: 14px; color: #fff; background: #ffc210; border-radius: 4px; border: none; outline: none; transition: all .25s ease; display: inline-block; line-height: 32px; text-align: center; cursor: pointer; } </style>
index.js
{ path: '/myorder/', component: Myorder },
users/urls.py
urlpatterns = [ path(r'myorder/', views.MyOrderView.as_view()), ]
users/views.py
class MyOrderView(ListAPIView): permission_classes = [IsAuthenticated, ] serializer_class = MyOrderModelSerializer def get_queryset(self): return Order.objects.filter(user=self.request.user)
user/serializers.py
class MyOrderModelSerializer(serializers.ModelSerializer): class Meta: model = Order fields = ['id', 'order_number' ,'pay_time', 'get_order_status_display', 'order_detail_data']
order/models.py
在我的订单页面中,需要展示一些数据
class Order(BaseModel): def order_detail_data(self): # 获取所有课程详情对象 order_detail_objs = self.order_courses.all() data_list = [] for order_detail in order_detail_objs: expire_id = order_detail.expire # 根据有效期来决定expire_text返回什么 if expire_id > 0: expire_obj = CourseExpire.objects.get(id=expire_id) expire_text = expire_obj.expire_text else: expire_text = '永久有效' # 每个课程应该包含的字段 order_dict = { 'course_img':contains.SERVER_ADDR + order_detail.course.course_img.url, 'course_name':order_detail.course.name, 'expire_text':expire_text, 'price':order_detail.price, 'real_price': self.real_price, 'discount_name':order_detail.discount_name, } # 将每个课程的详情信息添加到一个列表里返回给前端 data_list.append(order_dict) return data_list
Myorder.vue
// js get_order_data(){ // 检查当前访问者是否登录了! let token = localStorage.token || sessionStorage.token; this.$axios.get(`${this.$settings.Host}/users/myorder/`,{ headers:{ 'Authorization':'jwt ' + token } }).then((res)=>{ this.order_list = res.data; }).catch((error)=>{ })
<!-- html --> <div class="my-order-item" v-for="(order_obj,index) in order_list" :key="index"> <div class="user-data-header"> <span class="order-time">{{order_obj.pay_time.replace('T', ' ')}}</span> <span class="order-num">订单号: <span class="my-older-number">{{order_obj.order_number}}</span> </span> </div> <ul class="nav user-data-list" v-for="(course_obj,course_index) in order_obj.order_detail_data"> <li class="order-info"> <img :src="course_obj.course_img" alt=""> <div class="order-info-title"> <p class="course-title">{{course_obj.course_name}}</p> <p class="price-service">{{course_obj.discount_name}}</p> </div> </li> <li class="course-expire">{{course_obj.expire_text}}</li> <li class="course-price">{{course_obj.price}}</li> <li class="real-price">{{course_obj.real_price}}</li> <li class="order-status">{{order_obj.get_order_status_display}}</li> <li class="order-do"> <span class="btn btn2" v-if="order_obj.get_order_status_display==='已支付'">去学习</span> <span class="btn btn2" v-else-if="order_obj.get_order_status_display==='未支付'" @click="go_pay(order_obj.order_number)">去付款</span> <span class="btn btn2" v-else-if="order_obj.get_order_status_display==='超时取消'">超时取消</span> <span class="btn btn2" v-else>已取消</span> </li> </ul> </div>
Myorder.vue
<!-- html --> <span class="btn btn2" v-else-if="order_obj.get_order_status_display==='未支付'" @click="go_pay(order_obj.order_number)">去付款</span>
// js go_pay(order_number){ let token = localStorage.token || sessionStorage.token; this.$axios.get(`${this.$settings.Host}/payment/alipay/?order_number=${order_number}`,{ headers:{ 'Authorization':'jwt ' + token } }).then((res)=>{ location.href = res.data.url; }).catch((error)=>{ this.$message.error(error.response.data.msg); })