diff --git a/backend/alembic/versions/80f8cb64544a_init_production_orders_products_tasks_.py b/backend/alembic/versions/80f8cb64544a_init_production_orders_products_tasks_.py new file mode 100644 index 0000000..b0c8cd3 --- /dev/null +++ b/backend/alembic/versions/80f8cb64544a_init_production_orders_products_tasks_.py @@ -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 ### diff --git a/backend/app/api/v1/router.py b/backend/app/api/v1/router.py index 4f13dd1..17e9cc1 100644 --- a/backend/app/api/v1/router.py +++ b/backend/app/api/v1/router.py @@ -1,7 +1,15 @@ from fastapi import APIRouter +from app.api.v1.endpoints.products import router as products_router +from app.api.v1.endpoints.tasks import router as tasks_router +from app.api.v1.endpoints.orders import router as orders_router +from app.api.v1.endpoints.dashboard import router as dashboard_router +from app.api.v1.endpoints.auth import router as auth_router + api_router = APIRouter() -# 后续在此注册子路由 -# from app.api.v1 import users, products, orders, ... -# api_router.include_router(users.router, prefix="/users", tags=["用户管理"]) +api_router.include_router(auth_router) +api_router.include_router(dashboard_router) +api_router.include_router(orders_router) +api_router.include_router(products_router) +api_router.include_router(tasks_router) diff --git a/backend/app/core/mom_database.py b/backend/app/core/mom_database.py new file mode 100644 index 0000000..8c0d853 --- /dev/null +++ b/backend/app/core/mom_database.py @@ -0,0 +1,24 @@ +"""MOM 系统数据库 — 只读连接,用于登录验证 sys_user 表""" +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, Session +from sqlalchemy.pool import NullPool + +# MOM 数据库连接(同步引擎,仅用于登录验证) +import os +# Docker 容器内用 host.docker.internal 访问宿主机 +MOM_HOST = os.environ.get("MOM_DB_HOST", "host.docker.internal") +MOM_PORT = os.environ.get("MOM_DB_PORT", "5435") +MOM_DATABASE_URL = f"postgresql://prod_user:StrongPassword123!@{MOM_HOST}:{MOM_PORT}/inventory_system" + +mom_engine = create_engine( + MOM_DATABASE_URL, + echo=False, + poolclass=NullPool, # 登录频率低,不用连接池 +) + +MomSessionLocal = sessionmaker( + mom_engine, + class_=Session, + autocommit=False, + autoflush=False, +) diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py index a28a5f9..a9381da 100644 --- a/backend/app/models/__init__.py +++ b/backend/app/models/__init__.py @@ -4,7 +4,6 @@ from app.models.production_order import ProductionOrder from app.models.product import Product from app.models.task import Task from app.models.task_log import TaskLog - __all__ = [ "Base", "ProductionOrder", diff --git a/backend/requirements.txt b/backend/requirements.txt index 1fbc985..92335fe 100644 --- a/backend/requirements.txt +++ b/backend/requirements.txt @@ -36,3 +36,10 @@ typing_extensions==4.16.0 uvicorn==0.52.1 watchfiles==1.2.0 websockets==17.0.1 + +# QR code generation +qrcode[pil]==8.2 +Pillow==12.3.0 + +# MOM 登录对接 — Werkzeug scrypt 密码验证 +werkzeug==3.0.6