from __future__ import annotations from datetime import datetime, timezone from typing import Any from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.enums.credit_record import ( CreditRecordBillingScene, CreditRecordChargeKind, CreditRecordSubject, CreditRecordSourceModule, ) from app.enums.team import TeamStatus from app.enums.user import UserType from app.models.team import Team from app.models.user import User from app.services.credits import add_credits, deduct_credits from app.services.credit_record_meta_service import CreditRecordMeta from app.utils.id_gen import generate_id async def set_team_manager(db: AsyncSession, team_id: str, user_id: str | None) -> Team: """设置团队管理人。user_id 为 None 表示取消管理人。""" result = await db.execute( select(Team).where(Team.id == team_id, Team.deleted_at.is_(None)).limit(1) ) team = result.scalar_one_or_none() if not team: raise HTTPException(status_code=404, detail="团队不存在") if user_id is None: team.manager_id = None await db.flush() await db.refresh(team) return team # 验证用户存在、是前台用户、属于该团队 user_result = await db.execute( select(User).where(User.id == user_id, User.is_active.is_(True)).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="用户不属于该团队,请先将其加入团队") team.manager_id = user_id await db.flush() await db.refresh(team) 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) ) manager_id = result.scalar_one_or_none() return manager_id == 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="团队不存在") # 总数 from sqlalchemy import func 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()) return { "items": [ { "id": m.id, "username": m.username, "phone": m.phone, "credits": m.credits, "is_active": m.is_active, "joined_at": m.created_at, } for m in members ], "total": total, } async def transfer_credits_to_member( db: AsyncSession, manager_id: str, target_member_id: str, amount: float, direction: str = "increase", # "increase" 管理人→成员; "decrease" 成员扣减 description: str | None = None, ) -> None: """管理人为团队成员增加或扣减积分。 direction: - "increase": 管理人从自己余额转积分给成员(管理人减少,成员增加) - "decrease": 从成员扣积分回到管理人(成员减少,管理人增加) """ if amount <= 0: raise HTTPException(status_code=400, description="积分数量必须大于0") if direction not in ("increase", "decrease"): raise HTTPException(status_code=400, description="无效操作方向") # 获取管理人 manager_result = await db.execute( select(User).where(User.id == manager_id, User.is_active.is_(True)).limit(1) ) manager = manager_result.scalar_one_or_none() if not manager: raise HTTPException(status_code=404, detail="管理员不存在") # 获取目标成员 member_result = await db.execute( select(User).where(User.id == target_member_id, User.is_active.is_(True)).limit(1) ) member = member_result.scalar_one_or_none() if not member: raise HTTPException(status_code=404, detail="成员不存在") # 验证管理人是该团队的管理人且目标是同团队成员 if not manager.team_id: raise HTTPException(status_code=400, detail="您不在任何团队中") if member.team_id != manager.team_id: raise HTTPException(status_code=400, detail="只能操作同团队成员") team_result = await db.execute( select(Team).where(Team.id == manager.team_id, Team.deleted_at.is_(None)).limit(1) ) team = team_result.scalar_one_or_none() if not team or team.manager_id != manager_id: raise HTTPException(status_code=403, detail="只有团队管理人才能分配积分") # 禁止管理人给自己转积分 if target_member_id == manager_id: raise HTTPException(status_code=400, detail="不能给自己调整积分") desc = description or ("团队积分发放" if direction == "increase" else "团队积分扣减") xfer_id = generate_id() # 构建团队内部转账的 meta,确保 team_id_snapshot 等字段被正确设置 def _build_team_transfer_meta(uid: str) -> CreditRecordMeta: meta = CreditRecordMeta( owner_type="team_internal_transfer", owner_id=xfer_id, charge_kind=CreditRecordChargeKind.TEAM_INTERNAL.value, credit_subject=CreditRecordSubject.TEAM_INTERNAL.value, source_module=CreditRecordSourceModule.TEAM.value, billing_scene=CreditRecordBillingScene.TEAM_INTERNAL_TRANSFER.value, ) return meta if direction == "increase": # 管理人扣减 await deduct_credits( db, manager_id, amount, f"分配给成员 {member.username}: {desc}", record_type="team_internal", biz_key=f"mgr_xfer_out:{manager_id}:{target_member_id}:{xfer_id}", record_meta=_build_team_transfer_meta(manager_id), ) # 成员增加 await add_credits( db, target_member_id, amount, f"来自团队管理人: {desc}", record_type="team_internal", biz_key=f"mgr_xfer_in:{manager_id}:{target_member_id}:{xfer_id}", record_meta=_build_team_transfer_meta(target_member_id), ) else: # 成员扣减 await deduct_credits( db, target_member_id, amount, f"扣减给团队管理人: {desc}", record_type="team_internal", biz_key=f"mgr_deduct_out:{manager_id}:{target_member_id}:{xfer_id}", record_meta=_build_team_transfer_meta(target_member_id), ) # 管理人增加 await add_credits( db, manager_id, amount, f"来自成员 {member.username}: {desc}", record_type="team_internal", biz_key=f"mgr_deduct_in:{manager_id}:{target_member_id}:{xfer_id}", record_meta=_build_team_transfer_meta(manager_id), )