68 lines
2.6 KiB
Go
68 lines
2.6 KiB
Go
package user
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"example.com/userService/internal/model"
|
|
repo "example.com/userService/internal/repository/user"
|
|
"example.com/userService/internal/servicecontext"
|
|
"example.com/userService/internal/types"
|
|
apierrors "git.yuntuo.hk/k2safe/go-omni/errors"
|
|
"git.yuntuo.hk/k2safe/go-omni/logx"
|
|
"net/mail"
|
|
"strings"
|
|
"time"
|
|
"unicode/utf8"
|
|
)
|
|
|
|
type Service struct{ deps *servicecontext.Context }
|
|
|
|
func New(deps *servicecontext.Context) *Service { return &Service{deps: deps} }
|
|
func normalize(name, email string) (model.UserInput, error) {
|
|
input := model.UserInput{Name: strings.TrimSpace(name), Email: strings.ToLower(strings.TrimSpace(email))}
|
|
address, err := mail.ParseAddress(input.Email)
|
|
if input.Name == "" || utf8.RuneCountInString(input.Name) > 100 || len(input.Email) > 254 || err != nil || address.Address != input.Email {
|
|
return model.UserInput{}, model.ErrInvalidUser
|
|
}
|
|
return input, nil
|
|
}
|
|
func response(u model.User) *types.UserResp {
|
|
return &types.UserResp{ID: u.ID, Name: u.Name, Email: u.Email, CreatedAt: u.CreatedAt.UTC().Format(time.RFC3339Nano), UpdatedAt: u.UpdatedAt.UTC().Format(time.RFC3339Nano)}
|
|
}
|
|
func (s *Service) failure(ctx context.Context, err error) error {
|
|
switch {
|
|
case err == nil:
|
|
return nil
|
|
case errors.Is(err, model.ErrUserNotFound):
|
|
return apierrors.NotFound("USER_NOT_FOUND", "user not found")
|
|
case errors.Is(err, model.ErrEmailTaken):
|
|
return apierrors.New(409, "EMAIL_TAKEN", "email already in use")
|
|
case errors.Is(err, model.ErrInvalidUser):
|
|
return apierrors.BadRequest("INVALID_USER", "invalid user")
|
|
case errors.Is(err, model.ErrInvalidPage):
|
|
return apierrors.BadRequest("INVALID_PAGE", "invalid pagination")
|
|
default:
|
|
// Do not log SQL/DSN or user input; requestId correlates the failed operation.
|
|
s.deps.Logger.ErrorContext(ctx, "user.operationFailed", "category", "persistence")
|
|
// HTTP access logging also sees the returned error. Keep raw driver
|
|
// errors out of that path because their messages may contain input.
|
|
return apierrors.New(500, "INTERNAL_ERROR", "internal server error")
|
|
}
|
|
}
|
|
func (s *Service) mutate(ctx context.Context, operation string, fn func(repo.UserRepository) (model.User, error)) (*types.UserResp, error) {
|
|
var u model.User
|
|
err := s.deps.Users.WithTx(ctx, func(r repo.UserRepository, audit func(string, int64, string) error) error {
|
|
var err error
|
|
u, err = fn(r)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return audit(operation, u.ID, logx.RequestID(ctx))
|
|
})
|
|
if err != nil {
|
|
return nil, s.failure(ctx, err)
|
|
}
|
|
s.deps.Logger.InfoContext(ctx, "user."+operation, "userId", u.ID)
|
|
return response(u), nil
|
|
}
|