feat(api): /api/sessions 账号会话列表(好友+群,今日消息数+最近联系时间,全本地聚合)
Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
"""GET /api/sessions — 账号会话列表(好友+群,今日消息数+最近联系时间),全本地 message 聚合。
|
||||
|
||||
镜像 staff_chat_stats 聚合模式(GROUP BY + case 计数 + Python 侧合并昵称/头像)。
|
||||
today_start 钉死 Asia/Shanghai(UTC+8)当日 0 点,不依赖服务器时区。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Annotated
|
||||
from zoneinfo import ZoneInfo
|
||||
|
||||
import sqlalchemy as sa
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.api.deps import get_session
|
||||
from app.api.schemas import SessionListOut, SessionOut
|
||||
from app.db.models import ChatGroup, Customer, Message
|
||||
|
||||
router = APIRouter(prefix="/api", tags=["sessions"])
|
||||
|
||||
SessionDep = Annotated[AsyncSession, Depends(get_session)]
|
||||
_TZ = ZoneInfo("Asia/Shanghai")
|
||||
|
||||
|
||||
def _today_start_ms() -> int:
|
||||
"""Asia/Shanghai 当日 0 点 ms-epoch(部署无关、测试确定性)。"""
|
||||
midnight = datetime.now(_TZ).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
return int(midnight.timestamp() * 1000)
|
||||
|
||||
|
||||
def _base(msg, owner: str, channel: str):
|
||||
return [
|
||||
msg.channel == channel,
|
||||
msg.owner_account_id == owner,
|
||||
msg.timestamp.is_not(None),
|
||||
]
|
||||
|
||||
|
||||
async def _friend_rows(session: AsyncSession, owner: str, channel: str, today_start: int):
|
||||
stmt = (
|
||||
sa.select(
|
||||
Message.talker_id,
|
||||
sa.func.max(Message.timestamp).label("last_chat_time"),
|
||||
sa.func.sum(sa.case((Message.timestamp >= today_start, 1), else_=0)).label("today_count"),
|
||||
)
|
||||
.where(*_base(Message, owner, channel), sa.or_(Message.room_id.is_(None), Message.room_id == ""), Message.talker_id.is_not(None))
|
||||
.group_by(Message.talker_id)
|
||||
)
|
||||
return (await session.execute(stmt)).all()
|
||||
|
||||
|
||||
async def _group_rows(session: AsyncSession, owner: str, channel: str, today_start: int):
|
||||
stmt = (
|
||||
sa.select(
|
||||
Message.room_id,
|
||||
sa.func.max(Message.timestamp).label("last_chat_time"),
|
||||
sa.func.sum(sa.case((Message.timestamp >= today_start, 1), else_=0)).label("today_count"),
|
||||
)
|
||||
.where(*_base(Message, owner, channel), Message.room_id.is_not(None), Message.room_id != "")
|
||||
.group_by(Message.room_id)
|
||||
)
|
||||
return (await session.execute(stmt)).all()
|
||||
|
||||
|
||||
async def _meta(session: AsyncSession, owner: str, channel: str, friend_ids: list[str], room_ids: list[str]):
|
||||
friends: dict[str, tuple[str, str | None]] = {}
|
||||
if friend_ids:
|
||||
for fid, remark, nick, real, avatar in (await session.execute(
|
||||
sa.select(Customer.friend_id, Customer.friend_remark, Customer.friend_nickname, Customer.real_name, Customer.head_image_url)
|
||||
.where(Customer.channel == channel, Customer.owner_account_id == owner, Customer.friend_id.in_(friend_ids))
|
||||
)).all():
|
||||
friends[fid] = (remark or nick or real or fid, avatar)
|
||||
groups: dict[str, tuple[str, str | None]] = {}
|
||||
if room_ids:
|
||||
for rid, name, avatar in (await session.execute(
|
||||
sa.select(ChatGroup.room_id, ChatGroup.room_name, ChatGroup.head_image_url)
|
||||
.where(ChatGroup.channel == channel, ChatGroup.owner_account_id == owner, ChatGroup.room_id.in_(room_ids))
|
||||
)).all():
|
||||
groups[rid] = (name or rid, avatar)
|
||||
return friends, groups
|
||||
|
||||
|
||||
@router.get("/sessions")
|
||||
async def list_sessions(
|
||||
session: SessionDep,
|
||||
owner_account_id: Annotated[str, Query(min_length=1)],
|
||||
channel: Annotated[str, Query()] = "wechat",
|
||||
) -> SessionListOut:
|
||||
"""账号会话列表:好友(talker_id,room_id 空)+ 群(room_id),按 last_chat_time desc。"""
|
||||
today_start = _today_start_ms()
|
||||
f_rows = await _friend_rows(session, owner_account_id, channel, today_start)
|
||||
g_rows = await _group_rows(session, owner_account_id, channel, today_start)
|
||||
fnames, gnames = await _meta(
|
||||
session, owner_account_id, channel, [r.talker_id for r in f_rows], [r.room_id for r in g_rows]
|
||||
)
|
||||
sessions: list[SessionOut] = []
|
||||
for r in f_rows:
|
||||
name, avatar = fnames.get(r.talker_id, (r.talker_id, None))
|
||||
sessions.append(SessionOut(type="friend", talker_id=r.talker_id, name=name, avatar=avatar,
|
||||
today_count=int(r.today_count or 0), last_chat_time=r.last_chat_time))
|
||||
for r in g_rows:
|
||||
name, avatar = gnames.get(r.room_id, (r.room_id, None))
|
||||
sessions.append(SessionOut(type="group", room_id=r.room_id, name=name, avatar=avatar,
|
||||
today_count=int(r.today_count or 0), last_chat_time=r.last_chat_time))
|
||||
sessions.sort(key=lambda s: (s.last_chat_time is None, -(s.last_chat_time or 0)))
|
||||
return SessionListOut(owner_account_id=owner_account_id, channel=channel, today_start=today_start, sessions=sessions)
|
||||
Reference in New Issue
Block a user