feat: complete API-driven projects with infrastructure and business acceptance

This commit is contained in:
2026-09-23 17:33:50 +08:00
parent 04a1f7999d
commit 2c74dc0859
137 changed files with 11926 additions and 105 deletions

View 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) })
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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
}

View 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) })
}