From e51dc18d06efe17c08bfc2a7768f579bb53ed8d2 Mon Sep 17 00:00:00 2001 From: HuangHai <10402852@qq.com> Date: Wed, 21 Jan 2026 08:41:47 +0800 Subject: [PATCH] 'commit' --- Controller/DouYinController.py | 612 ++++++++---------- Controller/HaiBaoController.py | 65 +- Controller/YltAnalyticsController.py | 130 ++-- .../DouYinController.cpython-310.pyc | Bin 10262 -> 7587 bytes .../HaiBaoController.cpython-310.pyc | Bin 8776 -> 7400 bytes .../YltAnalyticsController.cpython-310.pyc | Bin 15806 -> 16006 bytes DbKit/Db.py | 40 +- DbKit/Sql/DouYin.sql | 57 ++ DbKit/Sql/HaiBao.sql | 12 + DbKit/Sql/YltAnalytics.sql | 79 +++ DbKit/SqlTemplateLoader.py | 4 + DbKit/__pycache__/Db.cpython-310.pyc | Bin 29677 -> 30536 bytes .../SqlTemplateLoader.cpython-310.pyc | Bin 13878 -> 13899 bytes Model/DouYinModel.py | 79 +++ Model/HaiBaoModel.py | 24 + Model/YltAnalyticsModel.py | 29 + Model/__pycache__/DouYinModel.cpython-310.pyc | Bin 0 -> 2863 bytes Model/__pycache__/HaiBaoModel.cpython-310.pyc | Bin 0 -> 1078 bytes .../YltAnalyticsModel.cpython-310.pyc | Bin 2071 -> 3690 bytes Start.py | 2 + Tools/T6_Export.py | 51 +- Tools/__pycache__/T6_Export.cpython-310.pyc | Bin 14994 -> 13960 bytes Util/RedisKit.py | 38 +- Util/__pycache__/RedisKit.cpython-310.pyc | Bin 3493 -> 4536 bytes clear_sql_cache.py | 35 + static/css/dashboard.css | 105 ++- static/css/douyin.css | 57 +- static/{HaiBao/css/app.css => css/haibao.css} | 0 static/css/query.css | 91 ++- static/{HaiBao => }/css/scheme.css | 0 static/dashboard.html | 27 +- static/douyin.html | 4 + static/{HaiBao/index.html => haibao.html} | 16 +- static/index.html | 2 +- static/index_old.html | 352 ---------- static/js/dashboard.js | 153 ++++- static/js/douyin.js | 93 ++- static/{HaiBao/js/app.js => js/haibao.js} | 0 static/js/query.js | 114 +++- static/query.html | 3 + 40 files changed, 1309 insertions(+), 965 deletions(-) create mode 100644 DbKit/Sql/DouYin.sql create mode 100644 DbKit/Sql/HaiBao.sql create mode 100644 DbKit/Sql/YltAnalytics.sql create mode 100644 Model/DouYinModel.py create mode 100644 Model/HaiBaoModel.py create mode 100644 Model/__pycache__/DouYinModel.cpython-310.pyc create mode 100644 Model/__pycache__/HaiBaoModel.cpython-310.pyc create mode 100644 clear_sql_cache.py rename static/{HaiBao/css/app.css => css/haibao.css} (100%) rename static/{HaiBao => }/css/scheme.css (100%) rename static/{HaiBao/index.html => haibao.html} (93%) delete mode 100644 static/index_old.html rename static/{HaiBao/js/app.js => js/haibao.js} (100%) diff --git a/Controller/DouYinController.py b/Controller/DouYinController.py index 8109091..29fad06 100644 --- a/Controller/DouYinController.py +++ b/Controller/DouYinController.py @@ -7,357 +7,263 @@ import asyncio from datetime import datetime from typing import List, Optional -from fastapi import APIRouter, HTTPException, BackgroundTasks -from fastapi.responses import StreamingResponse -from pydantic import BaseModel -import pymysql - -# Import custom modules -from Config.Config import OBS_CLOUD_PREFIX, OBS_BUCKET, OBS_TMP_PREFIX, DORIS_HOST, DORIS_PORT, DORIS_USER, DORIS_PWD, DORIS_DATABASE, OBS_SERVER -from Util.DouYinDownloader import DouYinDownloader -from Util.ObsUtil import ObsUploader -from Util.ASRClient import ASRClient -from Util.LlmUtil import get_llm_response - -# Logger setup -logger = logging.getLogger(__name__) - -router = APIRouter() - -# Database connection -def get_db_connection(): - return pymysql.connect( - host=DORIS_HOST, - port=DORIS_PORT, - user=DORIS_USER, - password=DORIS_PWD, - database=DORIS_DATABASE, - charset='utf8mb4', - cursorclass=pymysql.cursors.DictCursor - ) - -class ParseRequest(BaseModel): - text: str - -class SummaryRequest(BaseModel): - ids: List[str] = [] - -def update_status(id, status, error_msg=None): - try: - conn = get_db_connection() - cursor = conn.cursor() - if error_msg: - sql = "UPDATE t_douyin_record SET status=%s, error_msg=%s WHERE id=%s" - cursor.execute(sql, (status, error_msg, id)) - else: - sql = "UPDATE t_douyin_record SET status=%s WHERE id=%s" - cursor.execute(sql, (status, id)) - conn.commit() - conn.close() - except Exception as e: - logger.error(f"DB Error update_status: {e}") - -def update_record(id, title, obs_url, transcript, status): - try: - # Truncate title to 100 chars to fit DB schema (approx 400 bytes max for utf8mb4) - if title and len(title) > 100: - title = title[:100] + "..." - - conn = get_db_connection() - cursor = conn.cursor() - sql = """ - UPDATE t_douyin_record - SET video_name=%s, obs_url=%s, transcript=%s, status=%s - WHERE id=%s - """ - cursor.execute(sql, (title, obs_url, transcript, status, id)) - conn.commit() - conn.close() - except Exception as e: - logger.error(f"DB Error update_record: {e}") - -async def process_video_task(url: str, request_id: str, share_text: str = ""): - logger.info(f"Processing task {request_id}") - - # 1. Update status - await asyncio.to_thread(update_status, request_id, "PROCESSING") - - temp_dir = os.path.abspath(f"temp_{request_id}") - try: - if not os.path.exists(temp_dir): - os.makedirs(temp_dir) - - # 2. Parse & Download - downloader = DouYinDownloader() - # url is passed directly now - if not url: - raise Exception("No valid URL found") - - logger.info(f"Downloading from {url}") - # Run download in thread to avoid blocking main loop - local_video_path, title = await asyncio.to_thread(downloader.download_video, url, temp_dir) - - # Title handling strategy: - # Priority 1: Extracted from share text (if available and valid) - # Priority 2: Extracted from video download (often "Unknown Title") - # Priority 3: Generated by LLM (done later) - - extracted_title = downloader.extract_title_from_text(share_text) - logger.info(f"Extracted title from text: {extracted_title}") - - # If we have a valid extracted title, use it. - # But if we don't have a title yet (or it's Unknown), we definitely want to use extracted_title. - # Even if we have a title from yt-dlp, if it's just "Unknown Title", we prefer extracted one. - if extracted_title and extracted_title != "Unknown Title": - title = extracted_title - elif not title: - title = "Unknown Title" - - if not local_video_path or not os.path.exists(local_video_path): - raise Exception("Download failed") - - # 3. Upload Video to OBS (Long term storage) - logger.info("Uploading video to OBS...") - uploader = ObsUploader() - video_filename = os.path.basename(local_video_path) - obs_video_key = f"{OBS_CLOUD_PREFIX}/DouYin/{video_filename}" - - success, _ = await asyncio.to_thread(uploader.upload_file, obs_video_key, local_video_path, OBS_BUCKET) - if not success: - raise Exception("OBS Upload failed") - - # Construct public URL (Assuming standard OBS pattern or Config logic) - obs_url = f"https://{OBS_BUCKET}.{OBS_SERVER}/{obs_video_key}" - - # 4. Convert to MP3 - logger.info("Converting to MP3...") - mp3_path = os.path.splitext(local_video_path)[0] + ".mp3" - cmd = [ - "ffmpeg", "-y", "-i", local_video_path, - "-acodec", "libmp3lame", "-ar", "16000", "-ac", "1", "-q:a", "2", - mp3_path - ] - # Run ffmpeg in thread - result = await asyncio.to_thread(subprocess.run, cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) - if result.returncode != 0: - raise Exception(f"FFmpeg failed: {result.stderr.decode()}") - - # 5. ASR (Upload MP3 to tmp and transcribe) - logger.info("Transcribing...") - asr = ASRClient() - # Run ASR in thread - transcript = await asyncio.to_thread(asr.upload_and_transcribe_sync, mp3_path) - - if not transcript: - raise Exception("Transcription failed (returned empty)") - - # 6. LLM Title Generation (Enhancement) - # If the title is still Unknown or weak, OR if we just want to ensure we have a good title. - # The user said: "Alternatively, call LlmUtil.py to summarize title". - # Let's do it if title is Unknown or matches default filename pattern, OR if extracted title was also missing. - if (not title or title == "Unknown Title" or title == "Unknown"): - try: - logger.info("Generating title from transcript via LLM...") - prompt = f"请根据以下视频文案总结一个简短的标题(20字以内),不要包含任何解释性文字,直接返回标题:\n\n{transcript[:1000]}" - - llm_title_chunks = [] - # get_llm_response is already async - async for chunk in get_llm_response(prompt, stream=False): - llm_title_chunks.append(chunk) - llm_title = "".join(llm_title_chunks) - - if llm_title: - # Clean up quotes if any - llm_title = llm_title.strip().strip('"').strip('“').strip('”') - logger.info(f"LLM generated title: {llm_title}") - # We overwrite the title if LLM succeeds - title = llm_title - except Exception as llm_e: - logger.warning(f"LLM Title generation failed: {llm_e}") - - # 7. Save to DB (Update) - logger.info("Saving to DB...") - await asyncio.to_thread(update_record, request_id, title, obs_url, transcript, "COMPLETED") - logger.info(f"Task {request_id} completed successfully.") - - except Exception as e: - logger.error(f"Task {request_id} failed: {e}", exc_info=True) - await asyncio.to_thread(update_status, request_id, "FAILED", str(e)) - finally: - # 8. Cleanup - if os.path.exists(temp_dir): - try: - # shutil.rmtree is sync, wrap it - await asyncio.to_thread(shutil.rmtree, temp_dir, ignore_errors=True) - except Exception as e: - logger.error(f"Cleanup failed: {e}") - -@router.post("/api/parse") -def parse(request: ParseRequest, background_tasks: BackgroundTasks): - downloader = DouYinDownloader() - urls = downloader.extract_urls(request.text) - - if not urls: - # If no URLs found, try using the text as is (might be a direct link not caught by regex) - # But regex is quite broad. Let's just fail or try one. - # Let's assume text might be the URL if it's clean. - if request.text.startswith("http"): - urls = [request.text] - else: - raise HTTPException(status_code=400, detail="No valid URLs found") - - created_ids = [] - try: - conn = get_db_connection() - cursor = conn.cursor() - - for url in urls: - req_id = str(uuid.uuid4()) - sql = """ - INSERT INTO t_douyin_record (id, original_text, status, create_time) - VALUES (%s, %s, 'PENDING', %s) - """ - cursor.execute(sql, (req_id, url, datetime.now())) - created_ids.append(req_id) - # Pass request.text (the full share text) so we can extract title from it - background_tasks.add_task(process_video_task, url, req_id, request.text) - - conn.commit() - conn.close() - except Exception as e: - raise HTTPException(status_code=500, detail=f"DB Init Error: {e}") - - return {"id": created_ids[0] if created_ids else None, "ids": created_ids, "status": "PENDING"} - -@router.get("/api/records") -def get_records(): - try: - conn = get_db_connection() - cursor = conn.cursor() - cursor.execute("SELECT * FROM t_douyin_record ORDER BY create_time DESC LIMIT 50") - records = cursor.fetchall() - conn.close() - - # Manually handle datetime serialization to be safe - for r in records: - if 'create_time' in r and r['create_time']: - r['create_time'] = r['create_time'].strftime("%Y-%m-%d %H:%M:%S") - if 'update_time' in r and r['update_time']: - r['update_time'] = r['update_time'].strftime("%Y-%m-%d %H:%M:%S") - - return records - except Exception as e: - logger.error(f"Get records error: {e}", exc_info=True) - return [] - -@router.delete("/api/records/{id}") -def delete_record(id: str): - try: - conn = get_db_connection() - cursor = conn.cursor() - cursor.execute("DELETE FROM t_douyin_record WHERE id=%s", (id,)) - conn.commit() - conn.close() - return {"status": "deleted"} - except Exception as e: - raise HTTPException(status_code=500, detail=str(e)) - -@router.post("/api/douyin/summary") -async def generate_summary(request: SummaryRequest): - try: - # Fetch transcripts - conn = get_db_connection() - cursor = conn.cursor() - - if request.ids: - # Secure way to handle list in SQL - format_strings = ','.join(['%s'] * len(request.ids)) - sql = f"SELECT video_name, transcript FROM t_douyin_record WHERE id IN ({format_strings}) AND status='COMPLETED'" - cursor.execute(sql, tuple(request.ids)) - else: - # Default to latest 20 - cursor.execute("SELECT video_name, transcript FROM t_douyin_record WHERE status='COMPLETED' ORDER BY create_time DESC LIMIT 20") - - records = cursor.fetchall() - conn.close() - - if not records: - # If no records, just return a simple message stream - async def empty_stream(): - yield "未找到可总结的已完成记录,请先解析视频。" - return StreamingResponse(empty_stream(), media_type="text/event-stream") - - # Prepare text - full_text = "" - for r in records: - if r['transcript']: - full_text += f"【标题:{r['video_name']}】\n内容:{r['transcript']}\n\n" - - if not full_text: - async def empty_text_stream(): - yield "记录中没有有效的文案内容。" - return StreamingResponse(empty_text_stream(), media_type="text/event-stream") - - # Prompt - prompt = f""" - 请对以下充电行业相关的视频内容进行知识精华提取。 - 要求: - 1. 忽略无关闲聊和口语化表达; - 2. 按条目列出核心知识点,不要长篇大论; - 3. 保持简洁专业,只保留干货; - 4. 返回格式为Markdown列表。 - - 内容如下: - {full_text[:15000]} - """ - - # Limit context to avoid errors, 15000 chars is roughly safe for most models, - # but if using a small model, might need less. Assuming robust model. - - return StreamingResponse(get_llm_response(prompt), media_type="text/event-stream") - - except Exception as e: - logger.error(f"Summary generation failed: {e}") - raise HTTPException(status_code=500, detail=str(e)) - -async def recover_pending_tasks(): - """ - Check for tasks stuck in PENDING or PROCESSING state (due to server restart) - and restart them. - """ - logger.info("Scanning for interrupted Douyin tasks...") - try: - # Use asyncio.to_thread for DB operation - def fetch_pending(): - conn = get_db_connection() - cursor = conn.cursor() - # Select recent pending/processing tasks (limit 20 to avoid storm) - sql = """ - SELECT id, original_text, status - FROM t_douyin_record - WHERE status IN ('PENDING', 'PROCESSING') - ORDER BY create_time DESC LIMIT 20 - """ - cursor.execute(sql) - tasks = cursor.fetchall() - conn.close() - return tasks - - tasks = await asyncio.to_thread(fetch_pending) - - if not tasks: - logger.info("No interrupted tasks found.") - return - - logger.info(f"Found {len(tasks)} interrupted tasks. Restarting...") - for task in tasks: - req_id = task['id'] - url = task['original_text'] - # Restart task in background - # Note: We lost the original share text for title extraction, - # so we pass empty string. It will use the URL or 'Unknown Title'. - # If LLM is enabled, it might fix the title later. - asyncio.create_task(process_video_task(url, req_id, share_text="")) - - except Exception as e: +from fastapi import APIRouter, HTTPException, BackgroundTasks +from fastapi.responses import StreamingResponse +from pydantic import BaseModel + +# Import custom modules +from Config.Config import OBS_CLOUD_PREFIX, OBS_BUCKET, OBS_TMP_PREFIX, OBS_SERVER +from Util.DouYinDownloader import DouYinDownloader +from Util.ObsUtil import ObsUploader +from Util.ASRClient import ASRClient +from Util.LlmUtil import get_llm_response +from Model.DouYinModel import DouYinModel + +# Logger setup +logger = logging.getLogger(__name__) + +router = APIRouter() + +class ParseRequest(BaseModel): + text: str + +class SummaryRequest(BaseModel): + ids: List[str] = [] + +async def process_video_task(url: str, request_id: str, share_text: str = ""): + logger.info(f"Processing task {request_id}") + + model = DouYinModel() + # 1. Update status + await model.update_status(request_id, "PROCESSING") + + temp_dir = os.path.abspath(f"temp_{request_id}") + try: + if not os.path.exists(temp_dir): + os.makedirs(temp_dir) + + # 2. Parse & Download + downloader = DouYinDownloader() + # url is passed directly now + if not url: + raise Exception("No valid URL found") + + logger.info(f"Downloading from {url}") + # Run download in thread to avoid blocking main loop + local_video_path, title = await asyncio.to_thread(downloader.download_video, url, temp_dir) + + # Title handling strategy: + # Priority 1: Extracted from share text (if available and valid) + # Priority 2: Extracted from video download (often "Unknown Title") + # Priority 3: Generated by LLM (done later) + + extracted_title = downloader.extract_title_from_text(share_text) + logger.info(f"Extracted title from text: {extracted_title}") + + # If we have a valid extracted title, use it. + # But if we don't have a title yet (or it's Unknown), we definitely want to use extracted_title. + # Even if we have a title from yt-dlp, if it's just "Unknown Title", we prefer extracted one. + if extracted_title and extracted_title != "Unknown Title": + title = extracted_title + elif not title: + title = "Unknown Title" + + if not local_video_path or not os.path.exists(local_video_path): + raise Exception("Download failed") + + # 3. Upload Video to OBS (Long term storage) + logger.info("Uploading video to OBS...") + uploader = ObsUploader() + video_filename = os.path.basename(local_video_path) + obs_video_key = f"{OBS_CLOUD_PREFIX}/DouYin/{video_filename}" + + success, _ = await asyncio.to_thread(uploader.upload_file, obs_video_key, local_video_path, OBS_BUCKET) + if not success: + raise Exception("OBS Upload failed") + + # Construct public URL (Assuming standard OBS pattern or Config logic) + obs_url = f"https://{OBS_BUCKET}.{OBS_SERVER}/{obs_video_key}" + + # 4. Convert to MP3 + logger.info("Converting to MP3...") + mp3_path = os.path.splitext(local_video_path)[0] + ".mp3" + cmd = [ + "ffmpeg", "-y", "-i", local_video_path, + "-acodec", "libmp3lame", "-ar", "16000", "-ac", "1", "-q:a", "2", + mp3_path + ] + # Run ffmpeg in thread + result = await asyncio.to_thread(subprocess.run, cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE) + if result.returncode != 0: + raise Exception(f"FFmpeg failed: {result.stderr.decode()}") + + # 5. ASR (Upload MP3 to tmp and transcribe) + logger.info("Transcribing...") + asr = ASRClient() + # Run ASR in thread + transcript = await asyncio.to_thread(asr.upload_and_transcribe_sync, mp3_path) + + if not transcript: + raise Exception("Transcription failed (returned empty)") + + # 6. LLM Title Generation (Enhancement) + # If the title is still Unknown or weak, OR if we just want to ensure we have a good title. + # The user said: "Alternatively, call LlmUtil.py to summarize title". + # Let's do it if title is Unknown or matches default filename pattern, OR if extracted title was also missing. + if (not title or title == "Unknown Title" or title == "Unknown"): + try: + logger.info("Generating title from transcript via LLM...") + prompt = f"请根据以下视频文案总结一个简短的标题(20字以内),不要包含任何解释性文字,直接返回标题:\n\n{transcript[:1000]}" + + llm_title_chunks = [] + # get_llm_response is already async + async for chunk in get_llm_response(prompt, stream=False): + llm_title_chunks.append(chunk) + llm_title = "".join(llm_title_chunks) + + if llm_title: + # Clean up quotes if any + llm_title = llm_title.strip().strip('"').strip('“').strip('”') + logger.info(f"LLM generated title: {llm_title}") + # We overwrite the title if LLM succeeds + title = llm_title + except Exception as llm_e: + logger.warning(f"LLM Title generation failed: {llm_e}") + + # 7. Save to DB (Update) + logger.info("Saving to DB...") + await model.update_record(request_id, title, obs_url, transcript, "COMPLETED") + logger.info(f"Task {request_id} completed successfully.") + + except Exception as e: + logger.error(f"Task {request_id} failed: {e}", exc_info=True) + await model.update_status(request_id, "FAILED", str(e)) + finally: + # 8. Cleanup + if os.path.exists(temp_dir): + try: + # shutil.rmtree is sync, wrap it + await asyncio.to_thread(shutil.rmtree, temp_dir, ignore_errors=True) + except Exception as e: + logger.error(f"Cleanup failed: {e}") + +@router.post("/api/parse") +async def parse(request: ParseRequest, background_tasks: BackgroundTasks): + downloader = DouYinDownloader() + urls = downloader.extract_urls(request.text) + + if not urls: + # If no URLs found, try using the text as is (might be a direct link not caught by regex) + # But regex is quite broad. Let's just fail or try one. + # Let's assume text might be the URL if it's clean. + if request.text.startswith("http"): + urls = [request.text] + else: + raise HTTPException(status_code=400, detail="No valid URLs found") + + created_ids = [] + try: + model = DouYinModel() + for url in urls: + req_id = str(uuid.uuid4()) + await model.insert_record(req_id, url) + created_ids.append(req_id) + # Pass request.text (the full share text) so we can extract title from it + background_tasks.add_task(process_video_task, url, req_id, request.text) + + except Exception as e: + raise HTTPException(status_code=500, detail=f"DB Init Error: {e}") + + return {"id": created_ids[0] if created_ids else None, "ids": created_ids, "status": "PENDING"} + +@router.get("/api/records") +async def get_records(): + try: + model = DouYinModel() + records = await model.get_records() + return records + except Exception as e: + logger.error(f"Get records error: {e}", exc_info=True) + return [] + +@router.delete("/api/records/{id}") +async def delete_record(id: str): + try: + model = DouYinModel() + await model.delete_record(id) + return {"status": "deleted"} + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + +@router.post("/api/douyin/summary") +async def generate_summary(request: SummaryRequest): + try: + # Fetch transcripts + model = DouYinModel() + records = await model.get_transcripts(ids=request.ids) + + if not records: + # If no records, just return a simple message stream + async def empty_stream(): + yield "未找到可总结的已完成记录,请先解析视频。" + return StreamingResponse(empty_stream(), media_type="text/event-stream") + + # Prepare text + full_text = "" + for r in records: + if r['transcript']: + full_text += f"【标题:{r['video_name']}】\n内容:{r['transcript']}\n\n" + + if not full_text: + async def empty_text_stream(): + yield "记录中没有有效的文案内容。" + return StreamingResponse(empty_text_stream(), media_type="text/event-stream") + + # Prompt + prompt = f""" + 请对以下充电行业相关的视频内容进行知识精华提取。 + 要求: + 1. 忽略无关闲聊和口语化表达; + 2. 按条目列出核心知识点,不要长篇大论; + 3. 保持简洁专业,只保留干货; + 4. 返回格式为Markdown列表。 + + 内容如下: + {full_text[:15000]} + """ + + # Limit context to avoid errors, 15000 chars is roughly safe for most models, + # but if using a small model, might need less. Assuming robust model. + + return StreamingResponse(get_llm_response(prompt), media_type="text/event-stream") + + except Exception as e: + logger.error(f"Summary generation failed: {e}") + raise HTTPException(status_code=500, detail=str(e)) + +async def recover_pending_tasks(): + """ + Check for tasks stuck in PENDING or PROCESSING state (due to server restart) + and restart them. + """ + logger.info("Scanning for interrupted Douyin tasks...") + try: + model = DouYinModel() + tasks = await model.get_interrupted_tasks() + + if not tasks: + logger.info("No interrupted tasks found.") + return + + logger.info(f"Found {len(tasks)} interrupted tasks. Restarting...") + for task in tasks: + req_id = task['id'] + url = task['original_text'] + # Restart task in background + # Note: We lost the original share text for title extraction, + # so we pass empty string. It will use the URL or 'Unknown Title'. + # If LLM is enabled, it might fix the title later. + asyncio.create_task(process_video_task(url, req_id, share_text="")) + + except Exception as e: logger.error(f"Failed to recover tasks: {e}", exc_info=True) diff --git a/Controller/HaiBaoController.py b/Controller/HaiBaoController.py index 95798f4..96d93a0 100644 --- a/Controller/HaiBaoController.py +++ b/Controller/HaiBaoController.py @@ -9,7 +9,7 @@ from sqlalchemy.sql import text from Util.BananaClient import BananaClient from Util.LlmUtil import get_llm_response -from DbKit.Db import Db +from Model.HaiBaoModel import HaiBaoModel router = APIRouter(prefix="/haibao") logger = logging.getLogger("HaiBaoController") @@ -21,43 +21,8 @@ class GenerateRequest(BaseModel): @router.on_event("startup") async def startup_event(): - """初始化时检查并创建表""" - db = Db() - await db.init_db() - - # Doris 建表语句 - create_table_sql = """ - CREATE TABLE IF NOT EXISTS haibao_history ( - id VARCHAR(50) COMMENT "ID", - prompt TEXT COMMENT "提示词", - image_url VARCHAR(500) COMMENT "图片URL", - scheme_content TEXT COMMENT "文案方案", - created_at DATETIME COMMENT "创建时间" - ) - DUPLICATE KEY(id) - DISTRIBUTED BY HASH(id) BUCKETS 1 - PROPERTIES ( - "replication_num" = "1" - ); - """ - try: - # 使用 engine 直接执行 DDL - async with db.engine.begin() as conn: - await conn.execute(text(create_table_sql)) - - # 尝试添加列(如果表已存在但列不存在) - # Doris 不支持 IF NOT EXISTS for ADD COLUMN directly nicely in all versions without error if exists - # 所以这里简单捕获异常,如果列已存在则忽略 - try: - alter_sql = "ALTER TABLE haibao_history ADD COLUMN scheme_content TEXT COMMENT '文案方案'" - await conn.execute(text(alter_sql)) - except Exception as e: - # 忽略列已存在的错误 - pass - - logger.info("海报历史表检查/更新成功") - except Exception as e: - logger.error(f"海报历史表创建/更新失败: {e}") + """初始化时逻辑""" + logger.info("海报控制器启动成功") class RefineRequest(BaseModel): prompt: str @@ -184,26 +149,11 @@ async def generate_poster(req: GenerateRequest): image_url, scheme_content = await asyncio.gather(generate_image_task(), generate_text_task()) # 4. 保存到数据库 - db = Db() + model = HaiBaoModel() record_id = str(uuid.uuid4()) created_at = datetime.now() - insert_sql = """ - INSERT INTO haibao_history (id, prompt, image_url, scheme_content, created_at) - VALUES (:id, :prompt, :image_url, :scheme_content, :created_at) - """ - - params = { - "id": record_id, - "prompt": req.prompt, - "image_url": image_url, - "scheme_content": scheme_content, - "created_at": created_at - } - - async with await db.get_session() as session: - async with session.begin(): - await session.execute(text(insert_sql), params) + await model.insert_record(record_id, req.prompt, image_url, scheme_content, created_at) return { "id": record_id, @@ -220,10 +170,9 @@ async def generate_poster(req: GenerateRequest): @router.get("/history") async def get_history(): """获取海报生成历史""" - db = Db() - sql = "SELECT * FROM haibao_history ORDER BY created_at DESC LIMIT 50" try: - result = await db.find(sql) + model = HaiBaoModel() + result = await model.get_history(50) formatted_result = [] for item in result: diff --git a/Controller/YltAnalyticsController.py b/Controller/YltAnalyticsController.py index a72f853..ff15a88 100644 --- a/Controller/YltAnalyticsController.py +++ b/Controller/YltAnalyticsController.py @@ -6,7 +6,6 @@ from typing import List, Optional, Dict, Any from fastapi import APIRouter, HTTPException from fastapi.responses import StreamingResponse, FileResponse, JSONResponse -from DbKit.Db import Db from Config.Config import DB_URL from Util.LlmUtil import get_llm_response from Tools.T6_Export import export_excel, DorisExcelExporter, extract_hourly_prices_from_schedule @@ -24,31 +23,35 @@ from Model.YltAnalyticsModel import ( PriceSeries, PriceComparisonResponse, PriceComparisonSummary, + YltAnalyticsModel, ) +from DbKit.Db import Db + router = APIRouter() -db = Db(db_url=DB_URL) +# db = Db(db_url=DB_URL) # Removed direct db instance async def init_db(): + db = Db() await db.init_db() async def close_db(): + db = Db() await db.close() @router.get("/api/operators/hourly-prices") async def get_operators_hourly_prices(): operators = ["新电途", "特来电", "驿来特", "艾特吉易充"] - exporter = DorisExcelExporter(db_url=DB_URL) - await exporter.init() + model = YltAnalyticsModel() try: result = [] for op in operators: - rows = await exporter.fetch_current_station_rows(op) + rows = await model.fetch_current_station_rows(op) if not rows: result.append({"operator": op, "series": [None] * 24}) continue @@ -71,11 +74,72 @@ async def get_operators_hourly_prices(): else: avg_series.append(None) result.append({"operator": op, "series": avg_series}) - finally: - await exporter.close() + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) return {"operators": result} +@router.get("/api/operators/price-trends") +async def get_operators_price_trends(days: int = 7): + operators = ["新电途", "特来电", "驿来特", "艾特吉易充"] + + model = YltAnalyticsModel() + rows = await model.get_operators_price_trends(days) + + # 数据结构: { operator: { date_str: [sums_of_24h, counts_of_24h] } } + trend_data = {} + for op in operators: + trend_data[op] = {} + + for row in rows: + op = row.get("operator") + if op not in trend_data: + continue + d_str = str(row.get("date_str")) + schedule_json = row.get("schedule_json") + + if d_str not in trend_data[op]: + trend_data[op][d_str] = {"sums": [0.0] * 24, "counts": [0] * 24} + + series = extract_hourly_prices_from_schedule(schedule_json) + for i in range(24): + v = series[i] + if v is not None: + trend_data[op][d_str]["sums"][i] += float(v) + trend_data[op][d_str]["counts"][i] += 1 + + # 转换为 ECharts 友好格式 + # 1. 获取所有日期并排序 + all_dates = sorted(list(set(str(row.get("date_str")) for row in rows))) + + # 2. 为每个运营商计算每天的平均价格(24小时的平均值) + series_result = [] + for op in operators: + op_trend = [] + for d in all_dates: + if d in trend_data[op]: + day_stats = trend_data[op][d] + day_avg_sum = 0.0 + day_hour_count = 0 + for i in range(24): + if day_stats["counts"][i] > 0: + day_avg_sum += (day_stats["sums"][i] / day_stats["counts"][i]) + day_hour_count += 1 + + if day_hour_count > 0: + op_trend.append(round(day_avg_sum / day_hour_count, 4)) + else: + op_trend.append(None) + else: + op_trend.append(None) + series_result.append({"name": op, "data": op_trend}) + + return { + "dates": all_dates, + "series": series_result + } + + @router.get("/api/export/prices-zip") async def export_prices_zip(): operators = ["新电途", "特来电", "驿来特", "艾特吉易充"] @@ -177,26 +241,8 @@ async def ai_pricing_strategy_summary(): @router.get("/api/ylt/stations", response_model=List[StationBase]) async def list_ylt_stations(q: Optional[str] = None): - base_sql = """ - SELECT - p.station_hash, - p.operator, - p.station_name, - p.address, - p.coord_x, - p.coord_y, - s.current_price - FROM t_station_profile_scd p - LEFT JOIN t_station_status_scd s - ON p.station_hash = s.station_hash AND s.is_current = 1 - WHERE p.operator = '驿来特' - """ - params: Dict[str, Any] = {} - if q: - base_sql += " AND (p.station_name LIKE :kw OR p.address LIKE :kw)" - params["kw"] = f"%{q}%" - base_sql += " ORDER BY p.station_name" - rows = await db.find(base_sql, params) + model = YltAnalyticsModel() + rows = await model.list_ylt_stations(q) result: List[StationBase] = [] for r in rows: result.append( @@ -225,21 +271,8 @@ def haversine_km(lon1: float, lat1: float, lon2: float, lat2: float) -> float: async def fetch_current_stations() -> List[dict]: - sql = """ - SELECT - p.station_hash, - p.operator, - p.station_name, - p.address, - p.coord_x, - p.coord_y, - s.current_price - FROM t_station_profile_scd p - LEFT JOIN t_station_status_scd s - ON p.station_hash = s.station_hash AND s.is_current = 1 - WHERE p.is_current = 1 - """ - return await db.find(sql, {}) + model = YltAnalyticsModel() + return await model.fetch_current_stations() async def build_geo_competition(station_hash: str, radius_km: float = 3.0) -> GeoCompetitionResponse: @@ -318,17 +351,8 @@ async def build_geo_competition(station_hash: str, radius_km: float = 3.0) -> Ge async def fetch_station_schedule_json(station_hash: str) -> Optional[str]: - sql = """ - SELECT schedule_json - FROM t_station_price_schedule_scd - WHERE station_hash = :h AND is_current = 1 - ORDER BY valid_start_time DESC - LIMIT 1 - """ - rows = await db.find(sql, {"h": station_hash}) - if not rows: - return None - value = rows[0].get("schedule_json") + model = YltAnalyticsModel() + value = await model.fetch_station_schedule_json(station_hash) if value is None: return None if isinstance(value, str): diff --git a/Controller/__pycache__/DouYinController.cpython-310.pyc b/Controller/__pycache__/DouYinController.cpython-310.pyc index 79aa9ead42b14af38624eaf034217ecaec9aff39..7458bbeb52d9ecaeecf5d7304900fd0a115f3b60 100644 GIT binary patch delta 4260 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 literal 10262 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 diff --git a/Controller/__pycache__/HaiBaoController.cpython-310.pyc b/Controller/__pycache__/HaiBaoController.cpython-310.pyc index f5d1386d948ee91193e353d2cec6fb57180e6e32..461ece4a822edba5e1603abee166a921a97d13b8 100644 GIT binary patch delta 1396 zcmZ8hU2GIp6u#%q&(8i$wq^Uk}}VT({%#2*Kxex@!H^`=NdlV$tD(b)aC6+J-r``YDV9zX#gnm$=ADAe;l2 z)WLui0AkQFl9Q%4q1;%y+PHqPap{eGgy?VW|bf@vw zjmDJ^8p}7SD!o&h!F&VQNFY6;|AIzJ;(x?XevMnO6mCTCC3a**u zzJ6*xi0=MpNxxvhx;Qf@tcsbhGP_)L#s51h0$qqndKY^H>1?=fW3M9MeB%it5fxp; z;e3T~4rYg3oW4RXkP9$D4KV0SS`Co%Z(9cEJ*6J@)Fn6>@gz_7LMzHs#88F_5Kr}_ zKu5J6HPjU_LhmRi$QY4{8=ENrscArF6K=e&8=9xRO|DXLDsGDrc4bdn1S4{po(Dsz zX>-~cI74#L>CKqZ&#zy@BWNDYYFq0iNMulDSphu)dRIT%T$4B|vLek<7nhxzx(dum3=+0}2_Z&7_K z@+Eo`?^z{G|Ohb zXjfdb;`)&yGx11`e8J_%Q-hG;ms5M@$KzJTF`1iZX3=Iw^HmL#pKz_R*~~xqwCy4X z;jCP^X_o;Z3<}sMU_XMdl*M%z4>vcuVT{EP06dEquu?%>*ef&^`}SJmC$e)SZnYypO{X<_ZeVf68Rqgq%zJA<=29N{JoDIXlRc z+4C=qw+2ma*MAxwi(=SUnLX`dTMA=(BSD7>+6&V~P_rv}^E4WdrNmaEIrwtbMrVn> Zz(xc-Ent-YnmrnqH3?5PgX5$Qe**-gU3UNg delta 2783 zcmZ`*TW}lI8Q!xutu9uK9l%aPSOmsa;#^!3XdJ>swl=k}d_j_N;+c#_@~)Ll?3HqM zO=8U|GN?lgg_4<_h7Kl0nvyB$LtE+;3hnfTmp*rRV77hWMM{|AjTx9J9n$~oO13bq zH2d#4|M@Rx|I2sI-D7``2J;;qJ_>$c{Zmtay#M{+EyU2#?8YZ(4wX6J`{DTn9{p<| zFCv4gP$kNws;EZAC>f1Wh?40NDkeZo{AhR{#5S=9Gfcc_pzPJNcy)tLA>(mX}m1%h+vU1sROdN*!<-PFv^|c%I z-~6op+V#epA2fcw+IZ(?{i6@+YwPvDe0;a|-sJKx5k>h#_>Cr{NJ-3kCpxMuJ|9Vpjztm!M-PX^(W!~@gfy8FL$PRR za8s&)^GgLoOi8CxM8uV7ym9T$yC2{E{p)Rl>QW}Bq?hqxn^o7Wt5aY9+nrag&LrY( zI{N%MWl2fT=QTso46(e;%k`^`+FIlKM?l+@=CP776gizS#3+1pDmEc)?aU1hgzJkp zZ-<GQaww>%9wjKot?qM4a(xrjugu=DuL z#H8p%qP!J~??s}&(H4pRZJ()z_kn}sZEbKcA%7;+d$ z!98HmCB4M7b%W`CVjqm?Rh|hVmZ5_?>79t?Sk!}pC_oFV^mp{?z&D*8L9`da>>hp` z#H~)|e(N91LU%t*o`FYaf%vEZ!u=cL>=k|oei#^7B!ooI`rH?P1oy${7QzFB>+HnY zgpgA-1w(~Jt#SXwva|ZsrpS|I~!G2NUJudkzK=q>W7` z0Y2cBn)(48tMZ`eme8QM388~79&EzFptuEkE^-mG&qv}j1d@h{jp3G&Vb@9okQlh% zM_6F|FGHkfRT=MqFSWg!o3N&O_8k28eSK#R^er9elf}NV;l7FCzGRrgQ4$l05Tb)m z5%Lot)`gxEsN1^L^HkZj=FcfcT32)(-~vBH#K^Jc0*L@wS}@clWm|J%(!Ak-+m|Cm ze3TH!o#W|In9g~zGM~qC8um>*&-qkMS1>pOsNoBlUYuFdZBNTJX?3^9Pg>Kxy?hqt z`bjI_d;ejUrg;|8&Y%CbN9Y2q*>`W?9?j8+@C77%8x%TGC&8QVaLW2s@2;{3^hv2X z7nQ%lpmkA!I|C{nC)X9zy@JqVREf?}}1~hX8X~<+t9B{cR zD>F4@a1|O4nVih@QdOSZCCK<`=s=Tu0pVjZZ*tGd+|!iFR6Qm~t`QfzO{Upm!pd1_ zG2Ur0Z`CJzOup4(IcQ^^TyN+Se}#wZhV!;cyBw(jhDsQ)=wvH47)plI^8 zbca2dB-CagbJpX#2M=%MYOBkfI%-~F{sq!HfqimHK!&W#yTu32!aV%2?I9yjX&cTAofnu|T eJd`RybetmOG$CiLYkS5!`UM8+LWE}}i2e&YrQfCi diff --git a/Controller/__pycache__/YltAnalyticsController.cpython-310.pyc b/Controller/__pycache__/YltAnalyticsController.cpython-310.pyc index 4d27e2aac7ee6783fd0c2f7d58e58a95b8c70a5e..bcfba8a6e52c279d0e3dadaff31ad74ec6459f69 100644 GIT binary patch delta 6407 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 0 + except Exception as e: + logger.debug(f"检查列是否存在时出错 (可能表不存在): {str(e)}") + return False + @db_retry() async def execute_update(self, sql, params=None, session=None): """执行SQL更新操作(插入、更新、删除)(异步版本) diff --git a/DbKit/Sql/DouYin.sql b/DbKit/Sql/DouYin.sql new file mode 100644 index 0000000..4512720 --- /dev/null +++ b/DbKit/Sql/DouYin.sql @@ -0,0 +1,57 @@ +#namespace("DouYin") + #sql("updateStatus") + UPDATE t_douyin_record + SET status = #para(status) + #if(error_msg) + , error_msg = #para(error_msg) + #end + WHERE id = #para(id) + #end + + #sql("updateRecord") + UPDATE t_douyin_record + SET video_name = #para(title), + obs_url = #para(obs_url), + transcript = #para(transcript), + status = #para(status) + WHERE id = #para(id) + #end + + #sql("insertRecord") + INSERT INTO t_douyin_record (id, original_text, status, create_time) + VALUES (#para(id), #para(url), 'PENDING', #para(create_time)) + #end + + #sql("getRecords") + SELECT * FROM t_douyin_record + ORDER BY create_time DESC + LIMIT #para(limit) + #end + + #sql("deleteRecord") + DELETE FROM t_douyin_record WHERE id = #para(id) + #end + + #sql("getTranscriptsByIds") + SELECT video_name, transcript + FROM t_douyin_record + WHERE id IN (#para(ids)) + AND status = 'COMPLETED' + #end + + #sql("getLatestTranscripts") + SELECT video_name, transcript + FROM t_douyin_record + WHERE status = 'COMPLETED' + ORDER BY create_time DESC + LIMIT #para(limit) + #end + + #sql("getInterruptedTasks") + SELECT id, original_text, status + FROM t_douyin_record + WHERE status IN ('PENDING', 'PROCESSING') + ORDER BY create_time DESC + LIMIT #para(limit) + #end +#end diff --git a/DbKit/Sql/HaiBao.sql b/DbKit/Sql/HaiBao.sql new file mode 100644 index 0000000..6af5add --- /dev/null +++ b/DbKit/Sql/HaiBao.sql @@ -0,0 +1,12 @@ +#namespace("HaiBao") + #sql("insertHistory") + INSERT INTO haibao_history (id, prompt, image_url, scheme_content, created_at) + VALUES (#para(id), #para(prompt), #para(image_url), #para(scheme_content), #para(created_at)) + #end + + #sql("getHistory") + SELECT * FROM haibao_history + ORDER BY created_at DESC + LIMIT #para(limit) + #end +#end diff --git a/DbKit/Sql/YltAnalytics.sql b/DbKit/Sql/YltAnalytics.sql new file mode 100644 index 0000000..2dc7cec --- /dev/null +++ b/DbKit/Sql/YltAnalytics.sql @@ -0,0 +1,79 @@ +#namespace("YltAnalytics") + #sql("getOperatorsPriceTrends") + SELECT + p.operator, + DATE(sc.valid_start_time) as date_str, + sc.schedule_json + FROM t_station_price_schedule_scd sc + JOIN t_station_profile_scd p ON sc.station_hash = p.station_hash AND p.is_current = 1 + WHERE sc.valid_start_time >= DATE_SUB(CURDATE(), INTERVAL #para(days) DAY) + ORDER BY date_str ASC + #end + + #sql("listYltStations") + SELECT + p.station_hash, + p.operator, + p.station_name, + p.address, + p.coord_x, + p.coord_y, + s.current_price + FROM t_station_profile_scd p + LEFT JOIN t_station_status_scd s + ON p.station_hash = s.station_hash AND s.is_current = 1 + WHERE p.operator = '驿来特' + #if(q) + AND (p.station_name LIKE #para(kw) OR p.address LIKE #para(kw)) + #end + ORDER BY p.station_name + #end + + #sql("fetchCurrentStations") + SELECT + p.station_hash, + p.operator, + p.station_name, + p.address, + p.coord_x, + p.coord_y, + s.current_price + FROM t_station_profile_scd p + LEFT JOIN t_station_status_scd s + ON p.station_hash = s.station_hash AND s.is_current = 1 + WHERE p.is_current = 1 + #end + + #sql("fetchStationScheduleJson") + SELECT sc.schedule_json, p.operator + FROM t_station_price_schedule_scd sc + JOIN t_station_profile_scd p ON sc.station_hash = p.station_hash AND p.is_current = 1 + WHERE sc.station_hash = #para(h) AND sc.is_current = 1 + ORDER BY sc.valid_start_time DESC + LIMIT 1 + #end + + #sql("fetchCurrentStationRows") + SELECT + p.station_hash, + p.station_name, + p.address, + p.coord_x, + p.coord_y, + s.total_piles AS total_guns, + s.free_piles AS free_guns, + s.current_price, + s.pro_price, + s.parking_info, + s.distance, + s.valid_start_time AS status_update_time, + sc.schedule_json, + sc.valid_start_time AS schedule_update_time, + s.piles_detail_json + FROM t_station_profile_scd p + LEFT JOIN t_station_status_scd s ON p.station_hash = s.station_hash AND s.is_current = 1 + LEFT JOIN t_station_price_schedule_scd sc ON p.station_hash = sc.station_hash AND sc.is_current = 1 + WHERE p.operator = #para(op) AND p.is_current = 1 + ORDER BY p.station_name ASC + #end +#end diff --git a/DbKit/SqlTemplateLoader.py b/DbKit/SqlTemplateLoader.py index 240bba4..443e756 100644 --- a/DbKit/SqlTemplateLoader.py +++ b/DbKit/SqlTemplateLoader.py @@ -489,6 +489,10 @@ class SqlTemplateLoader: template_map_str = await redisKit.get_data(REDIS_SQL_TEMPLATE_MAP_KEY) if template_map_str: self._template_map = json.loads(template_map_str) + + # 无论从Redis加载了什么,都尝试重新构建一次映射以确保一致性 + if self.templates and not self._template_map: + self._build_template_map() except Exception as e: logger.error(f"从Redis加载SQL模板数据失败: {str(e)}") diff --git a/DbKit/__pycache__/Db.cpython-310.pyc b/DbKit/__pycache__/Db.cpython-310.pyc index 637cbeef253131199fce2c44a7e9b71d6da5f3b5..6982c04af59be8ad7b2b9323e149cdbb61b506cc 100644 GIT binary patch delta 5491 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 cFd@#7E>gGqZZ^*y+-JNy+1v|g5f&c&j delta 4701 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 aCoT3ASCe$nH*B5a^zfJT3-Q^tTmB0|(ZWgq diff --git a/DbKit/__pycache__/SqlTemplateLoader.cpython-310.pyc b/DbKit/__pycache__/SqlTemplateLoader.cpython-310.pyc index 6dc94ef2acdde09523d50163b47e790ef11400f6..582e1734291244c58697911e7c15543b3038d666 100644 GIT binary patch delta 215 zcmdm%b2^7FpO=@50SLCs6=ZIW-pIFuSy0=>Dkh~kJijPACNa|`u_STxQ)Yf`juSvt zjf_>UlmF<2dzzFmFJP%*UdUL(5YJV^ki}Z$2qd|RU1}H>a4%$FWC&+SVGKqgSr|%q zDw7!*YFQW=ssu}T7x1MpmGDn)(r;rFn9OO=#HcZOgOT{;c?J>kDnP548kkrFm{=Hj z7zLPF7zMy24DpcDkh~kJijPACNifapeR2pHMs;R;#`zi zo|9U%`8qSdHpe=ko<_zhi^=cw!u8Edm>00rFfU}RVTk7nXGmcTW`K|^3? 100: + title = title[:100] + "..." + + params = { + "id": id, + "title": title, + "obs_url": obs_url, + "transcript": transcript, + "status": status + } + return await self.db.execute_update("DouYin.updateRecord", params) + + async def insert_record(self, id, url, create_time=None): + await self.db.init_db() + if create_time is None: + create_time = datetime.now() + params = { + "id": id, + "url": url, + "create_time": create_time + } + return await self.db.execute_update("DouYin.insertRecord", params) + + async def get_records(self, limit=50): + await self.db.init_db() + params = {"limit": limit} + records = await self.db.find("DouYin.getRecords", params) + + # Manually handle datetime serialization + for r in records: + if 'create_time' in r and r['create_time']: + r['create_time'] = r['create_time'].strftime("%Y-%m-%d %H:%M:%S") + if 'update_time' in r and r['update_time']: + r['update_time'] = r['update_time'].strftime("%Y-%m-%d %H:%M:%S") + return records + + async def delete_record(self, id): + await self.db.init_db() + params = {"id": id} + return await self.db.execute_update("DouYin.deleteRecord", params) + + async def get_transcripts(self, ids=None, limit=20): + await self.db.init_db() + if ids: + params = {"ids": ids} + return await self.db.find("DouYin.getTranscriptsByIds", params) + else: + params = {"limit": limit} + return await self.db.find("DouYin.getLatestTranscripts", params) + + async def get_interrupted_tasks(self, limit=20): + await self.db.init_db() + params = {"limit": limit} + return await self.db.find("DouYin.getInterruptedTasks", params) + + async def close(self): + await self.db.shutdown() diff --git a/Model/HaiBaoModel.py b/Model/HaiBaoModel.py new file mode 100644 index 0000000..b75821b --- /dev/null +++ b/Model/HaiBaoModel.py @@ -0,0 +1,24 @@ +import logging +from DbKit.Db import Db + +logger = logging.getLogger("HaiBaoModel") + +class HaiBaoModel: + def __init__(self): + self.db = Db() + + async def insert_record(self, id, prompt, image_url, scheme_content, created_at): + """插入生成记录""" + params = { + "id": id, + "prompt": prompt, + "image_url": image_url, + "scheme_content": scheme_content, + "created_at": created_at + } + return await self.db.execute_update("HaiBao.insertHistory", params) + + async def get_history(self, limit=50): + """获取历史记录""" + params = {"limit": limit} + return await self.db.find("HaiBao.getHistory", params) diff --git a/Model/YltAnalyticsModel.py b/Model/YltAnalyticsModel.py index 2794d12..3e6f6fb 100644 --- a/Model/YltAnalyticsModel.py +++ b/Model/YltAnalyticsModel.py @@ -53,3 +53,32 @@ class PriceComparisonResponse(BaseModel): class PriceComparisonSummary(BaseModel): summary: str + +from DbKit.Db import Db + +class YltAnalyticsModel: + def __init__(self): + self.db = Db() + + async def get_operators_price_trends(self, days: int): + return await self.db.find("YltAnalytics.getOperatorsPriceTrends", {"days": days}) + + async def list_ylt_stations(self, q: str = None): + params = {} + if q: + params["q"] = True + params["kw"] = f"%{q}%" + return await self.db.find("YltAnalytics.listYltStations", params) + + async def fetch_current_stations(self): + return await self.db.find("YltAnalytics.fetchCurrentStations") + + async def fetch_station_schedule_json(self, station_hash: str): + rows = await self.db.find("YltAnalytics.fetchStationScheduleJson", {"h": station_hash}) + if not rows: + return None + return rows[0].get("schedule_json") + + async def fetch_current_station_rows(self, operator: str): + return await self.db.find("YltAnalytics.fetchCurrentStationRows", {"op": operator}) + diff --git a/Model/__pycache__/DouYinModel.cpython-310.pyc b/Model/__pycache__/DouYinModel.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7b4ed041b1896cdf1b6b7c4c223018840b25ee26 GIT binary patch literal 2863 zcmb7G&2Jk;6rYb>dt*C^le!=72WkNe7Fr`XmV&BciAGQ#NCm1Dv~v75WvRb9v!>MA zNI@5otV1Kwpl-r}69c`|d!}*3l!twP*BWsjM&vx2TqaC+*%xpWEnd`%ssTTxMKZ=8Z9BFb{=Hp7%Xll^msGJt6lkuPUom@ik z8&BBJ>@7cJTewFL8ST_IQ#n*&sN_7&n~7leh^sudO_)}!klGVNBMvHjT+YTp z>JF)R=HtYwR?JTxvO?i zVCOBOrCviJ4$52z!3EF#TXDzLECO>QIt1;s5T4XFP<)S-B~S=ejYT3}ap2D!RZRq? z)$gK;33Zw63ky}^BreD|?tad|h3S>#GH|)gV?gg?S+Cb|xwO*Mjib05ruo6H4o0e* z7Gl-z>yC4#b0;!B+ za-)fm1Y}ttn3BLWfc{g-F9p<&JfB++(A^S1C9o036^pm>knPJeTyvIi%|a##r(#Ei z$g6;krx`|3UxzB5VC0@+q$I|W%KqSm1u9Fm8>mw6kXQbu9lH5OlB%SDUH|IEU@$DUUcNhg0Zb-Ph7ipLw1ynPx@fYb(^@kZ^*om%fs4Q|#R z-mN{lTib3GCn-gHFh@yeFX}_;=~(R<-30GtDY;(vN+RaFYmrfXmmSpMz`txNb@0OCt z4w*TvaFZku&4_2HApr#9HJRGn$n}LW=Wf0Fkm8U{8+~SZTm2YstB>J77kVP=o;Tzq~;mhb6*6CuCuwj-_t*hVi>&Wt5s4h z9N})KJJ2COz_rf4Z3qjBuz^F_0QV={J zwYyehUrY4pRBI&Yicr@{+$8Y<37-Vzm->XnrzB1zH8pyvs40U@Vte>t@?=4JD~0t! w(OWIJg_V+PY?{84dX3$0qPX7JwE<8f7~L|*sdvu?|0O-IG+pQ=R^prDKM=TUAOHXW literal 0 HcmV?d00001 diff --git a/Model/__pycache__/HaiBaoModel.cpython-310.pyc b/Model/__pycache__/HaiBaoModel.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ce0b1a517c912d07c82bfbf5e2c861804b78216c GIT binary patch literal 1078 zcmZuwziSjh6rP#g+uQpUO*j!7trWI7#8N~=4~(d3EQI4=*xQ+0Mt5(|**!4F1`-m* zDiKjD!KARTQL(YI@(-||RxT$0f;PT)>%|p)%ePzPjqiw__p001ZVsN^}l(CFxK>I!sW(wnzsO2pj&ks0c@tw z&Y=%-4MtK2m6A=85e{$=K$kqG(QKILK~;7l!{xU`JFxZRZ2MwYBzI$dzw4{EpZeWt zrQJf+t>LP1IadRfa$d$12v(=#X3=Tp5FqSK&>PoZU?kY%2F+;3GApw)AnR3W&B@3; zvI`w&&K|n~G%jMv_Gkf|_jjI69=-Yg{Pq5mrw8voOg_KxN`@(6oS}}RVQMNW^81pH zbZF+1U`0lf2k{`4gVa<5E&Wsq?x(XlTWXLQ(w8gj(ll~(u4NEwfq}_!P;8x9jOL&7vsoUWDw6hB z%;5#_50KlL;6Y=JQ~FNBWP=JIP|-DZL>v2uwu9|Ylbx57?Z=ayk2!6o6skz21PjFePYb>n>~3@yTLi1wVKQdLG<72XE29 zheP6~%6SxvQHb+8=MP4Hm|L`q8Id{NMD?eD=?tl!-hW&r=<5=fy4G9`;LBm$@2f%I sxb5CemA2Zw{MuKbnpBJA2NYotbmK^Ua(|RH+mV{Qf-E3wy3% z{6k9iXG6J+NB)I|8=SR`4r{WG*)$pHX4~r6O`93l4Q_FJ-{5v&Kew6=bcg4l=d_-K zp64!fSL=D`1zv<+)Vd43#LLjjS}#Da@HyynS}#JM=T+!ct(UmLYwdEU(yZvU7x*G< zi_gvG9ADZn>c{?tzee3u_N`FH%Dvr-!>H@G>r54{`ZD-F;z7Gb-(-Ill*@SJPiO+8 z$+*!pe>4IsusK^x-e&r?@OA?0qyd|)<(!tCjGW8J`8C82&kCxv5&Pr~`Ju`(QM?^r z?kG3v1;URbG3xL7oj~P%&P5=l%D19OaPP@b=&MR=SBRh+dp!}hf<`^BT+gFHo~MeQ z*NOOUo8+?RJ>2!%sl}49Q1hPWce_#Ifb={;@hfM$9r^KrAr_(O=Jh-4mo~ZlIT8;x z{czon{Y{PE<}dB|3PA10VN2?s)m~pX7?#5$t7wXb%jmDH|Ef$|XXAbeL;e@;3Ff)L z8Dh?Jz&5XH*P>1@h(kb_qAbW4k*5X(H&ls(5Pr86cn>p^->S)OQRGeJg36VW=)cm}Hq9=C80$3DXG@3K7NffvZC!nhj;5*-$2$6+k_r z7`;I+{dPgK0-z|WB|uqW zH=de+9nF;Ed>70BE)=B4Zc{+WZ@V2m`6P zu5wZzro$I_KFC3Ld12pa z=6R8q@Rf^7OubZv?Ph_`Ypu#_(2DxBT~xJ+0?=acY4{_Vu)T&yACa-~+}JZbl+_L6 zfHmr-cn>CJ@~s1=Y#Fq-Gf~qCOSLTQhOy_Z(Ht~cnGIGZmR%iwrflx_Ws5!>{z#_d z;%pw~x%NlTBsimx>-%%?W9v4yHQT?S{f zt0fprjGSN@g|5RU(v0%^k{xE&Yz)q1XI|Y7;?~Z!26_^r2j35*p%KmHh4GdUlyOKGw3>Y1f=Xv7N246$^AMdt9RhJGNWF?)O4Hb`{1bH z>9b~KYO*|voQj(G63w(NI(wM-fSfFi*BgC;pQb74zMI@OFOzhIns2DNLCwfNEs>Lg zxIvES%HyIS&skN=&Cgj2Ic;my=T$E5_rmTyL3ky1dbii-eizqtL1fjJ@Z^p~7bJZV z(N$g7E?riXyT0{Z7_Y8x=_6WCJkB;Ba0M%k=d)ne7Hma2BusTRui Gg83h33^Eu1 delta 862 zcmZ{hyKWOf6o$Rt_5DJ8Nx;b>kSL;6kf5PU2vQ0vRzSLuB5T;4BqO{Romoe5fl}}S zwx&R$E6uO7b7ube=AYm0r>$<%=`>6DeVI?>M`zJ> zt@+|$Ug@2dy#6BpF<>m^z2&Ta>TJ8L6I=?O3c&s}&RA3NNGxm3ozcq; z(h6Z1(y{>}F}MVt+cM4y!3X-g^S~M)_}SrxrSB}qE zRSA$n`Ck41VH(60T_M;W+V0~}RpVc`h8FCZeBV~#hE*<;DQ0P7mN3K>tWHse^86et ze!Z;t)rF~iudxYZ5VmpWZD@4dIqEn$1!fU%)CSR!?Ukk24tnp jfd1(o?s&dD!>|;J2~6 diff --git a/Start.py b/Start.py index 07fad2e..1890fd1 100644 --- a/Start.py +++ b/Start.py @@ -59,6 +59,8 @@ async def lifespan(app: FastAPI): finally: logger.info("驿来特AI智能分析系统关闭...") await close_db() + # Close Redis connection + await RedisKit().close() app = FastAPI(title="驿来特AI智能分析系统", lifespan=lifespan) diff --git a/Tools/T6_Export.py b/Tools/T6_Export.py index e05d03a..be3bb72 100644 --- a/Tools/T6_Export.py +++ b/Tools/T6_Export.py @@ -21,7 +21,7 @@ from Util import Win32Patch Win32Patch.patch() from Config.Config import DB_URL -from DbKit.Db import Db +from Model.YltAnalyticsModel import YltAnalyticsModel try: from openpyxl import Workbook @@ -175,46 +175,17 @@ def _set_column_widths(ws, widths: dict[int, float]): class DorisExcelExporter: - def __init__(self, db_url: str): - self.db = Db(db_url=db_url) - - async def init(self): - await self.db.init_db() - - async def close(self): - try: - await self.db.close() - finally: - if getattr(self.db, "engine", None): - await self.db.engine.dispose() - + def __init__(self, db_url: str = None): + self.model = YltAnalyticsModel() + + async def init(self): + pass # Model handles initialization if needed + + async def close(self): + pass # Model handles closing if needed + async def fetch_current_station_rows(self, operator: str) -> List[Dict[str, Any]]: - sql = """ - SELECT - p.station_hash, - p.station_name, - p.address, - p.coord_x, - p.coord_y, - s.total_piles AS total_guns, - s.free_piles AS free_guns, - s.piles_detail_json, - s.current_price, - s.pro_price, - s.parking_info, - s.distance, - s.valid_start_time AS status_update_time, - pr.schedule_json, - pr.valid_start_time AS schedule_update_time - FROM t_station_profile_scd p - LEFT JOIN t_station_status_scd s - ON p.station_hash = s.station_hash AND s.is_current = 1 - LEFT JOIN t_station_price_schedule_scd pr - ON p.station_hash = pr.station_hash AND pr.is_current = 1 - WHERE p.is_current = 1 AND p.operator = :operator - ORDER BY p.station_name - """ - return await self.db.find(sql, {"operator": operator}) + return await self.model.fetch_current_station_rows(operator) def extract_hourly_prices_from_schedule(schedule_json: Any) -> List[Optional[float]]: diff --git a/Tools/__pycache__/T6_Export.cpython-310.pyc b/Tools/__pycache__/T6_Export.cpython-310.pyc index 65261b30ebd19bf93b4f295e73e19e977ee69e91..eccaa56f49db34dbe0049ad79b9b0aadbcd0046a 100644 GIT binary patch delta 1714 zcmZvdYfKzf6oBW>%r3jjqbw@0hDu9em!Sw-2<1_%U|ZX&?H|%6Y)rT7&RrR@>~8K3 z76}cdPz8+zd!$KQw9sf%Dpu)QttF<$7y850YNE{)6Q9va{n0c&6U2LFQ82p6d~?pZ z=iKv{y)%c}``a?2>FExE|9;4eX{9?SGv=f$XLDF@j0D1u>RK?qEvnLRB14`KiIJGH zp0;=2$GxfGBdXvN)pS*)Qb<%KRqnI+WYwZt`vjjwO;K(9ZdL76QXMpf+NebBA<-qc zgpjPJs%d?)&!LhgLCG+(Hb$8i-_#ML;Y}T}DAOt1Zdlca{Ov3p6tTF4AIf4C#|D1I z(>QdYSLhP_yp|?`-OUY($kO@Eu(V;_ifG7j8V<2;aAw{D<<2aU2KPA2R}${?4!>?% zIeHeuSv}Xs3F|Ta-*Vb*H)U=S_sMYCx!U!280+OmGoKm2`p|uuB^Zvz>3Ljwi~~N( zS_?CoHa8}U-^$TLx`b9?lys3XvQ!PTM%i-$1aX~ZFp?TW-2P}z(a=HT-d z%e?Fejw97x|1gcvju>0N8TPsxycldqG@`PD=x-)Jh0`il9sQ2W$-$U_z#59LuVP2HW5w9R7 z5oe*(6Cmed#uLn0V#TEi4rL`9hMmQy$TKis+;H0n+Mh){hd7QHg>{M>I+b2>6#i5S z$q`spvif$jvwv5xP$0CF>>^hZ(4HU7pPN}h?F4^B=u`l4N*{)gkSyav1 zu|+nDco{KgD4F-OUxQy71xCH6m0zP)Hx2O*PRt(QOiy`(RKIc&EOhfr=gn3+MQ2G*a1#uJx zy%ppfyzOnwyT%RU#IS_5HcdBDqwRXEUH8WVdJCIP)+4qdunAvUkkNon{qYt` zb%u{$NljLM9erle$Hvc#`2sZiGQ>SlS9vd)fu71*aw9QS*(j19VXkV@bpt~_Lp;D? zSSVvrra0IvOs_4?!Fl+WEwmZZ!ZZ?SqwG33sy*U7fy(MSB15FQlKc!Os$14Pf&0uM zenI?(_#N>FhmjUz(N-GN{VW>Q&720Tt8wS%n4gBS|9mW$;s8*)E~`j$MTw+bYM3Od`+! E1tk;4rvLx| delta 2726 zcmZ{mYitx%6oBW>Y`5F(L)y~PRvz02E!&nYv^0?lNNpEEpbagAuptcFo!fTmc4xga z3#9>pP$(2SZHiM#2WzL2(Jr_% z&75Y9sk*l>K!e`Cu*M9^OuNz{M_7 zHDsj&f^On_v&X0FHV}&&$CCbWd`WgL|0z2!ip{ZtcEDhO^iLQWcDZ1PC{rNE%@VWp zJ<^~pd0|{U$!;3@B-^bO)?$sOorb#95Ot?OnoA+ilt56^shQeE4ZmS9_Oz9}qUvD~ zv!`IjW+gBiM_U0W;j!8%ZWKul$rAHP&YiJDywhJ`6*oJlI4Xfn3OrVU6Jam_4c}mu zGf39MHm@ft*)PUB2pb@^_*a9E(>3&K_b&1_Elo^O8usXrrK*MGZol4LeGgs8-%G2V z(YtZtj($a9RM$uP1T>8)a^L9Ub;)AgV`zpyD2LS`)m@G4uK2Vo5*iU!_jEEErT-bi zl**&oS)pVFdanlM9$gEKTrm(~jD`$3%+vrBHM}O>6K2{y6n^$tR1I~>YN%5i=>>ah z_(T7Xw#FY+6&ZBQkPWq$Vujf5h%QIMir=7?G&x$BdGtUxRU$z;GW{alFXkC7D*tBE z*l41bd2OwWT?Q^}?JN}42@RI@fZ_@#j23z4EpncvVDq}p(IoJgRxt=Ze=dU(aK%JCc|^lTfPz`K`|ty7&8Qrr^T z1VT$=O8Vo$;U}Fxn=>i@nt;2mKr--!hjNIx;o6&s>0xt!ImJE+P z!@G`c=gV^|99hI6Ii!-**p}SW#J&?1yZFMq26B;a$(u|1!CZ?B$N~sv@htRFFaRJ~?V=9g>Bk(k_pAnZ37ZLloQC&;EsjE9q33T1tAkTumdxt;7PKUP~k4twFdZ>U7PyYb>-*Z5zx z72z@8R#vuf76G#6bZXs?1%nZ2yduYIrnWh=mnm%4Kqdd*g z+_6wKJk1?^^^6rHm7kii&3+!ee&7r1tI3bNufCk@;rr?<@&-|iwKA(ER`L#hq5kFU Us{BGBI&oE5g`?C77me-CzqM%7cmMzZ diff --git a/Util/RedisKit.py b/Util/RedisKit.py index deb562f..2d69b76 100644 --- a/Util/RedisKit.py +++ b/Util/RedisKit.py @@ -98,6 +98,14 @@ class RedisKit: return False async def set_data(self, key, value, expire=None): + """ + 异步保存数据到Redis + + Args: + key (str): Redis键名 + value (any): 要保存的值 + expire (int, optional): 过期时间(秒) + """ try: await self._ensure_pool() if expire: @@ -108,6 +116,34 @@ class RedisKit: except Exception as e: logger.error(f"保存数据到Redis失败(key={key}): {e}") return False + + async def close(self): + """ + 关闭Redis连接池 + """ + if RedisKit._redis_pool is not None: + try: + # redis-py 4.x+ supports close() + await asyncio.to_thread(RedisKit._redis_pool.close) + RedisKit._redis_pool = None + logger.info("Redis连接池已关闭") + except Exception as e: + logger.error(f"关闭Redis连接池失败: {e}") + + async def delete_data(self, key): + """ + 异步删除Redis中的数据 + + Args: + key (str): Redis键名 + """ + try: + await self._ensure_pool() + await asyncio.to_thread(RedisKit._redis_pool.delete, key) + return True + except Exception as e: + logger.error(f"从Redis删除数据失败(key={key}): {e}") + return False -# ȫʵ +# ȫ��ʵ�� redisKit = RedisKit() diff --git a/Util/__pycache__/RedisKit.cpython-310.pyc b/Util/__pycache__/RedisKit.cpython-310.pyc index d15430c5c0ffa3833d19a6f6459d1927a69fba02..faecc317dde87c73c903a22d5135edab45bdc882 100644 GIT binary patch delta 1395 zcmZvc&u<$=6vyYy{_uMJBaU%G`Xh;(K*cXW|M` z$wYUCSgCp*stPpff2cMjhH(xalIBq~7Kk^Eo_bNg1u!he%)TSayN=5TWxQXSG@9=e zegq#4g)N4Owzei@(L~=!U6BTSQI7Y9kuMq~X5+G*?Q!>Pb8g-Stn%62&p*NJhQE@!dlTj;hjWDgEULz~&4Ulr+RaTBvjv&6ai#Uu z_15x_+sk)%))()8`^nDwN1aUZe&kpiVc195CpIG5llw4gdoXJLjv9Kf9v>1rkr4=s zzNl551pITmjiMv#JH28+iH z%{&L>5&$W8VboxX1isSN_^4qPSQW)c9bL&M@}PkFKq9}G5s`XR{i4rACKyWw(hQ;Oj}`I?G!UVh-!WaO+%Y3OqHCVga_nX zA8i^viw0C5R*%nM{37;JFB&>$suWUGhXll70E{{MpZa$>?r-(=ocpxD#3W6lcDE_$ zbm-@CFc0M(@ryB3^{bltSoN6`h?OGNq#N4B%G3(oSI2agGR3?_|JQZ>#00Ye>QqW}N^ delta 476 zcmdm?yi}SmpO=@50SGP~$;-6no5=TvF={e5qmM9u3U3Nu3u6>tI|B - - - - - + + + + + diff --git a/static/index.html b/static/index.html index c2d9334..641de33 100644 --- a/static/index.html +++ b/static/index.html @@ -41,7 +41,7 @@ - +
🎨

智能海报生成

一键生成精美的营销海报与数据战报。支持自定义模板与实时数据填充,提升品牌传播效率。

diff --git a/static/index_old.html b/static/index_old.html deleted file mode 100644 index 9a6f451..0000000 --- a/static/index_old.html +++ /dev/null @@ -1,352 +0,0 @@ - - - - - -驿来特AI智能数据分析平台 - - - - - - - -
- - -
-
-
-

⚡ 系统特性介绍

-
-
-
-
- 📱 -
-

本系统采用 手机爬虫 获取4家充电供应商准实时各时段电价

-
-
-
- 🧠 -
-

结合 数据仓库与AI技术,对我司电价进行智能分析,给出定价建议

-
-
-
- 📊 -
-

对我司的各场站营业情况进行 分析,查询

-
-
-
- 🎨 -
-

新增 智能海报生成 功能,未来将结合业务数据,一键生成精美的数据战报与营销海报

-
-
-
- 🎥 -
-

新增 抖音知识库:支持视频解析、知识获取与总结、博主专栏订阅,自动生成 充电企业知识日报,助力企业构建专属知识库

-
-
-
- 🎯 -
-

未来:可以根据用户充电信息,形成用户画像,结合企业微信,实现 用户广告的精准推送

-
-
-
- 🧭 -
-

未来:基于 LBS位置服务,智能对比周边竞对场站的价格与配套(快充、休息室等),精准引导用户选择我司优势站点

-
-
- -
-
-
- -
-
⚡ 驿来特AI智能数据分析平台
- -
- -
- - -
-
- -
-
- - -
-
-
-
全网供应商24小时电价监控
-
- - - - - - - - - - - - - - - - -
时段{{ op.label }}
{{ row.hour }} - {{ formatCell(cell.price) }} -
- 数据加载中... -
-
-
-
-
- -
-
- 智能决策分析助手 - -
-
-
-
当前分析任务
-
- 请根据爬取的各供应商分时电价等信息,对各司的定价策略, - 与我司(驿来特)的定价策略进行综合对比,分析我司可能存在的潜在问题。 -
-
- -
-
-
-
- - -
- - -
-
-

⚡ AI正在阅读您的知识库并提炼精华,请稍候...

-
-
- | -
- -
- -
-

- 抖音知识库 - - -

-

自动解析视频、提取文案,构建企业充电知识图谱

-
- - - - - -
- - {{ douyinLoading ? '解析处理中...' : '开始解析' }} - -
-
- - -
- -
-
-
-
-
- - {{ record.status }} - - - {{ formatDate(record.create_time) }} - -
-

- {{ record.video_name || '处理中...' }} -

- - 📺 点击观看视频 - -
- - Del - -
- - -
- Error: {{ record.error_msg }} -
- - -
-

视频文案

-

- {{ record.transcript }} -

- - {{ record.expanded ? '收起' : '展开全文' }} - -
- - -
-
- {{ record.showOriginal ? '收起原始链接' : '查看原始链接信息' }} -
-
- {{ record.original_text }} -
-
-
-
-
- -
-

暂无记录,请粘贴链接开始解析

-
-
-
- - -
- -
-
-

手机扫码访问

-
- -
-

驿来特AI智能数据查询

-

基于大语言模型,为您提供实时、精准的业务数据分析

- -
- - - -
- -
- - {{ text }} - -
-
- -
-
-
- 分析结果 - 完成 - 生成中 -
- - 停止生成 - -
- -
-
-

正在分析数据,请稍候...

-
- | -
-
- -
- - - - - - - - - diff --git a/static/js/dashboard.js b/static/js/dashboard.js index 1a99de4..a3f10bc 100644 --- a/static/js/dashboard.js +++ b/static/js/dashboard.js @@ -12,6 +12,7 @@ createApp({ window.addEventListener('resize', () => { isMobile.value = window.innerWidth <= 768; if (chartInstance) chartInstance.resize(); + if (trendChartInstance) trendChartInstance.resize(); }); // ========================================== @@ -35,7 +36,9 @@ createApp({ const priceTableRows = ref([]); const hourlyPricesByOperator = ref({}); let chartInstance = null; + let trendChartInstance = null; const chartType = ref('line'); + const trendDays = ref(7); // ECharts Initialization const initChart = () => { @@ -49,6 +52,17 @@ createApp({ } }; + const initTrendChart = () => { + const dom = document.getElementById("trendChart"); + if (dom && !trendChartInstance) { + if (typeof echarts === 'undefined') { + console.error("ECharts not loaded"); + return; + } + trendChartInstance = echarts.init(dom); + } + }; + const renderChart = () => { if (!chartInstance) return; @@ -152,6 +166,65 @@ createApp({ } }; + const loadTrendData = async () => { + try { + const res = await axios.get(apiBase.value + "/api/operators/price-trends?days=" + trendDays.value); + if (res && res.data) { + renderTrendChart(res.data); + } + } catch (e) { + console.error("Failed to load trend data:", e); + } + }; + + const renderTrendChart = (data) => { + if (!trendChartInstance) initTrendChart(); + if (!trendChartInstance) return; + + const option = { + backgroundColor: 'transparent', + tooltip: { + trigger: "axis", + backgroundColor: 'rgba(30, 41, 59, 0.9)', + borderColor: '#334155', + textStyle: { color: '#f1f5f9' } + }, + legend: { + data: data.series.map(s => s.name), + textStyle: { color: "#94a3b8" }, + top: 10 + }, + grid: { left: 50, right: 30, top: 60, bottom: 40 }, + xAxis: { + type: "category", + data: data.dates.map(d => d.split('-').slice(1).join('/')), // Show MM/DD + axisLine: { lineStyle: { color: "#475569" } }, + axisLabel: { color: "#94a3b8" } + }, + yAxis: { + type: "value", + name: "元/度", + nameTextStyle: { color: "#94a3b8" }, + axisLine: { lineStyle: { color: "#475569" } }, + axisLabel: { color: "#94a3b8" }, + splitLine: { lineStyle: { color: "#334155", type: 'dashed' } }, + min: (value) => (value.min * 0.95).toFixed(2), + max: (value) => (value.max * 1.05).toFixed(2) + }, + series: data.series.map(s => ({ + name: s.name, + type: 'line', + smooth: true, + symbol: 'circle', + symbolSize: 6, + data: s.data, + emphasis: { focus: 'series' } + })) + }; + + trendChartInstance.setOption(option, true); + }; + const exportAllPrices = async () => { try{ exporting.value = true; @@ -244,48 +317,102 @@ createApp({ return '#f1f5f9'; // Slate-100 }; - // 内置简易 Markdown 解析器 + // Configure Marked + if (typeof marked !== 'undefined') { + marked.use({ + gfm: true, + breaks: true + }); + } + + // 降级用的简易 Markdown 解析器 const simpleMarkdown = (text) => { if (!text) return ''; let lines = text.split('\n'); let html = ''; let inList = false; - const parseInline = (str) => { return str .replace(/\*\*(.*?)\*\*/g, '$1') - .replace(/`(.*?)`/g, '$1'); + .replace(/`(.*?)`/g, '$1'); }; - for (let line of lines) { let trimmed = line.trim(); - if (!trimmed) continue; - + if (!trimmed) { + if (inList) { html += ''; inList = false; } + html += '
'; + continue; + } if (trimmed.startsWith('### ')) { if (inList) { html += ''; inList = false; } html += `

${parseInline(trimmed.substring(4))}

`; - } - else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { + } else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { if (!inList) { html += '
    '; inList = true; } let content = trimmed.replace(/^(- |\d+\. )/, ''); html += `
  • ${parseInline(content)}
  • `; - } - else { + } else { if (inList) { html += '
'; inList = false; } - html += `

${parseInline(trimmed)}

`; + html += `

${parseInline(trimmed)}

`; } } if (inList) html += ''; return html; }; + // 增强的 Markdown & LaTeX 解析器 + const renderMarkdownAndLatex = (text) => { + if (!text) return ''; + + try { + // 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $) + let processedText = text; + + // 处理块级 LaTeX: $$ ... $$ + processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => { + try { + if (typeof katex !== 'undefined') { + return '
' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '
'; + } + return match; + } catch (e) { + return match; + } + }); + + // 处理行内 LaTeX: $ ... $ + processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => { + try { + if (typeof katex !== 'undefined') { + return katex.renderToString(formula, { displayMode: false, throwOnError: false }); + } + return match; + } catch (e) { + return match; + } + }); + + // 2. 使用 marked 解析 Markdown + if (typeof marked !== 'undefined') { + return marked.parse(processedText); + } else { + // 降级使用之前的 simpleMarkdown + return simpleMarkdown(processedText); + } + } catch (e) { + console.error('Markdown/LaTeX rendering error:', e); + return text; + } + }; + const renderedAiText = computed(() => { - return simpleMarkdown(aiText.value); + return renderMarkdownAndLatex(aiText.value); }); onMounted(() => { initChart(); + initTrendChart(); loadAllOperatorsPrices(); + loadTrendData(); }); return { @@ -299,9 +426,11 @@ createApp({ priceTableRows, hourlyPricesByOperator, chartType, + trendDays, // Actions loadAllOperatorsPrices, + loadTrendData, exportAllPrices, exportAiReport, startAiAnalysis, diff --git a/static/js/douyin.js b/static/js/douyin.js index a031d8d..051dfca 100644 --- a/static/js/douyin.js +++ b/static/js/douyin.js @@ -15,53 +15,96 @@ createApp({ const summaryLoading = ref(false); const summaryText = ref(''); - // Simple Markdown Parser (Zero Dependency) + // 降级用的简易 Markdown 解析器 const simpleMarkdown = (text) => { if (!text) return ''; let lines = text.split('\n'); let html = ''; let inList = false; - - // Helper: Parse inline styles const parseInline = (str) => { return str - .replace(/\*\*(.*?)\*\*/g, '$1') // Bold - .replace(/`(.*?)`/g, '$1'); // Code + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/`(.*?)`/g, '$1'); }; - for (let line of lines) { let trimmed = line.trim(); - if (!trimmed) continue; - - // Headers + if (!trimmed) { + if (inList) { html += ''; inList = false; } + html += '
'; + continue; + } if (trimmed.startsWith('### ')) { if (inList) { html += ''; inList = false; } - html += `

${parseInline(trimmed.substring(4))}

`; - } - // Lists - else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { - if (!inList) { html += '
    '; inList = true; } + html += `

    ${parseInline(trimmed.substring(4))}

    `; + } else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { + if (!inList) { html += '
      '; inList = true; } let content = trimmed.replace(/^(- |\d+\. )/, ''); - html += `
    • ${parseInline(content)}
    • `; - } - // Paragraphs - else { + html += `
    • ${parseInline(content)}
    • `; + } else { if (inList) { html += '
    '; inList = false; } - html += `

    ${parseInline(trimmed)}

    `; + html += `

    ${parseInline(trimmed)}

    `; } } if (inList) html += '
'; return html; }; + // Configure Marked + if (typeof marked !== 'undefined') { + marked.use({ + gfm: true, + breaks: true + }); + } + + // 增强的 Markdown & LaTeX 解析器 + const renderMarkdownAndLatex = (text) => { + if (!text) return ''; + + try { + // 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $) + let processedText = text; + + // 处理块级 LaTeX: $$ ... $$ + processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => { + try { + if (typeof katex !== 'undefined') { + return '
' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '
'; + } + return match; + } catch (e) { + return match; + } + }); + + // 处理行内 LaTeX: $ ... $ + processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => { + try { + if (typeof katex !== 'undefined') { + return katex.renderToString(formula, { displayMode: false, throwOnError: false }); + } + return match; + } catch (e) { + return match; + } + }); + + // 2. 使用 marked 解析 Markdown + if (typeof marked !== 'undefined') { + return marked.parse(processedText); + } else { + // 降级使用之前的 simpleMarkdown + return simpleMarkdown(processedText); + } + } catch (e) { + console.error('Markdown/LaTeX rendering error:', e); + return text; + } + }; + const renderedSummary = computed(() => { if (!summaryText.value) return ''; - try { - return simpleMarkdown(summaryText.value); - } catch (e) { - console.error("Simple markdown error:", e); - return summaryText.value; - } + return renderMarkdownAndLatex(summaryText.value); }); // Methods diff --git a/static/HaiBao/js/app.js b/static/js/haibao.js similarity index 100% rename from static/HaiBao/js/app.js rename to static/js/haibao.js diff --git a/static/js/query.js b/static/js/query.js index 7e67c9a..ce6c417 100644 --- a/static/js/query.js +++ b/static/js/query.js @@ -168,34 +168,104 @@ createApp({ // Configure Marked if (typeof marked !== 'undefined') { - marked.use({ - gfm: true, - breaks: true, - renderer: { - code(code, language) { - if (language === 'echarts') { - const id = 'chart-' + Math.random().toString(36).substr(2, 9); - return `
`; - } - return false; - } + const renderer = new marked.Renderer(); + const oldCode = renderer.code.bind(renderer); + renderer.code = function(code, language) { + if (language === 'echarts') { + const id = 'chart-' + Math.random().toString(36).substr(2, 9); + return `
`; } + return oldCode(code, language); + }; + marked.setOptions({ + renderer: renderer, + gfm: true, + breaks: true }); } - const renderedResult = computed(() => { - if (!queryResult.value) return ''; - try { - // Remove escapes that might break markdown - const cleanText = queryResult.value.replace(/\\([\*_`#\[\]\(\)!>-])/g, '$1'); - if (typeof marked !== 'undefined') { - return marked.parse(cleanText); + // 降级用的简易 Markdown 解析器 + const simpleMarkdown = (text) => { + if (!text) return ''; + let lines = text.split('\n'); + let html = ''; + let inList = false; + const parseInline = (str) => { + return str + .replace(/\*\*(.*?)\*\*/g, '$1') + .replace(/`(.*?)`/g, '$1'); + }; + for (let line of lines) { + let trimmed = line.trim(); + if (!trimmed) { + if (inList) { html += ''; inList = false; } + html += '
'; + continue; + } + if (trimmed.startsWith('### ')) { + if (inList) { html += ''; inList = false; } + html += `

${parseInline(trimmed.substring(4))}

`; + } else if (trimmed.startsWith('- ') || /^\d+\./.test(trimmed)) { + if (!inList) { html += '
    '; inList = true; } + let content = trimmed.replace(/^(- |\d+\. )/, ''); + html += `
  • ${parseInline(content)}
  • `; + } else { + if (inList) { html += '
'; inList = false; } + html += `

${parseInline(trimmed)}

`; } - return cleanText; - } catch (e) { - console.error('Markdown parsing error:', e); - return queryResult.value; } + if (inList) html += ''; + return html; + }; + + // 增强的 Markdown & LaTeX 解析器 + const renderMarkdownAndLatex = (text) => { + if (!text) return ''; + + try { + // 1. 处理 LaTeX (简单替换,先处理 $$ 再处理 $) + let processedText = text; + + // 处理块级 LaTeX: $$ ... $$ + processedText = processedText.replace(/\$\$\s*([\s\S]*?)\s*\$\$/g, (match, formula) => { + try { + if (typeof katex !== 'undefined') { + return '
' + katex.renderToString(formula, { displayMode: true, throwOnError: false }) + '
'; + } + return match; + } catch (e) { + return match; + } + }); + + // 处理行内 LaTeX: $ ... $ + processedText = processedText.replace(/\$([^\$\n]+?)\$/g, (match, formula) => { + try { + if (typeof katex !== 'undefined') { + return katex.renderToString(formula, { displayMode: false, throwOnError: false }); + } + return match; + } catch (e) { + return match; + } + }); + + // 2. 使用 marked 解析 Markdown + if (typeof marked !== 'undefined') { + // 直接解析处理后的文本,marked 会处理 Markdown 转义 + return marked.parse(processedText); + } else { + // 降级使用之前的 simpleMarkdown + return simpleMarkdown(processedText); + } + } catch (e) { + console.error('Markdown/LaTeX rendering error:', e); + return text; + } + }; + + const renderedResult = computed(() => { + return renderMarkdownAndLatex(queryResult.value); }); watch(queryResult, () => { diff --git a/static/query.html b/static/query.html index dc1ade9..b1e2425 100644 --- a/static/query.html +++ b/static/query.html @@ -82,6 +82,9 @@ + + +
- +
- 🕒 分时电价明细 + 📊 供应商实时价格对比
@@ -50,6 +50,18 @@
+ + +
+
+ 📉 供应商价格变动趋势 (最近{{ trendDays }}天) +
+ + +
+
+
+
@@ -66,15 +78,12 @@
-
+
🤖 AI 调价策略建议 -
- 点击“开始分析”获取AI智能定价建议 + AI 正在深度分析中
@@ -85,6 +94,10 @@ + + + + diff --git a/static/douyin.html b/static/douyin.html index bbfcd0e..22c7833 100644 --- a/static/douyin.html +++ b/static/douyin.html @@ -163,6 +163,10 @@ + + + + diff --git a/static/HaiBao/index.html b/static/haibao.html similarity index 93% rename from static/HaiBao/index.html rename to static/haibao.html index 29450c2..8700df7 100644 --- a/static/HaiBao/index.html +++ b/static/haibao.html @@ -4,8 +4,8 @@ 驿来特 - 智能海报生成工作台 - - + + @@ -17,7 +17,7 @@
基于 AI 大模型,快速生成高质量企业宣传海报
@@ -151,10 +151,10 @@