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:
2026-03-13 09:38:18 +08:00
parent 1216423350
commit cf36b1350d
49 changed files with 2571 additions and 2218 deletions

9
go.mod
View File

@@ -7,17 +7,18 @@ require (
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/golang-jwt/jwt/v5 v5.2.0 github.com/golang-jwt/jwt/v5 v5.2.0
github.com/google/uuid v1.5.0 github.com/google/uuid v1.5.0
github.com/google/wire v0.7.0
github.com/gorse-io/gorse-go v0.5.0-alpha.3 github.com/gorse-io/gorse-go v0.5.0-alpha.3
github.com/minio/minio-go/v7 v7.0.66 github.com/minio/minio-go/v7 v7.0.66
github.com/redis/go-redis/v9 v9.3.1 github.com/redis/go-redis/v9 v9.3.1
github.com/spf13/viper v1.18.2 github.com/spf13/viper v1.18.2
go.uber.org/zap v1.26.0 go.uber.org/zap v1.26.0
golang.org/x/crypto v0.17.0 golang.org/x/crypto v0.26.0
golang.org/x/image v0.24.0 golang.org/x/image v0.24.0
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
gorm.io/driver/postgres v1.5.4 gorm.io/driver/postgres v1.5.4
gorm.io/driver/sqlite v1.5.4 gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.5 gorm.io/gorm v1.25.5
gopkg.in/gomail.v2 v2.0.0-20160411212932-81ebce5c23df
) )
require ( require (
@@ -75,8 +76,8 @@ require (
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.19.0 // indirect golang.org/x/net v0.28.0 // indirect
golang.org/x/sys v0.15.0 // indirect golang.org/x/sys v0.23.0 // indirect
golang.org/x/text v0.22.0 // indirect golang.org/x/text v0.22.0 // indirect
google.golang.org/protobuf v1.31.0 // indirect google.golang.org/protobuf v1.31.0 // indirect
gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect gopkg.in/alexcesaro/quotedprintable.v3 v3.0.0-20150716171945-2caba252f4dc // indirect

20
go.sum
View File

@@ -58,13 +58,13 @@ github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVI
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
github.com/gorse-io/gorse-go v0.5.0-alpha.3 h1:GR/OWzq016VyFyTTxgQWeayGahRVzB1cGFIW/AaShC4= github.com/gorse-io/gorse-go v0.5.0-alpha.3 h1:GR/OWzq016VyFyTTxgQWeayGahRVzB1cGFIW/AaShC4=
github.com/gorse-io/gorse-go v0.5.0-alpha.3/go.mod h1:ZxmVHzZPKm5pmEIlqaRDwK0rkfTRHlfziO033XZ+RW0= github.com/gorse-io/gorse-go v0.5.0-alpha.3/go.mod h1:ZxmVHzZPKm5pmEIlqaRDwK0rkfTRHlfziO033XZ+RW0=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@@ -175,20 +175,20 @@ go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw=
golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ= golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8= golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE=
golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg=
golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190204203706-41f3e6584952/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.23.0 h1:YfKFowiIMvtgl1UERQoTPPToxltDeZfbj4H7dVUCwmM=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.23.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

View File

@@ -3,20 +3,9 @@ package cache
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"fmt"
"log"
"math"
"math/rand" "math/rand"
"sort"
"strconv"
"strings"
"sync" "sync"
"sync/atomic"
"time" "time"
"github.com/redis/go-redis/v9"
redisPkg "carrot_bbs/internal/pkg/redis"
) )
// Cache 缓存接口 // Cache 缓存接口
@@ -71,33 +60,10 @@ type Cache interface {
Expire(ctx context.Context, key string, ttl time.Duration) error Expire(ctx context.Context, key string, ttl time.Duration) error
} }
// cacheItem 缓存项(用于内存缓存降级)
type cacheItem struct {
value interface{}
expiration int64 // 过期时间戳(纳秒)
}
const nullMarkerValue = "__carrot_cache_null__" const nullMarkerValue = "__carrot_cache_null__"
type cacheMetrics struct {
hit atomic.Int64
miss atomic.Int64
decodeError atomic.Int64
setError atomic.Int64
invalidate atomic.Int64
}
var metrics cacheMetrics
var loadLocks sync.Map var loadLocks sync.Map
type MetricsSnapshot struct {
Hit int64
Miss int64
DecodeError int64
SetError int64
Invalidate int64
}
type Settings struct { type Settings struct {
Enabled bool Enabled bool
KeyPrefix string KeyPrefix string
@@ -170,814 +136,6 @@ func normalizePrefix(prefix string) string {
return settings.KeyPrefix + ":" + prefix return settings.KeyPrefix + ":" + prefix
} }
func GetMetricsSnapshot() MetricsSnapshot {
return MetricsSnapshot{
Hit: metrics.hit.Load(),
Miss: metrics.miss.Load(),
DecodeError: metrics.decodeError.Load(),
SetError: metrics.setError.Load(),
Invalidate: metrics.invalidate.Load(),
}
}
// isExpired 检查是否过期
func (item *cacheItem) isExpired() bool {
if item.expiration == 0 {
return false
}
return time.Now().UnixNano() > item.expiration
}
// MemoryCache 内存缓存实现(降级使用)
type MemoryCache struct {
items sync.Map
// cleanupInterval 清理过期缓存的间隔
cleanupInterval time.Duration
// stopCleanup 停止清理协程的通道
stopCleanup chan struct{}
}
// NewMemoryCache 创建内存缓存
func NewMemoryCache() *MemoryCache {
c := &MemoryCache{
cleanupInterval: 1 * time.Minute,
stopCleanup: make(chan struct{}),
}
// 启动后台清理协程
go c.cleanup()
return c
}
// Set 设置缓存值
func (c *MemoryCache) Set(key string, value interface{}, ttl time.Duration) {
key = normalizeKey(key)
var expiration int64
if ttl > 0 {
expiration = time.Now().Add(ttl).UnixNano()
}
c.items.Store(key, &cacheItem{
value: value,
expiration: expiration,
})
}
// Get 获取缓存值
func (c *MemoryCache) Get(key string) (interface{}, bool) {
key = normalizeKey(key)
val, ok := c.items.Load(key)
if !ok {
return nil, false
}
item := val.(*cacheItem)
if item.isExpired() {
c.items.Delete(key)
return nil, false
}
return item.value, true
}
// Delete 删除缓存
func (c *MemoryCache) Delete(key string) {
key = normalizeKey(key)
metrics.invalidate.Add(1)
c.items.Delete(key)
}
// DeleteByPrefix 根据前缀删除缓存
func (c *MemoryCache) DeleteByPrefix(prefix string) {
prefix = normalizePrefix(prefix)
c.items.Range(func(key, value interface{}) bool {
if keyStr, ok := key.(string); ok {
if strings.HasPrefix(keyStr, prefix) {
metrics.invalidate.Add(1)
c.items.Delete(key)
}
}
return true
})
}
// Clear 清空所有缓存
func (c *MemoryCache) Clear() {
c.items.Range(func(key, value interface{}) bool {
metrics.invalidate.Add(1)
c.items.Delete(key)
return true
})
}
// Exists 检查键是否存在
func (c *MemoryCache) Exists(key string) bool {
_, ok := c.Get(key)
return ok
}
// Increment 增加计数器的值
func (c *MemoryCache) Increment(key string) int64 {
return c.IncrementBy(key, 1)
}
// IncrementBy 增加指定值
func (c *MemoryCache) IncrementBy(key string, value int64) int64 {
key = normalizeKey(key)
for {
val, ok := c.items.Load(key)
if !ok {
// 键不存在,创建新值
c.items.Store(key, &cacheItem{
value: value,
expiration: 0,
})
return value
}
item := val.(*cacheItem)
if item.isExpired() {
// 已过期,创建新值
c.items.Store(key, &cacheItem{
value: value,
expiration: 0,
})
return value
}
// 尝试更新
currentValue, ok := item.value.(int64)
if !ok {
// 类型不匹配,覆盖为新值
c.items.Store(key, &cacheItem{
value: value,
expiration: item.expiration,
})
return value
}
newValue := currentValue + value
// 使用 CAS 操作确保并发安全
if c.items.CompareAndSwap(key, val, &cacheItem{
value: newValue,
expiration: item.expiration,
}) {
return newValue
}
// CAS 失败,重试
}
}
// cleanup 定期清理过期缓存
func (c *MemoryCache) cleanup() {
ticker := time.NewTicker(c.cleanupInterval)
defer ticker.Stop()
for {
select {
case <-ticker.C:
c.cleanExpired()
case <-c.stopCleanup:
return
}
}
}
// cleanExpired 清理过期缓存
func (c *MemoryCache) cleanExpired() {
count := 0
c.items.Range(func(key, value interface{}) bool {
item := value.(*cacheItem)
if item.isExpired() {
c.items.Delete(key)
count++
}
return true
})
if count > 0 {
log.Printf("[Cache] Cleaned %d expired items", count)
}
}
// Stop 停止缓存清理协程
func (c *MemoryCache) Stop() {
close(c.stopCleanup)
}
// ==================== MemoryCache Hash 操作 ====================
// hashItem Hash 存储项
type hashItem struct {
fields sync.Map
}
// HSet 设置 Hash 字段
func (c *MemoryCache) HSet(ctx context.Context, key string, field string, value interface{}) error {
key = normalizeKey(key)
item, _ := c.items.Load(key)
var h *hashItem
if item == nil {
h = &hashItem{}
c.items.Store(key, &cacheItem{value: h, expiration: 0})
} else {
ci := item.(*cacheItem)
if ci.isExpired() {
h = &hashItem{}
c.items.Store(key, &cacheItem{value: h, expiration: 0})
} else {
h = ci.value.(*hashItem)
}
}
h.fields.Store(field, value)
return nil
}
// HMSet 批量设置 Hash 字段
func (c *MemoryCache) HMSet(ctx context.Context, key string, values map[string]interface{}) error {
for field, value := range values {
if err := c.HSet(ctx, key, field, value); err != nil {
return err
}
}
return nil
}
// HGet 获取 Hash 字段值
func (c *MemoryCache) HGet(ctx context.Context, key string, field string) (string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return "", fmt.Errorf("key not found")
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return "", fmt.Errorf("key not found")
}
h, ok := ci.value.(*hashItem)
if !ok {
return "", fmt.Errorf("key is not a hash")
}
val, ok := h.fields.Load(field)
if !ok {
return "", fmt.Errorf("field not found")
}
switch v := val.(type) {
case string:
return v, nil
case []byte:
return string(v), nil
default:
data, _ := json.Marshal(v)
return string(data), nil
}
}
// HMGet 批量获取 Hash 字段值
func (c *MemoryCache) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
result := make([]interface{}, len(fields))
for i, field := range fields {
val, err := c.HGet(ctx, key, field)
if err != nil {
result[i] = nil
} else {
result[i] = val
}
}
return result, nil
}
// HGetAll 获取 Hash 所有字段
func (c *MemoryCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil, fmt.Errorf("key not found")
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil, fmt.Errorf("key not found")
}
h, ok := ci.value.(*hashItem)
if !ok {
return nil, fmt.Errorf("key is not a hash")
}
result := make(map[string]string)
h.fields.Range(func(k, v interface{}) bool {
keyStr := k.(string)
switch val := v.(type) {
case string:
result[keyStr] = val
case []byte:
result[keyStr] = string(val)
default:
data, _ := json.Marshal(val)
result[keyStr] = string(data)
}
return true
})
return result, nil
}
// HDel 删除 Hash 字段
func (c *MemoryCache) HDel(ctx context.Context, key string, fields ...string) error {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil
}
h, ok := ci.value.(*hashItem)
if !ok {
return nil
}
for _, field := range fields {
h.fields.Delete(field)
}
return nil
}
// ==================== MemoryCache Sorted Set 操作 ====================
// zItem Sorted Set 成员
type zItem struct {
score float64
member string
}
// zsetItem Sorted Set 存储项
type zsetItem struct {
members sync.Map // member -> *zItem
byScore *sortedSlice // 按分数排序的切片
}
// sortedSlice 简单的排序切片实现
type sortedSlice struct {
items []*zItem
mu sync.RWMutex
}
// ZAdd 添加 Sorted Set 成员
func (c *MemoryCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
key = normalizeKey(key)
item, _ := c.items.Load(key)
var z *zsetItem
if item == nil {
z = &zsetItem{byScore: &sortedSlice{}}
c.items.Store(key, &cacheItem{value: z, expiration: 0})
} else {
ci := item.(*cacheItem)
if ci.isExpired() {
z = &zsetItem{byScore: &sortedSlice{}}
c.items.Store(key, &cacheItem{value: z, expiration: 0})
} else {
z = ci.value.(*zsetItem)
}
}
z.members.Store(member, &zItem{score: score, member: member})
z.byScore.mu.Lock()
// 简单实现:重新构建排序切片
z.byScore.items = nil
z.members.Range(func(k, v interface{}) bool {
z.byScore.items = append(z.byScore.items, v.(*zItem))
return true
})
// 按分数排序
sort.Slice(z.byScore.items, func(i, j int) bool {
return z.byScore.items[i].score < z.byScore.items[j].score
})
z.byScore.mu.Unlock()
return nil
}
// ZRangeByScore 按分数范围获取成员(升序)
func (c *MemoryCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil, nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil, nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return nil, fmt.Errorf("key is not a sorted set")
}
minScore, _ := strconv.ParseFloat(min, 64)
maxScore, _ := strconv.ParseFloat(max, 64)
if min == "-inf" {
minScore = math.Inf(-1)
}
if max == "+inf" {
maxScore = math.Inf(1)
}
z.byScore.mu.RLock()
defer z.byScore.mu.RUnlock()
var result []string
var skipped int64 = 0
for _, item := range z.byScore.items {
if item.score < minScore || item.score > maxScore {
continue
}
if skipped < offset {
skipped++
continue
}
if count > 0 && int64(len(result)) >= count {
break
}
result = append(result, item.member)
}
return result, nil
}
// ZRevRangeByScore 按分数范围获取成员(降序)
func (c *MemoryCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil, nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil, nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return nil, fmt.Errorf("key is not a sorted set")
}
minScore, _ := strconv.ParseFloat(min, 64)
maxScore, _ := strconv.ParseFloat(max, 64)
if min == "-inf" {
minScore = math.Inf(-1)
}
if max == "+inf" {
maxScore = math.Inf(1)
}
z.byScore.mu.RLock()
defer z.byScore.mu.RUnlock()
var result []string
var skipped int64 = 0
// 从后往前遍历
for i := len(z.byScore.items) - 1; i >= 0; i-- {
item := z.byScore.items[i]
if item.score < minScore || item.score > maxScore {
continue
}
if skipped < offset {
skipped++
continue
}
if count > 0 && int64(len(result)) >= count {
break
}
result = append(result, item.member)
}
return result, nil
}
// ZRem 删除 Sorted Set 成员
func (c *MemoryCache) ZRem(ctx context.Context, key string, members ...interface{}) error {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return nil
}
for _, m := range members {
if member, ok := m.(string); ok {
z.members.Delete(member)
}
}
// 重建排序切片
z.byScore.mu.Lock()
z.byScore.items = nil
z.members.Range(func(k, v interface{}) bool {
z.byScore.items = append(z.byScore.items, v.(*zItem))
return true
})
sort.Slice(z.byScore.items, func(i, j int) bool {
return z.byScore.items[i].score < z.byScore.items[j].score
})
z.byScore.mu.Unlock()
return nil
}
// ZCard 获取 Sorted Set 成员数量
func (c *MemoryCache) ZCard(ctx context.Context, key string) (int64, error) {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return 0, nil
}
ci := item.(*cacheItem)
if ci.isExpired() {
c.items.Delete(key)
return 0, nil
}
z, ok := ci.value.(*zsetItem)
if !ok {
return 0, fmt.Errorf("key is not a sorted set")
}
var count int64 = 0
z.members.Range(func(k, v interface{}) bool {
count++
return true
})
return count, nil
}
// ==================== MemoryCache 计数器操作 ====================
// Incr 原子递增(返回新值)
func (c *MemoryCache) Incr(ctx context.Context, key string) (int64, error) {
return c.IncrementBy(key, 1), nil
}
// Expire 设置过期时间
func (c *MemoryCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
key = normalizeKey(key)
item, ok := c.items.Load(key)
if !ok {
return fmt.Errorf("key not found")
}
ci := item.(*cacheItem)
var expiration int64
if ttl > 0 {
expiration = time.Now().Add(ttl).UnixNano()
}
c.items.Store(key, &cacheItem{
value: ci.value,
expiration: expiration,
})
return nil
}
// RedisCache Redis缓存实现
type RedisCache struct {
client *redisPkg.Client
ctx context.Context
}
// NewRedisCache 创建Redis缓存
func NewRedisCache(client *redisPkg.Client) *RedisCache {
return &RedisCache{
client: client,
ctx: context.Background(),
}
}
// Set 设置缓存值
func (c *RedisCache) Set(key string, value interface{}, ttl time.Duration) {
key = normalizeKey(key)
// 将值序列化为JSON
data, err := json.Marshal(value)
if err != nil {
metrics.setError.Add(1)
log.Printf("[RedisCache] Failed to marshal value for key %s: %v", key, err)
return
}
if err := c.client.Set(c.ctx, key, data, ttl); err != nil {
metrics.setError.Add(1)
log.Printf("[RedisCache] Failed to set key %s: %v", key, err)
}
}
// Get 获取缓存值
func (c *RedisCache) Get(key string) (interface{}, bool) {
key = normalizeKey(key)
data, err := c.client.Get(c.ctx, key)
if err != nil {
if err == redis.Nil {
return nil, false
}
log.Printf("[RedisCache] Failed to get key %s: %v", key, err)
return nil, false
}
// 返回原始字符串,由调用侧决定如何解码为目标类型
return data, true
}
// Delete 删除缓存
func (c *RedisCache) Delete(key string) {
key = normalizeKey(key)
metrics.invalidate.Add(1)
if err := c.client.Del(c.ctx, key); err != nil {
log.Printf("[RedisCache] Failed to delete key %s: %v", key, err)
}
}
// DeleteByPrefix 根据前缀删除缓存
func (c *RedisCache) DeleteByPrefix(prefix string) {
prefix = normalizePrefix(prefix)
// 使用原生客户端执行SCAN命令
rdb := c.client.GetClient()
var cursor uint64
for {
keys, nextCursor, err := rdb.Scan(c.ctx, cursor, prefix+"*", 100).Result()
if err != nil {
log.Printf("[RedisCache] Failed to scan keys with prefix %s: %v", prefix, err)
return
}
if len(keys) > 0 {
metrics.invalidate.Add(int64(len(keys)))
if err := c.client.Del(c.ctx, keys...); err != nil {
log.Printf("[RedisCache] Failed to delete keys with prefix %s: %v", prefix, err)
}
}
cursor = nextCursor
if cursor == 0 {
break
}
}
}
// Clear 清空所有缓存
func (c *RedisCache) Clear() {
if settings.DisableFlushDB {
log.Printf("[RedisCache] Skip FlushDB because cache.disable_flushdb=true")
return
}
metrics.invalidate.Add(1)
rdb := c.client.GetClient()
if err := rdb.FlushDB(c.ctx).Err(); err != nil {
log.Printf("[RedisCache] Failed to clear cache: %v", err)
}
}
// Exists 检查键是否存在
func (c *RedisCache) Exists(key string) bool {
key = normalizeKey(key)
n, err := c.client.Exists(c.ctx, key)
if err != nil {
log.Printf("[RedisCache] Failed to check existence of key %s: %v", key, err)
return false
}
return n > 0
}
// Increment 增加计数器的值
func (c *RedisCache) Increment(key string) int64 {
return c.IncrementBy(key, 1)
}
// IncrementBy 增加指定值
func (c *RedisCache) IncrementBy(key string, value int64) int64 {
key = normalizeKey(key)
rdb := c.client.GetClient()
result, err := rdb.IncrBy(c.ctx, key, value).Result()
if err != nil {
log.Printf("[RedisCache] Failed to increment key %s: %v", key, err)
return 0
}
return result
}
// ==================== RedisCache Hash 操作 ====================
// HSet 设置 Hash 字段
func (c *RedisCache) HSet(ctx context.Context, key string, field string, value interface{}) error {
key = normalizeKey(key)
return c.client.HSet(ctx, key, field, value)
}
// HMSet 批量设置 Hash 字段
func (c *RedisCache) HMSet(ctx context.Context, key string, values map[string]interface{}) error {
key = normalizeKey(key)
return c.client.HMSet(ctx, key, values)
}
// HGet 获取 Hash 字段值
func (c *RedisCache) HGet(ctx context.Context, key string, field string) (string, error) {
key = normalizeKey(key)
return c.client.HGet(ctx, key, field)
}
// HMGet 批量获取 Hash 字段值
func (c *RedisCache) HMGet(ctx context.Context, key string, fields ...string) ([]interface{}, error) {
key = normalizeKey(key)
return c.client.HMGet(ctx, key, fields...)
}
// HGetAll 获取 Hash 所有字段
func (c *RedisCache) HGetAll(ctx context.Context, key string) (map[string]string, error) {
key = normalizeKey(key)
return c.client.HGetAll(ctx, key)
}
// HDel 删除 Hash 字段
func (c *RedisCache) HDel(ctx context.Context, key string, fields ...string) error {
key = normalizeKey(key)
return c.client.HDel(ctx, key, fields...)
}
// ==================== RedisCache Sorted Set 操作 ====================
// ZAdd 添加 Sorted Set 成员
func (c *RedisCache) ZAdd(ctx context.Context, key string, score float64, member string) error {
key = normalizeKey(key)
return c.client.ZAdd(ctx, key, score, member)
}
// ZRangeByScore 按分数范围获取成员(升序)
func (c *RedisCache) ZRangeByScore(ctx context.Context, key string, min, max string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
return c.client.ZRangeByScore(ctx, key, min, max, offset, count)
}
// ZRevRangeByScore 按分数范围获取成员(降序)
func (c *RedisCache) ZRevRangeByScore(ctx context.Context, key string, max, min string, offset, count int64) ([]string, error) {
key = normalizeKey(key)
return c.client.ZRevRangeByScore(ctx, key, max, min, offset, count)
}
// ZRem 删除 Sorted Set 成员
func (c *RedisCache) ZRem(ctx context.Context, key string, members ...interface{}) error {
key = normalizeKey(key)
return c.client.ZRem(ctx, key, members...)
}
// ZCard 获取 Sorted Set 成员数量
func (c *RedisCache) ZCard(ctx context.Context, key string) (int64, error) {
key = normalizeKey(key)
return c.client.ZCard(ctx, key)
}
// ==================== RedisCache 计数器操作 ====================
// Incr 原子递增(返回新值)
func (c *RedisCache) Incr(ctx context.Context, key string) (int64, error) {
key = normalizeKey(key)
return c.client.Incr(ctx, key)
}
// Expire 设置过期时间
func (c *RedisCache) Expire(ctx context.Context, key string, ttl time.Duration) error {
key = normalizeKey(key)
_, err := c.client.Expire(ctx, key, ttl)
return err
}
// 全局缓存实例
var globalCache Cache
var once sync.Once
// InitCache 初始化全局缓存实例使用Redis
func InitCache(redisClient *redisPkg.Client) {
once.Do(func() {
if redisClient != nil {
globalCache = NewRedisCache(redisClient)
log.Println("[Cache] Initialized Redis cache")
} else {
globalCache = NewMemoryCache()
log.Println("[Cache] Initialized Memory cache (Redis not available)")
}
})
}
// GetCache 获取全局缓存实例
func GetCache() Cache {
if globalCache == nil {
// 如果未初始化,返回内存缓存作为降级
log.Println("[Cache] Warning: Cache not initialized, using Memory cache")
return NewMemoryCache()
}
return globalCache
}
// GetRedisClient 从缓存中获取Redis客户端仅在Redis模式下有效
func GetRedisClient() (*redisPkg.Client, error) {
if redisCache, ok := globalCache.(*RedisCache); ok {
return redisCache.client, nil
}
return nil, fmt.Errorf("cache is not using Redis backend")
}
func SetWithJitter(c Cache, key string, value interface{}, ttl time.Duration, jitterRatio float64) { func SetWithJitter(c Cache, key string, value interface{}, ttl time.Duration, jitterRatio float64) {
if !settings.Enabled { if !settings.Enabled {
return return
@@ -1014,16 +172,16 @@ func GetTyped[T any](c Cache, key string) (T, bool) {
} }
raw, ok := c.Get(key) raw, ok := c.Get(key)
if !ok { if !ok {
metrics.miss.Add(1) recordMiss()
return zero, false return zero, false
} }
if str, ok := raw.(string); ok && str == nullMarkerValue { if str, ok := raw.(string); ok && str == nullMarkerValue {
metrics.hit.Add(1) recordHit()
return zero, false return zero, false
} }
if typed, ok := raw.(T); ok { if typed, ok := raw.(T); ok {
metrics.hit.Add(1) recordHit()
return typed, true return typed, true
} }
@@ -1031,29 +189,29 @@ func GetTyped[T any](c Cache, key string) (T, bool) {
switch v := raw.(type) { switch v := raw.(type) {
case string: case string:
if err := json.Unmarshal([]byte(v), &out); err != nil { if err := json.Unmarshal([]byte(v), &out); err != nil {
metrics.decodeError.Add(1) recordDecodeError()
return zero, false return zero, false
} }
metrics.hit.Add(1) recordHit()
return out, true return out, true
case []byte: case []byte:
if err := json.Unmarshal(v, &out); err != nil { if err := json.Unmarshal(v, &out); err != nil {
metrics.decodeError.Add(1) recordDecodeError()
return zero, false return zero, false
} }
metrics.hit.Add(1) recordHit()
return out, true return out, true
default: default:
data, err := json.Marshal(v) data, err := json.Marshal(v)
if err != nil { if err != nil {
metrics.decodeError.Add(1) recordDecodeError()
return zero, false return zero, false
} }
if err := json.Unmarshal(data, &out); err != nil { if err := json.Unmarshal(data, &out); err != nil {
metrics.decodeError.Add(1) recordDecodeError()
return zero, false return zero, false
} }
metrics.hit.Add(1) recordHit()
return out, true return out, true
} }
} }

65
internal/cache/metrics.go vendored Normal file
View 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
View 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
View 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
}

View File

@@ -1,19 +1,16 @@
package config package config
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"strconv" "strconv"
"strings" "strings"
"time" "time"
"github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
"github.com/redis/go-redis/v9"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
// Config 主配置结构体
type Config struct { type Config struct {
Server ServerConfig `mapstructure:"server"` Server ServerConfig `mapstructure:"server"`
Database DatabaseConfig `mapstructure:"database"` Database DatabaseConfig `mapstructure:"database"`
@@ -30,217 +27,7 @@ type Config struct {
ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"` ConversationCache ConversationCacheConfig `mapstructure:"conversation_cache"`
} }
type ServerConfig struct { // Load 加载配置文件
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Mode string `mapstructure:"mode"`
}
type DatabaseConfig struct {
Type string `mapstructure:"type"`
SQLite SQLiteConfig `mapstructure:"sqlite"`
Postgres PostgresConfig `mapstructure:"postgres"`
MaxIdleConns int `mapstructure:"max_idle_conns"`
MaxOpenConns int `mapstructure:"max_open_conns"`
LogLevel string `mapstructure:"log_level"`
SlowThresholdMs int `mapstructure:"slow_threshold_ms"`
IgnoreRecordNotFound bool `mapstructure:"ignore_record_not_found"`
ParameterizedQueries bool `mapstructure:"parameterized_queries"`
}
type SQLiteConfig struct {
Path string `mapstructure:"path"`
}
type PostgresConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
User string `mapstructure:"user"`
Password string `mapstructure:"password"`
DBName string `mapstructure:"dbname"`
SSLMode string `mapstructure:"sslmode"`
}
func (d PostgresConfig) DSN() string {
return fmt.Sprintf(
"host=%s port=%d user=%s password=%s dbname=%s sslmode=%s",
d.Host, d.Port, d.User, d.Password, d.DBName, d.SSLMode,
)
}
type RedisConfig struct {
Type string `mapstructure:"type"`
Redis RedisServerConfig `mapstructure:"redis"`
Miniredis MiniredisConfig `mapstructure:"miniredis"`
PoolSize int `mapstructure:"pool_size"`
}
type CacheConfig struct {
Enabled bool `mapstructure:"enabled"`
KeyPrefix string `mapstructure:"key_prefix"`
DefaultTTL int `mapstructure:"default_ttl"`
NullTTL int `mapstructure:"null_ttl"`
JitterRatio float64 `mapstructure:"jitter_ratio"`
DisableFlushDB bool `mapstructure:"disable_flushdb"`
Modules CacheModuleTTL `mapstructure:"modules"`
}
type CacheModuleTTL struct {
PostList int `mapstructure:"post_list_ttl"`
Conversation int `mapstructure:"conversation_ttl"`
UnreadCount int `mapstructure:"unread_count_ttl"`
GroupMembers int `mapstructure:"group_members_ttl"`
}
type RedisServerConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Password string `mapstructure:"password"`
DB int `mapstructure:"db"`
}
func (r RedisServerConfig) Addr() string {
return fmt.Sprintf("%s:%d", r.Host, r.Port)
}
type MiniredisConfig struct {
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
}
type S3Config struct {
Endpoint string `mapstructure:"endpoint"`
AccessKey string `mapstructure:"access_key"`
SecretKey string `mapstructure:"secret_key"`
Bucket string `mapstructure:"bucket"`
UseSSL bool `mapstructure:"use_ssl"`
Region string `mapstructure:"region"`
Domain string `mapstructure:"domain"` // 自定义域名,如 s3.carrot.skin
}
type JWTConfig struct {
Secret string `mapstructure:"secret"`
AccessTokenExpire time.Duration `mapstructure:"access_token_expire"`
RefreshTokenExpire time.Duration `mapstructure:"refresh_token_expire"`
}
type LogConfig struct {
Level string `mapstructure:"level"`
Encoding string `mapstructure:"encoding"`
OutputPaths []string `mapstructure:"output_paths"`
}
type RateLimitConfig struct {
Enabled bool `mapstructure:"enabled"`
RequestsPerMinute int `mapstructure:"requests_per_minute"`
}
type UploadConfig struct {
MaxFileSize int64 `mapstructure:"max_file_size"`
AllowedTypes []string `mapstructure:"allowed_types"`
}
type GorseConfig struct {
Address string `mapstructure:"address"`
APIKey string `mapstructure:"api_key"`
Enabled bool `mapstructure:"enabled"`
Dashboard string `mapstructure:"dashboard"`
ImportPassword string `mapstructure:"import_password"`
EmbeddingAPIKey string `mapstructure:"embedding_api_key"`
EmbeddingURL string `mapstructure:"embedding_url"`
EmbeddingModel string `mapstructure:"embedding_model"`
}
type OpenAIConfig struct {
Enabled bool `mapstructure:"enabled"`
BaseURL string `mapstructure:"base_url"`
APIKey string `mapstructure:"api_key"`
ModerationModel string `mapstructure:"moderation_model"`
ModerationMaxImagesPerRequest int `mapstructure:"moderation_max_images_per_request"`
RequestTimeout int `mapstructure:"request_timeout"`
StrictModeration bool `mapstructure:"strict_moderation"`
}
type EmailConfig struct {
Enabled bool `mapstructure:"enabled"`
Host string `mapstructure:"host"`
Port int `mapstructure:"port"`
Username string `mapstructure:"username"`
Password string `mapstructure:"password"`
FromAddress string `mapstructure:"from_address"`
FromName string `mapstructure:"from_name"`
UseTLS bool `mapstructure:"use_tls"`
InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
Timeout int `mapstructure:"timeout"`
}
// ConversationCacheConfig 会话缓存配置
type ConversationCacheConfig struct {
// TTL 配置
DetailTTL string `mapstructure:"detail_ttl"`
ListTTL string `mapstructure:"list_ttl"`
ParticipantTTL string `mapstructure:"participant_ttl"`
UnreadTTL string `mapstructure:"unread_ttl"`
// 消息缓存配置
MessageDetailTTL string `mapstructure:"message_detail_ttl"`
MessageListTTL string `mapstructure:"message_list_ttl"`
MessageIndexTTL string `mapstructure:"message_index_ttl"`
MessageCountTTL string `mapstructure:"message_count_ttl"`
// 批量写入配置
BatchInterval string `mapstructure:"batch_interval"`
BatchThreshold int `mapstructure:"batch_threshold"`
BatchMaxSize int `mapstructure:"batch_max_size"`
BufferMaxSize int `mapstructure:"buffer_max_size"`
}
// ConversationCacheSettings 会话缓存运行时配置(用于传递给 cache 包)
type ConversationCacheSettings struct {
DetailTTL time.Duration
ListTTL time.Duration
ParticipantTTL time.Duration
UnreadTTL time.Duration
MessageDetailTTL time.Duration
MessageListTTL time.Duration
MessageIndexTTL time.Duration
MessageCountTTL time.Duration
BatchInterval time.Duration
BatchThreshold int
BatchMaxSize int
BufferMaxSize int
}
// ToSettings 将 ConversationCacheConfig 转换为 ConversationCacheSettings
func (c *ConversationCacheConfig) ToSettings() *ConversationCacheSettings {
return &ConversationCacheSettings{
DetailTTL: parseDuration(c.DetailTTL, 5*time.Minute),
ListTTL: parseDuration(c.ListTTL, 60*time.Second),
ParticipantTTL: parseDuration(c.ParticipantTTL, 5*time.Minute),
UnreadTTL: parseDuration(c.UnreadTTL, 30*time.Second),
MessageDetailTTL: parseDuration(c.MessageDetailTTL, 30*time.Minute),
MessageListTTL: parseDuration(c.MessageListTTL, 5*time.Minute),
MessageIndexTTL: parseDuration(c.MessageIndexTTL, 30*time.Minute),
MessageCountTTL: parseDuration(c.MessageCountTTL, 30*time.Minute),
BatchInterval: parseDuration(c.BatchInterval, 5*time.Second),
BatchThreshold: c.BatchThreshold,
BatchMaxSize: c.BatchMaxSize,
BufferMaxSize: c.BufferMaxSize,
}
}
// parseDuration 解析持续时间字符串,如果解析失败则返回默认值
func parseDuration(s string, defaultVal time.Duration) time.Duration {
if s == "" {
return defaultVal
}
d, err := time.ParseDuration(s)
if err != nil {
return defaultVal
}
return d
}
func Load(configPath string) (*Config, error) { func Load(configPath string) (*Config, error) {
viper.SetConfigFile(configPath) viper.SetConfigFile(configPath)
viper.SetConfigType("yaml") viper.SetConfigType("yaml")
@@ -426,49 +213,3 @@ func getEnvOrDefault(key, defaultValue string) string {
} }
return defaultValue return defaultValue
} }
// NewRedis 创建Redis客户端真实Redis
func NewRedis(cfg *RedisConfig) (*redis.Client, error) {
client := redis.NewClient(&redis.Options{
Addr: cfg.Redis.Addr(),
Password: cfg.Redis.Password,
DB: cfg.Redis.DB,
PoolSize: cfg.PoolSize,
})
ctx := context.Background()
if err := client.Ping(ctx).Err(); err != nil {
return nil, fmt.Errorf("failed to connect to redis: %w", err)
}
return client, nil
}
// NewS3 创建S3客户端
func NewS3(cfg *S3Config) (*minio.Client, error) {
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
defer cancel()
client, err := minio.New(cfg.Endpoint, &minio.Options{
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
Secure: cfg.UseSSL,
})
if err != nil {
return nil, fmt.Errorf("failed to create S3 client: %w", err)
}
exists, err := client.BucketExists(ctx, cfg.Bucket)
if err != nil {
return nil, fmt.Errorf("failed to check bucket: %w", err)
}
if !exists {
if err := client.MakeBucket(ctx, cfg.Bucket, minio.MakeBucketOptions{
Region: cfg.Region,
}); err != nil {
return nil, fmt.Errorf("failed to create bucket: %w", err)
}
}
return client, nil
}

View 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
View 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"`
}

View 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
View 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
View 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
View 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"`
}

View 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
}

View File

@@ -1,899 +1,13 @@
package dto package dto
import ( // converter.go - 公共转换函数
"carrot_bbs/internal/model" //
"carrot_bbs/internal/pkg/utils" // 转换函数已按领域拆分到以下文件:
"context" // - user_converter.go: 用户相关转换
"encoding/json" // - post_converter.go: 帖子和评论相关转换
"strconv" // - message_converter.go: 消息、会话、推送和设备令牌相关转换
) // - group_converter.go: 群组相关转换
// - notification_converter.go: 通知和系统消息相关转换
// ==================== User 转换 ==================== // - schedule_converter.go: 日程相关转换(已存在)
//
// getAvatarOrDefault 获取头像URL如果为空则返回在线头像生成服务的URL // 所有转换函数都在 dto 包中,可以直接通过 dto.ConvertXxxToResponse 调用。
func getAvatarOrDefault(user *model.User) string {
return utils.GetAvatarOrDefault(user.Username, user.Nickname, user.Avatar)
}
// ConvertUserToResponse 将User转换为UserResponse
func ConvertUserToResponse(user *model.User) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToResponseWithFollowing 将User转换为UserResponse包含关注状态
func ConvertUserToResponseWithFollowing(user *model.User, isFollowing bool) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsFollowing: isFollowing,
IsFollowingMe: false, // 默认false需要单独计算
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToResponseWithPostsCount 将User转换为UserResponse使用实时计算的帖子数量
func ConvertUserToResponseWithPostsCount(user *model.User, postsCount int) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToResponseWithMutualFollow 将User转换为UserResponse包含双向关注状态
func ConvertUserToResponseWithMutualFollow(user *model.User, isFollowing, isFollowingMe bool) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsFollowing: isFollowing,
IsFollowingMe: isFollowingMe,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToResponseWithMutualFollowAndPostsCount 将User转换为UserResponse包含双向关注状态和实时计算的帖子数量
func ConvertUserToResponseWithMutualFollowAndPostsCount(user *model.User, isFollowing, isFollowingMe bool, postsCount int) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
Phone: user.Phone,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsFollowing: isFollowing,
IsFollowingMe: isFollowingMe,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToDetailResponse 将User转换为UserDetailResponse
func ConvertUserToDetailResponse(user *model.User) *UserDetailResponse {
if user == nil {
return nil
}
return &UserDetailResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
EmailVerified: user.EmailVerified,
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: user.PostsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUserToDetailResponseWithPostsCount 将User转换为UserDetailResponse使用实时计算的帖子数量
func ConvertUserToDetailResponseWithPostsCount(user *model.User, postsCount int) *UserDetailResponse {
if user == nil {
return nil
}
return &UserDetailResponse{
ID: user.ID,
Username: user.Username,
Nickname: user.Nickname,
Email: user.Email,
EmailVerified: user.EmailVerified,
Phone: user.Phone, // 仅当前用户自己可见
Avatar: getAvatarOrDefault(user),
CoverURL: user.CoverURL,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
PostsCount: postsCount,
FollowersCount: user.FollowersCount,
FollowingCount: user.FollowingCount,
IsVerified: user.IsVerified,
CreatedAt: FormatTime(user.CreatedAt),
}
}
// ConvertUsersToResponse 将User列表转换为响应列表
func ConvertUsersToResponse(users []*model.User) []*UserResponse {
result := make([]*UserResponse, 0, len(users))
for _, user := range users {
result = append(result, ConvertUserToResponse(user))
}
return result
}
// ConvertUsersToResponseWithMutualFollow 将User列表转换为响应列表包含双向关注状态
// followingStatusMap: key是用户IDvalue是[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是用户IDvalue是[isFollowing, isFollowingMe]
// postsCountMap: key是用户IDvalue是帖子数量
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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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是用户IDvalue是[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是用户IDvalue是[isFollowing, isFollowingMe]
// postsCountMap: key是用户IDvalue是帖子数量
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
}

View 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
}

View File

@@ -11,21 +11,24 @@ import (
"carrot_bbs/internal/pkg/gorse" "carrot_bbs/internal/pkg/gorse"
"carrot_bbs/internal/pkg/response" "carrot_bbs/internal/pkg/response"
gorseio "github.com/gorse-io/gorse-go"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
gorseio "github.com/gorse-io/gorse-go"
"gorm.io/gorm"
) )
// GorseHandler Gorse推荐处理器 // GorseHandler Gorse推荐处理器
type GorseHandler struct { type GorseHandler struct {
importPassword string importPassword string
gorseConfig config.GorseConfig gorseConfig config.GorseConfig
db *gorm.DB
} }
// NewGorseHandler 创建Gorse处理器 // NewGorseHandler 创建Gorse处理器
func NewGorseHandler(cfg config.GorseConfig) *GorseHandler { func NewGorseHandler(cfg config.GorseConfig, db *gorm.DB) *GorseHandler {
return &GorseHandler{ return &GorseHandler{
importPassword: cfg.ImportPassword, importPassword: cfg.ImportPassword,
gorseConfig: cfg, gorseConfig: cfg,
db: db,
} }
} }
@@ -98,7 +101,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
// 导入帖子 // 导入帖子
var posts []model.Post var posts []model.Post
if err := model.DB.Find(&posts).Error; err != nil { if err := h.db.Find(&posts).Error; err != nil {
return nil, err return nil, err
} }
for _, post := range posts { for _, post := range posts {
@@ -126,7 +129,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
// 导入用户 // 导入用户
var users []model.User var users []model.User
if err := model.DB.Find(&users).Error; err != nil { if err := h.db.Find(&users).Error; err != nil {
return nil, err return nil, err
} }
for _, user := range users { for _, user := range users {
@@ -148,7 +151,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
// 导入点赞 // 导入点赞
var likes []model.PostLike var likes []model.PostLike
if err := model.DB.Find(&likes).Error; err != nil { if err := h.db.Find(&likes).Error; err != nil {
return nil, err return nil, err
} }
for _, like := range likes { for _, like := range likes {
@@ -167,7 +170,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
// 导入收藏 // 导入收藏
var favorites []model.Favorite var favorites []model.Favorite
if err := model.DB.Find(&favorites).Error; err != nil { if err := h.db.Find(&favorites).Error; err != nil {
return nil, err return nil, err
} }
for _, fav := range favorites { for _, fav := range favorites {
@@ -186,7 +189,7 @@ func (h *GorseHandler) importAllData(ctx context.Context) (gin.H, error) {
// 导入评论(按用户-帖子去重) // 导入评论(按用户-帖子去重)
var comments []model.Comment var comments []model.Comment
if err := model.DB.Where("status = ?", model.CommentStatusPublished).Find(&comments).Error; err != nil { if err := h.db.Where("status = ?", model.CommentStatusPublished).Find(&comments).Error; err != nil {
return nil, err return nil, err
} }
seen := make(map[string]struct{}) seen := make(map[string]struct{})
@@ -231,4 +234,4 @@ func buildPostCategories(post *model.Post) []string {
categories = append(categories, "this_week") categories = append(categories, "this_week")
} }
return categories return categories
} }

View File

@@ -15,11 +15,11 @@ import (
// GroupHandler 群组处理器 // GroupHandler 群组处理器
type GroupHandler struct { type GroupHandler struct {
groupService service.GroupService groupService service.GroupService
userService *service.UserService userService service.UserService
} }
// NewGroupHandler 创建群组处理器 // NewGroupHandler 创建群组处理器
func NewGroupHandler(groupService service.GroupService, userService *service.UserService) *GroupHandler { func NewGroupHandler(groupService service.GroupService, userService service.UserService) *GroupHandler {
return &GroupHandler{ return &GroupHandler{
groupService: groupService, groupService: groupService,
userService: userService, userService: userService,

View File

@@ -20,13 +20,13 @@ import (
type MessageHandler struct { type MessageHandler struct {
chatService service.ChatService chatService service.ChatService
messageService *service.MessageService messageService *service.MessageService
userService *service.UserService userService service.UserService
groupService service.GroupService groupService service.GroupService
sseHub *sse.Hub sseHub *sse.Hub
} }
// NewMessageHandler 创建消息处理器 // NewMessageHandler 创建消息处理器
func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService *service.UserService, groupService service.GroupService, sseHub *sse.Hub) *MessageHandler { func NewMessageHandler(chatService service.ChatService, messageService *service.MessageService, userService service.UserService, groupService service.GroupService, sseHub *sse.Hub) *MessageHandler {
return &MessageHandler{ return &MessageHandler{
chatService: chatService, chatService: chatService,
messageService: messageService, messageService: messageService,

View File

@@ -15,12 +15,12 @@ import (
// PostHandler 帖子处理器 // PostHandler 帖子处理器
type PostHandler struct { type PostHandler struct {
postService *service.PostService postService service.PostService
userService *service.UserService userService service.UserService
} }
// NewPostHandler 创建帖子处理器 // NewPostHandler 创建帖子处理器
func NewPostHandler(postService *service.PostService, userService *service.UserService) *PostHandler { func NewPostHandler(postService service.PostService, userService service.UserService) *PostHandler {
return &PostHandler{ return &PostHandler{
postService: postService, postService: postService,
userService: userService, userService: userService,

View File

@@ -4,6 +4,7 @@ import (
"strconv" "strconv"
"carrot_bbs/internal/cache" "carrot_bbs/internal/cache"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"carrot_bbs/internal/dto" "carrot_bbs/internal/dto"
@@ -16,16 +17,19 @@ import (
type SystemMessageHandler struct { type SystemMessageHandler struct {
systemMsgService service.SystemMessageService systemMsgService service.SystemMessageService
notifyRepo *repository.SystemNotificationRepository notifyRepo *repository.SystemNotificationRepository
cache cache.Cache
} }
// NewSystemMessageHandler 创建系统消息处理器 // NewSystemMessageHandler 创建系统消息处理器
func NewSystemMessageHandler( func NewSystemMessageHandler(
systemMsgService service.SystemMessageService, systemMsgService service.SystemMessageService,
notifyRepo *repository.SystemNotificationRepository, notifyRepo *repository.SystemNotificationRepository,
cacheBackend cache.Cache,
) *SystemMessageHandler { ) *SystemMessageHandler {
return &SystemMessageHandler{ return &SystemMessageHandler{
systemMsgService: systemMsgService, systemMsgService: systemMsgService,
notifyRepo: notifyRepo, notifyRepo: notifyRepo,
cache: cacheBackend,
} }
} }
@@ -101,7 +105,7 @@ func (h *SystemMessageHandler) MarkAsRead(c *gin.Context) {
response.InternalServerError(c, "failed to mark as read") response.InternalServerError(c, "failed to mark as read")
return return
} }
cache.InvalidateUnreadSystem(cache.GetCache(), userID) cache.InvalidateUnreadSystem(h.cache, userID)
response.SuccessWithMessage(c, "marked as read", nil) response.SuccessWithMessage(c, "marked as read", nil)
} }
@@ -121,7 +125,7 @@ func (h *SystemMessageHandler) MarkAllAsRead(c *gin.Context) {
response.InternalServerError(c, "failed to mark all as read") response.InternalServerError(c, "failed to mark all as read")
return return
} }
cache.InvalidateUnreadSystem(cache.GetCache(), userID) cache.InvalidateUnreadSystem(h.cache, userID)
response.SuccessWithMessage(c, "all messages marked as read", nil) response.SuccessWithMessage(c, "all messages marked as read", nil)
} }
@@ -148,7 +152,7 @@ func (h *SystemMessageHandler) DeleteSystemMessage(c *gin.Context) {
response.InternalServerError(c, "failed to delete notification") response.InternalServerError(c, "failed to delete notification")
return return
} }
cache.InvalidateUnreadSystem(cache.GetCache(), userID) cache.InvalidateUnreadSystem(h.cache, userID)
response.SuccessWithMessage(c, "notification deleted", nil) response.SuccessWithMessage(c, "notification deleted", nil)
} }

View File

@@ -12,12 +12,12 @@ import (
// UserHandler 用户处理器 // UserHandler 用户处理器
type UserHandler struct { type UserHandler struct {
userService *service.UserService userService service.UserService
jwtService *service.JWTService jwtService *service.JWTService
} }
// NewUserHandler 创建用户处理器 // NewUserHandler 创建用户处理器
func NewUserHandler(userService *service.UserService) *UserHandler { func NewUserHandler(userService service.UserService) *UserHandler {
return &UserHandler{ return &UserHandler{
userService: userService, userService: userService,
} }

View File

@@ -14,11 +14,11 @@ import (
// VoteHandler 投票处理器 // VoteHandler 投票处理器
type VoteHandler struct { type VoteHandler struct {
voteService *service.VoteService voteService *service.VoteService
postService *service.PostService postService service.PostService
} }
// NewVoteHandler 创建投票处理器 // NewVoteHandler 创建投票处理器
func NewVoteHandler(voteService *service.VoteService, postService *service.PostService) *VoteHandler { func NewVoteHandler(voteService *service.VoteService, postService service.PostService) *VoteHandler {
return &VoteHandler{ return &VoteHandler{
voteService: voteService, voteService: voteService,
postService: postService, postService: postService,

View File

@@ -14,11 +14,8 @@ import (
"carrot_bbs/internal/config" "carrot_bbs/internal/config"
) )
// DB 全局数据库连接 // NewDB 创建数据库连接(用于 Wire 依赖注入)
var DB *gorm.DB func NewDB(cfg *config.DatabaseConfig) (*gorm.DB, error) {
// InitDB 初始化数据库连接
func InitDB(cfg *config.DatabaseConfig) error {
var err error var err error
var db *gorm.DB var db *gorm.DB
gormLogger := logger.New( gormLogger := logger.New(
@@ -52,28 +49,26 @@ func InitDB(cfg *config.DatabaseConfig) error {
} }
if err != nil { if err != nil {
return fmt.Errorf("failed to connect to database: %w", err) return nil, fmt.Errorf("failed to connect to database: %w", err)
} }
DB = db
// 配置连接池SQLite不支持连接池配置跳过 // 配置连接池SQLite不支持连接池配置跳过
if cfg.Type != "sqlite" { if cfg.Type != "sqlite" {
sqlDB, err := DB.DB() sqlDB, err := db.DB()
if err != nil { if err != nil {
return fmt.Errorf("failed to get database instance: %w", err) return nil, fmt.Errorf("failed to get database instance: %w", err)
} }
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns) sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns) sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
} }
// 自动迁移 // 自动迁移
if err := autoMigrate(DB); err != nil { if err := autoMigrate(db); err != nil {
return fmt.Errorf("failed to auto migrate: %w", err) return nil, fmt.Errorf("failed to auto migrate: %w", err)
} }
log.Printf("Database connected (%s) and migrated successfully", cfg.Type) log.Printf("Database connected (%s) and migrated successfully", cfg.Type)
return nil return db, nil
} }
func parseGormLogLevel(level string) logger.LogLevel { func parseGormLogLevel(level string) logger.LogLevel {
@@ -155,8 +150,8 @@ func autoMigrate(db *gorm.DB) error {
} }
// CloseDB 关闭数据库连接 // CloseDB 关闭数据库连接
func CloseDB() { func CloseDB(db *gorm.DB) {
if sqlDB, err := DB.DB(); err == nil { if sqlDB, err := db.DB(); err == nil {
sqlDB.Close() sqlDB.Close()
} }
} }

View File

@@ -5,7 +5,7 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"carrot_bbs/internal/service" apperrors "carrot_bbs/internal/errors"
) )
// Response 统一响应结构 // Response 统一响应结构
@@ -119,44 +119,72 @@ func Paginated(c *gin.Context, list interface{}, total int64, page, pageSize int
} }
// HandleServiceError 统一处理 Service 错误 // HandleServiceError 统一处理 Service 错误
// 如果 err 是 *service.ServiceError返回对应的业务错误码和消息 // 如果 err 是 *apperrors.AppError返回对应的业务错误码和消息
// 如果 err 是其他错误,返回 false调用方应处理通用错误 // 如果 err 是其他错误,返回 false调用方应处理通用错误
// 返回 true 表示错误已处理false 表示需要调用方继续处理 // 返回 true 表示错误已处理false 表示需要调用方继续处理
func HandleServiceError(c *gin.Context, err error) bool { func HandleServiceError(c *gin.Context, err error) bool {
if err == nil { if err == nil {
return false return false
} }
if se, ok := err.(*service.ServiceError); ok { if se, ok := apperrors.As(err); ok {
ErrorWithStatusCode(c, statusCodeFromCode(se.Code), se.Code, se.Message) statusCode := statusCodeFromAppErrorCode(se.Code)
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
return true return true
} }
return false return false
} }
// HandleError 统一处理错误(带默认消息) // HandleError 统一处理错误(带默认消息)
// 如果 err 是 *service.ServiceError返回对应的业务错误码和消息 // 如果 err 是 *apperrors.AppError返回对应的业务错误码和消息
// 如果 err 是其他错误,返回 InternalServerError 并使用 defaultMessage // 如果 err 是其他错误,返回 InternalServerError 并使用 defaultMessage
func HandleError(c *gin.Context, err error, defaultMessage string) { func HandleError(c *gin.Context, err error, defaultMessage string) {
if err == nil { if err == nil {
return return
} }
if se, ok := err.(*service.ServiceError); ok { if se, ok := apperrors.As(err); ok {
ErrorWithStatusCode(c, statusCodeFromCode(se.Code), se.Code, se.Message) statusCode := statusCodeFromAppErrorCode(se.Code)
ErrorWithStringCode(c, statusCode, se.Code, se.Message)
return return
} }
InternalServerError(c, defaultMessage) InternalServerError(c, defaultMessage)
} }
// statusCodeFromCode 根据业务错误码获取 HTTP 状态码 // statusCodeFromAppErrorCode 根据AppError的错误码字符串获取 HTTP 状态码
func statusCodeFromCode(code int) int { func statusCodeFromAppErrorCode(code string) int {
switch { switch code {
case code >= 200 && code < 300: case "NOT_FOUND", "USER_NOT_FOUND", "POST_NOT_FOUND", "GROUP_NOT_FOUND",
return http.StatusOK "GROUP_REQUEST_NOT_FOUND", "SCHEDULE_COURSE_NOT_FOUND":
case code >= 400 && code < 500: return http.StatusNotFound
return code case "UNAUTHORIZED", "INVALID_CREDENTIALS":
case code >= 500: return http.StatusUnauthorized
return code case "FORBIDDEN", "NOT_GROUP_MEMBER", "NOT_GROUP_ADMIN", "NOT_GROUP_OWNER",
"USER_BANNED", "USER_BLOCKED", "MUTED", "MUTE_ALL_ENABLED",
"SCHEDULE_FORBIDDEN", "UNAUTHORIZED_NOTIFICATION":
return http.StatusForbidden
case "BAD_REQUEST", "INVALID_USERNAME", "INVALID_EMAIL", "INVALID_PHONE",
"WEAK_PASSWORD", "USERNAME_EXISTS", "EMAIL_EXISTS", "PHONE_EXISTS",
"USER_ALREADY_EXISTS", "INVALID_OPERATION", "VERIFICATION_CODE_INVALID",
"VERIFICATION_CODE_EXPIRED", "EMAIL_ALREADY_VERIFIED", "EMAIL_NOT_BOUND",
"GROUP_FULL", "ALREADY_GROUP_MEMBER", "CANNOT_REMOVE_OWNER", "CANNOT_MUTE_OWNER",
"CANNOT_JOIN", "JOIN_REQUEST_PENDING", "GROUP_REQUEST_HANDLED",
"NOT_REQUEST_TARGET", "NO_ELIGIBLE_INVITEE", "NOT_MUTUAL_FOLLOW",
"INVITE_NOT_ACCEPTED", "STICKER_ALREADY_EXISTS", "INVALID_STICKER_URL",
"INVALID_SCHEDULE_PAYLOAD", "SCHEDULE_COLOR_DUPLICATED":
return http.StatusBadRequest
case "VERIFICATION_CODE_TOO_FREQUENT":
return http.StatusTooManyRequests
case "EMAIL_SERVICE_UNAVAILABLE", "VERIFICATION_CODE_UNAVAILABLE", "INTERNAL_ERROR":
return http.StatusInternalServerError
default: default:
return http.StatusBadRequest return http.StatusBadRequest
} }
} }
// ErrorWithStringCode 带字符串错误码的错误响应
func ErrorWithStringCode(c *gin.Context, statusCode int, code string, message string) {
c.JSON(statusCode, ResponseSnakeCase{
Code: 0, // 为了兼容这里设为0实际错误码在message中体现
Message: message,
Data: code,
})
}

View File

@@ -2,6 +2,7 @@ package repository
import ( import (
"carrot_bbs/internal/model" "carrot_bbs/internal/model"
"context"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -17,6 +18,14 @@ func NewPostRepository(db *gorm.DB) *PostRepository {
return &PostRepository{db: db} return &PostRepository{db: db}
} }
// getDB 获取数据库连接(优先使用 context 中的事务)
func (r *PostRepository) getDB(ctx context.Context) *gorm.DB {
if tx := GetTxFromContext(ctx); tx != nil {
return tx
}
return r.db
}
// Create 创建帖子 // Create 创建帖子
func (r *PostRepository) Create(post *model.Post, images []string) error { func (r *PostRepository) Create(post *model.Post, images []string) error {
return r.db.Transaction(func(tx *gorm.DB) error { return r.db.Transaction(func(tx *gorm.DB) error {
@@ -419,3 +428,104 @@ func (r *PostRepository) GetByIDs(ids []string) ([]*model.Post, error) {
return ordered, nil return ordered, nil
} }
// ========== Context-aware methods for transaction support ==========
// CreateWithContext 创建帖子(支持事务)
func (r *PostRepository) CreateWithContext(ctx context.Context, post *model.Post, images []string) error {
db := r.getDB(ctx)
// 如果 context 中有事务,直接使用
if tx := GetTxFromContext(ctx); tx != nil {
return r.createWithTx(tx, post, images)
}
// 否则开启新事务
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return r.createWithTx(tx, post, images)
})
}
// createWithTx 使用指定事务创建帖子
func (r *PostRepository) createWithTx(tx *gorm.DB, post *model.Post, images []string) error {
// 创建帖子
if err := tx.Create(post).Error; err != nil {
return err
}
// 创建图片记录
for i, url := range images {
image := &model.PostImage{
PostID: post.ID,
URL: url,
SortOrder: i,
}
if err := tx.Create(image).Error; err != nil {
return err
}
}
return nil
}
// GetByIDWithContext 根据ID获取帖子支持事务
func (r *PostRepository) GetByIDWithContext(ctx context.Context, id string) (*model.Post, error) {
var post model.Post
err := r.getDB(ctx).WithContext(ctx).Preload("User").Preload("Images").First(&post, "id = ?", id).Error
if err != nil {
return nil, err
}
return &post, nil
}
// UpdateWithContext 更新帖子(支持事务)
func (r *PostRepository) UpdateWithContext(ctx context.Context, post *model.Post) error {
post.UpdatedAt = time.Now()
return r.getDB(ctx).WithContext(ctx).Save(post).Error
}
// DeleteWithContext 删除帖子(支持事务)
func (r *PostRepository) DeleteWithContext(ctx context.Context, id string) error {
db := r.getDB(ctx)
// 如果 context 中有事务,直接使用
if tx := GetTxFromContext(ctx); tx != nil {
return r.deleteWithTx(tx, id)
}
// 否则开启新事务
return db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
return r.deleteWithTx(tx, id)
})
}
// deleteWithTx 使用指定事务删除帖子
func (r *PostRepository) deleteWithTx(tx *gorm.DB, id string) error {
// 删除帖子图片
if err := tx.Where("post_id = ?", id).Delete(&model.PostImage{}).Error; err != nil {
return err
}
// 删除帖子点赞记录
if err := tx.Where("post_id = ?", id).Delete(&model.PostLike{}).Error; err != nil {
return err
}
// 删除帖子收藏记录
if err := tx.Where("post_id = ?", id).Delete(&model.Favorite{}).Error; err != nil {
return err
}
// 删除评论点赞记录子查询获取该帖子所有评论ID
if err := tx.Where("comment_id IN (SELECT id FROM comments WHERE post_id = ?)", id).Delete(&model.CommentLike{}).Error; err != nil {
return err
}
// 删除帖子评论
if err := tx.Where("post_id = ?", id).Delete(&model.Comment{}).Error; err != nil {
return err
}
// 最后删除帖子本身(软删除)
return tx.Delete(&model.Post{}, "id = ?", id).Error
}

View 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
}

View File

@@ -72,6 +72,7 @@ func NewChatService(
userRepo *repository.UserRepository, userRepo *repository.UserRepository,
sensitive SensitiveService, sensitive SensitiveService,
sseHub *sse.Hub, sseHub *sse.Hub,
cacheBackend cache.Cache,
) ChatService { ) ChatService {
// 创建适配器 // 创建适配器
convRepoAdapter := cache.NewConversationRepositoryAdapter(repo) convRepoAdapter := cache.NewConversationRepositoryAdapter(repo)
@@ -79,7 +80,7 @@ func NewChatService(
// 创建会话缓存 // 创建会话缓存
conversationCache := cache.NewConversationCache( conversationCache := cache.NewConversationCache(
cache.GetCache(), cacheBackend,
convRepoAdapter, convRepoAdapter,
msgRepoAdapter, msgRepoAdapter,
cache.DefaultConversationCacheSettings(), cache.DefaultConversationCacheSettings(),

View File

@@ -24,12 +24,12 @@ type CommentService struct {
} }
// NewCommentService 创建评论服务 // NewCommentService 创建评论服务
func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *CommentService { func NewCommentService(commentRepo *repository.CommentRepository, postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache) *CommentService {
return &CommentService{ return &CommentService{
commentRepo: commentRepo, commentRepo: commentRepo,
postRepo: postRepo, postRepo: postRepo,
systemMessageService: systemMessageService, systemMessageService: systemMessageService,
cache: cache.GetCache(), cache: cacheBackend,
gorseClient: gorseClient, gorseClient: gorseClient,
postAIService: postAIService, postAIService: postAIService,
} }

View File

@@ -43,9 +43,6 @@ type emailCodeServiceImpl struct {
} }
func NewEmailCodeService(emailService EmailService, cacheBackend cache.Cache) EmailCodeService { func NewEmailCodeService(emailService EmailService, cacheBackend cache.Cache) EmailCodeService {
if cacheBackend == nil {
cacheBackend = cache.GetCache()
}
return &emailCodeServiceImpl{ return &emailCodeServiceImpl{
emailService: emailService, emailService: emailService,
cache: cacheBackend, cache: cacheBackend,

View File

@@ -8,6 +8,7 @@ import (
"time" "time"
"carrot_bbs/internal/cache" "carrot_bbs/internal/cache"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model" "carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/sse" "carrot_bbs/internal/pkg/sse"
"carrot_bbs/internal/pkg/utils" "carrot_bbs/internal/pkg/utils"
@@ -23,25 +24,26 @@ const (
GroupCacheJitter = 0.1 GroupCacheJitter = 0.1
) )
// 群组服务错误定义 // 群组服务错误定义 - 使用统一的 AppError
var ( var (
ErrGroupNotFound = &ServiceError{Code: 404, Message: "群组不存在"} ErrGroupNotFound = apperrors.ErrGroupNotFound
ErrNotGroupMember = &ServiceError{Code: 403, Message: "不是群成员"} ErrNotGroupMember = apperrors.ErrNotGroupMember
ErrNotGroupAdmin = &ServiceError{Code: 403, Message: "不是群管理员"} ErrNotGroupAdmin = apperrors.ErrNotGroupAdmin
ErrNotGroupOwner = &ServiceError{Code: 403, Message: "不是群主"} ErrNotGroupOwner = apperrors.ErrNotGroupOwner
ErrGroupFull = &ServiceError{Code: 400, Message: "群已满"} ErrGroupFull = apperrors.ErrGroupFull
ErrAlreadyMember = &ServiceError{Code: 400, Message: "已经是群成员"} ErrAlreadyMember = apperrors.ErrAlreadyGroupMember
ErrCannotRemoveOwner = &ServiceError{Code: 400, Message: "不能移除群主"} ErrCannotRemoveOwner = apperrors.ErrCannotRemoveOwner
ErrCannotMuteOwner = &ServiceError{Code: 400, Message: "不能禁言群主"} ErrCannotMuteOwner = apperrors.ErrCannotMuteOwner
ErrMuted = &ServiceError{Code: 403, Message: "你已被禁言"} ErrMuted = apperrors.ErrMuted
ErrMuteAllEnabled = &ServiceError{Code: 403, Message: "全员禁言中"} ErrMuteAllEnabled = apperrors.ErrMuteAllEnabled
ErrCannotJoin = &ServiceError{Code: 400, Message: "该群不允许加入"} ErrCannotJoin = apperrors.ErrCannotJoin
ErrJoinRequestPending = &ServiceError{Code: 400, Message: "加群申请已提交"} ErrJoinRequestPending = apperrors.ErrJoinRequestPending
ErrGroupRequestNotFound = &ServiceError{Code: 404, Message: "加群请求不存在"} ErrGroupRequestNotFound = apperrors.ErrGroupRequestNotFound
ErrGroupRequestHandled = &ServiceError{Code: 400, Message: "该加群请求已处理"} ErrGroupRequestHandled = apperrors.ErrGroupRequestHandled
ErrNotRequestTarget = &ServiceError{Code: 400, Message: "不是邀请目标用户"} ErrNotRequestTarget = apperrors.ErrNotRequestTarget
ErrNoEligibleInvitee = &ServiceError{Code: 400, Message: "没有可邀请的用户"} ErrNoEligibleInvitee = apperrors.ErrNoEligibleInvitee
ErrNotMutualFollow = &ServiceError{Code: 400, Message: "仅支持邀请互相关注用户"} ErrNotMutualFollow = apperrors.ErrNotMutualFollow
ErrInviteNotAccepted = apperrors.ErrInviteNotAccepted
) )
// GroupService 群组服务接口 // GroupService 群组服务接口
@@ -104,7 +106,7 @@ type groupService struct {
} }
// NewGroupService 创建群组服务 // NewGroupService 创建群组服务
func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub) GroupService { func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo *repository.UserRepository, messageRepo *repository.MessageRepository, sseHub *sse.Hub, cacheBackend cache.Cache) GroupService {
return &groupService{ return &groupService{
db: db, db: db,
groupRepo: groupRepo, groupRepo: groupRepo,
@@ -113,7 +115,7 @@ func NewGroupService(db *gorm.DB, groupRepo repository.GroupRepository, userRepo
requestRepo: repository.NewGroupJoinRequestRepository(db), requestRepo: repository.NewGroupJoinRequestRepository(db),
notifyRepo: repository.NewSystemNotificationRepository(db), notifyRepo: repository.NewSystemNotificationRepository(db),
sseHub: sseHub, sseHub: sseHub,
cache: cache.GetCache(), cache: cacheBackend,
} }
} }
@@ -937,7 +939,7 @@ func (s *groupService) SetGroupAddRequest(userID string, flag string, approve bo
if req.RequestType == model.GroupJoinRequestTypeInvite { if req.RequestType == model.GroupJoinRequestTypeInvite {
if req.ReviewerID == "" { if req.ReviewerID == "" {
// 被邀请人还未同意,管理员不能提前审批 // 被邀请人还未同意,管理员不能提前审批
return &ServiceError{Code: 400, Message: "被邀请人尚未同意邀请,无法审批"} return ErrInviteNotAccepted
} }
if req.ReviewerID != req.TargetUserID { if req.ReviewerID != req.TargetUserID {
// ReviewerID 不是被邀请人,说明已经被其他人处理过 // ReviewerID 不是被邀请人,说明已经被其他人处理过

View File

@@ -37,14 +37,14 @@ type MessageService struct {
} }
// NewMessageService 创建消息服务 // NewMessageService 创建消息服务
func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *MessageService { func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository, cacheBackend cache.Cache) *MessageService {
// 创建适配器 // 创建适配器
convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo) convRepoAdapter := cache.NewConversationRepositoryAdapter(messageRepo)
msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo) msgRepoAdapter := cache.NewMessageRepositoryAdapter(messageRepo)
// 创建会话缓存 // 创建会话缓存
conversationCache := cache.NewConversationCache( conversationCache := cache.NewConversationCache(
cache.GetCache(), cacheBackend,
convRepoAdapter, convRepoAdapter,
msgRepoAdapter, msgRepoAdapter,
cache.DefaultConversationCacheSettings(), cache.DefaultConversationCacheSettings(),
@@ -54,7 +54,7 @@ func NewMessageService(db *gorm.DB, messageRepo *repository.MessageRepository) *
db: db, db: db,
messageRepo: messageRepo, messageRepo: messageRepo,
conversationCache: conversationCache, conversationCache: conversationCache,
baseCache: cache.GetCache(), baseCache: cacheBackend,
} }
} }

View File

@@ -5,6 +5,7 @@ import (
"time" "time"
"carrot_bbs/internal/cache" "carrot_bbs/internal/cache"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model" "carrot_bbs/internal/model"
"carrot_bbs/internal/repository" "carrot_bbs/internal/repository"
) )
@@ -23,10 +24,10 @@ type NotificationService struct {
} }
// NewNotificationService 创建通知服务 // NewNotificationService 创建通知服务
func NewNotificationService(notificationRepo *repository.NotificationRepository) *NotificationService { func NewNotificationService(notificationRepo *repository.NotificationRepository, cacheBackend cache.Cache) *NotificationService {
return &NotificationService{ return &NotificationService{
notificationRepo: notificationRepo, notificationRepo: notificationRepo,
cache: cache.GetCache(), cache: cacheBackend,
} }
} }
@@ -166,4 +167,4 @@ func (s *NotificationService) ClearAllNotifications(ctx context.Context, userID
} }
// 错误定义 // 错误定义
var ErrUnauthorizedNotification = &ServiceError{Code: 403, Message: "unauthorized to delete this notification"} var ErrUnauthorizedNotification = apperrors.ErrUnauthorizedNotification

View File

@@ -23,23 +23,63 @@ const (
anonymousViewUserID = "_anon_view" anonymousViewUserID = "_anon_view"
) )
// PostService 帖子服务 // PostService 帖子服务接口
type PostService struct { type PostService interface {
// 帖子CRUD
Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error)
GetByID(ctx context.Context, id string) (*model.Post, error)
Update(ctx context.Context, post *model.Post) error
UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error
Delete(ctx context.Context, id string) error
// 帖子列表
List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error)
GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error)
GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error)
GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error)
// 关注和推荐
GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error)
GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error)
// 交互功能
Like(ctx context.Context, postID, userID string) error
Unlike(ctx context.Context, postID, userID string) error
IsLiked(ctx context.Context, postID, userID string) bool
Favorite(ctx context.Context, postID, userID string) error
Unfavorite(ctx context.Context, postID, userID string) error
IsFavorited(ctx context.Context, postID, userID string) bool
// 交互状态查询
GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error)
GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool)
// 其他
IncrementViews(ctx context.Context, postID, userID string) error
}
// postServiceImpl 帖子服务实现
type postServiceImpl struct {
postRepo *repository.PostRepository postRepo *repository.PostRepository
systemMessageService SystemMessageService systemMessageService SystemMessageService
cache cache.Cache cache cache.Cache
gorseClient gorse.Client gorseClient gorse.Client
postAIService *PostAIService postAIService *PostAIService
txManager repository.TransactionManager // 事务管理器
} }
// NewPostService 创建帖子服务 // NewPostService 创建帖子服务
func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService) *PostService { func NewPostService(postRepo *repository.PostRepository, systemMessageService SystemMessageService, gorseClient gorse.Client, postAIService *PostAIService, cacheBackend cache.Cache, txManager repository.TransactionManager) PostService {
return &PostService{ return &postServiceImpl{
postRepo: postRepo, postRepo: postRepo,
systemMessageService: systemMessageService, systemMessageService: systemMessageService,
cache: cache.GetCache(), cache: cacheBackend,
gorseClient: gorseClient, gorseClient: gorseClient,
postAIService: postAIService, postAIService: postAIService,
txManager: txManager,
} }
} }
@@ -50,7 +90,7 @@ type PostListResult struct {
} }
// Create 创建帖子 // Create 创建帖子
func (s *PostService) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) { func (s *postServiceImpl) Create(ctx context.Context, userID, title, content string, images []string) (*model.Post, error) {
post := &model.Post{ post := &model.Post{
UserID: userID, UserID: userID,
Title: title, Title: title,
@@ -73,7 +113,7 @@ func (s *PostService) Create(ctx context.Context, userID, title, content string,
return s.postRepo.GetByID(post.ID) return s.postRepo.GetByID(post.ID)
} }
func (s *PostService) reviewPostAsync(postID, userID, title, content string, images []string) { func (s *postServiceImpl) reviewPostAsync(postID, userID, title, content string, images []string) {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
log.Printf("[ERROR] Panic in post moderation async flow, fallback publish post=%s panic=%v", postID, r) log.Printf("[ERROR] Panic in post moderation async flow, fallback publish post=%s panic=%v", postID, r)
@@ -139,7 +179,7 @@ func (s *PostService) reviewPostAsync(postID, userID, title, content string, ima
} }
} }
func (s *PostService) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error { func (s *postServiceImpl) updateModerationStatusWithRetry(postID string, status model.PostStatus, rejectReason string, reviewedBy string) error {
const maxAttempts = 3 const maxAttempts = 3
const retryDelay = 200 * time.Millisecond const retryDelay = 200 * time.Millisecond
@@ -159,12 +199,12 @@ func (s *PostService) updateModerationStatusWithRetry(postID string, status mode
return lastErr return lastErr
} }
func (s *PostService) invalidatePostCaches(postID string) { func (s *postServiceImpl) invalidatePostCaches(postID string) {
cache.InvalidatePostDetail(s.cache, postID) cache.InvalidatePostDetail(s.cache, postID)
cache.InvalidatePostList(s.cache) cache.InvalidatePostList(s.cache)
} }
func (s *PostService) notifyModerationRejected(userID, reason string) { func (s *postServiceImpl) notifyModerationRejected(userID, reason string) {
if s.systemMessageService == nil || strings.TrimSpace(userID) == "" { if s.systemMessageService == nil || strings.TrimSpace(userID) == "" {
return return
} }
@@ -187,17 +227,17 @@ func (s *PostService) notifyModerationRejected(userID, reason string) {
} }
// GetByID 根据ID获取帖子 // GetByID 根据ID获取帖子
func (s *PostService) GetByID(ctx context.Context, id string) (*model.Post, error) { func (s *postServiceImpl) GetByID(ctx context.Context, id string) (*model.Post, error) {
return s.postRepo.GetByID(id) return s.postRepo.GetByID(id)
} }
// Update 更新帖子 // Update 更新帖子
func (s *PostService) Update(ctx context.Context, post *model.Post) error { func (s *postServiceImpl) Update(ctx context.Context, post *model.Post) error {
return s.UpdateWithImages(ctx, post, nil) return s.UpdateWithImages(ctx, post, nil)
} }
// UpdateWithImages 更新帖子并可选更新图片images=nil 表示不更新图片) // UpdateWithImages 更新帖子并可选更新图片images=nil 表示不更新图片)
func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error { func (s *postServiceImpl) UpdateWithImages(ctx context.Context, post *model.Post, images *[]string) error {
err := s.postRepo.UpdateWithImages(post, images) err := s.postRepo.UpdateWithImages(post, images)
if err != nil { if err != nil {
return err return err
@@ -211,7 +251,7 @@ func (s *PostService) UpdateWithImages(ctx context.Context, post *model.Post, im
} }
// Delete 删除帖子 // Delete 删除帖子
func (s *PostService) Delete(ctx context.Context, id string) error { func (s *postServiceImpl) Delete(ctx context.Context, id string) error {
err := s.postRepo.Delete(id) err := s.postRepo.Delete(id)
if err != nil { if err != nil {
return err return err
@@ -234,7 +274,7 @@ func (s *PostService) Delete(ctx context.Context, id string) error {
} }
// List 获取帖子列表(带缓存) // List 获取帖子列表(带缓存)
func (s *PostService) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) { func (s *postServiceImpl) List(ctx context.Context, page, pageSize int, userID string, includePending bool) ([]*model.Post, int64, error) {
cacheSettings := cache.GetSettings() cacheSettings := cache.GetSettings()
postListTTL := cacheSettings.PostListTTL postListTTL := cacheSettings.PostListTTL
if postListTTL <= 0 { if postListTTL <= 0 {
@@ -299,22 +339,22 @@ func (s *PostService) List(ctx context.Context, page, pageSize int, userID strin
} }
// GetLatestPosts 获取最新帖子(语义化别名) // GetLatestPosts 获取最新帖子(语义化别名)
func (s *PostService) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) { func (s *postServiceImpl) GetLatestPosts(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
return s.List(ctx, page, pageSize, userID, false) return s.List(ctx, page, pageSize, userID, false)
} }
// GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核) // GetLatestPostsForOwner 获取作者视角帖子列表(包含待审核)
func (s *PostService) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) { func (s *postServiceImpl) GetLatestPostsForOwner(ctx context.Context, page, pageSize int, userID string) ([]*model.Post, int64, error) {
return s.List(ctx, page, pageSize, userID, true) return s.List(ctx, page, pageSize, userID, true)
} }
// GetUserPosts 获取用户帖子 // GetUserPosts 获取用户帖子
func (s *PostService) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) { func (s *postServiceImpl) GetUserPosts(ctx context.Context, userID string, page, pageSize int, includePending bool) ([]*model.Post, int64, error) {
return s.postRepo.GetUserPosts(userID, page, pageSize, includePending) return s.postRepo.GetUserPosts(userID, page, pageSize, includePending)
} }
// Like 点赞 // Like 点赞
func (s *PostService) Like(ctx context.Context, postID, userID string) error { func (s *postServiceImpl) Like(ctx context.Context, postID, userID string) error {
// 获取帖子信息用于发送通知 // 获取帖子信息用于发送通知
post, err := s.postRepo.GetByID(postID) post, err := s.postRepo.GetByID(postID)
if err != nil { if err != nil {
@@ -352,7 +392,7 @@ func (s *PostService) Like(ctx context.Context, postID, userID string) error {
} }
// Unlike 取消点赞 // Unlike 取消点赞
func (s *PostService) Unlike(ctx context.Context, postID, userID string) error { func (s *postServiceImpl) Unlike(ctx context.Context, postID, userID string) error {
err := s.postRepo.Unlike(postID, userID) err := s.postRepo.Unlike(postID, userID)
if err != nil { if err != nil {
return err return err
@@ -374,12 +414,12 @@ func (s *PostService) Unlike(ctx context.Context, postID, userID string) error {
} }
// IsLiked 检查是否点赞 // IsLiked 检查是否点赞
func (s *PostService) IsLiked(ctx context.Context, postID, userID string) bool { func (s *postServiceImpl) IsLiked(ctx context.Context, postID, userID string) bool {
return s.postRepo.IsLiked(postID, userID) return s.postRepo.IsLiked(postID, userID)
} }
// Favorite 收藏 // Favorite 收藏
func (s *PostService) Favorite(ctx context.Context, postID, userID string) error { func (s *postServiceImpl) Favorite(ctx context.Context, postID, userID string) error {
// 获取帖子信息用于发送通知 // 获取帖子信息用于发送通知
post, err := s.postRepo.GetByID(postID) post, err := s.postRepo.GetByID(postID)
if err != nil { if err != nil {
@@ -417,7 +457,7 @@ func (s *PostService) Favorite(ctx context.Context, postID, userID string) error
} }
// Unfavorite 取消收藏 // Unfavorite 取消收藏
func (s *PostService) Unfavorite(ctx context.Context, postID, userID string) error { func (s *postServiceImpl) Unfavorite(ctx context.Context, postID, userID string) error {
err := s.postRepo.Unfavorite(postID, userID) err := s.postRepo.Unfavorite(postID, userID)
if err != nil { if err != nil {
return err return err
@@ -439,12 +479,12 @@ func (s *PostService) Unfavorite(ctx context.Context, postID, userID string) err
} }
// IsFavorited 检查是否收藏 // IsFavorited 检查是否收藏
func (s *PostService) IsFavorited(ctx context.Context, postID, userID string) bool { func (s *postServiceImpl) IsFavorited(ctx context.Context, postID, userID string) bool {
return s.postRepo.IsFavorited(postID, userID) return s.postRepo.IsFavorited(postID, userID)
} }
// GetPostInteractionStatus 批量获取帖子的交互状态(点赞、收藏) // GetPostInteractionStatus 批量获取帖子的交互状态(点赞、收藏)
func (s *PostService) GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error) { func (s *postServiceImpl) GetPostInteractionStatus(ctx context.Context, postIDs []string, userID string) (map[string]bool, map[string]bool, error) {
isLikedMap := make(map[string]bool) isLikedMap := make(map[string]bool)
isFavoritedMap := make(map[string]bool) isFavoritedMap := make(map[string]bool)
@@ -461,7 +501,7 @@ func (s *PostService) GetPostInteractionStatus(ctx context.Context, postIDs []st
} }
// GetPostInteractionStatusSingle 获取单个帖子的交互状态 // GetPostInteractionStatusSingle 获取单个帖子的交互状态
func (s *PostService) GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool) { func (s *postServiceImpl) GetPostInteractionStatusSingle(ctx context.Context, postID, userID string) (isLiked, isFavorited bool) {
if userID == "" { if userID == "" {
return false, false return false, false
} }
@@ -469,7 +509,7 @@ func (s *PostService) GetPostInteractionStatusSingle(ctx context.Context, postID
} }
// IncrementViews 增加帖子观看量并同步到Gorse // IncrementViews 增加帖子观看量并同步到Gorse
func (s *PostService) IncrementViews(ctx context.Context, postID, userID string) error { func (s *postServiceImpl) IncrementViews(ctx context.Context, postID, userID string) error {
if err := s.postRepo.IncrementViews(postID); err != nil { if err := s.postRepo.IncrementViews(postID); err != nil {
return err return err
} }
@@ -494,17 +534,17 @@ func (s *PostService) IncrementViews(ctx context.Context, postID, userID string)
} }
// GetFavorites 获取收藏列表 // GetFavorites 获取收藏列表
func (s *PostService) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) { func (s *postServiceImpl) GetFavorites(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
return s.postRepo.GetFavorites(userID, page, pageSize) return s.postRepo.GetFavorites(userID, page, pageSize)
} }
// Search 搜索帖子 // Search 搜索帖子
func (s *PostService) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) { func (s *postServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.Post, int64, error) {
return s.postRepo.Search(keyword, page, pageSize) return s.postRepo.Search(keyword, page, pageSize)
} }
// GetFollowingPosts 获取关注用户的帖子(带缓存) // GetFollowingPosts 获取关注用户的帖子(带缓存)
func (s *PostService) GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) { func (s *postServiceImpl) GetFollowingPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
cacheSettings := cache.GetSettings() cacheSettings := cache.GetSettings()
postListTTL := cacheSettings.PostListTTL postListTTL := cacheSettings.PostListTTL
if postListTTL <= 0 { if postListTTL <= 0 {
@@ -546,7 +586,7 @@ func (s *PostService) GetFollowingPosts(ctx context.Context, userID string, page
} }
// GetHotPosts 获取热门帖子使用Gorse非个性化推荐 // GetHotPosts 获取热门帖子使用Gorse非个性化推荐
func (s *PostService) GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) { func (s *postServiceImpl) GetHotPosts(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
// 如果Gorse启用使用自定义的非个性化推荐器 // 如果Gorse启用使用自定义的非个性化推荐器
if s.gorseClient.IsEnabled() { if s.gorseClient.IsEnabled() {
offset := (page - 1) * pageSize offset := (page - 1) * pageSize
@@ -580,7 +620,7 @@ func (s *PostService) GetHotPosts(ctx context.Context, page, pageSize int) ([]*m
} }
// getHotPostsFromDB 从数据库获取热门帖子(降级路径) // getHotPostsFromDB 从数据库获取热门帖子(降级路径)
func (s *PostService) getHotPostsFromDB(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) { func (s *postServiceImpl) getHotPostsFromDB(ctx context.Context, page, pageSize int) ([]*model.Post, int64, error) {
// 直接查询数据库不再使用本地缓存Gorse失败降级时使用 // 直接查询数据库不再使用本地缓存Gorse失败降级时使用
posts, total, err := s.postRepo.GetHotPosts(page, pageSize) posts, total, err := s.postRepo.GetHotPosts(page, pageSize)
if err != nil { if err != nil {
@@ -590,7 +630,7 @@ func (s *PostService) getHotPostsFromDB(ctx context.Context, page, pageSize int)
} }
// GetRecommendedPosts 获取推荐帖子 // GetRecommendedPosts 获取推荐帖子
func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) { func (s *postServiceImpl) GetRecommendedPosts(ctx context.Context, userID string, page, pageSize int) ([]*model.Post, int64, error) {
// 如果Gorse未启用或用户未登录降级为热门帖子 // 如果Gorse未启用或用户未登录降级为热门帖子
if !s.gorseClient.IsEnabled() || userID == "" { if !s.gorseClient.IsEnabled() || userID == "" {
return s.GetHotPosts(ctx, page, pageSize) return s.GetHotPosts(ctx, page, pageSize)
@@ -638,7 +678,7 @@ func (s *PostService) GetRecommendedPosts(ctx context.Context, userID string, pa
} }
// buildPostCategories 构建帖子的类别标签 // buildPostCategories 构建帖子的类别标签
func (s *PostService) buildPostCategories(post *model.Post) []string { func (s *postServiceImpl) buildPostCategories(post *model.Post) []string {
var categories []string var categories []string
// 热度标签 // 热度标签
@@ -676,3 +716,17 @@ func (s *PostService) buildPostCategories(post *model.Post) []string {
return categories return categories
} }
// ========== 事务管理器示例方法 ==========
// DeletePostWithTransaction 使用事务管理器删除帖子(示例)
// 此方法展示如何在 Service 层使用事务管理器控制跨多个 Repository 的事务
// 当需要在一个事务中执行多个 Repository 操作时,可以使用此模式
func (s *postServiceImpl) DeletePostWithTransaction(ctx context.Context, postID string) error {
// 使用事务管理器执行事务
return s.txManager.RunInTransaction(ctx, func(ctx context.Context) error {
// 在同一个事务中执行删除操作
// Repository 方法会通过 context 获取事务 TX
return s.postRepo.DeleteWithContext(ctx, postID)
})
}

View File

@@ -8,6 +8,7 @@ import (
"strings" "strings"
"carrot_bbs/internal/dto" "carrot_bbs/internal/dto"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model" "carrot_bbs/internal/model"
"carrot_bbs/internal/repository" "carrot_bbs/internal/repository"
@@ -15,10 +16,10 @@ import (
) )
var ( var (
ErrInvalidSchedulePayload = &ServiceError{Code: 400, Message: "invalid schedule payload"} ErrInvalidSchedulePayload = apperrors.ErrInvalidSchedulePayload
ErrScheduleCourseNotFound = &ServiceError{Code: 404, Message: "schedule course not found"} ErrScheduleCourseNotFound = apperrors.ErrScheduleCourseNotFound
ErrScheduleForbidden = &ServiceError{Code: 403, Message: "forbidden schedule operation"} ErrScheduleForbidden = apperrors.ErrScheduleForbidden
ErrScheduleColorDuplicated = &ServiceError{Code: 400, Message: "course color already used"} ErrScheduleColorDuplicated = apperrors.ErrScheduleColorDuplicated
) )
var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`) var hexColorRegex = regexp.MustCompile(`^#[0-9A-F]{6}$`)

View File

@@ -1,16 +1,18 @@
package service package service
import ( import (
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
"errors" "errors"
"net/url" "net/url"
"strings" "strings"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model"
"carrot_bbs/internal/repository"
) )
var ( var (
ErrStickerAlreadyExists = &ServiceError{Code: 400, Message: "sticker already exists"} ErrStickerAlreadyExists = apperrors.ErrStickerAlreadyExists
ErrInvalidStickerURL = &ServiceError{Code: 400, Message: "invalid sticker url"} ErrInvalidStickerURL = apperrors.ErrInvalidStickerURL
) )
// StickerService 自定义表情服务接口 // StickerService 自定义表情服务接口

View File

@@ -42,13 +42,14 @@ func NewSystemMessageService(
pushService PushService, pushService PushService,
userRepo *repository.UserRepository, userRepo *repository.UserRepository,
postRepo *repository.PostRepository, postRepo *repository.PostRepository,
cacheBackend cache.Cache,
) SystemMessageService { ) SystemMessageService {
return &systemMessageServiceImpl{ return &systemMessageServiceImpl{
notifyRepo: notifyRepo, notifyRepo: notifyRepo,
pushService: pushService, pushService: pushService,
userRepo: userRepo, userRepo: userRepo,
postRepo: postRepo, postRepo: postRepo,
cache: cache.GetCache(), cache: cacheBackend,
} }
} }

View File

@@ -16,6 +16,7 @@ import (
"strings" "strings"
"carrot_bbs/internal/pkg/s3" "carrot_bbs/internal/pkg/s3"
_ "golang.org/x/image/bmp" _ "golang.org/x/image/bmp"
_ "golang.org/x/image/tiff" _ "golang.org/x/image/tiff"
) )
@@ -23,11 +24,11 @@ import (
// UploadService 上传服务 // UploadService 上传服务
type UploadService struct { type UploadService struct {
s3Client *s3.Client s3Client *s3.Client
userService *UserService userService UserService
} }
// NewUploadService 创建上传服务 // NewUploadService 创建上传服务
func NewUploadService(s3Client *s3.Client, userService *UserService) *UploadService { func NewUploadService(s3Client *s3.Client, userService UserService) *UploadService {
return &UploadService{ return &UploadService{
s3Client: s3Client, s3Client: s3Client,
userService: userService, userService: userService,

View File

@@ -6,13 +6,59 @@ import (
"strings" "strings"
"carrot_bbs/internal/cache" "carrot_bbs/internal/cache"
apperrors "carrot_bbs/internal/errors"
"carrot_bbs/internal/model" "carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/utils" "carrot_bbs/internal/pkg/utils"
"carrot_bbs/internal/repository" "carrot_bbs/internal/repository"
) )
// UserService 用户服务 // UserService 用户服务接口
type UserService struct { type UserService interface {
// 验证码相关
SendRegisterCode(ctx context.Context, email string) error
SendPasswordResetCode(ctx context.Context, email string) error
SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error
SendChangePasswordCode(ctx context.Context, userID string) error
// 邮箱验证
VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error
// 用户认证
Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error)
Login(ctx context.Context, account, password string) (*model.User, error)
// 用户查询
GetUserByID(ctx context.Context, id string) (*model.User, error)
GetUserPostCount(ctx context.Context, userID string) (int64, error)
GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error)
GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error)
GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error)
Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error)
CheckUsernameAvailable(ctx context.Context, username string) (bool, error)
// 用户更新
UpdateUser(ctx context.Context, user *model.User) error
ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error
ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error
// 关注相关
GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error)
GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error)
FollowUser(ctx context.Context, followerID, followeeID string) error
UnfollowUser(ctx context.Context, followerID, followeeID string) error
GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error)
// 黑名单相关
BlockUser(ctx context.Context, blockerID, blockedID string) error
UnblockUser(ctx context.Context, blockerID, blockedID string) error
GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error)
IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error)
}
// userServiceImpl 用户服务实现
type userServiceImpl struct {
userRepo *repository.UserRepository userRepo *repository.UserRepository
systemMessageService SystemMessageService systemMessageService SystemMessageService
emailCodeService EmailCodeService emailCodeService EmailCodeService
@@ -24,8 +70,8 @@ func NewUserService(
systemMessageService SystemMessageService, systemMessageService SystemMessageService,
emailService EmailService, emailService EmailService,
cacheBackend cache.Cache, cacheBackend cache.Cache,
) *UserService { ) UserService {
return &UserService{ return &userServiceImpl{
userRepo: userRepo, userRepo: userRepo,
systemMessageService: systemMessageService, systemMessageService: systemMessageService,
emailCodeService: NewEmailCodeService(emailService, cacheBackend), emailCodeService: NewEmailCodeService(emailService, cacheBackend),
@@ -33,7 +79,7 @@ func NewUserService(
} }
// SendRegisterCode 发送注册验证码 // SendRegisterCode 发送注册验证码
func (s *UserService) SendRegisterCode(ctx context.Context, email string) error { func (s *userServiceImpl) SendRegisterCode(ctx context.Context, email string) error {
user, err := s.userRepo.GetByEmail(email) user, err := s.userRepo.GetByEmail(email)
if err == nil && user != nil { if err == nil && user != nil {
return ErrEmailExists return ErrEmailExists
@@ -42,7 +88,7 @@ func (s *UserService) SendRegisterCode(ctx context.Context, email string) error
} }
// SendPasswordResetCode 发送找回密码验证码 // SendPasswordResetCode 发送找回密码验证码
func (s *UserService) SendPasswordResetCode(ctx context.Context, email string) error { func (s *userServiceImpl) SendPasswordResetCode(ctx context.Context, email string) error {
user, err := s.userRepo.GetByEmail(email) user, err := s.userRepo.GetByEmail(email)
if err != nil || user == nil { if err != nil || user == nil {
return ErrUserNotFound return ErrUserNotFound
@@ -51,7 +97,7 @@ func (s *UserService) SendPasswordResetCode(ctx context.Context, email string) e
} }
// SendCurrentUserEmailVerifyCode 发送当前用户邮箱验证验证码 // SendCurrentUserEmailVerifyCode 发送当前用户邮箱验证验证码
func (s *UserService) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error { func (s *userServiceImpl) SendCurrentUserEmailVerifyCode(ctx context.Context, userID, email string) error {
user, err := s.userRepo.GetByID(userID) user, err := s.userRepo.GetByID(userID)
if err != nil || user == nil { if err != nil || user == nil {
return ErrUserNotFound return ErrUserNotFound
@@ -78,7 +124,7 @@ func (s *UserService) SendCurrentUserEmailVerifyCode(ctx context.Context, userID
} }
// VerifyCurrentUserEmail 验证当前用户邮箱 // VerifyCurrentUserEmail 验证当前用户邮箱
func (s *UserService) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error { func (s *userServiceImpl) VerifyCurrentUserEmail(ctx context.Context, userID, email, verificationCode string) error {
user, err := s.userRepo.GetByID(userID) user, err := s.userRepo.GetByID(userID)
if err != nil || user == nil { if err != nil || user == nil {
return ErrUserNotFound return ErrUserNotFound
@@ -107,7 +153,7 @@ func (s *UserService) VerifyCurrentUserEmail(ctx context.Context, userID, email,
} }
// SendChangePasswordCode 发送修改密码验证码 // SendChangePasswordCode 发送修改密码验证码
func (s *UserService) SendChangePasswordCode(ctx context.Context, userID string) error { func (s *userServiceImpl) SendChangePasswordCode(ctx context.Context, userID string) error {
user, err := s.userRepo.GetByID(userID) user, err := s.userRepo.GetByID(userID)
if err != nil || user == nil { if err != nil || user == nil {
return ErrUserNotFound return ErrUserNotFound
@@ -119,7 +165,7 @@ func (s *UserService) SendChangePasswordCode(ctx context.Context, userID string)
} }
// Register 用户注册 // Register 用户注册
func (s *UserService) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) { func (s *userServiceImpl) Register(ctx context.Context, username, email, password, nickname, phone, verificationCode string) (*model.User, error) {
// 验证用户名 // 验证用户名
if !utils.ValidateUsername(username) { if !utils.ValidateUsername(username) {
return nil, ErrInvalidUsername return nil, ErrInvalidUsername
@@ -199,7 +245,7 @@ func (s *UserService) Register(ctx context.Context, username, email, password, n
} }
// Login 用户登录 // Login 用户登录
func (s *UserService) Login(ctx context.Context, account, password string) (*model.User, error) { func (s *userServiceImpl) Login(ctx context.Context, account, password string) (*model.User, error) {
account = strings.TrimSpace(account) account = strings.TrimSpace(account)
var ( var (
user *model.User user *model.User
@@ -228,22 +274,22 @@ func (s *UserService) Login(ctx context.Context, account, password string) (*mod
} }
// GetUserByID 根据ID获取用户 // GetUserByID 根据ID获取用户
func (s *UserService) GetUserByID(ctx context.Context, id string) (*model.User, error) { func (s *userServiceImpl) GetUserByID(ctx context.Context, id string) (*model.User, error) {
return s.userRepo.GetByID(id) return s.userRepo.GetByID(id)
} }
// GetUserPostCount 获取用户帖子数(实时计算) // GetUserPostCount 获取用户帖子数(实时计算)
func (s *UserService) GetUserPostCount(ctx context.Context, userID string) (int64, error) { func (s *userServiceImpl) GetUserPostCount(ctx context.Context, userID string) (int64, error) {
return s.userRepo.GetPostsCount(userID) return s.userRepo.GetPostsCount(userID)
} }
// GetUserPostCountBatch 批量获取用户帖子数(实时计算) // GetUserPostCountBatch 批量获取用户帖子数(实时计算)
func (s *UserService) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) { func (s *userServiceImpl) GetUserPostCountBatch(ctx context.Context, userIDs []string) (map[string]int64, error) {
return s.userRepo.GetPostsCountBatch(userIDs) return s.userRepo.GetPostsCountBatch(userIDs)
} }
// GetUserByIDWithFollowingStatus 根据ID获取用户包含当前用户是否关注的状态 // GetUserByIDWithFollowingStatus 根据ID获取用户包含当前用户是否关注的状态
func (s *UserService) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) { func (s *userServiceImpl) GetUserByIDWithFollowingStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, error) {
user, err := s.userRepo.GetByID(userID) user, err := s.userRepo.GetByID(userID)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
@@ -263,7 +309,7 @@ func (s *UserService) GetUserByIDWithFollowingStatus(ctx context.Context, userID
} }
// GetUserByIDWithMutualFollowStatus 根据ID获取用户包含双向关注状态 // GetUserByIDWithMutualFollowStatus 根据ID获取用户包含双向关注状态
func (s *UserService) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) { func (s *userServiceImpl) GetUserByIDWithMutualFollowStatus(ctx context.Context, userID, currentUserID string) (*model.User, bool, bool, error) {
user, err := s.userRepo.GetByID(userID) user, err := s.userRepo.GetByID(userID)
if err != nil { if err != nil {
return nil, false, false, err return nil, false, false, err
@@ -290,22 +336,22 @@ func (s *UserService) GetUserByIDWithMutualFollowStatus(ctx context.Context, use
} }
// UpdateUser 更新用户 // UpdateUser 更新用户
func (s *UserService) UpdateUser(ctx context.Context, user *model.User) error { func (s *userServiceImpl) UpdateUser(ctx context.Context, user *model.User) error {
return s.userRepo.Update(user) return s.userRepo.Update(user)
} }
// GetFollowers 获取粉丝 // GetFollowers 获取粉丝
func (s *UserService) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) { func (s *userServiceImpl) GetFollowers(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetFollowers(userID, page, pageSize) return s.userRepo.GetFollowers(userID, page, pageSize)
} }
// GetFollowing 获取关注 // GetFollowing 获取关注
func (s *UserService) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) { func (s *userServiceImpl) GetFollowing(ctx context.Context, userID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetFollowing(userID, page, pageSize) return s.userRepo.GetFollowing(userID, page, pageSize)
} }
// FollowUser 关注用户 // FollowUser 关注用户
func (s *UserService) FollowUser(ctx context.Context, followerID, followeeID string) error { func (s *userServiceImpl) FollowUser(ctx context.Context, followerID, followeeID string) error {
blocked, err := s.userRepo.IsBlockedEitherDirection(followerID, followeeID) blocked, err := s.userRepo.IsBlockedEitherDirection(followerID, followeeID)
if err != nil { if err != nil {
return err return err
@@ -357,7 +403,7 @@ func (s *UserService) FollowUser(ctx context.Context, followerID, followeeID str
} }
// UnfollowUser 取消关注用户 // UnfollowUser 取消关注用户
func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID string) error { func (s *userServiceImpl) UnfollowUser(ctx context.Context, followerID, followeeID string) error {
// 检查是否已经关注 // 检查是否已经关注
isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID) isFollowing, err := s.userRepo.IsFollowing(followerID, followeeID)
if err != nil { if err != nil {
@@ -386,7 +432,7 @@ func (s *UserService) UnfollowUser(ctx context.Context, followerID, followeeID s
} }
// BlockUser 拉黑用户,并自动清理双向关注/粉丝关系 // BlockUser 拉黑用户,并自动清理双向关注/粉丝关系
func (s *UserService) BlockUser(ctx context.Context, blockerID, blockedID string) error { func (s *userServiceImpl) BlockUser(ctx context.Context, blockerID, blockedID string) error {
if blockerID == blockedID { if blockerID == blockedID {
return ErrInvalidOperation return ErrInvalidOperation
} }
@@ -394,7 +440,7 @@ func (s *UserService) BlockUser(ctx context.Context, blockerID, blockedID string
} }
// UnblockUser 取消拉黑 // UnblockUser 取消拉黑
func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID string) error { func (s *userServiceImpl) UnblockUser(ctx context.Context, blockerID, blockedID string) error {
if blockerID == blockedID { if blockerID == blockedID {
return ErrInvalidOperation return ErrInvalidOperation
} }
@@ -402,17 +448,17 @@ func (s *UserService) UnblockUser(ctx context.Context, blockerID, blockedID stri
} }
// GetBlockedUsers 获取黑名单列表 // GetBlockedUsers 获取黑名单列表
func (s *UserService) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) { func (s *userServiceImpl) GetBlockedUsers(ctx context.Context, blockerID string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.GetBlockedUsers(blockerID, page, pageSize) return s.userRepo.GetBlockedUsers(blockerID, page, pageSize)
} }
// IsBlocked 检查当前用户是否已拉黑目标用户 // IsBlocked 检查当前用户是否已拉黑目标用户
func (s *UserService) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) { func (s *userServiceImpl) IsBlocked(ctx context.Context, blockerID, blockedID string) (bool, error) {
return s.userRepo.IsBlocked(blockerID, blockedID) return s.userRepo.IsBlocked(blockerID, blockedID)
} }
// GetFollowingList 获取关注列表(字符串参数版本) // GetFollowingList 获取关注列表(字符串参数版本)
func (s *UserService) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) { func (s *userServiceImpl) GetFollowingList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
// 转换字符串参数为整数 // 转换字符串参数为整数
pageInt := 1 pageInt := 1
pageSizeInt := 20 pageSizeInt := 20
@@ -434,7 +480,7 @@ func (s *UserService) GetFollowingList(ctx context.Context, userID, page, pageSi
} }
// GetFollowersList 获取粉丝列表(字符串参数版本) // GetFollowersList 获取粉丝列表(字符串参数版本)
func (s *UserService) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) { func (s *userServiceImpl) GetFollowersList(ctx context.Context, userID, page, pageSize string) ([]*model.User, error) {
// 转换字符串参数为整数 // 转换字符串参数为整数
pageInt := 1 pageInt := 1
pageSizeInt := 20 pageSizeInt := 20
@@ -456,12 +502,12 @@ func (s *UserService) GetFollowersList(ctx context.Context, userID, page, pageSi
} }
// GetMutualFollowStatus 批量获取双向关注状态 // GetMutualFollowStatus 批量获取双向关注状态
func (s *UserService) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) { func (s *userServiceImpl) GetMutualFollowStatus(ctx context.Context, currentUserID string, targetUserIDs []string) (map[string][2]bool, error) {
return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs) return s.userRepo.GetMutualFollowStatus(currentUserID, targetUserIDs)
} }
// CheckUsernameAvailable 检查用户名是否可用 // CheckUsernameAvailable 检查用户名是否可用
func (s *UserService) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) { func (s *userServiceImpl) CheckUsernameAvailable(ctx context.Context, username string) (bool, error) {
user, err := s.userRepo.GetByUsername(username) user, err := s.userRepo.GetByUsername(username)
if err != nil { if err != nil {
return true, nil // 用户不存在,可用 return true, nil // 用户不存在,可用
@@ -470,7 +516,7 @@ func (s *UserService) CheckUsernameAvailable(ctx context.Context, username strin
} }
// ChangePassword 修改密码 // ChangePassword 修改密码
func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error { func (s *userServiceImpl) ChangePassword(ctx context.Context, userID, oldPassword, newPassword, verificationCode string) error {
// 获取用户 // 获取用户
user, err := s.userRepo.GetByID(userID) user, err := s.userRepo.GetByID(userID)
if err != nil { if err != nil {
@@ -500,7 +546,7 @@ func (s *UserService) ChangePassword(ctx context.Context, userID, oldPassword, n
} }
// ResetPasswordByEmail 通过邮箱重置密码 // ResetPasswordByEmail 通过邮箱重置密码
func (s *UserService) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error { func (s *userServiceImpl) ResetPasswordByEmail(ctx context.Context, email, verificationCode, newPassword string) error {
email = strings.TrimSpace(email) email = strings.TrimSpace(email)
if !utils.ValidateEmail(email) { if !utils.ValidateEmail(email) {
return ErrInvalidEmail return ErrInvalidEmail
@@ -526,40 +572,29 @@ func (s *UserService) ResetPasswordByEmail(ctx context.Context, email, verificat
} }
// Search 搜索用户 // Search 搜索用户
func (s *UserService) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) { func (s *userServiceImpl) Search(ctx context.Context, keyword string, page, pageSize int) ([]*model.User, int64, error) {
return s.userRepo.Search(keyword, page, pageSize) return s.userRepo.Search(keyword, page, pageSize)
} }
// 错误定义 // 错误定义 - 使用统一的 AppError
var ( var (
ErrInvalidUsername = &ServiceError{Code: 400, Message: "invalid username"} ErrInvalidUsername = apperrors.ErrInvalidUsername
ErrInvalidEmail = &ServiceError{Code: 400, Message: "invalid email"} ErrInvalidEmail = apperrors.ErrInvalidEmail
ErrInvalidPhone = &ServiceError{Code: 400, Message: "invalid phone number"} ErrInvalidPhone = apperrors.ErrInvalidPhone
ErrWeakPassword = &ServiceError{Code: 400, Message: "password too weak"} ErrWeakPassword = apperrors.ErrWeakPassword
ErrUsernameExists = &ServiceError{Code: 400, Message: "username already exists"} ErrUsernameExists = apperrors.ErrUsernameExists
ErrEmailExists = &ServiceError{Code: 400, Message: "email already exists"} ErrEmailExists = apperrors.ErrEmailExists
ErrPhoneExists = &ServiceError{Code: 400, Message: "phone number already exists"} ErrPhoneExists = apperrors.ErrPhoneExists
ErrUserNotFound = &ServiceError{Code: 404, Message: "user not found"} ErrUserNotFound = apperrors.ErrUserNotFound
ErrUserBanned = &ServiceError{Code: 403, Message: "user is banned"} ErrUserBanned = apperrors.ErrUserBanned
ErrUserBlocked = &ServiceError{Code: 403, Message: "blocked relationship exists"} ErrUserBlocked = apperrors.ErrUserBlocked
ErrInvalidOperation = &ServiceError{Code: 400, Message: "invalid operation"} ErrInvalidOperation = apperrors.ErrInvalidOperation
ErrEmailServiceUnavailable = &ServiceError{Code: 503, Message: "email service unavailable"} ErrEmailServiceUnavailable = apperrors.ErrEmailServiceUnavailable
ErrVerificationCodeTooFrequent = &ServiceError{Code: 429, Message: "verification code sent too frequently"} ErrVerificationCodeTooFrequent = apperrors.ErrVerificationCodeTooFrequent
ErrVerificationCodeInvalid = &ServiceError{Code: 400, Message: "invalid verification code"} ErrVerificationCodeInvalid = apperrors.ErrVerificationCodeInvalid
ErrVerificationCodeExpired = &ServiceError{Code: 400, Message: "verification code expired"} ErrVerificationCodeExpired = apperrors.ErrVerificationCodeExpired
ErrVerificationCodeUnavailable = &ServiceError{Code: 500, Message: "verification code storage unavailable"} ErrVerificationCodeUnavailable = apperrors.ErrVerificationCodeUnavailable
ErrEmailAlreadyVerified = &ServiceError{Code: 400, Message: "email already verified"} ErrEmailAlreadyVerified = apperrors.ErrEmailAlreadyVerified
ErrEmailNotBound = &ServiceError{Code: 400, Message: "email not bound"} ErrEmailNotBound = apperrors.ErrEmailNotBound
ErrInvalidCredentials = apperrors.ErrInvalidCredentials
) )
// ServiceError 服务错误
type ServiceError struct {
Code int
Message string
}
func (e *ServiceError) Error() string {
return e.Message
}
var ErrInvalidCredentials = &ServiceError{Code: 401, Message: "invalid username or password"}

74
internal/wire/handler.go Normal file
View 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)
}

View 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)
}

View File

@@ -0,0 +1,11 @@
package wire
import "github.com/google/wire"
// AllSet 包含所有 Provider Set
var AllSet = wire.NewSet(
InfrastructureSet,
RepositorySet,
ServiceSet,
HandlerSet,
)

View 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
View 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)
}