微信小程序页面迁移校验之前 P5任务处理之前

This commit is contained in:
Neo
2026-03-09 01:19:21 +08:00
parent 263bf96035
commit 6e20987d2f
1112 changed files with 153824 additions and 219694 deletions

View 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=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()