小册子解析:上传后3秒内返回,解析通过 Celery 异步执行,状态流转 pending → queued → parsing → parsed/failed,失败保留错误原因,启动时自动恢复卡住任务 保司Logo:支持多张图片上传、设主图、删除、排序,主图自动同步到 logo_url 保持向后兼容 文件存储:统一使用 get_storage_root() 持久化目录,Docker 重启不丢文件
115 lines
4.4 KiB
Python
115 lines
4.4 KiB
Python
"""迁移 023: 产品小册子异步解析 + 保司多Logo。
|
||
|
||
- 扩展 insurance_ppt_products:新增小册子解析状态字段
|
||
- 新建 insurance_ppt_company_logos:保司多Logo表
|
||
- 清理卡住的 parsing 数据
|
||
|
||
兼容 PostgreSQL 和 MySQL。
|
||
"""
|
||
import logging
|
||
from sqlalchemy import text
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
def _column_exists(db, table: str, column: str) -> bool:
|
||
"""检查列是否存在(兼容 PostgreSQL 和 MySQL)。"""
|
||
try:
|
||
result = db.session.execute(text(
|
||
"SELECT COUNT(*) FROM information_schema.columns "
|
||
"WHERE table_name = :table AND column_name = :column"
|
||
), {"table": table, "column": column})
|
||
return result.scalar() > 0
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _table_exists(db, table_name: str) -> bool:
|
||
"""检查表是否存在(兼容 PostgreSQL 和 MySQL)。"""
|
||
try:
|
||
result = db.session.execute(text(
|
||
"SELECT COUNT(*) FROM information_schema.tables "
|
||
"WHERE table_name = :table"
|
||
), {"table": table_name})
|
||
return result.scalar() > 0
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _index_exists(db, index_name: str) -> bool:
|
||
"""检查索引是否存在(兼容 PostgreSQL 和 MySQL)。"""
|
||
try:
|
||
result = db.session.execute(text(
|
||
"SELECT COUNT(*) FROM pg_indexes WHERE indexname = :name"
|
||
), {"name": index_name})
|
||
return result.scalar() > 0
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def migrate():
|
||
"""执行迁移。"""
|
||
from insurance.db.compat import db
|
||
|
||
# ─── 扩展 PptProduct 小册子字段 ───────────────────────────
|
||
manual_columns = [
|
||
("manual_parse_message", "VARCHAR(500) DEFAULT ''"),
|
||
("manual_parse_error", "TEXT"),
|
||
("manual_parse_task_id", "VARCHAR(200)"),
|
||
("manual_parse_started_at", "TIMESTAMP"),
|
||
("manual_parse_finished_at", "TIMESTAMP"),
|
||
]
|
||
|
||
for col_name, col_def in manual_columns:
|
||
if not _column_exists(db, "insurance_ppt_products", col_name):
|
||
db.session.execute(text(
|
||
f"ALTER TABLE insurance_ppt_products ADD COLUMN {col_name} {col_def}"
|
||
))
|
||
logger.info(f"[migrate_023] 已为 insurance_ppt_products 添加 {col_name} 列")
|
||
|
||
# ─── 清理卡住的 parsing 数据 ─────────────────────────────
|
||
# 将 manual_parse_status='parsing' 且无有效 manual_parsed_rules 的记录改为 failed
|
||
result = db.session.execute(text(
|
||
"UPDATE insurance_ppt_products "
|
||
"SET manual_parse_status = 'failed', "
|
||
" manual_parse_error = '任务因服务重启而中断,请重新解析', "
|
||
" manual_parse_finished_at = CURRENT_TIMESTAMP "
|
||
"WHERE manual_parse_status = 'parsing' "
|
||
"AND (manual_parsed_rules IS NULL OR manual_parsed_rules = '')"
|
||
))
|
||
if result.rowcount > 0:
|
||
logger.info(f"[migrate_023] 已恢复 {result.rowcount} 个卡住的 parsing 记录为 failed")
|
||
|
||
# ─── 新建 insurance_ppt_company_logos ────────────────────
|
||
if not _table_exists(db, "insurance_ppt_company_logos"):
|
||
db.session.execute(text("""
|
||
CREATE TABLE insurance_ppt_company_logos (
|
||
id VARCHAR(36) PRIMARY KEY,
|
||
company_id VARCHAR(50) NOT NULL,
|
||
file_path VARCHAR(500) NOT NULL,
|
||
file_url VARCHAR(500),
|
||
original_name VARCHAR(200),
|
||
mime_type VARCHAR(50),
|
||
file_size INT,
|
||
is_primary BOOLEAN DEFAULT false,
|
||
sort_order INT DEFAULT 0,
|
||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||
)
|
||
"""))
|
||
logger.info("[migrate_023] 已创建 insurance_ppt_company_logos 表")
|
||
|
||
# 创建索引
|
||
indexes = [
|
||
("idx_company_logos_company", "company_id"),
|
||
("idx_company_logos_primary", "company_id, is_primary"),
|
||
]
|
||
for idx_name, idx_cols in indexes:
|
||
if not _index_exists(db, idx_name):
|
||
db.session.execute(text(
|
||
f"CREATE INDEX {idx_name} ON insurance_ppt_company_logos ({idx_cols})"
|
||
))
|
||
logger.info(f"[migrate_023] 已创建索引 {idx_name}")
|
||
|
||
db.session.commit()
|
||
logger.info("[migrate_023] 产品小册子异步解析 + 保司多Logo 迁移完成")
|