feat: complete API-driven projects with infrastructure and business acceptance
This commit is contained in:
16
examples/userService/internal/service/user/createUser.go
Normal file
16
examples/userService/internal/service/user/createUser.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/userService/internal/model"
|
||||
repo "example.com/userService/internal/repository/user"
|
||||
"example.com/userService/internal/types"
|
||||
)
|
||||
|
||||
func (s *Service) CreateUser(ctx context.Context, req *types.CreateUserReq) (*types.UserResp, error) {
|
||||
input, err := normalize(req.Name, req.Email)
|
||||
if err != nil {
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
return s.mutate(ctx, "created", func(r repo.UserRepository) (model.User, error) { return r.Create(ctx, input) })
|
||||
}
|
||||
16
examples/userService/internal/service/user/deleteUser.go
Normal file
16
examples/userService/internal/service/user/deleteUser.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/userService/internal/model"
|
||||
repo "example.com/userService/internal/repository/user"
|
||||
"example.com/userService/internal/types"
|
||||
)
|
||||
|
||||
func (s *Service) DeleteUser(ctx context.Context, req *types.UserIDReq) error {
|
||||
_, err := s.mutate(ctx, "deleted", func(r repo.UserRepository) (model.User, error) {
|
||||
err := r.Delete(ctx, req.ID)
|
||||
return model.User{ID: req.ID}, err
|
||||
})
|
||||
return err
|
||||
}
|
||||
40
examples/userService/internal/service/user/exportUser.go
Normal file
40
examples/userService/internal/service/user/exportUser.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"example.com/userService/internal/types"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (s *Service) ExportUser(ctx context.Context, req *types.UserIDReq) (*types.ExportResp, error) {
|
||||
u, err := s.GetUser(ctx, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
payload, err := json.Marshal(u)
|
||||
if err != nil {
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
var nonce [16]byte
|
||||
if _, err = rand.Read(nonce[:]); err != nil {
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
key := fmt.Sprintf("exports/users/%d/%s.txt", u.ID, hex.EncodeToString(nonce[:]))
|
||||
if _, err = s.deps.Uploads.Put(ctx, key, strings.NewReader(string(payload)), int64(len(payload)), "text/plain"); err != nil {
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
url, err := s.deps.Downloads.PresignGet(ctx, key, 5*time.Minute)
|
||||
if err != nil {
|
||||
cleanup, cancel := context.WithTimeout(context.WithoutCancel(ctx), 2*time.Second)
|
||||
defer cancel()
|
||||
_ = s.deps.Files.Delete(cleanup, key)
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
s.deps.Logger.InfoContext(ctx, "user.exported", "userId", u.ID)
|
||||
return &types.ExportResp{DownloadURL: url.String()}, nil
|
||||
}
|
||||
14
examples/userService/internal/service/user/getUser.go
Normal file
14
examples/userService/internal/service/user/getUser.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/userService/internal/types"
|
||||
)
|
||||
|
||||
func (s *Service) GetUser(ctx context.Context, req *types.UserIDReq) (*types.UserResp, error) {
|
||||
u, err := s.deps.Users.Get(ctx, req.ID)
|
||||
if err != nil {
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
return response(u), nil
|
||||
}
|
||||
18
examples/userService/internal/service/user/listUsers.go
Normal file
18
examples/userService/internal/service/user/listUsers.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/userService/internal/types"
|
||||
)
|
||||
|
||||
func (s *Service) ListUsers(ctx context.Context, req *types.ListUsersReq) (*types.UserPageResp, error) {
|
||||
page, err := s.deps.Users.List(ctx, req.Page, req.PageSize)
|
||||
if err != nil {
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
result := &types.UserPageResp{Items: []types.UserResp{}, Total: page.Total, Page: page.Page, PageSize: page.PageSize}
|
||||
for _, item := range page.Items {
|
||||
result.Items = append(result.Items, *response(item))
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
65
examples/userService/internal/service/user/service.go
Normal file
65
examples/userService/internal/service/user/service.go
Normal file
@@ -0,0 +1,65 @@
|
||||
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")
|
||||
return apierrors.Internal(err)
|
||||
}
|
||||
}
|
||||
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
|
||||
}
|
||||
16
examples/userService/internal/service/user/updateUser.go
Normal file
16
examples/userService/internal/service/user/updateUser.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"example.com/userService/internal/model"
|
||||
repo "example.com/userService/internal/repository/user"
|
||||
"example.com/userService/internal/types"
|
||||
)
|
||||
|
||||
func (s *Service) UpdateUser(ctx context.Context, req *types.UpdateUserReq) (*types.UserResp, error) {
|
||||
input, err := normalize(req.Name, req.Email)
|
||||
if err != nil {
|
||||
return nil, s.failure(ctx, err)
|
||||
}
|
||||
return s.mutate(ctx, "updated", func(r repo.UserRepository) (model.User, error) { return r.Update(ctx, req.ID, input) })
|
||||
}
|
||||
Reference in New Issue
Block a user