feat: 小程序超级个体/个人资料/CKB获客;VIP列表展示过滤;管理端与API联调
- 超级个体:去掉首位特例;列表仅展示有头像且非微信默认昵称(vip.go) - 个人资料:居中头像、低调联系方式、点头像优先走存客宝 lead(ckbLeadToken) - 阅读页分享朋友圈复制与 toast 去重 - soul-api: miniprogram users 带 ckbLeadToken;其它 handler 与路由调整 - 脚本:content_upload、miniprogram 上传辅助等 Made-with: Cursor
This commit is contained in:
@@ -35,6 +35,7 @@ func buildMiniprogramConfig() gin.H {
|
||||
"mchId": "1318592501",
|
||||
"auditMode": false,
|
||||
"supportWechat": true,
|
||||
"shareIcon": "", // 分享图标URL,由管理端配置
|
||||
}
|
||||
|
||||
out := gin.H{
|
||||
@@ -139,9 +140,12 @@ func buildMiniprogramConfig() gin.H {
|
||||
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 _, has := out["linkedMiniprograms"]; !has {
|
||||
} else {
|
||||
// 未找到配置或查询失败,使用空数组作为默认值
|
||||
out["linkedMiniprograms"] = []gin.H{}
|
||||
}
|
||||
// 明确归一化 auditMode:仅当 DB 显式为 true 时返回 true,否则一律 false(避免历史脏数据/类型异常导致误判)
|
||||
@@ -232,7 +236,33 @@ func GetCoreConfig(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// GetReadExtras GET /api/miniprogram/config/read-extras 阅读页扩展(linkTags、linkedMiniprograms),懒加载
|
||||
// buildMentionPersonsForRead 阅读页 @ 自动解析用:与后台 persons 表一致(含 name/label/aliases/token)。
|
||||
// inPool=true 表示已绑定会员 user_id(超级个体/可进匹配流量池),仍参与 @ 匹配以便正文与后台人物库对齐。
|
||||
func buildMentionPersonsForRead() []gin.H {
|
||||
db := database.DB()
|
||||
var rows []model.Person
|
||||
if err := db.Select("person_id", "token", "name", "aliases", "label", "user_id").
|
||||
Where("token IS NOT NULL AND token != ?", "").
|
||||
Order("name ASC").
|
||||
Find(&rows).Error; err != nil {
|
||||
return []gin.H{}
|
||||
}
|
||||
out := make([]gin.H, 0, len(rows))
|
||||
for _, p := range rows {
|
||||
inPool := p.UserID != nil && strings.TrimSpace(*p.UserID) != ""
|
||||
out = append(out, gin.H{
|
||||
"personId": p.PersonID,
|
||||
"token": p.Token,
|
||||
"name": strings.TrimSpace(p.Name),
|
||||
"aliases": strings.TrimSpace(p.Aliases),
|
||||
"label": strings.TrimSpace(p.Label),
|
||||
"inPool": inPool,
|
||||
})
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
// GetReadExtras GET /api/miniprogram/config/read-extras 阅读页扩展(linkTags、linkedMiniprograms、mentionPersons),懒加载
|
||||
func GetReadExtras(c *gin.Context) {
|
||||
var cached gin.H
|
||||
if cache.Get(context.Background(), cache.KeyConfigReadExtras, &cached) && len(cached) > 0 {
|
||||
@@ -243,6 +273,7 @@ func GetReadExtras(c *gin.Context) {
|
||||
out := gin.H{
|
||||
"linkTags": full["linkTags"],
|
||||
"linkedMiniprograms": full["linkedMiniprograms"],
|
||||
"mentionPersons": buildMentionPersonsForRead(),
|
||||
}
|
||||
if out["linkTags"] == nil {
|
||||
out["linkTags"] = []gin.H{}
|
||||
@@ -250,6 +281,9 @@ func GetReadExtras(c *gin.Context) {
|
||||
if out["linkedMiniprograms"] == nil {
|
||||
out["linkedMiniprograms"] = []gin.H{}
|
||||
}
|
||||
if out["mentionPersons"] == nil {
|
||||
out["mentionPersons"] = []gin.H{}
|
||||
}
|
||||
cache.Set(context.Background(), cache.KeyConfigReadExtras, out, cache.ConfigTTL)
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
@@ -289,6 +323,7 @@ func WarmConfigCache() {
|
||||
readExtras := gin.H{
|
||||
"linkTags": out["linkTags"],
|
||||
"linkedMiniprograms": out["linkedMiniprograms"],
|
||||
"mentionPersons": buildMentionPersonsForRead(),
|
||||
}
|
||||
if readExtras["linkTags"] == nil {
|
||||
readExtras["linkTags"] = []gin.H{}
|
||||
@@ -296,6 +331,9 @@ func WarmConfigCache() {
|
||||
if readExtras["linkedMiniprograms"] == nil {
|
||||
readExtras["linkedMiniprograms"] = []gin.H{}
|
||||
}
|
||||
if readExtras["mentionPersons"] == nil {
|
||||
readExtras["mentionPersons"] = []gin.H{}
|
||||
}
|
||||
cache.Set(context.Background(), cache.KeyConfigReadExtras, readExtras, cache.ConfigTTL)
|
||||
}
|
||||
|
||||
@@ -382,7 +420,15 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
}
|
||||
case "oss_config":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["ossConfig"] = m
|
||||
safe := make(map[string]interface{})
|
||||
for k, v := range m {
|
||||
if k == "accessKeySecret" {
|
||||
safe[k] = "****"
|
||||
} else {
|
||||
safe[k] = v
|
||||
}
|
||||
}
|
||||
out["ossConfig"] = safe
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,17 +885,29 @@ func DBUsersList(c *gin.Context) {
|
||||
pendingWithdrawMap[r.UserID] = r.Total
|
||||
}
|
||||
|
||||
// 3. 绑定人数:从 referral_bindings 计算
|
||||
// 3. 绑定人数:综合 referral_bindings + orders.referrer_id + users.referral_count 三源取最大值
|
||||
referralCountMap := make(map[string]int)
|
||||
var refCountRows []struct {
|
||||
ReferrerID string
|
||||
Count int64
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
db.Model(&model.ReferralBinding{}).Select("referrer_id, COUNT(*) as count").
|
||||
Group("referrer_id").Find(&refCountRows)
|
||||
Group("referrer_id").Scan(&refCountRows)
|
||||
for _, r := range refCountRows {
|
||||
referralCountMap[r.ReferrerID] = int(r.Count)
|
||||
}
|
||||
var orderRefRows []struct {
|
||||
ReferrerID string `gorm:"column:referrer_id"`
|
||||
Count int64 `gorm:"column:count"`
|
||||
}
|
||||
db.Model(&model.Order{}).Select("referrer_id, COUNT(DISTINCT user_id) as count").
|
||||
Where("referrer_id IS NOT NULL AND referrer_id != '' AND status = ?", "paid").
|
||||
Group("referrer_id").Scan(&orderRefRows)
|
||||
for _, r := range orderRefRows {
|
||||
if int(r.Count) > referralCountMap[r.ReferrerID] {
|
||||
referralCountMap[r.ReferrerID] = int(r.Count)
|
||||
}
|
||||
}
|
||||
|
||||
// 填充每个用户的实时计算字段
|
||||
for i := range users {
|
||||
@@ -874,7 +932,15 @@ func DBUsersList(c *gin.Context) {
|
||||
}
|
||||
users[i].Earnings = ptrFloat64(totalE)
|
||||
users[i].PendingEarnings = ptrFloat64(available)
|
||||
users[i].ReferralCount = ptrInt(referralCountMap[uid])
|
||||
bindCount := referralCountMap[uid]
|
||||
dbCount := 0
|
||||
if users[i].ReferralCount != nil {
|
||||
dbCount = *users[i].ReferralCount
|
||||
}
|
||||
if dbCount > bindCount {
|
||||
bindCount = dbCount
|
||||
}
|
||||
users[i].ReferralCount = ptrInt(bindCount)
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
@@ -1096,23 +1162,40 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
|
||||
var bindings []model.ReferralBinding
|
||||
if err := db.Where("referrer_id = ?", userId).Order("binding_date DESC").Find(&bindings).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "referrals": []interface{}{}, "stats": gin.H{"total": 0, "purchased": 0, "free": 0, "earnings": 0, "pendingEarnings": 0, "withdrawnEarnings": 0}})
|
||||
return
|
||||
bindings = []model.ReferralBinding{}
|
||||
}
|
||||
refereeIds := make([]string, 0, len(bindings))
|
||||
refereeIdSet := make(map[string]bool, len(bindings))
|
||||
for _, b := range bindings {
|
||||
refereeIds = append(refereeIds, b.RefereeID)
|
||||
refereeIdSet[b.RefereeID] = true
|
||||
}
|
||||
|
||||
// 补充:从 orders.referrer_id 找到通过推荐码购买但未在 referral_bindings 中的用户
|
||||
var orderBuyerIDs []string
|
||||
db.Model(&model.Order{}).Select("DISTINCT user_id").
|
||||
Where("referrer_id = ? AND status = ? AND user_id != ''", userId, "paid").
|
||||
Pluck("user_id", &orderBuyerIDs)
|
||||
for _, buyerID := range orderBuyerIDs {
|
||||
if !refereeIdSet[buyerID] {
|
||||
refereeIdSet[buyerID] = true
|
||||
}
|
||||
}
|
||||
|
||||
allRefereeIds := make([]string, 0, len(refereeIdSet))
|
||||
for id := range refereeIdSet {
|
||||
allRefereeIds = append(allRefereeIds, id)
|
||||
}
|
||||
var users []model.User
|
||||
if len(refereeIds) > 0 {
|
||||
db.Where("id IN ?", refereeIds).Find(&users)
|
||||
if len(allRefereeIds) > 0 {
|
||||
db.Where("id IN ?", allRefereeIds).Find(&users)
|
||||
}
|
||||
userMap := make(map[string]*model.User)
|
||||
for i := range users {
|
||||
userMap[users[i].ID] = &users[i]
|
||||
}
|
||||
referrals := make([]gin.H, 0, len(bindings))
|
||||
referrals := make([]gin.H, 0, len(bindings)+len(orderBuyerIDs))
|
||||
bindingRefereeSet := make(map[string]bool, len(bindings))
|
||||
for _, b := range bindings {
|
||||
bindingRefereeSet[b.RefereeID] = true
|
||||
u := userMap[b.RefereeID]
|
||||
nick := "微信用户"
|
||||
var avatar *string
|
||||
@@ -1135,9 +1218,8 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
if b.ExpiryDate.After(time.Now()) {
|
||||
daysRemaining = int(b.ExpiryDate.Sub(time.Now()).Hours() / 24)
|
||||
}
|
||||
// 已付费:与小程序一致,以绑定记录的 purchase_count > 0 为准(支付回调会更新该字段)
|
||||
hasPaid := b.PurchaseCount != nil && *b.PurchaseCount > 0
|
||||
displayStatus := bindingStatusDisplay(hasPaid, hasFullBook) // vip | paid | free,供前端徽章展示
|
||||
displayStatus := bindingStatusDisplay(hasPaid, hasFullBook)
|
||||
avStr := ""
|
||||
if avatar != nil {
|
||||
avStr = resolveAvatarURL(*avatar)
|
||||
@@ -1150,6 +1232,34 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
"status": displayStatus,
|
||||
})
|
||||
}
|
||||
for _, buyerID := range orderBuyerIDs {
|
||||
if bindingRefereeSet[buyerID] {
|
||||
continue
|
||||
}
|
||||
u := userMap[buyerID]
|
||||
nick := "微信用户"
|
||||
avStr := ""
|
||||
var phone *string
|
||||
hasFullBook := false
|
||||
if u != nil {
|
||||
if u.Nickname != nil {
|
||||
nick = *u.Nickname
|
||||
}
|
||||
if u.Avatar != nil {
|
||||
avStr = resolveAvatarURL(*u.Avatar)
|
||||
}
|
||||
phone = u.Phone
|
||||
if u.HasFullBook != nil {
|
||||
hasFullBook = *u.HasFullBook
|
||||
}
|
||||
}
|
||||
referrals = append(referrals, gin.H{
|
||||
"id": buyerID, "nickname": nick, "avatar": avStr, "phone": phone,
|
||||
"hasFullBook": hasFullBook, "purchasedSections": 0,
|
||||
"createdAt": nil, "bindingStatus": "order_only", "daysRemaining": 0, "commission": nil,
|
||||
"status": "paid",
|
||||
})
|
||||
}
|
||||
|
||||
// 累计收益、待提现:与小程序 MyEarnings 一致,从订单逐条 computeOrderCommission 求和
|
||||
var refOrders []model.Order
|
||||
@@ -1174,18 +1284,26 @@ func DBUsersReferrals(c *gin.Context) {
|
||||
availableE = 0
|
||||
}
|
||||
|
||||
// 已付费人数:与小程序一致,绑定中 purchase_count > 0 的条数
|
||||
// 已付费人数:绑定中 purchase_count > 0 的 + 仅订单用户(order_only 都已付费)
|
||||
purchased := 0
|
||||
for _, b := range bindings {
|
||||
if b.PurchaseCount != nil && *b.PurchaseCount > 0 {
|
||||
purchased++
|
||||
}
|
||||
}
|
||||
orderOnlyCount := 0
|
||||
for _, buyerID := range orderBuyerIDs {
|
||||
if !bindingRefereeSet[buyerID] {
|
||||
orderOnlyCount++
|
||||
purchased++
|
||||
}
|
||||
}
|
||||
totalReferrals := len(bindings) + orderOnlyCount
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "referrals": referrals,
|
||||
"stats": gin.H{
|
||||
"total": len(bindings), "purchased": purchased, "free": len(bindings) - purchased,
|
||||
"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