更新小程序开发文档,新增2026-03-03的最佳实践记录,优化个人中心类页面的卡片区边距规范,确保一致性与可用性。调整相关页面以反映最新设计稿,提升用户体验与功能一致性。

This commit is contained in:
Alex-larget
2026-03-04 19:06:06 +08:00
parent 7064f82126
commit 5a5f0087d2
66 changed files with 2555 additions and 1059 deletions

View File

@@ -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 鉴权检查JWTAuthorization 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},
})
}

View File

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

View File

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

View File

@@ -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 {

View File

@@ -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"`

View File

@@ -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},
}

View File

@@ -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"

View File

@@ -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,
}
}