"""法规原文读取服务 — 从 law-pack 目录读取 markdown,从 SQLite 取列表""" import sqlite3 import logging from typing import Optional, Dict, Any, List from pathlib import Path from app.config import SQLITE_PATH, LAW_PACK_DIR logger = logging.getLogger(__name__) class LawReader: """法规原文读取单例""" _instance: Optional["LawReader"] = None @classmethod def get_instance(cls) -> "LawReader": if cls._instance is None: cls._instance = cls() return cls._instance def __init__(self): self._conn: Optional[sqlite3.Connection] = None def _ensure_conn(self): if self._conn is None and SQLITE_PATH.exists(): self._conn = sqlite3.connect(str(SQLITE_PATH), check_same_thread=False) self._conn.row_factory = sqlite3.Row def list_laws( self, category: Optional[str] = None, province: Optional[str] = None, keyword: Optional[str] = None, page: int = 1, page_size: int = 50, ) -> Dict[str, Any]: """法规列表(分页 + 过滤)""" self._ensure_conn() if self._conn is None: return {"results": [], "total": 0, "page": page, "page_size": page_size} where = [] params: List[Any] = [] if category: where.append("category = ?") params.append(category) if province: where.append("province = ?") params.append(province) if keyword: where.append("name LIKE ?") params.append(f"%{keyword}%") where_clause = f"WHERE {' AND '.join(where)}" if where else "" # 总数 total = self._conn.execute( f"SELECT COUNT(*) FROM laws {where_clause}", params ).fetchone()[0] # 分页 offset = (page - 1) * page_size rows = self._conn.execute( f""" SELECT id, name, category, publish_date, province, city, clause_count, file_path FROM laws {where_clause} ORDER BY category, name LIMIT ? OFFSET ? """, params + [page_size, offset], ).fetchall() results = [ { "law_id": row["id"], "name": row["name"], "category": row["category"], "publish_date": row["publish_date"], "province": row["province"], "city": row["city"], "clause_count": row["clause_count"], "file_path": row["file_path"], } for row in rows ] return { "results": results, "total": total, "page": page, "page_size": page_size, } def get_law(self, law_id: int) -> Optional[Dict[str, Any]]: """法规详情(含原文 + 条文列表)""" self._ensure_conn() if self._conn is None: return None row = self._conn.execute( "SELECT * FROM laws WHERE id = ?", (law_id,) ).fetchone() if row is None: return None # 读取原文(路径校验防穿越) file_path = Path(row["file_path"]) try: # 确保在 law-pack 目录内 abs_path = file_path.resolve() pack_root = LAW_PACK_DIR.resolve() if not str(abs_path).startswith(str(pack_root)): logger.error(f"路径穿越尝试: {file_path}") return None content = abs_path.read_text(encoding="utf-8") except Exception as e: logger.error(f"读取原文失败 {file_path}: {e}") content = f"[读取失败: {e}]" # 条文列表 clauses = self._conn.execute( "SELECT id, chapter, clause_no, content FROM clauses WHERE law_id = ? ORDER BY id", (law_id,), ).fetchall() clause_list = [ { "clause_id": c["id"], "chapter": c["chapter"], "clause_no": c["clause_no"], "content": c["content"], } for c in clauses ] return { "law_id": row["id"], "name": row["name"], "category": row["category"], "publish_date": row["publish_date"], "province": row["province"], "city": row["city"], "content": content, "file_path": row["file_path"], "clauses": clause_list, }