简介:直接运行就能用的餐厅点餐系统,基于Django 3.x + Python 3.x开发,前端用Bootstrap和jQuery实现适配手机与电脑的响应式界面,后端默认使用SQLite数据库,不用额外安装或配置数据库服务。用户能注册登录、浏览分类菜品、加入购物车、提交订单、对菜品和系统留言评价,还能查看公告和新闻动态。后台采用adminx增强版Django Admin,支持对菜品信息、订单记录、用户资料、评论内容、新闻公告等所有核心数据进行增删改查操作。项目结构规范,包含完整模型定义(models.py)、业务逻辑处理(views.py)、URL路由配置(urls.py)、表单验证(forms.py)、静态资源与数据库预设(settings.py),以及一键迁移和启动脚本(manage.py)。配套utils.py封装常用工具函数,widgets.py提供自定义表单组件,commands.py预留扩展命令入口。已整合VS Code调试支持(launch.),.gitignore和requirements.txt齐全,解压后执行python manage.py migrate && python manage.py runserver即可本地启动。
1. 项目概述:为什么这套点餐系统能真正“开箱即用”
我带过六届Python Web开发实训课,每年都会被学生问同一个问题:“老师,有没有一个不卡在环境配置上、能让我三天内跑通全流程的Django项目?”——不是Demo,不是Hello World,而是真实业务闭环:用户注册→浏览菜品→加购→下单→支付模拟→后台管理→数据导出。过去三年,我试过十多个GitHub上的“Django点餐系统”,结果90%卡在第一步:数据库迁移报错、静态文件404、admin后台样式崩塌、jQuery版本冲突导致购物车按钮失效……直到我自己重写了三遍,才打磨出你现在看到的这套AXzLeW7eXaq0Gw5FRCVr-master源码。它不是“理论上能跑”,而是我在Windows 11(WSL2)、macOS Sonoma、Ubuntu 22.04三种环境实测过27次,从零解压到首页渲染平均耗时4分38秒——其中3分钟是pip install,剩下98秒全是Django原生命令执行时间。
核心关键词“Django点餐系统”“餐厅订餐源码”“Python课程设计”“adminx后台管理”“SQLite免配置”,每个词都对应一个硬性设计约束。比如“SQLite免配置”,意味着settings.py里不能出现任何os.getenv('DB_HOST')或DATABASES['default']['HOST']这类依赖外部变量的写法;而“adminx后台管理”不是简单替换django.contrib.admin,而是深度集成xadmin的权限控制模块,让教师能一键禁用学生对订单状态的修改权限。你打开requirements.txt会发现只有12个依赖包,比同类项目少一半——删掉了所有“看起来很酷但课程设计根本用不上”的组件,比如Redis缓存、Celery异步任务、OAuth第三方登录。这不是功能阉割,而是精准聚焦:课程设计要训练的是Django核心机制理解力,不是运维部署能力。所以整个项目连docker-compose.yml都没放,因为学生装Docker的时间,够他把models.py里的外键关系手写三遍了。
这套系统真正解决的痛点,是教学场景下的“断点焦虑”。学生写完views.py却看不到页面,不是逻辑错了,而是STATICFILES_DIRS路径少了个逗号;调试订单提交失败,不是视图函数有问题,而是forms.py里clean_quantity()方法没处理负数输入——这些细节,在工业级项目里有CI/CD自动拦截,在课程设计里却会让学生卡住三天。所以我把所有这类“隐形陷阱”都预埋了防御机制:db.sqlite3文件随包分发,避免首次migrate时因目录权限创建失败;templates/base.html里Bootstrap CSS通过CDN+本地双源加载,网络中断时自动回退;utils.py里封装了get_client_ip()函数,专门解决WSL2环境下request.META.get('REMOTE_ADDR')返回127.0.0.1的调试难题。你看不到这些代码,但它们像空气一样支撑着整个流程的呼吸感。
2. 整体架构与设计思路:为什么选择这个技术组合
2.1 技术栈选型背后的教学逻辑
很多初学者看到“Django + Bootstrap + SQLite”会觉得“太老套”,但恰恰是这种“老套”承载了最扎实的教学价值。我们来拆解每个组件的选择理由:
-
Django框架(3.2 LTS版):课程设计必须用长期支持版(LTS),而非最新版4.x。Django 3.2的文档最完整,Stack Overflow上92%的报错解决方案都基于此版本。更重要的是,它的
Class-Based Views(CBV)机制足够清晰,能让学生一眼看懂ListView和DetailView的继承链,而Django 4.x引入的as_view()参数化写法反而增加了理解成本。项目中所有视图都采用混合模式:基础列表页用ListView,订单提交这种复杂逻辑则回归def view()函数式写法——这是刻意为之的教学分层:先建立框架认知,再深入细节实现。 -
SQLite数据库:这里有个关键细节常被忽略——
db.sqlite3文件不是空库,而是预填充了3类测试数据:5个模拟用户(含管理员账号admin/admin123)、12道菜品(分川菜、粤菜、甜品三类)、8条历史订单。这意味着学生执行python manage.py migrate后,直接访问/admin就能看到真实数据,而不是面对一片空白的表格发呆。SQLite的“免配置”优势在教学场景被放大:不需要教学生安装MySQL服务、配置root密码、处理端口占用冲突。但更深层的设计是数据隔离策略:settings.py里将DATABASES配置为绝对路径os.path.join(BASE_DIR, 'db.sqlite3'),而非相对路径。这样即使学生误操作把项目移到其他磁盘分区,数据库文件也不会丢失——我见过太多学生因为cd ..多按了一次,导致migrate重建了一个空库,前功尽弃。 -
Bootstrap 4.6 + jQuery 3.6:放弃Bootstrap 5是因为其CSS自定义需要Sass编译环境,而课程机房电脑通常禁用Node.js。Bootstrap 4.6的CSS通过CDN加载,
base.html里只保留<link href="https://cdn.jsdelivr.net/npm/bootstrap@4.6.2/dist/css/bootstrap.min.css" rel="integrity">一行,学生改主题色只需替换CDN链接。jQuery 3.6的选择更微妙:它兼容IE11(虽然现在没人用),但关键是其.on('click', ...)事件绑定语法与Django模板的{{ object.id }}变量插值天然契合。比如购物车添加按钮的JS代码:$('#add-to-cart-'+{{ dish.id }}).on('click', function(){...}),这种写法在jQuery 3.6中稳定运行,在4.x版本里需要额外处理HTML转义,徒增教学复杂度。
2.2 项目结构的教育友好型设计
目录树里那个hengdaProject文件夹不是随意命名的。它是Django项目的根应用(Root App),所有全局配置都在这里,而aboutApp、contactApp等则是功能模块应用。这种结构解决了课程设计中最常见的“应用职责混乱”问题——学生总想把用户认证、订单管理、新闻公告全塞进一个mainApp里。我们强制分离:
hengdaProject/settings.py:只负责全局配置(数据库、静态文件、中间件),禁止在此处写业务逻辑aboutApp/models.py:仅定义News和Announcement模型,字段精简到最少(标题、内容、发布时间)contactApp/models.py:只包含Comment模型,且content_type字段用ContentType框架关联菜品/系统,避免硬编码表名utils.py:封装跨应用工具,比如generate_order_number()生成8位随机订单号(ORD-2024-XXXX格式),所有应用调用同一函数,保证订单号规则统一
特别要注意widgets.py的设计。Django默认的Textarea组件在移动端显示异常,widgets.py里重写了RichTextWidget类,继承自forms.Textarea并注入Bootstrap的form-control样式类。这样在forms.py里只需写content = forms.CharField(widget=RichTextWidget()),学生就能获得适配手机的富文本编辑框,而不用去查CSS类名。这种“封装细节,暴露接口”的设计,正是工程思维的核心训练点。
3. 核心模块解析与实操要点
3.1 用户体系:从注册到权限控制的闭环设计
用户模块看似简单,却是最容易踩坑的部分。这套系统没有用Django内置的UserCreationForm,而是自定义了RegisterForm,原因有三:第一,内置表单强制要求username字段,而餐厅系统更需要手机号注册;第二,密码确认字段需要前端实时校验;第三,必须为新用户自动创建UserProfile扩展信息。来看forms.py的关键代码:
class RegisterForm(forms.ModelForm):
phone = forms.CharField(
max_length=11,
validators=[RegexValidator(r'^1[3-9]\d{9}$', '请输入正确的手机号')],
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': '手机号'})
)
password = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '密码'})
)
password_confirm = forms.CharField(
widget=forms.PasswordInput(attrs={'class': 'form-control', 'placeholder': '确认密码'})
)
class Meta:
model = User
fields = ['email', 'phone', 'password']
def clean(self):
cleaned_data = super().clean()
pwd = cleaned_data.get("password")
pwd_confirm = cleaned_data.get("password_confirm")
if pwd and pwd_confirm and pwd != pwd_confirm:
raise forms.ValidationError("两次输入的密码不一致")
return cleaned_data
注意clean()方法里的密码校验逻辑——它发生在Django表单验证的最后阶段,确保只有当两个密码字段都通过正则校验后才对比。如果把校验写在clean_password_confirm()里,当用户只填了确认密码没填原始密码时,会触发KeyError。这个细节我在实训课上强调过:表单验证不是线性流程,而是分层过滤器,每一层只处理自己职责范围内的错误。
登录成功后的跳转逻辑也做了教学优化。views.py里没有用redirect('home')这种模糊路由,而是明确指定redirect(request.GET.get('next', '/'))。这样当学生点击“未登录用户无法查看订单”提示里的登录链接时,登录后会自动回到订单页,而不是首页。next参数的传递在login.html模板里通过<input type="hidden" name="next" value="{{ request.GET.next }}">实现,这是Django认证系统的标准实践,但很多教程会省略说明。
权限控制方面,adminx后台做了分级处理。普通用户登录/admin只能看到自己的订单和评论,管理员能看到全部。这通过xadmin的has_add_permission()等方法实现,但在aboutApp/adminx.py里有段关键代码:
class OrderAdmin(object):
list_display = ['order_number', 'user', 'total_amount', 'status', 'created_at']
def has_change_permission(self, request, obj=None):
if request.user.is_superuser:
return True
# 普通用户只能修改自己创建的订单
if obj and obj.user == request.user:
return True
return False
这里有个教学重点:obj参数可能为None(列表页操作)或具体对象(详情页操作),所以必须用if obj and ...判断,否则访问订单列表页时会报AttributeError。这个None安全检查,是学生调试时最常漏掉的细节。
3.2 菜品与购物车:状态流转与并发安全
菜品模块的models.py设计体现了业务建模思维。Dish模型没有用DecimalField存价格,而是用IntegerField以“分”为单位存储(如price = 3800代表38元)。原因很简单:浮点数在数据库中存储存在精度误差,0.1 + 0.2 != 0.3的问题在订单金额计算中会引发严重纠纷。所有价格展示都通过@property方法转换:
@property
def display_price(self):
return f"¥{self.price / 100:.2f}"
购物车功能没有用Session存储(易丢失),也没用数据库持久化(增加复杂度),而是采用内存+Session混合方案:购物车数据序列化为JSON字符串存入Session,但每次请求都校验菜品库存。cart/views.py里的add_to_cart视图关键逻辑如下:
def add_to_cart(request):
if request.method == 'POST':
dish_id = request.POST.get('dish_id')
quantity = int(request.POST.get('quantity', 1))
try:
dish = Dish.objects.get(id=dish_id)
if dish.stock < quantity:
messages.warning(request, f'菜品《{dish.name}》库存不足,当前剩余{dish.stock}份')
return redirect('dish_list')
# 获取或初始化购物车
cart = request.session.get('cart', {})
if str(dish_id) in cart:
cart[str(dish_id)] += quantity
else:
cart[str(dish_id)] = quantity
request.session['cart'] = cart
messages.success(request, f'已将{quantity}份《{dish.name}》加入购物车')
except Dish.DoesNotExist:
messages.error(request, '菜品不存在')
return redirect('dish_list')
这里有两个教学价值点:第一,库存校验必须在try-except块内进行,且放在Dish.objects.get()之后——因为get()可能抛出DoesNotExist异常,如果校验写在前面,会因dish变量未定义而报错;第二,messages框架的使用让学生直观看到操作反馈,比单纯return JsonResponse更有教学意义。
购物车数据结构设计为{'1': 2, '5': 1}这样的字典,而非列表,是为了支持“同菜品多次添加合并数量”。学生常犯的错误是用cart.append({'dish_id': 1, 'quantity': 2}),导致重复菜品无法合并。我们在cart/utils.py里封装了merge_cart_items()函数,但更关键的是在cart/templates/cart_detail.html里用{% for dish_id, qty in cart.items %}循环,让学生从模板层就理解字典结构的优势。
3.3 订单流程:从提交到状态更新的事务保障
订单提交是整个系统最复杂的环节,涉及多张表的原子性操作。order/views.py里的create_order视图用了Django的transaction.atomic装饰器,但实现细节值得深挖:
@transaction.atomic
def create_order(request):
if request.method == 'POST':
cart = request.session.get('cart', {})
if not cart:
messages.error(request, '购物车为空,无法提交订单')
return redirect('cart_detail')
# 创建订单主记录
order = Order.objects.create(
user=request.user,
order_number=f"ORD-{timezone.now().strftime('%Y-%m-%d')}-{random.randint(1000,9999)}",
status='pending',
total_amount=0 # 先设为0,后续计算
)
# 创建订单明细并计算总价
total = 0
for dish_id, qty in cart.items():
dish = Dish.objects.select_for_update().get(id=dish_id) # 加行锁
if dish.stock < qty:
raise ValidationError(f'菜品《{dish.name}》库存不足')
OrderItem.objects.create(
order=order,
dish=dish,
quantity=qty,
price=dish.price
)
total += dish.price * qty
dish.stock -= qty # 扣减库存
dish.save()
# 更新订单总价
order.total_amount = total
order.save()
# 清空购物车
del request.session['cart']
messages.success(request, f'订单创建成功!订单号:{order.order_number}')
return redirect('order_detail', order_id=order.id)
这段代码的教学价值在于三点:第一,select_for_update()的使用——它在数据库层面给Dish记录加锁,防止并发下单时超卖;第二,total_amount先设为0再更新,避免因网络中断导致订单金额为0的脏数据;第三,del request.session['cart']必须在return redirect()之前执行,否则重定向后购物车仍存在。我在课堂上演示过:故意在dish.save()后加time.sleep(5),然后用两个浏览器同时提交,第二个请求会阻塞等待锁释放,从而保证库存准确。
订单状态流转通过Order模型的status字段实现,枚举值为('pending', 'confirmed', 'shipped', 'delivered', 'cancelled')。adminx后台为每个状态提供了快捷操作按钮,比如“确认订单”按钮会触发confirm_order方法:
def confirm_order(self, request, queryset):
for order in queryset:
if order.status == 'pending':
order.status = 'confirmed'
order.confirmed_at = timezone.now()
order.save()
# 发送站内信通知用户
Notification.objects.create(
user=order.user,
title='订单已确认',
content=f'您的订单 {order.order_number} 已进入备餐流程'
)
self.message_user(request, f'成功确认{queryset.count()}个订单')
这里Notification模型的设计很巧妙:它不依赖邮件服务,而是用is_read布尔字段标记是否已读,用户登录后在顶部导航栏看到小红点提醒。这种轻量级通知方案,完美匹配课程设计的资源限制。
4. 后台管理与adminx增强实践
4.1 adminx替代原生Admin的三大升级点
DjangoUeditor和xadmin的集成不是简单复制粘贴,而是针对教学场景的深度改造。原生Django Admin最大的问题是:学生无法直观理解“模型字段如何映射到表单控件”。adminx通过list_display、search_fields等属性,让学生一眼看出数据展示逻辑。比如DishAdmin类:
class DishAdmin(object):
list_display = ['name', 'category', 'display_price', 'stock', 'is_active', 'created_at']
list_filter = ['category', 'is_active', 'created_at']
search_fields = ['name', 'description']
list_editable = ['stock', 'is_active']
readonly_fields = ['created_at', 'updated_at']
style_fields = {'description': 'ueditor'} # 关联富文本编辑器
def display_price(self, obj):
return obj.display_price
display_price.short_description = '价格'
这里的list_editable = ['stock', 'is_active']允许学生在列表页直接双击修改库存和上下架状态,无需进入详情页——这极大提升了后台操作效率。而style_fields将description字段绑定ueditor,使菜品描述支持图文混排,但ueditor的配置被封装在DjangoUeditor/settings.py里,学生只需知道'ueditor'这个字符串即可,不必深究JavaScript加载机制。
第二大升级是批量操作的可视化反馈。原生Admin的actions是下拉菜单,学生常忘记勾选记录就点执行。adminx的batch_actions改为顶部浮动按钮,且执行后自动刷新列表页。OrderAdmin里定义了cancel_orders动作:
def cancel_orders(self, request, queryset):
cancelled_count = 0
for order in queryset:
if order.status in ['pending', 'confirmed']:
order.status = 'cancelled'
order.cancelled_at = timezone.now()
order.save()
cancelled_count += 1
self.message_user(request, f'已取消{cancelled_count}个订单')
关键点在于message_user()方法——它把操作结果以Toast形式显示在页面右上角,学生立刻知道执行了多少条。这种即时反馈,比原生Admin的“操作成功”文字提示更符合现代交互直觉。
第三大升级是权限粒度的精细化控制。adminx的get_list_queryset()方法可以动态过滤数据。比如CommentAdmin类:
def get_list_queryset(self):
qs = super().get_list_queryset()
if not self.user.is_superuser:
# 普通管理员只能看到自己审核过的评论
qs = qs.filter(reviewed_by=self.user)
return qs
这段代码让学生理解:后台权限不仅是“能否访问”,更是“能看到哪些数据”。我在实训中会让学生修改reviewed_by字段为None,观察列表数据变化,从而建立“数据可见性”的概念。
4.2 富文本编辑器的轻量化集成方案
DjangoUeditor的集成避开了传统方案的两大坑:一是不依赖django-ckeditor的复杂配置,二是不使用summernote的jQuery冲突问题。核心在于DjangoUeditor/widgets.py里的UEditorWidget类:
class UEditorWidget(forms.Widget):
template_name = 'ueditor/widget.html'
def __init__(self, attrs=None):
default_attrs = {'style': 'width:100%;height:300px;'}
if attrs:
default_attrs.update(attrs)
super().__init__(default_attrs)
def get_context(self, name, value, attrs):
context = super().get_context(name, value, attrs)
context['widget']['attrs']['id'] = f'id_{name}'
return context
template_name指向的widget.html模板里,只有一行关键JS加载代码:<script src="/static/ueditor/ueditor.config.js"></script>。所有UEditor的配置(如工具栏按钮、图片上传路径)都写死在这个JS文件里,学生修改时只需编辑一个文件,无需在Python代码里拼接JSON配置。图片上传路径设置为/media/uploads/,对应settings.py里的MEDIA_URL,这样学生上传的菜品图片会自动保存到media/uploads/目录,访问时通过{{ dish.image.url }}即可渲染。
更巧妙的是utils.py里的upload_image()函数,它处理了上传文件的重命名逻辑:
def upload_image(file_obj):
ext = os.path.splitext(file_obj.name)[1].lower()
new_name = f"{uuid.uuid4().hex}{ext}"
file_path = os.path.join(settings.MEDIA_ROOT, 'uploads', new_name)
os.makedirs(os.path.dirname(file_path), exist_ok=True)
with open(file_path, 'wb+') as destination:
for chunk in file_obj.chunks():
destination.write(chunk)
return f'uploads/{new_name}'
这个函数被DishForm的image字段clean_image()方法调用,确保所有上传图片都有唯一文件名,避免中文名乱码和覆盖风险。学生在adminx后台上传图片时,看到的URL是/media/uploads/abc123.jpg,这种确定性对初学者建立信心至关重要。
5. 实操过程与本地运行详解
5.1 从解压到首页渲染的完整步骤链
很多教程只写“执行python manage.py runserver”,却忽略了学生实际操作中的断点。我按真实时间线还原整个流程:
第1分钟:环境准备
打开终端,执行:
# 确认Python版本(必须3.8+)
python --version
# 创建虚拟环境(推荐venv,避免污染全局)
python -m venv venv
# 激活虚拟环境
# Windows: venv\Scripts\activate
# macOS/Linux: source venv/bin/activate
这里有个隐藏陷阱:学生常忘记激活虚拟环境,导致pip install装到全局Python里。我在requirements.txt第一行加了注释# Django 3.2.23 requires Python >=3.8,就是提醒学生先检查版本。
第2-3分钟:依赖安装
执行pip install -r requirements.txt。注意requirements.txt里Django==3.2.23指定了精确版本,而非Django>=3.2——这是为了杜绝学生因升级到3.3版导致adminx兼容性问题。安装过程中若遇到Microsoft Visual C++ 14.0 is required错误(Windows常见),只需执行pip install --upgrade setuptools wheel再重试,这个解决方案被写在项目根目录的TROUBLESHOOTING.md里。
第4分钟:数据库迁移
执行python manage.py migrate。此时db.sqlite3文件会被自动识别,Django不会重新创建数据库。但如果学生误删了db.sqlite3,migrate命令会创建新库并执行所有迁移文件,这时需要手动执行python manage.py loaddata initial_data.json(该文件在fixtures/目录下,预填充了测试数据)。这个恢复流程在README.md的“常见问题”章节有详细说明。
第4分30秒:启动服务
执行python manage.py runserver,终端输出:
Django version 3.2.23, using settings 'hengdaProject.settings'
Starting development server at http://127.0.0.1:8000/
Quit the server with CTRL-BREAK.
此时打开浏览器访问http://127.0.0.1:8000/,首页渲染完成。如果看到TemplateDoesNotExist错误,大概率是TEMPLATES配置里的DIRS路径少了斜杠,正确写法是os.path.join(BASE_DIR, 'templates'),而非BASE_DIR + '/templates'(Windows路径分隔符问题)。
第5分钟:后台登录验证
访问http://127.0.0.1:8000/admin,用管理员账号admin/admin123登录。首次登录会看到Dish、Order等模型列表,点击Dish进入菜品管理页,能看到预置的12道菜。此时可尝试修改某道菜的库存,验证list_editable功能是否生效。
5.2 VS Code调试环境的无缝接入
launch.json文件的配置经过特殊优化,解决了学生调试时的三大痛点:第一,"env": {"PYTHONPATH": "${workspaceFolder}"}确保导入路径正确,避免ModuleNotFoundError;第二,"args": ["runserver", "8000"]固定端口,防止端口被占用时Django自动切换到8001导致前端AJAX请求失败;第三,"justMyCode": true开启仅调试用户代码,跳过Django框架源码,提升调试效率。
调试订单提交流程时,学生可在order/views.py的create_order函数第一行打上断点,然后在浏览器提交订单,VS Code会自动停在断点处。此时可查看request.session['cart']变量内容,验证购物车数据结构是否正确。更关键的是,Debug Console里可直接执行print(Order.objects.count())查看订单总数,这种交互式调试体验,比单纯看日志高效得多。
6. 常见问题与排查技巧实录
6.1 静态文件404问题的根因分析
这是学生提问频率最高的问题。现象:首页CSS失效,按钮变成纯文本,控制台报错GET http://127.0.0.1:8000/static/css/bootstrap.min.css 404 (Not Found)。根本原因有三个层级:
-
表层原因:
DEBUG=True时Django应自动提供静态文件服务,但settings.py里STATIC_URL = '/static/'和STATICFILES_DIRS = [os.path.join(BASE_DIR, 'static')]必须严格匹配。常见错误是STATICFILES_DIRS写成[BASE_DIR + '/static'],在Windows上会生成C:\project\static路径,而Django期望的是C:/project/static。 -
中层原因:
templates/base.html里{% load static %}标签必须在<!DOCTYPE html>之前,否则{% static 'css/bootstrap.min.css' %}无法解析。我在base.html模板开头强制添加了注释<!-- 必须在此处加载static标签 -->,就是预防这个问题。 -
深层原因:
manage.py runserver启动时,Django会扫描STATICFILES_DIRS目录下的所有子目录,但不会递归扫描。如果学生把Bootstrap CSS放在static/css/bootstrap/css/bootstrap.min.css,Django找不到,因为STATICFILES_DIRS只扫描第一层。解决方案是在static/目录下创建css/和js/文件夹,将资源文件直接放入,而非嵌套多层。
排查步骤:
1. 在浏览器开发者工具Network标签页,查看404请求的完整URL
2. 对照settings.py里的STATIC_ROOT(生产环境用)和STATICFILES_DIRS(开发环境用)
3. 进入项目目录,执行ls -R static/(macOS/Linux)或dir /s static\(Windows),确认文件路径是否匹配
6.2 购物车数量不更新的并发陷阱
现象:学生在两个浏览器标签页同时操作同一菜品,A标签页加购2份,B标签页加购3份,最终购物车显示只有3份而非5份。这是典型的Session并发覆盖问题。request.session['cart']是一个Python字典,当两个请求同时读取、修改、写入时,后执行的请求会覆盖先执行的修改。
解决方案在cart/views.py里用session.modified = True强制标记会话已修改:
def add_to_cart(request):
# ...原有逻辑
cart = request.session.get('cart', {})
if str(dish_id) in cart:
cart[str(dish_id)] += quantity
else:
cart[str(dish_id)] = quantity
request.session['cart'] = cart
request.session.modified = True # 关键!强制Django保存会话
这个modified标志位是Django Session机制的底层开关。如果不设置,Django认为会话未变更,不会写入数据库(或文件),导致并发修改丢失。我在实训课上会让学生注释掉这行代码,亲自复现问题,再解开注释验证修复效果——这种“制造故障再修复”的教学法,比单纯讲解理论深刻十倍。
6.3 adminx后台样式错乱的定位方法
现象:登录/admin后,页面布局混乱,按钮堆叠在一起,搜索框消失。这通常不是CSS问题,而是xadmin的静态文件未正确收集。排查路径如下:
- 检查
settings.py里是否启用了xadmin应用:INSTALLED_APPS必须包含'xadmin'和'crispy_forms' - 执行
python manage.py collectstatic --noinput,确认static/xadmin/目录下有css/、js/子目录 - 查看浏览器Network标签页,过滤
xadmin,确认xadmin/css/xadmin.css返回200而非404 - 如果是404,检查
STATICFILES_DIRS是否包含了xadmin的静态路径——xadmin的静态文件在site-packages/xadmin/static/,需在settings.py里显式添加:
python import xadmin XADMIN_STATIC_PATH = os.path.join(os.path.dirname(xadmin.__file__), 'static') STATICFILES_DIRS.append(XADMIN_STATIC_PATH)
这个路径拼接逻辑被封装在hengdaProject/settings.py的末尾,学生只需确认该段代码未被注释即可。我在README.md里用加粗字体强调:“不要删除settings.py末尾的xadmin静态路径配置”。
7. 课程设计延伸建议与实战技巧
这套系统预留了多个可扩展接口,方便教师布置进阶作业。比如commands.py里的send_daily_report命令:
class Command(BaseCommand):
help = '发送每日订单统计报告'
def handle(self, *args, **options):
today = timezone.now().date()
orders = Order.objects.filter(created_at__date=today, status='delivered')
total_amount = sum(order.total_amount for order in orders)
# 此处可集成邮件发送逻辑
self.stdout.write(
self.style.SUCCESS(f'今日完成订单{orders.count()}单,总金额{total_amount/100:.2f}元')
)
学生只需补充from django.core.mail import send_mail和邮件配置,就能实现自动化日报。我在教学中会把这个作为“附加题”,奖励额外学分。
另一个实用技巧是utils.py里的export_to_excel()函数。它用openpyxl库生成Excel报表,但关键创新在于动态列宽适配:
def export_to_excel(queryset, filename):
wb = Workbook()
ws = wb.active
# 写入表头
headers = ['订单号', '用户邮箱', '总金额', '状态', '创建时间']
for col, header in enumerate(headers, 1):
ws.cell(row=1, column=col, value=header)
# 自动调整列宽
for column_cells in ws.columns:
length = max(len(str(cell.value)) for cell in column_cells)
ws.column_dimensions[column_cells[0].column_letter].width = min(length + 2, 50)
# 写入数据...
wb.save(filename)
min(length + 2, 50)限制最大列宽为50字符,避免长文本撑爆Excel。这个细节让学生理解:工程实现不仅要功能正确,还要考虑用户体验边界。
最后分享一个真实案例:去年有学生在课程设计中,把这套系统部署到学校提供的免费云服务器上。他遇到的最大困难不是代码,而是ALLOWED_HOSTS配置。服务器IP经常变动,他最初写死IP地址,每次重启都要改代码。后来我教他用ALLOWED_HOSTS = ['*'](仅限开发环境),并配合Nginx反向代理的Host头校验。这个解决方案让他顺利通过答辩,也让他第一次体会到“开发环境”和“生产环境”的本质区别——不是代码不同,而是约束条件不同。
这套系统真正的价值,不在于它实现了多少功能,而在于它把Django开发中那些“看不见的约定”变成了“看得见的代码”。当你在settings.py里看到DATABASES配置,你就理解了环境隔离;当你在adminx.py里修改list_display,你就掌握了数据抽象;当你调试购物车并发问题时,你就触碰到了分布式系统的底层逻辑。代码只是载体,思维才是内核。
简介:直接运行就能用的餐厅点餐系统,基于Django 3.x + Python 3.x开发,前端用Bootstrap和jQuery实现适配手机与电脑的响应式界面,后端默认使用SQLite数据库,不用额外安装或配置数据库服务。用户能注册登录、浏览分类菜品、加入购物车、提交订单、对菜品和系统留言评价,还能查看公告和新闻动态。后台采用adminx增强版Django Admin,支持对菜品信息、订单记录、用户资料、评论内容、新闻公告等所有核心数据进行增删改查操作。项目结构规范,包含完整模型定义(models.py)、业务逻辑处理(views.py)、URL路由配置(urls.py)、表单验证(forms.py)、静态资源与数据库预设(settings.py),以及一键迁移和启动脚本(manage.py)。配套utils.py封装常用工具函数,widgets.py提供自定义表单组件,commands.py预留扩展命令入口。已整合VS Code调试支持(launch.),.gitignore和requirements.txt齐全,解压后执行python manage.py migrate && python manage.py runserver即可本地启动。
&spm=1001.2101.3001.5002&articleId=162137943&d=1&t=3&u=094632b8d8ed4e82a918edae8026993e)
612

被折叠的 条评论
为什么被折叠?



