Merge pull request #20 from AI-Cultivation/fix-misorder-events
fix: incorrect order in event panel
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 返回倒序(最新在前),反转成时间正序(最旧在前,最新在后)
|
||||
|
||||
@@ -196,6 +196,9 @@ export interface GameEvent {
|
||||
isMajor: boolean;
|
||||
isStory: boolean;
|
||||
|
||||
// 真实创建时间 (用于精确排序)
|
||||
createdAt?: number;
|
||||
|
||||
// 运行时辅助字段
|
||||
_seq?: number;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user