Merge pull request #20 from AI-Cultivation/fix-misorder-events

fix: incorrect order in event panel
This commit is contained in:
4thfever
2026-01-11 19:27:55 +08:00
committed by GitHub
6 changed files with 77 additions and 26 deletions

View File

@@ -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:

View File

@@ -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)

View File

@@ -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

View File

@@ -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 返回倒序(最新在前),反转成时间正序(最旧在前,最新在后)

View File

@@ -196,6 +196,9 @@ export interface GameEvent {
isMajor: boolean;
isStory: boolean;
// 真实创建时间 (用于精确排序)
createdAt?: number;
// 运行时辅助字段
_seq?: number;
}

View File

@@ -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 数据有 createdAtAPI 数据也有。
// 如果没有 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);
}