feat: 开发采集、采集箱和商品编辑功能

This commit is contained in:
Joey
2026-08-15 22:17:26 +08:00
parent c61d1a3154
commit 36357843d0
130 changed files with 18005 additions and 12 deletions
+53
View File
@@ -0,0 +1,53 @@
"""Alembic 迁移环境。URL 从 server/config/settings.py 读取,支持 autogenerate。"""
from __future__ import annotations
import sys
from pathlib import Path
from alembic import context
from sqlalchemy import create_engine, pool
# 让 `from config import ...` / `from db import ...` / `import models` 可解析
SERVER_DIR = Path(__file__).resolve().parents[1]
if str(SERVER_DIR) not in sys.path:
sys.path.insert(0, str(SERVER_DIR))
from config import get_settings # noqa: E402
from db import Base # noqa: E402
import models # noqa: E402,F401 确保所有模型注册到 Base.metadata
config = context.config
target_metadata = Base.metadata
def _sync_url(url: str) -> str:
"""异步 URL → 同步 URL(迁移用同步引擎跑更稳)。"""
return url.replace("+aiosqlite", "").replace("+asyncpg", "")
def run_migrations_offline() -> None:
context.configure(
url=_sync_url(get_settings().database_url),
target_metadata=target_metadata,
literal_binds=True,
dialect_opts={"paramstyle": "named"},
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online() -> None:
connectable = create_engine(
_sync_url(get_settings().database_url),
poolclass=pool.NullPool,
)
with connectable.connect() as connection:
context.configure(connection=connection, target_metadata=target_metadata)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
+25
View File
@@ -0,0 +1,25 @@
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade() -> None:
${upgrades if upgrades else "pass"}
def downgrade() -> None:
${downgrades if downgrades else "pass"}
View File
@@ -0,0 +1,203 @@
"""initial v2 schema
Revision ID: 51715d16e5c3
Revises:
Create Date: 2026-08-15 10:06:57.793304
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = '51715d16e5c3'
down_revision = None
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('attribute_values',
sa.Column('id', sa.BigInteger(), nullable=False),
sa.Column('attribute_id', sa.BigInteger(), nullable=False),
sa.Column('description_category_id', sa.BigInteger(), nullable=False),
sa.Column('type_id', sa.BigInteger(), nullable=False),
sa.Column('value', sa.String(length=512), nullable=False),
sa.Column('picture', sa.Text(), nullable=False),
sa.Column('info', sa.Text(), nullable=False),
sa.Column('lang', sa.String(length=8), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id', 'attribute_id', 'description_category_id', 'type_id')
)
op.create_table('category_attributes',
sa.Column('description_category_id', sa.BigInteger(), nullable=False),
sa.Column('type_id', sa.BigInteger(), nullable=False),
sa.Column('attribute_id', sa.BigInteger(), nullable=False),
sa.Column('name', sa.String(length=255), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.Column('type', sa.String(length=32), nullable=False),
sa.Column('dictionary_id', sa.BigInteger(), nullable=False),
sa.Column('group_id', sa.BigInteger(), nullable=True),
sa.Column('group_name', sa.String(length=255), nullable=False),
sa.Column('is_required', sa.Boolean(), nullable=False),
sa.Column('is_aspect', sa.Boolean(), nullable=False),
sa.Column('is_collection', sa.Boolean(), nullable=False),
sa.Column('max_value_count', sa.Integer(), nullable=False),
sa.Column('attribute_complex_id', sa.BigInteger(), nullable=True),
sa.Column('complex_is_collection', sa.Boolean(), nullable=False),
sa.Column('category_dependent', sa.Boolean(), nullable=False),
sa.Column('lang', sa.String(length=8), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('description_category_id', 'type_id', 'attribute_id')
)
op.create_table('category_tree',
sa.Column('description_category_id', sa.BigInteger(), nullable=False),
sa.Column('parent_id', sa.BigInteger(), nullable=True),
sa.Column('category_name', sa.String(length=255), nullable=False),
sa.Column('type_id', sa.BigInteger(), nullable=True),
sa.Column('type_name', sa.String(length=255), nullable=False),
sa.Column('disabled', sa.Boolean(), nullable=False),
sa.Column('level', sa.Integer(), nullable=False),
sa.Column('lang', sa.String(length=8), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('description_category_id')
)
op.create_index(op.f('ix_category_tree_parent_id'), 'category_tree', ['parent_id'], unique=False)
op.create_table('products',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=True),
sa.Column('stage', sa.Enum('collected', 'editing', 'ready', 'publishing', 'published', 'failed', 'archived', name='stage', native_enum=False, length=16), nullable=False),
sa.Column('source_platform', sa.String(length=16), nullable=True),
sa.Column('source_item_id', sa.String(length=64), nullable=True),
sa.Column('source_url', sa.Text(), nullable=True),
sa.Column('offer_id', sa.String(length=255), nullable=False),
sa.Column('ozon_product_id', sa.BigInteger(), nullable=True),
sa.Column('ozon_sku', sa.BigInteger(), nullable=True),
sa.Column('name', sa.Text(), nullable=False),
sa.Column('description', sa.Text(), nullable=False),
sa.Column('description_category_id', sa.BigInteger(), nullable=True),
sa.Column('type_id', sa.BigInteger(), nullable=True),
sa.Column('price', sa.Float(), nullable=True),
sa.Column('old_price', sa.Float(), nullable=True),
sa.Column('currency_code', sa.String(length=3), server_default='RUB', nullable=False),
sa.Column('vat', sa.String(length=8), server_default='0', nullable=False),
sa.Column('depth', sa.Float(), nullable=True),
sa.Column('width', sa.Float(), nullable=True),
sa.Column('height', sa.Float(), nullable=True),
sa.Column('dimension_unit', sa.String(length=4), server_default='mm', nullable=False),
sa.Column('weight', sa.Float(), nullable=True),
sa.Column('weight_unit', sa.String(length=4), server_default='g', nullable=False),
sa.Column('barcode', sa.String(length=64), nullable=True),
sa.Column('images', sa.JSON(), nullable=True),
sa.Column('primary_image', sa.Text(), nullable=True),
sa.Column('images360', sa.JSON(), nullable=True),
sa.Column('color_image', sa.Text(), nullable=True),
sa.Column('pdf_list', sa.JSON(), nullable=True),
sa.Column('promotions', sa.JSON(), nullable=True),
sa.Column('attributes', sa.JSON(), nullable=True),
sa.Column('complex_attributes', sa.JSON(), nullable=True),
sa.Column('raw', sa.JSON(), nullable=True),
sa.Column('pricing', sa.JSON(), nullable=True),
sa.Column('copy', sa.JSON(), nullable=True),
sa.Column('fx_rate', sa.Float(), nullable=True),
sa.Column('published_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('asset_counts', sa.JSON(), nullable=True),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_products_offer_id'), 'products', ['offer_id'], unique=False)
op.create_index(op.f('ix_products_ozon_product_id'), 'products', ['ozon_product_id'], unique=False)
op.create_index(op.f('ix_products_source_item_id'), 'products', ['source_item_id'], unique=False)
op.create_index(op.f('ix_products_stage'), 'products', ['stage'], unique=False)
op.create_index(op.f('ix_products_updated_at'), 'products', ['updated_at'], unique=False)
op.create_table('shops',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('user_id', sa.Uuid(), nullable=True),
sa.Column('name', sa.String(length=128), nullable=False),
sa.Column('client_id_enc', sa.String(length=1024), nullable=False),
sa.Column('api_key_enc', sa.String(length=1024), nullable=False),
sa.Column('currency_code', sa.String(length=3), server_default='RUB', nullable=False),
sa.Column('status', sa.Enum('active', 'invalid', 'disabled', name='shopstatus', native_enum=False, length=16), nullable=False),
sa.Column('last_checked_at', sa.DateTime(timezone=True), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id')
)
op.create_table('users',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('username', sa.String(length=64), nullable=False),
sa.Column('password_hash', sa.String(length=255), nullable=False),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('username')
)
op.create_table('product_assets',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('product_id', sa.Uuid(), nullable=False),
sa.Column('group_key', sa.String(length=16), nullable=False),
sa.Column('variant_name', sa.String(length=128), nullable=True),
sa.Column('sort_order', sa.Integer(), nullable=False),
sa.Column('type', sa.String(length=8), nullable=False),
sa.Column('source_url', sa.Text(), nullable=False),
sa.Column('stored_url', sa.Text(), nullable=True),
sa.Column('status', sa.Enum('pending', 'downloading', 'uploaded', 'failed', name='assetstatus', native_enum=False, length=16), nullable=False),
sa.Column('dedupe_key', sa.String(length=512), nullable=True),
sa.Column('width', sa.Integer(), nullable=True),
sa.Column('height', sa.Integer(), nullable=True),
sa.Column('error', sa.Text(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_product_assets_dedupe_key'), 'product_assets', ['dedupe_key'], unique=False)
op.create_index(op.f('ix_product_assets_product_id'), 'product_assets', ['product_id'], unique=False)
op.create_index(op.f('ix_product_assets_status'), 'product_assets', ['status'], unique=False)
op.create_table('publish_tasks',
sa.Column('id', sa.Uuid(), nullable=False),
sa.Column('product_id', sa.Uuid(), nullable=False),
sa.Column('shop_id', sa.Uuid(), nullable=False),
sa.Column('ozon_task_id', sa.BigInteger(), nullable=True),
sa.Column('status', sa.Enum('pending', 'processing', 'moderation', 'imported', 'failed', name='publishstatus', native_enum=False, length=16), nullable=False),
sa.Column('request_payload', sa.JSON(), nullable=True),
sa.Column('response', sa.JSON(), nullable=True),
sa.Column('errors', sa.JSON(), nullable=True),
sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False),
sa.Column('completed_at', sa.DateTime(timezone=True), nullable=True),
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ondelete='CASCADE'),
sa.ForeignKeyConstraint(['shop_id'], ['shops.id'], ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id')
)
op.create_index(op.f('ix_publish_tasks_ozon_task_id'), 'publish_tasks', ['ozon_task_id'], unique=False)
op.create_index(op.f('ix_publish_tasks_product_id'), 'publish_tasks', ['product_id'], unique=False)
op.create_index(op.f('ix_publish_tasks_shop_id'), 'publish_tasks', ['shop_id'], unique=False)
op.create_index(op.f('ix_publish_tasks_status'), 'publish_tasks', ['status'], unique=False)
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_index(op.f('ix_publish_tasks_status'), table_name='publish_tasks')
op.drop_index(op.f('ix_publish_tasks_shop_id'), table_name='publish_tasks')
op.drop_index(op.f('ix_publish_tasks_product_id'), table_name='publish_tasks')
op.drop_index(op.f('ix_publish_tasks_ozon_task_id'), table_name='publish_tasks')
op.drop_table('publish_tasks')
op.drop_index(op.f('ix_product_assets_status'), table_name='product_assets')
op.drop_index(op.f('ix_product_assets_product_id'), table_name='product_assets')
op.drop_index(op.f('ix_product_assets_dedupe_key'), table_name='product_assets')
op.drop_table('product_assets')
op.drop_table('users')
op.drop_table('shops')
op.drop_index(op.f('ix_products_updated_at'), table_name='products')
op.drop_index(op.f('ix_products_stage'), table_name='products')
op.drop_index(op.f('ix_products_source_item_id'), table_name='products')
op.drop_index(op.f('ix_products_ozon_product_id'), table_name='products')
op.drop_index(op.f('ix_products_offer_id'), table_name='products')
op.drop_table('products')
op.drop_index(op.f('ix_category_tree_parent_id'), table_name='category_tree')
op.drop_table('category_tree')
op.drop_table('category_attributes')
op.drop_table('attribute_values')
# ### end Alembic commands ###
@@ -0,0 +1,29 @@
"""add shop_id to products
Revision ID: 658b0503f71c
Revises: 51715d16e5c3
Create Date: 2026-08-15 14:30:34.342589
"""
from __future__ import annotations
from alembic import op
import sqlalchemy as sa
revision = '658b0503f71c'
down_revision = '51715d16e5c3'
branch_labels = None
depends_on = None
def upgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('products', sa.Column('shop_id', sa.Uuid(), nullable=True))
# ### end Alembic commands ###
def downgrade() -> None:
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('products', 'shop_id')
# ### end Alembic commands ###