from __future__ import annotations from datetime import datetime, timedelta from sqlalchemy import delete, func, or_, select from sqlalchemy.ext.asyncio import AsyncSession from app.models.activity import Activity from app.models.match import Match from app.models.registration import Registration from app.models.user import User from app.schemas.activity import AdminActivityUpsertRequest from app.services.admin_service import get_system_config_value async def get_activity_by_id(session: AsyncSession, activity_id: int) -> Activity | None: result = await session.execute(select(Activity).where(Activity.id == activity_id)) return result.scalar_one_or_none() async def get_registration( session: AsyncSession, user_id: int, activity_id: int ) -> Registration | None: result = await session.execute( select(Registration).where( Registration.user_id == user_id, Registration.activity_id == activity_id, ) ) return result.scalar_one_or_none() async def _count_registered_by_gender( session: AsyncSession, activity_id: int, gender: int ) -> int: stmt = ( select(func.count(Registration.id)) .join(User, User.id == Registration.user_id) .where( Registration.activity_id == activity_id, Registration.status.in_([1, 2, 4]), User.gender == gender, ) ) result = await session.execute(stmt) return int(result.scalar() or 0) async def _registered_preview( session: AsyncSession, activity_id: int, current_user: User | None ) -> list[dict]: stmt = ( select(User.id, User.nickname, User.avatar_blur_url) .join(Registration, Registration.user_id == User.id) .where(Registration.activity_id == activity_id, Registration.status.in_([1, 2, 4])) .limit(16) ) if current_user and current_user.gender in {1, 2}: stmt = stmt.where(User.gender != current_user.gender) result = await session.execute(stmt) rows = result.all()[:8] return [ { "user_id": row.id, "nickname": row.nickname, "avatar_blur_url": row.avatar_blur_url, } for row in rows ] def _format_datetime(value: datetime | None) -> str | None: return value.isoformat() if value else None async def _get_activity_match_limit(session: AsyncSession) -> int: value = await get_system_config_value(session, "matches_per_activity_limit", 3) try: limit = int(value) except (TypeError, ValueError): return 3 return max(limit, 1) async def _count_user_activity_matches(session: AsyncSession, activity_id: int, user_id: int) -> int: stmt = select(func.count(Match.id)).where( Match.source_activity_id == activity_id, or_(Match.user_a_id == user_id, Match.user_b_id == user_id), ) result = await session.execute(stmt) return int(result.scalar() or 0) async def _build_activity_item( session: AsyncSession, activity: Activity, current_user: User | None ) -> dict: registered_male = await _count_registered_by_gender(session, activity.id, 1) registered_female = await _count_registered_by_gender(session, activity.id, 2) registration = None if current_user is not None: registration = await get_registration(session, current_user.id, activity.id) is_registered = bool(registration and registration.status in {1, 2, 4}) can_register = await can_user_register(session, activity, current_user) match_limit = await _get_activity_match_limit(session) match_count = await _count_user_activity_matches(session, activity.id, current_user.id) if current_user else 0 return { "id": activity.id, "title": activity.title, "category": activity.category, "cover_image": activity.cover_image, "start_time": _format_datetime(activity.start_time), "end_time": _format_datetime(activity.end_time), "signup_deadline": _format_datetime(activity.signup_deadline), "location": activity.location, "status": activity.status, "capacity_male": activity.capacity_male, "capacity_female": activity.capacity_female, "registered_male": registered_male, "registered_female": registered_female, "is_registered": is_registered, "can_register": can_register, "match_limit": match_limit, "match_count": match_count, "match_remaining": max(match_limit - match_count, 0), } async def list_activities( session: AsyncSession, current_user: User | None, *, status: int | None, page: int, page_size: int, ) -> list[dict]: stmt = select(Activity).where(Activity.is_published == 1) if status is not None: stmt = stmt.where(Activity.status == status) stmt = stmt.order_by(Activity.start_time.asc()).offset((page - 1) * page_size).limit(page_size) result = await session.execute(stmt) activities = result.scalars().all() return [await _build_activity_item(session, activity, current_user) for activity in activities] async def list_my_activities(session: AsyncSession, current_user: User) -> list[dict]: stmt = ( select(Activity) .join(Registration, Registration.activity_id == Activity.id) .where( Registration.user_id == current_user.id, Registration.status.in_([1, 2, 4]), ) .order_by(Activity.start_time.asc()) ) result = await session.execute(stmt) activities = result.scalars().all() return [await _build_activity_item(session, activity, current_user) for activity in activities] async def get_activity_detail( session: AsyncSession, activity: Activity, current_user: User | None ) -> dict: data = await _build_activity_item(session, activity, current_user) data.update( { "description": activity.description, "require_audit": activity.require_audit, "registered_preview": await _registered_preview(session, activity.id, current_user), } ) return data async def can_user_register( session: AsyncSession, activity: Activity, current_user: User | None ) -> bool: if current_user is None: return False if activity.is_published != 1: return False if activity.signup_deadline and activity.signup_deadline <= datetime.now(): return False if activity.require_audit == 1 and current_user.audit_status != 2: return False if current_user.gender not in {1, 2}: return False existing = await get_registration(session, current_user.id, activity.id) if existing and existing.status in {1, 2, 4}: return True if current_user.gender == 1 and activity.capacity_male > 0: registered = await _count_registered_by_gender(session, activity.id, 1) if registered >= activity.capacity_male: return False if current_user.gender == 2 and activity.capacity_female > 0: registered = await _count_registered_by_gender(session, activity.id, 2) if registered >= activity.capacity_female: return False return True async def register_activity(session: AsyncSession, activity: Activity, current_user: User) -> Registration: if activity.is_published != 1: raise ValueError("活动不存在或未发布") if activity.signup_deadline and activity.signup_deadline <= datetime.now(): raise ValueError("报名已截止") if activity.require_audit == 1 and current_user.audit_status != 2: raise ValueError("请先完善资料并通过审核") if current_user.gender not in {1, 2}: raise ValueError("用户性别信息不完整,暂无法报名") existing = await get_registration(session, current_user.id, activity.id) if existing and existing.status in {1, 2, 4}: return existing if current_user.gender == 1 and activity.capacity_male > 0: registered = await _count_registered_by_gender(session, activity.id, 1) if registered >= activity.capacity_male: raise ValueError("男性名额已满") if current_user.gender == 2 and activity.capacity_female > 0: registered = await _count_registered_by_gender(session, activity.id, 2) if registered >= activity.capacity_female: raise ValueError("女性名额已满") if existing and existing.status == 3: existing.status = 1 session.add(existing) await session.commit() await session.refresh(existing) return existing registration = Registration(user_id=current_user.id, activity_id=activity.id, status=1) session.add(registration) await session.commit() await session.refresh(registration) return registration async def cancel_registration(session: AsyncSession, activity: Activity, current_user: User) -> Registration: registration = await get_registration(session, current_user.id, activity.id) if registration is None or registration.status == 3: raise ValueError("未找到有效报名记录") if activity.start_time <= datetime.now() + timedelta(hours=24): raise ValueError("活动开始前24小时内不可取消报名") registration.status = 3 session.add(registration) await session.commit() await session.refresh(registration) return registration async def list_admin_activities(session: AsyncSession) -> list[Activity]: result = await session.execute(select(Activity).order_by(Activity.start_time.desc())) return result.scalars().all() def _parse_datetime(value: str | None) -> datetime | None: if not value: return None return datetime.fromisoformat(value) async def create_activity( session: AsyncSession, payload: AdminActivityUpsertRequest, admin_id: int, ) -> Activity: activity = Activity( title=payload.title, description=payload.description, category=payload.category, location=payload.location, cover_image=payload.cover_image, start_time=_parse_datetime(payload.start_time), end_time=_parse_datetime(payload.end_time), signup_deadline=_parse_datetime(payload.signup_deadline), capacity_male=payload.capacity_male, capacity_female=payload.capacity_female, match_window_hours=payload.match_window_hours, require_audit=1 if payload.require_audit else 0, is_published=1 if payload.is_published else 0, created_by=admin_id, status=1 if payload.is_published else 0, ) session.add(activity) await session.commit() await session.refresh(activity) return activity async def update_activity( session: AsyncSession, activity: Activity, payload: AdminActivityUpsertRequest, ) -> Activity: if activity.status == 4: raise ValueError("已结束的活动不可修改") activity.title = payload.title activity.description = payload.description activity.category = payload.category activity.location = payload.location activity.cover_image = payload.cover_image activity.start_time = _parse_datetime(payload.start_time) activity.end_time = _parse_datetime(payload.end_time) activity.signup_deadline = _parse_datetime(payload.signup_deadline) activity.capacity_male = payload.capacity_male activity.capacity_female = payload.capacity_female activity.match_window_hours = payload.match_window_hours activity.require_audit = 1 if payload.require_audit else 0 activity.is_published = 1 if payload.is_published else 0 if activity.status == 0 and payload.is_published: activity.status = 1 session.add(activity) await session.commit() await session.refresh(activity) return activity async def toggle_publish_activity(session: AsyncSession, activity: Activity) -> Activity: activity.is_published = 0 if activity.is_published == 1 else 1 if activity.status == 0 and activity.is_published == 1: activity.status = 1 session.add(activity) await session.commit() await session.refresh(activity) return activity async def delete_activity(session: AsyncSession, activity: Activity) -> None: registration_count = await session.execute( select(func.count(Registration.id)).where(Registration.activity_id == activity.id) ) if int(registration_count.scalar() or 0) > 0: raise ValueError("已有报名记录的活动不可删除") await session.execute(delete(Activity).where(Activity.id == activity.id)) await session.commit() async def get_admin_activity_registration_detail(session: AsyncSession, activity: Activity) -> dict: stmt = ( select(Registration, User) .join(User, User.id == Registration.user_id) .where(Registration.activity_id == activity.id) .order_by(Registration.created_at.asc()) ) result = await session.execute(stmt) rows = result.all() registrations = [] registered_male = 0 registered_female = 0 for registration, user in rows: if registration.status in {1, 2, 4}: if user.gender == 1: registered_male += 1 elif user.gender == 2: registered_female += 1 registrations.append( { "registration_id": registration.id, "status": registration.status, "created_at": registration.created_at.isoformat() if registration.created_at else None, "checked_in_at": registration.checked_in_at.isoformat() if registration.checked_in_at else None, "user": { "id": user.id, "nickname": user.nickname, "real_name": user.real_name, "gender": user.gender, "city": user.city, "phone": None, "education": user.education, "job_company": user.job_company, "audit_status": user.audit_status, }, } ) return { "activity": { "id": activity.id, "title": activity.title, "location": activity.location, "start_time": activity.start_time.isoformat() if activity.start_time else None, "status": activity.status, "capacity_male": activity.capacity_male, "capacity_female": activity.capacity_female, "registered_male": registered_male, "registered_female": registered_female, }, "registrations": registrations, }