0 视频托管
# 1 文件:头像,文件,视频---》使用第三方托管
-阿里oss
-七牛云
-腾讯
# 2 自己搭建 文件服务器
-fastdfs:适合存储小文件
-minIO:第三开源文件存储
# 3 桶:bucket
# 4 文件上传到七牛云
-python
-js
# 使用代码上传到 minio
#pip install minio
from minio import Minio
# 使用endpoint、access key和secret key来初始化minioClient对象。
minioClient = Minio('192.168.1.252:9000',
access_key='B1SKQVR6PRS1DT0NCSYM',
secret_key='Nqk3O0lHsbrv58OtyiMoCI41ZnTmSCMhsZZ2hptS',
secure=False)
# 调用make_bucket来创建一个存储桶。
# minioClient.make_bucket("maylogs", location="us-east-1")
# test01 为桶名字
res = minioClient.fput_object('lqz-test', 'lqz.jpg', './lqz.jpg')
print(res.object_name)
print('http://192.168.1.252:9090/lqz-test/lqz.jpg')
print('文件地址为【文件在浏览器打开会直接下载,放到index.html 中使用img引入查看】:\n', 'http://192.168.1.252:9000/test01/' + res.object_name)
# pip3 install py3Fdfs
from fdfs_client.client import get_tracker_conf, Fdfs_client
tracker_conf = get_tracker_conf('./client.conf')
client = Fdfs_client(tracker_conf)
#文件上传
# result = client.upload_by_filename('./lqz.jpg')
# print(result)
# {'Group name': b'group1', 'Remote file_id': b'group1/M00/00/00/rBMGZWCeGhqAR_vRAAIAABZebgw.sqlite', 'Status': 'Upload successed.', 'Local file name': './db.sqlite3', 'Uploaded size': '128.00KB', 'Storage IP': b'101.133.225.166'}
# 访问地址即可下载:http://192.168.1.252:8888/group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg
#文件下载
# result = client.download_to_file('./xx.jpg', b'group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg')
# print(result)
# #文件删除
result = client.delete_file(b'group1/M00/00/00/CgAAzmSihyKAUybqAAH8LXKkrrY060.jpg')
print(result)
# ('Delete file successed.', b'group1/M00/00/00/rBMGZWCeGhqAR_vRAAIAABZebgw.sqlite', b'192.168.1.252')
# #列出所有的group信息
# result = client.list_all_groups()
# print(result)
1 搜索功能前端
##### html####
<form class="search">
<div class="tips" v-if="is_search_tip">
<span @click="search_action('Python')">Python</span>
<span @click="search_action('Linux')">Linux</span>
</div>
<input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search" v-model="search_word">
<el-button icon="el-icon-search" @click="search_action(search_word)"></el-button>
</form>
#### js data:
is_search_tip: true,
search_placeholder: '',
search_word: ''
###js :methods
search_action(search_word) {
if (!search_word) {
this.$message('请输入要搜索的内容');
return
}
if (search_word !== this.$route.query.word) {
this.$router.push(`/course/search?word=${search_word}`);
}
this.search_word = '';
},
on_search() {
this.search_placeholder = '请输入想搜索的课程';
this.is_search_tip = false;
},
off_search() {
this.search_placeholder = '';
this.is_search_tip = true;
},
# css
.search {
float: right;
position: relative;
margin-top: 22px;
margin-right: 10px;
}
.search input, .search button {
border: none;
outline: none;
background-color: white;
}
.search input {
border-bottom: 1px solid #eeeeee;
}
.search input:focus {
border-bottom-color: orange;
}
.search input:focus + button {
color: orange;
}
.search .tips {
position: absolute;
bottom: 3px;
left: 0;
}
.search .tips span {
border-radius: 11px;
background-color: #eee;
line-height: 22px;
display: inline-block;
padding: 0 7px;
margin-right: 3px;
cursor: pointer;
color: #aaa;
font-size: 14px;
}
.search .tips span:hover {
color: orange;
}
2.1 搜索结果页面
<template>
<div class="search-course course">
<Header/>
<!-- 课程列表 -->
<div class="main">
<div v-if="course_list.length > 0" class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入学习</span></h3>
<p class="teather-info">
{{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
<span v-if="course.sections>course.pub_sections">共{{course.sections}}课时/已更新{{course.pub_sections}}课时</span>
<span v-else>共{{course.sections}}课时/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{key+1}} | {{section.name}}</span>
<span class="free" v-if="section.free_trail">免费</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{course.discount_type}}</span>
<span class="discount-price">¥{{course.real_price}}元</span>
<span class="original-price">原价:{{course.price}}元</span>
</div>
<span v-else class="discount-price">¥{{course.price}}元</span>
<span class="buy-now">立即购买</span>
</div>
</div>
</div>
</div>
<div v-else style="text-align: center; line-height: 60px">
没有搜索结果
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[2, 3, 5, 10]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import Header from '../components/Header'
import api from '../assets/js/settings'
export default {
name: "SearchCourse",
components: {
Header,
},
data() {
return {
course_list: [],
course_total: 0,
filter: {
page_size: 10,
page: 1,
search: '',
}
}
},
created() {
this.get_course()
},
watch: {
'$route.query' () {
this.get_course()
}
},
methods: {
handleSizeChange(val) {
// 每页数据量发生变化时执行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 页码发生变化时执行的方法
this.filter.page = val;
},
get_course() {
// 获取搜索的关键字
this.filter.search = this.$route.query.word
// 获取课程列表信息
this.$axios.get(api.search, {
params: this.filter
}).then(response => {
// 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
this.course_list = response.data.results;
this.course_total = response.data.count;
}).catch(() => {
this.$message({
message: "获取课程信息有误,请联系客服工作人员"
})
})
}
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .ordering .price_up::before {
border-bottom-color: #ffc210;
}
.course .ordering .price_down::after {
border-top-color: #ffc210;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 过渡动画 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 顶级元素 父级元素 当前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
max-width: 100%;
max-height: 210px;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 a {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .section-list::after {
content: "";
display: block;
clear: both;
}
.course-item .section-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路径") 是否平铺 x轴位置 y轴位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .section-list li .section-title {
/* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .section-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .section-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .section-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item {
position: relative;
}
.course-item .pay-box {
position: absolute;
bottom: 20px;
width: 600px;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
position: absolute;
right: 0;
bottom: 5px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
.course .course_pagination {
margin-bottom: 60px;
text-align: center;
}
</style>
2 搜索功能接口
from rest_framework.filters import SearchFilter
class CourseSearchView(GenericViewSet, APIListModelMixin):
queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
serializer_class = CourseSerializer
pagination_class = PageNumberPagination # 分页
filter_backends = [SearchFilter]
search_fields = ['name']
# 咱么目前搜索接口:只能搜索 实战课
# 后期要能搜索:实战课,免费课,轻课
# 甚至更强大:输入老师名字搜到老师
# 搜索接口后期可以返回很多数据--》结合原型图
# 有个性化推荐
# def list(self, request, *args, **kwargs):
# # 实战课
# res=super().list(request, *args, **kwargs)
# search=request.query_params.get('search')
# # 根据search=高级 去老师表中,根据名字或老师介绍---》推荐几个老师
# # 搜名字或老师介绍中有 ---》mysql--》介绍--》文字很多 like =%高级%-->效率非常低
# # 可以使用专业的搜索引擎:es 全文检索
# # 个性化推荐:用户画像---》通过打标签:青年 本科 单身 宅
# return APIResponse(actual_course_list=res.results,teacher_list=[])
3 支付宝支付介绍
# 1 支付
-1 支付宝支付
-2 微信支付
-3 银联支付
-4 自己支付:支付牌照
# 2 支付宝支付
-商户号:别人把钱付款--》付到商户里面
-商户再提现
----需要营业执照----没有可以使用沙箱环境测试----
-扫码登录:沙箱环境--》不需要申请条件--》可以测试
-https://open.alipay.com/develop/manage
-网站支付:https://opendocs.alipay.com/open/270/105899
-手机网站支付:可以掉起支付宝app
-咱们不会:输入账号密码支付
-https://opendocs.alipay.com/open/270/105898?pathHash=b3b2b667
-网站支付:
-跳转到支付宝支付页面
-手机扫码付款
-在网页上输入支付宝账号密码付款
# 3 申请支付宝商户号,限制条件
#申请条件
支持的账号类型:支付宝企业账号、支付宝个人账号。
# 签约申请提交材料要求如下:
• 提供网站地址,网站能正常访问且页面显示完整,网站需要明确经营内容且有完整的商品信息。
• 网站必须通过 ICP 备案,且备案主体需与支付宝账号主体一致。若网站备案主体与当前账号主体不同时需上传授权函。
• 如以个人账号申请,需提供营业执照,且支付宝账号名称需与营业执照主体一致。
注意:需按照要求提交材料,若部分材料不合格,收款额度将受到限制(单笔收款 ≤ 2000 元,单日收款 ≤ 20000 元)。若签约时未能提供相关材料(如营业执照),请在合约生效后的 30 天内补全,否则会影响正常收款
# 4 沙箱环境--》测试环境
-https://open.alipay.com/develop/sandbox/app
-商户号:
-买家号:
-安卓沙箱app--》跟支付宝一样
# 5 web端,集成,支付流程
-1 前端:购买按钮
-2 点击支付按钮,触发后端下单接口:生成支付链接,生成订单[订单表生成一条记录]
-3 用户扫码付款[登陆后输入密码付款]
-4 支付宝收到付款成功---》get回调--》回调到前端--》前端支付成功页面
-5 支付宝收到付款成功---》post回调--》回调后端---》修改订单状态--》已支付状态
3.1 快速体验
# 1 API 接口和sdk
-早期没有python的sdk---》只能使用api接口--》第三方基于api接口封装了非官方sdk
-https://github.com/fzlee/alipay
-pip install python-alipay-sdk
-后期有了官方sdk:
-https://opendocs.alipay.com/common/02np8q?pathHash=7847ca4f
# 2 支付宝支付通信,验证签名,都是使用非对称加密--》支付宝提供的软件
-软件:https://opendocs.alipay.com/common/02kipk?pathHash=0d20b438
-公钥
-私钥
# 3 在支付宝沙箱环境中[正式环境]
-把刚刚生成的公钥填入
-会生成一个支付宝公钥--》把这个东西复制出来
# 4 在代码中,使用【支付宝公钥】和刚刚生成的【私钥】,放到代码中
GitHub开源框架
https://github.com/fzlee/alipay
# 5、公钥私钥设置
"""
# alipay_public_key.pem
-----BEGIN PUBLIC KEY-----
支付宝公钥
-----END PUBLIC KEY-----
# app_private_key.pem
-----BEGIN RSA PRIVATE KEY-----
用户私钥
-----END RSA PRIVATE KEY-----
"""
# 6测试代码
from alipay import AliPay, DCAliPay, ISVAliPay
from alipay.utils import AliPayConfig
# 支付宝网页下载的证书不能直接被使用,需要加上头尾
# 你可以在此处找到例子: tests/certs/ali/ali_private_key.pem
app_private_key_string = open("private_key.pem").read()
alipay_public_key_string = open("al_public_key.pem").read()
alipay = AliPay(
appid="9021000137627113",
app_notify_url=None, # 默认回调 url
app_private_key_string=app_private_key_string,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # RSA 或者 RSA2
debug=False, # 默认 False
verbose=False, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)
#老版本
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no="20161112",
total_amount=99,
subject='python',
return_url="https://example.com",
notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
# print(f'支付地址:https://openapi-sandbox.dl.alipaydev.com/gateway.do?{order_string}')
#新版本
res=alipay.client_api(
"alipay.trade.page.pay",
biz_content={
"out_trade_no": "20161112",
"total_amount": 0.01,
"subject": "an order"
},
return_url="https://example.com", # this is optional
)
print(f'支付地址:https://openapi-sandbox.dl.alipaydev.com/gateway.do?{res}')
![image-20240521182512894](https://img2023.cnblogs.com/blog/3331555/202405/3331555-20240521182514530-949059727.png)
3.2 官方sdk测试
pip install alipay-sdk-python
from alipay.aop.api.AlipayClientConfig import AlipayClientConfig
from alipay.aop.api.DefaultAlipayClient import DefaultAlipayClient
from alipay.aop.api.domain.AlipayTradePagePayModel import AlipayTradePagePayModel
from alipay.aop.api.request.AlipayTradePagePayRequest import AlipayTradePagePayRequest
if __name__ == '__main__':
"""
设置配置,包括支付宝网关地址、app_id、应用私钥、支付宝公钥等,其他配置值可以查看AlipayClientConfig的定义。
"""
alipay_client_config = AlipayClientConfig()
# alipay_client_config.server_url = 'https://openapi.alipay.com/gateway.do' # 真实环境
alipay_client_config.server_url = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do' # 沙箱环境
alipay_client_config.app_id = '9021000129694319'
# 应用私钥
alipay_client_config.app_private_key = 'MIIEowIBAAKCAQEA2+5a8/zdCXHnzE3T0DrWgalKE2CLhRX0+HVhVxC7+OL1Aoxag5evU6CQW0F/aixnu6bBowy+tKOggqLuRzJsZhitCpt1PyJAsQO10STIa7GAhfmZMW+SEkmLvo5n/DIQmHmjcEjQj+dS1QQH/qnIsquT7D3BPxJanTUQurIiiL/TlxV5ur4qvuYu/1Pg4uixhi/UJa2sunwQK9u1+D8fYRf4zuYEltSA3ZfgFDFOyZK/mkVdEWL9snZqJRYVQIC6xPX3qeVX7Rx0768B5HzTlzT1zp9j5tMktxjRHJX98/lYluE6vy9loBYa6VF6UOVAvRLenQKyswSiUIU+WqyQ1wIDAQABAoIBAQDBwkqrLhlmWs3GtsJnb47QCN9UFviUNXXu9yrc08dnTDxjFFgiGx7B5HGJlDi2x4xUTGPITFAvQQEGVpjqbMgHYrIA6FjxWDH6QbSLH4bbKjR61B1c6licd+L/7OI707e/PVr6b4wfW8MkHDsW52oDzxmxRe7crETcv4WPlaTLJ+JPRILVP3rWcG/DZJTe+HwHHEwWZmBKrSRUqy+PF9CCQ3FYxRIKYcg7JxnA/JlGPjnzQMckrBfDoNi2XsK4w23ioh6n6mWKQ86ENDyUQMRoB32zcSOGj2If5uVYf6ydMNTNJeGT6CUn2o4jkTdKtljXWKSnDm8zRQDqif6TsSLxAoGBAPyOO5E9dmXtPBD9YyjUauEzLNG5B87iibOn2jLfQ7PuenZgPXXG2L+q6+iAUxJzQKFUkbvQXVwGHk2/pVCeogoZdCOUQebuSSuqSkBH+UGP0WiHcOFeoJckmT74ijZ1ESWVHek8foIBab2+8gSWNBMyWaTsmYI02zq8VF9r3IiTAoGBAN7uN9DpUD70Ev5yydVLkxwC6vI85ML/G9b6iHDvd3HAlG5GcRraSdThRQ3YCDkLJufGJ7bnAEYHARdaohgNZfRLWK1HkUYk3tQThMPYBxDXbJtfm7p2Kl+/WKm4+TtrKUdnDhZPod8bL2awVth4hfoKO9GzF80R8vBPo9W6yZUtAoGAA5CKXLFuY1/m0iKRbLkazRTo1Aj1iEEASo3a8Y7fKMH77oHLPEdTNdlWvRBam88OoXhNGkaFms/nS5eh4LJsfRIA5qOoDndchwY/SAr8BKXgAcavnC62u4tjslTVtpEObeZd5rXY30Lf2DLCvbfVAlRamY5RWFogogKYekROd4sCgYAFkmKmwA4XZLZM0cWlpRvqKVCB+W+mSAYEG4Lpf7K2jx+mmfAdwbLytSaqr+mUs2inhlZbxe5F0cr/MG64ty0DLBbtTcqdvDItjsdUtcOHcjrurzcPNADfH8MxisP/7i+77yF1AUyEbQOER4gEJQ8ELtlL5nQD1h0CUJtBrkd3iQKBgHab6GJDk5JGapKSwC32FP+LNDsm++0RhZGYlGzlsa8JnkOYuMoY4Y7D9IfJA1xbfxNECDFAg/FdyOQ+vzESx1i39HYZ6WXa0wmy2kCVugGVM/8Wdp3gjbZ378iMl3GQ+rDerkmyYpzm9t1L0/8GRrhiS+OkfMBhs7gcZbL74BsZ'
# 阿里公钥
alipay_client_config.alipay_public_key = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAhddgdkn8X2t+3gnPA9dkqUNe1+SIcPQ8Mmb/8Ynac0In/s9BC5rBNrtkKEtvejQN+jmOJh9x03yHGwObYYUckwAXEIKw9LhXQWu3LdTbczl3UWgH9IW3BsZYvOY9xJWZJN1cq3MskFbqoIgB+lDiq86JRYS/QpyvCk7t5ZtY58w5A/iSTOGAqINzIW9BZBmQM8euJd26u5JNEMuotXHWlJzPeERNnxzJRUi8MpltDXfSzlxmATI/Aw2u1HGY91OIv1h7A46lURaCdyc57aj7ot+rLFymTMvKyYhyyfA2FjyyXlwZQowBFE7rDBIe+uLttgBu6O+1sUl2kRw9IeOk0QIDAQAB'
"""
得到客户端对象。
注意,一个alipay_client_config对象对应一个DefaultAlipayClient,定义DefaultAlipayClient对象后,alipay_client_config不得修改,如果想使用不同的配置,请定义不同的DefaultAlipayClient。
logger参数用于打印日志,不传则不打印,建议传递。
"""
client = DefaultAlipayClient(alipay_client_config=alipay_client_config)
"""
页面接口示例:alipay.trade.page.pay
"""
# 对照接口文档,构造请求对象
model = AlipayTradePagePayModel()
model.out_trade_no = "000010004"
model.total_amount = 999
model.subject = "重启娃娃-保密发货"
model.body = "重启娃娃"
model.product_code = "FAST_INSTANT_TRADE_PAY"
request = AlipayTradePagePayRequest(biz_model=model)
# 两个回调地址:get回调 post 回调
request.return_url='http://www.baidu.com' # get回调
request.notify_url='http://www.baidu.com/post' # post 回调 我们看不到
# 得到构造的请求,如果http_method是GET,则是一个带完成请求参数的url,如果http_method是POST,则是一段HTML表单片段
response = client.page_execute(request, http_method="GET")
print("alipay.trade.page.pay response:" + response)
3.3 第三方sdk测试
from alipay import AliPay
from alipay.utils import AliPayConfig
# 支付宝网页下载的证书不能直接被使用,需要加上头尾
# 你可以在此处找到例子: tests/certs/ali/ali_private_key.pem
app_private_key_string = open("./private_key.pem").read()
alipay_public_key_string = open("./al_public_key.pem").read()
alipay = AliPay(
appid="9021000129694319", # 商户申请好久有了
app_notify_url=None, # 默认回调 url
app_private_key_string=app_private_key_string,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # RSA 或者 RSA2
debug=False, # 默认 False
verbose=False, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)
# 老版本
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no="10001010",
total_amount=99,
subject='Go语言入门',
return_url="https://example.com",
notify_url="https://example.com/notify" # 可选,不填则使用默认 notify url
)
print(f'支付地址:https://openapi-sandbox.dl.alipaydev.com/gateway.do?{order_string}')
# 新版本--暂时没通
# res=alipay.client_api(
# "alipay.trade.page.pay",
# biz_content={
# "out_trade_no": "100212",
# "total_amount": 8889,
# "subject": "性感内衣"
# },
# return_url="https://example.com", # this is optional
# notify_url='https://example.com'
# )
4 支付宝二次封装
libs
├── al_pay # aliapy二次封装包
│ ├── __init__.py # 包文件
│ ├── pem # 公钥私钥文件夹
│ │ ├── alipay_public_key.pem # 支付宝公钥文件
│ │ ├── app_private_key.pem # 应用私钥文件
│ ├── pay.py # 支付文件
└── └── settings.py # 应用配置
4.2.1 pay.py
from alipay import AliPay
from alipay.utils import AliPayConfig
from . import settings
alipay = AliPay(
appid=settings.APP_ID,
app_notify_url=None, # 默认回调 url
app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
# 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
sign_type=settings.SIGN, # RSA 或者 RSA2
debug=settings.DEBUG, # 默认 False
verbose=False, # 输出调试数据
config=AliPayConfig(timeout=15) # 可选,请求超时时间
)
4.2.2 settings.py
import os
#### 替换应用私钥 支付宝公钥 和 应用ID即可
# 应用私钥
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
# 支付宝公钥
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
# 应用ID
APP_ID = '9021000129694319'
# 加密方式
SIGN = 'RSA2'
# 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
DEBUG = True
# 支付网关
GATEWAY = 'https://openapi-sandbox.dl.alipaydev.com/gateway.do' if DEBUG else 'https://openapi.alipay.com/gateway.do'
4.2.3 init.py
from .pay import alipay
from .settings import GATEWAY
5 支付相关表
# 1 创建一个新的app,order
# 2 表模型有
# 用户在前端点击立即购买---》触发我们后端下单接口---》下单接口返回给前端支付地址---》前端跳转到支付链接---》用户去付款
# 表分析
-1 订单表
-2 订单详情表 :一个订单有多个订单详情
from django.db import models
from user.models import User
from course.models import Course
# 订单表
class Order(models.Model):
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超时取消'),
)
pay_choices = (
(1, '支付宝'),
(2, '微信支付'),
)
subject = models.CharField(max_length=150, verbose_name="订单标题")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
# 咱们生成的---全局唯一
out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True)
# 支付宝付款后会返回这个号---》支付宝流水号
trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号")
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
# 支付宝会返回支付时间
pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,
verbose_name="下单用户")
# 下单时间
created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
class Meta:
db_table = "luffy_order"
verbose_name = "订单记录"
verbose_name_plural = "订单记录"
def __str__(self):
return "%s - ¥%s" % (self.subject, self.total_amount)
# 订单详情表
class OrderDetail(models.Model):
# 跟订单一对多,关联字段写在多的一方
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,
verbose_name="订单")
# 课程和订单详情,一对多,一个课程,可以对应多个订单详情
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, db_constraint=False,
verbose_name="课程")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
class Meta:
db_table = "luffy_order_detail"
verbose_name = "订单详情"
verbose_name_plural = "订单详情"
def __str__(self):
try:
return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
except:
return super().__str__()
from order.models import Order,OrderDetail
# Register your models here.
admin.site.register(Order)
admin.site.register(OrderDetail)
6 下单接口(登陆后才能使用)
# 1 前端携带数据什么格式?不需要携带用户id,携带token--》request.user 就是当前登陆用户
post请求--》{'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}
# 2 后端接口
6.1 路由
router = SimpleRouter()
router.register('pay', OrderPayView, 'pay')
urlpatterns = [
# 127.0.0.1:8000/api/v1/order/success/--->get
path('success/', OrderSuccessView.as_view()),
]
urlpatterns += router.urls
6.2 视图类
pip install python-alipay-sdk --upgrade
class OrderPayView(GenericViewSet):
# 必须登录才能访问:认证类 权限类
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]
# 校验--》生成支付链接--》生成订单--》序列化类的validate中
serializer_class = OrderPaySerializer
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data, context={'request': request})
serializer.is_valid(raise_exception=True)
serializer.save() # 下单-》生成订单,--》重写create方法
pay_url = serializer.context.get('pay_url')
return APIResponse(pay_url=pay_url)
6.3 序列化类
from rest_framework import serializers
from .models import Order, OrderDetail
from course.models import Course
from rest_framework.exceptions import APIException
import uuid
from django.conf import settings
from libs.al_pay import alipay, GATEWAY
# 1 校验--》{'courses':[1,],'total_amount':0.1,'subject':课程名,'pay_type':1}
# 2 反序列化的保存
class OrderPaySerializer(serializers.ModelSerializer):
# courses 重写
# courses 本来是 [1,4,5]--->会去Course.objects.all() 数据集中映射---》变成 --》[course1对象,course4对象,course5对象,]
courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(),many=True)
# courses=serializers.ListField()
class Meta:
model = Order
fields = [
'courses', # 不是表中字段,需要重写
'total_amount',
'subject',
'pay_type'
]
def _check_total_amount(self, attrs):
# 1 取出用户传的 courses---》课程 对象 列表
courses = attrs.get('courses')
# 2 取出总价格
real_total_amount = 0
total_amount = attrs.get('total_amount')
# 3 通过 课程对象 列表 获取价格--》累加到一起,跟传入的 总价格比较,如果一致,就什么都不做,如果不一致,抛异常
for course in courses:
real_total_amount += course.price
if real_total_amount != total_amount:
raise APIException('价格不合法')
return total_amount
def _get_out_trade_no(self):
# 使用uuid生成--》后期会有别的生成id的方案:1 效率高 2 不重复 3 单调递增趋势 4 在分布式节点中不会重复
out_trade_no = str(uuid.uuid4()).replace('-', '')
return out_trade_no
def _get_user(self):
return self.context.get('request').user
def _get_pay_url(self, out_trade_no, total_amount, subject):
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=out_trade_no,
total_amount=float(total_amount), # 只有生成支付宝链接时,不能用Decimal
subject=subject,
return_url=settings.RETURN_URL, # get 回调 --》前端
notify_url=settings.NOTIFY_URL, # post 回调--》后端
)
pay_url = GATEWAY + '?' + order_string
# 将支付链接存入,传递给views
self.context['pay_url'] = pay_url
def _before_create(self, attrs, user, out_trade_no):
# attrs ={'courses':[对象,],'total_amount':0.1,'subject':课程名,'pay_type':1}
attrs['user'] = user
attrs['out_trade_no'] = out_trade_no
def validate(self, attrs):
# 1 校验数据是否正确[订单总价校验]--》total_amount 和 courses 比较价格是否正确
total_amount = self._check_total_amount(attrs)
# 2 生成订单号--》唯一的--》
out_trade_no = self._get_out_trade_no()
# 3 获取支付人 --》当前登录用户
user = self._get_user()
# 4 获取支付链接--》
self._get_pay_url(out_trade_no, total_amount, attrs.get('subject'))
# 5 入库(两个表)的信息准备
self._before_create(attrs, user, out_trade_no)
return attrs
def create(self, validated_data):
# validated_data = {'courses': [对象, ], 'total_amount': 0.1, 'subject': 课程名, 'pay_type': 1,user:对象,out_trade_no:3333}
# 存两个表
courses = validated_data.pop('courses')
order = Order.objects.create(**validated_data)
for course in courses:
OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
return order
BASE_URL = 'http://127.0.0.1:8000'
# 前台基URL
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付宝同步异步回调接口配置
# 后台异步回调接口
NOTIFY_URL = BASE_URL + "/api/v1/order/success/"
# 前台同步回调接口,没有 / 结尾
RETURN_URL = LUFFY_URL + "/api/v1/pay/success"
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(days=7),
"AUTH_HEADER_TYPES": ("Bearer",), } # Access Token的有效期
7 支付前端
<template>
<div class="pay-success">
<!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)-->
<Header/>
<div class="main">
<div class="title">
<div class="success-tips">
<p class="tips">您已成功购买 1 门课程!</p>
</div>
</div>
<div class="order-info">
<p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p>
<p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p>
<p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p>
</div>
<div class="study">
<span>立即学习</span>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/Header"
import api from '../assets/js/settings'
export default {
name: "Success",
data() {
return {
result: {},
};
},
created() {
// url后拼接的参数:?及后面的所有参数 => ?a=1&b=2
console.log(location.search);
// 解析支付宝回调的url参数
let params = location.search.substring(1); // 去除? => a=1&b=2
let items = params.length ? params.split('&') : []; // ['a=1', 'b=2']
//逐个将每一项添加到args对象中
for (let i = 0; i < items.length; i++) { // 第一次循环a=1,第二次b=2
let k_v = items[i].split('='); // ['a', '1']
//解码操作,因为查询字符串经过编码的
if (k_v.length >= 2) {
// url编码反解
let k = decodeURIComponent(k_v[0]);
this.result[k] = decodeURIComponent(k_v[1]);
// 没有url编码反解
// this.result[k_v[0]] = k_v[1];
}
}
// 解析后的结果
console.log(this.result);
// 把地址栏上面的支付结果,再get请求转发给后端
this.$axios({
url: api.success + location.search,
method: 'get',
}).then(response => {
// console.log(response.data);
this.$message({
type:"success",
message:response.data.msg
})
}).catch(() => {
console.log('支付结果同步失败');
})
},
components: {
Header,
}
}
</script>
<style scoped>
.main {
padding: 60px 0;
margin: 0 auto;
width: 1200px;
background: #fff;
}
.main .title {
display: flex;
-ms-flex-align: center;
align-items: center;
padding: 25px 40px;
border-bottom: 1px solid #f2f2f2;
}
.main .title .success-tips {
box-sizing: border-box;
}
.title img {
vertical-align: middle;
width: 60px;
height: 60px;
margin-right: 40px;
}
.title .success-tips {
box-sizing: border-box;
}
.title .tips {
font-size: 26px;
color: #000;
}
.info span {
color: #ec6730;
}
.order-info {
padding: 25px 48px;
padding-bottom: 15px;
border-bottom: 1px solid #f2f2f2;
}
.order-info p {
display: -ms-flexbox;
display: flex;
margin-bottom: 10px;
font-size: 16px;
}
.order-info p b {
font-weight: 400;
color: #9d9d9d;
white-space: nowrap;
}
.study {
padding: 25px 40px;
}
.study span {
display: block;
width: 140px;
height: 42px;
text-align: center;
line-height: 42px;
cursor: pointer;
background: #ffc210;
border-radius: 6px;
font-size: 16px;
color: #fff;
}
</style>
8 支付成功前端回调
8.1 内网穿透
https://zhuanlan.zhihu.com/p/370483324
8.2 支付宝回调数据格式
# 回调数据格式
data = {
"subject": "测试订单",
"gmt_payment": "2016-11-16 11:42:19",
"charset": "utf-8",
"seller_id": "xxxx",
"trade_status": "TRADE_SUCCESS",
"buyer_id": "xxxx",
"auth_app_id": "xxxx",
"buyer_pay_amount": "0.01",
"version": "1.0",
"gmt_create": "2016-11-16 11:42:18",
"trade_no": "xxxx",
"fund_bill_list": "[{\"amount\":\"0.01\",\"fundChannel\":\"ALIPAYACCOUNT\"}]",
"app_id": "xxxx",
"notify_time": "2016-11-16 11:42:19",
"point_amount": "0.00",
"total_amount": "0.01",
"notify_type": "trade_status_sync",
"out_trade_no": "订单号", # 咱们的uuid
"buyer_logon_id": "xxxx",
"notify_id": "xxxx",
"seller_email": "xxxx",
"receipt_amount": "0.01",
"invoice_amount": "0.01",
"sign": "签名" # 验证签名
}
8.3 支付宝回调
class OrderSuccessView(APIView):
# http://127.0.0.1:8000/api/v1/order/success/--->get
def get(self, request, *args, **kwargs):
# 给咱们前端用
# 1 取出订单号
out_trade_no = request.query_params.get('out_trade_no')
# 2 去数据库查询
order = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()
if order: # 说明支付宝的post回调回来了,修改了订单状态
return APIResponse(msg='支付成功,请去学习把')
else:
return APIResponse(code=101, msg='暂未收到您的付款,请稍后刷新再试')
# post 给支付宝回调用--修改订单状态--->
# 支付宝异步回调--》如果不返回正常的响应,会多次回调
# 这个接口需要加认证类吗?--》支付宝用,没有token的,所以一定不能加认证类
# 支付宝,在公网---》目前咱们测试--》永远回调不进来--》写完没法测试
# 内网穿透:花生壳,第三方内外穿透软件,原来免费,后来收费
def post(self, request, *args, **kwargs):
try:
# json编码 -->是字典
# urlencoded--》querydict---》dict()----》纯字典
# 支付宝回调编码是:urlencoded
result_data = request.data.dict() # 把request.data ---> 转成字典格式
out_trade_no = result_data.get('out_trade_no')
trade_no = result_data.get('trade_no')
signature = result_data.pop('sign')
pay_time = result_data.get('notify_time','2024-06-01')
result = alipay.verify(result_data, signature)
if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
# 完成订单修改:订单状态、流水号、支付时间
Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1, trade_no=trade_no,pay_time=pay_time)
# 完成日志记录
logger.warning('%s订单支付成功' % out_trade_no)
return Response('success')
else:
logger.error('%s订单支付失败' % out_trade_no)
except:
pass