Files
aiagent/backend/app/services/feishu_open_id_service.py
renjianbo f33bc461ff fix: resolve Feishu cross-app notification routing bug
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>
2026-05-06 00:36:40 +08:00

212 lines
7.3 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.
"""飞书多应用 open_id 管理 — 存储/查询各应用下用户的 open_idunion_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