Files
Neo-ZQYY/scripts/ops/init_test_user.py

322 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
一键初始化测试用户 — 打通认证→业务页面的完整链路。
用途:
在没有租户管理后台的情况下,直接在 test_zqyy_app 中插入必要的
映射数据,让测试用户能通过 dev-login 登录后正常访问业务页面。
操作内容:
1. 确保 site_code_mapping 中有真实门店映射
2. 创建/更新测试用户为 approved 状态
3. 分配门店角色user_site_roles
4. 创建助教绑定user_assistant_binding
5. 输出可用于测试的 JWT token
使用方式:
cd C:\\NeoZQYY
python scripts/ops/init_test_user.py [--openid <openid>] [--role <role_code>] [--reset]
环境要求:
- 根 .env 中配置 APP_DB_DSN指向 test_zqyy_app
- WX_DEV_MODE=truedev-login 端点需要)
"""
from __future__ import annotations
import argparse
import json
import logging
import os
import sys
from pathlib import Path
import psycopg2
from dotenv import load_dotenv
# ── 环境加载 ──────────────────────────────────────────────
_ROOT = Path(__file__).resolve().parents[2]
load_dotenv(_ROOT / ".env", override=False)
APP_DB_DSN = os.environ.get("APP_DB_DSN")
if not APP_DB_DSN:
print("❌ 环境变量 APP_DB_DSN 未定义,请检查根 .env", file=sys.stderr)
sys.exit(1)
# 安全检查:禁止连正式库
if "test_" not in APP_DB_DSN and "test-" not in APP_DB_DSN:
print("❌ APP_DB_DSN 不包含 'test_',疑似正式库,拒绝执行", file=sys.stderr)
sys.exit(1)
logging.basicConfig(level=logging.INFO, format="%(message)s")
log = logging.getLogger(__name__)
# ── ETL 真实数据(从 test_etl_feiqiu 查询所得) ──────────
# 朗朗桌球 — 唯一的测试门店
REAL_SITE_ID = 2790685415443269
REAL_TENANT_ID = 2790683160709957
REAL_SHOP_NAME = "朗朗桌球"
SITE_CODE = "LL001" # 已存在的映射
# 默认绑定的助教(李楚欣,欣欣)
DEFAULT_ASSISTANT_ID = 2793361915547781
DEFAULT_ASSISTANT_NAME = "李楚欣"
# 默认绑定的员工(厉超)
DEFAULT_STAFF_ID = 3020235774380805
DEFAULT_STAFF_NAME = "厉超"
# ── 角色映射 ──────────────────────────────────────────────
ROLE_MAP = {
"coach": 1, # 助教
"staff": 2, # 员工
"site_admin": 3, # 店铺管理员
"tenant_admin": 4, # 租户管理员
}
def get_conn():
"""获取数据库连接。"""
conn = psycopg2.connect(APP_DB_DSN)
conn.autocommit = False
return conn
def _safe_tenant_id():
"""返回真实 tenant_id列已迁移为 bigint无需范围检查"""
return REAL_TENANT_ID
def ensure_site_code_mapping(cur) -> int:
"""确保 site_code_mapping 中有朗朗桌球的映射,返回 site_id。"""
cur.execute(
"SELECT site_id FROM auth.site_code_mapping WHERE site_code = %s",
(SITE_CODE,),
)
row = cur.fetchone()
if row:
existing_site_id = row[0]
if existing_site_id != REAL_SITE_ID:
# site_id 不匹配(旧测试数据),更新为真实值
cur.execute(
"""
UPDATE auth.site_code_mapping
SET site_id = %s, site_name = %s, tenant_id = %s
WHERE site_code = %s
""",
(REAL_SITE_ID, REAL_SHOP_NAME, _safe_tenant_id(), SITE_CODE),
)
log.info("✅ site_code_mapping 已更新: %s%s (%s)(旧值 %s",
SITE_CODE, REAL_SITE_ID, REAL_SHOP_NAME, existing_site_id)
else:
log.info("✅ site_code_mapping 已存在: %s%s (%s)", SITE_CODE, REAL_SITE_ID, REAL_SHOP_NAME)
return REAL_SITE_ID
cur.execute(
"""
INSERT INTO auth.site_code_mapping (site_code, site_id, site_name, tenant_id)
VALUES (%s, %s, %s, %s)
RETURNING site_id
""",
(SITE_CODE, REAL_SITE_ID, REAL_SHOP_NAME, _safe_tenant_id()),
)
site_id = cur.fetchone()[0]
log.info("✅ 已创建 site_code_mapping: %s%s (%s)", SITE_CODE, site_id, REAL_SHOP_NAME)
return site_id
def ensure_user(cur, openid: str, nickname: str, reset: bool) -> tuple[int, str]:
"""创建或更新测试用户,返回 (user_id, status)。"""
cur.execute(
"SELECT id, status FROM auth.users WHERE wx_openid = %s",
(openid,),
)
row = cur.fetchone()
if row and not reset:
user_id, status = row
if status != "approved":
cur.execute(
"UPDATE auth.users SET status = 'approved', updated_at = NOW() WHERE id = %s",
(user_id,),
)
log.info("✅ 用户 %s (id=%d) 状态更新: %s → approved", openid, user_id, status)
return user_id, "approved"
log.info("✅ 用户已存在且已审核: %s (id=%d)", openid, user_id)
return user_id, status
if row and reset:
user_id = row[0]
# 清理旧数据
cur.execute("DELETE FROM auth.user_assistant_binding WHERE user_id = %s", (user_id,))
cur.execute("DELETE FROM auth.user_site_roles WHERE user_id = %s", (user_id,))
cur.execute("DELETE FROM auth.user_applications WHERE user_id = %s", (user_id,))
cur.execute(
"""
UPDATE auth.users
SET status = 'approved', nickname = %s, updated_at = NOW()
WHERE id = %s
""",
(nickname, user_id),
)
log.info("✅ 用户 %s (id=%d) 已重置为 approved", openid, user_id)
return user_id, "approved"
# 新建用户
cur.execute(
"""
INSERT INTO auth.users (wx_openid, nickname, status)
VALUES (%s, %s, 'approved')
RETURNING id
""",
(openid, nickname),
)
user_id = cur.fetchone()[0]
log.info("✅ 已创建用户: %s (id=%d, nickname=%s)", openid, user_id, nickname)
return user_id, "approved"
def ensure_site_role(cur, user_id: int, site_id: int, role_code: str) -> None:
"""分配门店角色。"""
role_id = ROLE_MAP.get(role_code)
if not role_id:
log.error("❌ 未知角色: %s(可选: %s", role_code, ", ".join(ROLE_MAP))
sys.exit(1)
cur.execute(
"""
SELECT id FROM auth.user_site_roles
WHERE user_id = %s AND site_id = %s AND role_id = %s
""",
(user_id, site_id, role_id),
)
if cur.fetchone():
log.info("✅ 角色已分配: user_id=%d, site_id=%d, role=%s", user_id, site_id, role_code)
return
cur.execute(
"""
INSERT INTO auth.user_site_roles (user_id, site_id, role_id)
VALUES (%s, %s, %s)
""",
(user_id, site_id, role_id),
)
log.info("✅ 已分配角色: user_id=%d, site_id=%d, role=%s (role_id=%d)", user_id, site_id, role_code, role_id)
def ensure_binding(cur, user_id: int, site_id: int, role_code: str) -> None:
"""创建助教/员工绑定。"""
if role_code == "coach":
binding_type = "assistant"
assistant_id = DEFAULT_ASSISTANT_ID
staff_id = None
label = f"助教 {DEFAULT_ASSISTANT_NAME} (id={assistant_id})"
elif role_code == "staff":
binding_type = "staff"
assistant_id = None
staff_id = DEFAULT_STAFF_ID
label = f"员工 {DEFAULT_STAFF_NAME} (id={staff_id})"
elif role_code in ("site_admin", "tenant_admin"):
binding_type = "manager"
assistant_id = None
staff_id = DEFAULT_STAFF_ID
label = f"管理员绑定员工 {DEFAULT_STAFF_NAME} (id={staff_id})"
else:
log.warning("⚠️ 未知角色 %s,跳过绑定", role_code)
return
cur.execute(
"""
SELECT id FROM auth.user_assistant_binding
WHERE user_id = %s AND site_id = %s
""",
(user_id, site_id),
)
if cur.fetchone():
log.info("✅ 绑定已存在: user_id=%d, site_id=%d", user_id, site_id)
return
cur.execute(
"""
INSERT INTO auth.user_assistant_binding
(user_id, site_id, assistant_id, staff_id, binding_type)
VALUES (%s, %s, %s, %s, %s)
""",
(user_id, site_id, assistant_id, staff_id, binding_type),
)
log.info("✅ 已创建绑定: %s%s", f"user_id={user_id}", label)
def print_summary(openid: str, user_id: int, site_id: int, role_code: str) -> None:
"""输出测试信息摘要。"""
log.info("")
log.info("=" * 60)
log.info(" 测试用户初始化完成")
log.info("=" * 60)
log.info(" openid: %s", openid)
log.info(" user_id: %d", user_id)
log.info(" site_id: %d", site_id)
log.info(" site_code: %s (%s)", SITE_CODE, REAL_SHOP_NAME)
log.info(" 角色: %s (role_id=%d)", role_code, ROLE_MAP[role_code])
log.info(" 状态: approved")
log.info("")
log.info(" 下一步:")
log.info(" 1. 启动后端: cd apps/backend && uvicorn app.main:app --reload")
log.info(" 2. 调用 dev-login 获取 JWT:")
log.info(' curl -X POST http://localhost:8000/api/xcx/dev-login \\')
log.info(' -H "Content-Type: application/json" \\')
log.info(' -d \'{"openid": "%s", "status": "approved"}\'', openid)
log.info("")
log.info(" 3. 用返回的 access_token 测试业务接口:")
log.info(' curl http://localhost:8000/api/xcx/me \\')
log.info(' -H "Authorization: Bearer <access_token>"')
log.info("=" * 60)
def main():
parser = argparse.ArgumentParser(
description="一键初始化测试用户,打通认证→业务页面完整链路",
)
parser.add_argument(
"--openid",
default="dev_test_openid",
help="测试用户的 wx_openid默认: dev_test_openid",
)
parser.add_argument(
"--nickname",
default="测试管理员",
help="测试用户昵称(默认: 测试管理员)",
)
parser.add_argument(
"--role",
default="site_admin",
choices=list(ROLE_MAP.keys()),
help="分配的角色(默认: site_admin",
)
parser.add_argument(
"--reset",
action="store_true",
help="重置用户:清除旧的角色/绑定/申请,重新初始化",
)
args = parser.parse_args()
conn = get_conn()
try:
with conn.cursor() as cur:
site_id = ensure_site_code_mapping(cur)
user_id, _ = ensure_user(cur, args.openid, args.nickname, args.reset)
ensure_site_role(cur, user_id, site_id, args.role)
ensure_binding(cur, user_id, site_id, args.role)
conn.commit()
print_summary(args.openid, user_id, site_id, args.role)
except Exception:
conn.rollback()
log.exception("❌ 初始化失败,已回滚")
sys.exit(1)
finally:
conn.close()
if __name__ == "__main__":
main()