Преглед изворни кода

v1.0.0: 希姆计算资产管理系统正式版

cnbugs пре 1 месец
родитељ
комит
855547d70f

+ 171 - 0
README.md

@@ -0,0 +1,171 @@
+# 希姆计算资产管理管理系统
+
+基于 Django + MySQL 的企业级硬件资产管理系统,支持 Excel 批量导入导出。
+
+## ✨ 功能特性
+
+- **仪表盘** — 资产统计总览、分类统计、状态分布、最近变更记录
+- **资产管理** — 设备增删改查、资产编号/品牌/型号/序列号/资产面值等完整字段
+- **分类管理** — 自定义设备分类(服务器、网络设备、存储设备等)
+- **变更追踪** — 资产信息修改自动记录变更历史
+- **Excel 导入导出** — 批量导入(分类不存在自动创建)、筛选导出、模板下载
+- **多维度搜索** — 按编号/名称/部门/使用人/位置搜索,按分类/状态/位置筛选
+- **质保提醒** — 已过保/即将过保状态标识
+- **用户认证** — 登录/退出,管理员权限控制
+- **深色主题** — 护眼深色 UI,适合长时间使用
+
+## 🛠️ 技术栈
+
+| 组件 | 技术 |
+|------|------|
+| 后端 | Django 4.2 |
+| 数据库 | MySQL / MariaDB |
+| 前端 | Bootstrap 5 + 自定义深色主题 |
+| 部署 | Docker Compose / 直接运行 |
+
+## 📋 资产字段
+
+| 字段 | 说明 |
+|------|------|
+| 资产编号 | 自定义编号,可重复 |
+| 设备名称 | 设备名称 |
+| 分类 | 服务器/网络设备/存储设备等(支持自定义) |
+| 品牌 | 设备品牌 |
+| 型号 | 设备型号 |
+| 资产面值 | 资产价值(元) |
+| 序列号 | 设备序列号 |
+| 状态 | 在用/闲置/维修中/已报废 |
+| IP地址 | 管理IP |
+| BMC地址 | BMC管理地址 |
+| 设备位置 | 机房/机柜/U位 |
+| 负责人 | 设备负责人 |
+| 使用部门 | 使用部门 |
+| 使用人 | 实际使用人 |
+| 采购日期 | 采购时间 |
+| 质保到期 | 质保截止日期 |
+| 备注 | 补充说明 |
+
+## 🚀 快速部署
+
+### 方式一:Docker Compose(推荐)
+
+```bash
+# 克隆仓库
+git clone ssh://git@git.cnbugs.com:10022/cnbugs/asset-management.git
+cd asset-management
+
+# 启动服务
+docker-compose up -d
+
+# 访问系统
+# http://<服务器IP>:8010
+# 默认账号: admin / admin123
+```
+
+### 方式二:直接运行
+
+```bash
+# 克隆仓库
+git clone ssh://git@git.cnbugs.com:10022/cnbugs/asset-management.git
+cd asset-management
+
+# 创建虚拟环境
+python3 -m venv venv
+source venv/bin/activate
+
+# 安装依赖
+pip install -r requirements.txt
+
+# 配置数据库(修改 config/settings.py 中的 DATABASES)
+# 创建 MySQL 数据库: CREATE DATABASE asset_management CHARACTER SET utf8mb4;
+
+# 初始化
+python manage.py migrate
+python manage.py createsuperuser
+
+# 启动
+python manage.py runserver 0.0.0.0:8010
+```
+
+## ⚙️ 配置说明
+
+### 数据库配置
+
+编辑 `config/settings.py` 中的 `DATABASES`:
+
+```python
+DATABASES = {
+    'default': {
+        'ENGINE': 'django.db.backends.mysql',
+        'NAME': 'asset_management',
+        'USER': 'root',
+        'PASSWORD': 'password123',
+        'HOST': 'localhost',
+        'PORT': '3306',
+    }
+}
+```
+
+### Docker 环境变量
+
+在 `docker-compose.yml` 中配置:
+
+| 变量 | 默认值 | 说明 |
+|------|--------|------|
+| MYSQL_HOST | db | MySQL 主机 |
+| MYSQL_PORT | 3306 | MySQL 端口 |
+| MYSQL_USER | root | MySQL 用户 |
+| MYSQL_PASSWORD | password123 | MySQL 密码 |
+| MYSQL_DATABASE | asset_management | 数据库名 |
+| DJANGO_DEBUG | False | 调试模式 |
+| DJANGO_SECRET_KEY | your-secret-key... | Django 密钥 |
+
+## 📊 Excel 导入格式
+
+| 资产编号 | 设备名称 | 分类 | 品牌 | 型号 | 资产面值 | 序列号 | 状态 | IP地址 | BMC地址 | 设备位置 | 负责人 | 使用部门 | 使用人 | 采购日期 | 质保到期 | 备注 |
+|----------|---------|------|------|------|---------|--------|------|--------|---------|---------|--------|---------|--------|---------|---------|------|
+
+- 分类不存在时自动创建
+- 资产编号允许重复
+- 日期格式:YYYY-MM-DD
+- 状态可选:在用、闲置、维修中、已报废
+
+## 📁 项目结构
+
+```
+asset-management/
+├── assetapp/                  # 主应用
+│   ├── models.py              # 数据模型
+│   ├── views.py               # 视图逻辑
+│   ├── forms.py               # 表单定义
+│   ├── excel_utils.py         # Excel 导入导出工具
+│   ├── urls.py                # URL 路由
+│   ├── management/commands/   # 管理命令
+│   ├── migrations/            # 数据库迁移
+│   └── templatetags/          # 自定义模板标签
+├── config/                    # Django 配置
+│   ├── settings.py
+│   ├── urls.py
+│   └── wsgi.py
+├── templates/assetapp/        # HTML 模板
+├── static/css/                # 静态资源
+├── docker-compose.yml         # Docker 编排
+├── Dockerfile                 # 容器构建
+├── entrypoint.sh              # 启动脚本
+├── requirements.txt           # Python 依赖
+└── manage.py                  # Django 入口
+```
+
+## 📝 版本历史
+
+### v1.0.0
+
+- 资产增删改查
+- 仪表盘统计
+- 分类管理
+- 变更追踪
+- Excel 导入导出
+- 多维度搜索筛选
+- 质保到期提醒
+- 深色主题
+- Docker 部署支持

+ 17 - 8
assetapp/excel_utils.py

@@ -13,6 +13,7 @@ EXPORT_COLUMNS = [
     ('category', '设备分类', 12),
     ('brand', '品牌', 12),
     ('model', '型号', 20),
+    ('asset_value', '资产面值', 12),
     ('serial_number', '序列号', 25),
     ('location', '设备位置', 20),
     ('cabinet', '机柜', 10),
@@ -23,6 +24,8 @@ EXPORT_COLUMNS = [
     ('warranty_expire', '质保到期', 12),
     ('supplier', '供应商', 15),
     ('responsible_person', '负责人', 10),
+    ('department', '使用部门', 15),
+    ('user', '使用人', 10),
     ('status', '状态', 8),
     ('remark', '备注', 30),
 ]
@@ -100,8 +103,8 @@ def generate_import_template():
     # 示例数据行
     example_data = [
         'IT-2024-0001', '测试服务器', '服务器', 'Dell', 'PowerEdge R740',
-        'ABC123456', '3楼机房A区', 'A01', 'U10-U15', '192.168.1.200',
-        '192.168.1.100', '2024-01-15', '2027-01-15', '戴尔科技', '张三', '在用', '测试备注'
+        '50000.00', 'ABC123456', '3楼机房A区', 'A01', 'U10-U15', '192.168.1.200',
+        '192.168.1.100', '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)
@@ -147,12 +150,6 @@ def import_assets_from_excel(ws, category_map, operator=None):
                 results['errors'].append(f'第{row_idx}行: 缺少资产编号')
                 continue
 
-            # 检查重复
-            if Asset.objects.filter(asset_number=asset_number).exists():
-                results['skipped'] += 1
-                results['errors'].append(f'第{row_idx}行: 资产编号 {asset_number} 已存在')
-                continue
-
             # 处理分类 - 不存在则自动创建
             category_name = data.get('category', '').strip()
             category = category_map.get(category_name)
@@ -187,12 +184,22 @@ def import_assets_from_excel(ws, category_map, operator=None):
             bmc_address = data.get('bmc_address') or None
             ip_address = data.get('ip_address') or None
 
+            # 处理资产面值
+            from decimal import Decimal, InvalidOperation
+            asset_value = None
+            if data.get('asset_value'):
+                try:
+                    asset_value = Decimal(str(data['asset_value']).replace(',', ''))
+                except (InvalidOperation, ValueError):
+                    pass
+
             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', ''),
@@ -203,6 +210,8 @@ def import_assets_from_excel(ws, category_map, operator=None):
                 warranty_expire=warranty_expire,
                 supplier=data.get('supplier', ''),
                 responsible_person=data.get('responsible_person', ''),
+                department=data.get('department', ''),
+                user=data.get('user', ''),
                 status=status,
                 remark=data.get('remark', ''),
                 created_by=operator,

+ 2 - 2
assetapp/forms.py

@@ -6,10 +6,10 @@ class AssetForm(forms.ModelForm):
     class Meta:
         model = Asset
         fields = [
-            'asset_number', 'name', 'category', 'brand', 'model', 'serial_number',
+            'asset_number', 'name', 'category', 'brand', 'model', 'asset_value', 'serial_number',
             'location', 'cabinet', 'cabinet_position', 'bmc_address', 'ip_address',
             'purchase_date', 'warranty_expire', 'supplier',
-            'responsible_person', 'status', 'remark',
+            'responsible_person', 'department', 'user', 'status', 'remark',
         ]
         widgets = {
             'purchase_date': forms.DateInput(attrs={'type': 'date'}),

+ 18 - 0
assetapp/migrations/0003_alter_asset_asset_number.py

@@ -0,0 +1,18 @@
+# Generated by Django 5.2.13 on 2026-04-25 00:10
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('assetapp', '0002_asset_bmc_address'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='asset',
+            name='asset_number',
+            field=models.CharField(db_index=True, max_length=50, verbose_name='资产编号'),
+        ),
+    ]

+ 23 - 0
assetapp/migrations/0004_asset_department_asset_user.py

@@ -0,0 +1,23 @@
+# Generated by Django 5.2.13 on 2026-04-25 00:21
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('assetapp', '0003_alter_asset_asset_number'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='asset',
+            name='department',
+            field=models.CharField(blank=True, default='', max_length=100, verbose_name='使用部门'),
+        ),
+        migrations.AddField(
+            model_name='asset',
+            name='user',
+            field=models.CharField(blank=True, default='', max_length=50, verbose_name='使用人'),
+        ),
+    ]

+ 18 - 0
assetapp/migrations/0005_asset_asset_value.py

@@ -0,0 +1,18 @@
+# Generated by Django 5.2.13 on 2026-04-25 00:25
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('assetapp', '0004_asset_department_asset_user'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='asset',
+            name='asset_value',
+            field=models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True, verbose_name='资产面值'),
+        ),
+    ]

+ 4 - 1
assetapp/models.py

@@ -29,13 +29,14 @@ class Asset(models.Model):
     ]
 
     # 基本信息
-    asset_number = models.CharField('资产编号', max_length=50, unique=True, db_index=True)
+    asset_number = models.CharField('资产编号', max_length=50, db_index=True)
     name = models.CharField('设备名称', max_length=100)
     category = models.ForeignKey(Category, on_delete=models.PROTECT, verbose_name='设备分类', related_name='assets')
 
     # 硬件信息
     brand = models.CharField('品牌', max_length=50, blank=True, default='')
     model = models.CharField('型号', max_length=100, blank=True, default='')
+    asset_value = models.DecimalField('资产面值', max_digits=12, decimal_places=2, blank=True, null=True)
     serial_number = models.CharField('序列号', max_length=100, blank=True, default='', db_index=True)
 
     # 位置信息
@@ -54,6 +55,8 @@ 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='')
     status = models.CharField('状态', max_length=20, choices=STATUS_CHOICES, default='in_use')
     remark = models.TextField('备注', blank=True, default='')
 

+ 5 - 3
assetapp/views.py

@@ -99,7 +99,9 @@ def asset_list(request):
             Q(brand__icontains=search) |
             Q(model__icontains=search) |
             Q(location__icontains=search) |
-            Q(responsible_person__icontains=search)
+            Q(responsible_person__icontains=search) |
+            Q(department__icontains=search) |
+            Q(user__icontains=search)
         )
 
     # 筛选
@@ -116,8 +118,8 @@ def asset_list(request):
         queryset = queryset.filter(location__icontains=location)
 
     # 排序
-    sort = request.GET.get('sort', '-created_at')
-    valid_sorts = ['asset_number', '-asset_number', 'name', '-name',
+    sort = request.GET.get('sort', 'id')
+    valid_sorts = ['id', '-id', 'asset_number', '-asset_number', 'name', '-name',
                    'created_at', '-created_at', 'updated_at', '-updated_at',
                    'purchase_date', '-purchase_date']
     if sort not in valid_sorts:

+ 3 - 0
templates/assetapp/asset_detail.html

@@ -39,6 +39,7 @@
                     <tr><td class="text-muted">设备分类</td><td><span class="badge bg-secondary">{{ asset.category.name }}</span></td></tr>
                     <tr><td class="text-muted">品牌</td><td>{{ asset.brand|default:"-" }}</td></tr>
                     <tr><td class="text-muted">型号</td><td>{{ asset.model|default:"-" }}</td></tr>
+                    <tr><td class="text-muted">资产面值</td><td>{% if asset.asset_value %}¥{{ asset.asset_value }}{% else %}-{% endif %}</td></tr>
                     <tr><td class="text-muted">序列号</td><td><code>{{ asset.serial_number|default:"-" }}</code></td></tr>
                 </table>
             </div>
@@ -57,6 +58,8 @@
                     <tr><td class="text-muted">BMC地址</td><td><code>{{ asset.bmc_address|default:"-" }}</code></td></tr>
                     <tr><td class="text-muted">IP地址</td><td><code>{{ asset.ip_address|default:"-" }}</code></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><span class="badge 
                             {% if asset.status == 'in_use' %}bg-success

+ 8 - 0
templates/assetapp/asset_form.html

@@ -89,6 +89,14 @@
                         <label class="form-label text-muted">{{ form.responsible_person.label }}</label>
                         {{ form.responsible_person }}
                     </div>
+                    <div class="mb-3">
+                        <label class="form-label text-muted">{{ form.department.label }}</label>
+                        {{ form.department }}
+                    </div>
+                    <div class="mb-3">
+                        <label class="form-label text-muted">{{ form.user.label }}</label>
+                        {{ form.user }}
+                    </div>
                 </div>
             </div>
         </div>

+ 10 - 4
templates/assetapp/asset_list.html

@@ -25,7 +25,7 @@
             <div class="col-md-4">
                 <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/品牌/型号/位置/负责人/部门/使用人" value="{{ search }}">
             </div>
             <div class="col-md-2">
                 <label class="form-label text-muted small">分类</label>
@@ -73,10 +73,13 @@
                         <th>设备名称</th>
                         <th>分类</th>
                         <th>品牌/型号</th>
+                        <th>资产面值</th>
                         <th>位置</th>
                         <th>BMC地址</th>
                         <th>IP地址</th>
                         <th>负责人</th>
+                        <th>使用部门</th>
+                        <th>使用人</th>
                         <th>状态</th>
                         <th>操作</th>
                     </tr>
@@ -88,6 +91,7 @@
                         <td>{{ asset.name }}</td>
                         <td><span class="badge bg-secondary">{{ asset.category.name }}</span></td>
                         <td>{{ asset.brand }} {% if asset.model %}{{ asset.model }}{% endif %}</td>
+                        <td>{% if asset.asset_value %}¥{{ asset.asset_value }}{% else %}-{% endif %}</td>
                         <td>
                             {{ asset.location }}
                             {% if asset.cabinet %}<small class="text-muted"> {{ asset.cabinet }}{% if asset.cabinet_position %}/{{ asset.cabinet_position }}{% endif %}</small>{% endif %}
@@ -95,6 +99,8 @@
                         <td><code>{{ asset.bmc_address|default:"-" }}</code></td>
                         <td><code>{{ asset.ip_address|default:"-" }}</code></td>
                         <td>{{ asset.responsible_person|default:"-" }}</td>
+                        <td>{{ asset.department|default:"-" }}</td>
+                        <td>{{ asset.user|default:"-" }}</td>
                         <td>
                             <span class="badge 
                                 {% if asset.status == 'in_use' %}bg-success
@@ -130,19 +136,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 }}&search={{ search }}&category={{ current_category }}&status={{ current_status }}&location={{ current_location }}"><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_location %}&location={{ current_location }}{% 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 }}&search={{ search }}&category={{ current_category }}&status={{ current_status }}&location={{ current_location }}">{{ 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_location %}&location={{ current_location }}{% 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 }}&search={{ search }}&category={{ current_category }}&status={{ current_status }}&location={{ current_location }}"><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_location %}&location={{ current_location }}{% endif %}"><i class="bi bi-chevron-right"></i></a></li>
         {% endif %}
     </ul>
 </nav>