refactor: cleanup unused code and simplify internal packages
Some checks failed
Build Backend / build (push) Successful in 1m56s
Build Backend / build-docker (push) Has been cancelled

This commit performs a significant cleanup of the codebase by removing unused functions, methods, and entire files across various internal modules. This reduces technical debt and simplifies the project structure.

Key changes include:
- **cache**: Removed unused cache key generators and metrics snapshots.
- **dto**: Removed redundant converter functions and segment creation helpers.
- **middleware**: Deleted the unused `logger.go` middleware and simplified `ratelimit.go` and `casbin.go`.
- **model**: Removed unused ID helpers, database closing functions, and batch decryption logic.
- **pkg**: Cleaned up unused utility functions in `circuitbreaker`, `crypto`, `cursor`, `hook`, and `utils`.
- **service**: Deleted `account_cleanup_service.go` and removed unused helper functions in `log_cleanup_service.go` and `sensitive_service.go`.
- **repository**: Removed unused private loading methods in `comment_repo.go`.
This commit is contained in:
2026-05-14 02:24:30 +08:00
parent d2894066f8
commit 9a1851f023
36 changed files with 0 additions and 1449 deletions

View File

@@ -86,25 +86,13 @@ func ConversationListKey(userID string, page, pageSize int) string {
return fmt.Sprintf("%s:%s:%d:%d", PrefixConversationList, userID, page, pageSize) return fmt.Sprintf("%s:%s:%d:%d", PrefixConversationList, userID, page, pageSize)
} }
// ConversationDetailKey 生成会话详情缓存键
func ConversationDetailKey(conversationID, userID string) string {
return fmt.Sprintf("%s:%s:%s", PrefixConversationDetail, conversationID, userID)
}
// GroupMembersKey 生成群组成员缓存键 // GroupMembersKey 生成群组成员缓存键
func GroupMembersKey(groupID string, page, pageSize int) string { func GroupMembersKey(groupID string, page, pageSize int) string {
return fmt.Sprintf("%s:%s:page:%d:size:%d", PrefixGroupMembers, groupID, page, pageSize) return fmt.Sprintf("%s:%s:page:%d:size:%d", PrefixGroupMembers, groupID, page, pageSize)
} }
// GroupMembersAllKey 生成群组全量成员ID列表缓存键
func GroupMembersAllKey(groupID string) string {
return fmt.Sprintf("%s:all:%s", PrefixGroupMembers, groupID)
}
// GroupInfoKey 生成群组信息缓存键
func GroupInfoKey(groupID string) string {
return fmt.Sprintf("%s:%s", PrefixGroupInfo, groupID)
}
// UnreadSystemKey 生成系统消息未读数缓存键 // UnreadSystemKey 生成系统消息未读数缓存键
func UnreadSystemKey(userID string) string { func UnreadSystemKey(userID string) string {
@@ -121,15 +109,6 @@ func UnreadDetailKey(userID, conversationID string) string {
return fmt.Sprintf("%s:%s:%s", PrefixUnreadDetail, userID, conversationID) return fmt.Sprintf("%s:%s:%s", PrefixUnreadDetail, userID, conversationID)
} }
// UserInfoKey 生成用户信息缓存键
func UserInfoKey(userID string) string {
return fmt.Sprintf("%s:%s", PrefixUserInfo, userID)
}
// UserMeKey 生成当前用户信息缓存键
func UserMeKey(userID string) string {
return fmt.Sprintf("%s:%s", PrefixUserMe, userID)
}
// ChannelsAllListKey 频道全量列表缓存键(含未启用,供 MapByIDs 解析历史帖子 channel // ChannelsAllListKey 频道全量列表缓存键(含未启用,供 MapByIDs 解析历史帖子 channel
func ChannelsAllListKey() string { func ChannelsAllListKey() string {
@@ -156,20 +135,12 @@ func InvalidateConversationList(cache Cache, userID string) {
cache.DeleteByPrefix(PrefixConversationList + ":" + userID + ":") cache.DeleteByPrefix(PrefixConversationList + ":" + userID + ":")
} }
// InvalidateConversationDetail 失效会话详情缓存
func InvalidateConversationDetail(cache Cache, conversationID, userID string) {
cache.Delete(ConversationDetailKey(conversationID, userID))
}
// InvalidateGroupMembers 失效群组成员缓存 // InvalidateGroupMembers 失效群组成员缓存
func InvalidateGroupMembers(cache Cache, groupID string) { func InvalidateGroupMembers(cache Cache, groupID string) {
cache.DeleteByPrefix(PrefixGroupMembers + ":" + groupID) cache.DeleteByPrefix(PrefixGroupMembers + ":" + groupID)
} }
// InvalidateGroupInfo 失效群组信息缓存
func InvalidateGroupInfo(cache Cache, groupID string) {
cache.Delete(GroupInfoKey(groupID))
}
// InvalidateUnreadSystem 失效系统消息未读数缓存 // InvalidateUnreadSystem 失效系统消息未读数缓存
func InvalidateUnreadSystem(cache Cache, userID string) { func InvalidateUnreadSystem(cache Cache, userID string) {
@@ -186,11 +157,6 @@ func InvalidateUnreadDetail(cache Cache, userID, conversationID string) {
cache.Delete(UnreadDetailKey(userID, conversationID)) cache.Delete(UnreadDetailKey(userID, conversationID))
} }
// InvalidateUserInfo 失效用户信息缓存
func InvalidateUserInfo(cache Cache, userID string) {
cache.Delete(UserInfoKey(userID))
cache.Delete(UserMeKey(userID))
}
// ============================================================ // ============================================================
// 消息缓存 Key 生成函数 // 消息缓存 Key 生成函数
@@ -231,17 +197,9 @@ func MessageIdempotentKey(senderID, clientMsgID string) string {
return fmt.Sprintf("%s:%s:%s", keyPrefixMsgIdempotent, senderID, clientMsgID) return fmt.Sprintf("%s:%s:%s", keyPrefixMsgIdempotent, senderID, clientMsgID)
} }
// PushDedupKey 推送去重键,防止同一消息重复推送给同一用户
func PushDedupKey(userID, messageID string) string {
return fmt.Sprintf("push_dedup:%s:%s", userID, messageID)
}
// UserReadSeqKey 用户已读位置缓存键OpenIM 风格 SEQ_USER_READ:{convID}:{userID} // UserReadSeqKey 用户已读位置缓存键OpenIM 风格 SEQ_USER_READ:{convID}:{userID}
func UserReadSeqKey(convID, userID string) string { func UserReadSeqKey(convID, userID string) string {
return fmt.Sprintf("%s:%s:%s", PrefixUserReadSeq, convID, userID) return fmt.Sprintf("%s:%s:%s", PrefixUserReadSeq, convID, userID)
} }
// ConvMaxSeqKey 会话最大 seq 缓存键
func ConvMaxSeqKey(convID string) string {
return fmt.Sprintf("%s:%s", keyPrefixMsgSeq, convID)
}

View File

@@ -2,13 +2,11 @@
import ( import (
"context" "context"
"encoding/json"
"strconv" "strconv"
"sync" "sync"
"time" "time"
redislib "github.com/redis/go-redis/v9" redislib "github.com/redis/go-redis/v9"
"go.uber.org/zap"
redisPkg "with_you/internal/pkg/redis" redisPkg "with_you/internal/pkg/redis"
) )
@@ -412,59 +410,6 @@ func (c *LayeredCache) GetRedisClient() *redisPkg.Client {
return c.redis.GetRedisClient() return c.redis.GetRedisClient()
} }
func GetTypedLayered[T any](c *LayeredCache, key string) (T, bool) {
var zero T
if !c.enabled {
return zero, false
}
raw, ok := c.Get(key)
if !ok {
return zero, false
}
if typed, ok := raw.(T); ok {
return typed, true
}
if str, ok := raw.(string); ok {
var out T
if err := json.Unmarshal([]byte(str), &out); err != nil {
return zero, false
}
return out, true
}
if data, err := json.Marshal(raw); err == nil {
var out T
if err := json.Unmarshal(data, &out); err != nil {
return zero, false
}
return out, true
}
return zero, false
}
func GetOrLoadLayered[T any](
c *LayeredCache,
key string,
ttl time.Duration,
loader func() (T, error),
) (T, error) {
if cached, ok := GetTypedLayered[T](c, key); ok {
return cached, nil
}
loaded, err := loader()
if err != nil {
var zero T
return zero, err
}
c.Set(key, loaded, ttl)
return loaded, nil
}
func (c *LayeredCache) InvalidateUserCache(userID uint) { func (c *LayeredCache) InvalidateUserCache(userID uint) {
c.DeleteByPrefix(cacheKeyPrefixUser + ":") c.DeleteByPrefix(cacheKeyPrefixUser + ":")
@@ -509,15 +454,3 @@ func buildGroupMembersKey(groupID uint) string {
} }
var _ Cache = (*LayeredCache)(nil) var _ Cache = (*LayeredCache)(nil)
func (c *LayeredCache) logStats() {
stats := c.GetStats()
zap.L().Debug("LayeredCache stats",
zap.Int64("local_hits", stats.LocalHits),
zap.Int64("redis_hits", stats.RedisHits),
zap.Int64("misses", stats.Misses),
zap.Int64("sets", stats.Sets),
zap.Int64("invalidates", stats.Invalidates),
zap.Float64("hit_rate", c.GetHitRate()),
)
}

View File

@@ -14,25 +14,6 @@ type cacheMetrics struct {
// metrics 全局缓存指标实例 // metrics 全局缓存指标实例
var metrics cacheMetrics var metrics cacheMetrics
// MetricsSnapshot 指标快照
type MetricsSnapshot struct {
Hit int64
Miss int64
DecodeError int64
SetError int64
Invalidate int64
}
// GetMetricsSnapshot 获取当前指标快照
func GetMetricsSnapshot() MetricsSnapshot {
return MetricsSnapshot{
Hit: metrics.hit.Load(),
Miss: metrics.miss.Load(),
DecodeError: metrics.decodeError.Load(),
SetError: metrics.setError.Load(),
Invalidate: metrics.invalidate.Load(),
}
}
// recordHit 记录缓存命中 // recordHit 记录缓存命中
func recordHit() { func recordHit() {

View File

@@ -143,49 +143,3 @@ type CallConfigResponse struct {
ICEServers []ICEServerConfig `json:"ice_servers"` ICEServers []ICEServerConfig `json:"ice_servers"`
} }
// ==================== Converter Functions ====================
// ConvertCallSessionToResponse 转换通话会话为响应格式
func ConvertCallSessionToResponse(call *model.CallSession, caller *model.User, participants []*model.User) *CallSessionResponse {
resp := &CallSessionResponse{
ID: call.ID,
ConversationID: call.ConversationID,
CallerID: call.CallerID,
CallType: call.CallType,
Status: call.Status,
StartedAt: call.StartedAt,
EndedAt: call.EndedAt,
Duration: call.Duration,
CreatedAt: call.CreatedAt,
}
if call.GroupID != nil {
resp.GroupID = *call.GroupID
}
if caller != nil {
resp.Caller = ConvertUserToResponse(caller)
}
// 转换参与者
resp.Participants = make([]CallParticipantResponse, 0, len(call.Participants))
userMap := make(map[string]*model.User)
for _, u := range participants {
userMap[u.ID] = u
}
for _, p := range call.Participants {
pr := CallParticipantResponse{
UserID: p.UserID,
Status: p.Status,
JoinedAt: p.JoinedAt,
LeftAt: p.LeftAt,
}
if u, ok := userMap[p.UserID]; ok {
pr.User = ConvertUserToResponse(u)
}
resp.Participants = append(resp.Participants, pr)
}
return resp
}

View File

@@ -50,26 +50,6 @@ func GroupMemberToResponse(member *model.GroupMember) *GroupMemberResponse {
} }
} }
// GroupMemberToResponseWithUser 将GroupMember转换为GroupMemberResponse包含用户信息
func GroupMemberToResponseWithUser(member *model.GroupMember, user *model.User) *GroupMemberResponse {
if member == nil {
return nil
}
resp := GroupMemberToResponse(member)
if user != nil {
resp.User = ConvertUserToResponse(user)
}
return resp
}
// GroupMembersToResponse 将GroupMember列表转换为GroupMemberResponse列表
func GroupMembersToResponse(members []model.GroupMember) []*GroupMemberResponse {
result := make([]*GroupMemberResponse, 0, len(members))
for i := range members {
result = append(result, GroupMemberToResponse(&members[i]))
}
return result
}
// GroupAnnouncementToResponse 将GroupAnnouncement转换为GroupAnnouncementResponse // GroupAnnouncementToResponse 将GroupAnnouncement转换为GroupAnnouncementResponse
func GroupAnnouncementToResponse(announcement *model.GroupAnnouncement) *GroupAnnouncementResponse { func GroupAnnouncementToResponse(announcement *model.GroupAnnouncement) *GroupAnnouncementResponse {

View File

@@ -107,14 +107,6 @@ func ConvertMessagesToResponse(messages []*model.Message) []*MessageResponse {
return result return result
} }
// ConvertConversationsToResponse 将Conversation列表转换为响应列表
func ConvertConversationsToResponse(convs []*model.Conversation) []*ConversationResponse {
result := make([]*ConversationResponse, 0, len(convs))
for _, conv := range convs {
result = append(result, ConvertConversationToResponse(conv, nil, 0, nil, false, false))
}
return result
}
// ==================== PushRecord 转换 ==================== // ==================== PushRecord 转换 ====================

View File

@@ -33,49 +33,6 @@ func ConvertNotificationsToResponse(notifications []*model.Notification) []*Noti
return result return result
} }
// ==================== SystemMessage 转换 ====================
// SystemMessageToResponse 将Message转换为SystemMessageResponse
func SystemMessageToResponse(msg *model.Message) *SystemMessageResponse {
if msg == nil {
return nil
}
// 从 segments 中提取文本内容
content := ExtractTextContentFromModel(msg.Segments)
resp := &SystemMessageResponse{
ID: msg.ID,
SenderID: msg.SenderID,
ReceiverID: "", // 系统消息的接收者需要从上下文获取
Content: content,
Category: string(msg.Category),
SystemType: string(msg.SystemType),
CreatedAt: msg.CreatedAt,
}
if msg.ExtraData != nil {
resp.ExtraData = map[string]any{
"actor_id": msg.ExtraData.ActorID,
"actor_name": msg.ExtraData.ActorName,
"avatar_url": msg.ExtraData.AvatarURL,
"target_id": msg.ExtraData.TargetID,
"target_type": msg.ExtraData.TargetType,
"action_url": msg.ExtraData.ActionURL,
"action_time": msg.ExtraData.ActionTime,
}
}
return resp
}
// SystemMessagesToResponse 将Message列表转换为SystemMessageResponse列表
func SystemMessagesToResponse(messages []*model.Message) []*SystemMessageResponse {
result := make([]*SystemMessageResponse, 0, len(messages))
for _, msg := range messages {
result = append(result, SystemMessageToResponse(msg))
}
return result
}
// SystemNotificationToResponse 将SystemNotification转换为SystemMessageResponse // SystemNotificationToResponse 将SystemNotification转换为SystemMessageResponse
func SystemNotificationToResponse(n *model.SystemNotification) *SystemMessageResponse { func SystemNotificationToResponse(n *model.SystemNotification) *SystemMessageResponse {
if n == nil { if n == nil {
@@ -118,11 +75,3 @@ func SystemNotificationToResponse(n *model.SystemNotification) *SystemMessageRes
return resp return resp
} }
// SystemNotificationsToResponse 将SystemNotification列表转换为SystemMessageResponse列表
func SystemNotificationsToResponse(notifications []*model.SystemNotification) []*SystemMessageResponse {
result := make([]*SystemMessageResponse, 0, len(notifications))
for _, n := range notifications {
result = append(result, SystemNotificationToResponse(n))
}
return result
}

View File

@@ -212,14 +212,6 @@ func ConvertCommentToResponse(comment *model.Comment, isLiked bool) *CommentResp
} }
} }
// ConvertCommentsToResponse 将Comment列表转换为响应列表
func ConvertCommentsToResponse(comments []*model.Comment, isLiked bool) []*CommentResponse {
result := make([]*CommentResponse, 0, len(comments))
for _, comment := range comments {
result = append(result, ConvertCommentToResponse(comment, isLiked))
}
return result
}
// IsLikedChecker 点赞状态检查器接口 // IsLikedChecker 点赞状态检查器接口
type IsLikedChecker interface { type IsLikedChecker interface {

View File

@@ -108,11 +108,6 @@ type AdminBatchHandleResponse struct {
// ==================== 转换函数 ==================== // ==================== 转换函数 ====================
func baseAdminReportFields(report *model.Report) (string, string) {
reason := string(report.Reason)
status := string(report.Status)
return reason, status
}
// ConvertReportToResponse 将 Report 模型转换为响应 // ConvertReportToResponse 将 Report 模型转换为响应
func ConvertReportToResponse(report *model.Report) *ReportResponse { func ConvertReportToResponse(report *model.Report) *ReportResponse {

View File

@@ -8,15 +8,6 @@ import (
"with_you/internal/model" "with_you/internal/model"
) )
// ParseSegmentData 解析Segment数据到目标结构体
func ParseSegmentData(segment model.MessageSegment, target any) error {
dataBytes, err := json.Marshal(segment.Data)
if err != nil {
return err
}
return json.Unmarshal(dataBytes, target)
}
// NewTextSegment 创建文本Segment // NewTextSegment 创建文本Segment
func NewTextSegment(content string) model.MessageSegment { func NewTextSegment(content string) model.MessageSegment {
return model.MessageSegment{ return model.MessageSegment{
@@ -25,137 +16,6 @@ func NewTextSegment(content string) model.MessageSegment {
} }
} }
// NewImageSegment 创建图片Segment
func NewImageSegment(url string, width, height int, thumbnailURL string) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeImage),
Data: map[string]any{
"url": url,
"width": width,
"height": height,
"thumbnail_url": thumbnailURL,
},
}
}
// NewImageSegmentWithSize 创建带文件大小的图片Segment
func NewImageSegmentWithSize(url string, width, height int, thumbnailURL string, fileSize int64) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeImage),
Data: map[string]any{
"url": url,
"width": width,
"height": height,
"thumbnail_url": thumbnailURL,
"file_size": fileSize,
},
}
}
// NewVoiceSegment 创建语音Segment
func NewVoiceSegment(url string, duration int, fileSize int64) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeVoice),
Data: map[string]any{
"url": url,
"duration": duration,
"file_size": fileSize,
},
}
}
// NewVideoSegment 创建视频Segment
func NewVideoSegment(url string, width, height, duration int, thumbnailURL string, fileSize int64) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeVideo),
Data: map[string]any{
"url": url,
"width": width,
"height": height,
"duration": duration,
"thumbnail_url": thumbnailURL,
"file_size": fileSize,
},
}
}
// NewFileSegment 创建文件Segment
func NewFileSegment(url, name string, size int64, mimeType string) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeFile),
Data: map[string]any{
"url": url,
"name": name,
"size": size,
"mime_type": mimeType,
},
}
}
// NewAtSegment 创建@Segment只存储 user_id昵称由前端根据群成员列表实时解析
func NewAtSegment(userID string) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeAt),
Data: map[string]any{
"user_id": userID,
},
}
}
// NewAtAllSegment 创建@所有人Segment
func NewAtAllSegment() model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeAt),
Data: map[string]any{
"user_id": "all",
},
}
}
// NewReplySegment 创建回复Segment
func NewReplySegment(messageID string) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeReply),
Data: map[string]any{"id": messageID},
}
}
// NewFaceSegment 创建表情Segment
func NewFaceSegment(id int, name, url string) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeFace),
Data: map[string]any{
"id": id,
"name": name,
"url": url,
},
}
}
// NewLinkSegment 创建链接Segment
func NewLinkSegment(url, title, description, image string) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypeLink),
Data: map[string]any{
"url": url,
"title": title,
"description": description,
"image": image,
},
}
}
// NewPostRefSegment 创建帖子内链Segment
func NewPostRefSegment(postID, title string) model.MessageSegment {
return model.MessageSegment{
Type: string(SegmentTypePostRef),
Data: map[string]any{
"post_id": postID,
"title": title,
},
}
}
// ExtractVoteSegmentData 从消息链中提取投票Segment数据 // ExtractVoteSegmentData 从消息链中提取投票Segment数据
func ExtractVoteSegmentData(segments model.MessageSegments) *VoteSegmentData { func ExtractVoteSegmentData(segments model.MessageSegments) *VoteSegmentData {
for _, segment := range segments { for _, segment := range segments {
@@ -179,22 +39,6 @@ func HasVoteSegment(segments model.MessageSegments) bool {
return HasSegmentType(segments, SegmentTypeVote) return HasSegmentType(segments, SegmentTypeVote)
} }
// ExtractTextContentFromJSON 从JSON格式的segments中提取纯文本内容
// 用于从数据库读取的 []byte 格式的 segments
// 已废弃:现在数据库直接存储 model.MessageSegments 类型
func ExtractTextContentFromJSON(segmentsJSON []byte) string {
if len(segmentsJSON) == 0 {
return ""
}
var segments model.MessageSegments
if err := json.Unmarshal(segmentsJSON, &segments); err != nil {
return ""
}
return ExtractTextContentFromModel(segments)
}
// ExtractTextContentFromModel 从 model.MessageSegments 中提取纯文本内容 // ExtractTextContentFromModel 从 model.MessageSegments 中提取纯文本内容
func ExtractTextContentFromModel(segments model.MessageSegments) string { func ExtractTextContentFromModel(segments model.MessageSegments) string {
var result string var result string
@@ -290,72 +134,6 @@ func ExtractReferencedPosts(segments model.MessageSegments) []string {
return postIDs return postIDs
} }
// HasPostRefSegment 检查消息链中是否包含帖子引用
func HasPostRefSegment(segments model.MessageSegments) bool {
return HasSegmentType(segments, SegmentTypePostRef)
}
// IsAtAll 检查消息是否@了所有人
func IsAtAll(segments model.MessageSegments) bool {
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
})
}
// GetReplyMessageID 从消息链中获取被回复的消息ID
// 如果没有回复segment返回空字符串
func GetReplyMessageID(segments model.MessageSegments) string {
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 ""
}
// BuildSegmentsFromContent 从旧版content构建segments
// 用于兼容旧版本消息
func BuildSegmentsFromContent(contentType, content string, mediaURL *string) model.MessageSegments {
var segments model.MessageSegments
switch contentType {
case "text":
segments = append(segments, NewTextSegment(content))
case "image":
if mediaURL != nil {
segments = append(segments, NewImageSegment(*mediaURL, 0, 0, ""))
}
case "voice":
if mediaURL != nil {
segments = append(segments, NewVoiceSegment(*mediaURL, 0, 0))
}
case "video":
if mediaURL != nil {
segments = append(segments, NewVideoSegment(*mediaURL, 0, 0, 0, "", 0))
}
case "file":
if mediaURL != nil {
segments = append(segments, NewFileSegment(*mediaURL, content, 0, ""))
}
default:
// 默认当作文本处理
if content != "" {
segments = append(segments, NewTextSegment(content))
}
}
return segments
}
// HasSegmentType 检查消息链中是否包含指定类型的segment // HasSegmentType 检查消息链中是否包含指定类型的segment
func HasSegmentType(segments model.MessageSegments, segmentType SegmentType) bool { func HasSegmentType(segments model.MessageSegments, segmentType SegmentType) bool {
return slices.ContainsFunc(segments, func(segment model.MessageSegment) bool { return slices.ContainsFunc(segments, func(segment model.MessageSegment) bool {
@@ -363,71 +141,3 @@ func HasSegmentType(segments model.MessageSegments, segmentType SegmentType) boo
}) })
} }
// GetSegmentsByType 获取消息链中所有指定类型的segment
func GetSegmentsByType(segments model.MessageSegments, segmentType SegmentType) []model.MessageSegment {
var result []model.MessageSegment
for _, segment := range segments {
if segment.Type == string(segmentType) {
result = append(result, segment)
}
}
return result
}
// GetFirstImageURL 获取消息链中第一张图片的URL
// 如果没有图片,返回空字符串
func GetFirstImageURL(segments model.MessageSegments) string {
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 ""
}
// GetFirstMediaURL 获取消息链中第一个媒体文件的URL图片/视频/语音/文件)
// 用于兼容旧版本API
func GetFirstMediaURL(segments model.MessageSegments) string {
idx := slices.IndexFunc(segments, func(segment model.MessageSegment) bool {
switch segment.Type {
case string(SegmentTypeImage), string(SegmentTypeVideo), string(SegmentTypeVoice), string(SegmentTypeFile):
return true
}
return false
})
if idx == -1 {
return ""
}
if url, ok := segments[idx].Data["url"].(string); ok {
return url
}
return ""
}
// DetermineContentType 从消息链推断消息类型(用于兼容旧版本)
func DetermineContentType(segments model.MessageSegments) string {
if len(segments) == 0 {
return "text"
}
// 优先检查媒体类型
for _, segment := range segments {
switch segment.Type {
case string(SegmentTypeImage):
return "image"
case string(SegmentTypeVideo):
return "video"
case string(SegmentTypeVoice):
return "voice"
case string(SegmentTypeFile):
return "file"
}
}
// 默认返回text
return "text"
}

View File

@@ -66,6 +66,3 @@ func ConvertTradeItemsToResponse(items []*model.TradeItem, favoriteMap map[strin
return result return result
} }
func ConvertTradeItemToDetailResponse(item *model.TradeItem, isFavorited bool) *TradeItemResponse {
return ConvertTradeItemToResponse(item, isFavorited)
}

View File

@@ -151,16 +151,6 @@ func Wrap(err error, appErr *AppError) *AppError {
} }
} }
// New 创建新错误
func New(code, message string) *AppError {
return &AppError{Code: code, Message: message}
}
// Is 判断是否为特定错误
func Is(err, target error) bool {
return errors.Is(err, target)
}
// As 将错误转换为 AppError // As 将错误转换为 AppError
func As(err error) (*AppError, bool) { func As(err error) (*AppError, bool) {
var appErr *AppError var appErr *AppError

View File

@@ -1,7 +1,6 @@
package runner package runner
import ( import (
"context"
"fmt" "fmt"
"net" "net"
"time" "time"
@@ -12,7 +11,6 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials" "google.golang.org/grpc/credentials"
"google.golang.org/grpc/credentials/insecure"
) )
// Server gRPC 服务器 // Server gRPC 服务器
@@ -23,19 +21,6 @@ type Server struct {
grpcSrv *grpc.Server grpcSrv *grpc.Server
} }
// NewServer 创建新的 gRPC 服务器(内部构造 Hub + TaskManager
func NewServer(cfg *config.GRPCConfig, logger *zap.Logger) *Server {
taskManager := NewTaskManager(nil, 60*time.Second, logger)
hub := NewRunnerHub(taskManager, logger)
taskManager.SetDispatcher(hub)
return &Server{
cfg: cfg,
logger: logger,
hub: hub,
}
}
// NewServerWithDeps 使用外部依赖创建 gRPC 服务器Wire DI 使用) // NewServerWithDeps 使用外部依赖创建 gRPC 服务器Wire DI 使用)
func NewServerWithDeps(cfg *config.GRPCConfig, logger *zap.Logger, hub *RunnerHub) *Server { func NewServerWithDeps(cfg *config.GRPCConfig, logger *zap.Logger, hub *RunnerHub) *Server {
return &Server{ return &Server{
@@ -111,43 +96,3 @@ func (s *Server) Stop() {
} }
} }
// GetHub 获取 RunnerHub
func (s *Server) GetHub() *RunnerHub {
return s.hub
}
// GetTaskManager 获取 TaskManager
func (s *Server) GetTaskManager() *TaskManager {
return s.hub.taskManager
}
// Shutdown 优雅关闭
func (s *Server) Shutdown(ctx context.Context) error {
if s.grpcSrv == nil {
return nil
}
done := make(chan struct{})
go func() {
s.grpcSrv.GracefulStop()
close(done)
}()
select {
case <-done:
s.logger.Info("gRPC server stopped gracefully")
return nil
case <-ctx.Done():
s.logger.Warn("gRPC server shutdown timeout, forcing stop")
s.grpcSrv.Stop()
return ctx.Err()
}
}
// GetClientCredentials 获取客户端凭证配置
func GetClientCredentials(cfg *config.GRPCConfig) (credentials.TransportCredentials, error) {
if cfg.TLSEnabled {
return credentials.NewClientTLSFromFile(cfg.TLSCertFile, "")
}
return insecure.NewCredentials(), nil
}

View File

@@ -1,7 +1,6 @@
package handler package handler
import ( import (
"strconv"
"with_you/internal/dto" "with_you/internal/dto"
"with_you/internal/pkg/response" "with_you/internal/pkg/response"
"with_you/internal/service" "with_you/internal/service"
@@ -138,15 +137,3 @@ func (h *AdminReportHandler) BatchHandle(c *gin.Context) {
response.Success(c, result) response.Success(c, result)
} }
// parsePageParams 解析分页参数
func parsePageParams(c *gin.Context) (int, int) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page < 1 {
page = 1
}
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
return page, pageSize
}

View File

@@ -66,23 +66,6 @@ func CasbinAuth(casbinService service.CasbinService) gin.HandlerFunc {
} }
} }
// OptionalCasbinAuth 可选的 Casbin 权限检查
// 不阻止请求,但会设置权限标志
func OptionalCasbinAuth(casbinService service.CasbinService) gin.HandlerFunc {
return func(c *gin.Context) {
userID, exists := c.Get("user_id")
if exists {
path := c.Request.URL.Path
method := c.Request.Method
allowed, err := casbinService.EnforceForUser(c.Request.Context(), userID.(string), path, method)
if err == nil {
c.Set("permission_granted", allowed)
}
}
c.Next()
}
}
// RequireRole 要求特定角色的中间件 // RequireRole 要求特定角色的中间件
func RequireRole(casbinService service.CasbinService, requiredRoles ...string) gin.HandlerFunc { func RequireRole(casbinService service.CasbinService, requiredRoles ...string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {

View File

@@ -1,49 +0,0 @@
package middleware
import (
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
// Logger 日志中间件
func Logger(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
path := c.Request.URL.Path
c.Next()
latency := time.Since(start)
statusCode := c.Writer.Status()
logger.Info("request",
zap.String("method", c.Request.Method),
zap.String("path", path),
zap.Int("status", statusCode),
zap.Duration("latency", latency),
zap.String("ip", c.ClientIP()),
zap.String("user-agent", c.Request.UserAgent()),
)
}
}
// Recovery 恢复中间件
func Recovery(logger *zap.Logger) gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
logger.Error("panic recovered",
zap.Any("error", err),
)
c.JSON(500, gin.H{
"code": 500,
"message": "internal server error",
})
c.Abort()
}
}()
c.Next()
}
}

View File

@@ -75,27 +75,6 @@ func (rl *RateLimiter) isAllowed(key string) bool {
return true return true
} }
func (rl *RateLimiter) count(key string) int {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
times := rl.requests[key]
var valid []time.Time
for _, t := range times {
if now.Sub(t) < rl.window {
valid = append(valid, t)
}
}
rl.requests[key] = valid
return len(valid)
}
func RateLimit(requestsPerMinute int) gin.HandlerFunc {
return RateLimitWithDuration(requestsPerMinute, time.Minute)
}
func RateLimitWithDuration(limit int, window time.Duration) gin.HandlerFunc { func RateLimitWithDuration(limit int, window time.Duration) gin.HandlerFunc {
limiter := NewRateLimiter(limit, window) limiter := NewRateLimiter(limit, window)
@@ -115,32 +94,6 @@ func RateLimitWithDuration(limit int, window time.Duration) gin.HandlerFunc {
} }
} }
func RateLimitWithKey(requestsPerMinute int, keyFunc func(c *gin.Context) string) gin.HandlerFunc {
return RateLimitWithDurationAndKey(requestsPerMinute, time.Minute, keyFunc)
}
func RateLimitWithDurationAndKey(limit int, window time.Duration, keyFunc func(c *gin.Context) string) gin.HandlerFunc {
limiter := NewRateLimiter(limit, window)
return func(c *gin.Context) {
key := keyFunc(c)
if key == "" {
key = c.ClientIP()
}
if !limiter.isAllowed(key) {
c.JSON(http.StatusTooManyRequests, gin.H{
"code": 429,
"message": "too many requests, please try again later",
})
c.Abort()
return
}
c.Next()
}
}
// ============================================================ // ============================================================
// IP 封禁与登录失败追踪 // IP 封禁与登录失败追踪
// ============================================================ // ============================================================

View File

@@ -4,7 +4,6 @@ import (
"strconv" "strconv"
"github.com/google/uuid" "github.com/google/uuid"
"gorm.io/gorm"
"with_you/internal/pkg/utils" "with_you/internal/pkg/utils"
) )
@@ -37,20 +36,3 @@ func SetSnowflakeInt64ID(id *int64) error {
return nil return nil
} }
func GenerateUUID() string {
return uuid.New().String()
}
func GenerateSnowflakeString() (string, error) {
id, err := utils.GetSnowflake().GenerateID()
if err != nil {
return "", err
}
return strconv.FormatInt(id, 10), nil
}
func GenerateSnowflakeInt64() (int64, error) {
return utils.GetSnowflake().GenerateID()
}
func nop(_ *gorm.DB) error { return nil }

View File

@@ -415,9 +415,3 @@ func seedMaterialSubjects(db *gorm.DB) error {
return nil return nil
} }
// CloseDB 关闭数据库连接
func CloseDB(db *gorm.DB) {
if sqlDB, err := db.DB(); err == nil {
sqlDB.Close()
}
}

View File

@@ -303,76 +303,6 @@ func (m *Message) IsInteractionNotification() bool {
} }
} }
// BatchDecryptMessages 批量并行解密消息
// 用于批量查询后提高解密性能,比逐条调用 AfterFind 更高效
// workerCount 指定并行工作数,如果 <= 0 则使用默认值
func BatchDecryptMessages(messages []*Message, workerCount int) error {
if len(messages) == 0 {
return nil
}
encryptor := crypto.GetMessageEncryptor()
if encryptor == nil {
// 加密器未初始化,直接解析 JSON兼容模式
for _, m := range messages {
if m.decrypted || m.SegmentsEncrypted == "" {
m.decrypted = true
continue
}
if err := json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments); err != nil {
return err
}
m.decrypted = true
}
return nil
}
// 小批量直接串行处理
if len(messages) <= 10 {
for _, m := range messages {
if err := m.decryptSegments(); err != nil {
return err
}
}
return nil
}
// 并行解密
// 先收集需要解密的密文
ciphertexts := make([]string, len(messages))
for i, m := range messages {
ciphertexts[i] = m.SegmentsEncrypted
}
// 批量并行解密
plaintexts := encryptor.BatchDecrypt(ciphertexts, workerCount)
// 解析结果
for i, m := range messages {
if m.decrypted {
continue
}
if m.SegmentsEncrypted == "" {
m.decrypted = true
continue
}
plaintext := plaintexts[i]
if plaintext == nil {
// 解密失败,尝试直接解析(兼容旧数据)
if err := json.Unmarshal([]byte(m.SegmentsEncrypted), &m.Segments); err != nil {
continue // 忽略单条错误
}
} else {
if err := json.Unmarshal(plaintext, &m.Segments); err != nil {
continue // 忽略单条错误
}
}
m.decrypted = true
}
return nil
}
// BatchDecryptMessagesParallel 使用 worker pool 并行解密 // BatchDecryptMessagesParallel 使用 worker pool 并行解密
// 这是一个更高效的版本,适用于大量消息 // 这是一个更高效的版本,适用于大量消息

View File

@@ -22,15 +22,6 @@ type Config struct {
Name string // 熔断器名称,用于日志 Name string // 熔断器名称,用于日志
} }
func DefaultConfig(name string) Config {
return Config{
FailureThreshold: 5,
SuccessThreshold: 3,
Timeout: 30 * time.Second,
Name: name,
}
}
type Breaker struct { type Breaker struct {
cfg Config cfg Config
mu sync.Mutex mu sync.Mutex

View File

@@ -1,7 +1,6 @@
package crypto package crypto
import ( import (
"encoding/json"
"sync" "sync"
) )
@@ -75,35 +74,3 @@ func (e *MessageEncryptor) BatchDecrypt(ciphertexts []string, workerCount int) [
return results return results
} }
// DecryptMessageSegments 解密消息段 JSON
// 这是一个便捷方法,直接返回解析后的 MessageSegments
func (e *MessageEncryptor) DecryptMessageSegments(ciphertext string) ([]byte, error) {
if e == nil || ciphertext == "" {
return nil, nil
}
return e.Decrypt(ciphertext)
}
// TryParseMessageSegments 尝试解析消息段
// 先尝试解密,如果失败则尝试直接解析(兼容未加密的旧数据)
func TryParseMessageSegments(ciphertext string, segments any) error {
if ciphertext == "" {
return nil
}
encryptor := GetMessageEncryptor()
if encryptor == nil {
// 加密器未初始化,直接解析
return json.Unmarshal([]byte(ciphertext), segments)
}
// 尝试解密
plaintext, err := encryptor.Decrypt(ciphertext)
if err != nil {
// 解密失败,可能是未加密的旧数据,尝试直接解析
return json.Unmarshal([]byte(ciphertext), segments)
}
// 解析解密后的数据
return json.Unmarshal(plaintext, segments)
}

View File

@@ -167,7 +167,3 @@ func (e *MessageEncryptor) RotateKey(newKey string, newVersion int) error {
return nil return nil
} }
// IsInitialized 检查加密器是否已初始化
func IsInitialized() bool {
return globalEncryptor != nil
}

View File

@@ -64,19 +64,6 @@ func (b *CursorBuilder) WithPageSize(size int) *CursorBuilder {
return b return b
} }
// WithDirection 设置分页方向
func (b *CursorBuilder) WithDirection(direction Direction) *CursorBuilder {
if b.err != nil {
return b
}
if direction == "" {
b.direction = Forward
} else {
b.direction = direction
}
return b
}
// Error 返回构建过程中的错误 // Error 返回构建过程中的错误
func (b *CursorBuilder) Error() error { func (b *CursorBuilder) Error() error {
@@ -166,79 +153,11 @@ func HasMore(itemsLen int, pageSize int) bool {
return itemsLen > pageSize return itemsLen > pageSize
} }
// TrimItems 裁剪结果到实际页面大小
func TrimItems(itemsLen int, pageSize int) int {
if itemsLen > pageSize {
return pageSize
}
return itemsLen
}
// BuildResponse 从结果构建响应
// items: 结果切片(需要是切片类型)
// getSortValue: 获取排序字段值的函数
// getID: 获取ID的函数
func BuildResponse[T any](items []T, order SortOrder, pageSize int, getSortValue func(T) string, getID func(T) string) *PageResponse {
hasMore := len(items) > pageSize
// 裁剪到实际页面大小
if hasMore {
items = items[:pageSize]
}
var nextCursor, prevCursor string
// 生成下一页游标
if hasMore && len(items) > 0 {
lastItem := items[len(items)-1]
nextCursor = NewCursor(getSortValue(lastItem), getID(lastItem), order).Encode()
}
return &PageResponse{
Items: items,
NextCursor: nextCursor,
PrevCursor: prevCursor,
HasMore: hasMore,
}
}
// BuildResponseWithPrev 从结果构建响应(包含上一页游标)
func BuildResponseWithPrev[T any](items []T, order SortOrder, pageSize int, getSortValue func(T) string, getID func(T) string) *PageResponse {
hasMore := len(items) > pageSize
// 裁剪到实际页面大小
if hasMore {
items = items[:pageSize]
}
var nextCursor, prevCursor string
// 生成下一页游标
if hasMore && len(items) > 0 {
lastItem := items[len(items)-1]
nextCursor = NewCursor(getSortValue(lastItem), getID(lastItem), order).Encode()
}
// 生成上一页游标
if len(items) > 0 {
firstItem := items[0]
prevCursor = NewCursor(getSortValue(firstItem), getID(firstItem), order).Encode()
}
return &PageResponse{
Items: items,
NextCursor: nextCursor,
PrevCursor: prevCursor,
HasMore: hasMore,
}
}
// FormatTime 格式化时间为字符串(用于游标) // FormatTime 格式化时间为字符串(用于游标)
func FormatTime(t time.Time) string { func FormatTime(t time.Time) string {
return t.Format(time.RFC3339Nano) return t.Format(time.RFC3339Nano)
} }
// ParseTime 解析时间字符串(从游标)
func ParseTime(s string) (time.Time, error) {
return time.Parse(time.RFC3339Nano, s)
}

View File

@@ -99,15 +99,6 @@ type PageResponse struct {
HasMore bool `json:"has_more"` HasMore bool `json:"has_more"`
} }
// NewPageResponse 创建分页响应
func NewPageResponse(items any, nextCursor, prevCursor string, hasMore bool) *PageResponse {
return &PageResponse{
Items: items,
NextCursor: nextCursor,
PrevCursor: prevCursor,
HasMore: hasMore,
}
}
// CursorPageResult 游标分页结果(泛型版本,供内部使用) // CursorPageResult 游标分页结果(泛型版本,供内部使用)
type CursorPageResult[T any] struct { type CursorPageResult[T any] struct {

View File

@@ -187,11 +187,6 @@ func WithAsync() HookOption {
} }
} }
func WithTimeout(timeout time.Duration) HookOption {
return func(h *Hook) {
h.Timeout = timeout
}
}
func (m *Manager) Trigger(ctx context.Context, hookType HookType, userID uint, data any) error { func (m *Manager) Trigger(ctx context.Context, hookType HookType, userID uint, data any) error {
return m.TriggerWithMetadata(ctx, hookType, userID, data, nil) return m.TriggerWithMetadata(ctx, hookType, userID, data, nil)

View File

@@ -1,8 +1,6 @@
package hook package hook
import ( import (
"context"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -196,144 +194,3 @@ func (b *BuiltinHooks) registerLoggingHooks() {
func (b *BuiltinHooks) registerCacheInvalidationHooks() { func (b *BuiltinHooks) registerCacheInvalidationHooks() {
} }
type HookTriggerHelper struct {
manager *Manager
}
func NewHookTriggerHelper(manager *Manager) *HookTriggerHelper {
return &HookTriggerHelper{manager: manager}
}
func (h *HookTriggerHelper) TriggerPostCreated(ctx context.Context, userID uint, data *PostHookData) {
h.manager.Trigger(ctx, HookPostCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerPostUpdated(ctx context.Context, userID uint, data *PostHookData) {
h.manager.Trigger(ctx, HookPostUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerPostDeleted(ctx context.Context, userID uint, postID uint) {
h.manager.Trigger(ctx, HookPostDeleted, userID, postID)
}
func (h *HookTriggerHelper) TriggerCommentCreated(ctx context.Context, userID uint, data *CommentHookData) {
h.manager.Trigger(ctx, HookCommentCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerCommentDeleted(ctx context.Context, userID uint, commentID uint) {
h.manager.Trigger(ctx, HookCommentDeleted, userID, commentID)
}
func (h *HookTriggerHelper) TriggerUserCreated(ctx context.Context, data *UserHookData) {
h.manager.Trigger(ctx, HookUserCreated, data.UserID, data)
}
func (h *HookTriggerHelper) TriggerUserUpdated(ctx context.Context, userID uint, data *UserHookData) {
h.manager.Trigger(ctx, HookUserUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerUserDeleted(ctx context.Context, userID uint) {
h.manager.Trigger(ctx, HookUserDeleted, userID, userID)
}
func (h *HookTriggerHelper) TriggerUserLogin(ctx context.Context, userID uint, data *UserHookData) {
h.manager.Trigger(ctx, HookUserLogin, userID, data)
}
func (h *HookTriggerHelper) TriggerUserLogout(ctx context.Context, userID uint) {
h.manager.Trigger(ctx, HookUserLogout, userID, userID)
}
func (h *HookTriggerHelper) TriggerGroupCreated(ctx context.Context, userID uint, data *GroupHookData) {
h.manager.Trigger(ctx, HookGroupCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerGroupUpdated(ctx context.Context, userID uint, data *GroupHookData) {
h.manager.Trigger(ctx, HookGroupUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerGroupDeleted(ctx context.Context, userID uint, groupID uint) {
h.manager.Trigger(ctx, HookGroupDeleted, userID, groupID)
}
func (h *HookTriggerHelper) TriggerGroupJoined(ctx context.Context, userID uint, groupID uint) {
h.manager.Trigger(ctx, HookGroupJoined, userID, groupID)
}
func (h *HookTriggerHelper) TriggerGroupLeft(ctx context.Context, userID uint, groupID uint) {
h.manager.Trigger(ctx, HookGroupLeft, userID, groupID)
}
func (h *HookTriggerHelper) TriggerMessageSent(ctx context.Context, userID uint, data *MessageHookData) {
h.manager.Trigger(ctx, HookMessageSent, userID, data)
}
func (h *HookTriggerHelper) TriggerMessageRead(ctx context.Context, userID uint, messageID uint) {
h.manager.Trigger(ctx, HookMessageRead, userID, messageID)
}
func (h *HookTriggerHelper) TriggerNotificationSent(ctx context.Context, userID uint, data *NotificationHookData) {
h.manager.Trigger(ctx, HookNotificationSent, userID, data)
}
func (h *HookTriggerHelper) TriggerVoteCreated(ctx context.Context, userID uint, data *VoteHookData) {
h.manager.Trigger(ctx, HookVoteCreated, userID, data)
}
func (h *HookTriggerHelper) TriggerVoteUpdated(ctx context.Context, userID uint, data *VoteHookData) {
h.manager.Trigger(ctx, HookVoteUpdated, userID, data)
}
func (h *HookTriggerHelper) TriggerFileUploaded(ctx context.Context, userID uint, data *FileHookData) {
h.manager.Trigger(ctx, HookFileUploaded, userID, data)
}
func (h *HookTriggerHelper) TriggerPostPreModerate(ctx context.Context, userID uint, data *PostModerateHookData, result *ModerationResult) {
h.manager.TriggerWithMetadata(ctx, HookPostPreModerate, userID, data, map[string]any{
"result": result,
})
}
func (h *HookTriggerHelper) TriggerPostModerated(ctx context.Context, userID uint, data *PostModeratedHookData) {
h.manager.Trigger(ctx, HookPostModerated, userID, data)
}
func (h *HookTriggerHelper) TriggerCommentPreModerate(ctx context.Context, userID uint, data *CommentModerateHookData, result *ModerationResult) {
h.manager.TriggerWithMetadata(ctx, HookCommentPreModerate, userID, data, map[string]any{
"result": result,
})
}
func (h *HookTriggerHelper) TriggerCommentModerated(ctx context.Context, userID uint, data *CommentModeratedHookData) {
h.manager.Trigger(ctx, HookCommentModerated, userID, data)
}
func (h *HookTriggerHelper) TriggerUserAvatarPreModerate(ctx context.Context, userID uint, data *UserAvatarModerateHookData, result *ModerationResult) {
h.manager.TriggerWithMetadata(ctx, HookUserAvatarPreModerate, userID, data, map[string]any{
"result": result,
})
}
func (h *HookTriggerHelper) TriggerUserAvatarModerated(ctx context.Context, userID uint, data *UserAvatarModeratedHookData) {
h.manager.Trigger(ctx, HookUserAvatarModerated, userID, data)
}
func (h *HookTriggerHelper) TriggerUserCoverPreModerate(ctx context.Context, userID uint, data *UserCoverModerateHookData, result *ModerationResult) {
h.manager.TriggerWithMetadata(ctx, HookUserCoverPreModerate, userID, data, map[string]any{
"result": result,
})
}
func (h *HookTriggerHelper) TriggerUserCoverModerated(ctx context.Context, userID uint, data *UserCoverModeratedHookData) {
h.manager.Trigger(ctx, HookUserCoverModerated, userID, data)
}
func (h *HookTriggerHelper) TriggerUserBioPreModerate(ctx context.Context, userID uint, data *UserBioModerateHookData, result *ModerationResult) {
h.manager.TriggerWithMetadata(ctx, HookUserBioPreModerate, userID, data, map[string]any{
"result": result,
})
}
func (h *HookTriggerHelper) TriggerUserBioModerated(ctx context.Context, userID uint, data *UserBioModeratedHookData) {
h.manager.Trigger(ctx, HookUserBioModerated, userID, data)
}

View File

@@ -111,21 +111,6 @@ func Paginated(c *gin.Context, list any, total int64, page, pageSize int) {
}) })
} }
// HandleServiceError 统一处理 Service 错误
// 如果 err 是 *apperrors.AppError返回对应的业务错误码和消息
// 如果 err 是其他错误,返回 false调用方应处理通用错误
// 返回 true 表示错误已处理false 表示需要调用方继续处理
func HandleServiceError(c *gin.Context, err error) bool {
if err == nil {
return false
}
if se, ok := apperrors.As(err); ok {
statusCode := statusCodeFromAppErrorCode(se.Code)
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
return true
}
return false
}
// HandleError 统一处理错误(带默认消息) // HandleError 统一处理错误(带默认消息)
// 如果 err 是 *apperrors.AppError返回对应的业务错误码和消息 // 如果 err 是 *apperrors.AppError返回对应的业务错误码和消息

View File

@@ -46,13 +46,3 @@ type tencentAPIResponse struct {
} `json:"Response"` } `json:"Response"`
} }
func ConfigFromAppConfig(cfg *Config) Config {
return Config{
Enabled: cfg.Enabled,
SecretID: cfg.SecretID,
SecretKey: cfg.SecretKey,
Region: cfg.Region,
BizType: cfg.BizType,
Timeout: cfg.Timeout,
}
}

View File

@@ -15,6 +15,3 @@ func GetAvatarOrDefault(username, nickname, avatar string) string {
return DefaultAvatarURL return DefaultAvatarURL
} }
func GetAvatarOrDefaultFromInfo(info AvatarInfo) string {
return GetAvatarOrDefault(info.Username, info.Nickname, info.Avatar)
}

View File

@@ -61,12 +61,6 @@ var (
globalSnowflakeErr error globalSnowflakeErr error
) )
// InitSnowflake 初始化全局雪花算法实例
// nodeID: 机器ID范围0-1023可以通过环境变量 NODE_ID 配置
func InitSnowflake(nodeID int64) error {
globalSnowflake, globalSnowflakeErr = NewSnowflake(nodeID)
return globalSnowflakeErr
}
// GetSnowflake 获取全局雪花算法实例 // GetSnowflake 获取全局雪花算法实例
// 如果未初始化,会自动使用默认配置初始化 // 如果未初始化,会自动使用默认配置初始化
@@ -191,71 +185,12 @@ func (s *Snowflake) waitNextMillis(timestamp int64) int64 {
return now return now
} }
// ParseID 解析雪花算法ID提取其中的信息
// id: 要解析的雪花算法ID
// 返回值包含时间戳、机器ID、序列号的结构体
func ParseID(id int64) *IDInfo {
// 提取序列号低12位
sequence := id & maxSequence
// 提取机器ID中间10位
nodeID := (id >> nodeIDShift) & maxNodeID
// 提取时间戳高41位
timestamp := (id >> timestampShift) + customEpoch
return &IDInfo{
Timestamp: timestamp,
NodeID: nodeID,
Sequence: sequence,
}
}
// currentTimestamp 获取当前时间戳(毫秒) // currentTimestamp 获取当前时间戳(毫秒)
func currentTimestamp() int64 { func currentTimestamp() int64 {
return time.Now().UnixNano() / 1e6 return time.Now().UnixNano() / 1e6
} }
// GetNodeID 获取当前机器ID
func (s *Snowflake) GetNodeID() int64 {
return s.nodeID
}
// GetCustomEpoch 获取自定义纪元时间
func GetCustomEpoch() int64 {
return customEpoch
}
// IDToTime 将雪花算法ID转换为生成时间
// 这是一个便捷方法,等价于 ParseID(id).Timestamp
func IDToTime(id int64) time.Time {
info := ParseID(id)
return time.Unix(0, info.Timestamp*1e6) // 毫秒转纳秒
}
// ValidateID 验证ID是否为有效的雪花算法ID
// 检查时间戳是否在合理范围内
func ValidateID(id int64) bool {
if id <= 0 {
return false
}
info := ParseID(id)
// 检查时间戳是否在合理范围内
// 不能早于纪元时间不能晚于当前时间太多允许1分钟的时钟偏差
now := currentTimestamp()
if info.Timestamp < customEpoch || info.Timestamp > now+60000 {
return false
}
// 检查机器ID和序列号是否在有效范围内
if info.NodeID < 0 || info.NodeID > maxNodeID {
return false
}
if info.Sequence < 0 || info.Sequence > maxSequence {
return false
}
return true
}

View File

@@ -2,7 +2,6 @@ package utils
import ( import (
"regexp" "regexp"
"strings"
) )
// ValidateEmail 验证邮箱 // ValidateEmail 验证邮箱
@@ -43,39 +42,6 @@ func ValidatePassword(password string) bool {
return hasUpper && hasLower && hasDigit return hasUpper && hasLower && hasDigit
} }
// ValidatePasswordWithDetail 验证密码强度并返回详细信息
func ValidatePasswordWithDetail(password string) (bool, string) {
if len(password) < 8 {
return false, "密码长度至少8位"
}
if len(password) > 50 {
return false, "密码长度不能超过50位"
}
var hasUpper, hasLower, hasDigit bool
for _, c := range password {
switch {
case 'A' <= c && c <= 'Z':
hasUpper = true
case 'a' <= c && c <= 'z':
hasLower = true
case '0' <= c && c <= '9':
hasDigit = true
}
}
if !hasUpper {
return false, "密码必须包含大写字母"
}
if !hasLower {
return false, "密码必须包含小写字母"
}
if !hasDigit {
return false, "密码必须包含数字"
}
return true, ""
}
// ValidatePhone 验证手机号 // ValidatePhone 验证手机号
func ValidatePhone(phone string) bool { func ValidatePhone(phone string) bool {
@@ -84,13 +50,3 @@ func ValidatePhone(phone string) bool {
return matched return matched
} }
// SanitizeHTML 清理HTML防止XSS攻击
func SanitizeHTML(input string) string {
input = strings.ReplaceAll(input, "&", "&amp;")
input = strings.ReplaceAll(input, "<", "&lt;")
input = strings.ReplaceAll(input, ">", "&gt;")
input = strings.ReplaceAll(input, "\"", "&quot;")
input = strings.ReplaceAll(input, "'", "&#x27;")
input = strings.ReplaceAll(input, "/", "&#x2F;")
return input
}

View File

@@ -241,20 +241,6 @@ func (r *commentRepository) GetByPostIDWithReplies(postID string, page, pageSize
return comments, total, nil return comments, total, nil
} }
// loadFlatReplies 加载评论的所有回复(扁平化,所有层级都在同一层)
func (r *commentRepository) loadFlatReplies(rootComment *model.Comment, limit int) {
var allReplies []*model.Comment
// 查询所有以该评论为根评论的回复(不包括顶级评论本身)
r.db.Where("root_id = ? AND status = ?", rootComment.ID, model.CommentStatusPublished).
Preload("User").
Order("created_at ASC").
Limit(limit).
Find(&allReplies)
rootComment.Replies = allReplies
}
// GetRepliesByRootID 根据根评论ID分页获取回复扁平化 // GetRepliesByRootID 根据根评论ID分页获取回复扁平化
func (r *commentRepository) GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error) { func (r *commentRepository) GetRepliesByRootID(rootID string, page, pageSize int) ([]*model.Comment, int64, error) {
var replies []*model.Comment var replies []*model.Comment

View File

@@ -1,114 +0,0 @@
package service
import (
"context"
"time"
"with_you/internal/model"
"with_you/internal/repository"
"go.uber.org/zap"
"gorm.io/gorm"
)
const AccountDeletionCoolDownDays = 90
type AccountCleanupService interface {
CleanupExpiredAccounts(ctx context.Context) (int64, error)
}
type accountCleanupServiceImpl struct {
db *gorm.DB
userRepo repository.UserRepository
logService *LogService
}
func NewAccountCleanupService(
db *gorm.DB,
userRepo repository.UserRepository,
logService *LogService,
) AccountCleanupService {
return &accountCleanupServiceImpl{
db: db,
userRepo: userRepo,
logService: logService,
}
}
func (s *accountCleanupServiceImpl) CleanupExpiredAccounts(ctx context.Context) (int64, error) {
cutoffTime := time.Now().AddDate(0, 0, -AccountDeletionCoolDownDays)
users, err := s.userRepo.GetPendingDeletionUsers(cutoffTime)
if err != nil {
return 0, err
}
if len(users) == 0 {
return 0, nil
}
var deletedCount int64
for _, user := range users {
if err := s.anonymizeAndDeleteUser(ctx, user); err != nil {
zap.L().Error("Failed to cleanup user",
zap.String("user_id", user.ID),
zap.Error(err),
)
continue
}
deletedCount++
}
zap.L().Info("Cleaned up expired accounts",
zap.Int64("count", deletedCount),
)
return deletedCount, nil
}
func (s *accountCleanupServiceImpl) anonymizeAndDeleteUser(ctx context.Context, user *model.User) error {
deletedUserID := "deleted_user"
return s.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Model(&model.Post{}).Where("user_id = ?", user.ID).Update("user_id", deletedUserID).Error; err != nil {
return err
}
if err := tx.Model(&model.Comment{}).Where("user_id = ?", user.ID).Update("user_id", deletedUserID).Error; err != nil {
return err
}
if err := tx.Where("follower_id = ? OR following_id = ?", user.ID, user.ID).Delete(&model.Follow{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", user.ID).Delete(&model.Favorite{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", user.ID).Delete(&model.PostLike{}).Error; err != nil {
return err
}
if err := tx.Where("user_id = ?", user.ID).Delete(&model.CommentLike{}).Error; err != nil {
return err
}
if err := tx.Unscoped().Delete(&model.User{}, "id = ?", user.ID).Error; err != nil {
return err
}
if s.logService != nil {
s.logService.OperationLog.RecordOperation(&model.OperationLog{
UserID: user.ID,
UserName: user.Username,
Operation: "account_deleted",
Module: "user",
TargetType: "user",
TargetID: user.ID,
Status: "success",
})
}
return nil
})
}

View File

@@ -197,9 +197,3 @@ func NewLogService(
} }
} }
// GetLogCleanupServiceFromApp 从应用中获取日志清理服务
func GetLogCleanupServiceFromApp(app any) LogCleanupService {
// 这里需要根据实际的app结构来获取
// 暂时返回nil实际使用时需要实现
return nil
}

View File

@@ -4,11 +4,9 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"regexp"
"strings" "strings"
"sync" "sync"
"time" "time"
"unicode/utf8"
"with_you/internal/model" "with_you/internal/model"
redisclient "with_you/internal/pkg/redis" redisclient "with_you/internal/pkg/redis"
@@ -501,51 +499,3 @@ func (s *sensitiveServiceImpl) loadFromRedis(ctx context.Context) error {
return nil return nil
} }
// ==================== 辅助函数 ====================
// ContainsSensitiveWord 快速检查文本是否包含敏感词
func ContainsSensitiveWord(text string, tree *SensitiveWordTree) bool {
if tree == nil || text == "" {
return false
}
hasSensitive, _ := tree.Check(text)
return hasSensitive
}
// FilterSensitiveWords 过滤敏感词并返回替换后的文本
func FilterSensitiveWords(text string, tree *SensitiveWordTree, repl string) string {
if tree == nil || text == "" {
return text
}
if repl == "" {
repl = "***"
}
return tree.Replace(text, repl)
}
// ValidateTextLength 验证文本长度是否合法
func ValidateTextLength(text string, minLen, maxLen int) bool {
length := utf8.RuneCountInString(text)
return length >= minLen && length <= maxLen
}
// SanitizeText 清理文本,移除多余空白字符
func SanitizeText(text string) string {
// 替换多个连续空白字符为单个空格
spaceReg := regexp.MustCompile(`\s+`)
text = spaceReg.ReplaceAllString(text, " ")
// 去除首尾空白
return strings.TrimSpace(text)
}
// ==================== 默认敏感词列表 ====================
// DefaultSensitiveWords 返回默认敏感词列表(示例)
func DefaultSensitiveWords() map[string]struct{} {
return map[string]struct{}{
// 示例敏感词,实际需要从数据库或配置加载
"测试敏感词1": {},
"测试敏感词2": {},
"测试敏感词3": {},
}
}