样式:Tab栏美化并将接口接齐

This commit is contained in:
xiao12feng8 2026-02-03 17:13:56 +08:00
parent d7ec1d530a
commit 21c75461b4
10 changed files with 3600 additions and 227 deletions

View File

@ -21,6 +21,7 @@ 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
@ -82,6 +83,7 @@ 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.exception_handler(HTTPException)

View File

@ -422,6 +422,35 @@ 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(10), nullable=False, default="link") # file, link
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"

View File

@ -18,3 +18,7 @@ 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)

View File

@ -0,0 +1,398 @@
"""
音乐库路由
"""
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
from lover.db import get_db
from lover.deps import get_current_user
from lover.models import User, MusicLibrary, MusicLike
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
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="时长(秒)")
# ========== 辅助函数 ==========
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),
cover_url=_cdnize(music.cover_url) if music.cover_url else "",
duration=music.duration,
upload_type=music.upload_type,
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,
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,
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),
cover_url=_cdnize(music.cover_url) if music.cover_url else "",
duration=music.duration,
upload_type=music.upload_type,
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))

161
start_php_advanced.bat Normal file
View File

@ -0,0 +1,161 @@
@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

View File

@ -1,51 +0,0 @@
#!/usr/bin/env python3
"""测试视频生成次数重置功能"""
from datetime import datetime, date
from lover.db import SessionLocal
from lover.models import User
def test_reset_logic():
db = SessionLocal()
try:
user = db.query(User).filter(User.id == 70).with_for_update().first()
if not user:
print("用户不存在")
return
print(f"重置前:")
print(f" video_gen_remaining: {user.video_gen_remaining}")
print(f" video_gen_reset_date: {user.video_gen_reset_date}")
# 模拟重置逻辑
current_timestamp = int(datetime.utcnow().timestamp())
is_vip = user.vip_endtime and user.vip_endtime > current_timestamp
if is_vip:
last_reset = user.video_gen_reset_date
today = datetime.utcnow().date()
print(f"\n检查:")
print(f" 是否 VIP: {is_vip}")
print(f" 上次重置日期: {last_reset}")
print(f" 今天日期: {today}")
print(f" 需要重置: {not last_reset or last_reset < today}")
if not last_reset or last_reset < today:
user.video_gen_remaining = 2
user.video_gen_reset_date = today
db.add(user)
db.commit()
print(f"\n重置后:")
print(f" video_gen_remaining: {user.video_gen_remaining}")
print(f" video_gen_reset_date: {user.video_gen_reset_date}")
else:
print("\n今天已经重置过了,不需要再次重置")
else:
print("用户不是 VIP不重置")
finally:
db.close()
if __name__ == "__main__":
test_reset_logic()

View File

@ -1,33 +0,0 @@
#!/usr/bin/env python3
"""测试 VIP 功能"""
from datetime import datetime
from lover.db import SessionLocal
from lover.models import User
def test_vip_status():
db = SessionLocal()
try:
# 测试用户 70 和 84
for user_id in [70, 84]:
user = db.query(User).filter(User.id == user_id).first()
if not user:
print(f"用户 {user_id} 不存在")
continue
current_timestamp = int(datetime.utcnow().timestamp())
is_vip = user.vip_endtime and user.vip_endtime > current_timestamp
print(f"\n用户 {user_id} ({user.nickname}):")
print(f" VIP 到期时间戳: {user.vip_endtime}")
if user.vip_endtime:
vip_end_date = datetime.fromtimestamp(user.vip_endtime)
print(f" VIP 到期日期: {vip_end_date}")
print(f" 当前时间戳: {current_timestamp}")
print(f" 是否 VIP: {is_vip}")
print(f" 视频生成次数: {user.video_gen_remaining}")
print(f" 上次重置日期: {user.video_gen_reset_date}")
finally:
db.close()
if __name__ == "__main__":
test_vip_status()

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,2 @@
1. 将所有tab栏的功能接上并且将界面美化
2. 增加音乐库功能

View File

@ -0,0 +1,34 @@
-- 音乐库表
CREATE TABLE IF NOT EXISTS `nf_music_library` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) NOT NULL COMMENT '上传用户ID',
`title` varchar(255) NOT NULL COMMENT '歌曲标题',
`artist` varchar(255) DEFAULT NULL COMMENT '艺术家',
`music_url` varchar(500) NOT NULL COMMENT '音乐文件URL或链接',
`cover_url` varchar(500) DEFAULT NULL COMMENT '封面图URL',
`duration` int(11) DEFAULT NULL COMMENT '时长(秒)',
`upload_type` enum('file','link') NOT NULL DEFAULT 'link' COMMENT '上传类型file=文件上传link=链接',
`is_public` tinyint(1) NOT NULL DEFAULT 1 COMMENT '是否公开1=公开0=私有',
`play_count` int(11) NOT NULL DEFAULT 0 COMMENT '播放次数',
`like_count` int(11) NOT NULL DEFAULT 0 COMMENT '点赞次数',
`status` enum('pending','approved','rejected') NOT NULL DEFAULT 'approved' COMMENT '审核状态',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`deleted_at` datetime DEFAULT NULL COMMENT '删除时间',
PRIMARY KEY (`id`),
KEY `idx_user_id` (`user_id`),
KEY `idx_status` (`status`),
KEY `idx_is_public` (`is_public`),
KEY `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='音乐库表';
-- 用户音乐点赞表
CREATE TABLE IF NOT EXISTS `nf_music_likes` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'ID',
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`music_id` bigint(20) NOT NULL COMMENT '音乐ID',
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_user_music` (`user_id`, `music_id`),
KEY `idx_music_id` (`music_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户音乐点赞表';