news 2026/9/30 3:22:19

Django+Vue前后端分离实战:从零开发羽毛球交流平台

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django+Vue前后端分离实战:从零开发羽毛球交流平台

羽毛球圈的球友组织活动,之前一直靠微信群接龙,消息一多就乱、报名状态靠人工盯、场地信息散在各个群里。我做了个小而全的解决方案,用 Django 做后端、Vue 做前端,搭了一个羽毛球交流平台。这个项目麻雀虽小但五脏俱全,涉及用户登录、活动发布、组队报名、场地管理、实时消息推送这些典型场景。这篇文章把我从设计到实操踩过的坑完整捋一遍,适合刚学完 Django 基础、想找一个完整项目练手的前端同学,也适合已经在做前后端分离项目、想看看别人怎么处理联调问题的朋友。

现在的社区类项目,技术栈无非就是那几套,为什么我最终定了 Django + Vue?一句话说清楚:Django 的 ORM 和管理后台能让我在最短时间内把业务模型跑起来,Vue 的组件化开发则让前端页面维护起来不痛苦。前后端分离的架构下,两边可以独立开发、独立测试,后面我要加移动端 H5 或者小程序,后端接口可以直接复用。

先说项目背景。羽毛球圈子有个真实痛点:约球信息散落、报名统计麻烦、临时取消通知不到位。那么一个交流平台至少要解决这几个问题:用户注册登录后能看到附近球局、可以发布新活动、能一键报名/取消、活动状态变化要实时通知到场人员。再加上一个基础的球友留言区,让不在一起打球的人也能交流技术心得或者找固定球搭子。后端我用 Django 3.2 + Django REST Framework,前端用 Vue 3 + Vite + Element Plus,数据库用的 MySQL,缓存和实时推送用 Redis。这套选型不是最潮的,但每一环都稳定,社区资料也多,遇到问题基本都能搜到答案。

1. 项目整体设计与思路拆解

1.1 核心业务模块梳理

我把整个平台拆成了五个模块,每个模块对应一组数据模型和一组接口:

  • 用户模块:注册、登录、个人资料、我的活动列表。
  • 活动模块:活动发布、活动列表、活动详情、报名/取消报名。
  • 场地模块:场地信息管理、场地空闲状态。
  • 消息模块:站内通知、WebSocket 实时推送。
  • 交流模块:球友留言、帖子发布。

为什么不做一个大而全的社区?因为羽毛球交流平台的核心价值是"约球 + 找人",社交属性应该围绕活动展开,而不是做成一个泛论坛。所以留言区我刻意做得轻量,只有简单的发帖和回帖,不做私信、不做好友关系,这样后端逻辑不膨胀,开发周期能控制住。

1.2 前后端分离的架构选择

前后端分离是这几年做 Web 项目的默认姿势,但很多人只知其然不知其所以然。我实际对比过传统 Django 模板渲染和前后端分离两种方案,最终选分离的原因有三个:

第一,模板渲染的项目后期改版成本高。做活动列表页的时候,我可能只想改一个排序逻辑,但因为模板和业务代码耦合,一个改动牵连好几个文件。分离之后,前端只管调接口渲染数据,后端只管接口逻辑,职责边界清清楚楚。

第二,移动端复用接口。羽毛球球友很多在场地上直接掏手机看信息,后续大概率要做 H5 或者小程序。前后端分离模式下,接口写好之后,多端共用一套,不用像模板渲染那样重写一遍后端逻辑。

第三,团队协作效率。哪怕你现在是单人开发,分离之后你也能在启动 Vue 的开发服务器时同时调试 Django 接口。我用 Vite 的 proxy 把/api请求转发到 Django 的 8000 端口,开发时两边都不用管跨域。

1.3 为什么选 Django 而不是其他后端框架

我承认 Node 的 Express、Python 的 FastAPI 都能干这活,但 Django 在这类业务型项目里有几个别人替代不了的优势:

  • 自带 Admin 后台。球场管理员可以登录 Django admin 直接维护场地数据,不需要我先做一个管理页面。这个对个人开发者的意义很大,省掉一个完整的前端管理平台。
  • ORM 写复杂查询省力。羽毛球活动需要按地理位置、时间、人数筛选,Django ORM 的filter()链式调用配合Q对象,几行代码就能组合出复杂的查询条件。
  • 生态成熟。认证、权限、分页、序列化,Django REST Framework 全都内置了最佳实践。我做 JWT 登录用的djangorestframework-simplejwt,做 WebSocket 用的channels,都是经过大规模项目验证的方案。

如果项目规模再大两个量级,我可能会考虑 FastAPI 的异步性能优势,但对于一个羽毛球交流平台这种业务密集型项目(大部分时间花在数据库查询和业务判断上),Django 的成熟稳定反而是更重要的属性。

2. 后端核心细节:数据模型、认证与实时通信

2.1 数据模型设计:三张核心业务表

写任何项目,我习惯先把数据模型想清楚。这个平台我用五张表支撑核心流程:User(复用 Django 自带的)、Venue(场地)、Activity(活动)、Enrollment(报名记录)、Message(站内消息)。其中活动表是最关键的,它长这样:

from django.db import models from django.contrib.auth.models import AbstractUser class Venue(models.Model): name = models.CharField("场地名称", max_length=100) address = models.CharField("详细地址", max_length=255) court_count = models.IntegerField("场地数量", default=1) price_per_hour = models.DecimalField("每小时价格", max_digits=6, decimal_places=2) class Activity(models.Model): STATUS_CHOICES = [ ("recruiting", "招募中"), ("full", "已满员"), ("in_progress", "进行中"), ("finished", "已结束"), ("cancelled", "已取消"), ] title = models.CharField("活动标题", max_length=100) venue = models.ForeignKey(Venue, on_delete=models.CASCADE, related_name="activities") organizer = models.ForeignKey("auth.User", on_delete=models.CASCADE, related_name="organized_activities") start_time = models.DateTimeField("开始时间") end_time = models.DateTimeField("结束时间") max_players = models.IntegerField("最大人数", default=12) current_count = models.IntegerField("当前报名人数", default=0) status = models.CharField("状态", max_length=20, choices=STATUS_CHOICES, default="recruiting") description = models.TextField("活动说明", blank=True) created_at = models.DateTimeField(auto_now_add=True)

这里有几个我一开始没注意、后来吃了亏的设计细节:

current_count这个字段是一个故意冗余的设计。按理说当前报名人数可以通过统计Enrollment表得到,但如果每次前端显示活动列表都要annotate一下报名人数,活动一多,查询就会明显变慢。用冗余字段存一个计数,报名时F()表达式原子加一,列表接口直接查这个字段,性能好很多。

status我用了字符串而不是外键,因为活动状态是固定的枚举值,用choices足够了。数据库里存"recruiting"这种可读字符串,调试的时候看数据一目了然,不用去翻关联表。

还有一个关键点是on_delete=models.CASCADE。当场地被删除时,关联的活动也删掉,避免因为外键约束导致程序报错。实际开发中如果平台已经有了大量真实活动,你可能希望改成PROTECT不允许删场地,但开发前期CASCADE省心。

2.2 Django ORM 的高频操作实战

这个项目的接口基本就是增删改查,但"查询"这个事值得多聊几句。羽毛球活动的筛选条件特别典型:用户要看"今天有哪些活动""离我近的场地""还没满员的活动"。我封装了一个查询函数:

from django.db.models import Q, F from django.utils import timezone def get_available_activities(date=None, venue_id=None): """获取可报名的活动,支持按日期和场地筛选""" queryset = Activity.objects.select_related("venue", "organizer").filter( status__in=["recruiting", "in_progress"], start_time__gt=timezone.now(), ) if date: queryset = queryset.filter(start_time__date=date) if venue_id: queryset = queryset.filter(venue_id=venue_id) return queryset.order_by("start_time")

重点看select_related。活动列表接口返回的数据里包含场地名称和组织者昵称,如果不加这个,Django ORM 会对每个活动额外发起一次场地查询和一次用户查询,一个 20 条数据的列表就是 41 条 SQL,N+1 查询是新手最容易踩的坑。加了select_related之后变回一条 JOIN 查询,性能差距在数据量小的时候看不出来,但页面响应速度的体感差异是实实在在的。

删除对象的操作也要小心。羽毛球活动结束后我写了定期清理脚本:

# 清理已结束超过7天的活动,连带删除报名记录 expired_activities = Activity.objects.filter( status="finished", end_time__lt=timezone.now() - timezone.timedelta(days=7) ) expired_count, _ = expired_activities.delete()

注意delete()方法是 QuerySet 级别的,Activity.objects().filter().delete()会把关联的外键也按on_delete规则处理。但如果你的Enrollment表数据量大,这个操作可能锁表影响线上业务,稳妥做法是分批删:

from itertools import islice ids = list(expired_activities.values_list("id", flat=True)) # 每次删 200 条,避免一次锁太多行 for batch_start in range(0, len(ids), 200): batch_ids = ids[batch_start:batch_start + 200] Activity.objects.filter(id__in=batch_ids).delete()

2.3 JWT 认证与权限控制

用户模块我直接用了 JWT。这里要说明一下,Django REST Framework 自带的SessionAuthentication其实也很好用,但前后端分离项目用 JWT 更贴合场景——前端把 token 存在 localStorage 或者内存里,每次请求带在Authorization头,后端无状态验证,不需要维护 session 记录。

安装djangorestframework-simplejwt之后,配置非常简单:

# settings.py REST_FRAMEWORK = { "DEFAULT_AUTHENTICATION_CLASSES": ( "rest_framework_simplejwt.authentication.JWTAuthentication", ), "DEFAULT_PERMISSION_CLASSES": ( "rest_framework.permissions.IsAuthenticated", ), } from datetime import timedelta SIMPLE_JWT = { "ACCESS_TOKEN_LIFETIME": timedelta(hours=2), "REFRESH_TOKEN_LIFETIME": timedelta(days=7), "AUTH_HEADER_TYPES": ("Bearer",), }

关于 token 有效期,我的建议是 access token 设短一点(1~2小时),refresh token 设长一点(7~14天)。每次前端发现 access token 过期,就拿 refresh token 去换新的,用户无感知。很多新手图省事把 access token 设成 7 天,一旦泄露等于永久通行证,这是安全大忌。

权限控制方面,Django 的 ModelViewSet 配合权限类是常规操作。发布活动必须登录,修改和删除活动只有组织者本人能做:

from rest_framework.permissions import BasePermission class IsOrganizerOrReadOnly(BasePermission): def has_object_permission(self, request, view, obj): if request.method in SAFE_METHODS: return True return obj.organizer == request.user

SAFE_METHODS是 GET/HEAD/OPTIONS 这些只读方法,任何登录用户都能访问;写操作就校验是不是组织者本人。这个权限类的逻辑很清爽,也符合真实业务:你可以看别人的活动,但不能改别人的活动。

2.4 Django Channels 实现 WebSocket 实时推送

活动中有人报名、有人取消、活动被取消了,这些事件都需要实时通知相关用户。轮询当然也能做,但体验很差——每 5 秒发一次请求,数据库压力大,信息延迟又高。WebSocket 是更合适的方案,长连接建立后,服务端可以主动往客户端推数据。

Django 里做 WebSocket 需要引入channels,它把 Django 从同步 HTTP 世界扩展到异步协议层。核心逻辑是写一个消费者:

# consumers.py import json from channels.generic.websocket import AsyncWebsocketConsumer class ActivityNotificationConsumer(AsyncWebsocketConsumer): async def connect(self): self.user = self.scope["user"] if self.user.is_authenticated: # 每个用户一个独立分组,只推给自己的群 self.group_name = f"user_{self.user.id}" await self.channel_layer.group_add(self.group_name, self.channel_name) await self.accept() else: await self.close() async def disconnect(self, close_code): if hasattr(self, "group_name"): await self.channel_layer.group_discard(self.group_name, self.channel_name) async def send_notification(self, event): await self.send(text_data=json.dumps({ "type": event["notify_type"], "message": event["message"], "activity_id": event["activity_id"], "created_at": event["created_at"], }))

业务后端在某个动作发生时,通过channel_layer.group_send往用户分组里发消息。这里最关键的配置是 Redis 作为 channel layer 的后端,因为多个 Django worker 进程之间需要共享连接信息:

# settings.py CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": {"hosts": [("127.0.0.1", 6379)]}, }, }

WebSocket 部署的时候还有个坑:你不能只用runserver,生产环境需要daphne或者uvicorn启动 ASGI 应用,配合 Redis 做 channel layer,否则多进程下消息会乱。

3. Vue 前端工程化:从环境搭建到业务落地

3.1 Vite 构建 Vue 3 项目

前端部分我用 Vue 3 + Vite,不用官方 Vue CLI。原因很简单:Vite 基于原生 ESM,开发服务器启动是秒级,热更新也快。Vue CLI 基于 Webpack,配置能力强大但冷启动确实慢,对于这个体量的项目个人认为 Vite 的效率收益更明显。

初始化项目:

npm create vue@latest

选好 TypeScript、Vue Router、状态管理(我用的 Pinia)之后,把依赖装上:

npm install npm install axios element-plus

这个脚手架会生成标准的src/views、src/components、src/router目录结构。我第一次做 Vue 项目时习惯把所有页面都堆在views里,组件库、业务组件、页面组件混在一起,后来前端文件多了(活动列表、活动详情、发布页、个人中心、留言板),光是找文件就能花半分钟。现在我的习惯是每个业务模块一个文件夹,src/views/activity/下面只放页面,通用组件丢src/components/,接口请求统一放src/api/。

3.2 路由设计与权限守卫

羽毛球交流平台的路由分两块:公开页面(活动列表、活动详情、留言板)和需要登录的页面(发布活动、个人中心)。Vue Router 的 4.x 版本写法:

// src/router/index.js import { createRouter, createWebHistory } from 'vue-router' const routes = [ { path: '/', name: 'home', component: () => import('@/views/activity/ActivityList.vue') }, { path: '/activity/:id', name: 'activity-detail', component: () => import('@/views/activity/ActivityDetail.vue'), props: true }, { path: '/publish', name: 'publish', component: () => import('@/views/activity/ActivityPublish.vue'), meta: { requiresAuth: true } }, { path: '/profile', name: 'profile', component: () => import('@/views/user/UserProfile.vue'), meta: { requiresAuth: true } }, { path: '/forum', name: 'forum', component: () => import('@/views/forum/ForumList.vue') }, ] const router = createRouter({ history: createWebHistory(), routes, }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('access_token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else { next() } })

这里有个易错的细节:localStorage.getItem('access_token')只能判断 token 是否存在,不能判断是否过期。严谨的做法是前端在请求 401 时自动跳登录页(配合 axios 拦截器),把"token 是否有效"的判断交给后端,前端只管"有没有 token"这个粗粒度判断。登录后跳回原始页面的逻辑也很关键,我用query: { redirect: to.fullPath }记录用户想去哪里,登录成功之后router.push(route.query.redirect)回跳,体验就顺滑了。

动态路由是另一个话题。如果平台要做会员等级(普通球友、球场管理员、平台运营),不同角色能访问的页面不同,那就需要从后端拉取权限配置,用router.addRoute()动态注册。我目前没有做复杂的角色体系,管理员直接走 Django admin,所以暂不需要,但路由设计的结构要为后面扩展留出余地。

3.3 axios 封装与 Vue 组件封装

请求封装是前后端联调的第一道关卡。我统一用了 axios 实例,把 baseURL、超时时间、请求拦截器、响应拦截器都集中管起来:

// src/api/request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000, }) // 请求拦截器:自动带上 token request.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) // 响应拦截器:统一处理错误 request.interceptors.response.use( response => response.data, error => { if (error.response) { if (error.response.status === 401) { localStorage.removeItem('access_token') localStorage.removeItem('refresh_token') router.push({ path: '/login', query: { redirect: router.currentRoute.value.fullPath } }) } else { ElMessage.error(error.response.data.detail || '请求失败,请稍后重试') } } else { ElMessage.error('网络异常,请检查网络连接') } return Promise.reject(error) } )

前端组件封装方面,我用 Element Plus 做基础 UI,但业务组件自己封装了一层。比如活动卡片ActivityCard.vue,接收一个 activity 对象,展示时间、场地、人数、状态,点击跳详情。这个组件在活动列表页和个人中心都会用到,封装一次后面都省事。羽毛球活动的报名状态用一个彩色 Tag 展示——招募中是绿色、已满员是橙色、已取消是灰色——这些视觉细节虽然不起眼,但用户浏览列表时的信息获取效率完全不同。

3.4 与后端 API 对接的细节

对接接口时,Vue 这边最需要注意的是数据结构的约定。DRF 默认返回的字段名是 Python 风格的snake_case,比如max_players、start_time,前端保持原样使用就行,不要多做一层转换。很多新手会写映射函数把字段名改成驼峰,这是多余的,反而是给自己埋坑。

上传图片(比如用户头像、活动宣传图)的时候,注意 Django 的MEDIA_ROOT配置和前端提交格式:

// 上传图片 const formData = new FormData() formData.append('avatar', file) request.post('/users/me/avatar/', formData, { headers: { 'Content-Type': 'multipart/form-data' } })

axios 在传 FormData 的时候千万不要手动设Content-Type: application/json,如果设错了后端解析不了文件,而且前端浏览器会一直报 400。正确的做法是让浏览器自动带上multipart/form-data; boundary=...,像上面代码里显式写headers其实也多余,axios 检测到 FormData 会自动处理,不写反而最稳。

4. 前后端联调与 WebSocket 对接实录

4.1 开发环境的跨域与代理配置

开发阶段最典型的问题就是跨域。我的代码架构是这样的:Vue 跑在 5173 端口,Django 跑在 8000 端口,浏览器访问 Vue 页面时,页面的请求地址是http://localhost:5173/api/activities,指向 Vue 服务器,根本没有 Django 的事。解决办法是 Vite 的 proxy 把/api开头的请求转发到http://localhost:8000:

// vite.config.js import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true, }, '/ws': { target: 'ws://localhost:8000', ws: true, }, }, }, })

changeOrigin: true的意思是修改请求头里的Host为 target 的地址,否则 Django 的ALLOWED_HOSTS校验会拒绝来自localhost:5173的请求。WebSocket 的代理单独配,ws: true是关键。

跨域的另一种做法是后端配django-cors-headers,但那只适用于生产环境前后端域名不同的情况。开发阶段用 Vite proxy 更清爽,不用管 CORS 预检请求,而且修改后端接口地址只需要改一处 proxy 配置,前端代码一行都不用动。

4.2 WebSocket 前端的连接管理

WebSocket 前端不能用 axios,得用浏览器原生 WebSocket API 或者封装好的库。我写了一个简单的连接管理模块:

// src/utils/websocket.js class WSManager { constructor() { this.socket = null this.reconnectAttempts = 0 this.heartbeatTimer = null } connect() { const token = localStorage.getItem('access_token') if (!token) return const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws' this.socket = new WebSocket(`${protocol}://${window.location.host}/ws/notifications/?token=${token}`) this.socket.onopen = () => { this.reconnectAttempts = 0 // 心跳保活:每 30 秒发一次 ping this.heartbeatTimer = setInterval(() => { if (this.socket.readyState === WebSocket.OPEN) { this.socket.send(JSON.stringify({ type: 'ping' })) } }, 30000) } this.socket.onclose = () => { clearInterval(this.heartbeatTimer) if (this.reconnectAttempts < 5) { setTimeout(() => this.connect(), 3000 * (this.reconnectAttempts + 1)) this.reconnectAttempts++ } } this.socket.onmessage = (event) => { const data = JSON.parse(event.data) // 这里触发一个全局事件,让组件响应 window.dispatchEvent(new CustomEvent('notification', { detail: data })) } } } export const wsManager = new WSManager()

WebSocket 的 token 怎么传?我用了 URL query 参数,因为浏览器的 WebSocket API 不支持自定义 Header。但这里有个安全细节:token 会出现在服务端访问日志里,生产环境建议用短期有效的专用 token,或者建立连接后第一条消息里鉴权,连接先不算完成。我这个项目规模不大,query 传参配合 wss 加密传输可以接受,但如果敏感程度高的项目,建议第一条消息鉴权的方案。

前端的重连逻辑必须做。羽毛球活动场景里,用户可能在地铁上信号中断、手机锁屏后网络切换,WebSocket 连接说断就断。我做的是指数退避重连,第一次重连等 3 秒,第二次等 6 秒,最多 5 次,避免高频重连把服务端压垮。

4.3 联调中的接口规范与调试技巧

前后端联调最容易出的问题就是"数据格式对不上"。我的做法很简单,后端写接口之前先定义一份接口文档(不需要什么 Swagger,先就关键字段对齐):活动列表接口返回{ id, title, venue_name, start_time, end_time, max_players, current_count, status },前端照着这个结构写界面和类型定义。DRF 的序列化器天然负责这个输出格式,但字段命名一定要在后端开发时就和前端确认好,不要"先返回再说"。

调试接口我用的是浏览器 DevTools 的 Network 面板加 vue-devtools。Network 面板能直观看到每个请求的 URL、请求头、响应体,接口报错了先看响应状态码:400 是参数校验失败、401 是没带 token 或 token 过期、403 是权限不足、404 是路径写错了。分清这四种状态码,排查速度快一倍。

Django 后端这边,我强烈建议配django-debug-toolbar,它能列出每个请求执行的 SQL 语句数量和时间。我看一个列表接口慢,打开 debug toolbar 一看,发现 N+1 查询没处理干净,select_related没加对,SQL 执行了 37 次。修完再看,变成一条 SQL,响应时间从 400ms 降到 60ms。这个工具对排查性能问题是神器级的存在。

5. 部署上线与常见问题速查

5.1 生产环境部署方案

开发完以后要上线,我用的是 Nginx + Gunicorn + Daphne + MySQL + Redis 这套组合。Nginx 负责静态文件、反向代理和 SSL 终止;Gunicorn 跑 Django 的 WSGI 接口;Daphne 跑 Channels 的 ASGI 接口;Redis 既做缓存又做 WebSocket 的 channel layer。

一个容易踩的坑是:HTTP 和 WebSocket 要分两套入口。Nginx 配置大致是:

server { listen 80; server_name your-domain.com; location /static/ { alias /var/www/badminton/static/; } location /media/ { alias /var/www/badminton/media/; } location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /ws/ { proxy_pass http://127.0.0.1:8001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; } }

注意/ws/这段配置里Connection: upgrade必不可少,没有这个头,WebSocket 握手会直接失败。另一个注意点是 Django 的ALLOWED_HOSTS一定要配置成你的域名,否则生产环境访问会报DisallowedHost。

前端部署更简单:npm run build生成dist/目录,Nginx 把它指向一个 location 作为静态站点。需要注意 Vue Router 的history模式,刷新页面时 Nginx 会去服务器找不存在的路径,需要在 Nginx 里加try_files $uri $uri/ /index.html,否则用户点进活动详情页后按 F5 就会收到 404。

5.2 实战中遇到的高频问题汇总

前前后后开发加调试,我攒了一份问题和解决方法的清单,这里挑几个最有代表性的:

问题现象根本原因解决办法
前端请求接口报 403 CSRF 错误DRF 的 SessionAuthentication 会校验 CSRF,前后端分离下容易误触发使用 JWT 认证替代 SessionAuthentication,或者在 ViewSet 上标记csrf_exempt不需要的视图
活动列表加载慢忘记select_related,每个活动的场地和用户额外查询ORM 链式调用里加select_related("venue", "organizer")
报名人数不准多个用户并发报名时读旧值再 +1用F("current_count") + 1原子更新,别先查再更新
WebSocket 连接秒断Nginx 没有配置 Upgrade 头,或者连接 60 秒无消息被网关断开配置Connection: upgrade+ 前端 30 秒心跳
刷新页面 404Vue Router history 模式没有 fallbackNginxtry_files $uri $uri/ /index.html
图片上传 400axios 手动设置了Content-Type: application/json去掉 headers 配置,让浏览器自动处理 FormData
活动发不了,偶发报错表单里时间格式不对,Django 解析不了前端用dayjs格式化时间,统一传YYYY-MM-DDTHH:mm格式

关于并发报名,这里再展开一下。两个用户同时看到活动还剩最后一个名额,同时点了报名。如果后端代码是"查一下当前人数,小于最大人数就插入报名记录再 +1",那么两个请求都能通过校验,最后人数超了。正确做法是把判断和更新放在一个原子操作里:

from django.db import transaction @transaction.atomic def enroll_activity(user_id, activity_id): activity = Activity.objects.select_for_update().get(id=activity_id) if activity.current_count >= activity.max_players: raise ValueError("活动已满员") Enrollment.objects.create(user_id=user_id, activity_id=activity_id) Activity.objects.filter(id=activity_id).update(current_count=F("current_count") + 1)

select_for_update在事务里给活动行加锁,两个并发请求会排队执行,第二个请求进来时发现人数已满,直接拒绝。这是数据库层面解决并发问题的标准姿势。业务量小的项目这招够用了,如果量大再做 Redis 分布式锁。

还有个容易被忽略的点:清理活动状态。活动开始时间到了,状态应该从"招募中"变成"进行中";活动结束,变成"已结束"。我写了一个 Django management command,用 Celery 定时任务每小时跑一次,用 Django ORM 批量更新状态,不用人工盯。

5.3 优化空间与后续演进方向

这个羽毛球交流平台做到现在,核心流程已经完整跑通了。如果继续往下做,我有几个明确的演进方向:

第一,接入地图选场。羽毛球爱好者最关心的其实是"离我多远"。目前场地只有文本地址,后续可以接地图组件,用坐标做距离排序,让用户按地理位置找场地。

第二,WebSocket 的扩展。现在只做了"有人报名/取消"的通知推送。真正打起来可以做到:活动开始前 30 分钟自动给所有人推送提醒;用户关注某个场地后,新活动发布立刻推送。这些都需要调度任务配合 WebSocket。

第三,数据可视化。后台管理如果不想用 Django admin 的原始界面,可以做一个统计面板,展示活跃用户数、最热门场地、每周活动场次数。这些数据端的支撑可以先通过增加几个聚合接口来提供。

第四,移动端 H5 适配。羽毛球用户很多在球场临时约人,手机是主要终端。当前 Vue 3 的项目可以加上移动端响应式适配,或者单独做一个 H5 版本。

不是所有功能都要在第一个版本做出来。我的原则是先把核心链路(发布活动 → 用户报名 → 到场打球 → 消息通知)做得足够顺滑,再考虑增强功能。很多个人项目死在"想做的太多、做出来的太少",先砍需求、保住核心闭环,比一步到位更现实。

最后说句题外话:我在开发过程中感受最深的是,前后端分离项目真正考验人的不是某个具体技术,而是"接口契约"的把握。前端和后端是两个人(哪怕时间上可能是一个人),只要接口字段、状态码、错误处理这些约定做得足够细致,联调阶段就不会互相等来等去。我吃过的亏,都是"我以为后端会返回这个字段"和"我以为前端会传那个参数"这种沟通断档造成的。所以我的建议是,动手写代码之前,先花半小时把接口文档的字段从头到尾对齐一遍,把异常情况说清楚。这半小时的投入,会在后面省出几天的联调时间。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 3:22:18

自定义Windows右键菜单:高频工具一键直达

大家有没有这种感觉&#xff1a;刚装完的 Windows 跑得飞快&#xff0c;但用一段时间后&#xff0c;开机慢、启动软件也慢。以前我第一反应是清启动项、换固态、关服务&#xff0c;后来发现真正拖累工作效率的不是开机那几秒&#xff0c;而是每天反复“找图标、双击、等加载”的…

作者头像 李华
网站建设 2026/9/30 3:22:18

Helm部署Bitnami PostgreSQL与Pgpool实现主从读写分离

上个月帮一个内部项目搭数据库层&#xff0c;需求很明确&#xff1a;业务量不大&#xff0c;但不想把数据库放在单点上&#xff0c;要求先有一套 PostgreSQL 主从集群&#xff0c;同时把连接池和读写入口的问题一起解决。折腾一圈之后&#xff0c;最后落地的方案就是标题里这套…

作者头像 李华
网站建设 2026/9/30 3:22:16

同目录双仓库:前端项目同时管理SVN与Gitee的完整实践

最近在公司里折腾了一个有点意思的事情&#xff1a;让同一个前端项目同时被SVN和Gitee管着。听起来像两个版本控制系统的“左右互搏”&#xff0c;但实际用起来&#xff0c;这是一套很多团队都会碰到的组合——公司内部的代码规范、历史包袱和审批流程还牢牢绑在SVN上&#xff…

作者头像 李华
网站建设 2026/9/30 3:22:16

Linux服务器故障排查实战:从fstab到GRUB的完整排障笔记

简介&#xff1a;面向Linux/Unix系统运维人员&#xff0c;这份PDF资源聚焦服务器日常运行中高频出现的故障场景&#xff0c;以故障现象、排查过程、解决命令为主线&#xff0c;帮助读者建立从定位问题到恢复服务的完整思路。内容包含RAID1数据分区挂载异常、依赖库缺失导致root…

作者头像 李华
网站建设 2026/9/30 3:21:56

Windows蓝屏代码查询与排查实战:从STOP 0x0000001E到转储分析

简介&#xff1a;这份资料面向经常遭遇Windows蓝屏的普通用户与初级运维人员&#xff0c;系统整理了蓝屏代码的查询与解读方法。内容围绕蓝屏时显示的关键信息展开&#xff0c;包括以STOP开头的停机码&#xff08;如0x0000001E&#xff09;、括号内四个开发者参数、错误名&…

作者头像 李华
网站建设 2026/9/30 3:21:01

EMC DS300B 光纤交换机维护实战:从登录到固件升级的完整指南

简介&#xff1a;这份EMC DS300B光纤交换机维护手册面向数据中心存储运维人员与系统集成工程师&#xff0c;针对光纤交换机日常维护与故障排查场景&#xff0c;提供一套可落地的操作参考。资源包共1个doc文档&#xff0c;约361KB&#xff0c;内容以设备概况、开关机流程、状态检…

作者头像 李华