0723 PPT加海报功能01版本修复

This commit is contained in:
wsb1224 2026-07-23 17:40:12 +08:00
parent 5d28e4518e
commit effc2a24e4
7 changed files with 342 additions and 178 deletions

BIN
api/instance/baodan.db Normal file

Binary file not shown.

View File

@ -1,5 +1,6 @@
"""PPT/海报管理后台服务。""" """PPT/海报管理后台服务。"""
import json import json
import logging
import os import os
import uuid import uuid
from flask import current_app, request from flask import current_app, request
@ -12,6 +13,8 @@ from insurance.models.poster_record import PosterRecord
from insurance.models.system_setting import SystemSetting from insurance.models.system_setting import SystemSetting
from sqlalchemy import or_ from sqlalchemy import or_
logger = logging.getLogger(__name__)
def _safe_page_params(params: dict) -> tuple[int, int]: def _safe_page_params(params: dict) -> tuple[int, int]:
"""安全获取分页参数,防止负数和超大值。""" """安全获取分页参数,防止负数和超大值。"""

View File

@ -1,7 +1,6 @@
"""保单智能客服系统 — 独立后端入口 """淇濆崟鏅鸿兘瀹㈡湇绯荤粺 鈥?鐙珛鍚庣鍏ュ彛
独立部署时使用的 Flask 应用不依赖 Dify 扩展 珛閮ㄧ讲鏃朵娇鐢ㄧ殑 Flask 搴旂敤锛屼笉渚濊禆 Dify 睍銆?"""
"""
import os import os
import logging import logging
from logging.handlers import RotatingFileHandler from logging.handlers import RotatingFileHandler
@ -9,26 +8,25 @@ from flask import Flask
from flask_cors import CORS from flask_cors import CORS
from dotenv import load_dotenv from dotenv import load_dotenv
# 加载环境变量 # 鍔犺浇鐜鍙橀噺
load_dotenv() load_dotenv()
def setup_logging(app: Flask) -> None: def setup_logging(app: Flask) -> None:
"""配置统一的日志系统。""" """閰嶇疆缁熶竴鐨勬棩蹇楃郴缁熴€?""
log_level = app.config.get('LOG_LEVEL', 'INFO') log_level = app.config.get('LOG_LEVEL', 'INFO')
log_file = app.config.get('LOG_FILE', 'logs/insurance.log') log_file = app.config.get('LOG_FILE', 'logs/insurance.log')
# 创建日志目录 # 鍒涘缓鏃ュ織鐩綍
log_dir = os.path.dirname(log_file) log_dir = os.path.dirname(log_file)
if log_dir and not os.path.exists(log_dir): if log_dir and not os.path.exists(log_dir):
os.makedirs(log_dir, exist_ok=True) os.makedirs(log_dir, exist_ok=True)
# 配置根日志记录器 # 閰嶇疆鏍规棩蹇楄褰曞櫒
root_logger = logging.getLogger() root_logger = logging.getLogger()
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO)) root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
# 控制台输出 # 鎺у埗鍙拌緭鍑? console_handler = logging.StreamHandler()
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.INFO) console_handler.setLevel(logging.INFO)
console_format = logging.Formatter( console_format = logging.Formatter(
'%(asctime)s [%(levelname)s] %(name)s: %(message)s', '%(asctime)s [%(levelname)s] %(name)s: %(message)s',
@ -37,8 +35,7 @@ def setup_logging(app: Flask) -> None:
console_handler.setFormatter(console_format) console_handler.setFormatter(console_format)
root_logger.addHandler(console_handler) root_logger.addHandler(console_handler)
# 文件输出(带轮转) # 鏂囦欢杈撳嚭锛堝甫杞浆锛? try:
try:
file_handler = RotatingFileHandler( file_handler = RotatingFileHandler(
log_file, log_file,
maxBytes=10 * 1024 * 1024, # 10MB maxBytes=10 * 1024 * 1024, # 10MB
@ -53,9 +50,9 @@ def setup_logging(app: Flask) -> None:
file_handler.setFormatter(file_format) file_handler.setFormatter(file_format)
root_logger.addHandler(file_handler) root_logger.addHandler(file_handler)
except Exception as e: except Exception as e:
print(f"[logging] 文件日志配置失败: {e}") print(f"[logging] 鏂囦欢鏃ュ織閰嶇疆澶辫触: {e}")
# 将 print 替换为 logging # 灏?print 鏇挎崲涓?logging
import builtins import builtins
original_print = builtins.print original_print = builtins.print
def logging_print(*args, **kwargs): def logging_print(*args, **kwargs):
@ -65,22 +62,22 @@ def setup_logging(app: Flask) -> None:
def create_app() -> Flask: def create_app() -> Flask:
"""创建 Flask 应用。""" """鍒涘缓 Flask 搴旂敤銆?""
app = Flask(__name__) app = Flask(__name__)
# 配置 # 閰嶇疆
app.config['SECRET_KEY'] = os.getenv('JWT_SECRET', 'baodan-jwt-secret-2026') app.config['SECRET_KEY'] = os.getenv('JWT_SECRET', 'baodan-jwt-secret-2026')
app.config['JWT_SECRET'] = os.getenv('JWT_SECRET', 'baodan-jwt-secret-2026') app.config['JWT_SECRET'] = os.getenv('JWT_SECRET', 'baodan-jwt-secret-2026')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
app.config['REDIS_URL'] = os.getenv('REDIS_URL') app.config['REDIS_URL'] = os.getenv('REDIS_URL')
# 配置日志 # 閰嶇疆鏃ュ織
setup_logging(app) setup_logging(app)
# 数据库:优先 DATABASE_URL其次 DB_* 变量构建 PostgreSQL URL最后回退到 SQLite # 鏁版嵁搴擄細浼樺厛 DATABASE_URL锛屽叾娆?DB_* 鍙橀噺鏋勫缓 PostgreSQL URL锛屾渶鍚庡洖閫€鍒?SQLite
db_url = os.getenv('DATABASE_URL', '') db_url = os.getenv('DATABASE_URL', '')
if not db_url: if not db_url:
# 从 DB_* 环境变量构建 PostgreSQL URL # 浠?DB_* 鐜鍙橀噺鏋勫缓 PostgreSQL URL
db_host = os.getenv('DB_HOST', '') db_host = os.getenv('DB_HOST', '')
db_port = os.getenv('DB_PORT', '5432') db_port = os.getenv('DB_PORT', '5432')
db_user = os.getenv('DB_USERNAME', '') db_user = os.getenv('DB_USERNAME', '')
@ -89,55 +86,53 @@ def create_app() -> Flask:
if db_host and db_user and db_name: if db_host and db_user and db_name:
db_url = f"postgresql://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}" db_url = f"postgresql://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}"
if db_url and 'postgresql' in db_url: if db_url and 'postgresql' in db_url:
# 检查 psycopg2 是否可用 # 妫€鏌?psycopg2 鏄惁鍙敤
try: try:
import psycopg2 import psycopg2
app.config['SQLALCHEMY_DATABASE_URI'] = db_url app.config['SQLALCHEMY_DATABASE_URI'] = db_url
except ImportError: except ImportError:
print("[warning] psycopg2 未安装,回退到 SQLite") print("[warning] psycopg2 鏈畨瑁咃紝鍥為€€鍒?SQLite")
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db'
else: else:
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db' app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db'
app.config['MIGRATION_ENABLED'] = True app.config['MIGRATION_ENABLED'] = True
# Dify 配置 # Dify 閰嶇疆
app.config['BAODAN_API_URL'] = os.getenv('DIFY_BASE_URL', 'http://localhost:5001') app.config['BAODAN_API_URL'] = os.getenv('DIFY_BASE_URL', 'http://localhost:5001')
app.config['BAODAN_CHAT_API_KEY'] = os.getenv('DIFY_CHAT_APP_API_KEY', '') app.config['BAODAN_CHAT_API_KEY'] = os.getenv('DIFY_CHAT_APP_API_KEY', '')
app.config['BAODAN_WORKFLOW_API_KEY'] = os.getenv('DIFY_WORKFLOW_APP_API_KEY', '') app.config['BAODAN_WORKFLOW_API_KEY'] = os.getenv('DIFY_WORKFLOW_APP_API_KEY', '')
# 企业微信配置 # 浼佷笟寰俊閰嶇疆
app.config['WECOM_CORP_ID'] = os.getenv('WECOM_CORP_ID', '') app.config['WECOM_CORP_ID'] = os.getenv('WECOM_CORP_ID', '')
app.config['WECOM_SECRET'] = os.getenv('WECOM_SECRET', '') app.config['WECOM_SECRET'] = os.getenv('WECOM_SECRET', '')
app.config['WECOM_TOKEN'] = os.getenv('WECOM_TOKEN', '') app.config['WECOM_TOKEN'] = os.getenv('WECOM_TOKEN', '')
app.config['WECOM_AES_KEY'] = os.getenv('WECOM_AES_KEY', '') app.config['WECOM_AES_KEY'] = os.getenv('WECOM_AES_KEY', '')
app.config['WECOM_WEBHOOK_URL'] = os.getenv('WECOM_WEBHOOK_URL', '') app.config['WECOM_WEBHOOK_URL'] = os.getenv('WECOM_WEBHOOK_URL', '')
# 访客模式 # 璁垮妯″紡
app.config['GUEST_MODE'] = os.getenv('GUEST_MODE', 'true').lower() == 'true' app.config['GUEST_MODE'] = os.getenv('GUEST_MODE', 'true').lower() == 'true'
# 域名(分享链接用) # 鍩熷悕锛堝垎浜摼鎺ョ敤锛? app.config['DOMAIN'] = os.getenv('DOMAIN', 'localhost')
app.config['DOMAIN'] = os.getenv('DOMAIN', 'localhost')
# CORS # CORS
CORS(app, resources={r"/*": {"origins": "*"}}) CORS(app, resources={r"/*": {"origins": "*"}})
# 初始化数据库 # 鍒濆鍖栨暟鎹簱
from insurance.db.database import init_db from insurance.db.database import init_db
init_db(app) init_db(app)
# 初始化 Redis # 鍒濆鍖?Redis
from insurance.db.redis import init_redis from insurance.db.redis import init_redis
init_redis(app) init_redis(app)
# 注册路由 # 娉ㄥ唽璺敱
from insurance.routes import register_insurance_routes from insurance.routes import register_insurance_routes
register_insurance_routes(app) register_insurance_routes(app)
# 注册错误处理器 # 娉ㄥ唽閿欒澶勭悊鍣? from insurance.utils.error_handler import register_error_handlers
from insurance.utils.error_handler import register_error_handlers
register_error_handlers(app) register_error_handlers(app)
# 健康检查(检查实际依赖) # 鍋ュ悍妫€鏌ワ紙妫€鏌ュ疄闄呬緷璧栵級
@app.route('/health') @app.route('/health')
def health(): def health():
checks = {'status': 'ok', 'service': 'baodanagent-api'} checks = {'status': 'ok', 'service': 'baodanagent-api'}
@ -163,10 +158,11 @@ def create_app() -> Flask:
return app return app
# 创建应用实例 # 鍒涘缓搴旂敤瀹炰緥
app = create_app() app = create_app()
if __name__ == '__main__': if __name__ == '__main__':
port = int(os.getenv('PORT', 5001)) port = int(os.getenv('PORT', 5001))
debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true' debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true'
app.run(host='0.0.0.0', port=port, debug=debug) app.run(host='0.0.0.0', port=port, debug=debug)

View File

@ -30,7 +30,10 @@ def init_db(app: Flask) -> None:
chat_record, chat_session, role, prompt, chat_record, chat_session, role, prompt,
template, notification, department, template, notification, department,
wecom_group, email_verification, wecom_group, email_verification,
ppt_session, ppt_config, ppt_session, ppt_config, ppt_history,
poster_case_upload, poster_template_model,
poster_copy_template, poster_record,
system_setting,
) )
db.create_all() db.create_all()
logger.info("数据库初始化完成") logger.info("数据库初始化完成")

View File

@ -28,11 +28,212 @@ def _add_column(table, column_def, db):
logger.warning(f"添加列 {table}.{column_def} 失败: {e}") 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(): def migrate():
"""迁移入口。""" """迁移入口。"""
from insurance.db.compat import db from insurance.db.compat import db
from sqlalchemy import text from sqlalchemy import text
dialect = db.engine.dialect.name
is_postgres = dialect == 'postgresql'
# ========== 1. 扩展 insurance_ppt_companies ========== # ========== 1. 扩展 insurance_ppt_companies ==========
logger.info("[migrate_015] 扩展 insurance_ppt_companies ...") logger.info("[migrate_015] 扩展 insurance_ppt_companies ...")
for col in [ for col in [
@ -60,7 +261,6 @@ def migrate():
"extra_fields TEXT", "extra_fields TEXT",
"status SMALLINT DEFAULT 1", "status SMALLINT DEFAULT 1",
"sort_order INTEGER DEFAULT 0", "sort_order INTEGER DEFAULT 0",
"updated_at TIMESTAMP DEFAULT NOW()",
"manual_file_url VARCHAR(500)", "manual_file_url VARCHAR(500)",
"manual_parse_status VARCHAR(20) DEFAULT 'none'", "manual_parse_status VARCHAR(20) DEFAULT 'none'",
"manual_parsed_rules TEXT", "manual_parsed_rules TEXT",
@ -68,6 +268,11 @@ def migrate():
"manual_reviewed_at TIMESTAMP", "manual_reviewed_at TIMESTAMP",
]: ]:
_add_column("insurance_ppt_products", col, db) _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( db.session.execute(text(
"UPDATE insurance_ppt_products SET status = 1 WHERE status IS NULL" "UPDATE insurance_ppt_products SET status = 1 WHERE status IS NULL"
)) ))
@ -88,133 +293,52 @@ def migrate():
db.session.commit() db.session.commit()
logger.info("[migrate_015] insurance_ppt_templates 扩展完成") logger.info("[migrate_015] insurance_ppt_templates 扩展完成")
# ========== 4. 创建 insurance_ppt_history ========== # ========== 4-9. 创建新表(根据数据库方言选择 SQL ==========
logger.info("[migrate_015] 创建 insurance_ppt_history ...") sql_map = _get_create_table_sql_postgres() if is_postgres else _get_create_table_sql_sqlite()
db.session.execute(text("""
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()
)
"""))
db.session.execute(text(
"CREATE INDEX IF NOT EXISTS idx_ppt_history_user ON insurance_ppt_history(user_id, created_at DESC)"
))
db.session.commit()
logger.info("[migrate_015] insurance_ppt_history 创建完成")
# ========== 5. 创建 poster_case_uploads ========== for table_name in ["insurance_ppt_history", "poster_case_uploads", "poster_templates",
logger.info("[migrate_015] 创建 poster_case_uploads ...") "poster_copy_templates", "poster_records", "system_settings"]:
db.session.execute(text(""" if _table_exists(table_name, db):
CREATE TABLE IF NOT EXISTS poster_case_uploads ( logger.info(f"[migrate_015] 表 {table_name} 已存在,跳过创建")
id BIGSERIAL PRIMARY KEY, continue
user_id VARCHAR(50) NOT NULL, logger.info(f"[migrate_015] 创建 {table_name} ...")
product_id VARCHAR(50) NOT NULL, db.session.execute(text(sql_map[table_name]))
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()
)
"""))
db.session.commit() db.session.commit()
logger.info("[migrate_015] poster_case_uploads 创建完成") logger.info(f"[migrate_015] {table_name} 创建完成")
# ========== 6. 创建 poster_templates ========== # ========== 索引 ==========
logger.info("[migrate_015] 创建 poster_templates ...") indexes = [
db.session.execute(text(""" "CREATE INDEX IF NOT EXISTS idx_ppt_history_user ON insurance_ppt_history(user_id, created_at DESC)",
CREATE TABLE IF NOT EXISTS poster_templates ( "CREATE INDEX IF NOT EXISTS idx_poster_records_user ON poster_records(user_id, created_at DESC)",
id BIGSERIAL PRIMARY KEY, ]
name VARCHAR(100) NOT NULL, for idx_sql in indexes:
scenario_tag VARCHAR(50), try:
style_description TEXT NOT NULL, db.session.execute(text(idx_sql))
color_scheme TEXT, except Exception as e:
reference_image VARCHAR(500), logger.warning(f"创建索引失败: {e}")
preview_image VARCHAR(500),
status SMALLINT DEFAULT 1,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
"""))
db.session.commit() db.session.commit()
logger.info("[migrate_015] poster_templates 创建完成")
# ========== 7. 创建 poster_copy_templates ========== # ========== 种子数据 ==========
logger.info("[migrate_015] 创建 poster_copy_templates ...") logger.info("[migrate_015] 写入种子数据 ...")
db.session.execute(text("""
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()
)
"""))
db.session.commit()
logger.info("[migrate_015] poster_copy_templates 创建完成")
# ========== 8. 创建 poster_records ==========
logger.info("[migrate_015] 创建 poster_records ...")
db.session.execute(text("""
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()
)
"""))
db.session.execute(text(
"CREATE INDEX IF NOT EXISTS idx_poster_records_user ON poster_records(user_id, created_at DESC)"
))
db.session.commit()
logger.info("[migrate_015] poster_records 创建完成")
# ========== 9. 创建 system_settings + 种子数据 ==========
logger.info("[migrate_015] 创建 system_settings ...")
db.session.execute(text("""
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()
)
"""))
# 种子数据(幂等:已存在则跳过)
for key, value, desc in [ for key, value, desc in [
("ppt_history_retention_days", "365", "PPT 历史记录保留天数0=永久保留)"), ("ppt_history_retention_days", "365", "PPT 历史记录保留天数0=永久保留)"),
("poster_history_retention_days", "365", "海报历史记录保留天数0=永久保留)"), ("poster_history_retention_days", "365", "海报历史记录保留天数0=永久保留)"),
]: ]:
if is_postgres:
db.session.execute(text( db.session.execute(text(
"INSERT INTO system_settings (key, value, description) " "INSERT INTO system_settings (key, value, description) "
"SELECT :key, :value, :desc " "SELECT :key, :value, :desc "
"WHERE NOT EXISTS (SELECT 1 FROM system_settings WHERE key = :key)" "WHERE NOT EXISTS (SELECT 1 FROM system_settings WHERE key = :key)"
), {"key": key, "value": value, "desc": desc}) ), {"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() db.session.commit()
logger.info("[migrate_015] system_settings 创建完成") logger.info("[migrate_015] 种子数据写入完成")
logger.info("[migrate_015] 全部迁移完成") logger.info("[migrate_015] 全部迁移完成")

View File

@ -39,18 +39,10 @@
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
<span>PPT生成</span> <span>PPT生成</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/ppt/history">
<el-icon><Clock /></el-icon>
<span>建议书历史</span>
</el-menu-item>
<el-menu-item index="/poster"> <el-menu-item index="/poster">
<el-icon><Picture /></el-icon> <el-icon><Picture /></el-icon>
<span>海报生成</span> <span>海报生成</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/poster/history">
<el-icon><Clock /></el-icon>
<span>海报历史</span>
</el-menu-item>
<!-- <el-menu-item index="/recommend">--> <!-- <el-menu-item index="/recommend">-->
<!-- <el-icon><Document /></el-icon>--> <!-- <el-icon><Document /></el-icon>-->
<!-- <span>产品推荐</span>--> <!-- <span>产品推荐</span>-->
@ -161,18 +153,10 @@
<el-icon><Document /></el-icon> <el-icon><Document /></el-icon>
<span>PPT生成</span> <span>PPT生成</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/ppt/history">
<el-icon><Clock /></el-icon>
<span>建议书历史</span>
</el-menu-item>
<el-menu-item index="/poster"> <el-menu-item index="/poster">
<el-icon><Picture /></el-icon> <el-icon><Picture /></el-icon>
<span>海报生成</span> <span>海报生成</span>
</el-menu-item> </el-menu-item>
<el-menu-item index="/poster/history">
<el-icon><Clock /></el-icon>
<span>海报历史</span>
</el-menu-item>
<!-- <el-menu-item index="/recommend">--> <!-- <el-menu-item index="/recommend">-->
<!-- <el-icon><Document /></el-icon>--> <!-- <el-icon><Document /></el-icon>-->
<!-- <span>产品推荐</span>--> <!-- <span>产品推荐</span>-->
@ -520,6 +504,57 @@ html, body, #app {
color: inherit !important; color: inherit !important;
} }
/* 子菜单标题样式 */
.sidebar-menu .el-sub-menu__title,
.drawer-menu .el-sub-menu__title {
color: rgba(255, 255, 255, 0.7) !important;
}
.sidebar-menu .el-sub-menu__title:hover,
.drawer-menu .el-sub-menu__title:hover {
background: rgba(255, 255, 255, 0.1) !important;
color: #fff !important;
}
.sidebar-menu .el-sub-menu__title .el-icon,
.drawer-menu .el-sub-menu__title .el-icon {
color: inherit !important;
}
/* 子菜单弹出/展开区域背景 */
.sidebar-menu .el-menu--popup,
.drawer-menu .el-menu--popup {
background: #1a1a2e !important;
}
.sidebar-menu .el-sub-menu .el-menu,
.drawer-menu .el-sub-menu .el-menu {
background: transparent !important;
}
.sidebar-menu .el-sub-menu .el-menu-item,
.drawer-menu .el-sub-menu .el-menu-item {
color: rgba(255, 255, 255, 0.7) !important;
background: transparent !important;
}
.sidebar-menu .el-sub-menu .el-menu-item:hover,
.drawer-menu .el-sub-menu .el-menu-item:hover {
background: rgba(255, 255, 255, 0.1) !important;
color: #fff !important;
}
.sidebar-menu .el-sub-menu .el-menu-item.is-active,
.drawer-menu .el-sub-menu .el-menu-item.is-active {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
color: #fff !important;
}
.sidebar-menu .el-sub-menu .el-menu-item .el-icon,
.drawer-menu .el-sub-menu .el-menu-item .el-icon {
color: inherit !important;
}
/* 底部用户信息 */ /* 底部用户信息 */
.sidebar-footer { .sidebar-footer {
padding: 16px; padding: 16px;

View File

@ -1,5 +1,4 @@
"""修补基础平台 app.py注册 insurance Blueprint。 """修补基础平台 app.py注册 insurance Blueprint。
Docker 构建时执行将注册代码注入到 app.py Docker 构建时执行将注册代码注入到 app.py
""" """
APP_PY = "/app/api/app.py" APP_PY = "/app/api/app.py"
@ -7,9 +6,9 @@ APP_PY = "/app/api/app.py"
INSURANCE_FUNC = ( INSURANCE_FUNC = (
"\n# ====== Insurance Blueprint Auto-Register ======\n" "\n# ====== Insurance Blueprint Auto-Register ======\n"
"def _register_insurance(app):\n" "def _register_insurance(app):\n"
' """在基础平台 app 创建后注册 insurance Blueprint。"""\n' ' """Register insurance Blueprint after base platform app is created."""\n'
" import os\n" " import os\n"
" # 加载保险模块配置到 Flask app.config\n" " # Load insurance module config into Flask app.config\n"
" for key in ['BAODAN_CHAT_API_KEY', 'BAODAN_WORKFLOW_API_KEY', 'BAODAN_API_URL',\n" " for key in ['BAODAN_CHAT_API_KEY', 'BAODAN_WORKFLOW_API_KEY', 'BAODAN_API_URL',\n"
" 'BAODAN_KB_API_KEY', 'WECOM_CORP_ID', 'WECOM_SECRET',\n" " 'BAODAN_KB_API_KEY', 'WECOM_CORP_ID', 'WECOM_SECRET',\n"
" 'WECOM_TOKEN', 'WECOM_AES_KEY', 'WECOM_WEBHOOK_URL',\n" " 'WECOM_TOKEN', 'WECOM_AES_KEY', 'WECOM_WEBHOOK_URL',\n"
@ -23,6 +22,11 @@ INSURANCE_FUNC = (
" try:\n" " try:\n"
" from insurance.routes import register_insurance_routes\n" " from insurance.routes import register_insurance_routes\n"
" register_insurance_routes(app)\n" " register_insurance_routes(app)\n"
" # Run insurance database migrations on startup\n"
" app.config['MIGRATION_ENABLED'] = True\n"
" from insurance.db import run_migrations\n"
" with app.app_context():\n"
" run_migrations()\n"
" except Exception as e:\n" " except Exception as e:\n"
" import logging\n" " import logging\n"
' logging.warning(f"Insurance module not loaded: {e}")\n' ' logging.warning(f"Insurance module not loaded: {e}")\n'
@ -38,20 +42,19 @@ def patch_app_py():
print("[patch_app.py] Already patched, skipping.") print("[patch_app.py] Already patched, skipping.")
return return
# 1. 在文件开头插入 _register_insurance 函数定义 # 1. Insert _register_insurance function definition before "def is_db_command"
# 插入到 "def is_db_command" 之前
marker = "\ndef is_db_command" marker = "\ndef is_db_command"
if marker in content: if marker in content:
content = content.replace(marker, "\n" + INSURANCE_FUNC + "def is_db_command", 1) content = content.replace(marker, "\n" + INSURANCE_FUNC + "def is_db_command", 1)
# 2. db 命令分支app 创建后注册 # 2. db command branch: register after app creation
content = content.replace( content = content.replace(
" app = create_migrations_app()\n socketio_app = app", " app = create_migrations_app()\n socketio_app = app",
" app = create_migrations_app()\n _register_insurance(app)\n socketio_app = app", " app = create_migrations_app()\n _register_insurance(app)\n socketio_app = app",
1, 1,
) )
# 3. 正常启动分支flask_app 创建后注册 # 3. Normal startup branch: register after flask_app creation
content = content.replace( content = content.replace(
" socketio_app, flask_app = create_app()\n app = flask_app", " socketio_app, flask_app = create_app()\n app = flask_app",
" socketio_app, flask_app = create_app()\n _register_insurance(flask_app)\n app = flask_app", " socketio_app, flask_app = create_app()\n _register_insurance(flask_app)\n app = flask_app",
@ -61,7 +64,7 @@ def patch_app_py():
with open(APP_PY, "w") as f: with open(APP_PY, "w") as f:
f.write(content) f.write(content)
print("[patch_app.py] Patched app.py with insurance Blueprint registration.") print("[patch_app.py] Patched app.py with insurance Blueprint registration and migration.")
if __name__ == "__main__": if __name__ == "__main__":