# 问题 修复 文件 1 前端构建失败(引号错误) size="small type=" → size="small" type=" PosterHistoryPage.vue 2 migrate_014 ORM vs 缺失列 全部改为原始 SQL,不再引用 ORM 模型 migrate_014.py 3 cleanup 字段名错误 output_path → ppt_path cleanup.py 4 文案生成 case 越权 添加 case.user_id != user_id 校验 poster/service.py 5 存储路径未接通持久化卷 全部改用 get_storage_root()(默认 /app/api/storage/insurance) config.py, ppt/routes.py, poster/service.py, poster/tasks.py 高风险问题修复 # 问题 修复 文件 6 migrate_019 rollback 撤销成功字段 每个 ALTER 后立即 commit,失败只回滚当前语句 migrate_019.py 7 迁移锁 Windows 不兼容 + 句柄未持久化 全局变量保存锁句柄,支持 Windows msvcrt api/insurance/db/__init__.py 8 PDF 校验异常时放行 异常返回 False(文件损坏) security.py 9 健康检查始终返回成功 缺少关键资源时返回 503 + missing 列表 poster/routes.py 10 短密钥掩码泄露原值 ≤4 字符返回 **** ppt_admin_service.py 11 设置无键名白名单 添加 _ALLOWED_SETTING_KEYS 白名单 ppt_admin_service.py 12 容器重启任务永久 stuck 添加 recover_stale_tasks() 启动恢复函数 poster/tasks.py, ppt/parse_worker.py
144 lines
5.8 KiB
Python
144 lines
5.8 KiB
Python
"""迁移 014: 导入 PPT 配置数据。
|
||
|
||
使用原始 SQL 而非 ORM,避免模型字段与实际表结构不一致的问题。
|
||
(ORM 模型包含 migrate_017 才创建的 slides_config_json 字段)
|
||
"""
|
||
import os
|
||
import json
|
||
import logging
|
||
from sqlalchemy import text
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
CONFIG_DIR = os.path.join(os.path.dirname(__file__), "..", "ppt", "config")
|
||
|
||
|
||
def _read_json_file(filepath):
|
||
try:
|
||
with open(filepath, "r", encoding="utf-8") as f:
|
||
return json.load(f)
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def _read_json_files_in_dir(dirpath):
|
||
results = []
|
||
if not os.path.isdir(dirpath):
|
||
return results
|
||
for root, _, files in os.walk(dirpath):
|
||
for fname in files:
|
||
if fname.endswith(".json"):
|
||
data = _read_json_file(os.path.join(root, fname))
|
||
if data is not None:
|
||
results.append(data)
|
||
return results
|
||
|
||
|
||
def _record_exists(db, table: str, id_col: str, id_val: str) -> bool:
|
||
"""检查记录是否存在。"""
|
||
result = db.session.execute(
|
||
text(f"SELECT 1 FROM {table} WHERE {id_col} = :id LIMIT 1"),
|
||
{"id": id_val},
|
||
)
|
||
return result.fetchone() is not None
|
||
|
||
|
||
def migrate():
|
||
"""迁移入口。"""
|
||
from insurance.db.compat import db
|
||
|
||
# 公司
|
||
companies_dir = os.path.join(CONFIG_DIR, "companies")
|
||
count = 0
|
||
for d in _read_json_files_in_dir(companies_dir):
|
||
if not d.get("id") or _record_exists(db, "insurance_ppt_companies", "id", d["id"]):
|
||
continue
|
||
db.session.execute(text(
|
||
"INSERT INTO insurance_ppt_companies "
|
||
"(id, display_name, aliases_json, tenant_id, knowledge_directories_json, "
|
||
"evidence_ranking_json, company_intro, company_highlights_json) "
|
||
"VALUES (:id, :display_name, :aliases_json, :tenant_id, :knowledge_directories_json, "
|
||
":evidence_ranking_json, :company_intro, :company_highlights_json)"
|
||
), {
|
||
"id": d["id"],
|
||
"display_name": d.get("displayName", ""),
|
||
"aliases_json": json.dumps(d.get("aliases", []), ensure_ascii=False),
|
||
"tenant_id": d.get("tenantId", "default"),
|
||
"knowledge_directories_json": json.dumps(d.get("knowledgeDirectories", []), ensure_ascii=False),
|
||
"evidence_ranking_json": json.dumps(d.get("evidenceRanking", []), ensure_ascii=False),
|
||
"company_intro": d.get("companyIntro", ""),
|
||
"company_highlights_json": json.dumps(d.get("companyHighlights", []), ensure_ascii=False),
|
||
})
|
||
count += 1
|
||
db.session.commit()
|
||
logger.info(f"[migrate_014] 导入 {count} 家公司")
|
||
|
||
# 产品
|
||
products_dir = os.path.join(CONFIG_DIR, "products")
|
||
count = 0
|
||
for d in _read_json_files_in_dir(products_dir):
|
||
if not d.get("id") or _record_exists(db, "insurance_ppt_products", "id", d["id"]):
|
||
continue
|
||
db.session.execute(text(
|
||
"INSERT INTO insurance_ppt_products "
|
||
"(id, company_id, plan_type, display_name, aliases_json, required_modules_json) "
|
||
"VALUES (:id, :company_id, :plan_type, :display_name, :aliases_json, :required_modules_json)"
|
||
), {
|
||
"id": d["id"],
|
||
"company_id": d.get("companyId", ""),
|
||
"plan_type": d.get("planType", "savings"),
|
||
"display_name": d.get("displayName", ""),
|
||
"aliases_json": json.dumps(d.get("aliases", []), ensure_ascii=False),
|
||
"required_modules_json": json.dumps(d.get("requiredModules", []), ensure_ascii=False),
|
||
})
|
||
count += 1
|
||
db.session.commit()
|
||
logger.info(f"[migrate_014] 导入 {count} 个产品")
|
||
|
||
# 模板(只使用 migrate_014 时已存在的列)
|
||
templates_dir = os.path.join(CONFIG_DIR, "templates")
|
||
count = 0
|
||
for d in _read_json_files_in_dir(templates_dir):
|
||
if not d.get("id") or _record_exists(db, "insurance_ppt_templates", "id", d["id"]):
|
||
continue
|
||
db.session.execute(text(
|
||
"INSERT INTO insurance_ppt_templates "
|
||
"(id, plan_type, style_preset, source_template_asset_id, clone_ready, "
|
||
"clone_renderer, required_page_types_json) "
|
||
"VALUES (:id, :plan_type, :style_preset, :source_template_asset_id, :clone_ready, "
|
||
":clone_renderer, :required_page_types_json)"
|
||
), {
|
||
"id": d["id"],
|
||
"plan_type": d.get("planType", "savings"),
|
||
"style_preset": d.get("stylePreset", "broker"),
|
||
"source_template_asset_id": d.get("sourceTemplateAssetId"),
|
||
"clone_ready": d.get("cloneReady", False),
|
||
"clone_renderer": d.get("cloneRenderer"),
|
||
"required_page_types_json": json.dumps(d.get("requiredPageTypes", []), ensure_ascii=False),
|
||
})
|
||
count += 1
|
||
db.session.commit()
|
||
logger.info(f"[migrate_014] 导入 {count} 个模板")
|
||
|
||
# Bundle
|
||
bundles_dir = os.path.join(CONFIG_DIR, "bundles")
|
||
count = 0
|
||
for d in _read_json_files_in_dir(bundles_dir):
|
||
if not d.get("id") or _record_exists(db, "insurance_ppt_bundles", "id", d["id"]):
|
||
continue
|
||
db.session.execute(text(
|
||
"INSERT INTO insurance_ppt_bundles "
|
||
"(id, display_name, products_json, template_family, status, modules_json) "
|
||
"VALUES (:id, :display_name, :products_json, :template_family, :status, :modules_json)"
|
||
), {
|
||
"id": d["id"],
|
||
"display_name": d.get("displayName", ""),
|
||
"products_json": json.dumps(d.get("products", []), ensure_ascii=False),
|
||
"template_family": d.get("templateFamily", "bundle"),
|
||
"status": d.get("status", "active"),
|
||
"modules_json": json.dumps(d.get("modules", []), ensure_ascii=False),
|
||
})
|
||
count += 1
|
||
db.session.commit()
|
||
logger.info(f"[migrate_014] 导入 {count} 个 Bundle")
|