更新管理后台布局,优化菜单项标签,新增支付配置项。同时,调整API响应字段命名,确保一致性,提升代码可读性和维护性。
This commit is contained in:
42
soul-api/internal/config/config.go
Normal file
42
soul-api/internal/config/config.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
)
|
||||
|
||||
// Config 应用配置(从环境变量读取)
|
||||
type Config struct {
|
||||
Port string
|
||||
Mode string
|
||||
DBDSN string
|
||||
TrustedProxies []string
|
||||
CORSOrigins []string
|
||||
}
|
||||
|
||||
// Load 加载配置,开发环境可读 .env
|
||||
func Load() (*Config, error) {
|
||||
_ = godotenv.Load()
|
||||
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "8080"
|
||||
}
|
||||
mode := os.Getenv("GIN_MODE")
|
||||
if mode == "" {
|
||||
mode = "debug"
|
||||
}
|
||||
dsn := os.Getenv("DB_DSN")
|
||||
if dsn == "" {
|
||||
dsn = "user:pass@tcp(127.0.0.1:3306)/soul?charset=utf8mb4&parseTime=True"
|
||||
}
|
||||
|
||||
return &Config{
|
||||
Port: port,
|
||||
Mode: mode,
|
||||
DBDSN: dsn,
|
||||
TrustedProxies: []string{"127.0.0.1", "::1"},
|
||||
CORSOrigins: []string{"http://localhost:5174", "http://127.0.0.1:5174", "https://soul.quwanzhi.com"},
|
||||
}, nil
|
||||
}
|
||||
26
soul-api/internal/database/database.go
Normal file
26
soul-api/internal/database/database.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"log"
|
||||
|
||||
"gorm.io/driver/mysql"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
||||
// Init 使用 DSN 连接 MySQL,供 handler 通过 DB() 使用
|
||||
func Init(dsn string) error {
|
||||
var err error
|
||||
db, err = gorm.Open(mysql.Open(dsn), &gorm.Config{})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println("database: connected")
|
||||
return nil
|
||||
}
|
||||
|
||||
// DB 返回全局 *gorm.DB,仅在 Init 成功后调用
|
||||
func DB() *gorm.DB {
|
||||
return db
|
||||
}
|
||||
33
soul-api/internal/handler/admin.go
Normal file
33
soul-api/internal/handler/admin.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminCheck GET /api/admin 鉴权检查
|
||||
func AdminCheck(c *gin.Context) {
|
||||
// TODO: 校验 session/token,返回 success: true 或 401
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// AdminLogin POST /api/admin 登录
|
||||
func AdminLogin(c *gin.Context) {
|
||||
var body struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "参数错误"})
|
||||
return
|
||||
}
|
||||
// TODO: 校验用户名密码,写 session,返回 success
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// AdminLogout POST /api/admin/logout
|
||||
func AdminLogout(c *gin.Context) {
|
||||
// TODO: 清除 session
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
101
soul-api/internal/handler/admin_chapters.go
Normal file
101
soul-api/internal/handler/admin_chapters.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminChaptersList GET /api/admin/chapters 从 chapters 表组树:part -> chapters -> sections
|
||||
func AdminChaptersList(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"structure": []interface{}{}, "stats": nil}})
|
||||
return
|
||||
}
|
||||
type section struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Price float64 `json:"price"`
|
||||
IsFree bool `json:"isFree"`
|
||||
Status string `json:"status"`
|
||||
}
|
||||
type chapter struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Sections []section `json:"sections"`
|
||||
}
|
||||
type part struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Type string `json:"type"`
|
||||
Chapters []chapter `json:"chapters"`
|
||||
}
|
||||
partMap := make(map[string]*part)
|
||||
chapterMap := make(map[string]map[string]*chapter)
|
||||
for _, row := range list {
|
||||
if partMap[row.PartID] == nil {
|
||||
partMap[row.PartID] = &part{ID: row.PartID, Title: row.PartTitle, Type: "part", Chapters: []chapter{}}
|
||||
chapterMap[row.PartID] = make(map[string]*chapter)
|
||||
}
|
||||
p := partMap[row.PartID]
|
||||
if chapterMap[row.PartID][row.ChapterID] == nil {
|
||||
ch := chapter{ID: row.ChapterID, Title: row.ChapterTitle, Sections: []section{}}
|
||||
p.Chapters = append(p.Chapters, ch)
|
||||
chapterMap[row.PartID][row.ChapterID] = &p.Chapters[len(p.Chapters)-1]
|
||||
}
|
||||
ch := chapterMap[row.PartID][row.ChapterID]
|
||||
price := 1.0
|
||||
if row.Price != nil {
|
||||
price = *row.Price
|
||||
}
|
||||
isFree := false
|
||||
if row.IsFree != nil {
|
||||
isFree = *row.IsFree
|
||||
}
|
||||
st := "published"
|
||||
if row.Status != nil {
|
||||
st = *row.Status
|
||||
}
|
||||
ch.Sections = append(ch.Sections, section{ID: row.ID, Title: row.SectionTitle, Price: price, IsFree: isFree, Status: st})
|
||||
}
|
||||
structure := make([]part, 0, len(partMap))
|
||||
for _, p := range partMap {
|
||||
structure = append(structure, *p)
|
||||
}
|
||||
var total int64
|
||||
database.DB().Model(&model.Chapter{}).Count(&total)
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{"structure": structure, "stats": gin.H{"totalSections": total}},
|
||||
})
|
||||
}
|
||||
|
||||
// AdminChaptersAction POST/PUT/DELETE /api/admin/chapters
|
||||
func AdminChaptersAction(c *gin.Context) {
|
||||
var body struct {
|
||||
Action string `json:"action"`
|
||||
ID string `json:"id"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
Status *string `json:"status"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
if body.Action == "updatePrice" && body.ID != "" && body.Price != nil {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("price", *body.Price)
|
||||
}
|
||||
if body.Action == "toggleFree" && body.ID != "" && body.IsFree != nil {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("is_free", *body.IsFree)
|
||||
}
|
||||
if body.Action == "updateStatus" && body.ID != "" && body.Status != nil {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Update("status", *body.Status)
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
99
soul-api/internal/handler/admin_distribution.go
Normal file
99
soul-api/internal/handler/admin_distribution.go
Normal file
@@ -0,0 +1,99 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminDistributionOverview GET /api/admin/distribution/overview(全部使用 GORM,无 Raw SQL)
|
||||
func AdminDistributionOverview(c *gin.Context) {
|
||||
now := time.Now()
|
||||
todayStart := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, now.Location())
|
||||
todayEnd := todayStart.Add(24 * time.Hour)
|
||||
monthStart := time.Date(now.Year(), now.Month(), 1, 0, 0, 0, 0, now.Location())
|
||||
db := database.DB()
|
||||
overview := gin.H{
|
||||
"todayClicks": 0, "todayBindings": 0, "todayConversions": 0, "todayEarnings": 0,
|
||||
"monthClicks": 0, "monthBindings": 0, "monthConversions": 0, "monthEarnings": 0,
|
||||
"totalClicks": 0, "totalBindings": 0, "totalConversions": 0, "totalEarnings": 0,
|
||||
"expiringBindings": 0, "pendingWithdrawals": 0, "pendingWithdrawAmount": 0,
|
||||
"conversionRate": "0.00", "totalDistributors": 0, "activeDistributors": 0,
|
||||
}
|
||||
|
||||
// 订单:仅用 Where + Count / Select(Sum) 参数化
|
||||
var totalOrders int64
|
||||
db.Model(&model.Order{}).Where("status = ?", "paid").Count(&totalOrders)
|
||||
var totalAmount float64
|
||||
db.Model(&model.Order{}).Where("status = ?", "paid").Select("COALESCE(SUM(amount),0)").Scan(&totalAmount)
|
||||
var todayOrders int64
|
||||
db.Model(&model.Order{}).Where("status = ? AND created_at >= ? AND created_at < ?", "paid", todayStart, todayEnd).Count(&todayOrders)
|
||||
var todayAmount float64
|
||||
db.Model(&model.Order{}).Where("status = ? AND created_at >= ? AND created_at < ?", "paid", todayStart, todayEnd).Select("COALESCE(SUM(amount),0)").Scan(&todayAmount)
|
||||
var monthOrders int64
|
||||
db.Model(&model.Order{}).Where("status = ? AND created_at >= ?", "paid", monthStart).Count(&monthOrders)
|
||||
var monthAmount float64
|
||||
db.Model(&model.Order{}).Where("status = ? AND created_at >= ?", "paid", monthStart).Select("COALESCE(SUM(amount),0)").Scan(&monthAmount)
|
||||
overview["totalEarnings"] = totalAmount
|
||||
overview["todayEarnings"] = todayAmount
|
||||
overview["monthEarnings"] = monthAmount
|
||||
|
||||
// 绑定:全部 GORM Where
|
||||
var totalBindings int64
|
||||
db.Model(&model.ReferralBinding{}).Count(&totalBindings)
|
||||
var converted int64
|
||||
db.Model(&model.ReferralBinding{}).Where("status = ?", "converted").Count(&converted)
|
||||
var todayBindings int64
|
||||
db.Model(&model.ReferralBinding{}).Where("binding_date >= ? AND binding_date < ?", todayStart, todayEnd).Count(&todayBindings)
|
||||
var todayConv int64
|
||||
db.Model(&model.ReferralBinding{}).Where("status = ? AND binding_date >= ? AND binding_date < ?", "converted", todayStart, todayEnd).Count(&todayConv)
|
||||
var monthBindings int64
|
||||
db.Model(&model.ReferralBinding{}).Where("binding_date >= ?", monthStart).Count(&monthBindings)
|
||||
var monthConv int64
|
||||
db.Model(&model.ReferralBinding{}).Where("status = ? AND binding_date >= ?", "converted", monthStart).Count(&monthConv)
|
||||
expiringEnd := now.Add(7 * 24 * time.Hour)
|
||||
var expiring int64
|
||||
db.Model(&model.ReferralBinding{}).Where("status = ? AND expiry_date > ? AND expiry_date <= ?", "active", now, expiringEnd).Count(&expiring)
|
||||
overview["totalBindings"] = totalBindings
|
||||
overview["totalConversions"] = converted
|
||||
overview["todayBindings"] = todayBindings
|
||||
overview["todayConversions"] = todayConv
|
||||
overview["monthBindings"] = monthBindings
|
||||
overview["monthConversions"] = monthConv
|
||||
overview["expiringBindings"] = expiring
|
||||
|
||||
// 访问数
|
||||
var visitTotal int64
|
||||
db.Model(&model.ReferralVisit{}).Count(&visitTotal)
|
||||
overview["totalClicks"] = visitTotal
|
||||
if visitTotal > 0 && converted > 0 {
|
||||
overview["conversionRate"] = formatPercent(float64(converted)/float64(visitTotal)*100)
|
||||
}
|
||||
|
||||
// 提现待处理
|
||||
var pendCount int64
|
||||
db.Model(&model.Withdrawal{}).Where("status = ?", "pending").Count(&pendCount)
|
||||
var pendSum float64
|
||||
db.Model(&model.Withdrawal{}).Where("status = ?", "pending").Select("COALESCE(SUM(amount),0)").Scan(&pendSum)
|
||||
overview["pendingWithdrawals"] = pendCount
|
||||
overview["pendingWithdrawAmount"] = pendSum
|
||||
|
||||
// 分销商
|
||||
var distTotal int64
|
||||
db.Model(&model.User{}).Where("referral_code IS NOT NULL AND referral_code != ?", "").Count(&distTotal)
|
||||
var distActive int64
|
||||
db.Model(&model.User{}).Where("referral_code IS NOT NULL AND referral_code != ? AND earnings > ?", "", 0).Count(&distActive)
|
||||
overview["totalDistributors"] = distTotal
|
||||
overview["activeDistributors"] = distActive
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "overview": overview})
|
||||
}
|
||||
|
||||
func formatPercent(v float64) string {
|
||||
return fmt.Sprintf("%.2f", v) + "%"
|
||||
}
|
||||
22
soul-api/internal/handler/admin_extra.go
Normal file
22
soul-api/internal/handler/admin_extra.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminContent GET/POST/PUT/DELETE /api/admin/content
|
||||
func AdminContent(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// AdminPayment GET/POST/PUT/DELETE /api/admin/payment
|
||||
func AdminPayment(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// AdminReferral GET/POST/PUT/DELETE /api/admin/referral
|
||||
func AdminReferral(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
119
soul-api/internal/handler/admin_withdrawals.go
Normal file
119
soul-api/internal/handler/admin_withdrawals.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminWithdrawalsList GET /api/admin/withdrawals
|
||||
func AdminWithdrawalsList(c *gin.Context) {
|
||||
statusFilter := c.Query("status")
|
||||
var list []model.Withdrawal
|
||||
q := database.DB().Order("created_at DESC").Limit(100)
|
||||
if statusFilter != "" {
|
||||
q = q.Where("status = ?", statusFilter)
|
||||
}
|
||||
if err := q.Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "withdrawals": []interface{}{}, "stats": gin.H{"total": 0}})
|
||||
return
|
||||
}
|
||||
userIds := make([]string, 0, len(list))
|
||||
seen := make(map[string]bool)
|
||||
for _, w := range list {
|
||||
if !seen[w.UserID] {
|
||||
seen[w.UserID] = true
|
||||
userIds = append(userIds, w.UserID)
|
||||
}
|
||||
}
|
||||
var users []model.User
|
||||
if len(userIds) > 0 {
|
||||
database.DB().Where("id IN ?", userIds).Find(&users)
|
||||
}
|
||||
userMap := make(map[string]*model.User)
|
||||
for i := range users {
|
||||
userMap[users[i].ID] = &users[i]
|
||||
}
|
||||
withdrawals := make([]gin.H, 0, len(list))
|
||||
for _, w := range list {
|
||||
u := userMap[w.UserID]
|
||||
userName := "未知用户"
|
||||
var userAvatar *string
|
||||
account := "未绑定微信号"
|
||||
if w.WechatID != nil && *w.WechatID != "" {
|
||||
account = *w.WechatID
|
||||
}
|
||||
if u != nil {
|
||||
if u.Nickname != nil {
|
||||
userName = *u.Nickname
|
||||
}
|
||||
userAvatar = u.Avatar
|
||||
if u.WechatID != nil && *u.WechatID != "" {
|
||||
account = *u.WechatID
|
||||
}
|
||||
}
|
||||
st := "pending"
|
||||
if w.Status != nil {
|
||||
st = *w.Status
|
||||
if st == "success" {
|
||||
st = "completed"
|
||||
} else if st == "failed" {
|
||||
st = "rejected"
|
||||
} else if st == "pending_confirm" {
|
||||
st = "pending_confirm"
|
||||
}
|
||||
}
|
||||
withdrawals = append(withdrawals, gin.H{
|
||||
"id": w.ID, "userId": w.UserID, "userName": userName, "userAvatar": userAvatar,
|
||||
"amount": w.Amount, "status": st, "createdAt": w.CreatedAt,
|
||||
"method": "wechat", "account": account,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "withdrawals": withdrawals, "stats": gin.H{"total": len(withdrawals)}})
|
||||
}
|
||||
|
||||
// AdminWithdrawalsAction PUT /api/admin/withdrawals 审核/打款
|
||||
func AdminWithdrawalsAction(c *gin.Context) {
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
Action string `json:"action"`
|
||||
ErrorMessage string `json:"errorMessage"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
|
||||
return
|
||||
}
|
||||
reason := body.ErrorMessage
|
||||
if reason == "" {
|
||||
reason = body.Reason
|
||||
}
|
||||
if reason == "" && body.Action == "reject" {
|
||||
reason = "管理员拒绝"
|
||||
}
|
||||
var newStatus string
|
||||
switch body.Action {
|
||||
case "approve":
|
||||
newStatus = "success"
|
||||
case "reject":
|
||||
newStatus = "failed"
|
||||
default:
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "action 须为 approve 或 reject"})
|
||||
return
|
||||
}
|
||||
now := time.Now()
|
||||
err := database.DB().Model(&model.Withdrawal{}).Where("id = ?", body.ID).Updates(map[string]interface{}{
|
||||
"status": newStatus,
|
||||
"error_message": reason,
|
||||
"processed_at": now,
|
||||
}).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "操作成功"})
|
||||
}
|
||||
17
soul-api/internal/handler/auth.go
Normal file
17
soul-api/internal/handler/auth.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AuthLogin POST /api/auth/login
|
||||
func AuthLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// AuthResetPassword POST /api/auth/reset-password
|
||||
func AuthResetPassword(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
160
soul-api/internal/handler/book.go
Normal file
160
soul-api/internal/handler/book.go
Normal file
@@ -0,0 +1,160 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// BookAllChapters GET /api/book/all-chapters 返回所有章节(列表,来自 chapters 表)
|
||||
func BookAllChapters(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
// BookChapterByID GET /api/book/chapter/:id
|
||||
func BookChapterByID(c *gin.Context) {
|
||||
id := c.Param("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
var ch model.Chapter
|
||||
if err := database.DB().Where("id = ?", id).First(&ch).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusNotFound, gin.H{"success": false, "error": "章节不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": ch})
|
||||
}
|
||||
|
||||
// BookChapters GET/POST/PUT/DELETE /api/book/chapters(与 app/api/book/chapters 一致,用 GORM)
|
||||
func BookChapters(c *gin.Context) {
|
||||
db := database.DB()
|
||||
switch c.Request.Method {
|
||||
case http.MethodGet:
|
||||
partId := c.Query("partId")
|
||||
status := c.Query("status")
|
||||
if status == "" {
|
||||
status = "published"
|
||||
}
|
||||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "100"))
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
if pageSize < 1 || pageSize > 500 {
|
||||
pageSize = 100
|
||||
}
|
||||
q := db.Model(&model.Chapter{})
|
||||
if partId != "" {
|
||||
q = q.Where("part_id = ?", partId)
|
||||
}
|
||||
if status != "" && status != "all" {
|
||||
q = q.Where("status = ?", status)
|
||||
}
|
||||
var total int64
|
||||
q.Count(&total)
|
||||
var list []model.Chapter
|
||||
q.Order("sort_order ASC, id ASC").Offset((page - 1) * pageSize).Limit(pageSize).Find(&list)
|
||||
totalPages := int(total) / pageSize
|
||||
if int(total)%pageSize > 0 {
|
||||
totalPages++
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{
|
||||
"list": list, "total": total, "page": page, "pageSize": pageSize, "totalPages": totalPages,
|
||||
},
|
||||
})
|
||||
return
|
||||
case http.MethodPost:
|
||||
var body model.Chapter
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
if body.ID == "" || body.PartID == "" || body.ChapterID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少必要字段 id/partId/chapterId"})
|
||||
return
|
||||
}
|
||||
if err := db.Create(&body).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": body})
|
||||
return
|
||||
case http.MethodPut:
|
||||
var body model.Chapter
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
if err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(map[string]interface{}{
|
||||
"part_title": body.PartTitle, "chapter_title": body.ChapterTitle, "section_title": body.SectionTitle,
|
||||
"content": body.Content, "word_count": body.WordCount, "is_free": body.IsFree, "price": body.Price,
|
||||
"sort_order": body.SortOrder, "status": body.Status,
|
||||
}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
return
|
||||
case http.MethodDelete:
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
if err := db.Where("id = ?", id).Delete(&model.Chapter{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||
}
|
||||
|
||||
// BookHot GET /api/book/hot
|
||||
func BookHot(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
database.DB().Order("sort_order ASC, id ASC").Limit(10).Find(&list)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
// BookLatestChapters GET /api/book/latest-chapters
|
||||
func BookLatestChapters(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
database.DB().Order("updated_at DESC, id ASC").Limit(20).Find(&list)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
// BookSearch GET /api/book/search 同 /api/search,由 SearchGet 处理
|
||||
func BookSearch(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
}
|
||||
|
||||
// BookStats GET /api/book/stats
|
||||
func BookStats(c *gin.Context) {
|
||||
var total int64
|
||||
database.DB().Model(&model.Chapter{}).Count(&total)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"totalChapters": total}})
|
||||
}
|
||||
|
||||
// BookSync GET/POST /api/book/sync
|
||||
func BookSync(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步由 DB 维护"})
|
||||
}
|
||||
22
soul-api/internal/handler/ckb.go
Normal file
22
soul-api/internal/handler/ckb.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CKBJoin POST /api/ckb/join
|
||||
func CKBJoin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// CKBMatch POST /api/ckb/match
|
||||
func CKBMatch(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// CKBSync GET/POST /api/ckb/sync
|
||||
func CKBSync(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
63
soul-api/internal/handler/config.go
Normal file
63
soul-api/internal/handler/config.go
Normal file
@@ -0,0 +1,63 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// GetConfig GET /api/config 从 system_config 读取并合并(与 app/api/config 结构一致)
|
||||
func GetConfig(c *gin.Context) {
|
||||
var list []model.SystemConfig
|
||||
if err := database.DB().Order("config_key ASC").Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "paymentMethods": gin.H{}, "liveQRCodes": []interface{}{},
|
||||
"siteConfig": gin.H{}, "menuConfig": gin.H{}, "pageConfig": gin.H{},
|
||||
})
|
||||
return
|
||||
}
|
||||
out := gin.H{
|
||||
"success": true, "paymentMethods": gin.H{}, "liveQRCodes": []interface{}{},
|
||||
"siteConfig": gin.H{}, "menuConfig": gin.H{}, "pageConfig": gin.H{},
|
||||
"authorInfo": gin.H{}, "marketing": gin.H{}, "system": gin.H{},
|
||||
}
|
||||
for _, row := range list {
|
||||
var val interface{}
|
||||
_ = json.Unmarshal(row.ConfigValue, &val)
|
||||
switch row.ConfigKey {
|
||||
case "site_config", "siteConfig":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["siteConfig"] = m
|
||||
}
|
||||
case "menu_config", "menuConfig":
|
||||
out["menuConfig"] = val
|
||||
case "page_config", "pageConfig":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["pageConfig"] = m
|
||||
}
|
||||
case "payment_methods", "paymentMethods":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["paymentMethods"] = m
|
||||
}
|
||||
case "live_qr_codes", "liveQRCodes":
|
||||
out["liveQRCodes"] = val
|
||||
case "author_info", "authorInfo":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["authorInfo"] = m
|
||||
}
|
||||
case "marketing":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["marketing"] = m
|
||||
}
|
||||
case "system":
|
||||
if m, ok := val.(map[string]interface{}); ok {
|
||||
out["system"] = m
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
12
soul-api/internal/handler/content.go
Normal file
12
soul-api/internal/handler/content.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ContentGet GET /api/content
|
||||
func ContentGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
17
soul-api/internal/handler/cron.go
Normal file
17
soul-api/internal/handler/cron.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// CronSyncOrders GET/POST /api/cron/sync-orders
|
||||
func CronSyncOrders(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// CronUnbindExpired GET/POST /api/cron/unbind-expired
|
||||
func CronUnbindExpired(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
391
soul-api/internal/handler/db.go
Normal file
391
soul-api/internal/handler/db.go
Normal file
@@ -0,0 +1,391 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DBConfigGet GET /api/db/config
|
||||
func DBConfigGet(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
db := database.DB()
|
||||
var list []model.SystemConfig
|
||||
q := db.Table("system_config")
|
||||
if key != "" {
|
||||
q = q.Where("config_key = ?", key)
|
||||
}
|
||||
if err := q.Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
if key != "" && len(list) == 1 {
|
||||
var val interface{}
|
||||
_ = json.Unmarshal(list[0].ConfigValue, &val)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": val})
|
||||
return
|
||||
}
|
||||
data := make([]gin.H, 0, len(list))
|
||||
for _, row := range list {
|
||||
var val interface{}
|
||||
_ = json.Unmarshal(row.ConfigValue, &val)
|
||||
data = append(data, gin.H{"configKey": row.ConfigKey, "configValue": val})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
||||
}
|
||||
|
||||
// DBConfigPost POST /api/db/config
|
||||
func DBConfigPost(c *gin.Context) {
|
||||
var body struct {
|
||||
Key string `json:"key"`
|
||||
Value interface{} `json:"value"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.Key == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "配置键不能为空"})
|
||||
return
|
||||
}
|
||||
valBytes, err := json.Marshal(body.Value)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
desc := body.Description
|
||||
var row model.SystemConfig
|
||||
err = db.Where("config_key = ?", body.Key).First(&row).Error
|
||||
if err != nil {
|
||||
row = model.SystemConfig{ConfigKey: body.Key, ConfigValue: valBytes, Description: &desc}
|
||||
err = db.Create(&row).Error
|
||||
} else {
|
||||
row.ConfigValue = valBytes
|
||||
if body.Description != "" {
|
||||
row.Description = &desc
|
||||
}
|
||||
err = db.Save(&row).Error
|
||||
}
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "配置保存成功"})
|
||||
}
|
||||
|
||||
// DBUsersList GET /api/db/users
|
||||
func DBUsersList(c *gin.Context) {
|
||||
var users []model.User
|
||||
if err := database.DB().Find(&users).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "users": []interface{}{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "users": users})
|
||||
}
|
||||
|
||||
// DBUsersAction POST /api/db/users(创建)、PUT /api/db/users(更新)
|
||||
func DBUsersAction(c *gin.Context) {
|
||||
db := database.DB()
|
||||
if c.Request.Method == http.MethodPost {
|
||||
var body struct {
|
||||
OpenID *string `json:"openId"`
|
||||
Phone *string `json:"phone"`
|
||||
Nickname *string `json:"nickname"`
|
||||
WechatID *string `json:"wechatId"`
|
||||
Avatar *string `json:"avatar"`
|
||||
IsAdmin *bool `json:"isAdmin"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
userID := "user_" + randomSuffix()
|
||||
code := "SOUL" + randomSuffix()[:4]
|
||||
nick := "用户"
|
||||
if body.Nickname != nil && *body.Nickname != "" {
|
||||
nick = *body.Nickname
|
||||
} else {
|
||||
nick = nick + userID[len(userID)-4:]
|
||||
}
|
||||
u := model.User{
|
||||
ID: userID, Nickname: &nick, ReferralCode: &code,
|
||||
OpenID: body.OpenID, Phone: body.Phone, WechatID: body.WechatID, Avatar: body.Avatar,
|
||||
}
|
||||
if body.IsAdmin != nil {
|
||||
u.IsAdmin = body.IsAdmin
|
||||
}
|
||||
if err := db.Create(&u).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "user": u, "isNew": true, "message": "用户创建成功"})
|
||||
return
|
||||
}
|
||||
// PUT 更新
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
Nickname *string `json:"nickname"`
|
||||
Phone *string `json:"phone"`
|
||||
WechatID *string `json:"wechatId"`
|
||||
Avatar *string `json:"avatar"`
|
||||
HasFullBook *bool `json:"hasFullBook"`
|
||||
IsAdmin *bool `json:"isAdmin"`
|
||||
Earnings *float64 `json:"earnings"`
|
||||
PendingEarnings *float64 `json:"pendingEarnings"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户ID不能为空"})
|
||||
return
|
||||
}
|
||||
updates := map[string]interface{}{}
|
||||
if body.Nickname != nil {
|
||||
updates["nickname"] = *body.Nickname
|
||||
}
|
||||
if body.Phone != nil {
|
||||
updates["phone"] = *body.Phone
|
||||
}
|
||||
if body.WechatID != nil {
|
||||
updates["wechat_id"] = *body.WechatID
|
||||
}
|
||||
if body.Avatar != nil {
|
||||
updates["avatar"] = *body.Avatar
|
||||
}
|
||||
if body.HasFullBook != nil {
|
||||
updates["has_full_book"] = *body.HasFullBook
|
||||
}
|
||||
if body.IsAdmin != nil {
|
||||
updates["is_admin"] = *body.IsAdmin
|
||||
}
|
||||
if body.Earnings != nil {
|
||||
updates["earnings"] = *body.Earnings
|
||||
}
|
||||
if body.PendingEarnings != nil {
|
||||
updates["pending_earnings"] = *body.PendingEarnings
|
||||
}
|
||||
if len(updates) == 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "没有需要更新的字段"})
|
||||
return
|
||||
}
|
||||
if err := db.Model(&model.User{}).Where("id = ?", body.ID).Updates(updates).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户更新成功"})
|
||||
}
|
||||
|
||||
func randomSuffix() string {
|
||||
return fmt.Sprintf("%d%x", time.Now().UnixNano()%100000, time.Now().UnixNano()&0xfff)
|
||||
}
|
||||
|
||||
// DBUsersDelete DELETE /api/db/users
|
||||
func DBUsersDelete(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户ID不能为空"})
|
||||
return
|
||||
}
|
||||
if err := database.DB().Where("id = ?", id).Delete(&model.User{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "用户删除成功"})
|
||||
}
|
||||
|
||||
// DBUsersReferrals GET /api/db/users/referrals
|
||||
func DBUsersReferrals(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
if userId == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
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
|
||||
}
|
||||
refereeIds := make([]string, 0, len(bindings))
|
||||
for _, b := range bindings {
|
||||
refereeIds = append(refereeIds, b.RefereeID)
|
||||
}
|
||||
var users []model.User
|
||||
if len(refereeIds) > 0 {
|
||||
db.Where("id IN ?", refereeIds).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))
|
||||
for _, b := range bindings {
|
||||
u := userMap[b.RefereeID]
|
||||
nick := "微信用户"
|
||||
var avatar *string
|
||||
var phone *string
|
||||
hasFullBook := false
|
||||
if u != nil {
|
||||
if u.Nickname != nil {
|
||||
nick = *u.Nickname
|
||||
}
|
||||
avatar, phone = u.Avatar, u.Phone
|
||||
if u.HasFullBook != nil {
|
||||
hasFullBook = *u.HasFullBook
|
||||
}
|
||||
}
|
||||
status := "active"
|
||||
if b.Status != nil {
|
||||
status = *b.Status
|
||||
}
|
||||
daysRemaining := 0
|
||||
if b.ExpiryDate.After(time.Now()) {
|
||||
daysRemaining = int(b.ExpiryDate.Sub(time.Now()).Hours() / 24)
|
||||
}
|
||||
referrals = append(referrals, gin.H{
|
||||
"id": b.RefereeID, "nickname": nick, "avatar": avatar, "phone": phone,
|
||||
"hasFullBook": hasFullBook || status == "converted",
|
||||
"createdAt": b.BindingDate, "bindingStatus": status, "daysRemaining": daysRemaining, "commission": b.CommissionAmount,
|
||||
"status": status,
|
||||
})
|
||||
}
|
||||
var referrer model.User
|
||||
earningsE, pendingE, withdrawnE := 0.0, 0.0, 0.0
|
||||
if err := db.Where("id = ?", userId).Select("earnings", "pending_earnings", "withdrawn_earnings").First(&referrer).Error; err == nil {
|
||||
if referrer.Earnings != nil {
|
||||
earningsE = *referrer.Earnings
|
||||
}
|
||||
if referrer.PendingEarnings != nil {
|
||||
pendingE = *referrer.PendingEarnings
|
||||
}
|
||||
if referrer.WithdrawnEarnings != nil {
|
||||
withdrawnE = *referrer.WithdrawnEarnings
|
||||
}
|
||||
}
|
||||
purchased := 0
|
||||
for _, b := range bindings {
|
||||
u := userMap[b.RefereeID]
|
||||
if (u != nil && u.HasFullBook != nil && *u.HasFullBook) || (b.Status != nil && *b.Status == "converted") {
|
||||
purchased++
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true, "referrals": referrals,
|
||||
"stats": gin.H{
|
||||
"total": len(bindings), "purchased": purchased, "free": len(bindings) - purchased,
|
||||
"earnings": earningsE, "pendingEarnings": pendingE, "withdrawnEarnings": withdrawnE,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// DBInit POST /api/db/init
|
||||
func DBInit(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "初始化接口已就绪(表结构由迁移维护)"}})
|
||||
}
|
||||
|
||||
// DBDistribution GET /api/db/distribution
|
||||
func DBDistribution(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
db := database.DB()
|
||||
var bindings []model.ReferralBinding
|
||||
q := db.Order("binding_date DESC").Limit(500)
|
||||
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})
|
||||
return
|
||||
}
|
||||
referrerIds := make(map[string]bool)
|
||||
refereeIds := make(map[string]bool)
|
||||
for _, b := range bindings {
|
||||
referrerIds[b.ReferrerID] = true
|
||||
refereeIds[b.RefereeID] = true
|
||||
}
|
||||
allIds := make([]string, 0, len(referrerIds)+len(refereeIds))
|
||||
for id := range referrerIds {
|
||||
allIds = append(allIds, id)
|
||||
}
|
||||
for id := range refereeIds {
|
||||
if !referrerIds[id] {
|
||||
allIds = append(allIds, id)
|
||||
}
|
||||
}
|
||||
var users []model.User
|
||||
if len(allIds) > 0 {
|
||||
db.Where("id IN ?", allIds).Find(&users)
|
||||
}
|
||||
userMap := make(map[string]*model.User)
|
||||
for i := range users {
|
||||
userMap[users[i].ID] = &users[i]
|
||||
}
|
||||
out := make([]gin.H, 0, len(bindings))
|
||||
for _, b := range bindings {
|
||||
refNick := "用户"
|
||||
if u := userMap[b.RefereeID]; u != nil && u.Nickname != nil {
|
||||
refNick = *u.Nickname
|
||||
} else {
|
||||
refNick = refNick + b.RefereeID
|
||||
}
|
||||
var referrerName *string
|
||||
if u := userMap[b.ReferrerID]; u != nil {
|
||||
referrerName = u.Nickname
|
||||
}
|
||||
days := 0
|
||||
if b.ExpiryDate.After(time.Now()) {
|
||||
days = int(b.ExpiryDate.Sub(time.Now()).Hours() / 24)
|
||||
}
|
||||
var refereePhone *string
|
||||
if u := userMap[b.RefereeID]; u != nil {
|
||||
refereePhone = u.Phone
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": b.ID, "referrer_id": b.ReferrerID, "referrer_name": referrerName, "referrer_code": b.ReferralCode,
|
||||
"referee_id": b.RefereeID, "referee_nickname": refNick, "referee_phone": refereePhone,
|
||||
"bound_at": b.BindingDate, "expires_at": b.ExpiryDate, "status": b.Status,
|
||||
"days_remaining": days, "commission": b.CommissionAmount, "source": "miniprogram",
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "bindings": out, "total": len(out)})
|
||||
}
|
||||
|
||||
// DBChapters GET/POST /api/db/chapters
|
||||
func DBChapters(c *gin.Context) {
|
||||
var list []model.Chapter
|
||||
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "data": []interface{}{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": list})
|
||||
}
|
||||
|
||||
// DBConfigDelete DELETE /api/db/config
|
||||
func DBConfigDelete(c *gin.Context) {
|
||||
key := c.Query("key")
|
||||
if key == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "配置键不能为空"})
|
||||
return
|
||||
}
|
||||
if err := database.DB().Where("config_key = ?", key).Delete(&model.SystemConfig{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// DBInitGet GET /api/db/init
|
||||
func DBInitGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"message": "ok"}})
|
||||
}
|
||||
|
||||
// DBMigrateGet GET /api/db/migrate
|
||||
func DBMigrateGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "迁移状态查询(由 Prisma/外部维护)"})
|
||||
}
|
||||
|
||||
// DBMigratePost POST /api/db/migrate
|
||||
func DBMigratePost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "迁移由 Prisma/外部执行"})
|
||||
}
|
||||
247
soul-api/internal/handler/db_book.go
Normal file
247
soul-api/internal/handler/db_book.go
Normal file
@@ -0,0 +1,247 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// sectionListItem 与前端 SectionListItem 一致(小写驼峰)
|
||||
type sectionListItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Price float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree,omitempty"`
|
||||
PartID string `json:"partId"`
|
||||
PartTitle string `json:"partTitle"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
ChapterTitle string `json:"chapterTitle"`
|
||||
FilePath *string `json:"filePath,omitempty"`
|
||||
}
|
||||
|
||||
// DBBookAction GET/POST/PUT /api/db/book
|
||||
func DBBookAction(c *gin.Context) {
|
||||
db := database.DB()
|
||||
switch c.Request.Method {
|
||||
case http.MethodGet:
|
||||
action := c.Query("action")
|
||||
id := c.Query("id")
|
||||
switch action {
|
||||
case "list":
|
||||
var rows []model.Chapter
|
||||
if err := db.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "sections": []sectionListItem{}})
|
||||
return
|
||||
}
|
||||
sections := make([]sectionListItem, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
price := 1.0
|
||||
if r.Price != nil {
|
||||
price = *r.Price
|
||||
}
|
||||
sections = append(sections, sectionListItem{
|
||||
ID: r.ID,
|
||||
Title: r.SectionTitle,
|
||||
Price: price,
|
||||
IsFree: r.IsFree,
|
||||
PartID: r.PartID,
|
||||
PartTitle: r.PartTitle,
|
||||
ChapterID: r.ChapterID,
|
||||
ChapterTitle: r.ChapterTitle,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections, "total": len(sections)})
|
||||
return
|
||||
case "read":
|
||||
if id == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
var ch model.Chapter
|
||||
if err := db.Where("id = ?", id).First(&ch).Error; err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "章节不存在"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
price := 1.0
|
||||
if ch.Price != nil {
|
||||
price = *ch.Price
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"section": gin.H{
|
||||
"id": ch.ID,
|
||||
"title": ch.SectionTitle,
|
||||
"price": price,
|
||||
"content": ch.Content,
|
||||
"partId": ch.PartID,
|
||||
"partTitle": ch.PartTitle,
|
||||
"chapterId": ch.ChapterID,
|
||||
"chapterTitle": ch.ChapterTitle,
|
||||
},
|
||||
})
|
||||
return
|
||||
case "export":
|
||||
var rows []model.Chapter
|
||||
if err := db.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
sections := make([]sectionListItem, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
price := 1.0
|
||||
if r.Price != nil {
|
||||
price = *r.Price
|
||||
}
|
||||
sections = append(sections, sectionListItem{
|
||||
ID: r.ID, Title: r.SectionTitle, Price: price, IsFree: r.IsFree,
|
||||
PartID: r.PartID, PartTitle: r.PartTitle, ChapterID: r.ChapterID, ChapterTitle: r.ChapterTitle,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "sections": sections})
|
||||
return
|
||||
default:
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无效的 action"})
|
||||
return
|
||||
}
|
||||
case http.MethodPost:
|
||||
var body struct {
|
||||
Action string `json:"action"`
|
||||
Data []importItem `json:"data"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
switch body.Action {
|
||||
case "sync":
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "同步完成(Gin 无文件源时可从 DB 已存在数据视为已同步)"})
|
||||
return
|
||||
case "import":
|
||||
imported, failed := 0, 0
|
||||
for _, item := range body.Data {
|
||||
price := 1.0
|
||||
if item.Price != nil {
|
||||
price = *item.Price
|
||||
}
|
||||
isFree := false
|
||||
if item.IsFree != nil {
|
||||
isFree = *item.IsFree
|
||||
}
|
||||
wordCount := len(item.Content)
|
||||
status := "published"
|
||||
ch := model.Chapter{
|
||||
ID: item.ID,
|
||||
PartID: strPtr(item.PartID, "part-1"),
|
||||
PartTitle: strPtr(item.PartTitle, "未分类"),
|
||||
ChapterID: strPtr(item.ChapterID, "chapter-1"),
|
||||
ChapterTitle: strPtr(item.ChapterTitle, "未分类"),
|
||||
SectionTitle: item.Title,
|
||||
Content: item.Content,
|
||||
WordCount: &wordCount,
|
||||
IsFree: &isFree,
|
||||
Price: &price,
|
||||
Status: &status,
|
||||
}
|
||||
err := db.Where("id = ?", item.ID).First(&model.Chapter{}).Error
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
err = db.Create(&ch).Error
|
||||
} else if err == nil {
|
||||
err = db.Model(&model.Chapter{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"section_title": ch.SectionTitle,
|
||||
"content": ch.Content,
|
||||
"word_count": ch.WordCount,
|
||||
"is_free": ch.IsFree,
|
||||
"price": ch.Price,
|
||||
}).Error
|
||||
}
|
||||
if err != nil {
|
||||
failed++
|
||||
continue
|
||||
}
|
||||
imported++
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "导入完成", "imported": imported, "failed": failed})
|
||||
return
|
||||
default:
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无效的 action"})
|
||||
return
|
||||
}
|
||||
case http.MethodPut:
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
|
||||
return
|
||||
}
|
||||
price := 1.0
|
||||
if body.Price != nil {
|
||||
price = *body.Price
|
||||
}
|
||||
isFree := false
|
||||
if body.IsFree != nil {
|
||||
isFree = *body.IsFree
|
||||
}
|
||||
wordCount := len(body.Content)
|
||||
updates := map[string]interface{}{
|
||||
"section_title": body.Title,
|
||||
"content": body.Content,
|
||||
"word_count": wordCount,
|
||||
"price": price,
|
||||
"is_free": isFree,
|
||||
}
|
||||
err := db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||
}
|
||||
|
||||
type importItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
PartID *string `json:"partId"`
|
||||
PartTitle *string `json:"partTitle"`
|
||||
ChapterID *string `json:"chapterId"`
|
||||
ChapterTitle *string `json:"chapterTitle"`
|
||||
}
|
||||
|
||||
func strPtr(s *string, def string) string {
|
||||
if s != nil && *s != "" {
|
||||
return *s
|
||||
}
|
||||
return def
|
||||
}
|
||||
|
||||
// DBBookDelete DELETE /api/db/book
|
||||
func DBBookDelete(c *gin.Context) {
|
||||
id := c.Query("id")
|
||||
if id == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
if err := database.DB().Where("id = ?", id).Delete(&model.Chapter{}).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
22
soul-api/internal/handler/distribution.go
Normal file
22
soul-api/internal/handler/distribution.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DistributionGet POST /api/distribution GET/POST/PUT
|
||||
func DistributionGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// DistributionAutoWithdrawConfig GET/POST/DELETE /api/distribution/auto-withdraw-config
|
||||
func DistributionAutoWithdrawConfig(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// DistributionMessages GET/POST /api/distribution/messages
|
||||
func DistributionMessages(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
12
soul-api/internal/handler/documentation.go
Normal file
12
soul-api/internal/handler/documentation.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// DocGenerate POST /api/documentation/generate
|
||||
func DocGenerate(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
22
soul-api/internal/handler/match.go
Normal file
22
soul-api/internal/handler/match.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MatchConfigGet GET /api/match/config
|
||||
func MatchConfigGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MatchConfigPost POST /api/match/config
|
||||
func MatchConfigPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MatchUsers POST /api/match/users (Next 为 POST,拆解计划写 GET,两法都挂)
|
||||
func MatchUsers(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
}
|
||||
12
soul-api/internal/handler/menu.go
Normal file
12
soul-api/internal/handler/menu.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MenuGet GET /api/menu
|
||||
func MenuGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
}
|
||||
32
soul-api/internal/handler/miniprogram.go
Normal file
32
soul-api/internal/handler/miniprogram.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// MiniprogramLogin POST /api/miniprogram/login
|
||||
func MiniprogramLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MiniprogramPay GET/POST /api/miniprogram/pay
|
||||
func MiniprogramPay(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MiniprogramPayNotify POST /api/miniprogram/pay/notify
|
||||
func MiniprogramPayNotify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MiniprogramPhone POST /api/miniprogram/phone
|
||||
func MiniprogramPhone(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// MiniprogramQrcode POST /api/miniprogram/qrcode (Next 为 POST)
|
||||
func MiniprogramQrcode(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
20
soul-api/internal/handler/orders.go
Normal file
20
soul-api/internal/handler/orders.go
Normal file
@@ -0,0 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// OrdersList GET /api/orders
|
||||
func OrdersList(c *gin.Context) {
|
||||
var orders []model.Order
|
||||
if err := database.DB().Order("created_at DESC").Find(&orders).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error(), "orders": []interface{}{}})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "orders": orders})
|
||||
}
|
||||
52
soul-api/internal/handler/payment.go
Normal file
52
soul-api/internal/handler/payment.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// PaymentAlipayNotify POST /api/payment/alipay/notify
|
||||
func PaymentAlipayNotify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// PaymentCallback POST /api/payment/callback
|
||||
func PaymentCallback(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// PaymentCreateOrder POST /api/payment/create-order
|
||||
func PaymentCreateOrder(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// PaymentMethods GET /api/payment/methods
|
||||
func PaymentMethods(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
}
|
||||
|
||||
// PaymentQuery GET /api/payment/query
|
||||
func PaymentQuery(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// PaymentStatusOrderSn GET /api/payment/status/:orderSn
|
||||
func PaymentStatusOrderSn(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// PaymentVerify POST /api/payment/verify
|
||||
func PaymentVerify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// PaymentWechatNotify POST /api/payment/wechat/notify
|
||||
func PaymentWechatNotify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// PaymentWechatTransferNotify POST /api/payment/wechat/transfer/notify
|
||||
func PaymentWechatTransferNotify(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
22
soul-api/internal/handler/referral.go
Normal file
22
soul-api/internal/handler/referral.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// ReferralBind POST /api/referral/bind
|
||||
func ReferralBind(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// ReferralData GET /api/referral/data
|
||||
func ReferralData(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// ReferralVisit POST /api/referral/visit
|
||||
func ReferralVisit(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
81
soul-api/internal/handler/search.go
Normal file
81
soul-api/internal/handler/search.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// escapeLike 转义 LIKE 中的 % _ \,防止注入与通配符滥用
|
||||
func escapeLike(s string) string {
|
||||
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||
s = strings.ReplaceAll(s, "%", "\\%")
|
||||
s = strings.ReplaceAll(s, "_", "\\_")
|
||||
return s
|
||||
}
|
||||
|
||||
// SearchGet GET /api/search?q= 从 chapters 表搜索(GORM,参数化)
|
||||
func SearchGet(c *gin.Context) {
|
||||
q := strings.TrimSpace(c.Query("q"))
|
||||
if q == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请输入搜索关键词"})
|
||||
return
|
||||
}
|
||||
pattern := "%" + escapeLike(q) + "%"
|
||||
var list []model.Chapter
|
||||
err := database.DB().Model(&model.Chapter{}).
|
||||
Where("section_title LIKE ? OR content LIKE ?", pattern, pattern).
|
||||
Order("sort_order ASC, id ASC").
|
||||
Limit(50).
|
||||
Find(&list).Error
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"keyword": q, "total": 0, "results": []interface{}{}}})
|
||||
return
|
||||
}
|
||||
lowerQ := strings.ToLower(q)
|
||||
results := make([]gin.H, 0, len(list))
|
||||
for _, ch := range list {
|
||||
matchType := "content"
|
||||
score := 5
|
||||
if strings.Contains(strings.ToLower(ch.SectionTitle), lowerQ) {
|
||||
matchType = "title"
|
||||
score = 10
|
||||
}
|
||||
snippet := ""
|
||||
pos := strings.Index(strings.ToLower(ch.Content), lowerQ)
|
||||
if pos >= 0 && len(ch.Content) > 0 {
|
||||
start := pos - 50
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
end := pos + utf8.RuneCountInString(q) + 50
|
||||
if end > len(ch.Content) {
|
||||
end = len(ch.Content)
|
||||
}
|
||||
snippet = ch.Content[start:end]
|
||||
if start > 0 {
|
||||
snippet = "..." + snippet
|
||||
}
|
||||
if end < len(ch.Content) {
|
||||
snippet = snippet + "..."
|
||||
}
|
||||
}
|
||||
price := 1.0
|
||||
if ch.Price != nil {
|
||||
price = *ch.Price
|
||||
}
|
||||
results = append(results, gin.H{
|
||||
"id": ch.ID, "title": ch.SectionTitle, "partTitle": ch.PartTitle, "chapterTitle": ch.ChapterTitle,
|
||||
"price": price, "isFree": ch.IsFree, "matchType": matchType, "score": score, "snippet": snippet,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"data": gin.H{"keyword": q, "total": len(results), "results": results},
|
||||
})
|
||||
}
|
||||
22
soul-api/internal/handler/sync.go
Normal file
22
soul-api/internal/handler/sync.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// SyncGet GET /api/sync
|
||||
func SyncGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// SyncPost POST /api/sync
|
||||
func SyncPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// SyncPut PUT /api/sync
|
||||
func SyncPut(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
17
soul-api/internal/handler/upload.go
Normal file
17
soul-api/internal/handler/upload.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UploadPost POST /api/upload
|
||||
func UploadPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "url": ""})
|
||||
}
|
||||
|
||||
// UploadDelete DELETE /api/upload
|
||||
func UploadDelete(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
157
soul-api/internal/handler/user.go
Normal file
157
soul-api/internal/handler/user.go
Normal file
@@ -0,0 +1,157 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// UserAddressesGet GET /api/user/addresses
|
||||
func UserAddressesGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
}
|
||||
|
||||
// UserAddressesPost POST /api/user/addresses
|
||||
func UserAddressesPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserAddressesByID GET/PUT/DELETE /api/user/addresses/:id
|
||||
func UserAddressesByID(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserCheckPurchased GET /api/user/check-purchased
|
||||
func UserCheckPurchased(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserProfileGet GET /api/user/profile
|
||||
func UserProfileGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserProfilePost POST /api/user/profile
|
||||
func UserProfilePost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserPurchaseStatus GET /api/user/purchase-status
|
||||
func UserPurchaseStatus(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserReadingProgressGet GET /api/user/reading-progress
|
||||
func UserReadingProgressGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserReadingProgressPost POST /api/user/reading-progress
|
||||
func UserReadingProgressPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// UserTrackGet GET /api/user/track?userId=&limit= 从 user_tracks 表查(GORM)
|
||||
func UserTrackGet(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
phone := c.Query("phone")
|
||||
limit, _ := strconv.Atoi(c.DefaultQuery("limit", "50"))
|
||||
if limit < 1 || limit > 100 {
|
||||
limit = 50
|
||||
}
|
||||
if userId == "" && phone == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "需要用户ID或手机号"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
if userId == "" && phone != "" {
|
||||
var u model.User
|
||||
if err := db.Where("phone = ?", phone).First(&u).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
userId = u.ID
|
||||
}
|
||||
var tracks []model.UserTrack
|
||||
if err := db.Where("user_id = ?", userId).Order("created_at DESC").Limit(limit).Find(&tracks).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "tracks": []interface{}{}, "stats": gin.H{}, "total": 0})
|
||||
return
|
||||
}
|
||||
stats := make(map[string]int)
|
||||
formatted := make([]gin.H, 0, len(tracks))
|
||||
for _, t := range tracks {
|
||||
stats[t.Action]++
|
||||
target := ""
|
||||
if t.Target != nil {
|
||||
target = *t.Target
|
||||
}
|
||||
if t.ChapterID != nil && target == "" {
|
||||
target = *t.ChapterID
|
||||
}
|
||||
formatted = append(formatted, gin.H{
|
||||
"id": t.ID, "action": t.Action, "target": target, "chapterTitle": t.ChapterID,
|
||||
"createdAt": t.CreatedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "tracks": formatted, "stats": stats, "total": len(formatted)})
|
||||
}
|
||||
|
||||
// UserTrackPost POST /api/user/track 记录行为(GORM)
|
||||
func UserTrackPost(c *gin.Context) {
|
||||
var body struct {
|
||||
UserID string `json:"userId"`
|
||||
Phone string `json:"phone"`
|
||||
Action string `json:"action"`
|
||||
Target string `json:"target"`
|
||||
ExtraData interface{} `json:"extraData"`
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
if body.UserID == "" && body.Phone == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "需要用户ID或手机号"})
|
||||
return
|
||||
}
|
||||
if body.Action == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "error": "行为类型不能为空"})
|
||||
return
|
||||
}
|
||||
db := database.DB()
|
||||
userId := body.UserID
|
||||
if userId == "" {
|
||||
var u model.User
|
||||
if err := db.Where("phone = ?", body.Phone).First(&u).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "用户不存在"})
|
||||
return
|
||||
}
|
||||
userId = u.ID
|
||||
}
|
||||
trackID := fmt.Sprintf("track_%d", time.Now().UnixNano()%100000000)
|
||||
chID := body.Target
|
||||
if body.Action == "view_chapter" {
|
||||
chID = body.Target
|
||||
}
|
||||
t := model.UserTrack{
|
||||
ID: trackID, UserID: userId, Action: body.Action, Target: &body.Target,
|
||||
}
|
||||
if body.Target != "" {
|
||||
t.ChapterID = &chID
|
||||
}
|
||||
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, "trackId": trackID, "message": "行为记录成功"})
|
||||
}
|
||||
|
||||
// UserUpdate POST /api/user/update
|
||||
func UserUpdate(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
12
soul-api/internal/handler/wechat.go
Normal file
12
soul-api/internal/handler/wechat.go
Normal file
@@ -0,0 +1,12 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// WechatLogin POST /api/wechat/login
|
||||
func WechatLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
60
soul-api/internal/handler/withdraw.go
Normal file
60
soul-api/internal/handler/withdraw.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// WithdrawPost POST /api/withdraw 创建提现申请(占位:仅返回成功,实际需对接微信打款)
|
||||
func WithdrawPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
}
|
||||
|
||||
// WithdrawRecords GET /api/withdraw/records?userId= 当前用户提现记录(GORM)
|
||||
func WithdrawRecords(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
if userId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
|
||||
return
|
||||
}
|
||||
var list []model.Withdrawal
|
||||
if err := database.DB().Where("user_id = ?", userId).Order("created_at DESC").Limit(100).Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": []interface{}{}}})
|
||||
return
|
||||
}
|
||||
out := make([]gin.H, 0, len(list))
|
||||
for _, w := range list {
|
||||
st := ""
|
||||
if w.Status != nil {
|
||||
st = *w.Status
|
||||
}
|
||||
out = append(out, gin.H{
|
||||
"id": w.ID, "amount": w.Amount, "status": st,
|
||||
"createdAt": w.CreatedAt, "processedAt": w.ProcessedAt,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": out}})
|
||||
}
|
||||
|
||||
// WithdrawPendingConfirm GET /api/withdraw/pending-confirm?userId= 待确认收款列表(GORM)
|
||||
func WithdrawPendingConfirm(c *gin.Context) {
|
||||
userId := c.Query("userId")
|
||||
if userId == "" {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"success": false, "message": "缺少 userId"})
|
||||
return
|
||||
}
|
||||
var list []model.Withdrawal
|
||||
if err := database.DB().Where("user_id = ? AND status = ?", userId, "pending_confirm").Order("created_at DESC").Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": []interface{}{}, "mch_id": "", "app_id": ""}})
|
||||
return
|
||||
}
|
||||
out := make([]gin.H, 0, len(list))
|
||||
for _, w := range list {
|
||||
out = append(out, gin.H{"id": w.ID, "amount": w.Amount, "createdAt": w.CreatedAt})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"list": out, "mchId": "", "appId": ""}})
|
||||
}
|
||||
25
soul-api/internal/middleware/admin_auth.go
Normal file
25
soul-api/internal/middleware/admin_auth.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// AdminAuth 管理端鉴权:校验登录态(Cookie 或 Authorization),未登录返回 401
|
||||
// 开发模式(GIN_MODE=debug)下暂不校验,便于联调;生产请实现 Session/JWT
|
||||
func AdminAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if os.Getenv("GIN_MODE") == "debug" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
_, err := c.Cookie("admin_session")
|
||||
if err != nil {
|
||||
c.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"success": false, "error": "未登录"})
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
65
soul-api/internal/middleware/ratelimit.go
Normal file
65
soul-api/internal/middleware/ratelimit.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// RateLimiter 按 IP 的限流器
|
||||
type RateLimiter struct {
|
||||
mu sync.Mutex
|
||||
clients map[string]*rate.Limiter
|
||||
r rate.Limit
|
||||
b int
|
||||
}
|
||||
|
||||
// NewRateLimiter 创建限流中间件,r 每秒请求数,b 突发容量
|
||||
func NewRateLimiter(r rate.Limit, b int) *RateLimiter {
|
||||
return &RateLimiter{
|
||||
clients: make(map[string]*rate.Limiter),
|
||||
r: r,
|
||||
b: b,
|
||||
}
|
||||
}
|
||||
|
||||
// getLimiter 获取或创建该 key 的 limiter
|
||||
func (rl *RateLimiter) getLimiter(key string) *rate.Limiter {
|
||||
rl.mu.Lock()
|
||||
defer rl.mu.Unlock()
|
||||
if lim, ok := rl.clients[key]; ok {
|
||||
return lim
|
||||
}
|
||||
lim := rate.NewLimiter(rl.r, rl.b)
|
||||
rl.clients[key] = lim
|
||||
return lim
|
||||
}
|
||||
|
||||
// Middleware 返回 Gin 限流中间件(按客户端 IP)
|
||||
func (rl *RateLimiter) Middleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
key := c.ClientIP()
|
||||
lim := rl.getLimiter(key)
|
||||
if !lim.Allow() {
|
||||
c.AbortWithStatus(http.StatusTooManyRequests)
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup 定期清理过期 limiter(可选,避免 map 无限增长)
|
||||
func (rl *RateLimiter) Cleanup(interval time.Duration) {
|
||||
ticker := time.NewTicker(interval)
|
||||
go func() {
|
||||
for range ticker.C {
|
||||
rl.mu.Lock()
|
||||
rl.clients = make(map[string]*rate.Limiter)
|
||||
rl.mu.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
25
soul-api/internal/middleware/secure.go
Normal file
25
soul-api/internal/middleware/secure.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/unrolled/secure"
|
||||
)
|
||||
|
||||
// Secure 安全响应头中间件
|
||||
func Secure() gin.HandlerFunc {
|
||||
s := secure.New(secure.Options{
|
||||
FrameDeny: true,
|
||||
ContentTypeNosniff: true,
|
||||
BrowserXssFilter: true,
|
||||
ContentSecurityPolicy: "frame-ancestors 'none'",
|
||||
ReferrerPolicy: "no-referrer",
|
||||
})
|
||||
return func(c *gin.Context) {
|
||||
err := s.Process(c.Writer, c.Request)
|
||||
if err != nil {
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
1
soul-api/internal/model/README.txt
Normal file
1
soul-api/internal/model/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
在此目录放置 GORM 模型与请求/响应结构体,例如 User、Order、Withdrawal、Config 等。
|
||||
23
soul-api/internal/model/chapter.go
Normal file
23
soul-api/internal/model/chapter.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Chapter 对应表 chapters(与 Prisma 一致),JSON 小写驼峰
|
||||
type Chapter struct {
|
||||
ID string `gorm:"column:id;primaryKey;size:20" json:"id"`
|
||||
PartID string `gorm:"column:part_id;size:20" json:"partId"`
|
||||
PartTitle string `gorm:"column:part_title;size:100" json:"partTitle"`
|
||||
ChapterID string `gorm:"column:chapter_id;size:20" json:"chapterId"`
|
||||
ChapterTitle string `gorm:"column:chapter_title;size:200" json:"chapterTitle"`
|
||||
SectionTitle string `gorm:"column:section_title;size:200" json:"sectionTitle"`
|
||||
Content string `gorm:"column:content;type:longtext" json:"content,omitempty"`
|
||||
WordCount *int `gorm:"column:word_count" json:"wordCount,omitempty"`
|
||||
IsFree *bool `gorm:"column:is_free" json:"isFree,omitempty"`
|
||||
Price *float64 `gorm:"column:price;type:decimal(10,2)" json:"price,omitempty"`
|
||||
SortOrder *int `gorm:"column:sort_order" json:"sortOrder,omitempty"`
|
||||
Status *string `gorm:"column:status;size:20" json:"status,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Chapter) TableName() string { return "chapters" }
|
||||
24
soul-api/internal/model/order.go
Normal file
24
soul-api/internal/model/order.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Order 对应表 orders,JSON 输出与现网接口 1:1(小写驼峰)
|
||||
type Order struct {
|
||||
ID string `gorm:"column:id;primaryKey;size:50" json:"id"`
|
||||
OrderSN string `gorm:"column:order_sn;uniqueIndex;size:50" json:"orderSn"`
|
||||
UserID string `gorm:"column:user_id;size:50" json:"userId"`
|
||||
OpenID string `gorm:"column:open_id;size:100" json:"openId"`
|
||||
ProductType string `gorm:"column:product_type;size:50" json:"productType"`
|
||||
ProductID *string `gorm:"column:product_id;size:50" json:"productId,omitempty"`
|
||||
Amount float64 `gorm:"column:amount;type:decimal(10,2)" json:"amount"`
|
||||
Description *string `gorm:"column:description;size:200" json:"description,omitempty"`
|
||||
Status *string `gorm:"column:status;size:20" json:"status,omitempty"`
|
||||
TransactionID *string `gorm:"column:transaction_id;size:100" json:"transactionId,omitempty"`
|
||||
PayTime *time.Time `gorm:"column:pay_time" json:"payTime,omitempty"`
|
||||
ReferralCode *string `gorm:"column:referral_code;size:255" json:"referralCode,omitempty"`
|
||||
ReferrerID *string `gorm:"column:referrer_id;size:255" json:"referrerId,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (Order) TableName() string { return "orders" }
|
||||
19
soul-api/internal/model/referral_binding.go
Normal file
19
soul-api/internal/model/referral_binding.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ReferralBinding 对应表 referral_bindings
|
||||
type ReferralBinding struct {
|
||||
ID string `gorm:"column:id;primaryKey;size:50"`
|
||||
ReferrerID string `gorm:"column:referrer_id;size:50"`
|
||||
RefereeID string `gorm:"column:referee_id;size:50"`
|
||||
ReferralCode string `gorm:"column:referral_code;size:20"`
|
||||
Status *string `gorm:"column:status;size:20"`
|
||||
BindingDate time.Time `gorm:"column:binding_date"`
|
||||
ExpiryDate time.Time `gorm:"column:expiry_date"`
|
||||
CommissionAmount *float64 `gorm:"column:commission_amount;type:decimal(10,2)"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at"`
|
||||
}
|
||||
|
||||
func (ReferralBinding) TableName() string { return "referral_bindings" }
|
||||
13
soul-api/internal/model/referral_visit.go
Normal file
13
soul-api/internal/model/referral_visit.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// ReferralVisit 对应表 referral_visits
|
||||
type ReferralVisit struct {
|
||||
ID int `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
ReferrerID string `gorm:"column:referrer_id;size:50"`
|
||||
VisitorID *string `gorm:"column:visitor_id;size:50"`
|
||||
CreatedAt time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
func (ReferralVisit) TableName() string { return "referral_visits" }
|
||||
35
soul-api/internal/model/system_config.go
Normal file
35
soul-api/internal/model/system_config.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"database/sql/driver"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ConfigValue 存 system_config.config_value(JSON 列,可为 object 或 array)
|
||||
type ConfigValue []byte
|
||||
|
||||
func (c ConfigValue) Value() (driver.Value, error) { return []byte(c), nil }
|
||||
func (c *ConfigValue) Scan(value interface{}) error {
|
||||
if value == nil {
|
||||
*c = nil
|
||||
return nil
|
||||
}
|
||||
b, ok := value.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
*c = append((*c)[0:0], b...)
|
||||
return nil
|
||||
}
|
||||
|
||||
// SystemConfig 对应表 system_config,JSON 输出与现网 1:1(小写驼峰)
|
||||
type SystemConfig struct {
|
||||
ID int `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
|
||||
ConfigKey string `gorm:"column:config_key;uniqueIndex;size:100" json:"configKey"`
|
||||
ConfigValue ConfigValue `gorm:"column:config_value;type:json" json:"configValue"`
|
||||
Description *string `gorm:"column:description;size:200" json:"description,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
}
|
||||
|
||||
func (SystemConfig) TableName() string { return "system_config" }
|
||||
26
soul-api/internal/model/user.go
Normal file
26
soul-api/internal/model/user.go
Normal file
@@ -0,0 +1,26 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
|
||||
// User 对应表 users,JSON 输出与现网接口 1:1(小写驼峰)
|
||||
type User struct {
|
||||
ID string `gorm:"column:id;primaryKey;size:50" json:"id"`
|
||||
OpenID *string `gorm:"column:open_id;size:100" json:"openId,omitempty"`
|
||||
Nickname *string `gorm:"column:nickname;size:100" json:"nickname,omitempty"`
|
||||
Avatar *string `gorm:"column:avatar;size:500" json:"avatar,omitempty"`
|
||||
Phone *string `gorm:"column:phone;size:20" json:"phone,omitempty"`
|
||||
WechatID *string `gorm:"column:wechat_id;size:100" json:"wechatId,omitempty"`
|
||||
ReferralCode *string `gorm:"column:referral_code;size:20" json:"referralCode,omitempty"`
|
||||
HasFullBook *bool `gorm:"column:has_full_book" json:"hasFullBook,omitempty"`
|
||||
Earnings *float64 `gorm:"column:earnings;type:decimal(10,2)" json:"earnings,omitempty"`
|
||||
PendingEarnings *float64 `gorm:"column:pending_earnings;type:decimal(10,2)" json:"pendingEarnings,omitempty"`
|
||||
ReferralCount *int `gorm:"column:referral_count" json:"referralCount,omitempty"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
UpdatedAt time.Time `gorm:"column:updated_at" json:"updatedAt"`
|
||||
IsAdmin *bool `gorm:"column:is_admin" json:"isAdmin,omitempty"`
|
||||
WithdrawnEarnings *float64 `gorm:"column:withdrawn_earnings;type:decimal(10,2)" json:"withdrawnEarnings,omitempty"`
|
||||
Source *string `gorm:"column:source;size:50" json:"source,omitempty"`
|
||||
}
|
||||
|
||||
func (User) TableName() string { return "users" }
|
||||
16
soul-api/internal/model/user_track.go
Normal file
16
soul-api/internal/model/user_track.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// UserTrack 对应表 user_tracks
|
||||
type UserTrack struct {
|
||||
ID string `gorm:"column:id;primaryKey;size:50"`
|
||||
UserID string `gorm:"column:user_id;size:100"`
|
||||
Action string `gorm:"column:action;size:50"`
|
||||
ChapterID *string `gorm:"column:chapter_id;size:100"`
|
||||
Target *string `gorm:"column:target;size:200"`
|
||||
ExtraData []byte `gorm:"column:extra_data;type:json"`
|
||||
CreatedAt *time.Time `gorm:"column:created_at"`
|
||||
}
|
||||
|
||||
func (UserTrack) TableName() string { return "user_tracks" }
|
||||
17
soul-api/internal/model/withdrawal.go
Normal file
17
soul-api/internal/model/withdrawal.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
// Withdrawal 对应表 withdrawals
|
||||
type Withdrawal struct {
|
||||
ID string `gorm:"column:id;primaryKey;size:50" json:"id"`
|
||||
UserID string `gorm:"column:user_id;size:50" json:"userId"`
|
||||
Amount float64 `gorm:"column:amount;type:decimal(10,2)" json:"amount"`
|
||||
Status *string `gorm:"column:status;size:20" json:"status"`
|
||||
WechatID *string `gorm:"column:wechat_id;size:100" json:"wechatId"`
|
||||
WechatOpenid *string `gorm:"column:wechat_openid;size:100" json:"wechatOpenid"`
|
||||
CreatedAt time.Time `gorm:"column:created_at" json:"createdAt"`
|
||||
ProcessedAt *time.Time `gorm:"column:processed_at" json:"processedAt"`
|
||||
}
|
||||
|
||||
func (Withdrawal) TableName() string { return "withdrawals" }
|
||||
1
soul-api/internal/repository/README.txt
Normal file
1
soul-api/internal/repository/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
在此目录放置数据库访问层,供 service 调用,例如 UserRepo、OrderRepo、ConfigRepo 等。
|
||||
213
soul-api/internal/router/router.go
Normal file
213
soul-api/internal/router/router.go
Normal file
@@ -0,0 +1,213 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"soul-api/internal/config"
|
||||
"soul-api/internal/handler"
|
||||
"soul-api/internal/middleware"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// Setup 创建并配置 Gin 引擎,路径与 app/api 一致
|
||||
func Setup(cfg *config.Config) *gin.Engine {
|
||||
gin.SetMode(cfg.Mode)
|
||||
r := gin.New()
|
||||
r.Use(gin.Recovery())
|
||||
r.Use(gin.Logger())
|
||||
_ = r.SetTrustedProxies(cfg.TrustedProxies)
|
||||
|
||||
r.Use(middleware.Secure())
|
||||
r.Use(cors.New(cors.Config{
|
||||
AllowOrigins: cfg.CORSOrigins,
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
||||
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
|
||||
AllowCredentials: true,
|
||||
MaxAge: 86400,
|
||||
}))
|
||||
rateLimiter := middleware.NewRateLimiter(100, 200)
|
||||
r.Use(rateLimiter.Middleware())
|
||||
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// ----- 管理端 -----
|
||||
api.GET("/admin", handler.AdminCheck)
|
||||
api.POST("/admin", handler.AdminLogin)
|
||||
api.POST("/admin/logout", handler.AdminLogout)
|
||||
|
||||
admin := api.Group("/admin")
|
||||
admin.Use(middleware.AdminAuth())
|
||||
{
|
||||
admin.GET("/chapters", handler.AdminChaptersList)
|
||||
admin.POST("/chapters", handler.AdminChaptersAction)
|
||||
admin.PUT("/chapters", handler.AdminChaptersAction)
|
||||
admin.DELETE("/chapters", handler.AdminChaptersAction)
|
||||
admin.GET("/content", handler.AdminContent)
|
||||
admin.POST("/content", handler.AdminContent)
|
||||
admin.PUT("/content", handler.AdminContent)
|
||||
admin.DELETE("/content", handler.AdminContent)
|
||||
admin.GET("/distribution/overview", handler.AdminDistributionOverview)
|
||||
admin.GET("/payment", handler.AdminPayment)
|
||||
admin.POST("/payment", handler.AdminPayment)
|
||||
admin.PUT("/payment", handler.AdminPayment)
|
||||
admin.DELETE("/payment", handler.AdminPayment)
|
||||
admin.GET("/referral", handler.AdminReferral)
|
||||
admin.POST("/referral", handler.AdminReferral)
|
||||
admin.PUT("/referral", handler.AdminReferral)
|
||||
admin.DELETE("/referral", handler.AdminReferral)
|
||||
admin.GET("/withdrawals", handler.AdminWithdrawalsList)
|
||||
admin.PUT("/withdrawals", handler.AdminWithdrawalsAction)
|
||||
}
|
||||
|
||||
// ----- 鉴权 -----
|
||||
api.POST("/auth/login", handler.AuthLogin)
|
||||
api.POST("/auth/reset-password", handler.AuthResetPassword)
|
||||
|
||||
// ----- 书籍/章节 -----
|
||||
api.GET("/book/all-chapters", handler.BookAllChapters)
|
||||
api.GET("/book/chapter/:id", handler.BookChapterByID)
|
||||
api.GET("/book/chapters", handler.BookChapters)
|
||||
api.POST("/book/chapters", handler.BookChapters)
|
||||
api.PUT("/book/chapters", handler.BookChapters)
|
||||
api.DELETE("/book/chapters", handler.BookChapters)
|
||||
api.GET("/book/hot", handler.BookHot)
|
||||
api.GET("/book/latest-chapters", handler.BookLatestChapters)
|
||||
api.GET("/book/search", handler.BookSearch)
|
||||
api.GET("/book/stats", handler.BookStats)
|
||||
api.GET("/book/sync", handler.BookSync)
|
||||
api.POST("/book/sync", handler.BookSync)
|
||||
|
||||
// ----- CKB -----
|
||||
api.POST("/ckb/join", handler.CKBJoin)
|
||||
api.POST("/ckb/match", handler.CKBMatch)
|
||||
api.GET("/ckb/sync", handler.CKBSync)
|
||||
api.POST("/ckb/sync", handler.CKBSync)
|
||||
|
||||
// ----- 配置 -----
|
||||
api.GET("/config", handler.GetConfig)
|
||||
|
||||
// ----- 内容 -----
|
||||
api.GET("/content", handler.ContentGet)
|
||||
|
||||
// ----- 定时任务 -----
|
||||
api.GET("/cron/sync-orders", handler.CronSyncOrders)
|
||||
api.POST("/cron/sync-orders", handler.CronSyncOrders)
|
||||
api.GET("/cron/unbind-expired", handler.CronUnbindExpired)
|
||||
api.POST("/cron/unbind-expired", handler.CronUnbindExpired)
|
||||
|
||||
// ----- 数据库(管理端) -----
|
||||
db := api.Group("/db")
|
||||
db.Use(middleware.AdminAuth())
|
||||
{
|
||||
db.GET("/book", handler.DBBookAction)
|
||||
db.POST("/book", handler.DBBookAction)
|
||||
db.PUT("/book", handler.DBBookAction)
|
||||
db.DELETE("/book", handler.DBBookDelete)
|
||||
db.GET("/chapters", handler.DBChapters)
|
||||
db.POST("/chapters", handler.DBChapters)
|
||||
db.GET("/config", handler.DBConfigGet)
|
||||
db.POST("/config", handler.DBConfigPost)
|
||||
db.DELETE("/config", handler.DBConfigDelete)
|
||||
db.GET("/distribution", handler.DBDistribution)
|
||||
db.GET("/init", handler.DBInitGet)
|
||||
db.POST("/init", handler.DBInit)
|
||||
db.GET("/migrate", handler.DBMigrateGet)
|
||||
db.POST("/migrate", handler.DBMigratePost)
|
||||
db.GET("/users", handler.DBUsersList)
|
||||
db.POST("/users", handler.DBUsersAction)
|
||||
db.PUT("/users", handler.DBUsersAction)
|
||||
db.DELETE("/users", handler.DBUsersDelete)
|
||||
db.GET("/users/referrals", handler.DBUsersReferrals)
|
||||
}
|
||||
|
||||
// ----- 分销 -----
|
||||
api.GET("/distribution", handler.DistributionGet)
|
||||
api.POST("/distribution", handler.DistributionGet)
|
||||
api.PUT("/distribution", handler.DistributionGet)
|
||||
api.GET("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||||
api.POST("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||||
api.DELETE("/distribution/auto-withdraw-config", handler.DistributionAutoWithdrawConfig)
|
||||
api.GET("/distribution/messages", handler.DistributionMessages)
|
||||
api.POST("/distribution/messages", handler.DistributionMessages)
|
||||
|
||||
// ----- 文档生成 -----
|
||||
api.POST("/documentation/generate", handler.DocGenerate)
|
||||
|
||||
// ----- 找伙伴 -----
|
||||
api.GET("/match/config", handler.MatchConfigGet)
|
||||
api.POST("/match/config", handler.MatchConfigPost)
|
||||
api.POST("/match/users", handler.MatchUsers)
|
||||
|
||||
// ----- 菜单 -----
|
||||
api.GET("/menu", handler.MenuGet)
|
||||
|
||||
// ----- 小程序 -----
|
||||
api.POST("/miniprogram/login", handler.MiniprogramLogin)
|
||||
api.GET("/miniprogram/pay", handler.MiniprogramPay)
|
||||
api.POST("/miniprogram/pay", handler.MiniprogramPay)
|
||||
api.POST("/miniprogram/pay/notify", handler.MiniprogramPayNotify)
|
||||
api.POST("/miniprogram/phone", handler.MiniprogramPhone)
|
||||
api.POST("/miniprogram/qrcode", handler.MiniprogramQrcode)
|
||||
|
||||
// ----- 订单 -----
|
||||
api.GET("/orders", handler.OrdersList)
|
||||
|
||||
// ----- 支付 -----
|
||||
api.POST("/payment/alipay/notify", handler.PaymentAlipayNotify)
|
||||
api.POST("/payment/callback", handler.PaymentCallback)
|
||||
api.POST("/payment/create-order", handler.PaymentCreateOrder)
|
||||
api.GET("/payment/methods", handler.PaymentMethods)
|
||||
api.GET("/payment/query", handler.PaymentQuery)
|
||||
api.GET("/payment/status/:orderSn", handler.PaymentStatusOrderSn)
|
||||
api.POST("/payment/verify", handler.PaymentVerify)
|
||||
api.POST("/payment/wechat/notify", handler.PaymentWechatNotify)
|
||||
api.POST("/payment/wechat/transfer/notify", handler.PaymentWechatTransferNotify)
|
||||
|
||||
// ----- 推荐 -----
|
||||
api.POST("/referral/bind", handler.ReferralBind)
|
||||
api.GET("/referral/data", handler.ReferralData)
|
||||
api.POST("/referral/visit", handler.ReferralVisit)
|
||||
|
||||
// ----- 搜索 -----
|
||||
api.GET("/search", handler.SearchGet)
|
||||
|
||||
// ----- 同步 -----
|
||||
api.GET("/sync", handler.SyncGet)
|
||||
api.POST("/sync", handler.SyncPost)
|
||||
api.PUT("/sync", handler.SyncPut)
|
||||
|
||||
// ----- 上传 -----
|
||||
api.POST("/upload", handler.UploadPost)
|
||||
api.DELETE("/upload", handler.UploadDelete)
|
||||
|
||||
// ----- 用户 -----
|
||||
api.GET("/user/addresses", handler.UserAddressesGet)
|
||||
api.POST("/user/addresses", handler.UserAddressesPost)
|
||||
api.GET("/user/addresses/:id", handler.UserAddressesByID)
|
||||
api.PUT("/user/addresses/:id", handler.UserAddressesByID)
|
||||
api.DELETE("/user/addresses/:id", handler.UserAddressesByID)
|
||||
api.GET("/user/check-purchased", handler.UserCheckPurchased)
|
||||
api.GET("/user/profile", handler.UserProfileGet)
|
||||
api.POST("/user/profile", handler.UserProfilePost)
|
||||
api.GET("/user/purchase-status", handler.UserPurchaseStatus)
|
||||
api.GET("/user/reading-progress", handler.UserReadingProgressGet)
|
||||
api.POST("/user/reading-progress", handler.UserReadingProgressPost)
|
||||
api.GET("/user/track", handler.UserTrackGet)
|
||||
api.POST("/user/track", handler.UserTrackPost)
|
||||
api.POST("/user/update", handler.UserUpdate)
|
||||
|
||||
// ----- 微信登录 -----
|
||||
api.POST("/wechat/login", handler.WechatLogin)
|
||||
|
||||
// ----- 提现 -----
|
||||
api.POST("/withdraw", handler.WithdrawPost)
|
||||
api.GET("/withdraw/records", handler.WithdrawRecords)
|
||||
api.GET("/withdraw/pending-confirm", handler.WithdrawPendingConfirm)
|
||||
}
|
||||
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
return r
|
||||
}
|
||||
1
soul-api/internal/service/README.txt
Normal file
1
soul-api/internal/service/README.txt
Normal file
@@ -0,0 +1 @@
|
||||
在此目录放置业务逻辑,供 handler 调用,例如 AdminService、UserService、PaymentService 等。
|
||||
Reference in New Issue
Block a user