forked from kevin/meshtastic_mqtt_server
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d33ec0075d | ||
|
|
5d174caabb | ||
|
|
6fddcd11b5 |
+31
-1
@@ -435,7 +435,7 @@ func (MapReportRecord) TableName() string {
|
|||||||
|
|
||||||
type TextMessageRecord struct {
|
type TextMessageRecord struct {
|
||||||
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
|
ID uint64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||||
FromID string `gorm:"column:from_id;not null"`
|
FromID string `gorm:"column:from_id;not null;type:varchar(191);index:idx_text_message_from_id,priority:1"`
|
||||||
FromNum int64 `gorm:"column:from_num;not null;index:idx_text_message_from_num_created_at,priority:1"`
|
FromNum int64 `gorm:"column:from_num;not null;index:idx_text_message_from_num_created_at,priority:1"`
|
||||||
Text *string `gorm:"column:text"`
|
Text *string `gorm:"column:text"`
|
||||||
PayloadHex *string `gorm:"column:payload_hex"`
|
PayloadHex *string `gorm:"column:payload_hex"`
|
||||||
@@ -742,6 +742,9 @@ func (s *Store) migrate() error {
|
|||||||
if err := migrateBotNodePSK(tx, migrator, s.driver); err != nil {
|
if err := migrateBotNodePSK(tx, migrator, s.driver); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := migrateTextMessageFromIDIndex(tx, migrator, s.driver); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := migrateBotDirectMessages(tx, migrator); err != nil {
|
if err := migrateBotDirectMessages(tx, migrator); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -768,6 +771,33 @@ func (s *Store) migrate() error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// migrateTextMessageFromIDIndex 为 text_message.from_id 建立索引。
|
||||||
|
// 该列历史版本为 longtext(无法直接建索引),节点详情页/聊天按 from_id 过滤时
|
||||||
|
// 会全表扫描(百万级行,耗时数秒)。与 channel_id 的 DBv1 处理一致:
|
||||||
|
// MySQL 先把列改为 VARCHAR(191) 再建完整索引;SQLite 直接建索引即可。
|
||||||
|
// 幂等:索引已存在时跳过(老库可能已有手工建的前缀索引)。
|
||||||
|
func migrateTextMessageFromIDIndex(tx *gorm.DB, migrator gorm.Migrator, driver string) error {
|
||||||
|
if !migrator.HasTable(&TextMessageRecord{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if migrator.HasIndex(&TextMessageRecord{}, "idx_text_message_from_id") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if driver == config.DriverSQLite {
|
||||||
|
if err := tx.Exec("CREATE INDEX idx_text_message_from_id ON text_message(from_id)").Error; err != nil {
|
||||||
|
return fmt.Errorf("create text_message.from_id index: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := tx.Exec("ALTER TABLE text_message MODIFY COLUMN from_id VARCHAR(191) NOT NULL").Error; err != nil {
|
||||||
|
return fmt.Errorf("alter text_message.from_id to varchar: %w", err)
|
||||||
|
}
|
||||||
|
if err := tx.Exec("ALTER TABLE text_message ADD KEY idx_text_message_from_id (from_id)").Error; err != nil {
|
||||||
|
return fmt.Errorf("add text_message.from_id index: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func migrateBotNodePSK(tx *gorm.DB, migrator gorm.Migrator, driver string) error {
|
func migrateBotNodePSK(tx *gorm.DB, migrator gorm.Migrator, driver string) error {
|
||||||
if !migrator.HasTable(&BotNodeRecord{}) {
|
if !migrator.HasTable(&BotNodeRecord{}) {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -13,11 +13,29 @@ const total = ref(0)
|
|||||||
const calendarMonth = ref(new Date())
|
const calendarMonth = ref(new Date())
|
||||||
const dailyCounts = ref<Record<string, number>>({})
|
const dailyCounts = ref<Record<string, number>>({})
|
||||||
const selectedDate = ref('')
|
const selectedDate = ref('')
|
||||||
|
// 服务器时区偏移(如 "-04:00"/"+08:00"/"Z"):从接口返回的 sign_time 中推导。
|
||||||
|
// 日历按服务器本地日期分组,点选某天时也必须用同一时区构造筛选区间,
|
||||||
|
// 否则浏览器时区与服务器时区不同会导致日历数字与列表条数不一致。
|
||||||
|
const serverOffset = ref<string | null>(null)
|
||||||
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
const weekdays = ['日', '一', '二', '三', '四', '五', '六']
|
||||||
const todayDate = formatDateKey(new Date())
|
const todayDate = formatDateKey(new Date())
|
||||||
|
|
||||||
const calendarDays = computed(() => monthDays(calendarMonth.value))
|
const calendarDays = computed(() => monthDays(calendarMonth.value))
|
||||||
|
|
||||||
|
function extractServerOffset(value: string): string | null {
|
||||||
|
const match = /(Z|[+-]\d{2}:\d{2})$/.exec(value)
|
||||||
|
return match ? match[1] : null
|
||||||
|
}
|
||||||
|
|
||||||
|
function serverLocalISO(dateKey: string, hour: number, minute: number, second: number, millisecond: number): string {
|
||||||
|
const hh = String(hour).padStart(2, '0')
|
||||||
|
const mm = String(minute).padStart(2, '0')
|
||||||
|
const ss = String(second).padStart(2, '0')
|
||||||
|
const ms = String(millisecond).padStart(3, '0')
|
||||||
|
const offset = serverOffset.value ?? ''
|
||||||
|
return new Date(`${dateKey}T${hh}:${mm}:${ss}.${ms}${offset}`).toISOString()
|
||||||
|
}
|
||||||
|
|
||||||
function formatTime(value: string): string {
|
function formatTime(value: string): string {
|
||||||
return new Date(value).toLocaleString()
|
return new Date(value).toLocaleString()
|
||||||
}
|
}
|
||||||
@@ -51,10 +69,12 @@ function monthDays(value: Date): Array<string | null> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function dateRangeForDay(value: string): { since: string; until: string } {
|
function dateRangeForDay(value: string): { since: string; until: string } {
|
||||||
const [year, month, day] = value.split('-').map(Number)
|
// 用服务器时区构造当天 00:00 ~ 23:59:59.999 的区间,
|
||||||
const start = new Date(year, month - 1, day)
|
// 与后端按服务器本地日期分组的日历保持一致(任意浏览器时区下均一致)。
|
||||||
const end = new Date(year, month - 1, day, 23, 59, 59, 999)
|
return {
|
||||||
return { since: start.toISOString(), until: end.toISOString() }
|
since: serverLocalISO(value, 0, 0, 0, 0),
|
||||||
|
until: serverLocalISO(value, 23, 59, 59, 999),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function canPrev(): boolean {
|
function canPrev(): boolean {
|
||||||
@@ -71,8 +91,12 @@ async function loadCalendar() {
|
|||||||
try {
|
try {
|
||||||
const year = calendarMonth.value.getFullYear()
|
const year = calendarMonth.value.getFullYear()
|
||||||
const month = calendarMonth.value.getMonth()
|
const month = calendarMonth.value.getMonth()
|
||||||
const since = new Date(year, month, 1).toISOString()
|
// 按服务器时区取该月首日 00:00 与末日 23:59:59.999 的区间。
|
||||||
const until = new Date(year, month + 1, 1).toISOString()
|
const firstDay = `${year}-${String(month + 1).padStart(2, '0')}-01`
|
||||||
|
const lastDayOfMonth = new Date(year, month + 1, 0).getDate()
|
||||||
|
const lastDay = `${year}-${String(month + 1).padStart(2, '0')}-${String(lastDayOfMonth).padStart(2, '0')}`
|
||||||
|
const since = serverLocalISO(firstDay, 0, 0, 0, 0)
|
||||||
|
const until = serverLocalISO(lastDay, 23, 59, 59, 999)
|
||||||
const response = await getSignDailyCounts({ since, until })
|
const response = await getSignDailyCounts({ since, until })
|
||||||
dailyCounts.value = Object.fromEntries(response.items.map((item) => [item.date, item.count]))
|
dailyCounts.value = Object.fromEntries(response.items.map((item) => [item.date, item.count]))
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -93,6 +117,11 @@ async function loadRecords(nextPage = page.value) {
|
|||||||
records.value = response.items
|
records.value = response.items
|
||||||
total.value = response.total ?? response.offset + response.items.length
|
total.value = response.total ?? response.offset + response.items.length
|
||||||
page.value = safePage
|
page.value = safePage
|
||||||
|
// 从返回记录推导服务器时区偏移;首次获知后刷新日历(保证日历区间与列表筛选同钟)。
|
||||||
|
if (serverOffset.value === null && records.value.length > 0 && records.value[0].sign_time) {
|
||||||
|
serverOffset.value = extractServerOffset(records.value[0].sign_time)
|
||||||
|
loadCalendar()
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
error.value = err instanceof Error ? err.message : String(err)
|
error.value = err instanceof Error ? err.message : String(err)
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user