fix column

This commit is contained in:
felix
2025-11-22 18:50:52 +08:00
parent 9215112d6f
commit 505a1ce8b7
19 changed files with 75 additions and 886 deletions

View File

@@ -10,7 +10,6 @@ RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list.d/debi
RUN apt-get update \
&& apt-get install -y --no-install-recommends gcc python3-dev supervisor \
&& rm -rf /var/lib/apt/lists/* \
# 某些包可能存在同步不及时导致安装失败的情况可更改为官方源https://pypi.org/simple
&& pip install --upgrade pip -i https://mirrors.aliyun.com/pypi/simple \
&& pip install -r requirements.txt -i https://mirrors.aliyun.com/pypi/simple \
&& pip install gunicorn wait-for-it -i https://mirrors.aliyun.com/pypi/simple
@@ -27,4 +26,4 @@ COPY deploy/fastapi_server.conf /etc/supervisor/conf.d/
EXPOSE 8001
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8001"]
CMD ["uvicorn", "backend.main:app", "--host", "0.0.0.0", "--port", "8080"]

View File

@@ -1,7 +1,6 @@
from datetime import datetime
from datetime import datetime, timedelta
from typing import Optional, Dict, Any
from asyncpg.pgproto.pgproto import timedelta
from sqlalchemy import select, update, func
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus

View File

@@ -1,342 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import List
from fastapi import APIRouter, Request, Query
from starlette.background import BackgroundTasks
from backend.app.ai.schema.article import ArticleSchema, ArticleWithParagraphsSchema, CreateArticleParam, UpdateArticleParam, ArticleParagraphSchema, CreateArticleParagraphParam, UpdateArticleParagraphParam, ArticleSentenceSchema, CreateArticleSentenceParam, UpdateArticleSentenceParam
from backend.app.ai.service.article_service import article_service
from backend.common.response.response_schema import response_base, ResponseSchemaModel
from backend.common.security.jwt import DependsJwtAuth
router = APIRouter()
@router.post("", summary="创建文章", dependencies=[DependsJwtAuth])
async def create_article(
request: Request,
background_tasks: BackgroundTasks,
params: CreateArticleParam
) -> ResponseSchemaModel[ArticleSchema]:
"""
创建文章记录
请求体参数:
- title: 文章标题
- content: 文章完整内容
- author: 作者(可选)
- category: 分类(可选)
- level: 难度等级(可选)
- info: 附加信息(可选)
返回:
- 创建的文章记录
"""
article_id = await article_service.create_article(obj=params)
article = await article_service.get_article_by_id(article_id)
return response_base.success(data=article)
@router.get("/{article_id}", summary="获取文章详情", dependencies=[DependsJwtAuth])
async def get_article(
article_id: int
) -> ResponseSchemaModel[ArticleWithParagraphsSchema]:
"""
获取文章详情,包括所有段落和句子
参数:
- article_id: 文章ID
返回:
- 文章记录及其所有段落和句子
"""
article = await article_service.get_article_with_content(article_id)
if not article:
return response_base.fail(code=404, msg="文章不存在")
return response_base.success(data=article)
@router.put("/{article_id}", summary="更新文章", dependencies=[DependsJwtAuth])
async def update_article(
article_id: int,
request: Request,
background_tasks: BackgroundTasks,
params: UpdateArticleParam
) -> ResponseSchemaModel[ArticleSchema]:
"""
更新文章记录
参数:
- article_id: 文章ID
请求体参数:
- title: 文章标题
- content: 文章完整内容
- author: 作者(可选)
- category: 分类(可选)
- level: 难度等级(可选)
- info: 附加信息(可选)
返回:
- 更新后的文章记录
"""
success = await article_service.update_article(article_id, params)
if not success:
return response_base.fail(code=404, msg="文章不存在")
article = await article_service.get_article_by_id(article_id)
return response_base.success(data=article)
@router.delete("/{article_id}", summary="删除文章", dependencies=[DependsJwtAuth])
async def delete_article(
article_id: int,
request: Request,
background_tasks: BackgroundTasks
) -> ResponseSchemaModel[None]:
"""
删除文章记录
参数:
- article_id: 文章ID
返回:
- 无
"""
success = await article_service.delete_article(article_id)
if not success:
return response_base.fail(code=404, msg="文章不存在")
return response_base.success()
@router.post("/paragraph", summary="创建文章段落", dependencies=[DependsJwtAuth])
async def create_article_paragraph(
request: Request,
background_tasks: BackgroundTasks,
params: CreateArticleParagraphParam
) -> ResponseSchemaModel[ArticleParagraphSchema]:
"""
创建文章段落记录
请求体参数:
- article_id: 关联的文章ID
- paragraph_index: 段落序号
- content: 段落内容
- standard_audio_id: 标准朗读音频文件ID可选
- info: 附加信息(可选)
返回:
- 创建的段落记录
"""
paragraph_id = await article_service.create_article_paragraph(obj=params)
paragraph = await article_service.get_article_paragraph_by_id(paragraph_id)
return response_base.success(data=paragraph)
@router.put("/paragraph/{paragraph_id}", summary="更新文章段落", dependencies=[DependsJwtAuth])
async def update_article_paragraph(
paragraph_id: int,
request: Request,
background_tasks: BackgroundTasks,
params: UpdateArticleParagraphParam
) -> ResponseSchemaModel[ArticleParagraphSchema]:
"""
更新文章段落记录
参数:
- paragraph_id: 段落ID
请求体参数:
- article_id: 关联的文章ID
- paragraph_index: 段落序号
- content: 段落内容
- standard_audio_id: 标准朗读音频文件ID可选
- info: 附加信息(可选)
返回:
- 更新后的段落记录
"""
success = await article_service.update_article_paragraph(paragraph_id, params)
if not success:
return response_base.fail(code=404, msg="段落不存在")
paragraph = await article_service.get_article_paragraph_by_id(paragraph_id)
return response_base.success(data=paragraph)
@router.delete("/paragraph/{paragraph_id}", summary="删除文章段落", dependencies=[DependsJwtAuth])
async def delete_article_paragraph(
paragraph_id: int,
request: Request,
background_tasks: BackgroundTasks
) -> ResponseSchemaModel[None]:
"""
删除文章段落记录
参数:
- paragraph_id: 段落ID
返回:
- 无
"""
success = await article_service.delete_article_paragraph(paragraph_id)
if not success:
return response_base.fail(code=404, msg="段落不存在")
return response_base.success()
@router.get("/paragraph/{paragraph_id}", summary="获取段落详情", dependencies=[DependsJwtAuth])
async def get_article_paragraph(
paragraph_id: int
) -> ResponseSchemaModel[ArticleParagraphSchema]:
"""
获取文章段落详情
参数:
- paragraph_id: 段落ID
返回:
- 段落记录
"""
paragraph = await article_service.get_article_paragraph_by_id(paragraph_id)
if not paragraph:
return response_base.fail(code=404, msg="段落不存在")
return response_base.success(data=paragraph)
@router.get("/paragraph", summary="获取文章的所有段落", dependencies=[DependsJwtAuth])
async def get_article_paragraphs(
article_id: int = Query(..., description="文章ID")
) -> ResponseSchemaModel[List[ArticleParagraphSchema]]:
"""
获取指定文章的所有段落记录
参数:
- article_id: 文章ID
返回:
- 文章的所有段落记录列表
"""
paragraphs = await article_service.get_article_paragraphs_by_article_id(article_id)
return response_base.success(data=paragraphs)
@router.post("/sentence", summary="创建文章句子", dependencies=[DependsJwtAuth])
async def create_article_sentence(
request: Request,
background_tasks: BackgroundTasks,
params: CreateArticleSentenceParam
) -> ResponseSchemaModel[ArticleSentenceSchema]:
"""
创建文章句子记录
请求体参数:
- paragraph_id: 关联的段落ID
- sentence_index: 句子序号
- content: 句子内容
- standard_audio_id: 标准朗读音频文件ID可选
- info: 附加信息(可选)
返回:
- 创建的句子记录
"""
sentence_id = await article_service.create_article_sentence(obj=params)
sentence = await article_service.get_article_sentence_by_id(sentence_id)
return response_base.success(data=sentence)
@router.put("/sentence/{sentence_id}", summary="更新文章句子", dependencies=[DependsJwtAuth])
async def update_article_sentence(
sentence_id: int,
request: Request,
background_tasks: BackgroundTasks,
params: UpdateArticleSentenceParam
) -> ResponseSchemaModel[ArticleSentenceSchema]:
"""
更新文章句子记录
参数:
- sentence_id: 句子ID
请求体参数:
- paragraph_id: 关联的段落ID
- sentence_index: 句子序号
- content: 句子内容
- standard_audio_id: 标准朗读音频文件ID可选
- info: 附加信息(可选)
返回:
- 更新后的句子记录
"""
success = await article_service.update_article_sentence(sentence_id, params)
if not success:
return response_base.fail(code=404, msg="句子不存在")
sentence = await article_service.get_article_sentence_by_id(sentence_id)
return response_base.success(data=sentence)
@router.delete("/sentence/{sentence_id}", summary="删除文章句子", dependencies=[DependsJwtAuth])
async def delete_article_sentence(
sentence_id: int,
request: Request,
background_tasks: BackgroundTasks
) -> ResponseSchemaModel[None]:
"""
删除文章句子记录
参数:
- sentence_id: 句子ID
返回:
- 无
"""
success = await article_service.delete_article_sentence(sentence_id)
if not success:
return response_base.fail(code=404, msg="句子不存在")
return response_base.success()
@router.get("/sentence/{sentence_id}", summary="获取句子详情", dependencies=[DependsJwtAuth])
async def get_article_sentence(
sentence_id: int
) -> ResponseSchemaModel[ArticleSentenceSchema]:
"""
获取文章句子详情
参数:
- sentence_id: 句子ID
返回:
- 句子记录
"""
sentence = await article_service.get_article_sentence_by_id(sentence_id)
if not sentence:
return response_base.fail(code=404, msg="句子不存在")
return response_base.success(data=sentence)
@router.get("/sentence", summary="获取段落的所有句子", dependencies=[DependsJwtAuth])
async def get_article_sentences(
paragraph_id: int = Query(..., description="段落ID")
) -> ResponseSchemaModel[List[ArticleSentenceSchema]]:
"""
获取指定段落的所有句子记录
参数:
- paragraph_id: 段落ID
返回:
- 段落的所有句子记录列表
"""
sentences = await article_service.get_article_sentences_by_paragraph_id(paragraph_id)
return response_base.success(data=sentences)

View File

@@ -5,12 +5,10 @@ from fastapi import APIRouter
from backend.app.ai.api.image import router as image_router
from backend.app.ai.api.recording import router as recording_router
from backend.app.ai.api.image_text import router as image_text_router
from backend.app.ai.api.article import router as article_router
from backend.core.conf import settings
v1 = APIRouter(prefix=settings.FASTAPI_API_V1_PATH)
v1.include_router(image_router, prefix='/image', tags=['AI图片服务'])
v1.include_router(recording_router, prefix='/recording', tags=['AI录音服务'])
v1.include_router(image_text_router, prefix='/image_text', tags=['AI图片文本服务'])
# v1.include_router(article_router, prefix='/article', tags=['AI文章服务'])
v1.include_router(image_text_router, prefix='/image_text', tags=['AI图片文本服务'])

View File

@@ -1,65 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Optional, List
from sqlalchemy import select, and_
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy_crud_plus import CRUDPlus
from backend.app.ai.model.article import Article, ArticleParagraph, ArticleSentence
class CRUDArticle(CRUDPlus[Article]):
async def get_by_title(self, db: AsyncSession, title: str) -> Optional[Article]:
"""根据标题获取文章"""
stmt = select(self.model).where(self.model.title == title)
result = await db.execute(stmt)
return result.scalar_one_or_none()
async def get_articles_by_category(self, db: AsyncSession, category: str) -> List[Article]:
"""根据分类获取文章列表"""
stmt = select(self.model).where(self.model.category == category)
result = await db.execute(stmt)
return list(result.scalars().all())
class CRUDArticleParagraph(CRUDPlus[ArticleParagraph]):
async def get_by_article_id(self, db: AsyncSession, article_id: int) -> List[ArticleParagraph]:
"""根据文章ID获取所有段落按序号排序"""
stmt = select(self.model).where(self.model.article_id == article_id).order_by(self.model.paragraph_index)
result = await db.execute(stmt)
return list(result.scalars().all())
async def get_by_article_id_and_index(self, db: AsyncSession, article_id: int, paragraph_index: int) -> Optional[ArticleParagraph]:
"""根据文章ID和段落序号获取段落"""
stmt = select(self.model).where(
and_(
self.model.article_id == article_id,
self.model.paragraph_index == paragraph_index
)
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
class CRUDArticleSentence(CRUDPlus[ArticleSentence]):
async def get_by_paragraph_id(self, db: AsyncSession, paragraph_id: int) -> List[ArticleSentence]:
"""根据段落ID获取所有句子按序号排序"""
stmt = select(self.model).where(self.model.paragraph_id == paragraph_id).order_by(self.model.sentence_index)
result = await db.execute(stmt)
return list(result.scalars().all())
async def get_by_paragraph_id_and_index(self, db: AsyncSession, paragraph_id: int, sentence_index: int) -> Optional[ArticleSentence]:
"""根据段落ID和句子序号获取句子"""
stmt = select(self.model).where(
and_(
self.model.paragraph_id == paragraph_id,
self.model.sentence_index == sentence_index
)
)
result = await db.execute(stmt)
return result.scalar_one_or_none()
article_dao: CRUDArticle = CRUDArticle(Article)
article_paragraph_dao: CRUDArticleParagraph = CRUDArticleParagraph(ArticleParagraph)
article_sentence_dao: CRUDArticleSentence = CRUDArticleSentence(ArticleSentence)

View File

@@ -1,68 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
from typing import Optional
from datetime import datetime
from sqlalchemy import BigInteger, Text, String, DateTime, ForeignKey
from sqlalchemy.dialects.mysql import JSON as MySQLJSON
from sqlalchemy.orm import mapped_column, Mapped
from backend.common.model import snowflake_id_key, Base
class Article(Base):
"""
文章内容
用于存储朗读文章的完整内容,支持多段落结构
"""
__tablename__ = 'article'
id: Mapped[snowflake_id_key] = mapped_column(BigInteger, init=False, primary_key=True)
title: Mapped[str] = mapped_column(String(255), nullable=False, comment="文章标题")
content: Mapped[str] = mapped_column(Text, nullable=False, comment="文章完整内容")
author: Mapped[Optional[str]] = mapped_column(String(100), nullable=True, comment="作者")
category: Mapped[Optional[str]] = mapped_column(String(50), nullable=True, comment="分类")
level: Mapped[Optional[str]] = mapped_column(String(20), nullable=True, comment="难度等级")
info: Mapped[Optional[dict]] = mapped_column(MySQLJSON, default=None, comment="附加信息")
# 表参数 - 包含所有必要的约束
__table_args__ = (
)
class ArticleParagraph(Base):
"""
文章段落
用于存储文章中的各个段落,支持段落级别的朗读和评估
"""
__tablename__ = 'article_paragraph'
id: Mapped[snowflake_id_key] = mapped_column(BigInteger, init=False, primary_key=True)
article_id: Mapped[int] = mapped_column(BigInteger, ForeignKey('article.id'), nullable=False, comment="关联的文章ID")
paragraph_index: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="段落序号")
content: Mapped[str] = mapped_column(Text, nullable=False, comment="段落内容")
standard_audio_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('file.id'), nullable=True, comment="标准朗读音频文件ID")
info: Mapped[Optional[dict]] = mapped_column(MySQLJSON, default=None, comment="附加信息")
# 表参数 - 包含所有必要的约束
__table_args__ = (
)
class ArticleSentence(Base):
"""
文章句子
用于存储段落中的各个句子,支持句子级别的朗读和评估
"""
__tablename__ = 'article_sentence'
id: Mapped[snowflake_id_key] = mapped_column(BigInteger, init=False, primary_key=True)
paragraph_id: Mapped[int] = mapped_column(BigInteger, ForeignKey('article_paragraph.id'), nullable=False, comment="关联的段落ID")
sentence_index: Mapped[int] = mapped_column(BigInteger, nullable=False, comment="句子序号")
content: Mapped[str] = mapped_column(Text, nullable=False, comment="句子内容")
standard_audio_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('file.id'), nullable=True, comment="标准朗读音频文件ID")
info: Mapped[Optional[dict]] = mapped_column(MySQLJSON, default=None, comment="附加信息")
# 表参数 - 包含所有必要的约束
__table_args__ = (
)

View File

@@ -19,7 +19,6 @@ class ImageText(Base):
id: Mapped[snowflake_id_key] = mapped_column(BigInteger, init=False, primary_key=True)
image_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('image.id'), nullable=True, comment="关联的图片ID")
article_sentence_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('article_sentence.id'), nullable=True, comment="关联的文章句子ID")
content: Mapped[str] = mapped_column(Text, nullable=False, comment="文本内容")
standard_audio_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('file.id'), nullable=True, comment="标准朗读音频文件ID")
ipa: Mapped[Optional[str]] = mapped_column(String(100), default=None, comment="ipa")

View File

@@ -20,7 +20,6 @@ class Recording(Base):
user_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('wx_user.id'), nullable=True, comment="关联的用户ID")
image_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('image.id'), nullable=True, comment="关联的图片ID")
image_text_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('image_text.id'), nullable=True, comment="关联的图片文本ID")
article_sentence_id: Mapped[Optional[int]] = mapped_column(BigInteger, ForeignKey('article_sentence.id'), nullable=True, comment="关联的文章句子ID")
text: Mapped[Optional[str]] = mapped_column(String(255), nullable=True, comment='朗读文本')
eval_mode: Mapped[Optional[int]] = mapped_column(Integer, nullable=True, comment='评测模式')
info: Mapped[Optional[RecordingMetadata]] = mapped_column(PydanticType(pydantic_type=RecordingMetadata), default=None, comment="附加元数据") # 其他可能的字段(根据实际需求添加)

View File

@@ -1,90 +0,0 @@
from typing import Optional, Dict, Any, List
from pydantic import BaseModel
from backend.common.schema import SchemaBase
class ArticleSchemaBase(SchemaBase):
"""文章基础结构"""
title: str
content: str
author: Optional[str] = None
category: Optional[str] = None
level: Optional[str] = None
info: Optional[dict] = None
class CreateArticleParam(ArticleSchemaBase):
"""创建文章参数"""
class UpdateArticleParam(ArticleSchemaBase):
"""更新文章参数"""
class ArticleSchema(ArticleSchemaBase):
"""文章信息"""
id: int
created_time: Optional[str] = None
updated_time: Optional[str] = None
class ArticleParagraphSchemaBase(SchemaBase):
"""文章段落基础结构"""
article_id: int
paragraph_index: int
content: str
standard_audio_id: Optional[int] = None
info: Optional[dict] = None
class CreateArticleParagraphParam(ArticleParagraphSchemaBase):
"""创建文章段落参数"""
class UpdateArticleParagraphParam(ArticleParagraphSchemaBase):
"""更新文章段落参数"""
class ArticleParagraphSchema(ArticleParagraphSchemaBase):
"""文章段落信息"""
id: int
created_time: Optional[str] = None
class ArticleSentenceSchemaBase(SchemaBase):
"""文章句子基础结构"""
paragraph_id: int
sentence_index: int
content: str
standard_audio_id: Optional[int] = None
info: Optional[dict] = None
class CreateArticleSentenceParam(ArticleSentenceSchemaBase):
"""创建文章句子参数"""
class UpdateArticleSentenceParam(ArticleSentenceSchemaBase):
"""更新文章句子参数"""
class ArticleSentenceSchema(ArticleSentenceSchemaBase):
"""文章句子信息"""
id: int
created_time: Optional[str] = None
class ArticleWithParagraphsSchema(ArticleSchema):
"""包含段落的文章信息"""
paragraphs: Optional[List['ArticleParagraphWithSentencesSchema']] = None
class ArticleParagraphWithSentencesSchema(ArticleParagraphSchema):
"""包含句子的段落信息"""
sentences: Optional[List[ArticleSentenceSchema]] = None
# Update forward references
ArticleWithParagraphsSchema.model_rebuild()
ArticleParagraphWithSentencesSchema.model_rebuild()

View File

@@ -1,215 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import logging
from typing import Optional, List
from sqlalchemy.ext.asyncio import AsyncSession
from backend.app.ai.crud.article_crud import article_dao, article_paragraph_dao, article_sentence_dao
from backend.app.ai.model.article import Article, ArticleParagraph, ArticleSentence
from backend.app.ai.schema.article import CreateArticleParam, UpdateArticleParam, CreateArticleParagraphParam, UpdateArticleParagraphParam, CreateArticleSentenceParam, UpdateArticleSentenceParam
from backend.database.db import async_db_session
logger = logging.getLogger(__name__)
class ArticleService:
@staticmethod
async def create_article(*, obj: CreateArticleParam) -> int:
"""
创建文章
:param obj: 文章创建参数
:return: 文章ID
"""
async with async_db_session.begin() as db:
article = await article_dao.create(db, obj)
return article.id
@staticmethod
async def get_article_by_id(article_id: int) -> Optional[Article]:
"""
根据ID获取文章
:param article_id: 文章ID
:return: 文章记录
"""
async with async_db_session() as db:
return await article_dao.get(db, article_id)
@staticmethod
async def get_article_with_content(article_id: int) -> Optional[dict]:
"""
获取文章及其所有段落和句子
:param article_id: 文章ID
:return: 包含段落和句子的文章信息
"""
async with async_db_session() as db:
article = await article_dao.get(db, article_id)
if not article:
return None
# 获取所有段落
paragraphs = await article_paragraph_dao.get_by_article_id(db, article_id)
# 获取所有句子
paragraph_ids = [p.id for p in paragraphs]
if paragraph_ids:
stmt = select(ArticleSentence).where(ArticleSentence.paragraph_id.in_(paragraph_ids)).order_by(ArticleSentence.paragraph_id, ArticleSentence.sentence_index)
result = await db.execute(stmt)
sentences = list(result.scalars().all())
# 将句子按段落分组
sentences_by_paragraph = {}
for sentence in sentences:
if sentence.paragraph_id not in sentences_by_paragraph:
sentences_by_paragraph[sentence.paragraph_id] = []
sentences_by_paragraph[sentence.paragraph_id].append(sentence)
# 将句子添加到对应的段落中
for paragraph in paragraphs:
paragraph.sentences = sentences_by_paragraph.get(paragraph.id, [])
article.paragraphs = paragraphs
return article
@staticmethod
async def update_article(article_id: int, obj: UpdateArticleParam) -> bool:
"""
更新文章
:param article_id: 文章ID
:param obj: 文章更新参数
:return: 是否更新成功
"""
async with async_db_session.begin() as db:
return await article_dao.update(db, article_id, obj)
@staticmethod
async def delete_article(article_id: int) -> bool:
"""
删除文章
:param article_id: 文章ID
:return: 是否删除成功
"""
async with async_db_session.begin() as db:
return await article_dao.delete(db, article_id)
@staticmethod
async def create_article_paragraph(*, obj: CreateArticleParagraphParam) -> int:
"""
创建文章段落
:param obj: 段落创建参数
:return: 段落ID
"""
async with async_db_session.begin() as db:
paragraph = await article_paragraph_dao.create(db, obj)
return paragraph.id
@staticmethod
async def get_article_paragraph_by_id(paragraph_id: int) -> Optional[ArticleParagraph]:
"""
根据ID获取文章段落
:param paragraph_id: 段落ID
:return: 段落记录
"""
async with async_db_session() as db:
return await article_paragraph_dao.get(db, paragraph_id)
@staticmethod
async def get_article_paragraphs_by_article_id(article_id: int) -> List[ArticleParagraph]:
"""
根据文章ID获取所有段落
:param article_id: 文章ID
:return: 段落记录列表
"""
async with async_db_session() as db:
return await article_paragraph_dao.get_by_article_id(db, article_id)
@staticmethod
async def update_article_paragraph(paragraph_id: int, obj: UpdateArticleParagraphParam) -> bool:
"""
更新文章段落
:param paragraph_id: 段落ID
:param obj: 段落更新参数
:return: 是否更新成功
"""
async with async_db_session.begin() as db:
return await article_paragraph_dao.update(db, paragraph_id, obj)
@staticmethod
async def delete_article_paragraph(paragraph_id: int) -> bool:
"""
删除文章段落
:param paragraph_id: 段落ID
:return: 是否删除成功
"""
async with async_db_session.begin() as db:
return await article_paragraph_dao.delete(db, paragraph_id)
@staticmethod
async def create_article_sentence(*, obj: CreateArticleSentenceParam) -> int:
"""
创建文章句子
:param obj: 句子创建参数
:return: 句子ID
"""
async with async_db_session.begin() as db:
sentence = await article_sentence_dao.create(db, obj)
return sentence.id
@staticmethod
async def get_article_sentence_by_id(sentence_id: int) -> Optional[ArticleSentence]:
"""
根据ID获取文章句子
:param sentence_id: 句子ID
:return: 句子记录
"""
async with async_db_session() as db:
return await article_sentence_dao.get(db, sentence_id)
@staticmethod
async def get_article_sentences_by_paragraph_id(paragraph_id: int) -> List[ArticleSentence]:
"""
根据段落ID获取所有句子
:param paragraph_id: 段落ID
:return: 句子记录列表
"""
async with async_db_session() as db:
return await article_sentence_dao.get_by_paragraph_id(db, paragraph_id)
@staticmethod
async def update_article_sentence(sentence_id: int, obj: UpdateArticleSentenceParam) -> bool:
"""
更新文章句子
:param sentence_id: 句子ID
:param obj: 句子更新参数
:return: 是否更新成功
"""
async with async_db_session.begin() as db:
return await article_sentence_dao.update(db, sentence_id, obj)
@staticmethod
async def delete_article_sentence(sentence_id: int) -> bool:
"""
删除文章句子
:param sentence_id: 句子ID
:return: 是否删除成功
"""
async with async_db_session.begin() as db:
return await article_sentence_dao.delete(db, sentence_id)
article_service = ArticleService()

View File

@@ -195,7 +195,6 @@ class ImageTextService:
# 创建新的文本记录
new_text = ImageText(
image_id=image_id,
article_sentence_id=None,
content=text_content,
standard_audio_id=None,
source=source,

View File

@@ -256,7 +256,6 @@ class RecordingService:
file_id=file_id,
image_id=image_id,
image_text_id=image_text_id,
article_sentence_id=None,
eval_mode=eval_mode,
info=metadata,
text=ref_text,
@@ -288,7 +287,6 @@ class RecordingService:
file_id=file_id,
image_id=image_id,
image_text_id=image_text_id,
article_sentence_id=None,
eval_mode=eval_mode,
info=metadata,
text=ref_text,

9
deploy/app_server.conf Executable file
View File

@@ -0,0 +1,9 @@
[program:app_server]
directory=/app
command=/usr/local/bin/gunicorn -c /app/deploy/gunicorn.conf.py main:app
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/app_server/app_server.log

View File

@@ -1,14 +1,28 @@
# Env: dev、pro
ENVIRONMENT='dev'
# MySQL
DATABASE_HOST='fsm_mysql'
ENVIRONMENT='prod'
# Database
DATABASE_HOST='127.0.0.1'
DATABASE_PORT=3306
DATABASE_USER='root'
DATABASE_PASSWORD='123456'
DATABASE_USER='app'
DATABASE_PASSWORD='Im_614000'
DATABASE_DB_NAME='app'
# Redis
REDIS_HOST='fsm_redis'
REDIS_HOST='127.0.0.1'
REDIS_PORT=6379
REDIS_PASSWORD=''
REDIS_DATABASE=0
# Token
TOKEN_SECRET_KEY='1VkVF75nsNABBjK_7-qz7GtzNy3AMvktc9TCPwKczCk'
# model
QWEN_API_KEY='sk-901901c68d6e44359ed31e9c59f6c8f9'
#QWEN_VISION_MODEL='qwen-vl-max-latest'
QWEN_VISION_MODEL='qwen-vl-plus'
QWEN_VISION_EMBEDDING_MODEL='multimodal-embedding-v1'
TENCENT_CLOUD_APP_ID='1251798270'
TENCENT_CLOUD_SECRET_ID='AKIDZ3CIFMVcadaRCrQr9HfLxRdlhYG0b2MX'
TENCENT_CLOUD_SECRET_KEY='51oZRMWirvsgLqCORK6kFdOPMAylakqw'
TENCENT_CLOUD_VOICE_MODEL_TYPE='501009'
YOUDAO_APP_ID='638c41df17d15cd6'
YOUDAO_APP_SECRET='VFicuogEnlQWaPPf4maGPnO1IztVzxVh'
WX_APPID='wxe739c0e6fb02eda8'
WX_SECRET='f0f56b636adf6baa9832cfcd4cb3276b'

View File

@@ -1,70 +1,70 @@
version: '3.8'
services:
fsm_server:
app_server:
build:
context: ../../
dockerfile: Dockerfile
ports:
- "8000:8000"
container_name: fsm_server
- "8080:8000"
container_name: app_server
restart: always
depends_on:
- fsm_mysql
- fsm_redis
- app_mysql
- app_redis
volumes:
- fsm_static:/www/fsm_server/backend/static
- app_static:/www/app_server/backend/static
environment:
- SERVER_HOST=0.0.0.0
- SERVER_PORT=8000
- DATABASE_HOST=fsm_mysql
- DATABASE_HOST=app_mysql
- DATABASE_PORT=3306
- DATABASE_USER=root
- DATABASE_PASSWORD=123456
- DATABASE_DB_NAME=fsm
- REDIS_HOST=fsm_redis
- DATABASE_PASSWORD=Im_614000
- DATABASE_DB_NAME=app
- REDIS_HOST=app_redis
- REDIS_PORT=6379
- REDIS_PASSWORD=
- REDIS_DATABASE=0
networks:
- fsm_network
- app_network
command: |
sh -c "
wait-for-it -s fsm_mysql:3306 -s fsm_redis:6379 -t 300
wait-for-it -s app_mysql:3306 -s app_redis:6379 -t 300
supervisord -c /etc/supervisor/supervisord.conf
supervisorctl restart
"
fsm_mysql:
image: mysql:8.0.29
app_mysql:
image: mysql:8.0.44
ports:
- "3306:3306"
container_name: fsm_mysql
container_name: app_mysql
restart: always
environment:
MYSQL_DATABASE: fsm
MYSQL_ROOT_PASSWORD: 123456
MYSQL_DATABASE: app
MYSQL_ROOT_PASSWORD: Im_614000
TZ: Asia/Shanghai
volumes:
- fsm_mysql:/var/lib/mysql
- app_mysql:/var/lib/mysql
networks:
- fsm_network
- app_network
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_general_ci
--lower_case_table_names=1
fsm_redis:
image: redis:7.0.4
app_redis:
image: redis:8.4.0-alpine
ports:
- "6379:6379"
container_name: fsm_redis
container_name: app_redis
restart: always
volumes:
- fsm_redis:/data
- app_redis:/data
networks:
- fsm_network
- app_network
command: |
--requirepass ""
--appendonly yes
@@ -75,23 +75,23 @@ services:
--maxmemory 256mb
--maxmemory-policy allkeys-lru
fsm_nginx:
app_nginx:
image: nginx:stable
ports:
- "8000:80"
container_name: fsm_nginx
container_name: app_nginx
restart: always
depends_on:
- fsm_server
- app_server
volumes:
- ../nginx.conf:/etc/nginx/conf.d/default.conf:ro
- fsm_static:/www/fsm_server/backend/static
- app_static:/www/app_server/backend/static
networks:
- fsm_network
- app_network
networks:
fsm_network:
name: fsm_network
app_network:
name: app_network
driver: bridge
ipam:
driver: default
@@ -99,9 +99,9 @@ networks:
- subnet: 172.10.10.0/24
volumes:
fsm_mysql:
name: fsm_mysql
fsm_redis:
name: fsm_redis
fsm_static:
name: fsm_static
app_mysql:
name: app_mysql
app_redis:
name: app_redis
app_static:
name: app_static

View File

@@ -1,9 +0,0 @@
[program:fastapi_server]
directory=/fsm
command=/usr/local/bin/gunicorn -c /fsm/deploy/gunicorn.conf.py main:app
user=root
autostart=true
autorestart=true
startretries=5
redirect_stderr=true
stdout_logfile=/var/log/fastapi_server/fsm_server.log

View File

@@ -3,7 +3,7 @@
bind = '0.0.0.0:8001'
# 工作目录
chdir = '/fsm/backend/'
chdir = '/app/backend/'
# 并行工作进程数
workers = 1
@@ -26,11 +26,11 @@ worker_class = 'uvicorn.workers.UvicornWorker'
worker_connections = 2000
# 设置进程文件目录
pidfile = '/fsm/gunicorn.pid'
pidfile = '/app/gunicorn.pid'
# 设置访问日志和错误信息日志路径
accesslog = '/var/log/fastapi_server/gunicorn_access.log'
errorlog = '/var/log/fastapi_server/gunicorn_error.log'
accesslog = '/var/log/app_server/gunicorn_access.log'
errorlog = '/var/log/app_server/gunicorn_error.log'
# 设置这个值为true 才会把打印信息记录到错误日志里
capture_output = True

View File

@@ -3,7 +3,7 @@ server {
listen [::]:80 default_server;
server_name 127.0.0.1;
root /fsm;
root /app;
client_max_body_size 5M;
client_body_buffer_size 5M;
@@ -16,7 +16,7 @@ server {
keepalive_timeout 300;
location / {
proxy_pass http://fsm_server:8001;
proxy_pass http://app_server:8080;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;

View File

@@ -1,36 +0,0 @@
@echo off
REM 首次部署时预加载字典链接缓存的脚本 (Windows版本)
REM 该脚本应在应用启动前运行
echo 开始预加载字典链接缓存...
REM 检查是否在正确的目录
if not exist "backend\utils\preload_dict_links.py" (
echo 错误: 请在项目根目录运行此脚本
exit /b 1
)
REM 激活虚拟环境(如果存在)
if exist "venv\Scripts\activate.bat" (
call venv\Scripts\activate.bat
echo 已激活虚拟环境
) else if exist ".venv\Scripts\activate.bat" (
call .venv\Scripts\activate.bat
echo 已激活虚拟环境
)
REM 运行预加载脚本
echo 正在运行预加载脚本...
python backend/utils/preload_dict_links.py preload --batch-size 2000
if %errorlevel% neq 0 (
echo 预加载脚本执行失败
exit /b %errorlevel%
)
echo 字典链接缓存预加载完成!
echo 显示缓存统计信息:
python backend/utils/preload_dict_links.py stats
echo 部署预加载完成!