1、后台/teams需要可以设置管理人,后台只给这个管理人分配积分,然后前台管理员自己分配积分
2、前台判断用户是管理人,左下角显示团队管理 3、团队管理可以看到团队的人员、分配人员积分、人员的积分情况的功能 4、团队管理还可以邀请用户,比如生成个链接,未注册需要注册绑定团队,已注册用户访问弹窗是否加入这个团队,然后都需要管理同意才可以加入团队
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
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.models.team import Team
|
||||
from app.models.team_invitation import TeamInvitation
|
||||
from app.models.team_join_request import TeamJoinRequest
|
||||
from app.models.user import User
|
||||
from app.utils.id_gen import generate_id
|
||||
import secrets
|
||||
|
||||
|
||||
def _generate_invite_code() -> str:
|
||||
"""生成唯一邀请码。"""
|
||||
return secrets.token_urlsafe(12)
|
||||
|
||||
|
||||
async def _get_team_or_404(db: AsyncSession, team_id: str) -> Team:
|
||||
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="团队不存在")
|
||||
return team
|
||||
|
||||
|
||||
async def _assert_is_manager(db: AsyncSession, user_id: str, team_id: str) -> None:
|
||||
team = await _get_team_or_404(db, team_id)
|
||||
if team.manager_id != user_id:
|
||||
raise HTTPException(status_code=403, detail="只有团队管理人才能操作")
|
||||
|
||||
|
||||
async def create_invitation(
|
||||
db: AsyncSession,
|
||||
team_id: str,
|
||||
created_by: str,
|
||||
max_uses: int | None = None,
|
||||
expires_at: datetime | None = None,
|
||||
) -> TeamInvitation:
|
||||
"""创建邀请码(仅团队管理人)。"""
|
||||
await _assert_is_manager(db, created_by, team_id)
|
||||
code = _generate_invite_code()
|
||||
invitation = TeamInvitation(
|
||||
id=generate_id(),
|
||||
team_id=team_id,
|
||||
code=code,
|
||||
created_by=created_by,
|
||||
status="active",
|
||||
max_uses=max_uses,
|
||||
expires_at=expires_at,
|
||||
)
|
||||
db.add(invitation)
|
||||
await db.flush()
|
||||
return invitation
|
||||
|
||||
|
||||
async def get_invitation_by_code(db: AsyncSession, code: str) -> TeamInvitation | None:
|
||||
"""根据邀请码获取邀请记录(验证有效)。"""
|
||||
result = await db.execute(
|
||||
select(TeamInvitation).where(
|
||||
TeamInvitation.code == code,
|
||||
TeamInvitation.deleted_at.is_(None),
|
||||
).limit(1)
|
||||
)
|
||||
invitation = result.scalar_one_or_none()
|
||||
if not invitation or invitation.status != "active":
|
||||
return None
|
||||
if invitation.expires_at and invitation.expires_at < datetime.now(timezone.utc):
|
||||
return None
|
||||
if invitation.max_uses and invitation.use_count >= invitation.max_uses:
|
||||
return None
|
||||
return invitation
|
||||
|
||||
|
||||
async def revoke_invitation(db: AsyncSession, invitation_id: str, revoked_by: str) -> None:
|
||||
"""撤销邀请码。"""
|
||||
result = await db.execute(
|
||||
select(TeamInvitation).where(
|
||||
TeamInvitation.id == invitation_id,
|
||||
TeamInvitation.deleted_at.is_(None),
|
||||
).limit(1)
|
||||
)
|
||||
invitation = result.scalar_one_or_none()
|
||||
if not invitation:
|
||||
raise HTTPException(status_code=404, detail="邀请码不存在")
|
||||
await _assert_is_manager(db, revoked_by, invitation.team_id)
|
||||
invitation.status = "revoked"
|
||||
await db.flush()
|
||||
|
||||
|
||||
async def create_join_request(
|
||||
db: AsyncSession,
|
||||
user_id: str,
|
||||
invitation_code: str,
|
||||
) -> TeamJoinRequest:
|
||||
"""通过邀请码创建加入申请。"""
|
||||
invitation = await get_invitation_by_code(db, invitation_code)
|
||||
if not invitation:
|
||||
raise HTTPException(status_code=400, detail="邀请码无效或已过期/已用完")
|
||||
|
||||
# 验证用户存在
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == user_id, User.deleted_at.is_(None)).limit(1)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查是否已在该团队
|
||||
if user.team_id == invitation.team_id:
|
||||
raise HTTPException(status_code=400, detail="您已在此团队中")
|
||||
|
||||
# 检查是否已有待审批的申请
|
||||
existing = await db.execute(
|
||||
select(TeamJoinRequest).where(
|
||||
TeamJoinRequest.user_id == user_id,
|
||||
TeamJoinRequest.team_id == invitation.team_id,
|
||||
TeamJoinRequest.status == "pending",
|
||||
).limit(1)
|
||||
)
|
||||
if existing.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="您已提交申请,请等待审批")
|
||||
|
||||
# 检查是否已有待审批的其他团队申请
|
||||
existing_any = await db.execute(
|
||||
select(TeamJoinRequest).where(
|
||||
TeamJoinRequest.user_id == user_id,
|
||||
TeamJoinRequest.status == "pending",
|
||||
).limit(1)
|
||||
)
|
||||
if existing_any.scalar_one_or_none():
|
||||
raise HTTPException(status_code=400, detail="您已有一个待审批的团队申请,请先等待处理")
|
||||
|
||||
request = TeamJoinRequest(
|
||||
id=generate_id(),
|
||||
team_id=invitation.team_id,
|
||||
user_id=user_id,
|
||||
invitation_id=invitation.id,
|
||||
status="pending",
|
||||
)
|
||||
db.add(request)
|
||||
|
||||
# 增加邀请码使用次数
|
||||
invitation.use_count = (invitation.use_count or 0) + 1
|
||||
if invitation.max_uses and invitation.use_count >= invitation.max_uses:
|
||||
invitation.status = "revoked"
|
||||
|
||||
await db.flush()
|
||||
return request
|
||||
|
||||
|
||||
async def handle_join_request(
|
||||
db: AsyncSession,
|
||||
request_id: str,
|
||||
manager_id: str,
|
||||
action: str,
|
||||
note: str | None = None,
|
||||
) -> None:
|
||||
"""审批/拒绝加入申请(仅团队管理人)。"""
|
||||
result = await db.execute(
|
||||
select(TeamJoinRequest).where(TeamJoinRequest.id == request_id).limit(1)
|
||||
)
|
||||
request = result.scalar_one_or_none()
|
||||
if not request:
|
||||
raise HTTPException(status_code=404, detail="申请不存在")
|
||||
if request.status != "pending":
|
||||
raise HTTPException(status_code=400, detail="该申请已处理")
|
||||
|
||||
await _assert_is_manager(db, manager_id, request.team_id)
|
||||
|
||||
if action == "approve":
|
||||
# 检查用户是否已在其他团队
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == request.user_id, User.deleted_at.is_(None)).limit(1)
|
||||
)
|
||||
user = user_result.scalar_one_or_none()
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
if user.team_id and user.team_id != request.team_id:
|
||||
raise HTTPException(status_code=400, detail="用户已在其他团队中,无法加入")
|
||||
|
||||
user.team_id = request.team_id
|
||||
request.status = "approved"
|
||||
elif action == "reject":
|
||||
request.status = "rejected"
|
||||
request.note = note
|
||||
else:
|
||||
raise HTTPException(status_code=400, detail="无效操作")
|
||||
|
||||
request.handled_by = manager_id
|
||||
await db.flush()
|
||||
|
||||
|
||||
async def get_pending_requests(db: AsyncSession, team_id: str) -> list[dict[str, Any]]:
|
||||
"""获取团队待审批的申请列表。"""
|
||||
result = await db.execute(
|
||||
select(TeamJoinRequest, User.username, User.phone)
|
||||
.join(User, User.id == TeamJoinRequest.user_id)
|
||||
.where(
|
||||
TeamJoinRequest.team_id == team_id,
|
||||
TeamJoinRequest.status == "pending",
|
||||
)
|
||||
.order_by(TeamJoinRequest.created_at.asc())
|
||||
)
|
||||
rows = result.all()
|
||||
return [
|
||||
{
|
||||
"id": req.id,
|
||||
"team_id": req.team_id,
|
||||
"user_id": req.user_id,
|
||||
"username": username,
|
||||
"phone": phone,
|
||||
"status": req.status,
|
||||
"note": req.note,
|
||||
"created_at": req.created_at,
|
||||
}
|
||||
for req, username, phone in rows
|
||||
]
|
||||
|
||||
|
||||
async def get_invitations_for_team(db: AsyncSession, team_id: str) -> list[TeamInvitation]:
|
||||
"""获取团队的所有邀请码。"""
|
||||
result = await db.execute(
|
||||
select(TeamInvitation).where(
|
||||
TeamInvitation.team_id == team_id,
|
||||
TeamInvitation.deleted_at.is_(None),
|
||||
).order_by(TeamInvitation.created_at.desc())
|
||||
)
|
||||
return list(result.scalars().all())
|
||||
@@ -0,0 +1,183 @@
|
||||
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.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.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()
|
||||
return team
|
||||
|
||||
# 验证用户存在、是前台用户、属于该团队
|
||||
user_result = await db.execute(
|
||||
select(User).where(User.id == user_id, User.deleted_at.is_(None)).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()
|
||||
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.deleted_at.is_(None),
|
||||
)
|
||||
)).scalar() or 0
|
||||
|
||||
# 列表
|
||||
result = await db.execute(
|
||||
select(User).where(
|
||||
User.user_type == UserType.FRONTEND.value,
|
||||
User.team_id == team_id,
|
||||
User.deleted_at.is_(None),
|
||||
)
|
||||
.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,
|
||||
description: str | None = None,
|
||||
) -> None:
|
||||
"""管理人从自己余额转积分给团队成员。"""
|
||||
if amount <= 0:
|
||||
raise HTTPException(status_code=400, description="转账积分必须大于0")
|
||||
|
||||
# 获取管理人
|
||||
manager_result = await db.execute(
|
||||
select(User).where(User.id == manager_id, User.deleted_at.is_(None)).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.deleted_at.is_(None)).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="只有团队管理人才能分配积分")
|
||||
|
||||
desc = description or "团队积分发放"
|
||||
|
||||
# 从管理人扣减
|
||||
await deduct_credits(
|
||||
db,
|
||||
manager_id,
|
||||
amount,
|
||||
f"分配给成员 {member.username}: {desc}",
|
||||
biz_key=f"mgr_xfer_out:{manager_id}:{target_member_id}:{generate_id()}",
|
||||
)
|
||||
# 给成员增加
|
||||
await add_credits(
|
||||
db,
|
||||
target_member_id,
|
||||
amount,
|
||||
f"来自团队管理人: {desc}",
|
||||
record_type="admin",
|
||||
biz_key=f"mgr_xfer_in:{manager_id}:{target_member_id}:{generate_id()}",
|
||||
)
|
||||
Reference in New Issue
Block a user