feat: init project

This commit is contained in:
yanghao05
2025-08-05 17:26:59 +08:00
parent c5d621ad03
commit e034a2e54e
30 changed files with 5159 additions and 0 deletions

470
requirements.md Normal file
View File

@@ -0,0 +1,470 @@
# 数据库动态渲染系统 - 完整需求文档
## 项目概述
构建一个可配置的数据渲染应用,通过读取 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+