荏苒资讯支付宝打赏功能

前端赞赏功能弹框

要人家赞赏,首先得给人弄出打赏的弹框。前端 Article.vue 页面代码:

<template>
  <div class="_21bLU4 _3kbg6I">
    <Header></Header>
    <div class="_3VRLsv" role="main">
      <div class="_gp-ck">
        <section class="ouvJEz">
          <h1 class="_1RuRku">{{ article_detail.name }}</h1>
          <div class="rEsl9f">
            <div class="_2mYfmT">
              <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
                <img :src="article_detail.user.avatar" :alt="article_detail.user.nickname" class="_13D2Eh"/>
              </a>
              <div style="margin-left: 8px;">
                <div class="_3U4Smb">
                  <span class="FxYr8x"><a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer"
                                          target="_blank">{{ article_detail.user.nickname }}</a></span>
                  <button class="_3kba3h _1OyPqC _3Mi9q9 _34692-" data-locale="zh-CN" type="button"><span>关注</span>
                  </button>
                </div>
                <div class="s-dsoj">
                  <time>{{ article_detail.updated_time|time_format }}</time>
                  <span>字数 {{ article_detail.word_count }}</span>
                  <span>阅读 {{ article_detail.read_count }}</span>
                </div>
              </div>
            </div>
          </div>
          <article class="_2rhmJa" v-html="article_detail.render">
          </article>
          <div></div>
          <div class="_1kCBjS">
            <div class="_18vaTa">
              <div class="_3BUZPB">
                <div aria-label="给文章点赞" class="_2Bo4Th" role="button" tabindex="-1">
                  <i aria-label="ic-like" class="anticon">
                    <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                      <use xlink:href="#ic-like"></use>
                    </svg>
                  </i>
                </div>
                <span aria-label="查看点赞列表" class="_1LOh_5" role="button" tabindex="-1">
                  {{ article_detail.like_count }}人点赞
                  <i aria-label="icon: right" class="anticon anticon-right">
           <svg aria-hidden="true" class="" data-icon="right" fill="currentColor" focusable="false" height="1em"
                viewbox="64 64 896 896" width="1em">
            <path
              d="M765.7 486.8L314.9 134.7A7.97 7.97 0 0 0 302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 0 0 0-50.4z"></path>
           </svg></i></span>
              </div>
              <div class="_3BUZPB">
                <div class="_2Bo4Th" role="button" tabindex="-1">
                  <i aria-label="ic-dislike" class="anticon">
                    <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                      <use xlink:href="#ic-dislike"></use>
                    </svg>
                  </i>
                </div>
              </div>
            </div>
            <div class="_18vaTa">
              <a class="_3BUZPB _1x1ok9 _1OhGeD" href="/nb/38290018" rel="noopener noreferrer" target="_blank"><i
                aria-label="ic-notebook" class="anticon">
                <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                  <use xlink:href="#ic-notebook"></use>
                </svg>
              </i><span>随笔</span></a>
              <div class="_3BUZPB ant-dropdown-trigger">
                <div class="_2Bo4Th">
                  <i aria-label="ic-others" class="anticon">
                    <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                      <use xlink:href="#ic-others"></use>
                    </svg>
                  </i>
                </div>
              </div>
            </div>
          </div>
          <div class="_19DgIp" style="margin-top:24px;margin-bottom:24px"></div>
          <div class="_13lIbp">
            <div class="_191KSt">
              "小礼物走一走,来简书关注我"
            </div>
            <button class="_1OyPqC _3Mi9q9 _2WY0RL _1YbC5u" type="button" @click.stop="show_reward_window=true">
              <span>赞赏支持</span>
            </button>
            <span class="_3zdmIj">还没有人赞赏,支持一下</span>
          </div>
          <div class="d0hShY">
            <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
              <img :alt="article_detail.user.nickname" class="_27NmgV" :src="article_detail.user.avatar"/>
            </a>
            <div class="Uz-vZq">
              <div class="Cqpr1X">
                <a class="HC3FFO _1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank"
                   :title="article_detail.user.nickname">{{ article_detail.user.nickname }}</a>
                <span class="_2WEj6j" title="你读书的样子真好看。">你读书的样子真好看。</span>
              </div>
              <div class="lJvI3S">
                <span>总资产0</span>
                <span>共写了78.7W字</span>
                <span>获得6,072个赞</span>
                <span>共1,308个粉丝</span>
              </div>
            </div>
            <button class="_1OyPqC _3Mi9q9" data-locale="zh-CN" type="button"><span>关注</span></button>
          </div>
        </section>
        <div id="note-page-comment">
          <div class="lazyload-placeholder"></div>
        </div>
      </div>
      <aside class="_2OwGUo">
        <section class="_3Z3nHf">
          <div class="_3Oo-T1">
            <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
              <img alt="" class="_3T9iJQ" :src="article_detail.user.avatar"/></a>
            <div class="_32ZTTG">
              <div class="_2O0T_w">
                <div class="_2v-h3G">
                  <span class="_2vh4fr" :title="article_detail.user.nickname">
                    <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
                      {{ article_detail.user.nickname }}
                    </a>
                  </span>
                </div>
                <button class="tzrf9N _1OyPqC _3Mi9q9 _34692-" data-locale="zh-CN" type="button"><span>关注</span>
                </button>
              </div>
              <div class="_1pXc22">
                总资产0
              </div>
            </div>
          </div>
          <div class="_19DgIp"></div>
        </section>
        <div>
          <div class="">
            <section class="_3Z3nHf">
              <h3 class="QHRnq8 QxT4hD"><span>推荐阅读</span></h3>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="这些话没人告诉你,但必须知道的社会规则">
                  <a class="_1-HJSV _1OhGeD" href="/p/a3e56a0559ff" rel="noopener noreferrer" target="_blank">这些话没人告诉你,但必须知道的社会规则</a>
                </div>
                <div class="_19haGh">
                  阅读 5,837
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="浙大学霸最美笔记曝光:真正的牛人,都“变态”到了极致">
                  <a class="_1-HJSV _1OhGeD" href="/p/d2a3724e2839" rel="noopener noreferrer" target="_blank">浙大学霸最美笔记曝光:真正的牛人,都“变态”到了极致</a>
                </div>
                <div class="_19haGh">
                  阅读 12,447
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="征服一个女人最好的方式:不是讨好她,而是懂得去折腾她">
                  <a class="_1-HJSV _1OhGeD" href="/p/f6acf67f039b" rel="noopener noreferrer" target="_blank">征服一个女人最好的方式:不是讨好她,而是懂得去折腾她</a>
                </div>
                <div class="_19haGh">
                  阅读 5,311
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="告别平庸的15个小方法">
                  <a class="_1-HJSV _1OhGeD" href="/p/cff7eb6b232b" rel="noopener noreferrer" target="_blank">告别平庸的15个小方法</a>
                </div>
                <div class="_19haGh">
                  阅读 7,040
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="轻微抑郁的人,会说这3句“口头禅”,若你一个不占,偷着乐吧">
                  <a class="_1-HJSV _1OhGeD" href="/p/2a0ca1729b4b" rel="noopener noreferrer" target="_blank">轻微抑郁的人,会说这3句“口头禅”,若你一个不占,偷着乐吧</a>
                </div>
                <div class="_19haGh">
                  阅读 16,411
                </div>
              </div>
            </section>
          </div>
        </div>
      </aside>
    </div>
    <div class="_23ISFX-body" v-if="show_reward_window" @click.stop="show_reward_window=true">
      <div class="_3uZ5OL">
        <div class="_2PLkjk">
          <img class="_2R1-48"
               src="https://upload.jianshu.io/users/upload_avatars/9602437/8fb37921-2e4f-42a7-8568-63f187c5721b.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/100/h/100/format/webp"
               alt=""/>
          <div class="_2h5tnQ">
            给作者送糖
          </div>
        </div>
        <div class="_1-bCJJ">
          <div class="LMa6S_" :class="reward_info.money===num?'_1vONvL':''" @click="reward_info.money=num"
               v-for="num in reward_list"><span>{{num}}</span></div>
        </div>
        <textarea class="_1yN79W" placeholder="给Ta留言..."></textarea>
        <div class="_1_B577">
          选择支付方式
        </div>
        <div class="_1-bCJJ">
          <div class="LMa6S_ _3PA8BN" :class="{'_1vONvL': reward_info.pay_type===type}"
               @click="reward_info.pay_type=type" v-for="type in pay_type_list"><span>{{type}}</span></div>
        </div>
        <button type="button" class="_3A-4KL _1OyPqC _3Mi9q9 _1YbC5u">
          <span>确认支付</span><span> ¥</span>{{reward_info.money}}
        </button>
      </div>
    </div>
    <Footer></Footer>
  </div>
</template>

<script>
    import Header from "./common/Header";
    import Footer from "./common/Footer";

    export default {
        name: "Article",
        components: {
            Header,
            Footer,
        },
        data() {
            return {
                article_id: 0,
                token: '',
                article_detail: {
                    user: {}
                },
                show_reward_window: false,
                reward_list: [2, 5, 10, 20, 50, 100],
                pay_type_list: ['支付宝', '账户余额'],
                reward_info: {
                    'money': this.reward_list[0],
                    'pay_type': '支付宝'
                },
            }
        },
        filters: {
            time_format(time) {
                let t = new Date(time);
                return `${t.getFullYear()}.${t.getMonth() + 1}.${t.getDate()} ${t.getHours()}:${t.getMinutes()}`;
            },
        },
        methods: {
            get_article_detail() {
                this.$axios.get(`${this.$settings.Host}/article/detail/${this.article_id}/`).then(response => {
                    this.article_detail = response.data;
                }).catch(errors => {
                    this.$message.error('获取文章详情信息失败!')
                })
            },

        },
        created() {
            this.article_id = this.$route.params.pk;
            this.token = localStorage.user_token || sessionStorage.user_token;
            this.get_article_detail();
        },
        mounted() {
            document.onclick = () => {
                this.show_reward_window = false;
            }
        }
    }
</script>

第三方支付接口

第三方支付接口,可以实现网络转账。

常见的第三方支付接口有很多:

国外:万事达,applePay,PayPal,Visa,八达通,西联(邮政汇款)

国内:支付宝,微信,京东钱包,百度钱包,贝宝(PayPal 中国版)

支付宝支付接口

支付宝开放平台登录

支付宝开放平台官网:https://open.alipay.com/platform/home.htm

使用支付宝账号登录即可。如果是第一次登录,可能会要求等级一些信息,如是填写就好。

支付宝的申请需要企业资质,但是我们作为开发者可以使用支付宝提供的测试账号先开发功能,将来调整账号即可用于公司项目的正式运营。

地址:https://openhome.alipay.com/platform/developerIndex.htm

沙箱环境

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

支付宝开发者文档

电脑网站支付流程

【前后端不分离】时序图(时间顺序流程图 )

【前后端分离】时序图

RSA 算法,属于非对称加密,一旦加密以后不能解密的。
可以通过密钥来进行验证。
密钥成对生成的。分公钥和私钥。
公钥用于验证数据(解签)
私钥用于加密数据(签名)

开发支付功能

首先创建专门用于第三方支付的 payments 子应用:

cd renranapi/apps
python ../../manage.py startapp payments

注册子应用

INSTALLED_APPS = [
	...
    'payments',
]

配置秘钥

生成应用的私钥和公钥

下载对应系统的秘钥生成工具:https://doc.open.alipay.com/docs/doc.htm?treeId=291&articleId=105971&docType=1

Windows 操作系统

生成如下,安装软件时需要管理员身份来安装。

Linux 系统

生成方法如下:

cd renranapi/apps/payments/
mkdir keys
cd keys
openssl
OpenSSL> genrsa -out app_private_key.pem 2048                         # 生成私钥到指定文件中
OpenSSL> rsa -in app_private_key.pem -pubout -out app_public_key.pem  # 导出公钥
OpenSSL> exit

先要注册应用,然后设置接口加签方式。

应用公钥复制粘贴到支付宝网站页面中。点击修改以后,粘贴进去。

如果是使用沙箱测试,则需要在沙箱应用处配置应用公钥,从而获取支付宝公钥。

保存应用私钥文件

在 payments 应用中新建 keys 目录,用来保存秘钥文件。

将应用私钥文件 app_private_key.pem 复制到 payment/keys 目录下。

Windows 系统生成的私钥必须在上下两行加上以下标识:

-----BEGIN RSA PRIVATE KEY-----
私钥
-----END RSA PRIVATE KEY-----

保存支付宝公钥到项目中

在 payments/key 目录下新建 alipay_public_key.pem 文件,用于保存支付宝的公钥文件。

将支付宝的公钥内容复制到 alipay_public_key.pem 文件中

-----BEGIN PUBLIC KEY-----
公钥
-----END PUBLIC KEY-----

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

SDK:https://docs.open.alipay.com/270/106291/

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

安装命令:

pip install python-alipay-sdk --upgrade

后端提供发起支付的接口 url 地址

用户模型新增 money 字段,表示作者用户接收别人打赏的资金资产。

class User(AbstractUser):
    """用户模型类"""
    mobile = models.CharField(max_length=15, null=True, unique=True, help_text="手机号码", verbose_name="手机号码")
    wechat = models.CharField(max_length=100, null=True, unique=True, help_text="微信账号", verbose_name="微信账号")
    alipay = models.CharField(max_length=100, null=True, unique=True, help_text="支付宝账号", verbose_name="支付宝账号")
    qq_number = models.CharField(max_length=11, null=True, unique=True, help_text="QQ号", verbose_name="QQ号")
    # 保存文件的子目录
    avatar = models.ImageField(upload_to="avatar", null=True, default=None, verbose_name="头像")
    nickname = models.CharField(max_length=100, null=True, default=None, verbose_name="用户昵称")
    money = models.DecimalField(max_digits=11, decimal_places=2, default=0, verbose_name="资金")

    class Meta:
        db_table = "rr_users"
        verbose_name = "用户信息"
        verbose_name_plural = verbose_name

    def __str__(self):
        return self.username

payments/models.py 中创建模型,保存打赏记录:

from django.db import models
from users.models import User
from article.models import Article
from renranapi.utils.models import BaseModel

class Reward(BaseModel):
    REWARD_OPT = (
        (0, "支付宝"),
        (1, "余额"),
    )
    STATUS_OPT = (
        (0, "未付款"),
        (1, "已付款"),
        (2, "已取消"),
        (3, "超时取消"),
    )
    user = models.ForeignKey(User, on_delete=models.DO_NOTHING, verbose_name="打赏用户")
    money = models.DecimalField(decimal_places=2, max_digits=6, verbose_name="打赏金额")
    article = models.ForeignKey(Article, on_delete=models.DO_NOTHING, verbose_name="文章")
    status = models.SmallIntegerField(default=0,choices=STATUS_OPT, verbose_name="打赏状态")
    trade_no = models.CharField(max_length=255, null=True, blank=True, verbose_name="流水号")
    out_trade_no = models.CharField(max_length=255, null=True, blank=True, verbose_name="支付平台返回的流水号")
    reward_type = models.SmallIntegerField(default=0,choices=REWARD_OPT, verbose_name="打赏类型")
    message = models.TextField(null=True,blank=True, verbose_name="打赏留言")

    class Meta:
        db_table = "rr_reward"
        verbose_name = "打赏记录"
        verbose_name_plural = verbose_name

    def __str__(self):
        return f'{self.user.nickname}{self.article.user.nickname}的文章《{self.article.name}》打赏了{self.money}元'

迁移迁移

python manage.py makemigrations
python manage.py migrate

注册模型到 xadmin 中,创建 adminx.py,代码

import xadmin

from .models import Reward
class RewardModelAdmin(object):
    pass
xadmin.site.register(Reward,RewardModelAdmin)

编写视图提供支付的 url 跳转链接地址:

import random
from datetime import datetime

from alipay import AliPay
from django.conf import settings
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet

from . import models
from article.models import Article


class AlipayAPIViewSet(ViewSet):
    permission_classes = [IsAuthenticated]
    def post(self, request):
        """生成打赏链接,并创建打赏记录"""
        user = request.user
        reward_type = request.data.get('pay_type')
        message = request.data.get('content')
        money = request.data.get('money')
        # 打赏金额应该有效
        if money <= 0:
            return Response({'error_msg': '打赏金额应该大于 0!'}, status=status.HTTP_400_BAD_REQUEST)
        article_id = request.data.get('article_id')
        # 确保文章存在
        try:
            Article.objects.get(id=article_id, is_public=True)
        except Article.DoesNotExist:
            return Response({'error_msg': '文章不存在或尚未发布,无法打赏'}, status=status.HTTP_400_BAD_REQUEST)
        # 确保文章的作者不是自己
        if Article.object.filter(user=user, id=article_id):
            return Response({'error_mes': '不能给自己的文章打赏!'}, status=status.HTTP_400_BAD_REQUEST)
        # 生成唯一随机流水号
        while 1:
            trade_no = f'{datetime.now().strftime("%Y%m%d%H%M%S")}{"%06d" % user.id}{"%06d" % random.randint(1, 999999)}'
            try:
                models.Reward.objects.get(trade_no=trade_no)
            except models.Reward.DoesNotExist:
                break

        reward = models.Reward.objects.create(
            user=user,
            money=money,
            article_id=article_id,
            status=0,
            trade_no=trade_no,
            out_trade_no=None,
            reward_type=reward_type,
            message=message,
            orders=0,
        )
        # 以支付宝的方式打赏
        if reward_type == 0:
            # 生成支付链接
            # 获取公钥和私钥
            with open(settings.ALIAPY_CONFIG["app_private_key_path"]) as fh:
                app_private_key_string = fh.read()
            with open(settings.ALIAPY_CONFIG["alipay_public_key_path"]) as fh:
                alipay_public_key_string = fh.read()
            # 初始化支付对象
            alipay = AliPay(
                appid=settings.ALIAPY_CONFIG["appid"],
                app_notify_url=settings.ALIAPY_CONFIG["app_notify_url"],  # 默认回调url
                app_private_key_string=app_private_key_string,
                # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
                alipay_public_key_string=alipay_public_key_string,
                sign_type=settings.ALIAPY_CONFIG["sign_type"],
                debug=settings.ALIAPY_CONFIG["debug"]  # 默认False
            )

            # 调用接口
            order_string = alipay.api_alipay_trade_page_pay(
                out_trade_no=reward.trade_no,
                total_amount=float(reward.money),  # 打赏金额
                subject="打赏文章",
                return_url=settings.ALIAPY_CONFIG["return_url"],
                notify_url=settings.ALIAPY_CONFIG["notify_url"]  # 可选, 不填则使用默认notify url
            )

            url = settings.ALIAPY_CONFIG["gateway_url"] + order_string
        else:
            url = ''
        return Response(url)

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

setttins/dev.py 中添加支付宝接口相关的配置代码:

# 支付宝相关配置
ALIAPY_CONFIG = {
    # "gateway_url": "https://openapi.alipay.com/gateway.do?",    # 真实支付宝网关地址
    "gateway_url": "https://openapi.alipaydev.com/gateway.do?",    # 沙箱支付宝网关地址
    "appid": "2016101900722372",   # 沙箱应用的 appid
    "app_notify_url": None,
    "app_private_key_path": os.path.join(BASE_DIR, "apps/payments/keys/app_private_key.pem"),    # 本地生成的私钥
    "alipay_public_key_path": os.path.join(BASE_DIR, "apps/payments/keys/alipay_public_key.pem"),    # 使用本地公钥在沙箱生成的公钥
    "sign_type": "RSA2",
    "debug": False,
    "return_url": "http://www.moluo.net:8080/wallet",    # 同步回调地址
    "notify_url": "http://api.renran.cn:8000/payments/alipay/result/",    # 异步结果通知
}

注册 url 地址,payments/urls.py 代码:

from django.urls import path,re_path
from . import views
urlpatterns = [
    path('alipay/', views.AlipayAPIViewSet.as_view({'post': 'post'})),
]

总路由,代码:

path('payments/', include('payments.urls')),

客户端实现点击打赏请求

<template>
  <div class="_21bLU4 _3kbg6I">
    <Header></Header>
    <div class="_3VRLsv" role="main">
      <div class="_gp-ck">
        <section class="ouvJEz">
          <h1 class="_1RuRku">{{ article_detail.name }}</h1>
          <div class="rEsl9f">
            <div class="_2mYfmT">
              <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
                <img :src="article_detail.user.avatar" :alt="article_detail.user.nickname" class="_13D2Eh"/>
              </a>
              <div style="margin-left: 8px;">
                <div class="_3U4Smb">
                  <span class="FxYr8x"><a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer"
                                          target="_blank">{{ article_detail.user.nickname }}</a></span>
                  <button class="_3kba3h _1OyPqC _3Mi9q9 _34692-" data-locale="zh-CN" type="button"><span>关注</span>
                  </button>
                </div>
                <div class="s-dsoj">
                  <time>{{ article_detail.updated_time|time_format }}</time>
                  <span>字数 {{ article_detail.word_count }}</span>
                  <span>阅读 {{ article_detail.read_count }}</span>
                </div>
              </div>
            </div>
          </div>
          <article class="_2rhmJa" v-html="article_detail.render">
          </article>
          <div></div>
          <div class="_1kCBjS">
            <div class="_18vaTa">
              <div class="_3BUZPB">
                <div aria-label="给文章点赞" class="_2Bo4Th" role="button" tabindex="-1">
                  <i aria-label="ic-like" class="anticon">
                    <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                      <use xlink:href="#ic-like"></use>
                    </svg>
                  </i>
                </div>
                <span aria-label="查看点赞列表" class="_1LOh_5" role="button" tabindex="-1">
                  {{ article_detail.like_count }}人点赞
                  <i aria-label="icon: right" class="anticon anticon-right">
           <svg aria-hidden="true" class="" data-icon="right" fill="currentColor" focusable="false" height="1em"
                viewbox="64 64 896 896" width="1em">
            <path
              d="M765.7 486.8L314.9 134.7A7.97 7.97 0 0 0 302 141v77.3c0 4.9 2.3 9.6 6.1 12.6l360 281.1-360 281.1c-3.9 3-6.1 7.7-6.1 12.6V883c0 6.7 7.7 10.4 12.9 6.3l450.8-352.1a31.96 31.96 0 0 0 0-50.4z"></path>
           </svg></i></span>
              </div>
              <div class="_3BUZPB">
                <div class="_2Bo4Th" role="button" tabindex="-1">
                  <i aria-label="ic-dislike" class="anticon">
                    <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                      <use xlink:href="#ic-dislike"></use>
                    </svg>
                  </i>
                </div>
              </div>
            </div>
            <div class="_18vaTa">
              <a class="_3BUZPB _1x1ok9 _1OhGeD" href="/nb/38290018" rel="noopener noreferrer" target="_blank"><i
                aria-label="ic-notebook" class="anticon">
                <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                  <use xlink:href="#ic-notebook"></use>
                </svg>
              </i><span>随笔</span></a>
              <div class="_3BUZPB ant-dropdown-trigger">
                <div class="_2Bo4Th">
                  <i aria-label="ic-others" class="anticon">
                    <svg aria-hidden="true" class="" fill="currentColor" focusable="false" height="1em" width="1em">
                      <use xlink:href="#ic-others"></use>
                    </svg>
                  </i>
                </div>
              </div>
            </div>
          </div>
          <div class="_19DgIp" style="margin-top:24px;margin-bottom:24px"></div>
          <div class="_13lIbp">
            <div class="_191KSt">
              "小礼物走一走,来简书关注我"
            </div>
            <button class="_1OyPqC _3Mi9q9 _2WY0RL _1YbC5u" type="button" @click.stop="article_reward">
              <span>赞赏支持</span>
            </button>
            <span class="_3zdmIj">还没有人赞赏,支持一下</span>
          </div>
          <div class="d0hShY">
            <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
              <img :alt="article_detail.user.nickname" class="_27NmgV" :src="article_detail.user.avatar"/>
            </a>
            <div class="Uz-vZq">
              <div class="Cqpr1X">
                <a class="HC3FFO _1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank"
                   :title="article_detail.user.nickname">{{ article_detail.user.nickname }}</a>
                <span class="_2WEj6j" title="你读书的样子真好看。">你读书的样子真好看。</span>
              </div>
              <div class="lJvI3S">
                <span>总资产0</span>
                <span>共写了78.7W字</span>
                <span>获得6,072个赞</span>
                <span>共1,308个粉丝</span>
              </div>
            </div>
            <button class="_1OyPqC _3Mi9q9" data-locale="zh-CN" type="button"><span>关注</span></button>
          </div>
        </section>
        <div id="note-page-comment">
          <div class="lazyload-placeholder"></div>
        </div>
      </div>
      <aside class="_2OwGUo">
        <section class="_3Z3nHf">
          <div class="_3Oo-T1">
            <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
              <img alt="" class="_3T9iJQ" :src="article_detail.user.avatar"/></a>
            <div class="_32ZTTG">
              <div class="_2O0T_w">
                <div class="_2v-h3G">
                  <span class="_2vh4fr" :title="article_detail.user.nickname">
                    <a class="_1OhGeD" href="/u/a70487cda447" rel="noopener noreferrer" target="_blank">
                      {{ article_detail.user.nickname }}
                    </a>
                  </span>
                </div>
                <button class="tzrf9N _1OyPqC _3Mi9q9 _34692-" data-locale="zh-CN" type="button"><span>关注</span>
                </button>
              </div>
              <div class="_1pXc22">
                总资产0
              </div>
            </div>
          </div>
          <div class="_19DgIp"></div>
        </section>
        <div>
          <div class="">
            <section class="_3Z3nHf">
              <h3 class="QHRnq8 QxT4hD"><span>推荐阅读</span></h3>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="这些话没人告诉你,但必须知道的社会规则">
                  <a class="_1-HJSV _1OhGeD" href="/p/a3e56a0559ff" rel="noopener noreferrer" target="_blank">这些话没人告诉你,但必须知道的社会规则</a>
                </div>
                <div class="_19haGh">
                  阅读 5,837
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="浙大学霸最美笔记曝光:真正的牛人,都“变态”到了极致">
                  <a class="_1-HJSV _1OhGeD" href="/p/d2a3724e2839" rel="noopener noreferrer" target="_blank">浙大学霸最美笔记曝光:真正的牛人,都“变态”到了极致</a>
                </div>
                <div class="_19haGh">
                  阅读 12,447
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="征服一个女人最好的方式:不是讨好她,而是懂得去折腾她">
                  <a class="_1-HJSV _1OhGeD" href="/p/f6acf67f039b" rel="noopener noreferrer" target="_blank">征服一个女人最好的方式:不是讨好她,而是懂得去折腾她</a>
                </div>
                <div class="_19haGh">
                  阅读 5,311
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="告别平庸的15个小方法">
                  <a class="_1-HJSV _1OhGeD" href="/p/cff7eb6b232b" rel="noopener noreferrer" target="_blank">告别平庸的15个小方法</a>
                </div>
                <div class="_19haGh">
                  阅读 7,040
                </div>
              </div>
              <div class="cuOxAY" role="listitem">
                <div class="_3L5YSq" title="轻微抑郁的人,会说这3句“口头禅”,若你一个不占,偷着乐吧">
                  <a class="_1-HJSV _1OhGeD" href="/p/2a0ca1729b4b" rel="noopener noreferrer" target="_blank">轻微抑郁的人,会说这3句“口头禅”,若你一个不占,偷着乐吧</a>
                </div>
                <div class="_19haGh">
                  阅读 16,411
                </div>
              </div>
            </section>
          </div>
        </div>
      </aside>
    </div>
    <div class="_23ISFX-body" v-if="show_reward_window" @click.stop="show_reward_window=true">
      <div class="_3uZ5OL">
        <div class="_2PLkjk">
          <img class="_2R1-48"
               src="https://upload.jianshu.io/users/upload_avatars/9602437/8fb37921-2e4f-42a7-8568-63f187c5721b.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/100/h/100/format/webp"
               alt=""/>
          <div class="_2h5tnQ">
            给作者送糖
          </div>
        </div>
        <div class="_1-bCJJ">
          <div class="LMa6S_" :class="reward_info.money===num?'_1vONvL':''" @click="reward_info.money=num"
               v-for="num in reward_list"><span>{{num}}</span></div>
        </div>
        <textarea class="_1yN79W" placeholder="给Ta留言..." v-model="reward_msg"></textarea>
        <div class="_1_B577">
          选择支付方式
        </div>
        <div class="_1-bCJJ">
          <div class="LMa6S_ _3PA8BN" :class="{'_1vONvL': reward_info.pay_type===key}"
               @click="reward_info.pay_type=key" v-for="type,key in pay_type_list" :key="key">
            <span>{{type}}</span>
          </div>
        </div>
        <button type="button" class="_3A-4KL _1OyPqC _3Mi9q9 _1YbC5u" @click="confirm_payment">
          <span>确认支付</span><span> ¥</span>{{reward_info.money}}
        </button>
      </div>
    </div>
    <Footer></Footer>
  </div>
</template>

<script>
    import Header from "./common/Header";
    import Footer from "./common/Footer";

    export default {
        name: "Article",
        components: {
            Header,
            Footer,
        },
        data() {
            return {
                article_id: 0,
                token: '',
                article_detail: {
                    user: {}
                },
                show_reward_window: false,
                reward_list: [2, 5, 10, 20, 50, 100],
                pay_type_list: ['支付宝', '账户余额'],
                reward_info: {
                    'money': 2,
                    'pay_type': 0
                },
                reward_msg: '',
            }
        },
        filters: {
            time_format(time) {
                let t = new Date(time);
                return `${t.getFullYear()}.${t.getMonth() + 1}.${t.getDate()} ${t.getHours()}:${t.getMinutes()}`;
            },
        },
        methods: {
            get_article_detail () {
                this.$axios.get(`${this.$settings.Host}/article/detail/${this.article_id}/`).then(response => {
                    this.article_detail = response.data;
                }).catch(errors => {
                    this.$message.error('获取文章详情信息失败!')
                })
            },
            article_reward () {
                this.$settings.check_login(this);
                this.show_reward_window=true;
            },
            confirm_payment () {
                this.$axios.post(`${this.$settings.Host}/payments/alipay/`, {
                    article_id: this.article_id,
                    pay_type: this.reward_info.pay_type,
                    money: this.reward_info.money,
                    message: this.reward_msg,
                }, {
                    headers: {
                        Authorization: `jwt ${this.token}`
                    }
                }).then(response=>{
                    location.href = response.data
                }).catch(errors=>{
                    this.$message.error('获取支付宝链接失败!')
                })
            },
        },
        created() {
            this.article_id = this.$route.params.pk;
            this.token = localStorage.user_token || sessionStorage.user_token;
            this.get_article_detail();
        },
        mounted() {
            document.onclick = () => {
                this.show_reward_window = false;
            }
        }
    }
</script>

用户支付完成以后的支付结果处理

客户端接收支付宝跳转发送回来的同步结果参数,并发起请求服务端的同步处理结果的 API 接口。

首先创建前端页面组件,Wallet.vue

<template>

</template>

<script>
    export default {
        name: "Wallet",
        data () {
            return {
                token: '',
            }
        },
        created() {
            this.token = this.$settings.check_login(this);
            // 把支付宝的同步结果通知转发给服务端
            this.get_pay_result();
        },
        methods: {
            get_pay_result() {
                // 判断是否是从支付页面返回
                if(this.$route.query.out_trade_no) {
                    // 将结果返回给后台
                    this.$axios.get(`${this.$settings.Host}/payments/alipay/result/${location.search}`, {
                        headers: {
                            Authorization: `jwt ${this.token}`
                        }
                    }).then(response=>{
                        this.$router.push('/')
                    }).catch(errors=>{
                        this.$router.push('/')
                    })
                }
            }

        }
    }
</script>

<style scoped>

</style>

路由代码:

import Wallet from "../components/Wallet";

export default new Router({
	...
    {
      path: '/wallet',
      name: 'Wallet',
      component: Wallet,
    },
  ]
})

服务端完成同步支付结果和异步通知的处理

用户付过钱之后,会跳转到回调页面,也就是 wallet。url 中会携带付款情况的各种参数。前端 wallet 页面会把这些参数传给后端,我们就在这里处理一下。除了前端会有支付结果返回外,支付宝还会给我们的后端发送异步通知。这两个通知的数据是一样的,只不过一个是 get 请求,一个是 post 请求,在获取数据的时候稍微处理一下即可。

首先是在视图中,增加一个 return_result 方法,用来处理前端发送过来的通知参数和支付宝给我们后端发送的通知:

import random
from datetime import datetime

from alipay import AliPay
from django.conf import settings
from django.db import transaction
from rest_framework import status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.viewsets import ViewSet

from . import models
from article.models import Article


class AlipayAPIViewSet(ViewSet):
    permission_classes = [IsAuthenticated]

    def get_alipay(self):
        # 生成支付链接
        # 获取公钥和私钥
        with open(settings.ALIAPY_CONFIG["app_private_key_path"]) as fh:
            app_private_key_string = fh.read()
        with open(settings.ALIAPY_CONFIG["alipay_public_key_path"]) as fh:
            alipay_public_key_string = fh.read()
        # 初始化支付对象
        alipay = AliPay(
            appid=settings.ALIAPY_CONFIG["appid"],
            app_notify_url=settings.ALIAPY_CONFIG["app_notify_url"],  # 默认回调url
            app_private_key_string=app_private_key_string,
            # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
            alipay_public_key_string=alipay_public_key_string,
            sign_type=settings.ALIAPY_CONFIG["sign_type"],
            debug=settings.ALIAPY_CONFIG["debug"]  # 默认False
        )
        return alipay

    def post(self, request):
        """生成打赏链接,并创建打赏记录"""
        user = request.user
        reward_type = request.data.get('pay_type')
        message = request.data.get('content')
        money = request.data.get('money')
        # 打赏金额应该有效
        if money <= 0:
            return Response({'error_msg': '打赏金额应该大于 0!'}, status=status.HTTP_400_BAD_REQUEST)
        article_id = request.data.get('article_id')
        # 确保文章存在
        try:
            Article.objects.get(id=article_id, is_public=True)
        except Article.DoesNotExist:
            return Response({'error_msg': '文章不存在或尚未发布,无法打赏'}, status=status.HTTP_400_BAD_REQUEST)
        # 确保文章的作者不是自己
        if Article.objects.filter(user=user, id=article_id):
            return Response({'error_mes': '不能给自己的文章打赏!'}, status=status.HTTP_400_BAD_REQUEST)
        # 生成唯一随机流水号
        while 1:
            trade_no = f'{datetime.now().strftime("%Y%m%d%H%M%S")}{"%06d" % user.id}{"%06d" % random.randint(1, 999999)}'
            try:
                models.Reward.objects.get(trade_no=trade_no)
            except models.Reward.DoesNotExist:
                break

        reward = models.Reward.objects.create(
            user=user,
            money=money,
            article_id=article_id,
            status=0,
            trade_no=trade_no,
            out_trade_no=None,
            reward_type=reward_type,
            message=message,
            orders=0,
        )
        # 以支付宝的方式打赏
        if reward_type == 0:
            alipay = self.get_alipay()
            # 调用接口
            order_string = alipay.api_alipay_trade_page_pay(
                out_trade_no=reward.trade_no,
                total_amount=float(reward.money),  # 打赏金额
                subject="打赏文章",
                return_url=settings.ALIAPY_CONFIG["return_url"],
                notify_url=settings.ALIAPY_CONFIG["notify_url"]  # 可选, 不填则使用默认notify url
            )

            url = settings.ALIAPY_CONFIG["gateway_url"] + order_string
        else:
            url = ''
        return Response(url)

    def return_result(self, request):
        """支付宝同步结果和异步通知处理"""
        # post和get请求获取data的方式略有不同,需要处理一下
        # 支付宝的异步回调需要有域名,且可以解析到IP,需要在服务器中测试使用
        data = request.query_params.dict() or request.data.dict()
        signature = data.pop('sign')
        alipay = self.get_alipay()
        success = alipay.verify(data, signature)
        if success:
            """支付结果处理"""
            # 开启ORM的MySQL事务的自动提交,在with语句范围内,所有的SQL会全部被事务控制,要么一起提交,要么一起不提交
            with transaction.atomic():
                # 设置事务的回滚点,用于指定在事务失败时,在哪一部分的SQL语句无效
                save_point = transaction.savepoint()
                try:
                    # 修改打赏记录的状态为已付款
                    reward = models.Reward.objects.get(trade_no=data.get('out_trade_no'), status=0)
                    reward.status = 1
                    reward.save()
                    # 增加文章的打赏人数
                    article = reward.article
                    article.reward_count += 1
                    article.save()
                    # 给文章作者资产增加打赏的资金
                    author = article.user
                    author.money = round(author.money + reward.money, 2)
                    author.save()
                except:
                    transaction.savepoint_rollback(save_point)
                    return Response({'error_msg': '支付有误!'}, status=status.HTTP_400_BAD_REQUEST)
        else:
            return Response({'error_msg': '支付有误!'}, status=status.HTTP_400_BAD_REQUEST)

路由代码:

from django.urls import path,re_path
from . import views
urlpatterns = [
    path("alipay/", views.AliPayAPIViewSet.as_view({"post":"post"})),
    path("alipay/result/", views.AliPayAPIViewSet.as_view({"get":"return_result","post":"return_result"})),
]

支付宝功能就此实现,使用支付宝沙箱账号可以进行测试。

因为支付宝的异步通知是要发送请求给域名,需要在服务器中才能测试。