增强用户隐私保护,新增昵称授权功能。更新头像选择逻辑,用户可直接通过按钮选择微信头像或相册图片。优化个人资料页面,强化手机号必填提示,提升用户体验。调整多个页面以支持新隐私授权机制,确保符合最新隐私规范。
This commit is contained in:
@@ -178,7 +178,7 @@ func buildRecentOrdersOut(db *gorm.DB, recentOrders []model.Order) []gin.H {
|
||||
_ = json.Unmarshal(b, &m)
|
||||
if u := userMap[o.UserID]; u != nil {
|
||||
m["userNickname"] = dashStr(u.Nickname)
|
||||
m["userAvatar"] = dashStr(u.Avatar)
|
||||
m["userAvatar"] = resolveAvatarURL(dashStr(u.Avatar))
|
||||
} else {
|
||||
m["userNickname"] = ""
|
||||
m["userAvatar"] = ""
|
||||
|
||||
@@ -168,7 +168,7 @@ func DBUsersRFM(c *gin.Context) {
|
||||
ru.Phone = *u.Phone
|
||||
}
|
||||
if u.Avatar != nil {
|
||||
ru.Avatar = *u.Avatar
|
||||
ru.Avatar = resolveAvatarURL(*u.Avatar)
|
||||
}
|
||||
result = append(result, ru)
|
||||
}
|
||||
|
||||
@@ -93,8 +93,12 @@ func AdminWithdrawalsList(c *gin.Context) {
|
||||
if w.UserConfirmedAt != nil && !w.UserConfirmedAt.IsZero() {
|
||||
userConfirmedAt = w.UserConfirmedAt.Format("2006-01-02 15:04:05")
|
||||
}
|
||||
avStr := ""
|
||||
if userAvatar != nil {
|
||||
avStr = resolveAvatarURL(*userAvatar)
|
||||
}
|
||||
withdrawals = append(withdrawals, gin.H{
|
||||
"id": w.ID, "userId": w.UserID, "userName": userName, "userAvatar": userAvatar,
|
||||
"id": w.ID, "userId": w.UserID, "userName": userName, "userAvatar": avStr,
|
||||
"amount": w.Amount, "status": st, "createdAt": w.CreatedAt,
|
||||
"method": "wechat", "account": account,
|
||||
"userConfirmedAt": userConfirmedAt,
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"soul-api/internal/config"
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
"soul-api/internal/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -157,24 +158,42 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
// GetAuditMode GET /api/miniprogram/config/audit-mode 审核模式独立接口,管理端开关后快速生效
|
||||
// 缓存未命中时仅查 mp_config 一条记录,避免 buildMiniprogramConfig 全量查询导致超时
|
||||
// 无 Redis 时直接返回数据库中的 auditMode 值,保证可用
|
||||
func GetAuditMode(c *gin.Context) {
|
||||
// 无 Redis 时跳过缓存,直接返回数据库值
|
||||
if redis.Client() == nil {
|
||||
auditMode := getAuditModeFromDB()
|
||||
c.JSON(http.StatusOK, gin.H{"auditMode": auditMode})
|
||||
return
|
||||
}
|
||||
var cached gin.H
|
||||
if cache.Get(context.Background(), cache.KeyConfigAuditMode, &cached) && len(cached) > 0 {
|
||||
c.JSON(http.StatusOK, cached)
|
||||
return
|
||||
}
|
||||
full := buildMiniprogramConfig()
|
||||
auditMode := false
|
||||
if mp, ok := full["mpConfig"].(gin.H); ok {
|
||||
if v, ok := mp["auditMode"].(bool); ok && v {
|
||||
auditMode = true
|
||||
}
|
||||
}
|
||||
auditMode := getAuditModeFromDB()
|
||||
out := gin.H{"auditMode": auditMode}
|
||||
cache.Set(context.Background(), cache.KeyConfigAuditMode, out, cache.AuditModeTTL)
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// getAuditModeFromDB 仅查询 mp_config 的 auditMode,轻量级避免超时
|
||||
func getAuditModeFromDB() bool {
|
||||
var row model.SystemConfig
|
||||
if err := database.DB().Where("config_key = ?", "mp_config").First(&row).Error; err != nil {
|
||||
return false
|
||||
}
|
||||
var mp map[string]interface{}
|
||||
if err := json.Unmarshal(row.ConfigValue, &mp); err != nil {
|
||||
return false
|
||||
}
|
||||
if v, ok := mp["auditMode"].(bool); ok && v {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetCoreConfig GET /api/miniprogram/config/core 核心配置(prices、features、userDiscount、mpConfig),首屏/Tab 用
|
||||
func GetCoreConfig(c *gin.Context) {
|
||||
var cached gin.H
|
||||
@@ -949,7 +968,7 @@ func DBUsersAction(c *gin.Context) {
|
||||
updates["wechat_id"] = *body.WechatID
|
||||
}
|
||||
if body.Avatar != nil {
|
||||
updates["avatar"] = *body.Avatar
|
||||
updates["avatar"] = avatarToPath(*body.Avatar)
|
||||
}
|
||||
if body.Tags != nil {
|
||||
updates["ckb_tags"] = *body.Tags
|
||||
@@ -1001,7 +1020,7 @@ func DBUsersAction(c *gin.Context) {
|
||||
updates["vip_name"] = *body.VipName
|
||||
}
|
||||
if body.VipAvatar != nil {
|
||||
updates["vip_avatar"] = *body.VipAvatar
|
||||
updates["vip_avatar"] = avatarToPath(*body.VipAvatar)
|
||||
}
|
||||
if body.VipProject != nil {
|
||||
updates["vip_project"] = *body.VipProject
|
||||
@@ -1112,8 +1131,12 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
// 已付费:与小程序一致,以绑定记录的 purchase_count > 0 为准(支付回调会更新该字段)
|
||||
hasPaid := b.PurchaseCount != nil && *b.PurchaseCount > 0
|
||||
displayStatus := bindingStatusDisplay(hasPaid, hasFullBook) // vip | paid | free,供前端徽章展示
|
||||
avStr := ""
|
||||
if avatar != nil {
|
||||
avStr = resolveAvatarURL(*avatar)
|
||||
}
|
||||
referrals = append(referrals, gin.H{
|
||||
"id": b.RefereeID, "nickname": nick, "avatar": avatar, "phone": phone,
|
||||
"id": b.RefereeID, "nickname": nick, "avatar": avStr, "phone": phone,
|
||||
"hasFullBook": hasFullBook || status == "converted",
|
||||
"purchasedSections": getBindingPurchaseCount(b),
|
||||
"createdAt": b.BindingDate, "bindingStatus": status, "daysRemaining": daysRemaining, "commission": b.TotalCommission,
|
||||
@@ -1288,8 +1311,8 @@ func DBDistribution(c *gin.Context) {
|
||||
statusVal = *b.Status
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": b.ID, "referrerId": b.ReferrerID, "referrerName": getStr(referrerName), "referrerCode": b.ReferralCode, "referrerAvatar": getStr(referrerAvatar),
|
||||
"refereeId": b.RefereeID, "refereeNickname": refNick, "refereePhone": getStr(refereePhone), "refereeAvatar": getStr(refereeAvatar),
|
||||
"id": b.ID, "referrerId": b.ReferrerID, "referrerName": getStr(referrerName), "referrerCode": b.ReferralCode, "referrerAvatar": resolveAvatarURL(getStr(referrerAvatar)),
|
||||
"refereeId": b.RefereeID, "refereeNickname": refNick, "refereePhone": getStr(refereePhone), "refereeAvatar": resolveAvatarURL(getStr(refereeAvatar)),
|
||||
"boundAt": b.BindingDate, "expiresAt": b.ExpiryDate, "status": statusVal,
|
||||
"daysRemaining": days, "commission": commissionVal, "totalCommission": commissionVal, "source": "miniprogram",
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ func DBCKBLeadList(c *gin.Context) {
|
||||
}
|
||||
|
||||
if mode == "contact" {
|
||||
// ckb_lead_records:链接卡若留资
|
||||
// ckb_lead_records:链接卡若留资,关联 persons 获取 @人 与获客计划
|
||||
q := db.Model(&model.CkbLeadRecord{})
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
@@ -36,17 +36,42 @@ func DBCKBLeadList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
// 批量查 persons 获取 personName、ckbPlanId
|
||||
personIDs := make([]string, 0)
|
||||
for _, r := range records {
|
||||
if r.TargetPersonID != "" {
|
||||
personIDs = append(personIDs, r.TargetPersonID)
|
||||
}
|
||||
}
|
||||
personMap := make(map[string]*model.Person)
|
||||
if len(personIDs) > 0 {
|
||||
var persons []model.Person
|
||||
db.Where("person_id IN ?", personIDs).Find(&persons)
|
||||
for i := range persons {
|
||||
personMap[persons[i].PersonID] = &persons[i]
|
||||
}
|
||||
}
|
||||
out := make([]gin.H, 0, len(records))
|
||||
for _, r := range records {
|
||||
personName := ""
|
||||
ckbPlanId := int64(0)
|
||||
if p := personMap[r.TargetPersonID]; p != nil {
|
||||
personName = p.Name
|
||||
ckbPlanId = p.CkbPlanID
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": r.ID,
|
||||
"userId": r.UserID,
|
||||
"userNickname": r.Nickname,
|
||||
"matchType": "lead",
|
||||
"phone": r.Phone,
|
||||
"wechatId": r.WechatID,
|
||||
"name": r.Name,
|
||||
"createdAt": r.CreatedAt,
|
||||
"id": r.ID,
|
||||
"userId": r.UserID,
|
||||
"userNickname": r.Nickname,
|
||||
"matchType": "lead",
|
||||
"phone": r.Phone,
|
||||
"wechatId": r.WechatID,
|
||||
"name": r.Name,
|
||||
"source": r.Source,
|
||||
"targetPersonId": r.TargetPersonID,
|
||||
"personName": personName,
|
||||
"ckbPlanId": ckbPlanId,
|
||||
"createdAt": r.CreatedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize})
|
||||
|
||||
@@ -380,7 +380,7 @@ func GiftPayDetail(c *gin.Context) {
|
||||
nickname = *initiator.Nickname
|
||||
}
|
||||
if initiator.Avatar != nil && *initiator.Avatar != "" {
|
||||
initiatorAvatar = *initiator.Avatar
|
||||
initiatorAvatar = resolveAvatarURL(*initiator.Avatar)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -417,7 +417,7 @@ func GiftPayDetail(c *gin.Context) {
|
||||
nickname = *u.Nickname
|
||||
}
|
||||
if u.Avatar != nil && *u.Avatar != "" {
|
||||
avatar = *u.Avatar
|
||||
avatar = resolveAvatarURL(*u.Avatar)
|
||||
}
|
||||
}
|
||||
redeemList = append(redeemList, gin.H{"userId": o.UserID, "nickname": nickname, "avatar": avatar, "redeemAt": o.CreatedAt.Format("2006-01-02 15:04")})
|
||||
@@ -625,7 +625,7 @@ func GiftPayMyRequests(c *gin.Context) {
|
||||
nickname = *u.Nickname
|
||||
}
|
||||
if u.Avatar != nil && *u.Avatar != "" {
|
||||
avatar = *u.Avatar
|
||||
avatar = resolveAvatarURL(*u.Avatar)
|
||||
}
|
||||
}
|
||||
redeemAt := o.CreatedAt.Format("2006-01-02 15:04")
|
||||
|
||||
@@ -293,7 +293,7 @@ func MatchUsers(c *gin.Context) {
|
||||
}
|
||||
avatar := ""
|
||||
if r.Avatar != nil {
|
||||
avatar = *r.Avatar
|
||||
avatar = resolveAvatarURL(*r.Avatar)
|
||||
}
|
||||
wechat := ""
|
||||
if r.WechatID != nil {
|
||||
|
||||
@@ -104,10 +104,10 @@ func DBMatchRecordsList(c *gin.Context) {
|
||||
userAvatar := ""
|
||||
matchedUserAvatar := ""
|
||||
if u != nil && u.Avatar != nil {
|
||||
userAvatar = *u.Avatar
|
||||
userAvatar = resolveAvatarURL(*u.Avatar)
|
||||
}
|
||||
if mu != nil && mu.Avatar != nil {
|
||||
matchedUserAvatar = *mu.Avatar
|
||||
matchedUserAvatar = resolveAvatarURL(*mu.Avatar)
|
||||
}
|
||||
userNickname := ""
|
||||
if u != nil {
|
||||
|
||||
@@ -43,6 +43,7 @@ func MiniprogramMentorsList(c *gin.Context) {
|
||||
result := make([]mentorItem, len(list))
|
||||
for i, m := range list {
|
||||
result[i] = mentorItem{Mentor: m}
|
||||
result[i].Avatar = resolveAvatarURL(m.Avatar) // OSS 时 DB 存路径,返回需解析为完整 URL
|
||||
if m.Tags != "" {
|
||||
result[i].TagsArr = strings.Split(m.Tags, ",")
|
||||
for j := range result[i].TagsArr {
|
||||
@@ -79,7 +80,7 @@ func MiniprogramMentorsDetail(c *gin.Context) {
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"id": m.ID,
|
||||
"avatar": m.Avatar,
|
||||
"avatar": resolveAvatarURL(m.Avatar),
|
||||
"name": m.Name,
|
||||
"intro": m.Intro,
|
||||
"tags": m.Tags,
|
||||
@@ -257,7 +258,7 @@ func DBMentorsAction(c *gin.Context) {
|
||||
updates["name"] = *body.Name
|
||||
}
|
||||
if body.Avatar != nil {
|
||||
updates["avatar"] = *body.Avatar
|
||||
updates["avatar"] = avatarToPath(*body.Avatar)
|
||||
}
|
||||
if body.Intro != nil {
|
||||
updates["intro"] = *body.Intro
|
||||
|
||||
@@ -134,7 +134,7 @@ func MiniprogramLogin(c *gin.Context) {
|
||||
"id": user.ID,
|
||||
"openId": getStringValue(user.OpenID),
|
||||
"nickname": getStringValue(user.Nickname),
|
||||
"avatar": getStringValue(user.Avatar),
|
||||
"avatar": resolveAvatarURL(getStringValue(user.Avatar)),
|
||||
"phone": getStringValue(user.Phone),
|
||||
"wechatId": getStringValue(user.WechatID),
|
||||
"referralCode": getStringValue(user.ReferralCode),
|
||||
@@ -212,7 +212,7 @@ func MiniprogramDevLoginAs(c *gin.Context) {
|
||||
"id": user.ID,
|
||||
"openId": openID,
|
||||
"nickname": getStringValue(user.Nickname),
|
||||
"avatar": getStringValue(user.Avatar),
|
||||
"avatar": resolveAvatarURL(getStringValue(user.Avatar)),
|
||||
"phone": getStringValue(user.Phone),
|
||||
"wechatId": getStringValue(user.WechatID),
|
||||
"referralCode": getStringValue(user.ReferralCode),
|
||||
@@ -1021,11 +1021,11 @@ func MiniprogramUsers(c *gin.Context) {
|
||||
item := gin.H{
|
||||
"id": user.ID,
|
||||
"nickname": getStringValue(user.Nickname),
|
||||
"avatar": getStringValue(user.Avatar),
|
||||
"avatar": resolveAvatarURL(getStringValue(user.Avatar)),
|
||||
"phone": getStringValue(user.Phone),
|
||||
"wechatId": getStringValue(user.WechatID),
|
||||
"vipName": getStringValue(user.VipName),
|
||||
"vipAvatar": getStringValue(user.VipAvatar),
|
||||
"vipAvatar": resolveAvatarURL(getStringValue(user.VipAvatar)),
|
||||
"vipContact": getStringValue(user.VipContact),
|
||||
"vipProject": getStringValue(user.VipProject),
|
||||
"vipBio": getStringValue(user.VipBio),
|
||||
@@ -1060,7 +1060,7 @@ func MiniprogramUsers(c *gin.Context) {
|
||||
list = append(list, gin.H{
|
||||
"id": u.ID,
|
||||
"nickname": getStringValue(u.Nickname),
|
||||
"avatar": getStringValue(u.Avatar),
|
||||
"avatar": resolveAvatarURL(getStringValue(u.Avatar)),
|
||||
"is_vip": uvip,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -146,7 +146,7 @@ func OrdersList(c *gin.Context) {
|
||||
if u := userMap[o.UserID]; u != nil {
|
||||
m["userNickname"] = getStr(u.Nickname)
|
||||
m["userPhone"] = getStr(u.Phone)
|
||||
m["userAvatar"] = getStr(u.Avatar)
|
||||
m["userAvatar"] = resolveAvatarURL(getStr(u.Avatar))
|
||||
} else {
|
||||
m["userNickname"] = ""
|
||||
m["userPhone"] = ""
|
||||
|
||||
@@ -283,7 +283,7 @@ func ReferralData(c *gin.Context) {
|
||||
activeUsers = append(activeUsers, gin.H{
|
||||
"id": b.RefereeID,
|
||||
"nickname": getStringValue(referee.Nickname),
|
||||
"avatar": getStringValue(referee.Avatar),
|
||||
"avatar": resolveAvatarURL(getStringValue(referee.Avatar)),
|
||||
"daysRemaining": daysRemaining,
|
||||
"hasFullBook": getBoolValue(referee.HasFullBook),
|
||||
"bindingDate": b.BindingDate,
|
||||
@@ -312,7 +312,7 @@ func ReferralData(c *gin.Context) {
|
||||
convertedUsers = append(convertedUsers, gin.H{
|
||||
"id": b.RefereeID,
|
||||
"nickname": getStringValue(referee.Nickname),
|
||||
"avatar": getStringValue(referee.Avatar),
|
||||
"avatar": resolveAvatarURL(getStringValue(referee.Avatar)),
|
||||
"commission": commission,
|
||||
"orderAmount": orderAmount,
|
||||
"purchaseCount": getIntValue(b.PurchaseCount),
|
||||
@@ -336,7 +336,7 @@ func ReferralData(c *gin.Context) {
|
||||
expiredUsers = append(expiredUsers, gin.H{
|
||||
"id": b.RefereeID,
|
||||
"nickname": getStringValue(referee.Nickname),
|
||||
"avatar": getStringValue(referee.Avatar),
|
||||
"avatar": resolveAvatarURL(getStringValue(referee.Avatar)),
|
||||
"bindingDate": b.BindingDate,
|
||||
"expiryDate": b.ExpiryDate,
|
||||
"status": "expired",
|
||||
@@ -366,7 +366,7 @@ func ReferralData(c *gin.Context) {
|
||||
"productId": getStringValue(e.ProductID),
|
||||
"description": getStringValue(e.Description),
|
||||
"buyerNickname": getStringValue(buyer.Nickname),
|
||||
"buyerAvatar": getStringValue(buyer.Avatar),
|
||||
"buyerAvatar": resolveAvatarURL(getStringValue(buyer.Avatar)),
|
||||
"payTime": e.PayTime,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -5,14 +5,59 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/config"
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
"soul-api/internal/oss"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// avatarToPath 从头像 URL 提取路径(不含域名),用于保存到 DB
|
||||
func avatarToPath(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
if idx := strings.Index(s, "/uploads/"); idx >= 0 {
|
||||
return s[idx:]
|
||||
}
|
||||
if strings.HasPrefix(s, "/") {
|
||||
return s
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// resolveAvatarURL 将路径解析为完整可访问 URL(返回时使用)
|
||||
func resolveAvatarURL(s string) string {
|
||||
s = strings.TrimSpace(s)
|
||||
if s == "" {
|
||||
return s
|
||||
}
|
||||
// 已是完整 URL,直接返回
|
||||
if strings.HasPrefix(s, "http://") || strings.HasPrefix(s, "https://") {
|
||||
return s
|
||||
}
|
||||
path := s
|
||||
if !strings.HasPrefix(path, "/") {
|
||||
path = "/" + path
|
||||
}
|
||||
// OSS 存储:用 OSS 公网 URL
|
||||
if oss.IsEnabled() {
|
||||
if u := oss.PublicURL(path); u != "" {
|
||||
return u
|
||||
}
|
||||
}
|
||||
// 本地存储:用 BaseURL 拼接
|
||||
if cfg := config.Get(); cfg != nil && cfg.BaseURL != "" {
|
||||
return cfg.BaseURLJoin(path)
|
||||
}
|
||||
return path
|
||||
}
|
||||
|
||||
// UserAddressesGet GET /api/user/addresses?userId=
|
||||
func UserAddressesGet(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
@@ -243,8 +288,9 @@ func UserProfileGet(c *gin.Context) {
|
||||
profileComplete := (user.Phone != nil && *user.Phone != "") || (user.WechatID != nil && *user.WechatID != "")
|
||||
hasAvatar := user.Avatar != nil && *user.Avatar != "" && len(*user.Avatar) > 0
|
||||
str := func(p *string) interface{} { if p != nil { return *p }; return "" }
|
||||
avatarVal := resolveAvatarURL(str(user.Avatar).(string))
|
||||
resp := gin.H{
|
||||
"id": user.ID, "openId": user.OpenID, "nickname": str(user.Nickname), "avatar": str(user.Avatar),
|
||||
"id": user.ID, "openId": user.OpenID, "nickname": str(user.Nickname), "avatar": avatarVal,
|
||||
"phone": str(user.Phone), "wechatId": str(user.WechatID), "referralCode": user.ReferralCode,
|
||||
"hasFullBook": user.HasFullBook, "earnings": user.Earnings, "pendingEarnings": user.PendingEarnings,
|
||||
"referralCount": user.ReferralCount, "profileComplete": profileComplete, "hasAvatar": hasAvatar,
|
||||
@@ -311,7 +357,7 @@ func UserProfilePost(c *gin.Context) {
|
||||
updates["nickname"] = *body.Nickname
|
||||
}
|
||||
if body.Avatar != nil {
|
||||
updates["avatar"] = *body.Avatar
|
||||
updates["avatar"] = avatarToPath(*body.Avatar)
|
||||
}
|
||||
if body.Phone != nil {
|
||||
updates["phone"] = *body.Phone
|
||||
@@ -343,8 +389,9 @@ func UserProfilePost(c *gin.Context) {
|
||||
"story_best_month", "story_achievement", "story_turning", "help_offer", "help_need", "project_intro"}
|
||||
if err := database.DB().Select(profileCols).Where("id = ?", user.ID).First(&user).Error; err == nil {
|
||||
str := func(p *string) interface{} { if p != nil { return *p }; return "" }
|
||||
avatarVal := resolveAvatarURL(str(user.Avatar).(string))
|
||||
resp := gin.H{
|
||||
"id": user.ID, "openId": user.OpenID, "nickname": str(user.Nickname), "avatar": str(user.Avatar),
|
||||
"id": user.ID, "openId": user.OpenID, "nickname": str(user.Nickname), "avatar": avatarVal,
|
||||
"phone": str(user.Phone), "wechatId": str(user.WechatID), "referralCode": user.ReferralCode,
|
||||
"createdAt": user.CreatedAt,
|
||||
"mbti": str(user.Mbti), "region": str(user.Region), "industry": str(user.Industry),
|
||||
@@ -355,8 +402,12 @@ func UserProfilePost(c *gin.Context) {
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "资料更新成功", "data": resp})
|
||||
} else {
|
||||
avatarVal := ""
|
||||
if body.Avatar != nil {
|
||||
avatarVal = resolveAvatarURL(avatarToPath(*body.Avatar))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "资料更新成功", "data": gin.H{
|
||||
"id": user.ID, "nickname": body.Nickname, "avatar": body.Avatar, "phone": body.Phone, "wechatId": body.WechatID, "referralCode": user.ReferralCode,
|
||||
"id": user.ID, "nickname": body.Nickname, "avatar": avatarVal, "phone": body.Phone, "wechatId": body.WechatID, "referralCode": user.ReferralCode,
|
||||
}})
|
||||
}
|
||||
}
|
||||
@@ -689,7 +740,7 @@ func UserUpdate(c *gin.Context) {
|
||||
updates["nickname"] = *body.Nickname
|
||||
}
|
||||
if body.Avatar != nil {
|
||||
updates["avatar"] = *body.Avatar
|
||||
updates["avatar"] = avatarToPath(*body.Avatar)
|
||||
}
|
||||
if body.Phone != nil {
|
||||
updates["phone"] = *body.Phone
|
||||
|
||||
@@ -126,7 +126,7 @@ func buildVipProfile(u *model.User) gin.H {
|
||||
"vipName": getStr(u.VipName),
|
||||
"vipProject": getStr(u.VipProject),
|
||||
"vipContact": getStr(u.VipContact),
|
||||
"vipAvatar": getStr(u.VipAvatar),
|
||||
"vipAvatar": resolveAvatarURL(getStr(u.VipAvatar)),
|
||||
"vipBio": getStr(u.VipBio),
|
||||
}
|
||||
}
|
||||
@@ -195,7 +195,7 @@ func VipProfilePost(c *gin.Context) {
|
||||
updates["vip_contact"] = req.VipContact
|
||||
}
|
||||
if req.VipAvatar != "" {
|
||||
updates["vip_avatar"] = req.VipAvatar
|
||||
updates["vip_avatar"] = avatarToPath(req.VipAvatar)
|
||||
}
|
||||
if req.VipBio != "" {
|
||||
updates["vip_bio"] = req.VipBio
|
||||
@@ -293,6 +293,7 @@ func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
if avatar == "" && u.VipAvatar != nil && *u.VipAvatar != "" {
|
||||
avatar = *u.VipAvatar
|
||||
}
|
||||
avatar = resolveAvatarURL(avatar)
|
||||
project := getStringValue(u.VipProject)
|
||||
if project == "" {
|
||||
project = getStringValue(u.ProjectIntro)
|
||||
|
||||
@@ -112,7 +112,7 @@ func WechatPhoneLogin(c *gin.Context) {
|
||||
"id": user.ID,
|
||||
"openId": strVal(user.OpenID),
|
||||
"nickname": strVal(user.Nickname),
|
||||
"avatar": strVal(user.Avatar),
|
||||
"avatar": resolveAvatarURL(strVal(user.Avatar)),
|
||||
"phone": strVal(user.Phone),
|
||||
"wechatId": strVal(user.WechatID),
|
||||
"referralCode": strVal(user.ReferralCode),
|
||||
|
||||
149
soul-api/internal/handler/ws.go
Normal file
149
soul-api/internal/handler/ws.go
Normal file
@@ -0,0 +1,149 @@
|
||||
// Package handler - WebSocket 占位:用户在线检测
|
||||
// 小程序连接 WSS 发心跳,Redis 记录在线;管理端通过 HTTP 获取在线人数
|
||||
// 后续可扩展:管理端 WSS 订阅、消息推送等
|
||||
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/config"
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
"soul-api/internal/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
const (
|
||||
wsOnlinePrefix = "user:online:"
|
||||
wsOfflineTimeout = 300 // 5 分钟无心跳视为离线(秒)
|
||||
)
|
||||
|
||||
var wsUpgrader = websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
// WsMiniprogram 处理小程序 WSS 连接:鉴权后记录心跳,维持在线状态
|
||||
// 路径:GET /ws/miniprogram?token=xxx
|
||||
// 首条消息需包含 {"type":"auth","userId":"user_xxx"},占位阶段不校验 token
|
||||
// 容错:panic 时 recover 并关闭连接,不影响 HTTP API 及其他请求
|
||||
func WsMiniprogram(c *gin.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[WS] WsMiniprogram panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
conn, err := wsUpgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
var userID string
|
||||
authOK := false
|
||||
|
||||
// 读取首条消息:auth
|
||||
conn.SetReadDeadline(time.Now().Add(15 * time.Second))
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var authMsg struct {
|
||||
Type string `json:"type"`
|
||||
UserID string `json:"userId"`
|
||||
}
|
||||
if json.Unmarshal(msg, &authMsg) == nil && authMsg.Type == "auth" && authMsg.UserID != "" {
|
||||
userID = strings.TrimSpace(authMsg.UserID)
|
||||
// 占位:校验用户存在即可
|
||||
db := database.DB()
|
||||
var u model.User
|
||||
if db.Where("id = ?", userID).First(&u).Error == nil {
|
||||
authOK = true
|
||||
}
|
||||
}
|
||||
if !authOK {
|
||||
conn.WriteJSON(map[string]interface{}{"type": "error", "message": "auth failed"})
|
||||
return
|
||||
}
|
||||
|
||||
// 鉴权通过,开始处理心跳
|
||||
conn.SetReadDeadline(time.Time{}) // 取消超时
|
||||
client := redis.Client()
|
||||
if client == nil {
|
||||
log.Printf("[WS] Redis 未启用,在线状态不可用")
|
||||
return
|
||||
}
|
||||
key := wsOnlinePrefix + userID
|
||||
ctx := context.Background()
|
||||
ttl := time.Duration(wsOfflineTimeout) * time.Second
|
||||
|
||||
// 立即写入一次在线
|
||||
client.Set(ctx, key, "1", ttl)
|
||||
|
||||
// 心跳读取循环
|
||||
for {
|
||||
_, msg, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
var m struct {
|
||||
Type string `json:"type"`
|
||||
}
|
||||
if json.Unmarshal(msg, &m) == nil && (m.Type == "ping" || m.Type == "heartbeat") {
|
||||
client.Set(ctx, key, "1", ttl)
|
||||
conn.WriteJSON(map[string]interface{}{"type": "pong"})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// AdminUsersOnlineStats GET /api/admin/users/online-stats 管理端在线人数统计
|
||||
// 容错:Redis 不可用时返回 success + onlineCount: 0,不影响管理端其他功能
|
||||
func AdminUsersOnlineStats(c *gin.Context) {
|
||||
client := redis.Client()
|
||||
if client == nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "onlineCount": 0})
|
||||
return
|
||||
}
|
||||
ctx := context.Background()
|
||||
iter := client.Scan(ctx, 0, wsOnlinePrefix+"*", 0).Iterator()
|
||||
count := 0
|
||||
for iter.Next(ctx) {
|
||||
count++
|
||||
}
|
||||
if err := iter.Err(); err != nil {
|
||||
log.Printf("[WS] AdminUsersOnlineStats Redis scan err: %v,降级返回 0", err)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "onlineCount": 0})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "onlineCount": count})
|
||||
}
|
||||
|
||||
// GetWsURL 返回小程序可用的 WSS 地址(基于 API_BASE_URL 派生)
|
||||
func GetWsURL() string {
|
||||
cfg := config.Get()
|
||||
if cfg == nil {
|
||||
return ""
|
||||
}
|
||||
base := strings.TrimSuffix(cfg.BaseURL, "/")
|
||||
if base == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(base, "https://") {
|
||||
return "wss" + strings.TrimPrefix(base, "https") + "/ws/miniprogram"
|
||||
}
|
||||
if strings.HasPrefix(base, "http://") {
|
||||
return strings.Replace(base, "http", "ws", 1) + "/ws/miniprogram"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
Reference in New Issue
Block a user