Skip to content

Commit 8483e17

Browse files
authored
Merge pull request #75 from BUAA-SE-coders007/feature/group_basic_management
Feature/group basic management
2 parents 70ee850 + 8af0227 commit 8483e17

File tree

7 files changed

+288
-72
lines changed

7 files changed

+288
-72
lines changed

.env

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
SECRET_KEY=bN3hZ6LbHG7nH9YXWULCr-crcS3GAaRELbNBdAyHBuiHH5TRctd0Zbd6OuLRHHa4Fbs
22
SENDER_PASSWORD=TXVU2unpCAE2EtEX
3-
KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9
3+
KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9
4+
FERNET_SECRET_KEY=6WssEkvinI_YqwKXdokii2yI6iBiLO_Cjoyq0bBBC5o=
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""优化user_group和group表
2+
3+
Revision ID: fd8714315ad3
4+
Revises: 004c4aa2b3f3
5+
Create Date: 2025-05-23 13:09:52.425623
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
from sqlalchemy.dialects import mysql
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = 'fd8714315ad3'
16+
down_revision: Union[str, None] = '004c4aa2b3f3'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Upgrade schema."""
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.drop_table('enter_application')
25+
op.add_column('groups', sa.Column('avatar', sa.String(length=100), nullable=True))
26+
op.add_column('user_group', sa.Column('level', sa.Integer(), nullable=True))
27+
op.drop_column('user_group', 'is_admin')
28+
# ### end Alembic commands ###
29+
30+
31+
def downgrade() -> None:
32+
"""Downgrade schema."""
33+
# ### commands auto generated by Alembic - please adjust! ###
34+
op.add_column('user_group', sa.Column('is_admin', mysql.TINYINT(display_width=1), autoincrement=False, nullable=True))
35+
op.drop_column('user_group', 'level')
36+
op.drop_column('groups', 'avatar')
37+
op.create_table('enter_application',
38+
sa.Column('user_id', mysql.INTEGER(), autoincrement=False, nullable=False),
39+
sa.Column('group_id', mysql.INTEGER(), autoincrement=False, nullable=False),
40+
sa.ForeignKeyConstraint(['group_id'], ['groups.id'], name='enter_application_ibfk_1'),
41+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], name='enter_application_ibfk_2'),
42+
sa.PrimaryKeyConstraint('user_id', 'group_id'),
43+
mysql_collate='utf8mb4_0900_ai_ci',
44+
mysql_default_charset='utf8mb4',
45+
mysql_engine='InnoDB'
46+
)
47+
# ### end Alembic commands ###

app/api/v1/endpoints/group.py

Lines changed: 101 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from fastapi import APIRouter, Query, Body, UploadFile, File, Depends, HTTPException
22
from sqlalchemy.ext.asyncio import AsyncSession
3+
from cryptography.fernet import Fernet
34
import os
5+
import uuid
6+
from datetime import date, datetime
7+
import json
48

59
from app.utils.get_db import get_db
610
from app.utils.auth import get_current_user
7-
from app.curd.group import crud_create, crud_apply_to_enter, crud_get_applications, crud_reply_to_enter
8-
from app.schemas.group import ApplyToEnter
11+
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
12+
from app.schemas.group import EnterGroup, LeaveGroup
913

1014
router = APIRouter()
1115

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

29-
@router.post("/applyToEnter", response_model=dict)
30-
async def apply_to_enter(model: ApplyToEnter, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
35+
@router.get("/genInviteCode", response_model=dict)
36+
async def gen_invite_code(user_email: str = Query(...), group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
37+
await crud_gen_invite_code(user_email, db)
38+
today = date.today()
39+
data = {
40+
"email": user_email,
41+
"group_id": group_id,
42+
"date": today.isoformat()
43+
}
44+
json_data = json.dumps(data).encode()
45+
fernet = Fernet(os.getenv("FERNET_SECRET_KEY"))
46+
encrypted = fernet.encrypt(json_data)
47+
return {"inviteCode": encrypted}
48+
49+
@router.post("/enterGroup", response_model=dict)
50+
async def enter_group(inviteCode: EnterGroup, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
51+
code = inviteCode.inviteCode
52+
fernet = Fernet(os.getenv("FERNET_SECRET_KEY"))
53+
54+
decrypted = fernet.decrypt(code.encode())
55+
data = json.loads(decrypted)
56+
57+
user_email = user.get("email")
58+
invite_email = data["email"]
59+
if user_email != invite_email:
60+
raise HTTPException(status_code=405, detail="Not your invite code")
61+
62+
invite_date = datetime.strptime(data["date"], "%Y-%m-%d").date()
63+
today = date.today()
64+
if today > invite_date:
65+
raise HTTPException(status_code=406, detail="Invite Code already expired")
66+
67+
await crud_enter_group(user.get("id"), data["group_id"], db)
68+
return {"msg": "Enter thr group successfully"}
69+
70+
@router.post("/modifyBasicInfo", response_model=dict)
71+
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)):
72+
if group_name and len(group_name) > 30:
73+
raise HTTPException(status_code=405, detail="Invalid group name, longer than 30")
74+
if group_desc and len(group_desc) > 200:
75+
raise HTTPException(status_code=405, detail="Invalid group description, longer than 200")
76+
new_path = None
77+
if group_avatar:
78+
os.makedirs("/lhcos-data/group-avatar", exist_ok=True)
79+
# 存储新头像,保留扩展名
80+
ext = os.path.splitext(group_avatar.filename)[1]
81+
new_path = f"/lhcos-data/group-avatar/{uuid.uuid4()}{ext}"
82+
with open(new_path, "wb") as f:
83+
content = await group_avatar.read()
84+
f.write(content)
85+
old_path = await crud_modify_basic_info(db=db, id=group_id, name=group_name, desc=group_desc, avatar=new_path)
86+
if group_avatar and old_path != "/lhcos-data/group-avatar/default.png":
87+
os.remove(old_path)
88+
return {"msg": "Basic info modified successfully"}
89+
90+
@router.post("/modifyAdminList", response_model=dict)
91+
async def modify_admin_list(group_id: int = Body(...), user_id: int = Body(...), add_admin: bool = Body(...), db: AsyncSession = Depends(get_db)):
92+
msg = await crud_modify_admin_list(group_id, user_id, add_admin, db)
93+
return {"msg": msg}
94+
95+
@router.post("/removeMember", response_model=dict)
96+
async def remove_member(group_id: int = Body(...), user_id: int = Body(...), db: AsyncSession = Depends(get_db)):
97+
await crud_remove_member(group_id, user_id, db)
98+
return {"msg": "Member removed successfully"}
99+
100+
@router.post("/leaveGroup", response_model=dict)
101+
async def leave_group(model: LeaveGroup, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
31102
group_id = model.group_id
32103
user_id = user.get("id")
33-
await crud_apply_to_enter(user_id, group_id, db)
34-
return {"msg": "Application sent successfully"}
35-
36-
@router.get("/getApplications", response_model=dict)
37-
async def get_applications(group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
38-
users = await crud_get_applications(group_id, db)
39-
return {"users": users}
40-
41-
@router.post("/replyToEnter", response_model=dict)
42-
async def reply_to_enter(user_id: int = Body(...), group_id: int = Body(...), reply: int = Body(...), db: AsyncSession = Depends(get_db)):
43-
if reply != 0 and reply != 1:
44-
raise HTTPException(status_code=405, detail="Wrong parameter, reply should be either 0 or 1")
45-
msg = await crud_reply_to_enter(user_id, group_id, reply, db)
46-
return {"msg": msg}
104+
await crud_leave_group(group_id, user_id, db)
105+
return {"msg": "You successfully left the group"}
106+
107+
@router.get("/getBasicInfo", response_model=dict)
108+
async def get_basic_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
109+
name, desc, avatar = await crud_get_basic_info(group_id, db)
110+
return {"avatar": avatar, "name": name, "desc": desc}
111+
112+
@router.get("/getPeopleInfo", response_model=dict)
113+
async def get_people_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)):
114+
leader, admins, members = await crud_get_people_info(group_id, db)
115+
return {"leader": leader, "admins": admins, "members": members}
116+
117+
@router.get("/getMyLevel", response_model=dict)
118+
async def get_my_level(group_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
119+
user_id = user.get("id")
120+
level = await crud_get_my_level(user_id, group_id, db)
121+
return {"level": level}
122+
123+
@router.get("/allGroups", response_model=dict)
124+
async def all_groups(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
125+
user_id = user.get("id")
126+
leader, admin, member = await crud_all_groups(user_id, db)
127+
return {"leader": leader, "admin": admin, "member": member}

app/curd/group.py

Lines changed: 131 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,58 +1,147 @@
11
from fastapi import HTTPException
22
from sqlalchemy.ext.asyncio import AsyncSession
3-
from sqlalchemy.exc import IntegrityError
4-
from sqlalchemy import select, insert, delete
5-
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, enter_application
3+
from sqlalchemy import select, insert, delete, update
4+
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin
65

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

14-
async def crud_apply_to_enter(user_id: int, group_id: int, db: AsyncSession):
15-
# 是否已经在组织中
14+
async def crud_gen_invite_code(user_email: str, db: AsyncSession):
15+
# 检查邮箱存在性
16+
query = select(User.id).where(User.email == user_email)
17+
result = await db.execute(query)
18+
user_id = result.scalar_one_or_none()
19+
if not user_id:
20+
raise HTTPException(status_code=405, detail="User not existed")
21+
22+
async def crud_enter_group(user_id: int, group_id: int, db: AsyncSession):
23+
# 检查是否已经在组织内
1624
query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id)
1725
result = await db.execute(query)
18-
existing = result.first()
19-
if existing:
20-
raise HTTPException(status_code=405, detail="Already in the group")
21-
query = select(Group).where(Group.id == group_id)
26+
exist = result.first()
27+
if exist:
28+
raise HTTPException(status_code=408, detail="You are already in the group")
29+
new_relation = insert(user_group).values(user_id=user_id, group_id=group_id)
30+
await db.execute(new_relation)
31+
await db.commit()
32+
33+
async def crud_modify_basic_info(db: AsyncSession, id: int, name: str | None = None, desc: str | None = None, avatar: str | None = None):
34+
query = select(Group.avatar).where(Group.id == id)
2235
result = await db.execute(query)
23-
group = result.scalar_one_or_none()
24-
if group.leader == user_id:
25-
raise HTTPException(status_code=405, detail="Already in the group")
36+
old_path = result.scalar_one_or_none()
37+
update_data = {}
38+
if name:
39+
update_data["name"] = name
40+
if desc:
41+
update_data["description"] = desc
42+
if avatar:
43+
update_data["avatar"] = avatar
44+
query = update(Group).where(Group.id == id).values(**update_data)
45+
await db.execute(query)
46+
await db.commit()
47+
return old_path
48+
49+
async def crud_modify_admin_list(group_id: int, user_id: int, add_admin: bool, db: AsyncSession):
50+
# 检查组织中是否有该成员
51+
query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id)
52+
result = await db.execute(query)
53+
relation = result.first()
54+
if not relation:
55+
raise HTTPException(status_code=405, detail="User currently not in the group")
2656

27-
# 插入申请表,若已存在申请则抛出异常
28-
query = insert(enter_application).values(user_id=user_id, group_id=group_id)
29-
try:
30-
await db.execute(query)
31-
await db.commit()
32-
except IntegrityError:
33-
await db.rollback()
34-
raise HTTPException(status_code=405, detail="Don't apply repeatedly")
57+
# 将该成员设为或取消管理员
58+
if add_admin:
59+
query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(level=2)
60+
else:
61+
query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(level=3)
62+
await db.execute(query)
63+
await db.commit()
64+
65+
return "The user is an admin now" if add_admin else "The user is not an admin now"
66+
67+
async def crud_remove_member(group_id: int, user_id: int, db: AsyncSession):
68+
# 不必先检查组织中是否有该成员,若没有则再执行一次delete也不会报错
69+
query = delete(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id)
70+
await db.execute(query)
71+
await db.commit()
72+
73+
async def crud_leave_group(group_id: int, user_id: int, db: AsyncSession):
74+
# 不必先检查组织中是否有该成员,若没有则再执行一次delete也不会报错
75+
query = delete(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id)
76+
await db.execute(query)
77+
await db.commit()
78+
79+
async def crud_get_basic_info(group_id: int, db: AsyncSession):
80+
query = select(Group.name, Group.description, Group.avatar).where(Group.id == group_id)
81+
result = await db.execute(query)
82+
group = result.first()
83+
return group.name, group.description, group.avatar
84+
85+
async def crud_get_people_info(group_id: int, db: AsyncSession):
86+
# 创建者信息
87+
query = select(Group.leader).where(Group.id == group_id)
88+
result = await db.execute(query)
89+
leader_id = result.scalar_one_or_none()
90+
query = select(User).where(User.id == leader_id)
91+
result = await db.execute(query)
92+
user = result.scalar_one_or_none()
93+
leader = {"id": user.id, "name": user.username, "avatar": user.avatar}
94+
95+
# 管理者信息
96+
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 2)
97+
result = await db.execute(query)
98+
admin_ids = result.scalars().all()
99+
query = select(User).where(User.id.in_(admin_ids))
100+
result = await db.execute(query)
101+
users = result.scalars().all()
102+
admins = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users]
35103

36-
async def crud_get_applications(group_id: int, db: AsyncSession):
37-
query = select(User.id, User.username).where(User.id.in_(
38-
select(enter_application.c.user_id).where(enter_application.c.group_id == group_id)
39-
))
104+
# 普通成员信息
105+
query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 3)
106+
result = await db.execute(query)
107+
member_ids = result.scalars().all()
108+
query = select(User).where(User.id.in_(member_ids))
40109
result = await db.execute(query)
41-
users = result.all()
42-
return [{"user_id": user.id, "user_name": user.username} for user in users]
110+
users = result.scalars().all()
111+
members = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users]
112+
113+
return leader, admins, members
43114

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

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

58-
return "Refuse the application successfully"
131+
query = select(user_group.c.group_id).where(user_group.c.user_id == user_id, user_group.c.level == 2)
132+
result = await db.execute(query)
133+
group_ids = result.scalars().all()
134+
query = select(Group).where(Group.id.in_(group_ids)).order_by(Group.id.desc())
135+
result = await db.execute(query)
136+
groups = result.scalars().all()
137+
admin = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]
138+
139+
query = select(user_group.c.group_id).where(user_group.c.user_id == user_id, user_group.c.level == 3)
140+
result = await db.execute(query)
141+
group_ids = result.scalars().all()
142+
query = select(Group).where(Group.id.in_(group_ids)).order_by(Group.id.desc())
143+
result = await db.execute(query)
144+
groups = result.scalars().all()
145+
member = [{"group_id": group.id, "group_name": group.name, "group_avatar": group.avatar, "group_desc": group.description} for group in groups]
146+
147+
return leader, admin, member

0 commit comments

Comments
 (0)