From effc2a24e4416310563770a49fabaac8ef681060 Mon Sep 17 00:00:00 2001 From: wsb1224 Date: Thu, 23 Jul 2026 17:40:12 +0800 Subject: [PATCH] =?UTF-8?q?0723=20PPT=E5=8A=A0=E6=B5=B7=E6=8A=A5=E5=8A=9F?= =?UTF-8?q?=E8=83=BD01=E7=89=88=E6=9C=AC=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/instance/baodan.db | Bin 0 -> 258048 bytes api/insurance/admin/ppt_admin_service.py | 3 + api/insurance/app.py | 60 ++-- api/insurance/db/database.py | 5 +- api/insurance/db/migrate_015.py | 364 +++++++++++++++-------- frontend/src/App.vue | 67 ++++- scripts/setup/patch_app.py | 21 +- 7 files changed, 342 insertions(+), 178 deletions(-) create mode 100644 api/instance/baodan.db diff --git a/api/instance/baodan.db b/api/instance/baodan.db new file mode 100644 index 0000000000000000000000000000000000000000..191c1d6b3864c79cf43677ec60e067d728708f69 GIT binary patch literal 258048 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 UfCP{L5 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__":