Implement per-app open_id storage via user_feishu_open_ids table with union_id-based cross-app user identification. WS handlers now auto-capture open_id+union_id and resolve/associate user accounts. Schedule notifications route through the correct bot's open_id instead of always falling back to 苹果. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
212 lines
7.3 KiB
Python
212 lines
7.3 KiB
Python
"""飞书多应用 open_id 管理 — 存储/查询各应用下用户的 open_id,union_id 跨应用关联"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
from typing import Optional, Tuple
|
||
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.core.config import settings
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def get_app_id_for_agent(agent_id: str) -> str:
|
||
"""根据 agent_id 返回对应的飞书应用 app_id。默认返回主飞书应用(苹果)。"""
|
||
if settings.LINGXI_AGENT_ID and agent_id == settings.LINGXI_AGENT_ID:
|
||
return settings.LINGXI_APP_ID or ""
|
||
if settings.ORANGE_AGENT_ID and agent_id == settings.ORANGE_AGENT_ID:
|
||
return settings.ORANGE_APP_ID or ""
|
||
if settings.SUYAO_AGENT_ID and agent_id == settings.SUYAO_AGENT_ID:
|
||
return settings.SUYAO_APP_ID or ""
|
||
if settings.TIANTIAN_AGENT_ID and agent_id == settings.TIANTIAN_AGENT_ID:
|
||
return settings.TIANTIAN_APP_ID or ""
|
||
return settings.FEISHU_APP_ID or ""
|
||
|
||
|
||
def save_open_id(
|
||
db: Session,
|
||
app_id: str,
|
||
open_id: str,
|
||
union_id: Optional[str] = None,
|
||
user_id: Optional[str] = None,
|
||
) -> None:
|
||
"""保存或更新 (app_id, open_id) 记录。
|
||
|
||
如果已有同 app+open_id 的记录,更新 union_id 和 user_id;
|
||
否则新建记录。user_id 可以为空(首次捕获时可能未知)。
|
||
"""
|
||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||
|
||
try:
|
||
existing = (
|
||
db.query(UserFeishuOpenId)
|
||
.filter(
|
||
UserFeishuOpenId.app_id == app_id,
|
||
UserFeishuOpenId.open_id == open_id,
|
||
)
|
||
.first()
|
||
)
|
||
if existing:
|
||
changed = False
|
||
if union_id and existing.union_id != union_id:
|
||
existing.union_id = union_id
|
||
changed = True
|
||
if user_id and existing.user_id != user_id:
|
||
existing.user_id = user_id
|
||
changed = True
|
||
if changed:
|
||
db.commit()
|
||
else:
|
||
record = UserFeishuOpenId(
|
||
user_id=user_id,
|
||
app_id=app_id,
|
||
open_id=open_id,
|
||
union_id=union_id,
|
||
)
|
||
db.add(record)
|
||
db.commit()
|
||
except Exception as e:
|
||
logger.warning("保存飞书 open_id 失败: %s", e)
|
||
db.rollback()
|
||
|
||
|
||
def find_user_id_by_open_id(db: Session, app_id: str, open_id: str) -> Optional[str]:
|
||
"""通过 (app_id, open_id) 查找已关联的 user_id。"""
|
||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||
|
||
try:
|
||
record = (
|
||
db.query(UserFeishuOpenId)
|
||
.filter(
|
||
UserFeishuOpenId.app_id == app_id,
|
||
UserFeishuOpenId.open_id == open_id,
|
||
)
|
||
.first()
|
||
)
|
||
return record.user_id if record else None
|
||
except Exception as e:
|
||
logger.warning("查询 user_id 失败: %s", e)
|
||
return None
|
||
|
||
|
||
def find_user_id_by_union_id(db: Session, union_id: str) -> Optional[str]:
|
||
"""通过 union_id 查找已关联的 user_id(跨应用识别)。"""
|
||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||
|
||
try:
|
||
record = (
|
||
db.query(UserFeishuOpenId)
|
||
.filter(
|
||
UserFeishuOpenId.union_id == union_id,
|
||
UserFeishuOpenId.user_id.isnot(None),
|
||
)
|
||
.first()
|
||
)
|
||
return record.user_id if record else None
|
||
except Exception as e:
|
||
logger.warning("通过 union_id 查询 user_id 失败: %s", e)
|
||
return None
|
||
|
||
|
||
def get_open_id_for_app(db: Session, user_id: str, app_id: str) -> Optional[str]:
|
||
"""查询用户在某飞书应用下的 open_id。"""
|
||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||
|
||
try:
|
||
record = (
|
||
db.query(UserFeishuOpenId)
|
||
.filter(
|
||
UserFeishuOpenId.user_id == user_id,
|
||
UserFeishuOpenId.app_id == app_id,
|
||
)
|
||
.first()
|
||
)
|
||
return record.open_id if record else None
|
||
except Exception as e:
|
||
logger.warning("查询飞书 open_id 失败: %s", e)
|
||
return None
|
||
|
||
|
||
def link_open_id_to_user(
|
||
db: Session,
|
||
app_id: str,
|
||
open_id: str,
|
||
user_id: str,
|
||
union_id: Optional[str] = None,
|
||
) -> None:
|
||
"""将 (app_id, open_id) 关联到平台用户。已有记录则更新,否则创建。"""
|
||
save_open_id(db, app_id=app_id, open_id=open_id, union_id=union_id, user_id=user_id)
|
||
|
||
|
||
def resolve_user_and_save(
|
||
db: Session,
|
||
app_id: str,
|
||
open_id: str,
|
||
union_id: Optional[str] = None,
|
||
) -> Optional[str]:
|
||
"""WS 消息处理入口:自动解析用户身份并保存 open_id。
|
||
|
||
查找策略(按优先级):
|
||
1. 已有 (app_id, open_id) 记录中的 user_id
|
||
2. 通过 union_id 在 user_feishu_open_ids 中查找
|
||
3. 通过 open_id 匹配 User.feishu_open_id(苹果旧数据兼容)
|
||
4. 通过 union_id 匹配 User.feishu_union_id
|
||
|
||
找到用户后自动 link,否则只保存 (app_id, open_id, union_id)。
|
||
|
||
Returns:
|
||
找到的 user_id,未找到返回 None
|
||
"""
|
||
from app.models.user import User
|
||
|
||
# 1. 已有记录
|
||
existing_user_id = find_user_id_by_open_id(db, app_id, open_id)
|
||
if existing_user_id:
|
||
# 补充 union_id
|
||
if union_id:
|
||
save_open_id(db, app_id=app_id, open_id=open_id, union_id=union_id, user_id=existing_user_id)
|
||
return existing_user_id
|
||
|
||
# 2. 通过 union_id 查找
|
||
if union_id:
|
||
user_id_by_union = find_user_id_by_union_id(db, union_id)
|
||
if user_id_by_union:
|
||
link_open_id_to_user(db, app_id=app_id, open_id=open_id, user_id=user_id_by_union, union_id=union_id)
|
||
return user_id_by_union
|
||
|
||
# 3. 兼容旧 User.feishu_open_id(苹果的 open_id)
|
||
user = db.query(User).filter(User.feishu_open_id == open_id).first()
|
||
if user:
|
||
link_open_id_to_user(db, app_id=app_id, open_id=open_id, user_id=user.id, union_id=union_id)
|
||
return user.id
|
||
|
||
# 4. 有 union_id 但前面都没匹配到:尝试通过飞书 API 查找苹果 open_id 的 union_id
|
||
if union_id:
|
||
from app.services.feishu_app_service import lookup_union_id_by_open_id
|
||
|
||
users_with_apple = db.query(User).filter(User.feishu_open_id.isnot(None)).all()
|
||
for u in users_with_apple:
|
||
apple_union_id = lookup_union_id_by_open_id(u.feishu_open_id)
|
||
if apple_union_id and apple_union_id == union_id:
|
||
# 回填苹果记录的 union_id
|
||
from app.models.user_feishu_open_id import UserFeishuOpenId
|
||
apple_record = (
|
||
db.query(UserFeishuOpenId)
|
||
.filter(
|
||
UserFeishuOpenId.user_id == u.id,
|
||
UserFeishuOpenId.app_id == (settings.FEISHU_APP_ID or ""),
|
||
)
|
||
.first()
|
||
)
|
||
if apple_record:
|
||
apple_record.union_id = union_id
|
||
db.commit()
|
||
# 关联当前记录
|
||
link_open_id_to_user(db, app_id=app_id, open_id=open_id, user_id=u.id, union_id=union_id)
|
||
return u.id
|
||
|
||
# 5. 未找到,只保存记录(user_id 为空)
|
||
save_open_id(db, app_id=app_id, open_id=open_id, union_id=union_id)
|
||
return None
|