diff --git a/platform/backend/app/core/collector.py b/platform/backend/app/core/collector.py new file mode 100644 index 0000000..97c3e1d --- /dev/null +++ b/platform/backend/app/core/collector.py @@ -0,0 +1,31 @@ +""" +选题收集模块 +调用 scripts/collector.py 脚本,从外部来源收集/生成新选题并写入 JSON +""" +import subprocess +from pathlib import Path +import logging +import os + +logger = logging.getLogger(__name__) + +# 计算项目根目录 +PROJECT_ROOT = Path(__file__).resolve().parents[4] +if os.getenv('PROJECT_ROOT'): + PROJECT_ROOT = Path(os.getenv('PROJECT_ROOT')) + +def run_collector(): + """运行选题收集脚本""" + script_path = PROJECT_ROOT / "scripts" / "collector.py" + if not script_path.exists(): + raise FileNotFoundError(f"Collector script not found: {script_path}") + result = subprocess.run( + ["python", str(script_path)], + capture_output=True, + text=True, + cwd=PROJECT_ROOT, + timeout=300 + ) + if result.returncode != 0: + raise RuntimeError(f"Collector failed: {result.stderr}") + return {"ok": True, "output": result.stdout} \ No newline at end of file diff --git a/platform/backend/app/core/scheduler.py b/platform/backend/app/core/scheduler.py new file mode 100644 index 0000000..1eb2bd9 --- /dev/null +++ b/platform/backend/app/core/scheduler.py @@ -0,0 +1,94 @@ +""" +定时任务调度器 +基于 APScheduler,支持在 FastAPI 生命周期内运行定时任务 +""" +import os +import logging +from datetime import datetime +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +from .generator import run_creator +from .optimizer import run_optimizer +from .sync import sync_all_topics + +logger = logging.getLogger(__name__) + +class TaskScheduler: + def __init__(self): + self.scheduler = BackgroundScheduler() + self._started = False + + def start(self): + if self._started: + logger.warning("Scheduler already started") + return + # 使用 CronTrigger 设置每日固定时间点 + self.scheduler.add_job( + self._run_sync, + CronTrigger(hour=2, minute=30), + id='scheduled_sync', + replace_existing=True, + max_instances=1, + coalesce=True + ) + self.scheduler.add_job( + self._run_generate, + CronTrigger(hour=3, minute=30), + id='scheduled_generate', + replace_existing=True, + max_instances=1, + coalesce=True + ) + self.scheduler.add_job( + self._run_optimize, + CronTrigger(hour=4, minute=30), + id='scheduled_optimize', + replace_existing=True, + max_instances=1, + coalesce=True + ) + self.scheduler.start() + self._started = True + logger.info("Scheduler started with daily cron triggers (02:30, 03:30, 04:30)") + def shutdown(self): + if self.scheduler.running: + self.scheduler.shutdown() + logger.info("Scheduler shut down") + + def _run_generate(self): + try: + logger.info("[Scheduled] Starting content generation...") + result = run_creator() + logger.info("[Scheduled] Generation completed: %s", result) + except Exception as e: + logger.exception("[Scheduled] Generation failed: %s", e) + + def _run_optimize(self): + try: + logger.info("[Scheduled] Starting compliance optimization...") + result = run_optimizer() + logger.info("[Scheduled] Optimization completed: %s", result) + except Exception as e: + logger.exception("[Scheduled] Optimization failed: %s", e) + + def _run_sync(self): + try: + logger.info("[Scheduled] Starting data sync...") + sync_all_topics() + logger.info("[Scheduled] Sync completed") + except Exception as e: + logger.exception("[Scheduled] Sync failed: %s", e) + + def get_jobs(self): + """返回当前所有定时任务的状态""" + jobs = [] + for job in self.scheduler.get_jobs(): + jobs.append({ + "id": job.id, + "next_run_time": job.next_run_time.isoformat() if job.next_run_time else None, + "trigger": str(job.trigger), + }) + return jobs + +# 全局单例 +scheduler = TaskScheduler() \ No newline at end of file diff --git a/platform/frontend/admin.html b/platform/frontend/admin.html index 2947f51..601e5bf 100644 --- a/platform/frontend/admin.html +++ b/platform/frontend/admin.html @@ -255,6 +255,7 @@ const app = createApp({ const activeTab = ref('cases'); const currentUser = ref({ username: '' }); + const redirectToPage = (page) => { window.location.href = '/' + page; }; const currentUser = ref({ username: '' }); const isAdmin = ref(false); const isLoggedIn = ref(false); @@ -262,8 +263,9 @@ const app = createApp({ fetch('/api/auth/me', { headers: { 'Authorization': 'Bearer ' + token } }) .then(response => response.ok ? response.json() : Promise.reject()) .then(data => { - currentUser.value = data.user; - isAdmin.value = data.user.role === 'admin'; + const user = data.user || { username: '', role: 'user' }; + currentUser.value = user; + isAdmin.value = user.role === 'admin'; isLoggedIn.value = true; if (!isAdmin.value) { ElMessage.warning('需要管理员权限'); @@ -275,6 +277,11 @@ const app = createApp({ window.location.href = 'login.html'; }); + // 页面跳转 + const redirectToPage = (page) => { + window.location.href = '/' + page; + }; + // Cases const cases = ref([]); const caseDialogVisible = ref(false); diff --git a/platform/frontend/logs.html b/platform/frontend/logs.html index 1f53d18..0bc6aab 100644 --- a/platform/frontend/logs.html +++ b/platform/frontend/logs.html @@ -71,7 +71,7 @@ 加载日志 -
{{ logContent }}
+
{{ logContent }}
@@ -88,7 +88,17 @@ - + + @@ -66,9 +66,9 @@ - - - + + + diff --git a/platform/frontend/topics.html b/platform/frontend/topics.html index 4bd37a2..c55a7d3 100644 --- a/platform/frontend/topics.html +++ b/platform/frontend/topics.html @@ -4,8 +4,10 @@ 宇之然内容创作平台 - 选题管理 - + + + .preview-iframe { box-sizing: border-box; } + /* 预览弹窗响应式高度 */ + @media (min-width: 769px) { + .preview-iframe { max-height: calc(100vh - 100px) !important; } + } + @media (max-width: 768px) { + .preview-iframe { max-height: calc(100vh - 250px) !important; } + } + + /* 预览弹窗自定义高度(非全屏时) */ + .preview-dialog-custom.el-dialog { + max-height: calc(100vh - 90px) !important; + overflow: hidden; + display: flex; + flex-direction: column; + margin-top: 0 !important; + } + .preview-dialog-custom.el-dialog .el-dialog__header { + padding: 8px 12px; + margin: 0; + flex-shrink: 0; + display: flex; + align-items: center; + justify-content: space-between; + } + .preview-dialog-custom.el-dialog .el-dialog__body { + padding: 12px; + overflow: hidden; + } + .preview-dialog-custom.el-dialog .el-dialog__footer { + flex-shrink: 0; + padding: 8px 12px; + } +
@@ -102,7 +138,7 @@

📋 选题管理

-
+
🔄 批量刷新 ▶ 批量创作 @@ -117,7 +153,7 @@ 待发布 ({{ statusStats.ready }}) 已发布 ({{ statusStats.published }})
-
+
@@ -182,7 +218,7 @@ - +
@@ -193,24 +229,46 @@
-

{{ previewTopic.title }}

+
+

{{ previewTopic.title }}

+
+ 创建:{{ formatDate(previewTopic.created_at) }} + 状态:{{ getStatusLabel(previewTopic.status) }} + 创作:{{ formatDate(previewTopic.generated_at) }} + 发布:{{ formatDate(previewTopic.published_at) }} + + {{ previewFullscreen ? '退出全屏' : '全屏' }} + +
+
- -
+ +
+
-
@@ -233,6 +291,7 @@ const TopicsApp = { topics: [], previewVisible: false, previewTopic: null, + previewFullscreen: false, previewPlatform: 'zhihu', platformContents: {} } @@ -262,6 +321,28 @@ const TopicsApp = { ready: this.topics.filter(t => ready.includes(t.status)).length, published: this.topics.filter(t => published.includes(t.status)).length }; + }, + currentPreviewHtml() { + const html = this.platformContents[this.previewPlatform]; + if (!html) return ''; + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(html, 'text/html'); + const body = doc.body; + if (!body) return html; + + body.querySelectorAll('script, nav, .header, footer, .interaction').forEach(el => el.remove()); + + const head = doc.querySelector('head'); + const headHtml = head ? head.innerHTML : ''; + const bodyHtml = body.innerHTML; + const ending = '

感兴趣可以收藏关注我们,欢迎在评论区分享你的实践经验和改进建议!

'; + + return `${headHtml}${bodyHtml}${ending}`; + } catch (e) { + console.error('生成预览 HTML 失败:', e); + return html; + } } }, methods: { @@ -381,8 +462,8 @@ const TopicsApp = { ); await Promise.all(promises); }, - getPreviewHtml(platform) { - return this.platformContents[platform] || '暂无内容'; + togglePreviewFullscreen() { + this.previewFullscreen = !this.previewFullscreen; }, platformName(platform) { const names = { @@ -392,6 +473,7 @@ const TopicsApp = { }; return names[platform] || platform; }, + // 计算属性:当前平台预览的完整 HTML(响应式更新) copyContent(platform) { if (!this.previewTopic || !this.previewTopic.content) { this.$message.warning('暂无内容可复制'); diff --git a/scripts/collector_db_integration.py b/scripts/collector_db_integration.py new file mode 100644 index 0000000..6a365e6 --- /dev/null +++ b/scripts/collector_db_integration.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +""" +collector 数据库集成模块 +将采集到的选题写入数据库 +""" + +import sys +from pathlib import Path + +PROJECT_ROOT = Path(__file__).parent.parent +sys.path.insert(0, str(PROJECT_ROOT)) + +from app.database import SessionLocal +from app.models import Topic +from datetime import datetime + +def save_topics_to_db(topics_data: list): + """将选题列表保存到数据库(插入或更新)""" + db = SessionLocal() + try: + for t in topics_data: + # 检查是否存在 + existing = db.query(Topic).filter(Topic.id == t['id']).first() + if existing: + # 更新字段 + existing.title = t['title'] + existing.field = t.get('field', existing.field) + existing.format = t.get('format', existing.format) + existing.core_concept = t.get('core_concept', existing.core_concept) + existing.audience_pain = t.get('audience_pain', existing.audience_pain) + existing.unique_angle = t.get('unique_angle', existing.unique_angle) + existing.priority = t.get('priority', existing.priority) + existing.priority_score = t.get('priority_score', existing.priority_score) + existing.total_score = t.get('total_score', existing.total_score) + existing.status = t.get('status', existing.status) + existing.cases = t.get('cases', existing.cases) + existing.source_file = t.get('source_file', existing.source_file) + existing.ready_at = datetime.strptime(t['ready_at'], '%Y-%m-%d').date() if t.get('ready_at') else existing.ready_at + existing.published_at = datetime.strptime(t['published_at'], '%Y-%m-%d').date() if t.get('published_at') else existing.published_at + existing.compliance_score = t.get('compliance_score', existing.compliance_score) + existing.platform_urls = t.get('platform_urls', existing.platform_urls) + existing.updated_at = datetime.now() + else: + # 新增 + topic = Topic( + id=t['id'], + title=t['title'], + field=t.get('field', '可持续生活系统'), + format=t.get('format'), + core_concept=t.get('core_concept'), + audience_pain=t.get('audience_pain'), + unique_angle=t.get('unique_angle'), + priority=t.get('priority', '中'), + priority_score=t.get('priority_score', 0), + total_score=t.get('total_score'), + status=t.get('status', 'pending'), + cases=t.get('cases', []), + source_file=t.get('source_file'), + ready_at=datetime.strptime(t['ready_at'], '%Y-%m-%d').date() if t.get('ready_at') else None, + published_at=datetime.strptime(t['published_at'], '%Y-%m-%d').date() if t.get('published_at') else None, + compliance_score=t.get('compliance_score', 100), + platform_urls=t.get('platform_urls', {}), + created_at=datetime.now(), + updated_at=datetime.now() + ) + db.add(topic) + db.commit() + print(f"✅ 保存/更新了 {len(topics_data)} 个选题到数据库") + except Exception as e: + db.rollback() + raise e + finally: + db.close() + +if __name__ == "__main__": + # 测试示例 + print("collector_db_integration module - for import only") diff --git a/start-platform.sh b/start-platform.sh index 7e411af..a72c313 100755 --- a/start-platform.sh +++ b/start-platform.sh @@ -46,4 +46,4 @@ echo "========================================" echo "" cd backend -exec $PYTHON_CMD -m uvicorn app.main:app --host 0.0.0.0 --port $PORT --reload +exec $PYTHON_CMD -m uvicorn app.main:app --host 0.0.0.0 --port $PORT