feat(trade): implement second-hand trading/flea market feature
Add complete trade functionality including: - TradeItem, TradeImage, TradeFavorite models with auto-migration - TradeRepository with CRUD and favorite operations - TradeService with business logic for listing, creating, updating, status management - TradeHandler with RESTful endpoints for trade operations - DTOs and converters for request/response handling Routes include: list, get by id, create, update, delete, status update, view recording, favorite/unfavorite.
This commit is contained in:
230
internal/service/trade_service.go
Normal file
230
internal/service/trade_service.go
Normal file
@@ -0,0 +1,230 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"with_you/internal/dto"
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/pkg/cursor"
|
||||
"with_you/internal/repository"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TradeService interface {
|
||||
Create(ctx context.Context, userID string, req dto.CreateTradeItemRequest) (*model.TradeItem, error)
|
||||
GetByID(ctx context.Context, id string, currentUserID *string) (*dto.TradeItemDetailResponse, error)
|
||||
Update(ctx context.Context, userID string, id string, req dto.UpdateTradeItemRequest) error
|
||||
Delete(ctx context.Context, userID string, id string) error
|
||||
UpdateStatus(ctx context.Context, userID string, id string, status model.TradeItemStatus) error
|
||||
List(ctx context.Context, tradeType *model.TradeType, category *model.TradeCategory, cursorStr string, pageSize int, currentUserID *string) (*dto.TradeCursorPageResponse, error)
|
||||
Search(ctx context.Context, keyword string, tradeType *model.TradeType, category *model.TradeCategory, cursorStr string, pageSize int, currentUserID *string) (*dto.TradeCursorPageResponse, error)
|
||||
RecordView(ctx context.Context, id string) error
|
||||
Favorite(ctx context.Context, userID string, id string) error
|
||||
Unfavorite(ctx context.Context, userID string, id string) error
|
||||
}
|
||||
|
||||
type tradeService struct {
|
||||
tradeRepo repository.TradeRepository
|
||||
}
|
||||
|
||||
func NewTradeService(tradeRepo repository.TradeRepository) TradeService {
|
||||
return &tradeService{tradeRepo: tradeRepo}
|
||||
}
|
||||
|
||||
func (s *tradeService) Create(ctx context.Context, userID string, req dto.CreateTradeItemRequest) (*model.TradeItem, error) {
|
||||
item := &model.TradeItem{
|
||||
UserID: userID,
|
||||
TradeType: model.TradeType(req.TradeType),
|
||||
Category: model.TradeCategory(req.Category),
|
||||
Title: req.Title,
|
||||
Content: req.Content,
|
||||
Segments: req.Segments,
|
||||
Price: req.Price,
|
||||
OriginalPrice: req.OriginalPrice,
|
||||
Condition: req.Condition,
|
||||
Location: req.Location,
|
||||
Status: model.TradeItemActive,
|
||||
}
|
||||
if item.Content == "" {
|
||||
item.Content = item.Title
|
||||
}
|
||||
|
||||
images := req.Images
|
||||
if images == nil {
|
||||
images = []string{}
|
||||
}
|
||||
if err := s.tradeRepo.Create(ctx, item, images); err != nil {
|
||||
return nil, fmt.Errorf("create trade item: %w", err)
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (s *tradeService) GetByID(ctx context.Context, id string, currentUserID *string) (*dto.TradeItemDetailResponse, error) {
|
||||
item, err := s.tradeRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, fmt.Errorf("trade item not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isFavorited := false
|
||||
if currentUserID != nil {
|
||||
fav, err := s.tradeRepo.IsFavorited(ctx, id, *currentUserID)
|
||||
if err == nil {
|
||||
isFavorited = fav
|
||||
}
|
||||
}
|
||||
|
||||
_ = s.tradeRepo.IncrementViews(ctx, id)
|
||||
|
||||
return dto.ConvertTradeItemToDetailResponse(item, isFavorited), nil
|
||||
}
|
||||
|
||||
func (s *tradeService) Update(ctx context.Context, userID string, id string, req dto.UpdateTradeItemRequest) error {
|
||||
item, err := s.tradeRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("trade item not found")
|
||||
}
|
||||
if item.UserID != userID {
|
||||
return fmt.Errorf("not authorized")
|
||||
}
|
||||
|
||||
if req.TradeType != nil {
|
||||
item.TradeType = model.TradeType(*req.TradeType)
|
||||
}
|
||||
if req.Category != nil {
|
||||
item.Category = model.TradeCategory(*req.Category)
|
||||
}
|
||||
if req.Title != nil {
|
||||
item.Title = *req.Title
|
||||
}
|
||||
if req.Content != nil {
|
||||
item.Content = *req.Content
|
||||
}
|
||||
if req.Segments != nil {
|
||||
item.Segments = req.Segments
|
||||
}
|
||||
if req.Price != nil {
|
||||
item.Price = req.Price
|
||||
}
|
||||
if req.OriginalPrice != nil {
|
||||
item.OriginalPrice = req.OriginalPrice
|
||||
}
|
||||
if req.Condition != nil {
|
||||
item.Condition = *req.Condition
|
||||
}
|
||||
if req.Location != nil {
|
||||
item.Location = *req.Location
|
||||
}
|
||||
item.UpdatedAt = time.Now()
|
||||
|
||||
if req.Images != nil {
|
||||
return s.tradeRepo.UpdateWithImages(ctx, item, &req.Images)
|
||||
}
|
||||
return s.tradeRepo.Update(ctx, item)
|
||||
}
|
||||
|
||||
func (s *tradeService) Delete(ctx context.Context, userID string, id string) error {
|
||||
item, err := s.tradeRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("trade item not found")
|
||||
}
|
||||
if item.UserID != userID {
|
||||
return fmt.Errorf("not authorized")
|
||||
}
|
||||
return s.tradeRepo.Delete(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tradeService) UpdateStatus(ctx context.Context, userID string, id string, status model.TradeItemStatus) error {
|
||||
item, err := s.tradeRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("trade item not found")
|
||||
}
|
||||
if item.UserID != userID {
|
||||
return fmt.Errorf("not authorized")
|
||||
}
|
||||
return s.tradeRepo.UpdateStatus(ctx, id, status)
|
||||
}
|
||||
|
||||
func (s *tradeService) List(ctx context.Context, tradeType *model.TradeType, category *model.TradeCategory, cursorStr string, pageSize int, currentUserID *string) (*dto.TradeCursorPageResponse, error) {
|
||||
req := cursor.NewPageRequest(cursorStr, cursor.Forward, pageSize)
|
||||
result, err := s.tradeRepo.ListByCursor(ctx, tradeType, category, nil, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.buildCursorResponse(ctx, result, currentUserID)
|
||||
}
|
||||
|
||||
func (s *tradeService) Search(ctx context.Context, keyword string, tradeType *model.TradeType, category *model.TradeCategory, cursorStr string, pageSize int, currentUserID *string) (*dto.TradeCursorPageResponse, error) {
|
||||
req := cursor.NewPageRequest(cursorStr, cursor.Forward, pageSize)
|
||||
result, err := s.tradeRepo.SearchByCursor(ctx, keyword, tradeType, category, req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.buildCursorResponse(ctx, result, currentUserID)
|
||||
}
|
||||
|
||||
func (s *tradeService) buildCursorResponse(ctx context.Context, result *cursor.CursorPageResult[*model.TradeItem], currentUserID *string) (*dto.TradeCursorPageResponse, error) {
|
||||
favoriteMap := make(map[string]bool)
|
||||
if currentUserID != nil && len(result.Items) > 0 {
|
||||
ids := make([]string, 0, len(result.Items))
|
||||
for _, item := range result.Items {
|
||||
ids = append(ids, item.ID)
|
||||
}
|
||||
fm, err := s.tradeRepo.IsFavoritedBatch(ctx, ids, *currentUserID)
|
||||
if err == nil {
|
||||
favoriteMap = fm
|
||||
}
|
||||
}
|
||||
|
||||
resp := &dto.TradeCursorPageResponse{
|
||||
Items: dto.ConvertTradeItemsToResponse(result.Items, favoriteMap),
|
||||
NextCursor: result.NextCursor,
|
||||
PrevCursor: result.PrevCursor,
|
||||
HasMore: result.HasMore,
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (s *tradeService) RecordView(ctx context.Context, id string) error {
|
||||
return s.tradeRepo.IncrementViews(ctx, id)
|
||||
}
|
||||
|
||||
func (s *tradeService) Favorite(ctx context.Context, userID string, id string) error {
|
||||
_, err := s.tradeRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("trade item not found")
|
||||
}
|
||||
exists, err := s.tradeRepo.IsFavorited(ctx, id, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if exists {
|
||||
return nil
|
||||
}
|
||||
if err := s.tradeRepo.CreateFavorite(ctx, id, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.tradeRepo.IncrementFavorites(ctx, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *tradeService) Unfavorite(ctx context.Context, userID string, id string) error {
|
||||
exists, err := s.tradeRepo.IsFavorited(ctx, id, userID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !exists {
|
||||
return nil
|
||||
}
|
||||
if err := s.tradeRepo.DeleteFavorite(ctx, id, userID); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = s.tradeRepo.DecrementFavorites(ctx, id)
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user