feat(db): 引入 Alembic 取代裸 DDL 迁移

- alembic init + env.py 接 settings/models metadata(asyncpg->psycopg2 同步 URL)
- 补 3 个 CheckConstraint 到 models(原只在裸 DDL)
- 离线生成初始迁移(31 表+约束+95 索引,全 sa.* 通用类型)
- init_db 用 _run_alembic_migrations(自动基线+upgrade head)替换 create_tables+ensure_schema_updates(删 92 行裸 DDL)
- 删破坏性 migrate_db.py(drop_all)
- 既有 DB 首次启动自动 stamp 基线,无需手动

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2026-07-20 11:04:23 +08:00
parent 66ba6e00f5
commit 00ca81287e
8 changed files with 1023 additions and 150 deletions
+149
View File
@@ -0,0 +1,149 @@
# A generic, single database configuration.
[alembic]
# path to migration scripts.
# this is typically a path given in POSIX (e.g. forward slashes)
# format, relative to the token %(here)s which refers to the location of this
# ini file
script_location = %(here)s/alembic
# template used to generate migration file names; The default value is %%(rev)s_%%(slug)s
# Uncomment the line below if you want the files to be prepended with date and time
# see https://alembic.sqlalchemy.org/en/latest/tutorial.html#editing-the-ini-file
# for all available tokens
# file_template = %%(year)d_%%(month).2d_%%(day).2d_%%(hour).2d%%(minute).2d-%%(rev)s_%%(slug)s
# Or organize into date-based subdirectories (requires recursive_version_locations = true)
# file_template = %%(year)d/%%(month).2d/%%(day).2d_%%(hour).2d%%(minute).2d_%%(second).2d_%%(rev)s_%%(slug)s
# sys.path path, will be prepended to sys.path if present.
# defaults to the current working directory. for multiple paths, the path separator
# is defined by "path_separator" below.
prepend_sys_path = .
# timezone to use when rendering the date within the migration file
# as well as the filename.
# If specified, requires the tzdata library which can be installed by adding
# `alembic[tz]` to the pip requirements.
# string value is passed to ZoneInfo()
# leave blank for localtime
# timezone =
# max length of characters to apply to the "slug" field
# truncate_slug_length = 40
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# set to 'true' to allow .pyc and .pyo files without
# a source .py file to be detected as revisions in the
# versions/ directory
# sourceless = false
# version location specification; This defaults
# to <script_location>/versions. When using multiple version
# directories, initial revisions must be specified with --version-path.
# The path separator used here should be the separator specified by "path_separator"
# below.
# version_locations = %(here)s/bar:%(here)s/bat:%(here)s/alembic/versions
# path_separator; This indicates what character is used to split lists of file
# paths, including version_locations and prepend_sys_path within configparser
# files such as alembic.ini.
# The default rendered in new alembic.ini files is "os", which uses os.pathsep
# to provide os-dependent path splitting.
#
# Note that in order to support legacy alembic.ini files, this default does NOT
# take place if path_separator is not present in alembic.ini. If this
# option is omitted entirely, fallback logic is as follows:
#
# 1. Parsing of the version_locations option falls back to using the legacy
# "version_path_separator" key, which if absent then falls back to the legacy
# behavior of splitting on spaces and/or commas.
# 2. Parsing of the prepend_sys_path option falls back to the legacy
# behavior of splitting on spaces, commas, or colons.
#
# Valid values for path_separator are:
#
# path_separator = :
# path_separator = ;
# path_separator = space
# path_separator = newline
#
# Use os.pathsep. Default configuration used for new projects.
path_separator = os
# set to 'true' to search source files recursively
# in each "version_locations" directory
# new in Alembic version 1.10
# recursive_version_locations = false
# the output encoding used when revision files
# are written from script.py.mako
# output_encoding = utf-8
# database URL. This is consumed by the user-maintained env.py script only.
# other means of configuring database URLs may be customized within the env.py
# file.
sqlalchemy.url = driver://user:pass@localhost/dbname
[post_write_hooks]
# post_write_hooks defines scripts or Python functions that are run
# on newly generated revision scripts. See the documentation for further
# detail and examples
# format using "black" - use the console_scripts runner, against the "black" entrypoint
# hooks = black
# black.type = console_scripts
# black.entrypoint = black
# black.options = -l 79 REVISION_SCRIPT_FILENAME
# lint with attempts to fix using "ruff" - use the module runner, against the "ruff" module
# hooks = ruff
# ruff.type = module
# ruff.module = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Alternatively, use the exec runner to execute a binary found on your PATH
# hooks = ruff
# ruff.type = exec
# ruff.executable = ruff
# ruff.options = check --fix REVISION_SCRIPT_FILENAME
# Logging configuration. This is also consumed by the user-maintained
# env.py script only.
[loggers]
keys = root,sqlalchemy,alembic
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARNING
handlers = console
qualname =
[logger_sqlalchemy]
level = WARNING
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
+1
View File
@@ -0,0 +1 @@
Generic single-database configuration.
+73
View File
@@ -0,0 +1,73 @@
"""Alembic 迁移环境配置
- 从 shared.config.settings 读取 DB 配置,构造同步 URL(psycopg2)供 alembic 使用
(项目运行时用 asyncpg,但 alembic 是同步库,需 psycopg2)
- target_metadata 指向 shared.models.database.Base.metadata
- 支持 ALEMBIC_URL 环境变量覆盖(用于离线/空库生成初始迁移,如 sqlite:///empty.db)
"""
from logging.config import fileConfig
from pathlib import Path
import os
import sys
from sqlalchemy import engine_from_config, pool
from alembic import context
# 让 alembic 能 import 项目模块(src 在项目根下)
project_root = Path(__file__).parent.parent
sys.path.insert(0, str(project_root / "src"))
from shared.config.settings import settings # noqa: E402
from shared.models.database import Base # noqa: E402
import shared.models.database # noqa: E402,F401 # 导入所有模型,确保 metadata 注册
config = context.config
if config.config_file_name is not None:
fileConfig(config.config_file_name)
# 构造同步 URL:asyncpg -> psycopg2
_sync_url = settings.DATABASE_URL.replace("postgresql+asyncpg://", "postgresql+psycopg2://")
# 支持 ALEMBIC_URL 覆盖(离线生成/测试用)
config.set_main_option("sqlalchemy.url", os.getenv("ALEMBIC_URL", _sync_url))
target_metadata = Base.metadata
def run_migrations_offline() -> None:
"""离线模式:生成 SQL 脚本,不连接 DB"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url,
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
"""在线模式:连接 DB 执行迁移"""
connectable = engine_from_config(
config.get_section(config.config_ini_section, {}),
prefix="sqlalchemy.",
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=target_metadata,
compare_type=True,
compare_server_default=True,
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+28
View File
@@ -0,0 +1,28 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision: str = ${repr(up_revision)}
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
def upgrade() -> None:
"""Upgrade schema."""
${upgrades if upgrades else "pass"}
def downgrade() -> None:
"""Downgrade schema."""
${downgrades if downgrades else "pass"}
@@ -0,0 +1,728 @@
"""initial schema
Revision ID: 9928d7f8c1ef
Revises:
Create Date: 2026-07-20 10:30:30.249866
"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision: str = '9928d7f8c1ef'
down_revision: Union[str, Sequence[str], None] = None
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def upgrade() -> None:
"""Upgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('customers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=50), nullable=True),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('contact_person', sa.String(length=100), nullable=True),
sa.Column('phone', sa.String(length=50), nullable=True),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('address', sa.Text(), nullable=True),
sa.Column('bank_name', sa.String(length=100), nullable=True),
sa.Column('bank_account', sa.String(length=50), nullable=True),
sa.Column('tax_number', sa.String(length=50), nullable=True),
sa.Column('credit_limit', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_customers_code'), 'customers', ['code'], unique=True)
op.create_index(op.f('ix_customers_id'), 'customers', ['id'], unique=False)
op.create_table('permissions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=100), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('module', sa.String(length=50), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_permissions_code'), 'permissions', ['code'], unique=True)
op.create_index(op.f('ix_permissions_id'), 'permissions', ['id'], unique=False)
op.create_table('products',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('sku', sa.String(length=50), nullable=False),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('category', sa.String(length=100), nullable=True),
sa.Column('unit', sa.String(length=20), nullable=True),
sa.Column('item_type', sa.String(length=20), nullable=True),
sa.Column('cost_price', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('sale_price', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('min_stock', sa.Integer(), nullable=True),
sa.Column('max_stock', sa.Integer(), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_products_id'), 'products', ['id'], unique=False)
op.create_index(op.f('ix_products_item_type'), 'products', ['item_type'], unique=False)
op.create_index(op.f('ix_products_sku'), 'products', ['sku'], unique=True)
op.create_table('roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=50), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('is_system', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_roles_code'), 'roles', ['code'], unique=True)
op.create_index(op.f('ix_roles_id'), 'roles', ['id'], unique=False)
op.create_table('suppliers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=50), nullable=True),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('contact_person', sa.String(length=100), nullable=True),
sa.Column('phone', sa.String(length=50), nullable=True),
sa.Column('email', sa.String(length=100), nullable=True),
sa.Column('address', sa.Text(), nullable=True),
sa.Column('bank_name', sa.String(length=100), nullable=True),
sa.Column('bank_account', sa.String(length=50), nullable=True),
sa.Column('tax_number', sa.String(length=50), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_suppliers_code'), 'suppliers', ['code'], unique=True)
op.create_index(op.f('ix_suppliers_id'), 'suppliers', ['id'], unique=False)
op.create_table('users',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('username', sa.String(length=50), nullable=False),
sa.Column('email', sa.String(length=255), nullable=False),
sa.Column('hashed_password', sa.String(length=255), nullable=False),
sa.Column('full_name', sa.String(length=100), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('last_login', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True)
op.create_index(op.f('ix_users_id'), 'users', ['id'], unique=False)
op.create_index(op.f('ix_users_username'), 'users', ['username'], unique=True)
op.create_table('warehouses',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('code', sa.String(length=50), nullable=True),
sa.Column('name', sa.String(length=200), nullable=False),
sa.Column('address', sa.Text(), nullable=True),
sa.Column('manager', sa.String(length=100), nullable=True),
sa.Column('phone', sa.String(length=50), nullable=True),
sa.Column('is_active', sa.Boolean(), nullable=True),
sa.Column('is_default', sa.Boolean(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_warehouses_code'), 'warehouses', ['code'], unique=True)
op.create_index(op.f('ix_warehouses_id'), 'warehouses', ['id'], unique=False)
op.create_table('finance_transactions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('txn_no', sa.String(length=50), nullable=False),
sa.Column('txn_type', sa.String(length=20), nullable=False),
sa.Column('partner_type', sa.String(length=20), nullable=False),
sa.Column('partner_id', sa.Integer(), nullable=False),
sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('txn_date', sa.DateTime(), nullable=True),
sa.Column('method', sa.String(length=30), nullable=True),
sa.Column('account_name', sa.String(length=100), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('remark', sa.Text(), nullable=True),
sa.Column('operator_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_finance_transactions_created_at'), 'finance_transactions', ['created_at'], unique=False)
op.create_index(op.f('ix_finance_transactions_id'), 'finance_transactions', ['id'], unique=False)
op.create_index(op.f('ix_finance_transactions_partner_id'), 'finance_transactions', ['partner_id'], unique=False)
op.create_index(op.f('ix_finance_transactions_partner_type'), 'finance_transactions', ['partner_type'], unique=False)
op.create_index(op.f('ix_finance_transactions_status'), 'finance_transactions', ['status'], unique=False)
op.create_index(op.f('ix_finance_transactions_txn_date'), 'finance_transactions', ['txn_date'], unique=False)
op.create_index(op.f('ix_finance_transactions_txn_no'), 'finance_transactions', ['txn_no'], unique=True)
op.create_index(op.f('ix_finance_transactions_txn_type'), 'finance_transactions', ['txn_type'], unique=False)
op.create_table('inventory',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('warehouse_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column('locked_quantity', sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column('batch_number', sa.String(length=50), nullable=True),
sa.Column('location', sa.String(length=100), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.CheckConstraint('quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity', name='ck_inventory_qty_nonnegative'),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['warehouse_id'], ['warehouses.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('product_id', 'warehouse_id', name='uq_inventory_product_warehouse')
)
op.create_index(op.f('ix_inventory_id'), 'inventory', ['id'], unique=False)
op.create_index(op.f('ix_inventory_product_id'), 'inventory', ['product_id'], unique=False)
op.create_index(op.f('ix_inventory_warehouse_id'), 'inventory', ['warehouse_id'], unique=False)
op.create_table('material_price_history',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('price', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('effective_date', sa.DateTime(), nullable=True),
sa.Column('supplier_id', sa.Integer(), nullable=True),
sa.Column('remark', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_material_price_history_effective_date'), 'material_price_history', ['effective_date'], unique=False)
op.create_index(op.f('ix_material_price_history_id'), 'material_price_history', ['id'], unique=False)
op.create_index(op.f('ix_material_price_history_product_id'), 'material_price_history', ['product_id'], unique=False)
op.create_index(op.f('ix_material_price_history_supplier_id'), 'material_price_history', ['supplier_id'], unique=False)
op.create_table('material_suppliers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('supplier_id', sa.Integer(), nullable=False),
sa.Column('is_primary', sa.Boolean(), nullable=True),
sa.Column('contact_person', sa.String(length=100), nullable=True),
sa.Column('contact_phone', sa.String(length=50), nullable=True),
sa.Column('lead_time', sa.Integer(), nullable=True),
sa.Column('min_order_quantity', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_material_suppliers_id'), 'material_suppliers', ['id'], unique=False)
op.create_index(op.f('ix_material_suppliers_product_id'), 'material_suppliers', ['product_id'], unique=False)
op.create_index(op.f('ix_material_suppliers_supplier_id'), 'material_suppliers', ['supplier_id'], unique=False)
op.create_table('product_materials',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('finished_product_id', sa.Integer(), nullable=False),
sa.Column('material_product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Numeric(precision=12, scale=4), nullable=False),
sa.Column('loss_rate', sa.Numeric(precision=5, scale=4), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['finished_product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['material_product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('finished_product_id', 'material_product_id', name='uq_product_material_unique')
)
op.create_index(op.f('ix_product_materials_finished_product_id'), 'product_materials', ['finished_product_id'], unique=False)
op.create_index(op.f('ix_product_materials_id'), 'product_materials', ['id'], unique=False)
op.create_index(op.f('ix_product_materials_material_product_id'), 'product_materials', ['material_product_id'], unique=False)
op.create_table('purchase_orders',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_no', sa.String(length=50), nullable=False),
sa.Column('supplier_id', sa.Integer(), nullable=False),
sa.Column('order_date', sa.DateTime(), nullable=True),
sa.Column('expected_date', sa.Date(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('paid_amount', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('remark', sa.Text(), nullable=True),
sa.Column('operator_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.Column('received_date', sa.DateTime(), nullable=True),
sa.Column('paid_date', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['supplier_id'], ['suppliers.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_purchase_orders_id'), 'purchase_orders', ['id'], unique=False)
op.create_index(op.f('ix_purchase_orders_order_no'), 'purchase_orders', ['order_no'], unique=True)
op.create_index(op.f('ix_purchase_orders_supplier_id'), 'purchase_orders', ['supplier_id'], unique=False)
op.create_table('role_permissions',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('permission_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['permission_id'], ['permissions.id'], ),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_role_permissions_id'), 'role_permissions', ['id'], unique=False)
op.create_index(op.f('ix_role_permissions_permission_id'), 'role_permissions', ['permission_id'], unique=False)
op.create_index(op.f('ix_role_permissions_role_id'), 'role_permissions', ['role_id'], unique=False)
op.create_table('sales_orders',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_no', sa.String(length=50), nullable=False),
sa.Column('customer_id', sa.Integer(), nullable=False),
sa.Column('order_date', sa.DateTime(), nullable=True),
sa.Column('delivery_date', sa.Date(), nullable=True),
sa.Column('manufacturing_date', sa.DateTime(), nullable=True),
sa.Column('actual_delivery_date', sa.DateTime(), nullable=True),
sa.Column('actual_payment_date', sa.DateTime(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('production_status', sa.String(length=20), nullable=True),
sa.Column('production_no', sa.String(length=50), nullable=True),
sa.Column('planned_material_cost', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('actual_material_cost', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('received_amount', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('remark', sa.Text(), nullable=True),
sa.Column('operator_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['customer_id'], ['customers.id'], ),
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_sales_orders_customer_id'), 'sales_orders', ['customer_id'], unique=False)
op.create_index(op.f('ix_sales_orders_id'), 'sales_orders', ['id'], unique=False)
op.create_index(op.f('ix_sales_orders_order_no'), 'sales_orders', ['order_no'], unique=True)
op.create_index(op.f('ix_sales_orders_production_no'), 'sales_orders', ['production_no'], unique=False)
op.create_index(op.f('ix_sales_orders_production_status'), 'sales_orders', ['production_status'], unique=False)
op.create_table('stock_movements',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('warehouse_id', sa.Integer(), nullable=False),
sa.Column('movement_type', sa.String(length=20), nullable=False),
sa.Column('quantity', sa.Numeric(precision=12, scale=4), nullable=False),
sa.Column('before_quantity', sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column('after_quantity', sa.Numeric(precision=12, scale=4), nullable=True),
sa.Column('reference_type', sa.String(length=50), nullable=True),
sa.Column('reference_id', sa.Integer(), nullable=True),
sa.Column('reference_no', sa.String(length=50), nullable=True),
sa.Column('unit_price', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('total_amount', sa.Numeric(precision=12, scale=2), nullable=True),
sa.Column('remark', sa.Text(), nullable=True),
sa.Column('operator_id', sa.Integer(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['operator_id'], ['users.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.ForeignKeyConstraint(['warehouse_id'], ['warehouses.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_stock_movements_created_at'), 'stock_movements', ['created_at'], unique=False)
op.create_index(op.f('ix_stock_movements_id'), 'stock_movements', ['id'], unique=False)
op.create_index(op.f('ix_stock_movements_product_id'), 'stock_movements', ['product_id'], unique=False)
op.create_table('stp_files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('object_key', sa.String(length=500), nullable=False),
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
sa.Column('object_url', sa.String(length=1000), nullable=True),
sa.Column('original_filename', sa.String(length=255), nullable=False),
sa.Column('file_size', sa.Integer(), nullable=False),
sa.Column('file_hash', sa.String(length=64), nullable=True),
sa.Column('mime_type', sa.String(length=50), nullable=True),
sa.Column('upload_batch', sa.String(length=36), nullable=True),
sa.Column('upload_time', sa.DateTime(), nullable=True),
sa.Column('processed_time', sa.DateTime(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('volume', sa.Float(), nullable=True),
sa.Column('surface_area', sa.Float(), nullable=True),
sa.Column('product_weight', sa.Float(), nullable=True),
sa.Column('file_path', sa.String(length=500), nullable=True),
sa.Column('file_content', sa.LargeBinary(), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_stp_files_file_hash'), 'stp_files', ['file_hash'], unique=False)
op.create_index(op.f('ix_stp_files_id'), 'stp_files', ['id'], unique=False)
op.create_index(op.f('ix_stp_files_object_key'), 'stp_files', ['object_key'], unique=False)
op.create_index(op.f('ix_stp_files_original_filename'), 'stp_files', ['original_filename'], unique=False)
op.create_index(op.f('ix_stp_files_status'), 'stp_files', ['status'], unique=False)
op.create_index(op.f('ix_stp_files_upload_batch'), 'stp_files', ['upload_batch'], unique=False)
op.create_index(op.f('ix_stp_files_user_id'), 'stp_files', ['user_id'], unique=False)
op.create_table('system_logs',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('level', sa.String(length=20), nullable=False),
sa.Column('message', sa.Text(), nullable=False),
sa.Column('module', sa.String(length=100), nullable=True),
sa.Column('function_name', sa.String(length=100), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('user_id', sa.Integer(), nullable=True),
sa.Column('request_id', sa.String(length=100), nullable=True),
sa.Column('execution_time_ms', sa.Integer(), nullable=True),
sa.Column('resource_type', sa.String(length=50), nullable=True),
sa.Column('resource_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_system_logs_created_at'), 'system_logs', ['created_at'], unique=False)
op.create_index(op.f('ix_system_logs_id'), 'system_logs', ['id'], unique=False)
op.create_index(op.f('ix_system_logs_level'), 'system_logs', ['level'], unique=False)
op.create_table('user_activities',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('activity_type', sa.String(length=50), nullable=False),
sa.Column('resource_type', sa.String(length=50), nullable=True),
sa.Column('resource_id', sa.Integer(), nullable=True),
sa.Column('description', sa.Text(), nullable=True),
sa.Column('meta_data', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('ip_address', sa.String(length=45), nullable=True),
sa.Column('user_agent', sa.String(length=500), nullable=True),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_activities_activity_type'), 'user_activities', ['activity_type'], unique=False)
op.create_index(op.f('ix_user_activities_created_at'), 'user_activities', ['created_at'], unique=False)
op.create_index(op.f('ix_user_activities_id'), 'user_activities', ['id'], unique=False)
op.create_index(op.f('ix_user_activities_user_id'), 'user_activities', ['user_id'], unique=False)
op.create_table('user_roles',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('user_id', sa.Integer(), nullable=False),
sa.Column('role_id', sa.Integer(), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['role_id'], ['roles.id'], ),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_user_roles_id'), 'user_roles', ['id'], unique=False)
op.create_index(op.f('ix_user_roles_role_id'), 'user_roles', ['role_id'], unique=False)
op.create_index(op.f('ix_user_roles_user_id'), 'user_roles', ['user_id'], unique=False)
op.create_table('analysis_metrics',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('volume_utilization', sa.Float(), nullable=True),
sa.Column('topology_complexity', sa.Float(), nullable=True),
sa.Column('wall_uniformity', sa.Float(), nullable=True),
sa.Column('analysis_summary', sa.Text(), nullable=True),
sa.Column('verification_status', sa.String(length=20), nullable=True),
sa.Column('verification_volume_diff', sa.Float(), nullable=True),
sa.Column('verification_area_diff', sa.Float(), nullable=True),
sa.Column('verification_details', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_analysis_metrics_id'), 'analysis_metrics', ['id'], unique=False)
op.create_index(op.f('ix_analysis_metrics_stp_file_id'), 'analysis_metrics', ['stp_file_id'], unique=False)
op.create_table('design_recommendations',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('rec_type', sa.String(length=50), nullable=False),
sa.Column('priority', sa.String(length=20), nullable=False),
sa.Column('description', sa.String(length=500), nullable=False),
sa.Column('reason', sa.Text(), nullable=True),
sa.Column('parameters', sa.JSON(), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('user_notes', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.Column('updated_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_design_recommendations_id'), 'design_recommendations', ['id'], unique=False)
op.create_index(op.f('ix_design_recommendations_stp_file_id'), 'design_recommendations', ['stp_file_id'], unique=False)
op.create_table('finance_allocations',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('transaction_id', sa.Integer(), nullable=False),
sa.Column('order_type', sa.String(length=20), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('allocated_amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('created_at', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['transaction_id'], ['finance_transactions.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_finance_allocations_created_at'), 'finance_allocations', ['created_at'], unique=False)
op.create_index(op.f('ix_finance_allocations_id'), 'finance_allocations', ['id'], unique=False)
op.create_index(op.f('ix_finance_allocations_order_id'), 'finance_allocations', ['order_id'], unique=False)
op.create_index(op.f('ix_finance_allocations_order_type'), 'finance_allocations', ['order_type'], unique=False)
op.create_index(op.f('ix_finance_allocations_transaction_id'), 'finance_allocations', ['transaction_id'], unique=False)
op.create_table('geometry_data',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('object_key', sa.String(length=500), nullable=False),
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
sa.Column('object_url', sa.String(length=1000), nullable=True),
sa.Column('analysis_method', sa.String(length=50), nullable=True),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('volume', sa.Float(), nullable=True),
sa.Column('surface_area', sa.Float(), nullable=True),
sa.Column('bounding_box_min', sa.JSON(), nullable=True),
sa.Column('bounding_box_max', sa.JSON(), nullable=True),
sa.Column('center_of_mass', sa.JSON(), nullable=True),
sa.Column('topology_faces', sa.Integer(), nullable=True),
sa.Column('topology_edges', sa.Integer(), nullable=True),
sa.Column('topology_vertices', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_geometry_data_id'), 'geometry_data', ['id'], unique=False)
op.create_index(op.f('ix_geometry_data_stp_file_id'), 'geometry_data', ['stp_file_id'], unique=False)
op.create_table('html_files',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('object_key', sa.String(length=500), nullable=False),
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
sa.Column('object_url', sa.String(length=1000), nullable=True),
sa.Column('filename', sa.String(length=255), nullable=False),
sa.Column('generated_time', sa.DateTime(), nullable=True),
sa.Column('visualization_type', sa.String(length=50), nullable=True),
sa.Column('has_interactive_elements', sa.Boolean(), nullable=True),
sa.Column('file_path', sa.String(length=500), nullable=True),
sa.Column('html_content', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_html_files_id'), 'html_files', ['id'], unique=False)
op.create_index(op.f('ix_html_files_stp_file_id'), 'html_files', ['stp_file_id'], unique=False)
op.create_table('mesh_data',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('object_key', sa.String(length=500), nullable=False),
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
sa.Column('object_url', sa.String(length=1000), nullable=True),
sa.Column('quality', sa.String(length=20), nullable=True),
sa.Column('vertex_count', sa.Integer(), nullable=True),
sa.Column('face_count', sa.Integer(), nullable=True),
sa.Column('point_count', sa.Integer(), nullable=True),
sa.Column('bounding_box_min', sa.JSON(), nullable=True),
sa.Column('bounding_box_max', sa.JSON(), nullable=True),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_mesh_data_id'), 'mesh_data', ['id'], unique=False)
op.create_index(op.f('ix_mesh_data_stp_file_id'), 'mesh_data', ['stp_file_id'], unique=False)
op.create_table('mold_cavity_data',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('detailed_object_key', sa.String(length=500), nullable=False),
sa.Column('storage_bucket', sa.String(length=100), nullable=False),
sa.Column('mold_material', sa.String(length=100), nullable=True),
sa.Column('mold_type', sa.String(length=50), nullable=True),
sa.Column('shrinkage_rate', sa.Float(), nullable=False),
sa.Column('draft_angle', sa.Float(), nullable=False),
sa.Column('parting_line_length', sa.Float(), nullable=True),
sa.Column('generated_time', sa.DateTime(), nullable=True),
sa.Column('cavity_key_info', sa.JSON(), nullable=True),
sa.Column('mold_size_length', sa.Float(), nullable=True),
sa.Column('mold_size_width', sa.Float(), nullable=True),
sa.Column('mold_size_height', sa.Float(), nullable=True),
sa.Column('estimated_clamping_force', sa.String(length=50), nullable=True),
sa.Column('product_weight', sa.String(length=50), nullable=True),
sa.Column('product_volume', sa.Float(), nullable=True),
sa.Column('wall_thickness_range', sa.String(length=50), nullable=True),
sa.Column('complexity_score', sa.Float(), nullable=True),
sa.Column('weld_line_risk', sa.String(length=50), nullable=True),
sa.Column('sink_mark_risk', sa.String(length=50), nullable=True),
sa.Column('warpage_risk', sa.String(length=50), nullable=True),
sa.Column('best_scheme_id', sa.String(length=64), nullable=True),
sa.Column('confidence_score', sa.Float(), nullable=True),
sa.Column('is_fallback', sa.Boolean(), nullable=True),
sa.Column('fallback_reason', sa.Text(), nullable=True),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_mold_cavity_data_best_scheme_id'), 'mold_cavity_data', ['best_scheme_id'], unique=False)
op.create_index(op.f('ix_mold_cavity_data_id'), 'mold_cavity_data', ['id'], unique=False)
op.create_index(op.f('ix_mold_cavity_data_is_fallback'), 'mold_cavity_data', ['is_fallback'], unique=False)
op.create_index(op.f('ix_mold_cavity_data_stp_file_id'), 'mold_cavity_data', ['stp_file_id'], unique=False)
op.create_table('processing_tasks',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('task_id', sa.String(length=36), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('task_type', sa.String(length=50), nullable=True),
sa.Column('status', sa.String(length=20), nullable=True),
sa.Column('created_time', sa.DateTime(), nullable=True),
sa.Column('started_time', sa.DateTime(), nullable=True),
sa.Column('completed_time', sa.DateTime(), nullable=True),
sa.Column('progress', sa.Integer(), nullable=True),
sa.Column('current_step', sa.String(length=100), nullable=True),
sa.Column('error_message', sa.Text(), nullable=True),
sa.Column('error_stack', sa.Text(), nullable=True),
sa.Column('parameters', sa.JSON(), nullable=True),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_processing_tasks_id'), 'processing_tasks', ['id'], unique=False)
op.create_index(op.f('ix_processing_tasks_stp_file_id'), 'processing_tasks', ['stp_file_id'], unique=False)
op.create_index(op.f('ix_processing_tasks_task_id'), 'processing_tasks', ['task_id'], unique=True)
op.create_table('purchase_order_items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('received_quantity', sa.Integer(), nullable=True),
sa.Column('unit_price', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('remark', sa.Text(), nullable=True),
sa.CheckConstraint('quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity', name='ck_purchase_order_items_qty'),
sa.ForeignKeyConstraint(['order_id'], ['purchase_orders.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_purchase_order_items_id'), 'purchase_order_items', ['id'], unique=False)
op.create_table('sales_order_items',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('order_id', sa.Integer(), nullable=False),
sa.Column('product_id', sa.Integer(), nullable=False),
sa.Column('quantity', sa.Integer(), nullable=False),
sa.Column('delivered_quantity', sa.Integer(), nullable=True),
sa.Column('unit_price', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('amount', sa.Numeric(precision=12, scale=2), nullable=False),
sa.Column('remark', sa.Text(), nullable=True),
sa.CheckConstraint('quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity', name='ck_sales_order_items_qty'),
sa.ForeignKeyConstraint(['order_id'], ['sales_orders.id'], ),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_sales_order_items_id'), 'sales_order_items', ['id'], unique=False)
op.create_table('feature_detections',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('stp_file_id', sa.Integer(), nullable=False),
sa.Column('feature_type', sa.String(length=50), nullable=False),
sa.Column('confidence', sa.Float(), nullable=False),
sa.Column('location', sa.JSON(), nullable=True),
sa.Column('dimensions', sa.JSON(), nullable=True),
sa.Column('parameters', sa.JSON(), nullable=True),
sa.Column('detected_at', sa.DateTime(), nullable=True),
sa.Column('geometry_data_id', sa.Integer(), nullable=True),
sa.ForeignKeyConstraint(['geometry_data_id'], ['geometry_data.id'], ),
sa.ForeignKeyConstraint(['stp_file_id'], ['stp_files.id'], ),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_feature_detections_feature_type'), 'feature_detections', ['feature_type'], unique=False)
op.create_index(op.f('ix_feature_detections_id'), 'feature_detections', ['id'], unique=False)
op.create_index(op.f('ix_feature_detections_stp_file_id'), 'feature_detections', ['stp_file_id'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
"""Downgrade schema."""
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_feature_detections_stp_file_id'), table_name='feature_detections')
op.drop_index(op.f('ix_feature_detections_id'), table_name='feature_detections')
op.drop_index(op.f('ix_feature_detections_feature_type'), table_name='feature_detections')
op.drop_table('feature_detections')
op.drop_index(op.f('ix_sales_order_items_id'), table_name='sales_order_items')
op.drop_table('sales_order_items')
op.drop_index(op.f('ix_purchase_order_items_id'), table_name='purchase_order_items')
op.drop_table('purchase_order_items')
op.drop_index(op.f('ix_processing_tasks_task_id'), table_name='processing_tasks')
op.drop_index(op.f('ix_processing_tasks_stp_file_id'), table_name='processing_tasks')
op.drop_index(op.f('ix_processing_tasks_id'), table_name='processing_tasks')
op.drop_table('processing_tasks')
op.drop_index(op.f('ix_mold_cavity_data_stp_file_id'), table_name='mold_cavity_data')
op.drop_index(op.f('ix_mold_cavity_data_is_fallback'), table_name='mold_cavity_data')
op.drop_index(op.f('ix_mold_cavity_data_id'), table_name='mold_cavity_data')
op.drop_index(op.f('ix_mold_cavity_data_best_scheme_id'), table_name='mold_cavity_data')
op.drop_table('mold_cavity_data')
op.drop_index(op.f('ix_mesh_data_stp_file_id'), table_name='mesh_data')
op.drop_index(op.f('ix_mesh_data_id'), table_name='mesh_data')
op.drop_table('mesh_data')
op.drop_index(op.f('ix_html_files_stp_file_id'), table_name='html_files')
op.drop_index(op.f('ix_html_files_id'), table_name='html_files')
op.drop_table('html_files')
op.drop_index(op.f('ix_geometry_data_stp_file_id'), table_name='geometry_data')
op.drop_index(op.f('ix_geometry_data_id'), table_name='geometry_data')
op.drop_table('geometry_data')
op.drop_index(op.f('ix_finance_allocations_transaction_id'), table_name='finance_allocations')
op.drop_index(op.f('ix_finance_allocations_order_type'), table_name='finance_allocations')
op.drop_index(op.f('ix_finance_allocations_order_id'), table_name='finance_allocations')
op.drop_index(op.f('ix_finance_allocations_id'), table_name='finance_allocations')
op.drop_index(op.f('ix_finance_allocations_created_at'), table_name='finance_allocations')
op.drop_table('finance_allocations')
op.drop_index(op.f('ix_design_recommendations_stp_file_id'), table_name='design_recommendations')
op.drop_index(op.f('ix_design_recommendations_id'), table_name='design_recommendations')
op.drop_table('design_recommendations')
op.drop_index(op.f('ix_analysis_metrics_stp_file_id'), table_name='analysis_metrics')
op.drop_index(op.f('ix_analysis_metrics_id'), table_name='analysis_metrics')
op.drop_table('analysis_metrics')
op.drop_index(op.f('ix_user_roles_user_id'), table_name='user_roles')
op.drop_index(op.f('ix_user_roles_role_id'), table_name='user_roles')
op.drop_index(op.f('ix_user_roles_id'), table_name='user_roles')
op.drop_table('user_roles')
op.drop_index(op.f('ix_user_activities_user_id'), table_name='user_activities')
op.drop_index(op.f('ix_user_activities_id'), table_name='user_activities')
op.drop_index(op.f('ix_user_activities_created_at'), table_name='user_activities')
op.drop_index(op.f('ix_user_activities_activity_type'), table_name='user_activities')
op.drop_table('user_activities')
op.drop_index(op.f('ix_system_logs_level'), table_name='system_logs')
op.drop_index(op.f('ix_system_logs_id'), table_name='system_logs')
op.drop_index(op.f('ix_system_logs_created_at'), table_name='system_logs')
op.drop_table('system_logs')
op.drop_index(op.f('ix_stp_files_user_id'), table_name='stp_files')
op.drop_index(op.f('ix_stp_files_upload_batch'), table_name='stp_files')
op.drop_index(op.f('ix_stp_files_status'), table_name='stp_files')
op.drop_index(op.f('ix_stp_files_original_filename'), table_name='stp_files')
op.drop_index(op.f('ix_stp_files_object_key'), table_name='stp_files')
op.drop_index(op.f('ix_stp_files_id'), table_name='stp_files')
op.drop_index(op.f('ix_stp_files_file_hash'), table_name='stp_files')
op.drop_table('stp_files')
op.drop_index(op.f('ix_stock_movements_product_id'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_id'), table_name='stock_movements')
op.drop_index(op.f('ix_stock_movements_created_at'), table_name='stock_movements')
op.drop_table('stock_movements')
op.drop_index(op.f('ix_sales_orders_production_status'), table_name='sales_orders')
op.drop_index(op.f('ix_sales_orders_production_no'), table_name='sales_orders')
op.drop_index(op.f('ix_sales_orders_order_no'), table_name='sales_orders')
op.drop_index(op.f('ix_sales_orders_id'), table_name='sales_orders')
op.drop_index(op.f('ix_sales_orders_customer_id'), table_name='sales_orders')
op.drop_table('sales_orders')
op.drop_index(op.f('ix_role_permissions_role_id'), table_name='role_permissions')
op.drop_index(op.f('ix_role_permissions_permission_id'), table_name='role_permissions')
op.drop_index(op.f('ix_role_permissions_id'), table_name='role_permissions')
op.drop_table('role_permissions')
op.drop_index(op.f('ix_purchase_orders_supplier_id'), table_name='purchase_orders')
op.drop_index(op.f('ix_purchase_orders_order_no'), table_name='purchase_orders')
op.drop_index(op.f('ix_purchase_orders_id'), table_name='purchase_orders')
op.drop_table('purchase_orders')
op.drop_index(op.f('ix_product_materials_material_product_id'), table_name='product_materials')
op.drop_index(op.f('ix_product_materials_id'), table_name='product_materials')
op.drop_index(op.f('ix_product_materials_finished_product_id'), table_name='product_materials')
op.drop_table('product_materials')
op.drop_index(op.f('ix_material_suppliers_supplier_id'), table_name='material_suppliers')
op.drop_index(op.f('ix_material_suppliers_product_id'), table_name='material_suppliers')
op.drop_index(op.f('ix_material_suppliers_id'), table_name='material_suppliers')
op.drop_table('material_suppliers')
op.drop_index(op.f('ix_material_price_history_supplier_id'), table_name='material_price_history')
op.drop_index(op.f('ix_material_price_history_product_id'), table_name='material_price_history')
op.drop_index(op.f('ix_material_price_history_id'), table_name='material_price_history')
op.drop_index(op.f('ix_material_price_history_effective_date'), table_name='material_price_history')
op.drop_table('material_price_history')
op.drop_index(op.f('ix_inventory_warehouse_id'), table_name='inventory')
op.drop_index(op.f('ix_inventory_product_id'), table_name='inventory')
op.drop_index(op.f('ix_inventory_id'), table_name='inventory')
op.drop_table('inventory')
op.drop_index(op.f('ix_finance_transactions_txn_type'), table_name='finance_transactions')
op.drop_index(op.f('ix_finance_transactions_txn_no'), table_name='finance_transactions')
op.drop_index(op.f('ix_finance_transactions_txn_date'), table_name='finance_transactions')
op.drop_index(op.f('ix_finance_transactions_status'), table_name='finance_transactions')
op.drop_index(op.f('ix_finance_transactions_partner_type'), table_name='finance_transactions')
op.drop_index(op.f('ix_finance_transactions_partner_id'), table_name='finance_transactions')
op.drop_index(op.f('ix_finance_transactions_id'), table_name='finance_transactions')
op.drop_index(op.f('ix_finance_transactions_created_at'), table_name='finance_transactions')
op.drop_table('finance_transactions')
op.drop_index(op.f('ix_warehouses_id'), table_name='warehouses')
op.drop_index(op.f('ix_warehouses_code'), table_name='warehouses')
op.drop_table('warehouses')
op.drop_index(op.f('ix_users_username'), table_name='users')
op.drop_index(op.f('ix_users_id'), table_name='users')
op.drop_index(op.f('ix_users_email'), table_name='users')
op.drop_table('users')
op.drop_index(op.f('ix_suppliers_id'), table_name='suppliers')
op.drop_index(op.f('ix_suppliers_code'), table_name='suppliers')
op.drop_table('suppliers')
op.drop_index(op.f('ix_roles_id'), table_name='roles')
op.drop_index(op.f('ix_roles_code'), table_name='roles')
op.drop_table('roles')
op.drop_index(op.f('ix_products_sku'), table_name='products')
op.drop_index(op.f('ix_products_item_type'), table_name='products')
op.drop_index(op.f('ix_products_id'), table_name='products')
op.drop_table('products')
op.drop_index(op.f('ix_permissions_id'), table_name='permissions')
op.drop_index(op.f('ix_permissions_code'), table_name='permissions')
op.drop_table('permissions')
op.drop_index(op.f('ix_customers_id'), table_name='customers')
op.drop_index(op.f('ix_customers_code'), table_name='customers')
op.drop_table('customers')
# ### end Alembic commands ###
+36 -94
View File
@@ -7,9 +7,44 @@ from shared.models.database import User, Role, Permission, UserRole, RolePermiss
from shared.services.auth_service import get_password_hash from shared.services.auth_service import get_password_hash
from shared.config.settings import settings from shared.config.settings import settings
from shared.utils.logger import get_logger from shared.utils.logger import get_logger
from alembic.config import Config
from alembic import command
logger = get_logger(__name__) logger = get_logger(__name__)
_ALEMBIC_INI = Path(__file__).resolve().parents[3] / "alembic.ini"
def _alembic_stamp_head() -> None:
"""将当前 DB 标记为已到最新版本(基线既有 DB,不执行 SQL)"""
cfg = Config(str(_ALEMBIC_INI))
command.stamp(cfg, "head")
def _alembic_upgrade_head() -> None:
"""执行 alembic 迁移到最新版本"""
cfg = Config(str(_ALEMBIC_INI))
command.upgrade(cfg, "head")
async def _run_alembic_migrations() -> None:
"""以 alembic 管理 schema:既有未纳入管理的 DB 自动 stamp 基线,再 upgrade head
- 全新 DB:upgrade head 执行初始迁移,创建全部表
- 既有已纳入管理:upgrade head 为 no-op
- 既有但无 alembic_version(历史 DB):先 stamp head 基线,再 upgrade(no-op)
"""
async with db_manager.engine.begin() as conn:
has_alembic = await conn.execute(text("SELECT to_regclass('public.alembic_version')")).scalar()
if not has_alembic:
table_count = await conn.execute(
text("SELECT count(*) FROM information_schema.tables WHERE table_schema='public' AND table_name <> 'alembic_version'")
).scalar()
if table_count and table_count > 0:
logger.info("检测到既有 DB 未纳入 alembic 管理,自动 stamp head 作为基线")
await asyncio.to_thread(_alembic_stamp_head)
await asyncio.to_thread(_alembic_upgrade_head)
DEFAULT_PERMISSIONS = [ DEFAULT_PERMISSIONS = [
{"code": "view_dashboard", "name": "查看仪表盘", "module": "dashboard"}, {"code": "view_dashboard", "name": "查看仪表盘", "module": "dashboard"},
{"code": "view_moldinsight", "name": "使用模具分析", "module": "moldinsight"}, {"code": "view_moldinsight", "name": "使用模具分析", "module": "moldinsight"},
@@ -115,8 +150,7 @@ async def init_database(keep_connected: bool = True):
"""初始化数据库""" """初始化数据库"""
try: try:
await db_manager.connect() await db_manager.connect()
await db_manager.create_tables() await _run_alembic_migrations()
await ensure_schema_updates()
async with db_manager.session() as session: async with db_manager.session() as session:
perm_map = await init_permissions(session) perm_map = await init_permissions(session)
@@ -153,97 +187,5 @@ async def init_database(keep_connected: bool = True):
await db_manager.disconnect() await db_manager.disconnect()
async def ensure_schema_updates():
async with db_manager.engine.begin() as conn:
await conn.execute(text("ALTER TABLE products ADD COLUMN IF NOT EXISTS item_type VARCHAR(20) DEFAULT 'finished'"))
await conn.execute(text("UPDATE products SET item_type = 'finished' WHERE item_type IS NULL"))
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS production_status VARCHAR(20) DEFAULT 'not_started'"))
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS production_no VARCHAR(50)"))
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS planned_material_cost DOUBLE PRECISION DEFAULT 0"))
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS actual_material_cost DOUBLE PRECISION DEFAULT 0"))
await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS received_date TIMESTAMP WITHOUT TIME ZONE"))
await conn.execute(text("ALTER TABLE purchase_orders ADD COLUMN IF NOT EXISTS paid_date TIMESTAMP WITHOUT TIME ZONE"))
await conn.execute(text("""
CREATE TABLE IF NOT EXISTS product_materials (
id SERIAL PRIMARY KEY,
finished_product_id INTEGER NOT NULL REFERENCES products(id),
material_product_id INTEGER NOT NULL REFERENCES products(id),
quantity DOUBLE PRECISION NOT NULL,
loss_rate DOUBLE PRECISION DEFAULT 0,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
"""))
await conn.execute(text("""
CREATE UNIQUE INDEX IF NOT EXISTS uq_product_material_unique
ON product_materials (finished_product_id, material_product_id)
"""))
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS best_scheme_id VARCHAR(64)"))
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS confidence_score DOUBLE PRECISION"))
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS is_fallback BOOLEAN"))
await conn.execute(text("ALTER TABLE mold_cavity_data ADD COLUMN IF NOT EXISTS fallback_reason TEXT"))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_mold_cavity_best_scheme_id
ON mold_cavity_data (best_scheme_id)
"""))
await conn.execute(text("""
CREATE INDEX IF NOT EXISTS idx_mold_cavity_is_fallback
ON mold_cavity_data (is_fallback)
"""))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'uq_inventory_product_warehouse'
) THEN
ALTER TABLE inventory
ADD CONSTRAINT uq_inventory_product_warehouse UNIQUE (product_id, warehouse_id);
END IF;
END $$;
"""))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'ck_inventory_qty_nonnegative'
) THEN
ALTER TABLE inventory
ADD CONSTRAINT ck_inventory_qty_nonnegative
CHECK (quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity);
END IF;
END $$;
"""))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'ck_purchase_order_items_qty'
) THEN
ALTER TABLE purchase_order_items
ADD CONSTRAINT ck_purchase_order_items_qty
CHECK (quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity);
END IF;
END $$;
"""))
await conn.execute(text("""
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint
WHERE conname = 'ck_sales_order_items_qty'
) THEN
ALTER TABLE sales_order_items
ADD CONSTRAINT ck_sales_order_items_qty
CHECK (quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity);
END IF;
END $$;
"""))
await conn.execute(text("ALTER TABLE sales_orders ADD COLUMN IF NOT EXISTS manufacturing_date TIMESTAMP WITHOUT TIME ZONE"))
await conn.execute(text("ALTER TABLE sales_orders ALTER COLUMN manufacturing_date TYPE TIMESTAMP WITHOUT TIME ZONE"))
if __name__ == "__main__": if __name__ == "__main__":
asyncio.run(init_database(keep_connected=False)) asyncio.run(init_database(keep_connected=False))
-55
View File
@@ -1,55 +0,0 @@
"""数据库迁移脚本 - 删除旧表并重新创建"""
import asyncio
from shared.database.database import db_manager
from shared.models.database import Base
from shared.utils.logger import get_logger
logger = get_logger(__name__)
async def migrate_database():
"""迁移数据库:删除所有表并重新创建"""
try:
# 连接数据库
await db_manager.connect()
# 删除所有表
logger.info("正在删除所有数据库表...")
async with db_manager.engine.begin() as conn:
await conn.run_sync(Base.metadata.drop_all)
# 重新创建所有表
logger.info("正在创建所有数据库表...")
async with db_manager.engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
logger.info("数据库迁移完成!")
return True
except Exception as e:
logger.error(f"数据库迁移失败: {e}")
return False
finally:
await db_manager.disconnect()
if __name__ == "__main__":
import sys
# 检查命令行参数
if len(sys.argv) > 1 and sys.argv[1] == '--force':
confirm = 'yes'
else:
print("=== 数据库迁移 ===")
print("警告:这将删除所有数据库表和数据!")
confirm = input("确认继续?(yes/no): ")
if confirm.lower() == 'yes':
asyncio.run(migrate_database())
else:
print("已取消迁移")
+8 -1
View File
@@ -1,5 +1,5 @@
# models/database.py # models/database.py
from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint, Numeric from sqlalchemy import Column, Integer, String, Text, DateTime, Date, JSON, LargeBinary, Boolean, Float, ForeignKey, UniqueConstraint, Numeric, CheckConstraint
from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.sql import func from sqlalchemy.sql import func
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
@@ -662,6 +662,7 @@ class Inventory(Base):
__tablename__ = "inventory" __tablename__ = "inventory"
__table_args__ = ( __table_args__ = (
UniqueConstraint("product_id", "warehouse_id", name="uq_inventory_product_warehouse"), UniqueConstraint("product_id", "warehouse_id", name="uq_inventory_product_warehouse"),
CheckConstraint("quantity >= 0 AND locked_quantity >= 0 AND locked_quantity <= quantity", name="ck_inventory_qty_nonnegative"),
) )
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
@@ -740,6 +741,9 @@ class PurchaseOrder(Base):
class PurchaseOrderItem(Base): class PurchaseOrderItem(Base):
"""采购订单明细表""" """采购订单明细表"""
__tablename__ = "purchase_order_items" __tablename__ = "purchase_order_items"
__table_args__ = (
CheckConstraint("quantity > 0 AND received_quantity >= 0 AND received_quantity <= quantity", name="ck_purchase_order_items_qty"),
)
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
order_id = Column(Integer, ForeignKey("purchase_orders.id"), nullable=False) order_id = Column(Integer, ForeignKey("purchase_orders.id"), nullable=False)
@@ -860,6 +864,9 @@ class AnalysisMetrics(Base):
class SalesOrderItem(Base): class SalesOrderItem(Base):
"""销售订单明细表""" """销售订单明细表"""
__tablename__ = "sales_order_items" __tablename__ = "sales_order_items"
__table_args__ = (
CheckConstraint("quantity > 0 AND delivered_quantity >= 0 AND delivered_quantity <= quantity", name="ck_sales_order_items_qty"),
)
id = Column(Integer, primary_key=True, index=True) id = Column(Integer, primary_key=True, index=True)
order_id = Column(Integer, ForeignKey("sales_orders.id"), nullable=False) order_id = Column(Integer, ForeignKey("sales_orders.id"), nullable=False)