class User(AbstractUser):
"""用户模型类"""
mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
class Meta:
db_table = 'tb_users'
verbose_name = '用户'
verbose_name_plural = verbose_name # 去掉s
AUTH_USER_MODEL = 'users.User'
AUTH_USER_MODEL
参数的设置以点.
来分隔,表示应用名.模型类名
。
注意:Django建议我们对于AUTH_USER_MODEL参数的设置一定要在第一次数据库迁移之前就设置好,否则后续使用可能出现未知错误。
python manage.py makemigrations
python manage.py migrate
管理器方法即可以通过User.objects.
进行调用的方法。
create_user
(username, email=None, password=None, *extra_fields) # 会自动给用户的密码进行加密*
创建、保存并返回一个User
对象。
create_superuser
(username, email, password, **extra_fields)
与create_user()
相同,但是设置is_staff
和is_superuser
为True
。
set_password
(raw_password)
设置用户的密码为给定的原始字符串,并负责密码的。 不会保存User
对象。当None
为raw_password
时,密码将设置为一个不可用的密码。
check_password
(raw_password) # 检验密码是否正确
如果给定的raw_password是用户的真实密码,则返回True,可以在校验用户密码时使用。
# 自关联
class Area(models.Model):
name = models.CharField(verbose_name="名称", max_length=20)
parent = models.ForeignKey("self", on_delete=models.SET_NULL, related_name="subs", verbose_name="上级行政区", null=True,blank=True)
# 自关联,外键属性设置在多方,on_delete表示是否在删除一方时。多方的数据是否也删除
# realted_name = "subs" ,表示反属性,等于 多类小写_set
# 这个self表示自关联,一般为类名,有的时候还会是users.User,表示应用名.类名
class Meta:
db_table = 'tb_users' # 表名
verbose_name = '用户' # 显示用户表的表名
verbose_name_plural = verbose_name # 去掉s
from rest_framework import serializers
# 第一种方法:
class UserAddBrowseHistorySerializer(serializers.Serializer):
"""历史浏览添加序列化器"""
sku_id = serializers.IntegerField(label='商品SKU编号', min_value=1)
# 第二种方法
class UserAddressSerializer(serializers.ModelSerializer):
"""
用户地址序列化器
"""
province = serializers.StringRelatedField(read_only=True)
city = serializers.StringRelatedField(read_only=True)
district = serializers.StringRelatedField(read_only=True)
province_id = serializers.IntegerField(label='省ID', required=True)
city_id = serializers.IntegerField(label='市ID', required=True)
district_id = serializers.IntegerField(label='区ID', required=True)
class Meta:
model = Address # 指定模型类
exclude = ('user', 'is_deleted', 'create_time', 'update_time') # 去掉这些字段
# fields = ("id","user",...) 序列化这些字段
fields = "__all__" # 表示所有
# extra_kwargs = { "字段":{ "选项名":值} }
# extra_kwagrs = {"user":{"read_only":True}}
# read_only_fields = ('id', 'bread', 'bcomment') 这些字段都设置成read_only = True
# 注意:如果数据只用来序列化或反序列化,就不需要去考虑,在里面添加read_only(序列化,json->obj)和write_only(反序列化,obj-json)
# 第一种 多方模型类小写_set
# PrimaryKeyRelatedField,例如,关联属性名 要写成 多方模型类小写_set
heroinfo_set = serializers.PrimaryKeyRelatedField(label="英雄", many=True, read_only=True)
# 得到的都是英雄对应的id,例如:"heroinfo_set": [1,2,3,4,5]
# 第二种
heroinfo_set = HeroInfoSerializre(many=True) # 该类为多方的序列化器类
# 得到的是英雄序列化器类的序列化的信息,需要添加 many = True
# 第三种
# (__str__方法的返回值)单方模型类里的
heroinfo_set = serializers.StringRelatedField(label="英雄", many=True)
"heroinfo_set": ["郭靖","黄蓉","黄药师","欧阳锋","梅超风"]
# 第一种
# PrimaryKeyRelatedField,此字段将被序列化为关联对象的主键。得到关联属性的id,得到的"hbook":1
hbook = serializers.PrimaryKeyRelatedField(label='图书', read_only=True)
或
hbook = serializers.PrimaryKeyRelatedField(label='图书', queryset=BookInfo.objects.all())
# 第二种
# 使用关联对象的序列化器,得到的是图书序列化器类的序列化的信息
hbook = BookInfoSerializer() # 该类为单方的序列化器类
# 得到的"hbook"为:
"hbook": {
"id": 1,
"btitle": "\u5c04\u96d5\u82f1\u96c4\u4f20",
"bpub_date": "1980-05-01",
"bread": 12,
"bcomment": 34,
"image": null
}
# 第三种
3) StringRelatedField
# 此字段将被序列化为关联对象的字符串表示方式(即__str__方法的返回值)
hbook = serializers.StringRelatedField(label="书籍")
# 模型类
"hbook": "射雕英雄传"
# 第一种
# validators,在字段中添加validators选项参数,也可以补充验证行为
btitle = serializers.CharField(label='标题', max_length=20, validators=[check_Django])
# 函数名,在上面定义函数 def check_Django().....
# 第二种
# validate_,指定函数名,例如,在图书序列器类里定义一个函数,
class BookInfoSerializer(serializers.Serializer):
"""图书数据序列化器"""
pass
def validate_btitle(self, value): # value为前端传过来的数据,即为bittle的数据
pass
# 第三种
# validate,例如,在序列化器中需要同时对多个字段进行比较验证时,可以定义validate方法来验证
class BookInfoSerializer(serializers.Serializer):
"""图书数据序列化器"""
pass
def validate(self, attrs): # attrs接收传过来的字典数据,即为request.data
pass
# 当条件不满足时抛出异常:raise serializers.ValidationError("图书不是django")
# 1.先查询出一个图书对象
book = BookInfo.objects.get(id=2)
# 2.构造序列化器对象
serializer = BookInfoSerializer(book)
# 3.获取序列化数据
return Response(serializer.data)
# 注意: many = True
# 如果要被序列化的是包含多条数据的查询集QuerySet,可以通过添加many=True参数补充说明,序列化类里面添加
book_qs = BookInfo.objects.all()
serializer = BookInfoSerializer(book_qs, many=True)
serializer.data
def post(self, request):
"""新建一本图书信息"""
# 1.获取参数
# 前端传递参数时,使用json传递
# 获取参数(btitle, bpub_date)并进行参数校验
req_data = request.body.decode() # bytes,经过decode变成字符串
# 将json字符串-> python字典
req_dict = json.loads(req_data)
# 2.创建反序列化对象
serializer = BookInfoSerializer(data=req_dict, many=True)
# 3.参数校验
serializer.is_valid(raise_exception=True)
# 4.反序列化-数据保存
serializer.save()
# 5.返回json数据
return JsonResponse(serializer.data, status=201)
# 调用create()方法 增加,只传递数据
serializer = BookInfoSerializer(data=data) # 没有传对象
serializer.save() # create() 该序列器类 BookInfoSerializer里的方法
# 该方法定义在序列化器类里
def create(self, validated_data): # 接受校验后的数据,即为字典,所以只需要把字典解包就可以了
# print("调用了create方法")
# 创建图书对象
book = BookInfo.objects.create(**validated_data) # 解包
return book
# 调用update()方法, 更新,需要传递对象和数据
# 获取id为1的图书对象
book = BookInfo.objects.get(id=1)
# 创建图书序列化器类对象
serializer = BookInfoSerializer(book, data=data)
serializer.save() # update() 该序列器类 BookInfoSerializer里的方法
def update(self, instance, validated_data):
# print("调用了update方法")
# 更新对象,instance为图书的对象
instance.bpub_date = validated_data.get("bpub_date")
instance.btitle = validated_data.get("btitle")
# 模型类里的save(),添加和更新都要这个操作,这个是用对象来操作添加和更新
instance.save()
return instance # 返回图书对象
"""
1) 在对序列化器进行save()保存时,可以额外传递数据,这些数据可以在create()和update()中的validated_data参数获取到
2)默认序列化器必须传递所有required的字段,否则会抛出验证异常。但是我们可以使用partial参数来允许部分字段更新,有的字段就可以不用传
# Update `comment` with partial data
serializer = BookInfoSerializer(book, data=data, partial=True) # 部分更新
"""
# 1. 接收参数selected并进行校验(必传)
serializer = CartSelectAllSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
# 获取参数,序列化后的数据
selected = serializer.validated_data['selected']
# 2. 获取user并进行处理
try:
# user是被@property装饰的函数
user = request.user
except Exception as e:
user = None
redis的优点:从redis中存取数据,效率要比mysql数据库要快的多
redis里面存取的数据类型:
# 注意:从redis里取出的数据都为bytes类型,需要对其进行编译decode()
# string: 字符串
'history_' : <value>
history_1: '1,2,3'
history_2:
# hash: 哈希 value: 属性:值 key: :
history: {
history_<user_id>: <value>,
}
# list: 列表 key:[,]
'history_' : [1, 2, 3]
# set: 无序集合 key:(,)
排除
# zset: 有序集合
有序集合中的每个元素都有一个权重。
# 注意:查询redis里的数据的时候不用管道,修改redis里数据的时候用管道
def create(self, validated_data):
"""在redis中保存用户浏览的记录"""
# 获取登录用户user
user = self.context['request'].user
# context为序列化器类对象里的一个属性,而且是一个字典
# 获取redis链接 history为配置里redis存储的位置
redis_conn = get_redis_connection('history') # type:StrictRedis
history_key = 'history_%s' % user.id
# 创建redis管道对象
pl = redis_conn.pipeline()
# 去重:如果用户已经浏览过该商品,先将商品id从列表中移除。
sku_id = validated_data['sku_id']
# lrem(key, count, value): 如果value存在直接移除,不存在就忽略
pl.lrem(history_key, 0, sku_id) # 操作redis里的方法
# 左侧加入: 保持浏览顺序
# lpush(key, *values): 向列表左侧加入元素
pl.lpush(history_key, sku_id)
# 截取: 只保留最近几条浏览记录。
# ltrim(key, start, end): 截取列表元素,只保留指定区间内的元素
pl.ltrim(history_key, 0, constants.USER_BROWSING_HISTORY_COUNTS_LIMIT-1)
# 执行管道命令
pl.execute()
# 返回
return validated_data
注意:从redis里取出的值都为bytes类型
hash命令: 格式: key : { }
1)hincrby(key, field, value)
# 给hash中field属性值累加一个value,如果field不存在,新建一个属性和值,例如
# 如果用户的购物车记录中已经添加过该商品,数量需要进行累加
# 如果用户的购物车记录中没有添加过该商品,设置一个新的属性和值
# hincrby(key, field, value): 给hash中field属性值累加一个value,如果field不存在,新建一个属性和值
redis_conn.hincrby(cart_key, sku_id, count) # sku_id商品的id,count商品的数量
2)hgetall(key)
# 返回hash中所有属性和值
cart_redis = redis_conn.hgetall(cart_key) # dict
3)hset(key, field, value)
# hset(key, field, value): 将hash中指定属性field的值设置为value
pl.hset(cart_key, sku_id, count)
4)hdel(key, *fields)
# hdel(key, *fields): 删除hash中对应的属性和值,有则删除,无则忽略
pl.hdel(cart_key, sku_id)
5)hkeys(key)
# hkeys(key): 返回hash中所有属性
# [b'', b'', ...]
sku_ids = redis_conn.hkeys(cart_key) # list
set命令:
1)sadd(key, *members)
# 向set中添加元素,不需要关注是否重复,set中元素唯一
# sadd(key, *values): 向set中添加元素,不需要关注是否重复,set中元素唯一
redis-conn.sadd(cart_selected_key, sku_id) # sku_id商品的id
2)smembers(key)
# 获取set中所有元素
sku_ids = redis_conn.smembers(cart_selected_key)
3)srem(key, *members)
# srem(key, *members): 从set中移除元素,如果元素不存在,直接忽略
pl.srem(cart_selected_key, sku_id)
list命令:
1)lrange(key, start, end )
# lrange(key, start, end): 获取redis列表指定区间内的元素
# [b'', b'', ...]
sku_ids = redis_conn.lrange(history_key, 0, 4)
# 需要定义请求方式get,post,delete,put
# 返回Response
# request.data
包含客户端发送请求体数据(表单数据或json数据),原始Django request对象通过request.POST和request.body,
并且已经转成的字典(dict, OrderedDict, QueryDict)
# request.query_params
包含查询字符串的数据,相当原始Django request对象request.GET并且已经转成的字典(dict, OrderedDict, QueryDict)
# self.kwargs
保存的是从url地址中提取的所有命名参数,为一个字典
例如,127.0.0.1:8000/goods/66,
应用里的url("^goods/(?P\d+)$" , views.GoodsView.as_view())
得到pk的值:self.kwargs.get("pk")
# self.action
def get_serializer_class(self)
if self.action == "list":
return UserSerializer # 返回不同的序列化器类
else:
return BookSerializer
# request.user 登录用户 ,前端请求头里传JWT token数据才能这样用
def get_object(self):
"""返回当前登录user"""
return self.request.user # request 为self.request
# def get(self, request):
"""
self.request: request对象
request.user:
1. 如果用户已认证,request.user就是登录的用户对象
2. 如果用户未认证,request.user就是匿名用户的对象
# 里面封装了四大方法: get_serializer()
# 指明序列化器类
serializer_class = CreateUserSerializer
# 指定查询集(不用的时候,可以不用指定)
queryset
# get_serializer_class(self) 得到序列化器类
返回序列化器类,默认返回serializer_class,可以重写
# get_serializer(self, args, *kwargs) 得到序列化器类对象
返回序列化器对象,主要用来提供给Mixin扩展类使用,如果我们在视图中想要获取序列化器对象,也可以直接调用此方法。
# get_queryset(self) 得到查询集
返回视图使用的查询集,主要用来提供给Mixin扩展类使用,是列表视图与详情视图获取数据的基础,默认返回queryset属性
# get_object(self) 得到对象,默认是根据pk即id查出来的对象
返回详情视图所需的模型类数据对象,主要用来提供给Mixin扩展类使用。
在试图中可以调用该方法获取详情信息的模型类对象。
默认,按照pk(id)来得到对象
# 里面分别封装了:5个action操作方法 list create retrieve update destroy
# 扩展类一般和GenericViewSet搭配使用,注意:继承的时候,扩展类写在前面,视图集类写在后面
DRF提供了5个扩展类,封装了5个通用的操作流程。
类名 | 说明 |
---|---|
ListModelMixin | 提供了一个list方法,封装了返回模型数据列表信息的通用流程。 |
CreateModelMixin | 提供了一个create方法,封装了创建一个模型对象数据信息的通用流程。 |
RetrieveModelMixin | 提供了一个retrieve方法,封装了获取一个模型对象数据信息的通用流程。 |
UpdateModelMixin | 提供了一个update方法,封装了更新一个模型对象数据信息的通用流程。 |
DestroyModelMixin | 提供了一个destroy方法,封装了删除一个模型对象数据信息的通用流程。 |
为了方便我们开发RestAPI,DRF框架除了提供APIView和GenericAPIView视图类之外,还提供了一些子类视图类,这些子类视图类同时继承了GenericAPIView
和对应的Mixin扩展类,并且提供了对应的请求方法。
类名 | 说明 |
---|---|
ListAPIView | 1)继承自ListModelMixin和GenericAPIView。2)如果想定义一个视图只提供列出模型列表 信息的接口,继承此视图类是最快的方式。 |
CreateAPIView | 1)继承自CreateModelMixin和GenericAPIView。2)如果想定义一个视图只提供创建一个模型信息 的接口,继承此视图类是最快的方式。 |
RetrieveAPIView | 1)继承自RetrieveModelMixin和GenericAPIView。2)如果想定义一个视图只提供获取一个模型信息 的接口,继承此视图类是最快的方式。 |
UpdateAPIView | 1)继承自UpdateModelMixin和GenericAPIView。2)如果只想定义一个视图只提供更新一个模型信息 的接口,继承此视图类是最快的方式。 |
DestroyAPIView | 1)继承自DestroyModelMixin和GenericAPIView。2)如果只想定义一个视图只提供删除一个模型信息 的接口,继承此视图类是最快的方式。 |
ListCreateAPIView | 1)继承自ListModelMixin,CreateModelMixin和GenericAPIView。2)如果只想定义一个视图提供列出模型列表 和创建一个模型信息 的接口,继承此视图类是最快的方式。 |
RetrieveUpdateAPIView | 1)继承自RetrieveModelMixin,UpdateModelMixin和GenericAPIView。2)如果只想定义一个视图提供获取一个模型信息 和更新一个模型信息 的接口,继承此视图类是最快的方式。 |
RetrieveDestroyAPIView | 1)继承自RetrieveModelMixin,DestroyModelMixin和GenericAPIView。2)如果只想定义一个视图提供获取一个模型信息 和删除一个模型信息 的接口,继承此视图类是最快的方式。 |
RetrieveUpdateDestoryAPIView | 1)继承自RetrieveModelMixin,UpdateModelMixin,DestroyModelMixin和GenericAPIView。2)如果只想定义一个视图提供获取一个模型信息 和更新一个模型信息 和删除一个模型信息 的接口,继承此视图类是最快的方式 |
将操作同一资源的处理方法放在同一个类中(视图集),处理方法不要以请求方式命名,而是以对应的action命名,
list: 提供一组数据 例如,获取所有图书信息(list) GET /books/
create: 创建一条新数据 例如,新建一本图书信息(create) POST /books/
retrieve: 获取指定的数据 例如,获取指定的图书信息(retrieve) GET /books/(?P\d+)/
update: 更新指定的数据 例如,修改指定的图书信息(update) PUT /books/(?P\d+)/
destroy: 删除指定的数据 例如,删除指定的图书信息(destroy) DELETE /books/(?P\d+)/
进行url配置时需要指明请求方法和处理函数之间的对应关系。
类名 | 说明 |
---|---|
ViewSet | 1)继承自ViewSetMixin和APIView。2)如果使用视图集时不涉及数据库和序列化器的操作,可以直接继承此类。 |
GenericViewSet | 1)继承自ViewSetMixin和GenericAPIView。2)如果使用视图集涉及数据库和序列化器的操作,可以直接继承此类。 |
ModelViewSet | 1)继承自5个Mixin扩展类和GenericViewSet。2)如果使用视图集想一次提供通用的5种操作,继承这个类是最快的。 |
ReadOnlyModelViewSet | 1)继承自ListModelMixin,RetrieveModelMixin和GenericViewSet。2)如果使用视图集想一次提供list操作和retrieve操作,继承这个类是最快的。 |
1)路由Router是专门配合视图集来使用的,可以使用Router自动生成视图集中相应处理函数对应的URL配置项。
2)使用Router自动生成视图集中相应处理函数对应的URL配置项时,除了常见的5种基本操作之外,如果视图集中有添加的其他处理方法,则需要给这些方法加上action装饰器之后,才会动态生成其对应的URL配置项。
# 应用urls.py:
urlpatterns = [
url('^index/$', views.index),
]
route = DefaultRouter()
route.register(r'books', BookInfoViewSet, base_name='books') # books为前缀不是正则表达式
urlpatterns += route.urls
"""
register里的参数:
prefix 该视图集的路由前缀
viewset 视图集
base_name 路由名称的前缀
"""
# @action的作用,对于视图集
在视图集中,如果想要让Router自动帮助我们为自定义的动作生成路由信息,需要使用rest_framework.decorators.action装饰器。@action(method=['GET'],detail=True)
"""
methods: 声明该action对应的请求方式,列表传递
detail: 声明该action的路径是否与单一资源对应,及是否是xxx//action方法名/
True 表示路径格式是xxx//action方法名/ ,例如,books/6/read
False 表示路径格式是xxx/action方法名/,例如,books/latest/
"""
# setting.py 里设置,全局认证都要满足该认证
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.BasicAuthentication', # 基本认证
'rest_framework.authentication.SessionAuthentication', # session认证
)
}
# views.py 每个视图中通过设置authentication_classess属性来设置
class BookInfoViewSet(ModelViewSet):
authentication_classes = (SessionAuthentication, BasicAuthentication)
# setting.py 里设置,全局权限都要满足该权限
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
)
}
# views.py 视图集或视图函数
class BookInfoViewSet(ModelViewSet):
permission_classes = [IsAuthenticated]
# 自定义权限
INSTALLED_APPS = [
'django_filters', # 需要注册应用,
]
REST_FRAMEWORK = {
'DEFAULT_FILTER_BACKENDS': ('django_filters.rest_framework.DjangoFilterBackend',)
}
# 指定过滤字段
# 127.0.0.1:8000/books/?bread=20 访问的地址
# 123.0.0.1:8000/books/?btitle=西游记
filter_fields = ("btitle","bread")
# 指定排序字段
# 127.0.0.1:8000/books/?ordering=-bread,降序
filter_backends=[OrderingFilter]
ordering_fields = ("id","bpub_date","bread")
# setting.py:
REST_FRAMEWORK = {
'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination', # 全局
'PAGE_SIZE': 2 # 每页数目,显示2条
}
# views.py:
# 127.0.0.1:8000/books/?offset=3&limit=2
# pagination_class = LimitOffsetPagination
# 127.0.0.1:8000/books 默认用全局的
pagination_class = PageNumberPagination
# 关闭当前视图分页
pagination_class = None
# 自定义分页
class StandrdResultPagination(PageNumberPagination):
# 127.0.0.1/books/?page=1&page_size=2
# 指定默认页容量
page_size = 3
# 指定获取分页数据是页容量参数名字
page_size_query_param = 'page_size'
# 指定最大页容量
max_page_size = 5
# 使用LimitOffsetPagination分页时,获取分页数据时可以传递参数offset(偏移量)和limit(限制条数)。
json
:
json.dumps(dict): 将python字典转换成json字符串。
json.loads(json_str): 将json字符串转换成python字典。
pickle
:
pickle.dumps(obj): 将obj转换成bytes字节流
pickle.loads(bytes字节流):将bytes字节流转换成obj
base64
:
base64.b64encode(byte字节流): 将byte字节流进行base64编码,返回编码之后内容(bytes字节流)
base64.b64decode(bytes字节流&str): 将参数内容进行base64解码,返回解码之后内容(bytes字节流)
设置购物车cookie数据
:
res = base64.b64encode(pickle.dumps(cart_dict)).decode()
解析cookie购物车数据
:
res = pickle.loads(base64.b64decode(cart_data))
# 添加运行的配置环境
if not os.getenv('DJANGO_SETTINGS_MODULE'):
os.environ['DJANGO_SETTINGS_MODULE'] = 'meiduo_mall.settings.dev' # 运行环境的配置文件件
# 让Django进行一次初始化
import django
django.setup()
# 有的还需要添加路径
# 添加搜索包路径
import sys
sys.path.insert(0, '../')
print(sys.path)
对于订单保存的过程,其中的数据库操作,要么都应该成功,要么都应该失败。
mysql事务:一组mysql语句,放在同一事务中,要么都成功,要么都失败。
1)mysql事务基本操作:
begin或start transaction:手动开启一个mysql事务。
commit: 事务提交,让事务中sql语句的执行结果永久有效。
rollback: 事务回滚,撤销事务中sql语句的执行结果。
2)mysql事务的保存点:
在mysql事务中,可以设置事务保存点,在进行事务语句回滚时可以只回滚到某个保存点。
savepoint 保存点名称
: 设置mysql事务保存点。
rollback to 保存点名称
: 回滚事务语句到保存点的位置,保存点之后sql语句的执行结果会被撤销。
3)django使用事务:
from django.db import transaction
with transaction.atomic():
# 在with语句块中代码,凡是涉及到数据库操作的代码,都会放到同一个事务中
# 设置一个事务保存点
sid = transaction.savepoint()
# ...
# 回滚到指定的事务保存点 事务并没有结束,结束事务只有commit和rollback
trnasaction.savepoint_rollback(sid)
多个人同时购买同一件商品,可能会产生订单并发问题。
订单并发问题解决:
1)悲观锁:
在事务中查询数据的时候尝试对数据进行加锁(互斥锁), 获取到锁的事务可以对数据进行操作,获取不到 锁的事务会阻塞,直到锁被释放。
# select * from tb_sku where id= for update; 设置悲观锁
sku = SKU.objects.select_for_update().get(id=sku_id)
2)乐观’锁’:
本质不是锁,乐观锁本质上不是加锁,查询数据的时候不加锁,对数据进行修改的时候需要进行判断,修改失败需要重新进行尝试。
def create(self, validated_data):
"""保存订单信息(订单并发-乐观锁)"""
# 获取参数address和pay_method
address = validated_data['address']
pay_method = validated_data['pay_method']
# 组织参数
# 用户user
user = self.context['request'].user # 序列化器类对象里面有context属性
# 订单id: '年月日时分秒'+用户id
order_id = datetime.now().strftime('%Y%m%d%H%M%S') + ('%010d' % user.id)
# 运费
freight = Decimal(10) # 精度高
# 订单商品总数量和实付款
total_count = 0
total_amount = Decimal(0)
# 订单状态
# if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH']:
# # 货到付款
# status = OrderInfo.ORDER_STATUS_ENUM['UNSEND'] # 待发货
# else:
# # 在线支付
# status = OrderInfo.ORDER_STATUS_ENUM['UNPAID'] # 待支付
# 三步运算
status = OrderInfo.ORDER_STATUS_ENUM['UNSEND'] if pay_method == OrderInfo.PAY_METHODS_ENUM['CASH'] else OrderInfo.ORDER_STATUS_ENUM['UNPAID']
# 获取redis链接
redis_conn = get_redis_connection('cart') # cart配置里存redis的地方
# 从redis中获取用户所要购买的商品sku_id set
cart_selected_key = 'cart_selected_%s' % user.id
# (b'', ...)
sku_ids = redis_conn.smembers(cart_selected_key)
# 从redis中获取用户购物车商品的sku_id和对应的数量count hash
cart_key = 'cart_%s' % user.id
# {
# b'': b'',
# ...
# }
cart_dict = redis_conn.hgetall(cart_key)
with transaction.atomic():
# 在with语句块中代码,凡是涉及到数据库操作的代码,都会放到同一个事务中
# 设置一个事务保存点
sid = transaction.savepoint()
try:
# 1)向订单基本信息表添加一条记录
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=total_count,
total_amount=total_amount,
freight=freight,
pay_method=pay_method,
status=status
)
# 2)订单中包含几个商品,就需要向订单商品表中添加几条记录
for sku_id in sku_ids:
# 获取用户所要购买的该商品的数量count
count = cart_dict[sku_id]
count = int(count)
for i in range(3):
# 根据sku_id获取商品信息
# select * from tb_sku where id=;
sku = SKU.objects.get(id=sku_id)
# 判断商品库存是否充足
if count > sku.stock:
# 回滚到sid事务保存点
transaction.savepoint_rollback(sid)
raise serializers.ValidationError('商品库存不足')
# 记录商品原始库存
origin_stock = sku.stock
new_stock = origin_stock - count
new_sales = sku.sales + count
# 模拟订单并发问题
# print('user: %d times: %s stock: %s' % (user.id, i, origin_stock))
# import time
# time.sleep(10)
# 减少商品库存,增加销量 乐观锁的体现
# update tb_sku
# set stock=, sales=
# where id= and stock=;
# update方法返回的是更新的行数
res = SKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales)
if res == 0:
if i == 2:
# 更新了3次,仍然失败,下单失败
# 回滚到sid事务保存点
transaction.savepoint_rollback(sid)
raise serializers.ValidationError('下单失败2')
# 更新失败,重新进行尝试
continue
# 向订单商品表中添加一条记录
OrderGoods.objects.create(
order=order,
sku=sku,
count=count,
price=sku.price
)
# 累加计算订单中商品总数量和总金额
total_count += count
total_amount += sku.price*count
# 更新成功,跳出循环
break
# 实付款
total_amount += freight
# 更新订单记录中商品的总数量和实付款
order.total_count = total_count
order.total_amount = total_amount
order.save()
except serializers.ValidationError:
# 继续向外抛出异常
raise
except Exception as e:
# 回滚sid事务保存点
transaction.savepoint_rollback(sid)
raise serializers.ValidationError('下单失败1')
# 3)清除购物车中对应的记录
pl = redis_conn.pipeline()
pl.hdel(cart_key, *sku_ids)
pl.srem(cart_selected_key, *sku_ids)
pl.execute()
# 返回订单对象
return order
3)celery任务队列。
把订单保存代码放到celery的任务中,只启动一个worker。
mysql事务的隔离级别:
隔离级别 | 说明 |
---|---|
Repeatable read(可重复读) | 在一个事务中执行一个查询语句,获取到结果永远和第一次获取的结果相同,即使其他事务把数据进行修改而且已经提交,当前事务仍然获取不到更新之后结果。 |
Read committed(读取已提交) | 其他事务进行数据的修改并且提交之后,当前事务能获取到更新之后的结果。 |
Repeatable read是mysql事务默认隔离级别。
修改mysql事务的隔离级别:
1)编辑mysql配置文件
sudo vi /etc/mysql/mysql.conf.d/mysqld.conf
2)修改配置信息
[mysqld]
28 #
29 # * Basic Settings
30 #
31 user = mysql
32 pid-file = /var/run/mysqld/mysqld.pid
33 socket = /var/run/mysqld/mysqld.sock
34 port = 3306
35 basedir = /usr
36 datadir = /var/lib/mysql
37 tmpdir = /tmp
38 lc-messages-dir = /usr/share/mysql
39 skip-external-locking
40 # 将mysql事务的隔离级别设置为读取已提交
41 transaction-isolation=READ-COMMITTED
3)重启mysql服务
sudo service mysql restart
# 第一种
User.objects.raw(sql)
# 第二种
from django.db import connection
with connection.cursor() as cs:
sql = """DESC {};""".format(table_name)
cs.execute(sql)
fields = cs.fetchall()
# 其它种查看文档,聚合等等
# 下面的代码都在一个事务里面执行,可以查看文档
from django.db import transaction
try:
with transaction.atomic():
# 删掉所有user id的信息
IntentSlot.objects.filter(intent_id=intent_id).delete()
# 对Excel文件进行解析
data_list = ExcelHelper.load_excel(file_obj)
except Exception as e:
return
class IntentSlotFileApiView(APIView):
def post(self, request):
# 获取传过来的文件,必须的数据,slot_name,entity, slot_rule, entity, request_slot,default_value,is_required
file_obj = request.data.get('filename')
bot_id = request.data.get('bot_id')
intent_id = request.query_params.get("intent_id")
if not all([intent_id, file_obj]):
return Response({"msg": "Incomplete parameters", "status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
# 文件名
name = file_obj.name
# 传的不是excel就直接返回,不用解析
if not (name.endswith('.xlsx') or name.endswith('.xls')):
return Response({"msg": "Upload file is not Excel", "status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
try:
# 下面的代码都在一个事务里面执行
with transaction.atomic():
# 删掉所有user id的信息
IntentSlot.objects.filter(intent_id=intent_id).delete()
# 对Excel文件进行解析
data_list = ExcelHelper.load_excel(file_obj)
# 校验上传数据
except Exception as e:
return Response({"msg": repr(e), "status": "fail"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
return Response({"msg": "Upload Success", "status": "success"}, status=status.HTTP_200_OK)
def get(self, request):
# 获取参数
intent_id = request.query_params.get("intent_id")
if not intent_id:
return Response({"msg": "Incomplete parameters", "status": "fail"}, status=status.HTTP_400_BAD_REQUEST)
# 下载模板
curr_time = datetime.date.today()
sheet_name = 'intent_slot_%s_%s' % (intent_id, curr_time)
try:
# 根据intent_id查询数据
intent_slot_objs = IntentSlot.objects.filter(intent_id=intent_id)
if intent_slot_objs:
result_list = IntentSlotSerializer(intent_slot_objs, many=True).data
else:
result_list = []
# 导出excel对象
wb = ExcelHelper.export_excel("botzero_intent_slot", sheet_name, result_list)
# 拼接表名
file_name = "{}.xlsx".format(sheet_name)
if os.path.exists(file_name):
os.remove(file_name)
response = HttpResponse(content_type='application/msexcel', status=status.HTTP_200_OK)
response['Content-Disposition'] = 'attachment;filename={}'.format(file_name)
wb.save(response)
return response
except Exception as e:
return Response({'msg': repr(e), "status": "fail"}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
# -*- coding: utf-8 -*-
import openpyxl
from django.db import connection
from openpyxl.styles import Alignment, Font
class ExcelHelper(object):
@staticmethod
def del_filed_by_table_name(table_name, results_list):
if table_name == "temp_faq":
for field_dict in results_list:
del field_dict["state"]
del field_dict["gmt_modify"]
del field_dict["gmt_create"]
del field_dict["question_id"]
del field_dict["category_id"]
del field_dict["user_id"]
del field_dict["row_number"]
elif table_name == "faq_word":
for field_dict in results_list:
del field_dict["faq_id"]
del field_dict["id"]
@staticmethod
def load_excel(file_obj):
wb = openpyxl.load_workbook(file_obj, data_only=True)
sheets = wb.sheetnames
worksheet = wb[sheets[0]]
all_list, title_list = [], []
for k, row in enumerate(worksheet.rows):
# 获取表的字段
if k == 0:
title_list = [cell.value for cell in row]
# 获取表的每行字段的值
else:
row_list = []
for cell in row:
if isinstance(cell.value, bool):
value = str(cell.value).upper()
else:
value = cell.value
row_list.append(value)
if any(row_list):
# 把字段名和字段值一一对应起来,生成字典并添加到row_list里
all_list.append(dict(zip(title_list, row_list)))
return all_list
@staticmethod
def export_excel(table_name, sheet_name, results_list):
wb = openpyxl.Workbook()
w = wb.create_sheet(sheet_name, 0)
i = 1
# results_list的从数据库查询出来的数据
if not results_list:
with connection.cursor() as cs:
sql = """DESC {};""".format(table_name)
cs.execute(sql)
fields = cs.fetchall()
field_dict = {}
for field in fields:
field_dict[field[0]] = None
results_list.append(field_dict)
# 根据表名删除不必要的字段
ExcelHelper.del_filed_by_table_name(table_name, results_list)
for k, v in results_list[0].items():
w.cell(row=1, column=i).value = k # .replace("t_", "").replace("F_", "")
s = chr(i + 64) + str(1)
w[s].alignment = Alignment(horizontal='center', vertical='center')
w[s].font = Font(bold=True)
w.column_dimensions[chr(i + 64)].width = 35
i = i + 1
excel_row = 2
# write data
for i in range(0, len(results_list)):
excel_col = 1
for k, v in results_list[i].items():
w.cell(row=excel_row, column=excel_col).value = v
s = chr(excel_col + 64) + str(excel_row)
w[s].alignment = Alignment(horizontal='center', vertical='center')
excel_col = excel_col + 1
excel_row = excel_row + 1
return wb
# 表示用户认证,即用户登录的状态
if user.is_authenticated:
pass # 用户登录执行
# 未登录用户状态,储存数据,可以存到其浏览器里的cookie里
# cookie里的value值必须为字符串
# 前端请求头里传 JWT token数据,或请求头里有Authorization字段,才能用request.user
# 使用配置里的变量,需要这样导入 from django.conf import settings
settings.变量
# 第一步在api模块下的tests.py文件下编写testCase
from django.test import TestCase, Client
# Create your tests here.
class ApiTestCase(TestCase):
def setUp(self) -> None:
print("这个方法会在每一个测试方法执行之前被调用,通常用来做一些准备工作")
# 实例化 client 对象
self.token = "s"
self.client = Client()
def tearDown(self) -> None:
print("这个方法会在每一个测试用法执行之后被被调用,通常用来做一些清理工作")
def test_get_user(self):
path = "/api/user/"
data = {
"code": 200
}
HTTP_AUTHORIZATION = "12345567"
# 给请求头里添加信息 HTTP_AUTHORIZATION
resp = self.client.get(path, data=data, HTTP_AUTHORIZATION=HTTP_AUTHORIZATION)
result = resp.json()
print(result["token"])
self.assertEqual(result['status'], "success", result['message'])
def test_post_user(self):
path = "/api/user/"
data = {
"name": "zs",
"age": 18
}
resp = self.client.post(path, data=data)
result = resp.json()
self.assertEqual(result['status'], "success", result['message'])
class UserView(APIView):
def get(self, request):
code = request.query_params.get("code")
# 获取请求头里的信息
token = request.META.get("HTTP_AUTHORIZATION")
if not code:
return Response({
'status': "fail",
'message': '用户未登录',
'token': token
})
return Response({
'status': "success",
'message': 'OK',
'token': token
})
python manage.py test # 执行整个项目的单元测试
python manage.py test api # 只执行api模块
python manage.py test api.tests.ApiTestCase # 只执行某一个测试类