文章目录
前言
在构建线上教育平台的征途中,购物车与订单模块 是不可或缺的核心。本次,我们将聚焦于这两个模块的精简实现,包括购物车的添加、展示、修改功能,以及订单列表的展示、优惠券与积分的应用、支付宝支付的流程,特别是提交订单时,我们将深入探讨如何运用幂等性和事务性技术,确保订单数据的一致性和系统的稳定性,避免因网络延迟、重复点击等问题导致的订单异常。
一、购物车模块
点击购买,加入购物车,判断购物车是否存在此课程,如果存在则特使不能重复添加,不存在加入购物车。
1.后端核心逻辑
购物车表
- 字段:id、user(外键)、course(外键)、is_checked(是否选中)
添加购物车 、获取购物车列表信息、修改购物车选中状态:
# 0.购物车获取、添加、修改选中状态classCartView(APIView):# 获取购物车列表,传参 useriddefget(self, request):# 获取参数
userid = request.GET.get('userid')
carts = CartsModel.objects.filter(user_id=userid)
cartsSer = CartsSerializer(carts, many=True)
carts_list = cartsSer.data
return Response({"code":"200","carts":carts_list,"total":len(carts_list),"others":"待添加..."})# post-加入购物车defpost(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 isNone:# 查询用户表课程是否存在
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"})# 修改购物车状态defpatch(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><divclass="cart"><Header/><divclass="cart-main"><!-- 购物车头部:我的购物车 --><divclass="cart-header"><divclass="cart-header-warp"><divclass="cart-title left"><h1class="left">我的购物车</h1><divclass="left">
共<span>{{cart.course_list.length}}</span>门,已选择<span>{{cart.selected_course_total}}</span>门
</div></div><divclass="right"><divclass=""><spanclass="left"><router-linkclass="myorder-history"to="/myorder">我的订单列表</router-link></span></div></div></div></div><!-- 购物车体 --><divclass="cart-body"id="cartBody"><!-- 购物车体-标题栏 --><divclass="cart-body-title"><divclass="item-1 l"><el-checkboxv-model="cart.checked">全选</el-checkbox></div><divclass="item-2 l"><spanclass="course">课程</span></div><divclass="item-3 l"><span>金额</span></div><divclass="item-4 l"><span>操作</span></div></div><!-- 购物车 表格 --><divclass="cart-body-table"><divclass="item"v-for="(course_info, key) in cart.course_list"><divclass="item-1"><el-checkbox@change="change_select_course(course_info)"v-model="course_info.selected"></el-checkbox></div><divclass="item-2"><router-link:to="`/project/${course_info.course.id}`"class="img-box l"><img:src="course_info.course.picurl"></router-link><dlclass="l has-package"><dt>【{{course_info.course.name}}】 {{course_info.course.describe}}</dt></dl></div><divclass="item-3"><divclass="price"v-if="course_info.course.price !== undefined"><spanclass="discount-price"><em>¥</em><span>{{course_info.course.price.toFixed(2)}}</span></span><br></div><divclass="price"v-else><divclass="discount-price"><em>¥</em><span>{{course_info.course.price.toFixed(2)}}</span></div></div></div><divclass="item-4"><el-icon@click="delete_course(key)":size="26"class="close"><Close/></el-icon></div></div><!-- 结算 --><divclass="cart-body-bot fixed"><divclass=" cart-body-bot-box"><divclass="right"><divclass="add-coupon-box"><divclass="li-left"><divclass="li-2"><spanclass="topdiv w70">总计金额:</span><spanclass="price price-red w100"><em>¥</em><span>{{cart.total_price.toFixed(2)}}</span></span></div></div><divclass="li-3"><router-linkto="/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();})},})exportdefault cart;
3.操作流程及演示
1. 加入购物车:
2.购物车页面:
3.点击去结算,跳转到订单页面
二、订单模块
确认订单页面
1.订单模块模型类设计
订单模块:订单表、订单详情表、优惠券表、用户优惠券表、积分记录表
# 1.订单表classOrdersModel(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):returnf'订单ID: {self.id}'classMeta:
verbose_name ='订单表'
verbose_name_plural ='订单表'
db_table ='orders'# 2.订单详情表classOrdersDetailModel(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):returnf'订单详情ID: {self.id}'classMeta:
verbose_name ='订单详情表'
verbose_name_plural ='订单详情表'
db_table ="order_detail"# 3.优惠券表:id、name、man满、jian减、开始时间、结束时间、总张数、已发classCouponModel(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.name
classMeta:
verbose_name ="优惠券表"
verbose_name_plural ="优惠券表"
db_table ="coupon"# 4.用户优惠券表:user、优惠券、name、man满、jian减、开始时间、结束时间、使用状态classUserCouponModel(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.name
classMeta:
verbose_name ="用户优惠券表"
verbose_name_plural ="用户优惠券表"
db_table ="usercoupon"# 5.积分记录表classScoreRecordModel(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.descp
classMeta:
verbose_name ="积分记录表"
verbose_name_plural ="积分记录表"
db_table ="score_record"
1.展示订单信息
a.页面展示
购物车页面点击去结算后,进入订单页面
b.前端核心代码
订单页面 — 展示订单信息 src\views\Order.vue
<!-- 订单中展示课程信息 --><divclass="item"v-for="course_info in order.selected_course_list"><!-- {{course_info}} --><divclass="item-2"><router-link:to="`/project/${course_info.id}`"class="img-box l"><img:src="course_info.course.picurl"></router-link><dlclass="l has-package"><dt>【{{course_info.course.name}}】{{course_info.course.describe}} </dt></dl></div><divclass="item-3"><divclass="price"><pclass="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.订单前,信息classPreOrderView(APIView):defget(self, request):# 获取userid
userid = 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.data
return Response({"code":"200","cart":cartSer.data,"score":score,"coupon":coupons_list})
2.订单是否使用优惠券与积分
a.页面展示
b.前端核心代码
src\views\Order.vue
<!-- 优惠券/积分 --><divclass="coupons-box"><divclass="coupon-title-box"><pclass="coupon-title">
使用优惠券/积分
<spanv-if="order.use_coupon"@click="order.use_coupon=!order.use_coupon"><svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 1024 1024"data-v-394d1fd8=""><pathfill="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><spanv-else@click="order.use_coupon=!order.use_coupon"><svgxmlns="http://www.w3.org/2000/svg"viewBox="0 0 1024 1024"data-v-394d1fd8=""><pathfill="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><!-- 优惠券、积分选项卡 --><transitionname="el-zoom-in-top"><divclass="coupon-del-box"v-if="order.use_coupon"><divclass="coupon-switch-box"><divclass="switch-btn ticket":class="{'checked': order.discount_type===0}"@click="order.discount_type=0">优惠券 ({{order.coupon_list.length}})<em><iclass="imv2-check"></i></em></div><divclass="switch-btn code":class="{'checked': order.discount_type===1}"@click="order.discount_type=1">积分 ({{order.has_credit}})<em><iclass="imv2-check"></i></em></div></div><!-- 优惠券渲染 --><divclass="coupon-content ticket"v-if="order.discount_type===0"><pclass="no-coupons"v-if="order.coupon_list.length<1">暂无可用优惠券</p><divclass="coupons-box"v-else><divclass="content-box"><ulclass="nouse-box"><liclass="l":class="{select: order.select === key}"@click="order.select = (order.select === key?-1:key)"v-for="(coupon,key) in order.coupon_list":key="key"><divclass="detail-box more-del-box"><divclass="price-box"><pclass="coupon-price l"v-if="coupon.discount === '1'"> ¥{{Math.abs(coupon.sale)}} </p><pclass="coupon-price l"v-if="coupon.discount === '2'"> {{coupon.sale.replace("*0.","")}}折 </p><pclass="use-inst l"v-if="coupon.condition>0">满{{coupon.coupon_full}}元可用</p><pclass="use-inst l"v-else>任意使用</p></div><divclass="use-detail-box"><divclass="use-ajust-box">适用于:{{coupon.name}}</div><divclass="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><!-- 积分渲染 --><divclass="coupon-content code"v-else><divclass="input-box"><el-input-numberv-model="order.credit":step="1":min="0":max="order.max_use_credit"></el-input-number><aclass="convert-btn"@click="conver_credit">兑换</a><aclass="convert-btn"@click="max_conver_credit">最大积分兑换</a></div><divclass="converted-box"><p>使用积分:<spanclass="code-num">200</span></p><divv-for="course_info in order.selected_course_list"><pclass="course-title"v-if="course_info.credit>0">
课程:<spanclass="c_name">{{course_info.course.name}}</span><spanclass="discount-cash">{{course_info.credit}}积分抵扣:<em>{{parseInt(course_info.credit / order.credit_to_money)}}</em>元</span></p></div></div><pclass="error-msg">本次订单最多可以使用{{order.max_use_credit}}积分,您当前拥有{{order.has_credit}}积分。({{order.credit_to_money}}积分=1元)</p><pclass="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.前端核心代码
<!-- 选择支付方式 --><divclass="pay-type"><pclass="title">选择支付方式</p><divclass="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.订单uidclassOrderKeysView(APIView):defget(self, request):
userid = request.GET.get('userid')
uid = uuid.uuid1().hex
key =str(uid)+str(userid)
r.set_str(key,'111')return Response({"code":"200","orderuid":key})import datetime
from django.db import transaction
# 3.提交订单classOrderView(APIView):@transaction.atomic()defpost(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 =0ifint(user_coupon_id)>0:
user_coupon = UserCouponModel.objects.filter(id=user_coupon_id).first()
user_coupon.status ="已使用"
couponmoney = user_coupon.coupon_reduce #价格减少coupon_reduce
user_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 =0
pay_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_money
coupon =int(couponmoney)+int(credit)#总优惠金额if total_money > coupon:
orders.pay_money = total_money - coupon
else:
orders.pay_money =0
orders.save()#如果使用积分,更新积分记录表,更新用户表中的总积分ifint(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())+1800
r.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.pyfrom 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 json
classAliPay(object):"""
支付宝支付接口(PC端支付接口)
"""def__init__(self, appid, app_notify_url, app_private_key_path,
alipay_public_key_path, return_url, debug=False):
self.appid = appid
self.app_notify_url = app_notify_url
self.app_private_key_path = app_private_key_path
self.app_private_key =None
self.return_url = return_url
withopen(self.app_private_key_path)as fp:
self.app_private_key = RSA.importKey(fp.read())
self.alipay_public_key_path = alipay_public_key_path
withopen(self.alipay_public_key_path)as fp:
self.alipay_public_key = RSA.importKey(fp.read())if debug isTrue:
self.__gateway ="https://openapi.alipaydev.com/gateway.do"else:
self.__gateway ="https://openapi.alipay.com/gateway.do"defdirect_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)#查询接口defquery_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)#转账接口defdeposit(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)#转账接口defrefund(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)defbuild_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 isnotNone:
data["notify_url"]= self.app_notify_url
data["return_url"]= self.return_url
return data
defsign_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_string
defordered_data(self, data):
complex_keys =[]for key, value in data.items():ifisinstance(value,dict):
complex_keys.append(key)# 将字典类型的数据dump出来for key in complex_keys:
data[key]= json.dumps(data[key], separators=(',',':'))returnsorted([(k, v)for k, v in data.items()])defsign(self, unsigned_string):# 开始计算签名
key = self.app_private_key
signer = PKCS1_v1_5.new(key)
signature = signer.sign(SHA256.new(unsigned_string))# base64 编码,转换为unicode表示并移除回车
sign = encodebytes(signature).decode("utf8").replace("\n","")return sign
def_verify(self, raw_content, signature):# 开始计算签名
key = self.alipay_public_key
signer = PKCS1_v1_5.new(key)
digest = SHA256.new()
digest.update(raw_content.encode("utf8"))if signer.verify(digest, decodebytes(signature.encode("utf8"))):returnTruereturnFalsedefverify(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.pyfrom.pay import AliPay
defget_alipay():# 初始化支付实例# 公共参数# 沙箱环境地址:https://openhome.alipay.com/platform/appDaily.htm?tab=info
app_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.支付宝接口 支付
classPayView(APIView):
def post(self, request):
# 获取订单号
orderno =10
pay_money =100
pay =get_alipay()
query_params = pay.direct_pay(
subject="订单支付", # 商品简单描述
out_trade_no=str(orderno), # 用户购买的商品订单号(每次不一样) 20180301073422891
total_amount=float(pay_money), # 交易金额(单位: 元 保留俩位小数))
payUrl ="https://openapi-sandbox.dl.alipaydev.com/gateway.do?{0}".format(query_params)returnResponse({"url":payUrl})
def get(self, request):
# 获取参数
data = request.GET
print("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 = True
if flag == True:
# 通过订单号查询订单表
orders = OrdersModel.objects.filter(orderno=orderno).first()
orders.status=2 #状态
orders.orders_status =2 #支付状态
orders.transaction_no = transaction_no
orders.save()
# 把成功的加入到成功队列中
r.list_push("ordersuccess",orderno)returnHttpResponseRedirect("http://127.0.0.1:3000/")else:
orders = OrdersModel.objects.filter(orderno=orderno).first()
orders.status=3
orders.orders_status =3
orders.transaction_no = transaction_no
orders.save()
# 把成功的加入到成功队列中
r.list_push("orderfail",orderno)returnHttpResponseRedirect("http://127.0.0.1:3000/error")
d.操作流程及展示
1.获取url支付链接地址,为了测试方便这里使用postman
2.点击链接地址,跳转到支付宝平台
3.使用沙箱测试账号登录,进入支付页面
4.点击付款
5.完成后,自动跳转平台支付成功的地址
6.取消订单⭐
*** 通过celery设置定时任务,定时检查订单状态。在订单超时未支付时,修改订单状态为取消订单状态。***
参考:【Django+Vue3 线上教育平台项目实战】Celery赋能:优化订单超时处理与自动化定时任务调度
版权归原作者 么凹猫' 所有, 如有侵权,请联系我们删除。