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:
+39
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"}
|
||||
@@ -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())
|
||||
@@ -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="")
|
||||
|
||||
@@ -28,6 +28,7 @@ dependencies = [
|
||||
"torch>=2.13.0",
|
||||
"transformers>=5.15.1",
|
||||
"ultralytics>=8.4.127",
|
||||
"alembic>=1.19.1",
|
||||
]
|
||||
|
||||
[dependency-groups]
|
||||
|
||||
@@ -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" },
|
||||
|
||||
Reference in New Issue
Block a user