Skip to content

Commit 4ce37ea

Browse files
committed
[feat]: 实现回收站
1 parent 3c45b81 commit 4ce37ea

File tree

7 files changed

+283
-40
lines changed

7 files changed

+283
-40
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""个人回收站表增加上级信息
2+
3+
Revision ID: 004c4aa2b3f3
4+
Revises: d6d6ae6d9680
5+
Create Date: 2025-05-21 21:29:14.873544
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = '004c4aa2b3f3'
16+
down_revision: Union[str, None] = 'd6d6ae6d9680'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Upgrade schema."""
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.drop_constraint('articles_ibfk_1', 'articles', type_='foreignkey')
25+
op.create_foreign_key(None, 'articles', 'folders', ['folder_id'], ['id'], ondelete='CASCADE')
26+
op.drop_constraint('notes_ibfk_1', 'notes', type_='foreignkey')
27+
op.create_foreign_key(None, 'notes', 'articles', ['article_id'], ['id'], ondelete='CASCADE')
28+
op.add_column('self_recycle_bin', sa.Column('article_id', sa.Integer(), nullable=True))
29+
op.add_column('self_recycle_bin', sa.Column('folder_id', sa.Integer(), nullable=True))
30+
op.create_foreign_key(None, 'self_recycle_bin', 'folders', ['folder_id'], ['id'], ondelete='CASCADE')
31+
op.create_foreign_key(None, 'self_recycle_bin', 'articles', ['article_id'], ['id'], ondelete='CASCADE')
32+
# ### end Alembic commands ###
33+
34+
35+
def downgrade() -> None:
36+
"""Downgrade schema."""
37+
# ### commands auto generated by Alembic - please adjust! ###
38+
op.drop_constraint(None, 'self_recycle_bin', type_='foreignkey')
39+
op.drop_constraint(None, 'self_recycle_bin', type_='foreignkey')
40+
op.drop_column('self_recycle_bin', 'folder_id')
41+
op.drop_column('self_recycle_bin', 'article_id')
42+
op.drop_constraint(None, 'notes', type_='foreignkey')
43+
op.create_foreign_key('notes_ibfk_1', 'notes', 'articles', ['article_id'], ['id'])
44+
op.drop_constraint(None, 'articles', type_='foreignkey')
45+
op.create_foreign_key('articles_ibfk_1', 'articles', 'folders', ['folder_id'], ['id'])
46+
# ### end Alembic commands ###
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""增加个人回收站表
2+
3+
Revision ID: d6d6ae6d9680
4+
Revises: 7af566a6091b
5+
Create Date: 2025-05-14 11:25:12.719964
6+
7+
"""
8+
from typing import Sequence, Union
9+
10+
from alembic import op
11+
import sqlalchemy as sa
12+
13+
14+
# revision identifiers, used by Alembic.
15+
revision: str = 'd6d6ae6d9680'
16+
down_revision: Union[str, None] = '7af566a6091b'
17+
branch_labels: Union[str, Sequence[str], None] = None
18+
depends_on: Union[str, Sequence[str], None] = None
19+
20+
21+
def upgrade() -> None:
22+
"""Upgrade schema."""
23+
# ### commands auto generated by Alembic - please adjust! ###
24+
op.create_table('self_recycle_bin',
25+
sa.Column('user_id', sa.Integer(), nullable=True),
26+
sa.Column('type', sa.Integer(), nullable=False),
27+
sa.Column('id', sa.Integer(), nullable=False),
28+
sa.Column('name', sa.Text(), nullable=False),
29+
sa.Column('create_time', sa.DateTime(), nullable=False),
30+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
31+
sa.PrimaryKeyConstraint('type', 'id')
32+
)
33+
# ### end Alembic commands ###
34+
35+
36+
def downgrade() -> None:
37+
"""Downgrade schema."""
38+
# ### commands auto generated by Alembic - please adjust! ###
39+
op.drop_table('self_recycle_bin')
40+
# ### end Alembic commands ###

app/api/v1/endpoints/article.py

Lines changed: 24 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111

1212
from app.utils.get_db import get_db
1313
from app.utils.auth import get_current_user
14-
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
14+
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
1515
from app.schemas.article import SelfCreateFolder
1616

1717
router = APIRouter()
@@ -72,13 +72,15 @@ async def self_create_folder(model: SelfCreateFolder, db: AsyncSession = Depends
7272
return {"msg": "User Folder Created Successfully", "folder_id": folder_id}
7373

7474
@router.delete("/selfArticleToRecycleBin", response_model="dict")
75-
async def self_article_to_recycle_bin(article_id: int = Query(...), db: AsyncSession = Depends(get_db)):
76-
await crud_self_article_to_recycle_bin(article_id, db)
75+
async def self_article_to_recycle_bin(article_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
76+
user_id = user.get("id")
77+
await crud_self_article_to_recycle_bin(article_id, user_id, db)
7778
return {"msg": "Article is moved to recycle bin"}
7879

7980
@router.delete("/selfFolderToRecycleBin", response_model="dict")
80-
async def self_folder_to_recycle_bin(folder_id: int = Query(...), db: AsyncSession = Depends(get_db)):
81-
await crud_self_folder_to_recycle_bin(folder_id, db)
81+
async def self_folder_to_recycle_bin(folder_id: int = Query(...), db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
82+
user_id = user.get("id")
83+
await crud_self_folder_to_recycle_bin(folder_id, user_id, db)
8284
return {"msg": "Folder is moved to recycle bin"}
8385

8486
@router.post("/annotateSelfArticle", response_model="dict")
@@ -201,4 +203,20 @@ async def self_tree(page_number: Optional[int] = Query(None, ge=1), page_size: O
201203
async def self_article_statistic(db: AsyncSession = Depends(get_db), user: dict = Depends(get_current_user)):
202204
user_id = user.get("id")
203205
article_total_num, articles = await crud_self_article_statistic(user_id, db)
204-
return {"article_total_num": article_total_num, "articles": articles}
206+
return {"article_total_num": article_total_num, "articles": articles}
207+
208+
@router.get("/itemsInRecycleBin", response_model=dict)
209+
async def items_in_recycle_bin(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)):
210+
user_id = user.get("id")
211+
items = await crud_items_in_recycle_bin(user_id, page_number, page_size, db)
212+
return {"items": items}
213+
214+
@router.delete("/deleteForever", response_model=dict)
215+
async def delete_forever(type: int = Query(...), id: int = Query(...), db: AsyncSession = Depends(get_db)):
216+
await crud_delete_forever(type, id, db)
217+
return {"msg": "Item and its child nodes deleted forever successfully"}
218+
219+
@router.post("/recover", response_model=dict)
220+
async def recover(type: int = Body(...), id: int = Body(...), db: AsyncSession = Depends(get_db)):
221+
return_value = await crud_recover(type, id, db)
222+
return return_value

app/api/v1/endpoints/note.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,9 @@ async def create_note(note: NoteCreate, db: AsyncSession = Depends(get_db), curr
1414
return {"msg": "Note created successfully", "note_id": new_note.id}
1515

1616
@router.delete("/{note_id}", response_model=dict)
17-
async def delete_note(note_id: int, db: AsyncSession = Depends(get_db)):
18-
note = await delete_note_in_db(note_id, db)
17+
async def delete_note(note_id: int, db: AsyncSession = Depends(get_db), current_user: dict = Depends(get_current_user)):
18+
user_id = current_user["id"]
19+
note = await delete_note_in_db(note_id, user_id, db)
1920
if not note:
2021
raise HTTPException(status_code=404, detail="Note not found")
2122
return {"msg": "Note deleted successfully"}

app/curd/article.py

Lines changed: 112 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
from sqlalchemy.ext.asyncio import AsyncSession
2-
from sqlalchemy import select, delete
2+
from sqlalchemy import select, delete, insert, desc
33
from sqlalchemy import func, cast, Date
44
from datetime import datetime, timedelta
5-
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group
5+
from app.models.model import User, Group, Folder, Article, Note, Tag, user_group, self_recycle_bin
66

77
async def crud_upload_to_self_folder(name: str, folder_id: int, db: AsyncSession):
88
new_article = Article(name=name, folder_id=folder_id)
@@ -46,25 +46,31 @@ async def crud_self_create_folder(name: str, user_id: int, db: AsyncSession):
4646
await db.refresh(new_folder)
4747
return new_folder.id
4848

49-
async def crud_self_article_to_recycle_bin(article_id: int, db: AsyncSession):
50-
# 查询 article
49+
async def crud_self_article_to_recycle_bin(article_id: int, user_id: int, db: AsyncSession):
50+
# 维护 article
5151
query = select(Article).where(Article.id == article_id)
5252
result = await db.execute(query)
5353
article = result.scalar_one_or_none()
54-
55-
# 修改 visible 字段
5654
article.visible = False
55+
56+
# 维护 self_recycle_bin 表
57+
recycle = insert(self_recycle_bin).values(user_id=user_id, type=2, id=article_id, name=article.name, folder_id=article.folder_id)
58+
await db.execute(recycle)
59+
5760
await db.commit()
5861
await db.refresh(article)
5962

60-
async def crud_self_folder_to_recycle_bin(folder_id: int, db: AsyncSession):
61-
# 查询 folder
63+
async def crud_self_folder_to_recycle_bin(folder_id: int, user_id: int, db: AsyncSession):
64+
# 维护 folder
6265
query = select(Folder).where(Folder.id == folder_id)
6366
result = await db.execute(query)
6467
folder = result.scalar_one_or_none()
65-
66-
# 修改 visible 字段
6768
folder.visible = False
69+
70+
# 维护 self_recycle_bin 表
71+
recycle = insert(self_recycle_bin).values(user_id=user_id, type=1, id=folder_id, name=folder.name)
72+
await db.execute(recycle)
73+
6874
await db.commit()
6975
await db.refresh(folder)
7076

@@ -166,11 +172,11 @@ async def crud_article_statistic(db: AsyncSession):
166172
tomorrow = datetime.now().date() + timedelta(days=1)
167173
seven_days_ago = datetime.now().date() - timedelta(days=6)
168174

169-
# 查询近7天内的笔记数目,按日期分组
175+
# 查询近7天内的文献数目,按日期分组
170176
query = (
171177
select(
172178
cast(Article.create_time, Date).label("date"), # 按日期分组
173-
func.count(Article.id).label("count") # 统计每日期的笔记数
179+
func.count(Article.id).label("count") # 统计每日期的文献数
174180
)
175181
.where(
176182
Article.create_time >= seven_days_ago, # 大于等于7天前的0点
@@ -243,11 +249,11 @@ async def crud_self_article_statistic(user_id: int, db: AsyncSession):
243249
tomorrow = datetime.now().date() + timedelta(days=1)
244250
seven_days_ago = datetime.now().date() - timedelta(days=6)
245251

246-
# 查询近7天内的笔记数目,按日期分组
252+
# 查询近7天内的文献数目,按日期分组
247253
query = (
248254
select(
249255
cast(Article.create_time, Date).label("date"), # 按日期分组
250-
func.count(Article.id).label("count") # 统计每日期的笔记数
256+
func.count(Article.id).label("count") # 统计每日期的文献数
251257
)
252258
.join(Folder, Article.folder_id == Folder.id)
253259
.where(
@@ -273,4 +279,95 @@ async def crud_self_article_statistic(user_id: int, db: AsyncSession):
273279
if i == len(articles) or articles[i].get("date") != seven_days_ago + timedelta(days=i):
274280
articles.insert(i, {"date": seven_days_ago + timedelta(days=i), "count": 0})
275281

276-
return article_total_num, articles
282+
return article_total_num, articles
283+
284+
async def crud_items_in_recycle_bin(user_id: int, page_number: int, page_size: int, db: AsyncSession):
285+
query = select(
286+
self_recycle_bin.c.type,
287+
self_recycle_bin.c.id,
288+
self_recycle_bin.c.name,
289+
self_recycle_bin.c.create_time
290+
).where(self_recycle_bin.c.user_id == user_id).order_by(desc(self_recycle_bin.c.create_time))
291+
292+
if page_number and page_size:
293+
offset = (page_number - 1) * page_size
294+
query = query.offset(offset).limit(page_size)
295+
296+
result = await db.execute(query)
297+
items = result.fetchall()
298+
299+
return [{"type": item.type, "id": item.id, "name": item.name, "time": item.create_time.strftime("%Y-%m-%d %H:%M:%S")} for item in items]
300+
301+
async def crud_delete_forever(type: int, id: int, db: AsyncSession):
302+
query = delete(self_recycle_bin).where(self_recycle_bin.c.type == type, self_recycle_bin.c.id == id)
303+
await db.execute(query)
304+
if type == 1:
305+
query = delete(Folder).where(Folder.id==id)
306+
elif type == 2:
307+
query = delete(Article).where(Article.id==id)
308+
else:
309+
query = delete(Note).where(Note.id==id)
310+
await db.execute(query)
311+
await db.commit()
312+
313+
async def crud_recover(type: int, id: int, db: AsyncSession):
314+
query = select(self_recycle_bin).where(self_recycle_bin.c.type == type, self_recycle_bin.c.id == id)
315+
result = await db.execute(query)
316+
item = result.first()
317+
if type == 3:
318+
# 检查上级文献存在性
319+
query = select(Article).where(Article.id == item.article_id)
320+
result = await db.execute(query)
321+
article = result.scalar_one_or_none()
322+
article_name = article.name
323+
article_visible = article.visible
324+
# 检查上级文件夹存在性
325+
query = select(Folder).where(Folder.id == item.folder_id)
326+
result = await db.execute(query)
327+
folder = result.scalar_one_or_none()
328+
folder_name = folder.name
329+
folder_visible = folder.visible
330+
# 若上级不存在,则给用户以提示信息,请用户先恢复相应的文件夹和文献
331+
if not article_visible or not folder_visible:
332+
return {"info": "Note recovered failed, please check its upper-level node", "folder_name": folder_name, "article_name": article_name}
333+
# 若上级存在,则正常恢复即可,在回收站表中删除该表项,并将Note表中visible改为True
334+
query = delete(self_recycle_bin).where(self_recycle_bin.c.type == type, self_recycle_bin.c.id == id)
335+
await db.execute(query)
336+
query = select(Note).where(Note.id == id)
337+
result = await db.execute(query)
338+
note = result.scalar_one_or_none()
339+
note.visible = True
340+
await db.commit()
341+
await db.refresh(note)
342+
return {"info": "Note recovered successfully"}
343+
if type == 2:
344+
# 检查上级文件夹存在性
345+
query = select(Folder).where(Folder.id == item.folder_id)
346+
result = await db.execute(query)
347+
folder = result.scalar_one_or_none()
348+
folder_name = folder.name
349+
folder_visible = folder.visible
350+
# 若上级不存在,则给用户以提示信息,请用户先恢复相应的文件夹
351+
if not folder_visible:
352+
return {"info": "Article recovered failed, please check its upper-level node", "folder_name": folder_name}
353+
# 若上级存在,则正常恢复即可,在回收站表中删除该表项,并将Article表中visible改为True
354+
query = delete(self_recycle_bin).where(self_recycle_bin.c.type == type, self_recycle_bin.c.id == id)
355+
await db.execute(query)
356+
query = select(Article).where(Article.id == id)
357+
result = await db.execute(query)
358+
article = result.scalar_one_or_none()
359+
article.visible = True
360+
await db.commit()
361+
await db.refresh(article)
362+
return {"info": "Article recovered successfully"}
363+
if type == 1:
364+
# 正常恢复即可,在回收站表中删除该表项,并将Folder表中visible改为True
365+
query = delete(self_recycle_bin).where(self_recycle_bin.c.type == type, self_recycle_bin.c.id == id)
366+
await db.execute(query)
367+
query = select(Folder).where(Folder.id == id)
368+
result = await db.execute(query)
369+
folder = result.scalar_one_or_none()
370+
folder.visible = True
371+
await db.commit()
372+
await db.refresh(folder)
373+
return {"info": "Folder recovered successfully"}

0 commit comments

Comments
 (0)