From 7c8a3b42d39c212d59e0ae8e335153babc5770df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A9=E6=98=95=E7=9D=BF?= <22371298@buaa.edu.cn> Date: Mon, 12 May 2025 20:06:32 +0800 Subject: [PATCH 1/4] =?UTF-8?q?[feat]:=20=E7=BB=84=E7=BB=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 52 +++++++++++++++++++++++++++++++---- app/curd/group.py | 43 +++++++++++++++++++++++++++-- app/schemas/group.py | 3 ++ 3 files changed, 89 insertions(+), 9 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index 56f850e..5258ea9 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -1,11 +1,12 @@ from fastapi import APIRouter, Query, Body, UploadFile, File, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession import os +import glob 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_apply_to_enter, crud_get_applications, crud_reply_to_enter, crud_modify_basic_info, crud_modify_admin_list, crud_remove_member, crud_leave_group +from app.schemas.group import ApplyToEnter, LeaveGroup router = APIRouter() @@ -17,6 +18,7 @@ async def create(group_name: str = Query(...), group_desc: str = Query(...), gro 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) + # 存储头像,文件名为 {group_id}.上传文件的扩展名 if group_avatar: os.makedirs("/lhcos-data/group-avatar", exist_ok=True) ext = os.path.splitext(group_avatar.filename)[1] @@ -39,8 +41,46 @@ async def get_applications(group_id: int = Query(...), db: AsyncSession = Depend 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") +async def reply_to_enter(user_id: int = Body(...), group_id: int = Body(...), reply: bool = Body(...), db: AsyncSession = Depends(get_db)): msg = await crud_reply_to_enter(user_id, group_id, reply, db) - return {"msg": msg} \ No newline at end of file + return {"msg": msg} + +@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") + await crud_modify_basic_info(db=db, id=group_id, name=group_name, desc=group_desc) + if group_avatar: + os.makedirs("/lhcos-data/group-avatar", exist_ok=True) + # 若之前存储了旧头像,则将其删除;若之前就没头像,则不做处理 + old_avatar = glob.glob(os.path.join("/lhcos-data/group-avatar", group_id + ".*")) # 基本名为group_id的文件列表,最多有一个元素 + if old_avatar: + os.remove(old_avatar[0]) + # 存储新头像,文件名为 {group_id}.上传文件的扩展名 + ext = os.path.splitext(group_avatar.filename)[1] + path = os.path.join("/lhcos-data/group-avatar", f"{group_id}{ext}") + with open(path, "wb") as f: + content = await group_avatar.read() + f.write(content) + 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_leave_group(group_id, user_id, db) + return {"msg": "You successfully left the group"} + +# 写返回个人文件树的后端时记得加 visible = True \ No newline at end of file diff --git a/app/curd/group.py b/app/curd/group.py index 4c8382d..d1c0dea 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -1,7 +1,7 @@ from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.exc import IntegrityError -from sqlalchemy import select, insert, delete +from sqlalchemy import select, insert, delete, update from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, enter_application async def crud_create(leader: int, name: str, description: str, db: AsyncSession): @@ -41,7 +41,7 @@ async def crud_get_applications(group_id: int, db: AsyncSession): users = result.all() return [{"user_id": user.id, "user_name": user.username} for user in users] -async def crud_reply_to_enter(user_id: int, group_id: int, reply: int, db: AsyncSession): +async def crud_reply_to_enter(user_id: int, group_id: int, reply: bool, db: AsyncSession): # 答复后,需要从待处理申请的表中删除表项 query = delete(enter_application).where(enter_application.c.user_id == user_id, enter_application.c.group_id == group_id) result = await db.execute(query) @@ -49,10 +49,47 @@ async def crud_reply_to_enter(user_id: int, group_id: int, reply: int, db: Async raise HTTPException(status_code=405, detail="Application is not existed or already handled") await db.commit() - if reply == 1: + if reply: 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" return "Refuse the application successfully" + +async def crud_modify_basic_info(db: AsyncSession, id: int, name: str | None = None, desc: str | None = None): + update_data = {} + if name: + update_data["name"] = name + if desc: + update_data["description"] = desc + query = update(Group).where(Group.id == id).values(**update_data) + await db.execute(query) + await db.commit() + +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 = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(is_admin=add_admin) + 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() \ No newline at end of file diff --git a/app/schemas/group.py b/app/schemas/group.py index dd69fe7..1e5f31f 100644 --- a/app/schemas/group.py +++ b/app/schemas/group.py @@ -1,4 +1,7 @@ from pydantic import BaseModel class ApplyToEnter(BaseModel): + group_id: int + +class LeaveGroup(BaseModel): group_id: int \ No newline at end of file From 5ce61500b8ddb9a31ee6f2155646d663531cd7fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A9=E6=98=95=E7=9D=BF?= <22371298@buaa.edu.cn> Date: Wed, 14 May 2025 10:30:37 +0800 Subject: [PATCH 2/4] =?UTF-8?q?[feat]:=20=E7=BB=84=E7=BB=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 26 +++++++++++++--- app/curd/group.py | 57 ++++++++++++++++++++++++++++++++++- app/schemas/group.py | 3 ++ 3 files changed, 81 insertions(+), 5 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index 5258ea9..cc06aea 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -5,8 +5,8 @@ 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, crud_modify_basic_info, crud_modify_admin_list, crud_remove_member, crud_leave_group -from app.schemas.group import ApplyToEnter, LeaveGroup +from app.curd.group import crud_create, crud_apply_to_enter, crud_get_applications, crud_reply_to_enter, 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 +from app.schemas.group import ApplyToEnter, LeaveGroup, GetBasicInfo router = APIRouter() @@ -55,7 +55,7 @@ async def modify_basic_info(group_id: int = Query(...), group_name: str | None = if group_avatar: os.makedirs("/lhcos-data/group-avatar", exist_ok=True) # 若之前存储了旧头像,则将其删除;若之前就没头像,则不做处理 - old_avatar = glob.glob(os.path.join("/lhcos-data/group-avatar", group_id + ".*")) # 基本名为group_id的文件列表,最多有一个元素 + old_avatar = glob.glob(os.path.join("/lhcos-data/group-avatar", f"{group_id}.*")) # 基本名为group_id的文件列表,最多有一个元素 if old_avatar: os.remove(old_avatar[0]) # 存储新头像,文件名为 {group_id}.上传文件的扩展名 @@ -83,4 +83,22 @@ async def leave_group(model: LeaveGroup, db: AsyncSession = Depends(get_db), use await crud_leave_group(group_id, user_id, db) return {"msg": "You successfully left the group"} -# 写返回个人文件树的后端时记得加 visible = True \ No newline at end of file +@router.get("/getBasicInfo", response_model=dict) +async def get_basic_info(model: GetBasicInfo, db: AsyncSession = Depends(get_db)): + group_id = model.group_id + name, desc = crud_get_basic_info(group_id, db) + find = glob.glob(os.path.join("/lhcos-data/group-avatar", f"{group_id}.*")) + avatar = 'default.png' if not find else find[0].removeprefix("/lhcos-data/group-avatar\\\\") + avatar = '/lhcos-data/group-avatar/' + avatar + 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} \ No newline at end of file diff --git a/app/curd/group.py b/app/curd/group.py index d1c0dea..2544cc2 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -92,4 +92,59 @@ 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() \ No newline at end of file + await db.commit() + +async def crud_get_basic_info(group_id: int, db: AsyncSession): + query = select(Group.name, Group.description).where(Group.id == group_id) + result = await db.execute(query) + group = result.first() + return group.name, group.description + +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.is_admin == True) + 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.all() + admins = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users] + + # 普通成员信息 + query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.is_admin == False) + 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() + members = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users] + + return leader, admins, members + +async def crud_get_my_level(user_id: int, 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() + if user_id == leader_id: + return 1 + 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 relation and relation["is_admin"]: + return 2 + # 是否是普通成员 + if relation: # and not relation["is_admin"]: + return 3 + # 未加入组织 + return 4 \ No newline at end of file diff --git a/app/schemas/group.py b/app/schemas/group.py index 1e5f31f..0ee72ed 100644 --- a/app/schemas/group.py +++ b/app/schemas/group.py @@ -4,4 +4,7 @@ class ApplyToEnter(BaseModel): group_id: int class LeaveGroup(BaseModel): + group_id: int + +class GetBasicInfo(BaseModel): group_id: int \ No newline at end of file From 75fd5af39054e189942e35e0bba2526c125151b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A9=E6=98=95=E7=9D=BF?= <22371298@buaa.edu.cn> Date: Fri, 23 May 2025 10:14:38 +0800 Subject: [PATCH 3/4] =?UTF-8?q?[feat]:=20=E7=BB=84=E7=BB=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 58 ++++++++++++++++++--------- app/curd/group.py | 74 ++++++++++++----------------------- 2 files changed, 65 insertions(+), 67 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index cc06aea..bb07127 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -1,12 +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 glob +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, 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 -from app.schemas.group import ApplyToEnter, LeaveGroup, GetBasicInfo +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 +from app.schemas.group import EnterGroup, LeaveGroup router = APIRouter() @@ -28,22 +31,40 @@ async def create(group_name: str = Query(...), group_desc: str = Query(...), gro f.write(content) 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)): - 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("/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.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("/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")) -@router.post("/replyToEnter", response_model=dict) -async def reply_to_enter(user_id: int = Body(...), group_id: int = Body(...), reply: bool = Body(...), db: AsyncSession = Depends(get_db)): - msg = await crud_reply_to_enter(user_id, group_id, reply, db) - return {"msg": msg} + 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)): @@ -84,9 +105,8 @@ async def leave_group(model: LeaveGroup, db: AsyncSession = Depends(get_db), use return {"msg": "You successfully left the group"} @router.get("/getBasicInfo", response_model=dict) -async def get_basic_info(model: GetBasicInfo, db: AsyncSession = Depends(get_db)): - group_id = model.group_id - name, desc = crud_get_basic_info(group_id, db) +async def get_basic_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)): + name, desc = await crud_get_basic_info(group_id, db) find = glob.glob(os.path.join("/lhcos-data/group-avatar", f"{group_id}.*")) avatar = 'default.png' if not find else find[0].removeprefix("/lhcos-data/group-avatar\\\\") avatar = '/lhcos-data/group-avatar/' + avatar diff --git a/app/curd/group.py b/app/curd/group.py index 2544cc2..ac615a7 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -1,8 +1,7 @@ from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.exc import IntegrityError from sqlalchemy import select, insert, delete, update -from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, enter_application +from app.models.model import User, Group, Folder, Article, Note, Tag, user_group async def crud_create(leader: int, name: str, description: str, db: AsyncSession): new_group = Group(leader=leader, name=name, description=description) @@ -11,51 +10,30 @@ async def crud_create(leader: int, name: str, description: str, db: AsyncSession await db.refresh(new_group) return new_group.id -async def crud_apply_to_enter(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) +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) - existing = result.first() - if existing: - raise HTTPException(status_code=405, detail="Already in the group") - query = select(Group).where(Group.id == group_id) + 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): + # 检查是否已经在组织内 + # 已经是组织leader + query = select(Group).where(Group.id == group_id, Group.leader == user_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") - - # 插入申请表,若已存在申请则抛出异常 - 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") - -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) - )) - result = await db.execute(query) - users = result.all() - return [{"user_id": user.id, "user_name": user.username} for user in users] - -async def crud_reply_to_enter(user_id: int, group_id: int, reply: bool, db: AsyncSession): - # 答复后,需要从待处理申请的表中删除表项 - query = delete(enter_application).where(enter_application.c.user_id == user_id, enter_application.c.group_id == group_id) + # 已经是组织admin或member + 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() - - if reply: - 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" + row = result.first() + if group or row: + raise HTTPException(status_code=408, detail="You are already in the group") - return "Refuse the application successfully" + 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): update_data = {} @@ -116,16 +94,16 @@ async def crud_get_people_info(group_id: int, db: AsyncSession): admin_ids = result.scalars().all() query = select(User).where(User.id.in_(admin_ids)) result = await db.execute(query) - users = result.all() + users = result.scalars().all() admins = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users] # 普通成员信息 query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.is_admin == False) result = await db.execute(query) - member_ids = result.scalars.all() + member_ids = result.scalars().all() query = select(User).where(User.id.in_(member_ids)) result = await db.execute(query) - users = result.all() + users = result.scalars().all() members = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users] return leader, admins, members @@ -139,12 +117,12 @@ async def crud_get_my_level(user_id: int, group_id: int, db: AsyncSession): return 1 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() + relation = result.first() # relation[0] relation[1] relation[2] 分别为表的第1、2、3列 # 是否是管理员 - if relation and relation["is_admin"]: + if relation and relation[2]: return 2 # 是否是普通成员 - if relation: # and not relation["is_admin"]: + if relation: return 3 # 未加入组织 return 4 \ No newline at end of file From 8af02276411662bfd12c4d54ac6f80fc50c57c97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9F=A9=E6=98=95=E7=9D=BF?= <22371298@buaa.edu.cn> Date: Fri, 23 May 2025 13:50:29 +0800 Subject: [PATCH 4/4] =?UTF-8?q?[feat]:=20=E7=BB=84=E7=BB=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=9F=BA=E6=9C=AC=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 3 +- ...ser_group\345\222\214group\350\241\250.py" | 47 ++++++++++ app/api/v1/endpoints/group.py | 39 ++++---- app/curd/group.py | 87 +++++++++++------- app/models/model.py | 9 +- app/schemas/group.py | 7 +- requirements.txt | Bin 1691 -> 2112 bytes 7 files changed, 127 insertions(+), 65 deletions(-) create mode 100644 "alembic/versions/fd8714315ad3_\344\274\230\345\214\226user_group\345\222\214group\350\241\250.py" diff --git a/.env b/.env index 453d6a6..4121775 100644 --- a/.env +++ b/.env @@ -1,3 +1,4 @@ SECRET_KEY=bN3hZ6LbHG7nH9YXWULCr-crcS3GAaRELbNBdAyHBuiHH5TRctd0Zbd6OuLRHHa4Fbs SENDER_PASSWORD=TXVU2unpCAE2EtEX -KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9 \ No newline at end of file +KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9 +FERNET_SECRET_KEY=6WssEkvinI_YqwKXdokii2yI6iBiLO_Cjoyq0bBBC5o= \ No newline at end of file diff --git "a/alembic/versions/fd8714315ad3_\344\274\230\345\214\226user_group\345\222\214group\350\241\250.py" "b/alembic/versions/fd8714315ad3_\344\274\230\345\214\226user_group\345\222\214group\350\241\250.py" new file mode 100644 index 0000000..8b56563 --- /dev/null +++ "b/alembic/versions/fd8714315ad3_\344\274\230\345\214\226user_group\345\222\214group\350\241\250.py" @@ -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 ### diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index bb07127..6ae0974 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -2,13 +2,13 @@ from sqlalchemy.ext.asyncio import AsyncSession from cryptography.fernet import Fernet import os -import glob +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_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 +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() @@ -20,15 +20,16 @@ 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) - # 存储头像,文件名为 {group_id}.上传文件的扩展名 + 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.get("/genInviteCode", response_model=dict) @@ -72,19 +73,18 @@ async def modify_basic_info(group_id: int = Query(...), group_name: str | None = 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") - await crud_modify_basic_info(db=db, id=group_id, name=group_name, desc=group_desc) + new_path = None if group_avatar: os.makedirs("/lhcos-data/group-avatar", exist_ok=True) - # 若之前存储了旧头像,则将其删除;若之前就没头像,则不做处理 - old_avatar = glob.glob(os.path.join("/lhcos-data/group-avatar", f"{group_id}.*")) # 基本名为group_id的文件列表,最多有一个元素 - if old_avatar: - os.remove(old_avatar[0]) - # 存储新头像,文件名为 {group_id}.上传文件的扩展名 + # 存储新头像,保留扩展名 ext = os.path.splitext(group_avatar.filename)[1] - path = os.path.join("/lhcos-data/group-avatar", f"{group_id}{ext}") - with open(path, "wb") as f: + 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) @@ -106,10 +106,7 @@ async def leave_group(model: LeaveGroup, db: AsyncSession = Depends(get_db), use @router.get("/getBasicInfo", response_model=dict) async def get_basic_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)): - name, desc = await crud_get_basic_info(group_id, db) - find = glob.glob(os.path.join("/lhcos-data/group-avatar", f"{group_id}.*")) - avatar = 'default.png' if not find else find[0].removeprefix("/lhcos-data/group-avatar\\\\") - avatar = '/lhcos-data/group-avatar/' + avatar + name, desc, avatar = await crud_get_basic_info(group_id, db) return {"avatar": avatar, "name": name, "desc": desc} @router.get("/getPeopleInfo", response_model=dict) @@ -121,4 +118,10 @@ async def get_people_info(group_id: int = Query(...), db: AsyncSession = Depends 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} \ No newline at end of file + 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} \ No newline at end of file diff --git a/app/curd/group.py b/app/curd/group.py index ac615a7..50f1eb1 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -1,14 +1,15 @@ from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, insert, delete, update -from app.models.model import User, Group, Folder, Article, Note, Tag, user_group +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_gen_invite_code(user_email: str, db: AsyncSession): # 检查邮箱存在性 @@ -20,30 +21,30 @@ async def crud_gen_invite_code(user_email: str, db: AsyncSession): async def crud_enter_group(user_id: int, group_id: int, db: AsyncSession): # 检查是否已经在组织内 - # 已经是组织leader - query = select(Group).where(Group.id == group_id, Group.leader == user_id) - result = await db.execute(query) - group = result.scalar_one_or_none() - # 已经是组织admin或member query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id) result = await db.execute(query) - row = result.first() - if group or row: - raise HTTPException(status_code=408, detail="You are already in the group") - + 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): +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) + 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): # 检查组织中是否有该成员 @@ -54,7 +55,10 @@ async def crud_modify_admin_list(group_id: int, user_id: int, add_admin: bool, d raise HTTPException(status_code=405, detail="User currently not in the group") # 将该成员设为或取消管理员 - query = update(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id).values(is_admin=add_admin) + 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() @@ -73,10 +77,10 @@ async def crud_leave_group(group_id: int, user_id: int, db: AsyncSession): await db.commit() async def crud_get_basic_info(group_id: int, db: AsyncSession): - query = select(Group.name, Group.description).where(Group.id == group_id) + 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 + return group.name, group.description, group.avatar async def crud_get_people_info(group_id: int, db: AsyncSession): # 创建者信息 @@ -89,7 +93,7 @@ async def crud_get_people_info(group_id: int, db: AsyncSession): 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.is_admin == True) + 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)) @@ -98,7 +102,7 @@ async def crud_get_people_info(group_id: int, db: AsyncSession): admins = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users] # 普通成员信息 - query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.is_admin == False) + 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)) @@ -109,20 +113,35 @@ async def crud_get_people_info(group_id: int, db: AsyncSession): return leader, admins, members async def crud_get_my_level(user_id: int, 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() - if user_id == leader_id: - return 1 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() # relation[0] relation[1] relation[2] 分别为表的第1、2、3列 - # 是否是管理员 - if relation and relation[2]: - return 2 - # 是否是普通成员 + relation = result.first() + # 在组织中 if relation: - return 3 - # 未加入组织 - return 4 \ No newline at end of file + return relation[2] # relation[0] relation[1] relation[2] 分别为表的第1、2、3列 + # 不在组织中 + return 4 + +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] + + 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 \ No newline at end of file diff --git a/app/models/model.py b/app/models/model.py index 8c89021..4da65ed 100644 --- a/app/models/model.py +++ b/app/models/model.py @@ -8,13 +8,7 @@ 'user_group', Base.metadata, Column('user_id', Integer, ForeignKey('users.id'), primary_key=True), Column('group_id', Integer, ForeignKey('groups.id'), primary_key=True), - Column('is_admin', Boolean, default=False) -) - -enter_application = Table( - 'enter_application', Base.metadata, - Column('user_id', Integer, ForeignKey('users.id'), primary_key=True), - Column('group_id', Integer, ForeignKey('groups.id'), primary_key=True), + Column('level', Integer, default=3) # 1: leader 2: admin 3:member ) self_recycle_bin = Table( @@ -51,6 +45,7 @@ class Group(Base): leader = Column(Integer) name = Column(String(30), nullable=False) description = Column(String(200), nullable=False) + avatar = Column(String(100)) create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 users = relationship('User', secondary=user_group, back_populates='groups') diff --git a/app/schemas/group.py b/app/schemas/group.py index 0ee72ed..79fcaf0 100644 --- a/app/schemas/group.py +++ b/app/schemas/group.py @@ -1,10 +1,7 @@ from pydantic import BaseModel -class ApplyToEnter(BaseModel): - group_id: int - class LeaveGroup(BaseModel): group_id: int -class GetBasicInfo(BaseModel): - group_id: int \ No newline at end of file +class EnterGroup(BaseModel): + inviteCode: str \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 91c416d07f2485de74149fd96ec39d80a4387588..4e6397855868750eeaf9f08f06488e8c4d8cacb9 100644 GIT binary patch literal 2112 zcmZ8iOK;j>5ZrU6{uEG*Ns}CU=(TF3R;kn@BF4M|hT5hH`SERMcD}U@LT)Jgnw_0p z|NA?)jUBA9y|s3<9sc)hpYfU6*lzs!Xdi42O9`K6*bBF3uw{0Oe1%*bqagNzldtR2-J2DS#a3YC~+gSsXBPWVn=)?}_M)eYZ0VxrE(?Kkf6 zj6Ag?Q9L`#JqK|Y$X$DtB`Ps~g6tEo!XEQfr?4eH@=)*WMHmX_t*{r5zxUtAcJI{- zAm8Ds!OI>`AS8-U$s^DmY~_AOTc=*8-5kX70tgzQtrKrV((|?eXAKxX!($DPD)lqu zeSzOOpUVDpSeW6#v6SFrfgC-~%=4>HDu8;~*$mOt#0K7+H~SNJ)C1>3ZkMnT?HriI zoaQFoWJx)fUL*LY1MRCe~s^1*JYNtl%++)5#17b z={}5O4b5#0J3ZdI_z5gAarRAo>{&I{F;R0vh?#0xxrfvktZeZ+Cm_aHv7g}9USVTa zqETkzrGi%2fErIIi2BQ&=SLuUfyXdT^Q5JQ?SxLA@O_zj6^pUE73_O>Xn%;7&(w1t z@H^GQxkvuHw1Y&(X;Jwxv*=h`8XlGFM0X(a3p?W*eEQ47&e@yl^Cq3p`zoQMb(s6? z@zr3?+$_zI9TkqSMulKp^|ep*pn@x~9+!=F>L%E^_L*I%hTi#LQs0sBtNpT{&X#gX zx#WV@M?6pV!@hNVvX(ZCwWtkl4cVsRIICNqg{<1_QQEu`>=65|t0y&$(KqjUn|AR! z_VNk{K`8Xnck6kyBgCRNgC^Twnh_Lq0t#Z`OnFm7sqCLuwU2pEXyejq8r(!(I7aH- z63lRd+Beo`k7BGrJ?*G^=-)7n@1m~uR#nK}5dD9}UE0orbXqNhNyS2JyWmgr8c3EZPT^a@n^#NMFT+BEOf{O!}OgZQA zL{Cj#gT}L#pD3DJji=DBB&6&kSzfq+u)49yX3@UnVK^Y_u3T1nBe`hVWsGZpwdWoD zFItss=3EBiS!&zIjLfC|9yc11eR!0tkpWnUo_M7PaBY zrLa553diKLb`Q$%%3qltr&QC4SGej$8GC-9!fA^r5wT@6P*VZjAR&-O(p}R8bv3R| zr5XvL*=&O&=x6V8_TFL^JwK4<8nt#-1J4c}3w9HtdZIOU%Qtucpn~GrLS+o#02{ zYW=muLS84{G5fJtqC^MFo4Qv#I{@~k;=P2gY6d%L0+KnM-+jEJQkp2_vIItdSWPle^N_8~_VCh+yW?A9isg38xTK^7FRQ3g1u2-}# zc+D7n_Lnjb!;yCcR#;VXb>uxJ99Gaj1eyj7sO7<@d?aCw?{^dn{p zlGcRV`+avv`T6dfPn8Qq;oWs9HXF2neD#7=!xLHPI!KiE@0T}JcC7sQ3#&E#TAr|@ z3z}Qh*kK|ct#j1h3o5Sz>1ZO67M8A=U}-dWlbd}Yz?Y&!N&G{NRnobHeFsm}W6RP_ z1#K%usf}b7d=r&J`I6bXro~Fm;EL_shY2??8T~-JxFrneezD_C)3KzB+`}QOi0#hx zboWOJ2co;{6C~NTR|0)3=TcAIMmfQ7JCVmyTtW!!ZCU<4fylUmbU$&>5v2jUgC6N8 zK)@2BDsId{xUw4iZEm6Bh1;AF%arX%^?xK5sNq7i*T9Yp4q F{RbSB`lbK?