0%

荏苒资讯支付宝打赏功能

前端赞赏功能弹框

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
<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">
&quot;小礼物走一走,来简书关注我&quot;
</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

沙箱环境

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

支付宝开发者文档

电脑网站支付流程

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

【前后端分离】时序图

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

开发支付功能

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

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

注册子应用

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

配置秘钥

生成应用的私钥和公钥

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

Windows 操作系统

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

Linux 系统

生成方法如下:

1
2
3
4
5
6
7
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 系统生成的私钥必须在上下两行加上以下标识:

1
2
3
-----BEGIN RSA PRIVATE KEY-----
私钥
-----END RSA PRIVATE KEY-----

保存支付宝公钥到项目中

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

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

1
2
3
-----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

安装命令:

1
pip install python-alipay-sdk --upgrade

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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 中创建模型,保存打赏记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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}元'

迁移迁移

1
2
python manage.py makemigrations
python manage.py migrate

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

1
2
3
4
5
6
import xadmin

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
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 中添加支付宝接口相关的配置代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
# 支付宝相关配置
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 代码:

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

总路由,代码:

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

客户端实现点击打赏请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
<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">
&quot;小礼物走一走,来简书关注我&quot;
</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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<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>

路由代码:

1
2
3
4
5
6
7
8
9
10
11
import Wallet from "../components/Wallet";

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

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

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

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
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)

路由代码:

1
2
3
4
5
6
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"})),
]

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

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