feat: 门户网站初始提交
- Go + Gin + html/template 服务端渲染 - 主页:Google 风格搜索框 + 导航卡片 - 后台:卡片 CRUD、搜索引擎配置、主页背景/标题配置 - 图片上传:支持 jpg/jpeg/png/gif,自动压缩,缩略图参数 ?thumb=1 - 安全:登录日志、修改密码、IP 自动封禁、IP 白名单 - 访问统计:主页访问/卡片点击/搜索追踪、实时流量、IP 统计 - SQLite 存储(modernc.org/sqlite,纯 Go) - 内存 Session + bcrypt 密码哈希
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
classDiagram
|
||||
direction LR
|
||||
|
||||
class Card {
|
||||
+int ID
|
||||
+string Icon
|
||||
+string Title
|
||||
+string Subtitle
|
||||
+string URL
|
||||
+int Sort
|
||||
+bool Enabled
|
||||
+time Time CreatedAt
|
||||
}
|
||||
|
||||
class Setting {
|
||||
+string Key
|
||||
+string Value
|
||||
}
|
||||
|
||||
class Admin {
|
||||
+int ID
|
||||
+string Username
|
||||
+string Password
|
||||
}
|
||||
|
||||
class SessionData {
|
||||
+int AdminID
|
||||
+string Username
|
||||
+time Time CreatedAt
|
||||
}
|
||||
|
||||
class SessionStore {
|
||||
-map~string-SessionData~ store
|
||||
-sync.RWMutex mu
|
||||
+Create(adminID int, username string) string
|
||||
+Get(sessionID string) *SessionData
|
||||
+Delete(sessionID string)
|
||||
}
|
||||
|
||||
class CardModel {
|
||||
+GetAllCards() []Card
|
||||
+GetEnabledCards() []Card
|
||||
+GetCardByID(id int) *Card
|
||||
+CreateCard(card *Card) error
|
||||
+UpdateCard(card *Card) error
|
||||
+DeleteCard(id int) error
|
||||
+ToggleCard(id int) error
|
||||
+MoveCardUp(id int) error
|
||||
+MoveCardDown(id int) error
|
||||
}
|
||||
|
||||
class SettingModel {
|
||||
+GetSetting(key string) string
|
||||
+SetSetting(key string, value string) error
|
||||
+GetSearchEngines() map~string-string
|
||||
+SetSearchEngine(name string, url string) error
|
||||
}
|
||||
|
||||
class AdminModel {
|
||||
+GetAdminByUsername(username string) *Admin
|
||||
+CreateAdmin(username string, password string) error
|
||||
+VerifyPassword(username string, password string) bool
|
||||
+ChangePassword(adminID int, newPassword string) error
|
||||
}
|
||||
|
||||
class HandlerHome {
|
||||
+HomeHandler(c *gin.Context)
|
||||
}
|
||||
|
||||
class HandlerAdmin {
|
||||
+LoginGet(c *gin.Context)
|
||||
+LoginPost(c *gin.Context)
|
||||
+Logout(c *gin.Context)
|
||||
+AdminIndex(c *gin.Context)
|
||||
}
|
||||
|
||||
class HandlerCards {
|
||||
+CardsList(c *gin.Context)
|
||||
+CardCreateGet(c *gin.Context)
|
||||
+CardCreatePost(c *gin.Context)
|
||||
+CardEditGet(c *gin.Context)
|
||||
+CardEditPost(c *gin.Context)
|
||||
+CardDelete(c *gin.Context)
|
||||
+CardToggle(c *gin.Context)
|
||||
+CardMoveUp(c *gin.Context)
|
||||
+CardMoveDown(c *gin.Context)
|
||||
}
|
||||
|
||||
class HandlerSettings {
|
||||
+SettingsGet(c *gin.Context)
|
||||
+SettingsPost(c *gin.Context)
|
||||
}
|
||||
|
||||
class AuthMiddleware {
|
||||
+AuthRequired() gin.HandlerFunc
|
||||
}
|
||||
|
||||
CardModel --> Card : returns
|
||||
SettingModel --> Setting : returns
|
||||
AdminModel --> Admin : returns
|
||||
SessionStore --> SessionData : stores
|
||||
HandlerHome --> CardModel : uses
|
||||
HandlerHome --> SettingModel : uses
|
||||
HandlerAdmin --> AdminModel : uses
|
||||
HandlerAdmin --> SessionStore : uses
|
||||
HandlerCards --> CardModel : uses
|
||||
HandlerSettings --> SettingModel : uses
|
||||
AuthMiddleware --> SessionStore : uses
|
||||
@@ -0,0 +1,53 @@
|
||||
sequenceDiagram
|
||||
participant U as 管理员浏览器
|
||||
participant G as Gin Router
|
||||
participant MW as AuthMiddleware
|
||||
participant H as HandlerCards
|
||||
participant CM as CardModel
|
||||
|
||||
Note over U,CM: 卡片列表
|
||||
U->>G: GET /admin/cards
|
||||
G->>MW: AuthRequired()
|
||||
MW->>MW: 检查 Cookie session_id
|
||||
alt 未登录
|
||||
MW-->>U: 302 → /admin/login
|
||||
else 已登录
|
||||
MW->>H: CardsList(c)
|
||||
H->>CM: GetAllCards()
|
||||
CM-->>H: []Card
|
||||
H-->>U: 卡片列表 HTML
|
||||
end
|
||||
|
||||
Note over U,CM: 新增卡片
|
||||
U->>G: GET /admin/cards/new
|
||||
G->>H: CardCreateGet(c)
|
||||
H-->>U: 空表单 HTML
|
||||
|
||||
U->>G: POST /admin/cards {icon, title, subtitle, url}
|
||||
G->>H: CardCreatePost(c)
|
||||
H->>CM: CreateCard(&Card{...})
|
||||
CM->>CM: INSERT INTO cards(...)
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
|
||||
Note over U,CM: 编辑卡片
|
||||
U->>G: GET /admin/cards/:id/edit
|
||||
G->>H: CardEditGet(c)
|
||||
H->>CM: GetCardByID(id)
|
||||
CM-->>H: *Card
|
||||
H-->>U: 预填表单 HTML
|
||||
|
||||
U->>G: POST /admin/cards/:id {icon, title, subtitle, url}
|
||||
G->>H: CardEditPost(c)
|
||||
H->>CM: UpdateCard(&Card{...})
|
||||
CM->>CM: UPDATE cards SET ... WHERE id=?
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
|
||||
Note over U,CM: 删除卡片
|
||||
U->>G: POST /admin/cards/:id/delete
|
||||
G->>H: CardDelete(c)
|
||||
H->>CM: DeleteCard(id)
|
||||
CM->>CM: DELETE FROM cards WHERE id=?
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
@@ -0,0 +1,18 @@
|
||||
sequenceDiagram
|
||||
participant U as 用户浏览器
|
||||
participant G as Gin Router
|
||||
participant H as HomeHandler
|
||||
participant CM as CardModel
|
||||
participant SM as SettingModel
|
||||
participant T as html/template
|
||||
|
||||
U->>G: GET /
|
||||
G->>H: HomeHandler(c)
|
||||
H->>CM: GetEnabledCards()
|
||||
CM->>CM: SELECT * FROM cards WHERE enabled=1 ORDER BY sort
|
||||
CM-->>H: []Card
|
||||
H->>SM: GetSetting("search_engine")
|
||||
SM->>SM: SELECT value FROM settings WHERE key='search_engine'
|
||||
SM-->>H: search URL template
|
||||
H->>T: ExecuteTemplate("home.html", data)
|
||||
T-->>U: HTML (搜索框 + 卡片网格)
|
||||
@@ -0,0 +1,25 @@
|
||||
sequenceDiagram
|
||||
participant U as 用户浏览器
|
||||
participant G as Gin Router
|
||||
participant H as HandlerAdmin
|
||||
participant AM as AdminModel
|
||||
participant SS as SessionStore
|
||||
|
||||
U->>G: GET /admin/login
|
||||
G->>H: LoginGet(c)
|
||||
H-->>U: 登录页 HTML
|
||||
|
||||
U->>G: POST /admin/login {username, password}
|
||||
G->>H: LoginPost(c)
|
||||
H->>AM: VerifyPassword(username, password)
|
||||
AM->>AM: bcrypt.CompareHashAndPassword()
|
||||
AM-->>H: true/false
|
||||
|
||||
alt 验证成功
|
||||
H->>SS: Create(adminID, username)
|
||||
SS-->>H: sessionID
|
||||
H->>H: c.SetCookie("session_id", sessionID, 86400, "/", "", false, true)
|
||||
H-->>U: 302 Redirect → /admin
|
||||
else 验证失败
|
||||
H-->>U: 200 + 登录页(错误提示)
|
||||
end
|
||||
@@ -0,0 +1,10 @@
|
||||
sequenceDiagram
|
||||
participant U as 用户浏览器
|
||||
participant JS as 前端 JavaScript
|
||||
participant SE as 搜索引擎
|
||||
|
||||
U->>JS: 输入关键词,按回车/点击搜索
|
||||
JS->>JS: 读取隐藏域 search_url_template
|
||||
JS->>JS: fmt.Sprintf(template, query)
|
||||
JS->>U: window.location.href = 拼接后的URL
|
||||
U->>SE: HTTP 请求到搜索引擎
|
||||
@@ -0,0 +1,673 @@
|
||||
# Simple Portal — 系统架构设计文档
|
||||
|
||||
## Part A: 系统设计
|
||||
|
||||
---
|
||||
|
||||
### 1. 实现方案分析
|
||||
|
||||
#### 核心技术挑战
|
||||
|
||||
| 挑战 | 解决方案 |
|
||||
|------|---------|
|
||||
| 纯服务端渲染,无 SPA 路由 | Gin 路由 + html/template 模板继承(layout 嵌套子模板),表单 POST 提交后 302 重定向 |
|
||||
| SQLite 并发写入 | 单文件 SQLite + WAL 模式,Go 侧使用全局 `*sql.DB` 连接池(`SetMaxOpenConns(1)` 保证串行写) |
|
||||
| Session 管理(无需外部依赖) | 内存 `map[string]*Session` + `sync.RWMutex`,Cookie 存储 session ID,重启失效可接受 |
|
||||
| 卡片排序 | `sort` 字段整数排序,上移/下移通过交换相邻卡片的 sort 值实现 |
|
||||
| 搜索引擎占位符 | settings 表存储引擎 URL 模板(`%s` 占位符),前端 JS 拼接跳转,后端验证模板合法性 |
|
||||
|
||||
#### 框架与库选型
|
||||
|
||||
| 组件 | 选型 | 理由 |
|
||||
|------|------|------|
|
||||
| HTTP 框架 | `github.com/gin-gonic/gin` | PRD 指定 |
|
||||
| SQLite 驱动 | `modernc.org/sqlite` | 纯 Go 实现,无需 CGO,交叉编译友好 |
|
||||
| 密码哈希 | `golang.org/x/crypto/bcrypt` | PRD 指定,工业标准 |
|
||||
| 模板引擎 | `html/template` | PRD 指定,Go 原生 |
|
||||
| CSS | 单文件 `style.css` | PRD 约束,无 CSS 框架 |
|
||||
|
||||
#### 架构模式
|
||||
|
||||
采用 **经典 MVC 分层**:
|
||||
|
||||
```
|
||||
Request → Gin Router → Middleware(auth) → Handler → Model → SQLite
|
||||
↓
|
||||
Template Rendering → HTML Response
|
||||
```
|
||||
|
||||
- **Model**:纯数据访问层,封装 SQL 操作
|
||||
- **Handler**(Controller):处理 HTTP 请求,调用 Model,渲染模板
|
||||
- **Template**(View):html/template 模板文件
|
||||
- **Middleware**:鉴权拦截,未登录重定向到登录页
|
||||
- **Session**:独立包,提供 Create/Get/Delete 操作
|
||||
|
||||
---
|
||||
|
||||
### 2. 文件列表
|
||||
|
||||
```
|
||||
Portal_page/
|
||||
├── main.go # 程序入口:初始化 DB、路由、启动服务
|
||||
├── go.mod # Go 模块定义
|
||||
├── go.sum # 依赖校验
|
||||
│
|
||||
├── database/
|
||||
│ └── db.go # DB 初始化、表创建、全局 *sql.DB
|
||||
│
|
||||
├── models/
|
||||
│ ├── card.go # Card CRUD + 排序操作
|
||||
│ ├── setting.go # Setting 读写(搜索引擎配置等)
|
||||
│ └── admin.go # Admin 用户操作 + 密码验证
|
||||
│
|
||||
├── handlers/
|
||||
│ ├── home.go # 主页渲染(卡片列表 + 搜索框)
|
||||
│ ├── admin.go # 登录/登出 + 后台首页
|
||||
│ ├── cards.go # 卡片增删改查 + 启用/禁用 + 排序
|
||||
│ └── settings.go # 搜索引擎配置页面 + 保存
|
||||
│
|
||||
├── middleware/
|
||||
│ └── auth.go # 登录鉴权中间件
|
||||
│
|
||||
├── session/
|
||||
│ └── session.go # 内存 Session 管理
|
||||
│
|
||||
├── templates/
|
||||
│ ├── layout.html # 基础布局(head/nav/footer)
|
||||
│ ├── home.html # 主页模板
|
||||
│ └── admin/
|
||||
│ ├── login.html # 登录页
|
||||
│ ├── index.html # 后台首页(仪表盘)
|
||||
│ ├── cards.html # 卡片列表管理
|
||||
│ ├── card_form.html # 卡片新增/编辑表单
|
||||
│ └── settings.html # 搜索引擎设置
|
||||
│
|
||||
└── static/
|
||||
└── style.css # 全局样式
|
||||
```
|
||||
|
||||
#### 各文件职责说明
|
||||
|
||||
| 文件 | 职责 |
|
||||
|------|------|
|
||||
| `main.go` | 加载配置、初始化 DB、注册路由和中间件、启动 HTTP 服务 |
|
||||
| `database/db.go` | `InitDB()` 打开 SQLite 连接、建表、插入默认管理员和默认搜索引擎配置 |
|
||||
| `models/card.go` | `Card` struct;`GetAllCards()`/`GetEnabledCards()`/`CreateCard()`/`UpdateCard()`/`DeleteCard()`/`ToggleCard()`/`MoveCardUp()`/`MoveCardDown()` |
|
||||
| `models/setting.go` | `Setting` struct;`GetSetting(key)`/`SetSetting(key, value)`;预定义搜索引擎常量 |
|
||||
| `models/admin.go` | `Admin` struct;`GetAdminByUsername()`/`CreateAdmin()`/`VerifyPassword()`/`ChangePassword()` |
|
||||
| `handlers/home.go` | `HomeHandler`:查询启用卡片 + 当前搜索引擎配置,渲染主页 |
|
||||
| `handlers/admin.go` | `LoginGet`/`LoginPost`/`Logout`/`AdminIndex`:登录表单、认证、登出、后台首页 |
|
||||
| `handlers/cards.go` | `CardsList`/`CardCreate`/`CardEdit`/`CardDelete`/`CardToggle`/`CardMoveUp`/`CardMoveDown` |
|
||||
| `handlers/settings.go` | `SettingsGet`/`SettingsPost`:展示/保存搜索引擎配置 |
|
||||
| `middleware/auth.go` | `AuthRequired()`:检查 Cookie 中的 session_id,未登录则 302 到 /admin/login |
|
||||
| `session/session.go` | `SessionStore`:内存 map + RWMutex;`Create()/Get()/Delete()` |
|
||||
| `templates/layout.html` | HTML 骨架:`{{define "layout"}}...{{template "content" .}}{{end}}` |
|
||||
| `templates/home.html` | 搜索框 + 卡片网格 |
|
||||
| `templates/admin/*.html` | 后台各页面 |
|
||||
| `static/style.css` | 全局 CSS:卡片网格、搜索框、表单、后台布局 |
|
||||
|
||||
---
|
||||
|
||||
### 3. 数据结构与接口
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
direction LR
|
||||
|
||||
class Card {
|
||||
+int ID
|
||||
+string Icon
|
||||
+string Title
|
||||
+string Subtitle
|
||||
+string URL
|
||||
+int Sort
|
||||
+bool Enabled
|
||||
+time Time CreatedAt
|
||||
}
|
||||
|
||||
class Setting {
|
||||
+string Key
|
||||
+string Value
|
||||
}
|
||||
|
||||
class Admin {
|
||||
+int ID
|
||||
+string Username
|
||||
+string Password
|
||||
}
|
||||
|
||||
class SessionData {
|
||||
+int AdminID
|
||||
+string Username
|
||||
+time Time CreatedAt
|
||||
}
|
||||
|
||||
class SessionStore {
|
||||
-map~string-SessionData~ store
|
||||
-sync.RWMutex mu
|
||||
+Create(adminID int, username string) string
|
||||
+Get(sessionID string) *SessionData
|
||||
+Delete(sessionID string)
|
||||
}
|
||||
|
||||
class CardModel {
|
||||
+GetAllCards() []Card
|
||||
+GetEnabledCards() []Card
|
||||
+GetCardByID(id int) *Card
|
||||
+CreateCard(card *Card) error
|
||||
+UpdateCard(card *Card) error
|
||||
+DeleteCard(id int) error
|
||||
+ToggleCard(id int) error
|
||||
+MoveCardUp(id int) error
|
||||
+MoveCardDown(id int) error
|
||||
}
|
||||
|
||||
class SettingModel {
|
||||
+GetSetting(key string) string
|
||||
+SetSetting(key string, value string) error
|
||||
+GetSearchEngines() map~string-string
|
||||
+SetSearchEngine(name string, url string) error
|
||||
}
|
||||
|
||||
class AdminModel {
|
||||
+GetAdminByUsername(username string) *Admin
|
||||
+CreateAdmin(username string, password string) error
|
||||
+VerifyPassword(username string, password string) bool
|
||||
+ChangePassword(adminID int, newPassword string) error
|
||||
}
|
||||
|
||||
class HandlerHome {
|
||||
+HomeHandler(c *gin.Context)
|
||||
}
|
||||
|
||||
class HandlerAdmin {
|
||||
+LoginGet(c *gin.Context)
|
||||
+LoginPost(c *gin.Context)
|
||||
+Logout(c *gin.Context)
|
||||
+AdminIndex(c *gin.Context)
|
||||
}
|
||||
|
||||
class HandlerCards {
|
||||
+CardsList(c *gin.Context)
|
||||
+CardCreateGet(c *gin.Context)
|
||||
+CardCreatePost(c *gin.Context)
|
||||
+CardEditGet(c *gin.Context)
|
||||
+CardEditPost(c *gin.Context)
|
||||
+CardDelete(c *gin.Context)
|
||||
+CardToggle(c *gin.Context)
|
||||
+CardMoveUp(c *gin.Context)
|
||||
+CardMoveDown(c *gin.Context)
|
||||
}
|
||||
|
||||
class HandlerSettings {
|
||||
+SettingsGet(c *gin.Context)
|
||||
+SettingsPost(c *gin.Context)
|
||||
}
|
||||
|
||||
class AuthMiddleware {
|
||||
+AuthRequired() gin.HandlerFunc
|
||||
}
|
||||
|
||||
CardModel --> Card : returns
|
||||
SettingModel --> Setting : returns
|
||||
AdminModel --> Admin : returns
|
||||
SessionStore --> SessionData : stores
|
||||
HandlerHome --> CardModel : uses
|
||||
HandlerHome --> SettingModel : uses
|
||||
HandlerAdmin --> AdminModel : uses
|
||||
HandlerAdmin --> SessionStore : uses
|
||||
HandlerCards --> CardModel : uses
|
||||
HandlerSettings --> SettingModel : uses
|
||||
AuthMiddleware --> SessionStore : uses
|
||||
```
|
||||
|
||||
#### 路由表
|
||||
|
||||
| 方法 | 路径 | Handler | 中间件 | 说明 |
|
||||
|------|------|---------|--------|------|
|
||||
| GET | `/` | HomeHandler | — | 主页 |
|
||||
| GET | `/admin/login` | LoginGet | — | 登录页 |
|
||||
| POST | `/admin/login` | LoginPost | — | 登录提交 |
|
||||
| POST | `/admin/logout` | Logout | AuthRequired | 登出 |
|
||||
| GET | `/admin` | AdminIndex | AuthRequired | 后台首页 |
|
||||
| GET | `/admin/cards` | CardsList | AuthRequired | 卡片列表 |
|
||||
| GET | `/admin/cards/new` | CardCreateGet | AuthRequired | 新增卡片表单 |
|
||||
| POST | `/admin/cards` | CardCreatePost | AuthRequired | 新增卡片提交 |
|
||||
| GET | `/admin/cards/:id/edit` | CardEditGet | AuthRequired | 编辑卡片表单 |
|
||||
| POST | `/admin/cards/:id` | CardEditPost | AuthRequired | 编辑卡片提交 |
|
||||
| POST | `/admin/cards/:id/delete` | CardDelete | AuthRequired | 删除卡片 |
|
||||
| POST | `/admin/cards/:id/toggle` | CardToggle | AuthRequired | 启用/禁用卡片 |
|
||||
| POST | `/admin/cards/:id/move-up` | CardMoveUp | AuthRequired | 卡片上移 |
|
||||
| POST | `/admin/cards/:id/move-down` | CardMoveDown | AuthRequired | 卡片下移 |
|
||||
| GET | `/admin/settings` | SettingsGet | AuthRequired | 搜索引擎设置页 |
|
||||
| POST | `/admin/settings` | SettingsPost | AuthRequired | 保存搜索引擎设置 |
|
||||
|
||||
---
|
||||
|
||||
### 4. 程序调用流程
|
||||
|
||||
#### 4.1 主页加载
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户浏览器
|
||||
participant G as Gin Router
|
||||
participant H as HomeHandler
|
||||
participant CM as CardModel
|
||||
participant SM as SettingModel
|
||||
participant T as html/template
|
||||
|
||||
U->>G: GET /
|
||||
G->>H: HomeHandler(c)
|
||||
H->>CM: GetEnabledCards()
|
||||
CM->>CM: SELECT * FROM cards WHERE enabled=1 ORDER BY sort
|
||||
CM-->>H: []Card
|
||||
H->>SM: GetSetting("search_engine")
|
||||
SM->>SM: SELECT value FROM settings WHERE key='search_engine'
|
||||
SM-->>H: search URL template
|
||||
H->>T: ExecuteTemplate("home.html", data)
|
||||
T-->>U: HTML (搜索框 + 卡片网格)
|
||||
```
|
||||
|
||||
#### 4.2 搜索跳转
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户浏览器
|
||||
participant JS as 前端 JavaScript
|
||||
participant SE as 搜索引擎
|
||||
|
||||
U->>JS: 输入关键词,按回车/点击搜索
|
||||
JS->>JS: 读取隐藏域 search_url_template
|
||||
JS->>JS: fmt.Sprintf(template, query)
|
||||
JS->>U: window.location.href = 拼接后的URL
|
||||
U->>SE: HTTP 请求到搜索引擎
|
||||
```
|
||||
|
||||
> 注意:搜索跳转纯前端 JS 完成,不经过后端路由。
|
||||
|
||||
#### 4.3 后台登录
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户浏览器
|
||||
participant G as Gin Router
|
||||
participant H as HandlerAdmin
|
||||
participant AM as AdminModel
|
||||
participant SS as SessionStore
|
||||
|
||||
U->>G: GET /admin/login
|
||||
G->>H: LoginGet(c)
|
||||
H-->>U: 登录页 HTML
|
||||
|
||||
U->>G: POST /admin/login {username, password}
|
||||
G->>H: LoginPost(c)
|
||||
H->>AM: VerifyPassword(username, password)
|
||||
AM->>AM: bcrypt.CompareHashAndPassword()
|
||||
AM-->>H: true/false
|
||||
|
||||
alt 验证成功
|
||||
H->>SS: Create(adminID, username)
|
||||
SS-->>H: sessionID
|
||||
H->>H: c.SetCookie("session_id", sessionID, 86400, "/", "", false, true)
|
||||
H-->>U: 302 Redirect → /admin
|
||||
else 验证失败
|
||||
H-->>U: 200 + 登录页(错误提示)
|
||||
end
|
||||
```
|
||||
|
||||
#### 4.4 卡片增删改查
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 管理员浏览器
|
||||
participant G as Gin Router
|
||||
participant MW as AuthMiddleware
|
||||
participant H as HandlerCards
|
||||
participant CM as CardModel
|
||||
|
||||
Note over U,CM: 卡片列表
|
||||
U->>G: GET /admin/cards
|
||||
G->>MW: AuthRequired()
|
||||
MW->>MW: 检查 Cookie session_id
|
||||
alt 未登录
|
||||
MW-->>U: 302 → /admin/login
|
||||
else 已登录
|
||||
MW->>H: CardsList(c)
|
||||
H->>CM: GetAllCards()
|
||||
CM-->>H: []Card
|
||||
H-->>U: 卡片列表 HTML
|
||||
end
|
||||
|
||||
Note over U,CM: 新增卡片
|
||||
U->>G: GET /admin/cards/new
|
||||
G->>H: CardCreateGet(c)
|
||||
H-->>U: 空表单 HTML
|
||||
|
||||
U->>G: POST /admin/cards {icon, title, subtitle, url}
|
||||
G->>H: CardCreatePost(c)
|
||||
H->>CM: CreateCard(&Card{...})
|
||||
CM->>CM: INSERT INTO cards(...)
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
|
||||
Note over U,CM: 编辑卡片
|
||||
U->>G: GET /admin/cards/:id/edit
|
||||
G->>H: CardEditGet(c)
|
||||
H->>CM: GetCardByID(id)
|
||||
CM-->>H: *Card
|
||||
H-->>U: 预填表单 HTML
|
||||
|
||||
U->>G: POST /admin/cards/:id {icon, title, subtitle, url}
|
||||
G->>H: CardEditPost(c)
|
||||
H->>CM: UpdateCard(&Card{...})
|
||||
CM->>CM: UPDATE cards SET ... WHERE id=?
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
|
||||
Note over U,CM: 删除卡片
|
||||
U->>G: POST /admin/cards/:id/delete
|
||||
G->>H: CardDelete(c)
|
||||
H->>CM: DeleteCard(id)
|
||||
CM->>CM: DELETE FROM cards WHERE id=?
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
```
|
||||
|
||||
#### 4.5 卡片排序(上移/下移)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 管理员浏览器
|
||||
participant G as Gin Router
|
||||
participant H as HandlerCards
|
||||
participant CM as CardModel
|
||||
|
||||
U->>G: POST /admin/cards/:id/move-up
|
||||
G->>H: CardMoveUp(c)
|
||||
H->>CM: MoveCardUp(id)
|
||||
CM->>CM: 获取当前卡片 sort 值
|
||||
CM->>CM: 获取 sort 值仅次于当前卡片的上一张卡片
|
||||
CM->>CM: 交换两者 sort 值 (UPDATE)
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
|
||||
Note over U,CM: 下移同理,方向相反
|
||||
```
|
||||
|
||||
#### 4.6 卡片启用/禁用
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 管理员浏览器
|
||||
participant G as Gin Router
|
||||
participant H as HandlerCards
|
||||
participant CM as CardModel
|
||||
|
||||
U->>G: POST /admin/cards/:id/toggle
|
||||
G->>H: CardToggle(c)
|
||||
H->>CM: ToggleCard(id)
|
||||
CM->>CM: UPDATE cards SET enabled = CASE WHEN enabled=1 THEN 0 ELSE 1 END WHERE id=?
|
||||
CM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/cards
|
||||
```
|
||||
|
||||
#### 4.7 搜索引擎配置
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 管理员浏览器
|
||||
participant G as Gin Router
|
||||
participant H as HandlerSettings
|
||||
participant SM as SettingModel
|
||||
|
||||
U->>G: GET /admin/settings
|
||||
G->>H: SettingsGet(c)
|
||||
H->>SM: GetSetting("search_engine")
|
||||
SM-->>H: 当前搜索引擎 URL 模板
|
||||
H-->>U: 设置页 HTML(预设选项 + 自定义输入框)
|
||||
|
||||
U->>G: POST /admin/settings {engine: "google" | "bing" | "baidu" | "custom", custom_url: "..."}
|
||||
G->>H: SettingsPost(c)
|
||||
H->>H: 根据选项确定 URL 模板
|
||||
alt 预设引擎
|
||||
H->>H: 映射到固定 URL 模板
|
||||
else 自定义
|
||||
H->>H: 使用 custom_url,验证含 %s
|
||||
end
|
||||
H->>SM: SetSetting("search_engine", urlTemplate)
|
||||
SM->>SM: INSERT OR REPLACE INTO settings(key, value) VALUES(...)
|
||||
SM-->>H: nil
|
||||
H-->>U: 302 Redirect → /admin/settings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. 不确定事项与假设
|
||||
|
||||
| 编号 | 问题 | 假设/默认决策 |
|
||||
|------|------|--------------|
|
||||
| UNC-1 | 管理员是否支持多账号? | PRD 已定:初始 admin/admin123 硬编码,暂不考虑多账号管理 UI,但数据模型支持 |
|
||||
| UNC-2 | 搜索引擎配置是否需要多个? | 当前仅支持配置一个默认搜索引擎,settings 表 key=`search_engine` |
|
||||
| UNC-3 | 卡片图标是否需要文件上传? | PRD 已定:仅支持 emoji 文字和 favicon URL,通过文本输入框填写 |
|
||||
| UNC-4 | CSRF 防护 | 简易项目暂不实现 CSRF Token(所有写操作需登录 + Cookie HttpOnly) |
|
||||
| UNC-5 | HTTPS | 不在应用层处理,由部署环境(反向代理)负责 |
|
||||
| UNC-6 | 端口配置 | 默认监听 `:8080`,可通过环境变量 `PORT` 覆盖 |
|
||||
|
||||
---
|
||||
|
||||
## Part B: 任务分解
|
||||
|
||||
---
|
||||
|
||||
### 6. 依赖包列表
|
||||
|
||||
```
|
||||
github.com/gin-gonic/gin@v1.10.0 # HTTP 框架
|
||||
modernc.org/sqlite@latest # 纯 Go SQLite 驱动
|
||||
golang.org/x/crypto@latest # bcrypt 密码哈希
|
||||
```
|
||||
|
||||
> 仅 3 个直接依赖,保持极简。
|
||||
|
||||
---
|
||||
|
||||
### 7. 任务列表
|
||||
|
||||
#### T01: 项目基础设施
|
||||
|
||||
**说明**:创建项目骨架,包括模块初始化、数据库初始化、会话管理、全局布局模板、静态文件服务和程序入口。
|
||||
|
||||
**源文件**:
|
||||
- `go.mod`, `go.sum`
|
||||
- `main.go`
|
||||
- `database/db.go`
|
||||
- `session/session.go`
|
||||
- `middleware/auth.go`
|
||||
- `templates/layout.html`
|
||||
- `static/style.css`(基础框架样式:布局、导航、表单通用样式)
|
||||
|
||||
**具体内容**:
|
||||
1. `go mod init` + 添加依赖
|
||||
2. `database/db.go`:`InitDB()` — 打开 SQLite 文件、建表(cards/settings/admins)、插入默认管理员(admin/admin123 bcrypt hash)、插入默认搜索引擎(Google)
|
||||
3. `session/session.go`:`SessionStore` 结构体 + `Create/Get/Delete` 方法
|
||||
4. `middleware/auth.go`:`AuthRequired()` 中间件,从 Cookie 读取 session_id,校验失败 302 到 `/admin/login`
|
||||
5. `templates/layout.html`:HTML 骨架(`{{define "layout"}}` 含 head/nav/footer,`{{template "content" .}}` 插槽)
|
||||
6. `static/style.css`:CSS 变量定义、reset、布局容器、导航栏、表单基础样式
|
||||
7. `main.go`:初始化 DB、创建 SessionStore、注册所有路由(此阶段 handler 只返回空字符串占位)、启动服务
|
||||
|
||||
**依赖**:无
|
||||
|
||||
**优先级**:P0
|
||||
|
||||
---
|
||||
|
||||
#### T02: 数据模型层
|
||||
|
||||
**说明**:实现所有数据模型的 CRUD 操作,与 SQLite 交互。
|
||||
|
||||
**源文件**:
|
||||
- `models/card.go`
|
||||
- `models/setting.go`
|
||||
- `models/admin.go`
|
||||
|
||||
**具体内容**:
|
||||
1. `models/card.go`:
|
||||
- `Card` struct(映射 cards 表)
|
||||
- `GetAllCards()` — 查询全部卡片(后台用),按 sort ASC
|
||||
- `GetEnabledCards()` — 仅查询启用的卡片(主页用),按 sort ASC
|
||||
- `GetCardByID(id int)` — 单卡片查询
|
||||
- `CreateCard(card *Card)` — INSERT,sort 自动取 MAX(sort)+1
|
||||
- `UpdateCard(card *Card)` — UPDATE by ID
|
||||
- `DeleteCard(id int)` — DELETE by ID
|
||||
- `ToggleCard(id int)` — 切换 enabled 字段
|
||||
- `MoveCardUp(id int)` — 交换 sort 值上移
|
||||
- `MoveCardDown(id int)` — 交换 sort 值下移
|
||||
2. `models/setting.go`:
|
||||
- `Setting` struct
|
||||
- 预定义搜索引擎常量:`SearchEngineGoogle = "https://www.google.com/search?q=%s"` 等
|
||||
- `GetSetting(key string) (string, error)`
|
||||
- `SetSetting(key, value string) error`
|
||||
3. `models/admin.go`:
|
||||
- `Admin` struct
|
||||
- `GetAdminByUsername(username string) (*Admin, error)`
|
||||
- `CreateAdmin(username, hashedPassword string) error`
|
||||
- `VerifyPassword(username, password string) (bool, *Admin, error)` — bcrypt 校验
|
||||
- `ChangePassword(adminID int, newHashedPassword string) error`
|
||||
|
||||
**依赖**:T01(需要 `database/db.go` 中初始化的 `*sql.DB`)
|
||||
|
||||
**优先级**:P0
|
||||
|
||||
---
|
||||
|
||||
#### T03: 后台管理 — 登录 + 卡片管理
|
||||
|
||||
**说明**:实现后台核心功能:管理员登录/登出、卡片 CRUD、启用/禁用、排序。
|
||||
|
||||
**源文件**:
|
||||
- `handlers/admin.go`
|
||||
- `handlers/cards.go`
|
||||
- `templates/admin/login.html`
|
||||
- `templates/admin/index.html`
|
||||
- `templates/admin/cards.html`
|
||||
- `templates/admin/card_form.html`
|
||||
|
||||
**具体内容**:
|
||||
1. `handlers/admin.go`:
|
||||
- `LoginGet` — 渲染登录页
|
||||
- `LoginPost` — 表单验证 → `AdminModel.VerifyPassword()` → 创建 Session → 写 Cookie → 302 到 `/admin`
|
||||
- `Logout` — 删除 Session → 清 Cookie → 302 到 `/admin/login`
|
||||
- `AdminIndex` — 渲染后台首页
|
||||
2. `handlers/cards.go`:
|
||||
- `CardsList` — 调用 `GetAllCards()`,渲染列表页
|
||||
- `CardCreateGet` / `CardCreatePost` — 新增卡片
|
||||
- `CardEditGet` / `CardEditPost` — 编辑卡片
|
||||
- `CardDelete` — 删除卡片
|
||||
- `CardToggle` — 切换启用/禁用
|
||||
- `CardMoveUp` / `CardMoveDown` — 排序
|
||||
3. 模板文件:登录页、后台首页、卡片列表(含操作按钮)、卡片表单(新增/编辑共用)
|
||||
|
||||
**依赖**:T01(路由和中间件)、T02(数据模型)
|
||||
|
||||
**优先级**:P0
|
||||
|
||||
---
|
||||
|
||||
#### T04: 主页 + 搜索引擎配置
|
||||
|
||||
**说明**:实现面向用户的主页(搜索框 + 导航卡片)和后台搜索引擎配置。
|
||||
|
||||
**源文件**:
|
||||
- `handlers/home.go`
|
||||
- `handlers/settings.go`
|
||||
- `templates/home.html`
|
||||
- `templates/admin/settings.html`
|
||||
|
||||
**具体内容**:
|
||||
1. `handlers/home.go`:
|
||||
- `HomeHandler` — 获取启用卡片 + 当前搜索引擎 URL 模板,渲染主页
|
||||
- 搜索框前端 JS:读取隐藏域中的 URL 模板,拼接 `%s` 占位符,`window.location.href` 跳转
|
||||
- 搜索框自动聚焦(`autofocus` 属性)
|
||||
2. `handlers/settings.go`:
|
||||
- `SettingsGet` — 读取当前配置,渲染设置页
|
||||
- `SettingsPost` — 接收引擎选择(预设/自定义),自定义 URL 需包含 `%s`,保存到 settings 表
|
||||
3. `templates/home.html`:搜索框(大尺寸居中)+ 卡片网格(图标/标题/副标题,可点击跳转)
|
||||
4. `templates/admin/settings.html`:预设引擎单选 + 自定义 URL 输入
|
||||
|
||||
**依赖**:T01、T02
|
||||
|
||||
**优先级**:P0
|
||||
|
||||
---
|
||||
|
||||
#### T05: 样式完善 + 集成调试
|
||||
|
||||
**说明**:完善所有页面 CSS 样式,端到端集成测试,修复问题。
|
||||
|
||||
**源文件**:
|
||||
- `static/style.css`(补充完善所有页面样式)
|
||||
- `templates/layout.html`(可能微调)
|
||||
- `main.go`(最终路由确认)
|
||||
|
||||
**具体内容**:
|
||||
1. 完善主页样式:卡片网格(CSS Grid/Flexbox)、hover 效果、搜索框美观
|
||||
2. 完善后台样式:表单对齐、按钮样式、卡片操作按钮布局
|
||||
3. 响应式基础:卡片网格自适应列数
|
||||
4. 端到端手动验证:
|
||||
- 首次启动 → 默认管理员 → 登录成功
|
||||
- 新增/编辑/删除/排序卡片 → 主页正确展示
|
||||
- 切换搜索引擎 → 主页搜索跳转正确
|
||||
- 未登录访问后台 → 重定向到登录页
|
||||
5. 修复发现的问题
|
||||
|
||||
**依赖**:T03、T04
|
||||
|
||||
**优先级**:P1
|
||||
|
||||
---
|
||||
|
||||
### 8. 跨文件共享约定
|
||||
|
||||
```
|
||||
- 全局 *sql.DB 实例:通过 database 包导出的 DB 变量访问,models 包直接引用 database.DB
|
||||
- Session Cookie 名称:"session_id",HttpOnly=true,SameSite=Lax,MaxAge=86400(24小时)
|
||||
- Session Store 实例:main.go 中创建,通过闭包注入到 middleware 和 handlers
|
||||
- 模板数据传递:所有 handler 通过 gin.H 传递,模板中统一使用 .Cards / .Card / .SearchEngine / .Error / .Message
|
||||
- 模板继承:所有页面通过 {{template "layout" .}} 包裹,子页面 {{define "content"}}...{{end}}
|
||||
- 错误处理:handler 层 c.HTML(500, ...) 或 302 重定向,不向用户暴露内部错误
|
||||
- 表单提交:所有写操作使用 POST + 302 重定向(PRG 模式),防止重复提交
|
||||
- URL 模板占位符:%s(Go fmt.Sprintf 兼容)
|
||||
- 静态文件路径:/static/ → ./static/(gin.Static)
|
||||
- SQLite 文件路径:./data/portal.db(自动创建 data 目录)
|
||||
- 卡片 sort 值:数字越小越靠前,新建卡片 sort=MAX(sort)+1
|
||||
- 搜索引擎配置 key:"search_engine",value 为完整 URL 模板
|
||||
- 管理员默认账号:admin / admin123(bcrypt hash 存储于 DB)
|
||||
- 所有日期字段使用 SQLite CURRENT_TIMESTAMP(RFC 3339 格式)
|
||||
- 后台路由统一前缀:/admin/
|
||||
- 模板文件路径:相对于项目根目录的 templates/ 目录
|
||||
- CSS 类命名:BEM 风格简化版,如 .card-grid / .card-item / .card-item__icon / .search-box
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 9. 任务依赖图
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
T01[T01: 项目基础设施<br/>go.mod, main.go, db, session,<br/>middleware, layout, style.css]
|
||||
T02[T02: 数据模型层<br/>models/card.go, setting.go, admin.go]
|
||||
T03[T03: 后台管理<br/>handlers/admin.go, cards.go,<br/>templates/admin/*]
|
||||
T04[T04: 主页 + 搜索引擎配置<br/>handlers/home.go, settings.go,<br/>templates/home.html, admin/settings.html]
|
||||
T05[T05: 样式完善 + 集成调试<br/>style.css 补充, 全流程验证]
|
||||
|
||||
T01 --> T02
|
||||
T01 --> T03
|
||||
T01 --> T04
|
||||
T02 --> T03
|
||||
T02 --> T04
|
||||
T03 --> T05
|
||||
T04 --> T05
|
||||
```
|
||||
|
||||
> T03 和 T04 可并行开发(均依赖 T01 + T02),最终汇聚到 T05 集成。
|
||||
Reference in New Issue
Block a user