更新管理后台布局,优化菜单项标签,新增支付配置项。同时,调整API响应字段命名,确保一致性,提升代码可读性和维护性。

This commit is contained in:
乘风
2026-02-09 14:33:41 +08:00
parent bee72dc7f8
commit dfbe3eb427
77 changed files with 3041 additions and 240 deletions

View 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
}

View 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
}

View 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})
}

View 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})
}

View 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) + "%"
}

View 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})
}

View 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": "操作成功"})
}

View 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})
}

View 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 维护"})
}

View 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})
}

View 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)
}

View 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})
}

View 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})
}

View 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/外部执行"})
}

View 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})
}

View 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})
}

View 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})
}

View 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{}{}})
}

View 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{}{}})
}

View 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})
}

View 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})
}

View 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})
}

View 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})
}

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

View 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})
}

View 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})
}

View 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})
}

View 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})
}

View 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": ""}})
}

View 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()
}
}

View 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()
}
}()
}

View 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()
}
}

View File

@@ -0,0 +1 @@
在此目录放置 GORM 模型与请求/响应结构体,例如 User、Order、Withdrawal、Config 等。

View 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" }

View File

@@ -0,0 +1,24 @@
package model
import "time"
// Order 对应表 ordersJSON 输出与现网接口 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" }

View 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" }

View 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" }

View File

@@ -0,0 +1,35 @@
package model
import (
"database/sql/driver"
"time"
)
// ConfigValue 存 system_config.config_valueJSON 列,可为 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_configJSON 输出与现网 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" }

View File

@@ -0,0 +1,26 @@
package model
import "time"
// User 对应表 usersJSON 输出与现网接口 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" }

View 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" }

View 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" }

View File

@@ -0,0 +1 @@
在此目录放置数据库访问层,供 service 调用,例如 UserRepo、OrderRepo、ConfigRepo 等。

View 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
}

View File

@@ -0,0 +1 @@
在此目录放置业务逻辑,供 handler 调用,例如 AdminService、UserService、PaymentService 等。