Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion backend/app/api/dingtalk.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,43 @@ async def process_dingtalk_message(
# P2P / single chat
conv_id = f"dingtalk_p2p_{sender_staff_id}"

# Fetch user detail from DingTalk corp API for cross-channel matching
extra_info: dict = {"unionid": sender_staff_id}
try:
cfg_r = await db.execute(
_select(ChannelConfig).where(
ChannelConfig.agent_id == agent_id,
ChannelConfig.channel_type == "dingtalk",
)
)
dt_config = cfg_r.scalar_one_or_none()
if dt_config and dt_config.app_id and dt_config.app_secret:
from app.services.dingtalk_service import get_dingtalk_user_detail
user_detail = await get_dingtalk_user_detail(
dt_config.app_id, dt_config.app_secret, sender_staff_id
)
if user_detail:
dt_mobile = user_detail.get("mobile", "")
dt_email = user_detail.get("email", "") or user_detail.get("org_email", "")
dt_unionid = user_detail.get("unionid", "")
dt_name = user_detail.get("name", "")
extra_info = {
"unionid": dt_unionid or sender_staff_id,
"name": dt_name,
"mobile": dt_mobile or None,
"email": dt_email or None,
"avatar_url": user_detail.get("avatar", ""),
}
except Exception as e:
logger.warning(f"[DingTalk] Failed to fetch user detail for {sender_staff_id}: {e}")

# Resolve channel user via unified service (uses OrgMember + SSO patterns)
platform_user = await channel_user_service.resolve_channel_user(
db=db,
agent=agent_obj,
channel_type="dingtalk",
external_user_id=sender_staff_id,
extra_info={"unionid": sender_staff_id},
extra_info=extra_info,
)
platform_user_id = platform_user.id

Expand Down
44 changes: 43 additions & 1 deletion backend/app/services/channel_user_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ async def resolve_channel_user(
logger.debug(
f"[{channel_type}] Found user via linked OrgMember: {user.id}"
)
await self._enrich_user_from_extra_info(db, user, extra_info)
return user

# Step 4: Try to find User by email/mobile from extra_info
Expand All @@ -90,8 +91,9 @@ async def resolve_channel_user(
f"[{channel_type}] Matched user by mobile: {user.id}"
)

# If found User by email/mobile, link OrgMember if exists (only for org-sync channels)
# If found User by email/mobile, enrich and link OrgMember
if user:
await self._enrich_user_from_extra_info(db, user, extra_info)
if channel_type in ("feishu", "dingtalk", "wecom"):
if org_member and not org_member.user_id:
# Existing shell OrgMember not yet linked → link it
Expand Down Expand Up @@ -273,6 +275,46 @@ async def _find_existing_org_member_for_user(
result = await db.execute(query.limit(1))
return result.scalar_one_or_none()

async def _enrich_user_from_extra_info(
self,
db: AsyncSession,
user: User,
extra_info: dict[str, Any],
) -> None:
"""Enrich existing user with mobile/email/name from channel extra_info.

Only fills in fields that are currently empty on the user, to avoid
overwriting data the user may have set themselves.
"""
from app.models.user import Identity

updated = False
name = extra_info.get("name")
mobile = extra_info.get("mobile")
email = extra_info.get("email")
avatar = extra_info.get("avatar_url")

if name and not user.display_name:
user.display_name = name
updated = True
if avatar and not user.avatar_url:
user.avatar_url = avatar
updated = True

# Enrich Identity-level fields (phone, email) if available
if user.identity_id and (mobile or email):
identity = await db.get(Identity, user.identity_id)
if identity:
if mobile and not identity.phone:
identity.phone = mobile
updated = True
if email and not identity.email:
identity.email = email
updated = True

if updated:
await db.flush()

async def _create_channel_user(
self,
db: AsyncSession,
Expand Down
32 changes: 32 additions & 0 deletions backend/app/services/dingtalk_service.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,38 @@ async def get_dingtalk_access_token(app_id: str, app_secret: str) -> dict:
return {"errcode": -1, "errmsg": str(e)}


async def get_dingtalk_user_detail(app_id: str, app_secret: str, userid: str) -> dict | None:
"""Fetch user detail from DingTalk corp API by userid (staff_id).

Returns dict with mobile, email, org_email, unionid, name, etc.
Returns None on failure.

API: https://open.dingtalk.com/document/orgapp/query-user-details
"""
token_result = await get_dingtalk_access_token(app_id, app_secret)
access_token = token_result.get("access_token")
if not access_token:
return None

url = "https://oapi.dingtalk.com/topapi/v2/user/get"
async with httpx.AsyncClient(timeout=10) as client:
try:
resp = await client.post(
url,
params={"access_token": access_token},
json={"userid": userid},
)
data = resp.json()
if data.get("errcode") == 0:
return data.get("result", {})
else:
logger.warning(f"[DingTalk] user/get failed for {userid}: {data.get('errmsg')}")
return None
except Exception as e:
logger.warning(f"[DingTalk] user/get error for {userid}: {e}")
return None


async def send_dingtalk_v1_robot_oto_message(
app_id: str,
app_secret: str,
Expand Down