微信小程序页面迁移校验之前 P5任务处理之前
This commit is contained in:
321
scripts/ops/init_test_user.py
Normal file
321
scripts/ops/init_test_user.py
Normal file
@@ -0,0 +1,321 @@
|
||||
# -*- 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=true(dev-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()
|
||||
Reference in New Issue
Block a user