Refactor code structure for improved readability and maintainability

This commit is contained in:
2026-04-06 18:24:07 +00:00
parent db5c1f0881
commit ff90d0e400
3 changed files with 6991 additions and 53 deletions
+101 -52
View File
@@ -574,6 +574,31 @@ async def healthcheck(db: AsyncSession = Depends(get_db_session)):
} }
_VN_ORDER: dict[str, tuple[int, int]] = {
**{c: (1, i) for i, c in enumerate("aàảãáạ")},
**{c: (2, i) for i, c in enumerate("ăằẳẵắặ")},
**{c: (3, i) for i, c in enumerate("âầẩẫấậ")},
"b": (4, 0), "c": (5, 0), "d": (6, 0), "đ": (7, 0),
**{c: (8, i) for i, c in enumerate("eèẻẽéẹ")},
**{c: (9, i) for i, c in enumerate("êềểễếệ")},
"g": (10, 0), "h": (11, 0),
**{c: (12, i) for i, c in enumerate("iìỉĩíị")},
"k": (13, 0), "l": (14, 0), "m": (15, 0), "n": (16, 0),
**{c: (17, i) for i, c in enumerate("oòỏõóọ")},
**{c: (18, i) for i, c in enumerate("ôồổỗốộ")},
**{c: (19, i) for i, c in enumerate("ơờởỡớợ")},
"p": (20, 0), "q": (21, 0), "r": (22, 0), "s": (23, 0), "t": (24, 0),
**{c: (25, i) for i, c in enumerate("uùủũúụ")},
**{c: (26, i) for i, c in enumerate("ưừửữứự")},
"v": (27, 0), "x": (28, 0),
**{c: (29, i) for i, c in enumerate("yỳỷỹýỵ")},
}
def _vn_sort_key(s: str) -> list[tuple[int, int]]:
return [_VN_ORDER.get(c, (ord(c), 0)) for c in s.lower()]
@app.get("/api/genres") @app.get("/api/genres")
async def list_genres(db: AsyncSession = Depends(get_db_session)): async def list_genres(db: AsyncSession = Depends(get_db_session)):
result = await db.execute( result = await db.execute(
@@ -581,11 +606,24 @@ async def list_genres(db: AsyncSession = Depends(get_db_session)):
'SELECT g.id, g.name, g.slug, g.description, g.icon, COUNT(ng."novelId")::int AS "novelCount" ' 'SELECT g.id, g.name, g.slug, g.description, g.icon, COUNT(ng."novelId")::int AS "novelCount" '
'FROM "Genre" g ' 'FROM "Genre" g '
'LEFT JOIN "NovelGenre" ng ON ng."genreId" = g.id ' 'LEFT JOIN "NovelGenre" ng ON ng."genreId" = g.id '
'GROUP BY g.id ' 'GROUP BY g.id'
'ORDER BY g.name ASC'
) )
) )
return [dict(row) for row in result.mappings().all()] rows = [dict(row) for row in result.mappings().all()]
rows.sort(key=lambda r: _vn_sort_key(r["name"]))
return rows
@app.get("/api/genres/{slug}")
async def get_genre_by_slug(slug: str, db: AsyncSession = Depends(get_db_session)):
result = await db.execute(
text('SELECT id, name, slug, description, icon FROM "Genre" WHERE slug = :slug LIMIT 1'),
{"slug": slug},
)
row = result.mappings().first()
if not row:
raise HTTPException(status_code=404, detail="Genre not found")
return dict(row)
@app.get("/api/novels/browse") @app.get("/api/novels/browse")
@@ -595,11 +633,18 @@ async def browse_novels(
status: str = "", status: str = "",
sort: str = "latest", sort: str = "latest",
page: int = Query(default=1, ge=1), page: int = Query(default=1, ge=1),
limit: int = Query(default=20, ge=1, le=100), limit: int = Query(default=20, ge=1, le=500),
collapse_series: bool = Query(default=False),
db: AsyncSession = Depends(get_db_session), db: AsyncSession = Depends(get_db_session),
): ):
skip = (page - 1) * limit skip = (page - 1) * limit
order_clause = { outer_order_clause = {
"popular": 'views DESC',
"rating": 'rating DESC',
"name": 'title ASC',
"latest": '"updatedAt" DESC',
}.get(sort, '"updatedAt" DESC')
inner_order_clause = {
"popular": 'n.views DESC', "popular": 'n.views DESC',
"rating": 'n.rating DESC', "rating": 'n.rating DESC',
"name": 'n.title ASC', "name": 'n.title ASC',
@@ -629,32 +674,59 @@ async def browse_novels(
where_sql = f"WHERE {' AND '.join(where_parts)}" if where_parts else "" where_sql = f"WHERE {' AND '.join(where_parts)}" if where_parts else ""
total_count = ( base_select = (
await db.execute( 'n.id, n.title, n.slug, n."originalTitle", n."authorName", n."coverUrl", n."coverColor", '
text( 'n.status, n."totalChapters", n.views, n.rating, n."ratingCount", n."bookmarkCount", '
f'SELECT COUNT(*)::int FROM "Novel" n ' 'n."seriesId", s.id AS series_id, s.name AS series_name, s.slug AS series_slug, n."updatedAt"'
f'LEFT JOIN "Series" s ON s.id = n."seriesId" ' )
f'{where_sql}' base_from = (
), 'FROM "Novel" n '
params, 'LEFT JOIN "Series" s ON s.id = n."seriesId" '
) )
).scalar_one()
rows = ( if collapse_series:
await db.execute( # DISTINCT ON picks the best novel per series (most recent), then outer query sorts+paginates
text( inner_sql = (
f'SELECT n.id, n.title, n.slug, n."originalTitle", n."authorName", n."coverUrl", n."coverColor", ' f'SELECT DISTINCT ON (COALESCE(n."seriesId", n.id)) {base_select} '
f'n.status, n."totalChapters", n.views, n.rating, n."ratingCount", n."bookmarkCount", ' f'{base_from}'
f'n."seriesId", s.id AS series_id, s.name AS series_name, s.slug AS series_slug, n."updatedAt" ' f'{where_sql} '
f'FROM "Novel" n ' f'ORDER BY COALESCE(n."seriesId", n.id), n."updatedAt" DESC'
f'LEFT JOIN "Series" s ON s.id = n."seriesId" '
f'{where_sql} '
f'ORDER BY {order_clause} '
f'OFFSET :skip LIMIT :limit'
),
params,
) )
).mappings().all() total_count = (
await db.execute(
text(f'SELECT COUNT(*)::int FROM ({inner_sql}) AS _c'),
params,
)
).scalar_one()
rows = (
await db.execute(
text(
f'SELECT * FROM ({inner_sql}) AS collapsed '
f'ORDER BY {outer_order_clause} '
f'OFFSET :skip LIMIT :limit'
),
params,
)
).mappings().all()
else:
total_count = (
await db.execute(
text(f'SELECT COUNT(*)::int {base_from}{where_sql}'),
params,
)
).scalar_one()
rows = (
await db.execute(
text(
f'SELECT {base_select} '
f'{base_from}'
f'{where_sql} '
f'ORDER BY {inner_order_clause} '
f'OFFSET :skip LIMIT :limit'
),
params,
)
).mappings().all()
novel_ids = [row["id"] for row in rows] novel_ids = [row["id"] for row in rows]
genre_map: dict[str, list[dict[str, str]]] = {novel_id: [] for novel_id in novel_ids} genre_map: dict[str, list[dict[str, str]]] = {novel_id: [] for novel_id in novel_ids}
@@ -677,29 +749,6 @@ async def browse_novels(
) )
chapter_map: dict[str, dict[str, Any]] = {} chapter_map: dict[str, dict[str, Any]] = {}
if novel_ids:
latest_chapters = await mongo_db["chapters"].aggregate(
[
{"$match": {"novelId": {"$in": novel_ids}}},
{"$sort": {"novelId": 1, "number": -1}},
{
"$group": {
"_id": "$novelId",
"latestChapterNumber": {"$first": "$number"},
"latestChapterTitle": {"$first": "$title"},
"latestChapterAt": {"$first": "$createdAt"},
}
},
]
).to_list(length=None)
chapter_map = {
item["_id"]: {
"number": item.get("latestChapterNumber"),
"title": item.get("latestChapterTitle"),
"createdAt": _iso(item.get("latestChapterAt")),
}
for item in latest_chapters
}
items: list[dict[str, Any]] = [] items: list[dict[str, Any]] = []
for row in rows: for row in rows:
+67 -1
View File
@@ -214,6 +214,72 @@ async def mod_delete_genre(
return {"success": True} return {"success": True}
class GenreUpdateBody(BaseModel):
id: str
name: str
description: str | None = None
icon: str | None = None
@router.put("/mod/the-loai")
async def mod_update_genre(
body: GenreUpdateBody,
db: AsyncSession = Depends(get_db_session),
user: dict = Depends(require_mod_user),
):
slug = slugify(body.name)
try:
result = await db.execute(
text(
'UPDATE "Genre" SET name = :name, slug = :slug, description = :desc, icon = :icon '
'WHERE id = :id RETURNING id, name, slug, description, icon'
),
{"id": body.id, "name": body.name, "slug": slug, "desc": body.description, "icon": body.icon},
)
await db.commit()
row = result.mappings().first()
if not row:
raise HTTPException(status_code=404, detail="Không tìm thấy thể loại")
return dict(row)
except Exception as e:
await db.rollback()
if "unique" in str(e).lower() or "23505" in str(e):
raise HTTPException(status_code=400, detail="Tên thể loại đã tồn tại")
raise
class GenreMergeBody(BaseModel):
sourceId: str # thể loại sẽ bị xóa
targetId: str # thể loại giữ lại
@router.post("/mod/the-loai/merge")
async def mod_merge_genre(
body: GenreMergeBody,
db: AsyncSession = Depends(get_db_session),
user: dict = Depends(require_mod_user),
):
if body.sourceId == body.targetId:
raise HTTPException(status_code=400, detail="sourceId và targetId phải khác nhau")
# Re-point NovelGenre rows: ON CONFLICT DO NOTHING để tránh duplicate
await db.execute(
text(
'INSERT INTO "NovelGenre" ("novelId", "genreId") '
'SELECT "novelId", :target FROM "NovelGenre" WHERE "genreId" = :source '
'ON CONFLICT DO NOTHING'
),
{"source": body.sourceId, "target": body.targetId},
)
await db.execute(
text('DELETE FROM "NovelGenre" WHERE "genreId" = :source'),
{"source": body.sourceId},
)
await db.execute(text('DELETE FROM "Genre" WHERE id = :id'), {"id": body.sourceId})
await db.commit()
return {"success": True}
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# /api/mod/series # /api/mod/series
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
@@ -2229,7 +2295,7 @@ async def mod_upload_epub(
), ),
{ {
"id": novel_id, "title": meta_title, "slug": slug, "id": novel_id, "title": meta_title, "slug": slug,
"author": meta_author, "desc": meta_description, "author": meta_author, "desc": meta_description or "",
"cover": cover_url, "uid": user["id"], "sid": series_id, "cover": cover_url, "uid": user["id"], "sid": series_id,
}, },
) )
+6823
View File
File diff suppressed because it is too large Load Diff