Files
database_render/requirements.md
2025-08-05 17:26:59 +08:00

13 KiB
Raw Blame History

数据库动态渲染系统 - 完整需求文档

项目概述

构建一个可配置的数据渲染应用,通过读取 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 配置文件结构

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 获取表列表

GET /api/tables

响应:

{
  "tables": ["技术文章", "系统日志"]
}

3.2 获取分页数据

GET /api/data/{table_alias}?page=1&per_page=20&search=keyword&sort=created_at&order=desc

响应:

{
  "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 通用列表模板

<!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 模板辅助函数

// 模板函数注册
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

构建与部署

开发环境

# 安装依赖
go mod tidy

# 运行开发服务器
make dev

# 运行测试
make test

生产部署

# 构建二进制
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+