diff --git a/alembic.ini b/alembic.ini new file mode 100644 index 0000000..e3e300d --- /dev/null +++ b/alembic.ini @@ -0,0 +1,39 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +# 数据库 URL 由 alembic/env.py 从服务端配置读取(config.DATABASE_URL),此处仅占位 +sqlalchemy.url = sqlite+aiosqlite:///serverdata/data/app.db + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/alembic/env.py b/alembic/env.py new file mode 100644 index 0000000..5ec481c --- /dev/null +++ b/alembic/env.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- +"""Alembic 迁移环境(async engine,target_metadata=Base.metadata)。 + +数据库 URL 取服务端配置 config.DATABASE_URL(同应用唯一总库), +迁移/建表均通过 alembic 运行,不在应用启动时执行。 +""" +from __future__ import annotations + +import asyncio +import os +import sys +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import async_engine_from_config + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +from app import config as app_config +from app.infrastructure.db import Base +import app.infrastructure.models # noqa: F401 确保模型注册到 Base.metadata + +config = context.config +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +# 用应用统一 DATABASE_URL(可被 env PINEAGENTS_DEMO_DATABASE_URL 覆盖) +config.set_main_option("sqlalchemy.url", app_config.DATABASE_URL) +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + context.configure(url=app_config.DATABASE_URL, target_metadata=target_metadata, + literal_binds=True, dialect_opts={"paramstyle": "named"}) + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection) -> None: + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", poolclass=pool.NullPool, + ) + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + await connectable.dispose() + + +def run_migrations_online() -> None: + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/alembic/script.py.mako b/alembic/script.py.mako new file mode 100644 index 0000000..dd9a708 --- /dev/null +++ b/alembic/script.py.mako @@ -0,0 +1,25 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from __future__ import annotations + +import sqlalchemy as sa +from alembic import op +${imports if imports else ""} + +revision = ${repr(up_revision)} +down_revision = ${repr(down_revision)} +branch_labels = ${repr(branch_labels)} +depends_on = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/alembic/versions/0001_initial.py b/alembic/versions/0001_initial.py new file mode 100644 index 0000000..bab87bb --- /dev/null +++ b/alembic/versions/0001_initial.py @@ -0,0 +1,31 @@ +"""baseline:建全部表(平台 + 培训 + 园区,唯一总库) + +Revision ID: 0001_initial +Revises: +Create Date: 2026-08-24 +""" +from __future__ import annotations + +import sys +import os +from alembic import op + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) + +revision = "0001_initial" +down_revision = None +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # 幂等:create_all 只建缺失的表。既有的平台表保留,新增培训/园区表被创建。 + from app.infrastructure.db import Base + import app.infrastructure.models # noqa: F401 注册全部模型 + Base.metadata.create_all(bind=op.get_bind()) + + +def downgrade() -> None: + from app.infrastructure.db import Base + import app.infrastructure.models # noqa: F401 + Base.metadata.drop_all(bind=op.get_bind()) diff --git a/app/infrastructure/models.py b/app/infrastructure/models.py index 0180864..ddc1771 100644 --- a/app/infrastructure/models.py +++ b/app/infrastructure/models.py @@ -579,3 +579,188 @@ class UserIdentity(Base): status: Mapped[str] = mapped_column(String, default="active") # active | disabled created_at: Mapped[str] = mapped_column(String, default="") updated_at: Mapped[str] = mapped_column(String, default="") + + +# =========================================================================== +# 培训业务子应用(原 app/training 独立 opc.db → 并入唯一总库) +# =========================================================================== + +class TrainingAccount(Base): + """培训站账号(原 accounts,含手机/微信登录)。""" + __tablename__ = "training_accounts" + id: Mapped[str] = mapped_column(String, primary_key=True) + username: Mapped[str] = mapped_column(String, unique=True, nullable=False) + wxid: Mapped[str] = mapped_column(String, default="") + phone: Mapped[str] = mapped_column(String, default="") + contact: Mapped[str] = mapped_column(String, default="") + name: Mapped[str] = mapped_column(String, default="") + avatar: Mapped[str] = mapped_column(String, default="") + password: Mapped[str] = mapped_column(String, default="") + phone_bound: Mapped[int] = mapped_column(Integer, default=0) + identities: Mapped[str] = mapped_column(Text, default="[]") + status_label: Mapped[str] = mapped_column(String, default="") + topics: Mapped[str] = mapped_column(Text, default="") + source: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") + + +class Event(Base): + __tablename__ = "training_events" + id: Mapped[str] = mapped_column(String, primary_key=True) + type: Mapped[str] = mapped_column(String, default="") + mode: Mapped[str] = mapped_column(String, default="offline") + title: Mapped[str] = mapped_column(String, default="") + subtitle: Mapped[str] = mapped_column(String, default="") + desc: Mapped[str] = mapped_column(Text, default="") + location: Mapped[str] = mapped_column(String, default="") + host: Mapped[str] = mapped_column(String, default="") + image: Mapped[str] = mapped_column(String, default="") + link: Mapped[str] = mapped_column(String, default="") + start_at: Mapped[str] = mapped_column(String, default="") + end_at: Mapped[str] = mapped_column(String, default="") + checkin_at: Mapped[str] = mapped_column(String, default="") + duration_min: Mapped[int] = mapped_column(Integer, default=90) + capacity: Mapped[int] = mapped_column(Integer, default=0) + show_capacity: Mapped[int] = mapped_column(Integer, default=0) + audit_mode: Mapped[str] = mapped_column(String, default="auto") + status: Mapped[str] = mapped_column(String, default="open") + + +class Booking(Base): + __tablename__ = "training_bookings" + id: Mapped[str] = mapped_column(String, primary_key=True) + created_at: Mapped[str] = mapped_column(String, default="") + status: Mapped[str] = mapped_column(String, default="pending") + audit_status: Mapped[str] = mapped_column(String, default="pending") + username: Mapped[str] = mapped_column(String, default="") + name: Mapped[str] = mapped_column(String, default="") + contact: Mapped[str] = mapped_column(String, default="") + status_label: Mapped[str] = mapped_column(String, default="") + want: Mapped[str] = mapped_column(Text, default="") + event_id: Mapped[str] = mapped_column(String, default="") + event_title: Mapped[str] = mapped_column(String, default="") + event_start: Mapped[str] = mapped_column(String, default="") + topics: Mapped[str] = mapped_column(Text, default="") + question: Mapped[str] = mapped_column(Text, default="") + source: Mapped[str] = mapped_column(String, default="") + checkin_at: Mapped[str] = mapped_column(String, default="") + + +class Test(Base): + __tablename__ = "training_tests" + id: Mapped[str] = mapped_column(String, primary_key=True) + created_at: Mapped[str] = mapped_column(String, default="") + username: Mapped[str] = mapped_column(String, default="") + type_code: Mapped[str] = mapped_column(String, default="") + persona: Mapped[str] = mapped_column(String, default="") + adapt_index: Mapped[int] = mapped_column(Integer, default=0) + adapt_level: Mapped[str] = mapped_column(String, default="") + tracks: Mapped[str] = mapped_column(Text, default="") + version: Mapped[str] = mapped_column(String, default="") + + +class Course(Base): + __tablename__ = "training_courses" + id: Mapped[str] = mapped_column(String, primary_key=True) + category: Mapped[str] = mapped_column(String, default="") + level: Mapped[str] = mapped_column(String, default="") + title: Mapped[str] = mapped_column(String, default="") + subtitle: Mapped[str] = mapped_column(String, default="") + desc: Mapped[str] = mapped_column(Text, default="") + price: Mapped[float] = mapped_column(Float, default=0.0) + status: Mapped[str] = mapped_column(String, default="draft") + start_at: Mapped[str] = mapped_column(String, default="") + end_at: Mapped[str] = mapped_column(String, default="") + venue: Mapped[str] = mapped_column(String, default="") + quota: Mapped[int] = mapped_column(Integer, default=0) + image: Mapped[str] = mapped_column(String, default="") + host: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") + + +class PolicyLog(Base): + __tablename__ = "training_policy_logs" + id: Mapped[str] = mapped_column(String, primary_key=True) + created_at: Mapped[str] = mapped_column(String, default="") + username: Mapped[str] = mapped_column(String, default="") + answers: Mapped[str] = mapped_column(Text, default="") + policies_count: Mapped[int] = mapped_column(Integer, default=0) + subsidies_count: Mapped[int] = mapped_column(Integer, default=0) + loans_count: Mapped[int] = mapped_column(Integer, default=0) + summary: Mapped[str] = mapped_column(Text, default="") + + +class PlanLog(Base): + __tablename__ = "training_plan_logs" + id: Mapped[str] = mapped_column(String, primary_key=True) + created_at: Mapped[str] = mapped_column(String, default="") + username: Mapped[str] = mapped_column(String, default="") + region: Mapped[str] = mapped_column(String, default="") + status: Mapped[str] = mapped_column(String, default="") + need_park: Mapped[int] = mapped_column(Integer, default=0) + has_staff: Mapped[int] = mapped_column(Integer, default=0) + steps_count: Mapped[int] = mapped_column(Integer, default=0) + + +class SurveyLog(Base): + __tablename__ = "training_survey_logs" + id: Mapped[str] = mapped_column(String, primary_key=True) + created_at: Mapped[str] = mapped_column(String, default="") + username: Mapped[str] = mapped_column(String, default="") + source: Mapped[str] = mapped_column(String, default="") + answers: Mapped[str] = mapped_column(Text, default="") + + +# =========================================================================== +# 园区子应用(原 app/park 独立 park.db → 并入唯一总库) +# =========================================================================== + +class ParkTenant(Base): + __tablename__ = "park_tenants" + id: Mapped[str] = mapped_column(String, primary_key=True) + name: Mapped[str] = mapped_column(String, default="") + intro_json: Mapped[str] = mapped_column(Text, default="[]") + username: Mapped[str] = mapped_column(String, default="") + salt: Mapped[str] = mapped_column(String, default="") + password_hash: Mapped[str] = mapped_column(String, default="") + admin_username: Mapped[str] = mapped_column(String, default="") + status: Mapped[str] = mapped_column(String, default="active") + data_json: Mapped[str] = mapped_column(Text, default="{}") + agent_json: Mapped[str] = mapped_column(Text, default="{}") + created_at: Mapped[str] = mapped_column(String, default="") + + +class ParkCompany(Base): + __tablename__ = "park_companies" + id: Mapped[str] = mapped_column(String, primary_key=True) + tenant_id: Mapped[str] = mapped_column(ForeignKey("park_tenants.id", ondelete="CASCADE"), nullable=False, index=True) + name: Mapped[str] = mapped_column(String, default="") + zone: Mapped[str] = mapped_column(String, default="") + room: Mapped[str] = mapped_column(String, default="") + industry: Mapped[str] = mapped_column(String, default="") + bio: Mapped[str] = mapped_column(Text, default="") + founder: Mapped[str] = mapped_column(String, default="") + status: Mapped[str] = mapped_column(String, default="applying") + employees: Mapped[int | None] = mapped_column(Integer, nullable=True) + created_at: Mapped[str] = mapped_column(String, default="") + + +class ParkKbDoc(Base): + __tablename__ = "park_kb_docs" + id: Mapped[str] = mapped_column(String, primary_key=True) + tenant_id: Mapped[str] = mapped_column(ForeignKey("park_tenants.id", ondelete="CASCADE"), nullable=False, index=True) + grp: Mapped[str] = mapped_column(String, default="general") + title: Mapped[str] = mapped_column(String, default="") + content_md: Mapped[str] = mapped_column(Text, default="") + created_at: Mapped[str] = mapped_column(String, default="") + + +class ParkScreen(Base): + __tablename__ = "park_screens" + id: Mapped[str] = mapped_column(String, primary_key=True) + tenant_id: Mapped[str] = mapped_column(ForeignKey("park_tenants.id", ondelete="CASCADE"), nullable=False, index=True) + device_id: Mapped[str] = mapped_column(String, default="") + name: Mapped[str] = mapped_column(String, default="") + role: Mapped[str] = mapped_column(String, default="main") + location: Mapped[str] = mapped_column(String, default="") + created_at: Mapped[str] = mapped_column(String, default="") diff --git a/pyproject.toml b/pyproject.toml index df432a2..0f8ae87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "torch>=2.13.0", "transformers>=5.15.1", "ultralytics>=8.4.127", + "alembic>=1.19.1", ] [dependency-groups] diff --git a/uv.lock b/uv.lock index 79e6a7a..0ab6e17 100644 --- a/uv.lock +++ b/uv.lock @@ -213,6 +213,20 @@ wheels = [ { url = "https://mirrors.cloud.tencent.com/pypi/packages/00/b7/e3bf5133d697a08128598c8d0abc5e16377b51465a33756de24fa7dee953/aiosqlite-0.22.1-py3-none-any.whl", hash = "sha256:21c002eb13823fad740196c5a2e9d8e62f6243bd9e7e4a1f87fb5e44ecb4fceb" }, ] +[[package]] +name = "alembic" +version = "1.19.1" +source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" } +dependencies = [ + { name = "mako" }, + { name = "sqlalchemy" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/16/2b/e4153978368de59918115c9e01d3ebf58a558a7285efa7e960c383c4b59a/alembic-1.19.1.tar.gz", hash = "sha256:e0fca0518118c78acc493e31bcb5402f190057aaf6df8b5b95ce94c4789cf648" } +wheels = [ + { url = "https://mirrors.cloud.tencent.com/pypi/packages/20/89/e62cc37b69ad357cc8ecd6e7367f5245f523d3cbb338a66197212bdf6749/alembic-1.19.1-py3-none-any.whl", hash = "sha256:b39018cb3d9413a19cbd54cf3c02ad33998641f0538eb77413a488a21c3e14be" }, +] + [[package]] name = "annotated-doc" version = "0.0.5" @@ -1568,6 +1582,18 @@ wheels = [ { url = "https://mirrors.cloud.tencent.com/pypi/packages/b4/de/88b3be5c31b22333b3ca2f6ff1de4e863d8fe45aaea7485f591970ec1d3e/linkify_it_py-2.1.0-py3-none-any.whl", hash = "sha256:0d252c1594ecba2ecedc444053db5d3a9b7ec1b0dd929c8f1d74dce89f86c05e" }, ] +[[package]] +name = "mako" +version = "1.4.1" +source = { registry = "https://mirrors.cloud.tencent.com/pypi/simple/" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://mirrors.cloud.tencent.com/pypi/packages/2a/12/b5fa2353e2754cd67fb9f83793fa48ff42c213a5da7e719869d2301f6ab8/mako-1.4.1.tar.gz", hash = "sha256:d7904710b662996425a21627710c4777c45053146942cf8a7aebf757c92b8c27" } +wheels = [ + { url = "https://mirrors.cloud.tencent.com/pypi/packages/a5/54/12ed58d458474aaab5c3d180173e745a4fe131bb330370596876d19ff60f/mako-1.4.1-py3-none-any.whl", hash = "sha256:a359d9a94a541213958742b2698d0a7757bb83551767bc468a74b9905aba9617" }, +] + [[package]] name = "markdown-it-py" version = "4.2.0" @@ -2363,6 +2389,7 @@ source = { editable = "." } dependencies = [ { name = "aioboto3" }, { name = "aiosqlite" }, + { name = "alembic" }, { name = "asyncmy" }, { name = "dashscope" }, { name = "fastapi" }, @@ -2399,6 +2426,7 @@ dev = [ requires-dist = [ { name = "aioboto3", specifier = ">=13.0.0" }, { name = "aiosqlite", specifier = ">=0.20.0" }, + { name = "alembic", specifier = ">=1.19.1" }, { name = "asyncmy", specifier = ">=0.2.9" }, { name = "dashscope", specifier = ">=1.27.1" }, { name = "fastapi", specifier = ">=0.115.0" },