diff --git a/src/backend/finance/__init__.py b/src/backend/finance/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/backend/finance/admin.py b/src/backend/finance/admin.py
new file mode 100644
index 0000000..c84e14b
--- /dev/null
+++ b/src/backend/finance/admin.py
@@ -0,0 +1,30 @@
+from django.contrib import admin
+from .models import Department, Category, FinancialRecord, ProofImage
+
+
+@admin.register(Department)
+class DepartmentAdmin(admin.ModelAdmin):
+ list_display = ['name']
+ search_fields = ['name']
+
+
+@admin.register(Category)
+class CategoryAdmin(admin.ModelAdmin):
+ list_display = ['name']
+ search_fields = ['name']
+
+
+@admin.register(ProofImage)
+class ProofImageAdmin(admin.ModelAdmin):
+ list_display = ['financial_record', 'image', 'description', 'uploaded_at']
+ list_filter = ['uploaded_at', 'financial_record']
+ search_fields = ['description', 'financial_record__title']
+ readonly_fields = ['uploaded_at']
+
+
+@admin.register(FinancialRecord)
+class FinancialRecordAdmin(admin.ModelAdmin):
+ list_display = ['title', 'amount', 'record_type', 'transaction_date', 'department', 'category']
+ list_filter = ['record_type', 'department', 'category', 'transaction_date']
+ search_fields = ['title', 'description']
+ date_hierarchy = 'transaction_date'
diff --git a/src/backend/finance/apps.py b/src/backend/finance/apps.py
new file mode 100644
index 0000000..2fc5a78
--- /dev/null
+++ b/src/backend/finance/apps.py
@@ -0,0 +1,6 @@
+from django.apps import AppConfig
+
+
+class FinanceConfig(AppConfig):
+ default_auto_field = "django.db.models.BigAutoField"
+ name = "finance"
diff --git a/src/backend/finance/management/__init__.py b/src/backend/finance/management/__init__.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/backend/finance/management/__init__.py
@@ -0,0 +1 @@
+
diff --git a/src/backend/finance/management/commands/__init__.py b/src/backend/finance/management/commands/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/backend/finance/management/commands/init_finance_data.py b/src/backend/finance/management/commands/init_finance_data.py
new file mode 100644
index 0000000..93186ab
--- /dev/null
+++ b/src/backend/finance/management/commands/init_finance_data.py
@@ -0,0 +1,55 @@
+from django.core.management.base import BaseCommand
+from finance.models import Department, Category
+
+# 创建:python manage.py init_finance_data
+
+class Command(BaseCommand):
+ help = '初始化财务系统的部门和类别数据'
+
+ def handle(self, *args, **options):
+ # 创建部门
+ departments = [
+ '爱特工作室本部',
+ '程序部',
+ 'Web部',
+ '游戏部',
+ 'IOS部',
+ 'APP部',
+ 'UI部',
+ '智能应用部',
+ 'OpenHarmony部',
+ 'FOSS部'
+ ]
+
+ for dept_name in departments:
+ dept, created = Department.objects.get_or_create(name=dept_name)
+ if created:
+ self.stdout.write(self.style.SUCCESS(f'创建部门: {dept_name}'))
+ else:
+ self.stdout.write(f'部门已存在: {dept_name}')
+
+ # 创建类别
+ categories = [
+ '办公用品',
+ '设备采购',
+ '软件授权',
+ '差旅费',
+ '会议费',
+ '宣传费用',
+ '日用品费用',
+ '维护费用',
+ '其他支出',
+ '项目收入',
+ '服务收入',
+ '资金交接',
+ '其他收入'
+ ]
+
+ for cat_name in categories:
+ cat, created = Category.objects.get_or_create(name=cat_name)
+ if created:
+ self.stdout.write(self.style.SUCCESS(f'创建类别: {cat_name}'))
+ else:
+ self.stdout.write(f'类别已存在: {cat_name}')
+
+ self.stdout.write(self.style.SUCCESS('初始化完成!'))
diff --git a/src/backend/finance/models.py b/src/backend/finance/models.py
new file mode 100644
index 0000000..28c1c63
--- /dev/null
+++ b/src/backend/finance/models.py
@@ -0,0 +1,80 @@
+from django.db import models
+from django.contrib.auth.models import User
+
+
+def proof_image_upload_path(instance, filename):
+ """
+ 自定义文件上传路径
+ 格式: proofs/{记录ID}-{年月日}/{文件名}
+ """
+ record = instance.financial_record
+ date_str = record.transaction_date.strftime('%Y%m%d')
+ folder_name = f"{record.id}-{date_str}"
+ return f'proofs/{folder_name}/{filename}'
+
+
+class Department(models.Model):
+ """部门"""
+ name = models.CharField(max_length=100, unique=True, verbose_name="部门名称")
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name = "部门"
+ verbose_name_plural = verbose_name
+
+
+class Category(models.Model):
+ """财务记录类别"""
+ name = models.CharField(max_length=100, unique=True, verbose_name="类别名称")
+
+ def __str__(self):
+ return self.name
+
+ class Meta:
+ verbose_name = "财务类别"
+ verbose_name_plural = verbose_name
+
+
+class FinancialRecord(models.Model):
+ """财务记录"""
+ RECORD_TYPE_CHOICES = [
+ ('expense', '支出'),
+ ('income', '收入'),
+ ]
+
+ title = models.CharField(max_length=200, verbose_name="标题")
+ description = models.TextField(blank=True, null=True, verbose_name="描述")
+ amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="金额")
+ record_type = models.CharField(max_length=10, choices=RECORD_TYPE_CHOICES, verbose_name="记录类型")
+ transaction_date = models.DateField(verbose_name="交易日期")
+ department = models.ForeignKey(Department, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="所属部门")
+ category = models.ForeignKey(Category, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="类别")
+ fund_manager = models.CharField(max_length=100, blank=True, null=True, verbose_name="批准人")
+ created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="创建人")
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
+ updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
+
+ def __str__(self):
+ return f"{self.title} - {self.amount}"
+
+ class Meta:
+ verbose_name = "财务记录"
+ verbose_name_plural = verbose_name
+ ordering = ['-transaction_date']
+
+
+class ProofImage(models.Model):
+ """凭证图片"""
+ financial_record = models.ForeignKey(FinancialRecord, on_delete=models.CASCADE, related_name='proof_images', verbose_name="财务记录")
+ image = models.ImageField(upload_to=proof_image_upload_path, verbose_name="凭证图片")
+ description = models.CharField(max_length=200, blank=True, null=True, verbose_name="图片描述")
+ uploaded_at = models.DateTimeField(auto_now_add=True, verbose_name="上传时间")
+
+ def __str__(self):
+ return f"{self.financial_record.title} - 凭证{self.id}"
+
+ class Meta:
+ verbose_name = "凭证图片"
+ verbose_name_plural = verbose_name
diff --git a/src/backend/finance/serializers.py b/src/backend/finance/serializers.py
new file mode 100644
index 0000000..d91f4cb
--- /dev/null
+++ b/src/backend/finance/serializers.py
@@ -0,0 +1,48 @@
+from rest_framework import serializers
+from .models import FinancialRecord, Department, Category, ProofImage
+
+
+class DepartmentSerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Department
+ fields = '__all__'
+
+
+class CategorySerializer(serializers.ModelSerializer):
+ class Meta:
+ model = Category
+ fields = '__all__'
+
+
+class ProofImageSerializer(serializers.ModelSerializer):
+ """凭证图片序列化器"""
+ class Meta:
+ model = ProofImage
+ fields = ['id', 'image', 'description', 'uploaded_at']
+
+
+class FinancialRecordWriteSerializer(serializers.ModelSerializer):
+ """用于创建和更新财务记录的序列化器"""
+ class Meta:
+ model = FinancialRecord
+ fields = '__all__'
+
+
+class FinancialRecordReadSerializer(serializers.ModelSerializer):
+ """用于读取财务记录的序列化器"""
+ department = DepartmentSerializer(read_only=True)
+ category = CategorySerializer(read_only=True)
+ proof_images = ProofImageSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = FinancialRecord
+ fields = '__all__'
+
+
+# 保持向后兼容的默认序列化器
+class FinancialRecordSerializer(serializers.ModelSerializer):
+ proof_images = ProofImageSerializer(many=True, read_only=True)
+
+ class Meta:
+ model = FinancialRecord
+ fields = '__all__'
diff --git a/src/backend/finance/tests.py b/src/backend/finance/tests.py
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/src/backend/finance/tests.py
@@ -0,0 +1 @@
+
diff --git a/src/backend/finance/urls.py b/src/backend/finance/urls.py
new file mode 100644
index 0000000..922cbb4
--- /dev/null
+++ b/src/backend/finance/urls.py
@@ -0,0 +1,13 @@
+from django.urls import path, include
+from rest_framework.routers import DefaultRouter
+from .views import FinancialRecordViewSet, DepartmentViewSet, CategoryViewSet, ProofImageViewSet
+
+router = DefaultRouter()
+router.register(r'finance', FinancialRecordViewSet)
+router.register(r'departments', DepartmentViewSet)
+router.register(r'finance_categories', CategoryViewSet)
+router.register(r'proof-images', ProofImageViewSet)
+
+urlpatterns = [
+ path('api/', include(router.urls)),
+]
diff --git a/src/backend/finance/views.py b/src/backend/finance/views.py
new file mode 100644
index 0000000..3383d65
--- /dev/null
+++ b/src/backend/finance/views.py
@@ -0,0 +1,98 @@
+import os
+from rest_framework import viewsets, status
+from rest_framework.decorators import action
+from rest_framework.response import Response
+from .models import FinancialRecord, Department, Category, ProofImage
+from .serializers import (
+ FinancialRecordWriteSerializer,
+ FinancialRecordReadSerializer,
+ DepartmentSerializer,
+ CategorySerializer,
+ ProofImageSerializer
+)
+
+class FinancialRecordViewSet(viewsets.ModelViewSet):
+ """
+ API endpoint that allows financial records to be viewed or edited.
+ """
+ queryset = FinancialRecord.objects.all()
+
+ def get_serializer_class(self):
+ """根据操作类型返回不同的序列化器"""
+ if self.action in ['list', 'retrieve']:
+ return FinancialRecordReadSerializer
+ return FinancialRecordWriteSerializer
+
+ @action(detail=True, methods=['post'])
+ def upload_images(self, request, pk=None):
+ """为财务记录上传多张凭证图片"""
+ record = self.get_object()
+ files = request.FILES.getlist('images')
+
+ if not files:
+ return Response({'error': '没有接收到图片文件'}, status=status.HTTP_400_BAD_REQUEST)
+
+ created_images = []
+ for file in files:
+ proof_image = ProofImage.objects.create(
+ financial_record=record,
+ image=file,
+ description=request.data.get('description', '')
+ )
+ created_images.append(ProofImageSerializer(proof_image).data)
+
+ return Response({
+ 'message': f'成功上传 {len(created_images)} 张图片',
+ 'images': created_images
+ }, status=status.HTTP_201_CREATED)
+
+
+class ProofImageViewSet(viewsets.ModelViewSet):
+ """
+ API endpoint that allows proof images to be viewed or edited.
+ """
+ queryset = ProofImage.objects.all()
+ serializer_class = ProofImageSerializer
+
+ def destroy(self, request, *args, **kwargs):
+ """重写删除方法,确保删除图片记录时同时删除物理文件"""
+ instance = self.get_object()
+
+ # 获取文件路径
+ file_path = None
+ if instance.image:
+ try:
+ file_path = instance.image.path
+ except (ValueError, AttributeError):
+ # 如果文件路径无效或文件不存在,只删除数据库记录
+ pass
+
+ # 删除数据库记录
+ super().destroy(request, *args, **kwargs)
+
+ # 删除物理文件
+ if file_path and os.path.isfile(file_path):
+ try:
+ os.remove(file_path)
+ print(f"成功删除文件: {file_path}")
+ except OSError as e:
+ print(f"删除文件失败: {file_path}, 错误: {e}")
+ # 即使文件删除失败,也不抛出异常,因为数据库记录已经删除
+
+ return Response({'message': '图片删除成功'}, status=status.HTTP_200_OK)
+
+
+class DepartmentViewSet(viewsets.ModelViewSet):
+ """
+ API endpoint that allows departments to be viewed or edited.
+ """
+ queryset = Department.objects.all()
+ serializer_class = DepartmentSerializer
+
+
+class CategoryViewSet(viewsets.ModelViewSet):
+ """
+ API endpoint that allows categories to be viewed or edited.
+ """
+ queryset = Category.objects.all()
+ serializer_class = CategorySerializer
diff --git a/src/backend/item_manager/settings.py b/src/backend/item_manager/settings.py
index b37b9dc..71b09b4 100644
--- a/src/backend/item_manager/settings.py
+++ b/src/backend/item_manager/settings.py
@@ -40,6 +40,7 @@ INSTALLED_APPS = [
"rest_framework",
"corsheaders",
"items",
+ "finance",
]
MIDDLEWARE = [
@@ -141,6 +142,9 @@ USE_TZ = True
STATIC_URL = "static/"
STATIC_ROOT = BASE_DIR / "staticfiles"
+MEDIA_URL = "/media/"
+MEDIA_ROOT = BASE_DIR / "media"
+
# 安全设置(生产环境)
SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https')
SECURE_SSL_REDIRECT = False # 如果使用HTTPS,设置为True
diff --git a/src/backend/item_manager/urls.py b/src/backend/item_manager/urls.py
index 12c3fd9..949beb4 100644
--- a/src/backend/item_manager/urls.py
+++ b/src/backend/item_manager/urls.py
@@ -17,9 +17,15 @@ Including another URLconf
from django.contrib import admin
from django.urls import path, include
+from django.conf import settings
+from django.conf.urls.static import static
urlpatterns = [
path("admin/", admin.site.urls),
path("", include("items.urls")),
+ path("", include("finance.urls")),
path("api-auth/", include("rest_framework.urls")),
]
+
+if settings.DEBUG:
+ urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
\ No newline at end of file
diff --git a/src/backend/items/urls.py b/src/backend/items/urls.py
index 53636bd..b2808f0 100644
--- a/src/backend/items/urls.py
+++ b/src/backend/items/urls.py
@@ -5,7 +5,7 @@ from . import views
router = DefaultRouter()
router.register(r'items', views.ItemViewSet)
router.register(r'usages', views.ItemUsageViewSet)
-router.register(r'categories', views.CategoryViewSet)
+router.register(r'item_categories', views.CategoryViewSet)
router.register(r'users', views.UserViewSet)
urlpatterns = [
diff --git a/src/fronted/src/components/AppHeader.vue b/src/fronted/src/components/AppHeader.vue
index ba4e312..3a91c05 100644
--- a/src/fronted/src/components/AppHeader.vue
+++ b/src/fronted/src/components/AppHeader.vue
@@ -1,7 +1,7 @@
{{ record.description }} {{ image.description }} 暂无凭证图片 可通过编辑记录来添加凭证图片爱特工作室物品管理系统
+ 爱特工作室物品管理及财务管理系统
{{ dept.name }}
+
+
+
+
+
+
+
+