166 lines
5.5 KiB
Python
166 lines
5.5 KiB
Python
"""T6.5 案例管理 DAO。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from contextlib import contextmanager
|
|
from typing import Iterator, cast
|
|
|
|
from admin.db import get_connection, utc_now_iso
|
|
from data.cases.models import CaseCategory, CaseRecord, CaseReviewStatus
|
|
|
|
|
|
class CaseNotFound(LookupError):
|
|
"""案例不存在。"""
|
|
|
|
|
|
class CasesDAO:
|
|
def __init__(self, conn: sqlite3.Connection):
|
|
self.conn = conn
|
|
|
|
@classmethod
|
|
@contextmanager
|
|
def connect(cls, db_path: str) -> Iterator["CasesDAO"]:
|
|
conn = get_connection(db_path)
|
|
try:
|
|
yield cls(conn)
|
|
finally:
|
|
conn.close()
|
|
|
|
def list(
|
|
self,
|
|
*,
|
|
category: str | None = None,
|
|
review_status: str | None = None,
|
|
limit: int = 50,
|
|
offset: int = 0,
|
|
) -> tuple[list[CaseRecord], int]:
|
|
where = []
|
|
params: list[object] = []
|
|
if category is not None:
|
|
where.append("category = ?")
|
|
params.append(category)
|
|
if review_status is not None:
|
|
where.append("review_status = ?")
|
|
params.append(review_status)
|
|
where_sql = f" WHERE {' AND '.join(where)}" if where else ""
|
|
|
|
total_row = self.conn.execute(
|
|
f"SELECT COUNT(*) AS n FROM cases{where_sql}", params
|
|
).fetchone()
|
|
rows = self.conn.execute(
|
|
f"SELECT * FROM cases{where_sql} ORDER BY id DESC LIMIT ? OFFSET ?",
|
|
[*params, limit, offset],
|
|
).fetchall()
|
|
total = int(total_row["n"] if total_row is not None else 0)
|
|
return [self._from_row(row) for row in rows], total
|
|
|
|
def get(self, case_id: int) -> CaseRecord:
|
|
row = self.conn.execute(
|
|
"SELECT * FROM cases WHERE id = ?", (case_id,)
|
|
).fetchone()
|
|
if row is None:
|
|
raise CaseNotFound(case_id)
|
|
return self._from_row(row)
|
|
|
|
def create(self, record: CaseRecord) -> CaseRecord:
|
|
now = utc_now_iso()
|
|
cur = self.conn.execute(
|
|
"""
|
|
INSERT INTO cases(
|
|
title, category, summary, content,
|
|
review_status, review_note, reviewer, reviewed_at,
|
|
created_at, updated_at, tags
|
|
) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
""",
|
|
(
|
|
record.title,
|
|
record.category,
|
|
record.summary,
|
|
record.content,
|
|
record.review_status,
|
|
record.review_note,
|
|
record.reviewer,
|
|
record.reviewed_at,
|
|
now,
|
|
now,
|
|
json.dumps(record.tags, ensure_ascii=False),
|
|
),
|
|
)
|
|
case_id = int(cur.lastrowid or 0)
|
|
return self.get(case_id)
|
|
|
|
def update(self, case_id: int, *, updates: dict[str, object]) -> CaseRecord:
|
|
existing = self.get(case_id)
|
|
merged = CaseRecord(
|
|
id=existing.id,
|
|
title=cast(str, updates.get("title", existing.title)),
|
|
category=cast(CaseCategory, updates.get("category", existing.category)),
|
|
summary=cast(str | None, updates.get("summary", existing.summary)),
|
|
content=cast(str | None, updates.get("content", existing.content)),
|
|
review_status=cast(
|
|
CaseReviewStatus,
|
|
updates.get("review_status", existing.review_status),
|
|
),
|
|
review_note=cast(
|
|
str | None, updates.get("review_note", existing.review_note)
|
|
),
|
|
reviewer=cast(str | None, updates.get("reviewer", existing.reviewer)),
|
|
reviewed_at=cast(
|
|
str | None, updates.get("reviewed_at", existing.reviewed_at)
|
|
),
|
|
created_at=existing.created_at,
|
|
updated_at=utc_now_iso(),
|
|
tags=cast(list[str], updates.get("tags", existing.tags)),
|
|
)
|
|
self.conn.execute(
|
|
"""
|
|
UPDATE cases
|
|
SET title = ?, category = ?, summary = ?, content = ?,
|
|
review_status = ?, review_note = ?, reviewer = ?, reviewed_at = ?,
|
|
updated_at = ?, tags = ?
|
|
WHERE id = ?
|
|
""",
|
|
(
|
|
merged.title,
|
|
merged.category,
|
|
merged.summary,
|
|
merged.content,
|
|
merged.review_status,
|
|
merged.review_note,
|
|
merged.reviewer,
|
|
merged.reviewed_at,
|
|
merged.updated_at,
|
|
json.dumps(merged.tags, ensure_ascii=False),
|
|
case_id,
|
|
),
|
|
)
|
|
return self.get(case_id)
|
|
|
|
def delete(self, case_id: int) -> None:
|
|
cur = self.conn.execute("DELETE FROM cases WHERE id = ?", (case_id,))
|
|
if cur.rowcount == 0:
|
|
raise CaseNotFound(case_id)
|
|
|
|
@staticmethod
|
|
def _from_row(row: sqlite3.Row) -> CaseRecord:
|
|
tags_raw = row["tags"] or "[]"
|
|
tags = json.loads(tags_raw)
|
|
if not isinstance(tags, list):
|
|
tags = []
|
|
return CaseRecord(
|
|
id=int(row["id"]),
|
|
title=row["title"],
|
|
category=row["category"],
|
|
summary=row["summary"],
|
|
content=row["content"],
|
|
review_status=row["review_status"],
|
|
review_note=row["review_note"],
|
|
reviewer=row["reviewer"],
|
|
reviewed_at=row["reviewed_at"],
|
|
created_at=row["created_at"],
|
|
updated_at=row["updated_at"],
|
|
tags=[str(item) for item in tags],
|
|
)
|