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:
101
internal/dto/trade_converter.go
Normal file
101
internal/dto/trade_converter.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"with_you/internal/model"
|
||||
)
|
||||
|
||||
func ConvertTradeImageToResponse(img *model.TradeImage) TradeImageResponse {
|
||||
if img == nil {
|
||||
return TradeImageResponse{}
|
||||
}
|
||||
return TradeImageResponse{
|
||||
ID: img.ID,
|
||||
URL: img.URL,
|
||||
ThumbnailURL: img.ThumbnailURL,
|
||||
PreviewURL: img.PreviewURL,
|
||||
PreviewURLLarge: img.PreviewURLLarge,
|
||||
Width: img.Width,
|
||||
Height: img.Height,
|
||||
}
|
||||
}
|
||||
|
||||
func ConvertTradeImagesToResponse(images []model.TradeImage) []TradeImageResponse {
|
||||
result := make([]TradeImageResponse, 0, len(images))
|
||||
for i := range images {
|
||||
result = append(result, ConvertTradeImageToResponse(&images[i]))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ConvertTradeItemToResponse(item *model.TradeItem, isFavorited bool) *TradeItemResponse {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
resp := &TradeItemResponse{
|
||||
ID: item.ID,
|
||||
UserID: item.UserID,
|
||||
TradeType: string(item.TradeType),
|
||||
Category: string(item.Category),
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
Price: item.Price,
|
||||
OriginalPrice: item.OriginalPrice,
|
||||
Condition: item.Condition,
|
||||
Location: item.Location,
|
||||
Status: string(item.Status),
|
||||
ViewsCount: item.ViewsCount,
|
||||
FavoritesCount: item.FavoritesCount,
|
||||
IsFavorited: isFavorited,
|
||||
Images: ConvertTradeImagesToResponse(item.Images),
|
||||
CreatedAt: item.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: item.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
if len(item.Segments) > 0 {
|
||||
resp.Segments = item.Segments
|
||||
}
|
||||
if item.User != nil {
|
||||
resp.Author = ConvertUserToResponse(item.User)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
|
||||
func ConvertTradeItemsToResponse(items []*model.TradeItem, favoriteMap map[string]bool) []*TradeItemResponse {
|
||||
result := make([]*TradeItemResponse, 0, len(items))
|
||||
for _, item := range items {
|
||||
isFav := favoriteMap[item.ID]
|
||||
result = append(result, ConvertTradeItemToResponse(item, isFav))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func ConvertTradeItemToDetailResponse(item *model.TradeItem, isFavorited bool) *TradeItemDetailResponse {
|
||||
if item == nil {
|
||||
return nil
|
||||
}
|
||||
resp := &TradeItemDetailResponse{
|
||||
ID: item.ID,
|
||||
UserID: item.UserID,
|
||||
TradeType: string(item.TradeType),
|
||||
Category: string(item.Category),
|
||||
Title: item.Title,
|
||||
Content: item.Content,
|
||||
Price: item.Price,
|
||||
OriginalPrice: item.OriginalPrice,
|
||||
Condition: item.Condition,
|
||||
Location: item.Location,
|
||||
Status: string(item.Status),
|
||||
ViewsCount: item.ViewsCount,
|
||||
FavoritesCount: item.FavoritesCount,
|
||||
IsFavorited: isFavorited,
|
||||
Images: ConvertTradeImagesToResponse(item.Images),
|
||||
CreatedAt: item.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
UpdatedAt: item.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
|
||||
}
|
||||
if len(item.Segments) > 0 {
|
||||
resp.Segments = item.Segments
|
||||
}
|
||||
if item.User != nil {
|
||||
resp.Author = ConvertUserToResponse(item.User)
|
||||
}
|
||||
return resp
|
||||
}
|
||||
100
internal/dto/trade_dto.go
Normal file
100
internal/dto/trade_dto.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package dto
|
||||
|
||||
import (
|
||||
"with_you/internal/model"
|
||||
)
|
||||
|
||||
type TradeImageResponse struct {
|
||||
ID string `json:"id"`
|
||||
URL string `json:"url"`
|
||||
ThumbnailURL string `json:"thumbnail_url"`
|
||||
PreviewURL string `json:"preview_url"`
|
||||
PreviewURLLarge string `json:"preview_url_large"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
}
|
||||
|
||||
type TradeItemResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
TradeType string `json:"trade_type"`
|
||||
Category string `json:"category"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Segments model.MessageSegments `json:"segments,omitempty"`
|
||||
Images []TradeImageResponse `json:"images"`
|
||||
Price *float64 `json:"price,omitempty"`
|
||||
OriginalPrice *float64 `json:"original_price,omitempty"`
|
||||
Condition string `json:"condition,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ViewsCount int `json:"views_count"`
|
||||
FavoritesCount int `json:"favorites_count"`
|
||||
IsFavorited bool `json:"is_favorited"`
|
||||
Author *UserResponse `json:"author"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type TradeItemDetailResponse struct {
|
||||
ID string `json:"id"`
|
||||
UserID string `json:"user_id"`
|
||||
TradeType string `json:"trade_type"`
|
||||
Category string `json:"category"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Segments model.MessageSegments `json:"segments,omitempty"`
|
||||
Images []TradeImageResponse `json:"images"`
|
||||
Price *float64 `json:"price,omitempty"`
|
||||
OriginalPrice *float64 `json:"original_price,omitempty"`
|
||||
Condition string `json:"condition,omitempty"`
|
||||
Location string `json:"location,omitempty"`
|
||||
Status string `json:"status"`
|
||||
ViewsCount int `json:"views_count"`
|
||||
FavoritesCount int `json:"favorites_count"`
|
||||
IsFavorited bool `json:"is_favorited"`
|
||||
Author *UserResponse `json:"author"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
UpdatedAt string `json:"updated_at"`
|
||||
}
|
||||
|
||||
type CreateTradeItemRequest struct {
|
||||
TradeType string `json:"trade_type" binding:"required,oneof=sell buy"`
|
||||
Category string `json:"category" binding:"required,oneof=digital book clothing daily cosmetics sport ticket other"`
|
||||
Title string `json:"title" binding:"required,max=100"`
|
||||
Content string `json:"content"`
|
||||
Segments model.MessageSegments `json:"segments"`
|
||||
Images []string `json:"images"`
|
||||
Price *float64 `json:"price"`
|
||||
OriginalPrice *float64 `json:"original_price"`
|
||||
Condition string `json:"condition" binding:"omitempty,oneof=brand_new like_new lightly_used well_used"`
|
||||
Location string `json:"location" binding:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
type UpdateTradeItemRequest struct {
|
||||
TradeType *string `json:"trade_type" binding:"omitempty,oneof=sell buy"`
|
||||
Category *string `json:"category" binding:"omitempty,oneof=digital book clothing daily cosmetics sport ticket other"`
|
||||
Title *string `json:"title" binding:"omitempty,max=100"`
|
||||
Content *string `json:"content"`
|
||||
Segments model.MessageSegments `json:"segments"`
|
||||
Images []string `json:"images"`
|
||||
Price *float64 `json:"price"`
|
||||
OriginalPrice *float64 `json:"original_price"`
|
||||
Condition *string `json:"condition" binding:"omitempty,oneof=brand_new like_new lightly_used well_used"`
|
||||
Location *string `json:"location" binding:"omitempty,max=100"`
|
||||
}
|
||||
|
||||
type UpdateTradeItemStatusRequest struct {
|
||||
Status string `json:"status" binding:"required,oneof=active sold bought offline"`
|
||||
}
|
||||
|
||||
type TradeCursorPageResponse struct {
|
||||
Items []*TradeItemResponse `json:"list"`
|
||||
NextCursor string `json:"next_cursor,omitempty"`
|
||||
PrevCursor string `json:"prev_cursor,omitempty"`
|
||||
HasMore bool `json:"has_more"`
|
||||
}
|
||||
|
||||
type TradeFavoriteResponse struct {
|
||||
Favorited bool `json:"favorited"`
|
||||
}
|
||||
220
internal/handler/trade_handler.go
Normal file
220
internal/handler/trade_handler.go
Normal file
@@ -0,0 +1,220 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"with_you/internal/dto"
|
||||
"with_you/internal/model"
|
||||
"with_you/internal/pkg/response"
|
||||
"with_you/internal/service"
|
||||
)
|
||||
|
||||
type TradeHandler struct {
|
||||
tradeService service.TradeService
|
||||
}
|
||||
|
||||
func NewTradeHandler(tradeService service.TradeService) *TradeHandler {
|
||||
return &TradeHandler{tradeService: tradeService}
|
||||
}
|
||||
|
||||
func (h *TradeHandler) List(c *gin.Context) {
|
||||
currentUserID := c.GetString("user_id")
|
||||
if currentUserID == "" {
|
||||
currentUserID = ""
|
||||
}
|
||||
|
||||
var tradeType *model.TradeType
|
||||
if v := c.Query("trade_type"); v != "" {
|
||||
t := model.TradeType(v)
|
||||
tradeType = &t
|
||||
}
|
||||
|
||||
var category *model.TradeCategory
|
||||
if v := c.Query("category"); v != "" {
|
||||
cat := model.TradeCategory(v)
|
||||
category = &cat
|
||||
}
|
||||
|
||||
cursorStr := c.Query("cursor")
|
||||
pageSize := 20
|
||||
if v := c.Query("page_size"); v != "" {
|
||||
if ps, err := strconv.Atoi(v); err == nil && ps > 0 && ps <= 100 {
|
||||
pageSize = ps
|
||||
}
|
||||
}
|
||||
|
||||
keyword := c.Query("keyword")
|
||||
|
||||
var currentUserIDPtr *string
|
||||
if currentUserID != "" {
|
||||
currentUserIDPtr = ¤tUserID
|
||||
}
|
||||
|
||||
var resp *dto.TradeCursorPageResponse
|
||||
var err error
|
||||
if keyword != "" {
|
||||
resp, err = h.tradeService.Search(c.Request.Context(), keyword, tradeType, category, cursorStr, pageSize, currentUserIDPtr)
|
||||
} else {
|
||||
resp, err = h.tradeService.List(c.Request.Context(), tradeType, category, cursorStr, pageSize, currentUserIDPtr)
|
||||
}
|
||||
if err != nil {
|
||||
response.HandleError(c, err, "获取交易列表失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *TradeHandler) GetByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
currentUserID := c.GetString("user_id")
|
||||
|
||||
var currentUserIDPtr *string
|
||||
if currentUserID != "" {
|
||||
currentUserIDPtr = ¤tUserID
|
||||
}
|
||||
|
||||
resp, err := h.tradeService.GetByID(c.Request.Context(), id, currentUserIDPtr)
|
||||
if err != nil {
|
||||
response.HandleError(c, err, "获取交易详情失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, resp)
|
||||
}
|
||||
|
||||
func (h *TradeHandler) Create(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.CreateTradeItemRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
item, err := h.tradeService.Create(c.Request.Context(), userID, req)
|
||||
if err != nil {
|
||||
response.HandleError(c, err, "创建交易失败")
|
||||
return
|
||||
}
|
||||
|
||||
var currentUserIDPtr *string
|
||||
if userID != "" {
|
||||
currentUserIDPtr = &userID
|
||||
}
|
||||
detail, err := h.tradeService.GetByID(c.Request.Context(), item.ID, currentUserIDPtr)
|
||||
if err != nil {
|
||||
response.Success(c, item)
|
||||
return
|
||||
}
|
||||
response.Success(c, detail)
|
||||
}
|
||||
|
||||
func (h *TradeHandler) Update(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var req dto.UpdateTradeItemRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.tradeService.Update(c.Request.Context(), userID, id, req); err != nil {
|
||||
response.HandleError(c, err, "更新交易失败")
|
||||
return
|
||||
}
|
||||
|
||||
var currentUserIDPtr *string
|
||||
if userID != "" {
|
||||
currentUserIDPtr = &userID
|
||||
}
|
||||
detail, err := h.tradeService.GetByID(c.Request.Context(), id, currentUserIDPtr)
|
||||
if err != nil {
|
||||
response.Success(c, nil)
|
||||
return
|
||||
}
|
||||
response.Success(c, detail)
|
||||
}
|
||||
|
||||
func (h *TradeHandler) Delete(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
if err := h.tradeService.Delete(c.Request.Context(), userID, id); err != nil {
|
||||
response.HandleError(c, err, "删除交易失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *TradeHandler) UpdateStatus(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
|
||||
var req dto.UpdateTradeItemStatusRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.tradeService.UpdateStatus(c.Request.Context(), userID, id, model.TradeItemStatus(req.Status)); err != nil {
|
||||
response.HandleError(c, err, "更新交易状态失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *TradeHandler) RecordView(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
_ = h.tradeService.RecordView(c.Request.Context(), id)
|
||||
response.Success(c, nil)
|
||||
}
|
||||
|
||||
func (h *TradeHandler) Favorite(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
if err := h.tradeService.Favorite(c.Request.Context(), userID, id); err != nil {
|
||||
response.HandleError(c, err, "收藏失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, dto.TradeFavoriteResponse{Favorited: true})
|
||||
}
|
||||
|
||||
func (h *TradeHandler) Unfavorite(c *gin.Context) {
|
||||
userID := c.GetString("user_id")
|
||||
if userID == "" {
|
||||
response.Unauthorized(c, "请先登录")
|
||||
return
|
||||
}
|
||||
|
||||
id := c.Param("id")
|
||||
if err := h.tradeService.Unfavorite(c.Request.Context(), userID, id); err != nil {
|
||||
response.HandleError(c, err, "取消收藏失败")
|
||||
return
|
||||
}
|
||||
response.Success(c, dto.TradeFavoriteResponse{Favorited: false})
|
||||
}
|
||||
@@ -179,6 +179,11 @@ func autoMigrate(db *gorm.DB) error {
|
||||
|
||||
// 帖子内链引用关系
|
||||
&PostReference{},
|
||||
|
||||
// 二手交易相关
|
||||
&TradeItem{},
|
||||
&TradeImage{},
|
||||
&TradeFavorite{},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
112
internal/model/trade.go
Normal file
112
internal/model/trade.go
Normal file
@@ -0,0 +1,112 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TradeType string
|
||||
|
||||
const (
|
||||
TradeTypeSell TradeType = "sell"
|
||||
TradeTypeBuy TradeType = "buy"
|
||||
)
|
||||
|
||||
type TradeCategory string
|
||||
|
||||
const (
|
||||
TradeCategoryDigital TradeCategory = "digital"
|
||||
TradeCategoryBook TradeCategory = "book"
|
||||
TradeCategoryClothing TradeCategory = "clothing"
|
||||
TradeCategoryDaily TradeCategory = "daily"
|
||||
TradeCategoryCosmetics TradeCategory = "cosmetics"
|
||||
TradeCategorySport TradeCategory = "sport"
|
||||
TradeCategoryTicket TradeCategory = "ticket"
|
||||
TradeCategoryOther TradeCategory = "other"
|
||||
)
|
||||
|
||||
type TradeItemStatus string
|
||||
|
||||
const (
|
||||
TradeItemActive TradeItemStatus = "active"
|
||||
TradeItemSold TradeItemStatus = "sold"
|
||||
TradeItemBought TradeItemStatus = "bought"
|
||||
TradeItemOffline TradeItemStatus = "offline"
|
||||
)
|
||||
|
||||
type TradeItem struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);index;not null"`
|
||||
TradeType TradeType `json:"trade_type" gorm:"type:varchar(10);not null;index"`
|
||||
Category TradeCategory `json:"category" gorm:"type:varchar(20);not null;index"`
|
||||
Title string `json:"title" gorm:"type:varchar(100);not null"`
|
||||
Content string `json:"content" gorm:"type:text"`
|
||||
Segments MessageSegments `json:"segments,omitempty" gorm:"type:text"`
|
||||
User *User `json:"user,omitempty" gorm:"foreignKey:UserID"`
|
||||
Images []TradeImage `json:"images" gorm:"foreignKey:TradeItemID"`
|
||||
Price *float64 `json:"price,omitempty" gorm:"type:decimal(10,2)"`
|
||||
OriginalPrice *float64 `json:"original_price,omitempty" gorm:"type:decimal(10,2)"`
|
||||
Condition string `json:"condition,omitempty" gorm:"type:varchar(20)"`
|
||||
Location string `json:"location,omitempty" gorm:"type:varchar(100)"`
|
||||
Status TradeItemStatus `json:"status" gorm:"type:varchar(10);not null;default:'active';index"`
|
||||
ViewsCount int `json:"views_count" gorm:"default:0"`
|
||||
FavoritesCount int `json:"favorites_count" gorm:"default:0"`
|
||||
DeletedAt gorm.DeletedAt `json:"-" gorm:"index"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index"`
|
||||
UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime:false"`
|
||||
}
|
||||
|
||||
func (t *TradeItem) BeforeCreate(tx *gorm.DB) error {
|
||||
if t.ID == "" {
|
||||
t.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (TradeItem) TableName() string {
|
||||
return "trade_items"
|
||||
}
|
||||
|
||||
type TradeImage struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
TradeItemID string `json:"trade_item_id" gorm:"type:varchar(36);not null;index"`
|
||||
URL string `json:"url" gorm:"type:text;not null"`
|
||||
ThumbnailURL string `json:"thumbnail_url" gorm:"type:text"`
|
||||
PreviewURL string `json:"preview_url" gorm:"type:text"`
|
||||
PreviewURLLarge string `json:"preview_url_large" gorm:"type:text"`
|
||||
Width int `json:"width" gorm:"default:0"`
|
||||
Height int `json:"height" gorm:"default:0"`
|
||||
SortOrder int `json:"sort_order" gorm:"default:0"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
func (ti *TradeImage) BeforeCreate(tx *gorm.DB) error {
|
||||
if ti.ID == "" {
|
||||
ti.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (TradeImage) TableName() string {
|
||||
return "trade_images"
|
||||
}
|
||||
|
||||
type TradeFavorite struct {
|
||||
ID string `json:"id" gorm:"type:varchar(36);primaryKey"`
|
||||
TradeItemID string `json:"trade_item_id" gorm:"type:varchar(36);not null;index;uniqueIndex:idx_trade_fav_item_user,priority:1"`
|
||||
UserID string `json:"user_id" gorm:"type:varchar(36);not null;index;uniqueIndex:idx_trade_fav_item_user,priority:2"`
|
||||
CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime"`
|
||||
}
|
||||
|
||||
func (f *TradeFavorite) BeforeCreate(tx *gorm.DB) error {
|
||||
if f.ID == "" {
|
||||
f.ID = uuid.New().String()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (TradeFavorite) TableName() string {
|
||||
return "trade_favorites"
|
||||
}
|
||||
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
|
||||
}
|
||||
@@ -43,6 +43,7 @@ type Router struct {
|
||||
adminVerificationHandler *handler.AdminVerificationHandler
|
||||
adminProfileAuditHandler *handler.AdminProfileAuditHandler
|
||||
setupHandler *handler.SetupHandler
|
||||
tradeHandler *handler.TradeHandler
|
||||
wsHandler *handler.WSHandler
|
||||
logService *service.LogService
|
||||
jwtService *service.JWTService
|
||||
@@ -81,8 +82,9 @@ func New(
|
||||
verificationHandler *handler.VerificationHandler,
|
||||
adminVerificationHandler *handler.AdminVerificationHandler,
|
||||
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
|
||||
setupHandler *handler.SetupHandler,
|
||||
logService *service.LogService,
|
||||
setupHandler *handler.SetupHandler,
|
||||
tradeHandler *handler.TradeHandler,
|
||||
logService *service.LogService,
|
||||
activityService service.UserActivityService,
|
||||
casbinService service.CasbinService,
|
||||
wsHandler *handler.WSHandler,
|
||||
@@ -124,6 +126,7 @@ func New(
|
||||
adminVerificationHandler: adminVerificationHandler,
|
||||
adminProfileAuditHandler: adminProfileAuditHandler,
|
||||
setupHandler: setupHandler,
|
||||
tradeHandler: tradeHandler,
|
||||
logService: logService,
|
||||
jwtService: jwtService,
|
||||
casbinService: casbinService,
|
||||
@@ -311,6 +314,22 @@ func (r *Router) setupRoutes() {
|
||||
}
|
||||
}
|
||||
|
||||
// 二手交易路由
|
||||
if r.tradeHandler != nil {
|
||||
trade := v1.Group("/trade")
|
||||
{
|
||||
trade.GET("", middleware.OptionalAuth(r.jwtService), r.tradeHandler.List)
|
||||
trade.GET("/:id", middleware.OptionalAuth(r.jwtService), r.tradeHandler.GetByID)
|
||||
trade.POST("", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Create)
|
||||
trade.PUT("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Update)
|
||||
trade.DELETE("/:id", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Delete)
|
||||
trade.PUT("/:id/status", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.UpdateStatus)
|
||||
trade.POST("/:id/view", middleware.OptionalAuth(r.jwtService), r.tradeHandler.RecordView)
|
||||
trade.POST("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Favorite)
|
||||
trade.DELETE("/:id/favorite", authMiddleware, middleware.RequireVerified(r.userRepo), r.tradeHandler.Unfavorite)
|
||||
}
|
||||
}
|
||||
|
||||
// 课表路由
|
||||
if r.scheduleHandler != nil {
|
||||
schedule := v1.Group("/schedule")
|
||||
|
||||
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
|
||||
}
|
||||
@@ -34,6 +34,7 @@ var HandlerSet = wire.NewSet(
|
||||
handler.NewVerificationHandler,
|
||||
handler.NewAdminProfileAuditHandler,
|
||||
handler.NewPostHandler,
|
||||
handler.NewTradeHandler,
|
||||
|
||||
// 需要特殊处理的 Handler
|
||||
ProvideUserHandler,
|
||||
|
||||
@@ -46,6 +46,9 @@ var RepositorySet = wire.NewSet(
|
||||
|
||||
// 帖子内链引用关系
|
||||
repository.NewPostRefRepository,
|
||||
|
||||
// 二手交易相关
|
||||
repository.NewTradeRepository,
|
||||
)
|
||||
|
||||
// ProvideUserActivityRepository 提供用户活跃数据仓储
|
||||
|
||||
@@ -68,6 +68,9 @@ var ServiceSet = wire.NewSet(
|
||||
// 帖子内链引用
|
||||
ProvidePostRefService,
|
||||
|
||||
// 二手交易
|
||||
ProvideTradeService,
|
||||
|
||||
// 日志服务
|
||||
ProvideAsyncLogManager,
|
||||
ProvideOperationLogService,
|
||||
@@ -496,3 +499,7 @@ func ProvidePostRefService(
|
||||
) service.PostRefService {
|
||||
return service.NewPostRefService(refRepo, postRepo)
|
||||
}
|
||||
|
||||
func ProvideTradeService(tradeRepo repository.TradeRepository) service.TradeService {
|
||||
return service.NewTradeService(tradeRepo)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user