文件存储

视频文件存储在某个位置,如果放在自己服务器上

  • 放在项目的media文件夹
  • 服务器上线后,用户既要访问接口,又需要看视频,都是使用一个域名和端口
  • 分开:问价你单独放在文件服务器上,文件服务器带宽比较高
  1. # 文件服务器:专门存储文件的服务器
  2. -第三方:
  3. -阿里云:对象存储 oss
  4. -腾讯对象存储
  5. -七牛云存储
  6. -自己搭建:
  7. fastdfs:文件对象存储 https://zhuanlan.zhihu.com/p/372286804
  8. minio

我们可以使用对应的sdk包将文件传输上去

在此项目中我们选用七牛云来存储视频文件资源

使用代码,上传视频

我们参考官方文档使用即可

1.创建七牛云对象存储仓库

2.直接在桌面上传文件即可

1.1代码控制文件上传

python安装七牛云

pip install qiniu

本地测试

我们scripts文件夹下新建qiniu_test.py文件

  1. # -*- coding: utf-8 -*-
  2. # flake8: noqa
  3. from qiniu import Auth, put_file, etag
  4. import qiniu.config
  5. #需要填写你的 Access Key 和 Secret Key
  6. # 在这里查看密钥 > https://portal.qiniu.com/user/key
  7. access_key = 'Access_Key'
  8. secret_key = 'Secret_Key'
  9. #构建鉴权对象
  10. q = Auth(access_key, secret_key)
  11. #要上传的空间
  12. bucket_name = 'Bucket_Name'
  13. #上传后保存的文件名
  14. key = 'my-python-logo.png'
  15. #生成上传 Token,可以指定过期时间等
  16. token = q.upload_token(bucket_name, key, 3600)
  17. #要上传文件的本地路径
  18. localfile = './sync/bbb.jpg'
  19. ret, info = put_file(token, key, localfile, version='v2')
  20. print(info)
  21. assert ret['key'] == key
  22. assert ret['hash'] == etag(localfile)

尝试上传本地文件:

成功

搜索导航栏

前端Header组件上有个搜索框>>>输入内容,即可搜索

在所有商城类的网站,app都会有搜索功能,其实搜索功能非常复杂,且功能非常复杂技术含量高

  • 咱们目前只是简单的搜索,输入课程名字/价格,就可以把实战课搜出来
  • 输入:课程名字,价格把所有类型课程都搜出来(查询多个表)
  • 后面会有专门的搜索引擎:分布式全文检索引擎 es 做专门的搜索

前端页面Header.vue

  1. <template>
  2. <div class="header">
  3. <div class="slogan">
  4. <p>老男孩IT教育 | 帮助有志向的年轻人通过努力学习获得体面的工作和生活</p>
  5. </div>
  6. <div class="nav">
  7. <ul class="left-part">
  8. <li class="logo">
  9. <router-link to="/">
  10. <img src="../assets/img/head-logo.svg" alt="">
  11. </router-link>
  12. </li>
  13. <li class="ele">
  14. <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免费课</span>
  15. </li>
  16. <li class="ele">
  17. <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">实战课</span>
  18. </li>
  19. <li class="ele">
  20. <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">轻课</span>
  21. </li>
  22. </ul>
  23. <div class="right-part">
  24. <div v-if="!username">
  25. <span @click="put_login">登录</span>
  26. <span class="line">|</span>
  27. <span @click="put_register">注册</span>
  28. </div>
  29. <div v-else>
  30. <span>{{ username }}</span>
  31. <span class="line">|</span>
  32. <span>注销</span>
  33. </div>
  34. </div>
  35. </div>
  36. <Login v-if="is_login" @close="close_login" @go="put_register" @success="success_login"/>
  37. <Register v-if="is_register" @close="close_register" @go="put_login" @success="success_register"/>
  38. <form class="search">
  39. <div class="tips" v-if="is_search_tip">
  40. <span @click="search_action('Python')">Python</span>
  41. <span @click="search_action('Linux')">Linux</span>
  42. </div>
  43. <input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search" v-model="search_word">
  44. <button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)">搜索</button>
  45. </form>
  46. </div>
  47. </template>
  48. <script>
  49. import Login from "@/components/Login";
  50. import Register from "@/components/Register";
  51. export default {
  52. name: "Header",
  53. data() {
  54. return {
  55. // 当前所在路径,去sessionStorage取的,如果取不到,就是 /
  56. url_path: sessionStorage.url_path || '/',
  57. is_login: false,
  58. is_register: false,
  59. username: this.$cookies.get('username'),
  60. token: this.$cookies.get('token'),
  61. is_search_tip: true,
  62. search_placeholder: '',
  63. search_word: ''
  64. }
  65. },
  66. methods: {
  67. search_action(search_word) {
  68. console.log(search_word)
  69. if (!search_word) {
  70. this.$message('请输入要搜索的内容');
  71. return
  72. }
  73. if (search_word !== this.$route.query.word) {
  74. this.$router.push(`/course/search?word=${search_word}`);
  75. }
  76. this.search_word = '';
  77. },
  78. on_search() {
  79. this.search_placeholder = '请输入想搜索的课程';
  80. this.is_search_tip = false;
  81. },
  82. off_search() {
  83. this.search_placeholder = '';
  84. this.is_search_tip = true;
  85. },
  86. goPage(url_path) {
  87. // 已经是当前路由就没有必要重新跳转
  88. if (this.url_path !== url_path) {
  89. this.$router.push(url_path);
  90. }
  91. sessionStorage.url_path = url_path;
  92. },
  93. put_login() {
  94. this.is_login = true;
  95. this.is_register = false;
  96. },
  97. put_register() {
  98. this.is_login = false;
  99. this.is_register = true;
  100. },
  101. close_login() {
  102. this.is_login = false;
  103. },
  104. close_register() {
  105. this.is_register = false;
  106. },
  107. success_login() {
  108. this.is_login = false;
  109. this.username = this.$cookies.get('username')
  110. this.token = this.$cookies.get('token')
  111. },
  112. success_register() {
  113. this.is_login = true
  114. this.is_register = false
  115. }
  116. },
  117. created() {
  118. // 组件加载万成,就取出当前的路径,存到sessionStorage this.$route.path
  119. sessionStorage.url_path = this.$route.path;
  120. // 把url_path = 当前路径
  121. this.url_path = this.$route.path;
  122. },
  123. components: {
  124. Login,
  125. Register
  126. }
  127. }
  128. </script>
  129. <style scoped>
  130. .search {
  131. float: right;
  132. position: relative;
  133. margin-top: 22px;
  134. margin-right: 10px;
  135. }
  136. .search input, .search button {
  137. border: none;
  138. outline: none;
  139. background-color: white;
  140. }
  141. .search input {
  142. border-bottom: 1px solid #eeeeee;
  143. }
  144. .search input:focus {
  145. border-bottom-color: orange;
  146. }
  147. .search input:focus + button {
  148. color: orange;
  149. }
  150. .search .tips {
  151. position: absolute;
  152. bottom: 3px;
  153. left: 0;
  154. }
  155. .search .tips span {
  156. border-radius: 11px;
  157. background-color: #eee;
  158. line-height: 22px;
  159. display: inline-block;
  160. padding: 0 7px;
  161. margin-right: 3px;
  162. cursor: pointer;
  163. color: #aaa;
  164. font-size: 14px;
  165. }
  166. .search .tips span:hover {
  167. color: orange;
  168. }
  169. .header {
  170. background-color: white;
  171. box-shadow: 0 0 5px 0 #aaa;
  172. }
  173. .header:after {
  174. content: "";
  175. display: block;
  176. clear: both;
  177. }
  178. .slogan {
  179. background-color: #eee;
  180. height: 40px;
  181. }
  182. .slogan p {
  183. width: 1200px;
  184. margin: 0 auto;
  185. color: #aaa;
  186. font-size: 13px;
  187. line-height: 40px;
  188. }
  189. .nav {
  190. background-color: white;
  191. user-select: none;
  192. width: 1200px;
  193. margin: 0 auto;
  194. }
  195. .nav ul {
  196. padding: 15px 0;
  197. float: left;
  198. }
  199. .nav ul:after {
  200. clear: both;
  201. content: '';
  202. display: block;
  203. }
  204. .nav ul li {
  205. float: left;
  206. }
  207. .logo {
  208. margin-right: 20px;
  209. }
  210. .ele {
  211. margin: 0 20px;
  212. }
  213. .ele span {
  214. display: block;
  215. font: 15px/36px '微软雅黑';
  216. border-bottom: 2px solid transparent;
  217. cursor: pointer;
  218. }
  219. .ele span:hover {
  220. border-bottom-color: orange;
  221. }
  222. .ele span.active {
  223. color: orange;
  224. border-bottom-color: orange;
  225. }
  226. .right-part {
  227. float: right;
  228. }
  229. .right-part .line {
  230. margin: 0 10px;
  231. }
  232. .right-part span {
  233. line-height: 68px;
  234. cursor: pointer;
  235. }
  236. </style>

搜索页面

  1. <template>
  2. <div class="search-course course">
  3. <Header/>
  4. <!-- 课程列表 -->
  5. <div class="main">
  6. <div v-if="course_list.length > 0" class="course-list">
  7. <div class="course-item" v-for="course in course_list" :key="course.name">
  8. <div class="course-image">
  9. <img :src="course.course_img" alt="">
  10. </div>
  11. <div class="course-info">
  12. <h3>
  13. <router-link :to="'/free/detail/'+course.id">{{ course.name }}</router-link>
  14. <span><img src="@/assets/img/avatar1.svg" alt="">{{ course.students }}人已加入学习</span></h3>
  15. <p class="teather-info">
  16. {{ course.teacher.name }} {{ course.teacher.title }} {{ course.teacher.signature }}
  17. <span
  18. v-if="course.sections>course.pub_sections">共{{ course.sections }}课时/已更新{{ course.pub_sections }}课时</span>
  19. <span v-else>共{{ course.sections }}课时/更新完成</span>
  20. </p>
  21. <ul class="section-list">
  22. <li v-for="(section, key) in course.section_list" :key="section.name"><span
  23. class="section-title">0{{ key + 1 }} | {{ section.name }}</span>
  24. <span class="free" v-if="section.free_trail">免费</span></li>
  25. </ul>
  26. <div class="pay-box">
  27. <div v-if="course.discount_type">
  28. <span class="discount-type">{{ course.discount_type }}</span>
  29. <span class="discount-price">¥{{ course.real_price }}元</span>
  30. <span class="original-price">原价:{{ course.price }}元</span>
  31. </div>
  32. <span v-else class="discount-price">¥{{ course.price }}元</span>
  33. <span class="buy-now">立即购买</span>
  34. </div>
  35. </div>
  36. </div>
  37. </div>
  38. <div v-else style="text-align: center; line-height: 60px">
  39. 没有搜索结果
  40. </div>
  41. <div class="course_pagination block">
  42. <el-pagination
  43. @size-change="handleSizeChange"
  44. @current-change="handleCurrentChange"
  45. :current-page.sync="filter.page"
  46. :page-sizes="[2, 3, 5, 10]"
  47. :page-size="filter.page_size"
  48. layout="sizes, prev, pager, next"
  49. :total="course_total">
  50. </el-pagination>
  51. </div>
  52. </div>
  53. </div>
  54. </template>
  55. <script>
  56. import Header from '../components/Header'
  57. export default {
  58. name: "SearchCourse",
  59. components: {
  60. Header,
  61. },
  62. data() {
  63. return {
  64. course_list: [],
  65. course_total: 0,
  66. filter: {
  67. page_size: 10,
  68. page: 1,
  69. search: '',
  70. }
  71. }
  72. },
  73. created() {
  74. this.get_course()
  75. },
  76. watch: {
  77. '$route.query'() {
  78. this.get_course()
  79. }
  80. },
  81. methods: {
  82. handleSizeChange(val) {
  83. // 每页数据量发生变化时执行的方法
  84. this.filter.page = 1;
  85. this.filter.page_size = val;
  86. },
  87. handleCurrentChange(val) {
  88. // 页码发生变化时执行的方法
  89. this.filter.page = val;
  90. },
  91. get_course() {
  92. // 获取搜索的关键字
  93. this.filter.search = this.$route.query.word || this.$route.query.wd;
  94. // 获取课程列表信息
  95. this.$axios.get(`${this.$settings.BASE_URL}/course/search/`, {
  96. params: this.filter
  97. }).then(response => {
  98. console.log(response)
  99. // 如果后台不分页,数据在response.data中;如果后台分页,数据在response.data.results中
  100. this.course_list = response.data.data.results;
  101. this.course_total = response.data.data.count;
  102. }).catch(() => {
  103. this.$message({
  104. message: "获取课程信息有误,请联系客服工作人员"
  105. })
  106. })
  107. }
  108. }
  109. }
  110. </script>
  111. <style scoped>
  112. .course {
  113. background: #f6f6f6;
  114. }
  115. .course .main {
  116. width: 1100px;
  117. margin: 35px auto 0;
  118. }
  119. .course .condition {
  120. margin-bottom: 35px;
  121. padding: 25px 30px 25px 20px;
  122. background: #fff;
  123. border-radius: 4px;
  124. box-shadow: 0 2px 4px 0 #f0f0f0;
  125. }
  126. .course .cate-list {
  127. border-bottom: 1px solid #333;
  128. border-bottom-color: rgba(51, 51, 51, .05);
  129. padding-bottom: 18px;
  130. margin-bottom: 17px;
  131. }
  132. .course .cate-list::after {
  133. content: "";
  134. display: block;
  135. clear: both;
  136. }
  137. .course .cate-list li {
  138. float: left;
  139. font-size: 16px;
  140. padding: 6px 15px;
  141. line-height: 16px;
  142. margin-left: 14px;
  143. position: relative;
  144. transition: all .3s ease;
  145. cursor: pointer;
  146. color: #4a4a4a;
  147. border: 1px solid transparent; /* transparent 透明 */
  148. }
  149. .course .cate-list .title {
  150. color: #888;
  151. margin-left: 0;
  152. letter-spacing: .36px;
  153. padding: 0;
  154. line-height: 28px;
  155. }
  156. .course .cate-list .this {
  157. color: #ffc210;
  158. border: 1px solid #ffc210 !important;
  159. border-radius: 30px;
  160. }
  161. .course .ordering::after {
  162. content: "";
  163. display: block;
  164. clear: both;
  165. }
  166. .course .ordering ul {
  167. float: left;
  168. }
  169. .course .ordering ul::after {
  170. content: "";
  171. display: block;
  172. clear: both;
  173. }
  174. .course .ordering .condition-result {
  175. float: right;
  176. font-size: 14px;
  177. color: #9b9b9b;
  178. line-height: 28px;
  179. }
  180. .course .ordering ul li {
  181. float: left;
  182. padding: 6px 15px;
  183. line-height: 16px;
  184. margin-left: 14px;
  185. position: relative;
  186. transition: all .3s ease;
  187. cursor: pointer;
  188. color: #4a4a4a;
  189. }
  190. .course .ordering .title {
  191. font-size: 16px;
  192. color: #888;
  193. letter-spacing: .36px;
  194. margin-left: 0;
  195. padding: 0;
  196. line-height: 28px;
  197. }
  198. .course .ordering .this {
  199. color: #ffc210;
  200. }
  201. .course .ordering .price {
  202. position: relative;
  203. }
  204. .course .ordering .price::before,
  205. .course .ordering .price::after {
  206. cursor: pointer;
  207. content: "";
  208. display: block;
  209. width: 0px;
  210. height: 0px;
  211. border: 5px solid transparent;
  212. position: absolute;
  213. right: 0;
  214. }
  215. .course .ordering .price::before {
  216. border-bottom: 5px solid #aaa;
  217. margin-bottom: 2px;
  218. top: 2px;
  219. }
  220. .course .ordering .price::after {
  221. border-top: 5px solid #aaa;
  222. bottom: 2px;
  223. }
  224. .course .ordering .price_up::before {
  225. border-bottom-color: #ffc210;
  226. }
  227. .course .ordering .price_down::after {
  228. border-top-color: #ffc210;
  229. }
  230. .course .course-item:hover {
  231. box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
  232. }
  233. .course .course-item {
  234. width: 1100px;
  235. background: #fff;
  236. padding: 20px 30px 20px 20px;
  237. margin-bottom: 35px;
  238. border-radius: 2px;
  239. cursor: pointer;
  240. box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
  241. /* css3.0 过渡动画 hover 事件操作 */
  242. transition: all .2s ease;
  243. }
  244. .course .course-item::after {
  245. content: "";
  246. display: block;
  247. clear: both;
  248. }
  249. /* 顶级元素 父级元素 当前元素{} */
  250. .course .course-item .course-image {
  251. float: left;
  252. width: 423px;
  253. height: 210px;
  254. margin-right: 30px;
  255. }
  256. .course .course-item .course-image img {
  257. max-width: 100%;
  258. max-height: 210px;
  259. }
  260. .course .course-item .course-info {
  261. float: left;
  262. width: 596px;
  263. }
  264. .course-item .course-info h3 a {
  265. font-size: 26px;
  266. color: #333;
  267. font-weight: normal;
  268. margin-bottom: 8px;
  269. }
  270. .course-item .course-info h3 span {
  271. font-size: 14px;
  272. color: #9b9b9b;
  273. float: right;
  274. margin-top: 14px;
  275. }
  276. .course-item .course-info h3 span img {
  277. width: 11px;
  278. height: auto;
  279. margin-right: 7px;
  280. }
  281. .course-item .course-info .teather-info {
  282. font-size: 14px;
  283. color: #9b9b9b;
  284. margin-bottom: 14px;
  285. padding-bottom: 14px;
  286. border-bottom: 1px solid #333;
  287. border-bottom-color: rgba(51, 51, 51, .05);
  288. }
  289. .course-item .course-info .teather-info span {
  290. float: right;
  291. }
  292. .course-item .section-list::after {
  293. content: "";
  294. display: block;
  295. clear: both;
  296. }
  297. .course-item .section-list li {
  298. float: left;
  299. width: 44%;
  300. font-size: 14px;
  301. color: #666;
  302. padding-left: 22px;
  303. /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
  304. background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
  305. margin-bottom: 15px;
  306. }
  307. .course-item .section-list li .section-title {
  308. /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
  309. text-overflow: ellipsis;
  310. overflow: hidden;
  311. white-space: nowrap;
  312. display: inline-block;
  313. max-width: 200px;
  314. }
  315. .course-item .section-list li:hover {
  316. background-image: url("/src/assets/img/play-icon-yellow.svg");
  317. color: #ffc210;
  318. }
  319. .course-item .section-list li .free {
  320. width: 34px;
  321. height: 20px;
  322. color: #fd7b4d;
  323. vertical-align: super;
  324. margin-left: 10px;
  325. border: 1px solid #fd7b4d;
  326. border-radius: 2px;
  327. text-align: center;
  328. font-size: 13px;
  329. white-space: nowrap;
  330. }
  331. .course-item .section-list li:hover .free {
  332. color: #ffc210;
  333. border-color: #ffc210;
  334. }
  335. .course-item {
  336. position: relative;
  337. }
  338. .course-item .pay-box {
  339. position: absolute;
  340. bottom: 20px;
  341. width: 600px;
  342. }
  343. .course-item .pay-box::after {
  344. content: "";
  345. display: block;
  346. clear: both;
  347. }
  348. .course-item .pay-box .discount-type {
  349. padding: 6px 10px;
  350. font-size: 16px;
  351. color: #fff;
  352. text-align: center;
  353. margin-right: 8px;
  354. background: #fa6240;
  355. border: 1px solid #fa6240;
  356. border-radius: 10px 0 10px 0;
  357. float: left;
  358. }
  359. .course-item .pay-box .discount-price {
  360. font-size: 24px;
  361. color: #fa6240;
  362. float: left;
  363. }
  364. .course-item .pay-box .original-price {
  365. text-decoration: line-through;
  366. font-size: 14px;
  367. color: #9b9b9b;
  368. margin-left: 10px;
  369. float: left;
  370. margin-top: 10px;
  371. }
  372. .course-item .pay-box .buy-now {
  373. width: 120px;
  374. height: 38px;
  375. background: transparent;
  376. color: #fa6240;
  377. font-size: 16px;
  378. border: 1px solid #fd7b4d;
  379. border-radius: 3px;
  380. transition: all .2s ease-in-out;
  381. float: right;
  382. text-align: center;
  383. line-height: 38px;
  384. position: absolute;
  385. right: 0;
  386. bottom: 5px;
  387. }
  388. .course-item .pay-box .buy-now:hover {
  389. color: #fff;
  390. background: #ffc210;
  391. border: 1px solid #ffc210;
  392. }
  393. .course .course_pagination {
  394. margin-bottom: 60px;
  395. text-align: center;
  396. }
  397. </style>

搜索接口

  1. class CourseSearchView(GenericViewSet, CommonListModelMixin):
  2. queryset = Course.objects.all().filter(is_delete=False, is_show=True).order_by('orders')
  3. serializer_class = CourseSerializer
  4. pagination_class = CommonPageNumberPagination
  5. filter_backends = [SearchFilter]
  6. search_fields = ['name']

支付宝支付介绍

前端点击立即购买功能,会生成订单并跳转到付款界面

  1. # 支付宝支付
  2. -测试环境:大家都可以测试
  3. -https://openhome.alipay.com/develop/sandbox/app
  4. -正式环境:需要申请,有营业执照

咱们开发虽然用的沙箱环境,后期上线,公司会自己注册,

注册成功后有个商户id号,作为开发,只要有商户id号,其他步骤都是一样,

所有无论开发还是测试,代码都一样,只是商户号不一样

使用支付宝支付

  • API接口

  • SDK:优先使用,早期支付宝没有python的sdk,后期有了

    1. -使用了第三方sdk
    2. -第三方人通过api接口,使用python封装了sdk,开源出来了

沙箱环境

-安卓的支付宝app,付款用的(买家用)

-扫码使用这个app,付款,这个app的钱都是假的,付款测试商户(卖家)

支付测试,生成支付链接

安装

pip install python-alipay-sdk

生成公钥私钥





我们可以将生成的公钥配置在支付宝的(沙箱环境)上,生成一个支付宝公钥

以后我们使用这个支付宝公钥即可



我们需要将支付宝的公钥,以及项目的应用私钥放入项目中

-pub.pem

-pri.pem

注意:

我们的公钥密钥需要符合要求格式

教程参考:https://github.com/fzlee/alipay/tree/master/tests/certs/ali

支付测试代码:

  1. from alipay import AliPay
  2. from alipay.utils import AliPayConfig
  3. app_private_key_string = open("pri.pem").read()
  4. alipay_public_key_string = open("pub.pem").read()
  5. alipay = AliPay(
  6. appid="2021000122628354", # 沙盒支付宝appid
  7. app_notify_url=None, # 默认回调 url
  8. app_private_key_string=app_private_key_string,
  9. # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
  10. alipay_public_key_string=alipay_public_key_string,
  11. sign_type="RSA2", # RSA 或者 RSA2
  12. debug=False, # 默认 False
  13. verbose=False, # 输出调试数据
  14. config=AliPayConfig(timeout=15) # 可选,请求超时时间
  15. )
  16. res=alipay.api_alipay_trade_page_pay(subject='基尼台妹', out_trade_no='asdbasbdjqweo', total_amount='2888')
  17. print('https://openapi.alipaydev.com/gateway.do?'+res)

运行脚本获取链接,打开

支付宝支付二次封装

目录结构

  1. libs
  2. ├── iPay # aliapy二次封装包
  3. ├── __init__.py # 包文件
  4. ├── pem # 公钥私钥文件夹
  5. ├── alipay_public_key.pem # 支付宝公钥文件
  6. ├── app_private_key.pem # 应用私钥文件
  7. ├── pay.py # 支付文件
  8. └── └── settings.py # 应用配置

init.py

  1. from .pay import alipay
  2. from .settings import GETWAY

pay.py

  1. from alipay import AliPay
  2. from alipay.utils import AliPayConfig
  3. from . import settings
  4. alipay = AliPay(
  5. appid=settings.APP_ID,
  6. app_notify_url=None, # 默认回调 url
  7. app_private_key_string=settings.APP_PRIVATE_KEY_STRING,
  8. # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥,
  9. alipay_public_key_string=settings.ALIPAY_PUBLIC_KEY_STRING,
  10. sign_type=settings.SIGN, # RSA 或者 RSA2
  11. debug=settings.DEBUG, # 默认 False
  12. verbose=settings.DEBUG, # 输出调试数据
  13. config=AliPayConfig(timeout=15) # 可选,请求超时时间
  14. )

settings.py

  1. import os
  2. # 应用私钥
  3. APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'app_private_key.pem')).read()
  4. # 支付宝公钥
  5. ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'pem', 'alipay_public_key.pem')).read()
  6. # 应用ID
  7. APP_ID = '22222222222'
  8. # 加密方式
  9. SIGN = 'RSA2'
  10. # 是否是支付宝测试环境(沙箱环境),如果采用真是支付宝环境,配置False
  11. DEBUG = True
  12. # 支付网关
  13. GATEWAY = 'https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'

订单表设计

-订单表

-订单详情表

  1. 下单接口-->没有支付是订单时待支付状态
  2. 支付宝post回调接口--> 修改订单状态 --已完成
  3. 前端get回调接口

我们需要新建order app

models.py

  1. # Create your models here.
  2. # 订单板块需要写的接口
  3. # 新建order 的app,在models.py中写入表
  4. from django.db import models
  5. from django.db import models
  6. from course.models import Course
  7. '''
  8. ForeignKey 中on_delete
  9. -CASCADE 级联删除
  10. -DO_NOTHING 啥都不做,没有外键约束才能用它
  11. -SET_NULL 字段置为空,字段 null=True
  12. -SET_DEFAULT 设置为默认值,default='xx'
  13. -PROTECT 受保护的,很少用
  14. -models.SET(函数内存地址) 会设置成set内的值
  15. '''
  16. class Order(models.Model):
  17. """订单模型"""
  18. status_choices = (
  19. (0, '未支付'),
  20. (1, '已支付'),
  21. (2, '已取消'),
  22. (3, '超时取消'),
  23. )
  24. pay_choices = (
  25. (1, '支付宝'),
  26. (2, '微信支付'),
  27. )
  28. # 订单标题
  29. subject = models.CharField(max_length=150, verbose_name="订单标题")
  30. # 订单总价格
  31. total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="订单总价", default=0)
  32. # 订单号,咱们后端生成的,唯一:后期支付宝回调回来的数据会带着这个订单号,根据这个订单号修改订单状态
  33. # 使用什么生成? uuid(可能重复,概率很多) 【分布式id的生成】 雪花算法
  34. out_trade_no = models.CharField(max_length=64, verbose_name="订单号", unique=True)
  35. # 流水号:支付宝生成的,回调回来,会带着
  36. trade_no = models.CharField(max_length=64, null=True, verbose_name="流水号")
  37. # 订单状态
  38. order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
  39. # 支付类型,目前只有支付宝
  40. pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
  41. # 支付时间---》支付宝回调回来,会带着
  42. pay_time = models.DateTimeField(null=True, verbose_name="支付时间")
  43. # 跟用户一对多 models.DO_NOTHING
  44. user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False,
  45. verbose_name="下单用户")
  46. created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
  47. class Meta:
  48. db_table = "luffy_order"
  49. verbose_name = "订单记录"
  50. verbose_name_plural = "订单记录"
  51. def __str__(self):
  52. return "%s - ¥%s" % (self.subject, self.total_amount)
  53. class OrderDetail(models.Model):
  54. """订单详情"""
  55. # related_name 反向查询替换表名小写_set
  56. # on_delete 级联删除
  57. # db_constraint=False ----》默认是True,会在表中为Order何OrderDetail创建外键约束
  58. # db_constraint=False 没有外键约束,插入数据 速度快, 可能会产生脏数据【不合理】,所以咱们要用程序控制,以后公司惯用的
  59. # 对到数据库上,它是不建立外键,基于对象的跨表查,基于连表的查询,继续用,跟之前没有任何区别
  60. order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False,
  61. verbose_name="订单")
  62. course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.DO_NOTHING, db_constraint=False,
  63. verbose_name="课程")
  64. price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
  65. real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
  66. class Meta:
  67. db_table = "luffy_order_detail"
  68. verbose_name = "订单详情"
  69. verbose_name_plural = "订单详情"
  70. def __str__(self):
  71. try:
  72. return "%s的订单:%s" % (self.course.name, self.order.out_trade_no)
  73. except:
  74. return super().__str__()

执行迁移命令>>>

下单接口

  1. 接口分析:
  2. 用户登录后才能使用
  3. 前端点击立即购买 ---> post请求携带数据
  4. {courses:[1,],total_amount:99.9,subject:'xx课程'}
  5. 视图类中重写create方法
  6. 将主要逻辑写到序列化类中
  7. # 主要逻辑:
  8. 1 取出所有课程id号,拿到课程
  9. 2 统计总价格,跟传入的total_amount做比较,如果一样,继续往后
  10. 3 获取购买人信息:登录后才能访问的接口 request.user
  11. 4 生成订单号 支付链接需要,存订单表需要
  12. 5 生成支付链接:支付宝支付生成,
  13. 6 生成订单记录,订单是待支付状态(order,order_detail)
  14. 7 返回前端支付链接

路由

  1. from rest_framework.routers import SimpleRouter
  2. from . import views
  3. router = SimpleRouter()
  4. router.register('pay', views.PayView, 'pay')
  5. urlpatterns = [
  6. # path('',include(router.urls))
  7. ]
  8. urlpatterns += router.urls

视图层

  1. from rest_framework.viewsets import GenericViewSet
  2. from rest_framework.mixins import CreateModelMixin
  3. from .models import Order
  4. from .serializer import PaySerializer
  5. from utils.response import APIResponse
  6. from rest_framework_jwt.authentication import JSONWebTokenAuthentication
  7. from rest_framework.permissions import IsAuthenticated
  8. # Create your views here.
  9. class PayView(GenericViewSet,CreateModelMixin):
  10. queryset = Order.objects.all()
  11. serializer_class = PaySerializer
  12. authentication_classes = [JSONWebTokenAuthentication] # 使用JWT权限类配置必须配权限类
  13. permission_classes = [IsAuthenticated]
  14. def create(self, request, *args, **kwargs):
  15. serializer = self.get_serializer(data=request.data,context={'request':request})
  16. serializer.is_valid(raise_exception=True)
  17. self.perform_create(serializer)
  18. pay_url = serializer.context.get("pay_url")
  19. return APIResponse(pay_url=pay_url)

序列化类

  1. # 校验字段,反序列化 不会序列化的
  2. class PaySerializer(serializers.ModelSerializer):
  3. # courses 不是表的字段,需要重写--->新东西
  4. # courses=serializers.ListField() # 咱们不用这种 courses=[1,2,3]
  5. # 前端传入的 courses=[1,2,3]--->根据queryset对应的qs对象 做映射,映射成courses=[课程对象1,课程对象2,课程对象3]
  6. courses = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), many=True)
  7. class Meta:
  8. model = Order
  9. fields = ['courses', 'total_amount', 'subject'] # 前端传入的字段是什么,这里就写什么
  10. def _check_total_amount(self, attrs):
  11. courses = attrs.get('courses') # 课程对象列表 [课程对象1,课程对象2]
  12. total_amount = attrs.get('total_amount')
  13. new_total_amount = 0
  14. for course in courses:
  15. new_total_amount += course.price
  16. if total_amount == new_total_amount:
  17. return new_total_amount
  18. raise APIException('价格有误!!')
  19. def _get_out_trade_no(self):
  20. # uuid生成
  21. return str(uuid.uuid4())
  22. def _get_user(self):
  23. user = self.context.get('request').user
  24. return user
  25. def _get_pay_url(self, out_trade_no, total_amount, subject):
  26. # 生成支付链接
  27. res = alipay.api_alipay_trade_page_pay(
  28. total_amount=float(total_amount),
  29. subject=subject,
  30. out_trade_no=out_trade_no,
  31. return_url=settings.RETURN_URL, # 前端的
  32. notify_url=settings.NOTIFY_URL # 后端接口,写这个接口该订单状态
  33. )
  34. # return GATEWAY + res
  35. self.context['pay_url'] = GATEWAY + res
  36. def _before_create(self, attrs, user, out_trade_no):
  37. # 剔除courses----》要不要剔除,要pop,但是不在这,在create方法中pop
  38. # 订单号,加入到attrs中
  39. attrs['out_trade_no'] = out_trade_no
  40. # 把user加入到attrs中
  41. attrs['user'] = user
  42. def validate(self, attrs):
  43. # 1)订单总价校验
  44. total_amount = self._check_total_amount(attrs)
  45. # 2)生成订单号
  46. out_trade_no = self._get_out_trade_no()
  47. # 3)支付用户:request.user
  48. user = self._get_user()
  49. # 4)支付链接生成
  50. self._get_pay_url(out_trade_no, total_amount, attrs.get('subject'))
  51. # 5)入库(两个表)的信息准备
  52. self._before_create(attrs, user, out_trade_no)
  53. return attrs
  54. # 生成订单,存订单表,一定要重写create,存俩表
  55. def create(self, validated_data):
  56. # validated_data:{subject,total_amount,user,out_trade_no,courses}
  57. courses = validated_data.pop('courses')
  58. order = Order.objects.create(**validated_data)
  59. # 存订单详情表,存几条,取决于courses有几个
  60. for course in courses:
  61. OrderDetail.objects.create(order=order, course=course, price=course.price, real_price=course.price)
  62. return order

序列化类中要使用request对象,所以可以将request传入context上下文,在序列化类使用。

我们还是在全局钩子里写逻辑。

分析我们要使用序列化类做的事情:校验字段、反序列化。(不做序列化)

courses不是订单表的字段,需要在序列化类重写。courses是个列表,需要使用ListField。但是还有别的方法:



因为是反序列化多条数据,所以要加many=True

注意我们必须要登录之后才能获取订单链接,

我们使用权限类+认证类来限制登录用户下单

前端支付页面



需要携带token向后端发送请求。

数据库查看订单状态

支付成功后会回调到前端地址



所以要在前端再写一个支付成功页面:

CourseDetail.vue

  1. go_pay() {
  2. // 判断是否登录
  3. let token = this.$cookies.get('token')
  4. if (token) {
  5. this.$axios.post(this.$settings.BASE_URL + '/order/pay/', {
  6. subject: this.course_info.name,
  7. total_amount: this.course_info.price,
  8. courses: [this.course_id]
  9. }, {
  10. headers: {
  11. Authorization: `jwt ${token}`
  12. }
  13. }).then(res => {
  14. if (res.data.code == 100) {
  15. // 打开支付连接地址
  16. open(res.data.pay_url, '_self');
  17. } else {
  18. this.$message(res.data.msg)
  19. }
  20. })
  21. } else {
  22. this.$message('您没有登录,请先登录')
  23. }
  24. }

PaySuccess.vue

  1. <template>
  2. <div class="pay-success">
  3. <!--如果是单独的页面,就没必要展示导航栏(带有登录的用户)-->
  4. <Header/>
  5. <div class="main">
  6. <div class="title">
  7. <div class="success-tips">
  8. <p class="tips">您已成功购买 1 门课程!</p>
  9. </div>
  10. </div>
  11. <div class="order-info">
  12. <p class="info"><b>订单号:</b><span>{{ result.out_trade_no }}</span></p>
  13. <p class="info"><b>交易号:</b><span>{{ result.trade_no }}</span></p>
  14. <p class="info"><b>付款时间:</b><span><span>{{ result.timestamp }}</span></span></p>
  15. </div>
  16. <div class="study">
  17. <span>立即学习</span>
  18. </div>
  19. </div>
  20. </div>
  21. </template>
  22. <script>
  23. import Header from "@/components/Header"
  24. export default {
  25. name: "Success",
  26. data() {
  27. return {
  28. result: {},
  29. };
  30. },
  31. created() {
  32. // 解析支付宝回调的url参数
  33. let params = location.search.substring(1); // 去除? => a=1&b=2
  34. let items = params.length ? params.split('&') : []; // ['a=1', 'b=2']
  35. //逐个将每一项添加到args对象中
  36. for (let i = 0; i < items.length; i++) { // 第一次循环a=1,第二次b=2
  37. let k_v = items[i].split('='); // ['a', '1']
  38. //解码操作,因为查询字符串经过编码的
  39. if (k_v.length >= 2) {
  40. // url编码反解
  41. let k = decodeURIComponent(k_v[0]);
  42. this.result[k] = decodeURIComponent(k_v[1]);
  43. // 没有url编码反解
  44. // this.result[k_v[0]] = k_v[1];
  45. }
  46. }
  47. // 把地址栏上面的支付结果,再get请求转发给后端
  48. this.$axios({
  49. url: this.$settings.BASE_URL + '/order/success/' + location.search,
  50. method: 'get',
  51. }).then(response => {
  52. if (response.data.code != 100) {
  53. alert(response.data.msg)
  54. }
  55. }).catch(() => {
  56. console.log('支付结果同步失败');
  57. })
  58. },
  59. components: {
  60. Header,
  61. }
  62. }
  63. </script>
  64. <style scoped>
  65. .main {
  66. padding: 60px 0;
  67. margin: 0 auto;
  68. width: 1200px;
  69. background: #fff;
  70. }
  71. .main .title {
  72. display: flex;
  73. -ms-flex-align: center;
  74. align-items: center;
  75. padding: 25px 40px;
  76. border-bottom: 1px solid #f2f2f2;
  77. }
  78. .main .title .success-tips {
  79. box-sizing: border-box;
  80. }
  81. .title img {
  82. vertical-align: middle;
  83. width: 60px;
  84. height: 60px;
  85. margin-right: 40px;
  86. }
  87. .title .success-tips {
  88. box-sizing: border-box;
  89. }
  90. .title .tips {
  91. font-size: 26px;
  92. color: #000;
  93. }
  94. .info span {
  95. color: #ec6730;
  96. }
  97. .order-info {
  98. padding: 25px 48px;
  99. padding-bottom: 15px;
  100. border-bottom: 1px solid #f2f2f2;
  101. }
  102. .order-info p {
  103. display: -ms-flexbox;
  104. display: flex;
  105. margin-bottom: 10px;
  106. font-size: 16px;
  107. }
  108. .order-info p b {
  109. font-weight: 400;
  110. color: #9d9d9d;
  111. white-space: nowrap;
  112. }
  113. .study {
  114. padding: 25px 40px;
  115. }
  116. .study span {
  117. display: block;
  118. width: 140px;
  119. height: 42px;
  120. text-align: center;
  121. line-height: 42px;
  122. cursor: pointer;
  123. background: #ffc210;
  124. border-radius: 6px;
  125. font-size: 16px;
  126. color: #fff;
  127. }
  128. </style>

支付成功回调接口

  1. # 支付成功,支付宝会有俩回调
  2. -get 回调,调前端
  3. -为了保证准确性,支付宝回调会前端后,我们自己向后端发送一个请求,查询一下这个订单是否支付成功
  4. -post 回调,调后端接口
  5. -后端接口,接受支付宝的回调,修改订单状态
  6. -这个接口需要登录吗?不需要任何的认证和权限
  7. -如果用户点了支付----》跳转到了支付宝页面---》你的服务挂机了---》会出现什么情况
  8. -支付宝在24小时内,会有8次回调,
  9. # 两个接口:
  10. -post回调,给支付宝用
  11. -get回调,给我们前端做二次校验使用

由于我们现在处于内网,所以接收不到回调信息

  1. class PaySuccess(APIView):
  2. def get(self, request): # 咱们用的
  3. out_trade_no = request.query_params.get('out_trade_no')
  4. order = Order.objects.filter(out_trade_no=out_trade_no, order_status=1).first()
  5. if order: # 支付宝回调完, 订单状态改了
  6. return APIResponse()
  7. else:
  8. return APIResponse(code=101, msg='暂未收到您的付款,请稍后刷新再试')
  9. def post(self, request): # 给支付宝用的,项目需要上线后才能看到 内网中,无法回调成功【使用内网穿透】
  10. try:
  11. result_data = request.data.dict() # requset.data 是post提交的数据,如果是urlencoded格式,requset.data是QueryDict对象,方法dict()---》转成真正的字典
  12. out_trade_no = result_data.get('out_trade_no')
  13. signature = result_data.pop('sign')
  14. # 验证签名的---》验签
  15. result = alipay_v1.alipay.verify(result_data, signature)
  16. if result and result_data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
  17. # 完成订单修改:订单状态、流水号、支付时间
  18. Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1)
  19. # 完成日志记录
  20. logger.warning('%s订单支付成功' % out_trade_no)
  21. return Response('success') # 都是支付宝要求的
  22. else:
  23. logger.error('%s订单支付失败' % out_trade_no)
  24. except:
  25. pass
  26. return Response('failed') # 都是支付宝要求的

Response的格式需要符合支付宝要求。如果支付宝回调回不去了(后端崩了),48小时之内支付宝会进行8次回调,任意一次回调成功就可以了(给支付宝返回success)。如果8次回调都没有收到,还有一个对账单的功能。

这两个接口是否需要添加认证?

不能加任何认证和权限,会导致支付宝无法回调。加个频率没关系。

Python实战项目-10文件存储/支付宝支付/支付成功回调接口的更多相关文章

  1. 再一波Python实战项目列表

    前言: 近几年Python可谓是大热啊,很多人都纷纷投入Python的学习中,以前我们实验楼总结过多篇Python实战项目列表,不但有用还有趣,最主要的是咱们实验楼不但有详细的开发教程,更有在线开发环 ...

  2. Python实战项目网络爬虫 之 爬取小说吧小说正文

    本次实战项目适合,有一定Python语法知识的小白学员.本人也是根据一些网上的资料,自己摸索编写的内容.有不明白的童鞋,欢迎提问. 目的:爬取百度小说吧中的原创小说<猎奇师>部分小说内容 ...

  3. Python常用的数据文件存储的4种格式(txt/json/csv/excel)及操作Excel相关的第三方库(xlrd/xlwt/pandas/openpyxl)(2021最新版)

    序言:保存数据的方式各种各样,最简单的方式是直接保存为文本文件,如TXT.JSON.CSV等,除此之外Excel也是现在比较流行的存储格式,通过这篇文章你也将掌握通过一些第三方库(xlrd/xlwt/ ...

  4. python实战项目 — 使用bs4 爬取猫眼电影热榜(存入本地txt、以及存储数据库列表)

    案例一: 重点: 1. 使用bs4 爬取 2. 数据写入本地 txt from bs4 import BeautifulSoup import requests url = "http:// ...

  5. python实战项目

    没有一个完整的项目开发过程,是不会对整个开发流程以及理论知识有牢固的认知的,对于怎样将所学的理论知识应用到实际开发中更是不得而知了! 以上就是我们在学习过程中必须要有项目实战开发经验的原因,其实无论项 ...

  6. python实战项目 — 爬取中国票房网年度电影信息并保存在csv

    import pandas as pd import requests from bs4 import BeautifulSoup import time def spider(url, header ...

  7. python实战项目练习-Django商城项目之注册功能实现

    设计到的前端知识 项目的前端页面使用vue来实现局部刷新,通过数据的双向绑定实现与用户的交互,下面来看一下需求,在用户输入内容后,前端需要做一些简单的规则校验,我们希望在在用户输入后能够实时检测,如果 ...

  8. ansible 实战项目之文件操作(二)

    一,前言 如果没有安装好的话看我以前的贴子哦!! 上次安装已经确定通了,所以下面步骤应该是完全ok的 特点: (1).轻量级,无需在客户端安装agent,更新时,只需在操作机上进行一次更新即可: (2 ...

  9. python实战项目 — 爬取 妹子图网,保存图片到本地

    重点: 1. 用def函数 2. 使用 os.path.dirname("路径保存") , 实现每组图片保存在独立的文件夹中 方法1: import requests from l ...

  10. 7 个有趣的 Python 实战项目,超级适合练手

    关于Python,有一句名言:不要重复造轮子. 但是问题有三个: 1.你不知道已经有哪些轮子已经造好了,哪个适合你用.有名有姓的的著名轮子就400多个,更别说没名没姓自己在制造中的轮子. 2.确实没重 ...

随机推荐

  1. [OC] APP唤醒,URL Scheme,工程中的 URL Types 和 LSApplicationQueriesSchemes

    1.网页唤醒APP: 假设我们有一个APP,名字叫做 "APP甲",需要通过网页唤起 APP甲,我们首先需要在 APP甲的工程文件里配置参数 URL Types: 在 info.p ...

  2. springsecurity 配置swagger

    最近在学习springsecurity 安全框架,具体是什么概念在这里不一一赘述了.下面呢,咱们一起搭建一下简单的springsecurity swagger 项目感受一下. 首先初始化spring ...

  3. CSR,SSR,PreRender原理解密

    CSR.SSR.Prerender 原理全解密   做前端的同学们肯定或多或少听说过CSR,SSR,Prerender这些名词,但是大多肯定只是停留在听说过,了解过,略懂一点,但是,你真的理解这些技术 ...

  4. 安卓10.0蓝牙HIDL的直通式初始化流程

    本文仅介绍扼要的流程,没有系统化介绍. 首先从system\bt\hci\src\hci_layer_android.cc文件的函数void hci_initialize() 开始初始化: void ...

  5. jmeter中使用csv文件时设置编码

    1.新建XLS文件,另存为CSV格式文件 2.在jmeter中可以尝试将编码设置成GB2312,或者utf-8

  6. ucharts的区域图、折线图(有x轴的),修改x轴显示为隔一个显示

    1.原本的显示方式: 2.想要的效果: 3.这边我使用的是uchart的组件,在uni_modules > qiun-data-charts > js_sdk > u-charts, ...

  7. python之变量

    什么是变量? 用来记录事务的变化状态,计算机模拟人,就需要具备人类某一个功能.你通过记录我的名字年龄等一系列的身份信息,以此和我进行匹配,确定我就是phoebe这个人. 为什么有变量? 游戏里的英雄角 ...

  8. Python人脸识别——电脑摄像头检测人脸与眼睛

    ##10 电脑摄像头检测人脸与眼睛 import cv2 #检测图片 def detect(frame):     #灰度化图片,减少运算量     #img = cv2.cvtColor(frame ...

  9. vs xamarin获取sha1申请百度sdk密钥

    请查看微软帮助文档 查找密钥存储的签名 - Xamarin | Microsoft Docs

  10. centos mail 发邮件

    1.安装mailx yum -y install mailx 2. /etc/mail.rc 最后增加邮件配置如 set smtp=smtp.qq.comset smtp-auth=loginset ...