做宠物健康档案管理系统,最让我意外的是"档案"这两个字背后的复杂度。很多人一听觉得简单,不就是一张表存宠物名字、主人电话和打针记录嘛。可真把需求细化后发现,我今天要能查出去年这只猫的体重曲线,明天要能提醒哪位主人该给狗做驱虫了,后天还要在门店电脑上快速调出某只宠物对什么药物过敏——这些零散信息如果不结构化,用起来就是灾难。所以我基于Python和Vue搭了一套宠物健康档案信息管理系统,后端在Django和Flask之间反复权衡过,最后用Django,前端用Vue全家桶,日常开发都在PyCharm里完成。这篇文章我会把项目从需求收敛、表结构设计、接口实现到前端页面和调试部署的完整过程讲一遍,重点说清楚每个决策背后的理由,以及我踩过哪些坑,给正在做类似管理系统的同学一个参考。
1. 项目动机与功能边界:一张档案表背后的需求陷阱
1.1 宠物主的真实痛点
我接触过一位开了三年宠物店的店主,他跟我说过一句话:店里最值钱的不是那几台美容台,而是客户带宠物看病的记录。纸质本子容易丢,微信聊天记录翻起来费劲,老板脑子再好也记不住上百只宠物各自的疫苗日和过敏史。宠物主人更头疼——换一家医院就要重新讲一遍既往病史,接诊医生最怕听到"好像打过疫苗"这种模糊回答。
这些痛点指向同一个需求:把宠物从出生到当下的所有健康事件串起来,形成一份可持续更新的电子档案。它不能只是登记,要按照时间线记录疫苗、驱虫、体检、体重变化,还要能提前算出下次该打疫苗的日期,在到期前提醒主人。这个系统不是给技术极客玩的,而是给宠物店主、普通宠物主人用的,界面必须简单直接。
1.2 需求收敛:先做最小可用闭环
我最初列需求的时候根本刹不住车,什么智能诊断、大数据分析、物联网体温监测都想过。后来发现,一旦陷入功能扩张,项目铁定烂尾。于是我砍到只剩四个闭环:
- 档案创建:登记宠物基本信息、主人联系方式和过敏史。
- 健康记录:录入疫苗、驱虫、体检、体重四类核心事件。
- 到期提醒:根据疫苗和驱虫的间隔天,自动生成待办提醒。
- 查询与导出:按宠物、日期、记录类型过滤,并支持简单导出。
这四个闭环已经能覆盖宠物店日常运营的九成需求。一个给宠物店主用的MVP,最怕的不是功能少,而是功能多到没人愿意录。所以我在MVP里甚至砍掉了多门店权限,只保留单店管理。
1.3 功能清单(MVP版)
用表格列出来更直观:
| 模块 | 功能点 | 说明 |
|---|---|---|
| 宠物档案 | 新增/编辑/删除宠物的头像、品种、性别、绝育状态、过敏史 | 软删除,不物理删除 |
| 主人管理 | 关联主联系方式、默认紧急联系人 | 一个主人可拥有多只宠物 |
| 疫苗记录 | 疫苗名称、批号、接种日期、下次接种日期 | 到期自动提醒 |
| 驱虫记录 | 驱虫药名称、用药方式、驱虫日期 | 到期自动提醒 |
| 体检记录 | 体温、心率、诊断描述、用药方案 | 体格指标用Decimal存 |
| 体重管理 | 每隔一段时间的体重快照,绘制趋势 | 折线图展示 |
| 提醒中心 | 筛选未来7天、30天需要疫苗接种/驱虫的宠物 | 主页默认展示 |
这套MVP在前后端联调阶段非常顺,因为每个模块都可以独立验证,没有那种"一个功能要等另一个功能完成才能测"的耦合感。
2. Django 与 Flask 的选型:我为什么在这类管理系统上押注 Django
2.1 两条路线在宠物档案系统上的差异
Django和Flask都是Python生态里成熟的Web框架,但在宠物档案这种业务上,差异非常明显。Django自带了一整套"全家桶":ORM、迁移工具、Admin后台、认证系统、表单校验。这些恰好对应管理系统的常见需求——我有多个数据模型要联动管理,有后台维护数据的刚需。Flask则像一个工具箱,只提供路由和WSGI,其他一切都要自己选:用SQLAlchemy还是Peewee?用Flask-Login还是要手写JWT?用Flask-Migrate?好在社区都有现成扩展,但所有组件要自己拼装,拼接过程的坑不在少数。
这不是说Flask就不好。如果你只是做一个给某个内部团队用的小接口,三张表一个登录,Flask的轻盈会让你很舒服。但宠物健康档案牵涉宠物、主人、四种健康记录、提醒、权限,关系网复杂,Django的ORM可以直接用外键关联和多对多关系把这些关系表达清楚,迁移工具也能让我在迭代中不断改表而不手写SQL。
2.2 为什么我最终选了 Django
我的真实理由很简单:开发效率。Django自带一个完整的Admin后台,当我给店长演示系统时,可以直接登录Admin,临时修改一条错误的疫苗记录,不需要专门写管理页面。这个“免费”后台在MVP阶段帮我省掉了至少两个星期的开发量。
另一个理由是认证和权限。Django的auth框架自带用户、组、权限;我只需要把“店主”“店员”“宠物主人”映射到用户组,就能迅速实现“店员只能查看,不能删除”的规则。Flask需要引入Flask-Security或Flask-Admin,这些扩展之间偶尔存在版本兼容问题。还有一个细节是Django的中间件和信号机制,比如我想在新增体重记录后自动更新宠物的“最近一次体重”字段,用post_save信号几行代码就搞定了,Flask则需要手动在业务逻辑里调用。
2.3 如果就是更偏好 Flask,核心部分怎么写
这里也给你们一条能跑通的Flask路线:用Flask + SQLAlchemy + Flask-RESTful + PyJWT。核心模型和Django版本大同小异,只是路由换成蓝图:
from flask import Blueprint, request from flask_restful import Api, Resource from models import db, Pet pet_bp = Blueprint('pet', __name__) api = Api(pet_bp) class PetListResource(Resource): def get(self): pets = Pet.query.filter_by(is_active=True).all() return [pet.to_dict() for pet in pets] api.add_resource(PetListResource, '/api/pets')Flask写起来代码确实更少,但你很快会发现要手动处理跨域、手动写JWT校验装饰器、手动做序列化嵌套。如果你的项目本身很小,这些都可以接受;一旦业务复杂,你的手写代码量成倍增长,那就不如Django那一套现成方案划算。
2.4 选型自查清单
我梳理了一张表,可以帮你在项目开始时快速决策:
| 判断项 | 偏向Django | 偏向Flask |
|---|---|---|
| 数据表数量 | 超过10张,关系复杂 | 少于6张,结构简单 |
| 是否需要后台管理 | 很需要 | 可自行开发 |
| 团队熟悉ORM | 想少写SQL,用Django ORM | 愿意自己设计SQLAlchemy |
| 权限模型 | 基础RBAC就够 | 需要极度定制 |
| 部署体积 | 可以接受较重的框架 | 追求轻量、启动快 |
| 总结 | 管理系统全家桶 | 接口服务工具箱 |
如果你的项目类型和我类似,是把数据管理作为核心,那我建议直接选Django。其他情况,Flask也完全撑得住。
3. 数据库模型设计:健康档案不是一张表,而是一张关系网
3.1 核心实体与关系梳理
我的第一版设计是做一个统一记录表,加一个“记录类型”字段,疫苗、驱虫、体检全塞进去。结果越做越难受:疫苗需要“批号”,驱虫需要“用药方式”,体检需要“体温和心率”,这些字段强行塞进一张表会产生大量空值,查询也要到处加if。后来我推翻重来,遵守一个原则:字段差异大的业务对象,就应该拆成独立的表。
最终的实体关系大致是这样:
- 宠物主人 Owner:具有姓名、电话、微信。
- 宠物 Pet:属于一个主人,记录品种、性别、生日、绝育状态、过敏史。
- 疫苗记录 Vaccination:属于一只宠物,记录疫苗名称、批号、接种日期、下次接种日期。
- 驱虫记录 Deworming:属于一只宠物,记录药品名称、用药方式、驱虫日期。
- 体检记录 HealthCheck:属于一只宠物,记录体温、心率、诊断、处方。
- 体重记录 Weight:属于一只宠物,记录体重值、记录时间。
- 提醒 Reminder:可以由系统根据疫苗/驱虫日期生成,也会记录已读/未读状态。
3.2 Django 模型的代码落地
我把代码精简一下,关键部分如下:
from django.db import models from django.contrib.auth.models import User class Owner(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, null=True, blank=True) name = models.CharField(max_length=50) phone = models.CharField(max_length=20) wechat = models.CharField(max_length=50, blank=True) created_at = models.DateTimeField(auto_now_add=True) class Pet(models.Model): owner = models.ForeignKey(Owner, on_delete=models.CASCADE, related_name='pets') name = models.CharField(max_length=50) breed = models.CharField(max_length=50, blank=True) gender = models.CharField(max_length=10, choices=[('male', '公'), ('female', '母')]) birthday = models.DateField(null=True, blank=True) neutered = models.BooleanField(default=False) allergy_info = models.JSONField(default=list, blank=True) avatar = models.ImageField(upload_to='pet_avatars/', null=True, blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class VaccinationRecord(models.Model): pet = models.ForeignKey(Pet, on_delete=models.CASCADE, related_name='vaccinations') vaccine_name = models.CharField(max_length=100) batch_no = models.CharField(max_length=50, blank=True) vaccinated_date = models.DateField() next_due_date = models.DateField(null=True, blank=True) notes = models.TextField(blank=True) created_at = models.DateTimeField(auto_now_add=True) class DewormingRecord(models.Model): pet = models.ForeignKey(Pet, on_delete=models.CASCADE, related_name='dewormings') medicine_name = models.CharField(max_length=100) route = models.CharField(max_length=20, choices=[('oral', '口服'), ('topical', '外用')], default='topical') deworming_date = models.DateField() next_due_date = models.DateField(null=True, blank=True) notes = models.TextField(blank=True) class HealthCheckRecord(models.Model): pet = models.ForeignKey(Pet, on_delete=models.CASCADE, related_name='health_checks') check_date = models.DateField() temperature = models.DecimalField(max_digits=4, decimal_places=1, null=True, blank=True) heart_rate = models.IntegerField(null=True, blank=True) diagnosis = models.TextField(blank=True) prescription = models.TextField(blank=True) class WeightRecord(models.Model): pet = models.ForeignKey(Pet, on_delete=models.CASCADE, related_name='weights') weight = models.DecimalField(max_digits=5, decimal_places=2) record_date = models.DateField()3.3 字段设计的隐藏坑
这段我不想一笔带过,因为真的都是血泪经验。
先说related_name。如果不写,Django默认用pet_set,在宠物档案这种以宠物为中心的系统里,我想写pet.vaccinations.all()而不是pet.vaccinationrecord_set.all(),所以必须设置。这是一开始就要想好的。
再说体重字段。很多人图省事用FloatField,但浮点数在计算和历史比较时有精度问题。例如记录2.1kg和2.2kg,某些情况下显示出来是2.0999999。我统一改用DecimalField,搭配max_digits=5, decimal_places=2。
日期字段也要按需选择。疫苗和体重记录需要区分到天,用DateField;体检记录可能一天测多次,但MVP里只关心天数,所以用DateField即可。如果要做精确时间线,就要用DateTimeField,但要注意时区问题。
null和blank经常混。null是数据库角度允许为空,blank是表单提交角度允许不填。对外键字段,我一般写null=True,否则删除关联数据时会报错。对字符串字段,我通常只写blank=True,因为数据库中空字符串比NULL更好处理。
还有软删除。宠物档案不能随便物理删除,万一是老客户呢。所以我在Pet上加了is_active字段,所有列表接口默认过滤掉is_active=False。删除操作实际是软删除,保留历史记录。
3.4 疫苗和驱虫这类“高频明细”的处理
我前面提到统一表方案会失控,这里用代码对比一下两种处理方式:
统一表的致命伤是你取一条疫苗记录时必须判断类型,再强转字段。比如:
if record.event_type == 'vaccination': vaccine_name = record.vaccine_name elif record.event_type == 'deworming': medicine_name = record.medicine_name这种代码在两种类型时还能忍,类型一多就是灾难。拆表之后,每种记录都有自己的序列化器、自己的校验规则、自己的过滤字段,前端也能分别调用,逻辑清晰。虽然表数量变多,但每个表的字段和语义是完整自洽的。这是我认为本项目在数据建模上最对的一个决策。
4. 后端接口设计:RESTful 不是按 URL 风格,而是按资源语义
4.1 接口清单与状态码约定
接口设计看起来简单,但容易陷入“把后端接口写成参数堆砌”的误区。我按资源维度列了这样一组接口:
| 方法 | 路径 | 功能 | 权限 |
|---|---|---|---|
| GET | /api/owners/ | 主人列表 | 登录用户 |
| POST | /api/owners/ | 新增主人 | 登录用户 |
| GET | /api/owners/{id}/ | 主人详情 | 本人/员工 |
| PATCH | /api/owners/{id}/ | 更新主人 | 本人/员工 |
| GET | /api/pets/ | 宠物列表,支持筛选 | 登录用户 |
| POST | /api/pets/ | 新增宠物 | 登录用户 |
| GET | /api/pets/{id}/ | 宠物详情(含四类记录) | 本人/员工 |
| PATCH | /api/pets/{id}/ | 更新宠物 | 本人/员工 |
| DELETE | /api/pets/{id}/ | 软删除宠物 | 员工 |
| POST | /api/pets/{id}/vaccinations/ | 添加疫苗记录 | 本人/员工 |
| POST | /api/pets/{id}/dewormings/ | 添加驱虫记录 | 本人/员工 |
| POST | /api/pets/{id}/health-checks/ | 添加体检记录 | 员工 |
| POST | /api/pets/{id}/weights/ | 添加体重记录 | 本人/员工 |
| GET | /api/reminders/ | 获取提醒列表 | 登录用户 |
状态码我也做了统一约定:列表查询成功返回200,新增成功返回201,修改成功返回200,删除成功返回204,校验失败返回400,未登录返回401,无权限返回403。这个约定写在接口文档里,前后端都按这个执行,联调时少了大量无谓争吵。
4.2 序列化器嵌套与校验
我用Django REST Framework的序列化器实现输出结构。宠物详情同时返回主人信息、疫苗记录、驱虫记录等。
from rest_framework import serializers from .models import Owner, Pet, VaccinationRecord, DewormingRecord class OwnerBriefSerializer(serializers.ModelSerializer): class Meta: model = Owner fields = ['id', 'name', 'phone'] class VaccinationRecordSerializer(serializers.ModelSerializer): class Meta: model = VaccinationRecord fields = '__all__' class PetDetailSerializer(serializers.ModelSerializer): owner = OwnerBriefSerializer(read_only=True) vaccinations = VaccinationRecordSerializer(many=True, read_only=True) dewormings = DewormingRecordSerializer(many=True, read_only=True) class Meta: model = Pet fields = ['id', 'name', 'breed', 'gender', 'birthday', 'neutered', 'allergy_info', 'created_at', 'owner', 'vaccinations', 'dewormings']写接口的时候有个细节要注意:嵌套序列化器作为只读输出很好用,但写入时前端传的owner应该只是一个ID。所以新增和更新接口我会另外定义一个PetWriteSerializer,把owner设置成PrimaryKeyRelatedField。这样读写分离,输出给前端的是结构化对象,输入接收的是ID,不会出现明明要传{"owner": {"name": "张三"}}这种繁琐结构。
我在validate方法中做了业务校验,比如疫苗的接种日期不能晚于今天,体重必须在0.01kg到200kg之间。这些校验前端口头承诺再严格也没用,后端的唯一权威校验必须保留。
4.3 宠物主与兽医的权限区分
MVP里我把用户分成两个角色:宠物主人和店员。宠物主人登录后,所有宠物列表和管理操作都限定在当前用户关联的Owner对象下。店员则可以查看全部。
具体做法是在视图集的get_queryset中动态过滤:
from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ModelViewSet class PetViewSet(ModelViewSet): serializer_class = PetDetailSerializer permission_classes = [IsAuthenticated] def get_queryset(self): user = self.request.user if user.groups.filter(name='staff').exists(): return Pet.objects.filter(is_active=True) owner = Owner.objects.filter(user=user).first() if owner: return Pet.objects.filter(owner=owner, is_active=True) return Pet.objects.none()这个过滤在列表和详情时都会生效,从而避免“宠物主人A看到宠物主人B的狗”这种越权情况。店员和主人的差异只在能否删除和能否看全部数据上,其他功能共用一套接口,这样省脑子。
4.4 分页、搜索、过滤的通用实现
宠物列表页往往需要按品种筛选、按主人电话搜索、按疫苗到期日筛选。我不建议手写一堆if参数判断,用Django Filter最稳:
from django_filters.rest_framework import DjangoFilterBackend from rest_framework.filters import SearchFilter, OrderingFilter class PetViewSet(ModelViewSet): filter_backends = [DjangoFilterBackend, SearchFilter, OrderingFilter] filterset_fields = ['breed', 'gender', 'neutered'] search_fields = ['name', 'owner__name', 'owner__phone'] ordering_fields = ['created_at', 'birthday']分页配置放在REST Framework全局设置里,默认每页20条。前端请求/api/pets/?page=1&page_size=10或者/api/pets/?search=金毛都能拿到过滤后的分页数据。
这里要注意一个细节:如果前端希望一次拿到所有品种列表用于下拉框,最好不要直接走分页接口,我单独加了一个/api/pets/breeds/返回去重后的品种列表,接口语义更清晰。
5. Vue 前端的快速搭建:列表、详情、表单三步走
5.1 脚手架选择与目录结构
前端我使用 Vue 3 + Vite + Vue Router + Pinia。Vite相比webpack启动速度快一大截,对本地开发体验提升非常明显。UI组件库选的是Element Plus,表单和表格都很成熟,能省不少样式时间。
目录结构大致如下:
src/ api/ pet.js owner.js reminder.js request.js views/ Login.vue Dashboard.vue PetList.vue PetDetail.vue ReminderCenter.vue router/ index.js store/ user.js components/ WeightChart.vue RecordFormDialog.vueapi目录按资源拆分,比如pet.js里只放宠物相关的接口函数。这种方式在多人协作时很有用,不会所有请求都堆在一个文件里。
5.2 核心页面拆解
宠物列表页是系统的第一个落地页面。我用el-table展示宠物名、主人、品种、性别、最近体重、下一次疫苗日期,右上角放搜索框,支持按主人电话搜索。当日期临近30天时,那一行会高亮提醒。
宠物详情页是核心,我采用Tabs布局:基本信息、疫苗记录、驱虫记录、体检记录、体重趋势。基本信息用el-descriptions展示,记录表用el-table展示,每条记录后面带“编辑”和“删除”按钮。体重趋势使用了echarts折线图组件,数据从体重接口拉取后按日期排序。
健康记录表单我用了一个动态表单弹窗,根据当前选择的记录类型切换字段。比如选择疫苗,显示疫苗名称、批号、接种日期、下次接种日期;选择驱虫,显示药品名称和用药方式。这个动态表单让店主录数据时可以一次完成,不需要在不同弹窗间切换。
5.3 数据请求封装与 Token 管理
我封装了一个request.js,核心思路是统一处理baseURL、Token注入和错误提示。
import axios from 'axios' import router from '@/router' const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => response.data, error => { if (error.response.status === 401) { router.push('/login') } else { const msg = error.response?.data?.detail || '请求失败' alert(msg) } return Promise.reject(error) } ) export default requestToken 是登录成功时后端返回的JWT,本地存到localStorage。每次请求自动带上,401时强制回到登录页。跨域的问题我在vite.config.js里用代理解决:
server: { proxy: { '/api': { target: 'http://localhost:8000', changeOrigin: true } } }这样前端开发服务器转发/api到后端,避免浏览器跨域拦截。
5.4 与后端调试时的常见 Bug
我踩过最坑的一个Bug是前后端字段命名不一致。Django默认输出created_at,前端组件喜欢用createdAt。让我更痛的是,我把宠物生日字段写成birthday,前端某次接口却传了birthDate。解决方式是用前端API辅助函数统一做字段映射,而不要依赖后端改名。另外后端返回的日期格式是 ISO 字符串,前端直接渲染没问题,但要参与日期计算时必须先转new Date()。还有一点:后端返回的列表都可能为空数组[],前端不要判断data是否为真,要判断data.length。
6. PyCharm 调试与前后端联调:让 Bug 无处可藏
6.1 运行配置与虚拟环境
我所有的开发都在PyCharm里完成。项目一开始,我就建议不要用系统默认 Python,而是创建独立的虚拟环境,在PyCharm的Settings→Project Interpreter里选择虚拟环境路径。运行Django项目时,配置一个Django Server类型的运行配置,Environment variables里设置PYTHONUNBUFFERED=1,Run配置里勾选Run on port为8000。
前端则配置一个 npm脚本运行项,执行npm run dev,端口习惯设为5173。
6.2 Debug 模式下查看 Django ORM 查询
这是PyCharm专业版一个非常好用的功能。后端代码打断点后,在Debug窗口可以逐行查看ORM的SQL查询。我用这个方法抓到一个隐藏的N+1问题——列表页获取每只宠物的记录时,如果不加prefetch_related,宠物数量每多一只就多几次查询。在Debug窗口看到SQL次数从3变成20几次,瞬间明白怎么改:
pets = Pet.objects.select_related('owner').prefetch_related( 'vaccinations', 'dewormings', 'weights' ).filter(is_active=True)6.3 前后端联调时的断点策略
后端断点通常打在视图第一行和序列化器的validate方法里。前端断点打在request.js的拦截器和表单提交函数里。联调时出现“数据没刷新”,我一般先看前端的请求有没有发出去,再看后端有没有收到请求。这两个位置各打断点,很快能定位是前端组件状态问题还是后端接口返回问题。
6.4 PyCharm 的 HTTP Client 预置接口测试
不用每次打开Postman。PyCharm自带.http文件,写起来简单:
### 获取宠物列表 GET http://localhost:8000/api/pets/?search=金毛 Authorization: Bearer {{access_token}} ### 新增疫苗记录 POST http://localhost:8000/api/pets/3/vaccinations/ Content-Type: application/json Authorization: Bearer {{access_token}} { "vaccine_name": "狂犬疫苗", "batch_no": "20240301", "vaccinated_date": "2024-03-01", "next_due_date": "2025-03-01" }这些请求可以保存下来,每次改完代码直接点绿色箭头重新请求,省去在工具间切换的时间。
7. 安全、备份与部署:系统能跑不等于能给人用
7.1 表单校验的二次防线
前端校验只是提升体验,后端校验才是安全底线。系统里两类数据必须重点校验:日期和金额类。比如疫苗next_due_date必须晚于vaccinated_date;体重不能为负数;体温如果超出了常见宠物范围(如35—42度),数据库不会拒绝,但业务上可能是误录。
Django的字段验证和序列化器的validate都可以做。我倾向于在序列化器里写业务校验,因为它能拿到完整的数据上下文;在模型层只做基础的类型约束。
7.2 图片上传目录穿越与类型校验
宠物头像上传是个容易被忽略的安全点。用户上传一张名为../../shell.php的文件到服务器,如果直接拼接路径,后果不堪设想。我的处理方式是:
- 重命名文件为UUID,不保留原始文件名。
- 校验扩展名和ContentType,只允许jpg、jpeg、png、webp。
- 限制最大文件大小为5MB。
- 图片路径由Django处理的
upload_to生成,而不是自己拼字符串。
部分代码:
import os import uuid def pet_avatar_upload_path(instance, filename): ext = filename.lower().rsplit('.', 1)[-1] return f'pet_avatars/{uuid.uuid4().hex}.{ext}'7.3 备份:SQLite 只能救急,MySQL 才能救长期
开发期我用了SQLite,因为零配置、随处可跑。但上线给宠物店实际使用后,并发一上来,SQLite写锁会让人崩溃。所以生产环境换成了MySQL。备份我写了一个简单的定时脚本,用mysqldump每天夜里把数据导出到服务器本地,再同步到另一台机器。这个脚本不用很复杂,但必须有,因为数据丢了一旦是宠物主人的疫苗记录,信誉直接崩掉。
7.4 部署时的流量入口与静态文件
前端npm run build后生成dist目录,里面有静态文件。部署时我用Nginx托管这些静态文件,并把/api的请求反向代理到后端进程(uwsgi或gunicorn)。需要注意的是,Django的DEBUG必须设为False,ALLOWED_HOSTS要填实际域名或IP。还有密钥不要写死在代码里,我是放在环境变量里读取。
这些部署细节看着不起眼,但如果不做,系统跑不到一个月就各种问题。
8. 项目复盘:最值得保留的三个决策和最该改掉的三个习惯
8.1 最值得保留的三个决策
第一,把健康记录拆成独立表。虽然当时写代码多花了半天,但后续每个类型的扩展都非常从容。第二,后端校验不依赖前端。这个保证了很多脏数据根本进不到数据库,维护成本直线下降。第三,MVP阶段拒绝全景图。没有一上来就做权限矩阵和共享预约,而是围绕核心数据闭环迭代,两周后就能交给宠物店试用,收到真实反馈后再调整。
8.2 最该改掉的三个习惯
第一,一开始就把权限模型设计得过于复杂。我给自己挖了个大坑,设计了用户、角色、菜单、按钮四级权限,实际上宠物店只需要“店员”和“店长”两种角色。第二,我忽略了体重趋势的日期连续性问题:宠物可能某段时间没有记录,折线图直接把点连起来,看起来像体重突变。后来我改成按时间散点图,并在跳空区域做空心点标记。第三,我没有在开发期做数据库迁移演练,导致上线前改字段时出现了一次外键冲突。
8.3 给后来的开发者一句话
宠物健康档案系统真正难的不是Vue组件怎么写、Django模型怎么建,而是业务规则怎么理解。下次打疫苗的日期怎么算、驱虫药是外用还是口服、体重记录要不要单独成表,这些都需要在写代码前和宠物店的实际使用者反复确认。技术选型会过时,但把业务想明白的习惯永远不会过时。