diff --git a/hunliji-api/hl-api b/hunliji-api/hl-api index a5e690f..f4aa63d 100644 Binary files a/hunliji-api/hl-api and b/hunliji-api/hl-api differ diff --git a/hunliji-api/main.go b/hunliji-api/main.go index 5442d6a..3821a55 100644 --- a/hunliji-api/main.go +++ b/hunliji-api/main.go @@ -35,6 +35,7 @@ func migrateSchema(db *gorm.DB) { {&models.Danmaku{}, "祝福弹幕", "danmakus"}, {&models.SiteLike{}, "站点点赞", "site_likes"}, {&models.SiteGift{}, "站点礼物", "site_gifts"}, + {&models.GiftConfig{}, "礼物兑换配置", "gift_configs"}, {&models.AiModerationCache{}, "AI审核缓存", "ai_moderation_caches"}, {&visit.SiteVisit{}, "访客记录", "site_visits"}, } diff --git a/hunliji-api/models/models.go b/hunliji-api/models/models.go index fcc7cf9..e85d3a5 100644 --- a/hunliji-api/models/models.go +++ b/hunliji-api/models/models.go @@ -71,11 +71,21 @@ type SiteGift struct { GiftType string `gorm:"column:gift_type;size:32;not null;index;comment:礼物类型" json:"gift_type"` Name string `gorm:"column:name;size:50;not null;default:'';comment:送礼人姓名" json:"name"` ClientID string `gorm:"column:client_id;size:64;not null;default:'';index;comment:客户端标识" json:"client_id"` + Cost int64 `gorm:"column:cost;not null;default:0;comment:兑换消耗的点赞数" json:"cost"` CreatedAt time.Time `gorm:"column:created_at;not null;comment:送礼时间" json:"created_at"` } func (SiteGift) TableName() string { return "site_gifts" } +// GiftConfig 礼物兑换配置(各礼物所需点赞数) +type GiftConfig struct { + ID uint `gorm:"primaryKey;comment:主键ID" json:"id"` + CostsData string `gorm:"type:json;column:costs_data;not null;comment:礼物代价JSON map" json:"costs_data"` + UpdatedAt time.Time `gorm:"column:updated_at;comment:更新时间" json:"updated_at"` +} + +func (GiftConfig) TableName() string { return "gift_configs" } + // AiModerationCache 缓存 AI 审核通过/失败结果 type AiModerationCache struct { ID uint `gorm:"primaryKey;comment:主键ID" json:"id"` diff --git a/hunliji-api/router/routers.go b/hunliji-api/router/routers.go index 313d3ef..4a1a3d5 100644 --- a/hunliji-api/router/routers.go +++ b/hunliji-api/router/routers.go @@ -86,39 +86,96 @@ func loadGiftCounts() map[string]int64 { return counts } +// defaultGiftCosts 默认三阶梯:50 / 100 / 200 +var defaultGiftCosts = map[string]int64{ + "520": 50, + "forever": 50, + "bald": 50, + "concentric": 50, + "1314": 100, + "heaven": 100, + "moisten": 100, + "match": 200, +} + const ( - giftBaseAllow int64 = 1 - giftLikesPerExtra int64 = 50 + giftCostMin int64 = 1 + giftCostMax int64 = 10000 + likeBatchMax = 200 ) -// giftQuota 每个 client 基础 1 次赠送,每点赞 50 次多 1 次 -func giftQuota(clientID string) (myLikes, used, allow, remain, likesToNext int64) { +func defaultGiftCostsCopy() map[string]int64 { + out := make(map[string]int64, len(defaultGiftCosts)) + for k, v := range defaultGiftCosts { + out[k] = v + } + return out +} + +func loadGiftCosts() map[string]int64 { + costs := defaultGiftCostsCopy() + var cfg models.GiftConfig + if err := db.First(&cfg, 1).Error; err != nil || strings.TrimSpace(cfg.CostsData) == "" { + return costs + } + parsed := map[string]int64{} + if err := json.Unmarshal([]byte(cfg.CostsData), &parsed); err != nil { + return costs + } + for k := range giftTypeWhitelist { + if v, ok := parsed[k]; ok && v >= giftCostMin && v <= giftCostMax { + costs[k] = v + } + } + return costs +} + +func saveGiftCosts(costs map[string]int64) error { + normalized := defaultGiftCostsCopy() + for k := range giftTypeWhitelist { + if v, ok := costs[k]; ok && v >= giftCostMin && v <= giftCostMax { + normalized[k] = v + } + } + raw, err := json.Marshal(normalized) + if err != nil { + return err + } + var cfg models.GiftConfig + if err := db.First(&cfg, 1).Error; err != nil { + cfg = models.GiftConfig{ID: 1, CostsData: string(raw)} + return db.Create(&cfg).Error + } + return db.Model(&cfg).Update("costs_data", string(raw)).Error +} + +// giftBalance 点赞余额:可用 = 我的点赞 - 已兑换消耗 +func giftBalance(clientID string) (myLikes, spent, available int64) { clientID = strings.TrimSpace(clientID) - if clientID != "" { - db.Model(&models.SiteLike{}).Where("client_id = ?", clientID).Count(&myLikes) - db.Model(&models.SiteGift{}).Where("client_id = ?", clientID).Count(&used) + if clientID == "" { + return 0, 0, 0 } - allow = giftBaseAllow + myLikes/giftLikesPerExtra - remain = allow - used - if remain < 0 { - remain = 0 - } - likesToNext = giftLikesPerExtra - (myLikes % giftLikesPerExtra) - if likesToNext <= 0 { - likesToNext = giftLikesPerExtra + db.Model(&models.SiteLike{}).Where("client_id = ?", clientID).Count(&myLikes) + db.Model(&models.SiteGift{}).Where("client_id = ?", clientID).Select("COALESCE(SUM(cost), 0)").Scan(&spent) + available = myLikes - spent + if available < 0 { + available = 0 } return } func giftQuotaPayload(clientID string) gin.H { - myLikes, used, allow, remain, likesToNext := giftQuota(clientID) + myLikes, spent, available := giftBalance(clientID) + var giftUsed int64 + if strings.TrimSpace(clientID) != "" { + db.Model(&models.SiteGift{}).Where("client_id = ?", clientID).Count(&giftUsed) + } return gin.H{ - "my_likes": myLikes, - "gift_used": used, - "gift_allow": allow, - "gift_remain": remain, - "likes_to_next": likesToNext, - "likes_per_gift": giftLikesPerExtra, + "my_likes": myLikes, + "spent": spent, + "available": available, + "gift_used": giftUsed, + "costs": loadGiftCosts(), } } @@ -156,6 +213,8 @@ func registerAPI(api *gin.RouterGroup) { api.GET("/gift", handleGiftStatus) api.POST("/gift", handleCreateGift) api.GET("/gift/stats", handleGiftStats) + api.GET("/gift/costs", handleGetGiftCosts) + api.POST("/gift/costs", handleSaveGiftCosts) visit.Register(api, db, utils.RequireAdmin) configsection.Register(api, db, utils.RequireAdmin) @@ -457,6 +516,7 @@ func handleLikeCount(c *gin.Context) { func handleLike(c *gin.Context) { var body struct { ClientID string `json:"client_id"` + Count int `json:"count"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(400, gin.H{"error": "参数错误"}) @@ -467,13 +527,34 @@ func handleLike(c *gin.Context) { c.JSON(400, gin.H{"error": "无效的 client_id"}) return } - if err := db.Create(&models.SiteLike{ClientID: clientID}).Error; err != nil { + n := body.Count + if n <= 0 { + n = 1 + } + if n > likeBatchMax { + c.JSON(400, gin.H{"error": fmt.Sprintf("单次最多点赞 %d 次", likeBatchMax)}) + return + } + rows := make([]models.SiteLike, n) + now := time.Now() + for i := 0; i < n; i++ { + rows[i] = models.SiteLike{ClientID: clientID, CreatedAt: now} + } + if err := db.CreateInBatches(&rows, 100).Error; err != nil { c.JSON(500, gin.H{"error": "点赞失败"}) return } var count int64 db.Model(&models.SiteLike{}).Count(&count) - c.JSON(200, gin.H{"code": 200, "data": gin.H{"count": count}, "msg": "点赞成功"}) + c.JSON(200, gin.H{ + "code": 200, + "data": gin.H{ + "count": count, + "added": n, + "quota": giftQuotaPayload(clientID), + }, + "msg": "点赞成功", + }) } func handleGiftStatus(c *gin.Context) { @@ -541,10 +622,16 @@ func handleCreateGift(c *gin.Context) { c.JSON(400, gin.H{"error": "无效的 client_id"}) return } - _, _, _, remain, likesToNext := giftQuota(clientID) - if remain <= 0 { + costs := loadGiftCosts() + cost := costs[giftType] + if cost < giftCostMin { + cost = defaultGiftCosts[giftType] + } + _, _, available := giftBalance(clientID) + if available < cost { + need := cost - available c.JSON(400, gin.H{ - "error": fmt.Sprintf("赠送次数已用完,再点赞 %d 次可多送 1 次", likesToNext), + "error": fmt.Sprintf("点赞不足,再点赞 %d 次可兑换「%s」", need, giftTypeWhitelist[giftType]), "data": giftQuotaPayload(clientID), }) return @@ -553,6 +640,7 @@ func handleCreateGift(c *gin.Context) { GiftType: giftType, Name: name, ClientID: clientID, + Cost: cost, }).Error; err != nil { c.JSON(500, gin.H{"error": "送礼失败"}) return @@ -568,6 +656,7 @@ func handleCreateGift(c *gin.Context) { "counts": counts, "total": total, "gift_type": giftType, + "cost": cost, "quota": giftQuotaPayload(clientID), }, "msg": "送礼成功", @@ -593,10 +682,60 @@ func handleGiftStats(c *gin.Context) { "counts": counts, "labels": labels, "total": total, + "costs": loadGiftCosts(), }, }) } +func handleGetGiftCosts(c *gin.Context) { + if !utils.RequireAdmin(c) { + return + } + labels := make(map[string]string, len(giftTypeWhitelist)) + for k, v := range giftTypeWhitelist { + labels[k] = v + } + c.JSON(200, gin.H{ + "code": 200, + "data": gin.H{ + "costs": loadGiftCosts(), + "labels": labels, + "defaults": defaultGiftCostsCopy(), + }, + }) +} + +func handleSaveGiftCosts(c *gin.Context) { + if !utils.RequireAdmin(c) { + return + } + var body struct { + Costs map[string]int64 `json:"costs"` + } + if err := c.ShouldBindJSON(&body); err != nil || body.Costs == nil { + c.JSON(400, gin.H{"error": "参数错误"}) + return + } + for k, v := range body.Costs { + if _, ok := giftTypeWhitelist[k]; !ok { + continue + } + if v < giftCostMin || v > giftCostMax { + c.JSON(400, gin.H{"error": fmt.Sprintf("「%s」兑换点赞需在 %d–%d 之间", giftTypeWhitelist[k], giftCostMin, giftCostMax)}) + return + } + } + if err := saveGiftCosts(body.Costs); err != nil { + c.JSON(500, gin.H{"error": "保存失败"}) + return + } + c.JSON(200, gin.H{ + "code": 200, + "data": gin.H{"costs": loadGiftCosts()}, + "msg": "礼物兑换配置已保存", + }) +} + func normalizeDanmakuColor(color string) string { c := strings.TrimSpace(color) if c == "rose" || c == "sage" { diff --git a/vite-tailwindcss/src/api/wedding.js b/vite-tailwindcss/src/api/wedding.js index c2adf6f..60adaf6 100644 --- a/vite-tailwindcss/src/api/wedding.js +++ b/vite-tailwindcss/src/api/wedding.js @@ -134,8 +134,8 @@ export function getLikeStatus(clientId) { return service.get('/like', { params: { client_id: clientId } }) } -export function submitLike(clientId) { - return service.post('/like', { client_id: clientId }) +export function submitLike(clientId, count = 1) { + return service.post('/like', { client_id: clientId, count }) } export function getGiftStatus(clientId) { @@ -154,6 +154,14 @@ export function getGiftStats() { return service.get('/gift/stats') } +export function getGiftCosts() { + return service.get('/gift/costs') +} + +export function saveGiftCosts(costs) { + return service.post('/gift/costs', { costs }) +} + export function adminLogin(credentials) { return service.post('/admin/login', credentials) } diff --git a/vite-tailwindcss/src/components/DanmakuLayer.vue b/vite-tailwindcss/src/components/DanmakuLayer.vue index a4acf25..eac121a 100644 --- a/vite-tailwindcss/src/components/DanmakuLayer.vue +++ b/vite-tailwindcss/src/components/DanmakuLayer.vue @@ -1,5 +1,6 @@