xiangqinxiaochengxu/backend/app/services/activity_service.py
2026-04-17 10:49:14 +08:00

406 lines
14 KiB
Python

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,
}