diff --git a/api/instance/baodan.db b/api/instance/baodan.db new file mode 100644 index 0000000..191c1d6 Binary files /dev/null and b/api/instance/baodan.db differ diff --git a/api/insurance/admin/ppt_admin_service.py b/api/insurance/admin/ppt_admin_service.py index efaab3e..82c5416 100644 --- a/api/insurance/admin/ppt_admin_service.py +++ b/api/insurance/admin/ppt_admin_service.py @@ -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]: """安全获取分页参数,防止负数和超大值。""" diff --git a/api/insurance/app.py b/api/insurance/app.py index 772de4a..5a01ac9 100644 --- a/api/insurance/app.py +++ b/api/insurance/app.py @@ -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) + diff --git a/api/insurance/db/database.py b/api/insurance/db/database.py index 85d326b..f35112d 100644 --- a/api/insurance/db/database.py +++ b/api/insurance/db/database.py @@ -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("数据库初始化完成") diff --git a/api/insurance/db/migrate_015.py b/api/insurance/db/migrate_015.py index 3feec8c..a34586b 100644 --- a/api/insurance/db/migrate_015.py +++ b/api/insurance/db/migrate_015.py @@ -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() - ) - """)) - db.session.commit() - logger.info("[migrate_015] poster_case_uploads 创建完成") + 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} 创建完成") - # ========== 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=永久保留)"), ]: - 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}) + 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] 全部迁移完成") diff --git a/frontend/src/App.vue b/frontend/src/App.vue index c5a970f..b401800 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -39,18 +39,10 @@ PPT生成 - - - 建议书历史 - 海报生成 - - - 海报历史 - @@ -161,18 +153,10 @@ PPT生成 - - - 建议书历史 - 海报生成 - - - 海报历史 - @@ -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; diff --git a/scripts/setup/patch_app.py b/scripts/setup/patch_app.py index dde1abb..43baae4 100644 --- a/scripts/setup/patch_app.py +++ b/scripts/setup/patch_app.py @@ -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__":