package handler import ( "encoding/json" "errors" "net/http" "strings" "soul-api/internal/database" "soul-api/internal/model" "github.com/gin-gonic/gin" "gorm.io/gorm" ) const mbtiAvatarsConfigKey = "mbti_avatars" const mbtiAvatarsDescription = "MBTI 16型人格头像映射" var allMbtiTypes = []string{ "INTJ", "INTP", "ENTJ", "ENTP", "INFJ", "INFP", "ENFJ", "ENFP", "ISTJ", "ISFJ", "ESTJ", "ESFJ", "ISTP", "ISFP", "ESTP", "ESFP", } // buildDefaultMbtiAvatarMap returns the default PNG avatar mapping for all 16 types. func buildDefaultMbtiAvatarMap() map[string]string { m := make(map[string]string, 16) for _, t := range allMbtiTypes { m[t] = "/static/mbti-avatars-png-male/" + t + ".png" } return m } // absolutizeMbtiAvatarMap 将 /static、/uploads 等相对路径转为当前 API 公网可访问的绝对 URL, // 避免管理端页面 请求到前端 dev 端口、以及小程序端拼接 baseUrl 异常时不显示。 func absolutizeMbtiAvatarMap(c *gin.Context, m map[string]string) map[string]string { if m == nil { return nil } out := make(map[string]string, len(m)) for k, v := range m { out[k] = resolveAvatarURLWithRequest(c, strings.TrimSpace(v)) } return out } // AdminMbtiAvatarsGet GET /api/admin/mbti-avatars 读取 MBTI 头像映射(system_config.mbti_avatars) // 无配置时自动填充 PNG 默认路径并持久化。 func AdminMbtiAvatarsGet(c *gin.Context) { db := database.DB() var row model.SystemConfig err := db.Where("config_key = ?", mbtiAvatarsConfigKey).First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { defaults := buildDefaultMbtiAvatarMap() valBytes, _ := json.Marshal(defaults) desc := mbtiAvatarsDescription newRow := model.SystemConfig{ ConfigKey: mbtiAvatarsConfigKey, ConfigValue: valBytes, Description: &desc, } db.Create(&newRow) _mbtiAvatarCacheTs = 0 c.JSON(http.StatusOK, gin.H{"success": true, "avatars": absolutizeMbtiAvatarMap(c, defaults)}) return } c.JSON(http.StatusOK, gin.H{"success": false, "error": "读取配置失败: " + err.Error()}) return } out := make(map[string]string) if len(row.ConfigValue) > 0 { if uerr := json.Unmarshal(row.ConfigValue, &out); uerr != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "配置 JSON 无效: " + uerr.Error()}) return } } if len(out) == 0 { out = buildDefaultMbtiAvatarMap() } c.JSON(http.StatusOK, gin.H{"success": true, "avatars": absolutizeMbtiAvatarMap(c, out)}) } // AdminMbtiAvatarsPost POST /api/admin/mbti-avatars 保存 MBTI 头像映射(upsert) func AdminMbtiAvatarsPost(c *gin.Context) { var body struct { Avatars map[string]string `json:"avatars"` } if err := c.ShouldBindJSON(&body); err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"}) return } avatars := body.Avatars if avatars == nil { avatars = map[string]string{} } valBytes, err := json.Marshal(avatars) if err != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "序列化失败: " + err.Error()}) return } db := database.DB() desc := mbtiAvatarsDescription var row model.SystemConfig err = db.Where("config_key = ?", mbtiAvatarsConfigKey).First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { row = model.SystemConfig{ ConfigKey: mbtiAvatarsConfigKey, ConfigValue: valBytes, Description: &desc, } if cerr := db.Create(&row).Error; cerr != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存失败: " + cerr.Error()}) return } } else { c.JSON(http.StatusOK, gin.H{"success": false, "error": "查询配置失败: " + err.Error()}) return } } else { row.ConfigValue = valBytes row.Description = &desc if serr := db.Save(&row).Error; serr != nil { c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存失败: " + serr.Error()}) return } } _mbtiAvatarCacheTs = 0 c.JSON(http.StatusOK, gin.H{"success": true, "message": "MBTI 头像映射已保存"}) } // AdminMbtiAvatarsBatchAssign POST /api/admin/mbti-avatars/batch-assign // 为无头像(avatar IS NULL 或空)的用户批量分配 MBTI 默认头像。 // 有 MBTI 的按 MBTI 匹配;无 MBTI 的随机分配。 // 无配置时自动使用 PNG 默认路径。 func AdminMbtiAvatarsBatchAssign(c *gin.Context) { db := database.DB() avatarMap := make(map[string]string) var row model.SystemConfig if err := db.Where("config_key = ?", mbtiAvatarsConfigKey).First(&row).Error; err == nil { _ = json.Unmarshal(row.ConfigValue, &avatarMap) } if len(avatarMap) == 0 { avatarMap = buildDefaultMbtiAvatarMap() } availableTypes := make([]string, 0, len(avatarMap)) for k, v := range avatarMap { if v != "" { availableTypes = append(availableTypes, k) } } if len(availableTypes) == 0 { c.JSON(http.StatusOK, gin.H{"success": false, "error": "无可用头像"}) return } var users []model.User db.Where("(avatar IS NULL OR avatar = '')").Limit(500).Find(&users) if len(users) == 0 { c.JSON(http.StatusOK, gin.H{"success": true, "assigned": 0, "message": "无需分配"}) return } assigned := 0 for i := range users { mbti := "" if users[i].Mbti != nil { mbti = *users[i].Mbti } url := "" if mbti != "" { url = avatarMap[mbti] } if url == "" { idx := (int(users[i].CreatedAt.UnixNano()) + i) % len(availableTypes) url = avatarMap[availableTypes[idx]] } if url == "" { continue } if err := db.Model(&model.User{}).Where("id = ?", users[i].ID).Update("avatar", url).Error; err == nil { assigned++ } } c.JSON(http.StatusOK, gin.H{"success": true, "assigned": assigned, "total": len(users), "message": "批量分配完成"}) }