diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ee146d --- /dev/null +++ b/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 部署支持 diff --git a/assetapp/excel_utils.py b/assetapp/excel_utils.py index 7bf909f..81b214d 100644 --- a/assetapp/excel_utils.py +++ b/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, diff --git a/assetapp/forms.py b/assetapp/forms.py index fed44af..99a68cb 100644 --- a/assetapp/forms.py +++ b/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'}), diff --git a/assetapp/migrations/0003_alter_asset_asset_number.py b/assetapp/migrations/0003_alter_asset_asset_number.py new file mode 100644 index 0000000..6fadcbb --- /dev/null +++ b/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='资产编号'), + ), + ] diff --git a/assetapp/migrations/0004_asset_department_asset_user.py b/assetapp/migrations/0004_asset_department_asset_user.py new file mode 100644 index 0000000..d09e057 --- /dev/null +++ b/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='使用人'), + ), + ] diff --git a/assetapp/migrations/0005_asset_asset_value.py b/assetapp/migrations/0005_asset_asset_value.py new file mode 100644 index 0000000..f5227b6 --- /dev/null +++ b/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='资产面值'), + ), + ] diff --git a/assetapp/models.py b/assetapp/models.py index be16ff1..260170c 100644 --- a/assetapp/models.py +++ b/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='') diff --git a/assetapp/views.py b/assetapp/views.py index f88cc2d..97356a5 100644 --- a/assetapp/views.py +++ b/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: diff --git a/templates/assetapp/asset_detail.html b/templates/assetapp/asset_detail.html index b124f6c..c12bc86 100644 --- a/templates/assetapp/asset_detail.html +++ b/templates/assetapp/asset_detail.html @@ -39,6 +39,7 @@ 设备分类{{ asset.category.name }} 品牌{{ asset.brand|default:"-" }} 型号{{ asset.model|default:"-" }} + 资产面值{% if asset.asset_value %}¥{{ asset.asset_value }}{% else %}-{% endif %} 序列号{{ asset.serial_number|default:"-" }} @@ -57,6 +58,8 @@ BMC地址{{ asset.bmc_address|default:"-" }} IP地址{{ asset.ip_address|default:"-" }} 负责人{{ asset.responsible_person|default:"-" }} + 使用部门{{ asset.department|default:"-" }} + 使用人{{ asset.user|default:"-" }} 状态 {{ form.responsible_person.label }} {{ form.responsible_person }} +
+ + {{ form.department }} +
+
+ + {{ form.user }} +
diff --git a/templates/assetapp/asset_list.html b/templates/assetapp/asset_list.html index 0f4ccf7..dc5ca2c 100644 --- a/templates/assetapp/asset_list.html +++ b/templates/assetapp/asset_list.html @@ -25,7 +25,7 @@
+ placeholder="编号/名称/序列号/IP/品牌/型号/位置/负责人/部门/使用人" value="{{ search }}">
@@ -73,10 +73,13 @@ 设备名称 分类 品牌/型号 + 资产面值 位置 BMC地址 IP地址 负责人 + 使用部门 + 使用人 状态 操作 @@ -88,6 +91,7 @@ {{ asset.name }} {{ asset.category.name }} {{ asset.brand }} {% if asset.model %}{{ asset.model }}{% endif %} + {% if asset.asset_value %}¥{{ asset.asset_value }}{% else %}-{% endif %} {{ asset.location }} {% if asset.cabinet %} {{ asset.cabinet }}{% if asset.cabinet_position %}/{{ asset.cabinet_position }}{% endif %}{% endif %} @@ -95,6 +99,8 @@ {{ asset.bmc_address|default:"-" }} {{ asset.ip_address|default:"-" }} {{ asset.responsible_person|default:"-" }} + {{ asset.department|default:"-" }} + {{ asset.user|default:"-" }}
    {% if page_obj.has_previous %} -
  • +
  • {% endif %} {% for num in page_obj.paginator.page_range %} {% if page_obj.number == num %}
  • {{ num }}
  • {% elif num > page_obj.number|add:'-3' and num < page_obj.number|add:'3' %} -
  • {{ num }}
  • +
  • {{ num }}
  • {% endif %} {% endfor %} {% if page_obj.has_next %} -
  • +
  • {% endif %}