Merge branch 'devlop' into yongxu-dev
# Conflicts: # miniprogram/app.js resolved by devlop version # miniprogram/pages/chapters/chapters.js resolved by devlop version # miniprogram/pages/match/match.js resolved by devlop version # miniprogram/pages/member-detail/member-detail.js resolved by devlop version # miniprogram/pages/my/my.js resolved by devlop version # miniprogram/pages/read/read.js resolved by devlop version # miniprogram/pages/referral/referral.js resolved by devlop version # soul-api/internal/model/person.go resolved by devlop version
This commit is contained in:
@@ -17,6 +17,197 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// parseConfigBool 将 JSON/map 中可能出现的 bool、字符串、数字归一为开关态(auditMode 等)
|
||||
func parseConfigBool(v interface{}) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
switch t := v.(type) {
|
||||
case bool:
|
||||
return t
|
||||
case string:
|
||||
s := strings.ToLower(strings.TrimSpace(t))
|
||||
return s == "1" || s == "true" || s == "yes" || s == "on"
|
||||
case float64:
|
||||
return t != 0
|
||||
case int:
|
||||
return t != 0
|
||||
case int64:
|
||||
return t != 0
|
||||
case json.Number:
|
||||
if i, err := t.Int64(); err == nil {
|
||||
return i != 0
|
||||
}
|
||||
if f, err := t.Float64(); err == nil {
|
||||
return f != 0
|
||||
}
|
||||
return false
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// isLikelyWxMiniProgramAppID 判断是否为微信小程序 AppID 常见形态:wx + 16 位十六进制(共 18 字符)。
|
||||
// 后台「链接标签」若 type=miniprogram 且在此列直接填真实 AppID,C 端会把该值当作 mpKey 去 linkedMiniprograms 里匹配 key;
|
||||
// 若未单独配置 linked_miniprograms,会提示「未找到关联小程序配置」。mergeDirectMiniProgramLinksFromLinkTags 会据此自动补全映射。
|
||||
func isLikelyWxMiniProgramAppID(s string) bool {
|
||||
s = strings.TrimSpace(s)
|
||||
if len(s) != 18 || !strings.HasPrefix(s, "wx") {
|
||||
return false
|
||||
}
|
||||
for i := 2; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F') {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func linkedMiniprogramItemKey(item gin.H) string {
|
||||
if v, ok := item["key"]; ok && v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
return strings.TrimSpace(s)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func linkedMiniprogramItemAppIDEmpty(item gin.H) bool {
|
||||
v, ok := item["appId"]
|
||||
if !ok || v == nil {
|
||||
return true
|
||||
}
|
||||
s, ok := v.(string)
|
||||
return !ok || strings.TrimSpace(s) == ""
|
||||
}
|
||||
|
||||
func linkedMiniprogramItemPathEmpty(item gin.H) bool {
|
||||
v, ok := item["path"]
|
||||
if !ok || v == nil {
|
||||
return true
|
||||
}
|
||||
s, ok := v.(string)
|
||||
return !ok || strings.TrimSpace(s) == ""
|
||||
}
|
||||
|
||||
// mergeDirectMiniProgramLinksFromLinkTags 将「直接填写微信 AppID」的链接标签并入 linkedMiniprograms,兼容现有小程序 navigateToMiniProgram 查表逻辑(不改 C 端)。
|
||||
func mergeDirectMiniProgramLinksFromLinkTags(linked *[]gin.H, tags []model.LinkTag) {
|
||||
if linked == nil {
|
||||
return
|
||||
}
|
||||
byKey := make(map[string]int)
|
||||
for i := range *linked {
|
||||
k := linkedMiniprogramItemKey((*linked)[i])
|
||||
if k != "" {
|
||||
byKey[k] = i
|
||||
}
|
||||
}
|
||||
for _, t := range tags {
|
||||
if strings.TrimSpace(strings.ToLower(t.Type)) != "miniprogram" {
|
||||
continue
|
||||
}
|
||||
app := strings.TrimSpace(t.AppID)
|
||||
if app == "" || !isLikelyWxMiniProgramAppID(app) {
|
||||
continue
|
||||
}
|
||||
path := strings.TrimSpace(t.PagePath)
|
||||
if idx, ok := byKey[app]; ok {
|
||||
item := (*linked)[idx]
|
||||
if linkedMiniprogramItemAppIDEmpty(item) {
|
||||
item["appId"] = app
|
||||
}
|
||||
if path != "" && linkedMiniprogramItemPathEmpty(item) {
|
||||
item["path"] = path
|
||||
}
|
||||
(*linked)[idx] = item
|
||||
continue
|
||||
}
|
||||
entry := gin.H{"key": app, "appId": app}
|
||||
if path != "" {
|
||||
entry["path"] = path
|
||||
}
|
||||
*linked = append(*linked, entry)
|
||||
byKey[app] = len(*linked) - 1
|
||||
}
|
||||
}
|
||||
|
||||
// defaultMpUi 小程序文案与导航默认值,存于 mp_config.mpUi;管理端系统设置可部分覆盖(深合并)
|
||||
func defaultMpUi() gin.H {
|
||||
return gin.H{
|
||||
"tabBar": gin.H{
|
||||
"home": "首页", "chapters": "目录", "match": "找伙伴", "my": "我的",
|
||||
},
|
||||
"chaptersPage": gin.H{
|
||||
"bookTitle": "一场SOUL的创业实验场",
|
||||
"bookSubtitle": "来自Soul派对房的真实商业故事",
|
||||
},
|
||||
"homePage": gin.H{
|
||||
"logoTitle": "卡若创业派对", "logoSubtitle": "来自派对房的真实故事",
|
||||
"linkKaruoText": "点击链接卡若", "linkKaruoAvatar": "",
|
||||
"searchPlaceholder": "搜索章节标题或内容...",
|
||||
"bannerTag": "推荐", "bannerReadMoreText": "点击阅读",
|
||||
"superSectionTitle": "超级个体", "superSectionLinkText": "获客入口",
|
||||
"superSectionLinkPath": "/pages/match/match",
|
||||
"pickSectionTitle": "精选推荐",
|
||||
"latestSectionTitle": "最新新增",
|
||||
},
|
||||
"myPage": gin.H{
|
||||
"cardLabel": "名片", "vipLabelVip": "会员中心", "vipLabelGuest": "成为会员",
|
||||
"cardPath": "", "vipPath": "/pages/vip/vip",
|
||||
"readStatLabel": "已读章节", "recentReadTitle": "最近阅读",
|
||||
"readStatPath": "/pages/reading-records/reading-records?focus=all",
|
||||
"recentReadPath": "/pages/reading-records/reading-records?focus=recent",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func asStringMap(v interface{}) map[string]interface{} {
|
||||
if v == nil {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
m, ok := v.(map[string]interface{})
|
||||
if !ok {
|
||||
return map[string]interface{}{}
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// deepMergeMpUi 将 DB 中的 mpUi 与默认值深合并(嵌套 map)
|
||||
func deepMergeMpUi(base gin.H, overRaw interface{}) gin.H {
|
||||
over := asStringMap(overRaw)
|
||||
out := gin.H{}
|
||||
for k, v := range base {
|
||||
out[k] = v
|
||||
}
|
||||
for k, v := range over {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
bv := out[k]
|
||||
vm := asStringMap(v)
|
||||
if len(vm) == 0 && v != nil {
|
||||
// 非 map 覆盖
|
||||
out[k] = v
|
||||
continue
|
||||
}
|
||||
if len(vm) > 0 {
|
||||
bm := asStringMap(bv)
|
||||
if len(bm) == 0 {
|
||||
out[k] = deepMergeMpUi(gin.H{}, vm)
|
||||
} else {
|
||||
sub := gin.H{}
|
||||
for sk, sv := range bm {
|
||||
sub[sk] = sv
|
||||
}
|
||||
out[k] = deepMergeMpUi(sub, vm)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// buildMiniprogramConfig 从 DB 构建小程序配置,供 GetPublicDBConfig 与 WarmConfigCache 复用
|
||||
func buildMiniprogramConfig() gin.H {
|
||||
defaultPrices := gin.H{"section": float64(1), "fullbook": 9.9}
|
||||
@@ -36,6 +227,7 @@ func buildMiniprogramConfig() gin.H {
|
||||
"auditMode": false,
|
||||
"supportWechat": true,
|
||||
"shareIcon": "", // 分享图标URL,由管理端配置
|
||||
"mpUi": defaultMpUi(),
|
||||
}
|
||||
|
||||
out := gin.H{
|
||||
@@ -87,6 +279,7 @@ func buildMiniprogramConfig() gin.H {
|
||||
for k, v := range m {
|
||||
merged[k] = v
|
||||
}
|
||||
merged["mpUi"] = deepMergeMpUi(defaultMpUi(), m["mpUi"])
|
||||
out["mpConfig"] = merged
|
||||
out["configs"].(gin.H)["mp_config"] = merged
|
||||
}
|
||||
@@ -123,42 +316,46 @@ func buildMiniprogramConfig() gin.H {
|
||||
if _, has := out["userDiscount"]; !has {
|
||||
out["userDiscount"] = float64(5)
|
||||
}
|
||||
// 链接标签列表(小程序 onLinkTagTap 需要 type;miniprogram 类型存 mpKey,用 key 查 linkedMiniprograms 得 appId)
|
||||
// 链接标签列表(小程序 onLinkTagTap:miniprogram 类型下发 mpKey=C 端用其匹配 linkedMiniprograms[].key;历史设计为「密钥→appId」,现支持 app_id 列直接填微信 AppID 并由下方 merge 自动补 linkedMiniprograms)
|
||||
var linkTagRows []model.LinkTag
|
||||
if err := db.Order("label ASC").Find(&linkTagRows).Error; err == nil {
|
||||
tags := make([]gin.H, 0, len(linkTagRows))
|
||||
for _, t := range linkTagRows {
|
||||
h := gin.H{"tagId": t.TagID, "label": t.Label, "url": t.URL, "type": t.Type, "pagePath": t.PagePath}
|
||||
if t.Type == "miniprogram" {
|
||||
h["mpKey"] = t.AppID // miniprogram 类型时 AppID 列存的是密钥
|
||||
} else {
|
||||
h["appId"] = t.AppID
|
||||
_ = db.Order("label ASC").Find(&linkTagRows).Error
|
||||
tags := make([]gin.H, 0, len(linkTagRows))
|
||||
for _, t := range linkTagRows {
|
||||
cType := t.Type
|
||||
cURL := t.URL
|
||||
// wxlink(小程序短链):下发给 C 端时转为 url 类型,现有 read.js web-view 可直接跳转,无需升级小程序
|
||||
if strings.EqualFold(cType, "wxlink") {
|
||||
cType = "url"
|
||||
if cURL == "" {
|
||||
cURL = t.AppID
|
||||
}
|
||||
tags = append(tags, h)
|
||||
}
|
||||
out["linkTags"] = tags
|
||||
h := gin.H{"tagId": t.TagID, "label": t.Label, "url": cURL, "type": cType, "pagePath": t.PagePath}
|
||||
if t.Type == "miniprogram" {
|
||||
h["mpKey"] = t.AppID
|
||||
} else {
|
||||
h["appId"] = t.AppID
|
||||
}
|
||||
tags = append(tags, h)
|
||||
}
|
||||
// 关联小程序列表(key 为 32 位密钥,小程序用 key 查 appId 后 wx.navigateToMiniProgram)
|
||||
out["linkTags"] = tags
|
||||
|
||||
// 关联小程序列表(小程序:find(m => m.key === mpKey) → navigateToMiniProgram)
|
||||
var linkedList []gin.H
|
||||
var linkedMpRow model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "linked_miniprograms").First(&linkedMpRow).Error; err == nil && len(linkedMpRow.ConfigValue) > 0 {
|
||||
var linkedList []gin.H
|
||||
if err := json.Unmarshal(linkedMpRow.ConfigValue, &linkedList); err == nil && len(linkedList) > 0 {
|
||||
out["linkedMiniprograms"] = linkedList
|
||||
} else {
|
||||
// JSON解析失败,使用空数组
|
||||
out["linkedMiniprograms"] = []gin.H{}
|
||||
if err := json.Unmarshal(linkedMpRow.ConfigValue, &linkedList); err != nil {
|
||||
linkedList = nil
|
||||
}
|
||||
} else {
|
||||
// 未找到配置或查询失败,使用空数组作为默认值
|
||||
out["linkedMiniprograms"] = []gin.H{}
|
||||
}
|
||||
// 明确归一化 auditMode:仅当 DB 显式为 true 时返回 true,否则一律 false(避免历史脏数据/类型异常导致误判)
|
||||
if linkedList == nil {
|
||||
linkedList = []gin.H{}
|
||||
}
|
||||
mergeDirectMiniProgramLinksFromLinkTags(&linkedList, linkTagRows)
|
||||
out["linkedMiniprograms"] = linkedList
|
||||
// 归一化 auditMode(兼容历史 bool / 字符串 / 数字)
|
||||
if mp, ok := out["mpConfig"].(gin.H); ok {
|
||||
if v, ok := mp["auditMode"].(bool); ok && v {
|
||||
mp["auditMode"] = true
|
||||
} else {
|
||||
mp["auditMode"] = false
|
||||
}
|
||||
mp["auditMode"] = parseConfigBool(mp["auditMode"])
|
||||
}
|
||||
return out
|
||||
}
|
||||
@@ -203,10 +400,7 @@ func getAuditModeFromDB() bool {
|
||||
if err := json.Unmarshal(row.ConfigValue, &mp); err != nil {
|
||||
return false
|
||||
}
|
||||
if v, ok := mp["auditMode"].(bool); ok && v {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
return parseConfigBool(mp["auditMode"])
|
||||
}
|
||||
|
||||
// GetCoreConfig GET /api/miniprogram/config/core 核心配置(prices、features、userDiscount、mpConfig),首屏/Tab 用
|
||||
@@ -303,9 +497,7 @@ func WarmConfigCache() {
|
||||
// 拆分接口预热
|
||||
auditMode := false
|
||||
if mp, ok := out["mpConfig"].(gin.H); ok {
|
||||
if v, ok := mp["auditMode"].(bool); ok && v {
|
||||
auditMode = true
|
||||
}
|
||||
auditMode = parseConfigBool(mp["auditMode"])
|
||||
}
|
||||
cache.Set(context.Background(), cache.KeyConfigAuditMode, gin.H{"auditMode": auditMode}, cache.AuditModeTTL)
|
||||
core := gin.H{
|
||||
@@ -392,6 +584,7 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
"minWithdraw": float64(10),
|
||||
"auditMode": false,
|
||||
"supportWechat": true,
|
||||
"mpUi": defaultMpUi(),
|
||||
}
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
@@ -428,6 +621,7 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
for k, v := range m {
|
||||
merged[k] = v
|
||||
}
|
||||
merged["mpUi"] = deepMergeMpUi(defaultMpUi(), m["mpUi"])
|
||||
out["mpConfig"] = merged
|
||||
}
|
||||
case "oss_config":
|
||||
@@ -818,7 +1012,9 @@ func DBUsersList(c *gin.Context) {
|
||||
pattern := "%" + search + "%"
|
||||
query = query.Where("COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?", pattern, pattern, pattern)
|
||||
}
|
||||
if vipFilter == "true" || vipFilter == "1" {
|
||||
if poolFilter == "complete" {
|
||||
query = query.Where("(phone IS NOT NULL AND phone != '') AND (nickname IS NOT NULL AND nickname != '' AND nickname != '微信用户') AND (avatar IS NOT NULL AND avatar != '')")
|
||||
} else if vipFilter == "true" || vipFilter == "1" {
|
||||
query = query.Where("id IN (SELECT user_id FROM orders WHERE product_type IN ? AND (status = ? OR status = ?)) OR (is_vip = 1 AND vip_expire_date > ?)",
|
||||
[]string{"fullbook", "vip"}, "paid", "completed", time.Now())
|
||||
}
|
||||
@@ -853,7 +1049,7 @@ func DBUsersList(c *gin.Context) {
|
||||
var fullbookRows []struct {
|
||||
UserID string
|
||||
}
|
||||
db.Model(&model.Order{}).Select("user_id").Where("product_type IN ? AND status = ?", []string{"fullbook", "vip"}, "paid").Find(&fullbookRows)
|
||||
db.Model(&model.Order{}).Select("user_id").Where("product_type IN ? AND status IN ?", []string{"fullbook", "vip"}, []string{"paid", "completed", "success"}).Find(&fullbookRows)
|
||||
for _, r := range fullbookRows {
|
||||
hasFullBookMap[r.UserID] = true
|
||||
}
|
||||
@@ -862,7 +1058,7 @@ func DBUsersList(c *gin.Context) {
|
||||
Count int64
|
||||
}
|
||||
db.Model(&model.Order{}).Select("user_id, COUNT(*) as count").
|
||||
Where("product_type = ? AND status = ?", "section", "paid").
|
||||
Where("product_type = ? AND status IN ?", "section", []string{"paid", "completed", "success"}).
|
||||
Group("user_id").Find(§ionRows)
|
||||
for _, r := range sectionRows {
|
||||
sectionCountMap[r.UserID] = int(r.Count)
|
||||
@@ -925,6 +1121,35 @@ func DBUsersList(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 4. RFM 实时打分:对当前页用户批量计算(只查当前页 userIDs 的聚合)
|
||||
type rfmAgg struct {
|
||||
UserID string
|
||||
OrderCount int
|
||||
TotalAmount float64
|
||||
LastOrderAt time.Time
|
||||
}
|
||||
var rfmAggs []rfmAgg
|
||||
db.Raw(`SELECT user_id, COUNT(*) as order_count, SUM(amount) as total_amount, MAX(created_at) as last_order_at
|
||||
FROM orders WHERE user_id IN ? AND status IN ('paid','success','completed')
|
||||
GROUP BY user_id`, userIDs).Scan(&rfmAggs)
|
||||
rfmAggMap := make(map[string]rfmAgg, len(rfmAggs))
|
||||
var rfmMaxRecency, rfmMaxFreq int
|
||||
var rfmMaxMonetary float64
|
||||
now := time.Now()
|
||||
for _, a := range rfmAggs {
|
||||
rfmAggMap[a.UserID] = a
|
||||
days := int(now.Sub(a.LastOrderAt).Hours() / 24)
|
||||
if days > rfmMaxRecency {
|
||||
rfmMaxRecency = days
|
||||
}
|
||||
if a.OrderCount > rfmMaxFreq {
|
||||
rfmMaxFreq = a.OrderCount
|
||||
}
|
||||
if a.TotalAmount > rfmMaxMonetary {
|
||||
rfmMaxMonetary = a.TotalAmount
|
||||
}
|
||||
}
|
||||
|
||||
// 填充每个用户的实时计算字段
|
||||
for i := range users {
|
||||
uid := users[i].ID
|
||||
@@ -957,6 +1182,16 @@ func DBUsersList(c *gin.Context) {
|
||||
bindCount = dbCount
|
||||
}
|
||||
users[i].ReferralCount = ptrInt(bindCount)
|
||||
|
||||
// RFM 打分(有订单的用户才有分数)
|
||||
if agg, ok := rfmAggMap[uid]; ok {
|
||||
recencyDays := int(now.Sub(agg.LastOrderAt).Hours() / 24)
|
||||
score := calcRFMScoreForUser(recencyDays, agg.OrderCount, agg.TotalAmount,
|
||||
rfmMaxRecency, rfmMaxFreq, rfmMaxMonetary)
|
||||
level := calcRFMLevel(score)
|
||||
users[i].RFMScore = ptrFloat64(score)
|
||||
users[i].RFMLevel = &level
|
||||
}
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -1176,6 +1411,106 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
// 入站来源链路:即使未完成绑定,也保留“通过谁的分享链接点击进入”的历史
|
||||
var currentUser model.User
|
||||
_ = db.Select("id,open_id").Where("id = ?", userId).First(¤tUser).Error
|
||||
var inboundVisits []model.ReferralVisit
|
||||
visitQ := db.Model(&model.ReferralVisit{}).Where("visitor_id = ?", userId)
|
||||
if currentUser.OpenID != nil && strings.TrimSpace(*currentUser.OpenID) != "" {
|
||||
visitQ = visitQ.Or("visitor_openid = ?", strings.TrimSpace(*currentUser.OpenID))
|
||||
}
|
||||
_ = visitQ.Order("created_at ASC").Limit(300).Find(&inboundVisits).Error
|
||||
|
||||
referrerVisitIDs := make(map[string]bool)
|
||||
for _, v := range inboundVisits {
|
||||
if strings.TrimSpace(v.ReferrerID) != "" {
|
||||
referrerVisitIDs[strings.TrimSpace(v.ReferrerID)] = true
|
||||
}
|
||||
}
|
||||
referrerVisitList := make([]string, 0, len(referrerVisitIDs))
|
||||
for id := range referrerVisitIDs {
|
||||
referrerVisitList = append(referrerVisitList, id)
|
||||
}
|
||||
referrerVisitUserMap := make(map[string]*model.User)
|
||||
if len(referrerVisitList) > 0 {
|
||||
var rs []model.User
|
||||
_ = db.Where("id IN ?", referrerVisitList).Find(&rs).Error
|
||||
for i := range rs {
|
||||
referrerVisitUserMap[rs[i].ID] = &rs[i]
|
||||
}
|
||||
}
|
||||
|
||||
inboundVisitItems := make([]gin.H, 0, len(inboundVisits))
|
||||
firstInbound := gin.H{}
|
||||
latestInbound := gin.H{}
|
||||
for i, v := range inboundVisits {
|
||||
nickname := "微信用户"
|
||||
avatar := ""
|
||||
if u := referrerVisitUserMap[v.ReferrerID]; u != nil {
|
||||
if u.Nickname != nil && strings.TrimSpace(*u.Nickname) != "" {
|
||||
nickname = strings.TrimSpace(*u.Nickname)
|
||||
}
|
||||
if u.Avatar != nil {
|
||||
avatar = resolveAvatarURL(strings.TrimSpace(*u.Avatar))
|
||||
}
|
||||
}
|
||||
source := ""
|
||||
page := ""
|
||||
if v.Source != nil {
|
||||
source = strings.TrimSpace(*v.Source)
|
||||
}
|
||||
if v.Page != nil {
|
||||
page = strings.TrimSpace(*v.Page)
|
||||
}
|
||||
item := gin.H{
|
||||
"seq": i + 1,
|
||||
"visitedAt": v.CreatedAt,
|
||||
"referrerId": v.ReferrerID,
|
||||
"referrerNickname": nickname,
|
||||
"referrerAvatar": avatar,
|
||||
"source": source,
|
||||
"page": page,
|
||||
}
|
||||
if i == 0 {
|
||||
firstInbound = item
|
||||
}
|
||||
latestInbound = item
|
||||
inboundVisitItems = append(inboundVisitItems, item)
|
||||
}
|
||||
|
||||
activeBinding := gin.H{}
|
||||
var activeRef model.ReferralBinding
|
||||
if err := db.Where("referee_id = ? AND status = ?", userId, "active").Order("binding_date DESC").First(&activeRef).Error; err == nil {
|
||||
bindNick := "微信用户"
|
||||
bindAvatar := ""
|
||||
if u := referrerVisitUserMap[activeRef.ReferrerID]; u != nil {
|
||||
if u.Nickname != nil && strings.TrimSpace(*u.Nickname) != "" {
|
||||
bindNick = strings.TrimSpace(*u.Nickname)
|
||||
}
|
||||
if u.Avatar != nil {
|
||||
bindAvatar = resolveAvatarURL(strings.TrimSpace(*u.Avatar))
|
||||
}
|
||||
} else {
|
||||
var ru model.User
|
||||
if err := db.Select("id,nickname,avatar").Where("id = ?", activeRef.ReferrerID).First(&ru).Error; err == nil {
|
||||
if ru.Nickname != nil && strings.TrimSpace(*ru.Nickname) != "" {
|
||||
bindNick = strings.TrimSpace(*ru.Nickname)
|
||||
}
|
||||
if ru.Avatar != nil {
|
||||
bindAvatar = resolveAvatarURL(strings.TrimSpace(*ru.Avatar))
|
||||
}
|
||||
}
|
||||
}
|
||||
activeBinding = gin.H{
|
||||
"referrerId": activeRef.ReferrerID,
|
||||
"referrerNickname": bindNick,
|
||||
"referrerAvatar": bindAvatar,
|
||||
"referralCode": activeRef.ReferralCode,
|
||||
"bindingDate": activeRef.BindingDate,
|
||||
"expiryDate": activeRef.ExpiryDate,
|
||||
}
|
||||
}
|
||||
|
||||
var bindings []model.ReferralBinding
|
||||
if err := db.Where("referrer_id = ?", userId).Order("binding_date DESC").Find(&bindings).Error; err != nil {
|
||||
bindings = []model.ReferralBinding{}
|
||||
@@ -1318,6 +1653,13 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "referrals": referrals,
|
||||
"inboundSource": gin.H{
|
||||
"totalVisits": len(inboundVisitItems),
|
||||
"firstVisit": firstInbound,
|
||||
"latestVisit": latestInbound,
|
||||
"activeBinding": activeBinding,
|
||||
"visits": inboundVisitItems,
|
||||
},
|
||||
"stats": gin.H{
|
||||
"total": totalReferrals, "purchased": purchased, "free": totalReferrals - purchased,
|
||||
"earnings": roundFloat(earningsE, 2), "pendingEarnings": roundFloat(availableE, 2), "withdrawnEarnings": roundFloat(withdrawnE, 2),
|
||||
|
||||
Reference in New Issue
Block a user