feat: add super article management features and enhance UI

- Introduced a new page for managing user-specific articles, allowing users to view and edit their submissions.
- Implemented article audit status handling, including visual indicators for pending, approved, and rejected articles.
- Enhanced the article detail view with audit feedback and editing capabilities for articles in pending or rejected states.
- Updated the UI to dynamically reflect ownership and article status, improving user experience and clarity.

This update aims to streamline article management for users and provide better feedback on article submission statuses.
This commit is contained in:
乘风
2026-05-09 17:49:45 +08:00
parent c84c53515e
commit 99d65dc664
28 changed files with 1526 additions and 94 deletions

View File

@@ -64,7 +64,10 @@ func parseSuperArticleImagesJSON(raw string) []string {
// superArticleFeedAllowedAuthorIDs 文章中出现过、且当前仍为超级个体的用户 ID用于动态广场
func superArticleFeedAllowedAuthorIDs(db *gorm.DB) []string {
var raw []string
_ = db.Model(&model.SuperArticle{}).Distinct("user_id").Pluck("user_id", &raw).Error
_ = db.Model(&model.SuperArticle{}).
Where("audit_status = ?", model.SuperArticleAuditApproved).
Distinct("user_id").
Pluck("user_id", &raw).Error
out := make([]string, 0, len(raw))
seen := map[string]bool{}
for _, uid := range raw {
@@ -99,7 +102,9 @@ func MiniprogramSuperArticleFeed(c *gin.Context) {
return
}
q := db.Model(&model.SuperArticle{}).Where("user_id IN ?", allowed)
q := db.Model(&model.SuperArticle{}).
Where("user_id IN ?", allowed).
Where("audit_status = ?", model.SuperArticleAuditApproved)
var total int64
if err := q.Count(&total).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
@@ -108,6 +113,7 @@ func MiniprogramSuperArticleFeed(c *gin.Context) {
var rows []model.SuperArticle
if err := db.Where("user_id IN ?", allowed).
Where("audit_status = ?", model.SuperArticleAuditApproved).
Order("created_at DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
@@ -233,10 +239,16 @@ func MiniprogramSuperArticleCreate(c *gin.Context) {
}
row := model.SuperArticle{
UserID: req.UserID,
Title: req.Title,
Content: req.Content,
Images: string(imgJSONBytes),
UserID: req.UserID,
Title: req.Title,
Content: req.Content,
Images: string(imgJSONBytes),
AuditStatus: model.SuperArticleAuditPending,
RejectReason: "",
}
if !SuperArticleAuditEnabled() {
row.AuditStatus = model.SuperArticleAuditApproved
row.RejectReason = ""
}
if err := db.Create(&row).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
@@ -268,9 +280,13 @@ func MiniprogramSuperArticleList(c *gin.Context) {
}
var total int64
db.Model(&model.SuperArticle{}).Where("user_id = ?", authorUserID).Count(&total)
db.Model(&model.SuperArticle{}).
Where("user_id = ?", authorUserID).
Where("audit_status = ?", model.SuperArticleAuditApproved).
Count(&total)
var rows []model.SuperArticle
if err := db.Where("user_id = ?", authorUserID).
Where("audit_status = ?", model.SuperArticleAuditApproved).
Order("created_at DESC").
Offset((page - 1) * pageSize).
Limit(pageSize).
@@ -310,6 +326,78 @@ func MiniprogramSuperArticleList(c *gin.Context) {
})
}
// MiniprogramSuperArticleMine GET /api/miniprogram/super/articles/mine?userId=&page=&pageSize=
// 本人全部动态(含待审/通过/驳回),仅超级个体可查。
func MiniprogramSuperArticleMine(c *gin.Context) {
userID := strings.TrimSpace(c.Query("userId"))
if userID == "" {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "缺少 userId"})
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, userID); !ok {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅超级个体可查看", "forbidden": true})
return
}
var total int64
db.Model(&model.SuperArticle{}).Where("user_id = ?", userID).Count(&total)
var rows []model.SuperArticle
if err := db.Where("user_id = ?", userID).
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{userID})
author := authorMap[userID]
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]) + "..."
}
ast := strings.TrimSpace(strings.ToLower(r.AuditStatus))
if ast == "" {
ast = model.SuperArticleAuditApproved
}
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,
"auditStatus": ast,
"rejectReason": strings.TrimSpace(r.RejectReason),
})
}
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"))
@@ -318,6 +406,7 @@ func MiniprogramSuperArticleDetail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "参数错误"})
return
}
viewer := strings.TrimSpace(c.Query("viewerUserId"))
db := database.DB()
var row model.SuperArticle
if err := db.First(&row, id).Error; err != nil {
@@ -328,6 +417,16 @@ func MiniprogramSuperArticleDetail(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "文章不可见"})
return
}
if !model.SuperArticlePublicApproved(row.AuditStatus) {
if viewer == "" || viewer != strings.TrimSpace(row.UserID) {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "文章审核中或不可见"})
return
}
}
ast := strings.TrimSpace(strings.ToLower(row.AuditStatus))
if ast == "" {
ast = model.SuperArticleAuditApproved
}
authorMap := loadSuperArticleAuthorMap([]string{row.UserID})
author := authorMap[row.UserID]
c.JSON(http.StatusOK, gin.H{
@@ -341,6 +440,94 @@ func MiniprogramSuperArticleDetail(c *gin.Context) {
"authorNickname": strings.TrimSpace(author.Nickname),
"authorAvatar": strings.TrimSpace(author.Avatar),
"createdAt": row.CreatedAt,
"auditStatus": ast,
"rejectReason": strings.TrimSpace(row.RejectReason),
},
})
}
// MiniprogramSuperArticleUpdate PUT /api/miniprogram/super/articles/:id
// 仅作者本人;状态须为待审核或已驳回;提交后重置为待审核。
func MiniprogramSuperArticleUpdate(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
}
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()
var row model.SuperArticle
if err := db.First(&row, id).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "文章不存在"})
return
}
if strings.TrimSpace(row.UserID) != req.UserID {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "无权修改"})
return
}
if _, ok := miniprogramSuperIndividualPerson(db, req.UserID); !ok {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅超级个体可修改", "forbidden": true})
return
}
st := strings.TrimSpace(strings.ToLower(row.AuditStatus))
if st == "" {
st = model.SuperArticleAuditApproved
}
if st != model.SuperArticleAuditPending && st != model.SuperArticleAuditRejected {
c.JSON(http.StatusOK, gin.H{"success": false, "error": "仅待审核或已驳回的动态可修改"})
return
}
row.Title = req.Title
row.Content = req.Content
row.Images = string(imgJSONBytes)
if SuperArticleAuditEnabled() {
row.AuditStatus = model.SuperArticleAuditPending
row.RejectReason = ""
} else {
row.AuditStatus = model.SuperArticleAuditApproved
row.RejectReason = ""
}
if err := db.Save(&row).Error; err != nil {
c.JSON(http.StatusOK, gin.H{"success": false, "error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"success": true})
}