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 01/37] =?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 02/37] =?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 03/37] =?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 04/37] =?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? From c4366eeab10e7678446b1e0249a92bafad0fe12b Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 25 May 2025 15:58:17 +0800 Subject: [PATCH 05/37] =?UTF-8?q?[feat]:=20=E5=AE=9E=E7=8E=B0=E5=9B=BE?= =?UTF-8?q?=E5=BA=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/auth.py | 48 ++++++++++++++++++++++++++++++++++-- app/main.py | 3 ++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/api/v1/endpoints/auth.py b/app/api/v1/endpoints/auth.py index 52c779d..29a795d 100644 --- a/app/api/v1/endpoints/auth.py +++ b/app/api/v1/endpoints/auth.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, UploadFile from sqlalchemy.ext.asyncio import AsyncSession from passlib.context import CryptContext from datetime import datetime, timedelta @@ -17,6 +17,10 @@ from app.utils.get_db import get_db from app.utils.redis import get_redis_client from app.curd.note import find_recent_notes_in_db +from fastapi import File, UploadFile +from fastapi.responses import FileResponse +import os +from uuid import uuid4 router = APIRouter() @@ -159,4 +163,44 @@ async def get_recent_notes(db: AsyncSession = Depends(get_db)): notes = await find_recent_notes_in_db(db) return { "notes": notes - } \ No newline at end of file + } + +# 上传图片接口 +@router.post("/image/upload", response_model=dict) +async def upload_image(image: UploadFile = File(...)): + """ + 上传图片接口 + """ + try: + # 生成唯一文件名 + file_extension = os.path.splitext(image.filename)[1] + unique_filename = f"{uuid4()}{file_extension}" + image_path = os.path.join("/lhcos-data/images", unique_filename) + + # 确保以二进制模式写入文件,避免编码问题 + with open(image_path, "wb") as f: + f.write(await image.read()) + + # # 生成 URL 路径 + image_url = f"/images/{unique_filename}" + + return {"image_url": image_url} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.get("/image/{imgname}", response_model=dict) +async def get_image(imgname: str): + """ + 获取图片接口 + """ + try: + image_path = os.path.join("/lhcos-data/images", imgname) + if not os.path.exists(image_path): + raise HTTPException(status_code=404, detail="Image not found") + return FileResponse( + path=image_path, + media_type="image/png" # 根据实际图片类型修改或动态设置 + ) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) \ No newline at end of file diff --git a/app/main.py b/app/main.py index e4e1c3c..df2a5a3 100644 --- a/app/main.py +++ b/app/main.py @@ -41,4 +41,5 @@ async def log_requests(request: Request, call_next): ) # 挂载静态文件目录 -app.mount("/static", StaticFiles(directory="app/static"), name="static") \ No newline at end of file +app.mount("/static", StaticFiles(directory="app/static"), name="static") +app.mount("/images", StaticFiles(directory="/lhcos-data/images"), name="images") \ No newline at end of file From 6fea4fc500a5a06afb7d02858dc0bc3bf7088b3e Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 25 May 2025 16:09:54 +0800 Subject: [PATCH 06/37] =?UTF-8?q?[feat]:=20=E6=9B=B4=E6=96=B0=E5=A4=B4?= =?UTF-8?q?=E5=83=8F=E4=BD=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/curd/user.py | 2 +- app/main.py | 2 +- requirements.txt | Bin 2112 -> 1691 bytes 3 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/curd/user.py b/app/curd/user.py index 6545c34..67b0673 100644 --- a/app/curd/user.py +++ b/app/curd/user.py @@ -9,7 +9,7 @@ async def get_user_by_email(db: AsyncSession, email: str): return result.scalar_one_or_none() async def create_user(db: AsyncSession, email: str, username: str, hashed_password: str): - new_user = User(email=email, username=username, password=hashed_password, avatar="/static/avatar/default.png") + new_user = User(email=email, username=username, password=hashed_password, avatar="/lhcos-data/avatar/default.png") db.add(new_user) await db.commit() await db.refresh(new_user) diff --git a/app/main.py b/app/main.py index df2a5a3..bd2079f 100644 --- a/app/main.py +++ b/app/main.py @@ -41,5 +41,5 @@ async def log_requests(request: Request, call_next): ) # 挂载静态文件目录 -app.mount("/static", StaticFiles(directory="app/static"), name="static") +app.mount("/avatar", StaticFiles(directory="/lhcos-data/avatar"), name="avatar") app.mount("/images", StaticFiles(directory="/lhcos-data/images"), name="images") \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 4e6397855868750eeaf9f08f06488e8c4d8cacb9..52cdfb819870e1d46eb86c2ed20b6667d1963880 100644 GIT binary patch literal 1691 zcmah~O^@3+487~WSg>R#nK}4S6zE~G1GL)(7Ci-uEjwytNtNWxIKO@$WzY0jz!#F@ z*W)9tj8Ah8)?B)-j)WXd^@ zCwi#z8Z@5O{6x{*YCMI0CLv`X$@0Vngw^#`Hna9i?z#h_?#g+kHwiPCa*6vOjp7}G={g6sJ@eEfzD`U@hR5)xAB_h^r1Zpav8zcnMNV;pPpeEzW zREm)h8qGF1f`0TaXYVa$(eeXnrl_^E8hAG7Sg@-Q#q$W;HRrI6U7)q6BEOMU(y+|n zP$;}=W@a^MnyKUs1vSp}-i>A=4Sd;8lL=vJ5yC67VI+uQ)nh$duZGRqGCL1oo#02{ zYW=jtg}hF@VfJmdM2QBL7j-LmHUR8J#oH3Ts}by^2}tI2e)aK&N@=2ympNcs1YOH~ z&`uQ|oItN4_V)_a=4dX}`(!|b((>{B!5<9ORja}3RB-+6nk#KwT4t(*MiEqhqX7{v zls%{cUcLu3MC}jFrr-LZS8h;c$p9~;l*(!fz|yiXjIzMLQyb5Xwf+^NDC`S%nI^O^ zc*z)j_LpVsx+8B0tgx!&%E((xIIN(52s8~EP|br+`AF(O`%>zZmX6uk54bC45Wwj4_0gVvOHi% z7c{r1vBN|@TIZ;LEvUQ5^9Ek3w2S~DQuLSy7&bgerjdFtFb|R0ZxP%bcTeJLY0FiM8>3-s%BT5B!2i?<8 zfPf{yH^W)K;i#qU=TJ^ETppM%a_*Qf(SpVeRZ4!#9Va7}Fb&DC#x?Sc6OHII>L4=L F?0+S|`ltW^ 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~u Date: Sun, 25 May 2025 16:14:58 +0800 Subject: [PATCH 07/37] =?UTF-8?q?[docs]:=20=E6=9B=B4=E6=96=B0requirements.?= =?UTF-8?q?txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 52cdfb8..ccc5d03 100644 --- a/requirements.txt +++ b/requirements.txt @@ -27,7 +27,7 @@ fastapi-pagination==0.13.0 fire==0.7.0 fonttools==4.57.0 greenlet==3.1.1 -h11==0.16.0 +h11==0.14.0 httpcore==1.0.8 httpx==0.28.1 idna==3.10 @@ -85,7 +85,7 @@ six==1.17.0 sniffio==1.3.1 soupsieve==2.7 SQLAlchemy==2.0.40 -starlette==0.46.1 +starlette==0.46.2 stringzilla==3.12.5 termcolor==3.0.1 tifffile==2025.3.30 @@ -94,5 +94,5 @@ typing-inspection==0.4.0 typing_extensions==4.13.1 tzdata==2025.2 urllib3==2.4.0 -uvicorn==0.34.0 +uvicorn==0.34.2 win32_setctime==1.2.0 From da39a51ddd5a1768a69e04c0edc275affd3f27e7 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 25 May 2025 16:24:34 +0800 Subject: [PATCH 08/37] =?UTF-8?q?[fix]:=20=E4=BF=AE=E8=A1=A5=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/main.py b/app/main.py index bd2079f..137c0c0 100644 --- a/app/main.py +++ b/app/main.py @@ -4,6 +4,7 @@ from loguru import logger from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles +import os app = FastAPI() @@ -41,5 +42,8 @@ async def log_requests(request: Request, call_next): ) # 挂载静态文件目录 +os.makedirs("/lhcos-data/avatar", exist_ok=True) +os.makedirs("/lhcos-data/images", exist_ok=True) + app.mount("/avatar", StaticFiles(directory="/lhcos-data/avatar"), name="avatar") app.mount("/images", StaticFiles(directory="/lhcos-data/images"), name="images") \ No newline at end of file From bb0d212192b6e7483e366e81d9b3df1967ed37e2 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 25 May 2025 16:39:51 +0800 Subject: [PATCH 09/37] =?UTF-8?q?[fix]:=20=E4=BF=AE=E8=A1=A5=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/app/main.py b/app/main.py index 137c0c0..ae95cc1 100644 --- a/app/main.py +++ b/app/main.py @@ -41,9 +41,23 @@ async def log_requests(request: Request, call_next): allow_headers=["*"], # 允许的请求头 ) +from pathlib import Path +is_github_actions = os.environ.get("GITHUB_ACTIONS") == "true" + +if is_github_actions: + # GitHub Actions 环境 - 使用项目内临时目录 + BASE_DIR = Path(__file__).resolve().parent.parent + AVATAR_DIR = os.path.join(BASE_DIR, "static", "avatar") + IMAGES_DIR = os.path.join(BASE_DIR, "static", "images") +else: + # 生产环境 - 使用实际云存储目录 + AVATAR_DIR = "/lhcos-data/avatar" + IMAGES_DIR = "/lhcos-data/images" + +# 确保目录存在 +os.makedirs(AVATAR_DIR, exist_ok=True) +os.makedirs(IMAGES_DIR, exist_ok=True) + # 挂载静态文件目录 -os.makedirs("/lhcos-data/avatar", exist_ok=True) -os.makedirs("/lhcos-data/images", exist_ok=True) - -app.mount("/avatar", StaticFiles(directory="/lhcos-data/avatar"), name="avatar") -app.mount("/images", StaticFiles(directory="/lhcos-data/images"), name="images") \ No newline at end of file +app.mount("/avatar", StaticFiles(directory=AVATAR_DIR), name="avatar") +app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images") \ No newline at end of file From 46eef76d01c0e46412535212a6ad38214b8273e9 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 25 May 2025 18:03:55 +0800 Subject: [PATCH 10/37] =?UTF-8?q?[feat]:=20=E5=AE=8C=E6=88=90=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...34\350\200\205\345\220\215\345\255\227.py" | 32 +++++++++++++++++++ app/api/v1/endpoints/articleDB.py | 21 ++++++++++-- app/curd/articleDB.py | 29 +++++++++++++++-- app/curd/note.py | 3 +- app/main.py | 2 +- app/schemas/articleDB.py | 7 ++++ app/schemas/note.py | 1 + 7 files changed, 89 insertions(+), 6 deletions(-) create mode 100644 "alembic/versions/f89d896e9b57_\344\275\234\350\200\205\345\220\215\345\255\227.py" diff --git "a/alembic/versions/f89d896e9b57_\344\275\234\350\200\205\345\220\215\345\255\227.py" "b/alembic/versions/f89d896e9b57_\344\275\234\350\200\205\345\220\215\345\255\227.py" new file mode 100644 index 0000000..404dded --- /dev/null +++ "b/alembic/versions/f89d896e9b57_\344\275\234\350\200\205\345\220\215\345\255\227.py" @@ -0,0 +1,32 @@ +"""作者名字 + +Revision ID: f89d896e9b57 +Revises: fd8714315ad3 +Create Date: 2025-05-25 17:48:14.924693 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = 'f89d896e9b57' +down_revision: Union[str, None] = 'fd8714315ad3' +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! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/app/api/v1/endpoints/articleDB.py b/app/api/v1/endpoints/articleDB.py index 9b0e030..ec212df 100644 --- a/app/api/v1/endpoints/articleDB.py +++ b/app/api/v1/endpoints/articleDB.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, HTTPException, Depends, UploadFile, Form, File from sqlalchemy.ext.asyncio import AsyncSession from app.utils.get_db import get_db -from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse -from app.curd.articleDB import create_article_in_db, get_article_in_db, get_article_in_db_by_id, get_article_info_in_db_by_id +from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse, SearchArticle +from app.curd.articleDB import create_article_in_db, get_article_in_db, get_article_in_db_by_id, get_article_info_in_db_by_id, search_article_in_db from app.core.config import settings import os import uuid @@ -54,6 +54,23 @@ async def get_article(get_article: GetArticle = Depends(), db: AsyncSession = De "articles": [articles.model_dump() for articles in articles] } + +@router.get("/search", response_model=dict) +async def search_article(search_article: SearchArticle = Depends(), db: AsyncSession = Depends(get_db)): + """ + Search for an article by title. + """ + # 根据标题查询文章信息 + articles, total_count = await search_article_in_db(db=db, search_article=search_article) + return { + "pagination": { + "page": search_article.page, + "page_size": search_article.page_size, + "total_count": total_count + }, + "articles": [articles.model_dump() for articles in articles] + } + @router.get("/download/{article_id}", response_model=dict) async def download_article(article_id: int, db: AsyncSession = Depends(get_db)): """ diff --git a/app/curd/articleDB.py b/app/curd/articleDB.py index 9af3af5..b11e9f5 100644 --- a/app/curd/articleDB.py +++ b/app/curd/articleDB.py @@ -2,7 +2,7 @@ from sqlalchemy.future import select from sqlalchemy import func from app.models.model import ArticleDB -from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse +from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse, SearchArticle async def create_article_in_db(db: AsyncSession, upload_article: UploadArticle): """ @@ -22,7 +22,7 @@ async def get_article_in_db(db: AsyncSession, get_article: GetArticle): total_count = 1 articles = [articles] if articles else [] elif get_article.page and get_article.page_size: - count_result = await db.execute(select(func.count()).select_from(UploadArticle)) + count_result = await db.execute(select(func.count()).select_from(ArticleDB)) total_count = count_result.scalar() # 获取总数 # 分页查询文章 result = await db.execute( @@ -37,6 +37,31 @@ async def get_article_in_db(db: AsyncSession, get_article: GetArticle): total_count = len(articles) return [GetResponse.model_validate(article) for article in articles], total_count + +async def search_article_in_db(db: AsyncSession, search_article: SearchArticle): + """ + Search for articles by title. + """ + if search_article.author: + result = await db.execute(select(ArticleDB).where(ArticleDB.title.like(f"%{search_article.query}%"), ArticleDB.author.like(f"%{search_article.author}%"))) + articles = result.scalars().all() + total_count = len(articles) + elif search_article.page and search_article.page_size: + count_result = await db.execute(select(func.count()).select_from(ArticleDB).where(ArticleDB.title.like(f"%{search_article.query}%"))) + total_count = count_result.scalar() + # 分页查询文章 + result = await db.execute( + select(ArticleDB) + .where(ArticleDB.title.like(f"%{search_article.query}%")) + .offset((search_article.page - 1) * search_article.page_size) + .limit(search_article.page_size) + ) + articles = result.scalars().all() + else: + result = await db.execute(select(ArticleDB).where(ArticleDB.title.like(f"%{search_article.query}%"))) + articles = result.scalars().all() + total_count = len(articles) + return [GetResponse.model_validate(article) for article in articles], total_count async def get_article_in_db_by_id(db: AsyncSession, article_id: int): """ diff --git a/app/curd/note.py b/app/curd/note.py index 92f007b..3705f6b 100644 --- a/app/curd/note.py +++ b/app/curd/note.py @@ -49,7 +49,8 @@ async def find_notes_in_db(note_find: NoteFind, db: AsyncSession): stmt = stmt.where(Note.id == note_find.id) elif note_find.article_id is not None: stmt = stmt.where(Note.article_id == note_find.article_id) - + if note_find.query is not None: + stmt = stmt.where(Note.content.like(f"%{note_find.query}%") | Note.title.like(f"%{note_find.query}%")) total_count_stmt = select(func.count()).select_from(stmt) total_count_result = await db.execute(total_count_stmt) total_count = total_count_result.scalar() diff --git a/app/main.py b/app/main.py index ae95cc1..6b2cf17 100644 --- a/app/main.py +++ b/app/main.py @@ -59,5 +59,5 @@ async def log_requests(request: Request, call_next): os.makedirs(IMAGES_DIR, exist_ok=True) # 挂载静态文件目录 -app.mount("/avatar", StaticFiles(directory=AVATAR_DIR), name="avatar") +app.mount("/lhcos-data/avatar", StaticFiles(directory=AVATAR_DIR), name="avatar") app.mount("/images", StaticFiles(directory=IMAGES_DIR), name="images") \ No newline at end of file diff --git a/app/schemas/articleDB.py b/app/schemas/articleDB.py index 688188e..46de335 100644 --- a/app/schemas/articleDB.py +++ b/app/schemas/articleDB.py @@ -12,6 +12,12 @@ class GetArticle(BaseModel): page: int | None = None page_size: int | None = None +class SearchArticle(BaseModel): + query: str + author: str | None = None + page: int | None = None + page_size: int | None = None + class DeLArticle(BaseModel): id: int @@ -21,6 +27,7 @@ class GetResponse(BaseModel): url: str create_time: datetime update_time: datetime + author: str file_path: str class Config: diff --git a/app/schemas/note.py b/app/schemas/note.py index 4800eb9..2197f06 100644 --- a/app/schemas/note.py +++ b/app/schemas/note.py @@ -19,6 +19,7 @@ class NoteFind(BaseModel): article_id: int | None = None page: int | None = None page_size: int | None = None + query: str | None = None class NoteResponse(BaseModel): id: int From 9d9ff2833c7b2605dfd809e7e8c5326e466d19a2 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 25 May 2025 19:03:49 +0800 Subject: [PATCH 11/37] =?UTF-8?q?[feat]:=20=E5=A2=9E=E5=8A=A0=E6=90=9C?= =?UTF-8?q?=E7=B4=A2=E6=9D=A1=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/note.py | 5 +++-- app/curd/note.py | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/app/api/v1/endpoints/note.py b/app/api/v1/endpoints/note.py index 56cb98c..4d2bf52 100644 --- a/app/api/v1/endpoints/note.py +++ b/app/api/v1/endpoints/note.py @@ -32,8 +32,9 @@ async def update_note(note_id: int, content: Optional[str] = None, title: Option return {"msg": "Note updated successfully", "note_id": updated_note.id} @router.get("/get", response_model=dict) -async def get_notes(note_find: NoteFind = Depends(), db: AsyncSession = Depends(get_db)): - notes, total_count = await find_notes_in_db(note_find, db) +async def get_notes(note_find: NoteFind = Depends(), db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)): + user_id = current_user["id"] + notes, total_count = await find_notes_in_db(note_find, db, user_id) return { "pagination": { "total_count": total_count, diff --git a/app/curd/note.py b/app/curd/note.py index 3705f6b..aa48945 100644 --- a/app/curd/note.py +++ b/app/curd/note.py @@ -42,7 +42,8 @@ async def update_note_in_db(note_id: int, note: NoteUpdate, db: AsyncSession): await db.refresh(existing_note) return existing_note -async def find_notes_in_db(note_find: NoteFind, db: AsyncSession): +async def find_notes_in_db(note_find: NoteFind, db: AsyncSession, user_id: int): + stmt = select(Note).where(Note.visible == True) # 只查询可见的笔记 if note_find.id is not None: @@ -50,7 +51,7 @@ async def find_notes_in_db(note_find: NoteFind, db: AsyncSession): elif note_find.article_id is not None: stmt = stmt.where(Note.article_id == note_find.article_id) if note_find.query is not None: - stmt = stmt.where(Note.content.like(f"%{note_find.query}%") | Note.title.like(f"%{note_find.query}%")) + stmt = stmt.where((Note.content.like(f"%{note_find.query}%") | Note.title.like(f"%{note_find.query}%")) & Note.creator_id == user_id) total_count_stmt = select(func.count()).select_from(stmt) total_count_result = await db.execute(total_count_stmt) total_count = total_count_result.scalar() From 5d4eb57110a73bb604343082b88b1c5c704ad1ed Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Mon, 26 May 2025 17:46:08 +0800 Subject: [PATCH 12/37] =?UTF-8?q?[feat]:=20=E5=A2=9E=E5=8A=A0ai=E7=9B=B8?= =?UTF-8?q?=E5=85=B3=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 2 +- app/api/v1/endpoints/aichat.py | 84 +++++++++++++++++++++++++++++++++- app/curd/note.py | 10 +++- app/utils/aichat.py | 18 ++++++-- app/utils/readPDF.py | 17 +++++++ 5 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 app/utils/readPDF.py diff --git a/.env b/.env index 4121775..b8ec1b0 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ SECRET_KEY=bN3hZ6LbHG7nH9YXWULCr-crcS3GAaRELbNBdAyHBuiHH5TRctd0Zbd6OuLRHHa4Fbs SENDER_PASSWORD=TXVU2unpCAE2EtEX -KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9 +KIMI_API_KEY=sk-WFAukbN3TVJKhkGLF55a5aF702Ec435b8c36A580E8E4D92d FERNET_SECRET_KEY=6WssEkvinI_YqwKXdokii2yI6iBiLO_Cjoyq0bBBC5o= \ No newline at end of file diff --git a/app/api/v1/endpoints/aichat.py b/app/api/v1/endpoints/aichat.py index f4748ff..e9dd4a6 100644 --- a/app/api/v1/endpoints/aichat.py +++ b/app/api/v1/endpoints/aichat.py @@ -1,11 +1,15 @@ from fastapi import Depends, Request from fastapi.responses import StreamingResponse -from app.utils.aichat import kimi_chat_stream +from app.utils.aichat import kimi_chat_stream, kimi_chat from app.utils.redis import get_redis_client from app.utils.auth import get_current_user import json from fastapi import APIRouter from app.schemas.aichat import NoteInput +from sqlalchemy.ext.asyncio import AsyncSession +from app.utils.get_db import get_db +from app.utils.readPDF import read_pdf +from fastapi import HTTPException router = APIRouter() @@ -47,4 +51,80 @@ async def clear_notes( user_id = current_user["id"] redis_key = f"aichat:{user_id}" redis_client.delete(redis_key) - return {"msg": "clear successfully"} \ No newline at end of file + return {"msg": "clear successfully"} + +@router.get("/review", response_model=dict) +async def review_notes( + article_id: int, +): + path = f"/lhcos-data/{article_id}.pdf" + text = await read_pdf(path) + text += "\n\n请根据以上内容生成文章综述。" + async def ai_stream(): + full_reply = "" + try: + async for content in kimi_chat_stream([{"role": "user", "content": text}]): + full_reply += content + yield f"data: {json.dumps({'content': content}, ensure_ascii=False)}\n\n" + except Exception as e: + error_str = str(e) + if "exceeded model token limit" in error_str: + raise HTTPException( + status_code=413, + detail="输入内容过长,超出了模型的token限制" + ) + # 其他类型的错误重新抛出 + raise + return StreamingResponse(ai_stream(), media_type="text/event-stream") + +@router.get("/graph", response_model=dict) +async def generate_graph( + note_id: int, + db : AsyncSession = Depends(get_db), +): + # 读取数据库获取笔记内容 + from app.curd.note import get_note_by_id + note = await get_note_by_id(db, note_id) + if not note: + raise HTTPException(status_code=404, detail="Note not found") + text = note.content + text += """ + 我需要你对于上面的内容生成思维导图,请仅给我返回mermaid代码,不要有其他内容,下面是生成样例, + graph TD + A[Natural Language Navigation for Service Robots] --> B[Task Definition] + A --> C[Challenges] + A --> D[Proposed Solution] + A --> E[Experimental Results] + + B --> B1["- Predict action sequence from NL instructions"] + B --> B2["- Example: 'Walk out of bathroom to right stairs'"] + + C --> C1["- Environment exploration"] + C --> C2["- Accurate path following"] + C --> C3["- Language-vision relationship modeling"] + + D --> D1[CrossMap Transformer Network] + D --> D2[Transformer-based Speaker] + D --> D3[Double Back-Translation Model] + + D1 --> D11["- Encodes linguistic/visual features"] + D1 --> D12["- Sequentially generates paths"] + + D2 --> D21["- Generates navigation instructions"] + + D3 --> D31["- Paths → Instructions"] + D3 --> D32["- Instructions → Paths"] + D3 --> D33["- Shared latent features"] + + E --> E1["- Improved instruction understanding"] + E --> E2["- Enhanced instruction generation" + """ + try: + ans = await kimi_chat([{"role": "user", "content": text}], model="moonshot-v1-32k") + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"AI服务异常: {str(e)}" + ) + return {"mermaid_code": ans.strip().replace("```mermaid", "").replace("```", "").strip()} + \ No newline at end of file diff --git a/app/curd/note.py b/app/curd/note.py index aa48945..ac4b8ee 100644 --- a/app/curd/note.py +++ b/app/curd/note.py @@ -179,4 +179,12 @@ async def find_self_notes_count_in_db(db: AsyncSession, user_id: int): ) result = await db.execute(stmt) count = result.scalar_one_or_none() - return count \ No newline at end of file + return count + +async def get_note_by_id(db: AsyncSession, note_id: int): + """ + 根据 ID 获取笔记 + """ + stmt = select(Note).where(Note.id == note_id, Note.visible == True) + result = await db.execute(stmt) + return result.scalar_one_or_none() # 返回单个笔记或 None \ No newline at end of file diff --git a/app/utils/aichat.py b/app/utils/aichat.py index c3e7f0c..778a0f5 100644 --- a/app/utils/aichat.py +++ b/app/utils/aichat.py @@ -3,10 +3,10 @@ client = AsyncOpenAI( api_key=settings.KIMI_API_KEY, - base_url="https://api.moonshot.cn/v1", + base_url="http://47.93.172.156:3001/v1", ) -async def kimi_chat_stream(messages, model="moonshot-v1-8k", temperature=0.3): +async def kimi_chat_stream(messages, model="moonshot-v1-32k", temperature=0.3): """ 异步AI流式对话工具方法,传入消息列表,流式返回AI回复内容。 :param messages: List[dict] @@ -21,4 +21,16 @@ async def kimi_chat_stream(messages, model="moonshot-v1-8k", temperature=0.3): async for chunk in stream: content = getattr(chunk.choices[0].delta, "content", None) if content: - yield content \ No newline at end of file + yield content + + +async def kimi_chat(messages, model="moonshot-v1-32k", temperature=0): + """ + 异步但不流式AI对话工具方法,传入消息列表,返回AI回复内容。 + """ + response = await client.chat.completions.create( + model=model, + messages=messages, + temperature=temperature + ) + return response.choices[0].message.content if response.choices else "" \ No newline at end of file diff --git a/app/utils/readPDF.py b/app/utils/readPDF.py new file mode 100644 index 0000000..e12a3dd --- /dev/null +++ b/app/utils/readPDF.py @@ -0,0 +1,17 @@ +import fitz +import asyncio + +def extract_text_from_pdf(pdf_path): + # 打开PDF文件 + doc = fitz.open(pdf_path) + text = "" + # 遍历每一页 + for page_num in range(len(doc) - 2): + page = doc.load_page(page_num) # 加载页面 + page_text = page.get_text("text") + text += page_text + doc.close() # 关闭PDF文件 + return text + +async def read_pdf(pdf_path: str): + return await asyncio.to_thread(extract_text_from_pdf, pdf_path) \ No newline at end of file From 2d493f84f97d63155f36c51e23791dfc065be7a3 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Mon, 26 May 2025 17:46:53 +0800 Subject: [PATCH 13/37] =?UTF-8?q?[docs]:=20=E6=9B=B4=E6=96=B0requirements.?= =?UTF-8?q?txt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index ccc5d03..65d9e22 100644 --- a/requirements.txt +++ b/requirements.txt @@ -63,6 +63,7 @@ pycparser==2.22 pydantic==2.11.2 pydantic_core==2.33.1 PyJWT==2.10.1 +PyMuPDF==1.26.0 PyMySQL==1.1.1 pytest==8.3.5 python-dateutil==2.9.0.post0 From ccf0706023f91d83085ae157029cde7e0fc38dc1 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Tue, 27 May 2025 21:32:34 +0800 Subject: [PATCH 14/37] =?UTF-8?q?[fix]:=20=E5=AE=8C=E6=88=90=E6=96=87?= =?UTF-8?q?=E7=8C=AE=E7=BB=BC=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/aichat.py | 11 +++++++---- app/curd/note.py | 8 +++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/app/api/v1/endpoints/aichat.py b/app/api/v1/endpoints/aichat.py index e9dd4a6..468e014 100644 --- a/app/api/v1/endpoints/aichat.py +++ b/app/api/v1/endpoints/aichat.py @@ -79,15 +79,18 @@ async def ai_stream(): @router.get("/graph", response_model=dict) async def generate_graph( - note_id: int, + article_id: int, db : AsyncSession = Depends(get_db), ): # 读取数据库获取笔记内容 from app.curd.note import get_note_by_id - note = await get_note_by_id(db, note_id) - if not note: + notes = await get_note_by_id(db, article_id) + if not notes: raise HTTPException(status_code=404, detail="Note not found") - text = note.content + text = f"以下是关于文章ID {article_id} 的笔记内容:\n\n" + for note in notes: + text += f"标题: {note.title}\n" if note.title else "" + text += note.content if note.content else "" text += """ 我需要你对于上面的内容生成思维导图,请仅给我返回mermaid代码,不要有其他内容,下面是生成样例, graph TD diff --git a/app/curd/note.py b/app/curd/note.py index ac4b8ee..c751a61 100644 --- a/app/curd/note.py +++ b/app/curd/note.py @@ -181,10 +181,12 @@ async def find_self_notes_count_in_db(db: AsyncSession, user_id: int): count = result.scalar_one_or_none() return count -async def get_note_by_id(db: AsyncSession, note_id: int): +async def get_note_by_id(db: AsyncSession, article_id: int): """ 根据 ID 获取笔记 """ - stmt = select(Note).where(Note.id == note_id, Note.visible == True) + stmt = select(Note).where(Note.article_id == article_id and Note.visible == True) result = await db.execute(stmt) - return result.scalar_one_or_none() # 返回单个笔记或 None \ No newline at end of file + # 返回所有笔记 + notes = result.scalars().all() + return notes if notes else None \ No newline at end of file From d6a8a2b7bf4f28286e16bbf223859627b22b30c4 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, 28 May 2025 20:42:11 +0800 Subject: [PATCH 15/37] =?UTF-8?q?[feat]:=20=E7=BB=84=E7=BB=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...40\347\202\271\345\207\273\351\207\217.py" | 34 +++++++++ ...72\346\210\226\347\273\204\347\273\207.py" | 38 ++++++++++ ...267\350\241\250_article\350\241\250url.py" | 53 +++++++++++++ .../6a0b40746e6c_note\350\241\250group_id.py" | 34 +++++++++ ...06\346\225\260\346\215\256\345\272\223.py" | 32 ++++++++ app/api/v1/endpoints/article.py | 71 +++++++++--------- app/curd/article.py | 74 ++++++++++++++----- app/models/model.py | 70 ++++++++++++++++-- 8 files changed, 343 insertions(+), 63 deletions(-) create mode 100644 "alembic/versions/023952869ee6_article_articledb\350\241\250\345\242\236\345\212\240\347\202\271\345\207\273\351\207\217.py" create mode 100644 "alembic/versions/56dcd6190dd0_article\350\241\250\345\242\236\345\212\240\346\211\200\345\261\236\344\270\252\344\272\272\346\210\226\347\273\204\347\273\207.py" create mode 100644 "alembic/versions/618f8bcbc41e_\346\235\203\351\231\220\345\256\232\344\271\211_\345\210\240\351\231\244\347\224\263\350\257\267\350\241\250_article\350\241\250url.py" create mode 100644 "alembic/versions/6a0b40746e6c_note\350\241\250group_id.py" create mode 100644 "alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" diff --git "a/alembic/versions/023952869ee6_article_articledb\350\241\250\345\242\236\345\212\240\347\202\271\345\207\273\351\207\217.py" "b/alembic/versions/023952869ee6_article_articledb\350\241\250\345\242\236\345\212\240\347\202\271\345\207\273\351\207\217.py" new file mode 100644 index 0000000..174badf --- /dev/null +++ "b/alembic/versions/023952869ee6_article_articledb\350\241\250\345\242\236\345\212\240\347\202\271\345\207\273\351\207\217.py" @@ -0,0 +1,34 @@ +"""Article、ArticleDB表增加点击量 + +Revision ID: 023952869ee6 +Revises: f89d896e9b57 +Create Date: 2025-05-27 20:53:36.631307 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '023952869ee6' +down_revision: Union[str, None] = 'f89d896e9b57' +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.add_column('articleDB', sa.Column('clicks', sa.Integer(), nullable=False)) + op.add_column('articles', sa.Column('clicks', sa.Integer(), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('articles', 'clicks') + op.drop_column('articleDB', 'clicks') + # ### end Alembic commands ### diff --git "a/alembic/versions/56dcd6190dd0_article\350\241\250\345\242\236\345\212\240\346\211\200\345\261\236\344\270\252\344\272\272\346\210\226\347\273\204\347\273\207.py" "b/alembic/versions/56dcd6190dd0_article\350\241\250\345\242\236\345\212\240\346\211\200\345\261\236\344\270\252\344\272\272\346\210\226\347\273\204\347\273\207.py" new file mode 100644 index 0000000..d375707 --- /dev/null +++ "b/alembic/versions/56dcd6190dd0_article\350\241\250\345\242\236\345\212\240\346\211\200\345\261\236\344\270\252\344\272\272\346\210\226\347\273\204\347\273\207.py" @@ -0,0 +1,38 @@ +"""Article表增加所属个人或组织 + +Revision ID: 56dcd6190dd0 +Revises: 023952869ee6 +Create Date: 2025-05-27 21:37:05.492437 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '56dcd6190dd0' +down_revision: Union[str, None] = '023952869ee6' +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.add_column('articles', sa.Column('user_id', sa.Integer(), nullable=True)) + op.add_column('articles', sa.Column('group_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'articles', 'groups', ['group_id'], ['id']) + op.create_foreign_key(None, 'articles', 'users', ['user_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'articles', type_='foreignkey') + op.drop_constraint(None, 'articles', type_='foreignkey') + op.drop_column('articles', 'group_id') + op.drop_column('articles', 'user_id') + # ### end Alembic commands ### diff --git "a/alembic/versions/618f8bcbc41e_\346\235\203\351\231\220\345\256\232\344\271\211_\345\210\240\351\231\244\347\224\263\350\257\267\350\241\250_article\350\241\250url.py" "b/alembic/versions/618f8bcbc41e_\346\235\203\351\231\220\345\256\232\344\271\211_\345\210\240\351\231\244\347\224\263\350\257\267\350\241\250_article\350\241\250url.py" new file mode 100644 index 0000000..0667fdd --- /dev/null +++ "b/alembic/versions/618f8bcbc41e_\346\235\203\351\231\220\345\256\232\344\271\211_\345\210\240\351\231\244\347\224\263\350\257\267\350\241\250_article\350\241\250url.py" @@ -0,0 +1,53 @@ +"""权限定义、删除申请表,Article表URL + +Revision ID: 618f8bcbc41e +Revises: 56dcd6190dd0 +Create Date: 2025-05-27 23:08:11.483163 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '618f8bcbc41e' +down_revision: Union[str, None] = '56dcd6190dd0' +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.create_table('delete_applications', + sa.Column('group_id', sa.Integer(), nullable=True), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('item_type', sa.Integer(), nullable=False), + sa.Column('item_id', sa.Integer(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('user_id', 'item_type', 'item_id') + ) + op.create_table('operate_permissions', + sa.Column('group_id', sa.Integer(), nullable=False), + sa.Column('user_id', sa.Integer(), nullable=False), + sa.Column('item_type', sa.Integer(), nullable=False), + sa.Column('item_id', sa.Integer(), nullable=False), + sa.Column('accessible', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['group_id'], ['groups.id'], ), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ), + sa.PrimaryKeyConstraint('group_id', 'user_id', 'item_type', 'item_id') + ) + op.add_column('articles', sa.Column('url', sa.String(length=200), nullable=False)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('articles', 'url') + op.drop_table('operate_permissions') + op.drop_table('delete_applications') + # ### end Alembic commands ### diff --git "a/alembic/versions/6a0b40746e6c_note\350\241\250group_id.py" "b/alembic/versions/6a0b40746e6c_note\350\241\250group_id.py" new file mode 100644 index 0000000..0a1165e --- /dev/null +++ "b/alembic/versions/6a0b40746e6c_note\350\241\250group_id.py" @@ -0,0 +1,34 @@ +"""Note表group_id + +Revision ID: 6a0b40746e6c +Revises: 618f8bcbc41e +Create Date: 2025-05-27 23:56:45.363419 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '6a0b40746e6c' +down_revision: Union[str, None] = '618f8bcbc41e' +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.add_column('notes', sa.Column('group_id', sa.Integer(), nullable=True)) + op.create_foreign_key(None, 'notes', 'groups', ['group_id'], ['id']) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'notes', type_='foreignkey') + op.drop_column('notes', 'group_id') + # ### end Alembic commands ### diff --git "a/alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" "b/alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" new file mode 100644 index 0000000..e561307 --- /dev/null +++ "b/alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" @@ -0,0 +1,32 @@ +"""组织管理数据库 + +Revision ID: 8384af137b71 +Revises: 6a0b40746e6c +Create Date: 2025-05-28 19:30:06.380078 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '8384af137b71' +down_revision: Union[str, None] = '6a0b40746e6c' +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! ### + pass + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + pass + # ### end Alembic commands ### diff --git a/app/api/v1/endpoints/article.py b/app/api/v1/endpoints/article.py index 9159aba..eb1e017 100644 --- a/app/api/v1/endpoints/article.py +++ b/app/api/v1/endpoints/article.py @@ -5,13 +5,14 @@ from typing import Optional, List import os import io +import uuid from zipfile import ZipFile import zipfile import tempfile from app.utils.get_db import get_db from app.utils.auth import get_current_user -from app.curd.article import crud_upload_to_self_folder, crud_get_self_folders, crud_get_articles_in_folder, crud_self_create_folder, crud_self_article_to_recycle_bin, crud_self_folder_to_recycle_bin, crud_read_article, crud_import_self_folder, crud_export_self_folder,crud_create_tag, crud_delete_tag, crud_get_article_tags, crud_all_tags_order, crud_change_folder_name, crud_change_article_name, crud_article_statistic, crud_self_tree, crud_self_article_statistic, crud_items_in_recycle_bin, crud_delete_forever, crud_recover +from app.curd.article import crud_upload_to_self_folder, crud_get_self_folders, crud_get_articles_in_folder, crud_self_create_folder, crud_self_article_to_recycle_bin, crud_self_folder_to_recycle_bin, crud_annotate_self_article, crud_read_article, crud_read_article_by_url, crud_import_self_folder, crud_export_self_folder,crud_create_tag, crud_delete_tag, crud_get_article_tags, crud_all_tags_order, crud_change_folder_name, crud_change_article_name, crud_article_statistic, crud_self_tree, crud_self_article_statistic, crud_items_in_recycle_bin, crud_delete_forever, crud_recover from app.schemas.article import SelfCreateFolder router = APIRouter() @@ -24,19 +25,19 @@ async def upload_to_self_folder(folder_id: int = Query(...), article: UploadFile raise HTTPException(status_code=405, detail="File uploaded must be a PDF.") await article.seek(0) # 重置文件指针位置 - # 用文件名(不带扩展名)作为 Article 名称 - name = os.path.splitext(article.filename)[0] - - # 新建 Article 记录 - article_id = await crud_upload_to_self_folder(name, folder_id, db) - # 存储到云存储位置 os.makedirs("/lhcos-data", exist_ok=True) - save_path = os.path.join("/lhcos-data", f"{article_id}.pdf") - with open(save_path, "wb") as f: + url = f"/lhcos-data/{uuid.uuid4()}.pdf" + with open(url, "wb") as f: content = await article.read() f.write(content) + # 用文件名(不带扩展名)作为 Article 名称 + name = os.path.splitext(article.filename)[0] + + # 新建 Article 记录 + article_id = await crud_upload_to_self_folder(name, folder_id, url, db) + return {"msg": "Article created successfully.", "article_id": article_id} @router.get("/getSelfFolders", response_model="dict") @@ -84,26 +85,24 @@ async def self_folder_to_recycle_bin(folder_id: int = Query(...), db: AsyncSessi return {"msg": "Folder is moved to recycle bin"} @router.post("/annotateSelfArticle", response_model="dict") -async def annotate_self_article(article_id: int = Query(...), article: UploadFile = File(...)): +async def annotate_self_article(article_id: int = Query(...), article: UploadFile = File(...), db: AsyncSession = Depends(get_db)): # 将新文件存储到云存储位置 os.makedirs("/lhcos-data", exist_ok=True) - save_path = os.path.join("/lhcos-data", f"{article_id}.pdf") - with open(save_path, "wb") as f: + url = await crud_annotate_self_article(article_id, db) + with open(url, "wb") as f: content = await article.read() f.write(content) - return {"msg": "Article annotated successfully."} @router.get("/readArticle", response_class=FileResponse) async def read_article(article_id: int = Query(...), db: AsyncSession = Depends(get_db)): + article_name, url = await crud_read_article(article_id, db) + return FileResponse(path=url, filename=f"{article_name}.pdf", media_type='application/pdf') - file_path = f"/lhcos-data/{article_id}.pdf" - - # 查询文件名 - article_name = await crud_read_article(article_id, db) - - # 返回结果 - return FileResponse(path=file_path, filename=f"{article_name}.pdf", media_type='application/pdf') +@router.get("/readArticleByUrl", response_model="dict") +async def read_article_by_url(article_id: int = Query(...), db: AsyncSession = Depends(get_db)): + url, update_time = await crud_read_article_by_url(article_id, db) + return {"article_url": url, "update_time": update_time} @router.post("/importSelfFolder", response_model="dict") async def import_self_folder(folder_name: str = Query(...), zip: UploadFile = File(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): @@ -118,34 +117,30 @@ async def import_self_folder(folder_name: str = Query(...), zip: UploadFile = Fi zip_file = ZipFile(io.BytesIO(zip_bytes)) article_names = [os.path.splitext(os.path.basename(name))[0] for name in zip_file.namelist() if name.endswith('.pdf')] - # 记入数据库 - result = await crud_import_self_folder(folder_name, article_names, user_id, db) - # 存储文献到云存储 + urls = [f"/lhcos-data/{uuid.uuid4()}.pdf" for article_name in article_names] os.makedirs("/lhcos-data", exist_ok=True) - for i in range(0, len(result), 2): - article_id = result[i] - article_name = result[i + 1] - pdf_filename_in_zip = f"{article_name}.pdf" - with zip_file.open(pdf_filename_in_zip) as source_file: - target_path = os.path.join("/lhcos-data", f"{article_id}.pdf") + for i in range(0, len(article_names)): + article_name_in_zip = f"{article_names[i]}.pdf" + with zip_file.open(article_name_in_zip) as source_file: + target_path = urls[i] with open(target_path, "wb") as out_file: out_file.write(source_file.read()) + # 记入数据库 + await crud_import_self_folder(folder_name, article_names, urls, user_id, db) return {"msg": "Successfully import articles"} @router.get("/exportSelfFolder", response_class=FileResponse) async def export_self_folder(background_tasks: BackgroundTasks, folder_id: int = Query(...), db: AsyncSession = Depends(get_db)): - zip_name, article_ids, article_names = await crud_export_self_folder(folder_id, db) + zip_name, article_ids, article_names, article_urls = await crud_export_self_folder(folder_id, db) tmp_dir = tempfile.gettempdir() zip_path = os.path.join(tmp_dir, f"{zip_name}.zip") with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zipf: - for article_id, article_name in zip(article_ids, article_names): - pdf_path = os.path.join("/lhcos-data", f"{article_id}.pdf") - arcname = f"{article_name}.pdf" # 压缩包内的文件名 - zipf.write(pdf_path, arcname=arcname) + for article_id, article_name, article_url in zip(article_ids, article_names, article_urls): + zipf.write(article_url, arcname=f"{article_name}.pdf") # 将对应位置上的文献写入压缩包,写入时的文件名为文献名 background_tasks.add_task(os.remove, zip_path) @@ -159,8 +154,8 @@ async def export_self_folder(background_tasks: BackgroundTasks, folder_id: int = async def create_tag(article_id: int = Body(...), content: str = Body(...), db: AsyncSession = Depends(get_db)): if len(content) > 30: raise HTTPException(status_code=405, detail="Invalid tag content, longer than 30") - await crud_create_tag(article_id, content, db) - return {"msg": "Tag Created Successfully"} + tag_id = await crud_create_tag(article_id, content, db) + return {"msg": "Tag Created Successfully", "tag_id": tag_id} @router.delete("/deleteTag", response_model="dict") async def delete_tag(tag_id: int = Query(...), db: AsyncSession = Depends(get_db)): @@ -213,7 +208,9 @@ async def items_in_recycle_bin(page_number: Optional[int] = Query(None, ge=1), p @router.delete("/deleteForever", response_model=dict) async def delete_forever(type: int = Query(...), id: int = Query(...), db: AsyncSession = Depends(get_db)): - await crud_delete_forever(type, id, db) + article_urls = await crud_delete_forever(type, id, db) + for article_url in article_urls: + os.remove(article_url) return {"msg": "Item and its child nodes deleted forever successfully"} @router.post("/recover", response_model=dict) diff --git a/app/curd/article.py b/app/curd/article.py index 5656475..b555782 100644 --- a/app/curd/article.py +++ b/app/curd/article.py @@ -4,8 +4,11 @@ from datetime import datetime, timedelta from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin -async def crud_upload_to_self_folder(name: str, folder_id: int, db: AsyncSession): - new_article = Article(name=name, folder_id=folder_id) +async def crud_upload_to_self_folder(name: str, folder_id: int, url: str, db: AsyncSession): + query = select(Folder.user_id).where(Folder.id == folder_id) + result = await db.execute(query) + user_id = result.scalar_one_or_none() + new_article = Article(name=name, folder_id=folder_id, url=url, user_id=user_id) db.add(new_article) await db.commit() await db.refresh(new_article) @@ -74,15 +77,34 @@ async def crud_self_folder_to_recycle_bin(folder_id: int, user_id: int, db: Asyn await db.commit() await db.refresh(folder) +async def crud_annotate_self_article(article_id: int, db: AsyncSession): + query = select(Article).where(Article.id == article_id) + result = await db.execute(query) + article = result.scalar_one_or_none() + article.update_time = datetime.now() + await db.commit() + await db.refresh(article) + return article.url + async def crud_read_article(article_id: int, db: AsyncSession): query = select(Article).where(Article.id == article_id) result = await db.execute(query) article = result.scalar_one_or_none() - return article.name + article.clicks = article.clicks + 1 + await db.commit() + await db.refresh(article) + return article.name, article.url -async def crud_import_self_folder(folder_name: str, article_names, user_id: int, db: AsyncSession): - result = [] +async def crud_read_article_by_url(article_id: int, db: AsyncSession): + query = select(Article).where(Article.id == article_id) + result = await db.execute(query) + article = result.scalar_one_or_none() + article.clicks = article.clicks + 1 + await db.commit() + await db.refresh(article) + return article.url, article.update_time +async def crud_import_self_folder(folder_name: str, article_names, urls, user_id: int, db: AsyncSession): # 新建文件夹 new_folder = Folder(name=folder_name, user_id=user_id) db.add(new_folder) @@ -90,15 +112,13 @@ async def crud_import_self_folder(folder_name: str, article_names, user_id: int, await db.refresh(new_folder) # 新建文献 - new_articles = [Article(name=article_name, folder_id=new_folder.id) for article_name in article_names] + new_articles = [] + for i in range(len(article_names)): + new_articles.append(Article(folder_id=new_folder.id, name=article_names[i], url=urls[i], user_id=user_id)) db.add_all(new_articles) await db.commit() for new_article in new_articles: await db.refresh(new_article) - result.append(new_article.id) - result.append(new_article.name) - - return result async def crud_export_self_folder(folder_id: int, db: AsyncSession): query = select(Folder).where(Folder.id == folder_id) @@ -111,17 +131,20 @@ async def crud_export_self_folder(folder_id: int, db: AsyncSession): articles = result.scalars().all() article_id = [] article_name = [] + article_url = [] for article in articles: article_id.append(article.id) article_name.append(article.name) + article_url.append(article.url) - return folder_name, article_id, article_name + return folder_name, article_id, article_name, article_url async def crud_create_tag(article_id: int, content: str, db: AsyncSession): new_tag = Tag(article_id=article_id, content=content) db.add(new_tag) await db.commit() await db.refresh(new_tag) + return new_tag.id async def crud_delete_tag(tag_id: int, db: AsyncSession): query = select(Tag).filter(Tag.id == tag_id) @@ -301,14 +324,27 @@ async def crud_items_in_recycle_bin(user_id: int, page_number: int, page_size: i async def crud_delete_forever(type: int, id: int, db: AsyncSession): query = delete(self_recycle_bin).where(self_recycle_bin.c.type == type, self_recycle_bin.c.id == id) await db.execute(query) - if type == 1: - query = delete(Folder).where(Folder.id==id) - elif type == 2: - query = delete(Article).where(Article.id==id) - else: - query = delete(Note).where(Note.id==id) - await db.execute(query) - await db.commit() + if type == 1: + query = select(Article.url).where(Article.folder_id == id) + result = await db.execute(query) + urls = result.scalars().all() # 只获取name字段形成列表 + query = delete(Folder).where(Folder.id == id) + result = await db.execute(query) + await db.commit() + return urls + if type == 2: + query = select(Article.url).where(Article.id == id) + result = await db.execute(query) + url = result.scalar_one_or_none() + query = delete(Article).where(Article.id == id) + result = await db.execute(query) + await db.commit() + return [url] + if type == 3: + query = delete(Note).where(Note.id == id) + result = await db.execute(query) + await db.commit() + return [] async def crud_recover(type: int, id: int, db: AsyncSession): query = select(self_recycle_bin).where(self_recycle_bin.c.type == type, self_recycle_bin.c.id == id) diff --git a/app/models/model.py b/app/models/model.py index 4da65ed..703b6ca 100644 --- a/app/models/model.py +++ b/app/models/model.py @@ -7,7 +7,7 @@ user_group = Table( '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('group_id', Integer, ForeignKey('groups.id', ondelete="CASCADE"), primary_key=True), Column('level', Integer, default=3) # 1: leader 2: admin 3:member ) @@ -23,6 +23,46 @@ # 最后两列为有上级时的上级节点信息,用于恢复时检查是否有上级节点在回收站中,和彻底删除时的级联删除 ) +operate_permissions = Table( + 'operate_permissions', Base.metadata, + Column('group_id', Integer, ForeignKey('groups.id', ondelete='CASCADE')), + Column('user_id', Integer, ForeignKey('users.id'), primary_key=True), + Column('item_type', Integer, primary_key=True), # 2:article 3:note + Column('item_id', Integer, primary_key=True), + Column('accessible', Boolean, nullable=False), + Column('article_id', Integer, ForeignKey('articles.id', ondelete="CASCADE")), + Column('folder_id', Integer, ForeignKey('folders.id', ondelete="CASCADE")) + # 最后两列为有上级时的上级节点信息,用于彻底删除时级联删除子节点的操作权限定义 +) + +delete_applications = Table( + 'delete_applications', Base.metadata, + Column('group_id', Integer, ForeignKey('groups.id', ondelete='CASCADE')), + Column('user_id', Integer, ForeignKey('users.id'), primary_key=True), + Column('item_type', Integer, primary_key=True), # 1: folder 2: article 3: note + Column('item_id', Integer, primary_key=True), + Column('article_id', Integer, ForeignKey('articles.id', ondelete="CASCADE")), + Column('folder_id', Integer, ForeignKey('folders.id', ondelete="CASCADE")) + # 最后两列为有上级时的上级节点信息,用于彻底删除时级联删除对子节点的删除申请 +) + +group_logs = Table( + 'group_logs', Base.metadata, + Column('group_id', Integer, ForeignKey('groups.id', ondelete="CASCADE")), + Column('type', Integer, nullable=False), + Column('person1', Integer, ForeignKey('users.id')), + Column('person2', Integer, ForeignKey('users.id')), + Column('folder', String(30)), + Column('article', String(30)), + Column('note', String(100)), + Column('article_tags', Text), + Column('note_content', Text), + Column('folder_new', String(30)), + Column('article_new', Text), + Column('note_new', Text), + Column('time', DateTime, default=func.now(), nullable=False) +) + class User(Base): __tablename__ = 'users' @@ -49,7 +89,7 @@ class Group(Base): 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') - folders = relationship('Folder', back_populates='group') + folders = relationship('Folder', back_populates='group', cascade="all, delete-orphan") class Folder(Base): __tablename__ = 'folders' @@ -58,7 +98,7 @@ class Folder(Base): name = Column(String(30), nullable=False) user_id = Column(Integer, ForeignKey('users.id')) - group_id = Column(Integer, ForeignKey('groups.id')) + group_id = Column(Integer, ForeignKey('groups.id', ondelete="CASCADE")) create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 @@ -74,7 +114,7 @@ class Folder(Base): # 不能同时为空 UniqueConstraint('user_id', 'group_id', name='uq_user_group_folder'), # SQL中认为null 和 null 不相等 CheckConstraint('user_id IS NOT NULL OR group_id IS NOT NULL', name='check_user_or_group'), - ) + ) class Article(Base): __tablename__ = 'articles' @@ -82,14 +122,24 @@ class Article(Base): id = Column(Integer, primary_key=True, index=True, autoincrement=True) name = Column(Text, nullable=False) folder_id = Column(Integer, ForeignKey('folders.id', ondelete="CASCADE")) + url = Column(String(200), nullable=False) create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 - update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 + update_time = Column(DateTime, default=func.now(), nullable=False) # 更新时间 + clicks = Column(Integer, default=0, nullable=False) # 点击量 visible = Column(Boolean, default=True, nullable=False) # 是否可见 False表示在回收站中 + + user_id = Column(Integer, ForeignKey('users.id')) + group_id = Column(Integer, ForeignKey('groups.id')) folder = relationship('Folder', back_populates='articles', lazy='selectin') notes = relationship('Note', back_populates='article', cascade="all, delete-orphan") - tags = relationship('Tag', back_populates='article') + tags = relationship('Tag', back_populates='article', cascade="all, delete-orphan") + + __table_args__ = ( + # 不能同时为空 + CheckConstraint('user_id IS NOT NULL OR group_id IS NOT NULL', name='check_user_or_group'), + ) class Note(Base): __tablename__ = 'notes' @@ -101,16 +151,21 @@ class Note(Base): create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 creator_id = Column(Integer, ForeignKey('users.id')) # 创建者ID + group_id = Column(Integer, ForeignKey('groups.id')) visible = Column(Boolean, default=True, nullable=False) # 是否可见 False表示在回收站中 article = relationship('Article', back_populates='notes') + __table_args__ = ( + # 不能同时为空 + CheckConstraint('creator_id IS NOT NULL OR group_id IS NOT NULL', name='check_creator_or_group'), + ) class Tag(Base): __tablename__ = 'tags' id = Column(Integer, primary_key=True, index=True, autoincrement=True) content = Column(String(30)) - article_id = Column(Integer, ForeignKey('articles.id')) + article_id = Column(Integer, ForeignKey('articles.id', ondelete="CASCADE")) create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 article = relationship('Article', back_populates='tags') @@ -124,6 +179,7 @@ class ArticleDB(Base): url = Column(String(200), nullable=False) author = Column(String(300), nullable=False) file_path = Column(String(200), nullable=False) + clicks = Column(Integer, default=0, nullable=False) # 点击量 create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 \ No newline at end of file From ca2f0e562225559a86de010f55830ecffc0c6364 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Wed, 28 May 2025 21:09:25 +0800 Subject: [PATCH 16/37] =?UTF-8?q?[feat]:=20=E5=A2=9E=E5=8A=A0=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E6=8E=A8=E8=8D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/articleDB.py | 15 ++++++++++++--- app/curd/articleDB.py | 17 +++++++++++++++-- app/schemas/articleDB.py | 5 ++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/api/v1/endpoints/articleDB.py b/app/api/v1/endpoints/articleDB.py index ec212df..dbd9bc9 100644 --- a/app/api/v1/endpoints/articleDB.py +++ b/app/api/v1/endpoints/articleDB.py @@ -1,8 +1,8 @@ from fastapi import APIRouter, HTTPException, Depends, UploadFile, Form, File from sqlalchemy.ext.asyncio import AsyncSession from app.utils.get_db import get_db -from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse, SearchArticle -from app.curd.articleDB import create_article_in_db, get_article_in_db, get_article_in_db_by_id, get_article_info_in_db_by_id, search_article_in_db +from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse, SearchArticle, RecommendArticle +from app.curd.articleDB import create_article_in_db, get_article_in_db, get_article_in_db_by_id, get_article_info_in_db_by_id, search_article_in_db, recommend_article_in_db from app.core.config import settings import os import uuid @@ -120,6 +120,15 @@ async def copy_article(folder_id: int, article_id: int, db: AsyncSession = Depen raise HTTPException(status_code=500, detail=str(e)) return {"msg": "Article copied successfully", "new_article_id": new_article_id} - + +@router.get("/recommend", response_model=dict) +async def recommend_article(recommend_article: RecommendArticle = Depends(), db: AsyncSession = Depends(get_db)): + articles = await recommend_article_in_db(db=db, recommend_article=recommend_article) + return { + "pagination": { + "total_count": recommend_article.size, + }, + "articles": [articles.model_dump() for articles in articles] + } diff --git a/app/curd/articleDB.py b/app/curd/articleDB.py index b11e9f5..b2e6ae7 100644 --- a/app/curd/articleDB.py +++ b/app/curd/articleDB.py @@ -2,7 +2,7 @@ from sqlalchemy.future import select from sqlalchemy import func from app.models.model import ArticleDB -from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse, SearchArticle +from app.schemas.articleDB import UploadArticle, GetArticle, DeLArticle, GetResponse, SearchArticle, RecommendArticle async def create_article_in_db(db: AsyncSession, upload_article: UploadArticle): """ @@ -77,4 +77,17 @@ async def get_article_info_in_db_by_id(db: AsyncSession, article_id: int): """ result = await db.execute(select(ArticleDB).where(ArticleDB.id == article_id)) article = result.scalars().first() - return article.file_path, article.title \ No newline at end of file + return article.file_path, article.title + +async def recommend_article_in_db(db: AsyncSession, recommend_article: RecommendArticle): + """ + Recommend articles based on the number of clicks. + """ + size = recommend_article.size or 10 + result = await db.execute( + select(ArticleDB).order_by(ArticleDB.clicks.desc()) + .limit(size) + ) + articles = result.scalars().all() + + return [GetResponse.model_validate(article) for article in articles] \ No newline at end of file diff --git a/app/schemas/articleDB.py b/app/schemas/articleDB.py index 46de335..53b5b6d 100644 --- a/app/schemas/articleDB.py +++ b/app/schemas/articleDB.py @@ -31,4 +31,7 @@ class GetResponse(BaseModel): file_path: str class Config: - from_attributes = True \ No newline at end of file + from_attributes = True + +class RecommendArticle(BaseModel): + size: int \ No newline at end of file From 01da00512fd08758093aeb1594d39596335ec1d9 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, 28 May 2025 23:49:48 +0800 Subject: [PATCH 17/37] =?UTF-8?q?[feat]:=20=E7=BB=84=E7=BB=87=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...06\346\225\260\346\215\256\345\272\223.py" | 32 - ...06\346\225\260\346\215\256\345\272\223.py" | 38 ++ app/api/v1/endpoints/group.py | 146 ++++- app/curd/group.py | 620 +++++++++++++++++- app/models/model.py | 10 +- requirements.txt | Bin 1707 -> 2146 bytes 6 files changed, 797 insertions(+), 49 deletions(-) delete mode 100644 "alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" create mode 100644 "alembic/versions/9256d579f585_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" diff --git "a/alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" "b/alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" deleted file mode 100644 index e561307..0000000 --- "a/alembic/versions/8384af137b71_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" +++ /dev/null @@ -1,32 +0,0 @@ -"""组织管理数据库 - -Revision ID: 8384af137b71 -Revises: 6a0b40746e6c -Create Date: 2025-05-28 19:30:06.380078 - -""" -from typing import Sequence, Union - -from alembic import op -import sqlalchemy as sa - - -# revision identifiers, used by Alembic. -revision: str = '8384af137b71' -down_revision: Union[str, None] = '6a0b40746e6c' -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! ### - pass - # ### end Alembic commands ### - - -def downgrade() -> None: - """Downgrade schema.""" - # ### commands auto generated by Alembic - please adjust! ### - pass - # ### end Alembic commands ### diff --git "a/alembic/versions/9256d579f585_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" "b/alembic/versions/9256d579f585_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" new file mode 100644 index 0000000..e1d18f2 --- /dev/null +++ "b/alembic/versions/9256d579f585_\347\273\204\347\273\207\347\256\241\347\220\206\346\225\260\346\215\256\345\272\223.py" @@ -0,0 +1,38 @@ +"""组织管理数据库 + +Revision ID: 9256d579f585 +Revises: 6a0b40746e6c +Create Date: 2025-05-28 23:35:01.332825 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '9256d579f585' +down_revision: Union[str, None] = '6a0b40746e6c' +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_constraint('articles_ibfk_2', 'articles', type_='foreignkey') + op.create_foreign_key(None, 'articles', 'groups', ['group_id'], ['id'], ondelete='CASCADE') + op.drop_constraint('notes_ibfk_4', 'notes', type_='foreignkey') + op.create_foreign_key(None, 'notes', 'groups', ['group_id'], ['id'], ondelete='CASCADE') + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, 'notes', type_='foreignkey') + op.create_foreign_key('notes_ibfk_4', 'notes', 'groups', ['group_id'], ['id']) + op.drop_constraint(None, 'articles', type_='foreignkey') + op.create_foreign_key('articles_ibfk_2', 'articles', 'groups', ['group_id'], ['id']) + # ### end Alembic commands ### diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index 6ae0974..381ac5d 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -1,6 +1,7 @@ from fastapi import APIRouter, Query, Body, UploadFile, File, Depends, HTTPException from sqlalchemy.ext.asyncio import AsyncSession from cryptography.fernet import Fernet +from typing import Optional, List import os import uuid from datetime import date, datetime @@ -8,7 +9,7 @@ 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, crud_all_groups +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, crud_new_folder, crud_new_article, crud_new_note, crud_article_tags, crud_file_tree, crud_permission_define, crud_apply_to_delete, crud_all_delete_applications, crud_reply_to_delete, crud_delete, crud_get_permissions, crud_logs, crud_disband, crud_change_folder_name, crud_change_article_name, crud_change_note, crud_read_note from app.schemas.group import EnterGroup, LeaveGroup router = APIRouter() @@ -68,7 +69,7 @@ async def enter_group(inviteCode: EnterGroup, db: AsyncSession = Depends(get_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)): +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), user: dict = Depends(get_current_user)): 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: @@ -82,7 +83,8 @@ async def modify_basic_info(group_id: int = Query(...), group_name: str | None = 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) + user_id=user.get("id") + old_path = await crud_modify_basic_info(db=db, id=group_id, user_id=user_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"} @@ -93,8 +95,9 @@ async def modify_admin_list(group_id: int = Body(...), user_id: int = Body(...), 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) +async def remove_member(group_id: int = Body(...), user_id: int = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + remover_id = user.get("id") + await crud_remove_member(group_id, remover_id, user_id, db) return {"msg": "Member removed successfully"} @router.post("/leaveGroup", response_model=dict) @@ -124,4 +127,135 @@ async def get_my_level(group_id: int = Query(...), db: AsyncSession = Depends(ge 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 + return {"leader": leader, "admin": admin, "member": member} + +@router.post("/newFolder", response_model=dict) +async def new_folder(group_id: int = Body(...), folder_name: str = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + if len(folder_name) > 30: + raise HTTPException(status_code=405, detail="Invalid folder name, longer than 30") + user_id = user.get("id") + await crud_new_folder(user_id, group_id, folder_name, db) + return {"msg": "Folder created successfully"} + +@router.post("/newArticle", response_model=dict) +async def new_article(folder_id: int = Query(...), article: UploadFile = File(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + # 检查上传的必须为 PDF + head = await article.read(5) # 读取文件的前 5 个字节,用于魔数检测 + if not head.startswith(b"%PDF-"): + raise HTTPException(status_code=405, detail="File uploaded must be a PDF.") + await article.seek(0) # 重置文件指针位置 + # 存储到云存储位置 + os.makedirs("/lhcos-data", exist_ok=True) + url = f"/lhcos-data/{uuid.uuid4()}.pdf" + with open(url, "wb") as f: + content = await article.read() + f.write(content) + # 用文件名(不带扩展名)作为 Article 名称 + name = os.path.splitext(article.filename)[0] + # 新建 Article 记录 + user_id = user.get("id") + await crud_new_article(user_id, folder_id, name, url, db) + + return {"msg": "Article created successfully"} + +@router.post("/newNote", response_model=dict) +async def new_note(article_id: int = Body(...), title: str = Body(...), content: str = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + if len(title) > 100: + raise HTTPException(status_code=405, detail="Invalid note title, longer than 100") + user_id = user.get("id") + await crud_new_note(article_id, title, content, user_id, db) + return {"msg": "Note created successfully"} + +@router.post("/articleTags", response_model=dict) +async def article_tags(article_id: int = Body(...), tag_contents: List[str] = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + for tag_content in tag_contents: + if len(tag_content) > 30: + raise HTTPException(status_code=405, detail="Invalid tag content existed, longer than 30") + user_id = user.get("id") + await crud_article_tags(article_id, user_id, tag_contents, db) + return {"msg": "Tags and order changed successfully"} + +@router.get("/fileTree", response_model=dict) +async def file_tree(group_id: int = Query(...), page_number: Optional[int] = Query(None, ge=1), page_size: Optional[int] = Query(None, ge=1), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + total_folder_num, folders = await crud_file_tree(group_id, user_id, page_number, page_size, db) + return {"total_folder_num": total_folder_num, "folders": folders} + +@router.post("/permissionDefine", response_model=dict) +async def permission_define(group_id: int = Body(...), user_id: int = Body(...), item_type: int = Body(...), item_id: int = Body(...), permission: int = Body(...), db: AsyncSession = Depends(get_db)): + await crud_permission_define(group_id, user_id, item_type, item_id, permission, db) + return {"msg": "Permission defined successfully"} + +@router.post("/applyToDelete", response_model=dict) +async def apply_to_delete(group_id: int = Body(...), item_type: int = Body(...), item_id: int = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + await crud_apply_to_delete(group_id, user_id, item_type, item_id, db) + return {"msg": "Delete application sent successfully"} + +@router.get("/allDeleteApplications", response_model=dict) +async def all_delete_applications(group_id: int = Query(...), db: AsyncSession = Depends(get_db)): + applications = await crud_all_delete_applications(group_id, db) + return {"applications": applications} + +@router.post("/replyToDelete", response_model=dict) +async def reply_to_delete(item_type: int = Body(...), item_id: int = Body(...), agree: bool = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + msg, article_urls = await crud_reply_to_delete(user_id, item_type, item_id, agree, db) + for article_url in article_urls: + os.remove(article_url) + return {"msg": msg} + +@router.delete("/delete", response_model=dict) +async def delete(item_type: int = Body(...), item_id: int = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + article_urls = await crud_delete(user_id, item_type, item_id, db) + for article_url in article_urls: + os.remove(article_url) + return {"msg": "Item and its child nodes deleted forever successfully"} + +@router.get("/getPermissions", response_model=dict) +async def get_permissions(group_id: int = Query(...), item_type: int = Query(...), item_id: int = Query(...), db: AsyncSession = Depends(get_db)): + unaccessible, read_only, writeable = await crud_get_permissions(group_id, item_type, item_id, db) + return {"unaccessible": unaccessible, "read_only": read_only, "writeable": writeable} + +@router.post("/changeFolderName", response_model=dict) +async def change_folder_name(folder_id: int = Body(...), folder_name: str = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + if len(folder_name) > 30: + raise HTTPException(status_code=405, detail="Invalid folder name, longer than 30") + user_id = user.get("id") + await crud_change_folder_name(folder_id, folder_name, user_id, db) + return {"msg": "Folder name changed successfully"} + +@router.post("/changeArticleName", response_model=dict) +async def change_article_name(article_id: int = Body(...), article_name: str = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + await crud_change_article_name(article_id, article_name, user_id, db) + return {"msg": "Article name changed successfully"} + +@router.post("/changeNote", response_model=dict) +async def change_note(note_id: int = Body(...), note_title: Optional[str] = Body(default=None), note_content: Optional[str] = Body(default=None), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + if len(note_title) > 100: + raise HTTPException(status_code=405, detail="Invalid note title, longer than 100") + user_id = user.get("id") + await crud_change_note(user_id, note_id, note_title, note_content, db) + return {"msg": "Note changed successfully"} + +@router.get("/readNote", response_model=dict) +async def read_note(note_id: int = Query(...), db: AsyncSession = Depends(get_db)): + note_content, update_time = await crud_read_note(note_id, db) + return {"note_content": note_content, "update_time": update_time} + +@router.get("/logs", response_model=dict) +async def logs(group_id: int = Query(...), page_number: Optional[int] = Query(None, ge=1), page_size: Optional[int] = Query(None, ge=1), db: AsyncSession = Depends(get_db)): + total_num, return_value = await crud_logs(group_id, page_number, page_size, db) + return {"total_num": total_num, "logs": return_value} + +@router.delete("/disband", response_model=dict) +async def disband(group_id: int, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + article_urls, avatar_url = await crud_disband(group_id, user_id, db) + for article_url in article_urls: + os.remove(article_url) + if avatar_url != "/lhcos-data/group-avatar/default.png": + os.remove(avatar_url) + return {"msg": "Group disbanded successfully"} \ No newline at end of file diff --git a/app/curd/group.py b/app/curd/group.py index 50f1eb1..353b684 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -1,7 +1,8 @@ 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, self_recycle_bin +from sqlalchemy import select, insert, delete, update, not_, exists +from sqlalchemy import func +from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin, operate_permissions, delete_applications, group_logs 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) @@ -9,6 +10,8 @@ async def crud_create(leader: int, name: str, description: str, path: str, db: A 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) + new_log = insert(group_logs).values(group_id=new_group.id, type=0, person1=leader) + await db.execute(new_log) await db.commit() async def crud_gen_invite_code(user_email: str, db: AsyncSession): @@ -28,9 +31,11 @@ async def crud_enter_group(user_id: int, group_id: int, db: AsyncSession): 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) + new_log = insert(group_logs).values(group_id=group_id, type=1, person1=user_id) + await db.execute(new_log) 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): +async def crud_modify_basic_info(db: AsyncSession, id: int, user_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() @@ -43,6 +48,8 @@ async def crud_modify_basic_info(db: AsyncSession, id: int, name: str | None = N update_data["avatar"] = avatar query = update(Group).where(Group.id == id).values(**update_data) await db.execute(query) + new_log = insert(group_logs).values(group_id=id, type=2, person1=user_id) + await db.execute(new_log) await db.commit() return old_path @@ -57,23 +64,58 @@ async def crud_modify_admin_list(group_id: int, user_id: int, add_admin: bool, d # 将该成员设为或取消管理员 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) + await db.execute(query) + query = delete(operate_permissions).where(operate_permissions.c.group_id == group_id, operate_permissions.c.user_id == user_id) + await db.execute(query) + new_log = insert(group_logs).values(group_id=group_id, type=3, person2=user_id) + await db.execute(new_log) 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.execute(query) + new_log = insert(group_logs).values(group_id=group_id, type=4, person2=user_id) + await db.execute(new_log) 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) +async def crud_remove_member(group_id: int, user_id: int, remove_member_id: int, db: AsyncSession): + # 若已无此人,则直接return + query = select(user_group).where(user_group.c.user_id == remove_member_id, user_group.c.group_id == group_id) + result = await db.execute(query) + relation = result.first() + if not relation: + return + # 写日志 + new_log = insert(group_logs).values(group_id=group_id, type=5, person1=user_id, person2=remove_member_id) + await db.execute(new_log) + # 删除组织成员记录 + query = delete(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == remove_member_id) + await db.execute(query) + # 清除权限限制和删除申请记录 + query = delete(operate_permissions).where(operate_permissions.c.group_id == group_id, operate_permissions.c.user_id == remove_member_id) + await db.execute(query) + query = delete(delete_applications).where(delete_applications.c.group_id == group_id, delete_applications.c.user_id == remove_member_id) await db.execute(query) await db.commit() async def crud_leave_group(group_id: int, user_id: int, db: AsyncSession): + # 若已无此人,则直接return + 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: + return + # 写日志 + new_log = insert(group_logs).values(group_id=group_id, type=6, person1=user_id) + await db.execute(new_log) # 不必先检查组织中是否有该成员,若没有则再执行一次delete也不会报错 query = delete(user_group).where(user_group.c.group_id == group_id, user_group.c.user_id == user_id) await db.execute(query) + # 清除权限限制和删除申请记录 + query = delete(operate_permissions).where(operate_permissions.c.group_id == group_id, operate_permissions.c.user_id == user_id) + await db.execute(query) + query = delete(delete_applications).where(delete_applications.c.group_id == group_id, delete_applications.c.user_id == user_id) + await db.execute(query) await db.commit() async def crud_get_basic_info(group_id: int, db: AsyncSession): @@ -144,4 +186,566 @@ async def crud_all_groups(user_id: int, db: AsyncSession): 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 + return leader, admin, member + +async def crud_new_folder(user_id: int, group_id: int, folder_name: str, db: AsyncSession): + new_folder = Folder(group_id=group_id, name=folder_name) + db.add(new_folder) + new_log = insert(group_logs).values(group_id=group_id, type=7, person1=user_id, folder=folder_name) + await db.execute(new_log) + await db.commit() + await db.refresh(new_folder) + +async def crud_new_article(user_id: int, folder_id: int, article_name: str, url: str, db: AsyncSession): + # 查询必要信息 + query = select(Folder.group_id, Folder.name).where(Folder.id == folder_id) + result = await db.execute(query) + folder_info = result.one_or_none() + group_id, folder_name = folder_info + # 新建文献记录 + new_article = Article(folder_id=folder_id, name=article_name, url=url, group_id=group_id) + db.add(new_article) + # 写日志 + new_log = insert(group_logs).values(group_id=group_id, type=8, person1=user_id, folder=folder_name, article=article_name) + await db.execute(new_log) + + await db.commit() + await db.refresh(new_article) + return new_article.id + +async def crud_new_note(article_id: int, title: str, content: str, user_id: int, db: AsyncSession): + # 查询必要信息 + query = select(Article.name, Article.group_id, Article.folder_id).where(Article.id == article_id) + result = await db.execute(query) + article_info = result.one_or_none() + article_name, group_id, folder_id = article_info + query = select(Folder.name).where(Folder.id == folder_id) + result = await db.execute(query) + folder_name = result.scalar_one_or_none() + # 新建笔记记录 + new_note = Note(content=content, article_id=article_id, title=title, group_id=group_id) + db.add(new_note) + # 写日志 + new_log = insert(group_logs).values(group_id=group_id, type=9, person1=user_id, folder=folder_name, article=article_name, note=title) + await db.execute(new_log) + + await db.commit() + await db.refresh(new_note) + +async def crud_article_tags(article_id: int, user_id: int, tag_contents, db: AsyncSession): + # 权限检查 + query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 2, operate_permissions.c.item_id == article_id) + result = await db.execute(query) + relation = result.first() + if relation: + raise HTTPException(status_code=403, detail="You have no permission to edit") + # 查询必要信息 + query = select(Article.name, Article.folder_id, Article.group_id).where(Article.id == article_id) # 所属文献名 + result = await db.execute(query) + article_info = result.one_or_none() + article_name, folder_id, group_id = article_info + query = select(Folder.name).where(Folder.id == folder_id) # 所属文件夹名 + result = await db.execute(query) + folder_name = result.scalar_one_or_none() + query = select(Tag).where(Tag.article_id == article_id).order_by(Tag.id.asc()) # 原Tag + result = await db.execute(query) + tags = result.scalars().all() + article_tags = "" + for i in range(len(tags)): + article_tags = article_tags + tags[i].content + ", " + article_tags = article_tags[:-2] if article_tags else "无Tag" + article_new = "" # 修改后Tag + for i in range(len(tag_contents)): + article_new = article_new + tag_contents[i] + ", " + article_new = article_new[:-2] if article_new else "无Tag" + # 写日志 + new_log = insert(group_logs).values(group_id=group_id, type=12, person1=user_id, folder=folder_name, article=article_name, article_tags=article_tags, article_new=article_new) + await db.execute(new_log) + # Tag 修改 + query = delete(Tag).where(Tag.article_id == article_id) + await db.execute(query) + await db.commit() + new_tags = [] + for i in range(0, len(tag_contents)): + new_tags.append(Tag(content=tag_contents[i], article_id=article_id)) + db.add_all(new_tags) + await db.commit() + for i in range(0, len(new_tags)): + await db.refresh(new_tags[i]) + +async def crud_file_tree(group_id: int, user_id: int, page_number: int, page_size: int, db: AsyncSession): + query = select(Folder).where(Folder.group_id == group_id).order_by(Folder.id.desc()) + count_query = select(func.count()).select_from(query.subquery()) + count_result = await db.execute(count_query) + total_num = count_result.scalar() + + if page_number and page_size: + offset = (page_number - 1) * page_size + query = query.offset(offset).limit(page_size) + result = await db.execute(query) + folders = result.scalars().all() + + folder_array = [{"folder_id": folder.id, "folder_name": folder.name, "articles": []} for folder in folders] + for i in range(len(folder_array)): + query = select(Article).where( + Article.folder_id == folder_array[i].get("folder_id"), + # 不存在不可见限制 + not_(exists( + select(operate_permissions.c.item_id).where( + operate_permissions.c.group_id == group_id, + operate_permissions.c.user_id == user_id, + operate_permissions.c.item_type == 2, + operate_permissions.c.item_id == Article.id, + operate_permissions.c.accessible == False + ) + ) + ) + ).order_by(Article.id.desc()) + result = await db.execute(query) + articles = result.scalars().all() + article_array = [{"article_id": article.id, "article_name": article.name, "tags": [], "notes": []} for article in articles] + folder_array[i]["articles"] = article_array + for j in range(len(article_array)): + # 查找所有tag + query = select(Tag).where(Tag.article_id == article_array[j].get("article_id")).order_by(Tag.id.asc()) + result = await db.execute(query) + tags = result.scalars().all() + tag_array = [{"tag_id": tag.id, "tag_content": tag.content} for tag in tags] + article_array[j]["tags"] = tag_array + # 查找所有note + query = select(Note).where( + Note.article_id == article_array[j].get("article_id"), + # 不存在不可见限制 + not_(exists( + select(operate_permissions.c.item_id).where( + operate_permissions.c.group_id == group_id, + operate_permissions.c.user_id == user_id, + operate_permissions.c.item_type == 3, + operate_permissions.c.item_id == Note.id, + operate_permissions.c.accessible == False + ) + ) + ) + ).order_by(Note.id.desc()) + result = await db.execute(query) + notes = result.scalars().all() + note_array = [{"note_id": note.id, "note_title": note.title} for note in notes] + article_array[j]["notes"] = note_array + + return total_num, folder_array + +async def crud_permission_define(group_id: int, user_id: int, item_type: int, item_id: int, permission: 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) + relation = result.first() + if not relation: + raise HTTPException(status_code=405, detail="User currently not in the group") + if relation[2] != 3: + raise HTTPException(status_code=405, detail="Permission can only be defined to common members") + # 可编辑 + if permission == 2: + query = delete(operate_permissions).where(operate_permissions.c.group_id == group_id, operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == item_type, operate_permissions.c.item_id == item_id) + await db.execute(query) + await db.commit() + return + # 不可见 0 或仅查看 1 + from sqlalchemy.dialects.mysql import insert # 用 MySQL 的 insert + if item_type == 2: + query = select(Article.folder_id).where(Article.id == item_id) + result = await db.execute(query) + folder_id = result.scalar_one_or_none() + query = insert(operate_permissions).values(group_id=group_id, user_id=user_id, item_type=item_type, item_id=item_id, folder_id=folder_id, accessible=(permission == 1)).on_duplicate_key_update(accessible=(permission == 1)) + if item_type == 3: + query = select(Note.article_id).where(Note.id == item_id) + result = await db.execute(query) + article_id = result.scalar_one_or_none() + query = select(Article.folder_id).where(Article.id == article_id) + result = await db.execute(query) + folder_id = result.scalar_one_or_none() + query = insert(operate_permissions).values(group_id=group_id, user_id=user_id, item_type=item_type, item_id=item_id, folder_id=folder_id, article_id=article_id, accessible=(permission == 1)).on_duplicate_key_update(accessible=(permission == 1)) + await db.execute(query) + await db.commit() + +async def crud_apply_to_delete(group_id: int, user_id: int, item_type: int, item_id: int, db: AsyncSession): + if item_type == 3: + query = select(Note.article_id).where(Note.id == item_id) + result = await db.execute(query) + article_id = result.scalar_one_or_none() + query = select(Article.folder_id).where(Article.id == article_id) + result = await db.execute(query) + folder_id = result.scalar_one_or_none() + # 将申请插入申请表,若已经申请过则什么都不做(IGNORE) + query = insert(delete_applications).prefix_with("IGNORE").values( + group_id=group_id, + user_id=user_id, + item_type=item_type, + item_id=item_id, + article_id=article_id, + folder_id=folder_id + ) + if item_type == 2: + query = select(Article.folder_id).where(Article.id == item_id) + result = await db.execute(query) + folder_id = result.scalar_one_or_none() + # 将申请插入申请表,若已经申请过则什么都不做(IGNORE) + query = insert(delete_applications).prefix_with("IGNORE").values( + group_id=group_id, + user_id=user_id, + item_type=item_type, + item_id=item_id, + folder_id=folder_id + ) + if item_type == 1: + # 将申请插入申请表,若已经申请过则什么都不做(IGNORE) + query = insert(delete_applications).prefix_with("IGNORE").values( + group_id=group_id, + user_id=user_id, + item_type=item_type, + item_id=item_id, + ) + await db.execute(query) + await db.commit() + +async def crud_all_delete_applications(group_id: int, db: AsyncSession): + query = select( + delete_applications.c.user_id, + delete_applications.c.item_type, + delete_applications.c.item_id + ).where(delete_applications.c.group_id == group_id) + result = await db.execute(query) + applications = result.fetchall() + + return_value = [] + for application in applications: + # 查询申请者name + query = select(User.username, User.avatar).where(User.id == application.user_id) + result = await db.execute(query) + applier = result.one_or_none() + applier_name, applier_avatar = applier + # 查询待删除内容的本级名字和上级名字 + if application.item_type == 1: + # 文件夹名字 + query = select(Folder.name).where(Folder.id == application.item_id) + result = await db.execute(query) + folder = result.scalar_one_or_none() + return_value.append({"applier_name": applier_name, "applier_avatar": applier_avatar, "item_type": 1, "item_id": application.item_id, "folder": folder}) + if application.item_type == 2: + # 文献名字 + query = select(Article.name, Article.folder_id).where(Article.id == application.item_id) + result = await db.execute(query) + article_info = result.one_or_none() + article, folder_id = article_info + # 文件夹名字 + query = select(Folder.name).where(Folder.id == folder_id) + result = await db.execute(query) + folder = result.scalar_one_or_none() + return_value.append({"applier_name": applier_name, "applier_avatar": applier_avatar, "item_type": 2, "item_id": application.item_id, "folder": folder, "article": article}) + if application.item_type == 3: + # 笔记名字 + query = select(Note.title, Note.article_id).where(Note.id == application.item_id) + result = await db.execute(query) + note_info = result.one_or_none() + note, article_id = note_info + # 文献名字 + query = select(Article.name, Article.folder_id).where(Article.id == article_id) + result = await db.execute(query) + article_info = result.one_or_none() + article, folder_id = article_info + # 文件夹名字 + query = select(Folder.name).where(Folder.id == folder_id) + result = await db.execute(query) + folder = result.scalar_one_or_none() + return_value.append({"applier_name": applier_name, "applier_avatar": applier_avatar, "item_type": 3, "item_id": application.item_id, "folder": folder, "article": article, "note": note}) + + return return_value + +async def crud_reply_to_delete(user_id: int, item_type: int, item_id: int, agree: bool, db: AsyncSession): + # 申请是否已被处理 + query = select(delete_applications).where(delete_applications.c.item_type == item_type, delete_applications.c.item_id == item_id) + result = await db.execute(query) + records = result.fetchall() + if not records: + return "Application has already been replied, please refresh the page", [] + # 处理 + if agree: + article_urls = await crud_delete(user_id, item_type, item_id, db) + return "Agree to delete item and its child nodes forever successfully", article_urls + query = delete(delete_applications).where(delete_applications.c.item_type == item_type, delete_applications.c.item_id == item_id) + await db.execute(query) + await db.commit() + return "Refuse to delete successfully", [] + +async def crud_delete(user_id: int, item_type: int, item_id: int, db: AsyncSession): + # 清除删除申请和权限定义 + query = delete(delete_applications).where(delete_applications.c.item_type == item_type, delete_applications.c.item_id == item_id) + await db.execute(query) + query = delete(operate_permissions).where(operate_permissions.c.item_type == item_type, operate_permissions.c.item_id == item_id) + await db.execute(query) + # 彻底删除文件夹 + if item_type == 1: + # 写日志 + query = select(Folder.name, Folder.group_id).where(Folder.id == item_id) + result = await db.execute(query) + folder_info = result.one_or_none() + folder_name, group_id = folder_info + new_log = insert(group_logs).values(group_id=group_id, type=15, person1=user_id, folder=folder_name) + await db.execute(new_log) + # 删除 + query = select(Article.url).where(Article.folder_id == item_id) + result = await db.execute(query) + urls = result.scalars().all() + query = delete(Folder).where(Folder.id == item_id) + result = await db.execute(query) + await db.commit() + return urls + # 彻底删除文献 + if item_type == 2: + # 写日志 + query = select(Article.name, Article.url, Article.group_id, Article.folder_id).where(Article.id == item_id) # 组织号和文献名 + result = await db.execute(query) + article_info = result.one_or_none() + article_name, url, group_id, folder_id = article_info + query = select(Folder.name).where(Folder.id == folder_id) # 文件夹名 + result = await db.execute(query) + folder_name = result.scalar_one_or_none() + new_log = insert(group_logs).values(group_id=group_id, type=16, person1=user_id, folder=folder_name, article=article_name) + await db.execute(new_log) + # 删除 + query = delete(Article).where(Article.id == item_id) + result = await db.execute(query) + await db.commit() + return [url] + # 彻底删除笔记 + if item_type == 3: + # 写日志 + query = select(Note).where(Note.id == item_id) + result = await db.execute(query) + note = result.scalar_one_or_none() + query = select(Article.name, Article.folder_id, Article.group_id).where(Article.id == note.article_id) # 所属文献名 + result = await db.execute(query) + article_info = result.one_or_none() + article_name, folder_id, group_id = article_info + query = select(Folder.name).where(Folder.id == folder_id) # 所属文件夹名 + result = await db.execute(query) + folder_name = result.scalar_one_or_none() + new_log = insert(group_logs).values(group_id=group_id, type=17, person1=user_id, folder=folder_name, article=article_name, note=note.title) + await db.execute(new_log) + # 删除 + query = delete(Note).where(Note.id == item_id) + result = await db.execute(query) + await db.commit() + return [] + +async def crud_get_permissions(group_id: int, item_type: int, item_id: int, db: AsyncSession): + # 所有普通成员 + 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(operate_permissions.c.user_id).where(operate_permissions.c.group_id == group_id, operate_permissions.c.item_type == item_type, operate_permissions.c.item_id == item_id, operate_permissions.c.accessible == False) + result = await db.execute(query) + unaccessible_ids = result.scalars().all() + unaccessible = [] + for unaccessible_id in unaccessible_ids: + query = select(User.username, User.avatar).where(User.id == unaccessible_id) + result = await db.execute(query) + user_info = result.one_or_none() + user_name, user_avatar = user_info + unaccessible.append({"user_name": user_name, "user_avatar": user_avatar}) + # 对该实体仅查看的普通成员id + query = select(operate_permissions.c.user_id).where(operate_permissions.c.group_id == group_id, operate_permissions.c.item_type == item_type, operate_permissions.c.item_id == item_id, operate_permissions.c.accessible == True) + result = await db.execute(query) + read_only_ids = result.scalars().all() + read_only = [] + for read_only_id in read_only_ids: + query = select(User.username, User.avatar).where(User.id == read_only_id) + result = await db.execute(query) + user_info = result.one_or_none() + user_name, user_avatar = user_info + read_only.append({"user_name": user_name, "user_avatar": user_avatar}) + # 对该实体可编辑的普通成员id + writeable_ids = [] + for member_id in member_ids: + if member_id not in set(unaccessible_ids) and member_id not in set(read_only_ids): + writeable_ids.append(member_id) + writeable = [] + for writeable_id in writeable_ids: + query = select(User.username, User.avatar).where(User.id == writeable_id) + result = await db.execute(query) + user_info = result.one_or_none() + user_name, user_avatar = user_info + writeable.append({"user_name": user_name, "user_avatar": user_avatar}) + + return unaccessible, read_only, writeable + +async def crud_change_folder_name(folder_id: int, folder_name: str, user_id: int, db: AsyncSession): + query = select(Folder).where(Folder.id == folder_id) + result = await db.execute(query) + folder = result.scalar_one_or_none() + # 写日志 + new_log = insert(group_logs).values(group_id=folder.group_id, type=10, person1=user_id, folder=folder.name, folder_new=folder_name) + await db.execute(new_log) + # 改名字 + folder.name = folder_name + await db.commit() + await db.refresh(folder) + +async def crud_change_article_name(article_id: int, article_name: str, user_id: int, db: AsyncSession): + # 权限检查 + query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 2, operate_permissions.c.item_id == article_id) + result = await db.execute(query) + relation = result.first() + if relation: + raise HTTPException(status_code=403, detail="You have no permission to edit") + # 查询必要信息 + query = select(Article).where(Article.id == article_id) + result = await db.execute(query) + article = result.scalar_one_or_none() + query = select(Folder).where(Folder.id == article.folder_id) + result = await db.execute(query) + folder = result.scalar_one_or_none() + # 写日志 + new_log = insert(group_logs).values(group_id=folder.group_id, type=11, person1=user_id, folder=folder.name, article=article.name, article_new=article_name) + await db.execute(new_log) + # 改名字 + article.name = article_name + await db.commit() + await db.refresh(article) + +async def crud_change_note(user_id: int, note_id: int, note_title: str, note_content: str, db: AsyncSession): + # 权限检查 + query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 3, operate_permissions.c.item_id == note_id) + result = await db.execute(query) + relation = result.first() + if relation: + raise HTTPException(status_code=403, detail="You have no permission to edit") + # 查询必要信息 + query = select(Note).where(Note.id == note_id) + result = await db.execute(query) + note = result.scalar_one_or_none() + query = select(Article.name, Article.folder_id, Article.group_id).where(Article.id == note.article_id) # 所属文献名 + result = await db.execute(query) + article_info = result.one_or_none() + article_name, folder_id, group_id = article_info + query = select(Folder.name).where(Folder.id == folder_id) # 所属文件夹名 + result = await db.execute(query) + folder_name = result.scalar_one_or_none() + # 修改并写日志 + if note_title: + new_log = insert(group_logs).values(group_id=group_id, type=13, person1=user_id, folder=folder_name, article=article_name, note=note.title, note_new=note_title) + await db.execute(new_log) + note.title = note_title + if note_content: + new_log = insert(group_logs).values(group_id=group_id, type=14, person1=user_id, folder=folder_name, article=article_name, note=note.title, note_content=note.content, note_new=note_content) + await db.execute(new_log) + note.content = note_content + await db.commit() + await db.refresh(note) + +async def crud_read_note(note_id: int, db: AsyncSession): + query = select(Note).where(Note.id == note_id) + result = await db.execute(query) + note = result.scalar_one_or_none() + return note.content, note.update_time + +async def crud_logs(group_id: int, page_number: int, page_size: int, db: AsyncSession): + # 查询log总条数和对应页的log + query = select(group_logs).where(group_logs.c.group_id == group_id) + count_query = select(func.count()).select_from(query.subquery()) + count_result = await db.execute(count_query) + total_num = count_result.scalar() + result = await db.execute(query) + logs = result.fetchall() + # 反序和分页 + logs = logs[::-1] + logs = logs[(page_number - 1) * page_size : page_number * page_size] if page_number and page_size else logs + # 处理18种情况 + return_value = [] + for log in logs: + if log.type == 0 or log.type == 1 or log.type == 2 or log.type == 6: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "time": log.time}) + continue + if log.type == 3 or log.type == 4: + person2 = await get_username_by_id(log.person2, db) + return_value.append({"type": log.type, "person2": person2, "time": log.time}) + continue + if log.type == 5: + person1 = await get_username_by_id(log.person1, db) + person2 = await get_username_by_id(log.person2, db) + return_value.append({"type": log.type, "person1": person1, "person2": person2, "time": log.time}) + continue + if log.type == 7: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "time": log.time}) + continue + if log.type == 8: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "time": log.time}) + continue + if log.type == 9: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "note": log.note, "time": log.time}) + continue + if log.type == 10: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "folder_new": log.folder_new, "time": log.time}) + continue + if log.type == 11: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "article_new": log.article_new, "time": log.time}) + continue + if log.type == 12: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "article_tags": log.article_tags, "article_new": log.article_new, "time": log.time}) + continue + if log.type == 13: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "note": log.note, "note_new": log.note_new, "time": log.time}) + continue + if log.type == 14: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "note": log.note, "note_content": log.note_content, "note_new": log.note_new, "time": log.time}) + continue + if log.type == 15: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "time": log.time}) + continue + if log.type == 16: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "time": log.time}) + continue + if log.type == 17: + person1 = await get_username_by_id(log.person1, db) + return_value.append({"type": log.type, "person1": person1, "folder": log.folder, "article": log.article, "note": log.note, "time": log.time}) + continue + return total_num, return_value + +async def get_username_by_id(user_id: int, db: AsyncSession): + query = select(User.username).where(User.id == user_id) + result = await db.execute(query) + username = result.scalar_one_or_none() + return username + +async def crud_disband(group_id: int, user_id: int, db: AsyncSession): + # 非组织 leader 不得解散组织 + 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: + raise HTTPException(status_code=405, detail="Only leader can disband the group") + # 找到该组织的所有文献 + query = select(Article.url).where(Article.group_id == group_id) + result = await db.execute(query) + article_urls = result.scalars().all() + # 找到该组织的头像 + query = select(Group.avatar).where(Group.id == group_id) + result = await db.execute(query) + avatar_url = result.scalar_one_or_none() + # 解散组织 + query = delete(Group).where(Group.id == group_id) + result = await db.execute(query) + await db.commit() + + return article_urls, avatar_url \ No newline at end of file diff --git a/app/models/model.py b/app/models/model.py index 703b6ca..77c3421 100644 --- a/app/models/model.py +++ b/app/models/model.py @@ -53,7 +53,7 @@ Column('person1', Integer, ForeignKey('users.id')), Column('person2', Integer, ForeignKey('users.id')), Column('folder', String(30)), - Column('article', String(30)), + Column('article', Text), Column('note', String(100)), Column('article_tags', Text), Column('note_content', Text), @@ -90,6 +90,8 @@ class Group(Base): update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 users = relationship('User', secondary=user_group, back_populates='groups') folders = relationship('Folder', back_populates='group', cascade="all, delete-orphan") + articles = relationship('Article', back_populates='group', cascade="all, delete-orphan") + notes = relationship('Note', back_populates='group', cascade="all, delete-orphan") class Folder(Base): __tablename__ = 'folders' @@ -130,9 +132,10 @@ class Article(Base): visible = Column(Boolean, default=True, nullable=False) # 是否可见 False表示在回收站中 user_id = Column(Integer, ForeignKey('users.id')) - group_id = Column(Integer, ForeignKey('groups.id')) + group_id = Column(Integer, ForeignKey('groups.id', ondelete="CASCADE")) folder = relationship('Folder', back_populates='articles', lazy='selectin') + group = relationship('Group', back_populates='articles') notes = relationship('Note', back_populates='article', cascade="all, delete-orphan") tags = relationship('Tag', back_populates='article', cascade="all, delete-orphan") @@ -151,10 +154,11 @@ class Note(Base): create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 creator_id = Column(Integer, ForeignKey('users.id')) # 创建者ID - group_id = Column(Integer, ForeignKey('groups.id')) + group_id = Column(Integer, ForeignKey('groups.id', ondelete="CASCADE")) visible = Column(Boolean, default=True, nullable=False) # 是否可见 False表示在回收站中 article = relationship('Article', back_populates='notes') + group = relationship('Group', back_populates='notes') __table_args__ = ( # 不能同时为空 CheckConstraint('creator_id IS NOT NULL OR group_id IS NOT NULL', name='check_creator_or_group'), diff --git a/requirements.txt b/requirements.txt index 65d9e22bb4ecd05768cb0a551833f0f58859717e..afb895ab3f233f1851d39c29244aae0ddf956a71 100644 GIT binary patch literal 2146 zcmZ8iOK;j>5ZrU6{uEG*Ns}CUs8sb(HBzcp>Jfo4uYjSpX+nN{+nJqju|dcUWoLJ1 zXCMCecWP_fTVp$G?OS;`yO9eg6s$n7_d zm?2K>OytkXa*sjY1!C8pWr0leA0hh4v#`cID;WIF{X zDW|?^Zn88vm#6`-Q|Pb;Lq_I=s1EsdougKzSHjs;Z)sI`n16-u*}2LrX(>w!j{~YD z^U^W&V-EFg1v?S1Xa5l_(Q(#IeymwJRWVs}L&%xavUG%|F;v;$cXmLEF=N-kjlICe zsAQwa3Me(EPJHEc&z@&n%&saB~NyIaA&148>lwtU7O zdyn5aE$n;5ze_tvX6zOxKYA7wYfA%B`JCtuM0|0le}hkVd007XQ-0p0BWhnJcC_|m zpWMId^_iQc9=UiYveNi3m<`1=ZuS(ayOEb*?d!i>V{c?NcsUCa+=K{Bc z*RR_UG9D+!x^XpdyhH3x)HU|bDq3s5!F}g8UH|S4qBkY4#}e;6{^cF#HNL)powj|S z39zr8nV!0t+BV{|-h8(9mgYiQb#FTd1RNeQ|y{`ZYl=RxFl$OB1in HiAnhnV6ZS; literal 1707 zcmah~O>f*b5WVxih|t%r?Sl?MiyVp=NSXpgPX<@w?lMb}3`yD6{`$U0Dekd=E;GaV zdh=$aj8Ah8)|`f+m$j^gvZrPAQ4a$o*>*)SYnN3v-lbt^p*}#X=aU(Sp^?o&wn91Q z@kCEmUW3N7T0StETa9PvCkiS1NXw&aAgpezvYEAC@-Q3_byvP%IfwY8`(8tJX1z#4+uJ5RZKcp&c|6!X?#llSr{N@{|M^@Tg;-D2Wl=+YiBj^?9g!|u1!-s(VDpC9JaAHXzj7c@3giY zmYEz1g;y=itS*`}ZDq%x#+lK(v$;?MU-83aBFq*cydfJ#f*4la*B9&6v{`#$=LxK9 zxB>6sltN?(5r~!YlUiaG^gsVFmp|;50a(S_Yr;k?O#GDRs7Vnv~1jbd-%W zABvZaE~t~!{ks0~_g{N!l%AKbzkg=r_J~#?uiw9Yp-kcdVVyvXN8~+F!cJ9Sy~cWD z@GYrR%0Y*_(T|uhy;;Eae%~c3Ki>WDsc?go-rvoO%?2$YU%6<-a7Pwiix#E*^XVCt zp-f1BV%4V4%N=%f#W_ZeohBK%-ckQtP;<0|r= z;=m(H1-1u0@~1#x3HWAs(eF5NY5Q4}6AhO;ri+|A=1a7ob;nBSx4hzn#1f|C62BSO QwJ$i?h(4nZlDiiF0x`k Date: Fri, 30 May 2025 19:42:21 +0800 Subject: [PATCH 18/37] =?UTF-8?q?[chore]:=20=E5=A2=9E=E5=8A=A0=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E8=BF=94=E5=9B=9E=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 12 ++++++------ app/curd/group.py | 2 ++ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index 381ac5d..11e9495 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -134,8 +134,8 @@ async def new_folder(group_id: int = Body(...), folder_name: str = Body(...), db if len(folder_name) > 30: raise HTTPException(status_code=405, detail="Invalid folder name, longer than 30") user_id = user.get("id") - await crud_new_folder(user_id, group_id, folder_name, db) - return {"msg": "Folder created successfully"} + folder_id = await crud_new_folder(user_id, group_id, folder_name, db) + return {"msg": "Folder created successfully", "folder_id": folder_id} @router.post("/newArticle", response_model=dict) async def new_article(folder_id: int = Query(...), article: UploadFile = File(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): @@ -154,17 +154,17 @@ async def new_article(folder_id: int = Query(...), article: UploadFile = File(.. name = os.path.splitext(article.filename)[0] # 新建 Article 记录 user_id = user.get("id") - await crud_new_article(user_id, folder_id, name, url, db) + article_id = await crud_new_article(user_id, folder_id, name, url, db) - return {"msg": "Article created successfully"} + return {"msg": "Article created successfully", "article_id": article_id} @router.post("/newNote", response_model=dict) async def new_note(article_id: int = Body(...), title: str = Body(...), content: str = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): if len(title) > 100: raise HTTPException(status_code=405, detail="Invalid note title, longer than 100") user_id = user.get("id") - await crud_new_note(article_id, title, content, user_id, db) - return {"msg": "Note created successfully"} + note_id = await crud_new_note(article_id, title, content, user_id, db) + return {"msg": "Note created successfully", "note_id": note_id} @router.post("/articleTags", response_model=dict) async def article_tags(article_id: int = Body(...), tag_contents: List[str] = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): diff --git a/app/curd/group.py b/app/curd/group.py index 353b684..e038b6c 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -195,6 +195,7 @@ async def crud_new_folder(user_id: int, group_id: int, folder_name: str, db: Asy await db.execute(new_log) await db.commit() await db.refresh(new_folder) + return new_folder.id async def crud_new_article(user_id: int, folder_id: int, article_name: str, url: str, db: AsyncSession): # 查询必要信息 @@ -231,6 +232,7 @@ async def crud_new_note(article_id: int, title: str, content: str, user_id: int, await db.commit() await db.refresh(new_note) + return new_note.id async def crud_article_tags(article_id: int, user_id: int, tag_contents, db: AsyncSession): # 权限检查 From 452e9c20c2a4c1fed69dc210e747180f9c95fdfa 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: Sat, 31 May 2025 11:41:45 +0800 Subject: [PATCH 19/37] [feat]: query note permission --- app/api/v1/endpoints/group.py | 8 +++++++- app/api/v1/endpoints/note.py | 2 +- app/curd/group.py | 6 ++++++ app/curd/note.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index 11e9495..59f2e90 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -9,7 +9,7 @@ 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, crud_all_groups, crud_new_folder, crud_new_article, crud_new_note, crud_article_tags, crud_file_tree, crud_permission_define, crud_apply_to_delete, crud_all_delete_applications, crud_reply_to_delete, crud_delete, crud_get_permissions, crud_logs, crud_disband, crud_change_folder_name, crud_change_article_name, crud_change_note, crud_read_note +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, crud_new_folder, crud_new_article, crud_new_note, crud_article_tags, crud_file_tree, crud_permission_define, crud_apply_to_delete, crud_all_delete_applications, crud_reply_to_delete, crud_delete, crud_get_permissions, crud_if_edit_note, crud_logs, crud_disband, crud_change_folder_name, crud_change_article_name, crud_change_note, crud_read_note from app.schemas.group import EnterGroup, LeaveGroup router = APIRouter() @@ -218,6 +218,12 @@ async def get_permissions(group_id: int = Query(...), item_type: int = Query(... unaccessible, read_only, writeable = await crud_get_permissions(group_id, item_type, item_id, db) return {"unaccessible": unaccessible, "read_only": read_only, "writeable": writeable} +@router.get("/ifEditNote", response_model=dict) +async def if_edit_note(note_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + editable = await crud_if_edit_note(note_id, user_id, db) + return {"editable": editable} + @router.post("/changeFolderName", response_model=dict) async def change_folder_name(folder_id: int = Body(...), folder_name: str = Body(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): if len(folder_name) > 30: diff --git a/app/api/v1/endpoints/note.py b/app/api/v1/endpoints/note.py index 4d2bf52..8693925 100644 --- a/app/api/v1/endpoints/note.py +++ b/app/api/v1/endpoints/note.py @@ -41,7 +41,7 @@ async def get_notes(note_find: NoteFind = Depends(), db: AsyncSession = Depends( "page": note_find.page, "page_size": note_find.page_size }, - "notes": [note.model_dump() for note in notes] + "notes": notes } @router.get("/title", response_model=dict) diff --git a/app/curd/group.py b/app/curd/group.py index e038b6c..053edfe 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -581,6 +581,12 @@ async def crud_get_permissions(group_id: int, item_type: int, item_id: int, db: return unaccessible, read_only, writeable +async def crud_if_edit_note(note_id: int, user_id: int, db: AsyncSession): + query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 3, operate_permissions.c.item_id == note_id) + result = await db.execute(query) + permission = result.first() + return False if permission else True + async def crud_change_folder_name(folder_id: int, folder_name: str, user_id: int, db: AsyncSession): query = select(Folder).where(Folder.id == folder_id) result = await db.execute(query) diff --git a/app/curd/note.py b/app/curd/note.py index c751a61..67c1f5a 100644 --- a/app/curd/note.py +++ b/app/curd/note.py @@ -61,7 +61,7 @@ async def find_notes_in_db(note_find: NoteFind, db: AsyncSession, user_id: int): stmt = stmt.offset(offset).limit(note_find.page_size) result = await db.execute(stmt) - notes = [NoteResponse.model_validate(note) for note in result.scalars().all()] + notes = [{"id": note.id, "title": note.title, "content": note.content, "article_id": note.article_id, "is_group": True if note.group_id else False, "create_time": note.create_time, "update_time": note.update_time} for note in result.scalars().all()] return notes, total_count async def find_notes_title_in_db(note_find: NoteFind, db: AsyncSession): From 6f1c052db927de11aefc8c3f695889d7e55c4548 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sat, 31 May 2025 16:12:49 +0800 Subject: [PATCH 20/37] =?UTF-8?q?[fix]:=20=E7=BA=B5=E5=90=91=E6=8E=92?= =?UTF-8?q?=E5=88=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/aichat.py | 82 ++++++++++++++++++++++------------ 1 file changed, 53 insertions(+), 29 deletions(-) diff --git a/app/api/v1/endpoints/aichat.py b/app/api/v1/endpoints/aichat.py index 468e014..6d3770d 100644 --- a/app/api/v1/endpoints/aichat.py +++ b/app/api/v1/endpoints/aichat.py @@ -92,35 +92,59 @@ async def generate_graph( text += f"标题: {note.title}\n" if note.title else "" text += note.content if note.content else "" text += """ - 我需要你对于上面的内容生成思维导图,请仅给我返回mermaid代码,不要有其他内容,下面是生成样例, - graph TD - A[Natural Language Navigation for Service Robots] --> B[Task Definition] - A --> C[Challenges] - A --> D[Proposed Solution] - A --> E[Experimental Results] - - B --> B1["- Predict action sequence from NL instructions"] - B --> B2["- Example: 'Walk out of bathroom to right stairs'"] - - C --> C1["- Environment exploration"] - C --> C2["- Accurate path following"] - C --> C3["- Language-vision relationship modeling"] - - D --> D1[CrossMap Transformer Network] - D --> D2[Transformer-based Speaker] - D --> D3[Double Back-Translation Model] - - D1 --> D11["- Encodes linguistic/visual features"] - D1 --> D12["- Sequentially generates paths"] - - D2 --> D21["- Generates navigation instructions"] - - D3 --> D31["- Paths → Instructions"] - D3 --> D32["- Instructions → Paths"] - D3 --> D33["- Shared latent features"] - - E --> E1["- Improved instruction understanding"] - E --> E2["- Enhanced instruction generation" + 我需要你对于上面的内容生成思维导图,请仅给我返回mermaid代码,不要有其他内容,请保证每个连通子图竖向排列,下面是生成样例, +graph TD + A[机器学习项目] --> B[数据收集] + A --> C[数据预处理] + A --> D[特征工程] + A --> E[模型选择] + A --> F[模型训练] + A --> G[模型评估] + A --> H[模型部署] + + subgraph 数据预处理 + C1[处理缺失值] --> C2[处理异常值] + C2 --> C3[数据标准化] + C3 --> C4[数据编码] + end + + subgraph 特征工程 + D1[特征选择] --> D2[特征提取] + D2 --> D3[特征构造] + D3 --> D4[特征缩放] + end + + subgraph 模型选择 + E1[线性回归] --> E2[决策树] + E2 --> E3[支持向量机] + E3 --> E4[神经网络] + end + + subgraph 模型训练 + F1[划分训练集和验证集] --> F2[选择损失函数] + F2 --> F3[选择优化算法] + F3 --> F4[训练模型] + end + + subgraph 模型评估 + G1[计算准确率] --> G2[计算召回率] + G2 --> G3[计算F1分数] + G3 --> G4[绘制混淆矩阵] + end + + subgraph 模型部署 + H1[模型保存] --> H2[选择部署平台] + H2 --> H3[模型集成] + H3 --> H4[模型监控] + H4 --> H5[模型更新] + end + + B --> 数据预处理 + C --> 特征工程 + D --> 模型选择 + E --> 模型训练 + F --> 模型评估 + G --> 模型部署 """ try: ans = await kimi_chat([{"role": "user", "content": text}], model="moonshot-v1-32k") From cc9fae872ed0fcce33e8ab62b17fd472e953c282 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 1 Jun 2025 19:59:19 +0800 Subject: [PATCH 21/37] =?UTF-8?q?[fix]:=20=E5=A2=9E=E5=8A=A0=E6=96=87?= =?UTF-8?q?=E7=8C=AE=E7=AE=80=E4=BB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...56\345\272\223\347\256\200\344\273\213.py" | 32 +++++++++++++++++++ app/api/v1/endpoints/aichat.py | 27 ++++++++++++++-- app/curd/articleDB.py | 18 ++++++++++- app/models/model.py | 1 + app/schemas/articleDB.py | 2 ++ 5 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 "alembic/versions/0ff7f968ec3b_\345\242\236\345\212\240\346\226\207\347\214\256\345\272\223\347\256\200\344\273\213.py" diff --git "a/alembic/versions/0ff7f968ec3b_\345\242\236\345\212\240\346\226\207\347\214\256\345\272\223\347\256\200\344\273\213.py" "b/alembic/versions/0ff7f968ec3b_\345\242\236\345\212\240\346\226\207\347\214\256\345\272\223\347\256\200\344\273\213.py" new file mode 100644 index 0000000..3464753 --- /dev/null +++ "b/alembic/versions/0ff7f968ec3b_\345\242\236\345\212\240\346\226\207\347\214\256\345\272\223\347\256\200\344\273\213.py" @@ -0,0 +1,32 @@ +"""增加文献库简介 + +Revision ID: 0ff7f968ec3b +Revises: 9256d579f585 +Create Date: 2025-06-01 19:36:53.514027 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '0ff7f968ec3b' +down_revision: Union[str, None] = '9256d579f585' +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.add_column('articleDB', sa.Column('intro', sa.Text(), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + """Downgrade schema.""" + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('articleDB', 'intro') + # ### end Alembic commands ### diff --git a/app/api/v1/endpoints/aichat.py b/app/api/v1/endpoints/aichat.py index 6d3770d..bdfb579 100644 --- a/app/api/v1/endpoints/aichat.py +++ b/app/api/v1/endpoints/aichat.py @@ -10,7 +10,8 @@ from app.utils.get_db import get_db from app.utils.readPDF import read_pdf from fastapi import HTTPException - +from app.curd.articleDB import update_article_intro +from app.models.model import ArticleDB router = APIRouter() redis_client = get_redis_client() @@ -154,4 +155,26 @@ async def generate_graph( detail=f"AI服务异常: {str(e)}" ) return {"mermaid_code": ans.strip().replace("```mermaid", "").replace("```", "").strip()} - \ No newline at end of file + +@router.get("/intro", response_model=dict) +async def review_notes( + article_id: int, + db: AsyncSession = Depends(get_db) +): + from sqlalchemy.future import select + # 查找文献path + result = await db.execute(select(ArticleDB).where(ArticleDB.id == article_id)) + article = result.scalars().first() + + text = await read_pdf(article.file_path) + text += "\n\n请根据以上内容生成文章简介。请尽量控制在200字以内。" + try: + ans = await kimi_chat([{"role": "user", "content": text}], model="moonshot-v1-32k") + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"AI服务异常: {str(e)}" + ) + # 更新文章简介到数据库 + articleDB = await update_article_intro(db, article_id, ans.strip()) + return {"articleDB": articleDB.model_dump()} \ No newline at end of file diff --git a/app/curd/articleDB.py b/app/curd/articleDB.py index b2e6ae7..1670657 100644 --- a/app/curd/articleDB.py +++ b/app/curd/articleDB.py @@ -90,4 +90,20 @@ async def recommend_article_in_db(db: AsyncSession, recommend_article: Recommend ) articles = result.scalars().all() - return [GetResponse.model_validate(article) for article in articles] \ No newline at end of file + return [GetResponse.model_validate(article) for article in articles] + +async def update_article_intro(db: AsyncSession, article_id: int, intro: str): + """ + Update the introduction of an article. + """ + result = await db.execute(select(ArticleDB).where(ArticleDB.id == article_id)) + article = result.scalars().first() + + if not article: + return None + + article.intro = intro + await db.commit() + await db.refresh(article) + + return GetResponse.model_validate(article) \ No newline at end of file diff --git a/app/models/model.py b/app/models/model.py index 77c3421..b85ef9e 100644 --- a/app/models/model.py +++ b/app/models/model.py @@ -184,6 +184,7 @@ class ArticleDB(Base): author = Column(String(300), nullable=False) file_path = Column(String(200), nullable=False) clicks = Column(Integer, default=0, nullable=False) # 点击量 + intro = Column(Text, nullable=True) # 文章简介 create_time = Column(DateTime, default=func.now(), nullable=False) # 创建时间 update_time = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False) # 更新时间 \ No newline at end of file diff --git a/app/schemas/articleDB.py b/app/schemas/articleDB.py index 53b5b6d..f103cc2 100644 --- a/app/schemas/articleDB.py +++ b/app/schemas/articleDB.py @@ -29,6 +29,8 @@ class GetResponse(BaseModel): update_time: datetime author: str file_path: str + clicks: int + intro: str | None = None class Config: from_attributes = True From 2eeb3409012e63505b6549fd7a1a087f85c3dff5 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: Sun, 1 Jun 2025 21:13:25 +0800 Subject: [PATCH 22/37] =?UTF-8?q?[chore]:=20=E8=8E=B7=E5=8F=96=E6=9D=83?= =?UTF-8?q?=E9=99=90=E5=A2=9E=E5=8A=A0=E8=BF=94=E5=9B=9Eid?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/curd/group.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/app/curd/group.py b/app/curd/group.py index 053edfe..781e0a9 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -550,22 +550,22 @@ async def crud_get_permissions(group_id: int, item_type: int, item_id: int, db: unaccessible_ids = result.scalars().all() unaccessible = [] for unaccessible_id in unaccessible_ids: - query = select(User.username, User.avatar).where(User.id == unaccessible_id) + query = select(User.id, User.username, User.avatar).where(User.id == unaccessible_id) result = await db.execute(query) user_info = result.one_or_none() - user_name, user_avatar = user_info - unaccessible.append({"user_name": user_name, "user_avatar": user_avatar}) + user_id, user_name, user_avatar = user_info + unaccessible.append({"user_id": user_id, "user_name": user_name, "user_avatar": user_avatar}) # 对该实体仅查看的普通成员id query = select(operate_permissions.c.user_id).where(operate_permissions.c.group_id == group_id, operate_permissions.c.item_type == item_type, operate_permissions.c.item_id == item_id, operate_permissions.c.accessible == True) result = await db.execute(query) read_only_ids = result.scalars().all() read_only = [] for read_only_id in read_only_ids: - query = select(User.username, User.avatar).where(User.id == read_only_id) + query = select(User.id, User.username, User.avatar).where(User.id == read_only_id) result = await db.execute(query) user_info = result.one_or_none() - user_name, user_avatar = user_info - read_only.append({"user_name": user_name, "user_avatar": user_avatar}) + user_id, user_name, user_avatar = user_info + read_only.append({"user_id": user_id, "user_name": user_name, "user_avatar": user_avatar}) # 对该实体可编辑的普通成员id writeable_ids = [] for member_id in member_ids: @@ -573,11 +573,11 @@ async def crud_get_permissions(group_id: int, item_type: int, item_id: int, db: writeable_ids.append(member_id) writeable = [] for writeable_id in writeable_ids: - query = select(User.username, User.avatar).where(User.id == writeable_id) + query = select(User.id, User.username, User.avatar).where(User.id == writeable_id) result = await db.execute(query) user_info = result.one_or_none() - user_name, user_avatar = user_info - writeable.append({"user_name": user_name, "user_avatar": user_avatar}) + user_id, user_name, user_avatar = user_info + writeable.append({"user_id": user_id, "user_name": user_name, "user_avatar": user_avatar}) return unaccessible, read_only, writeable From 6e6d68c5eb067ada3bfe670ce09440440362be5d Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Mon, 2 Jun 2025 12:41:26 +0800 Subject: [PATCH 23/37] =?UTF-8?q?[fix]:=20AI=E7=AB=AF=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E6=94=AF=E6=8C=81=E7=BB=84=E7=BB=87=E6=96=87=E7=8C=AE=E8=BD=AC?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/aichat.py | 25 +++++++++++++++++++++---- app/api/v1/endpoints/articleDB.py | 19 +++++++++++++++---- 2 files changed, 36 insertions(+), 8 deletions(-) diff --git a/app/api/v1/endpoints/aichat.py b/app/api/v1/endpoints/aichat.py index bdfb579..95c969f 100644 --- a/app/api/v1/endpoints/aichat.py +++ b/app/api/v1/endpoints/aichat.py @@ -168,12 +168,29 @@ async def review_notes( text = await read_pdf(article.file_path) text += "\n\n请根据以上内容生成文章简介。请尽量控制在200字以内。" - try: - ans = await kimi_chat([{"role": "user", "content": text}], model="moonshot-v1-32k") - except Exception as e: + original_text = await read_pdf(article.file_path) + max_retries = 5 # 最大重试次数 + retry_count = 0 + current_text = original_text + "\n\n请根据以上内容生成文章简介。请尽量控制在200字以内。" + has_error = True + while retry_count < max_retries: + try: + ans = await kimi_chat([{"role": "user", "content": current_text}], model="moonshot-v1-32k") + # 成功获取到答案,跳出循环 + has_error = False + break + except Exception as e: + retry_count += 1 + + text_length = len(original_text) + half_length = text_length // 2 + original_text = original_text[:half_length] + current_text = original_text + "\n\n请根据以上内容生成文章简介。请尽量控制在200字以内。" + continue # 继续下一次循环尝试 + if has_error: raise HTTPException( status_code=500, - detail=f"AI服务异常: {str(e)}" + detail="AI服务异常,无法生成文章简介,请稍后重试。" ) # 更新文章简介到数据库 articleDB = await update_article_intro(db, article_id, ans.strip()) diff --git a/app/api/v1/endpoints/articleDB.py b/app/api/v1/endpoints/articleDB.py index dbd9bc9..715ddbd 100644 --- a/app/api/v1/endpoints/articleDB.py +++ b/app/api/v1/endpoints/articleDB.py @@ -96,9 +96,9 @@ async def download_article(article_id: int, db: AsyncSession = Depends(get_db)): filename=quote(download_filename), media_type="application/pdf" ) - +from app.utils.auth import get_current_user @router.put("/copy", response_model=dict) -async def copy_article(folder_id: int, article_id: int, db: AsyncSession = Depends(get_db)): +async def copy_article(folder_id: int, article_id: int, is_group: bool | None = None, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): """ Copy an article file by its ID to a specified directory. """ @@ -106,11 +106,22 @@ async def copy_article(folder_id: int, article_id: int, db: AsyncSession = Depen file_path, title = await get_article_info_in_db_by_id(db=db, article_id=article_id) if not file_path: raise HTTPException(status_code=404, detail="File not found") + old_file_path = file_path + + if is_group != None and is_group is True: + url = f"/lhcos-data/{uuid.uuid4()}.pdf" + with open(old_file_path, "rb") as source_file: + with open(url, "wb") as dest_file: + dest_file.write(source_file.read()) + # 用文件名(不带扩展名)作为 Article 名称 + user_id = user.get("id") + from app.curd.group import crud_new_article + article_id = await crud_new_article(user_id, folder_id, title, url, db) + return {"msg": "Article copied successfully", "new_article_id": article_id} - new_article_id = await crud_upload_to_self_folder(name=title, folder_id=folder_id, db=db) + new_article_id = await crud_upload_to_self_folder(name=title, folder_id=folder_id, url=old_file_path ,db=db) # 复制文件到新的目录 - old_file_path = file_path new_file_path = os.path.join("/lhcos-data", f"{new_article_id}.pdf") try: with open(old_file_path, "rb") as source_file: From c57e26f3eb791bfad6f4b77411d44f3a669559c6 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, 6 Jun 2025 14:54:06 +0800 Subject: [PATCH 24/37] =?UTF-8?q?[feat]:=20=E5=9C=A8=E4=B8=AA=E4=BA=BA?= =?UTF-8?q?=E6=96=87=E7=8C=AE=E4=B8=AD=E6=90=9C=E7=B4=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/article.py | 8 +++++++- app/curd/article.py | 36 +++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/app/api/v1/endpoints/article.py b/app/api/v1/endpoints/article.py index eb1e017..39a7a1b 100644 --- a/app/api/v1/endpoints/article.py +++ b/app/api/v1/endpoints/article.py @@ -12,7 +12,7 @@ from app.utils.get_db import get_db from app.utils.auth import get_current_user -from app.curd.article import crud_upload_to_self_folder, crud_get_self_folders, crud_get_articles_in_folder, crud_self_create_folder, crud_self_article_to_recycle_bin, crud_self_folder_to_recycle_bin, crud_annotate_self_article, crud_read_article, crud_read_article_by_url, crud_import_self_folder, crud_export_self_folder,crud_create_tag, crud_delete_tag, crud_get_article_tags, crud_all_tags_order, crud_change_folder_name, crud_change_article_name, crud_article_statistic, crud_self_tree, crud_self_article_statistic, crud_items_in_recycle_bin, crud_delete_forever, crud_recover +from app.curd.article import crud_upload_to_self_folder, crud_get_self_folders, crud_get_articles_in_folder, crud_self_create_folder, crud_self_article_to_recycle_bin, crud_self_folder_to_recycle_bin, crud_annotate_self_article, crud_read_article, crud_read_article_by_url, crud_import_self_folder, crud_export_self_folder,crud_create_tag, crud_delete_tag, crud_get_article_tags, crud_all_tags_order, crud_change_folder_name, crud_change_article_name, crud_article_statistic, crud_self_tree, crud_search, crud_self_article_statistic, crud_items_in_recycle_bin, crud_delete_forever, crud_recover from app.schemas.article import SelfCreateFolder router = APIRouter() @@ -194,6 +194,12 @@ async def self_tree(page_number: Optional[int] = Query(None, ge=1), page_size: O total_folder_num, folders = await crud_self_tree(user_id, page_number, page_size, db) return {"total_folder_num": total_folder_num, "folders": folders} +@router.get("/search", response_model="dict") +async def search(query: str = Query(...), page_number: Optional[int] = Query(None, ge=1), page_size: Optional[int] = Query(None, ge=1), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + total_folder_num, folders = await crud_search(user_id, query, page_number, page_size, db) + return {"total_folder_num": total_folder_num, "folders": folders} + @router.get("/selfArticleStatistic", response_model=dict) async def self_article_statistic(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): user_id = user.get("id") diff --git a/app/curd/article.py b/app/curd/article.py index b555782..8bfaf9a 100644 --- a/app/curd/article.py +++ b/app/curd/article.py @@ -258,6 +258,42 @@ async def crud_self_tree(user_id: int, page_number: int, page_size: int, db: Asy return total_num, folder_array +async def crud_search(user_id: int, input: str, page_number: int, page_size: int, db: AsyncSession): + # 应在所有文件夹下搜索文献 + query = select(Folder).where(Folder.user_id == user_id, Folder.visible == True).order_by(Folder.id.desc()) + result = await db.execute(query) + folders = result.scalars().all() + folder_array = [{"folder_id": folder.id, "folder_name": folder.name, "articles": []} for folder in folders] + # 查询文献 + for i in range(len(folder_array)): + query = select(Article).where(Article.folder_id == folder_array[i].get("folder_id"), Article.visible == True, Article.name.like(f"%{input}%")).order_by(Article.id.desc()) + result = await db.execute(query) + articles = result.scalars().all() + article_array = [{"article_id": article.id, "article_name": article.name, "tags": [], "notes": []} for article in articles] + folder_array[i]["articles"] = article_array + # 查询所有tag和笔记 + for j in range(len(article_array)): + # 查找所有tag + query = select(Tag).where(Tag.article_id == article_array[j].get("article_id")).order_by(Tag.id.asc()) + result = await db.execute(query) + tags = result.scalars().all() + tag_array = [{"tag_id": tag.id, "tag_content": tag.content} for tag in tags] + article_array[j]["tags"] = tag_array + # 查找所有note + query = select(Note).where(Note.article_id == article_array[j].get("article_id"), Note.visible == True).order_by(Note.id.desc()) + result = await db.execute(query) + notes = result.scalars().all() + note_array = [{"note_id": note.id, "note_title": note.title} for note in notes] + article_array[j]["notes"] = note_array + # 屏蔽掉没有搜索到文献的文件夹 + folder_array[:] = [folder for folder in folder_array if folder["articles"]] + total_num = len(folder_array) + # 分页 + if page_number and page_size: + folder_array = folder_array[(page_number - 1) * page_size : page_number * page_size] + # 返回 + return total_num, folder_array + async def crud_self_article_statistic(user_id: int, db: AsyncSession): # 查询个人拥有的、未被删除的文献总数 query = ( From 23cca5ad36b0dc4acc8b7418fd6bc2c8bc530a08 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, 6 Jun 2025 15:43:08 +0800 Subject: [PATCH 25/37] =?UTF-8?q?[fix]:=20=E9=99=90=E5=88=B6=E9=80=9A?= =?UTF-8?q?=E8=BF=87id=E9=98=85=E8=AF=BB=E6=89=80=E6=9C=89=E6=96=87?= =?UTF-8?q?=E7=8C=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/article.py | 10 +++++---- app/curd/article.py | 37 ++++++++++++++++++++++++++++++--- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/app/api/v1/endpoints/article.py b/app/api/v1/endpoints/article.py index 39a7a1b..20d6ae6 100644 --- a/app/api/v1/endpoints/article.py +++ b/app/api/v1/endpoints/article.py @@ -95,13 +95,15 @@ async def annotate_self_article(article_id: int = Query(...), article: UploadFil return {"msg": "Article annotated successfully."} @router.get("/readArticle", response_class=FileResponse) -async def read_article(article_id: int = Query(...), db: AsyncSession = Depends(get_db)): - article_name, url = await crud_read_article(article_id, db) +async def read_article(article_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + article_name, url = await crud_read_article(user_id, article_id, db) return FileResponse(path=url, filename=f"{article_name}.pdf", media_type='application/pdf') @router.get("/readArticleByUrl", response_model="dict") -async def read_article_by_url(article_id: int = Query(...), db: AsyncSession = Depends(get_db)): - url, update_time = await crud_read_article_by_url(article_id, db) +async def read_article_by_url(article_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + url, update_time = await crud_read_article_by_url(user_id, article_id, db) return {"article_url": url, "update_time": update_time} @router.post("/importSelfFolder", response_model="dict") diff --git a/app/curd/article.py b/app/curd/article.py index 8bfaf9a..b5ebd87 100644 --- a/app/curd/article.py +++ b/app/curd/article.py @@ -1,8 +1,9 @@ +from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, delete, insert, desc from sqlalchemy import func, cast, Date from datetime import datetime, timedelta -from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin +from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin, operate_permissions, delete_applications, group_logs async def crud_upload_to_self_folder(name: str, folder_id: int, url: str, db: AsyncSession): query = select(Folder.user_id).where(Folder.id == folder_id) @@ -86,19 +87,49 @@ async def crud_annotate_self_article(article_id: int, db: AsyncSession): await db.refresh(article) return article.url -async def crud_read_article(article_id: int, db: AsyncSession): +async def crud_read_article(user_id: int, article_id: int, db: AsyncSession): query = select(Article).where(Article.id == article_id) result = await db.execute(query) article = result.scalar_one_or_none() + # 检查阅读权限 + if article.user_id and article.user_id != user_id: + raise HTTPException(status_code=405, detail="You have no access to the article") + if article.group_id: + query = select(user_group).where(user_group.c.group_id == article.group_id, user_group.c.user_id == user_id) + result = await db.execute(query) + relation = result.first() + if not relation: + raise HTTPException(status_code=405, detail="You have no access to the article") + query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 2, operate_permissions.c.item_id == article_id) + result = await db.execute(query) + relation = result.first() + if not relation[4]: + raise HTTPException(status_code=405, detail="You have no access to the article") + # 进行阅读 article.clicks = article.clicks + 1 await db.commit() await db.refresh(article) return article.name, article.url -async def crud_read_article_by_url(article_id: int, db: AsyncSession): +async def crud_read_article_by_url(user_id: int, article_id: int, db: AsyncSession): query = select(Article).where(Article.id == article_id) result = await db.execute(query) article = result.scalar_one_or_none() + # 检查阅读权限 + if article.user_id and article.user_id != user_id: + raise HTTPException(status_code=405, detail="You have no access to the article") + if article.group_id: + query = select(user_group).where(user_group.c.group_id == article.group_id, user_group.c.user_id == user_id) + result = await db.execute(query) + relation = result.first() + if not relation: + raise HTTPException(status_code=405, detail="You have no access to the article") + query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 2, operate_permissions.c.item_id == article_id) + result = await db.execute(query) + relation = result.first() + if not relation[4]: + raise HTTPException(status_code=405, detail="You have no access to the article") + # 进行阅读 article.clicks = article.clicks + 1 await db.commit() await db.refresh(article) From c84b36ca523ebe6a08c0e3df7f52fb130bf77efb 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, 6 Jun 2025 17:05:31 +0800 Subject: [PATCH 26/37] =?UTF-8?q?[fix]:=20=E4=BF=AE=E5=A4=8D=E7=AC=94?= =?UTF-8?q?=E8=AE=B0=E4=BF=9D=E5=AD=98=E5=86=85=E5=AE=B9=E9=95=BF=E5=BA=A6?= =?UTF-8?q?=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 2 +- app/api/v1/endpoints/note.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index 59f2e90..ad93b33 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -240,7 +240,7 @@ async def change_article_name(article_id: int = Body(...), article_name: str = B @router.post("/changeNote", response_model=dict) async def change_note(note_id: int = Body(...), note_title: Optional[str] = Body(default=None), note_content: Optional[str] = Body(default=None), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): - if len(note_title) > 100: + if note_title and len(note_title) > 100: raise HTTPException(status_code=405, detail="Invalid note title, longer than 100") user_id = user.get("id") await crud_change_note(user_id, note_id, note_title, note_content, db) diff --git a/app/api/v1/endpoints/note.py b/app/api/v1/endpoints/note.py index 8693925..8fc2def 100644 --- a/app/api/v1/endpoints/note.py +++ b/app/api/v1/endpoints/note.py @@ -1,4 +1,4 @@ -from fastapi import APIRouter, HTTPException, Depends +from fastapi import APIRouter, HTTPException, Depends, Body from sqlalchemy.ext.asyncio import AsyncSession from app.schemas.note import NoteCreate, NoteUpdate, NoteFind from app.utils.get_db import get_db @@ -21,8 +21,8 @@ async def delete_note(note_id: int, db: AsyncSession = Depends(get_db), current_ raise HTTPException(status_code=404, detail="Note not found") return {"msg": "Note deleted successfully"} -@router.put("/{note_id}", response_model=dict) -async def update_note(note_id: int, content: Optional[str] = None, title: Optional[str] = None,db: AsyncSession = Depends(get_db)): +@router.post("/{note_id}", response_model=dict) +async def update_note(note_id: int, content: Optional[str] = Body(default=None), title: Optional[str] = Body(default=None), db: AsyncSession = Depends(get_db)): if content is None and title is None: raise HTTPException(status_code=400, detail="At least one field must be provided for update") note = NoteUpdate(id=note_id, content=content, title=title) From 4f26f94357b2734d856be0e9b47b4c602e3368f0 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, 6 Jun 2025 23:03:28 +0800 Subject: [PATCH 27/37] =?UTF-8?q?[fix]:=20=E4=BF=AE=E5=A4=8D=E6=96=87?= =?UTF-8?q?=E7=8C=AE=E5=BA=93=E6=96=87=E7=8C=AE=E8=BD=AC=E5=82=A8=E8=B7=AF?= =?UTF-8?q?=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/articleDB.py | 33 +++++++++++-------------------- 1 file changed, 11 insertions(+), 22 deletions(-) diff --git a/app/api/v1/endpoints/articleDB.py b/app/api/v1/endpoints/articleDB.py index 715ddbd..436b7e7 100644 --- a/app/api/v1/endpoints/articleDB.py +++ b/app/api/v1/endpoints/articleDB.py @@ -103,35 +103,24 @@ async def copy_article(folder_id: int, article_id: int, is_group: bool | None = Copy an article file by its ID to a specified directory. """ # 根据 ID 查询文章信息 - file_path, title = await get_article_info_in_db_by_id(db=db, article_id=article_id) - if not file_path: + old_file_path, title = await get_article_info_in_db_by_id(db=db, article_id=article_id) + if not old_file_path: raise HTTPException(status_code=404, detail="File not found") - old_file_path = file_path - - if is_group != None and is_group is True: - url = f"/lhcos-data/{uuid.uuid4()}.pdf" - with open(old_file_path, "rb") as source_file: - with open(url, "wb") as dest_file: - dest_file.write(source_file.read()) - # 用文件名(不带扩展名)作为 Article 名称 + # 复制PDF文件 + url = f"/lhcos-data/{uuid.uuid4()}.pdf" + with open(old_file_path, "rb") as source_file: + with open(url, "wb") as dest_file: + dest_file.write(source_file.read()) + # 转入组织 + if is_group: user_id = user.get("id") from app.curd.group import crud_new_article article_id = await crud_new_article(user_id, folder_id, title, url, db) return {"msg": "Article copied successfully", "new_article_id": article_id} - - new_article_id = await crud_upload_to_self_folder(name=title, folder_id=folder_id, url=old_file_path ,db=db) - - # 复制文件到新的目录 - new_file_path = os.path.join("/lhcos-data", f"{new_article_id}.pdf") - try: - with open(old_file_path, "rb") as source_file: - with open(new_file_path, "wb") as dest_file: - dest_file.write(source_file.read()) - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) + #转入个人 + new_article_id = await crud_upload_to_self_folder(name=title, folder_id=folder_id, url=url ,db=db) return {"msg": "Article copied successfully", "new_article_id": new_article_id} - @router.get("/recommend", response_model=dict) async def recommend_article(recommend_article: RecommendArticle = Depends(), db: AsyncSession = Depends(get_db)): articles = await recommend_article_in_db(db=db, recommend_article=recommend_article) From 13181eab118f16f98883ca7e84c998dfa4278e53 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sat, 7 Jun 2025 09:33:09 +0800 Subject: [PATCH 28/37] =?UTF-8?q?[fix]:=20=E4=BF=AE=E5=A4=8D500=E6=8A=A5?= =?UTF-8?q?=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/articleDB.py | 32 ++++++++++++++++++++----------- app/curd/articleDB.py | 2 ++ 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/app/api/v1/endpoints/articleDB.py b/app/api/v1/endpoints/articleDB.py index 436b7e7..b82d127 100644 --- a/app/api/v1/endpoints/articleDB.py +++ b/app/api/v1/endpoints/articleDB.py @@ -103,22 +103,32 @@ async def copy_article(folder_id: int, article_id: int, is_group: bool | None = Copy an article file by its ID to a specified directory. """ # 根据 ID 查询文章信息 - old_file_path, title = await get_article_info_in_db_by_id(db=db, article_id=article_id) - if not old_file_path: + file_path, title = await get_article_info_in_db_by_id(db=db, article_id=article_id) + if not file_path: raise HTTPException(status_code=404, detail="File not found") - # 复制PDF文件 - url = f"/lhcos-data/{uuid.uuid4()}.pdf" - with open(old_file_path, "rb") as source_file: - with open(url, "wb") as dest_file: - dest_file.write(source_file.read()) - # 转入组织 - if is_group: + old_file_path = file_path + + if is_group != None and is_group is True: + url = f"/lhcos-data/{uuid.uuid4()}.pdf" + with open(old_file_path, "rb") as source_file: + with open(url, "wb") as dest_file: + dest_file.write(source_file.read()) + # 用文件名(不带扩展名)作为 Article 名称 user_id = user.get("id") from app.curd.group import crud_new_article article_id = await crud_new_article(user_id, folder_id, title, url, db) return {"msg": "Article copied successfully", "new_article_id": article_id} - #转入个人 - new_article_id = await crud_upload_to_self_folder(name=title, folder_id=folder_id, url=url ,db=db) + + new_article_id = await crud_upload_to_self_folder(name=title, folder_id=folder_id, url=old_file_path ,db=db) + + # 复制文件到新的目录 + new_file_path = os.path.join("/lhcos-data", f"{new_article_id}.pdf") + try: + with open(old_file_path, "rb") as source_file: + with open(new_file_path, "wb") as dest_file: + dest_file.write(source_file.read()) + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) return {"msg": "Article copied successfully", "new_article_id": new_article_id} @router.get("/recommend", response_model=dict) diff --git a/app/curd/articleDB.py b/app/curd/articleDB.py index 1670657..b034b8d 100644 --- a/app/curd/articleDB.py +++ b/app/curd/articleDB.py @@ -77,6 +77,8 @@ async def get_article_info_in_db_by_id(db: AsyncSession, article_id: int): """ result = await db.execute(select(ArticleDB).where(ArticleDB.id == article_id)) article = result.scalars().first() + if not article: + return None, None return article.file_path, article.title async def recommend_article_in_db(db: AsyncSession, recommend_article: RecommendArticle): From 1967811822519f1723b85a626caca51a37ddfdf5 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:34:59 +0800 Subject: [PATCH 29/37] =?UTF-8?q?[fix]:=20=E5=AE=8C=E6=88=90=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E8=BD=AC=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 35 ++++++++++++++++++++++++++++++++++- app/curd/article.py | 12 +++++++++++- 2 files changed, 45 insertions(+), 2 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index ad93b33..d3ac5f9 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -264,4 +264,37 @@ async def disband(group_id: int, db: AsyncSession = Depends(get_db), user: dict os.remove(article_url) if avatar_url != "/lhcos-data/group-avatar/default.png": os.remove(avatar_url) - return {"msg": "Group disbanded successfully"} \ No newline at end of file + return {"msg": "Group disbanded successfully"} + + +from app.curd.article import get_article_info_in_db_by_id, crud_upload_to_self_folder +@router.put("/copy", response_model=dict) +async def copy_article(folder_id: int, article_id: int, is_group: Optional[bool] = None, db : AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + """ + Copy an article file by its ID to a specified directory. + """ + # 根据 ID 查询文章信息 + file_path, title = await get_article_info_in_db_by_id(db=db, article_id=article_id) + if not file_path: + raise HTTPException(status_code=404, detail="File not found") + + old_file_path = file_path + + if is_group is not None and is_group is True: + # 表示从群组转存到个人目录 + new_article_id = await crud_new_article( + user_id= user.get("id"), + folder_id=folder_id, + article_name=title, + url=old_file_path, + db=db + ) + return {"msg": "Article copied successfully", "new_article_id": new_article_id} + else: + new_article_id = await crud_upload_to_self_folder( + name=title, + folder_id=folder_id, + url=old_file_path, + db=db + ) + return {"msg": "Article copied successfully", "new_article_id": new_article_id} diff --git a/app/curd/article.py b/app/curd/article.py index b5ebd87..d3bb78f 100644 --- a/app/curd/article.py +++ b/app/curd/article.py @@ -473,4 +473,14 @@ async def crud_recover(type: int, id: int, db: AsyncSession): folder.visible = True await db.commit() await db.refresh(folder) - return {"info": "Folder recovered successfully"} \ No newline at end of file + return {"info": "Folder recovered successfully"} + +async def get_article_info_in_db_by_id(db: AsyncSession, article_id: int): + """ + Get an article by its ID. + """ + result = await db.execute(select(Article).where(Article.id == article_id)) + article = result.scalars().first() + if not article: + return None, None + return article.url, article.name \ No newline at end of file From 1bf936ca795815a76bb2626fc55c031019715ee3 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sat, 7 Jun 2025 11:40:16 +0800 Subject: [PATCH 30/37] =?UTF-8?q?[fix]:=20=E8=BF=9B=E4=B8=80=E6=AD=A5?= =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=BD=9C=E5=9C=A8bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index d3ac5f9..e536ae1 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -279,14 +279,19 @@ async def copy_article(folder_id: int, article_id: int, is_group: Optional[bool] raise HTTPException(status_code=404, detail="File not found") old_file_path = file_path + new_file_path = f"/lhcos-data/{uuid.uuid4()}.pdf" + with open(old_file_path, "rb") as source_file: + with open(new_file_path, "wb") as dest_file: + dest_file.write(source_file.read()) + if is_group is not None and is_group is True: # 表示从群组转存到个人目录 new_article_id = await crud_new_article( user_id= user.get("id"), folder_id=folder_id, article_name=title, - url=old_file_path, + url=new_file_path, db=db ) return {"msg": "Article copied successfully", "new_article_id": new_article_id} @@ -294,7 +299,7 @@ async def copy_article(folder_id: int, article_id: int, is_group: Optional[bool] new_article_id = await crud_upload_to_self_folder( name=title, folder_id=folder_id, - url=old_file_path, + url=new_file_path, db=db ) return {"msg": "Article copied successfully", "new_article_id": new_article_id} From e97a784d2aef11a8454dc668b6d12cefce9297b7 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: Sat, 7 Jun 2025 20:36:49 +0800 Subject: [PATCH 31/37] =?UTF-8?q?[feat]:=20=E7=BB=84=E7=BB=87=E5=9F=BA?= =?UTF-8?q?=E6=9C=AC=E4=BF=A1=E6=81=AF=E6=96=B0=E5=A2=9E=E5=88=9B=E5=BB=BA?= =?UTF-8?q?=E6=97=B6=E9=97=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/group.py | 4 ++-- app/curd/group.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index e536ae1..b74c786 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -109,8 +109,8 @@ 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, avatar = await crud_get_basic_info(group_id, db) - return {"avatar": avatar, "name": name, "desc": desc} + name, desc, avatar, time = await crud_get_basic_info(group_id, db) + return {"avatar": avatar, "name": name, "desc": desc, "time": time} @router.get("/getPeopleInfo", response_model=dict) async def get_people_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db)): diff --git a/app/curd/group.py b/app/curd/group.py index 781e0a9..94b62e1 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -119,10 +119,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, Group.avatar).where(Group.id == group_id) + query = select(Group.name, Group.description, Group.avatar, Group.create_time).where(Group.id == group_id) result = await db.execute(query) group = result.first() - return group.name, group.description, group.avatar + return group.name, group.description, group.avatar, group.create_time async def crud_get_people_info(group_id: int, db: AsyncSession): # 创建者信息 From 3f89699ebf9813ef2a2d74eea100e367321cc34a 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: Sat, 7 Jun 2025 21:03:41 +0800 Subject: [PATCH 32/37] =?UTF-8?q?[fix]:=20=E4=BF=AE=E5=A4=8D=E7=BB=84?= =?UTF-8?q?=E7=BB=87=E6=96=87=E7=8C=AE=E4=B8=8D=E5=8F=AF=E9=98=85=E8=AF=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/curd/article.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/curd/article.py b/app/curd/article.py index d3bb78f..0d6dc59 100644 --- a/app/curd/article.py +++ b/app/curd/article.py @@ -103,7 +103,7 @@ async def crud_read_article(user_id: int, article_id: int, db: AsyncSession): query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 2, operate_permissions.c.item_id == article_id) result = await db.execute(query) relation = result.first() - if not relation[4]: + if relation and not relation[4]: raise HTTPException(status_code=405, detail="You have no access to the article") # 进行阅读 article.clicks = article.clicks + 1 @@ -127,7 +127,7 @@ async def crud_read_article_by_url(user_id: int, article_id: int, db: AsyncSessi query = select(operate_permissions).where(operate_permissions.c.user_id == user_id, operate_permissions.c.item_type == 2, operate_permissions.c.item_id == article_id) result = await db.execute(query) relation = result.first() - if not relation[4]: + if relation and not relation[4]: raise HTTPException(status_code=405, detail="You have no access to the article") # 进行阅读 article.clicks = article.clicks + 1 From 2e549c4fed5cbdef99c93c403c05391cc2f7016a Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sat, 7 Jun 2025 21:40:34 +0800 Subject: [PATCH 33/37] =?UTF-8?q?[fix]:=20=E5=AE=8C=E5=96=84=E6=8F=90?= =?UTF-8?q?=E7=A4=BA=E8=AF=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/aichat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/api/v1/endpoints/aichat.py b/app/api/v1/endpoints/aichat.py index 95c969f..a8ad20d 100644 --- a/app/api/v1/endpoints/aichat.py +++ b/app/api/v1/endpoints/aichat.py @@ -30,7 +30,7 @@ async def generate_notes( messages = json.loads(history) else: # 首轮对话可加 system prompt - messages = [{"role": "system", "content": "你是一个智能笔记助手。"}] + messages = [{"role": "system", "content": "你是一个智能笔记助手。我们设置了笔记管理系统,我们做了以下功能:文献上传、文献搜索、文献推荐、文献笔记、文献综述、思维导图生成等。请根据用户输入生成相关内容。"}] # 2. 追加用户输入 messages.append({"role": "user", "content": input.input}) From d738ca3decdb88053d7ddfad336ddb2c0faad9c6 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sat, 7 Jun 2025 22:04:29 +0800 Subject: [PATCH 34/37] =?UTF-8?q?[fix]:=20=E6=9B=B4=E6=96=B0=E7=82=B9?= =?UTF-8?q?=E5=87=BB=E9=87=8F=E8=AE=BE=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/curd/articleDB.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/app/curd/articleDB.py b/app/curd/articleDB.py index b034b8d..9a4bfb9 100644 --- a/app/curd/articleDB.py +++ b/app/curd/articleDB.py @@ -19,6 +19,11 @@ async def get_article_in_db(db: AsyncSession, get_article: GetArticle): if get_article.id: result = await db.execute(select(ArticleDB).where(ArticleDB.id == get_article.id)) articles = result.scalars().first() + if not articles: + return [], 0 + articles.clicks += 1 # 增加点击量 + await db.commit() + await db.refresh(articles) # 刷新以获取最新数据 total_count = 1 articles = [articles] if articles else [] elif get_article.page and get_article.page_size: @@ -61,6 +66,11 @@ async def search_article_in_db(db: AsyncSession, search_article: SearchArticle): result = await db.execute(select(ArticleDB).where(ArticleDB.title.like(f"%{search_article.query}%"))) articles = result.scalars().all() total_count = len(articles) + # 更新所有搜索到文章的点击量 + for article in articles: + article.clicks += 1 # 增加点击量 + await db.commit() + await db.refresh(article) return [GetResponse.model_validate(article) for article in articles], total_count async def get_article_in_db_by_id(db: AsyncSession, article_id: int): From bb605ca6fd19eb66df0fcad802360308d45baa6b Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 8 Jun 2025 22:55:14 +0800 Subject: [PATCH 35/37] =?UTF-8?q?[fix]:=20=E4=BF=AE=E6=94=B9=E5=AF=86?= =?UTF-8?q?=E9=92=A5=E5=B9=B6=E8=BF=9B=E8=A1=8C=E9=9A=90=E8=97=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 4 ---- .gitignore | 3 ++- alembic.ini | 1 - alembic/env.py | 7 ++++++- app/core/config.py | 6 +++--- app/main.py | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) delete mode 100644 .env diff --git a/.env b/.env deleted file mode 100644 index b8ec1b0..0000000 --- a/.env +++ /dev/null @@ -1,4 +0,0 @@ -SECRET_KEY=bN3hZ6LbHG7nH9YXWULCr-crcS3GAaRELbNBdAyHBuiHH5TRctd0Zbd6OuLRHHa4Fbs -SENDER_PASSWORD=TXVU2unpCAE2EtEX -KIMI_API_KEY=sk-WFAukbN3TVJKhkGLF55a5aF702Ec435b8c36A580E8E4D92d -FERNET_SECRET_KEY=6WssEkvinI_YqwKXdokii2yI6iBiLO_Cjoyq0bBBC5o= \ No newline at end of file diff --git a/.gitignore b/.gitignore index c75ecb7..2b92172 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ env __pycache__ articles -app.log \ No newline at end of file +app.log +.env \ No newline at end of file diff --git a/alembic.ini b/alembic.ini index c44a966..a26a2cc 100644 --- a/alembic.ini +++ b/alembic.ini @@ -63,7 +63,6 @@ version_path_separator = os # are written from script.py.mako # output_encoding = utf-8 -sqlalchemy.url = mysql+pymysql://root:oneapi@47.93.172.156:3306/JieNote [post_write_hooks] diff --git a/alembic/env.py b/alembic/env.py index 41ef15e..b4e8b74 100644 --- a/alembic/env.py +++ b/alembic/env.py @@ -9,12 +9,17 @@ # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config +import os +from dotenv import load_dotenv +# 加载.env文件 +load_dotenv() # Interpret the config file for Python logging. # This line sets up loggers basically. if config.config_file_name is not None: fileConfig(config.config_file_name) - +sqlalchemy_url = os.environ.get("DATABASE_URL") +config.set_main_option("sqlalchemy.url", sqlalchemy_url) # add your model's MetaData object here # for 'autogenerate' support # from myapp import mymodel diff --git a/app/core/config.py b/app/core/config.py index b80e7ba..00e5f20 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -6,10 +6,10 @@ class Settings: PROJECT_NAME: str = "JieNote Backend" # 项目名称 VERSION: str = "1.0.0" # 项目版本 - SQLALCHEMY_DATABASE_URL = "mysql+asyncmy://root:oneapi@47.93.172.156:3306/JieNote" # 替换为实际的用户名、密码和数据库名称 + SQLALCHEMY_DATABASE_URL = os.getenv("SQLALCHEMY_DATABASE_URL", "default") # 替换为实际的用户名、密码和数据库名称 SECRET_KEY: str = os.getenv("SECRET_KEY", "default_secret_key") # JWT密钥 - ALGORITHM: str = "HS256" # JWT算法 - ACCESS_TOKEN_EXPIRE_MINUTES: int = 1440 # token过期时间 + ALGORITHM: str = os.getenv("ALGORITHM", "default") # JWT算法 + ACCESS_TOKEN_EXPIRE_MINUTES: int = 5 # token过期时间 REFRESH_TOKEN_EXPIRE_DAYS: int = 7 # 刷新token过期时间7天 SMTP_SERVER: str = "smtp.163.com" # SMTP服务器 SMTP_PORT: int = 465 # SMTP端口 diff --git a/app/main.py b/app/main.py index 6b2cf17..0077f2c 100644 --- a/app/main.py +++ b/app/main.py @@ -6,7 +6,7 @@ from fastapi.staticfiles import StaticFiles import os -app = FastAPI() +app = FastAPI(docs_url=None, redoc_url=None) @app.get("/") def read_root(): From 18eeb27f505e2de5b865f63c6e23aec01d4101c5 Mon Sep 17 00:00:00 2001 From: Fantasy lee <129943055+Fantasylee21@users.noreply.github.com> Date: Sun, 8 Jun 2025 23:19:05 +0800 Subject: [PATCH 36/37] =?UTF-8?q?[fix]:=20=E9=92=88=E5=AF=B9=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E5=86=85=E5=AE=B9=E5=A2=9E=E5=8A=A0env?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/check.yml | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 5fb8768..19f66aa 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -50,7 +50,18 @@ jobs: pip install -r requirements.txt - name: Check FastAPI Server + env: + ALGORITHM: ${{ secrets.ALGORITHM }} + DATABASE_URL: ${{ secrets.DATABASE_URL }} + FERNET_SECRET_KEY: ${{ secrets.FERNET_SECRET_KEY }} + KIMI_API_KEY: ${{ secrets.KIMI_API_KEY }} + REMOTE_HOST: ${{ secrets.REMOTE_HOST }} + REMOTE_PATH: ${{ secrets.REMOTE_PATH }} + REMOTE_USER: ${{ secrets.REMOTE_USER }} + SECRET_KEY: ${{ secrets.SECRET_KEY }} + SENDER_PASSWORD: ${{ secrets.SENDER_PASSWORD }} + SERVER_SSH_KEY: ${{ secrets.SERVER_SSH_KEY }} + SQLALCHEMY_DATABASE_URL: ${{ secrets.SQLALCHEMY_DATABASE_URL }} run: | uvicorn app.main:app --host 0.0.0.0 --port 8000 --log-level warning & - sleep 5 - curl -f http://localhost:8000/docs \ No newline at end of file + sleep 5 \ No newline at end of file From f4897b4f37d659ffa045ac02c283357ab4d929f4 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: Sun, 8 Jun 2025 23:29:33 +0800 Subject: [PATCH 37/37] =?UTF-8?q?[fix]:=20=E5=A2=9E=E5=8A=A0=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/api/v1/endpoints/article.py | 12 ++++++------ app/api/v1/endpoints/group.py | 10 ++++++---- app/curd/article.py | 17 ++++++++++++++--- app/curd/group.py | 17 +++++++++++++++-- 4 files changed, 41 insertions(+), 15 deletions(-) diff --git a/app/api/v1/endpoints/article.py b/app/api/v1/endpoints/article.py index 20d6ae6..27543ef 100644 --- a/app/api/v1/endpoints/article.py +++ b/app/api/v1/endpoints/article.py @@ -43,17 +43,16 @@ async def upload_to_self_folder(folder_id: int = Query(...), article: UploadFile @router.get("/getSelfFolders", response_model="dict") async def get_self_folders(page_number: Optional[int] = Query(None, ge=1), page_size: Optional[int] = Query(None, ge=1), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): - # 获取用户id user_id = user.get("id") - total_num, folders = await crud_get_self_folders(user_id, page_number, page_size, db) result = [{"folder_id": folder.id, "folder_name": folder.name} for folder in folders] return {"total_num": total_num, "result": result} @router.get("/getArticlesInFolder", response_model="dict") async def get_articles_in_folder(folder_id: int = Query(...), page_number: Optional[int] = Query(None, ge=1), page_size: Optional[int] = Query(None, ge=1), - db: AsyncSession = Depends(get_db)): - total_num, articles = await crud_get_articles_in_folder(folder_id, page_number, page_size, db) + db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + total_num, articles = await crud_get_articles_in_folder(user_id, folder_id, page_number, page_size, db) result = [{"article_id": article.id, "article_name": article.name} for article in articles] return {"total_num": total_num, "result": result} @@ -134,8 +133,9 @@ async def import_self_folder(folder_name: str = Query(...), zip: UploadFile = Fi return {"msg": "Successfully import articles"} @router.get("/exportSelfFolder", response_class=FileResponse) -async def export_self_folder(background_tasks: BackgroundTasks, folder_id: int = Query(...), db: AsyncSession = Depends(get_db)): - zip_name, article_ids, article_names, article_urls = await crud_export_self_folder(folder_id, db) +async def export_self_folder(background_tasks: BackgroundTasks, folder_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + zip_name, article_ids, article_names, article_urls = await crud_export_self_folder(folder_id, user_id, db) tmp_dir = tempfile.gettempdir() zip_path = os.path.join(tmp_dir, f"{zip_name}.zip") diff --git a/app/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index b74c786..3eabbc0 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -108,13 +108,15 @@ 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(group_id: int = Query(...), db: AsyncSession = Depends(get_db)): - name, desc, avatar, time = await crud_get_basic_info(group_id, db) +async def get_basic_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + name, desc, avatar, time = await crud_get_basic_info(group_id, user_id, db) return {"avatar": avatar, "name": name, "desc": desc, "time": time} @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) +async def get_people_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + leader, admins, members = await crud_get_people_info(group_id, user_id, db) return {"leader": leader, "admins": admins, "members": members} @router.get("/getMyLevel", response_model=dict) diff --git a/app/curd/article.py b/app/curd/article.py index 0d6dc59..3782420 100644 --- a/app/curd/article.py +++ b/app/curd/article.py @@ -29,7 +29,14 @@ async def crud_get_self_folders(user_id: int, page_number: int, page_size: int, return total_num, folders -async def crud_get_articles_in_folder(folder_id: int, page_number: int, page_size: int, db: AsyncSession): +async def crud_get_articles_in_folder(user_id: int, folder_id: int, page_number: int, page_size: int, db: AsyncSession): + # 先检查权限 + query = select(Folder).where(Folder.id == folder_id) + result = await db.execute(query) + folder = result.scalar_one_or_none() + if folder.user_id != user_id: + raise HTTPException(status_code=405, detail="You have no access to it") + # 查找 query = select(Article).where(Article.folder_id == folder_id, Article.visible == True).order_by(Article.id.desc()) count_query = select(func.count()).select_from(query.subquery()) count_result = await db.execute(count_query) @@ -151,12 +158,16 @@ async def crud_import_self_folder(folder_name: str, article_names, urls, user_id for new_article in new_articles: await db.refresh(new_article) -async def crud_export_self_folder(folder_id: int, db: AsyncSession): +async def crud_export_self_folder(folder_id: int, user_id: int, db: AsyncSession): + # 权限检查 query = select(Folder).where(Folder.id == folder_id) result = await db.execute(query) folder = result.scalar_one_or_none() + if folder.user_id != user_id: + raise HTTPException(status_code=405, detail="You have no access to it") + # 文件夹名 folder_name = folder.name - + #文献 query = select(Article).where(Article.folder_id == folder_id, Article.visible == True).order_by(Article.id.desc()) result = await db.execute(query) articles = result.scalars().all() diff --git a/app/curd/group.py b/app/curd/group.py index 94b62e1..8fc2773 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -118,13 +118,26 @@ async def crud_leave_group(group_id: int, user_id: int, db: AsyncSession): await db.execute(query) await db.commit() -async def crud_get_basic_info(group_id: int, db: AsyncSession): +async def crud_get_basic_info(group_id: int, user_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) + relation = result.first() + if not relation: + raise HTTPException(status_code=405, detail="You have no access to it") + # 查询基本信息 query = select(Group.name, Group.description, Group.avatar, Group.create_time).where(Group.id == group_id) result = await db.execute(query) group = result.first() return group.name, group.description, group.avatar, group.create_time -async def crud_get_people_info(group_id: int, db: AsyncSession): +async def crud_get_people_info(group_id: int, user_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) + relation = result.first() + if not relation: + raise HTTPException(status_code=405, detail="You have no access to it") # 创建者信息 query = select(Group.leader).where(Group.id == group_id) result = await db.execute(query)