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/golang-jwt/jwt/v5 v5.2.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/minio/minio-go/v7 v7.0.66
|
||||
github.com/redis/go-redis/v9 v9.3.1
|
||||
github.com/spf13/viper v1.18.2
|
||||
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
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
gorm.io/driver/postgres v1.5.4
|
||||
gorm.io/driver/sqlite v1.5.4
|
||||
gorm.io/gorm v1.25.5
|
||||
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -75,8 +76,8 @@ require (
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/arch v0.3.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
|
||||
golang.org/x/net v0.19.0 // indirect
|
||||
golang.org/x/sys v0.15.0 // indirect
|
||||
golang.org/x/net v0.28.0 // indirect
|
||||
golang.org/x/sys v0.23.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // 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/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.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
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/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
|
||||
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/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
|
||||
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/go.mod h1:ZxmVHzZPKm5pmEIlqaRDwK0rkfTRHlfziO033XZ+RW0=
|
||||
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.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
|
||||
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.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
|
||||
golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
|
||||
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/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
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.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U=
|
||||
golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
|
||||
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-20220715151400-c0bba94af5f8/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.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
|
||||
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
|
||||
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/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
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 (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
|
||||
redisPkg "carrot_bbs/internal/pkg/redis"
|
||||
)
|
||||
|
||||
// Cache 缓存接口
|
||||
@@ -71,33 +60,10 @@ type Cache interface {
|
||||
Expire(ctx context.Context, key string, ttl time.Duration) error
|
||||
}
|
||||
|
||||
// cacheItem 缓存项(用于内存缓存降级)
|
||||
type cacheItem struct {
|
||||
value interface{}
|
||||
expiration int64 // 过期时间戳(纳秒)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
type MetricsSnapshot struct {
|
||||
Hit int64
|
||||
Miss int64
|
||||
DecodeError int64
|
||||
SetError int64
|
||||
Invalidate int64
|
||||
}
|
||||
|
||||
type Settings struct {
|
||||
Enabled bool
|
||||
KeyPrefix string
|
||||
@@ -170,814 +136,6 @@ func normalizePrefix(prefix string) string {
|
||||
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) {
|
||||
if !settings.Enabled {
|
||||
return
|
||||
@@ -1014,16 +172,16 @@ func GetTyped[T any](c Cache, key string) (T, bool) {
|
||||
}
|
||||
raw, ok := c.Get(key)
|
||||
if !ok {
|
||||
metrics.miss.Add(1)
|
||||
recordMiss()
|
||||
return zero, false
|
||||
}
|
||||
if str, ok := raw.(string); ok && str == nullMarkerValue {
|
||||
metrics.hit.Add(1)
|
||||
recordHit()
|
||||
return zero, false
|
||||
}
|
||||
|
||||
if typed, ok := raw.(T); ok {
|
||||
metrics.hit.Add(1)
|
||||
recordHit()
|
||||
return typed, true
|
||||
}
|
||||
|
||||
@@ -1031,29 +189,29 @@ func GetTyped[T any](c Cache, key string) (T, bool) {
|
||||
switch v := raw.(type) {
|
||||
case string:
|
||||
if err := json.Unmarshal([]byte(v), &out); err != nil {
|
||||
metrics.decodeError.Add(1)
|
||||
recordDecodeError()
|
||||
return zero, false
|
||||
}
|
||||
metrics.hit.Add(1)
|
||||
recordHit()
|
||||
return out, true
|
||||
case []byte:
|
||||
if err := json.Unmarshal(v, &out); err != nil {
|
||||
metrics.decodeError.Add(1)
|
||||
recordDecodeError()
|
||||
return zero, false
|
||||
}
|
||||
metrics.hit.Add(1)
|
||||
recordHit()
|
||||
return out, true
|
||||
default:
|
||||
data, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
metrics.decodeError.Add(1)
|
||||
recordDecodeError()
|
||||
return zero, false
|
||||
}
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
metrics.decodeError.Add(1)
|
||||
recordDecodeError()
|
||||
return zero, false
|
||||
}
|
||||
metrics.hit.Add(1)
|
||||
recordHit()
|
||||
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
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"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"
|
||||
)
|
||||
|
||||
// Config 主配置结构体
|
||||
type Config struct {
|
||||
Server ServerConfig `mapstructure:"server"`
|
||||
Database DatabaseConfig `mapstructure:"database"`
|
||||
@@ -30,217 +27,7 @@ type Config struct {
|
||||
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
|
||||
}
|
||||
|
||||
type ServerConfig struct {
|
||||
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
|
||||
}
|
||||
|
||||
// Load 加载配置文件
|
||||
func Load(configPath string) (*Config, error) {
|
||||
viper.SetConfigFile(configPath)
|
||||
viper.SetConfigType("yaml")
|
||||
@@ -426,49 +213,3 @@ func getEnvOrDefault(key, defaultValue string) string {
|
||||
}
|
||||
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
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// ==================== 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
|
||||
}
|
||||
|
||||
// ==================== 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
|
||||
}
|
||||
// converter.go - 公共转换函数
|
||||
//
|
||||
// 转换函数已按领域拆分到以下文件:
|
||||
// - user_converter.go: 用户相关转换
|
||||
// - post_converter.go: 帖子和评论相关转换
|
||||
// - message_converter.go: 消息、会话、推送和设备令牌相关转换
|
||||
// - group_converter.go: 群组相关转换
|
||||
// - notification_converter.go: 通知和系统消息相关转换
|
||||
// - schedule_converter.go: 日程相关转换(已存在)
|
||||
//
|
||||
// 所有转换函数都在 dto 包中,可以直接通过 dto.ConvertXxxToResponse 调用。
|
||||
|
||||
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/response"
|
||||
|
||||
gorseio "github.com/gorse-io/gorse-go"
|
||||
"github.com/gin-gonic/gin"
|
||||
gorseio "github.com/gorse-io/gorse-go"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// GorseHandler Gorse推荐处理器
|
||||
type GorseHandler struct {
|
||||
importPassword string
|
||||
gorseConfig config.GorseConfig
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
// NewGorseHandler 创建Gorse处理器
|
||||
func NewGorseHandler(cfg config.GorseConfig) *GorseHandler {
|
||||
func NewGorseHandler(cfg config.GorseConfig, db *gorm.DB) *GorseHandler {
|
||||
return &GorseHandler{
|
||||
importPassword: cfg.ImportPassword,
|
||||
gorseConfig: cfg,
|
||||
db: db,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +101,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
||||
|
||||
// 导入帖子
|
||||
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
|
||||
}
|
||||
for _, post := range posts {
|
||||
@@ -126,7 +129,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
||||
|
||||
// 导入用户
|
||||
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
|
||||
}
|
||||
for _, user := range users {
|
||||
@@ -148,7 +151,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
||||
|
||||
// 导入点赞
|
||||
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
|
||||
}
|
||||
for _, like := range likes {
|
||||
@@ -167,7 +170,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
||||
|
||||
// 导入收藏
|
||||
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
|
||||
}
|
||||
for _, fav := range favorites {
|
||||
@@ -186,7 +189,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
|
||||
|
||||
// 导入评论(按用户-帖子去重)
|
||||
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
|
||||
}
|
||||
seen := make(map[string]struct{})
|
||||
|
||||
@@ -15,11 +15,11 @@ import (
|
||||
// GroupHandler 群组处理器
|
||||
type GroupHandler struct {
|
||||
groupService service.GroupService
|
||||
userService *service.UserService
|
||||
userService service.UserService
|
||||
}
|
||||
|
||||
// NewGroupHandler 创建群组处理器
|
||||
func NewGroupHandler(groupService service.GroupService, userService *service.UserService) *GroupHandler {
|
||||
func NewGroupHandler(groupService service.GroupService, userService service.UserService) *GroupHandler {
|
||||
return &GroupHandler{
|
||||
groupService: groupService,
|
||||
userService: userService,
|
||||
|
||||
@@ -20,13 +20,13 @@ import (
|
||||
type MessageHandler struct {
|
||||
chatService service.ChatService
|
||||
messageService *service.MessageService
|
||||
userService *service.UserService
|
||||
userService service.UserService
|
||||
groupService service.GroupService
|
||||
sseHub *sse.Hub
|
||||
}
|
||||
|
||||
// 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{
|
||||
chatService: chatService,
|
||||
messageService: messageService,
|
||||
|
||||
@@ -15,12 +15,12 @@ import (
|
||||
|
||||
// PostHandler 帖子处理器
|
||||
type PostHandler struct {
|
||||
postService *service.PostService
|
||||
userService *service.UserService
|
||||
postService service.PostService
|
||||
userService service.UserService
|
||||
}
|
||||
|
||||
// NewPostHandler 创建帖子处理器
|
||||
func NewPostHandler(postService *service.PostService, userService *service.UserService) *PostHandler {
|
||||
func NewPostHandler(postService service.PostService, userService service.UserService) *PostHandler {
|
||||
return &PostHandler{
|
||||
postService: postService,
|
||||
userService: userService,
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strconv"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"carrot_bbs/internal/dto"
|
||||
@@ -16,16 +17,19 @@ import (
|
||||
type SystemMessageHandler struct {
|
||||
systemMsgService service.SystemMessageService
|
||||
notifyRepo *repository.SystemNotificationRepository
|
||||
cache cache.Cache
|
||||
}
|
||||
|
||||
// NewSystemMessageHandler 创建系统消息处理器
|
||||
func NewSystemMessageHandler(
|
||||
systemMsgService service.SystemMessageService,
|
||||
notifyRepo *repository.SystemNotificationRepository,
|
||||
cacheBackend cache.Cache,
|
||||
) *SystemMessageHandler {
|
||||
return &SystemMessageHandler{
|
||||
systemMsgService: systemMsgService,
|
||||
notifyRepo: notifyRepo,
|
||||
cache: cacheBackend,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +105,7 @@ func (h *SystemMessageHandler) MarkAsRead(c *gin.Context) {
|
||||
response.InternalServerError(c, "failed to mark as read")
|
||||
return
|
||||
}
|
||||
cache.InvalidateUnreadSystem(cache.GetCache(), userID)
|
||||
cache.InvalidateUnreadSystem(h.cache, userID)
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
cache.InvalidateUnreadSystem(cache.GetCache(), userID)
|
||||
cache.InvalidateUnreadSystem(h.cache, userID)
|
||||
|
||||
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")
|
||||
return
|
||||
}
|
||||
cache.InvalidateUnreadSystem(cache.GetCache(), userID)
|
||||
cache.InvalidateUnreadSystem(h.cache, userID)
|
||||
|
||||
response.SuccessWithMessage(c, "notification deleted", nil)
|
||||
}
|
||||
|
||||
@@ -12,12 +12,12 @@ import (
|
||||
|
||||
// UserHandler 用户处理器
|
||||
type UserHandler struct {
|
||||
userService *service.UserService
|
||||
userService service.UserService
|
||||
jwtService *service.JWTService
|
||||
}
|
||||
|
||||
// NewUserHandler 创建用户处理器
|
||||
func NewUserHandler(userService *service.UserService) *UserHandler {
|
||||
func NewUserHandler(userService service.UserService) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
}
|
||||
|
||||
@@ -14,11 +14,11 @@ import (
|
||||
// VoteHandler 投票处理器
|
||||
type VoteHandler struct {
|
||||
voteService *service.VoteService
|
||||
postService *service.PostService
|
||||
postService service.PostService
|
||||
}
|
||||
|
||||
// NewVoteHandler 创建投票处理器
|
||||
func NewVoteHandler(voteService *service.VoteService, postService *service.PostService) *VoteHandler {
|
||||
func NewVoteHandler(voteService *service.VoteService, postService service.PostService) *VoteHandler {
|
||||
return &VoteHandler{
|
||||
voteService: voteService,
|
||||
postService: postService,
|
||||
|
||||
@@ -14,11 +14,8 @@ import (
|
||||
"carrot_bbs/internal/config"
|
||||
)
|
||||
|
||||
// DB 全局数据库连接
|
||||
var DB *gorm.DB
|
||||
|
||||
// InitDB 初始化数据库连接
|
||||
func InitDB(cfg *config.DatabaseConfig) error {
|
||||
// NewDB 创建数据库连接(用于 Wire 依赖注入)
|
||||
func NewDB(cfg *config.DatabaseConfig) (*gorm.DB, error) {
|
||||
var err error
|
||||
var db *gorm.DB
|
||||
gormLogger := logger.New(
|
||||
@@ -52,28 +49,26 @@ func InitDB(cfg *config.DatabaseConfig) error {
|
||||
}
|
||||
|
||||
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不支持连接池配置,跳过)
|
||||
if cfg.Type != "sqlite" {
|
||||
sqlDB, err := DB.DB()
|
||||
sqlDB, err := db.DB()
|
||||
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.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||
}
|
||||
|
||||
// 自动迁移
|
||||
if err := autoMigrate(DB); err != nil {
|
||||
return fmt.Errorf("failed to auto migrate: %w", err)
|
||||
if err := autoMigrate(db); err != nil {
|
||||
return nil, fmt.Errorf("failed to auto migrate: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("Database connected (%s) and migrated successfully", cfg.Type)
|
||||
return nil
|
||||
return db, nil
|
||||
}
|
||||
|
||||
func parseGormLogLevel(level string) logger.LogLevel {
|
||||
@@ -155,8 +150,8 @@ func autoMigrate(db *gorm.DB) error {
|
||||
}
|
||||
|
||||
// CloseDB 关闭数据库连接
|
||||
func CloseDB() {
|
||||
if sqlDB, err := DB.DB(); err == nil {
|
||||
func CloseDB(db *gorm.DB) {
|
||||
if sqlDB, err := db.DB(); err == nil {
|
||||
sqlDB.Close()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"carrot_bbs/internal/service"
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
)
|
||||
|
||||
// Response 统一响应结构
|
||||
@@ -119,44 +119,72 @@ func Paginated(c *gin.Context, list interface{}, total int64, page, pageSize int
|
||||
}
|
||||
|
||||
// HandleServiceError 统一处理 Service 错误
|
||||
// 如果 err 是 *service.ServiceError,返回对应的业务错误码和消息
|
||||
// 如果 err 是 *apperrors.AppError,返回对应的业务错误码和消息
|
||||
// 如果 err 是其他错误,返回 false(调用方应处理通用错误)
|
||||
// 返回 true 表示错误已处理,false 表示需要调用方继续处理
|
||||
func HandleServiceError(c *gin.Context, err error) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
if se, ok := err.(*service.ServiceError); ok {
|
||||
ErrorWithStatusCode(c, statusCodeFromCode(se.Code), se.Code, se.Message)
|
||||
if se, ok := apperrors.As(err); ok {
|
||||
statusCode := statusCodeFromAppErrorCode(se.Code)
|
||||
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// HandleError 统一处理错误(带默认消息)
|
||||
// 如果 err 是 *service.ServiceError,返回对应的业务错误码和消息
|
||||
// 如果 err 是 *apperrors.AppError,返回对应的业务错误码和消息
|
||||
// 如果 err 是其他错误,返回 InternalServerError 并使用 defaultMessage
|
||||
func HandleError(c *gin.Context, err error, defaultMessage string) {
|
||||
if err == nil {
|
||||
return
|
||||
}
|
||||
if se, ok := err.(*service.ServiceError); ok {
|
||||
ErrorWithStatusCode(c, statusCodeFromCode(se.Code), se.Code, se.Message)
|
||||
if se, ok := apperrors.As(err); ok {
|
||||
statusCode := statusCodeFromAppErrorCode(se.Code)
|
||||
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
|
||||
return
|
||||
}
|
||||
InternalServerError(c, defaultMessage)
|
||||
}
|
||||
|
||||
// statusCodeFromCode 根据业务错误码获取 HTTP 状态码
|
||||
func statusCodeFromCode(code int) int {
|
||||
switch {
|
||||
case code >= 200 && code < 300:
|
||||
return http.StatusOK
|
||||
case code >= 400 && code < 500:
|
||||
return code
|
||||
case code >= 500:
|
||||
return code
|
||||
// statusCodeFromAppErrorCode 根据AppError的错误码字符串获取 HTTP 状态码
|
||||
func statusCodeFromAppErrorCode(code string) int {
|
||||
switch code {
|
||||
case "NOT_FOUND", "USER_NOT_FOUND", "POST_NOT_FOUND", "GROUP_NOT_FOUND",
|
||||
"GROUP_REQUEST_NOT_FOUND", "SCHEDULE_COURSE_NOT_FOUND":
|
||||
return http.StatusNotFound
|
||||
case "UNAUTHORIZED", "INVALID_CREDENTIALS":
|
||||
return http.StatusUnauthorized
|
||||
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:
|
||||
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 (
|
||||
"carrot_bbs/internal/model"
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -17,6 +18,14 @@ func NewPostRepository(db *gorm.DB) *PostRepository {
|
||||
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 创建帖子
|
||||
func (r *PostRepository) Create(post *model.Post, images []string) 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
|
||||
}
|
||||
|
||||
// ========== 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,
|
||||
sensitive SensitiveService,
|
||||
sseHub *sse.Hub,
|
||||
cacheBackend cache.Cache,
|
||||
) ChatService {
|
||||
// 创建适配器
|
||||
convRepoAdapter := cache.NewConversationRepositoryAdapter(repo)
|
||||
@@ -79,7 +80,7 @@ func NewChatService(
|
||||
|
||||
// 创建会话缓存
|
||||
conversationCache := cache.NewConversationCache(
|
||||
cache.GetCache(),
|
||||
cacheBackend,
|
||||
convRepoAdapter,
|
||||
msgRepoAdapter,
|
||||
cache.DefaultConversationCacheSettings(),
|
||||
|
||||
@@ -24,12 +24,12 @@ type CommentService struct {
|
||||
}
|
||||
|
||||
// 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{
|
||||
commentRepo: commentRepo,
|
||||
postRepo: postRepo,
|
||||
systemMessageService: systemMessageService,
|
||||
cache: cache.GetCache(),
|
||||
cache: cacheBackend,
|
||||
gorseClient: gorseClient,
|
||||
postAIService: postAIService,
|
||||
}
|
||||
|
||||
@@ -43,9 +43,6 @@ type emailCodeServiceImpl struct {
|
||||
}
|
||||
|
||||
func NewEmailCodeService(emailService EmailService, cacheBackend cache.Cache) EmailCodeService {
|
||||
if cacheBackend == nil {
|
||||
cacheBackend = cache.GetCache()
|
||||
}
|
||||
return &emailCodeServiceImpl{
|
||||
emailService: emailService,
|
||||
cache: cacheBackend,
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/sse"
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
@@ -23,25 +24,26 @@ const (
|
||||
GroupCacheJitter = 0.1
|
||||
)
|
||||
|
||||
// 群组服务错误定义
|
||||
// 群组服务错误定义 - 使用统一的 AppError
|
||||
var (
|
||||
ErrGroupNotFound = &ServiceError{Code: 404, Message: "群组不存在"}
|
||||
ErrNotGroupMember = &ServiceError{Code: 403, Message: "不是群成员"}
|
||||
ErrNotGroupAdmin = &ServiceError{Code: 403, Message: "不是群管理员"}
|
||||
ErrNotGroupOwner = &ServiceError{Code: 403, Message: "不是群主"}
|
||||
ErrGroupFull = &ServiceError{Code: 400, Message: "群已满"}
|
||||
ErrAlreadyMember = &ServiceError{Code: 400, Message: "已经是群成员"}
|
||||
ErrCannotRemoveOwner = &ServiceError{Code: 400, Message: "不能移除群主"}
|
||||
ErrCannotMuteOwner = &ServiceError{Code: 400, Message: "不能禁言群主"}
|
||||
ErrMuted = &ServiceError{Code: 403, Message: "你已被禁言"}
|
||||
ErrMuteAllEnabled = &ServiceError{Code: 403, Message: "全员禁言中"}
|
||||
ErrCannotJoin = &ServiceError{Code: 400, Message: "该群不允许加入"}
|
||||
ErrJoinRequestPending = &ServiceError{Code: 400, Message: "加群申请已提交"}
|
||||
ErrGroupRequestNotFound = &ServiceError{Code: 404, Message: "加群请求不存在"}
|
||||
ErrGroupRequestHandled = &ServiceError{Code: 400, Message: "该加群请求已处理"}
|
||||
ErrNotRequestTarget = &ServiceError{Code: 400, Message: "不是邀请目标用户"}
|
||||
ErrNoEligibleInvitee = &ServiceError{Code: 400, Message: "没有可邀请的用户"}
|
||||
ErrNotMutualFollow = &ServiceError{Code: 400, Message: "仅支持邀请互相关注用户"}
|
||||
ErrGroupNotFound = apperrors.ErrGroupNotFound
|
||||
ErrNotGroupMember = apperrors.ErrNotGroupMember
|
||||
ErrNotGroupAdmin = apperrors.ErrNotGroupAdmin
|
||||
ErrNotGroupOwner = apperrors.ErrNotGroupOwner
|
||||
ErrGroupFull = apperrors.ErrGroupFull
|
||||
ErrAlreadyMember = apperrors.ErrAlreadyGroupMember
|
||||
ErrCannotRemoveOwner = apperrors.ErrCannotRemoveOwner
|
||||
ErrCannotMuteOwner = apperrors.ErrCannotMuteOwner
|
||||
ErrMuted = apperrors.ErrMuted
|
||||
ErrMuteAllEnabled = apperrors.ErrMuteAllEnabled
|
||||
ErrCannotJoin = apperrors.ErrCannotJoin
|
||||
ErrJoinRequestPending = apperrors.ErrJoinRequestPending
|
||||
ErrGroupRequestNotFound = apperrors.ErrGroupRequestNotFound
|
||||
ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled
|
||||
ErrNotRequestTarget = apperrors.ErrNotRequestTarget
|
||||
ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee
|
||||
ErrNotMutualFollow = apperrors.ErrNotMutualFollow
|
||||
ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted
|
||||
)
|
||||
|
||||
// GroupService 群组服务接口
|
||||
@@ -104,7 +106,7 @@ type groupService struct {
|
||||
}
|
||||
|
||||
// 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{
|
||||
db: db,
|
||||
groupRepo: groupRepo,
|
||||
@@ -113,7 +115,7 @@ func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo
|
||||
requestRepo: repository.NewGroupJoinRequestRepository(db),
|
||||
notifyRepo: repository.NewSystemNotificationRepository(db),
|
||||
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.ReviewerID == "" {
|
||||
// 被邀请人还未同意,管理员不能提前审批
|
||||
return &ServiceError{Code: 400, Message: "被邀请人尚未同意邀请,无法审批"}
|
||||
return ErrInviteNotAccepted
|
||||
}
|
||||
if req.ReviewerID != req.TargetUserID {
|
||||
// ReviewerID 不是被邀请人,说明已经被其他人处理过
|
||||
|
||||
@@ -37,14 +37,14 @@ type MessageService struct {
|
||||
}
|
||||
|
||||
// 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)
|
||||
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
|
||||
|
||||
// 创建会话缓存
|
||||
conversationCache := cache.NewConversationCache(
|
||||
cache.GetCache(),
|
||||
cacheBackend,
|
||||
convRepoAdapter,
|
||||
msgRepoAdapter,
|
||||
cache.DefaultConversationCacheSettings(),
|
||||
@@ -54,7 +54,7 @@ func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *
|
||||
db: db,
|
||||
messageRepo: messageRepo,
|
||||
conversationCache: conversationCache,
|
||||
baseCache: cache.GetCache(),
|
||||
baseCache: cacheBackend,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"time"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
)
|
||||
@@ -23,10 +24,10 @@ type NotificationService struct {
|
||||
}
|
||||
|
||||
// NewNotificationService 创建通知服务
|
||||
func NewNotificationService(notificationRepo *repository.NotificationRepository) *NotificationService {
|
||||
func NewNotificationService(notificationRepo *repository.NotificationRepository, cacheBackend cache.Cache) *NotificationService {
|
||||
return &NotificationService{
|
||||
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"
|
||||
)
|
||||
|
||||
// PostService 帖子服务
|
||||
type PostService struct {
|
||||
// PostService 帖子服务接口
|
||||
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
|
||||
systemMessageService SystemMessageService
|
||||
cache cache.Cache
|
||||
gorseClient gorse.Client
|
||||
postAIService *PostAIService
|
||||
txManager repository.TransactionManager // 事务管理器
|
||||
}
|
||||
|
||||
// NewPostService 创建帖子服务
|
||||
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *PostService {
|
||||
return &PostService{
|
||||
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager) PostService {
|
||||
return &postServiceImpl{
|
||||
postRepo: postRepo,
|
||||
systemMessageService: systemMessageService,
|
||||
cache: cache.GetCache(),
|
||||
cache: cacheBackend,
|
||||
gorseClient: gorseClient,
|
||||
postAIService: postAIService,
|
||||
txManager: txManager,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,7 +90,7 @@ type PostListResult struct {
|
||||
}
|
||||
|
||||
// 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{
|
||||
UserID: userID,
|
||||
Title: title,
|
||||
@@ -73,7 +113,7 @@ func (s *PostService) Create(ctx context.Context, userID, title, content string,
|
||||
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() {
|
||||
if r := recover(); r != nil {
|
||||
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 retryDelay = 200 * time.Millisecond
|
||||
|
||||
@@ -159,12 +199,12 @@ func (s *PostService) updateModerationStatusWithRetry(postID string, status mode
|
||||
return lastErr
|
||||
}
|
||||
|
||||
func (s *PostService) invalidatePostCaches(postID string) {
|
||||
func (s *postServiceImpl) invalidatePostCaches(postID string) {
|
||||
cache.InvalidatePostDetail(s.cache, postID)
|
||||
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) == "" {
|
||||
return
|
||||
}
|
||||
@@ -187,17 +227,17 @@ func (s *PostService) notifyModerationRejected(userID, reason string) {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -211,7 +251,7 @@ func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, im
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -234,7 +274,7 @@ func (s *PostService) Delete(ctx context.Context, id string) error {
|
||||
}
|
||||
|
||||
// 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()
|
||||
postListTTL := cacheSettings.PostListTTL
|
||||
if postListTTL <= 0 {
|
||||
@@ -299,22 +339,22 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
@@ -352,7 +392,7 @@ func (s *PostService) Like(ctx context.Context, postID, userID string) error {
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -374,12 +414,12 @@ func (s *PostService) Unlike(ctx context.Context, postID, userID string) error {
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
@@ -417,7 +457,7 @@ func (s *PostService) Favorite(ctx context.Context, postID, userID string) error
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -439,12 +479,12 @@ func (s *PostService) Unfavorite(ctx context.Context, postID, userID string) err
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
isFavoritedMap := make(map[string]bool)
|
||||
|
||||
@@ -461,7 +501,7 @@ func (s *PostService) GetPostInteractionStatus(ctx context.Context, postIDs []st
|
||||
}
|
||||
|
||||
// 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 == "" {
|
||||
return false, false
|
||||
}
|
||||
@@ -469,7 +509,7 @@ func (s *PostService) GetPostInteractionStatusSingle(ctx context.Context, postID
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return err
|
||||
}
|
||||
@@ -494,17 +534,17 @@ func (s *PostService) IncrementViews(ctx context.Context, postID, userID string)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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()
|
||||
postListTTL := cacheSettings.PostListTTL
|
||||
if postListTTL <= 0 {
|
||||
@@ -546,7 +586,7 @@ func (s *PostService) GetFollowingPosts(ctx context.Context, userID string, page
|
||||
}
|
||||
|
||||
// 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启用,使用自定义的非个性化推荐器
|
||||
if s.gorseClient.IsEnabled() {
|
||||
offset := (page - 1) * pageSize
|
||||
@@ -580,7 +620,7 @@ func (s *PostService) GetHotPosts(ctx context.Context, page, pageSize int) ([]*m
|
||||
}
|
||||
|
||||
// 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失败降级时使用)
|
||||
posts, total, err := s.postRepo.GetHotPosts(page, pageSize)
|
||||
if err != nil {
|
||||
@@ -590,7 +630,7 @@ func (s *PostService) getHotPostsFromDB(ctx context.Context, page, pageSize int)
|
||||
}
|
||||
|
||||
// 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未启用或用户未登录,降级为热门帖子
|
||||
if !s.gorseClient.IsEnabled() || userID == "" {
|
||||
return s.GetHotPosts(ctx, page, pageSize)
|
||||
@@ -638,7 +678,7 @@ func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, pa
|
||||
}
|
||||
|
||||
// buildPostCategories 构建帖子的类别标签
|
||||
func (s *PostService) buildPostCategories(post *model.Post) []string {
|
||||
func (s *postServiceImpl) buildPostCategories(post *model.Post) []string {
|
||||
var categories []string
|
||||
|
||||
// 热度标签
|
||||
@@ -676,3 +716,17 @@ func (s *PostService) buildPostCategories(post *model.Post) []string {
|
||||
|
||||
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"
|
||||
|
||||
"carrot_bbs/internal/dto"
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
|
||||
@@ -15,10 +16,10 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInvalidSchedulePayload = &ServiceError{Code: 400, Message: "invalid schedule payload"}
|
||||
ErrScheduleCourseNotFound = &ServiceError{Code: 404, Message: "schedule course not found"}
|
||||
ErrScheduleForbidden = &ServiceError{Code: 403, Message: "forbidden schedule operation"}
|
||||
ErrScheduleColorDuplicated = &ServiceError{Code: 400, Message: "course color already used"}
|
||||
ErrInvalidSchedulePayload = apperrors.ErrInvalidSchedulePayload
|
||||
ErrScheduleCourseNotFound = apperrors.ErrScheduleCourseNotFound
|
||||
ErrScheduleForbidden = apperrors.ErrScheduleForbidden
|
||||
ErrScheduleColorDuplicated = apperrors.ErrScheduleColorDuplicated
|
||||
)
|
||||
|
||||
var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`)
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
"errors"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/repository"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrStickerAlreadyExists = &ServiceError{Code: 400, Message: "sticker already exists"}
|
||||
ErrInvalidStickerURL = &ServiceError{Code: 400, Message: "invalid sticker url"}
|
||||
ErrStickerAlreadyExists = apperrors.ErrStickerAlreadyExists
|
||||
ErrInvalidStickerURL = apperrors.ErrInvalidStickerURL
|
||||
)
|
||||
|
||||
// StickerService 自定义表情服务接口
|
||||
|
||||
@@ -42,13 +42,14 @@ func NewSystemMessageService(
|
||||
pushService PushService,
|
||||
userRepo *repository.UserRepository,
|
||||
postRepo *repository.PostRepository,
|
||||
cacheBackend cache.Cache,
|
||||
) SystemMessageService {
|
||||
return &systemMessageServiceImpl{
|
||||
notifyRepo: notifyRepo,
|
||||
pushService: pushService,
|
||||
userRepo: userRepo,
|
||||
postRepo: postRepo,
|
||||
cache: cache.GetCache(),
|
||||
cache: cacheBackend,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/pkg/s3"
|
||||
|
||||
_ "golang.org/x/image/bmp"
|
||||
_ "golang.org/x/image/tiff"
|
||||
)
|
||||
@@ -23,11 +24,11 @@ import (
|
||||
// UploadService 上传服务
|
||||
type UploadService struct {
|
||||
s3Client *s3.Client
|
||||
userService *UserService
|
||||
userService UserService
|
||||
}
|
||||
|
||||
// NewUploadService 创建上传服务
|
||||
func NewUploadService(s3Client *s3.Client, userService *UserService) *UploadService {
|
||||
func NewUploadService(s3Client *s3.Client, userService UserService) *UploadService {
|
||||
return &UploadService{
|
||||
s3Client: s3Client,
|
||||
userService: userService,
|
||||
|
||||
@@ -6,13 +6,59 @@ import (
|
||||
"strings"
|
||||
|
||||
"carrot_bbs/internal/cache"
|
||||
apperrors "carrot_bbs/internal/errors"
|
||||
"carrot_bbs/internal/model"
|
||||
"carrot_bbs/internal/pkg/utils"
|
||||
"carrot_bbs/internal/repository"
|
||||
)
|
||||
|
||||
// UserService 用户服务
|
||||
type UserService struct {
|
||||
// UserService 用户服务接口
|
||||
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
|
||||
systemMessageService SystemMessageService
|
||||
emailCodeService EmailCodeService
|
||||
@@ -24,8 +70,8 @@ func NewUserService(
|
||||
systemMessageService SystemMessageService,
|
||||
emailService EmailService,
|
||||
cacheBackend cache.Cache,
|
||||
) *UserService {
|
||||
return &UserService{
|
||||
) UserService {
|
||||
return &userServiceImpl{
|
||||
userRepo: userRepo,
|
||||
systemMessageService: systemMessageService,
|
||||
emailCodeService: NewEmailCodeService(emailService, cacheBackend),
|
||||
@@ -33,7 +79,7 @@ func NewUserService(
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err == nil && user != nil {
|
||||
return ErrEmailExists
|
||||
@@ -42,7 +88,7 @@ func (s *UserService) SendRegisterCode(ctx context.Context, email string) error
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
@@ -51,7 +97,7 @@ func (s *UserService) SendPasswordResetCode(ctx context.Context, email string) e
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
@@ -78,7 +124,7 @@ func (s *UserService) SendCurrentUserEmailVerifyCode(ctx context.Context, userID
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
@@ -107,7 +153,7 @@ func (s *UserService) VerifyCurrentUserEmail(ctx context.Context, userID, email,
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil || user == nil {
|
||||
return ErrUserNotFound
|
||||
@@ -119,7 +165,7 @@ func (s *UserService) SendChangePasswordCode(ctx context.Context, userID string)
|
||||
}
|
||||
|
||||
// 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) {
|
||||
return nil, ErrInvalidUsername
|
||||
@@ -199,7 +245,7 @@ func (s *UserService) Register(ctx context.Context, username, email, password, n
|
||||
}
|
||||
|
||||
// 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)
|
||||
var (
|
||||
user *model.User
|
||||
@@ -228,22 +274,22 @@ func (s *UserService) Login(ctx context.Context, account, password string) (*mod
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
@@ -263,7 +309,7 @@ func (s *UserService) GetUserByIDWithFollowingStatus(ctx context.Context, userID
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return nil, false, false, err
|
||||
@@ -290,22 +336,22 @@ func (s *UserService) GetUserByIDWithMutualFollowStatus(ctx context.Context, use
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -357,7 +403,7 @@ func (s *UserService) FollowUser(ctx context.Context, followerID, followeeID str
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
@@ -386,7 +432,7 @@ func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID s
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return ErrInvalidOperation
|
||||
}
|
||||
@@ -394,7 +440,7 @@ func (s *UserService) BlockUser(ctx context.Context, blockerID, blockedID string
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return ErrInvalidOperation
|
||||
}
|
||||
@@ -402,17 +448,17 @@ func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID stri
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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
|
||||
pageSizeInt := 20
|
||||
@@ -434,7 +480,7 @@ func (s *UserService) GetFollowingList(ctx context.Context, userID, page, pageSi
|
||||
}
|
||||
|
||||
// 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
|
||||
pageSizeInt := 20
|
||||
@@ -456,12 +502,12 @@ func (s *UserService) GetFollowersList(ctx context.Context, userID, page, pageSi
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
return true, nil // 用户不存在,可用
|
||||
@@ -470,7 +516,7 @@ func (s *UserService) CheckUsernameAvailable(ctx context.Context, username strin
|
||||
}
|
||||
|
||||
// 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)
|
||||
if err != nil {
|
||||
@@ -500,7 +546,7 @@ func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, n
|
||||
}
|
||||
|
||||
// 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)
|
||||
if !utils.ValidateEmail(email) {
|
||||
return ErrInvalidEmail
|
||||
@@ -526,40 +572,29 @@ func (s *UserService) ResetPasswordByEmail(ctx context.Context, email, verificat
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
|
||||
// 错误定义
|
||||
// 错误定义 - 使用统一的 AppError
|
||||
var (
|
||||
ErrInvalidUsername = &ServiceError{Code: 400, Message: "invalid username"}
|
||||
ErrInvalidEmail = &ServiceError{Code: 400, Message: "invalid email"}
|
||||
ErrInvalidPhone = &ServiceError{Code: 400, Message: "invalid phone number"}
|
||||
ErrWeakPassword = &ServiceError{Code: 400, Message: "password too weak"}
|
||||
ErrUsernameExists = &ServiceError{Code: 400, Message: "username already exists"}
|
||||
ErrEmailExists = &ServiceError{Code: 400, Message: "email already exists"}
|
||||
ErrPhoneExists = &ServiceError{Code: 400, Message: "phone number already exists"}
|
||||
ErrUserNotFound = &ServiceError{Code: 404, Message: "user not found"}
|
||||
ErrUserBanned = &ServiceError{Code: 403, Message: "user is banned"}
|
||||
ErrUserBlocked = &ServiceError{Code: 403, Message: "blocked relationship exists"}
|
||||
ErrInvalidOperation = &ServiceError{Code: 400, Message: "invalid operation"}
|
||||
ErrEmailServiceUnavailable = &ServiceError{Code: 503, Message: "email service unavailable"}
|
||||
ErrVerificationCodeTooFrequent = &ServiceError{Code: 429, Message: "verification code sent too frequently"}
|
||||
ErrVerificationCodeInvalid = &ServiceError{Code: 400, Message: "invalid verification code"}
|
||||
ErrVerificationCodeExpired = &ServiceError{Code: 400, Message: "verification code expired"}
|
||||
ErrVerificationCodeUnavailable = &ServiceError{Code: 500, Message: "verification code storage unavailable"}
|
||||
ErrEmailAlreadyVerified = &ServiceError{Code: 400, Message: "email already verified"}
|
||||
ErrEmailNotBound = &ServiceError{Code: 400, Message: "email not bound"}
|
||||
ErrInvalidUsername = apperrors.ErrInvalidUsername
|
||||
ErrInvalidEmail = apperrors.ErrInvalidEmail
|
||||
ErrInvalidPhone = apperrors.ErrInvalidPhone
|
||||
ErrWeakPassword = apperrors.ErrWeakPassword
|
||||
ErrUsernameExists = apperrors.ErrUsernameExists
|
||||
ErrEmailExists = apperrors.ErrEmailExists
|
||||
ErrPhoneExists = apperrors.ErrPhoneExists
|
||||
ErrUserNotFound = apperrors.ErrUserNotFound
|
||||
ErrUserBanned = apperrors.ErrUserBanned
|
||||
ErrUserBlocked = apperrors.ErrUserBlocked
|
||||
ErrInvalidOperation = apperrors.ErrInvalidOperation
|
||||
ErrEmailServiceUnavailable = apperrors.ErrEmailServiceUnavailable
|
||||
ErrVerificationCodeTooFrequent = apperrors.ErrVerificationCodeTooFrequent
|
||||
ErrVerificationCodeInvalid = apperrors.ErrVerificationCodeInvalid
|
||||
ErrVerificationCodeExpired = apperrors.ErrVerificationCodeExpired
|
||||
ErrVerificationCodeUnavailable = apperrors.ErrVerificationCodeUnavailable
|
||||
ErrEmailAlreadyVerified = apperrors.ErrEmailAlreadyVerified
|
||||
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