baodan/api/insurance/db/migrate_015.py

345 lines
13 KiB
Python
Raw Normal View History

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] 全部迁移完成")