345 lines
13 KiB
Python
345 lines
13 KiB
Python
"""迁移 015: PPT/海报功能扩展 — 扩展现有表 + 创建新表。
|
||
|
||
覆盖:
|
||
- 扩展 insurance_ppt_companies(+3 字段)
|
||
- 扩展 insurance_ppt_products(+16 字段)
|
||
- 扩展 insurance_ppt_templates(+6 字段)
|
||
- 创建 insurance_ppt_history
|
||
- 创建 poster_case_uploads
|
||
- 创建 poster_templates
|
||
- 创建 poster_copy_templates
|
||
- 创建 poster_records
|
||
- 创建 system_settings + 种子数据
|
||
"""
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _add_column(table, column_def, db):
|
||
"""安全添加列(已存在则跳过)。"""
|
||
from sqlalchemy import text
|
||
try:
|
||
db.session.execute(text(f"ALTER TABLE {table} ADD COLUMN {column_def}"))
|
||
except Exception as e:
|
||
# 仅忽略"列已存在"错误,其他错误记录日志
|
||
err_msg = str(e).lower()
|
||
if "already exists" not in err_msg and "duplicate column" not in err_msg:
|
||
logger.warning(f"添加列 {table}.{column_def} 失败: {e}")
|
||
|
||
|
||
def _table_exists(table_name, db):
|
||
"""检查表是否存在。"""
|
||
from sqlalchemy import text
|
||
try:
|
||
dialect = db.engine.dialect.name
|
||
if dialect == 'postgresql':
|
||
result = db.session.execute(text(
|
||
"SELECT EXISTS (SELECT FROM information_schema.tables WHERE table_name = :name)"
|
||
), {"name": table_name})
|
||
return result.fetchone()[0]
|
||
else:
|
||
# SQLite
|
||
result = db.session.execute(text(
|
||
"SELECT name FROM sqlite_master WHERE type='table' AND name=:name"
|
||
), {"name": table_name})
|
||
return result.fetchone() is not None
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _get_create_table_sql_postgres():
|
||
"""返回 PostgreSQL 的 CREATE TABLE 语句。"""
|
||
return {
|
||
"insurance_ppt_history": """
|
||
CREATE TABLE IF NOT EXISTS insurance_ppt_history (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
session_id VARCHAR(50),
|
||
action_type VARCHAR(20) NOT NULL,
|
||
company_id VARCHAR(50),
|
||
product_id VARCHAR(50),
|
||
template_id VARCHAR(50),
|
||
content_snapshot TEXT,
|
||
file_url VARCHAR(500),
|
||
ip VARCHAR(50),
|
||
user_agent VARCHAR(500),
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
)
|
||
""",
|
||
"poster_case_uploads": """
|
||
CREATE TABLE IF NOT EXISTS poster_case_uploads (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
product_id VARCHAR(50) NOT NULL,
|
||
source_file_url VARCHAR(500) NOT NULL,
|
||
parse_status VARCHAR(20) DEFAULT 'pending',
|
||
parsed_data TEXT,
|
||
confirmed_data TEXT,
|
||
confirmed_by VARCHAR(50),
|
||
confirmed_at TIMESTAMP,
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
)
|
||
""",
|
||
"poster_templates": """
|
||
CREATE TABLE IF NOT EXISTS poster_templates (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
name VARCHAR(100) NOT NULL,
|
||
scenario_tag VARCHAR(50),
|
||
style_description TEXT NOT NULL,
|
||
color_scheme TEXT,
|
||
reference_image VARCHAR(500),
|
||
preview_image VARCHAR(500),
|
||
status SMALLINT DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT NOW(),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
)
|
||
""",
|
||
"poster_copy_templates": """
|
||
CREATE TABLE IF NOT EXISTS poster_copy_templates (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
name VARCHAR(100) NOT NULL,
|
||
scenario_tag VARCHAR(50),
|
||
content TEXT NOT NULL,
|
||
variables TEXT,
|
||
status SMALLINT DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
)
|
||
""",
|
||
"poster_records": """
|
||
CREATE TABLE IF NOT EXISTS poster_records (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
product_id VARCHAR(50),
|
||
case_upload_id BIGINT,
|
||
template_id BIGINT,
|
||
copy_mode VARCHAR(20),
|
||
copy_content TEXT,
|
||
ai_raw_content TEXT,
|
||
export_url VARCHAR(500),
|
||
export_format VARCHAR(10),
|
||
export_size VARCHAR(20),
|
||
reference_image_used VARCHAR(500),
|
||
prompt_used TEXT,
|
||
created_at TIMESTAMP DEFAULT NOW()
|
||
)
|
||
""",
|
||
"system_settings": """
|
||
CREATE TABLE IF NOT EXISTS system_settings (
|
||
id BIGSERIAL PRIMARY KEY,
|
||
key VARCHAR(100) UNIQUE NOT NULL,
|
||
value TEXT NOT NULL,
|
||
description VARCHAR(500),
|
||
updated_by VARCHAR(50),
|
||
updated_at TIMESTAMP DEFAULT NOW()
|
||
)
|
||
""",
|
||
}
|
||
|
||
|
||
def _get_create_table_sql_sqlite():
|
||
"""返回 SQLite 的 CREATE TABLE 语句。"""
|
||
return {
|
||
"insurance_ppt_history": """
|
||
CREATE TABLE IF NOT EXISTS insurance_ppt_history (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
session_id VARCHAR(50),
|
||
action_type VARCHAR(20) NOT NULL,
|
||
company_id VARCHAR(50),
|
||
product_id VARCHAR(50),
|
||
template_id VARCHAR(50),
|
||
content_snapshot TEXT,
|
||
file_url VARCHAR(500),
|
||
ip VARCHAR(50),
|
||
user_agent VARCHAR(500),
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
""",
|
||
"poster_case_uploads": """
|
||
CREATE TABLE IF NOT EXISTS poster_case_uploads (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
product_id VARCHAR(50) NOT NULL,
|
||
source_file_url VARCHAR(500) NOT NULL,
|
||
parse_status VARCHAR(20) DEFAULT 'pending',
|
||
parsed_data TEXT,
|
||
confirmed_data TEXT,
|
||
confirmed_by VARCHAR(50),
|
||
confirmed_at TIMESTAMP,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
""",
|
||
"poster_templates": """
|
||
CREATE TABLE IF NOT EXISTS poster_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(100) NOT NULL,
|
||
scenario_tag VARCHAR(50),
|
||
style_description TEXT NOT NULL,
|
||
color_scheme TEXT,
|
||
reference_image VARCHAR(500),
|
||
preview_image VARCHAR(500),
|
||
status SMALLINT DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
""",
|
||
"poster_copy_templates": """
|
||
CREATE TABLE IF NOT EXISTS poster_copy_templates (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
name VARCHAR(100) NOT NULL,
|
||
scenario_tag VARCHAR(50),
|
||
content TEXT NOT NULL,
|
||
variables TEXT,
|
||
status SMALLINT DEFAULT 1,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
""",
|
||
"poster_records": """
|
||
CREATE TABLE IF NOT EXISTS poster_records (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
user_id VARCHAR(50) NOT NULL,
|
||
product_id VARCHAR(50),
|
||
case_upload_id BIGINT,
|
||
template_id BIGINT,
|
||
copy_mode VARCHAR(20),
|
||
copy_content TEXT,
|
||
ai_raw_content TEXT,
|
||
export_url VARCHAR(500),
|
||
export_format VARCHAR(10),
|
||
export_size VARCHAR(20),
|
||
reference_image_used VARCHAR(500),
|
||
prompt_used TEXT,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
""",
|
||
"system_settings": """
|
||
CREATE TABLE IF NOT EXISTS system_settings (
|
||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
key VARCHAR(100) UNIQUE NOT NULL,
|
||
value TEXT NOT NULL,
|
||
description VARCHAR(500),
|
||
updated_by VARCHAR(50),
|
||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
""",
|
||
}
|
||
|
||
|
||
def migrate():
|
||
"""迁移入口。"""
|
||
from insurance.db.compat import db
|
||
from sqlalchemy import text
|
||
|
||
dialect = db.engine.dialect.name
|
||
is_postgres = dialect == 'postgresql'
|
||
|
||
# ========== 1. 扩展 insurance_ppt_companies ==========
|
||
logger.info("[migrate_015] 扩展 insurance_ppt_companies ...")
|
||
for col in [
|
||
"logo_url VARCHAR(500)",
|
||
"status SMALLINT DEFAULT 1",
|
||
"sort_order INTEGER DEFAULT 0",
|
||
]:
|
||
_add_column("insurance_ppt_companies", col, db)
|
||
db.session.execute(text(
|
||
"UPDATE insurance_ppt_companies SET status = 1 WHERE status IS NULL"
|
||
))
|
||
db.session.commit()
|
||
logger.info("[migrate_015] insurance_ppt_companies 扩展完成")
|
||
|
||
# ========== 2. 扩展 insurance_ppt_products ==========
|
||
logger.info("[migrate_015] 扩展 insurance_ppt_products ...")
|
||
for col in [
|
||
"product_code VARCHAR(50)",
|
||
"product_type VARCHAR(20)",
|
||
"coverage_period VARCHAR(50)",
|
||
"payment_period VARCHAR(50)",
|
||
"insured_age_range VARCHAR(50)",
|
||
"waiting_period VARCHAR(50)",
|
||
"highlights TEXT",
|
||
"extra_fields TEXT",
|
||
"status SMALLINT DEFAULT 1",
|
||
"sort_order INTEGER DEFAULT 0",
|
||
"manual_file_url VARCHAR(500)",
|
||
"manual_parse_status VARCHAR(20) DEFAULT 'none'",
|
||
"manual_parsed_rules TEXT",
|
||
"manual_reviewed_by VARCHAR(50)",
|
||
"manual_reviewed_at TIMESTAMP",
|
||
]:
|
||
_add_column("insurance_ppt_products", col, db)
|
||
# updated_at 列可能已存在
|
||
if is_postgres:
|
||
_add_column("insurance_ppt_products", "updated_at TIMESTAMP DEFAULT NOW()", db)
|
||
else:
|
||
_add_column("insurance_ppt_products", "updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP", db)
|
||
db.session.execute(text(
|
||
"UPDATE insurance_ppt_products SET status = 1 WHERE status IS NULL"
|
||
))
|
||
db.session.commit()
|
||
logger.info("[migrate_015] insurance_ppt_products 扩展完成")
|
||
|
||
# ========== 3. 扩展 insurance_ppt_templates ==========
|
||
logger.info("[migrate_015] 扩展 insurance_ppt_templates ...")
|
||
for col in [
|
||
"name VARCHAR(100)",
|
||
"scenario_tag VARCHAR(50)",
|
||
"preview_image VARCHAR(500)",
|
||
"applicable_company_ids TEXT",
|
||
"applicable_product_ids TEXT",
|
||
"status SMALLINT DEFAULT 1",
|
||
]:
|
||
_add_column("insurance_ppt_templates", col, db)
|
||
db.session.commit()
|
||
logger.info("[migrate_015] insurance_ppt_templates 扩展完成")
|
||
|
||
# ========== 4-9. 创建新表(根据数据库方言选择 SQL) ==========
|
||
sql_map = _get_create_table_sql_postgres() if is_postgres else _get_create_table_sql_sqlite()
|
||
|
||
for table_name in ["insurance_ppt_history", "poster_case_uploads", "poster_templates",
|
||
"poster_copy_templates", "poster_records", "system_settings"]:
|
||
if _table_exists(table_name, db):
|
||
logger.info(f"[migrate_015] 表 {table_name} 已存在,跳过创建")
|
||
continue
|
||
logger.info(f"[migrate_015] 创建 {table_name} ...")
|
||
db.session.execute(text(sql_map[table_name]))
|
||
db.session.commit()
|
||
logger.info(f"[migrate_015] {table_name} 创建完成")
|
||
|
||
# ========== 索引 ==========
|
||
indexes = [
|
||
"CREATE INDEX IF NOT EXISTS idx_ppt_history_user ON insurance_ppt_history(user_id, created_at DESC)",
|
||
"CREATE INDEX IF NOT EXISTS idx_poster_records_user ON poster_records(user_id, created_at DESC)",
|
||
]
|
||
for idx_sql in indexes:
|
||
try:
|
||
db.session.execute(text(idx_sql))
|
||
except Exception as e:
|
||
logger.warning(f"创建索引失败: {e}")
|
||
db.session.commit()
|
||
|
||
# ========== 种子数据 ==========
|
||
logger.info("[migrate_015] 写入种子数据 ...")
|
||
for key, value, desc in [
|
||
("ppt_history_retention_days", "365", "PPT 历史记录保留天数(0=永久保留)"),
|
||
("poster_history_retention_days", "365", "海报历史记录保留天数(0=永久保留)"),
|
||
]:
|
||
if is_postgres:
|
||
db.session.execute(text(
|
||
"INSERT INTO system_settings (key, value, description) "
|
||
"SELECT :key, :value, :desc "
|
||
"WHERE NOT EXISTS (SELECT 1 FROM system_settings WHERE key = :key)"
|
||
), {"key": key, "value": value, "desc": desc})
|
||
else:
|
||
# SQLite 不支持 SELECT ... INSERT 语法,用 try/except
|
||
try:
|
||
db.session.execute(text(
|
||
"INSERT INTO system_settings (key, value, description) VALUES (:key, :value, :desc)"
|
||
), {"key": key, "value": value, "desc": desc})
|
||
except Exception:
|
||
pass # 已存在则忽略
|
||
db.session.commit()
|
||
logger.info("[migrate_015] 种子数据写入完成")
|
||
|
||
logger.info("[migrate_015] 全部迁移完成")
|