Round6: 前台图片缩略图+下载/视频修复; 后台连接测试单次+自动消失; 导航重组; 存储中文化; 自动化去掉创建按钮; 短信验证码(注册/重置/充值通知)

This commit is contained in:
QihuAI Design committed 2026-07-14 18:49:27 +08:00
1 parent ba09017884
commit d179cc2f02
14 files changed
+806 -149

No files matched your search

+113 -2
View File
@@ -4,15 +4,18 @@ from sqlalchemy import and_
from pydantic import BaseModel, Field
from typing import Optional
import uuid
from datetime import datetime
import random
import re
from datetime import datetime, timedelta
from ..database import get_db
from ..models import (
User, LoginLog, RegisterSetting,
User, LoginLog, RegisterSetting, SmsConfig, SmsVerificationCode,
gen_uuid
)
from ..auth import get_current_user, hash_password, verify_password, create_access_token
from ..middleware.log_service import LogService
from ..sms_client import send_sms_code
router = APIRouter()
@@ -27,6 +30,18 @@ class RegisterRequest(BaseModel):
password: str = Field(min_length=6, max_length=100)
email: Optional[str] = None
phone: Optional[str] = None
sms_code: Optional[str] = None
class SendCodeRequest(BaseModel):
phone: str
purpose: str = "register" # register / reset_password
class ResetPasswordRequest(BaseModel):
phone: str
code: str
new_password: str = Field(min_length=6, max_length=100)
@router.post("/login")
@@ -117,6 +132,20 @@ def register(req: RegisterRequest, db: Session = Depends(get_db)):
if existing_phone:
raise HTTPException(status_code=400, detail="手机号已被注册")
# 短信验证码校验(手机号注册且开启短信验证)
if req.phone and reg_setting and reg_setting.phone_sms_required:
if not req.sms_code:
raise HTTPException(status_code=400, detail="请先获取并填写短信验证码")
vrec = db.query(SmsVerificationCode).filter(
SmsVerificationCode.phone == req.phone,
SmsVerificationCode.purpose == "register",
SmsVerificationCode.used == False,
SmsVerificationCode.expires_at > datetime.now(),
).order_by(SmsVerificationCode.created_at.desc()).first()
if not vrec or vrec.code != req.sms_code:
raise HTTPException(status_code=400, detail="短信验证码错误或已过期")
vrec.used = True
user = User(
id=gen_uuid(),
username=req.username,
@@ -179,6 +208,88 @@ def register(req: RegisterRequest, db: Session = Depends(get_db)):
}
@router.post("/send-code")
def send_code(req: SendCodeRequest, db: Session = Depends(get_db)):
"""发送短信验证码(注册 / 重置密码)。公开接口。"""
if not re.match(r'^1[3-9]\d{9}$', req.phone or ''):
raise HTTPException(status_code=400, detail="手机号格式不正确")
purpose = req.purpose or "register"
if purpose not in ("register", "reset_password"):
raise HTTPException(status_code=400, detail="不支持的验证码类型")
config = db.query(SmsConfig).first()
if not config or not config.provider:
raise HTTPException(status_code=400, detail="短信服务未配置,请联系管理员")
# 注册场景:手机号已注册则直接提示,避免浪费短信
if purpose == "register":
existing = db.query(User).filter(User.phone == req.phone).first()
if existing:
raise HTTPException(status_code=400, detail="该手机号已注册,请直接登录")
# 频率限制:同一手机号+用途 60 秒内不重复下发
recent = db.query(SmsVerificationCode).filter(
SmsVerificationCode.phone == req.phone,
SmsVerificationCode.purpose == purpose,
SmsVerificationCode.used == False,
SmsVerificationCode.expires_at > datetime.now(),
).order_by(SmsVerificationCode.created_at.desc()).first()
if recent:
age = (datetime.now() - recent.created_at).total_seconds()
if age < 60:
raise HTTPException(status_code=429, detail=f"验证码发送过于频繁,请{int(60 - age)}秒后再试")
code = f"{random.randint(100000, 999999)}"
rec = SmsVerificationCode(
id=gen_uuid(),
phone=req.phone,
code=code,
purpose=purpose,
expires_at=datetime.now() + timedelta(minutes=5),
used=False,
)
db.add(rec)
db.commit()
ok, msg, _ = send_sms_code(config, req.phone, code, purpose)
if not ok:
raise HTTPException(status_code=400, detail=f"短信发送失败:{msg}")
return {"code": 0, "data": {"sent": True}, "message": "验证码已发送"}
@router.post("/reset-password")
def reset_password(req: ResetPasswordRequest, db: Session = Depends(get_db)):
"""通过短信验证码重置密码(公开接口)。"""
user = db.query(User).filter(User.phone == req.phone).first()
if not user:
raise HTTPException(status_code=400, detail="该手机号未注册")
vrec = db.query(SmsVerificationCode).filter(
SmsVerificationCode.phone == req.phone,
SmsVerificationCode.purpose == "reset_password",
SmsVerificationCode.used == False,
SmsVerificationCode.expires_at > datetime.now(),
).order_by(SmsVerificationCode.created_at.desc()).first()
if not vrec or vrec.code != req.code:
raise HTTPException(status_code=400, detail="验证码错误或已过期")
user.password_hash = hash_password(req.new_password)
user.updated_at = datetime.now()
vrec.used = True
LogService.log_operation(
user_id=user.id,
username=user.username,
action="reset_password",
target="user",
detail=f"用户 {user.username} 通过短信验证码重置了密码",
)
db.commit()
return {"code": 0, "message": "密码重置成功"}
@router.get("/me")
def get_me(current_user: User = Depends(get_current_user)):
return {
+5 -4
View File
@@ -157,21 +157,22 @@ async def _generate_media(db, ai_model, prompt: str, user_id: str = None) -> str
video_id = data.get("video_id") or data.get("task_id") or data.get("id")
if not video_id:
return f"⚠️ 视频生成失败:未返回任务ID {str(data)[:200]}"
# 轮询结果(最多约 5 分钟)
# 轮询结果(最多约 5 分钟)。AGNES 视频状态端点为 GET {base}/v1/videos/{video_id}
# 该端点顶层返回 status(queued/in_progress/completed/failed),成片地址在 metadata.url
result_url = None
err_msg = None
for _ in range(60):
await asyncio.sleep(5)
r2 = await client.get(f"{base}/agnesapi?video_id={video_id}", headers=headers)
r2 = await client.get(f"{base}/v1/videos/{video_id}", headers=headers)
if r2.status_code != 200:
continue
rd = r2.json()
st = rd.get("status")
if st == "completed":
result_url = rd.get("url")
result_url = (rd.get("metadata") or {}).get("url") or rd.get("url")
break
if st == "failed":
err_msg = rd.get("error")
err_msg = rd.get("error") or (rd.get("metadata") or {}).get("error")
break
if result_url:
return (f'<video class="md-video" controls src="{result_url}"></video>\n\n'
+11 -1
View File
@@ -9,10 +9,11 @@ from datetime import datetime, date
from ..database import get_db
from ..models import (
User, LoginLog, ApiKey, MembershipSubscription, AppPurchase, MarketApp,
gen_uuid
SmsConfig, gen_uuid
)
from ..auth import get_current_user, hash_password, verify_password
from ..middleware.log_service import LogService
from ..sms_client import send_sms_text
router = APIRouter()
@@ -145,6 +146,15 @@ def update_password(
db_user.password_hash = hash_password(req.new_password)
db_user.updated_at = datetime.now()
# 密码修改成功后发送短信通知(若已配置短信且用户绑定手机号)
try:
sms_cfg = db.query(SmsConfig).first()
if sms_cfg and sms_cfg.provider and db_user.phone:
send_sms_text(sms_cfg, db_user.phone, "您的登录密码已修改,如非本人操作请尽快联系客服。")
except Exception as sms_err:
import logging
logging.getLogger("aiflow").warning(f"密码修改短信通知失败(不影响修改结果): {sms_err}")
LogService.log_operation(
user_id=db_user.id, username=db_user.username, action="change_password",
target="user", target_id=db_user.id,
+14 -1
View File
@@ -8,11 +8,12 @@ from datetime import datetime, date, timedelta
from ..database import get_db
from ..models import (
User, TokenUsageLog, TokenBalance, AIModel,
User, TokenUsageLog, TokenBalance, AIModel, SmsConfig,
gen_uuid
)
from ..auth import get_current_user
from ..middleware.log_service import LogService
from ..sms_client import send_sms_text
router = APIRouter()
@@ -237,6 +238,18 @@ def recharge(
db.commit()
# 充值成功后发送短信通知(若已配置短信且用户绑定手机号)
try:
sms_cfg = db.query(SmsConfig).first()
if sms_cfg and sms_cfg.provider and current_user.phone:
send_sms_text(
sms_cfg, current_user.phone,
f"您已成功充值 {req.amount} tokens,当前余额 {balance.balance} tokens。",
)
except Exception as sms_err:
import logging
logging.getLogger("aiflow").warning(f"充值短信通知失败(不影响充值结果): {sms_err}")
return {
"code": 0,
"data": {