Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
SECRET_KEY=bN3hZ6LbHG7nH9YXWULCr-crcS3GAaRELbNBdAyHBuiHH5TRctd0Zbd6OuLRHHa4Fbs
SENDER_PASSWORD=TXVU2unpCAE2EtEX
KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9
KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9
FERNET_SECRET_KEY=6WssEkvinI_YqwKXdokii2yI6iBiLO_Cjoyq0bBBC5o=
47 changes: 47 additions & 0 deletions alembic/versions/fd8714315ad3_优化user_group和group表.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
"""优化user_group和group表

Revision ID: fd8714315ad3
Revises: 004c4aa2b3f3
Create Date: 2025-05-23 13:09:52.425623

"""
from typing import Sequence, Union

from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects import mysql

# revision identifiers, used by Alembic.
revision: str = 'fd8714315ad3'
down_revision: Union[str, None] = '004c4aa2b3f3'
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None


def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('enter_application')
op.add_column('groups', sa.Column('avatar', sa.String(length=100), nullable=True))
op.add_column('user_group', sa.Column('level', sa.Integer(), nullable=True))
op.drop_column('user_group', 'is_admin')
# ### end Alembic commands ###


def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('user_group', sa.Column('is_admin', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True))
op.drop_column('user_group', 'level')
op.drop_column('groups', 'avatar')
op.create_table('enter_application',
sa.Column('user_id', mysql.INTEGER(), autoincrement=False, nullable=False),
sa.Column('group_id', mysql.INTEGER(), autoincrement=False, nullable=False),
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], name='enter_application_ibfk_1'),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='enter_application_ibfk_2'),
sa.PrimaryKeyConstraint('user_id', 'group_id'),
mysql_collate='utf8mb4_0900_ai_ci',
mysql_default_charset='utf8mb4',
mysql_engine='InnoDB'
)
# ### end Alembic commands ###
121 changes: 101 additions & 20 deletions app/api/v1/endpoints/group.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
from fastapi import APIRouter, Query, Body, UploadFile, File, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from cryptography.fernet import Fernet
import os
import uuid
from datetime import date, datetime
import json

from app.utils.get_db import get_db
from app.utils.auth import get_current_user
from app.curd.group import crud_create, crud_apply_to_enter, crud_get_applications, crud_reply_to_enter
from app.schemas.group import ApplyToEnter
from app.curd.group import crud_create, crud_gen_invite_code, crud_enter_group, crud_modify_basic_info, crud_modify_admin_list, crud_remove_member, crud_leave_group, crud_get_basic_info, crud_get_people_info, crud_get_my_level, crud_all_groups
from app.schemas.group import EnterGroup, LeaveGroup

router = APIRouter()

Expand All @@ -16,31 +20,108 @@ async def create(group_name: str = Query(...), group_desc: str = Query(...), gro
raise HTTPException(status_code=405, detail="Invalid group name, longer than 30")
if len(group_desc) > 200:
raise HTTPException(status_code=405, detail="Invalid group description, longer than 200")
group_id = await crud_create(user.get("id"), group_name, group_desc, db)
path = "/lhcos-data/group-avatar/default.png"
# 存储头像,保留扩展名
if group_avatar:
os.makedirs("/lhcos-data/group-avatar", exist_ok=True)
ext = os.path.splitext(group_avatar.filename)[1]
path = os.path.join("/lhcos-data/group-avatar", f"{group_id}{ext}")
path = f"/lhcos-data/group-avatar/{uuid.uuid4()}{ext}"
with open(path, "wb") as f:
content = await group_avatar.read()
f.write(content)
await crud_create(user.get("id"), group_name, group_desc, path, db)
return {"msg": "Group created successfully"}

@router.post("/applyToEnter", response_model=dict)
async def apply_to_enter(model: ApplyToEnter, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
@router.get("/genInviteCode", response_model=dict)
async def gen_invite_code(user_email: str = Query(...), group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
await crud_gen_invite_code(user_email, db)
today = date.today()
data = {
"email": user_email,
"group_id": group_id,
"date": today.isoformat()
}
json_data = json.dumps(data).encode()
fernet = Fernet(os.getenv("FERNET_SECRET_KEY"))
encrypted = fernet.encrypt(json_data)
return {"inviteCode": encrypted}

@router.post("/enterGroup", response_model=dict)
async def enter_group(inviteCode: EnterGroup, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
code = inviteCode.inviteCode
fernet = Fernet(os.getenv("FERNET_SECRET_KEY"))

decrypted = fernet.decrypt(code.encode())
data = json.loads(decrypted)

user_email = user.get("email")
invite_email = data["email"]
if user_email != invite_email:
raise HTTPException(status_code=405, detail="Not your invite code")

invite_date = datetime.strptime(data["date"], "%Y-%m-%d").date()
today = date.today()
if today > invite_date:
raise HTTPException(status_code=406, detail="Invite Code already expired")

await crud_enter_group(user.get("id"), data["group_id"], db)
return {"msg": "Enter thr group successfully"}

@router.post("/modifyBasicInfo", response_model=dict)
async def modify_basic_info(group_id: int = Query(...), group_name: str | None = Query(None), group_desc: str | None = Query(None), group_avatar: UploadFile | None = File(None), db: AsyncSession = Depends(get_db)):
if group_name and len(group_name) > 30:
raise HTTPException(status_code=405, detail="Invalid group name, longer than 30")
if group_desc and len(group_desc) > 200:
raise HTTPException(status_code=405, detail="Invalid group description, longer than 200")
new_path = None
if group_avatar:
os.makedirs("/lhcos-data/group-avatar", exist_ok=True)
# 存储新头像,保留扩展名
ext = os.path.splitext(group_avatar.filename)[1]
new_path = f"/lhcos-data/group-avatar/{uuid.uuid4()}{ext}"
with open(new_path, "wb") as f:
content = await group_avatar.read()
f.write(content)
old_path = await crud_modify_basic_info(db=db, id=group_id, name=group_name, desc=group_desc, avatar=new_path)
if group_avatar and old_path != "/lhcos-data/group-avatar/default.png":
os.remove(old_path)
return {"msg": "Basic info modified successfully"}

@router.post("/modifyAdminList", response_model=dict)
async def modify_admin_list(group_id: int = Body(...), user_id: int = Body(...), add_admin: bool = Body(...), db: AsyncSession = Depends(get_db)):
msg = await crud_modify_admin_list(group_id, user_id, add_admin, db)
return {"msg": msg}

@router.post("/removeMember", response_model=dict)
async def remove_member(group_id: int = Body(...), user_id: int = Body(...), db: AsyncSession = Depends(get_db)):
await crud_remove_member(group_id, user_id, db)
return {"msg": "Member removed successfully"}

@router.post("/leaveGroup", response_model=dict)
async def leave_group(model: LeaveGroup, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
group_id = model.group_id
user_id = user.get("id")
await crud_apply_to_enter(user_id, group_id, db)
return {"msg": "Application sent successfully"}

@router.get("/getApplications", response_model=dict)
async def get_applications(group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
users = await crud_get_applications(group_id, db)
return {"users": users}

@router.post("/replyToEnter", response_model=dict)
async def reply_to_enter(user_id: int = Body(...), group_id: int = Body(...), reply: int = Body(...), db: AsyncSession = Depends(get_db)):
if reply != 0 and reply != 1:
raise HTTPException(status_code=405, detail="Wrong parameter, reply should be either 0 or 1")
msg = await crud_reply_to_enter(user_id, group_id, reply, db)
return {"msg": msg}
await crud_leave_group(group_id, user_id, db)
return {"msg": "You successfully left the group"}

@router.get("/getBasicInfo", response_model=dict)
async def get_basic_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
name, desc, avatar = await crud_get_basic_info(group_id, db)
return {"avatar": avatar, "name": name, "desc": desc}

@router.get("/getPeopleInfo", response_model=dict)
async def get_people_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
leader, admins, members = await crud_get_people_info(group_id, db)
return {"leader": leader, "admins": admins, "members": members}

@router.get("/getMyLevel", response_model=dict)
async def get_my_level(group_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
user_id = user.get("id")
level = await crud_get_my_level(user_id, group_id, db)
return {"level": level}

@router.get("/allGroups", response_model=dict)
async def all_groups(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
user_id = user.get("id")
leader, admin, member = await crud_all_groups(user_id, db)
return {"leader": leader, "admin": admin, "member": member}
173 changes: 131 additions & 42 deletions app/curd/group.py
Original file line number Diff line number Diff line change
@@ -1,58 +1,147 @@
from fastapi import HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.exc import IntegrityError
from sqlalchemy import select, insert, delete
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, enter_application
from sqlalchemy import select, insert, delete, update
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin

async def crud_create(leader: int, name: str, description: str, db: AsyncSession):
new_group = Group(leader=leader, name=name, description=description)
async def crud_create(leader: int, name: str, description: str, path: str, db: AsyncSession):
new_group = Group(leader=leader, name=name, description=description, avatar=path)
db.add(new_group)
await db.flush() # 仅将数据同步到数据库,事务尚未提交,此时 new_group.id 已可用
new_relation = insert(user_group).values(user_id=leader, group_id=new_group.id, level=1)
await db.execute(new_relation)
await db.commit()
await db.refresh(new_group)
return new_group.id

async def crud_apply_to_enter(user_id: int, group_id: int, db: AsyncSession):
# 是否已经在组织中
async def crud_gen_invite_code(user_email: str, db: AsyncSession):
# 检查邮箱存在性
query = select(User.id).where(User.email == user_email)
result = await db.execute(query)
user_id = result.scalar_one_or_none()
if not user_id:
raise HTTPException(status_code=405, detail="User not existed")

async def crud_enter_group(user_id: int, group_id: int, db: AsyncSession):
# 检查是否已经在组织内
query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id)
result = await db.execute(query)
existing = result.first()
if existing:
raise HTTPException(status_code=405, detail="Already in the group")
query = select(Group).where(Group.id == group_id)
exist = result.first()
if exist:
raise HTTPException(status_code=408, detail="You are already in the group")
new_relation = insert(user_group).values(user_id=user_id, group_id=group_id)
await db.execute(new_relation)
await db.commit()

async def crud_modify_basic_info(db: AsyncSession, id: int, name: str | None = None, desc: str | None = None, avatar: str | None = None):
query = select(Group.avatar).where(Group.id == id)
result = await db.execute(query)
group = result.scalar_one_or_none()
if group.leader == user_id:
raise HTTPException(status_code=405, detail="Already in the group")
old_path = result.scalar_one_or_none()
update_data = {}
if name:
update_data["name"] = name
if desc:
update_data["description"] = desc
if avatar:
update_data["avatar"] = avatar
query = update(Group).where(Group.id == id).values(**update_data)
await db.execute(query)
await db.commit()
return old_path

async def crud_modify_admin_list(group_id: int, user_id: int, add_admin: bool, db: AsyncSession):
# 检查组织中是否有该成员
query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id)
result = await db.execute(query)
relation = result.first()
if not relation:
raise HTTPException(status_code=405, detail="User currently not in the group")

# 插入申请表,若已存在申请则抛出异常
query = insert(enter_application).values(user_id=user_id, group_id=group_id)
try:
await db.execute(query)
await db.commit()
except IntegrityError:
await db.rollback()
raise HTTPException(status_code=405, detail="Don't apply repeatedly")
# 将该成员设为或取消管理员
if add_admin:
query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(level=2)
else:
query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(level=3)
await db.execute(query)
await db.commit()

return "The user is an admin now" if add_admin else "The user is not an admin now"

async def crud_remove_member(group_id: int, user_id: int, db: AsyncSession):
# 不必先检查组织中是否有该成员,若没有则再执行一次delete也不会报错
query = delete(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id)
await db.execute(query)
await db.commit()

async def crud_leave_group(group_id: int, user_id: int, db: AsyncSession):
# 不必先检查组织中是否有该成员,若没有则再执行一次delete也不会报错
query = delete(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id)
await db.execute(query)
await db.commit()

async def crud_get_basic_info(group_id: int, db: AsyncSession):
query = select(Group.name, Group.description, Group.avatar).where(Group.id == group_id)
result = await db.execute(query)
group = result.first()
return group.name, group.description, group.avatar

async def crud_get_people_info(group_id: int, db: AsyncSession):
# 创建者信息
query = select(Group.leader).where(Group.id == group_id)
result = await db.execute(query)
leader_id = result.scalar_one_or_none()
query = select(User).where(User.id == leader_id)
result = await db.execute(query)
user = result.scalar_one_or_none()
leader = {"id": user.id, "name": user.username, "avatar": user.avatar}

# 管理者信息
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 2)
result = await db.execute(query)
admin_ids = result.scalars().all()
query = select(User).where(User.id.in_(admin_ids))
result = await db.execute(query)
users = result.scalars().all()
admins = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users]

async def crud_get_applications(group_id: int, db: AsyncSession):
query = select(User.id, User.username).where(User.id.in_(
select(enter_application.c.user_id).where(enter_application.c.group_id == group_id)
))
# 普通成员信息
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 3)
result = await db.execute(query)
member_ids = result.scalars().all()
query = select(User).where(User.id.in_(member_ids))
result = await db.execute(query)
users = result.all()
return [{"user_id": user.id, "user_name": user.username} for user in users]
users = result.scalars().all()
members = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users]

return leader, admins, members

async def crud_reply_to_enter(user_id: int, group_id: int, reply: int, db: AsyncSession):
# 答复后,需要从待处理申请的表中删除表项
query = delete(enter_application).where(enter_application.c.user_id == user_id, enter_application.c.group_id == group_id)
async def crud_get_my_level(user_id: int, group_id: int, db: AsyncSession):
query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id)
result = await db.execute(query)
if result.rowcount == 0: # 如果没有删除任何行,说明不存在该项
raise HTTPException(status_code=405, detail="Application is not existed or already handled")
await db.commit()
relation = result.first()
# 在组织中
if relation:
return relation[2] # relation[0] relation[1] relation[2] 分别为表的第1、2、3列
# 不在组织中
return 4

if reply == 1:
new_relation = insert(user_group).values(user_id=user_id, group_id=group_id)
await db.execute(new_relation)
await db.commit()
return "Add new member successfully"
async def crud_all_groups(user_id: int, db: AsyncSession):
query = select(Group).where(Group.leader == user_id).order_by(Group.id.desc())
result = await db.execute(query)
groups = result.scalars().all()
leader = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]

return "Refuse the application successfully"
query = select(user_group.c.group_id).where(user_group.c.user_id == user_id, user_group.c.level == 2)
result = await db.execute(query)
group_ids = result.scalars().all()
query = select(Group).where(Group.id.in_(group_ids)).order_by(Group.id.desc())
result = await db.execute(query)
groups = result.scalars().all()
admin = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]

query = select(user_group.c.group_id).where(user_group.c.user_id == user_id, user_group.c.level == 3)
result = await db.execute(query)
group_ids = result.scalars().all()
query = select(Group).where(Group.id.in_(group_ids)).order_by(Group.id.desc())
result = await db.execute(query)
groups = result.scalars().all()
member = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]

return leader, admin, member
Loading