淘过气的领养系统踩过技术选的坑宠物领养管理系统与志愿者募捐双模块实战笔记做这类系统的同行都知道宠物领养管理系统和志愿者募捐系统这类题目看起来就是个普通的后台管理项目真正动手才会发现里面的角色关系、业务状态流转、还有募捐资金流那一块都是需要提前想清楚的硬骨头。最近我把这套东西完整落地了一遍前端用Vue后端在Flask和Django之间做了取舍IDE全程PyCharm过程中踩了不少值得记录的坑今天把它们整理出来。这套系统适合正在做课程设计、毕业设计或者想练手全栈项目的人参考尤其是那种“领养募捐”双核心、还带志愿者管理的组合型系统思路和代码细节都能直接复用。我先把整体的设计思路、框架选型逻辑、核心功能拆解、实操过程还有问题排查依次讲清楚尽量让看完的人能自己动手搭一套出来。1. 项目整体设计与思路拆解1.1 先看懂这个项目的真实需求宠物领养管理系统核心不是“展示宠物照片”那么简单。它背后是一套完整的业务闭环宠物从哪里来通常是救助站、个人送养、管理员后台录入三种来源。宠物处于什么状态待领养、已被申请、已领养、暂时离世或找回等。领养人怎么申请填写领养申请提交个人资料、居住环境、养宠经验。谁来审核管理员或志愿者初审、回访、终审。领养之后如何跟进回访记录、疫苗提醒、绝育记录等。志愿者募捐系统则是另一条业务线它要处理的不是简单的“有人捐了钱”而是募捐项目的发布与管理谁发起的、目标金额是多少、截止什么时间。捐赠流程用户在线捐赠生成捐赠记录。资金透明化每一笔募捐的累计金额、支出明细、剩余金额需要可追踪。志愿者管理志愿者注册、审核、服务时长记录、参与活动报名。这两条业务线之间还存在交叉关系比如领养审核可以由志愿者参与募捐活动需要志愿者协助执行。所以权限系统不能简单做成“管理员/普通用户”两级至少要有“超级管理员、内容管理员、志愿者、普通用户”四类角色。1.2 技术选型里的关键取舍逻辑这个项目标题里同时出现了 Flast 和 Django这是典型的两难场景。实际上这两个框架在真实项目里很少混着用标题把两个关键词都写上通常说明需求方一开始没想清楚用哪个或者是照着某个方向先调研了一遍。我的做法是主体业务用 Flask 实现Django 的设计思想做参考但不再引入第二套框架。原因有三Flask 轻量路由和视图逻辑清晰对于这类中等复杂度的业务系统代码量可控调试方便。Django 的 ORM、Admin 后台很强大但项目本身并不需要那么重的内置功能硬套 Django 反而会增加学习成本和部署体积。前端用了 Vue 之后前后端完全分离后端只需要提供 API 接口Flask 配合蓝图Blueprint就能组织得很好。提示如果你的项目是毕业设计标题已经定了“基于Django”那也别慌把下面的业务逻辑原样搬过去只是路由写法和ORM语法换一下工作量大概增加20%不会伤筋动骨。1.3 为什么前端选 Vue前后端分离架构下Vue 是目前入门最快、生态最完善的选择之一。对于这套系统前端不需要太重几个核心页面就够了前台首页宠物列表募捐列表用户登录注册页宠物详情页与领养申请页募捐详情页与捐赠支付页用户中心我的申请、我的捐赠、我的志愿者申请后台管理页宠物管理、领养审核、募捐管理、志愿者审核用 Vue Router 管路由Vuex 或 Pinia 管登录态和用户信息Axios 统一请求后端接口。重点是不需要引入太多UI框架自己用 Bootstrap 或 Element 写样式完全够用避免过度工程化。2. 核心细节拆解与实操要点2.1 数据库设计一张关系图决定系统上限这套系统最核心的资产是数据表结构设计是否合理直接决定了后面开发的顺畅程度。我按模块拆成六组核心表用户与权限userid、username、password_hash、roleadmin/volunteer/user、real_name、phone、avatar、create_timevolunteer_applyid、user_id、statuspending/approved/rejected、reason、audit_time宠物管理petid、name、speciescat/dog/other、breed、gender、age_month、health_status、vaccine_status、sterilized、photo_url、description、statusavailable/applying/adopted/other、shelter_id救助站来源、create_timeadoption_applicationid、pet_id、user_id、apply_reason、has_experience、house_type、is_rented租房与否则需要房东同意、statuspending/first_review/approved/rejected/completed、apply_time、audit_remark募捐管理donation_projectid、title、description、cover_url、target_amount、raised_amount、statusongoing/ended、start_time、end_time、organizer_iddonation_recordid、project_id、user_id、amount、pay_method、statuspaid/unpaid/refunded、donate_time、message支出管理expense_recordid、project_id、item_name、amount、expense_type、operator_id、remark、happen_time活动与回访volunteer_activityid、title、description、location、start_time、end_time、need_peopleactivity_signupid、activity_id、user_id、status、signup_timefollow_up_recordid、adoption_id、user_id、content、next_follow_date、create_time这张设计的核心逻辑是把“领养”和“募捐”作为两条相互独立又通过“志愿者活动”和“回访记录”产生关联的业务线。比如志愿者既可以去审核领养申请也可以报名参加募捐线下活动这些行为都通过中间表跟用户关联。2.2 状态流转设计是这类系统的灵魂宠物领养管理系统的难点不在增删改查而在“状态”的变化。一只宠物从录入到被领养至少要经历available可领养applying被申请锁定状态避免多人同时申请adopted已领养completed完成回访归档用户申请领养的流程也对应不同状态pending提交申请等待初审first_review志愿者初审通过进入复审approved管理员最终通过可以线下接宠rejected未通过completed领养完成我在设计接口时遵循一个原则状态只能向后流转不能回退除非管理员手动操作。比如 pending 的申请不能直接改成 approved必须先走 first_review。这样做的目的是防止业务逻辑被绕过前端页面再怎么调接口也跳不过审核环节。2.3 募捐资金流设计钱的透明度不能糊弄募捐模块如果只做“用户捐钱、后台看总数”就太单薄了实际做的时候一定要把“资金生命周期”做出来这既是对用户负责也是让项目答辩时拿得出手的关键点。资金生命周期分三段募集期用户查看募捐项目贡献金额系统累计金额。使用期管理员或志愿者录入支出记录比如购买猫粮、支付绝育手术费用、驱虫药采购等。结项期达到目标金额或到期停止募捐公示总金额和支出总额剩余资金转入平台备用金或用于其他项目。费用记录要按项目id关联不能只记一个总支出。否则用户看到募捐明细时完全无法追溯这笔钱花在了哪里那系统就失去了“透明”的意义。注意实际开发中支付推荐接入第三方的沙箱测试环境不要自己去处理资金通道也不要真的收款。项目展示环节用模拟支付回调就能满足需求记录支付状态时可提供“模拟成功”按钮标注清楚仅用于演示。3. 实操过程与核心功能模块实现3.1 环境搭建PyCharm 下 Flask 后端初始化PyCharm 里新建 Flask 项目我习惯把项目结构调整成下面的样子pet_adoption/ ├── app.py # 应用工厂 ├── config.py # 配置管理 ├── requirements.txt ├── extensions.py # db, ma, jwt 等扩展实例 ├── models/ # SQLAlchemy 模型 │ ├── __init__.py │ ├── user.py │ ├── pet.py │ ├── adoption.py │ ├── donation.py │ └── volunteer.py ├── api/ # 蓝图接口 │ ├── __init__.py │ ├── auth.py │ ├── pets.py │ ├── adoptions.py │ ├── donations.py │ └── volunteers.py ├── utils/ # 工具函数 │ ├── auth_decorator.py │ ├── response.py │ └── upload.py └── migrations/ # 数据库迁移关于数据库选型本地开发建议用 SQLite部署演示再用 MySQL。SQLite 零配置拿过来就能跑等系统稳定后再换 MySQL 只需要改一处连接字符串和重新迁移数据成本很低。开发环境推荐 Python 3.10 及以上Flask 2.3 以上。依赖安装pip install flask flask-sqlalchemy flask-migrate flask-cors flask-jwt-extended marshmallow pip install pymysql # 如果用MySQL3.2 后端核心接口设计与代码实现3.2.1 JWT登录与权限校验用户体系我用 JWT 做无状态认证比 Session 更适合前后端分离。登录接口返回 access_token前端存到 localStorage请求时放在请求头里。from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity auth_bp.route(/login, methods[POST]) def login(): data request.get_json() username data.get(username) password data.get(password) if not username or not password: return response_error(用户名和密码不能为空) user User.query.filter_by(usernameusername).first() if not user or not check_password_hash(user.password_hash, password): return response_error(用户名或密码错误) token create_access_token(identitystr(user.id)) return response_success({ token: token, user_info: { id: user.id, username: user.username, role: user.role, avatar: user.avatar } })权限控制写成装饰器方便复用def role_required(*roles): def wrapper(fn): wraps(fn) jwt_required() def decorator(*args, **kwargs): user_id get_jwt_identity() user User.query.get(int(user_id)) if not user: return response_error(用户不存在) if user.role not in roles: return response_error(权限不足), 403 return fn(*args, **kwargs) return decorator return wrapper用法veterans_bp.route(/apply, methods[POST]) role_required(admin, volunteer) def review_adoption(): ...3.2.2 宠物列表与状态过滤宠物列表接口是最常被访问的接口需要考虑筛选和分页。前端传参数 species、status、keyword后端用 SQLAlchemy 条件查询pet_bp.route(/list, methods[GET]) def pet_list(): page request.args.get(page, 1, typeint) per_page request.args.get(per_page, 12, typeint) species request.args.get(species) status request.args.get(status) keyword request.args.get(keyword, ) query Pet.query if species and species ! all: query query.filter_by(speciesspecies) if status and status ! all: query query.filter_by(statusstatus) if keyword: query query.filter(Pet.name.like(f%{keyword}%)) pets query.order_by(Pet.create_time.desc()).paginate( pagepage, per_pageper_page, error_outFalse ) # 序列化后返回实操心得分页建议用 paginate 而不是自己写 offset/limitFlask-SQLAlchemy 的 paginate 自带总页数、总数等元数据前端做分页器直接拿 total 和 pages 就行省掉很多重复工作。3.2.3 领养申请的双重审核这个接口是业务核心。用户提交领养申请时系统需要同时做三件事校验宠物是否可领养status必须是available。将宠物状态置为 applying。创建领养申请记录。这三个必须在一个数据库事务里完成否则会出现用户A申请成功但宠物状态还是available、用户B也能提交申请的并发问题。from sqlalchemy import and_ adoption_bp.route(/apply, methods[POST]) jwt_required() def apply_adoption(): user_id int(get_jwt_identity()) data request.get_json() pet_id data.get(pet_id) pet Pet.query.filter_by(idpet_id).with_for_update().first() if not pet: return response_error(宠物不存在) if pet.status ! available: return response_error(该宠物已被申请或已领养) # 检查是否重复申请 existing AdoptionApplication.query.filter( and_(AdoptionApplication.pet_id pet_id, AdoptionApplication.user_id user_id, AdoptionApplication.status.in_([pending, first_review])) ).first() if existing: return response_error(you已有处理中的申请请勿重复提交) application AdoptionApplication( pet_idpet_id, user_iduser_id, apply_reasondata.get(apply_reason), has_experiencedata.get(has_experience, False), house_typedata.get(house_type, ), is_renteddata.get(is_rented, False) ) pet.status applying db.session.add(application) db.session.commit() return response_success(申请提交成功)提交申请后志愿者端显示待审核列表。志愿者通过初审管理员做最终审核通过后把宠物状态改为adopted。3.2.4 募捐模块的金额更新与事务募捐的核心接口是“用户捐赠成功后需要更新项目的已募捐金额”。如果用户刷新页面发现钱捐了但金额没变那就尴尬了。更好的方式是把更新累计金额放在支付回调里做而不是前端捐赠成功后立即做。因为前端点击“模拟支付成功”只是回调请求万一被重复调用金额会叠加多次。我用的方式是在回调接口里查询该捐赠记录的状态只有状态从 unpaid 转为 paid 时才执行累计金额更新。donation_bp.route(/callback, methods[POST]) def donation_callback(): data request.get_json() record_id data.get(record_id) record DonationRecord.query.filter_by(idrecord_id).with_for_update().first() if not record: return response_error(记录不存在) if record.status paid: # 防止重复回调 return response_success(重复回调已忽略) project DonationProject.query.filter_by(idrecord.project_id).with_for_update().first() record.status paid project.raised_amount record.amount db.session.add_all([record, project]) db.session.commit() return response_success(支付成功)这里用 with_for_update() 加行锁是为了避免两个并发回调同时读到旧的 raised_amount导致金额丢失。3.3 前端 Vue 项目搭建与接口对接3.3.1 项目初始化与跨域问题用 Vue CLI 创建项目后第一件事就是配置 Axios 和跨域代理。开发环境下让 Vue 开发服务器代理 API 请求到 Flask能避开跨域问题。在vue.config.js里配置module.exports { devServer: { port: 8080, proxy: { /api: { target: http://127.0.0.1:5000, changeOrigin: true } } } };后端同时开启 CORSfrom flask_cors import CORS CORS(app, supports_credentialsTrue, resources{r/api/*: {origins: *}})这里有个细节开发环境下用代理就够了部署到生产环境时前端构建出的静态文件由后端统一托管就不存在跨域问题。所以不要过度纠结CORS配置能跑通开发流程再说。3.3.2 登录态管理与路由守卫Vuex 存用户信息和token路由守卫里判断是否登录router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else if (to.meta.requiresAdmin localStorage.getItem(user_role) ! admin) { next(/); } else { next(); } });注意前端的路由守卫只是用户体验层面的限制真正的权限校验必须在后端接口做。前端可以直接改代码绕过路由守卫但后端role_required是绕不过去的。3.3.3 宠物图片上传宠物图片上传是少不了的。我的方案是Flask 后端提供/api/upload接口接收 multipart/form-data 格式的图片保存到服务器static/uploads目录返回可访问的 URL。upload_bp.route(/upload, methods[POST]) jwt_required() def upload_image(): file request.files.get(file) if not file: return response_error(没有文件上传) # 校验类型 if file.filename.split(.)[-1].lower() not in [jpg, jpeg, png, gif, webp]: return response_error(不支持的图片格式) filename f{uuid.uuid4().hex}.{file.filename.split(.)[-1].lower()} upload_path os.path.join(current_app.config[UPLOAD_FOLDER], filename) os.makedirs(current_app.config[UPLOAD_FOLDER], exist_okTrue) file.save(upload_path) url f/static/uploads/{filename} return response_success({url: url})文件名一定要用 uuid 重命名不能用用户上传的原始文件名否则会出现重名覆盖、非法字符等问题这是很多新手会踩的坑。4. 常见问题与排查技巧实录4.1 前端报错 Unexpected tokenin JSON这个报错几乎每个做前后端分离的人都遇到过。原因很简单后端返回的不是 JSON而是 HTML 错误页。打开 F12 看 Network 里该请求的 Response如果返回的是 404 HTML 页面说明前端请求的 URL 跟后端路由不匹配。比如 Vue 代理把/api/pets/list转发了但 Flask 蓝图注册的是/api/pets/list/带斜杠就可能匹配失败。排查步骤确认后端蓝图的 url_prefix 和路由拼接后跟前端请求地址完全一致。直接在后端测试接口返回在浏览器访问该 URL看是否返回 JSON。如果是 POST 请求确认前端是否设置了正确的 Content-Type。4.2 SQLAlchemy 报错Object already attached to session这个是在序列化模型时容易踩的坑。比如在请求中查询出 Pet 对象返回时直接jsonify(pet.__dict__)就会报错或者出现多余字段。我的解决方案是在模型里定义 to_dict 方法class Pet(db.Model): __tablename__ pet ... def to_dict(self): return { id: self.id, name: self.name, species: self.species, breed: self.breed, gender: self.gender, age_month: self.age_month, health_status: self.health_status, vaccine_status: self.vaccine_status, sterilized: self.sterilized, photo_url: self.photo_url, description: self.description, status: self.status, create_time: self.create_time.strftime(%Y-%m-%d %H:%M:%S) }这个方法的优点是可以自由控制输出字段不会把密码哈希、内部字段暴露给前端同时时间字段也统一转了格式避免前端解析时分不一致。4.3 宠物状态更新后列表没刷新前端页面在申请领养成功后宠物详情页会显示“已申请”但返回列表页发现宠物还在“可领养”分类里。这是前端组件状态不同步导致的不是后端问题。解决方案有两个提交申请成功后重新拉取列表接口数据。列表页在 created 钩子里重新调用 API不要只依赖路由参数。我建议用第二种因为即使用户手动刷新页面数据也会正确加载。4.4 Flask 中的login_required和jwt_required混用报错有人在 Flask 项目里用 Flask-Login 的login_required同时用 Flask-JWT-Extended 的jwt_required两者会互相干扰。像这套系统用了 JWT 之后就不要再用 Flask-Login。Flask-Login 的上下文基于 sessionJWT 基于 token混在一起容易出现“登录了还是提示未登录”的问题。原则就是一个项目里只保留一套认证机制。前后端分离项目优先选 JWT。4.5 捐款金额小数点精度问题很多人用 Float 字段存金额这是个隐患。Python 的 Float 是双精度浮点数在累加时会产生精度误差。募捐金额必须用 Numeric(10, 2) 类型class DonationRecord(db.Model): amount db.Column(db.Numeric(10, 2), nullableFalse)这样在 SQLite 里存的是小数MySQL 里存的是 DECIMAL(10, 2)累加结果精确到分不会出现 0.1 0.2 0.30000000000000004 那种问题。4.6 Axios 请求拦截器统一处理 401当 token 过期时后端会返回 401前端每个接口单独处理太麻烦。在 Axios 拦截器里做统一处理是标准的做法axios.interceptors.response.use( response response, error { if (error.response error.response.status 401) { localStorage.removeItem(token); localStorage.removeItem(user_role); window.location.href /login; } return Promise.reject(error); } );这样做的好处是token 过期后自动跳转到登录页用户体验友好不用手动清缓存。4.7 PyCharm 运行 Vue 项目的配置PyCharm 开发这种项目要同时跑两个进程Flask 后端端口5000和 Vue 前端端口8080。很多人在 PyCharm 里只启动 Flask然后访问 8080 发现打不开页面。推荐在 PyCharm 的 Run Configuration 里配置两个运行配置Flask 运行选择 app.py端口默认 5000。npm 运行Command 填写run serve。然后在浏览器里访问http://localhost:8080Vue 开发服务器会把 API 请求代理到 5000 端口。如果觉得两个进程切换麻烦也可以直接把 Vue 的npm run build产物放到 Flask 的 static 目录用 Flask 直接托管前端页面避免同时开两个进程。不过开发调试阶段还是建议分开跑改动代码后热更新时间更短。5. 实操总结与扩展思路我整理一个核心模块与业务的映射参考表方便后续开发时对照功能模块前端页面后端接口核心数据表权限要求用户登录注册/login, /registerPOST /api/auth/login, POST /api/auth/registeruser公开/登录宠物浏览/pets, /pet/:idGET /api/pets/list, GET /api/pets/detailpet公开领养申请/applyPOST /api/adoptions/applyadoption_application登录用户领养审核/admin/auditGET /api/adoptions/pending, PUT /api/adoptions/reviewadoption_applicationadmin/volunteer募捐项目列表/donateGET /api/donations/listdonation_project公开在线捐赠/donate/:idPOST /api/donations/donatedonation_record登录用户募捐支出管理/admin/expensePOST /api/donations/expenseexpense_recordadmin志愿者活动/activitiesGET /api/volunteers/activitiesvolunteer_activity公开/登录回访记录/admin/followupPOST /api/followupsfollow_up_recordadmin/volunteer这套系统后续可以继续扩展的方向我个人认为最有价值的是两个一是增加消息通知机制比如用户提交领养申请后审核结果出来时给用户发短信或邮件通知。这个用 Celery Redis 异步任务队列可以实现不至于让主线程阻塞。二是引入更完整的业务报表模块比如领养成功率按宠物品种统计、募捐资金支出分类占比图、志愿者活跃度排行等用 ECharts 画图表展示。这两个方向无论是放到课程设计加分答辩还是上线做真实运营都是比较实用的功能。最后分享一个我在实际开发中的体会做这类管理系统编码的难度其实不高真正花时间的部分是业务流程的梳理和状态机的设计。先把领养、募捐各自的流程图画明白把每个状态之间的流转关系列清楚后端接口写起来就是水到渠成的事。如果一开始就急着写代码后面改起状态和字段来返工的成本远比想象的高。希望这篇笔记能帮准备做类似系统的人少走几个弯路尤其是那些一开始就被 Flask 和 Django 的选型搞纠结的朋友先动手把业务流程跑通比什么都强。