diff --git a/backend/app/api/v1/module_ai/chat/crud.py b/backend/app/api/v1/module_ai/chat/crud.py index 72dd6ca..9af532c 100644 --- a/backend/app/api/v1/module_ai/chat/crud.py +++ b/backend/app/api/v1/module_ai/chat/crud.py @@ -3,7 +3,6 @@ from typing import Any from agno.db.base import SessionType from agno.db.mysql import MySQLDb from agno.db.postgres import PostgresDb -from agno.db.sqlite import SqliteDb from agno.session.team import TeamSession from app.config.setting import settings @@ -34,7 +33,6 @@ class ChatSessionCRUD: db_mapping = { "mysql": lambda: MySQLDb(db_url=db_uri, db_schema=settings.DATABASE_NAME, create_schema=False), "postgres": lambda: PostgresDb(db_url=db_uri, db_schema="public", create_schema=False), - "sqlite": lambda: SqliteDb(db_file=db_uri.replace("sqlite:///", "")), } if db_type not in db_mapping: diff --git a/backend/app/config/setting.py b/backend/app/config/setting.py index 9600c8c..113f725 100644 --- a/backend/app/config/setting.py +++ b/backend/app/config/setting.py @@ -10,7 +10,6 @@ from pydantic_settings import BaseSettings, SettingsConfigDict from app.common.enums import EnvironmentEnum from app.config.path_conf import ENV_DIR, STATIC_DIR - # JWT 开发默认密钥:仅允许 DEV 环境使用,生产命中即拒绝启动 _DEV_DEFAULT_SECRET_KEY = "dpb-dev-secret-key-do-not-use-in-production" @@ -110,8 +109,8 @@ class Settings(BaseSettings): AUTOFETCH: bool | None = None # AUTOFLUSH 别名(优先级高于 AUTOFLUSH,兼容旧环境变量名) EXPIRE_ON_COMMIT: bool = False # 是否在提交时过期 - # 数据库连接(生产 PostgreSQL;测试套件用 SQLite) - DATABASE_TYPE: Literal["postgres", "sqlite"] = "postgres" + # 数据库连接(开发与生产统一 PostgreSQL) + DATABASE_TYPE: Literal["postgres"] = "postgres" DATABASE_HOST: str = "localhost" DATABASE_PORT: int = 5432 DATABASE_USER: str = "dpb" @@ -305,19 +304,11 @@ class Settings(BaseSettings): @property def ASYNC_DB_URI(self) -> str: - if self.DATABASE_TYPE == "postgres": - db_connect = f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" - else: - db_connect = f"sqlite+aiosqlite:///{self.DATABASE_NAME}.db" - return db_connect + return f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" @property def DB_URI(self) -> str: - if self.DATABASE_TYPE == "postgres": - db_connect = f"postgresql+psycopg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" - else: - db_connect = f"sqlite:///{self.DATABASE_NAME}.db" - return db_connect + return f"postgresql+psycopg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}" @property def FASTAPI_CONFIG(self) -> dict[str, Any]: diff --git a/backend/app/core/database.py b/backend/app/core/database.py index ca3e5f8..665a3e1 100644 --- a/backend/app/core/database.py +++ b/backend/app/core/database.py @@ -49,29 +49,19 @@ def create_async_engine_and_session(db_url: str = settings.ASYNC_DB_URI) -> tupl - tuple[AsyncEngine, async_sessionmaker[AsyncSession]]: 异步数据库引擎和会话工厂。 """ try: - # 异步数据库引擎 - if settings.DATABASE_TYPE == "sqlite": - async_engine = create_async_engine( - url=db_url, - echo=settings.DATABASE_ECHO, - echo_pool=settings.ECHO_POOL, - pool_pre_ping=settings.POOL_PRE_PING, - future=settings.FUTURE, - pool_recycle=settings.POOL_RECYCLE, - ) - else: - async_engine = create_async_engine( - url=db_url, - echo=settings.DATABASE_ECHO, - echo_pool=settings.ECHO_POOL, - pool_pre_ping=settings.POOL_PRE_PING, - future=settings.FUTURE, - pool_recycle=settings.POOL_RECYCLE, - pool_size=settings.POOL_SIZE, - max_overflow=settings.MAX_OVERFLOW, - pool_timeout=settings.POOL_TIMEOUT, - pool_use_lifo=settings.POOL_USE_LIFO, - ) + # 异步数据库引擎(统一 PostgreSQL 连接池参数) + async_engine = create_async_engine( + url=db_url, + echo=settings.DATABASE_ECHO, + echo_pool=settings.ECHO_POOL, + pool_pre_ping=settings.POOL_PRE_PING, + future=settings.FUTURE, + pool_recycle=settings.POOL_RECYCLE, + pool_size=settings.POOL_SIZE, + max_overflow=settings.MAX_OVERFLOW, + pool_timeout=settings.POOL_TIMEOUT, + pool_use_lifo=settings.POOL_USE_LIFO, + ) except Exception as e: logger.error(f"❌ 数据库连接失败 {e}") raise diff --git a/backend/app/utils/number_gen.py b/backend/app/utils/number_gen.py index 97f19db..0328431 100644 --- a/backend/app/utils/number_gen.py +++ b/backend/app/utils/number_gen.py @@ -7,7 +7,7 @@ 存量数据迁移:首次使用某序列名时由 seed_fn(服务端传入)从既有记录推算 起始号(原扫描逻辑),此后全部走原子递增,不再扫描。 """ -from typing import Any, Awaitable, Callable +from collections.abc import Awaitable, Callable from sqlalchemy import select, text from sqlalchemy.ext.asyncio import AsyncSession @@ -23,10 +23,6 @@ class NumberGenService: @staticmethod async def _acquire_lock(db: AsyncSession, name: str) -> None: - from app.config.setting import settings - - if settings.DATABASE_TYPE != "postgres": - return # sqlite 测试库无 advisory lock,退化为普通自增(单进程无并发竞争) # hashtextextended 对文本做稳定 64 位哈希,同名序列恒映射同一把锁 await db.execute(text("SELECT pg_advisory_xact_lock(hashtextextended(:n, 0))"), {"n": name}) diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 66282ad..3d835f5 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -43,15 +43,13 @@ postgres = ["asyncpg==0.31.0"] [dependency-groups] dev = [ - "aiosqlite==0.22.1", # 测试:SQLite 内存库(conftest 固定 DATABASE_TYPE=sqlite,勿移除) - "fakeredis>=2.21.0,<3", # 测试:内存 Redis,配合 TESTING "pytest==9.0.2", "pytest-asyncio>=0.24.0", # 异步测试支持 "ruff==0.14.13", # 代码检查/格式化 ] [tool.uv] -default-groups = ["dev"] # 本地 uv sync 含 dev;生产 pip -r requirements.txt 不含 pytest/ruff/fakeredis +default-groups = ["dev"] # 本地 uv sync 含 dev;生产 pip -r requirements.txt 不含 pytest/ruff [[tool.uv.index]] name = "tuna" @@ -118,7 +116,7 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"tests/conftest.py" = ["E402"] # 先设置 TESTING/SQLite 环境再 import 应用 +"tests/conftest.py" = ["E402"] # 先设置 TESTING/PostgreSQL 环境再 import 应用 [tool.ruff.format] docstring-code-format = true diff --git a/backend/requirements.txt b/backend/requirements.txt index 0a65db7..c5fd087 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -1,4 +1,4 @@ -# 与 pyproject.toml [project] dependencies 一致(生产 pip;不含 dev 组:pytest / ruff / fakeredis) +# 与 pyproject.toml [project] dependencies 一致(生产 pip;不含 dev 组:pytest / ruff) agno==2.5.8 # AI agent框架 aiofiles==24.1.0 # 文件操作 alembic==1.18.4 # 数据库迁移 diff --git a/backend/tests/test_stat_analysis_tc.py b/backend/tests/test_stat_analysis_tc.py index f052c45..45d5881 100644 --- a/backend/tests/test_stat_analysis_tc.py +++ b/backend/tests/test_stat_analysis_tc.py @@ -6,9 +6,9 @@ 本包装层以 subprocess 顺序调用这些脚本,断言 rc==0 —— 与 run_regression.py 的执行契约一致。 关键点: -- 全部基于真实 PG16 + Redis(本工程无 SQLite),仅当探测到 PG/Redis 可达才跑,否则 skipif 跳过。 -- 不 import 任何 app 模块(避免被 conftest 的 sqlite 环境污染);subprocess env 显式构建: - 剥掉 conftest 注入的 DATABASE_TYPE/DATABASE_NAME/POOL_SIZE/MAX_OVERFLOW/CAPTCHA_ENABLE, +- 全部基于真实 PG16 + Redis,仅当探测到 PG/Redis 可达才跑,否则 skipif 跳过。 +- 不 import 任何 app 模块(测试库生命周期由 conftest 管理,与统计引擎 tc 独立);subprocess env 显式构建: + 清空可能残留的 DATABASE_*/REDIS_*/POOL_*/CAPTCHA_ENABLE 等连接变量, 置 ENVIRONMENT=dev / DATABASE_TYPE=postgres / PYTHONPATH=backend / PYTHONUTF8=1, 再并入从 backend/env/.env.dev 解析的真实连接值(可被 TC_CI_* 覆盖,用于验证 skipif 路径)。 - 标记 pg(pyproject 已注册),`pytest -m pg` 只跑本组;run_ci.py 的验收门即此命令。 @@ -118,7 +118,7 @@ def pg_ready() -> tuple[bool, bool, str]: def _sub_env() -> dict: - """显式构建子进程环境:剥掉 conftest 注入的 sqlite 变量,置真实 PG/Redis 连接。""" + """显式构建子进程环境:清空可能残留的连接变量,置真实 PG/Redis 连接。""" env = dict(os.environ) for k in ("DATABASE_TYPE", "DATABASE_NAME", "POOL_SIZE", "MAX_OVERFLOW", "CAPTCHA_ENABLE", "ENVIRONMENT", "PYTHONPATH", "PYTHONIOENCODING"): diff --git a/backend/uv.lock b/backend/uv.lock index 11afa79..0b62e2d 100644 --- a/backend/uv.lock +++ b/backend/uv.lock @@ -46,15 +46,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, ] -[[package]] -name = "aiosqlite" -version = "0.22.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/8a/64761f4005f17809769d23e518d915db74e6310474e733e3593cfc854ef1/aiosqlite-0.22.1.tar.gz", hash = "sha256:043e0bd78d32888c0a9ca90fc788b38796843360c855a7262a532813133a0650", size = 14821, upload-time = "2025-12-23T19:25:43.997Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb", size = 17405, upload-time = "2025-12-23T19:25:42.139Z" }, -] - [[package]] name = "alembic" version = "1.18.4" @@ -197,8 +188,6 @@ postgres = [ [package.dev-dependencies] dev = [ - { name = "aiosqlite" }, - { name = "fakeredis" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "ruff" }, @@ -243,8 +232,6 @@ provides-extras = ["postgres"] [package.metadata.requires-dev] dev = [ - { name = "aiosqlite", specifier = "==0.22.1" }, - { name = "fakeredis", specifier = ">=2.21.0,<3" }, { name = "pytest", specifier = "==9.0.2" }, { name = "pytest-asyncio", specifier = ">=0.24.0" }, { name = "ruff", specifier = "==0.14.13" }, @@ -353,19 +340,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] -[[package]] -name = "fakeredis" -version = "2.34.1" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -dependencies = [ - { name = "redis" }, - { name = "sortedcontainers" }, -] -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/40/fd09efa66205eb32253d2b2ebc63537281384d2040f0a88bcd2289e120e4/fakeredis-2.34.1.tar.gz", hash = "sha256:4ff55606982972eecce3ab410e03d746c11fe5deda6381d913641fbd8865ea9b", size = 177315, upload-time = "2026-02-25T13:17:51.315Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/49/b5/82f89307d0d769cd9bf46a54fb9136be08e4e57c5570ae421db4c9a2ba62/fakeredis-2.34.1-py3-none-any.whl", hash = "sha256:0107ec99d48913e7eec2a5e3e2403d1bd5f8aa6489d1a634571b975289c48f12", size = 122160, upload-time = "2026-02-25T13:17:49.701Z" }, -] - [[package]] name = "fastapi" version = "0.138.2" @@ -1251,15 +1225,6 @@ wheels = [ { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] -[[package]] -name = "sortedcontainers" -version = "2.4.0" -source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" } -sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/e8/c4/ba2f8066cceb6f23394729afe52f3bf7adec04bf9ed2c820b39e19299111/sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88", size = 30594, upload-time = "2021-05-16T22:03:42.897Z" } -wheels = [ - { url = "https://pypi.tuna.tsinghua.edu.cn/packages/32/46/9cb0e58b2deb7f82b84065f37f3bffeb12413f947f9388e4cac22c4621ce/sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0", size = 29575, upload-time = "2021-05-16T22:03:41.177Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.51"