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__":