支付的实现一般我们开发中都是通过第三方支付平台来实现的!

目前国内外比较常见的第三方支付平台:

  1. 小额支付
  2. 1. 国内
  3. 微信支付,支付宝,京东支付,百度钱包,贝宝[paypal中文版]
  4. 2. 国外
  5. apple paypaypal[贝宝国际版],万事达信用卡
  6. 大额支付
  7. 银联支付

支付宝

支付宝开发平台登录

官网:https://open.alipay.com/platform/home.htm

公司以企业账号进行支付签约:https://b.alipay.com/signing/productDetailV2.htm?productId=I1011000290000001000

目前我们是作为开发者给项目测试支付功能,所以我们先采用支付平台提供的测试服务端使用测试账号进行功能测试。将来等企业账号申请支付签约通过以后,则可以直接修改代码中的配置信息,就可以在线上运营使用了。

chapter7.3-支付 - 图1

沙箱环境

  1. 真实的支付宝网关: https://openapi.alipay.com/gateway.do
  2. 沙箱的支付宝网关: https://openapi.alipaydev.com/gateway.do

支付宝开发者文档

电脑网站支付产品介绍https://opendocs.alipay.com/open/270

电脑网站支付流程的时序图:

chapter7.3-支付 - 图2

chapter7.3-支付 - 图3

开发支付功能

终端下创建新的git分支,并创建新的子应用。

  1. cd /home/moluo/Desktop/luffycity
  2. git checkout -b feature/payments
  3. cd luffycityapi/luffycityapi/apps
  4. python ../../manage.py startapp payments

注册子应用,settings/dev.py,代码:

  1. INSTALLED_APPS = [
  2. 。。。。
  3. 'payments',
  4. ]

子应用路由,payments/urls.py,代码:

  1. from django.urls import path,re_path
  2. from . import views
  3. urlpatterns = [
  4. ]

总路由,luffycityapi/urls.py,代码:

  1. path("payments/", include("payments.urls")),

配置秘钥

  1. RSA2算法:非对称加密算法。一般加密的密钥是成对出现的。
  2. 私钥用于解密,自己保存,
  3. 公钥用于加密,提供给别人。
  4. 目前所有的第三方支付基本都是使用RSA2算法加密。所以要开发支付宝这种第三方支付平台的支付接口基本都有2对密钥。
  5. 支付宝密钥对:支付宝公钥,支付宝密钥。
  6. 商户(应用)密钥对:应用公钥,应用私钥。
  7. 往往我们需要从支付宝的官网上获取支付宝的公钥,把自己的应用公钥填写到支付宝官网。

1. 生成应用的私钥和公钥

支付宝公钥

获取支付宝公钥:https://openhome.alipay.com/platform/appDaily.htm?tab=info

第一次需要启用RSA2秘钥( 公钥模式 )

chapter7.3-支付 - 图4

点击启用了以后,就可以新窗口下看到支付宝提供的公钥。

chapter7.3-支付 - 图5

得到了公钥以后,复制保存到luffycityapi/apps/payments/keys/alipay_public_key.pem,这个pem文件一般就直接保存到当前子应用目录下的keys目录下。

内容格式如下:

  1. -----BEGIN PUBLIC KEY-----
  2. 支付宝公钥信息[不能手动调整,复制下来就原样不动]
  3. -----END PUBLIC KEY-----

效果如下:

chapter7.3-支付 - 图6

应用私钥

应用的公钥和私钥,支付宝本身已经提供了。所以,我们直接复制到本地保存起来即可。

注意:我们使用的是非java语言,所以不要选错了!

chapter7.3-支付 - 图7

luffycityapi/apps/payments/keys/app_private_key.pem,代码:

  1. -----BEGIN RSA PRIVATE KEY-----
  2. 应用私钥信息[不能手动调整,复制下来就原样不动]
  3. -----END RSA PRIVATE KEY-----

效果如下:

chapter7.3-支付 - 图8

如果要保留公钥到本地将来作为备份,或者用着其他的支付方式,可以保存,但是不推荐。

luffycityapi/apps/payments/keys/app_public_key.pem,代码:

  1. -----BEGIN PUBLIC KEY-----
  2. 应用公钥信息[不能手动调整,复制下来就原样不动]
  3. -----END PUBLIC KEY-----

效果:

chapter7.3-支付 - 图9

4. 使用支付宝的sdk开发支付接口

SDK:https://opendocs.alipay.com/open/54/103419

python版本的支付宝SDK文档:https://github.com/fzlee/alipay/blob/master/README.zh-hans.md

安装命令:

  1. pip install python-alipay-sdk --upgrade

使用文档:https://github.com/fzlee/alipay/blob/master/docs/apis_new.zh-hans.md

调整原来下单的序列化器,不在序列化器中返回支付信息,而是由客户端根据订单号请求服务端生成支付信息。

orders/serializers.py,代码:

  1. import logging
  2. import constants
  3. from datetime import datetime
  4. from rest_framework import serializers
  5. from django_redis import get_redis_connection
  6. from django.db import transaction
  7. from .models import Order, OrderDetail, Course
  8. from coupon.models import CouponLog
  9. logger = logging.getLogger("django")
  10. class OrderModelSerializer(serializers.ModelSerializer):
  11. user_coupon_id = serializers.IntegerField(write_only=True, default=-1)
  12. class Meta:
  13. model = Order
  14. fields = ["pay_type", "id", "order_number", "user_coupon_id", "credit"]
  15. read_only_fields = ["id", "order_number"]
  16. extra_kwargs = {
  17. "pay_type": {"write_only": True},
  18. "credit": {"write_only": True},
  19. }
  20. def create(self, validated_data):
  21. """创建订单"""
  22. redis = get_redis_connection("cart")
  23. user = self.context["request"].user
  24. user_id = user.id
  25. # 判断用户如果使用了优惠券,则优惠券需要判断验证
  26. user_coupon_id = validated_data.get("user_coupon_id")
  27. # 本次下单时,用户使用的优惠券
  28. user_coupon = None
  29. if user_coupon_id != -1:
  30. user_coupon = CouponLog.objects.filter(pk=user_coupon_id, user_id=user_id).first()
  31. # 本次下单时使用的积分数量
  32. use_credit = validated_data.get("credit", 0)
  33. if use_credit > 0 and use_credit > user.credit:
  34. raise serializers.ValidationError(detail="您拥有的积分不足以抵扣本次下单的积分,请重新下单!")
  35. # 开启事务操作,保证下单过程中的所有数据库的原子性
  36. with transaction.atomic():
  37. # 设置事务的回滚点标记
  38. t1 = transaction.savepoint()
  39. try:
  40. # 创建订单记录
  41. order = Order.objects.create(
  42. name="购买课程", # 订单标题
  43. user_id=user_id, # 当前下单的用户ID
  44. # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
  45. order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
  46. pay_type=validated_data.get("pay_type"), # 支付方式
  47. )
  48. # 记录本次下单的商品列表
  49. cart_hash = redis.hgetall(f"cart_{user_id}")
  50. if len(cart_hash) < 1:
  51. raise serializers.ValidationError(detail="购物车没有要下单的商品")
  52. # 提取购物车中所有勾选状态为b'1'的商品
  53. course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']
  54. # 添加订单与课程的关系
  55. course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
  56. detail_list = []
  57. total_price = 0 # 本次订单的总价格
  58. real_price = 0 # 本次订单的实付总价
  59. # 用户使用优惠券或积分以后,需要在服务端计算本次使用优惠券或积分的最大优惠额度
  60. total_discount_price = 0 # 总优惠价格
  61. max_discount_course = None # 享受最大优惠的课程
  62. # 本次下单最多可以抵扣的积分
  63. max_use_credit = 0
  64. for course in course_list:
  65. discount_price = course.discount.get("price", None) # 获取课程原价
  66. if discount_price is not None:
  67. discount_price = float(discount_price)
  68. discount_name = course.discount.get("type", "")
  69. detail_list.append(OrderDetail(
  70. order=order,
  71. course=course,
  72. name=course.name,
  73. price=course.price,
  74. real_price=course.price if discount_price is None else discount_price,
  75. discount_name=discount_name,
  76. ))
  77. # 统计订单的总价和实付总价
  78. total_price += float(course.price)
  79. real_price += float(course.price if discount_price is None else discount_price)
  80. # 在用户使用了优惠券,并且当前课程没有参与其他优惠活动时,找到最佳优惠课程
  81. if user_coupon and discount_price is None:
  82. if max_discount_course is None:
  83. max_discount_course = course
  84. else:
  85. if course.price >= max_discount_course.price:
  86. max_discount_course = course
  87. # 添加每个课程的可用积分
  88. if use_credit > 0 and course.credit > 0:
  89. max_use_credit += course.credit
  90. # 在用户使用了优惠券以后,根据循环中得到的最佳优惠课程进行计算最终抵扣金额
  91. if user_coupon:
  92. # 优惠公式
  93. sale = float(user_coupon.coupon.sale[1:])
  94. if user_coupon.coupon.discount == 1:
  95. """减免优惠券"""
  96. total_discount_price = sale
  97. elif user_coupon.coupon.discount == 2:
  98. """折扣优惠券"""
  99. total_discount_price = float(max_discount_course.price) * (1 - sale)
  100. if use_credit > 0:
  101. if max_use_credit < use_credit:
  102. raise serializers.ValidationError(detail="本次使用的抵扣积分数额超过了限制!")
  103. # 当前订单添加积分抵扣的数量
  104. order.credit = use_credit
  105. total_discount_price = float(use_credit / constants.CREDIT_TO_MONEY)
  106. # todo 扣除用户拥有的积分,后续在订单超时未支付,则返还订单中对应数量的积分给用户。如果订单成功支付,则添加一个积分流水记录。
  107. user.credit = user.credit - use_credit
  108. user.save()
  109. # 一次性批量添加本次下单的商品记录
  110. OrderDetail.objects.bulk_create(detail_list)
  111. # 保存订单的总价格和实付价格
  112. order.total_price = real_price
  113. order.real_price = float(real_price - total_discount_price)
  114. order.save()
  115. # 删除购物车中被勾选的商品,保留没有被勾选的商品信息
  116. cart = {key: value for key, value in cart_hash.items() if value == b'0'}
  117. pipe = redis.pipeline()
  118. pipe.multi()
  119. # 删除原来的购物车
  120. pipe.delete(f"cart_{user_id}")
  121. # 重新把未勾选的商品记录到购物车中
  122. if cart:
  123. pipe.hmset(f"cart_{user_id}", cart)
  124. pipe.execute()
  125. # 如果有使用了优惠券,则把优惠券和当前订单进行绑定
  126. if user_coupon:
  127. user_coupon.order = order
  128. user_coupon.save()
  129. # 把优惠券从redis中移除
  130. redis = get_redis_connection("coupon")
  131. redis.delete(f"{user_id}:{user_coupon_id}")
  132. return order
  133. except Exception as e:
  134. # 1. 记录日志
  135. logger.error(f"订单创建失败:{e}")
  136. # 2. 事务回滚
  137. transaction.savepoint_rollback(t1)
  138. # 3. 抛出异常,通知视图返回错误提示
  139. raise serializers.ValidationError(detail="订单创建失败!")

后端提供发起支付的api接口

payments/views.py,代码:

  1. from django.conf import settings
  2. from rest_framework.viewsets import ViewSet
  3. from rest_framework.response import Response
  4. from orders.models import Order
  5. from alipay import AliPay
  6. from alipay.utils import AliPayConfig
  7. # Create your views here.
  8. class AlipayAPIViewSet(ViewSet):
  9. """支付宝接口"""
  10. def link(self, request, order_number):
  11. """生成支付宝支付链接信息"""
  12. try:
  13. order = Order.objects.get(order_number=order_number)
  14. if order.order_status > 0:
  15. return Response({"message": "对不起,当前订单不能重复支付或订单已超时!"})
  16. except Order.DoesNotExist:
  17. return Response({"message": "对不起,当前订单不存在!"})
  18. # 读取支付宝公钥与商户私钥
  19. app_private_key_string = open(settings.ALIPAY["app_private_key_path"]).read()
  20. alipay_public_key_string = open(settings.ALIPAY["alipay_public_key_path"]).read()
  21. # 创建alipay SDK操作对象
  22. alipay = AliPay(
  23. appid=settings.ALIPAY["appid"],
  24. app_notify_url=settings.ALIPAY["notify_url"], # 默认全局回调 url
  25. app_private_key_string=app_private_key_string,
  26. # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
  27. alipay_public_key_string=alipay_public_key_string,
  28. sign_type=settings.ALIPAY["sign_type"], # RSA2
  29. debug=settings.ALIPAY["debug"], # 默认 False,沙箱模式下必须设置为True
  30. verbose=settings.ALIPAY["verbose"], # 输出调试数据
  31. config=AliPayConfig(timeout=settings.ALIPAY["timeout"]) # 可选,请求超时时间,单位:秒
  32. )
  33. # 生成支付信息
  34. order_string = alipay.client_api(
  35. "alipay.trade.page.pay", # 接口名称
  36. biz_content={
  37. "out_trade_no": order_number, # 订单号
  38. "total_amount": float(order.real_price), # 订单金额 单位:元
  39. "subject": order.name, # 订单标题
  40. "product_code": "FAST_INSTANT_TRADE_PAY", # 产品码,目前只能支持 FAST_INSTANT_TRADE_PAY
  41. },
  42. return_url=settings.ALIPAY["return_url"], # 可选,同步回调地址,必须填写客户端的路径
  43. notify_url=settings.ALIPAY["notify_url"] # 可选,不填则使用采用全局默认notify_url,必须填写服务端的路径
  44. )
  45. # 拼接完整的支付链接
  46. link = f"{settings.ALIPAY['gateway']}?{order_string}"
  47. return Response({
  48. "pay_type": 0, # 支付类型
  49. "get_pay_type_display": "支付宝", # 支付类型的提示
  50. "link": link # 支付连接地址
  51. })

在配置文件中编辑支付宝的配置信息[实际的值根据自己的账号而定]

setttins/dev.py,代码:

  1. # 支付宝相关配置
  2. ALIPAY = {
  3. # 'gateway': 'https://openapi.alipay.com/gateway.do', # 真实网关地址
  4. 'gateway': 'https://openapi.alipaydev.com/gateway.do', # 沙箱网关地址
  5. 'appid': '2016091600523592', # 支付应用ID
  6. 'sign_type': 'RSA2', # 签证的加密算法
  7. 'debug': True, # 沙箱模式下必须设置为True
  8. 'verbose': True, # 是否在调试模式下输出调试数据
  9. 'timeout': 15, # 请求超时时间,单位:秒
  10. "app_private_key_path": BASE_DIR / "apps/payments/keys/app_private_key.pem", # 应用私钥路径
  11. "alipay_public_key_path": BASE_DIR / "apps/payments/keys/alipay_public_key.pem", # 支付宝公钥路径
  12. "return_url": "http://www.luffycity.cn:3000/alipay", # 同步回调结果通知地址
  13. "notify_url": "http://api.luffycity.cn:8000/payments/alipay/notify", # 异步回调结果通知地址
  14. }

payments.urls,代码:

  1. from django.urls import path,re_path
  2. from . import views
  3. urlpatterns = [
  4. re_path("^alipay/(?P<order_number>[0-9]+)/$", views.AlipayAPIViewSet.as_view({"get":"link"})),
  5. ]

提交代码版本:

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 服务端提供生成支付宝支付链接的api接口"
  4. git push --set-upstream origin feature/payments

前端生成订单以后发起支付

views/Order.vue,代码:

  1. <script setup>
  2. // ... 代码省略
  3. const commit_order = ()=>{
  4. // 生成订单
  5. let token = sessionStorage.token || localStorage.token;
  6. let user_coupon_id = -1;
  7. if(order.select !== -1){
  8. user_coupon_id = order.coupon_list[order.select].user_coupon_id;
  9. }
  10. order.create_order(user_coupon_id, token).then(response=>{
  11. // 成功提示
  12. ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
  13. // 扣除掉被下单的商品数量,更新购物车中的商品数量
  14. store.commit("set_cart_total", store.state.cart_total - cart.select_course_list.length);
  15. // 根据订单号到服务端获取支付链接,并打开支付页面。
  16. order.alipay_page_pay(response.data.order_number).then(response=>{
  17. window.open(response.data.link,"_blank");
  18. })
  19. })
  20. }
  21. // ... 代码省略
  22. </script>

src/api/order.js,代码:

  1. import http from "../utils/http";
  2. import {reactive} from "vue";
  3. const order = reactive({
  4. // ... 代码省略
  5. alipay_page_pay(order_number){
  6. // 获取订单的支付宝支付链接信息
  7. return http.get(`/payments/alipay/${order_number}`)
  8. }
  9. });
  10. export default order;

完成了上面的功能以后,我们就可以在沙箱环境中进行支付宝的付款了,我们会接受到支付宝界面那边通过前端js跳转回来的同步通知支付结果,跳转回到我们的客户端页面,

支付结果参数说明:https://docs.open.alipay.com/api_1/alipay.trade.page.pay

  1. http://www.luffycity.cn:3000/alipay?charset=utf-8&out_trade_no=202207120000000100000022&method=alipay.trade.page.pay.return&total_amount=1579.00&sign=YqSmTxOPfaXV%2BTSMv8Lg1dOx71JulfaYL6Ab34LSy57Y%2BCvVAb895jPrxpqMeODxiLi65DRLLAJYK%2FQO1m6ykWuTbeQf1FpPhqTkH5LeipJ1LfPy3efj0KJFJFLVJ9pkIGs3tTD7tg%2FL9X70EVmzCxXruWtlM5pAh%2B2%2FsUVbZ4l1tMwDAt4%2FhNoPlc3jvQ07X1r7B17PPBa8Qk%2FF9PbXbIQBsoOkFa78l%2Fs5GBpLB7OTDoOCv16ijV7vTegqi9riucbJkxbk%2F%2FNR7yvLysKUkPMbkcY6uvXz9LD%2F6DQ%2BNKCz694fe0NLXgovVlhyA8l8FA9cSCYunWNELNK0MF%2FPMQ%3D%3D&trade_no=2022071222001439880503951070&auth_app_id=2016091600523592&version=1.0&app_id=2016091600523592&sign_type=RSA2&seller_id=2088102175868026&timestamp=2022-07-12+12%3A50%3A02

支付成功的模板

提供一个接受支付结果的页面,展示支付成功的信息。

views/AliPaySuccess.vue,代码:

  1. <template>
  2. <div class="success" v-if="order.is_show">
  3. <Header/>
  4. <div class="main">
  5. <div class="title">
  6. <i class="el-icon-chat-dot-round"></i>
  7. <div class="success-tips">
  8. <p class="tips1">您已成功购买 1 门课程!</p>
  9. <p class="tips2">你还可以加入QQ群 <span>747556033</span> 学习交流</p>
  10. </div>
  11. </div>
  12. <div class="order-info">
  13. <p class="info1"><b>付款时间:</b><span>2019/04/02 10:27</span></p>
  14. <p class="info2"><b>付款金额:</b><span >0</span></p>
  15. <p class="info3"><b>课程信息:</b><span><span>《Pycharm使用秘籍》</span></span></p>
  16. </div>
  17. <div class="wechat-code">
  18. <img src="../assets/wechat.jpg" alt="" class="er">
  19. <p><i class="el-icon-warning"></i>重要!微信扫码关注获得学习通知&amp;课程更新提醒!否则将严重影响学习进度和课程体验!</p>
  20. </div>
  21. <div class="study">
  22. <span>立即学习</span>
  23. </div>
  24. </div>
  25. <Footer/>
  26. </div>
  27. </template>
  28. <script setup>
  29. import Header from "../components/Header.vue"
  30. import Footer from "../components/Footer.vue"
  31. import {ElMessage} from "element-plus";
  32. import order from "../api/order";
  33. </script>
  34. <style scoped>
  35. .success{
  36. padding-top: 80px;
  37. }
  38. .main{
  39. height: 100%;
  40. padding-top: 25px;
  41. padding-bottom: 25px;
  42. margin: 0 auto;
  43. width: 1200px;
  44. background: #fff;
  45. }
  46. .main .title{
  47. display: flex;
  48. -ms-flex-align: center;
  49. align-items: center;
  50. padding: 25px 40px;
  51. border-bottom: 1px solid #f2f2f2;
  52. }
  53. .main .title .success-tips{
  54. box-sizing: border-box;
  55. }
  56. .title img{
  57. vertical-align: middle;
  58. width: 60px;
  59. height: 60px;
  60. margin-right: 40px;
  61. }
  62. .title .success-tips{
  63. box-sizing: border-box;
  64. }
  65. .title .tips1{
  66. font-size: 22px;
  67. color: #000;
  68. }
  69. .title .tips2{
  70. font-size: 16px;
  71. color: #4a4a4a;
  72. letter-spacing: 0;
  73. text-align: center;
  74. margin-top: 10px;
  75. }
  76. .title .tips2 span{
  77. color: #ec6730;
  78. }
  79. .order-info{
  80. padding: 25px 48px;
  81. padding-bottom: 15px;
  82. border-bottom: 1px solid #f2f2f2;
  83. }
  84. .order-info p{
  85. display: -ms-flexbox;
  86. display: flex;
  87. margin-bottom: 10px;
  88. font-size: 16px;
  89. }
  90. .order-info p b{
  91. font-weight: 400;
  92. color: #9d9d9d;
  93. white-space: nowrap;
  94. }
  95. .wechat-code{
  96. display: flex;
  97. -ms-flex-align: center;
  98. align-items: center;
  99. padding: 25px 40px;
  100. border-bottom: 1px solid #f2f2f2;
  101. }
  102. .wechat-code>img{
  103. width: 180px;
  104. height: 180px;
  105. margin-right: 15px;
  106. }
  107. .wechat-code p{
  108. font-size: 14px;
  109. color: #d0021b;
  110. display: -ms-flexbox;
  111. display: flex;
  112. -ms-flex-align: center;
  113. align-items: center;
  114. }
  115. .wechat-code p>img{
  116. width: 16px;
  117. height: 16px;
  118. margin-right: 10px;
  119. }
  120. .study{
  121. padding: 25px 52px;
  122. }
  123. .study span{
  124. display: block;
  125. width: 140px;
  126. height: 42px;
  127. text-align: center;
  128. line-height: 42px;
  129. cursor: pointer;
  130. background: #ffc210;
  131. border-radius: 6px;
  132. font-size: 16px;
  133. color: #fff;
  134. }
  135. .el-icon-warning{
  136. font-size: 22px;
  137. margin-right: 5px;
  138. }
  139. .el-icon-chat-dot-round{
  140. font-size: 122px;
  141. margin-right: 10px;
  142. }
  143. </style>

routers/index.js,路由代码:

  1. ,
  2. {
  3. meta:{
  4. title: "支付成功",
  5. keepAlive: true
  6. },
  7. path: '/alipay',
  8. name: "PaySuccess",
  9. component: ()=> import("../views/AliPaySuccess.vue"),
  10. },

src/api/order.js,代码:

  1. import http from "../utils/http";
  2. import {reactive} from "vue";
  3. const order = reactive({
  4. // ... 代码省略
  5. // ... 代码省略
  6. // ... 代码省略
  7. course_list: [], // 本次购买的商品课程列表
  8. real_price: 0, // 付款金额
  9. pay_time: undefined, // 付款时间
  10. is_show: false, // 是否展示支付成功的内容
  11. // ... 代码省略
  12. // ... 代码省略
  13. // ... 代码省略
  14. relay_alipay_result(query_string){
  15. // 把地址栏中的查询字符串(支付成功以后的同步回调通知)转发给服务端
  16. return http.get(`/payments/alipay/result/${query_string}`)
  17. }
  18. });
  19. export default order;

转发支付结果到服务端。views/AliPaySuccess.vue,代码:

  1. <script setup>
  2. import Header from "../components/Header.vue"
  3. import Footer from "../components/Footer.vue"
  4. import {ElMessage} from "element-plus";
  5. import order from "../api/order";
  6. let query_string = location.search; // 获取查询字符串的支付结果参数
  7. order.relay_alipay_result(query_string).then(response=>{
  8. order.is_show = true;
  9. order.course_list = response.data.course_list;
  10. order.real_price = response.data.real_price;
  11. order.pay_time = response.data.pay_time;
  12. }).catch(error=>{
  13. console.log(error);
  14. })
  15. </script>

提交代码版本:

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 客户端实现发起支付并转换同步支付结果到服务端"
  4. git push

服务端接收并处理同步通知支付结果

因为我们需要多次对支付接口进行调用,所以我们可以在缩减重复代码的情况下,对原有代码进行封装。

luffycityapi/utils/alipaysdk.py,代码:

  1. from django.conf import settings
  2. from alipay import AliPay
  3. from alipay.utils import AliPayConfig
  4. from datetime import datetime
  5. class AliPaySDK(AliPay):
  6. """支付宝接口sdk工具类"""
  7. def __init__(self, config=None):
  8. if config is None:
  9. self.config = settings.ALIPAY
  10. else:
  11. self.config = config
  12. app_private_key_string = open(self.config["app_private_key_path"]).read()
  13. alipay_public_key_string = open(self.config["alipay_public_key_path"]).read()
  14. super().__init__(
  15. appid=self.config["appid"],
  16. app_notify_url=self.config["notify_url"], # 默认全局回调 url
  17. app_private_key_string=app_private_key_string,
  18. # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
  19. alipay_public_key_string=alipay_public_key_string,
  20. sign_type=self.config["sign_type"], # RSA 或者 RSA2
  21. debug=self.config["debug"], # 默认 False,沙箱模式下必须设置为True
  22. verbose=self.config["verbose"], # 输出调试数据
  23. config=AliPayConfig(timeout=self.config["timeout"]) # 可选,请求超时时间,单位:秒
  24. )
  25. def page_pay(self,order_number,real_price,order_name):
  26. """
  27. 生成支付链接
  28. @parmas order_number: 商户订单号
  29. @parmas real_price: 订单金额
  30. @parmas order_name: 订单标题
  31. @return 支付链接
  32. """
  33. order_string = self.client_api(
  34. "alipay.trade.page.pay",
  35. biz_content={
  36. "out_trade_no": order_number, # 订单号
  37. "total_amount": float(real_price), # 订单金额
  38. "subject": order_name, # 订单标题
  39. "product_code": "FAST_INSTANT_TRADE_PAY",
  40. },
  41. return_url=self.config["return_url"], # 可选,同步回调地址,必须填写客户端的路径
  42. notify_url=self.config["notify_url"] # 可选,不填则使用采用全局默认notify_url,必须填写服务端的路径
  43. )
  44. return f"{self.config['gateway']}?{order_string}"
  45. def check_sign(self, data):
  46. """
  47. 验证返回的支付结果中的签证信息
  48. @params data: 支付平台返回的支付结果,字典格式
  49. """
  50. signature = data.pop("sign")
  51. success = self.verify(data, signature)
  52. return success
  53. def query(self,order_number):
  54. """
  55. 根据订单号查询订单状态
  56. @params order_number: 订单号
  57. """
  58. return self.server_api(
  59. "alipay.trade.query",
  60. biz_content={
  61. "out_trade_no": order_number
  62. }
  63. )
  64. def refund(self,order_number, real_price):
  65. """
  66. 原路退款
  67. @params order_number: 退款的订单号
  68. @params real_price: 退款的订单金额
  69. """
  70. self.server_api(
  71. "alipay.rade.refund",
  72. biz_content={
  73. "out_trade_no": order_number,
  74. "refund_amount": real_price
  75. }
  76. )
  77. def transfer(self, account,amount):
  78. """
  79. 转账给个人
  80. @params account: 收款人的支付宝账号
  81. @params amount: 转账金额
  82. """
  83. return self.server_api(
  84. "alipay.fund.trans.toaccount.transfer",
  85. biz_content = {
  86. "out_biz_no": datetime.now().strftime("%Y%m%d%H%M%S"),
  87. "payee_type": "ALIPAY_LOGONID/ALIPAY_USERID",
  88. "payee_account": account,
  89. "amount": amount
  90. }
  91. )

payments/views.py,代码:

  1. from datetime import datetime
  2. from rest_framework.viewsets import ViewSet
  3. from rest_framework.response import Response
  4. from rest_framework import status
  5. from courses.serializers import CourseInfoModelSerializer
  6. from orders.models import Order
  7. from alipaysdk import AliPaySDK
  8. # Create your views here.
  9. class AlipayAPIViewSet(ViewSet):
  10. """支付宝接口"""
  11. def link(self, request, order_number):
  12. """生成支付宝支付链接信息"""
  13. try:
  14. order = Order.objects.get(order_number=order_number)
  15. if order.order_status > 0:
  16. return Response({"message": "对不起,当前订单不能重复支付或订单已超时!"})
  17. except Order.DoesNotExist:
  18. return Response({"message": "对不起,当前订单不存在!"})
  19. # # 读取支付宝公钥与商户私钥
  20. # app_private_key_string = open(settings.ALIPAY["app_private_key_path"]).read()
  21. # alipay_public_key_string = open(settings.ALIPAY["alipay_public_key_path"]).read()
  22. #
  23. # # 创建alipay SDK操作对象
  24. # alipay = AliPay(
  25. # appid=settings.ALIPAY["appid"],
  26. # app_notify_url=settings.ALIPAY["notify_url"], # 默认全局回调 url
  27. # app_private_key_string=app_private_key_string,
  28. # # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
  29. # alipay_public_key_string=alipay_public_key_string,
  30. # sign_type=settings.ALIPAY["sign_type"], # RSA2
  31. # debug=settings.ALIPAY["debug"], # 默认 False,沙箱模式下必须设置为True
  32. # verbose=settings.ALIPAY["verbose"], # 输出调试数据
  33. # config=AliPayConfig(timeout=settings.ALIPAY["timeout"]) # 可选,请求超时时间,单位:秒
  34. # )
  35. # 生成支付信息
  36. # order_string = alipay.client_api(
  37. # "alipay.trade.page.pay", # 接口名称
  38. # biz_content={
  39. # "out_trade_no": order_number, # 订单号
  40. # "total_amount": float(order.real_price), # 订单金额 单位:元
  41. # "subject": order.name, # 订单标题
  42. # "product_code": "FAST_INSTANT_TRADE_PAY", # 产品码,目前只能支持 FAST_INSTANT_TRADE_PAY
  43. # },
  44. # return_url=settings.ALIPAY["return_url"], # 可选,同步回调地址,必须填写客户端的路径
  45. # notify_url=settings.ALIPAY["notify_url"] # 可选,不填则使用采用全局默认notify_url,必须填写服务端的路径
  46. # )
  47. #
  48. # # 拼接完整的支付链接
  49. # link = f"{settings.ALIPAY['gateway']}?{order_string}"
  50. alipay = AliPaySDK()
  51. link = alipay.page_pay(order_number, order.real_price, order.name)
  52. print(link)
  53. return Response({
  54. "pay_type": 0, # 支付类型
  55. "get_pay_type_display": "支付宝", # 支付类型的提示
  56. "link": link # 支付连接地址
  57. })
  58. def return_result(self, request):
  59. """支付宝支付结果的同步通知处理"""
  60. data = request.query_params.dict() # QueryDict
  61. alipay = AliPaySDK()
  62. success = alipay.check_sign(data)
  63. if not success:
  64. return Response({"errmsg": "通知通知结果不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  65. order_number = data.get("out_trade_no")
  66. try:
  67. order = Order.objects.get(order_number=order_number)
  68. if order.order_status > 1:
  69. return Response({"errmsg": "订单超时或已取消!"}, status=status.HTTP_400_BAD_REQUEST)
  70. except Order.DoesNotExist:
  71. return Response({"errmsg": "订单不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  72. # 获取当前订单相关的课程信息,用于返回给客户端
  73. order_courses = order.order_courses.all()
  74. course_list = [item.course for item in order_courses]
  75. if order.order_status == 0:
  76. result = alipay.query(order_number)
  77. print(f"result-{result}")
  78. if result.get("trade_status", None) in ["TRADE_FINISHED", "TRADE_SUCCESS"]:
  79. """支付成功"""
  80. # todo 1. 修改订单状态
  81. order.pay_time = datetime.now()
  82. order.order_status = 1
  83. order.save()
  84. # todo 2. 记录扣除个人积分的流水信息,补充个人的优惠券使用记录
  85. # todo 3. 用户和课程的关系绑定
  86. # todo 4. 取消订单超时
  87. # 返回客户端结果
  88. serializer = CourseInfoModelSerializer(course_list, many=True)
  89. return Response({
  90. "pay_time": order.pay_time.strftime("%Y-%m-%d %H:%M:%S"),
  91. "real_price": float(order.real_price),
  92. "course_list": serializer.data
  93. })

payments/urls.py,代码:

  1. from django.urls import path,re_path
  2. from . import views
  3. urlpatterns = [
  4. re_path("^alipay/(?P<order_number>[0-9]+)/$", views.AlipayAPIViewSet.as_view({"get":"link"})),
  5. path("alipay/result/", views.AlipayAPIViewSet.as_view({"get":"return_result"}))
  6. ]

提交代码版本:

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 服务端接收客户端转发的同步支付结果并验证修改订单状态"
  4. git push

服务端更新用户购买商品课程的记录

user/models.py,模型代码:

  1. from django.contrib.auth.models import AbstractUser
  2. from django.utils.safestring import mark_safe
  3. from luffycityapi.utils.models import BaseModel,models
  4. from luffycityapi.settings import constants
  5. # Create your models here.
  6. class User(AbstractUser):
  7. name = models.CharField(max_length=150, default="", null=True, blank=True, verbose_name='用户昵称')
  8. mobile = models.CharField(max_length=15, unique=True, verbose_name='手机号')
  9. money = models.DecimalField(max_digits=9, default=0.0, decimal_places=2, verbose_name="钱包余额")
  10. credit = models.IntegerField(default=0, verbose_name="积分")
  11. avatar = models.ImageField(upload_to="avatar/%Y/", blank=True, null=True, default=constants.DEFAULT_USER_AVATAR, verbose_name="个人头像")
  12. study_time = models.IntegerField(default=0, verbose_name="总学习时长")
  13. class Meta:
  14. db_table = 'fg_users'
  15. verbose_name = '用户信息'
  16. verbose_name_plural = verbose_name
  17. def avatar_image(self):
  18. if self.avatar:
  19. return mark_safe( f'<img style="border-radius: 100%; max-width: 50px;" src="{self.avatar.url}">' )
  20. else:
  21. return mark_safe(f'<img style="border-radius: 100%; max-width: 50px;" src="/uploads/{constants.DEFAULT_USER_AVATAR}">')
  22. avatar_image.short_description = "个人头像"
  23. avatar_image.allow_tags = True
  24. avatar_image.admin_order_field = "avatar"
  25. class Credit(BaseModel):
  26. """积分流水"""
  27. opera_choices = (
  28. (0, "业务增值"),
  29. (1, "购物消费"),
  30. (2, "系统赠送"),
  31. )
  32. operation = models.SmallIntegerField(choices=opera_choices, default=1, verbose_name="积分操作类型")
  33. number = models.IntegerField(default=0, verbose_name="积分数量", help_text="如果是扣除积分则需要设置积分为负数,如果消费10积分,则填写-10,<br>如果是添加积分则需要设置积分为正数,如果获得10积分,则填写10。")
  34. user = models.ForeignKey(User, related_name='user_credits', on_delete=models.CASCADE, verbose_name="用户")
  35. remark = models.CharField(max_length=500, null=True, blank=True, verbose_name="备注信息")
  36. class Meta:
  37. db_table = 'fg_credit'
  38. verbose_name = '积分流水'
  39. verbose_name_plural = verbose_name
  40. def __str__(self):
  41. if self.number > 0:
  42. oper_text = "获得"
  43. else:
  44. oper_text = "减少"
  45. return "[%s] %s 用户%s %s %s积分" % (self.get_operation_display(),self.created_time.strftime("%Y-%m-%d %H:%M:%S"), self.user.username, oper_text, abs(self.number))
  46. from courses.models import Course,CourseChapter,CourseLesson
  47. class UserCourse(BaseModel):
  48. """用户的课程"""
  49. user = models.ForeignKey(User, related_name='user_courses', on_delete=models.CASCADE,verbose_name="用户", db_constraint=False)
  50. course = models.ForeignKey(Course, related_name='course_users', on_delete=models.CASCADE, verbose_name="课程名称", db_constraint=False)
  51. chapter = models.ForeignKey(CourseChapter, related_name="user_chapter", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="章节信息", db_constraint=False)
  52. lesson = models.ForeignKey(CourseLesson, related_name="user_lesson", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="课时信息", db_constraint=False)
  53. study_time = models.IntegerField(default=0, verbose_name="学习时长")
  54. class Meta:
  55. db_table = 'ly_user_course'
  56. verbose_name = '用户课程购买记录'
  57. verbose_name_plural = verbose_name

数据迁移

  1. /home/moluo/Desktop/luffycity/luffycityapi
  2. python manage.py makemigrations
  3. python manage.py migrate

在订单结果处理的视图中把用户购买课程逻辑代码加上。

payments/views.py,代码:

  1. import logging
  2. from datetime import datetime
  3. from rest_framework.viewsets import ViewSet
  4. from rest_framework.response import Response
  5. from rest_framework import status
  6. from django.db import transaction
  7. from orders.models import Order
  8. from alipaysdk import AliPaySDK
  9. from users.models import Credit, UserCourse
  10. from courses.serializers import CourseInfoModelSerializer
  11. from coupon.models import CouponLog
  12. logger = logging.getLogger("django")
  13. # Create your views here.
  14. class AlipayAPIViewSet(ViewSet):
  15. """支付宝接口"""
  16. def link(self, request, order_number):
  17. """生成支付宝支付链接信息"""
  18. try:
  19. order = Order.objects.get(order_number=order_number)
  20. if order.order_status > 0:
  21. return Response({"message": "对不起,当前订单不能重复支付或订单已超时!"})
  22. except Order.DoesNotExist:
  23. return Response({"message": "对不起,当前订单不存在!"})
  24. # # 读取支付宝公钥与商户私钥
  25. # app_private_key_string = open(settings.ALIPAY["app_private_key_path"]).read()
  26. # alipay_public_key_string = open(settings.ALIPAY["alipay_public_key_path"]).read()
  27. #
  28. # # 创建alipay SDK操作对象
  29. # alipay = AliPay(
  30. # appid=settings.ALIPAY["appid"],
  31. # app_notify_url=settings.ALIPAY["notify_url"], # 默认全局回调 url
  32. # app_private_key_string=app_private_key_string,
  33. # # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
  34. # alipay_public_key_string=alipay_public_key_string,
  35. # sign_type=settings.ALIPAY["sign_type"], # RSA2
  36. # debug=settings.ALIPAY["debug"], # 默认 False,沙箱模式下必须设置为True
  37. # verbose=settings.ALIPAY["verbose"], # 输出调试数据
  38. # config=AliPayConfig(timeout=settings.ALIPAY["timeout"]) # 可选,请求超时时间,单位:秒
  39. # )
  40. # 生成支付信息
  41. # order_string = alipay.client_api(
  42. # "alipay.trade.page.pay", # 接口名称
  43. # biz_content={
  44. # "out_trade_no": order_number, # 订单号
  45. # "total_amount": float(order.real_price), # 订单金额 单位:元
  46. # "subject": order.name, # 订单标题
  47. # "product_code": "FAST_INSTANT_TRADE_PAY", # 产品码,目前只能支持 FAST_INSTANT_TRADE_PAY
  48. # },
  49. # return_url=settings.ALIPAY["return_url"], # 可选,同步回调地址,必须填写客户端的路径
  50. # notify_url=settings.ALIPAY["notify_url"] # 可选,不填则使用采用全局默认notify_url,必须填写服务端的路径
  51. # )
  52. #
  53. # # 拼接完整的支付链接
  54. # link = f"{settings.ALIPAY['gateway']}?{order_string}"
  55. alipay = AliPaySDK()
  56. link = alipay.page_pay(order_number, order.real_price, order.name)
  57. print(link)
  58. return Response({
  59. "pay_type": 0, # 支付类型
  60. "get_pay_type_display": "支付宝", # 支付类型的提示
  61. "link": link # 支付连接地址
  62. })
  63. def return_result(self, request):
  64. """支付宝支付结果的同步通知处理"""
  65. data = request.query_params.dict() # QueryDict
  66. alipay = AliPaySDK()
  67. success = alipay.check_sign(data)
  68. if not success:
  69. return Response({"errmsg": "通知通知结果不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  70. order_number = data.get("out_trade_no")
  71. try:
  72. order = Order.objects.get(order_number=order_number)
  73. if order.order_status > 1:
  74. return Response({"errmsg": "订单超时或已取消!"}, status=status.HTTP_400_BAD_REQUEST)
  75. except Order.DoesNotExist:
  76. return Response({"errmsg": "订单不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  77. # 获取当前订单相关的课程信息,用于返回给客户端
  78. order_courses = order.order_courses.all()
  79. course_list = [item.course for item in order_courses]
  80. if order.order_status == 0:
  81. # 请求支付宝,查询订单的支付结果
  82. result = alipay.query(order_number)
  83. print(f"result-{result}")
  84. if result.get("trade_status", None) in ["TRADE_FINISHED", "TRADE_SUCCESS"]:
  85. """支付成功"""
  86. with transaction.atomic():
  87. save_id = transaction.savepoint()
  88. try:
  89. now_time = datetime.now()
  90. # 1. 修改订单状态
  91. order.pay_time = now_time
  92. order.order_status = 1
  93. order.save()
  94. # 2.1 记录扣除个人积分的流水信息
  95. if order.credit > 0:
  96. Credit.objects.create(operation=1, number=order.credit, user=order.user)
  97. # 2.2 补充个人的优惠券使用记录
  98. coupon_log = CouponLog.objects.filter(order=order).first()
  99. if coupon_log:
  100. coupon_log.use_time = now_time
  101. coupon_log.use_status = 1 # 1 表示已使用
  102. coupon_log.save()
  103. # 3. 用户和课程的关系绑定
  104. user_course_list = []
  105. for course in course_list:
  106. user_course_list.append(UserCourse(course=course, user=order.user))
  107. UserCourse.objects.bulk_create(user_course_list)
  108. # todo 4. 取消订单超时
  109. except Exception as e:
  110. logger.error(f"订单支付处理同步结果发生未知错误:{e}")
  111. transaction.savepoint_rollback(save_id)
  112. return Response({"errmsg": "当前订单支付未完成!请联系客服工作人员!"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  113. # 返回客户端结果
  114. serializer = CourseInfoModelSerializer(course_list, many=True)
  115. return Response({
  116. "pay_time": order.pay_time.strftime("%Y-%m-%d %H:%M:%S"),
  117. "real_price": float(order.real_price),
  118. "course_list": serializer.data
  119. })

因为在处理同步通知结果的代码中,还进行了一次向支付宝查询订单支付结果的操作,所以有时候网络延迟的话,就会出现服务端成功,但是客户端因为超时等待而导致无法获取结果报错,此时应该设置axios的timeout为5~10秒,原来项目搭建时我们设置的是2.5秒。

utils/http.js,代码:

  1. import axios from "axios"
  2. const http = axios.create({
  3. // timeout: 2500, // 请求超时,有大文件上传需要关闭这个配置
  4. baseURL: "http://api.luffycity.cn:8000", // 设置api服务端的默认地址[如果基于服务端实现的跨域,这里可以填写api服务端的地址,如果基于nodejs客户端测试服务器实现的跨域,则这里不能填写api服务端地址]
  5. withCredentials: false, // 是否允许客户端ajax请求时携带cookie
  6. })
  7. // 后续代码省略....

客户端展示支付处理后的结果。views/AliPaySuccess.vue,代码:

  1. <template>
  2. <div class="success" v-if="order.is_show">
  3. <Header/>
  4. <div class="main">
  5. <div class="title">
  6. <i class="el-icon-chat-dot-round"></i>
  7. <div class="success-tips">
  8. <p class="tips1">您已成功购买 {{order.course_list?.length}} 门课程!</p>
  9. <p class="tips2">你还可以加入QQ群 <span>747556033</span> 学习交流</p>
  10. </div>
  11. </div>
  12. <div class="order-info">
  13. <p class="info1"><b>付款时间:</b><span>{{order.pay_time}}</span></p>
  14. <p class="info2"><b>付款金额:</b><span >{{order.real_price?.toFixed(2)}}</span></p>
  15. <p class="info3"><b>课程信息:</b>
  16. <span v-for="course in order.course_list">《{{course.name}}》</span>
  17. </p>
  18. </div>
  19. <div class="wechat-code">
  20. <img src="../assets/wechat.jpg" alt="" class="er">
  21. <p><i class="el-icon-warning"></i>重要!微信扫码关注获得学习通知&amp;课程更新提醒!否则将严重影响学习进度和课程体验!</p>
  22. </div>
  23. <div class="study">
  24. <router-link to="/user/study"><span>立即学习</span></router-link>
  25. </div>
  26. </div>
  27. <Footer/>
  28. </div>
  29. </template>
  1. <script setup>
  2. import Header from "../components/Header.vue"
  3. import Footer from "../components/Footer.vue"
  4. import {ElMessage} from "element-plus";
  5. import order from "../api/order";
  6. import {useRouter} from "vue-router";
  7. const router = useRouter()
  8. let query_string = location.search; // 获取查询字符串的支付结果参数
  9. order.relay_alipay_result(query_string).then(response=>{
  10. order.is_show = true;
  11. order.course_list = response.data.course_list;
  12. order.real_price = response.data.real_price;
  13. order.pay_time = response.data.pay_time;
  14. }).catch(error=>{
  15. ElMessage.error(error.response.data.errmsg);
  16. router.push("/");
  17. })
  18. </script>

我们当前完成的项目具有一定特殊性,和传统卖实物商品不一样的是,我们卖的是虚拟商品,所以不存在多次购买同一款商品的,所以后续用户添加商品到购物车时, 要判断用户是否曾经购买了当前商品课程,如果在UserCourse中查询到购买记录,则不能添加商品到购物车!!cart/views.py,代码:

  1. from rest_framework.views import APIView
  2. from rest_framework.permissions import IsAuthenticated
  3. from rest_framework.response import Response
  4. from rest_framework import status
  5. from django_redis import get_redis_connection
  6. from courses.models import Course
  7. from users.models import UserCourse
  8. # Create your views here.
  9. class CartAPIView(APIView):
  10. permission_classes = [IsAuthenticated] # 保证用户必须时登录状态才能调用当前视图
  11. def post(self, request):
  12. """添加课程商品到购物车中"""
  13. # 1. 接受客户端提交的商品信息:用户ID,课程ID,勾选状态
  14. # 用户ID 可以通过self.request.user.id 或 request.user.id 来获取
  15. user_id = request.user.id
  16. course_id = request.data.get("course_id", None)
  17. selected = 1 # 默认商品是勾选状态的
  18. print(f"user_id={user_id},course_id={course_id}")
  19. try:
  20. # 判断课程是否存在
  21. course = Course.objects.get(is_show=True, is_deleted=False, pk=course_id)
  22. except:
  23. return Response({"errmsg": "当前课程不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  24. try:
  25. # 判断用户是否已经购买了
  26. UserCourse.objects.get(user_id=user_id, course_id=course_id)
  27. return Response({"errmsg": "对不起,您已经购买过当前课程!不需要重新购买了."}, status=status.HTTP_400_BAD_REQUEST)
  28. except:
  29. pass
  30. # 3. 添加商品到购物车
  31. redis = get_redis_connection("cart")
  32. """
  33. cart_用户ID: {
  34. 课程ID: 勾选状态
  35. }
  36. """
  37. redis.hset(f"cart_{user_id}", course_id, selected)
  38. # 4. 获取购物车中的商品课程数量
  39. cart_total = redis.hlen(f"cart_{user_id}")
  40. # 5. 返回结果给客户端
  41. return Response({"errmsg": "成功添加商品课程到购物车!", "cart_total": cart_total}, status=status.HTTP_201_CREATED)
  42. # ... 代码省略

提交代码版本:

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 服务端更新用户购买的课程记录以及的订单支付成功后的积分流水记录与优惠券的使用状态记录,最后在购物车中防止用户重复购买商品"
  4. git push

上面的支付结果处理都是基于支付平台返回的同步结果,是支付宝那边通过js页面跳转来完成支付结果通知的。在项目运营过程中,很容易出现跳转页面过程中无法跳转或者跳转过程中被用户手动关闭页面的情况,这些情况都会导致服务端没法完成同步通知代码的执行,也就是说,我们不能单纯等待用户付款完成以后支付宝通过页面跳转的同步通知结果,还要结合用户的行为操作和支付宝的异步通知结果处理,才能防止订单状态的丢失

提供支付倒计时功能

客户端订单页面中添加一个倒计时的遮罩层,当用户关闭当前遮罩层则发送ajax轮询请求查询当前订单的支付结果。

views/Order.vue,代码

  1. <div class="loadding" v-if="order.loading" @click="check_order">
  2. <div class="box">
  3. <p class="time">{{fill0(parseInt(order.timeout/60))}}:{{ fill0(order.timeout%60)}}</p>
  4. <i class="el-icon-loading"></i><br>
  5. <p>支付完成!点击关闭当前页面</p>
  6. </div>
  7. </div> <!-- 这里添加到Footer上方即可 -->
  8. <Footer/>
  1. <script setup>
  2. import {reactive,watch} from "vue"
  3. import Header from "../components/Header.vue"
  4. import Footer from "../components/Footer.vue"
  5. import {useStore} from "vuex";
  6. import cart from "../api/cart"
  7. import order from "../api/order";
  8. import {ElMessage} from "element-plus";
  9. import {useRouter} from "vue-router";
  10. import {fill0} from "../utils/func";
  11. let store = useStore()
  12. let router = useRouter()
  13. const get_select_course = ()=>{
  14. // 获取购物车中的勾选商品列表
  15. let token = sessionStorage.token || localStorage.token;
  16. cart.get_select_course(token).then(response=>{
  17. cart.select_course_list = response.data.cart
  18. if(response.data.cart.length === 0){
  19. ElMessage.error("当前购物车中没有下单的商品!请重新重新选择购物车中要购买的商品~");
  20. router.back();
  21. }
  22. // 计算本次下单的总价格
  23. let sum = 0
  24. let credit_course_list= [] // 可使用积分抵扣的课程列表
  25. let max_use_credit = 0 // 本次下单最多可以用于抵扣的积分
  26. response.data.cart?.forEach((course,key)=>{
  27. if(course.discount.price > 0 || course.discount.price === 0){
  28. sum+=course.discount.price
  29. }else{
  30. sum+=course.price
  31. }
  32. if(course.credit > 0){
  33. max_use_credit = max_use_credit + course.credit
  34. credit_course_list.push(course)
  35. }
  36. })
  37. cart.total_price = sum;
  38. order.credit_course_list = credit_course_list
  39. order.max_use_credit = max_use_credit // 本次下单最多可以用于抵扣的积分
  40. console.log(`order.max_use_credit=${order.max_use_credit}`);
  41. // 本次订单最多可以使用的积分数量
  42. // 如果用户积分不足,则最多只能用完自己的积分
  43. if(order.max_use_credit > order.has_credit){
  44. order.max_use_credit = order.has_credit
  45. }
  46. }).catch(error=>{
  47. if(error?.response?.status===400){
  48. ElMessage.error("登录超时!请重新登录后再继续操作~");
  49. }
  50. })
  51. }
  52. get_select_course();
  53. const commit_order = ()=>{
  54. // 生成订单
  55. let token = sessionStorage.token || localStorage.token;
  56. // 当用户选择了优惠券,则需要获取当前选择的优惠券发放记录的id
  57. let user_coupon_id = -1;
  58. if(order.select !== -1){
  59. user_coupon_id = order.coupon_list[order.select].user_coupon_id;
  60. }
  61. order.create_order(user_coupon_id, token).then(response=>{
  62. // 支付倒计时提示
  63. order.order_number = response.data.order_number // 订单号
  64. order.loading = true // 显示遮罩层
  65. order.timeout = response.data.order_timeout // 订单超时的时间,为15分钟
  66. clearInterval(order.timer) // 先清除原有定时器,保证整个页面中timer对应的定时器是唯一的。
  67. order.timer = setInterval(() => {
  68. if(order.timeout > 1){
  69. order.timeout=order.timeout - 1;
  70. }else{
  71. ElMessage.error("订单超时!如果您已经支付成功!请点击关闭当前弹窗!当前页面15秒后关闭!");
  72. clearInterval(order.timer);
  73. // 发送一个订单查询
  74. check_order();
  75. // 关闭页面
  76. setTimeout(()=>{
  77. // 跳转到用户的订单用心
  78. router.push("/user/order");
  79. }, 1500);
  80. }
  81. }, 3000);
  82. // 成功提示
  83. ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
  84. // 扣除掉被下单的商品数量,更新购物车中的商品数量
  85. store.commit("cart_total", store.state.cart_total - cart.select_course_list.length);
  86. // 订单生成以后,先扣除临时用户积分
  87. order.has_credit = order.has_credit - order.credit
  88. // 根据订单号到服务端获取支付链接,并打开支付页面。
  89. console.log(order.order_number) // 订单号
  90. order.alipay_page_pay().then(response=>{
  91. window.open(response.data.link,"_blank");
  92. })
  93. }).catch(error=>{
  94. if(error?.response?.status===400){
  95. ElMessage.success("登录超时!请重新登录后再继续操作~");
  96. }
  97. })
  98. }
  99. // 获取本次下单的可用优惠券
  100. const get_enable_coupon_list = ()=>{
  101. let token = sessionStorage.token || localStorage.token;
  102. order.get_enable_coupon_list(token).then(response=>{
  103. order.coupon_list = response.data.coupon_list;
  104. // 获取积分相关信息
  105. order.credit_to_money = response.data.credit_to_money;
  106. order.has_credit = response.data.has_credit;
  107. })
  108. }
  109. get_enable_coupon_list()
  110. // 积分兑换抵扣
  111. const conver_credit = ()=>{
  112. order.discount_price = parseFloat( (order.credit / order.credit_to_money).toFixed(2) )
  113. }
  114. // 本次下单的最大兑换积分
  115. const max_conver_credit = ()=>{
  116. order.credit=order.max_use_credit
  117. conver_credit();
  118. }
  119. // 查询订单状态
  120. const check_order = ()=>{
  121. let token = sessionStorage.token || localStorage.token;
  122. order.query_order(token).then(response=>{
  123. order.loading = false;
  124. router.push("/user/order");
  125. }).catch(error=>{
  126. console.log(error);
  127. ElMessage.error(error.response.data.errmsg);
  128. })
  129. }
  130. // 监听用户选择的支付方式
  131. watch(
  132. ()=>order.pay_type,
  133. ()=>{
  134. console.log(order.pay_type)
  135. }
  136. )
  137. // 监听用户选择的优惠券
  138. watch(
  139. ()=>order.select,
  140. ()=>{
  141. order.discount_price = 0;
  142. // 如果没有选择任何的优惠券,则select 为-1,那么不用进行计算优惠券折扣的价格了
  143. if (order.select === -1) {
  144. return // 阻止代码继续往下执行
  145. }
  146. // 根据下标select,获取当前选中的优惠券信息
  147. let current_coupon = order.coupon_list[order.select]
  148. console.log(current_coupon);
  149. // 针对折扣优惠券,找到最大优惠的课程
  150. let max_discount = -1;
  151. for(let course of cart.select_course_list) { // 循环本次下单的勾选商品
  152. // 找到当前优惠券的可用课程
  153. if(current_coupon.enable_course === "__all__") { // 如果当前优惠券是通用优惠券
  154. if(max_discount !== -1){
  155. if(course.price > max_discount.price){ // 在每次循环中,那当前循环的课程的价格与之前循环中得到的最大优惠课程的价格进行比较
  156. max_discount = course
  157. }
  158. }else{
  159. max_discount = course
  160. }
  161. }else if((current_coupon.enable_course.indexOf(course.id) > -1) && (course.price >= parseFloat(current_coupon.condition))){
  162. // 判断 当前优惠券如果包含了当前课程, 并 课程的价格 > 当前优惠券的使用门槛
  163. // 只允许没有参与其他优惠券活动的课程使用优惠券,基本所有的平台都不存在折上折的。
  164. if( course.discount.price === undefined ) {
  165. if(max_discount !== -1){
  166. if(course.price > max_discount.price){
  167. max_discount = course
  168. }
  169. }else{
  170. max_discount = course
  171. }
  172. }
  173. }
  174. }
  175. if(max_discount !== -1){
  176. if(current_coupon.discount === '1') { // 抵扣优惠券[抵扣的价格就是当前优惠券的价格]
  177. order.discount_price = parseFloat( Math.abs(current_coupon.sale) )
  178. }else if(current_coupon.discount === '2') { // 折扣优惠券]抵扣的价格就是(1-折扣百分比) * 课程原价]
  179. order.discount_price = parseFloat(max_discount.price * (1-parseFloat(current_coupon.sale.replace("*",""))) )
  180. }
  181. }else{
  182. order.select = -1
  183. order.discount_price = 0
  184. ElMessage.error("当前课程商品已经参与了其他优惠活动,无法再次使用当前优惠券!")
  185. }
  186. })
  187. // 在切换不同的优惠类型,重置积分和优惠券信息
  188. watch(
  189. ()=>order.discount_type,
  190. ()=>{
  191. order.select = -1
  192. order.credit = 0
  193. order.discount_price = 0
  194. }
  195. )
  196. // 底部订单总价信息固定浮动效果
  197. window.onscroll = ()=>{
  198. let cart_body_table = document.querySelector(".cart-body-table")
  199. let offsetY = window.scrollY
  200. let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  201. order.fixed = offsetY < maxY
  202. }
  203. </script>
  1. <style>
  2. .loadding{
  3. width: 100%;
  4. height: 100%;
  5. margin: auto;
  6. position: fixed;
  7. top: 0;
  8. left: 0;
  9. right: 0;
  10. bottom: 0;
  11. z-index: 999;
  12. background-color: rgba(0,0,0,.7);
  13. }
  14. .box{
  15. width: 300px;
  16. height: 150px;
  17. position: absolute;
  18. top: 0;
  19. left: 0;
  20. right: 0;
  21. bottom: 0;
  22. margin: auto;
  23. font-size: 40px;
  24. text-align: center;
  25. padding-top: 50px;
  26. color: #fff;
  27. }
  28. .box .time{
  29. font-size: 22px;
  30. }
  31. </style>

api/order.js,代码:

  1. import http from "../utils/http";
  2. import {reactive} from "vue";
  3. const order = reactive({
  4. total_price: 0, // 勾选商品的总价格
  5. discount_price: 0, // 本次下单的优惠抵扣价格
  6. discount_type: 0, // 0表示优惠券,1表示积分
  7. use_coupon: false, // 用户是否使用优惠
  8. coupon_list:[], // 用户拥有的可用优惠券列表
  9. select: -1, // 当前用户选中的优惠券下标,-1表示没有选择
  10. credit: 0, // 当前用户选择抵扣的积分,0表示没有使用积分
  11. fixed: true, // 底部订单总价是否固定浮动
  12. pay_type: 0, // 支付方式
  13. credit_to_money: 0, // 积分兑换现金的比例
  14. has_credit: 0, // 用户拥有的积分
  15. max_use_credit: 0, // 当前用户本次下单可用最大积分数量
  16. credit_course_list:[], // 可使用积分抵扣的课程列表
  17. course_list: [], // 本次购买的商品课程列表
  18. real_price: 0, // 付款金额
  19. pay_time: undefined, // 付款时间
  20. order_number: null, // 订单号
  21. is_show: false, // 是否展示支付成功的内容[接收到支付宝的同步处理结果以后,先把结果转发给后端验证成功以后,才把前端的页面内容展示处理]
  22. loading: false, // 订单支付时的倒计时背景遮罩层
  23. timeout: 0, // 订单支付超时倒计时
  24. timer: 0, // 订单支付倒计时定时器的标记符
  25. create_order(user_coupon_id, token){
  26. // 生成订单
  27. return http.post("/orders/",{
  28. pay_type: this.pay_type,
  29. user_coupon_id,
  30. credit: this.credit,
  31. },{
  32. headers:{
  33. Authorization: "jwt " + token,
  34. }
  35. })
  36. },
  37. get_enable_coupon_list(token){
  38. // 获取本次下单的可用优惠券列表
  39. return http.get("/coupon/enable/",{
  40. headers:{
  41. Authorization: "jwt " + token,
  42. }
  43. })
  44. },
  45. alipay_page_pay(){
  46. // 获取订单的支付宝支付链接信息
  47. return http.get(`/payments/alipay/${this.order_number}`)
  48. },
  49. relay_alipay_result(query_string){
  50. // 把地址栏中的查询字符串(支付成功以后的同步回调通知)转发给服务端
  51. return http.get(`/payments/alipay/result/${query_string}`)
  52. },
  53. query_order(token){
  54. // 查询订单支付结果
  55. return http.get(`/payments/alipay/query/${this.order_number}`,{
  56. headers:{
  57. Authorization: "jwt " + token,
  58. }
  59. })
  60. }
  61. })
  62. export default order;

服务端下单成功返回订单号时同时返回服务端配置的订单超时时间。

order/serializers.py,代码:

  1. import logging
  2. import constants
  3. from datetime import datetime
  4. from rest_framework import serializers
  5. from django_redis import get_redis_connection
  6. from django.db import transaction
  7. from .models import Order, OrderDetail, Course
  8. from coupon.models import CouponLog
  9. logger = logging.getLogger("django")
  10. class OrderModelSerializer(serializers.ModelSerializer):
  11. user_coupon_id = serializers.IntegerField(write_only=True, default=-1)
  12. order_timeout = serializers.IntegerField(read_only=True)
  13. class Meta:
  14. model = Order
  15. fields = ["pay_type", "id", "order_number", "user_coupon_id", "credit", "order_timeout"]
  16. read_only_fields = ["id", "order_number"]
  17. extra_kwargs = {
  18. "pay_type": {"write_only": True},
  19. "credit": {"write_only": True},
  20. }
  21. def create(self, validated_data):
  22. """创建订单"""
  23. redis = get_redis_connection("cart")
  24. user = self.context["request"].user
  25. user_id = user.id
  26. # 判断用户如果使用了优惠券,则优惠券需要判断验证
  27. user_coupon_id = validated_data.get("user_coupon_id")
  28. # 本次下单时,用户使用的优惠券
  29. user_coupon = None
  30. if user_coupon_id != -1:
  31. user_coupon = CouponLog.objects.filter(pk=user_coupon_id, user_id=user_id).first()
  32. # 本次下单时使用的积分数量
  33. use_credit = validated_data.get("credit", 0)
  34. if use_credit > 0 and use_credit > user.credit:
  35. raise serializers.ValidationError(detail="您拥有的积分不足以抵扣本次下单的积分,请重新下单!")
  36. # 开启事务操作,保证下单过程中的所有数据库的原子性
  37. with transaction.atomic():
  38. # 设置事务的回滚点标记
  39. t1 = transaction.savepoint()
  40. try:
  41. # 创建订单记录
  42. order = Order.objects.create(
  43. name="购买课程", # 订单标题
  44. user_id=user_id, # 当前下单的用户ID
  45. # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
  46. order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
  47. pay_type=validated_data.get("pay_type"), # 支付方式
  48. )
  49. # 记录本次下单的商品列表
  50. cart_hash = redis.hgetall(f"cart_{user_id}")
  51. if len(cart_hash) < 1:
  52. raise serializers.ValidationError(detail="购物车没有要下单的商品")
  53. # 提取购物车中所有勾选状态为b'1'的商品
  54. course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']
  55. # 添加订单与课程的关系
  56. course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
  57. detail_list = []
  58. total_price = 0 # 本次订单的总价格
  59. real_price = 0 # 本次订单的实付总价
  60. # 用户使用优惠券或积分以后,需要在服务端计算本次使用优惠券或积分的最大优惠额度
  61. total_discount_price = 0 # 总优惠价格
  62. max_discount_course = None # 享受最大优惠的课程
  63. # 本次下单最多可以抵扣的积分
  64. max_use_credit = 0
  65. for course in course_list:
  66. discount_price = course.discount.get("price", None) # 获取课程原价
  67. if discount_price is not None:
  68. discount_price = float(discount_price)
  69. discount_name = course.discount.get("type", "")
  70. detail_list.append(OrderDetail(
  71. order=order,
  72. course=course,
  73. name=course.name,
  74. price=course.price,
  75. real_price=course.price if discount_price is None else discount_price,
  76. discount_name=discount_name,
  77. ))
  78. # 统计订单的总价和实付总价
  79. total_price += float(course.price)
  80. real_price += float(course.price if discount_price is None else discount_price)
  81. # 在用户使用了优惠券,并且当前课程没有参与其他优惠活动时,找到最佳优惠课程
  82. if user_coupon and discount_price is None:
  83. if max_discount_course is None:
  84. max_discount_course = course
  85. else:
  86. if course.price >= max_discount_course.price:
  87. max_discount_course = course
  88. # 添加每个课程的可用积分
  89. if use_credit > 0 and course.credit > 0:
  90. max_use_credit += course.credit
  91. # 在用户使用了优惠券以后,根据循环中得到的最佳优惠课程进行计算最终抵扣金额
  92. if user_coupon:
  93. # 优惠公式
  94. sale = float(user_coupon.coupon.sale[1:])
  95. if user_coupon.coupon.discount == 1:
  96. """减免优惠券"""
  97. total_discount_price = sale
  98. elif user_coupon.coupon.discount == 2:
  99. """折扣优惠券"""
  100. total_discount_price = float(max_discount_course.price) * (1 - sale)
  101. if use_credit > 0:
  102. if max_use_credit < use_credit:
  103. raise serializers.ValidationError(detail="本次使用的抵扣积分数额超过了限制!")
  104. # 当前订单添加积分抵扣的数量
  105. order.credit = use_credit
  106. total_discount_price = float(use_credit / constants.CREDIT_TO_MONEY)
  107. # todo 扣除用户拥有的积分,后续在订单超时未支付,则返还订单中对应数量的积分给用户。如果订单成功支付,则添加一个积分流水记录。
  108. user.credit = user.credit - use_credit
  109. user.save()
  110. # 一次性批量添加本次下单的商品记录
  111. OrderDetail.objects.bulk_create(detail_list)
  112. # 保存订单的总价格和实付价格
  113. order.total_price = real_price
  114. order.real_price = float(real_price - total_discount_price)
  115. order.save()
  116. # 删除购物车中被勾选的商品,保留没有被勾选的商品信息
  117. cart = {key: value for key, value in cart_hash.items() if value == b'0'}
  118. pipe = redis.pipeline()
  119. pipe.multi()
  120. # 删除原来的购物车
  121. pipe.delete(f"cart_{user_id}")
  122. # 重新把未勾选的商品记录到购物车中
  123. if cart:
  124. pipe.hmset(f"cart_{user_id}", cart)
  125. pipe.execute()
  126. # 如果有使用了优惠券,则把优惠券和当前订单进行绑定
  127. if user_coupon:
  128. user_coupon.order = order
  129. user_coupon.save()
  130. # 把优惠券从redis中移除
  131. redis = get_redis_connection("coupon")
  132. redis.delete(f"{user_id}:{user_coupon_id}")
  133. # 返回订单超时时间
  134. order.order_timeout = constants.ORDER_TIMEOUT
  135. return order
  136. except Exception as e:
  137. # 1. 记录日志
  138. logger.error(f"订单创建失败:{e}")
  139. # 2. 事务回滚
  140. transaction.savepoint_rollback(t1)
  141. # 3. 抛出异常,通知视图返回错误提示
  142. raise serializers.ValidationError(detail="订单创建失败!")

utils/constants.py,代码:

  1. # 订单超时的时间(单位:秒)
  2. ORDER_TIMEOUT = 15 * 60

服务端提供查询订单的api接口

payments/urls.py,代码:

  1. from django.urls import path,re_path
  2. from . import views
  3. urlpatterns = [
  4. re_path("^alipay/(?P<order_number>[0-9]+)/$", views.AlipayAPIViewSet.as_view({"get":"link"})),
  5. path("alipay/result/", views.AlipayAPIViewSet.as_view({"get":"return_result"})),
  6. re_path("^alipay/query/(?P<order_number>[0-9]+)/$", views.AlipayAPIViewSet.as_view({"get":"query"})),
  7. ]

payments/views.py,代码:

  1. import logging
  2. from datetime import datetime
  3. from rest_framework.viewsets import ViewSet
  4. from rest_framework.response import Response
  5. from rest_framework import status
  6. from django.db import transaction
  7. from orders.models import Order
  8. from alipaysdk import AliPaySDK
  9. from users.models import Credit, UserCourse
  10. from courses.serializers import CourseInfoModelSerializer
  11. from coupon.models import CouponLog
  12. logger = logging.getLogger("django")
  13. # Create your views here.
  14. class AlipayAPIViewSet(ViewSet):
  15. """支付宝接口"""
  16. # // ... 代码省略
  17. # // ... 代码省略
  18. # // ... 代码省略
  19. def query(self, request, order_number):
  20. """主动查询订单支付的支付结果"""
  21. try:
  22. order = Order.objects.get(order_number=order_number)
  23. if order.order_status > 1:
  24. return Response({"errmsg": "订单超时或已取消!"}, status=status.HTTP_400_BAD_REQUEST)
  25. except Order.DoesNotExist:
  26. return Response({"errmsg": "订单不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  27. # 获取当前订单相关的课程信息,用于返回给客户端
  28. order_courses = order.order_courses.all()
  29. course_list = [item.course for item in order_courses]
  30. courses_list = []
  31. for course in course_list:
  32. courses_list.append(UserCourse(course=course, user=order.user))
  33. if order.order_status == 0:
  34. # 请求支付宝,查询订单的支付结果
  35. alipay = AliPaySDK()
  36. result = alipay.query(order_number)
  37. print(f"result-{result}")
  38. if result.get("trade_status", None) in ["TRADE_FINISHED", "TRADE_SUCCESS"]:
  39. """支付成功"""
  40. with transaction.atomic():
  41. save_id = transaction.savepoint()
  42. try:
  43. now_time = datetime.now()
  44. # 1. 修改订单状态
  45. order.pay_time = now_time
  46. order.order_status = 1
  47. order.save()
  48. # 2.1 记录扣除个人积分的流水信息
  49. if order.credit > 0:
  50. Credit.objects.create(operation=1, number=order.credit, user=order.user)
  51. # 2.2 补充个人的优惠券使用记录
  52. coupon_log = CouponLog.objects.filter(order=order).first()
  53. if coupon_log:
  54. coupon_log.use_time = now_time
  55. coupon_log.use_status = 1 # 1 表示已使用
  56. coupon_log.save()
  57. # 3. 用户和课程的关系绑定
  58. user_course_list = []
  59. for course in course_list:
  60. user_course_list.append(UserCourse(course=course, user=order.user))
  61. UserCourse.objects.bulk_create(user_course_list)
  62. # todo 4. 取消订单超时
  63. except Exception as e:
  64. logger.error(f"订单支付处理同步结果发生未知错误:{e}")
  65. transaction.savepoint_rollback(save_id)
  66. return Response({"errmsg": "当前订单支付未完成!请联系客服工作人员!"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  67. else:
  68. """当前订单未支付"""
  69. return Response({"errmsg": "当前订单未支付!"}, status=status.HTTP_400_BAD_REQUEST)
  70. return Response({"errmsg":"当前订单已支付!"})

提交代码版本:

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 客户端实现支付倒计时功能"
  4. git push

虽然上面提供了支付倒计时进一步确认用户是否支付了。但是还是会存在用户不点击页面而是通过页面刷新来避开了倒计时。这样的话,我们还是存存在订单状态丢失的情况。

接受支付宝异步发送支付结果

支付宝异步通知结果相关文档:https://opendocs.alipay.com/open/270/105902

注意:程序执行完后必须打印输出“success”(不包含引号)。如果商家反馈给支付宝的字符不是 success 这7个字符,支付宝服务器会不断重发通知,直到超过 24 小时 22 分钟。一般情况下,25 小时以内完成 8 次通知(通知的间隔频率一般是:4m,10m,10m,1h,2h,6h,15h)。

payments/views.py,代码:

  1. import logging
  2. from datetime import datetime
  3. from rest_framework.viewsets import ViewSet
  4. from rest_framework.response import Response
  5. from rest_framework import status
  6. from django.db import transaction
  7. from django.http.response import HttpResponse
  8. from orders.models import Order
  9. from alipaysdk import AliPaySDK
  10. from users.models import Credit, UserCourse
  11. from courses.serializers import CourseInfoModelSerializer
  12. from coupon.models import CouponLog
  13. logger = logging.getLogger("django")
  14. # Create your views here.
  15. class AlipayAPIViewSet(ViewSet):
  16. """支付宝接口"""
  17. def link(self, request, order_number):
  18. """生成支付宝支付链接信息"""
  19. try:
  20. order = Order.objects.get(order_number=order_number)
  21. if order.order_status > 0:
  22. return Response({"message": "对不起,当前订单不能重复支付或订单已超时!"})
  23. except Order.DoesNotExist:
  24. return Response({"message": "对不起,当前订单不存在!"})
  25. # # 读取支付宝公钥与商户私钥
  26. # app_private_key_string = open(settings.ALIPAY["app_private_key_path"]).read()
  27. # alipay_public_key_string = open(settings.ALIPAY["alipay_public_key_path"]).read()
  28. #
  29. # # 创建alipay SDK操作对象
  30. # alipay = AliPay(
  31. # appid=settings.ALIPAY["appid"],
  32. # app_notify_url=settings.ALIPAY["notify_url"], # 默认全局回调 url
  33. # app_private_key_string=app_private_key_string,
  34. # # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
  35. # alipay_public_key_string=alipay_public_key_string,
  36. # sign_type=settings.ALIPAY["sign_type"], # RSA2
  37. # debug=settings.ALIPAY["debug"], # 默认 False,沙箱模式下必须设置为True
  38. # verbose=settings.ALIPAY["verbose"], # 输出调试数据
  39. # config=AliPayConfig(timeout=settings.ALIPAY["timeout"]) # 可选,请求超时时间,单位:秒
  40. # )
  41. # 生成支付信息
  42. # order_string = alipay.client_api(
  43. # "alipay.trade.page.pay", # 接口名称
  44. # biz_content={
  45. # "out_trade_no": order_number, # 订单号
  46. # "total_amount": float(order.real_price), # 订单金额 单位:元
  47. # "subject": order.name, # 订单标题
  48. # "product_code": "FAST_INSTANT_TRADE_PAY", # 产品码,目前只能支持 FAST_INSTANT_TRADE_PAY
  49. # },
  50. # return_url=settings.ALIPAY["return_url"], # 可选,同步回调地址,必须填写客户端的路径
  51. # notify_url=settings.ALIPAY["notify_url"] # 可选,不填则使用采用全局默认notify_url,必须填写服务端的路径
  52. # )
  53. #
  54. # # 拼接完整的支付链接
  55. # link = f"{settings.ALIPAY['gateway']}?{order_string}"
  56. alipay = AliPaySDK()
  57. link = alipay.page_pay(order_number, order.real_price, order.name)
  58. print(link)
  59. return Response({
  60. "pay_type": 0, # 支付类型
  61. "get_pay_type_display": "支付宝", # 支付类型的提示
  62. "link": link # 支付连接地址
  63. })
  64. def return_result(self, request):
  65. """支付宝支付结果的同步通知处理"""
  66. data = request.query_params.dict() # QueryDict
  67. alipay = AliPaySDK()
  68. success = alipay.check_sign(data)
  69. if not success:
  70. return Response({"errmsg": "通知通知结果不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  71. order_number = data.get("out_trade_no")
  72. try:
  73. order = Order.objects.get(order_number=order_number)
  74. if order.order_status > 1:
  75. return Response({"errmsg": "订单超时或已取消!"}, status=status.HTTP_400_BAD_REQUEST)
  76. except Order.DoesNotExist:
  77. return Response({"errmsg": "订单不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  78. # 获取当前订单相关的课程信息,用于返回给客户端
  79. order_courses = order.order_courses.all()
  80. course_list = [item.course for item in order_courses]
  81. if order.order_status == 0:
  82. # 请求支付宝,查询订单的支付结果
  83. result = alipay.query(order_number)
  84. print(f"result-{result}")
  85. if result.get("trade_status", None) in ["TRADE_FINISHED", "TRADE_SUCCESS"]:
  86. """支付成功"""
  87. with transaction.atomic():
  88. save_id = transaction.savepoint()
  89. try:
  90. now_time = datetime.now()
  91. # 1. 修改订单状态
  92. order.pay_time = now_time
  93. order.order_status = 1
  94. order.save()
  95. # 2.1 记录扣除个人积分的流水信息
  96. if order.credit > 0:
  97. Credit.objects.create(operation=1, number=order.credit, user=order.user)
  98. # 2.2 补充个人的优惠券使用记录
  99. coupon_log = CouponLog.objects.filter(order=order).first()
  100. if coupon_log:
  101. coupon_log.use_time = now_time
  102. coupon_log.use_status = 1 # 1 表示已使用
  103. coupon_log.save()
  104. # 3. 用户和课程的关系绑定
  105. user_course_list = []
  106. for course in course_list:
  107. user_course_list.append(UserCourse(course=course, user=order.user))
  108. UserCourse.objects.bulk_create(user_course_list)
  109. # todo 4. 取消订单超时
  110. except Exception as e:
  111. logger.error(f"订单支付处理同步结果发生未知错误:{e}")
  112. transaction.savepoint_rollback(save_id)
  113. return Response({"errmsg": "当前订单支付未完成!请联系客服工作人员!"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  114. # 返回客户端结果
  115. serializer = CourseInfoModelSerializer(course_list, many=True)
  116. return Response({
  117. "pay_time": order.pay_time.strftime("%Y-%m-%d %H:%M:%S"),
  118. "real_price": float(order.real_price),
  119. "course_list": serializer.data
  120. })
  121. def notify_result(self, request):
  122. """支付宝支付结果的异步通知处理"""
  123. # drf中接收POST参数需要使用request.data
  124. data = request.data
  125. alipay = AliPaySDK()
  126. success = alipay.check_sign(data)
  127. if not success:
  128. # 因为是属于异步处理,这个过程无法通过终端调试,因此,需要把支付发送过来的结果,记录到日志中。
  129. logger.error(f"[支付宝]>> 异步通知结果验证失败:{data}")
  130. return HttpResponse("fail")
  131. if data.get("trade_status") not in ["TRADE_FINISHED", "TRADE_SUCCESS"]:
  132. return HttpResponse("fail")
  133. # 基于支付包异步请求的支付结果中提取订单号
  134. order_number = data.get("out_trade_no")
  135. try:
  136. order = Order.objects.get(order_number=order_number)
  137. if order.order_status > 1:
  138. return HttpResponse("fail")
  139. except Order.DoesNotExist:
  140. return HttpResponse("fail")
  141. # 如果已经支付完成,则不需要继续往下处理
  142. if order.order_status == 1:
  143. return HttpResponse("success")
  144. # 获取本次下单的商品课程列表
  145. order_courses = order.order_courses.all()
  146. course_list = [item.course for item in order_courses]
  147. courses_list = []
  148. for course in course_list:
  149. courses_list.append(UserCourse(course=course, user=order.user))
  150. """支付成功"""
  151. with transaction.atomic():
  152. save_id = transaction.savepoint()
  153. try:
  154. now_time = datetime.now()
  155. # 1. 修改订单状态
  156. order.pay_time = now_time
  157. order.order_status = 1
  158. order.save()
  159. # 2.1 记录扣除个人积分的流水信息
  160. if order.credit > 0:
  161. Credit.objects.create(operation=1, number=order.credit, user=order.user)
  162. # 2.2 补充个人的优惠券使用记录
  163. coupon_log = CouponLog.objects.filter(order=order).first()
  164. if coupon_log:
  165. coupon_log.use_time = now_time
  166. coupon_log.use_status = 1 # 1 表示已使用
  167. coupon_log.save()
  168. # 3. 用户和课程的关系绑定
  169. user_course_list = []
  170. for course in course_list:
  171. user_course_list.append(UserCourse(course=course, user=order.user))
  172. UserCourse.objects.bulk_create(user_course_list)
  173. # todo 4. 取消订单超时
  174. except Exception as e:
  175. logger.error(f"订单支付处理同步结果发生未知错误:{e}")
  176. transaction.savepoint_rollback(save_id)
  177. return HttpResponse("fail")
  178. return HttpResponse("success")
  179. def query(self, request, order_number):
  180. """主动查询订单支付的支付结果"""
  181. try:
  182. order = Order.objects.get(order_number=order_number)
  183. if order.order_status > 1:
  184. return Response({"errmsg": "订单超时或已取消!"}, status=status.HTTP_400_BAD_REQUEST)
  185. except Order.DoesNotExist:
  186. return Response({"errmsg": "订单不存在!"}, status=status.HTTP_400_BAD_REQUEST)
  187. # 获取当前订单相关的课程信息,用于返回给客户端
  188. order_courses = order.order_courses.all()
  189. course_list = [item.course for item in order_courses]
  190. courses_list = []
  191. for course in course_list:
  192. courses_list.append(UserCourse(course=course, user=order.user))
  193. if order.order_status == 0:
  194. # 请求支付宝,查询订单的支付结果
  195. alipay = AliPaySDK()
  196. result = alipay.query(order_number)
  197. print(f"result-{result}")
  198. if result.get("trade_status", None) in ["TRADE_FINISHED", "TRADE_SUCCESS"]:
  199. """支付成功"""
  200. with transaction.atomic():
  201. save_id = transaction.savepoint()
  202. try:
  203. now_time = datetime.now()
  204. # 1. 修改订单状态
  205. order.pay_time = now_time
  206. order.order_status = 1
  207. order.save()
  208. # 2.1 记录扣除个人积分的流水信息
  209. if order.credit > 0:
  210. Credit.objects.create(operation=1, number=order.credit, user=order.user)
  211. # 2.2 补充个人的优惠券使用记录
  212. coupon_log = CouponLog.objects.filter(order=order).first()
  213. if coupon_log:
  214. coupon_log.use_time = now_time
  215. coupon_log.use_status = 1 # 1 表示已使用
  216. coupon_log.save()
  217. # 3. 用户和课程的关系绑定
  218. user_course_list = []
  219. for course in course_list:
  220. user_course_list.append(UserCourse(course=course, user=order.user))
  221. UserCourse.objects.bulk_create(user_course_list)
  222. # todo 4. 取消订单超时
  223. except Exception as e:
  224. logger.error(f"订单支付处理同步结果发生未知错误:{e}")
  225. transaction.savepoint_rollback(save_id)
  226. return Response({"errmsg": "当前订单支付未完成!请联系客服工作人员!"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
  227. else:
  228. """当前订单未支付"""
  229. return Response({"errmsg": "当前订单未支付!"}, status=status.HTTP_400_BAD_REQUEST)
  230. return Response({"errmsg":"当前订单已支付!"})

路由,payments/urls.py,代码:

  1. from django.urls import path,re_path
  2. from . import views
  3. urlpatterns = [
  4. re_path("^alipay/(?P<order_number>[0-9]+)/$", views.AlipayAPIViewSet.as_view({"get":"link"})),
  5. path("alipay/result/", views.AlipayAPIViewSet.as_view({"get":"return_result"})),
  6. re_path("^alipay/query/(?P<order_number>[0-9]+)/$", views.AlipayAPIViewSet.as_view({"get":"query"})),
  7. path("alipay/notify", views.AlipayAPIViewSet.as_view({"post": "notify_result"})),
  8. ]

补充:异步支付结果的处理代码的验证,只能放在线上服务器中通过支付在日志中检验。无法在本地开发中校验。因为异步通知结果是需要支付宝能访问到我们的当前站点,但是现在属于本地开发。

提交代码版本:

  1. cd /home/moluo/Desktop/luffycity
  2. git add .
  3. git commit -m "feature: 服务端接收并处理支付宝异步通知结果"
  4. git push