Initial commit: 视频主设备管理系统 - 完整DRF后端项目
This commit is contained in:
0
device_management/__init__.py
Normal file
0
device_management/__init__.py
Normal file
65
device_management/admin.py
Normal file
65
device_management/admin.py
Normal file
@@ -0,0 +1,65 @@
|
||||
from django.contrib import admin
|
||||
from .models import (
|
||||
Device, DeviceSerial, DeviceIP, MaintenanceRecord, DeviceAttachment
|
||||
)
|
||||
|
||||
|
||||
class DeviceSerialInline(admin.TabularInline):
|
||||
model = DeviceSerial
|
||||
extra = 1
|
||||
|
||||
|
||||
class DeviceIPInline(admin.TabularInline):
|
||||
model = DeviceIP
|
||||
extra = 1
|
||||
|
||||
|
||||
class MaintenanceRecordInline(admin.TabularInline):
|
||||
model = MaintenanceRecord
|
||||
extra = 1
|
||||
|
||||
|
||||
class DeviceAttachmentInline(admin.TabularInline):
|
||||
model = DeviceAttachment
|
||||
extra = 1
|
||||
|
||||
|
||||
@admin.register(Device)
|
||||
class DeviceAdmin(admin.ModelAdmin):
|
||||
list_display = [
|
||||
'id', 'device_name', 'location', 'building', 'status',
|
||||
'responsible_person', 'enable_date', 'service_duration_days'
|
||||
]
|
||||
list_filter = ['status', 'location', 'building']
|
||||
search_fields = ['device_name', 'location', 'brand', 'model']
|
||||
inlines = [DeviceSerialInline, DeviceIPInline, MaintenanceRecordInline, DeviceAttachmentInline]
|
||||
date_hierarchy = 'enable_date'
|
||||
|
||||
|
||||
@admin.register(DeviceSerial)
|
||||
class DeviceSerialAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'serial_number', 'serial_type', 'is_primary']
|
||||
list_filter = ['serial_type', 'is_primary']
|
||||
search_fields = ['serial_number']
|
||||
|
||||
|
||||
@admin.register(DeviceIP)
|
||||
class DeviceIPAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'ip_address', 'ip_type', 'is_primary']
|
||||
list_filter = ['ip_type', 'is_primary']
|
||||
search_fields = ['ip_address']
|
||||
|
||||
|
||||
@admin.register(MaintenanceRecord)
|
||||
class MaintenanceRecordAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'maintenance_date', 'maintenance_by', 'cost']
|
||||
list_filter = ['maintenance_date', 'maintenance_by']
|
||||
search_fields = ['fault_description', 'repair_content']
|
||||
date_hierarchy = 'maintenance_date'
|
||||
|
||||
|
||||
@admin.register(DeviceAttachment)
|
||||
class DeviceAttachmentAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'device', 'file_name', 'file_type', 'uploaded_at']
|
||||
list_filter = ['file_type', 'uploaded_at']
|
||||
search_fields = ['file_name']
|
||||
6
device_management/apps.py
Normal file
6
device_management/apps.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class DeviceManagementConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'device_management'
|
||||
120
device_management/migrations/0001_initial.py
Normal file
120
device_management/migrations/0001_initial.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# Generated by Django 5.2.14 on 2026-05-18 09:21
|
||||
|
||||
import device_management.models
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Device',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('location', models.CharField(max_length=100, verbose_name='地点')),
|
||||
('building', models.CharField(blank=True, max_length=50, null=True, verbose_name='楼栋')),
|
||||
('floor', models.CharField(blank=True, max_length=20, null=True, verbose_name='楼层')),
|
||||
('cabinet', models.CharField(blank=True, max_length=50, null=True, verbose_name='机柜')),
|
||||
('device_name', models.CharField(max_length=100, verbose_name='设备名称')),
|
||||
('model', models.CharField(blank=True, max_length=100, null=True, verbose_name='型号')),
|
||||
('brand', models.CharField(blank=True, max_length=100, null=True, verbose_name='品牌')),
|
||||
('mac_address', models.CharField(blank=True, max_length=17, null=True, verbose_name='MAC地址')),
|
||||
('enable_date', models.DateField(blank=True, null=True, verbose_name='启用日期')),
|
||||
('last_inspection_date', models.DateField(blank=True, null=True, verbose_name='最后巡检日期')),
|
||||
('thumbnail', models.ImageField(blank=True, null=True, upload_to=device_management.models.device_thumbnail_path, verbose_name='缩略图')),
|
||||
('status', models.CharField(choices=[('normal', '正常'), ('warning', '告警'), ('offline', '离线'), ('repair', '维修中'), ('scrap', '已报废')], default='normal', max_length=20, verbose_name='状态')),
|
||||
('responsible_person', models.CharField(blank=True, max_length=50, null=True, verbose_name='负责人')),
|
||||
('warranty_expire', models.DateField(blank=True, null=True, verbose_name='保修到期日期')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备',
|
||||
'verbose_name_plural': '设备',
|
||||
'db_table': 'device',
|
||||
'indexes': [models.Index(fields=['location', 'building'], name='device_locatio_6a4f0a_idx'), models.Index(fields=['status'], name='device_status_6321a6_idx')],
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceAttachment',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('file', models.FileField(upload_to=device_management.models.device_attachment_path, verbose_name='文件')),
|
||||
('file_name', models.CharField(max_length=255, verbose_name='文件名')),
|
||||
('file_type', models.CharField(blank=True, max_length=50, null=True, verbose_name='文件类型')),
|
||||
('uploaded_at', models.DateTimeField(auto_now_add=True, verbose_name='上传时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attachments', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备附件',
|
||||
'verbose_name_plural': '设备附件',
|
||||
'db_table': 'device_attachment',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceIP',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('ip_address', models.CharField(max_length=45, unique=True, verbose_name='IP地址')),
|
||||
('ip_type', models.CharField(default='management', max_length=20, verbose_name='IP类型')),
|
||||
('is_primary', models.BooleanField(default=False, verbose_name='是否主IP')),
|
||||
('subnet_mask', models.CharField(blank=True, max_length=15, null=True, verbose_name='子网掩码')),
|
||||
('gateway', models.CharField(blank=True, max_length=45, null=True, verbose_name='网关')),
|
||||
('vlan_id', models.CharField(blank=True, max_length=10, null=True, verbose_name='VLAN ID')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ips', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备IP地址',
|
||||
'verbose_name_plural': '设备IP地址',
|
||||
'db_table': 'device_ip',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DeviceSerial',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('serial_number', models.CharField(max_length=100, unique=True, verbose_name='序列号')),
|
||||
('serial_type', models.CharField(default='main', max_length=50, verbose_name='序列号类型')),
|
||||
('is_primary', models.BooleanField(default=False, verbose_name='是否主序列号')),
|
||||
('remark', models.CharField(blank=True, max_length=255, null=True, verbose_name='备注')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='serials', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '设备序列号',
|
||||
'verbose_name_plural': '设备序列号',
|
||||
'db_table': 'device_serial',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='MaintenanceRecord',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('maintenance_date', models.DateField(verbose_name='维修日期')),
|
||||
('fault_description', models.TextField(blank=True, null=True, verbose_name='故障描述')),
|
||||
('repair_content', models.TextField(blank=True, null=True, verbose_name='维修内容')),
|
||||
('replaced_parts', models.CharField(blank=True, max_length=255, null=True, verbose_name='更换配件')),
|
||||
('maintenance_by', models.CharField(blank=True, max_length=50, null=True, verbose_name='维修人')),
|
||||
('cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='费用')),
|
||||
('remark', models.TextField(blank=True, null=True, verbose_name='备注')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
('device', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='maintenance_records', to='device_management.device', verbose_name='设备')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '维修记录',
|
||||
'verbose_name_plural': '维修记录',
|
||||
'db_table': 'maintenance_record',
|
||||
'ordering': ['-maintenance_date'],
|
||||
},
|
||||
),
|
||||
]
|
||||
0
device_management/migrations/__init__.py
Normal file
0
device_management/migrations/__init__.py
Normal file
170
device_management/models.py
Normal file
170
device_management/models.py
Normal file
@@ -0,0 +1,170 @@
|
||||
import os
|
||||
from datetime import date
|
||||
from django.db import models
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from PIL import Image
|
||||
from io import BytesIO
|
||||
from django.core.files.base import ContentFile
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class DeviceStatus(models.TextChoices):
|
||||
NORMAL = 'normal', _('正常')
|
||||
WARNING = 'warning', _('告警')
|
||||
OFFLINE = 'offline', _('离线')
|
||||
REPAIR = 'repair', _('维修中')
|
||||
SCRAP = 'scrap', _('已报废')
|
||||
|
||||
|
||||
def device_attachment_path(instance, filename):
|
||||
return f'devices/{instance.id}/attachments/{filename}'
|
||||
|
||||
|
||||
def device_thumbnail_path(instance, filename):
|
||||
return f'devices/{instance.id}/thumbnails/{filename}'
|
||||
|
||||
|
||||
class Device(models.Model):
|
||||
location = models.CharField(max_length=100, verbose_name='地点')
|
||||
building = models.CharField(max_length=50, blank=True, null=True, verbose_name='楼栋')
|
||||
floor = models.CharField(max_length=20, blank=True, null=True, verbose_name='楼层')
|
||||
cabinet = models.CharField(max_length=50, blank=True, null=True, verbose_name='机柜')
|
||||
device_name = models.CharField(max_length=100, verbose_name='设备名称')
|
||||
model = models.CharField(max_length=100, blank=True, null=True, verbose_name='型号')
|
||||
brand = models.CharField(max_length=100, blank=True, null=True, verbose_name='品牌')
|
||||
mac_address = models.CharField(max_length=17, blank=True, null=True, verbose_name='MAC地址')
|
||||
enable_date = models.DateField(blank=True, null=True, verbose_name='启用日期')
|
||||
last_inspection_date = models.DateField(blank=True, null=True, verbose_name='最后巡检日期')
|
||||
thumbnail = models.ImageField(upload_to=device_thumbnail_path, blank=True, null=True, verbose_name='缩略图')
|
||||
status = models.CharField(
|
||||
max_length=20,
|
||||
choices=DeviceStatus.choices,
|
||||
default=DeviceStatus.NORMAL,
|
||||
verbose_name='状态'
|
||||
)
|
||||
responsible_person = models.CharField(max_length=50, blank=True, null=True, verbose_name='负责人')
|
||||
warranty_expire = models.DateField(blank=True, null=True, 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 = 'device'
|
||||
verbose_name = '设备'
|
||||
verbose_name_plural = '设备'
|
||||
indexes = [
|
||||
models.Index(fields=['location', 'building']),
|
||||
models.Index(fields=['status']),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.device_name} - {self.location}'
|
||||
|
||||
@property
|
||||
def service_duration_days(self):
|
||||
if self.enable_date:
|
||||
return (date.today() - self.enable_date).days
|
||||
return 0
|
||||
|
||||
@property
|
||||
def is_warranty_expired(self):
|
||||
if self.warranty_expire:
|
||||
return date.today() > self.warranty_expire
|
||||
return False
|
||||
|
||||
def generate_thumbnail(self, image_field, size=(200, 200)):
|
||||
if not image_field:
|
||||
return
|
||||
|
||||
try:
|
||||
img = Image.open(image_field)
|
||||
if img.mode in ('RGBA', 'P'):
|
||||
img = img.convert('RGB')
|
||||
|
||||
img.thumbnail(size)
|
||||
thumb_io = BytesIO()
|
||||
img.save(thumb_io, format='JPEG', quality=85)
|
||||
|
||||
thumb_name = f'thumb_{os.path.basename(image_field.name)}'
|
||||
self.thumbnail.save(thumb_name, ContentFile(thumb_io.getvalue()), save=False)
|
||||
logger.info(f'Generated thumbnail for device {self.id}')
|
||||
except Exception as e:
|
||||
logger.error(f'Failed to generate thumbnail: {e}')
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
|
||||
class DeviceSerial(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='serials', verbose_name='设备')
|
||||
serial_number = models.CharField(max_length=100, unique=True, verbose_name='序列号')
|
||||
serial_type = models.CharField(max_length=50, default='main', verbose_name='序列号类型')
|
||||
is_primary = models.BooleanField(default=False, verbose_name='是否主序列号')
|
||||
remark = models.CharField(max_length=255, blank=True, null=True, 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 = 'device_serial'
|
||||
verbose_name = '设备序列号'
|
||||
verbose_name_plural = '设备序列号'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.serial_number} ({self.serial_type})'
|
||||
|
||||
|
||||
class DeviceIP(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='ips', verbose_name='设备')
|
||||
ip_address = models.CharField(max_length=45, unique=True, verbose_name='IP地址')
|
||||
ip_type = models.CharField(max_length=20, default='management', verbose_name='IP类型')
|
||||
is_primary = models.BooleanField(default=False, verbose_name='是否主IP')
|
||||
subnet_mask = models.CharField(max_length=15, blank=True, null=True, verbose_name='子网掩码')
|
||||
gateway = models.CharField(max_length=45, blank=True, null=True, verbose_name='网关')
|
||||
vlan_id = models.CharField(max_length=10, blank=True, null=True, verbose_name='VLAN ID')
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name='更新时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'device_ip'
|
||||
verbose_name = '设备IP地址'
|
||||
verbose_name_plural = '设备IP地址'
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.ip_address} ({self.ip_type})'
|
||||
|
||||
|
||||
class MaintenanceRecord(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='maintenance_records', verbose_name='设备')
|
||||
maintenance_date = models.DateField(verbose_name='维修日期')
|
||||
fault_description = models.TextField(blank=True, null=True, verbose_name='故障描述')
|
||||
repair_content = models.TextField(blank=True, null=True, verbose_name='维修内容')
|
||||
replaced_parts = models.CharField(max_length=255, blank=True, null=True, verbose_name='更换配件')
|
||||
maintenance_by = models.CharField(max_length=50, blank=True, null=True, verbose_name='维修人')
|
||||
cost = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True, verbose_name='费用')
|
||||
remark = models.TextField(blank=True, null=True, 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 = 'maintenance_record'
|
||||
verbose_name = '维修记录'
|
||||
verbose_name_plural = '维修记录'
|
||||
ordering = ['-maintenance_date']
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.device.device_name} - {self.maintenance_date}'
|
||||
|
||||
|
||||
class DeviceAttachment(models.Model):
|
||||
device = models.ForeignKey(Device, on_delete=models.CASCADE, related_name='attachments', verbose_name='设备')
|
||||
file = models.FileField(upload_to=device_attachment_path, verbose_name='文件')
|
||||
file_name = models.CharField(max_length=255, verbose_name='文件名')
|
||||
file_type = models.CharField(max_length=50, blank=True, null=True, verbose_name='文件类型')
|
||||
uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name='上传时间')
|
||||
|
||||
class Meta:
|
||||
db_table = 'device_attachment'
|
||||
verbose_name = '设备附件'
|
||||
verbose_name_plural = '设备附件'
|
||||
|
||||
def __str__(self):
|
||||
return self.file_name
|
||||
138
device_management/serializers.py
Normal file
138
device_management/serializers.py
Normal file
@@ -0,0 +1,138 @@
|
||||
from rest_framework import serializers
|
||||
from django.db import transaction
|
||||
from .models import (
|
||||
Device, DeviceSerial, DeviceIP, MaintenanceRecord, DeviceAttachment
|
||||
)
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class DeviceSerialSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceSerial
|
||||
fields = ['id', 'serial_number', 'serial_type', 'is_primary', 'remark']
|
||||
|
||||
|
||||
class DeviceIPSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceIP
|
||||
fields = ['id', 'ip_address', 'ip_type', 'is_primary', 'subnet_mask', 'gateway', 'vlan_id']
|
||||
|
||||
|
||||
class MaintenanceRecordSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = MaintenanceRecord
|
||||
fields = ['id', 'maintenance_date', 'fault_description', 'repair_content',
|
||||
'replaced_parts', 'maintenance_by', 'cost', 'remark']
|
||||
|
||||
|
||||
class DeviceAttachmentSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = DeviceAttachment
|
||||
fields = ['id', 'file', 'file_name', 'file_type', 'uploaded_at']
|
||||
read_only_fields = ['uploaded_at']
|
||||
|
||||
|
||||
class DeviceSerializer(serializers.ModelSerializer):
|
||||
serials = DeviceSerialSerializer(many=True, required=False)
|
||||
ips = DeviceIPSerializer(many=True, required=False)
|
||||
maintenance_records = MaintenanceRecordSerializer(many=True, read_only=True)
|
||||
attachments = DeviceAttachmentSerializer(many=True, read_only=True)
|
||||
service_duration_days = serializers.IntegerField(read_only=True)
|
||||
is_warranty_expired = serializers.BooleanField(read_only=True)
|
||||
primary_serial = serializers.SerializerMethodField()
|
||||
primary_ip = serializers.SerializerMethodField()
|
||||
latest_maintenance = serializers.SerializerMethodField()
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'location', 'building', 'floor', 'cabinet', 'device_name',
|
||||
'model', 'brand', 'mac_address', 'enable_date', 'last_inspection_date',
|
||||
'thumbnail', 'status', 'responsible_person', 'warranty_expire',
|
||||
'service_duration_days', 'is_warranty_expired', 'primary_serial',
|
||||
'primary_ip', 'latest_maintenance', 'serials', 'ips',
|
||||
'maintenance_records', 'attachments', 'created_at', 'updated_at']
|
||||
read_only_fields = ['created_at', 'updated_at']
|
||||
|
||||
def get_primary_serial(self, obj):
|
||||
primary = obj.serials.filter(is_primary=True).first()
|
||||
if primary:
|
||||
return DeviceSerialSerializer(primary).data
|
||||
return None
|
||||
|
||||
def get_primary_ip(self, obj):
|
||||
primary = obj.ips.filter(is_primary=True).first()
|
||||
if primary:
|
||||
return DeviceIPSerializer(primary).data
|
||||
return None
|
||||
|
||||
def get_latest_maintenance(self, obj):
|
||||
latest = obj.maintenance_records.first()
|
||||
if latest:
|
||||
return MaintenanceRecordSerializer(latest).data
|
||||
return None
|
||||
|
||||
@transaction.atomic
|
||||
def create(self, validated_data):
|
||||
serials_data = validated_data.pop('serials', [])
|
||||
ips_data = validated_data.pop('ips', [])
|
||||
|
||||
device = Device.objects.create(**validated_data)
|
||||
|
||||
for serial_data in serials_data:
|
||||
DeviceSerial.objects.create(device=device, **serial_data)
|
||||
|
||||
for ip_data in ips_data:
|
||||
DeviceIP.objects.create(device=device, **ip_data)
|
||||
|
||||
logger.info(f'Created device: {device.id} - {device.device_name}')
|
||||
return device
|
||||
|
||||
@transaction.atomic
|
||||
def update(self, instance, validated_data):
|
||||
serials_data = validated_data.pop('serials', None)
|
||||
ips_data = validated_data.pop('ips', None)
|
||||
|
||||
for attr, value in validated_data.items():
|
||||
setattr(instance, attr, value)
|
||||
instance.save()
|
||||
|
||||
if serials_data is not None:
|
||||
instance.serials.all().delete()
|
||||
for serial_data in serials_data:
|
||||
DeviceSerial.objects.create(device=instance, **serial_data)
|
||||
|
||||
if ips_data is not None:
|
||||
instance.ips.all().delete()
|
||||
for ip_data in ips_data:
|
||||
DeviceIP.objects.create(device=instance, **ip_data)
|
||||
|
||||
logger.info(f'Updated device: {instance.id}')
|
||||
return instance
|
||||
|
||||
|
||||
class DeviceListSerializer(serializers.ModelSerializer):
|
||||
primary_serial_number = serializers.SerializerMethodField()
|
||||
primary_ip_address = serializers.SerializerMethodField()
|
||||
latest_maintenance_summary = serializers.SerializerMethodField()
|
||||
service_duration_days = serializers.IntegerField(read_only=True)
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['id', 'location', 'building', 'device_name', 'model', 'brand',
|
||||
'status', 'responsible_person', 'enable_date', 'thumbnail',
|
||||
'primary_serial_number', 'primary_ip_address',
|
||||
'latest_maintenance_summary', 'service_duration_days']
|
||||
|
||||
def get_primary_serial_number(self, obj):
|
||||
primary = obj.serials.filter(is_primary=True).first()
|
||||
return primary.serial_number if primary else None
|
||||
|
||||
def get_primary_ip_address(self, obj):
|
||||
primary = obj.ips.filter(is_primary=True).first()
|
||||
return primary.ip_address if primary else None
|
||||
|
||||
def get_latest_maintenance_summary(self, obj):
|
||||
latest = obj.maintenance_records.first()
|
||||
if latest:
|
||||
return f"{latest.maintenance_date}: {latest.fault_description[:50]}..." if latest.fault_description else str(latest.maintenance_date)
|
||||
return None
|
||||
3
device_management/tests.py
Normal file
3
device_management/tests.py
Normal file
@@ -0,0 +1,3 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
17
device_management/urls.py
Normal file
17
device_management/urls.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.urls import path, include
|
||||
from rest_framework.routers import DefaultRouter
|
||||
from .views import (
|
||||
DeviceViewSet, DeviceSerialViewSet, DeviceIPViewSet,
|
||||
MaintenanceRecordViewSet, DeviceAttachmentViewSet
|
||||
)
|
||||
|
||||
router = DefaultRouter()
|
||||
router.register(r'devices', DeviceViewSet, basename='device')
|
||||
router.register(r'serials', DeviceSerialViewSet, basename='deviceserial')
|
||||
router.register(r'ips', DeviceIPViewSet, basename='deviceip')
|
||||
router.register(r'maintenance', MaintenanceRecordViewSet, basename='maintenancerecord')
|
||||
router.register(r'attachments', DeviceAttachmentViewSet, basename='deviceattachment')
|
||||
|
||||
urlpatterns = [
|
||||
path('', include(router.urls)),
|
||||
]
|
||||
210
device_management/views.py
Normal file
210
device_management/views.py
Normal file
@@ -0,0 +1,210 @@
|
||||
import os
|
||||
from datetime import date
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework import viewsets, status, parsers, renderers
|
||||
from rest_framework.decorators import action, api_view, parser_classes
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.permissions import IsAuthenticatedOrReadOnly, IsAuthenticated
|
||||
from openpyxl import Workbook, load_workbook
|
||||
from openpyxl.styles import Font
|
||||
import pandas as pd
|
||||
from loguru import logger
|
||||
|
||||
from .models import (
|
||||
Device, DeviceSerial, DeviceIP, MaintenanceRecord, DeviceAttachment
|
||||
)
|
||||
from .serializers import (
|
||||
DeviceSerializer, DeviceListSerializer, DeviceSerialSerializer,
|
||||
DeviceIPSerializer, MaintenanceRecordSerializer, DeviceAttachmentSerializer
|
||||
)
|
||||
|
||||
|
||||
class DeviceFilter(filters.FilterSet):
|
||||
location = filters.CharFilter(lookup_expr='icontains')
|
||||
building = filters.CharFilter(lookup_expr='icontains')
|
||||
from .models import DeviceStatus
|
||||
status = filters.ChoiceFilter(choices=DeviceStatus.choices)
|
||||
device_name = filters.CharFilter(lookup_expr='icontains')
|
||||
brand = filters.CharFilter(lookup_expr='icontains')
|
||||
enable_date_from = filters.DateFilter(field_name='enable_date', lookup_expr='gte')
|
||||
enable_date_to = filters.DateFilter(field_name='enable_date', lookup_expr='lte')
|
||||
warranty_expire_soon = filters.BooleanFilter(method='filter_warranty_expire_soon')
|
||||
|
||||
class Meta:
|
||||
model = Device
|
||||
fields = ['location', 'building', 'status', 'device_name', 'brand']
|
||||
|
||||
def filter_warranty_expire_soon(self, queryset, name, value):
|
||||
if value:
|
||||
from datetime import timedelta
|
||||
soon_date = date.today() + timedelta(days=30)
|
||||
return queryset.filter(warranty_expire__lte=soon_date, warranty_expire__gte=date.today())
|
||||
return queryset
|
||||
|
||||
|
||||
class DeviceViewSet(viewsets.ModelViewSet):
|
||||
queryset = Device.objects.all().prefetch_related('serials', 'ips', 'maintenance_records')
|
||||
serializer_class = DeviceSerializer
|
||||
filterset_class = DeviceFilter
|
||||
search_fields = ['device_name', 'location', 'building', 'brand', 'model']
|
||||
ordering_fields = ['id', 'device_name', 'location', 'enable_date', 'created_at']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
def get_serializer_class(self):
|
||||
if self.action == 'list':
|
||||
return DeviceListSerializer
|
||||
return DeviceSerializer
|
||||
|
||||
@action(detail=True, methods=['post'], parser_classes=[parsers.MultiPartParser])
|
||||
def upload_attachment(self, request, pk=None):
|
||||
device = self.get_object()
|
||||
file_obj = request.FILES.get('file')
|
||||
|
||||
if not file_obj:
|
||||
return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
file_type = request.data.get('file_type', '')
|
||||
attachment = DeviceAttachment.objects.create(
|
||||
device=device,
|
||||
file=file_obj,
|
||||
file_name=file_obj.name,
|
||||
file_type=file_type
|
||||
)
|
||||
|
||||
if file_type.startswith('image/') or file_obj.name.lower().endswith(('.png', '.jpg', '.jpeg', '.gif', '.bmp')):
|
||||
device.generate_thumbnail(file_obj)
|
||||
device.save()
|
||||
|
||||
logger.info(f'Uploaded attachment {attachment.id} for device {device.id}')
|
||||
return Response(DeviceAttachmentSerializer(attachment).data, status=status.HTTP_201_CREATED)
|
||||
|
||||
@action(detail=False, methods=['get'])
|
||||
def export_excel(self, request):
|
||||
queryset = self.filter_queryset(self.get_queryset())
|
||||
|
||||
wb = Workbook()
|
||||
ws = wb.active
|
||||
ws.title = '设备列表'
|
||||
|
||||
headers = ['ID', '地点', '楼栋', '设备名称', '型号', '品牌', '状态',
|
||||
'负责人', '启用日期', '主序列号', '主IP', '最近维修简述', '服役天数']
|
||||
for col, header in enumerate(headers, 1):
|
||||
cell = ws.cell(row=1, column=col, value=header)
|
||||
cell.font = Font(bold=True)
|
||||
|
||||
for row, device in enumerate(queryset, 2):
|
||||
primary_serial = device.serials.filter(is_primary=True).first()
|
||||
primary_ip = device.ips.filter(is_primary=True).first()
|
||||
latest_maintenance = device.maintenance_records.first()
|
||||
|
||||
ws.cell(row=row, column=1, value=device.id)
|
||||
ws.cell(row=row, column=2, value=device.location)
|
||||
ws.cell(row=row, column=3, value=device.building or '')
|
||||
ws.cell(row=row, column=4, value=device.device_name)
|
||||
ws.cell(row=row, column=5, value=device.model or '')
|
||||
ws.cell(row=row, column=6, value=device.brand or '')
|
||||
ws.cell(row=row, column=7, value=device.get_status_display())
|
||||
ws.cell(row=row, column=8, value=device.responsible_person or '')
|
||||
ws.cell(row=row, column=9, value=str(device.enable_date) if device.enable_date else '')
|
||||
ws.cell(row=row, column=10, value=primary_serial.serial_number if primary_serial else '')
|
||||
ws.cell(row=row, column=11, value=primary_ip.ip_address if primary_ip else '')
|
||||
ws.cell(row=row, column=12, value=str(latest_maintenance) if latest_maintenance else '')
|
||||
ws.cell(row=row, column=13, value=device.service_duration_days)
|
||||
|
||||
response = HttpResponse(content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
|
||||
response['Content-Disposition'] = f'attachment; filename="devices_{date.today()}.xlsx"'
|
||||
wb.save(response)
|
||||
|
||||
logger.info(f'Exported {queryset.count()} devices to Excel')
|
||||
return response
|
||||
|
||||
@action(detail=False, methods=['post'], parser_classes=[parsers.MultiPartParser])
|
||||
def import_excel(self, request):
|
||||
file_obj = request.FILES.get('file')
|
||||
if not file_obj:
|
||||
return Response({'error': 'No file provided'}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
try:
|
||||
df = pd.read_excel(file_obj)
|
||||
created_count = 0
|
||||
|
||||
for _, row in df.iterrows():
|
||||
device_data = {
|
||||
'location': str(row.get('地点', '')),
|
||||
'building': str(row.get('楼栋', '')) if pd.notna(row.get('楼栋')) else None,
|
||||
'device_name': str(row.get('设备名称', '')),
|
||||
'model': str(row.get('型号', '')) if pd.notna(row.get('型号')) else None,
|
||||
'brand': str(row.get('品牌', '')) if pd.notna(row.get('品牌')) else None,
|
||||
'status': str(row.get('状态', 'normal')) if pd.notna(row.get('状态')) else 'normal',
|
||||
'responsible_person': str(row.get('负责人', '')) if pd.notna(row.get('负责人')) else None,
|
||||
}
|
||||
|
||||
device = Device.objects.create(**device_data)
|
||||
|
||||
if pd.notna(row.get('主序列号')):
|
||||
DeviceSerial.objects.create(
|
||||
device=device,
|
||||
serial_number=str(row.get('主序列号')),
|
||||
serial_type='main',
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
if pd.notna(row.get('主IP')):
|
||||
DeviceIP.objects.create(
|
||||
device=device,
|
||||
ip_address=str(row.get('主IP')),
|
||||
ip_type='management',
|
||||
is_primary=True
|
||||
)
|
||||
|
||||
created_count += 1
|
||||
|
||||
logger.info(f'Imported {created_count} devices from Excel')
|
||||
return Response({'message': f'成功导入 {created_count} 台设备', 'count': created_count})
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f'Excel import failed: {e}')
|
||||
return Response({'error': str(e)}, status=status.HTTP_400_BAD_REQUEST)
|
||||
|
||||
|
||||
class DeviceSerialViewSet(viewsets.ModelViewSet):
|
||||
queryset = DeviceSerial.objects.all()
|
||||
serializer_class = DeviceSerialSerializer
|
||||
search_fields = ['serial_number', 'serial_type']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
|
||||
class DeviceIPViewSet(viewsets.ModelViewSet):
|
||||
queryset = DeviceIP.objects.all()
|
||||
serializer_class = DeviceIPSerializer
|
||||
search_fields = ['ip_address', 'ip_type']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
|
||||
class MaintenanceRecordViewSet(viewsets.ModelViewSet):
|
||||
queryset = MaintenanceRecord.objects.all()
|
||||
serializer_class = MaintenanceRecordSerializer
|
||||
filterset_fields = ['device_id', 'maintenance_by']
|
||||
search_fields = ['fault_description', 'repair_content', 'maintenance_by']
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
|
||||
class DeviceAttachmentViewSet(viewsets.ModelViewSet):
|
||||
queryset = DeviceAttachment.objects.all()
|
||||
serializer_class = DeviceAttachmentSerializer
|
||||
parser_classes = [parsers.MultiPartParser, parsers.FormParser]
|
||||
permission_classes = [IsAuthenticatedOrReadOnly]
|
||||
|
||||
def perform_create(self, serializer):
|
||||
device_id = self.request.data.get('device')
|
||||
device = get_object_or_404(Device, id=device_id)
|
||||
file_obj = self.request.FILES.get('file')
|
||||
|
||||
attachment = serializer.save(
|
||||
device=device,
|
||||
file_name=file_obj.name if file_obj else ''
|
||||
)
|
||||
|
||||
logger.info(f'Created attachment {attachment.id}')
|
||||
Reference in New Issue
Block a user