diff --git a/video-gen-admin/src/utils/formatDate.ts b/video-gen-admin/src/utils/formatDate.ts index e6c4fe48..8fba6595 100644 --- a/video-gen-admin/src/utils/formatDate.ts +++ b/video-gen-admin/src/utils/formatDate.ts @@ -1,11 +1,38 @@ +const CST_OFFSET = 8 * 60; // CST = UTC+8, in minutes + export function formatDate(iso: string | null | undefined): string { if (!iso) return '-'; - let s = iso.trim(); - if (!s.includes('T')) s = s.replace(' ', 'T'); - // Truncate microseconds: 2026-05-13T15:04:04.313751 → 2026-05-13T15:04:04 - const dotIdx = s.indexOf('.'); - if (dotIdx > 0) s = s.slice(0, dotIdx); - // Remove any trailing timezone info (backend now sends naive datetimes) - s = s.replace(/[+-]\d{2}:?\d{0,2}$/, '').replace(/Z$/, ''); - return s.replace('T', ' ').slice(0, 16); + const s = iso.trim(); + if (!s) return '-'; + + // Parse the ISO string, handling timezone offset + // Match: 2026-05-13T15:04:04.313751+00:00 or 2026-05-13T15:04:04Z or 2026-05-13T15:04:04 + const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/); + if (!m) return s.slice(0, 16).replace('T', ' '); + + const [, year, month, day, hour, min, sec, tz] = m; + // Build a Date in UTC + const utcMs = Date.UTC(+year, +month - 1, +day, +hour, +min, +sec); + + if (tz && tz !== 'Z') { + // Has explicit offset like +00:00 or +08:00 — already accounted for in the matched components + // We parsed HH:MM:SS as-is, which are in the given offset. + // Convert to UTC first by subtracting the offset + const sign = tz[0] === '+' ? 1 : -1; + const [oh, om] = tz.slice(1).split(':'); + const offsetMin = sign * (+oh * 60 + +om); + const localMs = utcMs - offsetMin * 60000 + CST_OFFSET * 60000; + const d = new Date(localMs); + return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`; + } + + // No tz or Z: if Z it's UTC, if no tz it's naive (assume CST from backend) + const isUTC = tz === 'Z'; + const localMs = isUTC ? utcMs + CST_OFFSET * 60000 : utcMs; + const d = new Date(localMs); + return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`; +} + +function pad(n: number): string { + return n < 10 ? `0${n}` : String(n); } diff --git a/video-gen-api/app/api/v1/admin.py b/video-gen-api/app/api/v1/admin.py index 604bc6ba..cea64263 100644 --- a/video-gen-api/app/api/v1/admin.py +++ b/video-gen-api/app/api/v1/admin.py @@ -1668,21 +1668,21 @@ async def get_stats( start_date: str = Query(None), end_date: str = Query(None), ): - today_start = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0) - + today_start = datetime.now(CST).replace(hour=0, minute=0, second=0, microsecond=0) + try: if start_date: - date_start = datetime.strptime(start_date, "%Y-%m-%d") + date_start = datetime.strptime(start_date, "%Y-%m-%d").replace(tzinfo=CST) else: date_start = today_start if end_date: - date_end = datetime.strptime(end_date, "%Y-%m-%d") + date_end = datetime.strptime(end_date, "%Y-%m-%d").replace(tzinfo=CST) date_end = date_end.replace(hour=23, minute=59, second=59, microsecond=999999) else: - date_end = datetime.now() + date_end = datetime.now(CST) except: date_start = today_start - date_end = datetime.now() + date_end = datetime.now(CST) total_users = (await db.execute( select(func.count(User.id)).where( @@ -1983,7 +1983,7 @@ async def admin_update_generation_status( if body.get("image_url"): record.image_url = body["image_url"] if new_status == "completed": - record.generated_at = datetime.now() + record.generated_at = datetime.now(CST) await db.flush() await log_operation( db, diff --git a/video-gen-api/app/api/v1/auth.py b/video-gen-api/app/api/v1/auth.py index cbee25c5..d1b5ba16 100644 --- a/video-gen-api/app/api/v1/auth.py +++ b/video-gen-api/app/api/v1/auth.py @@ -1,4 +1,6 @@ -from datetime import datetime +from datetime import datetime, timezone, timedelta + +CST = timezone(timedelta(hours=8)) from fastapi import APIRouter, Depends, HTTPException, status from sqlalchemy import select @@ -117,8 +119,8 @@ async def _handle_daily_login_credits(db: AsyncSession, user: User) -> None: credits = int(credits_result.scalar_one_or_none() or "0") if credits <= 0: return - - today = datetime.now().date() + + today = datetime.now(CST).date() if user.last_login_at: last_login_date = user.last_login_at.date() if last_login_date >= today: @@ -160,7 +162,7 @@ async def login(req: LoginRequest, db: AsyncSession = Depends(get_db)): ) await _handle_daily_login_credits(db, user) - user.last_login_at = datetime.now() + user.last_login_at = datetime.now(CST) await db.flush() return _token_response(user, req.remember_me) @@ -191,7 +193,7 @@ async def sms_login(req: SmsLoginRequest, db: AsyncSession = Depends(get_db)): ) await _handle_daily_login_credits(db, user) - user.last_login_at = datetime.now() + user.last_login_at = datetime.now(CST) await db.flush() return _token_response(user, req.remember_me) @@ -223,7 +225,7 @@ async def register(req: RegisterRequest, db: AsyncSession = Depends(get_db)): username=req.phone, phone=req.phone, hashed_password=hash_password(req.password), - password_set_at=datetime.now(), + password_set_at=datetime.now(CST), credits=register_credits, is_admin=False, user_type="frontend", @@ -295,7 +297,7 @@ async def set_password( ) current_user.hashed_password = hash_password(req.new_password) - current_user.password_set_at = datetime.now() + current_user.password_set_at = datetime.now(CST) await db.flush() return {"message": "密码设置成功", "must_set_password": False} @@ -319,7 +321,7 @@ async def change_password( ) current_user.hashed_password = hash_password(req.new_password) - current_user.password_set_at = datetime.now() + current_user.password_set_at = datetime.now(CST) await db.flush() return {"message": "密码修改成功"} @@ -382,7 +384,7 @@ async def admin_login(req: LoginRequest, db: AsyncSession = Depends(get_db)): detail="该账号不是管理员账号", ) - user.last_login_at = datetime.now() + user.last_login_at = datetime.now(CST) await db.flush() token = create_access_token(user.id, req.remember_me) diff --git a/video-gen-api/app/api/v1/generation.py b/video-gen-api/app/api/v1/generation.py index 92fa81a4..aac12273 100644 --- a/video-gen-api/app/api/v1/generation.py +++ b/video-gen-api/app/api/v1/generation.py @@ -1,7 +1,9 @@ import json import logging import os -from datetime import datetime +from datetime import datetime, timezone, timedelta + +CST = timezone(timedelta(hours=8)) from fastapi import APIRouter, Depends, HTTPException, Query, Request, UploadFile, File, status from fastapi.responses import RedirectResponse @@ -733,7 +735,7 @@ async def seedance_callback(request: Request, db: AsyncSession = Depends(get_db) record.video_url = remote_url else: record.video_url = remote_url - record.generated_at = datetime.now() + record.generated_at = datetime.now(CST) if record.video_url: await record_generation_record_generated_resource( db, diff --git a/video-gen-api/app/api/v1/team.py b/video-gen-api/app/api/v1/team.py index 91345b76..88cde7c2 100644 --- a/video-gen-api/app/api/v1/team.py +++ b/video-gen-api/app/api/v1/team.py @@ -1,6 +1,8 @@ from __future__ import annotations -from datetime import datetime +from datetime import datetime, timezone, timedelta + +CST = timezone(timedelta(hours=8)) from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy import select @@ -405,7 +407,7 @@ async def export_team_credit_records( from urllib.parse import quote output.seek(0) safe_team_name = team.name or "team" - filename = f"团队积分_{safe_team_name}_{datetime.now().strftime('%Y%m%d_%H%M%S')}.csv" + filename = f"团队积分_{safe_team_name}_{datetime.now(CST).strftime('%Y%m%d_%H%M%S')}.csv" encoded_filename = quote(filename) return StreamingResponse( iter([output.getvalue()]), diff --git a/video-gen-app/src/utils/formatDate.ts b/video-gen-app/src/utils/formatDate.ts index c4e25f1c..0a620b89 100644 --- a/video-gen-app/src/utils/formatDate.ts +++ b/video-gen-app/src/utils/formatDate.ts @@ -1,12 +1,32 @@ +const CST_OFFSET = 8 * 60; // CST = UTC+8, in minutes + export function formatDate(iso: string | null | undefined): string { if (!iso) return '-'; - let s = iso.trim(); - // Normalize: space → T - if (!s.includes('T')) s = s.replace(' ', 'T'); - // Truncate microseconds: 2026-05-13T15:04:04.313751 → 2026-05-13T15:04:04 - const dotIdx = s.indexOf('.'); - if (dotIdx > 0) s = s.slice(0, dotIdx); - // Remove any trailing timezone info (backend now sends naive datetimes) - s = s.replace(/[+-]\d{2}:?\d{0,2}$/, '').replace(/Z$/, ''); - return s.replace('T', ' ').slice(0, 16); + const s = iso.trim(); + if (!s) return '-'; + + // Parse the ISO string, handling timezone offset + const m = s.match(/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/); + if (!m) return s.slice(0, 16).replace('T', ' '); + + const [, year, month, day, hour, min, sec, tz] = m; + const utcMs = Date.UTC(+year, +month - 1, +day, +hour, +min, +sec); + + if (tz && tz !== 'Z') { + const sign = tz[0] === '+' ? 1 : -1; + const [oh, om] = tz.slice(1).split(':'); + const offsetMin = sign * (+oh * 60 + +om); + const localMs = utcMs - offsetMin * 60000 + CST_OFFSET * 60000; + const d = new Date(localMs); + return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`; + } + + const isUTC = tz === 'Z'; + const localMs = isUTC ? utcMs + CST_OFFSET * 60000 : utcMs; + const d = new Date(localMs); + return `${d.getUTCFullYear()}-${pad(d.getUTCMonth() + 1)}-${pad(d.getUTCDate())} ${pad(d.getUTCHours())}:${pad(d.getUTCMinutes())}`; +} + +function pad(n: number): string { + return n < 10 ? `0${n}` : String(n); }