下单结算

客户端展示下单结算界面展示

views/Cart.vue,代码:

  1. <div class="li-3"><router-link to="/order" class="btn">去结算</router-link></div>

router/index.js,代码:

  1. // 路由列表
  2. const routes = [
  3. {
  4. meta:{
  5. title: "luffy2.0-首页",
  6. keepAlive: true
  7. },
  8. path:'/', // uri访问地址
  9. component: ()=> import("../views/Home.vue")
  10. },{
  11. meta:{
  12. title: "登录",
  13. keepAlive: true
  14. },
  15. path: '/login',
  16. name: "Login", // 路由名称
  17. component: ()=> import("../views/Login.vue"), // uri绑定的组件页面
  18. },{
  19. meta:{
  20. title: "注册",
  21. keepAlive: true
  22. },
  23. path: '/register',
  24. name: "Register", // 路由名称
  25. component: ()=> import("../views/Register.vue"), // uri绑定的组件页面
  26. },{
  27. meta:{
  28. title: "项目课",
  29. keepAlive: true
  30. },
  31. path: '/project',
  32. name: "Course", // 路由名称
  33. component: ()=> import("../views/Course.vue"), // uri绑定的组件页面
  34. },{
  35. meta:{
  36. title: "项目课",
  37. keepAlive: true
  38. },
  39. path: '/project/:id',
  40. name: "Info",
  41. component: ()=> import("../views/Info.vue"),
  42. },{
  43. meta:{
  44. title: "购物车",
  45. keepAlive: true
  46. },
  47. path: '/cart',
  48. name: "Cart",
  49. component: ()=> import("../views/Cart.vue"),
  50. },{
  51. meta:{
  52. title: "确认下单",
  53. keepAlive: true
  54. },
  55. path: '/order',
  56. name: "Order",
  57. component: ()=> import("../views/Order.vue"),
  58. }
  59. ]

views/Order.vue,代码:

  1. <template>
  2. <div class="cart">
  3. <Header/>
  4. <div class="cart-main">
  5. <div class="cart-header">
  6. <div class="cart-header-warp">
  7. <div class="cart-title left">
  8. <h1 class="left">确认订单</h1>
  9. </div>
  10. <div class="right">
  11. <div class="">
  12. <span class="left"><router-link class="myorder-history" to="/cart">返回购物车</router-link></span>
  13. </div>
  14. </div>
  15. </div>
  16. </div>
  17. <div class="cart-body" id="cartBody">
  18. <div class="cart-body-title"><p class="item-1 l">课程信息</p></div>
  19. <div class="cart-body-table">
  20. <div class="item">
  21. <div class="item-2">
  22. <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
  23. <dl class="l has-package">
  24. <dt>【实战课程】3天Typescript精修 </dt>
  25. <p class="package-item">减免价</p>
  26. </dl>
  27. </div>
  28. <div class="item-3">
  29. <div class="price">
  30. <p class="discount-price"><em></em><span>998.00</span></p>
  31. <p class="original-price"><em></em><span>800.00</span></p>
  32. </div>
  33. </div>
  34. </div>
  35. <div class="item">
  36. <div class="item-2">
  37. <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
  38. <dl class="l has-package">
  39. <dt>【实战课程】3天Typescript精修 </dt>
  40. <p class="package-item">减免价</p>
  41. </dl>
  42. </div>
  43. <div class="item-3">
  44. <div class="price">
  45. <p class="discount-price"><em></em><span>998.00</span></p>
  46. <p class="original-price"><em></em><span>800.00</span></p>
  47. </div>
  48. </div>
  49. </div>
  50. <div class="item">
  51. <div class="item-2">
  52. <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
  53. <dl class="l has-package">
  54. <dt>【实战课程】3天Typescript精修 </dt>
  55. <p class="package-item">减免价</p>
  56. </dl>
  57. </div>
  58. <div class="item-3">
  59. <div class="price">
  60. <p class="discount-price"><em></em><span>998.00</span></p>
  61. <p class="original-price"><em></em><span>800.00</span></p>
  62. </div>
  63. </div>
  64. </div>
  65. <div class="item">
  66. <div class="item-2">
  67. <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
  68. <dl class="l has-package">
  69. <dt>【实战课程】3天Typescript精修 </dt>
  70. <p class="package-item">减免价</p>
  71. </dl>
  72. </div>
  73. <div class="item-3">
  74. <div class="price">
  75. <p class="discount-price"><em></em><span>998.00</span></p>
  76. <p class="original-price"><em></em><span>800.00</span></p>
  77. </div>
  78. </div>
  79. </div>
  80. <div class="item">
  81. <div class="item-2">
  82. <a href="" class="img-box l"><img src="../assets/course-9.png"></a>
  83. <dl class="l has-package">
  84. <dt>【实战课程】3天Typescript精修 </dt>
  85. <p class="package-item">减免价</p>
  86. </dl>
  87. </div>
  88. <div class="item-3">
  89. <div class="price">
  90. <p class="discount-price"><em></em><span>998.00</span></p>
  91. <p class="original-price"><em></em><span>800.00</span></p>
  92. </div>
  93. </div>
  94. </div>
  95. </div>
  96. <div class="coupons-box">
  97. <div class="coupon-title-box">
  98. <p class="coupon-title">
  99. 使用优惠券/积分
  100. <span v-if="state.use_coupon" @click="state.use_coupon=!state.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></span>
  101. <span v-else @click="state.use_coupon=!state.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"></path></svg></span>
  102. <!-- <i :class="state.use_coupon?'el-icon-arrow-up':'el-icon-arrow-down'" @click="state.use_coupon=!state.use_coupon"></i>-->
  103. </p>
  104. </div>
  105. <transition name="el-zoom-in-top">
  106. <div class="coupon-del-box" v-if="state.use_coupon">
  107. <div class="coupon-switch-box">
  108. <div class="switch-btn ticket" :class="{'checked': state.discount_type===0}" @click="state.discount_type=0">优惠券 (4)<em><i class="imv2-check"></i></em></div>
  109. <div class="switch-btn code" :class="{'checked': state.discount_type===1}" @click="state.discount_type=1">积分<em><i class="imv2-check"></i></em></div>
  110. </div>
  111. <div class="coupon-content ticket" v-if="state.discount_type===0">
  112. <p class="no-coupons" v-if="state.coupon_list.length<1">暂无可用优惠券</p>
  113. <div class="coupons-box" v-else>
  114. <div class="content-box">
  115. <ul class="nouse-box">
  116. <li class="l">
  117. <div class="detail-box more-del-box">
  118. <div class="price-box">
  119. <p class="coupon-price l"> ¥100 </p>
  120. <p class="use-inst l">满499可用</p>
  121. </div>
  122. <div class="use-detail-box">
  123. <div class="use-ajust-box">适用于:全部实战课程</div>
  124. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  125. </div>
  126. </div>
  127. </li>
  128. <li class="l select">
  129. <div class="detail-box more-del-box">
  130. <div class="price-box">
  131. <p class="coupon-price l"> ¥248 </p>
  132. <p class="use-inst l">满999可用</p>
  133. </div>
  134. <div class="use-detail-box">
  135. <div class="use-ajust-box">适用于:全部实战课程</div>
  136. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  137. </div>
  138. </div>
  139. </li>
  140. <li class="l wait-use">
  141. <div class="detail-box more-del-box">
  142. <div class="price-box">
  143. <p class="coupon-price l"> ¥248 </p>
  144. <p class="use-inst l">满999可用</p>
  145. </div>
  146. <div class="use-detail-box">
  147. <div class="use-ajust-box">适用于:全部实战课程</div>
  148. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  149. </div>
  150. </div>
  151. </li>
  152. <li class="l wait-use">
  153. <div class="detail-box more-del-box">
  154. <div class="price-box">
  155. <p class="coupon-price l"> ¥248 </p>
  156. <p class="use-inst l">满999可用</p>
  157. </div>
  158. <div class="use-detail-box">
  159. <div class="use-ajust-box">适用于:全部实战课程</div>
  160. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  161. </div>
  162. </div>
  163. </li>
  164. </ul>
  165. <ul class="use-box">
  166. <li class="l useing">
  167. <div class="detail-box more-del-box">
  168. <div class="price-box">
  169. <p class="coupon-price l"> ¥100 </p>
  170. <p class="use-inst l">满499可用</p>
  171. </div>
  172. <div class="use-detail-box">
  173. <div class="use-ajust-box">适用于:全部实战课程</div>
  174. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  175. </div>
  176. </div>
  177. </li>
  178. <li class="l">
  179. <div class="detail-box more-del-box">
  180. <div class="price-box">
  181. <p class="coupon-price l"> ¥248 </p>
  182. <p class="use-inst l">满999可用</p>
  183. </div>
  184. <div class="use-detail-box">
  185. <div class="use-ajust-box">适用于:全部实战课程</div>
  186. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  187. </div>
  188. </div>
  189. </li>
  190. </ul>
  191. <ul class="overdue-box">
  192. <li class="l useing">
  193. <div class="detail-box more-del-box">
  194. <div class="price-box">
  195. <p class="coupon-price l"> ¥100 </p>
  196. <p class="use-inst l">满499可用</p>
  197. </div>
  198. <div class="use-detail-box">
  199. <div class="use-ajust-box">适用于:全部实战课程</div>
  200. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  201. </div>
  202. </div>
  203. </li>
  204. <li class="l">
  205. <div class="detail-box more-del-box">
  206. <div class="price-box">
  207. <p class="coupon-price l"> ¥248 </p>
  208. <p class="use-inst l">满999可用</p>
  209. </div>
  210. <div class="use-detail-box">
  211. <div class="use-ajust-box">适用于:全部实战课程</div>
  212. <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
  213. </div>
  214. </div>
  215. </li>
  216. </ul>
  217. </div>
  218. </div>
  219. </div>
  220. <div class="coupon-content code" v-else>
  221. <div class="input-box">
  222. <el-input-number placeholder="10积分=1元" v-model="state.credit" :step="1" :min="0" :max="1000"></el-input-number>
  223. <a class="convert-btn">兑换</a>
  224. </div>
  225. <div class="converted-box">
  226. <p>使用积分:<span class="code-num">200</span></p>
  227. <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
  228. <span class="discount-cash">100积分抵扣:<em>10</em></span>
  229. </p>
  230. <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
  231. <span class="discount-cash">100积分抵扣:<em>10</em></span>
  232. </p>
  233. </div>
  234. <p class="error-msg">本次订单最多可以使用1000积分,您当前拥有200积分。(10积分=1元)</p>
  235. <p class="tip">说明:每笔订单只能使用一次积分,并只有在部分允许使用积分兑换的课程中才能使用。</p>
  236. </div>
  237. </div>
  238. </transition>
  239. </div>
  240. <div class="pay-type">
  241. <p class="title">选择支付方式</p>
  242. <div class="list">
  243. <img :src="state.pay_type==0?'/src/assets/alipay2.png':'/src/assets/alipay1.png'" @click="state.pay_type=0" alt="支付宝">
  244. <img :src="state.pay_type==1?'/src/assets/wechat2.png':'/src/assets/wechat1.png'" @click="state.pay_type=1" alt="微信">
  245. <img :src="state.pay_type==2?'/src/assets/yue2.png':'/src/assets/yue1.png'" @click="state.pay_type=2" alt="余额">
  246. </div>
  247. </div>
  248. <div class="pay-box" :class="{fixed:state.fixed}">
  249. <div class="row-bottom">
  250. <div class="row">
  251. <div class="goods-total-price-box">
  252. <p class="r rw price-num"><em></em><span>1811.00</span></p>
  253. <p class="r price-text"><span><span>5</span>件商品,</span>商品总金额:</p>
  254. </div>
  255. </div>
  256. <div class="coupons-discount-box">
  257. <p class="r rw price-num">-<em></em><span>60.00</span></p>
  258. <p class="r price-text">优惠券/积分抵扣:</p>
  259. </div>
  260. <div class="pay-price-box clearfix">
  261. <p class="r rw price"><em></em><span id="js-pay-price">1751.00</span></p>
  262. <p class="r price-text">应付:</p>
  263. </div>
  264. <span class="r btn btn-red submit-btn">提交订单</span>
  265. </div>
  266. <div class="pay-add-sign">
  267. <ul class="clearfix">
  268. <li>支持花呗</li>
  269. <li>可开发票</li>
  270. <li class="drawback">7天可退款</li>
  271. </ul>
  272. </div>
  273. </div>
  274. </div>
  275. </div>
  276. <Footer/>
  277. </div>
  278. </template>
  279. <script setup>
  280. import {reactive,watch} from "vue"
  281. import Header from "../components/Header.vue"
  282. import Footer from "../components/Footer.vue"
  283. import {useStore} from "vuex";
  284. let store = useStore()
  285. let state = reactive({
  286. course_list: [], // 购物车中的商品课程列表
  287. total_price: 0, // 勾选商品的总价格
  288. use_coupon: false, // 用户是否使用优惠
  289. discount_type: 0, // 0表示优惠券,1表示积分
  290. coupon_list:[1,2,3], // 用户拥有的可用优惠券列表
  291. select: -1, // 当前用户选中的优惠券
  292. credit: 0, // 当前用户选择抵扣的积分
  293. fixed: true, // 底部订单总价是否固定浮动
  294. pay_type: 0, // 支付方式
  295. })
  296. // 监听用户选择的支付方式
  297. watch(
  298. ()=>state.pay_type,
  299. ()=>{
  300. console.log(state.pay_type)
  301. }
  302. )
  303. // 底部订单总价信息固定浮动效果
  304. window.onscroll = ()=>{
  305. let cart_body_table = document.querySelector(".cart-body-table")
  306. let offsetY = window.scrollY
  307. let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  308. state.fixed = offsetY < maxY
  309. }
  310. </script>
  311. <style scoped>
  312. .cart-header {
  313. height: 160px;
  314. background-color: #e3e6e9;
  315. background: url("/src/assets/cart-header-bg.jpeg") repeat-x;
  316. background-size: 38%;
  317. }
  318. .cart-header .cart-header-warp {
  319. width: 1500px;
  320. height: 120px;
  321. line-height: 120px;
  322. margin-left: auto;
  323. margin-right: auto;
  324. font-size: 14px
  325. }
  326. .cart-header .cart-header-warp .myorder-history {
  327. font-weight: 200
  328. }
  329. .cart-header .left {
  330. float: left
  331. }
  332. .cart-header .right {
  333. float: right
  334. }
  335. .cart-header .cart-title {
  336. color: #4d555d;
  337. font-weight: 200;
  338. font-size: 14px
  339. }
  340. .cart-header .cart-title h1 {
  341. font-size: 32px;
  342. line-height: 115px;
  343. margin-right: 25px;
  344. color: #07111b;
  345. font-weight: 200
  346. }
  347. .cart-header .cart-title span {
  348. margin: 0 4px
  349. }
  350. .l {
  351. float: left;
  352. }
  353. .r {
  354. float: right;
  355. }
  356. .cart-body {
  357. width: 1500px;
  358. padding: 0 36px 32px;
  359. background-color: #fff;
  360. margin-top: -40px;
  361. margin-left: auto;
  362. margin-right: auto;
  363. box-shadow: 0 8px 16px 0 rgba(7,17,27, .1);
  364. border-radius: 8px;
  365. box-sizing: border-box
  366. }
  367. .cart-body .left {
  368. float: left!important
  369. }
  370. .cart-body .right {
  371. float: right!important
  372. }
  373. .cart-body .cart-body-title {
  374. min-height: 88px;
  375. line-height: 88px;
  376. border-bottom: 1px solid #b7bbbf;
  377. box-sizing: border-box
  378. }
  379. body {
  380. background: #f8fafc
  381. }
  382. .cart-body .cart-body-title span {
  383. font-size: 14px
  384. }
  385. .cart-body .cart-body-title .item-1>span,
  386. .cart-body .cart-body-title .item-2>span,
  387. .cart-body .cart-body-title .item-3>span{
  388. display: inline-block;
  389. font-size: 14px;
  390. line-height: 24px;
  391. color: #4d555d
  392. }
  393. .cart-body .cart-body-title .item-1>span {
  394. color: #93999f
  395. }
  396. .cart-body .cart-body-title .item-2>span {
  397. margin-left: 40px
  398. }
  399. .cart-body .item {
  400. height: 88px;
  401. padding: 24px 0;
  402. background: #f3f5f7;
  403. }
  404. .cart-body .cart-body-table {
  405. padding-bottom: 36px;
  406. border-bottom: 1px solid #d9dde1;
  407. }
  408. .cart-body .item>div {
  409. float: left
  410. }
  411. .cart-body .item .item-1 {
  412. padding-top: 34px;
  413. position: relative;
  414. z-index: 1
  415. }
  416. .cart-body .item:last-child>.item-1::after {
  417. display: none
  418. }
  419. .cart-body .item-1 {
  420. width: 120px
  421. }
  422. .cart-body .item-1 i {
  423. margin-left: 12px;
  424. margin-right: 8px;
  425. font-size: 24px
  426. }
  427. .cart-body .item-2 {
  428. width: 1020px;
  429. position:relative;
  430. }
  431. .cart-body .item-2>span{
  432. line-height: 88px;
  433. }
  434. .cart-body .item-2 dl {
  435. width: 464px;
  436. margin-left: 24px;
  437. padding-top: 12px
  438. }
  439. .cart-body .item-2 dl a {
  440. display: block;
  441. }
  442. .cart-body .item-2 dl.has-package {
  443. padding-top: 4px;
  444. }
  445. .cart-body .item-2 dl.has-package .package-item {
  446. display: inline-block;
  447. padding: 0 12px;
  448. margin-top: 4px;
  449. font-size: 12px;
  450. color: rgba(240,20,20, .6);
  451. line-height: 24px;
  452. background: rgba(240,20,20, .08);
  453. border-radius: 12px;
  454. cursor: pointer
  455. }
  456. .cart-body .item-2 dl.has-package .package-item:hover {
  457. color: #fff;
  458. background: rgba(240,20,20, .2)
  459. }
  460. .cart-body .item-2 dt {
  461. font-size: 16px;
  462. color: #07111b;
  463. line-height: 24px;
  464. margin-bottom: 4px
  465. }
  466. .cart-body .item-2 .img-box {
  467. display: block;
  468. margin-left: 42px;
  469. }
  470. .cart-body .item-2 .img-box img{
  471. height: 94px;
  472. }
  473. .cart-body .item-2 dd {
  474. font-size: 12px;
  475. color: #93999f;
  476. line-height: 24px;
  477. font-weight: 200
  478. }
  479. .cart-body .item-2 dd a {
  480. display: inline-block;
  481. margin-left: 12px;
  482. color: rgba(240,20,20, .4)
  483. }
  484. .cart-body .item-2 dd a:hover {
  485. color: #f01414
  486. }
  487. .cart-body .item-3 {
  488. width: 280px;
  489. margin-left: 48px;
  490. position: relative;
  491. }
  492. .cart-body .item-3 .price {
  493. display: inline-block;
  494. height: 46px;
  495. width: 96px;
  496. padding-top: 24px;
  497. padding-bottom: 24px;
  498. color: #f01414;
  499. }
  500. .cart-body .item-3 .price em,
  501. .cart-body .item-3 .price span{
  502. font-size: 18px;
  503. }
  504. .cart-body .item-3 .price .original-price em,
  505. .cart-body .item-3 .price .original-price span{
  506. font-size: 15px;
  507. color: #aaa;
  508. text-decoration: line-through;
  509. }
  510. .cart-body .cart-body-bot li {
  511. float: left
  512. }
  513. .cart-body .cart-body-bot .li-1 em,
  514. .cart-body .cart-body-bot .li-3 em {
  515. font-style: normal;
  516. color: red
  517. }
  518. .cart-body .cart-body-bot .li-2 .price {
  519. font-size: 16px;
  520. color: #f01414;
  521. line-height: 24px;
  522. font-weight: 700
  523. }
  524. .coupons-box::after{
  525. display: block;
  526. content: "";
  527. overflow: hidden;
  528. clear: both;
  529. }
  530. .coupons-box .coupon-title-box {
  531. margin: 27px 0 0 12px
  532. }
  533. .coupons-box .coupon-title-box .coupon-title {
  534. color: #07111b;
  535. font-size: 16px;
  536. line-height: 34px
  537. }
  538. .coupons-box .coupon-title-box .coupon-title svg {
  539. position: relative;
  540. width: 26px;
  541. height: 26px;
  542. top: 5px;
  543. margin-left: 12px;
  544. font-size: 24px;
  545. color: #999;
  546. cursor: pointer
  547. }
  548. .coupons-box .coupon-del-box {
  549. width: 100%;
  550. padding-top: 24px;
  551. box-sizing: border-box
  552. }
  553. .coupons-box .coupon-del-box .coupon-switch-box {
  554. margin-bottom: 16px
  555. }
  556. .coupons-box .coupon-del-box .coupon-switch-box .switch-btn {
  557. position: relative;
  558. display: inline-block;
  559. width: 138px;
  560. height: 58px;
  561. line-height: 20px;
  562. border: 1px solid #d9dde1;
  563. border-radius: 8px;
  564. padding: 18px 0;
  565. color: #1c1f21;
  566. text-align: center;
  567. font-size: 16px;
  568. margin-right: 16px;
  569. box-sizing: border-box;
  570. cursor: pointer
  571. }
  572. .coupons-box .coupon-del-box .coupon-switch-box .switch-btn em {
  573. display: none;
  574. position: absolute;
  575. bottom: 0;
  576. right: 0;
  577. width: 0;
  578. height: 0;
  579. line-height: 54px;
  580. border-left-width: 20px;
  581. border-left-style: solid;
  582. border-left-color: transparent;
  583. border-bottom-width: 20px;
  584. border-bottom-style: solid;
  585. border-bottom-color: #f01414
  586. }
  587. .coupons-box .coupon-del-box .coupon-switch-box .switch-btn em i {
  588. color: #fff;
  589. position: absolute;
  590. bottom: -20px;
  591. right: 0;
  592. font-size: 12px
  593. }
  594. .coupons-box .coupon-del-box .coupon-switch-box .switch-btn.checked {
  595. border: 2px solid #f01414
  596. }
  597. .coupons-box .coupon-del-box .coupon-switch-box .switch-btn.checked em {
  598. display: block
  599. }
  600. .coupons-box .coupon-del-box .coupon-content {
  601. position: relative;
  602. background: #f3f5f7;
  603. border-radius: 8px;
  604. padding: 24px
  605. }
  606. .coupons-box .coupon-del-box .coupon-content:before {
  607. content: "";
  608. display: block;
  609. position: absolute;
  610. top: -7px;
  611. left: 62px;
  612. border-left: 12px solid transparent;
  613. border-right: 12px solid transparent;
  614. border-bottom: 7px solid #f3f5f7
  615. }
  616. .coupons-box .coupon-del-box .coupon-content.ticket li {
  617. padding-top: 8px;
  618. box-sizing: border-box;
  619. width: 320px;
  620. background-color: #fff6f0;
  621. cursor: pointer;
  622. margin: 12px
  623. }
  624. .coupons-box .coupon-del-box .coupon-content.ticket li .more-del-box {
  625. padding: 16px 22px 24px 22px;
  626. width: 100%;
  627. box-sizing: border-box;
  628. background-repeat: no-repeat
  629. }
  630. .coupons-box .coupon-del-box .coupon-content.ticket li .price-box {
  631. height: 32px;
  632. line-height: 32px
  633. }
  634. .coupons-box .coupon-del-box .coupon-content.ticket li .price-box .price {
  635. font-size: 30px;
  636. margin-right: 4px
  637. }
  638. .coupons-box .coupon-del-box .coupon-content.ticket li .price-box .price sub {
  639. font-size: 24px;
  640. letter-spacing: -5px
  641. }
  642. .coupons-box .coupon-del-box .coupon-content.ticket li .price-box .use-inst {
  643. font-size: 12px;
  644. margin-top: 5px;
  645. }
  646. .coupons-box .coupon-del-box .coupon-content.ticket .active .price,
  647. .coupons-box .coupon-del-box .coupon-content.ticket .active .use-inst {
  648. color: #fff
  649. }
  650. .coupons-box .coupon-del-box .coupon-content.ticket .active i {
  651. position: absolute;
  652. top: 12px;
  653. right: 12px;
  654. color: #fff;
  655. font-size: 24px
  656. }
  657. .coupons-box .coupon-del-box .coupon-content.ticket .no-coupons {
  658. font-size: 14px;
  659. color: #4d555d;
  660. line-height: 14px
  661. }
  662. .coupons-box .coupon-del-box .coupon-content.code {
  663. padding-left: 38px
  664. }
  665. .coupons-box .coupon-del-box .coupon-content.code:before {
  666. left: 216px
  667. }
  668. .coupons-box .coupon-del-box .coupon-content.code .input-box {
  669. position: relative;
  670. left: -12px;
  671. margin-top: 12px
  672. }
  673. .coupons-box .coupon-del-box .coupon-content.code .input-box .convert-input {
  674. background: #fff;
  675. border: 1px solid #9199a1;
  676. width: 356px;
  677. height: 48px;
  678. border-radius: 8px;
  679. font-size: 16px;
  680. font-weight: 600;
  681. color: #07111b;
  682. letter-spacing: 2px;
  683. line-height: 24px;
  684. padding: 12px 16px;
  685. box-sizing: border-box;
  686. vertical-align: middle
  687. }
  688. .coupons-box .coupon-del-box .coupon-content.code .input-box .convert-btn {
  689. display: inline-block;
  690. width: 124px;
  691. height: 48px;
  692. line-height: 22px;
  693. font-size: 16px;
  694. color: #fff;
  695. padding: 12px;
  696. background: #f01414;
  697. border-radius: 8px;
  698. margin-left: 24px;
  699. box-sizing: border-box;
  700. text-align: center;
  701. cursor: pointer
  702. }
  703. .coupons-box .coupon-del-box .coupon-content.code .converted-box p {
  704. line-height: 24px;
  705. font-size: 16px;
  706. color: #07111b;
  707. margin-top: 10px;
  708. }
  709. .coupons-box .coupon-del-box .coupon-content.code .converted-box .c_name,
  710. .coupons-box .coupon-del-box .coupon-content.code .converted-box .code-num {
  711. padding-left: 8px
  712. }
  713. .coupons-box .coupon-del-box .coupon-content.code .converted-box .cancel-btn {
  714. background: #fff;
  715. border: 1px solid #d9dde1;
  716. line-height: 20px;
  717. padding: 2px 12px;
  718. text-align: center;
  719. border-radius: 4px;
  720. color: #f01414;
  721. font-size: 14px;
  722. margin-left: 16px;
  723. cursor: pointer
  724. }
  725. .coupons-box .coupon-del-box .coupon-content.code .converted-box .course-title {
  726. font-size: 14px;
  727. color: #07111b;
  728. font-weight: 600;
  729. margin-top: 12px
  730. }
  731. .coupons-box .coupon-del-box .coupon-content.code .converted-box .course-title .discount-cash {
  732. margin-left: 12px;
  733. color: #f01414
  734. }
  735. .coupons-box .coupon-del-box .coupon-content.code .error-msg {
  736. font-size: 14px;
  737. color: #f01414;
  738. margin-top: 8px;
  739. line-height: 20px;
  740. height: 20px
  741. }
  742. .coupons-box .coupon-del-box .coupon-content.code .tip {
  743. font-size: 14px;
  744. color: #93999f;
  745. margin-top: 8px;
  746. line-height: 20px
  747. }
  748. .coupons-box .content-box ul {
  749. width: 100%
  750. }
  751. .coupons-box .content-box .nouse-box::after,
  752. .coupons-box .content-box .overdue-box::after,
  753. .coupons-box .content-box .use-box::after {
  754. display: block;
  755. content: "";
  756. overflow: hidden;
  757. clear: both;
  758. }
  759. .coupons-box .content-box .nouse-box li,
  760. .coupons-box .content-box .overdue-box li,
  761. .coupons-box .content-box .use-box li {
  762. position: relative;
  763. padding: 24px 32px;
  764. margin-right: 16px;
  765. margin-bottom: 16px;
  766. width: 320px;
  767. height: 144px;
  768. border-radius: 8px;
  769. box-sizing: border-box;
  770. background-color: #fff;
  771. box-shadow: 0 8px 16px 0 rgba(7,17,27, .2);
  772. background-repeat: no-repeat;
  773. background-size: 320px 144px;
  774. }
  775. .coupons-box .content-box .nouse-box li.select{
  776. background-color: orangered;
  777. }
  778. .coupons-box .content-box .nouse-box li .detail-box,
  779. .coupons-box .content-box .overdue-box li .detail-box,
  780. .coupons-box .content-box .use-box li .detail-box {
  781. width: 100%;
  782. height: 100%
  783. }
  784. .coupons-box .content-box .nouse-box li .detail-box .price-box,
  785. .coupons-box .content-box .overdue-box li .detail-box .price-box,
  786. .coupons-box .content-box .use-box li .detail-box .price-box {
  787. margin-bottom: 8px;
  788. height: 40px;
  789. color: #93999f;
  790. line-height: 40px;
  791. font-weight: 700
  792. }
  793. .coupons-box .content-box .nouse-box li .detail-box .price-box .coupon-price,
  794. .coupons-box .content-box .overdue-box li .detail-box .price-box .coupon-price,
  795. .coupons-box .content-box .use-box li .detail-box .price-box .coupon-price {
  796. margin-right: 12px;
  797. font-size: 36px;
  798. margin-top: 5px;
  799. }
  800. .coupons-box .content-box .nouse-box li .detail-box .price-box .use-inst,
  801. .coupons-box .content-box .overdue-box li .detail-box .price-box .use-inst,
  802. .coupons-box .content-box .use-box li .detail-box .price-box .use-inst {
  803. font-size: 14px
  804. }
  805. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box,
  806. .coupons-box .content-box .overdue-box li .detail-box .use-detail-box,
  807. .coupons-box .content-box .use-box li .detail-box .use-detail-box {
  808. font-size: 12px;
  809. color: #93999f;
  810. line-height: 24px
  811. }
  812. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box,
  813. .coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box,
  814. .coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box {
  815. position: relative
  816. }
  817. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box i,
  818. .coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box i,
  819. .coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box i {
  820. position: relative;
  821. top: 3px;
  822. left: 0;
  823. font-size: 16px;
  824. color: #93999f;
  825. line-height: 24px;
  826. cursor: pointer
  827. }
  828. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box .use-course a,
  829. .coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box .use-course a,
  830. .coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box .use-course a {
  831. padding: 16px 0;
  832. width: 100%;
  833. display: block;
  834. font-size: 12px;
  835. color: #4d555d;
  836. line-height: 20px;
  837. border-bottom: 1px solid #d9dde1;
  838. box-sizing: border-box
  839. }
  840. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box .use-course a:hover,
  841. .coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box .use-course a:hover,
  842. .coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box .use-course a:hover {
  843. color: #07111b
  844. }
  845. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box .use-course a:last-child,
  846. .coupons-box .content-box .overdue-box li .detail-box .use-detail-box .use-ajust-box .use-course a:last-child,
  847. .coupons-box .content-box .use-box li .detail-box .use-detail-box .use-ajust-box .use-course a:last-child {
  848. border-bottom: none
  849. }
  850. .coupons-box .content-box li {
  851. background-image: url(/src/assets/coupons_bg.png)
  852. }
  853. .coupons-box .content-box .nouse-box li .detail-box .price-box .coupon-price {
  854. color: #f01414
  855. }
  856. .coupons-box .content-box .nouse-box li .detail-box .price-box .use-inst {
  857. color: #f01414
  858. }
  859. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box {
  860. color: #07111b
  861. }
  862. .coupons-box .content-box .nouse-box li .detail-box .use-detail-box .use-ajust-box i {
  863. color: #4d555d
  864. }
  865. .coupons-box .content-box .nouse-box li.wait-use {
  866. background-image: url(/src/assets/coupon_start_bg.png)
  867. }
  868. .coupons-box .content-box .use-box li {
  869. background-image: url(/src/assets/coupons_used_bg.png)
  870. }
  871. .coupons-box .content-box .use-box li.useing {
  872. background-image: url(/src/assets/coupon_useing_bg.png)
  873. }
  874. .coupons-box .content-box .overdue-box li {
  875. background-image: url(/src/assets/coupons_overdue.png)
  876. }
  877. .tip-box ol {
  878. margin-top: 16px;
  879. width: 100%;
  880. list-style: decimal;
  881. margin-left: 14px;
  882. box-sizing: border-box
  883. }
  884. .tip-box ol li {
  885. font-size: 12px
  886. }
  887. .pay-box {
  888. margin-top: 36px;
  889. position: relative
  890. }
  891. .pay-box::after,
  892. .goods-total-price-box::after,
  893. .package-discount-box::after,
  894. .pay-price-box::after,
  895. .coupons-discount-box::after{
  896. display: block;
  897. content: "";
  898. clear: both;
  899. overflow: hidden;
  900. }
  901. .pay-box .rw {
  902. width: 140px;
  903. box-sizing: border-box;
  904. text-align: right
  905. }
  906. .pay-box .bargain-discount-box,.pay-box .coupons-discount-box,.pay-box .goods-total-price-box,.pay-box .package-discount-box,.pay-box .redpackage-discount-box,.pay-box .student-discount-box {
  907. margin-bottom: 12px;
  908. line-height: 26px
  909. }
  910. .pay-box .bargain-discount-box .price-num,.pay-box .coupons-discount-box .price-num,.pay-box .goods-total-price-box .price-num,.pay-box .package-discount-box .price-num,.pay-box .redpackage-discount-box .price-num,.pay-box .student-discount-box .price-num {
  911. position: relative;
  912. font-size: 14px;
  913. color: #07111b
  914. }
  915. .pay-box .bargain-discount-box .price-text,.pay-box .coupons-discount-box .price-text,.pay-box .goods-total-price-box .price-text,.pay-box .package-discount-box .price-text,.pay-box .redpackage-discount-box .price-text,.pay-box .student-discount-box .price-text {
  916. text-align: right;
  917. font-size: 14px;
  918. color: #07111b
  919. }
  920. .pay-box .bargain-discount-box .price-text span,.pay-box .coupons-discount-box .price-text span,.pay-box .goods-total-price-box .price-text span,.pay-box .package-discount-box .price-text span,.pay-box .redpackage-discount-box .price-text span,.pay-box .student-discount-box .price-text span {
  921. margin-left: 4px;
  922. margin-right: 4px
  923. }
  924. .pay-box .pay-add-sign {
  925. text-align: right;
  926. position: absolute;
  927. top: -10px
  928. }
  929. .pay-box .pay-add-sign li {
  930. float: left;
  931. padding: 0 12px;
  932. height: 26px;
  933. line-height: 26px;
  934. border: 1px solid #f01414;
  935. border-radius: 18px;
  936. font-size: 12px;
  937. color: #f01414;
  938. margin-right: 15px
  939. }
  940. .pay-box .pay-add-sign li.drawback {
  941. position: relative
  942. }
  943. .pay-box .pay-add-sign li.drawback .imv2-ques {
  944. position: absolute;
  945. top: -4px;
  946. right: -2px;
  947. background: #fff;
  948. color: #d7dbdf;
  949. font-size: 14px;
  950. display: inline-block;
  951. width: 14px;
  952. height: 14px;
  953. cursor: pointer
  954. }
  955. .pay-box .pay-add-sign li.drawback .imv2-ques:hover {
  956. color: #f20d0d
  957. }
  958. .pay-box .pay-add-sign a.checkbackbtn {
  959. display: none;
  960. color: #fff;
  961. font-size: 12px;
  962. text-align: center;
  963. border-radius: 8px;
  964. vertical-align: top;
  965. position: absolute;
  966. left: 100%;
  967. top: -12px;
  968. background: rgba(28,31,33,.25);
  969. width: 100px;
  970. height: 26px;
  971. line-height: 26px;
  972. margin-left: 8px
  973. }
  974. .pay-box .pay-add-sign a.checkbackbtn i.arrow {
  975. width: 0;
  976. height: 0;
  977. border-top: 5px solid transparent;
  978. border-right: 5px solid;
  979. border-bottom: 5px solid transparent;
  980. position: absolute;
  981. left: -5px;
  982. top: 8px;
  983. border-right-color: rgba(28,31,33,.25)
  984. }
  985. .pay-box .pay-price-box {
  986. color: #07111b
  987. }
  988. .pay-box .pay-price-box .price {
  989. position: relative;
  990. color: #f01414;
  991. font-size: 24px;
  992. font-weight: 700;
  993. line-height: 36px;
  994. height: 36px;
  995. }
  996. .pay-box .pay-price-box .price-text{
  997. line-height: 36px;
  998. height: 36px;
  999. }
  1000. .pay-box .pay-price-box .price span {
  1001. float: none;
  1002. font-weight: 700
  1003. }
  1004. .pay-box .pay-account {
  1005. font-size: 12px;
  1006. color: #93999f;
  1007. line-height: 24px;
  1008. margin-bottom: 20px;
  1009. margin-top: 15px
  1010. }
  1011. .pay-box .submit-btn {
  1012. padding: 0;
  1013. width: 140px;
  1014. height: 40px;
  1015. margin-top: 12px;
  1016. text-align: center;
  1017. font-size: 14px;
  1018. line-height: 40px;
  1019. border-radius: 24px
  1020. }
  1021. .pay-box .disabled {
  1022. background: #ccc;
  1023. cursor: not-allowed;
  1024. border: none
  1025. }
  1026. .pay-box .presale-wrap {
  1027. text-align: right
  1028. }
  1029. .pay-box .presale-wrap .submit-btn {
  1030. margin-top: 24px
  1031. }
  1032. .pay-box .presale-box {
  1033. display: inline-block;
  1034. font-size: 0;
  1035. text-align: left
  1036. }
  1037. .pay-box .presale-box .step {
  1038. width: 213px;
  1039. padding-bottom: 10px;
  1040. position: relative
  1041. }
  1042. .pay-box .presale-box .step .title {
  1043. font-size: 14px;
  1044. color: #07111b;
  1045. line-height: 26px
  1046. }
  1047. .pay-box .presale-box .step .title .price {
  1048. color: #93999f;
  1049. float: right
  1050. }
  1051. .pay-box .presale-box .step .title .price.active {
  1052. color: #f01414
  1053. }
  1054. .pay-box .presale-box .step .desc {
  1055. font-size: 12px;
  1056. color: #93999f;
  1057. line-height: 16px
  1058. }
  1059. .pay-box .presale-box .step:nth-child(3) .price {
  1060. color: #f01414;
  1061. font-size: 24px;
  1062. font-weight: 700
  1063. }
  1064. .pay-box .presale-box .step .step-line {
  1065. position: absolute;
  1066. top: 8px;
  1067. left: -16px;
  1068. width: 9px;
  1069. display: flex;
  1070. flex-direction: column;
  1071. align-items: center
  1072. }
  1073. .pay-box .presale-box .step .step-line .circle {
  1074. width: 9px;
  1075. height: 9px;
  1076. border-radius: 50%;
  1077. background: rgba(147,153,159,.3)
  1078. }
  1079. .pay-box .presale-box .step .step-line .circle.active {
  1080. background: #f01414
  1081. }
  1082. .pay-box .presale-box .step .step-line .line {
  1083. height: 43px;
  1084. border-left: 1px dashed rgba(147,153,159,.3)
  1085. }
  1086. .pay-box .presale-box .step .step-line .line.short {
  1087. height: 27px
  1088. }
  1089. .pay-box.fixed {
  1090. position: fixed;
  1091. bottom: 0;
  1092. left: 0;
  1093. width: 100%;
  1094. height: 80px;
  1095. line-height: 80px;
  1096. background-color: #fff;
  1097. z-index: 300;
  1098. box-shadow: 10px -2px 12px rgba(7,17,27,.2);
  1099. padding-top: 10px;
  1100. }
  1101. .pay-box.fixed .row-bottom {
  1102. max-width: 1500px;
  1103. position: relative;
  1104. margin: 0 auto;
  1105. }
  1106. .pay-box.fixed .row-bottom .row {
  1107. float: left
  1108. }
  1109. .pay-box.fixed .row-bottom .bargain-discount-box,.pay-box.fixed .row-bottom .coupons-discount-box,.pay-box.fixed .row-bottom .js-total-hide,.pay-box.fixed .row-bottom .package-discount-box {
  1110. display: none
  1111. }
  1112. .pay-box.fixed .bargain-discount-box,.pay-box.fixed .coupons-discount-box,.pay-box.fixed .goods-total-price-box,.pay-box.fixed .package-discount-box,.pay-box.fixed .pay-add-sign,.pay-box.fixed .pay-price-box,.pay-box.fixed .redpackage-discount-box {
  1113. float: left;
  1114. margin-bottom: 0
  1115. }
  1116. .pay-box.fixed .coupons-discount-box,.pay-box.fixed .package-discount-box,.pay-box.fixed .redpackage-discount-box {
  1117. margin-left: 20px
  1118. }
  1119. .pay-box.fixed .goods-total-price-box {
  1120. width: auto
  1121. }
  1122. .pay-box.fixed .rw {
  1123. text-align: left;
  1124. width: auto
  1125. }
  1126. .pay-box.fixed .price,.pay-box.fixed .price-num,.pay-box.fixed .price-text {
  1127. line-height: 80px
  1128. }
  1129. .pay-box.fixed .pay-add-sign {
  1130. position: static!important;
  1131. margin-left: 20px
  1132. }
  1133. .pay-box.fixed .pay-add-sign li {
  1134. float: left;
  1135. padding: 0 12px;
  1136. height: 26px;
  1137. line-height: 26px;
  1138. border: 1px solid #f01414;
  1139. border-radius: 18px;
  1140. font-size: 12px;
  1141. color: #f01414;
  1142. margin: 27px 20px 27px 0
  1143. }
  1144. .pay-box.fixed .pay-price-box {
  1145. width: auto;
  1146. margin-left: 20px
  1147. }
  1148. .pay-box.fixed .submit-btn {
  1149. margin-top: 16px;
  1150. width: 148px;
  1151. height: 48px;
  1152. line-height: 48px;
  1153. font-size: 16px;
  1154. border-radius: 24px
  1155. }
  1156. .pay-box.fixed .presale-wrap {
  1157. float: left;
  1158. text-align: left
  1159. }
  1160. .pay-box.fixed .presale-wrap .presale-box {
  1161. height: 80px;
  1162. display: flex;
  1163. align-items: center
  1164. }
  1165. .pay-box.fixed .presale-wrap .presale-box .step {
  1166. padding-right: 38px;
  1167. padding-bottom: 0;
  1168. width: auto;
  1169. min-width: 118px;
  1170. height: 45px
  1171. }
  1172. .pay-box.fixed .presale-wrap .presale-box .step:nth-child(3) {
  1173. height: auto
  1174. }
  1175. .pay-box.fixed .presale-wrap .presale-box .step .title {
  1176. float: none;
  1177. background: #fff
  1178. }
  1179. .pay-box.fixed .presale-wrap .presale-box .step .title .price {
  1180. line-height: 26px;
  1181. float: none
  1182. }
  1183. .pay-box.fixed .presale-wrap .presale-box .step .step-line {
  1184. flex-direction: row;
  1185. width: 100%;
  1186. left: -14px
  1187. }
  1188. .pay-box.fixed .presale-wrap .presale-box .step .step-line .line {
  1189. border-left: none;
  1190. border-top: 1px dashed rgba(147,153,159,.3);
  1191. width: 30px;
  1192. height: 1px;
  1193. position: absolute;
  1194. right: 5px
  1195. }
  1196. .pay-box.fixed .presale-wrap .presale-box .step .step-line .circle:nth-child(3) {
  1197. position: absolute;
  1198. right: -10px
  1199. }
  1200. .btn {
  1201. position: relative;
  1202. display: inline-block;
  1203. margin-bottom: 0;
  1204. text-align: center;
  1205. vertical-align: middle;
  1206. touch-action: manipulation;
  1207. text-decoration: none;
  1208. box-sizing: border-box;
  1209. background-image: none;
  1210. -webkit-appearance: none;
  1211. white-space: nowrap;
  1212. outline: none;
  1213. -webkit-user-select: none;
  1214. -moz-user-select: none;
  1215. -ms-user-select: none;
  1216. user-select: none;
  1217. border-style: solid;
  1218. border-width: 1px;
  1219. cursor: pointer;
  1220. transition: all .3s;
  1221. color: #545c63;
  1222. background-color: transparent;
  1223. border-color: #9199a1;
  1224. opacity: 1;
  1225. padding: 7px 16px;
  1226. font-size: 14px;
  1227. line-height: 1.42857143;
  1228. border-radius: 18px;
  1229. }
  1230. .btn-red {
  1231. border-style: solid;
  1232. border-width: 1px;
  1233. cursor: pointer;
  1234. -moz-transition: all .3s;
  1235. transition: all .3s;
  1236. color: #fff;
  1237. background-color: #f20d0d;
  1238. border-color: #f20d0d;
  1239. opacity: 1;
  1240. }
  1241. .btn-red:hover {
  1242. color: #fff;
  1243. border-color: #c20a0a;
  1244. background: #c20a0a;
  1245. opacity: 1;
  1246. }
  1247. .pay-type {
  1248. margin-top: 28px;
  1249. margin-left: 12px;
  1250. }
  1251. .pay-type .title {
  1252. margin-top: 28px;
  1253. }
  1254. .pay-type .list {
  1255. padding-top: 20px;
  1256. }
  1257. .pay-type .list img {
  1258. margin-right: 10px;
  1259. }
  1260. </style>

提交代码版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature: 客户端展示下单结算页面"
git push

展示购物车勾选商品列表

服务端实现购物车勾选商品列表的api接口

cart/views,视图,代码:

from rest_framework.views import APIView
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status
from django_redis import get_redis_connection
from courses.models import Course


# .... 中间代码省略

class CartOrderAPIView(APIView):
    """购物车确认下单接口"""
    # 保证用户必须是登录状态才能调用当前视图
    permission_classes = [IsAuthenticated]

    def get(self,request):
        """获取勾选商品列表"""
        # 查询购物车中的商品课程ID列表
        user_id = request.user.id
        redis = get_redis_connection("cart")
        cart_hash = redis.hgetall(f"cart_{user_id}")
        """
        cart_hash = {
            # b'商品课程ID': b'勾选状态', 
            b'2': b'1', 
            b'4': b'1', 
            b'5': b'1'
        }
        """
        if len(cart_hash) < 1:
            return Response({"errmsg": "购物车没有任何商品。"}, status=status.HTTP_204_NO_CONTENT)

        # 把redis中的购物车勾选课程ID信息转换成普通列表
        cart_list = [int(course_id.decode()) for course_id, selected in cart_hash.items() if selected == b'1']

        course_list = Course.objects.filter(pk__in=cart_list, is_deleted=False, is_show=True).all()

        # 把course_list进行遍历,提取课程中的信息组成列表
        data = []
        for course in course_list:
            data.append({
                "id": course.id,
                "name": course.name,
                "course_cover": course.course_cover.url,
                "price": float(course.price),
                "discount": course.discount,
                "course_type": course.get_course_type_display(),
            })

        # 返回客户端
        return Response({"errmsg": "ok!", "cart": data})

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

from django.urls import path
from . import views
urlpatterns = [
    path("", views.CartAPIView.as_view()),
    path("order/", views.CartOrderAPIView.as_view()),
]

客户端获取购物车勾选商品的数据

api/cart.js,代码:

import http from "../utils/http";
import {reactive, ref} from "vue"

const cart = reactive({
    // ... 中间代码省略
    select_course_list: [], // 购物车中被勾选的商品磕碜列表
    // ... 中间代码省略
    get_select_course(token){
        // 获取购物车中被勾选的商品列表
        return http.get("/cart/order/", {
            headers:{
                Authorization: "jwt " + token,
            }
        })
    }
})

export default cart;

api/order.js,代码:

import http from "../utils/http";
import {reactive} from "vue";

const order = reactive({
  total_price: 0,      // 勾选商品的总价格
  use_coupon: false,   // 用户是否使用优惠
  discount_type: 0,    // 0表示优惠券,1表示积分
  coupon_list:[1,2,3], // 用户拥有的可用优惠券列表
  select: -1,          // 当前用户选中的优惠券下标,-1表示没有选择
  credit: 0,           // 当前用户选择抵扣的积分,0表示没有使用积分
  fixed: true,         // 底部订单总价是否固定浮动
  pay_type: 0,         // 支付方式
})

export default order;

views/Order.vue,代码:

<template>
  <div class="cart">
    <Header/>
    <div class="cart-main">
      <div class="cart-header">
        <div class="cart-header-warp">
          <div class="cart-title left">
            <h1 class="left">确认订单</h1>
          </div>
          <div class="right">
            <div class="">
              <span class="left"><router-link class="myorder-history" to="/cart">返回购物车</router-link></span>
            </div>
          </div>
        </div>
      </div>
      <div class="cart-body" id="cartBody">
        <div class="cart-body-title"><p class="item-1 l">课程信息</p></div>
        <div class="cart-body-table">
          <div class="item" v-for="course_info in cart.select_course_list">
              <div class="item-2">
                  <router-link :to="`/project/${course_info.id}`" class="img-box l"><img :src="course_info.course_cover"></router-link>
                  <dl class="l has-package">
                    <dt>【{{course_info.course_type}}】{{course_info.name}} </dt>
                    <p class="package-item" v-if="course_info.discount.type">{{course_info.discount.type}}</p>
                  </dl>
              </div>
              <div class="item-3">
                  <div class="price">
                      <p class="discount-price" v-if="course_info.discount.price>=0"><em>¥</em><span>{{course_info.discount.price.toFixed(2)}}</span></p>
                      <p :class="{'original-price': course_info.discount.price>=0}"><em>¥</em><span>{{course_info.price.toFixed(2)}}</span></p>
                  </div>
              </div>
          </div>
        </div>
        <div class="coupons-box">
          <div class="coupon-title-box">
            <p class="coupon-title">
              使用优惠券/积分
                <span v-if="order.use_coupon" @click="order.use_coupon=!order.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="M831.872 340.864 512 652.672 192.128 340.864a30.592 30.592 0 0 0-42.752 0 29.12 29.12 0 0 0 0 41.6L489.664 714.24a32 32 0 0 0 44.672 0l340.288-331.712a29.12 29.12 0 0 0 0-41.728 30.592 30.592 0 0 0-42.752 0z"></path></svg></span>
                <span v-else @click="order.use_coupon=!order.use_coupon"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" data-v-394d1fd8=""><path fill="currentColor" d="m488.832 344.32-339.84 356.672a32 32 0 0 0 0 44.16l.384.384a29.44 29.44 0 0 0 42.688 0l320-335.872 319.872 335.872a29.44 29.44 0 0 0 42.688 0l.384-.384a32 32 0 0 0 0-44.16L535.168 344.32a32 32 0 0 0-46.336 0z"></path></svg></span>
<!--                <i :class="order.use_coupon?'el-icon-arrow-up':'el-icon-arrow-down'" @click="order.use_coupon=!order.use_coupon"></i>-->
            </p>
          </div>
          <transition name="el-zoom-in-top">
          <div class="coupon-del-box" v-if="order.use_coupon">
            <div class="coupon-switch-box">
              <div class="switch-btn ticket" :class="{'checked': order.discount_type===0}" @click="order.discount_type=0">优惠券 (4)<em><i class="imv2-check"></i></em></div>
              <div class="switch-btn code" :class="{'checked': order.discount_type===1}" @click="order.discount_type=1">积分<em><i class="imv2-check"></i></em></div>
            </div>
            <div class="coupon-content ticket" v-if="order.discount_type===0">
              <p class="no-coupons" v-if="order.coupon_list.length<1">暂无可用优惠券</p>
              <div class="coupons-box" v-else>
               <div class="content-box">
                <ul class="nouse-box">
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l select">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l wait-use">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l wait-use">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
                <ul class="use-box">
                 <li class="l useing">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
                <ul class="overdue-box">
                 <li class="l useing">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥100 </p>
                    <p class="use-inst l">满499可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                 <li class="l">
                  <div class="detail-box more-del-box">
                   <div class="price-box">
                    <p class="coupon-price l"> ¥248 </p>
                    <p class="use-inst l">满999可用</p>
                   </div>
                   <div class="use-detail-box">
                    <div class="use-ajust-box">适用于:全部实战课程</div>
                    <div class="use-ajust-box">有效期:2021.06.01-2021.06.18</div>
                   </div>
                  </div>
                 </li>
                </ul>
               </div>
              </div>
            </div>
            <div class="coupon-content code" v-else>
                <div class="input-box">
                  <el-input-number placeholder="10积分=1元" v-model="order.credit" :step="1" :min="0" :max="1000"></el-input-number>
                  <a class="convert-btn">兑换</a>
                </div>
                <div class="converted-box">
                  <p>使用积分:<span class="code-num">200</span></p>
                  <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
                    <span class="discount-cash">100积分抵扣:<em>10</em>元</span>
                  </p>
                  <p class="course-title">课程:<span class="c_name">3天JavaScript入门</span>
                    <span class="discount-cash">100积分抵扣:<em>10</em>元</span>
                  </p>
                </div>
                <p class="error-msg">本次订单最多可以使用1000积分,您当前拥有200积分。(10积分=1元)</p>
                <p class="tip">说明:每笔订单只能使用一次积分,并只有在部分允许使用积分兑换的课程中才能使用。</p>
              </div>
          </div>
          </transition>
        </div>
        <div class="pay-type">
          <p class="title">选择支付方式</p>
          <div class="list">
            <img :src="order.pay_type==0?'/src/assets/alipay2.png':'/src/assets/alipay1.png'" @click="order.pay_type=0" alt="支付宝">
            <img :src="order.pay_type==1?'/src/assets/wechat2.png':'/src/assets/wechat1.png'" @click="order.pay_type=1" alt="微信">
            <img :src="order.pay_type==2?'/src/assets/yue2.png':'/src/assets/yue1.png'"  @click="order.pay_type=2" alt="余额">
          </div>
        </div>
        <div class="pay-box" :class="{fixed:order.fixed}">
                  <div class="row-bottom">
            <div class="row">
              <div class="goods-total-price-box">
                <p class="r rw price-num"><em>¥</em><span>1811.00</span></p>
                <p class="r price-text"><span>共<span>5</span>件商品,</span>商品总金额:</p>
              </div>
            </div>
            <div class="coupons-discount-box">
              <p class="r rw price-num">-<em>¥</em><span>60.00</span></p>
              <p class="r price-text">优惠券/积分抵扣:</p>
            </div>
            <div class="pay-price-box clearfix">
              <p class="r rw price"><em>¥</em><span id="js-pay-price">1751.00</span></p>
              <p class="r price-text">应付:</p>
            </div>
            <span class="r btn btn-red submit-btn">提交订单</span>
                    </div>
          <div class="pay-add-sign">
            <ul class="clearfix">
              <li>支持花呗</li>
              <li>可开发票</li>
              <li class="drawback">7天可退款</li>
            </ul>
          </div>
          </div>
      </div>
    </div>
    <Footer/>
  </div>
</template>
<script setup>
import {reactive,watch} from "vue"
import Header from "../components/Header.vue"
import Footer from "../components/Footer.vue"
import {useStore} from "vuex";
import cart from "../api/cart"
import order from "../api/order";

// let store = useStore()

const get_select_course = ()=>{
    // 获取购物车中的勾选商品列表
    let token = sessionStorage.token || localStorage.token;
    cart.get_select_course(token).then(response=>{
        cart.select_course_list = response.data.cart
    })
}

get_select_course();


// 监听用户选择的支付方式
watch(
    ()=>order.pay_type,
    ()=>{
      console.log(order.pay_type)
    }
)

// 底部订单总价信息固定浮动效果
window.onscroll = ()=>{
  let cart_body_table = document.querySelector(".cart-body-table")
  let offsetY = window.scrollY
  let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  order.fixed = offsetY < maxY
}
</script>

提交代码版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature: 确认下单页面中展示购物车勾选商品列表"
git push

订单生成

创建订单子应用

完成了勾选商品列表展示以后,因为优惠券或积分属于增值业务,所以可以先把优惠券功能和积分功能延后处理,先完成主流程中的订单生成功能。同时,为了方便以后项目的代码管理和维护,我们再次创建子应用orders来完成接下来的订单功能。

# 确认前面功能已经开发完整,review代码结束,向公司申请合并分支,开发合并分支
git checkout master
git merge feature/cart
# 查看线上本地所有的分支列表,可以看到本地的feature/user分支已经删除,但是线上的依然存在。
git branch --all
git branch -d feature/cart
# 本地删除了分支以后,线上分支也要同步一下。
git push origin --delete feature/cart
# 因为属于一个较大功能的开发合并,往往项目中都会打一个标签
git tag v0.0.4
# 提交标签版本
git push --tag
# git push origin v0.0.4

# 后续的功能属于购物流程里面的订单生成部分了
git checkout -b feature/order

# 创建订单子应用
cd luffycityapi/luffycityapi/apps
python ../../manage.py startapp orders

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

INSTALLED_APPS = [
    # 子应用
    。。。

    'orders',
]

子路由,orders/urls.py,代码:

from django.urls import path
from . import views
urlpatterns = [

]

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

    path("orders/", include("orders.urls")),

订单模型

订单相关的模型分析:

订单基本信息:订单ID,支付方式,订单状态,支付时间,订单总价格,实付价格,订单标题,订单号,用户ID等等
订单项详情(订单与商品的关系):商品ID,商品原价、商品实价,优惠方式,订单ID等等

用户课程(用户与课程的关系):用户ID,课程ID,学习总时长等等
用户学习课程的进度跟踪记录(用户与课时的关系):用户ID,课时ID,课程ID,章节ID,学习进度(视频进度),学习时间等等

优惠券:优惠券标题、优惠券面额、优惠券优惠方式、优惠类型、领取方式(用户领取,系统发放)、起用时间、过期时间等等
用户的优惠券(用户与优惠券的关系):  用户ID,优惠券ID,领取时间等等。(我们采用redis来记录)
优惠券的使用记录(用户的优惠券与订单的关系):用户ID,优惠券ID、使用状态、订单ID等等。
积分流水:操作方式、积分面值、用户ID、订单ID等等。
余额流水:操作方式、货币面值、用户ID、订单ID等等。

为什么有订单号?

原因是支付平台需要记录每一个商家的资金流水,所以需要我们这边提供一个足够复杂的流水号和支付平台保持一致。
所以订单号是支付平台那边强制要求在支付时提供给平台的。用于对账。

`orders/models.py,订单模型,代码:

from models import BaseModel,models
from users.models import User
from courses.models import Course
# Create your models here.


class Order(BaseModel):
    """订单基本信息模型"""
    status_choices = (
        # 模型对象.<字段名>                   获取元组的第一个成员
        # 模型对象.get_<字段名>_display()     获取元组的第二个成员
        (0, '未支付'),
        (1, '已支付'),
        (2, '已取消'),
        (3, '超时取消'),
    )
    pay_choices = (
        (0, '支付宝'),
        (1, '微信'),
        (2, '余额'),
    )

    total_price = models.DecimalField(default=0, max_digits=10, decimal_places=2, verbose_name="订单总价")
    real_price = models.DecimalField(default=0, max_digits=10, decimal_places=2, verbose_name="实付金额")
    order_number = models.CharField(max_length=64, verbose_name="订单号")
    order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="订单状态")
    pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
    order_desc = models.TextField(null=True, blank=True, max_length=500, verbose_name="订单描述")
    pay_time = models.DateTimeField(null=True, blank=True, verbose_name="支付时间")
    user = models.ForeignKey(User, related_name='user_orders', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下单用户")

    class Meta:
        db_table = "ly_order"
        verbose_name = "订单记录"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s,总价: %s,实付: %s" % (self.name, self.total_price, self.real_price)


class OrderDetail(BaseModel):
    """
    订单详情
    """
    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=10, decimal_places=2, verbose_name="课程原价")
    real_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="课程实价")
    discount_name = models.CharField(max_length=120,default="",verbose_name="优惠类型")

    class Meta:
        db_table = "ly_order_course"
        verbose_name = "订单详情"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s" % self.course.name

数据迁移:

cd ../../
python manage.py makemigrations
python manage.py migrate

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:订单子应用创建以及订单信息和订单项模型的创建"
git push --set-upstream origin feature/order

把订单子应用相关的模型注册到admin管理站点

orders/admin.py,代码:

from django.contrib import admin
from .models import Order, OrderDetail


# class OrderDetailInLine(admin.StackedInline):
class OrderDetailInLine(admin.TabularInline):
    """订单项的内嵌类"""
    model = OrderDetail
    fields = ["course", "price", "real_price", "discount_name"]
    # readonly_fields = ["discount_name"]


class OrderModelAdmin(admin.ModelAdmin):
    """订单信息的模型管理器"""
    list_display = ["id","order_number","user","total_price","total_price","order_status"]
    inlines = [OrderDetailInLine, ]


admin.site.register(Order, OrderModelAdmin)

orders/apps.py,代码:

from django.apps import AppConfig

class OrdersConfig(AppConfig):
    default_auto_field = 'django.db.models.BigAutoField'
    name = 'orders'
    verbose_name = "订单管理"
    verbose_name_plural = verbose_name

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:把订单子应用相关的模型注册到admin管理站点"
git push

服务端提供创建订单的api接口

orders/views.py,代码:

from rest_framework.generics import CreateAPIView
from .models import Order
from .serializers import OrderModelSerializer
from rest_framework.permissions import IsAuthenticated


# Create your views here.
class OrderCreateAPIView(CreateAPIView):
    """创建订单"""
    permission_classes = [IsAuthenticated]
    queryset = Order.objects.all()
    serializer_class = OrderModelSerializer

子路由,orders/urls.py,代码:

from django.urls import path
from . import views

urlpatterns = [
    path("", views.OrderCreateAPIView.as_view()),
]

序列化器,orders/serializers.py,代码:

from datetime import datetime
from rest_framework import serializers
from django_redis import get_redis_connection
from .models import Order, OrderDetail, Course


class OrderModelSerializer(serializers.ModelSerializer):
    pay_link = serializers.CharField(read_only=True)

    class Meta:
        model = Order
        fields = ["pay_type", "id", "order_number", "pay_link"]
        read_only_fields = ["id", "order_number"]
        extra_kwargs = {
            "pay_type": {"write_only": True},
        }

    def create(self, validated_data):
        """创建订单"""
        redis = get_redis_connection("cart")
        user_id = self.context["request"].user.id  # 1

        # 创建订单记录
        order = Order.objects.create(
            name="购买课程",  # 订单标题
            user_id=user_id,  # 当前下单的用户ID
            # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
            order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
            pay_type=validated_data.get("pay_type"),  # 支付方式
        )

        # 记录本次下单的商品列表
        cart_hash = redis.hgetall(f"cart_{user_id}")
        if len(cart_hash) < 1:
            raise serializers.ValidationError(detail="购物车没有要下单的商品")

        # 提取购物车中所有勾选状态为b'1'的商品
        course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']

        # 添加订单与课程的关系
        course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
        detail_list = []
        total_price = 0 # 本次订单的总价格
        real_price = 0  # 本次订单的实付总价

        for course in course_list:
            discount_price = float(course.discount.get("price", 0)) # 获取课程原价
            discount_name = course.discount.get("type", "")
            detail_list.append(OrderDetail(
                order=order,
                course=course,
                name=course.name,
                price=course.price,
                real_price=discount_price,
                discount_name=discount_name,
            ))

            # 统计订单的总价和实付总价
            total_price += float(course.price)
            real_price += discount_price if discount_price > 0 else float(course.price)

        # 一次性批量添加本次下单的商品记录
        OrderDetail.objects.bulk_create(detail_list)

        # 保存订单的总价格和实付价格
        order.total_price = total_price
        order.real_price = real_price
        order.save()

        # todo 支付链接地址[后面实现支付功能的时候,再做]
        order.pay_link = ""
        return order

生成订单时,在序列化器中要接收客户端用户的user_id

用户ID在序列化器中接收到视图中的数据,那么在序列化器初始化的时候,其实有3个参数可以填写:
   1. instance 模型对象,数据模型,
   2. data     字典,客户端提交数据,
   3. context  字典,额外参数[执行上下文],如果要自定义参数,可以直接通过字典格式声明,然后到context

   OrderModerSerializer(instance="模型对象",data="客户端数据", context={})

利用序列化器初始化时提供的第三个参数就可以调用到视图类的
   context的属性          描述                       序列化器中的调用代码
       request    本次客户端的请求对象            self.context["request"]
       format     本次服务器响应的数据格式           self.context["format"]
       view       调用当前序列化器的视图类          self.context["view"]

因此,我们要在序列化器中提取用户的id,代码如下:
   user_id = self.context["request"].user.id

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:服务端提供创建订单的API接口"
git push

上面我们完成了订单信息的添加,但是下单不是一个数据记录而已,而是多张表记录的同时添加操作。所以针对这种多个记录或者多张表连贯进行的操作,为了保证数据的完整性和一致性以及原子性,我们要使用数据库的事务(Transaction)来完成,当然我们这个项目中不需要使用到数据库原生的事务语句,而是使用django的ORM提供的事务模块即可。

事务(Transaction),是以功能或业务作为逻辑单位,把一条或多条SQL语句组成一个不可分割的操作序列来执行的数据库特性。
在完成一个整体功能时,操作到了多个表数据,或者同一个表的多条记录,如果要保证这些SQL语句操作作为一个整体保存到数据库中,那么可以使用事务(transaction),保证这些操作作为不可分割的整体,要么一起成功,要么一起失败。

事务具有4个特性(ACID),5个隔离等级

  四个特性:一致性,原子性,隔离性,持久性
  # 隔离性:两个事务的隔离性,隔离性的修改可以通过数据库的配置文件mysqld.cnf进行修改,默认mysql是属于可重复级别
  五个隔离级别(从高到低): 串行隔离,可重复读,已提交读,未提交读,没有隔离
    原子性(Atomicity)
    一致性(Consistency)
    隔离性(Isolation)[事务隔离级别->幻读,脏读, 不可重复读]
    持久性(Durability)

  在mysql中有专门的SQl语句来完成事务的操作,事务的代码操作一般有3个步骤:
     设置事务开始  begin;
     事务的处理[mysql:增删改]
         redis.sadd()
         事务的处理[mysql:增删改]
     设置事务的回滚或者提交 rollback / commit;  # 这个事务过程中,事务无法对mysql数据库以外的其他类型的数据库操作进行管理和回滚
mysql中底层的事务是如何实现事务的回滚操作:undo.log重做日志
在ORM框架一般都会实现了事务操作封装,所以我们可以直接使用ORM框架即可完成事务的操作

django框架本身就提供了2种事务操作的写法,主要都是通过 django.db.transaction模块完成的。

启用事务写法1:基于装饰器对函数或方法进行事务管理:

from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    @transaction.atomic          # 开启事务,当函数/方法执行完成以后,自动提交事务
    def post(self,request):      # 不一定是视图方法,也可以是其他函数方法。
        ....  # 在整个函数或者方法中,进行的所有SQL数据写操作[增删改],都属于同一个事务操作

启用事务写法2,基于with上下文管理器进行事务管理:

from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
    def post(self,request):
        .... # 事务以外的,其他的SQL数据操作
        with transation.atomic(): # 开启事务,当with语句执行完成以后,自动提交事务
            # 数据库操作【DML增删改】

        .... # with语句以外的其他的SQL数据操作,无法被上面事务管理

在使用事务过程中, 有时候会出现异常,当出现异常时我们需要回滚事务。

from django.db import transaction
from rest_framework.generics import CreateAPIView
class OrderCreateAPIView(CreateAPIView):
    def post(self,request):
        ....
        with transaction.atomic():
            # 1、设置事务回滚的标记点【一个事物中可以设置多个回滚标记】
            sid1 = transaction.savepoint()
            try:
                .... # 增删改等数据库操作
                ....
            except:
                transaction.savepoint_rallback(sid1)

        .... # 数据库操作,注意,如果这里被执行,因为没有在with里面,所以是不会被上面的事务操作影响。

django的事务操作是支持嵌套事务的,但是mysql本身不支持嵌套事务。

from django.db import transaction
from rest_framework.generics import CreateAPIView
class OrderCreateAPIView(CreateAPIView):
    def post(self,request):
        ....
        with transaction.atomic():
            # 1、设置事务回滚的标记点【一个事物中可以设置多个回滚标记】
            sid1 = transaction.savepoint()

            try:
                .... # 增删改等数据库操作
                ....
                with transaction.atomic():
                    # 2. 设置回滚点
                    sid2 = transaction.savepoint()
                    try:
                        .... # 其他内部数据库处理
                        ....
                    except:
                        transaction.savepoint_rallback(sid2)
            except:
                transaction.savepoint_rallback(sid1)

        .... # 数据库操作,注意,如果这里被执行,因为没有在with里面,所以是不会被上面的事务操作影响。

使用Django的ORM提供的mysql事务操作保证下单过程中的数据原子性

orders/serializers.py,代码:

from datetime import datetime
from rest_framework import serializers
from django_redis import get_redis_connection
from django.db import transaction
from .models import Order, OrderDetail, Course
import logging

logger = logging.getLogger("django")


class OrderModelSerializer(serializers.ModelSerializer):
    pay_link = serializers.CharField(read_only=True)

    class Meta:
        model = Order
        fields = ["pay_type", "id", "order_number", "pay_link"]
        read_only_fields = ["id", "order_number"]
        extra_kwargs = {
            "pay_type": {"write_only": True},
        }

    def create(self, validated_data):
        """创建订单"""
        redis = get_redis_connection("cart")
        user_id = self.context["request"].user.id  # 1

        # 开启事务操作,保证下单过程中的所有数据库的原子性
        with transaction.atomic():
            # 设置事务的回滚点标记
            t1 = transaction.savepoint()
            try:
                # 创建订单记录
                order = Order.objects.create(
                    name="购买课程",  # 订单标题
                    user_id=user_id,  # 当前下单的用户ID
                    # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
                    order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
                    pay_type=validated_data.get("pay_type"),  # 支付方式
                )

                # 记录本次下单的商品列表
                cart_hash = redis.hgetall(f"cart_{user_id}")
                if len(cart_hash) < 1:
                    raise serializers.ValidationError(detail="购物车没有要下单的商品")

                # 提取购物车中所有勾选状态为b'1'的商品
                course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']

                # 添加订单与课程的关系
                course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
                detail_list = []
                total_price = 0 # 本次订单的总价格
                real_price = 0  # 本次订单的实付总价

                for course in course_list:
                    discount_price = float(course.discount.get("price", 0)) # 获取课程原价
                    discount_name = course.discount.get("type", "")
                    detail_list.append(OrderDetail(
                        order=order,
                        course=course,
                        name=course.name,
                        price=course.price,
                        real_price=discount_price,
                        discount_name=discount_name,
                    ))

                    # 统计订单的总价和实付总价
                    total_price += float(course.price)
                    real_price += discount_price if discount_price > 0 else float(course.price)

                # 一次性批量添加本次下单的商品记录
                OrderDetail.objects.bulk_create(detail_list)

                # 保存订单的总价格和实付价格
                order.total_price = total_price
                order.real_price = real_price
                order.save()

                # todo 支付链接地址[后面实现支付功能的时候,再做]
                order.pay_link = ""
                return order
            except Exception as e:
                # 1. 记录日志
                logger.error(f"订单创建失败:{e}")
                # 2. 事务回滚
                transaction.savepoint_rollback(t1)
                # 3. 抛出异常,通知视图返回错误提示
                raise serializers.ValidationError(detail="订单创建失败!")

购物车中选中的商品被记录到了订单中,那么购物车中原来的勾选商品是否要删除?

如果不删除,那么订单中的商品与购物车中就重复了,所以要删除,购物车中只需要保留没有勾选过的商品。

orders/serializers.py,代码:

from datetime import datetime
from rest_framework import serializers
from django_redis import get_redis_connection
from django.db import transaction
from .models import Order, OrderDetail, Course
import logging

logger = logging.getLogger("django")


class OrderModelSerializer(serializers.ModelSerializer):
    pay_link = serializers.CharField(read_only=True)

    class Meta:
        model = Order
        fields = ["pay_type", "id", "order_number", "pay_link"]
        read_only_fields = ["id", "order_number"]
        extra_kwargs = {
            "pay_type": {"write_only": True},
        }

    def create(self, validated_data):
        """创建订单"""
        redis = get_redis_connection("cart")
        user_id = self.context["request"].user.id  # 1

        # 开启事务操作,保证下单过程中的所有数据库的原子性
        with transaction.atomic():
            # 设置事务的回滚点标记
            t1 = transaction.savepoint()
            try:
                # 创建订单记录
                order = Order.objects.create(
                    name="购买课程",  # 订单标题
                    user_id=user_id,  # 当前下单的用户ID
                    # order_number = datetime.now().strftime("%Y%m%d%H%M%S") + ("%08d" % user_id) + "%08d" % random.randint(1,99999999) # 基于随机数生成唯一订单号
                    order_number=datetime.now().strftime("%Y%m%d") + ("%08d" % user_id) + "%08d" % redis.incr("order_number"), # 基于redis生成分布式唯一订单号
                    pay_type=validated_data.get("pay_type"),  # 支付方式
                )

                # 记录本次下单的商品列表
                cart_hash = redis.hgetall(f"cart_{user_id}")
                if len(cart_hash) < 1:
                    raise serializers.ValidationError(detail="购物车没有要下单的商品")

                # 提取购物车中所有勾选状态为b'1'的商品
                course_id_list = [int(key.decode()) for key, value in cart_hash.items() if value == b'1']

                # 添加订单与课程的关系
                course_list = Course.objects.filter(pk__in=course_id_list, is_deleted=False, is_show=True).all()
                detail_list = []
                total_price = 0 # 本次订单的总价格
                real_price = 0  # 本次订单的实付总价

                for course in course_list:
                    discount_price = float(course.discount.get("price", 0)) # 获取课程原价
                    discount_name = course.discount.get("type", "")
                    detail_list.append(OrderDetail(
                        order=order,
                        course=course,
                        name=course.name,
                        price=course.price,
                        real_price=discount_price,
                        discount_name=discount_name,
                    ))

                    # 统计订单的总价和实付总价
                    total_price += float(course.price)
                    real_price += discount_price if discount_price > 0 else float(course.price)

                # 一次性批量添加本次下单的商品记录
                OrderDetail.objects.bulk_create(detail_list)

                # 保存订单的总价格和实付价格
                order.total_price = total_price
                order.real_price = real_price
                order.save()

                # todo 支付链接地址[后面实现支付功能的时候,再做]
                order.pay_link = ""

                # 删除购物车中被勾选的商品,保留没有被勾选的商品信息
                cart = {key: value for key, value in cart_hash.items() if value == b'0'}
                pipe = redis.pipeline()
                pipe.multi()
                # 删除原来的购物车
                pipe.delete(f"cart_{user_id}")
                # 重新把未勾选的商品记录到购物车中
                pipe.hmset(f"cart_{user_id}", cart)  # hset 在新版本的redis中实际上hmset已经被废弃了,改用hset替代hmset
                pipe.execute()

                return order
            except Exception as e:
                # 1. 记录日志
                logger.error(f"订单创建失败:{e}")
                # 2. 事务回滚
                transaction.savepoint_rollback(t1)
                # 3. 抛出异常,通知视图返回错误提示
                raise serializers.ValidationError(detail="订单创建失败!")

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:服务端基于事务保证订单生成操作的原子性"
git push

客户端请求生成订单

api/order.js,代码:

import http from "../utils/http";
import {reactive} from "vue";

const order = reactive({
  total_price: 0,      // 勾选商品的总价格
  use_coupon: false,   // 用户是否使用优惠
  discount_type: 0,    // 0表示优惠券,1表示积分
  coupon_list:[1,2,3], // 用户拥有的可用优惠券列表
  select: -1,          // 当前用户选中的优惠券下标,-1表示没有选择
  credit: 0,           // 当前用户选择抵扣的积分,0表示没有使用积分
  fixed: true,         // 底部订单总价是否固定浮动
  pay_type: 0,         // 支付方式
  create_order(token){
    // 生成订单
    return http.post("/orders/",{
        pay_type: this.pay_type
    },{
        headers:{
            Authorization: "jwt " + token,
        }
    })
  }
})

export default order;

views/Order.vue,代码:

<span class="r btn btn-red submit-btn" @click="commit_order">提交订单</span>
<script setup>
import {reactive,watch} from "vue"
import Header from "../components/Header.vue"
import Footer from "../components/Footer.vue"
import {useStore} from "vuex";
import cart from "../api/cart"
import order from "../api/order";
import {ElMessage} from "element-plus";
import router from "../router";

// let store = useStore()

const get_select_course = ()=>{
    // 获取购物车中的勾选商品列表
    let token = sessionStorage.token || localStorage.token;
    cart.get_select_course(token).then(response=>{
        cart.select_course_list = response.data.cart
        if(response.data.cart.length === 0){
          ElMessage.error("当前购物车中没有下单的商品!请重新重新选择购物车中要购买的商品~");
          router.back();
        }
    }).catch(error=>{
    if(error?.response?.status===400){
      ElMessage.error("登录超时!请重新登录后再继续操作~");
    }
  })
}

get_select_course();


const commit_order = ()=>{
    // 生成订单
    let token = sessionStorage.token || localStorage.token;
    order.create_order(token).then(response=>{
    console.log(response.data.order_number)  // todo 订单号
    console.log(response.data.pay_link)      // todo 支付链接
    // 成功提示
    ElMessage.success("下单成功!马上跳转到支付页面,请稍候~")
    // 扣除掉被下单的商品数量,更新购物车中的商品数量
    store.commit("set_cart_total", store.state.cart_total - cart.select_course_list.length);
  }).catch(error=>{
    if(error?.response?.status===400){
          ElMessage.success("登录超时!请重新登录后再继续操作~");
    }
  })
}


// 监听用户选择的支付方式
watch(
    ()=>order.pay_type,
    ()=>{
      console.log(order.pay_type)
    }
)

// 底部订单总价信息固定浮动效果
window.onscroll = ()=>{
  let cart_body_table = document.querySelector(".cart-body-table")
  let offsetY = window.scrollY
  let maxY = cart_body_table.offsetTop+cart_body_table.offsetHeight
  order.fixed = offsetY < maxY
}
</script>

提交版本

cd /home/moluo/Desktop/luffycity
git add .
git commit -m "feature:客户端请求生成订单"
git push