refactor: 移除 PostgreSQL 支持,简化为纯 SQLite 部署

- config.py: DATABASE_URL 默认值改为 SQLite
- database.py: 移除 PostgreSQL 分支,简化为纯 SQLite
- models/base.py: 移除 pgvector 导入和条件分支
- search_service.py: 移除 _search_postgres 方法
- import_service.py: 移除 pgvector 相关代码
- requirements.txt: 移除 asyncpg/alembic/pgvector 依赖
- pyproject.toml: 同步移除相关依赖
- docker-compose.yml: 移除 db 服务,- 删除 alembic.ini/Dockerfile.db/sql 目录
- README.md: 更新文档,移除 PostgreSQL 相关内容

适合 NAS 等资源受限环境的轻量级部署
This commit is contained in:
EduBrain Dev
2026-04-14 14:50:53 +08:00
parent f60b45358d
commit 496e11e26e
14 changed files with 112 additions and 697 deletions

View File

@@ -1,26 +1,18 @@
"""
SQLAlchemy 基础模型 + 通用 Mixin
提供 DeclarativeBase、时间戳 Mixin 和公共序列化方法
兼容 PostgreSQLpgvector和 SQLite本地开发
ORM 模型定义
SQLite 数据库模型,使用 Text 存储 JSON 格式的向量。
"""
from __future__ import annotations
import json
from datetime import datetime
from typing import Any, Dict
from sqlalchemy import DateTime, Float, Integer, String, Text, func
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from app.database import IS_SQLITE
# SQLite 下不使用 pgvector用 Text 存储向量JSON 格式)
if IS_SQLITE:
VectorType = Text
else:
from pgvector.sqlalchemy import Vector
VectorType = Vector
class Base(DeclarativeBase):
"""所有 ORM 模型的基类"""
@@ -28,96 +20,83 @@ class Base(DeclarativeBase):
class TimestampMixin:
"""
时间戳 Mixin为模型自动添加 created_at / updated_at 字段。
"""
"""时间戳 Mixin"""
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
comment="创建时间",
DateTime, default=func.now(), comment="创建时间"
)
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
onupdate=func.now(),
comment="更新时间",
DateTime, default=func.now(), onupdate=func.now(), comment="更新时间"
)
def to_dict(self) -> Dict[str, Any]:
"""将模型实例转换为字典"""
result: Dict[str, Any] = {}
for column in self.__table__.columns: # type: ignore[attr-defined]
value = getattr(self, column.name, None)
if isinstance(value, datetime):
value = value.isoformat()
elif hasattr(value, "tolist"):
value = value.tolist()
result[column.name] = value
return result
# ──────────────────────────── 业务模型 ────────────────────────────
class KnowledgePage(Base, TimestampMixin):
"""知识页面模型"""
"""知识页面"""
__tablename__ = "knowledge_pages"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(500), nullable=False, comment="页面标题")
content: Mapped[str] = mapped_column(Text, nullable=False, comment="页面正文内容")
source_file: Mapped[str | None] = mapped_column(String(500), nullable=True, comment="来源文件名")
course_name: Mapped[str | None] = mapped_column(String(200), nullable=True, comment="课程名称")
teacher_name: Mapped[str | None] = mapped_column(String(100), nullable=True, comment="讲师名称")
live_date: Mapped[str | None] = mapped_column(String(20), nullable=True, comment="直播日期")
page_number: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="原始页码")
metadata_json: Mapped[str | None] = mapped_column(Text, nullable=True, comment="额外元数据")
content: Mapped[str] = mapped_column(Text, nullable=False, comment="页面内容")
source: Mapped[str | None] = mapped_column(String(200), nullable=True, comment="来源文件名")
page_number: Mapped[int | None] = mapped_column(Integer, nullable=True, comment="原文件页码")
# 向量嵌入JSON 格式存储)
embedding: Mapped[str | None] = mapped_column(Text, nullable=True, comment="向量嵌入JSON")
@property
def embedding_vector(self) -> list[float] | None:
if self.embedding is None:
return None
return json.loads(self.embedding)
class KnowledgeChunk(Base, TimestampMixin):
"""知识分块模型"""
__tablename__ = "knowledge_chunks"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
page_id: Mapped[int] = mapped_column(Integer, nullable=False, index=True, comment="关联页面 ID")
chunk_index: Mapped[int] = mapped_column(Integer, nullable=False, comment="分块序号")
content: Mapped[str] = mapped_column(Text, nullable=False, comment="分块文本内容")
embedding: Mapped[list | None] = mapped_column(
VectorType(768) if not IS_SQLITE else Text,
nullable=True,
comment="嵌入向量",
)
search_vector: Mapped[Any | None] = mapped_column(
Text, nullable=True, comment="全文搜索向量(仅 PG",
)
def to_dict(self) -> Dict[str, Any]:
result = super().to_dict()
result.pop("embedding", None)
result.pop("search_vector", None)
return result
@embedding_vector.setter
def embedding_vector(self, value: list[float] | None):
self.embedding = json.dumps(value) if value else None
class OCRImage(Base, TimestampMixin):
"""OCR 图片记录模型"""
"""OCR 图片记录"""
__tablename__ = "ocr_images"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
file_path: Mapped[str] = mapped_column(String(500), nullable=False, comment="图片文件路径")
ocr_text: Mapped[str | None] = mapped_column(Text, nullable=True, comment="OCR 识别全文")
confidence: Mapped[float | None] = mapped_column(Float, nullable=True, comment="平均置信度")
original_filename: Mapped[str | None] = mapped_column(String(200), nullable=True, comment="原始文件名")
ocr_text: Mapped[str | None] = mapped_column(Text, nullable=True, comment="OCR 识别文本")
confidence: Mapped[float | None] = mapped_column(Float, nullable=True, comment="OCR 置信度")
provider: Mapped[str | None] = mapped_column(String(50), nullable=True, comment="OCR 提供商")
status: Mapped[str] = mapped_column(String(20), default="pending", comment="处理状态")
error_message: Mapped[str | None] = mapped_column(Text, nullable=True, comment="错误信息")
tags: Mapped[str | None] = mapped_column(Text, nullable=True, comment="LLM 提取的标签JSON 数组格式")
story_summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="Qwen3-8B 提炼的故事摘要")
status: Mapped[str] = mapped_column(
String(20), default="pending", comment="状态: pending/processing/completed/failed"
)
tags: Mapped[str | None] = mapped_column(Text, nullable=True, comment="关键词标签JSON 数组)")
story_summary: Mapped[str | None] = mapped_column(Text, nullable=True, comment="故事摘要")
blocks: Mapped[str | None] = mapped_column(Text, nullable=True, comment="OCR 文本块JSON")
def to_dict(self) -> Dict[str, Any]:
result = super().to_dict()
# 将 tags 从 JSON 字符串解析为列表返回
if result.get("tags") and isinstance(result["tags"], str):
try:
import json
result["tags"] = json.loads(result["tags"])
except (json.JSONDecodeError, TypeError):
pass
return result
@property
def tags_list(self) -> list[str]:
if self.tags is None:
return []
return json.loads(self.tags)
@tags_list.setter
def tags_list(self, value: list[str]):
self.tags = json.dumps(value) if value else None
@property
def blocks_list(self) -> list[dict]:
if self.blocks is None:
return []
return json.loads(self.blocks)
@blocks_list.setter
def blocks_list(self, value: list[dict]):
self.blocks = json.dumps(value) if value else None
class WebsiteSettings(Base):
"""网站设置(单例)"""
__tablename__ = "website_settings"
key: Mapped[str] = mapped_column(String(100), primary_key=True, comment="设置键")
value: Mapped[str | None] = mapped_column(Text, nullable=True, comment="设置值JSON")
updated_at: Mapped[datetime] = mapped_column(
DateTime, default=func.now(), onupdate=func.now(), comment="更新时间"
)