chore: fork from IRIS track 供 LICA 部门独立运行
- 复制来源: /home/yueli/track @ 192c8ee (feature/ai-audit-update) - 组织隔离目标: LICA - 端口规划: 前端 8030 / 后端 8031 / 数据库 8032 - 已排除 deploy.sh、deploy_full.sh、docker-compose.prod.yml(IRIS 生产发布脚本) - 已排除工作区未提交改动,取干净的 192c8ee 状态
This commit is contained in:
1
backend/alembic/README
Normal file
1
backend/alembic/README
Normal file
@ -0,0 +1 @@
|
||||
Generic single-database configuration.
|
||||
59
backend/alembic/env.py
Normal file
59
backend/alembic/env.py
Normal file
@ -0,0 +1,59 @@
|
||||
"""Alembic 迁移环境配置 — 异步引擎 + 自动加载模型"""
|
||||
import asyncio
|
||||
from logging.config import fileConfig
|
||||
|
||||
from alembic import context
|
||||
from sqlalchemy.ext.asyncio import create_async_engine
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models import Base # 自动发现所有 SQLAlchemy 模型
|
||||
|
||||
# Alembic Config 对象
|
||||
config = context.config
|
||||
|
||||
# 日志配置
|
||||
if config.config_file_name is not None:
|
||||
fileConfig(config.config_file_name)
|
||||
|
||||
# 将 DATABASE_URL 同步到 alembic 配置中(覆盖 alembic.ini 中的占位值)
|
||||
config.set_main_option("sqlalchemy.url", settings.DATABASE_URL)
|
||||
|
||||
# 自动生成迁移时需要的 models 元数据
|
||||
target_metadata = Base.metadata
|
||||
|
||||
|
||||
def run_migrations_offline() -> None:
|
||||
"""离线模式:生成 SQL 脚本而非直接执行"""
|
||||
url = config.get_main_option("sqlalchemy.url")
|
||||
context.configure(
|
||||
url=url,
|
||||
target_metadata=target_metadata,
|
||||
literal_binds=True,
|
||||
dialect_opts={"paramstyle": "named"},
|
||||
)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
def do_run_migrations(connection):
|
||||
"""在线模式:通过数据库连接执行迁移"""
|
||||
context.configure(connection=connection, target_metadata=target_metadata)
|
||||
with context.begin_transaction():
|
||||
context.run_migrations()
|
||||
|
||||
|
||||
async def run_migrations_online() -> None:
|
||||
"""在线模式:异步引擎"""
|
||||
connectable = create_async_engine(
|
||||
config.get_main_option("sqlalchemy.url"),
|
||||
echo=settings.DEBUG,
|
||||
)
|
||||
async with connectable.connect() as connection:
|
||||
await connection.run_sync(do_run_migrations)
|
||||
await connectable.dispose()
|
||||
|
||||
|
||||
if context.is_offline_mode():
|
||||
run_migrations_offline()
|
||||
else:
|
||||
asyncio.run(run_migrations_online())
|
||||
28
backend/alembic/script.py.mako
Normal file
28
backend/alembic/script.py.mako
Normal file
@ -0,0 +1,28 @@
|
||||
"""${message}
|
||||
|
||||
Revision ID: ${up_revision}
|
||||
Revises: ${down_revision | comma,n}
|
||||
Create Date: ${create_date}
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
${imports if imports else ""}
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = ${repr(up_revision)}
|
||||
down_revision: Union[str, Sequence[str], None] = ${repr(down_revision)}
|
||||
branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)}
|
||||
depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)}
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
${upgrades if upgrades else "pass"}
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
${downgrades if downgrades else "pass"}
|
||||
@ -0,0 +1,85 @@
|
||||
"""init: production_orders, products, tasks, task_logs
|
||||
|
||||
Revision ID: 80f8cb64544a
|
||||
Revises:
|
||||
Create Date: 2026-08-04 10:08:19.670158
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = '80f8cb64544a'
|
||||
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('production_orders',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('order_no', sa.String(length=64), nullable=False, comment='订单编号'),
|
||||
sa.Column('customer_info', sa.String(length=500), nullable=True, comment='客户信息'),
|
||||
sa.Column('status', sa.String(length=50), nullable=False, comment='订单状态'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_production_orders_order_no'), 'production_orders', ['order_no'], unique=True)
|
||||
op.create_table('products',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('serial_number', sa.String(length=16), nullable=False, comment='产品序列号(16位)'),
|
||||
sa.Column('order_id', sa.UUID(), nullable=False, comment='所属订单ID'),
|
||||
sa.Column('material_id', sa.String(length=64), nullable=True, comment='物料ID(逻辑外键→老系统)'),
|
||||
sa.Column('parent_product_id', sa.UUID(), nullable=True, comment='父产品ID'),
|
||||
sa.Column('status', sa.String(length=50), nullable=False, comment='产品状态'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.ForeignKeyConstraint(['order_id'], ['production_orders.id'], ),
|
||||
sa.ForeignKeyConstraint(['parent_product_id'], ['products.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_products_serial_number'), 'products', ['serial_number'], unique=True)
|
||||
op.create_table('tasks',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('product_id', sa.UUID(), nullable=False, comment='所属产品ID'),
|
||||
sa.Column('parent_task_id', sa.UUID(), nullable=True, comment='父任务ID'),
|
||||
sa.Column('task_name', sa.String(length=200), nullable=False, comment='任务名称'),
|
||||
sa.Column('assignee_id', sa.String(length=64), nullable=True, comment='负责人ID(逻辑外键→老系统)'),
|
||||
sa.Column('status', sa.String(length=50), nullable=False, comment='任务状态'),
|
||||
sa.Column('notify_parent_on_complete', sa.Boolean(), nullable=False, comment='完成后是否通知父任务'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.ForeignKeyConstraint(['parent_task_id'], ['tasks.id'], ),
|
||||
sa.ForeignKeyConstraint(['product_id'], ['products.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_tasks_parent_task_id'), 'tasks', ['parent_task_id'], unique=False)
|
||||
op.create_table('task_logs',
|
||||
sa.Column('id', sa.UUID(), nullable=False),
|
||||
sa.Column('task_id', sa.UUID(), nullable=False, comment='所属任务ID'),
|
||||
sa.Column('operator_id', sa.String(length=64), nullable=True, comment='操作人ID(逻辑外键→老系统)'),
|
||||
sa.Column('action_type', sa.String(length=50), nullable=False, comment='操作类型'),
|
||||
sa.Column('remark', sa.Text(), nullable=True, comment='备注'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='创建时间'),
|
||||
sa.ForeignKeyConstraint(['task_id'], ['tasks.id'], ),
|
||||
sa.PrimaryKeyConstraint('id')
|
||||
)
|
||||
op.create_index(op.f('ix_task_logs_task_id'), 'task_logs', ['task_id'], unique=False)
|
||||
# ### end Alembic commands ###
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# ### commands auto generated by Alembic - please adjust! ###
|
||||
op.drop_index(op.f('ix_task_logs_task_id'), table_name='task_logs')
|
||||
op.drop_table('task_logs')
|
||||
op.drop_index(op.f('ix_tasks_parent_task_id'), table_name='tasks')
|
||||
op.drop_table('tasks')
|
||||
op.drop_index(op.f('ix_products_serial_number'), table_name='products')
|
||||
op.drop_table('products')
|
||||
op.drop_index(op.f('ix_production_orders_order_no'), table_name='production_orders')
|
||||
op.drop_table('production_orders')
|
||||
# ### end Alembic commands ###
|
||||
@ -0,0 +1,102 @@
|
||||
"""add task rework fields and product current_location
|
||||
|
||||
Revision ID: a1b2c3d4e5f6
|
||||
Revises: 80f8cb64544a
|
||||
Create Date: 2026-08-04 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a1b2c3d4e5f6'
|
||||
down_revision: Union[str, Sequence[str], None] = '80f8cb64544a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
"""Upgrade schema."""
|
||||
# === 数据迁移:将已有任务状态从小写转为大写 ===
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'PENDING' WHERE lower(status) = 'pending';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'WIP' WHERE lower(status) = 'wip';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'COMPLETED' WHERE lower(status) = 'completed';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'REJECTED' WHERE lower(status) = 'rejected';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'ARCHIVED' WHERE lower(status) = 'archived';
|
||||
""")
|
||||
|
||||
# === tasks 表新增字段 ===
|
||||
op.add_column('tasks', sa.Column(
|
||||
'received_at',
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment='操作员确认接收时间',
|
||||
))
|
||||
op.add_column('tasks', sa.Column(
|
||||
'completed_at',
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=True,
|
||||
comment='任务完工转交时间',
|
||||
))
|
||||
op.add_column('tasks', sa.Column(
|
||||
'reject_reason',
|
||||
sa.String(length=500),
|
||||
nullable=True,
|
||||
comment='驳回原因',
|
||||
))
|
||||
op.add_column('tasks', sa.Column(
|
||||
'is_rework',
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default=sa.text('false'),
|
||||
comment='是否为返工任务',
|
||||
))
|
||||
|
||||
# === products 表新增字段 ===
|
||||
op.add_column('products', sa.Column(
|
||||
'current_location_id',
|
||||
sa.String(length=64),
|
||||
nullable=True,
|
||||
comment="当前持有者ID 或 'virtual_warehouse'(仓库)",
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
"""Downgrade schema."""
|
||||
# === products 表移除字段 ===
|
||||
op.drop_column('products', 'current_location_id')
|
||||
|
||||
# === tasks 表移除字段 ===
|
||||
op.drop_column('tasks', 'is_rework')
|
||||
op.drop_column('tasks', 'reject_reason')
|
||||
op.drop_column('tasks', 'completed_at')
|
||||
op.drop_column('tasks', 'received_at')
|
||||
|
||||
# === 数据回迁:将任务状态从大写转回小写 ===
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'pending' WHERE status = 'PENDING';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'wip' WHERE status = 'WIP';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'completed' WHERE status = 'COMPLETED';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'rejected' WHERE status = 'REJECTED';
|
||||
""")
|
||||
op.execute("""
|
||||
UPDATE tasks SET status = 'archived' WHERE status = 'ARCHIVED';
|
||||
""")
|
||||
24
backend/alembic/versions/a7b8c9d0e1f2_add_task_type.py
Normal file
24
backend/alembic/versions/a7b8c9d0e1f2_add_task_type.py
Normal file
@ -0,0 +1,24 @@
|
||||
"""add_task_type
|
||||
|
||||
Revision ID: a7b8c9d0e1f2
|
||||
Revises: f6a7b8c9d0e1
|
||||
Create Date: 2026-08-06
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "a7b8c9d0e1f2"
|
||||
down_revision: Union[str, None] = "f6a7b8c9d0e1"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("tasks", sa.Column("task_type", sa.String(20), nullable=True, comment="派生类型: TRANSFER/SPAWN/RECOVERY"))
|
||||
# 刷老数据:parent_task_id 非空的默认为 TRANSFER
|
||||
op.execute("UPDATE tasks SET task_type = 'TRANSFER' WHERE parent_task_id IS NOT NULL AND task_type IS NULL")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("tasks", "task_type")
|
||||
@ -0,0 +1,44 @@
|
||||
"""product hex counter + material fields + optional order
|
||||
|
||||
Revision ID: b2c3d4e5f6a7
|
||||
Revises: a1b2c3d4e5f6
|
||||
Create Date: 2026-08-04 15:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = 'b2c3d4e5f6a7'
|
||||
down_revision: Union[str, Sequence[str], None] = 'a1b2c3d4e5f6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# === 16进制计数器 Sequence ===
|
||||
op.execute("CREATE SEQUENCE IF NOT EXISTS product_hex_counter START 1;")
|
||||
|
||||
# === products 表: order_id → 可选 ===
|
||||
op.alter_column('products', 'order_id',
|
||||
existing_type=sa.UUID(),
|
||||
nullable=True,
|
||||
existing_comment='所属订单ID(可选)')
|
||||
|
||||
# === products 表: 新增 external_serial ===
|
||||
op.add_column('products', sa.Column(
|
||||
'external_serial',
|
||||
sa.String(length=64),
|
||||
nullable=True,
|
||||
comment='用户自定义产品序列号(可选)',
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('products', 'external_serial')
|
||||
op.alter_column('products', 'order_id',
|
||||
existing_type=sa.UUID(),
|
||||
nullable=False)
|
||||
op.execute("DROP SEQUENCE IF EXISTS product_hex_counter;")
|
||||
32
backend/alembic/versions/b8c9d0e1f2a3_add_notifications.py
Normal file
32
backend/alembic/versions/b8c9d0e1f2a3_add_notifications.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""add_notifications
|
||||
|
||||
Revision ID: b8c9d0e1f2a3
|
||||
Revises: a7b8c9d0e1f2
|
||||
Create Date: 2026-08-07
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "b8c9d0e1f2a3"
|
||||
down_revision: Union[str, None] = "a7b8c9d0e1f2"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"notifications",
|
||||
sa.Column("id", sa.UUID(), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("user_id", sa.String(64), nullable=False, index=True, comment="接收人ID(逻辑外键→老系统)"),
|
||||
sa.Column("title", sa.String(200), nullable=False, comment="通知标题"),
|
||||
sa.Column("content", sa.Text(), nullable=False, comment="通知内容详情"),
|
||||
sa.Column("type", sa.String(20), nullable=False, comment="通知类型: TRANSFER(转交派发) | REJECT(驳回)"),
|
||||
sa.Column("task_id", sa.UUID(), sa.ForeignKey("tasks.id", ondelete="SET NULL"), nullable=True, comment="关联任务ID"),
|
||||
sa.Column("is_read", sa.Boolean(), server_default=sa.text("false"), comment="是否已读"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now(), comment="创建时间"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("notifications")
|
||||
@ -0,0 +1,35 @@
|
||||
"""add product material snapshot fields
|
||||
|
||||
Revision ID: c3d4e5f6a7b8
|
||||
Revises: b2c3d4e5f6a7
|
||||
Create Date: 2026-08-05 10:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = 'c3d4e5f6a7b8'
|
||||
down_revision: Union[str, Sequence[str], None] = 'b2c3d4e5f6a7'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('products', sa.Column(
|
||||
'material_name', sa.String(length=255), nullable=True, comment='物料名称快照'))
|
||||
op.add_column('products', sa.Column(
|
||||
'spec_model', sa.String(length=255), nullable=True, comment='规格型号快照'))
|
||||
op.add_column('products', sa.Column(
|
||||
'category', sa.String(length=255), nullable=True, comment='物料分类快照'))
|
||||
op.add_column('products', sa.Column(
|
||||
'material_type', sa.String(length=100), nullable=True, comment='物料类型快照'))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('products', 'material_type')
|
||||
op.drop_column('products', 'category')
|
||||
op.drop_column('products', 'spec_model')
|
||||
op.drop_column('products', 'material_name')
|
||||
36
backend/alembic/versions/c9d0e1f2a3b4_add_app_versions.py
Normal file
36
backend/alembic/versions/c9d0e1f2a3b4_add_app_versions.py
Normal file
@ -0,0 +1,36 @@
|
||||
"""add_app_versions
|
||||
|
||||
Revision ID: c9d0e1f2a3b4
|
||||
Revises: b8c9d0e1f2a3
|
||||
Create Date: 2026-08-07
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "c9d0e1f2a3b4"
|
||||
down_revision: Union[str, None] = "b8c9d0e1f2a3"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"app_versions",
|
||||
sa.Column("id", sa.UUID(), primary_key=True, server_default=sa.text("gen_random_uuid()")),
|
||||
sa.Column("version", sa.String(20), nullable=False, unique=True, comment="版本号"),
|
||||
sa.Column("version_code", sa.Integer(), nullable=False, server_default="100", comment="数字版本号"),
|
||||
sa.Column("wgt_url", sa.String(500), nullable=False, comment="WGT下载地址"),
|
||||
sa.Column("description", sa.Text(), nullable=True, comment="更新说明"),
|
||||
sa.Column("is_active", sa.Boolean(), server_default=sa.text("true"), comment="是否启用"),
|
||||
sa.Column("created_at", sa.DateTime(timezone=True), server_default=sa.func.now()),
|
||||
)
|
||||
# 插入初始版本记录
|
||||
op.execute(
|
||||
"INSERT INTO app_versions (version, version_code, wgt_url, description, is_active) "
|
||||
"VALUES ('T1.0.1', 101, '', '初始版本', true)"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("app_versions")
|
||||
@ -0,0 +1,30 @@
|
||||
"""add product overall_status
|
||||
|
||||
Revision ID: d4e5f6a7b8c9
|
||||
Revises: c3d4e5f6a7b8
|
||||
Create Date: 2026-08-05 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = 'd4e5f6a7b8c9'
|
||||
down_revision: Union[str, Sequence[str], None] = 'c3d4e5f6a7b8'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column('products', sa.Column(
|
||||
'overall_status',
|
||||
sa.String(length=20),
|
||||
nullable=True,
|
||||
comment='宏观状态: 备货/生产/测试/维修/在库',
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column('products', 'overall_status')
|
||||
35
backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py
Normal file
35
backend/alembic/versions/e5f6a7b8c9d0_add_task_records.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""add task_records table
|
||||
|
||||
Revision ID: e5f6a7b8c9d0
|
||||
Revises: d4e5f6a7b8c9
|
||||
Create Date: 2026-08-06 10:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
|
||||
revision: str = 'e5f6a7b8c9d0'
|
||||
down_revision: Union[str, Sequence[str], None] = 'd4e5f6a7b8c9'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table('task_records',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
|
||||
sa.Column('task_id', UUID(as_uuid=True), sa.ForeignKey('tasks.id'), nullable=False, comment='所属任务ID'),
|
||||
sa.Column('remark', sa.String(length=2000), nullable=True, comment='备注文本'),
|
||||
sa.Column('images', sa.String(length=4000), nullable=True, comment='图片URL列表(JSON字符串)'),
|
||||
sa.Column('created_at', sa.DateTime(timezone=True), nullable=False, comment='记录时间'),
|
||||
sa.PrimaryKeyConstraint('id'),
|
||||
)
|
||||
op.create_index(op.f('ix_task_records_task_id'), 'task_records', ['task_id'], unique=False)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index(op.f('ix_task_records_task_id'), table_name='task_records')
|
||||
op.drop_table('task_records')
|
||||
23
backend/alembic/versions/f6a7b8c9d0e1_add_task_remark.py
Normal file
23
backend/alembic/versions/f6a7b8c9d0e1_add_task_remark.py
Normal file
@ -0,0 +1,23 @@
|
||||
"""add_task_remark
|
||||
|
||||
Revision ID: f6a7b8c9d0e1
|
||||
Revises: e5f6a7b8c9d0
|
||||
Create Date: 2026-08-05
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "f6a7b8c9d0e1"
|
||||
down_revision: Union[str, None] = "e5f6a7b8c9d0"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column("tasks", sa.Column("remark", sa.String(2000), nullable=True, comment="任务初始描述/交接备注"))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("tasks", "remark")
|
||||
@ -0,0 +1,33 @@
|
||||
"""add_product_messages
|
||||
|
||||
Revision ID: g1h2i3j4k5l6
|
||||
Revises: c9d0e1f2a3b4
|
||||
Create Date: 2026-08-10
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = "g1h2i3j4k5l6"
|
||||
down_revision: Union[str, None] = "c9d0e1f2a3b4"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"product_messages",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("product_id", postgresql.UUID(as_uuid=True),
|
||||
sa.ForeignKey("products.id", ondelete="CASCADE"),
|
||||
index=True, nullable=False),
|
||||
sa.Column("operator_id", sa.String(50), nullable=False),
|
||||
sa.Column("content", sa.Text, nullable=False),
|
||||
sa.Column("created_at", sa.DateTime, nullable=True),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("product_messages")
|
||||
32
backend/alembic/versions/h1h2h3h4h5h6_add_holidays.py
Normal file
32
backend/alembic/versions/h1h2h3h4h5h6_add_holidays.py
Normal file
@ -0,0 +1,32 @@
|
||||
"""add holidays table
|
||||
|
||||
Revision ID: h1h2h3h4h5h6
|
||||
Revises: g1h2i3j4k5l6
|
||||
Create Date: 2026-08-28 12:00:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
revision: str = 'h1h2h3h4h5h6'
|
||||
down_revision: Union[str, Sequence[str], None] = 'g1h2i3j4k5l6'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
'holidays',
|
||||
sa.Column('id', sa.Integer(), autoincrement=True, primary_key=True),
|
||||
sa.Column('day', sa.Date(), nullable=False, comment='放假日期'),
|
||||
sa.Column('name', sa.String(length=100), nullable=True, comment='放假说明(如国庆节)'),
|
||||
)
|
||||
op.create_index('ix_holidays_day', 'holidays', ['day'], unique=True)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index('ix_holidays_day', table_name='holidays')
|
||||
op.drop_table('holidays')
|
||||
@ -0,0 +1,45 @@
|
||||
"""add_product_lifecycle_phase
|
||||
|
||||
Revision ID: i1j2k3l4m5n6
|
||||
Revises: h1h2h3h4h5h6
|
||||
Create Date: 2026-09-14
|
||||
|
||||
产品生命周期阶段(lifecycle_phase)
|
||||
--------------------------------
|
||||
用于区分「生产阶段的测试(发货测试)」与「出库后再次返厂的售后维修」。
|
||||
|
||||
存量数据一律回填 'PRODUCTION'(历史产品视为从未出库回流);
|
||||
新数据由 SQLAlchemy 端 default 提供,双保险。
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "i1j2k3l4m5n6"
|
||||
down_revision: Union[str, None] = "h1h2h3h4h5h6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"products",
|
||||
sa.Column(
|
||||
"lifecycle_phase",
|
||||
sa.String(20),
|
||||
nullable=False,
|
||||
server_default="PRODUCTION",
|
||||
comment="生命周期阶段: PRODUCTION(生产制造) | AFTER_SALES(出库后返厂售后)",
|
||||
),
|
||||
)
|
||||
# 存量数据回填:历史产品从未出库回流,统一视为生产阶段
|
||||
op.execute("UPDATE products SET lifecycle_phase = 'PRODUCTION' WHERE lifecycle_phase IS NULL")
|
||||
# 加索引:售后设备排查(按阶段筛选)会高频用到
|
||||
op.create_index(
|
||||
"ix_products_lifecycle_phase", "products", ["lifecycle_phase"], unique=False,
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_products_lifecycle_phase", table_name="products")
|
||||
op.drop_column("products", "lifecycle_phase")
|
||||
81
backend/alembic/versions/j1k2l3m4n5o6_add_audit_logs.py
Normal file
81
backend/alembic/versions/j1k2l3m4n5o6_add_audit_logs.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""add_audit_logs
|
||||
|
||||
Revision ID: j1k2l3m4n5o6
|
||||
Revises: i1j2k3l4m5n6
|
||||
Create Date: 2026-09-21
|
||||
|
||||
操作审计日志表(audit_logs)
|
||||
--------------------------
|
||||
新增一张独立的审计表,用于记录 task_logs 覆盖不到的操作:
|
||||
登录、导出、产品增删改、收口、权限/配置变更等与单个任务无关的动作。
|
||||
|
||||
为什么另起一张表而不复用 task_logs:
|
||||
task_logs.task_id 是 NOT NULL 外键,只能挂在任务上,无法表达「张三导出了
|
||||
产品清单」这类动作;且缺少来源 IP / UA / 结果状态等审计必需字段。
|
||||
|
||||
存量数据无需回填(本表从上线时刻开始记录)。
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
revision: str = "j1k2l3m4n5o6"
|
||||
down_revision: Union[str, None] = "i1j2k3l4m5n6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"audit_logs",
|
||||
sa.Column("id", postgresql.UUID(as_uuid=True), primary_key=True, nullable=False),
|
||||
# 操作人
|
||||
sa.Column("user_id", sa.String(64), nullable=True, comment="操作人账号(逻辑外键→MOM)"),
|
||||
sa.Column("display_name", sa.String(100), nullable=True, comment="操作人显示名"),
|
||||
sa.Column("role", sa.String(50), nullable=True, comment="操作时角色快照"),
|
||||
# 业务语义
|
||||
sa.Column("action", sa.String(50), nullable=False, comment="动作"),
|
||||
sa.Column("module", sa.String(50), nullable=False, comment="业务模块"),
|
||||
sa.Column("target_type", sa.String(50), nullable=True),
|
||||
sa.Column("target_id", sa.String(100), nullable=True),
|
||||
sa.Column("target_name", sa.String(200), nullable=True),
|
||||
sa.Column("details", postgresql.JSONB(), nullable=True, comment="变更详情"),
|
||||
# 请求上下文
|
||||
sa.Column("ip_address", sa.String(50), nullable=True),
|
||||
sa.Column("user_agent", sa.String(500), nullable=True),
|
||||
sa.Column("method", sa.String(10), nullable=True),
|
||||
sa.Column("url", sa.String(500), nullable=True),
|
||||
sa.Column("status_code", sa.Integer(), nullable=True),
|
||||
sa.Column("error_message", sa.Text(), nullable=True),
|
||||
# 与结构化日志对账
|
||||
sa.Column("request_id", sa.String(64), nullable=True),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
)
|
||||
|
||||
# 索引:按审计页最常用的检索维度建
|
||||
op.create_index("ix_audit_logs_created_at", "audit_logs", ["created_at"])
|
||||
op.create_index("ix_audit_logs_user_id", "audit_logs", ["user_id"])
|
||||
op.create_index("ix_audit_logs_module", "audit_logs", ["module"])
|
||||
op.create_index("ix_audit_logs_action", "audit_logs", ["action"])
|
||||
op.create_index("ix_audit_logs_target_id", "audit_logs", ["target_id"])
|
||||
op.create_index("ix_audit_logs_request_id", "audit_logs", ["request_id"])
|
||||
# 组合索引:审计页默认「按时间倒序 + 按模块/动作过滤」
|
||||
op.create_index("ix_audit_logs_module_created", "audit_logs", ["module", "created_at"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_audit_logs_module_created", table_name="audit_logs")
|
||||
op.drop_index("ix_audit_logs_request_id", table_name="audit_logs")
|
||||
op.drop_index("ix_audit_logs_target_id", table_name="audit_logs")
|
||||
op.drop_index("ix_audit_logs_action", table_name="audit_logs")
|
||||
op.drop_index("ix_audit_logs_module", table_name="audit_logs")
|
||||
op.drop_index("ix_audit_logs_user_id", table_name="audit_logs")
|
||||
op.drop_index("ix_audit_logs_created_at", table_name="audit_logs")
|
||||
op.drop_table("audit_logs")
|
||||
53
backend/alembic/versions/k1l2m3n4o5p6_add_user_daily_seen.py
Normal file
53
backend/alembic/versions/k1l2m3n4o5p6_add_user_daily_seen.py
Normal file
@ -0,0 +1,53 @@
|
||||
"""add_user_daily_seen
|
||||
|
||||
Revision ID: k1l2m3n4o5p6
|
||||
Revises: j1k2l3m4n5o6
|
||||
Create Date: 2026-09-21
|
||||
|
||||
每日用户活动表(user_daily_seen)
|
||||
--------------------------------
|
||||
一天一人一行,记录当天首次 / 末次活动时刻,供日活报表计算
|
||||
「上线时间 / 下线时间」。
|
||||
|
||||
为什么不复用 audit_logs:
|
||||
· 上线/下线时间不能取登录时间 —— Refresh Token 有效期 7 天,用户不必每天
|
||||
重新登录,「登录次数 0 却操作 35 次」的报表没有意义。
|
||||
· 也不能只取写操作时间 —— 审计中间件只记写操作,普通 GET 不入账,
|
||||
当天只翻看的人会被漏掉。
|
||||
· 更不能把活动写进审计表 —— 「末次活动」是需要不断 UPDATE 的状态,
|
||||
而审计流水必须只增不改;能改的审计记录等于没有审计价值。
|
||||
|
||||
存量数据无需回填:本表从上线时刻开始记录;日活接口对更早的日期会自动
|
||||
回退到审计表的写操作时间去推算。
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
import sqlalchemy as sa
|
||||
from alembic import op
|
||||
|
||||
revision: str = "k1l2m3n4o5p6"
|
||||
down_revision: Union[str, None] = "j1k2l3m4n5o6"
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.create_table(
|
||||
"user_daily_seen",
|
||||
sa.Column("user_id", sa.String(64), primary_key=True,
|
||||
comment="操作人账号(逻辑外键→MOM)"),
|
||||
sa.Column("day", sa.Date(), primary_key=True,
|
||||
comment="北京时间自然日"),
|
||||
sa.Column("first_seen_at", sa.DateTime(timezone=True), nullable=False,
|
||||
comment="当天首次活动时刻"),
|
||||
sa.Column("last_seen_at", sa.DateTime(timezone=True), nullable=False,
|
||||
comment="当天末次活动时刻"),
|
||||
)
|
||||
# 日活查询按日期区间扫,给 day 单独建索引。
|
||||
# (主键是 (user_id, day),前缀是 user_id,按 day 过滤用不上,故需补一条)
|
||||
op.create_index("ix_user_daily_seen_day", "user_daily_seen", ["day"])
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_index("ix_user_daily_seen_day", table_name="user_daily_seen")
|
||||
op.drop_table("user_daily_seen")
|
||||
Reference in New Issue
Block a user