refactor: cleanup unused code and simplify internal packages
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:
42
internal/cache/keys.go
vendored
42
internal/cache/keys.go
vendored
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
67
internal/cache/layered_cache.go
vendored
67
internal/cache/layered_cache.go
vendored
@@ -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()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
19
internal/cache/metrics.go
vendored
19
internal/cache/metrics.go
vendored
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 转换 ====================
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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"
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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 封禁与登录失败追踪
|
||||||
// ============================================================
|
// ============================================================
|
||||||
|
|||||||
@@ -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 }
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 并行解密
|
||||||
// 这是一个更高效的版本,适用于大量消息
|
// 这是一个更高效的版本,适用于大量消息
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -167,7 +167,3 @@ func (e *MessageEncryptor) RotateKey(newKey string, newVersion int) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsInitialized 检查加密器是否已初始化
|
|
||||||
func IsInitialized() bool {
|
|
||||||
return globalEncryptor != nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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,返回对应的业务错误码和消息
|
||||||
|
|||||||
@@ -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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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, "&", "&")
|
|
||||||
input = strings.ReplaceAll(input, "<", "<")
|
|
||||||
input = strings.ReplaceAll(input, ">", ">")
|
|
||||||
input = strings.ReplaceAll(input, "\"", """)
|
|
||||||
input = strings.ReplaceAll(input, "'", "'")
|
|
||||||
input = strings.ReplaceAll(input, "/", "/")
|
|
||||||
return input
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -197,9 +197,3 @@ func NewLogService(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLogCleanupServiceFromApp 从应用中获取日志清理服务
|
|
||||||
func GetLogCleanupServiceFromApp(app any) LogCleanupService {
|
|
||||||
// 这里需要根据实际的app结构来获取
|
|
||||||
// 暂时返回nil,实际使用时需要实现
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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": {},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user