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:
卡若
2026-03-22 08:34:28 +08:00
parent 17ce20c8ee
commit 5724fba877
119 changed files with 8198 additions and 4369 deletions

View File

@@ -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),
},
})