From 9f4d4e5adfe8d9681ccf8e7b02de51666ebe1d41 Mon Sep 17 00:00:00 2001 From: long Date: Sat, 10 Jan 2026 13:23:03 +0800 Subject: [PATCH] =?UTF-8?q?=20=20feat:=20=E5=AE=9E=E7=8E=B0=E6=B3=A8?= =?UTF-8?q?=E5=86=8C=E4=BC=98=E6=83=A0=E7=A0=81=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 支持创建/编辑/删除优惠码,设置赠送金额和使用限制 - 注册页面实时验证优惠码并显示赠送金额 - 支持 URL 参数自动填充 (?promo=CODE) - 添加优惠码验证接口速率限制 - 使用数据库行锁防止并发超限 - 新增后台优惠码管理页面,支持复制注册链接 --- backend/internal/handler/admin/promo_handler.go | 10 +++++----- backend/internal/server/api_contract_test.go | 2 +- backend/internal/service/auth_service_register_test.go | 7 ++++--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/backend/internal/handler/admin/promo_handler.go b/backend/internal/handler/admin/promo_handler.go index 14914a81..3eafa380 100644 --- a/backend/internal/handler/admin/promo_handler.go +++ b/backend/internal/handler/admin/promo_handler.go @@ -27,11 +27,11 @@ func NewPromoHandler(promoService *service.PromoService) *PromoHandler { // CreatePromoCodeRequest represents create promo code request type CreatePromoCodeRequest struct { - Code string `json:"code"` // 可选,为空则自动生成 - BonusAmount float64 `json:"bonus_amount" binding:"required,min=0"` // 赠送余额 - MaxUses int `json:"max_uses" binding:"min=0"` // 最大使用次数,0=无限 - ExpiresAt *int64 `json:"expires_at"` // 过期时间戳(秒) - Notes string `json:"notes"` // 备注 + Code string `json:"code"` // 可选,为空则自动生成 + BonusAmount float64 `json:"bonus_amount" binding:"required,min=0"` // 赠送余额 + MaxUses int `json:"max_uses" binding:"min=0"` // 最大使用次数,0=无限 + ExpiresAt *int64 `json:"expires_at"` // 过期时间戳(秒) + Notes string `json:"notes"` // 备注 } // UpdatePromoCodeRequest represents update promo code request diff --git a/backend/internal/server/api_contract_test.go b/backend/internal/server/api_contract_test.go index 6e52c5bc..d12c9ece 100644 --- a/backend/internal/server/api_contract_test.go +++ b/backend/internal/server/api_contract_test.go @@ -398,7 +398,7 @@ func newContractDeps(t *testing.T) *contractDeps { settingRepo := newStubSettingRepo() settingService := service.NewSettingService(settingRepo, cfg) - authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService) + authHandler := handler.NewAuthHandler(cfg, nil, userService, settingService, nil) apiKeyHandler := handler.NewAPIKeyHandler(apiKeyService) usageHandler := handler.NewUsageHandler(usageService, apiKeyService) adminSettingHandler := adminhandler.NewSettingHandler(settingService, nil, nil) diff --git a/backend/internal/service/auth_service_register_test.go b/backend/internal/service/auth_service_register_test.go index ab1f20a0..bc8f6f68 100644 --- a/backend/internal/service/auth_service_register_test.go +++ b/backend/internal/service/auth_service_register_test.go @@ -100,6 +100,7 @@ func newAuthService(repo *userRepoStub, settings map[string]string, emailCache E emailService, nil, nil, + nil, // promoService ) } @@ -131,7 +132,7 @@ func TestAuthService_Register_EmailVerifyEnabledButServiceNotConfigured(t *testi }, nil) // 应返回服务不可用错误,而不是允许绕过验证 - _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "any-code") + _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "any-code", "") require.ErrorIs(t, err, ErrServiceUnavailable) } @@ -143,7 +144,7 @@ func TestAuthService_Register_EmailVerifyRequired(t *testing.T) { SettingKeyEmailVerifyEnabled: "true", }, cache) - _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "") + _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "", "") require.ErrorIs(t, err, ErrEmailVerifyRequired) } @@ -157,7 +158,7 @@ func TestAuthService_Register_EmailVerifyInvalid(t *testing.T) { SettingKeyEmailVerifyEnabled: "true", }, cache) - _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "wrong") + _, _, err := service.RegisterWithVerification(context.Background(), "user@test.com", "password", "wrong", "") require.ErrorIs(t, err, ErrInvalidVerifyCode) require.ErrorContains(t, err, "verify code") }