refactor(api): extract repository layer and thin HTTP handlers

Introduce internal/repository for data access and cache-backed system
config reads. Move business logic into logics.go across admin push,
user, template, cache, system_config, and upload/handler packages.

Remove Gin from internal/util by relocating request-scoped helpers to
oauth/gin_context.go. Propagate request context for config lookups in
user flows. Slim model entities and delete model-level DB/cache helpers.

Wire handlers to logics/repository so targeted packages no longer call
db.DB directly. Update admin router tests to use ErrorHandlerMiddleware.
This commit is contained in:
ryan
2026-06-18 12:12:49 +08:00
parent e5b3a60f73
commit 1b2e083aec
77 changed files with 2370 additions and 1783 deletions
+10 -8
View File
@@ -5,17 +5,19 @@
// Package user 提供用户认证与帐户管理功能
package user
import ("net/http"
import (
"net/http"
"strconv"
"strings"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
type createTokenRequest struct {
Name string `json:"name"`
@@ -38,7 +40,7 @@ type tokenResponse struct {
// @Router /api/v1/user/access-tokens [get]
// ListAccessTokens 获取当前用户的 AccessToken 列表
func ListAccessTokens(c *gin.Context) {
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
ctx := c.Request.Context()
var tokens []model.AccessToken
@@ -62,7 +64,7 @@ func ListAccessTokens(c *gin.Context) {
// @Failure 400 {object} response.Any "参数错误或超限"
// @Router /api/v1/user/access-tokens [post]
func CreateAccessToken(c *gin.Context) {
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
ctx := c.Request.Context()
var req createTokenRequest
@@ -85,7 +87,7 @@ func CreateAccessToken(c *gin.Context) {
// 检查最大限制(基于 ConfigKeyMaxAPIKeysPerUser 配置,默认值为 5)
maxLimit := 5
if val, err := model.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
if val, err := repository.GetIntByKey(ctx, model.ConfigKeyMaxAPIKeysPerUser); err == nil {
maxLimit = val
}
@@ -140,7 +142,7 @@ func CreateAccessToken(c *gin.Context) {
// @Failure 400 {object} response.Any "参数错误"
// @Router /api/v1/user/access-tokens/{id} [delete]
func DeleteAccessToken(c *gin.Context) {
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
ctx := c.Request.Context()
idStr := c.Param("id")
@@ -175,7 +177,7 @@ func DeleteAccessToken(c *gin.Context) {
// @Failure 400 {object} response.Any "参数错误"
// @Router /api/v1/user/access-tokens/{id}/rotate [post]
func RotateAccessToken(c *gin.Context) {
currUser, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
currUser, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
ctx := c.Request.Context()
idStr := c.Param("id")
+40 -27
View File
@@ -14,6 +14,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/task"
pkgu "github.com/Rain-kl/Wavelet/pkg/util"
)
@@ -47,8 +48,32 @@ type updateProfileInput struct {
Location string
}
func isPasswordLoginEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordLoginEnabled)
if err != nil {
return true
}
return enabled
}
func isPasswordRegisterEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyPasswordRegisterEnabled)
if err != nil {
return true
}
return enabled
}
func isRegistrationEnabled(ctx context.Context) bool {
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyRegistrationEnabled)
if err != nil {
return true
}
return enabled
}
func isEmailLoginVerificationEnabled(ctx context.Context) bool {
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled)
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailLoginVerificationEnabled)
if err != nil {
return false
}
@@ -56,7 +81,7 @@ func isEmailLoginVerificationEnabled(ctx context.Context) bool {
}
func isEmailRegisterVerificationEnabled(ctx context.Context) bool {
enabled, err := model.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled)
enabled, err := repository.GetBoolByKey(ctx, model.ConfigKeyEmailRegisterVerificationEnabled)
if err != nil {
return false
}
@@ -64,26 +89,14 @@ func isEmailRegisterVerificationEnabled(ctx context.Context) bool {
}
func isSMTPConfigured(ctx context.Context) bool {
var host, port, username, password string
var scHost model.SystemConfig
if err := scHost.GetByKey(ctx, model.ConfigKeySMTPHost); err == nil {
host = scHost.Value
scHost, errHost := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPHost)
scPort, errPort := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPort)
scUser, errUser := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPUsername)
scPass, errPass := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPassword)
if errHost != nil || errPort != nil || errUser != nil || errPass != nil {
return false
}
var scPort model.SystemConfig
if err := scPort.GetByKey(ctx, model.ConfigKeySMTPPort); err == nil {
port = scPort.Value
}
var scUser model.SystemConfig
if err := scUser.GetByKey(ctx, model.ConfigKeySMTPUsername); err == nil {
username = scUser.Value
}
var scPass model.SystemConfig
if err := scPass.GetByKey(ctx, model.ConfigKeySMTPPassword); err == nil {
password = scPass.Value
}
return host != "" && port != "" && username != "" && password != ""
return scHost.Value != "" && scPort.Value != "" && scUser.Value != "" && scPass.Value != ""
}
func generateVerificationCode() (string, error) {
@@ -114,11 +127,11 @@ func sendEmailVerificationCode(ctx context.Context, email, scene, templateName s
codeKey := getEmailCodeKey(scene, email)
cooldownKey := getEmailCooldownKey(scene, email)
emailSubject, emailBody, err := model.RenderTemplate(
ctx,
templateName,
map[string]any{"Code": code},
)
tmpl, err := repository.GetTemplateByKey(ctx, templateName)
if err != nil {
return fmt.Errorf("模板 %s 不存在或不可用: %w", templateName, err)
}
emailSubject, emailBody, err := tmpl.Render(map[string]any{"Code": code})
if err != nil {
return fmt.Errorf(errRenderEmailTemplateFailed, err)
}
@@ -271,4 +284,4 @@ func updateUserProfile(ctx context.Context, userID uint64, input updateProfileIn
return nil, err
}
return &dbUser, nil
}
}
+4 -3
View File
@@ -10,6 +10,7 @@ import (
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/testhelper"
)
@@ -39,7 +40,7 @@ func TestProcessLoginEmailVerificationSMTPFallback(t *testing.T) {
Update("value", "").Error; err != nil {
t.Fatalf("clear SMTP host failed: %v", err)
}
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
t.Fatalf("invalidate system config cache failed: %v", err)
}
@@ -104,7 +105,7 @@ func TestProcessLoginEmailVerificationEmptyEmailFallback(t *testing.T) {
t.Fatalf("set %s failed: %v", cfg.key, err)
}
}
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
t.Fatalf("invalidate system config cache failed: %v", err)
}
@@ -148,4 +149,4 @@ func TestProcessLoginEmailVerificationInvalidCode(t *testing.T) {
if result.Status != LoginEmailVerificationRejected || result.Message != errEmailCodeInvalidOrExpired {
t.Fatalf("processLoginEmailVerification() = %+v, want rejected invalid code", result)
}
}
}
+11 -35
View File
@@ -3,23 +3,24 @@
package user
import ("context"
import (
"context"
"net/http"
"strings"
"time"
"github.com/Rain-kl/Wavelet/internal/apps/oauth"
"github.com/Rain-kl/Wavelet/internal/common"
"github.com/Rain-kl/Wavelet/internal/common/response"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/db/idgen"
"github.com/Rain-kl/Wavelet/internal/listener"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/util"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/pkg/logger"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response"
)
type loginRequest struct {
@@ -53,30 +54,6 @@ type updateProfileRequest struct {
Location string `json:"location"`
}
func isPasswordLoginEnabled() bool {
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyPasswordLoginEnabled)
if err != nil {
return true
}
return enabled
}
func isPasswordRegisterEnabled() bool {
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyPasswordRegisterEnabled)
if err != nil {
return true
}
return enabled
}
func isRegistrationEnabled() bool {
enabled, err := model.GetBoolByKey(context.Background(), model.ConfigKeyRegistrationEnabled)
if err != nil {
return true
}
return enabled
}
func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) error {
session := sessions.Default(c)
session.Set(oauth.UserIDKey, user.ID)
@@ -87,7 +64,7 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
maxAge := config.Config.App.SessionAge
isSessionCookie := false
ttlHours, err := model.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours)
ttlHours, err := repository.GetIntByKey(ctx, model.ConfigKeyLoginSessionTTLHours)
if err == nil {
switch {
case ttlHours == -1:
@@ -124,7 +101,8 @@ func setLoginSession(ctx context.Context, c *gin.Context, user *model.User) erro
// @Failure 500 {object} response.Any "服务内部错误"
// @Router /api/v1/user/login [post]
func Login(c *gin.Context) {
if !isPasswordLoginEnabled() {
ctx := c.Request.Context()
if !isPasswordLoginEnabled(ctx) {
response.AbortBadRequest(c, errPasswordLoginDisabled)
return
}
@@ -140,7 +118,6 @@ func Login(c *gin.Context) {
}
var user model.User
ctx := c.Request.Context()
if err := db.DB(ctx).Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error; err != nil {
logger.WarnF(ctx, "[LoginAudit] failed login attempt (username not found) for input: %s, IP: %s", req.Username, c.ClientIP())
response.AbortBadRequest(c, errUsernameOrPasswordWrong)
@@ -211,7 +188,8 @@ func Login(c *gin.Context) {
// @Failure 500 {object} response.Any "服务内部错误"
// @Router /api/v1/user/register [post]
func Register(c *gin.Context) {
if !isRegistrationEnabled() || !isPasswordRegisterEnabled() {
ctx := c.Request.Context()
if !isRegistrationEnabled(ctx) || !isPasswordRegisterEnabled(ctx) {
response.AbortBadRequest(c, errRegistrationDisabled)
return
}
@@ -242,8 +220,6 @@ func Register(c *gin.Context) {
return
}
ctx := c.Request.Context()
// 邮箱注册验证校验
if err := validateRegisterEmailVerification(ctx, req.Email, req.Code); err != nil {
response.AbortBadRequest(c, err.Error())
@@ -344,7 +320,7 @@ func ChangePassword(c *gin.Context) {
return
}
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
userObj, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
if userObj == nil {
response.AbortUnauthorized(c, errLoginRequired)
return
@@ -443,7 +419,7 @@ func UpdateProfile(c *gin.Context) {
return
}
userObj, _ := util.GetFromContext[*model.User](c, oauth.UserObjKey)
userObj, _ := oauth.GetFromContext[*model.User](c, oauth.UserObjKey)
if userObj == nil {
response.AbortUnauthorized(c, errLoginRequired)
return
+7 -4
View File
@@ -3,7 +3,8 @@
package user
import ("bytes"
import (
"bytes"
"context"
"encoding/json"
"net/http"
@@ -16,12 +17,14 @@ import ("bytes"
"github.com/Rain-kl/Wavelet/internal/config"
"github.com/Rain-kl/Wavelet/internal/db"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/testhelper"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin"
"github.com/Rain-kl/Wavelet/internal/common/response")
"github.com/Rain-kl/Wavelet/internal/common/response"
)
func setupUserTestRouter(t *testing.T) *gin.Engine {
t.Helper()
@@ -281,7 +284,7 @@ func TestLoginEmailVerificationFallbackWhenSMTPUnconfigured(t *testing.T) {
}
// 2.5 Invalidate the system config cache in Redis
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
t.Fatalf("invalidate system config cache failed: %v", err)
}
@@ -387,7 +390,7 @@ func TestLoginEmailVerificationFallbackForEmptyEmail(t *testing.T) {
}
// Invalidate the system config cache in Redis
if err := db.Redis.Del(context.Background(), db.PrefixedKey(model.SystemConfigRedisHashKey)).Err(); err != nil {
if err := db.Redis.Del(context.Background(), db.PrefixedKey(repository.SystemConfigRedisHashKey)).Err(); err != nil {
t.Fatalf("invalidate system config cache failed: %v", err)
}
+5 -5
View File
@@ -12,6 +12,7 @@ import (
"strings"
"github.com/Rain-kl/Wavelet/internal/model"
"github.com/Rain-kl/Wavelet/internal/repository"
"github.com/Rain-kl/Wavelet/internal/task"
"github.com/Rain-kl/Wavelet/pkg/mail"
)
@@ -111,17 +112,16 @@ func (h *SendEmailHandler) Execute(ctx context.Context, payload []byte) (*task.T
var smtpUsername string
var smtpPassword string
var sc model.SystemConfig
if err := sc.GetByKey(ctx, model.ConfigKeySMTPHost); err == nil {
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPHost); err == nil {
smtpHost = sc.Value
}
if err := sc.GetByKey(ctx, model.ConfigKeySMTPPort); err == nil {
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPort); err == nil {
smtpPortVal = sc.Value
}
if err := sc.GetByKey(ctx, model.ConfigKeySMTPUsername); err == nil {
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPUsername); err == nil {
smtpUsername = sc.Value
}
if err := sc.GetByKey(ctx, model.ConfigKeySMTPPassword); err == nil {
if sc, err := repository.GetSystemConfigByKey(ctx, model.ConfigKeySMTPPassword); err == nil {
smtpPassword = sc.Value
}