Files
soul-yongping/soul-api/internal/handler/db.go

474 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package handler
import (
"encoding/json"
"fmt"
"net/http"
"time"
"soul-api/internal/database"
"soul-api/internal/model"
"github.com/gin-gonic/gin"
)
// GetPublicDBConfig GET /api/miniprogram/config 公开接口,供小程序获取完整配置(与 next-project 对齐)
// 从 system_config 读取 free_chapters、mp_config、feature_config、chapter_config合并后返回
func GetPublicDBConfig(c *gin.Context) {
defaultFree := []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"}
defaultPrices := gin.H{"section": float64(1), "fullbook": 9.9}
defaultFeatures := gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true}
defaultMp := gin.H{"appId": "wxb8bbb2b10dec74aa", "apiDomain": "https://soul.quwanzhi.com", "buyerDiscount": 5, "referralBindDays": 30, "minWithdraw": 10}
out := gin.H{
"success": true,
"freeChapters": defaultFree,
"prices": defaultPrices,
"features": defaultFeatures,
"mpConfig": defaultMp,
"configs": gin.H{}, // 兼容 miniprogram 备用格式 res.configs.feature_config
}
db := database.DB()
keys := []string{"chapter_config", "free_chapters", "feature_config", "mp_config"}
for _, k := range keys {
var row model.SystemConfig
if err := db.Where("config_key = ?", k).First(&row).Error; err != nil {
continue
}
var val interface{}
if err := json.Unmarshal(row.ConfigValue, &val); err != nil {
continue
}
switch k {
case "chapter_config":
if m, ok := val.(map[string]interface{}); ok {
if v, ok := m["freeChapters"].([]interface{}); ok && len(v) > 0 {
arr := make([]string, 0, len(v))
for _, x := range v {
if s, ok := x.(string); ok {
arr = append(arr, s)
}
}
if len(arr) > 0 {
out["freeChapters"] = arr
}
}
if v, ok := m["prices"].(map[string]interface{}); ok {
out["prices"] = v
}
if v, ok := m["features"].(map[string]interface{}); ok {
out["features"] = v
}
out["configs"].(gin.H)["chapter_config"] = m
}
case "free_chapters":
if arr, ok := val.([]interface{}); ok && len(arr) > 0 {
ss := make([]string, 0, len(arr))
for _, x := range arr {
if s, ok := x.(string); ok {
ss = append(ss, s)
}
}
if len(ss) > 0 {
out["freeChapters"] = ss
}
out["configs"].(gin.H)["free_chapters"] = arr
}
case "feature_config":
if m, ok := val.(map[string]interface{}); ok {
// 合并到 features不整体覆盖以保留 chapter_config 里的
cur := out["features"].(gin.H)
for kk, vv := range m {
cur[kk] = vv
}
out["configs"].(gin.H)["feature_config"] = m
}
case "mp_config":
if m, ok := val.(map[string]interface{}); ok {
out["mpConfig"] = m
out["configs"].(gin.H)["mp_config"] = m
}
}
}
c.JSON(http.StatusOK, out)
}
// DBConfigGet GET /api/db/config管理端鉴权后同路径由 db 组处理时用)
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/外部执行"})
}