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/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/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/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/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/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/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/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 4da65ed..77c3421 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', Text), + 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,9 @@ 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") + 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' @@ -58,7 +100,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 +116,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 +124,25 @@ 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', 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') + 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 +154,22 @@ 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', 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'), + ) 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 +183,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 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 diff --git a/requirements.txt b/requirements.txt index 65d9e22..afb895a 100644 Binary files a/requirements.txt and b/requirements.txt differ