Files
hunli/hunliji-api/router/routers.go
2026-08-03 16:00:13 +08:00

1853 lines
46 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package router
import (
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"log"
"mime/multipart"
"net/url"
"os"
"path/filepath"
"strconv"
"strings"
"sync"
"time"
"hunliji-api/configsection"
"hunliji-api/models"
"hunliji-api/posterconfig"
"hunliji-api/spark"
"hunliji-api/utils"
"hunliji-api/visit"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss"
"github.com/aliyun/alibabacloud-oss-go-sdk-v2/oss/credentials"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type Deps struct {
DB *gorm.DB
Spark *spark.Client
AdminAccount string
AdminPassword string
}
var (
db *gorm.DB
sparkClient *spark.Client
adminAccount string
adminPassword string
)
var danmakuColorWhitelist = map[string]struct{}{
"champagne": {}, "blush": {}, "apricot": {}, "gold": {}, "lilac": {},
"sky": {}, "mauve": {}, "slate": {}, "ink": {},
"gradSunset": {}, "gradChampagne": {}, "gradBlush": {},
"gradOcean": {}, "gradAurora": {}, "gradEmber": {},
}
// giftTypeWhitelist 礼物类型白名单(与前端常量表一致)
var giftTypeWhitelist = map[string]string{
"520": "520",
"1314": "1314",
"forever": "长长久久",
"bald": "白头偕老",
"concentric": "永结同心",
"heaven": "天作之合",
"moisten": "相濡以沫",
"match": "佳偶天成",
}
func emptyGiftCounts() map[string]int64 {
counts := make(map[string]int64, len(giftTypeWhitelist))
for k := range giftTypeWhitelist {
counts[k] = 0
}
return counts
}
func loadGiftCounts() map[string]int64 {
counts := emptyGiftCounts()
type row struct {
GiftType string `gorm:"column:gift_type"`
Cnt int64 `gorm:"column:cnt"`
}
var rows []row
db.Model(&models.SiteGift{}).
Select("gift_type, COUNT(*) as cnt").
Group("gift_type").
Find(&rows)
for _, r := range rows {
if _, ok := giftTypeWhitelist[r.GiftType]; ok {
counts[r.GiftType] = r.Cnt
}
}
return counts
}
// defaultGiftCosts 默认三阶梯50 / 100 / 200永结同心为终章默认 200
var defaultGiftCosts = map[string]int64{
"520": 50,
"forever": 50,
"bald": 50,
"1314": 100,
"heaven": 100,
"moisten": 100,
"match": 200,
"concentric": 200,
}
const (
giftCostMin int64 = 1
giftCostMax int64 = 10000
likeBatchMax = 200
)
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 == "" {
return 0, 0, 0
}
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, 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,
"spent": spent,
"available": available,
"gift_used": giftUsed,
"costs": loadGiftCosts(),
}
}
// Register 挂载静态资源、中间件与全部 /api 路由
func Register(r *gin.Engine, deps Deps) {
db = deps.DB
sparkClient = deps.Spark
adminAccount = deps.AdminAccount
adminPassword = deps.AdminPassword
r.Use(utils.CORSMiddleware())
r.Static("/uploads", "./uploads")
api := r.Group("/api")
registerAPI(api)
}
func registerAPI(api *gin.RouterGroup) {
api.POST("/admin/login", handleAdminLogin)
api.GET("/config", handleGetConfig)
api.POST("/config", handleSaveConfig)
api.GET("/album", handleAlbum)
api.GET("/upload/config", handleGetUploadConfig)
api.POST("/upload/config", handleSaveUploadConfig)
api.POST("/upload", handleUpload)
api.GET("/upload/assets", handleUploadAssets)
api.POST("/ai/blessing", handleAIBlessing)
api.POST("/rsvp", handleCreateRsvp)
api.GET("/rsvp/list", handleRsvpList)
api.DELETE("/rsvp/:id", handleDeleteRsvp)
api.GET("/danmaku", handlePublicDanmaku)
api.POST("/danmaku", handleCreateDanmaku)
api.GET("/danmaku/list", handleDanmakuList)
api.POST("/danmaku/:id/status", handleDanmakuStatus)
api.GET("/like", handleLikeCount)
api.POST("/like", handleLike)
api.GET("/gift", handleGiftStatus)
api.POST("/gift", handleCreateGift)
api.GET("/gift/stats", handleGiftStats)
api.GET("/gift/list", handleGiftList)
api.DELETE("/gift/:id", handleDeleteGift)
api.GET("/gift/costs", handleGetGiftCosts)
api.POST("/gift/costs", handleSaveGiftCosts)
api.POST("/cash-gift", handleCreateCashGift)
api.GET("/cash-gift/recent", handleCashGiftRecent)
api.GET("/cash-gift/notes", handleGetCashGiftNotes)
api.POST("/cash-gift/notes", handleSaveCashGiftNotes)
api.GET("/cash-gift/view-pass", handleGetCashGiftViewPass)
api.POST("/cash-gift/view-pass", handleSaveCashGiftViewPass)
api.POST("/cash-gift/view/unlock", handleCashGiftViewUnlock)
api.GET("/cash-gift/view/list", handleCashGiftViewList)
api.GET("/cash-gift/entry-pass", handleGetCashGiftEntryPass)
api.POST("/cash-gift/entry-pass", handleSaveCashGiftEntryPass)
api.POST("/cash-gift/entry/unlock", handleCashGiftEntryUnlock)
api.GET("/cash-gift/list", handleCashGiftList)
api.PUT("/cash-gift/:id", handleUpdateCashGift)
api.DELETE("/cash-gift/:id", handleDeleteCashGift)
visit.Register(api, db, utils.RequireAdmin)
configsection.Register(api, db, utils.RequireAdmin)
posterconfig.Register(api, db, utils.RequireAdmin)
}
func handleAdminLogin(c *gin.Context) {
var body struct {
Account string `json:"account"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
if body.Account != adminAccount || body.Password != adminPassword {
c.JSON(401, gin.H{"error": "账号或密码错误"})
return
}
c.JSON(200, gin.H{
"code": 200,
"token": utils.SignToken(body.Account),
"user": gin.H{"account": body.Account},
})
}
func handleGetConfig(c *gin.Context) {
var config models.TemplateConfig
if err := db.First(&config, 1).Error; err != nil {
c.JSON(200, gin.H{"code": 200, "data": json.RawMessage(`{}`)})
return
}
raw := strings.TrimSpace(config.ConfigData)
if raw == "" {
raw = "{}"
}
c.JSON(200, gin.H{"code": 200, "data": json.RawMessage(raw)})
}
func handleSaveConfig(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var rawBody interface{}
if err := c.BindJSON(&rawBody); err != nil {
c.JSON(400, gin.H{"error": "无效的JSON数据"})
return
}
jsonBytes, _ := json.Marshal(rawBody)
var config models.TemplateConfig
db.Attrs(models.TemplateConfig{ConfigData: "{}"}).FirstOrCreate(&config, models.TemplateConfig{ID: 1})
config.ConfigData = string(jsonBytes)
if err := db.Save(&config).Error; err != nil {
c.JSON(500, gin.H{"error": "数据库保存配置失败"})
return
}
// 保存后从配置抓取 OSS/远程图写入 media_assets
go harvestConfigMediaAssets()
c.JSON(200, gin.H{"code": 200, "msg": "配置保存成功"})
}
func handleAlbum(c *gin.Context) {
var config models.TemplateConfig
if err := db.First(&config, 1).Error; err != nil {
c.JSON(200, gin.H{"code": 200, "data": []string{}})
return
}
c.JSON(200, gin.H{"code": 200, "data": collectAlbumImages(config.ConfigData)})
}
func handleGetUploadConfig(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
c.JSON(200, gin.H{"code": 200, "data": uploadConfigResponse(loadUploadConfig())})
}
func handleSaveUploadConfig(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var body struct {
Provider string `json:"provider"`
AccessKeyID string `json:"accessKeyId"`
AccessKeySecret string `json:"accessKeySecret"`
Bucket string `json:"bucket"`
Folder string `json:"folder"`
Domain string `json:"domain"`
Region string `json:"region"`
Endpoint string `json:"endpoint"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
config := loadUploadConfig()
provider := strings.TrimSpace(body.Provider)
if provider != "oss" {
provider = "local"
}
config.Provider = provider
config.AccessKeyID = strings.TrimSpace(body.AccessKeyID)
if strings.TrimSpace(body.AccessKeySecret) != "" {
config.AccessKeySecret = strings.TrimSpace(body.AccessKeySecret)
}
config.Bucket = strings.TrimSpace(body.Bucket)
config.Folder = strings.Trim(body.Folder, "/ ")
config.Domain = strings.TrimSpace(body.Domain)
config.Region = strings.TrimSpace(body.Region)
config.Endpoint = strings.TrimSpace(body.Endpoint)
if err := db.Save(&config).Error; err != nil {
c.JSON(500, gin.H{"error": "上传配置保存失败"})
return
}
c.JSON(200, gin.H{"code": 200, "data": uploadConfigResponse(config), "msg": "上传配置保存成功"})
}
func handleUpload(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
file, err := c.FormFile("file")
if err != nil {
c.JSON(400, gin.H{"error": "未获取到上传文件"})
return
}
config := loadUploadConfig()
if canUseOSS(config) {
saveOSSUpload(c, file, config)
return
}
saveLocalUpload(c, file)
}
func recordMediaAsset(fileURL, name, provider string) {
fileURL = strings.TrimSpace(fileURL)
if fileURL == "" {
return
}
// 与表字段 varchar(768) 对齐,避免超长写入失败
if len(fileURL) > 768 {
fileURL = fileURL[:768]
}
var n int64
if db.Model(&models.MediaAsset{}).Where("url = ?", fileURL).Limit(1).Count(&n); n > 0 {
return
}
if strings.TrimSpace(provider) == "" {
provider = guessMediaProvider(fileURL)
}
if strings.TrimSpace(name) == "" {
name = mediaNameFromURL(fileURL)
}
_ = db.Create(&models.MediaAsset{
URL: fileURL,
Name: strings.TrimSpace(name),
Provider: provider,
}).Error
}
// recordMediaAssetsBulk 批量登记:先一次性拉取已有 URL再只插入缺失项
func recordMediaAssetsBulk(items []models.MediaAsset) {
if len(items) == 0 {
return
}
var existing []string
_ = db.Model(&models.MediaAsset{}).Pluck("url", &existing)
have := make(map[string]bool, len(existing))
for _, u := range existing {
have[u] = true
}
toCreate := make([]models.MediaAsset, 0, 16)
for _, it := range items {
u := strings.TrimSpace(it.URL)
if u == "" {
continue
}
if len(u) > 768 {
u = u[:768]
}
if have[u] {
continue
}
have[u] = true
name := strings.TrimSpace(it.Name)
if name == "" {
name = mediaNameFromURL(u)
}
provider := strings.TrimSpace(it.Provider)
if provider == "" {
provider = guessMediaProvider(u)
}
toCreate = append(toCreate, models.MediaAsset{
URL: u,
Name: name,
Provider: provider,
})
}
if len(toCreate) == 0 {
return
}
_ = db.CreateInBatches(&toCreate, 100).Error
}
// harvestConfigMediaAssets 从请柬/海报配置抓取已用图片(含 OSS写入 media_assets不调 OSS 列表接口
func harvestConfigMediaAssets() {
items := make([]models.MediaAsset, 0, 64)
var tpl models.TemplateConfig
if err := db.First(&tpl, 1).Error; err == nil {
for _, u := range collectImageURLsFromJSON(tpl.ConfigData) {
items = append(items, models.MediaAsset{
URL: u,
Name: mediaNameFromURL(u),
Provider: guessMediaProvider(u),
})
}
}
var poster models.PosterConfig
if err := db.First(&poster, 1).Error; err == nil {
for _, u := range collectImageURLsFromJSON(poster.ConfigData) {
items = append(items, models.MediaAsset{
URL: u,
Name: mediaNameFromURL(u),
Provider: guessMediaProvider(u),
})
}
}
recordMediaAssetsBulk(items)
}
func scanLocalUploadAssets() []models.MediaAsset {
entries, err := os.ReadDir("./uploads")
if err != nil {
return nil
}
out := make([]models.MediaAsset, 0, len(entries))
for _, e := range entries {
if e.IsDir() || !isImageFilename(e.Name()) {
continue
}
info, err := e.Info()
if err != nil {
continue
}
out = append(out, models.MediaAsset{
URL: fmt.Sprintf("http://localhost:8080/uploads/%s", e.Name()),
Name: e.Name(),
Provider: "local",
CreatedAt: info.ModTime(),
})
}
return out
}
func syncMediaLibrary() {
harvestConfigMediaAssets()
recordMediaAssetsBulk(scanLocalUploadAssets())
}
var (
mediaSyncMu sync.Mutex
mediaSyncing bool
mediaLastSync time.Time
)
// trySyncMediaLibraryAsync 后台低频同步图库,不阻塞列表接口
func trySyncMediaLibraryAsync() {
mediaSyncMu.Lock()
if mediaSyncing || time.Since(mediaLastSync) < 2*time.Minute {
mediaSyncMu.Unlock()
return
}
mediaSyncing = true
mediaSyncMu.Unlock()
go func() {
defer func() {
mediaSyncMu.Lock()
mediaSyncing = false
mediaLastSync = time.Now()
mediaSyncMu.Unlock()
}()
syncMediaLibrary()
}()
}
func handleUploadAssets(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
// 默认只读库refresh=1 时同步补全后再返回(手动刷新)
if strings.TrimSpace(c.Query("refresh")) == "1" {
syncMediaLibrary()
} else {
trySyncMediaLibraryAsync()
}
page := 1
pageSize := 6
if v := strings.TrimSpace(c.Query("page")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
page = n
}
}
if v := strings.TrimSpace(c.Query("pageSize")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 100 {
pageSize = n
}
}
var total int64
db.Model(&models.MediaAsset{}).Count(&total)
var list []models.MediaAsset
offset := (page - 1) * pageSize
db.Order("created_at desc, id desc").Offset(offset).Limit(pageSize).Find(&list)
c.JSON(200, gin.H{
"code": 200,
"data": list,
"total": total,
"page": page,
"pageSize": pageSize,
})
}
func guessMediaProvider(fileURL string) string {
u := strings.ToLower(strings.TrimSpace(fileURL))
if strings.Contains(u, "/uploads/") || strings.Contains(u, "localhost") || strings.Contains(u, "127.0.0.1") {
return "local"
}
return "oss"
}
func mediaNameFromURL(fileURL string) string {
u := strings.TrimSpace(fileURL)
if i := strings.IndexAny(u, "?#"); i >= 0 {
u = u[:i]
}
base := filepath.Base(u)
if base == "" || base == "." || base == "/" {
return "图片"
}
if dec, err := url.PathUnescape(base); err == nil && dec != "" {
return dec
}
return base
}
func isImageFilename(name string) bool {
ext := strings.ToLower(filepath.Ext(name))
switch ext {
case ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".svg", ".avif":
return true
default:
return false
}
}
func looksLikeImageURL(s string) bool {
s = strings.TrimSpace(s)
if !strings.HasPrefix(s, "http://") && !strings.HasPrefix(s, "https://") {
return false
}
path := s
if i := strings.IndexAny(path, "?#"); i >= 0 {
path = path[:i]
}
if isImageFilename(path) {
return true
}
lower := strings.ToLower(path)
// 配置里常见 OSS / CDN 图(无扩展名时也尽量纳入)
for _, hint := range []string{
"aliyuncs.com", "oss-", "/uploads/", ".jpg", ".jpeg", ".png", ".gif", ".webp", ".bmp", ".avif",
} {
if strings.Contains(lower, hint) {
return true
}
}
return false
}
func collectImageURLsFromValue(v interface{}, out *[]string, seen map[string]bool) {
switch t := v.(type) {
case string:
src := strings.TrimSpace(t)
if !looksLikeImageURL(src) || seen[src] {
return
}
seen[src] = true
*out = append(*out, src)
case []interface{}:
for _, item := range t {
collectImageURLsFromValue(item, out, seen)
}
case map[string]interface{}:
for _, item := range t {
collectImageURLsFromValue(item, out, seen)
}
}
}
func collectImageURLsFromJSON(rawJSON string) []string {
rawJSON = strings.TrimSpace(rawJSON)
if rawJSON == "" {
return nil
}
var parsed interface{}
if err := json.Unmarshal([]byte(rawJSON), &parsed); err != nil {
return nil
}
out := make([]string, 0, 32)
seen := map[string]bool{}
collectImageURLsFromValue(parsed, &out, seen)
return out
}
func handleAIBlessing(c *gin.Context) {
if sparkClient == nil || !sparkClient.Enabled() {
c.JSON(503, gin.H{"error": "AI 未配置"})
return
}
var body struct {
Style string `json:"style"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
if _, ok := spark.NormalizeStyle(body.Style); !ok {
c.JSON(400, gin.H{"error": "请选择祝福风格"})
return
}
text, err := sparkClient.GenerateBlessing(body.Style)
if err != nil {
c.JSON(500, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"code": 200, "data": gin.H{"text": text}})
}
func handleCreateRsvp(c *gin.Context) {
var rsvp models.Rsvp
if err := c.ShouldBindJSON(&rsvp); err != nil {
c.JSON(400, gin.H{"error": "表单数据绑定失败", "details": err.Error()})
return
}
wishes := strings.TrimSpace(rsvp.Wishes)
if wishes != "" {
ok, reason := moderateOrPass(rsvp.Name, wishes, "rsvp")
if !ok {
c.JSON(400, gin.H{"error": "内容未通过审核:" + reason})
return
}
}
if err := db.Create(&rsvp).Error; err != nil {
c.JSON(500, gin.H{"error": "出席信息写入数据库失败"})
return
}
var danmaku *models.Danmaku
if wishes != "" {
rid := rsvp.ID
item := models.Danmaku{
Name: rsvp.Name, Content: wishes, Color: "champagne",
Source: "rsvp", RsvpID: &rid, Status: "approved",
}
if err := db.Create(&item).Error; err == nil {
danmaku = &item
}
}
resp := gin.H{"code": 200, "msg": "回执提交成功"}
if danmaku != nil {
resp["danmaku"] = danmaku
}
c.JSON(200, resp)
}
func handleRsvpList(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var list []models.Rsvp
db.Order("created_at desc").Find(&list)
c.JSON(200, gin.H{"code": 200, "data": list})
}
func handleDeleteRsvp(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
c.JSON(400, gin.H{"error": "缺少 ID"})
return
}
res := db.Delete(&models.Rsvp{}, id)
if res.Error != nil {
c.JSON(500, gin.H{"error": "删除失败"})
return
}
if res.RowsAffected == 0 {
c.JSON(404, gin.H{"error": "记录不存在"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "已删除"})
}
func handlePublicDanmaku(c *gin.Context) {
q := db.Where("status = ?", "approved")
if after := strings.TrimSpace(c.Query("after_id")); after != "" {
var afterID uint64
if _, err := fmt.Sscanf(after, "%d", &afterID); err == nil && afterID > 0 {
q = q.Where("id > ?", afterID)
}
}
var list []models.Danmaku
q.Order("id asc").Find(&list)
c.JSON(200, gin.H{"code": 200, "data": list})
}
func handleCreateDanmaku(c *gin.Context) {
var body struct {
Name string `json:"name"`
Content string `json:"content"`
Color string `json:"color"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "表单数据绑定失败", "details": err.Error()})
return
}
name := strings.TrimSpace(body.Name)
content := strings.TrimSpace(body.Content)
if name == "" || content == "" {
c.JSON(400, gin.H{"error": "请填写姓名和祝福"})
return
}
if len([]rune(name)) > 50 {
c.JSON(400, gin.H{"error": "姓名过长"})
return
}
ok, reason := moderateOrPass(name, content, "danmaku")
if !ok {
c.JSON(400, gin.H{"error": "内容未通过审核:" + reason})
return
}
item := models.Danmaku{
Name: name, Content: content, Color: normalizeDanmakuColor(body.Color),
Source: "danmaku", Status: "approved",
}
if err := db.Create(&item).Error; err != nil {
c.JSON(500, gin.H{"error": "祝福提交失败"})
return
}
c.JSON(200, gin.H{"code": 200, "data": item, "msg": "发送成功"})
}
func handleDanmakuList(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
q := db.Order("created_at desc")
if status := strings.TrimSpace(c.Query("status")); status != "" {
q = q.Where("status = ?", status)
}
var list []models.Danmaku
q.Find(&list)
c.JSON(200, gin.H{"code": 200, "data": list})
}
func handleDanmakuStatus(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var body struct {
Status string `json:"status"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
status := strings.TrimSpace(body.Status)
if status != "approved" && status != "rejected" && status != "pending" {
c.JSON(400, gin.H{"error": "无效的状态"})
return
}
id := c.Param("id")
res := db.Model(&models.Danmaku{}).Where("id = ?", id).Update("status", status)
if res.Error != nil {
c.JSON(500, gin.H{"error": "更新失败"})
return
}
if res.RowsAffected == 0 {
c.JSON(404, gin.H{"error": "记录不存在"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "状态已更新"})
}
func handleLikeCount(c *gin.Context) {
var count int64
db.Model(&models.SiteLike{}).Count(&count)
c.JSON(200, gin.H{"code": 200, "data": gin.H{"count": count}})
}
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": "参数错误"})
return
}
clientID := strings.TrimSpace(body.ClientID)
if clientID == "" || len(clientID) > 64 {
c.JSON(400, gin.H{"error": "无效的 client_id"})
return
}
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,
"added": n,
"quota": giftQuotaPayload(clientID),
},
"msg": "点赞成功",
})
}
func handleGiftStatus(c *gin.Context) {
counts := loadGiftCounts()
var recent []models.SiteGift
db.Model(&models.SiteGift{}).
Order("id DESC").
Limit(20).
Find(&recent)
items := make([]gin.H, 0, len(recent))
for _, g := range recent {
items = append(items, gin.H{
"gift_type": g.GiftType,
"name": g.Name,
"created_at": g.CreatedAt,
})
}
// 回放按时间正序(最早在前)
for i, j := 0, len(items)-1; i < j; i, j = i+1, j-1 {
items[i], items[j] = items[j], items[i]
}
var total int64
for _, n := range counts {
total += n
}
clientID := strings.TrimSpace(c.Query("client_id"))
quota := giftQuotaPayload(clientID)
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"counts": counts,
"total": total,
"recent": items,
"quota": quota,
},
})
}
func handleCreateGift(c *gin.Context) {
var body struct {
GiftType string `json:"gift_type"`
Name string `json:"name"`
ClientID string `json:"client_id"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
giftType := strings.TrimSpace(body.GiftType)
if _, ok := giftTypeWhitelist[giftType]; !ok {
c.JSON(400, gin.H{"error": "无效的礼物类型"})
return
}
name := strings.TrimSpace(body.Name)
if name == "" {
c.JSON(400, gin.H{"error": "请填写姓名"})
return
}
if len([]rune(name)) > 50 {
c.JSON(400, gin.H{"error": "姓名过长"})
return
}
clientID := strings.TrimSpace(body.ClientID)
if clientID == "" || len(clientID) > 64 {
c.JSON(400, gin.H{"error": "无效的 client_id"})
return
}
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 次可兑换「%s」", need, giftTypeWhitelist[giftType]),
"data": giftQuotaPayload(clientID),
})
return
}
if err := db.Create(&models.SiteGift{
GiftType: giftType,
Name: name,
ClientID: clientID,
Cost: cost,
}).Error; err != nil {
c.JSON(500, gin.H{"error": "送礼失败"})
return
}
counts := loadGiftCounts()
var total int64
for _, n := range counts {
total += n
}
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"counts": counts,
"total": total,
"gift_type": giftType,
"cost": cost,
"quota": giftQuotaPayload(clientID),
},
"msg": "送礼成功",
})
}
func handleGiftStats(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
counts := loadGiftCounts()
var total int64
for _, n := range counts {
total += n
}
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{
"counts": counts,
"labels": labels,
"total": total,
"costs": loadGiftCosts(),
},
})
}
func handleGiftList(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var list []models.SiteGift
db.Order("created_at desc, id desc").Find(&list)
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"list": list,
"count": len(list),
},
})
}
func handleDeleteGift(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
c.JSON(400, gin.H{"error": "缺少 ID"})
return
}
res := db.Delete(&models.SiteGift{}, id)
if res.Error != nil {
c.JSON(500, gin.H{"error": "删除失败"})
return
}
if res.RowsAffected == 0 {
c.JSON(404, gin.H{"error": "记录不存在"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "已删除"})
}
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" {
return "champagne"
}
if _, ok := danmakuColorWhitelist[c]; ok {
return c
}
return "champagne"
}
func moderationCacheContent(name, content string) string {
return strings.TrimSpace(name) + "\n" + strings.TrimSpace(content)
}
func hashModerationContent(content string) string {
sum := sha256.Sum256([]byte(content))
return hex.EncodeToString(sum[:])
}
func saveModerationCache(typ, content, status, reason string) {
row := models.AiModerationCache{
Content: content, ContentHash: hashModerationContent(content),
Type: typ, Status: status, Reason: reason,
}
if err := db.Create(&row).Error; err != nil {
log.Printf("写入审核缓存失败: %v", err)
}
}
func moderateOrPass(name, content, typ string) (bool, string) {
guest := strings.TrimSpace(name)
body := strings.TrimSpace(content)
if body == "" {
return true, ""
}
cacheKey := moderationCacheContent(guest, body)
cacheHash := hashModerationContent(cacheKey)
var cached models.AiModerationCache
err := db.Where("type = ? AND content_hash = ?", typ, cacheHash).
Order("id desc").First(&cached).Error
if err == nil {
if cached.Status == "approved" {
return true, ""
}
reason := strings.TrimSpace(cached.Reason)
if reason == "" {
reason = "内容未通过审核"
}
return false, reason
}
if sparkClient == nil || !sparkClient.Enabled() {
return true, ""
}
ok, reason, err := sparkClient.ModerateText(guest, body)
if err != nil {
log.Printf("AI 审核失败,放行: %v", err)
return true, ""
}
if !ok {
if strings.TrimSpace(reason) == "" {
reason = "内容未通过审核"
}
saveModerationCache(typ, cacheKey, "rejected", reason)
return false, reason
}
saveModerationCache(typ, cacheKey, "approved", "")
return true, ""
}
func defaultUploadConfig() models.UploadConfig {
return models.UploadConfig{ID: 1, Provider: "local"}
}
func loadUploadConfig() models.UploadConfig {
var config models.UploadConfig
if err := db.First(&config, 1).Error; err != nil {
config = defaultUploadConfig()
_ = db.Create(&config).Error
}
if config.Provider == "" {
config.Provider = "local"
}
return config
}
func uploadConfigResponse(config models.UploadConfig) gin.H {
return gin.H{
"provider": config.Provider, "accessKeyId": config.AccessKeyID,
"hasSecret": config.AccessKeySecret != "", "bucket": config.Bucket,
"folder": config.Folder, "domain": config.Domain,
"region": config.Region, "endpoint": config.Endpoint,
"updated_at": config.UpdatedAt,
}
}
func objectURL(config models.UploadConfig, objectKey string) string {
escapedKey := utils.EscapedObjectKey(objectKey)
if strings.TrimSpace(config.Domain) != "" {
domain := strings.TrimRight(strings.TrimSpace(config.Domain), "/")
if !strings.HasPrefix(domain, "http://") && !strings.HasPrefix(domain, "https://") {
domain = "https://" + domain
}
return domain + "/" + escapedKey
}
endpoint := strings.TrimRight(utils.NormalizeEndpoint(config.Endpoint, config.Region), "/")
u, err := url.Parse(endpoint)
if err == nil && u.Host != "" {
return u.Scheme + "://" + config.Bucket + "." + u.Host + "/" + escapedKey
}
if endpoint == "" {
return escapedKey
}
return endpoint + "/" + escapedKey
}
func canUseOSS(config models.UploadConfig) bool {
return config.Provider == "oss" &&
strings.TrimSpace(config.AccessKeyID) != "" &&
strings.TrimSpace(config.AccessKeySecret) != "" &&
strings.TrimSpace(config.Bucket) != "" &&
(strings.TrimSpace(config.Endpoint) != "" || strings.TrimSpace(config.Region) != "")
}
func saveLocalUpload(c *gin.Context, file *multipart.FileHeader) {
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), utils.CleanObjectName(file.Filename))
savePath := filepath.Join("uploads", filename)
if err := c.SaveUploadedFile(file, savePath); err != nil {
c.JSON(500, gin.H{"error": "文件保存至服务器失败"})
return
}
fileURL := fmt.Sprintf("http://localhost:8080/uploads/%s", filename)
recordMediaAsset(fileURL, file.Filename, "local")
c.JSON(200, gin.H{"code": 200, "url": fileURL, "provider": "local"})
}
func saveOSSUpload(c *gin.Context, fileHeader *multipart.FileHeader, config models.UploadConfig) bool {
file, err := fileHeader.Open()
if err != nil {
c.JSON(500, gin.H{"error": "文件打开失败"})
return false
}
defer file.Close()
endpoint := utils.NormalizeEndpoint(config.Endpoint, config.Region)
provider := credentials.NewStaticCredentialsProvider(config.AccessKeyID, config.AccessKeySecret)
ossConfig := oss.LoadDefaultConfig().
WithCredentialsProvider(provider).
WithRegion(config.Region).
WithEndpoint(endpoint)
client := oss.NewClient(ossConfig)
objectKey := utils.BuildObjectKey(config.Folder, fileHeader.Filename)
_, err = client.PutObject(context.Background(), &oss.PutObjectRequest{
Bucket: oss.Ptr(config.Bucket),
Key: oss.Ptr(objectKey),
Acl: oss.ObjectACLPublicRead,
Body: file,
})
if err != nil {
c.JSON(500, gin.H{"error": "OSS上传失败: " + err.Error()})
return false
}
fileURL := objectURL(config, objectKey)
recordMediaAsset(fileURL, fileHeader.Filename, "oss")
c.JSON(200, gin.H{"code": 200, "url": fileURL, "provider": "oss", "key": objectKey})
return true
}
func collectAlbumImages(configData string) []string {
var raw map[string]interface{}
if err := json.Unmarshal([]byte(configData), &raw); err != nil {
return []string{}
}
seen := map[string]bool{}
images := []string{}
add := func(value interface{}) {
src, ok := value.(string)
if !ok {
return
}
src = strings.TrimSpace(src)
if src == "" || seen[src] {
return
}
seen[src] = true
images = append(images, src)
}
// 相册灯箱优先原图;无原图时再退缩放展示图
addPrefer := func(primary interface{}, fallback interface{}) {
if src, ok := primary.(string); ok && strings.TrimSpace(src) != "" {
add(primary)
return
}
add(fallback)
}
add(raw["heroImg"])
add(raw["endImg"])
if pages, ok := raw["photoPages"].([]interface{}); ok {
for _, item := range pages {
page, ok := item.(map[string]interface{})
if !ok {
continue
}
addPrefer(page["img1"], page["img1Resized"])
addPrefer(page["img2"], page["img2Resized"])
pageImages, _ := page["images"].([]interface{})
pageResized, _ := page["imagesResized"].([]interface{})
n := len(pageImages)
if len(pageResized) > n {
n = len(pageResized)
}
for i := 0; i < n; i++ {
var primary, fallback interface{}
if i < len(pageImages) {
primary = pageImages[i]
}
if i < len(pageResized) {
fallback = pageResized[i]
}
addPrefer(primary, fallback)
}
}
}
return images
}
func handleCreateCashGift(c *gin.Context) {
if !requireCashGiftEntryAccess(c) {
return
}
var body struct {
Name string `json:"name"`
Amount int `json:"amount"`
Note string `json:"note"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
name := strings.TrimSpace(body.Name)
note := strings.TrimSpace(body.Note)
if name == "" {
c.JSON(400, gin.H{"error": "请填写姓名"})
return
}
if len([]rune(name)) > 50 {
c.JSON(400, gin.H{"error": "姓名过长"})
return
}
if body.Amount <= 0 {
c.JSON(400, gin.H{"error": "请填写正确金额"})
return
}
if body.Amount > 10000000 {
c.JSON(400, gin.H{"error": "金额过大"})
return
}
if len([]rune(note)) > 200 {
c.JSON(400, gin.H{"error": "备注过长"})
return
}
item := models.CashGift{
Name: name,
Amount: body.Amount,
Note: note,
}
if err := db.Create(&item).Error; err != nil {
c.JSON(500, gin.H{"error": "保存失败"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "已登记", "data": item})
}
func handleCashGiftRecent(c *gin.Context) {
if !requireCashGiftEntryAccess(c) {
return
}
limit := 3
if v := strings.TrimSpace(c.Query("limit")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 && n <= 20 {
limit = n
}
}
var list []models.CashGift
db.Order("created_at desc, id desc").Limit(limit).Find(&list)
c.JSON(200, gin.H{"code": 200, "data": list})
}
func defaultCashGiftNotes() []string {
return []string{
"男方亲戚", "女方亲戚",
"男方朋友", "女方朋友",
"男方同学", "女方同学",
"男方同事", "女方同事",
"男方邻居", "女方邻居",
"其他",
}
}
func normalizeCashGiftNotes(raw []string) []string {
seen := map[string]bool{}
out := make([]string, 0, len(raw))
for _, s := range raw {
s = strings.TrimSpace(s)
if s == "" || seen[s] {
continue
}
if len([]rune(s)) > 30 {
s = string([]rune(s)[:30])
}
seen[s] = true
out = append(out, s)
if len(out) >= 40 {
break
}
}
return out
}
func loadCashGiftNotes() []string {
var cfg models.CashGiftConfig
if err := db.First(&cfg, 1).Error; err != nil || strings.TrimSpace(cfg.NotesData) == "" {
return defaultCashGiftNotes()
}
var notes []string
if err := json.Unmarshal([]byte(cfg.NotesData), &notes); err != nil {
return defaultCashGiftNotes()
}
notes = normalizeCashGiftNotes(notes)
if len(notes) == 0 {
return defaultCashGiftNotes()
}
return notes
}
func saveCashGiftNotes(notes []string) error {
notes = normalizeCashGiftNotes(notes)
if len(notes) == 0 {
notes = defaultCashGiftNotes()
}
raw, err := json.Marshal(notes)
if err != nil {
return err
}
var cfg models.CashGiftConfig
if err := db.First(&cfg, 1).Error; err != nil {
cfg = models.CashGiftConfig{ID: 1, NotesData: string(raw)}
return db.Create(&cfg).Error
}
cfg.NotesData = string(raw)
return db.Save(&cfg).Error
}
func loadCashGiftViewPass() string {
var cfg models.CashGiftConfig
if err := db.First(&cfg, 1).Error; err != nil {
return ""
}
return strings.TrimSpace(cfg.ViewPass)
}
func loadCashGiftEntryPass() string {
var cfg models.CashGiftConfig
if err := db.First(&cfg, 1).Error; err != nil {
return ""
}
return strings.TrimSpace(cfg.EntryPass)
}
func saveCashGiftViewPass(pass string) error {
pass = strings.TrimSpace(pass)
if len([]rune(pass)) > 64 {
pass = string([]rune(pass)[:64])
}
var cfg models.CashGiftConfig
if err := db.First(&cfg, 1).Error; err != nil {
notes, _ := json.Marshal(defaultCashGiftNotes())
cfg = models.CashGiftConfig{ID: 1, NotesData: string(notes), ViewPass: pass}
return db.Create(&cfg).Error
}
cfg.ViewPass = pass
return db.Save(&cfg).Error
}
func saveCashGiftEntryPass(pass string) error {
pass = strings.TrimSpace(pass)
if len([]rune(pass)) > 64 {
pass = string([]rune(pass)[:64])
}
var cfg models.CashGiftConfig
if err := db.First(&cfg, 1).Error; err != nil {
notes, _ := json.Marshal(defaultCashGiftNotes())
cfg = models.CashGiftConfig{ID: 1, NotesData: string(notes), EntryPass: pass}
return db.Create(&cfg).Error
}
cfg.EntryPass = pass
return db.Save(&cfg).Error
}
func cashGiftViewTokenFromRequest(c *gin.Context) string {
if t := strings.TrimSpace(c.GetHeader("X-Cash-Gift-Token")); t != "" {
return t
}
return utils.BearerToken(c)
}
func cashGiftEntryTokenFromRequest(c *gin.Context) string {
if t := strings.TrimSpace(c.GetHeader("X-Cash-Gift-Entry-Token")); t != "" {
return t
}
return utils.BearerToken(c)
}
func requireCashGiftViewAccess(c *gin.Context) bool {
token := cashGiftViewTokenFromRequest(c)
if utils.VerifyCashGiftViewToken(token) {
return true
}
if utils.VerifyToken(token) {
return true
}
c.JSON(403, gin.H{"error": "请先输入查看口令"})
return false
}
func requireCashGiftEntryAccess(c *gin.Context) bool {
token := cashGiftEntryTokenFromRequest(c)
if utils.VerifyCashGiftEntryToken(token) {
return true
}
if utils.VerifyToken(token) {
return true
}
c.JSON(403, gin.H{"error": "请先输入记账口令"})
return false
}
func handleGetCashGiftViewPass(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
pass := loadCashGiftViewPass()
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"has_pass": pass != "",
"password": pass,
},
})
}
func handleSaveCashGiftViewPass(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var body struct {
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
pass := strings.TrimSpace(body.Password)
if pass == "" {
c.JSON(400, gin.H{"error": "请设置查看口令"})
return
}
if err := saveCashGiftViewPass(pass); err != nil {
c.JSON(500, gin.H{"error": "保存失败"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "口令已保存", "data": gin.H{"has_pass": true}})
}
func handleCashGiftViewUnlock(c *gin.Context) {
var body struct {
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
pass := loadCashGiftViewPass()
if pass == "" {
c.JSON(403, gin.H{"error": "尚未配置查看口令,请先在后台设置"})
return
}
if strings.TrimSpace(body.Password) != pass {
c.JSON(403, gin.H{"error": "口令错误"})
return
}
c.JSON(200, gin.H{
"code": 200,
"msg": "已解锁",
"data": gin.H{"token": utils.SignCashGiftViewToken()},
})
}
func handleGetCashGiftEntryPass(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
pass := loadCashGiftEntryPass()
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"has_pass": pass != "",
"password": pass,
},
})
}
func handleSaveCashGiftEntryPass(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var body struct {
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
pass := strings.TrimSpace(body.Password)
if pass == "" {
c.JSON(400, gin.H{"error": "请设置记账口令"})
return
}
if err := saveCashGiftEntryPass(pass); err != nil {
c.JSON(500, gin.H{"error": "保存失败"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "口令已保存", "data": gin.H{"has_pass": true}})
}
func handleCashGiftEntryUnlock(c *gin.Context) {
var body struct {
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
pass := loadCashGiftEntryPass()
if pass == "" {
c.JSON(403, gin.H{"error": "尚未配置记账口令,请先在后台设置"})
return
}
if strings.TrimSpace(body.Password) != pass {
c.JSON(403, gin.H{"error": "口令错误"})
return
}
c.JSON(200, gin.H{
"code": 200,
"msg": "已解锁",
"data": gin.H{"token": utils.SignCashGiftEntryToken()},
})
}
func handleCashGiftViewList(c *gin.Context) {
if !requireCashGiftViewAccess(c) {
return
}
q := strings.TrimSpace(c.Query("q"))
note := strings.TrimSpace(c.Query("note"))
side := strings.TrimSpace(c.Query("side")) // male | female | other | ""
sortKey := strings.TrimSpace(c.Query("sort"))
minAmount := 0
maxAmount := 0
if v := strings.TrimSpace(c.Query("min")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
minAmount = n
}
}
if v := strings.TrimSpace(c.Query("max")); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
maxAmount = n
}
}
tx := db.Model(&models.CashGift{})
if q != "" {
like := "%" + q + "%"
tx = tx.Where("name LIKE ? OR note LIKE ?", like, like)
}
if note != "" {
tx = tx.Where("note = ?", note)
}
switch side {
case "male":
tx = tx.Where("note LIKE ?", "男方%")
case "female":
tx = tx.Where("note LIKE ?", "女方%")
case "other":
tx = tx.Where("note NOT LIKE ? AND note NOT LIKE ? AND note <> ''", "男方%", "女方%")
}
if minAmount > 0 {
tx = tx.Where("amount >= ?", minAmount)
}
if maxAmount > 0 {
tx = tx.Where("amount <= ?", maxAmount)
}
order := "created_at desc, id desc"
switch sortKey {
case "time_asc":
order = "created_at asc, id asc"
case "amount_desc":
order = "amount desc, created_at desc"
case "amount_asc":
order = "amount asc, created_at desc"
case "name_asc":
order = "name asc, created_at desc"
}
var list []models.CashGift
tx.Order(order).Find(&list)
total := 0
for _, it := range list {
total += it.Amount
}
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"list": list,
"total": total,
"count": len(list),
"notes": loadCashGiftNotes(),
},
})
}
func handleGetCashGiftNotes(c *gin.Context) {
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"notes": loadCashGiftNotes(),
"defaults": defaultCashGiftNotes(),
},
})
}
func handleSaveCashGiftNotes(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var body struct {
Notes []string `json:"notes"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
notes := normalizeCashGiftNotes(body.Notes)
if len(notes) == 0 {
c.JSON(400, gin.H{"error": "至少保留一个快捷备注"})
return
}
if err := saveCashGiftNotes(notes); err != nil {
c.JSON(500, gin.H{"error": "保存失败"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "已保存", "data": gin.H{"notes": notes}})
}
func handleCashGiftList(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
var list []models.CashGift
db.Order("created_at desc, id desc").Find(&list)
total := 0
for _, it := range list {
total += it.Amount
}
c.JSON(200, gin.H{
"code": 200,
"data": gin.H{
"list": list,
"total": total,
"count": len(list),
},
})
}
func handleUpdateCashGift(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
id, err := strconv.ParseUint(strings.TrimSpace(c.Param("id")), 10, 64)
if err != nil || id == 0 {
c.JSON(400, gin.H{"error": "无效 ID"})
return
}
var body struct {
Name string `json:"name"`
Amount int `json:"amount"`
Note string `json:"note"`
}
if err := c.ShouldBindJSON(&body); err != nil {
c.JSON(400, gin.H{"error": "参数错误"})
return
}
name := strings.TrimSpace(body.Name)
note := strings.TrimSpace(body.Note)
if name == "" {
c.JSON(400, gin.H{"error": "请填写姓名"})
return
}
if body.Amount <= 0 {
c.JSON(400, gin.H{"error": "请填写正确金额"})
return
}
var item models.CashGift
if err := db.First(&item, id).Error; err != nil {
c.JSON(404, gin.H{"error": "记录不存在"})
return
}
item.Name = name
item.Amount = body.Amount
item.Note = note
if err := db.Save(&item).Error; err != nil {
c.JSON(500, gin.H{"error": "保存失败"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "已更新", "data": item})
}
func handleDeleteCashGift(c *gin.Context) {
if !utils.RequireAdmin(c) {
return
}
id := strings.TrimSpace(c.Param("id"))
if id == "" {
c.JSON(400, gin.H{"error": "缺少 ID"})
return
}
res := db.Delete(&models.CashGift{}, id)
if res.Error != nil {
c.JSON(500, gin.H{"error": "删除失败"})
return
}
if res.RowsAffected == 0 {
c.JSON(404, gin.H{"error": "记录不存在"})
return
}
c.JSON(200, gin.H{"code": 200, "msg": "已删除"})
}