feat(db): P1 Alembic+模型整合 — 唯一总库基线迁移 + 培训/园区 ORM 模型

- 新增 alembic(ini/env async/script.mako/versions/0001_initial)。
- models.py 并入培训表(training_accounts/events/bookings/tests/courses/policy/plan/survey_logs)
  与园区表(park_tenants/companies/kb_docs/screens),与平台同库、命名加域前缀避免撞名。
- baseline 迁移用 create_all(幂等):既有平台表保留,新增培训/园区表被建。
- 验证:对临时库 uv run alembic upgrade head 建出全部新表。
This commit is contained in:
Pine
2026-08-24 19:28:18 +08:00
parent c4fb4fb498
commit 8b0bca36a1
7 changed files with 372 additions and 0 deletions
+39
View File
@@ -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
+63
View File
@@ -0,0 +1,63 @@
# -*- coding: utf-8 -*-
"""Alembic 迁移环境(async enginetarget_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()
+25
View File
@@ -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"}
+31
View File
@@ -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())
+185
View File
@@ -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="")
+1
View File
@@ -28,6 +28,7 @@ dependencies = [
"torch>=2.13.0",
"transformers>=5.15.1",
"ultralytics>=8.4.127",
"alembic>=1.19.1",
]
[dependency-groups]
Generated
+28
View File
@@ -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" },