导入和平精英商品目录并优化皮肤列表展示
- 皮肤增加 sku 字段,启动时按 sku 幂等导入 27 个和平精英商品 - 佣金默认 0,支持中英文名搜索 - 列表去掉品类列并固定列宽,避免中文名列空白过大 - 新增商品中英文名对照文档
This commit is contained in:
@@ -46,7 +46,7 @@ func main() {
|
|||||||
if err := authSvc.EnsureAdmin(); err != nil {
|
if err := authSvc.EnsureAdmin(); err != nil {
|
||||||
log.Fatalf("ensure admin: %v", err)
|
log.Fatalf("ensure admin: %v", err)
|
||||||
}
|
}
|
||||||
if err := skinSvc.SeedDemo(); err != nil {
|
if err := skinSvc.SeedCatalog(); err != nil {
|
||||||
log.Printf("seed skins: %v", err)
|
log.Printf("seed skins: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -52,10 +52,11 @@ func (h *SkinHandler) Get(c *gin.Context) {
|
|||||||
|
|
||||||
type skinCreateReq struct {
|
type skinCreateReq struct {
|
||||||
Name string `json:"name" binding:"required"`
|
Name string `json:"name" binding:"required"`
|
||||||
|
SKU string `json:"sku" binding:"required"`
|
||||||
Game string `json:"game"`
|
Game string `json:"game"`
|
||||||
Category string `json:"category"`
|
Category string `json:"category"`
|
||||||
CoverURL string `json:"cover_url"`
|
CoverURL string `json:"cover_url"`
|
||||||
Price float64 `json:"price" binding:"required"`
|
Price float64 `json:"price"`
|
||||||
CostPrice float64 `json:"cost_price"`
|
CostPrice float64 `json:"cost_price"`
|
||||||
Commission float64 `json:"commission"`
|
Commission float64 `json:"commission"`
|
||||||
Stock int `json:"stock"`
|
Stock int `json:"stock"`
|
||||||
@@ -66,19 +67,20 @@ type skinCreateReq struct {
|
|||||||
func (h *SkinHandler) Create(c *gin.Context) {
|
func (h *SkinHandler) Create(c *gin.Context) {
|
||||||
var req skinCreateReq
|
var req skinCreateReq
|
||||||
if err := c.ShouldBindJSON(&req); err != nil {
|
if err := c.ShouldBindJSON(&req); err != nil {
|
||||||
response.BadRequest(c, "参数错误")
|
response.BadRequest(c, "参数错误:名称与 sku 必填")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
status := req.Status
|
status := req.Status
|
||||||
if status != 0 && status != 1 {
|
if status != 1 {
|
||||||
|
// 未传或非 1 时:创建默认上架;若传 0 仍默认上架(下架请创建后更新)
|
||||||
status = 1
|
status = 1
|
||||||
}
|
}
|
||||||
// 未传 status 时默认上架;若明确要下架请先创建再更新
|
if req.Game == "" {
|
||||||
if status == 0 {
|
req.Game = "和平精英"
|
||||||
status = 1
|
|
||||||
}
|
}
|
||||||
skin := &model.Skin{
|
skin := &model.Skin{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
|
SKU: req.SKU,
|
||||||
Game: req.Game,
|
Game: req.Game,
|
||||||
Category: req.Category,
|
Category: req.Category,
|
||||||
CoverURL: req.CoverURL,
|
CoverURL: req.CoverURL,
|
||||||
|
|||||||
@@ -35,15 +35,16 @@ type Skin struct {
|
|||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||||
|
|
||||||
Name string `gorm:"size:128;not null" json:"name"`
|
Name string `gorm:"size:128;not null" json:"name"` // 中文名(展示用,保持原名)
|
||||||
Game string `gorm:"size:64;index" json:"game"` // 所属游戏
|
SKU string `gorm:"uniqueIndex;size:64;not null" json:"sku"` // 英文固定标识
|
||||||
Category string `gorm:"size:64;index" json:"category"` // 分类
|
Game string `gorm:"size:64;index" json:"game"` // 所属游戏
|
||||||
|
Category string `gorm:"size:64;index" json:"category"` // 品类:套装/背包/...
|
||||||
CoverURL string `gorm:"size:512" json:"cover_url"`
|
CoverURL string `gorm:"size:512" json:"cover_url"`
|
||||||
Price float64 `gorm:"not null;default:0" json:"price"` // 售价
|
Price float64 `gorm:"not null;default:0" json:"price"` // 售价
|
||||||
CostPrice float64 `gorm:"default:0" json:"cost_price"` // 成本价
|
CostPrice float64 `gorm:"default:0" json:"cost_price"` // 成本价
|
||||||
Commission float64 `gorm:"default:0" json:"commission"` // 佣金比例 0-1
|
Commission float64 `gorm:"default:0" json:"commission"` // 佣金比例 0-1
|
||||||
Stock int `gorm:"default:0" json:"stock"` // -1 无限
|
Stock int `gorm:"default:0" json:"stock"` // -1 无限
|
||||||
Status int `gorm:"default:1" json:"status"` // 1上架 0下架
|
Status int `gorm:"default:1" json:"status"` // 1上架 0下架
|
||||||
Description string `gorm:"type:text" json:"description"`
|
Description string `gorm:"type:text" json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,8 @@ func (s *SkinService) List(q SkinListQuery) ([]model.Skin, int64, error) {
|
|||||||
}
|
}
|
||||||
tx := s.db.Model(&model.Skin{})
|
tx := s.db.Model(&model.Skin{})
|
||||||
if q.Keyword != "" {
|
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 != "" {
|
if q.Game != "" {
|
||||||
tx = tx.Where("game = ?", q.Game)
|
tx = tx.Where("game = ?", q.Game)
|
||||||
@@ -91,16 +92,75 @@ func (s *SkinService) Delete(id uint) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SkinService) SeedDemo() error {
|
// SeedCatalog 按 sku 幂等导入商品目录(已存在则跳过)
|
||||||
var count int64
|
func (s *SkinService) SeedCatalog() error {
|
||||||
s.db.Model(&model.Skin{}).Count(&count)
|
// 清理早期无 sku 的演示数据,避免唯一索引冲突
|
||||||
if count > 0 {
|
_ = s.db.Where("sku = ? OR sku IS NULL", "").Delete(&model.Skin{}).Error
|
||||||
return nil
|
|
||||||
|
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{
|
return nil
|
||||||
{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: "传说皮肤示例"},
|
type catalogItem struct {
|
||||||
}
|
Name string
|
||||||
return s.db.Create(&demos).Error
|
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: "套装"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 |
|
||||||
@@ -51,7 +51,12 @@ export default function Skins() {
|
|||||||
const openCreate = () => {
|
const openCreate = () => {
|
||||||
setEditing(null)
|
setEditing(null)
|
||||||
form.resetFields()
|
form.resetFields()
|
||||||
form.setFieldsValue({ stock: -1, commission: 0.1, status: 1 })
|
form.setFieldsValue({
|
||||||
|
game: '和平精英',
|
||||||
|
stock: -1,
|
||||||
|
commission: 0,
|
||||||
|
status: 1,
|
||||||
|
})
|
||||||
setOpen(true)
|
setOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,20 +104,26 @@ export default function Skins() {
|
|||||||
|
|
||||||
const columns: ColumnsType<Skin> = [
|
const columns: ColumnsType<Skin> = [
|
||||||
{ title: 'ID', dataIndex: 'id', width: 70 },
|
{ title: 'ID', dataIndex: 'id', width: 70 },
|
||||||
{ title: '名称', dataIndex: 'name' },
|
{ title: '中文名', dataIndex: 'name', width: 180, ellipsis: true },
|
||||||
{ title: '游戏', dataIndex: 'game', width: 120 },
|
{
|
||||||
{ title: '分类', dataIndex: 'category', width: 100 },
|
title: '英文名',
|
||||||
|
dataIndex: 'sku',
|
||||||
|
width: 220,
|
||||||
|
ellipsis: true,
|
||||||
|
render: (v: string) => <Typography.Text code>{v}</Typography.Text>,
|
||||||
|
},
|
||||||
|
{ title: '游戏', dataIndex: 'game', width: 100 },
|
||||||
{
|
{
|
||||||
title: '售价',
|
title: '售价',
|
||||||
dataIndex: 'price',
|
dataIndex: 'price',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (v: number) => `¥${v.toFixed(2)}`,
|
render: (v: number) => `¥${Number(v ?? 0).toFixed(2)}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '佣金比例',
|
title: '佣金比例',
|
||||||
dataIndex: 'commission',
|
dataIndex: 'commission',
|
||||||
width: 100,
|
width: 100,
|
||||||
render: (v: number) => `${(v * 100).toFixed(0)}%`,
|
render: (v: number) => `${((v ?? 0) * 100).toFixed(0)}%`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '库存',
|
title: '库存',
|
||||||
@@ -130,7 +141,7 @@ export default function Skins() {
|
|||||||
{
|
{
|
||||||
title: '操作',
|
title: '操作',
|
||||||
key: 'action',
|
key: 'action',
|
||||||
width: isAdmin ? 200 : 100,
|
width: isAdmin ? 140 : 80,
|
||||||
render: (_, record) => (
|
render: (_, record) => (
|
||||||
<Space>
|
<Space>
|
||||||
{isAdmin ? (
|
{isAdmin ? (
|
||||||
@@ -145,7 +156,12 @@ export default function Skins() {
|
|||||||
</Popconfirm>
|
</Popconfirm>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<Button type="link" size="small" disabled={record.status !== 1} onClick={() => onOrder(record)}>
|
<Button
|
||||||
|
type="link"
|
||||||
|
size="small"
|
||||||
|
disabled={record.status !== 1}
|
||||||
|
onClick={() => onOrder(record)}
|
||||||
|
>
|
||||||
下单
|
下单
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
@@ -162,13 +178,13 @@ export default function Skins() {
|
|||||||
</Typography.Title>
|
</Typography.Title>
|
||||||
<Space>
|
<Space>
|
||||||
<Input.Search
|
<Input.Search
|
||||||
placeholder="搜索名称"
|
placeholder="搜索中文名 / 英文名"
|
||||||
allowClear
|
allowClear
|
||||||
onSearch={(v) => {
|
onSearch={(v) => {
|
||||||
setPage(1)
|
setPage(1)
|
||||||
setKeyword(v)
|
setKeyword(v)
|
||||||
}}
|
}}
|
||||||
style={{ width: 220 }}
|
style={{ width: 240 }}
|
||||||
/>
|
/>
|
||||||
<Button icon={<ReloadOutlined />} onClick={load}>
|
<Button icon={<ReloadOutlined />} onClick={load}>
|
||||||
刷新
|
刷新
|
||||||
@@ -186,6 +202,7 @@ export default function Skins() {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={list}
|
dataSource={list}
|
||||||
|
tableLayout="fixed"
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page,
|
current: page,
|
||||||
pageSize: size,
|
pageSize: size,
|
||||||
@@ -207,15 +224,39 @@ export default function Skins() {
|
|||||||
width={560}
|
width={560}
|
||||||
>
|
>
|
||||||
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
<Form form={form} layout="vertical" style={{ marginTop: 16 }}>
|
||||||
<Form.Item name="name" label="名称" rules={[{ required: true }]}>
|
<Form.Item name="name" label="中文名" rules={[{ required: true }]}>
|
||||||
<Input />
|
<Input placeholder="如:套装-糯粉咩咩" />
|
||||||
|
</Form.Item>
|
||||||
|
<Form.Item
|
||||||
|
name="sku"
|
||||||
|
label="英文名 (sku)"
|
||||||
|
rules={[{ required: true, message: '请填写英文固定标识' }]}
|
||||||
|
>
|
||||||
|
<Input placeholder="如:suit_pink_sheep" disabled={!!editing} />
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Space style={{ width: '100%' }} size="middle">
|
<Space style={{ width: '100%' }} size="middle">
|
||||||
<Form.Item name="game" label="游戏" style={{ width: 240 }}>
|
<Form.Item name="game" label="游戏" style={{ width: 240 }}>
|
||||||
<Input />
|
<Select
|
||||||
|
options={[
|
||||||
|
{ value: '和平精英', label: '和平精英' },
|
||||||
|
{ value: '王者荣耀', label: '王者荣耀' },
|
||||||
|
{ value: '英雄联盟', label: '英雄联盟' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
<Form.Item name="category" label="分类" style={{ width: 240 }}>
|
<Form.Item name="category" label="品类" style={{ width: 240 }}>
|
||||||
<Input />
|
<Select
|
||||||
|
allowClear
|
||||||
|
options={[
|
||||||
|
{ value: '套装', label: '套装' },
|
||||||
|
{ value: '上衣', label: '上衣' },
|
||||||
|
{ value: '背包', label: '背包' },
|
||||||
|
{ value: '头盔', label: '头盔' },
|
||||||
|
{ value: '枪械', label: '枪械' },
|
||||||
|
{ value: '投掷物', label: '投掷物' },
|
||||||
|
{ value: '礼包', label: '礼包' },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</Space>
|
</Space>
|
||||||
<Space style={{ width: '100%' }} size="middle">
|
<Space style={{ width: '100%' }} size="middle">
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ export interface User {
|
|||||||
export interface Skin {
|
export interface Skin {
|
||||||
id: number
|
id: number
|
||||||
name: string
|
name: string
|
||||||
|
sku: string
|
||||||
game: string
|
game: string
|
||||||
category: string
|
category: string
|
||||||
cover_url: string
|
cover_url: string
|
||||||
|
|||||||
Reference in New Issue
Block a user