Files
2026-08-14 15:23:46 +08:00

276 lines
10 KiB
Python

from __future__ import annotations
from typing import Any
from fastapi import HTTPException
from sqlalchemy import and_, func, or_, select
from sqlalchemy.ext.asyncio import AsyncSession
from app.enums.credit_product import CreditProductType
from app.enums.credit_subscription import CreditSubscriptionStatus
from app.enums.team import TEAM_STATUS_LABELS, TeamStatus
from app.enums.user import UserType
from app.models.credit.subscription import UserCreditSubscription
from app.models.payment_order import PaymentOrder
from app.models.team import Team
from app.models.team_manager_history import TeamManagerHistory
from app.models.user import User
from app.services.credit.locking import acquire_team_business_lock
from app.services.credit.query_service import attach_credit_snapshot, get_user_credit_summary_map
from app.services.credit.utils import utc_now
from app.services.operation_log_service import log_operation_event
from app.utils.id_gen import generate_id
async def _assert_transfer_allowed(db: AsyncSession, team: Team) -> None:
if team.status != TeamStatus.ACTIVE.value:
raise HTTPException(status_code=409, detail="团队已禁用,当前仅允许查看,不能更换队长")
checked_at = utc_now()
active_subscription = (await db.execute(
select(UserCreditSubscription.id)
.where(
UserCreditSubscription.team_id == team.id,
UserCreditSubscription.product_type_snapshot == CreditProductType.TEAM_SUBSCRIPTION.value,
UserCreditSubscription.status == CreditSubscriptionStatus.ACTIVE.value,
UserCreditSubscription.start_at <= checked_at,
UserCreditSubscription.expires_at > checked_at,
)
.limit(1)
)).scalar_one_or_none()
if active_subscription:
raise HTTPException(status_code=409, detail="团队仍有有效团队订阅,全部订阅结束后才能更换队长")
incomplete_order = (await db.execute(
select(PaymentOrder.id)
.where(
PaymentOrder.team_id_snapshot == team.id,
PaymentOrder.product_type == CreditProductType.TEAM_SUBSCRIPTION.value,
or_(
PaymentOrder.status == "pending",
and_(PaymentOrder.status == "paid", PaymentOrder.fulfillment_status != "fulfilled"),
),
)
.limit(1)
)).scalar_one_or_none()
if incomplete_order:
raise HTTPException(status_code=409, detail="团队仍有待支付或待履约团队订阅订单,暂不能更换队长")
async def set_team_manager(db: AsyncSession, team_id: str, user_id: str) -> Team:
"""更换团队队长;仅允许转给当前团队成员。"""
await acquire_team_business_lock(db, team_id)
result = await db.execute(
select(Team)
.where(Team.id == team_id, Team.deleted_at.is_(None))
.with_for_update()
.limit(1)
)
team = result.scalar_one_or_none()
if not team:
raise HTTPException(status_code=404, detail="团队不存在")
if team.manager_id == user_id:
return team
await _assert_transfer_allowed(db, team)
user_result = await db.execute(
select(User).where(User.id == user_id, User.is_active.is_(True)).with_for_update().limit(1)
)
user = user_result.scalar_one_or_none()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.user_type != UserType.FRONTEND.value:
raise HTTPException(status_code=400, detail="仅前台用户可设为团队队长")
if user.team_id != team_id:
raise HTTPException(status_code=400, detail="新队长必须是当前团队成员")
checked_at = utc_now()
if team.manager_id:
current_history_result = await db.execute(
select(TeamManagerHistory)
.where(
TeamManagerHistory.team_id == team.id,
TeamManagerHistory.manager_user_id == team.manager_id,
TeamManagerHistory.ended_at.is_(None),
)
.order_by(TeamManagerHistory.started_at.desc(), TeamManagerHistory.id.desc())
.with_for_update()
.limit(1)
)
current_history = current_history_result.scalar_one_or_none()
if current_history:
current_history.ended_at = checked_at
previous_manager_id = team.manager_id
db.add(
TeamManagerHistory(
id=generate_id(),
team_id=team.id,
manager_user_id=user_id,
started_at=checked_at,
)
)
team.manager_id = user_id
await db.flush()
log_operation_event(
domain="team",
module="team_manager",
event_type="TEAM_MANAGER_CHANGED",
user_id=user_id,
message="团队队长更换成功",
detail={
"team_id": team.id,
"previous_manager_user_id": previous_manager_id,
"new_manager_user_id": user_id,
},
)
return team
async def is_team_manager(db: AsyncSession, user_id: str, team_id: str | None) -> bool:
if not team_id:
return False
result = await db.execute(
select(Team.manager_id).where(Team.id == team_id, Team.deleted_at.is_(None)).limit(1)
)
return result.scalar_one_or_none() == user_id
async def get_managed_team(db: AsyncSession, user_id: str) -> Team | None:
result = await db.execute(
select(Team).where(Team.manager_id == user_id, Team.deleted_at.is_(None)).limit(1)
)
return result.scalar_one_or_none()
async def get_team_members(
db: AsyncSession,
team_id: str,
*,
page: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
page = max(int(page or 1), 1)
page_size = min(max(int(page_size or 20), 1), 100)
team_result = await db.execute(
select(Team).where(Team.id == team_id, Team.deleted_at.is_(None)).limit(1)
)
if not team_result.scalar_one_or_none():
raise HTTPException(status_code=404, detail="团队不存在")
total = (await db.execute(
select(func.count(User.id)).where(
User.user_type == UserType.FRONTEND.value,
User.team_id == team_id,
User.is_active.is_(True),
)
)).scalar() or 0
result = await db.execute(
select(User)
.where(
User.user_type == UserType.FRONTEND.value,
User.team_id == team_id,
User.is_active.is_(True),
)
.order_by(User.created_at.asc())
.offset((page - 1) * page_size)
.limit(page_size)
)
members = list(result.scalars().all())
summary_map = await get_user_credit_summary_map(db, [item.id for item in members])
items = []
for member in members:
summary = summary_map.get(member.id)
credits = float(summary.available_credits if summary else 0)
attach_credit_snapshot(member, credits)
items.append(
{
"id": member.id,
"username": member.username,
"phone": member.phone,
"credits": credits,
"personal_credits": float(summary.personal_credits if summary else 0),
"team_available_credits": float(summary.team_available_credits if summary else 0),
"team_frozen_credits": float(summary.team_frozen_credits if summary else 0),
"is_active": member.is_active,
"joined_at": member.created_at,
}
)
return {"items": items, "total": int(total)}
async def get_manager_history(db: AsyncSession, team_id: str) -> list[dict[str, Any]]:
result = await db.execute(
select(TeamManagerHistory, User.username)
.join(User, User.id == TeamManagerHistory.manager_user_id)
.where(TeamManagerHistory.team_id == team_id)
.order_by(TeamManagerHistory.started_at.desc(), TeamManagerHistory.id.desc())
)
return [
{
"id": history.id,
"team_id": history.team_id,
"manager_user_id": history.manager_user_id,
"manager_name": username,
"started_at": history.started_at,
"ended_at": history.ended_at,
}
for history, username in result.all()
]
async def transfer_credits_to_member(*args, **kwargs) -> None:
raise HTTPException(status_code=409, detail="当前版本不支持团队积分转账,请使用团队订阅席位额度")
async def list_manager_access_teams(db: AsyncSession, user_id: str) -> list[dict[str, Any]]:
"""返回用户作为当前/历史队长可查看团队流水的团队,按当前团队优先、最近任期倒序。"""
history_result = await db.execute(
select(TeamManagerHistory.team_id, func.max(TeamManagerHistory.started_at).label("last_started_at"))
.where(TeamManagerHistory.manager_user_id == user_id)
.group_by(TeamManagerHistory.team_id)
)
history_rows = list(history_result.all())
team_ids = [row.team_id for row in history_rows]
current_result = await db.execute(
select(Team.id).where(
Team.manager_id == user_id,
Team.deleted_at.is_(None),
)
)
for team_id in current_result.scalars().all():
if team_id not in team_ids:
team_ids.append(team_id)
if not team_ids:
return []
teams_result = await db.execute(select(Team).where(Team.id.in_(team_ids)))
team_map = {item.id: item for item in teams_result.scalars().all()}
started_map = {row.team_id: row.last_started_at for row in history_rows}
items: list[dict[str, Any]] = []
for team_id in team_ids:
team = team_map.get(team_id)
if not team:
continue
is_current = team.deleted_at is None and team.manager_id == user_id
items.append(
{
"id": team.id,
"name": team.name,
"code": team.code,
"status": team.status,
"status_label": TEAM_STATUS_LABELS.get(team.status, "其他状态"),
"is_current_manager": is_current,
"last_managed_at": started_map.get(team.id),
"deleted": team.deleted_at is not None,
}
)
items.sort(
key=lambda item: (
0 if item["is_current_manager"] else 1,
-(item["last_managed_at"].timestamp() if item["last_managed_at"] else 0),
item["id"],
)
)
return items