订单模型
订单这一块应该是一个单独的子应用(为了方便开发,和以后项目的维护),当用户点击下单之后,我们应该把订单的信息保存下来,方便用户取消掉当次结算之后的半小时内(时间自己定)依然能够继续支付。
订单模型分析
根据各种情况分析,我们的订单模型应该有如下字段: 订单模型: 优惠券ID,积分兑换数量,订单总价格,订单标题,订单支付时间,用户ID,订单状态,订单有效时间,订单号,支付方式, 订单详情模型: 商品ID,商品原价,商品实价,商品有效期,商品优惠方式
原因是支付平台需要记录每一个商家的资金流水,所以需要我们这边提供一个足够复杂的流水号和支付平台保持一致。所以订单号是支付平台那边强制要求在支付时提供给平台的。订单号是唯一的,因为第三方支付平台依靠这个订单号来识别当前订单是否已经支付过的。
对应的模型代码如下:
from django.db import models
from luffyapi.utils.models import BaseModel
from users.models import User
from course.models import Course
class Order(BaseModel):
"""订单模型"""
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超时取消'),
)
pay_choices = (
(0, '支付宝'),
(1, '微信支付'),
)
order_title = models.CharField(max_length=150,verbose_name="订单标题")
total_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="订单总价", default=0)
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="实付金额", default=0)
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="支付方式")
credit = models.IntegerField(default=0, verbose_name="使用的积分数量")
coupon = models.IntegerField(null=True, verbose_name="用户优惠券ID")
order_desc = models.TextField(max_length=500, null=True, blank=True, 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,verbose_name="下单用户")
class Meta:
db_table="ly_order"
verbose_name= "订单记录"
verbose_name_plural= "订单记录"
def __str__(self):
return "%s,总价: %s,实付: %s" % (self.order_title, self.total_price, self.real_price)
class OrderDetail(BaseModel):
"""
订单详情
"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="订单ID")
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="课程ID")
expire = models.IntegerField(default='0', verbose_name="有效期周期",help_text="0表示永久有效")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
discount_name = models.CharField(max_length=120,default="",verbose_name="优惠类型")
class Meta:
db_table="ly_order_detail"
verbose_name= "订单详情"
verbose_name_plural= "订单详情"
def __str__(self):
return "%s" % (self.course.name)
记得在模型定义好之后进行数据迁移哟~
xadmin
想要在xadmin中管理模型,必须有以下步骤:
- 在新建的子应用中的apps.py下继承AppConfig:
from django.apps import AppConfig
class OrderConfig(AppConfig):
name = 'order'
verbose_name="订单管理"
其中的verbose_name就是指定在xadmin中显示的内容
- 在子应用的__init__.py中声明default_app_config:
default_app_config = "order.apps.OrderConfig"
- 在adminx.py中注册模型:
import xadmin
from .models import Order
class OrderModelAdmin(object):
"""订单模型管理类"""
pass
xadmin.site.register(Order, OrderModelAdmin)
生成订单
生成订单流程
- 视图中定义订单序列化器、queryset
- 在订单序列化器中验证数据:验证从客户端传过来的参数是否正确,只有正确才返回(重写ModelSerializer的validate方法)
- 生成唯一订单号
- Order.objects.create生成订单信息
- Order.objects.create生成订单详情信息
- 第3、4、5步需要需要保证数据库的原子性,需要使用到数据库的事务处理
如何生成一个唯一的订单号?
前面说了,我们要是使得订单号唯一。可以使用用户的id和一个随机数以及当前的时间共同生成一个订单号,这个随机数可以用redis中的自增数来代替,代码如下:
user_id = self.context["request"].user.id
incr = int( redis_conn.incr("order") )
order_number = datetime.now().strftime("%Y%m%d%H%M%S") + "%06d" % user_id + "%06d" % incr
其中redis_conn.incr("order") 表示,order这个变量每被调用一次便会加一。
为什么要使用数据库事务处理生成订单?
-
原因 在生成订单过程中,有可能会出现一些错误,比如在生成订单详情的时候,一个用户已经生成订单了,打算买商品A,但是刚好另外一个用户把商品A给买了,而且仓库没有存货了,这个时候查库会出现错误,那么我们应该把前面已经生成好的订单写进数据库吗?显然,是不能够的。所以,这就牵扯到了事务以及事务的原子性了。具体的参见另外的博客-事务的ACID -
代码实现 在 django等web框架中,只要ORM模型,一般都会实现了事务操作封装 所以在django中我们可以直接使用ORM模型提供的事务操作方法即可完成事务的操作,可以用with transation.atomic(),如下:
from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
def post(self,request):
....
with transation.atomic():
sid = transation.savepoint()
....
try:
....
except:
transation.savepoint_rallback(sid)
还可以用装饰器:
from django.db import transaction
from rest_framework.views import APIView
class OrderAPIView(APIView):
@transaction.atomic
def post(self,request):
....
发起支付
支付流程
查阅支付宝官方文档,给出的支付流程时序图如下: 但是,我们是前后端分离开发的,所以时序图有所不同: 代码部分就是要实现上述的流程
安装第三方支付SDK
pip install python-alipay-sdk --upgrade
生成公私钥🔑
生成公私钥存放在子应用的keys下存放,使用下面的命令:
openssl
OpenSSL> genrsa -out app_private_key.pem 2048
OpenSSL> rsa -in app_private_key.pem -pubout -out app_public_key.pem
OpenSSL> exit
保存支付宝公钥
在自己支付宝应用之查看支付宝的公钥,保存下来,同样放在keys文件夹下。
配置支付宝
在dev下,存放支付宝的一些配置参数:
ALIAPY_CONFIG = {
"gateway_url": "https://openapi.alipaydev.com/gateway.do?",
"appid": "2021000119676831",
"app_notify_url": None,
"app_private_key_path": "-----BEGIN RSA PRIVATE KEY----- \n MIIEowIBAAKCAQEApaxiK5J6MhA7sihze0CQDWX53hFXmwh2PFjEXPRVEQj5vPWjauFMGYgD74S/iv/uCKFjDubQK0oZnvYDSZZ2gGywhIDzUT+JsLzPF4xcii2TCfZdnoXMnceAK46j0cXj7WkeG/ih6cv74IBPXBs9BGsNFDMrSzeRdRYQKmhI2ZDl0A07KflbmPcnCp2Ki2Yr9/hUcBIv6e7V5T2/qaU5ViK8OIV5GiJnEOLqeJ6bev0RZrBD7wOcfwDKRmWWRJim8fMJ0DuHklIMnLr583lusGdHP8Tv1uZUwoLRxAAKDag7y5MEEWUuCBCGDps05ZE2T/jiO3z806E+Db0bYqs+9wIDAQABAoIBABdBJhp1mf9S9f4g3WzMeCvZR4RbLM/PFUGNOCrQBOy6NMvIMuL6ssLWq67AFF6/Y7sEZDIgRdH9ubDtWTLIEuQd3X0L2Jtr7rjHF853XjnrAbuhQCzTHRB8g0oZ4Ha+byrQ2XAVqyx0/Grbt8f27s6i9BgEwqvxJdKogSZisu8OxMJjQoeBiPQ9imbiJwd1sVCDt/3Odod9F5pqN0rGqVM+ensocaguYXizOmxzrIYYO6/OhzmL9I5WkyDfYNkHyOtFpo7LPVHRodM5rE+CJE5ljO6hEgpAWl6aK7haRG98C0T8IDJtFwzm/mafzX+TqwtUG7JBCvLRTjXyacePPiECgYEA19fv4IXL1AvMDDtBcbb3gAYLtiN05wLkXotKzxx41KwJ+uxcm0WHovMn4K7F1gI8b7SQ6xBAya1dnJ1n9C6kHjj+6nehDTQoK10BqYgw21lX4+ey5c+R9lYT/rM/No04K/zOwI4LyUjKi9a423vlYD7iJxeTcvvUYbEPC0US8ccCgYEAxH75WHMHpCqAGtN2iX4reHu0jnMsQuuryf1d1uSrX+iF2lNz9GHrc8yPu5HaFHwnhZ13nsZZkinoYFoxQ5EU2FcI8X/K3qJuA/Sux6JJ//9uIxTkEx6q0Q2MZdl1BowVtRyfG1ce6UwkoTQfH+hC4BXX5GaZG09Dd7mVf1ltSVECgYAFNqsNok21Fn/cP8Yp6AB8cCjyQlL9jX1Up0qsTATDJlCrZfAqs/g9wF/TNoWC0NUC4bqqYt8dv/lF4itzo3jFXh0SLseOGRdrTLjQjoCXm8XatGsG0Ae3ioa7HTtGpwyXS2j3D7dLKl3yGMKoUorM01gFF7WxDlIn9e3mGMoHWwKBgAK2oijXhc59i8FTFDr31A/Y3XhuHLlb2cZzSj1ycO9lHZwTNPG1/HNf6sgzTmikAkO1FfbKVGhRTdUuyaleMoR+RzBWjG2gnpZNZbB6DD2NwG6ZlkCxVriGUF8DrrsFajNTDttoy5N7KrJTLu7Y3TuM8atEw25+HLdlh6v3hyvRAoGBALB3GpxK3LvV9GIZFryFi4NClGvAABcD4vbZ6xnd8WfHu5QhYzJYPeOnH0kA7MXsUjVUK1GxmakFASf2EzmtereL1FRtyABRP7xRz4PfuChHDZdqtHAKBSxAttriSJRhNH9jqUI1XBUq1087aFnFahaLdpP70pq8zz/RMWRLIfa+ \n -----END RSA PRIVATE KEY-----",
"alipay_public_key_path": "-----BEGIN PUBLIC KEY----- \n MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsZHsoaRqjEUcUOK5vw94v+RTfMHkUt0Z3Re31A0jR1jxpmaBcLV7im8FQTFVX04kFKc9qYTY2HQ8ouMEWYpkcsfdPId+GMH6BaZarK13BNlecvOhS8th6KOGjgHV2IiC48OkKMe1ojmPidhkYXCSUXptdGAkxfWaZi8q2m41DxjwH4IbYN1r/MTP1dwd4h+owoYmSjydpeV2JvL3AKuDYxAa3cXjYMxfeD5AgQ7j5OUwYeZ+S6bjjnyd3mtvQbQ3F09k/0TlqZFRiROdMIskUJKSpWPGnKucvwWqze8uDYSEMxCM0w0BYUbZuediz13nEcXGVShXDx9ircvbRFWX5QIDAQAB \n -----END PUBLIC KEY-----",
"sign_type": "RSA2",
"debug": False,
"return_url": "http://www.luffycity.cn:8080/payments/result",
"notify_url": "http://api.luffycity.cn:8000/payments/result",
}
注意??!这里我放的是钥匙的具体内容,因为不同版本的python-alipay-sdk初始化的时候不太一样,后面会有个bug解决的地方。 非对称加密也值得用一篇单独的博客来讲讲
发起支付
使用刚才安装的第三方支付SDK中的AliPay,初始化之,我们便可以发起一个支付请求了:
from rest_framework.views import APIView
from alipay import AliPay
from django.conf import settings
from rest_framework.response import Response
from order.models import Order
from rest_framework import status
class AlipayAPIView(APIView):
def get(self,request):
"""获取支付宝的支付地址"""
order_number = request.query_params.get("order_number")
try:
order = Order.objects.get(order_number=order_number)
except Order.DoesNotExist:
return Response({"message":"对不起,订单不存在!"}, status=status.HTTP_400_BAD_REQUEST)
alipay = AliPay(
appid=settings.ALIAPY_CONFIG["appid"],
app_notify_url=settings.ALIAPY_CONFIG["app_notify_url"],
app_private_key_path=settings.ALIAPY_CONFIG["app_private_key_path"],
alipay_public_key_path=settings.ALIAPY_CONFIG["alipay_public_key_path"],
sign_type=settings.ALIAPY_CONFIG["sign_type"],
debug = settings.ALIAPY_CONFIG["debug"]
)
order_string = alipay.api_alipay_trade_page_pay(
out_trade_no=order.order_number,
total_amount=float(order.real_price),
subject=order.order_title,
return_url=settings.ALIAPY_CONFIG["return_url"],
notify_url=settings.ALIAPY_CONFIG["notify_url"]
)
url = settings.ALIAPY_CONFIG["gateway_url"] + order_string
return Response(url)
bug!!!!
-
RSA key format is not supported 这个就是不同版本的python-alipay-sdk中的AliPay初始方法不同,点进源码看看就清楚了!一个是存放路径,一个是要直接的key。 -
进入支付页面的时候显示钓鱼风险(FISH_RISK) 因为打开的页面太多了,有两种解决方法:1、用无痕浏览打开;2、关闭掉不想关的页面。
同步异步返回结果
- 同步返回:即用户正常支付,没有出现意外,支付成功后,对课程、用户等信息要做相应的操作,而且得保证事务的原子性,操作完跳转到成功页面。
- 异步返回:用户在支付过程中出现意外情况导致支付流程不正正常进行,则进入异步处理方式,查询支付状态,做相应的调整。
代码如下:
from django.http.response import HttpResponse
class AliPayResultAPIView(APIView):
def get(self,request):
"""处理支付宝同步通知结果"""
alipay = AliPay(
appid=settings.ALIAPY_CONFIG["appid"],
app_notify_url=settings.ALIAPY_CONFIG["app_notify_url"],
app_private_key_string=settings.ALIAPY_CONFIG["app_private_key_path"],
alipay_public_key_string=settings.ALIAPY_CONFIG["alipay_public_key_path"],
sign_type=settings.ALIAPY_CONFIG["sign_type"],
debug = settings.ALIAPY_CONFIG["debug"]
)
data = request.query_params.dict()
signature = data.pop("sign")
success = alipay.verify(data, signature)
if success:
return self.change_order_status(data)
return Response({"message":"对不起,当前订单支付失败!"})
def post(self,request):
"""处理支付宝异步通知结果"""
alipay = AliPay(
appid=settings.ALIAPY_CONFIG["appid"],
app_notify_url=settings.ALIAPY_CONFIG["app_notify_url"],
app_private_key_string=settings.ALIAPY_CONFIG["app_private_key_path"],
alipay_public_key_string=settings.ALIAPY_CONFIG["alipay_public_key_path"],
sign_type=settings.ALIAPY_CONFIG["sign_type"],
debug = settings.ALIAPY_CONFIG["debug"]
)
data = request.data
signature = data.pop("sign")
success = alipay.verify(data, signature)
if success and data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED" ):
response = self.change_order_status(data)
if "credit" in response.data:
return HttpResponse("success")
return Response({"message":"对不起,当前订单支付失败!"})
def change_order_status(self,data):
order_number = data.get("out_trade_no")
try:
order = Order.objects.get(order_number=order_number, order_status=0)
except Order.DoesNotExist:
return Response({"message": "对不起,支付结果查询失败!订单不存在!111"}, status=status.HTTP_400_BAD_REQUEST)
with transaction.atomic():
save_id = transaction.savepoint()
try:
order.pay_time = datetime.now()
order.order_status = 1
order.save()
credit = order.credit
if credit > 0:
user = order.user
user.credit -= credit
user.save()
order_detail_list = order.order_courses.all()
course_list = []
for order_detail in order_detail_list:
"""循环本次订单中所有购买的商品课程"""
course = order_detail.course
course.students += 1
course.save()
course_list.append({
"id": course.id,
"name": course.name
})
except:
log.error("订单结果处理出现故障!无法修改订单相关记录的状态")
transaction.savepoint_rollback(save_id)
return Response({"message": "对不起,更新订单相关记录失败!"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({
"message": "支付成功!",
"pay_time":order.pay_time,
"real_price":order.real_price,
"course_list": course_list
})
|