fix: add UTC timezone indicator to all datetime serialization

Database stores times in UTC but serialized without timezone info,
causing frontend to misinterpret as local time. Now all datetime
fields include 'Z' suffix to indicate UTC, enabling proper timezone
conversion in the browser.

- Add UTCDatetimeBaseModel base class for Pydantic schemas
- Update model to_dict() methods to append 'Z' suffix
- Affects: tasks, users, sessions, audit logs, translations

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
egg
2025-12-14 15:48:17 +08:00
parent 7233e9cb7b
commit ee49751c38
9 changed files with 88 additions and 37 deletions

View File

@@ -77,7 +77,10 @@ class AuditLog(Base):
return f"<AuditLog(id={self.id}, type='{self.event_type}', user_id={self.user_id})>"
def to_dict(self):
"""Convert audit log to dictionary"""
"""Convert audit log to dictionary.
All datetime fields are serialized with 'Z' suffix to indicate UTC timezone.
"""
return {
"id": self.id,
"user_id": self.user_id,
@@ -91,5 +94,5 @@ class AuditLog(Base):
"success": bool(self.success),
"error_message": self.error_message,
"extra_data": self.extra_data,
"created_at": self.created_at.isoformat() if self.created_at else None
"created_at": self.created_at.isoformat() + 'Z' if self.created_at else None
}

View File

@@ -56,16 +56,19 @@ class Session(Base):
return f"<Session(id={self.id}, user_id={self.user_id}, expires_at='{self.expires_at}')>"
def to_dict(self):
"""Convert session to dictionary (excluding sensitive tokens)"""
"""Convert session to dictionary (excluding sensitive tokens).
All datetime fields are serialized with 'Z' suffix to indicate UTC timezone.
"""
return {
"id": self.id,
"user_id": self.user_id,
"token_type": self.token_type,
"expires_at": self.expires_at.isoformat() if self.expires_at else None,
"issued_at": self.issued_at.isoformat() if self.issued_at else None,
"expires_at": self.expires_at.isoformat() + 'Z' if self.expires_at else None,
"issued_at": self.issued_at.isoformat() + 'Z' if self.issued_at else None,
"ip_address": self.ip_address,
"created_at": self.created_at.isoformat() if self.created_at else None,
"last_accessed_at": self.last_accessed_at.isoformat() if self.last_accessed_at else None
"created_at": self.created_at.isoformat() + 'Z' if self.created_at else None,
"last_accessed_at": self.last_accessed_at.isoformat() + 'Z' if self.last_accessed_at else None
}
@property

View File

@@ -66,7 +66,11 @@ class Task(Base):
return f"<Task(id={self.id}, task_id='{self.task_id}', status='{self.status.value}')>"
def to_dict(self):
"""Convert task to dictionary"""
"""Convert task to dictionary.
All datetime fields are serialized with 'Z' suffix to indicate UTC timezone.
This ensures proper timezone conversion in the frontend.
"""
return {
"id": self.id,
"task_id": self.task_id,
@@ -78,11 +82,11 @@ class Task(Base):
"result_pdf_path": self.result_pdf_path,
"error_message": self.error_message,
"processing_time_ms": self.processing_time_ms,
"created_at": self.created_at.isoformat() if self.created_at else None,
"updated_at": self.updated_at.isoformat() if self.updated_at else None,
"completed_at": self.completed_at.isoformat() if self.completed_at else None,
"created_at": self.created_at.isoformat() + 'Z' if self.created_at else None,
"updated_at": self.updated_at.isoformat() + 'Z' if self.updated_at else None,
"completed_at": self.completed_at.isoformat() + 'Z' if self.completed_at else None,
"file_deleted": self.file_deleted,
"deleted_at": self.deleted_at.isoformat() if self.deleted_at else None
"deleted_at": self.deleted_at.isoformat() + 'Z' if self.deleted_at else None
}
@@ -116,7 +120,10 @@ class TaskFile(Base):
return f"<TaskFile(id={self.id}, task_id={self.task_id}, original_name='{self.original_name}')>"
def to_dict(self):
"""Convert task file to dictionary"""
"""Convert task file to dictionary.
All datetime fields are serialized with 'Z' suffix to indicate UTC timezone.
"""
return {
"id": self.id,
"task_id": self.task_id,
@@ -125,5 +132,5 @@ class TaskFile(Base):
"file_size": self.file_size,
"mime_type": self.mime_type,
"file_hash": self.file_hash,
"created_at": self.created_at.isoformat() if self.created_at else None
"created_at": self.created_at.isoformat() + 'Z' if self.created_at else None
}

View File

@@ -67,7 +67,10 @@ class TranslationLog(Base):
return f"<TranslationLog(id={self.id}, task_id='{self.task_id}', target_lang='{self.target_lang}', tokens={self.total_tokens})>"
def to_dict(self):
"""Convert translation log to dictionary"""
"""Convert translation log to dictionary.
All datetime fields are serialized with 'Z' suffix to indicate UTC timezone.
"""
return {
"id": self.id,
"user_id": self.user_id,
@@ -83,5 +86,5 @@ class TranslationLog(Base):
"estimated_cost": self.estimated_cost,
"processing_time_seconds": self.processing_time_seconds,
"provider": self.provider,
"created_at": self.created_at.isoformat() if self.created_at else None
"created_at": self.created_at.isoformat() + 'Z' if self.created_at else None
}

View File

@@ -39,12 +39,15 @@ class User(Base):
return f"<User(id={self.id}, email='{self.email}', display_name='{self.display_name}')>"
def to_dict(self):
"""Convert user to dictionary"""
"""Convert user to dictionary.
All datetime fields are serialized with 'Z' suffix to indicate UTC timezone.
"""
return {
"id": self.id,
"email": self.email,
"display_name": self.display_name,
"created_at": self.created_at.isoformat() if self.created_at else None,
"last_login": self.last_login.isoformat() if self.last_login else None,
"created_at": self.created_at.isoformat() + 'Z' if self.created_at else None,
"last_login": self.last_login.isoformat() + 'Z' if self.last_login else None,
"is_active": self.is_active
}