【Django+Vue3 线上教育平台项目实战】购物车与订单模块的精简实现与数据安全策略

在这里插入图片描述


文章目录

  • 前言
  • 一、购物车模块
    • 1.后端核心逻辑
    • 2.前端页面代码
    • 3.操作流程及演示
  • 二、订单模块
    • 1.订单模块模型类设计
    • 1.展示订单信息
      • a.页面展示
      • b.前端核心代码
      • c.后端核心逻辑
    • 2.订单是否使用优惠券与积分
      • a.页面展示
      • b.前端核心代码
    • 3.订单支付方式
      • a.页面展示
      • b.前端核心代码
    • 4.提交订单(幂等性、事务性)⭐
    • 5.支付宝支付订单 ⭐
      • a.准备工作
      • b.前端获取支付链接地址
      • c.支付宝支付接口
      • d.操作流程及展示
    • 6.取消订单⭐


前言

    在构建线上教育平台的征途中,购物车与订单模块 是不可或缺的核心。本次,我们将聚焦于这两个模块的精简实现,包括购物车的添加、展示、修改功能,以及订单列表的展示、优惠券与积分的应用、支付宝支付的流程,特别是提交订单时,我们将深入探讨如何运用幂等性和事务性技术,确保订单数据的一致性和系统的稳定性,避免因网络延迟、重复点击等问题导致的订单异常。


一、购物车模块

  点击购买,加入购物车,判断购物车是否存在此课程,如果存在则特使不能重复添加,不存在加入购物车。

1.后端核心逻辑

购物车表

  • 字段:id、user(外键)、course(外键)、is_checked(是否选中)

添加购物车 、获取购物车列表信息、修改购物车选中状态:

# 0.购物车获取、添加、修改选中状态
class CartView(APIView):# 获取购物车列表,传参 useriddef get(self, request):# 获取参数userid = request.GET.get('userid')carts = CartsModel.objects.filter(user_id=userid)cartsSer = CartsSerializer(carts, many=True)carts_list = cartsSer.datareturn Response({"code":"200","carts":carts_list,"total":len(carts_list),"others":"待添加..."})# post-加入购物车def post(self, request):# 获取参数userid = request.data.get('userid')courseid = request.data.get('courseid')# 查询购物车表,是否已存在对应数据cart = CartsModel.objects.filter(user_id=userid, course_id=courseid).first()if cart is None:# 查询用户表课程是否存在ucourse = UserCourseModel.objects.filter(user_id=userid, course_id=courseid).first()if ucourse:# 用户表已有此课程,不能重复购买return Response({"code":10010,"message":"该课程已经购买,不可重复购买!"})# 不存在,添加购物车数据# cartInfo = {"user": userid, "course": courseid}cartSer = CartsSerializer(data=request.data)if cartSer.is_valid():cartSer.save()return Response({"message":"添加购物车成功!","code":"200"})else:print(cartSer.errors)return Response({"message":cartSer.errors,"code":"1001"})else:# 购物车已存在此信息,不能重复添加购物车return Response({"message":"购物车已存在此信息,切勿重复添加!","code":"1001"})# 修改购物车状态def patch(self, request):# 获取参数-购物车id,购物车选中状态cartid = request.data.get('course_id')selected = request.data.get('selected')print(cartid,selected)CartsModel.objects.filter(id=cartid).update(is_checked=selected)return Response({"message":"patch方法修改状态成功!","code":"200"})

2.前端页面代码

购物车页面 src\views\Cart.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 class="left"><span>{{cart.course_list.length}}</span>门,已选择<span>{{cart.selected_course_total}}</span></div></div><div class="right"><div class=""><span class="left"><router-link class="myorder-history" to="/myorder">我的订单列表</router-link></span></div></div></div></div><!-- 购物车体 --><div class="cart-body" id="cartBody"><!-- 购物车体-标题栏 --><div class="cart-body-title"><div class="item-1 l"><el-checkbox v-model="cart.checked">全选</el-checkbox></div><div class="item-2 l"><span class="course">课程</span></div><div class="item-3 l"><span>金额</span></div><div class="item-4 l"><span>操作</span></div></div><!-- 购物车 表格 --><div class="cart-body-table"><div class="item" v-for="(course_info, key) in cart.course_list"><div class="item-1"><el-checkbox @change="change_select_course(course_info)" v-model="course_info.selected"></el-checkbox></div><div class="item-2"><router-link :to="`/project/${course_info.course.id}`" class="img-box l"><img :src="course_info.course.picurl"></router-link><dl class="l has-package"> <dt>【{{course_info.course.name}}】 {{course_info.course.describe}}</dt></dl></div><div class="item-3"><div class="price" v-if="course_info.course.price !== undefined"><span class="discount-price"><em></em><span>{{course_info.course.price.toFixed(2)}}</span></span><br></div><div class="price" v-else><div class="discount-price"><em></em><span>{{course_info.course.price.toFixed(2)}}</span></div></div></div><div class="item-4"><el-icon @click="delete_course(key)" :size="26" class="close"><Close /></el-icon></div></div><!-- 结算 --><div class="cart-body-bot fixed"><div class=" cart-body-bot-box"><div class="right"><div class="add-coupon-box"><div class="li-left"><div class="li-2"><span class="topdiv w70">总计金额:</span><span class="price price-red w100"><em></em><span>{{cart.total_price.toFixed(2)}}</span></span></div></div><div class="li-3"><router-link to="/order" class="btn">去结算</router-link></div></div></div></div></div></div></div></div><Footer/></div>
</template>
import cart from "../api/cart";let token = sessionStorage.access_token || localStorage.access_token;
cart.list(token)

src\api\cart.js

import { reactive } from "vue";
import http from "../http";
import store from "../store";
import router from "../router";const cart = reactive({course_list: [],  // 购物车中的商品列表total_price: 0,  // 购物车中的商品总价格selected_course_total: 0, // 购物车中被勾选商品的数量checked: false,  // 购物车中是否全选商品了add(course_id, token) {// 添加商品到购物车return http.post("/carts/cart/", {course_id: course_id}, {// 因为当前课程端添加课程商品到购物车必须登录,所以接口操作时必须发送jwtheaders: {Authorization: "Bearer " + token,}})},list(token) {// 购物车商品列表// 本地浏览器获取useridconst userid = localStorage.getItem('userid')return http.get("/carts/cart/?userid="+userid, {headers: {Authorization: "Bearer " + token,}}).then(response => {if (response.data.carts) {this.course_list = response.data.carts;this.calc();} else {ElMessage.error("当前购物车没有任何商品,请购物后再继续操作!");router.push("/");}return response})},calc() {// 计算当前购物车中的商品总价格和勾选商品的总数let total_price = 0;  // 临时设置一个变量用于累计总价格let select_total = 0; // 临时设置一个变量用于累计勾选商品的数量this.course_list.forEach(course => {// 累计当前购物车中有多少商品课程被勾选了if (course.is_checked) {console.log("累计当前购物车中有多少商品课程被勾选了********");// 统计当前课程的总价格total_price += parseFloat(course.course.price);select_total += 1;}})this.total_price = total_price;this.selected_course_total = select_total;this.checked = this.selected_course_total === this.course_list.length;},select(course_id, selected, token) {// 切换商品课程的勾选状态return http.patch("/carts/cart/", {course_id: course_id,selected: selected,}, {// 因为当前课程端添加课程商品到购物车必须登录,所以接口操作时必须发送jwtheaders: {Authorization: "Bearer " + token,}}).then(response => {// 重新计算被勾选的商品数量this.calc();})},select_all(selected, token) {// 切换购物车对应商品课程的全选状态return http.put("/cart/", {selected,}, {headers: {Authorization: "Bearer " + token,}}).then(response => {this.calc();})},delete_course(key, token) {// 从购物车中删除商品课程let course_id = this.course_list[key].id;return http.delete("/cart/", {params: {course_id,  // course_id: course_id,的简写},headers: {Authorization: "Bearer " + token,}}).then(response => {this.course_list.splice(key, 1);// 通知vuex更新购物车中商品总数store.commit("set_cart_total", this.course_list.length);this.calc();})},
})export default cart;

3.操作流程及演示

1. 加入购物车:
在这里插入图片描述
2.购物车页面:
在这里插入图片描述
3.点击去结算,跳转到订单页面


二、订单模块

确认订单页面
在这里插入图片描述

1.订单模块模型类设计

订单模块:订单表、订单详情表、优惠券表、用户优惠券表、积分记录表

# 1.订单表
class OrdersModel(models.Model):orderno = models.CharField(max_length=255,default="",primary_key=True)user = models.ForeignKey(UsersModel, on_delete=models.CASCADE, verbose_name='用户id')  # 注意:这里通常使用外键关联User模型pay_status = models.IntegerField(choices=((1, '生成'), (2, '已支付'), (3, '支付失败')), verbose_name='支付状态')pay_type = models.IntegerField(choices=((1, '支付宝'), (2, '微信'), (3, '网银')), verbose_name='支付类型')orders_status = models.IntegerField(choices=((1, '生成'), (2, '已支付'), (3, '支付失败'), (4, '退款'), (5, '评价'), (6, '已经完成'), (7, '取消')),verbose_name='订单状态')transaction = models.CharField(max_length=50, verbose_name='流水号')score = models.IntegerField(default=0,verbose_name="积分")coupon_id = models.IntegerField(default=0,verbose_name="优惠券id")coupon_money = models.IntegerField(default=0,verbose_name="优惠金额")total_money = models.IntegerField(default=0,verbose_name="总金额")pay_money = models.IntegerField(default=0,verbose_name="实际支付金额")def __str__(self):return f'订单ID: {self.id}'class Meta:verbose_name = '订单表'verbose_name_plural = '订单表'db_table = 'orders'# 2.订单详情表
class OrdersDetailModel(models.Model):user = models.ForeignKey(UsersModel, on_delete=models.CASCADE)orders = models.ForeignKey(OrdersModel, on_delete=models.CASCADE)course = models.ForeignKey(CourseModel, on_delete=models.CASCADE)name = models.CharField(max_length=255)  # course被修改时,下订单时的属性不改变picurl = models.CharField(max_length=100, default='')# course被修改时,下订单时的属性不改变price = models.FloatField()# course被修改时,下订单时的属性不改变comment_status = models.IntegerField(default=0,verbose_name="评价状态")def __str__(self):return f'订单详情ID: {self.id}'class Meta:verbose_name = '订单详情表'verbose_name_plural = '订单详情表'db_table = "order_detail"# 3.优惠券表:id、name、man满、jian减、开始时间、结束时间、总张数、已发
class CouponModel(models.Model):name = models.CharField(max_length=255,default="优惠券默认名")coupon_full = models.IntegerField(verbose_name="价格满-",default=0)coupon_reduce = models.IntegerField(verbose_name="价格减少-",default=0)start_time = models.DateTimeField(verbose_name="开始时间")end_time = models.DateTimeField(verbose_name="结束时间")count = models.IntegerField(verbose_name="优惠券总量",default=100)count_issued = models.IntegerField(verbose_name="已发出的优惠券数量",default=0)def __str__(self):return self.nameclass Meta:verbose_name = "优惠券表"verbose_name_plural = "优惠券表"db_table = "coupon"# 4.用户优惠券表:user、优惠券、name、man满、jian减、开始时间、结束时间、使用状态
class UserCouponModel(models.Model):user = models.ForeignKey(UsersModel, on_delete=models.CASCADE)coupon = models.ForeignKey(CouponModel, on_delete=models.CASCADE)name = models.CharField(max_length=255,default="用户优惠券的默认名")coupon_full = models.IntegerField(verbose_name="价格满-",default=0)coupon_reduce = models.IntegerField(verbose_name="价格减少-",default=0)start_time = models.DateTimeField(verbose_name="开始时间")end_time = models.DateTimeField(verbose_name="结束时间")status = models.CharField(max_length=100,default="未使用")def __str__(self):return self.nameclass Meta:verbose_name = "用户优惠券表"verbose_name_plural = "用户优惠券表"db_table = "usercoupon"# 5.积分记录表
class ScoreRecordModel(models.Model):user = models.ForeignKey(UsersModel, on_delete=models.CASCADE)types = models.IntegerField(default=1) #1+积分 2-积分score = models.IntegerField(default=0)descp = models.CharField(max_length=255, default='')def __str__(self):return self.descpclass Meta:verbose_name = "积分记录表"verbose_name_plural = "积分记录表"db_table = "score_record"

1.展示订单信息

a.页面展示

购物车页面点击去结算后,进入订单页面
在这里插入图片描述

b.前端核心代码

订单页面 — 展示订单信息 src\views\Order.vue

<!-- 订单中展示课程信息 -->
<div class="item" v-for="course_info in order.selected_course_list"><!-- {{course_info}} --><div class="item-2"><router-link :to="`/project/${course_info.id}`" class="img-box l"><img :src="course_info.course.picurl"></router-link><dl class="l has-package"><dt>【{{course_info.course.name}}】{{course_info.course.describe}} </dt></dl></div><div class="item-3"><div class="price"><p class="discount-price"><em></em><span>{{course_info.course.price.toFixed(2)}}</span></p></div></div>
</div>

src\api\order.js

  // 1.获取购物车中的勾选商品列表get_selected(token) {return http.get("/carts/preorder/?userid=1", {headers: {Authorization: "Bearer " + token,}}).then(response => {console.log("response.data-----------获取购物车中的勾选商品列表---------------")console.log(response.data)if (response.data.cart) {this.selected_course_list = response.data.cart;this.calc_cart();// this.get_coupon_list(token);// 此用户拥有的优惠券this.coupon_list = response.data.coupon// 总积分this.has_credit = response.data.score;// 把用户拥有的积分与本次下单的最大对换积分进行比较,判断用户可以使用的最大数量积分if (this.has_credit < this.max_use_credit) {this.max_use_credit = this.has_credit}} else {ElMessage.error("当前购物车没有任何商品,请购物后再继续操作!");router.push("/");}return response})},

c.后端核心逻辑

cart/views.py

# 1.订单前,信息
class PreOrderView(APIView):def get(self, request):# 获取useriduserid = request.GET.get('userid')# 查询购物车中已经选中的记录cart = CartsModel.objects.filter(user_id=userid,is_checked=True).all()cartSer = CartsSerializer(cart, many=True)# 查询用户的总积分user = UsersModel.objects.filter(id=userid).first()score = user.points# 查询优惠券coupons = UserCouponModel.objects.filter(user_id=userid)couponsSer = UserCouponSerializer(coupons, many=True)coupons_list = couponsSer.datareturn Response({"code":"200","cart":cartSer.data,"score":score,"coupon":coupons_list})

2.订单是否使用优惠券与积分

a.页面展示

在这里插入图片描述
在这里插入图片描述

b.前端核心代码

src\views\Order.vue

<!-- 优惠券/积分 -->
<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></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">优惠券 ({{order.coupon_list.length}})<em><i class="imv2-check"></i></em></div><div class="switch-btn code" :class="{'checked': order.discount_type===1}" @click="order.discount_type=1">积分 ({{order.has_credit}})<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" :class="{select: order.select === key}" @click="order.select = (order.select === key?-1:key)" v-for="(coupon,key) in order.coupon_list" :key="key"><div class="detail-box more-del-box"><div class="price-box"><p class="coupon-price l" v-if="coupon.discount === '1'"> ¥{{Math.abs(coupon.sale)}} </p><p class="coupon-price l" v-if="coupon.discount === '2'"> {{coupon.sale.replace("*0.","")}}折 </p><p class="use-inst l" v-if="coupon.condition>0">满{{coupon.coupon_full}}元可用</p><p class="use-inst l" v-else>任意使用</p></div><div class="use-detail-box"><div class="use-ajust-box">适用于:{{coupon.name}}</div><div class="use-ajust-box">有效期:{{coupon.start_time.split("T")[0].replaceAll("-",".")}}-{{coupon.end_time.split("T")[0].replaceAll("-",".")}}</div></div></div></li></ul></div></div></div><!-- 积分渲染 --><div class="coupon-content code" v-else><div class="input-box"><el-input-number v-model="order.credit" :step="1" :min="0" :max="order.max_use_credit"></el-input-number><a class="convert-btn" @click="conver_credit">兑换</a><a class="convert-btn" @click="max_conver_credit">最大积分兑换</a></div><div class="converted-box"><p>使用积分:<span class="code-num">200</span></p><div v-for="course_info in order.selected_course_list"><p class="course-title" v-if="course_info.credit>0">课程:<span class="c_name">{{course_info.course.name}}</span><span class="discount-cash">{{course_info.credit}}积分抵扣:<em>{{parseInt(course_info.credit / order.credit_to_money)}}</em></span></p></div></div><p class="error-msg">本次订单最多可以使用{{order.max_use_credit}}积分,您当前拥有{{order.has_credit}}积分。({{order.credit_to_money}}积分=1元)</p><p class="tip">说明:每笔订单只能使用一次积分,并只有在部分允许使用积分兑换的课程中才能使用。</p></div></div></transition>
</div>
  // 1.获取购物车中的勾选商品列表get_selected(token) {return http.get("/carts/preorder/?userid=1", {headers: {Authorization: "Bearer " + token,}}).then(response => {console.log("response.data-----------获取购物车中的勾选商品列表---------------")console.log(response.data)if (response.data.cart) {console.log(111111);this.selected_course_list = response.data.cart;this.calc_cart();// 此用户拥有的优惠券this.coupon_list = response.data.coupon// 总积分this.has_credit = response.data.score;// 把用户拥有的积分与本次下单的最大对换积分进行比较,判断用户可以使用的最大数量积分if (this.has_credit < this.max_use_credit) {this.max_use_credit = this.has_credit}} else {ElMessage.error("当前购物车没有任何商品,请购物后再继续操作!");router.push("/");}return response})},// 计算购物车内商品价格calc_cart() {// 计算本次下单的所有商品的相关价格let original_total_price = 0;  // 所有的课程的原价总价格let real_total_price = 0;      // 所有的课程的实付总价格// let max_use_credit = 0;        // 所有的课程的累计可以最大兑换积分this.selected_course_list.forEach(course => {original_total_price += course.course.price;real_total_price += course.course.price;})this.total_price = original_total_price;this.real_price = real_total_price;this.max_use_credit = this.real_price;},

3.订单支付方式

a.页面展示

在这里插入图片描述

b.前端核心代码

<!-- 选择支付方式 -->
<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=1" alt="支付宝"><img :src="order.pay_type==1?'/src/assets/wechat2.png':'/src/assets/wechat1.png'" @click="order.pay_type=2" alt="微信"><img :src="order.pay_type==2?'/src/assets/yue2.png':'/src/assets/yue1.png'"  @click="order.pay_type=3" alt="余额"></div>
</div>

4.提交订单(幂等性、事务性)⭐

在提交订单时,可能会遇到如下问题:

1.解决网络慢导致的频繁点击生成无效订单问题

  • 问题描述: 由于网络延迟,用户可能在短时间内频繁点击生成订单按钮,导致生成多个无效订单。
  • 解决方案: 通过 接口幂等性操作 来避免重复订单的生成。
  • 实现流程:
    • 1.前端准备: 在确认订单页面的onMounted生命周期钩子中,调用后端接口请求一个唯一的标识符(UUID)。
    • 2.后端生成UUID: 后端接口接收到请求后,使用uuid.uuid4().hex()生成一个唯一的UUID,并将其存储在Redis中(可以设置一个合理的过期时间,如几分钟),然后将这个UUID返回给前端。
    • 3.前端携带UUID生成订单: 用户点击生成订单按钮时,将之前获取的UUID作为参数一同发送到后端。
    • 4.后端验证UUID: 后端接收到订单生成请求时,首先检查Redis中是否存在该UUID。
      • 如果存在,说明是重复请求,直接返回“订单已生成,请勿重复操作”的提示,并从Redis中删除该UUID(可选,视业务逻辑而定)。
      • 如果不存在,则认为是有效请求,继续订单生成流程,并在订单生成成功后,确保不将UUID存入Redis(因为是单次有效)。

2.解决订单与订单详情写入不一致问题

  • 问题描述: 在生成订单的过程中,订单表已成功写入数据,但在写入订单详情表时发生错误,导致订单详情缺失,用户支付成功后可能无法获得相应的课程。
  • 解决方案: 使用数据库事务来保证订单和订单详情的一致性。
  • 实现流程:
    • 1.引入事务支持: 确保你的数据库框架或ORM支持事务处理。例如,在Django中可以使用transaction模块,在SQLAlchemy中也有相应的事务管理。
    • 2.开启事务: 在生成订单的接口逻辑中,首先开启一个数据库事务。这通常在数据库操作开始前进行。
    • 3.执行订单和订单详情写入: 在事务的上下文中,先执行订单表的写入操作,然后执行订单详情表的写入操作。
    • 4.异常处理与事务回滚: 如果在写入订单详情表时发生异常,立即捕获该异常,并执行事务回滚操作,以确保订单表中的数据也被撤销。这样可以保证数据库的一致性和完整性。
    • 5.提交事务: 如果所有操作都成功完成,没有发生异常,则提交事务,使所有更改永久生效。
# 2.订单uid
class OrderKeysView(APIView):def get(self, request):userid = request.GET.get('userid')uid = uuid.uuid1().hexkey = str(uid) + str(userid)r.set_str(key,'111')return Response({"code":"200","orderuid":key})import datetime
from django.db import transaction
# 3.提交订单
class OrderView(APIView):@transaction.atomic()def post(self, request):# 获取参数:用户id、支付方式...userid = request.data.get('userid')pay_type = request.data.get('pay_type')credit = request.data.get('credit') #// 当前用户选择抵扣的积分user_coupon_id = request.data.get('user_coupon_id')orderuid = request.data.get('orderuid')print("验证接口幂等性1")# ***验证接口幂等性***value = r.get_str(orderuid)if value:#删除r.delete_str(orderuid)# 生成订单号print("验证接口幂等性2")orderno = datetime.datetime.strftime(datetime.datetime.now(), '%Y%m%d%H%M%S')+str(userid)+str(random.randint(10000,99999))print("orderno" + str(orderno))# 开启事务# 创建保存点save_id = transaction.savepoint()try:# 判断优惠券id,若存在查询用户优惠券表couponmoney = 0if int(user_coupon_id)>0:user_coupon = UserCouponModel.objects.filter(id=user_coupon_id).first()user_coupon.status = "已使用"couponmoney = user_coupon.coupon_reduce #价格减少coupon_reduceuser_coupon.save()# 添加订单表orders = OrdersModel.objects.create(user_id=userid,pay_type=pay_type,score=credit,coupon_id=user_coupon_id,coupon_money=0,transaction=orderno,pay_status=2,orders_status=1)# 根据用户查询选中购物车total_money = 0pay_money = 0# 遍历购物车,写入订单表,计算出总价格,删除购物车中商品carts = CartsModel.objects.filter(user_id=userid,is_checked=True).all()for cart in carts:total_money += float(cart.course.price)# 添加订单详情表OrdersDetailModel.objects.create(user_id=userid,orders_id=orders.id,course_id=cart.course.id,name=cart.course.name,picurl = cart.course.picurl,price = cart.course.price)# 删除购物车表# CartsModel.objects.filter(id=cart.id).delete()# 更新订单总价格---orders.total_money = total_moneycoupon = int(couponmoney) + int(credit) #总优惠金额if total_money > coupon:orders.pay_money = total_money - couponelse:orders.pay_money = 0orders.save()#如果使用积分,更新积分记录表,更新用户表中的总积分if int(credit) > 0:user = UsersModel.objects.filter(id=userid).first()user.points -= int(credit) #更新用户表总积分user.save()ScoreRecordModel.objects.create(user_id=userid,types=2,score=int(credit),descp="购买订单"+orderno+"时所用积分")# 提交事务# zset --- 把订单号 + score 加入队列times = int(time.time())+1800r.zset_zadd("orderno",times,orderno)transaction.savepoint_commit(save_id)return Response({"code":"200"})except:# 失败回滚transaction.savepoint_rollback(save_id)return Response({"code": "1001", "message": "订单生成失败!"})

5.支付宝支付订单 ⭐

支付宝开放平台:https://open.alipay.com/


操作流程:
1.支付宝开放平台注册账号,注册应用
2.在应用中配置回调地址,生成公钥私钥,在支付宝上配置
3.获取支付链接
4.点击链接跳转到支付宝平台,用测试账号登录,输入支付密码。
5.支付成功后支付宝会根据配置的回调地址回调接口
6.在接口中进行业务的处理

  • 获取参数
  • 验证签名
  • 更新订单表中订单状态、支付状态、流水号

a.准备工作

1.登录后,注册沙箱应用
在这里插入图片描述
2.配置回调地址以及应用公钥
在这里插入图片描述
3.通过本地公钥,获得支付宝应用公钥

在这里插入图片描述

4.在项目中配置
在这里插入图片描述

5.支付宝 支付工具类

  • tools/pay.py
  • tools/common.py - - - get_alipay()
# tools/pay.py
from datetime import datetime
from Crypto.PublicKey import RSA
from Crypto.Signature import PKCS1_v1_5
from Crypto.Hash import SHA256
from urllib.parse import quote_plus
from urllib.parse import urlparse, parse_qs
from base64 import decodebytes, encodebytes
import jsonclass AliPay(object):"""支付宝支付接口(PC端支付接口)"""def __init__(self, appid, app_notify_url, app_private_key_path,alipay_public_key_path, return_url, debug=False):self.appid = appidself.app_notify_url = app_notify_urlself.app_private_key_path = app_private_key_pathself.app_private_key = Noneself.return_url = return_urlwith open(self.app_private_key_path) as fp:self.app_private_key = RSA.importKey(fp.read())self.alipay_public_key_path = alipay_public_key_pathwith open(self.alipay_public_key_path) as fp:self.alipay_public_key = RSA.importKey(fp.read())if debug is True:self.__gateway = "https://openapi.alipaydev.com/gateway.do"else:self.__gateway = "https://openapi.alipay.com/gateway.do"def direct_pay(self, subject, out_trade_no, total_amount, return_url=None, **kwargs):biz_content = {"subject": subject,"out_trade_no": out_trade_no,"total_amount": total_amount,"product_code": "FAST_INSTANT_TRADE_PAY",# "qr_pay_mode":4}biz_content.update(kwargs)data = self.build_body("alipay.trade.page.pay", biz_content, self.return_url)return self.sign_data(data)#查询接口def query_pay(self, out_trade_no,return_url=None, **kwargs):biz_content = {"out_trade_no": out_trade_no,# "product_code": "FAST_INSTANT_TRADE_PAY",# "qr_pay_mode":4}biz_content.update(kwargs)data = self.build_body("alipay.trade.query", biz_content, self.return_url)return self.sign_data(data)#转账接口def deposit(self,out_biz_no,trans_amount,title,payee_info,return_url=None,**kwargs):biz_content = {"out_biz_no":out_biz_no,"trans_amount":trans_amount,"biz_scene":"DIRECT_TRANSFER","product_code":"TRANS_ACCOUNT_NO_PWD","order_title":title,"payee_info":payee_info}biz_content.update(kwargs)data = self.build_body("alipay.fund.trans.uni.transfer", biz_content, self.return_url)return self.sign_data(data)#转账接口def refund(self,trade_no,refund_amount,return_url=None,**kwargs):biz_content = {"trade_no":trade_no,"refund_amount":refund_amount,}biz_content.update(kwargs)data = self.build_body("alipay.trade.refund", biz_content, self.return_url)return self.sign_data(data)def build_body(self, method, biz_content, return_url=None):data = {"app_id": self.appid,"method": method,"charset": "utf-8","sign_type": "RSA2","timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),"version": "1.0","biz_content": biz_content}if return_url is not None:data["notify_url"] = self.app_notify_urldata["return_url"] = self.return_urlreturn datadef sign_data(self, data):data.pop("sign", None)# 排序后的字符串unsigned_items = self.ordered_data(data)unsigned_string = "&".join("{0}={1}".format(k, v) for k, v in unsigned_items)sign = self.sign(unsigned_string.encode("utf-8"))# ordered_items = self.ordered_data(data)quoted_string = "&".join("{0}={1}".format(k, quote_plus(v)) for k, v in unsigned_items)# 获得最终的订单信息字符串signed_string = quoted_string + "&sign=" + quote_plus(sign)return signed_stringdef ordered_data(self, data):complex_keys = []for key, value in data.items():if isinstance(value, dict):complex_keys.append(key)# 将字典类型的数据dump出来for key in complex_keys:data[key] = json.dumps(data[key], separators=(',', ':'))return sorted([(k, v) for k, v in data.items()])def sign(self, unsigned_string):# 开始计算签名key = self.app_private_keysigner = PKCS1_v1_5.new(key)signature = signer.sign(SHA256.new(unsigned_string))# base64 编码,转换为unicode表示并移除回车sign = encodebytes(signature).decode("utf8").replace("\n", "")return signdef _verify(self, raw_content, signature):# 开始计算签名key = self.alipay_public_keysigner = PKCS1_v1_5.new(key)digest = SHA256.new()digest.update(raw_content.encode("utf8"))if signer.verify(digest, decodebytes(signature.encode("utf8"))):return Truereturn Falsedef verify(self, data, signature):if "sign_type" in data:sign_type = data.pop("sign_type")# 排序后的字符串unsigned_items = self.ordered_data(data)message = "&".join(u"{}={}".format(k, v) for k, v in unsigned_items)return self._verify(message, signature)
# tools/common.py
from .pay import AliPay
def get_alipay():# 初始化支付实例# 公共参数# 沙箱环境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=infoapp_id = "1111111111111"  # APPID (沙箱应用)# 支付完成后,支付偷偷向这里地址发送一个post请求,识别公网IP,如果是 192.168.20.13局域网IP ,支付宝找不到,def page2() 接收不到这个请求notify_url = "http://127.0.0.1:8000/carts/pay/"# 支付完成后,跳转的地址。return_url = "http://127.0.0.1:8000/carts/pay/"merchant_private_key_path = "tools/keys/private.txt"  # 应用私钥alipay_public_key_path = "tools/keys/public.txt"  # 支付宝公钥alipay = AliPay(appid=app_id,app_notify_url=notify_url,return_url=return_url,app_private_key_path=merchant_private_key_path,alipay_public_key_path=alipay_public_key_path,  # 支付宝的公钥,验证支付宝回传消息使用,不是你自己的公钥debug=True,  # 默认False,)return alipay

b.前端获取支付链接地址

  get_pay_link(order_id) {// 获取支付宝支付链接地址return http.post("/carts/pay/", {order:this.order,pay_money:this.pay_money}).then(response => {window.open(response.data, "_blank");})},

c.支付宝支付接口

# 4.支付宝接口 支付
class PayView(APIView):def post(self, request):# 获取订单号orderno = 10pay_money = 100pay = get_alipay()query_params = pay.direct_pay(subject="订单支付",  # 商品简单描述out_trade_no=str(orderno),  # 用户购买的商品订单号(每次不一样) 20180301073422891total_amount=float(pay_money),  # 交易金额(单位: 元 保留俩位小数))payUrl = "https://openapi-sandbox.dl.alipaydev.com/gateway.do?{0}".format(query_params)return Response({"url":payUrl})def get(self, request):# 获取参数data = request.GETprint("data----------" + str(data))rdata = {k:v for k,v in data.items()}print("rdata++++++++++++" + str(rdata))# 验证签名sign = rdata.pop('sign')pay = get_alipay()flag = pay.verify(rdata,sign)print(flag)# 业务逻辑if flag:# 更新订单表orderno = data['out_trade_no']# 支付宝流水号transaction_no = data['trade_no']# 调用支付宝查询接口查询flag = Trueif flag == True:# 通过订单号查询订单表orders = OrdersModel.objects.filter(orderno=orderno).first()orders.status=2 #状态orders.orders_status = 2 #支付状态orders.transaction_no = transaction_noorders.save()# 把成功的加入到成功队列中r.list_push("ordersuccess",orderno)return HttpResponseRedirect("http://127.0.0.1:3000/")else:orders = OrdersModel.objects.filter(orderno=orderno).first()orders.status=3orders.orders_status = 3orders.transaction_no = transaction_noorders.save()# 把成功的加入到成功队列中r.list_push("orderfail",orderno)return HttpResponseRedirect("http://127.0.0.1:3000/error")

d.操作流程及展示

1.获取url支付链接地址,为了测试方便这里使用postman
在这里插入图片描述
2.点击链接地址,跳转到支付宝平台
在这里插入图片描述
3.使用沙箱测试账号登录,进入支付页面
在这里插入图片描述
在这里插入图片描述
4.点击付款
在这里插入图片描述
5.完成后,自动跳转平台支付成功的地址
在这里插入图片描述

6.取消订单⭐

    通过celery设置定时任务,定时检查订单状态。在订单超时未支付时,修改订单状态为取消订单状态。
    参考:【Django+Vue3 线上教育平台项目实战】Celery赋能:优化订单超时处理与自动化定时任务调度


在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/web/47098.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

PyTorch Autograd内部实现

原文&#xff1a; 克補 爆炸篇 25s (youtube.com) 必应视频 (bing.com)https://www.bing.com/videos/riverview/relatedvideo?&qPyTorchautograd&qpvtPyTorchautograd&mid1B8AD76943EFADD541E01B8AD76943EFADD541E0&&FORMVRDGAR 前面只要有一个node的re…

北京交通大学《深度学习》专业课,实验3卷积、空洞卷积、残差神经网络实验

一、实验要求 1. 二维卷积实验&#xff08;平台课与专业课要求相同&#xff09; ⚫ 手写二维卷积的实现&#xff0c;并在至少一个数据集上进行实验&#xff0c;从训练时间、预测精 度、Loss变化等角度分析实验结果&#xff08;最好使用图表展示&#xff09; ⚫ 使用torch.nn…

Matlab基础语法篇(下)

Matlab基础语法&#xff08;下&#xff09; 一、逻辑基础&#xff08;一&#xff09;逻辑运算符&#xff08;二&#xff09;all、any、find函数&#xff08;三&#xff09;练习 二、结构基础&#xff08;一&#xff09;条件结构&#xff08;1&#xff09;if-elseif-else-end&am…

十、操作符详解

目录 1、操作符分类 2、二进制转换 2.1二进制转十进制 2.1.1、十进制转二进制 2.2、二进制转八进制和十六进制 2.2.1、二进制转八进制 2.2.2、二进制转十六进制 3、原码、反码、补码 4、移位操作符&#xff08;移动的是二进制位&#xff09; 4.1、左移操作符 4.2、右…

VMware虚拟机下安装Ubuntu(详细教程,最小系统的安装,含VMware Tools)

1.VM的下载安装 VMware的下载安装教程_vm16 pro下载-CSDN博客 2. Ubuntu 下载 在官网或者镜像站下载所需版本的.ios镜像&#xff0c;这个镜像在接下来的步骤中会用到&#xff1a; Ubuntu 22.04.4 LTS 下载 和 清华大学开源软件镜像站 - Ubuntu 22.04.4 下载 3. 创建虚拟机 […

【C语言】深入解析希尔排序

文章目录 什么是希尔排序&#xff1f;希尔排序的基本实现代码解释希尔排序的优化希尔排序的性能分析希尔排序的实际应用结论 在C语言编程中&#xff0c;希尔排序是一种高效的排序算法&#xff0c;是插入排序的一种更高效的改进版本。它通过比较相距一定间隔的元素来进行排序&am…

【STM32嵌入式系统设计与开发---拓展】——1_10矩阵按键

这里写目录标题 1、矩阵按键2、代码片段分析 1、矩阵按键 通过将4x4矩阵按键的每一行依次设为低电平&#xff0c;同时保持其它行为高电平&#xff0c;然后读取所有列的电平状态&#xff0c;可以检测到哪个按键被按下。如果某列变为低电平&#xff0c;说明对应行和列的按键被按下…

【Java】详解抽象类和接口的区别

一、抽象类和接口的主要区别表格 特性抽象类接口声明关键字abstractinterface声明访问修饰符public、protected、default&#xff08;不能用private&#xff09;public、default继承关键字extendsimplements变量跟普通类一样&#xff0c;可以包含实例变量、静态变量等 只能包含…

excel表怎么增乱序单词表 和正序单词表四六级要来了?!Excel帮你构建自己的单词库

excel表怎么增乱序单词表 和正序单词表四六级要来了&#xff1f;&#xff01;Excel帮你构建自己的单词库 1.背单词的第一步&#xff0c;当然是先上网找电子版的单词集。 盘搜搜 2. 建立 xls 格式的表格 3. 把下载的单词数据 复制到 表格 粘贴 4.新建一列 辅助列 生成随机数来…

网络编程-TCP/IP

网络概述 网络采用分而治之的方法设计&#xff0c;将网络的功能划分为不同的模块&#xff0c;以分层的形式有机组合在一起。 每层实现不同的功能&#xff0c;其内部实现方法对外部其他层次来说是透明的。每层向上层提供服务&#xff0c;同时使用下层提供的服务 网络体系结构…

SpringMVC注解全解析:构建高效Web应用的终极指南 (上)

SpringMVC 是一个强大的 Web 框架&#xff0c;广泛应用于 Java Web 开发中。它通过注解简化了配置&#xff0c;增强了代码的可读性。本文将全面解析 SpringMVC 中常用的注解及其用法&#xff0c;帮助你构建高效的 Web 应用。 一. MVC介绍 MVC 是 Model View Controller 的缩写…

数字通云平台 智慧政务OA PayslipUser SQL注入漏洞复现

0x01 产品简介 数字通云平台智慧政务OA产品是基于云计算、大数据、人工智能等先进技术,为政府部门量身定制的智能化办公系统。该系统旨在提高政府部门的办公效率、协同能力和信息资源共享水平,推动电子政务向更高层次发展。 0x02 漏洞概述 数字通云平台 智慧政务OA Paysli…

使用百度语音技术实现文字转语音

使用百度语音技术实现文字转语音 SpringBootVue前后端分离项目 调用api接口需要使用AK和SK生成AccessToken,生成getAccessToken的接口有跨域限制,所以统一的由后端处理了 部分参数在控制台->语音技术->在线调试里面能找到 Controller RestController RequestMapping(&q…

PostgreSQL的引号、数据类型转换和数据类型

一、单引号和双引号&#xff08;重要&#xff09;&#xff1a; 1、在mysql没啥区别 2、在pgsql中&#xff0c;实际字符串用单引号&#xff0c;双引号相当于mysql的,用来包含关键字&#xff1b; -- 单引号&#xff0c;表示user_name的字符串实际值 insert into t_user(user_nam…

浏览器跨tab页面通信方式总结

需求&#xff1a; 浏览器不同 tab 标签页之间是独立的&#xff0c; 如果要通信必须通过特殊手段来实现跨标签页通信。 1.StorageEvent 事件 当一个标签页 localStorage 变化时&#xff08;sessionStorage 无效&#xff09;&#xff0c;同源下另一个或其他所有标签页使用 DO…

python多级表头汇总

需求&#xff1a;将图一的数据展示为图二样式 图一&#xff1a; 图二&#xff1a; 图一具体的Excel截图 图二具体样式 python解决办法&#xff1a; # 导入 pandas 库&#xff0c;用于数据处理 import pandas as pd# 加载 Excel 文件 file_path 多级表头读取实例.xl…

科研绘图系列:R语言circos图(circos plot)

介绍 Circos图是一种数据可视化工具,它以圆形布局展示数据,通常用于显示数据之间的关系和模式。这种图表特别适合于展示分层数据或网络关系。Circos图的一些关键特点包括: 圆形布局:数据被组织在一个或多个同心圆中,每个圆可以代表不同的数据维度或层次。扇区:每个圆被划…

【BUG】已解决:SyntaxError invalid syntax

SyntaxError invalid syntax 目录 SyntaxError invalid syntax 【常见模块错误】 错误原因&#xff1a; 解决办法&#xff1a; 欢迎来到英杰社区https://bbs.csdn.net/topics/617804998 欢迎来到我的主页&#xff0c;我是博主英杰&#xff0c;211科班出身&#xff0c;就职于…

图书馆定位导航:RFID、VR与AR技术在图书馆中的应用

图书馆作为知识的宝库&#xff0c;承载着无数求知者的梦想与期待&#xff0c;随着馆藏书籍数量的激增与图书馆布局的日益复杂&#xff0c;读者在寻找目标书籍往往有许多困难。传统的索引号查询方式虽能提供书籍的基本信息&#xff0c;但在寻找过程中&#xff0c;因不熟悉图书馆…

【Android】使用视图绑定ViewBinding来代替findViewById

文章目录 介绍作用用法开启ViewBinding功能自动生成绑定类在Activity中使用访问视图控件 区别 介绍 ViewBinding 是 Android 开发中的一个功能&#xff0c;它简化了访问视图的过程&#xff0c;避免了使用 findViewById 的繁琐步骤。它通过生成与布局文件相对应的绑定类&#xf…