diff --git a/backend/internal/modules/systemconfig/dto.go b/backend/internal/modules/systemconfig/dto.go index 2cbce04..2973173 100644 --- a/backend/internal/modules/systemconfig/dto.go +++ b/backend/internal/modules/systemconfig/dto.go @@ -16,3 +16,36 @@ type UpdateRequest struct { Value string `json:"value" binding:"required"` Description string `json:"description"` } + +type PublishOptionsDTO struct { + ServerOptions []string `json:"server_options"` + FaceOptions []string `json:"face_options"` + RankOptions []string `json:"rank_options"` + InsuranceOptions []string `json:"insurance_options"` + LevelOptions []string `json:"level_options"` + LoginMethodOptions []string `json:"login_method_options"` + RegionOptions []string `json:"region_options"` + SkinGroups []PublishOptionGroup `json:"skin_groups"` + QuantityItems []PublishQuantityItem `json:"quantity_items"` + ScreenshotSlots []PublishScreenshotSlot `json:"screenshot_slots"` +} + +type PublishOptionGroup struct { + Key string `json:"key"` + Title string `json:"title"` + Options []string `json:"options"` +} + +type PublishQuantityItem struct { + Key string `json:"key"` + Label string `json:"label"` + Price string `json:"price"` + Placeholder string `json:"placeholder,omitempty"` +} + +type PublishScreenshotSlot struct { + Key string `json:"key"` + Label string `json:"label"` + Required bool `json:"required"` + Hint string `json:"hint"` +} diff --git a/backend/internal/modules/systemconfig/handler.go b/backend/internal/modules/systemconfig/handler.go index 2227763..2ab6334 100644 --- a/backend/internal/modules/systemconfig/handler.go +++ b/backend/internal/modules/systemconfig/handler.go @@ -27,6 +27,15 @@ func (h *Handler) List(c *gin.Context) { response.OK(c, gin.H{"items": items}) } +func (h *Handler) PublishOptions(c *gin.Context) { + options, err := h.service.PublishOptions() + if err != nil { + writeConfigError(c, err) + return + } + response.OK(c, options) +} + func (h *Handler) Update(c *gin.Context) { adminID, ok := currentAdminID(c) if !ok { diff --git a/backend/internal/modules/systemconfig/publish_options.go b/backend/internal/modules/systemconfig/publish_options.go new file mode 100644 index 0000000..befe5e5 --- /dev/null +++ b/backend/internal/modules/systemconfig/publish_options.go @@ -0,0 +1,76 @@ +package systemconfig + +import "encoding/json" + +const publishOptionsConfigKey = "listing.publish_options" + +func defaultPublishOptionsConfigValue() string { + raw, err := json.Marshal(DefaultPublishOptions()) + if err != nil { + return "{}" + } + return string(raw) +} + +func DefaultPublishOptions() PublishOptionsDTO { + return PublishOptionsDTO{ + ServerOptions: []string{"QQ", "微信", "Steam"}, + FaceOptions: []string{"是", "否"}, + RankOptions: []string{"青铜", "白银", "黄金", "铂金", "钻石", "黑鹰", "三角洲巅峰"}, + InsuranceOptions: []string{"2*1", "2*2", "2*3", "3*3"}, + LevelOptions: []string{"1级", "2级", "3级", "4级", "5级", "6级", "7级"}, + LoginMethodOptions: []string{"扫码", "账号密码"}, + RegionOptions: []string{ + "北京", "天津", "河北", "山西", "内蒙古", "辽宁", "吉林", "黑龙江", + "上海", "江苏", "浙江", "安徽", "福建", "江西", "山东", "河南", + "湖北", "湖南", "广东", "广西", "海南", "重庆", "四川", "贵州", + "云南", "陕西", "甘肃", "青海", "宁夏", "新疆", "西藏", "香港", + "澳门", "台湾", + }, + SkinGroups: []PublishOptionGroup{ + { + Key: "melee", + Title: "近战皮肤", + Options: []string{ + "坠星者", "暗星", "龙牙", "信条", "怜悯", "赤枭", "影锋", "黑海", "北极星", "电锯惊魂", "处刑者", + }, + }, + { + Key: "operator", + Title: "干员皮肤", + Options: []string{ + "威龙-凌霄戍卫", "红狼-蚀金玫瑰", "露娜-黑天际线", "蛊-能天使午夜邮差", + "骇爪-维什戴尔", "骇爪-水墨云图", "威龙-吴彦祖", "威龙-铁面判官", + "威龙-壮志凌云", "威龙-蛟龙特战队", "蜂医-送葬人", "蜂医-危险物质", + "蜂医-黑鹰坠落", "牧羊人-街头之星", "牧羊人-黑鹰坠落", "乌鲁鲁-黑鹰坠落", + "红狼-黑鹰坠落", "无名-夜鹰", "深蓝-不破誓约", "红狼-电锯惊魂", + "露娜-古墓丽影劳拉", "赛伊德", + }, + }, + { + Key: "weapon", + Title: "武器皮肤", + Options: []string{ + "M7棱镜攻势S2", "腾龙气象感应", "MP7电玩高手S2", "M250电玩高手S2", + "AS Val悬赏令", "K416命运", "M4A1棱镜攻势", "SCAR-H电玩高手", + "QBZ95王牌之剑", "AUG气象感应", "Vector美杜莎", "KC17-造物纪元", + }, + }, + }, + QuantityItems: []PublishQuantityItem{ + {Key: "awmAmmo", Label: "AWM子弹", Price: "0.8元/发"}, + {Key: "helmet6", Label: "6头数量", Price: "2元/个"}, + {Key: "armor6", Label: "6甲数量", Price: "3元/个"}, + {Key: "kit5", Label: "5级全装套数量", Price: "5元/个"}, + {Key: "armor6Ammo", Label: "6甲子弹", Price: "0.26元/发"}, + {Key: "gridCard9", Label: "9格体验卡", Price: "5元/张", Placeholder: "如果是赛季9格,此处填0即可"}, + }, + ScreenshotSlots: []PublishScreenshotSlot{ + {Key: "coin", Label: "纯币截图", Required: true, Hint: "请上传纯币截图(必传)"}, + {Key: "gameId", Label: "游戏ID截图", Required: true, Hint: "请上传游戏ID截图(必传)"}, + {Key: "totalAsset", Label: "总资产截图", Required: true, Hint: "请上传总资产截图(必传)"}, + {Key: "tencentSecurity", Label: "腾讯安全中心截图", Required: true, Hint: "请上传腾讯安全中心截图(必传)"}, + {Key: "skin", Label: "皮肤截图", Required: false, Hint: "请上传皮肤截图(选传)"}, + }, + } +} diff --git a/backend/internal/modules/systemconfig/repository.go b/backend/internal/modules/systemconfig/repository.go index 195affc..01e3f68 100644 --- a/backend/internal/modules/systemconfig/repository.go +++ b/backend/internal/modules/systemconfig/repository.go @@ -38,6 +38,7 @@ var defaultConfigs = []defaultConfig{ {Key: "settlement.platform_fee_rate", Value: "0.00", Description: "平台抽成比例"}, {Key: "settlement.owner_cycle_days", Value: "0", Description: "号主结算周期天数"}, {Key: "withdraw.min_amount", Value: "100", Description: "提现最低金额预留"}, + {Key: publishOptionsConfigKey, Value: defaultPublishOptionsConfigValue(), Description: "发布页选项配置 JSON"}, } func NewRepository(db *gorm.DB) *Repository { @@ -59,6 +60,17 @@ func (r *Repository) List() ([]ConfigDTO, error) { return items, nil } +func (r *Repository) FindValue(key string) (string, error) { + if err := r.ensureDefaults(); err != nil { + return "", err + } + var row model.SystemConfig + if err := r.db.Where("`key` = ?", key).First(&row).Error; err != nil { + return "", err + } + return row.Value, nil +} + func (r *Repository) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) { var row model.SystemConfig err := r.db.Transaction(func(tx *gorm.DB) error { diff --git a/backend/internal/modules/systemconfig/service.go b/backend/internal/modules/systemconfig/service.go index dab1ee1..b4f6f1f 100644 --- a/backend/internal/modules/systemconfig/service.go +++ b/backend/internal/modules/systemconfig/service.go @@ -1,6 +1,9 @@ package systemconfig -import "errors" +import ( + "encoding/json" + "errors" +) var ( ErrDependencyUnavailable = errors.New("dependency unavailable") @@ -22,6 +25,22 @@ func (s *Service) List() ([]ConfigDTO, error) { return s.repo.List() } +func (s *Service) PublishOptions() (*PublishOptionsDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + value, err := s.repo.FindValue(publishOptionsConfigKey) + if err != nil { + return nil, err + } + options := DefaultPublishOptions() + if err := json.Unmarshal([]byte(value), &options); err != nil { + options = DefaultPublishOptions() + } + normalizePublishOptions(&options) + return &options, nil +} + func (s *Service) Update(actorID uint64, key string, req UpdateRequest, meta AuditMeta) (*ConfigDTO, error) { if s.repo == nil { return nil, ErrDependencyUnavailable @@ -31,3 +50,37 @@ func (s *Service) Update(actorID uint64, key string, req UpdateRequest, meta Aud } return s.repo.Update(actorID, key, req, meta) } + +func normalizePublishOptions(options *PublishOptionsDTO) { + defaults := DefaultPublishOptions() + if len(options.ServerOptions) == 0 { + options.ServerOptions = defaults.ServerOptions + } + if len(options.FaceOptions) == 0 { + options.FaceOptions = defaults.FaceOptions + } + if len(options.RankOptions) == 0 { + options.RankOptions = defaults.RankOptions + } + if len(options.InsuranceOptions) == 0 { + options.InsuranceOptions = defaults.InsuranceOptions + } + if len(options.LevelOptions) == 0 { + options.LevelOptions = defaults.LevelOptions + } + if len(options.LoginMethodOptions) == 0 { + options.LoginMethodOptions = defaults.LoginMethodOptions + } + if len(options.RegionOptions) == 0 { + options.RegionOptions = defaults.RegionOptions + } + if len(options.SkinGroups) == 0 { + options.SkinGroups = defaults.SkinGroups + } + if len(options.QuantityItems) == 0 { + options.QuantityItems = defaults.QuantityItems + } + if len(options.ScreenshotSlots) == 0 { + options.ScreenshotSlots = defaults.ScreenshotSlots + } +} diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 4c8ff0e..c3c1ba9 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -126,6 +126,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { api := engine.Group("/api") { api.GET("/health", health.Check) + api.GET("/listing-publish-options", systemConfigHandler.PublishOptions) authRoutes := api.Group("/auth") { diff --git a/frontend/components.d.ts b/frontend/components.d.ts index 3785345..70802f1 100644 --- a/frontend/components.d.ts +++ b/frontend/components.d.ts @@ -17,14 +17,21 @@ declare module 'vue' { VanCell: typeof import('vant/es')['Cell'] VanCellGroup: typeof import('vant/es')['CellGroup'] VanCheckbox: typeof import('vant/es')['Checkbox'] + VanDropdownItem: typeof import('vant/es')['DropdownItem'] + VanDropdownMenu: typeof import('vant/es')['DropdownMenu'] VanEmpty: typeof import('vant/es')['Empty'] VanField: typeof import('vant/es')['Field'] + VanGrid: typeof import('vant/es')['Grid'] + VanGridItem: typeof import('vant/es')['GridItem'] VanIcon: typeof import('vant/es')['Icon'] + VanImage: typeof import('vant/es')['Image'] VanLoading: typeof import('vant/es')['Loading'] VanNoticeBar: typeof import('vant/es')['NoticeBar'] VanPopup: typeof import('vant/es')['Popup'] VanPullRefresh: typeof import('vant/es')['PullRefresh'] VanSearch: typeof import('vant/es')['Search'] + VanTabbar: typeof import('vant/es')['Tabbar'] + VanTabbarItem: typeof import('vant/es')['TabbarItem'] VanTag: typeof import('vant/es')['Tag'] } } diff --git a/frontend/src/api/listingOptions.ts b/frontend/src/api/listingOptions.ts new file mode 100644 index 0000000..4960d9b --- /dev/null +++ b/frontend/src/api/listingOptions.ts @@ -0,0 +1,133 @@ +import { apiClient } from './client' + +export type ChargeMode = '赠送' | '收费' + +export type QuantityKey = + | 'awmAmmo' + | 'helmet6' + | 'armor6' + | 'kit5' + | 'armor6Ammo' + | 'gridCard9' + +export type ScreenshotKey = 'coin' | 'gameId' | 'totalAsset' | 'tencentSecurity' | 'skin' + +export type SkinCategoryKey = 'melee' | 'operator' | 'weapon' + +export interface PublishOptionGroup { + key: SkinCategoryKey | string + title: string + options: string[] +} + +export interface PublishQuantityItem { + key: QuantityKey + label: string + price: string + placeholder?: string +} + +export interface PublishScreenshotSlot { + key: ScreenshotKey + label: string + required: boolean + hint: string +} + +export interface ListingPublishOptions { + server_options: string[] + face_options: string[] + rank_options: string[] + insurance_options: string[] + level_options: string[] + login_method_options: string[] + region_options: string[] + skin_groups: PublishOptionGroup[] + quantity_items: PublishQuantityItem[] + screenshot_slots: PublishScreenshotSlot[] +} + +interface ApiResponse { + code: string + message: string + data: T +} + +export const defaultListingPublishOptions: ListingPublishOptions = { + server_options: ['QQ', '微信', 'Steam'], + face_options: ['是', '否'], + rank_options: ['青铜', '白银', '黄金', '铂金', '钻石', '黑鹰', '三角洲巅峰'], + insurance_options: ['2*1', '2*2', '2*3', '3*3'], + level_options: ['1级', '2级', '3级', '4级', '5级', '6级', '7级'], + login_method_options: ['扫码', '账号密码'], + region_options: [ + '北京', '天津', '河北', '山西', '内蒙古', '辽宁', '吉林', '黑龙江', + '上海', '江苏', '浙江', '安徽', '福建', '江西', '山东', '河南', + '湖北', '湖南', '广东', '广西', '海南', '重庆', '四川', '贵州', + '云南', '陕西', '甘肃', '青海', '宁夏', '新疆', '西藏', '香港', + '澳门', '台湾', + ], + skin_groups: [ + { + key: 'melee', + title: '近战皮肤', + options: ['坠星者', '暗星', '龙牙', '信条', '怜悯', '赤枭', '影锋', '黑海', '北极星', '电锯惊魂', '处刑者'], + }, + { + key: 'operator', + title: '干员皮肤', + options: [ + '威龙-凌霄戍卫', '红狼-蚀金玫瑰', '露娜-黑天际线', '蛊-能天使午夜邮差', + '骇爪-维什戴尔', '骇爪-水墨云图', '威龙-吴彦祖', '威龙-铁面判官', + '威龙-壮志凌云', '威龙-蛟龙特战队', '蜂医-送葬人', '蜂医-危险物质', + '蜂医-黑鹰坠落', '牧羊人-街头之星', '牧羊人-黑鹰坠落', '乌鲁鲁-黑鹰坠落', + '红狼-黑鹰坠落', '无名-夜鹰', '深蓝-不破誓约', '红狼-电锯惊魂', + '露娜-古墓丽影劳拉', '赛伊德', + ], + }, + { + key: 'weapon', + title: '武器皮肤', + options: [ + 'M7棱镜攻势S2', '腾龙气象感应', 'MP7电玩高手S2', 'M250电玩高手S2', + 'AS Val悬赏令', 'K416命运', 'M4A1棱镜攻势', 'SCAR-H电玩高手', + 'QBZ95王牌之剑', 'AUG气象感应', 'Vector美杜莎', 'KC17-造物纪元', + ], + }, + ], + quantity_items: [ + { key: 'awmAmmo', label: 'AWM子弹', price: '0.8元/发' }, + { key: 'helmet6', label: '6头数量', price: '2元/个' }, + { key: 'armor6', label: '6甲数量', price: '3元/个' }, + { key: 'kit5', label: '5级全装套数量', price: '5元/个' }, + { key: 'armor6Ammo', label: '6甲子弹', price: '0.26元/发' }, + { key: 'gridCard9', label: '9格体验卡', price: '5元/张', placeholder: '如果是赛季9格,此处填0即可' }, + ], + screenshot_slots: [ + { key: 'coin', label: '纯币截图', required: true, hint: '请上传纯币截图(必传)' }, + { key: 'gameId', label: '游戏ID截图', required: true, hint: '请上传游戏ID截图(必传)' }, + { key: 'totalAsset', label: '总资产截图', required: true, hint: '请上传总资产截图(必传)' }, + { key: 'tencentSecurity', label: '腾讯安全中心截图', required: true, hint: '请上传腾讯安全中心截图(必传)' }, + { key: 'skin', label: '皮肤截图', required: false, hint: '请上传皮肤截图(选传)' }, + ], +} + +export async function fetchListingPublishOptions() { + const { data } = await apiClient.get>('/listing-publish-options') + return mergeListingPublishOptions(data.data) +} + +export function mergeListingPublishOptions(options?: Partial): ListingPublishOptions { + return { + server_options: options?.server_options?.length ? options.server_options : defaultListingPublishOptions.server_options, + face_options: options?.face_options?.length ? options.face_options : defaultListingPublishOptions.face_options, + rank_options: options?.rank_options?.length ? options.rank_options : defaultListingPublishOptions.rank_options, + insurance_options: options?.insurance_options?.length ? options.insurance_options : defaultListingPublishOptions.insurance_options, + level_options: options?.level_options?.length ? options.level_options : defaultListingPublishOptions.level_options, + login_method_options: options?.login_method_options?.length ? options.login_method_options : defaultListingPublishOptions.login_method_options, + region_options: options?.region_options?.length ? options.region_options : defaultListingPublishOptions.region_options, + skin_groups: options?.skin_groups?.length ? options.skin_groups : defaultListingPublishOptions.skin_groups, + quantity_items: options?.quantity_items?.length ? options.quantity_items as PublishQuantityItem[] : defaultListingPublishOptions.quantity_items, + screenshot_slots: options?.screenshot_slots?.length ? options.screenshot_slots as PublishScreenshotSlot[] : defaultListingPublishOptions.screenshot_slots, + } +} diff --git a/frontend/src/views/admin/AdminSystemConfigsView.vue b/frontend/src/views/admin/AdminSystemConfigsView.vue index f59713c..a69bfde 100644 --- a/frontend/src/views/admin/AdminSystemConfigsView.vue +++ b/frontend/src/views/admin/AdminSystemConfigsView.vue @@ -1,7 +1,12 @@