v0.2.0: CRM/ERP 系统升级 - 清理 .gitignore 并移除误提交的 venv/env/db 文件
- 更新 .gitignore:全面覆盖环境变量、数据库、日志、缓存、上传文件 - 移除误跟踪的 server/venv/、crm_data.db、.env 文件 - 新增 server/.env.example 模板 - 新增合同管理、利润核算、AI教练等功能模块 - 新增 Playwright e2e 测试套件 - 前后端多项功能升级和 bug 修复
This commit is contained in:
@@ -0,0 +1,159 @@
|
||||
"""multi-tenant company isolation
|
||||
|
||||
Revision ID: a1b2c3d4e5f6
|
||||
Revises: 03d8dcc2d72a
|
||||
Create Date: 2026-03-18 08:45:00.000000
|
||||
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects import postgresql
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision: str = 'a1b2c3d4e5f6'
|
||||
down_revision: Union[str, Sequence[str], None] = '03d8dcc2d72a'
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
# 默认公司的固定 UUID
|
||||
DEFAULT_COMPANY_ID = 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeee0001'
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Step 1: 创建 sys_companies 公司主体表
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
op.create_table(
|
||||
'sys_companies',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column('name', sa.String(200), nullable=False),
|
||||
sa.Column('code', sa.String(50), unique=True, nullable=False),
|
||||
sa.Column('is_active', sa.Boolean(), server_default=sa.text('true'), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
)
|
||||
|
||||
# Step 2: 插入默认公司
|
||||
op.execute(f"""
|
||||
INSERT INTO sys_companies (id, name, code, is_active)
|
||||
VALUES ('{DEFAULT_COMPANY_ID}', '天津硕博霖', 'SHBL-TJ', true)
|
||||
""")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Step 3: 创建 sys_user_companies 用户-公司关联表
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
op.create_table(
|
||||
'sys_user_companies',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')),
|
||||
sa.Column('user_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('sys_users.id'), nullable=False),
|
||||
sa.Column('company_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('sys_companies.id'), nullable=False),
|
||||
sa.Column('is_default', sa.Boolean(), server_default=sa.text('false'), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.UniqueConstraint('user_id', 'company_id', name='uq_user_company'),
|
||||
)
|
||||
|
||||
# Step 4: 为所有现有用户关联默认公司
|
||||
op.execute(f"""
|
||||
INSERT INTO sys_user_companies (id, user_id, company_id, is_default)
|
||||
SELECT gen_random_uuid(), id, '{DEFAULT_COMPANY_ID}'::uuid, true
|
||||
FROM sys_users
|
||||
WHERE is_deleted = false
|
||||
""")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Step 5: 创建 erp_sku_inventory 分公司库存表
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
op.create_table(
|
||||
'erp_sku_inventory',
|
||||
sa.Column('id', postgresql.UUID(as_uuid=True), primary_key=True, server_default=sa.text('gen_random_uuid()')),
|
||||
sa.Column('sku_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('erp_product_skus.id'), nullable=False),
|
||||
sa.Column('company_id', postgresql.UUID(as_uuid=True), sa.ForeignKey('sys_companies.id'), nullable=False),
|
||||
sa.Column('stock_qty', sa.Numeric(12, 2), server_default=sa.text('0'), nullable=False),
|
||||
sa.Column('warning_threshold', sa.Numeric(12, 2), server_default=sa.text('0'), nullable=False),
|
||||
sa.Column('created_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.Column('updated_at', sa.DateTime(), server_default=sa.text('CURRENT_TIMESTAMP'), nullable=False),
|
||||
sa.UniqueConstraint('sku_id', 'company_id', name='uq_sku_company'),
|
||||
)
|
||||
op.create_index('ix_erp_sku_inventory_company_id', 'erp_sku_inventory', ['company_id'])
|
||||
|
||||
# Step 6: 迁移 erp_product_skus 的库存数据到 erp_sku_inventory
|
||||
op.execute(f"""
|
||||
INSERT INTO erp_sku_inventory (id, sku_id, company_id, stock_qty, warning_threshold)
|
||||
SELECT gen_random_uuid(), id, '{DEFAULT_COMPANY_ID}'::uuid, stock_qty, warning_threshold
|
||||
FROM erp_product_skus
|
||||
WHERE is_deleted = false
|
||||
""")
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Step 7: 为业务表追加 company_id 列(先 nullable → 填数据 → set NOT NULL)
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
tables_with_company_id = [
|
||||
'erp_orders',
|
||||
'erp_inventory_flows',
|
||||
'erp_shipping_records',
|
||||
'fin_invoice_pool',
|
||||
'fin_expense_records',
|
||||
'finance_sales_invoices',
|
||||
'sales_logs',
|
||||
]
|
||||
|
||||
for table in tables_with_company_id:
|
||||
# 添加列(先允许 NULL)
|
||||
op.add_column(table, sa.Column('company_id', postgresql.UUID(as_uuid=True), nullable=True))
|
||||
|
||||
# 填入默认公司 ID
|
||||
op.execute(f"""
|
||||
UPDATE {table} SET company_id = '{DEFAULT_COMPANY_ID}'::uuid WHERE company_id IS NULL
|
||||
""")
|
||||
|
||||
# 设 NOT NULL
|
||||
op.alter_column(table, 'company_id', nullable=False)
|
||||
|
||||
# 创建外键
|
||||
op.create_foreign_key(
|
||||
f'fk_{table}_company_id',
|
||||
table, 'sys_companies',
|
||||
['company_id'], ['id'],
|
||||
)
|
||||
|
||||
# 创建索引
|
||||
op.create_index(f'ix_{table}_company_id', table, ['company_id'])
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
# Step 8: 从 erp_product_skus 删除已迁移的库存字段
|
||||
# ═══════════════════════════════════════════════════════════════
|
||||
op.drop_column('erp_product_skus', 'stock_qty')
|
||||
op.drop_column('erp_product_skus', 'warning_threshold')
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 恢复 erp_product_skus 的库存字段
|
||||
op.add_column('erp_product_skus', sa.Column('stock_qty', sa.Numeric(12, 2), server_default=sa.text('0'), nullable=False))
|
||||
op.add_column('erp_product_skus', sa.Column('warning_threshold', sa.Numeric(12, 2), server_default=sa.text('0'), nullable=False))
|
||||
|
||||
# 从 erp_sku_inventory 回迁默认公司的库存数据
|
||||
op.execute(f"""
|
||||
UPDATE erp_product_skus SET
|
||||
stock_qty = inv.stock_qty,
|
||||
warning_threshold = inv.warning_threshold
|
||||
FROM erp_sku_inventory inv
|
||||
WHERE erp_product_skus.id = inv.sku_id AND inv.company_id = '{DEFAULT_COMPANY_ID}'::uuid
|
||||
""")
|
||||
|
||||
# 删除 company_id 列
|
||||
tables_with_company_id = [
|
||||
'erp_orders', 'erp_inventory_flows', 'erp_shipping_records',
|
||||
'fin_invoice_pool', 'fin_expense_records', 'finance_sales_invoices', 'sales_logs',
|
||||
]
|
||||
for table in tables_with_company_id:
|
||||
op.drop_index(f'ix_{table}_company_id', table_name=table)
|
||||
op.drop_constraint(f'fk_{table}_company_id', table, type_='foreignkey')
|
||||
op.drop_column(table, 'company_id')
|
||||
|
||||
# 删除新建的表
|
||||
op.drop_index('ix_erp_sku_inventory_company_id', table_name='erp_sku_inventory')
|
||||
op.drop_table('erp_sku_inventory')
|
||||
op.drop_table('sys_user_companies')
|
||||
op.drop_table('sys_companies')
|
||||
@@ -0,0 +1,43 @@
|
||||
"""add xinyu lubricant company
|
||||
|
||||
Revision ID: b2c3d4e5f6a7
|
||||
Revises: a1b2c3d4e5f6
|
||||
Create Date: 2026-03-19
|
||||
"""
|
||||
from alembic import op
|
||||
|
||||
revision = "b2c3d4e5f6a7"
|
||||
down_revision = "a1b2c3d4e5f6"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
XINYU_COMPANY_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeee0002"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. 插入第二个公司:新宇润滑油
|
||||
op.execute(f"""
|
||||
INSERT INTO sys_companies (id, name, code, is_active)
|
||||
VALUES ('{XINYU_COMPANY_ID}', '新宇润滑油', 'XY-LUB', true)
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
""")
|
||||
|
||||
# 2. 将所有现有用户关联到新宇润滑油(非默认)
|
||||
op.execute(f"""
|
||||
INSERT INTO sys_user_companies (id, user_id, company_id, is_default)
|
||||
SELECT gen_random_uuid(), id, '{XINYU_COMPANY_ID}'::uuid, false
|
||||
FROM sys_users
|
||||
WHERE id NOT IN (
|
||||
SELECT user_id FROM sys_user_companies
|
||||
WHERE company_id = '{XINYU_COMPANY_ID}'::uuid
|
||||
)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute(f"""
|
||||
DELETE FROM sys_user_companies WHERE company_id = '{XINYU_COMPANY_ID}'::uuid
|
||||
""")
|
||||
op.execute(f"""
|
||||
DELETE FROM sys_companies WHERE id = '{XINYU_COMPANY_ID}'::uuid
|
||||
""")
|
||||
@@ -0,0 +1,82 @@
|
||||
"""sales_logs company_id to involved_company_ids
|
||||
|
||||
Revision ID: c3d4e5f6a7b8
|
||||
Revises: b2c3d4e5f6a7
|
||||
Create Date: 2026-03-19
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, ARRAY
|
||||
|
||||
revision = "c3d4e5f6a7b8"
|
||||
down_revision = "b2c3d4e5f6a7"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
DEFAULT_COMPANY_ID = "aaaaaaaa-bbbb-cccc-dddd-eeeeeeee0001"
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. 添加新列 involved_company_ids (ARRAY UUID)
|
||||
op.add_column(
|
||||
"sales_logs",
|
||||
sa.Column("involved_company_ids", ARRAY(UUID(as_uuid=True)), nullable=True)
|
||||
)
|
||||
|
||||
# 2. 数据迁移:将现有 company_id 转为数组
|
||||
op.execute("""
|
||||
UPDATE sales_logs
|
||||
SET involved_company_ids = ARRAY[company_id]
|
||||
WHERE company_id IS NOT NULL
|
||||
""")
|
||||
|
||||
# 3. 没有 company_id 的行(不太可能但防御性处理)
|
||||
op.execute(f"""
|
||||
UPDATE sales_logs
|
||||
SET involved_company_ids = ARRAY['{DEFAULT_COMPANY_ID}'::uuid]
|
||||
WHERE involved_company_ids IS NULL
|
||||
""")
|
||||
|
||||
# 4. 设置 NOT NULL
|
||||
op.alter_column("sales_logs", "involved_company_ids", nullable=False)
|
||||
|
||||
# 5. 删除旧的 company_id 列及其外键
|
||||
op.drop_constraint(
|
||||
"fk_sales_logs_company_id", "sales_logs", type_="foreignkey"
|
||||
)
|
||||
op.drop_index("ix_sales_logs_company_id", table_name="sales_logs", if_exists=True)
|
||||
op.drop_column("sales_logs", "company_id")
|
||||
|
||||
# 6. 为 involved_company_ids 创建 GIN 索引(支持 ANY/contains 查询)
|
||||
op.create_index(
|
||||
"ix_sales_logs_involved_company_ids",
|
||||
"sales_logs",
|
||||
["involved_company_ids"],
|
||||
postgresql_using="gin"
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# 回滚:重新添加 company_id,从数组取第一个元素
|
||||
op.drop_index("ix_sales_logs_involved_company_ids", table_name="sales_logs")
|
||||
|
||||
op.add_column(
|
||||
"sales_logs",
|
||||
sa.Column("company_id", UUID(as_uuid=True), nullable=True)
|
||||
)
|
||||
|
||||
op.execute("""
|
||||
UPDATE sales_logs
|
||||
SET company_id = involved_company_ids[1]
|
||||
WHERE array_length(involved_company_ids, 1) > 0
|
||||
""")
|
||||
|
||||
op.alter_column("sales_logs", "company_id", nullable=False)
|
||||
op.create_foreign_key(
|
||||
"sales_logs_company_id_fkey",
|
||||
"sales_logs", "sys_companies",
|
||||
["company_id"], ["id"]
|
||||
)
|
||||
op.create_index("ix_sales_logs_company_id", "sales_logs", ["company_id"])
|
||||
|
||||
op.drop_column("sales_logs", "involved_company_ids")
|
||||
@@ -0,0 +1,30 @@
|
||||
"""add billing_info to crm_customers
|
||||
|
||||
Revision ID: d4e5f6a7b8c9
|
||||
Revises: c3d4e5f6a7b8
|
||||
Create Date: 2026-03-27
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import JSONB
|
||||
|
||||
revision = "d4e5f6a7b8c9"
|
||||
down_revision = "c3d4e5f6a7b8"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.add_column(
|
||||
"crm_customers",
|
||||
sa.Column(
|
||||
"billing_info",
|
||||
JSONB,
|
||||
nullable=True,
|
||||
comment="客户开票信息: company_name/tax_id/address/phone/bank_name/bank_account",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("crm_customers", "billing_info")
|
||||
@@ -0,0 +1,113 @@
|
||||
"""Phase B: contract management + order/invoice linkage
|
||||
|
||||
Revision ID: e5f6a7b8c9d0
|
||||
Revises: d4e5f6a7b8c9
|
||||
Create Date: 2026-03-27
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision = "e5f6a7b8c9d0"
|
||||
down_revision = "d4e5f6a7b8c9"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── 1. sys_companies 新增 full_info ──
|
||||
op.add_column(
|
||||
"sys_companies",
|
||||
sa.Column("full_info", JSONB, nullable=True,
|
||||
comment="公司完整信息: full_name/address/phone/bank_name/bank_account/tax_id"),
|
||||
)
|
||||
|
||||
# ── 2. erp_contracts 主表 ──
|
||||
op.create_table(
|
||||
"erp_contracts",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("contract_no", sa.String(30), unique=True, nullable=False),
|
||||
sa.Column("buyer_customer_id", UUID(as_uuid=True), sa.ForeignKey("crm_customers.id"), nullable=False),
|
||||
sa.Column("seller_company_id", UUID(as_uuid=True), sa.ForeignKey("sys_companies.id"), nullable=False),
|
||||
sa.Column("company_id", UUID(as_uuid=True), sa.ForeignKey("sys_companies.id"), nullable=False, index=True),
|
||||
sa.Column("total_amount_excl_tax", sa.Numeric(14, 2), default=0),
|
||||
sa.Column("total_amount_incl_tax", sa.Numeric(14, 2), default=0),
|
||||
sa.Column("total_amount_cn", sa.String(100), nullable=True),
|
||||
sa.Column("payment_terms", sa.String(50), nullable=False, server_default="货到付全款"),
|
||||
sa.Column("shipping_terms", sa.String(50), nullable=False, server_default="买方自提"),
|
||||
sa.Column("status", sa.String(20), nullable=False, server_default="draft"),
|
||||
sa.Column("is_signed", sa.Boolean, default=False, server_default="false"),
|
||||
sa.Column("signed_file_url", sa.String(500), nullable=True),
|
||||
sa.Column("linked_order_id", UUID(as_uuid=True), sa.ForeignKey("erp_orders.id"), nullable=True),
|
||||
sa.Column("salesperson_id", UUID(as_uuid=True), sa.ForeignKey("sys_users.id"), nullable=True),
|
||||
sa.Column("sign_date", sa.Date, nullable=True),
|
||||
sa.Column("remark", sa.Text, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column("is_deleted", sa.Boolean, default=False, server_default="false"),
|
||||
)
|
||||
|
||||
# ── 3. erp_contract_items 明细行 ──
|
||||
op.create_table(
|
||||
"erp_contract_items",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("contract_id", UUID(as_uuid=True), sa.ForeignKey("erp_contracts.id"), nullable=False),
|
||||
sa.Column("sku_id", UUID(as_uuid=True), sa.ForeignKey("erp_product_skus.id"), nullable=False),
|
||||
sa.Column("qty", sa.Numeric(12, 2), nullable=False),
|
||||
sa.Column("unit_price", sa.Numeric(12, 2), nullable=False),
|
||||
sa.Column("sub_total", sa.Numeric(14, 2), nullable=False),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column("updated_at", sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column("is_deleted", sa.Boolean, default=False, server_default="false"),
|
||||
)
|
||||
|
||||
# ── 4. erp_contract_attachments 附件 ──
|
||||
op.create_table(
|
||||
"erp_contract_attachments",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("contract_id", UUID(as_uuid=True), sa.ForeignKey("erp_contracts.id"), nullable=False),
|
||||
sa.Column("file_name", sa.String(200), nullable=False),
|
||||
sa.Column("file_url", sa.String(500), nullable=False),
|
||||
sa.Column("file_type", sa.String(30), nullable=False, server_default="signed_copy"),
|
||||
sa.Column("uploader_id", UUID(as_uuid=True), sa.ForeignKey("sys_users.id"), nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column("is_deleted", sa.Boolean, default=False, server_default="false"),
|
||||
)
|
||||
|
||||
# ── 5. erp_orders 新增 contract_id ──
|
||||
op.add_column(
|
||||
"erp_orders",
|
||||
sa.Column("contract_id", UUID(as_uuid=True),
|
||||
sa.ForeignKey("erp_contracts.id"), nullable=True,
|
||||
comment="来源合同(一键推单后回填)"),
|
||||
)
|
||||
|
||||
# ── 6. finance_sales_invoices 新增 order_id / shipping_record_id / payment_due_date ──
|
||||
op.add_column(
|
||||
"finance_sales_invoices",
|
||||
sa.Column("order_id", UUID(as_uuid=True),
|
||||
sa.ForeignKey("erp_orders.id"), nullable=True,
|
||||
comment="关联订单"),
|
||||
)
|
||||
op.add_column(
|
||||
"finance_sales_invoices",
|
||||
sa.Column("shipping_record_id", UUID(as_uuid=True),
|
||||
sa.ForeignKey("erp_shipping_records.id"), nullable=True,
|
||||
comment="关联发货单"),
|
||||
)
|
||||
op.add_column(
|
||||
"finance_sales_invoices",
|
||||
sa.Column("payment_due_date", sa.Date, nullable=True,
|
||||
comment="回款截止日(根据合同付款条件自动推算)"),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("finance_sales_invoices", "payment_due_date")
|
||||
op.drop_column("finance_sales_invoices", "shipping_record_id")
|
||||
op.drop_column("finance_sales_invoices", "order_id")
|
||||
op.drop_column("erp_orders", "contract_id")
|
||||
op.drop_table("erp_contract_attachments")
|
||||
op.drop_table("erp_contract_items")
|
||||
op.drop_table("erp_contracts")
|
||||
op.drop_column("sys_companies", "full_info")
|
||||
@@ -0,0 +1,57 @@
|
||||
"""Phase C: MWA inventory + profit accounting
|
||||
|
||||
Revision ID: f6a7b8c9d0e1
|
||||
Revises: e5f6a7b8c9d0
|
||||
Create Date: 2026-03-27
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
|
||||
revision = "f6a7b8c9d0e1"
|
||||
down_revision = "e5f6a7b8c9d0"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── 1. erp_sku_inventory 新增 mwa_unit_cost ──
|
||||
op.add_column(
|
||||
"erp_sku_inventory",
|
||||
sa.Column("mwa_unit_cost", sa.Numeric(12, 4), server_default="0",
|
||||
comment="移动加权均价 (Moving Weighted Average)"),
|
||||
)
|
||||
|
||||
# ── 2. erp_inventory_flows 新增 purchase_unit_price + is_special_zero_cost ──
|
||||
op.add_column(
|
||||
"erp_inventory_flows",
|
||||
sa.Column("purchase_unit_price", sa.Numeric(12, 2), server_default="0",
|
||||
comment="入库采购单价"),
|
||||
)
|
||||
op.add_column(
|
||||
"erp_inventory_flows",
|
||||
sa.Column("is_special_zero_cost", sa.Boolean, server_default="false",
|
||||
comment="特殊零元入库标识,不参与 MWA 计算"),
|
||||
)
|
||||
|
||||
# ── 3. erp_order_item_costs 新表 ──
|
||||
op.create_table(
|
||||
"erp_order_item_costs",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("order_item_id", UUID(as_uuid=True),
|
||||
sa.ForeignKey("erp_order_items.id"), nullable=False, unique=True),
|
||||
sa.Column("purchase_unit_price", sa.Numeric(12, 4), nullable=False,
|
||||
comment="MWA 成本快照"),
|
||||
sa.Column("profit_amount", sa.Numeric(14, 2), server_default="0",
|
||||
comment="利润额 = (售价-成本)*数量"),
|
||||
sa.Column("profit_rate", sa.Numeric(5, 4), server_default="0",
|
||||
comment="利润率"),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_table("erp_order_item_costs")
|
||||
op.drop_column("erp_inventory_flows", "is_special_zero_cost")
|
||||
op.drop_column("erp_inventory_flows", "purchase_unit_price")
|
||||
op.drop_column("erp_sku_inventory", "mwa_unit_cost")
|
||||
@@ -0,0 +1,49 @@
|
||||
"""Phase D: AI coaching engine (JSONB fields only, pgvector table deferred)
|
||||
|
||||
Revision ID: a7b8c9d0e1f2
|
||||
Revises: f6a7b8c9d0e1
|
||||
Create Date: 2026-03-27
|
||||
|
||||
Note: kb_obsidian_vectors (pgvector) 表需要先安装 postgresql-16-pgvector 包,
|
||||
安装后手动执行:
|
||||
CREATE EXTENSION IF NOT EXISTS vector;
|
||||
然后运行: alembic upgrade pgvector_head
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision = "a7b8c9d0e1f2"
|
||||
down_revision = "f6a7b8c9d0e1"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# ── 1. sales_logs 新增 ai_coaching_feedback ──
|
||||
op.add_column(
|
||||
"sales_logs",
|
||||
sa.Column("ai_coaching_feedback", JSONB, nullable=True,
|
||||
comment="AI 教练引擎回写的指导反馈"),
|
||||
)
|
||||
|
||||
# ── 2. crm_customers 新增 health_score / meddic_status ──
|
||||
op.add_column(
|
||||
"crm_customers",
|
||||
sa.Column("health_score", sa.Numeric(5, 2), server_default="0",
|
||||
comment="客户健康度评分 (AI 教练引擎计算)"),
|
||||
)
|
||||
op.add_column(
|
||||
"crm_customers",
|
||||
sa.Column("meddic_status", JSONB, nullable=True,
|
||||
comment="MEDDIC 六维评估状态"),
|
||||
)
|
||||
|
||||
# ── 3. kb_obsidian_vectors 表暂不在此迁移创建 ──
|
||||
# 需先安装 pgvector 扩展,见单独迁移脚本
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.drop_column("crm_customers", "meddic_status")
|
||||
op.drop_column("crm_customers", "health_score")
|
||||
op.drop_column("sales_logs", "ai_coaching_feedback")
|
||||
@@ -0,0 +1,43 @@
|
||||
"""Phase D addon: pgvector kb_obsidian_vectors table
|
||||
|
||||
Revision ID: b8c9d0e1f2a3
|
||||
Revises: a7b8c9d0e1f2
|
||||
Create Date: 2026-03-27
|
||||
|
||||
Prerequisites: sudo apt-get install postgresql-16-pgvector
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
from sqlalchemy.dialects.postgresql import UUID, JSONB
|
||||
|
||||
revision = "b8c9d0e1f2a3"
|
||||
down_revision = "a7b8c9d0e1f2"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.execute("CREATE EXTENSION IF NOT EXISTS vector")
|
||||
op.create_table(
|
||||
"kb_obsidian_vectors",
|
||||
sa.Column("id", UUID(as_uuid=True), primary_key=True),
|
||||
sa.Column("company_id", UUID(as_uuid=True), sa.ForeignKey("sys_companies.id"), nullable=False, index=True),
|
||||
sa.Column("source_path", sa.String(500), nullable=False, comment="源文件路径"),
|
||||
sa.Column("chunk_index", sa.SmallInteger, server_default="0"),
|
||||
sa.Column("content", sa.Text, nullable=False),
|
||||
sa.Column("metadata", JSONB, nullable=True),
|
||||
sa.Column("created_at", sa.DateTime, server_default=sa.func.now()),
|
||||
sa.Column("is_deleted", sa.Boolean, server_default="false"),
|
||||
)
|
||||
op.execute("ALTER TABLE kb_obsidian_vectors ADD COLUMN embedding vector(1536)")
|
||||
op.execute("""
|
||||
CREATE INDEX ix_kb_obsidian_vectors_embedding
|
||||
ON kb_obsidian_vectors
|
||||
USING hnsw (embedding vector_cosine_ops)
|
||||
""")
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
op.execute("DROP INDEX IF EXISTS ix_kb_obsidian_vectors_embedding")
|
||||
op.drop_table("kb_obsidian_vectors")
|
||||
op.execute("DROP EXTENSION IF EXISTS vector")
|
||||
Reference in New Issue
Block a user