体育馆场地预约系统开发实战:微信小程序+Django+Flask架构解析

发布时间:2026/9/24 22:02:09
体育馆场地预约系统开发实战:微信小程序+Django+Flask架构解析 体育馆场地预约平台开发手记从电话排队到小程序一键订场做体育馆场地预约系统最早是因为一个朋友在高校体育部上班天天被电话轰炸羽毛球场地有没有今晚七点的场子被人占了能不能调隔壁单位想包场怎么收费这些问题的重复度极高但纯靠人工记录信息永远对不齐经常出现两头确认、到场才发现场地撞车的情况。后来我就用微信小程序 uni-app 做了前端后端用 Python 的 Django 和 Flask 各司其职搭出了整套体育馆场地预约综合管理平台。用户端能看到每个场地的实时占用情况按日期和时段自助下单管理员在后台管理场地、审核订单、看使用率报表。整个流程跑起来后体育部的老师终于不用一下午接二十个电话了。这篇文章我会把项目的完整思路、数据库设计、核心接口、小程序页面实现、并发控制、部署上线这些环节都写清楚同时分享实际开发中踩过的坑和排查过程。如果你也在做类似的预约类系统不管面向的是体育馆、自习室还是会议室这套方案基本可以直接套用。1. 项目整体设计与定位1.1 线下预约的痛点到底在哪体育馆的场地预约场景核心矛盾就三个信息不透明、时段易冲突、统计全靠人。信息不透明说的是用户不知道当前哪些场地空着只能打电话问或者到现场看时段易冲突是因为口头预约没有锁定期限同一个场地下午三点被打电话订了晚上又被微信临时约了最后谁先到谁用毫无秩序统计全靠人则是月底想做场馆利用率报表时只能翻纸质登记本张数、时段、收入根本对不上。这个项目要做的就是把“场地-日期-时段”变成一个可查询、可锁定、可交易的三元组。用户打开小程序看到某块羽毛球场地本周六上午十点到十一点是否可约点一下下单支付完成后这个时段就被锁定别人再打开就是灰色不可选。所有操作留痕每笔订单可追溯管理后台能按天、按周、按月导出报表。系统本质上解决的是资源分配和信任记账的问题技术难度不算高但业务逻辑必须严谨。1.2 系统角色与核心业务流程整个平台分两类用户C端用户和管理员业务都围绕场地预约展开。C端用户在小程序里的操作路径很直接登录用微信授权换取用户身份浏览场地列表按羽毛球、篮球、网球等分类筛选进入场地详情选择日期查看当天每个时段的占用状态点选可约时段创建订单并支付在“我的订单”里查看已约场地、取消预约管理员的职责则包括维护场地信息、设置开放时段与价格、处理退款取消、查看订单记录、统计场地利用率。业务流程看起来简单但有一个地方极容易做错订单状态和时段状态必须双写一致。订单创建成功后时段立刻变为“占用”订单取消后时段必须恢复“可约”。如果这两个状态出现分歧系统就等于回到了人工登记时代。2. 技术选型与方案对比2.1 前端为什么选 uni-app 而不是原生小程序原生微信小程序开发上手快但对这个项目来说有个现实问题体育馆的管理者很可能将来还要出 App 版或者校内 H5 版用原生小程序开发就意味着要三套代码维护成本翻倍。uni-app 的好处是一套 Vue 语法的代码可以同时编译到微信小程序、支付宝小程序、H5 和 App。我用 Vue 写页面组件用 uni.request 发请求用 uni.login 拿微信授权码编译到哪端就由框架层处理适配开发时完全不用关心平台差异。实际开发中uni-app 的 HBuilderX 编辑器内置了微信小程序编译、预览、上传一体化流程比原生开发还顺。唯一需要注意的是某些微信小程序特有的 API比如字体图标、转发分享、支付参数等得留意 uni-app 的兼容封装和条件编译的使用。2.2 Django 和 Flask 在这个项目里怎么分工标题里同时出现了 Django 和 Flask这不是笔误我的做法是让它们各管一摊。Django 作为主后端框架承担用户、场地、时段、订单的核心 CRUD 和业务逻辑。选择 Django 的核心原因是它的全家桶属性自带 ORM、模型管理后台Admin、用户认证体系、迁移工具。对于场地预约这种典型的管理信息系统场景Django 的 Admin 站点基本上不用额外开发就能把场馆、场地、订单管理界面做出来大大缩短开发周期。Flask 则用来承载一个独立的小服务——场地热度推荐和拥挤度预测。这个服务要从订单表里统计近两个月的时段预约热度给用户在前端做一个“本周六下午羽毛球热门场地Top3”的推荐模块。业务逻辑轻、不涉及复杂业务表用 Flask 原生路由很容易讲清楚且可以独立部署、独立升级不需要牵动 Django 主服务。在 Python 技术栈里这属于很常规的混搭做法核心业务用全家桶保证规范和效率边缘轻服务用微型框架保证轻量和灵活。2.3 前后端通信与整体部署结构前后端通信统一走 HTTP JSON。小程序端不直接访问数据库所有数据操作都封装成接口由 Django 提供 RESTful API。这里我没有用 DRF而是直接基于 Django 的 JsonResponse 手写了一套轻量 API 封装因为预约系统的接口数量不算多手写反而更直接可控后面我会展示具体写法。Flask 推荐服务单独监听一个端口供 Django 通过 HTTP 调用获取推荐结果。这么做的好处是系统拆分清楚哪个服务挂了不影响主流程。后端部署用 Nginx uWSGI 托管 DjangoFlask 服务用 gunicorn 托管两个服务都跑在同一台 Linux 服务器上数据库用 MySQL。小程序端正式发布要求请求域名必须为 HTTPS且域名需要在小程序后台配置为服务器域名本地开发阶段我通过微信开发者工具的“不校验合法域名”开关绕过限制。3. 数据库设计与业务模型拆解3.1 核心数据表用户、场地、时段、订单数据库设计是这类预约系统的地基。我按业务对象拆成了四张核心表用户表auth_user 扩展、场地表、时段表、订单表。用户表在 Django 内置 User 模型基础上扩展了微信 openid、手机号、余额等字段。# users/models.py from django.contrib.auth.models import AbstractUser from django.db import models class User(AbstractUser): openid models.CharField(max_length64, uniqueTrue, nullTrue, blankTrue) phone models.CharField(max_length11, blankTrue) balance models.DecimalField(max_digits8, decimal_places2, default0) avatar models.URLField(blankTrue)场地表包含场馆名称、类型、位置、封面图、每小时价格、是否开放预约。这里没有把价格放在订单里而是放在场地表里订单创建时读取当前场地价格写入订单快照这样即使后来调价历史订单仍然能追溯到当时的成交价。时段表是预约系统的关键。我用“日期 起止时间”组合成一条可用时段记录。比如某个场地一天开放十个小时每小时拆一个时段那当天就有十条时段记录。每条记录有一个 is_booked 布尔字段标识是否已被预约外键关联场地。这样的设计最大化简化了预约逻辑——查场地就是查时段锁场就是锁时段。时段表设计如下# venues/models.py from django.db import models class Venue(models.Model): CATEGORY_CHOICES ( (basketball, 篮球), (badminton, 羽毛球), (tennis, 网球), (table_tennis, 乒乓球), ) name models.CharField(max_length50) category models.CharField(max_len20, choicesCATEGORY_CHOICES) location models.CharField(max_length100) description models.TextField(blankTrue) cover models.URLField(blankTrue) price_per_hour models.DecimalField(max_digits6, decimal_places2) is_active models.BooleanField(defaultTrue) class TimeSlot(models.Model): venue models.ForeignKey(Venue, on_deletemodels.CASCADE, related_nameslots) date models.DateField() start_time models.TimeField() end_time models.TimeField() is_booked models.BooleanField(defaultFalse) order models.ForeignKey(orders.Order, nullTrue, blankTrue, on_deletemodels.SET_NULL)订单表则记录了从创建到完成的全生命周期状态。# orders/models.py from django.db import models from django.conf import settings class Order(models.Model): STATUS_CHOICES ( (pending, 待支付), (paid, 已支付), (cancelled, 已取消), (completed, 已完成), (expired, 已过期), ) user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE, related_nameorders) slot models.OneToOneField(venues.TimeSlot, on_deletemodels.PROTECT) amount models.DecimalField(max_digits8, decimal_places2) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(auto_now_addTrue) paid_at models.DateTimeField(nullTrue, blankTrue)3.2 状态联动订单和时段必须步调一致预约系统的业务闭环本质是订单状态机和时段状态机的联动。创建订单时时段从 false 变为 true订单初始状态为 pending用户支付成功后订单状态变为 paid如果用户取消或支付超时订单变为 cancelled 或 expired同时时段状态必须恢复为 false。这里最关键的一步是创建订单和锁定时段必须在同一个数据库事务里执行否则可能出现订单创建成功但时段没锁住或者时段锁住了但订单没创建成功的中间状态。我在后续接口实现部分会给出具体代码。订单过期我采用的是定时任务扫描方案每隔五分钟跑一个 cron把创建时间超过十五分钟仍未支付的订单自动置为 expired同时释放对应的时段。Django 项目里我用 django-crontab 实现了这个定时任务。3.3 并发防重不是加个 if 检查就完事了场地预约系统最容易出问题的场景是并发两个用户同时看到某个时段可约同时提交订单如果代码只是先查 is_booked 再更新那在高并发下必然出现“超卖”。解决并发问题需要两把锁一个是数据库行锁一个是唯一约束。行锁通过 Django ORM 的 select_for_update 实现代码在事务内先对对应时段记录加锁再检查状态、更新状态。另一个用户的事务必须等待前一个事务提交后才能继续执行检查从而保证不会双写成功。TimeSlot 表上 pairdate, venue, start_time加 unique_together 约束也很有必要这是最后的兜底防线即使业务代码里出了漏洞数据库层面也会拒绝重复创建。我在开发时字段顺序是 (venue, date, start_time)因为一个场地在同一个日期同一个开始时间只可能有一条时段记录。4. 后端接口实现从微信登录到订单支付4.1 接口层统一封装我没有用 Django REST Framework而是手写了一个简单的 JSON 响应封装。所有接口返回统一格式code 为 0 表示成功非 0 为业务错误data 为具体数据msg 为错误提示。# common/response.py from django.http import JsonResponse def ok(dataNone, msgsuccess): return JsonResponse({code: 0, msg: msg, data: data}) def fail(msgerror, code1): return JsonResponse({code: code, msg: msg, data: None})这样的好处是前端封装请求时判断逻辑统一而且排错思路一致。接口路由全部写在 Django 的 urls.py 里每个业务模块建一个 urls.py 文件保持结构清晰。4.2 微信登录与用户身份绑定用户首次打开小程序前端调用 uni.login 拿到微信临时授权 code然后传给后端。Django 后端拿到 code 后调用微信官方的 jscode2session 接口换取 openid 和 session_key。# users/views.py import requests from django.conf import settings from .models import User from common.response import ok, fail WX_APPID settings.WX_APPID WX_SECRET settings.WX_SECRET def wx_login(request): if request.method ! POST: return fail(请求方式错误) import json data json.loads(request.body) code data.get(code) resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: WX_APPID, secret: WX_SECRET, js_code: code, grant_type: authorization_code }, timeout5 ) res resp.json() openid res.get(openid) if not openid: return fail(登录失败) user, created User.objects.get_or_create(openidopenid) if created: user.username fwx_{openid[-8:]} user.save() token create_token(user) return ok({token: token, user: {id: user.id, nickname: user.username}})token 生成我用的 Python 标准库 secrets 生成随机串存到 Redis过期时间七天。小程序端每次请求在 header 里带上 token后端通过 Django 中间件解析出当前用户。这个方案简单有效不需要引入 JWT 等额外依赖。4.3 场地列表与可预约时段查询场地列表接口需要支持分类筛选并按分类分类返回场地信息。返回的数据包含场地的基础字段和封面图小程序端直接渲染即可。可预约时段查询是这个系统最核心的只读接口前端传场地 id 和日期后端返回该场地当天的全部时段并带有 is_booked 和 price 字段。# venues/views.py import json from datetime import datetime from django.views.decorators.http import require_http_methods from .models import Venue, TimeSlot from common.response import ok, fail require_http_methods([GET]) def venue_detail(request, venue_id): try: venue Venue.objects.get(idvenue_id, is_activeTrue) except Venue.DoesNotExist: return fail(场地不存在) date_str request.GET.get(date) try: query_date datetime.strptime(date_str, %Y-%m-%d).date() except ValueError: return fail(日期格式错误) slots TimeSlot.objects.filter(venuevenue, datequery_date).order_by(start_time) slot_list [{ id: s.id, start_time: s.start_time.strftime(%H:%M), end_time: s.end_time.strftime(%H:%M), is_booked: s.is_booked, price: str(venue.price_per_hour) } for s in slots] return ok({venue: { id: venue.id, name: venue.name, category: venue.category, location: venue.location, cover: venue.cover, description: venue.description, price_per_hour: str(venue.price_per_hour) }, slots: slot_list})设计上特意把场地信息和时段信息放在一个接口里返回前端一次请求就能完成详情页渲染减少网络往返时间。4.4 创建订单事务和行锁的完整代码这是整个系统最值得说的接口。创建订单时必须检查时段是否存在、场地是否启用、时段是否已被预约然后在事务中锁定时段记录并更新为已预约状态同时生成订单。# orders/views.py import json from django.db import transaction from django.views.decorators.http import require_http_methods from venues.models import TimeSlot from .models import Order from common.response import ok, fail require_http_methods([POST]) def create_order(request): user request.current_user if not user: return fail(请先登录, code401) data json.loads(request.body) slot_id data.get(slot_id) try: with transaction.atomic(): slot TimeSlot.objects.select_for_update().get(idslot_id) if slot.is_booked: return fail(该时段刚刚被预约了手速再快点) if not slot.venue.is_active: return fail(该场地暂未开放预约) # 锁定时段 slot.is_booked True slot.save() # 创建订单 order Order.objects.create( useruser, slotslot, amountslot.venue.price_per_hour, statuspending ) slot.order order slot.save(update_fields[order]) except TimeSlot.DoesNotExist: return fail(时段不存在) return ok({order_id: order.id, amount: str(order.amount)})用 select_for_update 锁住的是目标时段记录而不是整张表所以并发时只有同时抢同一时段的请求才会串行等待不同时段不受影响。这里有个细节select_for_update 必须和事务配合使用所以代码包裹在 transaction.atomic() 里面。支付接口我这里做的是模拟支付。真实项目中接微信支付需要商户号学生项目或校内内部系统可以先走模拟。接口逻辑很简单校验订单属于当前用户、状态为 pending然后置为 paid记录支付时间。require_http_methods([POST]) def pay_order(request, order_id): user request.current_user try: order Order.objects.select_for_update().get(idorder_id, useruser) except Order.DoesNotExist: return fail(订单不存在) if order.status ! pending: return fail(当前状态不可支付) order.status paid order.paid_at now() order.save(update_fields[status, paid_at]) return ok()取消订单逻辑更要注意只有 pending 和 paid 状态的订单能取消取消后订单状态置为 cancelled对应的时段要恢复 is_bookedfalse 并解除与订单的关联。已完成的订单不允许取消需要在接口里校验。4.5 管理后台的统计接口场地利用率是整个管理端最有价值的数字。我用 Django ORM 的聚合查询按场地和日期统计已预约时段数占当天总时段数的比例。# stats/views.py from django.db.models import Count, Q from venues.models import TimeSlot def utilization(request): date_str request.GET.get(date) stats TimeSlot.objects.filter(datedate_str).values( venue_id, venue__name ).annotate( totalCount(id), bookedCount(id, filterQ(is_bookedTrue)) ) result [] for item in stats: total item[total] booked item[booked] result.append({ venue_id: item[venue_id], venue_name: item[venue__name], total: total, booked: booked, utilization: round(booked / total * 100, 2) if total else 0 }) return ok(result)这个接口支撑了管理端的一个简单看板每天各场地的预约率一目了然。后续如果要按周、按月聚合只要修改时间过滤条件即可。Flask 推荐服务同样是读取订单表统计热度因为 Django 和 Flask 共用同一个 MySQL 数据库Flask 端直接用 SQLAlchemy 查询联表结果。# flask_recommend/app.py from flask import Flask, jsonify from sqlalchemy import create_engine, text app Flask(__name__) engine create_engine(mysqlpymysql://user:passlocalhost/sports_booking) app.route(/recommend) def recommend(): sql SELECT v.category, TIME_FORMAT(s.start_time, %H:%i) as start_time, COUNT(*) as cnt FROM orders o JOIN time_slot s ON o.slot_id s.id JOIN venue v ON s.venue_id v.id WHERE o.status paid AND o.created_at DATE_SUB(NOW(), INTERVAL 30 DAY) GROUP BY v.category, TIME_FORMAT(s.start_time, %H:%i) ORDER BY cnt DESC LIMIT 5 with engine.connect() as conn: rows conn.execute(text(sql)).fetchall() return jsonify([{ category: r.category, start_time: r.start_time, count: r.cnt } for r in rows])5. 小程序端开发实战5.1 项目初始化与请求封装在 HBuilderX 中新建 uni-app 项目选择默认模板然后配置 manifest.json 里的微信小程序 AppID。需要特别的如果你的 uni-app 项目要编译到微信小程序必须在小程序后台申请 AppID否则无法在真机上预览发布。我把请求封装成统一模块核心逻辑是根据当前环境选择 baseURL。开发时连接本地电脑的局域网 IP 调试发布时切换到正式服务器域名。// utils/request.js const BASE_URL https://your-api-domain.com export function request(path, method GET, data {}) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL path, method, data, header: { Content-Type: application/json, Authorization: uni.getStorageSync(token) || }, success: (res) { if (res.data.code 0) { resolve(res.data.data) } else { uni.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail: (err) { uni.showToast({ title: 网络异常请重试, icon: none }) reject(err) } }) }) }5.2 登录流程处理登录是整个小程序的入口。我采用静默登录 用户信息更新的组合用户打开小程序先调用 uni.login 静默获取 code后端直接建立或查找用户账号如果后续需要展示昵称头像再引导用户点击头像昵称填写的微信授权组件。// utils/auth.js import { request } from ./request.js export async function login() { return new Promise((resolve, reject) { uni.login({ provider: weixin, success: async (res) { try { const data await request(/api/auth/wx_login/, POST, { code: res.code }) uni.setStorageSync(token, data.token) uni.setStorageSync(userId, data.user.id) resolve(data) } catch (e) { reject(e) } }, fail: (err) { uni.showToast({ title: 登录失败, icon: none }) reject(err) } }) }) }有个坑提醒一下微信小程序登录 code 五分钟过期、只能用一次。如果前端因为网络问题重复发送同一个 code后端拿 code 换 openid 时会失败所以前端要做好防重登录中加个 loading 状态避免用户反复点击。5.3 场地列表与详情选择场地列表页是一个典型的列表页我使用 scroll-view 实现顶部分类导航下方用 uni-app 的 v-for 渲染场地卡片场地图和价格信息都从接口数据来。场地详情页则是预约操作的核心界面。上部分是场地信息下面是日期选择器和时段列表。日期选择器我用微信小程序的 picker 组件modedate设置 start 为当天。时段列表用 flex 网格布局每个时段是一个格子用不同的类名区分可约/已约/选中状态。view classslot-grid view v-foritem in slotList :keyitem.id classslot-item :classitem.is_booked ? booked : (selectedSlotId item.id ? selected : available) clickselectSlot(item) text{{ item.start_time }}/text text classprice{{ item.price }}元/text /view /view选中时段后底部弹出的确认栏显示价格和预约按钮点击预约跳转到订单确认页或直接调用创建订单接口。我这里做的是先创建订单、再拉起支付订单创建成功后再提示支付。选择时段时我加了一个简单的判断不选未来某天已经过去的时间点。比如现在是下午四点用户选择的日期是今天那起止时间早于当前时间的时段自动标记为过期不做可约处理。5.4 订单列表与取消操作我的订单页用分段器切换“全部、待支付、已支付、已完成”等状态。订单卡片上显示场地名、日期、时段、金额和状态底部按钮根据状态动态展示待支付显示去支付和取消已支付显示取消预约。取消预约这里我踩过一个坑。第一次实现时前端取消订单成功后就立刻刷新但后端因为订单状态校验失败报错了导致前端以为取消成功、后端实际没取消时段一直被占着。最后我在后端取消接口里加了状态判断前端收到成功响应后再更新页面状态同时在下拉刷新时重新拉取订单列表保证两端状态最终一致。5.5 管理端复用 Django Admin管理系统这部分我几乎没写前端页面因为 Django Admin 自带的功能完全够用场地和时段的增删改查、订单的列表展示、按用户或场地筛选、批量修改订单状态。我需要做的只是把 Admin 模块写好。# orders/admin.py from django.contrib import admin from .models import Order admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (id, user, slot, amount, status, created_at) list_filter (status, created_at) search_fields (user__username, slot__venue__name) date_hierarchy created_at actions [mark_as_paid] def mark_as_paid(self, request, queryset): updated queryset.filter(statuspending).update(statuspaid) self.message_user(request, f已将 {updated} 个订单标记为已支付) mark_as_paid.short_description 标记所选订单为已支付Admin 端给体育馆工作人员使用完全足够还能根据需求和账号实现权限分级。发布到生产环境时Admin 路径要设一个不容易猜测的 URL 前缀并开启 Django 的登录认证不能裸奔在网上。6. 部署上线与常见问题速查6.1 后端部署要点Django 项目的生产环境部署我的方案是 Nginx uWSGI MySQL这是一套非常成熟的组合。uWSGI 的配置文件核心是端口、项目路径、虚拟环境路径。一个常见的配置文件如下[uwsgi] chdir /var/www/sports_booking module sports_booking.wsgi:application virtualenv /var/www/sports_booking/venv master true processes 4 threads 2 socket 127.0.0.1:8001 chmod-socket 664 vacuum trueNginx 负责接收小程序端的 HTTPS 请求把 /api 路径转发到 Django 的 uWSGI socket把 Flask 推荐服务监听在 8002 端口单独配置一个 location 转发。国内小程序正式上线要求所有 request 域名必须是已备案且支持 HTTPS 的域名并且必须在小程序后台把域名添加到服务器域名白名单。建议在项目早期就备好域名和 SSL 证书否则后期开发完才发现域名没备案会很被动。同时Django 的 settings.py 里必须把 DEBUG 设为 FalseALLOWED_HOSTS 配置成你的服务器域名或 IP否则会报 Bad Request。静态文件要收集到指定目录并由 Nginx 直接托管避免 Django 处理静态文件影响性能。6.2 小程序发布注意事项微信小程序的发布流程是上传代码到微信公众平台提交审核审核通过后发布。整个流程里最容易卡的环节是类目选择。场地预约类小程序通常归到“生活服务 体育健身”这类目下需要准备对应的资质证明或服务承诺函不同地区的审核要求有差异建议提前在微信公众平台上确认需要的材料。真机预览时还有一个常见问题是局域网 IP 访问不到。开发阶段如果手机和电脑不在同一 Wi-Fi 或有 AP 隔离后端服务无法访问解决办法是买一台云服务器把后端部署上去开发阶段就直接使用测试域名。6.3 开发过程中踩过的坑这里把我在项目中实际撞过的问题和排查过程整理成一个速查表给同行参考。第一Django 时区问题。默认 USE_TZTrue 时Django 存数据库的时间是 UTC如果场地开放日期和时间直接用 datetime 传参会出八小时偏差。我的做法是项目全部使用本地时间设置 USE_TZFalseTIME_ZONEAsia/Shanghai预约业务不涉及时区换算避免了很多隐形问题。第二select_for_update 失效。Django 的 select_for_update 如果事务没开启或者数据库表使用了 MyISAM 引擎行锁不会生效。排查时我确认了两点事务保证在 with transaction.atomic() 块内执行数据库表采用 InnoDB 引擎。最好用 ALTER TABLE 明确指定 ENGINEInnoDB。第三Flask 推荐服务和 Django 共用 MySQL 可能触发数据库连接数过高。两台服务各自维护连接池连接数爆发时数据库可能拒绝新连接。我的处理是控制两个服务的工作进程数并为 MySQL 调整了 max_connections 参数。第四时段表数据量增长快。如果场地多、时段密一张表一个月就可能积累几万条时段记录。虽然量级不大我还是在 date 和 venue_id 上建了联合索引查询速度稳定在毫秒级。第五微信开发者工具显示请求失败。排查时发现是开发环境的 baseURL 用了 localhost小程序真机访问的 localhost 指向手机本身必须改用电脑局域网 IP且关闭手机上的代理设置。在 HBuilderX 里如果是浏览器预览则没有这个问题但这个混淆点很容易忽略。第六预约开放时间控制。体育馆通常不是全天开放比如早上八点到晚上十点。如果在数据库里为凌晨和深夜时段也生成 slot 记录前端展示会很杂乱。我的做法是管理端设置一个每日开放时间范围脚本只在开放范围内批量生成时段。实操经验小结整个项目从零到上线我一个人前后花了差不多两周时间核心代码量不大难度集中在并发控制、状态同步和部署细节上。这里我特别想说的是预约系统这类项目业务边界一定要先理清楚谁可以约、什么时候可约、取消的限制是什么、超时怎么处理这些规则不定义清楚代码写得再漂亮也会出问题。如果你也准备做类似的系统建议先从数据库表设计开始把状态流转图画出来然后按“登录 - 查场地 - 锁时段 - 下单 - 支付 - 取消”这条链路逐步打通后端接口最后再写小程序页面。前后端并行开发容易因为接口定义不一致反复返工串行推进反而更稳。最后分享一个小技巧开发阶段我会在 Django 的响应头里加上一个自定义字段输出 SQL 执行时间在小程序端配合 vConsole 查看请求耗时方便及时发现那些被 N1 查询拖慢的接口。上线前我把所有列表接口的查询都加上 select_related 和 prefetch_related这是 Django 优化数据查询最常用的两个方法效果好且改动成本极低。