同步
This commit is contained in:
@@ -392,6 +392,29 @@ func AdminSuperIndividualStats(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": out, "total": len(out)})
|
||||
}
|
||||
|
||||
// AdminDashboardLeads GET /api/admin/dashboard/leads 管理端看板-存客宝线索/提交记录概览
|
||||
func AdminDashboardLeads(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var contactTotal, submitTotal, uniqueContactUsers int64
|
||||
db.Model(&model.CkbLeadRecord{}).Count(&contactTotal)
|
||||
db.Model(&model.CkbSubmitRecord{}).Count(&submitTotal)
|
||||
db.Raw(`SELECT COUNT(DISTINCT user_id) FROM ckb_lead_records WHERE user_id IS NOT NULL AND user_id != ''`).Scan(&uniqueContactUsers)
|
||||
var todayContact, todaySubmit int64
|
||||
db.Raw(`SELECT COUNT(*) FROM ckb_lead_records WHERE DATE(created_at) = CURDATE()`).Scan(&todayContact)
|
||||
db.Raw(`SELECT COUNT(*) FROM ckb_submit_records WHERE DATE(created_at) = CURDATE()`).Scan(&todaySubmit)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"contactLeadsTotal": contactTotal,
|
||||
"submitRecordsTotal": submitTotal,
|
||||
"uniqueContactUsers": uniqueContactUsers,
|
||||
"todayContactLeads": todayContact,
|
||||
"todaySubmitRecords": todaySubmit,
|
||||
"combinedTotal": contactTotal + submitTotal,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func buildNewUsersOut(newUsers []model.User) []gin.H {
|
||||
out := make([]gin.H, 0, len(newUsers))
|
||||
for _, u := range newUsers {
|
||||
|
||||
@@ -1075,3 +1075,31 @@ func BookStats(c *gin.Context) {
|
||||
func BookSync(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步由 DB 维护"})
|
||||
}
|
||||
|
||||
// BookRanking GET /api/miniprogram/book/ranking 内容排行榜(与 BookRecommended 同一套 computeArticleRankingSections)
|
||||
func BookRanking(c *gin.Context) {
|
||||
limit := 50
|
||||
if l := c.Query("limit"); l != "" {
|
||||
if n, err := strconv.Atoi(l); err == nil && n > 0 && n <= 200 {
|
||||
limit = n
|
||||
}
|
||||
}
|
||||
sections, err := computeArticleRankingSections(database.DB())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "message": "排行榜计算失败"})
|
||||
return
|
||||
}
|
||||
if len(sections) > limit {
|
||||
sections = sections[:limit]
|
||||
}
|
||||
out := make([]gin.H, 0, len(sections))
|
||||
for _, s := range sections {
|
||||
out = append(out, gin.H{
|
||||
"id": s.ID, "mid": s.MID, "title": s.Title,
|
||||
"partTitle": s.PartTitle, "chapterTitle": s.ChapterTitle,
|
||||
"hotScore": s.HotScore, "isPinned": s.IsPinned,
|
||||
"price": s.Price, "isFree": s.IsFree, "isNew": s.IsNew,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
|
||||
}
|
||||
|
||||
@@ -78,6 +78,22 @@ func ckbSign(params map[string]interface{}, apiKey string) string {
|
||||
return hex.EncodeToString(h2[:])
|
||||
}
|
||||
|
||||
// userHasContentPurchase 与小程序资源对接 requirePurchase 一致:已付章节或全书解锁
|
||||
func userHasContentPurchase(db *gorm.DB, userID string) bool {
|
||||
if strings.TrimSpace(userID) == "" {
|
||||
return false
|
||||
}
|
||||
var u model.User
|
||||
if db.Select("has_full_book").Where("id = ?", userID).First(&u).Error == nil {
|
||||
if u.HasFullBook != nil && *u.HasFullBook {
|
||||
return true
|
||||
}
|
||||
}
|
||||
var n int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND product_type = ?", userID, "paid", "section").Count(&n)
|
||||
return n > 0
|
||||
}
|
||||
|
||||
// getCkbLeadApiKey 链接卡若密钥优先级:system_config.site_settings.ckbLeadApiKey > .env CKB_LEAD_API_KEY > 代码内置 ckbAPIKey
|
||||
func getCkbLeadApiKey() string {
|
||||
var row model.SystemConfig
|
||||
@@ -119,6 +135,16 @@ func CKBJoin(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "无效的加入类型"})
|
||||
return
|
||||
}
|
||||
if body.Type == "investor" && body.UserID != "" {
|
||||
if !userHasContentPurchase(database.DB(), body.UserID) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "请先购买章节或解锁全书后再使用资源对接",
|
||||
"errorCode": "ERR_REQUIRE_PURCHASE",
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
nickname := strings.TrimSpace(body.Name)
|
||||
if nickname == "" && body.UserID != "" {
|
||||
var u model.User
|
||||
|
||||
@@ -106,7 +106,7 @@ func buildMiniprogramConfig() gin.H {
|
||||
}
|
||||
}
|
||||
}
|
||||
// 好友优惠(用于 read 页展示优惠价)
|
||||
// 好友优惠与分润(用于 read 页展示优惠价、分享提示分润比例)
|
||||
var refRow model.SystemConfig
|
||||
if err := db.Where("config_key = ?", "referral_config").First(&refRow).Error; err == nil {
|
||||
var refVal map[string]interface{}
|
||||
@@ -114,6 +114,10 @@ func buildMiniprogramConfig() gin.H {
|
||||
if v, ok := refVal["userDiscount"].(float64); ok {
|
||||
out["userDiscount"] = v
|
||||
}
|
||||
// 内容订单分润比例(0-100),供小程序 read/referral 页展示
|
||||
if v, ok := refVal["distributorShare"].(float64); ok {
|
||||
out["shareRate"] = int(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
if _, has := out["userDiscount"]; !has {
|
||||
@@ -218,6 +222,7 @@ func GetCoreConfig(c *gin.Context) {
|
||||
"prices": full["prices"],
|
||||
"features": full["features"],
|
||||
"userDiscount": full["userDiscount"],
|
||||
"shareRate": full["shareRate"],
|
||||
"mpConfig": full["mpConfig"],
|
||||
}
|
||||
if out["prices"] == nil {
|
||||
@@ -229,6 +234,9 @@ func GetCoreConfig(c *gin.Context) {
|
||||
if out["userDiscount"] == nil {
|
||||
out["userDiscount"] = float64(5)
|
||||
}
|
||||
if out["shareRate"] == nil {
|
||||
out["shareRate"] = 90
|
||||
}
|
||||
if out["mpConfig"] == nil {
|
||||
out["mpConfig"] = gin.H{}
|
||||
}
|
||||
@@ -305,6 +313,7 @@ func WarmConfigCache() {
|
||||
"prices": out["prices"],
|
||||
"features": out["features"],
|
||||
"userDiscount": out["userDiscount"],
|
||||
"shareRate": out["shareRate"],
|
||||
"mpConfig": out["mpConfig"],
|
||||
}
|
||||
if core["prices"] == nil {
|
||||
@@ -316,6 +325,9 @@ func WarmConfigCache() {
|
||||
if core["userDiscount"] == nil {
|
||||
core["userDiscount"] = float64(5)
|
||||
}
|
||||
if core["shareRate"] == nil {
|
||||
core["shareRate"] = 90
|
||||
}
|
||||
if core["mpConfig"] == nil {
|
||||
core["mpConfig"] = gin.H{}
|
||||
}
|
||||
|
||||
@@ -591,6 +591,58 @@ func DBPersonPinnedList(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "persons": out})
|
||||
}
|
||||
|
||||
// DBPersonPinnedToken GET /api/db/persons/pinned-token 当前置顶人物 token(管理端预览/配置用,与置顶列表首条一致)
|
||||
func DBPersonPinnedToken(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var p model.Person
|
||||
err := db.Where("is_pinned = ?", true).Order("updated_at DESC").First(&p).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "token": ""})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "token": strings.TrimSpace(p.Token)})
|
||||
}
|
||||
|
||||
// CKBPinnedPerson GET /api/miniprogram/ckb/pinned-person 小程序首页:当前置顶人物(无置顶时 data 为 null)
|
||||
func CKBPinnedPerson(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var p model.Person
|
||||
err := db.Where("is_pinned = ?", true).Order("updated_at DESC").First(&p).Error
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
nickname := strings.TrimSpace(p.Name)
|
||||
avatar := strings.TrimSpace(p.Avatar)
|
||||
if p.UserID != nil && *p.UserID != "" {
|
||||
var u model.User
|
||||
if db.Select("nickname", "avatar").Where("id = ?", *p.UserID).First(&u).Error == nil {
|
||||
if v := getStringValue(u.Nickname); v != "" {
|
||||
nickname = v
|
||||
}
|
||||
if v := getUrlValue(u.Avatar); v != "" {
|
||||
avatar = v
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"nickname": nickname,
|
||||
"avatar": avatar,
|
||||
"token": strings.TrimSpace(p.Token),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AdminCKBPlanCheck GET /api/admin/ckb/plan-check 管理端-检查存客宝计划在线状态
|
||||
// 查询所有有 ckb_plan_id 的 Person,对每个计划调用存客宝获取状态
|
||||
func AdminCKBPlanCheck(c *gin.Context) {
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
@@ -165,12 +166,22 @@ func MatchUsers(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
// 全书用户无限制,否则校验今日剩余次数
|
||||
var user model.User
|
||||
skipQuota := false
|
||||
if err := db.Where("id = ?", body.UserID).First(&user).Error; err == nil {
|
||||
skipQuota = user.HasFullBook != nil && *user.HasFullBook
|
||||
if err := db.Where("id = ?", body.UserID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "用户不存在"})
|
||||
return
|
||||
}
|
||||
phoneOK := user.Phone != nil && strings.TrimSpace(*user.Phone) != ""
|
||||
wechatOK := user.WechatID != nil && strings.TrimSpace(*user.WechatID) != ""
|
||||
if !phoneOK && !wechatOK {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"message": "请先完善手机号或微信号后再发起匹配",
|
||||
"errorCode": "ERR_PROFILE_INCOMPLETE",
|
||||
})
|
||||
return
|
||||
}
|
||||
skipQuota := user.HasFullBook != nil && *user.HasFullBook
|
||||
if !skipQuota {
|
||||
freeLimit := getFreeMatchLimit(db)
|
||||
quota := GetMatchQuota(db, body.UserID, freeLimit)
|
||||
|
||||
@@ -123,6 +123,13 @@ func WechatPhoneLogin(c *gin.Context) {
|
||||
"referralCount": intVal(user.ReferralCount),
|
||||
"createdAt": user.CreatedAt,
|
||||
}
|
||||
// 与 /api/miniprogram/login 一致,避免手机号登录后 VIP 引导、权益展示滞后
|
||||
if user.IsVip != nil {
|
||||
responseUser["isVip"] = *user.IsVip
|
||||
}
|
||||
if user.VipExpireDate != nil {
|
||||
responseUser["vipExpireDate"] = user.VipExpireDate.Format("2006-01-02")
|
||||
}
|
||||
token := fmt.Sprintf("tk_%s_%d", openID[len(openID)-8:], time.Now().Unix())
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/config"
|
||||
@@ -88,14 +89,20 @@ func WithdrawPost(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "用户未绑定微信"})
|
||||
return
|
||||
}
|
||||
// 与小程序 referral 一致:须填写资料中的微信号,便于运营到账核对(不再用 openid 顶替)
|
||||
if user.WechatID == nil || strings.TrimSpace(*user.WechatID) == "" {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": false,
|
||||
"needBindWechat": true,
|
||||
"message": "请先到设置页绑定微信号后再提现,便于到账核对",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
withdrawID := generateWithdrawID()
|
||||
status := "pending"
|
||||
// 根据 user_id 已查到的用户信息,填充提现表所需字段;仅写入表中存在的列,避免 remark 等列不存在报错
|
||||
wechatID := user.WechatID
|
||||
if (wechatID == nil || *wechatID == "") && user.OpenID != nil && *user.OpenID != "" {
|
||||
wechatID = user.OpenID
|
||||
}
|
||||
withdrawal := model.Withdrawal{
|
||||
ID: withdrawID,
|
||||
UserID: req.UserID,
|
||||
|
||||
Reference in New Issue
Block a user