ZHIWEI666 commited on
Commit
f1b05a2
·
verified ·
1 Parent(s): 3559190

Upload 5 files

Browse files
Files changed (2) hide show
  1. app.py +4 -232
  2. router_system_config.py +241 -0
app.py CHANGED
@@ -7,7 +7,6 @@ from rate_limit import shared_limit
7
  from response_privacy import PrivateResponseCacheControl
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.responses import Response, JSONResponse, FileResponse
10
- from sqlalchemy.orm import Session
11
  from pydantic import BaseModel
12
  from huggingface_hub import hf_hub_download, HfApi
13
  import uuid
@@ -15,10 +14,8 @@ import urllib.parse
15
  import urllib.request
16
  import urllib.error
17
  import os
18
- import json
19
  import logging
20
  import time
21
- import datetime
22
  import threading
23
  import 数据库连接 as db
24
  import asyncio
@@ -31,8 +28,7 @@ from rate_limit import start_shared_work, stop_shared_work, close_shared_work
31
  from script_execution import start_admin_scripts, stop_admin_scripts, close_admin_scripts
32
  from comment_store import start_comment_reads, stop_comment_reads, close_comment_reads
33
  from periodic_tasks import start_periodic_task, request_periodic_stop, wait_periodic_tasks
34
- from owned_thread import run_owned_thread, OwnedThreadGroup, owned_session_endpoint
35
- from contextlib import contextmanager
36
  from resource_work import start_resource_work, stop_resource_work, close_resource_work
37
  from shutdown_lifecycle import (prepare_application_shutdown, run_application_shutdown,
38
  shutdown_cancel_requested, ShutdownFailures)
@@ -83,31 +79,12 @@ from router_open_api import start_open_sql, stop_open_sql, close_open_sql
83
  from router_open_api import router as open_api_router # 🌐 通用开放API(第三方插件)
84
  from router_admin_plugins import router as admin_plugins_router # 🔒 管理后台 - 第三方插件管理API
85
 
86
- from database_sql import init_sql_db, get_db, SessionLocal
87
- from models_sql import SystemConfig # 🧹 P2清理:Ownership 在本文件无引用
88
 
89
  from fastapi.exceptions import RequestValidationError
90
  from starlette.exceptions import HTTPException as StarletteHTTPException
91
 
92
- MAX_CONFIG_SQL_WORK = 8
93
- _config_sql_work = OwnedThreadGroup(MAX_CONFIG_SQL_WORK,
94
- lambda: HTTPException(status_code=503, detail="service_unavailable"))
95
- _config_sql_lock = threading.Lock()
96
- start_config_sql = _config_sql_work.start
97
- stop_config_sql = _config_sql_work.stop
98
- close_config_sql = _config_sql_work.close
99
-
100
-
101
- @contextmanager
102
- def _config_sql_session():
103
- with _config_sql_lock:
104
- with SessionLocal() as session:
105
- yield session
106
-
107
-
108
- _config_endpoint = owned_session_endpoint(_config_sql_work, _config_sql_session, 'db')
109
-
110
-
111
  app = FastAPI(
112
  title="ComfyUI Ranking API",
113
  description="ComfyUI 社区排名系统 API",
@@ -518,212 +495,7 @@ def validate_resource(req_data: ValidateResourceRequest, request: Request, curre
518
  # ==========================================
519
  # 🔒 管理员:系统配置 API
520
  # ==========================================
521
- from 安全认证 import require_auth, ADMIN_ACCOUNTS
522
-
523
- def _is_admin(account: str) -> bool:
524
- """检查账号是否为管理员"""
525
- from 安全认证 import is_admin as _check_admin
526
- return _check_admin(account)
527
-
528
- # 配置存储文件
529
- SYSTEM_CONFIG_FILE = "/tmp/system_config.json" # 保留作为降级方案
530
-
531
- def _load_system_config_from_db(db: Session, *, strict: bool = False) -> dict:
532
- """从数据库加载所有系统配置"""
533
- try:
534
- configs = db.query(SystemConfig).all()
535
- result = {}
536
- for cfg in configs:
537
- try:
538
- result[cfg.config_key] = json.loads(cfg.config_value)
539
- except (json.JSONDecodeError, TypeError):
540
- result[cfg.config_key] = cfg.config_value
541
- return result
542
- except Exception as e:
543
- logger.warning(f"从数据库加载系统配置失败,降级到文件: error_type={type(e).__name__}")
544
- if strict:
545
- return _load_system_config_from_file(strict=True)
546
- return _load_system_config_from_file()
547
-
548
- def _load_system_config_from_file(*, strict: bool = False) -> dict:
549
- """降级方案;严格读取不把故障或缺失文件冒充空配置。"""
550
- if strict:
551
- with open(SYSTEM_CONFIG_FILE, "r", encoding="utf-8") as stream:
552
- config = json.load(stream)
553
- if not isinstance(config, dict):
554
- raise ValueError("invalid system configuration root")
555
- return config
556
- try:
557
- if os.path.exists(SYSTEM_CONFIG_FILE):
558
- with open(SYSTEM_CONFIG_FILE, "r") as f:
559
- return json.load(f)
560
- except Exception as e:
561
- logger.debug(f"加载系统配置文件失败: error_type={type(e).__name__}")
562
- return {}
563
-
564
- def _save_system_config_to_db(db: Session, config_key: str, value, updated_by: str = None, *, strict: bool = False):
565
- """保存单个配置项到数据库"""
566
- try:
567
- cfg = db.query(SystemConfig).filter(SystemConfig.config_key == config_key).first()
568
- if not cfg:
569
- cfg = SystemConfig(config_key=config_key)
570
- db.add(cfg)
571
- cfg.config_value = json.dumps(value, ensure_ascii=False, allow_nan=not strict)
572
- cfg.updated_by = updated_by
573
- cfg.updated_at = datetime.datetime.utcnow()
574
- db.commit()
575
- except Exception as e:
576
- if strict:
577
- # commit异常可能发生在真实提交之后;回滚不能证明尚未保存。
578
- try:
579
- db.rollback()
580
- except Exception:
581
- logger.warning("SYSTEM_CONFIG_ROLLBACK_UNCONFIRMED")
582
- raise
583
- db.rollback()
584
- logger.warning(f"保存系统配置到数据库失败,降级到文件: error_type={type(e).__name__}")
585
- # 降级:保存到文件
586
- config = _load_system_config_from_file()
587
- config[config_key] = value
588
- try:
589
- with open(SYSTEM_CONFIG_FILE, "w") as f:
590
- json.dump(config, f, ensure_ascii=False)
591
- except Exception as fe:
592
- logger.warning(f"降级保存系统配置文件也失败: error_type={type(fe).__name__}")
593
-
594
- # 兼容旧调用的包装函数
595
- def _load_system_config(db: Session = None, *, strict: bool = False) -> dict:
596
- """加载系统配置(优先数据库,降级文件);严格模式保留故障。"""
597
- if strict:
598
- if db is not None:
599
- return _load_system_config_from_db(db, strict=True)
600
- temp_db = SessionLocal()
601
- try:
602
- return _load_system_config_from_db(temp_db, strict=True)
603
- finally:
604
- temp_db.close()
605
- if db:
606
- return _load_system_config_from_db(db)
607
- # 无 db session 时(如公开端点),创建临时 session
608
- temp_db = SessionLocal()
609
- try:
610
- return _load_system_config_from_db(temp_db)
611
- except Exception:
612
- return _load_system_config_from_file()
613
- finally:
614
- temp_db.close()
615
-
616
- def _save_system_config(config: dict, db: Session = None, updated_by: str = None):
617
- """保存系统配置(优先数据库,降级文件)"""
618
- if db:
619
- for key, value in config.items():
620
- _save_system_config_to_db(db, key, value, updated_by)
621
- return
622
- # 无 db session 时,创建临时 session
623
- temp_db = SessionLocal()
624
- try:
625
- for key, value in config.items():
626
- _save_system_config_to_db(temp_db, key, value, updated_by)
627
- except Exception:
628
- pass
629
- finally:
630
- temp_db.close()
631
-
632
-
633
- @app.get("/api/admin/config/{config_key}")
634
- @_config_endpoint
635
- def get_system_config(config_key: str, current_user: str = Depends(require_auth), db: Session = Depends(get_db)):
636
- """ 🔒 获取系统配置(仅管理员) """
637
- # 🔒 权限验证
638
- if not _is_admin(current_user):
639
- raise HTTPException(status_code=403, detail="无权访问系统配置,仅管理员可操作")
640
-
641
- def reject_constant(_value):
642
- raise ValueError("invalid configuration constant")
643
- try:
644
- # 管理回读用于确认SQL保存结果,不能用另一个来源的旧值代替。
645
- storage_key = "image_moderation_enabled" if config_key == "image_moderation" else config_key
646
- row = db.query(SystemConfig).filter(SystemConfig.config_key == storage_key).first()
647
- value = None if row is None else json.loads(row.config_value, parse_constant=reject_constant)
648
- if config_key == "image_moderation":
649
- if row is None:
650
- from image_moderation import _is_moderation_enabled
651
- value = _is_moderation_enabled()
652
- if type(value) is not bool:
653
- raise ValueError("invalid moderation configuration")
654
- value = {"enabled": value}
655
- except Exception:
656
- raise HTTPException(status_code=503, detail="config_read_unavailable") from None
657
- return {"status": "success", "data": value}
658
-
659
-
660
- # 🏷️ 版本配置默认值
661
- DEFAULT_VERSION_CONFIG = {
662
- "stage": "beta",
663
- "major": 2,
664
- "minor": 0,
665
- "patch": 0
666
- }
667
-
668
-
669
- @app.get("/api/public/project-version")
670
- @_config_endpoint
671
- def get_public_project_version(db: Session = Depends(get_db)):
672
- """获取项目版本信息(公开接口,无需认证)"""
673
- config = _load_system_config(db)
674
- version_data = config.get("project_version", DEFAULT_VERSION_CONFIG)
675
- return {"status": "success", "data": version_data}
676
-
677
-
678
- @app.get("/api/public/banner_config")
679
- @_config_endpoint
680
- def get_public_banner_config(db: Session = Depends(get_db)):
681
- """公开接口:获取广告横幅配置(所有已登录/未登录用户均可访问)"""
682
- try:
683
- config = _load_system_config(db, strict=True)
684
- if not isinstance(config, dict):
685
- raise ValueError("invalid system configuration root")
686
- banner = config.get("banner_config")
687
- if banner is None:
688
- return {"status": "success", "data": None}
689
- if not isinstance(banner, dict) or type(banner.get("enabled")) is not bool:
690
- raise ValueError("invalid banner configuration")
691
- if not banner["enabled"]:
692
- return {"status": "success", "data": None}
693
- # 只投影展示字段,异常配置不能触发浏览器清除旧横幅。
694
- result = {"enabled": True}
695
- for field in ("bannerImage", "title", "description", "detailImage", "detailContent", "link"):
696
- value = banner.get(field, "")
697
- if not isinstance(value, str):
698
- raise ValueError("invalid banner display field")
699
- result[field] = value
700
- if not result["bannerImage"].strip():
701
- raise ValueError("missing banner image")
702
- return {"status": "success", "data": result}
703
- except Exception:
704
- raise HTTPException(status_code=503, detail="banner_config_unavailable") from None
705
-
706
-
707
- @app.put("/api/admin/config/{config_key}")
708
- @_config_endpoint
709
- def set_system_config(config_key: str, value: dict, current_user: str = Depends(require_auth), db: Session = Depends(get_db)):
710
- """ 🔒 设置系统配置(仅管理员) """
711
- # 🔒 权限验证
712
- if not _is_admin(current_user):
713
- raise HTTPException(status_code=403, detail="无权修改系统配置,仅管理员可操作")
714
- storage_key = config_key
715
- storage_value = value
716
- if config_key == "image_moderation":
717
- if set(value) != {"enabled"} or type(value["enabled"]) is not bool:
718
- raise HTTPException(status_code=400, detail="invalid_moderation_configuration")
719
- storage_key = "image_moderation_enabled"
720
- storage_value = value["enabled"]
721
- try:
722
- _save_system_config_to_db(db, storage_key, storage_value, updated_by=current_user, strict=True)
723
- except Exception:
724
- raise HTTPException(status_code=503, detail="config_save_unconfirmed") from None
725
- return {"status": "success", "message": "配置已更新"}
726
-
727
 
728
  # ==========================================
729
  # 🖥️ 管理界面静态文件挂载
 
7
  from response_privacy import PrivateResponseCacheControl
8
  from fastapi.middleware.cors import CORSMiddleware
9
  from fastapi.responses import Response, JSONResponse, FileResponse
 
10
  from pydantic import BaseModel
11
  from huggingface_hub import hf_hub_download, HfApi
12
  import uuid
 
14
  import urllib.request
15
  import urllib.error
16
  import os
 
17
  import logging
18
  import time
 
19
  import threading
20
  import 数据库连接 as db
21
  import asyncio
 
28
  from script_execution import start_admin_scripts, stop_admin_scripts, close_admin_scripts
29
  from comment_store import start_comment_reads, stop_comment_reads, close_comment_reads
30
  from periodic_tasks import start_periodic_task, request_periodic_stop, wait_periodic_tasks
31
+ from owned_thread import run_owned_thread
 
32
  from resource_work import start_resource_work, stop_resource_work, close_resource_work
33
  from shutdown_lifecycle import (prepare_application_shutdown, run_application_shutdown,
34
  shutdown_cancel_requested, ShutdownFailures)
 
79
  from router_open_api import router as open_api_router # 🌐 通用开放API(第三方插件)
80
  from router_admin_plugins import router as admin_plugins_router # 🔒 管理后台 - 第三方插件管理API
81
 
82
+ from database_sql import init_sql_db
83
+ from router_system_config import (router as system_config_router, start_config_sql, stop_config_sql, close_config_sql)
84
 
85
  from fastapi.exceptions import RequestValidationError
86
  from starlette.exceptions import HTTPException as StarletteHTTPException
87
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  app = FastAPI(
89
  title="ComfyUI Ranking API",
90
  description="ComfyUI 社区排名系统 API",
 
495
  # ==========================================
496
  # 🔒 管理员:系统配置 API
497
  # ==========================================
498
+ app.include_router(system_config_router)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
 
500
  # ==========================================
501
  # 🖥️ 管理界面静态文件挂载
router_system_config.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from contextlib import contextmanager
2
+ import datetime
3
+ import json
4
+ import logging
5
+ import os
6
+ import threading
7
+
8
+ from fastapi import APIRouter, Depends, HTTPException
9
+ from sqlalchemy.orm import Session
10
+ from database_sql import get_db, SessionLocal
11
+ from models_sql import SystemConfig
12
+ from owned_thread import OwnedThreadGroup, owned_session_endpoint
13
+ from 安全认证 import require_auth
14
+
15
+ logger = logging.getLogger("ComfyUI-Ranking")
16
+ router = APIRouter()
17
+
18
+ MAX_CONFIG_SQL_WORK = 8
19
+ _config_sql_work = OwnedThreadGroup(MAX_CONFIG_SQL_WORK,
20
+ lambda: HTTPException(status_code=503, detail="service_unavailable"))
21
+ _config_sql_lock = threading.Lock()
22
+ start_config_sql = _config_sql_work.start
23
+ stop_config_sql = _config_sql_work.stop
24
+ close_config_sql = _config_sql_work.close
25
+
26
+
27
+ @contextmanager
28
+ def _config_sql_session():
29
+ with _config_sql_lock:
30
+ with SessionLocal() as session:
31
+ yield session
32
+
33
+
34
+ _config_endpoint = owned_session_endpoint(_config_sql_work, _config_sql_session, 'db')
35
+
36
+
37
+ def _is_admin(account: str) -> bool:
38
+ """检查账号是否为管理员"""
39
+ from 安全认证 import is_admin as _check_admin
40
+ return _check_admin(account)
41
+
42
+ # 配置存储文件
43
+ SYSTEM_CONFIG_FILE = "/tmp/system_config.json" # 保留作为降级方案
44
+
45
+ def _load_system_config_from_db(db: Session, *, strict: bool = False) -> dict:
46
+ """从数据库加载所有系统配置"""
47
+ try:
48
+ configs = db.query(SystemConfig).all()
49
+ result = {}
50
+ for cfg in configs:
51
+ try:
52
+ result[cfg.config_key] = json.loads(cfg.config_value)
53
+ except (json.JSONDecodeError, TypeError):
54
+ result[cfg.config_key] = cfg.config_value
55
+ return result
56
+ except Exception as e:
57
+ logger.warning(f"从数据库加载系统配置失败,降级到文件: error_type={type(e).__name__}")
58
+ if strict:
59
+ return _load_system_config_from_file(strict=True)
60
+ return _load_system_config_from_file()
61
+
62
+ def _load_system_config_from_file(*, strict: bool = False) -> dict:
63
+ """降级方案;严格读取不把故障或缺失文件冒充空配置。"""
64
+ if strict:
65
+ with open(SYSTEM_CONFIG_FILE, "r", encoding="utf-8") as stream:
66
+ config = json.load(stream)
67
+ if not isinstance(config, dict):
68
+ raise ValueError("invalid system configuration root")
69
+ return config
70
+ try:
71
+ if os.path.exists(SYSTEM_CONFIG_FILE):
72
+ with open(SYSTEM_CONFIG_FILE, "r") as f:
73
+ return json.load(f)
74
+ except Exception as e:
75
+ logger.debug(f"加载系统配置文件失败: error_type={type(e).__name__}")
76
+ return {}
77
+
78
+ def _save_system_config_to_db(db: Session, config_key: str, value, updated_by: str = None, *, strict: bool = False):
79
+ """保存单个配置项到数据库"""
80
+ try:
81
+ cfg = db.query(SystemConfig).filter(SystemConfig.config_key == config_key).first()
82
+ if not cfg:
83
+ cfg = SystemConfig(config_key=config_key)
84
+ db.add(cfg)
85
+ cfg.config_value = json.dumps(value, ensure_ascii=False, allow_nan=not strict)
86
+ cfg.updated_by = updated_by
87
+ cfg.updated_at = datetime.datetime.utcnow()
88
+ db.commit()
89
+ except Exception as e:
90
+ if strict:
91
+ # commit异常可能发生在真实提交之后;回滚不能证明尚未保存。
92
+ try:
93
+ db.rollback()
94
+ except Exception:
95
+ logger.warning("SYSTEM_CONFIG_ROLLBACK_UNCONFIRMED")
96
+ raise
97
+ db.rollback()
98
+ logger.warning(f"保存系统配置到数据库失败,降级到文件: error_type={type(e).__name__}")
99
+ # 降级:保存到文件
100
+ config = _load_system_config_from_file()
101
+ config[config_key] = value
102
+ try:
103
+ with open(SYSTEM_CONFIG_FILE, "w") as f:
104
+ json.dump(config, f, ensure_ascii=False)
105
+ except Exception as fe:
106
+ logger.warning(f"降级保存系统配置文件也失败: error_type={type(fe).__name__}")
107
+
108
+ # 兼容旧调用的包装函数
109
+ def _load_system_config(db: Session = None, *, strict: bool = False) -> dict:
110
+ """加载系统配置(优先数据库,降级文件);严格模式保留故障。"""
111
+ if strict:
112
+ if db is not None:
113
+ return _load_system_config_from_db(db, strict=True)
114
+ temp_db = SessionLocal()
115
+ try:
116
+ return _load_system_config_from_db(temp_db, strict=True)
117
+ finally:
118
+ temp_db.close()
119
+ if db:
120
+ return _load_system_config_from_db(db)
121
+ # 无 db session 时(如公开端点),创建临时 session
122
+ temp_db = SessionLocal()
123
+ try:
124
+ return _load_system_config_from_db(temp_db)
125
+ except Exception:
126
+ return _load_system_config_from_file()
127
+ finally:
128
+ temp_db.close()
129
+
130
+ def _save_system_config(config: dict, db: Session = None, updated_by: str = None):
131
+ """保存系统配置(优先数据库,降级文件���"""
132
+ if db:
133
+ for key, value in config.items():
134
+ _save_system_config_to_db(db, key, value, updated_by)
135
+ return
136
+ # 无 db session 时,创建临时 session
137
+ temp_db = SessionLocal()
138
+ try:
139
+ for key, value in config.items():
140
+ _save_system_config_to_db(temp_db, key, value, updated_by)
141
+ except Exception:
142
+ pass
143
+ finally:
144
+ temp_db.close()
145
+
146
+
147
+ @router.get("/api/admin/config/{config_key}")
148
+ @_config_endpoint
149
+ def get_system_config(config_key: str, current_user: str = Depends(require_auth), db: Session = Depends(get_db)):
150
+ """ 🔒 获取系统配置(仅管理员) """
151
+ # 🔒 权限验证
152
+ if not _is_admin(current_user):
153
+ raise HTTPException(status_code=403, detail="无权访问系统配置,仅管理员可操作")
154
+
155
+ def reject_constant(_value):
156
+ raise ValueError("invalid configuration constant")
157
+ try:
158
+ # 管理回读用于确认SQL保存结果,不能用另一个来源的旧值代替。
159
+ storage_key = "image_moderation_enabled" if config_key == "image_moderation" else config_key
160
+ row = db.query(SystemConfig).filter(SystemConfig.config_key == storage_key).first()
161
+ value = None if row is None else json.loads(row.config_value, parse_constant=reject_constant)
162
+ if config_key == "image_moderation":
163
+ if row is None:
164
+ from image_moderation import _is_moderation_enabled
165
+ value = _is_moderation_enabled()
166
+ if type(value) is not bool:
167
+ raise ValueError("invalid moderation configuration")
168
+ value = {"enabled": value}
169
+ except Exception:
170
+ raise HTTPException(status_code=503, detail="config_read_unavailable") from None
171
+ return {"status": "success", "data": value}
172
+
173
+
174
+ # 🏷️ 版本配置默认值
175
+ DEFAULT_VERSION_CONFIG = {
176
+ "stage": "beta",
177
+ "major": 2,
178
+ "minor": 0,
179
+ "patch": 0
180
+ }
181
+
182
+
183
+ @router.get("/api/public/project-version")
184
+ @_config_endpoint
185
+ def get_public_project_version(db: Session = Depends(get_db)):
186
+ """获取项目版本信息(公开接口,无需认证)"""
187
+ config = _load_system_config(db)
188
+ version_data = config.get("project_version", DEFAULT_VERSION_CONFIG)
189
+ return {"status": "success", "data": version_data}
190
+
191
+
192
+ @router.get("/api/public/banner_config")
193
+ @_config_endpoint
194
+ def get_public_banner_config(db: Session = Depends(get_db)):
195
+ """公开接口:获取广告横幅配置(所有已登录/未登录用户均可访问)"""
196
+ try:
197
+ config = _load_system_config(db, strict=True)
198
+ if not isinstance(config, dict):
199
+ raise ValueError("invalid system configuration root")
200
+ banner = config.get("banner_config")
201
+ if banner is None:
202
+ return {"status": "success", "data": None}
203
+ if not isinstance(banner, dict) or type(banner.get("enabled")) is not bool:
204
+ raise ValueError("invalid banner configuration")
205
+ if not banner["enabled"]:
206
+ return {"status": "success", "data": None}
207
+ # 只投影展示字段,异常配置不能触发浏览器清除旧横幅。
208
+ result = {"enabled": True}
209
+ for field in ("bannerImage", "title", "description", "detailImage", "detailContent", "link"):
210
+ value = banner.get(field, "")
211
+ if not isinstance(value, str):
212
+ raise ValueError("invalid banner display field")
213
+ result[field] = value
214
+ if not result["bannerImage"].strip():
215
+ raise ValueError("missing banner image")
216
+ return {"status": "success", "data": result}
217
+ except Exception:
218
+ raise HTTPException(status_code=503, detail="banner_config_unavailable") from None
219
+
220
+
221
+ @router.put("/api/admin/config/{config_key}")
222
+ @_config_endpoint
223
+ def set_system_config(config_key: str, value: dict, current_user: str = Depends(require_auth), db: Session = Depends(get_db)):
224
+ """ 🔒 设置系统配置(仅管理员) """
225
+ # 🔒 权限验证
226
+ if not _is_admin(current_user):
227
+ raise HTTPException(status_code=403, detail="无权修改系统配置,仅管理员可操作")
228
+ storage_key = config_key
229
+ storage_value = value
230
+ if config_key == "image_moderation":
231
+ if set(value) != {"enabled"} or type(value["enabled"]) is not bool:
232
+ raise HTTPException(status_code=400, detail="invalid_moderation_configuration")
233
+ storage_key = "image_moderation_enabled"
234
+ storage_value = value["enabled"]
235
+ try:
236
+ _save_system_config_to_db(db, storage_key, storage_value, updated_by=current_user, strict=True)
237
+ except Exception:
238
+ raise HTTPException(status_code=503, detail="config_save_unconfirmed") from None
239
+ return {"status": "success", "message": "配置已更新"}
240
+
241
+