diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index a32760e..3a37190 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -46,7 +46,7 @@ func main() { if err := authSvc.EnsureAdmin(); err != nil { log.Fatalf("ensure admin: %v", err) } - if err := skinSvc.SeedDemo(); err != nil { + if err := skinSvc.SeedCatalog(); err != nil { log.Printf("seed skins: %v", err) } diff --git a/backend/internal/handler/skin.go b/backend/internal/handler/skin.go index 49e8e4a..02d81f5 100644 --- a/backend/internal/handler/skin.go +++ b/backend/internal/handler/skin.go @@ -52,10 +52,11 @@ func (h *SkinHandler) Get(c *gin.Context) { type skinCreateReq struct { Name string `json:"name" binding:"required"` + SKU string `json:"sku" binding:"required"` Game string `json:"game"` Category string `json:"category"` CoverURL string `json:"cover_url"` - Price float64 `json:"price" binding:"required"` + Price float64 `json:"price"` CostPrice float64 `json:"cost_price"` Commission float64 `json:"commission"` Stock int `json:"stock"` @@ -66,19 +67,20 @@ type skinCreateReq struct { func (h *SkinHandler) Create(c *gin.Context) { var req skinCreateReq if err := c.ShouldBindJSON(&req); err != nil { - response.BadRequest(c, "参数错误") + response.BadRequest(c, "参数错误:名称与 sku 必填") return } status := req.Status - if status != 0 && status != 1 { + if status != 1 { + // 未传或非 1 时:创建默认上架;若传 0 仍默认上架(下架请创建后更新) status = 1 } - // 未传 status 时默认上架;若明确要下架请先创建再更新 - if status == 0 { - status = 1 + if req.Game == "" { + req.Game = "和平精英" } skin := &model.Skin{ Name: req.Name, + SKU: req.SKU, Game: req.Game, Category: req.Category, CoverURL: req.CoverURL, diff --git a/backend/internal/model/models.go b/backend/internal/model/models.go index f2dfb39..9e2ab0f 100644 --- a/backend/internal/model/models.go +++ b/backend/internal/model/models.go @@ -35,15 +35,16 @@ type Skin struct { UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `gorm:"index" json:"-"` - Name string `gorm:"size:128;not null" json:"name"` - Game string `gorm:"size:64;index" json:"game"` // 所属游戏 - Category string `gorm:"size:64;index" json:"category"` // 分类 + Name string `gorm:"size:128;not null" json:"name"` // 中文名(展示用,保持原名) + SKU string `gorm:"uniqueIndex;size:64;not null" json:"sku"` // 英文固定标识 + Game string `gorm:"size:64;index" json:"game"` // 所属游戏 + Category string `gorm:"size:64;index" json:"category"` // 品类:套装/背包/... CoverURL string `gorm:"size:512" json:"cover_url"` - Price float64 `gorm:"not null;default:0" json:"price"` // 售价 - CostPrice float64 `gorm:"default:0" json:"cost_price"` // 成本价 - Commission float64 `gorm:"default:0" json:"commission"` // 佣金比例 0-1 - Stock int `gorm:"default:0" json:"stock"` // -1 无限 - Status int `gorm:"default:1" json:"status"` // 1上架 0下架 + Price float64 `gorm:"not null;default:0" json:"price"` // 售价 + CostPrice float64 `gorm:"default:0" json:"cost_price"` // 成本价 + Commission float64 `gorm:"default:0" json:"commission"` // 佣金比例 0-1 + Stock int `gorm:"default:0" json:"stock"` // -1 无限 + Status int `gorm:"default:1" json:"status"` // 1上架 0下架 Description string `gorm:"type:text" json:"description"` } diff --git a/backend/internal/service/skin.go b/backend/internal/service/skin.go index b0b9105..3817e66 100644 --- a/backend/internal/service/skin.go +++ b/backend/internal/service/skin.go @@ -34,7 +34,8 @@ func (s *SkinService) List(q SkinListQuery) ([]model.Skin, int64, error) { } tx := s.db.Model(&model.Skin{}) if q.Keyword != "" { - tx = tx.Where("name LIKE ?", "%"+q.Keyword+"%") + like := "%" + q.Keyword + "%" + tx = tx.Where("name LIKE ? OR sku LIKE ?", like, like) } if q.Game != "" { tx = tx.Where("game = ?", q.Game) @@ -91,16 +92,75 @@ func (s *SkinService) Delete(id uint) error { return nil } -func (s *SkinService) SeedDemo() error { - var count int64 - s.db.Model(&model.Skin{}).Count(&count) - if count > 0 { - return nil +// SeedCatalog 按 sku 幂等导入商品目录(已存在则跳过) +func (s *SkinService) SeedCatalog() error { + // 清理早期无 sku 的演示数据,避免唯一索引冲突 + _ = s.db.Where("sku = ? OR sku IS NULL", "").Delete(&model.Skin{}).Error + + for _, item := range peaceEliteCatalog { + var existing model.Skin + err := s.db.Where("sku = ?", item.SKU).First(&existing).Error + if err == nil { + // 已存在:仅同步默认佣金为 0(不改价格等业务字段) + if existing.Commission != 0 { + _ = s.db.Model(&existing).Update("commission", 0).Error + } + continue + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + skin := model.Skin{ + Name: item.Name, + SKU: item.SKU, + Game: "和平精英", + Category: item.Category, + Price: 0, + CostPrice: 0, + Commission: 0, + Stock: -1, + Status: 1, + } + if err := s.db.Create(&skin).Error; err != nil { + return err + } } - demos := []model.Skin{ - {Name: "龙之觉醒", Game: "王者荣耀", Category: "史诗", Price: 88, CostPrice: 50, Commission: 0.15, Stock: -1, Status: 1, Description: "史诗皮肤示例"}, - {Name: "星空旅人", Game: "和平精英", Category: "限定", Price: 128, CostPrice: 80, Commission: 0.12, Stock: 100, Status: 1, Description: "限定皮肤示例"}, - {Name: "暗夜骑士", Game: "英雄联盟", Category: "传说", Price: 199, CostPrice: 120, Commission: 0.10, Stock: 50, Status: 1, Description: "传说皮肤示例"}, - } - return s.db.Create(&demos).Error + return nil +} + +type catalogItem struct { + Name string + SKU string + Category string +} + +// 和平精英商品目录(中文名保持原样,英文名为固定 sku) +var peaceEliteCatalog = []catalogItem{ + {Name: "套装-Alan Walker", SKU: "suit_alan_walker", Category: "套装"}, + {Name: "套装-暗影哥特", SKU: "suit_shadow_gothic", Category: "套装"}, + {Name: "黑色高级特训官上衣", SKU: "top_black_elite_trainer", Category: "上衣"}, + {Name: "M416-仓鼠灰灰", SKU: "m416_hamster_gray", Category: "枪械"}, + {Name: "萌熊伴侣背包", SKU: "bag_cute_bear", Category: "背包"}, + {Name: "套装-双彩绵绵", SKU: "suit_dual_fluffy", Category: "套装"}, + {Name: "套装-糯粉咩咩", SKU: "suit_pink_sheep", Category: "套装"}, + {Name: "套装-恋恋初桃", SKU: "suit_first_peach", Category: "套装"}, + {Name: "套装-浪漫天命", SKU: "suit_romantic_destiny", Category: "套装"}, + {Name: "西部牛仔大礼包", SKU: "pack_western_cowboy", Category: "礼包"}, + {Name: "烟雾弹-糯粉咩咩", SKU: "smoke_pink_sheep", Category: "投掷物"}, + {Name: "破片手榴弹-糯粉咩咩", SKU: "frag_pink_sheep", Category: "投掷物"}, + {Name: "套装-仓鼠灰灰", SKU: "suit_hamster_gray", Category: "套装"}, + {Name: "套装-萌熊伴侣", SKU: "suit_cute_bear", Category: "套装"}, + {Name: "糯粉咩咩背包", SKU: "bag_pink_sheep", Category: "背包"}, + {Name: "糯粉咩咩头盔", SKU: "helmet_pink_sheep", Category: "头盔"}, + {Name: "仓鼠灰灰背包", SKU: "bag_hamster_gray", Category: "背包"}, + {Name: "仓鼠灰灰头盔", SKU: "helmet_hamster_gray", Category: "头盔"}, + {Name: "套装-西部谜踪", SKU: "suit_western_mystery", Category: "套装"}, + {Name: "国宝胖达头盔", SKU: "helmet_panda_treasure", Category: "头盔"}, + {Name: "套装-胖达圆圆", SKU: "suit_panda_round", Category: "套装"}, + {Name: "套装-胖达团团", SKU: "suit_panda_tuan", Category: "套装"}, + {Name: "熔岩游骑兵礼包", SKU: "pack_lava_ranger", Category: "礼包"}, + {Name: "套装-狂沙舞者", SKU: "suit_sand_dancer", Category: "套装"}, + {Name: "星际漫游服装礼包", SKU: "pack_star_roam_outfit", Category: "礼包"}, + {Name: "星际漫游枪械礼包", SKU: "pack_star_roam_weapon", Category: "礼包"}, + {Name: "套装-绵云熊熊", SKU: "suit_cloud_bear", Category: "套装"}, } diff --git a/docs/商品列表.md b/docs/商品列表.md index 6379ea4..00451e3 100644 --- a/docs/商品列表.md +++ b/docs/商品列表.md @@ -25,4 +25,4 @@ M416-仓鼠灰灰 套装-狂沙舞者 星际漫游服装礼包 星际漫游枪械礼包 -套装-绵云熊熊 \ No newline at end of file +套装-绵云熊熊 diff --git a/docs/商品英文名对照.md b/docs/商品英文名对照.md new file mode 100644 index 0000000..4e6809b --- /dev/null +++ b/docs/商品英文名对照.md @@ -0,0 +1,29 @@ +| 中文名 | 英文名 | +|--------|--------| +| 套装-Alan Walker | suit_alan_walker | +| 套装-暗影哥特 | suit_shadow_gothic | +| 黑色高级特训官上衣 | top_black_elite_trainer | +| M416-仓鼠灰灰 | m416_hamster_gray | +| 萌熊伴侣背包 | bag_cute_bear | +| 套装-双彩绵绵 | suit_dual_fluffy | +| 套装-糯粉咩咩 | suit_pink_sheep | +| 套装-恋恋初桃 | suit_first_peach | +| 套装-浪漫天命 | suit_romantic_destiny | +| 西部牛仔大礼包 | pack_western_cowboy | +| 烟雾弹-糯粉咩咩 | smoke_pink_sheep | +| 破片手榴弹-糯粉咩咩 | frag_pink_sheep | +| 套装-仓鼠灰灰 | suit_hamster_gray | +| 套装-萌熊伴侣 | suit_cute_bear | +| 糯粉咩咩背包 | bag_pink_sheep | +| 糯粉咩咩头盔 | helmet_pink_sheep | +| 仓鼠灰灰背包 | bag_hamster_gray | +| 仓鼠灰灰头盔 | helmet_hamster_gray | +| 套装-西部谜踪 | suit_western_mystery | +| 国宝胖达头盔 | helmet_panda_treasure | +| 套装-胖达圆圆 | suit_panda_round | +| 套装-胖达团团 | suit_panda_tuan | +| 熔岩游骑兵礼包 | pack_lava_ranger | +| 套装-狂沙舞者 | suit_sand_dancer | +| 星际漫游服装礼包 | pack_star_roam_outfit | +| 星际漫游枪械礼包 | pack_star_roam_weapon | +| 套装-绵云熊熊 | suit_cloud_bear | diff --git a/frontend/src/pages/Skins.tsx b/frontend/src/pages/Skins.tsx index 562594c..965df25 100644 --- a/frontend/src/pages/Skins.tsx +++ b/frontend/src/pages/Skins.tsx @@ -51,7 +51,12 @@ export default function Skins() { const openCreate = () => { setEditing(null) form.resetFields() - form.setFieldsValue({ stock: -1, commission: 0.1, status: 1 }) + form.setFieldsValue({ + game: '和平精英', + stock: -1, + commission: 0, + status: 1, + }) setOpen(true) } @@ -99,20 +104,26 @@ export default function Skins() { const columns: ColumnsType = [ { title: 'ID', dataIndex: 'id', width: 70 }, - { title: '名称', dataIndex: 'name' }, - { title: '游戏', dataIndex: 'game', width: 120 }, - { title: '分类', dataIndex: 'category', width: 100 }, + { title: '中文名', dataIndex: 'name', width: 180, ellipsis: true }, + { + title: '英文名', + dataIndex: 'sku', + width: 220, + ellipsis: true, + render: (v: string) => {v}, + }, + { title: '游戏', dataIndex: 'game', width: 100 }, { title: '售价', dataIndex: 'price', width: 100, - render: (v: number) => `¥${v.toFixed(2)}`, + render: (v: number) => `¥${Number(v ?? 0).toFixed(2)}`, }, { title: '佣金比例', dataIndex: 'commission', width: 100, - render: (v: number) => `${(v * 100).toFixed(0)}%`, + render: (v: number) => `${((v ?? 0) * 100).toFixed(0)}%`, }, { title: '库存', @@ -130,7 +141,7 @@ export default function Skins() { { title: '操作', key: 'action', - width: isAdmin ? 200 : 100, + width: isAdmin ? 140 : 80, render: (_, record) => ( {isAdmin ? ( @@ -145,7 +156,12 @@ export default function Skins() { ) : ( - )} @@ -162,13 +178,13 @@ export default function Skins() { { setPage(1) setKeyword(v) }} - style={{ width: 220 }} + style={{ width: 240 }} />