From 64039a90f35bf077204039a6389216e6c1d17e69 Mon Sep 17 00:00:00 2001 From: Yaosanqi137 Date: Fri, 19 Sep 2025 00:01:09 +0800 Subject: [PATCH] feat: init backend --- src/backend/item_manager/__init__.py | 0 src/backend/item_manager/asgi.py | 16 +++ src/backend/item_manager/settings.py | 147 +++++++++++++++++++++++ src/backend/item_manager/urls.py | 25 ++++ src/backend/item_manager/wsgi.py | 16 +++ src/backend/items/__init__.py | 0 src/backend/items/admin.py | 55 +++++++++ src/backend/items/apps.py | 6 + src/backend/items/migrations/__init__.py | 0 src/backend/items/models.py | 68 +++++++++++ src/backend/items/serializers.py | 79 ++++++++++++ src/backend/items/tests.py | 3 + src/backend/items/urls.py | 13 ++ src/backend/items/views.py | 146 ++++++++++++++++++++++ src/backend/manage.py | 22 ++++ src/backend/requirements.txt | Bin 0 -> 244 bytes 16 files changed, 596 insertions(+) create mode 100644 src/backend/item_manager/__init__.py create mode 100644 src/backend/item_manager/asgi.py create mode 100644 src/backend/item_manager/settings.py create mode 100644 src/backend/item_manager/urls.py create mode 100644 src/backend/item_manager/wsgi.py create mode 100644 src/backend/items/__init__.py create mode 100644 src/backend/items/admin.py create mode 100644 src/backend/items/apps.py create mode 100644 src/backend/items/migrations/__init__.py create mode 100644 src/backend/items/models.py create mode 100644 src/backend/items/serializers.py create mode 100644 src/backend/items/tests.py create mode 100644 src/backend/items/urls.py create mode 100644 src/backend/items/views.py create mode 100644 src/backend/manage.py create mode 100644 src/backend/requirements.txt diff --git a/src/backend/item_manager/__init__.py b/src/backend/item_manager/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/item_manager/asgi.py b/src/backend/item_manager/asgi.py new file mode 100644 index 0000000..a0f4fcb --- /dev/null +++ b/src/backend/item_manager/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for item_manager project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "item_manager.settings") + +application = get_asgi_application() diff --git a/src/backend/item_manager/settings.py b/src/backend/item_manager/settings.py new file mode 100644 index 0000000..a9caf29 --- /dev/null +++ b/src/backend/item_manager/settings.py @@ -0,0 +1,147 @@ +""" +Django settings for item_manager project. + +Generated by 'django-admin startproject' using Django 5.2.6. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/topics/settings/ + +For the full list of settings and their values, see +https://docs.djangoproject.com/en/5.2/ref/settings/ +""" + +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# Quick-start development settings - unsuitable for production +# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ + +# SECURITY WARNING: keep the secret key used in production secret! +SECRET_KEY = "django-insecure-=)6qk8+!)hmy7x@7=svo#-^0$pe+3ya@as6afe^o2wj*m-5#3(" + +# SECURITY WARNING: don't run with debug turned on in production! +DEBUG = True + +ALLOWED_HOSTS = [] + + +# Application definition + +INSTALLED_APPS = [ + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.messages", + "django.contrib.staticfiles", + "rest_framework", + "corsheaders", + "items", +] + +MIDDLEWARE = [ + "corsheaders.middleware.CorsMiddleware", + "django.middleware.security.SecurityMiddleware", + "django.contrib.sessions.middleware.SessionMiddleware", + "django.middleware.common.CommonMiddleware", + "django.middleware.csrf.CsrfViewMiddleware", + "django.contrib.auth.middleware.AuthenticationMiddleware", + "django.contrib.messages.middleware.MessageMiddleware", + "django.middleware.clickjacking.XFrameOptionsMiddleware", +] + +ROOT_URLCONF = "item_manager.urls" + +TEMPLATES = [ + { + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "context_processors": [ + "django.template.context_processors.request", + "django.contrib.auth.context_processors.auth", + "django.contrib.messages.context_processors.messages", + ], + }, + }, +] + +WSGI_APPLICATION = "item_manager.wsgi.application" + + +# Database +# https://docs.djangoproject.com/en/5.2/ref/settings/#databases + +DATABASES = { + "default": { + "ENGINE": "django.db.backends.sqlite3", + "NAME": BASE_DIR / "db.sqlite3", + } +} + + +# Password validation +# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators + +AUTH_PASSWORD_VALIDATORS = [ + { + "NAME": "django.contrib.auth.password_validation.UserAttributeSimilarityValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.MinimumLengthValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.CommonPasswordValidator", + }, + { + "NAME": "django.contrib.auth.password_validation.NumericPasswordValidator", + }, +] + + +# REST Framework configuration +REST_FRAMEWORK = { + "DEFAULT_PERMISSION_CLASSES": [ + "rest_framework.permissions.AllowAny", + ], + "DEFAULT_AUTHENTICATION_CLASSES": [ + "rest_framework.authentication.SessionAuthentication", + ], +} + + +# CORS settings +CORS_ALLOWED_ORIGINS = [ + "http://localhost:8080", + "http://127.0.0.1:8080", + "http://localhost:3000", +] + +CORS_ALLOW_ALL_ORIGINS = True # 开发阶段允许所有源 + + +# Internationalization +# https://docs.djangoproject.com/en/5.2/topics/i18n/ + +LANGUAGE_CODE = "zh-hans" + +TIME_ZONE = "Asia/Shanghai" + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +# https://docs.djangoproject.com/en/5.2/howto/static-files/ + +STATIC_URL = "static/" + +# Default primary key field type +# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField" diff --git a/src/backend/item_manager/urls.py b/src/backend/item_manager/urls.py new file mode 100644 index 0000000..12c3fd9 --- /dev/null +++ b/src/backend/item_manager/urls.py @@ -0,0 +1,25 @@ +""" +URL configuration for item_manager project. + +The `urlpatterns` list routes URLs to views. For more information please see: + https://docs.djangoproject.com/en/5.2/topics/http/urls/ +Examples: +Function views + 1. Add an import: from my_app import views + 2. Add a URL to urlpatterns: path('', views.home, name='home') +Class-based views + 1. Add an import: from other_app.views import Home + 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') +Including another URLconf + 1. Import the include() function: from django.urls import include, path + 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) +""" + +from django.contrib import admin +from django.urls import path, include + +urlpatterns = [ + path("admin/", admin.site.urls), + path("", include("items.urls")), + path("api-auth/", include("rest_framework.urls")), +] diff --git a/src/backend/item_manager/wsgi.py b/src/backend/item_manager/wsgi.py new file mode 100644 index 0000000..87ccb9c --- /dev/null +++ b/src/backend/item_manager/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for item_manager project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.2/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "item_manager.settings") + +application = get_wsgi_application() diff --git a/src/backend/items/__init__.py b/src/backend/items/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/items/admin.py b/src/backend/items/admin.py new file mode 100644 index 0000000..06b26b6 --- /dev/null +++ b/src/backend/items/admin.py @@ -0,0 +1,55 @@ +from django.contrib import admin +from .models import Item, ItemUsage, Category + + +@admin.register(Item) +class ItemAdmin(admin.ModelAdmin): + list_display = ['name', 'serial_number', 'category', 'status', 'location', 'created_at'] + list_filter = ['status', 'category', 'created_at'] + search_fields = ['name', 'serial_number', 'description'] + readonly_fields = ['created_at', 'updated_at'] + fieldsets = ( + ('基本信息', { + 'fields': ('name', 'description', 'serial_number', 'category') + }), + ('状态和位置', { + 'fields': ('status', 'location') + }), + ('购买信息', { + 'fields': ('purchase_date', 'value') + }), + ('系统信息', { + 'fields': ('created_at', 'updated_at'), + 'classes': ('collapse',) + }) + ) + + +@admin.register(ItemUsage) +class ItemUsageAdmin(admin.ModelAdmin): + list_display = ['item', 'user', 'start_time', 'end_time', 'is_returned', 'purpose'] + list_filter = ['is_returned', 'start_time', 'item__category'] + search_fields = ['item__name', 'user__username', 'purpose'] + readonly_fields = ['created_at'] + fieldsets = ( + ('使用信息', { + 'fields': ('item', 'user', 'purpose', 'notes') + }), + ('时间信息', { + 'fields': ('start_time', 'end_time', 'is_returned') + }), + ('状况记录', { + 'fields': ('condition_before', 'condition_after') + }), + ('系统信息', { + 'fields': ('created_at',), + 'classes': ('collapse',) + }) + ) + + +@admin.register(Category) +class CategoryAdmin(admin.ModelAdmin): + list_display = ['name', 'description', 'created_at'] + search_fields = ['name', 'description'] + readonly_fields = ['created_at'] diff --git a/src/backend/items/apps.py b/src/backend/items/apps.py new file mode 100644 index 0000000..e467c8d --- /dev/null +++ b/src/backend/items/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class ItemsConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "items" diff --git a/src/backend/items/migrations/__init__.py b/src/backend/items/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/backend/items/models.py b/src/backend/items/models.py new file mode 100644 index 0000000..7d16e4a --- /dev/null +++ b/src/backend/items/models.py @@ -0,0 +1,68 @@ +from django.db import models +from django.contrib.auth.models import User + + +class Item(models.Model): + """物品模型""" + STATUS_CHOICES = [ + ('available', '可用'), + ('in_use', '使用中'), + ('maintenance', '维护中'), + ('damaged', '损坏'), + ] + + name = models.CharField(max_length=100, verbose_name='物品名称') + description = models.TextField(blank=True, verbose_name='物品描述') + serial_number = models.CharField(max_length=50, unique=True, verbose_name='序列号') + category = models.CharField(max_length=50, verbose_name='物品类别') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='available', verbose_name='物品状态') + location = models.CharField(max_length=100, blank=True, verbose_name='存放位置') + purchase_date = models.DateField(null=True, blank=True, verbose_name='购买日期') + value = models.DecimalField(max_digits=10, decimal_places=2, 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='更新时间') + + class Meta: + verbose_name = '物品' + verbose_name_plural = '物品' + ordering = ['-created_at'] + + def __str__(self): + return f"{self.name} ({self.serial_number})" + + +class ItemUsage(models.Model): + """物品使用记录模型""" + item = models.ForeignKey(Item, on_delete=models.CASCADE, verbose_name='物品') + user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name='使用者') + start_time = models.DateTimeField(verbose_name='开始使用时间') + end_time = models.DateTimeField(null=True, blank=True, verbose_name='结束使用时间') + purpose = models.CharField(max_length=200, verbose_name='使用目的') + notes = models.TextField(blank=True, verbose_name='使用备注') + is_returned = models.BooleanField(default=False, verbose_name='是否已归还') + condition_before = models.CharField(max_length=200, blank=True, verbose_name='使用前状况') + condition_after = models.CharField(max_length=200, blank=True, verbose_name='使用后状况') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='记录创建时间') + + class Meta: + verbose_name = '使用记录' + verbose_name_plural = '使用记录' + ordering = ['-start_time'] + + def __str__(self): + return f"{self.item.name} - {self.user.username} ({self.start_time.strftime('%Y-%m-%d %H:%M')})" + + +class Category(models.Model): + """物品类别模型""" + name = models.CharField(max_length=50, unique=True, verbose_name='类别名称') + description = models.TextField(blank=True, verbose_name='类别描述') + created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') + + class Meta: + verbose_name = '物品类别' + verbose_name_plural = '物品类别' + ordering = ['name'] + + def __str__(self): + return self.name diff --git a/src/backend/items/serializers.py b/src/backend/items/serializers.py new file mode 100644 index 0000000..e8a99ac --- /dev/null +++ b/src/backend/items/serializers.py @@ -0,0 +1,79 @@ +from rest_framework import serializers +from django.contrib.auth.models import User +from .models import Item, ItemUsage, Category + + +class UserSerializer(serializers.ModelSerializer): + class Meta: + model = User + fields = ['id', 'username', 'first_name', 'last_name', 'email'] + + +class CategorySerializer(serializers.ModelSerializer): + class Meta: + model = Category + fields = ['id', 'name', 'description', 'created_at'] + + +class ItemSerializer(serializers.ModelSerializer): + current_user = serializers.SerializerMethodField() + + class Meta: + model = Item + fields = [ + 'id', 'name', 'description', 'serial_number', 'category', 'status', + 'location', 'purchase_date', 'value', 'created_at', 'updated_at', + 'current_user' + ] + + def get_current_user(self, obj): + """获取当前正在使用该物品的用户""" + current_usage = ItemUsage.objects.filter( + item=obj, is_returned=False + ).first() + if current_usage: + return UserSerializer(current_usage.user).data + return None + + +class ItemUsageSerializer(serializers.ModelSerializer): + user = UserSerializer(read_only=True) + user_id = serializers.IntegerField(write_only=True) + item = ItemSerializer(read_only=True) + item_id = serializers.IntegerField(write_only=True) + + class Meta: + model = ItemUsage + fields = [ + 'id', 'item', 'item_id', 'user', 'user_id', 'start_time', 'end_time', + 'purpose', 'notes', 'is_returned', 'condition_before', 'condition_after', + 'created_at' + ] + + def create(self, validated_data): + # 当创建新的使用记录时,更新物品状态为使用中 + item = Item.objects.get(id=validated_data['item_id']) + item.status = 'in_use' + item.save() + return super().create(validated_data) + + def update(self, instance, validated_data): + # 当归还物品时,更新物品状态为可用 + if validated_data.get('is_returned', False) and not instance.is_returned: + item = instance.item + item.status = 'available' + item.save() + return super().update(instance, validated_data) + + +class ItemDetailSerializer(ItemSerializer): + """物品详情序列化器,包含使用历史""" + usage_history = serializers.SerializerMethodField() + + class Meta(ItemSerializer.Meta): + fields = ItemSerializer.Meta.fields + ['usage_history'] + + def get_usage_history(self, obj): + """获取物品的使用历史""" + usages = ItemUsage.objects.filter(item=obj).order_by('-start_time')[:10] + return ItemUsageSerializer(usages, many=True).data diff --git a/src/backend/items/tests.py b/src/backend/items/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/src/backend/items/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/src/backend/items/urls.py b/src/backend/items/urls.py new file mode 100644 index 0000000..53636bd --- /dev/null +++ b/src/backend/items/urls.py @@ -0,0 +1,13 @@ +from django.urls import path, include +from rest_framework.routers import DefaultRouter +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'users', views.UserViewSet) + +urlpatterns = [ + path('api/', include(router.urls)), +] diff --git a/src/backend/items/views.py b/src/backend/items/views.py new file mode 100644 index 0000000..9e9388b --- /dev/null +++ b/src/backend/items/views.py @@ -0,0 +1,146 @@ +from django.shortcuts import render +from rest_framework import viewsets, status +from rest_framework.decorators import action +from rest_framework.response import Response +from django.contrib.auth.models import User +from django.utils import timezone +from .models import Item, ItemUsage, Category +from .serializers import ( + ItemSerializer, ItemDetailSerializer, ItemUsageSerializer, + CategorySerializer, UserSerializer +) + + +class ItemViewSet(viewsets.ModelViewSet): + """物品管理API""" + queryset = Item.objects.all() + serializer_class = ItemSerializer + + def get_serializer_class(self): + if self.action == 'retrieve': + return ItemDetailSerializer + return ItemSerializer + + @action(detail=True, methods=['post']) + def borrow(self, request, pk=None): + """借用物品""" + item = self.get_object() + + if item.status != 'available': + return Response( + {'error': '物品当前不可用'}, + status=status.HTTP_400_BAD_REQUEST + ) + + user_id = request.data.get('user_id') + purpose = request.data.get('purpose', '') + notes = request.data.get('notes', '') + condition_before = request.data.get('condition_before', '') + + if not user_id: + return Response( + {'error': '请指定使用者'}, + status=status.HTTP_400_BAD_REQUEST + ) + + try: + user = User.objects.get(id=user_id) + except User.DoesNotExist: + return Response( + {'error': '用户不存在'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 创建使用记录 + usage = ItemUsage.objects.create( + item=item, + user=user, + start_time=timezone.now(), + purpose=purpose, + notes=notes, + condition_before=condition_before + ) + + # 更新物品状态 + item.status = 'in_use' + item.save() + + return Response(ItemUsageSerializer(usage).data) + + @action(detail=True, methods=['post']) + def return_item(self, request, pk=None): + """归还物品""" + item = self.get_object() + + # 查找当前的使用记录 + current_usage = ItemUsage.objects.filter( + item=item, is_returned=False + ).first() + + if not current_usage: + return Response( + {'error': '该物品未被借用'}, + status=status.HTTP_400_BAD_REQUEST + ) + + # 更新使用记录 + current_usage.end_time = timezone.now() + current_usage.is_returned = True + current_usage.condition_after = request.data.get('condition_after', '') + current_usage.notes = request.data.get('return_notes', current_usage.notes) + current_usage.save() + + # 更新物品状态 + item.status = 'available' + item.save() + + return Response(ItemUsageSerializer(current_usage).data) + + @action(detail=False) + def available(self, request): + """获取可用物品列表""" + items = self.queryset.filter(status='available') + serializer = self.get_serializer(items, many=True) + return Response(serializer.data) + + @action(detail=False) + def in_use(self, request): + """获取使用中的物品列表""" + items = self.queryset.filter(status='in_use') + serializer = self.get_serializer(items, many=True) + return Response(serializer.data) + + +class ItemUsageViewSet(viewsets.ModelViewSet): + """使用记录管理API""" + queryset = ItemUsage.objects.all() + serializer_class = ItemUsageSerializer + + @action(detail=False) + def current(self, request): + """获取当前使用中的记录""" + usages = self.queryset.filter(is_returned=False) + serializer = self.get_serializer(usages, many=True) + return Response(serializer.data) + + @action(detail=False) + def by_user(self, request): + """根据用户ID获取使用记录""" + user_id = request.query_params.get('user_id') + if user_id: + usages = self.queryset.filter(user_id=user_id) + serializer = self.get_serializer(usages, many=True) + return Response(serializer.data) + return Response({'error': '请提供用户ID'}, status=status.HTTP_400_BAD_REQUEST) + + +class CategoryViewSet(viewsets.ModelViewSet): + """物品类别管理API""" + queryset = Category.objects.all() + serializer_class = CategorySerializer + + +class UserViewSet(viewsets.ReadOnlyModelViewSet): + """用户管理API(只读)""" + queryset = User.objects.all() + serializer_class = UserSerializer diff --git a/src/backend/manage.py b/src/backend/manage.py new file mode 100644 index 0000000..c1b4199 --- /dev/null +++ b/src/backend/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "item_manager.settings") + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == "__main__": + main() diff --git a/src/backend/requirements.txt b/src/backend/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..812c331dd3b9d36f20b253aebbaffe15b17bc6e9 GIT binary patch literal 244 zcmZXP+X})k5Jbr0?W*vnY^Bn%!VC_mX4)P z&4h(+$(Ru<_C(I|TwI+=sdVSAHa}{?TAn0c!B(_Lqt|AmPCwM}w`a+jUg=3YCrx+f p9DL8(luG;d$=o<{mD9+rdxBK!RXe-qq2wrHbhRR=@w>PEXaJnwCK&(# literal 0 HcmV?d00001