2 Commits

Author SHA1 Message Date
432b875ba4 皮肤部分拿apifox测过了 2026-01-20 11:50:24 +08:00
116612ffec 修改了皮肤收藏部分 2026-01-13 18:34:21 +08:00
11 changed files with 109 additions and 121 deletions

View File

@@ -1,6 +1,3 @@
# CarrotSkin 环境配置文件示例
# 复制此文件为 .env 并修改相应的配置值
# =============================================================================
# 站点配置
# =============================================================================
@@ -34,10 +31,10 @@ SERVER_SWAGGER_ENABLED=true
# 数据库配置
# =============================================================================
DATABASE_DRIVER=postgres
DATABASE_HOST=localhost
DATABASE_HOST=120.27.110.94
DATABASE_PORT=5432
DATABASE_USERNAME=postgres
DATABASE_PASSWORD=your_password_here
DATABASE_USERNAME=user_wc2MbZ
DATABASE_PASSWORD=password_65b5aN
DATABASE_NAME=carrotskin
DATABASE_SSL_MODE=disable
DATABASE_TIMEZONE=Asia/Shanghai
@@ -49,19 +46,19 @@ DATABASE_CONN_MAX_IDLE_TIME=10m
# =============================================================================
# Redis配置
# =============================================================================
REDIS_HOST=localhost
REDIS_HOST=120.27.110.94
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_PASSWORD=redis_ZXjbN5
REDIS_DATABASE=0
REDIS_POOL_SIZE=10
# =============================================================================
# RustFS对象存储配置 (S3兼容)
# =============================================================================
RUSTFS_ENDPOINT=127.0.0.1:9000
RUSTFS_PUBLIC_URL=http://127.0.0.1:9000
RUSTFS_ACCESS_KEY=your_access_key
RUSTFS_SECRET_KEY=your_secret_key
RUSTFS_ENDPOINT=120.27.110.94:9000
RUSTFS_PUBLIC_URL=http://120.27.110.94:9000
RUSTFS_ACCESS_KEY=ftbulyR6rj0AZ4n5ID7g
RUSTFS_SECRET_KEY=P8q3VZ1wfMEdGJayu4sxh7NRSAB2H0tkFeTQlXLW
RUSTFS_USE_SSL=false
RUSTFS_BUCKET_TEXTURES=carrot-skin-textures
RUSTFS_BUCKET_AVATARS=carrot-skin-avatars
@@ -78,6 +75,18 @@ JWT_EXPIRE_HOURS=168
LOG_LEVEL=info
LOG_FORMAT=json
LOG_OUTPUT=logs/app.log
# 保留的旧配置项
LOG_MAX_SIZE=100
LOG_MAX_BACKUPS=3
LOG_MAX_AGE=28
LOG_COMPRESS=true
# =============================================================================
# 文件上传配置 (保留的旧配置项)
# =============================================================================
UPLOAD_MAX_SIZE=10485760
UPLOAD_TEXTURE_MAX_SIZE=2097152
UPLOAD_AVATAR_MAX_SIZE=1048576
# =============================================================================
# 安全配置
@@ -85,15 +94,17 @@ LOG_OUTPUT=logs/app.log
# CORS 允许的来源,多个用逗号分隔
SECURITY_ALLOWED_ORIGINS=*
# 允许的头像/材质URL域名多个用逗号分隔
SECURITY_ALLOWED_DOMAINS=localhost,127.0.0.1
SECURITY_ALLOWED_DOMAINS=localhost,127.0.0.1,120.27.110.94
# 保留的旧配置项
MAX_LOGIN_ATTEMPTS=5
LOGIN_LOCK_DURATION=30m
# =============================================================================
# 邮件配置
# 腾讯企业邮箱SSL配置示例smtp.exmail.qq.com, 端口465
# =============================================================================
EMAIL_ENABLED=false
EMAIL_SMTP_HOST=smtp.example.com
EMAIL_SMTP_PORT=587
EMAIL_USERNAME=noreply@example.com
EMAIL_PASSWORD=your-email-password
EMAIL_FROM_NAME=CarrotSkin
EMAIL_ENABLED=true
EMAIL_SMTP_HOST=smtp.exmail.qq.com
EMAIL_SMTP_PORT=465
EMAIL_USERNAME=system@qczlit.cn
EMAIL_PASSWORD=545mkewZwMzEWUjD
EMAIL_FROM_NAME=CarrotSkin

2
.gitignore vendored
View File

@@ -60,7 +60,7 @@ configs/config.yaml
.env.production
# Keep example files
!.env.example
!.env
# Database files
*.db

View File

@@ -72,7 +72,7 @@ backend/
3. **配置环境变量**
```bash
cp .env.example .env
cp .env .env
# 根据实际环境填写数据库、Redis、对象存储、邮件等信息
```

View File

@@ -17,6 +17,7 @@ import (
"os/signal"
"syscall"
"time"
_ "time/tzdata"
"carrotskin/internal/container"
"carrotskin/internal/handler"

View File

@@ -56,17 +56,12 @@ type TextureRepository interface {
Delete(ctx context.Context, id int64) error
BatchDelete(ctx context.Context, ids []int64) (int64, error) // 批量删除
IncrementDownloadCount(ctx context.Context, id int64) error
IncrementFavoriteCount(ctx context.Context, id int64) error
DecrementFavoriteCount(ctx context.Context, id int64) error
CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error
IsFavorited(ctx context.Context, userID, textureID int64) (bool, error)
AddFavorite(ctx context.Context, userID, textureID int64) error
RemoveFavorite(ctx context.Context, userID, textureID int64) error
ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error)
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
}
// YggdrasilRepository Yggdrasil仓储接口
type YggdrasilRepository interface {
GetPasswordByID(ctx context.Context, id int64) (string, error)

View File

@@ -98,7 +98,6 @@ func TestProfileRepository_Basic(t *testing.T) {
t.Fatalf("CountByUserID mismatch: %d err=%v", count, err)
}
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
t.Fatalf("UpdateLastUsedAt err: %v", err)
}
@@ -150,22 +149,20 @@ func TestTextureRepository_Basic(t *testing.T) {
t.Fatalf("FindByHashAndUploaderID mismatch")
}
_ = textureRepo.IncrementFavoriteCount(ctx, tex.ID)
_ = textureRepo.DecrementFavoriteCount(ctx, tex.ID)
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID)
favList, _, _ := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10)
if len(favList) == 0 {
t.Fatalf("GetUserFavorites expected at least 1 favorite")
}
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID)
favList, _, _ = textureRepo.GetUserFavorites(ctx, u.ID, 1, 10)
if len(favList) != 0 {
t.Fatalf("GetUserFavorites expected 0 favorites after toggle off")
}
_ = textureRepo.IncrementDownloadCount(ctx, tex.ID)
_ = textureRepo.CreateDownloadLog(ctx, &model.TextureDownloadLog{TextureID: tex.ID, UserID: &u.ID, IPAddress: "127.0.0.1"})
// 收藏
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID)
if fav, err := textureRepo.IsFavorited(ctx, u.ID, tex.ID); err == nil {
if !fav {
t.Fatalf("IsFavorited expected true")
}
} else {
t.Skipf("IsFavorited not supported by sqlite: %v", err)
}
_ = textureRepo.RemoveFavorite(ctx, u.ID, tex.ID)
// 批量更新与删除
if affected, err := textureRepo.BatchUpdate(ctx, []int64{tex.ID}, map[string]interface{}{"name": "tex-new"}); err != nil || affected != 1 {
t.Fatalf("BatchUpdate mismatch, affected=%d err=%v", affected, err)
@@ -187,7 +184,7 @@ func TestTextureRepository_Basic(t *testing.T) {
if list, total, err := textureRepo.Search(ctx, "search", model.TextureTypeCape, true, 1, 10); err != nil || total == 0 || len(list) == 0 {
t.Fatalf("Search mismatch, total=%d len=%d err=%v", total, len(list), err)
}
_ = textureRepo.AddFavorite(ctx, u.ID, tex.ID+1)
_, _ = textureRepo.ToggleFavorite(ctx, u.ID, tex.ID+1)
if favList, total, err := textureRepo.GetUserFavorites(ctx, u.ID, 1, 10); err != nil || total == 0 || len(favList) == 0 {
t.Fatalf("GetUserFavorites mismatch, total=%d len=%d err=%v", total, len(favList), err)
}
@@ -206,7 +203,6 @@ func TestTextureRepository_Basic(t *testing.T) {
_ = textureRepo.Delete(ctx, tex.ID)
}
func TestClientRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)
repo := NewClientRepository(db)

View File

@@ -138,42 +138,52 @@ func (r *textureRepository) IncrementDownloadCount(ctx context.Context, id int64
UpdateColumn("download_count", gorm.Expr("download_count + ?", 1)).Error
}
func (r *textureRepository) IncrementFavoriteCount(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Model(&model.Texture{}).Where("id = ?", id).
UpdateColumn("favorite_count", gorm.Expr("favorite_count + ?", 1)).Error
}
func (r *textureRepository) DecrementFavoriteCount(ctx context.Context, id int64) error {
return r.db.WithContext(ctx).Model(&model.Texture{}).Where("id = ?", id).
UpdateColumn("favorite_count", gorm.Expr("favorite_count - ?", 1)).Error
}
func (r *textureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
return r.db.WithContext(ctx).Create(log).Error
}
func (r *textureRepository) IsFavorited(ctx context.Context, userID, textureID int64) (bool, error) {
var count int64
// 使用 Select("1") 优化,只查询是否存在,不需要查询所有字段
err := r.db.WithContext(ctx).Model(&model.UserTextureFavorite{}).
Select("1").
Where("user_id = ? AND texture_id = ?", userID, textureID).
Limit(1).
Count(&count).Error
return count > 0, err
}
func (r *textureRepository) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
var isAdded bool
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var count int64
err := tx.Model(&model.UserTextureFavorite{}).
Where("user_id = ? AND texture_id = ?", userID, textureID).
Count(&count).Error
if err != nil {
return err
}
func (r *textureRepository) AddFavorite(ctx context.Context, userID, textureID int64) error {
favorite := &model.UserTextureFavorite{
UserID: userID,
TextureID: textureID,
}
return r.db.WithContext(ctx).Create(favorite).Error
}
if count > 0 {
result := tx.Where("user_id = ? AND texture_id = ?", userID, textureID).
Delete(&model.UserTextureFavorite{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected > 0 {
if err := tx.Model(&model.Texture{}).Where("id = ?", textureID).
UpdateColumn("favorite_count", gorm.Expr("GREATEST(favorite_count - 1, 0)")).Error; err != nil {
return err
}
}
isAdded = false
return nil
}
func (r *textureRepository) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
return r.db.WithContext(ctx).Where("user_id = ? AND texture_id = ?", userID, textureID).
Delete(&model.UserTextureFavorite{}).Error
favorite := &model.UserTextureFavorite{
UserID: userID,
TextureID: textureID,
}
if err := tx.Create(favorite).Error; err != nil {
return err
}
if err := tx.Model(&model.Texture{}).Where("id = ?", textureID).
UpdateColumn("favorite_count", gorm.Expr("favorite_count + 1")).Error; err != nil {
return err
}
isAdded = true
return nil
})
return isAdded, err
}
func (r *textureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {

View File

@@ -391,37 +391,24 @@ func (m *MockTextureRepository) IncrementFavoriteCount(ctx context.Context, id i
return nil
}
func (m *MockTextureRepository) DecrementFavoriteCount(ctx context.Context, id int64) error {
if texture, ok := m.textures[id]; ok && texture.FavoriteCount > 0 {
texture.FavoriteCount--
}
return nil
}
func (m *MockTextureRepository) CreateDownloadLog(ctx context.Context, log *model.TextureDownloadLog) error {
return nil
}
func (m *MockTextureRepository) IsFavorited(ctx context.Context, userID, textureID int64) (bool, error) {
if userFavs, ok := m.favorites[userID]; ok {
return userFavs[textureID], nil
}
return false, nil
}
func (m *MockTextureRepository) AddFavorite(ctx context.Context, userID, textureID int64) error {
func (m *MockTextureRepository) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
if m.favorites[userID] == nil {
m.favorites[userID] = make(map[int64]bool)
}
m.favorites[userID][textureID] = true
return nil
}
func (m *MockTextureRepository) RemoveFavorite(ctx context.Context, userID, textureID int64) error {
if userFavs, ok := m.favorites[userID]; ok {
delete(userFavs, textureID)
isFavorited := m.favorites[userID][textureID]
m.favorites[userID][textureID] = !isFavorited
if texture, ok := m.textures[textureID]; ok {
if !isFavorited {
texture.FavoriteCount++
} else if texture.FavoriteCount > 0 {
texture.FavoriteCount--
}
}
return nil
return !isFavorited, nil
}
func (m *MockTextureRepository) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
@@ -474,7 +461,6 @@ func (m *MockTextureRepository) BatchDelete(ctx context.Context, ids []int64) (i
return deleted, nil
}
// ============================================================================
// Service Mocks
// ============================================================================

View File

@@ -219,39 +219,22 @@ func (s *textureService) Delete(ctx context.Context, textureID, uploaderID int64
}
func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID int64) (bool, error) {
// 确保材质存在
texture, err := s.textureRepo.FindByID(ctx, textureID)
if err != nil {
return false, err
}
if texture == nil {
if texture == nil || texture.Status != 1 || !texture.IsPublic {
return false, ErrTextureNotFound
}
isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
isAdded, err := s.textureRepo.ToggleFavorite(ctx, userID, textureID)
if err != nil {
return false, err
}
if isFavorited {
// 已收藏 -> 取消收藏
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return false, nil
}
s.cacheInv.BatchInvalidate(ctx, s.cacheKeys.UserFavoritesPattern(userID))
// 未收藏 -> 添加收藏
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
return false, err
}
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
return false, err
}
return true, nil
return isAdded, nil
}
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {

View File

@@ -3,6 +3,7 @@ package service
import (
"carrotskin/internal/model"
"context"
"strings"
"testing"
"go.uber.org/zap"
@@ -564,7 +565,7 @@ func TestTextureServiceImpl_Create(t *testing.T) {
ctx := context.Background()
// UploadTexture需要文件数据这里创建一个简单的测试数据
fileData := []byte("fake png data for testing")
fileData := []byte(strings.Repeat("x", 512))
texture, err := textureService.UploadTexture(
ctx,
tt.uploaderID,
@@ -760,7 +761,7 @@ func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) {
UploaderID: 1,
Name: "T",
})
_ = textureRepo.AddFavorite(context.Background(), 1, i)
_, _ = textureRepo.ToggleFavorite(context.Background(), 1, i)
}
cacheManager := NewMockCacheManager()

View File

@@ -369,6 +369,11 @@ func (b *CacheKeyBuilder) ProfilePattern(userID int64) string {
return fmt.Sprintf("%sprofile:*:%d*", b.prefix, userID)
}
// UserFavoritesPattern 用户收藏相关的所有缓存键模式
func (b *CacheKeyBuilder) UserFavoritesPattern(userID int64) string {
return fmt.Sprintf("%sfavorites:*:%d*", b.prefix, userID)
}
// Exists 检查缓存键是否存在
func (cm *CacheManager) Exists(ctx context.Context, key string) (bool, error) {
if !cm.config.Enabled || cm.redis == nil {