406 lines
14 KiB
Python
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,
|
|
}
|