0723 PPT加海报功能01版本修复
This commit is contained in:
parent
5d28e4518e
commit
effc2a24e4
BIN
api/instance/baodan.db
Normal file
BIN
api/instance/baodan.db
Normal file
Binary file not shown.
@ -1,5 +1,6 @@
|
||||
"""PPT/海报管理后台服务。"""
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import uuid
|
||||
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 sqlalchemy import or_
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _safe_page_params(params: dict) -> tuple[int, int]:
|
||||
"""安全获取分页参数,防止负数和超大值。"""
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
"""保单智能客服系统 — 独立后端入口
|
||||
"""淇濆崟鏅鸿兘瀹㈡湇绯荤粺 鈥?鐙珛鍚庣鍏ュ彛
|
||||
|
||||
独立部署时使用的 Flask 应用,不依赖 Dify 扩展。
|
||||
"""
|
||||
鐙珛閮ㄧ讲鏃朵娇鐢ㄧ殑 Flask 搴旂敤锛屼笉渚濊禆 Dify 鎵╁睍銆?"""
|
||||
import os
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
@ -9,26 +8,25 @@ from flask import Flask
|
||||
from flask_cors import CORS
|
||||
from dotenv import load_dotenv
|
||||
|
||||
# 加载环境变量
|
||||
# 鍔犺浇鐜鍙橀噺
|
||||
load_dotenv()
|
||||
|
||||
|
||||
def setup_logging(app: Flask) -> None:
|
||||
"""配置统一的日志系统。"""
|
||||
"""閰嶇疆缁熶竴鐨勬棩蹇楃郴缁熴€?""
|
||||
log_level = app.config.get('LOG_LEVEL', 'INFO')
|
||||
log_file = app.config.get('LOG_FILE', 'logs/insurance.log')
|
||||
|
||||
# 创建日志目录
|
||||
# 鍒涘缓鏃ュ織鐩綍
|
||||
log_dir = os.path.dirname(log_file)
|
||||
if log_dir and not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
|
||||
# 配置根日志记录器
|
||||
# 閰嶇疆鏍规棩蹇楄褰曞櫒
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
||||
|
||||
# 控制台输出
|
||||
console_handler = logging.StreamHandler()
|
||||
# 鎺у埗鍙拌緭鍑? console_handler = logging.StreamHandler()
|
||||
console_handler.setLevel(logging.INFO)
|
||||
console_format = logging.Formatter(
|
||||
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
||||
@ -37,8 +35,7 @@ def setup_logging(app: Flask) -> None:
|
||||
console_handler.setFormatter(console_format)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
# 文件输出(带轮转)
|
||||
try:
|
||||
# 鏂囦欢杈撳嚭锛堝甫杞浆锛? try:
|
||||
file_handler = RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=10 * 1024 * 1024, # 10MB
|
||||
@ -53,9 +50,9 @@ def setup_logging(app: Flask) -> None:
|
||||
file_handler.setFormatter(file_format)
|
||||
root_logger.addHandler(file_handler)
|
||||
except Exception as e:
|
||||
print(f"[logging] 文件日志配置失败: {e}")
|
||||
print(f"[logging] 鏂囦欢鏃ュ織閰嶇疆澶辫触: {e}")
|
||||
|
||||
# 将 print 替换为 logging
|
||||
# 灏?print 鏇挎崲涓?logging
|
||||
import builtins
|
||||
original_print = builtins.print
|
||||
def logging_print(*args, **kwargs):
|
||||
@ -65,22 +62,22 @@ def setup_logging(app: Flask) -> None:
|
||||
|
||||
|
||||
def create_app() -> Flask:
|
||||
"""创建 Flask 应用。"""
|
||||
"""鍒涘缓 Flask 搴旂敤銆?""
|
||||
app = Flask(__name__)
|
||||
|
||||
# 配置
|
||||
# 閰嶇疆
|
||||
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['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||||
app.config['REDIS_URL'] = os.getenv('REDIS_URL')
|
||||
|
||||
# 配置日志
|
||||
# 閰嶇疆鏃ュ織
|
||||
setup_logging(app)
|
||||
|
||||
# 数据库:优先 DATABASE_URL,其次 DB_* 变量构建 PostgreSQL URL,最后回退到 SQLite
|
||||
# 鏁版嵁搴擄細浼樺厛 DATABASE_URL锛屽叾娆?DB_* 鍙橀噺鏋勫缓 PostgreSQL URL锛屾渶鍚庡洖閫€鍒?SQLite
|
||||
db_url = os.getenv('DATABASE_URL', '')
|
||||
if not db_url:
|
||||
# 从 DB_* 环境变量构建 PostgreSQL URL
|
||||
# 浠?DB_* 鐜鍙橀噺鏋勫缓 PostgreSQL URL
|
||||
db_host = os.getenv('DB_HOST', '')
|
||||
db_port = os.getenv('DB_PORT', '5432')
|
||||
db_user = os.getenv('DB_USERNAME', '')
|
||||
@ -89,55 +86,53 @@ def create_app() -> Flask:
|
||||
if db_host and db_user and db_name:
|
||||
db_url = f"postgresql://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}"
|
||||
if db_url and 'postgresql' in db_url:
|
||||
# 检查 psycopg2 是否可用
|
||||
# 妫€鏌?psycopg2 鏄惁鍙敤
|
||||
try:
|
||||
import psycopg2
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = db_url
|
||||
except ImportError:
|
||||
print("[warning] psycopg2 未安装,回退到 SQLite")
|
||||
print("[warning] psycopg2 鏈畨瑁咃紝鍥為€€鍒?SQLite")
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db'
|
||||
else:
|
||||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db'
|
||||
app.config['MIGRATION_ENABLED'] = True
|
||||
|
||||
# Dify 配置
|
||||
# Dify 閰嶇疆
|
||||
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_WORKFLOW_API_KEY'] = os.getenv('DIFY_WORKFLOW_APP_API_KEY', '')
|
||||
|
||||
# 企业微信配置
|
||||
# 浼佷笟寰俊閰嶇疆
|
||||
app.config['WECOM_CORP_ID'] = os.getenv('WECOM_CORP_ID', '')
|
||||
app.config['WECOM_SECRET'] = os.getenv('WECOM_SECRET', '')
|
||||
app.config['WECOM_TOKEN'] = os.getenv('WECOM_TOKEN', '')
|
||||
app.config['WECOM_AES_KEY'] = os.getenv('WECOM_AES_KEY', '')
|
||||
app.config['WECOM_WEBHOOK_URL'] = os.getenv('WECOM_WEBHOOK_URL', '')
|
||||
|
||||
# 访客模式
|
||||
# 璁垮妯″紡
|
||||
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(app, resources={r"/*": {"origins": "*"}})
|
||||
|
||||
# 初始化数据库
|
||||
# 鍒濆鍖栨暟鎹簱
|
||||
from insurance.db.database import init_db
|
||||
init_db(app)
|
||||
|
||||
# 初始化 Redis
|
||||
# 鍒濆鍖?Redis
|
||||
from insurance.db.redis import init_redis
|
||||
init_redis(app)
|
||||
|
||||
# 注册路由
|
||||
# 娉ㄥ唽璺敱
|
||||
from insurance.routes import register_insurance_routes
|
||||
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)
|
||||
|
||||
# 健康检查(检查实际依赖)
|
||||
# 鍋ュ悍妫€鏌ワ紙妫€鏌ュ疄闄呬緷璧栵級
|
||||
@app.route('/health')
|
||||
def health():
|
||||
checks = {'status': 'ok', 'service': 'baodanagent-api'}
|
||||
@ -163,10 +158,11 @@ def create_app() -> Flask:
|
||||
return app
|
||||
|
||||
|
||||
# 创建应用实例
|
||||
# 鍒涘缓搴旂敤瀹炰緥
|
||||
app = create_app()
|
||||
|
||||
if __name__ == '__main__':
|
||||
port = int(os.getenv('PORT', 5001))
|
||||
debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true'
|
||||
app.run(host='0.0.0.0', port=port, debug=debug)
|
||||
|
||||
|
||||
@ -30,7 +30,10 @@ def init_db(app: Flask) -> None:
|
||||
chat_record, chat_session, role, prompt,
|
||||
template, notification, department,
|
||||
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()
|
||||
logger.info("数据库初始化完成")
|
||||
|
||||
@ -28,11 +28,212 @@ def _add_column(table, column_def, db):
|
||||
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():
|
||||
"""迁移入口。"""
|
||||
from insurance.db.compat import db
|
||||
from sqlalchemy import text
|
||||
|
||||
dialect = db.engine.dialect.name
|
||||
is_postgres = dialect == 'postgresql'
|
||||
|
||||
# ========== 1. 扩展 insurance_ppt_companies ==========
|
||||
logger.info("[migrate_015] 扩展 insurance_ppt_companies ...")
|
||||
for col in [
|
||||
@ -60,7 +261,6 @@ def migrate():
|
||||
"extra_fields TEXT",
|
||||
"status SMALLINT DEFAULT 1",
|
||||
"sort_order INTEGER DEFAULT 0",
|
||||
"updated_at TIMESTAMP DEFAULT NOW()",
|
||||
"manual_file_url VARCHAR(500)",
|
||||
"manual_parse_status VARCHAR(20) DEFAULT 'none'",
|
||||
"manual_parsed_rules TEXT",
|
||||
@ -68,6 +268,11 @@ def migrate():
|
||||
"manual_reviewed_at TIMESTAMP",
|
||||
]:
|
||||
_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(
|
||||
"UPDATE insurance_ppt_products SET status = 1 WHERE status IS NULL"
|
||||
))
|
||||
@ -88,133 +293,52 @@ def migrate():
|
||||
db.session.commit()
|
||||
logger.info("[migrate_015] insurance_ppt_templates 扩展完成")
|
||||
|
||||
# ========== 4. 创建 insurance_ppt_history ==========
|
||||
logger.info("[migrate_015] 创建 insurance_ppt_history ...")
|
||||
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 创建完成")
|
||||
# ========== 4-9. 创建新表(根据数据库方言选择 SQL) ==========
|
||||
sql_map = _get_create_table_sql_postgres() if is_postgres else _get_create_table_sql_sqlite()
|
||||
|
||||
# ========== 5. 创建 poster_case_uploads ==========
|
||||
logger.info("[migrate_015] 创建 poster_case_uploads ...")
|
||||
db.session.execute(text("""
|
||||
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()
|
||||
)
|
||||
"""))
|
||||
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("[migrate_015] poster_case_uploads 创建完成")
|
||||
logger.info(f"[migrate_015] {table_name} 创建完成")
|
||||
|
||||
# ========== 6. 创建 poster_templates ==========
|
||||
logger.info("[migrate_015] 创建 poster_templates ...")
|
||||
db.session.execute(text("""
|
||||
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()
|
||||
)
|
||||
"""))
|
||||
# ========== 索引 ==========
|
||||
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}")
|
||||
db.session.commit()
|
||||
logger.info("[migrate_015] poster_templates 创建完成")
|
||||
|
||||
# ========== 7. 创建 poster_copy_templates ==========
|
||||
logger.info("[migrate_015] 创建 poster_copy_templates ...")
|
||||
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()
|
||||
)
|
||||
"""))
|
||||
# 种子数据(幂等:已存在则跳过)
|
||||
# ========== 种子数据 ==========
|
||||
logger.info("[migrate_015] 写入种子数据 ...")
|
||||
for key, value, desc in [
|
||||
("ppt_history_retention_days", "365", "PPT 历史记录保留天数(0=永久保留)"),
|
||||
("poster_history_retention_days", "365", "海报历史记录保留天数(0=永久保留)"),
|
||||
]:
|
||||
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 # 已存在则忽略
|
||||
db.session.commit()
|
||||
logger.info("[migrate_015] system_settings 创建完成")
|
||||
logger.info("[migrate_015] 种子数据写入完成")
|
||||
|
||||
logger.info("[migrate_015] 全部迁移完成")
|
||||
|
||||
@ -39,18 +39,10 @@
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>PPT生成</span>
|
||||
</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-icon><Picture /></el-icon>
|
||||
<span>海报生成</span>
|
||||
</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-icon><Document /></el-icon>-->
|
||||
<!-- <span>产品推荐</span>-->
|
||||
@ -161,18 +153,10 @@
|
||||
<el-icon><Document /></el-icon>
|
||||
<span>PPT生成</span>
|
||||
</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-icon><Picture /></el-icon>
|
||||
<span>海报生成</span>
|
||||
</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-icon><Document /></el-icon>-->
|
||||
<!-- <span>产品推荐</span>-->
|
||||
@ -520,6 +504,57 @@ html, body, #app {
|
||||
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 {
|
||||
padding: 16px;
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
"""修补基础平台 app.py,注册 insurance Blueprint。
|
||||
|
||||
"""修补基础平台 app.py,注册 insurance Blueprint。
|
||||
在 Docker 构建时执行,将注册代码注入到 app.py 中。
|
||||
"""
|
||||
APP_PY = "/app/api/app.py"
|
||||
@ -7,9 +6,9 @@ APP_PY = "/app/api/app.py"
|
||||
INSURANCE_FUNC = (
|
||||
"\n# ====== Insurance Blueprint Auto-Register ======\n"
|
||||
"def _register_insurance(app):\n"
|
||||
' """在基础平台 app 创建后注册 insurance Blueprint。"""\n'
|
||||
' """Register insurance Blueprint after base platform app is created."""\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"
|
||||
" 'BAODAN_KB_API_KEY', 'WECOM_CORP_ID', 'WECOM_SECRET',\n"
|
||||
" 'WECOM_TOKEN', 'WECOM_AES_KEY', 'WECOM_WEBHOOK_URL',\n"
|
||||
@ -23,6 +22,11 @@ INSURANCE_FUNC = (
|
||||
" try:\n"
|
||||
" from insurance.routes import register_insurance_routes\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"
|
||||
" import logging\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.")
|
||||
return
|
||||
|
||||
# 1. 在文件开头插入 _register_insurance 函数定义
|
||||
# 插入到 "def is_db_command" 之前
|
||||
# 1. Insert _register_insurance function definition before "def is_db_command"
|
||||
marker = "\ndef is_db_command"
|
||||
if marker in content:
|
||||
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(
|
||||
" app = create_migrations_app()\n socketio_app = app",
|
||||
" app = create_migrations_app()\n _register_insurance(app)\n socketio_app = app",
|
||||
1,
|
||||
)
|
||||
|
||||
# 3. 正常启动分支:flask_app 创建后注册
|
||||
# 3. Normal startup branch: register after flask_app creation
|
||||
content = content.replace(
|
||||
" 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",
|
||||
@ -61,7 +64,7 @@ def patch_app_py():
|
||||
with open(APP_PY, "w") as f:
|
||||
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__":
|
||||
|
||||
Loading…
Reference in New Issue
Block a user