diff --git a/.env b/.env deleted file mode 100644 index 453d6a6..0000000 --- a/.env +++ /dev/null @@ -1,3 +0,0 @@ -SECRET_KEY=bN3hZ6LbHG7nH9YXWULCr-crcS3GAaRELbNBdAyHBuiHH5TRctd0Zbd6OuLRHHa4Fbs -SENDER_PASSWORD=TXVU2unpCAE2EtEX -KIMI_API_KEY=sk-icdiHIiv6x8XjJCaN6J6Un7uoVxm6df5WPhflq10ZVFo03D9 \ No newline at end of file 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 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/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/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/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/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/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/aichat.py b/app/api/v1/endpoints/aichat.py index f4748ff..a8ad20d 100644 --- a/app/api/v1/endpoints/aichat.py +++ b/app/api/v1/endpoints/aichat.py @@ -1,12 +1,17 @@ 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 +from app.curd.articleDB import update_article_intro +from app.models.model import ArticleDB router = APIRouter() redis_client = get_redis_client() @@ -25,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}) @@ -47,4 +52,146 @@ 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( + article_id: int, + db : AsyncSession = Depends(get_db), +): + # 读取数据库获取笔记内容 + from app.curd.note import get_note_by_id + notes = await get_note_by_id(db, article_id) + if not notes: + raise HTTPException(status_code=404, detail="Note not found") + 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 + 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") + except Exception as e: + raise HTTPException( + status_code=500, + detail=f"AI服务异常: {str(e)}" + ) + return {"mermaid_code": ans.strip().replace("```mermaid", "").replace("```", "").strip()} + +@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字以内。" + 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="AI服务异常,无法生成文章简介,请稍后重试。" + ) + # 更新文章简介到数据库 + 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/api/v1/endpoints/article.py b/app/api/v1/endpoints/article.py index 9159aba..27543ef 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_search, crud_self_article_statistic, crud_items_in_recycle_bin, crud_delete_forever, crud_recover from app.schemas.article import SelfCreateFolder router = APIRouter() @@ -24,35 +25,34 @@ 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") 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} @@ -84,26 +84,26 @@ 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)): - - file_path = f"/lhcos-data/{article_id}.pdf" - - # 查询文件名 - article_name = 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') - # 返回结果 - 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), 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") 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 +118,31 @@ 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) +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") 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 +156,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)): @@ -199,6 +196,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") @@ -213,7 +216,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 9b0e030..b82d127 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, 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 @@ -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)): """ @@ -79,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. """ @@ -89,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: @@ -103,6 +131,14 @@ 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/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/api/v1/endpoints/group.py b/app/api/v1/endpoints/group.py index 56f850e..3eabbc0 100644 --- a/app/api/v1/endpoints/group.py +++ b/app/api/v1/endpoints/group.py @@ -1,11 +1,16 @@ 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 +import json from app.utils.get_db import get_db from app.utils.auth import get_current_user -from app.curd.group import crud_create, crud_apply_to_enter, crud_get_applications, crud_reply_to_enter -from app.schemas.group import ApplyToEnter +from app.curd.group import crud_create, crud_gen_invite_code, crud_enter_group, crud_modify_basic_info, crud_modify_admin_list, crud_remove_member, crud_leave_group, crud_get_basic_info, crud_get_people_info, crud_get_my_level, crud_all_groups, 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() @@ -16,31 +21,287 @@ async def create(group_name: str = Query(...), group_desc: str = Query(...), gro raise HTTPException(status_code=405, detail="Invalid group name, longer than 30") if len(group_desc) > 200: raise HTTPException(status_code=405, detail="Invalid group description, longer than 200") - group_id = await crud_create(user.get("id"), group_name, group_desc, db) + path = "/lhcos-data/group-avatar/default.png" + # 存储头像,保留扩展名 if group_avatar: os.makedirs("/lhcos-data/group-avatar", exist_ok=True) ext = os.path.splitext(group_avatar.filename)[1] - path = os.path.join("/lhcos-data/group-avatar", f"{group_id}{ext}") + path = f"/lhcos-data/group-avatar/{uuid.uuid4()}{ext}" with open(path, "wb") as f: content = await group_avatar.read() f.write(content) + await crud_create(user.get("id"), group_name, group_desc, path, db) return {"msg": "Group created successfully"} -@router.post("/applyToEnter", response_model=dict) -async def apply_to_enter(model: ApplyToEnter, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): +@router.get("/genInviteCode", response_model=dict) +async def gen_invite_code(user_email: str = Query(...), group_id: int = Query(...), db: AsyncSession = Depends(get_db)): + await crud_gen_invite_code(user_email, db) + today = date.today() + data = { + "email": user_email, + "group_id": group_id, + "date": today.isoformat() + } + json_data = json.dumps(data).encode() + fernet = Fernet(os.getenv("FERNET_SECRET_KEY")) + encrypted = fernet.encrypt(json_data) + return {"inviteCode": encrypted} + +@router.post("/enterGroup", response_model=dict) +async def enter_group(inviteCode: EnterGroup, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + code = inviteCode.inviteCode + fernet = Fernet(os.getenv("FERNET_SECRET_KEY")) + + decrypted = fernet.decrypt(code.encode()) + data = json.loads(decrypted) + + user_email = user.get("email") + invite_email = data["email"] + if user_email != invite_email: + raise HTTPException(status_code=405, detail="Not your invite code") + + invite_date = datetime.strptime(data["date"], "%Y-%m-%d").date() + today = date.today() + if today > invite_date: + raise HTTPException(status_code=406, detail="Invite Code already expired") + + await crud_enter_group(user.get("id"), data["group_id"], db) + return {"msg": "Enter thr group successfully"} + +@router.post("/modifyBasicInfo", response_model=dict) +async def modify_basic_info(group_id: int = Query(...), group_name: str | None = Query(None), group_desc: str | None = Query(None), group_avatar: UploadFile | None = File(None), db: AsyncSession = Depends(get_db), 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: + raise HTTPException(status_code=405, detail="Invalid group description, longer than 200") + new_path = None + if group_avatar: + os.makedirs("/lhcos-data/group-avatar", exist_ok=True) + # 存储新头像,保留扩展名 + ext = os.path.splitext(group_avatar.filename)[1] + new_path = f"/lhcos-data/group-avatar/{uuid.uuid4()}{ext}" + with open(new_path, "wb") as f: + content = await group_avatar.read() + f.write(content) + 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"} + +@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), 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) +async def leave_group(model: LeaveGroup, db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): group_id = model.group_id user_id = user.get("id") - await crud_apply_to_enter(user_id, group_id, db) - return {"msg": "Application sent successfully"} + await crud_leave_group(group_id, user_id, db) + return {"msg": "You successfully left the group"} + +@router.get("/getBasicInfo", response_model=dict) +async def get_basic_info(group_id: int = Query(...), db: AsyncSession = Depends(get_db), 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), 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) +async def get_my_level(group_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + level = await crud_get_my_level(user_id, group_id, db) + return {"level": level} + +@router.get("/allGroups", response_model=dict) +async def all_groups(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)): + user_id = user.get("id") + leader, admin, member = await crud_all_groups(user_id, db) + return {"leader": leader, "admin": admin, "member": member} + +@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") + 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)): + # 检查上传的必须为 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") + article_id = await crud_new_article(user_id, folder_id, name, url, db) + + 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") + 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)): + 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.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: + 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 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) + 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"} + -@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} +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 + 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()) -@router.post("/replyToEnter", response_model=dict) -async def reply_to_enter(user_id: int = Body(...), group_id: int = Body(...), reply: int = Body(...), db: AsyncSession = Depends(get_db)): - if reply != 0 and reply != 1: - raise HTTPException(status_code=405, detail="Wrong parameter, reply should be either 0 or 1") - msg = await crud_reply_to_enter(user_id, group_id, reply, db) - return {"msg": msg} \ No newline at end of file + 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=new_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=new_file_path, + db=db + ) + return {"msg": "Article copied successfully", "new_article_id": new_article_id} diff --git a/app/api/v1/endpoints/note.py b/app/api/v1/endpoints/note.py index 56cb98c..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) @@ -32,15 +32,16 @@ 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, "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/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/curd/article.py b/app/curd/article.py index 5656475..3782420 100644 --- a/app/curd/article.py +++ b/app/curd/article.py @@ -1,11 +1,15 @@ +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, 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) @@ -25,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) @@ -74,15 +85,64 @@ 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_read_article(article_id: int, db: AsyncSession): +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() - return article.name + article.update_time = datetime.now() + await db.commit() + await db.refresh(article) + return article.url -async def crud_import_self_folder(folder_name: str, article_names, user_id: int, db: AsyncSession): - result = [] +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 relation and 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(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 relation and 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.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,38 +150,43 @@ 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): +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() 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) @@ -235,6 +300,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 = ( @@ -301,14 +402,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) @@ -370,4 +484,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 diff --git a/app/curd/articleDB.py b/app/curd/articleDB.py index 9af3af5..9a4bfb9 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, RecommendArticle async def create_article_in_db(db: AsyncSession, upload_article: UploadArticle): """ @@ -19,10 +19,15 @@ 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: - 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 +42,36 @@ 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) + # 更新所有搜索到文章的点击量 + 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): """ @@ -52,4 +87,35 @@ 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 + if not article: + return None, None + 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] + +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/curd/group.py b/app/curd/group.py index 4c8382d..8fc2773 100644 --- a/app/curd/group.py +++ b/app/curd/group.py @@ -1,58 +1,772 @@ from fastapi import HTTPException from sqlalchemy.ext.asyncio import AsyncSession -from sqlalchemy.exc import IntegrityError -from sqlalchemy import select, insert, delete -from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, enter_application +from sqlalchemy import select, insert, delete, update, 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, 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) + new_log = insert(group_logs).values(group_id=new_group.id, type=0, person1=leader) + await db.execute(new_log) await db.commit() - await db.refresh(new_group) - return new_group.id -async def crud_apply_to_enter(user_id: int, group_id: int, db: AsyncSession): - # 是否已经在组织中 +async def crud_gen_invite_code(user_email: str, db: AsyncSession): + # 检查邮箱存在性 + query = select(User.id).where(User.email == user_email) + result = await db.execute(query) + user_id = result.scalar_one_or_none() + if not user_id: + raise HTTPException(status_code=405, detail="User not existed") + +async def crud_enter_group(user_id: int, group_id: int, db: AsyncSession): + # 检查是否已经在组织内 query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id) result = await db.execute(query) - existing = result.first() - if existing: - raise HTTPException(status_code=405, detail="Already in the group") - query = select(Group).where(Group.id == group_id) + exist = result.first() + if exist: + raise HTTPException(status_code=408, detail="You are already in the group") + new_relation = insert(user_group).values(user_id=user_id, group_id=group_id) + await db.execute(new_relation) + 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, 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() + 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) + 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 + +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) - group = result.scalar_one_or_none() - if group.leader == user_id: - raise HTTPException(status_code=405, detail="Already in the group") + relation = result.first() + if not relation: + raise HTTPException(status_code=405, detail="User currently not in the group") - # 插入申请表,若已存在申请则抛出异常 - query = insert(enter_application).values(user_id=user_id, group_id=group_id) - try: + # 将该成员设为或取消管理员 + 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) - await db.commit() - except IntegrityError: - await db.rollback() - raise HTTPException(status_code=405, detail="Don't apply repeatedly") + 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) + 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, 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, 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, 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) + leader_id = result.scalar_one_or_none() + query = select(User).where(User.id == leader_id) + result = await db.execute(query) + user = result.scalar_one_or_none() + leader = {"id": user.id, "name": user.username, "avatar": user.avatar} + + # 管理者信息 + query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 2) + result = await db.execute(query) + admin_ids = result.scalars().all() + query = select(User).where(User.id.in_(admin_ids)) + result = await db.execute(query) + users = result.scalars().all() + admins = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users] + + # 普通成员信息 + query = select(user_group.c.user_id).where(user_group.c.group_id == group_id, user_group.c.level == 3) + result = await db.execute(query) + member_ids = result.scalars().all() + query = select(User).where(User.id.in_(member_ids)) + result = await db.execute(query) + users = result.scalars().all() + members = [{"id": user.id, "name": user.username, "avatar": user.avatar} for user in users] -async def crud_get_applications(group_id: int, db: AsyncSession): - query = select(User.id, User.username).where(User.id.in_( - select(enter_application.c.user_id).where(enter_application.c.group_id == group_id) - )) + return leader, admins, members + +async def crud_get_my_level(user_id: int, group_id: int, db: AsyncSession): + query = select(user_group).where(user_group.c.user_id == user_id, user_group.c.group_id == group_id) result = await db.execute(query) - users = result.all() - return [{"user_id": user.id, "user_name": user.username} for user in users] + relation = result.first() + # 在组织中 + if relation: + return relation[2] # relation[0] relation[1] relation[2] 分别为表的第1、2、3列 + # 不在组织中 + return 4 -async def crud_reply_to_enter(user_id: int, group_id: int, reply: int, db: AsyncSession): - # 答复后,需要从待处理申请的表中删除表项 - query = delete(enter_application).where(enter_application.c.user_id == user_id, enter_application.c.group_id == group_id) +async def crud_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) - if result.rowcount == 0: # 如果没有删除任何行,说明不存在该项 - raise HTTPException(status_code=405, detail="Application is not existed or already handled") + 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 + +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) + return new_folder.id + +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) + return new_note.id - if reply == 1: - new_relation = insert(user_group).values(user_id=user_id, group_id=group_id) - await db.execute(new_relation) +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 "Add new member successfully" + 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 [] - return "Refuse the application successfully" +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.id, User.username, User.avatar).where(User.id == unaccessible_id) + result = await db.execute(query) + user_info = result.one_or_none() + 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.id, User.username, User.avatar).where(User.id == read_only_id) + result = await db.execute(query) + user_info = result.one_or_none() + 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: + 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.id, User.username, User.avatar).where(User.id == writeable_id) + result = await db.execute(query) + user_info = result.one_or_none() + 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 + +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) + 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/curd/note.py b/app/curd/note.py index 92f007b..67c1f5a 100644 --- a/app/curd/note.py +++ b/app/curd/note.py @@ -42,14 +42,16 @@ 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: 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}%")) & 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() @@ -59,7 +61,7 @@ async def find_notes_in_db(note_find: NoteFind, db: AsyncSession): 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): @@ -177,4 +179,14 @@ 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, article_id: int): + """ + 根据 ID 获取笔记 + """ + stmt = select(Note).where(Note.article_id == article_id and Note.visible == True) + result = await db.execute(stmt) + # 返回所有笔记 + notes = result.scalars().all() + return notes if notes else None \ No newline at end of file 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 e4e1c3c..0077f2c 100644 --- a/app/main.py +++ b/app/main.py @@ -4,8 +4,9 @@ from loguru import logger from fastapi.middleware.cors import CORSMiddleware from fastapi.staticfiles import StaticFiles +import os -app = FastAPI() +app = FastAPI(docs_url=None, redoc_url=None) @app.get("/") def read_root(): @@ -40,5 +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) + # 挂载静态文件目录 -app.mount("/static", StaticFiles(directory="app/static"), name="static") \ No newline at end of file +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/models/model.py b/app/models/model.py index 8c89021..b85ef9e 100644 --- a/app/models/model.py +++ b/app/models/model.py @@ -7,14 +7,8 @@ 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('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('group_id', Integer, ForeignKey('groups.id', ondelete="CASCADE"), primary_key=True), + Column('level', Integer, default=3) # 1: leader 2: admin 3:member ) self_recycle_bin = Table( @@ -29,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' @@ -51,10 +85,13 @@ 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') - 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' @@ -63,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) # 更新时间 @@ -79,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' @@ -87,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' @@ -106,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') @@ -129,6 +183,8 @@ 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) # 点击量 + 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 688188e..f103cc2 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,7 +27,13 @@ class GetResponse(BaseModel): url: str create_time: datetime update_time: datetime + author: str file_path: str + clicks: int + intro: str | None = None 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/app/schemas/group.py b/app/schemas/group.py index dd69fe7..79fcaf0 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 \ No newline at end of file +class LeaveGroup(BaseModel): + group_id: int + +class EnterGroup(BaseModel): + inviteCode: str \ No newline at end of file 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 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 diff --git a/requirements.txt b/requirements.txt index 91c416d..afb895a 100644 Binary files a/requirements.txt and b/requirements.txt differ