v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成
- Docker bridge 网络隔离(8000 端口封死) - Gunicorn 4 Worker 多进程 - Alembic 数据库迁移基线 - 日志轮转 20m×3 - JWT 密钥 + DB 密码 + CORS 收紧 - 3-2-1 备份链路(NAS + R740-B 冷备) - 连接池 pool_pre_ping + pool_recycle=3600
This commit is contained in:
@@ -0,0 +1,143 @@
|
||||
"""initial_full_schema
|
||||
|
||||
Renames legacy tables to *_legacy backup, then creates
|
||||
all tables with correct UUID PKs, types, and constraints.
|
||||
|
||||
Revision ID: 0001
|
||||
Revises:
|
||||
Create Date: 2026-02-24
|
||||
"""
|
||||
from typing import Sequence, Union
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
revision: str = "0001"
|
||||
down_revision: Union[str, None] = None
|
||||
branch_labels: Union[str, Sequence[str], None] = None
|
||||
depends_on: Union[str, Sequence[str], None] = None
|
||||
|
||||
# Legacy tables to rename (preserve data)
|
||||
LEGACY_TABLES = ["users", "clients", "follow_ups", "expenses"]
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Step 1: Rename legacy tables to *_legacy
|
||||
for table in LEGACY_TABLES:
|
||||
exists = conn.execute(sa.text(
|
||||
f"SELECT EXISTS (SELECT 1 FROM information_schema.tables "
|
||||
f"WHERE table_schema='public' AND table_name='{table}')"
|
||||
)).scalar()
|
||||
if exists:
|
||||
conn.execute(sa.text(
|
||||
f'ALTER TABLE "{table}" RENAME TO "{table}_legacy"'
|
||||
))
|
||||
|
||||
# Step 2: Create new tables with proper schema
|
||||
|
||||
# ---- users ----
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE users (
|
||||
id UUID PRIMARY KEY,
|
||||
username VARCHAR(50) UNIQUE NOT NULL,
|
||||
password_hash VARCHAR(128) NOT NULL,
|
||||
role VARCHAR(20) NOT NULL DEFAULT 'viewer',
|
||||
permissions JSON DEFAULT '[]',
|
||||
is_active BOOLEAN DEFAULT true,
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
updated_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
"""))
|
||||
|
||||
# ---- clients ----
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE clients (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(200) NOT NULL,
|
||||
contact_person VARCHAR(100),
|
||||
phone VARCHAR(30),
|
||||
address VARCHAR(500),
|
||||
notes TEXT,
|
||||
created_at TIMESTAMP DEFAULT now(),
|
||||
updated_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX ix_clients_name ON clients (name);"
|
||||
))
|
||||
|
||||
# ---- customer_logs ----
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE customer_logs (
|
||||
id UUID PRIMARY KEY,
|
||||
customer_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX ix_customer_logs_cid ON customer_logs (customer_id);"
|
||||
))
|
||||
|
||||
# ---- customer_tags ----
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE customer_tags (
|
||||
id UUID PRIMARY KEY,
|
||||
customer_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
tag_name VARCHAR(100) NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX ix_customer_tags_cid ON customer_tags (customer_id);"
|
||||
))
|
||||
|
||||
# ---- follow_up_todos ----
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE follow_up_todos (
|
||||
id UUID PRIMARY KEY,
|
||||
customer_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
task_desc TEXT NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX ix_follow_up_todos_cid ON follow_up_todos (customer_id);"
|
||||
))
|
||||
|
||||
# ---- sales_opportunities ----
|
||||
conn.execute(sa.text("""
|
||||
CREATE TABLE sales_opportunities (
|
||||
id UUID PRIMARY KEY,
|
||||
customer_id UUID NOT NULL REFERENCES clients(id) ON DELETE CASCADE,
|
||||
amount NUMERIC(12, 2) NOT NULL DEFAULT 0,
|
||||
stage VARCHAR(20) NOT NULL DEFAULT 'intent',
|
||||
created_at TIMESTAMP DEFAULT now()
|
||||
);
|
||||
"""))
|
||||
conn.execute(sa.text(
|
||||
"CREATE INDEX ix_sales_opp_cid ON sales_opportunities (customer_id);"
|
||||
))
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
conn = op.get_bind()
|
||||
|
||||
# Drop new tables
|
||||
for t in ["sales_opportunities", "follow_up_todos", "customer_tags",
|
||||
"customer_logs", "clients", "users"]:
|
||||
conn.execute(sa.text(f'DROP TABLE IF EXISTS "{t}" CASCADE'))
|
||||
|
||||
# Restore legacy tables
|
||||
for table in LEGACY_TABLES:
|
||||
exists = conn.execute(sa.text(
|
||||
f"SELECT EXISTS (SELECT 1 FROM information_schema.tables "
|
||||
f"WHERE table_schema='public' AND table_name='{table}_legacy')"
|
||||
)).scalar()
|
||||
if exists:
|
||||
conn.execute(sa.text(
|
||||
f'ALTER TABLE "{table}_legacy" RENAME TO "{table}"'
|
||||
))
|
||||
Reference in New Issue
Block a user