Compare commits
4 Commits
f3d2951dc3
...
cf6d708566
| Author | SHA1 | Date | |
|---|---|---|---|
| cf6d708566 | |||
| a8c0e1af3d | |||
| 00ca81287e | |||
| 66ba6e00f5 |
+149
@@ -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
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Generic single-database configuration.
|
||||||
@@ -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()
|
||||||
@@ -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,38 @@
|
|||||||
|
"""add product_id to stp_files
|
||||||
|
|
||||||
|
Revision ID: 006c18c51b0d
|
||||||
|
Revises: 9928d7f8c1ef
|
||||||
|
Create Date: 2026-07-23 10:37:56.516787
|
||||||
|
|
||||||
|
"""
|
||||||
|
from typing import Sequence, Union
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision: str = '006c18c51b0d'
|
||||||
|
down_revision: Union[str, Sequence[str], None] = '9928d7f8c1ef'
|
||||||
|
branch_labels: Union[str, Sequence[str], None] = None
|
||||||
|
depends_on: Union[str, Sequence[str], None] = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
"""Upgrade schema: stp_files 加 product_id 外键,关联进销存成品。"""
|
||||||
|
op.add_column("stp_files", sa.Column("product_id", sa.Integer(), nullable=True))
|
||||||
|
op.create_index("ix_stp_files_product_id", "stp_files", ["product_id"])
|
||||||
|
op.create_foreign_key(
|
||||||
|
"fk_stp_files_product_id_products",
|
||||||
|
"stp_files",
|
||||||
|
"products",
|
||||||
|
["product_id"],
|
||||||
|
["id"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
"""Downgrade schema."""
|
||||||
|
op.drop_constraint("fk_stp_files_product_id_products", "stp_files", type_="foreignkey")
|
||||||
|
op.drop_index("ix_stp_files_product_id", table_name="stp_files")
|
||||||
|
op.drop_column("stp_files", "product_id")
|
||||||
@@ -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 ###
|
||||||
@@ -93,7 +93,8 @@
|
|||||||
- **现状**:模具类型硬编码 if-else(`multi_scheme_planner.py:40`);特征检测器硬编码 6 个(`geometry_analyzer.py:76-99`);`process_file_core` 280 行。
|
- **现状**:模具类型硬编码 if-else(`multi_scheme_planner.py:40`);特征检测器硬编码 6 个(`geometry_analyzer.py:76-99`);`process_file_core` 280 行。
|
||||||
- **目标**:`FeatureDetectorRegistry` + `MoldGeneratorRegistry`(`@register` 装饰器);`process_file_core` 拆成 Stage 链。
|
- **目标**:`FeatureDetectorRegistry` + `MoldGeneratorRegistry`(`@register` 装饰器);`process_file_core` 拆成 Stage 链。
|
||||||
- **解锁**:新增模具类型、IGES/BREP、批量分析。
|
- **解锁**:新增模具类型、IGES/BREP、批量分析。
|
||||||
- **状态**:- [ ]
|
- **进展(2026-07-15)**:✅ `MoldGeneratorRegistry`(消除 `multi_scheme_planner` if-else,新增模具类型只需 `register`)+ ✅ `FeatureDetectorRegistry`(消除 6 个检测器硬编码,新增检测器只需 `register`)+ 顺带移除 OCC 线程池改用注册表串行执行;⏳ Stage 流水线**暂缓**--`process_file_core`(278 行/10 stage/~20 跨 stage 变量)是 STP 处理核心,本环境无 OCC 无法运行时验证,盲改风险高。建议在有 OCC 的环境按现有 `_step_*` 模式增量抽取 inline stages(parse_stp / build_plan_result / generate_visualization / analyze_design / generate_llm_report / finalize)
|
||||||
|
- **状态**:- [~](2/3:两个注册表完成,Stage 流水线暂缓)
|
||||||
|
|
||||||
### P1-3 前端 OpenAPI 契约生成
|
### P1-3 前端 OpenAPI 契约生成
|
||||||
- **现状**:前端 40+ 处 `any`,字段全手写已大面积错配。
|
- **现状**:前端 40+ 处 `any`,字段全手写已大面积错配。
|
||||||
@@ -103,7 +104,7 @@
|
|||||||
### P1-4 引入 Alembic,废除裸 DDL
|
### P1-4 引入 Alembic,废除裸 DDL
|
||||||
- **现状**:无 `alembic.ini`;`init_db.py` 22 条 `ALTER TABLE ADD COLUMN IF NOT EXISTS`,无版本/无回滚;`migrate_db.py` 是 `drop_all` 破坏性脚本;两应用 startup 并发跑 DDL 争锁。
|
- **现状**:无 `alembic.ini`;`init_db.py` 22 条 `ALTER TABLE ADD COLUMN IF NOT EXISTS`,无版本/无回滚;`migrate_db.py` 是 `drop_all` 破坏性脚本;两应用 startup 并发跑 DDL 争锁。
|
||||||
- **目标**:`alembic init`,固化版本化迁移,启动只 `upgrade head`;删 `migrate_db.py`。
|
- **目标**:`alembic init`,固化版本化迁移,启动只 `upgrade head`;删 `migrate_db.py`。
|
||||||
- **状态**:- [ ]
|
- **状态**:- [x]
|
||||||
|
|
||||||
### P1-5 统一材料属性源
|
### P1-5 统一材料属性源
|
||||||
- **现状**:材料字典在 4 处重复定义且冲突(PE 收缩率 `material_service` 0.020 vs `aluminum_foam_mold.py:92` 0.025)。
|
- **现状**:材料字典在 4 处重复定义且冲突(PE 收缩率 `material_service` 0.020 vs `aluminum_foam_mold.py:92` 0.025)。
|
||||||
@@ -121,6 +122,8 @@
|
|||||||
- ✅ 全量 import 测试通过:55 inventory 路由无丢失,5 个 service 全部正常加载
|
- ✅ 全量 import 测试通过:55 inventory 路由无丢失,5 个 service 全部正常加载
|
||||||
- ⏳ 可选后续:剩余纯 CRUD 路由(product/supplier/customer/warehouse/material/dashboard)体量小,可按需增量抽取
|
- ⏳ 可选后续:剩余纯 CRUD 路由(product/supplier/customer/warehouse/material/dashboard)体量小,可按需增量抽取
|
||||||
- ✅ **P1-5 统一材料属性源**:`MaterialService` 成为唯一源,删除 geometry_analyzer/mold_generator/aluminum_foam_mold 三处重复字典,改查询 MaterialService;解决冲突(PE 收缩率统一 0.020、PC/PA/PMMA 收缩率、POM 密度统一)、补齐 PS、统一泡沫 `shrinkage` 键名、补 `min_wall`/泡沫字段;py_compile + 一致性核对通过
|
- ✅ **P1-5 统一材料属性源**:`MaterialService` 成为唯一源,删除 geometry_analyzer/mold_generator/aluminum_foam_mold 三处重复字典,改查询 MaterialService;解决冲突(PE 收缩率统一 0.020、PC/PA/PMMA 收缩率、POM 密度统一)、补齐 PS、统一泡沫 `shrinkage` 键名、补 `min_wall`/泡沫字段;py_compile + 一致性核对通过
|
||||||
|
- ✅ **P1-4 引入 Alembic**:`alembic init` + 配置 env.py(接 settings+models metadata);补 3 个 CheckConstraint 到 models;离线生成初始迁移(31 表+约束+95 索引,全 sa.* 通用类型);`init_db.py` 用 `_run_alembic_migrations`(自动基线+upgrade head)替换 `create_tables`+`ensure_schema_updates`(删 92 行裸 DDL);删破坏性 `migrate_db.py`。**既有 DB 自动 stamp 基线**(无需手动);全新部署建议先 `alembic upgrade head` 再启应用
|
||||||
|
- ✅ **P1-2 可插拔注册表**(Stage 流水线暂缓):新增 `MoldGeneratorRegistry`(`multi_scheme_planner` 消除 if-else,按 mold_type 选生成器)+ `FeatureDetectorRegistry`(`geometry_analyzer._detect_features` 消除 6 个检测器硬编码,改遍历注册表);新增模具类型/特征检测器只需 `register` 一行;顺带移除 OCC 线程池改串行。Stage 流水线(process_file_core 拆分)因无 OCC 运行环境暂缓,文档留计划
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -129,7 +132,7 @@
|
|||||||
### P2-1 打通模具分析 -> 进销存(最高产品价值)
|
### P2-1 打通模具分析 -> 进销存(最高产品价值)
|
||||||
- **现状**:`STPFile` 无 `product_id`,moldinsight 与 inventory 零数据关联。
|
- **现状**:`STPFile` 无 `product_id`,moldinsight 与 inventory 零数据关联。
|
||||||
- **目标**:`STPFile` 加 `product_id` 外键(可空),分析完成后一键创建 `Product(finished)` 并回写。
|
- **目标**:`STPFile` 加 `product_id` 外键(可空),分析完成后一键创建 `Product(finished)` 并回写。
|
||||||
- **状态**:- [ ]
|
- **状态**:- [x]
|
||||||
|
|
||||||
### P2-2 真 AI 落地,砍掉假 AI
|
### P2-2 真 AI 落地,砍掉假 AI
|
||||||
- **现状**:`ai_mold_assistant.py` 209 行纯 stub 从未被调用;`ai_parting_detector.py` GNN 框架完整但无权重;`llm_service` 是唯一真接 AI(且有 P0-2 bug)。
|
- **现状**:`ai_mold_assistant.py` 209 行纯 stub 从未被调用;`ai_parting_detector.py` GNN 框架完整但无权重;`llm_service` 是唯一真接 AI(且有 P0-2 bug)。
|
||||||
@@ -140,6 +143,10 @@
|
|||||||
- 依赖 P1-2 完成后才有性价比。
|
- 依赖 P1-2 完成后才有性价比。
|
||||||
- **状态**:- [ ]
|
- **状态**:- [ ]
|
||||||
|
|
||||||
|
### P2 执行结果
|
||||||
|
|
||||||
|
- ✅ **P2-1 打通模具分析 -> 进销存**:`STPFile` 加 `product_id` 外键(nullable+index+FK)+ Alembic 迁移 `006c18c51b0d`(首次真实迁移);inventory `POST /api/products/from-task/{task_id}` 端点(按 task_id 查 STPFile,幂等创建 `Product(finished)`,回写 product_id,SKU=`MI{stp_file_id}`,描述含体积/重量/表面积);前端 ResultView 导出栏加「创建为成品」按钮。py_compile + alembic heads + vue-tsc 0 错误通过。**模具分析 -> 成品 -> BOM -> 销售/采购的业务闭环接通**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## P3 工程治理(穿插顺手做)
|
## P3 工程治理(穿插顺手做)
|
||||||
@@ -159,6 +166,6 @@
|
|||||||
| 阶段 | 项数 | 已完成 | 进行中 |
|
| 阶段 | 项数 | 已完成 | 进行中 |
|
||||||
|------|------|--------|--------|
|
|------|------|--------|--------|
|
||||||
| P0 | 7 | 6 修复 + 1 排查 | - |
|
| P0 | 7 | 6 修复 + 1 排查 | - |
|
||||||
| P1 | 5 | 2 | P1-1 核心完成 + P1-5 完成 |
|
| P1 | 5 | 4 | P1-1+P1-5+P1-4 完成 + P1-2 注册表完成(Stage 暂缓) |
|
||||||
| P2 | 3 | 0 | - |
|
| P2 | 3 | 1 | P2-1 完成 |
|
||||||
| P3 | 7 | 0 | - |
|
| P3 | 7 | 0 | - |
|
||||||
|
|||||||
@@ -120,6 +120,9 @@
|
|||||||
<t-button type="default" size="small" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
|
<t-button type="default" size="small" @click="exportCAD('brep')" title="导出BRep格式(FreeCAD原生)">
|
||||||
导出 BRep
|
导出 BRep
|
||||||
</t-button>
|
</t-button>
|
||||||
|
<t-button type="default" size="small" @click="createProductFromAnalysis" :loading="state.creatingProduct" title="将本次模具分析创建为进销存成品,可在进销存模块继续配置 BOM / 销售">
|
||||||
|
📋 创建为成品
|
||||||
|
</t-button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="preview-3d" v-if="selectedHtmlFile" class="viewer-section viewer-section-hero">
|
<div id="preview-3d" v-if="selectedHtmlFile" class="viewer-section viewer-section-hero">
|
||||||
@@ -603,7 +606,8 @@ const state = reactive({
|
|||||||
surface_quality: 'standard',
|
surface_quality: 'standard',
|
||||||
controller: 'fanuc',
|
controller: 'fanuc',
|
||||||
include_gcode: false
|
include_gcode: false
|
||||||
}
|
},
|
||||||
|
creatingProduct: false
|
||||||
})
|
})
|
||||||
|
|
||||||
const camSteelOptions = [
|
const camSteelOptions = [
|
||||||
@@ -644,6 +648,19 @@ const loadTask = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const createProductFromAnalysis = async () => {
|
||||||
|
const taskId = route.params.taskId as string
|
||||||
|
try {
|
||||||
|
state.creatingProduct = true
|
||||||
|
const product = await apiRequest<any>(`/api/products/from-task/${taskId}`, { method: 'POST' })
|
||||||
|
addNotification(`已创建成品:${product.name}(SKU: ${product.sku})`, 'success')
|
||||||
|
} catch (e) {
|
||||||
|
handleApiError(e, '创建成品')
|
||||||
|
} finally {
|
||||||
|
state.creatingProduct = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (!appStore.user) {
|
if (!appStore.user) {
|
||||||
router.push('/login')
|
router.push('/login')
|
||||||
|
|||||||
@@ -14,10 +14,11 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
from sqlalchemy import select, or_, func, delete
|
from sqlalchemy import select, or_, func, delete
|
||||||
from typing import Optional, List, Dict
|
from typing import Optional, List, Dict
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from shared.database.database import get_db_session
|
from shared.database.database import get_db_session
|
||||||
from shared.services.auth_service import get_current_active_user, get_current_admin_user
|
from shared.services.auth_service import get_current_active_user, get_current_admin_user
|
||||||
from shared.models.database import User, Product, ProductMaterial
|
from shared.models.database import User, Product, ProductMaterial, STPFile, ProcessingTask
|
||||||
from ..schemas import (
|
from ..schemas import (
|
||||||
ProductCreate,
|
ProductCreate,
|
||||||
ProductResponse,
|
ProductResponse,
|
||||||
@@ -118,6 +119,70 @@ async def create_product(
|
|||||||
return _build_product_response(product, 0)
|
return _build_product_response(product, 0)
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/from-task/{task_id}", response_model=ProductResponse, status_code=201)
|
||||||
|
async def create_product_from_task(
|
||||||
|
task_id: str,
|
||||||
|
db_session: AsyncSession = Depends(get_db_session),
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
"""从模具分析任务创建进销存成品,回写 stp_files.product_id(P2-1)"""
|
||||||
|
task_result = await db_session.execute(select(ProcessingTask).where(ProcessingTask.task_id == task_id))
|
||||||
|
task = task_result.scalar_one_or_none()
|
||||||
|
if not task:
|
||||||
|
raise HTTPException(status_code=404, detail="分析任务不存在")
|
||||||
|
stp_result = await db_session.execute(select(STPFile).where(STPFile.id == task.stp_file_id))
|
||||||
|
stp_file = stp_result.scalar_one_or_none()
|
||||||
|
if not stp_file:
|
||||||
|
raise HTTPException(status_code=404, detail="STP 分析记录不存在")
|
||||||
|
|
||||||
|
# 已关联成品则直接返回(幂等)
|
||||||
|
if stp_file.product_id:
|
||||||
|
existed = await db_session.execute(select(Product).where(Product.id == stp_file.product_id))
|
||||||
|
product = existed.scalar_one_or_none()
|
||||||
|
if product:
|
||||||
|
return _build_product_response(product, 0)
|
||||||
|
|
||||||
|
# 生成唯一 SKU:MI{stp_file_id},冲突则追加序号
|
||||||
|
base_sku = f"MI{stp_file_id}"
|
||||||
|
sku = base_sku
|
||||||
|
n = 1
|
||||||
|
while True:
|
||||||
|
conflict = await db_session.execute(select(Product).where(Product.sku == sku))
|
||||||
|
if not conflict.scalar_one_or_none():
|
||||||
|
break
|
||||||
|
n += 1
|
||||||
|
sku = f"{base_sku}-{n}"
|
||||||
|
|
||||||
|
name = Path(stp_file.original_filename or f"mold_{stp_file_id}").stem or f"模具分析-{stp_file_id}"
|
||||||
|
desc_parts = []
|
||||||
|
if stp_file.volume:
|
||||||
|
desc_parts.append(f"体积 {stp_file.volume:.1f} mm³")
|
||||||
|
if stp_file.product_weight:
|
||||||
|
desc_parts.append(f"重量 {stp_file.product_weight:.2f} g")
|
||||||
|
if stp_file.surface_area:
|
||||||
|
desc_parts.append(f"表面积 {stp_file.surface_area:.1f} mm²")
|
||||||
|
description = "由模具分析创建" + (":" + ";".join(desc_parts) if desc_parts else "")
|
||||||
|
|
||||||
|
product = Product(
|
||||||
|
sku=sku,
|
||||||
|
name=name,
|
||||||
|
description=description,
|
||||||
|
category="模具成品",
|
||||||
|
unit="件",
|
||||||
|
item_type="finished",
|
||||||
|
cost_price=0,
|
||||||
|
sale_price=0,
|
||||||
|
min_stock=0,
|
||||||
|
max_stock=0,
|
||||||
|
)
|
||||||
|
db_session.add(product)
|
||||||
|
await db_session.flush()
|
||||||
|
stp_file.product_id = product.id
|
||||||
|
await db_session.commit()
|
||||||
|
await db_session.refresh(product)
|
||||||
|
return _build_product_response(product, 0)
|
||||||
|
|
||||||
|
|
||||||
@router.put("/{product_id}", response_model=ProductResponse)
|
@router.put("/{product_id}", response_model=ProductResponse)
|
||||||
async def update_product(
|
async def update_product(
|
||||||
product_id: int,
|
product_id: int,
|
||||||
|
|||||||
@@ -279,27 +279,6 @@ async def design_complete_mold_system(
|
|||||||
return {"status": "success", "data": result}
|
return {"status": "success", "data": result}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/ai-parting-detect")
|
|
||||||
async def ai_parting_surface_detect(
|
|
||||||
request: Request,
|
|
||||||
current_user: User = Depends(get_current_active_user),
|
|
||||||
):
|
|
||||||
body = await request.json()
|
|
||||||
task_id = body.get("task_id")
|
|
||||||
if not task_id:
|
|
||||||
raise HTTPException(404, "缺少 task_id")
|
|
||||||
task_data = await _get_task_data(task_id)
|
|
||||||
if not task_data:
|
|
||||||
raise HTTPException(404, "任务不存在")
|
|
||||||
geometry_data = task_data.get("geometry_data")
|
|
||||||
if not geometry_data:
|
|
||||||
raise HTTPException(400, "该任务尚未完成几何分析")
|
|
||||||
from moldinsight.core.ai_parting_detector import AIPartingSurfaceDetectorV2
|
|
||||||
detector = AIPartingSurfaceDetectorV2(use_gnn=True)
|
|
||||||
result = detector._detect_with_geometry(None, geometry_data)
|
|
||||||
return {"status": "success", "data": result}
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/detect-undercuts")
|
@router.post("/detect-undercuts")
|
||||||
async def detect_undercuts(
|
async def detect_undercuts(
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -323,6 +302,34 @@ async def detect_undercuts(
|
|||||||
return {"status": "success", "data": result}
|
return {"status": "success", "data": result}
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cost-estimate")
|
||||||
|
async def estimate_cost(
|
||||||
|
request: Request,
|
||||||
|
current_user: User = Depends(get_current_active_user),
|
||||||
|
):
|
||||||
|
"""LLM 模具成本估算(P2-2:真 AI 落地,需启用 LLM)"""
|
||||||
|
body = await request.json()
|
||||||
|
task_id = body.get("task_id")
|
||||||
|
if not task_id:
|
||||||
|
raise HTTPException(404, "缺少 task_id")
|
||||||
|
task_data = await _get_task_data(task_id)
|
||||||
|
if not task_data:
|
||||||
|
raise HTTPException(404, "任务不存在")
|
||||||
|
analysis_result = task_data.get("analysis_result")
|
||||||
|
if not analysis_result:
|
||||||
|
raise HTTPException(400, "该任务尚未完成分析")
|
||||||
|
detailed_context = {
|
||||||
|
"candidate_schemes": task_data.get("candidate_schemes", []),
|
||||||
|
"geometry_data": task_data.get("geometry_data", {}),
|
||||||
|
"metadata": {"selected_material": task_data.get("material")},
|
||||||
|
}
|
||||||
|
from moldinsight.services.llm_service import llm_service
|
||||||
|
result = await llm_service.estimate_cost(analysis_result, detailed_context)
|
||||||
|
if result is None:
|
||||||
|
raise HTTPException(503, "成本估算不可用(LLM 未启用或生成失败)")
|
||||||
|
return {"status": "success", "data": result}
|
||||||
|
|
||||||
|
|
||||||
@router.post("/design-cam")
|
@router.post("/design-cam")
|
||||||
async def design_mold_cam(
|
async def design_mold_cam(
|
||||||
request: Request,
|
request: Request,
|
||||||
|
|||||||
@@ -1,209 +0,0 @@
|
|||||||
"""
|
|
||||||
AI 分模辅助模型接口示例
|
|
||||||
|
|
||||||
此文件展示了如何创建 AI 模型来辅助分模过程。
|
|
||||||
实际使用时需要替换为真实的 AI 模型。
|
|
||||||
"""
|
|
||||||
from typing import Dict, Any, Optional
|
|
||||||
import numpy as np
|
|
||||||
from OCC.Core.TopoDS import TopoDS_Shape, TopoDS_Face
|
|
||||||
|
|
||||||
|
|
||||||
class AIPartingSurfaceDetector:
|
|
||||||
"""
|
|
||||||
AI 分型面检测器(示例接口)
|
|
||||||
|
|
||||||
功能:
|
|
||||||
- 分析产品 3D 几何
|
|
||||||
- 预测最优分型面位置和方向
|
|
||||||
- 识别倒扣区域
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, model_path: Optional[str] = None):
|
|
||||||
"""
|
|
||||||
初始化 AI 分型面检测器
|
|
||||||
|
|
||||||
Args:
|
|
||||||
model_path: 训练好的模型路径
|
|
||||||
"""
|
|
||||||
self.model_path = model_path
|
|
||||||
self.model = None
|
|
||||||
|
|
||||||
# 如果提供了模型路径,加载模型
|
|
||||||
if model_path:
|
|
||||||
self._load_model(model_path)
|
|
||||||
|
|
||||||
def _load_model(self, model_path: str):
|
|
||||||
"""加载训练好的 AI 模型"""
|
|
||||||
# TODO: 实现模型加载逻辑
|
|
||||||
# 示例:
|
|
||||||
# import torch
|
|
||||||
# self.model = torch.load(model_path)
|
|
||||||
print(f"AI 模型加载:{model_path}")
|
|
||||||
|
|
||||||
def detect(self, product_shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
检测最优分型面
|
|
||||||
|
|
||||||
Args:
|
|
||||||
product_shape: OpenCASCADE 形状对象
|
|
||||||
analysis: 几何分析结果(包含 bounding_box, volume 等)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{
|
|
||||||
"origin": [x, y, z], # 分型面原点
|
|
||||||
"normal": [nx, ny, nz], # 分型面法向量
|
|
||||||
"confidence": 0.95, # 置信度
|
|
||||||
"parting_line": [...] # 可选的分型线
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# TODO: 使用 AI 模型进行预测
|
|
||||||
# 这里是示例返回
|
|
||||||
|
|
||||||
# 1. 将产品形状转换为 AI 模型输入
|
|
||||||
# - 体素化 (voxelization)
|
|
||||||
# - 点云 (point cloud)
|
|
||||||
# - 多视图 (multi-view images)
|
|
||||||
input_data = self._preprocess_shape(product_shape, analysis)
|
|
||||||
|
|
||||||
# 2. 使用模型预测
|
|
||||||
# prediction = self.model.predict(input_data)
|
|
||||||
|
|
||||||
# 3. 返回预测结果
|
|
||||||
return {
|
|
||||||
"origin": [0, 0, analysis["bounding_box"]["center"][2]],
|
|
||||||
"normal": [0, 0, 1], # Z 方向
|
|
||||||
"confidence": 0.85,
|
|
||||||
"undercut_regions": [] # 倒扣区域
|
|
||||||
}
|
|
||||||
|
|
||||||
def _preprocess_shape(self, shape: TopoDS_Shape, analysis: Dict) -> TopoDS_Shape:
|
|
||||||
"""
|
|
||||||
预处理产品形状为 AI 模型输入
|
|
||||||
|
|
||||||
可能的预处理方式:
|
|
||||||
1. 体素化:将 3D 模型转换为 3D 网格
|
|
||||||
2. 点云:采样表面点
|
|
||||||
3. 多视图:渲染多个角度的 2D 图像
|
|
||||||
"""
|
|
||||||
# TODO: 实现预处理逻辑
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class AIDraftAnalyzer:
|
|
||||||
"""
|
|
||||||
AI 拔模分析器(示例接口)
|
|
||||||
|
|
||||||
功能:
|
|
||||||
- 分析哪些面需要拔模
|
|
||||||
- 预测最优拔模角度
|
|
||||||
- 检测脱模干涉
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, model_path: Optional[str] = None):
|
|
||||||
self.model_path = model_path
|
|
||||||
self.model = None
|
|
||||||
|
|
||||||
if model_path:
|
|
||||||
self._load_model(model_path)
|
|
||||||
|
|
||||||
def _load_model(self, model_path: str):
|
|
||||||
"""加载训练好的 AI 模型"""
|
|
||||||
print(f"AI 拔模分析模型加载:{model_path}")
|
|
||||||
|
|
||||||
def analyze(self, product_shape: TopoDS_Shape, parting_surface: TopoDS_Face,
|
|
||||||
base_draft_angle: float) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
分析拔模需求
|
|
||||||
|
|
||||||
Args:
|
|
||||||
product_shape: 产品形状
|
|
||||||
parting_surface: 分型面
|
|
||||||
base_draft_angle: 基础拔模角(度)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{
|
|
||||||
"drafted_shape": ..., # 应用拔模后的形状
|
|
||||||
"draft_angles": {...}, # 各面的拔模角
|
|
||||||
"interference_areas": [...], # 干涉区域
|
|
||||||
"recommendations": [...] # 优化建议
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# TODO: 使用 AI 模型分析拔模
|
|
||||||
|
|
||||||
# 示例返回
|
|
||||||
return {
|
|
||||||
"drafted_shape": product_shape, # 简化:返回原始形状
|
|
||||||
"draft_angles": {"default": base_draft_angle},
|
|
||||||
"interference_areas": [],
|
|
||||||
"recommendations": ["建议增加圆角", "壁厚均匀化"]
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
class AICavityLayoutOptimizer:
|
|
||||||
"""
|
|
||||||
AI 型腔布局优化器(示例接口)
|
|
||||||
|
|
||||||
功能:
|
|
||||||
- 优化多型腔排列
|
|
||||||
- 设计流道系统
|
|
||||||
- 平衡材料流动
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, model_path: Optional[str] = None):
|
|
||||||
self.model_path = model_path
|
|
||||||
self.model = None
|
|
||||||
|
|
||||||
if model_path:
|
|
||||||
self._load_model(model_path)
|
|
||||||
|
|
||||||
def optimize(self, product_shape: TopoDS_Shape, cavity_count: int,
|
|
||||||
mold_base_size: Dict) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
优化型腔布局
|
|
||||||
|
|
||||||
Args:
|
|
||||||
product_shape: 产品形状
|
|
||||||
cavity_count: 型腔数量
|
|
||||||
mold_base_size: 模架尺寸
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{
|
|
||||||
"cavity_positions": [...], # 各型腔位置
|
|
||||||
"runner_system": {...}, # 流道系统设计
|
|
||||||
"balance_score": 0.92, # 流动平衡评分
|
|
||||||
"material_efficiency": 0.85 # 材料利用率
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
# TODO: 使用 AI 优化型腔布局
|
|
||||||
|
|
||||||
return {
|
|
||||||
"cavity_positions": [[0, 0, 0]], # 示例
|
|
||||||
"runner_system": {"type": "cold_runner"},
|
|
||||||
"balance_score": 0.85,
|
|
||||||
"material_efficiency": 0.80
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# ==================== 使用示例 ====================
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# 示例:如何使用 AI 模型接口
|
|
||||||
|
|
||||||
# 1. 创建 AI 模型实例
|
|
||||||
parting_detector = AIPartingSurfaceDetector(model_path="models/parting_surface.pth")
|
|
||||||
draft_analyzer = AIDraftAnalyzer(model_path="models/draft_analysis.pth")
|
|
||||||
|
|
||||||
# 2. 设置到 MoldCavityGenerator
|
|
||||||
from moldinsight.core.mold_generator import MoldCavityGenerator
|
|
||||||
|
|
||||||
generator = MoldCavityGenerator()
|
|
||||||
generator.set_ai_model(
|
|
||||||
parting_detector=parting_detector,
|
|
||||||
draft_analyzer=draft_analyzer
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3. 使用(AI 模型会自动介入)
|
|
||||||
# result = generator.generate_mold_cavities(product_shape)
|
|
||||||
|
|
||||||
print("AI 模型接口已配置,分模时将自动使用 AI 辅助")
|
|
||||||
@@ -1,547 +0,0 @@
|
|||||||
"""
|
|
||||||
AI 分型面检测模块 - 基于 GNN 的分型面预测框架
|
|
||||||
|
|
||||||
架构设计:
|
|
||||||
1. ShapeGraphBuilder - 将 OCC 形状转换为图表示(面为节点,共享边为图边)
|
|
||||||
2. PartingSurfaceGNN - 图神经网络模型定义
|
|
||||||
3. AIPartingSurfaceDetectorV2 - 增强版分型面检测器(集成 GNN)
|
|
||||||
|
|
||||||
图构建策略:
|
|
||||||
- 节点:每个 TopoDS_Face 作为一个节点
|
|
||||||
- 节点特征:法向量(3) + 面积(1) + 曲率(2) + 面类型(1) = 7维
|
|
||||||
- 边:共享 TopoDS_Edge 的面之间建立边
|
|
||||||
- 边特征:共享边长度(1) + 二面角(1) = 2维
|
|
||||||
|
|
||||||
GNN 模型:
|
|
||||||
- 3层 GraphConv + 全局池化 + MLP 分类头
|
|
||||||
- 输出:每个面的分型面归属概率 + 分型方向
|
|
||||||
|
|
||||||
依赖:
|
|
||||||
- PyTorch + PyTorch Geometric(可选,缺失时回退到几何方法)
|
|
||||||
"""
|
|
||||||
|
|
||||||
from typing import Dict, List, Any, Optional, Tuple
|
|
||||||
import numpy as np
|
|
||||||
from OCC.Core.TopoDS import TopoDS_Shape
|
|
||||||
from shared.utils.logger import get_logger
|
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
|
||||||
|
|
||||||
_TORCH_AVAILABLE = False
|
|
||||||
_TORCH_GEOMETRIC_AVAILABLE = False
|
|
||||||
|
|
||||||
try:
|
|
||||||
import torch
|
|
||||||
import torch.nn as nn
|
|
||||||
import torch.nn.functional as F
|
|
||||||
_TORCH_AVAILABLE = True
|
|
||||||
try:
|
|
||||||
from torch_geometric.nn import GCNConv, global_mean_pool
|
|
||||||
from torch_geometric.data import Data
|
|
||||||
_TORCH_GEOMETRIC_AVAILABLE = True
|
|
||||||
except ImportError:
|
|
||||||
logger.info("PyTorch Geometric 未安装,GNN 模型不可用")
|
|
||||||
except ImportError:
|
|
||||||
logger.info("PyTorch 未安装,AI 分型面检测将使用几何回退方法")
|
|
||||||
|
|
||||||
|
|
||||||
class ShapeGraphBuilder:
|
|
||||||
"""将 OCC 形状转换为图表示"""
|
|
||||||
|
|
||||||
def build_graph(self, shape: TopoDS_Shape) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
从 OCC 形状构建图数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{
|
|
||||||
"node_features": np.ndarray (N, 7),
|
|
||||||
"edge_index": np.ndarray (2, E),
|
|
||||||
"edge_features": np.ndarray (E, 2),
|
|
||||||
"face_map": List[TopoDS_Face],
|
|
||||||
"num_nodes": int,
|
|
||||||
"num_edges": int
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from OCC.Core.TopExp import TopExp_Explorer
|
|
||||||
from OCC.Core.TopAbs import TopAbs_FACE, TopAbs_EDGE
|
|
||||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
|
||||||
from OCC.Core.GProp import GProp_GProps
|
|
||||||
from OCC.Core.BRepGProp import brepgprop
|
|
||||||
from OCC.Core.Bnd import Bnd_Box
|
|
||||||
from OCC.Core.BRepBndLib import brepbndlib
|
|
||||||
from OCC.Core.TopTools import TopTools_IndexedDataMapOfShapeListOfShape
|
|
||||||
from OCC.Core.TopExp import topexp_MapShapesAndAncestors
|
|
||||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Edge, topods
|
|
||||||
|
|
||||||
faces = []
|
|
||||||
face_features = []
|
|
||||||
|
|
||||||
explorer = TopExp_Explorer(shape, TopAbs_FACE)
|
|
||||||
while explorer.More():
|
|
||||||
face = topods.Face(explorer.Current())
|
|
||||||
features = self._extract_face_features(face)
|
|
||||||
if features is not None:
|
|
||||||
faces.append(face)
|
|
||||||
face_features.append(features)
|
|
||||||
explorer.Next()
|
|
||||||
|
|
||||||
if not faces:
|
|
||||||
logger.warning("未找到面,无法构建图")
|
|
||||||
return None
|
|
||||||
|
|
||||||
node_features = np.array(face_features, dtype=np.float32)
|
|
||||||
|
|
||||||
edge_map = TopTools_IndexedDataMapOfShapeListOfShape()
|
|
||||||
topexp_MapShapesAndAncestors(shape, TopAbs_EDGE, TopAbs_FACE, edge_map)
|
|
||||||
|
|
||||||
edge_list = []
|
|
||||||
edge_features_list = []
|
|
||||||
|
|
||||||
for i in range(1, edge_map.Extent() + 1):
|
|
||||||
edge = topods.Edge(edge_map.FindKey(i))
|
|
||||||
face_list = edge_map.FindFromIndex(i)
|
|
||||||
|
|
||||||
connected_faces = []
|
|
||||||
it = face_list.begin()
|
|
||||||
while it != face_list.end():
|
|
||||||
f = topods.Face(it.Value())
|
|
||||||
try:
|
|
||||||
idx = faces.index(f)
|
|
||||||
connected_faces.append(idx)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
it.next_ptr()
|
|
||||||
|
|
||||||
if len(connected_faces) >= 2:
|
|
||||||
edge_feat = self._extract_edge_features(edge, connected_faces, faces)
|
|
||||||
for j in range(len(connected_faces)):
|
|
||||||
for k in range(j + 1, len(connected_faces)):
|
|
||||||
edge_list.append([connected_faces[j], connected_faces[k]])
|
|
||||||
edge_features_list.append(edge_feat)
|
|
||||||
|
|
||||||
if not edge_list:
|
|
||||||
logger.warning("未找到边连接,返回无图边的图")
|
|
||||||
edge_index = np.zeros((2, 0), dtype=np.int64)
|
|
||||||
edge_features_arr = np.zeros((0, 2), dtype=np.float32)
|
|
||||||
else:
|
|
||||||
edge_index = np.array(edge_list, dtype=np.int64).T
|
|
||||||
rev_edges = np.array([[e[1], e[0]] for e in edge_list], dtype=np.int64).T
|
|
||||||
edge_index = np.concatenate([edge_index, rev_edges], axis=1)
|
|
||||||
edge_features_arr = np.array(edge_features_list, dtype=np.float32)
|
|
||||||
edge_features_arr = np.concatenate([edge_features_arr, edge_features_arr], axis=0)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"node_features": node_features,
|
|
||||||
"edge_index": edge_index,
|
|
||||||
"edge_features": edge_features_arr,
|
|
||||||
"face_map": faces,
|
|
||||||
"num_nodes": len(faces),
|
|
||||||
"num_edges": edge_index.shape[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"图构建失败: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _extract_face_features(self, face: Any) -> Optional[np.ndarray]:
|
|
||||||
"""
|
|
||||||
提取面特征:[nx, ny, nz, area, u_curvature, v_curvature, face_type]
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
|
||||||
from OCC.Core.GProp import GProp_GProps
|
|
||||||
from OCC.Core.BRepGProp import brepgprop
|
|
||||||
|
|
||||||
surface = BRepAdaptor_Surface(face)
|
|
||||||
|
|
||||||
u = (surface.FirstUParameter() + surface.LastUParameter()) / 2
|
|
||||||
v = (surface.FirstVParameter() + surface.LastVParameter()) / 2
|
|
||||||
|
|
||||||
if surface.GetType() == 0:
|
|
||||||
normal = surface.Plane().Position().Direction()
|
|
||||||
face_type = 0.0
|
|
||||||
u_curv = 0.0
|
|
||||||
v_curv = 0.0
|
|
||||||
elif surface.GetType() == 1:
|
|
||||||
normal = surface.Cylinder().Position().Direction()
|
|
||||||
face_type = 1.0
|
|
||||||
radius = surface.Cylinder().Radius()
|
|
||||||
u_curv = 1.0 / radius if radius > 0.001 else 0.0
|
|
||||||
v_curv = 0.0
|
|
||||||
elif surface.GetType() == 2:
|
|
||||||
normal = surface.Cone().Position().Direction()
|
|
||||||
face_type = 2.0
|
|
||||||
u_curv = 0.0
|
|
||||||
v_curv = 0.0
|
|
||||||
elif surface.GetType() == 3:
|
|
||||||
normal = surface.Sphere().Position().Direction()
|
|
||||||
face_type = 3.0
|
|
||||||
radius = surface.Sphere().Radius()
|
|
||||||
u_curv = 1.0 / radius if radius > 0.001 else 0.0
|
|
||||||
v_curv = 1.0 / radius if radius > 0.001 else 0.0
|
|
||||||
elif surface.GetType() == 4:
|
|
||||||
normal = surface.Torus().Position().Direction()
|
|
||||||
face_type = 4.0
|
|
||||||
u_curv = 0.0
|
|
||||||
v_curv = 0.0
|
|
||||||
else:
|
|
||||||
from OCC.Core.BRepLProp import BRepLProp_SLProps
|
|
||||||
props = BRepLProp_SLProps(surface, 2, 0.001)
|
|
||||||
props.SetParameters(u, v)
|
|
||||||
if props.IsNormalDefined():
|
|
||||||
normal = props.Normal()
|
|
||||||
else:
|
|
||||||
normal = gp_Dir(0, 0, 1)
|
|
||||||
face_type = 5.0
|
|
||||||
u_curv = 0.0
|
|
||||||
v_curv = 0.0
|
|
||||||
|
|
||||||
face_props = GProp_GProps()
|
|
||||||
brepgprop.SurfaceProperties(face, face_props)
|
|
||||||
area = face_props.Mass()
|
|
||||||
|
|
||||||
return np.array([
|
|
||||||
normal.X(), normal.Y(), normal.Z(),
|
|
||||||
area,
|
|
||||||
u_curv, v_curv,
|
|
||||||
face_type
|
|
||||||
], dtype=np.float32)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.debug(f"面特征提取失败: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _extract_edge_features(self, edge: Any, connected_faces: List[int],
|
|
||||||
faces: List) -> np.ndarray:
|
|
||||||
"""
|
|
||||||
提取边特征:[edge_length, dihedral_angle]
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Curve
|
|
||||||
from OCC.Core.GProp import GProp_GProps
|
|
||||||
from OCC.Core.BRepGProp import brepgprop
|
|
||||||
|
|
||||||
curve = BRepAdaptor_Curve(edge)
|
|
||||||
first = curve.FirstParameter()
|
|
||||||
last = curve.LastParameter()
|
|
||||||
|
|
||||||
edge_len = abs(last - first)
|
|
||||||
|
|
||||||
dihedral = 0.0
|
|
||||||
if len(connected_faces) >= 2:
|
|
||||||
n1 = self._get_face_normal_fast(faces[connected_faces[0]])
|
|
||||||
n2 = self._get_face_normal_fast(faces[connected_faces[1]])
|
|
||||||
if n1 is not None and n2 is not None:
|
|
||||||
dot = np.clip(np.dot(n1, n2), -1.0, 1.0)
|
|
||||||
dihedral = np.arccos(dot)
|
|
||||||
|
|
||||||
return np.array([edge_len, dihedral], dtype=np.float32)
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
return np.array([0.0, 0.0], dtype=np.float32)
|
|
||||||
|
|
||||||
def _get_face_normal_fast(self, face: Any) -> Optional[np.ndarray]:
|
|
||||||
"""快速获取面法向量(numpy数组)"""
|
|
||||||
try:
|
|
||||||
from OCC.Core.BRepAdaptor import BRepAdaptor_Surface
|
|
||||||
surface = BRepAdaptor_Surface(face)
|
|
||||||
if surface.GetType() == 0:
|
|
||||||
n = surface.Plane().Position().Direction()
|
|
||||||
return np.array([n.X(), n.Y(), n.Z()])
|
|
||||||
return None
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
if _TORCH_GEOMETRIC_AVAILABLE:
|
|
||||||
|
|
||||||
class PartingSurfaceGNN(nn.Module):
|
|
||||||
"""
|
|
||||||
分型面检测 GNN 模型
|
|
||||||
|
|
||||||
架构:
|
|
||||||
- 3层 GCNConv (hidden_dim=64)
|
|
||||||
- 全局平均池化
|
|
||||||
- 3层 MLP 分类头
|
|
||||||
- 输出:每个面的分型面归属概率 (0-1)
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, input_dim: int = 7, hidden_dim: int = 64,
|
|
||||||
num_layers: int = 3, dropout: float = 0.3):
|
|
||||||
super().__init__()
|
|
||||||
|
|
||||||
self.input_dim = input_dim
|
|
||||||
self.hidden_dim = hidden_dim
|
|
||||||
self.num_layers = num_layers
|
|
||||||
|
|
||||||
self.input_proj = nn.Linear(input_dim, hidden_dim)
|
|
||||||
|
|
||||||
self.convs = nn.ModuleList()
|
|
||||||
self.bns = nn.ModuleList()
|
|
||||||
for _ in range(num_layers):
|
|
||||||
self.convs.append(GCNConv(hidden_dim, hidden_dim))
|
|
||||||
self.bns.append(nn.BatchNorm1d(hidden_dim))
|
|
||||||
|
|
||||||
self.dropout = dropout
|
|
||||||
|
|
||||||
self.mlp = nn.Sequential(
|
|
||||||
nn.Linear(hidden_dim, hidden_dim),
|
|
||||||
nn.ReLU(),
|
|
||||||
nn.Dropout(dropout),
|
|
||||||
nn.Linear(hidden_dim, hidden_dim // 2),
|
|
||||||
nn.ReLU(),
|
|
||||||
nn.Dropout(dropout),
|
|
||||||
nn.Linear(hidden_dim // 2, 1),
|
|
||||||
)
|
|
||||||
|
|
||||||
def forward(self, data: Data) -> torch.Tensor:
|
|
||||||
x, edge_index = data.x, data.edge_index
|
|
||||||
|
|
||||||
x = self.input_proj(x)
|
|
||||||
x = F.relu(x)
|
|
||||||
|
|
||||||
for conv, bn in zip(self.convs, self.bns):
|
|
||||||
x = conv(x, edge_index)
|
|
||||||
x = bn(x)
|
|
||||||
x = F.relu(x)
|
|
||||||
x = F.dropout(x, p=self.dropout, training=self.training)
|
|
||||||
|
|
||||||
out = self.mlp(x)
|
|
||||||
return torch.sigmoid(out).squeeze(-1)
|
|
||||||
|
|
||||||
class PartingDirectionHead(nn.Module):
|
|
||||||
"""
|
|
||||||
分型方向预测头
|
|
||||||
|
|
||||||
基于全局池化的面特征,预测分型方向向量
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, hidden_dim: int = 64):
|
|
||||||
super().__init__()
|
|
||||||
self.direction_mlp = nn.Sequential(
|
|
||||||
nn.Linear(hidden_dim, hidden_dim),
|
|
||||||
nn.ReLU(),
|
|
||||||
nn.Linear(hidden_dim, 3),
|
|
||||||
)
|
|
||||||
|
|
||||||
def forward(self, node_embeddings: torch.Tensor,
|
|
||||||
batch: torch.Tensor) -> torch.Tensor:
|
|
||||||
pooled = global_mean_pool(node_embeddings, batch)
|
|
||||||
direction = self.direction_mlp(pooled)
|
|
||||||
direction = F.normalize(direction, p=2, dim=-1)
|
|
||||||
return direction
|
|
||||||
|
|
||||||
|
|
||||||
class AIPartingSurfaceDetectorV2:
|
|
||||||
"""
|
|
||||||
增强版 AI 分型面检测器
|
|
||||||
|
|
||||||
支持:
|
|
||||||
1. GNN 模型推理(需要 PyTorch + PyG)
|
|
||||||
2. 几何方法回退(无需任何 AI 依赖)
|
|
||||||
3. 模型训练数据收集
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, model_path: Optional[str] = None,
|
|
||||||
use_gnn: bool = True,
|
|
||||||
device: str = "cpu"):
|
|
||||||
self.model = None
|
|
||||||
self.direction_head = None
|
|
||||||
self.graph_builder = ShapeGraphBuilder()
|
|
||||||
self.device = device
|
|
||||||
self.use_gnn = use_gnn and _TORCH_GEOMETRIC_AVAILABLE
|
|
||||||
|
|
||||||
if model_path and self.use_gnn:
|
|
||||||
self._load_model(model_path)
|
|
||||||
|
|
||||||
def _load_model(self, model_path: str):
|
|
||||||
"""加载训练好的 GNN 模型"""
|
|
||||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
|
||||||
logger.warning("PyTorch Geometric 不可用,无法加载 GNN 模型")
|
|
||||||
return
|
|
||||||
|
|
||||||
try:
|
|
||||||
checkpoint = torch.load(model_path, map_location=self.device)
|
|
||||||
self.model = PartingSurfaceGNN(
|
|
||||||
input_dim=checkpoint.get("input_dim", 7),
|
|
||||||
hidden_dim=checkpoint.get("hidden_dim", 64),
|
|
||||||
)
|
|
||||||
self.model.load_state_dict(checkpoint["model_state_dict"])
|
|
||||||
self.model.to(self.device)
|
|
||||||
self.model.eval()
|
|
||||||
|
|
||||||
if "direction_head_state_dict" in checkpoint:
|
|
||||||
self.direction_head = PartingDirectionHead(
|
|
||||||
hidden_dim=checkpoint.get("hidden_dim", 64)
|
|
||||||
)
|
|
||||||
self.direction_head.load_state_dict(checkpoint["direction_head_state_dict"])
|
|
||||||
self.direction_head.to(self.device)
|
|
||||||
self.direction_head.eval()
|
|
||||||
|
|
||||||
logger.info(f"GNN 模型加载成功: {model_path}")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"GNN 模型加载失败: {e}")
|
|
||||||
self.model = None
|
|
||||||
|
|
||||||
def detect(self, product_shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
检测最优分型面
|
|
||||||
|
|
||||||
Args:
|
|
||||||
product_shape: OpenCASCADE 形状对象
|
|
||||||
analysis: 几何分析结果
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
{
|
|
||||||
"origin": [x, y, z],
|
|
||||||
"normal": [nx, ny, nz],
|
|
||||||
"confidence": float,
|
|
||||||
"parting_line": [...],
|
|
||||||
"method": "gnn" | "geometric"
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
if self.use_gnn and self.model is not None:
|
|
||||||
result = self._detect_with_gnn(product_shape, analysis)
|
|
||||||
if result is not None:
|
|
||||||
return result
|
|
||||||
|
|
||||||
return self._detect_with_geometry(product_shape, analysis)
|
|
||||||
|
|
||||||
def _detect_with_gnn(self, shape: TopoDS_Shape, analysis: Dict) -> Optional[Dict]:
|
|
||||||
"""使用 GNN 模型检测分型面"""
|
|
||||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
graph_data = self.graph_builder.build_graph(shape)
|
|
||||||
if graph_data is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
node_features = torch.tensor(
|
|
||||||
graph_data["node_features"], dtype=torch.float32
|
|
||||||
).to(self.device)
|
|
||||||
edge_index = torch.tensor(
|
|
||||||
graph_data["edge_index"], dtype=torch.long
|
|
||||||
).to(self.device)
|
|
||||||
|
|
||||||
data = Data(x=node_features, edge_index=edge_index)
|
|
||||||
|
|
||||||
with torch.no_grad():
|
|
||||||
face_probs = self.model(data)
|
|
||||||
|
|
||||||
if self.direction_head is not None:
|
|
||||||
batch = torch.zeros(
|
|
||||||
data.num_nodes, dtype=torch.long, device=self.device
|
|
||||||
)
|
|
||||||
direction = self.direction_head(data.x, batch)
|
|
||||||
normal = direction.cpu().numpy().tolist()
|
|
||||||
else:
|
|
||||||
normal = [0, 0, 1]
|
|
||||||
|
|
||||||
parting_face_mask = face_probs.cpu().numpy() > 0.5
|
|
||||||
confidence = float(face_probs.mean().cpu().numpy())
|
|
||||||
|
|
||||||
bbox = analysis.get("bounding_box", {})
|
|
||||||
center = bbox.get("center", [0, 0, 0])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"origin": center,
|
|
||||||
"normal": normal,
|
|
||||||
"confidence": confidence,
|
|
||||||
"method": "gnn",
|
|
||||||
"face_probabilities": face_probs.cpu().numpy().tolist(),
|
|
||||||
"parting_face_count": int(parting_face_mask.sum()),
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"GNN 检测失败,回退到几何方法: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
def _detect_with_geometry(self, shape: TopoDS_Shape, analysis: Dict) -> Dict:
|
|
||||||
"""几何方法回退:基于法向量统计的分型面检测"""
|
|
||||||
try:
|
|
||||||
graph_data = self.graph_builder.build_graph(shape)
|
|
||||||
if graph_data is not None:
|
|
||||||
node_features = graph_data["node_features"]
|
|
||||||
normals = node_features[:, :3]
|
|
||||||
areas = node_features[:, 3]
|
|
||||||
|
|
||||||
total_area = areas.sum()
|
|
||||||
if total_area > 0:
|
|
||||||
weights = areas / total_area
|
|
||||||
weighted_normal = np.sum(normals * weights[:, np.newaxis], axis=0)
|
|
||||||
else:
|
|
||||||
weighted_normal = np.mean(normals, axis=0)
|
|
||||||
|
|
||||||
length = np.linalg.norm(weighted_normal)
|
|
||||||
if length > 0.001:
|
|
||||||
weighted_normal /= length
|
|
||||||
else:
|
|
||||||
weighted_normal = np.array([0, 0, 1])
|
|
||||||
|
|
||||||
dot_products = np.abs(np.dot(normals, weighted_normal))
|
|
||||||
confidence = float(np.mean(dot_products))
|
|
||||||
|
|
||||||
bbox = analysis.get("bounding_box", {})
|
|
||||||
center = bbox.get("center", [0, 0, 0])
|
|
||||||
|
|
||||||
return {
|
|
||||||
"origin": center,
|
|
||||||
"normal": weighted_normal.tolist(),
|
|
||||||
"confidence": confidence,
|
|
||||||
"method": "geometric",
|
|
||||||
}
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"几何方法检测失败: {e}")
|
|
||||||
|
|
||||||
bbox = analysis.get("bounding_box", {})
|
|
||||||
center = bbox.get("center", [0, 0, 0])
|
|
||||||
return {
|
|
||||||
"origin": center,
|
|
||||||
"normal": [0, 0, 1],
|
|
||||||
"confidence": 0.5,
|
|
||||||
"method": "fallback",
|
|
||||||
}
|
|
||||||
|
|
||||||
def collect_training_sample(self, shape: TopoDS_Shape, analysis: Dict,
|
|
||||||
ground_truth_normal: List[float],
|
|
||||||
ground_truth_origin: List[float]) -> Optional[Dict]:
|
|
||||||
"""
|
|
||||||
收集训练样本
|
|
||||||
|
|
||||||
Args:
|
|
||||||
shape: OCC 形状
|
|
||||||
analysis: 几何分析
|
|
||||||
ground_truth_normal: 人工标注的分型方向
|
|
||||||
ground_truth_origin: 人工标注的分型面原点
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
可序列化的训练样本
|
|
||||||
"""
|
|
||||||
graph_data = self.graph_builder.build_graph(shape)
|
|
||||||
if graph_data is None:
|
|
||||||
return None
|
|
||||||
|
|
||||||
return {
|
|
||||||
"node_features": graph_data["node_features"].tolist(),
|
|
||||||
"edge_index": graph_data["edge_index"].tolist(),
|
|
||||||
"edge_features": graph_data["edge_features"].tolist(),
|
|
||||||
"label_normal": ground_truth_normal,
|
|
||||||
"label_origin": ground_truth_origin,
|
|
||||||
"bounding_box": analysis.get("bounding_box", {}),
|
|
||||||
}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def create_model(input_dim: int = 7, hidden_dim: int = 64,
|
|
||||||
num_layers: int = 3) -> Optional[Any]:
|
|
||||||
"""创建新的 GNN 模型实例"""
|
|
||||||
if not _TORCH_GEOMETRIC_AVAILABLE:
|
|
||||||
logger.warning("PyTorch Geometric 不可用,无法创建模型")
|
|
||||||
return None
|
|
||||||
return PartingSurfaceGNN(
|
|
||||||
input_dim=input_dim,
|
|
||||||
hidden_dim=hidden_dim,
|
|
||||||
num_layers=num_layers,
|
|
||||||
)
|
|
||||||
@@ -462,37 +462,6 @@ class AluminumFoamMoldGenerator(BaseMoldGenerator):
|
|||||||
"bounds": metadata["bounds"],
|
"bounds": metadata["bounds"],
|
||||||
}
|
}
|
||||||
|
|
||||||
def _create_parting_surface_from_ai(self, ai_result: Dict, analysis: Dict,
|
|
||||||
shape: Optional[TopoDS_Shape] = None) -> Dict:
|
|
||||||
"""从 AI 结果创建分型面"""
|
|
||||||
origin = ai_result.get("origin", [0, 0, 0])
|
|
||||||
normal = ai_result.get("normal", [0, 0, 1])
|
|
||||||
|
|
||||||
parting_plane = gp_Pln(
|
|
||||||
gp_Pnt(origin[0], origin[1], origin[2]),
|
|
||||||
gp_Dir(normal[0], normal[1], normal[2])
|
|
||||||
)
|
|
||||||
|
|
||||||
try:
|
|
||||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
|
||||||
except Exception:
|
|
||||||
parting_plane = gp_Pln(gp_Pnt(0, 0, 0), gp_Dir(0, 0, 1))
|
|
||||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
|
||||||
|
|
||||||
if shape is not None:
|
|
||||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
|
||||||
else:
|
|
||||||
parting_line = []
|
|
||||||
|
|
||||||
return {
|
|
||||||
"primary_surface": parting_surface,
|
|
||||||
"primary_line": parting_line,
|
|
||||||
"primary_direction": normal,
|
|
||||||
"confidence": ai_result.get("confidence", 0.8),
|
|
||||||
"additional_surfaces": [],
|
|
||||||
"surface_count": 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# ==================== 辅助方法 ====================
|
# ==================== 辅助方法 ====================
|
||||||
|
|
||||||
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
|
def _calculate_mold_size(self, analysis: Dict) -> Dict[str, float]:
|
||||||
|
|||||||
@@ -33,14 +33,6 @@ class BaseMoldGenerator:
|
|||||||
self.draft_angle = draft_angle
|
self.draft_angle = draft_angle
|
||||||
self.material_density = material_density
|
self.material_density = material_density
|
||||||
|
|
||||||
self.ai_parting_detector: Optional[Any] = None
|
|
||||||
self.ai_draft_analyzer: Optional[Any] = None
|
|
||||||
|
|
||||||
def set_ai_model(self, parting_detector: Any = None, draft_analyzer: Any = None):
|
|
||||||
self.ai_parting_detector = parting_detector
|
|
||||||
self.ai_draft_analyzer = draft_analyzer
|
|
||||||
logger.info("AI 模型接口已设置")
|
|
||||||
|
|
||||||
def _apply_shrinkage_compensation(self, shape: TopoDS_Shape) -> TopoDS_Shape:
|
def _apply_shrinkage_compensation(self, shape: TopoDS_Shape) -> TopoDS_Shape:
|
||||||
scale_factor = 1.0 + self.shrinkage_rate
|
scale_factor = 1.0 + self.shrinkage_rate
|
||||||
trsf = gp_Trsf()
|
trsf = gp_Trsf()
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"""特征检测器注册表
|
||||||
|
|
||||||
|
特征检测器通过 FeatureDetectorRegistry 注册,GeometryAnalyzer 遍历注册表执行,
|
||||||
|
不再硬编码检测器列表。新增检测器只需 register 一个 (name, fn, requires_shape)。
|
||||||
|
"""
|
||||||
|
from typing import Callable, List, Dict, Any, Optional, Tuple
|
||||||
|
|
||||||
|
|
||||||
|
class FeatureDetectorRegistry:
|
||||||
|
"""特征检测器注册表
|
||||||
|
|
||||||
|
每个检测器是一个 (name, fn, requires_shape) 条目:
|
||||||
|
- fn: callable(geometry_data, shape) -> List[Dict],返回检测到的特征列表
|
||||||
|
- requires_shape: True 表示仅当 shape 非 None 时才执行(如曲率/圆角检测需 OCC Shape)
|
||||||
|
|
||||||
|
串行执行(OCC 非线程安全),单个检测器失败不影响其他。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._entries: List[Tuple[str, Callable, bool]] = []
|
||||||
|
|
||||||
|
def register(self, name: str, fn: Callable, requires_shape: bool = False) -> None:
|
||||||
|
self._entries.append((name, fn, requires_shape))
|
||||||
|
|
||||||
|
def detect_all(self, geometry_data: Dict[str, Any], shape: Optional[Any]) -> List[Dict[str, Any]]:
|
||||||
|
features: List[Dict[str, Any]] = []
|
||||||
|
for _name, fn, requires_shape in self._entries:
|
||||||
|
if requires_shape and shape is None:
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
features.extend(fn(geometry_data, shape))
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return features
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
from typing import Dict, List, Any, Optional
|
from typing import Dict, List, Any, Optional
|
||||||
import math
|
import math
|
||||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from OCC.Core.TopoDS import TopoDS_Shape
|
from OCC.Core.TopoDS import TopoDS_Shape
|
||||||
from shared.models.schemas import (
|
from shared.models.schemas import (
|
||||||
@@ -10,6 +9,7 @@ from shared.models.schemas import (
|
|||||||
)
|
)
|
||||||
from shared.utils.logger import get_logger
|
from shared.utils.logger import get_logger
|
||||||
from moldinsight.services.material_service import MaterialService
|
from moldinsight.services.material_service import MaterialService
|
||||||
|
from moldinsight.core.feature_detector_registry import FeatureDetectorRegistry
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
@@ -32,6 +32,15 @@ class GeometryAnalyzer:
|
|||||||
"H13_Steel": {"thermal_conductivity": 25, "hardness": "HRC48", "cost": "high"}
|
"H13_Steel": {"thermal_conductivity": 25, "hardness": "HRC48", "cost": "high"}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# 特征检测器注册表 - 新增检测器只需在此 register
|
||||||
|
self._feature_detectors = FeatureDetectorRegistry()
|
||||||
|
self._feature_detectors.register("wall", lambda gd, s: self._detect_wall_features(gd, s))
|
||||||
|
self._feature_detectors.register("rib", lambda gd, s: self._detect_rib_features(gd, s))
|
||||||
|
self._feature_detectors.register("boss", lambda gd, s: self._detect_boss_features(gd, s))
|
||||||
|
self._feature_detectors.register("draft", lambda gd, s: self._analyze_draft_angles(gd, s))
|
||||||
|
self._feature_detectors.register("curvature", lambda gd, s: self._detect_curvature_features(s), requires_shape=True)
|
||||||
|
self._feature_detectors.register("fillet", lambda gd, s: self._detect_fillet_features(s), requires_shape=True)
|
||||||
|
|
||||||
def analyze_mold_design(self, geometry_data: Dict[str, Any],
|
def analyze_mold_design(self, geometry_data: Dict[str, Any],
|
||||||
product_material: str = "ABS",
|
product_material: str = "ABS",
|
||||||
mold_material: str = "Aluminum",
|
mold_material: str = "Aluminum",
|
||||||
@@ -69,26 +78,11 @@ class GeometryAnalyzer:
|
|||||||
|
|
||||||
def _detect_features(self, geometry_data: Dict[str, Any],
|
def _detect_features(self, geometry_data: Dict[str, Any],
|
||||||
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
|
shape: Optional[TopoDS_Shape] = None) -> List[Dict[str, Any]]:
|
||||||
"""检测模具特征 — 独立检测并行执行"""
|
"""检测模具特征 - 串行执行(OCC 非线程安全)
|
||||||
features: List[Dict[str, Any]] = []
|
|
||||||
|
|
||||||
with ThreadPoolExecutor(max_workers=1, thread_name_prefix="feat") as pool:
|
|
||||||
futures = {
|
|
||||||
pool.submit(self._detect_wall_features, geometry_data, shape): "wall",
|
|
||||||
pool.submit(self._detect_rib_features, geometry_data, shape): "rib",
|
|
||||||
pool.submit(self._detect_boss_features, geometry_data, shape): "boss",
|
|
||||||
pool.submit(self._analyze_draft_angles, geometry_data, shape): "draft",
|
|
||||||
}
|
|
||||||
if shape is not None:
|
|
||||||
futures[pool.submit(self._detect_curvature_features, shape)] = "curvature"
|
|
||||||
futures[pool.submit(self._detect_fillet_features, shape)] = "fillet"
|
|
||||||
|
|
||||||
for future in as_completed(futures):
|
|
||||||
try:
|
|
||||||
features.extend(future.result())
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
检测器通过 FeatureDetectorRegistry 注册,新增检测器只需在 __init__ 中 register。
|
||||||
|
"""
|
||||||
|
features = self._feature_detectors.detect_all(geometry_data, shape)
|
||||||
logger.info(f"检测到 {len(features)} 个特征")
|
logger.info(f"检测到 {len(features)} 个特征")
|
||||||
return features
|
return features
|
||||||
|
|
||||||
|
|||||||
@@ -214,21 +214,6 @@ class MoldCavityGenerator(BaseMoldGenerator):
|
|||||||
|
|
||||||
def _detect_primary_parting(self, shape: TopoDS_Shape, analysis: Dict) -> Dict[str, Any]:
|
def _detect_primary_parting(self, shape: TopoDS_Shape, analysis: Dict) -> Dict[str, Any]:
|
||||||
"""检测主分型面(AI优先 → 几何法向量 → 简化回退)"""
|
"""检测主分型面(AI优先 → 几何法向量 → 简化回退)"""
|
||||||
if self.ai_parting_detector is not None:
|
|
||||||
try:
|
|
||||||
ai_result = self.ai_parting_detector.detect(shape, analysis)
|
|
||||||
if ai_result is not None:
|
|
||||||
surface, line = self._create_parting_surface_from_ai(ai_result, analysis, shape)
|
|
||||||
return {
|
|
||||||
"surface": surface,
|
|
||||||
"line": line,
|
|
||||||
"direction": ai_result.get("normal", [0, 0, 1]),
|
|
||||||
"method": ai_result.get("method", "ai"),
|
|
||||||
"confidence": ai_result.get("confidence", 0.8),
|
|
||||||
}
|
|
||||||
except Exception as e:
|
|
||||||
logger.warning(f"AI 分型面检测失败: {e}")
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
normal_dir = self._analyze_face_normals(shape)
|
normal_dir = self._analyze_face_normals(shape)
|
||||||
parting_plane = self._create_optimal_parting_plane(shape, analysis, normal_dir)
|
parting_plane = self._create_optimal_parting_plane(shape, analysis, normal_dir)
|
||||||
@@ -364,40 +349,6 @@ class MoldCavityGenerator(BaseMoldGenerator):
|
|||||||
|
|
||||||
return parting_surface, parting_line
|
return parting_surface, parting_line
|
||||||
|
|
||||||
def _create_parting_surface_from_ai(self, ai_result: Dict,
|
|
||||||
analysis: Dict, shape: Optional[TopoDS_Shape] = None) -> Tuple[TopoDS_Face, List]:
|
|
||||||
"""
|
|
||||||
从 AI 模型结果创建分型面(预留接口)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
ai_result: AI 模型输出,应包含:
|
|
||||||
- origin: [x, y, z] 平面原点
|
|
||||||
- normal: [nx, ny, nz] 法向量
|
|
||||||
analysis: 几何分析结果
|
|
||||||
shape: 产品形状(用于计算分型线)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
(parting_surface, parting_line)
|
|
||||||
"""
|
|
||||||
origin = ai_result.get("origin", [0, 0, 0])
|
|
||||||
normal = ai_result.get("normal", [0, 0, 1])
|
|
||||||
|
|
||||||
parting_plane = gp_Pln(
|
|
||||||
gp_Pnt(origin[0], origin[1], origin[2]),
|
|
||||||
gp_Dir(normal[0], normal[1], normal[2])
|
|
||||||
)
|
|
||||||
parting_surface = BRepBuilderAPI_MakeFace(parting_plane).Face()
|
|
||||||
|
|
||||||
if "parting_line" in ai_result:
|
|
||||||
parting_line = ai_result["parting_line"]
|
|
||||||
elif shape is not None:
|
|
||||||
parting_line = self._calculate_parting_line(shape, parting_surface)
|
|
||||||
else:
|
|
||||||
parting_line = []
|
|
||||||
|
|
||||||
logger.info(f"从 AI 结果创建分型面:原点={origin}, 法向量={normal}")
|
|
||||||
return parting_surface, parting_line
|
|
||||||
|
|
||||||
def _extract_parting_surface_geometry(self, surface: TopoDS_Face) -> Dict[str, Any]:
|
def _extract_parting_surface_geometry(self, surface: TopoDS_Face) -> Dict[str, Any]:
|
||||||
"""提取分型面几何数据"""
|
"""提取分型面几何数据"""
|
||||||
metadata = self._extract_plane_metadata(surface)
|
metadata = self._extract_plane_metadata(surface)
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
"""模具生成器注册表
|
||||||
|
|
||||||
|
按 mold_type 注册生成器实例,MultiSchemeMoldPlanner 通过 mold_type 查询,
|
||||||
|
不再硬编码 if-else 选择生成器。新增模具类型只需 `register` 一个新生成器,无需改 planner。
|
||||||
|
"""
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from moldinsight.core.base_mold_generator import BaseMoldGenerator
|
||||||
|
from moldinsight.core.mold_generator import MoldCavityGenerator
|
||||||
|
from moldinsight.core.aluminum_foam_mold import AluminumFoamMoldGenerator
|
||||||
|
|
||||||
|
|
||||||
|
class MoldGeneratorRegistry:
|
||||||
|
"""模具生成器注册表"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
self._generators: Dict[str, BaseMoldGenerator] = {}
|
||||||
|
|
||||||
|
def register(self, mold_type: str, generator: BaseMoldGenerator) -> None:
|
||||||
|
"""注册一个模具生成器"""
|
||||||
|
self._generators[mold_type] = generator
|
||||||
|
|
||||||
|
def get_by_type(self, mold_type: str) -> BaseMoldGenerator:
|
||||||
|
"""按 mold_type 获取生成器,未注册则抛错"""
|
||||||
|
gen = self._generators.get(mold_type)
|
||||||
|
if gen is None:
|
||||||
|
raise ValueError(
|
||||||
|
f"未注册的模具类型: {mold_type},已注册: {list(self._generators.keys())}"
|
||||||
|
)
|
||||||
|
return gen
|
||||||
|
|
||||||
|
def list_types(self):
|
||||||
|
return list(self._generators.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# 全局单例,注册默认生成器
|
||||||
|
mold_generator_registry = MoldGeneratorRegistry()
|
||||||
|
mold_generator_registry.register("injection", MoldCavityGenerator(shrinkage_rate=0.005))
|
||||||
|
mold_generator_registry.register(
|
||||||
|
"aluminum_foam",
|
||||||
|
AluminumFoamMoldGenerator(shrinkage_rate=0.015, draft_angle=3.0),
|
||||||
|
)
|
||||||
@@ -8,8 +8,7 @@ from OCC.Core.TopAbs import TopAbs_FACE
|
|||||||
from OCC.Core.TopExp import TopExp_Explorer
|
from OCC.Core.TopExp import TopExp_Explorer
|
||||||
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
|
from OCC.Core.TopoDS import TopoDS_Face, TopoDS_Shape, topods
|
||||||
|
|
||||||
from moldinsight.core.mold_generator import MoldCavityGenerator
|
from moldinsight.core.mold_generator_registry import mold_generator_registry
|
||||||
from moldinsight.core.aluminum_foam_mold import AluminumFoamMoldGenerator
|
|
||||||
from moldinsight.core.parting_candidate_generator import PartingCandidateGenerator
|
from moldinsight.core.parting_candidate_generator import PartingCandidateGenerator
|
||||||
from moldinsight.core.parting_scheme_scorer import PartingSchemeScorer
|
from moldinsight.core.parting_scheme_scorer import PartingSchemeScorer
|
||||||
from shared.utils.logger import get_logger
|
from shared.utils.logger import get_logger
|
||||||
@@ -23,11 +22,6 @@ class MultiSchemeMoldPlanner:
|
|||||||
def __init__(self):
|
def __init__(self):
|
||||||
self.candidate_generator = PartingCandidateGenerator()
|
self.candidate_generator = PartingCandidateGenerator()
|
||||||
self.scheme_scorer = PartingSchemeScorer()
|
self.scheme_scorer = PartingSchemeScorer()
|
||||||
self.mold_generator = MoldCavityGenerator(shrinkage_rate=0.005)
|
|
||||||
self.aluminum_foam_generator = AluminumFoamMoldGenerator(
|
|
||||||
shrinkage_rate=0.015,
|
|
||||||
draft_angle=3.0,
|
|
||||||
)
|
|
||||||
|
|
||||||
def generate_plan(
|
def generate_plan(
|
||||||
self,
|
self,
|
||||||
@@ -37,7 +31,7 @@ class MultiSchemeMoldPlanner:
|
|||||||
max_schemes: int = 3,
|
max_schemes: int = 3,
|
||||||
process_params: Optional[Dict[str, Any]] = None,
|
process_params: Optional[Dict[str, Any]] = None,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
generator = self.aluminum_foam_generator if is_foam_material else self.mold_generator
|
generator = mold_generator_registry.get_by_type("aluminum_foam" if is_foam_material else "injection")
|
||||||
generator.set_material(material["name"])
|
generator.set_material(material["name"])
|
||||||
self._apply_process_params(generator, material, process_params)
|
self._apply_process_params(generator, material, process_params)
|
||||||
|
|
||||||
|
|||||||
@@ -179,6 +179,33 @@ _PARTING_USER = """请评估以下候选分模方向并推荐最优方案:
|
|||||||
请综合评估制造可行性、成本和风险,给出推荐。"""
|
请综合评估制造可行性、成本和风险,给出推荐。"""
|
||||||
|
|
||||||
|
|
||||||
|
_COST_ESTIMATE_SYSTEM = """你是一位资深模具报价工程师,擅长根据产品几何与模具设计方案估算模具造价与单件成本。
|
||||||
|
|
||||||
|
要求:
|
||||||
|
1. 使用中文,金额用人民币(¥)
|
||||||
|
2. 基于给定数据合理估算,数据不足时给出区间并标注假设
|
||||||
|
3. 综合考虑:模具材料、加工复杂度(滑块/斜顶/镶件)、型腔数、产品材料用量、成型周期
|
||||||
|
|
||||||
|
严格输出 JSON,不要输出其他内容。JSON 格式:
|
||||||
|
{
|
||||||
|
"mold_cost": {
|
||||||
|
"material": "¥XX(P20 钢,约 XX kg)",
|
||||||
|
"machining": "¥XX(含 CNC/EDM/线切割)",
|
||||||
|
"complexity_factor": "1.2(含 X 个滑块/斜顶)",
|
||||||
|
"subtotal": "¥XX"
|
||||||
|
},
|
||||||
|
"part_cost": {
|
||||||
|
"material": "¥XX(ABS,约 XX g)",
|
||||||
|
"cycle_time": "30 s",
|
||||||
|
"cost_per_part": "¥XX"
|
||||||
|
},
|
||||||
|
"total_mold_cost": "¥XX",
|
||||||
|
"cost_per_part": "¥XX",
|
||||||
|
"confidence": 0.7,
|
||||||
|
"assumptions": ["假设模具寿命 50 万模次", "假设..."]
|
||||||
|
}"""
|
||||||
|
|
||||||
|
|
||||||
class LLMService:
|
class LLMService:
|
||||||
"""LLM 增强分析服务(单例)"""
|
"""LLM 增强分析服务(单例)"""
|
||||||
|
|
||||||
@@ -308,6 +335,61 @@ class LLMService:
|
|||||||
logger.warning("LLM 分型推荐失败(不影响主流程): %s", e)
|
logger.warning("LLM 分型推荐失败(不影响主流程): %s", e)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def estimate_cost(
|
||||||
|
self,
|
||||||
|
analysis_result: Dict[str, Any],
|
||||||
|
detailed_cavity_json: Optional[Dict[str, Any]] = None,
|
||||||
|
) -> Optional[Dict[str, Any]]:
|
||||||
|
"""估算模具造价与单件成本 (结构化 JSON)"""
|
||||||
|
if not self._enabled:
|
||||||
|
return None
|
||||||
|
try:
|
||||||
|
prompt = self._build_cost_estimate_prompt(analysis_result, detailed_cavity_json)
|
||||||
|
response = await self._chat(
|
||||||
|
_COST_ESTIMATE_SYSTEM,
|
||||||
|
prompt,
|
||||||
|
min(self._max_tokens, 1200),
|
||||||
|
expect_json=True,
|
||||||
|
)
|
||||||
|
if not response:
|
||||||
|
return None
|
||||||
|
result = self._parse_json_response(response)
|
||||||
|
if result:
|
||||||
|
logger.info("LLM 成本估算生成成功: total=%s", result.get("total_mold_cost"))
|
||||||
|
return result
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("LLM 成本估算失败(不影响主流程): %s", e)
|
||||||
|
return None
|
||||||
|
|
||||||
|
def _build_cost_estimate_prompt(self, analysis_result: Dict[str, Any], detailed_cavity_json: Optional[Dict[str, Any]]) -> str:
|
||||||
|
geometry_data = analysis_result.get("geometry_data", {}) or (detailed_cavity_json or {}).get("geometry_data", {})
|
||||||
|
volume = geometry_data.get("volume", 0) or 0
|
||||||
|
bbox = geometry_data.get("bounding_box", {}) or {}
|
||||||
|
dims = bbox.get("dimensions", [0, 0, 0])
|
||||||
|
meta = (detailed_cavity_json or {}).get("metadata", {}) or {}
|
||||||
|
material = meta.get("selected_material") or analysis_result.get("material") or "ABS"
|
||||||
|
schemes = (detailed_cavity_json or {}).get("candidate_schemes", []) or []
|
||||||
|
best = schemes[0] if schemes else {}
|
||||||
|
cd = best.get("cavity_data", {}) if isinstance(best, dict) else {}
|
||||||
|
mfg = cd.get("manufacturing_info", {}) if isinstance(cd, dict) else {}
|
||||||
|
features = analysis_result.get("detected_features", []) or []
|
||||||
|
complexity_hints = [
|
||||||
|
f.get("description", f.get("feature_type", ""))
|
||||||
|
for f in features
|
||||||
|
if f.get("feature_type") in ("undercut", "side_action", "insert")
|
||||||
|
]
|
||||||
|
return (
|
||||||
|
f"产品材料:{material}\n"
|
||||||
|
f"体积:{float(volume):.1f} mm³\n"
|
||||||
|
f"边界框尺寸(长×宽×高):{float(dims[0]):.1f} × {float(dims[1]):.1f} × {float(dims[2]):.1f} mm\n"
|
||||||
|
f"预估锁模力:{mfg.get('estimated_clamping_force', '未知')}\n"
|
||||||
|
f"预估模具尺寸:{json.dumps(mfg.get('estimated_mold_size', {}), ensure_ascii=False)}\n"
|
||||||
|
f"预估成型周期:{mfg.get('estimated_cycle_time', '未知')}\n"
|
||||||
|
f"型腔数:{best.get('cavity_count', 1) if isinstance(best, dict) else 1}\n"
|
||||||
|
f"模具结构:{best.get('mold_structure_type', '未知') if isinstance(best, dict) else '未知'}\n"
|
||||||
|
f"复杂度线索:{', '.join(complexity_hints) if complexity_hints else '无明显倒扣/滑块'}\n"
|
||||||
|
)
|
||||||
|
|
||||||
def _build_design_report_prompt(self, analysis_result, detailed_cavity_json) -> str:
|
def _build_design_report_prompt(self, analysis_result, detailed_cavity_json) -> str:
|
||||||
features = json.dumps(analysis_result.get("detected_features", []), ensure_ascii=False, indent=2)
|
features = json.dumps(analysis_result.get("detected_features", []), ensure_ascii=False, indent=2)
|
||||||
if len(features) > 4000:
|
if len(features) > 4000:
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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("已取消迁移")
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -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
|
||||||
@@ -125,6 +125,8 @@ class STPFile(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, index=True)
|
id = Column(Integer, primary_key=True, index=True)
|
||||||
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
user_id = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||||
|
# 关联进销存成品(P2-1:分析结果可一键创建为成品并回写)
|
||||||
|
product_id = Column(Integer, ForeignKey("products.id"), nullable=True, index=True)
|
||||||
|
|
||||||
# 对象存储信息
|
# 对象存储信息
|
||||||
object_key = Column(String(500), nullable=False, index=True) # MinIO对象键
|
object_key = Column(String(500), nullable=False, index=True) # MinIO对象键
|
||||||
@@ -160,6 +162,7 @@ class STPFile(Base):
|
|||||||
|
|
||||||
# 关联关系
|
# 关联关系
|
||||||
user = relationship("User", back_populates="stp_files")
|
user = relationship("User", back_populates="stp_files")
|
||||||
|
product = relationship("Product") # P2-1: 关联的进销存成品
|
||||||
geometry_data = relationship("GeometryData", back_populates="stp_file", uselist=False)
|
geometry_data = relationship("GeometryData", back_populates="stp_file", uselist=False)
|
||||||
mesh_data = relationship("MeshData", back_populates="stp_file", uselist=False)
|
mesh_data = relationship("MeshData", back_populates="stp_file", uselist=False)
|
||||||
mold_cavity_data = relationship("MoldCavityData", back_populates="stp_file", uselist=False)
|
mold_cavity_data = relationship("MoldCavityData", back_populates="stp_file", uselist=False)
|
||||||
@@ -662,6 +665,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 +744,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 +867,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)
|
||||||
|
|||||||
Reference in New Issue
Block a user