Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions apps/product-query-svc/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@

- domain:领域模型与规则(不依赖外层)
- ports:核心对外边界
- inbound(use case 接口):`ProductService`
- outbound(基础设施接口):`ProductRepo`
- app:用例实现(编排业务,依赖 `ports`,不关心 HTTP/DB)
- inbound(use case 接口):`ProductUseCases`、`UserQueries`
- outbound(基础设施接口):`ProductRepository`、`UserRepository`
- application:用例实现(按聚合拆分到 `application/product` 与 `application/user`,依赖 ports,脱离 HTTP/DB)
- adapters:适配器实现
- inbound/http:实现 OpenAPI 生成的 `ServerInterface`,调用 `ports.ProductService`
- outbound/inmem、outbound/postgres:实现 `ports.ProductRepo`
- inbound/http:实现 OpenAPI 生成的 `ServerInterface`,调用 `ports/inbound.ProductUseCases`
- outbound/inmem、outbound/postgres:实现 `ports/outbound.ProductRepository`
- backend/cmd/.../main.go:组装根,选择具体适配器(inmem 或 postgres),注入到 app 层,再挂到 HTTP。

依赖方向:`adapters -> app -> ports <- domain`(领域最内层,适配器最外层)。
Expand All @@ -23,15 +23,15 @@ Client
↓
HTTP Inbound Adapter
apps/product-query-svc/adapters/inbound/http/handler_product_read.go (Server.GetProductByID)
↓ 依赖入站端口 ports.ProductService
↓ 依赖入站端口 ports/inbound.ProductUseCases
Ports (Inbound)
apps/product-query-svc/ports/inbound.go (interface ProductService)
↓ 由组装根注入 app 实现
apps/product-query-svc/ports/inbound/product.go (interface ProductUseCases)
↓ 由组装根注入 application 实现
Application (Use Case)
apps/product-query-svc/app/product_service.go (ProductService.GetProduct)
↓ 依赖出站端口 ports.ProductRepo
apps/product-query-svc/application/product/service.go (Service.FetchByID)
↓ 依赖出站端口 ports/outbound.ProductRepository
Ports (Outbound)
apps/product-query-svc/ports/outbound.go (interface ProductRepo)
apps/product-query-svc/ports/outbound/product.go (interface ProductRepository)
↓ 由组装根选择并注入具体适配器
Outbound Adapters (Persistence)
├─ apps/product-query-svc/adapters/outbound/inmem/product_repository.go (InMemRepo.GetByID)
Expand All @@ -42,8 +42,8 @@ Domain

Composition Root(组装根)
backend/cmd/marketplace/product-query-svc/main.go
- 读取配置,选择 inmem 或 postgres 作为 ProductRepo 的实现
- 构造 app.ProductService,并作为 ports.ProductService 注入 HTTP 适配器
- 读取配置,选择 inmem 或 postgres 作为 ProductRepository 的实现
- 构造 productapp.Service,并作为 ports/inbound.ProductUseCases 注入 HTTP 适配器
- 启动 HTTP 服务器
```

Expand All @@ -55,16 +55,16 @@ Client
HTTP Inbound Adapter
apps/product-query-svc/adapters/inbound/http/handler_product_write.go (Server.CreateProduct)
- 将 JSON DTO 映射为 domain.Product(美元转分,调用 NewProduct 校验不变式)
↓ 调用入站端口 ports.ProductService.CreateProduct
↓ 调用入站端口 ports/inbound.ProductUseCases.Create
Application (Use Case)
apps/product-query-svc/app/product_service.go (CreateProduct)
apps/product-query-svc/application/product/service.go (Service.Create)
- 调用 p.Validate / 富行为 → 通过出站端口持久化
↓
↓
Ports (Outbound)
apps/product-query-svc/ports/outbound.go (ProductRepo.Create)
apps/product-query-svc/ports/outbound/product.go (ProductRepository.Create)
↓
Outbound Adapter
apps/product-query-svc/adapters/outbound/postgres/inmem (真正落库/内存存储)
apps/product-query-svc/adapters/outbound/postgres|inmem (真正落库/内存存储)
↓
返回 HTTP(Created + JSON),领域错误映射为 400/404。
```
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ func (s *Server) GetProductByID(w http.ResponseWriter, r *http.Request, id int64
writeError(w, http.StatusBadRequest, "INVALID_ID", "id must be a positive integer")
return
}
p, err := s.products.GetProduct(r.Context(), id)
p, err := s.products.FetchByID(r.Context(), id)
if err != nil {
writeDomainError(w, err)
return
Expand All @@ -33,7 +33,7 @@ func (s *Server) SearchProducts(w http.ResponseWriter, r *http.Request, params S
if params.PageSize != nil {
pageSize = *params.PageSize
}
items, total, err := s.products.SearchProducts(r.Context(), q, page, pageSize)
items, total, err := s.products.Search(r.Context(), q, page, pageSize)
if err != nil {
writeDomainError(w, err)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ func (s *Server) DeleteProductByID(w http.ResponseWriter, r *http.Request, id in
writeError(w, http.StatusBadRequest, "INVALID_ID", "id must be a positive integer")
return
}
if err := s.products.DeleteProduct(r.Context(), id); err != nil {
if err := s.products.Remove(r.Context(), id); err != nil {
writeDomainError(w, err)
return
}
Expand All @@ -33,7 +33,7 @@ func (s *Server) CreateProduct(w http.ResponseWriter, r *http.Request) {
writeDomainError(w, err)
return
}
id, err := s.products.CreateProduct(r.Context(), p)
id, err := s.products.Create(r.Context(), p)
if err != nil {
writeDomainError(w, err)
return
Expand Down Expand Up @@ -61,7 +61,7 @@ func (s *Server) UpdateProduct(w http.ResponseWriter, r *http.Request, id int64)
return
}
p.ID = id
updated, err := s.products.UpdateProduct(r.Context(), p)
updated, err := s.products.Update(r.Context(), p)
if err != nil {
writeDomainError(w, err)
return
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ func (s *Server) GetUserByID(w http.ResponseWriter, r *http.Request, id int64) {
writeError(w, http.StatusBadRequest, "INVALID_ID", "id must be a positive integer")
return
}
u, err := s.users.GetUser(r.Context(), id)
u, err := s.users.FetchByID(r.Context(), id)
if err != nil {
writeDomainError(w, err)
return
Expand Down
8 changes: 4 additions & 4 deletions apps/product-query-svc/adapters/inbound/http/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ package httpadapter
import (
"net/http"

"github.com/fightingBald/GoTuto/apps/product-query-svc/ports"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/inbound"
)

// Server wires product and user services to HTTP handlers generated from OpenAPI.
type Server struct {
products ports.ProductService
users ports.UserService
products inbound.ProductUseCases
users inbound.UserQueries
}

func NewServer(products ports.ProductService, users ports.UserService) *Server {
func NewServer(products inbound.ProductUseCases, users inbound.UserQueries) *Server {
return &Server{products: products, users: users}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import (
"time"

"github.com/fightingBald/GoTuto/apps/product-query-svc/domain"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/outbound"
)

var (
_ outbound.ProductRepository = (*InMemRepo)(nil)
_ outbound.UserRepository = (*InMemRepo)(nil)
)

// 简单的内存实现,用于本地开发/测试和示例 wiring
Expand Down Expand Up @@ -101,7 +107,7 @@ func (r *InMemRepo) Update(ctx context.Context, p *domain.Product) error {
return nil
}

func (r *InMemRepo) GetUserByID(ctx context.Context, id int64) (*domain.User, error) {
func (r *InMemRepo) FindByID(ctx context.Context, id int64) (*domain.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
u, ok := r.users[id]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import (

"github.com/Masterminds/squirrel"
"github.com/fightingBald/GoTuto/apps/product-query-svc/domain"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/outbound"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)
Expand All @@ -16,7 +16,11 @@ var psql = squirrel.StatementBuilder.PlaceholderFormat(squirrel.Dollar)

type PGProductRepo struct{ pool *pgxpool.Pool }

func NewProductRepository(pool *pgxpool.Pool) ports.ProductRepo { return &PGProductRepo{pool: pool} }
var _ outbound.ProductRepository = (*PGProductRepo)(nil)

func NewProductRepository(pool *pgxpool.Pool) outbound.ProductRepository {
return &PGProductRepo{pool: pool}
}

func (r *PGProductRepo) GetByID(ctx context.Context, id int64) (*domain.Product, error) {
q, args, err := psql.Select("id", "name", "price", "tags").From("products").Where(squirrel.Eq{"id": id}).ToSql()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,18 @@ import (

"github.com/Masterminds/squirrel"
"github.com/fightingBald/GoTuto/apps/product-query-svc/domain"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/outbound"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
)

type PGUserRepo struct{ pool *pgxpool.Pool }

func NewUserRepository(pool *pgxpool.Pool) ports.UserRepo { return &PGUserRepo{pool: pool} }
var _ outbound.UserRepository = (*PGUserRepo)(nil)

func (r *PGUserRepo) GetUserByID(ctx context.Context, id int64) (*domain.User, error) {
func NewUserRepository(pool *pgxpool.Pool) outbound.UserRepository { return &PGUserRepo{pool: pool} }

func (r *PGUserRepo) FindByID(ctx context.Context, id int64) (*domain.User, error) {
q, args, err := psql.Select("id", "name", "email", "created_at").From("users").Where(squirrel.Eq{"id": id}).ToSql()
if err != nil {
return nil, err
Expand Down
45 changes: 0 additions & 45 deletions apps/product-query-svc/app/product_service.go

This file was deleted.

22 changes: 0 additions & 22 deletions apps/product-query-svc/app/user_service.go

This file was deleted.

52 changes: 52 additions & 0 deletions apps/product-query-svc/application/product/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package productapp

import (
"context"

"github.com/fightingBald/GoTuto/apps/product-query-svc/domain"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/inbound"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/outbound"
)

var _ inbound.ProductUseCases = (*Service)(nil)

// Service orchestrates product-related use cases across outbound dependencies.
type Service struct {
repository outbound.ProductRepository
}

func NewService(repository outbound.ProductRepository) *Service {
return &Service{repository: repository}
}

func (s *Service) FetchByID(ctx context.Context, id int64) (*domain.Product, error) {
return s.repository.GetByID(ctx, id)
}

func (s *Service) Search(ctx context.Context, query string, page, pageSize int) ([]domain.Product, int, error) {
return s.repository.Search(ctx, query, page, pageSize)
}

func (s *Service) Remove(ctx context.Context, id int64) error {
return s.repository.Delete(ctx, id)
}

func (s *Service) Create(ctx context.Context, product *domain.Product) (int64, error) {
if err := product.Validate(); err != nil {
return 0, err
}
return s.repository.Create(ctx, product)
}

func (s *Service) Update(ctx context.Context, product *domain.Product) (*domain.Product, error) {
if product.ID <= 0 {
return nil, domain.ValidationError("id must be a positive integer")
}
if err := product.Validate(); err != nil {
return nil, err
}
if err := s.repository.Update(ctx, product); err != nil {
return nil, err
}
return s.repository.GetByID(ctx, product.ID)
}
27 changes: 27 additions & 0 deletions apps/product-query-svc/application/user/service.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package userapp

import (
"context"

"github.com/fightingBald/GoTuto/apps/product-query-svc/domain"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/inbound"
"github.com/fightingBald/GoTuto/apps/product-query-svc/ports/outbound"
)

var _ inbound.UserQueries = (*Service)(nil)

// Service exposes user-specific use cases backed by a persistent repository.
type Service struct {
repository outbound.UserRepository
}

func NewService(repository outbound.UserRepository) *Service {
return &Service{repository: repository}
}

func (s *Service) FetchByID(ctx context.Context, id int64) (*domain.User, error) {
if id <= 0 {
return nil, domain.ValidationError("id must be a positive integer")
}
return s.repository.FindByID(ctx, id)
}
13 changes: 13 additions & 0 deletions apps/product-query-svc/domain/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package domain

import "errors"

var (
ErrValidation = errors.New("validation error")
ErrNotFound = errors.New("not found")
)

// ValidationError wraps ErrValidation with a more specific message.
func ValidationError(msg string) error {
return errors.Join(ErrValidation, errors.New(msg))
}
Loading
Loading