diff --git a/src/classes/event.py b/src/classes/event.py index 7f95ace..8a6deab 100644 --- a/src/classes/event.py +++ b/src/classes/event.py @@ -4,6 +4,8 @@ event class from dataclasses import dataclass, field from typing import List, Optional import uuid +import time +from datetime import datetime from src.classes.calendar import Month, Year, MonthStamp @@ -19,6 +21,8 @@ class Event: is_story: bool = False # 唯一ID,用于去重 id: str = field(default_factory=lambda: str(uuid.uuid4())) + # 创建时间戳 (Unix timestamp float) + created_at: float = field(default_factory=time.time) def __str__(self) -> str: year = self.month_stamp.get_year() @@ -33,7 +37,8 @@ class Event: "related_avatars": self.related_avatars, "is_major": self.is_major, "is_story": self.is_story, - "id": self.id + "id": self.id, + "created_at": self.created_at } @classmethod @@ -45,7 +50,8 @@ class Event: related_avatars=data.get("related_avatars"), is_major=data.get("is_major", False), is_story=data.get("is_story", False), - id=data.get("id", str(uuid.uuid4())) + id=data.get("id", str(uuid.uuid4())), + created_at=data.get("created_at", time.time()) ) class NullEvent: diff --git a/src/classes/event_storage.py b/src/classes/event_storage.py index 6af8cb3..3fc5ab4 100644 --- a/src/classes/event_storage.py +++ b/src/classes/event_storage.py @@ -9,12 +9,32 @@ import sqlite3 from pathlib import Path from typing import TYPE_CHECKING, Optional from contextlib import contextmanager +from datetime import datetime, timezone from src.run.log import get_logger if TYPE_CHECKING: from src.classes.event import Event +def _format_time(ts: float) -> str: + """将 timestamp float 转换为 SQLite 兼容的 UTC 字符串""" + return datetime.fromtimestamp(ts, timezone.utc).strftime('%Y-%m-%d %H:%M:%S.%f') + +def _parse_time(ts_str: str) -> float: + """将 SQLite 时间字符串解析为 timestamp float""" + if not ts_str: + return 0.0 + try: + # 尝试带微秒的格式 + dt = datetime.strptime(ts_str, '%Y-%m-%d %H:%M:%S.%f') + except ValueError: + try: + # 尝试不带微秒的格式 + dt = datetime.strptime(ts_str, '%Y-%m-%d %H:%M:%S') + except ValueError: + return 0.0 + # 假设数据库存的是 UTC (naive time string from sqlite usually treated as such) + return dt.replace(tzinfo=timezone.utc).timestamp() class EventStorage: """ @@ -115,8 +135,8 @@ class EventStorage: # 插入事件主表。 self._conn.execute( """ - INSERT OR IGNORE INTO events (id, month_stamp, content, is_major, is_story) - VALUES (?, ?, ?, ?, ?) + INSERT OR IGNORE INTO events (id, month_stamp, content, is_major, is_story, created_at) + VALUES (?, ?, ?, ?, ?, ?) """, ( event.id, @@ -124,6 +144,7 @@ class EventStorage: event.content, event.is_major, event.is_story, + _format_time(event.created_at), ) ) @@ -193,7 +214,7 @@ class EventStorage: # Pair 查询:两个角色都相关的事件。 id1, id2 = avatar_id_pair base_query = """ - SELECT DISTINCT e.rowid, e.id, e.month_stamp, e.content, e.is_major, e.is_story + SELECT DISTINCT e.rowid, e.id, e.month_stamp, e.content, e.is_major, e.is_story, e.created_at FROM events e JOIN event_avatars ea1 ON e.id = ea1.event_id AND ea1.avatar_id = ? JOIN event_avatars ea2 ON e.id = ea2.event_id AND ea2.avatar_id = ? @@ -202,7 +223,7 @@ class EventStorage: elif avatar_id: # 单角色查询。 base_query = """ - SELECT DISTINCT e.rowid, e.id, e.month_stamp, e.content, e.is_major, e.is_story + SELECT DISTINCT e.rowid, e.id, e.month_stamp, e.content, e.is_major, e.is_story, e.created_at FROM events e JOIN event_avatars ea ON e.id = ea.event_id AND ea.avatar_id = ? """ @@ -210,7 +231,7 @@ class EventStorage: else: # 全部事件。 base_query = """ - SELECT rowid, id, month_stamp, content, is_major, is_story + SELECT rowid, id, month_stamp, content, is_major, is_story, e.created_at FROM events e """ @@ -259,6 +280,7 @@ class EventStorage: is_major=bool(row["is_major"]), is_story=bool(row["is_story"]), id=row["id"], + created_at=_parse_time(row["created_at"]), ) events.append(event) last_rowid = row["rowid"] @@ -304,7 +326,7 @@ class EventStorage: try: rows = self._conn.execute( """ - SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story + SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story, e.created_at FROM events e JOIN event_avatars ea ON e.id = ea.event_id AND ea.avatar_id = ? WHERE e.is_major = TRUE AND e.is_story = FALSE @@ -329,6 +351,7 @@ class EventStorage: is_major=bool(row["is_major"]), is_story=bool(row["is_story"]), id=row["id"], + created_at=_parse_time(row["created_at"]), ) events.append(event) @@ -348,7 +371,7 @@ class EventStorage: try: rows = self._conn.execute( """ - SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story + SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story, e.created_at FROM events e JOIN event_avatars ea ON e.id = ea.event_id AND ea.avatar_id = ? WHERE e.is_major = FALSE OR e.is_story = TRUE @@ -373,6 +396,7 @@ class EventStorage: is_major=bool(row["is_major"]), is_story=bool(row["is_story"]), id=row["id"], + created_at=_parse_time(row["created_at"]), ) events.append(event) @@ -392,7 +416,7 @@ class EventStorage: try: rows = self._conn.execute( """ - SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story + SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story, e.created_at FROM events e JOIN event_avatars ea1 ON e.id = ea1.event_id AND ea1.avatar_id = ? JOIN event_avatars ea2 ON e.id = ea2.event_id AND ea2.avatar_id = ? @@ -418,6 +442,7 @@ class EventStorage: is_major=bool(row["is_major"]), is_story=bool(row["is_story"]), id=row["id"], + created_at=_parse_time(row["created_at"]), ) events.append(event) @@ -437,7 +462,7 @@ class EventStorage: try: rows = self._conn.execute( """ - SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story + SELECT DISTINCT e.id, e.month_stamp, e.content, e.is_major, e.is_story, e.created_at FROM events e JOIN event_avatars ea1 ON e.id = ea1.event_id AND ea1.avatar_id = ? JOIN event_avatars ea2 ON e.id = ea2.event_id AND ea2.avatar_id = ? @@ -463,6 +488,7 @@ class EventStorage: is_major=bool(row["is_major"]), is_story=bool(row["is_story"]), id=row["id"], + created_at=_parse_time(row["created_at"]), ) events.append(event) diff --git a/src/server/main.py b/src/server/main.py index 3d5474c..b74e50c 100644 --- a/src/server/main.py +++ b/src/server/main.py @@ -202,6 +202,7 @@ def serialize_events_for_client(events: List[Event]) -> List[dict]: "related_avatar_ids": related_ids, "is_major": bool(getattr(event, "is_major", False)), "is_story": bool(getattr(event, "is_story", False)), + "created_at": getattr(event, "created_at", 0.0), }) return serialized diff --git a/web/src/stores/world.ts b/web/src/stores/world.ts index 9b41980..0d6c33a 100644 --- a/web/src/stores/world.ts +++ b/web/src/stores/world.ts @@ -4,7 +4,7 @@ import type { AvatarSummary, GameEvent, MapMatrix, RegionSummary, CelestialPheno import type { TickPayloadDTO, InitialStateDTO } from '../types/api'; import type { FetchEventsParams } from '../api/game'; import { gameApi } from '../api/game'; -import { processNewEvents } from '../utils/eventHelper'; +import { processNewEvents, mergeAndSortEvents } from '../utils/eventHelper'; export const useWorldStore = defineStore('world', () => { // --- State --- @@ -91,13 +91,8 @@ export const useWorldStore = defineStore('world', () => { if (newEvents.length === 0) return; - // WebSocket 推送的新事件直接追加到末尾(最新事件在底部) - // 使用 Set 去重(基于 id) - const existingIds = new Set(events.value.map(e => e.id)); - const uniqueNewEvents = newEvents.filter(e => !existingIds.has(e.id)); - if (uniqueNewEvents.length > 0) { - events.value = [...events.value, ...uniqueNewEvents]; - } + // 使用通用合并排序函数,确保顺序正确(基于 createdAt 或时间戳) + events.value = mergeAndSortEvents(events.value, newEvents); } function handleTick(payload: TickPayloadDTO) { @@ -245,6 +240,7 @@ export const useWorldStore = defineStore('world', () => { relatedAvatarIds: e.related_avatar_ids, isMajor: e.is_major, isStory: e.is_story, + createdAt: e.created_at, })); // API 返回倒序(最新在前),反转成时间正序(最旧在前,最新在后) diff --git a/web/src/types/core.ts b/web/src/types/core.ts index 0ad29b7..fb96cff 100644 --- a/web/src/types/core.ts +++ b/web/src/types/core.ts @@ -196,6 +196,9 @@ export interface GameEvent { isMajor: boolean; isStory: boolean; + // 真实创建时间 (用于精确排序) + createdAt?: number; + // 运行时辅助字段 _seq?: number; } diff --git a/web/src/utils/eventHelper.ts b/web/src/utils/eventHelper.ts index ba169da..f47c077 100644 --- a/web/src/utils/eventHelper.ts +++ b/web/src/utils/eventHelper.ts @@ -18,20 +18,37 @@ export function processNewEvents(rawEvents: any[], currentYear: number, currentM relatedAvatarIds: e.related_avatar_ids || [], isMajor: e.is_major, isStory: e.is_story, + createdAt: e.created_at, _seq: index })); } /** * 合并并排序事件列表 - * 1. 按时间戳升序 - * 2. 时间戳相同时,按序列号升序 - * 3. 保留最新的 MAX_EVENTS 条 + * 1. 优先使用 createdAt (精确时间戳) 升序 + * 2. 其次按月时间戳升序 + * 3. 最后按序列号升序 + * 4. 保留最新的 MAX_EVENTS 条 */ export function mergeAndSortEvents(existingEvents: GameEvent[], newEvents: GameEvent[]): GameEvent[] { - const combined = [...newEvents, ...existingEvents]; + // 合并 + const combined = [...existingEvents]; // Copy existing + // Add new ones only if not exists (by id) + const existingIds = new Set(existingEvents.map(e => e.id)); + for (const ev of newEvents) { + if (!existingIds.has(ev.id)) { + combined.push(ev); + } + } combined.sort((a, b) => { + // 0. 如果都有 createdAt,优先比较 + // 注意:SQLite 可能没有返回历史数据的 createdAt,或者为 0 + if (a.createdAt && b.createdAt && a.createdAt > 0 && b.createdAt > 0) { + // float comparison + return a.createdAt - b.createdAt; + } + // 1. 先按时间戳升序(最旧的月在上面) const ta = a.timestamp; const tb = b.timestamp; @@ -40,17 +57,19 @@ export function mergeAndSortEvents(existingEvents: GameEvent[], newEvents: GameE } // 2. 时间相同时,按原始逻辑顺序升序(先发生的在上面) - // 旧事件通常没有 _seq (undefined),视为最旧 (-1) + // 如果其中一个有 createdAt 而另一个没有(不太可能,除非混合了旧数据) + // 假设 tick 数据有 createdAt,API 数据也有。 + + // 如果没有 createdAt,回退到 _seq const seqA = a._seq ?? -1; const seqB = b._seq ?? -1; - // 如果都是旧事件,保持相对顺序 (Stable) if (seqA === -1 && seqB === -1) return 0; return seqA - seqB; }); - // 保留最新的 N 条 (因为是升序,最新的在最后,所以取最后 N 条) + // 保留最新的 N 条 if (combined.length > MAX_EVENTS) { return combined.slice(-MAX_EVENTS); }