feat: opt message pages
This commit is contained in:
+11
-10
@@ -6,16 +6,17 @@ import (
|
||||
|
||||
// AppLog 统一应用日志与通知记录
|
||||
type AppLog struct {
|
||||
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||
Category string `json:"category" gorm:"size:50;index;not null"` // 大类:constant.LogCategorySystemNotice(系统通知), constant.LogCategoryPushLog(推送记录) 等
|
||||
Title string `json:"title" gorm:"size:255"` // 消息标题
|
||||
Content BigText `json:"content"` // 详细内容/Payload
|
||||
Level string `json:"level" gorm:"size:20;index"` // 级别:constant.LogLevelInfo, constant.LogLevelWarning, constant.LogLevelError
|
||||
Status string `json:"status" gorm:"size:20;index"` // 状态:系统通知为 constant.LogStatusRead/constant.LogStatusUnread,推送为 constant.LogStatusSuccess/constant.LogStatusFailed
|
||||
RefID string `json:"ref_id" gorm:"size:50;index"` // 关联对象ID(选填,比如绑定的通知渠道ID、任务ID等)
|
||||
ErrorMsg BigText `json:"error_msg"` // 执行错误信息详情
|
||||
CreatedAt LocalTime `json:"created_at" gorm:"index"`
|
||||
ReadAt *LocalTime `json:"read_at"` // 已读时间(仅对通知生效)
|
||||
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||
Category string `json:"category" gorm:"size:50;index;not null"` // 大类:constant.LogCategorySystemNotice(系统通知), constant.LogCategoryPushLog(推送记录) 等
|
||||
Title string `json:"title" gorm:"size:255"` // 消息标题
|
||||
Content BigText `json:"content"` // 详细内容/Payload
|
||||
Level string `json:"level" gorm:"size:20;index"` // 级别:constant.LogLevelInfo, constant.LogLevelWarning, constant.LogLevelError
|
||||
Status string `json:"status" gorm:"size:20;index"` // 状态:系统通知为 constant.LogStatusRead/constant.LogStatusUnread,推送为 constant.LogStatusSuccess/constant.LogStatusFailed
|
||||
RefID string `json:"ref_id" gorm:"size:50;index"` // 关联对象ID(选填,比如绑定的通知渠道ID、任务ID等)
|
||||
ErrorMsg BigText `json:"error_msg"` // 执行错误信息详情
|
||||
CreatedAt LocalTime `json:"created_at" gorm:"index"`
|
||||
ReadAt *LocalTime `json:"read_at"` // 已读时间(仅对通知生效)
|
||||
ChannelName string `json:"channel_name" gorm:"-"` // 推送记录的关联渠道名称(动态查询)
|
||||
}
|
||||
|
||||
func (AppLog) TableName() string {
|
||||
|
||||
@@ -38,24 +38,53 @@ func (s *AppLogService) Add(log *models.AppLog) error {
|
||||
func (s *AppLogService) List(category string, status string, level string, page, pageSize int, keyword string) ([]models.AppLog, int64, error) {
|
||||
var logs []models.AppLog
|
||||
var total int64
|
||||
query := database.DB.Model(&models.AppLog{})
|
||||
db := database.DB
|
||||
|
||||
// 如果是推送日志,尝试关联查询渠道名称
|
||||
if category == constant.LogCategoryPushLog {
|
||||
db = db.Table(models.AppLog{}.TableName() + " AS al").
|
||||
Select("al.*, nw.name as channel_name").
|
||||
Joins(fmt.Sprintf("LEFT JOIN %s AS nw ON al.ref_id = nw.id", models.NotifyWay{}.TableName()))
|
||||
} else {
|
||||
db = db.Model(&models.AppLog{})
|
||||
}
|
||||
|
||||
if category != "" {
|
||||
query = query.Where("category = ?", category)
|
||||
if category == constant.LogCategoryPushLog {
|
||||
db = db.Where("al.category = ?", category)
|
||||
} else {
|
||||
db = db.Where("category = ?", category)
|
||||
}
|
||||
}
|
||||
if status != "" {
|
||||
query = query.Where("status = ?", status)
|
||||
field := "status"
|
||||
if category == constant.LogCategoryPushLog {
|
||||
field = "al.status"
|
||||
}
|
||||
db = db.Where(field+" = ?", status)
|
||||
}
|
||||
if level != "" {
|
||||
query = query.Where("level = ?", level)
|
||||
field := "level"
|
||||
if category == constant.LogCategoryPushLog {
|
||||
field = "al.level"
|
||||
}
|
||||
db = db.Where(field+" = ?", level)
|
||||
}
|
||||
if keyword != "" {
|
||||
query = query.Where("(title LIKE ? OR content LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")
|
||||
if category == constant.LogCategoryPushLog {
|
||||
db = db.Where("(al.title LIKE ? OR al.content LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")
|
||||
} else {
|
||||
db = db.Where("(title LIKE ? OR content LIKE ?)", "%"+keyword+"%", "%"+keyword+"%")
|
||||
}
|
||||
}
|
||||
|
||||
query.Count(&total)
|
||||
db.Count(&total)
|
||||
offset := (page - 1) * pageSize
|
||||
err := query.Order("created_at desc").Offset(offset).Limit(pageSize).Find(&logs).Error
|
||||
order := "created_at desc"
|
||||
if category == constant.LogCategoryPushLog {
|
||||
order = "al.created_at desc"
|
||||
}
|
||||
err := db.Order(order).Offset(offset).Limit(pageSize).Scan(&logs).Error
|
||||
return logs, total, err
|
||||
}
|
||||
|
||||
@@ -171,7 +200,7 @@ func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) {
|
||||
content, _ := payload["content"].(string)
|
||||
success, _ := payload["success"].(bool)
|
||||
errorMsg, _ := payload["error_msg"].(string)
|
||||
channelName, _ := payload["channel_name"].(string)
|
||||
channelID, _ := payload["channel_id"].(string)
|
||||
|
||||
status := constant.LogStatusSuccess
|
||||
level := constant.LogLevelInfo
|
||||
@@ -182,10 +211,11 @@ func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) {
|
||||
|
||||
s.Add(&models.AppLog{
|
||||
Category: constant.LogCategoryPushLog,
|
||||
Title: fmt.Sprintf("[%s] %s", channelName, title),
|
||||
Title: title,
|
||||
Content: models.BigText(content),
|
||||
Level: level,
|
||||
Status: status,
|
||||
RefID: channelID,
|
||||
ErrorMsg: models.BigText(errorMsg),
|
||||
})
|
||||
})
|
||||
|
||||
@@ -203,15 +203,16 @@ func (s *NotificationService) SendToChannel(channel NotifyChannel, msg *NotifyMe
|
||||
Title: msg.Title,
|
||||
Text: msg.Text,
|
||||
})
|
||||
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"title": msg.Title,
|
||||
"content": msg.Text,
|
||||
"channel_id": channel.ID,
|
||||
"channel_name": channel.Name,
|
||||
"success": false,
|
||||
"error_msg": "",
|
||||
}
|
||||
|
||||
|
||||
if err != nil {
|
||||
payload["error_msg"] = err.Error()
|
||||
eventbus.DefaultBus.Publish(eventbus.Event{
|
||||
|
||||
@@ -589,6 +589,7 @@ export interface AppLog {
|
||||
level: string
|
||||
status: string
|
||||
ref_id: string
|
||||
channel_name?: string
|
||||
error_msg: string
|
||||
created_at: string
|
||||
read_at: string | null
|
||||
|
||||
@@ -289,9 +289,11 @@ function onDialogClose(open: boolean) {
|
||||
内容详情
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<pre v-if="detailDialogProps.content"
|
||||
class="text-xs font-mono bg-muted/30 p-4 rounded-lg border border-muted/50 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.content }}</pre>
|
||||
<div v-else class="text-xs text-muted-foreground italic py-2">无内容</div>
|
||||
<div v-if="detailDialogProps.content"
|
||||
class="text-sm text-foreground bg-muted/20 p-5 rounded-xl border border-border/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
|
||||
{{ detailDialogProps.content }}
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground italic py-2">无内容</div>
|
||||
</div>
|
||||
|
||||
<template v-if="detailDialogProps.error">
|
||||
@@ -300,8 +302,11 @@ function onDialogClose(open: boolean) {
|
||||
错误信息
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<pre
|
||||
class="text-xs font-mono bg-red-500/5 text-red-600/90 p-4 rounded-lg border border-red-500/20 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.error }}</pre>
|
||||
<div v-if="detailDialogProps.error"
|
||||
class="text-sm text-red-700 bg-red-500/10 p-5 rounded-xl border border-red-200/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
|
||||
{{ detailDialogProps.error }}
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground italic py-2">无错误信息</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -226,8 +226,10 @@ function onDialogClose(open: boolean) {
|
||||
class="flex items-center gap-2 sm:gap-4 px-3 sm:px-4 py-2 hover:bg-muted/50 transition-colors cursor-pointer group"
|
||||
:class="[selectedLogId === log.id && 'bg-accent/50']" @click="showDetail(log)">
|
||||
<span class="w-12 sm:w-16 shrink-0 text-muted-foreground text-xs sm:text-sm">#{{ getLogIndex(index) }}</span>
|
||||
<span class="w-40 sm:w-56 shrink-0 font-medium text-xs sm:text-sm truncate" :title="log.title">{{ log.title
|
||||
}}</span>
|
||||
<span class="w-40 sm:w-56 shrink-0 font-medium text-xs sm:text-sm truncate" :title="log.title">
|
||||
<span v-if="log.channel_name" class="mr-1 text-muted-foreground">[{{ log.channel_name }}]</span>{{ log.title
|
||||
}}
|
||||
</span>
|
||||
<span class="hidden sm:flex sm:flex-1 text-xs sm:text-sm text-muted-foreground truncate" :title="log.content">
|
||||
{{ log.content || '-' }}
|
||||
</span>
|
||||
@@ -270,6 +272,10 @@ function onDialogClose(open: boolean) {
|
||||
<span class="text-muted-foreground">标题</span>
|
||||
<span class="font-medium text-foreground">{{ detailDialogProps.title }}</span>
|
||||
</div>
|
||||
<div v-if="selectedLog?.channel_name" class="flex justify-between items-center text-sm">
|
||||
<span class="text-muted-foreground">发送渠道</span>
|
||||
<span class="font-medium text-foreground">{{ selectedLog.channel_name }}</span>
|
||||
</div>
|
||||
<div class="flex justify-between items-center text-sm">
|
||||
<span class="text-muted-foreground">发生时间</span>
|
||||
<span class="font-mono text-xs text-muted-foreground">{{ selectedLog ? formatDate(selectedLog.created_at)
|
||||
@@ -284,9 +290,11 @@ function onDialogClose(open: boolean) {
|
||||
推送内容
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<pre v-if="detailDialogProps.content"
|
||||
class="text-xs font-mono bg-muted/30 p-4 rounded-lg border border-muted/50 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.content }}</pre>
|
||||
<div v-else class="text-xs text-muted-foreground italic py-2">无推送内容</div>
|
||||
<div v-if="detailDialogProps.content"
|
||||
class="text-sm text-foreground bg-muted/20 p-5 rounded-xl border border-border/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
|
||||
{{ detailDialogProps.content }}
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground italic py-2">无推送内容</div>
|
||||
</div>
|
||||
|
||||
<template v-if="detailDialogProps.error">
|
||||
@@ -295,8 +303,11 @@ function onDialogClose(open: boolean) {
|
||||
错误信息
|
||||
</div>
|
||||
<div class="p-6">
|
||||
<pre
|
||||
class="text-xs font-mono bg-red-500/5 text-red-600/90 p-4 rounded-lg border border-red-500/20 whitespace-pre-wrap break-all leading-relaxed shadow-inner">{{ detailDialogProps.error }}</pre>
|
||||
<div v-if="detailDialogProps.error"
|
||||
class="text-sm text-red-700 bg-red-500/10 p-5 rounded-xl border border-red-200/50 whitespace-pre-wrap break-all leading-relaxed shadow-sm">
|
||||
{{ detailDialogProps.error }}
|
||||
</div>
|
||||
<div v-else class="text-sm text-muted-foreground italic py-2">无错误信息</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user