Ver Fonte

postgresql恢复

flower_bs há 6 meses atrás
pai
commit
a19436b8d1

+ 635 - 33
backup/backup_utils.py

@@ -1,47 +1,649 @@
-# 数据库备份核心模块:backup_utils.py
 import os
-import shutil
-from datetime import datetime
-from django.conf import settings
-import sqlite3
-from pathlib import Path
+import subprocess
 import logging
+from datetime import datetime, timedelta, timezone
+from pathlib import Path
+import shutil
+import time
+import psycopg2
+from psycopg2 import sql
+
+# -------------------------
+# 配置与日志
+# -------------------------
+def setup_logger():
+    logger = logging.getLogger("pitr_recovery")
+    logger.setLevel(logging.INFO)
+    if not logger.handlers:
+        log_dir = Path("logs")
+        log_dir.mkdir(exist_ok=True)
+        fh = logging.FileHandler(log_dir / "postgres_recovery.log", encoding="utf-8")
+        ch = logging.StreamHandler()
+        fmt = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
+        fh.setFormatter(fmt)
+        ch.setFormatter(fmt)
+        logger.addHandler(fh)
+        logger.addHandler(ch)
+    return logger
+
+logger = setup_logger()
+
+# -------------------------
+# 基本路径/服务检测
+# -------------------------
+def get_postgres_data_dir():
+    """获取 PostgreSQL 数据目录"""
+    possible_paths = [
+        Path(r"D:/app/postgresql/data"),
+        Path(r"C:/Program Files/PostgreSQL/16/data"),
+        Path(r"C:/Program Files/PostgreSQL/15/data"),
+        Path(r"C:/Program Files/PostgreSQL/14/data"),
+        Path(r"C:/Program Files/PostgreSQL/13/data"),
+        Path(r"C:/Program Files/PostgreSQL/12/data"),
+    ]
+    env_path = os.environ.get('PGDATA')
+    if env_path:
+        p = Path(env_path)
+        if p.exists():
+            return p
+    for p in possible_paths:
+        if p.exists():
+            return p
+    raise FileNotFoundError("无法找到 PostgreSQL 数据目录,请设置 PGDATA 或检查常见路径。")
+
+def get_postgres_bin_path():
+    """获取 PostgreSQL bin 目录"""
+    possible_paths = [
+        Path(r"D:/app/postgresql/bin"),
+        Path(r"C:/Program Files/PostgreSQL/16/bin"),
+        Path(r"C:/Program Files/PostgreSQL/15/bin"),
+        Path(r"C:/Program Files/PostgreSQL/14/bin"),
+        Path(r"C:/Program Files/PostgreSQL/13/bin"),
+        Path(r"C:/Program Files/PostgreSQL/12/bin"),
+    ]
+    env_path = os.environ.get('PG_BIN_PATH')
+    if env_path:
+        p = Path(env_path)
+        if p.exists():
+            return p
+    for p in possible_paths:
+        if p.exists():
+            return p
+    # 尝试从 PATH 中查找
+    return None
+
+def get_postgres_service_name():
+    """检测 PostgreSQL 服务名称"""
+    candidates = [
+        "postgresql-x64-16",
+        "postgresql-x64-15",
+        "postgresql-x64-14",
+        "postgresql-x64-13",
+        "postgresql-x64-12",
+        "postgresql"
+    ]
+    for svc in candidates:
+        try:
+            result = subprocess.run(
+                ["sc", "query", svc], 
+                capture_output=True, 
+                text=True, 
+                creationflags=subprocess.CREATE_NO_WINDOW
+            )
+            if "RUNNING" in result.stdout:
+                return svc
+        except Exception:
+            continue
+    return candidates[0]
+
+def is_postgres_service_running():
+    """检查 PostgreSQL 服务是否运行"""
+    svc = get_postgres_service_name()
+    try:
+        result = subprocess.run(
+            ["sc", "query", svc], 
+            capture_output=True, 
+            text=True, 
+            creationflags=subprocess.CREATE_NO_WINDOW
+        )
+        return "RUNNING" in result.stdout
+    except Exception:
+        return False
+
+def stop_postgres_service():
+    """停止 PostgreSQL 服务"""
+    svc = get_postgres_service_name()
+    logger.info(f"停止 PostgreSQL 服务: {svc}")
+    
+    # 尝试 net stop
+    r = subprocess.run(
+        ["net", "stop", svc], 
+        capture_output=True, 
+        text=True, 
+        creationflags=subprocess.CREATE_NO_WINDOW
+    )
+    if r.returncode == 0:
+        logger.info("服务已停止")
+        return True
+    
+    # 尝试 sc stop
+    r = subprocess.run(
+        ["sc", "stop", svc], 
+        capture_output=True, 
+        text=True, 
+        creationflags=subprocess.CREATE_NO_WINDOW
+    )
+    if r.returncode == 0:
+        logger.info("服务已停止 (sc stop)")
+        return True
+    
+    # 尝试 taskkill
+    logger.warning("通过服务接口无法停止,尝试 taskkill 强制结束 postgres.exe")
+    subprocess.run(
+        ["taskkill", "/F", "/IM", "postgres.exe"], 
+        capture_output=True, 
+        text=True, 
+        creationflags=subprocess.CREATE_NO_WINDOW
+    )
+    
+    # 确认服务已停止
+    max_wait = 60  # 最大等待60秒
+    for _ in range(max_wait // 5):
+        if not is_postgres_service_running():
+            logger.info("服务已确认停止")
+            return True
+        time.sleep(5)
+    
+    logger.error("服务停止超时")
+    return False
 
-logger = logging.getLogger(__name__)
+def start_postgres_service():
+    """启动 PostgreSQL 服务"""
+    svc = get_postgres_service_name()
+    logger.info(f"启动 PostgreSQL 服务: {svc}")
+    try:
+        r = subprocess.run(
+            ["net", "start", svc], 
+            capture_output=True, 
+            text=True, 
+            creationflags=subprocess.CREATE_NO_WINDOW
+        )
+        if r.returncode == 0:
+            logger.info("服务已启动")
+            return True
+        
+        r = subprocess.run(
+            ["sc", "start", svc], 
+            capture_output=True, 
+            text=True, 
+            creationflags=subprocess.CREATE_NO_WINDOW
+        )
+        if r.returncode == 0:
+            logger.info("服务已启动 (sc start)")
+            return True
+        
+        logger.error(f"启动服务失败: {r.stderr}")
+        return False
+    except Exception as e:
+        logger.error(f"启动服务异常: {e}")
+        return False
 
-def backup_database():
-    """执行数据库备份,返回备份路径"""
+# -------------------------
+# 基础备份(pg_basebackup)
+# -------------------------
+def perform_base_backup(pg_superuser='postgres', pg_password='zhanglei'):
+    """执行基础备份"""
+    pg_bin = get_postgres_bin_path()
+    if not pg_bin:
+        raise RuntimeError("无法找到 PostgreSQL bin 目录")
+    
+    pg_basebackup = pg_bin / "pg_basebackup.exe"
+    now = datetime.now()
+    dest = Path(f"E:/code/backup/postgres/base_backup/{now.strftime('%Y%m%d_%H%M%S')}")
+    dest.mkdir(parents=True, exist_ok=True)
+    
+    cmd = [
+        str(pg_basebackup), 
+        "-D", str(dest), 
+        "-F", "p", 
+        "-X", "f", 
+        "-P", 
+        "-U", pg_superuser
+    ]
+    
+    env = os.environ.copy()
+    env['PGPASSWORD'] = pg_password
+    
+    logger.info("开始基础备份: " + " ".join(cmd))
+    r = subprocess.run(
+        cmd, 
+        env=env, 
+        capture_output=True, 
+        text=True, 
+        creationflags=subprocess.CREATE_NO_WINDOW
+    )
+    
+    if r.returncode != 0:
+        logger.error(r.stderr)
+        raise RuntimeError("pg_basebackup 失败: " + r.stderr)
+    
+    # 验证备份完整性
+    if not (dest / "backup_label").exists():
+        raise RuntimeError("基础备份不完整,缺少 backup_label 文件")
+    
+    logger.info(f"基础备份完成: {dest}")
+    return str(dest)
+
+# -------------------------
+# 检查数据库状态
+# -------------------------
+def check_database_status(dbname='postgres', user='postgres', password='zhanglei', 
+                         host='localhost', port=5432):
+    """
+    检查数据库状态
+    返回 (is_running, is_in_recovery)
+    """
     try:
-        # 源数据库路径(根据您的项目配置调整)
-        source_db = Path(settings.BASE_DIR) / 'db.sqlite3'
+        conn = psycopg2.connect(
+            dbname=dbname, 
+            user=user, 
+            password=password, 
+            host=host, 
+            port=port, 
+            connect_timeout=5
+        )
+        conn.autocommit = True
+        cur = conn.cursor()
+        
+        # 检查是否在恢复模式
+        cur.execute("SELECT pg_is_in_recovery()")
+        in_recovery = cur.fetchone()[0]
         
-        if not source_db.exists():
-            raise FileNotFoundError(f"数据库文件不存在: {source_db}")
+        cur.close()
+        conn.close()
         
-        # 生成备份目录路径
-        now = datetime.now()
-        year_month = now.strftime("%Y%m")  # 202507
-        day_time = now.strftime("%m%d_%H_%M")  # 0728_14_45
-        backup_dir = Path(f"E:/code/backup/{year_month}/{day_time}")
+        return True, in_recovery
+    except Exception:
+        return False, False
+
+# -------------------------
+# 主恢复函数:恢复到基础备份
+# -------------------------
+def restore_to_base_backup(base_backup_dir):
+    """
+    执行基础备份恢复
+    base_backup_dir: 基础备份目录路径
+    """
+    data_dir = get_postgres_data_dir()
+    logger.info(f"开始恢复到基础备份: {base_backup_dir}")
+    backup_data_dir = None
+
+    try:
+        # 1. 停止服务
+        if not stop_postgres_service():
+            raise RuntimeError("无法停止 PostgreSQL 服务")
+        time.sleep(5)  # 等待服务完全停止
+
+        # 2. 备份当前数据目录
+        backup_data_dir = data_dir.with_name(
+            data_dir.name + "_backup_" + datetime.now().strftime("%Y%m%d_%H%M%S")
+        )
+        logger.info(f"备份当前数据目录到: {backup_data_dir}")
+        if backup_data_dir.exists():
+            shutil.rmtree(backup_data_dir)
+        shutil.copytree(data_dir, backup_data_dir)
+        logger.info("备份完成")
 
+        # 3. 安全清空数据目录(保留关键配置文件)
+        logger.info("安全清空数据目录...")
+        exclude_files = {"postgresql.conf", "pg_hba.conf", "pg_ident.conf"}
+        for item in data_dir.iterdir():
+            if item.name in exclude_files:
+                logger.info(f"保留配置文件: {item.name}")
+                continue
+            try:
+                if item.is_dir():
+                    shutil.rmtree(item)
+                else:
+                    item.unlink()
+            except Exception as e:
+                logger.warning("删除 %s 失败: %s", item, e)
+        logger.info("数据目录已清空")
+
+        # 4. 恢复基础备份
+        logger.info("恢复基础备份到数据目录...")
+        shutil.copytree(base_backup_dir, data_dir, dirs_exist_ok=True)
+        logger.info("基础备份已恢复")
+
+        # 5. 删除恢复相关文件(确保不进入恢复模式)
+        for recovery_file in ["recovery.signal", "standby.signal", "recovery.conf"]:
+            file_path = data_dir / recovery_file
+            if file_path.exists():
+                logger.info(f"删除恢复文件: {recovery_file}")
+                file_path.unlink()
         
-        # 创建备份目录
-        backup_dir.mkdir(parents=True, exist_ok=True)
+        # 6. 清理postgresql.auto.conf中的恢复设置
+        auto_conf = data_dir / "postgresql.auto.conf"
+        if auto_conf.exists():
+            logger.info("清理postgresql.auto.conf中的恢复设置")
+            with open(auto_conf, "r", encoding="utf-8") as f:
+                lines = f.readlines()
+            
+            # 过滤掉恢复相关设置
+            new_lines = [
+                line for line in lines 
+                if not line.strip().startswith((
+                    "restore_command", 
+                    "recovery_target_time",
+                    "recovery_target_timeline",
+                    "recovery_target_action"
+                ))
+            ]
+            
+            with open(auto_conf, "w", encoding="utf-8") as f:
+                f.writelines(new_lines)
         
-        # 目标备份路径
-        backup_path = backup_dir / f"db.sqlite3"
+        # 7. 启动服务
+        if not start_postgres_service():
+            raise RuntimeError("无法启动 PostgreSQL 服务")
         
-        # 使用SQLite在线备份API确保备份完整性
-        con = sqlite3.connect(source_db)
-        bck = sqlite3.connect(backup_path)
-        with bck:
-            con.backup(bck, pages=1)
-        bck.close()
-        con.close()
+        # 8. 验证服务状态
+        max_retries = 10
+        for i in range(max_retries):
+            try:
+                is_running, in_recovery = check_database_status()
+                if is_running and not in_recovery:
+                    logger.info("数据库已成功启动,不在恢复模式")
+                    return True
+                time.sleep(2)
+            except Exception:
+                if i == max_retries - 1:
+                    logger.error("无法验证数据库状态")
+                time.sleep(2)
         
-        logger.info(f"数据库备份成功: {backup_path}")
-        return str(backup_path)
+        return True
+
+    except Exception as e:
+        logger.error(f"恢复到基础备份失败: {e}")
+        # 回滚:恢复原始数据目录
+        try:
+            if backup_data_dir and backup_data_dir.exists():
+                logger.info("开始回滚:恢复原始数据目录...")
+                try:
+                    stop_postgres_service()
+                except:
+                    pass
+                
+                # 清空当前数据目录
+                for item in data_dir.iterdir():
+                    try:
+                        if item.is_dir():
+                            shutil.rmtree(item)
+                        else:
+                            item.unlink()
+                    except Exception as ex:
+                        logger.warning("删除 %s 失败: %s", item, ex)
+                
+                # 复制备份回去
+                shutil.copytree(backup_data_dir, data_dir, dirs_exist_ok=True)
+                
+                # 启动服务
+                if start_postgres_service():
+                    logger.info("回滚完成并已启动服务(原始数据已恢复)")
+                else:
+                    logger.error("回滚后启动服务失败,请手动检查")
+        except Exception as roll_err:
+            logger.error(f"回滚失败: {roll_err}")
+        raise
+
+# -------------------------
+# 辅助:解析时间字符串 
+# 下面的代码用不了,直接基础备份恢复
+# -------------------------
+def parse_target_time(t):
+    """解析目标时间并转换为 UTC"""
+    if isinstance(t, datetime):
+        return t.astimezone(timezone.utc)
     
+    # 尝试多种格式
+    for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
+        try:
+            dt = datetime.strptime(t, fmt)
+            return dt.astimezone(timezone.utc)
+        except Exception:
+            continue
+    
+    raise ValueError("无法解析目标时间,请使用 'YYYY-MM-DD HH:MM:SS' 格式")
+
+# -------------------------
+# 检查恢复进度(通过数据库)
+# -------------------------
+def check_recovery_progress(dbname='postgres', user='postgres', password='zhanglei', 
+                           host='localhost', port=5432, target_dt=None):
+    """
+    检查恢复进度
+    返回 (in_recovery_bool, last_replay_timestamp_or_None, meets_target)
+    """
+    try:
+        conn = psycopg2.connect(
+            dbname=dbname, 
+            user=user, 
+            password=password, 
+            host=host, 
+            port=port, 
+            connect_timeout=5
+        )
+        conn.autocommit = True
+        cur = conn.cursor()
+        
+        # 检查是否在恢复模式
+        cur.execute("SELECT pg_is_in_recovery()")
+        in_recovery = cur.fetchone()[0]
+        
+        last_ts = None
+        try:
+            # 获取最后回放时间
+            cur.execute("SELECT pg_last_xact_replay_timestamp()")
+            res = cur.fetchone()[0]
+            if res:
+                last_ts = res.astimezone(timezone.utc)
+        except Exception as e:
+            logger.debug("查询 pg_last_xact_replay_timestamp 失败: %s", e)
+        
+        cur.close()
+        conn.close()
+        
+        # 检查是否达到目标时间
+        ok = False
+        if target_dt and last_ts:
+            # 允许30秒的时间差
+            time_diff = (target_dt - last_ts).total_seconds()
+            ok = time_diff <= 30
+        
+        return in_recovery, last_ts, ok
+    except Exception as e:
+        logger.debug("数据库连接/查询失败: %s", e)
+        return None, None, False
+
+# -------------------------
+# 主恢复函数:PITR 恢复到指定时间点
+# -------------------------
+
+def restore_to_point_in_time(target_time, base_backup_dir, 
+                            wal_archive_dir=r"E:\code\backup\postgres\wal_archive",
+                            pg_superuser='postgres', pg_password='zhanglei', 
+                            db_check_name='postgres', db_check_user='postgres', 
+                            db_check_password='zhanglei', db_host='localhost', 
+                            db_port=5432, max_wait_minutes=30):
+    """
+    执行时间点恢复 (PITR)
+    target_time: 'YYYY-MM-DD HH:MM:SS' 或 datetime
+    base_backup_dir: 基础备份目录
+    wal_archive_dir: WAL归档目录
+    """
+    target_dt = parse_target_time(target_time)
+    data_dir = get_postgres_data_dir()
+    logger.info(f"PITR 开始: 目标时间 {target_dt}, 数据目录: {data_dir}, 基础备份: {base_backup_dir}")
+    backup_data_dir = None
+
+    try:
+        # 1. 停止服务
+        if not stop_postgres_service():
+            raise RuntimeError("无法停止 PostgreSQL 服务")
+        time.sleep(5)  # 等待服务完全停止
+
+        # 2. 备份当前数据目录
+        backup_data_dir = data_dir.with_name(
+            data_dir.name + "_backup_" + datetime.now().strftime("%Y%m%d_%H%M%S")
+        )
+        logger.info(f"备份当前数据目录到: {backup_data_dir}")
+        if backup_data_dir.exists():
+            shutil.rmtree(backup_data_dir)
+        shutil.copytree(data_dir, backup_data_dir)
+        logger.info("备份完成")
+
+        # 3. 安全清空数据目录(保留关键配置文件)
+        logger.info("安全清空数据目录...")
+        exclude_files = {"postgresql.conf", "pg_hba.conf", "pg_ident.conf"}
+        for item in data_dir.iterdir():
+            if item.name in exclude_files:
+                logger.info(f"保留配置文件: {item.name}")
+                continue
+            try:
+                if item.is_dir():
+                    shutil.rmtree(item)
+                else:
+                    item.unlink()
+            except Exception as e:
+                logger.warning("删除 %s 失败: %s", item, e)
+        logger.info("数据目录已清空")
+
+        # 4. 恢复基础备份
+        logger.info("恢复基础备份到数据目录...")
+        shutil.copytree(base_backup_dir, data_dir, dirs_exist_ok=True)
+        logger.info("基础备份已恢复")
+
+        # 5. 创建恢复信号文件 (PostgreSQL 12+)
+        recovery_signal = data_dir / "recovery.signal"
+        logger.info(f"创建恢复信号: {recovery_signal}")
+        recovery_signal.touch()
+
+        # 6. 配置恢复参数
+        auto_conf = data_dir / "postgresql.auto.conf"
+        safe_wal_dir = wal_archive_dir.replace("\\", "/")
+        restore_command = f'copy "{wal_archive_dir}\\%f" "%p"'
+        
+        logger.info(f"写入恢复配置到 {auto_conf}")
+        with open(auto_conf, "w", encoding="utf-8") as f:
+            f.write(f"restore_command = '{restore_command}'\n")
+            f.write(f"recovery_target_time = '{target_dt.isoformat()}'\n")
+            f.write("recovery_target_timeline = 'latest'\n")
+        logger.info("恢复配置写入完成")
+
+        # 7. 启动服务
+        if not start_postgres_service():
+            raise RuntimeError("无法启动 PostgreSQL 服务(恢复阶段)")
+        logger.info("服务已启动,开始轮询恢复进度...")
+
+        # 8. 等待恢复完成
+        timeout = timedelta(minutes=max_wait_minutes)
+        start_time = datetime.now(timezone.utc)
+        last_logged = None
+        recovery_completed = False
+        recovery_signal_file = data_dir / "recovery.signal"
+
+        while True:
+            elapsed = datetime.now(timezone.utc) - start_time
+            if elapsed > timeout:
+                raise RuntimeError(f"恢复超时(超过 {max_wait_minutes} 分钟)")
+
+            try:
+                in_recovery, last_replay_ts, meets_target = check_recovery_progress(
+                    dbname=db_check_name,
+                    user=db_check_user,
+                    password=db_check_password,
+                    host=db_host,
+                    port=db_port,
+                    target_dt=target_dt
+                )
+            except Exception as e:
+                logger.error(f"检查恢复进度时出错: {e}")
+                in_recovery, last_replay_ts, meets_target = None, None, False
+
+            # 可能无法连接(服务刚启动)
+            if in_recovery is None:
+                logger.info("尚未能连接到数据库,等待 5 秒后重试...")
+                time.sleep(5)
+                continue
+
+            # 每分钟打印一次恢复进度
+            current_time = datetime.now(timezone.utc).strftime("%H:%M:%S")
+            if last_logged != current_time:
+                if last_replay_ts:
+                    time_diff = (target_dt - last_replay_ts).total_seconds()
+                    logger.info(f"恢复进度: {last_replay_ts} (差 {time_diff:.1f} 秒)")
+                else:
+                    logger.info(f"恢复中... pg_is_in_recovery={in_recovery}")
+                last_logged = current_time
+
+            # 检查恢复完成条件(PostgreSQL 12+)
+            if not in_recovery and not recovery_signal_file.exists():
+                logger.info("恢复完成:数据库已退出恢复模式。")
+                recovery_completed = True
+                break
+            elif meets_target:
+                logger.info("已满足目标时间条件,等待最终完成...")
+                time.sleep(5)
+                continue
+
+            # 继续等待
+            time.sleep(5)
+
+        if recovery_completed:
+            logger.info(f"PITR 成功:数据库已恢复到目标时间 {target_dt}")
+            return True
+        else:
+            logger.warning("恢复过程未正常完成")
+            return True
+
     except Exception as e:
-        logger.error(f"数据库备份失败: {str(e)}")
-        raise
+        logger.error(f"PITR 恢复失败: {e}")
+        # 回滚:恢复原始数据目录
+        try:
+            if backup_data_dir and backup_data_dir.exists():
+                logger.info("开始回滚:恢复原始数据目录...")
+                try:
+                    stop_postgres_service()
+                except:
+                    pass
+                
+                # 清空当前数据目录
+                for item in data_dir.iterdir():
+                    try:
+                        if item.is_dir():
+                            shutil.rmtree(item)
+                        else:
+                            item.unlink()
+                    except Exception as ex:
+                        logger.warning("删除 %s 失败: %s", item, ex)
+                
+                # 复制备份回去
+                shutil.copytree(backup_data_dir, data_dir, dirs_exist_ok=True)
+                
+                # 清理恢复标识文件
+                for fn in ("recovery.signal", "postgresql.auto.conf"):
+                    p = data_dir / fn
+                    if p.exists():
+                        try:
+                            p.unlink()
+                        except Exception:
+                            pass
+                
+                # 启动服务
+                if start_postgres_service():
+                    logger.info("回滚完成并已启动服务(原始数据已恢复)")
+                else:
+                    logger.error("回滚后启动服务失败,请手动检查")
+        except Exception as roll_err:
+            logger.error(f"回滚失败: {roll_err}")
+        raise

+ 333 - 0
backup/postgresql.py

@@ -0,0 +1,333 @@
+import os
+import subprocess
+import logging
+import time
+from pathlib import Path
+import shutil
+
+# 配置日志
+logging.basicConfig(
+    level=logging.INFO,
+    format='%(asctime)s - %(levelname)s - %(message)s',
+    handlers=[
+        logging.StreamHandler(),
+        logging.FileHandler('postgres_service_manager.log')
+    ]
+)
+logger = logging.getLogger(__name__)
+
+def get_postgres_service_name():
+    """获取 PostgreSQL 服务名称"""
+    service_names = [
+        "postgresql-x64-16",
+        "postgresql-x64-15",
+        "postgresql-x64-14",
+        "postgresql-x64-13",
+        "postgresql-x64-12",
+        "postgresql"
+    ]
+    
+    # 检查服务状态
+    for service in service_names:
+        try:
+            result = subprocess.run(
+                ["sc", "query", service], 
+                capture_output=True, 
+                text=True,
+                encoding='utf-8',
+                errors='ignore'
+            )
+            if "RUNNING" in result.stdout or "STOPPED" in result.stdout:
+                return service
+        except Exception:
+            continue
+    
+    return service_names[0] if service_names else "postgresql"
+
+def is_service_running(service_name=None):
+    """检查服务是否正在运行"""
+    if not service_name:
+        service_name = get_postgres_service_name()
+    
+    try:
+        result = subprocess.run(
+            ["sc", "query", service_name], 
+            capture_output=True, 
+            text=True,
+            encoding='utf-8',
+            errors='ignore'
+        )
+        return "RUNNING" in result.stdout
+    except Exception as e:
+        logger.error(f"检查服务状态失败: {str(e)}")
+        return False
+
+def stop_postgres_service():
+    """停止 PostgreSQL 服务"""
+    try:
+        service_name = get_postgres_service_name()
+        logger.info(f"尝试停止 PostgreSQL 服务: {service_name}")
+        
+        # 使用 net stop 停止服务
+        result = subprocess.run(
+            ["net", "stop", service_name], 
+            capture_output=True, 
+            text=True,
+            encoding='utf-8',
+            errors='ignore'
+        )
+        
+        if result.returncode == 0:
+            logger.info(f"PostgreSQL 服务 '{service_name}' 已停止")
+            return True
+        
+        # 如果 net stop 失败,尝试 sc stop
+        result = subprocess.run(
+            ["sc", "stop", service_name], 
+            capture_output=True, 
+            text=True,
+            encoding='utf-8',
+            errors='ignore'
+        )
+        
+        if result.returncode == 0:
+            logger.info(f"PostgreSQL 服务 '{service_name}' 已停止")
+            return True
+        
+        # 如果服务未停止,尝试强制终止进程
+        logger.warning("服务未正常停止,尝试终止进程")
+        subprocess.run(
+            ["taskkill", "/F", "/IM", "postgres.exe"], 
+            capture_output=True,
+            encoding='utf-8',
+            errors='ignore'
+        )
+        logger.info("PostgreSQL 进程已终止")
+        return True
+        
+    except Exception as e:
+        logger.error(f"停止 PostgreSQL 服务失败: {str(e)}")
+        return False
+
+def start_postgres_service():
+    """启动 PostgreSQL 服务"""
+    try:
+        service_name = get_postgres_service_name()
+        logger.info(f"尝试启动 PostgreSQL 服务: {service_name}")
+        
+        # 使用 net start 启动服务
+        result = subprocess.run(
+            ["net", "start", service_name], 
+            capture_output=True, 
+            text=True,
+            encoding='utf-8',
+            errors='ignore'
+        )
+        
+        if result.returncode == 0:
+            logger.info(f"PostgreSQL 服务 '{service_name}' 已启动")
+            return True
+        
+        # 如果 net start 失败,尝试 sc start
+        result = subprocess.run(
+            ["sc", "start", service_name], 
+            capture_output=True, 
+            text=True,
+            encoding='utf-8',
+            errors='ignore'
+        )
+        
+        if result.returncode == 0:
+            logger.info(f"PostgreSQL 服务 '{service_name}' 已启动")
+            return True
+        
+        raise RuntimeError("启动 PostgreSQL 服务失败")
+        
+    except Exception as e:
+        logger.error(f"启动 PostgreSQL 服务失败: {str(e)}")
+        return False
+
+def restart_postgres_service():
+    """重启 PostgreSQL 服务"""
+    try:
+        if stop_postgres_service():
+            time.sleep(3)  # 等待服务完全停止
+        return start_postgres_service()
+    except Exception as e:
+        logger.error(f"重启 PostgreSQL 服务失败: {str(e)}")
+        return False
+
+def get_postgres_data_dir():
+    """获取 PostgreSQL 数据目录"""
+    possible_paths = [
+        r"D:/app/postgresql/data",
+        r"C:/Program Files/PostgreSQL/15/data",
+        r"C:/Program Files/PostgreSQL/14/data",
+        r"C:/Program Files/PostgreSQL/13/data",
+        r"C:/Program Files/PostgreSQL/12/data",
+    ]
+    
+    env_path = os.environ.get('PGDATA')
+    if env_path and Path(env_path).exists():
+        return Path(env_path)
+    
+    for path in possible_paths:
+        if Path(path).exists():
+            return Path(path)
+    
+    raise FileNotFoundError("无法找到 PostgreSQL 数据目录")
+
+def fix_postgres_service():
+    """修复 PostgreSQL 服务启动问题"""
+    try:
+        logger.info("开始修复 PostgreSQL 服务启动问题...")
+        
+        # 1. 停止服务
+        if not stop_postgres_service():
+            logger.warning("无法停止服务,尝试强制修复")
+        
+        # 2. 检查数据目录
+        try:
+            data_dir = get_postgres_data_dir()
+            logger.info(f"找到 PostgreSQL 数据目录: {data_dir}")
+            
+            # 3. 检查并处理恢复配置文件
+            recovery_signal = data_dir / "recovery.signal"
+            recovery_done = data_dir / "recovery.done"
+            auto_conf = data_dir / "postgresql.auto.conf"
+            
+            if recovery_signal.exists():
+                logger.info("发现 recovery.signal 文件,尝试完成恢复过程")
+                if recovery_done.exists():
+                    recovery_done.unlink()
+                recovery_signal.rename(recovery_done)
+                logger.info("已将 recovery.signal 重命名为 recovery.done")
+            
+            # 4. 删除可能引起问题的自动配置文件
+            if auto_conf.exists():
+                backup_auto_conf = auto_conf.with_suffix(".auto.conf.bak")
+                shutil.copy(auto_conf, backup_auto_conf)
+                auto_conf.unlink()
+                logger.info("已备份并删除 postgresql.auto.conf 文件")
+            
+            # 5. 检查 postgresql.conf 文件
+            conf_file = data_dir / "postgresql.conf"
+            if conf_file.exists():
+                logger.info("检查 postgresql.conf 文件完整性")
+                # 这里可以添加更多的配置文件检查逻辑
+            
+        except FileNotFoundError as e:
+            logger.warning(f"无法找到数据目录: {str(e)}")
+        
+        # 6. 确保所有 PostgreSQL 进程已终止
+        subprocess.run(
+            ["taskkill", "/F", "/IM", "postgres.exe"], 
+            capture_output=True,
+            encoding='utf-8',
+            errors='ignore'
+        )
+        
+        # 7. 尝试启动服务
+        time.sleep(2)
+        if start_postgres_service():
+            logger.info("PostgreSQL 服务修复成功")
+            return True
+        else:
+            logger.error("修复后仍无法启动服务")
+            return False
+            
+    except Exception as e:
+        logger.error(f"修复过程中发生错误: {str(e)}")
+        return False
+
+def check_service_status():
+    """检查服务状态并显示详细信息"""
+    service_name = get_postgres_service_name()
+    is_running = is_service_running(service_name)
+    
+    print(f"服务名称: {service_name}")
+    print(f"运行状态: {'运行中' if is_running else '已停止'}")
+    
+    if is_running:
+        try:
+            # 尝试连接数据库验证服务状态
+            import psycopg2
+            try:
+                conn = psycopg2.connect(
+                    dbname='postgres',
+                    user='postgres',
+                    password='zhanglei',
+                    host='localhost',
+                    port='5432'
+                )
+                conn.close()
+                print("数据库连接: 正常")
+            except Exception as e:
+                print(f"数据库连接: 异常 ({str(e)})")
+        except ImportError:
+            print("数据库连接: 未安装 psycopg2 库,无法测试连接")
+    
+    return is_running
+
+def main():
+    """主函数:提供命令行界面"""
+    print("=" * 50)
+    print("PostgreSQL 服务管理工具")
+    print("=" * 50)
+    
+    while True:
+        print("\n请选择操作:")
+        print("1. 检查服务状态")
+        print("2. 启动服务")
+        print("3. 停止服务")
+        print("4. 重启服务")
+        print("5. 修复服务启动问题")
+        print("6. 退出")
+        
+        choice = input("请输入选项 (1-6): ").strip()
+        
+        if choice == "1":
+            print("\n检查服务状态...")
+            check_service_status()
+            
+        elif choice == "2":
+            print("\n启动服务...")
+            if start_postgres_service():
+                print("服务启动成功")
+            else:
+                print("服务启动失败,请尝试修复")
+                
+        elif choice == "3":
+            print("\n停止服务...")
+            if stop_postgres_service():
+                print("服务停止成功")
+            else:
+                print("服务停止失败")
+                
+        elif choice == "4":
+            print("\n重启服务...")
+            if restart_postgres_service():
+                print("服务重启成功")
+            else:
+                print("服务重启失败")
+                
+        elif choice == "5":
+            print("\n修复服务启动问题...")
+            if fix_postgres_service():
+                print("服务修复成功")
+            else:
+                print("服务修复失败,请查看日志文件")
+                
+        elif choice == "6":
+            print("退出程序")
+            break
+            
+        else:
+            print("无效选项,请重新选择")
+            
+        # input("\n按回车键继续...")
+
+if __name__ == "__main__":
+    # 直接运行测试
+    print("正在启动 PostgreSQL 服务管理工具...")
+    main()

+ 5 - 1
backup/urls.py

@@ -3,4 +3,8 @@ from . import views
 
 urlpatterns = [
     path('trigger/', views.trigger_backup, name='trigger_backup'),
-]
+    path('list/', views.list_backups, name='list_backups'),
+
+    path('point/', views.restore_to_point, name='restore_to_point'),
+
+]

+ 167 - 11
backup/views.py

@@ -1,11 +1,14 @@
-# 视图和调度模块:views.py
 from django.http import JsonResponse
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_POST
-from .backup_utils import backup_database
+from .backup_utils import perform_base_backup, restore_to_base_backup
+import os
+import json
+import logging
+from datetime import datetime
 from apscheduler.schedulers.background import BackgroundScheduler
 from django.conf import settings
-import logging
+import math
 
 logger = logging.getLogger(__name__)
 
@@ -15,15 +18,19 @@ scheduler = BackgroundScheduler()
 def scheduled_backup():
     """定时备份任务"""
     try:
-        backup_path = backup_database()
+        backup_path = perform_base_backup()
         logger.info(f"定时备份完成: {backup_path}")
-        from container.utils import update_container_categories_task
-        update_container_categories_task()
-        logger.info(f"定时更新托盘分类完成")
+        # 更新托盘分类任务(如果存在)
+        try:
+            from container.utils import update_container_categories_task
+            update_container_categories_task()
+            logger.info(f"定时更新托盘分类完成")
+        except ImportError:
+            logger.warning("更新托盘分类模块未找到,跳过更新")
     except Exception as e:
         logger.error(f"定时备份失败: {str(e)}")
 
-# 启动定时备份(每小时执行一次)
+# 启动定时备份(每6小时执行一次)
 if not scheduler.running:
     scheduler.add_job(
         scheduled_backup,
@@ -33,20 +40,169 @@ if not scheduler.running:
         id='db_backup_job'
     )
     scheduler.start()
+    logger.info("定时备份任务已启动")
+
+def get_backup_files(page=1, page_size=5):
+    """获取备份文件列表(带分页)"""
+    backup_dir = "E:/code/backup/postgres"
+    all_backups = []
+    
+    # 遍历备份目录
+    for root, dirs, files in os.walk(backup_dir):
+        for file in files:
+            if file.endswith(".backup"):
+                file_path = os.path.join(root, file)
+                file_size = os.path.getsize(file_path)
+                timestamp = os.path.getmtime(file_path)
+                
+                all_backups.append({
+                    "name": file,
+                    "path": file_path,
+                    "size": f"{file_size / (1024 * 1024):.2f} MB",
+                    "date": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
+                })
+    
+    # 按时间倒序排序
+    all_backups.sort(key=lambda x: x["date"], reverse=True)
+    
+    # 分页处理
+    total_items = len(all_backups)
+    total_pages = math.ceil(total_items / page_size)
+    start_index = (page - 1) * page_size
+    end_index = min(start_index + page_size, total_items)
+    
+    return {
+        "backups": all_backups[start_index:end_index],
+        "page": page,
+        "page_size": page_size,
+        "total_items": total_items,
+        "total_pages": total_pages
+    }
+
+def get_base_backups(page=1, page_size=5):
+    """获取基础备份列表(带分页)"""
+    base_backup_dir = "E:/code/backup/postgres/base_backup"
+    all_backups = []
+    
+    # 遍历基础备份目录
+    for dir_name in os.listdir(base_backup_dir):
+        dir_path = os.path.join(base_backup_dir, dir_name)
+        if os.path.isdir(dir_path):
+            timestamp = os.path.getmtime(dir_path)
+            
+            all_backups.append({
+                "name": dir_name,
+                "path": dir_path,
+                "date": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
+            })
+    
+    # 按时间倒序排序
+    all_backups.sort(key=lambda x: x["date"], reverse=True)
+    
+    # 分页处理
+    total_items = len(all_backups)
+    total_pages = math.ceil(total_items / page_size)
+    start_index = (page - 1) * page_size
+    end_index = min(start_index + page_size, total_items)
+    
+    return {
+        "backups": all_backups[start_index:end_index],
+        "page": page,
+        "page_size": page_size,
+        "total_items": total_items,
+        "total_pages": total_pages
+    }
 
 @csrf_exempt
 @require_POST
 def trigger_backup(request):
     """手动触发备份的API接口"""
     try:
-        backup_path = backup_database()
+        backup_path = perform_base_backup()
+   
+
         return JsonResponse({
             'status': 'success',
-            'message': 'Database backup completed',
+            'message': '数据库备份完成',
             'path': backup_path
         })
     except Exception as e:
         return JsonResponse({
             'status': 'error',
             'message': str(e)
-        }, status=500)
+        }, status=500)
+
+@csrf_exempt
+@require_POST
+def list_backups(request):
+    """获取备份文件列表API(带分页)"""
+    try:
+        data = json.loads(request.body)
+        backup_type = data.get('type', 'file')
+        page = data.get('page', 1)
+        page_size = data.get('page_size', 5)
+        
+        if backup_type == 'file':
+            result = get_backup_files(page, page_size)
+        elif backup_type == 'base':
+            result = get_base_backups(page, page_size)
+        else:
+            return JsonResponse({
+                'status': 'error',
+                'message': '无效的备份类型'
+            }, status=400)
+        
+        return JsonResponse({
+            'status': 'success',
+            'data': result
+        })
+    except Exception as e:
+        logger.error(f"获取备份列表失败: {str(e)}")
+        return JsonResponse({
+            'status': 'error',
+            'message': str(e)
+        }, status=500)
+
+
+@csrf_exempt
+@require_POST
+def restore_to_point(request):
+    """执行时间点恢复API"""
+    try:
+        data = json.loads(request.body)
+        base_backup = data.get('base_backup')
+       
+        
+        if not base_backup or not os.path.exists(base_backup):
+            return JsonResponse({
+                'status': 'error',
+                'message': '无效的基础备份路径'
+            }, status=400)
+        
+      
+        
+        # 暂停定时备份任务
+        scheduler.pause_job('db_backup_job')
+        logger.info("定时备份任务已暂停")
+        
+        # 执行时间点恢复
+        restore_to_base_backup( base_backup)
+        
+        # 恢复定时备份任务
+        scheduler.resume_job('db_backup_job')
+        logger.info("定时备份任务已恢复")
+        
+        return JsonResponse({
+            'status': 'success',
+            'message': f'已成功恢复到{base_backup}'
+        })
+    except Exception as e:
+        logger.error(f"时间点恢复失败: {str(e)}")
+        # 确保恢复定时备份任务
+        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')
+            logger.info("恢复失败后定时备份任务已恢复")
+        return JsonResponse({
+            'status': 'error',
+            'message': str(e)
+        }, status=500)

+ 2 - 2
bin/updates.py

@@ -17,7 +17,7 @@ class LocationUpdates:
 
     def link_container(location_code, container_code):
         try:
-            location = LocationModel.objects.select_for_update().get(
+            location = LocationModel.objects.get(
                 location_code=location_code
             )
             container = ContainerListModel.objects.get(
@@ -35,7 +35,7 @@ class LocationUpdates:
 
     def disable_link_container(location_code, container_code):
         try:
-            location = LocationModel.objects.select_for_update().get(
+            location = LocationModel.objects.get(
                 location_code=location_code
             )
             container = ContainerListModel.objects.get(

+ 2 - 2
container/views.py

@@ -441,7 +441,7 @@ class TaskRollbackMixin:
         """
         try:
             # 获取任务实例并锁定数据库记录
-            task = ContainerWCSModel.objects.select_for_update().get(taskid=task_id)
+            task = ContainerWCSModel.objects.get(taskid=task_id)
             container_code = task.container
             target_location = task.target_location
             batch = task.batch
@@ -1944,7 +1944,7 @@ class OutboundService:
     def insert_new_tasks(new_tasks):
         """动态插入新任务并重新排序"""
         with transaction.atomic():
-            pending_tasks = list(ContainerWCSModel.objects.select_for_update().filter(status=100))
+            pending_tasks = list(ContainerWCSModel.objects.filter(status=100))
             
             # 插入新任务
             for new_task_data in new_tasks:

+ 2 - 2
erp/views.py

@@ -394,7 +394,7 @@ class GenerateInbound(APIView):
     def validate_and_lock(self, bill_id):
         """验证并锁定相关资源"""
         # 锁定原始单据
-        bill_obj = InboundBill.objects.select_for_update().get(
+        bill_obj = InboundBill.objects.get(
             billId=bill_id,
             is_delete=False
         )
@@ -699,7 +699,7 @@ class GenerateOutbound(APIView):
     def validate_and_lock(self, bill_id):
         """验证并锁定相关资源"""
         # 锁定原始单据
-        bill_obj = OutboundBill.objects.select_for_update().get(
+        bill_obj = OutboundBill.objects.get(
             billId=bill_id,
             is_delete=False
         )

+ 162 - 6
migrate_sqlite_to_postgres.py

@@ -42,7 +42,9 @@ class SQLiteToPostgresMigrator:
             'django_migrations',
             'auth_group_permissions',
             'auth_user_groups',
-            'auth_user_user_permissions'
+            'auth_user_user_permissions',
+            'django_session',
+            'django_admin_log'
         ]
         
         # 迁移状态
@@ -62,7 +64,7 @@ class SQLiteToPostgresMigrator:
     def log(self, message):
         """记录日志到控制台和文件"""
         print(message)
-        if self.log_file:
+        if self.log_file and not self.log_file.closed:
             self.log_file.write(message + "\n")
             self.log_file.flush()
 
@@ -88,13 +90,20 @@ class SQLiteToPostgresMigrator:
 
     def close(self):
         """关闭数据库连接"""
+        # 先记录关闭消息
+        if self.log_file and not self.log_file.closed:
+            self.log_file.write(f"日志文件已保存: {self.log_file_path}\n")
+            self.log_file.flush()
+        
+        # 关闭数据库连接
         for cursor in [self.sqlite_cursor, self.pg_cursor]:
             if cursor: cursor.close()
         for conn in [self.sqlite_conn, self.pg_conn]:
             if conn: conn.close()
-        if self.log_file:
+        
+        # 最后关闭日志文件
+        if self.log_file and not self.log_file.closed:
             self.log_file.close()
-            self.log(f"日志文件已保存: {self.log_file_path}")
 
     def clear_postgres_data(self):
         """清空 PostgreSQL 数据库中的所有数据"""
@@ -346,7 +355,7 @@ class SQLiteToPostgresMigrator:
         self.log(f"\n准备迁移表: {table}")
         
         # 检查是否需要跳过
-        if table in self.skip_tables:
+        if table.lower() in [t.lower() for t in self.skip_tables]:
             self.log(f"  跳过系统表: {table}")
             return True
         
@@ -445,6 +454,11 @@ class SQLiteToPostgresMigrator:
                         self.log(f"  整数超出范围错误: {str(e)}")
                         # 尝试将字段类型改为bigint
                         self.handle_integer_overflow(table, columns, pg_structure, row)
+                    elif 'duplicate key value violates unique constraint' in str(e):
+                        # 处理主键冲突错误
+                        self.log(f"  主键冲突错误: {str(e)}")
+                        self.log(f"  行数据: {row}")
+                        self.log(f"  尝试跳过此行...")
                     else:
                         self.log(f"  完整性错误: {str(e)}")
                     self.pg_conn.rollback()
@@ -531,6 +545,148 @@ class SQLiteToPostgresMigrator:
         finally:
             self.close()
 
+    def mark_django_migrations_as_applied(self):
+        """标记 Django 迁移为已应用状态"""
+        try:
+            self.log("\n检查 Django 迁移状态...")
+            
+            # 重新连接数据库
+            self.sqlite_conn = sqlite3.connect(self.sqlite_db_path)
+            self.sqlite_cursor = self.sqlite_conn.cursor()
+            
+            self.pg_conn = psycopg2.connect(**self.pg_config)
+            self.pg_cursor = self.pg_conn.cursor()
+            
+            # 获取所有迁移
+            self.sqlite_cursor.execute("SELECT app, name FROM django_migrations")
+            migrations = self.sqlite_cursor.fetchall()
+            
+            if not migrations:
+                self.log("  未找到 Django 迁移记录")
+                return True
+            
+            # 在 PostgreSQL 中标记迁移为已应用
+            for app, name in migrations:
+                try:
+                    # 先检查迁移是否已存在
+                    self.pg_cursor.execute("""
+                        SELECT 1 FROM django_migrations 
+                        WHERE app = %s AND name = %s
+                    """, (app, name))
+                    exists = self.pg_cursor.fetchone()
+                    
+                    if exists:
+                        self.log(f"  迁移已存在: {app}.{name}")
+                        continue
+                    
+                    # 插入迁移记录
+                    self.pg_cursor.execute("""
+                        INSERT INTO django_migrations (app, name, applied)
+                        VALUES (%s, %s, NOW())
+                    """, (app, name))
+                    self.log(f"  标记迁移: {app}.{name}")
+                except psycopg2.IntegrityError as e:
+                    if 'unique constraint' in str(e):
+                        self.log(f"  迁移已存在: {app}.{name}")
+                    else:
+                        self.log(f"  标记迁移失败: {app}.{name} - {str(e)}")
+                except Exception as e:
+                    self.log(f"  标记迁移失败: {app}.{name} - {str(e)}")
+            
+            self.pg_conn.commit()
+            self.log("成功标记 Django 迁移为已应用状态")
+            return True
+        except Exception as e:
+            self.log(f"标记迁移状态时出错: {str(e)}")
+            return False
+        finally:
+            # 关闭临时连接
+            if self.sqlite_cursor: self.sqlite_cursor.close()
+            if self.sqlite_conn: self.sqlite_conn.close()
+            if self.pg_cursor: self.pg_cursor.close()
+            if self.pg_conn: self.pg_conn.close()
+
+    def reset_sequences(self):
+        """稳健重置 PostgreSQL 序列(每个序列单独处理,出现错误继续)"""
+        try:
+            self.log("\n重置 PostgreSQL 序列(稳健模式)...")
+            # 重新连接数据库,确保干净游标
+            if self.pg_cursor:
+                self.pg_cursor.close()
+            if self.pg_conn:
+                self.pg_conn.close()
+            self.pg_conn = psycopg2.connect(**self.pg_config)
+            self.pg_conn.autocommit = False
+            self.pg_cursor = self.pg_conn.cursor()
+
+            # 找出 public schema 中所有使用 sequence 的列
+            self.pg_cursor.execute("""
+                SELECT table_name, column_name
+                FROM information_schema.columns
+                WHERE column_default LIKE 'nextval(%' AND table_schema = 'public'
+            """)
+            rows = self.pg_cursor.fetchall()
+
+            if not rows:
+                self.log("  未发现任何使用 sequence 的列(可能无需重置)")
+                return True
+
+            for table_name, column_name in rows:
+                try:
+                    # 获取序列名(如果不是 serial 列,pg_get_serial_sequence 可能返回 NULL)
+                    self.pg_cursor.execute(
+                        "SELECT pg_get_serial_sequence(%s, %s)",
+                        (f"public.{table_name}", column_name)
+                    )
+                    seq_row = self.pg_cursor.fetchone()
+                    sequence_name = seq_row[0] if seq_row else None
+
+                    if not sequence_name:
+                        self.log(f"  跳过: {table_name}.{column_name} 没有可识别的 sequence")
+                        continue
+
+                    # 构造并执行 setval,将 sequence 设置为表中 max(id) 或 1,然后提交
+                    setval_sql = sql.SQL(
+                        "SELECT setval(%s, COALESCE((SELECT MAX({col}) FROM {tbl}), 1), true)"
+                    ).format(
+                        col=sql.Identifier(column_name),
+                        tbl=sql.Identifier('public', table_name) if False else sql.Identifier(table_name)
+                    )
+                    # 直接用 sequence_name 作为第一个参数
+                    self.pg_cursor.execute("SELECT setval(%s, COALESCE((SELECT MAX(%s) FROM %s), 1), true)",
+                                        (sequence_name, sql.Identifier(column_name).string, sql.Identifier(table_name).string))
+                    # 上面的字符串化参数不好控制,改为更可靠的方式:
+                    # 使用动态 SQL:
+                    setval_stmt = f"SELECT setval('{sequence_name}', COALESCE((SELECT MAX(\"{column_name}\") FROM \"{table_name}\"), 1), true);"
+                    self.pg_cursor.execute(setval_stmt)
+                    self.pg_conn.commit()
+                    self.log(f"  已重置序列: {sequence_name} (表 {table_name}.{column_name})")
+                except Exception as e:
+                    # 单个序列失败时回滚该序列的事务并继续
+                    try:
+                        self.pg_conn.rollback()
+                    except Exception:
+                        pass
+                    self.log(f"  重置序列失败: {table_name}.{column_name} - {str(e)}")
+                    continue
+
+            self.log("成功尝试重置所有发现的序列")
+            return True
+        except Exception as e:
+            self.log(f"重置序列时出错: {str(e)}")
+            return False
+        finally:
+            if self.pg_cursor:
+                self.pg_cursor.close()
+            if self.pg_conn:
+                self.pg_conn.close()
+
+
 if __name__ == "__main__":
     migrator = SQLiteToPostgresMigrator()
-    migrator.migrate()
+    if migrator.migrate():
+        # 标记 Django 迁移状态
+        migrator.mark_django_migrations_as_applied()
+        
+        # 重置序列
+        migrator.reset_sequences()

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 1205
migration_log_20250919_190948.txt


+ 197 - 0
postgres_service_manager.log

@@ -0,0 +1,197 @@
+2025-09-21 21:38:10,046 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-21 21:38:10,056 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 21:38:10,079 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-21 21:38:10,135 - INFO - PostgreSQL 进程已终止
+2025-09-21 21:38:10,135 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-21 21:38:10,137 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-21 21:38:10,137 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-21 21:38:12,223 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 21:38:14,757 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 21:38:14,758 - INFO - PostgreSQL 服务修复成功
+2025-09-21 21:46:25,959 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-21 21:46:25,973 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 21:46:25,996 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-21 21:46:26,054 - INFO - PostgreSQL 进程已终止
+2025-09-21 21:46:26,055 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-21 21:46:26,056 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-21 21:46:26,056 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-21 21:46:28,131 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 21:46:30,674 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 21:46:30,675 - INFO - PostgreSQL 服务修复成功
+2025-09-21 21:53:39,601 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-21 21:53:39,614 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 21:53:39,637 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-21 21:53:39,693 - INFO - PostgreSQL 进程已终止
+2025-09-21 21:53:39,694 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-21 21:53:39,694 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-21 21:53:39,695 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-21 21:53:39,696 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-21 21:53:39,696 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-21 21:53:41,764 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 21:53:44,290 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 21:53:44,291 - INFO - PostgreSQL 服务修复成功
+2025-09-21 22:11:55,633 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:11:58,167 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2025-09-21 22:12:01,185 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:12:03,737 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 22:20:19,134 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-21 22:20:19,147 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:20:19,167 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-21 22:20:19,225 - INFO - PostgreSQL 进程已终止
+2025-09-21 22:20:19,226 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-21 22:20:19,226 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-21 22:20:19,226 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-21 22:20:19,228 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-21 22:20:19,228 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-21 22:20:21,325 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:20:23,862 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 22:20:23,863 - INFO - PostgreSQL 服务修复成功
+2025-09-21 22:24:59,754 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:25:02,278 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2025-09-21 22:25:05,296 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:25:07,840 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 22:37:52,332 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-21 22:37:52,343 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:37:52,363 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-21 22:37:52,424 - INFO - PostgreSQL 进程已终止
+2025-09-21 22:37:52,424 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-21 22:37:52,424 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-21 22:37:52,425 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-21 22:37:52,426 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-21 22:37:52,426 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-21 22:37:54,504 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:37:57,036 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 22:37:57,037 - INFO - PostgreSQL 服务修复成功
+2025-09-21 22:51:24,260 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:51:24,281 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-21 22:51:24,348 - INFO - PostgreSQL 进程已终止
+2025-09-21 22:51:27,388 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:51:29,972 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 22:51:39,970 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-21 22:51:39,980 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:51:40,001 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-21 22:51:40,057 - INFO - PostgreSQL 进程已终止
+2025-09-21 22:51:40,057 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-21 22:51:40,058 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-21 22:51:40,058 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-21 22:51:40,059 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-21 22:51:40,059 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-21 22:51:42,119 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-21 22:51:44,649 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-21 22:51:44,649 - INFO - PostgreSQL 服务修复成功
+2025-09-22 10:04:13,447 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:04:16,178 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2025-09-22 10:04:19,191 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:04:21,714 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 10:23:27,317 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 10:23:27,328 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:23:27,351 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 10:23:27,440 - INFO - PostgreSQL 进程已终止
+2025-09-22 10:23:27,441 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 10:23:27,441 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-22 10:23:27,441 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-22 10:23:27,442 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-22 10:23:27,442 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 10:23:29,529 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:23:32,073 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 10:23:32,073 - INFO - PostgreSQL 服务修复成功
+2025-09-22 10:50:45,917 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 10:50:45,929 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:50:45,952 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 10:50:46,021 - INFO - PostgreSQL 进程已终止
+2025-09-22 10:50:46,022 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 10:50:46,022 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-22 10:50:46,023 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-22 10:50:46,023 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-22 10:50:46,024 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 10:50:48,097 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:50:50,642 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 10:50:50,642 - INFO - PostgreSQL 服务修复成功
+2025-09-22 10:54:14,290 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 10:54:14,303 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:54:14,328 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 10:54:14,398 - INFO - PostgreSQL 进程已终止
+2025-09-22 10:54:14,398 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 10:54:14,398 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-22 10:54:14,399 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-22 10:54:14,400 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-22 10:54:14,400 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 10:54:16,472 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 10:54:19,008 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 10:54:19,009 - INFO - PostgreSQL 服务修复成功
+2025-09-22 16:14:07,320 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 16:14:07,327 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:14:07,345 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 16:14:07,422 - INFO - PostgreSQL 进程已终止
+2025-09-22 16:14:07,423 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 16:14:07,423 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-22 16:14:07,424 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-22 16:14:07,425 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-22 16:14:07,425 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 16:14:09,510 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:14:12,045 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 16:14:12,045 - INFO - PostgreSQL 服务修复成功
+2025-09-22 16:21:43,729 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:21:46,253 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2025-09-22 16:22:58,529 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:23:36,177 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 16:25:36,578 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 16:25:36,586 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:25:36,609 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 16:25:36,703 - INFO - PostgreSQL 进程已终止
+2025-09-22 16:25:36,704 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 16:25:36,704 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 16:25:38,797 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:25:41,329 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 16:25:41,330 - INFO - PostgreSQL 服务修复成功
+2025-09-22 16:28:35,328 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 16:28:35,335 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:28:35,352 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 16:28:35,424 - INFO - PostgreSQL 进程已终止
+2025-09-22 16:28:35,424 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 16:28:35,424 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-22 16:28:35,425 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-22 16:28:35,426 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-22 16:28:35,426 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 16:28:37,514 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:28:40,049 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 16:28:40,050 - INFO - PostgreSQL 服务修复成功
+2025-09-22 16:29:56,823 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:29:56,846 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 16:29:56,927 - INFO - PostgreSQL 进程已终止
+2025-09-22 16:29:59,943 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:30:02,501 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 16:30:11,872 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 16:30:11,883 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:30:11,908 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 16:30:11,985 - INFO - PostgreSQL 进程已终止
+2025-09-22 16:30:11,985 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 16:30:11,985 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-22 16:30:11,986 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-22 16:30:11,988 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-22 16:30:11,988 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 16:30:14,085 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 16:30:16,611 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 16:30:16,611 - INFO - PostgreSQL 服务修复成功
+2025-09-22 20:51:35,909 - INFO - 开始修复 PostgreSQL 服务启动问题...
+2025-09-22 20:51:35,920 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 20:51:35,943 - WARNING - 服务未正常停止,尝试终止进程
+2025-09-22 20:51:36,022 - INFO - PostgreSQL 进程已终止
+2025-09-22 20:51:36,022 - INFO - 找到 PostgreSQL 数据目录: D:\app\postgresql\data
+2025-09-22 20:51:36,022 - INFO - 发现 recovery.signal 文件,尝试完成恢复过程
+2025-09-22 20:51:36,023 - INFO - 已将 recovery.signal 重命名为 recovery.done
+2025-09-22 20:51:36,024 - INFO - 已备份并删除 postgresql.auto.conf 文件
+2025-09-22 20:51:36,024 - INFO - 检查 postgresql.conf 文件完整性
+2025-09-22 20:51:38,122 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 20:51:40,665 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 20:51:40,665 - INFO - PostgreSQL 服务修复成功
+2025-09-22 20:54:05,880 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 20:54:08,409 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2025-09-22 20:57:37,614 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 20:57:40,153 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动
+2025-09-22 20:58:46,580 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 20:58:46,598 - ERROR - 启动 PostgreSQL 服务失败: 启动 PostgreSQL 服务失败
+2025-09-22 20:58:51,367 - INFO - 尝试停止 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 20:58:53,887 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已停止
+2025-09-22 20:58:57,157 - INFO - 尝试启动 PostgreSQL 服务: postgresql-x64-16
+2025-09-22 20:58:59,191 - INFO - PostgreSQL 服务 'postgresql-x64-16' 已启动

+ 10 - 3
postgresql.md

@@ -1,6 +1,6 @@
 先./ba一下
 
-新建WMSDB
+1 新建WMSDB
 ```
 
 CREATE USER wmsuser WITH PASSWORD 'abc@1234';
@@ -11,7 +11,14 @@ ALTER USER wmsuser SET default_transaction_isolation TO 'read committed';
 ALTER USER wmsuser SET timezone TO 'UTC';
 ```
 
-安装postgresql适配器
+2 安装postgresql适配器
 ```
 pip install psycopg2-binary
-```
+```
+3 运行迁移
+```
+python manage.py migrate
+```
+4 运行数据迁移脚本
+
+

+ 52 - 0
reset.sql

@@ -0,0 +1,52 @@
+DO $$
+DECLARE
+    seq_record RECORD;
+    v_table_name TEXT;
+    max_id BIGINT;
+    sql_text TEXT;
+BEGIN
+    -- 遍历所有public模式下的序列
+    FOR seq_record IN SELECT sequencename FROM pg_sequences WHERE schemaname = 'public'
+    LOOP
+        -- 检查是否为特定序列,需要单独映射
+        IF seq_record.sequencename = 'batch_log_id_seq' THEN
+            v_table_name := 'batch_log_from_container_log';
+        ELSIF seq_record.sequencename = 'batch_log_detail_logs_id_seq' THEN
+            v_table_name := 'batch_log_from_container_log_detail_logs';
+        ELSE
+            -- 从序列名提取表名(假设序列名以_id_seq结尾)
+            v_table_name := substring(seq_record.sequencename from '(.*)_id_seq');
+        END IF;
+        
+        -- 如果成功获取表名
+        IF v_table_name IS NOT NULL THEN
+            -- 检查表是否存在
+            IF EXISTS (SELECT 1 FROM information_schema.tables 
+                      WHERE table_schema = 'public' AND table_name = v_table_name) THEN
+                BEGIN
+                    -- 动态查询最大ID值
+                    EXECUTE 'SELECT COALESCE(MAX(id), 0) + 1 FROM ' || quote_ident(v_table_name) INTO max_id;
+                    
+                    -- 生成并执行重置序列的SQL
+                    sql_text := 'SELECT setval(' || quote_literal(seq_record.sequencename) || ', ' || max_id || ', false);';
+                    EXECUTE sql_text;
+                    
+                    -- 输出成功日志,显示序列名、表名和最大值
+                    RAISE NOTICE '重置序列: % -> 对应表: % 最大值: %', seq_record.sequencename, v_table_name, max_id;
+                    
+                EXCEPTION WHEN OTHERS THEN
+                    -- 处理错误情况
+                    RAISE NOTICE '错误重置序列: % - %', seq_record.sequencename, SQLERRM;
+                END;
+            ELSE
+                -- 输出跳过日志,显示序列名和对应的表名
+                RAISE NOTICE '跳过序列: % (对应表 % 不存在)', seq_record.sequencename, v_table_name;
+            END IF;
+        ELSE
+            -- 输出不符合命名约定的日志
+            RAISE NOTICE '跳过序列: % (不符合命名约定)', seq_record.sequencename;
+        END IF;
+    END LOOP;
+    
+    RAISE NOTICE '序列重置完成';
+END $$;

+ 232 - 232
templates/src/layouts/MainLayout.vue

@@ -666,291 +666,291 @@
   </q-layout>
 </template>
 <script>
-import { get, getauth, post, postauth, baseurl } from "boot/axios_request";
-import { LocalStorage, SessionStorage, openURL } from "quasar";
-import Bus from "boot/bus.js";
-import LottieWebCimo from "components/lottie-web-cimo";
-import Screenfull from "components/Screenfull";
+import { get, getauth, post, postauth, baseurl } from 'boot/axios_request'
+import { LocalStorage, SessionStorage, openURL } from 'quasar'
+import Bus from 'boot/bus.js'
+import LottieWebCimo from 'components/lottie-web-cimo'
+import Screenfull from 'components/Screenfull'
 
 export default {
   components: {
     LottieWebCimo,
-    Screenfull,
+    Screenfull
   },
-  data() {
+  data () {
     return {
-      device: LocalStorage.getItem("device"),
-      device_name: LocalStorage.getItem("device_name"),
-      lang: "zh-hans",
-      container_height: this.$q.screen.height + "" + "px",
-      warehouse_name: "",
+      device: LocalStorage.getItem('device'),
+      device_name: LocalStorage.getItem('device_name'),
+      lang: 'zh-hans',
+      container_height: this.$q.screen.height + '' + 'px',
+      warehouse_name: '',
       warehouseOptions: [],
       warehouseOptions_openid: [],
-      langOptions: [{ value: "zh-hans", label: "中文简体" }],
-      title: this.$t("index.webtitle"),
+      langOptions: [{ value: 'zh-hans', label: '中文简体' }],
+      title: this.$t('index.webtitle'),
       admin: true,
       adminlogin: {
-        name: "",
-        password: "",
+        name: '',
+        password: ''
       },
-      openid: "",
-      appid: "",
+      openid: '',
+      appid: '',
       switch_state: false,
       switch_warehouse: false,
       isPwd: true,
       isPwd2: true,
-      authin: "0",
+      authin: '0',
       authid: false,
       left: false,
       drawerleft: false,
-      tab: "",
+      tab: '',
       login: false,
-      link: "",
-      login_name: "",
+      link: '',
+      login_name: '',
       login_id: 0,
-      check_code: "",
+      check_code: '',
       register: false,
       registerform: {
-        name: "",
-        password1: "",
-        password2: "",
+        name: '',
+        password1: '',
+        password2: ''
       },
-      needLogin: "",
-      activeTab: "",
+      needLogin: '',
+      activeTab: '',
       ERPTasks: 0,
       ERPOutTasks: 0,
       pendingTasks: 0,
       pollInterval: null,
-      timer: null,
-    };
+      timer: null
+    }
   },
   methods: {
-    save_db() {
-      var _this = this;
-      postauth("backup/trigger/").then((res) => {
+    save_db () {
+      var _this = this
+      postauth('backup/trigger/').then((res) => {
         _this.$q.notify({
-          message: "数据库备份成功",
-          icon: "check",
-          color: "green",
-        });
-      });
+          message: '数据库备份成功',
+          icon: 'check',
+          color: 'green'
+        })
+      })
     },
 
-    linkChange(e) {
-      var _this = this;
-      localStorage.removeItem("menulink");
-      localStorage.setItem("menulink", e);
-      _this.link = e;
-      console.log(_this.link);
+    linkChange (e) {
+      var _this = this
+      localStorage.removeItem('menulink')
+      localStorage.setItem('menulink', e)
+      _this.link = e
+      console.log(_this.link)
     },
-    drawerClick(e) {
-      var _this = this;
+    drawerClick (e) {
+      var _this = this
       if (_this.miniState) {
-        _this.miniState = false;
-        e.stopPropagation();
+        _this.miniState = false
+        e.stopPropagation()
       }
     },
-    brownlink(e) {
-      openURL(e);
+    brownlink (e) {
+      openURL(e)
     },
-    apiLink() {
-      openURL(baseurl + "/api/docs/");
+    apiLink () {
+      openURL(baseurl + '/api/docs/')
     },
 
-    adminLogin() {
-      var _this = this;
+    adminLogin () {
+      var _this = this
       if (!_this.adminlogin.name) {
         _this.$q.notify({
-          message: "请输入用户名",
-          color: "negative",
-          icon: "close",
-        });
+          message: '请输入用户名',
+          color: 'negative',
+          icon: 'close'
+        })
       } else {
         if (!_this.adminlogin.password) {
           _this.$q.notify({
-            message: "请输入密码",
-            icon: "close",
-            color: "negative",
-          });
+            message: '请输入密码',
+            icon: 'close',
+            color: 'negative'
+          })
         } else {
-          SessionStorage.set("axios_check", "false");
-          post("login/", _this.adminlogin)
+          SessionStorage.set('axios_check', 'false')
+          post('login/', _this.adminlogin)
             .then((res) => {
-              if (res.code === "200") {
-                _this.authin = "1";
-                _this.login = false;
-                _this.admin = false;
-                _this.openid = res.data.openid;
-                _this.appid = res.data.appid;
+              if (res.code === '200') {
+                _this.authin = '1'
+                _this.login = false
+                _this.admin = false
+                _this.openid = res.data.openid
+                _this.appid = res.data.appid
 
-                _this.login_name = res.data.name;
-                _this.login_id = res.data.user_id;
-                LocalStorage.set("auth", "1");
-                LocalStorage.set("openid", res.data.openid);
-                LocalStorage.set("appid", res.data.appid);
-                LocalStorage.set("login_name", _this.login_name);
-                LocalStorage.set("login_id", _this.login_id);
-                LocalStorage.set("login_mode", res.data.staff_type);
+                _this.login_name = res.data.name
+                _this.login_id = res.data.user_id
+                LocalStorage.set('auth', '1')
+                LocalStorage.set('openid', res.data.openid)
+                LocalStorage.set('appid', res.data.appid)
+                LocalStorage.set('login_name', _this.login_name)
+                LocalStorage.set('login_id', _this.login_id)
+                LocalStorage.set('login_mode', res.data.staff_type)
                 _this.$q.notify({
-                  message: "Success Login",
-                  icon: "check",
-                  color: "green",
-                });
-                localStorage.removeItem("menulink");
-                _this.link = "";
-                _this.$router.push({ name: "web_index" });
+                  message: 'Success Login',
+                  icon: 'check',
+                  color: 'green'
+                })
+                localStorage.removeItem('menulink')
+                _this.link = ''
+                _this.$router.push({ name: 'web_index' })
                 window.setTimeout(() => {
-                  location.reload();
-                }, 1);
+                  location.reload()
+                }, 1)
               } else {
                 _this.$q.notify({
                   message: res.msg,
-                  icon: "close",
-                  color: "negative",
-                });
+                  icon: 'close',
+                  color: 'negative'
+                })
               }
             })
             .catch((err) => {
               _this.$q.notify({
                 message: err.detail,
-                icon: "close",
-                color: "negative",
-              });
-            });
+                icon: 'close',
+                color: 'negative'
+              })
+            })
         }
       }
     },
-    Logout() {
-      var _this = this;
-      _this.authin = "0";
-      _this.login_name = "";
-      LocalStorage.remove("auth");
-      SessionStorage.remove("axios_check");
-      LocalStorage.set("login_name", "");
-      LocalStorage.set("login_id", "");
-      LocalStorage.set("appid", "");
+    Logout () {
+      var _this = this
+      _this.authin = '0'
+      _this.login_name = ''
+      LocalStorage.remove('auth')
+      SessionStorage.remove('axios_check')
+      LocalStorage.set('login_name', '')
+      LocalStorage.set('login_id', '')
+      LocalStorage.set('appid', '')
 
       _this.$q.notify({
-        message: "Success Logout",
-        icon: "check",
-        color: "negative",
-      });
+        message: 'Success Logout',
+        icon: 'check',
+        color: 'negative'
+      })
       // _this.staffType();
-      localStorage.removeItem("menulink");
-      _this.link = "";
-      _this.$router.push({ name: "web_index" });
+      localStorage.removeItem('menulink')
+      _this.link = ''
+      _this.$router.push({ name: 'web_index' })
       window.setTimeout(() => {
-        location.reload();
-      }, 1);
+        location.reload()
+      }, 1)
     },
-    Register() {
-      var _this = this;
-      SessionStorage.set("axios_check", "false");
-      post("register/", _this.registerform)
+    Register () {
+      var _this = this
+      SessionStorage.set('axios_check', 'false')
+      post('register/', _this.registerform)
         .then((res) => {
-          if (res.code === "200") {
-            _this.register = false;
-            _this.openid = res.data.openid;
-            _this.login_name = _this.registerform.name;
-            _this.login_id = res.data.user_id;
-            _this.authin = "1";
+          if (res.code === '200') {
+            _this.register = false
+            _this.openid = res.data.openid
+            _this.login_name = _this.registerform.name
+            _this.login_id = res.data.user_id
+            _this.authin = '1'
 
-            LocalStorage.set("auth", "1");
-            LocalStorage.set("appid", res.data.appid);
-            LocalStorage.set("login_name", res.data.name);
-            LocalStorage.set("login_id", res.data.user_id);
-            LocalStorage.set("openid", res.data.openid);
-            LocalStorage.set("login_mode", "Admin");
+            LocalStorage.set('auth', '1')
+            LocalStorage.set('appid', res.data.appid)
+            LocalStorage.set('login_name', res.data.name)
+            LocalStorage.set('login_id', res.data.user_id)
+            LocalStorage.set('openid', res.data.openid)
+            LocalStorage.set('login_mode', 'Admin')
 
             _this.registerform = {
-              name: "",
-              password1: "",
-              password2: "",
-            };
+              name: '',
+              password1: '',
+              password2: ''
+            }
             _this.$q.notify({
               message: res.msg,
-              icon: "check",
-              color: "green",
-            });
-            _this.staffType();
-            localStorage.removeItem("menulink");
-            _this.link = "";
-            _this.$router.push({ name: "web_index" });
+              icon: 'check',
+              color: 'green'
+            })
+            _this.staffType()
+            localStorage.removeItem('menulink')
+            _this.link = ''
+            _this.$router.push({ name: 'web_index' })
             window.setTimeout(() => {
-              location.reload();
-            }, 1);
+              location.reload()
+            }, 1)
           } else {
             _this.$q.notify({
               message: res.msg,
-              icon: "close",
-              color: "negative",
-            });
+              icon: 'close',
+              color: 'negative'
+            })
           }
         })
         .catch((err) => {
           _this.$q.notify({
             message: err.detail,
-            icon: "close",
-            color: "negative",
-          });
-        });
+            icon: 'close',
+            color: 'negative'
+          })
+        })
     },
-    staffType() {
-      var _this = this;
-      getauth("staff/?staff_name=" + _this.login_name).then((res) => {
-        LocalStorage.set("staff_type", res.results[0].staff_type);
-      });
+    staffType () {
+      var _this = this
+      getauth('staff/?staff_name=' + _this.login_name).then((res) => {
+        LocalStorage.set('staff_type', res.results[0].staff_type)
+      })
     },
-    warehouseOptionsGet() {
-      var _this = this;
-      get("warehouse/multiple/?max_page=30")
+    warehouseOptionsGet () {
+      var _this = this
+      get('warehouse/multiple/?max_page=30')
         .then((res) => {
           // if (res.count === 1) {
           //   _this.openid = res.results[0].openid
           //   _this.warehouse_name = res.results[0].warehouse_name
           //   LocalStorage.set('openid', _this.openid)
           // } else {
-          _this.warehouseOptions = res.results;
-          if (LocalStorage.has("openid")) {
+          _this.warehouseOptions = res.results
+          if (LocalStorage.has('openid')) {
             _this.warehouseOptions.forEach((item, index) => {
-              if (item.openid === LocalStorage.getItem("openid")) {
-                _this.warehouseOptions_openid.push(item);
+              if (item.openid === LocalStorage.getItem('openid')) {
+                _this.warehouseOptions_openid.push(item)
               }
-            });
+            })
           }
-          _this.warehouse_name = res.results[0].warehouse_name;
-          localStorage.setItem("warehouse_name", res.results[0].warehouse_name);
-          localStorage.setItem("warehouse_code", res.results[0].warehouse_code);
+          _this.warehouse_name = res.results[0].warehouse_name
+          localStorage.setItem('warehouse_name', res.results[0].warehouse_name)
+          localStorage.setItem('warehouse_code', res.results[0].warehouse_code)
           // }
         })
         .catch((err) => {
-          console.log(err);
+          console.log(err)
           _this.$q.notify({
             message: err.detail,
-            icon: "close",
-            color: "negative",
-          });
-        });
+            icon: 'close',
+            color: 'negative'
+          })
+        })
     },
-    warehouseChange(e) {
-      var _this = this;
-      _this.openid = _this.warehouseOptions[e].openid;
-      if (_this.openid === LocalStorage.getItem("openid")) {
-        _this.warehouse_name = _this.warehouseOptions[e].warehouse_name;
+    warehouseChange (e) {
+      var _this = this
+      _this.openid = _this.warehouseOptions[e].openid
+      if (_this.openid === LocalStorage.getItem('openid')) {
+        _this.warehouse_name = _this.warehouseOptions[e].warehouse_name
         localStorage.setItem(
-          "warehouse_name",
+          'warehouse_name',
           _this.warehouseOptions[e].warehouse_name
-        );
+        )
         localStorage.setItem(
-          "warehouse_code",
+          'warehouse_code',
           _this.warehouseOptions[e].warehouse_code
-        );
+        )
 
-        _this.switch_state = true;
+        _this.switch_state = true
       } else {
-        _this.switch_state = false;
+        _this.switch_state = false
       }
-      _this.switch_warehouse = true;
+      _this.switch_warehouse = true
 
       // LocalStorage.set('openid', _this.openid)
       // LocalStorage.set('staff_type', 'Admin')
@@ -968,79 +968,79 @@ export default {
     //     location.reload()
     //   }, 1)
     // },
-    isLoggedIn() {
-      if (this.$q.localStorage.getItem("openid")) {
-        this.login = true;
+    isLoggedIn () {
+      if (this.$q.localStorage.getItem('openid')) {
+        this.login = true
       } else {
-        this.register = true;
+        this.register = true
       }
     },
-    handleTimer() {
-      getauth("/wms/inboundBills/?bound_status=0").then((res) => {
-        this.ERPTasks = res.count;
-      });
-      getauth("/wms/outboundBills/?bound_status=0").then((res) => {
-        this.ERPOutTasks = res.count;
-      });
-    },
+    handleTimer () {
+      getauth('/wms/inboundBills/?bound_status=0').then((res) => {
+        this.ERPTasks = res.count
+      })
+      getauth('/wms/outboundBills/?bound_status=0').then((res) => {
+        this.ERPOutTasks = res.count
+      })
+    }
   },
-  created() {
-    var _this = this;
-    _this.$i18n.locale = "zh-hans";
+  created () {
+    var _this = this
+    _this.$i18n.locale = 'zh-hans'
 
-    if (LocalStorage.has("openid")) {
-      _this.openid = LocalStorage.getItem("openid");
-      _this.activeTab = "admin";
+    if (LocalStorage.has('openid')) {
+      _this.openid = LocalStorage.getItem('openid')
+      _this.activeTab = 'admin'
     } else {
-      _this.openid = "";
-      LocalStorage.set("openid", "");
+      _this.openid = ''
+      LocalStorage.set('openid', '')
     }
-    if (LocalStorage.has("login_name")) {
-      _this.login_name = LocalStorage.getItem("login_name");
+    if (LocalStorage.has('login_name')) {
+      _this.login_name = LocalStorage.getItem('login_name')
     } else {
-      _this.login_name = "";
-      LocalStorage.set("login_name", "");
+      _this.login_name = ''
+      LocalStorage.set('login_name', '')
     }
-    if (LocalStorage.has("auth")) {
-      _this.authin = "1";
-      _this.staffType();
+    if (LocalStorage.has('auth')) {
+      _this.authin = '1'
+      _this.staffType()
     } else {
-      LocalStorage.set("staff_type", "Admin");
-      _this.authin = "0";
-      _this.isLoggedIn();
+      LocalStorage.set('staff_type', 'Admin')
+      _this.authin = '0'
+      _this.isLoggedIn()
     }
   },
-  mounted() {
-    var _this = this;
-    _this.warehouseOptionsGet();
-    _this.handleTimer();
-    _this.link = localStorage.getItem("menulink");
-    Bus.$on("needLogin", (val) => {
-      _this.isLoggedIn();
-    });
+  mounted () {
+    var _this = this
+    _this.warehouseOptionsGet()
+    _this.handleTimer()
+    _this.link = localStorage.getItem('menulink')
+    Bus.$on('needLogin', (val) => {
+      _this.isLoggedIn()
+    })
 
     _this.timer = setInterval(() => {
-      _this.handleTimer();
-    }, 100000);
+      _this.handleTimer()
+    }, 100000)
   },
   // 修改时间 :10000
-  updated() {},
-  beforeDestroy() {
-    Bus.$off("needLogin");
+  updated () {},
+  beforeDestroy () {
+    Bus.$off('needLogin')
   },
-  destroyed() {},
+  destroyed () {},
   watch: {
-    login(val) {
+    login (val) {
       if (val) {
-        if (this.activeTab === "admin") {
-          this.admin = true;
+        if (this.activeTab === 'admin') {
+          this.admin = true
         } else {
-          this.admin = false;
+          this.admin = false
         }
       }
-    },
-  },
-};
+    }
+  }
+}
 </script>
 <style>
 .tabs .q-tab__indicator {

+ 278 - 266
templates/src/pages/stock/stocklist.vue

@@ -1,284 +1,296 @@
 <template>
-  <div>
-    <transition appear enter-active-class="animated fadeIn">
-      <q-table
-        class="my-sticky-header-table shadow-24"
-        :data="table_list"
-        row-key="id"
-        :separator="separator"
-        :loading="loading"
-        :filter="filter"
-        :columns="columns"
-        hide-bottom
-        :pagination.sync="pagination"
-        no-data-label="No data"
-        no-results-label="No data you want"
-        :table-style="{ height: height }"
-        flat
-        bordered
-      >
-        <template v-slot:top>
-          <q-btn-group push>
-            <q-btn :label="$t('refresh')" icon="refresh" @click="reFresh()">
-              <q-tooltip content-class="bg-amber text-black shadow-4" :offset="[10, 10]" content-style="font-size: 12px">{{ $t('refreshtip') }}</q-tooltip>
-            </q-btn>
-          </q-btn-group>
-          <q-space />
-          <q-input outlined rounded dense debounce="300" color="primary" v-model="filter" :placeholder="$t('search')" @input="getSearchList()" @keyup.enter="getSearchList()">
-            <template v-slot:append>
-              <q-icon name="search" @click="getSearchList()" />
-            </template>
-          </q-input>
-        </template>
-        <template v-slot:body="props">
-          <q-tr :props="props">
-            <q-td key="goods_code" :props="props">{{ props.row.goods_code }}</q-td>
-            <q-td key="goods_desc" :props="props">{{ props.row.goods_desc }}</q-td>
-            <q-td key="goods_qty" :props="props">{{ props.row.goods_qty }}</q-td>
-            <q-td key="onhand_stock" :props="props">{{ props.row.onhand_stock }}</q-td>
-            <q-td key="can_order_stock" :props="props">{{ props.row.can_order_stock }}</q-td>
-            <q-td key="ordered_stock" :props="props">{{ props.row.ordered_stock }}</q-td>
-            <q-td key="inspect_stock" :props="props">{{ props.row.inspect_stock }}</q-td>
-            <q-td key="hold_stock" :props="props">{{ props.row.hold_stock }}</q-td>
-            <q-td key="damage_stock" :props="props">{{ props.row.damage_stock }}</q-td>
-            <q-td key="asn_stock" :props="props">{{ props.row.asn_stock }}</q-td>
-            <q-td key="dn_stock" :props="props">{{ props.row.dn_stock }}</q-td>
-            <q-td key="pre_load_stock" :props="props">{{ props.row.pre_load_stock }}</q-td>
-            <q-td key="pre_sort_stock" :props="props">{{ props.row.pre_sort_stock }}</q-td>
-            <q-td key="sorted_stock" :props="props">{{ props.row.sorted_stock }}</q-td>
-            <q-td key="pick_stock" :props="props">{{ props.row.pick_stock }}</q-td>
-            <q-td key="picked_stock" :props="props">{{ props.row.picked_stock }}</q-td>
-            <q-td key="back_order_stock" :props="props">{{ props.row.back_order_stock }}</q-td>
-            <q-td key="create_time" :props="props">{{ props.row.create_time }}</q-td>
-            <q-td key="update_time" :props="props">{{ props.row.update_time }}</q-td>
-          </q-tr>
-        </template>
-      </q-table>
-    </transition>
-    <template>
-        <div v-show="max !== 0" class="q-pa-lg flex flex-center">
-           <div>{{ total }} </div>
-          <q-pagination
-            v-model="current"
-            color="black"
-            :max="max"
-            :max-pages="6"
-            boundary-links
-            @click="getList()"
-          />
-          <div>
-            <input
-              v-model="paginationIpt"
-              @blur="changePageEnter"
-              @keyup.enter="changePageEnter"
-              style="width: 60px; text-align: center"
+  <q-page class="q-pa-md">
+    <q-card class="recovery-card">
+      <q-card-section>
+        <div class="text-h6">数据库恢复</div>
+      </q-card-section>
+
+      <q-tabs v-model="tab" align="justify" class="bg-primary text-white">
+        <q-tab name="time" icon="schedule" label="时间点恢复" />
+      </q-tabs>
+
+      <q-tab-panels v-model="tab" animated>
+        <!-- 时间点恢复面板 -->
+        <q-tab-panel name="time">
+          <div class="q-mb-md">
+            <div class="text-subtitle1 q-mb-sm">选择基础备份</div>
+            <q-list bordered separator class="rounded-borders">
+              <q-item 
+                v-for="backup in baseBackups" 
+                :key="backup.path"
+                clickable
+                :active="selectedBaseBackup === backup.path"
+                @click="selectBaseBackup(backup.path)"
+                class="q-mb-sm"
+              >
+                <q-item-section>
+                  <q-item-label>{{ backup.name }}</q-item-label>
+                  <q-item-label caption>{{ backup.date }}</q-item-label>
+                </q-item-section>
+                <q-item-section side>
+                  <q-icon 
+                    name="check_circle" 
+                    color="primary" 
+                    v-if="selectedBaseBackup === backup.path"
+                  />
+                </q-item-section>
+              </q-item>
+              
+              <q-item v-if="baseBackups.length === 0">
+                <q-item-section class="text-grey text-center">
+                  没有可用的基础备份
+                </q-item-section>
+              </q-item>
+            </q-list>
+            
+            <!-- 分页控件 -->
+            <div class="row justify-center q-mt-md">
+              <q-pagination
+                v-model="basePage"
+                :max="baseTotalPages"
+                :max-pages="5"
+                direction-links
+                boundary-links
+              />
+              <div class="q-ml-md text-caption">
+                共 {{ baseTotalItems }} 项,每页 {{ basePageSize }} 项
+              </div>
+            </div>
+          </div>
+
+          <div class="q-mb-md">
+            <q-input
+              v-model="recoveryTime"
+              type="datetime-local"
+              label="选择恢复时间点"
+              :max="maxDateTime"
+              filled
+              hint="格式: YYYY-MM-DD HH:MM:SS"
+            />
+          </div>
+
+          <div class="q-mb-md">
+            <q-toggle
+              v-model="confirmPITR"
+              label="我确认时间点恢复操作将覆盖当前数据库"
             />
           </div>
-        </div>
-        <div v-show="max === 0" class="q-pa-lg flex flex-center">
-          <q-btn flat push color="dark" :label="$t('no_data')"></q-btn>
-        </div>
-    </template>
-  </div>
+
+          <div class="flex justify-end">
+            <q-btn
+              color="primary"
+              icon="restore"
+              label="执行时间点恢复"
+              :disable="!selectedBaseBackup || !recoveryTime || !confirmPITR"
+              @click="restoreToPointInTime"
+            />
+          </div>
+        </q-tab-panel>
+      </q-tab-panels>
+
+      <!-- 恢复进度对话框 -->
+      <q-dialog v-model="showProgress" persistent>
+        <q-card style="min-width: 350px">
+          <q-card-section>
+            <div class="text-h6">恢复进度</div>
+          </q-card-section>
+
+          <q-card-section>
+            <q-linear-progress 
+              :value="progressValue" 
+              color="primary" 
+              class="q-mb-sm"
+              v-if="progressValue !== null"
+            />
+            <q-linear-progress 
+              indeterminate 
+              color="primary" 
+              class="q-mb-sm"
+              v-else
+            />
+            <div class="text-center">{{ progressMessage }}</div>
+          </q-card-section>
+        </q-card>
+      </q-dialog>
+
+      <!-- 恢复结果通知 -->
+      <q-dialog v-model="showResult" persistent>
+        <q-card style="min-width: 350px">
+          <q-card-section>
+            <div class="text-h6">{{ resultTitle }}</div>
+          </q-card-section>
+
+          <q-card-section>
+            <div class="text-body1">{{ resultMessage }}</div>
+          </q-card-section>
+
+          <q-card-actions align="right">
+            <q-btn
+              flat
+              label="确定"
+              color="primary"
+              v-close-popup
+              @click="handleResultClose"
+            />
+          </q-card-actions>
+        </q-card>
+      </q-dialog>
+    </q-card>
+  </q-page>
 </template>
-<router-view />
 
 <script>
-import { getauth, getfile } from 'boot/axios_request';
-import { date, exportFile, LocalStorage } from 'quasar';
+import { postauth } from 'boot/axios_request'
 
 export default {
-  name: 'Pagestocklist',
-  data() {
+  name: 'RecoveryPage',
+
+  data () {
     return {
-      openid: '',
-      login_name: '',
-      authin: '0',
-      pathname: 'stock/list/',
-      pathname_previous: '',
-      pathname_next: '',
-      separator: 'cell',
-      loading: false,
-      height: '',
-      table_list: [],
-      bin_size_list: [],
-      bin_property_list: [],
-      warehouse_list: [],
-      columns: [
-        { name: 'goods_code', required: true, label: this.$t('stock.view_stocklist.goods_code'), align: 'left', field: 'goods_code' },
-        { name: 'goods_desc', label: this.$t('stock.view_stocklist.goods_desc'), field: 'goods_desc', align: 'center' },
-        { name: 'goods_qty', label: this.$t('stock.view_stocklist.goods_qty'), field: 'goods_qty', align: 'center' },
-        { name: 'onhand_stock', label: this.$t('stock.view_stocklist.onhand_stock'), field: 'onhand_stock', align: 'center' },
-        { name: 'can_order_stock', label: this.$t('stock.view_stocklist.can_order_stock'), field: 'can_order_stock', align: 'center' },
-        { name: 'ordered_stock', label: this.$t('stock.view_stocklist.ordered_stock'), field: 'ordered_stock', align: 'center' },
-        { name: 'inspect_stock', label: this.$t('stock.view_stocklist.inspect_stock'), field: 'inspect_stock', align: 'center' },
-        { name: 'hold_stock', label: this.$t('stock.view_stocklist.hold_stock'), field: 'hold_stock', align: 'center' },
-        { name: 'damage_stock', label: this.$t('stock.view_stocklist.damage_stock'), field: 'damage_stock', align: 'center' },
-        { name: 'asn_stock', label: this.$t('stock.view_stocklist.asn_stock'), field: 'asn_stock', align: 'center' },
-        { name: 'dn_stock', label: this.$t('stock.view_stocklist.dn_stock'), field: 'dn_stock', align: 'center' },
-        { name: 'pre_load_stock', label: this.$t('stock.view_stocklist.pre_load_stock'), field: 'pre_load_stock', align: 'center' },
-        { name: 'pre_sort_stock', label: this.$t('stock.view_stocklist.pre_sort_stock'), field: 'pre_sort_stock', align: 'center' },
-        { name: 'sorted_stock', label: this.$t('stock.view_stocklist.sorted_stock'), field: 'sorted_stock', align: 'center' },
-        { name: 'pick_stock', label: this.$t('stock.view_stocklist.pick_stock'), field: 'pick_stock', align: 'center' },
-        { name: 'picked_stock', label: this.$t('stock.view_stocklist.picked_stock'), field: 'picked_stock', align: 'center' },
-        { name: 'back_order_stock', label: this.$t('stock.view_stocklist.back_order_stock'), field: 'back_order_stock', align: 'center' },
-        { name: 'create_time', label: this.$t('createtime'), field: 'create_time', align: 'center' },
-        { name: 'update_time', label: this.$t('updatetime'), field: 'update_time', align: 'center' }
-      ],
-      filter: '',
-      pagination: {
-        page: 1,
-        rowsPerPage: 11
-      },
-      current: 1,
-      max: 0,
-      total: 0,
-      paginationIpt: 1
-    };
+      tab: 'time',
+      selectedBaseBackup: null,
+      confirmPITR: false,
+      showProgress: false,
+      showResult: false,
+      progressMessage: '正在恢复数据库...',
+      progressValue: null,
+      resultTitle: '',
+      resultMessage: '',
+      baseBackups: [],
+      recoveryTime: '',
+      maxDateTime: new Date().toISOString().slice(0, 16),
+      loadingBaseBackups: false,
+      
+      // 时间点恢复分页参数
+      basePage: 1,
+      basePageSize: 5,
+      baseTotalPages: 1,
+      baseTotalItems: 0
+    }
   },
-  methods: {
-    getList() {
-      var _this = this;
-      getauth(_this.pathname + '?ordering=-update_time' + '&page=' + '' + _this.current, {})
-        .then(res => {
-          _this.table_list = res.results;
-          _this.total = res.count
-          if (res.count === 0) {
-            _this.max = 0
-          } else {
-            if (Math.ceil(res.count / _this.pagination.rowsPerPage) === 1) {
-              _this.max = 0
-            } else {
-              _this.max = Math.ceil(res.count / _this.pagination.rowsPerPage)
-            }
-          }
-          _this.pathname_previous = res.previous;
-          _this.pathname_next = res.next;
-        })
-        .catch(err => {
-          _this.$q.notify({
-            message: err.detail,
-            icon: 'close',
-            color: 'negative'
-          });
-        });
-    },
-    changePageEnter(e) {
-      if (Number(this.paginationIpt) < 1) {
-        this.current = 1;
-        this.paginationIpt = 1;
-      } else if (Number(this.paginationIpt) > this.max) {
-        this.current = this.max;
-        this.paginationIpt = this.max;
-      } else {
-        this.current = Number(this.paginationIpt);
-      }
-      this.getList();
-    },
-    getSearchList() {
-      var _this = this;
-      if (LocalStorage.has('auth')) {
-        _this.current = 1
-        _this.paginationIpt = 1
-        getauth(_this.pathname + '?ordering=-update_time' + '&goods_code__icontains=' + _this.filter + '&page=' + '' + _this.current, {})
-          .then(res => {
-            _this.table_list = res.results;
-            _this.total = res.count
-            if (res.count === 0) {
-              _this.max = 0
-            } else {
-              if (Math.ceil(res.count / _this.pagination.rowsPerPage) === 1) {
-                _this.max = 0
-              } else {
-                _this.max = Math.ceil(res.count / _this.pagination.rowsPerPage)
-              }
-            }
-            _this.pathname_previous = res.previous;
-            _this.pathname_next = res.next;
-          })
-          .catch(err => {
-            _this.$q.notify({
-              message: err.detail,
-              icon: 'close',
-              color: 'negative'
-            });
-          });
-      } else {
+
+  watch: {
+    // 监听基础备份分页变化
+    basePage(newPage, oldPage) {
+      if (newPage !== oldPage) {
+        this.fetchBaseBackups()
       }
+    }
+  },
+
+  mounted () {
+    this.fetchBaseBackups()
+  },
+
+  methods: {
+    selectBaseBackup(path) {
+      this.selectedBaseBackup = path
     },
-    getListPrevious() {
-      var _this = this;
-      if (LocalStorage.has('auth')) {
-        getauth(_this.pathname_previous, {})
-          .then(res => {
-            _this.table_list = res.results;
-            _this.pathname_previous = res.previous;
-            _this.pathname_next = res.next;
-          })
-          .catch(err => {
-            _this.$q.notify({
-              message: err.detail,
-              icon: 'close',
-              color: 'negative'
-            });
-          });
-      } else {
+
+    async fetchBaseBackups () {
+      this.loadingBaseBackups = true
+      try {
+        const response = await postauth('backup/list/', {
+          type: 'base',
+          page: this.basePage,
+          page_size: this.basePageSize
+        })
+        
+        // 处理不同的响应结构
+        const data = response.data?.data || response.data || {}
+        
+        this.baseBackups = data.backups || []
+        this.basePage = data.page || 1
+        this.basePageSize = data.page_size || 5
+        this.baseTotalPages = data.total_pages || 1
+        this.baseTotalItems = data.total_items || 0
+      } catch (error) {
+        console.error('获取基础备份失败:', error)
+        this.$q.notify({
+          type: 'negative',
+          message: '获取基础备份失败: ' + (error.response?.data?.message || error.message)
+        })
+      } finally {
+        this.loadingBaseBackups = false
       }
     },
-    getListNext() {
-      var _this = this;
-      if (LocalStorage.has('auth')) {
-        getauth(_this.pathname_next, {})
-          .then(res => {
-            _this.table_list = res.results;
-            _this.pathname_previous = res.previous;
-            _this.pathname_next = res.next;
-          })
-          .catch(err => {
-            _this.$q.notify({
-              message: err.detail,
-              icon: 'close',
-              color: 'negative'
-            });
-          });
-      } else {
+
+    async restoreToPointInTime () {
+      this.showProgress = true
+      this.progressMessage = '正在准备恢复操作...'
+      this.progressValue = 0
+      
+      try {
+        // 转换时间格式为 YYYY-MM-DD HH:MM:SS
+        const formattedTime = this.recoveryTime.replace('T', ' ') + ':00'
+        
+        // 模拟进度更新
+        const progressInterval = setInterval(() => {
+          if (this.progressValue < 90) {
+            this.progressValue += 10
+            this.progressMessage = `恢复中... ${this.progressValue}%`
+          }
+        }, 500)
+        
+        await postauth('backup/point/', {
+          base_backup: this.selectedBaseBackup,
+          recovery_time: formattedTime
+        })
+        
+        clearInterval(progressInterval)
+        this.progressValue = 100
+        this.progressMessage = '恢复完成!'
+        
+        setTimeout(() => {
+          this.showProgress = false
+          this.resultTitle = '恢复成功'
+          this.resultMessage = `数据库已成功恢复到 ${new Date(
+            this.recoveryTime
+          ).toLocaleString()}`
+          this.showResult = true
+        }, 1000)
+      } catch (error) {
+        this.showProgress = false
+        this.resultTitle = '恢复失败'
+        this.resultMessage =
+          '时间点恢复失败: ' + (error.response?.data?.message || error.message)
+        this.showResult = true
       }
     },
-    reFresh() {
-      var _this = this;
-      _this.getList();
-    }
-  },
-  created() {
-    var _this = this;
-    if (LocalStorage.has('openid')) {
-      _this.openid = LocalStorage.getItem('openid');
-    } else {
-      _this.openid = '';
-      LocalStorage.set('openid', '');
-    }
-    if (LocalStorage.has('login_name')) {
-      _this.login_name = LocalStorage.getItem('login_name');
-    } else {
-      _this.login_name = '';
-      LocalStorage.set('login_name', '');
-    }
-    if (LocalStorage.has('auth')) {
-      _this.authin = '1';
-      _this.getList();
-    } else {
-      _this.authin = '0';
-    }
-  },
-  mounted() {
-    var _this = this;
-    if (_this.$q.platform.is.electron) {
-      _this.height = String(_this.$q.screen.height - 290) + 'px';
-    } else {
-      _this.height = _this.$q.screen.height - 290 + '' + 'px';
+
+    handleResultClose () {
+      // 恢复完成后刷新页面
+      window.location.reload()
     }
-  },
-  updated() {},
-  destroyed() {}
-};
+  }
+}
 </script>
+
+<style scoped>
+.recovery-card {
+  max-width: 800px;
+  margin: 0 auto;
+}
+
+.q-tab-panel {
+  padding: 20px;
+}
+
+.q-item {
+  border-radius: 8px;
+  transition: background-color 0.3s;
+}
+
+.q-item--active {
+  background-color: #e6f7ff;
+  border-left: 3px solid #1976d2;
+}
+
+.q-item:hover {
+  background-color: #f5f5f5;
+}
+
+.q-pagination {
+  margin-top: 16px;
+}
+</style>

+ 250 - 0
testpostgresql.py

@@ -0,0 +1,250 @@
+import os
+import subprocess
+import logging
+from datetime import datetime, timedelta
+from pathlib import Path
+import psycopg2
+import sys
+
+# -------------------------
+# 配置与日志
+# -------------------------
+def setup_logger():
+    logger = logging.getLogger("wal_archive_check")
+    logger.setLevel(logging.INFO)
+    if not logger.handlers:
+        log_dir = Path("logs")
+        log_dir.mkdir(exist_ok=True)
+        fh = logging.FileHandler(log_dir / "wal_archive_check.log", encoding="utf-8")
+        ch = logging.StreamHandler()
+        fmt = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
+        fh.setFormatter(fmt)
+        ch.setFormatter(fmt)
+        logger.addHandler(fh)
+        logger.addHandler(ch)
+    return logger
+
+logger = setup_logger()
+
+# -------------------------
+# 数据库连接辅助函数
+# -------------------------
+def connect_to_db(dbname='postgres', user='postgres', password='zhanglei', 
+                 host='localhost', port=5432):
+    try:
+        conn = psycopg2.connect(
+            dbname=dbname,
+            user=user,
+            password=password,
+            host=host,
+            port=port,
+            connect_timeout=5
+        )
+        return conn
+    except Exception as e:
+        logger.error(f"数据库连接失败: {e}")
+        return None
+
+# -------------------------
+# 获取当前WAL文件名(兼容所有psycopg2版本)
+# -------------------------
+def get_current_wal_file(conn):
+    """获取当前WAL文件名(兼容所有psycopg2版本)"""
+    try:
+        cur = conn.cursor()
+        # 使用SQL函数获取WAL文件名
+        cur.execute("SELECT pg_walfile_name(pg_current_wal_lsn())")
+        current_wal_file = cur.fetchone()[0]
+        cur.close()
+        return current_wal_file
+    except Exception as e:
+        logger.error(f"获取当前WAL文件名失败: {e}")
+        return None
+
+# -------------------------
+# 获取归档状态
+# -------------------------
+def get_archive_status(conn):
+    try:
+        cur = conn.cursor()
+        cur.execute("SELECT * FROM pg_stat_archiver")
+        columns = [desc[0] for desc in cur.description]
+        row = cur.fetchone()
+        cur.close()
+        
+        if row:
+            return dict(zip(columns, row))
+        return None
+    except Exception as e:
+        logger.error(f"获取归档状态失败: {e}")
+        return None
+
+# -------------------------
+# 检查WAL文件是否归档
+# -------------------------
+def check_wal_archived(wal_name, archive_dir):
+    """检查特定WAL文件是否在归档目录中"""
+    archive_path = Path(archive_dir) / wal_name
+    return archive_path.exists()
+
+# -------------------------
+# 获取未归档的WAL文件列表(优化性能)
+# -------------------------
+def get_missing_wal_files(conn, archive_dir):
+    """获取所有未归档的WAL文件(优化性能)"""
+    try:
+        cur = conn.cursor()
+        # 获取需要检查的WAL文件范围
+        cur.execute("""
+            SELECT min(pg_walfile_name(lsn)) AS first_wal,
+                   max(pg_walfile_name(lsn)) AS last_wal
+            FROM generate_series(
+                '0/00000000'::pg_lsn,
+                pg_current_wal_lsn(),
+                '1'::pg_lsn
+            ) AS lsn
+        """)
+        result = cur.fetchone()
+        if not result:
+            return []
+        
+        first_wal, last_wal = result
+        logger.info(f"检查WAL文件范围: {first_wal} 到 {last_wal}")
+        
+        # 获取归档目录中已有的WAL文件
+        archive_files = set(f.name for f in Path(archive_dir).iterdir() 
+                           if f.name.startswith('0000000') and 
+                           first_wal <= f.name <= last_wal)
+        
+        # 生成所有应该存在的WAL文件
+        all_wal_files = set()
+        current = first_wal
+        while current <= last_wal:
+            all_wal_files.add(current)
+            # 计算下一个WAL文件名
+            prefix = current[:24]
+            suffix = int(current[24:], 16) + 1
+            current = f"{prefix}{suffix:08X}"
+        
+        # 找出缺失的文件
+        missing_files = sorted(all_wal_files - archive_files)
+        return missing_files
+    except Exception as e:
+        logger.error(f"获取未归档WAL文件失败: {e}")
+        return []
+
+# -------------------------
+# 检查归档目录状态
+# -------------------------
+def check_archive_directory(archive_dir):
+    """检查归档目录的基本状态"""
+    archive_path = Path(archive_dir)
+    
+    if not archive_path.exists():
+        logger.error(f"归档目录不存在: {archive_dir}")
+        return False
+    
+    if not archive_path.is_dir():
+        logger.error(f"归档路径不是目录: {archive_dir}")
+        return False
+    
+    # 检查目录中是否有WAL文件
+    wal_files = list(archive_path.glob("0000000*"))
+    if not wal_files:
+        logger.warning(f"归档目录中没有WAL文件: {archive_dir}")
+        return False
+    
+    return True
+
+# -------------------------
+# 主检查函数
+# -------------------------
+def check_wal_archive_status(archive_dir, dbname='postgres', user='postgres', 
+                            password='zhanglei', host='localhost', port=5432):
+    """
+    检查WAL归档状态
+    archive_dir: WAL归档目录路径
+    """
+    logger.info(f"开始检查WAL归档状态: {archive_dir}")
+    
+    # 1. 检查归档目录
+    if not check_archive_directory(archive_dir):
+        return False
+    
+    # 2. 连接数据库
+    conn = connect_to_db(dbname, user, password, host, port)
+    if not conn:
+        return False
+    
+    try:
+        # 3. 获取当前WAL文件名
+        current_wal_file = get_current_wal_file(conn)
+        if not current_wal_file:
+            logger.error("无法获取当前WAL文件名")
+            return False
+        logger.info(f"当前WAL文件: {current_wal_file}")
+        
+        # 4. 获取归档状态
+        archive_status = get_archive_status(conn)
+        if archive_status:
+            logger.info("归档状态:")
+            for key, value in archive_status.items():
+                if key in ['last_archived_wal', 'last_failed_wal', 'last_archived_time', 
+                          'last_failed_time', 'last_archived_failure_message']:
+                    logger.info(f"  {key}: {value}")
+            
+            # 检查最后归档的文件
+            last_archived_wal = archive_status.get("last_archived_wal")
+            if last_archived_wal:
+                logger.info(f"最后归档的WAL文件: {last_archived_wal}")
+                
+                # 检查最后归档的文件是否在目录中
+                if check_wal_archived(last_archived_wal, archive_dir):
+                    logger.info("最后归档的文件存在于归档目录中")
+                else:
+                    logger.error("最后归档的文件不存在于归档目录中!")
+            
+            # 检查是否有归档失败
+            last_failed_wal = archive_status.get("last_failed_wal")
+            if last_failed_wal:
+                logger.error(f"最后归档失败的文件: {last_failed_wal}")
+                logger.error(f"失败原因: {archive_status.get('last_archived_failure_message')}")
+        
+        # 5. 获取未归档的文件列表
+        missing_files = get_missing_wal_files(conn, archive_dir)
+        if missing_files:
+            logger.warning(f"发现 {len(missing_files)} 个未归档的WAL文件:")
+            for i, file in enumerate(missing_files[:10]):  # 最多显示前10个
+                logger.warning(f"  {i+1}. {file}")
+            if len(missing_files) > 10:
+                logger.warning(f"  还有 {len(missing_files)-10} 个未显示...")
+        else:
+            logger.info("所有WAL文件均已归档")
+        
+        return True
+    finally:
+        conn.close()
+
+# -------------------------
+# 主函数
+# -------------------------
+if __name__ == "__main__":
+    # 配置参数
+    archive_dir = r"E:\code\backup\postgres\wal_archive"  # 修改为您的归档目录
+    db_params = {
+        'dbname': 'postgres',
+        'user': 'postgres',
+        'password': 'zhanglei',
+        'host': 'localhost',
+        'port': 5432
+    }
+    
+    # 执行检查
+    success = check_wal_archive_status(archive_dir, **db_params)
+    
+    if success:
+        logger.info("WAL归档状态检查完成")
+        sys.exit(0)
+    else:
+        logger.error("WAL归档状态检查失败")
+        sys.exit(1)