#!/usr/bin/env python3 """将 sys_user 中密码明文等价于 111111 的账号统一改为 Zkxw@123。 因 bcrypt 每次盐不同,无法用 SQL WHERE password='111111' 命中,必须在应用侧校验。 连接方式(任选其一,优先顺序见下方): 1) --database-url / 环境变量 DATABASE_URL 2) --db-host + --db-user + --db-password + --db-name(可选 --db-port) 3) --env-file / 环境变量 ENV_FILE(默认 backend/.env) 用法示例(在 backend 目录): # 显式指定库(推荐测试/生产,避免误连本地 .env) python scripts/reset_weak_password_111111.py \\ --database-url 'mysql+pymysql://user:pass@host:3306/dbname' --dry-run python scripts/reset_weak_password_111111.py \\ --db-host 1.2.3.4 --db-user root --db-password 'xxx' --db-name zkxw --dry-run # 使用指定环境文件 python scripts/reset_weak_password_111111.py --env-file ./.env.test --dry-run # 正式写入(去掉 --dry-run) python scripts/reset_weak_password_111111.py --database-url '...' 范围:del_flag=0(含停用账号 status!=1)。 """ from __future__ import annotations import argparse import os import re import sys from datetime import datetime from pathlib import Path from urllib.parse import quote_plus WEAK_PASSWORD = "111111" NEW_PASSWORD = "Zkxw@123" BACKEND_ROOT = Path(__file__).resolve().parent.parent def _mask_database_url(url: str) -> str: return re.sub(r":([^:@/]+)@", r":***@", url or "") def _build_url_from_parts( *, host: str, user: str, password: str, name: str, port: int, ) -> str: return ( f"mysql+pymysql://{quote_plus(user)}:{quote_plus(password)}" f"@{host}:{port}/{name}" ) def _parse_args(argv: list[str] | None = None) -> argparse.Namespace: parser = argparse.ArgumentParser( description="重置弱密码 111111 → Zkxw@123(支持变量指定目标库)" ) parser.add_argument( "--dry-run", action="store_true", help="只列出将修改的账号,不写库", ) parser.add_argument( "--env-file", default=None, help="环境文件路径(等同 ENV_FILE);未指定 database-url/分项时使用", ) parser.add_argument( "--database-url", default=None, help="完整 SQLAlchemy URL(等同 DATABASE_URL),优先于 env-file", ) parser.add_argument("--db-host", default=None, help="MySQL 主机") parser.add_argument("--db-port", type=int, default=3306, help="MySQL 端口,默认 3306") parser.add_argument("--db-user", default=None, help="MySQL 用户") parser.add_argument("--db-password", default=None, help="MySQL 密码") parser.add_argument("--db-name", default=None, help="数据库名") return parser.parse_args(argv) def _resolve_connection(args: argparse.Namespace) -> tuple[str, str]: """返回 (来源说明, DATABASE_URL)。在 import app 之前调用并写入 os.environ。""" parts = (args.db_host, args.db_user, args.db_password, args.db_name) if any(parts) and not all(p is not None and str(p) != "" for p in parts): raise SystemExit( "使用分项连接时须同时提供: --db-host --db-user --db-password --db-name" ) if all(p is not None and str(p) != "" for p in parts): url = _build_url_from_parts( host=args.db_host, user=args.db_user, password=args.db_password, name=args.db_name, port=args.db_port or 3306, ) os.environ["DATABASE_URL"] = url return ("cli --db-*", url) if args.database_url: url = args.database_url.strip() os.environ["DATABASE_URL"] = url return ("cli --database-url", url) env_url = (os.environ.get("DATABASE_URL") or "").strip() if env_url: return ("env DATABASE_URL", env_url) env_file = args.env_file or os.environ.get("ENV_FILE") or str(BACKEND_ROOT / ".env") env_file_path = Path(env_file).expanduser() if not env_file_path.is_absolute(): env_file_path = (Path.cwd() / env_file_path).resolve() if not env_file_path.is_file(): raise SystemExit( f"环境文件不存在: {env_file_path}\n" "请改用 --database-url 或 --db-host/--db-user/--db-password/--db-name" ) os.environ["ENV_FILE"] = str(env_file_path) # DATABASE_URL 由 Settings 从该文件加载;此处仅记录来源 return (f"env-file {env_file_path}", "") def main(argv: list[str] | None = None) -> int: args = _parse_args(argv) source, early_url = _resolve_connection(args) sys.path.insert(0, str(BACKEND_ROOT)) # 必须在设置 ENV_FILE / DATABASE_URL 之后再 import,否则 Settings 已冻结 from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from app.core.config import settings from app.core.security import get_password_hash, verify_password from app.models.models import SysUser database_url = (os.environ.get("DATABASE_URL") or "").strip() or settings.DATABASE_URL if early_url: database_url = early_url print(f"目标库来源: {source}") print(f"DATABASE_URL: {_mask_database_url(database_url)}") engine = create_engine( database_url, pool_pre_ping=True, pool_recycle=3600, echo=False, ) Session = sessionmaker(autocommit=False, autoflush=False, expire_on_commit=False, bind=engine) db = Session() try: rows = ( db.query(SysUser) .filter(SysUser.del_flag == 0) .order_by(SysUser.create_time.desc()) .all() ) hits = [] for u in rows: pwd = getattr(u, "password", None) or "" if verify_password(WEAK_PASSWORD, pwd): hits.append(u) if not hits: print("未发现密码为 111111 的用户(del_flag=0)。") return 0 print(f"命中 {len(hits)} 个弱密码用户:") for u in hits: print( f" id={u.id} user_no={u.user_no!r} status={u.status!r} " f"tenant_no={u.tenant_no!r}" ) if args.dry_run: print("dry-run:未写入数据库。去掉 --dry-run 后执行正式更新。") return 0 new_hash = get_password_hash(NEW_PASSWORD) now = datetime.now() for u in hits: u.password = new_hash u.update_time = now db.commit() print(f"已更新 {len(hits)} 个用户密码为强密码(明文 Zkxw@123)。请通知相关用户。") return 0 except Exception as exc: db.rollback() print(f"执行失败: {exc}", file=sys.stderr) return 1 finally: db.close() engine.dispose() if __name__ == "__main__": raise SystemExit(main())