更新小程序开发文档,新增2026-03-03的最佳实践记录,优化个人中心类页面的卡片区边距规范,确保一致性与可用性。调整相关页面以反映最新设计稿,提升用户体验与功能一致性。
This commit is contained in:
@@ -5,8 +5,12 @@ import (
|
||||
|
||||
"soul-api/internal/auth"
|
||||
"soul-api/internal/config"
|
||||
"soul-api/internal/database"
|
||||
"soul-api/internal/model"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// AdminCheck GET /api/admin 鉴权检查(JWT:Authorization Bearer 或 Cookie),已登录返回 success 或概览占位
|
||||
@@ -39,7 +43,7 @@ func AdminCheck(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// AdminLogin POST /api/admin 登录(校验 ADMIN_USERNAME/PASSWORD,返回 JWT,前端存 token 并带 Authorization: Bearer)
|
||||
// AdminLogin POST /api/admin 登录(优先校验 admin_users 表,表空时回退 ADMIN_USERNAME/PASSWORD 并自动初始化)
|
||||
func AdminLogin(c *gin.Context) {
|
||||
cfg := config.Get()
|
||||
if cfg == nil {
|
||||
@@ -56,11 +60,69 @@ func AdminLogin(c *gin.Context) {
|
||||
}
|
||||
username := trimSpace(body.Username)
|
||||
password := body.Password
|
||||
db := database.DB()
|
||||
|
||||
// 1. 尝试从 admin_users 表校验
|
||||
var u model.AdminUser
|
||||
err := db.Where("username = ?", username).First(&u).Error
|
||||
if err == nil {
|
||||
if u.Status != "active" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "账号已禁用"})
|
||||
return
|
||||
}
|
||||
if bcryptErr := bcrypt.CompareHashAndPassword([]byte(u.PasswordHash), []byte(password)); bcryptErr != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
token, err := auth.IssueAdminJWT(cfg.AdminSessionSecret, u.Username, u.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "签发失败"})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": token,
|
||||
"user": gin.H{"id": u.ID, "username": u.Username, "role": u.Role, "name": u.Name},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 2. 表内无匹配:若表为空且 env 账号正确,则创建初始 super_admin 并登录
|
||||
if err != gorm.ErrRecordNotFound {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "系统错误"})
|
||||
return
|
||||
}
|
||||
if cfg.AdminUsername == "" || cfg.AdminPassword == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
if username != cfg.AdminUsername || password != cfg.AdminPassword {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
token, err := auth.IssueAdminJWT(cfg.AdminSessionSecret, username)
|
||||
// 表为空时初始化超级管理员
|
||||
var cnt int64
|
||||
if db.Model(&model.AdminUser{}).Count(&cnt).Error != nil || cnt > 0 {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"success": false, "error": "用户名或密码错误"})
|
||||
return
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "初始化失败"})
|
||||
return
|
||||
}
|
||||
initial := model.AdminUser{
|
||||
Username: cfg.AdminUsername,
|
||||
PasswordHash: string(hash),
|
||||
Role: "super_admin",
|
||||
Name: "卡若",
|
||||
Status: "active",
|
||||
}
|
||||
if err := db.Create(&initial).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "初始化失败"})
|
||||
return
|
||||
}
|
||||
token, err := auth.IssueAdminJWT(cfg.AdminSessionSecret, initial.Username, initial.Role)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"success": false, "error": "签发失败"})
|
||||
return
|
||||
@@ -68,9 +130,7 @@ func AdminLogin(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{
|
||||
"success": true,
|
||||
"token": token,
|
||||
"user": gin.H{
|
||||
"id": "admin", "username": cfg.AdminUsername, "role": "admin", "name": "卡若",
|
||||
},
|
||||
"user": gin.H{"id": initial.ID, "username": initial.Username, "role": initial.Role, "name": initial.Name},
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -84,6 +84,9 @@ func AdminChaptersAction(c *gin.Context) {
|
||||
var body struct {
|
||||
Action string `json:"action"`
|
||||
ID string `json:"id"`
|
||||
ChapterID string `json:"chapterId"` // 前端兼容:section id
|
||||
SectionTitle string `json:"sectionTitle"`
|
||||
Ids []string `json:"ids"` // reorder:新顺序的 section id 列表
|
||||
Price *float64 `json:"price"`
|
||||
IsFree *bool `json:"isFree"`
|
||||
Status *string `json:"status"`
|
||||
@@ -94,26 +97,63 @@ func AdminChaptersAction(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
resolveID := func() string {
|
||||
if body.ID != "" {
|
||||
return body.ID
|
||||
}
|
||||
return body.ChapterID
|
||||
}
|
||||
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)
|
||||
}
|
||||
if body.Action == "updateEdition" && body.ID != "" {
|
||||
updates := make(map[string]interface{})
|
||||
if body.EditionStandard != nil {
|
||||
updates["edition_standard"] = *body.EditionStandard
|
||||
if body.Action == "updatePrice" {
|
||||
id := resolveID()
|
||||
if id != "" && body.Price != nil {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", id).Update("price", *body.Price)
|
||||
}
|
||||
if body.EditionPremium != nil {
|
||||
updates["edition_premium"] = *body.EditionPremium
|
||||
}
|
||||
if body.Action == "toggleFree" {
|
||||
id := resolveID()
|
||||
if id != "" && body.IsFree != nil {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", id).Update("is_free", *body.IsFree)
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", body.ID).Updates(updates)
|
||||
}
|
||||
if body.Action == "updateStatus" {
|
||||
id := resolveID()
|
||||
if id != "" && body.Status != nil {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", id).Update("status", *body.Status)
|
||||
}
|
||||
}
|
||||
if body.Action == "rename" {
|
||||
id := resolveID()
|
||||
if id != "" && body.SectionTitle != "" {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", id).Update("section_title", body.SectionTitle)
|
||||
}
|
||||
}
|
||||
if body.Action == "delete" {
|
||||
id := resolveID()
|
||||
if id != "" {
|
||||
db.Where("id = ?", id).Delete(&model.Chapter{})
|
||||
}
|
||||
}
|
||||
if body.Action == "reorder" && len(body.Ids) > 0 {
|
||||
for i, id := range body.Ids {
|
||||
if id != "" {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", id).Update("sort_order", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
if body.Action == "updateEdition" {
|
||||
id := resolveID()
|
||||
if id != "" {
|
||||
updates := make(map[string]interface{})
|
||||
if body.EditionStandard != nil {
|
||||
updates["edition_standard"] = *body.EditionStandard
|
||||
}
|
||||
if body.EditionPremium != nil {
|
||||
updates["edition_premium"] = *body.EditionPremium
|
||||
}
|
||||
if len(updates) > 0 {
|
||||
db.Model(&model.Chapter{}).Where("id = ?", id).Updates(updates)
|
||||
}
|
||||
}
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
|
||||
@@ -16,9 +16,18 @@ import (
|
||||
var excludeParts = []string{"序言", "尾声", "附录"}
|
||||
|
||||
// BookAllChapters GET /api/book/all-chapters 返回所有章节(列表,来自 chapters 表)
|
||||
// 排序须与管理端 PUT /api/db/book action=reorder 一致:按 sort_order 升序,同序按 id
|
||||
// COALESCE 处理 sort_order 为 NULL 的旧数据,避免错位
|
||||
// 支持 excludeFixed=1:排除序言、尾声、附录(目录页固定模块,不参与中间篇章)
|
||||
func BookAllChapters(c *gin.Context) {
|
||||
q := database.DB().Model(&model.Chapter{})
|
||||
if c.Query("excludeFixed") == "1" {
|
||||
for _, p := range excludeParts {
|
||||
q = q.Where("part_title NOT LIKE ?", "%"+p+"%")
|
||||
}
|
||||
}
|
||||
var list []model.Chapter
|
||||
if err := database.DB().Order("sort_order ASC, id ASC").Find(&list).Error; err != nil {
|
||||
if err := q.Order("COALESCE(sort_order, 999999) ASC, id ASC").Find(&list).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": []interface{}{}})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -16,9 +16,8 @@ import (
|
||||
)
|
||||
|
||||
// GetPublicDBConfig GET /api/miniprogram/config 公开接口,供小程序获取完整配置(与 next-project 对齐)
|
||||
// 从 system_config 读取 free_chapters、mp_config、feature_config、chapter_config,合并后返回
|
||||
// 从 system_config 读取 chapter_config、feature_config、mp_config,合并后返回(免费以章节 is_free/price 为准)
|
||||
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}
|
||||
apiDomain := "https://soulapi.quwanzhi.com"
|
||||
@@ -36,16 +35,15 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
}
|
||||
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"freeChapters": defaultFree,
|
||||
"prices": defaultPrices,
|
||||
"features": defaultFeatures,
|
||||
"mpConfig": defaultMp,
|
||||
"configs": gin.H{}, // 兼容 miniprogram 备用格式 res.configs.feature_config
|
||||
"success": true,
|
||||
"prices": defaultPrices,
|
||||
"features": defaultFeatures,
|
||||
"mpConfig": defaultMp,
|
||||
"configs": gin.H{},
|
||||
}
|
||||
db := database.DB()
|
||||
|
||||
keys := []string{"chapter_config", "free_chapters", "feature_config", "mp_config"}
|
||||
keys := []string{"chapter_config", "feature_config", "mp_config"}
|
||||
for _, k := range keys {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", k).First(&row).Error; err != nil {
|
||||
@@ -58,17 +56,6 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
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
|
||||
}
|
||||
@@ -77,19 +64,6 @@ func GetPublicDBConfig(c *gin.Context) {
|
||||
}
|
||||
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 里的
|
||||
@@ -158,7 +132,7 @@ func DBConfigGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": data})
|
||||
}
|
||||
|
||||
// AdminSettingsGet GET /api/admin/settings 系统设置页专用:仅返回免费章节、功能开关、站点/作者与价格、小程序配置
|
||||
// AdminSettingsGet GET /api/admin/settings 系统设置页专用:仅返回功能开关、站点/作者与价格、小程序配置
|
||||
func AdminSettingsGet(c *gin.Context) {
|
||||
db := database.DB()
|
||||
apiDomain := "https://soulapi.quwanzhi.com"
|
||||
@@ -174,12 +148,11 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
}
|
||||
out := gin.H{
|
||||
"success": true,
|
||||
"freeChapters": []string{"preface", "epilogue", "1.1", "appendix-1", "appendix-2", "appendix-3"},
|
||||
"featureConfig": gin.H{"matchEnabled": true, "referralEnabled": true, "searchEnabled": true, "aboutEnabled": true},
|
||||
"siteSettings": gin.H{"sectionPrice": float64(1), "baseBookPrice": 9.9, "distributorShare": float64(90), "authorInfo": gin.H{}},
|
||||
"mpConfig": defaultMp,
|
||||
}
|
||||
keys := []string{"free_chapters", "feature_config", "site_settings", "mp_config"}
|
||||
keys := []string{"feature_config", "site_settings", "mp_config"}
|
||||
for _, k := range keys {
|
||||
var row model.SystemConfig
|
||||
if err := db.Where("config_key = ?", k).First(&row).Error; err != nil {
|
||||
@@ -190,18 +163,6 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
switch k {
|
||||
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
|
||||
}
|
||||
}
|
||||
case "feature_config":
|
||||
if m, ok := val.(map[string]interface{}); ok && len(m) > 0 {
|
||||
out["featureConfig"] = m
|
||||
@@ -226,10 +187,9 @@ func AdminSettingsGet(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, out)
|
||||
}
|
||||
|
||||
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存免费章节、功能开关、站点/作者与价格、小程序配置
|
||||
// AdminSettingsPost POST /api/admin/settings 系统设置页专用:一次性保存功能开关、站点/作者与价格、小程序配置
|
||||
func AdminSettingsPost(c *gin.Context) {
|
||||
var body struct {
|
||||
FreeChapters []string `json:"freeChapters"`
|
||||
FeatureConfig map[string]interface{} `json:"featureConfig"`
|
||||
SiteSettings map[string]interface{} `json:"siteSettings"`
|
||||
MpConfig map[string]interface{} `json:"mpConfig"`
|
||||
@@ -256,12 +216,6 @@ func AdminSettingsPost(c *gin.Context) {
|
||||
}
|
||||
return db.Save(&row).Error
|
||||
}
|
||||
if body.FreeChapters != nil {
|
||||
if err := saveKey("free_chapters", "免费章节ID列表", body.FreeChapters); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存免费章节失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.FeatureConfig != nil {
|
||||
if err := saveKey("feature_config", "功能开关配置", body.FeatureConfig); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "保存功能开关失败: " + err.Error()})
|
||||
@@ -366,6 +320,140 @@ func AdminReferralSettingsPost(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "message": "推广设置已保存"})
|
||||
}
|
||||
|
||||
func authorConfigToResponse(row *model.AuthorConfig) gin.H {
|
||||
defaultStats := []gin.H{{"label": "商业案例", "value": "62"}, {"label": "连续直播", "value": "365天"}, {"label": "派对分享", "value": "1000+"}}
|
||||
defaultHighlights := []string{"5年私域运营经验", "帮助100+品牌从0到1增长", "连续创业者,擅长商业模式设计"}
|
||||
var stats []gin.H
|
||||
if row.Stats != "" {
|
||||
_ = json.Unmarshal([]byte(row.Stats), &stats)
|
||||
}
|
||||
if len(stats) == 0 {
|
||||
stats = defaultStats
|
||||
}
|
||||
var highlights []string
|
||||
if row.Highlights != "" {
|
||||
_ = json.Unmarshal([]byte(row.Highlights), &highlights)
|
||||
}
|
||||
if len(highlights) == 0 {
|
||||
highlights = defaultHighlights
|
||||
}
|
||||
return gin.H{
|
||||
"name": row.Name,
|
||||
"avatar": row.Avatar,
|
||||
"avatarImg": row.AvatarImg,
|
||||
"title": row.Title,
|
||||
"bio": row.Bio,
|
||||
"stats": stats,
|
||||
"highlights": highlights,
|
||||
}
|
||||
}
|
||||
|
||||
// AdminAuthorSettingsGet GET /api/admin/author-settings 作者详情配置(管理端专用)
|
||||
func AdminAuthorSettingsGet(c *gin.Context) {
|
||||
defaultAuthor := gin.H{
|
||||
"name": "卡若",
|
||||
"avatar": "K",
|
||||
"avatarImg": "",
|
||||
"title": "Soul派对房主理人 · 私域运营专家",
|
||||
"bio": "每天早上6点到9点,在Soul派对房分享真实的创业故事。专注私域运营与项目变现,用云阿米巴模式帮助创业者构建可持续的商业体系。",
|
||||
"stats": []gin.H{{"label": "商业案例", "value": "62"}, {"label": "连续直播", "value": "365天"}, {"label": "派对分享", "value": "1000+"}},
|
||||
"highlights": []string{"5年私域运营经验", "帮助100+品牌从0到1增长", "连续创业者,擅长商业模式设计"},
|
||||
}
|
||||
db := database.DB()
|
||||
var row model.AuthorConfig
|
||||
if err := db.First(&row).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": defaultAuthor})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": authorConfigToResponse(&row)})
|
||||
}
|
||||
|
||||
// AdminAuthorSettingsPost POST /api/admin/author-settings 保存作者详情配置
|
||||
func AdminAuthorSettingsPost(c *gin.Context) {
|
||||
var body map[string]interface{}
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
str := func(k string) string {
|
||||
if v, ok := body[k]; ok && v != nil {
|
||||
if s, ok := v.(string); ok {
|
||||
return s
|
||||
}
|
||||
return fmt.Sprintf("%v", v)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
name := str("name")
|
||||
if name == "" {
|
||||
name = "卡若"
|
||||
}
|
||||
avatar := str("avatar")
|
||||
if avatar == "" {
|
||||
avatar = "K"
|
||||
}
|
||||
statsVal := body["stats"]
|
||||
if statsVal == nil {
|
||||
statsVal = []gin.H{{"label": "商业案例", "value": "62"}, {"label": "连续直播", "value": "365天"}, {"label": "派对分享", "value": "1000+"}}
|
||||
}
|
||||
highlightsVal := body["highlights"]
|
||||
if highlightsVal == nil {
|
||||
highlightsVal = []string{}
|
||||
}
|
||||
statsBytes, _ := json.Marshal(statsVal)
|
||||
highlightsBytes, _ := json.Marshal(highlightsVal)
|
||||
|
||||
db := database.DB()
|
||||
var row model.AuthorConfig
|
||||
err := db.First(&row).Error
|
||||
if err != nil {
|
||||
row = model.AuthorConfig{
|
||||
Name: name,
|
||||
Avatar: avatar,
|
||||
AvatarImg: str("avatarImg"),
|
||||
Title: str("title"),
|
||||
Bio: str("bio"),
|
||||
Stats: string(statsBytes),
|
||||
Highlights: string(highlightsBytes),
|
||||
}
|
||||
err = db.Create(&row).Error
|
||||
} else {
|
||||
row.Name = name
|
||||
row.Avatar = avatar
|
||||
row.AvatarImg = str("avatarImg")
|
||||
row.Title = str("title")
|
||||
row.Bio = str("bio")
|
||||
row.Stats = string(statsBytes)
|
||||
row.Highlights = string(highlightsBytes)
|
||||
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": "作者设置已保存"})
|
||||
}
|
||||
|
||||
// MiniprogramAboutAuthor GET /api/miniprogram/about/author 小程序-关于作者页拉取作者配置(公开,无需鉴权)
|
||||
func MiniprogramAboutAuthor(c *gin.Context) {
|
||||
defaultAuthor := gin.H{
|
||||
"name": "卡若",
|
||||
"avatar": "K",
|
||||
"avatarImg": "",
|
||||
"title": "Soul派对房主理人 · 私域运营专家",
|
||||
"bio": "每天早上6点到9点,在Soul派对房分享真实的创业故事。",
|
||||
"stats": []gin.H{{"label": "商业案例", "value": "62"}, {"label": "连续直播", "value": "365天"}, {"label": "派对分享", "value": "1000+"}},
|
||||
"highlights": []string{"5年私域运营经验", "帮助100+品牌从0到1增长", "连续创业者,擅长商业模式设计"},
|
||||
}
|
||||
db := database.DB()
|
||||
var row model.AuthorConfig
|
||||
if err := db.First(&row).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": defaultAuthor})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"success": true, "data": authorConfigToResponse(&row)})
|
||||
}
|
||||
|
||||
// DBConfigPost POST /api/db/config
|
||||
func DBConfigPost(c *gin.Context) {
|
||||
var body struct {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
|
||||
"soul-api/internal/database"
|
||||
@@ -10,6 +11,12 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// listSelectCols 列表/导出不加载 content,大幅加速
|
||||
var listSelectCols = []string{
|
||||
"id", "section_title", "price", "is_free", "is_new",
|
||||
"part_id", "part_title", "chapter_id", "chapter_title", "sort_order",
|
||||
}
|
||||
|
||||
// sectionListItem 与前端 SectionListItem 一致(小写驼峰)
|
||||
type sectionListItem struct {
|
||||
ID string `json:"id"`
|
||||
@@ -34,7 +41,7 @@ func DBBookAction(c *gin.Context) {
|
||||
switch action {
|
||||
case "list":
|
||||
var rows []model.Chapter
|
||||
if err := db.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
if err := db.Select(listSelectCols).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
|
||||
}
|
||||
@@ -93,7 +100,7 @@ func DBBookAction(c *gin.Context) {
|
||||
return
|
||||
case "export":
|
||||
var rows []model.Chapter
|
||||
if err := db.Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
if err := db.Select(listSelectCols).Order("sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -179,7 +186,11 @@ func DBBookAction(c *gin.Context) {
|
||||
}
|
||||
case http.MethodPut:
|
||||
var body struct {
|
||||
ID string `json:"id"`
|
||||
Action string `json:"action"`
|
||||
// reorder:新顺序,支持跨篇跨章时附带 partId/chapterId
|
||||
IDs []string `json:"ids"`
|
||||
Items []reorderItem `json:"items"`
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
Price *float64 `json:"price"`
|
||||
@@ -188,8 +199,57 @@ func DBBookAction(c *gin.Context) {
|
||||
EditionStandard *bool `json:"editionStandard"` // 是否属于普通版
|
||||
EditionPremium *bool `json:"editionPremium"` // 是否属于增值版
|
||||
}
|
||||
if err := c.ShouldBindJSON(&body); err != nil || body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id 或请求体无效"})
|
||||
if err := c.ShouldBindJSON(&body); err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "请求体无效"})
|
||||
return
|
||||
}
|
||||
if body.Action == "reorder" {
|
||||
// 立即返回成功,后台异步执行排序更新
|
||||
if len(body.Items) > 0 {
|
||||
items := make([]reorderItem, len(body.Items))
|
||||
copy(items, body.Items)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
go func() {
|
||||
db := database.DB()
|
||||
for i, it := range items {
|
||||
if it.ID == "" {
|
||||
continue
|
||||
}
|
||||
up := map[string]interface{}{"sort_order": i}
|
||||
if it.PartID != "" {
|
||||
up["part_id"] = it.PartID
|
||||
}
|
||||
if it.PartTitle != "" {
|
||||
up["part_title"] = it.PartTitle
|
||||
}
|
||||
if it.ChapterID != "" {
|
||||
up["chapter_id"] = it.ChapterID
|
||||
}
|
||||
if it.ChapterTitle != "" {
|
||||
up["chapter_title"] = it.ChapterTitle
|
||||
}
|
||||
_ = db.WithContext(context.Background()).Model(&model.Chapter{}).Where("id = ?", it.ID).Updates(up).Error
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
if len(body.IDs) > 0 {
|
||||
ids := make([]string, len(body.IDs))
|
||||
copy(ids, body.IDs)
|
||||
c.JSON(http.StatusOK, gin.H{"success": true})
|
||||
go func() {
|
||||
db := database.DB()
|
||||
for i, id := range ids {
|
||||
if id != "" {
|
||||
_ = db.WithContext(context.Background()).Model(&model.Chapter{}).Where("id = ?", id).Update("sort_order", i).Error
|
||||
}
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
}
|
||||
if body.ID == "" {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 id"})
|
||||
return
|
||||
}
|
||||
price := 1.0
|
||||
@@ -228,6 +288,14 @@ func DBBookAction(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "不支持的请求方法"})
|
||||
}
|
||||
|
||||
type reorderItem struct {
|
||||
ID string `json:"id"`
|
||||
PartID string `json:"partId"`
|
||||
PartTitle string `json:"partTitle"`
|
||||
ChapterID string `json:"chapterId"`
|
||||
ChapterTitle string `json:"chapterTitle"`
|
||||
}
|
||||
|
||||
type importItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
|
||||
@@ -85,7 +85,7 @@ func GetMatchQuota(db *gorm.DB, userID string, freeLimit int) MatchQuota {
|
||||
var defaultMatchTypes = []gin.H{
|
||||
gin.H{"id": "partner", "label": "创业合伙", "matchLabel": "创业伙伴", "icon": "⭐", "matchFromDB": true, "showJoinAfterMatch": false, "price": 1, "enabled": true},
|
||||
gin.H{"id": "investor", "label": "资源对接", "matchLabel": "资源对接", "icon": "👥", "matchFromDB": false, "showJoinAfterMatch": true, "price": 1, "enabled": true},
|
||||
gin.H{"id": "mentor", "label": "导师顾问", "matchLabel": "商业顾问", "icon": "❤️", "matchFromDB": false, "showJoinAfterMatch": true, "price": 1, "enabled": true},
|
||||
gin.H{"id": "mentor", "label": "导师顾问", "matchLabel": "导师顾问", "icon": "❤️", "matchFromDB": false, "showJoinAfterMatch": true, "price": 1, "enabled": true},
|
||||
gin.H{"id": "team", "label": "团队招募", "matchLabel": "加入项目", "icon": "🎮", "matchFromDB": false, "showJoinAfterMatch": true, "price": 1, "enabled": true},
|
||||
}
|
||||
|
||||
|
||||
@@ -4,9 +4,13 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"soul-api/internal/database"
|
||||
@@ -17,6 +21,27 @@ import (
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var (
|
||||
orderPollLogger *log.Logger
|
||||
orderPollLoggerOnce sync.Once
|
||||
)
|
||||
|
||||
// orderPollLogf 将订单轮询检测日志写入 log/order-poll.log,不输出到控制台
|
||||
func orderPollLogf(format string, args ...interface{}) {
|
||||
orderPollLoggerOnce.Do(func() {
|
||||
_ = os.MkdirAll("log", 0755)
|
||||
f, err := os.OpenFile(filepath.Join("log", "order-poll.log"), os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
orderPollLogger = log.New(io.Discard, "", 0)
|
||||
return
|
||||
}
|
||||
orderPollLogger = log.New(f, "[OrderPoll] ", log.Ldate|log.Ltime)
|
||||
})
|
||||
if orderPollLogger != nil {
|
||||
orderPollLogger.Printf(format, args...)
|
||||
}
|
||||
}
|
||||
|
||||
// MiniprogramLogin POST /api/miniprogram/login
|
||||
func MiniprogramLogin(c *gin.Context) {
|
||||
var req struct {
|
||||
@@ -355,7 +380,7 @@ func miniprogramPayGet(c *gin.Context) {
|
||||
"transaction_id": transactionID,
|
||||
"pay_time": now,
|
||||
})
|
||||
fmt.Printf("[PayGet] 主动同步订单已支付: %s\n", orderSn)
|
||||
orderPollLogf("主动同步订单已支付: %s", orderSn)
|
||||
}
|
||||
case "CLOSED", "REVOKED", "PAYERROR":
|
||||
status = "failed"
|
||||
|
||||
@@ -272,7 +272,7 @@ func VipMembers(c *gin.Context) {
|
||||
}
|
||||
|
||||
// formatVipMember 构建会员展示数据;优先 vip_*,无则回退到用户 nickname/avatar
|
||||
// 用于首页超级个体、创业老板排行等场景,展示真实用户头像和昵称
|
||||
// 用于首页超级个体、创业老板排行、会员详情页等场景;含 P3 资料扩展以对接 member-detail
|
||||
func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
name := ""
|
||||
if u.VipName != nil && *u.VipName != "" {
|
||||
@@ -291,9 +291,9 @@ func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
if avatar == "" && u.Avatar != nil && *u.Avatar != "" {
|
||||
avatar = *u.Avatar
|
||||
}
|
||||
project := ""
|
||||
if u.VipProject != nil {
|
||||
project = *u.VipProject
|
||||
project := getStringValue(u.VipProject)
|
||||
if project == "" {
|
||||
project = getStringValue(u.ProjectIntro)
|
||||
}
|
||||
bio := ""
|
||||
if u.VipBio != nil {
|
||||
@@ -303,24 +303,51 @@ func formatVipMember(u *model.User, isVip bool) gin.H {
|
||||
if u.VipContact != nil {
|
||||
contact = *u.VipContact
|
||||
}
|
||||
if contact == "" {
|
||||
contact = getStringValue(u.Phone)
|
||||
}
|
||||
vipRole := ""
|
||||
if u.VipRole != nil {
|
||||
vipRole = *u.VipRole
|
||||
}
|
||||
return gin.H{
|
||||
"id": u.ID,
|
||||
"name": name,
|
||||
"nickname": name,
|
||||
"avatar": avatar,
|
||||
"vip_name": name,
|
||||
"vipName": name,
|
||||
"vipRole": vipRole,
|
||||
"vip_avatar": avatar,
|
||||
"vipAvatar": avatar,
|
||||
"vipProject": project,
|
||||
"vipContact": contact,
|
||||
"vipBio": bio,
|
||||
"is_vip": isVip,
|
||||
"id": u.ID,
|
||||
"name": name,
|
||||
"nickname": name,
|
||||
"avatar": avatar,
|
||||
"vip_name": name,
|
||||
"vipName": name,
|
||||
"vipRole": vipRole,
|
||||
"vip_avatar": avatar,
|
||||
"vipAvatar": avatar,
|
||||
"vipProject": project,
|
||||
"vip_project": project,
|
||||
"vipContact": contact,
|
||||
"vip_contact": contact,
|
||||
"vipBio": bio,
|
||||
"wechatId": getStringValue(u.WechatID),
|
||||
"wechat_id": getStringValue(u.WechatID),
|
||||
"phone": getStringValue(u.Phone),
|
||||
"mbti": getStringValue(u.Mbti),
|
||||
"region": getStringValue(u.Region),
|
||||
"industry": getStringValue(u.Industry),
|
||||
"position": getStringValue(u.Position),
|
||||
"businessScale": getStringValue(u.BusinessScale),
|
||||
"business_scale": getStringValue(u.BusinessScale),
|
||||
"skills": getStringValue(u.Skills),
|
||||
"storyBestMonth": getStringValue(u.StoryBestMonth),
|
||||
"story_best_month": getStringValue(u.StoryBestMonth),
|
||||
"storyAchievement": getStringValue(u.StoryAchievement),
|
||||
"story_achievement": getStringValue(u.StoryAchievement),
|
||||
"storyTurning": getStringValue(u.StoryTurning),
|
||||
"story_turning": getStringValue(u.StoryTurning),
|
||||
"helpOffer": getStringValue(u.HelpOffer),
|
||||
"help_offer": getStringValue(u.HelpOffer),
|
||||
"helpNeed": getStringValue(u.HelpNeed),
|
||||
"help_need": getStringValue(u.HelpNeed),
|
||||
"projectIntro": getStringValue(u.ProjectIntro),
|
||||
"project_intro": getStringValue(u.ProjectIntro),
|
||||
"is_vip": isVip,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user