Files
HuiBrain/app/services/page_service.py
EduBrain Dev b17786b57b feat: v1.2.0 - 图片去重与管理、微信机器人优化、搜索设置可配置
主要功能:
- 图片上传时 OCR 内容去重(3个上传端点统一使用公共函数 _check_ocr_duplicate)
- 图片管理 Tab:展示所有图片、手动删除、一键去重
- 搜索结果详情弹窗增加删除按钮(带确认弹窗)
- 图片管理卡片点击查看详情(复用 showOcrDetailModal)
- 搜索限制和 LLM 批量判断数量可通过网站设置
- MiniMax API 调用添加 reasoning_split=True
- 企业微信机器人:WebSocket 长连接、图片搜索、配置化搜索数量
- 版本号升级至 1.2.0
2026-04-13 22:25:08 +08:00

203 lines
6.6 KiB
Python

"""
知识页面服务
提供知识页面的 CRUD 操作
"""
from __future__ import annotations
import logging
from typing import List, Optional
from sqlalchemy import func, select, text
from sqlalchemy.ext.asyncio import AsyncSession
from app.models.base import KnowledgeChunk, KnowledgePage
from app.schemas.page import PageCreate, PageResponse, PageUpdate
logger = logging.getLogger(__name__)
class PageService:
"""知识页面服务"""
def __init__(self, db: AsyncSession):
self.db = db
async def create_page(self, data: PageCreate) -> KnowledgePage:
"""创建知识页面"""
page = KnowledgePage(
title=data.title,
content=data.content,
source_file=data.source_file,
course_name=data.course_name,
teacher_name=data.teacher_name,
live_date=data.live_date,
page_number=data.page_number,
metadata_json=data.metadata_json,
)
self.db.add(page)
await self.db.flush()
await self.db.refresh(page)
return page
async def get_page(self, page_id: int) -> Optional[KnowledgePage]:
"""根据 ID 获取知识页面"""
result = await self.db.execute(
select(KnowledgePage).where(KnowledgePage.id == page_id)
)
return result.scalar_one_or_none()
async def list_pages(
self,
page: int = 1,
page_size: int = 20,
course_name: Optional[str] = None,
teacher_name: Optional[str] = None,
keyword: Optional[str] = None,
) -> dict:
"""
分页查询知识页面列表。
Args:
page: 页码(从 1 开始)
page_size: 每页数量
course_name: 按课程名称过滤
teacher_name: 按讲师名称过滤
keyword: 关键词搜索(标题或内容)
Returns:
{"total": int, "page": int, "page_size": int, "items": list}
"""
query = select(KnowledgePage)
count_query = select(func.count(KnowledgePage.id))
# 过滤条件
if course_name:
query = query.where(KnowledgePage.course_name == course_name)
count_query = count_query.where(KnowledgePage.course_name == course_name)
if teacher_name:
query = query.where(KnowledgePage.teacher_name == teacher_name)
count_query = count_query.where(KnowledgePage.teacher_name == teacher_name)
if keyword:
like_pattern = f"%{keyword}%"
query = query.where(
KnowledgePage.title.ilike(like_pattern)
| KnowledgePage.content.ilike(like_pattern)
)
count_query = count_query.where(
KnowledgePage.title.ilike(like_pattern)
| KnowledgePage.content.ilike(like_pattern)
)
# 总数
total_result = await self.db.execute(count_query)
total = total_result.scalar() or 0
# 分页
offset = (page - 1) * page_size
query = query.order_by(KnowledgePage.created_at.desc()).offset(offset).limit(page_size)
result = await self.db.execute(query)
items = result.scalars().all()
return {
"total": total,
"page": page,
"page_size": page_size,
"items": items,
}
async def update_page(self, page_id: int, data: PageUpdate) -> Optional[KnowledgePage]:
"""更新知识页面"""
page = await self.get_page(page_id)
if page is None:
return None
update_data = data.model_dump(exclude_unset=True)
for field, value in update_data.items():
setattr(page, field, value)
await self.db.flush()
await self.db.refresh(page)
return page
async def delete_page(self, page_id: int) -> bool:
"""删除知识页面及其关联分块"""
page = await self.get_page(page_id)
if page is None:
return False
# 先删除关联的分块
await self.db.execute(
text("DELETE FROM knowledge_chunks WHERE page_id = :page_id"),
{"page_id": page_id},
)
# 再删除页面
await self.db.delete(page)
await self.db.flush()
return True
async def get_page_chunks(self, page_id: int) -> List[KnowledgeChunk]:
"""获取知识页面的所有分块"""
result = await self.db.execute(
select(KnowledgeChunk)
.where(KnowledgeChunk.page_id == page_id)
.order_by(KnowledgeChunk.chunk_index)
)
return list(result.scalars().all())
async def reindex_page(self, page_id: int) -> dict:
"""
重新索引知识页面:重新分块并生成嵌入向量。
用于页面内容更新后刷新向量索引。
"""
page = await self.get_page(page_id)
if page is None:
raise ValueError(f"知识页面不存在: {page_id}")
# 删除旧分块
await self.db.execute(
text("DELETE FROM knowledge_chunks WHERE page_id = :page_id"),
{"page_id": page_id},
)
# 重新分块
from app.services.import_service import ImportService
chunks = ImportService._chunk_text(page.content)
if not chunks:
await self.db.flush()
return {"page_id": page_id, "chunks": 0}
# 生成嵌入并插入
from app.services.embedding_service import EmbeddingService
try:
embeddings = await EmbeddingService.embed_batch(chunks)
except Exception as exc:
logger.error("重新索引嵌入失败: %s", exc)
embeddings = [None] * len(chunks)
for idx, (chunk_text, embedding) in enumerate(zip(chunks, embeddings)):
embedding_str = None
if embedding:
embedding_str = "[" + ",".join(str(x) for x in embedding) + "]"
await self.db.execute(text("""
INSERT INTO knowledge_chunks (page_id, chunk_index, content, embedding)
VALUES (:page_id, :chunk_index, :content, :embedding::vector)
"""), {
"page_id": page_id,
"chunk_index": idx,
"content": chunk_text,
"embedding": embedding_str,
})
# 更新全文搜索向量
await self.db.execute(text("""
UPDATE knowledge_chunks
SET search_vector = to_tsvector('chinese_zh', content)
WHERE page_id = :page_id
"""), {"page_id": page_id})
await self.db.flush()
return {"page_id": page_id, "chunks": len(chunks)}