微信小程序页面迁移校验之前 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

@@ -9,6 +9,11 @@
- GET /api/xcx/me/sites — 查询关联店铺
- POST /api/xcx/switch-site — 切换当前店铺
- POST /api/xcx/refresh — 刷新令牌
- POST /api/xcx/dev-login — 开发模式 mock 登录(仅 WX_DEV_MODE=true
- POST /api/xcx/dev-switch-role — 切换角色(仅 WX_DEV_MODE=true
- POST /api/xcx/dev-switch-status — 切换用户状态(仅 WX_DEV_MODE=true
- POST /api/xcx/dev-switch-binding — 切换人员绑定(仅 WX_DEV_MODE=true
- GET /api/xcx/dev-context — 查询调试上下文(仅 WX_DEV_MODE=true
"""
from __future__ import annotations
@@ -29,6 +34,7 @@ from app.auth.jwt import (
create_token_pair,
decode_refresh_token,
)
from app import config
from app.database import get_connection
from app.services.application import (
create_application,
@@ -37,6 +43,11 @@ from app.services.application import (
from app.schemas.xcx_auth import (
ApplicationRequest,
ApplicationResponse,
DevLoginRequest,
DevSwitchBindingRequest,
DevSwitchRoleRequest,
DevSwitchStatusRequest,
DevContextResponse,
RefreshTokenRequest,
SiteInfo,
SwitchSiteRequest,
@@ -45,6 +56,7 @@ from app.schemas.xcx_auth import (
WxLoginResponse,
)
from app.services.wechat import WeChatAuthError, code2session
from app.services.role import get_user_permissions
logger = logging.getLogger(__name__)
@@ -94,9 +106,9 @@ async def wx_login(body: WxLoginRequest):
流程code → code2session(openid) → 查找/创建 auth.users → 签发 JWT。
- disabled 用户返回 403
- 新用户自动创建status=pending
- 新用户自动创建status=new前端引导至申请页
- approved 用户签发包含 site_id + roles 的完整令牌
- pending/rejected 用户签发受限令牌
- new/pending/rejected 用户签发受限令牌
"""
# 1. 调用微信 code2Session
try:
@@ -125,12 +137,12 @@ async def wx_login(body: WxLoginRequest):
row = cur.fetchone()
if row is None:
# 新用户:创建 pending 记录
# 新用户:创建 new 记录(尚未提交申请)
try:
cur.execute(
"""
INSERT INTO auth.users (wx_openid, wx_union_id, status)
VALUES (%s, %s, 'pending')
VALUES (%s, %s, 'new')
RETURNING id, status
""",
(openid, unionid),
@@ -166,7 +178,7 @@ async def wx_login(body: WxLoginRequest):
# approved 但无 site 绑定(异常边界),签发受限令牌
tokens = create_limited_token_pair(user_id)
else:
# pending / rejected → 受限令牌
# new / pending / rejected → 受限令牌
tokens = create_limited_token_pair(user_id)
finally:
@@ -415,3 +427,333 @@ async def refresh_token(body: RefreshTokenRequest):
user_status=user_status,
user_id=user_id,
)
# ── POST /api/xcx/dev-login仅开发模式 ─────────────────
if config.WX_DEV_MODE:
@router.post("/dev-login", response_model=WxLoginResponse)
async def dev_login(body: DevLoginRequest):
"""
开发模式 mock 登录。
直接根据 openid 查找/创建用户,跳过微信 code2Session。
- 已有用户status 参数为空时保留当前状态,非空时覆盖
- 新用户status 参数为空时默认 new非空时使用指定值
仅在 WX_DEV_MODE=true 时注册。
"""
openid = body.openid
target_status = body.status # 可能为 None
conn = get_connection()
try:
with conn.cursor() as cur:
# 查找已有用户
cur.execute(
"SELECT id, status FROM auth.users WHERE wx_openid = %s",
(openid,),
)
row = cur.fetchone()
if row is None:
# 新用户:使用指定状态或默认 new
init_status = target_status or "new"
cur.execute(
"""
INSERT INTO auth.users (wx_openid, status)
VALUES (%s, %s)
RETURNING id, status
""",
(openid, init_status),
)
row = cur.fetchone()
conn.commit()
else:
# 已有用户:仅在显式传入 status 时覆盖
if target_status is not None:
user_id_existing = row[0]
cur.execute(
"UPDATE auth.users SET status = %s, updated_at = NOW() WHERE id = %s",
(target_status, user_id_existing),
)
conn.commit()
row = (user_id_existing, target_status)
user_id, user_status = row
# 签发令牌(逻辑与正常登录一致)
if user_status == "approved":
default_site_id = _get_user_default_site(conn, user_id)
if default_site_id is not None:
roles = _get_user_roles_at_site(conn, user_id, default_site_id)
tokens = create_token_pair(user_id, default_site_id, roles=roles)
else:
tokens = create_limited_token_pair(user_id)
else:
tokens = create_limited_token_pair(user_id)
finally:
conn.close()
return WxLoginResponse(
access_token=tokens["access_token"],
refresh_token=tokens["refresh_token"],
token_type=tokens["token_type"],
user_status=user_status,
user_id=user_id,
)
# ── GET /api/xcx/dev-context仅开发模式 ────────────────
@router.get("/dev-context", response_model=DevContextResponse)
async def dev_context(
user: CurrentUser = Depends(get_current_user_or_limited),
):
"""
返回当前用户的完整调试上下文。
包含:用户信息、当前门店、角色、权限、人员绑定、所有关联门店。
允许受限令牌访问(返回基础信息,门店/角色/权限为空)。
"""
conn = get_connection()
try:
with conn.cursor() as cur:
# 用户基本信息
cur.execute(
"SELECT wx_openid, status, nickname FROM auth.users WHERE id = %s",
(user.user_id,),
)
u_row = cur.fetchone()
if u_row is None:
raise HTTPException(status_code=404, detail="用户不存在")
openid, u_status, nickname = u_row
# 当前门店名称
site_name = None
if user.site_id:
cur.execute(
"SELECT site_name FROM auth.site_code_mapping WHERE site_id = %s",
(user.site_id,),
)
sn_row = cur.fetchone()
site_name = sn_row[0] if sn_row else None
# 当前门店下的角色
roles = _get_user_roles_at_site(conn, user.user_id, user.site_id) if user.site_id else []
# 当前门店下的权限
permissions = await get_user_permissions(user.user_id, user.site_id) if user.site_id else []
# 人员绑定
binding = None
if user.site_id:
cur.execute(
"""
SELECT assistant_id, staff_id, binding_type
FROM auth.user_assistant_binding
WHERE user_id = %s AND site_id = %s
LIMIT 1
""",
(user.user_id, user.site_id),
)
b_row = cur.fetchone()
if b_row:
binding = {
"assistant_id": b_row[0],
"staff_id": b_row[1],
"binding_type": b_row[2],
}
# 所有关联门店
cur.execute(
"""
SELECT usr.site_id,
COALESCE(scm.site_name, '') AS site_name,
r.code, r.name
FROM auth.user_site_roles usr
JOIN auth.roles r ON usr.role_id = r.id
LEFT JOIN auth.site_code_mapping scm ON usr.site_id = scm.site_id
WHERE usr.user_id = %s
ORDER BY usr.site_id, r.code
""",
(user.user_id,),
)
site_rows = cur.fetchall()
finally:
conn.close()
sites_map: dict[int, dict] = {}
for sid, sname, rcode, rname in site_rows:
if sid not in sites_map:
sites_map[sid] = {"site_id": sid, "site_name": sname, "roles": []}
sites_map[sid]["roles"].append({"code": rcode, "name": rname})
return DevContextResponse(
user_id=user.user_id,
openid=openid,
status=u_status,
nickname=nickname,
site_id=user.site_id,
site_name=site_name,
roles=roles,
permissions=permissions,
binding=binding,
all_sites=list(sites_map.values()),
)
# ── POST /api/xcx/dev-switch-role仅开发模式 ───────────
@router.post("/dev-switch-role", response_model=WxLoginResponse)
async def dev_switch_role(
body: DevSwitchRoleRequest,
user: CurrentUser = Depends(get_current_user),
):
"""
切换当前用户在当前门店下的角色。
删除旧角色绑定,插入新角色绑定,重签 token。
"""
valid_roles = ("coach", "staff", "site_admin", "tenant_admin")
if body.role_code not in valid_roles:
raise HTTPException(
status_code=400,
detail=f"无效角色,可选: {', '.join(valid_roles)}",
)
conn = get_connection()
try:
with conn.cursor() as cur:
# 查询目标角色 ID
cur.execute(
"SELECT id FROM auth.roles WHERE code = %s",
(body.role_code,),
)
role_row = cur.fetchone()
if role_row is None:
raise HTTPException(status_code=400, detail=f"角色 {body.role_code} 不存在")
role_id = role_row[0]
# 删除当前门店下的所有角色
cur.execute(
"DELETE FROM auth.user_site_roles WHERE user_id = %s AND site_id = %s",
(user.user_id, user.site_id),
)
# 插入新角色
cur.execute(
"""
INSERT INTO auth.user_site_roles (user_id, site_id, role_id)
VALUES (%s, %s, %s)
ON CONFLICT (user_id, site_id, role_id) DO NOTHING
""",
(user.user_id, user.site_id, role_id),
)
conn.commit()
# 重签 token
roles = _get_user_roles_at_site(conn, user.user_id, user.site_id)
cur.execute("SELECT status FROM auth.users WHERE id = %s", (user.user_id,))
u_status = cur.fetchone()[0]
finally:
conn.close()
tokens = create_token_pair(user.user_id, user.site_id, roles=roles)
return WxLoginResponse(
access_token=tokens["access_token"],
refresh_token=tokens["refresh_token"],
token_type=tokens["token_type"],
user_status=u_status,
user_id=user.user_id,
)
# ── POST /api/xcx/dev-switch-status仅开发模式 ─────────
@router.post("/dev-switch-status", response_model=WxLoginResponse)
async def dev_switch_status(
body: DevSwitchStatusRequest,
user: CurrentUser = Depends(get_current_user_or_limited),
):
"""
切换当前用户状态,重签 token。
允许受限令牌访问pending 用户也需要能切换状态)。
"""
valid_statuses = ("new", "pending", "approved", "rejected", "disabled")
if body.status not in valid_statuses:
raise HTTPException(
status_code=400,
detail=f"无效状态,可选: {', '.join(valid_statuses)}",
)
conn = get_connection()
try:
with conn.cursor() as cur:
cur.execute(
"UPDATE auth.users SET status = %s, updated_at = NOW() WHERE id = %s",
(body.status, user.user_id),
)
conn.commit()
# 根据新状态签发对应令牌
if body.status == "approved":
default_site_id = _get_user_default_site(conn, user.user_id)
if default_site_id is not None:
roles = _get_user_roles_at_site(conn, user.user_id, default_site_id)
tokens = create_token_pair(user.user_id, default_site_id, roles=roles)
else:
tokens = create_limited_token_pair(user.user_id)
else:
tokens = create_limited_token_pair(user.user_id)
finally:
conn.close()
return WxLoginResponse(
access_token=tokens["access_token"],
refresh_token=tokens["refresh_token"],
token_type=tokens["token_type"],
user_status=body.status,
user_id=user.user_id,
)
# ── POST /api/xcx/dev-switch-binding仅开发模式 ────────
@router.post("/dev-switch-binding")
async def dev_switch_binding(
body: DevSwitchBindingRequest,
user: CurrentUser = Depends(get_current_user),
):
"""
切换当前用户在当前门店下的人员绑定。
删除旧绑定,插入新绑定。
"""
valid_types = ("assistant", "staff", "manager")
if body.binding_type not in valid_types:
raise HTTPException(
status_code=400,
detail=f"无效绑定类型,可选: {', '.join(valid_types)}",
)
conn = get_connection()
try:
with conn.cursor() as cur:
# 删除当前门店下的旧绑定
cur.execute(
"DELETE FROM auth.user_assistant_binding WHERE user_id = %s AND site_id = %s",
(user.user_id, user.site_id),
)
# 插入新绑定
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.user_id, user.site_id, body.assistant_id, body.staff_id, body.binding_type),
)
conn.commit()
finally:
conn.close()
return {"ok": True, "binding_type": body.binding_type}