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_length100) address models.CharField(详细地址, max_length255) court_count models.IntegerField(场地数量, default1) price_per_hour models.DecimalField(每小时价格, max_digits6, decimal_places2) class Activity(models.Model): STATUS_CHOICES [ (recruiting, 招募中), (full, 已满员), (in_progress, 进行中), (finished, 已结束), (cancelled, 已取消), ] title models.CharField(活动标题, max_length100) venue models.ForeignKey(Venue, on_deletemodels.CASCADE, related_nameactivities) organizer models.ForeignKey(auth.User, on_deletemodels.CASCADE, related_nameorganized_activities) start_time models.DateTimeField(开始时间) end_time models.DateTimeField(结束时间) max_players models.IntegerField(最大人数, default12) current_count models.IntegerField(当前报名人数, default0) status models.CharField(状态, max_length20, choicesSTATUS_CHOICES, defaultrecruiting) description models.TextField(活动说明, blankTrue) created_at models.DateTimeField(auto_now_addTrue)这里有几个我一开始没注意、后来吃了亏的设计细节current_count这个字段是一个故意冗余的设计。按理说当前报名人数可以通过统计Enrollment表得到但如果每次前端显示活动列表都要annotate一下报名人数活动一多查询就会明显变慢。用冗余字段存一个计数报名时F()表达式原子加一列表接口直接查这个字段性能好很多。status我用了字符串而不是外键因为活动状态是固定的枚举值用choices足够了。数据库里存recruiting这种可读字符串调试的时候看数据一目了然不用去翻关联表。还有一个关键点是on_deletemodels.CASCADE。当场地被删除时关联的活动也删掉避免因为外键约束导致程序报错。实际开发中如果平台已经有了大量真实活动你可能希望改成PROTECT不允许删场地但开发前期CASCADE省心。2.2 Django ORM 的高频操作实战这个项目的接口基本就是增删改查但查询这个事值得多聊几句。羽毛球活动的筛选条件特别典型用户要看今天有哪些活动离我近的场地还没满员的活动。我封装了一个查询函数from django.db.models import Q, F from django.utils import timezone def get_available_activities(dateNone, venue_idNone): 获取可报名的活动支持按日期和场地筛选 queryset Activity.objects.select_related(venue, organizer).filter( status__in[recruiting, in_progress], start_time__gttimezone.now(), ) if date: queryset queryset.filter(start_time__datedate) if venue_id: queryset queryset.filter(venue_idvenue_id) return queryset.order_by(start_time)重点看select_related。活动列表接口返回的数据里包含场地名称和组织者昵称如果不加这个Django ORM 会对每个活动额外发起一次场地查询和一次用户查询一个 20 条数据的列表就是 41 条 SQLN1 查询是新手最容易踩的坑。加了select_related之后变回一条 JOIN 查询性能差距在数据量小的时候看不出来但页面响应速度的体感差异是实实在在的。删除对象的操作也要小心。羽毛球活动结束后我写了定期清理脚本# 清理已结束超过7天的活动连带删除报名记录 expired_activities Activity.objects.filter( statusfinished, end_time__lttimezone.now() - timezone.timedelta(days7) ) 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, flatTrue)) # 每次删 200 条避免一次锁太多行 for batch_start in range(0, len(ids), 200): batch_ids ids[batch_start:batch_start 200] Activity.objects.filter(id__inbatch_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(hours2), REFRESH_TOKEN_LIFETIME: timedelta(days7), 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.userSAFE_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 fuser_{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_datajson.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 vuelatest选好 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 一看发现 N1 查询没处理干净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(idactivity_id) if activity.current_count activity.max_players: raise ValueError(活动已满员) Enrollment.objects.create(user_iduser_id, activity_idactivity_id) Activity.objects.filter(idactivity_id).update(current_countF(current_count) 1)select_for_update在事务里给活动行加锁两个并发请求会排队执行第二个请求进来时发现人数已满直接拒绝。这是数据库层面解决并发问题的标准姿势。业务量小的项目这招够用了如果量大再做 Redis 分布式锁。还有个容易被忽略的点清理活动状态。活动开始时间到了状态应该从招募中变成进行中活动结束变成已结束。我写了一个 Django management command用 Celery 定时任务每小时跑一次用 Django ORM 批量更新状态不用人工盯。5.3 优化空间与后续演进方向这个羽毛球交流平台做到现在核心流程已经完整跑通了。如果继续往下做我有几个明确的演进方向第一接入地图选场。羽毛球爱好者最关心的其实是离我多远。目前场地只有文本地址后续可以接地图组件用坐标做距离排序让用户按地理位置找场地。第二WebSocket 的扩展。现在只做了有人报名/取消的通知推送。真正打起来可以做到活动开始前 30 分钟自动给所有人推送提醒用户关注某个场地后新活动发布立刻推送。这些都需要调度任务配合 WebSocket。第三数据可视化。后台管理如果不想用 Django admin 的原始界面可以做一个统计面板展示活跃用户数、最热门场地、每周活动场次数。这些数据端的支撑可以先通过增加几个聚合接口来提供。第四移动端 H5 适配。羽毛球用户很多在球场临时约人手机是主要终端。当前 Vue 3 的项目可以加上移动端响应式适配或者单独做一个 H5 版本。不是所有功能都要在第一个版本做出来。我的原则是先把核心链路发布活动 → 用户报名 → 到场打球 → 消息通知做得足够顺滑再考虑增强功能。很多个人项目死在想做的太多、做出来的太少先砍需求、保住核心闭环比一步到位更现实。最后说句题外话我在开发过程中感受最深的是前后端分离项目真正考验人的不是某个具体技术而是接口契约的把握。前端和后端是两个人哪怕时间上可能是一个人只要接口字段、状态码、错误处理这些约定做得足够细致联调阶段就不会互相等来等去。我吃过的亏都是我以为后端会返回这个字段和我以为前端会传那个参数这种沟通断档造成的。所以我的建议是动手写代码之前先花半小时把接口文档的字段从头到尾对齐一遍把异常情况说清楚。这半小时的投入会在后面省出几天的联调时间。

相关新闻

VMware安装Ubuntu深度指南:从UEFI配置到VMware Tools实战

VMware安装Ubuntu深度指南:从UEFI配置到VMware Tools实战

1. 这不是“装个系统”那么简单:为什么你反复重装Ubuntu却总卡在第三步?我带过二十多个刚接触Linux的新人,几乎所有人第一次在VMware上装Ubuntu时,都以为只是点几下“下一步”——结果卡在“黑屏不动”“鼠标失灵”“网络打不开”…

2026/9/30 3:22:22 阅读更多 →
基于距离-时间像的雷达人体动作识别:从HRRP到深度学习分类

基于距离-时间像的雷达人体动作识别:从HRRP到深度学习分类

简介:一份发表于《桂林理工大学学报》的学术论文PDF,主题是基于时间距离像与深度卷积神经网络(DCNN)的人体动作分类研究,面向雷达目标识别、深度学习应用与信号处理等方向的研究人员、研究生。论文针对人体微多普勒特征…

2026/9/30 3:21:21 阅读更多 →
Java volatile原理与实战:可见性、有序性及典型应用

Java volatile原理与实战:可见性、有序性及典型应用

1. volatile 是什么?它真能“让变量变可见”吗?刚接触 Java 并发时,很多人会把volatile理解成“轻量级 synchronized”,或者干脆记成“让变量线程可见”。这说法不算错,但太模糊——就像说“螺丝刀是用来拧东西的”&am…

2026/9/30 3:21:21 阅读更多 →

最新新闻

BLE设备地址全解析:从蓝牙MAC到可解析私有地址的坑与对策

BLE设备地址全解析:从蓝牙MAC到可解析私有地址的坑与对策

1. 从“一串MAC”说起:BLE设备地址的真面目做BLE开发这些年,被问得最多的问题除了“为什么连不上”,就是“为什么同一个设备,每次扫描到的蓝牙设备地址都不一样”。很多人想当然地把蓝牙设备地址当成网卡MAC地址来管理&#xff0c…

2026/9/30 5:50:37 阅读更多 →
Python+WebSocket实现网页远程桌面:核心原理与代码实践

Python+WebSocket实现网页远程桌面:核心原理与代码实践

一直觉得“网页版的远程桌面”是个听起来很高级、实际做起来却很亲民的东西。我最近用 Python 浏览器原生能力从零写了一个最小可用的版本:被控端抓屏、编码成 JPEG、通过 WebSocket 推给前端页面,浏览器再把鼠标键盘事件回传。整套代码不臃肿&#xff…

2026/9/30 5:50:37 阅读更多 →
RAG中数据清洗与预处理阶段具体如何做?

RAG中数据清洗与预处理阶段具体如何做?

1.基本思路2.数据预处理全流程拆解原始数据(多格式) │ ├─ 格式转换(提取纯文本) │ ├─ PDF → PyPDF2提取文本 │ ├─ HTML → BeautifulSoup解析正文 │ └─ Excel → Pandas读取表格内容 │ ├─ 清洗降…

2026/9/30 5:50:37 阅读更多 →
OPNET实验手册:从网络仿真到性能分析的关键配置与排障指南

OPNET实验手册:从网络仿真到性能分析的关键配置与排障指南

简介:OPNET实验手册.doc 是一份面向理工科计算机与信息工程系学生的网络仿真实验指导,系统覆盖OPNET Modeler从基础到进阶的八个实验:从快速建立星型网络、理解工程/节点/进程编辑器,到基本进程模拟、SCE数据导入、Windows Perfmo…

2026/9/30 5:50:37 阅读更多 →
Linux kill命令详解:从信号机制到-9与-15的实战区别

Linux kill命令详解:从信号机制到-9与-15的实战区别

1. 先搞明白 kill 到底在干什么很多朋友刚开始接触 Linux,第一次敲kill -9的时候,理解就是“我把这个进程弄死”。这个理解不能说错,但它掩盖了 kill 真正的行为逻辑。如果你只停留在“kill 就是把进程干掉”这个层面,那遇到进程杀…

2026/9/30 5:50:37 阅读更多 →
不排序不打分的大语言模型讨论辅助系统设计与实践

不排序不打分的大语言模型讨论辅助系统设计与实践

1. 先讲清楚:这个系统到底要解决什么1.1 项目背景与定位先说结论:这是一个基于大语言模型的多人讨论辅助系统,但它的核心约束很简单——AI 不排序、不打分、不判谁赢。它只做三件事:把参与者的观点拆开、重新表述、再拉回讨论现场…

2026/9/30 5:49:37 阅读更多 →

日新闻

Base64 图片头部特征识别:从文件头到格式判断的完整指南

Base64 图片头部特征识别:从文件头到格式判断的完整指南

1. 项目概述:为什么说看懂 base64 图片头部是基本功这几年跟 base64 打交道的机会越来越多,后端接口返回图片、前端渲染验证码、小程序里存小图、还有一些老系统导出报表,动不动就给你一段长到怀疑人生的 base64 字符串。很多人拿到字符串就直…

2026/9/30 0:00:35 阅读更多 →
Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

Java公交站牌广告管理系统:JSP+Servlet+MySQL实战落地指南

简介:本资源是一份面向Java初学者与课程设计学生的公交站牌广告灯箱管理系统毕业设计文档,聚焦城市公共广告资源信息化管理痛点,提供从需求分析到技术实现的完整方案。文档采用标准学术论文结构,含摘要、英文摘要、目录及五章正文…

2026/9/30 0:00:35 阅读更多 →
用 Redis Lua 构建大模型 API 多租户原子配额治理体系

用 Redis Lua 构建大模型 API 多租户原子配额治理体系

我去年年底接了一个内部 AI 平台的治理需求,背景很直接:公司把 DeepSeek、MiniMax 这类大模型 API 统一封装成内部网关,开放给几个业务团队用。结果第一个月账单出来,额度直接超了 4 倍。仔细查日志,发现原因并不复杂—…

2026/9/30 0:00:35 阅读更多 →

周新闻

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解

如何划分训练/验证集:Spirula Studio五种eval_mode策略详解 【免费下载链接】spirula-studio Cross-vendor 3D Gaussian Splatting trainer - video to splat to mesh, Vulkan or CUDA. 项目地址: https://gitcode.com/GitHub_Trending/sp/spirula-studio Sp…

2026/9/29 8:16:59 阅读更多 →
SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南

SEO怎么推广速查手册新手避坑实战指南 模板网站太丑不够用?别急着加滤镜,那是治标不治本。很多老板盯着后台流量掉得眼红,却还在纠结首页Banner的圆角是不是3像素。这就像穿着西装去挖土,姿势不对,努力白费。我整理这份 速查手册…

2026/9/29 16:41:41 阅读更多 →
FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏

FireRed-OpenStoryline少样本仿写深度解析:AI Agent如何复刻你的独特文案风格与节奏 【免费下载链接】FireRed-OpenStoryline FireRed-OpenStoryline is an AI video editing agent that transforms manual editing into intention-driven directing through natural language …

2026/9/29 8:24:48 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/29 19:29:29 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/29 5:58:00 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/29 3:55:56 阅读更多 →