
在做这套宠物医院管理系统之前我翻遍了网上能找到的开源项目发现大多数所谓“管理系统”其实只是个半成品后端堆了一堆接口前端随便套了个模板数据表设计更是漏洞百出。真正能落地到一家小型宠物医院日常运转的几乎没有。所以这次我干脆从零开始把“Python Vue Django或Flask PyCharm”这套组合完整走了一遍从环境搭建到需求拆解从数据库设计到前后端联调最后把全过程整理成这篇实战记录。你看到的并不是一份“可运行的源码”搬运而是一份能直接照着做的项目方案——适合刚学完 Python 和 Django 基础、想独立撑起一个全栈项目的人也适合毕业设计选了这个题目、不想糊弄过关的同学。这个系统要解决的痛点很具体宠物医院里纸质病历容易丢、疫苗提醒靠人脑记、挂号排队靠手写、回访记录全是空白。把这些线下流程搬到线上就构成了系统的基本盘。1. 项目整体设计与需求拆解1.1 先理清“宠物医院管理系统”到底要管什么很多人一上来就写代码写到一半发现功能越加越多、数据表改来改去最后干脆烂尾。我建议先把手边的纸质流程走一遍再决定做哪些模块。一家典型的社区型宠物医院日常工作大致是这样前台登记宠物主人和宠物信息、安排挂号、医生接诊并记录病历、开处方、收银台结算、定期提醒主人带宠物接种疫苗。拆成系统功能就是用户管理区分管理员、前台、医生三种角色不同角色看到不同的菜单。宠物主人管理姓名、电话、地址、微信、备注。宠物档案管理品种、年龄、性别、体重、绝育状态、疫苗记录、既往病史。挂号预约按日期、医生查询空闲时段前台可代约主人也可电话约。医生工作台查看当日预约、填写诊断结果、开用药处方、录入医嘱。收费管理挂号费、诊疗费、药费、住院费生成收费明细。疫苗提醒根据疫苗到期时间自动生成待提醒列表前台可电话回访。这些功能听起来不多但每个模块都涉及一对多关系。比如一位主人可以带三只宠物来看病一只宠物可以多次挂号、多次就诊一次就诊可以开多条处方明细。理清这些关系再建表后面会省很多事。1.2 角色权限与页面跳转的对应关系这个系统我不建议做细粒度的权限控制那样会把自己累死。用 Django 自带的 User 模型加一个 user_type 字段用户登录后根据类型跳转到不同首页即可。管理员能看到全部菜单和统计报表前台主要操作主人档案、宠物档案、挂号预约医生只能看到自己的工作台和病历记录。Vue 前端这边我通过 Vue Router 的全局守卫做了一次拦截未登录跳转登录页登录后根据 user_type 动态生成菜单路由表里配置好 meta.roles 字段不匹配的角色直接重定向到 403 页面。这样权限逻辑集中在前端路由层后端接口倒不必每个都做太复杂的校验但核心写操作我会在 Django 里再用装饰器做二次校验。1.3 为什么用“前后端分离”而不是传统模板渲染传统 Django Jinja2 模板的方案完全能做这个系统而且开发速度更快。但我仍然选择 Vue REST API 的分离架构理由有二。第一管理后台需要较多的交互细节比如切换宠物、动态筛选、表格内编辑用 Vue 组件的体验远好过服务端模板加 jQuery 的玩法。第二后续如果想扩展一个微信小程序端或者移动端前端分离后后端 API 可以直接复用。代价是开发链路变长需要同时维护两套代码还要处理跨域问题。这个取舍我在项目规划阶段就已经想清楚所以建议你至少在动手前想清楚自己是“想快速做出一个能验收的系统”还是“想真正掌握全栈开发流程”——前者用 Jinja2 更快后者就老老实实前后端分离。2. 技术选型Django、Flask 与 Vue 的搭配逻辑2.1 Django 和 Flask这次到底该用哪个项目标题里把 Django 和 Flask 并列这让很多人纠结。我的经验是别纠结直接用 Django。理由可以从这个项目的具体需求出发来谈。首先这个系统里有大量数据库操作宠物档案、挂号、病历、收费单之间存在明确的外键关联。Django 自带 ORM 和迁移机制makemigrations 一条命令就能同步表结构而 Flask 默认只有 SQLAlchemy 的第三方集成模型定义和迁移工具需要自己组合。其次系统需要后台管理功能Django Admin 在开发阶段可以零成本生成一个数据维护后台临时改点数据、看看表记录非常方便。第三登录认证这部分跑不掉Django 内置了完整的用户体系、会话管理和密码哈希直接用就好。那 Flask 适合什么场景如果你只是做一个纯 API 网关、微服务组件或者前端页面极其简单数据模型只有三五张表那 Flask 确实更轻。这个宠物医院系统的数据表我建了 8 张按 Flask 的方式做也不是不行但你在表关系、序列化、权限上写的手动代码会多出几倍。所以结论是首选 DjangoFlask 可以作为备份方案留在技术文档里用于说明“如果项目继续拆分成微服务哪些模块可以独立用 Flask 重写”。2.2 Vue 前端框架的价值与版本选择Vue 在这个系统里承担的是 SPA 单页应用。核心优势有三块一是组件化宠物卡片、预约表格、病历时间线这些 UI 块可以抽出成独立组件来回复用二是响应式宠物档案切换时下方的病历记录、疫苗记录自动联动更新不需要手动操作 DOM三是生态成熟Element Plus 提供现成的表格、表单、日期选择器开发后台类界面效率非常高。版本上用 Vue 3 Vite 是当前的主流Vue Router 4 配合 Pinia 做状态管理。Vue 2 已经进入维护末期新项目不建议再选。另外Vue 官方推荐的构建工具 Vite 比 Webpack 启动快很多调试体验也更好。唯一要注意的是 Node.js 版本必须 16 以上否则 Vite 跑不起来。2.3 PyCharm 在项目中的实际定位PyCharm 在这个项目里不只是写代码的编辑器它承担了三件事。第一Python 解释器和虚拟环境的管理。PyCharm 可以在项目里直接创建 venv、指定 Python 解释器版本Django 项目还能一键跑 manage.py 命令。第二前后端联调。专业版的 PyCharm 可以直接识别 Vue 文件语法还可以配置多个启动项——一个启动 Django 后端一个启动 Vite 前端用端口区分不用来回切终端。第三调试功能。Django 视图里的断点调试非常直观请求进来变量值逐行看排查问题效率高。社区版虽然没有 Vue 语法支持装个 Vue.js 插件也能应付大部分场景。3. 数据库设计与 Django 模型建模3.1 核心表结构与实体关系我建了 8 张核心数据表分别是用户表、宠物主人表、宠物表、医生信息表、预约表、病历表、处方明细表、收费表。它们之间的关系可以归纳为一个用户user对应一个宠物主人owner或医生doctor用 user_type 区分。一个宠物主人可以养多只宠物宠物表里用 owner_id 外键关联。一个宠物可以多次预约就诊预约表关联宠物和医生。一次就诊对应一份病历一份病历下挂多条处方明细。一次就诊或预约可以产生一条或多条收费记录。这里最容易犯的错误是预约表和病历表混为一谈。实际上预约只是占了医生的一个时间框病人爽约了预约记录还在而病历是实际上发生的诊疗活动。一张预约单可能对应零份或一份病历。所以两张表要分开病历表里用 appointment_id 做外键允许为空。3.2 Django 模型代码示例我贴一段核心的 models.py 代码重点看外键和字段约束from django.db import models from django.contrib.auth.models import AbstractUser class User(AbstractUser): USER_TYPE_CHOICES ( (admin, 管理员), (front, 前台), (doctor, 医生), ) user_type models.CharField(max_length20, choicesUSER_TYPE_CHOICES, defaultfront) phone models.CharField(max_length11, blankTrue) class PetOwner(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, nullTrue, blankTrue) name models.CharField(max_length50) phone models.CharField(max_length20) address models.CharField(max_length200, blankTrue) remark models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Pet(models.Model): owner models.ForeignKey(PetOwner, on_deletemodels.CASCADE, related_namepets) name models.CharField(max_length50) species models.CharField(max_length20) breed models.CharField(max_length50, blankTrue) gender models.CharField(max_length10, choices((M,公),(F,母),(U,未知))) birthday models.DateField(nullTrue, blankTrue) weight models.DecimalField(max_digits5, decimal_places2, nullTrue, blankTrue) neutered models.BooleanField(defaultFalse) created_at models.DateTimeField(auto_now_addTrue) class Doctor(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE) name models.CharField(max_length50) title models.CharField(max_length50, blankTrue) specialty models.CharField(max_length100, blankTrue) available models.BooleanField(defaultTrue)这里有个细节值得说PetOwner 和 Doctor 都用了 OneToOneField 关联 User而不是直接把姓名电话字段塞进 User 表。原因是 Django 的 AbstractUser 扩展模型虽然方便但塞太多业务字段会让用户表变得臃肿而且宠物主人和医生在业务属性上差异很大。拆开的好处是后续扩展医生排班表、宠物主人会员积分时不需要动用户表结构。3.3 预约、病历与收费表设计要点预约表需要重点考虑时间冲突问题。我的做法是存日期和时段时段精确到小时并加一个 unique_together 约束class Appointment(models.Model): pet models.ForeignKey(Pet, on_deletemodels.CASCADE, related_nameappointments) doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE, related_nameappointments) date models.DateField() period models.CharField(max_length20) STATUS_CHOICES ((pending,待就诊),(done,已完成),(canceled,已取消)) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending) created_at models.DateTimeField(auto_now_addTrue) class Meta: unique_together (doctor, date, period)病历表关联预约和宠物处方表再关联病历class MedicalRecord(models.Model): appointment models.ForeignKey(Appointment, on_deletemodels.CASCADE) pet models.ForeignKey(Pet, on_deletemodels.CASCADE) doctor models.ForeignKey(Doctor, on_deletemodels.CASCADE) diagnosis models.TextField() advice models.TextField(blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Prescription(models.Model): record models.ForeignKey(MedicalRecord, on_deletemodels.CASCADE, related_nameprescriptions) drug_name models.CharField(max_length100) dosage models.CharField(max_length100) days models.IntegerField(default1) amount models.DecimalField(max_digits10, decimal_places2)为什么要通过 MedicalRecord 关联 Pet 和 Doctor而不是直接通过 Appointment因为一次预约虽然一般只对应一次诊疗但是万一出现转诊、复诊合并等特殊情况病历的归属必须清晰。病历是医疗行为的记录理论上不可修改或只能追加修改记录这与预约状态是两码事。收费表我采用了最简单的方案一张 charge 表记录收费类型、金额、关联的预约或挂号单。如果需要更细的财务统计可以再加字段但中小型宠物医院这个粒度就够用了。4. 后端接口设计与核心功能实现4.1 RESTful API 接口规划前后端分离项目里接口设计决定了联调效率。我习惯先列一张接口清单把方法、路径、入参、出参定好再写代码。这个系统的主要接口如下接口方法路径核心入参说明登录POST/api/login/username, password返回 token 和 user_type主人列表GET/api/owners/keyword可选支持按姓名电话搜索创建主人POST/api/owners/name, phone, address前台登记用宠物列表GET/api/pets/?owner_idowner_id按主人查宠物创建宠物POST/api/pets/owner, name, species...新增宠物档案预约列表GET/api/appointments/?doctor_iddate支持按医生、日期筛选前台查询排班创建预约POST/api/appointments/pet, doctor, date, period床位冲突由后端校验病历列表GET/api/records/?pet_idpet_id查看历史病历创建病历POST/api/records/appointment, pet, diagnosis...医生填写收费单POST/api/charges/record, items[]生成收费明细接口路径遵循 DRF 的 ModelViewSet 约定关键字搜索用 filtering 实现日期范围用 date 参数传。4.2 Django REST Framework 序列化与视图实现视图层我建议用 DRF 的 generics 或 ViewSet不要裸写 JSONResponse。序列化器负责数据校验和格式转换ViewSet 自动生成一组 CRUD 端点省下的代码量非常可观。给一个简单的示例# serializers.py from rest_framework import serializers from .models import Pet, PetOwner, Appointment class PetOwnerSerializer(serializers.ModelSerializer): pet_count serializers.IntegerField(sourcepets.count, read_onlyTrue) class Meta: model PetOwner fields [id, name, phone, address, remark, pet_count] class PetSerializer(serializers.ModelSerializer): owner_name serializers.ReadOnlyField(sourceowner.name) owner_id serializers.PrimaryKeyRelatedField(querysetPetOwner.objects.all(), sourceowner) class Meta: model Pet fields [id, owner_id, owner_name, name, species, breed, gender, birthday, weight, neutered] # views.py from rest_framework import viewsets from .models import Pet, PetOwner from .serializers import PetOwnerSerializer, PetSerializer class PetOwnerViewSet(viewsets.ModelViewSet): queryset PetOwner.objects.all().order_by(-created_at) serializer_class PetOwnerSerializer def get_queryset(self): qs super().get_queryset() keyword self.request.query_params.get(keyword) if keyword: qs qs.filter(phone__containskeyword) | qs.filter(name__containskeyword) return qs class PetViewSet(viewsets.ModelViewSet): queryset Pet.objects.select_related(owner).all() serializer_class PetSerializer def get_queryset(self): qs super().get_queryset() owner_id self.request.query_params.get(owner_id) if owner_id: qs qs.filter(owner_idowner_id) return qs这里我特别注意了查询性能。宠物列表和病历列表在后台使用频率很高如果用默认查询每次关联 owner 信息都会产生额外的 SQL 查询。加 select_related(owner) 后Django 会通过 JOIN 一次性把关联对象查出来列表页就不会出现 N1 查询问题。这个优化在数据量小的时候感受不明显数据量到几千条时差异巨大。4.3 预约冲突处理与状态流转预约是这套系统中最容易出 bug 的功能。用户在前台选好医生和时段点击提交如果这个时段已经被占必须给出明确提示不能让两条预约并存。我的做法是在视图中用 transaction 包裹先尝试创建捕获 IntegrityErrorfrom django.db import transaction, IntegrityError from rest_framework.response import Response from rest_framework import status class AppointmentViewSet(viewsets.ModelViewSet): queryset Appointment.objects.select_related(pet, doctor).all() serializer_class AppointmentSerializer def create(self, request, *args, **kwargs): serializer self.get_serializer(datarequest.data) serializer.is_valid(raise_exceptionTrue) try: with transaction.atomic(): self.perform_create(serializer) except IntegrityError: return Response({message: 该医生在所选时段已有预约请更换时间}, statusstatus.HTTP_400_BAD_REQUEST) return Response(serializer.data, statusstatus.HTTP_201_CREATED)除了后端拦截我在前端提交时也会先调用一个查询接口传医生、日期、时段查一下是否被占。这样用户不用等提交失败才被提醒体验更好。前后端双重校验算是一个稳妥的做法。时间段的粒度我建议半小时一个槽位医生一天排 12 个小时也就 24 个时段一个表格能放下冲突检测也简单。4.4 用 Django Admin 做数据运维后盾系统上线后总会遇到一些边缘情况没法通过前台页面处理。比如后台改疫苗的批号、修正一条错误的主人的手机号。这时候 Django Admin 的价值就体现出来了。我把 PetOwner、Pet、Appointment、MedicalRecord 都注册到 admin 里用 list_display 配置好展示字段再加 search_fields 和 list_filter基本不需要额外开发就能获得一个可用的数据管理后台。如果你开发时没有造数也可以直接通过 Admin 录入几条测试数据省得写一堆 Faker 脚本。我建议在项目里都保留 admin 的注册代码即使前端功能齐全admin 依然是运维兜底的重要工具。5. Vue 前端项目实现与联调5.1 项目初始化与目录组织前端我使用的是 Vue 3 Vite Element Plus Pinia。创建命令npm create vitelatest pet-hospital-frontend -- --template vue cd pet-hospital-frontend npm install npm install element-plus axios pinia vue-router4目录结构按模块划分而不是按文件类型堆叠src/ api/ # 接口封装按模块拆文件 owner.js pet.js appointment.js components/ # 公共组件 PetCard.vue SearchBar.vue views/ # 页面组件 dashboard/ # 工作台 owner/ # 主人管理 pet/ # 宠物档案 appointment/ # 挂号预约 record/ # 病历管理 router/ index.js # 路由配置 store/ # Pinia 状态 user.js5.2 路由配置与权限控制路由我分成两类基础路由和需要登录的受保护路由。在 router/index.js 中用 meta.requiresAuth 标记需要登录才能访问的页面。同时用 meta.roles 控制角色可见性。import { createRouter, createWebHistory } from vue-router import { useUserStore } from ../store/user const routes [ { path: /login, component: () import(../views/Login.vue) }, { path: /, component: () import(../layouts/BasicLayout.vue), redirect: /dashboard, children: [ { path: dashboard, name: Dashboard, meta: { title: 工作台, requiresAuth: true }, component: () import(../views/dashboard/index.vue) }, { path: owners, name: OwnerList, meta: { title: 宠物主人, requiresAuth: true, roles: [admin, front] }, component: () import(../views/owner/index.vue) }, { path: pets, name: PetList, meta: { title: 宠物档案, requiresAuth: true }, component: () import(../views/pet/index.vue) }, { path: appointments, name: AppointmentList, meta: { title: 挂号预约, requiresAuth: true, roles: [admin, front] }, component: () import(../views/appointment/index.vue) }, { path: records, name: RecordList, meta: { title: 病历管理, requiresAuth: true, roles: [admin, doctor] }, component: () import(../views/record/index.vue) }, ], }, ] router.beforeEach((to, from, next) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { next(/login) } else if (to.meta.roles !to.meta.roles.includes(userStore.userType)) { next(/403) } else { next() } })动态路由这块我也简单用了下。系统顶部菜单是根据用户类型从后端返回的菜单配置生成的属于纯前端逻辑并不需要后端生成路由所以没有用到 addRoute 做真正的动态注册。这样实现成本低得多代码也直观。5.3 前端接口封装与 Axios 拦截器Axios 的封装是前端联调的关键一环。我创建了一个 request.js 统一处理 baseURL、token 注入、错误响应解析import axios from axios import { ElMessage } from element-plus import { useUserStore } from ../store/user const request axios.create({ baseURL: http://localhost:8000/api, timeout: 5000, }) request.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Token ${userStore.token} } return config }) request.interceptors.response.use( response response.data, error { const status error.response?.status if (status 401) { ElMessage.error(登录状态已过期请重新登录) window.location.href /login } else { ElMessage.error(error.response?.data?.message || 接口请求失败) } return Promise.reject(error) } )接口模块再按数据对象拆文件比如 owner.js 里导出 getOwnerList、createOwner 等函数页面组件里只调用这些封装好的方法不直接拼 URL。这样后端接口路径变了只需改一个文件。5.4 宠物档案联动与表格交互实现宠物档案页是核心页面之一。我在主页面放了一个搜索框和主人列表表格点击某个主人后右侧面板显示该主人名下所有宠物卡片再点击宠物卡片下方显示宠物详情和疫苗记录。这个联动交互在 Vue 里非常简单template div classpet-container el-row :gutter16 el-col :span10 el-input v-modelkeyword placeholder搜索主人姓名/电话 / el-table :dataownerList highlight-current-row current-changehandleOwnerChange el-table-column propname label姓名 / el-table-column propphone label电话 / /el-table /el-col el-col :span14 el-cards v-ifcurrentOwner el-card v-forpet in petList :keypet.id classpet-card h4{{ pet.name }}{{ pet.species }}/h4 p体重{{ pet.weight }}kg/p p已绝育{{ pet.neutered ? 是 : 否 }}/p el-button typeprimary sizesmall clickopenRecord(pet)查看病历/el-button /el-card /el-cards el-empty v-else description请先在左侧选择主人 / /el-col /el-row /div /template script setup import { ref, watch } from vue import { getOwnerList } from ../api/owner import { getPetList } from ../api/pet import { ElMessage } from element-plus const keyword ref() const ownerList ref([]) const currentOwner ref(null) const petList ref([]) watch(keyword, async (val) { ownerList.value await getOwnerList({ keyword: val }) }) async function handleOwnerChange(row) { if (!row) return currentOwner.value row petList.value await getPetList({ owner_id: row.id }) } function openRecord(pet) { // 跳转病历页面并带参数 router.push({ path: /records, query: { pet_id: pet.id } }) } /script这里的核心思路是用响应式数据驱动 UI 更新只要 currentOwner 变了右侧宠物列表自动重新拉取这是 Vue 相对 jQuery 操作 DOM 的巨大优势。页面里我没有写一行手动 DOM 更新的代码。6. 开发环境配置、联调与部署问题排查6.1 开发环境配置全套流程环境配置看起来繁琐实际上按步骤走很顺。我先说 Python 和 Django 侧1. 安装 Python 3.10勾选 Add to PATH。 2. 创建虚拟环境python -m venv venv 3. 激活虚拟环境venv\Scripts\activateWindows或 source venv/bin/activatemacOS/Linux 4. 安装依赖pip install django djangorestframework django-cors-headers 5. 创建项目django-admin startproject pet_hospital 6. 创建应用python manage.py startapp pets 7. 配置 settings.py注册 app、配置数据库默认 SQLite 即可、加 CORS 白名单。前端侧1. 安装 Node.js LTS 版本16 以上。 2. npm create vite pet-hospital-frontend 3. 安装依赖npm install 4. 安装组件库和请求库npm install element-plus axios pinia vue-router4 5. 启动开发服务npm run devPyCharm 里的操作重点有两处。一是在 Settings - Project - Python Interpreter 里选择虚拟环境中的 Python不要直接选全局解释器二是运行配置里可以同时添加 Django Server 和 npm 两个启动项分别设置端口 8000 和 5173。开发阶段的跨域问题前后端本地地址不一致浏览器会拦截。解决思路是 Django 后端加 django-cors-headers允许 localhost:5173 来源另一种是前端配置 Vite 代理把 /api 请求转发到 8000 端口。我两种都试过如果后台上线后前后端部署在同一个域名下其实不需要 CORS只需在前端 Vite 里配置代理联调时最省心。6.2 数据库迁移与初始数据准备Django 模型定义完成后依次执行python manage.py makemigrations python manage.py migrate python manage.py createsuperusermigrate 成功后SQLite 数据库文件会出现在项目目录下。为了快速验证业务功能我通过 Django Admin 手工录入了几个测试主人和宠物再写了一个 management command 生成了未来一周的医生排班表。这段脚本的核心逻辑是遍历每个医生、每天的每个时段创建 Appointment 对象时 status 设为 pending但已经关联了宠物。如果你用类似方案注意命令要可重复执行最好是先清理再生成否则会因 unique_together 约束直接报错。6.3 Django 查询与删除对象的常见坑开发中我踩了不少查询和删除的坑整理一下值得反复看查询单条对象用 get()找不到对象会抛 DoesNotExist多条会抛 MultipleObjectsReturned。捕获异常比较啰嗦我习惯用 filter().first() 代替至少不会抛异常。删除对象时Django 的 QuerySet.delete() 返回的是一个元组第一个元素是被删除对象的总数包括级联删除的关联对象。如果只想删预约本身不删病历就得小心外键的 on_delete 策略。我的经验是业务主表设 on_deleteCASCADE但像病历、处方这种需要留痕的数据宁可保留孤儿记录也不级联删。批量删除是物理删除Django 的 Model 没有软删除内置机制。我在项目里给需要标记删除的表加了 is_active 字段删除操作统一改成 update(is_activeFalse)。特别是在宠物主人这个表上物理删除会导致关联宠物和预约全部丢失后果很严重。6.4 前端页面显示 PDF 和特殊资源的处理宠物医院系统里有一个场景是上传动物检验报告 PDF让医生在病历页能查看。很多新手直接用 标签去显示 PDF肯定不行。img 标签只支持图片格式PDF 需要用 iframe 或 embed或者用专门的组件库比如 vue-pdf。我的做法很简单iframe :srcpdfUrl stylewidth: 100%; height: 600px /把 PDF 地址直接放进 iframe 的 src浏览器内置的 PDF 阅读器就能渲染。但要注意后端返回的文件要放在可访问的静态路径下或者通过接口以二进制流返回并设置正确的 Content-Type。如果是从 Django 后端动态生成或者登录鉴权的 PDF直接 iframe 可能会遇到 401 无法加载。那就改成先请求接口拿 blob 数据再通过 URL.createObjectURL 生成临时链接传给 iframe。6.5 部署时的典型差异与 PyCharm 小技巧开发环境和生产环境差别最大的就是静态文件和调试开关。Django 部署时DEBUG 必须设为 False然后收集静态文件python manage.py collectstatic如果忘记配置 ALLOWED_HOSTS访问会直接报错如果在 Nginx 前面挂了 HTTPS还需要设置 SECURE_PROXY_SSL_HEADER否则 Django 无法识别 HTTPS 请求。这些细节不会在开发时报错但到了部署环境全是问题。PyCharm 使用上的几个小技巧我也一起说了。第一安装 pandas 这类包如果速度太慢在 Settings - Project - Python Interpreter 里添加镜像源。第二PyCharm 的 AI 插件确实能辅助写代码但建议只用来解释代码块和生成测试数据别直接让它写业务逻辑生成的代码往往和项目风格不一致。第三专业版的数据库中可以直接查看 SQLite 表结构和查询数据调试时比命令行方便得多。7. 从开发到可运行的完整流程串联在这一节我把整个项目的操作顺序完整串一遍保证你照做就能跑起来。第一步到第四步是后端第五步之后是前端和联调。第一步创建项目和应用django-admin startproject pet_hospital cd pet_hospital python manage.py startapp users python manage.py startapp pets python manage.py startapp appointments第二步修改 settings.py。把四个 appusers、pets、appointments以及 rest_framework、corsheaders加入 INSTALLED_APPS配置 AUTH_USER_MODEL users.User添加 CORS 中间件允许来源。第三步编写 models.py 中的模型代码然后 makemigrations、migrate。这里有一个关键点一旦你设置了 AUTH_USER_MODEL必须在第一次 migrate 之前就设置好否则 Django 已经按默认 User 模型生成了 auth 表再想改就得重建数据库。做新项目时先把这个配置定好。第四步编写 serializers.py 和 views.py在 urls.py 里注册路由from rest_framework.routers import DefaultRouter router DefaultRouter() router.register(owners, PetOwnerViewSet) router.register(pets, PetViewSet) router.register(appointments, AppointmentViewSet) urlpatterns [ path(api/, include(router.urls)), ]第五步启动后端测试接口python manage.py runserver 8000用浏览器访问 http://localhost:8000/api/owners/如果能正常返回 JSON后端就绪。第六步创建前端项目配置路由和页面组件把业务页面逐步填充。我这里建议不要一次写全部页面而是先完成后端和前端联调的最小闭环——比如“创建主人 - 创建宠物 - 创建预约”这条路走通后再补齐医生工作台和收费模块。第七步部署。前端打包后生成 dist 目录把 dist 里的文件交给 Nginx 托管后端用 gunicorn 启动gunicorn pet_hospital.wsgi:application --bind 0.0.0.0:8000 --workers 3Nginx 配置里把 /api/ 反向代理到 8000 端口其他路径指向前端 dist 目录。这样前后端同域部署不需要处理跨域。8. 常见问题速查表与避坑要点我把开发过程中遇到的典型问题整理成一张速查表你遇到同样问题时可以对照排查。问题描述原因分析解决方案前端请求 /api 接口报 403没有正确携带 token或 CSRF 未配置在请求拦截器加 Authorization 头Django 创建对象时报 unique_together 冲突预约表时段冲突捕获 IntegrityError返回友好提示Vue 页面刷新后 404前端路由是 history 模式服务端未配置 fallbackNginx 增加 try_files $uri $uri/ /index.html图片能显示但 PDF 不显示img 标签不支持 PDF 渲染改用 iframe 或 vue-pdf 组件列表页响应慢关联查询产生 N1 问题使用 select_related 或 prefetch_relatedmigrate 后数据库没生成表忘记注册 app 到 INSTALLED_APPS检查 settings.py 的 app 配置上传的图片/文件在部署后无法访问Django 没配置 MEDIA_URL 和静态文件托管Nginx 配置 alias 指向 MEDIA_ROOT 目录前端提交表单后时间总是差 8 小时Django 时区设置影响 DateTimeField前后端统一用字符串传输时间或前端转本地时区最后说一个容易忽视的点Vue 路由的动态参数传递。从预约列表跳到病历详情我推荐用 path query 传参例如 /records?pet_id12而不是 params。原因很简单——query 参数刷新页面后还在params 在刷新后容易丢失。这是我在实际使用中踩过几次坑之后的总结直接影响用户体验值得记住。这套系统的扩展空间还很大比如接入短信提醒、实现线上支付、加宠物寄养管理。但我始终认为先把主流程做扎实比堆砌功能更重要。拿这套代码作为骨架后续加功能就像给树打枝丫一样自然。如果你也在做类似项目建议从今天开始把需求拆解、技术选型、数据库设计这三步认真走完再开写第一行代码你会发现后续开发顺畅非常多。