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:
325
internal/repository/trade_repo.go
Normal file
325
internal/repository/trade_repo.go
Normal file
@@ -0,0 +1,325 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/pkg/cursor"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TradeRepository interface {
|
||||
Create(ctx context.Context, item *model.TradeItem, imageUrls []string) error
|
||||
GetByID(ctx context.Context, id string) (*model.TradeItem, error)
|
||||
Update(ctx context.Context, item *model.TradeItem) error
|
||||
UpdateWithImages(ctx context.Context, item *model.TradeItem, imageUrls *[]string) error
|
||||
Delete(ctx context.Context, id string) error
|
||||
UpdateStatus(ctx context.Context, id string, status model.TradeItemStatus) error
|
||||
ListByCursor(ctx context.Context, tradeType *model.TradeType, category *model.TradeCategory, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error)
|
||||
SearchByCursor(ctx context.Context, keyword string, tradeType *model.TradeType, category *model.TradeCategory, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error)
|
||||
ListByUserByCursor(ctx context.Context, userID string, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error)
|
||||
IncrementViews(ctx context.Context, id string) error
|
||||
IncrementFavorites(ctx context.Context, id string) error
|
||||
DecrementFavorites(ctx context.Context, id string) error
|
||||
|
||||
CreateFavorite(ctx context.Context, tradeItemID, userID string) error
|
||||
DeleteFavorite(ctx context.Context, tradeItemID, userID string) error
|
||||
IsFavorited(ctx context.Context, tradeItemID, userID string) (bool, error)
|
||||
IsFavoritedBatch(ctx context.Context, tradeItemIDs []string, userID string) (map[string]bool, error)
|
||||
}
|
||||
|
||||
type tradeRepository struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewTradeRepository(db *gorm.DB) TradeRepository {
|
||||
return &tradeRepository{db: db}
|
||||
}
|
||||
|
||||
func (r *tradeRepository) getDB(ctx context.Context) *gorm.DB {
|
||||
if tx := GetTxFromContext(ctx); tx != nil {
|
||||
return tx
|
||||
}
|
||||
return r.db
|
||||
}
|
||||
|
||||
func (r *tradeRepository) Create(ctx context.Context, item *model.TradeItem, imageUrls []string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Create(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for i, url := range imageUrls {
|
||||
img := model.TradeImage{
|
||||
TradeItemID: item.ID,
|
||||
URL: url,
|
||||
SortOrder: i,
|
||||
}
|
||||
if err := tx.Create(&img).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *tradeRepository) GetByID(ctx context.Context, id string) (*model.TradeItem, error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
var item model.TradeItem
|
||||
if err := db.Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("sort_order ASC")
|
||||
}).First(&item, "id = ?", id).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *tradeRepository) Update(ctx context.Context, item *model.TradeItem) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Save(item).Error
|
||||
}
|
||||
|
||||
func (r *tradeRepository) UpdateWithImages(ctx context.Context, item *model.TradeItem, imageUrls *[]string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if imageUrls != nil {
|
||||
if err := tx.Where("trade_item_id = ?", item.ID).Delete(&model.TradeImage{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
for i, url := range *imageUrls {
|
||||
img := model.TradeImage{
|
||||
TradeItemID: item.ID,
|
||||
URL: url,
|
||||
SortOrder: i,
|
||||
}
|
||||
if err := tx.Create(&img).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (r *tradeRepository) Delete(ctx context.Context, id string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("trade_item_id = ?", id).Delete(&model.TradeImage{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("trade_item_id = ?", id).Delete(&model.TradeFavorite{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
return tx.Delete(&model.TradeItem{}, "id = ?", id).Error
|
||||
})
|
||||
}
|
||||
|
||||
func (r *tradeRepository) UpdateStatus(ctx context.Context, id string, status model.TradeItemStatus) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Model(&model.TradeItem{}).Where("id = ?", id).Update("status", status).Error
|
||||
}
|
||||
|
||||
func (r *tradeRepository) ListByCursor(ctx context.Context, tradeType *model.TradeType, category *model.TradeCategory, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
query := db.Model(&model.TradeItem{})
|
||||
|
||||
if tradeType != nil {
|
||||
query = query.Where("trade_type = ?", *tradeType)
|
||||
}
|
||||
if category != nil {
|
||||
query = query.Where("category = ?", *category)
|
||||
}
|
||||
defaultStatus := model.TradeItemActive
|
||||
if status != nil {
|
||||
defaultStatus = *status
|
||||
}
|
||||
query = query.Where("status = ?", defaultStatus)
|
||||
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
return cursor.NewCursorPageResult([]*model.TradeItem{}, "", "", false), nil
|
||||
}
|
||||
|
||||
var items []*model.TradeItem
|
||||
q := builder.Build().Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("sort_order ASC")
|
||||
})
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(items), pageSize)
|
||||
if hasMore {
|
||||
items = items[:pageSize]
|
||||
}
|
||||
|
||||
var nextCursor, prevCursor string
|
||||
if len(items) > 0 {
|
||||
if hasMore {
|
||||
last := items[len(items)-1]
|
||||
nextCursor = cursor.NewCursor(cursor.FormatTime(last.CreatedAt), last.ID, cursor.SortByCreatedAtDesc).Encode()
|
||||
}
|
||||
first := items[0]
|
||||
prevCursor = cursor.NewCursor(cursor.FormatTime(first.CreatedAt), first.ID, cursor.SortByCreatedAtDesc).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(items, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
func (r *tradeRepository) SearchByCursor(ctx context.Context, keyword string, tradeType *model.TradeType, category *model.TradeCategory, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
query := db.Model(&model.TradeItem{}).Where("status = ?", model.TradeItemActive)
|
||||
|
||||
if tradeType != nil {
|
||||
query = query.Where("trade_type = ?", *tradeType)
|
||||
}
|
||||
if category != nil {
|
||||
query = query.Where("category = ?", *category)
|
||||
}
|
||||
if keyword != "" {
|
||||
searchPattern := "%" + keyword + "%"
|
||||
query = query.Where("title LIKE ? OR content LIKE ?", searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
return cursor.NewCursorPageResult([]*model.TradeItem{}, "", "", false), nil
|
||||
}
|
||||
|
||||
var items []*model.TradeItem
|
||||
q := builder.Build().Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("sort_order ASC")
|
||||
})
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(items), pageSize)
|
||||
if hasMore {
|
||||
items = items[:pageSize]
|
||||
}
|
||||
|
||||
var nextCursor, prevCursor string
|
||||
if len(items) > 0 {
|
||||
if hasMore {
|
||||
last := items[len(items)-1]
|
||||
nextCursor = cursor.NewCursor(cursor.FormatTime(last.CreatedAt), last.ID, cursor.SortByCreatedAtDesc).Encode()
|
||||
}
|
||||
first := items[0]
|
||||
prevCursor = cursor.NewCursor(cursor.FormatTime(first.CreatedAt), first.ID, cursor.SortByCreatedAtDesc).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(items, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
func (r *tradeRepository) ListByUserByCursor(ctx context.Context, userID string, status *model.TradeItemStatus, req *cursor.PageRequest) (*cursor.CursorPageResult[*model.TradeItem], error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
query := db.Model(&model.TradeItem{}).Where("user_id = ?", userID)
|
||||
|
||||
if status != nil {
|
||||
query = query.Where("status = ?", *status)
|
||||
}
|
||||
|
||||
builder := cursor.NewBuilder(query, cursor.SortByCreatedAtDesc).
|
||||
WithCursor(req.Cursor, req.Direction).
|
||||
WithPageSize(req.PageSize)
|
||||
|
||||
if builder.Error() != nil {
|
||||
return cursor.NewCursorPageResult([]*model.TradeItem{}, "", "", false), nil
|
||||
}
|
||||
|
||||
var items []*model.TradeItem
|
||||
q := builder.Build().Preload("User").Preload("Images", func(db *gorm.DB) *gorm.DB {
|
||||
return db.Order("sort_order ASC")
|
||||
})
|
||||
if err := q.Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pageSize := builder.GetPageSize()
|
||||
hasMore := cursor.HasMore(len(items), pageSize)
|
||||
if hasMore {
|
||||
items = items[:pageSize]
|
||||
}
|
||||
|
||||
var nextCursor, prevCursor string
|
||||
if len(items) > 0 {
|
||||
if hasMore {
|
||||
last := items[len(items)-1]
|
||||
nextCursor = cursor.NewCursor(cursor.FormatTime(last.CreatedAt), last.ID, cursor.SortByCreatedAtDesc).Encode()
|
||||
}
|
||||
first := items[0]
|
||||
prevCursor = cursor.NewCursor(cursor.FormatTime(first.CreatedAt), first.ID, cursor.SortByCreatedAtDesc).Encode()
|
||||
}
|
||||
|
||||
return cursor.NewCursorPageResult(items, nextCursor, prevCursor, hasMore), nil
|
||||
}
|
||||
|
||||
func (r *tradeRepository) IncrementViews(ctx context.Context, id string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Model(&model.TradeItem{}).Where("id = ?", id).UpdateColumn("views_count", gorm.Expr("views_count + 1")).Error
|
||||
}
|
||||
|
||||
func (r *tradeRepository) IncrementFavorites(ctx context.Context, id string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Model(&model.TradeItem{}).Where("id = ?", id).UpdateColumn("favorites_count", gorm.Expr("favorites_count + 1")).Error
|
||||
}
|
||||
|
||||
func (r *tradeRepository) DecrementFavorites(ctx context.Context, id string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Model(&model.TradeItem{}).Where("id = ? AND favorites_count > 0", id).UpdateColumn("favorites_count", gorm.Expr("favorites_count - 1")).Error
|
||||
}
|
||||
|
||||
func (r *tradeRepository) CreateFavorite(ctx context.Context, tradeItemID, userID string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
fav := model.TradeFavorite{
|
||||
TradeItemID: tradeItemID,
|
||||
UserID: userID,
|
||||
}
|
||||
return db.Create(&fav).Error
|
||||
}
|
||||
|
||||
func (r *tradeRepository) DeleteFavorite(ctx context.Context, tradeItemID, userID string) error {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
return db.Where("trade_item_id = ? AND user_id = ?", tradeItemID, userID).Delete(&model.TradeFavorite{}).Error
|
||||
}
|
||||
|
||||
func (r *tradeRepository) IsFavorited(ctx context.Context, tradeItemID, userID string) (bool, error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
var count int64
|
||||
if err := db.Model(&model.TradeFavorite{}).Where("trade_item_id = ? AND user_id = ?", tradeItemID, userID).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count > 0, nil
|
||||
}
|
||||
|
||||
func (r *tradeRepository) IsFavoritedBatch(ctx context.Context, tradeItemIDs []string, userID string) (map[string]bool, error) {
|
||||
db := r.getDB(ctx).WithContext(ctx)
|
||||
result := make(map[string]bool, len(tradeItemIDs))
|
||||
if len(tradeItemIDs) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
for _, id := range tradeItemIDs {
|
||||
result[id] = false
|
||||
}
|
||||
var favorites []model.TradeFavorite
|
||||
if err := db.Where("trade_item_id IN ? AND user_id = ?", tradeItemIDs, userID).Find(&favorites).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, fav := range favorites {
|
||||
result[fav.TradeItemID] = true
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
Reference in New Issue
Block a user