2026-07-23 15:04:16 +08:00
|
|
|
|
"""迁移 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}")
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 17:40:12 +08:00
|
|
|
|
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
|
|
|
|
|
|
)
|
|
|
|
|
|
""",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-07-23 15:04:16 +08:00
|
|
|
|
def migrate():
|
|
|
|
|
|
"""迁移入口。"""
|
|
|
|
|
|
from insurance.db.compat import db
|
|
|
|
|
|
from sqlalchemy import text
|
|
|
|
|
|
|
2026-07-23 17:40:12 +08:00
|
|
|
|
dialect = db.engine.dialect.name
|
|
|
|
|
|
is_postgres = dialect == 'postgresql'
|
|
|
|
|
|
|
2026-07-23 15:04:16 +08:00
|
|
|
|
# ========== 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)
|
2026-07-23 17:40:12 +08:00
|
|
|
|
# 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)
|
2026-07-23 15:04:16 +08:00
|
|
|
|
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 扩展完成")
|
|
|
|
|
|
|
2026-07-23 17:40:12 +08:00
|
|
|
|
# ========== 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}")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
db.session.commit()
|
2026-07-23 17:40:12 +08:00
|
|
|
|
|
|
|
|
|
|
# ========== 种子数据 ==========
|
|
|
|
|
|
logger.info("[migrate_015] 写入种子数据 ...")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
for key, value, desc in [
|
|
|
|
|
|
("ppt_history_retention_days", "365", "PPT 历史记录保留天数(0=永久保留)"),
|
|
|
|
|
|
("poster_history_retention_days", "365", "海报历史记录保留天数(0=永久保留)"),
|
|
|
|
|
|
]:
|
2026-07-23 17:40:12 +08:00
|
|
|
|
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 # 已存在则忽略
|
2026-07-23 15:04:16 +08:00
|
|
|
|
db.session.commit()
|
2026-07-23 17:40:12 +08:00
|
|
|
|
logger.info("[migrate_015] 种子数据写入完成")
|
2026-07-23 15:04:16 +08:00
|
|
|
|
|
|
|
|
|
|
logger.info("[migrate_015] 全部迁移完成")
|