更新小程序,新增VIP会员状态管理功能,优化章节解锁逻辑,支持VIP用户访问增值内容。调整用户详情页面,增加VIP相关字段和功能,提升用户体验。更新会议记录,反映最新讨论内容。
This commit is contained in:
@@ -54,6 +54,12 @@ func Init(dsn string) error {
|
||||
if err := db.AutoMigrate(&model.CkbLeadRecord{}); err != nil {
|
||||
log.Printf("database: ckb_lead_records migrate warning: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.Person{}); err != nil {
|
||||
log.Printf("database: persons migrate warning: %v", err)
|
||||
}
|
||||
if err := db.AutoMigrate(&model.LinkTag{}); err != nil {
|
||||
log.Printf("database: link_tags migrate warning: %v", err)
|
||||
}
|
||||
log.Println("database: connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -86,18 +86,34 @@ func DBBookAction(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"section": gin.H{
|
||||
"id": ch.ID,
|
||||
"title": ch.SectionTitle,
|
||||
"price": price,
|
||||
"content": ch.Content,
|
||||
"isNew": ch.IsNew,
|
||||
"partId": ch.PartID,
|
||||
"partTitle": ch.PartTitle,
|
||||
"chapterId": ch.ChapterID,
|
||||
"chapterTitle": ch.ChapterTitle,
|
||||
"id": ch.ID,
|
||||
"title": ch.SectionTitle,
|
||||
"price": price,
|
||||
"content": ch.Content,
|
||||
"isNew": ch.IsNew,
|
||||
"partId": ch.PartID,
|
||||
"partTitle": ch.PartTitle,
|
||||
"chapterId": ch.ChapterID,
|
||||
"chapterTitle": ch.ChapterTitle,
|
||||
"editionStandard": ch.EditionStandard,
|
||||
"editionPremium": ch.EditionPremium,
|
||||
},
|
||||
})
|
||||
return
|
||||
case "section-orders":
|
||||
// 某章节的付款记录(管理端展示)
|
||||
if id == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
var orders []model.Order
|
||||
if err := db.Where("product_type = ? AND product_id = ? AND status IN ?", "section", id, []string{"paid", "completed", "success"}).
|
||||
Order("pay_time DESC, created_at DESC").Limit(200).Find(&orders).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "orders": []model.Order{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "orders": orders})
|
||||
return
|
||||
case "export":
|
||||
var rows []model.Chapter
|
||||
if err := db.Select(listSelectCols).Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
@@ -190,6 +206,12 @@ func DBBookAction(c *gin.Context) {
|
||||
// reorder:新顺序,支持跨篇跨章时附带 partId/chapterId
|
||||
IDs []string `json:"ids"`
|
||||
Items []reorderItem `json:"items"`
|
||||
// move-sections:批量移动节到目标篇/章
|
||||
SectionIds []string `json:"sectionIds"`
|
||||
TargetPartID string `json:"targetPartId"`
|
||||
TargetChapterID string `json:"targetChapterId"`
|
||||
TargetPartTitle string `json:"targetPartTitle"`
|
||||
TargetChapterTitle string `json:"targetChapterTitle"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
@@ -248,6 +270,28 @@ func DBBookAction(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.Action == "move-sections" {
|
||||
if len(body.SectionIds) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "批量移动缺少 sectionIds(请先勾选要移动的节)"})
|
||||
return
|
||||
}
|
||||
if body.TargetPartID == "" || body.TargetChapterID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "批量移动缺少目标篇或目标章(targetPartId、targetChapterId)"})
|
||||
return
|
||||
}
|
||||
up := map[string]interface{}{
|
||||
"part_id": body.TargetPartID,
|
||||
"chapter_id": body.TargetChapterID,
|
||||
"part_title": body.TargetPartTitle,
|
||||
"chapter_title": body.TargetChapterTitle,
|
||||
}
|
||||
if err := db.Model(&model.Chapter{}).Where("id IN ?", body.SectionIds).Updates(up).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "已移动", "count": len(body.SectionIds)})
|
||||
return
|
||||
}
|
||||
if body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
|
||||
138
soul-api/internal/handler/db_ckb_leads.go
Normal file
138
soul-api/internal/handler/db_ckb_leads.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DBCKBLeadList GET /api/db/ckb-leads 管理端-CKB线索明细
|
||||
// mode=submitted: ckb_submit_records(join/match 提交)
|
||||
// mode=contact: ckb_lead_records(链接卡若留资,有 phone/wechat)
|
||||
func DBCKBLeadList(c *gin.Context) {
|
||||
db := database.DB()
|
||||
mode := c.DefaultQuery("mode", "submitted")
|
||||
matchType := c.Query("matchType")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "20"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 20
|
||||
}
|
||||
|
||||
if mode == "contact" {
|
||||
// ckb_lead_records:链接卡若留资
|
||||
q := db.Model(&model.CkbLeadRecord{})
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
var records []model.CkbLeadRecord
|
||||
if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&records).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
out := make([]gin.H, 0, len(records))
|
||||
for _, r := range records {
|
||||
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,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize})
|
||||
return
|
||||
}
|
||||
|
||||
// mode=submitted: ckb_submit_records
|
||||
q := db.Model(&model.CkbSubmitRecord{})
|
||||
if matchType != "" {
|
||||
// matchType 对应 action: join 时 type 在 params 中,match 时 matchType 在 params 中
|
||||
// 简化:仅按 action 过滤,join 时 params 含 type
|
||||
if matchType == "join" || matchType == "match" {
|
||||
q = q.Where("action = ?", matchType)
|
||||
}
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
var records []model.CkbSubmitRecord
|
||||
if err := q.Order("created_at DESC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&records).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
userIDs := make(map[string]bool)
|
||||
for _, r := range records {
|
||||
if r.UserID != "" {
|
||||
userIDs[r.UserID] = true
|
||||
}
|
||||
}
|
||||
ids := make([]string, 0, len(userIDs))
|
||||
for id := range userIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var users []model.User
|
||||
if len(ids) > 0 {
|
||||
db.Where("id IN ?", ids).Find(&users)
|
||||
}
|
||||
userMap := make(map[string]*model.User)
|
||||
for i := range users {
|
||||
userMap[users[i].ID] = &users[i]
|
||||
}
|
||||
safeNickname := func(u *model.User) string {
|
||||
if u == nil || u.Nickname == nil {
|
||||
return ""
|
||||
}
|
||||
return *u.Nickname
|
||||
}
|
||||
out := make([]gin.H, 0, len(records))
|
||||
for _, r := range records {
|
||||
out = append(out, gin.H{
|
||||
"id": r.ID,
|
||||
"userId": r.UserID,
|
||||
"userNickname": safeNickname(userMap[r.UserID]),
|
||||
"matchType": r.Action,
|
||||
"nickname": r.Nickname,
|
||||
"params": r.Params,
|
||||
"createdAt": r.CreatedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "records": out, "total": total, "page": page, "pageSize": pageSize})
|
||||
}
|
||||
|
||||
// CKBPlanStats GET /api/db/ckb-plan-stats 存客宝获客计划统计(基于 ckb_submit_records + ckb_lead_records)
|
||||
func CKBPlanStats(c *gin.Context) {
|
||||
db := database.DB()
|
||||
type TypeStat struct {
|
||||
Action string `gorm:"column:action" json:"matchType"`
|
||||
Total int64 `gorm:"column:total" json:"total"`
|
||||
}
|
||||
var submitStats []TypeStat
|
||||
db.Raw("SELECT action, COUNT(*) as total FROM ckb_submit_records GROUP BY action").Scan(&submitStats)
|
||||
var submitTotal int64
|
||||
db.Model(&model.CkbSubmitRecord{}).Count(&submitTotal)
|
||||
var leadTotal int64
|
||||
db.Model(&model.CkbLeadRecord{}).Count(&leadTotal)
|
||||
withContact := leadTotal // ckb_lead_records 均有 phone 或 wechat
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"ckbTotal": submitTotal + leadTotal,
|
||||
"withContact": withContact,
|
||||
"byType": submitStats,
|
||||
"ckbApiKey": "***",
|
||||
"ckbApiUrl": "https://ckbapi.quwanzhi.com/v1/api/scenarios",
|
||||
"docNotes": "",
|
||||
"docContent": "",
|
||||
"routes": gin.H{},
|
||||
},
|
||||
})
|
||||
}
|
||||
75
soul-api/internal/handler/db_link_tag.go
Normal file
75
soul-api/internal/handler/db_link_tag.go
Normal file
@@ -0,0 +1,75 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DBLinkTagList GET /api/db/link-tags 管理端-链接标签列表
|
||||
func DBLinkTagList(c *gin.Context) {
|
||||
var rows []model.LinkTag
|
||||
if err := database.DB().Order("label ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTags": rows})
|
||||
}
|
||||
|
||||
// DBLinkTagSave POST /api/db/link-tags 管理端-新增或更新链接标签
|
||||
func DBLinkTagSave(c *gin.Context) {
|
||||
var body struct {
|
||||
TagID string `json:"tagId"`
|
||||
Label string `json:"label"`
|
||||
URL string `json:"url"`
|
||||
Type string `json:"type"`
|
||||
AppID string `json:"appId"`
|
||||
PagePath string `json:"pagePath"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
if body.TagID == "" || body.Label == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "tagId 和 label 必填"})
|
||||
return
|
||||
}
|
||||
if body.Type == "" {
|
||||
body.Type = "url"
|
||||
}
|
||||
db := database.DB()
|
||||
var existing model.LinkTag
|
||||
if db.Where("tag_id = ?", body.TagID).First(&existing).Error == nil {
|
||||
existing.Label = body.Label
|
||||
existing.URL = body.URL
|
||||
existing.Type = body.Type
|
||||
existing.AppID = body.AppID
|
||||
existing.PagePath = body.PagePath
|
||||
db.Save(&existing)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": existing})
|
||||
return
|
||||
}
|
||||
t := model.LinkTag{TagID: body.TagID, Label: body.Label, URL: body.URL, Type: body.Type, AppID: body.AppID, PagePath: body.PagePath}
|
||||
if err := db.Create(&t).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "linkTag": t})
|
||||
}
|
||||
|
||||
// DBLinkTagDelete DELETE /api/db/link-tags?tagId=xxx 管理端-删除链接标签
|
||||
func DBLinkTagDelete(c *gin.Context) {
|
||||
tid := c.Query("tagId")
|
||||
if tid == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 tagId"})
|
||||
return
|
||||
}
|
||||
if err := database.DB().Where("tag_id = ?", tid).Delete(&model.LinkTag{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
71
soul-api/internal/handler/db_person.go
Normal file
71
soul-api/internal/handler/db_person.go
Normal file
@@ -0,0 +1,71 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DBPersonList GET /api/db/persons 管理端-@提及人物列表
|
||||
func DBPersonList(c *gin.Context) {
|
||||
var rows []model.Person
|
||||
if err := database.DB().Order("name ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "persons": rows})
|
||||
}
|
||||
|
||||
// DBPersonSave POST /api/db/persons 管理端-新增或更新人物
|
||||
func DBPersonSave(c *gin.Context) {
|
||||
var body struct {
|
||||
PersonID string `json:"personId"`
|
||||
Name string `json:"name"`
|
||||
Label string `json:"label"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
if body.Name == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "name 必填"})
|
||||
return
|
||||
}
|
||||
if body.PersonID == "" {
|
||||
body.PersonID = fmt.Sprintf("%s_%d", body.Name, time.Now().UnixMilli())
|
||||
}
|
||||
db := database.DB()
|
||||
var existing model.Person
|
||||
if db.Where("person_id = ?", body.PersonID).First(&existing).Error == nil {
|
||||
existing.Name = body.Name
|
||||
existing.Label = body.Label
|
||||
db.Save(&existing)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "person": existing})
|
||||
return
|
||||
}
|
||||
p := model.Person{PersonID: body.PersonID, Name: body.Name, Label: body.Label}
|
||||
if err := db.Create(&p).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "person": p})
|
||||
}
|
||||
|
||||
// DBPersonDelete DELETE /api/db/persons?personId=xxx 管理端-删除人物
|
||||
func DBPersonDelete(c *gin.Context) {
|
||||
pid := c.Query("personId")
|
||||
if pid == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 personId"})
|
||||
return
|
||||
}
|
||||
if err := database.DB().Where("person_id = ?", pid).Delete(&model.Person{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
@@ -11,8 +11,40 @@ import (
|
||||
)
|
||||
|
||||
// DBMatchRecordsList GET /api/db/match-records 管理端-匹配记录列表(分页、按类型筛选)
|
||||
// 当 ?stats=true 时返回汇总统计
|
||||
func DBMatchRecordsList(c *gin.Context) {
|
||||
db := database.DB()
|
||||
if c.Query("stats") == "true" {
|
||||
var totalMatches int64
|
||||
db.Raw("SELECT COUNT(*) FROM match_records").Scan(&totalMatches)
|
||||
var todayMatches int64
|
||||
db.Raw("SELECT COUNT(*) FROM match_records WHERE created_at >= CURDATE()").Scan(&todayMatches)
|
||||
type TypeCount struct {
|
||||
MatchType string `json:"matchType" gorm:"column:match_type"`
|
||||
Count int64 `json:"count" gorm:"column:count"`
|
||||
}
|
||||
var byType []TypeCount
|
||||
db.Raw("SELECT match_type, COUNT(*) as count FROM match_records GROUP BY match_type").Scan(&byType)
|
||||
var uniqueUsers int64
|
||||
db.Raw("SELECT COUNT(DISTINCT user_id) FROM match_records WHERE user_id IS NOT NULL AND user_id != ''").Scan(&uniqueUsers)
|
||||
var matchRevenue float64
|
||||
db.Model(&model.Order{}).Where("product_type = ? AND status IN ?", "match", []string{"paid", "completed", "success"}).
|
||||
Select("COALESCE(SUM(amount), 0)").Scan(&matchRevenue)
|
||||
var paidMatchCount int64
|
||||
db.Model(&model.Order{}).Where("product_type = ? AND status IN ?", "match", []string{"paid", "completed", "success"}).Count(&paidMatchCount)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"totalMatches": totalMatches,
|
||||
"todayMatches": todayMatches,
|
||||
"byType": byType,
|
||||
"uniqueUsers": uniqueUsers,
|
||||
"matchRevenue": matchRevenue,
|
||||
"paidMatchCount": paidMatchCount,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
matchType := c.Query("matchType")
|
||||
@@ -94,3 +126,26 @@ func DBMatchRecordsList(c *gin.Context) {
|
||||
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
// DBMatchPoolCounts GET /api/db/match-pool-counts 返回各匹配池的用户人数
|
||||
func DBMatchPoolCounts(c *gin.Context) {
|
||||
db := database.DB()
|
||||
var vipCount int64
|
||||
db.Model(&model.User{}).Where("is_vip = 1 AND vip_expire_date > NOW()").Count(&vipCount)
|
||||
var completeCount int64
|
||||
db.Model(&model.User{}).Where(
|
||||
"(phone IS NOT NULL AND phone != '') AND (nickname IS NOT NULL AND nickname != '' AND nickname != '微信用户') AND (avatar IS NOT NULL AND avatar != '')",
|
||||
).Count(&completeCount)
|
||||
var allCount int64
|
||||
db.Model(&model.User{}).Where(
|
||||
"((wechat_id IS NOT NULL AND wechat_id != '') OR (phone IS NOT NULL AND phone != ''))",
|
||||
).Count(&allCount)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"vip": vipCount,
|
||||
"complete": completeCount,
|
||||
"all": allCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -171,18 +171,43 @@ func UserCheckPurchased(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "has_vip"}})
|
||||
return
|
||||
}
|
||||
hasFullBook := user.HasFullBook != nil && *user.HasFullBook
|
||||
if hasFullBook {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "has_full_book"}})
|
||||
return
|
||||
}
|
||||
if type_ == "fullbook" {
|
||||
// 9.9 买断:永久权益,写入 users.has_full_book;兜底再查订单
|
||||
if user.HasFullBook != nil && *user.HasFullBook {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "has_full_book"}})
|
||||
return
|
||||
}
|
||||
var count int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND product_type = ? AND status = ?", userId, "fullbook", "paid").Count(&count)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": count > 0, "reason": map[bool]string{true: "fullbook_order_exists"}[count > 0]}})
|
||||
return
|
||||
}
|
||||
if type_ == "section" && productId != "" {
|
||||
// 章节:需要区分普通版/增值版
|
||||
var ch model.Chapter
|
||||
// 不加载 content,避免大字段
|
||||
_ = db.Select("id", "is_free", "price", "edition_standard", "edition_premium").Where("id = ?", productId).First(&ch).Error
|
||||
|
||||
// 免费章节:直接可读
|
||||
if ch.ID != "" {
|
||||
if (ch.IsFree != nil && *ch.IsFree) || (ch.Price != nil && *ch.Price == 0) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "free_section"}})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
isPremium := ch.ID != "" && ch.EditionPremium != nil && *ch.EditionPremium
|
||||
// 默认普通版:未明确标记增值版时,按普通版处理
|
||||
isStandard := !isPremium
|
||||
|
||||
// 普通版:买断可读;增值版:买断不包含
|
||||
if isStandard {
|
||||
if user.HasFullBook != nil && *user.HasFullBook {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": true, "reason": "has_full_book"}})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var count int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND product_type = ? AND product_id = ? AND status = ?", userId, "section", productId, "paid").Count(&count)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"isPurchased": count > 0, "reason": map[bool]string{true: "section_order_exists"}[count > 0]}})
|
||||
@@ -382,11 +407,8 @@ func UserPurchaseStatus(c *gin.Context) {
|
||||
if user.PendingEarnings != nil {
|
||||
pendingEarnings = *user.PendingEarnings
|
||||
}
|
||||
// 超级VIP(管理端开通):与 check-purchased 一致,视为全章可读
|
||||
// 9.9 买断:仅表示“普通版买断”,不等同 VIP(增值版仍需 VIP 或单章购买)
|
||||
hasFullBook := user.HasFullBook != nil && *user.HasFullBook
|
||||
if !hasFullBook && user.IsVip != nil && *user.IsVip && user.VipExpireDate != nil && user.VipExpireDate.After(time.Now()) {
|
||||
hasFullBook = true
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{
|
||||
"hasFullBook": hasFullBook,
|
||||
"purchasedSections": purchasedSections,
|
||||
|
||||
@@ -45,8 +45,10 @@ func isVipFromUsers(db *gorm.DB, userID string) (bool, *time.Time) {
|
||||
// isVipFromOrders 从 orders 表判断是否 VIP(兜底)
|
||||
func isVipFromOrders(db *gorm.DB, userID string) (bool, *time.Time) {
|
||||
var order model.Order
|
||||
err := db.Where("user_id = ? AND (status = ? OR status = ?) AND (product_type = ? OR product_type = ?)",
|
||||
userID, "paid", "completed", "fullbook", "vip").
|
||||
// 注意:fullbook=9.9 买断(永久权益),不等同于 VIP(365天)。
|
||||
// VIP 仅认 product_type=vip。
|
||||
err := db.Where("user_id = ? AND (status = ? OR status = ?) AND product_type = ?",
|
||||
userID, "paid", "completed", "vip").
|
||||
Order("pay_time DESC").First(&order).Error
|
||||
if err != nil || order.PayTime == nil {
|
||||
return false, nil
|
||||
@@ -97,7 +99,7 @@ func VipStatus(c *gin.Context) {
|
||||
daysRemaining := 0
|
||||
expStr := ""
|
||||
if expireDate != nil {
|
||||
daysRemaining = int(expireDate.Sub(time.Now()).Hours()/24) + 1
|
||||
daysRemaining = int(time.Until(*expireDate).Hours()/24) + 1
|
||||
if daysRemaining < 0 {
|
||||
daysRemaining = 0
|
||||
}
|
||||
|
||||
18
soul-api/internal/model/link_tag.go
Normal file
18
soul-api/internal/model/link_tag.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// LinkTag 链接标签配置(ContentPage 用)
|
||||
type LinkTag struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
TagID string `gorm:"column:tag_id;size:50;uniqueIndex" json:"tagId"`
|
||||
Label string `gorm:"column:label;size:200" json:"label"`
|
||||
URL string `gorm:"column:url;size:500" json:"url"`
|
||||
Type string `gorm:"column:type;size:20" json:"type"`
|
||||
AppID string `gorm:"column:app_id;size:100" json:"appId,omitempty"`
|
||||
PagePath string `gorm:"column:page_path;size:500" json:"pagePath,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (LinkTag) TableName() string { return "link_tags" }
|
||||
15
soul-api/internal/model/person.go
Normal file
15
soul-api/internal/model/person.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Person @提及人物配置(ContentPage 用)
|
||||
type Person struct {
|
||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||
PersonID string `gorm:"column:person_id;size:50;uniqueIndex" json:"personId"`
|
||||
Name string `gorm:"column:name;size:100" json:"name"`
|
||||
Label string `gorm:"column:label;size:200" json:"label"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Person) TableName() string { return "persons" }
|
||||
@@ -143,11 +143,20 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
db.PUT("/vip-roles", handler.DBVipRolesAction)
|
||||
db.DELETE("/vip-roles", handler.DBVipRolesAction)
|
||||
db.GET("/match-records", handler.DBMatchRecordsList)
|
||||
db.GET("/match-pool-counts", handler.DBMatchPoolCounts)
|
||||
db.GET("/mentors", handler.DBMentorsList)
|
||||
db.POST("/mentors", handler.DBMentorsAction)
|
||||
db.PUT("/mentors", handler.DBMentorsAction)
|
||||
db.DELETE("/mentors", handler.DBMentorsAction)
|
||||
db.GET("/mentor-consultations", handler.DBMentorConsultationsList)
|
||||
db.GET("/persons", handler.DBPersonList)
|
||||
db.POST("/persons", handler.DBPersonSave)
|
||||
db.DELETE("/persons", handler.DBPersonDelete)
|
||||
db.GET("/link-tags", handler.DBLinkTagList)
|
||||
db.POST("/link-tags", handler.DBLinkTagSave)
|
||||
db.DELETE("/link-tags", handler.DBLinkTagDelete)
|
||||
db.GET("/ckb-leads", handler.DBCKBLeadList)
|
||||
db.GET("/ckb-plan-stats", handler.CKBPlanStats)
|
||||
}
|
||||
|
||||
// ----- 分销 -----
|
||||
|
||||
Reference in New Issue
Block a user