feat: opt message pages
This commit is contained in:
+11
-10
@@ -6,16 +6,17 @@ import (
|
|||||||
|
|
||||||
// AppLog 统一应用日志与通知记录
|
// AppLog 统一应用日志与通知记录
|
||||||
type AppLog struct {
|
type AppLog struct {
|
||||||
ID string `json:"id" gorm:"primaryKey;size:20"`
|
ID string `json:"id" gorm:"primaryKey;size:20"`
|
||||||
Category string `json:"category" gorm:"size:50;index;not null"` // 大类:constant.LogCategorySystemNotice(系统通知), constant.LogCategoryPushLog(推送记录) 等
|
Category string `json:"category" gorm:"size:50;index;not null"` // 大类:constant.LogCategorySystemNotice(系统通知), constant.LogCategoryPushLog(推送记录) 等
|
||||||
Title string `json:"title" gorm:"size:255"` // 消息标题
|
Title string `json:"title" gorm:"size:255"` // 消息标题
|
||||||
Content BigText `json:"content"` // 详细内容/Payload
|
Content BigText `json:"content"` // 详细内容/Payload
|
||||||
Level string `json:"level" gorm:"size:20;index"` // 级别:constant.LogLevelInfo, constant.LogLevelWarning, constant.LogLevelError
|
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
|
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等)
|
RefID string `json:"ref_id" gorm:"size:50;index"` // 关联对象ID(选填,比如绑定的通知渠道ID、任务ID等)
|
||||||
ErrorMsg BigText `json:"error_msg"` // 执行错误信息详情
|
ErrorMsg BigText `json:"error_msg"` // 执行错误信息详情
|
||||||
CreatedAt LocalTime `json:"created_at" gorm:"index"`
|
CreatedAt LocalTime `json:"created_at" gorm:"index"`
|
||||||
ReadAt *LocalTime `json:"read_at"` // 已读时间(仅对通知生效)
|
ReadAt *LocalTime `json:"read_at"` // 已读时间(仅对通知生效)
|
||||||
|
ChannelName string `json:"channel_name" gorm:"-"` // 推送记录的关联渠道名称(动态查询)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AppLog) TableName() string {
|
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) {
|
func (s *AppLogService) List(category string, status string, level string, page, pageSize int, keyword string) ([]models.AppLog, int64, error) {
|
||||||
var logs []models.AppLog
|
var logs []models.AppLog
|
||||||
var total int64
|
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 != "" {
|
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 != "" {
|
if status != "" {
|
||||||
query = query.Where("status = ?", status)
|
field := "status"
|
||||||
|
if category == constant.LogCategoryPushLog {
|
||||||
|
field = "al.status"
|
||||||
|
}
|
||||||
|
db = db.Where(field+" = ?", status)
|
||||||
}
|
}
|
||||||
if level != "" {
|
if level != "" {
|
||||||
query = query.Where("level = ?", level)
|
field := "level"
|
||||||
|
if category == constant.LogCategoryPushLog {
|
||||||
|
field = "al.level"
|
||||||
|
}
|
||||||
|
db = db.Where(field+" = ?", level)
|
||||||
}
|
}
|
||||||
if keyword != "" {
|
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
|
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
|
return logs, total, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,7 +200,7 @@ func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) {
|
|||||||
content, _ := payload["content"].(string)
|
content, _ := payload["content"].(string)
|
||||||
success, _ := payload["success"].(bool)
|
success, _ := payload["success"].(bool)
|
||||||
errorMsg, _ := payload["error_msg"].(string)
|
errorMsg, _ := payload["error_msg"].(string)
|
||||||
channelName, _ := payload["channel_name"].(string)
|
channelID, _ := payload["channel_id"].(string)
|
||||||
|
|
||||||
status := constant.LogStatusSuccess
|
status := constant.LogStatusSuccess
|
||||||
level := constant.LogLevelInfo
|
level := constant.LogLevelInfo
|
||||||
@@ -182,10 +211,11 @@ func (s *AppLogService) SubscribeEvents(bus *eventbus.EventBus) {
|
|||||||
|
|
||||||
s.Add(&models.AppLog{
|
s.Add(&models.AppLog{
|
||||||
Category: constant.LogCategoryPushLog,
|
Category: constant.LogCategoryPushLog,
|
||||||
Title: fmt.Sprintf("[%s] %s", channelName, title),
|
Title: title,
|
||||||
Content: models.BigText(content),
|
Content: models.BigText(content),
|
||||||
Level: level,
|
Level: level,
|
||||||
Status: status,
|
Status: status,
|
||||||
|
RefID: channelID,
|
||||||
ErrorMsg: models.BigText(errorMsg),
|
ErrorMsg: models.BigText(errorMsg),
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -203,15 +203,16 @@ func (s *NotificationService) SendToChannel(channel NotifyChannel, msg *NotifyMe
|
|||||||
Title: msg.Title,
|
Title: msg.Title,
|
||||||
Text: msg.Text,
|
Text: msg.Text,
|
||||||
})
|
})
|
||||||
|
|
||||||
payload := map[string]interface{}{
|
payload := map[string]interface{}{
|
||||||
"title": msg.Title,
|
"title": msg.Title,
|
||||||
"content": msg.Text,
|
"content": msg.Text,
|
||||||
|
"channel_id": channel.ID,
|
||||||
"channel_name": channel.Name,
|
"channel_name": channel.Name,
|
||||||
"success": false,
|
"success": false,
|
||||||
"error_msg": "",
|
"error_msg": "",
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
payload["error_msg"] = err.Error()
|
payload["error_msg"] = err.Error()
|
||||||
eventbus.DefaultBus.Publish(eventbus.Event{
|
eventbus.DefaultBus.Publish(eventbus.Event{
|
||||||
|
|||||||
@@ -589,6 +589,7 @@ export interface AppLog {
|
|||||||
level: string
|
level: string
|
||||||
status: string
|
status: string
|
||||||
ref_id: string
|
ref_id: string
|
||||||
|
channel_name?: string
|
||||||
error_msg: string
|
error_msg: string
|
||||||
created_at: string
|
created_at: string
|
||||||
read_at: string | null
|
read_at: string | null
|
||||||
|
|||||||
@@ -289,9 +289,11 @@ function onDialogClose(open: boolean) {
|
|||||||
内容详情
|
内容详情
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<pre v-if="detailDialogProps.content"
|
<div 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>
|
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">
|
||||||
<div v-else class="text-xs text-muted-foreground italic py-2">无内容</div>
|
{{ detailDialogProps.content }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground italic py-2">无内容</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="detailDialogProps.error">
|
<template v-if="detailDialogProps.error">
|
||||||
@@ -300,8 +302,11 @@ function onDialogClose(open: boolean) {
|
|||||||
错误信息
|
错误信息
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<pre
|
<div v-if="detailDialogProps.error"
|
||||||
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>
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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="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)">
|
: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-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 class="w-40 sm:w-56 shrink-0 font-medium text-xs sm:text-sm truncate" :title="log.title">
|
||||||
}}</span>
|
<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">
|
<span class="hidden sm:flex sm:flex-1 text-xs sm:text-sm text-muted-foreground truncate" :title="log.content">
|
||||||
{{ log.content || '-' }}
|
{{ log.content || '-' }}
|
||||||
</span>
|
</span>
|
||||||
@@ -270,6 +272,10 @@ function onDialogClose(open: boolean) {
|
|||||||
<span class="text-muted-foreground">标题</span>
|
<span class="text-muted-foreground">标题</span>
|
||||||
<span class="font-medium text-foreground">{{ detailDialogProps.title }}</span>
|
<span class="font-medium text-foreground">{{ detailDialogProps.title }}</span>
|
||||||
</div>
|
</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">
|
<div class="flex justify-between items-center text-sm">
|
||||||
<span class="text-muted-foreground">发生时间</span>
|
<span class="text-muted-foreground">发生时间</span>
|
||||||
<span class="font-mono text-xs text-muted-foreground">{{ selectedLog ? formatDate(selectedLog.created_at)
|
<span class="font-mono text-xs text-muted-foreground">{{ selectedLog ? formatDate(selectedLog.created_at)
|
||||||
@@ -284,9 +290,11 @@ function onDialogClose(open: boolean) {
|
|||||||
推送内容
|
推送内容
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<pre v-if="detailDialogProps.content"
|
<div 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>
|
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">
|
||||||
<div v-else class="text-xs text-muted-foreground italic py-2">无推送内容</div>
|
{{ detailDialogProps.content }}
|
||||||
|
</div>
|
||||||
|
<div v-else class="text-sm text-muted-foreground italic py-2">无推送内容</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template v-if="detailDialogProps.error">
|
<template v-if="detailDialogProps.error">
|
||||||
@@ -295,8 +303,11 @@ function onDialogClose(open: boolean) {
|
|||||||
错误信息
|
错误信息
|
||||||
</div>
|
</div>
|
||||||
<div class="p-6">
|
<div class="p-6">
|
||||||
<pre
|
<div v-if="detailDialogProps.error"
|
||||||
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>
|
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>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user