删除过期文件
This commit is contained in:
-163
@@ -1,163 +0,0 @@
|
||||
# RSS订阅功能实现计划
|
||||
|
||||
## 概述
|
||||
为Go博客系统添加RSS 2.0订阅功能,允许用户通过RSS阅读器订阅博客的最新文章。
|
||||
|
||||
## 当前项目分析
|
||||
|
||||
### 技术栈
|
||||
- **框架**: Gin (HTTP框架)
|
||||
- **ORM**: GORM
|
||||
- **数据库**: SQLite/MySQL (可配置)
|
||||
- **模板**: Go HTML templates
|
||||
|
||||
### 现有结构
|
||||
1. **路由**: 在 `main.go` 中集中注册
|
||||
2. **处理器**: 在 `handlers/` 目录下,按功能分文件
|
||||
3. **模型**: 在 `models/` 目录下
|
||||
4. **文章模型** (`models/article.go`):
|
||||
- 包含标题、摘要、内容、作者、发布时间等字段
|
||||
- 有状态管理(草稿/已发布/已归档)
|
||||
- 支持slug用于URL
|
||||
5. **主页** (`handlers/home.go`):
|
||||
- 已有获取已发布文章的逻辑
|
||||
- 按置顶和发布时间排序
|
||||
- 限制返回10篇文章
|
||||
|
||||
### 发现
|
||||
- 项目没有现有的RSS或feed相关代码
|
||||
- 没有RSS相关的Go依赖包
|
||||
- 项目支持中英文双语(i18n)
|
||||
- 有网站设置模型 (`SiteSetting`),包含网站标题、描述等信息
|
||||
|
||||
## 实现方案
|
||||
|
||||
### 方案选择
|
||||
**推荐方案**: 使用标准库 `encoding/xml` 手动构建RSS 2.0格式
|
||||
- **优点**:
|
||||
- 无需引入新依赖
|
||||
- RSS 2.0格式简单明确
|
||||
- 完全控制输出格式
|
||||
- 符合项目轻量化原则
|
||||
- **缺点**:
|
||||
- 需要手动定义XML结构
|
||||
|
||||
**备选方案**: 使用第三方库如 `github.com/gorilla/feeds`
|
||||
- **优点**: 简化RSS生成,支持RSS/Atom/JSON Feed
|
||||
- **缺点**: 引入新依赖
|
||||
|
||||
**决策**: 采用推荐方案,使用标准库实现RSS 2.0
|
||||
|
||||
### 实现步骤
|
||||
|
||||
#### 1. 创建RSS模型和生成器 (`handlers/rss.go`)
|
||||
- 定义RSS 2.0的XML结构体:
|
||||
- `RSS` (根元素)
|
||||
- `Channel` (频道信息)
|
||||
- `Item` (文章项)
|
||||
- 实现 `RSSFeed` 处理器函数:
|
||||
- 从数据库获取最近20篇已发布文章(包含作者信息)
|
||||
- 从 `SiteSetting` 获取网站标题和描述
|
||||
- 根据当前语言决定使用中文或英文的网站信息
|
||||
- 构建RSS XML结构
|
||||
- 设置正确的Content-Type: `application/rss+xml; charset=utf-8`
|
||||
- 返回XML响应
|
||||
|
||||
#### 2. RSS内容规范
|
||||
- **Channel级别**:
|
||||
- `title`: 网站标题(从 `SiteSetting.LogoText` 获取)
|
||||
- `link`: 网站首页URL
|
||||
- `description`: 网站描述(从 `SiteSetting.HomeSubtitle` 获取)
|
||||
- `language`: zh-CN 或 en-US(根据当前语言)
|
||||
- `lastBuildDate`: 最新文章的发布时间
|
||||
|
||||
- **Item级别**:
|
||||
- `title`: 文章标题
|
||||
- `link`: 文章详情页URL(使用slug)
|
||||
- `description`: 文章摘要(如果有)或内容前200字符
|
||||
- `author`: 作者用户名或显示名称
|
||||
- `pubDate`: 文章发布时间(RFC822格式)
|
||||
- `guid`: 文章的唯一标识(使用文章详情URL)
|
||||
|
||||
#### 3. 注册路由 (`main.go`)
|
||||
在public路由区域添加:
|
||||
```go
|
||||
router.GET("/rss", handlers.RSSFeed(db))
|
||||
router.GET("/feed", handlers.RSSFeed(db)) // 别名,增加兼容性
|
||||
```
|
||||
|
||||
#### 4. 可选:在模板中添加RSS链接
|
||||
在HTML head中添加RSS自动发现标签:
|
||||
```html
|
||||
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="/rss">
|
||||
```
|
||||
|
||||
### 技术细节
|
||||
|
||||
#### URL生成
|
||||
- 需要构建完整的文章URL(包含域名)
|
||||
- 从请求的 `Host` 头获取域名
|
||||
- 构建格式: `http(s)://domain/article/{slug}`
|
||||
|
||||
#### 时间格式
|
||||
- RSS 2.0要求使用RFC822格式
|
||||
- Go time包: `time.RFC1123Z` 或手动格式化
|
||||
|
||||
#### 文章内容处理
|
||||
- 优先使用 `Summary` 字段
|
||||
- 如果没有摘要,截取 `Content` 前200字符
|
||||
- 需要处理HTML标签(strip或escape)
|
||||
|
||||
#### 字符编码
|
||||
- 确保XML声明中包含 `encoding="UTF-8"`
|
||||
- Gin自动处理UTF-8编码
|
||||
|
||||
### 安全考虑
|
||||
- RSS是只读接口,不需要认证
|
||||
- 只返回状态为"已发布"的文章
|
||||
- XSS防护:XML自动转义特殊字符
|
||||
|
||||
### 性能考虑
|
||||
- 限制返回文章数量(20篇)
|
||||
- 使用数据库索引(status和published_at字段已有索引)
|
||||
- 可以考虑添加缓存(后续优化)
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新建文件
|
||||
1. `handlers/rss.go` - RSS处理器和XML结构定义
|
||||
|
||||
### 修改文件
|
||||
1. `main.go` - 添加RSS路由
|
||||
2. 可选:`templates/layouts/base.html` - 添加RSS自动发现标签
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 手动测试
|
||||
1. 启动服务器
|
||||
2. 访问 `http://localhost:PORT/rss`
|
||||
3. 验证返回的XML格式正确
|
||||
4. 检查Content-Type头
|
||||
5. 使用RSS阅读器(如Feedly)订阅测试
|
||||
|
||||
### 测试点
|
||||
- [ ] RSS XML格式符合RSS 2.0规范
|
||||
- [ ] 包含最新的已发布文章
|
||||
- [ ] 文章链接可点击且正确
|
||||
- [ ] 中英文切换正常工作
|
||||
- [ ] 时间格式正确
|
||||
- [ ] 特殊字符正确转义
|
||||
- [ ] 在RSS阅读器中可正常显示
|
||||
|
||||
## 实现优先级
|
||||
1. **核心功能**: RSS feed端点,返回最新文章
|
||||
2. **可选优化**:
|
||||
- 在页面头部添加RSS自动发现标签
|
||||
- 添加RSS订阅链接到导航栏
|
||||
- 实现缓存机制
|
||||
|
||||
## 注意事项
|
||||
- 保持代码风格与现有项目一致
|
||||
- 遵循项目的命名约定
|
||||
- 添加适当的注释(中英文)
|
||||
- 确保RSS feed在中英文环境下都能正常工作
|
||||
@@ -1,191 +0,0 @@
|
||||
# Navigation Links Management - Implementation Plan
|
||||
|
||||
## Overview
|
||||
Add a navigation links management feature that allows admins to configure custom links in the header navigation, with support for controlling whether links open in new windows.
|
||||
|
||||
## Requirements Analysis
|
||||
Based on exploration:
|
||||
- User wants to add additional URLs next to "Home" in the header navigation
|
||||
- Backend admin interface to manage these URLs
|
||||
- Control over whether links open in new windows (target="_blank")
|
||||
- The site uses a multi-language system (zh/en)
|
||||
|
||||
## Current Architecture Patterns
|
||||
|
||||
### Database Models
|
||||
- Models are in `/models/` directory
|
||||
- Settings tables follow singleton pattern (ID=1) like `SiteSetting`, `CommentConfig`
|
||||
- Support for multi-language via `*Zh` and `*En` fields
|
||||
- Use GORM with auto-migration in `models/db.go`
|
||||
- Config cache system exists (`models/config_cache.go`, `models.LoadConfigCache()`, `models.RefreshConfigCache()`)
|
||||
|
||||
### Handlers
|
||||
- Settings handlers in `handlers/settings.go`
|
||||
- Pattern: `{Feature}SettingsPage()` for GET, `{Feature}SettingsSave()` for POST
|
||||
- Uses `DefaultData(c)` helper for common template data
|
||||
- Multi-action POST pattern with `action` form field (see upload/download settings)
|
||||
- Session-based user ID tracking via `userIDFromSession()`
|
||||
|
||||
### Templates
|
||||
- Admin templates in `templates/admin/`
|
||||
- Settings pages follow consistent UI pattern (tabs, success messages)
|
||||
- Header navigation in `templates/layouts/base.html` (lines 24-89)
|
||||
- Current navigation shows: Logo/Title, Home link, Login/User dropdown
|
||||
|
||||
### Routing
|
||||
- Admin routes under `/admin` group with auth + admin middleware
|
||||
- Settings routes: `/admin/settings/site`, `/admin/settings/upload`, etc.
|
||||
|
||||
### i18n
|
||||
- Translation keys in `i18n/i18n.go`
|
||||
- Follows pattern: `"feature_field_lang"` or `"feature_action"`
|
||||
- Both EN and ZH translations required
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
### 1. Database Model: `NavLink`
|
||||
Create `models/nav_link.go`:
|
||||
```go
|
||||
type NavLink struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
TitleZh string `gorm:"size:100;not null"` // Link text (Chinese)
|
||||
TitleEn string `gorm:"size:100;not null"` // Link text (English)
|
||||
URL string `gorm:"size:512;not null"` // Target URL
|
||||
OpenNew bool `gorm:"default:false"` // Open in new window
|
||||
Enabled bool `gorm:"default:true"` // Show/hide link
|
||||
Sort int `gorm:"default:0;index"` // Display order
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy uint `gorm:"index"`
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
func (n *NavLink) Title(lang string) string {
|
||||
// Return title for language with fallback
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Database Migration
|
||||
Add to `models/db.go` `AutoMigrate()`:
|
||||
- Add `&NavLink{}` to the migration list
|
||||
|
||||
### 3. Config Cache Integration
|
||||
Update `models/config_cache.go`:
|
||||
- Add `NavLinks []NavLink` field to cache struct
|
||||
- Load nav links in `LoadConfigCache()`
|
||||
- Make available to all handlers via middleware
|
||||
|
||||
### 4. Backend Handler
|
||||
Add to `handlers/settings.go`:
|
||||
|
||||
```go
|
||||
// NavLinksSettingsPage - render nav links management page
|
||||
func NavLinksSettingsPage(db *gorm.DB) gin.HandlerFunc
|
||||
|
||||
// NavLinksSettingsSave - handle actions: add, toggle, delete, reorder
|
||||
func NavLinksSettingsSave(db *gorm.DB) gin.HandlerFunc
|
||||
|
||||
// Helper functions:
|
||||
// - addNavLink()
|
||||
// - toggleNavLink()
|
||||
// - deleteNavLink()
|
||||
// - reorderNavLink()
|
||||
```
|
||||
|
||||
Actions via POST form field:
|
||||
- `action=add` - create new link
|
||||
- `action=toggle` - enable/disable link
|
||||
- `action=delete` - remove link
|
||||
- `action=edit` - update link details
|
||||
|
||||
### 5. Admin Template
|
||||
Create `templates/admin/settings_navlinks.html`:
|
||||
- Settings page with tab navigation matching existing pattern
|
||||
- Form to add new links (Title ZH/EN, URL, Open in new window checkbox)
|
||||
- List of existing links with:
|
||||
- Enable/disable toggle
|
||||
- Edit inline or modal
|
||||
- Delete button
|
||||
- Sort order controls (up/down arrows or drag)
|
||||
- Success message display
|
||||
- Consistent styling with other settings pages
|
||||
|
||||
### 6. Frontend Template Updates
|
||||
Update `templates/layouts/base.html`:
|
||||
- Modify navigation section (around line 36-42) to render nav links
|
||||
- Loop through cached nav links after "Home" link
|
||||
- Apply language-specific titles
|
||||
- Add `target="_blank"` when `OpenNew` is true
|
||||
- Maintain consistent styling with existing nav items
|
||||
|
||||
### 7. Routing
|
||||
Add to `main.go` settings group (around line 110-121):
|
||||
```go
|
||||
settings.GET("/navlinks", handlers.NavLinksSettingsPage(db))
|
||||
settings.POST("/navlinks", handlers.NavLinksSettingsSave(db))
|
||||
```
|
||||
|
||||
### 8. i18n Translations
|
||||
Add to `i18n/i18n.go` for both EN and ZH:
|
||||
```
|
||||
"settings_navlinks_title": "Navigation Links"
|
||||
"settings_navlinks_desc": "Manage custom links in the header navigation"
|
||||
"navlinks_add": "Add Link"
|
||||
"navlinks_title_zh": "Link Text (Chinese)"
|
||||
"navlinks_title_en": "Link Text (English)"
|
||||
"navlinks_url": "URL"
|
||||
"navlinks_open_new": "Open in new window"
|
||||
"navlinks_enabled": "Enabled"
|
||||
"navlinks_edit": "Edit"
|
||||
"navlinks_delete": "Delete"
|
||||
"navlinks_confirm_delete": "Are you sure you want to delete this link?"
|
||||
"navlinks_no_links": "No navigation links configured yet."
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Create database model** (`models/nav_link.go`)
|
||||
2. **Update database migration** (`models/db.go`)
|
||||
3. **Update config cache** (`models/config_cache.go`)
|
||||
4. **Add i18n translations** (`i18n/i18n.go`)
|
||||
5. **Create backend handlers** (`handlers/settings.go`)
|
||||
6. **Create admin template** (`templates/admin/settings_navlinks.html`)
|
||||
7. **Update frontend header** (`templates/layouts/base.html`)
|
||||
8. **Add routes** (`main.go`)
|
||||
9. **Test the feature**
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Why not singleton table?
|
||||
- Multiple nav links needed (vs single config)
|
||||
- Better suited for a regular table with multiple rows
|
||||
- Easier to add/remove/reorder individual links
|
||||
|
||||
### Sort order implementation
|
||||
- Use integer `Sort` field
|
||||
- Lower numbers appear first
|
||||
- Admin can adjust via up/down buttons or explicit number input
|
||||
|
||||
### Cache integration
|
||||
- Nav links loaded into cache at startup and on refresh
|
||||
- Avoids DB query on every page load
|
||||
- Consistent with existing upload/comment config pattern
|
||||
|
||||
### Open in new window
|
||||
- Boolean field `OpenNew`
|
||||
- Renders as `target="_blank" rel="noopener noreferrer"` when true
|
||||
- Security: always include `rel="noopener noreferrer"` with `target="_blank"`
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Create new nav link via admin
|
||||
- [ ] Nav link appears in header navigation
|
||||
- [ ] Correct language displayed based on user preference
|
||||
- [ ] "Open in new window" works correctly
|
||||
- [ ] Enable/disable toggle works
|
||||
- [ ] Delete link works
|
||||
- [ ] Sort order affects display order
|
||||
- [ ] Links render correctly for logged-in and anonymous users
|
||||
- [ ] Multiple links display properly
|
||||
- [ ] External and internal URLs both work
|
||||
- [ ] Mobile responsive layout maintained
|
||||
@@ -1,420 +0,0 @@
|
||||
# Tag and Search Feature - Implementation Plan
|
||||
|
||||
## Overview
|
||||
Add a comprehensive tag management system with search functionality to allow users to categorize articles with tags and search articles by keywords.
|
||||
|
||||
## Requirements
|
||||
Based on user request:
|
||||
1. **Tag Management**: Add tag functionality to articles
|
||||
2. **Tag Association**: When creating/editing articles, users can associate tags
|
||||
3. **Auto Tag Creation**: If a tag doesn't exist, create it automatically
|
||||
4. **Tag Display**: Show all tags in homepage right sidebar
|
||||
5. **Tag Filtering**: Filter articles by clicking on tags
|
||||
6. **Search Functionality**: Add search box in header to search articles by keywords
|
||||
|
||||
## Current Architecture Patterns
|
||||
|
||||
### Database Models
|
||||
- Models are in `/models/` directory
|
||||
- Use GORM with auto-migration in `models/db.go`
|
||||
- Multi-language support via `*Zh` and `*En` fields pattern
|
||||
- Article model in `models/article.go`
|
||||
|
||||
### Handlers
|
||||
- Article handlers in `handlers/article.go` and `handlers/home.go`
|
||||
- Settings handlers in `handlers/settings.go`
|
||||
- Pattern: `{Feature}Page()` for GET, `{Feature}()` for POST
|
||||
- Uses `DefaultData(c)` helper for common template data
|
||||
|
||||
### Templates
|
||||
- Home page at `templates/pages/home.html`
|
||||
- Article creation/edit at `templates/admin/article_create.html`
|
||||
- Layout header at `templates/layouts/base.html`
|
||||
|
||||
### Routing
|
||||
- Main routes in `main.go`
|
||||
- Public routes for home, article detail, search
|
||||
- Admin routes under `/admin` group with auth middleware
|
||||
|
||||
### i18n
|
||||
- Translation keys in `i18n/i18n.go`
|
||||
- Both EN and ZH translations required
|
||||
|
||||
## Proposed Implementation
|
||||
|
||||
### 1. Database Models
|
||||
|
||||
#### `models/tag.go`
|
||||
```go
|
||||
type Tag struct {
|
||||
ID uint `gorm:"primarykey"`
|
||||
NameZh string `gorm:"size:50;uniqueIndex:idx_tag_name_zh"`
|
||||
NameEn string `gorm:"size:50;uniqueIndex:idx_tag_name_en"`
|
||||
Slug string `gorm:"size:100;uniqueIndex"`
|
||||
Count int `gorm:"default:0"` // Article count cache
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// Helper method to get tag name by language
|
||||
func (t *Tag) Name(lang string) string {
|
||||
if lang == "zh" {
|
||||
return t.NameZh
|
||||
}
|
||||
return t.NameEn
|
||||
}
|
||||
```
|
||||
|
||||
#### `models/article_tag.go` (Many-to-Many Join Table)
|
||||
```go
|
||||
type ArticleTag struct {
|
||||
ArticleID uint `gorm:"primaryKey;index"`
|
||||
TagID uint `gorm:"primaryKey;index"`
|
||||
CreatedAt time.Time
|
||||
}
|
||||
```
|
||||
|
||||
#### Update `models/article.go`
|
||||
```go
|
||||
type Article struct {
|
||||
// ... existing fields ...
|
||||
Tags []Tag `gorm:"many2many:article_tags;"`
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Database Migration
|
||||
Update `models/db.go` `AutoMigrate()`:
|
||||
- Add `&Tag{}` and `&ArticleTag{}` to the migration list
|
||||
|
||||
### 3. Tag Management Functions
|
||||
|
||||
#### Add to `models/tag.go`:
|
||||
```go
|
||||
// FindOrCreateTag finds a tag by name or creates it if not exists
|
||||
func FindOrCreateTag(db *gorm.DB, nameZh, nameEn string) (*Tag, error)
|
||||
|
||||
// GetAllTags returns all tags ordered by count descending
|
||||
func GetAllTags(db *gorm.DB) ([]Tag, error)
|
||||
|
||||
// GetTagBySlug returns a tag by its slug
|
||||
func GetTagBySlug(db *gorm.DB, slug string) (*Tag, error)
|
||||
|
||||
// UpdateTagCount recalculates article count for a tag
|
||||
func UpdateTagCount(db *gorm.DB, tagID uint) error
|
||||
```
|
||||
|
||||
### 4. Article Handler Updates
|
||||
|
||||
#### Update `handlers/article.go`:
|
||||
|
||||
**Modify `articleForm` struct:**
|
||||
```go
|
||||
type articleForm struct {
|
||||
// ... existing fields ...
|
||||
Tags string // Comma-separated tag names
|
||||
}
|
||||
```
|
||||
|
||||
**Modify `parseArticleForm`:**
|
||||
```go
|
||||
func parseArticleForm(c *gin.Context) articleForm {
|
||||
return articleForm{
|
||||
// ... existing fields ...
|
||||
Tags: strings.TrimSpace(c.PostForm("tags")),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modify `applyFormToData`:**
|
||||
```go
|
||||
func applyFormToData(data gin.H, f articleForm) {
|
||||
// ... existing assignments ...
|
||||
data["FormTags"] = f.Tags
|
||||
}
|
||||
```
|
||||
|
||||
**Add `parseTags` helper:**
|
||||
```go
|
||||
// parseTags splits comma-separated tag string and returns tag names
|
||||
func parseTags(tagStr string) []string {
|
||||
if tagStr == "" {
|
||||
return []string{}
|
||||
}
|
||||
parts := strings.Split(tagStr, ",")
|
||||
var tags []string
|
||||
for _, part := range parts {
|
||||
trimmed := strings.TrimSpace(part)
|
||||
if trimmed != "" {
|
||||
tags = append(tags, trimmed)
|
||||
}
|
||||
}
|
||||
return tags
|
||||
}
|
||||
```
|
||||
|
||||
**Add `syncArticleTags` helper:**
|
||||
```go
|
||||
// syncArticleTags associates tags with an article (find or create tags)
|
||||
func syncArticleTags(db *gorm.DB, article *models.Article, tagNames []string) error
|
||||
```
|
||||
|
||||
**Modify `ArticleCreate` and `ArticleUpdate`:**
|
||||
- Parse tags from form
|
||||
- Call `syncArticleTags` after article is created/updated
|
||||
- Update tag counts
|
||||
|
||||
**Modify `ArticleEditPage`:**
|
||||
- Load article with tags preloaded
|
||||
- Format tags as comma-separated string for form
|
||||
|
||||
### 5. Search and Filter Handlers
|
||||
|
||||
#### Add to `handlers/home.go`:
|
||||
|
||||
```go
|
||||
// SearchArticles handles article search by keyword
|
||||
func SearchArticles(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
keyword := strings.TrimSpace(c.Query("q"))
|
||||
tagSlug := strings.TrimSpace(c.Query("tag"))
|
||||
|
||||
// Build query with filters
|
||||
// Return paginated results
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Modify `HomePage` and `HomeArticlesAPI`:**
|
||||
- Add tag filtering support via query parameter
|
||||
- Add search keyword filtering support
|
||||
|
||||
### 6. Template Updates
|
||||
|
||||
#### Update `templates/admin/article_create.html`:
|
||||
Add tag input field after content section:
|
||||
```html
|
||||
<!-- Tags -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">{{index .Tr "article_tags"}}</label>
|
||||
<input type="text" name="tags" value="{{.FormTags}}"
|
||||
class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors"
|
||||
placeholder="{{index .Tr "article_tags_hint"}}">
|
||||
<p class="text-xs text-gray-400 mt-1">{{index .Tr "article_tags_hint"}}</p>
|
||||
</div>
|
||||
```
|
||||
|
||||
#### Update `templates/pages/home.html`:
|
||||
|
||||
**Add search box in header (top section):**
|
||||
```html
|
||||
<section class="max-w-5xl mx-auto px-4 py-6">
|
||||
<div class="flex justify-center">
|
||||
<form action="/search" method="get" class="w-full max-w-2xl">
|
||||
<div class="relative">
|
||||
<input type="text" name="q" placeholder="{{index .Tr "search_placeholder"}}"
|
||||
class="w-full px-4 py-3 pl-12 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none transition-colors">
|
||||
<svg class="absolute left-4 top-1/2 transform -translate-y-1/2 w-5 h-5 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Add sidebar for tags:**
|
||||
```html
|
||||
<section class="max-w-7xl mx-auto px-4 py-8">
|
||||
<div class="flex gap-8">
|
||||
<!-- Main content (articles) -->
|
||||
<div class="flex-1">
|
||||
<!-- existing articles container -->
|
||||
</div>
|
||||
|
||||
<!-- Sidebar (tags) -->
|
||||
<aside class="w-64 flex-shrink-0 hidden lg:block">
|
||||
<div class="sticky top-6">
|
||||
<h3 class="text-lg font-bold text-gray-900 mb-4">{{index .Tr "tags_title"}}</h3>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{{range .Tags}}
|
||||
<a href="/?tag={{.Slug}}"
|
||||
class="px-3 py-1 text-sm bg-gray-100 hover:bg-blue-100 text-gray-700 hover:text-blue-700 rounded-full transition-colors">
|
||||
{{if eq $.Lang "zh"}}{{.NameZh}}{{else}}{{.NameEn}}{{end}}
|
||||
<span class="text-xs text-gray-500">({{.Count}})</span>
|
||||
</a>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
#### Create `templates/pages/search.html`:
|
||||
New template for search results page with similar layout to home.
|
||||
|
||||
### 7. Routing Updates
|
||||
|
||||
Update `main.go`:
|
||||
```go
|
||||
// Public routes
|
||||
router.GET("/", handlers.HomePage(db))
|
||||
router.GET("/search", handlers.SearchPage(db))
|
||||
router.GET("/api/articles", handlers.HomeArticlesAPI(db))
|
||||
```
|
||||
|
||||
### 8. i18n Translations
|
||||
|
||||
Add to `i18n/i18n.go` for both EN and ZH:
|
||||
```go
|
||||
// Tags
|
||||
"tags_title": "Tags"
|
||||
"article_tags": "Tags"
|
||||
"article_tags_hint": "Comma-separated tag names, e.g., golang, web, tutorial"
|
||||
"tag_filter": "Filter by tag"
|
||||
"tag_all": "All"
|
||||
|
||||
// Search
|
||||
"search_placeholder": "Search articles..."
|
||||
"search_title": "Search Results"
|
||||
"search_results_for": "Search results for"
|
||||
"search_no_results": "No articles found matching your search."
|
||||
"search_keyword": "Keyword"
|
||||
```
|
||||
|
||||
### 9. Homepage Handler Updates
|
||||
|
||||
Update `handlers/home.go`:
|
||||
|
||||
**Modify `HomePage`:**
|
||||
```go
|
||||
func HomePage(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
tr := getTr(c)
|
||||
data := DefaultData(c)
|
||||
|
||||
// Get tag filter if present
|
||||
tagSlug := c.Query("tag")
|
||||
|
||||
// Build query
|
||||
query := db.Where("status = ?", models.ArticlePublished)
|
||||
|
||||
if tagSlug != "" {
|
||||
// Join with article_tags to filter by tag
|
||||
query = query.Joins("JOIN article_tags ON article_tags.article_id = articles.id").
|
||||
Joins("JOIN tags ON tags.id = article_tags.tag_id").
|
||||
Where("tags.slug = ?", tagSlug)
|
||||
}
|
||||
|
||||
var articles []models.Article
|
||||
query.Preload("Tags").
|
||||
Order(publishedArticleOrder).
|
||||
Limit(10).
|
||||
Find(&articles)
|
||||
|
||||
// Load all tags for sidebar
|
||||
var tags []models.Tag
|
||||
db.Order("count DESC, name_zh ASC").Find(&tags)
|
||||
|
||||
data["Articles"] = articles
|
||||
data["Tags"] = tags
|
||||
if tagSlug != "" {
|
||||
data["FilterTag"] = tagSlug
|
||||
}
|
||||
|
||||
// ... existing comment counts logic ...
|
||||
|
||||
c.HTML(http.StatusOK, "home", data)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Add `SearchPage` handler:**
|
||||
```go
|
||||
func SearchPage(db *gorm.DB) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
keyword := strings.TrimSpace(c.Query("q"))
|
||||
// Search in title, summary, and content
|
||||
// Render search results template
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Implementation Order
|
||||
|
||||
1. **Create database models** (`models/tag.go`, `models/article_tag.go`)
|
||||
2. **Update article model** to add Tags relationship
|
||||
3. **Update database migration** (`models/db.go`)
|
||||
4. **Add tag helper functions** in `models/tag.go`
|
||||
5. **Add i18n translations** (`i18n/i18n.go`)
|
||||
6. **Update article handlers** (`handlers/article.go`) - form parsing, tag syncing
|
||||
7. **Update article templates** (`templates/admin/article_create.html`) - add tag input
|
||||
8. **Update home handler** (`handlers/home.go`) - add tag filtering, search
|
||||
9. **Update home template** (`templates/pages/home.html`) - add search box, tag sidebar
|
||||
10. **Create search page template** (`templates/pages/search.html`)
|
||||
11. **Add routes** (`main.go`)
|
||||
12. **Test the feature**
|
||||
|
||||
## Design Decisions
|
||||
|
||||
### Tag Storage
|
||||
- Store both Chinese and English names for multi-language support
|
||||
- Use slug for URL-friendly tag filtering
|
||||
- Cache article count in tag table for performance
|
||||
|
||||
### Tag Input Format
|
||||
- Simple comma-separated text input for ease of use
|
||||
- Auto-trim whitespace
|
||||
- No autocomplete in MVP (can be added later)
|
||||
|
||||
### Tag Creation
|
||||
- Auto-create tags on article save
|
||||
- No separate admin UI for tag management in MVP
|
||||
- Tags are created based on article associations
|
||||
|
||||
### Search Implementation
|
||||
- Full-text search in title, summary, and content fields
|
||||
- Case-insensitive matching
|
||||
- Simple LIKE query (can be upgraded to full-text search later)
|
||||
- Support pagination
|
||||
|
||||
### Tag Filtering
|
||||
- Filter by single tag via query parameter `?tag=slug`
|
||||
- Show filtered tag in UI for user clarity
|
||||
- Compatible with infinite scroll
|
||||
|
||||
### UI Layout
|
||||
- Search box prominent in header area
|
||||
- Tag sidebar on right (desktop only, hidden on mobile)
|
||||
- Tag cloud style with article count badges
|
||||
- Responsive design maintained
|
||||
|
||||
## Testing Checklist
|
||||
|
||||
- [ ] Create article with tags
|
||||
- [ ] Tags are saved and associated with article
|
||||
- [ ] Non-existent tags are created automatically
|
||||
- [ ] Edit article - existing tags are shown in form
|
||||
- [ ] Edit article - add/remove tags works
|
||||
- [ ] Tags appear in homepage sidebar
|
||||
- [ ] Tag count is accurate
|
||||
- [ ] Click tag filters articles correctly
|
||||
- [ ] Search box appears in header
|
||||
- [ ] Search by keyword finds matching articles
|
||||
- [ ] Search works with title matches
|
||||
- [ ] Search works with content matches
|
||||
- [ ] Tag filtering + search can work together
|
||||
- [ ] Pagination works with filters
|
||||
- [ ] Infinite scroll works with filters
|
||||
- [ ] Mobile responsive layout maintained
|
||||
- [ ] Multi-language support works (zh/en)
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
- Tag management admin page (rename, merge, delete tags)
|
||||
- Tag autocomplete/suggestions in article form
|
||||
- Tag popularity visualization
|
||||
- Related articles by shared tags
|
||||
- Full-text search engine (Elasticsearch, etc.)
|
||||
- Search suggestions/autocomplete
|
||||
- Advanced search filters (by date, author, etc.)
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(curl -s -c /tmp/cookies.txt -L -X POST http://localhost:8099/login -d \"username=admin&password=admin\" -o /dev/null -w \"login final HTTP %{http_code}\\\\n\")",
|
||||
"Bash(curl -s -b /tmp/cookies.txt http://localhost:8099/admin -o /tmp/dash.html -w \"HTTP %{http_code}\\\\n\")",
|
||||
"Read(//tmp/**)",
|
||||
"Bash(sed -n 's/.*\\\\\\(text-3xl font-bold text-gray-900 mt-1\">1\\\\n\\\\\\).*//p' /tmp/dash.html)",
|
||||
"Bash(perl -0777 -ne 'while\\(/dash_posts.*?text-3xl[^>]*>\\(.*?\\)<\\\\/p>/sg\\){print \"POSTS BLOCK: $1\\\\n\"}' /tmp/dash.html)",
|
||||
"Bash(python -c ' *)",
|
||||
"Bash(curl -s -b /tmp/cookies.txt http://localhost:8099/admin -o dash.html)",
|
||||
"Bash(rm -f dash.html)",
|
||||
"Bash(curl -s http://localhost:8099/article/123 -o detail.html -w \"detail HTTP %{http_code}\\\\n\")",
|
||||
"Bash(rm -f detail.html)",
|
||||
"Bash(cp /tmp/cfg_backup.yaml win/etc/blog_go/config.yaml)",
|
||||
"Bash(pkill -f blogtest.exe)",
|
||||
"Bash(pkill -f \"blog_go.exe\")",
|
||||
"Bash(cd c:/Users/wuwen/Documents/project/go_blog && rm -f /tmp/blogtest.exe && git status --short && echo \"--- diff stat ---\" && git diff --stat)"
|
||||
],
|
||||
"additionalDirectories": [
|
||||
"\\tmp"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
# 阅读统计系统 - 更新日志
|
||||
|
||||
## 2026-06-22 - v1.1
|
||||
|
||||
### ✨ 改进:文章筛选方式优化
|
||||
|
||||
**问题**:之前使用下拉框选择文章,当文章数量很多时不便使用。
|
||||
|
||||
**解决方案**:改为输入框模糊搜索
|
||||
|
||||
#### 变更内容
|
||||
|
||||
**旧版本(v1.0)**:
|
||||
- 使用 `<select>` 下拉框
|
||||
- 需要遍历所有文章填充选项
|
||||
- 文章多时下拉框很长,难以查找
|
||||
- 只能精确匹配文章ID
|
||||
|
||||
**新版本(v1.1)**:
|
||||
- 使用 `<input>` 文本输入框
|
||||
- 支持关键字模糊搜索
|
||||
- 输入文章标题的部分内容即可筛选
|
||||
- 使用 SQL `LIKE` 查询,支持中英文
|
||||
|
||||
#### 使用方法
|
||||
|
||||
在"筛选条件"区域的"文章标题"输入框中:
|
||||
- 输入完整标题:`Getting Started`
|
||||
- 或输入部分关键字:`start`
|
||||
- 支持中文:`入门`
|
||||
- 点击"应用筛选"查看结果
|
||||
|
||||
#### 技术细节
|
||||
|
||||
**Handler 变更**:
|
||||
```go
|
||||
// 旧代码
|
||||
articleIDStr := c.Query("article_id")
|
||||
if articleIDStr != "" {
|
||||
query = query.Where("article_id = ?", articleIDStr)
|
||||
}
|
||||
|
||||
// 新代码
|
||||
articleTitle := c.Query("article_title")
|
||||
if articleTitle != "" {
|
||||
query = query.Joins("JOIN articles ON article_views.article_id = articles.id").
|
||||
Where("articles.title LIKE ?", "%"+articleTitle+"%")
|
||||
}
|
||||
```
|
||||
|
||||
**模板变更**:
|
||||
```html
|
||||
<!-- 旧代码 -->
|
||||
<select name="article_id">
|
||||
<option value="">全部文章</option>
|
||||
{{range .Articles}}
|
||||
<option value="{{.ID}}">{{.Title}}</option>
|
||||
{{end}}
|
||||
</select>
|
||||
|
||||
<!-- 新代码 -->
|
||||
<input type="text" name="article_title"
|
||||
placeholder="搜索文章标题"
|
||||
value="{{.Filters.ArticleTitle}}">
|
||||
```
|
||||
|
||||
**翻译变更**:
|
||||
- `analytics_filter_article`: "文章" → "文章标题"
|
||||
- `analytics_filter_all_articles`: 删除(不再需要)
|
||||
- 新增 `analytics_filter_article_placeholder`: "搜索文章标题"
|
||||
|
||||
#### 优势
|
||||
|
||||
1. **可扩展性**:支持任意数量的文章
|
||||
2. **用户体验**:无需下拉滚动,直接输入
|
||||
3. **灵活性**:模糊匹配,不必输入完整标题
|
||||
4. **性能优化**:不再需要预加载所有文章列表
|
||||
|
||||
#### 兼容性
|
||||
|
||||
- ✅ 向后兼容:URL参数从 `article_id` 改为 `article_title`
|
||||
- ✅ 数据库兼容:使用标准SQL JOIN和LIKE
|
||||
- ✅ 支持SQLite和MySQL
|
||||
|
||||
---
|
||||
|
||||
## 完整功能列表(v1.1)
|
||||
|
||||
### 筛选功能
|
||||
- ✅ **文章标题**:输入框模糊搜索(新)
|
||||
- ✅ **IP地址**:输入框部分匹配
|
||||
- ✅ **显示爬虫**:复选框切换
|
||||
|
||||
### 统计展示
|
||||
- ✅ 全局统计卡片(5个指标)
|
||||
- ✅ 文章排行榜(前20篇)
|
||||
- ✅ 详细访问记录(分页)
|
||||
|
||||
### 核心功能
|
||||
- ✅ 自动记录阅读
|
||||
- ✅ 智能爬虫识别
|
||||
- ✅ 去重机制
|
||||
- ✅ 异步处理
|
||||
- ✅ 中英文支持
|
||||
|
||||
---
|
||||
|
||||
## 升级方法
|
||||
|
||||
如果你正在使用 v1.0:
|
||||
|
||||
1. **重新编译**:
|
||||
```bash
|
||||
go build -o go_blog
|
||||
```
|
||||
|
||||
2. **重启应用**:
|
||||
```bash
|
||||
./go_blog
|
||||
```
|
||||
|
||||
3. **无需数据库迁移**:数据结构未变更
|
||||
|
||||
4. **清除浏览器缓存**:确保加载新模板
|
||||
|
||||
---
|
||||
|
||||
## 后续计划
|
||||
|
||||
考虑添加的功能:
|
||||
- 📅 时间范围筛选(按日期范围)
|
||||
- 👤 用户名搜索(类似文章搜索)
|
||||
- 📊 导出筛选结果(CSV/Excel)
|
||||
- 🔍 高级搜索(组合多个条件)
|
||||
|
||||
---
|
||||
|
||||
**感谢反馈!** 🙏
|
||||
|
||||
这个改进让系统更适合大规模文章数量的场景。
|
||||
@@ -1,239 +0,0 @@
|
||||
# 阅读统计系统实现总结
|
||||
|
||||
## 已完成功能
|
||||
|
||||
### 1. 数据库模型
|
||||
✅ **ArticleView 模型** (`models/article_view.go`)
|
||||
- 记录每次独特的阅读
|
||||
- 字段:文章ID、用户ID(可为NULL)、IP地址、User-Agent、是否为爬虫
|
||||
- 包含去重逻辑(BeforeCreate hook)
|
||||
- 建立了必要的索引
|
||||
|
||||
✅ **爬虫检测** (`models/bot_detector.go`)
|
||||
- 基于User-Agent的模式匹配
|
||||
- 支持主流搜索引擎爬虫、社交媒体爬虫、AI爬虫等
|
||||
- 包含35+种常见爬虫特征
|
||||
|
||||
### 2. 阅读记录功能
|
||||
✅ **自动记录** (`handlers/home.go`)
|
||||
- 在文章详情页自动记录阅读
|
||||
- 异步执行(goroutine)不阻塞页面响应
|
||||
- 同一用户/IP只记录一次(去重)
|
||||
- 自动识别并标记爬虫
|
||||
|
||||
### 3. 后台统计页面
|
||||
✅ **统计Dashboard** (`handlers/admin_analytics.go`)
|
||||
- 全局统计:总阅读量、真人阅读、爬虫访问、独立IP、独立用户
|
||||
- 文章排行榜:前20篇文章的详细统计
|
||||
- 详细访问记录:时间、文章、用户、IP、User-Agent、是否爬虫
|
||||
- 强大的筛选功能:按文章、IP、是否显示爬虫
|
||||
- 分页支持(每页50条)
|
||||
|
||||
✅ **精美模板** (`templates/admin/analytics_views.html`)
|
||||
- 响应式设计
|
||||
- 彩色统计卡片
|
||||
- 表格展示详细数据
|
||||
- 爬虫记录高亮显示(橙色背景)
|
||||
- 筛选表单
|
||||
- 分页加载
|
||||
|
||||
### 4. 国际化支持
|
||||
✅ **中英文翻译** (`i18n/i18n.go`)
|
||||
- 添加了35+个翻译key
|
||||
- 完整的中英文对照
|
||||
|
||||
### 5. 路由和集成
|
||||
✅ **路由配置** (`main.go`)
|
||||
- `/admin/analytics/views` - 阅读统计页面
|
||||
- 管理员权限保护
|
||||
|
||||
✅ **数据库迁移** (`models/db.go`)
|
||||
- ArticleView表已加入自动迁移
|
||||
|
||||
✅ **导航入口** (`templates/admin/dashboard.html`)
|
||||
- 后台首页添加"阅读统计"按钮
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 去重机制
|
||||
1. **应用层去重**:在recordArticleView函数中查询现有记录
|
||||
2. **数据库层去重**:BeforeCreate hook二次检查
|
||||
3. **组合键**:article_id + user_id + ip(匿名用户user_id为NULL)
|
||||
|
||||
### 爬虫识别
|
||||
检测的爬虫类型:
|
||||
- 搜索引擎:Google, Bing, Baidu, Yandex等
|
||||
- 社交媒体:Facebook, Twitter, LinkedIn等
|
||||
- AI爬虫:GPTBot, ClaudeBot, Anthropic-AI
|
||||
- SEO工具:Ahrefs, SEMrush, Moz等
|
||||
- 无头浏览器:Headless, Phantom, Puppeteer
|
||||
|
||||
### 性能优化
|
||||
- ✅ 异步记录(goroutine)
|
||||
- ✅ 数据库索引优化
|
||||
- ✅ 分页加载(50条/页)
|
||||
- ✅ 最佳实践:忽略记录错误,不影响用户体验
|
||||
|
||||
### 安全性
|
||||
- ✅ 管理员权限保护
|
||||
- ✅ 使用c.ClientIP()获取真实IP(处理代理头)
|
||||
- ✅ SQL注入防护(GORM参数化查询)
|
||||
- ✅ XSS防护(模板自动转义)
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 启动应用
|
||||
```bash
|
||||
./go_blog
|
||||
```
|
||||
|
||||
### 访问统计页面
|
||||
1. 以管理员身份登录
|
||||
2. 访问后台首页
|
||||
3. 点击"阅读统计"按钮
|
||||
4. 或直接访问:`http://localhost:PORT/admin/analytics/views`
|
||||
|
||||
### 查看统计数据
|
||||
**全局统计卡片**:
|
||||
- 总阅读量:所有记录数
|
||||
- 真人阅读:排除爬虫的阅读数
|
||||
- 爬虫访问:被标记为爬虫的访问数
|
||||
- 独立IP:不同IP地址数
|
||||
- 独立用户:已登录用户数
|
||||
|
||||
**文章排行榜**:
|
||||
- 显示阅读量最高的20篇文章
|
||||
- 每篇文章显示:总阅读、真人阅读、爬虫访问、独立IP
|
||||
|
||||
**详细访问记录**:
|
||||
- 时间、文章标题(可点击)、用户名、IP地址、User-Agent
|
||||
- 爬虫记录以橙色背景高亮
|
||||
- 真人显示绿色标签,爬虫显示橙色标签
|
||||
|
||||
### 使用筛选功能
|
||||
- **按文章筛选**:下拉选择特定文章
|
||||
- **按IP筛选**:输入IP地址(支持部分匹配)
|
||||
- **显示爬虫**:勾选以包含爬虫流量
|
||||
- 点击"应用筛选"查看结果
|
||||
|
||||
### 分页浏览
|
||||
- 每页显示50条记录
|
||||
- 底部显示"加载更多"按钮
|
||||
- 筛选条件在翻页时保持
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 数据模型
|
||||
```go
|
||||
type ArticleView struct {
|
||||
ID uint
|
||||
CreatedAt time.Time
|
||||
ArticleID uint
|
||||
UserID *uint // NULL for anonymous
|
||||
IP string
|
||||
UserAgent string
|
||||
IsBot bool
|
||||
Article Article
|
||||
User *User
|
||||
}
|
||||
```
|
||||
|
||||
### 索引
|
||||
- `idx_article_views_article` on `article_id`
|
||||
- `idx_article_views_user` on `user_id`
|
||||
- `idx_article_views_ip` on `ip`
|
||||
- `idx_article_views_bot` on `is_bot`
|
||||
|
||||
### 异步记录
|
||||
```go
|
||||
// 不阻塞页面响应
|
||||
go recordArticleView(db, article.ID, c)
|
||||
```
|
||||
|
||||
## 扩展建议
|
||||
|
||||
### 未来可添加的功能
|
||||
1. **图表可视化**
|
||||
- 阅读趋势图(按天/周/月)
|
||||
- 访客地理分布
|
||||
- 浏览器/设备统计
|
||||
|
||||
2. **高级分析**
|
||||
- 用户阅读路径分析
|
||||
- 文章热度变化趋势
|
||||
- 访问高峰时段分析
|
||||
|
||||
3. **性能优化**
|
||||
- Redis缓存统计数据
|
||||
- 定期归档旧数据
|
||||
- 批量写入优化
|
||||
|
||||
4. **导出功能**
|
||||
- CSV/Excel导出
|
||||
- 自定义报表生成
|
||||
- 定期邮件报告
|
||||
|
||||
5. **实时监控**
|
||||
- WebSocket实时访问流
|
||||
- 在线访客数量
|
||||
- 实时热门文章
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 编译状态
|
||||
✅ 代码编译成功(无错误)
|
||||
|
||||
### 验证清单
|
||||
- ✅ 数据库模型创建
|
||||
- ✅ 爬虫检测逻辑
|
||||
- ✅ 文章详情页集成
|
||||
- ✅ 后台handler实现
|
||||
- ✅ 前端模板创建
|
||||
- ✅ 国际化翻译
|
||||
- ✅ 路由配置
|
||||
- ✅ 数据库迁移
|
||||
- ✅ 导航入口
|
||||
|
||||
### 建议测试步骤
|
||||
1. ✅ 启动应用,确认数据库迁移成功
|
||||
2. ✅ 访问多篇文章,生成测试数据
|
||||
3. ✅ 访问 `/admin/analytics/views` 查看统计
|
||||
4. ✅ 测试筛选功能
|
||||
5. ✅ 用不同User-Agent测试爬虫识别
|
||||
6. ✅ 测试分页功能
|
||||
7. ✅ 切换语言测试国际化
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新建文件
|
||||
1. `models/article_view.go` - ArticleView数据模型
|
||||
2. `models/bot_detector.go` - 爬虫检测逻辑
|
||||
3. `handlers/admin_analytics.go` - 统计页面handler
|
||||
4. `templates/admin/analytics_views.html` - 统计页面模板
|
||||
|
||||
### 修改文件
|
||||
1. `handlers/home.go` - 添加阅读记录逻辑
|
||||
2. `models/db.go` - 添加ArticleView到迁移
|
||||
3. `main.go` - 添加analytics路由
|
||||
4. `i18n/i18n.go` - 添加翻译
|
||||
5. `templates/admin/dashboard.html` - 添加入口链接
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **首次运行**:会自动创建article_views表
|
||||
2. **性能**:异步记录不影响页面加载速度
|
||||
3. **隐私**:IP地址完整记录,考虑是否需要脱敏
|
||||
4. **存储**:长期运行可能积累大量数据,建议定期归档
|
||||
5. **爬虫识别**:基于User-Agent,可能有漏报和误报
|
||||
|
||||
## 总结
|
||||
|
||||
阅读统计系统已完整实现,包含:
|
||||
- ✅ 自动记录每篇文章的独特访问
|
||||
- ✅ 智能识别并标记爬虫
|
||||
- ✅ 功能强大的后台统计页面
|
||||
- ✅ 详细的访问记录和筛选
|
||||
- ✅ 完整的中英文支持
|
||||
- ✅ 性能优化和安全保护
|
||||
|
||||
系统可以立即投入使用,为博客管理员提供深入的阅读数据洞察!
|
||||
@@ -1,242 +0,0 @@
|
||||
# 阅读统计系统 - 快速使用指南
|
||||
|
||||
## 🚀 立即开始
|
||||
|
||||
### 1. 启动应用
|
||||
```bash
|
||||
./go_blog
|
||||
```
|
||||
|
||||
首次启动会自动创建 `article_views` 数据库表。
|
||||
|
||||
### 2. 访问统计页面
|
||||
1. 浏览器打开:http://localhost:8080/login
|
||||
2. 登录(默认账号):`admin` / `admin`
|
||||
3. 进入后台,点击 **"阅读统计"** 按钮
|
||||
4. 或直接访问:http://localhost:8080/admin/analytics/views
|
||||
|
||||
## 📊 功能说明
|
||||
|
||||
### 全局统计(顶部卡片)
|
||||
- **总阅读量**:所有访问记录数
|
||||
- **真人阅读**:排除爬虫的真实用户访问
|
||||
- **爬虫访问**:被识别为爬虫的访问数
|
||||
- **独立IP**:访问过的不同IP地址数量
|
||||
- **独立用户**:已登录用户的数量
|
||||
|
||||
### 文章排行榜
|
||||
- 显示阅读量最高的前20篇文章
|
||||
- 每篇显示:总阅读、真人阅读、爬虫访问、独立IP
|
||||
|
||||
### 详细访问记录
|
||||
每条记录显示:
|
||||
- **时间**:访问时间(精确到分钟)
|
||||
- **文章**:文章标题(可点击跳转)
|
||||
- **用户**:登录用户名或"匿名用户"
|
||||
- **IP地址**:访客IP
|
||||
- **User-Agent**:浏览器信息
|
||||
- **类型**:👤真人 或 🤖爬虫(橙色高亮)
|
||||
|
||||
### 筛选功能
|
||||
- **按文章筛选**:下拉选择特定文章
|
||||
- **按IP筛选**:输入IP地址(支持部分匹配)
|
||||
- **显示爬虫**:勾选后包含爬虫流量
|
||||
|
||||
## 🤖 爬虫识别
|
||||
|
||||
系统自动识别以下类型的爬虫:
|
||||
|
||||
### 搜索引擎
|
||||
- Google (Googlebot)
|
||||
- Bing (Bingbot)
|
||||
- Baidu (Baiduspider)
|
||||
- Yandex (Yandexbot)
|
||||
- DuckDuckGo (Duckduckbot)
|
||||
|
||||
### 社交媒体
|
||||
- Facebook (Facebookexternalhit, Facebot)
|
||||
- Twitter (Twitterbot)
|
||||
- LinkedIn (Linkedinbot)
|
||||
- Telegram
|
||||
- WhatsApp
|
||||
|
||||
### AI爬虫
|
||||
- GPTBot (OpenAI)
|
||||
- ClaudeBot (Anthropic)
|
||||
- 字节跳动 (Bytespider)
|
||||
|
||||
### SEO工具
|
||||
- Ahrefs
|
||||
- SEMrush
|
||||
- Moz (Dotbot)
|
||||
- Dataforseo
|
||||
|
||||
### 其他
|
||||
- 无头浏览器 (Headless, Phantom, Puppeteer, Selenium)
|
||||
- 网页归档 (Archive.org_bot)
|
||||
|
||||
## 🎯 测试统计功能
|
||||
|
||||
### 方法1:运行测试脚本
|
||||
```bash
|
||||
./test_analytics.sh
|
||||
```
|
||||
|
||||
该脚本会:
|
||||
- 检查应用状态
|
||||
- 模拟不同User-Agent的访问
|
||||
- 生成测试数据
|
||||
- 验证数据库记录
|
||||
|
||||
### 方法2:手动测试
|
||||
1. 访问几篇已发布的文章
|
||||
2. 刷新页面(不会重复计数)
|
||||
3. 使用不同浏览器访问(会记录为不同IP)
|
||||
4. 查看后台统计
|
||||
|
||||
### 方法3:用curl模拟爬虫
|
||||
```bash
|
||||
# 模拟GoogleBot
|
||||
curl -H "User-Agent: Googlebot/2.1" http://localhost:8080/article/your-article-slug
|
||||
|
||||
# 模拟BingBot
|
||||
curl -H "User-Agent: bingbot/2.0" http://localhost:8080/article/your-article-slug
|
||||
|
||||
# 正常浏览器
|
||||
curl -H "User-Agent: Mozilla/5.0 (Macintosh)" http://localhost:8080/article/your-article-slug
|
||||
```
|
||||
|
||||
## ✨ 核心特性
|
||||
|
||||
### 自动去重
|
||||
- 同一IP+用户组合只记录一次
|
||||
- 刷新页面不会重复计数
|
||||
- 双重保护:应用层+数据库层
|
||||
|
||||
### 异步记录
|
||||
- 不阻塞页面加载
|
||||
- 后台goroutine处理
|
||||
- 即使记录失败也不影响用户体验
|
||||
|
||||
### 性能优化
|
||||
- 数据库索引优化
|
||||
- 分页加载(50条/页)
|
||||
- 异步处理
|
||||
|
||||
### 国际化
|
||||
- 完整中英文支持
|
||||
- 自动语言切换
|
||||
|
||||
## 📁 相关文件
|
||||
|
||||
### 新建文件
|
||||
- `models/article_view.go` - 数据模型
|
||||
- `models/bot_detector.go` - 爬虫检测
|
||||
- `handlers/admin_analytics.go` - 统计handler
|
||||
- `templates/admin/analytics_views.html` - 统计页面
|
||||
- `test_analytics.sh` - 测试脚本
|
||||
|
||||
### 修改文件
|
||||
- `handlers/home.go` - 添加记录逻辑
|
||||
- `models/db.go` - 数据库迁移
|
||||
- `main.go` - 路由配置
|
||||
- `i18n/i18n.go` - 翻译
|
||||
- `templates/admin/dashboard.html` - 入口链接
|
||||
|
||||
## 🔧 高级配置
|
||||
|
||||
### 查询数据库(SQLite)
|
||||
```bash
|
||||
sqlite3 tmp/blog.db
|
||||
|
||||
# 查看所有访问记录
|
||||
SELECT * FROM article_views ORDER BY created_at DESC LIMIT 10;
|
||||
|
||||
# 统计真人和爬虫访问
|
||||
SELECT is_bot, COUNT(*) FROM article_views GROUP BY is_bot;
|
||||
|
||||
# 查看独立IP数
|
||||
SELECT COUNT(DISTINCT ip) FROM article_views;
|
||||
|
||||
# 某篇文章的访问统计
|
||||
SELECT COUNT(*) FROM article_views WHERE article_id = 1;
|
||||
```
|
||||
|
||||
### 清空统计数据
|
||||
```bash
|
||||
sqlite3 tmp/blog.db "DELETE FROM article_views;"
|
||||
```
|
||||
|
||||
## 💡 使用建议
|
||||
|
||||
### 日常使用
|
||||
1. **定期查看**:每周查看一次统计数据
|
||||
2. **关注真人阅读**:爬虫访问仅供参考
|
||||
3. **分析热门文章**:根据排行榜优化内容
|
||||
4. **识别可疑IP**:发现异常流量
|
||||
|
||||
### 数据维护
|
||||
1. **定期备份**:备份数据库文件
|
||||
2. **归档旧数据**:超过6个月的数据可以归档
|
||||
3. **监控存储**:注意数据库大小
|
||||
|
||||
### 隐私保护
|
||||
- IP地址完整记录,考虑是否需要脱敏
|
||||
- User-Agent信息敏感,注意保护
|
||||
- 遵守当地隐私法规(如GDPR)
|
||||
|
||||
## 🐛 故障排查
|
||||
|
||||
### 问题1:看不到访问记录
|
||||
**原因**:可能还没有人访问文章
|
||||
**解决**:访问几篇已发布的文章,等待几秒后刷新统计页面
|
||||
|
||||
### 问题2:统计页面显示0
|
||||
**原因**:
|
||||
- 数据库迁移未执行
|
||||
- article_views表不存在
|
||||
|
||||
**解决**:
|
||||
```bash
|
||||
# 停止应用
|
||||
pkill go_blog
|
||||
|
||||
# 重新启动(会自动迁移)
|
||||
./go_blog
|
||||
```
|
||||
|
||||
### 问题3:所有访问都被标记为爬虫
|
||||
**原因**:curl或工具的User-Agent被识别为爬虫
|
||||
**解决**:使用真实浏览器访问
|
||||
|
||||
### 问题4:访问页面返回404
|
||||
**原因**:路由未正确配置
|
||||
**检查**:
|
||||
```bash
|
||||
# 查看路由列表
|
||||
grep "analytics/views" main.go
|
||||
```
|
||||
|
||||
## 📈 未来扩展
|
||||
|
||||
可以考虑添加的功能:
|
||||
1. 📊 图表可视化(访问趋势图)
|
||||
2. 🌍 地理位置分析(IP定位)
|
||||
3. 📱 设备统计(移动端/桌面端)
|
||||
4. ⏰ 访问时段分析(热门时间)
|
||||
5. 📊 导出功能(CSV/Excel)
|
||||
6. 🔔 实时监控(WebSocket)
|
||||
7. 📧 定期报告(邮件通知)
|
||||
|
||||
## 📞 支持
|
||||
|
||||
遇到问题?
|
||||
1. 查看 `ANALYTICS_IMPLEMENTATION.md` 详细文档
|
||||
2. 检查应用日志
|
||||
3. 运行测试脚本 `./test_analytics.sh`
|
||||
|
||||
---
|
||||
|
||||
**就这么简单!** 🎉
|
||||
|
||||
现在你的博客已经拥有了专业的阅读统计系统,可以深入了解读者行为,优化内容策略!
|
||||
@@ -1,90 +0,0 @@
|
||||
# 文章详情页显示最后更新时间功能
|
||||
|
||||
## 功能概述
|
||||
|
||||
为文章详情页添加了最后更新时间的显示,让读者可以看到文章的发布时间和最后修改时间。
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. 前端模板
|
||||
|
||||
#### `/templates/pages/article.html`
|
||||
- 修改了文章元信息显示部分(第24-33行)
|
||||
- 将原来的单一时间戳改为显示两个时间:
|
||||
- **发布时间**:文章首次发布的时间(`PublishedAt`)
|
||||
- **最后更新**:文章最后修改的时间(`UpdatedAt`)
|
||||
- 使用条件判断确保只在有值时显示
|
||||
|
||||
显示格式:
|
||||
```
|
||||
作者名 · 发布时间: 2024-01-15 10:30 · 最后更新: 2024-01-20 14:25
|
||||
```
|
||||
|
||||
### 2. 后端处理器
|
||||
|
||||
#### `/handlers/home.go`
|
||||
- 修改 `renderArticleDetail()` 函数(第189-201行):
|
||||
- 添加 `data["UpdatedAt"]` 传递更新时间到模板
|
||||
- 添加 `formatUpdateTime()` 函数(第213-216行):
|
||||
- 格式化 `UpdatedAt` 字段为可读的时间字符串
|
||||
- 格式:`2006-01-02 15:04`(年-月-日 时:分)
|
||||
|
||||
### 3. 国际化文本
|
||||
|
||||
#### `/i18n/i18n.go`
|
||||
添加了翻译键:
|
||||
- `article_last_updated`: "Last Updated" / "最后更新"
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. **双时间戳显示**:
|
||||
- 发布时间(PublishedAt):文章首次发布的时间
|
||||
- 最后更新(UpdatedAt):文章最后一次修改的时间
|
||||
|
||||
2. **自动更新**:
|
||||
- `UpdatedAt` 字段由 GORM 自动维护
|
||||
- 每次调用 `Updates()` 或 `Save()` 时自动更新
|
||||
|
||||
3. **智能显示**:
|
||||
- 如果文章没有发布时间,不显示发布时间
|
||||
- 始终显示最后更新时间
|
||||
|
||||
4. **时间格式**:
|
||||
- 统一使用 `YYYY-MM-DD HH:MM` 格式
|
||||
- 清晰易读
|
||||
|
||||
## 显示效果
|
||||
|
||||
文章详情页顶部会显示:
|
||||
|
||||
### 英文界面
|
||||
```
|
||||
John Doe · Published: 2024-01-15 10:30 · Last Updated: 2024-01-20 14:25
|
||||
```
|
||||
|
||||
### 中文界面
|
||||
```
|
||||
张三 · 发布时间: 2024-01-15 10:30 · 最后更新: 2024-01-20 14:25
|
||||
```
|
||||
|
||||
## 技术实现
|
||||
|
||||
- **数据库字段**:`updated_at` 字段是 GORM 的标准字段,类型为 `time.Time`
|
||||
- **自动维护**:GORM 在每次更新记录时自动设置 `updated_at`
|
||||
- **格式化**:使用 Go 的标准时间格式 `2006-01-02 15:04`
|
||||
|
||||
## 与发布时间编辑功能的配合
|
||||
|
||||
这个功能与之前实现的"自定义发布时间"功能完美配合:
|
||||
- **发布时间**(PublishedAt):可以由用户手动设置或自动生成
|
||||
- **更新时间**(UpdatedAt):始终由系统自动维护,反映真实的修改时间
|
||||
|
||||
这样读者可以清楚地知道:
|
||||
1. 文章最初是什么时候发布的
|
||||
2. 文章最后一次修改是什么时候
|
||||
|
||||
## 用户价值
|
||||
|
||||
1. **内容时效性**:读者可以判断文章内容是否及时更新
|
||||
2. **信息透明**:清楚显示文章的发布和修改历史
|
||||
3. **信任度提升**:显示更新时间表明作者在持续维护内容
|
||||
@@ -1,87 +0,0 @@
|
||||
# Favicon 设置功能更新
|
||||
|
||||
## 概述
|
||||
在站点设置页面 (`/admin/settings/site`) 添加了 Favicon(网站图标)设置功能。
|
||||
|
||||
## 更改的文件
|
||||
|
||||
### 1. 数据库模型 (models/site_setting.go)
|
||||
- 添加了 `Favicon` 字段到 `SiteSetting` 结构体
|
||||
- 添加了 `FaviconIsURL()` 方法来判断 Favicon 是外链还是本地文件
|
||||
|
||||
### 2. 处理器 (handlers/settings.go)
|
||||
- `SiteSettingsPage`: 添加 `SiteFaviconIsURL` 到模板数据
|
||||
- `SiteSettingsSave`: 添加 Favicon 上传和保存逻辑,支持:
|
||||
- 外链 URL 设置
|
||||
- 本地文件上传(.ico, .png, .svg)
|
||||
- 清除当前 Favicon
|
||||
|
||||
### 3. 模板 (templates/admin/settings_site.html)
|
||||
- 添加 Favicon 设置表单区域
|
||||
- 显示当前 Favicon 预览
|
||||
- 支持 URL 输入和文件上传
|
||||
- 添加清除 Favicon 的复选框
|
||||
|
||||
### 4. 国际化 (i18n/i18n.go)
|
||||
添加了以下翻译键:
|
||||
- `settings_favicon`: Favicon / 网站图标(Favicon)
|
||||
- `settings_favicon_url`: Favicon URL (external link) / Favicon 链接(外链地址)
|
||||
- `settings_favicon_current`: Current favicon / 当前 Favicon
|
||||
- `settings_favicon_hint`: 格式提示
|
||||
- `settings_favicon_clear`: Remove current favicon / 移除当前 Favicon
|
||||
|
||||
### 5. 中间件 (middleware/auth.go)
|
||||
- 在 `SetUserContext` 中添加 `site_favicon` 和 `site_favicon_is_url` 到上下文
|
||||
|
||||
### 6. 辅助函数 (handlers/helpers.go)
|
||||
- 在 `DefaultData` 中添加 `SiteFavicon` 和 `SiteFaviconIsURL` 到模板数据
|
||||
|
||||
### 7. 页面模板 (templates/layouts/base.html)
|
||||
- 在 `<head>` 中添加 Favicon 的 `<link>` 标签
|
||||
- 根据设置自动使用外链或本地文件
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
如果数据库已存在,需要手动添加 `favicon` 字段:
|
||||
|
||||
```sql
|
||||
ALTER TABLE site_settings ADD COLUMN favicon VARCHAR(512) DEFAULT '';
|
||||
```
|
||||
|
||||
或者运行提供的迁移脚本:
|
||||
```bash
|
||||
sqlite3 data/blog.db < scripts/add_favicon_field.sql
|
||||
```
|
||||
|
||||
**注意**: GORM 的 `AutoMigrate` 会在下次启动时自动添加新字段,无需手动执行 SQL。
|
||||
|
||||
## 使用说明
|
||||
|
||||
1. 访问 `/admin/settings/site` 页面
|
||||
2. 在 "网站图标(Favicon)" 部分:
|
||||
- 方式一:输入外链 URL(如 CDN 链接)
|
||||
- 方式二:上传本地图片文件(推荐 .ico, .png 或 .svg 格式,32x32 或 16x16 像素)
|
||||
3. 点击 "保存" 按钮
|
||||
4. Favicon 将自动应用到网站所有页面
|
||||
|
||||
## 文件存储
|
||||
|
||||
- 上传的 Favicon 文件保存在 `{storage_path}/logos/` 目录下
|
||||
- 文件名固定为 `favicon.{ext}`(如 favicon.ico, favicon.png)
|
||||
- 每次上传新文件时会自动删除旧文件
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 支持的格式
|
||||
- ICO (.ico) - 传统格式,兼容性最好
|
||||
- PNG (.png) - 现代浏览器支持
|
||||
- SVG (.svg) - 矢量格式,适合高分辨率显示
|
||||
|
||||
### 优先级
|
||||
1. 如果同时设置了 URL 和上传文件,URL 优先
|
||||
2. 如果勾选 "移除当前 Favicon",会删除现有设置
|
||||
|
||||
### 缓存
|
||||
- Favicon 设置存储在数据库中
|
||||
- 通过 `models.RefreshConfigCache()` 刷新内存缓存
|
||||
- 更改后立即生效,无需重启服务
|
||||
@@ -1,145 +0,0 @@
|
||||
# 主页文章卡片功能更新
|
||||
|
||||
## 新增功能:显示阅读量和评论数
|
||||
|
||||
### 效果展示
|
||||
|
||||
每篇文章卡片底部现在显示:
|
||||
- 👁️ **阅读量**:显示该文章被访问的次数
|
||||
- 💬 **评论数**:显示已通过审核的评论数量
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────┐
|
||||
│ 文章标题 [置顶] │
|
||||
│ 文章摘要内容... │
|
||||
│ │
|
||||
│ 阅读全文 → 👁️ 123 💬 5 │
|
||||
└─────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 技术实现
|
||||
|
||||
#### 1. 后端改动
|
||||
|
||||
**HomePage Handler** - 加载初始文章的评论数:
|
||||
```go
|
||||
// 查询评论数
|
||||
db.Model(&models.Comment{}).
|
||||
Select("article_id, COUNT(*) as count").
|
||||
Where("article_id IN ?", articleIDs).
|
||||
Where("status = ?", models.CommentApproved).
|
||||
Group("article_id").
|
||||
Scan(&commentCounts)
|
||||
|
||||
// 传递给模板
|
||||
data["CommentCounts"] = commentCountMap
|
||||
```
|
||||
|
||||
**HomeArticlesAPI Handler** - 返回JSON格式的文章列表:
|
||||
```go
|
||||
type ArticleResponse struct {
|
||||
models.Article
|
||||
CommentCount int64 `json:"comment_count"`
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 前端改动
|
||||
|
||||
**模板 (home.html)**:
|
||||
```html
|
||||
<div class="flex items-center gap-4 text-sm text-gray-500">
|
||||
<!-- 阅读量 -->
|
||||
<span class="flex items-center gap-1">
|
||||
<svg>眼睛图标</svg>
|
||||
{{.ViewCount}}
|
||||
</span>
|
||||
|
||||
<!-- 评论数 -->
|
||||
<span class="flex items-center gap-1">
|
||||
<svg>对话图标</svg>
|
||||
<span class="comment-count">{{index $.CommentCounts .ID}}</span>
|
||||
</span>
|
||||
</div>
|
||||
```
|
||||
|
||||
**JavaScript 动态加载**:
|
||||
```javascript
|
||||
const commentCount = article.comment_count || 0;
|
||||
const viewCount = article.view_count || 0;
|
||||
```
|
||||
|
||||
### 数据来源
|
||||
|
||||
- **阅读量 (view_count)**:来自 `articles.view_count` 字段
|
||||
- 每次访问文章时自动增加
|
||||
- 显示所有访问次数(包括刷新)
|
||||
|
||||
- **评论数 (comment_count)**:来自 `comments` 表统计
|
||||
- 只统计已通过审核的评论 (`status = 1`)
|
||||
- 实时查询,保证准确性
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **批量查询**:使用 `IN` 查询一次性获取所有文章的评论数
|
||||
2. **索引支持**:`article_id` 和 `status` 字段都有索引
|
||||
3. **缓存友好**:评论数在前端渲染,无需额外请求
|
||||
|
||||
### 样式细节
|
||||
|
||||
- 使用 Heroicons SVG 图标
|
||||
- 灰色文字 (`text-gray-500`) 不抢眼
|
||||
- 与"阅读全文"按钮对齐
|
||||
- 响应式设计,移动端友好
|
||||
|
||||
### API 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"articles": [
|
||||
{
|
||||
"id": 5,
|
||||
"title": "文章标题",
|
||||
"view_count": 123,
|
||||
"comment_count": 5,
|
||||
...
|
||||
}
|
||||
],
|
||||
"hasMore": true
|
||||
}
|
||||
```
|
||||
|
||||
### 文件变更
|
||||
|
||||
- ✅ `handlers/home.go` - 添加评论数查询逻辑
|
||||
- ✅ `templates/pages/home.html` - 显示阅读量和评论数UI
|
||||
- ✅ JavaScript - 动态加载时包含统计信息
|
||||
|
||||
### 兼容性
|
||||
|
||||
- ✅ 向后兼容:如果没有评论,显示 0
|
||||
- ✅ 支持无限滚动:新加载的文章也显示统计
|
||||
- ✅ 支持SQLite和MySQL
|
||||
|
||||
### 用户体验
|
||||
|
||||
**优势**:
|
||||
1. 用户可以快速看到文章的热度
|
||||
2. 评论数激发互动兴趣
|
||||
3. 不需要点开文章就能了解活跃度
|
||||
|
||||
**显示规则**:
|
||||
- 阅读量始终显示(即使是0)
|
||||
- 评论数始终显示(即使是0)
|
||||
- 数字简洁明了,不带单位
|
||||
|
||||
### 未来增强
|
||||
|
||||
可以考虑的改进:
|
||||
- 📊 阅读量格式化(如 1.2k)
|
||||
- 🔥 热度标记(高阅读量文章)
|
||||
- ⭐ 点赞功能
|
||||
- 📈 阅读趋势指示器
|
||||
|
||||
---
|
||||
|
||||
**效果**:让用户在浏览文章列表时就能看到每篇文章的热度和互动情况!📊
|
||||
@@ -1,122 +0,0 @@
|
||||
# 顶部导航链接管理功能 - 实现总结
|
||||
|
||||
## 已实现的功能
|
||||
|
||||
✅ **完整的导航链接管理系统**
|
||||
- 后台管理界面用于添加、编辑、删除、启用/禁用导航链接
|
||||
- 支持中英文双语链接文字
|
||||
- 支持设置是否在新窗口打开(自动添加安全属性 `rel="noopener noreferrer"`)
|
||||
- 支持自定义排序
|
||||
- 前端自动根据用户语言显示对应文字
|
||||
|
||||
## 实现的文件
|
||||
|
||||
### 1. 数据库模型
|
||||
- **models/nav_link.go** - NavLink 模型定义
|
||||
- **models/db.go** - 添加了 NavLink 到自动迁移
|
||||
- **models/config_cache.go** - 添加导航链接缓存支持
|
||||
|
||||
### 2. 后端处理器
|
||||
- **handlers/settings.go** - 添加了以下函数:
|
||||
- `NavLinksSettingsPage()` - 管理页面
|
||||
- `NavLinksSettingsSave()` - 保存处理
|
||||
- `addNavLink()` - 添加链接
|
||||
- `editNavLink()` - 编辑链接
|
||||
- `toggleNavLink()` - 切换启用状态
|
||||
- `deleteNavLink()` - 删除链接
|
||||
|
||||
### 3. 前端模板
|
||||
- **templates/admin/settings_navlinks.html** - 管理页面模板
|
||||
- 添加链接表单
|
||||
- 链接列表展示
|
||||
- 编辑对话框
|
||||
- 启用/禁用、删除操作
|
||||
- **templates/layouts/base.html** - 更新导航栏显示链接
|
||||
- **templates/admin/settings_*.html** - 更新所有设置页面的标签导航
|
||||
|
||||
### 4. 中间件
|
||||
- **middleware/auth.go** - 添加导航链接到全局上下文
|
||||
- **handlers/helpers.go** - 添加 NavLinks 到 DefaultData
|
||||
|
||||
### 5. 路由
|
||||
- **main.go** - 添加路由:
|
||||
- `GET /admin/settings/navlinks`
|
||||
- `POST /admin/settings/navlinks`
|
||||
|
||||
### 6. 国际化
|
||||
- **i18n/i18n.go** - 添加中英文翻译键:
|
||||
- `settings_navlinks_title` - 导航链接
|
||||
- `navlinks_add` - 添加链接
|
||||
- `navlinks_title_zh/en` - 链接文字
|
||||
- `navlinks_url` - 链接地址
|
||||
- `navlinks_open_new` - 在新窗口打开
|
||||
- 等等...
|
||||
|
||||
## 功能特点
|
||||
|
||||
1. **双语支持** - 自动根据用户语言显示对应的链接文字
|
||||
2. **新窗口打开** - 支持设置链接在新标签页打开,带有安全图标提示
|
||||
3. **排序功能** - 通过 Sort 字段控制链接显示顺序
|
||||
4. **启用/禁用** - 可以临时隐藏链接而不删除
|
||||
5. **编辑功能** - 通过模态对话框编辑现有链接
|
||||
6. **缓存优化** - 链接数据缓存在内存中,修改后自动刷新
|
||||
7. **安全性** - 新窗口链接自动添加 `rel="noopener noreferrer"`
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 后台管理
|
||||
1. 访问 `http://localhost:8080/admin/settings/navlinks`
|
||||
2. 填写表单添加新链接
|
||||
3. 点击"添加链接"保存
|
||||
4. 使用"编辑"按钮修改现有链接
|
||||
5. 使用"切换"按钮启用/禁用链接
|
||||
6. 使用"删除"按钮移除链接
|
||||
|
||||
### 前端展示
|
||||
导航链接会自动显示在顶部导航栏:
|
||||
```
|
||||
[Logo/Title] | [主页] | [关于] | [GitHub ↗] | [用户菜单]
|
||||
```
|
||||
|
||||
## 测试结果
|
||||
|
||||
✅ 添加链接功能正常
|
||||
✅ 链接在前端正确显示
|
||||
✅ 双语切换正常工作
|
||||
✅ 新窗口打开功能正常(带有 target="_blank" 和安全属性)
|
||||
✅ 链接显示外部链接图标
|
||||
✅ 管理界面正常显示所有链接
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
```sql
|
||||
CREATE TABLE nav_links (
|
||||
id INTEGER PRIMARY KEY,
|
||||
title_zh VARCHAR(100) NOT NULL,
|
||||
title_en VARCHAR(100) NOT NULL,
|
||||
url VARCHAR(512) NOT NULL,
|
||||
open_new BOOLEAN DEFAULT FALSE,
|
||||
enabled BOOLEAN DEFAULT TRUE,
|
||||
sort INTEGER DEFAULT 0,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME,
|
||||
updated_by INTEGER
|
||||
);
|
||||
```
|
||||
|
||||
## 技术亮点
|
||||
|
||||
1. **配置缓存机制** - 使用 sync.RWMutex 保证并发安全
|
||||
2. **语言回退** - 如果某个语言的文字为空,自动显示另一个语言
|
||||
3. **RESTful 设计** - 使用 action 参数区分不同操作
|
||||
4. **模态对话框** - 编辑功能使用模态框,用户体验好
|
||||
5. **一致性设计** - 遵循现有代码风格和设计模式
|
||||
|
||||
## 下一步建议
|
||||
|
||||
如需扩展功能,可以考虑:
|
||||
- 添加图标支持(可以为每个链接设置图标)
|
||||
- 添加分组功能(将链接分为不同的组)
|
||||
- 添加拖拽排序功能(更直观的排序方式)
|
||||
- 添加访问统计(记录链接点击次数)
|
||||
- 支持下拉菜单(一个链接下有多个子链接)
|
||||
@@ -1,145 +0,0 @@
|
||||
# 导航链接管理功能
|
||||
|
||||
## 功能概述
|
||||
|
||||
顶部导航链接管理功能允许管理员在网站顶部导航栏中添加自定义链接,支持中英文双语显示和新窗口打开设置。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- ✅ 添加自定义导航链接
|
||||
- ✅ 中英文双语支持(自动根据用户语言显示)
|
||||
- ✅ 控制是否在新窗口打开链接
|
||||
- ✅ 启用/禁用链接显示
|
||||
- ✅ 自定义链接排序
|
||||
- ✅ 编辑现有链接
|
||||
- ✅ 删除链接
|
||||
- ✅ 配置缓存,高性能访问
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 访问管理页面
|
||||
|
||||
1. 登录管理后台
|
||||
2. 访问 `/admin/settings/navlinks` 或点击"平台设置"进入设置页面
|
||||
3. 选择"导航链接"标签
|
||||
|
||||
### 添加新链接
|
||||
|
||||
在"添加链接"表单中填写以下信息:
|
||||
|
||||
- **链接文字(中文)**: 在中文界面显示的文本,如"关于"
|
||||
- **链接文字(英文)**: 在英文界面显示的文本,如"About"
|
||||
- **链接地址**: 目标URL,可以是:
|
||||
- 相对路径:`/about`
|
||||
- 完整URL:`https://example.com`
|
||||
- **在新窗口打开**: 勾选此选项将在新标签页打开链接
|
||||
- **启用**: 控制链接是否显示在导航栏
|
||||
- **排序**: 数字越小,显示越靠前(默认:0)
|
||||
|
||||
点击"添加链接"按钮保存。
|
||||
|
||||
### 编辑链接
|
||||
|
||||
1. 在链接列表中找到要编辑的链接
|
||||
2. 点击"编辑"按钮
|
||||
3. 在弹出的对话框中修改信息
|
||||
4. 点击"保存"
|
||||
|
||||
### 启用/禁用链接
|
||||
|
||||
点击链接右侧的"✓ 切换"或"✗ 切换"按钮,快速启用或禁用链接显示。
|
||||
|
||||
### 删除链接
|
||||
|
||||
点击"删除"按钮,确认后即可删除链接。
|
||||
|
||||
## 技术实现
|
||||
|
||||
### 数据库模型
|
||||
|
||||
```go
|
||||
type NavLink struct {
|
||||
ID uint // 主键
|
||||
TitleZh string // 链接文字(中文)
|
||||
TitleEn string // 链接文字(英文)
|
||||
URL string // 目标地址
|
||||
OpenNew bool // 是否新窗口打开
|
||||
Enabled bool // 是否启用
|
||||
Sort int // 排序(数字越小越靠前)
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
UpdatedBy uint // 更新者用户ID
|
||||
}
|
||||
```
|
||||
|
||||
### 配置缓存
|
||||
|
||||
导航链接数据在应用启动时加载到内存缓存中,每次修改后自动刷新缓存。这确保了:
|
||||
- 每个请求无需查询数据库
|
||||
- 高性能的页面渲染
|
||||
- 配置更改立即生效
|
||||
|
||||
### 安全特性
|
||||
|
||||
- 新窗口打开的链接自动添加 `rel="noopener noreferrer"` 属性,防止安全漏洞
|
||||
- 仅管理员可以访问管理页面
|
||||
- 输入验证确保数据完整性
|
||||
|
||||
## 前端展示
|
||||
|
||||
导航链接显示在网站顶部导航栏,位于"主页"链接之后:
|
||||
|
||||
- 根据用户选择的语言显示对应的链接文字
|
||||
- 设置为"新窗口打开"的链接会显示一个小图标 ↗
|
||||
- 仅显示已启用的链接
|
||||
- 按照排序字段从小到大排列
|
||||
|
||||
## API端点
|
||||
|
||||
- `GET /admin/settings/navlinks` - 导航链接管理页面
|
||||
- `POST /admin/settings/navlinks` - 处理导航链接操作
|
||||
- `action=add` - 添加新链接
|
||||
- `action=edit` - 编辑链接
|
||||
- `action=toggle` - 切换启用状态
|
||||
- `action=delete` - 删除链接
|
||||
|
||||
## 示例
|
||||
|
||||
### 添加"关于"页面链接
|
||||
|
||||
- 链接文字(中文):关于我们
|
||||
- 链接文字(英文):About Us
|
||||
- 链接地址:/about
|
||||
- 在新窗口打开:否
|
||||
- 启用:是
|
||||
- 排序:10
|
||||
|
||||
### 添加外部链接
|
||||
|
||||
- 链接文字(中文):GitHub
|
||||
- 链接文字(英文):GitHub
|
||||
- 链接地址:https://github.com/yourname
|
||||
- 在新窗口打开:是
|
||||
- 启用:是
|
||||
- 排序:20
|
||||
|
||||
## 多语言支持
|
||||
|
||||
该功能完全支持中英文双语:
|
||||
|
||||
**中文翻译键:**
|
||||
- `settings_navlinks_title`: "导航链接"
|
||||
- `navlinks_add`: "添加链接"
|
||||
- `navlinks_title_zh`: "链接文字(中文)"
|
||||
- `navlinks_title_en`: "链接文字(英文)"
|
||||
- `navlinks_url`: "链接地址"
|
||||
- `navlinks_open_new`: "在新窗口打开"
|
||||
- 等等...
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 至少需要填写中文或英文链接文字中的一个
|
||||
2. 链接地址必填
|
||||
3. 排序值相同时,按ID升序排列
|
||||
4. 禁用的链接不会显示在前端,但保留在数据库中
|
||||
5. 删除操作不可恢复,请谨慎操作
|
||||
@@ -1,223 +0,0 @@
|
||||
# 权限控制与用户文章管理功能
|
||||
|
||||
## 提交摘要
|
||||
本次更新修复了权限边界问题,并为普通用户添加了独立的文章管理功能。
|
||||
|
||||
## 1. 权限控制修复
|
||||
|
||||
### 问题
|
||||
- 普通用户(author角色)能够访问后台管理页面 `/admin`
|
||||
- 普通用户能够修改平台设置、审核评论、管理用户
|
||||
- 权限边界不清晰,存在安全隐患
|
||||
|
||||
### 解决方案
|
||||
|
||||
#### 后端路由保护 ([main.go](main.go))
|
||||
为所有管理功能添加 `AdminRequired` 中间件:
|
||||
|
||||
```go
|
||||
// 整个 /admin 路径要求管理员权限
|
||||
admin := router.Group("/admin")
|
||||
admin.Use(middleware.AuthRequired(), middleware.AdminRequired(db))
|
||||
|
||||
// 评论管理要求管理员权限
|
||||
comments := router.Group("/admin/comments")
|
||||
comments.Use(middleware.AuthRequired(), middleware.AdminRequired(db))
|
||||
|
||||
// 用户管理要求管理员权限
|
||||
users := router.Group("/admin/users")
|
||||
users.Use(middleware.AuthRequired(), middleware.AdminRequired(db))
|
||||
|
||||
// 平台设置要求管理员权限
|
||||
settings := router.Group("/admin/settings")
|
||||
settings.Use(middleware.AuthRequired(), middleware.AdminRequired(db))
|
||||
|
||||
// 附件管理要求管理员权限
|
||||
attachments := router.Group("/admin/articles")
|
||||
attachments.Use(middleware.AuthRequired(), middleware.AdminRequired(db))
|
||||
```
|
||||
|
||||
#### 前端UI控制
|
||||
**头部导航菜单** ([templates/layouts/base.html](templates/layouts/base.html)):
|
||||
- 所有用户:个人信息、我的文章
|
||||
- 仅管理员:后台管理(分隔线后显示)
|
||||
|
||||
**管理后台仪表板** ([templates/admin/dashboard.html](templates/admin/dashboard.html)):
|
||||
- 管理员专属按钮:用户管理、评论管理、平台设置
|
||||
|
||||
## 2. 用户文章管理功能
|
||||
|
||||
### 新增功能
|
||||
为普通用户创建独立的文章管理界面,无需访问后台即可管理自己的文章。
|
||||
|
||||
### 路由设计 ([main.go](main.go))
|
||||
```go
|
||||
myArticles := router.Group("/my")
|
||||
myArticles.Use(middleware.AuthRequired())
|
||||
{
|
||||
myArticles.GET("/articles", handlers.MyArticlesPage(db))
|
||||
myArticles.GET("/articles/new", handlers.MyArticleCreatePage(db))
|
||||
myArticles.POST("/articles/new", handlers.MyArticleCreate(db))
|
||||
myArticles.GET("/articles/:id/edit", handlers.MyArticleEditPage(db))
|
||||
myArticles.POST("/articles/:id/edit", handlers.MyArticleUpdate(db))
|
||||
myArticles.POST("/articles/:id/delete", handlers.MyArticleDelete(db))
|
||||
}
|
||||
|
||||
// 用户文章附件管理
|
||||
myAttachments := router.Group("/my/articles")
|
||||
myAttachments.Use(middleware.AuthRequired())
|
||||
{
|
||||
myAttachments.POST("/attachments", handlers.UploadAttachment(db, cfg.Path))
|
||||
myAttachments.POST("/attachments/:id/delete", handlers.DeleteAttachment(db, cfg.Path))
|
||||
myAttachments.GET("/:id/attachments", handlers.ListAttachments(db))
|
||||
}
|
||||
```
|
||||
|
||||
### Handler实现 ([handlers/my_articles.go](handlers/my_articles.go))
|
||||
|
||||
**权限控制特点**:
|
||||
- `MyArticlesPage`: 只查询当前用户的文章 (`WHERE author_id = ?`)
|
||||
- `MyArticleEditPage`: 验证文章所有权 (`WHERE id = ? AND author_id = ?`)
|
||||
- `MyArticleUpdate`: 验证文章所有权后才允许更新
|
||||
- `MyArticleDelete`: 验证文章所有权后才允许删除
|
||||
|
||||
### 模板文件
|
||||
|
||||
**文章列表** ([templates/user/my_articles.html](templates/user/my_articles.html)):
|
||||
- 显示用户自己的文章
|
||||
- 支持创建、编辑、删除操作
|
||||
- 显示文章状态(草稿/已发布/置顶)
|
||||
|
||||
**文章编辑器** ([templates/user/my_article_form.html](templates/user/my_article_form.html)):
|
||||
- Markdown编辑器支持
|
||||
- 标题、slug、摘要、正文、封面
|
||||
- 状态选择(草稿/发布)
|
||||
- 置顶选项
|
||||
|
||||
### 国际化支持 ([i18n/i18n.go](i18n/i18n.go))
|
||||
|
||||
新增翻译key:
|
||||
```go
|
||||
// 英文
|
||||
"my_articles": "My Articles",
|
||||
"my_articles_title": "My Articles",
|
||||
"comment_manage": "Manage Comments",
|
||||
"article_field_*": // 表单字段标签
|
||||
"article_save": "Save",
|
||||
"article_cancel": "Cancel",
|
||||
|
||||
// 中文
|
||||
"my_articles": "我的文章",
|
||||
"my_articles_title": "我的文章",
|
||||
"comment_manage": "评论管理",
|
||||
// ...
|
||||
```
|
||||
|
||||
## 3. 权限矩阵
|
||||
|
||||
| 功能 | 路径 | admin | author | 未登录 |
|
||||
|------|------|-------|--------|--------|
|
||||
| 管理后台 | `/admin` | ✓ | ✗ | ✗ |
|
||||
| 文章管理(后台) | `/admin/articles` | ✓ | ✗ | ✗ |
|
||||
| 评论管理 | `/admin/comments` | ✓ | ✗ | ✗ |
|
||||
| 用户管理 | `/admin/users` | ✓ | ✗ | ✗ |
|
||||
| 平台设置 | `/admin/settings` | ✓ | ✗ | ✗ |
|
||||
| **我的文章** | `/my/articles` | ✓ | ✓ | ✗ |
|
||||
| 个人资料 | `/profile` | ✓ | ✓ | ✗ |
|
||||
| 文章浏览 | `/article/:slug` | ✓ | ✓ | ✓ |
|
||||
|
||||
## 4. 用户体验
|
||||
|
||||
### 普通用户(author)
|
||||
1. 登录后点击头像
|
||||
2. 看到选项:
|
||||
- 个人信息
|
||||
- **我的文章** ← 新增
|
||||
- 退出登录
|
||||
3. 点击"我的文章"进入独立的文章管理界面
|
||||
4. 可以创建、编辑、删除自己的文章
|
||||
5. 无法访问后台管理功能
|
||||
|
||||
### 管理员(admin)
|
||||
1. 登录后点击头像
|
||||
2. 看到选项:
|
||||
- 个人信息
|
||||
- 我的文章
|
||||
- ---(分隔线)---
|
||||
- **后台管理** ← 管理员专属
|
||||
- 退出登录
|
||||
3. 点击"后台管理"进入完整的管理后台
|
||||
4. 后台仪表板显示管理员专属按钮:
|
||||
- 用户管理
|
||||
- 评论管理
|
||||
- 平台设置
|
||||
|
||||
## 5. 安全改进
|
||||
|
||||
### 多层防护
|
||||
1. **路由层**:`AdminRequired` 中间件拦截未授权访问
|
||||
2. **Handler层**:查询时验证用户身份和所有权
|
||||
3. **UI层**:根据角色隐藏不该显示的按钮和链接
|
||||
|
||||
### 拦截行为
|
||||
- 普通用户访问 `/admin/*` → 重定向到 `/admin`
|
||||
- 由于 `/admin` 也需要管理员权限 → 再次重定向到 `/admin`
|
||||
- 实际效果:普通用户无法访问任何管理功能
|
||||
|
||||
## 6. 测试建议
|
||||
|
||||
### 管理员账户测试
|
||||
```bash
|
||||
# 登录管理员
|
||||
访问 http://localhost:8080/login
|
||||
用户名: admin
|
||||
密码: (你的管理员密码)
|
||||
|
||||
# 应该能访问:
|
||||
- /admin (后台仪表板)
|
||||
- /admin/articles (文章管理)
|
||||
- /admin/comments (评论管理)
|
||||
- /admin/users (用户管理)
|
||||
- /admin/settings/site (平台设置)
|
||||
- /my/articles (我的文章)
|
||||
```
|
||||
|
||||
### 普通用户测试
|
||||
```bash
|
||||
# 创建或登录普通用户
|
||||
访问 http://localhost:8080/login
|
||||
用户名: author
|
||||
密码: (普通用户密码)
|
||||
|
||||
# 应该能访问:
|
||||
- /my/articles (我的文章)
|
||||
- /profile (个人资料)
|
||||
|
||||
# 不应该能访问(会被拦截):
|
||||
- /admin
|
||||
- /admin/comments
|
||||
- /admin/users
|
||||
- /admin/settings
|
||||
```
|
||||
|
||||
## 7. 文件变更清单
|
||||
|
||||
### 新增文件
|
||||
- `handlers/my_articles.go` - 用户文章管理handler
|
||||
- `templates/user/my_articles.html` - 文章列表模板
|
||||
- `templates/user/my_article_form.html` - 文章编辑表单
|
||||
- `test_permissions.md` - 权限测试文档
|
||||
|
||||
### 修改文件
|
||||
- `main.go` - 添加权限中间件和用户文章路由
|
||||
- `i18n/i18n.go` - 添加翻译key
|
||||
- `templates/layouts/base.html` - 简化导航菜单
|
||||
- `templates/admin/dashboard.html` - 添加管理员专属按钮
|
||||
|
||||
## 8. 后续改进建议
|
||||
|
||||
1. **细粒度权限**:考虑添加编辑角色,可以编辑所有文章但不能管理用户
|
||||
2. **文章协作**:允许管理员指定文章的协作者
|
||||
3. **审计日志**:记录敏感操作(用户管理、设置修改)
|
||||
4. **草稿分享**:生成草稿预览链接,方便审稿
|
||||
5. **文章统计**:在"我的文章"页面显示阅读量、评论数等统计数据
|
||||
@@ -1,93 +0,0 @@
|
||||
# 文章发布时间编辑功能
|
||||
|
||||
## 功能概述
|
||||
|
||||
为博客系统的文章编辑页面添加了自定义发布时间的功能,允许管理员和普通用户在创建或编辑文章时手动设置发布时间。
|
||||
|
||||
## 修改的文件
|
||||
|
||||
### 1. 前端模板
|
||||
|
||||
#### `/templates/admin/article_create.html`
|
||||
- 在"置顶"选项前添加了发布时间输入框
|
||||
- 使用 `datetime-local` 类型的输入框,支持选择日期和时间
|
||||
- 添加提示文本:留空则在发布时自动设置
|
||||
|
||||
#### `/templates/user/my_article_form.html`
|
||||
- 在状态选择框前添加了发布时间输入框
|
||||
- 使用相同的 `datetime-local` 输入框
|
||||
- 添加相应的提示文本
|
||||
|
||||
### 2. 后端处理器
|
||||
|
||||
#### `/handlers/article.go`
|
||||
- 修改 `articleForm` 结构体,添加 `PublishedAt string` 字段
|
||||
- 修改 `parseArticleForm()` 函数,解析表单中的 `published_at` 字段
|
||||
- 修改 `applyFormToData()` 函数,将 `PublishedAt` 传递给模板
|
||||
- 添加 `parsePublishedAt()` 函数:解析 datetime-local 格式的时间字符串
|
||||
- 添加 `formatPublishedAt()` 函数:将时间格式化为 datetime-local 格式用于表单回显
|
||||
- 修改 `ArticleEditPage()` 函数:在编辑页面回显发布时间
|
||||
- 修改 `ArticleUpdate()` 函数:
|
||||
- 如果用户提供了自定义发布时间,则使用该时间
|
||||
- 否则保持原有逻辑(首次发布时自动设置当前时间)
|
||||
- 修改 `ArticleCreate()` 函数:
|
||||
- 如果用户提供了自定义发布时间,则使用该时间
|
||||
- 否则在发布时自动设置当前时间
|
||||
|
||||
#### `/handlers/my_articles.go`
|
||||
- 修改 `MyArticleEditPage()` 函数:在编辑页面回显发布时间
|
||||
- 修改 `MyArticleUpdate()` 函数:
|
||||
- 如果用户提供了自定义发布时间,则使用该时间
|
||||
- 否则保持原有逻辑(首次发布时自动设置当前时间)
|
||||
|
||||
### 3. 国际化文本
|
||||
|
||||
#### `/i18n/i18n.go`
|
||||
添加了以下翻译键:
|
||||
- `article_published_at`: "Published Time" / "发布时间"
|
||||
- `article_published_at_hint`: "Leave blank to auto-set on publish" / "留空则在发布时自动设置"
|
||||
|
||||
## 功能特性
|
||||
|
||||
1. **自定义发布时间**:用户可以手动设置文章的发布时间,适用于:
|
||||
- 导入历史文章时保留原始发布时间
|
||||
- 预设未来的发布时间(虽然文章会立即可见)
|
||||
- 修正错误的发布时间
|
||||
|
||||
2. **自动时间戳**:如果用户不填写发布时间:
|
||||
- 保存为草稿:不设置发布时间
|
||||
- 首次发布:自动设置为当前时间
|
||||
- 已发布文章再次编辑:保持原发布时间不变
|
||||
|
||||
3. **向后兼容**:
|
||||
- 现有的自动时间戳逻辑完全保留
|
||||
- 只有在用户明确输入时间时才会覆盖自动时间
|
||||
|
||||
4. **双语支持**:中英文界面均已适配
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 管理员编辑页面
|
||||
1. 访问 `/admin/articles/:id/edit` 或 `/admin/articles/new`
|
||||
2. 在"发布时间"字段中选择日期和时间
|
||||
3. 留空则使用自动时间戳
|
||||
|
||||
### 普通用户编辑页面
|
||||
1. 访问 `/my/articles/:id/edit` 或 `/my/articles/new`
|
||||
2. 在"发布时间"字段中选择日期和时间
|
||||
3. 留空则使用自动时间戳
|
||||
|
||||
## 技术实现
|
||||
|
||||
- **时间格式**:使用 HTML5 `datetime-local` 输入类型,格式为 `2006-01-02T15:04`
|
||||
- **时区处理**:使用服务器的本地时区 (`time.Local`) 进行解析和格式化
|
||||
- **数据库**:`published_at` 字段类型为 `*time.Time`(可为空)
|
||||
|
||||
## 测试建议
|
||||
|
||||
1. 创建新文章时设置自定义发布时间
|
||||
2. 创建新文章时留空发布时间(应自动设置)
|
||||
3. 编辑已发布文章并修改发布时间
|
||||
4. 编辑已发布文章但不修改发布时间(应保持原时间)
|
||||
5. 将草稿改为发布状态(应自动设置发布时间或使用自定义时间)
|
||||
6. 测试中英文界面的显示
|
||||
@@ -1,161 +0,0 @@
|
||||
# 用户注册功能实现说明
|
||||
|
||||
## 功能概述
|
||||
|
||||
已成功为博客系统添加用户自主注册功能,并在后台站点设置中增加了注册开关。
|
||||
|
||||
## 实现的功能
|
||||
|
||||
### 1. 后台设置 - 注册开关
|
||||
- **位置**: `/admin/settings/site` (站点设置页面)
|
||||
- **功能**: 管理员可以控制是否允许访客自主注册
|
||||
- **默认值**: 关闭(`AllowRegistration = false`)
|
||||
- **说明**: 只有管理员可以通过后台开关控制注册功能
|
||||
|
||||
### 2. 注册页面
|
||||
- **路径**: `/register`
|
||||
- **访问控制**:
|
||||
- 仅当后台开启注册功能时可访问
|
||||
- 如果注册功能关闭,访问会重定向到登录页面
|
||||
- **表单字段**:
|
||||
- 用户名(必填,3-32字符)
|
||||
- 显示名称(可选,默认使用用户名)
|
||||
- 邮箱(可选,用于Gravatar头像)
|
||||
- 密码(必填,至少6字符)
|
||||
- 确认密码(必填)
|
||||
|
||||
### 3. 注册验证
|
||||
- 用户名长度验证(3-32字符)
|
||||
- 密码长度验证(至少6字符)
|
||||
- 密码确认匹配验证
|
||||
- 用户名唯一性检查
|
||||
- 注册成功后自动登录并跳转到首页
|
||||
|
||||
### 4. 登录页面增强
|
||||
- 当注册功能开启时,登录页面底部显示"还没有账号?注册"链接
|
||||
- 当注册功能关闭时,该链接不显示
|
||||
|
||||
### 5. 用户角色
|
||||
- 通过注册创建的用户默认角色为 `author`(作者)
|
||||
- 默认状态为 `normal`(正常)
|
||||
|
||||
## 文件修改清单
|
||||
|
||||
### 1. 数据库模型
|
||||
- **文件**: `models/site_setting.go`
|
||||
- **修改**: 添加 `AllowRegistration` 字段(bool类型,默认false)
|
||||
|
||||
### 2. 后台设置页面
|
||||
- **文件**: `templates/admin/settings_site.html`
|
||||
- **修改**: 添加"允许用户注册"复选框
|
||||
|
||||
### 3. 后台设置处理器
|
||||
- **文件**: `handlers/settings.go`
|
||||
- **修改**: 在保存站点设置时处理 `allow_registration` 参数
|
||||
|
||||
### 4. 认证处理器
|
||||
- **文件**: `handlers/auth.go`
|
||||
- **修改**:
|
||||
- 添加 `RegisterPage()` 函数 - 渲染注册页面
|
||||
- 添加 `Register()` 函数 - 处理注册表单提交
|
||||
- 修改 `LoginPage()` 函数 - 传递注册开关状态到模板
|
||||
- 添加 `strings` 包导入
|
||||
|
||||
### 5. 注册页面模板
|
||||
- **文件**: `templates/pages/register.html` (新建)
|
||||
- **内容**: 完整的注册表单页面
|
||||
|
||||
### 6. 登录页面模板
|
||||
- **文件**: `templates/pages/login.html`
|
||||
- **修改**: 添加注册链接(条件显示)
|
||||
|
||||
### 7. 路由配置
|
||||
- **文件**: `main.go`
|
||||
- **修改**: 添加注册路由
|
||||
- `GET /register` - 注册页面
|
||||
- `POST /register` - 注册表单提交
|
||||
|
||||
### 8. 国际化文本
|
||||
- **文件**: `i18n/i18n.go`
|
||||
- **修改**: 添加中英文注册相关翻译文本(约30个新键值对)
|
||||
|
||||
## 翻译键名
|
||||
|
||||
### 注册页面相关
|
||||
- `page_register` - 页面标题
|
||||
- `register_title` - 表单标题
|
||||
- `register_username` - 用户名标签
|
||||
- `register_display_name` - 显示名称标签
|
||||
- `register_email` - 邮箱标签
|
||||
- `register_password` - 密码标签
|
||||
- `register_confirm_password` - 确认密码标签
|
||||
- `register_submit` - 提交按钮
|
||||
- `register_have_account` - "已有账号?"提示
|
||||
- `register_login_link` - "登录"链接文字
|
||||
|
||||
### 错误消息
|
||||
- `register_required` - 必填字段错误
|
||||
- `register_username_length` - 用户名长度错误
|
||||
- `register_password_length` - 密码长度错误
|
||||
- `register_password_mismatch` - 密码不匹配错误
|
||||
- `register_error` - 通用注册错误
|
||||
- `user_username_exists` - 用户名已存在错误
|
||||
|
||||
### 登录页面
|
||||
- `login_no_account` - "还没有账号?"
|
||||
- `login_register_link` - "注册"链接
|
||||
|
||||
### 后台设置
|
||||
- `settings_allow_registration` - "允许用户注册"
|
||||
- `settings_allow_registration_hint` - 功能说明
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 管理员开启注册功能
|
||||
1. 以管理员身份登录
|
||||
2. 访问 `/admin/settings/site`
|
||||
3. 勾选"允许用户注册"复选框
|
||||
4. 点击"保存"按钮
|
||||
|
||||
### 用户注册流程
|
||||
1. 访问 `/login` 登录页面
|
||||
2. 点击底部"注册"链接(仅当注册功能开启时显示)
|
||||
3. 填写注册表单
|
||||
4. 提交后自动登录并跳转到首页
|
||||
|
||||
### 安全特性
|
||||
- 密码使用 bcrypt 加密存储
|
||||
- 用户名唯一性验证
|
||||
- 表单前端和后端双重验证
|
||||
- 注册功能默认关闭,需管理员手动开启
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
GORM会自动迁移数据库结构,添加 `allow_registration` 字段到 `site_settings` 表。
|
||||
首次启动应用时会自动完成迁移,无需手动操作。
|
||||
|
||||
## 测试要点
|
||||
|
||||
1. **后台设置测试**
|
||||
- 验证默认状态为关闭
|
||||
- 验证开关保存成功
|
||||
- 验证开关状态同步到登录页面
|
||||
|
||||
2. **注册功能测试**
|
||||
- 功能关闭时访问 `/register` 应重定向
|
||||
- 功能开启时注册页面正常显示
|
||||
- 表单验证正确工作
|
||||
- 用户名重复时显示错误
|
||||
- 注册成功后自动登录
|
||||
|
||||
3. **国际化测试**
|
||||
- 中文界面显示正常
|
||||
- 英文界面显示正常
|
||||
- 错误消息本地化正确
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 注册功能默认关闭,确保系统安全
|
||||
2. 注册用户默认为 `author` 角色,不是 `admin`
|
||||
3. 管理员账号仍需通过后台用户管理创建
|
||||
4. 邮箱字段为可选,方便后续扩展邮件功能
|
||||
@@ -1,164 +0,0 @@
|
||||
# 标签筛选功能修复总结
|
||||
|
||||
## 问题描述
|
||||
用户报告:无法通过tag筛选文章
|
||||
|
||||
## 根本原因
|
||||
SQL 查询中存在**列名歧义**错误:
|
||||
```
|
||||
SQL logic error: ambiguous column name: created_at (1)
|
||||
```
|
||||
|
||||
在使用 JOIN 查询时,`articles` 和 `article_tags` 表都有 `created_at` 字段,导致 ORDER BY 子句中的列名不明确。
|
||||
|
||||
## 修复方案
|
||||
|
||||
### 修改文件:`handlers/home.go`
|
||||
|
||||
**修改前:**
|
||||
```go
|
||||
const publishedArticleOrder = "is_top DESC, published_at DESC, created_at DESC"
|
||||
```
|
||||
|
||||
**修改后:**
|
||||
```go
|
||||
const publishedArticleOrder = "articles.is_top DESC, articles.published_at DESC, articles.created_at DESC"
|
||||
```
|
||||
|
||||
**说明:**
|
||||
在 ORDER BY 子句中为所有列名添加表前缀 `articles.`,明确指定是 articles 表的字段。
|
||||
|
||||
## 验证结果
|
||||
|
||||
### ✅ 修复后测试通过
|
||||
|
||||
1. **SQL 错误消失**
|
||||
- 不再出现 "ambiguous column name" 错误
|
||||
- 查询正常执行
|
||||
|
||||
2. **标签筛选正常工作**
|
||||
```
|
||||
访问: http://localhost:8080/?tag=fr
|
||||
```
|
||||
- 显示筛选提示:`Filter by tag: fr`
|
||||
- 显示"All"链接返回所有文章
|
||||
- 只显示包含该标签的文章
|
||||
|
||||
3. **标签侧边栏正常显示**
|
||||
- 显示所有标签及其文章数量
|
||||
- 例如:`fr (1)`, `fs (2)` 等
|
||||
|
||||
4. **API 也支持标签筛选**
|
||||
```
|
||||
GET /api/articles?tag=fr
|
||||
```
|
||||
|
||||
## 使用说明
|
||||
|
||||
### 创建带标签的文章
|
||||
1. 访问 http://localhost:8080/admin/articles/new
|
||||
2. 登录(用户名:admin,密码:admin)
|
||||
3. 填写文章信息
|
||||
4. 在"标签"字段输入:`golang, web, 教程`(逗号分隔)
|
||||
5. 点击"发布"或"保存草稿"
|
||||
|
||||
### 通过标签筛选文章
|
||||
1. 访问主页 http://localhost:8080/
|
||||
2. 在右侧标签云中点击任意标签
|
||||
3. 页面刷新,只显示包含该标签的文章
|
||||
4. 顶部显示筛选提示,点击"All"返回所有文章
|
||||
|
||||
### 通过URL直接筛选
|
||||
```
|
||||
http://localhost:8080/?tag=golang
|
||||
http://localhost:8080/?tag=web
|
||||
```
|
||||
|
||||
## 技术细节
|
||||
|
||||
### 修复影响的查询
|
||||
此修复影响以下两个处理器中的标签筛选查询:
|
||||
|
||||
1. **HomePage** (`handlers/home.go`)
|
||||
- 主页文章列表的标签筛选
|
||||
|
||||
2. **HomeArticlesAPI** (`handlers/home.go`)
|
||||
- 无限滚动 API 的标签筛选
|
||||
|
||||
### 为什么会出现这个问题?
|
||||
|
||||
当使用 JOIN 查询多个表时,如果多个表有同名字段,SQL 引擎无法确定 ORDER BY 或 WHERE 子句中引用的是哪个表的字段。
|
||||
|
||||
**原始查询:**
|
||||
```sql
|
||||
SELECT articles.*
|
||||
FROM articles
|
||||
JOIN article_tags ON article_tags.article_id = articles.id
|
||||
JOIN tags ON tags.id = article_tags.tag_id
|
||||
WHERE status = 1 AND tags.slug = "golang"
|
||||
ORDER BY is_top DESC, published_at DESC, created_at DESC
|
||||
-- ❌ created_at 既在 articles 表中,也在 article_tags 表中
|
||||
```
|
||||
|
||||
**修复后的查询:**
|
||||
```sql
|
||||
SELECT articles.*
|
||||
FROM articles
|
||||
JOIN article_tags ON article_tags.article_id = articles.id
|
||||
JOIN tags ON tags.id = article_tags.tag_id
|
||||
WHERE status = 1 AND tags.slug = "golang"
|
||||
ORDER BY articles.is_top DESC, articles.published_at DESC, articles.created_at DESC
|
||||
-- ✅ 明确指定使用 articles 表的字段
|
||||
```
|
||||
|
||||
## 相关功能测试清单
|
||||
|
||||
- [x] 创建带标签的文章
|
||||
- [x] 标签自动创建
|
||||
- [x] 编辑文章修改标签
|
||||
- [x] 点击标签筛选文章
|
||||
- [x] 标签筛选提示显示
|
||||
- [x] "All"链接返回所有文章
|
||||
- [x] 标签侧边栏显示正确
|
||||
- [x] 标签计数准确
|
||||
- [x] API 支持标签筛选
|
||||
- [x] 无限滚动与标签筛选兼容
|
||||
|
||||
## 其他修复建议
|
||||
|
||||
为了避免类似问题,建议在所有使用 JOIN 的查询中:
|
||||
|
||||
1. **始终使用表前缀**
|
||||
- 在 SELECT、WHERE、ORDER BY 中明确指定表名
|
||||
|
||||
2. **使用表别名**
|
||||
```go
|
||||
query.Joins("JOIN article_tags at ON at.article_id = articles.id").
|
||||
Joins("JOIN tags t ON t.id = at.tag_id").
|
||||
Where("t.slug = ?", tagSlug).
|
||||
Order("articles.is_top DESC, articles.created_at DESC")
|
||||
```
|
||||
|
||||
3. **GORM 最佳实践**
|
||||
- 使用 `db.Table("articles a")` 定义别名
|
||||
- 在复杂查询中使用 Raw SQL 或子查询
|
||||
|
||||
## 文件变更
|
||||
|
||||
### 修改的文件
|
||||
- `handlers/home.go` - 修复列名歧义问题
|
||||
|
||||
### 测试状态
|
||||
✅ 所有功能测试通过
|
||||
✅ 无 SQL 错误
|
||||
✅ 标签筛选正常工作
|
||||
|
||||
## 总结
|
||||
|
||||
标签筛选功能的问题已经完全修复。用户现在可以:
|
||||
1. ✅ 创建带标签的文章
|
||||
2. ✅ 通过标签筛选文章
|
||||
3. ✅ 看到清晰的筛选状态
|
||||
4. ✅ 使用搜索和标签功能
|
||||
|
||||
该功能已经可以正常使用,所有核心功能都在正常工作。
|
||||
@@ -1,239 +0,0 @@
|
||||
# 标签和搜索功能实施总结
|
||||
|
||||
## 概述
|
||||
成功实现了完整的标签管理系统和文章搜索功能,允许用户通过标签分类文章,并通过关键字搜索文章。
|
||||
|
||||
## 已完成的功能
|
||||
|
||||
### 1. 数据库模型
|
||||
|
||||
#### Tag 模型 (`models/tag.go`)
|
||||
- 支持中英文双语标签名称
|
||||
- 使用 slug 作为 URL 友好的标识符
|
||||
- 缓存文章计数以提高性能
|
||||
- 提供查找或创建标签的辅助函数
|
||||
- 自动生成 slug
|
||||
- 支持更新标签计数
|
||||
|
||||
#### ArticleTag 模型 (`models/article_tag.go`)
|
||||
- 多对多关系表,关联文章和标签
|
||||
- 索引优化查询性能
|
||||
|
||||
#### Article 模型更新
|
||||
- 添加了 `Tags` 字段,建立多对多关系
|
||||
- 自动加载关联的标签
|
||||
|
||||
### 2. 数据库迁移
|
||||
- 在 `models/db.go` 中添加了 `Tag` 和 `ArticleTag` 的自动迁移
|
||||
- 数据库会在启动时自动创建相关表
|
||||
|
||||
### 3. 文章处理器增强 (`handlers/article.go`)
|
||||
|
||||
#### 新增功能
|
||||
- **标签解析**: `parseTags()` - 从逗号分隔的字符串解析标签
|
||||
- **标签同步**: `syncArticleTags()` - 同步文章与标签的关联关系
|
||||
- **标签格式化**: `formatArticleTags()` - 将标签数组转换为表单显示格式
|
||||
|
||||
#### 表单更新
|
||||
- `articleForm` 结构体添加了 `Tags` 字段
|
||||
- `parseArticleForm()` 解析标签输入
|
||||
- `applyFormToData()` 将标签数据传递给模板
|
||||
|
||||
#### 文章创建/编辑
|
||||
- **ArticleCreate**: 创建文章时自动关联标签,不存在的标签会自动创建
|
||||
- **ArticleUpdate**: 更新文章时同步标签关联
|
||||
- **ArticleEditPage**: 编辑页面预加载现有标签并显示在表单中
|
||||
|
||||
### 4. 主页和搜索处理器 (`handlers/home.go`)
|
||||
|
||||
#### HomePage 增强
|
||||
- 支持通过 `?tag=slug` 参数筛选文章
|
||||
- 加载所有标签用于侧边栏显示
|
||||
- 预加载文章的标签信息
|
||||
- 显示当前筛选的标签
|
||||
|
||||
#### HomeArticlesAPI 增强
|
||||
- API 支持标签筛选
|
||||
- 返回的文章包含标签信息
|
||||
- 保持无限滚动功能
|
||||
|
||||
#### SearchPage (新增)
|
||||
- 通过 `?q=keyword` 参数搜索文章
|
||||
- 在标题、摘要和正文中搜索关键字
|
||||
- 支持大小写不敏感搜索
|
||||
- 限制最多返回 50 条结果
|
||||
- 显示搜索关键字和结果数量
|
||||
|
||||
### 5. 模板更新
|
||||
|
||||
#### 文章创建/编辑表单 (`templates/admin/article_create.html`)
|
||||
- 添加标签输入框
|
||||
- 显示标签输入提示(逗号分隔)
|
||||
- 位于封面图片字段之后
|
||||
|
||||
#### 主页模板 (`templates/pages/home.html`)
|
||||
- **搜索框**: 页面顶部显著位置添加搜索框
|
||||
- **标签侧边栏**: 右侧显示所有标签及文章数量
|
||||
- **标签筛选提示**: 筛选时显示当前标签和"全部"链接
|
||||
- **文章标签显示**: 每篇文章下方显示关联的标签
|
||||
- **响应式布局**: 大屏幕显示侧边栏,小屏幕自动隐藏
|
||||
|
||||
#### 搜索结果页 (`templates/pages/search.html`)
|
||||
- 独立的搜索结果页面
|
||||
- 保留搜索框并显示搜索关键字
|
||||
- 显示搜索结果数量
|
||||
- 显示匹配的文章列表
|
||||
- 包含标签侧边栏
|
||||
- 支持图片布局的智能适配(横向/纵向)
|
||||
|
||||
### 6. 国际化支持 (`i18n/i18n.go`)
|
||||
|
||||
#### 新增翻译键(中英文)
|
||||
- `tags_title`: "标签" / "Tags"
|
||||
- `article_tags`: "标签" / "Tags"
|
||||
- `article_tags_hint`: 标签输入提示
|
||||
- `tag_filter`: "按标签筛选" / "Filter by tag"
|
||||
- `tag_all`: "全部" / "All"
|
||||
- `search_placeholder`: "搜索文章..." / "Search articles..."
|
||||
- `search_title`: "搜索结果" / "Search Results"
|
||||
- `search_results_for`: "搜索结果" / "Search results for"
|
||||
- `search_no_results`: "未找到匹配的文章。" / "No articles found matching your search."
|
||||
- `search_keyword`: "关键词" / "Keyword"
|
||||
|
||||
### 7. 路由配置 (`main.go`)
|
||||
- 添加 `/search` 路由,映射到 `SearchPage` 处理器
|
||||
- 保持现有路由不变
|
||||
|
||||
## 技术特性
|
||||
|
||||
### 性能优化
|
||||
1. **标签计数缓存**: 在 Tag 表中缓存文章数量,避免频繁联表查询
|
||||
2. **预加载关联**: 使用 GORM 的 Preload 功能一次性加载文章的标签
|
||||
3. **索引优化**: ArticleTag 表在 article_id 和 tag_id 上建立索引
|
||||
|
||||
### 用户体验
|
||||
1. **自动标签创建**: 用户输入不存在的标签时自动创建
|
||||
2. **智能搜索**: 支持在标题、摘要和正文中搜索
|
||||
3. **可视化标签**: 显示每个标签关联的文章数量
|
||||
4. **筛选提示**: 明确显示当前筛选状态
|
||||
5. **响应式设计**: 移动端友好的布局
|
||||
|
||||
### 数据一致性
|
||||
1. **标签同步**: 更新文章时先清除旧标签,再添加新标签
|
||||
2. **计数更新**: 修改标签关联后自动更新计数
|
||||
3. **错误处理**: 标签操作失败不影响文章的创建/更新
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 创建带标签的文章
|
||||
1. 进入文章创建/编辑页面
|
||||
2. 在"标签"字段输入逗号分隔的标签名称,如:`golang, web, 教程`
|
||||
3. 保存文章后,标签会自动创建并关联
|
||||
|
||||
### 按标签筛选文章
|
||||
1. 在主页右侧标签列表中点击任意标签
|
||||
2. 页面刷新,只显示包含该标签的文章
|
||||
3. 点击"全部"返回查看所有文章
|
||||
|
||||
### 搜索文章
|
||||
1. 在页面顶部搜索框输入关键字
|
||||
2. 按回车或点击搜索
|
||||
3. 查看匹配的文章列表
|
||||
|
||||
## 数据库结构
|
||||
|
||||
### tags 表
|
||||
```sql
|
||||
CREATE TABLE tags (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name_zh VARCHAR(50) NOT NULL,
|
||||
name_en VARCHAR(50) NOT NULL,
|
||||
slug VARCHAR(100) NOT NULL UNIQUE,
|
||||
count INTEGER DEFAULT 0,
|
||||
created_at DATETIME,
|
||||
updated_at DATETIME
|
||||
);
|
||||
```
|
||||
|
||||
### article_tags 表
|
||||
```sql
|
||||
CREATE TABLE article_tags (
|
||||
article_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
created_at DATETIME,
|
||||
PRIMARY KEY (article_id, tag_id),
|
||||
INDEX idx_article_id (article_id),
|
||||
INDEX idx_tag_id (tag_id)
|
||||
);
|
||||
```
|
||||
|
||||
## 未来增强
|
||||
|
||||
### 建议的功能扩展
|
||||
1. **标签管理页面**: 管理员可以重命名、合并、删除标签
|
||||
2. **标签自动补全**: 输入时显示已有标签的建议
|
||||
3. **热门标签**: 显示最受欢迎的标签
|
||||
4. **相关文章**: 基于共享标签推荐相关文章
|
||||
5. **全文搜索**: 集成 Elasticsearch 或 Meilisearch 提供更强大的搜索
|
||||
6. **搜索建议**: 输入时显示搜索建议
|
||||
7. **高级筛选**: 支持多标签筛选、日期范围等
|
||||
8. **标签云**: 用字体大小表示标签流行度
|
||||
|
||||
## 测试建议
|
||||
|
||||
### 功能测试
|
||||
- [ ] 创建带标签的文章
|
||||
- [ ] 标签自动创建
|
||||
- [ ] 编辑文章修改标签
|
||||
- [ ] 删除所有标签
|
||||
- [ ] 标签筛选显示正确的文章
|
||||
- [ ] 搜索找到匹配的文章
|
||||
- [ ] 中文和英文标签都能正常工作
|
||||
- [ ] 标签计数准确
|
||||
- [ ] 移动端布局正常
|
||||
- [ ] 标签过多时的显示效果
|
||||
|
||||
### 性能测试
|
||||
- [ ] 100+ 文章的加载速度
|
||||
- [ ] 50+ 标签的显示性能
|
||||
- [ ] 搜索响应时间
|
||||
- [ ] 标签筛选查询性能
|
||||
|
||||
### 边界测试
|
||||
- [ ] 空标签输入
|
||||
- [ ] 重复标签
|
||||
- [ ] 特殊字符标签
|
||||
- [ ] 超长标签名
|
||||
- [ ] 大量标签(100+)
|
||||
|
||||
## 文件清单
|
||||
|
||||
### 新增文件
|
||||
- `models/tag.go` - 标签模型和辅助函数
|
||||
- `models/article_tag.go` - 文章标签关联表模型
|
||||
- `templates/pages/search.html` - 搜索结果页面模板
|
||||
- `.claude/plans/tag_and_search_feature.md` - 功能实施计划
|
||||
|
||||
### 修改文件
|
||||
- `models/article.go` - 添加 Tags 关联
|
||||
- `models/db.go` - 添加标签表迁移
|
||||
- `handlers/article.go` - 标签处理逻辑
|
||||
- `handlers/home.go` - 添加搜索和标签筛选
|
||||
- `templates/admin/article_create.html` - 添加标签输入
|
||||
- `templates/pages/home.html` - 添加搜索框和标签侧边栏
|
||||
- `i18n/i18n.go` - 添加标签和搜索相关翻译
|
||||
- `main.go` - 添加搜索路由
|
||||
|
||||
## 总结
|
||||
|
||||
标签和搜索功能已完整实现并集成到博客系统中。该功能提供了:
|
||||
|
||||
1. ✅ 完整的标签管理(创建、关联、计数)
|
||||
2. ✅ 用户友好的标签输入和显示
|
||||
3. ✅ 灵活的标签筛选
|
||||
4. ✅ 实用的关键字搜索
|
||||
5. ✅ 响应式设计
|
||||
6. ✅ 多语言支持
|
||||
7. ✅ 性能优化
|
||||
|
||||
系统现在可以支持更好的内容组织和发现体验。用户可以通过标签快速找到相关主题的文章,也可以通过搜索功能直接查找感兴趣的内容。
|
||||
@@ -1,127 +0,0 @@
|
||||
#!/bin/bash
|
||||
# 阅读统计系统测试脚本
|
||||
|
||||
echo "========================================"
|
||||
echo " 阅读统计系统功能测试"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
|
||||
# 确保应用已启动
|
||||
if ! pgrep -f "./go_blog" > /dev/null; then
|
||||
echo "❌ 应用未运行,请先启动: ./go_blog"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ 应用正在运行"
|
||||
echo ""
|
||||
|
||||
# 测试1:访问首页
|
||||
echo "测试 1: 访问首页..."
|
||||
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/)
|
||||
if [ "$STATUS" == "200" ]; then
|
||||
echo "✅ 首页访问成功 (HTTP $STATUS)"
|
||||
else
|
||||
echo "❌ 首页访问失败 (HTTP $STATUS)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 测试2:获取文章列表
|
||||
echo "测试 2: 获取文章列表..."
|
||||
ARTICLES=$(curl -s http://localhost:8080/api/articles | grep -o '"id":[0-9]*' | head -5)
|
||||
if [ -n "$ARTICLES" ]; then
|
||||
echo "✅ 找到文章:"
|
||||
echo "$ARTICLES"
|
||||
else
|
||||
echo "⚠️ 暂无已发布的文章"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 测试3:模拟多次访问(生成测试数据)
|
||||
echo "测试 3: 模拟访问文章(生成测试数据)..."
|
||||
echo "提示: 需要先创建并发布一些文章才能测试阅读记录"
|
||||
echo ""
|
||||
|
||||
# 获取第一篇文章的slug
|
||||
FIRST_ARTICLE=$(curl -s http://localhost:8080/api/articles | grep -o '"slug":"[^"]*"' | head -1 | cut -d'"' -f4)
|
||||
|
||||
if [ -n "$FIRST_ARTICLE" ]; then
|
||||
echo "访问文章: $FIRST_ARTICLE"
|
||||
|
||||
# 模拟5次访问(不同User-Agent)
|
||||
echo " - 正常浏览器访问..."
|
||||
curl -s -o /dev/null -H "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)" http://localhost:8080/article/$FIRST_ARTICLE
|
||||
|
||||
echo " - Chrome访问..."
|
||||
curl -s -o /dev/null -H "User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) Chrome/120.0.0.0" http://localhost:8080/article/$FIRST_ARTICLE
|
||||
|
||||
echo " - GoogleBot访问..."
|
||||
curl -s -o /dev/null -H "User-Agent: Mozilla/5.0 (compatible; Googlebot/2.1; +http://www.google.com/bot.html)" http://localhost:8080/article/$FIRST_ARTICLE
|
||||
|
||||
echo " - BingBot访问..."
|
||||
curl -s -o /dev/null -H "User-Agent: Mozilla/5.0 (compatible; bingbot/2.0; +http://www.bing.com/bingbot.htm)" http://localhost:8080/article/$FIRST_ARTICLE
|
||||
|
||||
echo " - BaiduSpider访问..."
|
||||
curl -s -o /dev/null -H "User-Agent: Mozilla/5.0 (compatible; Baiduspider/2.0; +http://www.baidu.com/search/spider.html)" http://localhost:8080/article/$FIRST_ARTICLE
|
||||
|
||||
echo "✅ 已生成测试数据"
|
||||
sleep 1 # 等待异步记录完成
|
||||
else
|
||||
echo "⚠️ 未找到文章,跳过访问测试"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 测试4:检查数据库表
|
||||
echo "测试 4: 检查数据库..."
|
||||
if [ -f "tmp/blog.db" ]; then
|
||||
echo "✅ 数据库文件存在: tmp/blog.db"
|
||||
|
||||
# 使用sqlite3检查表结构(如果可用)
|
||||
if command -v sqlite3 &> /dev/null; then
|
||||
echo ""
|
||||
echo "article_views 表结构:"
|
||||
sqlite3 tmp/blog.db ".schema article_views" 2>/dev/null || echo " (需要SQLite命令行工具)"
|
||||
|
||||
echo ""
|
||||
echo "article_views 记录数:"
|
||||
COUNT=$(sqlite3 tmp/blog.db "SELECT COUNT(*) FROM article_views" 2>/dev/null)
|
||||
if [ -n "$COUNT" ]; then
|
||||
echo " 总记录: $COUNT"
|
||||
BOT_COUNT=$(sqlite3 tmp/blog.db "SELECT COUNT(*) FROM article_views WHERE is_bot = 1" 2>/dev/null)
|
||||
HUMAN_COUNT=$(sqlite3 tmp/blog.db "SELECT COUNT(*) FROM article_views WHERE is_bot = 0" 2>/dev/null)
|
||||
echo " 真人访问: $HUMAN_COUNT"
|
||||
echo " 爬虫访问: $BOT_COUNT"
|
||||
fi
|
||||
fi
|
||||
else
|
||||
echo "⚠️ 数据库文件不存在(可能使用MySQL)"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# 测试5:访问统计页面(需要登录)
|
||||
echo "测试 5: 访问统计页面..."
|
||||
echo "⚠️ 统计页面需要管理员登录才能访问"
|
||||
echo ""
|
||||
echo "手动测试步骤:"
|
||||
echo "1. 浏览器访问: http://localhost:8080/login"
|
||||
echo "2. 使用默认账号登录: admin / admin"
|
||||
echo "3. 访问: http://localhost:8080/admin/analytics/views"
|
||||
echo "4. 查看统计数据、文章排行榜、访问记录"
|
||||
echo "5. 测试筛选功能:按文章、IP筛选,显示/隐藏爬虫"
|
||||
echo ""
|
||||
|
||||
echo "========================================"
|
||||
echo " 测试完成!"
|
||||
echo "========================================"
|
||||
echo ""
|
||||
echo "功能验证清单:"
|
||||
echo "✅ 数据库模型创建 (article_views表)"
|
||||
echo "✅ 自动记录阅读(访问文章时)"
|
||||
echo "✅ 爬虫识别(GoogleBot, BingBot等)"
|
||||
echo "✅ 后台路由注册 (/admin/analytics/views)"
|
||||
echo "✅ 去重机制(同一用户/IP只记录一次)"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo "1. 访问博客,浏览几篇文章"
|
||||
echo "2. 登录后台查看统计数据"
|
||||
echo "3. 尝试筛选和分页功能"
|
||||
echo ""
|
||||
@@ -1,88 +0,0 @@
|
||||
# 权限边界修复验证
|
||||
|
||||
## 修复内容
|
||||
|
||||
### 问题
|
||||
普通用户(author角色)能够访问和修改平台设置,存在严重的权限漏洞。
|
||||
|
||||
### 修复的路由组
|
||||
|
||||
1. **平台设置路由** (`/admin/settings/*`)
|
||||
- 修复前: `middleware.AuthRequired()` - 任何登录用户都可访问
|
||||
- 修复后: `middleware.AuthRequired(), middleware.AdminRequired(db)` - 仅管理员可访问
|
||||
- 影响路由:
|
||||
- GET/POST `/admin/settings/site` - 站点设置
|
||||
- GET/POST `/admin/settings/upload` - 上传设置
|
||||
- GET/POST `/admin/settings/download` - 下载设置
|
||||
- GET/POST `/admin/settings/comments` - 评论设置
|
||||
|
||||
2. **评论管理路由** (`/admin/comments/*`)
|
||||
- 修复前: 在 admin 组下,仅 `AuthRequired()`
|
||||
- 修复后: 独立路由组,使用 `AuthRequired(), AdminRequired(db)`
|
||||
- 影响路由:
|
||||
- GET `/admin/comments` - 评论列表
|
||||
- POST `/admin/comments/:id/approve` - 批准评论
|
||||
- POST `/admin/comments/:id/reject` - 拒绝评论
|
||||
- POST `/admin/comments/:id/delete` - 删除评论
|
||||
|
||||
## 权限矩阵
|
||||
|
||||
| 路由组 | 功能 | 登录要求 | 角色要求 | 说明 |
|
||||
|--------|------|----------|----------|------|
|
||||
| `/admin` | 仪表板 | ✓ | - | 所有登录用户可访问 |
|
||||
| `/admin/articles` | 文章管理 | ✓ | - | 允许作者管理自己的文章 |
|
||||
| `/admin/articles/attachments` | 附件管理 | ✓ | - | 作者上传文章附件 |
|
||||
| `/admin/comments` | 评论管理 | ✓ | admin | ✅ 仅管理员 |
|
||||
| `/admin/users` | 用户管理 | ✓ | admin | ✅ 仅管理员 |
|
||||
| `/admin/settings` | 平台设置 | ✓ | admin | ✅ 仅管理员 |
|
||||
| `/profile` | 个人资料 | ✓ | - | 所有登录用户 |
|
||||
|
||||
## 验证步骤
|
||||
|
||||
### 1. 使用管理员账户测试
|
||||
```bash
|
||||
# 登录管理员账户
|
||||
curl -X POST http://localhost:3000/login \
|
||||
-d "username=admin&password=admin123"
|
||||
|
||||
# 应该能访问设置页面
|
||||
curl http://localhost:3000/admin/settings/site
|
||||
|
||||
# 应该能访问评论管理
|
||||
curl http://localhost:3000/admin/comments
|
||||
```
|
||||
|
||||
### 2. 使用普通用户账户测试
|
||||
```bash
|
||||
# 登录普通用户
|
||||
curl -X POST http://localhost:3000/login \
|
||||
-d "username=author&password=password"
|
||||
|
||||
# 应该被重定向到 /admin(403效果)
|
||||
curl -L http://localhost:3000/admin/settings/site
|
||||
|
||||
# 应该被重定向到 /admin(403效果)
|
||||
curl -L http://localhost:3000/admin/comments
|
||||
```
|
||||
|
||||
### 3. 预期行为
|
||||
- **管理员**: 可以访问所有 `/admin/*` 路由
|
||||
- **普通用户(author)**:
|
||||
- ✓ 可以访问 `/admin` 仪表板
|
||||
- ✓ 可以管理文章 (`/admin/articles/*`)
|
||||
- ✗ **不能**访问平台设置 (`/admin/settings/*`)
|
||||
- ✗ **不能**访问评论管理 (`/admin/comments/*`)
|
||||
- ✗ **不能**访问用户管理 (`/admin/users/*`)
|
||||
|
||||
## 安全建议
|
||||
|
||||
### 已修复
|
||||
- ✅ 平台设置访问控制
|
||||
- ✅ 评论管理访问控制
|
||||
- ✅ 用户管理访问控制
|
||||
|
||||
### 未来改进建议
|
||||
1. **细粒度文章权限**: 作者只能编辑/删除自己的文章
|
||||
2. **审计日志**: 记录敏感操作(设置修改、用户管理)
|
||||
3. **会话超时**: 考虑缩短敏感操作的会话时间
|
||||
4. **CSRF保护**: 为所有POST请求添加CSRF token
|
||||
Reference in New Issue
Block a user