forked from kevin/meshtastic_mqtt_server
Compare commits
5
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83f7f551fc | ||
|
|
89e56a4048 | ||
|
|
b817a82cad | ||
|
|
d33ec0075d | ||
|
|
5d174caabb |
+14
-12
@@ -95,18 +95,20 @@ database:
|
|||||||
path: ${DATA_DIR}/${SERVICE_NAME}.db
|
path: ${DATA_DIR}/${SERVICE_NAME}.db
|
||||||
mysql:
|
mysql:
|
||||||
dsn: ""
|
dsn: ""
|
||||||
web:
|
web:
|
||||||
enabled: true
|
enabled: true
|
||||||
host: 0.0.0.0
|
port_enabled: true
|
||||||
port: 8080
|
socket_enabled: true
|
||||||
socket_path: ${SOCKET_PATH}
|
host: 0.0.0.0
|
||||||
static_dir: ${INSTALL_DIR}/dist
|
port: 8080
|
||||||
admin:
|
socket_path: ${SOCKET_PATH}
|
||||||
username: admin
|
static_dir: ${INSTALL_DIR}/dist
|
||||||
password: ${ADMIN_PASSWORD}
|
admin:
|
||||||
session_secret: ""
|
username: admin
|
||||||
# 前端经 HTTPS(nginx 反代)访问时保持 true;纯 HTTP 部署需改回 false
|
password: ${ADMIN_PASSWORD}
|
||||||
session_secure: true
|
session_secret: ""
|
||||||
|
# 前端经 HTTPS(nginx 反代)访问时保持 true;纯 HTTP 部署需改回 false
|
||||||
|
session_secure: true
|
||||||
console_log:
|
console_log:
|
||||||
web: true
|
web: true
|
||||||
mqtt: true
|
mqtt: true
|
||||||
|
|||||||
+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
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { adminLogout, createNodeBlockingRule, deleteNode, deleteTextMessage, getAdminMe, getChannels, getHealth, getMapReportViewport, getNodeInfo, getPositions, getTextMessages, purgeNode } from './api'
|
import { adminLogout, createNodeBlockingRule, deleteNode, deleteTextMessage, getAdminMe, getChannels, getHealth, getMapReportViewport, getMapReports, getNodeInfo, getPositions, getTextMessages, purgeNode } from './api'
|
||||||
import AdminBlockingManagement from './components/AdminBlockingManagement.vue'
|
import AdminBlockingManagement from './components/AdminBlockingManagement.vue'
|
||||||
import AdminBot from './components/AdminBot.vue'
|
import AdminBot from './components/AdminBot.vue'
|
||||||
import AdminBotDirect from './components/AdminBotDirect.vue'
|
import AdminBotDirect from './components/AdminBotDirect.vue'
|
||||||
@@ -476,20 +476,32 @@ function requestPurgeNode(nodeId: string) {
|
|||||||
pendingDeleteAction.value = { kind: 'purge-node', nodeId }
|
pendingDeleteAction.value = { kind: 'purge-node', nodeId }
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestDeleteDisplayedNodes() {
|
async function requestDeleteDisplayedNodes() {
|
||||||
// 收集当前地图上正在显示的节点 ID(仅 type=node,跳过聚合点),并按筛选后的视图顺序去重。
|
// 收集当前地图上正在显示的节点 ID(仅 type=node,跳过聚合点),并按筛选后的视图顺序去重。
|
||||||
const nodeIds = Array.from(
|
const nodeIds = new Set(
|
||||||
new Set(
|
mapItems.value
|
||||||
mapItems.value
|
.filter((item): item is Extract<MapRenderable, { type: 'node' }> => item.type === 'node')
|
||||||
.filter((item): item is Extract<MapRenderable, { type: 'node' }> => item.type === 'node')
|
.map((item) => item.node_id),
|
||||||
.map((item) => item.node_id),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if (nodeIds.length === 0) {
|
// 聚合状态下视口接口只返回聚合点(无 node_id),改按当前视野 bounds 分页拉取全部节点 ID。
|
||||||
|
if (mapViewportMode.value === 'clusters' && currentMapBounds.value) {
|
||||||
|
const pageSize = 500
|
||||||
|
for (let offset = 0; ; offset += pageSize) {
|
||||||
|
const response = await getMapReports(pageSize, offset, currentMapBounds.value)
|
||||||
|
for (const report of response.items) {
|
||||||
|
nodeIds.add(report.node_id)
|
||||||
|
}
|
||||||
|
const loaded = offset + response.items.length
|
||||||
|
if (response.items.length === 0 || loaded >= (response.total ?? loaded)) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (nodeIds.size === 0) {
|
||||||
error.value = '当前地图上没有可删除的节点。'
|
error.value = '当前地图上没有可删除的节点。'
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
pendingDeleteAction.value = { kind: 'delete-displayed-nodes', nodeIds }
|
pendingDeleteAction.value = { kind: 'delete-displayed-nodes', nodeIds: Array.from(nodeIds) }
|
||||||
}
|
}
|
||||||
|
|
||||||
function requestDeleteAndBlockNode(payload: NodeActionRequest) {
|
function requestDeleteAndBlockNode(payload: NodeActionRequest) {
|
||||||
|
|||||||
Reference in New Issue
Block a user