diff --git a/README.md b/README.md index 6ba1eac..b3d156a 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,7 @@ npm run dev - 后台仪表盘已接入真实统计数据,页面为 `http://localhost:5173/admin/dashboard`。 - 用户管理后台已接入,页面为 `http://localhost:5173/admin/users`,支持冻结和解冻用户。 - 订单管理后台已接入,页面为 `http://localhost:5173/admin/orders`,支持查看全量订单和交接记录。 +- 商品管理后台已接入,页面为 `http://localhost:5173/admin/listings`,支持查看全量商品和状态筛选。 - 商品审核后台已接入,页面为 `http://localhost:5173/admin/listings/review`。 - 审计日志后台已接入,页面为 `http://localhost:5173/admin/audit-logs`,支持查看高风险操作明细。 diff --git a/backend/internal/modules/listing/dto.go b/backend/internal/modules/listing/dto.go index 11aaf68..7c6312a 100644 --- a/backend/internal/modules/listing/dto.go +++ b/backend/internal/modules/listing/dto.go @@ -6,6 +6,8 @@ type ListingDTO struct { ID uint64 `json:"id"` AccountID uint64 `json:"account_id"` OwnerID uint64 `json:"owner_id"` + OwnerPhone string `json:"owner_phone,omitempty"` + OwnerNickname string `json:"owner_nickname,omitempty"` Title string `json:"title"` Description string `json:"description"` GameName string `json:"game_name"` @@ -47,3 +49,10 @@ type UpdateRequest = CreateRequest type ReviewRequest struct { Reason string `json:"reason"` } + +type AdminListQuery struct { + OwnerID uint64 + Status string + ReviewStatus string + Limit int +} diff --git a/backend/internal/modules/listing/handler.go b/backend/internal/modules/listing/handler.go index b80faee..8a4c228 100644 --- a/backend/internal/modules/listing/handler.go +++ b/backend/internal/modules/listing/handler.go @@ -88,6 +88,19 @@ func (h *Handler) ListPendingReview(c *gin.Context) { response.OK(c, gin.H{"items": items}) } +func (h *Handler) ListAdmin(c *gin.Context) { + query, ok := parseAdminListQuery(c) + if !ok { + return + } + items, err := h.service.ListAdmin(query) + if err != nil { + writeListingError(c, err) + return + } + response.OK(c, gin.H{"items": items}) +} + func (h *Handler) Approve(c *gin.Context) { id, ok := parseID(c) if !ok { @@ -208,6 +221,29 @@ func parseID(c *gin.Context) (uint64, bool) { return id, true } +func parseAdminListQuery(c *gin.Context) (AdminListQuery, bool) { + var query AdminListQuery + if raw := c.Query("owner_id"); raw != "" { + value, err := strconv.ParseUint(raw, 10, 64) + if err != nil || value == 0 { + response.BadRequest(c, "号主 ID 不正确") + return query, false + } + query.OwnerID = value + } + if raw := c.Query("limit"); raw != "" { + value, err := strconv.Atoi(raw) + if err != nil || value <= 0 { + response.BadRequest(c, "查询条数不正确") + return query, false + } + query.Limit = value + } + query.Status = c.Query("status") + query.ReviewStatus = c.Query("review_status") + return query, true +} + func writeListingError(c *gin.Context, err error) { switch { case errors.Is(err, ErrDependencyUnavailable): diff --git a/backend/internal/modules/listing/repository.go b/backend/internal/modules/listing/repository.go index 7859e3b..1c4d70a 100644 --- a/backend/internal/modules/listing/repository.go +++ b/backend/internal/modules/listing/repository.go @@ -137,6 +137,31 @@ func (r *Repository) ListPendingReview() ([]ListingDTO, error) { return rowsToDTO(rows), nil } +func (r *Repository) ListAdmin(query AdminListQuery) ([]ListingDTO, error) { + limit := query.Limit + if limit <= 0 || limit > 500 { + limit = 200 + } + + db := r.baseQuery() + if query.OwnerID > 0 { + db = db.Where("l.owner_id = ?", query.OwnerID) + } + if query.Status != "" { + db = db.Where("l.status = ?", query.Status) + } + if query.ReviewStatus != "" { + db = db.Where("l.review_status = ?", query.ReviewStatus) + } + + var rows []listingRow + err := db.Order("l.id DESC").Limit(limit).Scan(&rows).Error + if err != nil { + return nil, err + } + return rowsToDTO(rows), nil +} + func (r *Repository) Approve(listingID uint64) (*ListingDTO, error) { var dto *ListingDTO err := r.db.Transaction(func(tx *gorm.DB) error { @@ -291,8 +316,10 @@ func (r *Repository) findDTO(where string, args ...any) (*ListingDTO, error) { func (r *Repository) baseQuery() *gorm.DB { return r.db.Table("rental_listings AS l"). - Select("l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level, a.haf_coin_amount"). - Joins("JOIN game_accounts AS a ON a.id = l.account_id") + Select(`l.*, a.title, a.description, a.game_name, a.server_region, a.login_platform, a.rank_level, + a.haf_coin_amount, COALESCE(u.phone, '') AS owner_phone, COALESCE(u.nickname, '') AS owner_nickname`). + Joins("JOIN game_accounts AS a ON a.id = l.account_id"). + Joins("LEFT JOIN users AS u ON u.id = l.owner_id") } func (r *Repository) findForReviewUpdate(tx *gorm.DB, listingID uint64) (*model.RentalListing, *model.GameAccount, error) { @@ -310,6 +337,8 @@ func (r *Repository) findForReviewUpdate(tx *gorm.DB, listingID uint64) (*model. type listingRow struct { model.RentalListing Title string + OwnerPhone string + OwnerNickname string Description string GameName string ServerRegion string @@ -331,6 +360,8 @@ func (row listingRow) toDTO() ListingDTO { ID: row.ID, AccountID: row.AccountID, OwnerID: row.OwnerID, + OwnerPhone: row.OwnerPhone, + OwnerNickname: row.OwnerNickname, Title: row.Title, Description: row.Description, GameName: row.GameName, diff --git a/backend/internal/modules/listing/service.go b/backend/internal/modules/listing/service.go index 5d81caa..5012d31 100644 --- a/backend/internal/modules/listing/service.go +++ b/backend/internal/modules/listing/service.go @@ -52,6 +52,13 @@ func (s *Service) ListPendingReview() ([]ListingDTO, error) { return s.repo.ListPendingReview() } +func (s *Service) ListAdmin(query AdminListQuery) ([]ListingDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + return s.repo.ListAdmin(query) +} + func (s *Service) Approve(id uint64) (*ListingDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 6fc3b7e..d1ed720 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -197,6 +197,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { adminRoutes.GET("/orders", orderHandler.AdminList) adminRoutes.GET("/orders/:id", orderHandler.AdminDetail) adminRoutes.GET("/orders/:id/handoff-records", orderHandler.AdminHandoffRecords) + adminRoutes.GET("/listings", listingHandler.ListAdmin) adminRoutes.GET("/listings/pending", listingHandler.ListPendingReview) adminRoutes.POST("/listings/:id/approve", listingHandler.Approve) adminRoutes.POST("/listings/:id/reject", listingHandler.Reject) diff --git a/docs/api.md b/docs/api.md index 6c61ba4..7908cc8 100644 --- a/docs/api.md +++ b/docs/api.md @@ -57,6 +57,7 @@ API 规划以 [项目计划](project-plan.md) 第 10 章为准。 - `GET /api/admin/orders` - `GET /api/admin/orders/{id}` - `GET /api/admin/orders/{id}/handoff-records` +- `GET /api/admin/listings` - `GET /api/admin/listings/pending` - `POST /api/admin/listings/{id}/approve` - `POST /api/admin/listings/{id}/reject` @@ -67,4 +68,4 @@ API 规划以 [项目计划](project-plan.md) 第 10 章为准。 - `PUT /api/admin/system-configs/{key}` - `GET /api/admin/audit-logs` -说明:`GET /api/admin/wallet/ledger` 支持按 `user_id`、`order_id`、`biz_type` 和 `limit` 查询最近资金流水。`GET /api/admin/audit-logs` 支持按 `actor_id`、`action`、`biz_type` 和 `limit` 查询最近审计日志。`/api/admin/*` 当前已使用独立后台登录,后续接入 RBAC 和 Casbin 权限后再按角色收紧访问控制。 +说明:`GET /api/admin/listings` 支持按 `owner_id`、`status`、`review_status` 和 `limit` 查询商品。`GET /api/admin/wallet/ledger` 支持按 `user_id`、`order_id`、`biz_type` 和 `limit` 查询最近资金流水。`GET /api/admin/audit-logs` 支持按 `actor_id`、`action`、`biz_type` 和 `limit` 查询最近审计日志。`/api/admin/*` 当前已使用独立后台登录,后续接入 RBAC 和 Casbin 权限后再按角色收紧访问控制。 diff --git a/docs/business-rules.md b/docs/business-rules.md index 49ff211..c8b1e4a 100644 --- a/docs/business-rules.md +++ b/docs/business-rules.md @@ -129,6 +129,13 @@ - 订单详情页 `/admin/orders/:id` 展示订单状态、双方用户、租期、交接记录和账号资产快照。 - 当前后台订单管理先做只读能力,后续再补后台关闭订单、标记异常和客服介入操作。 +## 开发态商品管理 + +- 商品管理接口为 `/api/admin/listings`,前端页面为 `/admin/listings`。 +- 后台可查看全量商品、号主、账号 ID、区服、平台、段位、哈夫币、时租、押金、商品状态和审核状态。 +- 当前支持按号主 ID、商品状态、审核状态和查询条数筛选。 +- 商品管理先做只读能力,强制下架、标记异常和后台改价等高风险操作后续接 RBAC 和审计后再做。 + ## 开发态审计日志 - 审计日志接口为 `/api/admin/audit-logs`,前端页面为 `/admin/audit-logs`。 diff --git a/frontend/src/api/listings.ts b/frontend/src/api/listings.ts index 7441bea..de3f3c2 100644 --- a/frontend/src/api/listings.ts +++ b/frontend/src/api/listings.ts @@ -4,6 +4,8 @@ export interface Listing { id: number account_id: number owner_id: number + owner_phone?: string + owner_nickname?: string title: string description: string game_name: string @@ -81,6 +83,19 @@ export async function fetchPendingReviewListings() { return data.data.items } +export interface AdminListingQuery { + owner_id?: string + status?: string + review_status?: string + limit?: number +} + +export async function fetchAdminListings(query: AdminListingQuery = {}) { + const params = Object.fromEntries(Object.entries(query).filter(([, value]) => value !== '' && value !== undefined)) + const { data } = await apiClient.get>('/admin/listings', { params }) + return data.data.items +} + export async function approveListing(id: number) { const { data } = await apiClient.post>(`/admin/listings/${id}/approve`) return data.data diff --git a/frontend/src/layouts/AdminLayout.vue b/frontend/src/layouts/AdminLayout.vue index 9b594db..01734bc 100644 --- a/frontend/src/layouts/AdminLayout.vue +++ b/frontend/src/layouts/AdminLayout.vue @@ -1,5 +1,5 @@ + +