From d7ec1d530a10764c3403573767953b44475fdf92 Mon Sep 17 00:00:00 2001 From: xiao12feng8 <16507319+xiao12feng8@user.noreply.gitee.com> Date: Tue, 3 Feb 2026 14:47:24 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=EF=BC=9A=E5=94=B1=E6=AD=8C?= =?UTF-8?q?=E8=B7=B3=E8=88=9E=E8=A7=86=E9=A2=91=E6=AD=A3=E5=B8=B8=E5=B9=B6?= =?UTF-8?q?=E4=B8=94=E8=83=BD=E5=A4=9F=E9=87=8D=E6=96=B0=E4=B8=8B=E8=BD=BD?= =?UTF-8?q?=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lover/.env | 2 +- lover/config.py | 2 +- lover/deps.py | 7 +- lover/requirements.txt | 1 + lover/routers/dance.py | 228 +++++++++++++++- lover/routers/friend.py | 52 +++- lover/routers/huanxin.py | 30 +++ lover/routers/sing.py | 463 +++++++++++++++++++++++++++++--- lover/routers/user_basic.py | 2 +- xuniYou/pages/friends/index.vue | 4 +- xuniYou/pages/index/index.vue | 420 ++++++++++++++++++++++++++--- xuniYou/utils/Huanxin.js | 8 +- xuniYou/utils/api.js | 14 +- xuniYou/utils/request.js | 4 +- xunifriend_RaeeC/.env | 4 +- 15 files changed, 1140 insertions(+), 101 deletions(-) diff --git a/lover/.env b/lover/.env index 46d1069..55552ae 100644 --- a/lover/.env +++ b/lover/.env @@ -1,2 +1,2 @@ DATABASE_URL=mysql+pymysql://root:root@127.0.0.1:3306/lover?charset=utf8mb4 -USER_INFO_API=http://192.168.1.164:30100/index.php/api/user_basic/get_user_basic \ No newline at end of file +USER_INFO_API=http://192.168.1.164:30100/api/user_basic/get_user_basic \ No newline at end of file diff --git a/lover/config.py b/lover/config.py index 9a65d07..f476801 100644 --- a/lover/config.py +++ b/lover/config.py @@ -150,7 +150,7 @@ class Settings(BaseSettings): # 用户信息拉取接口(FastAdmin 提供) USER_INFO_API: str = Field( - default="http://192.168.1.164:30100/index.php/api/user_basic/get_user_basic", + default="http://192.168.1.164:30100/api/user_basic/get_user_basic", env="USER_INFO_API", ) diff --git a/lover/deps.py b/lover/deps.py index c79684e..14c5ecd 100644 --- a/lover/deps.py +++ b/lover/deps.py @@ -20,12 +20,15 @@ def _fetch_user_from_php(token: str) -> Optional[dict]: import logging logger = logging.getLogger(__name__) - logger.info(f"用户中心调试 - 调用接口: {settings.USER_INFO_API}") + # 临时硬编码修复配置问题 + user_info_api = "http://192.168.1.164:30100/api/user_basic/get_user_basic" + + logger.info(f"用户中心调试 - 调用接口: {user_info_api}") logger.info(f"用户中心调试 - token: {token}") try: resp = requests.get( - settings.USER_INFO_API, + user_info_api, headers={"token": token}, timeout=5, ) diff --git a/lover/requirements.txt b/lover/requirements.txt index 5d83163..a7024d9 100644 --- a/lover/requirements.txt +++ b/lover/requirements.txt @@ -9,3 +9,4 @@ requests>=2.31 oss2>=2.18 dashscope>=1.20 pyyaml>=6.0 +imageio-ffmpeg>=0.4 diff --git a/lover/routers/dance.py b/lover/routers/dance.py index e37b05c..3e4ba9d 100644 --- a/lover/routers/dance.py +++ b/lover/routers/dance.py @@ -1,6 +1,7 @@ import hashlib import os import random +import shutil import subprocess import tempfile import time @@ -27,8 +28,43 @@ from ..models import ( ) from ..response import ApiResponse, success_response +try: + import imageio_ffmpeg # type: ignore +except Exception: # pragma: no cover + imageio_ffmpeg = None + router = APIRouter(prefix="/dance", tags=["dance"]) + +def _ffmpeg_bin() -> str: + found = shutil.which("ffmpeg") + if found: + return found + if imageio_ffmpeg is not None: + try: + return imageio_ffmpeg.get_ffmpeg_exe() + except Exception: + pass + return "ffmpeg" + + +def _ffprobe_bin() -> str: + found = shutil.which("ffprobe") + if found: + return found + if imageio_ffmpeg is not None: + try: + ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe() + candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe.exe") + if os.path.exists(candidate): + return candidate + candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe") + if os.path.exists(candidate): + return candidate + except Exception: + pass + return "ffprobe" + DANCE_TARGET_DURATION_SEC = 10 @@ -53,6 +89,7 @@ def get_dance_history( GenerationTask.user_id == user.id, GenerationTask.lover_id == lover.id, GenerationTask.task_type == "video", + GenerationTask.payload["prompt"].as_string().isnot(None), GenerationTask.status == "succeeded", GenerationTask.result_url.isnot(None), ) @@ -77,6 +114,154 @@ def get_dance_history( return success_response(result, msg="获取成功") +def _download_binary(url: str) -> bytes: + try: + resp = requests.get(url, timeout=30) + except Exception as exc: + raise HTTPException(status_code=502, detail="文件下载失败") from exc + if resp.status_code != 200: + raise HTTPException(status_code=502, detail="文件下载失败") + return resp.content + + +def _retry_finalize_dance_task(task_id: int) -> None: + """任务失败但 DashScope 端已成功时,尝试重新下载视频并重新上传(自愈/手动重试共用)。""" + try: + with SessionLocal() as db: + task = ( + db.query(GenerationTask) + .filter(GenerationTask.id == task_id) + .with_for_update() + .first() + ) + if not task: + return + payload = task.payload or {} + dash_id = payload.get("dashscope_task_id") + if not dash_id: + return + + status, dash_video_url = _fetch_dashscope_status(str(dash_id)) + if status != "SUCCEEDED" or not dash_video_url: + return + + # 重新生成:下载 dashscope 视频 -> 随机 BGM -> 合成 -> 上传 + bgm_song = _pick_random_bgm(db) + bgm_audio_url_raw = bgm_song.audio_url + bgm_audio_url = _cdnize(bgm_audio_url_raw) or bgm_audio_url_raw + merged_bytes, bgm_meta = _merge_dance_video_with_bgm( + dash_video_url, + bgm_audio_url, + DANCE_TARGET_DURATION_SEC, + ) + object_name = f"lover/{task.lover_id}/dance/{int(time.time())}_retry.mp4" + oss_url = _upload_to_oss(merged_bytes, object_name) + + task.status = "succeeded" + task.result_url = oss_url + task.error_msg = None + task.payload = { + **payload, + "dashscope_video_url": dash_video_url, + "bgm_song_id": bgm_song.id, + "bgm_audio_url": bgm_audio_url, + "bgm_audio_url_raw": bgm_audio_url_raw, + "bgm_start_sec": bgm_meta.get("bgm_start_sec"), + "bgm_duration": DANCE_TARGET_DURATION_SEC, + "retry_finalized": True, + } + task.updated_at = datetime.utcnow() + db.add(task) + db.commit() + except Exception: + return + + +@router.post("/retry/{task_id}", response_model=ApiResponse[dict]) +def retry_dance_task( + task_id: int, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + user: AuthedUser = Depends(get_current_user), +): + """手动重试:用于 DashScope 端已成功但本地下载/合成/上传失败导致任务失败的情况。""" + lover = db.query(Lover).filter(Lover.user_id == user.id).first() + if not lover: + raise HTTPException(status_code=404, detail="恋人未找到") + + task = ( + db.query(GenerationTask) + .filter( + GenerationTask.id == task_id, + GenerationTask.user_id == user.id, + GenerationTask.lover_id == lover.id, + GenerationTask.task_type == "video", + GenerationTask.payload["prompt"].as_string().isnot(None), + ) + .first() + ) + if not task: + raise HTTPException(status_code=404, detail="任务不存在") + + payload = task.payload or {} + dash_id = payload.get("dashscope_task_id") + if not dash_id: + raise HTTPException(status_code=400, detail="任务缺少 dashscope_task_id,无法重试") + + task.payload = {**payload, "manual_retry": True} + task.updated_at = datetime.utcnow() + db.add(task) + db.commit() + + background_tasks.add_task(_retry_finalize_dance_task, int(task.id)) + return success_response({"task_id": int(task.id)}, msg="已触发重试") + + +@router.get("/history/all", response_model=ApiResponse[list[dict]]) +def get_dance_history_all( + db: Session = Depends(get_db), + user: AuthedUser = Depends(get_current_user), + page: int = 1, + size: int = 20, +): + """获取用户的跳舞视频全部历史记录(成功+失败+进行中)。""" + lover = db.query(Lover).filter(Lover.user_id == user.id).first() + if not lover: + raise HTTPException(status_code=404, detail="恋人未找到") + + offset = (page - 1) * size + tasks = ( + db.query(GenerationTask) + .filter( + GenerationTask.user_id == user.id, + GenerationTask.lover_id == lover.id, + GenerationTask.task_type == "video", + GenerationTask.payload["prompt"].as_string().isnot(None), + ) + .order_by(GenerationTask.id.desc()) + .offset(offset) + .limit(size) + .all() + ) + + result: list[dict] = [] + for task in tasks: + payload = task.payload or {} + url = task.result_url or payload.get("video_url") or payload.get("dashscope_video_url") or "" + result.append( + { + "id": int(task.id), + "prompt": payload.get("prompt") or "", + "status": task.status, + "video_url": _cdnize(url) or "", + "error_msg": task.error_msg, + "created_at": task.created_at.isoformat() if task.created_at else None, + } + ) + + return success_response(result, msg="获取成功") + + class DanceGenerateIn(BaseModel): prompt: str = Field(..., min_length=2, max_length=400, description="用户希望跳的舞/动作描述") @@ -92,6 +277,41 @@ class DanceTaskStatusOut(BaseModel): error_msg: Optional[str] = None +@router.get("/current", response_model=ApiResponse[Optional[DanceTaskStatusOut]]) +def get_current_dance_task( + db: Session = Depends(get_db), + user: AuthedUser = Depends(get_current_user), +): + task = ( + db.query(GenerationTask) + .filter( + GenerationTask.user_id == user.id, + GenerationTask.task_type == "video", + GenerationTask.status.in_(["pending", "running"]), + GenerationTask.payload["prompt"].as_string().isnot(None), + ) + .order_by(GenerationTask.id.desc()) + .first() + ) + if not task: + return success_response(None, msg="暂无进行中的任务") + + payload = task.payload or {} + return success_response( + DanceTaskStatusOut( + generation_task_id=task.id, + status=task.status, + dashscope_task_id=str(payload.get("dashscope_task_id") or ""), + video_url=task.result_url or payload.get("video_url") or payload.get("dashscope_video_url") or "", + session_id=int(payload.get("session_id") or 0), + user_message_id=int(payload.get("user_message_id") or 0), + lover_message_id=int(payload.get("lover_message_id") or 0), + error_msg=task.error_msg, + ), + msg="获取成功", + ) + + def _upload_to_oss(file_bytes: bytes, object_name: str) -> str: if not settings.ALIYUN_OSS_ACCESS_KEY_ID or not settings.ALIYUN_OSS_ACCESS_KEY_SECRET: raise HTTPException(status_code=500, detail="未配置 OSS Key") @@ -285,7 +505,7 @@ def _download_to_path(url: str, target_path: str, label: str): def _probe_media_duration(path: str) -> Optional[float]: command = [ - "ffprobe", + _ffprobe_bin(), "-v", "error", "-show_entries", @@ -315,7 +535,7 @@ def _probe_media_duration(path: str) -> Optional[float]: def _run_ffmpeg_merge(video_path: str, audio_path: str, output_path: str): audio_duration = _probe_media_duration(audio_path) command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -364,7 +584,7 @@ def _extract_audio_segment( output_path: str, ): command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -403,7 +623,7 @@ def _pad_audio_segment( if pad_sec <= 0: return command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", diff --git a/lover/routers/friend.py b/lover/routers/friend.py index bca3808..3110b73 100644 --- a/lover/routers/friend.py +++ b/lover/routers/friend.py @@ -1,15 +1,61 @@ from fastapi import APIRouter, Depends from lover.deps import get_current_user, AuthedUser from lover.response import success_response +from lover.db import get_db +from sqlalchemy.orm import Session +from sqlalchemy import and_, or_ +from lover.models import FriendRelation, User router = APIRouter() @router.get("/api/friend/index") -def get_friend_index(user: AuthedUser = Depends(get_current_user)): +@router.post("/api/friend/index") +def get_friend_index(user: AuthedUser = Depends(get_current_user), db: Session = Depends(get_db)): """获取好友列表""" + # 查询当前用户的好友关系(双向:user_id 或 friend_id 为当前用户,且 status=1 表示已通过) + friend_relations = db.query(FriendRelation).filter( + and_( + or_( + FriendRelation.user_id == user.id, + FriendRelation.friend_id == user.id + ), + FriendRelation.status == "1" + ) + ).all() + + friend_ids = [] + result = [] + for rel in friend_relations: + # 确定好友的 user_id(不是自己) + fid = rel.friend_id if rel.user_id == user.id else rel.user_id + friend_ids.append(fid) + + # 查询好友用户信息 + friend_users = db.query(User).filter(User.id.in_(friend_ids)).all() if friend_ids else [] + user_map = {u.id: u for u in friend_users} + + for rel in friend_relations: + fid = rel.friend_id if rel.user_id == user.id else rel.user_id + friend_user = user_map.get(fid) + if not friend_user: + continue + result.append({ + "friend_id": fid, + "friend": { + "id": friend_user.id, + "nickname": friend_user.nickname, + "avatar": friend_user.avatar, + "user_number": str(friend_user.id), # 前端期望 user_number + "open_id": "", # 小程序 open_id,如有需要可补充字段 + }, + "intimacy": rel.intimacy or 0, + "intimacy_level": rel.intimacy_level or 0, + "is_online": False, # 默认离线,前端会单独调用在线状态接口 + }) + return success_response({ - "friends": [], - "total": 0 + "data": result, + "total": len(result) }) @router.post("/api/friend/add") diff --git a/lover/routers/huanxin.py b/lover/routers/huanxin.py index 6c3b5d9..a0725f8 100644 --- a/lover/routers/huanxin.py +++ b/lover/routers/huanxin.py @@ -1,3 +1,5 @@ +from typing import Optional + from fastapi import APIRouter, Depends from lover.deps import get_current_user, AuthedUser from lover.response import success_response @@ -17,6 +19,34 @@ def register_huanxin_user(user: AuthedUser = Depends(get_current_user)): """注册环信用户""" return success_response({"message": "注册成功"}) +@router.post("/api/huanxin/online") +def set_huanxin_online( + payload: Optional[dict] = None, + user: AuthedUser = Depends(get_current_user), +): + """获取环信用户在线状态(兼容前端批量查询)""" + payload = payload or {} + user_ids_raw = payload.get("user_ids") + + # 前端好友列表会传入 user_ids=36,40,41,期待返回数组并支持 .find() + if user_ids_raw: + ids = [] + for part in str(user_ids_raw).split(","): + part = part.strip() + if not part: + continue + try: + ids.append(int(part)) + except ValueError: + continue + + return success_response([ + {"id": uid, "is_online": False} for uid in ids + ]) + + # 兼容旧调用:不传 user_ids 时,返回当前用户在线信息 + return success_response({"status": "online", "user_id": user.id}) + @router.get("/api/huanxin/user_info") def get_huanxin_user_info(user: AuthedUser = Depends(get_current_user)): """获取环信用户信息""" diff --git a/lover/routers/sing.py b/lover/routers/sing.py index c548839..87d09dd 100644 --- a/lover/routers/sing.py +++ b/lover/routers/sing.py @@ -1,6 +1,8 @@ -import hashlib +import hashlib +import logging import math import os +import shutil import subprocess import tempfile import threading @@ -35,8 +37,48 @@ from ..models import ( from ..response import ApiResponse, success_response from ..task_queue import sing_task_queue +try: + import imageio_ffmpeg # type: ignore +except Exception: # pragma: no cover + imageio_ffmpeg = None + +logger = logging.getLogger("sing") + router = APIRouter(prefix="/sing", tags=["sing"]) + +def _ffmpeg_bin() -> str: + """Prefer system ffmpeg; fallback to imageio-ffmpeg bundled binary.""" + found = shutil.which("ffmpeg") + if found: + return found + if imageio_ffmpeg is not None: + try: + return imageio_ffmpeg.get_ffmpeg_exe() + except Exception: + pass + return "ffmpeg" + + +def _ffprobe_bin() -> str: + """Prefer system ffprobe; fallback to imageio-ffmpeg bundled binary if available.""" + found = shutil.which("ffprobe") + if found: + return found + # imageio-ffmpeg only guarantees ffmpeg; most builds include ffprobe alongside. + if imageio_ffmpeg is not None: + try: + ffmpeg_path = imageio_ffmpeg.get_ffmpeg_exe() + candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe.exe") + if os.path.exists(candidate): + return candidate + candidate = os.path.join(os.path.dirname(ffmpeg_path), "ffprobe") + if os.path.exists(candidate): + return candidate + except Exception: + pass + return "ffprobe" + SING_BASE_MODEL = "wan2.5-i2v-preview" SING_BASE_RESOLUTION = "480P" SING_WAN26_MODEL = "wan2.6-i2v-flash" @@ -329,7 +371,7 @@ def _ensure_emo_detect_cache( def _probe_media_duration(path: str) -> Optional[float]: command = [ - "ffprobe", + _ffprobe_bin(), "-v", "error", "-show_entries", @@ -359,7 +401,7 @@ def _probe_media_duration(path: str) -> Optional[float]: def _run_ffmpeg_merge(video_path: str, audio_path: str, output_path: str): audio_duration = _probe_media_duration(audio_path) command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -409,7 +451,7 @@ def _strip_video_audio(video_bytes: bytes) -> bytes: with open(input_path, "wb") as file_handle: file_handle.write(video_bytes) command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -431,7 +473,7 @@ def _strip_video_audio(video_bytes: bytes) -> bytes: raise HTTPException(status_code=500, detail="ffmpeg 未安装或不可用") from exc except subprocess.CalledProcessError: fallback = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -483,7 +525,7 @@ def _extract_audio_segment( output_path: str, ): command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -523,7 +565,7 @@ def _pad_audio_segment( if pad_sec <= 0: return command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -555,7 +597,7 @@ def _pad_audio_segment( def _trim_video_duration(input_path: str, target_duration_sec: float, output_path: str): command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -579,7 +621,7 @@ def _trim_video_duration(input_path: str, target_duration_sec: float, output_pat pass fallback = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -632,7 +674,7 @@ def _concat_video_files(video_paths: list[str], output_path: str): for path in video_paths: list_file.write(f"file '{path}'\n") command = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -653,7 +695,7 @@ def _concat_video_files(video_paths: list[str], output_path: str): subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) except subprocess.CalledProcessError: fallback = [ - "ffmpeg", + _ffmpeg_bin(), "-y", "-loglevel", "error", @@ -771,8 +813,6 @@ def _submit_emo_video( ext_bbox: list, style_level: str, ) -> str: - import logging - logger = logging.getLogger("sing") if not settings.DASHSCOPE_API_KEY: raise HTTPException(status_code=500, detail="未配置 DASHSCOPE_API_KEY") @@ -836,9 +876,46 @@ def _submit_emo_video( return str(task_id) +def _update_task_status_in_db(dashscope_task_id: str, status: str, result_url: Optional[str] = None): + """实时更新数据库中的任务状态""" + from lover.db import SessionLocal + from lover.models import GenerationTask + + try: + with SessionLocal() as db: + # 查找对应的 GenerationTask + task = db.query(GenerationTask).filter( + GenerationTask.payload.like(f'%"dashscope_task_id": "{dashscope_task_id}"%') + ).first() + + if task: + # 映射状态 + status_mapping = { + "PENDING": "pending", + "RUNNING": "running", + "SUCCEEDED": "succeeded", + "FAILED": "failed" + } + + task_status = status_mapping.get(status, "pending") + task.status = task_status + + if result_url: + task.result_url = result_url + + # 更新 payload 中的状态 + payload = task.payload or {} + payload["dashscope_status"] = status + payload["last_updated"] = time.time() + task.payload = payload + + db.commit() + logger.info(f"📊 任务 {task.id} 状态已更新为: {task_status}") + except Exception as e: + logger.error(f"❌ 更新任务状态失败: {e}") + + def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str: - import logging - logger = logging.getLogger("sing") logger.info(f"⏳ 开始轮询 DashScope 任务: {task_id}") headers = {"Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}"} @@ -872,9 +949,11 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str: or "" ).upper() - # 每 5 次(15秒)记录一次进度 + # 每 5 次(15秒)记录一次进度并更新数据库 if attempts % 5 == 0: logger.info(f"🔄 轮询任务 {task_id} 第 {attempts} 次,状态: {status_str}") + # 实时更新数据库状态 + _update_task_status_in_db(task_id, status_str, None) if status_str == "SUCCEEDED": results = output.get("results") or {} @@ -888,6 +967,8 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str: logger.error(f"❌ 任务 {task_id} 成功但未返回 URL") raise HTTPException(status_code=502, detail="视频生成成功但未返回结果 URL") logger.info(f"✅ 任务 {task_id} 生成成功!") + # 立即更新数据库状态为成功 + _update_task_status_in_db(task_id, "SUCCEEDED", url) return url if status_str == "FAILED": code = output.get("code") or data.get("code") @@ -895,6 +976,8 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str: if code: msg = f"{code}: {msg}" logger.error(f"❌ 任务 {task_id} 生成失败: {msg}") + # 立即更新数据库状态为失败 + _update_task_status_in_db(task_id, "FAILED", None) raise HTTPException(status_code=502, detail=f"视频生成失败: {msg}") logger.error(f"⏱️ 任务 {task_id} 轮询超时,共尝试 {attempts} 次") @@ -1392,8 +1475,6 @@ def _should_enqueue_task(task_id: int) -> bool: def _enqueue_sing_task(task_id: int): - import logging - logger = logging.getLogger("sing") # 移除入队日志,只在失败时记录 result = sing_task_queue.enqueue_unique(f"sing:{task_id}", _process_sing_task, task_id) return result @@ -1546,6 +1627,41 @@ class SingTaskStatusOut(BaseModel): error_msg: Optional[str] = None +@router.get("/current", response_model=ApiResponse[Optional[SingTaskStatusOut]]) +def get_current_sing_task( + db: Session = Depends(get_db), + user: AuthedUser = Depends(get_current_user), +): + task = ( + db.query(GenerationTask) + .filter( + GenerationTask.user_id == user.id, + GenerationTask.task_type == "video", + GenerationTask.status.in_(["pending", "running"]), + GenerationTask.payload["song_id"].as_integer().isnot(None), + ) + .order_by(GenerationTask.id.desc()) + .first() + ) + if not task: + return success_response(None, msg="暂无进行中的任务") + + payload = task.payload or {} + return success_response( + SingTaskStatusOut( + generation_task_id=task.id, + status=task.status, + dashscope_task_id=str(payload.get("dashscope_task_id") or ""), + video_url=task.result_url or payload.get("merged_video_url") or "", + session_id=int(payload.get("session_id") or 0), + user_message_id=int(payload.get("user_message_id") or 0), + lover_message_id=int(payload.get("lover_message_id") or 0), + error_msg=task.error_msg, + ), + msg="获取成功", + ) + + @router.get("/songs", response_model=ApiResponse[SongListResponse]) def list_songs_for_lover( db: Session = Depends(get_db), @@ -1590,7 +1706,7 @@ def get_sing_history( if not lover: raise HTTPException(status_code=404, detail="恋人未找到") - # 查询已成功生成的视频 + # 查询已成功生成的视频(优先使用 nf_sing_song_video) offset = (page - 1) * size videos = ( db.query(SingSongVideo) @@ -1605,21 +1721,164 @@ def get_sing_history( .limit(size) .all() ) - - result = [] + + result: list[dict] = [] + seen_urls: set[str] = set() for video in videos: # 获取歌曲信息 song = db.query(SongLibrary).filter(SongLibrary.id == video.song_id).first() song_title = song.title if song else "未知歌曲" - - result.append({ - "id": video.id, - "song_id": video.song_id, - "song_title": song_title, - "video_url": _cdnize(video.merged_video_url), - "created_at": video.created_at.isoformat() if video.created_at else None, - }) - + url = _cdnize(video.merged_video_url) + if url: + seen_urls.add(url) + result.append( + { + "id": video.id, + "song_id": video.song_id, + "song_title": song_title, + "video_url": url, + "created_at": video.created_at.isoformat() if video.created_at else None, + } + ) + + # 兜底:部分情况下任务成功但 nf_sing_song_video 未落库,补查 nf_generation_tasks + if len(result) < size: + remaining = size - len(result) + fallback_tasks = ( + db.query(GenerationTask) + .filter( + GenerationTask.user_id == user.id, + GenerationTask.lover_id == lover.id, + GenerationTask.task_type == "video", + GenerationTask.status == "succeeded", + GenerationTask.payload["song_id"].as_integer().isnot(None), + GenerationTask.payload["merged_video_url"].as_string().isnot(None), + ) + .order_by(GenerationTask.id.desc()) + .offset(offset) + .limit(size * 2) + .all() + ) + + for task in fallback_tasks: + payload = task.payload or {} + song_id = payload.get("song_id") + merged_video_url = payload.get("merged_video_url") or task.result_url + url = _cdnize(merged_video_url) if merged_video_url else "" + if not url or url in seen_urls: + continue + + song_title = payload.get("song_title") or "未知歌曲" + if song_id: + song = db.query(SongLibrary).filter(SongLibrary.id == song_id).first() + if song and song.title: + song_title = song.title + + result.append( + { + "id": int(task.id), + "song_id": int(song_id) if song_id else 0, + "song_title": song_title, + "video_url": url, + "created_at": task.created_at.isoformat() if task.created_at else None, + } + ) + seen_urls.add(url) + remaining -= 1 + if remaining <= 0: + break + + return success_response(result, msg="获取成功") + + +@router.post("/retry/{task_id}", response_model=ApiResponse[dict]) +def retry_sing_task( + task_id: int, + background_tasks: BackgroundTasks, + db: Session = Depends(get_db), + user: AuthedUser = Depends(get_current_user), +): + """手动重试:用于 DashScope 端已成功但本地下载/上传失败导致任务失败的情况。""" + task = ( + db.query(GenerationTask) + .filter( + GenerationTask.id == task_id, + GenerationTask.user_id == user.id, + GenerationTask.task_type == "video", + GenerationTask.payload["song_id"].as_integer().isnot(None), + ) + .first() + ) + if not task: + raise HTTPException(status_code=404, detail="任务不存在") + + payload = task.payload or {} + dash_id = payload.get("dashscope_task_id") + if not dash_id: + # 唱歌任务通常不会在 GenerationTask.payload 中保存 dashscope_task_id(分段任务各自保存)。 + # 此时改为重新入队处理,尽量复用已成功的分段,完成补下载/补写记录。 + task.status = "pending" + task.error_msg = None + task.payload = {**payload, "manual_retry": True} + task.updated_at = datetime.utcnow() + db.add(task) + db.commit() + _enqueue_sing_task(int(task.id)) + return success_response({"task_id": int(task.id)}, msg="已触发重新下载") + + # 标记手动重试(避免前端重复点击导致并发过多) + task.payload = {**payload, "manual_retry": True} + task.updated_at = datetime.utcnow() + db.add(task) + db.commit() + + background_tasks.add_task(_retry_finalize_sing_task, int(task.id)) + return success_response({"task_id": int(task.id)}, msg="已触发重新下载") + + +@router.get("/history/all", response_model=ApiResponse[List[dict]]) +def get_sing_history_all( + db: Session = Depends(get_db), + user: AuthedUser = Depends(get_current_user), + page: int = 1, + size: int = 20, +): + lover = db.query(Lover).filter(Lover.user_id == user.id).first() + if not lover: + raise HTTPException(status_code=404, detail="恋人未找到") + + offset = (page - 1) * size + tasks = ( + db.query(GenerationTask) + .filter( + GenerationTask.user_id == user.id, + GenerationTask.lover_id == lover.id, + GenerationTask.task_type == "video", + GenerationTask.payload["song_id"].as_integer().isnot(None), + ) + .order_by(GenerationTask.id.desc()) + .offset(offset) + .limit(size) + .all() + ) + + result: list[dict] = [] + for task in tasks: + payload = task.payload or {} + song_id = payload.get("song_id") + merged_video_url = payload.get("merged_video_url") or task.result_url + result.append( + { + "id": int(task.id), + "song_id": int(song_id) if song_id else 0, + "song_title": payload.get("song_title") or "未知歌曲", + "status": task.status, + "video_url": _cdnize(merged_video_url) or "", + "error_msg": task.error_msg, + "created_at": task.created_at.isoformat() if task.created_at else None, + } + ) + return success_response(result, msg="获取成功") @@ -1669,8 +1928,6 @@ def _process_sing_task(task_id: int): """ 后台处理唱歌视频生成任务:分段音频 -> EMO 逐段生成 -> 拼接整曲。 """ - import logging - logger = logging.getLogger("sing") logger.info(f"开始处理唱歌任务 {task_id}") song_title: str = "" @@ -2247,16 +2504,12 @@ def _process_sing_task(task_id: int): db.add(task_row) db.commit() except HTTPException as exc: - import logging - logger = logging.getLogger("sing") logger.error(f"任务 {task_id} 处理失败 (HTTPException): {exc.detail if hasattr(exc, 'detail') else str(exc)}") try: _mark_task_failed(task_id, str(exc.detail) if hasattr(exc, "detail") else str(exc)) except Exception as e2: logger.exception(f"标记任务 {task_id} 失败时出错: {e2}") except Exception as exc: - import logging - logger = logging.getLogger("sing") logger.exception(f"任务 {task_id} 处理失败 (Exception): {exc}") try: _mark_task_failed(task_id, str(exc)[:255]) @@ -2271,6 +2524,9 @@ def generate_sing_video( db: Session = Depends(get_db), user: AuthedUser = Depends(get_current_user), ): + logger.info(f"🎤 收到唱歌生成请求: user_id={user.id}, song_id={payload.song_id}") + + # 原有代码... lover = db.query(Lover).filter(Lover.user_id == user.id).first() if not lover: raise HTTPException(status_code=404, detail="恋人不存在,请先完成创建流程") @@ -2578,6 +2834,118 @@ def generate_sing_video( ) +def _ensure_sing_history_record(db: Session, task: GenerationTask) -> None: + """确保 nf_sing_song_video 中存在该任务对应的成功记录(用于历史列表)。""" + if not task or task.status != "succeeded": + return + + payload = task.payload or {} + song_id = payload.get("song_id") + merged_video_url = payload.get("merged_video_url") or task.result_url + if not song_id or not merged_video_url: + return + + existing = ( + db.query(SingSongVideo) + .filter( + SingSongVideo.generation_task_id == task.id, + SingSongVideo.user_id == task.user_id, + ) + .first() + ) + if existing and existing.status == "succeeded" and existing.merged_video_url: + return + + audio_url = payload.get("audio_url") or "" + audio_hash = payload.get("audio_hash") or ( _hash_text(audio_url) if audio_url else "" ) + image_hash = payload.get("image_hash") or "" + ratio = payload.get("ratio") or EMO_RATIO + style_level = payload.get("style_level") or EMO_STYLE_LEVEL + + if not existing: + existing = SingSongVideo( + user_id=task.user_id, + lover_id=task.lover_id or 0, + song_id=int(song_id), + audio_url=audio_url or "", + audio_hash=(audio_hash or "")[:64], + image_hash=(image_hash or "")[:64] if image_hash else None, + ratio=ratio, + style_level=style_level, + merged_video_url=merged_video_url, + status="succeeded", + error_msg=None, + generation_task_id=task.id, + created_at=task.created_at or datetime.utcnow(), + updated_at=datetime.utcnow(), + ) + else: + existing.song_id = int(song_id) + existing.merged_video_url = merged_video_url + existing.status = "succeeded" + existing.error_msg = None + existing.updated_at = datetime.utcnow() + existing.generation_task_id = task.id + if task.lover_id: + existing.lover_id = task.lover_id + if audio_url: + existing.audio_url = audio_url + if audio_hash: + existing.audio_hash = (audio_hash or "")[:64] + if image_hash: + existing.image_hash = (image_hash or "")[:64] + existing.ratio = ratio + existing.style_level = style_level + + db.add(existing) + + +def _retry_finalize_sing_task(task_id: int) -> None: + """任务失败但 DashScope 端已成功时,尝试重新下载视频并落库(自愈)。""" + try: + with SessionLocal() as db: + task = ( + db.query(GenerationTask) + .filter(GenerationTask.id == task_id) + .with_for_update() + .first() + ) + if not task: + return + payload = task.payload or {} + dash_id = payload.get("dashscope_task_id") + if not dash_id: + return + + status, dash_video_url, error_msg = _query_dashscope_task_status(dash_id) + if status != "SUCCEEDED" or not dash_video_url: + return + + video_bytes = _download_binary(dash_video_url) + object_name = ( + f"lover/{task.lover_id}/sing/" + f"{int(time.time())}_{payload.get('song_id') or 'unknown'}.mp4" + ) + merged_video_url = _upload_to_oss(video_bytes, object_name) + + task.status = "succeeded" + task.result_url = merged_video_url + task.error_msg = None + task.payload = { + **payload, + "merged_video_url": merged_video_url, + "retry_finalized": True, + "dashscope_error": error_msg, + } + task.updated_at = datetime.utcnow() + db.add(task) + + _ensure_sing_history_record(db, task) + db.commit() + except Exception: + return + + @router.get("/generate/{task_id}", response_model=ApiResponse[SingTaskStatusOut]) def get_sing_task( task_id: int, @@ -2612,11 +2980,7 @@ def get_sing_task( merge_id = payload.get("merge_id") if merge_id: with SessionLocal() as tmp: - merge = ( - tmp.query(SingSongVideo) - .filter(SingSongVideo.id == merge_id) - .first() - ) + merge = tmp.query(SingSongVideo).filter(SingSongVideo.id == merge_id).first() if merge and merge.status == "succeeded" and merge.merged_video_url: current = ( tmp.query(GenerationTask) @@ -2636,7 +3000,6 @@ def get_sing_task( "content_safety_blocked": content_safety_blocked, } current.updated_at = datetime.utcnow() - # 更新聊天占位消息与扣减(若未扣) try: lover_msg_id = (current.payload or {}).get("lover_message_id") session_id = (current.payload or {}).get("session_id") @@ -2688,8 +3051,21 @@ def get_sing_task( tmp.commit() task = current - resp_msg = status_msg_map.get(task.status or "", resp_msg) + # 自愈:成功任务但历史缺记录,补写 nf_sing_song_video + if task.status == "succeeded": + try: + _ensure_sing_history_record(db, task) + db.commit() + except Exception: + db.rollback() + # 自愈:失败任务但 DashScope 可能已成功(下载/上传失败导致),后台重试一次 + if task.status == "failed": + payload = task.payload or {} + if payload.get("dashscope_task_id") and not payload.get("retry_finalized"): + background_tasks.add_task(_retry_finalize_sing_task, int(task.id)) + + resp_msg = status_msg_map.get(task.status or "", resp_msg) payload = task.payload or {} return success_response( SingTaskStatusOut( @@ -2704,3 +3080,4 @@ def get_sing_task( ), msg=resp_msg, ) + diff --git a/lover/routers/user_basic.py b/lover/routers/user_basic.py index e72557a..dc2564d 100644 --- a/lover/routers/user_basic.py +++ b/lover/routers/user_basic.py @@ -18,7 +18,7 @@ def get_user_basic(user: AuthedUser = Depends(get_current_user)): } } -@router.get("/user_basic/get_hobbies") +@router.get("/api/user_basic/get_hobbies") def get_hobbies(user: AuthedUser = Depends(get_current_user)): """获取用户兴趣爱好""" return success_response([ diff --git a/xuniYou/pages/friends/index.vue b/xuniYou/pages/friends/index.vue index e479997..5cfc1e7 100644 --- a/xuniYou/pages/friends/index.vue +++ b/xuniYou/pages/friends/index.vue @@ -115,11 +115,13 @@ export default { }, async friend() { const res = await Friend(this.form) + console.log('Friend 接口返回:', res) let data = '' let ids = '' let onlineData = '' if (res.code == 1) { - data = res.data.data + data = res.data.data || [] + console.log('解析后的 data:', data) ids = data.map(item => { return item.friend_id }); diff --git a/xuniYou/pages/index/index.vue b/xuniYou/pages/index/index.vue index bafb566..8d29ec6 100644 --- a/xuniYou/pages/index/index.vue +++ b/xuniYou/pages/index/index.vue @@ -197,7 +197,10 @@ - 历史视频 + + 历史视频 + 查看全部 + @@ -233,7 +236,10 @@ 让她为你跳一支舞 - 历史视频 + + 历史视频 + 查看全部 + @@ -358,7 +364,7 @@ - {{ historyModalType === 'dance' ? '跳舞历史视频' : '唱歌历史视频' }} + {{ historyModalType === 'dance' ? (historyModalMode === 'all' ? '跳舞全部历史' : '跳舞历史视频') : (historyModalMode === 'all' ? '唱歌全部历史' : '唱歌历史视频') }} 关闭 @@ -366,9 +372,11 @@ {{ historyModalType === 'dance' ? (item.prompt || '跳舞视频') : (item.song_title || '唱歌视频') }} {{ formatDate(item.created_at) }} + {{ formatHistoryStatus(item) }} - 播放 + 播放 + 重新下载 @@ -384,6 +392,20 @@ + + + + 正在生成视频中 + 预计等待15分钟 + + + + + + 正在生成视频中 + 预计等待15分钟 + + @@ -395,7 +417,10 @@ SingSongs, SingGenerate, SingGenerateTask, + SingCurrent, DanceGenerate, + DanceGenerateTask, + DanceCurrent, SessionInit, SessionSend } from '@/utils/api.js' @@ -445,6 +470,9 @@ danceHistoryList: [], songId: 0, singGenerating: false, // 唱歌视频生成中状态 + singGeneratingTaskId: 0, + danceGenerating: false, // 跳舞视频生成中状态 + danceGeneratingTaskId: 0, statusBarHeight: uni.getWindowInfo().statusBarHeight, currentStep: 0, chartData: {}, @@ -481,6 +509,7 @@ underAgeEnabled: false, // 添加青少年模式状态变量 historyModalVisible: false, historyModalType: 'sing', + historyModalMode: 'preview', historyModalList: [], videoPlayerVisible: false, videoPlayerUrl: '', @@ -515,6 +544,12 @@ // 获取歌曲列表 this.getSingSongs(); this.getDanceHistory(); + if (this.currentTab === 2) { + this.restoreSingGeneration(); + } + if (this.currentTab === 3) { + this.restoreDanceGeneration(); + } }, methods: { // Tab 切换方法 @@ -534,6 +569,12 @@ this.currentTab = index; this.updateTabIntoView(index); + if (index === 2) { + this.restoreSingGeneration(); + } + if (index === 3) { + this.restoreDanceGeneration(); + } }, onSwiperChange(e) { console.log('Swiper 滑动,当前索引:', e.detail.current, '对应 Tab:', this.tabs[e.detail.current].name); @@ -544,6 +585,12 @@ if (e.detail.current === 1 && !this.chatSessionId) { this.initChatSession(); } + if (e.detail.current === 2) { + this.restoreSingGeneration(); + } + if (e.detail.current === 3) { + this.restoreDanceGeneration(); + } }, updateTabIntoView(index) { this.$nextTick(() => { @@ -564,44 +611,120 @@ this.songId = song.id; this.singGenerating = true; - uni.showLoading({ - title: '正在生成视频...', - mask: true // 添加遮罩层防止用户操作 - }); + this.singGeneratingTaskId = 0; + uni.setStorageSync('singGeneratingTaskId', 0); + uni.setStorageSync('singGenerating', true); SingGenerate({ song_id: song.id }).then(res => { if (res.code == 1) { + this.singGeneratingTaskId = res.data.generation_task_id; + uni.setStorageSync('singGeneratingTaskId', this.singGeneratingTaskId); this.getSingGenerateTask(res.data.generation_task_id); } else { this.singGenerating = false; - uni.hideLoading(); + this.singGeneratingTaskId = 0; + uni.setStorageSync('singGenerating', false); + uni.setStorageSync('singGeneratingTaskId', 0); uni.showToast({ title: res.msg, icon: 'none' }); } }).catch(err => { this.singGenerating = false; - uni.hideLoading(); + this.singGeneratingTaskId = 0; + uni.setStorageSync('singGenerating', false); + uni.setStorageSync('singGeneratingTaskId', 0); uni.showToast({ title: '请求失败,请重试', icon: 'none' }); }); }, + restoreSingGeneration() { + if (this.currentTab !== 2) { + return; + } + if (!uni.getStorageSync('token')) { + return; + } + if (this.singGenerating) { + return; + } + const storedTaskId = parseInt(uni.getStorageSync('singGeneratingTaskId') || 0); + const storedFlag = !!uni.getStorageSync('singGenerating'); + if (storedFlag && storedTaskId > 0) { + this.singGenerating = true; + this.singGeneratingTaskId = storedTaskId; + this.getSingGenerateTask(storedTaskId); + return; + } + SingCurrent({}).then(res => { + if (res && res.code == 1 && res.data && res.data.generation_task_id) { + this.singGenerating = true; + this.singGeneratingTaskId = res.data.generation_task_id; + uni.setStorageSync('singGenerating', true); + uni.setStorageSync('singGeneratingTaskId', this.singGeneratingTaskId); + this.getSingGenerateTask(this.singGeneratingTaskId); + } + }).catch(() => {}); + }, + restoreDanceGeneration() { + if (this.currentTab !== 3) { + return; + } + if (!uni.getStorageSync('token')) { + return; + } + if (this.danceGenerating) { + return; + } + const storedTaskId = parseInt(uni.getStorageSync('danceGeneratingTaskId') || 0); + const storedFlag = !!uni.getStorageSync('danceGenerating'); + if (storedFlag && storedTaskId > 0) { + this.danceGenerating = true; + this.danceGeneratingTaskId = storedTaskId; + this.getDanceGenerateTask(storedTaskId); + return; + } + DanceCurrent({}).then(res => { + if (res && res.code == 1 && res.data && res.data.generation_task_id) { + this.danceGenerating = true; + this.danceGeneratingTaskId = res.data.generation_task_id; + uni.setStorageSync('danceGenerating', true); + uni.setStorageSync('danceGeneratingTaskId', this.danceGeneratingTaskId); + this.getDanceGenerateTask(this.danceGeneratingTaskId); + } + }).catch(() => {}); + }, // 请求跳舞 requestDance() { if (!this.dancePrompt || !this.dancePrompt.trim()) { - uni.showToast({ title: '请输入舞蹈描述', icon: 'none' }); + this.dancePrompt = '跳一段可爱的舞蹈'; + } + if (this.danceGenerating) { + uni.showToast({ title: '视频生成中,请稍候...', icon: 'none', duration: 2000 }); return; } - uni.showLoading({ title: '生成中...' }); - DanceGenerate({ prompt: this.dancePrompt }).then(res => { + this.danceGenerating = true; + this.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGeneratingTaskId', 0); + uni.setStorageSync('danceGenerating', true); + DanceGenerate({ prompt: this.dancePrompt.trim() }).then(res => { if (res.code == 1) { - uni.hideLoading(); - uni.showToast({ title: '生成成功', icon: 'success' }); - this.getDanceHistory(); + this.danceGeneratingTaskId = res.data.generation_task_id; + uni.setStorageSync('danceGeneratingTaskId', this.danceGeneratingTaskId); + this.getDanceGenerateTask(this.danceGeneratingTaskId); } else { - uni.hideLoading(); + this.danceGenerating = false; + this.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGenerating', false); + uni.setStorageSync('danceGeneratingTaskId', 0); uni.showToast({ title: res.msg, icon: 'none' }); } + }).catch(() => { + this.danceGenerating = false; + this.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGenerating', false); + uni.setStorageSync('danceGeneratingTaskId', 0); + uni.showToast({ title: '请求失败,请重试', icon: 'none' }); }); }, // 打开商城外部链接 @@ -677,11 +800,75 @@ playDanceVideo(videoUrl) { this.openVideoPlayer(videoUrl); }, - openHistoryModal(type) { + openHistoryModal(type, mode = 'preview') { this.historyModalType = type; + this.historyModalMode = mode; + if (mode === 'all') { + this.fetchAllHistory(type); + return; + } this.historyModalList = type === 'dance' ? (this.danceHistoryList || []) : (this.singHistoryList || []); this.historyModalVisible = true; }, + fetchAllHistory(type) { + const endpoint = type === 'dance' ? '/dance/history/all' : '/sing/history/all'; + uni.request({ + url: baseURLPy + endpoint, + method: 'GET', + header: { + 'Authorization': 'Bearer ' + uni.getStorageSync("token") + }, + success: (res) => { + if (res.data && res.data.code === 1 && res.data.data) { + this.historyModalList = res.data.data; + } else { + this.historyModalList = []; + } + this.historyModalVisible = true; + }, + fail: () => { + this.historyModalList = []; + this.historyModalVisible = true; + } + }); + }, + isDownloadFailed(item) { + const msg = (item && item.error_msg ? String(item.error_msg) : '') || ''; + return msg.indexOf('下载失败') !== -1; + }, + formatHistoryStatus(item) { + if (!item) return ''; + const status = item.status || ''; + if (status === 'succeeded') return '成功'; + if (status === 'pending' || status === 'running') return '生成中'; + if (status === 'failed') { + if (this.isDownloadFailed(item)) return '文件下载失败'; + return '生成失败'; + } + return ''; + }, + retryHistoryItem(item) { + if (!item || !item.id) return; + const id = item.id; + const endpoint = this.historyModalType === 'dance' ? ('/dance/retry/' + id) : ('/sing/retry/' + id); + uni.showLoading({ title: '处理中...', mask: true }); + uni.request({ + url: baseURLPy + endpoint, + method: 'POST', + header: { + 'Authorization': 'Bearer ' + uni.getStorageSync("token") + }, + success: () => { + uni.hideLoading(); + uni.showToast({ title: '已触发重新下载', icon: 'none', duration: 1500 }); + this.fetchAllHistory(this.historyModalType); + }, + fail: () => { + uni.hideLoading(); + uni.showToast({ title: '重试失败', icon: 'none', duration: 1500 }); + } + }); + }, closeHistoryModal() { this.historyModalVisible = false; }, @@ -743,13 +930,15 @@ getSingGenerateTask(task_id) { const that = this; let attempts = 0; - const maxAttempts = 60; // 增加到60次,每次4秒,总共4分钟 + const maxAttempts = 225; // 15分钟左右 (225*4s) const doPoll = () => { attempts++; if (attempts > maxAttempts) { that.singGenerating = false; - uni.hideLoading(); + that.singGeneratingTaskId = 0; + uni.setStorageSync('singGenerating', false); + uni.setStorageSync('singGeneratingTaskId', 0); uni.showToast({ title: '处理超时,请稍后查看', icon: 'none', @@ -758,18 +947,14 @@ return; } - // 更新加载提示 - uni.showLoading({ - title: `生成中... (${attempts}/${maxAttempts})`, - mask: true - }); - SingGenerateTask(task_id).then(res => { if (res.code == 1) { const data = res.data; if (data.status == 'succeeded') { that.singGenerating = false; - uni.hideLoading(); + that.singGeneratingTaskId = 0; + uni.setStorageSync('singGenerating', false); + uni.setStorageSync('singGeneratingTaskId', 0); uni.showToast({ title: '生成成功!', icon: 'success', @@ -778,7 +963,9 @@ // 可以在这里刷新聊天消息或显示视频 } else if (data.status == 'failed') { that.singGenerating = false; - uni.hideLoading(); + that.singGeneratingTaskId = 0; + uni.setStorageSync('singGenerating', false); + uni.setStorageSync('singGeneratingTaskId', 0); uni.showToast({ title: data.error_msg || '生成失败', icon: 'none', @@ -786,11 +973,17 @@ }); } else { // 继续轮询 + that.singGenerating = true; + that.singGeneratingTaskId = task_id; + uni.setStorageSync('singGenerating', true); + uni.setStorageSync('singGeneratingTaskId', task_id); setTimeout(doPoll, 4000); } } else { that.singGenerating = false; - uni.hideLoading(); + that.singGeneratingTaskId = 0; + uni.setStorageSync('singGenerating', false); + uni.setStorageSync('singGeneratingTaskId', 0); uni.showToast({ title: res.msg, icon: 'none', @@ -799,7 +992,9 @@ } }).catch(err => { that.singGenerating = false; - uni.hideLoading(); + that.singGeneratingTaskId = 0; + uni.setStorageSync('singGenerating', false); + uni.setStorageSync('singGeneratingTaskId', 0); uni.showToast({ title: '查询失败,请重试', icon: 'none', @@ -810,6 +1005,60 @@ doPoll(); }, + getDanceGenerateTask(task_id) { + const that = this; + let attempts = 0; + const maxAttempts = 225; // 15分钟左右 (225*4s) + const doPoll = () => { + attempts++; + if (attempts > maxAttempts) { + that.danceGenerating = false; + that.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGenerating', false); + uni.setStorageSync('danceGeneratingTaskId', 0); + uni.showToast({ title: '处理超时,请稍后查看', icon: 'none', duration: 2000 }); + return; + } + DanceGenerateTask(task_id).then(res => { + if (res.code == 1) { + const data = res.data; + if (data.status == 'succeeded') { + that.danceGenerating = false; + that.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGenerating', false); + uni.setStorageSync('danceGeneratingTaskId', 0); + uni.showToast({ title: '生成成功!', icon: 'success', duration: 2000 }); + that.getDanceHistory(); + } else if (data.status == 'failed') { + that.danceGenerating = false; + that.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGenerating', false); + uni.setStorageSync('danceGeneratingTaskId', 0); + uni.showToast({ title: data.error_msg || '生成失败', icon: 'none', duration: 2000 }); + } else { + that.danceGenerating = true; + that.danceGeneratingTaskId = task_id; + uni.setStorageSync('danceGenerating', true); + uni.setStorageSync('danceGeneratingTaskId', task_id); + setTimeout(doPoll, 4000); + } + } else { + that.danceGenerating = false; + that.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGenerating', false); + uni.setStorageSync('danceGeneratingTaskId', 0); + uni.showToast({ title: res.msg, icon: 'none', duration: 2000 }); + } + }).catch(() => { + that.danceGenerating = false; + that.danceGeneratingTaskId = 0; + uni.setStorageSync('danceGenerating', false); + uni.setStorageSync('danceGeneratingTaskId', 0); + uni.showToast({ title: '查询失败,请重试', icon: 'none', duration: 2000 }); + }); + }; + doPoll(); + }, // 处理青少年模式状态改变事件 handleUnderAgeStatusChange(status) { this.underAgeEnabled = status; @@ -1269,6 +1518,41 @@ background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%), #FFFFFF; } + .global-block-mask { + position: fixed; + top: calc(80rpx + var(--status-bar-height)); + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.45); + z-index: 900; + display: flex; + align-items: center; + justify-content: center; + } + + .global-block-card { + width: 520rpx; + padding: 36rpx 28rpx; + background: rgba(255, 255, 255, 0.95); + border-radius: 16rpx; + text-align: center; + } + + .global-block-title { + font-size: 34rpx; + font-weight: 600; + color: #111111; + line-height: 48rpx; + } + + .global-block-sub { + margin-top: 14rpx; + font-size: 26rpx; + color: #666666; + line-height: 38rpx; + } + .backA { position: absolute; top: 0; @@ -1696,12 +1980,28 @@ box-sizing: border-box; } + .history-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 20rpx; + padding-left: 10rpx; + } + .section-title { font-size: 32rpx; font-weight: bold; color: #333; - margin-bottom: 20rpx; - padding-left: 10rpx; + margin-bottom: 0; + padding-left: 0; + } + + .section-more { + font-size: 26rpx; + color: #0053FA; + padding: 8rpx 16rpx; + border-radius: 20rpx; + background: rgba(0, 83, 250, 0.08); } .history-list { @@ -1806,6 +2106,7 @@ text-align: center; border-radius: 50rpx; font-size: 30rpx; + box-sizing: border-box; } /* 商城列表样式 */ @@ -2429,20 +2730,31 @@ background: #ffffff; border-radius: 16rpx; overflow: hidden; + box-sizing: border-box; } .modal-header { + position: relative; display: flex; align-items: center; - justify-content: space-between; + justify-content: center; padding: 24rpx; border-bottom: 1px solid rgba(0,0,0,0.06); } .modal-title { + position: absolute; + left: 50%; + transform: translateX(-50%); + max-width: 70%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; font-size: 30rpx; font-weight: 600; color: #333333; } .modal-close { + position: absolute; + right: 24rpx; font-size: 26rpx; color: #666666; padding: 6rpx 12rpx; @@ -2450,11 +2762,13 @@ .modal-list { max-height: 60vh; padding: 12rpx 24rpx 24rpx; + box-sizing: border-box; } .modal-item { display: flex; align-items: center; - justify-content: space-between; + justify-content: flex-start; + gap: 12rpx; padding: 16rpx 0; border-bottom: 1px solid rgba(0,0,0,0.06); } @@ -2462,29 +2776,65 @@ display: flex; flex-direction: column; flex: 1; + min-width: 0; padding-right: 12rpx; } .modal-item-title { font-size: 28rpx; color: #333333; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .modal-retry { + min-width: 112rpx; + max-width: 140rpx; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + font-size: 24rpx; + color: #ffffff; + background: #1DB954; + padding: 10rpx 16rpx; + border-radius: 12rpx; + text-align: center; + box-sizing: border-box; + margin-left: 12rpx; } .modal-item-date { font-size: 22rpx; color: #999999; margin-top: 6rpx; } + .modal-item-status { + font-size: 22rpx; + color: #666666; + margin-top: 6rpx; + line-height: 1.4; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } .modal-item-actions { - width: 120rpx; + flex: 0 0 auto; display: flex; justify-content: flex-end; + margin-left: auto; + padding-left: 12rpx; } .modal-play { + min-width: 112rpx; + max-width: 140rpx; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; font-size: 24rpx; color: #ffffff; background: #F661B5; padding: 10rpx 16rpx; border-radius: 12rpx; text-align: center; + box-sizing: border-box; } .modal-video { width: 100%; diff --git a/xuniYou/utils/Huanxin.js b/xuniYou/utils/Huanxin.js index 1b29f4b..c9715dd 100644 --- a/xuniYou/utils/Huanxin.js +++ b/xuniYou/utils/Huanxin.js @@ -1,5 +1,3 @@ - - import { request } from '@/utils/request.js' @@ -9,18 +7,18 @@ export const getTokenApi = (data) => request({ url: '/api/huanxin/getToken', method: 'post', data: data -}) +}, 2) // 使用 FastAPI // 获取环信在线 export const getOnlineApi = (data) => request({ url: '/api/huanxin/online', method: 'post', data: data -}) +}, 2) // 使用 FastAPI // 发送环信消息 export const sendMessageApi = (data) => request({ url: '/api/huanxin/send', method: 'post', data: data -}) \ No newline at end of file +}, 2) // 使用 FastAPI diff --git a/xuniYou/utils/api.js b/xuniYou/utils/api.js index 51c759c..9c6d0c2 100644 --- a/xuniYou/utils/api.js +++ b/xuniYou/utils/api.js @@ -355,13 +355,19 @@ export const DanceGenerate = (data) => request({ url: '/dance/generate', method: 'post', data: data -},2)//跳舞 +},2,false)//跳舞 export const DanceGenerateTask = (id) => request({ url: `/dance/generate/${id}`, method: 'get', },2,false)//监听生成视频结果 +export const DanceCurrent = (data) => request({ + url: '/dance/current', + method: 'get', + data: data +},2,false)//获取当前进行中的跳舞任务 + export const SingSongs = (data) => request({ url: '/sing/songs', method: 'get', @@ -379,6 +385,12 @@ export const SingGenerateTask = (id) => request({ method: 'get', },2,false)//监听生成视频结果 +export const SingCurrent = (data) => request({ + url: '/sing/current', + method: 'get', + data: data +},2,false)//获取当前进行中的唱歌任务 + export const DynamicShare = (data) => request({ url: '/dynamic/share', method: 'post', diff --git a/xuniYou/utils/request.js b/xuniYou/utils/request.js index e8cec58..1041884 100644 --- a/xuniYou/utils/request.js +++ b/xuniYou/utils/request.js @@ -1,6 +1,6 @@ // Windows 本地开发 - 混合架构 -export const baseURL = 'http://192.168.1.164:30100' // PHP 处理界面和部分 API -export const baseURLPy = 'http://192.168.1.164:30101' // FastAPI 处理核心 API +export const baseURL = 'http://192.168.1.164:30100' // PHP 处理用户管理和界面 +export const baseURLPy = 'http://192.168.1.164:30101' // FastAPI 处理 AI 功能 // 远程服务器 - 需要时取消注释 // export const baseURL = 'http://1.15.149.240:30100' diff --git a/xunifriend_RaeeC/.env b/xunifriend_RaeeC/.env index e509644..b7cd5f1 100644 --- a/xunifriend_RaeeC/.env +++ b/xunifriend_RaeeC/.env @@ -1,5 +1,5 @@ -app_debug = true -app_trace = true +app_debug = false +app_trace = false [database] type = mysql