refactor: introduce Wire dependency injection and interface-based architecture
- Add Google Wire for compile-time dependency injection - Replace concrete service types with interfaces across handlers - Remove global state (DB, cache) in favor of constructor injection - Split monolithic files into focused modules: - config: separate files for each config domain - dto: converters split by domain (user, post, message, group) - cache: separate metrics.go and redis_cache.go - Introduce unified apperrors package with string-based error codes - Add transaction support with context-aware repository methods - Upgrade golang.org/x/crypto from 0.17.0 to 0.26.0
This commit is contained in:
9
go.mod
9
go.mod
@@ -7,17 +7,18 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.9.1
|
github.com/gin-gonic/gin v1.9.1
|
||||||
github.com/golang-jwt/jwt/v5 v5.2.0
|
github.com/golang-jwt/jwt/v5 v5.2.0
|
||||||
github.com/google/uuid v1.5.0
|
github.com/google/uuid v1.5.0
|
||||||
|
github.com/google/wire v0.7.0
|
||||||
github.com/gorse-io/gorse-go v0.5.0-alpha.3
|
github.com/gorse-io/gorse-go v0.5.0-alpha.3
|
||||||
github.com/minio/minio-go/v7 v7.0.66
|
github.com/minio/minio-go/v7 v7.0.66
|
||||||
github.com/redis/go-redis/v9 v9.3.1
|
github.com/redis/go-redis/v9 v9.3.1
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
go.uber.org/zap v1.26.0
|
go.uber.org/zap v1.26.0
|
||||||
golang.org/x/crypto v0.17.0
|
golang.org/x/crypto v0.26.0
|
||||||
golang.org/x/image v0.24.0
|
golang.org/x/image v0.24.0
|
||||||
|
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||||
gorm.io/driver/postgres v1.5.4
|
gorm.io/driver/postgres v1.5.4
|
||||||
gorm.io/driver/sqlite v1.5.4
|
gorm.io/driver/sqlite v1.5.4
|
||||||
gorm.io/gorm v1.25.5
|
gorm.io/gorm v1.25.5
|
||||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -75,8 +76,8 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
golang.org/x/arch v0.3.0 // indirect
|
golang.org/x/arch v0.3.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||||
golang.org/x/net v0.19.0 // indirect
|
golang.org/x/net v0.28.0 // indirect
|
||||||
golang.org/x/sys v0.15.0 // indirect
|
golang.org/x/sys v0.23.0 // indirect
|
||||||
golang.org/x/text v0.22.0 // indirect
|
golang.org/x/text v0.22.0 // indirect
|
||||||
google.golang.org/protobuf v1.31.0 // indirect
|
google.golang.org/protobuf v1.31.0 // indirect
|
||||||
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect
|
||||||
|
|||||||
20
go.sum
20
go.sum
@@ -58,13 +58,13 @@ github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
|
|||||||
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||||
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
|
||||||
github.com/gorse-io/gorse-go v0.5.0-alpha.3 h1:GR/OWzq016VyFyTTxgQWeayGahRVzB1cGFIW/AaShC4=
|
github.com/gorse-io/gorse-go v0.5.0-alpha.3 h1:GR/OWzq016VyFyTTxgQWeayGahRVzB1cGFIW/AaShC4=
|
||||||
github.com/gorse-io/gorse-go v0.5.0-alpha.3/go.mod h1:ZxmVHzZPKm5pmEIlqaRDwK0rkfTRHlfziO033XZ+RW0=
|
github.com/gorse-io/gorse-go v0.5.0-alpha.3/go.mod h1:ZxmVHzZPKm5pmEIlqaRDwK0rkfTRHlfziO033XZ+RW0=
|
||||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||||
@@ -175,20 +175,20 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
|||||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||||
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
|
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||||
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c=
|
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||||
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
|
||||||
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
|||||||
862
internal/cache/cache.go
vendored
862
internal/cache/cache.go
vendored
@@ -3,20 +3,9 @@ package cache
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math"
|
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
|
|
||||||
redisPkg "carrot_bbs/internal/pkg/redis"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Cache 缓存接口
|
// Cache 缓存接口
|
||||||
@@ -71,33 +60,10 @@ type Cache interface {
|
|||||||
Expire(ctx context.Context, key string, ttl time.Duration) error
|
Expire(ctx context.Context, key string, ttl time.Duration) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// cacheItem 缓存项(用于内存缓存降级)
|
|
||||||
type cacheItem struct {
|
|
||||||
value interface{}
|
|
||||||
expiration int64 // 过期时间戳(纳秒)
|
|
||||||
}
|
|
||||||
|
|
||||||
const nullMarkerValue = "__carrot_cache_null__"
|
const nullMarkerValue = "__carrot_cache_null__"
|
||||||
|
|
||||||
type cacheMetrics struct {
|
|
||||||
hit atomic.Int64
|
|
||||||
miss atomic.Int64
|
|
||||||
decodeError atomic.Int64
|
|
||||||
setError atomic.Int64
|
|
||||||
invalidate atomic.Int64
|
|
||||||
}
|
|
||||||
|
|
||||||
var metrics cacheMetrics
|
|
||||||
var loadLocks sync.Map
|
var loadLocks sync.Map
|
||||||
|
|
||||||
type MetricsSnapshot struct {
|
|
||||||
Hit int64
|
|
||||||
Miss int64
|
|
||||||
DecodeError int64
|
|
||||||
SetError int64
|
|
||||||
Invalidate int64
|
|
||||||
}
|
|
||||||
|
|
||||||
type Settings struct {
|
type Settings struct {
|
||||||
Enabled bool
|
Enabled bool
|
||||||
KeyPrefix string
|
KeyPrefix string
|
||||||
@@ -170,814 +136,6 @@ func normalizePrefix(prefix string) string {
|
|||||||
return settings.KeyPrefix + ":" + prefix
|
return settings.KeyPrefix + ":" + prefix
|
||||||
}
|
}
|
||||||
|
|
||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// isExpired 检查是否过期
|
|
||||||
func (item *cacheItem) isExpired() bool {
|
|
||||||
if item.expiration == 0 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return time.Now().UnixNano() > item.expiration
|
|
||||||
}
|
|
||||||
|
|
||||||
// MemoryCache 内存缓存实现(降级使用)
|
|
||||||
type MemoryCache struct {
|
|
||||||
items sync.Map
|
|
||||||
// cleanupInterval 清理过期缓存的间隔
|
|
||||||
cleanupInterval time.Duration
|
|
||||||
// stopCleanup 停止清理协程的通道
|
|
||||||
stopCleanup chan struct{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewMemoryCache 创建内存缓存
|
|
||||||
func NewMemoryCache() *MemoryCache {
|
|
||||||
c := &MemoryCache{
|
|
||||||
cleanupInterval: 1 * time.Minute,
|
|
||||||
stopCleanup: make(chan struct{}),
|
|
||||||
}
|
|
||||||
// 启动后台清理协程
|
|
||||||
go c.cleanup()
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set 设置缓存值
|
|
||||||
func (c *MemoryCache) Set(key string, value interface{}, ttl time.Duration) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
var expiration int64
|
|
||||||
if ttl > 0 {
|
|
||||||
expiration = time.Now().Add(ttl).UnixNano()
|
|
||||||
}
|
|
||||||
c.items.Store(key, &cacheItem{
|
|
||||||
value: value,
|
|
||||||
expiration: expiration,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取缓存值
|
|
||||||
func (c *MemoryCache) Get(key string) (interface{}, bool) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
val, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
item := val.(*cacheItem)
|
|
||||||
if item.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
return item.value, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete 删除缓存
|
|
||||||
func (c *MemoryCache) Delete(key string) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
metrics.invalidate.Add(1)
|
|
||||||
c.items.Delete(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteByPrefix 根据前缀删除缓存
|
|
||||||
func (c *MemoryCache) DeleteByPrefix(prefix string) {
|
|
||||||
prefix = normalizePrefix(prefix)
|
|
||||||
c.items.Range(func(key, value interface{}) bool {
|
|
||||||
if keyStr, ok := key.(string); ok {
|
|
||||||
if strings.HasPrefix(keyStr, prefix) {
|
|
||||||
metrics.invalidate.Add(1)
|
|
||||||
c.items.Delete(key)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear 清空所有缓存
|
|
||||||
func (c *MemoryCache) Clear() {
|
|
||||||
c.items.Range(func(key, value interface{}) bool {
|
|
||||||
metrics.invalidate.Add(1)
|
|
||||||
c.items.Delete(key)
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists 检查键是否存在
|
|
||||||
func (c *MemoryCache) Exists(key string) bool {
|
|
||||||
_, ok := c.Get(key)
|
|
||||||
return ok
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment 增加计数器的值
|
|
||||||
func (c *MemoryCache) Increment(key string) int64 {
|
|
||||||
return c.IncrementBy(key, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementBy 增加指定值
|
|
||||||
func (c *MemoryCache) IncrementBy(key string, value int64) int64 {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
for {
|
|
||||||
val, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
// 键不存在,创建新值
|
|
||||||
c.items.Store(key, &cacheItem{
|
|
||||||
value: value,
|
|
||||||
expiration: 0,
|
|
||||||
})
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
item := val.(*cacheItem)
|
|
||||||
if item.isExpired() {
|
|
||||||
// 已过期,创建新值
|
|
||||||
c.items.Store(key, &cacheItem{
|
|
||||||
value: value,
|
|
||||||
expiration: 0,
|
|
||||||
})
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试更新
|
|
||||||
currentValue, ok := item.value.(int64)
|
|
||||||
if !ok {
|
|
||||||
// 类型不匹配,覆盖为新值
|
|
||||||
c.items.Store(key, &cacheItem{
|
|
||||||
value: value,
|
|
||||||
expiration: item.expiration,
|
|
||||||
})
|
|
||||||
return value
|
|
||||||
}
|
|
||||||
|
|
||||||
newValue := currentValue + value
|
|
||||||
// 使用 CAS 操作确保并发安全
|
|
||||||
if c.items.CompareAndSwap(key, val, &cacheItem{
|
|
||||||
value: newValue,
|
|
||||||
expiration: item.expiration,
|
|
||||||
}) {
|
|
||||||
return newValue
|
|
||||||
}
|
|
||||||
// CAS 失败,重试
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanup 定期清理过期缓存
|
|
||||||
func (c *MemoryCache) cleanup() {
|
|
||||||
ticker := time.NewTicker(c.cleanupInterval)
|
|
||||||
defer ticker.Stop()
|
|
||||||
|
|
||||||
for {
|
|
||||||
select {
|
|
||||||
case <-ticker.C:
|
|
||||||
c.cleanExpired()
|
|
||||||
case <-c.stopCleanup:
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// cleanExpired 清理过期缓存
|
|
||||||
func (c *MemoryCache) cleanExpired() {
|
|
||||||
count := 0
|
|
||||||
c.items.Range(func(key, value interface{}) bool {
|
|
||||||
item := value.(*cacheItem)
|
|
||||||
if item.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
count++
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
if count > 0 {
|
|
||||||
log.Printf("[Cache] Cleaned %d expired items", count)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop 停止缓存清理协程
|
|
||||||
func (c *MemoryCache) Stop() {
|
|
||||||
close(c.stopCleanup)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MemoryCache Hash 操作 ====================
|
|
||||||
|
|
||||||
// hashItem Hash 存储项
|
|
||||||
type hashItem struct {
|
|
||||||
fields sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
// HSet 设置 Hash 字段
|
|
||||||
func (c *MemoryCache) HSet(ctx context.Context, key string, field string, value interface{}) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, _ := c.items.Load(key)
|
|
||||||
var h *hashItem
|
|
||||||
if item == nil {
|
|
||||||
h = &hashItem{}
|
|
||||||
c.items.Store(key, &cacheItem{value: h, expiration: 0})
|
|
||||||
} else {
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
h = &hashItem{}
|
|
||||||
c.items.Store(key, &cacheItem{value: h, expiration: 0})
|
|
||||||
} else {
|
|
||||||
h = ci.value.(*hashItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
h.fields.Store(field, value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMSet 批量设置 Hash 字段
|
|
||||||
func (c *MemoryCache) HMSet(ctx context.Context, key string, values map[string]interface{}) error {
|
|
||||||
for field, value := range values {
|
|
||||||
if err := c.HSet(ctx, key, field, value); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HGet 获取 Hash 字段值
|
|
||||||
func (c *MemoryCache) HGet(ctx context.Context, key string, field string) (string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("key not found")
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return "", fmt.Errorf("key not found")
|
|
||||||
}
|
|
||||||
h, ok := ci.value.(*hashItem)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("key is not a hash")
|
|
||||||
}
|
|
||||||
val, ok := h.fields.Load(field)
|
|
||||||
if !ok {
|
|
||||||
return "", fmt.Errorf("field not found")
|
|
||||||
}
|
|
||||||
switch v := val.(type) {
|
|
||||||
case string:
|
|
||||||
return v, nil
|
|
||||||
case []byte:
|
|
||||||
return string(v), nil
|
|
||||||
default:
|
|
||||||
data, _ := json.Marshal(v)
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMGet 批量获取 Hash 字段值
|
|
||||||
func (c *MemoryCache) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
|
|
||||||
result := make([]interface{}, len(fields))
|
|
||||||
for i, field := range fields {
|
|
||||||
val, err := c.HGet(ctx, key, field)
|
|
||||||
if err != nil {
|
|
||||||
result[i] = nil
|
|
||||||
} else {
|
|
||||||
result[i] = val
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HGetAll 获取 Hash 所有字段
|
|
||||||
func (c *MemoryCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("key not found")
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return nil, fmt.Errorf("key not found")
|
|
||||||
}
|
|
||||||
h, ok := ci.value.(*hashItem)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("key is not a hash")
|
|
||||||
}
|
|
||||||
result := make(map[string]string)
|
|
||||||
h.fields.Range(func(k, v interface{}) bool {
|
|
||||||
keyStr := k.(string)
|
|
||||||
switch val := v.(type) {
|
|
||||||
case string:
|
|
||||||
result[keyStr] = val
|
|
||||||
case []byte:
|
|
||||||
result[keyStr] = string(val)
|
|
||||||
default:
|
|
||||||
data, _ := json.Marshal(val)
|
|
||||||
result[keyStr] = string(data)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// HDel 删除 Hash 字段
|
|
||||||
func (c *MemoryCache) HDel(ctx context.Context, key string, fields ...string) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
h, ok := ci.value.(*hashItem)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, field := range fields {
|
|
||||||
h.fields.Delete(field)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MemoryCache Sorted Set 操作 ====================
|
|
||||||
|
|
||||||
// zItem Sorted Set 成员
|
|
||||||
type zItem struct {
|
|
||||||
score float64
|
|
||||||
member string
|
|
||||||
}
|
|
||||||
|
|
||||||
// zsetItem Sorted Set 存储项
|
|
||||||
type zsetItem struct {
|
|
||||||
members sync.Map // member -> *zItem
|
|
||||||
byScore *sortedSlice // 按分数排序的切片
|
|
||||||
}
|
|
||||||
|
|
||||||
// sortedSlice 简单的排序切片实现
|
|
||||||
type sortedSlice struct {
|
|
||||||
items []*zItem
|
|
||||||
mu sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZAdd 添加 Sorted Set 成员
|
|
||||||
func (c *MemoryCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, _ := c.items.Load(key)
|
|
||||||
var z *zsetItem
|
|
||||||
if item == nil {
|
|
||||||
z = &zsetItem{byScore: &sortedSlice{}}
|
|
||||||
c.items.Store(key, &cacheItem{value: z, expiration: 0})
|
|
||||||
} else {
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
z = &zsetItem{byScore: &sortedSlice{}}
|
|
||||||
c.items.Store(key, &cacheItem{value: z, expiration: 0})
|
|
||||||
} else {
|
|
||||||
z = ci.value.(*zsetItem)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
z.members.Store(member, &zItem{score: score, member: member})
|
|
||||||
z.byScore.mu.Lock()
|
|
||||||
// 简单实现:重新构建排序切片
|
|
||||||
z.byScore.items = nil
|
|
||||||
z.members.Range(func(k, v interface{}) bool {
|
|
||||||
z.byScore.items = append(z.byScore.items, v.(*zItem))
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
// 按分数排序
|
|
||||||
sort.Slice(z.byScore.items, func(i, j int) bool {
|
|
||||||
return z.byScore.items[i].score < z.byScore.items[j].score
|
|
||||||
})
|
|
||||||
z.byScore.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZRangeByScore 按分数范围获取成员(升序)
|
|
||||||
func (c *MemoryCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
z, ok := ci.value.(*zsetItem)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("key is not a sorted set")
|
|
||||||
}
|
|
||||||
|
|
||||||
minScore, _ := strconv.ParseFloat(min, 64)
|
|
||||||
maxScore, _ := strconv.ParseFloat(max, 64)
|
|
||||||
if min == "-inf" {
|
|
||||||
minScore = math.Inf(-1)
|
|
||||||
}
|
|
||||||
if max == "+inf" {
|
|
||||||
maxScore = math.Inf(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
z.byScore.mu.RLock()
|
|
||||||
defer z.byScore.mu.RUnlock()
|
|
||||||
|
|
||||||
var result []string
|
|
||||||
var skipped int64 = 0
|
|
||||||
for _, item := range z.byScore.items {
|
|
||||||
if item.score < minScore || item.score > maxScore {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if skipped < offset {
|
|
||||||
skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if count > 0 && int64(len(result)) >= count {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result = append(result, item.member)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZRevRangeByScore 按分数范围获取成员(降序)
|
|
||||||
func (c *MemoryCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
z, ok := ci.value.(*zsetItem)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("key is not a sorted set")
|
|
||||||
}
|
|
||||||
|
|
||||||
minScore, _ := strconv.ParseFloat(min, 64)
|
|
||||||
maxScore, _ := strconv.ParseFloat(max, 64)
|
|
||||||
if min == "-inf" {
|
|
||||||
minScore = math.Inf(-1)
|
|
||||||
}
|
|
||||||
if max == "+inf" {
|
|
||||||
maxScore = math.Inf(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
z.byScore.mu.RLock()
|
|
||||||
defer z.byScore.mu.RUnlock()
|
|
||||||
|
|
||||||
var result []string
|
|
||||||
var skipped int64 = 0
|
|
||||||
// 从后往前遍历
|
|
||||||
for i := len(z.byScore.items) - 1; i >= 0; i-- {
|
|
||||||
item := z.byScore.items[i]
|
|
||||||
if item.score < minScore || item.score > maxScore {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if skipped < offset {
|
|
||||||
skipped++
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if count > 0 && int64(len(result)) >= count {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
result = append(result, item.member)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZRem 删除 Sorted Set 成员
|
|
||||||
func (c *MemoryCache) ZRem(ctx context.Context, key string, members ...interface{}) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
z, ok := ci.value.(*zsetItem)
|
|
||||||
if !ok {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, m := range members {
|
|
||||||
if member, ok := m.(string); ok {
|
|
||||||
z.members.Delete(member)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 重建排序切片
|
|
||||||
z.byScore.mu.Lock()
|
|
||||||
z.byScore.items = nil
|
|
||||||
z.members.Range(func(k, v interface{}) bool {
|
|
||||||
z.byScore.items = append(z.byScore.items, v.(*zItem))
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
sort.Slice(z.byScore.items, func(i, j int) bool {
|
|
||||||
return z.byScore.items[i].score < z.byScore.items[j].score
|
|
||||||
})
|
|
||||||
z.byScore.mu.Unlock()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZCard 获取 Sorted Set 成员数量
|
|
||||||
func (c *MemoryCache) ZCard(ctx context.Context, key string) (int64, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
if ci.isExpired() {
|
|
||||||
c.items.Delete(key)
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
z, ok := ci.value.(*zsetItem)
|
|
||||||
if !ok {
|
|
||||||
return 0, fmt.Errorf("key is not a sorted set")
|
|
||||||
}
|
|
||||||
var count int64 = 0
|
|
||||||
z.members.Range(func(k, v interface{}) bool {
|
|
||||||
count++
|
|
||||||
return true
|
|
||||||
})
|
|
||||||
return count, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== MemoryCache 计数器操作 ====================
|
|
||||||
|
|
||||||
// Incr 原子递增(返回新值)
|
|
||||||
func (c *MemoryCache) Incr(ctx context.Context, key string) (int64, error) {
|
|
||||||
return c.IncrementBy(key, 1), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expire 设置过期时间
|
|
||||||
func (c *MemoryCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
item, ok := c.items.Load(key)
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("key not found")
|
|
||||||
}
|
|
||||||
ci := item.(*cacheItem)
|
|
||||||
var expiration int64
|
|
||||||
if ttl > 0 {
|
|
||||||
expiration = time.Now().Add(ttl).UnixNano()
|
|
||||||
}
|
|
||||||
c.items.Store(key, &cacheItem{
|
|
||||||
value: ci.value,
|
|
||||||
expiration: expiration,
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RedisCache Redis缓存实现
|
|
||||||
type RedisCache struct {
|
|
||||||
client *redisPkg.Client
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRedisCache 创建Redis缓存
|
|
||||||
func NewRedisCache(client *redisPkg.Client) *RedisCache {
|
|
||||||
return &RedisCache{
|
|
||||||
client: client,
|
|
||||||
ctx: context.Background(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set 设置缓存值
|
|
||||||
func (c *RedisCache) Set(key string, value interface{}, ttl time.Duration) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
// 将值序列化为JSON
|
|
||||||
data, err := json.Marshal(value)
|
|
||||||
if err != nil {
|
|
||||||
metrics.setError.Add(1)
|
|
||||||
log.Printf("[RedisCache] Failed to marshal value for key %s: %v", key, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.client.Set(c.ctx, key, data, ttl); err != nil {
|
|
||||||
metrics.setError.Add(1)
|
|
||||||
log.Printf("[RedisCache] Failed to set key %s: %v", key, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get 获取缓存值
|
|
||||||
func (c *RedisCache) Get(key string) (interface{}, bool) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
data, err := c.client.Get(c.ctx, key)
|
|
||||||
if err != nil {
|
|
||||||
if err == redis.Nil {
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
log.Printf("[RedisCache] Failed to get key %s: %v", key, err)
|
|
||||||
return nil, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回原始字符串,由调用侧决定如何解码为目标类型
|
|
||||||
return data, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Delete 删除缓存
|
|
||||||
func (c *RedisCache) Delete(key string) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
metrics.invalidate.Add(1)
|
|
||||||
if err := c.client.Del(c.ctx, key); err != nil {
|
|
||||||
log.Printf("[RedisCache] Failed to delete key %s: %v", key, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeleteByPrefix 根据前缀删除缓存
|
|
||||||
func (c *RedisCache) DeleteByPrefix(prefix string) {
|
|
||||||
prefix = normalizePrefix(prefix)
|
|
||||||
// 使用原生客户端执行SCAN命令
|
|
||||||
rdb := c.client.GetClient()
|
|
||||||
var cursor uint64
|
|
||||||
for {
|
|
||||||
keys, nextCursor, err := rdb.Scan(c.ctx, cursor, prefix+"*", 100).Result()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[RedisCache] Failed to scan keys with prefix %s: %v", prefix, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(keys) > 0 {
|
|
||||||
metrics.invalidate.Add(int64(len(keys)))
|
|
||||||
if err := c.client.Del(c.ctx, keys...); err != nil {
|
|
||||||
log.Printf("[RedisCache] Failed to delete keys with prefix %s: %v", prefix, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cursor = nextCursor
|
|
||||||
if cursor == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clear 清空所有缓存
|
|
||||||
func (c *RedisCache) Clear() {
|
|
||||||
if settings.DisableFlushDB {
|
|
||||||
log.Printf("[RedisCache] Skip FlushDB because cache.disable_flushdb=true")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
metrics.invalidate.Add(1)
|
|
||||||
rdb := c.client.GetClient()
|
|
||||||
if err := rdb.FlushDB(c.ctx).Err(); err != nil {
|
|
||||||
log.Printf("[RedisCache] Failed to clear cache: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exists 检查键是否存在
|
|
||||||
func (c *RedisCache) Exists(key string) bool {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
n, err := c.client.Exists(c.ctx, key)
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[RedisCache] Failed to check existence of key %s: %v", key, err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return n > 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Increment 增加计数器的值
|
|
||||||
func (c *RedisCache) Increment(key string) int64 {
|
|
||||||
return c.IncrementBy(key, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// IncrementBy 增加指定值
|
|
||||||
func (c *RedisCache) IncrementBy(key string, value int64) int64 {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
rdb := c.client.GetClient()
|
|
||||||
result, err := rdb.IncrBy(c.ctx, key, value).Result()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("[RedisCache] Failed to increment key %s: %v", key, err)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== RedisCache Hash 操作 ====================
|
|
||||||
|
|
||||||
// HSet 设置 Hash 字段
|
|
||||||
func (c *RedisCache) HSet(ctx context.Context, key string, field string, value interface{}) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.HSet(ctx, key, field, value)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMSet 批量设置 Hash 字段
|
|
||||||
func (c *RedisCache) HMSet(ctx context.Context, key string, values map[string]interface{}) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.HMSet(ctx, key, values)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HGet 获取 Hash 字段值
|
|
||||||
func (c *RedisCache) HGet(ctx context.Context, key string, field string) (string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.HGet(ctx, key, field)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HMGet 批量获取 Hash 字段值
|
|
||||||
func (c *RedisCache) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.HMGet(ctx, key, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HGetAll 获取 Hash 所有字段
|
|
||||||
func (c *RedisCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.HGetAll(ctx, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// HDel 删除 Hash 字段
|
|
||||||
func (c *RedisCache) HDel(ctx context.Context, key string, fields ...string) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.HDel(ctx, key, fields...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== RedisCache Sorted Set 操作 ====================
|
|
||||||
|
|
||||||
// ZAdd 添加 Sorted Set 成员
|
|
||||||
func (c *RedisCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.ZAdd(ctx, key, score, member)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZRangeByScore 按分数范围获取成员(升序)
|
|
||||||
func (c *RedisCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.ZRangeByScore(ctx, key, min, max, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZRevRangeByScore 按分数范围获取成员(降序)
|
|
||||||
func (c *RedisCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.ZRevRangeByScore(ctx, key, max, min, offset, count)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZRem 删除 Sorted Set 成员
|
|
||||||
func (c *RedisCache) ZRem(ctx context.Context, key string, members ...interface{}) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.ZRem(ctx, key, members...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ZCard 获取 Sorted Set 成员数量
|
|
||||||
func (c *RedisCache) ZCard(ctx context.Context, key string) (int64, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.ZCard(ctx, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== RedisCache 计数器操作 ====================
|
|
||||||
|
|
||||||
// Incr 原子递增(返回新值)
|
|
||||||
func (c *RedisCache) Incr(ctx context.Context, key string) (int64, error) {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
return c.client.Incr(ctx, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Expire 设置过期时间
|
|
||||||
func (c *RedisCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
|
|
||||||
key = normalizeKey(key)
|
|
||||||
_, err := c.client.Expire(ctx, key, ttl)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// 全局缓存实例
|
|
||||||
var globalCache Cache
|
|
||||||
var once sync.Once
|
|
||||||
|
|
||||||
// InitCache 初始化全局缓存实例(使用Redis)
|
|
||||||
func InitCache(redisClient *redisPkg.Client) {
|
|
||||||
once.Do(func() {
|
|
||||||
if redisClient != nil {
|
|
||||||
globalCache = NewRedisCache(redisClient)
|
|
||||||
log.Println("[Cache] Initialized Redis cache")
|
|
||||||
} else {
|
|
||||||
globalCache = NewMemoryCache()
|
|
||||||
log.Println("[Cache] Initialized Memory cache (Redis not available)")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetCache 获取全局缓存实例
|
|
||||||
func GetCache() Cache {
|
|
||||||
if globalCache == nil {
|
|
||||||
// 如果未初始化,返回内存缓存作为降级
|
|
||||||
log.Println("[Cache] Warning: Cache not initialized, using Memory cache")
|
|
||||||
return NewMemoryCache()
|
|
||||||
}
|
|
||||||
return globalCache
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRedisClient 从缓存中获取Redis客户端(仅在Redis模式下有效)
|
|
||||||
func GetRedisClient() (*redisPkg.Client, error) {
|
|
||||||
if redisCache, ok := globalCache.(*RedisCache); ok {
|
|
||||||
return redisCache.client, nil
|
|
||||||
}
|
|
||||||
return nil, fmt.Errorf("cache is not using Redis backend")
|
|
||||||
}
|
|
||||||
|
|
||||||
func SetWithJitter(c Cache, key string, value interface{}, ttl time.Duration, jitterRatio float64) {
|
func SetWithJitter(c Cache, key string, value interface{}, ttl time.Duration, jitterRatio float64) {
|
||||||
if !settings.Enabled {
|
if !settings.Enabled {
|
||||||
return
|
return
|
||||||
@@ -1014,16 +172,16 @@ func GetTyped[T any](c Cache, key string) (T, bool) {
|
|||||||
}
|
}
|
||||||
raw, ok := c.Get(key)
|
raw, ok := c.Get(key)
|
||||||
if !ok {
|
if !ok {
|
||||||
metrics.miss.Add(1)
|
recordMiss()
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
if str, ok := raw.(string); ok && str == nullMarkerValue {
|
if str, ok := raw.(string); ok && str == nullMarkerValue {
|
||||||
metrics.hit.Add(1)
|
recordHit()
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
|
|
||||||
if typed, ok := raw.(T); ok {
|
if typed, ok := raw.(T); ok {
|
||||||
metrics.hit.Add(1)
|
recordHit()
|
||||||
return typed, true
|
return typed, true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1031,29 +189,29 @@ func GetTyped[T any](c Cache, key string) (T, bool) {
|
|||||||
switch v := raw.(type) {
|
switch v := raw.(type) {
|
||||||
case string:
|
case string:
|
||||||
if err := json.Unmarshal([]byte(v), &out); err != nil {
|
if err := json.Unmarshal([]byte(v), &out); err != nil {
|
||||||
metrics.decodeError.Add(1)
|
recordDecodeError()
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
metrics.hit.Add(1)
|
recordHit()
|
||||||
return out, true
|
return out, true
|
||||||
case []byte:
|
case []byte:
|
||||||
if err := json.Unmarshal(v, &out); err != nil {
|
if err := json.Unmarshal(v, &out); err != nil {
|
||||||
metrics.decodeError.Add(1)
|
recordDecodeError()
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
metrics.hit.Add(1)
|
recordHit()
|
||||||
return out, true
|
return out, true
|
||||||
default:
|
default:
|
||||||
data, err := json.Marshal(v)
|
data, err := json.Marshal(v)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
metrics.decodeError.Add(1)
|
recordDecodeError()
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
if err := json.Unmarshal(data, &out); err != nil {
|
if err := json.Unmarshal(data, &out); err != nil {
|
||||||
metrics.decodeError.Add(1)
|
recordDecodeError()
|
||||||
return zero, false
|
return zero, false
|
||||||
}
|
}
|
||||||
metrics.hit.Add(1)
|
recordHit()
|
||||||
return out, true
|
return out, true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
65
internal/cache/metrics.go
vendored
Normal file
65
internal/cache/metrics.go
vendored
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import "sync/atomic"
|
||||||
|
|
||||||
|
// cacheMetrics 缓存指标
|
||||||
|
type cacheMetrics struct {
|
||||||
|
hit atomic.Int64
|
||||||
|
miss atomic.Int64
|
||||||
|
decodeError atomic.Int64
|
||||||
|
setError atomic.Int64
|
||||||
|
invalidate atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
// metrics 全局缓存指标实例
|
||||||
|
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 记录缓存命中
|
||||||
|
func recordHit() {
|
||||||
|
metrics.hit.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordMiss 记录缓存未命中
|
||||||
|
func recordMiss() {
|
||||||
|
metrics.miss.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordDecodeError 记录解码错误
|
||||||
|
func recordDecodeError() {
|
||||||
|
metrics.decodeError.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordSetError 记录设置错误
|
||||||
|
func recordSetError() {
|
||||||
|
metrics.setError.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordInvalidate 记录缓存失效
|
||||||
|
func recordInvalidate() {
|
||||||
|
metrics.invalidate.Add(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// recordInvalidateMultiple 记录多个缓存失效
|
||||||
|
func recordInvalidateMultiple(count int64) {
|
||||||
|
metrics.invalidate.Add(count)
|
||||||
|
}
|
||||||
226
internal/cache/redis_cache.go
vendored
Normal file
226
internal/cache/redis_cache.go
vendored
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
package cache
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
|
||||||
|
redisPkg "carrot_bbs/internal/pkg/redis"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisCache Redis缓存实现
|
||||||
|
type RedisCache struct {
|
||||||
|
client *redisPkg.Client
|
||||||
|
ctx context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedisCache 创建Redis缓存
|
||||||
|
func NewRedisCache(client *redisPkg.Client) *RedisCache {
|
||||||
|
return &RedisCache{
|
||||||
|
client: client,
|
||||||
|
ctx: context.Background(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set 设置缓存值
|
||||||
|
func (c *RedisCache) Set(key string, value interface{}, ttl time.Duration) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
// 将值序列化为JSON
|
||||||
|
data, err := json.Marshal(value)
|
||||||
|
if err != nil {
|
||||||
|
recordSetError()
|
||||||
|
log.Printf("[RedisCache] Failed to marshal value for key %s: %v", key, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := c.client.Set(c.ctx, key, data, ttl); err != nil {
|
||||||
|
recordSetError()
|
||||||
|
log.Printf("[RedisCache] Failed to set key %s: %v", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get 获取缓存值
|
||||||
|
func (c *RedisCache) Get(key string) (interface{}, bool) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
data, err := c.client.Get(c.ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
if err == redis.Nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
log.Printf("[RedisCache] Failed to get key %s: %v", key, err)
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回原始字符串,由调用侧决定如何解码为目标类型
|
||||||
|
return data, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete 删除缓存
|
||||||
|
func (c *RedisCache) Delete(key string) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
recordInvalidate()
|
||||||
|
if err := c.client.Del(c.ctx, key); err != nil {
|
||||||
|
log.Printf("[RedisCache] Failed to delete key %s: %v", key, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteByPrefix 根据前缀删除缓存
|
||||||
|
func (c *RedisCache) DeleteByPrefix(prefix string) {
|
||||||
|
prefix = normalizePrefix(prefix)
|
||||||
|
// 使用原生客户端执行SCAN命令
|
||||||
|
rdb := c.client.GetClient()
|
||||||
|
var cursor uint64
|
||||||
|
for {
|
||||||
|
keys, nextCursor, err := rdb.Scan(c.ctx, cursor, prefix+"*", 100).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[RedisCache] Failed to scan keys with prefix %s: %v", prefix, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(keys) > 0 {
|
||||||
|
recordInvalidateMultiple(int64(len(keys)))
|
||||||
|
if err := c.client.Del(c.ctx, keys...); err != nil {
|
||||||
|
log.Printf("[RedisCache] Failed to delete keys with prefix %s: %v", prefix, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = nextCursor
|
||||||
|
if cursor == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear 清空所有缓存
|
||||||
|
func (c *RedisCache) Clear() {
|
||||||
|
if settings.DisableFlushDB {
|
||||||
|
log.Printf("[RedisCache] Skip FlushDB because cache.disable_flushdb=true")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
recordInvalidate()
|
||||||
|
rdb := c.client.GetClient()
|
||||||
|
if err := rdb.FlushDB(c.ctx).Err(); err != nil {
|
||||||
|
log.Printf("[RedisCache] Failed to clear cache: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exists 检查键是否存在
|
||||||
|
func (c *RedisCache) Exists(key string) bool {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
n, err := c.client.Exists(c.ctx, key)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[RedisCache] Failed to check existence of key %s: %v", key, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return n > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment 增加计数器的值
|
||||||
|
func (c *RedisCache) Increment(key string) int64 {
|
||||||
|
return c.IncrementBy(key, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementBy 增加指定值
|
||||||
|
func (c *RedisCache) IncrementBy(key string, value int64) int64 {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
rdb := c.client.GetClient()
|
||||||
|
result, err := rdb.IncrBy(c.ctx, key, value).Result()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[RedisCache] Failed to increment key %s: %v", key, err)
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RedisCache Hash 操作 ====================
|
||||||
|
|
||||||
|
// HSet 设置 Hash 字段
|
||||||
|
func (c *RedisCache) HSet(ctx context.Context, key string, field string, value interface{}) error {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.HSet(ctx, key, field, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMSet 批量设置 Hash 字段
|
||||||
|
func (c *RedisCache) HMSet(ctx context.Context, key string, values map[string]interface{}) error {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.HMSet(ctx, key, values)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HGet 获取 Hash 字段值
|
||||||
|
func (c *RedisCache) HGet(ctx context.Context, key string, field string) (string, error) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.HGet(ctx, key, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HMGet 批量获取 Hash 字段值
|
||||||
|
func (c *RedisCache) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.HMGet(ctx, key, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HGetAll 获取 Hash 所有字段
|
||||||
|
func (c *RedisCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.HGetAll(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HDel 删除 Hash 字段
|
||||||
|
func (c *RedisCache) HDel(ctx context.Context, key string, fields ...string) error {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.HDel(ctx, key, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RedisCache Sorted Set 操作 ====================
|
||||||
|
|
||||||
|
// ZAdd 添加 Sorted Set 成员
|
||||||
|
func (c *RedisCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.ZAdd(ctx, key, score, member)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZRangeByScore 按分数范围获取成员(升序)
|
||||||
|
func (c *RedisCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.ZRangeByScore(ctx, key, min, max, offset, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZRevRangeByScore 按分数范围获取成员(降序)
|
||||||
|
func (c *RedisCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.ZRevRangeByScore(ctx, key, max, min, offset, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZRem 删除 Sorted Set 成员
|
||||||
|
func (c *RedisCache) ZRem(ctx context.Context, key string, members ...interface{}) error {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.ZRem(ctx, key, members...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ZCard 获取 Sorted Set 成员数量
|
||||||
|
func (c *RedisCache) ZCard(ctx context.Context, key string) (int64, error) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.ZCard(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== RedisCache 计数器操作 ====================
|
||||||
|
|
||||||
|
// Incr 原子递增(返回新值)
|
||||||
|
func (c *RedisCache) Incr(ctx context.Context, key string) (int64, error) {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
return c.client.Incr(ctx, key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expire 设置过期时间
|
||||||
|
func (c *RedisCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
|
||||||
|
key = normalizeKey(key)
|
||||||
|
_, err := c.client.Expire(ctx, key, ttl)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetRedisClient 获取Redis客户端
|
||||||
|
func (c *RedisCache) GetRedisClient() *redisPkg.Client {
|
||||||
|
return c.client
|
||||||
|
}
|
||||||
70
internal/config/cache.go
Normal file
70
internal/config/cache.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// ConversationCacheConfig 会话缓存配置
|
||||||
|
type ConversationCacheConfig struct {
|
||||||
|
// TTL 配置
|
||||||
|
DetailTTL string `mapstructure:"detail_ttl"`
|
||||||
|
ListTTL string `mapstructure:"list_ttl"`
|
||||||
|
ParticipantTTL string `mapstructure:"participant_ttl"`
|
||||||
|
UnreadTTL string `mapstructure:"unread_ttl"`
|
||||||
|
|
||||||
|
// 消息缓存配置
|
||||||
|
MessageDetailTTL string `mapstructure:"message_detail_ttl"`
|
||||||
|
MessageListTTL string `mapstructure:"message_list_ttl"`
|
||||||
|
MessageIndexTTL string `mapstructure:"message_index_ttl"`
|
||||||
|
MessageCountTTL string `mapstructure:"message_count_ttl"`
|
||||||
|
|
||||||
|
// 批量写入配置
|
||||||
|
BatchInterval string `mapstructure:"batch_interval"`
|
||||||
|
BatchThreshold int `mapstructure:"batch_threshold"`
|
||||||
|
BatchMaxSize int `mapstructure:"batch_max_size"`
|
||||||
|
BufferMaxSize int `mapstructure:"buffer_max_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConversationCacheSettings 会话缓存运行时配置(用于传递给 cache 包)
|
||||||
|
type ConversationCacheSettings struct {
|
||||||
|
DetailTTL time.Duration
|
||||||
|
ListTTL time.Duration
|
||||||
|
ParticipantTTL time.Duration
|
||||||
|
UnreadTTL time.Duration
|
||||||
|
MessageDetailTTL time.Duration
|
||||||
|
MessageListTTL time.Duration
|
||||||
|
MessageIndexTTL time.Duration
|
||||||
|
MessageCountTTL time.Duration
|
||||||
|
BatchInterval time.Duration
|
||||||
|
BatchThreshold int
|
||||||
|
BatchMaxSize int
|
||||||
|
BufferMaxSize int
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToSettings 将 ConversationCacheConfig 转换为 ConversationCacheSettings
|
||||||
|
func (c *ConversationCacheConfig) ToSettings() *ConversationCacheSettings {
|
||||||
|
return &ConversationCacheSettings{
|
||||||
|
DetailTTL: parseDuration(c.DetailTTL, 5*time.Minute),
|
||||||
|
ListTTL: parseDuration(c.ListTTL, 60*time.Second),
|
||||||
|
ParticipantTTL: parseDuration(c.ParticipantTTL, 5*time.Minute),
|
||||||
|
UnreadTTL: parseDuration(c.UnreadTTL, 30*time.Second),
|
||||||
|
MessageDetailTTL: parseDuration(c.MessageDetailTTL, 30*time.Minute),
|
||||||
|
MessageListTTL: parseDuration(c.MessageListTTL, 5*time.Minute),
|
||||||
|
MessageIndexTTL: parseDuration(c.MessageIndexTTL, 30*time.Minute),
|
||||||
|
MessageCountTTL: parseDuration(c.MessageCountTTL, 30*time.Minute),
|
||||||
|
BatchInterval: parseDuration(c.BatchInterval, 5*time.Second),
|
||||||
|
BatchThreshold: c.BatchThreshold,
|
||||||
|
BatchMaxSize: c.BatchMaxSize,
|
||||||
|
BufferMaxSize: c.BufferMaxSize,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseDuration 解析持续时间字符串,如果解析失败则返回默认值
|
||||||
|
func parseDuration(s string, defaultVal time.Duration) time.Duration {
|
||||||
|
if s == "" {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
d, err := time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
return defaultVal
|
||||||
|
}
|
||||||
|
return d
|
||||||
|
}
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/minio/minio-go/v7"
|
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
||||||
"github.com/redis/go-redis/v9"
|
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Config 主配置结构体
|
||||||
type Config struct {
|
type Config struct {
|
||||||
Server ServerConfig `mapstructure:"server"`
|
Server ServerConfig `mapstructure:"server"`
|
||||||
Database DatabaseConfig `mapstructure:"database"`
|
Database DatabaseConfig `mapstructure:"database"`
|
||||||
@@ -30,217 +27,7 @@ type Config struct {
|
|||||||
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
|
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerConfig struct {
|
// Load 加载配置文件
|
||||||
Host string `mapstructure:"host"`
|
|
||||||
Port int `mapstructure:"port"`
|
|
||||||
Mode string `mapstructure:"mode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DatabaseConfig struct {
|
|
||||||
Type string `mapstructure:"type"`
|
|
||||||
SQLite SQLiteConfig `mapstructure:"sqlite"`
|
|
||||||
Postgres PostgresConfig `mapstructure:"postgres"`
|
|
||||||
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
|
||||||
MaxOpenConns int `mapstructure:"max_open_conns"`
|
|
||||||
LogLevel string `mapstructure:"log_level"`
|
|
||||||
SlowThresholdMs int `mapstructure:"slow_threshold_ms"`
|
|
||||||
IgnoreRecordNotFound bool `mapstructure:"ignore_record_not_found"`
|
|
||||||
ParameterizedQueries bool `mapstructure:"parameterized_queries"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SQLiteConfig struct {
|
|
||||||
Path string `mapstructure:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PostgresConfig struct {
|
|
||||||
Host string `mapstructure:"host"`
|
|
||||||
Port int `mapstructure:"port"`
|
|
||||||
User string `mapstructure:"user"`
|
|
||||||
Password string `mapstructure:"password"`
|
|
||||||
DBName string `mapstructure:"dbname"`
|
|
||||||
SSLMode string `mapstructure:"sslmode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d PostgresConfig) DSN() string {
|
|
||||||
return fmt.Sprintf(
|
|
||||||
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
|
||||||
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedisConfig struct {
|
|
||||||
Type string `mapstructure:"type"`
|
|
||||||
Redis RedisServerConfig `mapstructure:"redis"`
|
|
||||||
Miniredis MiniredisConfig `mapstructure:"miniredis"`
|
|
||||||
PoolSize int `mapstructure:"pool_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CacheConfig struct {
|
|
||||||
Enabled bool `mapstructure:"enabled"`
|
|
||||||
KeyPrefix string `mapstructure:"key_prefix"`
|
|
||||||
DefaultTTL int `mapstructure:"default_ttl"`
|
|
||||||
NullTTL int `mapstructure:"null_ttl"`
|
|
||||||
JitterRatio float64 `mapstructure:"jitter_ratio"`
|
|
||||||
DisableFlushDB bool `mapstructure:"disable_flushdb"`
|
|
||||||
Modules CacheModuleTTL `mapstructure:"modules"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type CacheModuleTTL struct {
|
|
||||||
PostList int `mapstructure:"post_list_ttl"`
|
|
||||||
Conversation int `mapstructure:"conversation_ttl"`
|
|
||||||
UnreadCount int `mapstructure:"unread_count_ttl"`
|
|
||||||
GroupMembers int `mapstructure:"group_members_ttl"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RedisServerConfig struct {
|
|
||||||
Host string `mapstructure:"host"`
|
|
||||||
Port int `mapstructure:"port"`
|
|
||||||
Password string `mapstructure:"password"`
|
|
||||||
DB int `mapstructure:"db"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r RedisServerConfig) Addr() string {
|
|
||||||
return fmt.Sprintf("%s:%d", r.Host, r.Port)
|
|
||||||
}
|
|
||||||
|
|
||||||
type MiniredisConfig struct {
|
|
||||||
Host string `mapstructure:"host"`
|
|
||||||
Port int `mapstructure:"port"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type S3Config struct {
|
|
||||||
Endpoint string `mapstructure:"endpoint"`
|
|
||||||
AccessKey string `mapstructure:"access_key"`
|
|
||||||
SecretKey string `mapstructure:"secret_key"`
|
|
||||||
Bucket string `mapstructure:"bucket"`
|
|
||||||
UseSSL bool `mapstructure:"use_ssl"`
|
|
||||||
Region string `mapstructure:"region"`
|
|
||||||
Domain string `mapstructure:"domain"` // 自定义域名,如 s3.carrot.skin
|
|
||||||
}
|
|
||||||
|
|
||||||
type JWTConfig struct {
|
|
||||||
Secret string `mapstructure:"secret"`
|
|
||||||
AccessTokenExpire time.Duration `mapstructure:"access_token_expire"`
|
|
||||||
RefreshTokenExpire time.Duration `mapstructure:"refresh_token_expire"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogConfig struct {
|
|
||||||
Level string `mapstructure:"level"`
|
|
||||||
Encoding string `mapstructure:"encoding"`
|
|
||||||
OutputPaths []string `mapstructure:"output_paths"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RateLimitConfig struct {
|
|
||||||
Enabled bool `mapstructure:"enabled"`
|
|
||||||
RequestsPerMinute int `mapstructure:"requests_per_minute"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UploadConfig struct {
|
|
||||||
MaxFileSize int64 `mapstructure:"max_file_size"`
|
|
||||||
AllowedTypes []string `mapstructure:"allowed_types"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GorseConfig struct {
|
|
||||||
Address string `mapstructure:"address"`
|
|
||||||
APIKey string `mapstructure:"api_key"`
|
|
||||||
Enabled bool `mapstructure:"enabled"`
|
|
||||||
Dashboard string `mapstructure:"dashboard"`
|
|
||||||
ImportPassword string `mapstructure:"import_password"`
|
|
||||||
EmbeddingAPIKey string `mapstructure:"embedding_api_key"`
|
|
||||||
EmbeddingURL string `mapstructure:"embedding_url"`
|
|
||||||
EmbeddingModel string `mapstructure:"embedding_model"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type OpenAIConfig struct {
|
|
||||||
Enabled bool `mapstructure:"enabled"`
|
|
||||||
BaseURL string `mapstructure:"base_url"`
|
|
||||||
APIKey string `mapstructure:"api_key"`
|
|
||||||
ModerationModel string `mapstructure:"moderation_model"`
|
|
||||||
ModerationMaxImagesPerRequest int `mapstructure:"moderation_max_images_per_request"`
|
|
||||||
RequestTimeout int `mapstructure:"request_timeout"`
|
|
||||||
StrictModeration bool `mapstructure:"strict_moderation"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type EmailConfig struct {
|
|
||||||
Enabled bool `mapstructure:"enabled"`
|
|
||||||
Host string `mapstructure:"host"`
|
|
||||||
Port int `mapstructure:"port"`
|
|
||||||
Username string `mapstructure:"username"`
|
|
||||||
Password string `mapstructure:"password"`
|
|
||||||
FromAddress string `mapstructure:"from_address"`
|
|
||||||
FromName string `mapstructure:"from_name"`
|
|
||||||
UseTLS bool `mapstructure:"use_tls"`
|
|
||||||
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
|
||||||
Timeout int `mapstructure:"timeout"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConversationCacheConfig 会话缓存配置
|
|
||||||
type ConversationCacheConfig struct {
|
|
||||||
// TTL 配置
|
|
||||||
DetailTTL string `mapstructure:"detail_ttl"`
|
|
||||||
ListTTL string `mapstructure:"list_ttl"`
|
|
||||||
ParticipantTTL string `mapstructure:"participant_ttl"`
|
|
||||||
UnreadTTL string `mapstructure:"unread_ttl"`
|
|
||||||
|
|
||||||
// 消息缓存配置
|
|
||||||
MessageDetailTTL string `mapstructure:"message_detail_ttl"`
|
|
||||||
MessageListTTL string `mapstructure:"message_list_ttl"`
|
|
||||||
MessageIndexTTL string `mapstructure:"message_index_ttl"`
|
|
||||||
MessageCountTTL string `mapstructure:"message_count_ttl"`
|
|
||||||
|
|
||||||
// 批量写入配置
|
|
||||||
BatchInterval string `mapstructure:"batch_interval"`
|
|
||||||
BatchThreshold int `mapstructure:"batch_threshold"`
|
|
||||||
BatchMaxSize int `mapstructure:"batch_max_size"`
|
|
||||||
BufferMaxSize int `mapstructure:"buffer_max_size"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConversationCacheSettings 会话缓存运行时配置(用于传递给 cache 包)
|
|
||||||
type ConversationCacheSettings struct {
|
|
||||||
DetailTTL time.Duration
|
|
||||||
ListTTL time.Duration
|
|
||||||
ParticipantTTL time.Duration
|
|
||||||
UnreadTTL time.Duration
|
|
||||||
MessageDetailTTL time.Duration
|
|
||||||
MessageListTTL time.Duration
|
|
||||||
MessageIndexTTL time.Duration
|
|
||||||
MessageCountTTL time.Duration
|
|
||||||
BatchInterval time.Duration
|
|
||||||
BatchThreshold int
|
|
||||||
BatchMaxSize int
|
|
||||||
BufferMaxSize int
|
|
||||||
}
|
|
||||||
|
|
||||||
// ToSettings 将 ConversationCacheConfig 转换为 ConversationCacheSettings
|
|
||||||
func (c *ConversationCacheConfig) ToSettings() *ConversationCacheSettings {
|
|
||||||
return &ConversationCacheSettings{
|
|
||||||
DetailTTL: parseDuration(c.DetailTTL, 5*time.Minute),
|
|
||||||
ListTTL: parseDuration(c.ListTTL, 60*time.Second),
|
|
||||||
ParticipantTTL: parseDuration(c.ParticipantTTL, 5*time.Minute),
|
|
||||||
UnreadTTL: parseDuration(c.UnreadTTL, 30*time.Second),
|
|
||||||
MessageDetailTTL: parseDuration(c.MessageDetailTTL, 30*time.Minute),
|
|
||||||
MessageListTTL: parseDuration(c.MessageListTTL, 5*time.Minute),
|
|
||||||
MessageIndexTTL: parseDuration(c.MessageIndexTTL, 30*time.Minute),
|
|
||||||
MessageCountTTL: parseDuration(c.MessageCountTTL, 30*time.Minute),
|
|
||||||
BatchInterval: parseDuration(c.BatchInterval, 5*time.Second),
|
|
||||||
BatchThreshold: c.BatchThreshold,
|
|
||||||
BatchMaxSize: c.BatchMaxSize,
|
|
||||||
BufferMaxSize: c.BufferMaxSize,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseDuration 解析持续时间字符串,如果解析失败则返回默认值
|
|
||||||
func parseDuration(s string, defaultVal time.Duration) time.Duration {
|
|
||||||
if s == "" {
|
|
||||||
return defaultVal
|
|
||||||
}
|
|
||||||
d, err := time.ParseDuration(s)
|
|
||||||
if err != nil {
|
|
||||||
return defaultVal
|
|
||||||
}
|
|
||||||
return d
|
|
||||||
}
|
|
||||||
|
|
||||||
func Load(configPath string) (*Config, error) {
|
func Load(configPath string) (*Config, error) {
|
||||||
viper.SetConfigFile(configPath)
|
viper.SetConfigFile(configPath)
|
||||||
viper.SetConfigType("yaml")
|
viper.SetConfigType("yaml")
|
||||||
@@ -426,49 +213,3 @@ func getEnvOrDefault(key, defaultValue string) string {
|
|||||||
}
|
}
|
||||||
return defaultValue
|
return defaultValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewRedis 创建Redis客户端(真实Redis)
|
|
||||||
func NewRedis(cfg *RedisConfig) (*redis.Client, error) {
|
|
||||||
client := redis.NewClient(&redis.Options{
|
|
||||||
Addr: cfg.Redis.Addr(),
|
|
||||||
Password: cfg.Redis.Password,
|
|
||||||
DB: cfg.Redis.DB,
|
|
||||||
PoolSize: cfg.PoolSize,
|
|
||||||
})
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
if err := client.Ping(ctx).Err(); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to connect to redis: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewS3 创建S3客户端
|
|
||||||
func NewS3(cfg *S3Config) (*minio.Client, error) {
|
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
|
||||||
defer cancel()
|
|
||||||
|
|
||||||
client, err := minio.New(cfg.Endpoint, &minio.Options{
|
|
||||||
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
|
|
||||||
Secure: cfg.UseSSL,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create S3 client: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exists, err := client.BucketExists(ctx, cfg.Bucket)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to check bucket: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
|
||||||
Region: cfg.Region,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to create bucket: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|||||||
39
internal/config/database.go
Normal file
39
internal/config/database.go
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// DatabaseConfig 数据库配置
|
||||||
|
type DatabaseConfig struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
SQLite SQLiteConfig `mapstructure:"sqlite"`
|
||||||
|
Postgres PostgresConfig `mapstructure:"postgres"`
|
||||||
|
MaxIdleConns int `mapstructure:"max_idle_conns"`
|
||||||
|
MaxOpenConns int `mapstructure:"max_open_conns"`
|
||||||
|
LogLevel string `mapstructure:"log_level"`
|
||||||
|
SlowThresholdMs int `mapstructure:"slow_threshold_ms"`
|
||||||
|
IgnoreRecordNotFound bool `mapstructure:"ignore_record_not_found"`
|
||||||
|
ParameterizedQueries bool `mapstructure:"parameterized_queries"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// SQLiteConfig SQLite 配置
|
||||||
|
type SQLiteConfig struct {
|
||||||
|
Path string `mapstructure:"path"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// PostgresConfig PostgreSQL 配置
|
||||||
|
type PostgresConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
User string `mapstructure:"user"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
DBName string `mapstructure:"dbname"`
|
||||||
|
SSLMode string `mapstructure:"sslmode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// DSN 返回 PostgreSQL 连接字符串
|
||||||
|
func (d PostgresConfig) DSN() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
|
||||||
|
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
|
||||||
|
)
|
||||||
|
}
|
||||||
15
internal/config/email.go
Normal file
15
internal/config/email.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// EmailConfig 邮件配置
|
||||||
|
type EmailConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
FromAddress string `mapstructure:"from_address"`
|
||||||
|
FromName string `mapstructure:"from_name"`
|
||||||
|
UseTLS bool `mapstructure:"use_tls"`
|
||||||
|
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
|
||||||
|
Timeout int `mapstructure:"timeout"`
|
||||||
|
}
|
||||||
24
internal/config/external.go
Normal file
24
internal/config/external.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// GorseConfig Gorse 推荐系统配置
|
||||||
|
type GorseConfig struct {
|
||||||
|
Address string `mapstructure:"address"`
|
||||||
|
APIKey string `mapstructure:"api_key"`
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
Dashboard string `mapstructure:"dashboard"`
|
||||||
|
ImportPassword string `mapstructure:"import_password"`
|
||||||
|
EmbeddingAPIKey string `mapstructure:"embedding_api_key"`
|
||||||
|
EmbeddingURL string `mapstructure:"embedding_url"`
|
||||||
|
EmbeddingModel string `mapstructure:"embedding_model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// OpenAIConfig OpenAI 配置
|
||||||
|
type OpenAIConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
BaseURL string `mapstructure:"base_url"`
|
||||||
|
APIKey string `mapstructure:"api_key"`
|
||||||
|
ModerationModel string `mapstructure:"moderation_model"`
|
||||||
|
ModerationMaxImagesPerRequest int `mapstructure:"moderation_max_images_per_request"`
|
||||||
|
RequestTimeout int `mapstructure:"request_timeout"`
|
||||||
|
StrictModeration bool `mapstructure:"strict_moderation"`
|
||||||
|
}
|
||||||
10
internal/config/jwt.go
Normal file
10
internal/config/jwt.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
// JWTConfig JWT 配置
|
||||||
|
type JWTConfig struct {
|
||||||
|
Secret string `mapstructure:"secret"`
|
||||||
|
AccessTokenExpire time.Duration `mapstructure:"access_token_expire"`
|
||||||
|
RefreshTokenExpire time.Duration `mapstructure:"refresh_token_expire"`
|
||||||
|
}
|
||||||
71
internal/config/redis.go
Normal file
71
internal/config/redis.go
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/redis/go-redis/v9"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RedisConfig Redis 配置
|
||||||
|
type RedisConfig struct {
|
||||||
|
Type string `mapstructure:"type"`
|
||||||
|
Redis RedisServerConfig `mapstructure:"redis"`
|
||||||
|
Miniredis MiniredisConfig `mapstructure:"miniredis"`
|
||||||
|
PoolSize int `mapstructure:"pool_size"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheConfig 缓存配置
|
||||||
|
type CacheConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
KeyPrefix string `mapstructure:"key_prefix"`
|
||||||
|
DefaultTTL int `mapstructure:"default_ttl"`
|
||||||
|
NullTTL int `mapstructure:"null_ttl"`
|
||||||
|
JitterRatio float64 `mapstructure:"jitter_ratio"`
|
||||||
|
DisableFlushDB bool `mapstructure:"disable_flushdb"`
|
||||||
|
Modules CacheModuleTTL `mapstructure:"modules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CacheModuleTTL 缓存模块 TTL 配置
|
||||||
|
type CacheModuleTTL struct {
|
||||||
|
PostList int `mapstructure:"post_list_ttl"`
|
||||||
|
Conversation int `mapstructure:"conversation_ttl"`
|
||||||
|
UnreadCount int `mapstructure:"unread_count_ttl"`
|
||||||
|
GroupMembers int `mapstructure:"group_members_ttl"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RedisServerConfig Redis 服务器配置
|
||||||
|
type RedisServerConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
DB int `mapstructure:"db"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addr 返回 Redis 服务器地址
|
||||||
|
func (r RedisServerConfig) Addr() string {
|
||||||
|
return fmt.Sprintf("%s:%d", r.Host, r.Port)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MiniredisConfig Miniredis 配置(用于测试)
|
||||||
|
type MiniredisConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRedis 创建 Redis 客户端(真实 Redis)
|
||||||
|
func NewRedis(cfg *RedisConfig) (*redis.Client, error) {
|
||||||
|
client := redis.NewClient(&redis.Options{
|
||||||
|
Addr: cfg.Redis.Addr(),
|
||||||
|
Password: cfg.Redis.Password,
|
||||||
|
DB: cfg.Redis.DB,
|
||||||
|
PoolSize: cfg.PoolSize,
|
||||||
|
})
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
if err := client.Ping(ctx).Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to connect to redis: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
27
internal/config/server.go
Normal file
27
internal/config/server.go
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
// ServerConfig 服务器配置
|
||||||
|
type ServerConfig struct {
|
||||||
|
Host string `mapstructure:"host"`
|
||||||
|
Port int `mapstructure:"port"`
|
||||||
|
Mode string `mapstructure:"mode"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// LogConfig 日志配置
|
||||||
|
type LogConfig struct {
|
||||||
|
Level string `mapstructure:"level"`
|
||||||
|
Encoding string `mapstructure:"encoding"`
|
||||||
|
OutputPaths []string `mapstructure:"output_paths"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// RateLimitConfig 限流配置
|
||||||
|
type RateLimitConfig struct {
|
||||||
|
Enabled bool `mapstructure:"enabled"`
|
||||||
|
RequestsPerMinute int `mapstructure:"requests_per_minute"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// UploadConfig 上传配置
|
||||||
|
type UploadConfig struct {
|
||||||
|
MaxFileSize int64 `mapstructure:"max_file_size"`
|
||||||
|
AllowedTypes []string `mapstructure:"allowed_types"`
|
||||||
|
}
|
||||||
50
internal/config/storage.go
Normal file
50
internal/config/storage.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/minio/minio-go/v7"
|
||||||
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
|
)
|
||||||
|
|
||||||
|
// S3Config S3 存储配置
|
||||||
|
type S3Config struct {
|
||||||
|
Endpoint string `mapstructure:"endpoint"`
|
||||||
|
AccessKey string `mapstructure:"access_key"`
|
||||||
|
SecretKey string `mapstructure:"secret_key"`
|
||||||
|
Bucket string `mapstructure:"bucket"`
|
||||||
|
UseSSL bool `mapstructure:"use_ssl"`
|
||||||
|
Region string `mapstructure:"region"`
|
||||||
|
Domain string `mapstructure:"domain"` // 自定义域名,如 s3.carrot.skin
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewS3 创建 S3 客户端
|
||||||
|
func NewS3(cfg *S3Config) (*minio.Client, error) {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
client, err := minio.New(cfg.Endpoint, &minio.Options{
|
||||||
|
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
|
||||||
|
Secure: cfg.UseSSL,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create S3 client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
exists, err := client.BucketExists(ctx, cfg.Bucket)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to check bucket: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
|
||||||
|
Region: cfg.Region,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to create bucket: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
@@ -1,899 +1,13 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
// converter.go - 公共转换函数
|
||||||
"carrot_bbs/internal/model"
|
//
|
||||||
"carrot_bbs/internal/pkg/utils"
|
// 转换函数已按领域拆分到以下文件:
|
||||||
"context"
|
// - user_converter.go: 用户相关转换
|
||||||
"encoding/json"
|
// - post_converter.go: 帖子和评论相关转换
|
||||||
"strconv"
|
// - message_converter.go: 消息、会话、推送和设备令牌相关转换
|
||||||
)
|
// - group_converter.go: 群组相关转换
|
||||||
|
// - notification_converter.go: 通知和系统消息相关转换
|
||||||
// ==================== User 转换 ====================
|
// - schedule_converter.go: 日程相关转换(已存在)
|
||||||
|
//
|
||||||
// getAvatarOrDefault 获取头像URL,如果为空则返回在线头像生成服务的URL
|
// 所有转换函数都在 dto 包中,可以直接通过 dto.ConvertXxxToResponse 调用。
|
||||||
func getAvatarOrDefault(user *model.User) string {
|
|
||||||
return utils.GetAvatarOrDefault(user.Username, user.Nickname, user.Avatar)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUserToResponse 将User转换为UserResponse
|
|
||||||
func ConvertUserToResponse(user *model.User) *UserResponse {
|
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &UserResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Username: user.Username,
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
Email: user.Email,
|
|
||||||
Phone: user.Phone,
|
|
||||||
EmailVerified: user.EmailVerified,
|
|
||||||
Avatar: getAvatarOrDefault(user),
|
|
||||||
CoverURL: user.CoverURL,
|
|
||||||
Bio: user.Bio,
|
|
||||||
Website: user.Website,
|
|
||||||
Location: user.Location,
|
|
||||||
PostsCount: user.PostsCount,
|
|
||||||
FollowersCount: user.FollowersCount,
|
|
||||||
FollowingCount: user.FollowingCount,
|
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUserToResponseWithFollowing 将User转换为UserResponse(包含关注状态)
|
|
||||||
func ConvertUserToResponseWithFollowing(user *model.User, isFollowing bool) *UserResponse {
|
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &UserResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Username: user.Username,
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
Email: user.Email,
|
|
||||||
Phone: user.Phone,
|
|
||||||
EmailVerified: user.EmailVerified,
|
|
||||||
Avatar: getAvatarOrDefault(user),
|
|
||||||
CoverURL: user.CoverURL,
|
|
||||||
Bio: user.Bio,
|
|
||||||
Website: user.Website,
|
|
||||||
Location: user.Location,
|
|
||||||
PostsCount: user.PostsCount,
|
|
||||||
FollowersCount: user.FollowersCount,
|
|
||||||
FollowingCount: user.FollowingCount,
|
|
||||||
IsFollowing: isFollowing,
|
|
||||||
IsFollowingMe: false, // 默认false,需要单独计算
|
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUserToResponseWithPostsCount 将User转换为UserResponse(使用实时计算的帖子数量)
|
|
||||||
func ConvertUserToResponseWithPostsCount(user *model.User, postsCount int) *UserResponse {
|
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &UserResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Username: user.Username,
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
Email: user.Email,
|
|
||||||
Phone: user.Phone,
|
|
||||||
EmailVerified: user.EmailVerified,
|
|
||||||
Avatar: getAvatarOrDefault(user),
|
|
||||||
CoverURL: user.CoverURL,
|
|
||||||
Bio: user.Bio,
|
|
||||||
Website: user.Website,
|
|
||||||
Location: user.Location,
|
|
||||||
PostsCount: postsCount,
|
|
||||||
FollowersCount: user.FollowersCount,
|
|
||||||
FollowingCount: user.FollowingCount,
|
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUserToResponseWithMutualFollow 将User转换为UserResponse(包含双向关注状态)
|
|
||||||
func ConvertUserToResponseWithMutualFollow(user *model.User, isFollowing, isFollowingMe bool) *UserResponse {
|
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &UserResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Username: user.Username,
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
Email: user.Email,
|
|
||||||
Phone: user.Phone,
|
|
||||||
EmailVerified: user.EmailVerified,
|
|
||||||
Avatar: getAvatarOrDefault(user),
|
|
||||||
CoverURL: user.CoverURL,
|
|
||||||
Bio: user.Bio,
|
|
||||||
Website: user.Website,
|
|
||||||
Location: user.Location,
|
|
||||||
PostsCount: user.PostsCount,
|
|
||||||
FollowersCount: user.FollowersCount,
|
|
||||||
FollowingCount: user.FollowingCount,
|
|
||||||
IsFollowing: isFollowing,
|
|
||||||
IsFollowingMe: isFollowingMe,
|
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUserToResponseWithMutualFollowAndPostsCount 将User转换为UserResponse(包含双向关注状态和实时计算的帖子数量)
|
|
||||||
func ConvertUserToResponseWithMutualFollowAndPostsCount(user *model.User, isFollowing, isFollowingMe bool, postsCount int) *UserResponse {
|
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &UserResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Username: user.Username,
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
Email: user.Email,
|
|
||||||
Phone: user.Phone,
|
|
||||||
EmailVerified: user.EmailVerified,
|
|
||||||
Avatar: getAvatarOrDefault(user),
|
|
||||||
CoverURL: user.CoverURL,
|
|
||||||
Bio: user.Bio,
|
|
||||||
Website: user.Website,
|
|
||||||
Location: user.Location,
|
|
||||||
PostsCount: postsCount,
|
|
||||||
FollowersCount: user.FollowersCount,
|
|
||||||
FollowingCount: user.FollowingCount,
|
|
||||||
IsFollowing: isFollowing,
|
|
||||||
IsFollowingMe: isFollowingMe,
|
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUserToDetailResponse 将User转换为UserDetailResponse
|
|
||||||
func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
|
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &UserDetailResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Username: user.Username,
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
Email: user.Email,
|
|
||||||
EmailVerified: user.EmailVerified,
|
|
||||||
Avatar: getAvatarOrDefault(user),
|
|
||||||
CoverURL: user.CoverURL,
|
|
||||||
Bio: user.Bio,
|
|
||||||
Website: user.Website,
|
|
||||||
Location: user.Location,
|
|
||||||
PostsCount: user.PostsCount,
|
|
||||||
FollowersCount: user.FollowersCount,
|
|
||||||
FollowingCount: user.FollowingCount,
|
|
||||||
IsVerified: user.IsVerified,
|
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUserToDetailResponseWithPostsCount 将User转换为UserDetailResponse(使用实时计算的帖子数量)
|
|
||||||
func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int) *UserDetailResponse {
|
|
||||||
if user == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &UserDetailResponse{
|
|
||||||
ID: user.ID,
|
|
||||||
Username: user.Username,
|
|
||||||
Nickname: user.Nickname,
|
|
||||||
Email: user.Email,
|
|
||||||
EmailVerified: user.EmailVerified,
|
|
||||||
Phone: user.Phone, // 仅当前用户自己可见
|
|
||||||
Avatar: getAvatarOrDefault(user),
|
|
||||||
CoverURL: user.CoverURL,
|
|
||||||
Bio: user.Bio,
|
|
||||||
Website: user.Website,
|
|
||||||
Location: user.Location,
|
|
||||||
PostsCount: postsCount,
|
|
||||||
FollowersCount: user.FollowersCount,
|
|
||||||
FollowingCount: user.FollowingCount,
|
|
||||||
IsVerified: user.IsVerified,
|
|
||||||
CreatedAt: FormatTime(user.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUsersToResponse 将User列表转换为响应列表
|
|
||||||
func ConvertUsersToResponse(users []*model.User) []*UserResponse {
|
|
||||||
result := make([]*UserResponse, 0, len(users))
|
|
||||||
for _, user := range users {
|
|
||||||
result = append(result, ConvertUserToResponse(user))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUsersToResponseWithMutualFollow 将User列表转换为响应列表(包含双向关注状态)
|
|
||||||
// followingStatusMap: key是用户ID,value是[isFollowing, isFollowingMe]
|
|
||||||
func ConvertUsersToResponseWithMutualFollow(users []*model.User, followingStatusMap map[string][2]bool) []*UserResponse {
|
|
||||||
result := make([]*UserResponse, 0, len(users))
|
|
||||||
for _, user := range users {
|
|
||||||
status, ok := followingStatusMap[user.ID]
|
|
||||||
if ok {
|
|
||||||
result = append(result, ConvertUserToResponseWithMutualFollow(user, status[0], status[1]))
|
|
||||||
} else {
|
|
||||||
result = append(result, ConvertUserToResponse(user))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertUsersToResponseWithMutualFollowAndPostsCount 将User列表转换为响应列表(包含双向关注状态和实时计算的帖子数量)
|
|
||||||
// followingStatusMap: key是用户ID,value是[isFollowing, isFollowingMe]
|
|
||||||
// postsCountMap: key是用户ID,value是帖子数量
|
|
||||||
func ConvertUsersToResponseWithMutualFollowAndPostsCount(users []*model.User, followingStatusMap map[string][2]bool, postsCountMap map[string]int64) []*UserResponse {
|
|
||||||
result := make([]*UserResponse, 0, len(users))
|
|
||||||
for _, user := range users {
|
|
||||||
status, hasStatus := followingStatusMap[user.ID]
|
|
||||||
postsCount, hasPostsCount := postsCountMap[user.ID]
|
|
||||||
|
|
||||||
// 如果没有帖子数量,使用数据库中的值
|
|
||||||
if !hasPostsCount {
|
|
||||||
postsCount = int64(user.PostsCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hasStatus {
|
|
||||||
result = append(result, ConvertUserToResponseWithMutualFollowAndPostsCount(user, status[0], status[1], int(postsCount)))
|
|
||||||
} else {
|
|
||||||
result = append(result, ConvertUserToResponseWithPostsCount(user, int(postsCount)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Post 转换 ====================
|
|
||||||
|
|
||||||
// ConvertPostImageToResponse 将PostImage转换为PostImageResponse
|
|
||||||
func ConvertPostImageToResponse(img *model.PostImage) PostImageResponse {
|
|
||||||
if img == nil {
|
|
||||||
return PostImageResponse{}
|
|
||||||
}
|
|
||||||
return PostImageResponse{
|
|
||||||
ID: img.ID,
|
|
||||||
URL: img.URL,
|
|
||||||
ThumbnailURL: img.ThumbnailURL,
|
|
||||||
Width: img.Width,
|
|
||||||
Height: img.Height,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertPostImagesToResponse 将PostImage列表转换为响应列表
|
|
||||||
func ConvertPostImagesToResponse(images []model.PostImage) []PostImageResponse {
|
|
||||||
result := make([]PostImageResponse, 0, len(images))
|
|
||||||
for i := range images {
|
|
||||||
result = append(result, ConvertPostImageToResponse(&images[i]))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertPostToResponse 将Post转换为PostResponse(列表用)
|
|
||||||
func ConvertPostToResponse(post *model.Post, isLiked, isFavorited bool) *PostResponse {
|
|
||||||
if post == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
images := make([]PostImageResponse, 0)
|
|
||||||
for _, img := range post.Images {
|
|
||||||
images = append(images, ConvertPostImageToResponse(&img))
|
|
||||||
}
|
|
||||||
|
|
||||||
var author *UserResponse
|
|
||||||
if post.User != nil {
|
|
||||||
author = ConvertUserToResponse(post.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PostResponse{
|
|
||||||
ID: post.ID,
|
|
||||||
UserID: post.UserID,
|
|
||||||
Title: post.Title,
|
|
||||||
Content: post.Content,
|
|
||||||
Images: images,
|
|
||||||
Status: string(post.Status),
|
|
||||||
LikesCount: post.LikesCount,
|
|
||||||
CommentsCount: post.CommentsCount,
|
|
||||||
FavoritesCount: post.FavoritesCount,
|
|
||||||
SharesCount: post.SharesCount,
|
|
||||||
ViewsCount: post.ViewsCount,
|
|
||||||
IsPinned: post.IsPinned,
|
|
||||||
IsLocked: post.IsLocked,
|
|
||||||
IsVote: post.IsVote,
|
|
||||||
CreatedAt: FormatTime(post.CreatedAt),
|
|
||||||
UpdatedAt: FormatTime(post.UpdatedAt),
|
|
||||||
Author: author,
|
|
||||||
IsLiked: isLiked,
|
|
||||||
IsFavorited: isFavorited,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertPostToDetailResponse 将Post转换为PostDetailResponse
|
|
||||||
func ConvertPostToDetailResponse(post *model.Post, isLiked, isFavorited bool) *PostDetailResponse {
|
|
||||||
if post == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
images := make([]PostImageResponse, 0)
|
|
||||||
for _, img := range post.Images {
|
|
||||||
images = append(images, ConvertPostImageToResponse(&img))
|
|
||||||
}
|
|
||||||
|
|
||||||
var author *UserResponse
|
|
||||||
if post.User != nil {
|
|
||||||
author = ConvertUserToResponse(post.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &PostDetailResponse{
|
|
||||||
ID: post.ID,
|
|
||||||
UserID: post.UserID,
|
|
||||||
Title: post.Title,
|
|
||||||
Content: post.Content,
|
|
||||||
Images: images,
|
|
||||||
Status: string(post.Status),
|
|
||||||
LikesCount: post.LikesCount,
|
|
||||||
CommentsCount: post.CommentsCount,
|
|
||||||
FavoritesCount: post.FavoritesCount,
|
|
||||||
SharesCount: post.SharesCount,
|
|
||||||
ViewsCount: post.ViewsCount,
|
|
||||||
IsPinned: post.IsPinned,
|
|
||||||
IsLocked: post.IsLocked,
|
|
||||||
IsVote: post.IsVote,
|
|
||||||
CreatedAt: FormatTime(post.CreatedAt),
|
|
||||||
UpdatedAt: FormatTime(post.UpdatedAt),
|
|
||||||
Author: author,
|
|
||||||
IsLiked: isLiked,
|
|
||||||
IsFavorited: isFavorited,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertPostsToResponse 将Post列表转换为响应列表(每个帖子独立检查点赞/收藏状态)
|
|
||||||
func ConvertPostsToResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse {
|
|
||||||
result := make([]*PostResponse, 0, len(posts))
|
|
||||||
for _, post := range posts {
|
|
||||||
isLiked := false
|
|
||||||
isFavorited := false
|
|
||||||
if isLikedMap != nil {
|
|
||||||
isLiked = isLikedMap[post.ID]
|
|
||||||
}
|
|
||||||
if isFavoritedMap != nil {
|
|
||||||
isFavorited = isFavoritedMap[post.ID]
|
|
||||||
}
|
|
||||||
result = append(result, ConvertPostToResponse(post, isLiked, isFavorited))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildPostResponse 构建单个帖子响应(包含交互状态)
|
|
||||||
// 这是一个语义化的辅助函数,便于 Handler 层调用
|
|
||||||
func BuildPostResponse(post *model.Post, isLiked, isFavorited bool) *PostResponse {
|
|
||||||
return ConvertPostToResponse(post, isLiked, isFavorited)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildPostsWithInteractionResponse 批量构建帖子响应(包含交互状态)
|
|
||||||
// 这是一个语义化的辅助函数,便于 Handler 层调用
|
|
||||||
func BuildPostsWithInteractionResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse {
|
|
||||||
return ConvertPostsToResponse(posts, isLikedMap, isFavoritedMap)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Comment 转换 ====================
|
|
||||||
|
|
||||||
// ConvertCommentToResponse 将Comment转换为CommentResponse
|
|
||||||
func ConvertCommentToResponse(comment *model.Comment, isLiked bool) *CommentResponse {
|
|
||||||
if comment == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var author *UserResponse
|
|
||||||
if comment.User != nil {
|
|
||||||
author = ConvertUserToResponse(comment.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换子回复(扁平化结构)
|
|
||||||
var replies []*CommentResponse
|
|
||||||
if len(comment.Replies) > 0 {
|
|
||||||
replies = make([]*CommentResponse, 0, len(comment.Replies))
|
|
||||||
for _, reply := range comment.Replies {
|
|
||||||
replies = append(replies, ConvertCommentToResponse(reply, false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TargetID 就是 ParentID,前端根据这个 ID 找到被回复用户的昵称
|
|
||||||
var targetID *string
|
|
||||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
|
||||||
targetID = comment.ParentID
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析图片JSON
|
|
||||||
var images []CommentImageResponse
|
|
||||||
if comment.Images != "" {
|
|
||||||
var urlList []string
|
|
||||||
if err := json.Unmarshal([]byte(comment.Images), &urlList); err == nil {
|
|
||||||
images = make([]CommentImageResponse, 0, len(urlList))
|
|
||||||
for _, url := range urlList {
|
|
||||||
images = append(images, CommentImageResponse{URL: url})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CommentResponse{
|
|
||||||
ID: comment.ID,
|
|
||||||
PostID: comment.PostID,
|
|
||||||
UserID: comment.UserID,
|
|
||||||
ParentID: comment.ParentID,
|
|
||||||
RootID: comment.RootID,
|
|
||||||
Content: comment.Content,
|
|
||||||
Images: images,
|
|
||||||
LikesCount: comment.LikesCount,
|
|
||||||
RepliesCount: comment.RepliesCount,
|
|
||||||
CreatedAt: FormatTime(comment.CreatedAt),
|
|
||||||
Author: author,
|
|
||||||
IsLiked: isLiked,
|
|
||||||
TargetID: targetID,
|
|
||||||
Replies: replies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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 点赞状态检查器接口
|
|
||||||
type IsLikedChecker interface {
|
|
||||||
IsLiked(ctx context.Context, commentID, userID string) bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertCommentToResponseWithUser 将Comment转换为CommentResponse(根据用户ID检查点赞状态)
|
|
||||||
func ConvertCommentToResponseWithUser(comment *model.Comment, userID string, checker IsLikedChecker) *CommentResponse {
|
|
||||||
if comment == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查当前用户是否点赞了该评论
|
|
||||||
isLiked := false
|
|
||||||
if userID != "" && checker != nil {
|
|
||||||
isLiked = checker.IsLiked(context.Background(), comment.ID, userID)
|
|
||||||
}
|
|
||||||
|
|
||||||
var author *UserResponse
|
|
||||||
if comment.User != nil {
|
|
||||||
author = ConvertUserToResponse(comment.User)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换子回复(扁平化结构),递归检查点赞状态
|
|
||||||
var replies []*CommentResponse
|
|
||||||
if len(comment.Replies) > 0 {
|
|
||||||
replies = make([]*CommentResponse, 0, len(comment.Replies))
|
|
||||||
for _, reply := range comment.Replies {
|
|
||||||
replies = append(replies, ConvertCommentToResponseWithUser(reply, userID, checker))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TargetID 就是 ParentID,前端根据这个 ID 找到被回复用户的昵称
|
|
||||||
var targetID *string
|
|
||||||
if comment.ParentID != nil && *comment.ParentID != "" {
|
|
||||||
targetID = comment.ParentID
|
|
||||||
}
|
|
||||||
|
|
||||||
// 解析图片JSON
|
|
||||||
var images []CommentImageResponse
|
|
||||||
if comment.Images != "" {
|
|
||||||
var urlList []string
|
|
||||||
if err := json.Unmarshal([]byte(comment.Images), &urlList); err == nil {
|
|
||||||
images = make([]CommentImageResponse, 0, len(urlList))
|
|
||||||
for _, url := range urlList {
|
|
||||||
images = append(images, CommentImageResponse{URL: url})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &CommentResponse{
|
|
||||||
ID: comment.ID,
|
|
||||||
PostID: comment.PostID,
|
|
||||||
UserID: comment.UserID,
|
|
||||||
ParentID: comment.ParentID,
|
|
||||||
RootID: comment.RootID,
|
|
||||||
Content: comment.Content,
|
|
||||||
Images: images,
|
|
||||||
LikesCount: comment.LikesCount,
|
|
||||||
RepliesCount: comment.RepliesCount,
|
|
||||||
CreatedAt: FormatTime(comment.CreatedAt),
|
|
||||||
Author: author,
|
|
||||||
IsLiked: isLiked,
|
|
||||||
TargetID: targetID,
|
|
||||||
Replies: replies,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertCommentsToResponseWithUser 将Comment列表转换为响应列表(根据用户ID检查点赞状态)
|
|
||||||
func ConvertCommentsToResponseWithUser(comments []*model.Comment, userID string, checker IsLikedChecker) []*CommentResponse {
|
|
||||||
result := make([]*CommentResponse, 0, len(comments))
|
|
||||||
for _, comment := range comments {
|
|
||||||
result = append(result, ConvertCommentToResponseWithUser(comment, userID, checker))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Notification 转换 ====================
|
|
||||||
|
|
||||||
// ConvertNotificationToResponse 将Notification转换为NotificationResponse
|
|
||||||
func ConvertNotificationToResponse(notification *model.Notification) *NotificationResponse {
|
|
||||||
if notification == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &NotificationResponse{
|
|
||||||
ID: notification.ID,
|
|
||||||
UserID: notification.UserID,
|
|
||||||
Type: string(notification.Type),
|
|
||||||
Title: notification.Title,
|
|
||||||
Content: notification.Content,
|
|
||||||
Data: notification.Data,
|
|
||||||
IsRead: notification.IsRead,
|
|
||||||
CreatedAt: FormatTime(notification.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertNotificationsToResponse 将Notification列表转换为响应列表
|
|
||||||
func ConvertNotificationsToResponse(notifications []*model.Notification) []*NotificationResponse {
|
|
||||||
result := make([]*NotificationResponse, 0, len(notifications))
|
|
||||||
for _, n := range notifications {
|
|
||||||
result = append(result, ConvertNotificationToResponse(n))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Message 转换 ====================
|
|
||||||
|
|
||||||
// ConvertMessageToResponse 将Message转换为MessageResponse
|
|
||||||
func ConvertMessageToResponse(message *model.Message) *MessageResponse {
|
|
||||||
if message == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 直接使用 segments,不需要解析
|
|
||||||
segments := make(model.MessageSegments, len(message.Segments))
|
|
||||||
for i, seg := range message.Segments {
|
|
||||||
segments[i] = model.MessageSegment{
|
|
||||||
Type: seg.Type,
|
|
||||||
Data: seg.Data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &MessageResponse{
|
|
||||||
ID: message.ID,
|
|
||||||
ConversationID: message.ConversationID,
|
|
||||||
SenderID: message.SenderID,
|
|
||||||
Seq: message.Seq,
|
|
||||||
Segments: segments,
|
|
||||||
ReplyToID: message.ReplyToID,
|
|
||||||
Status: string(message.Status),
|
|
||||||
Category: string(message.Category),
|
|
||||||
CreatedAt: FormatTime(message.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertConversationToResponse 将Conversation转换为ConversationResponse
|
|
||||||
// participants: 会话参与者列表(用户信息)
|
|
||||||
// unreadCount: 当前用户的未读消息数
|
|
||||||
// lastMessage: 最后一条消息
|
|
||||||
func ConvertConversationToResponse(conv *model.Conversation, participants []*model.User, unreadCount int, lastMessage *model.Message, isPinned bool) *ConversationResponse {
|
|
||||||
if conv == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var participantResponses []*UserResponse
|
|
||||||
for _, p := range participants {
|
|
||||||
participantResponses = append(participantResponses, ConvertUserToResponse(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换群组信息
|
|
||||||
var groupResponse *GroupResponse
|
|
||||||
if conv.Group != nil {
|
|
||||||
groupResponse = GroupToResponse(conv.Group)
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ConversationResponse{
|
|
||||||
ID: conv.ID,
|
|
||||||
Type: string(conv.Type),
|
|
||||||
IsPinned: isPinned,
|
|
||||||
Group: groupResponse,
|
|
||||||
LastSeq: conv.LastSeq,
|
|
||||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
|
||||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
|
||||||
UnreadCount: unreadCount,
|
|
||||||
Participants: participantResponses,
|
|
||||||
CreatedAt: FormatTime(conv.CreatedAt),
|
|
||||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertConversationToDetailResponse 将Conversation转换为ConversationDetailResponse
|
|
||||||
func ConvertConversationToDetailResponse(conv *model.Conversation, participants []*model.User, unreadCount int64, lastMessage *model.Message, myLastReadSeq int64, otherLastReadSeq int64, isPinned bool) *ConversationDetailResponse {
|
|
||||||
if conv == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var participantResponses []*UserResponse
|
|
||||||
for _, p := range participants {
|
|
||||||
participantResponses = append(participantResponses, ConvertUserToResponse(p))
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ConversationDetailResponse{
|
|
||||||
ID: conv.ID,
|
|
||||||
Type: string(conv.Type),
|
|
||||||
IsPinned: isPinned,
|
|
||||||
LastSeq: conv.LastSeq,
|
|
||||||
LastMessage: ConvertMessageToResponse(lastMessage),
|
|
||||||
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
|
||||||
UnreadCount: unreadCount,
|
|
||||||
Participants: participantResponses,
|
|
||||||
MyLastReadSeq: myLastReadSeq,
|
|
||||||
OtherLastReadSeq: otherLastReadSeq,
|
|
||||||
CreatedAt: FormatTime(conv.CreatedAt),
|
|
||||||
UpdatedAt: FormatTime(conv.UpdatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ConvertMessagesToResponse 将Message列表转换为响应列表
|
|
||||||
func ConvertMessagesToResponse(messages []*model.Message) []*MessageResponse {
|
|
||||||
result := make([]*MessageResponse, 0, len(messages))
|
|
||||||
for _, msg := range messages {
|
|
||||||
result = append(result, ConvertMessageToResponse(msg))
|
|
||||||
}
|
|
||||||
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))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== PushRecord 转换 ====================
|
|
||||||
|
|
||||||
// PushRecordToResponse 将PushRecord转换为PushRecordResponse
|
|
||||||
func PushRecordToResponse(record *model.PushRecord) *PushRecordResponse {
|
|
||||||
if record == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resp := &PushRecordResponse{
|
|
||||||
ID: record.ID,
|
|
||||||
MessageID: record.MessageID,
|
|
||||||
PushChannel: string(record.PushChannel),
|
|
||||||
PushStatus: string(record.PushStatus),
|
|
||||||
RetryCount: record.RetryCount,
|
|
||||||
CreatedAt: record.CreatedAt,
|
|
||||||
}
|
|
||||||
if record.PushedAt != nil {
|
|
||||||
resp.PushedAt = *record.PushedAt
|
|
||||||
}
|
|
||||||
if record.DeliveredAt != nil {
|
|
||||||
resp.DeliveredAt = *record.DeliveredAt
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// PushRecordsToResponse 将PushRecord列表转换为响应列表
|
|
||||||
func PushRecordsToResponse(records []*model.PushRecord) []*PushRecordResponse {
|
|
||||||
result := make([]*PushRecordResponse, 0, len(records))
|
|
||||||
for _, record := range records {
|
|
||||||
result = append(result, PushRecordToResponse(record))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== DeviceToken 转换 ====================
|
|
||||||
|
|
||||||
// DeviceTokenToResponse 将DeviceToken转换为DeviceTokenResponse
|
|
||||||
func DeviceTokenToResponse(token *model.DeviceToken) *DeviceTokenResponse {
|
|
||||||
if token == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resp := &DeviceTokenResponse{
|
|
||||||
ID: token.ID,
|
|
||||||
DeviceID: token.DeviceID,
|
|
||||||
DeviceType: string(token.DeviceType),
|
|
||||||
IsActive: token.IsActive,
|
|
||||||
DeviceName: token.DeviceName,
|
|
||||||
CreatedAt: token.CreatedAt,
|
|
||||||
}
|
|
||||||
if token.LastUsedAt != nil {
|
|
||||||
resp.LastUsedAt = *token.LastUsedAt
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
// DeviceTokensToResponse 将DeviceToken列表转换为响应列表
|
|
||||||
func DeviceTokensToResponse(tokens []*model.DeviceToken) []*DeviceTokenResponse {
|
|
||||||
result := make([]*DeviceTokenResponse, 0, len(tokens))
|
|
||||||
for _, token := range tokens {
|
|
||||||
result = append(result, DeviceTokenToResponse(token))
|
|
||||||
}
|
|
||||||
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]interface{}{
|
|
||||||
"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
|
|
||||||
func SystemNotificationToResponse(n *model.SystemNotification) *SystemMessageResponse {
|
|
||||||
if n == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
resp := &SystemMessageResponse{
|
|
||||||
ID: strconv.FormatInt(n.ID, 10),
|
|
||||||
SenderID: model.SystemSenderIDStr, // 系统发送者
|
|
||||||
ReceiverID: n.ReceiverID,
|
|
||||||
Content: n.Content,
|
|
||||||
Category: "notification",
|
|
||||||
SystemType: string(n.Type),
|
|
||||||
IsRead: n.IsRead,
|
|
||||||
CreatedAt: n.CreatedAt,
|
|
||||||
}
|
|
||||||
if n.ExtraData != nil {
|
|
||||||
resp.ExtraData = map[string]interface{}{
|
|
||||||
"actor_id": n.ExtraData.ActorID,
|
|
||||||
"actor_id_str": n.ExtraData.ActorIDStr,
|
|
||||||
"actor_name": n.ExtraData.ActorName,
|
|
||||||
"avatar_url": n.ExtraData.AvatarURL,
|
|
||||||
"target_id": n.ExtraData.TargetID,
|
|
||||||
"target_title": n.ExtraData.TargetTitle,
|
|
||||||
"target_type": n.ExtraData.TargetType,
|
|
||||||
"action_url": n.ExtraData.ActionURL,
|
|
||||||
"action_time": n.ExtraData.ActionTime,
|
|
||||||
"group_id": n.ExtraData.GroupID,
|
|
||||||
"group_name": n.ExtraData.GroupName,
|
|
||||||
"group_avatar": n.ExtraData.GroupAvatar,
|
|
||||||
"group_description": n.ExtraData.GroupDescription,
|
|
||||||
"flag": n.ExtraData.Flag,
|
|
||||||
"request_type": n.ExtraData.RequestType,
|
|
||||||
"request_status": n.ExtraData.RequestStatus,
|
|
||||||
"reason": n.ExtraData.Reason,
|
|
||||||
"target_user_id": n.ExtraData.TargetUserID,
|
|
||||||
"target_user_name": n.ExtraData.TargetUserName,
|
|
||||||
"target_user_avatar": n.ExtraData.TargetUserAvatar,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// ==================== Group 转换 ====================
|
|
||||||
|
|
||||||
// GroupToResponse 将Group转换为GroupResponse
|
|
||||||
func GroupToResponse(group *model.Group) *GroupResponse {
|
|
||||||
if group == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &GroupResponse{
|
|
||||||
ID: group.ID,
|
|
||||||
Name: group.Name,
|
|
||||||
Avatar: group.Avatar,
|
|
||||||
Description: group.Description,
|
|
||||||
OwnerID: group.OwnerID,
|
|
||||||
MemberCount: group.MemberCount,
|
|
||||||
MaxMembers: group.MaxMembers,
|
|
||||||
JoinType: int(group.JoinType),
|
|
||||||
MuteAll: group.MuteAll,
|
|
||||||
CreatedAt: FormatTime(group.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupsToResponse 将Group列表转换为GroupResponse列表
|
|
||||||
func GroupsToResponse(groups []model.Group) []*GroupResponse {
|
|
||||||
result := make([]*GroupResponse, 0, len(groups))
|
|
||||||
for i := range groups {
|
|
||||||
result = append(result, GroupToResponse(&groups[i]))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupMemberToResponse 将GroupMember转换为GroupMemberResponse
|
|
||||||
func GroupMemberToResponse(member *model.GroupMember) *GroupMemberResponse {
|
|
||||||
if member == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &GroupMemberResponse{
|
|
||||||
ID: member.ID,
|
|
||||||
GroupID: member.GroupID,
|
|
||||||
UserID: member.UserID,
|
|
||||||
Role: member.Role,
|
|
||||||
Nickname: member.Nickname,
|
|
||||||
Muted: member.Muted,
|
|
||||||
JoinTime: FormatTime(member.JoinTime),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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
|
|
||||||
func GroupAnnouncementToResponse(announcement *model.GroupAnnouncement) *GroupAnnouncementResponse {
|
|
||||||
if announcement == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return &GroupAnnouncementResponse{
|
|
||||||
ID: announcement.ID,
|
|
||||||
GroupID: announcement.GroupID,
|
|
||||||
Content: announcement.Content,
|
|
||||||
AuthorID: announcement.AuthorID,
|
|
||||||
IsPinned: announcement.IsPinned,
|
|
||||||
CreatedAt: FormatTime(announcement.CreatedAt),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GroupAnnouncementsToResponse 将GroupAnnouncement列表转换为GroupAnnouncementResponse列表
|
|
||||||
func GroupAnnouncementsToResponse(announcements []model.GroupAnnouncement) []*GroupAnnouncementResponse {
|
|
||||||
result := make([]*GroupAnnouncementResponse, 0, len(announcements))
|
|
||||||
for i := range announcements {
|
|
||||||
result = append(result, GroupAnnouncementToResponse(&announcements[i]))
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|||||||
96
internal/dto/group_converter.go
Normal file
96
internal/dto/group_converter.go
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Group 转换 ====================
|
||||||
|
|
||||||
|
// GroupToResponse 将Group转换为GroupResponse
|
||||||
|
func GroupToResponse(group *model.Group) *GroupResponse {
|
||||||
|
if group == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &GroupResponse{
|
||||||
|
ID: group.ID,
|
||||||
|
Name: group.Name,
|
||||||
|
Avatar: group.Avatar,
|
||||||
|
Description: group.Description,
|
||||||
|
OwnerID: group.OwnerID,
|
||||||
|
MemberCount: group.MemberCount,
|
||||||
|
MaxMembers: group.MaxMembers,
|
||||||
|
JoinType: int(group.JoinType),
|
||||||
|
MuteAll: group.MuteAll,
|
||||||
|
CreatedAt: FormatTime(group.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupsToResponse 将Group列表转换为GroupResponse列表
|
||||||
|
func GroupsToResponse(groups []model.Group) []*GroupResponse {
|
||||||
|
result := make([]*GroupResponse, 0, len(groups))
|
||||||
|
for i := range groups {
|
||||||
|
result = append(result, GroupToResponse(&groups[i]))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupMemberToResponse 将GroupMember转换为GroupMemberResponse
|
||||||
|
func GroupMemberToResponse(member *model.GroupMember) *GroupMemberResponse {
|
||||||
|
if member == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &GroupMemberResponse{
|
||||||
|
ID: member.ID,
|
||||||
|
GroupID: member.GroupID,
|
||||||
|
UserID: member.UserID,
|
||||||
|
Role: member.Role,
|
||||||
|
Nickname: member.Nickname,
|
||||||
|
Muted: member.Muted,
|
||||||
|
JoinTime: FormatTime(member.JoinTime),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func GroupAnnouncementToResponse(announcement *model.GroupAnnouncement) *GroupAnnouncementResponse {
|
||||||
|
if announcement == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &GroupAnnouncementResponse{
|
||||||
|
ID: announcement.ID,
|
||||||
|
GroupID: announcement.GroupID,
|
||||||
|
Content: announcement.Content,
|
||||||
|
AuthorID: announcement.AuthorID,
|
||||||
|
IsPinned: announcement.IsPinned,
|
||||||
|
CreatedAt: FormatTime(announcement.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GroupAnnouncementsToResponse 将GroupAnnouncement列表转换为GroupAnnouncementResponse列表
|
||||||
|
func GroupAnnouncementsToResponse(announcements []model.GroupAnnouncement) []*GroupAnnouncementResponse {
|
||||||
|
result := make([]*GroupAnnouncementResponse, 0, len(announcements))
|
||||||
|
for i := range announcements {
|
||||||
|
result = append(result, GroupAnnouncementToResponse(&announcements[i]))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
178
internal/dto/message_converter.go
Normal file
178
internal/dto/message_converter.go
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Message 转换 ====================
|
||||||
|
|
||||||
|
// ConvertMessageToResponse 将Message转换为MessageResponse
|
||||||
|
func ConvertMessageToResponse(message *model.Message) *MessageResponse {
|
||||||
|
if message == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 直接使用 segments,不需要解析
|
||||||
|
segments := make(model.MessageSegments, len(message.Segments))
|
||||||
|
for i, seg := range message.Segments {
|
||||||
|
segments[i] = model.MessageSegment{
|
||||||
|
Type: seg.Type,
|
||||||
|
Data: seg.Data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MessageResponse{
|
||||||
|
ID: message.ID,
|
||||||
|
ConversationID: message.ConversationID,
|
||||||
|
SenderID: message.SenderID,
|
||||||
|
Seq: message.Seq,
|
||||||
|
Segments: segments,
|
||||||
|
ReplyToID: message.ReplyToID,
|
||||||
|
Status: string(message.Status),
|
||||||
|
Category: string(message.Category),
|
||||||
|
CreatedAt: FormatTime(message.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertConversationToResponse 将Conversation转换为ConversationResponse
|
||||||
|
// participants: 会话参与者列表(用户信息)
|
||||||
|
// unreadCount: 当前用户的未读消息数
|
||||||
|
// lastMessage: 最后一条消息
|
||||||
|
func ConvertConversationToResponse(conv *model.Conversation, participants []*model.User, unreadCount int, lastMessage *model.Message, isPinned bool) *ConversationResponse {
|
||||||
|
if conv == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var participantResponses []*UserResponse
|
||||||
|
for _, p := range participants {
|
||||||
|
participantResponses = append(participantResponses, ConvertUserToResponse(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换群组信息
|
||||||
|
var groupResponse *GroupResponse
|
||||||
|
if conv.Group != nil {
|
||||||
|
groupResponse = GroupToResponse(conv.Group)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ConversationResponse{
|
||||||
|
ID: conv.ID,
|
||||||
|
Type: string(conv.Type),
|
||||||
|
IsPinned: isPinned,
|
||||||
|
Group: groupResponse,
|
||||||
|
LastSeq: conv.LastSeq,
|
||||||
|
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||||
|
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||||
|
UnreadCount: unreadCount,
|
||||||
|
Participants: participantResponses,
|
||||||
|
CreatedAt: FormatTime(conv.CreatedAt),
|
||||||
|
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertConversationToDetailResponse 将Conversation转换为ConversationDetailResponse
|
||||||
|
func ConvertConversationToDetailResponse(conv *model.Conversation, participants []*model.User, unreadCount int64, lastMessage *model.Message, myLastReadSeq int64, otherLastReadSeq int64, isPinned bool) *ConversationDetailResponse {
|
||||||
|
if conv == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var participantResponses []*UserResponse
|
||||||
|
for _, p := range participants {
|
||||||
|
participantResponses = append(participantResponses, ConvertUserToResponse(p))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ConversationDetailResponse{
|
||||||
|
ID: conv.ID,
|
||||||
|
Type: string(conv.Type),
|
||||||
|
IsPinned: isPinned,
|
||||||
|
LastSeq: conv.LastSeq,
|
||||||
|
LastMessage: ConvertMessageToResponse(lastMessage),
|
||||||
|
LastMessageAt: FormatTimePointer(conv.LastMsgTime),
|
||||||
|
UnreadCount: unreadCount,
|
||||||
|
Participants: participantResponses,
|
||||||
|
MyLastReadSeq: myLastReadSeq,
|
||||||
|
OtherLastReadSeq: otherLastReadSeq,
|
||||||
|
CreatedAt: FormatTime(conv.CreatedAt),
|
||||||
|
UpdatedAt: FormatTime(conv.UpdatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertMessagesToResponse 将Message列表转换为响应列表
|
||||||
|
func ConvertMessagesToResponse(messages []*model.Message) []*MessageResponse {
|
||||||
|
result := make([]*MessageResponse, 0, len(messages))
|
||||||
|
for _, msg := range messages {
|
||||||
|
result = append(result, ConvertMessageToResponse(msg))
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== PushRecord 转换 ====================
|
||||||
|
|
||||||
|
// PushRecordToResponse 将PushRecord转换为PushRecordResponse
|
||||||
|
func PushRecordToResponse(record *model.PushRecord) *PushRecordResponse {
|
||||||
|
if record == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := &PushRecordResponse{
|
||||||
|
ID: record.ID,
|
||||||
|
MessageID: record.MessageID,
|
||||||
|
PushChannel: string(record.PushChannel),
|
||||||
|
PushStatus: string(record.PushStatus),
|
||||||
|
RetryCount: record.RetryCount,
|
||||||
|
CreatedAt: record.CreatedAt,
|
||||||
|
}
|
||||||
|
if record.PushedAt != nil {
|
||||||
|
resp.PushedAt = *record.PushedAt
|
||||||
|
}
|
||||||
|
if record.DeliveredAt != nil {
|
||||||
|
resp.DeliveredAt = *record.DeliveredAt
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushRecordsToResponse 将PushRecord列表转换为响应列表
|
||||||
|
func PushRecordsToResponse(records []*model.PushRecord) []*PushRecordResponse {
|
||||||
|
result := make([]*PushRecordResponse, 0, len(records))
|
||||||
|
for _, record := range records {
|
||||||
|
result = append(result, PushRecordToResponse(record))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== DeviceToken 转换 ====================
|
||||||
|
|
||||||
|
// DeviceTokenToResponse 将DeviceToken转换为DeviceTokenResponse
|
||||||
|
func DeviceTokenToResponse(token *model.DeviceToken) *DeviceTokenResponse {
|
||||||
|
if token == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := &DeviceTokenResponse{
|
||||||
|
ID: token.ID,
|
||||||
|
DeviceID: token.DeviceID,
|
||||||
|
DeviceType: string(token.DeviceType),
|
||||||
|
IsActive: token.IsActive,
|
||||||
|
DeviceName: token.DeviceName,
|
||||||
|
CreatedAt: token.CreatedAt,
|
||||||
|
}
|
||||||
|
if token.LastUsedAt != nil {
|
||||||
|
resp.LastUsedAt = *token.LastUsedAt
|
||||||
|
}
|
||||||
|
return resp
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeviceTokensToResponse 将DeviceToken列表转换为响应列表
|
||||||
|
func DeviceTokensToResponse(tokens []*model.DeviceToken) []*DeviceTokenResponse {
|
||||||
|
result := make([]*DeviceTokenResponse, 0, len(tokens))
|
||||||
|
for _, token := range tokens {
|
||||||
|
result = append(result, DeviceTokenToResponse(token))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
128
internal/dto/notification_converter.go
Normal file
128
internal/dto/notification_converter.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Notification 转换 ====================
|
||||||
|
|
||||||
|
// ConvertNotificationToResponse 将Notification转换为NotificationResponse
|
||||||
|
func ConvertNotificationToResponse(notification *model.Notification) *NotificationResponse {
|
||||||
|
if notification == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &NotificationResponse{
|
||||||
|
ID: notification.ID,
|
||||||
|
UserID: notification.UserID,
|
||||||
|
Type: string(notification.Type),
|
||||||
|
Title: notification.Title,
|
||||||
|
Content: notification.Content,
|
||||||
|
Data: notification.Data,
|
||||||
|
IsRead: notification.IsRead,
|
||||||
|
CreatedAt: FormatTime(notification.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertNotificationsToResponse 将Notification列表转换为响应列表
|
||||||
|
func ConvertNotificationsToResponse(notifications []*model.Notification) []*NotificationResponse {
|
||||||
|
result := make([]*NotificationResponse, 0, len(notifications))
|
||||||
|
for _, n := range notifications {
|
||||||
|
result = append(result, ConvertNotificationToResponse(n))
|
||||||
|
}
|
||||||
|
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]interface{}{
|
||||||
|
"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
|
||||||
|
func SystemNotificationToResponse(n *model.SystemNotification) *SystemMessageResponse {
|
||||||
|
if n == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
resp := &SystemMessageResponse{
|
||||||
|
ID: strconv.FormatInt(n.ID, 10),
|
||||||
|
SenderID: model.SystemSenderIDStr, // 系统发送者
|
||||||
|
ReceiverID: n.ReceiverID,
|
||||||
|
Content: n.Content,
|
||||||
|
Category: "notification",
|
||||||
|
SystemType: string(n.Type),
|
||||||
|
IsRead: n.IsRead,
|
||||||
|
CreatedAt: n.CreatedAt,
|
||||||
|
}
|
||||||
|
if n.ExtraData != nil {
|
||||||
|
resp.ExtraData = map[string]interface{}{
|
||||||
|
"actor_id": n.ExtraData.ActorID,
|
||||||
|
"actor_id_str": n.ExtraData.ActorIDStr,
|
||||||
|
"actor_name": n.ExtraData.ActorName,
|
||||||
|
"avatar_url": n.ExtraData.AvatarURL,
|
||||||
|
"target_id": n.ExtraData.TargetID,
|
||||||
|
"target_title": n.ExtraData.TargetTitle,
|
||||||
|
"target_type": n.ExtraData.TargetType,
|
||||||
|
"action_url": n.ExtraData.ActionURL,
|
||||||
|
"action_time": n.ExtraData.ActionTime,
|
||||||
|
"group_id": n.ExtraData.GroupID,
|
||||||
|
"group_name": n.ExtraData.GroupName,
|
||||||
|
"group_avatar": n.ExtraData.GroupAvatar,
|
||||||
|
"group_description": n.ExtraData.GroupDescription,
|
||||||
|
"flag": n.ExtraData.Flag,
|
||||||
|
"request_type": n.ExtraData.RequestType,
|
||||||
|
"request_status": n.ExtraData.RequestStatus,
|
||||||
|
"reason": n.ExtraData.Reason,
|
||||||
|
"target_user_id": n.ExtraData.TargetUserID,
|
||||||
|
"target_user_name": n.ExtraData.TargetUserName,
|
||||||
|
"target_user_avatar": n.ExtraData.TargetUserAvatar,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
282
internal/dto/post_converter.go
Normal file
282
internal/dto/post_converter.go
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== Post 转换 ====================
|
||||||
|
|
||||||
|
// ConvertPostImageToResponse 将PostImage转换为PostImageResponse
|
||||||
|
func ConvertPostImageToResponse(img *model.PostImage) PostImageResponse {
|
||||||
|
if img == nil {
|
||||||
|
return PostImageResponse{}
|
||||||
|
}
|
||||||
|
return PostImageResponse{
|
||||||
|
ID: img.ID,
|
||||||
|
URL: img.URL,
|
||||||
|
ThumbnailURL: img.ThumbnailURL,
|
||||||
|
Width: img.Width,
|
||||||
|
Height: img.Height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertPostImagesToResponse 将PostImage列表转换为响应列表
|
||||||
|
func ConvertPostImagesToResponse(images []model.PostImage) []PostImageResponse {
|
||||||
|
result := make([]PostImageResponse, 0, len(images))
|
||||||
|
for i := range images {
|
||||||
|
result = append(result, ConvertPostImageToResponse(&images[i]))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertPostToResponse 将Post转换为PostResponse(列表用)
|
||||||
|
func ConvertPostToResponse(post *model.Post, isLiked, isFavorited bool) *PostResponse {
|
||||||
|
if post == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
images := make([]PostImageResponse, 0)
|
||||||
|
for _, img := range post.Images {
|
||||||
|
images = append(images, ConvertPostImageToResponse(&img))
|
||||||
|
}
|
||||||
|
|
||||||
|
var author *UserResponse
|
||||||
|
if post.User != nil {
|
||||||
|
author = ConvertUserToResponse(post.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PostResponse{
|
||||||
|
ID: post.ID,
|
||||||
|
UserID: post.UserID,
|
||||||
|
Title: post.Title,
|
||||||
|
Content: post.Content,
|
||||||
|
Images: images,
|
||||||
|
Status: string(post.Status),
|
||||||
|
LikesCount: post.LikesCount,
|
||||||
|
CommentsCount: post.CommentsCount,
|
||||||
|
FavoritesCount: post.FavoritesCount,
|
||||||
|
SharesCount: post.SharesCount,
|
||||||
|
ViewsCount: post.ViewsCount,
|
||||||
|
IsPinned: post.IsPinned,
|
||||||
|
IsLocked: post.IsLocked,
|
||||||
|
IsVote: post.IsVote,
|
||||||
|
CreatedAt: FormatTime(post.CreatedAt),
|
||||||
|
UpdatedAt: FormatTime(post.UpdatedAt),
|
||||||
|
Author: author,
|
||||||
|
IsLiked: isLiked,
|
||||||
|
IsFavorited: isFavorited,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertPostToDetailResponse 将Post转换为PostDetailResponse
|
||||||
|
func ConvertPostToDetailResponse(post *model.Post, isLiked, isFavorited bool) *PostDetailResponse {
|
||||||
|
if post == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
images := make([]PostImageResponse, 0)
|
||||||
|
for _, img := range post.Images {
|
||||||
|
images = append(images, ConvertPostImageToResponse(&img))
|
||||||
|
}
|
||||||
|
|
||||||
|
var author *UserResponse
|
||||||
|
if post.User != nil {
|
||||||
|
author = ConvertUserToResponse(post.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PostDetailResponse{
|
||||||
|
ID: post.ID,
|
||||||
|
UserID: post.UserID,
|
||||||
|
Title: post.Title,
|
||||||
|
Content: post.Content,
|
||||||
|
Images: images,
|
||||||
|
Status: string(post.Status),
|
||||||
|
LikesCount: post.LikesCount,
|
||||||
|
CommentsCount: post.CommentsCount,
|
||||||
|
FavoritesCount: post.FavoritesCount,
|
||||||
|
SharesCount: post.SharesCount,
|
||||||
|
ViewsCount: post.ViewsCount,
|
||||||
|
IsPinned: post.IsPinned,
|
||||||
|
IsLocked: post.IsLocked,
|
||||||
|
IsVote: post.IsVote,
|
||||||
|
CreatedAt: FormatTime(post.CreatedAt),
|
||||||
|
UpdatedAt: FormatTime(post.UpdatedAt),
|
||||||
|
Author: author,
|
||||||
|
IsLiked: isLiked,
|
||||||
|
IsFavorited: isFavorited,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertPostsToResponse 将Post列表转换为响应列表(每个帖子独立检查点赞/收藏状态)
|
||||||
|
func ConvertPostsToResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse {
|
||||||
|
result := make([]*PostResponse, 0, len(posts))
|
||||||
|
for _, post := range posts {
|
||||||
|
isLiked := false
|
||||||
|
isFavorited := false
|
||||||
|
if isLikedMap != nil {
|
||||||
|
isLiked = isLikedMap[post.ID]
|
||||||
|
}
|
||||||
|
if isFavoritedMap != nil {
|
||||||
|
isFavorited = isFavoritedMap[post.ID]
|
||||||
|
}
|
||||||
|
result = append(result, ConvertPostToResponse(post, isLiked, isFavorited))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPostResponse 构建单个帖子响应(包含交互状态)
|
||||||
|
// 这是一个语义化的辅助函数,便于 Handler 层调用
|
||||||
|
func BuildPostResponse(post *model.Post, isLiked, isFavorited bool) *PostResponse {
|
||||||
|
return ConvertPostToResponse(post, isLiked, isFavorited)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuildPostsWithInteractionResponse 批量构建帖子响应(包含交互状态)
|
||||||
|
// 这是一个语义化的辅助函数,便于 Handler 层调用
|
||||||
|
func BuildPostsWithInteractionResponse(posts []*model.Post, isLikedMap, isFavoritedMap map[string]bool) []*PostResponse {
|
||||||
|
return ConvertPostsToResponse(posts, isLikedMap, isFavoritedMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== Comment 转换 ====================
|
||||||
|
|
||||||
|
// ConvertCommentToResponse 将Comment转换为CommentResponse
|
||||||
|
func ConvertCommentToResponse(comment *model.Comment, isLiked bool) *CommentResponse {
|
||||||
|
if comment == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var author *UserResponse
|
||||||
|
if comment.User != nil {
|
||||||
|
author = ConvertUserToResponse(comment.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换子回复(扁平化结构)
|
||||||
|
var replies []*CommentResponse
|
||||||
|
if len(comment.Replies) > 0 {
|
||||||
|
replies = make([]*CommentResponse, 0, len(comment.Replies))
|
||||||
|
for _, reply := range comment.Replies {
|
||||||
|
replies = append(replies, ConvertCommentToResponse(reply, false))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetID 就是 ParentID,前端根据这个 ID 找到被回复用户的昵称
|
||||||
|
var targetID *string
|
||||||
|
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||||
|
targetID = comment.ParentID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析图片JSON
|
||||||
|
var images []CommentImageResponse
|
||||||
|
if comment.Images != "" {
|
||||||
|
var urlList []string
|
||||||
|
if err := json.Unmarshal([]byte(comment.Images), &urlList); err == nil {
|
||||||
|
images = make([]CommentImageResponse, 0, len(urlList))
|
||||||
|
for _, url := range urlList {
|
||||||
|
images = append(images, CommentImageResponse{URL: url})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CommentResponse{
|
||||||
|
ID: comment.ID,
|
||||||
|
PostID: comment.PostID,
|
||||||
|
UserID: comment.UserID,
|
||||||
|
ParentID: comment.ParentID,
|
||||||
|
RootID: comment.RootID,
|
||||||
|
Content: comment.Content,
|
||||||
|
Images: images,
|
||||||
|
LikesCount: comment.LikesCount,
|
||||||
|
RepliesCount: comment.RepliesCount,
|
||||||
|
CreatedAt: FormatTime(comment.CreatedAt),
|
||||||
|
Author: author,
|
||||||
|
IsLiked: isLiked,
|
||||||
|
TargetID: targetID,
|
||||||
|
Replies: replies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 点赞状态检查器接口
|
||||||
|
type IsLikedChecker interface {
|
||||||
|
IsLiked(ctx context.Context, commentID, userID string) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertCommentToResponseWithUser 将Comment转换为CommentResponse(根据用户ID检查点赞状态)
|
||||||
|
func ConvertCommentToResponseWithUser(comment *model.Comment, userID string, checker IsLikedChecker) *CommentResponse {
|
||||||
|
if comment == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查当前用户是否点赞了该评论
|
||||||
|
isLiked := false
|
||||||
|
if userID != "" && checker != nil {
|
||||||
|
isLiked = checker.IsLiked(context.Background(), comment.ID, userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
var author *UserResponse
|
||||||
|
if comment.User != nil {
|
||||||
|
author = ConvertUserToResponse(comment.User)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换子回复(扁平化结构),递归检查点赞状态
|
||||||
|
var replies []*CommentResponse
|
||||||
|
if len(comment.Replies) > 0 {
|
||||||
|
replies = make([]*CommentResponse, 0, len(comment.Replies))
|
||||||
|
for _, reply := range comment.Replies {
|
||||||
|
replies = append(replies, ConvertCommentToResponseWithUser(reply, userID, checker))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TargetID 就是 ParentID,前端根据这个 ID 找到被回复用户的昵称
|
||||||
|
var targetID *string
|
||||||
|
if comment.ParentID != nil && *comment.ParentID != "" {
|
||||||
|
targetID = comment.ParentID
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解析图片JSON
|
||||||
|
var images []CommentImageResponse
|
||||||
|
if comment.Images != "" {
|
||||||
|
var urlList []string
|
||||||
|
if err := json.Unmarshal([]byte(comment.Images), &urlList); err == nil {
|
||||||
|
images = make([]CommentImageResponse, 0, len(urlList))
|
||||||
|
for _, url := range urlList {
|
||||||
|
images = append(images, CommentImageResponse{URL: url})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &CommentResponse{
|
||||||
|
ID: comment.ID,
|
||||||
|
PostID: comment.PostID,
|
||||||
|
UserID: comment.UserID,
|
||||||
|
ParentID: comment.ParentID,
|
||||||
|
RootID: comment.RootID,
|
||||||
|
Content: comment.Content,
|
||||||
|
Images: images,
|
||||||
|
LikesCount: comment.LikesCount,
|
||||||
|
RepliesCount: comment.RepliesCount,
|
||||||
|
CreatedAt: FormatTime(comment.CreatedAt),
|
||||||
|
Author: author,
|
||||||
|
IsLiked: isLiked,
|
||||||
|
TargetID: targetID,
|
||||||
|
Replies: replies,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertCommentsToResponseWithUser 将Comment列表转换为响应列表(根据用户ID检查点赞状态)
|
||||||
|
func ConvertCommentsToResponseWithUser(comments []*model.Comment, userID string, checker IsLikedChecker) []*CommentResponse {
|
||||||
|
result := make([]*CommentResponse, 0, len(comments))
|
||||||
|
for _, comment := range comments {
|
||||||
|
result = append(result, ConvertCommentToResponseWithUser(comment, userID, checker))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
235
internal/dto/user_converter.go
Normal file
235
internal/dto/user_converter.go
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
"carrot_bbs/internal/pkg/utils"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ==================== User 转换 ====================
|
||||||
|
|
||||||
|
// getAvatarOrDefault 获取头像URL,如果为空则返回在线头像生成服务的URL
|
||||||
|
func getAvatarOrDefault(user *model.User) string {
|
||||||
|
return utils.GetAvatarOrDefault(user.Username, user.Nickname, user.Avatar)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUserToResponse 将User转换为UserResponse
|
||||||
|
func ConvertUserToResponse(user *model.User) *UserResponse {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Email: user.Email,
|
||||||
|
Phone: user.Phone,
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
Avatar: getAvatarOrDefault(user),
|
||||||
|
CoverURL: user.CoverURL,
|
||||||
|
Bio: user.Bio,
|
||||||
|
Website: user.Website,
|
||||||
|
Location: user.Location,
|
||||||
|
PostsCount: user.PostsCount,
|
||||||
|
FollowersCount: user.FollowersCount,
|
||||||
|
FollowingCount: user.FollowingCount,
|
||||||
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUserToResponseWithFollowing 将User转换为UserResponse(包含关注状态)
|
||||||
|
func ConvertUserToResponseWithFollowing(user *model.User, isFollowing bool) *UserResponse {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Email: user.Email,
|
||||||
|
Phone: user.Phone,
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
Avatar: getAvatarOrDefault(user),
|
||||||
|
CoverURL: user.CoverURL,
|
||||||
|
Bio: user.Bio,
|
||||||
|
Website: user.Website,
|
||||||
|
Location: user.Location,
|
||||||
|
PostsCount: user.PostsCount,
|
||||||
|
FollowersCount: user.FollowersCount,
|
||||||
|
FollowingCount: user.FollowingCount,
|
||||||
|
IsFollowing: isFollowing,
|
||||||
|
IsFollowingMe: false, // 默认false,需要单独计算
|
||||||
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUserToResponseWithPostsCount 将User转换为UserResponse(使用实时计算的帖子数量)
|
||||||
|
func ConvertUserToResponseWithPostsCount(user *model.User, postsCount int) *UserResponse {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Email: user.Email,
|
||||||
|
Phone: user.Phone,
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
Avatar: getAvatarOrDefault(user),
|
||||||
|
CoverURL: user.CoverURL,
|
||||||
|
Bio: user.Bio,
|
||||||
|
Website: user.Website,
|
||||||
|
Location: user.Location,
|
||||||
|
PostsCount: postsCount,
|
||||||
|
FollowersCount: user.FollowersCount,
|
||||||
|
FollowingCount: user.FollowingCount,
|
||||||
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUserToResponseWithMutualFollow 将User转换为UserResponse(包含双向关注状态)
|
||||||
|
func ConvertUserToResponseWithMutualFollow(user *model.User, isFollowing, isFollowingMe bool) *UserResponse {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Email: user.Email,
|
||||||
|
Phone: user.Phone,
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
Avatar: getAvatarOrDefault(user),
|
||||||
|
CoverURL: user.CoverURL,
|
||||||
|
Bio: user.Bio,
|
||||||
|
Website: user.Website,
|
||||||
|
Location: user.Location,
|
||||||
|
PostsCount: user.PostsCount,
|
||||||
|
FollowersCount: user.FollowersCount,
|
||||||
|
FollowingCount: user.FollowingCount,
|
||||||
|
IsFollowing: isFollowing,
|
||||||
|
IsFollowingMe: isFollowingMe,
|
||||||
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUserToResponseWithMutualFollowAndPostsCount 将User转换为UserResponse(包含双向关注状态和实时计算的帖子数量)
|
||||||
|
func ConvertUserToResponseWithMutualFollowAndPostsCount(user *model.User, isFollowing, isFollowingMe bool, postsCount int) *UserResponse {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UserResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Email: user.Email,
|
||||||
|
Phone: user.Phone,
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
Avatar: getAvatarOrDefault(user),
|
||||||
|
CoverURL: user.CoverURL,
|
||||||
|
Bio: user.Bio,
|
||||||
|
Website: user.Website,
|
||||||
|
Location: user.Location,
|
||||||
|
PostsCount: postsCount,
|
||||||
|
FollowersCount: user.FollowersCount,
|
||||||
|
FollowingCount: user.FollowingCount,
|
||||||
|
IsFollowing: isFollowing,
|
||||||
|
IsFollowingMe: isFollowingMe,
|
||||||
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUserToDetailResponse 将User转换为UserDetailResponse
|
||||||
|
func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UserDetailResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Email: user.Email,
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
Avatar: getAvatarOrDefault(user),
|
||||||
|
CoverURL: user.CoverURL,
|
||||||
|
Bio: user.Bio,
|
||||||
|
Website: user.Website,
|
||||||
|
Location: user.Location,
|
||||||
|
PostsCount: user.PostsCount,
|
||||||
|
FollowersCount: user.FollowersCount,
|
||||||
|
FollowingCount: user.FollowingCount,
|
||||||
|
IsVerified: user.IsVerified,
|
||||||
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUserToDetailResponseWithPostsCount 将User转换为UserDetailResponse(使用实时计算的帖子数量)
|
||||||
|
func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int) *UserDetailResponse {
|
||||||
|
if user == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &UserDetailResponse{
|
||||||
|
ID: user.ID,
|
||||||
|
Username: user.Username,
|
||||||
|
Nickname: user.Nickname,
|
||||||
|
Email: user.Email,
|
||||||
|
EmailVerified: user.EmailVerified,
|
||||||
|
Phone: user.Phone, // 仅当前用户自己可见
|
||||||
|
Avatar: getAvatarOrDefault(user),
|
||||||
|
CoverURL: user.CoverURL,
|
||||||
|
Bio: user.Bio,
|
||||||
|
Website: user.Website,
|
||||||
|
Location: user.Location,
|
||||||
|
PostsCount: postsCount,
|
||||||
|
FollowersCount: user.FollowersCount,
|
||||||
|
FollowingCount: user.FollowingCount,
|
||||||
|
IsVerified: user.IsVerified,
|
||||||
|
CreatedAt: FormatTime(user.CreatedAt),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUsersToResponse 将User列表转换为响应列表
|
||||||
|
func ConvertUsersToResponse(users []*model.User) []*UserResponse {
|
||||||
|
result := make([]*UserResponse, 0, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
result = append(result, ConvertUserToResponse(user))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUsersToResponseWithMutualFollow 将User列表转换为响应列表(包含双向关注状态)
|
||||||
|
// followingStatusMap: key是用户ID,value是[isFollowing, isFollowingMe]
|
||||||
|
func ConvertUsersToResponseWithMutualFollow(users []*model.User, followingStatusMap map[string][2]bool) []*UserResponse {
|
||||||
|
result := make([]*UserResponse, 0, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
status, ok := followingStatusMap[user.ID]
|
||||||
|
if ok {
|
||||||
|
result = append(result, ConvertUserToResponseWithMutualFollow(user, status[0], status[1]))
|
||||||
|
} else {
|
||||||
|
result = append(result, ConvertUserToResponse(user))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertUsersToResponseWithMutualFollowAndPostsCount 将User列表转换为响应列表(包含双向关注状态和实时计算的帖子数量)
|
||||||
|
// followingStatusMap: key是用户ID,value是[isFollowing, isFollowingMe]
|
||||||
|
// postsCountMap: key是用户ID,value是帖子数量
|
||||||
|
func ConvertUsersToResponseWithMutualFollowAndPostsCount(users []*model.User, followingStatusMap map[string][2]bool, postsCountMap map[string]int64) []*UserResponse {
|
||||||
|
result := make([]*UserResponse, 0, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
status, hasStatus := followingStatusMap[user.ID]
|
||||||
|
postsCount, hasPostsCount := postsCountMap[user.ID]
|
||||||
|
|
||||||
|
// 如果没有帖子数量,使用数据库中的值
|
||||||
|
if !hasPostsCount {
|
||||||
|
postsCount = int64(user.PostsCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasStatus {
|
||||||
|
result = append(result, ConvertUserToResponseWithMutualFollowAndPostsCount(user, status[0], status[1], int(postsCount)))
|
||||||
|
} else {
|
||||||
|
result = append(result, ConvertUserToResponseWithPostsCount(user, int(postsCount)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
121
internal/errors/app_errors.go
Normal file
121
internal/errors/app_errors.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AppError 统一的应用错误类型
|
||||||
|
type AppError struct {
|
||||||
|
Code string
|
||||||
|
Message string
|
||||||
|
Cause error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) Error() string {
|
||||||
|
if e.Cause != nil {
|
||||||
|
return fmt.Sprintf("[%s] %s: %v", e.Code, e.Message, e.Cause)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("[%s] %s", e.Code, e.Message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *AppError) Unwrap() error {
|
||||||
|
return e.Cause
|
||||||
|
}
|
||||||
|
|
||||||
|
// 预定义错误
|
||||||
|
var (
|
||||||
|
// 通用错误
|
||||||
|
ErrNotFound = &AppError{Code: "NOT_FOUND", Message: "资源不存在"}
|
||||||
|
ErrUnauthorized = &AppError{Code: "UNAUTHORIZED", Message: "未授权"}
|
||||||
|
ErrForbidden = &AppError{Code: "FORBIDDEN", Message: "禁止访问"}
|
||||||
|
ErrBadRequest = &AppError{Code: "BAD_REQUEST", Message: "请求参数错误"}
|
||||||
|
ErrInternal = &AppError{Code: "INTERNAL_ERROR", Message: "内部服务器错误"}
|
||||||
|
|
||||||
|
// 用户相关错误
|
||||||
|
ErrUserNotFound = &AppError{Code: "USER_NOT_FOUND", Message: "用户不存在"}
|
||||||
|
ErrUserAlreadyExists = &AppError{Code: "USER_ALREADY_EXISTS", Message: "用户已存在"}
|
||||||
|
ErrInvalidCredentials = &AppError{Code: "INVALID_CREDENTIALS", Message: "用户名或密码错误"}
|
||||||
|
ErrInvalidUsername = &AppError{Code: "INVALID_USERNAME", Message: "无效的用户名"}
|
||||||
|
ErrInvalidEmail = &AppError{Code: "INVALID_EMAIL", Message: "无效的邮箱"}
|
||||||
|
ErrInvalidPhone = &AppError{Code: "INVALID_PHONE", Message: "无效的手机号"}
|
||||||
|
ErrWeakPassword = &AppError{Code: "WEAK_PASSWORD", Message: "密码强度不足"}
|
||||||
|
ErrUsernameExists = &AppError{Code: "USERNAME_EXISTS", Message: "用户名已存在"}
|
||||||
|
ErrEmailExists = &AppError{Code: "EMAIL_EXISTS", Message: "邮箱已存在"}
|
||||||
|
ErrPhoneExists = &AppError{Code: "PHONE_EXISTS", Message: "手机号已存在"}
|
||||||
|
ErrUserBanned = &AppError{Code: "USER_BANNED", Message: "用户已被封禁"}
|
||||||
|
ErrUserBlocked = &AppError{Code: "USER_BLOCKED", Message: "存在屏蔽关系"}
|
||||||
|
ErrInvalidOperation = &AppError{Code: "INVALID_OPERATION", Message: "无效操作"}
|
||||||
|
ErrEmailServiceUnavailable = &AppError{Code: "EMAIL_SERVICE_UNAVAILABLE", Message: "邮件服务不可用"}
|
||||||
|
ErrVerificationCodeTooFrequent = &AppError{Code: "VERIFICATION_CODE_TOO_FREQUENT", Message: "验证码发送过于频繁"}
|
||||||
|
ErrVerificationCodeInvalid = &AppError{Code: "VERIFICATION_CODE_INVALID", Message: "验证码无效"}
|
||||||
|
ErrVerificationCodeExpired = &AppError{Code: "VERIFICATION_CODE_EXPIRED", Message: "验证码已过期"}
|
||||||
|
ErrVerificationCodeUnavailable = &AppError{Code: "VERIFICATION_CODE_UNAVAILABLE", Message: "验证码存储不可用"}
|
||||||
|
ErrEmailAlreadyVerified = &AppError{Code: "EMAIL_ALREADY_VERIFIED", Message: "邮箱已验证"}
|
||||||
|
ErrEmailNotBound = &AppError{Code: "EMAIL_NOT_BOUND", Message: "邮箱未绑定"}
|
||||||
|
|
||||||
|
// 帖子相关错误
|
||||||
|
ErrPostNotFound = &AppError{Code: "POST_NOT_FOUND", Message: "帖子不存在"}
|
||||||
|
ErrPostDeleted = &AppError{Code: "POST_DELETED", Message: "帖子已删除"}
|
||||||
|
|
||||||
|
// 群组相关错误
|
||||||
|
ErrGroupNotFound = &AppError{Code: "GROUP_NOT_FOUND", Message: "群组不存在"}
|
||||||
|
ErrNotGroupMember = &AppError{Code: "NOT_GROUP_MEMBER", Message: "不是群组成员"}
|
||||||
|
ErrAlreadyGroupMember = &AppError{Code: "ALREADY_GROUP_MEMBER", Message: "已是群组成员"}
|
||||||
|
ErrNotGroupAdmin = &AppError{Code: "NOT_GROUP_ADMIN", Message: "不是群管理员"}
|
||||||
|
ErrNotGroupOwner = &AppError{Code: "NOT_GROUP_OWNER", Message: "不是群主"}
|
||||||
|
ErrGroupFull = &AppError{Code: "GROUP_FULL", Message: "群已满"}
|
||||||
|
ErrCannotRemoveOwner = &AppError{Code: "CANNOT_REMOVE_OWNER", Message: "不能移除群主"}
|
||||||
|
ErrCannotMuteOwner = &AppError{Code: "CANNOT_MUTE_OWNER", Message: "不能禁言群主"}
|
||||||
|
ErrMuted = &AppError{Code: "MUTED", Message: "你已被禁言"}
|
||||||
|
ErrMuteAllEnabled = &AppError{Code: "MUTE_ALL_ENABLED", Message: "全员禁言中"}
|
||||||
|
ErrCannotJoin = &AppError{Code: "CANNOT_JOIN", Message: "该群不允许加入"}
|
||||||
|
ErrJoinRequestPending = &AppError{Code: "JOIN_REQUEST_PENDING", Message: "加群申请已提交"}
|
||||||
|
ErrGroupRequestNotFound = &AppError{Code: "GROUP_REQUEST_NOT_FOUND", Message: "加群请求不存在"}
|
||||||
|
ErrGroupRequestHandled = &AppError{Code: "GROUP_REQUEST_HANDLED", Message: "该加群请求已处理"}
|
||||||
|
ErrNotRequestTarget = &AppError{Code: "NOT_REQUEST_TARGET", Message: "不是邀请目标用户"}
|
||||||
|
ErrNoEligibleInvitee = &AppError{Code: "NO_ELIGIBLE_INVITEE", Message: "没有可邀请的用户"}
|
||||||
|
ErrNotMutualFollow = &AppError{Code: "NOT_MUTUAL_FOLLOW", Message: "仅支持邀请互相关注用户"}
|
||||||
|
ErrInviteNotAccepted = &AppError{Code: "INVITE_NOT_ACCEPTED", Message: "被邀请人尚未同意邀请,无法审批"}
|
||||||
|
|
||||||
|
// 表情包相关错误
|
||||||
|
ErrStickerAlreadyExists = &AppError{Code: "STICKER_ALREADY_EXISTS", Message: "表情包已存在"}
|
||||||
|
ErrInvalidStickerURL = &AppError{Code: "INVALID_STICKER_URL", Message: "无效的表情包URL"}
|
||||||
|
|
||||||
|
// 日程相关错误
|
||||||
|
ErrInvalidSchedulePayload = &AppError{Code: "INVALID_SCHEDULE_PAYLOAD", Message: "无效的日程数据"}
|
||||||
|
ErrScheduleCourseNotFound = &AppError{Code: "SCHEDULE_COURSE_NOT_FOUND", Message: "日程课程不存在"}
|
||||||
|
ErrScheduleForbidden = &AppError{Code: "SCHEDULE_FORBIDDEN", Message: "禁止的日程操作"}
|
||||||
|
ErrScheduleColorDuplicated = &AppError{Code: "SCHEDULE_COLOR_DUPLICATED", Message: "课程颜色已被使用"}
|
||||||
|
|
||||||
|
// 通知相关错误
|
||||||
|
ErrUnauthorizedNotification = &AppError{Code: "UNAUTHORIZED_NOTIFICATION", Message: "无权删除此通知"}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wrap 包装错误
|
||||||
|
func Wrap(err error, appErr *AppError) *AppError {
|
||||||
|
return &AppError{
|
||||||
|
Code: appErr.Code,
|
||||||
|
Message: appErr.Message,
|
||||||
|
Cause: err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
func As(err error) (*AppError, bool) {
|
||||||
|
var appErr *AppError
|
||||||
|
if errors.As(err, &appErr) {
|
||||||
|
return appErr, true
|
||||||
|
}
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
@@ -11,21 +11,24 @@ import (
|
|||||||
"carrot_bbs/internal/pkg/gorse"
|
"carrot_bbs/internal/pkg/gorse"
|
||||||
"carrot_bbs/internal/pkg/response"
|
"carrot_bbs/internal/pkg/response"
|
||||||
|
|
||||||
gorseio "github.com/gorse-io/gorse-go"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
gorseio "github.com/gorse-io/gorse-go"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// GorseHandler Gorse推荐处理器
|
// GorseHandler Gorse推荐处理器
|
||||||
type GorseHandler struct {
|
type GorseHandler struct {
|
||||||
importPassword string
|
importPassword string
|
||||||
gorseConfig config.GorseConfig
|
gorseConfig config.GorseConfig
|
||||||
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGorseHandler 创建Gorse处理器
|
// NewGorseHandler 创建Gorse处理器
|
||||||
func NewGorseHandler(cfg config.GorseConfig) *GorseHandler {
|
func NewGorseHandler(cfg config.GorseConfig, db *gorm.DB) *GorseHandler {
|
||||||
return &GorseHandler{
|
return &GorseHandler{
|
||||||
importPassword: cfg.ImportPassword,
|
importPassword: cfg.ImportPassword,
|
||||||
gorseConfig: cfg,
|
gorseConfig: cfg,
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +101,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
|||||||
|
|
||||||
// 导入帖子
|
// 导入帖子
|
||||||
var posts []model.Post
|
var posts []model.Post
|
||||||
if err := model.DB.Find(&posts).Error; err != nil {
|
if err := h.db.Find(&posts).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, post := range posts {
|
for _, post := range posts {
|
||||||
@@ -126,7 +129,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
|||||||
|
|
||||||
// 导入用户
|
// 导入用户
|
||||||
var users []model.User
|
var users []model.User
|
||||||
if err := model.DB.Find(&users).Error; err != nil {
|
if err := h.db.Find(&users).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, user := range users {
|
for _, user := range users {
|
||||||
@@ -148,7 +151,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
|||||||
|
|
||||||
// 导入点赞
|
// 导入点赞
|
||||||
var likes []model.PostLike
|
var likes []model.PostLike
|
||||||
if err := model.DB.Find(&likes).Error; err != nil {
|
if err := h.db.Find(&likes).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, like := range likes {
|
for _, like := range likes {
|
||||||
@@ -167,7 +170,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
|||||||
|
|
||||||
// 导入收藏
|
// 导入收藏
|
||||||
var favorites []model.Favorite
|
var favorites []model.Favorite
|
||||||
if err := model.DB.Find(&favorites).Error; err != nil {
|
if err := h.db.Find(&favorites).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
for _, fav := range favorites {
|
for _, fav := range favorites {
|
||||||
@@ -186,7 +189,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
|||||||
|
|
||||||
// 导入评论(按用户-帖子去重)
|
// 导入评论(按用户-帖子去重)
|
||||||
var comments []model.Comment
|
var comments []model.Comment
|
||||||
if err := model.DB.Where("status = ?", model.CommentStatusPublished).Find(&comments).Error; err != nil {
|
if err := h.db.Where("status = ?", model.CommentStatusPublished).Find(&comments).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
seen := make(map[string]struct{})
|
seen := make(map[string]struct{})
|
||||||
@@ -231,4 +234,4 @@ func buildPostCategories(post *model.Post) []string {
|
|||||||
categories = append(categories, "this_week")
|
categories = append(categories, "this_week")
|
||||||
}
|
}
|
||||||
return categories
|
return categories
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,11 +15,11 @@ import (
|
|||||||
// GroupHandler 群组处理器
|
// GroupHandler 群组处理器
|
||||||
type GroupHandler struct {
|
type GroupHandler struct {
|
||||||
groupService service.GroupService
|
groupService service.GroupService
|
||||||
userService *service.UserService
|
userService service.UserService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewGroupHandler 创建群组处理器
|
// NewGroupHandler 创建群组处理器
|
||||||
func NewGroupHandler(groupService service.GroupService, userService *service.UserService) *GroupHandler {
|
func NewGroupHandler(groupService service.GroupService, userService service.UserService) *GroupHandler {
|
||||||
return &GroupHandler{
|
return &GroupHandler{
|
||||||
groupService: groupService,
|
groupService: groupService,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
|||||||
@@ -20,13 +20,13 @@ import (
|
|||||||
type MessageHandler struct {
|
type MessageHandler struct {
|
||||||
chatService service.ChatService
|
chatService service.ChatService
|
||||||
messageService *service.MessageService
|
messageService *service.MessageService
|
||||||
userService *service.UserService
|
userService service.UserService
|
||||||
groupService service.GroupService
|
groupService service.GroupService
|
||||||
sseHub *sse.Hub
|
sseHub *sse.Hub
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewMessageHandler 创建消息处理器
|
// NewMessageHandler 创建消息处理器
|
||||||
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService *service.UserService, groupService service.GroupService, sseHub *sse.Hub) *MessageHandler {
|
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService service.UserService, groupService service.GroupService, sseHub *sse.Hub) *MessageHandler {
|
||||||
return &MessageHandler{
|
return &MessageHandler{
|
||||||
chatService: chatService,
|
chatService: chatService,
|
||||||
messageService: messageService,
|
messageService: messageService,
|
||||||
|
|||||||
@@ -15,12 +15,12 @@ import (
|
|||||||
|
|
||||||
// PostHandler 帖子处理器
|
// PostHandler 帖子处理器
|
||||||
type PostHandler struct {
|
type PostHandler struct {
|
||||||
postService *service.PostService
|
postService service.PostService
|
||||||
userService *service.UserService
|
userService service.UserService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPostHandler 创建帖子处理器
|
// NewPostHandler 创建帖子处理器
|
||||||
func NewPostHandler(postService *service.PostService, userService *service.UserService) *PostHandler {
|
func NewPostHandler(postService service.PostService, userService service.UserService) *PostHandler {
|
||||||
return &PostHandler{
|
return &PostHandler{
|
||||||
postService: postService,
|
postService: postService,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
"carrot_bbs/internal/cache"
|
"carrot_bbs/internal/cache"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"carrot_bbs/internal/dto"
|
"carrot_bbs/internal/dto"
|
||||||
@@ -16,16 +17,19 @@ import (
|
|||||||
type SystemMessageHandler struct {
|
type SystemMessageHandler struct {
|
||||||
systemMsgService service.SystemMessageService
|
systemMsgService service.SystemMessageService
|
||||||
notifyRepo *repository.SystemNotificationRepository
|
notifyRepo *repository.SystemNotificationRepository
|
||||||
|
cache cache.Cache
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSystemMessageHandler 创建系统消息处理器
|
// NewSystemMessageHandler 创建系统消息处理器
|
||||||
func NewSystemMessageHandler(
|
func NewSystemMessageHandler(
|
||||||
systemMsgService service.SystemMessageService,
|
systemMsgService service.SystemMessageService,
|
||||||
notifyRepo *repository.SystemNotificationRepository,
|
notifyRepo *repository.SystemNotificationRepository,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
) *SystemMessageHandler {
|
) *SystemMessageHandler {
|
||||||
return &SystemMessageHandler{
|
return &SystemMessageHandler{
|
||||||
systemMsgService: systemMsgService,
|
systemMsgService: systemMsgService,
|
||||||
notifyRepo: notifyRepo,
|
notifyRepo: notifyRepo,
|
||||||
|
cache: cacheBackend,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,7 +105,7 @@ func (h *SystemMessageHandler) MarkAsRead(c *gin.Context) {
|
|||||||
response.InternalServerError(c, "failed to mark as read")
|
response.InternalServerError(c, "failed to mark as read")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cache.InvalidateUnreadSystem(cache.GetCache(), userID)
|
cache.InvalidateUnreadSystem(h.cache, userID)
|
||||||
|
|
||||||
response.SuccessWithMessage(c, "marked as read", nil)
|
response.SuccessWithMessage(c, "marked as read", nil)
|
||||||
}
|
}
|
||||||
@@ -121,7 +125,7 @@ func (h *SystemMessageHandler) MarkAllAsRead(c *gin.Context) {
|
|||||||
response.InternalServerError(c, "failed to mark all as read")
|
response.InternalServerError(c, "failed to mark all as read")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cache.InvalidateUnreadSystem(cache.GetCache(), userID)
|
cache.InvalidateUnreadSystem(h.cache, userID)
|
||||||
|
|
||||||
response.SuccessWithMessage(c, "all messages marked as read", nil)
|
response.SuccessWithMessage(c, "all messages marked as read", nil)
|
||||||
}
|
}
|
||||||
@@ -148,7 +152,7 @@ func (h *SystemMessageHandler) DeleteSystemMessage(c *gin.Context) {
|
|||||||
response.InternalServerError(c, "failed to delete notification")
|
response.InternalServerError(c, "failed to delete notification")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
cache.InvalidateUnreadSystem(cache.GetCache(), userID)
|
cache.InvalidateUnreadSystem(h.cache, userID)
|
||||||
|
|
||||||
response.SuccessWithMessage(c, "notification deleted", nil)
|
response.SuccessWithMessage(c, "notification deleted", nil)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,12 +12,12 @@ import (
|
|||||||
|
|
||||||
// UserHandler 用户处理器
|
// UserHandler 用户处理器
|
||||||
type UserHandler struct {
|
type UserHandler struct {
|
||||||
userService *service.UserService
|
userService service.UserService
|
||||||
jwtService *service.JWTService
|
jwtService *service.JWTService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUserHandler 创建用户处理器
|
// NewUserHandler 创建用户处理器
|
||||||
func NewUserHandler(userService *service.UserService) *UserHandler {
|
func NewUserHandler(userService service.UserService) *UserHandler {
|
||||||
return &UserHandler{
|
return &UserHandler{
|
||||||
userService: userService,
|
userService: userService,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,11 +14,11 @@ import (
|
|||||||
// VoteHandler 投票处理器
|
// VoteHandler 投票处理器
|
||||||
type VoteHandler struct {
|
type VoteHandler struct {
|
||||||
voteService *service.VoteService
|
voteService *service.VoteService
|
||||||
postService *service.PostService
|
postService service.PostService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVoteHandler 创建投票处理器
|
// NewVoteHandler 创建投票处理器
|
||||||
func NewVoteHandler(voteService *service.VoteService, postService *service.PostService) *VoteHandler {
|
func NewVoteHandler(voteService *service.VoteService, postService service.PostService) *VoteHandler {
|
||||||
return &VoteHandler{
|
return &VoteHandler{
|
||||||
voteService: voteService,
|
voteService: voteService,
|
||||||
postService: postService,
|
postService: postService,
|
||||||
|
|||||||
@@ -14,11 +14,8 @@ import (
|
|||||||
"carrot_bbs/internal/config"
|
"carrot_bbs/internal/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DB 全局数据库连接
|
// NewDB 创建数据库连接(用于 Wire 依赖注入)
|
||||||
var DB *gorm.DB
|
func NewDB(cfg *config.DatabaseConfig) (*gorm.DB, error) {
|
||||||
|
|
||||||
// InitDB 初始化数据库连接
|
|
||||||
func InitDB(cfg *config.DatabaseConfig) error {
|
|
||||||
var err error
|
var err error
|
||||||
var db *gorm.DB
|
var db *gorm.DB
|
||||||
gormLogger := logger.New(
|
gormLogger := logger.New(
|
||||||
@@ -52,28 +49,26 @@ func InitDB(cfg *config.DatabaseConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to connect to database: %w", err)
|
return nil, fmt.Errorf("failed to connect to database: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
DB = db
|
|
||||||
|
|
||||||
// 配置连接池(SQLite不支持连接池配置,跳过)
|
// 配置连接池(SQLite不支持连接池配置,跳过)
|
||||||
if cfg.Type != "sqlite" {
|
if cfg.Type != "sqlite" {
|
||||||
sqlDB, err := DB.DB()
|
sqlDB, err := db.DB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("failed to get database instance: %w", err)
|
return nil, fmt.Errorf("failed to get database instance: %w", err)
|
||||||
}
|
}
|
||||||
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||||
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 自动迁移
|
// 自动迁移
|
||||||
if err := autoMigrate(DB); err != nil {
|
if err := autoMigrate(db); err != nil {
|
||||||
return fmt.Errorf("failed to auto migrate: %w", err)
|
return nil, fmt.Errorf("failed to auto migrate: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Database connected (%s) and migrated successfully", cfg.Type)
|
log.Printf("Database connected (%s) and migrated successfully", cfg.Type)
|
||||||
return nil
|
return db, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func parseGormLogLevel(level string) logger.LogLevel {
|
func parseGormLogLevel(level string) logger.LogLevel {
|
||||||
@@ -155,8 +150,8 @@ func autoMigrate(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CloseDB 关闭数据库连接
|
// CloseDB 关闭数据库连接
|
||||||
func CloseDB() {
|
func CloseDB(db *gorm.DB) {
|
||||||
if sqlDB, err := DB.DB(); err == nil {
|
if sqlDB, err := db.DB(); err == nil {
|
||||||
sqlDB.Close()
|
sqlDB.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
|
||||||
"carrot_bbs/internal/service"
|
apperrors "carrot_bbs/internal/errors"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Response 统一响应结构
|
// Response 统一响应结构
|
||||||
@@ -119,44 +119,72 @@ func Paginated(c *gin.Context, list interface{}, total int64, page, pageSize int
|
|||||||
}
|
}
|
||||||
|
|
||||||
// HandleServiceError 统一处理 Service 错误
|
// HandleServiceError 统一处理 Service 错误
|
||||||
// 如果 err 是 *service.ServiceError,返回对应的业务错误码和消息
|
// 如果 err 是 *apperrors.AppError,返回对应的业务错误码和消息
|
||||||
// 如果 err 是其他错误,返回 false(调用方应处理通用错误)
|
// 如果 err 是其他错误,返回 false(调用方应处理通用错误)
|
||||||
// 返回 true 表示错误已处理,false 表示需要调用方继续处理
|
// 返回 true 表示错误已处理,false 表示需要调用方继续处理
|
||||||
func HandleServiceError(c *gin.Context, err error) bool {
|
func HandleServiceError(c *gin.Context, err error) bool {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
if se, ok := err.(*service.ServiceError); ok {
|
if se, ok := apperrors.As(err); ok {
|
||||||
ErrorWithStatusCode(c, statusCodeFromCode(se.Code), se.Code, se.Message)
|
statusCode := statusCodeFromAppErrorCode(se.Code)
|
||||||
|
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
// HandleError 统一处理错误(带默认消息)
|
// HandleError 统一处理错误(带默认消息)
|
||||||
// 如果 err 是 *service.ServiceError,返回对应的业务错误码和消息
|
// 如果 err 是 *apperrors.AppError,返回对应的业务错误码和消息
|
||||||
// 如果 err 是其他错误,返回 InternalServerError 并使用 defaultMessage
|
// 如果 err 是其他错误,返回 InternalServerError 并使用 defaultMessage
|
||||||
func HandleError(c *gin.Context, err error, defaultMessage string) {
|
func HandleError(c *gin.Context, err error, defaultMessage string) {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if se, ok := err.(*service.ServiceError); ok {
|
if se, ok := apperrors.As(err); ok {
|
||||||
ErrorWithStatusCode(c, statusCodeFromCode(se.Code), se.Code, se.Message)
|
statusCode := statusCodeFromAppErrorCode(se.Code)
|
||||||
|
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
InternalServerError(c, defaultMessage)
|
InternalServerError(c, defaultMessage)
|
||||||
}
|
}
|
||||||
|
|
||||||
// statusCodeFromCode 根据业务错误码获取 HTTP 状态码
|
// statusCodeFromAppErrorCode 根据AppError的错误码字符串获取 HTTP 状态码
|
||||||
func statusCodeFromCode(code int) int {
|
func statusCodeFromAppErrorCode(code string) int {
|
||||||
switch {
|
switch code {
|
||||||
case code >= 200 && code < 300:
|
case "NOT_FOUND", "USER_NOT_FOUND", "POST_NOT_FOUND", "GROUP_NOT_FOUND",
|
||||||
return http.StatusOK
|
"GROUP_REQUEST_NOT_FOUND", "SCHEDULE_COURSE_NOT_FOUND":
|
||||||
case code >= 400 && code < 500:
|
return http.StatusNotFound
|
||||||
return code
|
case "UNAUTHORIZED", "INVALID_CREDENTIALS":
|
||||||
case code >= 500:
|
return http.StatusUnauthorized
|
||||||
return code
|
case "FORBIDDEN", "NOT_GROUP_MEMBER", "NOT_GROUP_ADMIN", "NOT_GROUP_OWNER",
|
||||||
|
"USER_BANNED", "USER_BLOCKED", "MUTED", "MUTE_ALL_ENABLED",
|
||||||
|
"SCHEDULE_FORBIDDEN", "UNAUTHORIZED_NOTIFICATION":
|
||||||
|
return http.StatusForbidden
|
||||||
|
case "BAD_REQUEST", "INVALID_USERNAME", "INVALID_EMAIL", "INVALID_PHONE",
|
||||||
|
"WEAK_PASSWORD", "USERNAME_EXISTS", "EMAIL_EXISTS", "PHONE_EXISTS",
|
||||||
|
"USER_ALREADY_EXISTS", "INVALID_OPERATION", "VERIFICATION_CODE_INVALID",
|
||||||
|
"VERIFICATION_CODE_EXPIRED", "EMAIL_ALREADY_VERIFIED", "EMAIL_NOT_BOUND",
|
||||||
|
"GROUP_FULL", "ALREADY_GROUP_MEMBER", "CANNOT_REMOVE_OWNER", "CANNOT_MUTE_OWNER",
|
||||||
|
"CANNOT_JOIN", "JOIN_REQUEST_PENDING", "GROUP_REQUEST_HANDLED",
|
||||||
|
"NOT_REQUEST_TARGET", "NO_ELIGIBLE_INVITEE", "NOT_MUTUAL_FOLLOW",
|
||||||
|
"INVITE_NOT_ACCEPTED", "STICKER_ALREADY_EXISTS", "INVALID_STICKER_URL",
|
||||||
|
"INVALID_SCHEDULE_PAYLOAD", "SCHEDULE_COLOR_DUPLICATED":
|
||||||
|
return http.StatusBadRequest
|
||||||
|
case "VERIFICATION_CODE_TOO_FREQUENT":
|
||||||
|
return http.StatusTooManyRequests
|
||||||
|
case "EMAIL_SERVICE_UNAVAILABLE", "VERIFICATION_CODE_UNAVAILABLE", "INTERNAL_ERROR":
|
||||||
|
return http.StatusInternalServerError
|
||||||
default:
|
default:
|
||||||
return http.StatusBadRequest
|
return http.StatusBadRequest
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ErrorWithStringCode 带字符串错误码的错误响应
|
||||||
|
func ErrorWithStringCode(c *gin.Context, statusCode int, code string, message string) {
|
||||||
|
c.JSON(statusCode, ResponseSnakeCase{
|
||||||
|
Code: 0, // 为了兼容,这里设为0,实际错误码在message中体现
|
||||||
|
Message: message,
|
||||||
|
Data: code,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package repository
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
@@ -17,6 +18,14 @@ func NewPostRepository(db *gorm.DB) *PostRepository {
|
|||||||
return &PostRepository{db: db}
|
return &PostRepository{db: db}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getDB 获取数据库连接(优先使用 context 中的事务)
|
||||||
|
func (r *PostRepository) getDB(ctx context.Context) *gorm.DB {
|
||||||
|
if tx := GetTxFromContext(ctx); tx != nil {
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
return r.db
|
||||||
|
}
|
||||||
|
|
||||||
// Create 创建帖子
|
// Create 创建帖子
|
||||||
func (r *PostRepository) Create(post *model.Post, images []string) error {
|
func (r *PostRepository) Create(post *model.Post, images []string) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||||
@@ -419,3 +428,104 @@ func (r *PostRepository) GetByIDs(ids []string) ([]*model.Post, error) {
|
|||||||
|
|
||||||
return ordered, nil
|
return ordered, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== Context-aware methods for transaction support ==========
|
||||||
|
|
||||||
|
// CreateWithContext 创建帖子(支持事务)
|
||||||
|
func (r *PostRepository) CreateWithContext(ctx context.Context, post *model.Post, images []string) error {
|
||||||
|
db := r.getDB(ctx)
|
||||||
|
|
||||||
|
// 如果 context 中有事务,直接使用
|
||||||
|
if tx := GetTxFromContext(ctx); tx != nil {
|
||||||
|
return r.createWithTx(tx, post, images)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则开启新事务
|
||||||
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
return r.createWithTx(tx, post, images)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// createWithTx 使用指定事务创建帖子
|
||||||
|
func (r *PostRepository) createWithTx(tx *gorm.DB, post *model.Post, images []string) error {
|
||||||
|
// 创建帖子
|
||||||
|
if err := tx.Create(post).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建图片记录
|
||||||
|
for i, url := range images {
|
||||||
|
image := &model.PostImage{
|
||||||
|
PostID: post.ID,
|
||||||
|
URL: url,
|
||||||
|
SortOrder: i,
|
||||||
|
}
|
||||||
|
if err := tx.Create(image).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetByIDWithContext 根据ID获取帖子(支持事务)
|
||||||
|
func (r *PostRepository) GetByIDWithContext(ctx context.Context, id string) (*model.Post, error) {
|
||||||
|
var post model.Post
|
||||||
|
err := r.getDB(ctx).WithContext(ctx).Preload("User").Preload("Images").First(&post, "id = ?", id).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &post, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateWithContext 更新帖子(支持事务)
|
||||||
|
func (r *PostRepository) UpdateWithContext(ctx context.Context, post *model.Post) error {
|
||||||
|
post.UpdatedAt = time.Now()
|
||||||
|
return r.getDB(ctx).WithContext(ctx).Save(post).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteWithContext 删除帖子(支持事务)
|
||||||
|
func (r *PostRepository) DeleteWithContext(ctx context.Context, id string) error {
|
||||||
|
db := r.getDB(ctx)
|
||||||
|
|
||||||
|
// 如果 context 中有事务,直接使用
|
||||||
|
if tx := GetTxFromContext(ctx); tx != nil {
|
||||||
|
return r.deleteWithTx(tx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 否则开启新事务
|
||||||
|
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
return r.deleteWithTx(tx, id)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// deleteWithTx 使用指定事务删除帖子
|
||||||
|
func (r *PostRepository) deleteWithTx(tx *gorm.DB, id string) error {
|
||||||
|
// 删除帖子图片
|
||||||
|
if err := tx.Where("post_id = ?", id).Delete(&model.PostImage{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除帖子点赞记录
|
||||||
|
if err := tx.Where("post_id = ?", id).Delete(&model.PostLike{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除帖子收藏记录
|
||||||
|
if err := tx.Where("post_id = ?", id).Delete(&model.Favorite{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除评论点赞记录(子查询获取该帖子所有评论ID)
|
||||||
|
if err := tx.Where("comment_id IN (SELECT id FROM comments WHERE post_id = ?)", id).Delete(&model.CommentLike{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 删除帖子评论
|
||||||
|
if err := tx.Where("post_id = ?", id).Delete(&model.Comment{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 最后删除帖子本身(软删除)
|
||||||
|
return tx.Delete(&model.Post{}, "id = ?", id).Error
|
||||||
|
}
|
||||||
|
|||||||
43
internal/repository/transaction.go
Normal file
43
internal/repository/transaction.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package repository
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TransactionManager 事务管理器接口
|
||||||
|
type TransactionManager interface {
|
||||||
|
// RunInTransaction 在事务中执行函数
|
||||||
|
RunInTransaction(ctx context.Context, fn func(ctx context.Context) error) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// gormTransactionManager GORM 事务管理器实现
|
||||||
|
type gormTransactionManager struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTransactionManager 创建事务管理器
|
||||||
|
func NewTransactionManager(db *gorm.DB) TransactionManager {
|
||||||
|
return &gormTransactionManager{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RunInTransaction 在事务中执行函数
|
||||||
|
func (m *gormTransactionManager) RunInTransaction(ctx context.Context, fn func(ctx context.Context) error) error {
|
||||||
|
return m.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 将事务 TX 存入 context
|
||||||
|
ctxWithTx := context.WithValue(ctx, txKey{}, tx)
|
||||||
|
return fn(ctxWithTx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// txKey 事务 context key
|
||||||
|
type txKey struct{}
|
||||||
|
|
||||||
|
// GetTxFromContext 从 context 获取事务 TX
|
||||||
|
func GetTxFromContext(ctx context.Context) *gorm.DB {
|
||||||
|
if tx, ok := ctx.Value(txKey{}).(*gorm.DB); ok {
|
||||||
|
return tx
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -72,6 +72,7 @@ func NewChatService(
|
|||||||
userRepo *repository.UserRepository,
|
userRepo *repository.UserRepository,
|
||||||
sensitive SensitiveService,
|
sensitive SensitiveService,
|
||||||
sseHub *sse.Hub,
|
sseHub *sse.Hub,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
) ChatService {
|
) ChatService {
|
||||||
// 创建适配器
|
// 创建适配器
|
||||||
convRepoAdapter := cache.NewConversationRepositoryAdapter(repo)
|
convRepoAdapter := cache.NewConversationRepositoryAdapter(repo)
|
||||||
@@ -79,7 +80,7 @@ func NewChatService(
|
|||||||
|
|
||||||
// 创建会话缓存
|
// 创建会话缓存
|
||||||
conversationCache := cache.NewConversationCache(
|
conversationCache := cache.NewConversationCache(
|
||||||
cache.GetCache(),
|
cacheBackend,
|
||||||
convRepoAdapter,
|
convRepoAdapter,
|
||||||
msgRepoAdapter,
|
msgRepoAdapter,
|
||||||
cache.DefaultConversationCacheSettings(),
|
cache.DefaultConversationCacheSettings(),
|
||||||
|
|||||||
@@ -24,12 +24,12 @@ type CommentService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewCommentService 创建评论服务
|
// NewCommentService 创建评论服务
|
||||||
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *CommentService {
|
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache) *CommentService {
|
||||||
return &CommentService{
|
return &CommentService{
|
||||||
commentRepo: commentRepo,
|
commentRepo: commentRepo,
|
||||||
postRepo: postRepo,
|
postRepo: postRepo,
|
||||||
systemMessageService: systemMessageService,
|
systemMessageService: systemMessageService,
|
||||||
cache: cache.GetCache(),
|
cache: cacheBackend,
|
||||||
gorseClient: gorseClient,
|
gorseClient: gorseClient,
|
||||||
postAIService: postAIService,
|
postAIService: postAIService,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,9 +43,6 @@ type emailCodeServiceImpl struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func NewEmailCodeService(emailService EmailService, cacheBackend cache.Cache) EmailCodeService {
|
func NewEmailCodeService(emailService EmailService, cacheBackend cache.Cache) EmailCodeService {
|
||||||
if cacheBackend == nil {
|
|
||||||
cacheBackend = cache.GetCache()
|
|
||||||
}
|
|
||||||
return &emailCodeServiceImpl{
|
return &emailCodeServiceImpl{
|
||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
cache: cacheBackend,
|
cache: cacheBackend,
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"carrot_bbs/internal/cache"
|
"carrot_bbs/internal/cache"
|
||||||
|
apperrors "carrot_bbs/internal/errors"
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
"carrot_bbs/internal/pkg/sse"
|
"carrot_bbs/internal/pkg/sse"
|
||||||
"carrot_bbs/internal/pkg/utils"
|
"carrot_bbs/internal/pkg/utils"
|
||||||
@@ -23,25 +24,26 @@ const (
|
|||||||
GroupCacheJitter = 0.1
|
GroupCacheJitter = 0.1
|
||||||
)
|
)
|
||||||
|
|
||||||
// 群组服务错误定义
|
// 群组服务错误定义 - 使用统一的 AppError
|
||||||
var (
|
var (
|
||||||
ErrGroupNotFound = &ServiceError{Code: 404, Message: "群组不存在"}
|
ErrGroupNotFound = apperrors.ErrGroupNotFound
|
||||||
ErrNotGroupMember = &ServiceError{Code: 403, Message: "不是群成员"}
|
ErrNotGroupMember = apperrors.ErrNotGroupMember
|
||||||
ErrNotGroupAdmin = &ServiceError{Code: 403, Message: "不是群管理员"}
|
ErrNotGroupAdmin = apperrors.ErrNotGroupAdmin
|
||||||
ErrNotGroupOwner = &ServiceError{Code: 403, Message: "不是群主"}
|
ErrNotGroupOwner = apperrors.ErrNotGroupOwner
|
||||||
ErrGroupFull = &ServiceError{Code: 400, Message: "群已满"}
|
ErrGroupFull = apperrors.ErrGroupFull
|
||||||
ErrAlreadyMember = &ServiceError{Code: 400, Message: "已经是群成员"}
|
ErrAlreadyMember = apperrors.ErrAlreadyGroupMember
|
||||||
ErrCannotRemoveOwner = &ServiceError{Code: 400, Message: "不能移除群主"}
|
ErrCannotRemoveOwner = apperrors.ErrCannotRemoveOwner
|
||||||
ErrCannotMuteOwner = &ServiceError{Code: 400, Message: "不能禁言群主"}
|
ErrCannotMuteOwner = apperrors.ErrCannotMuteOwner
|
||||||
ErrMuted = &ServiceError{Code: 403, Message: "你已被禁言"}
|
ErrMuted = apperrors.ErrMuted
|
||||||
ErrMuteAllEnabled = &ServiceError{Code: 403, Message: "全员禁言中"}
|
ErrMuteAllEnabled = apperrors.ErrMuteAllEnabled
|
||||||
ErrCannotJoin = &ServiceError{Code: 400, Message: "该群不允许加入"}
|
ErrCannotJoin = apperrors.ErrCannotJoin
|
||||||
ErrJoinRequestPending = &ServiceError{Code: 400, Message: "加群申请已提交"}
|
ErrJoinRequestPending = apperrors.ErrJoinRequestPending
|
||||||
ErrGroupRequestNotFound = &ServiceError{Code: 404, Message: "加群请求不存在"}
|
ErrGroupRequestNotFound = apperrors.ErrGroupRequestNotFound
|
||||||
ErrGroupRequestHandled = &ServiceError{Code: 400, Message: "该加群请求已处理"}
|
ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled
|
||||||
ErrNotRequestTarget = &ServiceError{Code: 400, Message: "不是邀请目标用户"}
|
ErrNotRequestTarget = apperrors.ErrNotRequestTarget
|
||||||
ErrNoEligibleInvitee = &ServiceError{Code: 400, Message: "没有可邀请的用户"}
|
ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee
|
||||||
ErrNotMutualFollow = &ServiceError{Code: 400, Message: "仅支持邀请互相关注用户"}
|
ErrNotMutualFollow = apperrors.ErrNotMutualFollow
|
||||||
|
ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted
|
||||||
)
|
)
|
||||||
|
|
||||||
// GroupService 群组服务接口
|
// GroupService 群组服务接口
|
||||||
@@ -104,7 +106,7 @@ type groupService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewGroupService 创建群组服务
|
// NewGroupService 创建群组服务
|
||||||
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub) GroupService {
|
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub, cacheBackend cache.Cache) GroupService {
|
||||||
return &groupService{
|
return &groupService{
|
||||||
db: db,
|
db: db,
|
||||||
groupRepo: groupRepo,
|
groupRepo: groupRepo,
|
||||||
@@ -113,7 +115,7 @@ func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo
|
|||||||
requestRepo: repository.NewGroupJoinRequestRepository(db),
|
requestRepo: repository.NewGroupJoinRequestRepository(db),
|
||||||
notifyRepo: repository.NewSystemNotificationRepository(db),
|
notifyRepo: repository.NewSystemNotificationRepository(db),
|
||||||
sseHub: sseHub,
|
sseHub: sseHub,
|
||||||
cache: cache.GetCache(),
|
cache: cacheBackend,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -937,7 +939,7 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
|
|||||||
if req.RequestType == model.GroupJoinRequestTypeInvite {
|
if req.RequestType == model.GroupJoinRequestTypeInvite {
|
||||||
if req.ReviewerID == "" {
|
if req.ReviewerID == "" {
|
||||||
// 被邀请人还未同意,管理员不能提前审批
|
// 被邀请人还未同意,管理员不能提前审批
|
||||||
return &ServiceError{Code: 400, Message: "被邀请人尚未同意邀请,无法审批"}
|
return ErrInviteNotAccepted
|
||||||
}
|
}
|
||||||
if req.ReviewerID != req.TargetUserID {
|
if req.ReviewerID != req.TargetUserID {
|
||||||
// ReviewerID 不是被邀请人,说明已经被其他人处理过
|
// ReviewerID 不是被邀请人,说明已经被其他人处理过
|
||||||
|
|||||||
@@ -37,14 +37,14 @@ type MessageService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewMessageService 创建消息服务
|
// NewMessageService 创建消息服务
|
||||||
func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *MessageService {
|
func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository, cacheBackend cache.Cache) *MessageService {
|
||||||
// 创建适配器
|
// 创建适配器
|
||||||
convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo)
|
convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo)
|
||||||
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
|
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
|
||||||
|
|
||||||
// 创建会话缓存
|
// 创建会话缓存
|
||||||
conversationCache := cache.NewConversationCache(
|
conversationCache := cache.NewConversationCache(
|
||||||
cache.GetCache(),
|
cacheBackend,
|
||||||
convRepoAdapter,
|
convRepoAdapter,
|
||||||
msgRepoAdapter,
|
msgRepoAdapter,
|
||||||
cache.DefaultConversationCacheSettings(),
|
cache.DefaultConversationCacheSettings(),
|
||||||
@@ -54,7 +54,7 @@ func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *
|
|||||||
db: db,
|
db: db,
|
||||||
messageRepo: messageRepo,
|
messageRepo: messageRepo,
|
||||||
conversationCache: conversationCache,
|
conversationCache: conversationCache,
|
||||||
baseCache: cache.GetCache(),
|
baseCache: cacheBackend,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"carrot_bbs/internal/cache"
|
"carrot_bbs/internal/cache"
|
||||||
|
apperrors "carrot_bbs/internal/errors"
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
"carrot_bbs/internal/repository"
|
"carrot_bbs/internal/repository"
|
||||||
)
|
)
|
||||||
@@ -23,10 +24,10 @@ type NotificationService struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewNotificationService 创建通知服务
|
// NewNotificationService 创建通知服务
|
||||||
func NewNotificationService(notificationRepo *repository.NotificationRepository) *NotificationService {
|
func NewNotificationService(notificationRepo *repository.NotificationRepository, cacheBackend cache.Cache) *NotificationService {
|
||||||
return &NotificationService{
|
return &NotificationService{
|
||||||
notificationRepo: notificationRepo,
|
notificationRepo: notificationRepo,
|
||||||
cache: cache.GetCache(),
|
cache: cacheBackend,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,4 +167,4 @@ func (s *NotificationService) ClearAllNotifications(ctx context.Context, userID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 错误定义
|
// 错误定义
|
||||||
var ErrUnauthorizedNotification = &ServiceError{Code: 403, Message: "unauthorized to delete this notification"}
|
var ErrUnauthorizedNotification = apperrors.ErrUnauthorizedNotification
|
||||||
|
|||||||
@@ -23,23 +23,63 @@ const (
|
|||||||
anonymousViewUserID = "_anon_view"
|
anonymousViewUserID = "_anon_view"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PostService 帖子服务
|
// PostService 帖子服务接口
|
||||||
type PostService struct {
|
type PostService interface {
|
||||||
|
// 帖子CRUD
|
||||||
|
Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error)
|
||||||
|
GetByID(ctx context.Context, id string) (*model.Post, error)
|
||||||
|
Update(ctx context.Context, post *model.Post) error
|
||||||
|
UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error
|
||||||
|
Delete(ctx context.Context, id string) error
|
||||||
|
|
||||||
|
// 帖子列表
|
||||||
|
List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error)
|
||||||
|
GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
|
||||||
|
GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
|
||||||
|
GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error)
|
||||||
|
GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
|
||||||
|
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error)
|
||||||
|
|
||||||
|
// 关注和推荐
|
||||||
|
GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
|
||||||
|
GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error)
|
||||||
|
GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
|
||||||
|
|
||||||
|
// 交互功能
|
||||||
|
Like(ctx context.Context, postID, userID string) error
|
||||||
|
Unlike(ctx context.Context, postID, userID string) error
|
||||||
|
IsLiked(ctx context.Context, postID, userID string) bool
|
||||||
|
Favorite(ctx context.Context, postID, userID string) error
|
||||||
|
Unfavorite(ctx context.Context, postID, userID string) error
|
||||||
|
IsFavorited(ctx context.Context, postID, userID string) bool
|
||||||
|
|
||||||
|
// 交互状态查询
|
||||||
|
GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error)
|
||||||
|
GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool)
|
||||||
|
|
||||||
|
// 其他
|
||||||
|
IncrementViews(ctx context.Context, postID, userID string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// postServiceImpl 帖子服务实现
|
||||||
|
type postServiceImpl struct {
|
||||||
postRepo *repository.PostRepository
|
postRepo *repository.PostRepository
|
||||||
systemMessageService SystemMessageService
|
systemMessageService SystemMessageService
|
||||||
cache cache.Cache
|
cache cache.Cache
|
||||||
gorseClient gorse.Client
|
gorseClient gorse.Client
|
||||||
postAIService *PostAIService
|
postAIService *PostAIService
|
||||||
|
txManager repository.TransactionManager // 事务管理器
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPostService 创建帖子服务
|
// NewPostService 创建帖子服务
|
||||||
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *PostService {
|
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager) PostService {
|
||||||
return &PostService{
|
return &postServiceImpl{
|
||||||
postRepo: postRepo,
|
postRepo: postRepo,
|
||||||
systemMessageService: systemMessageService,
|
systemMessageService: systemMessageService,
|
||||||
cache: cache.GetCache(),
|
cache: cacheBackend,
|
||||||
gorseClient: gorseClient,
|
gorseClient: gorseClient,
|
||||||
postAIService: postAIService,
|
postAIService: postAIService,
|
||||||
|
txManager: txManager,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +90,7 @@ type PostListResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Create 创建帖子
|
// Create 创建帖子
|
||||||
func (s *PostService) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) {
|
func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) {
|
||||||
post := &model.Post{
|
post := &model.Post{
|
||||||
UserID: userID,
|
UserID: userID,
|
||||||
Title: title,
|
Title: title,
|
||||||
@@ -73,7 +113,7 @@ func (s *PostService) Create(ctx context.Context, userID, title, content string,
|
|||||||
return s.postRepo.GetByID(post.ID)
|
return s.postRepo.GetByID(post.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostService) reviewPostAsync(postID, userID, title, content string, images []string) {
|
func (s *postServiceImpl) reviewPostAsync(postID, userID, title, content string, images []string) {
|
||||||
defer func() {
|
defer func() {
|
||||||
if r := recover(); r != nil {
|
if r := recover(); r != nil {
|
||||||
log.Printf("[ERROR] Panic in post moderation async flow, fallback publish post=%s panic=%v", postID, r)
|
log.Printf("[ERROR] Panic in post moderation async flow, fallback publish post=%s panic=%v", postID, r)
|
||||||
@@ -139,7 +179,7 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostService) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
|
func (s *postServiceImpl) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
|
||||||
const maxAttempts = 3
|
const maxAttempts = 3
|
||||||
const retryDelay = 200 * time.Millisecond
|
const retryDelay = 200 * time.Millisecond
|
||||||
|
|
||||||
@@ -159,12 +199,12 @@ func (s *PostService) updateModerationStatusWithRetry(postID string, status mode
|
|||||||
return lastErr
|
return lastErr
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostService) invalidatePostCaches(postID string) {
|
func (s *postServiceImpl) invalidatePostCaches(postID string) {
|
||||||
cache.InvalidatePostDetail(s.cache, postID)
|
cache.InvalidatePostDetail(s.cache, postID)
|
||||||
cache.InvalidatePostList(s.cache)
|
cache.InvalidatePostList(s.cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *PostService) notifyModerationRejected(userID, reason string) {
|
func (s *postServiceImpl) notifyModerationRejected(userID, reason string) {
|
||||||
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
|
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -187,17 +227,17 @@ func (s *PostService) notifyModerationRejected(userID, reason string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetByID 根据ID获取帖子
|
// GetByID 根据ID获取帖子
|
||||||
func (s *PostService) GetByID(ctx context.Context, id string) (*model.Post, error) {
|
func (s *postServiceImpl) GetByID(ctx context.Context, id string) (*model.Post, error) {
|
||||||
return s.postRepo.GetByID(id)
|
return s.postRepo.GetByID(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update 更新帖子
|
// Update 更新帖子
|
||||||
func (s *PostService) Update(ctx context.Context, post *model.Post) error {
|
func (s *postServiceImpl) Update(ctx context.Context, post *model.Post) error {
|
||||||
return s.UpdateWithImages(ctx, post, nil)
|
return s.UpdateWithImages(ctx, post, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// UpdateWithImages 更新帖子并可选更新图片(images=nil 表示不更新图片)
|
// UpdateWithImages 更新帖子并可选更新图片(images=nil 表示不更新图片)
|
||||||
func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error {
|
func (s *postServiceImpl) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error {
|
||||||
err := s.postRepo.UpdateWithImages(post, images)
|
err := s.postRepo.UpdateWithImages(post, images)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -211,7 +251,7 @@ func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, im
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Delete 删除帖子
|
// Delete 删除帖子
|
||||||
func (s *PostService) Delete(ctx context.Context, id string) error {
|
func (s *postServiceImpl) Delete(ctx context.Context, id string) error {
|
||||||
err := s.postRepo.Delete(id)
|
err := s.postRepo.Delete(id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -234,7 +274,7 @@ func (s *PostService) Delete(ctx context.Context, id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// List 获取帖子列表(带缓存)
|
// List 获取帖子列表(带缓存)
|
||||||
func (s *PostService) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) {
|
||||||
cacheSettings := cache.GetSettings()
|
cacheSettings := cache.GetSettings()
|
||||||
postListTTL := cacheSettings.PostListTTL
|
postListTTL := cacheSettings.PostListTTL
|
||||||
if postListTTL <= 0 {
|
if postListTTL <= 0 {
|
||||||
@@ -299,22 +339,22 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestPosts 获取最新帖子(语义化别名)
|
// GetLatestPosts 获取最新帖子(语义化别名)
|
||||||
func (s *PostService) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
|
||||||
return s.List(ctx, page, pageSize, userID, false)
|
return s.List(ctx, page, pageSize, userID, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核)
|
// GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核)
|
||||||
func (s *PostService) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
|
||||||
return s.List(ctx, page, pageSize, userID, true)
|
return s.List(ctx, page, pageSize, userID, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserPosts 获取用户帖子
|
// GetUserPosts 获取用户帖子
|
||||||
func (s *PostService) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
|
||||||
return s.postRepo.GetUserPosts(userID, page, pageSize, includePending)
|
return s.postRepo.GetUserPosts(userID, page, pageSize, includePending)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Like 点赞
|
// Like 点赞
|
||||||
func (s *PostService) Like(ctx context.Context, postID, userID string) error {
|
func (s *postServiceImpl) Like(ctx context.Context, postID, userID string) error {
|
||||||
// 获取帖子信息用于发送通知
|
// 获取帖子信息用于发送通知
|
||||||
post, err := s.postRepo.GetByID(postID)
|
post, err := s.postRepo.GetByID(postID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -352,7 +392,7 @@ func (s *PostService) Like(ctx context.Context, postID, userID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unlike 取消点赞
|
// Unlike 取消点赞
|
||||||
func (s *PostService) Unlike(ctx context.Context, postID, userID string) error {
|
func (s *postServiceImpl) Unlike(ctx context.Context, postID, userID string) error {
|
||||||
err := s.postRepo.Unlike(postID, userID)
|
err := s.postRepo.Unlike(postID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -374,12 +414,12 @@ func (s *PostService) Unlike(ctx context.Context, postID, userID string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsLiked 检查是否点赞
|
// IsLiked 检查是否点赞
|
||||||
func (s *PostService) IsLiked(ctx context.Context, postID, userID string) bool {
|
func (s *postServiceImpl) IsLiked(ctx context.Context, postID, userID string) bool {
|
||||||
return s.postRepo.IsLiked(postID, userID)
|
return s.postRepo.IsLiked(postID, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Favorite 收藏
|
// Favorite 收藏
|
||||||
func (s *PostService) Favorite(ctx context.Context, postID, userID string) error {
|
func (s *postServiceImpl) Favorite(ctx context.Context, postID, userID string) error {
|
||||||
// 获取帖子信息用于发送通知
|
// 获取帖子信息用于发送通知
|
||||||
post, err := s.postRepo.GetByID(postID)
|
post, err := s.postRepo.GetByID(postID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -417,7 +457,7 @@ func (s *PostService) Favorite(ctx context.Context, postID, userID string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Unfavorite 取消收藏
|
// Unfavorite 取消收藏
|
||||||
func (s *PostService) Unfavorite(ctx context.Context, postID, userID string) error {
|
func (s *postServiceImpl) Unfavorite(ctx context.Context, postID, userID string) error {
|
||||||
err := s.postRepo.Unfavorite(postID, userID)
|
err := s.postRepo.Unfavorite(postID, userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -439,12 +479,12 @@ func (s *PostService) Unfavorite(ctx context.Context, postID, userID string) err
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsFavorited 检查是否收藏
|
// IsFavorited 检查是否收藏
|
||||||
func (s *PostService) IsFavorited(ctx context.Context, postID, userID string) bool {
|
func (s *postServiceImpl) IsFavorited(ctx context.Context, postID, userID string) bool {
|
||||||
return s.postRepo.IsFavorited(postID, userID)
|
return s.postRepo.IsFavorited(postID, userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPostInteractionStatus 批量获取帖子的交互状态(点赞、收藏)
|
// GetPostInteractionStatus 批量获取帖子的交互状态(点赞、收藏)
|
||||||
func (s *PostService) GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error) {
|
func (s *postServiceImpl) GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error) {
|
||||||
isLikedMap := make(map[string]bool)
|
isLikedMap := make(map[string]bool)
|
||||||
isFavoritedMap := make(map[string]bool)
|
isFavoritedMap := make(map[string]bool)
|
||||||
|
|
||||||
@@ -461,7 +501,7 @@ func (s *PostService) GetPostInteractionStatus(ctx context.Context, postIDs []st
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPostInteractionStatusSingle 获取单个帖子的交互状态
|
// GetPostInteractionStatusSingle 获取单个帖子的交互状态
|
||||||
func (s *PostService) GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool) {
|
func (s *postServiceImpl) GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool) {
|
||||||
if userID == "" {
|
if userID == "" {
|
||||||
return false, false
|
return false, false
|
||||||
}
|
}
|
||||||
@@ -469,7 +509,7 @@ func (s *PostService) GetPostInteractionStatusSingle(ctx context.Context, postID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IncrementViews 增加帖子观看量并同步到Gorse
|
// IncrementViews 增加帖子观看量并同步到Gorse
|
||||||
func (s *PostService) IncrementViews(ctx context.Context, postID, userID string) error {
|
func (s *postServiceImpl) IncrementViews(ctx context.Context, postID, userID string) error {
|
||||||
if err := s.postRepo.IncrementViews(postID); err != nil {
|
if err := s.postRepo.IncrementViews(postID); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -494,17 +534,17 @@ func (s *PostService) IncrementViews(ctx context.Context, postID, userID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetFavorites 获取收藏列表
|
// GetFavorites 获取收藏列表
|
||||||
func (s *PostService) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||||
return s.postRepo.GetFavorites(userID, page, pageSize)
|
return s.postRepo.GetFavorites(userID, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Search 搜索帖子
|
// Search 搜索帖子
|
||||||
func (s *PostService) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||||
return s.postRepo.Search(keyword, page, pageSize)
|
return s.postRepo.Search(keyword, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFollowingPosts 获取关注用户的帖子(带缓存)
|
// GetFollowingPosts 获取关注用户的帖子(带缓存)
|
||||||
func (s *PostService) GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||||
cacheSettings := cache.GetSettings()
|
cacheSettings := cache.GetSettings()
|
||||||
postListTTL := cacheSettings.PostListTTL
|
postListTTL := cacheSettings.PostListTTL
|
||||||
if postListTTL <= 0 {
|
if postListTTL <= 0 {
|
||||||
@@ -546,7 +586,7 @@ func (s *PostService) GetFollowingPosts(ctx context.Context, userID string, page
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetHotPosts 获取热门帖子(使用Gorse非个性化推荐)
|
// GetHotPosts 获取热门帖子(使用Gorse非个性化推荐)
|
||||||
func (s *PostService) GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
|
||||||
// 如果Gorse启用,使用自定义的非个性化推荐器
|
// 如果Gorse启用,使用自定义的非个性化推荐器
|
||||||
if s.gorseClient.IsEnabled() {
|
if s.gorseClient.IsEnabled() {
|
||||||
offset := (page - 1) * pageSize
|
offset := (page - 1) * pageSize
|
||||||
@@ -580,7 +620,7 @@ func (s *PostService) GetHotPosts(ctx context.Context, page, pageSize int) ([]*m
|
|||||||
}
|
}
|
||||||
|
|
||||||
// getHotPostsFromDB 从数据库获取热门帖子(降级路径)
|
// getHotPostsFromDB 从数据库获取热门帖子(降级路径)
|
||||||
func (s *PostService) getHotPostsFromDB(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) getHotPostsFromDB(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
|
||||||
// 直接查询数据库,不再使用本地缓存(Gorse失败降级时使用)
|
// 直接查询数据库,不再使用本地缓存(Gorse失败降级时使用)
|
||||||
posts, total, err := s.postRepo.GetHotPosts(page, pageSize)
|
posts, total, err := s.postRepo.GetHotPosts(page, pageSize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -590,7 +630,7 @@ func (s *PostService) getHotPostsFromDB(ctx context.Context, page, pageSize int)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRecommendedPosts 获取推荐帖子
|
// GetRecommendedPosts 获取推荐帖子
|
||||||
func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
func (s *postServiceImpl) GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
|
||||||
// 如果Gorse未启用或用户未登录,降级为热门帖子
|
// 如果Gorse未启用或用户未登录,降级为热门帖子
|
||||||
if !s.gorseClient.IsEnabled() || userID == "" {
|
if !s.gorseClient.IsEnabled() || userID == "" {
|
||||||
return s.GetHotPosts(ctx, page, pageSize)
|
return s.GetHotPosts(ctx, page, pageSize)
|
||||||
@@ -638,7 +678,7 @@ func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, pa
|
|||||||
}
|
}
|
||||||
|
|
||||||
// buildPostCategories 构建帖子的类别标签
|
// buildPostCategories 构建帖子的类别标签
|
||||||
func (s *PostService) buildPostCategories(post *model.Post) []string {
|
func (s *postServiceImpl) buildPostCategories(post *model.Post) []string {
|
||||||
var categories []string
|
var categories []string
|
||||||
|
|
||||||
// 热度标签
|
// 热度标签
|
||||||
@@ -676,3 +716,17 @@ func (s *PostService) buildPostCategories(post *model.Post) []string {
|
|||||||
|
|
||||||
return categories
|
return categories
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ========== 事务管理器示例方法 ==========
|
||||||
|
|
||||||
|
// DeletePostWithTransaction 使用事务管理器删除帖子(示例)
|
||||||
|
// 此方法展示如何在 Service 层使用事务管理器控制跨多个 Repository 的事务
|
||||||
|
// 当需要在一个事务中执行多个 Repository 操作时,可以使用此模式
|
||||||
|
func (s *postServiceImpl) DeletePostWithTransaction(ctx context.Context, postID string) error {
|
||||||
|
// 使用事务管理器执行事务
|
||||||
|
return s.txManager.RunInTransaction(ctx, func(ctx context.Context) error {
|
||||||
|
// 在同一个事务中执行删除操作
|
||||||
|
// Repository 方法会通过 context 获取事务 TX
|
||||||
|
return s.postRepo.DeleteWithContext(ctx, postID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"carrot_bbs/internal/dto"
|
"carrot_bbs/internal/dto"
|
||||||
|
apperrors "carrot_bbs/internal/errors"
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
"carrot_bbs/internal/repository"
|
"carrot_bbs/internal/repository"
|
||||||
|
|
||||||
@@ -15,10 +16,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrInvalidSchedulePayload = &ServiceError{Code: 400, Message: "invalid schedule payload"}
|
ErrInvalidSchedulePayload = apperrors.ErrInvalidSchedulePayload
|
||||||
ErrScheduleCourseNotFound = &ServiceError{Code: 404, Message: "schedule course not found"}
|
ErrScheduleCourseNotFound = apperrors.ErrScheduleCourseNotFound
|
||||||
ErrScheduleForbidden = &ServiceError{Code: 403, Message: "forbidden schedule operation"}
|
ErrScheduleForbidden = apperrors.ErrScheduleForbidden
|
||||||
ErrScheduleColorDuplicated = &ServiceError{Code: 400, Message: "course color already used"}
|
ErrScheduleColorDuplicated = apperrors.ErrScheduleColorDuplicated
|
||||||
)
|
)
|
||||||
|
|
||||||
var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`)
|
var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`)
|
||||||
|
|||||||
@@ -1,16 +1,18 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"carrot_bbs/internal/model"
|
|
||||||
"carrot_bbs/internal/repository"
|
|
||||||
"errors"
|
"errors"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
apperrors "carrot_bbs/internal/errors"
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrStickerAlreadyExists = &ServiceError{Code: 400, Message: "sticker already exists"}
|
ErrStickerAlreadyExists = apperrors.ErrStickerAlreadyExists
|
||||||
ErrInvalidStickerURL = &ServiceError{Code: 400, Message: "invalid sticker url"}
|
ErrInvalidStickerURL = apperrors.ErrInvalidStickerURL
|
||||||
)
|
)
|
||||||
|
|
||||||
// StickerService 自定义表情服务接口
|
// StickerService 自定义表情服务接口
|
||||||
|
|||||||
@@ -42,13 +42,14 @@ func NewSystemMessageService(
|
|||||||
pushService PushService,
|
pushService PushService,
|
||||||
userRepo *repository.UserRepository,
|
userRepo *repository.UserRepository,
|
||||||
postRepo *repository.PostRepository,
|
postRepo *repository.PostRepository,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
) SystemMessageService {
|
) SystemMessageService {
|
||||||
return &systemMessageServiceImpl{
|
return &systemMessageServiceImpl{
|
||||||
notifyRepo: notifyRepo,
|
notifyRepo: notifyRepo,
|
||||||
pushService: pushService,
|
pushService: pushService,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
postRepo: postRepo,
|
postRepo: postRepo,
|
||||||
cache: cache.GetCache(),
|
cache: cacheBackend,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"carrot_bbs/internal/pkg/s3"
|
"carrot_bbs/internal/pkg/s3"
|
||||||
|
|
||||||
_ "golang.org/x/image/bmp"
|
_ "golang.org/x/image/bmp"
|
||||||
_ "golang.org/x/image/tiff"
|
_ "golang.org/x/image/tiff"
|
||||||
)
|
)
|
||||||
@@ -23,11 +24,11 @@ import (
|
|||||||
// UploadService 上传服务
|
// UploadService 上传服务
|
||||||
type UploadService struct {
|
type UploadService struct {
|
||||||
s3Client *s3.Client
|
s3Client *s3.Client
|
||||||
userService *UserService
|
userService UserService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewUploadService 创建上传服务
|
// NewUploadService 创建上传服务
|
||||||
func NewUploadService(s3Client *s3.Client, userService *UserService) *UploadService {
|
func NewUploadService(s3Client *s3.Client, userService UserService) *UploadService {
|
||||||
return &UploadService{
|
return &UploadService{
|
||||||
s3Client: s3Client,
|
s3Client: s3Client,
|
||||||
userService: userService,
|
userService: userService,
|
||||||
|
|||||||
@@ -6,13 +6,59 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"carrot_bbs/internal/cache"
|
"carrot_bbs/internal/cache"
|
||||||
|
apperrors "carrot_bbs/internal/errors"
|
||||||
"carrot_bbs/internal/model"
|
"carrot_bbs/internal/model"
|
||||||
"carrot_bbs/internal/pkg/utils"
|
"carrot_bbs/internal/pkg/utils"
|
||||||
"carrot_bbs/internal/repository"
|
"carrot_bbs/internal/repository"
|
||||||
)
|
)
|
||||||
|
|
||||||
// UserService 用户服务
|
// UserService 用户服务接口
|
||||||
type UserService struct {
|
type UserService interface {
|
||||||
|
// 验证码相关
|
||||||
|
SendRegisterCode(ctx context.Context, email string) error
|
||||||
|
SendPasswordResetCode(ctx context.Context, email string) error
|
||||||
|
SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error
|
||||||
|
SendChangePasswordCode(ctx context.Context, userID string) error
|
||||||
|
|
||||||
|
// 邮箱验证
|
||||||
|
VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error
|
||||||
|
|
||||||
|
// 用户认证
|
||||||
|
Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error)
|
||||||
|
Login(ctx context.Context, account, password string) (*model.User, error)
|
||||||
|
|
||||||
|
// 用户查询
|
||||||
|
GetUserByID(ctx context.Context, id string) (*model.User, error)
|
||||||
|
GetUserPostCount(ctx context.Context, userID string) (int64, error)
|
||||||
|
GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error)
|
||||||
|
GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error)
|
||||||
|
GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error)
|
||||||
|
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error)
|
||||||
|
CheckUsernameAvailable(ctx context.Context, username string) (bool, error)
|
||||||
|
|
||||||
|
// 用户更新
|
||||||
|
UpdateUser(ctx context.Context, user *model.User) error
|
||||||
|
ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error
|
||||||
|
ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error
|
||||||
|
|
||||||
|
// 关注相关
|
||||||
|
GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||||
|
GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
|
||||||
|
GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
|
||||||
|
GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
|
||||||
|
FollowUser(ctx context.Context, followerID, followeeID string) error
|
||||||
|
UnfollowUser(ctx context.Context, followerID, followeeID string) error
|
||||||
|
GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error)
|
||||||
|
|
||||||
|
// 黑名单相关
|
||||||
|
BlockUser(ctx context.Context, blockerID, blockedID string) error
|
||||||
|
UnblockUser(ctx context.Context, blockerID, blockedID string) error
|
||||||
|
GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error)
|
||||||
|
IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// userServiceImpl 用户服务实现
|
||||||
|
type userServiceImpl struct {
|
||||||
userRepo *repository.UserRepository
|
userRepo *repository.UserRepository
|
||||||
systemMessageService SystemMessageService
|
systemMessageService SystemMessageService
|
||||||
emailCodeService EmailCodeService
|
emailCodeService EmailCodeService
|
||||||
@@ -24,8 +70,8 @@ func NewUserService(
|
|||||||
systemMessageService SystemMessageService,
|
systemMessageService SystemMessageService,
|
||||||
emailService EmailService,
|
emailService EmailService,
|
||||||
cacheBackend cache.Cache,
|
cacheBackend cache.Cache,
|
||||||
) *UserService {
|
) UserService {
|
||||||
return &UserService{
|
return &userServiceImpl{
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
systemMessageService: systemMessageService,
|
systemMessageService: systemMessageService,
|
||||||
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
|
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
|
||||||
@@ -33,7 +79,7 @@ func NewUserService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendRegisterCode 发送注册验证码
|
// SendRegisterCode 发送注册验证码
|
||||||
func (s *UserService) SendRegisterCode(ctx context.Context, email string) error {
|
func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email string) error {
|
||||||
user, err := s.userRepo.GetByEmail(email)
|
user, err := s.userRepo.GetByEmail(email)
|
||||||
if err == nil && user != nil {
|
if err == nil && user != nil {
|
||||||
return ErrEmailExists
|
return ErrEmailExists
|
||||||
@@ -42,7 +88,7 @@ func (s *UserService) SendRegisterCode(ctx context.Context, email string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendPasswordResetCode 发送找回密码验证码
|
// SendPasswordResetCode 发送找回密码验证码
|
||||||
func (s *UserService) SendPasswordResetCode(ctx context.Context, email string) error {
|
func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email string) error {
|
||||||
user, err := s.userRepo.GetByEmail(email)
|
user, err := s.userRepo.GetByEmail(email)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
return ErrUserNotFound
|
return ErrUserNotFound
|
||||||
@@ -51,7 +97,7 @@ func (s *UserService) SendPasswordResetCode(ctx context.Context, email string) e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendCurrentUserEmailVerifyCode 发送当前用户邮箱验证验证码
|
// SendCurrentUserEmailVerifyCode 发送当前用户邮箱验证验证码
|
||||||
func (s *UserService) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error {
|
func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error {
|
||||||
user, err := s.userRepo.GetByID(userID)
|
user, err := s.userRepo.GetByID(userID)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
return ErrUserNotFound
|
return ErrUserNotFound
|
||||||
@@ -78,7 +124,7 @@ func (s *UserService) SendCurrentUserEmailVerifyCode(ctx context.Context, userID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// VerifyCurrentUserEmail 验证当前用户邮箱
|
// VerifyCurrentUserEmail 验证当前用户邮箱
|
||||||
func (s *UserService) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error {
|
func (s *userServiceImpl) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error {
|
||||||
user, err := s.userRepo.GetByID(userID)
|
user, err := s.userRepo.GetByID(userID)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
return ErrUserNotFound
|
return ErrUserNotFound
|
||||||
@@ -107,7 +153,7 @@ func (s *UserService) VerifyCurrentUserEmail(ctx context.Context, userID, email,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SendChangePasswordCode 发送修改密码验证码
|
// SendChangePasswordCode 发送修改密码验证码
|
||||||
func (s *UserService) SendChangePasswordCode(ctx context.Context, userID string) error {
|
func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID string) error {
|
||||||
user, err := s.userRepo.GetByID(userID)
|
user, err := s.userRepo.GetByID(userID)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
return ErrUserNotFound
|
return ErrUserNotFound
|
||||||
@@ -119,7 +165,7 @@ func (s *UserService) SendChangePasswordCode(ctx context.Context, userID string)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Register 用户注册
|
// Register 用户注册
|
||||||
func (s *UserService) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) {
|
func (s *userServiceImpl) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) {
|
||||||
// 验证用户名
|
// 验证用户名
|
||||||
if !utils.ValidateUsername(username) {
|
if !utils.ValidateUsername(username) {
|
||||||
return nil, ErrInvalidUsername
|
return nil, ErrInvalidUsername
|
||||||
@@ -199,7 +245,7 @@ func (s *UserService) Register(ctx context.Context, username, email, password, n
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Login 用户登录
|
// Login 用户登录
|
||||||
func (s *UserService) Login(ctx context.Context, account, password string) (*model.User, error) {
|
func (s *userServiceImpl) Login(ctx context.Context, account, password string) (*model.User, error) {
|
||||||
account = strings.TrimSpace(account)
|
account = strings.TrimSpace(account)
|
||||||
var (
|
var (
|
||||||
user *model.User
|
user *model.User
|
||||||
@@ -228,22 +274,22 @@ func (s *UserService) Login(ctx context.Context, account, password string) (*mod
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByID 根据ID获取用户
|
// GetUserByID 根据ID获取用户
|
||||||
func (s *UserService) GetUserByID(ctx context.Context, id string) (*model.User, error) {
|
func (s *userServiceImpl) GetUserByID(ctx context.Context, id string) (*model.User, error) {
|
||||||
return s.userRepo.GetByID(id)
|
return s.userRepo.GetByID(id)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserPostCount 获取用户帖子数(实时计算)
|
// GetUserPostCount 获取用户帖子数(实时计算)
|
||||||
func (s *UserService) GetUserPostCount(ctx context.Context, userID string) (int64, error) {
|
func (s *userServiceImpl) GetUserPostCount(ctx context.Context, userID string) (int64, error) {
|
||||||
return s.userRepo.GetPostsCount(userID)
|
return s.userRepo.GetPostsCount(userID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserPostCountBatch 批量获取用户帖子数(实时计算)
|
// GetUserPostCountBatch 批量获取用户帖子数(实时计算)
|
||||||
func (s *UserService) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) {
|
func (s *userServiceImpl) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) {
|
||||||
return s.userRepo.GetPostsCountBatch(userIDs)
|
return s.userRepo.GetPostsCountBatch(userIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByIDWithFollowingStatus 根据ID获取用户(包含当前用户是否关注的状态)
|
// GetUserByIDWithFollowingStatus 根据ID获取用户(包含当前用户是否关注的状态)
|
||||||
func (s *UserService) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) {
|
func (s *userServiceImpl) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) {
|
||||||
user, err := s.userRepo.GetByID(userID)
|
user, err := s.userRepo.GetByID(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
@@ -263,7 +309,7 @@ func (s *UserService) GetUserByIDWithFollowingStatus(ctx context.Context, userID
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetUserByIDWithMutualFollowStatus 根据ID获取用户(包含双向关注状态)
|
// GetUserByIDWithMutualFollowStatus 根据ID获取用户(包含双向关注状态)
|
||||||
func (s *UserService) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) {
|
func (s *userServiceImpl) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) {
|
||||||
user, err := s.userRepo.GetByID(userID)
|
user, err := s.userRepo.GetByID(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, false, err
|
return nil, false, false, err
|
||||||
@@ -290,22 +336,22 @@ func (s *UserService) GetUserByIDWithMutualFollowStatus(ctx context.Context, use
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateUser 更新用户
|
// UpdateUser 更新用户
|
||||||
func (s *UserService) UpdateUser(ctx context.Context, user *model.User) error {
|
func (s *userServiceImpl) UpdateUser(ctx context.Context, user *model.User) error {
|
||||||
return s.userRepo.Update(user)
|
return s.userRepo.Update(user)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFollowers 获取粉丝
|
// GetFollowers 获取粉丝
|
||||||
func (s *UserService) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
func (s *userServiceImpl) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||||
return s.userRepo.GetFollowers(userID, page, pageSize)
|
return s.userRepo.GetFollowers(userID, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFollowing 获取关注
|
// GetFollowing 获取关注
|
||||||
func (s *UserService) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
func (s *userServiceImpl) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||||
return s.userRepo.GetFollowing(userID, page, pageSize)
|
return s.userRepo.GetFollowing(userID, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// FollowUser 关注用户
|
// FollowUser 关注用户
|
||||||
func (s *UserService) FollowUser(ctx context.Context, followerID, followeeID string) error {
|
func (s *userServiceImpl) FollowUser(ctx context.Context, followerID, followeeID string) error {
|
||||||
blocked, err := s.userRepo.IsBlockedEitherDirection(followerID, followeeID)
|
blocked, err := s.userRepo.IsBlockedEitherDirection(followerID, followeeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -357,7 +403,7 @@ func (s *UserService) FollowUser(ctx context.Context, followerID, followeeID str
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnfollowUser 取消关注用户
|
// UnfollowUser 取消关注用户
|
||||||
func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID string) error {
|
func (s *userServiceImpl) UnfollowUser(ctx context.Context, followerID, followeeID string) error {
|
||||||
// 检查是否已经关注
|
// 检查是否已经关注
|
||||||
isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID)
|
isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -386,7 +432,7 @@ func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID s
|
|||||||
}
|
}
|
||||||
|
|
||||||
// BlockUser 拉黑用户,并自动清理双向关注/粉丝关系
|
// BlockUser 拉黑用户,并自动清理双向关注/粉丝关系
|
||||||
func (s *UserService) BlockUser(ctx context.Context, blockerID, blockedID string) error {
|
func (s *userServiceImpl) BlockUser(ctx context.Context, blockerID, blockedID string) error {
|
||||||
if blockerID == blockedID {
|
if blockerID == blockedID {
|
||||||
return ErrInvalidOperation
|
return ErrInvalidOperation
|
||||||
}
|
}
|
||||||
@@ -394,7 +440,7 @@ func (s *UserService) BlockUser(ctx context.Context, blockerID, blockedID string
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UnblockUser 取消拉黑
|
// UnblockUser 取消拉黑
|
||||||
func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID string) error {
|
func (s *userServiceImpl) UnblockUser(ctx context.Context, blockerID, blockedID string) error {
|
||||||
if blockerID == blockedID {
|
if blockerID == blockedID {
|
||||||
return ErrInvalidOperation
|
return ErrInvalidOperation
|
||||||
}
|
}
|
||||||
@@ -402,17 +448,17 @@ func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetBlockedUsers 获取黑名单列表
|
// GetBlockedUsers 获取黑名单列表
|
||||||
func (s *UserService) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) {
|
func (s *userServiceImpl) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) {
|
||||||
return s.userRepo.GetBlockedUsers(blockerID, page, pageSize)
|
return s.userRepo.GetBlockedUsers(blockerID, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// IsBlocked 检查当前用户是否已拉黑目标用户
|
// IsBlocked 检查当前用户是否已拉黑目标用户
|
||||||
func (s *UserService) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) {
|
func (s *userServiceImpl) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) {
|
||||||
return s.userRepo.IsBlocked(blockerID, blockedID)
|
return s.userRepo.IsBlocked(blockerID, blockedID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetFollowingList 获取关注列表(字符串参数版本)
|
// GetFollowingList 获取关注列表(字符串参数版本)
|
||||||
func (s *UserService) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
||||||
// 转换字符串参数为整数
|
// 转换字符串参数为整数
|
||||||
pageInt := 1
|
pageInt := 1
|
||||||
pageSizeInt := 20
|
pageSizeInt := 20
|
||||||
@@ -434,7 +480,7 @@ func (s *UserService) GetFollowingList(ctx context.Context, userID, page, pageSi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetFollowersList 获取粉丝列表(字符串参数版本)
|
// GetFollowersList 获取粉丝列表(字符串参数版本)
|
||||||
func (s *UserService) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
|
||||||
// 转换字符串参数为整数
|
// 转换字符串参数为整数
|
||||||
pageInt := 1
|
pageInt := 1
|
||||||
pageSizeInt := 20
|
pageSizeInt := 20
|
||||||
@@ -456,12 +502,12 @@ func (s *UserService) GetFollowersList(ctx context.Context, userID, page, pageSi
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetMutualFollowStatus 批量获取双向关注状态
|
// GetMutualFollowStatus 批量获取双向关注状态
|
||||||
func (s *UserService) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
|
func (s *userServiceImpl) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
|
||||||
return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs)
|
return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CheckUsernameAvailable 检查用户名是否可用
|
// CheckUsernameAvailable 检查用户名是否可用
|
||||||
func (s *UserService) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) {
|
func (s *userServiceImpl) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) {
|
||||||
user, err := s.userRepo.GetByUsername(username)
|
user, err := s.userRepo.GetByUsername(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return true, nil // 用户不存在,可用
|
return true, nil // 用户不存在,可用
|
||||||
@@ -470,7 +516,7 @@ func (s *UserService) CheckUsernameAvailable(ctx context.Context, username strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ChangePassword 修改密码
|
// ChangePassword 修改密码
|
||||||
func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error {
|
func (s *userServiceImpl) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error {
|
||||||
// 获取用户
|
// 获取用户
|
||||||
user, err := s.userRepo.GetByID(userID)
|
user, err := s.userRepo.GetByID(userID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -500,7 +546,7 @@ func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, n
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ResetPasswordByEmail 通过邮箱重置密码
|
// ResetPasswordByEmail 通过邮箱重置密码
|
||||||
func (s *UserService) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error {
|
func (s *userServiceImpl) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error {
|
||||||
email = strings.TrimSpace(email)
|
email = strings.TrimSpace(email)
|
||||||
if !utils.ValidateEmail(email) {
|
if !utils.ValidateEmail(email) {
|
||||||
return ErrInvalidEmail
|
return ErrInvalidEmail
|
||||||
@@ -526,40 +572,29 @@ func (s *UserService) ResetPasswordByEmail(ctx context.Context, email, verificat
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Search 搜索用户
|
// Search 搜索用户
|
||||||
func (s *UserService) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
func (s *userServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) {
|
||||||
return s.userRepo.Search(keyword, page, pageSize)
|
return s.userRepo.Search(keyword, page, pageSize)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 错误定义
|
// 错误定义 - 使用统一的 AppError
|
||||||
var (
|
var (
|
||||||
ErrInvalidUsername = &ServiceError{Code: 400, Message: "invalid username"}
|
ErrInvalidUsername = apperrors.ErrInvalidUsername
|
||||||
ErrInvalidEmail = &ServiceError{Code: 400, Message: "invalid email"}
|
ErrInvalidEmail = apperrors.ErrInvalidEmail
|
||||||
ErrInvalidPhone = &ServiceError{Code: 400, Message: "invalid phone number"}
|
ErrInvalidPhone = apperrors.ErrInvalidPhone
|
||||||
ErrWeakPassword = &ServiceError{Code: 400, Message: "password too weak"}
|
ErrWeakPassword = apperrors.ErrWeakPassword
|
||||||
ErrUsernameExists = &ServiceError{Code: 400, Message: "username already exists"}
|
ErrUsernameExists = apperrors.ErrUsernameExists
|
||||||
ErrEmailExists = &ServiceError{Code: 400, Message: "email already exists"}
|
ErrEmailExists = apperrors.ErrEmailExists
|
||||||
ErrPhoneExists = &ServiceError{Code: 400, Message: "phone number already exists"}
|
ErrPhoneExists = apperrors.ErrPhoneExists
|
||||||
ErrUserNotFound = &ServiceError{Code: 404, Message: "user not found"}
|
ErrUserNotFound = apperrors.ErrUserNotFound
|
||||||
ErrUserBanned = &ServiceError{Code: 403, Message: "user is banned"}
|
ErrUserBanned = apperrors.ErrUserBanned
|
||||||
ErrUserBlocked = &ServiceError{Code: 403, Message: "blocked relationship exists"}
|
ErrUserBlocked = apperrors.ErrUserBlocked
|
||||||
ErrInvalidOperation = &ServiceError{Code: 400, Message: "invalid operation"}
|
ErrInvalidOperation = apperrors.ErrInvalidOperation
|
||||||
ErrEmailServiceUnavailable = &ServiceError{Code: 503, Message: "email service unavailable"}
|
ErrEmailServiceUnavailable = apperrors.ErrEmailServiceUnavailable
|
||||||
ErrVerificationCodeTooFrequent = &ServiceError{Code: 429, Message: "verification code sent too frequently"}
|
ErrVerificationCodeTooFrequent = apperrors.ErrVerificationCodeTooFrequent
|
||||||
ErrVerificationCodeInvalid = &ServiceError{Code: 400, Message: "invalid verification code"}
|
ErrVerificationCodeInvalid = apperrors.ErrVerificationCodeInvalid
|
||||||
ErrVerificationCodeExpired = &ServiceError{Code: 400, Message: "verification code expired"}
|
ErrVerificationCodeExpired = apperrors.ErrVerificationCodeExpired
|
||||||
ErrVerificationCodeUnavailable = &ServiceError{Code: 500, Message: "verification code storage unavailable"}
|
ErrVerificationCodeUnavailable = apperrors.ErrVerificationCodeUnavailable
|
||||||
ErrEmailAlreadyVerified = &ServiceError{Code: 400, Message: "email already verified"}
|
ErrEmailAlreadyVerified = apperrors.ErrEmailAlreadyVerified
|
||||||
ErrEmailNotBound = &ServiceError{Code: 400, Message: "email not bound"}
|
ErrEmailNotBound = apperrors.ErrEmailNotBound
|
||||||
|
ErrInvalidCredentials = apperrors.ErrInvalidCredentials
|
||||||
)
|
)
|
||||||
|
|
||||||
// ServiceError 服务错误
|
|
||||||
type ServiceError struct {
|
|
||||||
Code int
|
|
||||||
Message string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *ServiceError) Error() string {
|
|
||||||
return e.Message
|
|
||||||
}
|
|
||||||
|
|
||||||
var ErrInvalidCredentials = &ServiceError{Code: 401, Message: "invalid username or password"}
|
|
||||||
|
|||||||
74
internal/wire/handler.go
Normal file
74
internal/wire/handler.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/cache"
|
||||||
|
"carrot_bbs/internal/config"
|
||||||
|
"carrot_bbs/internal/handler"
|
||||||
|
"carrot_bbs/internal/pkg/sse"
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
|
"carrot_bbs/internal/service"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// HandlerSet Handler 层 Provider Set
|
||||||
|
var HandlerSet = wire.NewSet(
|
||||||
|
// 直接使用构造函数的 Handler
|
||||||
|
handler.NewUserHandler,
|
||||||
|
handler.NewCommentHandler,
|
||||||
|
handler.NewNotificationHandler,
|
||||||
|
handler.NewUploadHandler,
|
||||||
|
handler.NewPushHandler,
|
||||||
|
handler.NewStickerHandler,
|
||||||
|
handler.NewVoteHandler,
|
||||||
|
handler.NewScheduleHandler,
|
||||||
|
|
||||||
|
// 需要特殊处理的 Handler
|
||||||
|
ProvidePostHandler,
|
||||||
|
ProvideMessageHandler,
|
||||||
|
ProvideSystemMessageHandler,
|
||||||
|
ProvideGroupHandler,
|
||||||
|
ProvideGorseHandler,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProvidePostHandler 提供帖子处理器
|
||||||
|
func ProvidePostHandler(
|
||||||
|
postService service.PostService,
|
||||||
|
userService service.UserService,
|
||||||
|
) *handler.PostHandler {
|
||||||
|
return handler.NewPostHandler(postService, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideMessageHandler 提供消息处理器
|
||||||
|
func ProvideMessageHandler(
|
||||||
|
chatService service.ChatService,
|
||||||
|
messageService *service.MessageService,
|
||||||
|
userService service.UserService,
|
||||||
|
groupService service.GroupService,
|
||||||
|
sseHub *sse.Hub,
|
||||||
|
) *handler.MessageHandler {
|
||||||
|
return handler.NewMessageHandler(chatService, messageService, userService, groupService, sseHub)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideSystemMessageHandler 提供系统消息处理器
|
||||||
|
func ProvideSystemMessageHandler(
|
||||||
|
systemMsgService service.SystemMessageService,
|
||||||
|
systemNotificationRepo *repository.SystemNotificationRepository,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) *handler.SystemMessageHandler {
|
||||||
|
return handler.NewSystemMessageHandler(systemMsgService, systemNotificationRepo, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideGroupHandler 提供群组处理器
|
||||||
|
func ProvideGroupHandler(
|
||||||
|
groupService service.GroupService,
|
||||||
|
userService service.UserService,
|
||||||
|
) *handler.GroupHandler {
|
||||||
|
return handler.NewGroupHandler(groupService, userService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideGorseHandler 提供 Gorse 处理器
|
||||||
|
func ProvideGorseHandler(cfg *config.Config, db *gorm.DB) *handler.GorseHandler {
|
||||||
|
return handler.NewGorseHandler(cfg.Gorse, db)
|
||||||
|
}
|
||||||
121
internal/wire/infrastructure.go
Normal file
121
internal/wire/infrastructure.go
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
package wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"carrot_bbs/internal/cache"
|
||||||
|
"carrot_bbs/internal/config"
|
||||||
|
"carrot_bbs/internal/model"
|
||||||
|
"carrot_bbs/internal/pkg/email"
|
||||||
|
"carrot_bbs/internal/pkg/gorse"
|
||||||
|
"carrot_bbs/internal/pkg/openai"
|
||||||
|
"carrot_bbs/internal/pkg/redis"
|
||||||
|
"carrot_bbs/internal/pkg/s3"
|
||||||
|
"carrot_bbs/internal/pkg/sse"
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InfrastructureSet 基础设施 Provider Set
|
||||||
|
var InfrastructureSet = wire.NewSet(
|
||||||
|
// 配置
|
||||||
|
ProvideConfig,
|
||||||
|
|
||||||
|
// 数据库
|
||||||
|
ProvideDB,
|
||||||
|
|
||||||
|
// 事务管理器
|
||||||
|
ProvideTransactionManager,
|
||||||
|
|
||||||
|
// Redis 和缓存
|
||||||
|
ProvideRedisClient,
|
||||||
|
ProvideCache,
|
||||||
|
|
||||||
|
// SSE Hub
|
||||||
|
ProvideSSEHub,
|
||||||
|
|
||||||
|
// 外部服务客户端
|
||||||
|
ProvideGorseClient,
|
||||||
|
ProvideOpenAIClient,
|
||||||
|
ProvideEmailClient,
|
||||||
|
ProvideS3Client,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProvideConfig 加载配置
|
||||||
|
func ProvideConfig() (*config.Config, error) {
|
||||||
|
return config.Load("configs/config.yaml")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideDB 提供数据库连接
|
||||||
|
func ProvideDB(cfg *config.Config) (*gorm.DB, error) {
|
||||||
|
return model.NewDB(&cfg.Database)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideRedisClient 提供 Redis 客户端
|
||||||
|
func ProvideRedisClient(cfg *config.Config) *redis.Client {
|
||||||
|
client, err := redis.New(&cfg.Redis)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("[WARNING] Failed to connect to Redis: %v, falling back to memory cache", err)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return client
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideCache 提供缓存实例
|
||||||
|
func ProvideCache(cfg *config.Config, redisClient *redis.Client) cache.Cache {
|
||||||
|
cache.Configure(cache.Settings{
|
||||||
|
Enabled: cfg.Cache.Enabled,
|
||||||
|
KeyPrefix: cfg.Cache.KeyPrefix,
|
||||||
|
DefaultTTL: time.Duration(cfg.Cache.DefaultTTL) * time.Second,
|
||||||
|
NullTTL: time.Duration(cfg.Cache.NullTTL) * time.Second,
|
||||||
|
JitterRatio: cfg.Cache.JitterRatio,
|
||||||
|
PostListTTL: time.Duration(cfg.Cache.Modules.PostList) * time.Second,
|
||||||
|
ConversationTTL: time.Duration(cfg.Cache.Modules.Conversation) * time.Second,
|
||||||
|
UnreadCountTTL: time.Duration(cfg.Cache.Modules.UnreadCount) * time.Second,
|
||||||
|
GroupMembersTTL: time.Duration(cfg.Cache.Modules.GroupMembers) * time.Second,
|
||||||
|
DisableFlushDB: cfg.Cache.DisableFlushDB,
|
||||||
|
})
|
||||||
|
|
||||||
|
// 直接创建 RedisCache 实例,不依赖全局变量
|
||||||
|
if redisClient == nil {
|
||||||
|
log.Println("[Wire] Warning: Redis client is nil, cache will not be available")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
cacheInstance := cache.NewRedisCache(redisClient)
|
||||||
|
log.Println("[Wire] Initialized Redis cache via dependency injection")
|
||||||
|
return cacheInstance
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideSSEHub 提供 SSE Hub
|
||||||
|
func ProvideSSEHub() *sse.Hub {
|
||||||
|
return sse.NewHub()
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideGorseClient 提供 Gorse 客户端
|
||||||
|
func ProvideGorseClient(cfg *config.Config) gorse.Client {
|
||||||
|
return gorse.NewClient(gorse.ConfigFromAppConfig(&cfg.Gorse))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideOpenAIClient 提供 OpenAI 客户端
|
||||||
|
func ProvideOpenAIClient(cfg *config.Config) openai.Client {
|
||||||
|
return openai.NewClient(openai.ConfigFromAppConfig(&cfg.OpenAI))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideEmailClient 提供邮件客户端
|
||||||
|
func ProvideEmailClient(cfg *config.Config) email.Client {
|
||||||
|
return email.NewClient(email.ConfigFromAppConfig(&cfg.Email))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideS3Client 提供 S3 客户端
|
||||||
|
func ProvideS3Client(cfg *config.Config) (*s3.Client, error) {
|
||||||
|
return s3.New(&cfg.S3)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideTransactionManager 提供事务管理器
|
||||||
|
func ProvideTransactionManager(db *gorm.DB) repository.TransactionManager {
|
||||||
|
return repository.NewTransactionManager(db)
|
||||||
|
}
|
||||||
11
internal/wire/provider_set.go
Normal file
11
internal/wire/provider_set.go
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package wire
|
||||||
|
|
||||||
|
import "github.com/google/wire"
|
||||||
|
|
||||||
|
// AllSet 包含所有 Provider Set
|
||||||
|
var AllSet = wire.NewSet(
|
||||||
|
InfrastructureSet,
|
||||||
|
RepositorySet,
|
||||||
|
ServiceSet,
|
||||||
|
HandlerSet,
|
||||||
|
)
|
||||||
29
internal/wire/repository.go
Normal file
29
internal/wire/repository.go
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
package wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RepositorySet Repository 层 Provider Set
|
||||||
|
var RepositorySet = wire.NewSet(
|
||||||
|
repository.NewUserRepository,
|
||||||
|
repository.NewPostRepository,
|
||||||
|
repository.NewCommentRepository,
|
||||||
|
repository.NewMessageRepository,
|
||||||
|
repository.NewNotificationRepository,
|
||||||
|
repository.NewPushRecordRepository,
|
||||||
|
repository.NewDeviceTokenRepository,
|
||||||
|
repository.NewSystemNotificationRepository,
|
||||||
|
repository.NewGroupRepository,
|
||||||
|
repository.NewStickerRepository,
|
||||||
|
repository.NewVoteRepository,
|
||||||
|
repository.NewScheduleRepository,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Note: 以下 Repository 返回接口类型而非指针,需要单独处理
|
||||||
|
// - ScheduleRepository (repository.ScheduleRepository)
|
||||||
|
// - StickerRepository (repository.StickerRepository)
|
||||||
|
// - GroupRepository (repository.GroupRepository)
|
||||||
|
// 这些已经在各自的 New* 函数中正确返回接口类型,Wire 可以自动处理
|
||||||
189
internal/wire/service.go
Normal file
189
internal/wire/service.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
package wire
|
||||||
|
|
||||||
|
import (
|
||||||
|
"carrot_bbs/internal/cache"
|
||||||
|
"carrot_bbs/internal/config"
|
||||||
|
"carrot_bbs/internal/pkg/email"
|
||||||
|
"carrot_bbs/internal/pkg/gorse"
|
||||||
|
"carrot_bbs/internal/pkg/openai"
|
||||||
|
"carrot_bbs/internal/pkg/s3"
|
||||||
|
"carrot_bbs/internal/pkg/sse"
|
||||||
|
"carrot_bbs/internal/repository"
|
||||||
|
"carrot_bbs/internal/service"
|
||||||
|
|
||||||
|
"github.com/google/wire"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ServiceSet Service 层 Provider Set
|
||||||
|
var ServiceSet = wire.NewSet(
|
||||||
|
// JWT 服务(需要配置参数)
|
||||||
|
ProvideJWTService,
|
||||||
|
|
||||||
|
// 基础服务
|
||||||
|
ProvideEmailService,
|
||||||
|
ProvidePostAIService,
|
||||||
|
|
||||||
|
// 核心服务(按依赖顺序)
|
||||||
|
ProvidePushService,
|
||||||
|
ProvideSystemMessageService,
|
||||||
|
ProvidePostService,
|
||||||
|
ProvideCommentService,
|
||||||
|
ProvideMessageService,
|
||||||
|
ProvideNotificationService,
|
||||||
|
ProvideUserService,
|
||||||
|
ProvideStickerService,
|
||||||
|
ProvideVoteService,
|
||||||
|
ProvideChatService,
|
||||||
|
ProvideScheduleService,
|
||||||
|
ProvideGroupService,
|
||||||
|
ProvideUploadService,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ProvideJWTService 提供 JWT 服务
|
||||||
|
func ProvideJWTService(cfg *config.Config) *service.JWTService {
|
||||||
|
return service.NewJWTService(
|
||||||
|
cfg.JWT.Secret,
|
||||||
|
int64(cfg.JWT.AccessTokenExpire.Seconds()),
|
||||||
|
int64(cfg.JWT.RefreshTokenExpire.Seconds()),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideEmailService 提供邮件服务
|
||||||
|
func ProvideEmailService(client email.Client) service.EmailService {
|
||||||
|
return service.NewEmailService(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvidePostAIService 提供 AI 服务
|
||||||
|
func ProvidePostAIService(client openai.Client) *service.PostAIService {
|
||||||
|
return service.NewPostAIService(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvidePushService 提供推送服务
|
||||||
|
func ProvidePushService(
|
||||||
|
pushRepo *repository.PushRecordRepository,
|
||||||
|
deviceTokenRepo *repository.DeviceTokenRepository,
|
||||||
|
messageRepo *repository.MessageRepository,
|
||||||
|
sseHub *sse.Hub,
|
||||||
|
) service.PushService {
|
||||||
|
return service.NewPushService(pushRepo, deviceTokenRepo, messageRepo, sseHub)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideSystemMessageService 提供系统消息服务
|
||||||
|
func ProvideSystemMessageService(
|
||||||
|
notifyRepo *repository.SystemNotificationRepository,
|
||||||
|
pushService service.PushService,
|
||||||
|
userRepo *repository.UserRepository,
|
||||||
|
postRepo *repository.PostRepository,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) service.SystemMessageService {
|
||||||
|
return service.NewSystemMessageService(notifyRepo, pushService, userRepo, postRepo, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvidePostService 提供帖子服务
|
||||||
|
func ProvidePostService(
|
||||||
|
postRepo *repository.PostRepository,
|
||||||
|
systemMessageService service.SystemMessageService,
|
||||||
|
gorseClient gorse.Client,
|
||||||
|
postAIService *service.PostAIService,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
txManager repository.TransactionManager,
|
||||||
|
) service.PostService {
|
||||||
|
return service.NewPostService(postRepo, systemMessageService, gorseClient, postAIService, cacheBackend, txManager)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideCommentService 提供评论服务
|
||||||
|
func ProvideCommentService(
|
||||||
|
commentRepo *repository.CommentRepository,
|
||||||
|
postRepo *repository.PostRepository,
|
||||||
|
systemMessageService service.SystemMessageService,
|
||||||
|
gorseClient gorse.Client,
|
||||||
|
postAIService *service.PostAIService,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) *service.CommentService {
|
||||||
|
return service.NewCommentService(commentRepo, postRepo, systemMessageService, gorseClient, postAIService, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideMessageService 提供消息服务
|
||||||
|
func ProvideMessageService(
|
||||||
|
db *gorm.DB,
|
||||||
|
messageRepo *repository.MessageRepository,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) *service.MessageService {
|
||||||
|
return service.NewMessageService(db, messageRepo, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideNotificationService 提供通知服务
|
||||||
|
func ProvideNotificationService(
|
||||||
|
notificationRepo *repository.NotificationRepository,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) *service.NotificationService {
|
||||||
|
return service.NewNotificationService(notificationRepo, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideUserService 提供用户服务
|
||||||
|
func ProvideUserService(
|
||||||
|
userRepo *repository.UserRepository,
|
||||||
|
systemMessageService service.SystemMessageService,
|
||||||
|
emailService service.EmailService,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) service.UserService {
|
||||||
|
return service.NewUserService(userRepo, systemMessageService, emailService, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideStickerService 提供表情服务
|
||||||
|
func ProvideStickerService(
|
||||||
|
stickerRepo repository.StickerRepository,
|
||||||
|
) service.StickerService {
|
||||||
|
return service.NewStickerService(stickerRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideVoteService 提供投票服务
|
||||||
|
func ProvideVoteService(
|
||||||
|
voteRepo *repository.VoteRepository,
|
||||||
|
postRepo *repository.PostRepository,
|
||||||
|
cache cache.Cache,
|
||||||
|
postAIService *service.PostAIService,
|
||||||
|
systemMessageService service.SystemMessageService,
|
||||||
|
) *service.VoteService {
|
||||||
|
return service.NewVoteService(voteRepo, postRepo, cache, postAIService, systemMessageService)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideChatService 提供聊天服务
|
||||||
|
// Note: sensitiveService 传 nil,与 main.go 保持一致
|
||||||
|
func ProvideChatService(
|
||||||
|
db *gorm.DB,
|
||||||
|
messageRepo *repository.MessageRepository,
|
||||||
|
userRepo *repository.UserRepository,
|
||||||
|
sseHub *sse.Hub,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) service.ChatService {
|
||||||
|
return service.NewChatService(db, messageRepo, userRepo, nil, sseHub, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideScheduleService 提供日程服务
|
||||||
|
func ProvideScheduleService(
|
||||||
|
scheduleRepo repository.ScheduleRepository,
|
||||||
|
) service.ScheduleService {
|
||||||
|
return service.NewScheduleService(scheduleRepo)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideGroupService 提供群组服务
|
||||||
|
func ProvideGroupService(
|
||||||
|
db *gorm.DB,
|
||||||
|
groupRepo repository.GroupRepository,
|
||||||
|
userRepo *repository.UserRepository,
|
||||||
|
messageRepo *repository.MessageRepository,
|
||||||
|
sseHub *sse.Hub,
|
||||||
|
cacheBackend cache.Cache,
|
||||||
|
) service.GroupService {
|
||||||
|
return service.NewGroupService(db, groupRepo, userRepo, messageRepo, sseHub, cacheBackend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProvideUploadService 提供上传服务
|
||||||
|
func ProvideUploadService(
|
||||||
|
s3Client *s3.Client,
|
||||||
|
userService service.UserService,
|
||||||
|
) *service.UploadService {
|
||||||
|
return service.NewUploadService(s3Client, userService)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user