做化妆造预约系统,前后端技术栈怎么配才顺手?这个标题里同时出现了 Flask 和 Django,老实说第一次看到的时候我也愣了一下——这两个框架平时很少出现在同一个项目里。但实际做下来你会发现,这个组合不但不冲突,反而把两边各自的强项发挥得挺到位:Django 自带一套成熟的后台管理体系,用来做运营管理端非常省事;Flask 轻量灵活,拿来写小程序要调的 API 接口,代码结构清爽,响应速度也容易控制。再加上微信小程序作为用户端入口,整个系统基本覆盖了"用户在小程序里下单、管理员在后台上架项目和处理订单"这条完整链路。
这套方案非常适合两类人参考:一类是正在做毕业设计、需要快速落地一个全栈项目的同学,另一类是美业门店想低成本搭建自己的预约系统、又不想被第三方 SaaS 平台抽成的经营者。下面我会从项目拆解、数据库设计、后端接口、小程序端开发到部署上线,把我的做法和踩过的坑都整理出来。
1. 项目整体设计与技术选型思路
1.1 为什么是 Python 而不是 Java 或 Node
化妆造服务预约系统本质上是一个"信息管理 + 交易撮合"的业务系统,核心动作就是用户浏览服务、选时间、提交预约,管理员接收订单、安排化妆师、处理状态。这类系统最大的特点就是 CRUD 密集、业务规则集中,没有特别重的高并发要求。选 Python 就是看中两点:一是开发效率高,模型定义完直接跑迁移,几分钟就能把数据库表建起来;二是生态里 Flask 和 Django 两个框架正好对应"轻接口"和"重后台"两种需求,不用硬着头皮在一个框架里塞所有东西。
我当时的判断标准很简单:如果只用 Flask,后台管理界面得自己写一大堆 HTML 和 CRUD 视图,浪费不少时间;如果只用 Django,做小程序接口又显得有点重——Django 的中间件、CSRF、Admin 这些机制对纯 JSON API 来说大部分用不上,还要额外处理跨域。两个框架配合使用,Flask 只负责 API,Django 只负责后台,职责边界非常清楚。
1.2 Flask 和 Django 的职责划分
这个项目里 Flask 和 Django 跑在同一个 Python 环境、连同一个数据库,但各自只做自己擅长的事:
- Flask 端:只暴露微信小程序需要的 JSON 接口,比如获取服务项目列表、获取化妆师排班、提交预约、查询订单状态。接口路径都在
/api前缀下,数据格式统一是 JSON,不渲染任何 HTML 页面。 - Django 端:作为管理后台,处理管理员登录、服务项目增删改、化妆师信息维护、预约订单审核、排班管理这些操作。Django Admin 自带的用户权限模型只需要简单配置就能用,改完数据直接写入同一张表,Flask 接口立刻就能读到最新数据。
这种"一个业务系统、两个框架各管一头"的做法,听起来有点非主流,但在中小型项目里非常实用。数据库表是两边共用的,所以表结构的设计必须提前想清楚,不能像单框架项目那样边写边改。我建议在动手写代码之前,先把所有表结构定下来,两边再各自开发,避免后期字段对不上。
1.3 为什么前端选微信小程序而不是 App 或 H5
美业预约的使用场景基本都在微信里:用户看到朋友圈或者公众号推荐,点开小程序就能预约,不用下载 App,也不用跳转浏览器。微信小程序对创业者来说还有一个好处——不需要上架各大应用商店,审核通过后直接通过微信搜索触达用户,获客成本比原生 App 低一个量级。
还有一个很现实的原因:微信小程序的支付体系成熟,用户习惯已经培养好了。虽然我这个版本先做的是"预约后到店支付"的简化流程,但接口设计上已经预留了支付回调的扩展位,后续接微信支付不用推翻重来。
2. 核心功能模块与数据库设计
2.1 功能模块拆解
整个系统按角色分三类:普通用户、化妆师、管理员。用户端面对的是小程序,化妆师和管理员面对的是 Django 后台。
用户端功能:
- 微信登录:小程序端调用
wx.login()获取 code,后端通过 code 换取 openid,作为用户唯一标识。 - 浏览服务:查看化妆造服务项目列表,包括服务名称、图片、价格、时长、适用场景说明。
- 选择化妆师:每个服务项目下可以关联多个化妆师,用户能看到化妆师的简介、评分、档期。
- 提交预约:选择日期和时段,填写联系方式和备注,提交后生成预约订单。
- 订单管理:查看自己的预约记录,包括待确认、已确认、已完成、已取消四种状态,支持取消预约。
后台端功能:
- 服务项目管理:上架、下架、编辑化妆造服务,设置价格和时长。
- 化妆师管理:添加化妆师资料,设置服务时段,查看每位化妆师当天的预约情况。
- 订单管理:查看所有预约订单,确认或拒绝预约,标记订单完成。
- 数据统计:按日期统计预约量、营收,虽然这版只做了简单聚合,但 Django 后台的 ORM 写统计查询非常顺手。
2.2 数据库表结构设计
先说明一个原则:所有表的关联字段,在一开始就要设计好,不要在开发过程中频繁改表结构。因为 Flask 和 Django 同时读写这些表,改一次表结构要同步修改两边的模型和迁移文件,非常容易出问题。
我设计的核心表有六张:
用户表 user
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| openid | varchar(64) | 微信 openid,唯一 |
| nickname | varchar(64) | 用户昵称 |
| avatar_url | varchar(255) | 头像地址 |
| phone | varchar(20) | 手机号 |
| created_at | datetime | 注册时间 |
服务项目表 service
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| name | varchar(100) | 服务名称 |
| description | text | 服务详情 |
| cover_image | varchar(255) | 封面图 |
| price | decimal(10,2) | 价格 |
| duration | int | 服务时长(分钟) |
| status | tinyint | 1上架 0下架 |
化妆师表 beautician
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| name | varchar(50) | 姓名 |
| avatar | varchar(255) | 头像 |
| title | varchar(100) | 头衔/职称 |
| intro | text | 个人简介 |
| rating | decimal(3,1) | 评分 |
预约表 appointment
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| user_id | int | 用户 ID |
| service_id | int | 服务 ID |
| beautician_id | int | 化妆师 ID |
| appoint_date | date | 预约日期 |
| start_time | time | 开始时间 |
| end_time | time | 结束时间 |
| remark | varchar(255) | 用户备注 |
| status | tinyint | 0待确认 1已确认 2已完成 3已取消 |
| created_at | datetime | 提交时间 |
时段表 time_slot(用于化妆师排班管理)
| 字段 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| beautician_id | int | 化妆师 ID |
| work_date | date | 排班日期 |
| slot_start | time | 时段开始 |
| slot_end | time | 时段结束 |
| is_booked | tinyint | 0空闲 1已预约 |
评价表 review(扩展用,这版已实现基础字段)
表结构设计完,接下来就要回答一个关键问题:Flask 和 Django 怎么共用这六张表?
2.3 双框架共用数据库的模型同步方案
这一步是这个项目最核心的工程难点。两个框架如果各自管理自己的一套模型,表结构很容易出现不一致。我的做法是:以 Django 的模型为主,统一用 Django 的迁移工具建表,Flask 端只定义和自己业务相关的映射模型,不执行建表操作。
具体操作是:
- 先在 Django 项目里建好所有 app 和 model,执行
python manage.py makemigrations和python manage.py migrate,把表全部建出来。 - 在 Flask 项目里,用 SQLAlchemy 的定义
db.Table或者db.Model映射到同一批表,只声明字段,严禁db.create_all()。因为create_all()默认只会建不存在的表,如果两边字段定义不一致,它不会帮你改,只会默默地在表结构不同的情况下运行,埋下隐患。 - Flask 端读取的表结构以实际数据库为准,字段多出或缺失都报错后立刻查 Django 的模型定义,保证两边对字段名、类型、长度完全一致。
用这个方法,我最开始遇到的"Flask 写进去了记录,Django 后台却查不到"问题就再也没出现过。
3. 后端接口开发与核心业务逻辑
3.1 Flask 接口目录结构和统一返回格式
Flask 端不是一个完整的 Web 应用,它只做 API Server。我的项目结构是这样:
flask_api/ ├── app.py # 入口文件,注册蓝图 ├── config.py # 数据库配置、微信小程序配置 ├── models.py # SQLAlchemy 模型映射 ├── utils.py # 工具函数,token 校验、时间处理 ├── api/ │ ├── auth.py # 微信登录、token 签发 │ ├── services.py # 服务项目接口 │ ├── beautician.py # 化妆师接口 │ └── appointment.py # 预约相关接口 └── requirements.txtFlask 接口的返回格式我统一成下面这种结构,小程序端解析起来特别方便,不用每个接口单独判断字段:
{ "code": 0, "message": "success", "data": {} }code为 0 表示成功,非 0 表示业务错误,比如 40001 表示该时段已被预约,40002 表示服务已下架。小程序端只需判断code,都不用看message就能决定下一步动作。
3.2 微信登录与 Token 校验
微信小程序登录的流程,核心是前端拿code换后端返回的openid,然后后端自己签发一个 token 给小程序,后续所有请求都带这个 token。
Flask 端通过requests调用微信官方接口换取 openid,这一步有个容易犯的错:直接拿官方接口返回的session_key当登录态。session_key是微信用来解密用户手机号等敏感信息的,不能暴露给前端。正确做法是后端自己生成 token,我这里用的是itsdangerous,生成带时间戳的签名 token,有效期设为 7 天,用户重新打开小程序时如果 token 过期就重新静默登录。
核心代码:
# auth.py from itsdangerous import TimedJSONWebSignatureSerializer as Serializer def generate_token(openid): s = Serializer(app.config['SECRET_KEY'], expires_in=7 * 86400) return s.dumps({'openid': openid}).decode() def verify_token(token): s = Serializer(app.config['SECRET_KEY']) try: data = s.loads(token) return data['openid'] except Exception: return None这个方案的优点是服务端无状态,不需要把 token 存数据库,Flask 和 Django 之间也不需要共享会话。小程序端的每个请求都在header里带上Authorization: Bearer <token>,Flask 写一个before_request装饰器统一校验,除了登录接口之外全部放行。
3.3 预约冲突检测——这里最需要花功夫
如果你只是简单地让用户提交预约就 INSERT 一条记录,上线之后很快会发现一个问题:同一个化妆师在同一个时间段被预约了两次。预约系统最核心的并发控制就是这里。
我的解决思路是两重保险:
第一重,数据库层面。给 appointment 表加一个唯一约束,字段组合是(beautician_id, appoint_date, start_time),这样即便代码层面漏了判断,数据库也会拒绝重复插入。
第二重,应用层面。查询待确认和已确认状态下的预约记录,判断新提交的时间段是否交叉。这里的判断逻辑不能只查绝对相等的时间,因为服务时长不同——化妆服务 60 分钟,美发服务可能 90 分钟,用户约了 10:00 的化妆,下一个用户不能约 10:30 的美发,因为时间重叠了。
判断代码:
def check_time_conflict(beautician_id, date, start_time, end_time): conflict = Appointment.query.filter( Appointment.beautician_id == beautician_id, Appointment.appoint_date == date, Appointment.status.in_([0, 1]), # 待确认和已确认都算占用 Appointment.start_time < end_time, Appointment.end_time > start_time ).first() return conflict is not None这段 SQL 的语义是"两条预约时间段是否存在交集"。一开始我用的是start_time <= conflict_end_time这种写法,结果时段相邻的预约被误判为冲突,后来改成上面的严格小于判断,前后两个时段恰好首尾相接时就不会误伤。
还有事务问题:提交预约时,先查冲突,再 INSERT,这两个操作要放在同一个db.session事务里,避免两个请求同时查到不冲突然后一起插入。遇到数据库唯一约束冲突时,捕获异常返回"该时段已被预约",用户刷新后自然能看到新状态。
3.4 Django 后台的模型与 Admin 配置
Django 端的事情就纯粹多了,核心工作是把六张表转换成 Django 的 model,然后注册进 Admin 后台。
有一个容易踩坑的点:不要在 Django 里用AutoField之外的字段类型和 Flask 端映射错位。比如数据库里status字段是tinyint,Django 模型里就建议也用SmallIntegerField,不要图省事用BooleanField,因为预约状态有 4 种取值,BooleanField装不下。同理,price字段用DecimalField(max_digits=10, decimal_places=2),两边保持一致。
Admin 注册时我做了几件事:
- 订单列表按创建时间倒序,默认展示最近 20 条,避免数据一多页面卡顿。
- 服务项目的
status字段用list_editable直接支持列表页切换上架/下架。 - 预约订单添加自定义筛选:按日期筛选、按状态筛选、按化妆师筛选。
- 化妆师详情页内嵌预约记录,用
TabularInline显示当天所有预约,方便查档期。
Django Admin 有一个你可能没注意的技巧:定义get_queryset时用select_related把关联的外键字段提前 join 出来,列表页加载速度会有肉眼可见的提升,尤其是订单多了以后。
4. 小程序端开发实战与关键细节
4.1 小程序项目的基本结构
前端我选的是原生微信小程序,没用 uniapp。原因很简单:这个业务只有 6 个页面,原生开发完全够用,而且原生小程序在调试工具里出现问题更好排查。uniapp 确实能一套代码多端复用,但如果你没有同时要出 H5 和 App 的需求,引入框架反而增加心智负担。
页面规划如下:
pages/index/index:首页,展示服务项目轮播图和列表pages/service/detail:服务详情页,选择化妆师和日期时段pages/appointment/confirm:预约确认页,填写联系方式和备注pages/order/list:订单列表页,按状态切换查看pages/order/detail:订单详情页,展示预约信息和状态操作pages/profile/profile:个人中心,显示用户信息和登录状态
4.2 小程序调用后端接口的两种方式对比
小程序发请求,官方推荐用wx.request,这里有一个开发环境适配要提前处理:微信开发者工具里默认不校验合法域名,但真机上必须配置 HTTPS 域名,且域名需要在小程序后台添加白名单。
开发阶段我的做法是:
- 在
app.js里定义一个全局变量baseUrl,开发环境填http://127.0.0.1:5000,生产环境填线上 HTTPS 域名。 - 因为本地开发用的是 HTTP,开发者工具里要勾选"不校验合法域名"选项,否则请求会被拦截。
- 后端 Flask 还要配置 CORS 跨域。这里提个醒,
flask-cors的默认配置是全放行,开发阶段方便,但上线前一定把origins参数改成白名单,避免任何网站都能调你的接口。
wx.request封装好公共请求函数后,小程序端不用关心 token 的获取逻辑,统一在请求头里带上。我的封装:
// utils/request.js const request = (url, method, data) => { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: getApp().globalData.baseUrl + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + token }, success: (res) => { if (res.data.code === 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: 'none' }); reject(res.data); } }, fail: (err) => reject(err) }); }); };4.3 预约选择页面的时段加载逻辑
用户在服务详情页选择化妆师之后,需要展示这位化妆师在选定日期下可预约的时段。这个逻辑前端不能自己算,必须交给后端,原因很简单:后端才知道每个化妆师今天有没有排班、已经预约了多少单。
小程序端发请求时会带beautician_id和date两个参数,Flask 查询出当天这个化妆师的排班数据,再过滤掉已经被预约的时段,返回可预约列表。前端拿到列表渲染成选择项。
这里有一个体验层面的细节:时间段按钮建议用"禁用态"而不是"隐藏"。用户看到灰色的不可选时段,会理解这是已被约走的,如果直接不显示,用户会觉得页面加载不全,甚至会反复刷新怀疑出 bug 了。
4.4 缓存与登录状态的正确处理
微信小程序的缓存我用得很克制,只存了三类数据:
- token:登录凭证,7 天有效。
- 用户昵称和头像:用于个人中心展示,避免每次打开都重新调接口。
- 服务项目列表的简单缓存:设置 30 分钟失效,小程序端获取时先读缓存,过期再请求。
有一个细节提醒大家:wx.getStorageSync读取缓存时如果 key 不存在,返回的是空字符串而不是null。判断缓存是否存在时,不要用if (cache === null),要用if (cache === '' || cache === null),否则会出现第一次进页面时缓存穿透的问题。
关于顶部导航栏,不同机型状态栏高度不一样,iPhone 有刘海屏,安卓机型状态栏高度各不相同。写死状态栏高度会在部分机型上出现布局错位。正确做法是通过wx.getSystemInfoSync()获取statusBarHeight,动态计算导航栏每部分的高度。这个坑我第一次做小程序时踩过,后来写的所有小程序都统一封装了这个适配方法。
4.5 表单校验与提交预约
预约确认页面用户要填手机号和备注,表单校验看似简单,但有几个边角情况:
手机号校验我用的是^1[3-9]\d{9}$,这个正则覆盖了目前所有正常手机号段。但注意,有些用户会填座机号,如果严格按手机号校验会拦截,所以在设计上要区分"预约联系"和"账号绑定"两种场景。我的处理是预约时的手机号允许留空,填了则校验格式,留空后管理员可以在后台看到用户通过微信客服联系的入口,不做强制。
提交预约成功后,不要立刻wx.navigateBack返回上一页,而是弹出一个成功提示,然后跳转到订单详情页。用户看到"预约成功"这个明确反馈,比返回到列表页自己找订单要好得多。订单详情页再展示"商家确认中,请留意微信通知"的说明。
5. 部署、联调与常见问题排查
5.1 本地部署环境和依赖管理
项目的 Python 环境我推荐用虚拟环境,不要直接装全局。两个框架依赖可以装同一个虚拟环境里,因为 Flask 和 Django 在依赖层面本身没有冲突。我的requirements.txt里核心依赖就这些:
flask==2.3.3 flask-sqlalchemy==3.0.5 flask-cors==4.0.0 requests==2.31.0 itsdangerous==2.1.2 django==4.2.7 mysqlclient==2.2.0数据库我用的 MySQL 5.7,因为微信小程序云开发虽然免费但没法让 Flask 和 Django 直接连接,还是自建 MySQL 最可控。数据库编码一定要设成utf8mb4,因为用户昵称会包含 emoji 和生僻字,utf8会报错。
启动方式分两个终端:
# Flask cd flask_api source venv/bin/activate python app.py # Django cd django_admin python manage.py runserver 0.0.0.0:8000注意两个服务占用不同端口:Flask 跑 5000,Django 跑 8000,这样同时开发互不干扰。
5.2 线上部署的注意事项
线上部署我没用特别复杂的架构,一台云服务器就搞定了。几个关键点:
- Flask 不要用自带的开发服务器上线,
app.run()那个是单线程的,并发一高就卡。我用了waitress,纯 Python 实现的 WSGI 服务器,安装简单,一行命令启动:waitress-serve --port=5000 app:app。 - Django 用
gunicorn或者uwsgi都行,我用的是gunicorn,配合 Nginx 反向代理。 - Nginx 配置两个 server 块,一个把
api.你的域名.com代理到 Flask 的 5000 端口,一个把admin.你的域名.com代理到 Django 的 8000 端口。 - 小程序要求所有请求必须是 HTTPS,所以服务器上必须配置 SSL 证书。我用的是免费证书,配置 Nginx 时把 HTTP 流量强制跳转到 HTTPS,避免小程序真机上请求失败。
数据库备份也很重要,我写了一个每天凌晨 3 点自动备份的 cron 任务,备份文件保留 7 天。预约数据是门店的核心资产,丢了很难找回。
5.3 高频问题排查实录
问题一:Django 后台修改了数据,Flask 接口查不到
先查数据库确认数据已经写入,如果写入成功但没有查到,绝大多数是缓存问题。我用 SQLAlchemy 时默认没有开查询缓存,但如果用了query的.all()结果又被手动缓存过,就会出这种事。排查方法很简单,重启 Flask 进程再请求一次,如果恢复了就是进程内缓存,在代码里去掉缓存逻辑就好。
问题二:小程序请求接口一直提示"开发者工具未配置合法域名"
这个看报错信息就能定位。开发阶段就在详情页勾选"不校验合法域名",上线之前一定记得在小程序后台配置 request 合法域名。有个坑是:配置完域名后,要等几分钟才生效,而且开发者工具里要重新编译一次才加载新配置。
问题三:提交预约后接口返回 500,日志显示数据库事务超时
这个我遇到过,原因是预约冲突检测和插入操作之间,发生了死锁。两个用户同时预约同一个化妆师同一时段时,两个事务都先查后插,互相持有锁等待对方释放。解决方法是:在 Flask 中把冲突检测和插入放到同一个事务里,并设置数据库隔离级别为READ COMMITTED,同时捕获唯一约束冲突异常。加一个try-except把 500 变成业务错误提示,用户体验完全不同。
问题四:Django 里删除对象时关联数据报错
Django 的 ForeignKey 默认是PROTECT行为,删除有子记录的父对象时会被拦截,提示"无法删除"。如果你确实需要级联删除,要在模型定义时指定on_delete=models.CASCADE。但预约记录这种数据,我建议用软删除而不是物理删除:加一个is_deleted字段,删除操作只更新字段,不真的删记录。这样以后要查历史订单、做数据统计,数据都还在。
问题五:时间显示的时区问题
如果你配置了TIME_ZONE = 'UTC',存进数据库的时间会比北京时间少 8 小时。预约场景里时间错了是灾难性的。统一方案是:Django 设置TIME_ZONE = 'Asia/Shanghai'和USE_TZ = True,Flask 端所有时间读写也统一使用本地时间,不要用 UTC。前后端约定所有时间参数只传日期和"HH:MM:SS"格式的字符串,不传时间戳,避免时区转换出错。
5.4 关于数据安全和接口鉴权
最后聊一下接口安全。很多人做小程序后端时觉得反正只有自己的小程序在调接口,就不做鉴权了,这是个巨大的坑。任何知道你的接口地址的人,都可以直接构造请求调用你的预约接口,批量下单把你的化妆师档期占满。
基础防护我做了四层:
- 微信登录返回的 token 必须校验,没有合法 token 的请求直接返回 401。
- Flask 端写了一个
before_request钩子,白名单放行/api/auth/login,其余接口全部校验 token。 - 对提交预约、取消预约这类写操作,校验请求频率,同一用户每分钟最多 20 次,超出直接拒绝。用简单的进程内计数器实现,不引额外组件。
- 手机端提交的数据全部做后端校验,绝对不信任前端传过来的价格、时长、化妆师 ID。服务项目信息和价格必须是后端根据
service_id自己查出来的,前端传什么价格字段都忽略。我见过只验证前端传值的结果,用户把价格改成 0 元下单的案例,后端校验一下就能彻底堵住。
还有一个容易被忽略的点:小程序端展示的图片,如果用的是自己的服务器存储,要注意图片访问路径不能泄露服务器目录结构。我这边处理方式是通过 Nginx 的 alias 映射,把实际目录遮蔽掉,图片 URL 统一用/media/xxx.jpg这样的格式对外。
一些个人体会
这套 Flask + Django + 微信小程序组合,整体做下来最大的感受就是边界清晰。后端两个框架各管各的,你不用在 Flask 里硬写管理后台,也不用在 Django 里为了返回 JSON 绕过一堆默认机制,省下来的时间正好都花在业务的打磨上——比如预约冲突检测做得更严谨、小程序端交互做得更顺手。
如果你也正在规划一个类似的预约系统,我建议你从数据库表设计开始,先把所有表的字段、关联关系、状态枚举定义清楚,再动手写代码。双框架项目最怕中途改表结构,改一处,两边都要跟着改,工作量翻倍。
最后送你一个我在实际项目里常用的自检清单:接口返回格式是否统一、事务边界是否清晰、预约冲突判断是否覆盖了所有状态、线上环境是否已经开启 HTTPS、小程序请求域名是否已配置。把这几条跑一遍,你的系统基本可以稳定跑起来。我后来把这个项目的经验复制到了美容、美发和摄影棚预约好几个场景,核心代码改一改字段就能复用,这套底子是靠谱的。