471 lines
13 KiB
Markdown
471 lines
13 KiB
Markdown
# 数据库动态渲染系统 - 完整需求文档
|
||
|
||
## 项目概述
|
||
|
||
构建一个可配置的数据渲染应用,通过读取 YAML 配置文件连接到指定数据库,根据配置查询不同数据表的内容,并按照预设的格式和模板进行处理,最终通过通用的列表页面向前端用户分页展示数据。
|
||
|
||
## 技术栈与框架选型
|
||
|
||
### 后端技术栈
|
||
|
||
- **HTTP 框架**: `github.com/gofiber/fiber/v3` - 高性能、零内存分配的 Go Web 框架
|
||
- **数据库 ORM**: `gorm.io/gorm` - 强大的数据库 ORM,支持多种数据库
|
||
- **日志系统**: `log/slog` - Go 官方结构化日志库
|
||
- **配置管理**: `github.com/spf13/viper` - 支持动态监听配置文件修改
|
||
- **依赖注入**: `github.com/google/wire` - Google 的依赖注入工具
|
||
|
||
### 数据库支持
|
||
|
||
- **SQLite**: `github.com/mattn/go-sqlite3` - 轻量级嵌入式数据库
|
||
- **MySQL**: `github.com/go-sql-driver/mysql` - MySQL 驱动
|
||
- **PostgreSQL**: `github.com/lib/pq` - PostgreSQL 驱动
|
||
|
||
### 前端技术栈
|
||
|
||
- **HTML 模板**: Go 标准库 `html/template` - 服务端模板渲染
|
||
- **CSS 框架**: TailwindCSS - 实用优先的 CSS 框架
|
||
- **JavaScript**: 原生 JavaScript (ES6+) - 轻量级交互
|
||
- **Markdown 渲染**: `marked.js` - Markdown 转 HTML
|
||
|
||
## 核心功能需求
|
||
|
||
### 1. 配置系统 (YAML)
|
||
|
||
#### 1.1 配置文件结构
|
||
|
||
```yaml
|
||
database:
|
||
type: sqlite
|
||
path: ./data/content.db
|
||
# MySQL配置示例
|
||
# type: mysql
|
||
# host: localhost
|
||
# port: 3306
|
||
# user: root
|
||
# password: password
|
||
# dbname: testdb
|
||
|
||
tables:
|
||
- name: "articles"
|
||
alias: "技术文章"
|
||
page_size: 15
|
||
columns:
|
||
- name: "id"
|
||
alias: "ID"
|
||
render_type: "raw"
|
||
sortable: true
|
||
width: "80px"
|
||
|
||
- name: "title"
|
||
alias: "标题"
|
||
render_type: "raw"
|
||
searchable: true
|
||
max_length: 50
|
||
|
||
- name: "content"
|
||
alias: "内容"
|
||
render_type: "markdown"
|
||
is_primary_content: true # 弹窗展示全文
|
||
show_in_list: false
|
||
|
||
- name: "category"
|
||
alias: "分类"
|
||
render_type: "category"
|
||
|
||
- name: "tags"
|
||
alias: "标签"
|
||
render_type: "tag"
|
||
values:
|
||
1: { label: "Go", color: "#00ADD8" }
|
||
2: { label: "JavaScript", color: "#f7df1e" }
|
||
|
||
- name: "created_at"
|
||
alias: "发布时间"
|
||
render_type: "time"
|
||
format: "2006-01-02 15:04:05"
|
||
sortable: true
|
||
|
||
filters:
|
||
- name: "category"
|
||
type: "select"
|
||
options: ["全部", "技术", "生活"]
|
||
|
||
- name: "logs"
|
||
alias: "系统日志"
|
||
page_size: 50
|
||
columns:
|
||
- name: "id"
|
||
alias: "ID"
|
||
render_type: "raw"
|
||
|
||
- name: "level"
|
||
alias: "级别"
|
||
render_type: "tag"
|
||
values:
|
||
1: { label: "INFO", color: "#52c41a" }
|
||
2: { label: "WARN", color: "#faad14" }
|
||
3: { label: "ERROR", color: "#f5222d" }
|
||
|
||
- name: "message"
|
||
alias: "日志信息"
|
||
render_type: "raw"
|
||
is_primary_content: true
|
||
|
||
- name: "timestamp"
|
||
alias: "时间"
|
||
render_type: "time"
|
||
format: "2006-01-02 15:04:05"
|
||
```
|
||
|
||
### 2. 字段渲染类型规范
|
||
|
||
| 类型 | 描述 | 配置参数 | 示例 |
|
||
| ---------- | ------------- | ------------------ | ------------------- |
|
||
| `raw` | 原始文本 | 无 | 普通文本显示 |
|
||
| `markdown` | Markdown 渲染 | `max_length` | 文章内容 |
|
||
| `time` | 时间格式化 | `format` | 2024-01-01 12:00:00 |
|
||
| `tag` | 标签样式 | `values`, `colors` | 状态标签 |
|
||
| `category` | 分类显示 | 无 | 分类名称 |
|
||
| `html` | HTML 内容 | `sanitize` | 富文本内容 |
|
||
|
||
### 3. 后端 API 接口
|
||
|
||
#### 3.1 获取表列表
|
||
|
||
```http
|
||
GET /api/tables
|
||
```
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"tables": ["技术文章", "系统日志"]
|
||
}
|
||
```
|
||
|
||
#### 3.2 获取分页数据
|
||
|
||
```http
|
||
GET /api/data/{table_alias}?page=1&per_page=20&search=keyword&sort=created_at&order=desc
|
||
```
|
||
|
||
响应:
|
||
|
||
```json
|
||
{
|
||
"data": [
|
||
{
|
||
"id": 1,
|
||
"title": "Go语言入门",
|
||
"category": "技术",
|
||
"tags": "Go",
|
||
"created_at": "2024-01-01 12:00:00"
|
||
}
|
||
],
|
||
"total": 100,
|
||
"page": 1,
|
||
"per_page": 20,
|
||
"pages": 5
|
||
}
|
||
```
|
||
|
||
### 4. Go 模板引擎规范
|
||
|
||
#### 4.1 通用列表模板
|
||
|
||
```html
|
||
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<title>{{.TableAlias}} - 数据管理</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||
<style>
|
||
.tag {
|
||
@apply inline-block px-2 py-1 text-xs font-medium rounded-full;
|
||
}
|
||
.modal {
|
||
@apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden;
|
||
}
|
||
.modal-content {
|
||
@apply bg-white rounded-lg max-w-4xl w-full mx-4 max-h-[80vh] overflow-y-auto;
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-50">
|
||
<div class="container mx-auto px-4 py-8">
|
||
<!-- 表选择器 -->
|
||
<div class="mb-6">
|
||
<select
|
||
id="tableSelector"
|
||
class="border rounded px-3 py-2"
|
||
onchange="changeTable(this.value)"
|
||
>
|
||
{{range .Tables}}
|
||
<option value="{{.}}" {{if eq . $.CurrentTable}}selected{{end}}>
|
||
{{.}}
|
||
</option>
|
||
{{end}}
|
||
</select>
|
||
</div>
|
||
|
||
<!-- 数据表格 -->
|
||
<div class="bg-white rounded-lg shadow overflow-hidden">
|
||
<table class="min-w-full divide-y divide-gray-200">
|
||
<thead class="bg-gray-50">
|
||
<tr>
|
||
{{range .Columns}} {{if .ShowInList}}
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||
>
|
||
{{.Alias}}
|
||
</th>
|
||
{{end}} {{end}}
|
||
<th
|
||
class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider"
|
||
>
|
||
操作
|
||
</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody class="bg-white divide-y divide-gray-200">
|
||
{{range .Data}}
|
||
<tr>
|
||
{{range $.Columns}} {{if .ShowInList}}
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||
{{template "render_field" dict "Value" (index $.Data (printf
|
||
"%s" .Name)) "Type" .RenderType "Column" .}}
|
||
</td>
|
||
{{end}} {{end}}
|
||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||
<button
|
||
onclick="showDetail('{{.ID}}')"
|
||
class="text-blue-600 hover:text-blue-900"
|
||
>
|
||
查看详情
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
{{end}}
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<!-- 分页 -->
|
||
<div class="mt-4 flex justify-between items-center">
|
||
<div class="text-sm text-gray-700">
|
||
共 {{.Total}} 条记录,第 {{.Page}} / {{.Pages}} 页
|
||
</div>
|
||
<div class="flex space-x-2">
|
||
{{if gt .Page 1}}
|
||
<a
|
||
href="?page={{sub .Page 1}}&per_page={{.PerPage}}"
|
||
class="px-3 py-1 border rounded text-sm"
|
||
>上一页</a
|
||
>
|
||
{{end}} {{if lt .Page .Pages}}
|
||
<a
|
||
href="?page={{add .Page 1}}&per_page={{.PerPage}}"
|
||
class="px-3 py-1 border rounded text-sm"
|
||
>下一页</a
|
||
>
|
||
{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 详情弹窗 -->
|
||
<div id="detailModal" class="modal">
|
||
<div class="modal-content">
|
||
<div class="p-6">
|
||
<h3 class="text-lg font-medium mb-4">详情信息</h3>
|
||
<div id="detailContent"></div>
|
||
<div class="mt-4 flex justify-end">
|
||
<button
|
||
onclick="closeModal()"
|
||
class="px-4 py-2 bg-gray-300 rounded"
|
||
>
|
||
关闭
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 渲染字段模板函数
|
||
function renderField(value, type, column) {
|
||
switch(type) {
|
||
case 'time':
|
||
return new Date(value).toLocaleString('zh-CN');
|
||
case 'tag':
|
||
const tagValue = column.values[value];
|
||
return `<span class="tag" style="background-color: ${tagValue.color}; color: white;">${tagValue.label}</span>`;
|
||
case 'markdown':
|
||
return marked.parse(value || '');
|
||
default:
|
||
return value || '';
|
||
}
|
||
}
|
||
|
||
// 显示详情
|
||
function showDetail(id) {
|
||
fetch(`/api/data/{{.CurrentTable}}/detail/${id}`)
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
let content = '';
|
||
{{range .Columns}}
|
||
{{if .IsPrimaryContent}}
|
||
content += `<div class="mb-4">
|
||
<label class="block text-sm font-medium text-gray-700">{{.Alias}}</label>
|
||
<div class="mt-1 text-sm text-gray-900">${renderField(data.{{.Name}}, '{{.RenderType}}', {{. | json}})}</div>
|
||
</div>`;
|
||
{{end}}
|
||
{{end}}
|
||
document.getElementById('detailContent').innerHTML = content;
|
||
document.getElementById('detailModal').classList.remove('hidden');
|
||
});
|
||
}
|
||
|
||
// 关闭弹窗
|
||
function closeModal() {
|
||
document.getElementById('detailModal').classList.add('hidden');
|
||
}
|
||
|
||
// 切换表格
|
||
function changeTable(tableAlias) {
|
||
window.location.href = `/?table=${tableAlias}&page=1`;
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|
||
```
|
||
|
||
#### 4.2 模板辅助函数
|
||
|
||
```go
|
||
// 模板函数注册
|
||
func templateFuncs() template.FuncMap {
|
||
return template.FuncMap{
|
||
"dict": func(values ...interface{}) map[string]interface{} {
|
||
dict := make(map[string]interface{})
|
||
for i := 0; i < len(values); i += 2 {
|
||
key := values[i].(string)
|
||
dict[key] = values[i+1]
|
||
}
|
||
return dict
|
||
},
|
||
"add": func(a, b int) int { return a + b },
|
||
"sub": func(a, b int) int { return a - b },
|
||
"json": func(v interface{}) string {
|
||
b, _ := json.Marshal(v)
|
||
return string(b)
|
||
},
|
||
"renderField": func(value interface{}, renderType string, column interface{}) template.HTML {
|
||
switch renderType {
|
||
case "time":
|
||
if t, ok := value.(time.Time); ok {
|
||
return template.HTML(t.Format("2006-01-02 15:04:05"))
|
||
}
|
||
return template.HTML(fmt.Sprintf("%v", value))
|
||
case "tag":
|
||
if columnMap, ok := column.(map[string]interface{}); ok {
|
||
if values, ok := columnMap["values"].(map[string]interface{}); ok {
|
||
if tag, ok := values[fmt.Sprintf("%v", value)].(map[string]interface{}); ok {
|
||
color := tag["color"].(string)
|
||
label := tag["label"].(string)
|
||
return template.HTML(fmt.Sprintf(
|
||
`<span class="tag" style="background-color: %s; color: white;">%s</span>`,
|
||
color, label,
|
||
))
|
||
}
|
||
}
|
||
}
|
||
return template.HTML(fmt.Sprintf("%v", value))
|
||
case "markdown":
|
||
// 服务端渲染Markdown
|
||
return template.HTML(fmt.Sprintf("%v", value))
|
||
default:
|
||
return template.HTML(fmt.Sprintf("%v", value))
|
||
}
|
||
},
|
||
}
|
||
}
|
||
```
|
||
|
||
## 项目结构
|
||
|
||
```
|
||
database-render/
|
||
├── cmd/
|
||
│ └── server/
|
||
│ └── main.go
|
||
├── internal/
|
||
│ ├── config/
|
||
│ │ └── config.go
|
||
│ ├── database/
|
||
│ │ └── connection.go
|
||
│ ├── handler/
|
||
│ │ └── data_handler.go
|
||
│ ├── model/
|
||
│ │ └── table_config.go
|
||
│ ├── repository/
|
||
│ │ └── data_repository.go
|
||
│ ├── service/
|
||
│ │ └── data_service.go
|
||
│ └── template/
|
||
│ └── renderer.go
|
||
├── web/
|
||
│ ├── static/
|
||
│ │ ├── css/
|
||
│ │ ├── js/
|
||
│ │ └── images/
|
||
│ └── templates/
|
||
│ └── list.html
|
||
├── config/
|
||
│ └── config.yaml
|
||
├── migrations/
|
||
├── scripts/
|
||
├── tests/
|
||
├── Makefile
|
||
├── Dockerfile
|
||
├── go.mod
|
||
└── README.md
|
||
```
|
||
|
||
## 构建与部署
|
||
|
||
### 开发环境
|
||
|
||
```bash
|
||
# 安装依赖
|
||
go mod tidy
|
||
|
||
# 运行开发服务器
|
||
make dev
|
||
|
||
# 运行测试
|
||
make test
|
||
```
|
||
|
||
### 生产部署
|
||
|
||
```bash
|
||
# 构建二进制
|
||
make build
|
||
|
||
# 构建Docker镜像
|
||
docker build -t database-render .
|
||
|
||
# 运行容器
|
||
docker run -p 8080:8080 -v ./config:/app/config database-render
|
||
```
|
||
|
||
## 环境要求
|
||
|
||
- **开发环境**: Go 1.21+, Node.js 18+, SQLite3
|
||
- **生产环境**: Linux/Windows/macOS, Docker, SQLite/MySQL 5.7+/PostgreSQL 12+
|