refactor: replace standard log with zap logger and optimize code patterns

- Migrate all log.Printf/log.Println calls to structured zap logging
- Use cmp.Or for cleaner default value handling
- Replace manual loops with slices.ContainsFunc/IndexFunc
- Add batch query methods (IsLikedBatch, IsFavoritedBatch) to solve N+1 problem
- Update JSON tags from omitempty to omitzero for numeric fields
- Use errgroup-style wg.Go for worker goroutines
- Remove duplicate casbin/v2 dependency (keeping v3)
- Add plans/ to gitignore
This commit is contained in:
lafay
2026-03-17 00:47:17 +08:00
parent b028f7e1d3
commit 5d6c982c9c
38 changed files with 2256 additions and 290 deletions

View File

@@ -224,34 +224,34 @@ type TextSegmentData struct {
// ImageSegmentData 图片数据
type ImageSegmentData struct {
URL string `json:"url"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Width int `json:"width,omitzero"`
Height int `json:"height,omitzero"`
ThumbnailURL string `json:"thumbnail_url,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
FileSize int64 `json:"file_size,omitzero"`
}
// VoiceSegmentData 语音数据
type VoiceSegmentData struct {
URL string `json:"url"`
Duration int `json:"duration,omitempty"` // 秒
FileSize int64 `json:"file_size,omitempty"`
Duration int `json:"duration,omitzero"` // 秒
FileSize int64 `json:"file_size,omitzero"`
}
// VideoSegmentData 视频数据
type VideoSegmentData struct {
URL string `json:"url"`
Width int `json:"width,omitempty"`
Height int `json:"height,omitempty"`
Duration int `json:"duration,omitempty"` // 秒
Width int `json:"width,omitzero"`
Height int `json:"height,omitzero"`
Duration int `json:"duration,omitzero"` // 秒
ThumbnailURL string `json:"thumbnail_url,omitempty"`
FileSize int64 `json:"file_size,omitempty"`
FileSize int64 `json:"file_size,omitzero"`
}
// FileSegmentData 文件数据
type FileSegmentData struct {
URL string `json:"url"`
Name string `json:"name"`
Size int64 `json:"size,omitempty"`
Size int64 `json:"size,omitzero"`
MimeType string `json:"mime_type,omitempty"`
}
@@ -268,7 +268,7 @@ type ReplySegmentData struct {
// FaceSegmentData 表情数据
type FaceSegmentData struct {
ID int `json:"id"`
ID int `json:"id,omitzero"`
Name string `json:"name,omitempty"`
URL string `json:"url,omitempty"`
}
@@ -458,7 +458,7 @@ type DeviceTokenResponse struct {
DeviceType string `json:"device_type"`
IsActive bool `json:"is_active"`
DeviceName string `json:"device_name"`
LastUsedAt time.Time `json:"last_used_at,omitempty"`
LastUsedAt time.Time `json:"last_used_at,omitzero"`
CreatedAt time.Time `json:"created_at"`
}
@@ -470,9 +470,9 @@ type PushRecordResponse struct {
MessageID string `json:"message_id"`
PushChannel string `json:"push_channel"`
PushStatus string `json:"push_status"`
RetryCount int `json:"retry_count"`
PushedAt time.Time `json:"pushed_at,omitempty"`
DeliveredAt time.Time `json:"delivered_at,omitempty"`
RetryCount int `json:"retry_count,omitzero"`
PushedAt time.Time `json:"pushed_at,omitzero"`
DeliveredAt time.Time `json:"delivered_at,omitzero"`
CreatedAt time.Time `json:"created_at"`
}
@@ -859,7 +859,7 @@ type VoteResultDTO struct {
Options []VoteOptionDTO `json:"options"`
TotalVotes int `json:"total_votes"`
HasVoted bool `json:"has_voted"`
VotedOptionID string `json:"voted_option_id,omitempty"`
VotedOptionID string `json:"voted_option_id,omitzero"`
}
// ==================== WebSocket Response DTOs ====================
@@ -882,10 +882,10 @@ type AdminPostListResponse struct {
Content string `json:"content"`
Images []PostImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
CommentsCount int `json:"comments_count"`
FavoritesCount int `json:"favorites_count"`
ViewsCount int `json:"views_count"`
LikesCount int `json:"likes_count,omitzero"`
CommentsCount int `json:"comments_count,omitzero"`
FavoritesCount int `json:"favorites_count,omitzero"`
ViewsCount int `json:"views_count,omitzero"`
IsPinned bool `json:"is_pinned"`
IsFeatured bool `json:"is_featured"`
IsLocked bool `json:"is_locked"`
@@ -906,11 +906,11 @@ type AdminPostDetailResponse struct {
Content string `json:"content"`
Images []PostImageResponse `json:"images"`
Status string `json:"status"`
LikesCount int `json:"likes_count"`
CommentsCount int `json:"comments_count"`
FavoritesCount int `json:"favorites_count"`
SharesCount int `json:"shares_count"`
ViewsCount int `json:"views_count"`
LikesCount int `json:"likes_count,omitzero"`
CommentsCount int `json:"comments_count,omitzero"`
FavoritesCount int `json:"favorites_count,omitzero"`
SharesCount int `json:"shares_count,omitzero"`
ViewsCount int `json:"views_count,omitzero"`
HotScore float64 `json:"hot_score"`
IsPinned bool `json:"is_pinned"`
IsFeatured bool `json:"is_featured"`

View File

@@ -5,9 +5,9 @@ type ScheduleCourseResponse struct {
Name string `json:"name"`
Teacher string `json:"teacher,omitempty"`
Location string `json:"location,omitempty"`
DayOfWeek int `json:"day_of_week"`
StartSection int `json:"start_section"`
EndSection int `json:"end_section"`
DayOfWeek int `json:"day_of_week,omitzero"`
StartSection int `json:"start_section,omitzero"`
EndSection int `json:"end_section,omitzero"`
Weeks []int `json:"weeks"`
Color string `json:"color,omitempty"`
}

View File

@@ -3,6 +3,7 @@ package dto
import (
"encoding/json"
"fmt"
"slices"
"carrot_bbs/internal/model"
)
@@ -232,25 +233,27 @@ func ExtractMentionedUsers(segments model.MessageSegments) []string {
// IsAtAll 检查消息是否@了所有人
func IsAtAll(segments model.MessageSegments) bool {
for _, segment := range segments {
return slices.ContainsFunc(segments, func(segment model.MessageSegment) bool {
if segment.Type == string(SegmentTypeAt) {
if userID, ok := segment.Data["user_id"].(string); ok && userID == "all" {
return true
}
}
}
return false
return false
})
}
// GetReplyMessageID 从消息链中获取被回复的消息ID
// 如果没有回复segment返回空字符串
func GetReplyMessageID(segments model.MessageSegments) string {
for _, segment := range segments {
if segment.Type == string(SegmentTypeReply) {
if id, ok := segment.Data["id"].(string); ok {
return id
}
}
idx := slices.IndexFunc(segments, func(segment model.MessageSegment) bool {
return segment.Type == string(SegmentTypeReply)
})
if idx == -1 {
return ""
}
if id, ok := segments[idx].Data["id"].(string); ok {
return id
}
return ""
}
@@ -291,12 +294,9 @@ func BuildSegmentsFromContent(contentType, content string, mediaURL *string) mod
// HasSegmentType 检查消息链中是否包含指定类型的segment
func HasSegmentType(segments model.MessageSegments, segmentType SegmentType) bool {
for _, segment := range segments {
if segment.Type == string(segmentType) {
return true
}
}
return false
return slices.ContainsFunc(segments, func(segment model.MessageSegment) bool {
return segment.Type == string(segmentType)
})
}
// GetSegmentsByType 获取消息链中所有指定类型的segment
@@ -313,12 +313,14 @@ func GetSegmentsByType(segments model.MessageSegments, segmentType SegmentType)
// GetFirstImageURL 获取消息链中第一张图片的URL
// 如果没有图片,返回空字符串
func GetFirstImageURL(segments model.MessageSegments) string {
for _, segment := range segments {
if segment.Type == string(SegmentTypeImage) {
if url, ok := segment.Data["url"].(string); ok {
return url
}
}
idx := slices.IndexFunc(segments, func(segment model.MessageSegment) bool {
return segment.Type == string(SegmentTypeImage)
})
if idx == -1 {
return ""
}
if url, ok := segments[idx].Data["url"].(string); ok {
return url
}
return ""
}
@@ -326,13 +328,18 @@ func GetFirstImageURL(segments model.MessageSegments) string {
// GetFirstMediaURL 获取消息链中第一个媒体文件的URL图片/视频/语音/文件)
// 用于兼容旧版本API
func GetFirstMediaURL(segments model.MessageSegments) string {
for _, segment := range segments {
idx := slices.IndexFunc(segments, func(segment model.MessageSegment) bool {
switch segment.Type {
case string(SegmentTypeImage), string(SegmentTypeVideo), string(SegmentTypeVoice), string(SegmentTypeFile):
if url, ok := segment.Data["url"].(string); ok {
return url
}
return true
}
return false
})
if idx == -1 {
return ""
}
if url, ok := segments[idx].Data["url"].(string); ok {
return url
}
return ""
}