소스 검색

操作记录

flower_bs 1 개월 전
부모
커밋
1b036572b8
100개의 변경된 파일2457개의 추가작업 그리고 300개의 파일을 삭제
  1. 84 1
      backup/views.py
  2. 544 37
      bin/views.py
  3. 340 162
      bound/views.py
  4. 733 12
      container/views.py
  5. 200 42
      erp/views.py
  6. 1 0
      greaterwms/urls.py
  7. 217 0
      operation_log/README.md
  8. 20 0
      operation_log/filter.py
  9. 13 0
      operation_log/serializers.py
  10. 12 0
      operation_log/urls.py
  11. 40 6
      operation_log/views.py
  12. 1 1
      reportcenter/files.py
  13. 62 0
      reportcenter/views.py
  14. 102 3
      staff/views.py
  15. 55 5
      stock/views.py
  16. 0 1
      templates/dist/spa/css/13.8b9d1b53.css
  17. 1 0
      templates/dist/spa/css/13.b352291e.css
  18. 1 0
      templates/dist/spa/css/33.103b121d.css
  19. 0 0
      templates/dist/spa/css/34.0d4c4716.css
  20. 0 1
      templates/dist/spa/css/34.c527e777.css
  21. 1 0
      templates/dist/spa/css/35.5557cd4a.css
  22. 0 0
      templates/dist/spa/css/36.8f3f6188.css
  23. 0 0
      templates/dist/spa/css/37.44ddcebd.css
  24. 0 0
      templates/dist/spa/css/38.2ac1dad1.css
  25. 0 0
      templates/dist/spa/css/39.12670fd1.css
  26. 0 0
      templates/dist/spa/css/40.9478c981.css
  27. 0 0
      templates/dist/spa/css/41.c4652654.css
  28. 0 0
      templates/dist/spa/css/42.7a23b7fb.css
  29. 0 0
      templates/dist/spa/css/43.2594d0b9.css
  30. 0 0
      templates/dist/spa/css/44.0faa4aeb.css
  31. 0 1
      templates/dist/spa/css/7.8879267a.css
  32. 1 0
      templates/dist/spa/css/7.b5042fa0.css
  33. 1 1
      templates/dist/spa/index.html
  34. 0 1
      templates/dist/spa/js/13.ac106c63.js
  35. BIN
      templates/dist/spa/js/13.ac106c63.js.gz
  36. 1 0
      templates/dist/spa/js/13.eb7c38b1.js
  37. BIN
      templates/dist/spa/js/13.eb7c38b1.js.gz
  38. 1 0
      templates/dist/spa/js/33.212de067.js
  39. BIN
      templates/dist/spa/js/33.212de067.js.gz
  40. 0 1
      templates/dist/spa/js/34.c66dd14f.js
  41. BIN
      templates/dist/spa/js/34.c66dd14f.js.gz
  42. 1 1
      templates/dist/spa/js/33.e044124a.js
  43. 1 0
      templates/dist/spa/js/35.08b4d310.js
  44. BIN
      templates/dist/spa/js/35.08b4d310.js.gz
  45. 0 1
      templates/dist/spa/js/35.191d823c.js
  46. BIN
      templates/dist/spa/js/35.191d823c.js.gz
  47. 1 0
      templates/dist/spa/js/36.0ef8719a.js
  48. BIN
      templates/dist/spa/js/36.0ef8719a.js.gz
  49. BIN
      templates/dist/spa/js/37.54c90e2b.js.gz
  50. 1 1
      templates/dist/spa/js/36.04984167.js
  51. BIN
      templates/dist/spa/js/36.04984167.js.gz
  52. 1 1
      templates/dist/spa/js/37.54c90e2b.js
  53. BIN
      templates/dist/spa/js/38.51ed54fd.js.gz
  54. 1 1
      templates/dist/spa/js/38.96100c54.js
  55. BIN
      templates/dist/spa/js/38.96100c54.js.gz
  56. 1 1
      templates/dist/spa/js/39.5872b98c.js
  57. 1 1
      templates/dist/spa/js/40.d194e156.js
  58. 1 1
      templates/dist/spa/js/41.eed3ba0d.js
  59. 1 1
      templates/dist/spa/js/42.135aa88c.js
  60. 1 1
      templates/dist/spa/js/43.ac48efb6.js
  61. BIN
      templates/dist/spa/js/44.65975fc6.js.gz
  62. 1 1
      templates/dist/spa/js/44.65975fc6.js
  63. BIN
      templates/dist/spa/js/45.85676583.js.gz
  64. 1 1
      templates/dist/spa/js/45.8feb3608.js
  65. BIN
      templates/dist/spa/js/47.8ee118cc.js.gz
  66. 1 1
      templates/dist/spa/js/46.b3afcac9.js
  67. 1 1
      templates/dist/spa/js/47.8ee118cc.js
  68. BIN
      templates/dist/spa/js/48.f5f2c346.js.gz
  69. 1 1
      templates/dist/spa/js/48.72e06ecf.js
  70. BIN
      templates/dist/spa/js/49.d3deb20b.js.gz
  71. BIN
      templates/dist/spa/js/50.89d8c121.js.gz
  72. 1 1
      templates/dist/spa/js/49.d3deb20b.js
  73. BIN
      templates/dist/spa/js/50.b2e076a3.js.gz
  74. 1 1
      templates/dist/spa/js/50.89d8c121.js
  75. BIN
      templates/dist/spa/js/51.f4758ae8.js.gz
  76. 1 1
      templates/dist/spa/js/51.67372365.js
  77. BIN
      templates/dist/spa/js/51.67372365.js.gz
  78. BIN
      templates/dist/spa/js/52.c0c678a9.js.gz
  79. 1 1
      templates/dist/spa/js/52.c0c678a9.js
  80. BIN
      templates/dist/spa/js/53.4bd72b33.js.gz
  81. BIN
      templates/dist/spa/js/53.d09f7623.js.gz
  82. 1 1
      templates/dist/spa/js/53.d09f7623.js
  83. BIN
      templates/dist/spa/js/54.13366a89.js.gz
  84. BIN
      templates/dist/spa/js/54.f7392283.js.gz
  85. BIN
      templates/dist/spa/js/55.7ff76385.js.gz
  86. 1 1
      templates/dist/spa/js/54.f7392283.js
  87. BIN
      templates/dist/spa/js/55.f93ef6f0.js.gz
  88. 1 1
      templates/dist/spa/js/55.7ff76385.js
  89. BIN
      templates/dist/spa/js/56.8954c039.js.gz
  90. BIN
      templates/dist/spa/js/56.c76fed61.js.gz
  91. 1 1
      templates/dist/spa/js/56.c76fed61.js
  92. BIN
      templates/dist/spa/js/57.21c754a8.js.gz
  93. BIN
      templates/dist/spa/js/57.79b8851a.js.gz
  94. 1 1
      templates/dist/spa/js/57.79b8851a.js
  95. BIN
      templates/dist/spa/js/58.b42c1c9b.js.gz
  96. BIN
      templates/dist/spa/js/58.d42ca317.js.gz
  97. 1 1
      templates/dist/spa/js/58.d42ca317.js
  98. BIN
      templates/dist/spa/js/59.9471f3f5.js.gz
  99. BIN
      templates/dist/spa/js/59.bf3b4865.js.gz
  100. 0 0
      templates/dist/spa/js/59.bf3b4865.js

+ 84 - 1
backup/views.py

@@ -9,6 +9,9 @@ from datetime import datetime
 from apscheduler.schedulers.background import BackgroundScheduler
 from apscheduler.schedulers.background import BackgroundScheduler
 from django.conf import settings
 from django.conf import settings
 import math
 import math
+from operation_log.views import log_success_operation, log_failure_operation
+from operation_log.models import OperationLog
+from django.utils import timezone
 
 
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
@@ -20,6 +23,21 @@ def scheduled_backup():
     try:
     try:
         backup_path = perform_base_backup()
         backup_path = perform_base_backup()
         logger.info(f"定时备份完成: {backup_path}")
         logger.info(f"定时备份完成: {backup_path}")
+        # 记录操作日志(定时任务没有request对象,直接创建日志)
+        try:
+            OperationLog.objects.create(
+                operator="系统定时任务",
+                operation_content=f"定时备份数据库完成,备份路径: {backup_path}",
+                operation_level="other",
+                operation_result="success",
+                ip_address=None,
+                user_agent="系统定时任务",
+                request_method="CRON",
+                request_path="/backup/scheduled",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"定时备份日志记录失败: {str(log_error)}")
         # 更新托盘分类任务(如果存在)
         # 更新托盘分类任务(如果存在)
         try:
         try:
             from container.utils import update_container_categories_task,reconcile_material_history
             from container.utils import update_container_categories_task,reconcile_material_history
@@ -30,6 +48,21 @@ def scheduled_backup():
             logger.warning("更新托盘分类模块未找到,跳过更新")
             logger.warning("更新托盘分类模块未找到,跳过更新")
     except Exception as e:
     except Exception as e:
         logger.error(f"定时备份失败: {str(e)}")
         logger.error(f"定时备份失败: {str(e)}")
+        # 记录失败日志
+        try:
+            OperationLog.objects.create(
+                operator="系统定时任务",
+                operation_content=f"定时备份数据库失败: {str(e)}",
+                operation_level="other",
+                operation_result="failure",
+                ip_address=None,
+                user_agent="系统定时任务",
+                request_method="CRON",
+                request_path="/backup/scheduled",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"定时备份失败日志记录失败: {str(log_error)}")
 
 
 # 启动定时备份(每6小时执行一次)
 # 启动定时备份(每6小时执行一次)
 if not scheduler.running:
 if not scheduler.running:
@@ -120,7 +153,16 @@ def trigger_backup(request):
     """手动触发备份的API接口"""
     """手动触发备份的API接口"""
     try:
     try:
         backup_path = perform_base_backup()
         backup_path = perform_base_backup()
-   
+        # 记录成功日志
+        try:
+            log_success_operation(
+                request=request,
+                operation_content=f"手动触发数据库备份,备份路径: {backup_path}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"备份成功日志记录失败: {str(log_error)}")
 
 
         return JsonResponse({
         return JsonResponse({
             'status': 'success',
             'status': 'success',
@@ -128,6 +170,16 @@ def trigger_backup(request):
             'path': backup_path
             'path': backup_path
         })
         })
     except Exception as e:
     except Exception as e:
+        # 记录失败日志
+        try:
+            log_failure_operation(
+                request=request,
+                operation_content=f"手动触发数据库备份失败: {str(e)}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"备份失败日志记录失败: {str(log_error)}")
         return JsonResponse({
         return JsonResponse({
             'status': 'error',
             'status': 'error',
             'message': str(e)
             'message': str(e)
@@ -175,6 +227,16 @@ def restore_to_point(request):
        
        
         
         
         if not base_backup or not os.path.exists(base_backup):
         if not base_backup or not os.path.exists(base_backup):
+            # 记录失败日志(无效路径)
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"执行数据库恢复失败: 无效的基础备份路径 - {base_backup}",
+                    operation_level="other",
+                    module_name="系统备份"
+                )
+            except Exception as log_error:
+                logger.error(f"恢复失败日志记录失败: {str(log_error)}")
             return JsonResponse({
             return JsonResponse({
                 'status': 'error',
                 'status': 'error',
                 'message': '无效的基础备份路径'
                 'message': '无效的基础备份路径'
@@ -193,12 +255,33 @@ def restore_to_point(request):
         scheduler.resume_job('db_backup_job')
         scheduler.resume_job('db_backup_job')
         logger.info("定时备份任务已恢复")
         logger.info("定时备份任务已恢复")
         
         
+        # 记录成功日志
+        try:
+            log_success_operation(
+                request=request,
+                operation_content=f"执行数据库时间点恢复,恢复路径: {base_backup}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"恢复成功日志记录失败: {str(log_error)}")
+        
         return JsonResponse({
         return JsonResponse({
             'status': 'success',
             'status': 'success',
             'message': f'已成功恢复到{base_backup}'
             'message': f'已成功恢复到{base_backup}'
         })
         })
     except Exception as e:
     except Exception as e:
         logger.error(f"时间点恢复失败: {str(e)}")
         logger.error(f"时间点恢复失败: {str(e)}")
+        # 记录失败日志
+        try:
+            log_failure_operation(
+                request=request,
+                operation_content=f"执行数据库时间点恢复失败: {str(e)}",
+                operation_level="other",
+                module_name="系统备份"
+            )
+        except Exception as log_error:
+            logger.error(f"恢复失败日志记录失败: {str(log_error)}")
         # 确保恢复定时备份任务
         # 确保恢复定时备份任务
         if scheduler.get_job('db_backup_job') and scheduler.get_job('db_backup_job').next_run_time is None:
         if scheduler.get_job('db_backup_job') and scheduler.get_job('db_backup_job').next_run_time is None:
             scheduler.resume_job('db_backup_job')
             scheduler.resume_job('db_backup_job')

+ 544 - 37
bin/views.py

@@ -27,6 +27,7 @@ import json
 from collections import defaultdict
 from collections import defaultdict
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 from operation_log.views import log_operation,log_failure_operation,log_success_operation
 from operation_log.views import log_operation,log_failure_operation,log_success_operation
+from operation_log.models import OperationLog
 # 库位分配
 # 库位分配
 # 入库规则函数
 # 入库规则函数
 # 逻辑根据批次下的托盘数目来找满足区间范围的库位,按照优先级排序,
 # 逻辑根据批次下的托盘数目来找满足区间范围的库位,按照优先级排序,
@@ -96,11 +97,66 @@ class locationViewSet(viewsets.ModelViewSet):
     def get_serializer_class(self):
     def get_serializer_class(self):
         if self.action == 'list':
         if self.action == 'list':
             return LocationListSerializer
             return LocationListSerializer
+        elif self.action == 'create':
+            return LocationPostSerializer
         elif self.action == 'update':
         elif self.action == 'update':
             return LocationPostSerializer
             return LocationPostSerializer
         elif self.action =='retrieve':
         elif self.action =='retrieve':
             return LocationListSerializer
             return LocationListSerializer
 
 
+    def create(self, request, *args, **kwargs):
+        """创建库位"""
+        serializer = self.get_serializer(data=request.data)
+        try:
+            serializer.is_valid(raise_exception=True)
+            instance = serializer.save()
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建库位成功,库位编码: {instance.location_code}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=instance.id,
+                module_name="库位"
+            )
+            headers = self.get_success_headers(serializer.data)
+            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建库位失败: {str(e)}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="库位"
+            )
+            raise
+
+    def destroy(self, request, *args, **kwargs):
+        """删除库位"""
+        instance = self.get_object()
+        location_code = instance.location_code
+        object_id = instance.id
+        try:
+            self.perform_destroy(instance)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"删除库位成功,库位编码: {location_code}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            return Response(status=status.HTTP_204_NO_CONTENT)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"删除库位失败,库位编码: {location_code}, 错误: {str(e)}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            raise
+
     def update(self, request, *args, **kwargs):
     def update(self, request, *args, **kwargs):
         qs = self.get_object()
         qs = self.get_object()
         data = self.request.data
         data = self.request.data
@@ -307,12 +363,29 @@ class locationGroupViewSet(viewsets.ModelViewSet):
         id = self.get_project()
         id = self.get_project()
         if self.request.auth:
         if self.request.auth:
             if id is None:
             if id is None:
+                log_operation(
+                    request=self.request,
+                    operation_content="查看库位组列表",
+                    operation_level="view",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    module_name="库位"
+                )
                 return LocationGroupModel.objects.filter()  
                 return LocationGroupModel.objects.filter()  
             else:
             else:
+                log_operation(
+                    request=self.request,
+                    operation_content=f"查看库位组详情 ID:{id}",
+                    operation_level="view",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=id,
+                    module_name="库位"
+                )
                 return LocationGroupModel.objects.filter(id=id)            
                 return LocationGroupModel.objects.filter(id=id)            
         else:
         else:
             return LocationGroupModel.objects.none()                           
             return LocationGroupModel.objects.none()                           
-    def get_serializer_class(self):                                             
+    def get_serializer_class(self):
+        if self.action == 'create':
+            return LocationGroupPostSerializer                                             
         if self.action == 'list':
         if self.action == 'list':
             return LocationGroupListSerializer          
             return LocationGroupListSerializer          
 
 
@@ -320,7 +393,63 @@ class locationGroupViewSet(viewsets.ModelViewSet):
             return LocationGroupPostSerializer
             return LocationGroupPostSerializer
 
 
         elif self.action =='retrieve':
         elif self.action =='retrieve':
-            return LocationGroupListSerializer      
+            return LocationGroupListSerializer
+
+    def create(self, request, *args, **kwargs):
+        """创建库位组"""
+        data = self.request.data.copy()
+        order_month = str(timezone.now().strftime('%Y%m'))
+        data['month'] = order_month
+        serializer = LocationGroupPostSerializer(data=data)
+        try:
+            serializer.is_valid(raise_exception=True)
+            instance = serializer.save()
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建库位组成功,库位组编码: {instance.group_code}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=instance.id,
+                module_name="库位"
+            )
+            headers = self.get_success_headers(serializer.data)
+            return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建库位组失败: {str(e)}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="库位"
+            )
+            raise
+
+    def destroy(self, request, *args, **kwargs):
+        """删除库位组"""
+        instance = self.get_object()
+        group_code = instance.group_code
+        object_id = instance.id
+        try:
+            self.perform_destroy(instance)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"删除库位组成功,库位组编码: {group_code}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            return Response(status=status.HTTP_204_NO_CONTENT)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"删除库位组失败,库位组编码: {group_code}, 错误: {str(e)}",
+                operation_level="delete",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=object_id,
+                module_name="库位"
+            )
+            raise
     
     
     def update(self, request, *args, **kwargs):
     def update(self, request, *args, **kwargs):
         data = self.request.data
         data = self.request.data
@@ -332,7 +461,30 @@ class locationGroupViewSet(viewsets.ModelViewSet):
         if group_obj:
         if group_obj:
             data['id'] = group_obj.id
             data['id'] = group_obj.id
             logger.info(f"库位组 {group_code} 已存在")
             logger.info(f"库位组 {group_code} 已存在")
-            
+            # 更新现有库位组
+            serializer = LocationGroupPostSerializer(group_obj, data=data)
+            try:
+                serializer.is_valid(raise_exception=True)
+                serializer.save()
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"更新库位组成功,库位组编码: {group_code}",
+                    operation_level="update",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=group_obj.id,
+                    module_name="库位"
+                )
+                return Response(serializer.data, status=status.HTTP_200_OK)
+            except Exception as e:
+                log_failure_operation(
+                    request=self.request,
+                    operation_content=f"更新库位组失败,库位组编码: {group_code}, 错误: {str(e)}",
+                    operation_level="update",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=group_obj.id,
+                    module_name="库位"
+                )
+                raise
         else:
         else:
             logger.info(f"库位组 {group_code} 不存在,创建库位组对象")
             logger.info(f"库位组 {group_code} 不存在,创建库位组对象")
             serializer_list = LocationGroupPostSerializer(data=data)
             serializer_list = LocationGroupPostSerializer(data=data)
@@ -340,14 +492,14 @@ class locationGroupViewSet(viewsets.ModelViewSet):
             serializer_list.save()
             serializer_list.save()
             log_success_operation(
             log_success_operation(
                 request=self.request,
                 request=self.request,
-                operation_content=f"创建库位组成功,库位组 {group_code} 已创建",
+                operation_content=f"创建库位组成功,库位组编码: {group_code}",
                 operation_level="new",  
                 operation_level="new",  
                 operator=self.request.auth.name if self.request.auth else None,
                 operator=self.request.auth.name if self.request.auth else None,
                 object_id=serializer_list.data.get('id'),
                 object_id=serializer_list.data.get('id'),
                 module_name="库位"
                 module_name="库位"
             )
             )
             data['id'] = serializer_list.data.get('id')
             data['id'] = serializer_list.data.get('id')
-        return Response(data, status=status.HTTP_201_CREATED)
+            return Response(data, status=status.HTTP_201_CREATED)
 
 
 class LocationAllocation:
 class LocationAllocation:
     # 入库规则函数
     # 入库规则函数
@@ -450,11 +602,12 @@ class LocationAllocation:
             return None
             return None
 
 
     
     
-    def update_location_container_link(self,location_code,container_code):
+    def update_location_container_link(self,location_code,container_code, request=None):
         """
         """
         更新库位和托盘的关联关系
         更新库位和托盘的关联关系
         :param location_code: 库位编码
         :param location_code: 库位编码
         :param container_code: 托盘编码
         :param container_code: 托盘编码
+        :param request: 请求对象(可选)
         :return:
         :return:
         """
         """
         try:
         try:
@@ -473,15 +626,72 @@ class LocationAllocation:
                 )
                 )
                 location_container_link.save()
                 location_container_link.save()
                 print(f"更新库位和托盘的关联关系成功!")
                 print(f"更新库位和托盘的关联关系成功!")
+                # 记录操作日志
+                try:
+                    if request:
+                        log_success_operation(
+                            request=request,
+                            operation_content=f"创建库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="other",
+                            module_name="库位分配"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"创建库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="other",
+                            operation_result="success",
+                            module_name="库位分配"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录库位-托盘关联日志失败: {str(log_error)}")
                 return True
                 return True
             # 3. 更新库位和托盘的关联关系
             # 3. 更新库位和托盘的关联关系
             else:
             else:
                 LocationContainerLink.objects.filter(location=location).update(location=location, container=container)
                 LocationContainerLink.objects.filter(location=location).update(location=location, container=container)
                 print(f"更新库位和托盘的关联关系成功!")
                 print(f"更新库位和托盘的关联关系成功!")
+                # 记录操作日志
+                try:
+                    if request:
+                        log_success_operation(
+                            request=request,
+                            operation_content=f"更新库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="update",
+                            module_name="库位分配"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"更新库位-托盘关联,库位编码: {location_code}, 托盘编码: {container_code}",
+                            operation_level="update",
+                            operation_result="success",
+                            module_name="库位分配"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录库位-托盘关联日志失败: {str(log_error)}")
                 return True
                 return True
         except Exception as e:       
         except Exception as e:       
             logger.error(f"更新库位和托盘的关联关系失败:{str(e)}")
             logger.error(f"更新库位和托盘的关联关系失败:{str(e)}")
             print(f"更新库位和托盘的关联关系失败:{str(e)}")
             print(f"更新库位和托盘的关联关系失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"更新库位-托盘关联失败,库位编码: {location_code}, 托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新库位-托盘关联失败,库位编码: {location_code}, 托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False 
             return False 
     def update_container_detail_status(self,container_code,status):
     def update_container_detail_status(self,container_code,status):
         try:
         try:
@@ -536,11 +746,12 @@ class LocationAllocation:
             logger.error(f"更新库位组的批次失败:{str(e)}")
             logger.error(f"更新库位组的批次失败:{str(e)}")
             print(f"更新库位组的批次失败:{str(e)}")
             print(f"更新库位组的批次失败:{str(e)}")
             return False    
             return False    
-    def update_location_status(self,location_code,status):
+    def update_location_status(self,location_code,status, request=None):
         """
         """
         更新库位状态
         更新库位状态
         :param location_code: 库位编码
         :param location_code: 库位编码
         :param status: 库位状态
         :param status: 库位状态
+        :param request: 请求对象(可选)
         :return:
         :return:
         """
         """
         try:
         try:
@@ -552,22 +763,65 @@ class LocationAllocation:
                 print(f"库位获取失败!")
                 print(f"库位获取失败!")
                 return False
                 return False
             # 2. 更新库位状态
             # 2. 更新库位状态
+            old_status = location.status
             location.status = status
             location.status = status
             location.save()
             location.save()
             print(f"更新库位状态成功!")
             print(f"更新库位状态成功!")
+            # 记录操作日志(状态变更敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"更新库位状态,库位编码: {location_code}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新库位状态,库位编码: {location_code}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        operation_result="success",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录库位状态更新日志失败: {str(log_error)}")
             return True
             return True
         except Exception as e:       
         except Exception as e:       
             logger.error(f"更新库位状态失败:{str(e)}")
             logger.error(f"更新库位状态失败:{str(e)}")
             print(f"更新库位状态失败:{str(e)}")
             print(f"更新库位状态失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"更新库位状态失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新库位状态失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False    
             return False    
     
     
-    def update_group_status_reserved(self,location_group_list):
+    def update_group_status_reserved(self,location_group_list, request=None):
         """
         """
-        更新库位组状态
+        更新库位组状态为预留
         :param location_group_list: 库位组对象列表
         :param location_group_list: 库位组对象列表
+        :param request: 请求对象(可选)
         :return:
         :return:
         """
         """
         try:
         try:
+            reserved_groups = []
             for location_group in location_group_list:
             for location_group in location_group_list:
                 # 1. 获取库位组
                 # 1. 获取库位组
                 if not location_group:
                 if not location_group:
@@ -582,13 +836,54 @@ class LocationAllocation:
                     print(f"库位组 {location_group} 不存在")
                     print(f"库位组 {location_group} 不存在")
                     return False
                     return False
                 # 3. 更新库位组状态
                 # 3. 更新库位组状态
+                old_status = location_group_item.status
                 location_group_item.status = 'reserved'
                 location_group_item.status = 'reserved'
                 location_group_item.save()
                 location_group_item.save()
+                reserved_groups.append(f"{location_group_item.group_code}({old_status}->reserved)")
+
+            # 记录预留库位组操作日志(敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"预留库位组,共{len(reserved_groups)}个: {', '.join(reserved_groups)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"预留库位组,共{len(reserved_groups)}个: {', '.join(reserved_groups)}",
+                        operation_level="update",
+                        operation_result="success",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录预留库位组日志失败: {str(log_error)}")
 
 
             return True
             return True
         except Exception as e:       
         except Exception as e:       
             logger.error(f"更新库位组状态失败:{str(e)}")
             logger.error(f"更新库位组状态失败:{str(e)}")
             print(f"更新库位组状态失败:{str(e)}")
             print(f"更新库位组状态失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"预留库位组失败,错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"预留库位组失败,错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False    
             return False    
     
     
     def update_location_group_status(self, location_code):
     def update_location_group_status(self, location_code):
@@ -636,11 +931,12 @@ class LocationAllocation:
         except Exception as e:       
         except Exception as e:       
             logger.error(f"更新库位组状态失败:{str(e)}")
             logger.error(f"更新库位组状态失败:{str(e)}")
             print(f"更新库位组状态失败:{str(e)}")
             print(f"更新库位组状态失败:{str(e)}")
-    def update_batch_status(self,container_code,status):
+    def update_batch_status(self,container_code,status, request=None):
         """
         """
         更新批次状态
         更新批次状态
-        :param batch_id: 批次id
+        :param container_code: 托盘编码
         :param status: 批次状态
         :param status: 批次状态
+        :param request: 请求对象(可选)
         :return:
         :return:
         """
         """
         try:
         try:
@@ -664,14 +960,55 @@ class LocationAllocation:
         
         
             # 3. 更新批次状态
             # 3. 更新批次状态
             batch = container_detail.batch
             batch = container_detail.batch
+            old_status = batch.status
             batch.status = status
             batch.status = status
             batch.save()
             batch.save()
 
 
             print(f"更新批次状态成功!")
             print(f"更新批次状态成功!")
+            # 记录操作日志(批次状态变更敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"更新批次状态,托盘编码: {container_code}, 批次号: {batch.bound_number}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        object_id=str(batch.id),
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新批次状态,托盘编码: {container_code}, 批次号: {batch.bound_number}, 状态: {old_status} -> {status}",
+                        operation_level="update",
+                        operation_result="success",
+                        object_id=str(batch.id),
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录批次状态更新日志失败: {str(log_error)}")
             return True
             return True
         except Exception as e:       
         except Exception as e:       
             logger.error(f"更新批次状态失败:{str(e)}")
             logger.error(f"更新批次状态失败:{str(e)}")
             print(f"更新批次状态失败:{str(e)}")
             print(f"更新批次状态失败:{str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"更新批次状态失败,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"更新批次状态失败,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
             return False  
             return False  
         
         
     # def update_batch_goods_in_location_qty(self,container_code,taskworking):
     # def update_batch_goods_in_location_qty(self,container_code,taskworking):
@@ -816,21 +1153,52 @@ class LocationAllocation:
                 self.update_current_finish_task(container_code,current_task)
                 self.update_current_finish_task(container_code,current_task)
                 return location_list[0]
                 return location_list[0]
     
     
-    def get_location_type(self, container_code):
+    def get_location_type(self, container_code, request=None):
         """
         """
         智能库位分配核心算法
         智能库位分配核心算法
         :param container_code: 托盘码
         :param container_code: 托盘码
+        :param request: 请求对象(可选)
         :return: 库位类型分配方案
         :return: 库位类型分配方案
         """
         """
         try:
         try:
             batch = self.get_batch(container_code)
             batch = self.get_batch(container_code)
             if not batch:
             if not batch:
                 logger.error("批次信息获取失败")
                 logger.error("批次信息获取失败")
+                # 记录失败日志
+                try:
+                    if request:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 错误: 批次信息获取失败",
+                            operation_level="other",
+                            module_name="库位分配算法"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 错误: 批次信息获取失败",
+                            operation_level="other",
+                            operation_result="failure",
+                            module_name="库位分配算法"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录算法失败日志失败: {str(log_error)}")
                 return None
                 return None
 
 
             # 检查已有分配方案
             # 检查已有分配方案
             existing_solution = alloction_pre.objects.filter(batch_number=batch).first()
             existing_solution = alloction_pre.objects.filter(batch_number=batch).first()
             if existing_solution:
             if existing_solution:
+                # 记录使用已有分配方案
+                try:
+                    if request:
+                        log_operation(
+                            request=request,
+                            operation_content=f"使用已有库位分配方案,托盘编码: {container_code}, 批次号: {batch}",
+                            operation_level="view",
+                            module_name="库位分配算法"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录使用已有方案日志失败: {str(log_error)}")
                 return existing_solution.layer_pre_type
                 return existing_solution.layer_pre_type
 
 
             # 获取关键参数
             # 获取关键参数
@@ -914,6 +1282,25 @@ class LocationAllocation:
             
             
             if not allocation:
             if not allocation:
                 logger.error("无法生成有效分配方案")
                 logger.error("无法生成有效分配方案")
+                # 记录失败日志
+                try:
+                    if request:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 批次号: {batch}, 托盘数: {total_pallets}, 错误: 无法生成有效分配方案",
+                            operation_level="other",
+                            module_name="库位分配算法"
+                        )
+                    else:
+                        OperationLog.objects.create(
+                            operator="系统自动",
+                            operation_content=f"库位分配算法执行失败,托盘编码: {container_code}, 批次号: {batch}, 托盘数: {total_pallets}, 错误: 无法生成有效分配方案",
+                            operation_level="other",
+                            operation_result="failure",
+                            module_name="库位分配算法"
+                        )
+                except Exception as log_error:
+                    logger.error(f"记录算法失败日志失败: {str(log_error)}")
                 return None
                 return None
 
 
             # 保存分配方案
             # 保存分配方案
@@ -938,11 +1325,50 @@ class LocationAllocation:
             solution.save()
             solution.save()
             solution_pressure.save()
             solution_pressure.save()
 
 
+            # 记录算法执行成功日志(关键操作)
+            try:
+                allocation_summary = f"批次号: {batch}, 托盘数: {total_pallets}, 分配方案: {len(allocation[0])}个库位组, 压力分布: L1={allocation[1][0]}, L2={allocation[1][1]}, L3={allocation[1][2]}"
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"库位分配算法执行成功,托盘编码: {container_code}, {allocation_summary}",
+                        operation_level="other",
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配算法执行成功,托盘编码: {container_code}, {allocation_summary}",
+                        operation_level="other",
+                        operation_result="success",
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录算法成功日志失败: {str(log_error)}")
 
 
             return [loc.split('_')[1] for loc in allocation[0]]
             return [loc.split('_')[1] for loc in allocation[0]]
 
 
         except Exception as e:
         except Exception as e:
             logger.error(f"分配算法异常:{str(e)}")
             logger.error(f"分配算法异常:{str(e)}")
+            # 记录异常日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"库位分配算法执行异常,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="other",
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配算法执行异常,托盘编码: {container_code}, 错误: {str(e)}",
+                        operation_level="other",
+                        operation_result="failure",
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录算法异常日志失败: {str(log_error)}")
             return None
             return None
 
 
 
 
@@ -1071,11 +1497,12 @@ class LocationAllocation:
 
 
 
 
     
     
-    def get_location_by_status(self,container_code,start_location):
+    def get_location_by_status(self,container_code,start_location, request=None):
         """
         """
         根据库位状态获取库位
         根据库位状态获取库位
-        :param location_type: 库位类型
+        :param container_code: 托盘编码
         :param start_location: 起始库位 if in1 优先考虑left_priority, if in2 优先考虑right_priority 就是获取库位组列表之后进行排序
         :param start_location: 起始库位 if in1 优先考虑left_priority, if in2 优先考虑right_priority 就是获取库位组列表之后进行排序
+        :param request: 请求对象(可选)
         :return: 库位列表
         :return: 库位列表
         """
         """
         # 1. 获取批次状态 1 为已组盘 2 为部分入库 3 为全部入库
         # 1. 获取批次状态 1 为已组盘 2 为部分入库 3 为全部入库
@@ -1086,20 +1513,35 @@ class LocationAllocation:
             print(f"[1]第一次入库")
             print(f"[1]第一次入库")
 
 
             # 重新获取最新数据
             # 重新获取最新数据
-            self.get_location_type(container_code) 
+            self.get_location_type(container_code, request) 
             
             
             location_type_list = json.loads(alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_pre_type)            
             location_type_list = json.loads(alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_pre_type)            
             location_list = self.get_location_by_type(location_type_list,start_location,container_code)
             location_list = self.get_location_by_type(location_type_list,start_location,container_code)
             # 预定这些库组
             # 预定这些库组
-            self.update_group_status_reserved(location_list)
+            self.update_group_status_reserved(location_list, request)
             location_min_value = self.get_location_list_remainder(location_list,container_code)
             location_min_value = self.get_location_list_remainder(location_list,container_code)
             print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
             print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
-            # if not location_list[location_min_index]:
-            #     # 库位已满,返回None
-            #     return None
-            # else:
-                
-            #     return location_list[location_min_index]
+            # 记录库位分配成功日志
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"库位分配成功(第一次入库),托盘编码: {container_code}, 起始位置: {start_location}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配成功(第一次入库),托盘编码: {container_code}, 起始位置: {start_location}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        operation_result="success",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录库位分配日志失败: {str(log_error)}")
             return location_min_value
             return location_min_value
       
       
         elif status == 2:
         elif status == 2:
@@ -1108,28 +1550,93 @@ class LocationAllocation:
             location_list = alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_solution_type
             location_list = alloction_pre.objects.filter(batch_number=self.get_batch(container_code)).first().layer_solution_type
             location_min_value = self.get_location_list_remainder(location_list,container_code)
             location_min_value = self.get_location_list_remainder(location_list,container_code)
             print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
             print(f"库位安排到第{location_min_value.c_number}个库位:{location_min_value}")
+            # 记录库位分配成功日志
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"库位分配成功(部分入库),托盘编码: {container_code}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"库位分配成功(部分入库),托盘编码: {container_code}, 分配库位: {location_min_value.location_code}, 库位组: {location_min_value.location_group}",
+                        operation_level="other",
+                        operation_result="success",
+                        object_id=str(location_min_value.id),
+                        module_name="库位分配算法"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录库位分配日志失败: {str(log_error)}")
             return location_min_value
             return location_min_value
 
 
 
 
 
 
 
 
 
 
-    def release_location(self, location_code):
-            """释放库位并更新关联数据"""
-            try:
-                location = LocationModel.objects.get(location_code=location_code)
-                links = LocationContainerLink.objects.filter(location=location, is_active=True).first()
-                if not links:
-                    logger.error(f"库位{location_code}未关联托盘")
-                    return True
-                print(f"释放库位: {location_code}, 关联托盘: {links.container_id}")
-                # 解除关联并标记为非活跃
-                links.is_active = False
-                links.save()
+    def release_location(self, location_code, request=None):
+        """释放库位并更新关联数据"""
+        try:
+            location = LocationModel.objects.get(location_code=location_code)
+            links = LocationContainerLink.objects.filter(location=location, is_active=True).first()
+            if not links:
+                logger.error(f"库位{location_code}未关联托盘")
                 return True
                 return True
-            except Exception as e:
-                logger.error(f"释放库位失败: {str(e)}")
-                return False      
+            container_id = links.container_id
+            print(f"释放库位: {location_code}, 关联托盘: {container_id}")
+            # 解除关联并标记为非活跃
+            links.is_active = False
+            links.save()
+            # 更新库位状态为可用
+            location.status = 'available'
+            location.save()
+            # 记录释放库位操作日志(敏感操作)
+            try:
+                if request:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"释放库位,库位编码: {location_code}, 托盘ID: {container_id}",
+                        operation_level="update",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"释放库位,库位编码: {location_code}, 托盘ID: {container_id}",
+                        operation_level="update",
+                        operation_result="success",
+                        object_id=str(location.id),
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录释放库位日志失败: {str(log_error)}")
+            return True
+        except Exception as e:
+            logger.error(f"释放库位失败: {str(e)}")
+            # 记录失败日志
+            try:
+                if request:
+                    log_failure_operation(
+                        request=request,
+                        operation_content=f"释放库位失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        module_name="库位分配"
+                    )
+                else:
+                    OperationLog.objects.create(
+                        operator="系统自动",
+                        operation_content=f"释放库位失败,库位编码: {location_code}, 错误: {str(e)}",
+                        operation_level="update",
+                        operation_result="failure",
+                        module_name="库位分配"
+                    )
+            except Exception as log_error:
+                logger.error(f"记录失败日志失败: {str(log_error)}")
+            return False      
   
   
       
       
   
   

+ 340 - 162
bound/views.py

@@ -84,36 +84,58 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
                 "msg": "Success Create",
                 "msg": "Success Create",
             	"data": data
             	"data": data
         }
         }
-    
+        log_operation(
+            request=self.request,
+            operation_content=f"查询出库需求列表,出库单ID:{data.get('bound_list_id')}",
+            operation_level="view",
+            operator=self.request.auth.name if self.request.auth else None,
+            module_name="出库需求"
+        )
         return Response(return_data,status=200,headers={})
         return Response(return_data,status=200,headers={})
 
 
   
   
     def create(self, request, *args, **kwargs):
     def create(self, request, *args, **kwargs):
         data = self.request.data
         data = self.request.data
-
-        data['openid'] = self.request.auth.openid
-        data['create_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
-        data['update_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
-        data['working'] = True
-        bound_list_obj = BoundListModel.objects.get(id=data['bound_list_id'])
-
-        OutBoundDemand_obj =OutBoundDemandModel.objects.create(
-            bound_list=bound_list_obj, 
-            goods_code=data['goods_code'], 
-            goods_desc=data['goods_desc'], 
-            goods_std=data['goods_std'], 
-            goods_unit=data['goods_unit'], 
-            goods_qty=data['goods_out_qty'],
-            out_type = data['out_type'],
-            creater=data['creater'], 
-            create_time=data['create_time'],
-            update_time=data['update_time'], 
-            working=data['working']
+        try:
+            data['openid'] = self.request.auth.openid
+            data['create_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
+            data['update_time'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
+            data['working'] = True
+            bound_list_obj = BoundListModel.objects.get(id=data['bound_list_id'])
+
+            OutBoundDemand_obj =OutBoundDemandModel.objects.create(
+                bound_list=bound_list_obj, 
+                goods_code=data['goods_code'], 
+                goods_desc=data['goods_desc'], 
+                goods_std=data['goods_std'], 
+                goods_unit=data['goods_unit'], 
+                goods_qty=data['goods_out_qty'],
+                out_type = data['out_type'],
+                creater=data['creater'], 
+                create_time=data['create_time'],
+                update_time=data['update_time'], 
+                working=data['working']
+                )
+            return_data = OutBoundDemandModelSerializer(OutBoundDemand_obj).data
+            headers = self.get_success_headers(return_data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建出库需求 ID:{OutBoundDemand_obj.id},创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=OutBoundDemand_obj.id,
+                module_name="出库需求"
             )
             )
-        return_data = OutBoundDemandModelSerializer(OutBoundDemand_obj).data
-        headers = self.get_success_headers(return_data)
-
-        return Response(return_data, status=200, headers=headers)
+            return Response(return_data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建出库需求失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库需求"
+            )
+            raise
     
     
     def batch_demanded_list(self, request):
     def batch_demanded_list(self, request):
         data =self.request.data
         data =self.request.data
@@ -124,7 +146,13 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
             "msg": "Success Create",
             "msg": "Success Create",
             "data": data
             "data": data
         }
         }
-
+        log_operation(
+            request=self.request,
+            operation_content=f"查询出库批次列表,出库单ID:{data.get('bound_list_id')}",
+            operation_level="view",
+            operator=self.request.auth.name if self.request.auth else None,
+            module_name="出库批次"
+        )
         return Response(return_data,status=200,headers={})
         return Response(return_data,status=200,headers={})
 
 
     def distribute(self, request):
     def distribute(self, request):
@@ -134,6 +162,13 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
                 bound_list_id, demands,out_type, creater= self.validate_distribute_request(request)
                 bound_list_id, demands,out_type, creater= self.validate_distribute_request(request)
 
 
                 if OutBatchModel.objects.filter(bound_list_id=bound_list_id, is_delete=False).exists():
                 if OutBatchModel.objects.filter(bound_list_id=bound_list_id, is_delete=False).exists():
+                    log_operation(
+                        request=self.request,
+                        operation_content=f"出库分配失败,出库单ID:{bound_list_id} 已分配,请勿重复分配",
+                        operation_level="other",
+                        operator=self.request.auth.name if self.request.auth else None,
+                        module_name="出库分配"
+                    )
                     return_data = {
                     return_data = {
                         "code": 200,
                         "code": 200,
                         "msg": "Success Create",
                         "msg": "Success Create",
@@ -144,10 +179,32 @@ class OutBoundDemandViewSet(viewsets.ModelViewSet):
                     return Response(return_data, status=200, headers={})
                     return Response(return_data, status=200, headers={})
                 aggregated_demands = self.aggregate_demands(demands)
                 aggregated_demands = self.aggregate_demands(demands)
                 result = self.process_all_goods(aggregated_demands, request, out_type, creater,bound_list_id)
                 result = self.process_all_goods(aggregated_demands, request, out_type, creater,bound_list_id)
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"出库分配成功,出库单ID:{bound_list_id},分配结果:{result}",
+                    operation_level="other",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=bound_list_id,
+                    module_name="出库分配"
+                )
                 return self.build_success_response(result)
                 return self.build_success_response(result)
         except APIException as e:
         except APIException as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"出库分配失败,出库单ID:{request.data.get('bound_list_id')},错误:{e.detail}",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库分配"
+            )
             return self.build_error_response(e.detail, 200)
             return self.build_error_response(e.detail, 200)
         except Exception as e:
         except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"出库分配异常,出库单ID:{request.data.get('bound_list_id')},错误:{str(e)}",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库分配"
+            )
             return self.build_error_response(str(e), 200)
             return self.build_error_response(str(e), 200)
 
 
     # 验证层方法
     # 验证层方法
@@ -457,35 +514,55 @@ class BoundListViewSet(viewsets.ModelViewSet):
 
 
         serializer = self.get_serializer(data=data)
         serializer = self.get_serializer(data=data)
         serializer.is_valid(raise_exception=True)
         serializer.is_valid(raise_exception=True)
-        serializer.save()
-
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"创建入库单 ID:{serializer.data['id']},创建内容:{data}",
-            operation_level="create",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库单"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建入库单 ID:{serializer.data['id']},创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库单"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建入库单失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库单"
+            )
+            raise
     
     
     def update(self, request, pk):
     def update(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()
         data = self.request.data
         data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新入库单 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库单"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新入库单 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库单"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新入库单失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="入库单"
+            )
+            raise
 
 
 
 
     def destroy(self, request, pk):
     def destroy(self, request, pk):
@@ -623,6 +700,13 @@ class BoundBatchViewSet(viewsets.ModelViewSet):
                 return Response(serializer.data, status=200, headers=headers)
                 return Response(serializer.data, status=200, headers=headers)
         except Exception as e:
         except Exception as e:
             print(e)
             print(e)
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建入库批次失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库批次"
+            )
             raise APIException({"detail": "{}".format(e)})
             raise APIException({"detail": "{}".format(e)})
     
     
     def add_batch_log(self, data, log_type, goods_qty):
     def add_batch_log(self, data, log_type, goods_qty):
@@ -658,19 +742,30 @@ class BoundBatchViewSet(viewsets.ModelViewSet):
     def update(self, request, pk):
     def update(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()
         data = self.request.data
         data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新入库批次 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库批次"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新入库批次 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库批次"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新入库批次失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="入库批次"
+            )
+            raise
 
 
     def destroy(self, request, pk):
     def destroy(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()
@@ -707,11 +802,25 @@ class BoundBatchViewSet(viewsets.ModelViewSet):
         batch_in_qty = request.data.get('batch_in_qty')
         batch_in_qty = request.data.get('batch_in_qty')
         
         
         if not batch_number:
         if not batch_number:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"批次进出库失败,批次号不能为空",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库批次"
+            )
             return Response({"code": 200,"message": "批次号不能为空","data": "批次进出库数目:{batch_in_qty}"}, status=200)
             return Response({"code": 200,"message": "批次号不能为空","data": "批次进出库数目:{batch_in_qty}"}, status=200)
             
             
         try:
         try:
             batch_obj = BoundBatchModel.objects.get(bound_number=batch_number, is_delete=False)
             batch_obj = BoundBatchModel.objects.get(bound_number=batch_number, is_delete=False)
         except BoundBatchModel.DoesNotExist:
         except BoundBatchModel.DoesNotExist:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"批次进出库失败,批次号不存在:{batch_number}",
+                operation_level="other",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="入库批次"
+            )
             return Response({"code": 200,"message": "批次号不存在","data": "批次进出库数目:{batch_in_qty}"}, status=200)
             return Response({"code": 200,"message": "批次号不存在","data": "批次进出库数目:{batch_in_qty}"}, status=200)
 
 
 
 
@@ -808,6 +917,13 @@ class BatchFileDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
             content_type="text/csv"
         )
         )
         response['Content-Disposition'] = "attachment; filename='batch_剩余量_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
         response['Content-Disposition'] = "attachment; filename='batch_剩余量_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        log_operation(
+            request=self.request,
+            operation_content=f"下载批次剩余量文件",
+            operation_level="download",
+            operator=self.request.auth.name if self.request.auth else None,
+            module_name="批次文件下载"
+        )
         return response
         return response
 
 
    
    
@@ -868,6 +984,14 @@ class BoundDetailViewSet(viewsets.ModelViewSet):
             detail_obj = BoundDetailModel.objects.get(bound_list=data['bound_list'], bound_batch=data['bound_batch'], is_delete=False)
             detail_obj = BoundDetailModel.objects.get(bound_list=data['bound_list'], bound_batch=data['bound_batch'], is_delete=False)
             serializer = self.get_serializer(detail_obj, many=False)
             serializer = self.get_serializer(detail_obj, many=False)
             headers = self.get_success_headers(serializer.data)
             headers = self.get_success_headers(serializer.data)
+            log_operation(
+                request=self.request,
+                operation_content=f"查询入库明细,入库单ID:{data['bound_list']}, 批次ID:{data['bound_batch']}",
+                operation_level="view",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=detail_obj.id,
+                module_name="入库明细"
+            )
             return Response(serializer.data, status=200, headers=headers)
             return Response(serializer.data, status=200, headers=headers)
         else:
         else:
             serializer = self.get_serializer(data=data)
             serializer = self.get_serializer(data=data)
@@ -889,19 +1013,30 @@ class BoundDetailViewSet(viewsets.ModelViewSet):
     def update(self, request, pk):
     def update(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()
         data = self.request.data
         data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新入库明细 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="入库明细"
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新入库明细 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="入库明细"
             )
             )
-        return Response(serializer.data, status=200, headers=headers)
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新入库明细失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="入库明细"
+            )
+            raise
 
 
     def destroy(self, request, pk):
     def destroy(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()
@@ -975,59 +1110,81 @@ class OutBoundDetailViewSet(viewsets.ModelViewSet):
 
 
     def create(self, request, *args, **kwargs):
     def create(self, request, *args, **kwargs):
         data = self.request.data
         data = self.request.data
-        data['openid'] = self.request.auth.openid
-        data.setdefault('is_delete', False)
+        try:
+            data['openid'] = self.request.auth.openid
+            data.setdefault('is_delete', False)
+
+            data['bound_batch_number'] = OutBatchModel.objects.get(id=data['bound_batch']).batch_number.id
+            # 验证并保存数据
+            data['detail_code'] = f"DC-{data['bound_list']:02}{data['bound_batch']:02}"
+            # print(data['detail_code'])
+            if OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).exists():
+                # 这里追加数目
+                detail_obj = OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).first()
+                OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).update(goods_qty=F('goods_qty')+data['goods_qty'])
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"更新出库明细 ID:{detail_obj.id},数量追加:{data['goods_qty']}",
+                    operation_level="update",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=detail_obj.id,
+                    module_name="出库明细"
+                )
+                return Response({"detail": "数据存在,数量追加"}, status=200)
+                # raise APIException({"detail": "Data exists"})
+            else:
+                serializer = self.get_serializer(data=data)
+                serializer.is_valid(raise_exception=True)
+                serializer.save()
 
 
-        data['bound_batch_number'] = OutBatchModel.objects.get(id=data['bound_batch']).batch_number.id
-        # 验证并保存数据
-        data['detail_code'] = f"DC-{data['bound_list']:02}{data['bound_batch']:02}"
-        # print(data['detail_code'])
-        if OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).exists():
-            # 这里追加数目
-            OutBoundDetailModel.objects.filter(detail_code=data['detail_code'], is_delete=False).update(goods_qty=F('goods_qty')+data['goods_qty'])
-            log_success_operation(
+                # 返回响应
+                headers = self.get_success_headers(serializer.data)
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"创建出库明细 ID:{serializer.data['id']},创建内容:{data}",
+                    operation_level="create",
+                    operator=self.request.auth.name if self.request.auth else None,
+                    object_id=serializer.data['id'],
+                    module_name="出库明细"
+                )
+                return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
                 request=self.request,
                 request=self.request,
-                operation_content=f"更新出库明细 ID:{data['id']}:{data}",
-                operation_level="update",
+                operation_content=f"创建出库明细失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
                 operator=self.request.auth.name if self.request.auth else None,
                 operator=self.request.auth.name if self.request.auth else None,
-                object_id=data['id'],
                 module_name="出库明细"
                 module_name="出库明细"
             )
             )
-            return Response({"detail": "数据存在,数量追加"}, status=200)
-            # raise APIException({"detail": "Data exists"})
-        else:
-            serializer = self.get_serializer(data=data)
+            raise
+
+    def update(self, request, pk):
+        qs = self.get_object()
+        data = self.request.data
+        try:
+            serializer = self.get_serializer(qs, data=data)
             serializer.is_valid(raise_exception=True)
             serializer.is_valid(raise_exception=True)
             serializer.save()
             serializer.save()
-
-            # 返回响应
             headers = self.get_success_headers(serializer.data)
             headers = self.get_success_headers(serializer.data)
             log_success_operation(
             log_success_operation(
                 request=self.request,
                 request=self.request,
-                operation_content=f"创建出库明细 ID:{serializer.data['id']},创建内容:{data}",
-                operation_level="create",
+                operation_content=f"更新出库明细 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
                 operator=self.request.auth.name if self.request.auth else None,
                 operator=self.request.auth.name if self.request.auth else None,
                 object_id=serializer.data['id'],
                 object_id=serializer.data['id'],
                 module_name="出库明细"
                 module_name="出库明细"
             )
             )
             return Response(serializer.data, status=200, headers=headers)
             return Response(serializer.data, status=200, headers=headers)
-
-    def update(self, request, pk):
-        qs = self.get_object()
-        data = self.request.data
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新出库明细 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="出库明细"
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新出库明细失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="出库明细"
             )
             )
-        return Response(serializer.data, status=200, headers=headers)
+            raise
 
 
     def destroy(self, request, pk):
     def destroy(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()
@@ -1102,64 +1259,85 @@ class OutBoundBatchViewSet(viewsets.ModelViewSet):
 
 
     def create(self, request, *args, **kwargs):
     def create(self, request, *args, **kwargs):
         data = self.request.data
         data = self.request.data
-        batch_obj = BoundBatchModel.objects.get(bound_number=data['out_number'])
-        if batch_obj is None:
-            raise APIException({"detail": "批次不存在"})
+        try:
+            batch_obj = BoundBatchModel.objects.get(bound_number=data['out_number'])
+            if batch_obj is None:
+                raise APIException({"detail": "批次不存在"})
 
 
-        bound_obj = BoundListModel.objects.get(id=data['bound_list_id'])
-        data['bound_list'] = bound_obj.id
-        data['batch_number'] = batch_obj.id
-        data['out_date'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
+            bound_obj = BoundListModel.objects.get(id=data['bound_list_id'])
+            data['bound_list'] = bound_obj.id
+            data['batch_number'] = batch_obj.id
+            data['out_date'] = str(timezone.now().strftime('%Y-%m-%d %H:%M:%S'))
 
 
-        data['openid'] = self.request.auth.openid
-        data.setdefault('is_delete', False)
-        data['goods_total_weight'] = data['goods_weight']*data['goods_out_qty']
-        from decimal import Decimal
+            data['openid'] = self.request.auth.openid
+            data.setdefault('is_delete', False)
+            data['goods_total_weight'] = data['goods_weight']*data['goods_out_qty']
+            from decimal import Decimal
 
 
-        #  data['goods_out_qty'] 是一个字符串或浮点数
-        data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
-        from decimal import Decimal
-        data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
-        data['goods_qty'] = batch_obj.goods_qty - batch_obj.goods_reserve_qty - data['goods_out_qty']
+            #  data['goods_out_qty'] 是一个字符串或浮点数
+            data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
+            from decimal import Decimal
+            data['goods_out_qty'] = Decimal(str(data['goods_out_qty']))
+            data['goods_qty'] = batch_obj.goods_qty - batch_obj.goods_reserve_qty - data['goods_out_qty']
 
 
-        data['status'] = 0  #现在处于出库申请状态
-   
-        serializer = self.get_serializer(data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        self.add_batch_log(serializer.data, 1, data['goods_out_qty'])
-        log_success_operation(
-            request=self.request,
-            operation_content=f"创建出库批次 ID:{serializer.data['id']},创建内容:{data}",
-            operation_level="create",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="出库批次"
-        )
-        
-        return Response(serializer.data, status=200, headers=headers)
+            data['status'] = 0  #现在处于出库申请状态
+       
+            serializer = self.get_serializer(data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            self.add_batch_log(serializer.data, 1, data['goods_out_qty'])
+            log_success_operation(
+                request=self.request,
+                operation_content=f"创建出库批次 ID:{serializer.data['id']},创建内容:{data}",
+                operation_level="create",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="出库批次"
+            )
+            
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"创建出库批次失败: {str(e)}, 创建内容:{data}",
+                operation_level="new",
+                operator=self.request.auth.name if self.request.auth else None,
+                module_name="出库批次"
+            )
+            raise
 
 
     def update(self, request, pk):        
     def update(self, request, pk):        
         qs = self.get_object()
         qs = self.get_object()
         data = self.request.data
         data = self.request.data
-        data['openid'] = self.request.auth.openid
-        data.setdefault('is_delete', False)
-        data['goods_total_weight'] = data['goods_weight']*data['goods_qty']
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        self.add_batch_log(serializer.data, 1, data['goods_qty'])
-        log_success_operation(
-            request=self.request,
-            operation_content=f"更新出库批次 ID:{serializer.data['id']}:{data}",
-            operation_level="update",
-            operator=self.request.auth.name if self.request.auth else None,
-            object_id=serializer.data['id'],
-            module_name="出库批次"
-        )
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            data['openid'] = self.request.auth.openid
+            data.setdefault('is_delete', False)
+            data['goods_total_weight'] = data['goods_weight']*data['goods_qty']
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            self.add_batch_log(serializer.data, 1, data['goods_qty'])
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新出库批次 ID:{serializer.data['id']}:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=serializer.data['id'],
+                module_name="出库批次"
+            )
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            log_failure_operation(
+                request=self.request,
+                operation_content=f"更新出库批次失败 ID:{qs.id}:{str(e)}, 更新内容:{data}",
+                operation_level="update",
+                operator=self.request.auth.name if self.request.auth else None,
+                object_id=qs.id,
+                module_name="出库批次"
+            )
+            raise
 
 
     def destroy(self, request, pk):
     def destroy(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 733 - 12
container/views.py


+ 200 - 42
erp/views.py

@@ -24,6 +24,7 @@ from django.db import transaction
 from bound.models import BoundListModel, BoundBatchModel, OutBatchModel,BoundDetailModel,OutBoundDetailModel,BatchOperateLogModel
 from bound.models import BoundListModel, BoundBatchModel, OutBatchModel,BoundDetailModel,OutBoundDetailModel,BatchOperateLogModel
 from warehouse.models import ProductListModel
 from warehouse.models import ProductListModel
 from rest_framework.response import Response
 from rest_framework.response import Response
+from operation_log.views import log_success_operation, log_failure_operation
 import json
 import json
 
 
 logger = logging.getLogger('wms.boundBill')
 logger = logging.getLogger('wms.boundBill')
@@ -1961,19 +1962,40 @@ class InboundBills(viewsets.ModelViewSet):
         return Response(return_data,status=status.HTTP_200_OK)
         return Response(return_data,status=status.HTTP_200_OK)
     
     
     def destroy(self, request, pk):
     def destroy(self, request, pk):
-
         qs = self.get_object()
         qs = self.get_object()
-        qs.is_delete = True
-        
-        MaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
-        qs.save()
-
-        # import datetime
-        # qs.save()
-        # qs.billId = f"404{qs.billId}{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.number = f"deleted_{qs.number}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.save()
-        return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        bill_number = qs.number
+        bill_id = qs.billId
+        try:
+            qs.is_delete = True
+            MaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
+            qs.save()
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"删除入库单 ID:{pk},单据编号: {bill_number}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP入库管理"
+                )
+            except Exception as e:
+                pass
+            return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"删除入库单失败 ID:{pk},单据编号: {bill_number},错误: {str(e)}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP入库管理"
+                )
+            except Exception as log_error:
+                pass
+            raise
     
     
     def update_inbound(self, request, *args, **kwargs):
     def update_inbound(self, request, *args, **kwargs):
         id = self.get_project()
         id = self.get_project()
@@ -1982,12 +2004,50 @@ class InboundBills(viewsets.ModelViewSet):
         else:
         else:
             bill_obj = InboundBill.objects.filter(billId=id).first()
             bill_obj = InboundBill.objects.filter(billId=id).first()
             if bill_obj:
             if bill_obj:
-                serializer = InboundApplyPOSTSerializer(bill_obj, data=request.data)
-                if serializer.is_valid():
-                    serializer.save()
-                    return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
-                else:
-                    return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                try:
+                    serializer = InboundApplyPOSTSerializer(bill_obj, data=request.data)
+                    if serializer.is_valid():
+                        serializer.save()
+                        # 记录成功日志
+                        try:
+                            log_success_operation(
+                                request=request,
+                                operation_content=f"更新入库单 ID:{id},单据编号: {bill_obj.number}",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP入库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
+                    else:
+                        # 记录失败日志(验证失败)
+                        try:
+                            log_failure_operation(
+                                request=request,
+                                operation_content=f"更新入库单失败 ID:{id},单据编号: {bill_obj.number},错误: 参数验证失败",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP入库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                except Exception as e:
+                    # 记录异常日志
+                    try:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"更新入库单异常 ID:{id},错误: {str(e)}",
+                            operation_level="update",
+                            operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                            module_name="ERP入库管理"
+                        )
+                    except Exception as log_error:
+                        pass
+                    raise
             else:
             else:
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
 
 
@@ -2048,19 +2108,40 @@ class OutboundBills(viewsets.ModelViewSet):
         return Response(return_data,status=status.HTTP_200_OK)
         return Response(return_data,status=status.HTTP_200_OK)
     
     
     def destroy(self, request, pk):
     def destroy(self, request, pk):
-
         qs = self.get_object()
         qs = self.get_object()
-        qs.is_delete = True
-        
-        OutMaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
-        qs.save()
-
-        # import datetime
-        # qs.save()
-        # qs.billId = f"404{qs.billId}{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.number = f"deleted_{qs.number}_{datetime.datetime.now().strftime('%Y%m%d%H%M%S')}"
-        # qs.save()
-        return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        bill_number = qs.number
+        bill_id = qs.billId
+        try:
+            qs.is_delete = True
+            OutMaterialDetail.objects.filter(bound_billId=qs).update(is_delete=True)
+            qs.save()
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"删除出库单 ID:{pk},单据编号: {bill_number}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP出库管理"
+                )
+            except Exception as e:
+                pass
+            return Response({'message': '删除成功'}, status=status.HTTP_200_OK)
+        except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"删除出库单失败 ID:{pk},单据编号: {bill_number},错误: {str(e)}",
+                    operation_level="delete",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    object_id=pk,
+                    module_name="ERP出库管理"
+                )
+            except Exception as log_error:
+                pass
+            raise
     
     
     def update_outbound(self, request, *args, **kwargs):
     def update_outbound(self, request, *args, **kwargs):
         id = self.get_project()
         id = self.get_project()
@@ -2069,12 +2150,50 @@ class OutboundBills(viewsets.ModelViewSet):
         else:
         else:
             bill_obj = OutboundBill.objects.filter(billId=id).first()
             bill_obj = OutboundBill.objects.filter(billId=id).first()
             if bill_obj:
             if bill_obj:
-                serializer = OutboundApplyPOSTSerializer(bill_obj, data=request.data)
-                if serializer.is_valid():
-                    serializer.save()
-                    return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
-                else:
-                    return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                try:
+                    serializer = OutboundApplyPOSTSerializer(bill_obj, data=request.data)
+                    if serializer.is_valid():
+                        serializer.save()
+                        # 记录成功日志
+                        try:
+                            log_success_operation(
+                                request=request,
+                                operation_content=f"更新出库单 ID:{id},单据编号: {bill_obj.number}",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP出库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
+                    else:
+                        # 记录失败日志(验证失败)
+                        try:
+                            log_failure_operation(
+                                request=request,
+                                operation_content=f"更新出库单失败 ID:{id},单据编号: {bill_obj.number},错误: 参数验证失败",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=bill_obj.id if hasattr(bill_obj, 'id') else None,
+                                module_name="ERP出库管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '上传参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                except Exception as e:
+                    # 记录异常日志
+                    try:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"更新出库单异常 ID:{id},错误: {str(e)}",
+                            operation_level="update",
+                            operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                            module_name="ERP出库管理"
+                        )
+                    except Exception as log_error:
+                        pass
+                    raise
             else:
             else:
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
 
 
@@ -2125,12 +2244,51 @@ class Materials(viewsets.ModelViewSet):
         else:
         else:
             material_obj = MaterialDetail.objects.filter(id=id).first()
             material_obj = MaterialDetail.objects.filter(id=id).first()
             if material_obj:
             if material_obj:
-                serializer = MaterialDetailPOSTSerializer(material_obj, data=request.data)
-                if serializer.is_valid():
-                    serializer.save()
-                    return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
-                else:
-                    return Response({'message': '参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                try:
+                    serializer = MaterialDetailPOSTSerializer(material_obj, data=request.data)
+                    if serializer.is_valid():
+                        serializer.save()
+                        # 记录成功日志
+                        try:
+                            log_success_operation(
+                                request=request,
+                                operation_content=f"更新物料明细 ID:{id},物料编码: {material_obj.goods_code}",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=id,
+                                module_name="ERP物料管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '操作成功', 'data': serializer.data, 'code': 200}, status=status.HTTP_200_OK)
+                    else:
+                        # 记录失败日志(验证失败)
+                        try:
+                            log_failure_operation(
+                                request=request,
+                                operation_content=f"更新物料明细失败 ID:{id},错误: 参数验证失败",
+                                operation_level="update",
+                                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                                object_id=id,
+                                module_name="ERP物料管理"
+                            )
+                        except Exception as e:
+                            pass
+                        return Response({'message': '参数错误', 'data': None, 'code': 400}, status=status.HTTP_200_OK)
+                except Exception as e:
+                    # 记录异常日志
+                    try:
+                        log_failure_operation(
+                            request=request,
+                            operation_content=f"更新物料明细异常 ID:{id},错误: {str(e)}",
+                            operation_level="update",
+                            operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                            object_id=id,
+                            module_name="ERP物料管理"
+                        )
+                    except Exception as log_error:
+                        pass
+                    raise
             else:
             else:
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)        
                 return Response({'message': '数据不存在', 'data': None, 'code': 400}, status=status.HTTP_200_OK)        
 
 

+ 1 - 0
greaterwms/urls.py

@@ -32,6 +32,7 @@ urlpatterns = [
     path ('wms/', include('erp.urls')),
     path ('wms/', include('erp.urls')),
     path ('backup/', include('backup.urls')),
     path ('backup/', include('backup.urls')),
     path('location_statistics/', include('location_statistics.urls')),
     path('location_statistics/', include('location_statistics.urls')),
+    path('operation_log/', include('operation_log.urls')),
 
 
     re_path(r'^favicon\.ico$', views.favicon, name='favicon'),
     re_path(r'^favicon\.ico$', views.favicon, name='favicon'),
     re_path('^css/.*$', views.css, name='css'),
     re_path('^css/.*$', views.css, name='css'),

+ 217 - 0
operation_log/README.md

@@ -0,0 +1,217 @@
+# 操作日志模块使用说明
+
+## 概述
+
+操作日志模块用于记录系统中的用户敏感操作,支持按操作级别进行分类,并提供前端界面查看操作历史。
+
+## 功能特性
+
+1. **操作级别分类**:
+   - `view`: 查看操作(蓝色)
+   - `update`: 更新操作(橙色)
+   - `new`: 新增操作(绿色)
+   - `delete`: 删除操作(红色)
+   - `download`: 下载操作(紫色)
+   - `login`: 登录操作(青色)
+   - `logout`: 登出操作(灰色)
+   - `other`: 其他操作(棕色)
+
+2. **操作结果记录**:
+   - `success`: 成功
+   - `failure`: 失败
+
+3. **详细信息记录**:
+   - 操作者信息
+   - IP地址
+   - 请求方法和路径
+   - 模块名称
+   - 操作对象ID
+
+## 使用方法
+
+### 在视图函数中记录操作日志
+
+#### 1. 导入日志记录函数
+
+```python
+from operation_log.views import log_operation, log_success_operation, log_failure_operation
+```
+
+#### 2. 记录成功操作
+
+```python
+def create(self, request, *args, **kwargs):
+    # ... 执行创建操作 ...
+    serializer.save()
+    
+    # 记录操作日志
+    log_success_operation(
+        request=self.request,
+        operation_content=f"创建新记录 ID:{serializer.data['id']}",
+        operation_level="new",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=serializer.data['id'],
+        module_name="模块名称"
+    )
+    
+    return Response(serializer.data, status=200)
+```
+
+#### 3. 记录更新操作
+
+```python
+def update(self, request, pk):
+    # ... 执行更新操作 ...
+    serializer.save()
+    
+    log_success_operation(
+        request=self.request,
+        operation_content=f"更新记录 ID:{pk}:{request.data}",
+        operation_level="update",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    return Response(serializer.data, status=200)
+```
+
+#### 4. 记录删除操作
+
+```python
+def destroy(self, request, pk):
+    # ... 执行删除操作 ...
+    
+    log_success_operation(
+        request=self.request,
+        operation_content=f"删除记录 ID:{pk}",
+        operation_level="delete",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    return Response(status=204)
+```
+
+#### 5. 记录失败操作
+
+```python
+def create(self, request, *args, **kwargs):
+    try:
+        # ... 执行操作 ...
+        serializer.save()
+        
+        log_success_operation(
+            request=self.request,
+            operation_content="创建记录成功",
+            operation_level="new",
+            module_name="模块名称"
+        )
+        
+        return Response(serializer.data, status=200)
+    except Exception as e:
+        # 记录失败操作
+        log_failure_operation(
+            request=self.request,
+            operation_content=f"创建记录失败: {str(e)}",
+            operation_level="new",
+            module_name="模块名称"
+        )
+        
+        raise APIException({"detail": str(e)})
+```
+
+#### 6. 记录查看操作
+
+```python
+def retrieve(self, request, pk):
+    obj = self.get_object()
+    
+    log_operation(
+        request=self.request,
+        operation_content=f"查看记录 ID:{pk}",
+        operation_level="view",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    serializer = self.get_serializer(obj)
+    return Response(serializer.data)
+```
+
+#### 7. 记录下载操作
+
+```python
+def download_file(self, request, pk):
+    # ... 执行下载操作 ...
+    
+    log_success_operation(
+        request=self.request,
+        operation_content=f"下载文件 ID:{pk}",
+        operation_level="download",
+        operator=self.request.auth.name if self.request.auth else None,
+        object_id=pk,
+        module_name="模块名称"
+    )
+    
+    return response
+```
+
+## API接口
+
+### 获取操作日志列表
+
+**GET** `/operation_log/`
+
+**查询参数**:
+- `page`: 页码(默认:1)
+- `page_size`: 每页数量(默认:11)
+- `operation_level`: 操作级别(可选值:view, update, new, delete, download, login, logout, other)
+- `operation_result`: 操作结果(可选值:success, failure)
+- `module_name`: 模块名称
+- `operator`: 操作者(支持模糊搜索)
+- `operation_content__icontains`: 操作内容(模糊搜索)
+- `operation_time__range`: 操作时间范围(格式:YYYY-MM-DD,YYYY-MM-DD)
+
+**示例**:
+```
+GET /operation_log/?operation_level=delete&operation_result=success&page=1
+```
+
+### 获取操作日志详情
+
+**GET** `/operation_log/{id}/`
+
+## 前端界面
+
+前端操作日志页面路径:`/operationlog`
+
+**功能**:
+1. 显示所有操作日志记录
+2. 按操作级别使用不同颜色标识
+3. 支持按操作级别、操作结果、模块名称筛选
+4. 支持按操作内容搜索
+5. 支持按时间范围筛选
+6. 支持分页查看
+
+## 操作级别颜色说明
+
+- **查看 (view)**: 蓝色
+- **更新 (update)**: 橙色
+- **新增 (new)**: 绿色
+- **删除 (delete)**: 红色
+- **下载 (download)**: 紫色
+- **登录 (login)**: 青色
+- **登出 (logout)**: 灰色
+- **其他 (other)**: 棕色
+
+## 注意事项
+
+1. 操作日志记录失败不会影响主要业务逻辑
+2. 操作日志只读,不能通过API创建、更新或删除
+3. 建议在关键操作的视图函数中添加日志记录
+4. 操作级别应该根据实际业务需求选择合适的值
+5. 建议为每个模块设置统一的 `module_name` 以便后续筛选和统计
+

+ 20 - 0
operation_log/filter.py

@@ -0,0 +1,20 @@
+from django_filters import FilterSet
+from .models import OperationLog
+
+
+class OperationLogFilter(FilterSet):
+    class Meta:
+        model = OperationLog
+        fields = {
+            "id": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
+            "operator": ['exact', 'iexact', 'contains', 'icontains'],
+            "operation_content": ['exact', 'iexact', 'contains', 'icontains'],
+            "operation_level": ['exact', 'iexact', 'in'],
+            "operation_result": ['exact', 'iexact', 'in'],
+            "module_name": ['exact', 'iexact', 'contains', 'icontains'],
+            "object_id": ['exact', 'iexact', 'contains', 'icontains'],
+            "ip_address": ['exact', 'iexact'],
+            "request_method": ['exact', 'iexact', 'in'],
+            "request_path": ['exact', 'iexact', 'contains', 'icontains'],
+            "operation_time": ['year', 'month', 'day', 'week_day', 'gt', 'gte', 'lt', 'lte', 'range'],
+        }

+ 13 - 0
operation_log/serializers.py

@@ -0,0 +1,13 @@
+from rest_framework import serializers
+from .models import OperationLog
+
+
+class OperationLogSerializer(serializers.ModelSerializer):
+    operation_time = serializers.DateTimeField(read_only=True, format='%Y-%m-%d %H:%M:%S')
+    operation_level_display = serializers.CharField(source='get_operation_level_display', read_only=True)
+    operation_result_display = serializers.CharField(source='get_operation_result_display', read_only=True)
+
+    class Meta:
+        model = OperationLog
+        fields = '__all__'
+        read_only_fields = ['id', 'operation_time']

+ 12 - 0
operation_log/urls.py

@@ -0,0 +1,12 @@
+from django.urls import path, re_path
+from . import views
+
+urlpatterns = [
+    # 操作日志列表查询接口(只读)
+    path(r'', views.OperationLogViewSet.as_view({"get": "list"}), name="operation_log"),
+    
+    # 操作日志详情查询接口
+    re_path(r'^(?P<pk>\d+)/$', views.OperationLogViewSet.as_view({
+        'get': 'retrieve'
+    }), name="operation_log_detail"),
+]

+ 40 - 6
operation_log/views.py

@@ -1,5 +1,11 @@
 from django.utils import timezone
 from django.utils import timezone
 from .models import OperationLog
 from .models import OperationLog
+from rest_framework import viewsets
+from rest_framework.filters import OrderingFilter
+from django_filters.rest_framework import DjangoFilterBackend
+from utils.page import MyPageNumberPagination
+from .serializers import OperationLogSerializer
+from .filter import OperationLogFilter
 
 
 class OperationLogger:
 class OperationLogger:
     @staticmethod
     @staticmethod
@@ -60,15 +66,29 @@ class OperationLogger:
         if operator is not None:
         if operator is not None:
             return str(operator)
             return str(operator)
         
         
-        # 尝试从request中获取用户信息
+        # 优先尝试从request.auth获取(系统的自定义认证方式)
+        if hasattr(request, 'auth') and request.auth:
+            # 如果auth对象有name属性,优先使用
+            if hasattr(request.auth, 'name') and request.auth.name:
+                return str(request.auth.name)
+            # 如果有openid属性
+            elif hasattr(request.auth, 'openid') and request.auth.openid:
+                return f"用户OpenID:{request.auth.openid}"
+            # 如果auth本身是字符串
+            elif isinstance(request.auth, str):
+                return request.auth
+        
+        # 尝试从request.user获取(标准Django认证)
         if hasattr(request, 'user') and request.user.is_authenticated:
         if hasattr(request, 'user') and request.user.is_authenticated:
             # 优先使用username,如果没有则使用其他标识
             # 优先使用username,如果没有则使用其他标识
-            if hasattr(request.user, 'username'):
+            if hasattr(request.user, 'username') and request.user.username:
                 return request.user.username
                 return request.user.username
             elif hasattr(request.user, 'get_username'):
             elif hasattr(request.user, 'get_username'):
-                return request.user.get_username()
-            else:
-                return f"用户ID:{request.user.id}" if hasattr(request.user, 'id') else "认证用户"
+                username = request.user.get_username()
+                if username:
+                    return username
+            if hasattr(request.user, 'id'):
+                return f"用户ID:{request.user.id}"
         
         
         # 如果都没有,返回未知用户
         # 如果都没有,返回未知用户
         return "未知用户"
         return "未知用户"
@@ -138,4 +158,18 @@ def log_failure_operation(request, operation_content, operation_level, operator=
         operation_result='failure',
         operation_result='failure',
         module_name=module_name,
         module_name=module_name,
         object_id=object_id
         object_id=object_id
-    )
+    )
+
+
+class OperationLogViewSet(viewsets.ReadOnlyModelViewSet):
+    """
+    操作日志 ViewSet
+    只读,不支持创建、更新、删除操作
+    """
+    queryset = OperationLog.objects.all()
+    serializer_class = OperationLogSerializer
+    pagination_class = MyPageNumberPagination
+    filter_backends = [DjangoFilterBackend, OrderingFilter]
+    filter_class = OperationLogFilter
+    ordering_fields = ['operation_time', 'id']
+    ordering = ['-operation_time']  # 默认按操作时间倒序排列

+ 1 - 1
reportcenter/files.py

@@ -85,7 +85,7 @@ def BatchList_cn_data_header_list():
         ('bound_number', '入库批次号'),
         ('bound_number', '入库批次号'),
         ('goods_qty', '商品数量'),
         ('goods_qty', '商品数量'),
         ('goods_in_qty', '组盘入库数量'),
         ('goods_in_qty', '组盘入库数量'),
-        ('goods_in_location_qty', '库位入库数量'),
+        ('goods_in_location_qty', '当前在库数目'),
         ('goods_out_qty', '出库数量'),
         ('goods_out_qty', '出库数量'),
 
 
 
 

+ 62 - 0
reportcenter/views.py

@@ -39,6 +39,7 @@ from django.db.models import Q, Max, Min
 from decimal import Decimal
 from decimal import Decimal
 from django.db.models import Sum
 from django.db.models import Sum
 from django.db.models import OuterRef, Subquery
 from django.db.models import OuterRef, Subquery
+from operation_log.views import log_success_operation, log_failure_operation
 
 
 
 
     
     
@@ -267,6 +268,17 @@ class MaterialChangeHistoryViewSet(viewsets.ModelViewSet):
         response = HttpResponse(content_type='text/csv')
         response = HttpResponse(content_type='text/csv')
         response['Content-Disposition'] = 'attachment; filename="material_change_summary.csv"'
         response['Content-Disposition'] = 'attachment; filename="material_change_summary.csv"'
         
         
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载物料库存变动汇总文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
+        
         # 创建CSV写入器
         # 创建CSV写入器
         csv_writer = csv.writer(response)
         csv_writer = csv.writer(response)
         
         
@@ -598,6 +610,16 @@ class MaterialChangeHistoryDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
             content_type="text/csv"
         )
         )
         response['Content-Disposition'] = "attachment; filename='MaterialChangeHistory_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
         response['Content-Disposition'] = "attachment; filename='MaterialChangeHistory_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载物料变动历史文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
         return response
 
 
     
     
@@ -669,6 +691,16 @@ class batchLogDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
             content_type="text/csv"
         )
         )
         response['Content-Disposition'] = "attachment; filename='batchLog_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
         response['Content-Disposition'] = "attachment; filename='batchLog_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载批次日志文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
         return response
 
 
 class ContainerDetailLogViewSet(viewsets.ModelViewSet):
 class ContainerDetailLogViewSet(viewsets.ModelViewSet):
@@ -737,6 +769,16 @@ class ContainerDetailLogDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
             content_type="text/csv"
         )
         )
         response['Content-Disposition'] = "attachment; filename='ContainerDetailLog_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
         response['Content-Disposition'] = "attachment; filename='ContainerDetailLog_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载托盘详情日志文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
         return response
 
 
 
 
@@ -797,6 +839,16 @@ class FileListDownloadView(viewsets.ModelViewSet):
             content_type="text/csv"
             content_type="text/csv"
         )
         )
         response['Content-Disposition'] = "attachment; filename='stocklist_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
         response['Content-Disposition'] = "attachment; filename='stocklist_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载库存流水文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
         return response
 
 
 class FlowsStatsViewSet(viewsets.ModelViewSet):
 class FlowsStatsViewSet(viewsets.ModelViewSet):
@@ -936,6 +988,16 @@ class BatchFileViewSet(viewsets.ModelViewSet):
             content_type="text/csv"
             content_type="text/csv"
         )
         )
         response['Content-Disposition'] = "attachment; filename='批次报表_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
         response['Content-Disposition'] = "attachment; filename='批次报表_{}.csv'".format(str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载批次列表文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="报表中心"
+            )
+        except Exception as e:
+            pass
         return response
         return response
 
 
 class bigScreenModelViewSet(viewsets.ModelViewSet):
 class bigScreenModelViewSet(viewsets.ModelViewSet):

+ 102 - 3
staff/views.py

@@ -20,6 +20,7 @@ from utils.md5 import Md5
 import random
 import random
 from django.contrib.auth.models import User
 from django.contrib.auth.models import User
 from .models import Role, Permission  # 新增角色和权限模型导入
 from .models import Role, Permission  # 新增角色和权限模型导入
+from operation_log.views import log_success_operation, log_failure_operation, log_operation
 
 
 class APIViewSet(viewsets.ModelViewSet):
 class APIViewSet(viewsets.ModelViewSet):
     """
     """
@@ -172,6 +173,17 @@ class APIViewSet(viewsets.ModelViewSet):
             serializer.is_valid(raise_exception=True)
             serializer.is_valid(raise_exception=True)
             serializer.save()
             serializer.save()
             headers = self.get_success_headers(serializer.data)
             headers = self.get_success_headers(serializer.data)
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"创建员工:{data.get('staff_name', '未知')}",
+                    operation_level="new",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=serializer.data.get('id'),
+                    module_name="员工管理"
+                )
+            except Exception as e:
+                pass  # 日志记录失败不影响业务
             return Response(serializer.data, status=200, headers=headers)
             return Response(serializer.data, status=200, headers=headers)
 
 
     def update(self, request, pk):
     def update(self, request, pk):
@@ -212,6 +224,17 @@ class APIViewSet(viewsets.ModelViewSet):
                     t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
                     t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
                 ))
                 ))
         headers = self.get_success_headers(serializer.data)
         headers = self.get_success_headers(serializer.data)
+        try:
+            log_success_operation(
+                request=self.request,
+                operation_content=f"更新员工 ID:{pk}:{data.get('staff_name', '未知')}",
+                operation_level="update",
+                operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                object_id=pk,
+                module_name="员工管理"
+            )
+        except Exception as e:
+            pass
         return Response(serializer.data, status=200, headers=headers)
         return Response(serializer.data, status=200, headers=headers)
 
 
     def change_check_code(self, request, pk=None):
     def change_check_code(self, request, pk=None):
@@ -257,6 +280,17 @@ class APIViewSet(viewsets.ModelViewSet):
             # 若不存在则创建,保持行为幂等
             # 若不存在则创建,保持行为幂等
             User.objects.create_user(username=str(target_staff.staff_name), password=str(new_code_int))
             User.objects.create_user(username=str(target_staff.staff_name), password=str(new_code_int))
 
 
+        try:
+            log_success_operation(
+                request=self.request,
+                operation_content=f"修改员工验证码 ID:{target_staff.id},员工:{target_staff.staff_name}",
+                operation_level="update",
+                operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                object_id=target_staff.id,
+                module_name="员工管理"
+            )
+        except Exception as e:
+            pass
         return Response({"message": "验证码已更新", "id": target_staff.id, "check_code": target_staff.check_code})
         return Response({"message": "验证码已更新", "id": target_staff.id, "check_code": target_staff.check_code})
 
 
     def partial_update(self, request, pk):
     def partial_update(self, request, pk):
@@ -294,6 +328,17 @@ class APIViewSet(viewsets.ModelViewSet):
                         t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
                         t_code=Md5.md5(str(timezone.now())), developer=1, ip=request.META.get('REMOTE_ADDR')
                     ))
                     ))
             headers = self.get_success_headers(serializer.data)
             headers = self.get_success_headers(serializer.data)
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"部分更新员工 ID:{pk}",
+                    operation_level="update",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="员工管理"
+                )
+            except Exception as e:
+                pass
             return Response(serializer.data, status=200, headers=headers)
             return Response(serializer.data, status=200, headers=headers)
 
 
     def destroy(self, request, pk):
     def destroy(self, request, pk):
@@ -305,6 +350,17 @@ class APIViewSet(viewsets.ModelViewSet):
             qs.save()
             qs.save()
             serializer = self.get_serializer(qs, many=False)
             serializer = self.get_serializer(qs, many=False)
             headers = self.get_success_headers(serializer.data)
             headers = self.get_success_headers(serializer.data)
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"删除员工 ID:{pk},员工名:{qs.staff_name}",
+                    operation_level="delete",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="员工管理"
+                )
+            except Exception as e:
+                pass
             return Response(serializer.data, status=200, headers=headers)
             return Response(serializer.data, status=200, headers=headers)
 
 
 class RoleViewSet(viewsets.ModelViewSet):
 class RoleViewSet(viewsets.ModelViewSet):
@@ -415,6 +471,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
             {"primary_page": "staff", "path": "/staff/stafflist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "staff", "path": "/staff/stafflist", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "staff", "path": "/staff/stafflist_check_code", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "staff", "path": "/staff/stafflist_check_code", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "staff", "path": "/staff/stafftype", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "staff", "path": "/staff/stafftype", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
+            {"primary_page": "staff", "path": "/staff/operationlog", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "uploadcenter", "path": "/uploadcenter/initializeupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "uploadcenter", "path": "/uploadcenter/initializeupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "uploadcenter", "path": "/uploadcenter/addupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "uploadcenter", "path": "/uploadcenter/addupload", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadinbound", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
             {"primary_page": "downloadcenter", "path": "/downloadcenter/downloadinbound", "components": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]},
@@ -442,7 +499,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
                     "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
                     "/warehouse/department", "/warehouse/boundcodetype", "/warehouse/boundtype", "/warehouse/boundbusiness", "/warehouse/status", "/warehouse/product",
                     "/warehouse/department", "/warehouse/boundcodetype", "/warehouse/boundtype", "/warehouse/boundbusiness", "/warehouse/status", "/warehouse/product",
-                    "/staff/stafflist", "/staff/stafflist_check_code", "/staff/stafftype"
+                    "/staff/stafflist", "/staff/stafflist_check_code", "/staff/stafftype", "/staff/operationlog"
                 ],
                 ],
                 "component_access": "all"
                 "component_access": "all"
             },
             },
@@ -457,7 +514,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/taskpage/task",
                     "/taskpage/task",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
                     "/dashboard/flows_statements", "/dashboard/flows", "/dashboard/MaterialChangeHistory", "/dashboard/batchlog", "/dashboard/ContainerDetailLogModel",
-                    "/staff/stafflist", "/staff/stafftype"
+                    "/staff/stafflist", "/staff/stafftype", "/staff/operationlog"
                 ],
                 ],
                 "component_access": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]
                 "component_access": ["view", "edit", "add", "delete", "export", "confirm", "adjust", "download"]
             },
             },
@@ -471,6 +528,7 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
                     "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
                     "/taskpage/task",
                     "/taskpage/task",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
+                    "/staff/operationlog",
                     "/dashboard/flows_statements", "/dashboard/flows"
                     "/dashboard/flows_statements", "/dashboard/flows"
                 ],
                 ],
                 "component_access": ["view", "edit", "add", "download", "confirm"]
                 "component_access": ["view", "edit", "add", "download", "confirm"]
@@ -485,7 +543,8 @@ class RolePermissionViewSet(viewsets.ViewSet):
                     "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
                     "/outbound/dn", "/outbound/backorder", "/outbound/container_check",
                     "/taskpage/task",
                     "/taskpage/task",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
                     "/count/batch", "/count/countbatchlog", "/count/detaillog", "/count/batchoperatelog",
-                    "/dashboard/flows_statements", "/dashboard/flows"
+                    "/dashboard/flows_statements", "/dashboard/flows",
+                    "/staff/operationlog",
                 ],
                 ],
                 "component_access": ["view", "download"]
                 "component_access": ["view", "download"]
             }
             }
@@ -574,6 +633,16 @@ class RolePermissionViewSet(viewsets.ViewSet):
                         role.permissions.set(assigned_permissions)
                         role.permissions.set(assigned_permissions)
                         permission_count += len(assigned_permissions)
                         permission_count += len(assigned_permissions)
             
             
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"恢复默认权限配置,创建权限数:{created_perm_count},更新角色数:{role_count}",
+                    operation_level="update",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    module_name="权限管理"
+                )
+            except Exception as e:
+                pass
             return Response({
             return Response({
                 "message": "默认权限已恢复成功",
                 "message": "默认权限已恢复成功",
                 "created_permissions": created_perm_count,
                 "created_permissions": created_perm_count,
@@ -581,6 +650,16 @@ class RolePermissionViewSet(viewsets.ViewSet):
                 "permissions_assigned": permission_count
                 "permissions_assigned": permission_count
             })
             })
         except Exception as e:
         except Exception as e:
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"恢复默认权限配置失败:{str(e)}",
+                    operation_level="update",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    module_name="权限管理"
+                )
+            except:
+                pass
             return Response({
             return Response({
                 "error": "恢复默认权限失败",
                 "error": "恢复默认权限失败",
                 "detail": str(e)
                 "detail": str(e)
@@ -653,6 +732,16 @@ class RolePermissionViewSet(viewsets.ViewSet):
             if new_perms:
             if new_perms:
                 role.permissions.add(*new_perms)
                 role.permissions.add(*new_perms)
             
             
+            try:
+                log_success_operation(
+                    request=request,
+                    operation_content=f"更新角色权限:{pk},页面:{target_page}",
+                    operation_level="update",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    module_name="权限管理"
+                )
+            except Exception as e:
+                pass
             return Response({"message": "Permissions updated successfully"})
             return Response({"message": "Permissions updated successfully"})
         except Role.DoesNotExist:
         except Role.DoesNotExist:
             return Response({"error": "Role not found"}, status=404)
             return Response({"error": "Role not found"}, status=404)
@@ -811,4 +900,14 @@ class FileDownloadView(viewsets.ModelViewSet):
         )
         )
         response['Content-Disposition'] = "attachment; filename='staff_{}.csv'".format(
         response['Content-Disposition'] = "attachment; filename='staff_{}.csv'".format(
             str(dt.strftime('%Y%m%d%H%M%S%f')))
             str(dt.strftime('%Y%m%d%H%M%S%f')))
+        try:
+            log_success_operation(
+                request=request,
+                operation_content="下载员工列表文件",
+                operation_level="download",
+                operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                module_name="员工管理"
+            )
+        except Exception as e:
+            pass
         return response
         return response

+ 55 - 5
stock/views.py

@@ -16,6 +16,7 @@ from .files import FileListRenderCN, FileListRenderEN, FileBinListRenderCN, File
 from rest_framework.settings import api_settings
 from rest_framework.settings import api_settings
 from django.db import transaction  
 from django.db import transaction  
 from rest_framework import status
 from rest_framework import status
+from operation_log.views import log_success_operation, log_failure_operation
 
 
 class stockshelfViewSet(viewsets.ModelViewSet):
 class stockshelfViewSet(viewsets.ModelViewSet):
 
 
@@ -94,7 +95,29 @@ class stockshelfViewSet(viewsets.ModelViewSet):
         try:
         try:
             with transaction.atomic():
             with transaction.atomic():
                 shelflist.objects.bulk_create(instances, batch_size=1000)
                 shelflist.objects.bulk_create(instances, batch_size=1000)
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"批量创建货架位置,仓库: {warehouse_name}({warehouse_code}), 货架: {shelf_name}, 创建数量: {len(instances)}, 跳过数量: {(rows*cols*layers) - len(instances)}",
+                    operation_level="new",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    module_name="库存管理"
+                )
+            except Exception as e:
+                pass
         except Exception as e:
         except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=self.request,
+                    operation_content=f"批量创建货架位置失败,仓库: {warehouse_name}({warehouse_code}), 货架: {shelf_name}, 错误: {str(e)}",
+                    operation_level="new",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    module_name="库存管理"
+                )
+            except Exception as log_error:
+                pass
             return Response(
             return Response(
                 {"error": str(e)},
                 {"error": str(e)},
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR
                 status=status.HTTP_500_INTERNAL_SERVER_ERROR
@@ -112,11 +135,38 @@ class stockshelfViewSet(viewsets.ModelViewSet):
        
        
         data = self.request.data
         data = self.request.data
         print(data)
         print(data)
-        serializer = self.get_serializer(qs, data=data)
-        serializer.is_valid(raise_exception=True)
-        serializer.save()
-        headers = self.get_success_headers(serializer.data)
-        return Response(serializer.data, status=200, headers=headers)
+        try:
+            serializer = self.get_serializer(qs, data=data)
+            serializer.is_valid(raise_exception=True)
+            serializer.save()
+            headers = self.get_success_headers(serializer.data)
+            # 记录成功日志
+            try:
+                log_success_operation(
+                    request=self.request,
+                    operation_content=f"更新货架位置 ID:{pk},仓库: {qs.warehouse_name}({qs.warehouse_code}), 货架: {qs.shelf_name}",
+                    operation_level="update",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="库存管理"
+                )
+            except Exception as e:
+                pass
+            return Response(serializer.data, status=200, headers=headers)
+        except Exception as e:
+            # 记录失败日志
+            try:
+                log_failure_operation(
+                    request=self.request,
+                    operation_content=f"更新货架位置失败 ID:{pk},错误: {str(e)}",
+                    operation_level="update",
+                    operator=self.request.auth.name if hasattr(self.request, 'auth') and self.request.auth else None,
+                    object_id=pk,
+                    module_name="库存管理"
+                )
+            except Exception as log_error:
+                pass
+            raise
 
 
     def retrieve(self, request, pk):
     def retrieve(self, request, pk):
         qs = self.get_object()
         qs = self.get_object()

+ 0 - 1
templates/dist/spa/css/13.8b9d1b53.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-11392faa]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-11392faa]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-11392faa]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-11392faa]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-11392faa]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-11392faa]{color:#485573}

+ 1 - 0
templates/dist/spa/css/13.b352291e.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-1ea9522c]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-1ea9522c]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-1ea9522c]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-1ea9522c]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-1ea9522c]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-1ea9522c]{color:#485573}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
templates/dist/spa/css/33.103b121d.css


templates/dist/spa/css/33.0d4c4716.css → templates/dist/spa/css/34.0d4c4716.css


+ 0 - 1
templates/dist/spa/css/34.c527e777.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-0bb3e2dd]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-0bb3e2dd]{background-color:rgba(25,118,210,0.1)}

+ 1 - 0
templates/dist/spa/css/35.5557cd4a.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-878c80e6]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-878c80e6]{background-color:rgba(25,118,210,0.1)}

templates/dist/spa/css/35.8f3f6188.css → templates/dist/spa/css/36.8f3f6188.css


templates/dist/spa/css/36.44ddcebd.css → templates/dist/spa/css/37.44ddcebd.css


templates/dist/spa/css/37.2ac1dad1.css → templates/dist/spa/css/38.2ac1dad1.css


templates/dist/spa/css/38.12670fd1.css → templates/dist/spa/css/39.12670fd1.css


templates/dist/spa/css/39.9478c981.css → templates/dist/spa/css/40.9478c981.css


templates/dist/spa/css/40.c4652654.css → templates/dist/spa/css/41.c4652654.css


templates/dist/spa/css/41.7a23b7fb.css → templates/dist/spa/css/42.7a23b7fb.css


templates/dist/spa/css/42.2594d0b9.css → templates/dist/spa/css/43.2594d0b9.css


templates/dist/spa/css/43.0faa4aeb.css → templates/dist/spa/css/44.0faa4aeb.css


+ 0 - 1
templates/dist/spa/css/7.8879267a.css

@@ -1 +0,0 @@
-.q-date__calendar-item--selected[data-v-264cfcfd]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-264cfcfd]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-264cfcfd]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-264cfcfd]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-264cfcfd]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-264cfcfd]{color:#485573}

+ 1 - 0
templates/dist/spa/css/7.b5042fa0.css

@@ -0,0 +1 @@
+.q-date__calendar-item--selected[data-v-6eb8828c]{transition:all 0.3s ease;background-color:#1976d2!important}.q-date__range[data-v-6eb8828c]{background-color:rgba(25,118,210,0.1)}.custom-title[data-v-6eb8828c]{font-size:0.9rem;font-weight:500}.custom-timeline[data-v-6eb8828c]{--q-timeline-color:#e0e0e0}.custom-node .q-timeline__dot[data-v-6eb8828c]{background:#485573!important;border:2px solid #5c6b8c!important}.custom-node .q-timeline__content[data-v-6eb8828c]{color:#485573}

파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/index.html


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
templates/dist/spa/js/13.ac106c63.js


BIN
templates/dist/spa/js/13.ac106c63.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
templates/dist/spa/js/13.eb7c38b1.js


BIN
templates/dist/spa/js/13.eb7c38b1.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
templates/dist/spa/js/33.212de067.js


BIN
templates/dist/spa/js/33.212de067.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
templates/dist/spa/js/34.c66dd14f.js


BIN
templates/dist/spa/js/34.c66dd14f.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/33.e044124a.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
templates/dist/spa/js/35.08b4d310.js


BIN
templates/dist/spa/js/35.08b4d310.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 0 - 1
templates/dist/spa/js/35.191d823c.js


BIN
templates/dist/spa/js/35.191d823c.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 0
templates/dist/spa/js/36.0ef8719a.js


BIN
templates/dist/spa/js/36.0ef8719a.js.gz


BIN
templates/dist/spa/js/37.54c90e2b.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/36.04984167.js


BIN
templates/dist/spa/js/36.04984167.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/37.54c90e2b.js


BIN
templates/dist/spa/js/38.51ed54fd.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/38.96100c54.js


BIN
templates/dist/spa/js/38.96100c54.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/39.5872b98c.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/40.d194e156.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/41.eed3ba0d.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/42.135aa88c.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/43.ac48efb6.js


BIN
templates/dist/spa/js/44.65975fc6.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/44.65975fc6.js


BIN
templates/dist/spa/js/45.85676583.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/45.8feb3608.js


BIN
templates/dist/spa/js/47.8ee118cc.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/46.b3afcac9.js


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/47.8ee118cc.js


BIN
templates/dist/spa/js/48.f5f2c346.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/48.72e06ecf.js


BIN
templates/dist/spa/js/49.d3deb20b.js.gz


BIN
templates/dist/spa/js/50.89d8c121.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/49.d3deb20b.js


BIN
templates/dist/spa/js/50.b2e076a3.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/50.89d8c121.js


BIN
templates/dist/spa/js/51.f4758ae8.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/51.67372365.js


BIN
templates/dist/spa/js/51.67372365.js.gz


BIN
templates/dist/spa/js/52.c0c678a9.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/52.c0c678a9.js


BIN
templates/dist/spa/js/53.4bd72b33.js.gz


BIN
templates/dist/spa/js/53.d09f7623.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/53.d09f7623.js


BIN
templates/dist/spa/js/54.13366a89.js.gz


BIN
templates/dist/spa/js/54.f7392283.js.gz


BIN
templates/dist/spa/js/55.7ff76385.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/54.f7392283.js


BIN
templates/dist/spa/js/55.f93ef6f0.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/55.7ff76385.js


BIN
templates/dist/spa/js/56.8954c039.js.gz


BIN
templates/dist/spa/js/56.c76fed61.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/56.c76fed61.js


BIN
templates/dist/spa/js/57.21c754a8.js.gz


BIN
templates/dist/spa/js/57.79b8851a.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/57.79b8851a.js


BIN
templates/dist/spa/js/58.b42c1c9b.js.gz


BIN
templates/dist/spa/js/58.d42ca317.js.gz


파일 크기가 너무 크기때문에 변경 상태를 표시하지 않습니다.
+ 1 - 1
templates/dist/spa/js/58.d42ca317.js


BIN
templates/dist/spa/js/59.9471f3f5.js.gz


BIN
templates/dist/spa/js/59.bf3b4865.js.gz


+ 0 - 0
templates/dist/spa/js/59.bf3b4865.js


이 변경점에서 너무 많은 파일들이 변경되어 몇몇 파일들은 표시되지 않았습니다.