4、数据库模型设计(上):用户模型、设备模型、固件模型、升级任务模型的ER图设计与Django ORM实现。
好,咱们进入正题。这一章聊数据库模型设计,说白了就是给整个OTA平台搭骨架。骨架搭歪了,后面写再多业务代码也是白搭。我见过不少项目,前期图省事把模型揉在一起,结果后期升级任务一多,查询慢得像蜗牛爬,改都改不动。
今天咱们先搞定四个核心模型:用户模型、设备模型、固件模型、升级任务模型。我会先画ER图,再给Django ORM实现。你跟着走一遍,后面写代码心里就有底了。
4.1 用户模型:谁在用这个平台?
用户模型其实不复杂。但有个坑——千万别直接用Django自带的User模型就完事了。为什么?因为OTA平台里,用户不光是人,还可能是API调用方(比如自动化测试脚本)。
我个人习惯的做法是:继承AbstractUser,加一个user_type字段,区分「管理员」和「普通用户」。另外,OTA平台经常需要记录操作日志,所以加个last_login_ip字段也挺实用。
ER图核心字段:
- id (主键)
- username (用户名)
- password (密码哈希)
- email (邮箱)
- user_type (用户类型: admin / normal)
- last_login_ip (最后登录IP)
- created_at (创建时间)
- updated_at (更新时间)
Django ORM实现如下:
from django.contrib.auth.models import AbstractUser
from django.db import models
class User(AbstractUser):
USER_TYPE_CHOICES = (
('admin', '管理员'),
('normal', '普通用户'),
)
user_type = models.CharField(
max_length=20, choices=USER_TYPE_CHOICES, default='normal',
verbose_name='用户类型'
)
last_login_ip = models.GenericIPAddressField(
blank=True, null=True, verbose_name='最后登录IP'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'ota_user'
verbose_name = '用户'
verbose_name_plural = '用户'
def __str__(self):
return self.username
小提示:我建议把db_table明确指定一下,比如'ota_user'。这样以后数据库迁移或者直接查库时,一眼就能看出是OTA模块的表,不会跟其他业务表搞混。
4.2 设备模型:每一台设备都得管起来
设备模型是OTA的核心。你想想看,一台设备从出厂到报废,可能要经历几十次升级。所以设备模型不光要记录设备本身的信息,还得记录它当前在哪个版本、上次升级是什么时候。
我在项目中遇到过一个问题:一开始只记录了设备型号和当前固件版本,结果排查故障时发现,根本不知道设备上次升级是成功还是失败。后来我加了一个last_upgrade_status字段,问题迎刃而解。
ER图核心字段:
- id (主键)
- device_id (设备唯一标识,比如IMEI或MAC)
- device_name (设备名称)
- device_type (设备型号)
- current_firmware_version (当前固件版本)
- last_upgrade_status (上次升级状态: success/failed/pending)
- last_upgrade_time (上次升级时间)
- owner (外键 -> User)
- created_at (创建时间)
- updated_at (更新时间)
Django ORM实现:
class Device(models.Model):
UPGRADE_STATUS_CHOICES = (
('success', '升级成功'),
('failed', '升级失败'),
('pending', '等待升级'),
)
device_id = models.CharField(
max_length=128, unique=True, verbose_name='设备唯一标识'
)
device_name = models.CharField(
max_length=128, blank=True, verbose_name='设备名称'
)
device_type = models.CharField(
max_length=64, verbose_name='设备型号'
)
current_firmware_version = models.CharField(
max_length=64, blank=True, verbose_name='当前固件版本'
)
last_upgrade_status = models.CharField(
max_length=20, choices=UPGRADE_STATUS_CHOICES,
default='pending', verbose_name='上次升级状态'
)
last_upgrade_time = models.DateTimeField(
blank=True, null=True, verbose_name='上次升级时间'
)
owner = models.ForeignKey(
User, on_delete=models.CASCADE,
related_name='devices', verbose_name='所属用户'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'ota_device'
verbose_name = '设备'
verbose_name_plural = '设备'
def __str__(self):
return f'{self.device_name}({self.device_id})'
注意:device_id字段一定要加unique=True。我曾经见过一个项目,因为没加唯一约束,结果同一个设备被重复注册了好几次,升级任务发出去后,设备收到好几份一样的固件包,直接崩溃了。这种低级错误,咱们不能犯。
4.3 固件模型:升级包得有「身份证」
固件模型相对简单,但有个细节容易被忽略——固件版本号。很多人直接用字符串存,比如"v1.0.1"。但这样排序和比较就很麻烦。我建议用三个整数字段分别存主版本、次版本、修订号,或者用Django的django-versionfield。
另外,固件文件本身怎么存?小项目可以存本地路径,但生产环境我强烈建议用对象存储(比如OSS、S3)。模型里存一个file_url字段就够了。
ER图核心字段:
- id (主键)
- firmware_name (固件名称)
- version_major (主版本号)
- version_minor (次版本号)
- version_patch (修订号)
- file_url (固件文件地址)
- file_size (文件大小,单位字节)
- md5 (文件MD5校验值)
- description (版本说明)
- uploader (外键 -> User)
- created_at (创建时间)
- updated_at (更新时间)
Django ORM实现:
class Firmware(models.Model):
firmware_name = models.CharField(
max_length=128, verbose_name='固件名称'
)
version_major = models.IntegerField(default=0, verbose_name='主版本号')
version_minor = models.IntegerField(default=0, verbose_name='次版本号')
version_patch = models.IntegerField(default=0, verbose_name='修订号')
file_url = models.URLField(
max_length=512, verbose_name='固件文件地址'
)
file_size = models.BigIntegerField(
default=0, verbose_name='文件大小(字节)'
)
md5 = models.CharField(
max_length=32, verbose_name='文件MD5'
)
description = models.TextField(
blank=True, verbose_name='版本说明'
)
uploader = models.ForeignKey(
User, on_delete=models.SET_NULL,
null=True, related_name='uploaded_firmwares',
verbose_name='上传者'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'ota_firmware'
verbose_name = '固件'
verbose_name_plural = '固件'
# 联合唯一约束:同一固件名称下不能有重复版本号
unique_together = ('firmware_name', 'version_major', 'version_minor', 'version_patch')
def __str__(self):
return f'{self.firmware_name} v{self.version_major}.{self.version_minor}.{self.version_patch}'
@property
def version_str(self):
return f'v{self.version_major}.{self.version_minor}.{self.version_patch}'
避坑指南:我曾经在项目里忘了加unique_together约束,结果上传了两个版本号一样的固件包。设备升级时不知道该用哪个,直接报错。所以,版本号唯一性一定要在数据库层面就卡死,别指望业务代码去判断。
4.4 升级任务模型:把设备、固件、用户串起来
升级任务模型是整个平台的「调度中心」。它要回答三个问题:给谁升?升到什么版本?什么时候升?
这里有个设计取舍:任务是一次性的,还是可以重复执行的?我个人建议设计成一次性的。每次升级都生成一条新任务记录,这样便于追溯历史。如果要做定时升级,可以在任务上加一个scheduled_time字段。
ER图核心字段:
- id (主键)
- task_name (任务名称)
- target_device (外键 -> Device)
- target_firmware (外键 -> Firmware)
- status (任务状态: pending/running/success/failed)
- progress (升级进度,0-100)
- error_message (错误信息)
- scheduled_time (计划执行时间)
- started_time (实际开始时间)
- finished_time (实际完成时间)
- creator (外键 -> User)
- created_at (创建时间)
- updated_at (更新时间)
Django ORM实现:
class UpgradeTask(models.Model):
TASK_STATUS_CHOICES = (
('pending', '等待执行'),
('running', '正在升级'),
('success', '升级成功'),
('failed', '升级失败'),
)
task_name = models.CharField(
max_length=128, verbose_name='任务名称'
)
target_device = models.ForeignKey(
Device, on_delete=models.CASCADE,
related_name='upgrade_tasks', verbose_name='目标设备'
)
target_firmware = models.ForeignKey(
Firmware, on_delete=models.CASCADE,
related_name='upgrade_tasks', verbose_name='目标固件'
)
status = models.CharField(
max_length=20, choices=TASK_STATUS_CHOICES,
default='pending', verbose_name='任务状态'
)
progress = models.IntegerField(
default=0, verbose_name='升级进度(%)'
)
error_message = models.TextField(
blank=True, verbose_name='错误信息'
)
scheduled_time = models.DateTimeField(
blank=True, null=True, verbose_name='计划执行时间'
)
started_time = models.DateTimeField(
blank=True, null=True, verbose_name='实际开始时间'
)
finished_time = models.DateTimeField(
blank=True, null=True, verbose_name='实际完成时间'
)
creator = models.ForeignKey(
User, on_delete=models.SET_NULL,
null=True, related_name='created_tasks',
verbose_name='创建者'
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
class Meta:
db_table = 'ota_upgrade_task'
verbose_name = '升级任务'
verbose_name_plural = '升级任务'
# 按创建时间降序排列,方便查看最新任务
ordering = ['-created_at']
def __str__(self):
return f'{self.task_name} - {self.target_device.device_name}'
注意:target_device和target_firmware都用CASCADE级联删除。这意味着如果删除了某个设备或固件,关联的升级任务也会被删掉。实际项目中,我建议改成SET_NULL,保留历史记录。但这里为了演示清晰,先用CASCADE。
4.5 模型之间的关系总结
四个模型的关系其实很清晰:
- User 1:N Device —— 一个用户可以有多个设备
- User 1:N Firmware —— 一个用户可以上传多个固件
- User 1:N UpgradeTask —— 一个用户可以创建多个升级任务
- Device 1:N UpgradeTask —— 一个设备可以有多次升级记录
- Firmware 1:N UpgradeTask —— 一个固件可以被多个升级任务使用
说白了,升级任务就是一张「关联表」,把设备、固件、用户三者串在一起,再加上状态和时间信息。设计好了,后面写业务逻辑就顺了。
嗯,这一章先到这里。下一章咱们继续聊剩下的模型,比如升级策略模型、日志模型,还有怎么用Django的迁移工具把这些模型同步到数据库里。到时候我会分享一些我在生产环境踩过的坑,保证有用。