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的迁移工具把这些模型同步到数据库里。到时候我会分享一些我在生产环境踩过的坑,保证有用。