基于Python与Vue的校园流浪动物领养募捐系统设计与实现 晚上十一点从实验室出来那窝流浪猫又蹲在垃圾桶边上等投喂。橘猫的右后腿好像瘸了想送去医院又怕耽误项目答辩更担心治完没人管、猫还是回到原处流浪。这种两难的心情校园里几乎每个路过的人都经历过——想帮但不知道帮到哪一只捐了钱看不到去向想领养又不知道流程该怎么走。跟社团几位同学聊了几轮最后决定做一套校园流浪动物领养募捐系统用 Python 做后端、Vue 做前端把领养申请和募捐公开两条线完整落地。这篇文章把整套设计与实现拆开讲包括需求分析、数据建模、前后端实现、部署细节和我实际踩过的坑给正在做同类课程设计、毕业设计或者想给学校流浪动物组织搭信息系统的同学当参考。这个项目听起来像普通的管理后台但真正做起来就会发现它跟图书管理系统、学生管理系统完全不一样。它有三个角色在同时使用系统三套流程互相穿插稍不留神就把权限和状态搅成一锅粥。1. 为什么校园流浪动物系统不能照搬通用管理后台1.1 这个项目真正的难点不在代码量我见过不少同学拿到这类题目之后先打开一个 Vue 后台管理模板画几个增删改查页面再对着宠物表做一套 CRUD 接口就认为完成了。这其实只做了外壳没有做内核。流浪动物领养系统表面上要管的是一只猫的信息实际上要管的是信任、流程和善意的闭环。拿最实际的场景说学生A在系统里看到一只流浪猫觉得投缘申请领养。管理员接受了申请此猫状态变成已被领养。半个月后如果学生A因为宿舍不让养又偷偷把猫放回校园系统能记录这个事实吗下一次有人再申请领养管理员能提前看到这个人之前有过弃养记录吗这就是状态机和用户行为记录的用武之地不是单纯改一个is_adopted字段就能解决的。再看看募捐侧。社团在系统里发了一个为橘猫做腿部手术筹款的项目目标金额 2000 元。捐款人B捐了 50 元系统需要同时做两件事给项目累计金额加 50同时生成一条属于B的捐款凭证。如果其中任何一步失败账就对不上。这种“双写一致性”问题在真实的募捐场景里非常敏感。1.2 标题里藏着的三类用户冲突系统涉及三类核心用户需求是互相拉扯的普通学生 / 领养申请者希望页面好看、操作简单、能快速看到动物的照片和健康信息申请后随时能查进度。社团管理员 / 志愿者希望后台能高效处理申请、批量更新动物状态、导出募捐明细同时权限分明、操作可追溯。关注者 / 捐赠人希望看到每一笔钱用在哪里、募捐进度实时更新、领养结果公开透明。这个冲突从一开始就决定了你不能只在后端做一套通用接口前面套一个通用模板。必须给前台访客设计一个信息透明、低门槛的展示端给管理员设计一个高密度、可审核的管理端两边逻辑分层但共享同一套业务数据。2. 技术栈选型为什么是 Vue 配 Python 而不是别的组合2.1 后端框架取舍Flask 还是 Django收到这类题目很多人会纠结 Flask 和 Django。我的建议是如果项目规定基于 Python 实现优先考虑 Flask SQLAlchemy 组合除非你确实需要 Django Admin 来快速搭建运营后台。为什么这个项目本质是 API 服务。Vue 负责渲染页面后端只需要提供 JSON 数据。Flask 足够轻、够直观一个文件能跑通中间件和扩展自己挑代码读起来像讲故事。Django 自带 Admin、ORM、迁移、认证确实一套很全但对于纯前后端分离项目很多内置能力用不上模型和序列化反而要额外处理学习曲线对初学者并不友好。当然这绝不是一刀切。如果你对 Django 更熟完全可以用 Django REST Framework 写。核心业务逻辑的难度不在框架本身而在你对流程状态和权限边界的理解。我这次选用 Flask理由很实在代码量少、调试直观、团队里另外几个同学能快速看懂上服务器部署也不用为静态文件另做一套处理。2.2 前端搭配Vue 3 Element Plus 的适配度前端选 Vue 3 有几个很现实的考量Vue 3 的 Composition API 在处理复杂表单和复用逻辑时很舒服比如领养申请信息、动物筛选条件、分页状态。把这些抽成useAnimalList、useAdoptionApplication之类的组合式函数页面代码一下子变得干净。Element Plus 是一套现成的组件库表格、表单、弹窗、上传组件都有能省掉大量样式时间。校园系统不需要炫酷的前端动效稳定、清晰、好用才是第一位的。和后端 API 结合的生态成熟axios加拦截器、vue-router加路由守卫、pinia存用户状态这一套组合在同类项目里几乎算标准答案遇到问题社区里都有现成方案。2.3 数据库选型与部署成本数据上用 MySQL理由不多说就是稳、通用、任何服务器都有运维经验。ORM 用 SQLAlchemy迁移工具用 Alembic。开发阶段用 SQLite 跑单元测试部署时切到 MySQL只需要改一个连接地址ORM 层基本上不用动。开发环境装出来大约是组件选择说明后端框架Flask 2.3轻量、易扩展ORMSQLAlchemy 2.0声明式模型方便迁移认证方案JWT无状态协议适合前后端分离数据库MySQL 8.0生产环境稳定前端框架Vue 3 Vite构建快开发体验好UI 组件Element Plus后台管理效率高状态管理Pinia风格和 Vue 3 契合选完这套组合我心里的一块石头落了地这个项目既不会因为技术栈太冷门而查不到资料也不会因为框架太笨重而拖慢进度。3. 需求拆解从领养和募捐两个动作倒推数据表3.1 领养流程的状态机设计领养不是一锤子买卖。我在需求阶段画过一版流程发现从看到动物到真正带回家至少要经过六个状态待审核 → 待沟通 → 待交接 → 已领养 → 已被退回 → 已结束待沟通是很多第一次做的人最容易丢的环节。领养人提交申请后管理员需要线下沟通问宿舍条件、问家人意见、问饲养经验聊完才决定是否批准。如果系统里直接表单一提交就变成已领养那管理员后续想拒绝都没有中间状态可以使用。对应的动物状态也要跟着变待领养正常展示在首页已申请该动物有人提交申请但未完成审核这里要注意不一定锁定不能看只是减少重复申请已领养完成交接属于已离校暂不开放动物在治疗中、性格观察期或暂时无人照看每个状态变更都要记录adoption_logs字段包括“谁操作的、从什么状态变到什么状态、备注原因”。这不仅是审计需求也是给普通用户看的进度可见基础。没有日志管理员说审批通过了用户凭什么相信有了日志每次变更都带时间戳和操作人信任感就建立起来了。3.2 募捐模块的边界做模拟支付还是对接真实渠道这是合规问题也是权益问题。校园社团一般不具备公开募捐资质如果我在系统里直接对接真实支付通道会碰红线。所以我当时和团队定的方案是系统内置模拟捐赠功能生成虚拟订单并记录捐赠人信息不实际收款为将来接入合规渠道预留好接口。设计捐赠表的时候就要做好扩展class DonationOrder(Base): __tablename__ donation_order id Column(Integer, primary_keyTrue) project_id Column(Integer, ForeignKey(donation_project.id), nullableFalse) user_id Column(Integer, ForeignKey(user.id), nullableFalse) amount Column(Numeric(10, 2), nullableFalse) status Column(String(20), defaultpending) # pending / paid / refunded channel Column(String(20), defaultmock) transaction_no Column(String(64), uniqueTrue, indexTrue) created_at Column(DateTime, defaultdatetime.utcnow)status虽然叫支付状态但在模拟场景下就是一个流程标记transaction_no生成唯一单号为将来真的接第三方支付做准备。这个设计让项目在演示时完整可用又不会引火烧身。3.3 用户权限的漏斗式设计系统的用户角色我设置了三个admin、volunteer、user。admin 拥有全部权限volunteer 可以处理领养申请、编辑动物资料、管理自己的募捐项目普通 user 只能浏览、申请领养、查看捐赠记录。但角色不等于权限。我刻意避免在接口里只依赖is_admin这种二值判断。而是用最小权限原则写了两个装饰器def role_required(*roles): def decorator(f): wraps(f) def wrapper(*args, **kwargs): current_user get_jwt_identity() user User.query.get(current_user[user_id]) if user.role not in roles: return jsonify({code: 403, msg: 没有权限操作}), 403 return f(*args, **kwargs) return wrapper return decorator然后在具体接口上标清楚哪些角色能调用比如提交领养申请user或更高审核领养申请volunteer或admin创建募捐项目volunteer或admin删除用户仅admin这个漏斗式的设计看似简单实际上避免了很多事故。后面前端就算有人绕过按钮直接调接口权限判断依然在后端兜底。4. 后端实现从零搭一个能真正跑起来的服务端4.1 用户认证与 JWT 踩坑认证方案我选了 JWT原因很简单前后端分离架构下JWT 不需要服务端保存会话信息用户信息直接放进 token客户端每次请求带上Authorization: Bearer token。Flask 生态里现成库是flask-jwt-extended。登录接口的核心逻辑app.post(/api/login) def login(): data request.get_json() user User.query.filter_by(usernamedata.get(username)).first() if user and check_password_hash(user.password_hash, data.get(password)): access_token create_access_token( identity{user_id: user.id, role: user.role}, expires_deltatimedelta(hours12) ) return jsonify({code: 0, data: {access_token: access_token, role: user.role}}) return jsonify({code: 40001, msg: 用户名或密码错误}), 401这里有个容易踩的点identity如果只放user_id每次请求要重新查库才能拿到角色如果直接把整个用户对象塞进 tokentoken 就会很长。最佳实践是把user_id和role两个值放进去既不臃肿又能快速做权限判断。还有密码安全。我曾见过有人直接把明文密码存在数据库里这是万万不行的。用werkzeug.security自带的generate_password_hash和check_password_hash已经足够没必要自己写加密算法。4.2 动物信息管理的上传思路动物信息除了基本字段名称、种类、性别、年龄、毛色、绝育情况、健康状况最核心的是图片。前端用 Element Plus 的el-upload传到后端后端不能直接把文件存进数据库字段但也不能随便往项目根目录一通乱存。我的做法是UPLOAD_FOLDER os.path.join( os.path.dirname(__file__), static/uploads ) app.post(/api/animal/image) jwt_required() role_required(admin, volunteer) def upload_animal_image(): file request.files.get(file) if not file: return jsonify({code: 40000, msg: 未收到文件}), 400 suffix os.path.splitext(file.filename)[1].lower() if suffix not in {.jpg, .jpeg, .png, .webp}: return jsonify({code: 40000, msg: 仅支持图片格式}), 400 filename str(int(time.time() * 1000)) suffix file.save(os.path.join(UPLOAD_FOLDER, filename)) return jsonify({code: 0, data: {url: f/static/uploads/{filename}}}), 201文件名用时间戳加毫秒级随机串避免重名覆盖。上传目录用变量UPLOAD_FOLDER指出来部署时直接改配置不把路径写死。这里我发现一个常见的坑开发时图片能正常显示部署后图片全裂原因多半是静态文件没有交给 Nginx 处理后面会专门讲。4.3 捐赠记录与统计的并发处理募捐进度不能拍脑袋加。每当有人捐赠成功donation_project.raised_amount和donation_order必须保持一致。如果同时有 10 个人都在捐赠两个请求同时读到旧的raised_amount各自加 50最后只加了 50而不是 500。这就是并发更新丢失。SQLAlchemy 层面的解法是用with_for_update()对项目行加锁app.post(/api/donation/project/int:project_id/donate) jwt_required() def donate(project_id): project DonationProject.query.filter_by(idproject_id).with_for_update().first() ... project.raised_amount project.raised_amount amount db.session.add(DonationOrder(...)) db.session.commit()注意把两条写操作放在同一个事务里任何一个失败就回滚。实际演示的时候我用十线程并发各捐 1 元做了一个 10 元募捐测试确认最终金额是 10 元而不是 1 元这个测试很能说明问题。5. 前端实现Vue 项目结构与核心页面拆解5.1 前台展示页面的交互设计前台首页是访客见到系统的第一眼承载了展示当前待领养动物和募捐项目进度两大功能。这里我不建议做太花哨的轮播图因为动物信息是实时变化的今天这只被领养了明天又新救助了一只。做一组卡片流更合适每只动物一张卡片显示图片、名字、健康状况标签、状态徽标下面放查看详情和申请领养两个入口。动物详情页要完整展示档案信息包括最近一次驱虫时间、疫苗记录、性格描述替换掉冷冰冰的列表。细节上有一个值得做的功能把领养进度条展示出来。这里展示的不是募捐进度而是这只动物的领养状态流转时间线让人觉得系统是活的。募捐进度条用 Element Plus 的el-progress配合百分比显示后端返回raised_amount和target_amount两个字段前端除法算百分比。前端不要直接计算再传给后端避免脏数据。5.2 后台管理页面的表格、表单与弹窗管理端我做了四个核心页面动物管理、领养申请处理、募捐项目管理、用户管理。整体用一个侧边栏布局Vue Router 配置嵌套路由两侧菜单跟权限挂钩。动物管理页面的表格列我花了心思。除了基础信息外加了一个来源列记录是同学发现的还是社团救助的。这一列对志愿者来说很有价值因为能看出校园里哪些区域流浪动物比较集中方便安排诱捕绝育工作。申请处理页面是整个后台最重要的部分。管理员点进一条申请能看到申请人的完整资料学号、宿舍楼栋、养宠经验说明、申请的动物信息、申请时间和当前状态。审核操作是三个按钮通过、待沟通、拒绝。每次点击后调后端接口同时更新adoption_logs这样在前台用户端才能看到进度。5.3 与后端对接的请求层封装Vue 侧的请求层我用了一个 axios 实例统一管理import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.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) } )这个封装有两个关键点。第一baseURL设成/api开发时用 Vite 的 proxy 代理到本地 Flask部署时用 Nginx 反向代理到 Gunicorn前端代码一行不用改。第二响应拦截器里统一处理 401token 过期或者未登录就直接送到登录页避免每个页面重复写判断。路由守卫我加了一重校验访问需要权限的页面时先看localStorage里有没有 token 和角色信息没有就跳登录。但这层只是交互优化真正安全永远以后端为准。6. 最容易翻车的几个点实测教训6.1 图片上传的存储路径与环境切换开发时上传的图片放在本机static/uploads一张张都能看。到了部署机上FLASK_APP启动目录跟sys.path不一样__file__相对路径算错图片就被存到了一个完全找不到的地方。我的解决办法是写一个配置函数BASE_DIR os.path.abspath(os.path.dirname(__file__)) UPLOAD_FOLDER os.path.join(BASE_DIR, static, uploads) ALLOWED_EXTENSIONS {png, jpg, jpeg, webp}凡是用到文件路径的地方一律基于BASE_DIR拼不依赖当前工作目录。部署后还要检查static/uploads目录的写权限服务器上没给www-data写权限的话上传接口会直接 500。6.2 跨域配置与请求拦截前后端分离开发时跨域几乎必出。Flask 后端设置Access-Control-Allow-Origin的方式很多但如果你在 Vite 里已经配置了代理开发阶段实际上没有跨域问题。真正的跨域出现在部署后前端在http://xxx.com后端 Gunicorn 在http://xxx.com:5000。如果直接让前端调http://xxx.com:5000/api/...浏览器会拦截跨域请求。我在实测中最省心的方案是不让前端直连后端端口而是用 Nginx 把https://xxx.com/api/反向代理到127.0.0.1:5000location /api/ { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }这样前端代码里所有请求都走同源不存在跨域问题也顺便把后端的端口对外隐藏了。6.3 数据权限不能让普通用户直接调管理员接口有次做内部测试我在浏览器控制台手动执行了一段 fetch访问管理员接口修改了某只动物的状态。这提醒我一个严肃的问题前端隐藏入口只是一层遮羞布后端权限校验才是真正的锁。在 Flask 里每个写操作都要加上role_required装饰器或者校验current_user.role是否在允许范围内。尤其是删除动物和删除用户这类高危操作必须只有admin能调。这不只是安全要求也符合系统要有审计能力的原始需求——出了事能追责。还有一个小坑删除动物时如果有领养申请记录引用它的外键直接删除会报 500。我最后改用软删除给animal表加一个is_deleted字段列表查询默认过滤掉已删除的。这样历史申请单还能看到动物曾经的信息不会因为记录被删而出现孤儿数据。7. 部署与验证让系统真正跑在校园服务器上7.1 服务器环境准备部署环境我用的是最常规的 Ubuntu 服务器。流程分成四步安装 Python 3.10、MySQL、Nginx。创建虚拟目录克隆代码用pip安装依赖。用flask db upgrade执行数据库迁移初始化管理员账号。写一个 Gunicorn systemd 服务文件让 Flask 进程常驻后台。Gunicorn 是 Python 社区最常用的 WSGI 容器直接用gunicorn -w 4 -b 127.0.0.1:5000 app:create_app()-w 4表示开 4 个 worker普通校园项目这边绰绰有余。注意绑定地址必须写127.0.0.1后面交给 Nginx 转发不要直接暴露 5000 端口公网。前端部分在本地服务器上执行npm run build生成的dist目录交给 Nginx 当静态文件用。因为我的接口走/api前缀所以 Nginx 配置里非常顺server { listen 80; server_name _; root /var/www/stray-animal/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:5000; } location /static/ { alias /var/www/stray-animal/server/static/; } }这里核心点在于try_files ... /index.html交给 Vue Router 处理前端路由/api反代给后端/static指到上传图片的目录。三个 location 各司其职前端路由刷新后不会 404图片放大后不会失效。7.2 配置完成后的冒烟测试清单部署完不能只看主页能打开就宣布完工。我列了一张冒烟测试清单逐条过每条都涉及项目核心功能测试项预期行为注册新用户提示成功并跳转登录页登录成功后访问领养页面能正常获取动物列表提交领养申请申请记录进入后台动物状态变为已申请管理员审核通过用户端看到状态变为待交接用户取消申请动物状态恢复为待领养普通用户直接请求管理员接口收到 403创建募捐项目并模拟捐赠项目进度正确累加并发提交两笔捐赠金额不丢失上传图片后访问静态路径图片正常显示这九条过完系统基本具备可演示、可提交、可上线的状态。我实测中卡得最久的是第五项用户取消申请后动物状态要能从已申请恢复成待领养。最初我没有做这个反向状态流转管理员只能手动去改这明显不符合实际需求后来在状态机里补了一条取消申请的合法迁移路径才解决。7.3 验收时容易被问到的几个问题做完项目给导师验收或者给社团负责人演示时对方通常会问几个为什么为什么募捐不直接收钱——答校园社团不具备公开募捐资质系统设计为模拟捐赠并预留支付接口是合规且可扩展的做法。怎么保证领养人不是冲动领养——答申请表单里强制填写养宠经验、宿舍情况、紧急联系人字段管理员需要线下沟通后才批准系统内记录完整审核日志。数据多久能备份一次——答MySQL 每天定时备份同时项目核心数据动物档案、领养记录、捐赠明细通过 Flask 管理命令可导出 Excel方便线下归档。这几个回答最好事先在文档里写好因为它们是系统设计深度的体现甚至比代码本身更能说明你理解了需求。最后说几句实际体会整套系统从需求梳理到部署上线前后改了三轮。第一轮纠结技术栈第二轮纠结状态机第三轮才发现最花时间的不是代码而是把流程理顺。校园流浪动物这件事真正做起来涉及的细节远超预期一只猫的救助记录可能有目击——诱捕——体检——驱虫——治疗——静养——待领养——交接八个阶段中间任何一环断掉都不算完整闭环。我自己最满意的一个功能改动是加退回记录。系统允许管理员在动物被领养后继续跟踪如果领养人因故退回动物会重新进入待领养列表同时保留一段 shelter_previously_adopted 的标签。这样其他申请者能看到这只动物有过一次退回经历对它的性格和饲养难度提前有预期避免二次退回的恶性循环。如果你也要做类似的项目我的建议是先围着你身边的真实流浪动物救助组织转一圈看看他们处在哪个环节最痛然后把你看到的那条真实流程原封不动搬进系统。哪怕这个系统最终只是能在校园里帮十几个人完成领养审批它的价值也远远大于一个功能完整的演示Demo。技术永远是为具体的生活服务的这也是我后来每次复盘这个项目时最深的体会。