refactor(affiliate): tighten DI and harden inviter code validation

- Drop SetAffiliateService setters and ProvideAuthService /
  ProvidePaymentService / ProvideUserHandler wrappers in favor of direct
  Wire constructor injection. AffiliateService has no back-edge to
  Auth/Payment/User, so the indirection was never required.
- Change RegisterWithVerification's variadic affiliateCode to a fixed
  parameter; adjust all call sites.
- Validate aff_code length and charset in BindInviterByCode before any
  DB lookup, eliminating timing-side-channel and useless DB roundtrips
  on malformed input.
- Make affiliate cache invalidation synchronous; surface Redis errors
  via the project logger instead of swallowing them in a detached
  goroutine.
- Add an integration test guarding cross-layer tx propagation in
  AccrueQuota and a unit test pinning the aff_code format rules.
This commit is contained in:
shaw
2026-04-25 08:44:18 +08:00
parent 5b5db88550
commit aa8ee33b0a
22 changed files with 188 additions and 157 deletions

View File

@@ -5,7 +5,6 @@ import (
"strings"
"github.com/Wei-Shaw/sub2api/internal/handler/dto"
infraerrors "github.com/Wei-Shaw/sub2api/internal/pkg/errors"
"github.com/Wei-Shaw/sub2api/internal/pkg/response"
middleware2 "github.com/Wei-Shaw/sub2api/internal/server/middleware"
"github.com/Wei-Shaw/sub2api/internal/service"
@@ -28,22 +27,17 @@ func NewUserHandler(
authService *service.AuthService,
emailService *service.EmailService,
emailCache service.EmailCache,
affiliateService *service.AffiliateService,
) *UserHandler {
return &UserHandler{
userService: userService,
authService: authService,
emailService: emailService,
emailCache: emailCache,
userService: userService,
authService: authService,
emailService: emailService,
emailCache: emailCache,
affiliateService: affiliateService,
}
}
func (h *UserHandler) SetAffiliateService(affiliateService *service.AffiliateService) {
if h == nil {
return
}
h.affiliateService = affiliateService
}
// ChangePasswordRequest represents the change password request payload
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
@@ -168,13 +162,6 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
response.Success(c, profileResp)
}
func (h *UserHandler) affiliateServiceOrErr() (*service.AffiliateService, error) {
if h == nil || h.affiliateService == nil {
return nil, infraerrors.ServiceUnavailable("SERVICE_UNAVAILABLE", "affiliate service unavailable")
}
return h.affiliateService, nil
}
// GetAffiliate returns the current user's affiliate details.
// GET /api/v1/user/aff
func (h *UserHandler) GetAffiliate(c *gin.Context) {
@@ -184,13 +171,7 @@ func (h *UserHandler) GetAffiliate(c *gin.Context) {
return
}
affiliateSvc, err := h.affiliateServiceOrErr()
if err != nil {
response.ErrorFrom(c, err)
return
}
detail, err := affiliateSvc.GetAffiliateDetail(c.Request.Context(), subject.UserID)
detail, err := h.affiliateService.GetAffiliateDetail(c.Request.Context(), subject.UserID)
if err != nil {
response.ErrorFrom(c, err)
return
@@ -207,13 +188,7 @@ func (h *UserHandler) TransferAffiliateQuota(c *gin.Context) {
return
}
affiliateSvc, err := h.affiliateServiceOrErr()
if err != nil {
response.ErrorFrom(c, err)
return
}
transferred, balance, err := affiliateSvc.TransferAffiliateQuota(c.Request.Context(), subject.UserID)
transferred, balance, err := h.affiliateService.TransferAffiliateQuota(c.Request.Context(), subject.UserID)
if err != nil {
response.ErrorFrom(c, err)
return