master
Li 1 year ago
parent 27877dc537
commit b97312dbdf

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class AreasConfig(AppConfig):
name = 'areas'

@ -0,0 +1,28 @@
# Generated by Django 2.2.8 on 2023-08-25 09:37
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Area',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, verbose_name='名称')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='subs', to='areas.Area', verbose_name='上级行政区划')),
],
options={
'verbose_name': '省市区',
'verbose_name_plural': '省市区',
'db_table': 'tb_areas',
},
),
]

@ -0,0 +1,17 @@
from django.db import models
# Create your models here.
class Area(models.Model):
"""省市区"""
name = models.CharField(max_length=20, verbose_name='名称')
parent = models.ForeignKey('self', on_delete=models.SET_NULL,
related_name='subs', null=True, blank=True, verbose_name='上级行政区划')
class Meta:
db_table = 'tb_areas'
verbose_name = '省市区'
verbose_name_plural = '省市区'
def __str__(self):
return self.name

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,7 @@
from django.contrib import admin
from django.urls import path
from .views import *
urlpatterns = [
path('areas/', AreasView.as_view()), # 省市区数据 ,子路由
]

@ -0,0 +1,52 @@
from django.core.cache import cache
from django.http import JsonResponse
from django.shortcuts import render
# Create your views here.
from django.views import View
from areas.models import Area
from utils.response_code import RETCODE
class AreasView(View):
"""省市区数据"""
def get(self, request):
"""提供省市区数据"""
area_id = request.GET.get('area_id')
if not area_id:
province_list = cache.get('province_list') # 读取省份缓存数据
if not province_list:
try:
province_model_list = Area.objects.filter(parent__isnull=True)
province_list = [] # 构建省级数据
for province_model in province_model_list:
province_list.append({'id': province_model.id, 'name': province_model.name})
except Exception as e:
return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '省份数据错误'})
# 存储省份缓存数据
cache.set('province_list', province_list, 3600)
# 响应省份数据
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'province_list': province_list})
else:
# 读取市或区缓存数据
sub_data = cache.get('sub_area_' + area_id)
if not sub_data:
try:
parent_model = Area.objects.get(id=area_id) # 查询市或区的父级
sub_model_list = parent_model.subs.all()
sub_list = [] # 构建市或区数据
for sub_model in sub_model_list:
sub_list.append({'id': sub_model.id, 'name': sub_model.name})
sub_data = {
'id': parent_model.id, # 父级pk
'name': parent_model.name, # 父级name
'subs': sub_list # 父级的子集
}
except Exception as e:
return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '城市或区数据错误'})
# 储存市或区缓存数据
cache.set('sub_area_' + area_id, sub_data, 3600)
# 响应市或区数据
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'sub_data': sub_data})

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class CartsConfig(AppConfig):
name = 'carts'

@ -0,0 +1,3 @@
from django.db import models
# Create your models here.

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,11 @@
from django.urls import path
from . import views
app_name = 'carts'
urlpatterns = [
# 购物车管理
path('carts/', views.CartsView.as_view(), name='info'),
# 选择购物车商品
path('carts/selection/', views.CartsSelectAllView.as_view()),
# 简单购物车
path('carts/simple/', views.CartsSimpleView.as_view()),
]

@ -0,0 +1,359 @@
import base64
import json
import pickle
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import render
# Create your views here.
from django.views import View
from django_redis import get_redis_connection
from goods.models import SKU
from utils.response_code import RETCODE
from xiaoyu_mall import settings
class CartsView(View):
def get(self, request):
"""查询购物车"""
user = request.user # 判断用户是否登录
if user.is_authenticated:
# 用户已登录查询redis购物车
# 创建链接到redis的对象
redis_conn = get_redis_connection('carts')
# 查询hash数据
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
# 查询set数据
redis_selected = redis_conn.smembers('selected_%s' % user.id)
cart_dict = {}
# 将redis_cart和redis_selected进行数据结构的构造合并数据数据结构跟未登录用户购物车结构一致
for sku_id, count in redis_cart.items():
cart_dict[int(sku_id)] = {
"count": int(count),
"selected": sku_id in redis_selected
}
else:
# 用户未登录查询cookies购物车
cart_str = request.COOKIES.get('carts')
if cart_str:
# 将 cart_str转成bytes类型的字符串
cart_str_bytes = cart_str.encode()
# 将cart_str_bytes转成bytes类型的字典
cart_dict_bytes = base64.b64decode(cart_str_bytes)
# 将cart_dict_bytes转成真正的字典
cart_dict = pickle.loads(cart_dict_bytes)
else:
cart_dict = {}
sku_ids = cart_dict.keys() # 构造响应数据
skus = SKU.objects.filter(id__in=sku_ids) # 一次性查询出所有的skus
cart_skus = []
for sku in skus:
cart_skus.append({
'id': sku.id,
'count': cart_dict.get(sku.id).get('count'),
'selected': str(cart_dict.get(sku.id).get('selected')),
'name': sku.name,
'default_image_url': settings.STATIC_URL + 'images/goods/' + sku.default_image.url + '.jpg',
'price': str(sku.price),
'amount': str(sku.price * cart_dict.get(sku.id).get('count')),
'stock': sku.stock
})
context = {
'cart_skus': cart_skus
}
return render(request, 'cart.html', context) # 渲染购物车页面
def post(self, request):
"""保存购物车"""
# 接收参数
json_dict = json.loads(request.body.decode())
sku_id = json_dict.get('sku_id')
count = json_dict.get('count')
selected = json_dict.get('selected', True)
# 校验参数
if not all([sku_id, count]):
return HttpResponseForbidden('缺少必传参数')
# 校验sku_id是否合法
try:
SKU.objects.get(id=sku_id)
except SKU.DoesNotExist:
return HttpResponseForbidden('参数sku_id错误')
# 校验count是否是数字
try:
count = int(count)
except Exception as e:
return HttpResponseForbidden('参数count错误')
# 校验勾选是否是bool
if selected:
if not isinstance(selected, bool):
return HttpResponseForbidden('参数selected错误')
# 判断用户是否登录
user = request.user
if user.is_authenticated:
# 如果用户已登录操作Redis购物车
redis_conn = get_redis_connection('carts')
pl = redis_conn.pipeline()
# 需要以增量计算的形式保存商品数据
pl.hincrby('carts_%s' % user.id, sku_id, count)
# 保存商品勾选状态
if selected:
pl.sadd('selected_%s' % user.id, sku_id)
pl.execute() # 执行
# 响应结果
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})
else: # 用户未登录操作Cookie购物车
# 获取cookie中的购物车数据并且判断是否有购物车数据
cart_str = request.COOKIES.get('carts')
if cart_str:
# 将 cart_str转成bytes类型的字符串
cart_str_bytes = cart_str.encode()
# 将cart_str_bytes转成bytes类型的字典
cart_dict_bytes = base64.b64decode(cart_str_bytes)
# 将cart_dict_bytes转成真正的字典
cart_dict = pickle.loads(cart_dict_bytes)
else:
cart_dict = {}
# 判断当前要添加的商品在cart_dict中是否存在
if sku_id in cart_dict:
# 购物车已存在,增量计算
origin_count = cart_dict[sku_id]['count']
count += origin_count
cart_dict[sku_id] = {
'count': count,
'selected': selected
}
# 将cart_dict转成bytes类型的字典
cart_dict_bytes = pickle.dumps(cart_dict)
# 将cart_dict_bytes转成bytes类型的字符串
cart_str_bytes = base64.b64encode(cart_dict_bytes)
# 将cart_str_bytes转成字符串
cookie_cart_str = cart_str_bytes.decode()
# 将新的购物车数据写入到cookie
response = JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})
response.set_cookie('carts', cookie_cart_str)
return response
def put(self, request):
"""修改购物车"""
# 接收参数
json_dict = json.loads(request.body.decode())
sku_id = json_dict.get('sku_id')
count = json_dict.get('count')
selected = json_dict.get('selected', True)
# 判断参数是否齐全
if not all([sku_id, count]):
return HttpResponseForbidden('缺少必传参数')
# 判断sku_id是否存在
try:
sku = SKU.objects.get(id=sku_id)
except SKU.DoesNotExist:
return HttpResponseForbidden('商品sku_id不存在')
# 判断count是否为数字
try:
count = int(count)
except Exception:
return HttpResponseForbidden('参数count有误')
# 判断selected是否为bool值
if selected:
if not isinstance(selected, bool):
return HttpResponseForbidden('参数selected有误')
# 判断用户是否登录
user = request.user
if user.is_authenticated:
# 用户已登录修改redis购物车
redis_conn = get_redis_connection('carts')
pl = redis_conn.pipeline()
# 由于后端收到的数据是最终的结果,所以"覆盖写入"
# redis_conn.hincrby() # 使用新值加上旧值(增量)
pl.hset('carts_%s' % user.id, sku_id, count)
# 修改勾选状态
if selected:
pl.sadd('selected_%s' % user.id, sku_id)
else:
pl.srem('selected_%s' % user.id, sku_id)
# 执行
pl.execute()
# 创建响应对象
cart_sku = {
'id': sku_id,
'count': count,
'selected': selected,
'name': sku.name,
'price': sku.price,
'amount': sku.price * count,
'default_image_url': settings.STATIC_URL + 'images/goods/' +
sku.default_image.url + '.jpg',
}
return JsonResponse({'code': RETCODE.OK, 'errmsg': '修改购物车成功', 'cart_sku': cart_sku})
else:
# 用户未登录修改cookie购物车
# 获取cookie中的购物车数据并且判断是否有购物车数据
cart_str = request.COOKIES.get('carts')
if cart_str:
# 将 cart_str转成bytes类型的字符串
cart_str_bytes = cart_str.encode()
# 将cart_str_bytes转成bytes类型的字典
cart_dict_bytes = base64.b64decode(cart_str_bytes)
# 将cart_dict_bytes转成真正的字典
cart_dict = pickle.loads(cart_dict_bytes)
else:
cart_dict = {}
# 由于后端收到的是最终的结果,所以"覆盖写入"
cart_dict[sku_id] = {
'count': count,
'selected': selected
}
# 创建响应对象
cart_sku = {
'id': sku_id,
'count': count,
'selected': selected,
'name': sku.name,
'price': sku.price,
'amount': sku.price * count,
'default_image_url': settings.STATIC_URL + 'images/goods/' +
sku.default_image.url + '.jpg'
}
# 将cart_dict转成bytes类型的字典
cart_dict_bytes = pickle.dumps(cart_dict)
# 将cart_dict_bytes转成bytes类型的字符串
cart_str_bytes = base64.b64encode(cart_dict_bytes)
# 将cart_str_bytes转成字符串
cookie_cart_str = cart_str_bytes.decode()
# 将新的购物车数据写入到cookie
response = JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK',
'cart_sku': cart_sku})
response.set_cookie('carts', cookie_cart_str)
# 响应结果
return response
def delete(self, request):
"""删除购物车"""
# 接收参数
json_dict = json.loads(request.body.decode())
sku_id = json_dict.get('sku_id')
# 判断sku_id是否存在
try:
SKU.objects.get(id=sku_id)
except SKU.DoesNotExist:
return HttpResponseForbidden('商品不存在')
# 判断用户是否登录
user = request.user
if user is not None and user.is_authenticated:
# 用户已登录删除redis购物车
redis_conn = get_redis_connection('carts')
pl = redis_conn.pipeline()
# 删除hash购物车商品记录
pl.hdel('carts_%s' % user.id, sku_id)
# 同步移除勾选状态
pl.srem('selected_%s' % user.id, sku_id)
pl.execute()
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})
else:
# 用户未登录删除cookie购物车
# 获取cookie中的购物车数据并且判断是否有购物车数据
cart_str = request.COOKIES.get('carts')
if cart_str:
# 将 cart_str转成bytes类型的字符串
cart_str_bytes = cart_str.encode()
# 将cart_str_bytes转成bytes类型的字典
cart_dict_bytes = base64.b64decode(cart_str_bytes)
# 将cart_dict_bytes转成真正的字典
cart_dict = pickle.loads(cart_dict_bytes)
else:
cart_dict = {}
# 构造响应对象
response = JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})
# 删除字典指定key所对应的记录
if sku_id in cart_dict:
del cart_dict[sku_id] # 如果删除的key不存在会抛出异常
# 将cart_dict转成bytes类型的字典
cart_dict_bytes = pickle.dumps(cart_dict)
# 将cart_dict_bytes转成bytes类型的字符串
cart_str_bytes = base64.b64encode(cart_dict_bytes)
# 将cart_str_bytes转成字符串
cookie_cart_str = cart_str_bytes.decode()
# 写入新的cookie
response.set_cookie('carts', cookie_cart_str)
return response
class CartsSimpleView(View):
"""商品页面右上角购物车"""
def get(self, request):
user = request.user # 判断用户是否登录
if user.is_authenticated:
# 用户已登录查询Redis购物车
redis_conn = get_redis_connection('carts')
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
cart_selected = redis_conn.smembers('selected_%s' % user.id)
# 将redis中的两个数据统一格式跟cookie中的格式一致方便统一查询
cart_dict = {}
for sku_id, count in redis_cart.items():
cart_dict[int(sku_id)] = {
'count': int(count),
'selected': sku_id in cart_selected
}
else:
# 用户未登录查询cookie购物车
cart_str = request.COOKIES.get('carts')
if cart_str:
cart_dict = pickle.loads(base64.b64decode(cart_str.encode()))
else:
cart_dict = {}
# 构造简单购物车JSON数据
cart_skus = []
sku_ids = cart_dict.keys()
skus = SKU.objects.filter(id__in=sku_ids)
for sku in skus:
cart_skus.append({
'id': sku.id,
'name': sku.name,
'count': cart_dict.get(sku.id).get('count'),
'default_image_url': settings.STATIC_URL + 'images/goods/' + sku.default_image.url + '.jpg',
})
# 响应json列表数据
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'cart_skus': cart_skus})
class CartsSelectAllView(View):
"""全选购物车"""
def put(self, request):
# 接收参数
json_dict = json.loads(request.body.decode())
selected = json_dict.get('selected', True)
# 校验参数
if selected and not isinstance(selected, bool):
return HttpResponseForbidden('参数selected有误')
# 判断用户是否登录
user = request.user
if user.is_authenticated:
# 用户已登录操作redis购物车
redis_conn = get_redis_connection('carts')
cart = redis_conn.hgetall('carts_%s' % user.id)
sku_id_list = cart.keys()
if selected:
# 全选
redis_conn.sadd('selected_%s' % user.id, *sku_id_list)
else:
# 取消全选
redis_conn.srem('selected_%s' % user.id, *sku_id_list)
return JsonResponse({'code': RETCODE.OK, 'errmsg': '全选购物车成功'})
else:
# 用户未登录操作cookie购物车
cart = request.COOKIES.get('carts')
response = JsonResponse({'code': RETCODE.OK, 'errmsg': '全选购物车成功'})
if cart is not None:
cart = pickle.loads(base64.b64decode(cart.encode()))
for sku_id in cart:
cart[sku_id]['selected'] = selected
cookie_cart = base64.b64encode(pickle.dumps(cart)).decode()
response.set_cookie('carts', cookie_cart)
return response

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ContentsConfig(AppConfig):
name = 'contents'

@ -0,0 +1,50 @@
# Generated by Django 2.2.8 on 2023-08-18 11:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='ContentCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=50, verbose_name='名称')),
('key', models.CharField(max_length=50, verbose_name='类别键名')),
],
options={
'verbose_name': '广告内容类别',
'verbose_name_plural': '广告内容类别',
'db_table': 'tb_content_category',
},
),
migrations.CreateModel(
name='Content',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('title', models.CharField(max_length=100, verbose_name='标题')),
('url', models.CharField(max_length=300, verbose_name='内容链接')),
('image', models.ImageField(blank=True, null=True, upload_to='', verbose_name='图片')),
('text', models.TextField(blank=True, null=True, verbose_name='内容')),
('sequence', models.IntegerField(verbose_name='排序')),
('status', models.BooleanField(default=True, verbose_name='是否展示')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='contents.ContentCategory', verbose_name='类别')),
],
options={
'verbose_name': '广告内容',
'verbose_name_plural': '广告内容',
'db_table': 'tb_content',
},
),
]

@ -0,0 +1,36 @@
from django.db import models
from utils.models import BaseModel
# Create your models here.
class ContentCategory(BaseModel):
"""广告内容类别"""
name = models.CharField(max_length=50, verbose_name='名称')
key = models.CharField(max_length=50, verbose_name='类别键名')
class Meta:
db_table = 'tb_content_category'
verbose_name = '广告内容类别'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class Content(BaseModel):
"""广告内容"""
category = models.ForeignKey(ContentCategory, on_delete=models.PROTECT, verbose_name='类别')
title = models.CharField(max_length=100, verbose_name='标题')
url = models.CharField(max_length=300, verbose_name='内容链接')
image = models.ImageField(null=True, blank=True, verbose_name='图片')
text = models.TextField(null=True, blank=True, verbose_name='内容')
sequence = models.IntegerField(verbose_name='排序')
status = models.BooleanField(default=True, verbose_name='是否展示')
class Meta:
db_table = 'tb_content'
verbose_name = '广告内容'
verbose_name_plural = verbose_name
def __str__(self):
return self.category.name + ': ' + self.title

@ -0,0 +1,20 @@
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import linear_kernel
class ContentBasedRecommender:
def __init__(self, products):
self.products = products
self.build()
def build(self):
tfidf_vectorizer = TfidfVectorizer(stop_words='english')
tfidf_matrix = tfidf_vectorizer.fit_transform([product.caption for product in self.products])
self.similarity_matrix = linear_kernel(tfidf_matrix, tfidf_matrix)
def recommend_products(self, product_id, num_recommendations=5):
product_index = next(index for (index, product) in enumerate(self.products) if product.id == product_id)
similarity_scores = list(enumerate(self.similarity_matrix[product_index]))
similarity_scores = sorted(similarity_scores, key=lambda x: x[1], reverse=True)
similar_products = similarity_scores[1:num_recommendations + 1]
return [self.products[product[0]] for product in similar_products]

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,9 @@
from django.contrib import admin
from django.urls import path
from .views import *
app_name = 'contents'
urlpatterns = [
path('', IndexView.as_view(), name='index'),
]

@ -0,0 +1,32 @@
from collections import OrderedDict
from goods.models import GoodsChannel
def get_categories():
"""获取商品分类"""
# 准备商品分类对应的字典
categories = OrderedDict()
# 查询并展示商品分类 37个一级类别
channels = GoodsChannel.objects.order_by('group_id', 'sequence')
# 遍历所有频道
for channel in channels:
group_id = channel.group_id # 当前组
# 获取当前频道所在的组只有11个组
if group_id not in categories:
categories[group_id] = {'channels': [], 'sub_cats': []}
cat1 = channel.category # 当前频道的类别
# 追加当前频道
categories[group_id]['channels'].append({
'id': cat1.id,
'name': cat1.name,
'url': channel.url
})
# 查询二级和三级类别
for cat2 in cat1.subs.all(): # 从一级类别查找二级类别
cat2.sub_cats = [] # 给二级类别添加一个保存三级类别的列表
for cat3 in cat2.subs.all(): # 从二级类别查找三级类别
cat2.sub_cats.append(cat3) # 将三级类别添加到二级sub_cats
# 将二级类别添加到一级类别的sub_cats
categories[group_id]['sub_cats'].append(cat2)
return categories

@ -0,0 +1,61 @@
from django.shortcuts import render
from django.views import View
from contents.recommender import ContentBasedRecommender
from contents.utils import get_categories
from collections import OrderedDict
from goods.models import SKU
from contents.models import ContentCategory
# class IndexView(View):
# def get(self, request):
# """提供首页广告页面"""
# categories = get_categories()
# # 查询首页广告数据
# # 查询所有的广告类别
# content_categories = ContentCategory.objects.all()
# # 使用广告类别查询出该类别对应的所有的广告内容
# contents = OrderedDict()
# for content_categorie in content_categories:
# contents[content_categorie.key] = content_categorie.content_set.filter(status=True).order_by(
# 'sequence') # 查询出未下架的广告并排序
# # 渲染模板的上下文
# context = {
# 'categories': categories,
# 'contents': contents,
# }
# return render(request, 'index.html', context)
class IndexView(View):
def get(self, request):
"""提供首页广告页面"""
skus = SKU.objects.filter(is_launched=True)
recommender = ContentBasedRecommender(skus)
user_interests = SKU.objects.all()
recommended_products = []
for interest in user_interests:
recommendations = recommender.recommend_products(interest.id)
recommended_products.extend(recommendations)
# 查询商品分类
categories = get_categories()
# 查询首页广告数据
# 查询所有的广告类别
content_categories = ContentCategory.objects.all()
# 使用广告类别查询出该类别对应的所有的广告内容
contents = OrderedDict()
for content_categorie in content_categories:
contents[content_categorie.key] = content_categorie.content_set.filter(status=True).order_by(
'sequence') # 查询出未下架的广告并排序
# 构造上下文
context = {
'categories': categories,
'page_skus': recommended_products,
'contents': contents,
}
return render(request, 'index2.html', context)

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class GoodsConfig(AppConfig):
name = 'goods'

@ -0,0 +1,188 @@
# Generated by Django 2.2.8 on 2023-08-18 11:39
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Brand',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=20, verbose_name='名称')),
('logo', models.ImageField(upload_to='', verbose_name='Logo图片')),
('first_letter', models.CharField(max_length=1, verbose_name='品牌首字母')),
],
options={
'verbose_name': '品牌',
'verbose_name_plural': '品牌',
'db_table': 'tb_brand',
},
),
migrations.CreateModel(
name='GoodsCategory',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=10, verbose_name='名称')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='subs', to='goods.GoodsCategory', verbose_name='父类别')),
],
options={
'verbose_name': '商品类别',
'verbose_name_plural': '商品类别',
'db_table': 'tb_goods_category',
},
),
migrations.CreateModel(
name='GoodsChannelGroup',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=20, verbose_name='频道组名')),
],
options={
'verbose_name': '商品频道组',
'verbose_name_plural': '商品频道组',
'db_table': 'tb_channel_group',
},
),
migrations.CreateModel(
name='SKU',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=50, verbose_name='名称')),
('caption', models.CharField(max_length=100, verbose_name='副标题')),
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='单价')),
('cost_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='进价')),
('market_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='市场价')),
('stock', models.IntegerField(default=0, verbose_name='库存')),
('sales', models.IntegerField(default=0, verbose_name='销量')),
('comments', models.IntegerField(default=0, verbose_name='评价数')),
('is_launched', models.BooleanField(default=True, verbose_name='是否上架销售')),
('default_image', models.ImageField(blank=True, default='', max_length=200, null=True, upload_to='', verbose_name='默认图片')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='goods.GoodsCategory', verbose_name='从属类别')),
],
options={
'verbose_name': '商品SKU',
'verbose_name_plural': '商品SKU',
'db_table': 'tb_sku',
},
),
migrations.CreateModel(
name='SPU',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=50, verbose_name='名称')),
('sales', models.IntegerField(default=0, verbose_name='销量')),
('comments', models.IntegerField(default=0, verbose_name='评价数')),
('desc_detail', models.TextField(default='', verbose_name='详细介绍')),
('desc_pack', models.TextField(default='', verbose_name='包装信息')),
('desc_service', models.TextField(default='', verbose_name='售后服务')),
('brand', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='goods.Brand', verbose_name='品牌')),
('category1', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cat1_spu', to='goods.GoodsCategory', verbose_name='一级类别')),
('category2', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cat2_spu', to='goods.GoodsCategory', verbose_name='二级类别')),
('category3', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='cat3_spu', to='goods.GoodsCategory', verbose_name='三级类别')),
],
options={
'verbose_name': '商品SPU',
'verbose_name_plural': '商品SPU',
'db_table': 'tb_spu',
},
),
migrations.CreateModel(
name='SPUSpecification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('name', models.CharField(max_length=20, verbose_name='规格名称')),
('spu', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specs', to='goods.SPU', verbose_name='商品SPU')),
],
options={
'verbose_name': '商品SPU规格',
'verbose_name_plural': '商品SPU规格',
'db_table': 'tb_spu_specification',
},
),
migrations.CreateModel(
name='SpecificationOption',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('value', models.CharField(max_length=20, verbose_name='选项值')),
('spec', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='goods.SPUSpecification', verbose_name='规格')),
],
options={
'verbose_name': '规格选项',
'verbose_name_plural': '规格选项',
'db_table': 'tb_specification_option',
},
),
migrations.CreateModel(
name='SKUSpecification',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('option', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='goods.SpecificationOption', verbose_name='规格值')),
('sku', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='specs', to='goods.SKU', verbose_name='sku')),
('spec', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='goods.SPUSpecification', verbose_name='规格名称')),
],
options={
'verbose_name': 'SKU规格',
'verbose_name_plural': 'SKU规格',
'db_table': 'tb_sku_specification',
},
),
migrations.CreateModel(
name='SKUImage',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('image', models.ImageField(upload_to='', verbose_name='图片')),
('sku', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.SKU', verbose_name='sku')),
],
options={
'verbose_name': 'SKU图片',
'verbose_name_plural': 'SKU图片',
'db_table': 'tb_sku_image',
},
),
migrations.AddField(
model_name='sku',
name='spu',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.SPU', verbose_name='商品'),
),
migrations.CreateModel(
name='GoodsChannel',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('url', models.CharField(max_length=50, verbose_name='频道页面链接')),
('sequence', models.IntegerField(verbose_name='组内顺序')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsCategory', verbose_name='顶级商品类别')),
('group', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='goods.GoodsChannelGroup', verbose_name='频道组名')),
],
options={
'verbose_name': '商品频道',
'verbose_name_plural': '商品频道',
'db_table': 'tb_goods_channel',
},
),
]

@ -0,0 +1,174 @@
from django.db import models
from utils.models import BaseModel
# Create your models here.
class GoodsCategory(BaseModel):
"""商品类别"""
name = models.CharField(max_length=10, verbose_name='名称')
parent = models.ForeignKey('self', related_name='subs', null=True, blank=True, on_delete=models.CASCADE,
verbose_name='父类别')
class Meta:
db_table = 'tb_goods_category'
verbose_name = '商品类别'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class GoodsChannelGroup(models.Model):
"""商品频道组"""
name = models.CharField(max_length=20, verbose_name='频道组名')
class Meta:
db_table = 'tb_channel_group'
verbose_name = '商品频道组'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class GoodsChannel(BaseModel):
"""商品频道"""
group = models.ForeignKey(GoodsChannelGroup, verbose_name='频道组名', on_delete=models.CASCADE)
category = models.ForeignKey(GoodsCategory, on_delete=models.CASCADE, verbose_name='顶级商品类别')
url = models.CharField(max_length=50, verbose_name='频道页面链接')
sequence = models.IntegerField(verbose_name='组内顺序')
class Meta:
db_table = 'tb_goods_channel'
verbose_name = '商品频道'
verbose_name_plural = verbose_name
def __str__(self):
return self.category.name
class Brand(BaseModel):
"""品牌"""
name = models.CharField(max_length=20, verbose_name='名称')
logo = models.ImageField(verbose_name='Logo图片')
first_letter = models.CharField(max_length=1, verbose_name='品牌首字母')
class Meta:
db_table = 'tb_brand'
verbose_name = '品牌'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class SPU(BaseModel):
"""商品SPU"""
name = models.CharField(max_length=50, verbose_name='名称')
brand = models.ForeignKey(Brand, on_delete=models.PROTECT, verbose_name='品牌')
category1 = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT,
related_name='cat1_spu', verbose_name='一级类别')
category2 = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT,
related_name='cat2_spu', verbose_name='二级类别')
category3 = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT,
related_name='cat3_spu', verbose_name='三级类别')
sales = models.IntegerField(default=0, verbose_name='销量')
comments = models.IntegerField(default=0, verbose_name='评价数')
desc_detail = models.TextField(default='', verbose_name='详细介绍')
desc_pack = models.TextField(default='', verbose_name='包装信息')
desc_service = models.TextField(default='', verbose_name='售后服务')
class Meta:
db_table = 'tb_spu'
verbose_name = '商品SPU'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
class SKU(BaseModel):
"""商品SKU"""
name = models.CharField(max_length=50, verbose_name='名称')
caption = models.CharField(max_length=100, verbose_name='副标题')
spu = models.ForeignKey(SPU, on_delete=models.CASCADE, verbose_name='商品')
category = models.ForeignKey(GoodsCategory, on_delete=models.PROTECT, verbose_name='从属类别')
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='单价')
cost_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='进价')
market_price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='市场价')
stock = models.IntegerField(default=0, verbose_name='库存')
sales = models.IntegerField(default=0, verbose_name='销量')
comments = models.IntegerField(default=0, verbose_name='评价数')
is_launched = models.BooleanField(default=True, verbose_name='是否上架销售')
default_image = models.ImageField(max_length=200, default='', null=True, blank=True, verbose_name='默认图片')
class Meta:
db_table = 'tb_sku'
verbose_name = '商品SKU'
verbose_name_plural = verbose_name
def __str__(self):
return '%s: %s' % (self.id, self.name)
class SKUImage(BaseModel):
"""SKU图片"""
sku = models.ForeignKey(SKU, on_delete=models.CASCADE, verbose_name='sku')
image = models.ImageField(verbose_name='图片')
class Meta:
db_table = 'tb_sku_image'
verbose_name = 'SKU图片'
verbose_name_plural = verbose_name
def __str__(self):
return '%s %s' % (self.sku.name, self.id)
class SPUSpecification(BaseModel):
"""商品SPU规格"""
spu = models.ForeignKey(SPU, on_delete=models.CASCADE, related_name='specs', verbose_name='商品SPU')
name = models.CharField(max_length=20, verbose_name='规格名称')
class Meta:
db_table = 'tb_spu_specification'
verbose_name = '商品SPU规格'
verbose_name_plural = verbose_name
def __str__(self):
return '%s: %s' % (self.spu.name, self.name)
class SpecificationOption(BaseModel):
"""规格选项"""
spec = models.ForeignKey(SPUSpecification, related_name='options',
on_delete=models.CASCADE, verbose_name='规格')
value = models.CharField(max_length=20, verbose_name='选项值')
class Meta:
db_table = 'tb_specification_option'
verbose_name = '规格选项'
verbose_name_plural = verbose_name
def __str__(self):
return '%s - %s' % (self.spec, self.value)
class SKUSpecification(BaseModel):
"""SKU具体规格"""
sku = models.ForeignKey(SKU, related_name='specs', on_delete=models.CASCADE, verbose_name='sku')
spec = models.ForeignKey(SPUSpecification, on_delete=models.PROTECT, verbose_name='规格名称')
option = models.ForeignKey(SpecificationOption, on_delete=models.PROTECT, verbose_name='规格值')
class Meta:
db_table = 'tb_sku_specification'
verbose_name = 'SKU规格'
verbose_name_plural = verbose_name
def __str__(self):
return '%s: %s - %s' % (self.sku, self.spec.name, self.option.value)

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,55 @@
from collections import OrderedDict
from goods.models import GoodsChannel
def get_categories():
"""获取商品分类"""
# 准备商品分类对应的字典
categories = OrderedDict()
# 查询并展示商品分类 37个一级类别
channels = GoodsChannel.objects.order_by('group_id', 'sequence')
# 遍历所有频道
for channel in channels:
group_id = channel.group_id # 当前组
# 获取当前频道所在的组只有11个组
if group_id not in categories:
categories[group_id] = {'channels': [], 'sub_cats': []}
cat1 = channel.category # 当前频道的类别
# 追加当前频道
categories[group_id]['channels'].append({
'id': cat1.id,
'name': cat1.name,
'url': channel.url
})
# 查询二级和三级类别
for cat2 in cat1.subs.all(): # 从一级类别查找二级类别
cat2.sub_cats = [] # 给二级类别添加一个保存三级类别的列表
for cat3 in cat2.subs.all(): # 从二级类别查找三级类别
cat2.sub_cats.append(cat3) # 将三级类别添加到二级sub_cats
# 将二级类别添加到一级类别的sub_cats
categories[group_id]['sub_cats'].append(cat2)
return categories
def get_breadcrumb(category):
"""
获取面包屑导航
:param category:类别对象一级 二级 三级
:return:一级返回一级 二级返回一级+二级 三级一级+二级+三级
"""
breadcrumb = {
'cat1': '',
'cat2': '',
'cat3': '',
}
if category.parent == None: # 说明category是一级
breadcrumb['cat1'] = category
elif category.subs.count() == 0: # 说明category是三级
cat2 = category.parent
breadcrumb['cat1'] = cat2.parent
breadcrumb['cat2'] = cat2
breadcrumb['cat3'] = category
else: # 说明category对应的是二级
breadcrumb['cat1'] = category.parent
breadcrumb['cat2'] = category
return breadcrumb

@ -0,0 +1,19 @@
from django.contrib import admin
from django.urls import path
from .views import *
app_name = 'goods'
urlpatterns = [
# 商品列表页
path('list/<int:category_id>/<int:page_num>/', ListView.as_view(), name='list'),
# 商品详情
path('detail/<int:sku_id>/', DetailView.as_view(), name='detail'),
# 热销排行
path('hot/<int:category_id>/', HostGoodsView.as_view()),
# 商品搜索
path('search/', SearchView.as_view()),
# 商品评价
path('comments/<int:sku_id>/', GoodsCommentView.as_view()),
# 商品类别
path('categorys/', CategorysView.as_view()),
]

@ -0,0 +1,226 @@
from django.core.cache import cache
from django.core.paginator import Paginator, EmptyPage
from django.http import HttpResponseNotFound, HttpResponse, JsonResponse
from django.shortcuts import render
# Create your views here.
from django import http
from django.views import View
from goods.models import GoodsCategory, SKU
from goods.tools import get_categories, get_breadcrumb
from orders.models import OrderGoods
from utils.response_code import RETCODE
from xiaoyu_mall import settings
class ListView(View):
"""商品列表页"""
def get(self, request, category_id, page_num):
"""提供商品列表页"""
# 校验参数category_id
try:
category = GoodsCategory.objects.get(id=category_id)
except GoodsCategory.DoesNotExist:
return http.HttpResponseNotFound('"参数category_id不存在"')
# 查询面包屑导航
breadcrumb = get_breadcrumb(category)
sort = request.GET.get('sort', 'default')
# 获取sort排序规则 如果sort没有值取default
# 查询字符串
# 按照排序规则查询该分类商品SKU信息
if sort == 'price': # 按照价格由低到高排序
sort_field = 'price'
elif sort == 'hot':
sort_field = '-sales' # 按照销量由高到低排序
else: # 只要不是price和-sales其他的所有情况都归为default
sort = 'default'
sort_field = 'create_time'
skus = SKU.objects.filter(category=category, is_launched=True).order_by(sort_field)
# 创建分页器
# Paginator('要分页的记录','每页记录的条数')
paginator = Paginator(skus, 5) # 把skus进行分页每页5条记录
# 需要获取用户当前要看的那一页
try:
page_skus = paginator.page(page_num) # 获取到page_num页中的5条记录
except EmptyPage:
return HttpResponseNotFound('Empty Page')
# 获取总页数: 前端的分页插件需要使用
total_page = paginator.num_pages
# 查询商品分类
categories = get_categories()
# 构造上下文
context = {
'categories': categories,
'page_skus': page_skus,
'total_page': total_page,
'page_num': page_num,
'sort': sort,
'category_id': category_id,
'breadcrumb': breadcrumb
}
return render(request, 'list.html', context=context)
class HostGoodsView(View):
"""热销排行"""
def get(self, request, category_id):
# 要查询指定分类的sku信息而且必须是一个上架转态然后按照由高到低排序最后切片取出前两位
skus = SKU.objects.filter(category_id=category_id, is_launched=True).order_by('-sales')[:2]
# 将模型列表转字典构造json数据
hot_skus = []
for sku in skus:
sku_dict = {
'id': sku.id,
'name': sku.name,
'price': sku.price,
'default_image_url': settings.STATIC_URL + 'images/goods/' +
sku.default_image.url + '.jpg'
}
hot_skus.append(sku_dict)
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'hot_skus': hot_skus})
class DetailView(View):
"""商品详情页"""
def get(self, request, sku_id):
"""提供商品详情页"""
# 获取当前sku的信息
try:
sku = SKU.objects.get(id=sku_id)
except SKU.DoesNotExist:
return HttpResponseNotFound('商品找不到')
# 查询商品频道分类
categories = get_categories()
# 查询面包屑导航
breadcrumb = get_breadcrumb(sku.category)
# 构建当前商品的规格键
sku_specs = sku.specs.order_by('spec_id')
# sku_key = []
# for spec in sku_specs:
# sku_key.append(spec.option.id)
# 获取当前商品的所有SKU
# skus = sku.spu.sku_set.all()
# 构建不同规格参数选项的sku字典
# spec_sku_map = {}
# for s in skus:
# # 获取sku的规格参数
# s_specs = s.specs.order_by('spec_id')
# # 用于形成规格参数-sku字典的键
# key = []
# for spec in s_specs:
# key.append(spec.option.id)
# # 向规格参数-sku字典添加记录
# spec_sku_map[tuple(key)] = s.id
# 获取当前商品的规格信息
# goods_specs = sku.spu.specs.order_by('id')
# 若当前sku的规格信息不完整则不再继续
# if len(sku_key) < len(goods_specs):
# return
# for index, spec in enumerate(goods_specs):
# # 复制当前sku的规格键
# key = sku_key[:]
# # 该规格的选项
# spec_options = spec.options.all()
# for option in spec_options:
# # 在规格参数sku字典中查找符合当前规格的sku
# key[index] = option.id
# option.sku_id = spec_sku_map.get(tuple(key))
# spec.spec_options = spec_options
# 渲染页面
context = {
'categories': categories,
'breadcrumb': breadcrumb,
'sku': sku,
# 'specs': goods_specs,
# 商品数量
'stock': sku.stock
}
return render(request, 'detail.html', context)
class SearchView(View):
"""商品列表页"""
def get(self, request):
search = request.GET.get('search')
skus = SKU.objects.filter(name__contains=search, is_launched=True)
# 查询商品分类
categories = get_categories()
# 构造上下文
context = {
'categories': categories,
'page_skus': skus,
}
return render(request, 'search.html', context=context)
class GoodsCommentView(View):
"""订单商品评价信息"""
def get(self, request, sku_id):
# 获取被评价的订单商品信息
order_goods_list = OrderGoods.objects.filter(sku_id=sku_id, is_commented=True).order_by('-create_time')[:30]
# 序列化
comment_list = []
for order_goods in order_goods_list:
username = order_goods.order.user.username
comment_list.append({
'username': username[0] + '***' + username[-1]
if order_goods.is_anonymous else username,
'comment': order_goods.comment,
'score': order_goods.score,
})
# print('评价信息')
# print(comment_list)
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'comment_list': comment_list})
class CategorysView(View):
def get(self, request):
category_id = request.GET.get('category_id')
if not category_id:
category1_list = cache.get('category1_list') # 读取类别1缓存数据
if not category1_list:
try:
category1_model_list = GoodsCategory.objects.filter(parent__isnull=True)
category1_list = [] # 构建类别1数据
for category1_model in category1_model_list:
category1_list.append({'id': category1_model.id, 'name': category1_model.name})
except Exception as e:
return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '类别1数据错误'})
# 存储类别缓存数据
cache.set('category1_list', category1_list, 3600)
# 响应省份数据
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'category1_list': category1_list})
else:
# 读取类别2缓存数据
sub_data = cache.get('sub_category_' + category_id)
if not sub_data:
try:
parent_model = GoodsCategory.objects.get(id=category_id) # 查询类别2的父级
sub_model_list = parent_model.subs.all()
sub_list = [] # 构建类别2数据
for sub_model in sub_model_list:
sub_list.append({'id': sub_model.id, 'name': sub_model.name})
sub_data = {
'id': parent_model.id, # 父级pk
'name': parent_model.name, # 父级name
'subs': sub_list # 父级的子集
}
except Exception as e:
return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '类别2数据错误'})
# 储存类别2缓存数据
cache.set('sub_category_' + category_id, sub_data, 3600)
# 响应类别2数据
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK', 'sub_data': sub_data})

@ -0,0 +1,21 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'xiaoyu_mall.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

@ -0,0 +1,3 @@
from django.contrib import admin
# Register your models here.

@ -0,0 +1,5 @@
from django.apps import AppConfig
class OrdersConfig(AppConfig):
name = 'orders'

@ -0,0 +1,60 @@
# Generated by Django 2.2.8 on 2023-08-25 09:37
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('goods', '0001_initial'),
('users', '0002_auto_20230825_1737'),
]
operations = [
migrations.CreateModel(
name='OrderInfo',
fields=[
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('order_id', models.CharField(max_length=64, primary_key=True, serialize=False, verbose_name='订单号')),
('total_count', models.IntegerField(default=1, verbose_name='商品总数')),
('total_amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='商品总金额')),
('freight', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='运费')),
('pay_method', models.SmallIntegerField(choices=[(1, '货到付款'), (2, '支付宝')], default=1, verbose_name='支付方式')),
('status', models.SmallIntegerField(choices=[(1, '待支付'), (2, '待发货'), (3, '待收货'), (4, '待评价'), (5, '已完成'), (6, '已取消')], default=1, verbose_name='订单状态')),
('address', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='users.Address', verbose_name='收货地址')),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL, verbose_name='下单用户')),
],
options={
'verbose_name': '订单基本信息',
'verbose_name_plural': '订单基本信息',
'db_table': 'tb_order_info',
},
),
migrations.CreateModel(
name='OrderGoods',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('create_time', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
('update_time', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
('count', models.IntegerField(default=1, verbose_name='数量')),
('price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='单价')),
('comment', models.TextField(default='', verbose_name='评价信息')),
('score', models.SmallIntegerField(choices=[(0, '0分'), (1, '20分'), (2, '40分'), (3, '60分'), (4, '80分'), (5, '100分')], default=5, verbose_name='满意度评分')),
('is_anonymous', models.BooleanField(default=False, verbose_name='是否匿名评价')),
('is_commented', models.BooleanField(default=False, verbose_name='是否评价了')),
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='skus', to='orders.OrderInfo', verbose_name='订单')),
('sku', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='goods.SKU', verbose_name='订单商品')),
],
options={
'verbose_name': '订单商品',
'verbose_name_plural': '订单商品',
'db_table': 'tb_order_goods',
},
),
]

@ -0,0 +1,78 @@
from django.db import models
# Create your models here.
from goods.models import SKU
from users.models import User, Address
from utils.models import BaseModel
class OrderInfo(BaseModel):
"""订单信息"""
PAY_METHODS_ENUM = {
"CASH": 1,
"ALIPAY": 2
}
PAY_METHOD_CHOICES = (
(1, "货到付款"),
(2, "支付宝"),
)
ORDER_STATUS_ENUM = {
"UNPAID": 1,
"UNSEND": 2,
"UNRECEIVED": 3,
"UNCOMMENT": 4,
"FINISHED": 5
}
ORDER_STATUS_CHOICES = (
(1, "待支付"),
(2, "待发货"),
(3, "待收货"),
(4, "待评价"),
(5, "已完成"),
(6, "已取消"),
)
order_id = models.CharField(max_length=64, primary_key=True, verbose_name="订单号")
user = models.ForeignKey(User, on_delete=models.PROTECT, verbose_name="下单用户")
address = models.ForeignKey(Address, on_delete=models.PROTECT, verbose_name="收货地址")
total_count = models.IntegerField(default=1, verbose_name="商品总数")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="商品总金额")
freight = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="运费")
pay_method = models.SmallIntegerField(choices=PAY_METHOD_CHOICES, default=1, verbose_name="支付方式")
status = models.SmallIntegerField(choices=ORDER_STATUS_CHOICES, default=1, verbose_name="订单状态")
class Meta:
db_table = "tb_order_info"
verbose_name = '订单基本信息'
verbose_name_plural = verbose_name
def __str__(self):
return self.order_id
class OrderGoods(BaseModel):
"""订单商品"""
SCORE_CHOICES = (
(0, '0分'),
(1, '20分'),
(2, '40分'),
(3, '60分'),
(4, '80分'),
(5, '100分'),
)
order = models.ForeignKey(OrderInfo, related_name='skus',
on_delete=models.CASCADE, verbose_name="订单")
sku = models.ForeignKey(SKU, on_delete=models.PROTECT, verbose_name="订单商品")
count = models.IntegerField(default=1, verbose_name="数量")
price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="单价")
comment = models.TextField(default="", verbose_name="评价信息")
score = models.SmallIntegerField(choices=SCORE_CHOICES, default=5, verbose_name='满意度评分')
is_anonymous = models.BooleanField(default=False, verbose_name='是否匿名评价')
is_commented = models.BooleanField(default=False, verbose_name='是否评价了')
class Meta:
db_table = "tb_order_goods"
verbose_name = '订单商品'
verbose_name_plural = verbose_name
def __str__(self):
return self.sku.name

@ -0,0 +1,3 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,13 @@
from django.contrib import admin
from django.urls import path
from .views import *
app_name = 'orders'
urlpatterns = [
# 结算订单
path('orders/settlement/', OrderSettlementView.as_view(), name='settlement'),
# 提交订单
path('orders/commit/', OrderCommitView.as_view()),
# 提交订单成功
path('orders/success/', OrderSuccessView.as_view()),
]

@ -0,0 +1,194 @@
import json
from decimal import Decimal
from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.http import HttpResponse, HttpResponseForbidden, JsonResponse
from django.shortcuts import render
# Create your views here.
from django.utils import timezone
from django.views import View
from django_redis import get_redis_connection
from goods.models import SKU
from orders.models import OrderInfo, OrderGoods
from users.models import Address
from utils.response_code import RETCODE
class OrderSettlementView(View):
def get(self, request):
# 获取登录用户
user = request.user
# 查询地址信息 模型类
try:
addresses = Address.objects.filter(user=user, is_deleted=False)
# 如果没有查询出地址,去编辑收货地址
if len(addresses) == 0:
address_list = []
# 构造上下文
context = {
'addresses': address_list # 使用空列表替代空的addresses
}
return render(request, 'user_center_site.html', context)
except Exception as e:
pass
# 查询redis购物车中被勾选的商品
redis_conn = get_redis_connection('carts')
# 所有的购物车数据,包含了勾选和未勾选 {b'1': b'1', b'2': b'2'}
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
# 被勾选的商品的sku_id[b'1']
redis_selected = redis_conn.smembers('selected_%s' % user.id)
# 构造购物车中被勾选的商品的数据 {b'1': b'1'}
new_cart_dict = {}
for sku_id in redis_selected:
new_cart_dict[int(sku_id)] = int(redis_cart[sku_id])
# 获取被勾选的商品的sku_id
sku_ids = new_cart_dict.keys()
skus = SKU.objects.filter(id__in=sku_ids)
total_count = 0
total_amount = Decimal(0.00)
# 取出所有的sku
for sku in skus:
# 遍历skus给每个sku补充count数量和amount小计
sku.count = new_cart_dict[sku.id]
sku.amount = sku.price * sku.count # Decimal类型的
# 累加数量和金额
total_count += sku.count
total_amount += sku.amount # 类型不同不能运算
freight = Decimal(10.00)
context = {
# 'addresses': addresses, # 收货地址
'skus': skus, # 商品
'total_count': total_count, # 商品总数量
'total_amount': total_amount, # 商品总金额
'freight': freight, # 运费
'payment_amount': total_amount + freight, # 实付款
}
return render(request, 'place_order.html', context)
class OrderCommitView(LoginRequiredMixin, View):
"""提交订单"""
def post(self, request):
"""保存订单基本信息和订单商品信息"""
# 接收参数
json_dict = json.loads(request.body.decode())
address_id = json_dict.get('address_id')
pay_method = json_dict.get('pay_method')
# 校验参数
if not all([address_id, pay_method]):
return HttpResponseForbidden('缺少必传参数')
# 判断address_id是否合法
try:
address = Address.objects.get(id=address_id)
except Address.DoesNotExist:
return HttpResponseForbidden('参数address_id错误')
# 判断pay_method是否合法
if pay_method not in [OrderInfo.PAY_METHODS_ENUM['CASH'], OrderInfo.PAY_METHODS_ENUM['ALIPAY']]:
return HttpResponseForbidden('参数pay_method错误')
# 获取登录用户
user = request.user
# 获取订单编号:时间+user_id
order_id = timezone.localtime().strftime('%Y%m%d%H%M%S') + ('%09d' % user.id)
# 显示开启一个事务
with transaction.atomic():
# 创建事务保存点
save_id = transaction.savepoint()
# 回滚
try:
# 保存订单基本信息(一)
order = OrderInfo.objects.create(
order_id=order_id,
user=user,
address=address,
total_count=0,
total_amount=Decimal(0.00),
freight=Decimal(10.00),
pay_method=pay_method,
status=OrderInfo.ORDER_STATUS_ENUM['UNPAID'] if pay_method == OrderInfo.PAY_METHODS_ENUM[
'ALIPAY'] else OrderInfo.ORDER_STATUS_ENUM['UNSEND']
)
# 从redis读取购物⻋中被勾选的商品信息
redis_conn = get_redis_connection('carts')
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
selected = redis_conn.smembers('selected_%s' % user.id)
carts = {}
for sku_id in selected:
carts[int(sku_id)] = int(redis_cart[sku_id])
sku_ids = carts.keys()
# 遍历购物车中被勾选的商品信息
for sku_id in sku_ids:
while True:
# 查询SKU信息
sku = SKU.objects.get(id=sku_id)
# 读取原始库存
origin_stock = sku.stock
origin_sales = sku.sales
# 判断SKU库存
sku_count = carts[sku.id]
if sku_count > sku.stock:
# 事务回滚
transaction.savepoint_rollback(save_id)
return JsonResponse({'code': RETCODE.STOCKERR, 'errmsg': '库存不足'})
# # SKU减少库存增加销量
# sku.stock -= sku_count
# sku.sales += sku_count
# sku.save()
new_stock = origin_stock - sku_count
new_sales = origin_sales + sku_count
# 基于乐观锁的数据更新
result = SKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock,
sales=new_sales)
# 如果下单失败,但库存充足,继续下单,直到下单成功或库存不足
if result == 0:
continue
# 修改SPU销量
sku.spu.sales += sku_count
sku.spu.save()
# 保存订单商品信息 OrderGoods
OrderGoods.objects.create(
order=order,
sku=sku,
count=sku_count,
price=sku.price,
)
# 保存商品订单中总价和总数量
order.total_count += sku_count
order.total_amount += (sku_count * sku.price)
# 下单成功,跳出循环
break
# 添加邮费和保存订单信息
order.total_amount += order.freight
order.save()
except Exception as e:
transaction.savepoint_rollback(save_id) # 出错回滚
return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '下单失败'})
# 清除购物车中已结算的商品
pl = redis_conn.pipeline()
pl.hdel('carts_%s' % user.id, *selected)
pl.srem('selected_%s' % user.id, *selected)
pl.execute()
# 响应提交订单结果
return JsonResponse({'code': RETCODE.OK, 'errmsg': '下单成功', 'order_id': order.order_id})
class OrderSuccessView(LoginRequiredMixin, View):
"""提交订单成功页面"""
def get(self, request):
"""提供提交订单成功页面"""
order_id = request.GET.get('order_id')
payment_amount = request.GET.get('payment_amount')
pay_method = request.GET.get('pay_method')
context = {
'order_id': order_id,
'payment_amount': payment_amount,
'pay_method': pay_method
}
return render(request, 'order_success.html', context)

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save