| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205 |
- #!/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())
|