Bläddra i källkod

git cancel task

flowerstonezl 2 månader sedan
förälder
incheckning
7e2d47cca1

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 274 - 0
.cursor/debug.log


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 879 - 6
backup/postgresql.py


+ 1 - 0
backup/query

@@ -0,0 +1 @@
+postgresql-x64-16

+ 55 - 0
backup/restore_backup.py

@@ -0,0 +1,55 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+"""
+快速恢复PostgreSQL备份脚本
+"""
+import sys
+from pathlib import Path
+
+# 添加当前目录到路径
+sys.path.insert(0, str(Path(__file__).parent))
+
+from postgresql import restore_from_backup
+
+def main():
+    # 备份路径
+    backup_path = r"D:\code\vue\greater_wms\backup\base_backup\20260103_144522"
+    
+    # 检查是否有命令行参数(非交互模式)
+    auto_confirm = len(sys.argv) > 1 and sys.argv[1] == '--yes'
+    
+    print("=" * 60)
+    print("PostgreSQL 数据库恢复工具")
+    print("=" * 60)
+    print(f"\n备份路径: {backup_path}")
+    print("\n警告:此操作将覆盖现有数据库数据!")
+    print("当前数据目录将被备份到新位置。")
+    
+    if not auto_confirm:
+        try:
+            confirm = input("\n确认要继续恢复吗?(yes/no): ").strip().lower()
+            if confirm != 'yes':
+                print("操作已取消")
+                return
+        except EOFError:
+            print("\n非交互模式,使用 --yes 参数自动确认")
+            print("操作已取消")
+            return
+    
+    print("\n开始恢复...")
+    print("-" * 60)
+    
+    success = restore_from_backup(backup_path)
+    
+    print("-" * 60)
+    if success:
+        print("\n[成功] 数据库恢复成功!")
+        print("PostgreSQL 服务已启动,可以开始使用数据库了。")
+    else:
+        print("\n[失败] 数据库恢复失败!")
+        print("请查看日志文件 postgres_service_manager.log 获取详细信息。")
+        sys.exit(1)
+
+if __name__ == "__main__":
+    main()
+

+ 1 - 0
container/filter.py

@@ -54,6 +54,7 @@ class ContainerListFilter(FilterSet):
             "id": ['exact', 'gt', 'gte', 'lt', 'lte', 'isnull', 'in', 'range'],
             "container_code": ['exact', 'icontains'],
             "current_location": ['exact', 'icontains'],
+            "target_location": ['exact', 'icontains'],
             "status": ['exact', 'icontains'],
             "last_operation": ['exact', 'icontains'],
             "category": ['exact', 'icontains'],

+ 12 - 3
container/models.py

@@ -425,6 +425,11 @@ def aggregate_to_batch_log(container_log):
 
         
         # 将托盘日志关联到批次日志
+        # 先检查记录是否仍然存在(防止在处理过程中被删除)
+        if not ContainerDetailLogModel.objects.filter(id=container_log.id).exists():
+            logger.warning(f"托盘日志 {container_log.id} 已不存在,跳过关联(可能已被级联删除)")
+            return
+        
         batch_log.detail_logs.add(container_log)
         
         # 从关联日志更新批次日志数据
@@ -452,10 +457,14 @@ def aggregate_to_batch_log(container_log):
     
         
         # 标记日志已处理
-        container_log.tobatchlog = True
-        container_log.save()
+        # 再次检查记录是否仍然存在(防止在处理过程中被删除)
+        if ContainerDetailLogModel.objects.filter(id=container_log.id).exists():
+            container_log.tobatchlog = True
+            container_log.save()
+            logger.info(f"成功聚合托盘日志 {container_log.id} 到批次日志 {batch_log.id}")
+        else:
+            logger.warning(f"托盘日志 {container_log.id} 在标记处理状态时已不存在,跳过保存")
         
-        logger.info(f"成功聚合托盘日志 {container_log.id} 到批次日志 {batch_log.id}")
         return batch_log
     
     except Exception as e:

+ 1 - 0
container/urls.py

@@ -53,6 +53,7 @@ re_path(r'^task/(?P<pk>\d+)/$', views.TaskViewSet.as_view({
 
 path(r'location_release/',views.ContainerWCSViewSet.as_view({"post": "release_location"}), name='ContainerWCS'),
 path(r'container_wcs/', views.ContainerWCSViewSet.as_view({"get": "get_container_wcs","put": "update_container_wcs","post": "generate_move_task"}), name='ContainerWCS'),
+path(r'cancel_task/', views.ContainerWCSViewSet.as_view({"post": "cancel_task"}), name='CancelTask'),
 path(r'issue_outbound/', views.ContainerWCSViewSet.as_view({"post": "generate_out_task"}), name='ContainerWCS1'),
 re_path(r'container_wcs/update/', views.ContainerWCSViewSet.as_view({"get": "update_container_wcs"}), name='ContainerWCS1'),
 

+ 473 - 13
container/views.py

@@ -728,12 +728,12 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                 }
             else:
                 # 生成任务
+                # 查询移库任务,排除已完成和已取消的任务
                 current_task = ContainerWCSModel.objects.filter(
                     container=container, 
                     tasktype='inbound',
-                    working = 1,
-                 
-                ).exclude(status=300).first()
+                    working=1
+                ).exclude(status=300).exclude(status=400).first()  # 排除已完成和已取消的任务
 
                 if current_task:
                     data_return = {
@@ -856,12 +856,12 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                 }
             else:
                 # 生成任务
+                # 查询出库任务,排除已完成和已取消的任务
                 current_task = ContainerWCSModel.objects.filter(
                     container=container, 
                     tasktype='outbound',
-                    working = 1,
-                 
-                ).exclude(status=300).first()
+                    working=1
+                ).exclude(status=300).exclude(status=400).first()  # 排除已完成和已取消的任务
 
                 if current_task:
                     data_return = {
@@ -991,12 +991,12 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
                     'data': data
                 }
             else:
+                # 查询入库任务,排除已完成和已取消的任务
                 current_task = ContainerWCSModel.objects.filter(
                     container=container, 
                     tasktype='inbound',
-                    working = 1,
-                 
-                ).exclude(status=300).first()
+                    working=1
+                ).exclude(status=300).exclude(status=400).first()  # 排除已完成和已取消的任务
 
                 if current_task:
                     
@@ -1378,6 +1378,446 @@ class ContainerWCSViewSet(viewsets.ModelViewSet):
             return Response({'code': '500', 'message': '服务器内部错误', 'data': None},
                         status=status.HTTP_500_INTERNAL_SERVER_ERROR)
 
+    def cancel_task(self, request, *args, **kwargs):
+        """取消任务并回滚相关数据 - 支持所有任务类型(入库、移库、出库、检查)"""
+        data = self.request.data
+        task_number = data.get('task_number')
+        container_code = data.get('container_code')
+        task_type = data.get('task_type', 'inbound')
+        
+        if not task_number:
+            return Response({
+                'code': '400',
+                'message': '缺少任务号参数',
+                'success': False
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        if not container_code:
+            return Response({
+                'code': '400',
+                'message': '缺少托盘编码参数',
+                'success': False
+            }, status=status.HTTP_400_BAD_REQUEST)
+        
+        try:
+            # 转换任务号格式(前端传入的是减去20000000000后的值)
+            full_task_number = task_number + 20000000000
+            
+            # 查找任务
+            task = ContainerWCSModel.objects.filter(
+                tasknumber=full_task_number,
+                container=container_code,
+                tasktype=task_type,
+                working=1
+            ).first()
+            
+            if not task:
+                return Response({
+                    'code': '404',
+                    'message': '任务不存在或已完成',
+                    'success': False
+                }, status=status.HTTP_404_NOT_FOUND)
+            
+            # 获取托盘对象
+            container_obj = ContainerListModel.objects.filter(
+                container_code=container_code
+            ).first()
+            
+            if not container_obj:
+                return Response({
+                    'code': '404',
+                    'message': '托盘不存在',
+                    'success': False
+                }, status=status.HTTP_404_NOT_FOUND)
+            
+            # 使用事务确保原子性
+            rollback_details = {}  # 收集回滚详情
+            with transaction.atomic():
+                # 1. 更新任务状态为取消
+                task.status = 400  # 使用400表示已取消
+                task.message = '任务已取消'
+                task.working = 0
+                task.save()
+                
+                allocator = LocationAllocation()
+                
+                # 2. 根据任务类型执行不同的回滚逻辑
+                if task_type == 'inbound':
+                    # 入库任务:回滚库位绑定
+                    rollback_details = self._rollback_inbound_task(task, container_obj, allocator)
+                    
+                elif task_type == 'move':
+                    # 移库任务:回滚库位状态(如果已更新)
+                    rollback_details = self._rollback_move_task(task, container_obj, allocator)
+                    
+                elif task_type == 'outbound':
+                    # 出库任务:回滚出库数量和出库明细
+                    rollback_details = self._rollback_outbound_task(task, container_obj, allocator)
+                    
+                elif task_type == 'check':
+                    # 检查任务:回滚库位状态(如果已更新)
+                    rollback_details = self._rollback_check_task(task, container_obj, allocator)
+                
+                # 3. 更新托盘目标位置为当前位置
+                old_target_location = container_obj.target_location
+                container_obj.target_location = container_obj.current_location
+                container_obj.save()
+                
+                # 记录托盘位置变化
+                if old_target_location != container_obj.target_location:
+                    if 'container_changes' not in rollback_details:
+                        rollback_details['container_changes'] = []
+                    rollback_details['container_changes'].append({
+                        'field': 'target_location',
+                        'old_value': old_target_location,
+                        'new_value': container_obj.target_location,
+                        'description': f'托盘目标位置已更新为当前位置'
+                    })
+                
+                # 4. 记录取消任务日志
+                try:
+                    log_success_operation(
+                        request=request,
+                        operation_content=f"取消任务,任务号:{task_number},托盘:{container_code},任务类型:{task_type}",
+                        operation_level="update",
+                        operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                        object_id=task.id,
+                        module_name="WCS任务管理"
+                    )
+                except Exception as log_error:
+                    logger.error(f"记录取消任务日志失败: {str(log_error)}")
+            
+            return Response({
+                'code': '200',
+                'message': '任务已取消,相关数据已回滚',
+                'success': True,
+                'data': {
+                    'task_number': task_number,
+                    'container_code': container_code,
+                    'task_type': task_type,
+                    'rollback_details': rollback_details
+                }
+            }, status=status.HTTP_200_OK)
+            
+        except Exception as e:
+            logger.error(f"取消任务失败: {str(e)}", exc_info=True)
+            try:
+                log_failure_operation(
+                    request=request,
+                    operation_content=f"取消任务失败:任务号 {task_number},托盘 {container_code},错误:{str(e)}",
+                    operation_level="update",
+                    operator=request.auth.name if hasattr(request, 'auth') and request.auth else None,
+                    module_name="WCS任务管理"
+                )
+            except Exception as log_error:
+                logger.error(f"记录取消任务失败日志失败: {str(log_error)}")
+            
+            return Response({
+                'code': '500',
+                'message': f'取消任务失败: {str(e)}',
+                'success': False
+            }, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
+    
+    def _rollback_inbound_task(self, task, container_obj, allocator):
+        """回滚入库任务相关数据,返回详细的回滚信息"""
+        rollback_details = {
+            'location_changes': [],
+            'link_changes': []
+        }
+        
+        if not task.target_location:
+            return rollback_details
+        
+        try:
+            # 获取目标库位编码
+            location_code = self.get_location_code(task.target_location)
+            
+            # 解除库位-托盘关联
+            link = LocationContainerLink.objects.filter(
+                location__location_code=location_code,
+                container=container_obj,
+                is_active=True
+            ).first()
+            
+            if link:
+                link.is_active = False
+                link.save()
+                rollback_details['link_changes'].append({
+                    'location_code': location_code,
+                    'action': '解除关联',
+                    'description': f'已解除库位 {location_code} 与托盘 {container_obj.container_code} 的绑定关系'
+                })
+            
+            # 更新库位状态为可用
+            location = LocationModel.objects.filter(
+                location_code=location_code
+            ).first()
+            if location and location.status in ['occupied', 'reserved']:
+                old_status = location.status
+                location.status = 'available'
+                location.save()
+                # 更新库位组状态
+                allocator.update_location_group_status(location_code)
+                
+                rollback_details['location_changes'].append({
+                    'location_code': location_code,
+                    'old_status': old_status,
+                    'new_status': 'available',
+                    'description': f'库位 {location_code} 状态已从 {old_status} 恢复为 available'
+                })
+            
+            logger.info(f"入库任务 {task.taskid} 取消成功,已回滚库位绑定关系")
+            return rollback_details
+            
+        except Exception as rollback_error:
+            logger.error(f"回滚入库任务失败: {str(rollback_error)}")
+            raise
+    
+    def _rollback_move_task(self, task, container_obj, allocator):
+        """回滚移库任务相关数据,返回详细的回滚信息"""
+        rollback_details = {
+            'location_changes': [],
+            'link_changes': []
+        }
+        
+        try:
+            # 移库任务如果已经更新了目标库位,需要回滚
+            if task.target_location and task.target_location not in ['103', '203']:
+                try:
+                    location_code = self.get_location_code(task.target_location)
+                    location = LocationModel.objects.filter(
+                        location_code=location_code
+                    ).first()
+                    
+                    if location and location.status in ['occupied', 'reserved']:
+                        old_status = location.status
+                        # 检查是否有活跃的关联
+                        active_link = LocationContainerLink.objects.filter(
+                            location=location,
+                            container=container_obj,
+                            is_active=True
+                        ).first()
+                        
+                        if active_link:
+                            active_link.is_active = False
+                            active_link.save()
+                            rollback_details['link_changes'].append({
+                                'location_code': location_code,
+                                'action': '解除关联',
+                                'description': f'已解除目标库位 {location_code} 与托盘 {container_obj.container_code} 的绑定关系'
+                            })
+                        
+                        location.status = 'available'
+                        location.save()
+                        allocator.update_location_group_status(location_code)
+                        
+                        rollback_details['location_changes'].append({
+                            'location_code': location_code,
+                            'old_status': old_status,
+                            'new_status': 'available',
+                            'description': f'目标库位 {location_code} 状态已从 {old_status} 恢复为 available'
+                        })
+                        
+                        logger.info(f"移库任务 {task.taskid} 取消成功,已回滚目标库位状态")
+                except Exception as e:
+                    logger.warning(f"回滚移库任务目标库位失败: {str(e)}")
+            
+            # 回滚起始库位状态(如果已更新为reserved)
+            if task.current_location and task.current_location not in ['103', '203']:
+                try:
+                    current_location_code = self.get_location_code(task.current_location)
+                    current_location = LocationModel.objects.filter(
+                        location_code=current_location_code
+                    ).first()
+                    
+                    if current_location and current_location.status == 'reserved':
+                        old_status = current_location.status
+                        current_location.status = 'available'
+                        current_location.save()
+                        allocator.update_location_group_status(current_location_code)
+                        
+                        rollback_details['location_changes'].append({
+                            'location_code': current_location_code,
+                            'old_status': old_status,
+                            'new_status': 'available',
+                            'description': f'起始库位 {current_location_code} 状态已从 {old_status} 恢复为 available'
+                        })
+                except Exception as e:
+                    logger.warning(f"回滚移库任务起始库位失败: {str(e)}")
+                    
+        except Exception as rollback_error:
+            logger.error(f"回滚移库任务失败: {str(rollback_error)}")
+            raise
+        
+        return rollback_details
+    
+    def _rollback_outbound_task(self, task, container_obj, allocator):
+        """回滚出库任务相关数据 - 包括出库数量和出库明细,返回详细的回滚信息"""
+        rollback_details = {
+            'batch_changes': [],
+            'out_detail_changes': [],
+            'container_status_changes': [],
+            'total_rollback_qty': 0
+        }
+        
+        try:
+            from decimal import Decimal
+            
+            # 1. 回滚出库明细(out_batch_detail)
+            out_details = out_batch_detail.objects.filter(
+                container=container_obj,
+                working=1,
+                is_delete=False
+            )
+            
+            total_rollback_qty = Decimal('0')
+            
+            for out_detail in out_details:
+                # 回滚托盘明细的出库数量
+                container_detail = out_detail.container_detail
+                if container_detail:
+                    # 回滚到上次的出库数量
+                    rollback_qty = out_detail.out_goods_qty
+                    old_out_qty = container_detail.goods_out_qty
+                    container_detail.goods_out_qty = out_detail.last_out_goods_qty
+                    container_detail.save()
+                    
+                    total_rollback_qty += rollback_qty
+                    
+                    batch_id = container_detail.batch.id if container_detail.batch else '未知'
+                    batch_number = container_detail.batch.bound_number if container_detail.batch else '未知'
+                    
+                    rollback_details['batch_changes'].append({
+                        'batch_id': batch_id,
+                        'batch_number': batch_number,
+                        'rollback_qty': float(rollback_qty),
+                        'old_out_qty': float(old_out_qty),
+                        'new_out_qty': float(container_detail.goods_out_qty),
+                        'description': f'批次 {batch_number} 出库数量已从 {old_out_qty} 回滚到 {container_detail.goods_out_qty},回滚数量: {rollback_qty}'
+                    })
+                    
+                    # 创建批次操作日志
+                    try:
+                        BatchOperateLogModel.objects.create(
+                            batch_id=container_detail.batch,
+                            log_type=1,  # 出库日志类型
+                            log_date=timezone.now(),
+                            goods_code=container_detail.batch.goods_code if container_detail.batch else '',
+                            goods_desc=container_detail.batch.goods_desc if container_detail.batch else '',
+                            goods_qty=-rollback_qty,  # 负数表示回滚
+                            log_content=f"取消出库任务回滚:托盘 {container_obj.container_code} 批次 {batch_id} 回滚数量 {rollback_qty}",
+                            creater="WMS",
+                            openid="WMS"
+                        )
+                    except Exception as log_error:
+                        logger.warning(f"创建批次操作日志失败: {str(log_error)}")
+                
+                # 标记出库明细为已取消
+                out_detail.working = 0
+                out_detail.is_delete = True
+                out_detail.save()
+                
+                rollback_details['out_detail_changes'].append({
+                    'out_detail_id': out_detail.id,
+                    'action': '已取消',
+                    'description': f'出库明细 ID {out_detail.id} 已标记为取消'
+                })
+            
+            rollback_details['total_rollback_qty'] = float(total_rollback_qty)
+            
+            # 2. 回滚库位状态(如果已更新)
+            if task.current_location and task.current_location not in ['103', '203']:
+                try:
+                    location_code = self.get_location_code(task.current_location)
+                    location = LocationModel.objects.filter(
+                        location_code=location_code
+                    ).first()
+                    
+                    if location:
+                        # 检查是否有活跃的关联
+                        active_link = LocationContainerLink.objects.filter(
+                            location=location,
+                            container=container_obj,
+                            is_active=True
+                        ).first()
+                        
+                        if not active_link:
+                            # 如果没有关联,说明可能已经解除了,需要重新绑定
+                            # 或者库位状态需要恢复
+                            if location.status == 'available':
+                                # 如果库位是可用状态,可能需要恢复为占用(如果托盘还在库位)
+                                pass
+                except Exception as e:
+                    logger.warning(f"回滚出库任务库位状态失败: {str(e)}")
+            
+            # 3. 恢复托盘状态(如果已更新为已出库)
+            if container_obj.status == 3:  # 3表示已出库
+                # 根据业务逻辑决定是否恢复状态
+                # 如果托盘还有在库的明细,应该恢复为在库状态
+                has_in_stock_details = ContainerDetailModel.objects.filter(
+                    container=container_obj,
+                    status=2,  # 2表示在库
+                    is_delete=False
+                ).exists()
+                
+                if has_in_stock_details:
+                    old_status = container_obj.status
+                    container_obj.status = 2  # 2表示在库
+                    container_obj.save()
+                    
+                    rollback_details['container_status_changes'].append({
+                        'field': 'status',
+                        'old_value': old_status,
+                        'new_value': 2,
+                        'description': f'托盘状态已从 已出库({old_status}) 恢复为 在库(2)'
+                    })
+            
+            logger.info(f"出库任务 {task.taskid} 取消成功,已回滚出库数量: {total_rollback_qty}")
+            
+        except Exception as rollback_error:
+            logger.error(f"回滚出库任务失败: {str(rollback_error)}")
+            raise
+        
+        return rollback_details
+    
+    def _rollback_check_task(self, task, container_obj, allocator):
+        """回滚检查任务相关数据,返回详细的回滚信息"""
+        rollback_details = {
+            'location_changes': []
+        }
+        
+        try:
+            # 检查任务通常不涉及库位状态变更,但如果有,需要回滚
+            if task.target_location and task.target_location not in ['103', '203']:
+                try:
+                    location_code = self.get_location_code(task.target_location)
+                    location = LocationModel.objects.filter(
+                        location_code=location_code
+                    ).first()
+                    
+                    if location and location.status == 'reserved':
+                        old_status = location.status
+                        location.status = 'available'
+                        location.save()
+                        allocator.update_location_group_status(location_code)
+                        
+                        rollback_details['location_changes'].append({
+                            'location_code': location_code,
+                            'old_status': old_status,
+                            'new_status': 'available',
+                            'description': f'库位 {location_code} 状态已从 {old_status} 恢复为 available'
+                        })
+                except Exception as e:
+                    logger.warning(f"回滚检查任务库位状态失败: {str(e)}")
+            
+            logger.info(f"检查任务 {task.taskid} 取消成功")
+            
+        except Exception as rollback_error:
+            logger.error(f"回滚检查任务失败: {str(rollback_error)}")
+            raise
+        
+        return rollback_details
+
     # ---------- 辅助函数 ----------
     def validate_container(self, data):
         """验证托盘是否存在"""
@@ -2462,12 +2902,14 @@ class OutboundService:
     def create_initial_tasks(container_list,bound_list_id):
         """生成初始任务队列,返回楼层信息用于初始化发送"""
         with transaction.atomic():
+            # 查询出库任务,排除已取消的任务(status=400表示已取消)
+            # status__lt=300 已经排除了 status=400,但为了明确性,仍然添加 exclude
             current_WCS = ContainerWCSModel.objects.filter(
                 tasktype='outbound',
                 bound_list_id=bound_list_id,
                 is_delete=False,
                 status__lt=300
-            ).first()
+            ).exclude(status=400).first()  # 明确排除已取消的任务
             if current_WCS:
                 logger.error(f"当前{bound_list_id}已有出库任务")
                 return {
@@ -2563,7 +3005,13 @@ class OutboundService:
     def create_initial_check_tasks(container_list,batch_id):
         """生成初始任务队列"""
         with transaction.atomic():
-            current_WCS = ContainerWCSModel.objects.filter(tasktype='check',batch_id = batch_id,is_delete=False,working=1).first()
+            # 查询检查任务,排除已取消的任务(status=400表示已取消)
+            current_WCS = ContainerWCSModel.objects.filter(
+                tasktype='check',
+                batch_id=batch_id,
+                is_delete=False,
+                working=1
+            ).exclude(status=400).first()  # 排除已取消的任务
             if current_WCS:
                 logger.error(f"当前{batch_id}已有检查任务")
                 return False
@@ -2943,7 +3391,13 @@ class OutTaskViewSet(ViewSet):
             except Exception as log_error:
                 pass
             
-            current_WCS = ContainerWCSModel.objects.filter(tasktype='outbound',bound_list_id = bound_list_id,is_delete=False).first()
+            # 查询出库任务,排除已取消的任务(status=400表示已取消)
+            current_WCS = ContainerWCSModel.objects.filter(
+                tasktype='outbound',
+                bound_list_id=bound_list_id,
+                is_delete=False
+            ).exclude(status=400).first()  # 排除已取消的任务
+            
             if current_WCS:
                 logger.info(f"当前{bound_list_id}已有出库任务{current_WCS.taskid}")
 
@@ -3113,7 +3567,13 @@ class OutTaskViewSet(ViewSet):
                 except Exception as log_error:
                     pass
                 return Response({"code": "400", "msg": "缺少抽检数目或批次号"}, status=200)
-            current_WCS = ContainerWCSModel.objects.filter(batch=batch_id,tasktype='check',is_delete=False,working=1).first()
+            # 查询检查任务,排除已取消的任务(status=400表示已取消)
+            current_WCS = ContainerWCSModel.objects.filter(
+                batch=batch_id,
+                tasktype='check',
+                is_delete=False,
+                working=1
+            ).exclude(status=400).first()  # 排除已取消的任务
             if current_WCS:
                 logger.info(f"当前{batch_id}已有出库抽检任务{current_WCS.taskid}")
                 if current_WCS.working == 1:

+ 142 - 5
location_statistics/views.py

@@ -270,8 +270,9 @@ class LocationConsistencyChecker:
         return self.results
     
     def check_location_link_consistency(self):
-        """检测库位状态与Link记录的一致性"""
+        """检测库位状态与Link记录的一致性,以及托盘当前位置与库位状态的一致性"""
         from bin.models import LocationModel, LocationContainerLink
+        from container.models import ContainerListModel
         
         # 构建查询条件
         filters = {'is_active': True, 'location_type__in': self.TARGET_LOCATION_TYPES}
@@ -281,16 +282,19 @@ class LocationConsistencyChecker:
             filters['layer'] = self.layer
         
         # 获取库位并预取Link记录
-        locations = LocationModel.objects.filter(**filters).prefetch_related('container_links')
+        locations = LocationModel.objects.filter(**filters).prefetch_related(
+            'container_links'
+        ).select_related()
         self.results['summary']['total_locations'] = locations.count()
         
         for location in locations:
             self.results['summary']['checked_locations'] += 1
             
-            # 获取该库位的活跃Link记录数量
-            active_links_count = location.container_links.filter(is_active=True).count()
+            # 获取该库位的活跃Link记录
+            active_links = location.container_links.filter(is_active=True).select_related('container')
+            active_links_count = active_links.count()
             
-            # 根据状态判断是否一致
+            # 1. 检查库位状态与Link记录的一致性
             is_consistent, expected_status, error_type = self._check_location_consistency(
                 location.status, active_links_count
             )
@@ -311,6 +315,122 @@ class LocationConsistencyChecker:
                     'error_type': error_type,
                     'detected_at': timezone.now()
                 })
+            
+            # 2. 检查托盘显示的当前位置与库位状态的一致性
+            # 查找所有显示当前位置为该库位的托盘
+            location_coordinate = self._get_location_coordinate(location)
+            containers_at_location = ContainerListModel.objects.filter(
+                current_location=location_coordinate,
+                available=True  # ContainerListModel 使用 available 字段而不是 is_delete
+            )
+            
+            for container in containers_at_location:
+                # 检查库位状态是否应该为占用
+                if location.status == 'available' and active_links_count == 0:
+                    # 库位是空闲的,没有link,但托盘显示当前位置是该库位 - 不一致
+                    self.results['summary']['error_locations'] += 1
+                    self.results['location_errors'].append({
+                        'location_id': location.id,
+                        'location_code': location.location_code,
+                        'warehouse_code': location.warehouse_code,
+                        'current_status': location.status,
+                        'expected_status': 'occupied',
+                        'active_links_count': active_links_count,
+                        'layer': location.layer,
+                        'row': location.row,
+                        'col': location.col,
+                        'location_group': location.location_group,
+                        'error_type': 'container_at_location_but_not_bound',
+                        'container_code': container.container_code,
+                        'container_current_location': container.current_location,
+                        'detected_at': timezone.now()
+                    })
+                
+                # 检查是否有对应的Link记录
+                container_has_link = active_links.filter(
+                    container__container_code=container.container_code
+                ).exists()
+                
+                if not container_has_link and location.status in ['occupied', 'reserved']:
+                    # 托盘显示在该库位,但Link记录中没有关联 - 不一致
+                    self.results['summary']['error_locations'] += 1
+                    self.results['location_errors'].append({
+                        'location_id': location.id,
+                        'location_code': location.location_code,
+                        'warehouse_code': location.warehouse_code,
+                        'current_status': location.status,
+                        'expected_status': location.status,
+                        'active_links_count': active_links_count,
+                        'layer': location.layer,
+                        'row': location.row,
+                        'col': location.col,
+                        'location_group': location.location_group,
+                        'error_type': 'container_at_location_without_link',
+                        'container_code': container.container_code,
+                        'container_current_location': container.current_location,
+                        'detected_at': timezone.now()
+                    })
+            
+            # 3. 检查Link记录中的托盘是否与托盘当前位置一致
+            for link in active_links:
+                container = link.container
+                if container:
+                    container_location_coordinate = self._get_location_coordinate_from_code(
+                        container.current_location
+                    )
+                    location_coordinate_str = self._format_location_coordinate(
+                        location.warehouse_code, location.row, location.col, location.layer
+                    )
+                    
+                    # 如果Link记录显示托盘在该库位,但托盘的当前位置不匹配
+                    if container.current_location and container.current_location != location_coordinate_str:
+                        # 检查是否是同一个位置(可能格式不同)
+                        if container_location_coordinate != location_coordinate_str:
+                            self.results['summary']['error_locations'] += 1
+                            self.results['location_errors'].append({
+                                'location_id': location.id,
+                                'location_code': location.location_code,
+                                'warehouse_code': location.warehouse_code,
+                                'current_status': location.status,
+                                'expected_status': location.status,
+                                'active_links_count': active_links_count,
+                                'layer': location.layer,
+                                'row': location.row,
+                                'col': location.col,
+                                'location_group': location.location_group,
+                                'error_type': 'link_container_location_mismatch',
+                                'container_code': container.container_code,
+                                'container_current_location': container.current_location,
+                                'expected_container_location': location_coordinate_str,
+                                'detected_at': timezone.now()
+                            })
+    
+    def _get_location_coordinate(self, location):
+        """获取库位的坐标字符串格式(与托盘current_location格式一致)"""
+        return self._format_location_coordinate(
+            location.warehouse_code, location.row, location.col, location.layer
+        )
+    
+    def _format_location_coordinate(self, warehouse_code, row, col, layer):
+        """格式化库位坐标为字符串"""
+        try:
+            return f"{warehouse_code}-{int(row):02d}-{int(col):02d}-{int(layer):02d}"
+        except (ValueError, TypeError):
+            return f"{warehouse_code}-{row}-{col}-{layer}"
+    
+    def _get_location_coordinate_from_code(self, location_str):
+        """从库位编码字符串提取坐标信息(用于比较)"""
+        if not location_str:
+            return None
+        try:
+            # 假设格式为 "W01-01-01-01" 或类似
+            parts = str(location_str).split('-')
+            if len(parts) >= 4:
+                # 标准化格式以便比较
+                return f"{parts[0]}-{int(parts[1]):02d}-{int(parts[2]):02d}-{int(parts[3]):02d}"
+        except (ValueError, TypeError, IndexError):
+            pass
+        return location_str
     
     def _check_location_consistency(self, current_status, active_links_count):
         """
@@ -760,6 +880,13 @@ def get_consistency_report(warehouse_code=None, layer=None, auto_fix=False):
 class BindContainerView(APIView):
     """重新绑定托盘到库位"""
     
+    def _format_location_coordinate(self, warehouse_code, row, col, layer):
+        """格式化库位坐标为字符串"""
+        try:
+            return f"{warehouse_code}-{int(row):02d}-{int(col):02d}-{int(layer):02d}"
+        except (ValueError, TypeError):
+            return f"{warehouse_code}-{row}-{col}-{layer}"
+    
     def post(self, request):
         location_code = request.data.get('location_code')
         container_code = request.data.get('container_code')
@@ -822,6 +949,11 @@ class BindContainerView(APIView):
                 is_active=True
             ).update(is_active=False)
             
+            # 格式化库位坐标为字符串格式(与托盘current_location格式一致)
+            location_coordinate = self._format_location_coordinate(
+                location.warehouse_code, location.row, location.col, location.layer
+            )
+            
             # 创建新的关联
             with transaction.atomic():
                 # 检查是否已存在关联(即使是非活跃的)
@@ -848,6 +980,11 @@ class BindContainerView(APIView):
                 location.status = 'occupied'
                 location.save()
                 
+                # 更新托盘的current_location和target_location到该库位
+                container.current_location = location_coordinate
+                container.target_location = location_coordinate
+                container.save(update_fields=['current_location', 'target_location'])
+                
                 # 更新库位组状态
                 allocator.update_location_group_status(location_code)
             

+ 183 - 0
postgres_service_manager.log

@@ -197,3 +197,186 @@
 2025-09-22 20:58:59,191 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
 2025-10-04 14:21:01,719 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
 2025-10-04 14:21:04,486 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:56:05,095 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:08,900 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:56:15,206 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2026-01-03 14:56:15,214 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:15,232 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 14:56:15,297 - INFO - PostgreSQL 进程已终止
+2026-01-03 14:56:15,298 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 14:56:15,298 - INFO - 检查 postgresql.conf 文件完整性
+2026-01-03 14:56:17,368 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:20,235 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:56:20,235 - INFO - PostgreSQL 服务修复成功
+2026-01-03 14:56:25,797 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:25,815 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 14:56:25,880 - INFO - PostgreSQL 进程已终止
+2026-01-03 14:56:28,899 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:31,960 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:56:37,670 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2026-01-03 14:56:37,677 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:37,697 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 14:56:37,758 - INFO - PostgreSQL 进程已终止
+2026-01-03 14:56:37,759 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 14:56:37,759 - INFO - 检查 postgresql.conf 文件完整性
+2026-01-03 14:56:39,833 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:42,697 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:56:42,698 - INFO - PostgreSQL 服务修复成功
+2026-01-03 14:56:51,835 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:51,854 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 14:56:51,916 - INFO - PostgreSQL 进程已终止
+2026-01-03 14:56:55,901 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:56:58,247 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:57:01,614 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:57:01,632 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 14:57:01,692 - INFO - PostgreSQL 进程已终止
+2026-01-03 14:57:04,697 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:57:07,547 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:57:13,704 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2026-01-03 14:57:13,710 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:57:13,728 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 14:57:13,787 - INFO - PostgreSQL 进程已终止
+2026-01-03 14:57:13,788 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 14:57:13,788 - INFO - 检查 postgresql.conf 文件完整性
+2026-01-03 14:57:15,852 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:57:18,739 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 14:57:18,739 - INFO - PostgreSQL 服务修复成功
+2026-01-03 14:57:21,304 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:57:21,321 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 14:57:21,382 - INFO - PostgreSQL 进程已终止
+2026-01-03 14:57:28,382 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 14:57:31,259 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 15:01:16,733 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:01:19,586 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 15:04:27,458 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:04:30,351 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 15:06:57,629 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:07:00,500 - INFO - PostgreSQL 服务 'postgresql-x64-16' 启动命令执行成功
+2026-01-03 15:07:05,508 - WARNING - PostgreSQL 服务 'postgresql-x64-16' 启动后立即停止
+2026-01-03 15:07:11,773 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2026-01-03 15:07:11,788 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:07:11,813 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 15:07:11,882 - INFO - PostgreSQL 进程已终止
+2026-01-03 15:07:11,884 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 15:07:11,884 - INFO - 检查 postgresql.conf 文件完整性
+2026-01-03 15:07:13,962 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:07:16,804 - INFO - PostgreSQL 服务 'postgresql-x64-16' 启动命令执行成功
+2026-01-03 15:07:21,827 - WARNING - PostgreSQL 服务 'postgresql-x64-16' 启动后立即停止
+2026-01-03 15:07:21,831 - ERROR - 修复后仍无法启动服务
+2026-01-03 15:08:50,522 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:08:53,433 - INFO - PostgreSQL 服务 'postgresql-x64-16' 启动命令执行成功
+2026-01-03 15:08:58,462 - WARNING - PostgreSQL 服务 'postgresql-x64-16' 启动后立即停止
+2026-01-03 15:08:58,466 - ERROR - 数据目录无效:缺少 PG_VERSION 文件。数据目录: D:\app\postgresql\data
+2026-01-03 15:09:05,149 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2026-01-03 15:09:05,162 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:09:05,185 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 15:09:05,254 - INFO - PostgreSQL 进程已终止
+2026-01-03 15:09:05,255 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 15:09:05,256 - ERROR - 数据目录无效:缺少关键文件 PG_VERSION
+2026-01-03 15:09:05,256 - ERROR - 数据目录: D:\app\postgresql\data
+2026-01-03 15:09:05,256 - ERROR - 这通常意味着数据目录被损坏或删除。
+2026-01-03 15:09:05,256 - ERROR - 解决方案:
+2026-01-03 15:09:05,256 - ERROR - 1. 如果您有数据备份,请恢复备份
+2026-01-03 15:09:05,256 - ERROR - 2. 如果没有备份,需要重新初始化数据库(会丢失所有数据)
+2026-01-03 15:09:05,256 - ERROR -    重新初始化命令: initdb -D "D:\app\postgresql\data"
+2026-01-03 15:09:05,257 - ERROR -    或者使用 PostgreSQL 安装程序重新配置数据目录
+2026-01-03 15:10:11,219 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:10:14,116 - INFO - PostgreSQL 服务 'postgresql-x64-16' 启动命令执行成功
+2026-01-03 15:10:19,134 - WARNING - PostgreSQL 服务 'postgresql-x64-16' 启动后立即停止
+2026-01-03 15:10:19,138 - ERROR - 数据目录无效:缺少 PG_VERSION 文件。数据目录: D:\app\postgresql\data
+2026-01-03 15:10:22,666 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2026-01-03 15:10:22,681 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:10:22,704 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 15:10:22,776 - INFO - PostgreSQL 进程已终止
+2026-01-03 15:10:22,778 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 15:10:22,778 - ERROR - 数据目录无效:缺少关键文件 PG_VERSION
+2026-01-03 15:10:22,778 - ERROR - 数据目录: D:\app\postgresql\data
+2026-01-03 15:10:22,778 - ERROR - 这通常意味着数据目录被损坏或删除。
+2026-01-03 15:10:22,778 - ERROR - 解决方案:
+2026-01-03 15:10:22,779 - ERROR - 1. 如果您有数据备份,请恢复备份
+2026-01-03 15:10:22,779 - ERROR - 2. 如果没有备份,需要重新初始化数据库(会丢失所有数据)
+2026-01-03 15:10:22,779 - ERROR -    重新初始化命令: initdb -D "D:\app\postgresql\data"
+2026-01-03 15:10:22,779 - ERROR -    或者使用 PostgreSQL 安装程序重新配置数据目录
+2026-01-03 15:10:47,484 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2026-01-03 15:10:47,498 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:10:47,524 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 15:10:47,587 - INFO - PostgreSQL 进程已终止
+2026-01-03 15:10:47,588 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 15:10:47,588 - ERROR - 数据目录无效:缺少关键文件 PG_VERSION
+2026-01-03 15:10:47,588 - ERROR - 数据目录: D:\app\postgresql\data
+2026-01-03 15:10:47,589 - ERROR - 这通常意味着数据目录被损坏或删除。
+2026-01-03 15:10:47,589 - ERROR - 解决方案:
+2026-01-03 15:10:47,589 - ERROR - 1. 如果您有数据备份,请恢复备份
+2026-01-03 15:10:47,589 - ERROR - 2. 如果没有备份,需要重新初始化数据库(会丢失所有数据)
+2026-01-03 15:10:47,589 - ERROR -    重新初始化命令: initdb -D "D:\app\postgresql\data"
+2026-01-03 15:10:47,589 - ERROR -    或者使用 PostgreSQL 安装程序重新配置数据目录
+2026-01-03 15:22:22,925 - INFO - 正在停止 PostgreSQL 服务...
+2026-01-03 15:22:22,934 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:22:22,956 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 15:22:23,027 - INFO - PostgreSQL 进程已终止
+2026-01-03 15:22:25,042 - INFO - 数据目录: D:\app\postgresql\data
+2026-01-03 15:22:25,043 - INFO - PostgreSQL bin 目录: D:\app\postgresql\bin
+2026-01-03 15:22:25,044 - INFO - 正在初始化数据库,这可能需要几分钟...
+2026-01-03 15:22:25,044 - INFO - 执行命令: D:\app\postgresql\bin\initdb.exe -D "D:\app\postgresql\data" -U postgres -A trust -E UTF8 --locale=C
+2026-01-03 15:22:28,177 - INFO - 数据库初始化成功!
+2026-01-03 15:22:28,180 - INFO - 验证成功:PG_VERSION 文件已创建
+2026-01-03 15:22:28,180 - INFO - PostgreSQL 版本: 16
+2026-01-03 15:22:28,181 - INFO - 正在启动 PostgreSQL 服务...
+2026-01-03 15:22:30,201 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:22:32,730 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 15:22:32,731 - INFO - PostgreSQL 服务启动成功!
+2026-01-03 15:31:01,815 - INFO - 正在停止 PostgreSQL 服务...
+2026-01-03 15:31:01,824 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:31:04,349 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2026-01-03 15:31:06,358 - INFO - 数据目录: D:\app\postgresql\data
+2026-01-03 15:31:06,358 - WARNING - 数据目录已存在且包含 PG_VERSION 文件
+2026-01-03 15:31:14,727 - INFO - 备份现有数据目录到: D:\app\postgresql\data_backup_1767425474
+2026-01-03 15:31:14,728 - INFO - 数据目录已备份
+2026-01-03 15:31:14,729 - INFO - PostgreSQL bin 目录: D:\app\postgresql\bin
+2026-01-03 15:31:14,729 - INFO - 正在初始化数据库,这可能需要几分钟...
+2026-01-03 15:31:14,729 - INFO - 执行命令: D:\app\postgresql\bin\initdb.exe -D "D:\app\postgresql\data" -U postgres -A trust -E UTF8 --locale=C
+2026-01-03 15:31:14,783 - ERROR - 数据库初始化失败
+2026-01-03 15:31:14,783 - ERROR - 返回码: 1
+2026-01-03 15:31:23,426 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:31:25,984 - INFO - PostgreSQL 服务 'postgresql-x64-16' 启动命令执行成功
+2026-01-03 15:31:30,997 - WARNING - PostgreSQL 服务 'postgresql-x64-16' 启动后立即停止
+2026-01-03 15:32:02,924 - INFO - 正在停止 PostgreSQL 服务...
+2026-01-03 15:32:02,934 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:32:02,950 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 15:32:03,017 - INFO - PostgreSQL 进程已终止
+2026-01-03 15:32:05,027 - ERROR - 初始化数据库时发生错误: 无法找到 PostgreSQL 数据目录
+2026-01-03 15:32:35,873 - INFO - 正在停止 PostgreSQL 服务...
+2026-01-03 15:32:35,879 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:32:35,895 - WARNING - 服务未正常停止,尝试终止进程
+2026-01-03 15:32:35,956 - INFO - PostgreSQL 进程已终止
+2026-01-03 15:32:37,962 - INFO - 数据目录: D:\app\postgresql\data
+2026-01-03 15:32:37,963 - INFO - PostgreSQL bin 目录: D:\app\postgresql\bin
+2026-01-03 15:32:37,963 - INFO - 正在初始化数据库,这可能需要几分钟...
+2026-01-03 15:32:37,963 - INFO - 执行命令: D:\app\postgresql\bin\initdb.exe -D "D:\app\postgresql\data" -U postgres -A trust -E UTF8 --locale=C
+2026-01-03 15:32:40,576 - INFO - 数据库初始化成功!
+2026-01-03 15:32:40,578 - INFO - 验证成功:PG_VERSION 文件已创建
+2026-01-03 15:32:40,579 - INFO - PostgreSQL 版本: 16
+2026-01-03 15:32:40,579 - INFO - 正在启动 PostgreSQL 服务...
+2026-01-03 15:32:42,599 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:32:45,128 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2026-01-03 15:32:45,128 - INFO - PostgreSQL 服务启动成功!
+2026-01-03 15:41:16,002 - INFO - 备份的 PostgreSQL 版本: 16
+2026-01-03 15:41:16,003 - INFO - 正在停止 PostgreSQL 服务...
+2026-01-03 15:41:16,011 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:41:18,540 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2026-01-03 15:41:21,548 - INFO - PostgreSQL 数据目录: D:\app\postgresql\data
+2026-01-03 15:41:21,549 - INFO - 检测到现有数据目录,正在备份...
+2026-01-03 15:41:21,551 - INFO - 当前数据目录已备份到: D:\app\postgresql\data_backup_1767426081
+2026-01-03 15:41:21,551 - INFO - 正在从备份恢复数据到: D:\app\postgresql\data
+2026-01-03 15:41:21,551 - INFO - 正在复制备份文件...
+2026-01-03 15:41:22,732 - INFO - 备份文件复制完成
+2026-01-03 15:41:22,732 - INFO - 检测到 backup_label 文件,PostgreSQL 将在启动时自动进入恢复模式
+2026-01-03 15:41:22,732 - INFO - 正在验证数据目录完整性...
+2026-01-03 15:41:22,733 - INFO - 正在启动 PostgreSQL 服务...
+2026-01-03 15:41:24,749 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:41:27,304 - INFO - PostgreSQL 服务 'postgresql-x64-16' 启动命令执行成功
+2026-01-03 15:41:32,334 - WARNING - PostgreSQL 服务 'postgresql-x64-16' 启动后立即停止
+2026-01-03 15:41:32,487 - ERROR - 服务启动失败
+2026-01-03 15:41:44,378 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2026-01-03 15:41:46,942 - INFO - PostgreSQL 服务 'postgresql-x64-16' 启动命令执行成功
+2026-01-03 15:41:51,965 - WARNING - PostgreSQL 服务 'postgresql-x64-16' 启动后立即停止

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
templates/dist/spa/css/35.4ed880df.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/css/35.86c3bf1f.css


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
templates/dist/spa/index.html


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 0 - 1
templates/dist/spa/js/35.a938d86d.js


BIN
templates/dist/spa/js/35.a938d86d.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 0
templates/dist/spa/js/35.b6961aac.js


BIN
templates/dist/spa/js/35.b6961aac.js.gz


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 1 - 1
templates/dist/spa/js/app.3082c386.js


BIN
templates/dist/spa/js/app.2a4df4a1.js.gz


BIN
templates/dist/spa/js/app.3082c386.js.gz


+ 220 - 0
templates/src/pages/task/task.vue

@@ -233,6 +233,22 @@
                     >{{ "手动确认到位" }}</q-tooltip
                   >
                 </q-btn>
+                <q-btn
+                  v-if="hasPermission('edit') && props.row.working === 1 && props.row.status !== 300"
+                  round
+                  flat
+                  push
+                  color="negative"
+                  icon="cancel"
+                  @click="cancelTask(props.row)"
+                >
+                  <q-tooltip
+                    content-class="bg-amber text-black shadow-4"
+                    :offset="[10, 10]"
+                    content-style="font-size: 12px"
+                    >{{ "取消任务" }}</q-tooltip
+                  >
+                </q-btn>
               </q-td>
             </template>
           </q-tr>
@@ -488,6 +504,125 @@
         </q-card-actions>
       </q-card>
     </q-dialog>
+    
+    <!-- 回滚详情对话框 -->
+    <q-dialog v-model="rollbackDetailsDialog" @hide="rollbackDetailsDialog = false">
+      <q-card class="shadow-24" style="min-width: 700px; max-width: 900px">
+        <q-bar class="bg-green-10 text-white">
+          <div>任务取消成功 - 数据回滚详情</div>
+          <q-space />
+          <q-btn dense flat icon="close" @click="rollbackDetailsDialog = false" v-close-popup>
+            <q-tooltip>关闭</q-tooltip>
+          </q-btn>
+        </q-bar>
+        <q-card-section v-if="rollbackDetailsData" style="max-height: 600px" class="scroll">
+          <div class="text-body1 q-mb-md">
+            <div class="text-weight-bold">任务信息:</div>
+            <div>任务类型:{{ rollbackDetailsData.taskType }}</div>
+            <div>任务号:#{{ rollbackDetailsData.taskNumber }}</div>
+            <div>托盘编码:{{ rollbackDetailsData.containerCode }}</div>
+          </div>
+          
+          <q-separator class="q-my-md" />
+          
+          <div class="text-h6 q-mb-sm">数据变化详情:</div>
+          
+          <!-- 库位变化 -->
+          <div v-if="rollbackDetailsData.details.location_changes && rollbackDetailsData.details.location_changes.length > 0" class="q-mb-md">
+            <div class="text-weight-medium text-primary q-mb-sm">库位状态变化:</div>
+            <q-list bordered separator>
+              <q-item v-for="(change, index) in rollbackDetailsData.details.location_changes" :key="index">
+                <q-item-section>
+                  <q-item-label>{{ change.description }}</q-item-label>
+                  <q-item-label caption>库位编码:{{ change.location_code }}</q-item-label>
+                </q-item-section>
+              </q-item>
+            </q-list>
+          </div>
+          
+          <!-- 关联关系变化 -->
+          <div v-if="rollbackDetailsData.details.link_changes && rollbackDetailsData.details.link_changes.length > 0" class="q-mb-md">
+            <div class="text-weight-medium text-primary q-mb-sm">库位-托盘关联变化:</div>
+            <q-list bordered separator>
+              <q-item v-for="(change, index) in rollbackDetailsData.details.link_changes" :key="index">
+                <q-item-section>
+                  <q-item-label>{{ change.description }}</q-item-label>
+                  <q-item-label caption>库位编码:{{ change.location_code }}</q-item-label>
+                </q-item-section>
+              </q-item>
+            </q-list>
+          </div>
+          
+          <!-- 批次出库数量变化 -->
+          <div v-if="rollbackDetailsData.details.batch_changes && rollbackDetailsData.details.batch_changes.length > 0" class="q-mb-md">
+            <div class="text-weight-medium text-primary q-mb-sm">批次出库数量变化:</div>
+            <q-list bordered separator>
+              <q-item v-for="(change, index) in rollbackDetailsData.details.batch_changes" :key="index">
+                <q-item-section>
+                  <q-item-label>{{ change.description }}</q-item-label>
+                  <q-item-label caption>
+                    批次号:{{ change.batch_number }} | 
+                    回滚数量:{{ change.rollback_qty }} | 
+                    原出库数量:{{ change.old_out_qty }} → 新出库数量:{{ change.new_out_qty }}
+                  </q-item-label>
+                </q-item-section>
+              </q-item>
+            </q-list>
+            <div v-if="rollbackDetailsData.details.total_rollback_qty" class="q-mt-sm text-weight-bold text-negative">
+              总回滚数量:{{ rollbackDetailsData.details.total_rollback_qty }}
+            </div>
+          </div>
+          
+          <!-- 出库明细变化 -->
+          <div v-if="rollbackDetailsData.details.out_detail_changes && rollbackDetailsData.details.out_detail_changes.length > 0" class="q-mb-md">
+            <div class="text-weight-medium text-primary q-mb-sm">出库明细变化:</div>
+            <q-list bordered separator>
+              <q-item v-for="(change, index) in rollbackDetailsData.details.out_detail_changes" :key="index">
+                <q-item-section>
+                  <q-item-label>{{ change.description }}</q-item-label>
+                </q-item-section>
+              </q-item>
+            </q-list>
+          </div>
+          
+          <!-- 托盘状态变化 -->
+          <div v-if="rollbackDetailsData.details.container_status_changes && rollbackDetailsData.details.container_status_changes.length > 0" class="q-mb-md">
+            <div class="text-weight-medium text-primary q-mb-sm">托盘状态变化:</div>
+            <q-list bordered separator>
+              <q-item v-for="(change, index) in rollbackDetailsData.details.container_status_changes" :key="index">
+                <q-item-section>
+                  <q-item-label>{{ change.description }}</q-item-label>
+                  <q-item-label caption>{{ change.field }}: {{ change.old_value }} → {{ change.new_value }}</q-item-label>
+                </q-item-section>
+              </q-item>
+            </q-list>
+          </div>
+          
+          <!-- 托盘位置变化 -->
+          <div v-if="rollbackDetailsData.details.container_changes && rollbackDetailsData.details.container_changes.length > 0" class="q-mb-md">
+            <div class="text-weight-medium text-primary q-mb-sm">托盘位置变化:</div>
+            <q-list bordered separator>
+              <q-item v-for="(change, index) in rollbackDetailsData.details.container_changes" :key="index">
+                <q-item-section>
+                  <q-item-label>{{ change.description }}</q-item-label>
+                  <q-item-label caption>{{ change.field }}: {{ change.old_value }} → {{ change.new_value }}</q-item-label>
+                </q-item-section>
+              </q-item>
+            </q-list>
+          </div>
+          
+          <!-- 如果没有变化信息 -->
+          <div v-if="!hasRollbackDetails" class="text-grey text-center q-pa-md">
+            本次取消未产生数据变化
+          </div>
+        </q-card-section>
+        <q-separator />
+        <q-card-actions align="right">
+          <q-btn flat label="关闭" color="primary" @click="rollbackDetailsDialog = false" />
+        </q-card-actions>
+      </q-card>
+    </q-dialog>
+    
     <q-dialog v-model="viewForm">
       <div
         id="printMe"
@@ -557,6 +692,7 @@ export default {
       searchUrl: '',
       pathname: 'container/wcs_task/',
       finishtaskUrl: 'container/container_wcs/',
+      canceltaskUrl: 'container/cancel_task/',
       pathname_previous: '',
       pathname_next: '',
       separator: 'cell',
@@ -569,6 +705,8 @@ export default {
       timeoutDialogGroupLabel: '',
       timeoutDialogRecommendation: '',
       timeoutDialogLoading: false,
+      rollbackDetailsDialog: false,
+      rollbackDetailsData: null,
       locationStatusDialog: false,
       locationStatusDialogData: null,
       locationStatusDialogTaskInfo: null,
@@ -720,6 +858,20 @@ export default {
     }
   },
   computed: {
+    hasRollbackDetails () {
+      if (!this.rollbackDetailsData || !this.rollbackDetailsData.details) {
+        return false
+      }
+      const details = this.rollbackDetailsData.details
+      return (
+        (details.location_changes && details.location_changes.length > 0) ||
+        (details.link_changes && details.link_changes.length > 0) ||
+        (details.batch_changes && details.batch_changes.length > 0) ||
+        (details.out_detail_changes && details.out_detail_changes.length > 0) ||
+        (details.container_status_changes && details.container_status_changes.length > 0) ||
+        (details.container_changes && details.container_changes.length > 0)
+      )
+    },
     interval () {
       return (
         this.$t('download_center.start') +
@@ -1312,6 +1464,74 @@ export default {
         this.getSearchList()
       })
     },
+    cancelTask (row) {
+      // 根据任务类型生成不同的提示信息
+      const taskTypeMap = {
+        'inbound': '入库',
+        'outbound': '出库',
+        'move': '移库',
+        'check': '检查'
+      }
+      const rollbackMap = {
+        'inbound': '回滚库位绑定关系',
+        'outbound': '回滚出库数量和出库明细',
+        'move': '回滚库位状态',
+        'check': '回滚库位状态'
+      }
+      const taskTypeName = taskTypeMap[row.tasktype] || '任务'
+      const rollbackInfo = rollbackMap[row.tasktype] || '回滚相关数据'
+      
+      // 确认对话框
+      this.$q
+        .dialog({
+          title: '确认取消任务',
+          message: `确定要取消${taskTypeName}任务 #${row.tasknumber - 20000000000} 吗?\n取消后将会${rollbackInfo}。`,
+          cancel: true,
+          persistent: true
+        })
+        .onOk(() => {
+          postauth(this.canceltaskUrl, {
+            task_number: row.tasknumber - 20000000000,
+            container_code: row.container,
+            task_type: row.tasktype
+          }).then((res) => {
+            if (res.code === '200' || res.success) {
+              // 显示成功通知
+              this.$q.notify({
+                message: `${taskTypeName}任务已取消,数据已回滚`,
+                icon: 'check',
+                color: 'positive',
+                timeout: 2000
+              })
+              
+              // 显示详细的数据变化信息
+              if (res.data && res.data.rollback_details) {
+                this.rollbackDetailsData = {
+                  taskType: taskTypeName,
+                  taskNumber: row.tasknumber - 20000000000,
+                  containerCode: row.container,
+                  details: res.data.rollback_details
+                }
+                this.rollbackDetailsDialog = true
+              }
+              
+              this.getSearchList()
+            } else {
+              this.$q.notify({
+                message: res.message || '取消任务失败',
+                icon: 'close',
+                color: 'negative'
+              })
+            }
+          }).catch((err) => {
+            this.$q.notify({
+              message: err.detail || err.message || '取消任务失败',
+              icon: 'close',
+              color: 'negative'
+            })
+          })
+        })
+    },
     getList (params = {}) {
       var _this = this
       _this.loading = true

+ 1 - 1
utils/throttle.py

@@ -10,7 +10,7 @@ class VisitThrottle(BaseThrottle):
     def allow_request(self, request, view):
         if request.path in ['/api/docs/', '/api/debug/', '/api/']:
             return (False, None)
-        elif request.path in ['/container/container_wcs/','/container/container_wcs/update/','/container/location_release/','/container/issue_outbound/',
+        elif request.path in ['/container/container_wcs/','/container/container_wcs/update/','/container/location_release/','/container/issue_outbound/', '/container/cancel_task/',
                                 '/container/batch/','/wms/createInboundApply','/wms/createOutboundApply'
                                 ,'/wms/productInfo','/wms/updateBatchInfo']:
             return True