feat(admin): 优化商品审核工作台界面、截图凭证核验与按住速览体验
- 深度优化商品审核工作台:指标概览卡片、风险边条队列、标价管道联动、核心资产高密度展示、底部决策浮动 Dock、全套键盘快捷键 - 重构截图大图查看体验:移除旧版底部溢出的全局画廊,封装全屏居中弹窗 AdminListingReviewPreviewDialog,支持「按住速览(松手即关闭)」与「常驻弹窗(缩放/翻页/打码/原图)」双模交互 - 新增在线截图编辑器 ImageEditorDialog:支持平滑马赛克打码、自由裁剪与旋转保存 - 弹窗组件解耦:抽离审核拒绝、调价、全量核验、快捷键速查等子组件,严格控制单文件行数 - 后端支持:新增商品审核调整价格接口与配套单元测试
This commit is contained in:
@@ -93,6 +93,11 @@ type TransferOwnerRequest struct {
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
|
||||
type AdminUpdateScreenshotsRequest struct {
|
||||
ScreenshotURLs []string `json:"screenshot_urls" binding:"required,min=1"`
|
||||
ScreenshotGroups map[string]any `json:"screenshot_groups"`
|
||||
}
|
||||
|
||||
type AdminListQuery struct {
|
||||
OwnerID uint64
|
||||
Keyword string
|
||||
|
||||
@@ -182,3 +182,27 @@ func (h *Handler) Reject(c *gin.Context) {
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
func (h *Handler) UpdateScreenshots(c *gin.Context) {
|
||||
adminID, ok := currentAdminID(c)
|
||||
if !ok {
|
||||
response.Unauthorized(c, "缺少管理员上下文")
|
||||
return
|
||||
}
|
||||
id, ok := parseID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var req AdminUpdateScreenshotsRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.BadRequest(c, "截图参数不正确")
|
||||
return
|
||||
}
|
||||
item, err := h.service.UpdateScreenshots(c.Request.Context(), adminID, id, req, auditMeta(c))
|
||||
if err != nil {
|
||||
writeListingError(c, err)
|
||||
return
|
||||
}
|
||||
response.OK(c, item)
|
||||
}
|
||||
|
||||
|
||||
@@ -386,3 +386,52 @@ func (r *Repository) Reject(ctx context.Context, listingID uint64, req ReviewReq
|
||||
})
|
||||
return dto, err
|
||||
}
|
||||
|
||||
func (r *Repository) UpdateScreenshots(ctx context.Context, adminID uint64, listingID uint64, req AdminUpdateScreenshotsRequest, meta AuditMeta) (*ListingDTO, error) {
|
||||
var dto *ListingDTO
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
listing, account, err := r.findForReviewUpdate(tx, listingID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if listing.Status == "rented" || listing.InTransaction {
|
||||
return ErrListingLocked
|
||||
}
|
||||
|
||||
screenshots, err := marshalScreenshots(req.ScreenshotURLs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.ScreenshotURLS = screenshots
|
||||
|
||||
if req.ScreenshotGroups != nil {
|
||||
summary := decodeAssetSummary(account.AssetSummary)
|
||||
if summary == nil {
|
||||
summary = map[string]any{}
|
||||
}
|
||||
summary["screenshot_groups"] = req.ScreenshotGroups
|
||||
assetSummary, err := marshalAssetSummary(summary)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
account.AssetSummary = assetSummary
|
||||
}
|
||||
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := appendAuditLog(tx, adminID, "listing.update_screenshots", "listing", listing.ID, meta, map[string]any{
|
||||
"listing_id": listing.ID,
|
||||
"account_id": account.ID,
|
||||
"screenshot_urls": req.ScreenshotURLs,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
dto = toDTO(*account, *listing)
|
||||
return nil
|
||||
})
|
||||
return dto, err
|
||||
}
|
||||
|
||||
|
||||
@@ -81,3 +81,14 @@ func (s *Service) Reject(ctx context.Context, id uint64, req ReviewRequest) (*Li
|
||||
}
|
||||
return s.repo.Reject(ctx, id, req)
|
||||
}
|
||||
|
||||
func (s *Service) UpdateScreenshots(ctx context.Context, adminID uint64, id uint64, req AdminUpdateScreenshotsRequest, meta AuditMeta) (*ListingDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if len(req.ScreenshotURLs) == 0 {
|
||||
return nil, ErrInvalidInput
|
||||
}
|
||||
return s.repo.UpdateScreenshots(ctx, adminID, id, req, meta)
|
||||
}
|
||||
|
||||
|
||||
@@ -699,3 +699,73 @@ func TestApplyPublicScreenshotGroups(t *testing.T) {
|
||||
t.Fatalf("expected /api/listings/88/screenshots/1, got %#v", skinList)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateScreenshots(t *testing.T) {
|
||||
db := database.NewTestDB()
|
||||
if err := database.MigrateListingLifecycleTestSchema(db); err != nil {
|
||||
t.Fatalf("failed to migrate test db: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.AuditLog{}); err != nil {
|
||||
t.Fatalf("failed to migrate audit log: %v", err)
|
||||
}
|
||||
repo := NewRepository(db, nil)
|
||||
account := model.GameAccount{
|
||||
OwnerID: 1,
|
||||
GameName: "delta_force",
|
||||
ServerRegion: "QQ",
|
||||
LoginPlatform: "QQ账号密码",
|
||||
Title: "待审核商品",
|
||||
Status: "draft",
|
||||
}
|
||||
if err := db.Create(&account).Error; err != nil {
|
||||
t.Fatalf("failed to create account: %v", err)
|
||||
}
|
||||
listing := model.RentalListing{
|
||||
ListingNo: "202609070001",
|
||||
AccountID: account.ID,
|
||||
OwnerID: 1,
|
||||
Status: "draft",
|
||||
ReviewStatus: "pending",
|
||||
}
|
||||
if err := db.Create(&listing).Error; err != nil {
|
||||
t.Fatalf("failed to create listing: %v", err)
|
||||
}
|
||||
|
||||
meta := AuditMeta{IP: "127.0.0.1", UserAgent: "TestAgent", RequestID: "req-1"}
|
||||
req := AdminUpdateScreenshotsRequest{
|
||||
ScreenshotURLs: []string{
|
||||
"/api/files/object?key=edited1.png",
|
||||
"/api/files/object?key=edited2.png",
|
||||
},
|
||||
ScreenshotGroups: map[string]any{
|
||||
"coin": []string{"/api/files/object?key=edited1.png"},
|
||||
"gameId": []string{"/api/files/object?key=edited2.png"},
|
||||
},
|
||||
}
|
||||
|
||||
updated, err := repo.UpdateScreenshots(t.Context(), 999, listing.ID, req, meta)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateScreenshots failed: %v", err)
|
||||
}
|
||||
|
||||
if len(updated.ScreenshotURLS) != 2 || updated.ScreenshotURLS[0] != "/api/files/object?key=edited1.png" {
|
||||
t.Fatalf("unexpected screenshot_urls: %#v", updated.ScreenshotURLS)
|
||||
}
|
||||
|
||||
groups, ok := updated.AssetSummary["screenshot_groups"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("screenshot_groups not found in AssetSummary: %#v", updated.AssetSummary)
|
||||
}
|
||||
coinList, ok := groups["coin"].([]string)
|
||||
if !ok {
|
||||
if anyList, isAny := groups["coin"].([]any); isAny {
|
||||
for _, v := range anyList {
|
||||
coinList = append(coinList, v.(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(coinList) != 1 || coinList[0] != "/api/files/object?key=edited1.png" {
|
||||
t.Fatalf("unexpected coin screenshots: %#v", coinList)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user