- Changed API base URLs in app.js to point to the production server for deployment. - Updated the my page UI by refining labels and removing unnecessary text for a cleaner look. - Adjusted CSS styles to enhance layout and reduce whitespace, improving overall user experience. This update aims to ensure proper API connectivity and enhance the visual presentation of the user interface.
347 lines
9.5 KiB
Go
347 lines
9.5 KiB
Go
package handler
|
||
|
||
import (
|
||
"encoding/json"
|
||
"net/http"
|
||
"strconv"
|
||
"strings"
|
||
|
||
"soul-api/internal/database"
|
||
"soul-api/internal/model"
|
||
|
||
"github.com/gin-gonic/gin"
|
||
"gorm.io/gorm"
|
||
)
|
||
|
||
type superArticleAuthorRow struct {
|
||
ID string `gorm:"column:id"`
|
||
Nickname string `gorm:"column:nickname"`
|
||
Avatar string `gorm:"column:avatar"`
|
||
}
|
||
|
||
func loadSuperArticleAuthorMap(ids []string) map[string]superArticleAuthorRow {
|
||
out := map[string]superArticleAuthorRow{}
|
||
if len(ids) == 0 {
|
||
return out
|
||
}
|
||
var rows []superArticleAuthorRow
|
||
db := database.DB()
|
||
_ = db.Table("users").Select("id", "nickname", "avatar").Where("id IN ?", ids).Find(&rows).Error
|
||
for _, r := range rows {
|
||
key := strings.TrimSpace(r.ID)
|
||
if key == "" {
|
||
continue
|
||
}
|
||
out[key] = r
|
||
}
|
||
return out
|
||
}
|
||
|
||
const (
|
||
superArticleMaxImages = 9
|
||
superArticleMaxImageURLLen = 2048
|
||
)
|
||
|
||
func parseSuperArticleImagesJSON(raw string) []string {
|
||
raw = strings.TrimSpace(raw)
|
||
if raw == "" {
|
||
return nil
|
||
}
|
||
var arr []string
|
||
if err := json.Unmarshal([]byte(raw), &arr); err != nil {
|
||
return nil
|
||
}
|
||
out := make([]string, 0, len(arr))
|
||
for _, u := range arr {
|
||
u = strings.TrimSpace(u)
|
||
if u != "" {
|
||
out = append(out, u)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// superArticleFeedAllowedAuthorIDs 文章中出现过、且当前仍为超级个体的用户 ID(用于动态广场)
|
||
func superArticleFeedAllowedAuthorIDs(db *gorm.DB) []string {
|
||
var raw []string
|
||
_ = db.Model(&model.SuperArticle{}).Distinct("user_id").Pluck("user_id", &raw).Error
|
||
out := make([]string, 0, len(raw))
|
||
seen := map[string]bool{}
|
||
for _, uid := range raw {
|
||
uid = strings.TrimSpace(uid)
|
||
if uid == "" || seen[uid] {
|
||
continue
|
||
}
|
||
seen[uid] = true
|
||
if _, ok := miniprogramSuperIndividualPerson(db, uid); ok {
|
||
out = append(out, uid)
|
||
}
|
||
}
|
||
return out
|
||
}
|
||
|
||
// MiniprogramSuperArticleFeed GET /api/miniprogram/super/articles/feed?page=&pageSize=
|
||
// 动态广场:当前仍为超级个体的作者所发文章,按创建时间倒序分页。
|
||
func MiniprogramSuperArticleFeed(c *gin.Context) {
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 30 {
|
||
pageSize = 10
|
||
}
|
||
|
||
db := database.DB()
|
||
allowed := superArticleFeedAllowedAuthorIDs(db)
|
||
if len(allowed) == 0 {
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "list": []gin.H{}, "total": int64(0), "page": page, "pageSize": pageSize})
|
||
return
|
||
}
|
||
|
||
q := db.Model(&model.SuperArticle{}).Where("user_id IN ?", allowed)
|
||
var total int64
|
||
if err := q.Count(&total).Error; err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
|
||
var rows []model.SuperArticle
|
||
if err := db.Where("user_id IN ?", allowed).
|
||
Order("created_at DESC").
|
||
Offset((page - 1) * pageSize).
|
||
Limit(pageSize).
|
||
Find(&rows).Error; err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
|
||
idSet := map[string]bool{}
|
||
var authorIDs []string
|
||
for _, r := range rows {
|
||
uid := strings.TrimSpace(r.UserID)
|
||
if uid == "" || idSet[uid] {
|
||
continue
|
||
}
|
||
idSet[uid] = true
|
||
authorIDs = append(authorIDs, uid)
|
||
}
|
||
authorMap := loadSuperArticleAuthorMap(authorIDs)
|
||
|
||
list := make([]gin.H, 0, len(rows))
|
||
for _, r := range rows {
|
||
content := strings.TrimSpace(r.Content)
|
||
preview := content
|
||
if len([]rune(preview)) > 120 {
|
||
preview = string([]rune(preview)[:120]) + "..."
|
||
}
|
||
a := authorMap[strings.TrimSpace(r.UserID)]
|
||
list = append(list, gin.H{
|
||
"id": r.ID,
|
||
"userId": r.UserID,
|
||
"title": r.Title,
|
||
"content": r.Content,
|
||
"preview": preview,
|
||
"images": parseSuperArticleImagesJSON(r.Images),
|
||
"authorNickname": strings.TrimSpace(a.Nickname),
|
||
"authorAvatar": strings.TrimSpace(a.Avatar),
|
||
"createdAt": r.CreatedAt,
|
||
})
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"list": list,
|
||
"total": total,
|
||
"page": page,
|
||
"pageSize": pageSize,
|
||
})
|
||
}
|
||
|
||
func normalizeSuperArticleImageURLs(in []string) ([]string, string) {
|
||
if len(in) == 0 {
|
||
return []string{}, ""
|
||
}
|
||
if len(in) > superArticleMaxImages {
|
||
return nil, "配图最多 9 张"
|
||
}
|
||
out := make([]string, 0, len(in))
|
||
for _, u := range in {
|
||
u = strings.TrimSpace(u)
|
||
if u == "" {
|
||
continue
|
||
}
|
||
if len(u) > superArticleMaxImageURLLen {
|
||
return nil, "图片链接过长"
|
||
}
|
||
low := strings.ToLower(u)
|
||
if !strings.HasPrefix(low, "http://") && !strings.HasPrefix(low, "https://") {
|
||
return nil, "图片链接须以 http(s):// 开头"
|
||
}
|
||
out = append(out, u)
|
||
}
|
||
if len(out) > superArticleMaxImages {
|
||
return nil, "配图最多 9 张"
|
||
}
|
||
return out, ""
|
||
}
|
||
|
||
// MiniprogramSuperArticleCreate POST /api/miniprogram/super/articles
|
||
// 仅超级个体本人可发文章。
|
||
func MiniprogramSuperArticleCreate(c *gin.Context) {
|
||
var req struct {
|
||
UserID string `json:"userId"`
|
||
Title string `json:"title"`
|
||
Content string `json:"content"`
|
||
Images []string `json:"images"`
|
||
}
|
||
if err := c.ShouldBindJSON(&req); err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
|
||
return
|
||
}
|
||
req.UserID = strings.TrimSpace(req.UserID)
|
||
req.Title = strings.TrimSpace(req.Title)
|
||
req.Content = strings.TrimSpace(req.Content)
|
||
if req.UserID == "" || req.Title == "" || req.Content == "" {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "标题和正文不能为空"})
|
||
return
|
||
}
|
||
if len([]rune(req.Title)) > 40 {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "标题最多 40 字"})
|
||
return
|
||
}
|
||
if len([]rune(req.Content)) > 5000 {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "正文最多 5000 字"})
|
||
return
|
||
}
|
||
|
||
imgURLs, imgErr := normalizeSuperArticleImageURLs(req.Images)
|
||
if imgErr != "" {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": imgErr})
|
||
return
|
||
}
|
||
imgJSONBytes, jerr := json.Marshal(imgURLs)
|
||
if jerr != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "配图数据无效"})
|
||
return
|
||
}
|
||
|
||
db := database.DB()
|
||
if _, ok := miniprogramSuperIndividualPerson(db, req.UserID); !ok {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅超级个体可发文章", "forbidden": true})
|
||
return
|
||
}
|
||
|
||
row := model.SuperArticle{
|
||
UserID: req.UserID,
|
||
Title: req.Title,
|
||
Content: req.Content,
|
||
Images: string(imgJSONBytes),
|
||
}
|
||
if err := db.Create(&row).Error; err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "data": gin.H{"id": row.ID}})
|
||
}
|
||
|
||
// MiniprogramSuperArticleList GET /api/miniprogram/super/articles?authorUserId=&page=&pageSize=
|
||
func MiniprogramSuperArticleList(c *gin.Context) {
|
||
authorUserID := strings.TrimSpace(c.Query("authorUserId"))
|
||
if authorUserID == "" {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 authorUserId"})
|
||
return
|
||
}
|
||
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
||
pageSize, _ := strconv.Atoi(c.DefaultQuery("pageSize", "10"))
|
||
if page < 1 {
|
||
page = 1
|
||
}
|
||
if pageSize < 1 || pageSize > 30 {
|
||
pageSize = 10
|
||
}
|
||
|
||
db := database.DB()
|
||
if _, ok := miniprogramSuperIndividualPerson(db, authorUserID); !ok {
|
||
c.JSON(http.StatusOK, gin.H{"success": true, "list": []gin.H{}, "total": 0, "page": page, "pageSize": pageSize})
|
||
return
|
||
}
|
||
|
||
var total int64
|
||
db.Model(&model.SuperArticle{}).Where("user_id = ?", authorUserID).Count(&total)
|
||
var rows []model.SuperArticle
|
||
if err := db.Where("user_id = ?", authorUserID).
|
||
Order("created_at DESC").
|
||
Offset((page - 1) * pageSize).
|
||
Limit(pageSize).
|
||
Find(&rows).Error; err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
|
||
return
|
||
}
|
||
|
||
authorMap := loadSuperArticleAuthorMap([]string{authorUserID})
|
||
author := authorMap[authorUserID]
|
||
list := make([]gin.H, 0, len(rows))
|
||
for _, r := range rows {
|
||
content := strings.TrimSpace(r.Content)
|
||
preview := content
|
||
if len([]rune(preview)) > 88 {
|
||
preview = string([]rune(preview)[:88]) + "..."
|
||
}
|
||
list = append(list, gin.H{
|
||
"id": r.ID,
|
||
"userId": r.UserID,
|
||
"title": r.Title,
|
||
"content": r.Content,
|
||
"preview": preview,
|
||
"images": parseSuperArticleImagesJSON(r.Images),
|
||
"authorNickname": strings.TrimSpace(author.Nickname),
|
||
"authorAvatar": strings.TrimSpace(author.Avatar),
|
||
"createdAt": r.CreatedAt,
|
||
})
|
||
}
|
||
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"list": list,
|
||
"total": total,
|
||
"page": page,
|
||
"pageSize": pageSize,
|
||
})
|
||
}
|
||
|
||
// MiniprogramSuperArticleDetail GET /api/miniprogram/super/articles/:id
|
||
func MiniprogramSuperArticleDetail(c *gin.Context) {
|
||
idRaw := strings.TrimSpace(c.Param("id"))
|
||
id, err := strconv.ParseUint(idRaw, 10, 64)
|
||
if err != nil || id == 0 {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
|
||
return
|
||
}
|
||
db := database.DB()
|
||
var row model.SuperArticle
|
||
if err := db.First(&row, id).Error; err != nil {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "文章不存在"})
|
||
return
|
||
}
|
||
if _, ok := miniprogramSuperIndividualPerson(db, row.UserID); !ok {
|
||
c.JSON(http.StatusOK, gin.H{"success": false, "error": "文章不可见"})
|
||
return
|
||
}
|
||
authorMap := loadSuperArticleAuthorMap([]string{row.UserID})
|
||
author := authorMap[row.UserID]
|
||
c.JSON(http.StatusOK, gin.H{
|
||
"success": true,
|
||
"data": gin.H{
|
||
"id": row.ID,
|
||
"userId": row.UserID,
|
||
"title": row.Title,
|
||
"content": row.Content,
|
||
"images": parseSuperArticleImagesJSON(row.Images),
|
||
"authorNickname": strings.TrimSpace(author.Nickname),
|
||
"authorAvatar": strings.TrimSpace(author.Avatar),
|
||
"createdAt": row.CreatedAt,
|
||
},
|
||
})
|
||
}
|