feat(trade): implement second-hand trading/flea market feature #2
396
ARCHITECTURE.md
396
ARCHITECTURE.md
@@ -1,396 +0,0 @@
|
|||||||
# 后端架构设计规范
|
|
||||||
|
|
||||||
## 1. 分层架构原则
|
|
||||||
|
|
||||||
### 层级定义
|
|
||||||
```
|
|
||||||
Handler (控制器层) → Service (业务逻辑层) → Repository (数据访问层) → Infrastructure (基础设施层)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 层级职责
|
|
||||||
|
|
||||||
| 层级 | 职责 | 依赖关系 |
|
|
||||||
|------|------|----------|
|
|
||||||
| Handler | 接收HTTP请求、参数校验、调用Service、返回响应 | 仅依赖Service |
|
|
||||||
| Service | 实现业务逻辑、事务管理、组合多个Repository | 仅依赖Repository接口 |
|
|
||||||
| Repository | 数据CRUD操作、数据库事务管理 | 仅依赖Model和GORM |
|
|
||||||
| Infrastructure | 缓存、消息队列、RPC等外部服务 | 被Service调用 |
|
|
||||||
|
|
||||||
### 关键约束
|
|
||||||
- **Handler禁止直接依赖Repository或直接操作数据库**
|
|
||||||
- **Service禁止直接依赖`gorm.DB`,必须通过Repository间接操作**
|
|
||||||
- **所有外部依赖通过接口抽象**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. Repository 层规范
|
|
||||||
|
|
||||||
### 2.1 接口 + 私有实现模式
|
|
||||||
|
|
||||||
所有Repository必须采用接口定义 + 私有实现结构体的模式:
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确:接口 + 私有实现
|
|
||||||
type UserRepository interface {
|
|
||||||
Create(user *model.User) error
|
|
||||||
GetByID(id string) (*model.User, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type userRepository struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
|
||||||
return &userRepository{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *userRepository) Create(user *model.User) error {
|
|
||||||
return r.db.Create(user).Error
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ❌ 错误:直接暴露结构体指针
|
|
||||||
type UserRepository struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
|
||||||
return &UserRepository{db: db}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2.2 Repository方法设计原则
|
|
||||||
|
|
||||||
- 方法签名中**使用接口类型而非指针**:`func (r *userRepository) GetByID(...) (*model.User, error)`
|
|
||||||
- **避免在Repository层直接返回指针切片**,使用值切片更安全
|
|
||||||
- **事务操作在Repository内部封装**,Service不应直接调用`db.Transaction()`
|
|
||||||
|
|
||||||
### 2.3 新增Repository流程
|
|
||||||
|
|
||||||
1. 在`repository/`目录下创建`xxx_repo.go`文件
|
|
||||||
2. 定义`XxxRepository`接口,包含所有数据访问方法
|
|
||||||
3. 创建私有实现结构体`xxxRepository`
|
|
||||||
4. 创建构造函数`NewXxxRepository(db *gorm.DB) XxxRepository`
|
|
||||||
5. 在`wire/repository.go`的`RepositorySet`中注册
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. Service 层规范
|
|
||||||
|
|
||||||
### 3.1 接口 + 私有实现模式
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确
|
|
||||||
type UserService interface {
|
|
||||||
CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*model.User, error)
|
|
||||||
GetUserByID(ctx context.Context, id string) (*model.User, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type userService struct {
|
|
||||||
userRepo repository.UserRepository
|
|
||||||
// 其他Repo都使用接口类型
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserService(userRepo repository.UserRepository) UserService {
|
|
||||||
return &userService{userRepo: userRepo}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3.2 Service依赖管理
|
|
||||||
|
|
||||||
- **所有Repository依赖必须使用接口类型**,禁止使用`*repository.XxxRepository`指针
|
|
||||||
- **禁止在Service中直接持有`gorm.DB`**,如有数据库操作必须下沉到Repository
|
|
||||||
- **如需事务支持**,在Repository中封装事务方法,Service调用封装好的方法
|
|
||||||
|
|
||||||
### 3.3 Service间依赖
|
|
||||||
|
|
||||||
- Service之间通过接口依赖,避免循环依赖
|
|
||||||
- 如Service A需要调用Service B的方法,确保该方法在接口中定义
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. Handler 层规范
|
|
||||||
|
|
||||||
### 4.1 Handler只依赖Service
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确
|
|
||||||
type UserHandler struct {
|
|
||||||
userService service.UserService // 仅依赖Service接口
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUserHandler(userService service.UserService) *UserHandler {
|
|
||||||
return &UserHandler{userService: userService}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ❌ 错误:直接依赖Repository或Cache
|
|
||||||
type UserHandler struct {
|
|
||||||
userRepo *repository.UserRepository // 禁止
|
|
||||||
cache cache.Cache // 禁止
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4.2 参数和类型分离
|
|
||||||
|
|
||||||
- **Query参数和Filter类型**应定义在`dto/`包中,而非`repository/`包
|
|
||||||
- Handler负责将HTTP参数转换为DTO后传递给Service
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. DTO/Filter 类型规范
|
|
||||||
|
|
||||||
### 5.1 放置位置
|
|
||||||
|
|
||||||
- **所有查询参数、过滤条件、分页参数**定义在`internal/dto/`目录下
|
|
||||||
- **禁止在`repository/`包中定义供Handler使用的Filter类型**
|
|
||||||
|
|
||||||
### 5.2 文件组织
|
|
||||||
|
|
||||||
```
|
|
||||||
internal/dto/
|
|
||||||
├── filter.go # 通用过滤条件(LogFilter, LoginFilter等)
|
|
||||||
├── user_converter.go # 用户相关转换
|
|
||||||
└── ...
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5.3 Filter类型定义示例
|
|
||||||
|
|
||||||
```go
|
|
||||||
// internal/dto/filter.go
|
|
||||||
|
|
||||||
type LogFilter struct {
|
|
||||||
UserID string
|
|
||||||
Operation string
|
|
||||||
TargetType string
|
|
||||||
StartTime string
|
|
||||||
EndTime string
|
|
||||||
}
|
|
||||||
|
|
||||||
type PaginationParams struct {
|
|
||||||
Page int
|
|
||||||
PageSize int
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. Wire 依赖注入规范
|
|
||||||
|
|
||||||
### 6.1 Provider函数设计
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确:Provider返回接口类型
|
|
||||||
func ProvideUserService(
|
|
||||||
userRepo repository.UserRepository, // 接口类型
|
|
||||||
cacheBackend cache.Cache,
|
|
||||||
) service.UserService {
|
|
||||||
return service.NewUserService(userRepo, cacheBackend)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ❌ 错误:返回指针类型
|
|
||||||
func ProvideUserService(userRepo *repository.UserRepository) *service.UserService
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.2 NewXxxService构造函数签名
|
|
||||||
|
|
||||||
- **构造函数参数使用接口类型**
|
|
||||||
- **返回值使用接口类型**
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确
|
|
||||||
func NewUserService(
|
|
||||||
userRepo repository.UserRepository, // 接口
|
|
||||||
cache cache.Cache,
|
|
||||||
) UserService { // 返回接口
|
|
||||||
return &userService{userRepo: userRepo}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6.3 禁止的直接注入
|
|
||||||
|
|
||||||
- **禁止在Handler的Provider中直接注入Repository和Cache**,应仅注入Service
|
|
||||||
- **禁止在Service的Provider中直接注入`gorm.DB`**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 事务管理规范
|
|
||||||
|
|
||||||
### 7.1 事务边界在Repository层
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ 正确:事务封装在Repository内部
|
|
||||||
func (r *userRepository) CreateUserWithProfile(user *model.User, profile *model.Profile) error {
|
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
|
||||||
if err := tx.Create(user).Error; err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
profile.UserID = user.ID
|
|
||||||
return tx.Create(profile).Error
|
|
||||||
})
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 7.2 Service调用事务方法
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ✅ Service通过调用Repository的封装方法使用事务
|
|
||||||
func (s *userService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) error {
|
|
||||||
user := &model.User{Name: req.Name}
|
|
||||||
profile := &model.Profile{Bio: req.Bio}
|
|
||||||
return s.userRepo.CreateUserWithProfile(user, profile)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 常见错误示例
|
|
||||||
|
|
||||||
### 错误1:Handler直接依赖Repository
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ❌ 错误
|
|
||||||
type PostHandler struct {
|
|
||||||
postRepo *repository.PostRepository
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 正确
|
|
||||||
type PostHandler struct {
|
|
||||||
postService service.PostService
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 错误2:Service直接使用gorm.DB
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ❌ 错误
|
|
||||||
type postService struct {
|
|
||||||
db *gorm.DB
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *postService) CreatePost(post *model.Post) error {
|
|
||||||
return s.db.Create(post).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 正确:使用Repository
|
|
||||||
func (s *postService) CreatePost(post *model.Post) error {
|
|
||||||
return s.postRepo.Create(post)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 错误3:Repository返回指针类型
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ❌ 错误
|
|
||||||
func NewUserRepository(db *gorm.DB) *UserRepository {
|
|
||||||
return &UserRepository{db: db}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 正确
|
|
||||||
func NewUserRepository(db *gorm.DB) UserRepository {
|
|
||||||
return &userRepository{db: db}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 错误4:Filter类型放在repository包
|
|
||||||
|
|
||||||
```go
|
|
||||||
// ❌ 错误:repository包不应该被handler直接引用
|
|
||||||
package repository
|
|
||||||
type LogFilter struct {
|
|
||||||
UserID string
|
|
||||||
}
|
|
||||||
|
|
||||||
// ✅ 正确:放在dto包
|
|
||||||
package dto
|
|
||||||
type LogFilter struct {
|
|
||||||
UserID string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 9. 新功能开发 Checklist
|
|
||||||
|
|
||||||
开发新功能时,确保满足以下规范:
|
|
||||||
|
|
||||||
- [ ] **Repository定义**
|
|
||||||
- [ ] 接口 + 私有实现结构体
|
|
||||||
- [ ] 构造函数返回接口类型
|
|
||||||
- [ ] 方法参数使用值类型而非指针
|
|
||||||
- [ ] 在`wire/repository.go`中注册
|
|
||||||
|
|
||||||
- [ ] **Service定义**
|
|
||||||
- [ ] 接口 + 私有实现结构体
|
|
||||||
- [ ] 所有Repository依赖使用接口类型
|
|
||||||
- [ ] 不直接持有`gorm.DB`
|
|
||||||
- [ ] 事务操作下沉到Repository
|
|
||||||
|
|
||||||
- [ ] **Handler定义**
|
|
||||||
- [ ] 仅依赖Service接口
|
|
||||||
- [ ] 不直接依赖Repository或Cache
|
|
||||||
- [ ] Query/Filter参数使用dto包类型
|
|
||||||
|
|
||||||
- [ ] **Wire Provider**
|
|
||||||
- [ ] 参数使用接口类型
|
|
||||||
- [ ] 返回值使用接口类型
|
|
||||||
- [ ] 不直接注入`gorm.DB`到Service
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 10. 相关文件位置参考
|
|
||||||
|
|
||||||
| 类别 | 目录 |
|
|
||||||
|------|------|
|
|
||||||
| Handler | `internal/handler/` |
|
|
||||||
| Service | `internal/service/` |
|
|
||||||
| Repository | `internal/repository/` |
|
|
||||||
| DTO/Filter | `internal/dto/` |
|
|
||||||
| Model | `internal/model/` |
|
|
||||||
| Wire | `internal/wire/` |
|
|
||||||
| Cache | `internal/cache/` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 11. Wire 生成的代码规范
|
|
||||||
|
|
||||||
### 11.1 禁止直接修改生成的文件
|
|
||||||
|
|
||||||
以下文件由 Wire 工具自动生成,**禁止直接修改**:
|
|
||||||
|
|
||||||
- `cmd/server/wire_gen.go`
|
|
||||||
- `cmd/server/wire.go` 中的 Provider 函数签名由 Wire 维护
|
|
||||||
|
|
||||||
如果需要修改这些文件:
|
|
||||||
1. **修改源文件**(如 `wire.go` 中的结构)
|
|
||||||
2. **运行 `wire` 命令**重新生成
|
|
||||||
|
|
||||||
### 11.2 正确的修改流程
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 修改 wire.go 或 wire/*.go
|
|
||||||
# 2. 运行 wire 重新生成
|
|
||||||
go run -mod=mod github.com/google/wire/cmd/wire ./cmd/server
|
|
||||||
```
|
|
||||||
|
|
||||||
### 11.3 Provider 依赖添加
|
|
||||||
|
|
||||||
新增依赖时,必须同步修改:
|
|
||||||
|
|
||||||
1. `wire/service.go` 中的 Provider 函数
|
|
||||||
2. `wire/handler.go` 中的 Handler Provider 函数
|
|
||||||
3. `cmd/server/wire.go` 中的 `ProvideRouter` 函数
|
|
||||||
4. `wire/repository.go`(如需新增 Repository)
|
|
||||||
5. 重新运行 `wire ./cmd/server` 生成 `wire_gen.go`
|
|
||||||
|
|
||||||
### 11.4 生成文件识别
|
|
||||||
|
|
||||||
```
|
|
||||||
// Code generated by Wire. DO NOT EDIT.
|
|
||||||
//go:generate go run -mod=mod github.com/google/wire/cmd/wire
|
|
||||||
//go:build !wireinject
|
|
||||||
```
|
|
||||||
|
|
||||||
带以上标记的文件均为自动生成文件。
|
|
||||||
@@ -143,7 +143,10 @@ func InitializeApp() (*App, error) {
|
|||||||
setupService := wire.ProvideSetupService(userRepository, roleRepository, casbinService, config)
|
setupService := wire.ProvideSetupService(userRepository, roleRepository, casbinService, config)
|
||||||
setupHandler := wire.ProvideSetupHandler(setupService)
|
setupHandler := wire.ProvideSetupHandler(setupService)
|
||||||
wsHandler := wire.ProvideWSHandler(hub, chatService, groupService, jwtService, callService, userRepository)
|
wsHandler := wire.ProvideWSHandler(hub, chatService, groupService, jwtService, callService, userRepository)
|
||||||
router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, setupHandler, logService, userActivityService, casbinService, wsHandler)
|
tradeRepository := repository.NewTradeRepository(db)
|
||||||
|
tradeService := wire.ProvideTradeService(tradeRepository)
|
||||||
|
tradeHandler := handler.NewTradeHandler(tradeService)
|
||||||
|
router := ProvideRouter(userRepository, userHandler, postHandler, commentHandler, messageHandler, notificationHandler, uploadHandler, jwtService, pushHandler, systemMessageHandler, groupHandler, stickerHandler, voteHandler, channelHandler, scheduleHandler, roleHandler, adminUserHandler, adminPostHandler, adminCommentHandler, adminGroupHandler, adminDashboardHandler, adminLogHandler, qrCodeHandler, materialHandler, callHandler, reportHandler, adminReportHandler, verificationHandler, adminVerificationHandler, adminProfileAuditHandler, setupHandler, tradeHandler, logService, userActivityService, casbinService, wsHandler)
|
||||||
hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, channelRepository, cache)
|
hotRankWorker := wire.ProvideHotRankWorker(config, postRepository, channelRepository, cache)
|
||||||
app := NewApp(config, db, router, pushService, hotRankWorker, server, logger)
|
app := NewApp(config, db, router, pushService, hotRankWorker, server, logger)
|
||||||
return app, nil
|
return app, nil
|
||||||
@@ -184,6 +187,7 @@ func ProvideRouter(
|
|||||||
adminVerificationHandler *handler.AdminVerificationHandler,
|
adminVerificationHandler *handler.AdminVerificationHandler,
|
||||||
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
|
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
|
||||||
setupHandler *handler.SetupHandler,
|
setupHandler *handler.SetupHandler,
|
||||||
|
tradeHandler *handler.TradeHandler,
|
||||||
logService *service.LogService,
|
logService *service.LogService,
|
||||||
activityService service.UserActivityService,
|
activityService service.UserActivityService,
|
||||||
casbinService service.CasbinService,
|
casbinService service.CasbinService,
|
||||||
@@ -221,6 +225,7 @@ func ProvideRouter(
|
|||||||
adminVerificationHandler,
|
adminVerificationHandler,
|
||||||
adminProfileAuditHandler,
|
adminProfileAuditHandler,
|
||||||
setupHandler,
|
setupHandler,
|
||||||
|
tradeHandler,
|
||||||
logService,
|
logService,
|
||||||
activityService,
|
activityService,
|
||||||
casbinService,
|
casbinService,
|
||||||
|
|||||||
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{},
|
&PostReference{},
|
||||||
|
|
||||||
|
// 二手交易相关
|
||||||
|
&TradeItem{},
|
||||||
|
&TradeImage{},
|
||||||
|
&TradeFavorite{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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
|
adminVerificationHandler *handler.AdminVerificationHandler
|
||||||
adminProfileAuditHandler *handler.AdminProfileAuditHandler
|
adminProfileAuditHandler *handler.AdminProfileAuditHandler
|
||||||
setupHandler *handler.SetupHandler
|
setupHandler *handler.SetupHandler
|
||||||
|
tradeHandler *handler.TradeHandler
|
||||||
wsHandler *handler.WSHandler
|
wsHandler *handler.WSHandler
|
||||||
logService *service.LogService
|
logService *service.LogService
|
||||||
jwtService *service.JWTService
|
jwtService *service.JWTService
|
||||||
@@ -82,6 +83,7 @@ func New(
|
|||||||
adminVerificationHandler *handler.AdminVerificationHandler,
|
adminVerificationHandler *handler.AdminVerificationHandler,
|
||||||
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
|
adminProfileAuditHandler *handler.AdminProfileAuditHandler,
|
||||||
setupHandler *handler.SetupHandler,
|
setupHandler *handler.SetupHandler,
|
||||||
|
tradeHandler *handler.TradeHandler,
|
||||||
logService *service.LogService,
|
logService *service.LogService,
|
||||||
activityService service.UserActivityService,
|
activityService service.UserActivityService,
|
||||||
casbinService service.CasbinService,
|
casbinService service.CasbinService,
|
||||||
@@ -124,6 +126,7 @@ func New(
|
|||||||
adminVerificationHandler: adminVerificationHandler,
|
adminVerificationHandler: adminVerificationHandler,
|
||||||
adminProfileAuditHandler: adminProfileAuditHandler,
|
adminProfileAuditHandler: adminProfileAuditHandler,
|
||||||
setupHandler: setupHandler,
|
setupHandler: setupHandler,
|
||||||
|
tradeHandler: tradeHandler,
|
||||||
logService: logService,
|
logService: logService,
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
casbinService: casbinService,
|
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 {
|
if r.scheduleHandler != nil {
|
||||||
schedule := v1.Group("/schedule")
|
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.NewVerificationHandler,
|
||||||
handler.NewAdminProfileAuditHandler,
|
handler.NewAdminProfileAuditHandler,
|
||||||
handler.NewPostHandler,
|
handler.NewPostHandler,
|
||||||
|
handler.NewTradeHandler,
|
||||||
|
|
||||||
// 需要特殊处理的 Handler
|
// 需要特殊处理的 Handler
|
||||||
ProvideUserHandler,
|
ProvideUserHandler,
|
||||||
|
|||||||
@@ -46,6 +46,9 @@ var RepositorySet = wire.NewSet(
|
|||||||
|
|
||||||
// 帖子内链引用关系
|
// 帖子内链引用关系
|
||||||
repository.NewPostRefRepository,
|
repository.NewPostRefRepository,
|
||||||
|
|
||||||
|
// 二手交易相关
|
||||||
|
repository.NewTradeRepository,
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProvideUserActivityRepository 提供用户活跃数据仓储
|
// ProvideUserActivityRepository 提供用户活跃数据仓储
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ var ServiceSet = wire.NewSet(
|
|||||||
// 帖子内链引用
|
// 帖子内链引用
|
||||||
ProvidePostRefService,
|
ProvidePostRefService,
|
||||||
|
|
||||||
|
// 二手交易
|
||||||
|
ProvideTradeService,
|
||||||
|
|
||||||
// 日志服务
|
// 日志服务
|
||||||
ProvideAsyncLogManager,
|
ProvideAsyncLogManager,
|
||||||
ProvideOperationLogService,
|
ProvideOperationLogService,
|
||||||
@@ -496,3 +499,7 @@ func ProvidePostRefService(
|
|||||||
) service.PostRefService {
|
) service.PostRefService {
|
||||||
return service.NewPostRefService(refRepo, postRepo)
|
return service.NewPostRefService(refRepo, postRepo)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ProvideTradeService(tradeRepo repository.TradeRepository) service.TradeService {
|
||||||
|
return service.NewTradeService(tradeRepo)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user