392 lines
12 KiB
Go
392 lines
12 KiB
Go
|
|
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/外部执行"})
|
|||
|
|
}
|