diff --git a/handlers/article_edit_test.go b/handlers/article_edit_test.go
new file mode 100644
index 0000000..c16b4f1
--- /dev/null
+++ b/handlers/article_edit_test.go
@@ -0,0 +1,68 @@
+package handlers
+
+import (
+ "net/http"
+ "strconv"
+ "strings"
+ "testing"
+
+ "go_blog/models"
+)
+
+// TestArticleDetailEditButton 验证文章页编辑按钮的可见性:
+// - 文章作者(普通用户)可见,链接指向 /my/articles/:id/edit
+// - 管理员对所有文章可见,链接指向 /admin/articles/:id/edit
+// - 其他登录用户与未登录访客不可见
+func TestArticleDetailEditButton(t *testing.T) {
+ e := newSecurityTestEnv(t)
+
+ var aliceArt models.Article
+ if err := e.db.Where("slug = ?", "alice-post").First(&aliceArt).Error; err != nil {
+ t.Fatalf("alice article not found: %v", err)
+ }
+ id := strconv.FormatUint(uint64(aliceArt.ID), 10)
+ myEdit := "/my/articles/" + id + "/edit"
+ adminEdit := "/admin/articles/" + id + "/edit"
+
+ // 未登录访客:两种链接都不得出现。
+ w := e.do(http.MethodGet, "/article/alice-post", "", nil, "")
+ if w.Code != http.StatusOK {
+ t.Fatalf("anonymous GET article: status = %d", w.Code)
+ }
+ if strings.Contains(w.Body.String(), myEdit) || strings.Contains(w.Body.String(), adminEdit) {
+ t.Fatal("anonymous viewer must not see any edit button")
+ }
+
+ // 文章作者:看到 /my/articles/:id/edit。
+ alice := e.login(t, "alice")
+ w = e.do(http.MethodGet, "/article/alice-post", alice, nil, "")
+ if w.Code != http.StatusOK {
+ t.Fatalf("author GET article: status = %d", w.Code)
+ }
+ if !strings.Contains(w.Body.String(), myEdit) {
+ t.Fatal("author should see their own edit button")
+ }
+ if strings.Contains(w.Body.String(), adminEdit) {
+ t.Fatal("author must not see the admin edit button")
+ }
+
+ // 其他作者:不可见。
+ bob := e.login(t, "bob")
+ w = e.do(http.MethodGet, "/article/alice-post", bob, nil, "")
+ if w.Code != http.StatusOK {
+ t.Fatalf("other author GET article: status = %d", w.Code)
+ }
+ if strings.Contains(w.Body.String(), myEdit) || strings.Contains(w.Body.String(), adminEdit) {
+ t.Fatal("other author must not see the edit button")
+ }
+
+ // 管理员:看到 /admin/articles/:id/edit。
+ admin := e.login(t, "admin")
+ w = e.do(http.MethodGet, "/article/alice-post", admin, nil, "")
+ if w.Code != http.StatusOK {
+ t.Fatalf("admin GET article: status = %d", w.Code)
+ }
+ if !strings.Contains(w.Body.String(), adminEdit) {
+ t.Fatal("admin should see the admin edit button")
+ }
+}
diff --git a/handlers/home.go b/handlers/home.go
index 601acc1..6225d4c 100644
--- a/handlers/home.go
+++ b/handlers/home.go
@@ -248,6 +248,23 @@ func renderArticleDetail(c *gin.Context, db *gorm.DB, article *models.Article, f
data["CommentError"] = formErr
data["CommentNotice"] = notice
data["MaxCommentLength"] = MaxCommentLength
+
+ // 文章页编辑按钮:管理员可编辑全部文章;登录用户仅可编辑自己的文章
+ // (普通作者跳转 /my/articles/:id/edit,编辑页/接口均有 author_id 所有权约束)。
+ canEdit := false
+ editURL := ""
+ uid := userIDFromSession(c)
+ role, _ := c.Get("role")
+ if r, _ := role.(string); r == models.RoleAdmin {
+ canEdit = true
+ editURL = fmt.Sprintf("/admin/articles/%d/edit", article.ID)
+ } else if uid != 0 && uid == article.AuthorID {
+ canEdit = true
+ editURL = fmt.Sprintf("/my/articles/%d/edit", article.ID)
+ }
+ data["CanEdit"] = canEdit
+ data["EditURL"] = editURL
+
c.HTML(http.StatusOK, "article", data)
}
diff --git a/handlers/security_test.go b/handlers/security_test.go
index a4b7670..449ec29 100644
--- a/handlers/security_test.go
+++ b/handlers/security_test.go
@@ -90,6 +90,7 @@ func newSecurityTestEnv(t *testing.T) *securityTestEnv {
r.GET("/login", LoginPage())
r.GET("/register", RegisterPage(db))
r.GET("/rss", RSSFeed(db))
+ r.GET("/article/:slug", ArticleDetail(db))
api := r.Group("/api")
{
diff --git a/templates/pages/article.html b/templates/pages/article.html
index 00540ea..1be6421 100644
--- a/templates/pages/article.html
+++ b/templates/pages/article.html
@@ -7,8 +7,8 @@
← {{index .Tr "article_back_home"}}
- {{if eq .Role "admin"}}
-