fix: 修复导入ID重复覆盖逻辑、Excel列顺序对齐及UI优化

This commit is contained in:
cnbugs
2026-04-29 13:48:29 +08:00
parent cb057b431e
commit 2eac2d867e
6 changed files with 107 additions and 65 deletions
+3 -3
View File
@@ -9,7 +9,7 @@
- **分类管理** — 自定义设备分类(服务器、网络设备、存储设备等)
- **变更追踪** — 资产信息修改自动记录变更历史
- **Excel 导入导出** — 批量导入(分类不存在自动创建)、筛选导出、模板下载
- **多维度搜索** — 按编号/名称/部门/使用人/位置搜索,按分类/状态/位置筛选
- **多维度搜索** — 按编号/名称/部门/维护人/位置搜索,按分类/状态/位置筛选
- **质保提醒** — 已过保/即将过保状态标识
- **用户认证** — 登录/退出,管理员权限控制
- **深色主题** — 护眼深色 UI,适合长时间使用
@@ -40,7 +40,7 @@
| 设备位置 | 机房/机柜/U位 |
| 负责人 | 设备负责人 |
| 使用部门 | 使用部门 |
| 使用人 | 实际使用人 |
| 维护人 | 实际使用人 |
| 采购日期 | 采购时间 |
| 质保到期 | 质保截止日期 |
| 备注 | 补充说明 |
@@ -122,7 +122,7 @@ DATABASES = {
## 📊 Excel 导入格式
| 资产编号 | 设备名称 | 分类 | 品牌 | 型号 | 资产面值 | 序列号 | 状态 | IP地址 | BMC地址 | 设备位置 | 负责人 | 使用部门 | 使用人 | 采购日期 | 质保到期 | 备注 |
| 资产编号 | 设备名称 | 分类 | 品牌 | 型号 | 资产面值 | 序列号 | 状态 | IP地址 | BMC地址 | 设备位置 | 负责人 | 使用部门 | 维护人 | 采购日期 | 质保到期 | 备注 |
|----------|---------|------|------|------|---------|--------|------|--------|---------|---------|--------|---------|--------|---------|---------|------|
- 分类不存在时自动创建
+52 -40
View File
@@ -6,9 +6,12 @@ from django.utils import timezone
from .models import Category
# Excel列定义
# Excel列定义(顺序与资产列表一致)
EXPORT_COLUMNS = [
('id', 'ID', 8),
('location', '机房', 20),
('cabinet', '机柜', 10),
('cabinet_position', '机柜位置', 10),
('asset_number', '资产编号', 18),
('name', '设备名称', 20),
('category', '设备分类', 12),
@@ -16,21 +19,18 @@ EXPORT_COLUMNS = [
('model', '型号', 20),
('asset_value', '资产面值', 12),
('serial_number', '序列号', 25),
('location', '机房', 20),
('cabinet', '机柜', 10),
('cabinet_position', '机柜位置', 10),
('bmc_address', 'BMC地址', 16),
('ip_address', 'IP地址', 16),
('gpu_type', '显卡类型', 15),
('gpu_count', '卡数', 6),
('responsible_person', '负责人', 10),
('department', '使用部门', 15),
('user', '维护人', 10),
('business_type', '业务类型', 15),
('status', '状态', 8),
('purchase_date', '采购日期', 12),
('warranty_expire', '质保到期', 12),
('supplier', '供应商', 15),
('responsible_person', '负责人', 10),
('department', '使用部门', 15),
('user', '使用人', 10),
('business_type', '业务类型', 15),
('status', '状态', 8),
('remark', '备注', 30),
]
@@ -106,12 +106,13 @@ def generate_import_template():
cell.border = THIN_BORDER
ws.column_dimensions[get_column_letter(col_idx)].width = width
# 示例数据行
# 示例数据行(顺序与EXPORT_COLUMNS一致)
example_data = [
'1', 'IT-2024-0001', '测试服务器', '服务器', 'Dell', 'PowerEdge R740',
'50000.00', 'ABC123456', '3楼机房A区', 'A01', 'U10-U15', '192.168.1.200',
'192.168.1.100', 'NVIDIA A100', '8', '2024-01-15', '2027-01-15', '戴尔科技',
'张三', '研发部', '李四', 'AI训练', '在用', '测试备注'
'1', '3楼机房A区', 'A01', 'U10-U15', 'IT-2024-0001', '测试服务器',
'服务器', 'Dell', 'PowerEdge R740', '50000.00', 'ABC123456',
'192.168.1.200', '192.168.1.100', 'NVIDIA A100', '8', '张三',
'研发部', '李四', 'AI训练', '在用', '2024-01-15', '2027-01-15',
'戴尔科技', '测试备注'
]
for col_idx, value in enumerate(example_data, 1):
cell = ws.cell(row=2, column=col_idx, value=value)
@@ -250,32 +251,43 @@ def import_assets_from_excel(ws, category_map, operator=None):
# 创建新记录
gpu_count_str = data.get('gpu_count', '').strip()
gpu_count = int(gpu_count_str) if gpu_count_str else None
asset = Asset.objects.create(
asset_number=asset_number,
name=data.get('name', ''),
category=category,
brand=data.get('brand', ''),
model=data.get('model', ''),
asset_value=asset_value,
serial_number=data.get('serial_number', ''),
location=data.get('location', ''),
cabinet=data.get('cabinet', ''),
cabinet_position=data.get('cabinet_position', ''),
bmc_address=bmc_address,
ip_address=ip_address,
gpu_type=data.get('gpu_type', ''),
gpu_count=gpu_count,
purchase_date=purchase_date,
warranty_expire=warranty_expire,
supplier=data.get('supplier', ''),
responsible_person=data.get('responsible_person', ''),
department=data.get('department', ''),
user=data.get('user', ''),
business_type=data.get('business_type', ''),
status=status,
remark=data.get('remark', ''),
created_by=operator,
)
# 创建参数
create_kwargs = {
'asset_number': asset_number,
'name': data.get('name', ''),
'category': category,
'brand': data.get('brand', ''),
'model': data.get('model', ''),
'asset_value': asset_value,
'serial_number': data.get('serial_number', ''),
'location': data.get('location', ''),
'cabinet': data.get('cabinet', ''),
'cabinet_position': data.get('cabinet_position', ''),
'bmc_address': bmc_address,
'ip_address': ip_address,
'gpu_type': data.get('gpu_type', ''),
'gpu_count': gpu_count,
'purchase_date': purchase_date,
'warranty_expire': warranty_expire,
'supplier': data.get('supplier', ''),
'responsible_person': data.get('responsible_person', ''),
'department': data.get('department', ''),
'user': data.get('user', ''),
'business_type': data.get('business_type', ''),
'status': status,
'remark': data.get('remark', ''),
'created_by': operator,
}
# 如果Excel提供了ID,使用该ID创建
if import_id:
try:
create_kwargs['id'] = int(import_id)
except ValueError:
pass
asset = Asset.objects.create(**create_kwargs)
AssetChangeLog.objects.create(
asset=asset,
+1 -1
View File
@@ -61,7 +61,7 @@ class Asset(models.Model):
# 管理信息
responsible_person = models.CharField('负责人', max_length=50, blank=True, default='')
department = models.CharField('使用部门', max_length=100, blank=True, default='')
user = models.CharField('使用', max_length=50, blank=True, default='')
user = models.CharField('维护', max_length=50, blank=True, default='')
business_type = models.CharField('业务类型', max_length=100, blank=True, default='')
status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='in_use')
remark = models.TextField('备注', blank=True, default='')
+29 -8
View File
@@ -88,7 +88,7 @@ def dashboard(request):
def asset_list(request):
queryset = Asset.objects.select_related('category')
# 搜索
# 搜索(模糊搜索所有字段)
search = request.GET.get('search', '').strip()
if search:
queryset = queryset.filter(
@@ -96,12 +96,18 @@ def asset_list(request):
Q(name__icontains=search) |
Q(serial_number__icontains=search) |
Q(ip_address__icontains=search) |
Q(bmc_address__icontains=search) |
Q(brand__icontains=search) |
Q(model__icontains=search) |
Q(location__icontains=search) |
Q(cabinet__icontains=search) |
Q(cabinet_position__icontains=search) |
Q(responsible_person__icontains=search) |
Q(department__icontains=search) |
Q(user__icontains=search)
Q(user__icontains=search) |
Q(business_type__icontains=search) |
Q(gpu_type__icontains=search) |
Q(gpu_count__icontains=search)
)
# 筛选
@@ -113,9 +119,13 @@ def asset_list(request):
if status:
queryset = queryset.filter(status=status)
location = request.GET.get('location')
if location:
queryset = queryset.filter(location__icontains=location)
cabinet = request.GET.get('cabinet')
if cabinet:
queryset = queryset.filter(cabinet__icontains=cabinet)
business_type = request.GET.get('business_type')
if business_type:
queryset = queryset.filter(business_type=business_type)
# 排序
sort = request.GET.get('sort', 'id')
@@ -132,7 +142,16 @@ def asset_list(request):
page_obj = paginator.get_page(page_number)
categories = Category.objects.all()
locations = Asset.objects.values_list('location', flat=True).exclude(location='').distinct()
cabinets = sorted(set(
Asset.objects.values_list('cabinet', flat=True)
.exclude(cabinet='')
.exclude(cabinet=None)
))
business_types = sorted(set(
Asset.objects.values_list('business_type', flat=True)
.exclude(business_type='')
.exclude(business_type=None)
))
context = {
'page_obj': page_obj,
@@ -140,8 +159,10 @@ def asset_list(request):
'categories': categories,
'current_category': category_id,
'current_status': status,
'current_location': location,
'locations': locations,
'current_cabinet': cabinet,
'current_business_type': business_type,
'cabinets': cabinets,
'business_types': business_types,
'status_map': STATUS_MAP,
'sort': sort,
}
+1 -1
View File
@@ -61,7 +61,7 @@
<tr><td class="text-muted">卡数</td><td>{{ asset.gpu_count|default:"-" }}</td></tr>
<tr><td class="text-muted">负责人</td><td>{{ asset.responsible_person|default:"-" }}</td></tr>
<tr><td class="text-muted">使用部门</td><td>{{ asset.department|default:"-" }}</td></tr>
<tr><td class="text-muted">使用</td><td>{{ asset.user|default:"-" }}</td></tr>
<tr><td class="text-muted">维护</td><td>{{ asset.user|default:"-" }}</td></tr>
<tr><td class="text-muted">业务类型</td><td>{{ asset.business_type|default:"-" }}</td></tr>
<tr><td class="text-muted">状态</td>
<td><span class="badge
+21 -12
View File
@@ -22,10 +22,10 @@
<div class="card card-dark mb-3">
<div class="card-body">
<form method="get" class="row g-2 align-items-end">
<div class="col-md-4">
<div class="col-md-3">
<label class="form-label text-muted small">搜索</label>
<input type="text" name="search" class="form-control form-control-sm"
placeholder="编号/名称/序列号/IP/品牌/型号/位置/负责人/部门/使用人" value="{{ search }}">
placeholder="编号/名称/序列号/IP/BMC/品牌/型号/机柜/负责人/部门/维护人/业务类型..." value="{{ search }}">
</div>
<div class="col-md-2">
<label class="form-label text-muted small">分类</label>
@@ -46,16 +46,25 @@
</select>
</div>
<div class="col-md-2">
<label class="form-label text-muted small">位置</label>
<select name="location" class="form-select form-select-sm">
<option value="">全部位置</option>
{% for loc in locations %}
<option value="{{ loc }}" {% if current_location == loc %}selected{% endif %}>{{ loc }}</option>
<label class="form-label text-muted small">机柜</label>
<select name="cabinet" class="form-select form-select-sm">
<option value="">全部机柜</option>
{% for cab in cabinets %}
<option value="{{ cab }}" {% if current_cabinet == cab %}selected{% endif %}>{{ cab }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-2">
<button type="submit" class="btn btn-primary btn-sm me-1"><i class="bi bi-search"></i> 搜索</button>
<label class="form-label text-muted small">业务类型</label>
<select name="business_type" class="form-select form-select-sm">
<option value="">全部业务</option>
{% for bt in business_types %}
<option value="{{ bt }}" {% if current_business_type == bt %}selected{% endif %}>{{ bt }}</option>
{% endfor %}
</select>
</div>
<div class="col-md-1">
<button type="submit" class="btn btn-primary btn-sm me-1"><i class="bi bi-search"></i></button>
<a href="{% url 'asset_list' %}" class="btn btn-outline-secondary btn-sm"><i class="bi bi-arrow-counterclockwise"></i></a>
</div>
</form>
@@ -84,7 +93,7 @@
<th>卡数</th>
<th style="width:60px">负责人</th>
<th>使用部门</th>
<th style="width:60px">使用</th>
<th style="width:60px">维护</th>
<th>业务类型</th>
<th>状态</th>
<th>操作</th>
@@ -145,19 +154,19 @@
<nav class="mt-3">
<ul class="pagination pagination-sm justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_location %}&location={{ current_location }}{% endif %}"><i class="bi bi-chevron-left"></i></a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.previous_page_number }}{% if search %}&search={{ search }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_cabinet %}&cabinet={{ current_cabinet }}{% endif %}{% if current_business_type %}&business_type={{ current_business_type }}{% endif %}"><i class="bi bi-chevron-left"></i></a></li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active"><span class="page-link">{{ num }}</span></li>
{% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %}
<li class="page-item"><a class="page-link" href="?page={{ num }}{% if search %}&search={{ search }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_location %}&location={{ current_location }}{% endif %}">{{ num }}</a></li>
<li class="page-item"><a class="page-link" href="?page={{ num }}{% if search %}&search={{ search }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_cabinet %}&cabinet={{ current_cabinet }}{% endif %}{% if current_business_type %}&business_type={{ current_business_type }}{% endif %}">{{ num }}</a></li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_location %}&location={{ current_location }}{% endif %}"><i class="bi bi-chevron-right"></i></a></li>
<li class="page-item"><a class="page-link" href="?page={{ page_obj.next_page_number }}{% if search %}&search={{ search }}{% endif %}{% if current_category %}&category={{ current_category }}{% endif %}{% if current_status %}&status={{ current_status }}{% endif %}{% if current_cabinet %}&cabinet={{ current_cabinet }}{% endif %}{% if current_business_type %}&business_type={{ current_business_type }}{% endif %}"><i class="bi bi-chevron-right"></i></a></li>
{% endif %}
</ul>
</nav>