276 lines
10 KiB
Python
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
|