删除 miniprogram2 目录及其所有文件,包括项目配置、样式、图标和自定义组件,简化项目结构,专注于 miniprogram 目录的开发和维护。
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
@@ -14,15 +15,32 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminWithdrawalsList GET /api/admin/withdrawals
|
||||
// AdminWithdrawalsList GET /api/admin/withdrawals(支持分页 page、pageSize,筛选 status)
|
||||
func AdminWithdrawalsList(c *gin.Context) {
|
||||
statusFilter := c.Query("status")
|
||||
var list []model.Withdrawal
|
||||
q := database.DB().Order("created_at DESC").Limit(100)
|
||||
if statusFilter != "" {
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
q := db.Model(&model.Withdrawal{})
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
q = q.Where("status = ?", statusFilter)
|
||||
}
|
||||
if err := q.Find(&list).Error; err != nil {
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var list []model.Withdrawal
|
||||
query := db.Order("created_at DESC")
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
query = query.Where("status = ?", statusFilter)
|
||||
}
|
||||
if err := query.Offset((page - 1) * pageSize).Limit(pageSize).Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "withdrawals": []interface{}{}, "stats": gin.H{"total": 0}})
|
||||
return
|
||||
}
|
||||
@@ -82,7 +100,27 @@ func AdminWithdrawalsList(c *gin.Context) {
|
||||
"userConfirmedAt": userConfirmedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "withdrawals": withdrawals, "stats": gin.H{"total": len(withdrawals)}})
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
var pendingCount, successCount, failedCount int64
|
||||
var pendingAmount, successAmount float64
|
||||
db.Model(&model.Withdrawal{}).Where("status IN ?", []string{"pending", "pending_confirm", "processing"}).Count(&pendingCount)
|
||||
db.Model(&model.Withdrawal{}).Where("status IN ?", []string{"pending", "pending_confirm", "processing"}).Select("COALESCE(SUM(amount), 0)").Scan(&pendingAmount)
|
||||
db.Model(&model.Withdrawal{}).Where("status IN ?", []string{"success", "completed"}).Count(&successCount)
|
||||
db.Model(&model.Withdrawal{}).Where("status IN ?", []string{"success", "completed"}).Select("COALESCE(SUM(amount), 0)").Scan(&successAmount)
|
||||
db.Model(&model.Withdrawal{}).Where("status IN ?", []string{"failed", "rejected"}).Count(&failedCount)
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "withdrawals": withdrawals,
|
||||
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||
"stats": gin.H{
|
||||
"total": total, "pendingCount": pendingCount, "pendingAmount": pendingAmount,
|
||||
"successCount": successCount, "successAmount": successAmount, "failedCount": failedCount,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// AdminWithdrawalsAction PUT /api/admin/withdrawals 审核/打款
|
||||
|
||||
@@ -20,7 +20,15 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
defaultFree := []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"}
|
||||
defaultPrices := gin.H{"section": float64(1), "fullbook": 9.9}
|
||||
defaultFeatures := gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true}
|
||||
defaultMp := gin.H{"appId": "wxb8bbb2b10dec74aa", "apiDomain": "https://soul.quwanzhi.com", "buyerDiscount": 5, "referralBindDays": 30, "minWithdraw": 10}
|
||||
defaultMp := gin.H{
|
||||
"appId": "wxb8bbb2b10dec74aa",
|
||||
"apiDomain": "https://soulapi.quwanzhi.com", // 保留以兼容线上旧版小程序(仍从 config 读取)
|
||||
"buyerDiscount": 5,
|
||||
"referralBindDays": 30,
|
||||
"minWithdraw": 10,
|
||||
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
|
||||
"mchId": "1318592501",
|
||||
}
|
||||
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
@@ -88,8 +96,16 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
}
|
||||
case "mp_config":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["mpConfig"] = m
|
||||
out["configs"].(gin.H)["mp_config"] = m
|
||||
// 合并默认值,DB 有则覆盖
|
||||
merged := make(gin.H)
|
||||
for k, v := range defaultMp {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range m {
|
||||
merged[k] = v
|
||||
}
|
||||
out["mpConfig"] = merged
|
||||
out["configs"].(gin.H)["mp_config"] = merged
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,16 +153,24 @@ func DBConfigGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
||||
}
|
||||
|
||||
// AdminSettingsGet GET /api/admin/settings 系统设置页专用:仅返回免费章节、功能开关、站点/作者与价格
|
||||
// AdminSettingsGet GET /api/admin/settings 系统设置页专用:仅返回免费章节、功能开关、站点/作者与价格、小程序配置
|
||||
func AdminSettingsGet(c *gin.Context) {
|
||||
db := database.DB()
|
||||
defaultMp := gin.H{
|
||||
"appId": "wxb8bbb2b10dec74aa",
|
||||
"apiDomain": "https://soulapi.quwanzhi.com", // 保留以兼容线上旧版小程序
|
||||
"withdrawSubscribeTmplId": "u3MbZGPRkrZIk-I7QdpwzFxnO_CeQPaCWF2FkiIablE",
|
||||
"mchId": "1318592501",
|
||||
"minWithdraw": float64(10),
|
||||
}
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"freeChapters": []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"},
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true},
|
||||
"siteSettings": gin.H{"sectionPrice": float64(1), "baseBookPrice": 9.9, "distributorShare": float64(90), "authorInfo": gin.H{}},
|
||||
"mpConfig": defaultMp,
|
||||
}
|
||||
keys := []string{"free_chapters", "feature_config", "site_settings"}
|
||||
keys := []string{"free_chapters", "feature_config", "site_settings", "mp_config"}
|
||||
for _, k := range keys {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", k).First(&row).Error; err != nil {
|
||||
@@ -177,17 +201,29 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
if m, ok := val.(map[string]interface{}); ok && len(m) > 0 {
|
||||
out["siteSettings"] = m
|
||||
}
|
||||
case "mp_config":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
merged := make(gin.H)
|
||||
for k, v := range defaultMp {
|
||||
merged[k] = v
|
||||
}
|
||||
for k, v := range m {
|
||||
merged[k] = v
|
||||
}
|
||||
out["mpConfig"] = merged
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存免费章节、功能开关、站点/作者与价格(不包含小程序配置,该配置已移除)
|
||||
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存免费章节、功能开关、站点/作者与价格、小程序配置
|
||||
func AdminSettingsPost(c *gin.Context) {
|
||||
var body struct {
|
||||
FreeChapters []string `json:"freeChapters"`
|
||||
FeatureConfig map[string]interface{} `json:"featureConfig"`
|
||||
SiteSettings map[string]interface{} `json:"siteSettings"`
|
||||
MpConfig map[string]interface{} `json:"mpConfig"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
@@ -229,6 +265,12 @@ func AdminSettingsPost(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.MpConfig != nil {
|
||||
if err := saveKey("mp_config", "小程序专用配置", body.MpConfig); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存小程序配置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "设置已保存"})
|
||||
}
|
||||
|
||||
@@ -342,13 +384,14 @@ func DBConfigPost(c *gin.Context) {
|
||||
func DBUsersList(c *gin.Context) {
|
||||
db := database.DB()
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "15"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
search := strings.TrimSpace(c.DefaultQuery("search", ""))
|
||||
vipFilter := c.Query("vip") // "true" 时仅返回 VIP(hasFullBook)
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 15
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
q := db.Model(&model.User{})
|
||||
@@ -356,11 +399,24 @@ func DBUsersList(c *gin.Context) {
|
||||
pattern := "%" + search + "%"
|
||||
q = q.Where("COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?", pattern, pattern, pattern)
|
||||
}
|
||||
if vipFilter == "true" || vipFilter == "1" {
|
||||
q = q.Where("id IN (SELECT user_id FROM orders WHERE product_type IN ? AND status = ?)",
|
||||
[]string{"fullbook", "vip"}, "paid")
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var users []model.User
|
||||
if err := q.Order("created_at DESC").
|
||||
query := db.Model(&model.User{})
|
||||
if search != "" {
|
||||
pattern := "%" + search + "%"
|
||||
query = query.Where("COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?", pattern, pattern, pattern)
|
||||
}
|
||||
if vipFilter == "true" || vipFilter == "1" {
|
||||
query = query.Where("id IN (SELECT user_id FROM orders WHERE product_type IN ? AND status = ?)",
|
||||
[]string{"fullbook", "vip"}, "paid")
|
||||
}
|
||||
if err := query.Order("created_at DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(&users).Error; err != nil {
|
||||
@@ -403,7 +459,7 @@ func DBUsersList(c *gin.Context) {
|
||||
var fullbookRows []struct {
|
||||
UserID string
|
||||
}
|
||||
db.Model(&model.Order{}).Select("user_id").Where("product_type = ? AND status = ?", "fullbook", "paid").Find(&fullbookRows)
|
||||
db.Model(&model.Order{}).Select("user_id").Where("product_type IN ? AND status = ?", []string{"fullbook", "vip"}, "paid").Find(&fullbookRows)
|
||||
for _, r := range fullbookRows {
|
||||
hasFullBookMap[r.UserID] = true
|
||||
}
|
||||
@@ -745,17 +801,40 @@ func DBInit(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "初始化接口已就绪(表结构由迁移维护)"}})
|
||||
}
|
||||
|
||||
// DBDistribution GET /api/db/distribution
|
||||
// DBDistribution GET /api/db/distribution(支持分页 page、pageSize,筛选 status、search)
|
||||
func DBDistribution(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
statusFilter := c.Query("status")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
db := database.DB()
|
||||
var bindings []model.ReferralBinding
|
||||
q := db.Order("binding_date DESC").Limit(500)
|
||||
q := db.Model(&model.ReferralBinding{})
|
||||
if userId != "" {
|
||||
q = q.Where("referrer_id = ?", userId)
|
||||
}
|
||||
if err := q.Find(&bindings).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": []interface{}{}, "total": 0})
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
q = q.Where("status = ?", statusFilter)
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var bindings []model.ReferralBinding
|
||||
query := db.Model(&model.ReferralBinding{}).Order("binding_date DESC")
|
||||
if userId != "" {
|
||||
query = query.Where("referrer_id = ?", userId)
|
||||
}
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
query = query.Where("status = ?", statusFilter)
|
||||
}
|
||||
if err := query.Offset((page-1)*pageSize).Limit(pageSize).Find(&bindings).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": []interface{}{}, "total": 0, "page": page, "pageSize": pageSize, "totalPages": 0})
|
||||
return
|
||||
}
|
||||
referrerIds := make(map[string]bool)
|
||||
@@ -825,7 +904,14 @@ func DBDistribution(c *gin.Context) {
|
||||
"daysRemaining": days, "commission": commissionVal, "totalCommission": commissionVal, "source": "miniprogram",
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": out, "total": len(out)})
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "bindings": out,
|
||||
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||
})
|
||||
}
|
||||
|
||||
// DBChapters GET/POST /api/db/chapters
|
||||
|
||||
96
soul-api/internal/handler/match_records.go
Normal file
96
soul-api/internal/handler/match_records.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DBMatchRecordsList GET /api/db/match-records 管理端-匹配记录列表(分页、按类型筛选)
|
||||
func DBMatchRecordsList(c *gin.Context) {
|
||||
db := database.DB()
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
matchType := c.Query("matchType")
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
q := db.Model(&model.MatchRecord{})
|
||||
if matchType != "" {
|
||||
q = q.Where("match_type = ?", matchType)
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var records []model.MatchRecord
|
||||
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(), "records": []interface{}{}})
|
||||
return
|
||||
}
|
||||
|
||||
userIDs := make(map[string]bool)
|
||||
for _, r := range records {
|
||||
userIDs[r.UserID] = true
|
||||
userIDs[r.MatchedUserID] = true
|
||||
}
|
||||
ids := make([]string, 0, len(userIDs))
|
||||
for id := range userIDs {
|
||||
ids = append(ids, id)
|
||||
}
|
||||
var users []model.User
|
||||
if len(ids) > 0 {
|
||||
database.DB().Where("id IN ?", ids).Find(&users)
|
||||
}
|
||||
userMap := make(map[string]*model.User)
|
||||
for i := range users {
|
||||
userMap[users[i].ID] = &users[i]
|
||||
}
|
||||
|
||||
getStr := func(s *string) string {
|
||||
if s == nil {
|
||||
return ""
|
||||
}
|
||||
return *s
|
||||
}
|
||||
|
||||
out := make([]gin.H, 0, len(records))
|
||||
for _, r := range records {
|
||||
u := userMap[r.UserID]
|
||||
mu := userMap[r.MatchedUserID]
|
||||
userAvatar := ""
|
||||
matchedUserAvatar := ""
|
||||
if u != nil && u.Avatar != nil {
|
||||
userAvatar = *u.Avatar
|
||||
}
|
||||
if mu != nil && mu.Avatar != nil {
|
||||
matchedUserAvatar = *mu.Avatar
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": r.ID, "userId": r.UserID, "matchedUserId": r.MatchedUserID,
|
||||
"matchType": r.MatchType, "phone": getStr(r.Phone), "wechatId": getStr(r.WechatID),
|
||||
"userNickname": getStr(u.Nickname),
|
||||
"matchedNickname": getStr(mu.Nickname),
|
||||
"userAvatar": userAvatar,
|
||||
"matchedUserAvatar": matchedUserAvatar,
|
||||
"matchScore": r.MatchScore,
|
||||
"createdAt": r.CreatedAt,
|
||||
})
|
||||
}
|
||||
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "records": out,
|
||||
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||
})
|
||||
}
|
||||
@@ -738,3 +738,61 @@ func base64Encode(data []byte) string {
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// MiniprogramUsers GET /api/miniprogram/users 小程序-用户列表/单个(首页超级个体补充、会员详情回退)
|
||||
// 支持 ?limit=20 返回列表;?id=xxx 返回单个。返回 { success, data } 格式
|
||||
func MiniprogramUsers(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "20"))
|
||||
if limit < 1 || limit > 50 {
|
||||
limit = 20
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
if id != "" {
|
||||
var user model.User
|
||||
if err := db.Where("id = ?", id).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
|
||||
return
|
||||
}
|
||||
var cnt int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
|
||||
id, "paid", "fullbook", "vip").Count(&cnt)
|
||||
nick := getStringValue(user.Nickname)
|
||||
avatar := getStringValue(user.Avatar)
|
||||
contact := getStringValue(user.Phone)
|
||||
if contact == "" {
|
||||
contact = getStringValue(user.WechatID)
|
||||
}
|
||||
item := gin.H{
|
||||
"id": user.ID,
|
||||
"nickname": nick,
|
||||
"avatar": avatar,
|
||||
"vip_name": nick,
|
||||
"vip_avatar": avatar,
|
||||
"vip_contact": contact,
|
||||
"vip_project": "",
|
||||
"vip_bio": "",
|
||||
"is_vip": cnt > 0,
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": item})
|
||||
return
|
||||
}
|
||||
|
||||
var users []model.User
|
||||
db.Order("created_at DESC").Limit(limit).Find(&users)
|
||||
list := make([]gin.H, 0, len(users))
|
||||
for i := range users {
|
||||
u := &users[i]
|
||||
var cnt int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
|
||||
u.ID, "paid", "fullbook", "vip").Count(&cnt)
|
||||
list = append(list, gin.H{
|
||||
"id": u.ID,
|
||||
"nickname": getStringValue(u.Nickname),
|
||||
"avatar": getStringValue(u.Avatar),
|
||||
"is_vip": cnt > 0,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
@@ -3,6 +3,9 @@ package handler
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
@@ -10,16 +13,75 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// OrdersList GET /api/orders(带用户昵称/头像/手机号,分销佣金按配置比例计算)
|
||||
// OrdersList GET /api/orders(带用户昵称/头像/手机号,分销佣金按配置比例计算;支持分页 page、pageSize,筛选 status,搜索 search)
|
||||
func OrdersList(c *gin.Context) {
|
||||
db := database.DB()
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||||
statusFilter := c.Query("status")
|
||||
search := strings.TrimSpace(c.Query("search"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 100 {
|
||||
pageSize = 10
|
||||
}
|
||||
|
||||
q := db.Model(&model.Order{})
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
if statusFilter == "completed" {
|
||||
q = q.Where("status IN ?", []string{"paid", "completed"})
|
||||
} else {
|
||||
q = q.Where("status = ?", statusFilter)
|
||||
}
|
||||
}
|
||||
if search != "" {
|
||||
pattern := "%" + search + "%"
|
||||
q = q.Where("order_sn LIKE ? OR id LIKE ? OR user_id IN (SELECT id FROM users WHERE COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?)",
|
||||
pattern, pattern, pattern, pattern, pattern)
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
|
||||
var totalRevenue, todayRevenue float64
|
||||
db.Model(&model.Order{}).Select("COALESCE(SUM(amount), 0)").
|
||||
Where("status IN ?", []string{"paid", "completed"}).Scan(&totalRevenue)
|
||||
todayStart := time.Now().Truncate(24 * time.Hour)
|
||||
todayEnd := todayStart.Add(24 * time.Hour)
|
||||
db.Model(&model.Order{}).Select("COALESCE(SUM(amount), 0)").
|
||||
Where("status IN ? AND created_at >= ? AND created_at < ?", []string{"paid", "completed"}, todayStart, todayEnd).
|
||||
Scan(&todayRevenue)
|
||||
|
||||
var orders []model.Order
|
||||
if err := db.Order("created_at DESC").Find(&orders).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "orders": []interface{}{}})
|
||||
query := db.Model(&model.Order{})
|
||||
if statusFilter != "" && statusFilter != "all" {
|
||||
if statusFilter == "completed" {
|
||||
query = query.Where("status IN ?", []string{"paid", "completed"})
|
||||
} else {
|
||||
query = query.Where("status = ?", statusFilter)
|
||||
}
|
||||
}
|
||||
if search != "" {
|
||||
pattern := "%" + search + "%"
|
||||
query = query.Where("order_sn LIKE ? OR id LIKE ? OR user_id IN (SELECT id FROM users WHERE COALESCE(nickname,'') LIKE ? OR COALESCE(phone,'') LIKE ? OR id LIKE ?)",
|
||||
pattern, pattern, pattern, pattern, pattern)
|
||||
}
|
||||
if err := query.Order("created_at DESC").
|
||||
Offset((page - 1) * pageSize).
|
||||
Limit(pageSize).
|
||||
Find(&orders).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "orders": []interface{}{}, "total": 0})
|
||||
return
|
||||
}
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
if len(orders) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "orders": []interface{}{}})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "orders": []interface{}{},
|
||||
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -97,5 +159,47 @@ func OrdersList(c *gin.Context) {
|
||||
}
|
||||
out = append(out, m)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "orders": out})
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "orders": out,
|
||||
"total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||
"totalRevenue": totalRevenue, "todayRevenue": todayRevenue,
|
||||
})
|
||||
}
|
||||
|
||||
// MiniprogramOrders GET /api/miniprogram/orders 小程序-当前用户订单列表(按 userId 过滤,返回 data)
|
||||
func MiniprogramOrders(c *gin.Context) {
|
||||
userID := c.Query("userId")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var orders []model.Order
|
||||
if err := db.Where("user_id = ?", userID).Order("created_at DESC").Find(&orders).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
return
|
||||
}
|
||||
out := make([]gin.H, 0, len(orders))
|
||||
for _, o := range orders {
|
||||
desc := ""
|
||||
if o.Description != nil {
|
||||
desc = *o.Description
|
||||
}
|
||||
productID := ""
|
||||
if o.ProductID != nil {
|
||||
productID = *o.ProductID
|
||||
}
|
||||
status := "created"
|
||||
if o.Status != nil {
|
||||
status = *o.Status
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": o.ID, "order_sn": o.OrderSN, "user_id": o.UserID,
|
||||
"product_id": productID, "product_type": o.ProductType,
|
||||
"product_name": desc, "section_id": productID,
|
||||
"amount": o.Amount, "status": status,
|
||||
"created_at": o.CreatedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": out})
|
||||
}
|
||||
|
||||
189
soul-api/internal/handler/vip.go
Normal file
189
soul-api/internal/handler/vip.go
Normal file
@@ -0,0 +1,189 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// VipStatus GET /api/miniprogram/vip/status 小程序-查询用户 VIP 状态
|
||||
func VipStatus(c *gin.Context) {
|
||||
userID := c.Query("userId")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
// 查是否有 fullbook 或 vip 的已支付订单
|
||||
var order model.Order
|
||||
err := db.Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
|
||||
userID, "paid", "fullbook", "vip").
|
||||
Order("pay_time DESC").First(&order).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"isVip": false,
|
||||
"daysRemaining": 0,
|
||||
"expireDate": "",
|
||||
"price": float64(1980),
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
expireDate := time.Now().AddDate(0, 0, 365)
|
||||
daysRemaining := 365
|
||||
if order.PayTime != nil {
|
||||
expireDate = order.PayTime.AddDate(0, 0, 365)
|
||||
if expireDate.After(time.Now()) {
|
||||
daysRemaining = int(expireDate.Sub(time.Now()).Hours() / 24)
|
||||
} else {
|
||||
daysRemaining = 0
|
||||
}
|
||||
}
|
||||
expStr := expireDate.Format("2006-01-02")
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"isVip": true,
|
||||
"daysRemaining": daysRemaining,
|
||||
"expireDate": expStr,
|
||||
"price": float64(1980),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// VipProfileGet GET /api/miniprogram/vip/profile 小程序-获取 VIP 资料
|
||||
func VipProfileGet(c *gin.Context) {
|
||||
userID := c.Query("userId")
|
||||
if userID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
var user model.User
|
||||
if err := db.Where("id = ?", userID).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"name": "", "project": "", "contact": "", "bio": ""}})
|
||||
return
|
||||
}
|
||||
name := ""
|
||||
if user.Nickname != nil {
|
||||
name = *user.Nickname
|
||||
}
|
||||
contact := ""
|
||||
if user.Phone != nil {
|
||||
contact = *user.Phone
|
||||
}
|
||||
if user.WechatID != nil && contact == "" {
|
||||
contact = *user.WechatID
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"name": name,
|
||||
"project": "",
|
||||
"contact": contact,
|
||||
"bio": "",
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// VipProfilePost POST /api/miniprogram/vip/profile 小程序-更新 VIP 资料
|
||||
func VipProfilePost(c *gin.Context) {
|
||||
var req struct {
|
||||
UserID string `json:"userId" binding:"required"`
|
||||
Name string `json:"name"`
|
||||
Project string `json:"project"`
|
||||
Contact string `json:"contact"`
|
||||
Bio string `json:"bio"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
updates := map[string]interface{}{}
|
||||
if req.Name != "" {
|
||||
updates["nickname"] = req.Name
|
||||
}
|
||||
if req.Contact != "" {
|
||||
updates["phone"] = req.Contact
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.User{}).Where("id = ?", req.UserID).Updates(updates)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// VipMembers GET /api/miniprogram/vip/members 小程序-VIP 会员列表(无 id 返回列表;有 id 返回单个)
|
||||
func VipMembers(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
db := database.DB()
|
||||
|
||||
// 有 id 时查单个:优先从已购 fullbook/vip 的用户中找
|
||||
if id != "" {
|
||||
var user model.User
|
||||
if err := db.Where("id = ?", id).First(&user).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": nil})
|
||||
return
|
||||
}
|
||||
// 检查是否 VIP(有 fullbook 或 vip 订单)
|
||||
var cnt int64
|
||||
db.Model(&model.Order{}).Where("user_id = ? AND status = ? AND (product_type = ? OR product_type = ?)",
|
||||
id, "paid", "fullbook", "vip").Count(&cnt)
|
||||
item := formatVipMember(&user, cnt > 0)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": item})
|
||||
return
|
||||
}
|
||||
|
||||
// 无 id:返回 VIP 会员列表(有 fullbook 或 vip 订单的用户)
|
||||
var userIDs []string
|
||||
db.Model(&model.Order{}).Select("DISTINCT user_id").
|
||||
Where("status = ? AND (product_type = ? OR product_type = ?)", "paid", "fullbook", "vip").
|
||||
Pluck("user_id", &userIDs)
|
||||
if len(userIDs) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
return
|
||||
}
|
||||
var users []model.User
|
||||
db.Where("id IN ?", userIDs).Find(&users)
|
||||
list := make([]gin.H, 0, len(users))
|
||||
for i := range users {
|
||||
list = append(list, formatVipMember(&users[i], true))
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
name := ""
|
||||
if u.Nickname != nil {
|
||||
name = *u.Nickname
|
||||
}
|
||||
avatar := ""
|
||||
if u.Avatar != nil {
|
||||
avatar = *u.Avatar
|
||||
}
|
||||
contact := ""
|
||||
if u.Phone != nil {
|
||||
contact = *u.Phone
|
||||
}
|
||||
if u.WechatID != nil && contact == "" {
|
||||
contact = *u.WechatID
|
||||
}
|
||||
return gin.H{
|
||||
"id": u.ID,
|
||||
"nickname": name,
|
||||
"avatar": avatar,
|
||||
"vip_name": name,
|
||||
"vip_avatar": avatar,
|
||||
"vip_contact": contact,
|
||||
"is_vip": isVip,
|
||||
}
|
||||
}
|
||||
@@ -130,6 +130,7 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
db.PUT("/users", handler.DBUsersAction)
|
||||
db.DELETE("/users", handler.DBUsersDelete)
|
||||
db.GET("/users/referrals", handler.DBUsersReferrals)
|
||||
db.GET("/match-records", handler.DBMatchRecordsList)
|
||||
}
|
||||
|
||||
// ----- 分销 -----
|
||||
@@ -250,6 +251,14 @@ func Setup(cfg *config.Config) *gin.Engine {
|
||||
miniprogram.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm)
|
||||
miniprogram.POST("/withdraw/confirm-received", handler.WithdrawConfirmReceived)
|
||||
miniprogram.GET("/withdraw/confirm-info", handler.WithdrawConfirmInfo)
|
||||
// VIP 接口(小程序专用,按使用方区分路径)
|
||||
miniprogram.GET("/vip/status", handler.VipStatus)
|
||||
miniprogram.GET("/vip/profile", handler.VipProfileGet)
|
||||
miniprogram.POST("/vip/profile", handler.VipProfilePost)
|
||||
miniprogram.GET("/vip/members", handler.VipMembers)
|
||||
// 用户列表/单个(首页超级个体、会员详情回退)
|
||||
miniprogram.GET("/users", handler.MiniprogramUsers)
|
||||
miniprogram.GET("/orders", handler.MiniprogramOrders)
|
||||
}
|
||||
|
||||
// ----- 提现 -----
|
||||
|
||||
55
soul-api/开发文档/miniprogram接口补全说明.md
Normal file
55
soul-api/开发文档/miniprogram接口补全说明.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# soul-api 小程序接口补全说明
|
||||
|
||||
## 变更背景
|
||||
|
||||
miniprogram 功能还原后,需将 VIP 相关接口从 `/api/vip/*` 迁移至 `/api/miniprogram/vip/*`,并补充 miniprogram 组下的 users 接口,符合项目边界(小程序只调 `/api/miniprogram/*`)。
|
||||
|
||||
## 新增接口
|
||||
|
||||
### 1. VIP 接口(handler/vip.go)
|
||||
|
||||
| 路径 | 方法 | Handler | 用途 |
|
||||
|------|------|---------|------|
|
||||
| `/api/miniprogram/vip/status` | GET | VipStatus | 查询用户 VIP 状态 |
|
||||
| `/api/miniprogram/vip/profile` | GET | VipProfileGet | 获取 VIP 资料 |
|
||||
| `/api/miniprogram/vip/profile` | POST | VipProfilePost | 更新 VIP 资料 |
|
||||
| `/api/miniprogram/vip/members` | GET | VipMembers | VIP 会员列表或单个 |
|
||||
|
||||
**实现说明**:
|
||||
- **status**:按 orders 表查 `product_type IN ('fullbook','vip')` 且 `status='paid'` 判断是否 VIP;返回 `isVip`、`daysRemaining`、`expireDate`、`price`
|
||||
- **profile**:GET 从 users 表读 nickname、phone;POST 更新 nickname、phone
|
||||
- **members**:无 `?id` 时返回有 fullbook/vip 订单的用户列表;有 `?id` 时返回单个用户,含 `vip_name`、`vip_avatar`、`vip_contact`、`is_vip` 等字段
|
||||
|
||||
### 2. 用户接口(handler/miniprogram.go)
|
||||
|
||||
| 路径 | 方法 | Handler | 用途 |
|
||||
|------|------|---------|------|
|
||||
| `/api/miniprogram/users` | GET | MiniprogramUsers | 用户列表或单个 |
|
||||
|
||||
**实现说明**:
|
||||
- `?limit=20`:返回用户列表,用于首页「超级个体」不足 4 人时的补充
|
||||
- `?id=xxx`:返回单个用户,用于会员详情页在 vip/members 失败时的回退
|
||||
- 返回格式:`{ success, data }`,与 miniprogram 期望一致
|
||||
|
||||
## 已有接口(无需变更)
|
||||
|
||||
以下接口已在 miniprogram 组挂载,miniprogram 已正确调用:
|
||||
|
||||
- `/api/miniprogram/book/all-chapters`
|
||||
- `/api/miniprogram/book/chapter/:id`
|
||||
- `/api/miniprogram/book/chapter/by-mid/:mid`
|
||||
- `/api/miniprogram/book/hot`
|
||||
- `/api/miniprogram/book/search`
|
||||
- `/api/miniprogram/book/stats`
|
||||
|
||||
## 路由注册位置
|
||||
|
||||
`internal/router/router.go` 中 miniprogram 组末尾:
|
||||
|
||||
```go
|
||||
miniprogram.GET("/vip/status", handler.VipStatus)
|
||||
miniprogram.GET("/vip/profile", handler.VipProfileGet)
|
||||
miniprogram.POST("/vip/profile", handler.VipProfilePost)
|
||||
miniprogram.GET("/vip/members", handler.VipMembers)
|
||||
miniprogram.GET("/users", handler.MiniprogramUsers)
|
||||
```
|
||||
Reference in New Issue
Block a user