ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Flask+Vue宠物领养寄养系统开发实战:从数据库设计到部署避坑

Flask+Vue宠物领养寄养系统开发实战:从数据库设计到部署避坑 最近整理项目代码时翻到一套之前花了两周时间搭出来的“宠物之家”系统技术栈是 Flask 做后端Vue 做前端页面开发环境用 PyCharm。这个项目在同类里非常典型宠物领养、临时寄养、后台管理、用户登录注册全都是老生常谈但又绕不开的功能。很多人拿到这种题目第一反应是套个管理系统模板但真要从零把一个可演示、可部署的完整系统写出来里面还是有不少值得说的细节。这篇博文我就拿这个项目当例子从需求拆解、技术选型、数据库设计、核心接口、前端联调到部署避坑完整串一遍。先说一下项目的真实定位它不是一个大厂级别的中台就是一个单体应用后端 Flask 提供 REST API前端 Vue 写单页页面数据库用的 SQLite 做开发环境、上线再切 MySQL。这个组合在课程设计、个人作品集、小团队内部工具里出现的频率非常高原因也很简单——能跑、能改、能演示还不重。接下来按模块拆开讲。1. 项目盘点与需求拆解1.1 宠物之家在解决什么业务问题宠物之家解决的是两个看起来相似、但逻辑上要分开的场景领养和寄养。领养场景里平台是信息撮合方。有宠物来源方通常是救助站、个人送养人把宠物信息发布出来有领养意向的用户浏览宠物列表看中之后提交领养申请管理员在后台对申请做审核审核通过后宠物状态改成“已领养”。这个流程看着简单但状态处理很考验设计。比如宠物一旦被别人提交了申请就不能继续出现在“可领养”列表里了否则两个人同时申请同一只宠物后台审核就会打架。寄养场景则更像一个服务预约流程。宠物主人因为出差、旅游等原因没法照顾宠物就在平台上下一个寄养订单选择寄养开始和结束日期系统根据时长算费用管理员确认后订单进入“寄养中”状态寄养结束后订单完成。这里面涉及日期重叠校验、订单状态流转、费用计算逻辑比领养复杂一点。这两个场景合在一起就是宠物之家的全部核心。剩下的是用户管理、内容管理这类周边功能本质上都是给这两个主流程服务的。1.2 角色、权限与核心功能边界系统里我设计了三种角色普通用户、管理员、访客。访客不是一种持久角色只是为了描述“未登录状态下的浏览行为”。角色主要能力典型操作访客浏览宠物列表、查看宠物详情打开首页看宠物不登录也能看普通用户注册登录后提交领养申请、创建寄养订单、管理个人资料申请领养“布丁”、给自家猫预约7天寄养管理员管理宠物信息、审核领养申请、处理寄养订单、管理用户上架新宠物、通过/驳回领养申请、确认寄养订单权限边界这件事我在开发时就强调了一点前端菜单隐藏只是体验问题后端接口必须再做一次鉴权。很多人前端的“删除按钮”用v-ifrole admin控制但后端的/api/pet/delete接口谁都能直接调。这种漏洞在演示时可能没事答辩或外包验收时一问就露馅。所以后端每个接口我都在路由装饰器层做了角色校验管理员接口除了要登录还要检查 token 里的角色字段。这个细节放在第三章详细讲。2. 技术选型Flask 作为后端Vue 作为前端2.1 为什么用 Flask 而不是 Django项目标题里同时出现了 Flask 和 Django很容易让人困惑。实际项目里我选的是 Flask原因很现实这个系统规模不大Flask 的轻量优势能让我把每个模块的逻辑都写得很清楚。具体展开说一下两者差异。Django 自带 ORM、Admin 后台、Form 校验、迁移工具功能完整但是“完整”的另一面是“重”。你建一个模型之后Django 自动帮你生成一套后台看起来省事可对自定义业务逻辑比较强的场景反而不顺手。比如领养申请这个流程状态流转和审核操作被我塞了比较多的自定义判断Django Admin 默认的增删改查页面要改很多配置才能贴合业务。Flask 不一样它只提供基础的路由、请求上下文剩下的一切由你自己拼装代码里面的每一个判断都清清楚楚调试时路径也短出错更容易定位。另外一个考虑因素是学习曲线。Flask 的代码从app Flask(__name__)开始写一个接口只要几行入门成本低。这个项目如果给一个刚开始接触前后端分离的人看Flask 的后端代码比 Django 更容易看懂因为数据流是肉眼可见的。2.2 前后端分离架构怎么落地前后端分离听起来高级实际上就是把“界面展示”和“业务逻辑”拆到两个进程里。前端 Vue 跑在 8080 端口负责渲染页面、收集用户操作、发请求后端 Flask 跑在 5000 端口只处理 API 请求并返回 JSON。项目的目录结构大概长这样pet-home/ ├── backend/ │ ├── app.py │ ├── models.py │ ├── auth.py │ ├── api/ │ │ ├── user_api.py │ │ ├── pet_api.py │ │ ├── adoption_api.py │ │ └── foster_api.py │ ├── requirements.txt │ └── config.py └── frontend/ ├── public/ ├── src/ │ ├── api/ │ ├── router/ │ ├── views/ │ └── components/ └── package.json后端不是把所有路由写在同一个文件里而是按照业务模块拆成了几个 Blueprint。这个拆分很重要不然随着接口数量增多app.py会膨胀到没法维护。开发阶段前端通过代理转发请求Vue CLI 的vue.config.js里配置一个 devServer.proxy 就能把/api开头的请求转发到后端。上线阶段则用 Nginx 做反向代理同一个域名下/指向前端静态文件/api指向后端服务这样跨域问题也顺带解决了。2.3 PyCharm 下的开发环境搭建PyCharm 配合 Flask 项目的体验确实好主要功劳集中在三块虚拟环境管理、代码补全和调试器。我建议不要用全局 Python 环境而是给项目建一个独立的虚拟环境。PyCharm 里新建项目时直接选 Virtualenv或者进入Settings - Project - Python Interpreter新增一个。虚拟环境可以把 Flask、SQLAlchemy、PyJWT 这些依赖隔离在项目目录内换机器部署时只需要用pip freeze requirements.txt导出一份依赖列表目标机器上pip install -r requirements.txt就能还原。调试这块Flask 自带 debug 模式能打印错误堆栈但 PyCharm 的断点调试更实用。在某个 API 函数里打一个断点浏览器发起一次请求PyCharm 会自动把请求拦截住一步一步看变量变化。这种能力在处理登录鉴权、状态流转类逻辑时特别好用。3. 核心业务模块的设计与实现3.1 数据库模型设计宠物之家涉及四张核心表用户表、宠物表、领养申请表、寄养订单表。四张表各自独立又通过外键关联成一个闭合的数据关系网。用 Flask-SQLAlchemy 定义模型代码大致是这样# models.py from datetime import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db SQLAlchemy() class User(db.Model): __tablename__ user id db.Column(db.Integer, primary_keyTrue) username db.Column(db.String(80), uniqueTrue, nullableFalse) password_hash db.Column(db.String(256), nullableFalse) role db.Column(db.String(20), defaultuser) phone db.Column(db.String(20)) created_at db.Column(db.DateTime, defaultdatetime.utcnow) def set_password(self, password): self.password_hash generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Pet(db.Model): __tablename__ pet id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(80)) species db.Column(db.String(20)) # cat / dog breed db.Column(db.String(50)) age db.Column(db.Integer) status db.Column(db.String(20), defaultavailable) cover_url db.Column(db.String(200)) description db.Column(db.Text) created_at db.Column(db.DateTime, defaultdatetime.utcnow) class Adoption(db.Model): __tablename__ adoption id db.Column(db.Integer, primary_keyTrue) pet_id db.Column(db.Integer, db.ForeignKey(pet.id)) user_id db.Column(db.Integer, db.ForeignKey(user.id)) status db.Column(db.String(20), defaultpending) remark db.Column(db.Text) apply_time db.Column(db.DateTime, defaultdatetime.utcnow) class FosterOrder(db.Model): __tablename__ foster_order id db.Column(db.Integer, primary_keyTrue) pet_id db.Column(db.Integer, db.ForeignKey(pet.id)) owner_id db.Column(db.Integer, db.ForeignKey(user.id)) start_date db.Column(db.Date) end_date db.Column(db.Date) fee db.Column(db.Float, default0.0) status db.Column(db.String(20), defaultpending) note db.Column(db.Text)设计时我重点考虑了两个点。第一个是密码存储。密码绝对不能明文存进数据库。这里用 Werkzeug 的generate_password_hash做加密校验时用check_password_hash。即使数据库文件泄露攻击者拿到的也是哈希值不是原始密码。第二个是宠物状态字段。宠物表里我留了一个status字段可选值有available可领养、pending申请审核中、adopted已领养、suspend下架。为什么不用布尔值的“是否可领养”因为状态之间存在过渡期用户提交申请之后到管理员审核之前宠物既不能算“可领养”也还没到“已领养”如果只用一个布尔字段这个区间没法表达。多状态枚举字段虽然看着笨但逻辑最清晰。3.2 基于 JWT 的登录与权限拦截登录模块我选的是 JWT不是传统的 Session。原因很简单前后端分离场景下前端和后端不在同一个端口Session 依赖 Cookie 的自动携带配置起来麻烦跨域时还要处理 CORS 的credentials设置。JWT 天然适合这种架构用户登录成功后后端返回一个 token 字符串前端存到本地存储里每次请求在请求头带上Authorization: Bearer token即可。核验逻辑写成一个装饰器# auth.py import jwt from functools import wraps from flask import request, jsonify, g from datetime import datetime, timedelta SECRET_KEY change-me-in-production def generate_token(user): payload { uid: user.id, role: user.role, exp: datetime.utcnow() timedelta(days7) } return jwt.encode(payload, SECRET_KEY, algorithmHS256) def login_required(fn): wraps(fn) def wrapper(*args, **kwargs): token request.headers.get(Authorization, ) if token.startswith(Bearer ): token token[7:] try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) g.user_id payload[uid] g.user_role payload[role] except Exception: return jsonify({code: 401, msg: 登录已失效}), 401 return fn(*args, **kwargs) return wrapper def admin_required(fn): wraps(fn) def wrapper(*args, **kwargs): token request.headers.get(Authorization, ) if token.startswith(Bearer ): token token[7:] try: payload jwt.decode(token, SECRET_KEY, algorithms[HS256]) except Exception: return jsonify({code: 401, msg: 登录已失效}), 401 if payload.get(role) ! admin: return jsonify({code: 403, msg: 无权限}), 403 g.user_id payload[uid] g.user_role payload[role] return fn(*args, **kwargs) return wrapperSECRET_KEY在实际项目里不能写死在代码中我上线时改成了从环境变量读取。JWT 有一个坑要注意它不需要服务端存储状态所以 token 一旦签发在过期之前无法主动撤销。如果遇到安全要求高的场景要么把 token 过期时间设短要么加一层服务端黑名单但那样反而牺牲了 JWT 的无状态优势取舍要看具体项目级。3.3 宠物管理与领养申请闭环宠物管理接口天然分成两个角色视角管理员可以增删改查宠物普通用户只能看列表和详情。删除接口我特意做了软删除策略。后来想了想在这个系统里宠物被删除通常意味着“下架”而不是“从数据库抹掉”直接DELETE FROM pet WHERE id1会把历史申请记录的外键关系搞断。所以实际实现里我没有用物理删除而是把status改成suspend列表接口默认过滤掉这些宠物。领养申请闭环是整套系统里逻辑最绕的一部分。用户提交申请时后端要做的检查按顺序来当前宠物状态必须是available当前用户之前没有对这只宠物提交过通过审核的申请创建申请记录同时把宠物状态改为pending前两步是为了防并发两个用户同时看到一只可领养的宠物都点了申请按钮如果后端不检查就产生了两条申请记录。虽然实际教学中并发量很小但这个逻辑该有还得有。管理员审核时接口接收adoption_id和审核结果。通过则把对应宠物改成adopted并驳回这只宠物的其他pending申请拒绝则把宠物改回available。这里面就是一个事务操作我用 Flask-SQLAlchemy 的db.session包了整个过程中间任何一步出错就回滚保证数据库不会出现“宠物已经领养但申请记录还是待审核”这种矛盾状态。# adoption_api.py 简化版 app.route(/api/adoption/int:adoption_id/review, methods[POST]) admin_required def review_adoption(adoption_id): action request.json.get(action) # approved / rejected adoption Adoption.query.get(adoption_id) if not adoption: return jsonify({code: 404, msg: 申请不存在}), 404 pet Pet.query.get(adoption.pet_id) if action approved: adoption.status approved pet.status adopted # 驳回同宠物其他待审核申请 Adoption.query.filter( Adoption.pet_id pet.id, Adoption.status pending, Adoption.id ! adoption.id ).update({status: rejected}) elif action rejected: adoption.status rejected if pet.status pending: pet.status available db.session.commit() return jsonify({code: 0, msg: 处理成功})3.4 寄养预约与订单状态流转寄养的复杂度比领养更高因为它多了一个“时间段”的维度。寄养订单表里必须校验起始日期和结束日期的先后顺序还要避免同一只宠物在同一时间段被预约两次。日期重叠的判断我卡了很久后来理清楚了一个法则两条时间记录重叠等价于“新开始时间 旧结束时间 且 新结束时间 旧开始时间”。比如旧订单是 9 月 1 日到 9 月 7 日新请求如果也是 9 月 1 日至 9 月 7 日重叠如果新请求是 9 月 8 日到 9 月 10 日不重叠。订单状态我设计了五个pending待确认、confirmed已确认、in_progress寄养中、completed已完成、canceled已取消。状态流转的约束规则写在业务层pending可以被管理员确认成confirmed也可以被用户自己取消成canceledconfirmed在开始日期到达后系统自动或由管理员手动改成in_progressin_progress在结束日期到达后改成completed只有pending和confirmed状态允许用户取消为了避免状态想怎么跳就怎么跳我专门写了一个校验函数每次更新状态前检查当前状态到目标状态是否合法。这样虽然代码多几行但防止了接口调用方传一个奇奇怪怪的状态过来搞坏数据。4. 前端 Vue 集成思路4.1 路由规划与页面结构前端我用 Vue 2 加了 vue-router页面不多但路由规划直接映射了业务结构。// router/index.js const routes [ { path: /, name: home, component: () import(../views/HomeView.vue) }, { path: /pets, name: petList, component: () import(../views/PetList.vue) }, { path: /pets/:id, name: petDetail, component: () import(../views/PetDetail.vue) }, { path: /login, name: login, component: () import(../views/LoginView.vue) }, { path: /register, name: register, component: () import(../views/RegisterView.vue) }, { path: /my/adoptions, name: myAdoptions, component: () import(../views/MyAdoptions.vue) }, { path: /my/fosters, name: myFosters, component: () import(../views/MyFosters.vue) }, { path: /admin, name: adminDashboard, component: () import(../views/admin/Dashboard.vue), meta: { role: admin } } ];普通用户能看到的页面有宠物列表、详情、登录注册、我的申请、我的寄养订单。管理员页面单独放在views/admin目录路由上加了meta.role标记。vue-router 的全局守卫里读取 token 解析后的角色如果不是管理员就重定向到首页。这个只是体验层后端接口的权限校验依然不可省略。4.2 Axios 请求封装与接口对接Vue 页面里如果每个页面都直接axios.get(url, { headers })写重复代码会很多而且 token 还要手动塞进头部。我封装了一个request.js实例// api/request.js import axios from axios const request axios.create({ baseURL: process.env.VUE_APP_BASE_URL || /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response response.data, error { if (error.response error.response.status 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(error) } ) export default request响应拦截器统一处理后端返回的结构。后端约定了一个固定的返回格式{ code: 0, msg: success, data: {...} }错误时code非 0。前端页面只需要判断response.code 0再取数据不用每个接口单独处理异常结构。4.3 关键页面实现细节宠物列表页是普通用户进系统的第一站我用卡片网格展示宠物。每个卡片包含宠物照片、名字、品种、年龄和当前状态。状态这个字段需要映射成中文展示available显示“可领养”pending显示“审核中”adopted显示“已领养”。这个映射我放在前端的公共utils/statusMap.js里避免页面组件里到处写重复的三元判断。宠物详情页比较有意思的地方是“申请领养”按钮的可点击条件。按钮是否可用要同时满足三个条件宠物状态是可领养、用户已经登录、用户不是管理员。用户未登录时按钮旁边显示“登录后即可申请”点击后跳转登录页面管理员登录时根本不显示申请按钮因为管理员不会申请领养自己的平台宠物。提交领养申请的表单里我留了一个必填的备注框引导用户填写喂养经验、居住环境等信息。这个设计是学真实领养平台的目的是辅助后台管理员做初步筛选也让整个申请流程更像真事。5. 联调、部署与踩坑记录5.1 跨域问题的三种解法前后端分离开发中遇到最多的第一个坑就是跨域。前端跑在 127.0.0.1:8080后端跑在 127.0.0.1:5000浏览器默认会拦截前后端不同端口之间的 AJAX 请求。最简单的解法是安装 Flask-Cors 扩展初始化时放开所有跨域限制。这种方式适合本地开发但不适合上线。上线后接口如果对任意域开放等于把接口暴露给第三方网页随意外调用存在被刷接口的风险。第二种解法前端开发环境用 devServer proxy。Vue CLI 项目在vue.config.js里写module.exports { devServer: { proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } } }这样前端请求/api/pet/list开发服务器会帮它转发给 5000 端口的后端。浏览器里看到的请求地址始终是同源的不会触发跨域限制。第三种解法也就是上线的做法用 Nginx 同时托管前端静态文件和后端反向代理server { listen 80; server_name pet-home.example.com; location / { root /var/www/pet-home/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:5000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }三种方案按开发阶段和上线阶段配合使用跨域问题就彻底不存在了。5.2 图片上传与静态资源处理宠物信息必然要上传图片。最开始我把图片路径直接存在数据库的cover_url字段里图片本身存到后端的uploads目录。开发时一切正常上线后遇到一个问题Nginx 只托管了前端静态文件访问/api走代理但用户上传的图片路径如/uploads/xxx.jpg没有匹配到任何 location直接 404。解决办法是给图片目录单独加一个 locationlocation /uploads/ { alias /var/www/pet-home/uploads/; }同时提醒一个细节后端返回的图片地址不要存成完整的http://127.0.0.1:5000/uploads/xxx.jpg因为服务器的 IP 或域名一变前端就全都崩了。正确做法是只存相对的资源路径uploads/xxx.jpg前端拼接域名显示图片。我这个项目里统一用了相对路径换环境部署时省去一大半麻烦。5.3 部署时常见问题部署到 Linux 服务器时有几个坑比较隐蔽一一记录。第一个是数据库迁移。开发时用的 SQLite文件数据库直接复制数据库文件到服务器没问题。但切到 MySQL 之后需要重新建库、重新配置连接字符串。如果模型改动频繁建议用 Flask-Migrate 管理数据库迁移省得每次手动删表重建。第二个是 Python 依赖和版本兼容。requirements.txt里最好锁定版本号不锁定的话过几个月在新环境装依赖可能装出一个不兼容的新版本。比如 Flask-SQLAlchemy 2.x 和 3.x 的某些 API 变化会造成代码报错。第三是服务进程存活。上线我用的 gunicorn 做 WSGI 服务但 gunicorn 进程如果挂了就不会自动起来。所以用 systemd 写了一个守护服务进程崩溃后自动重启。这个方法很老套但确实管用比定期人工检查靠谱得多。6. 项目复盘与可扩展方向6.1 维护心得宠物之家这个系统代码量不算大后端大概 2000 多行前端 3000 多行但我维护了很长一段时间中间没觉得头疼主要归功于一开始守住了几条原则。第一所有接口都按业务模块分组用 Blueprint 划分不往app.py里堆。第二API 返回格式统一成{ code, msg, data }前端拦截器统一处理。第三状态字段都用字符串枚举表示不藏数字魔法值。这三条看着是基本功但很多中途烂掉的项目都是在这上面翻的车。6.2 后续升级建议如果这个系统还要继续往下走我会优先补三块线上支付对接、消息通知、数据统计分析。寄养订单现在只是算费用不收款接个标准支付接口可以真实跑通商业闭环。消息通知方面领养申请审核通过后可以用简单的站内信或者推送小程序通知用户不用短信成本低。数据统计则适合给管理员后台加一个首页仪表盘展示宠物总数、领养转化率、寄养订单量直接查几张表做聚合就行。另外可以将前端 Vue 2 升级到 Vue 3组合式 API 写业务逻辑更顺手状态管理也能用更轻量的方式。后端可以逐步拆出服务层把业务逻辑从 API 视图函数里挪出去逻辑更清晰测试也好写。回看整个实现过程最深的体会是“别急着写代码”。一开始我拿到题目就上手建表结果做了两天推翻重来。后来老老实实把业务流程图和数据流图画清楚再动手写效率反而更高。中间还踩了不少环境、部署相关的坑我把每一条都记录在项目文档里后来别人接手也能少走弯路。如果你正在做类似领养寄养的系统建议直接拿这张表去对照一下自己的功能是否齐全模块必备功能加分功能用户注册、登录、角色区分找回密码、手机号绑定宠物发布、列表、详情、状态管理多图展示、筛选搜索领养申请、审核、状态流转申请记录回溯、免疫信息档案寄养下单、日期校验、费用计算支付、日历可视化、护理日志后台宠物管理、订单管理、用户管理数据统计、操作日志框架和语言只是工具核心还是把业务流程想透。希望这篇分享能给正在做类似系统的你一些启发。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表