商城支付功能
一、支付接口
1、支付接口路由
创建LuffyCity/shopping/payment_view.py文件,在 shopping/urls.py 中添加支付接口路由:
from django.urls import path from .views import ShoppingCarView from .settlement_view import SettlementView from .payment_view import PaymentView urlpatterns = [ path('shopping_car', ShoppingCarView.as_view()), # 购物车 path('settlement', SettlementView.as_view()), # 结算中心 path('payment', PaymentView.as_view()) # 支付接口 ]
2、支付接口视图——获取和校验贝里
前端提交给接口的数据,分析可知:不需要提供课程,只需要提供价格、抵扣贝里(积分)信息即可。
from rest_framework.views import APIView from rest_framework.response import Response from utils.my_auth import LoginAuth from utils.base_response import BaseResponse class PaymentView(APIView): authentication_classes = [LoginAuth, ] # 登录认证 def post(self, request): res = BaseResponse() # 1.获取数据 balance = request.data.get("balance", 0) # 贝里(积分),不传默认为0 price = request.data.get("price", "") # 价格,不传默认为空字符串 user_id = request.user.pk # 2.校验数据的合法性 # 2.1 校验贝里数是否合法 if int(balance) > request.user.balance: # 如果传来的贝里大于用户数据库中贝里 res.code = 1070 res.error = "抵扣的贝里错误" return Response(res.dict)
3、支付接口——redis与数据库中数据校验
获取前端传递来的数据后,需要校验数据的合法性。除了校验抵扣积分和价格之外。还要将redis中的数据和数据库中的数据进行比对。因为在加入结算中心的时候和进行支付具有一定的时间差。这个时间差会导致课程下架、优惠券过期等情况,为防止这种情况,必须让redis和数据库数据进行比对。
from rest_framework.views import APIView from rest_framework.response import Response from utils.my_auth import LoginAuth from utils.base_response import BaseResponse from .settlement_view import SETTLEMENT_KEY from utils.redis_pool import POOL # redis连接池 import redis from Course.models import Course from .models import Coupon from django.utils.timezone import now # 拿时区当前时间 CONN = redis.Redis(connection_pool=POOL) # 创建连接,指定redis连接池 class PaymentView(APIView): authentication_classes = [LoginAuth, ] # 登录认证 def post(self, request): res = BaseResponse() # 1.获取数据 balance = request.data.get("balance", 0) # 贝里(积分),不传默认为0 price = request.data.get("price", "") # 价格,不传默认为空字符串 user_id = request.user.pk # 2.校验数据的合法性 # 2.1 校验贝里数是否合法 if int(balance) > request.user.balance: # 如果传来的贝里大于用户数据库中贝里 res.code = 1070 res.error = "抵扣的贝里错误" return Response(res.dict) # 2.2 从用户的结算中心拿数据,与数据库比对是否合法 settlement_key = SETTLEMENT_KEY % (user_id, "*") # 课程id未知,用*代替 all_keys = CONN.scan_iter(settlement_key) # 得到所有的键 for key in all_keys: settlement_info = CONN.hgetall(key) # 结算中心信息 # 2.2.1 比对课程id是否合法 course_id = settlement_info["id"] course_obj = Course.objects.filter(id=course_id).first() # 拿到课程对象 if not course_obj or course_obj.status ==1: # status为1,课程下架 res.code = 1071 res.error = "课程id不合法" return Response(res.dict) # 2.2.2 校验优惠券是否过期 course_coupon_id = settlement_info.get("default_coupon_id", 0) # 用get取值,避免为空报错 if course_coupon_id: coupon_dict = Coupon.objects.filter( id=course_coupon_id, # 拿到优惠券对象 couponrecord__status=0, # 必须是未使用的 couponrecord__account_id=user_id, # 必须是当前用户的 object_id=course_id, # 必须是当前课程的 valid_begin_date__lte=now(), # 符合使用有效期 valid_end_date__gte=now() ).values( # 不拿对象,取如下数据 "coupon_type", # 优惠券类型 "money_equivalent_value", # 等值货币 "off_percent", # 折扣比 "minimum_consume" # 最小货币 ) if not coupon_dict: # 没有说明优惠券不合法 res.code = 1072 res.error = "优惠券不合法" return Response(res.dict)
4、价格计算
校验价格前,先定义好价格计算方法。
class PaymentView(APIView): authentication_classes = [LoginAuth, ] # 登录认证 def post(self, request):... def account_price(self, coupon_dict, price): """ 价格计算 :param coupon_dict: 优惠券信息 :param price: 课程价格 :return: """ coupon_type = coupon_dict["coupon_type"] if coupon_type == 0: # 通用优惠券 money_equivalent_value = coupon_dict["money_equivalent_value"] # 等值货币 if price - money_equivalent_value >= 0: rebate_price = price - money_equivalent_value else: rebate_price = 0 # 支付金额不能为负数 elif coupon_type == 1: # 满减优惠券 money_equivalent_value = coupon_dict["money_equivalent_value"] # 等值货币 minimum_consume = coupon_dict["minimum_consume"] # 最小限额 if price >= minimum_consume: # 满足满减要求 rebate_price = price - money_equivalent_value else: return -1 # 不满足,抛出异常 elif coupon_type == 2: # 折扣优惠券 minimum_consume = coupon_dict["minimum_consume"] # 最小限额 off_percent = coupon_dict["off_percent"] # 折扣 if price >= minimum_consume: # 满足折扣要求 rebate_price = price * (off_percent / 100) # 乘以折扣 else: return -1 return rebate_price # 返回待支付价格
5、校验价格
class PaymentView(APIView): authentication_classes = [LoginAuth, ] # 登录认证 def post(self, request): res = BaseResponse() # 1.获取数据 balance = request.data.get("balance", 0) # 贝里(积分),不传默认为0 price = request.data.get("price", "") # 价格,不传默认为空字符串 user_id = request.user.pk # 2.校验数据的合法性 # 2.1 校验贝里数是否合法 if int(balance) > request.user.balance: # 如果传来的贝里大于用户数据库中贝里 res.code = 1070 res.error = "抵扣的贝里错误" return Response(res.dict) # 2.2 从用户的结算中心拿数据,与数据库比对是否合法 settlement_key = SETTLEMENT_KEY % (user_id, "*") # 课程id未知,用*代替 all_keys = CONN.scan_iter(settlement_key) # 得到所有的键 course_rebate_total_price = 0 # 初始化总价为0 for key in all_keys: settlement_info = CONN.hgetall(key) # 结算中心信息 # 2.2.1 比对课程id是否合法 course_id = settlement_info["id"] course_obj = Course.objects.filter(id=course_id).first() # 拿到课程对象 if not course_obj or course_obj.status ==1: # status为1,课程下架 res.code = 1071 res.error = "课程id不合法" return Response(res.dict) # 2.2.2 校验优惠券是否过期 course_coupon_id = settlement_info.get("default_coupon_id", 0) # 用get取值,避免为空报错 if course_coupon_id: coupon_dict = Coupon.objects.filter( id=course_coupon_id, # 拿到优惠券对象 couponrecord__status=0, # 必须是未使用的 couponrecord__account_id=user_id, # 必须是当前用户的 object_id=course_id, # 必须是当前课程的 valid_begin_date__lte=now(), # 符合使用有效期 valid_end_date__gte=now() ).values( # 不拿对象,取如下数据 "coupon_type", # 优惠券类型 "money_equivalent_value", # 等值货币 "off_percent", # 折扣比 "minimum_consume" # 最小限额 ) if not coupon_dict: # 没有说明优惠券不合法 res.code = 1072 res.error = "优惠券不合法" return Response(res.dict) # 2.3 校验价格price # 2.3.1 得到所有的课程折后价格之和 course_price = settlement_info["price"] course_rebate_price = self.account_price(coupon_dict, course_price) if course_rebate_price == -1: # coupon_type有问题 res.code = 1074 res.error = "课程优惠券不符合要求" return Response(res.dict) course_rebate_total_price += course_rebate_price # 得到课程所有折后价格之后 # 2.3.2 校验全局优惠券是否合法 global_coupon_key = GLOBAL_COUPON_KEY % user_id global_coupon_id = int(CONN.hget(global_coupon_key, "default_global_coupon_id")) # 转整数 if global_coupon_id: global_coupon_dict = Coupon.objects.filter( id=global_coupon_id, couponrecord__status=0, couponrecord__account_id=user_id, valid_begin_date__lte=now(), valid_end_date__gte=now() ).values( "coupon_type", # 优惠券类型 "money_equivalent_value", # 等值货币 "off_percent", # 折扣比 "minimum_consume" # 最小限额 ) if not global_coupon_dict: res.code = 1073 res.error = "全局优惠券id不合法" return Response(res.dict) # 2.3.3 与全局优惠券做折扣 global_rebate_price = self.account_price(global_coupon_dict, course_rebate_total_price) if global_rebate_price == -1: res.code = 1076 res.error = "全局优惠券不符合要求" return Response(res.dict) # 2.3.4 折扣贝里 balance_money = balance / 100 balance_rebate_price = global_rebate_price - balance if balance_rebate_price < 0: balance_rebate_price = 0 # 抵扣完贝里不会为负 # 2.3.5 终极校验price if balance_rebate_price != price: res.code = 1078 res.error = "价格不合法" return Response(res.dict)
拿到价格后,就可以创建订单了,此时的订单状态是未支付状态。
然后调用支付宝接口来支付了,如果支付成功支付宝接口将返回回调。收到回调将改变订单状态。
注意:如果支付多个课程,订单详情表需要有多个记录,更改优惠券的使用状态、更改用户表的贝里、贝里记录表需添加交易记录。
二、支付宝支付
之前曾用过支付宝沙箱环境,博客地址:支付宝支付
查看沙箱环境支付宝API:https://docs.open.alipay.com/api
选择沙箱环境统一收单下单并支付页面接口:https://docs.open.alipay.com/api_1/alipay.trade.page.pay
之前支付宝没有Python的sdk,都是使用的github开源项目,现在有蚂蚁金服开放平台的官方SDK:https://pypi.org/project/alipay-sdk-python/